fix(engine): 修复 DOCX 公式因 undefined 虚根在 Word 中丢失的缺陷

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
WeiCHEN
2026-09-08 20:37:28 +08:00
co-authored by Cursor
parent 29efa0d348
commit a527f4dcd4
4 changed files with 83 additions and 7 deletions
+25 -2
View File
@@ -15,6 +15,7 @@ import {
Trash2,
Shield,
Layers,
FileText,
} from 'lucide-react';
interface DeckDrawerProps {
@@ -56,7 +57,29 @@ export const DeckDrawer: React.FC<DeckDrawerProps> = ({
return null;
}
const renderBadge = (state: DocLifecycleState, isClaimed: boolean) => {
const renderBadge = (state: DocLifecycleState, isClaimed: boolean, isTemplateOnly?: boolean) => {
if (isTemplateOnly || state === 'template') {
return (
<span
style={{
fontSize: '11px',
fontWeight: 600,
color: '#0284c7',
background: '#e0f2fe',
border: '1px solid #bae6fd',
padding: '2px 8px',
borderRadius: 'var(--r-pill)',
display: 'flex',
alignItems: 'center',
gap: '3px',
}}
title="该文档为项目标准预置模板副本,点击可全景查阅大纲与章节规范"
>
<FileText size={11} />
<span>📄 初始模板</span>
</span>
);
}
switch (state) {
case 'approved':
return (
@@ -388,7 +411,7 @@ export const DeckDrawer: React.FC<DeckDrawerProps> = ({
<span style={{ fontSize: '12.5px', fontWeight: 600, color: 'var(--text-strong)' }}>
{doc.title}
</span>
{renderBadge(doc.lifecycleState, doc.isClaimed)}
{renderBadge(doc.lifecycleState, doc.isClaimed, doc.isTemplateOnly)}
</div>
<div style={{ fontSize: '11px', color: 'var(--text-muted)', display: 'flex', gap: '12px' }}>
@@ -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, '📄 初始模板');
});
+12 -3
View File
@@ -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 中杜绝 <undefined> 且包含合法的 <m:oMath>
const zip = await JSZip.loadAsync(buffer);
const docXml = await zip.file('word/document.xml')!.async('text');
assert.ok(!docXml.includes('<undefined>'), 'Word document.xml 绝对不得包含 <undefined> 破坏性标签');
assert.ok(docXml.includes('<m:oMath'), 'Word document.xml 必须包含合法的微软 Office Math (<m:oMath>) 节点');
// 验证 exportDocumentToDocx 输出 Blob 正常
const blob = await exportDocumentToDocx(ast);
assert.equal(blob.type, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');