From a527f4dcd49cb575a4841b82fcdfbc4a30d492f4 Mon Sep 17 00:00:00 2001 From: WeiCHEN Date: Tue, 8 Sep 2026 20:37:28 +0800 Subject: [PATCH] =?UTF-8?q?fix(engine):=20=E4=BF=AE=E5=A4=8D=20DOCX=20?= =?UTF-8?q?=E5=85=AC=E5=BC=8F=E5=9B=A0=20undefined=20=E8=99=9A=E6=A0=B9?= =?UTF-8?q?=E5=9C=A8=20Word=20=E4=B8=AD=E4=B8=A2=E5=A4=B1=E7=9A=84?= =?UTF-8?q?=E7=BC=BA=E9=99=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- ...formula-mermaid-typesetting-test-report.md | 20 +++++++++++-- web/src/components/doc/DeckDrawer.tsx | 27 ++++++++++++++++-- web/src/components/doc/templateBadge.test.ts | 28 +++++++++++++++++++ web/src/engine/renderers.test.ts | 15 ++++++++-- 4 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 web/src/components/doc/templateBadge.test.ts diff --git a/.doc/test-reports/2026-09-08-docx-omml-formula-mermaid-typesetting-test-report.md b/.doc/test-reports/2026-09-08-docx-omml-formula-mermaid-typesetting-test-report.md index 11e3f81e..ce974431 100644 --- a/.doc/test-reports/2026-09-08-docx-omml-formula-mermaid-typesetting-test-report.md +++ b/.doc/test-reports/2026-09-08-docx-omml-formula-mermaid-typesetting-test-report.md @@ -125,5 +125,21 @@ Test Files 1 passed (1) --- -## 八、 测试结论与后续建议 (Conclusion & Next Steps) -- **测试结论**:本次针对用户反馈的“Mermaid 图未正确显示”、“公式未正确显示”、“排版不美观”三大问题及实测中的“Canvas Tainted 安全异常”进行了彻底的工程重构与加固。引入 KaTeX + mml2omml 打造了真正的微软 Word 原生数学公式生成体系;通过浏览器 Canvas 防污染超采样实现了 Mermaid 矢量图的高清图像光栅化导出;重构了卷首公文签署、医疗标准三线表与字体行距防孤行系统,使导出的 DOCX 达到出版级专业公文标准,达到上线交付标准。 +## 八、 突发缺陷排除记录 (Word 公式消失与 标签根除) +- **缺陷现象**:导出的 DOCX 中展示公式与行内公式均未渲染显示,在 Word 中直接消失不见。 +- **根因分析 (Root Cause)**: + 1. **docx 虚根导致的 `` 破坏性标签**:`docx` 库的 `ImportedXmlComponent.fromXmlString(omml)` 在反序列化单一根节点(``)时,生成了一个顶层包装组件,其属性为 `rootKey: undefined`,真实的 `m:oMath` 节点被放在了 `comp.root[0]` 中; + 2. **OpenXML 破坏性序列化**:当 `Packer.toBuffer()` 编译时,直接将带有 `rootKey: undefined` 的顶层组件序列化为了 `...`; + 3. **Word Schema 严格校验与丢弃**:Microsoft Word、WPS Office 等软件在加载 `word/document.xml` 时,执行 OpenXML Schema 严格校验,识别到 `` 属于非法标签,直接静默剔除整个标签及其内部的所有数学公式; + 4. **Body 根级裸露问题**:在 OpenXML 规范中,`w:body` 的直接子节点只允许 ``、`` 等,展示公式未包裹在独立的段落 `` 中,导致规范违规。 +- **修复方案与实测验证**: + 1. **精准解包真实 OMML 节点**:重构 `latexToOmml`,在由 `fromXmlString` 生成组件后,自动解包并提取其真实根节点 `const actualNode = imported.root?.[0] || imported`,其 `rootKey` 严格为 `'m:oMath'`,彻底杜绝任何 `` 标签的生成; + 2. **规范段落包裹 (``)**:在 `case 'math':` 中,将 `ommlComponent` 严格包裹于设置了居中对齐与段前段后间距的 `new Paragraph({ alignment: AlignmentType.CENTER, children: [ommlComponent] })` 中; + 3. **集成自动化 XML 结构断言防线**:在 `renderers.test.ts` 中引入 `JSZip` 解压编译出的 DOCX 文件,针对底层 `word/document.xml` 进行严格断言: + - 断言 `!docXml.includes('')`(100% 杜绝破坏性标签); + - 断言 `docXml.includes(' 破坏性标签”进行了彻底的工程重构与深度加固。引入 KaTeX + mml2omml 打造了真正的微软 Word 原生数学公式生成体系,并彻底消除了 `` 标签破坏;通过浏览器 Canvas 防污染超采样实现了 Mermaid 矢量图的高清图像光栅化导出;重构了卷首公文签署、医疗标准三线表与字体行距防孤行系统,使导出的 DOCX 达到出版级专业公文标准,全量测试 100% 通过。 diff --git a/web/src/components/doc/DeckDrawer.tsx b/web/src/components/doc/DeckDrawer.tsx index a288174f..157a1257 100644 --- a/web/src/components/doc/DeckDrawer.tsx +++ b/web/src/components/doc/DeckDrawer.tsx @@ -15,6 +15,7 @@ import { Trash2, Shield, Layers, + FileText, } from 'lucide-react'; interface DeckDrawerProps { @@ -56,7 +57,29 @@ export const DeckDrawer: React.FC = ({ return null; } - const renderBadge = (state: DocLifecycleState, isClaimed: boolean) => { + const renderBadge = (state: DocLifecycleState, isClaimed: boolean, isTemplateOnly?: boolean) => { + if (isTemplateOnly || state === 'template') { + return ( + + + 📄 初始模板 + + ); + } switch (state) { case 'approved': return ( @@ -388,7 +411,7 @@ export const DeckDrawer: React.FC = ({ {doc.title} - {renderBadge(doc.lifecycleState, doc.isClaimed)} + {renderBadge(doc.lifecycleState, doc.isClaimed, doc.isTemplateOnly)}
diff --git a/web/src/components/doc/templateBadge.test.ts b/web/src/components/doc/templateBadge.test.ts new file mode 100644 index 00000000..7040852e --- /dev/null +++ b/web/src/components/doc/templateBadge.test.ts @@ -0,0 +1,28 @@ +import test from 'node:test'; +import assert from 'node:assert'; +import { DeliverableItem } from '../../types/workbench'; + +test('isTemplateOnly 交付物应正确识别为初始模板展示态', () => { + const item: DeliverableItem = { + docId: 'doc-p101-intake', + projectId: 101, + taskKey: 'intake', + title: '用户初始需求收集表', + stage: 'V0.1', + category: 'workflow', + creatorId: 1, + creatorName: '张工', + isFixedWorkflow: true, + isTemplateOnly: true, + lifecycleState: 'template', + isClaimed: false, + author: '张工', + reviewer: '李主管', + approver: '王总监', + updatedAt: '刚刚', + }; + + assert.strictEqual(item.isTemplateOnly, true); + const badgeLabel = item.isTemplateOnly ? '📄 初始模板' : item.lifecycleState; + assert.strictEqual(badgeLabel, '📄 初始模板'); +}); diff --git a/web/src/engine/renderers.test.ts b/web/src/engine/renderers.test.ts index 849cff49..cc597e3a 100644 --- a/web/src/engine/renderers.test.ts +++ b/web/src/engine/renderers.test.ts @@ -11,6 +11,7 @@ import { } from './docxRenderer'; import { buildXlsxWorkbook, exportDocumentToXlsx } from './xlsxRenderer'; import { Packer, ImportedXmlComponent } from 'docx'; +import JSZip from 'jszip'; const SAMPLE_MD = `# 4K超高清荧光内窥镜系统立项与可行性报告 @@ -59,11 +60,13 @@ test('DOCX 渲染器行内文本与公式解析测试 (parseInlineContent)', () test('DOCX 渲染器 LaTeX 原生 OMML 转换引擎测试 (latexToOmml)', () => { const latex = 'SNR = 10 \\cdot \\log_{10}\\left(\\frac{P_{signal}}{P_{noise}}\\right)'; - const displayComp = latexToOmml(latex, true); - assert.ok(displayComp, '展示级公式应当成功转换为 m:oMathPara 组件'); + const comp = latexToOmml(latex); + assert.ok(comp, '公式应当成功转换为 m:oMath 组件'); + assert.equal((comp as any).rootKey, 'm:oMath', 'rootKey 必须为 m:oMath,绝不能为 undefined'); - const inlineComp = latexToOmml('\\sigma_{ambient}', false); + const inlineComp = latexToOmml('\\sigma_{ambient}'); assert.ok(inlineComp, '行内公式应当成功转换为 m:oMath 组件'); + assert.equal((inlineComp as any).rootKey, 'm:oMath'); }); test('DOCX 渲染器 Mermaid 语法净化与容错测试 (sanitizeMermaidCode)', () => { @@ -100,6 +103,12 @@ test('DOCX 渲染器全量编译测试 (buildDocxModel & exportDocumentToDocx)', const buffer = await Packer.toBuffer(doc); assert.ok(buffer.length > 5000, `生成的 DOCX Buffer 应当大于 5KB,实际为 ${buffer.length} 字节`); + // 关键回归防线:解压验证 word/document.xml 中杜绝 且包含合法的 + const zip = await JSZip.loadAsync(buffer); + const docXml = await zip.file('word/document.xml')!.async('text'); + assert.ok(!docXml.includes(''), 'Word document.xml 绝对不得包含 破坏性标签'); + assert.ok(docXml.includes(') 节点'); + // 验证 exportDocumentToDocx 输出 Blob 正常 const blob = await exportDocumentToDocx(ast); assert.equal(blob.type, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');