193 lines
6.2 KiB
JavaScript
193 lines
6.2 KiB
JavaScript
const $ = (id) => document.getElementById(id);
|
||
|
||
function renderBodyPreview() {
|
||
const raw = ($("body") && $("body").value) || "";
|
||
const el = $("bodyPreview");
|
||
if (!el) return;
|
||
if (typeof marked !== "undefined" && marked.parse) {
|
||
el.innerHTML = marked.parse(raw, { breaks: true });
|
||
} else {
|
||
el.textContent = raw;
|
||
}
|
||
}
|
||
|
||
const statusEl = $("status");
|
||
const rewriteBtn = $("rewriteBtn");
|
||
const wechatBtn = $("wechatBtn");
|
||
const imBtn = $("imBtn");
|
||
|
||
function countText(v) {
|
||
return (v || "").trim().length;
|
||
}
|
||
|
||
function updateCounters() {
|
||
$("sourceCount").textContent = `${countText($("sourceText").value)} 字`;
|
||
$("summaryCount").textContent = `${countText($("summary").value)} 字`;
|
||
$("bodyCount").textContent = `${countText($("body").value)} 字`;
|
||
renderBodyPreview();
|
||
}
|
||
|
||
function setLoading(button, loading, idleText, loadingText) {
|
||
if (!button) return;
|
||
button.disabled = loading;
|
||
button.textContent = loading ? loadingText : idleText;
|
||
}
|
||
|
||
function setStatus(msg, danger = false) {
|
||
statusEl.style.color = danger ? "#b42318" : "#0f5f3d";
|
||
statusEl.textContent = msg;
|
||
}
|
||
|
||
async function postJSON(url, body) {
|
||
const res = await fetch(url, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(body),
|
||
});
|
||
const data = await res.json();
|
||
if (!res.ok) throw new Error(data.detail || "请求失败");
|
||
data._requestId = res.headers.get("X-Request-ID") || "";
|
||
return data;
|
||
}
|
||
|
||
function renderTrace(trace, headerRid) {
|
||
const wrap = $("traceWrap");
|
||
const pre = $("traceJson");
|
||
const badge = $("traceBadge");
|
||
if (!pre || !wrap) return;
|
||
|
||
if (!trace || Object.keys(trace).length === 0) {
|
||
pre.textContent = headerRid
|
||
? JSON.stringify({ request_id: headerRid, note: "响应中无 trace 字段" }, null, 2)
|
||
: "(尚无数据)完成一次「AI 改写」后,这里会显示请求 ID、耗时、质检与降级原因。";
|
||
if (badge) badge.textContent = "";
|
||
return;
|
||
}
|
||
|
||
const merged = { ...trace };
|
||
if (headerRid && !merged.request_id) merged.request_id = headerRid;
|
||
pre.textContent = JSON.stringify(merged, null, 2);
|
||
|
||
const mode = merged.mode || "";
|
||
if (badge) {
|
||
badge.textContent = mode === "ai" ? "AI" : mode === "fallback" ? "保底" : "";
|
||
badge.className = "trace-badge " + (mode === "ai" ? "is-ai" : mode === "fallback" ? "is-fallback" : "");
|
||
}
|
||
wrap.open = true;
|
||
}
|
||
|
||
$("rewriteBtn").addEventListener("click", async () => {
|
||
const sourceText = $("sourceText").value.trim();
|
||
if (sourceText.length < 20) {
|
||
setStatus("原始内容太短,至少 20 个字符", true);
|
||
return;
|
||
}
|
||
|
||
setStatus("AI 改写中...");
|
||
setLoading(rewriteBtn, true, "AI 改写并排版", "AI 改写中...");
|
||
try {
|
||
const data = await postJSON("/api/rewrite", {
|
||
source_text: sourceText,
|
||
title_hint: $("titleHint").value,
|
||
tone: $("tone").value,
|
||
audience: $("audience").value,
|
||
keep_points: $("keepPoints").value,
|
||
avoid_words: $("avoidWords").value,
|
||
});
|
||
$("title").value = data.title || "";
|
||
$("summary").value = data.summary || "";
|
||
$("body").value = data.body_markdown || "";
|
||
updateCounters();
|
||
renderTrace(data.trace, data._requestId);
|
||
const tr = data.trace || {};
|
||
const modelLine = tr.model ? `模型 ${tr.model}` : "";
|
||
if (data.mode === "fallback") {
|
||
const note = (data.quality_notes || [])[0] || "当前为保底改写稿";
|
||
setStatus(
|
||
`改写完成(保底模式,未使用或未通过千问长文):${note}${modelLine ? ` · ${modelLine}` : ""}`,
|
||
true
|
||
);
|
||
} else if (tr.quality_soft_accept) {
|
||
setStatus(
|
||
`改写完成(AI,质检提示):${(data.quality_notes || []).join(";") || "见 quality_notes"} · ${modelLine || "AI"}`
|
||
);
|
||
statusEl.style.color = "#9a3412";
|
||
} else {
|
||
setStatus(`改写完成(AI 洗稿)${modelLine ? ` · ${modelLine}` : ""}`);
|
||
}
|
||
} catch (e) {
|
||
setStatus(`改写失败: ${e.message}`, true);
|
||
} finally {
|
||
setLoading(rewriteBtn, false, "AI 改写并排版", "AI 改写中...");
|
||
}
|
||
});
|
||
|
||
$("wechatBtn").addEventListener("click", async () => {
|
||
setStatus("正在发布到公众号草稿箱...");
|
||
setLoading(wechatBtn, true, "发布到公众号草稿箱", "发布中...");
|
||
try {
|
||
const data = await postJSON("/api/publish/wechat", {
|
||
title: $("title").value,
|
||
summary: $("summary").value,
|
||
body_markdown: $("body").value,
|
||
});
|
||
if (!data.ok) throw new Error(data.detail);
|
||
setStatus("公众号草稿发布成功");
|
||
} catch (e) {
|
||
setStatus(`公众号发布失败: ${e.message}`, true);
|
||
} finally {
|
||
setLoading(wechatBtn, false, "发布到公众号草稿箱", "发布中...");
|
||
}
|
||
});
|
||
|
||
$("imBtn").addEventListener("click", async () => {
|
||
setStatus("正在发送到 IM...");
|
||
setLoading(imBtn, true, "发送到 IM", "发送中...");
|
||
try {
|
||
const data = await postJSON("/api/publish/im", {
|
||
title: $("title").value,
|
||
body_markdown: $("body").value,
|
||
});
|
||
if (!data.ok) throw new Error(data.detail);
|
||
setStatus("IM 发送成功");
|
||
} catch (e) {
|
||
setStatus(`IM 发送失败: ${e.message}`, true);
|
||
} finally {
|
||
setLoading(imBtn, false, "发送到 IM", "发送中...");
|
||
}
|
||
});
|
||
|
||
["sourceText", "summary", "body"].forEach((id) => {
|
||
$(id).addEventListener("input", updateCounters);
|
||
});
|
||
|
||
async function loadBackendConfig() {
|
||
const el = $("backendConfig");
|
||
if (!el) return;
|
||
try {
|
||
const res = await fetch("/api/config");
|
||
const c = await res.json();
|
||
if (!c.openai_configured) {
|
||
el.textContent =
|
||
"后端未配置 OPENAI_API_KEY:改写将使用本地保底稿,千问不会参与。请在 .env 中配置并重启容器。";
|
||
el.style.color = "#b42318";
|
||
return;
|
||
}
|
||
const name =
|
||
c.provider === "dashscope"
|
||
? "通义千问(DashScope 兼容接口)"
|
||
: "OpenAI 兼容接口";
|
||
const host = c.base_url_host ? ` · ${c.base_url_host}` : "";
|
||
const to = c.openai_timeout_sec != null ? ` · 单轮最长等待 ${c.openai_timeout_sec}s` : "";
|
||
el.textContent = `已接入:${c.openai_model} · ${name}${host}${to}`;
|
||
el.style.color = "";
|
||
} catch (e) {
|
||
el.textContent = "无法读取 /api/config(请确认服务已启动)";
|
||
el.style.color = "#b42318";
|
||
}
|
||
}
|
||
|
||
loadBackendConfig();
|
||
updateCounters();
|
||
renderTrace(null, "");
|