Files
deerflow-code/frontend-web/src/core/scheduled-tasks/markdown-docx.ts
T
董宏凯andCursor f109a95535 feat: 新增报告协作页,并强化 Deep Research 运行诊断与报告兜底
支持浏览器侧素材快照撰写、拒绝报告读时修复、Mermaid PNG 导出与 Word 导出加固。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-03 11:16:50 +08:00

87 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Unified server-side Markdown -> Word export.
*
* Every Word download surface calls the Gateway so the document receives the
* same formal OOXML styles and the server-bundled 方正小标宋简体 title font.
* The user's browser and desktop Word installation are never used for font
* detection or document generation.
*/
import { apiFetch } from "@/core/api/fetch-client";
import { getBackendBaseURL } from "@/core/config";
import { OptType, writeYLog } from "@/core/log/y-log";
import { replaceMermaidFencesForWordExport } from "@/core/utils/mermaid";
const DOCX_MEDIA_TYPE =
"application/vnd.openxmlformats-officedocument.wordprocessingml.document";
function sanitizeFilename(value: string): string {
const name = value
.replace(/\.(md|markdown|docx)$/i, "")
.replace(/[<>:"/\\|?*\x00-\x1f]+/g, "_")
.trim();
return name || "document";
}
async function responseError(response: Response): Promise<Error> {
const payload = await response
.json()
.catch(() => ({ detail: `生成 Word 失败(HTTP ${response.status})` }));
const detail = payload?.detail;
return new Error(typeof detail === "string" ? detail : "生成 Word 失败");
}
/** Ask the Gateway to build a DOCX with its embedded licensed font resource. */
export async function markdownToDocxBlob(
markdown: string,
title?: string,
): Promise<Blob> {
const source = markdown?.trim();
if (!source) throw new Error("文档内容为空");
const prepared = await replaceMermaidFencesForWordExport(source);
const endpoint = `${getBackendBaseURL()}/api/writing/export/docx`;
const response = await apiFetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: title?.trim() || undefined,
markdown: prepared.markdown,
embedded_images: prepared.embeddedImages,
}),
});
if (!response.ok) throw await responseError(response);
const blob = await response.blob();
if (blob.size === 0) throw new Error("服务器返回了空的 Word 文件");
if (blob.type && blob.type !== DOCX_MEDIA_TYPE) {
throw new Error("服务器返回的文件类型不是 Word 文档");
}
return blob;
}
/** Generate and immediately download a formal DOCX. */
export async function downloadMarkdownAsDocx(
markdown: string,
fileName: string,
title?: string,
): Promise<void> {
const blob = await markdownToDocxBlob(markdown, title);
writeYLog({
optType: OptType.export,
optDetail: "CM助手-Word导出",
url: "/api/writing/export/docx",
reqParam: { fileName, title },
});
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = `${sanitizeFilename(fileName)}.docx`;
anchor.rel = "noopener";
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
window.setTimeout(() => URL.revokeObjectURL(url), 30_000);
}