release: 发布 v0.6.1 DOCX 视觉一致性修复

新增通用 CSS 到 OOXML 翻译修复,统一字体、字距、精确行距、段落、列表、表格、引用、代码块与行内代码连续性,不引入按主题 ID 分支。

新增 MdTP Mono 并统一 Serif、Sans、Mono 三字体包的 Chromium 与 DOCX 使用链;字体声明、嵌入部件和 Word/WPS 实际采用均进入硬门禁。

重建封面整页及正文语义块视觉差分,14 套主题、纵横两个方向、五组页边距共 140 个真实场景全部通过,阻断失败和诊断失败均为零。

源码服务、Docker Web API 与实际安装 Desktop 的 red-briefing 导出均包含 5 个字体部件;Word/WPS 原生渲染和逐页复核通过。修复 Docker 构建上下文与运行层复用软链接,并完善 v0.6.1 版本、发行说明和发布归集。

验证:npm test(116 个文件、616 项测试)、npm run typecheck、npm run build、git diff --check 全部通过。Desktop 安装器与 ZIP、Docker v0.6.1 镜像已生成;Windows 产物仍为未签名内部发行。
This commit is contained in:
SkyJourney
2026-08-04 10:30:44 +08:00
parent b275c671fc
commit 2c5c1bd317
84 changed files with 4404 additions and 344 deletions
+84 -2
View File
@@ -11,6 +11,9 @@ import { createPdfGenerator } from "../dist/pdf-engine.js";
const directory = path.dirname(fileURLToPath(import.meta.url));
const repositoryDirectory = path.resolve(directory, "../../..");
const appVersion = JSON.parse(
fs.readFileSync(path.join(repositoryDirectory, "package.json"), "utf8")
).version;
const themesDirectory = path.join(repositoryDirectory, "themes");
const samplesDirectory = path.join(repositoryDirectory, "samples", "themes");
const webDirectory = path.join(repositoryDirectory, "apps", "web", "dist");
@@ -32,6 +35,37 @@ const fontPackRoot = path.resolve(
);
const decoder = new TextDecoder();
const inlineCodeContinuityProbes = [
{
id: "paragraph",
code: "mdtp_ic_p",
text: "段落前mdtp_ic_p段落后"
},
{
id: "list-item",
code: "mdtp_ic_l",
text: "列表前mdtp_ic_l列表后"
},
{
id: "table-cell",
code: "mdtp_ic_c",
text: "单元格前mdtp_ic_c单元格后"
}
];
const inlineCodeContinuityMarkdown = `
## 行内代码连续性门禁
段落前\`mdtp_ic_p\`段落后
- 列表前\`mdtp_ic_l\`列表后
| 门禁 | 内容 |
| --- | --- |
| 行内代码 | 单元格前\`mdtp_ic_c\`单元格后 |
`;
const contentTypes = new Map([
[".css", "text/css; charset=utf-8"],
[".html", "text/html; charset=utf-8"],
@@ -226,6 +260,35 @@ function inspectDocx(content) {
count + (paragraph.match(/<w:tab\s*\/>/gu)?.length ?? 0),
0
);
const inlineCodeContinuity = inlineCodeContinuityProbes.map((probe) => {
const matchingParagraphs = paragraphs.filter((paragraph) =>
paragraph.includes(probe.code)
);
const paragraph = matchingParagraphs[0] ?? "";
const text = [...paragraph.matchAll(/<w:t(?:\s[^>]*)?>([\s\S]*?)<\/w:t>/gu)]
.map((match) => match[1])
.join("");
const codeRunPattern = new RegExp(
`<w:r(?:\\s|>)[\\s\\S]*?<w:rStyle\\s+w:val="VerbatimChar"\\s*\\/>[\\s\\S]*?<w:t(?:\\s[^>]*)?>${probe.code}<\\/w:t>[\\s\\S]*?<\\/w:r>`,
"u"
);
const hardBreakCount =
paragraph.match(/<w:br(?:\s[^>]*)?\s*\/>/gu)?.length ?? 0;
return {
id: probe.id,
code: probe.code,
expectedText: probe.text,
actualText: text,
paragraphCount: matchingParagraphs.length,
hardBreakCount,
characterStylePresent: codeRunPattern.test(paragraph),
passed:
matchingParagraphs.length === 1 &&
text === probe.text &&
hardBreakCount === 0 &&
codeRunPattern.test(paragraph)
};
});
return {
bytes: content.byteLength,
sha256: sha256(content),
@@ -247,6 +310,10 @@ function inspectDocx(content) {
officialIssueRowRightTabCount:
officialIssueRowsWithRightTab.length,
officialIssueRowContentTabCount,
inlineCodeContinuity,
inlineCodeContinuityPassed: inlineCodeContinuity.every(
(probe) => probe.passed
),
requiredStylesPresent: [
"Normal",
"Heading1",
@@ -366,13 +433,22 @@ const port = await reservePort();
const origin = `http://127.0.0.1:${port}`;
const pdfGenerator = createPdfGenerator({ renderOrigin: origin });
const docxMediaAdapter = createDocxMediaEngine({ renderOrigin: origin });
const captureDocxMedia = docxMediaAdapter.capture.bind(docxMediaAdapter);
let capturedDocxDocumentLayout;
docxMediaAdapter.capture = async (...args) => {
const output = await captureDocxMedia(...args);
capturedDocxDocumentLayout = structuredClone(
output.plan.documentLayout ?? { tables: [] }
);
return output;
};
const app = buildApp({
logger: { level: "error" },
pdfGenerator,
docxMediaAdapter,
fontPacks: {
roots: [fontPackRoot],
appVersion: "0.6.0"
appVersion
}
});
registerWebRuntime(app);
@@ -390,10 +466,11 @@ try {
);
for (const theme of themes) {
capturedDocxDocumentLayout = undefined;
process.stderr.write(`[DOCX R4 matrix] generating ${theme.id}\n`);
const samplePath = path.join(samplesDirectory, `${theme.id}.md`);
assert(fs.existsSync(samplePath), `主题缺少验收示例:${theme.id}`);
const markdown = fs.readFileSync(samplePath, "utf8");
const markdown = `${fs.readFileSync(samplePath, "utf8").trimEnd()}${inlineCodeContinuityMarkdown}`;
const exportConfig = mergeExportConfig({
...defaultExportConfig,
name: `${theme.name} R4 生产链验收`,
@@ -449,6 +526,10 @@ try {
inspection.requiredStylesPresent,
`主题 ${theme.id} 缺少标准 Word 样式`
);
assert(
inspection.inlineCodeContinuityPassed,
`主题 ${theme.id} 的行内代码连续性门禁失败:${JSON.stringify(inspection.inlineCodeContinuity)}`
);
const standard = standardByTheme.get(theme.id);
assert(standard, `标准矩阵缺少主题 ${theme.id}`);
assert(
@@ -504,6 +585,7 @@ try {
docx: {
outputFile: path.relative(repositoryDirectory, docxPath),
inspection,
documentLayout: capturedDocxDocumentLayout ?? { tables: [] },
diagnostics: responseDiagnostics(docx.response, "docx")
},
standardInspection: standard.inspection,