1. Y-Log 上报 optDetail 前缀 B1-CM助手 → CM助手-全量(全量替换 44 个前端文件 + 文档) 2. 知识库入库 nodeName 改为只取 md5(标题).md(去掉时间戳),同一篇 md 每次入库一致,避免重复记录 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
113 lines
3.7 KiB
TypeScript
113 lines
3.7 KiB
TypeScript
import { apiFetch } from "@/core/api/fetch-client";
|
||
import { getBackendBaseURL } from "@/core/config";
|
||
import { OptType, writeYLog } from "@/core/log/y-log";
|
||
import { downloadMarkdownAsDocx } from "@/core/scheduled-tasks/markdown-docx";
|
||
|
||
function filenameFromContentDisposition(header: string | null): string | null {
|
||
if (!header) return null;
|
||
const star = /filename\*=(?:UTF-8'')?([^;]+)/i.exec(header);
|
||
if (star) {
|
||
const raw = star[1].replace(/["']/g, "").trim();
|
||
try {
|
||
return decodeURIComponent(raw);
|
||
} catch {
|
||
return raw;
|
||
}
|
||
}
|
||
const plain = /filename="?([^";\n]+)"?/i.exec(header);
|
||
return plain ? plain[1].trim() : null;
|
||
}
|
||
|
||
function sanitizeFallbackFilename(title: string | undefined): string {
|
||
const base = (title?.trim() || "document").replace(/[<>:"/\\|?*\x00-\x1f]+/g, "_");
|
||
return base.toLowerCase().endsWith(".docx") ? base : `${base}.docx`;
|
||
}
|
||
|
||
async function downloadBlobResponse(response: Response, fallbackFilename: string) {
|
||
if (!response.ok) {
|
||
const error = await response
|
||
.json()
|
||
.catch(() => ({ detail: "导出 Word 失败" }));
|
||
const detail = error.detail;
|
||
if (typeof detail === "string") throw new Error(detail);
|
||
throw new Error("导出 Word 失败");
|
||
}
|
||
|
||
const blob = await response.blob();
|
||
const filename =
|
||
filenameFromContentDisposition(response.headers.get("Content-Disposition")) ??
|
||
fallbackFilename;
|
||
const url = URL.createObjectURL(blob);
|
||
const anchor = document.createElement("a");
|
||
anchor.href = url;
|
||
anchor.download = filename;
|
||
anchor.rel = "noopener";
|
||
document.body.appendChild(anchor);
|
||
anchor.click();
|
||
anchor.remove();
|
||
window.setTimeout(() => URL.revokeObjectURL(url), 30_000);
|
||
}
|
||
|
||
/**
|
||
* 通用 Markdown → Word 导出(Gateway `writing` 路由,与 `/api/writing/rewrite` 同级)。
|
||
*
|
||
* POST /api/writing/export/docx
|
||
* Body: { title?: string, markdown: string }
|
||
*
|
||
* 返回 `application/vnd.openxmlformats-officedocument.wordprocessingml.document`,
|
||
* 文件名在 `Content-Disposition` 中。
|
||
*/
|
||
export async function downloadMarkdownDocxFromBackend(options: {
|
||
title?: string;
|
||
markdown: string;
|
||
}): Promise<void> {
|
||
const markdown = options.markdown.trim();
|
||
if (!markdown) {
|
||
throw new Error("文档内容为空");
|
||
}
|
||
|
||
const fallbackFilename = sanitizeFallbackFilename(options.title);
|
||
|
||
try {
|
||
const response = await apiFetch(`${getBackendBaseURL()}/api/writing/export/docx`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
title: options.title?.trim() || undefined,
|
||
markdown,
|
||
}),
|
||
});
|
||
|
||
if (response.status === 404) {
|
||
await downloadMarkdownAsDocx(markdown, options.title?.trim() || "document", options.title);
|
||
writeYLog({
|
||
optType: OptType.export,
|
||
optDetail: "CM助手-全量-导出-Word",
|
||
url: `${getBackendBaseURL()}/api/writing/export/docx`,
|
||
reqParam: { title: options.title?.trim() || undefined },
|
||
});
|
||
return;
|
||
}
|
||
|
||
await downloadBlobResponse(response, fallbackFilename);
|
||
writeYLog({
|
||
optType: OptType.export,
|
||
optDetail: "CM助手-全量-导出-Word",
|
||
url: `${getBackendBaseURL()}/api/writing/export/docx`,
|
||
reqParam: { title: options.title?.trim() || undefined },
|
||
});
|
||
} catch (err) {
|
||
if (err instanceof TypeError) {
|
||
await downloadMarkdownAsDocx(markdown, options.title?.trim() || "document", options.title);
|
||
writeYLog({
|
||
optType: OptType.export,
|
||
optDetail: "CM助手-全量-导出-Word",
|
||
url: `${getBackendBaseURL()}/api/writing/export/docx`,
|
||
reqParam: { title: options.title?.trim() || undefined },
|
||
});
|
||
return;
|
||
}
|
||
throw err;
|
||
}
|
||
}
|