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"); const coverUploadBtn = $("coverUploadBtn"); 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, thumb_media_id: $("thumbMediaId") ? $("thumbMediaId").value.trim() : "", }); if (!data.ok) throw new Error(data.detail); setStatus("公众号草稿发布成功"); } catch (e) { setStatus(`公众号发布失败: ${e.message}`, true); } finally { setLoading(wechatBtn, false, "发布到公众号草稿箱", "发布中..."); } }); if (coverUploadBtn) { coverUploadBtn.addEventListener("click", async () => { const fileInput = $("coverFile"); const hint = $("coverHint"); const file = fileInput && fileInput.files && fileInput.files[0]; if (!file) { setStatus("请先选择封面图片再上传", true); return; } if (hint) hint.textContent = "正在上传封面..."; setLoading(coverUploadBtn, true, "上传封面并绑定", "上传中..."); try { const fd = new FormData(); fd.append("file", file); const res = await fetch("/api/wechat/cover/upload", { method: "POST", body: fd }); const data = await res.json(); if (!res.ok || !data.ok) throw new Error(data.detail || "封面上传失败"); const mid = data.data && data.data.thumb_media_id ? data.data.thumb_media_id : ""; if ($("thumbMediaId")) $("thumbMediaId").value = mid; if (hint) hint.textContent = `封面上传成功,已绑定 media_id:${mid}`; setStatus("封面上传成功,发布时将优先使用该封面。"); } catch (e) { if (hint) hint.textContent = "封面上传失败,请看状态提示。"; setStatus(`封面上传失败: ${e.message}`, true); } finally { setLoading(coverUploadBtn, 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, "");