@zeyue0329/xiaoma-cli 1.11.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/.playwright-cli/console-2026-05-13T06-36-26-793Z.log +2 -0
  2. package/.playwright-cli/page-2026-05-13T06-36-27-725Z.yml +1 -0
  3. package/CLAUDE.md +25 -7
  4. package/XiaoMa-CLI-2026H2-/350/277/255/344/273/243/350/247/204/345/210/222.pptx +0 -0
  5. package/demo/xiaoma-bug-circle-resolve/SKILL.md +6 -0
  6. package/demo/xiaoma-bug-circle-resolve/workflow.md +254 -0
  7. package/demo/xiaoma-bug-resolve/SKILL.md +6 -0
  8. package/demo/xiaoma-bug-resolve/workflow.md +269 -0
  9. package/demo/xiaoma-prd-saas-zh/README.md +57 -0
  10. package/demo/xiaoma-prd-saas-zh/domain-research.md +128 -0
  11. package/demo/xiaoma-prd-saas-zh/epics.md +303 -0
  12. package/demo/xiaoma-prd-saas-zh/market-research-2026-q1.md +183 -0
  13. package/demo/xiaoma-prd-saas-zh/prd-bad-examples.md +268 -0
  14. package/demo/xiaoma-prd-saas-zh/prd.md +409 -0
  15. package/demo/xiaoma-prd-saas-zh/product-brief.md +97 -0
  16. package/demo/xiaoma-prd-saas-zh/validation-report.md +279 -0
  17. package/docs/roadshow/01-/351/241/271/347/233/256/346/246/202/350/247/210/344/270/216/346/236/266/346/236/204.md +189 -0
  18. package/docs/roadshow/02-/346/231/272/350/203/275/344/275/223/347/263/273/347/273/237/350/257/246/350/247/243.md +464 -0
  19. package/docs/roadshow/03-/346/231/272/350/203/275/344/275/223/344/272/244/344/272/222/346/265/201/347/250/213/345/233/276.md +334 -0
  20. package/docs/roadshow/04-/345/267/245/344/275/234/346/265/201/346/211/247/350/241/214/350/257/246/350/247/243.md +1038 -0
  21. package/docs/roadshow/05-/346/212/200/346/234/257/345/256/236/347/216/260/344/270/216/345/210/233/346/226/260/344/272/256/347/202/271.md +205 -0
  22. package/docs/roadshow/06-/350/267/257/346/274/224/346/200/273/347/273/223/344/270/216/346/274/224/347/244/272/345/273/272/350/256/256.md +167 -0
  23. package/media/doc1_fig1.png +0 -0
  24. package/media/doc1_fig2.png +0 -0
  25. package/media/doc1_fig3.png +0 -0
  26. package/media/doc1_fig4.png +0 -0
  27. package/media/doc2_fig1.png +0 -0
  28. package/media/doc2_fig2.png +0 -0
  29. package/media/doc2_fig3.png +0 -0
  30. package/media/doc2_fig4.png +0 -0
  31. package/media/doc3_fig1.png +0 -0
  32. package/media/doc3_fig2.png +0 -0
  33. package/media/doc3_fig3.png +0 -0
  34. package/media/doc3_fig4.png +0 -0
  35. package/media/doc4_fig1.png +0 -0
  36. package/media/doc4_fig2.png +0 -0
  37. package/media/doc4_fig3.png +0 -0
  38. package/media/doc5_fig1.png +0 -0
  39. package/media/doc5_fig2.png +0 -0
  40. package/media/doc5_fig3.png +0 -0
  41. package/package.json +1 -1
  42. package/patent-disclosure-optimized/SKILL.md +416 -0
  43. package/patent-disclosure-optimized/references/disclosure-template.md +84 -0
  44. package/patent-disclosure-optimized/references/docx-format-spec.md +183 -0
  45. package/patent-disclosure-optimized/references/mining-principles.md +168 -0
  46. package/patent-disclosure-optimized/scripts/md2docx.js +777 -0
  47. package/src/core/tasks/xiaoma-create-prd/data/prd-purpose.md +157 -0
  48. package/src/core/tasks/xiaoma-create-prd/data/upstream-input-contract.md +168 -0
  49. package/src/core/tasks/xiaoma-create-prd/templates/prd-skeleton-reference.md +428 -0
  50. package/src/core/tasks/xiaoma-create-prd/templates/prd-template.md +101 -3
  51. package/src/xmc/agents/sm.agent.yaml +9 -1
  52. package/src/xmc/workflows/2-plan-workflows/xiaoma-validate-prd/data/prd-quality-rubric.csv +14 -0
  53. package/src/xmc/workflows/4-implementation/auto-story-pipeline/SKILL.md +1 -1
  54. package/src/xmc/workflows/4-implementation/auto-story-pipeline/steps/step-01-init-and-validate.md +10 -13
  55. package/src/xmc/workflows/4-implementation/auto-story-pipeline/steps/step-02-create-story.md +0 -1
  56. package/src/xmc/workflows/4-implementation/auto-story-pipeline/steps/step-08-complete-story.md +3 -4
  57. package/src/xmc/workflows/4-implementation/auto-story-pipeline/steps/step-09-finalize.md +69 -0
  58. package/src/xmc/workflows/4-implementation/auto-story-pipeline/workflow.md +9 -14
  59. package/src/xmc/workflows/4-implementation/auto-story-pipeline/xiaoma-skill-manifest.yaml +1 -1
  60. package/src/xmc/workflows/4-implementation/auto-story-pipeline-batch/SKILL.md +6 -0
  61. package/src/xmc/workflows/4-implementation/auto-story-pipeline-batch/workflow.md +333 -0
  62. package/src/xmc/workflows/4-implementation/auto-story-pipeline-batch/xiaoma-skill-manifest.yaml +3 -0
  63. package/src/xmc/workflows/5-full-pipeline/auto-full-pipeline/steps/step-01-init-and-validate.md +2 -2
  64. package/src/xmc/workflows/5-full-pipeline/auto-full-pipeline/steps/step-04-run-story-pipeline.md +30 -41
  65. package/src/xmc/workflows/5-full-pipeline/auto-full-pipeline/steps/step-05-finalize.md +2 -2
  66. package/src/xmc/workflows/5-full-pipeline/auto-full-pipeline/workflow.md +7 -9
  67. package/src/xmc/workflows/5-full-pipeline/auto-prd-to-stories/SKILL.md +6 -0
  68. package/src/xmc/workflows/5-full-pipeline/auto-prd-to-stories/checklist.md +43 -0
  69. package/src/xmc/workflows/5-full-pipeline/auto-prd-to-stories/steps/step-01-init-and-validate.md +155 -0
  70. package/src/xmc/workflows/5-full-pipeline/auto-prd-to-stories/steps/step-02-create-epics.md +156 -0
  71. package/src/xmc/workflows/5-full-pipeline/auto-prd-to-stories/steps/step-03-bridge-sprint-planning.md +143 -0
  72. package/src/xmc/workflows/5-full-pipeline/auto-prd-to-stories/steps/step-04-batch-create-stories.md +309 -0
  73. package/src/xmc/workflows/5-full-pipeline/auto-prd-to-stories/steps/step-05-finalize.md +311 -0
  74. package/src/xmc/workflows/5-full-pipeline/auto-prd-to-stories/workflow.md +105 -0
  75. package/src/xmc/workflows/5-full-pipeline/auto-prd-to-stories/xiaoma-skill-manifest.yaml +3 -0
  76. package//344/270/223/345/210/251/344/272/244/345/272/225/344/271/246_1_/351/235/242/345/220/221AI/346/231/272/350/203/275/344/275/223/347/232/204/345/244/232/351/200/232/351/201/223/344/276/235/350/265/226_20260318.md +483 -0
  77. package//344/270/223/345/210/251/344/272/244/345/272/225/344/271/246_2_/345/237/272/344/272/216/351/205/215/347/275/256/351/251/261/345/212/250/347/232/204/350/267/250/345/271/263/345/217/260IDE/346/231/272/350/203/275_20260318.md +592 -0
  78. package//344/270/223/345/210/251/344/272/244/345/272/225/344/271/246_3_AI/346/231/272/350/203/275/344/275/223/345/243/260/346/230/216/345/274/217/345/256/232/344/271/211/347/232/204/347/274/226/350/257/221/346/265/201/346/260/264_20260318.md +624 -0
  79. package//344/270/223/345/210/251/344/272/244/345/272/225/344/271/246_4_/345/237/272/344/272/216/345/223/210/345/270/214/346/214/207/347/272/271/347/232/204/346/231/272/350/203/275/344/275/223/351/231/204/345/261/236/350/265/204/346/272/220/351/200/211_20260318.md +628 -0
  80. package//344/270/223/345/210/251/344/272/244/345/272/225/344/271/246_5_AI/346/231/272/350/203/275/344/275/223/350/247/246/345/217/221/346/214/207/344/273/244/347/232/204/345/244/215/345/220/210/346/240/274/345/274/217/346/240/241_20260318.md +652 -0
  81. package/src/xmc/workflows/4-implementation/auto-story-pipeline/steps/step-09-cycle-check.md +0 -147
@@ -0,0 +1,777 @@
1
+ // patent-disclosure-md2docx (v2)
2
+ // 将专利交底书 Markdown 文件按上传的“技术交底书模板.doc”格式生成 .docx
3
+ // 模板规范(来自模板 styles.xml + document.xml):
4
+ // - 默认正文:东亚 宋体,西文 Times New Roman,五号 10.5pt(sz=21)
5
+ // - 大标题“技术交底书”:东亚 楷体_GB2312,加粗,18pt(sz=36),居中
6
+ // - 头部字段(交底书名称、本专利发明人、技术问题联系人、联系人电话、E-MAIL):
7
+ // 东亚 仿宋_GB2312,加粗,10.5pt
8
+ // - 章节大标题(1、2、3、…该发明所属技术领域、术语解释):东亚 楷体_GB2312,加粗,10.5pt
9
+ // - 二级 / 三级标题(3.1、3.2.1 等):东亚 楷体_GB2312,加粗,10.5pt
10
+ // - 行距:标题段 exact 21pt(line=420),正文 auto 1.5x(line=360)
11
+ // - 正文首行缩进 2 个字符(firstLine=420 twips, 五号 210 twips/char × 2)
12
+ // - 字体颜色:全部黑色(模板蓝色 0000FF 仅用于占位提示语,我们的真实内容用黑色)
13
+ // 用法: node md2docx.js <input.md> <output.docx>
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const {
18
+ Document,
19
+ Packer,
20
+ Paragraph,
21
+ TextRun,
22
+ Table,
23
+ TableRow,
24
+ TableCell,
25
+ Footer,
26
+ AlignmentType,
27
+ LevelFormat,
28
+ HeadingLevel,
29
+ BorderStyle,
30
+ WidthType,
31
+ ShadingType,
32
+ PageNumber,
33
+ ImageRun,
34
+ } = require('docx');
35
+
36
+ // 全局:当前正在处理的 markdown 文件所在目录,供解析图片相对路径用
37
+ let CURRENT_MD_DIR = process.cwd();
38
+
39
+ // =====================
40
+ // 模板字体与字号
41
+ // =====================
42
+ const FONT_BODY_EAST = '宋体'; // 正文东亚字体
43
+ const FONT_HEADER_FIELD_EAST = '仿宋_GB2312'; // 头部字段东亚字体
44
+ const FONT_HEADING_EAST = '楷体_GB2312'; // 章节标题东亚字体
45
+ const FONT_TITLE_EAST = '楷体_GB2312'; // 大标题东亚字体
46
+ const FONT_EN = 'Times New Roman'; // 西文字体
47
+ const FONT_CODE = 'Consolas'; // 代码块字体
48
+
49
+ // docx 字号单位是半磅
50
+ const SIZE_BODY = 21; // 五号 10.5pt
51
+ const SIZE_TITLE = 36; // 18pt 大标题
52
+
53
+ // 行距:1.5 倍 = 360 (auto);标题 exact = 420 (21pt)
54
+ const BODY_LINE_SPACING = { line: 360, lineRule: 'auto' };
55
+ const HEADING_LINE_SPACING = { line: 420, lineRule: 'exact' };
56
+
57
+ // 正文首行缩进 2 字 = 420 twips
58
+ const BODY_FIRST_LINE_INDENT = 420;
59
+
60
+ // 页面:A4,页边距上下 2.54cm,左右 3.17cm
61
+ const PAGE_WIDTH = 11906;
62
+ const PAGE_HEIGHT = 16838;
63
+ const MARGIN_LR = 1800;
64
+ const MARGIN_TB = 1440;
65
+ const CONTENT_WIDTH = PAGE_WIDTH - MARGIN_LR * 2;
66
+
67
+ // =====================
68
+ // Markdown -> token 流
69
+ // =====================
70
+ function tokenize(md) {
71
+ const lines = md.replace(/\r\n/g, '\n').split('\n');
72
+ const tokens = [];
73
+ let i = 0;
74
+ while (i < lines.length) {
75
+ const line = lines[i];
76
+
77
+ // 代码块
78
+ if (/^\s*```/.test(line)) {
79
+ const codeLines = [];
80
+ i += 1;
81
+ while (i < lines.length && !/^\s*```/.test(lines[i])) {
82
+ codeLines.push(lines[i]);
83
+ i += 1;
84
+ }
85
+ i += 1;
86
+ tokens.push({ type: 'code', text: codeLines.join('\n') });
87
+ continue;
88
+ }
89
+
90
+ // 独立成行的图片:![alt](path)
91
+ const imgMatch = line.match(/^\s*!\[([^\]]*)\]\(([^)]+)\)\s*$/);
92
+ if (imgMatch) {
93
+ tokens.push({ type: 'image', alt: imgMatch[1], src: imgMatch[2] });
94
+ i += 1;
95
+ continue;
96
+ }
97
+
98
+ // 标题
99
+ const headingMatch = line.match(/^(#{1,6})\s+(.+?)\s*$/);
100
+ if (headingMatch) {
101
+ tokens.push({
102
+ type: 'heading',
103
+ level: headingMatch[1].length,
104
+ text: headingMatch[2],
105
+ });
106
+ i += 1;
107
+ continue;
108
+ }
109
+
110
+ // 表格
111
+ if (/^\s*\|.*\|\s*$/.test(line) && i + 1 < lines.length && /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)+\|?\s*$/.test(lines[i + 1])) {
112
+ const headerRow = parseTableRow(line);
113
+ i += 2;
114
+ const rows = [];
115
+ while (i < lines.length && /^\s*\|.*\|\s*$/.test(lines[i])) {
116
+ rows.push(parseTableRow(lines[i]));
117
+ i += 1;
118
+ }
119
+ tokens.push({ type: 'table', header: headerRow, rows });
120
+ continue;
121
+ }
122
+
123
+ // 水平线
124
+ if (/^\s*-{3,}\s*$/.test(line) || /^\s*\*{3,}\s*$/.test(line)) {
125
+ tokens.push({ type: 'horizontal_rule' });
126
+ i += 1;
127
+ continue;
128
+ }
129
+
130
+ // 列表
131
+ if (/^\s*([-*])\s+/.test(line) || /^\s*\d+\.\s+/.test(line)) {
132
+ const items = [];
133
+ while (i < lines.length && /^\s*([-*]|\d+\.)\s+/.test(lines[i])) {
134
+ const m = lines[i].match(/^(\s*)([-*]|\d+\.)\s+(.*)$/);
135
+ const ordered = /\d+\./.test(m[2]);
136
+ const collected = [m[3]];
137
+ let k = i + 1;
138
+ while (
139
+ k < lines.length &&
140
+ lines[k].trim() !== '' &&
141
+ !/^\s*([-*]|\d+\.)\s+/.test(lines[k]) &&
142
+ !/^#{1,6}\s+/.test(lines[k]) &&
143
+ !/^\s*\|.*\|\s*$/.test(lines[k]) &&
144
+ !/^\s*```/.test(lines[k])
145
+ ) {
146
+ collected.push(lines[k].trim());
147
+ k += 1;
148
+ }
149
+ items.push({ ordered, text: collected.join(' ') });
150
+ i = k;
151
+ }
152
+ tokens.push({ type: 'list', items });
153
+ continue;
154
+ }
155
+
156
+ if (line.trim() === '') {
157
+ tokens.push({ type: 'blank' });
158
+ i += 1;
159
+ continue;
160
+ }
161
+
162
+ // 段落(合并相邻非空非特殊行)
163
+ // 但若当前行或下一行是 **字段名:** 形式的"头部字段行",则不与相邻行合并
164
+ const isHeaderField = (ln) => /^\s*\*\*[^*]+[::]\s*\*\*/.test(ln) || /^\s*\*\*技术交底书\*\*\s*$/.test(ln);
165
+ const paragraphLines = [line];
166
+ let j = i + 1;
167
+ if (!isHeaderField(line)) {
168
+ while (
169
+ j < lines.length &&
170
+ lines[j].trim() !== '' &&
171
+ !isHeaderField(lines[j]) &&
172
+ !/^#{1,6}\s+/.test(lines[j]) &&
173
+ !/^\s*\|.*\|\s*$/.test(lines[j]) &&
174
+ !/^\s*```/.test(lines[j]) &&
175
+ !/^\s*([-*]|\d+\.)\s+/.test(lines[j]) &&
176
+ !/^\s*!\[([^\]]*)\]\(([^)]+)\)\s*$/.test(lines[j])
177
+ ) {
178
+ paragraphLines.push(lines[j]);
179
+ j += 1;
180
+ }
181
+ }
182
+ tokens.push({ type: 'paragraph', text: paragraphLines.join(' ') });
183
+ i = j;
184
+ }
185
+ return tokens;
186
+ }
187
+
188
+ function parseTableRow(line) {
189
+ const trimmed = line
190
+ .trim()
191
+ .replace(/^\|/, '')
192
+ .replace(/\|\s*$/, '');
193
+ return trimmed.split('|').map((c) => c.trim());
194
+ }
195
+
196
+ // =====================
197
+ // 内联文本 -> TextRun[]
198
+ // =====================
199
+ function inlineRuns(text, options = {}) {
200
+ if (text == null) return [new TextRun({ text: '', ...options })];
201
+ const result = [];
202
+ text = text.replace(/\\\|/g, '|');
203
+
204
+ // 抠出 `code`
205
+ const tokens = [];
206
+ const codeRe = /`([^`]+)`/g;
207
+ let lastIdx = 0;
208
+ let m;
209
+ while ((m = codeRe.exec(text)) !== null) {
210
+ if (m.index > lastIdx) tokens.push({ kind: 'text', text: text.slice(lastIdx, m.index) });
211
+ tokens.push({ kind: 'code', text: m[1] });
212
+ lastIdx = m.index + m[0].length;
213
+ }
214
+ if (lastIdx < text.length) tokens.push({ kind: 'text', text: text.slice(lastIdx) });
215
+
216
+ // 在 text token 内处理 **bold** / *em*
217
+ const expanded = [];
218
+ for (const t of tokens) {
219
+ if (t.kind !== 'text') {
220
+ expanded.push(t);
221
+ continue;
222
+ }
223
+ const boldRe = /\*\*([^*]+)\*\*/g;
224
+ let li = 0;
225
+ let bm;
226
+ const sub = [];
227
+ while ((bm = boldRe.exec(t.text)) !== null) {
228
+ if (bm.index > li) sub.push({ kind: 'text', text: t.text.slice(li, bm.index) });
229
+ sub.push({ kind: 'bold', text: bm[1] });
230
+ li = bm.index + bm[0].length;
231
+ }
232
+ if (li < t.text.length) sub.push({ kind: 'text', text: t.text.slice(li) });
233
+
234
+ for (const st of sub) {
235
+ if (st.kind !== 'text') {
236
+ expanded.push(st);
237
+ continue;
238
+ }
239
+ const emRe = /(?<!\*)\*([^*\n]+)\*(?!\*)/g;
240
+ let lj = 0;
241
+ let em;
242
+ const sub2 = [];
243
+ while ((em = emRe.exec(st.text)) !== null) {
244
+ if (em.index > lj) sub2.push({ kind: 'text', text: st.text.slice(lj, em.index) });
245
+ sub2.push({ kind: 'em', text: em[1] });
246
+ lj = em.index + em[0].length;
247
+ }
248
+ if (lj < st.text.length) sub2.push({ kind: 'text', text: st.text.slice(lj) });
249
+ for (const x of sub2) expanded.push(x);
250
+ }
251
+ }
252
+
253
+ const baseSize = options.size || SIZE_BODY;
254
+ const baseFontEast = options.fontEast || FONT_BODY_EAST;
255
+ const baseFontEn = options.fontEn || FONT_EN;
256
+ const baseBold = options.bold || false;
257
+ const baseItalic = options.italic || false;
258
+ const baseColor = options.color || '000000';
259
+
260
+ const makeFont = (ascii) => ({ ascii, hAnsi: ascii, cs: ascii, eastAsia: baseFontEast });
261
+
262
+ for (const t of expanded) {
263
+ if (!t.text) continue;
264
+ if (t.kind === 'code') {
265
+ result.push(
266
+ new TextRun({
267
+ text: t.text,
268
+ font: makeFont(FONT_CODE),
269
+ size: baseSize,
270
+ bold: baseBold,
271
+ italics: baseItalic,
272
+ color: baseColor,
273
+ }),
274
+ );
275
+ } else if (t.kind === 'bold') {
276
+ result.push(
277
+ new TextRun({
278
+ text: t.text,
279
+ font: makeFont(baseFontEn),
280
+ size: baseSize,
281
+ bold: true,
282
+ italics: baseItalic,
283
+ color: baseColor,
284
+ }),
285
+ );
286
+ } else if (t.kind === 'em') {
287
+ result.push(
288
+ new TextRun({
289
+ text: t.text,
290
+ font: makeFont(baseFontEn),
291
+ size: baseSize,
292
+ bold: baseBold,
293
+ italics: true,
294
+ color: baseColor,
295
+ }),
296
+ );
297
+ } else {
298
+ result.push(
299
+ new TextRun({
300
+ text: t.text,
301
+ font: makeFont(baseFontEn),
302
+ size: baseSize,
303
+ bold: baseBold,
304
+ italics: baseItalic,
305
+ color: baseColor,
306
+ }),
307
+ );
308
+ }
309
+ }
310
+ if (result.length === 0) {
311
+ result.push(
312
+ new TextRun({
313
+ text: '',
314
+ font: makeFont(baseFontEn),
315
+ size: baseSize,
316
+ color: baseColor,
317
+ }),
318
+ );
319
+ }
320
+ return result;
321
+ }
322
+
323
+ // =====================
324
+ // 各类段落构造
325
+ // =====================
326
+
327
+ // 大标题 “技术交底书” 居中、18pt 楷体
328
+ function makeBigTitle(text) {
329
+ return new Paragraph({
330
+ alignment: AlignmentType.CENTER,
331
+ spacing: { line: 420, lineRule: 'exact', before: 240, after: 240 },
332
+ children: inlineRuns(text, {
333
+ bold: true,
334
+ size: SIZE_TITLE,
335
+ fontEast: FONT_TITLE_EAST,
336
+ fontEn: FONT_EN,
337
+ }),
338
+ });
339
+ }
340
+
341
+ // 头部字段段落(交底书名称、本专利发明人、技术问题联系人、联系人电话、E-MAIL)
342
+ // 仿宋_GB2312 加粗 10.5pt
343
+ function makeHeaderField(text) {
344
+ return new Paragraph({
345
+ alignment: AlignmentType.LEFT,
346
+ spacing: BODY_LINE_SPACING,
347
+ children: inlineRuns(text, {
348
+ bold: true,
349
+ size: SIZE_BODY,
350
+ fontEast: FONT_HEADER_FIELD_EAST,
351
+ fontEn: FONT_EN,
352
+ }),
353
+ });
354
+ }
355
+
356
+ // 章节标题 # / ## / ### / #### 全部按模板规范:楷体_GB2312 加粗 10.5pt
357
+ function makeHeading(level, text) {
358
+ const headingLevel = [HeadingLevel.HEADING_1, HeadingLevel.HEADING_2, HeadingLevel.HEADING_3, HeadingLevel.HEADING_4][
359
+ Math.min(level - 1, 3)
360
+ ];
361
+ return new Paragraph({
362
+ heading: headingLevel,
363
+ alignment: AlignmentType.LEFT,
364
+ spacing: { ...HEADING_LINE_SPACING, before: level === 1 ? 240 : 160, after: level === 1 ? 120 : 80 },
365
+ children: inlineRuns(text, {
366
+ bold: true,
367
+ size: SIZE_BODY,
368
+ fontEast: FONT_HEADING_EAST,
369
+ fontEn: FONT_EN,
370
+ }),
371
+ });
372
+ }
373
+
374
+ // 普通正文段:宋体 + Times New Roman,10.5pt,1.5 倍行距,两端对齐,首行缩进 2 字
375
+ function makeBodyParagraph(text) {
376
+ return new Paragraph({
377
+ alignment: AlignmentType.BOTH,
378
+ spacing: { ...BODY_LINE_SPACING, after: 0 },
379
+ indent: { firstLine: BODY_FIRST_LINE_INDENT },
380
+ children: inlineRuns(text, {
381
+ size: SIZE_BODY,
382
+ fontEast: FONT_BODY_EAST,
383
+ fontEn: FONT_EN,
384
+ }),
385
+ });
386
+ }
387
+
388
+ function makeCodeParagraphs(text) {
389
+ const lines = text.split('\n');
390
+ return lines.map(
391
+ (l) =>
392
+ new Paragraph({
393
+ alignment: AlignmentType.LEFT,
394
+ spacing: { line: 300, lineRule: 'auto', after: 0 },
395
+ shading: { fill: 'F5F5F5', type: ShadingType.CLEAR, color: 'auto' },
396
+ children: [
397
+ new TextRun({
398
+ text: l,
399
+ font: { ascii: FONT_CODE, hAnsi: FONT_CODE, cs: FONT_CODE, eastAsia: FONT_BODY_EAST },
400
+ size: 20, // 10pt 代码
401
+ color: '000000',
402
+ }),
403
+ ],
404
+ }),
405
+ );
406
+ }
407
+
408
+ function makeListParagraphs(items) {
409
+ return items.map(
410
+ (it) =>
411
+ new Paragraph({
412
+ alignment: AlignmentType.BOTH,
413
+ spacing: { ...BODY_LINE_SPACING, after: 0 },
414
+ numbering: {
415
+ reference: it.ordered ? 'patent-num' : 'patent-bullet',
416
+ level: 0,
417
+ },
418
+ children: inlineRuns(it.text, {
419
+ size: SIZE_BODY,
420
+ fontEast: FONT_BODY_EAST,
421
+ fontEn: FONT_EN,
422
+ }),
423
+ }),
424
+ );
425
+ }
426
+
427
+ // PNG 像素尺寸读取(IHDR)
428
+ function readPngSize(buffer) {
429
+ if (buffer.length < 24) return null;
430
+ // PNG 签名校验
431
+ const sig = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
432
+ if (!buffer.subarray(0, 8).equals(sig)) return null;
433
+ // 第 16-19 字节是宽度,第 20-23 字节是高度(big-endian)
434
+ const width = buffer.readUInt32BE(16);
435
+ const height = buffer.readUInt32BE(20);
436
+ return { width, height };
437
+ }
438
+
439
+ function makeImageParagraph(alt, src) {
440
+ // 解析路径(相对 markdown 文件目录)
441
+ const resolved = path.isAbsolute(src) ? src : path.resolve(CURRENT_MD_DIR, src);
442
+ if (!fs.existsSync(resolved)) {
443
+ // 找不到时退化为文本说明,避免转换中断
444
+ return new Paragraph({
445
+ alignment: AlignmentType.CENTER,
446
+ spacing: { ...BODY_LINE_SPACING, after: 80 },
447
+ children: inlineRuns(`[图片缺失] ${alt} (${src})`, {
448
+ size: SIZE_BODY,
449
+ fontEast: FONT_BODY_EAST,
450
+ fontEn: FONT_EN,
451
+ color: 'FF0000',
452
+ }),
453
+ });
454
+ }
455
+ const data = fs.readFileSync(resolved);
456
+ const ext = path.extname(resolved).slice(1).toLowerCase();
457
+ const typeMap = { png: 'png', jpg: 'jpg', jpeg: 'jpeg', gif: 'gif', bmp: 'bmp', svg: 'svg' };
458
+ const type = typeMap[ext] || 'png';
459
+
460
+ // 按内容区宽度等比缩放
461
+ // 内容区宽 8306 DXA ≈ 5.77 inch;docx ImageRun.transformation 用像素值(默认 96 dpi),可用 内容像素宽 ≈ 5.77*96 = 554
462
+ const MAX_PX_WIDTH = 540; // 留少量边距
463
+ const MAX_PX_HEIGHT = 720; // 控制最大高度,避免极长图占满页
464
+
465
+ let displayWidth = MAX_PX_WIDTH;
466
+ let displayHeight = MAX_PX_WIDTH; // 占位
467
+ const size = type === 'png' ? readPngSize(data) : null;
468
+ if (size && size.width && size.height) {
469
+ const ratio = size.height / size.width;
470
+ displayWidth = MAX_PX_WIDTH;
471
+ displayHeight = Math.round(displayWidth * ratio);
472
+ if (displayHeight > MAX_PX_HEIGHT) {
473
+ displayHeight = MAX_PX_HEIGHT;
474
+ displayWidth = Math.round(displayHeight / ratio);
475
+ }
476
+ } else {
477
+ // 兜底:默认 4:3
478
+ displayWidth = MAX_PX_WIDTH;
479
+ displayHeight = Math.round(MAX_PX_WIDTH * 0.75);
480
+ }
481
+
482
+ return [
483
+ new Paragraph({
484
+ alignment: AlignmentType.CENTER,
485
+ spacing: { line: 360, lineRule: 'auto', before: 120, after: 40 },
486
+ children: [
487
+ new ImageRun({
488
+ type,
489
+ data,
490
+ transformation: { width: displayWidth, height: displayHeight },
491
+ altText: { title: alt || '附图', description: alt || '附图', name: path.basename(resolved) },
492
+ }),
493
+ ],
494
+ }),
495
+ // 图注:alt 文字,居中、小一号、斜体
496
+ alt
497
+ ? new Paragraph({
498
+ alignment: AlignmentType.CENTER,
499
+ spacing: { line: 300, lineRule: 'auto', after: 120 },
500
+ children: inlineRuns(alt, {
501
+ size: 20, // 10pt 图注
502
+ fontEast: FONT_BODY_EAST,
503
+ fontEn: FONT_EN,
504
+ italic: true,
505
+ }),
506
+ })
507
+ : null,
508
+ ].filter(Boolean);
509
+ }
510
+
511
+ function makeTable(table) {
512
+ const colCount = table.header.length;
513
+ const colWidth = Math.floor(CONTENT_WIDTH / colCount);
514
+ const columnWidths = new Array(colCount).fill(colWidth);
515
+ columnWidths[colCount - 1] = CONTENT_WIDTH - colWidth * (colCount - 1);
516
+
517
+ const border = { style: BorderStyle.SINGLE, size: 4, color: '808080' };
518
+ const borders = { top: border, bottom: border, left: border, right: border };
519
+
520
+ const buildCell = (text, isHeader, width) =>
521
+ new TableCell({
522
+ borders,
523
+ width: { size: width, type: WidthType.DXA },
524
+ margins: { top: 80, bottom: 80, left: 120, right: 120 },
525
+ shading: isHeader ? { fill: 'E7E6E6', type: ShadingType.CLEAR, color: 'auto' } : undefined,
526
+ children: [
527
+ new Paragraph({
528
+ spacing: { line: 300, lineRule: 'auto' },
529
+ alignment: isHeader ? AlignmentType.CENTER : AlignmentType.LEFT,
530
+ children: inlineRuns(text, {
531
+ size: SIZE_BODY,
532
+ fontEast: FONT_BODY_EAST,
533
+ fontEn: FONT_EN,
534
+ bold: isHeader,
535
+ }),
536
+ }),
537
+ ],
538
+ });
539
+
540
+ const headerRow = new TableRow({
541
+ tableHeader: true,
542
+ children: table.header.map((c, idx) => buildCell(c, true, columnWidths[idx])),
543
+ });
544
+ const bodyRows = table.rows.map(
545
+ (r) =>
546
+ new TableRow({
547
+ children: r
548
+ .slice(0, colCount)
549
+ .concat(new Array(Math.max(0, colCount - r.length)).fill(''))
550
+ .map((c, idx) => buildCell(c, false, columnWidths[idx])),
551
+ }),
552
+ );
553
+
554
+ return new Table({
555
+ width: { size: CONTENT_WIDTH, type: WidthType.DXA },
556
+ columnWidths,
557
+ rows: [headerRow, ...bodyRows],
558
+ });
559
+ }
560
+
561
+ // =====================
562
+ // 整篇组装:识别开头三件套(大标题 + 头部字段 + 头部联系信息)
563
+ // =====================
564
+ function buildChildrenFromTokens(tokens) {
565
+ const children = [];
566
+ let pendingBlank = false;
567
+
568
+ // 头部识别:紧贴文件开头的若干个 paragraph,若文本是「**技术交底书**」/「**交底书名称:**XXX」/「**本专利发明人:**XXX」/「**技术问题联系人:**XXX」/「**联系人电话:**XXX」/「**E-MAIL:**XXX」就走模板头部样式
569
+ const HEADER_LABELS = ['交底书名称', '本专利发明人', '技术问题联系人', '联系人电话', 'E-MAIL', 'E-mail', 'e-mail', 'Email', 'email'];
570
+
571
+ for (let idx = 0; idx < tokens.length; idx += 1) {
572
+ const tk = tokens[idx];
573
+ switch (tk.type) {
574
+ case 'heading':
575
+ children.push(makeHeading(tk.level, tk.text));
576
+ break;
577
+ case 'paragraph': {
578
+ const stripped = tk.text.replace(/\*\*/g, '').trim();
579
+ // 大标题
580
+ if (stripped === '技术交底书') {
581
+ children.push(makeBigTitle('技术交底书'));
582
+ break;
583
+ }
584
+ // 头部字段:以 “XXX:” 形式开头的若干行
585
+ const matchedLabel = HEADER_LABELS.find((lbl) => stripped.startsWith(lbl + ':') || stripped.startsWith(lbl + ':'));
586
+ if (matchedLabel) {
587
+ children.push(makeHeaderField(stripped));
588
+ break;
589
+ }
590
+ children.push(makeBodyParagraph(tk.text));
591
+ break;
592
+ }
593
+ case 'table':
594
+ children.push(makeTable(tk));
595
+ children.push(
596
+ new Paragraph({
597
+ spacing: BODY_LINE_SPACING,
598
+ children: [new TextRun({ text: '', size: SIZE_BODY })],
599
+ }),
600
+ );
601
+ break;
602
+ case 'image':
603
+ children.push(...makeImageParagraph(tk.alt, tk.src));
604
+ break;
605
+ case 'code':
606
+ children.push(...makeCodeParagraphs(tk.text));
607
+ break;
608
+ case 'list':
609
+ children.push(...makeListParagraphs(tk.items));
610
+ break;
611
+ case 'horizontal_rule':
612
+ children.push(
613
+ new Paragraph({
614
+ border: { bottom: { style: BorderStyle.SINGLE, size: 6, color: '808080', space: 1 } },
615
+ children: [new TextRun({ text: '' })],
616
+ }),
617
+ );
618
+ break;
619
+ case 'blank':
620
+ pendingBlank = true;
621
+ break;
622
+ default:
623
+ break;
624
+ }
625
+ // pendingBlank 用于潜在的空段处理(这里默认不插入空段以保持紧凑)
626
+ if (pendingBlank) pendingBlank = false;
627
+ }
628
+ return children;
629
+ }
630
+
631
+ function buildDocument(tokens) {
632
+ const bodyFont = { ascii: FONT_EN, hAnsi: FONT_EN, cs: FONT_EN, eastAsia: FONT_BODY_EAST };
633
+ const headingFont = { ascii: FONT_EN, hAnsi: FONT_EN, cs: FONT_EN, eastAsia: FONT_HEADING_EAST };
634
+ const styles = {
635
+ default: {
636
+ document: {
637
+ run: {
638
+ font: bodyFont,
639
+ size: SIZE_BODY,
640
+ color: '000000',
641
+ },
642
+ paragraph: { spacing: BODY_LINE_SPACING },
643
+ },
644
+ },
645
+ paragraphStyles: [
646
+ {
647
+ id: 'Heading1',
648
+ name: 'Heading 1',
649
+ basedOn: 'Normal',
650
+ next: 'Normal',
651
+ quickFormat: true,
652
+ run: { font: headingFont, size: SIZE_BODY, bold: true, color: '000000' },
653
+ paragraph: { spacing: { ...HEADING_LINE_SPACING, before: 240, after: 120 }, outlineLevel: 0 },
654
+ },
655
+ {
656
+ id: 'Heading2',
657
+ name: 'Heading 2',
658
+ basedOn: 'Normal',
659
+ next: 'Normal',
660
+ quickFormat: true,
661
+ run: { font: headingFont, size: SIZE_BODY, bold: true, color: '000000' },
662
+ paragraph: { spacing: { ...HEADING_LINE_SPACING, before: 160, after: 80 }, outlineLevel: 1 },
663
+ },
664
+ {
665
+ id: 'Heading3',
666
+ name: 'Heading 3',
667
+ basedOn: 'Normal',
668
+ next: 'Normal',
669
+ quickFormat: true,
670
+ run: { font: headingFont, size: SIZE_BODY, bold: true, color: '000000' },
671
+ paragraph: { spacing: { ...HEADING_LINE_SPACING, before: 140, after: 60 }, outlineLevel: 2 },
672
+ },
673
+ {
674
+ id: 'Heading4',
675
+ name: 'Heading 4',
676
+ basedOn: 'Normal',
677
+ next: 'Normal',
678
+ quickFormat: true,
679
+ run: { font: headingFont, size: SIZE_BODY, bold: true, color: '000000' },
680
+ paragraph: { spacing: { ...HEADING_LINE_SPACING, before: 120, after: 60 }, outlineLevel: 3 },
681
+ },
682
+ ],
683
+ };
684
+
685
+ const numbering = {
686
+ config: [
687
+ {
688
+ reference: 'patent-bullet',
689
+ levels: [
690
+ {
691
+ level: 0,
692
+ format: LevelFormat.BULLET,
693
+ text: '•',
694
+ alignment: AlignmentType.LEFT,
695
+ style: { paragraph: { indent: { left: 720, hanging: 360 } } },
696
+ },
697
+ ],
698
+ },
699
+ {
700
+ reference: 'patent-num',
701
+ levels: [
702
+ {
703
+ level: 0,
704
+ format: LevelFormat.DECIMAL,
705
+ text: '%1.',
706
+ alignment: AlignmentType.LEFT,
707
+ style: { paragraph: { indent: { left: 720, hanging: 360 } } },
708
+ },
709
+ ],
710
+ },
711
+ ],
712
+ };
713
+
714
+ const children = buildChildrenFromTokens(tokens);
715
+
716
+ const doc = new Document({
717
+ creator: 'patent-disclosure-md2docx',
718
+ title: '技术交底书',
719
+ styles,
720
+ numbering,
721
+ sections: [
722
+ {
723
+ properties: {
724
+ page: {
725
+ size: { width: PAGE_WIDTH, height: PAGE_HEIGHT },
726
+ margin: { top: MARGIN_TB, bottom: MARGIN_TB, left: MARGIN_LR, right: MARGIN_LR },
727
+ },
728
+ },
729
+ footers: {
730
+ default: new Footer({
731
+ children: [
732
+ new Paragraph({
733
+ alignment: AlignmentType.CENTER,
734
+ children: [
735
+ new TextRun({
736
+ children: [PageNumber.CURRENT],
737
+ font: { ascii: FONT_EN, hAnsi: FONT_EN, cs: FONT_EN, eastAsia: FONT_BODY_EAST },
738
+ size: 18, // 9pt 页码
739
+ color: '000000',
740
+ }),
741
+ ],
742
+ }),
743
+ ],
744
+ }),
745
+ },
746
+ children,
747
+ },
748
+ ],
749
+ });
750
+
751
+ return doc;
752
+ }
753
+
754
+ async function convert(inputPath, outputPath) {
755
+ CURRENT_MD_DIR = path.dirname(path.resolve(inputPath));
756
+ const md = fs.readFileSync(inputPath, 'utf8');
757
+ const tokens = tokenize(md);
758
+ const doc = buildDocument(tokens);
759
+ const buffer = await Packer.toBuffer(doc);
760
+ fs.writeFileSync(outputPath, buffer);
761
+ }
762
+
763
+ async function main() {
764
+ const argv = process.argv.slice(2);
765
+ if (argv.length < 2) {
766
+ console.error('Usage: node md2docx.js <input.md> <output.docx>');
767
+ process.exit(1);
768
+ }
769
+ const [inp, outp] = argv;
770
+ await convert(inp, outp);
771
+ console.log(`OK -> ${outp}`);
772
+ }
773
+
774
+ main().catch((e) => {
775
+ console.error(e);
776
+ process.exit(1);
777
+ });