支持浏览器侧素材快照撰写、拒绝报告读时修复、Mermaid PNG 导出与 Word 导出加固。 Co-authored-by: Cursor <cursoragent@cursor.com>
87 lines
2.9 KiB
TypeScript
87 lines
2.9 KiB
TypeScript
/**
|
||
* 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);
|
||
}
|