weaver-work-cli 0.1.29 → 0.1.31

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 (185) hide show
  1. package/dist/chunks/archive-U766FTFH.js +2537 -0
  2. package/dist/chunks/archive-oa-DMWEKX3L.js +1317 -0
  3. package/dist/chunks/asset-TCR7VDEV.js +2109 -0
  4. package/dist/chunks/attachment-content-AQNX5N3S.js +23 -0
  5. package/dist/chunks/blog-EVZWGDVM.js +2157 -0
  6. package/dist/chunks/caizhilian-JOWS6SL5.js +10087 -0
  7. package/dist/chunks/calendar-5WNCV66Q.js +1195 -0
  8. package/dist/chunks/chunk-2DUV44UX.js +15606 -0
  9. package/dist/chunks/chunk-324YPTC2.js +1607 -0
  10. package/dist/chunks/chunk-65QMVWHS.js +231 -0
  11. package/dist/chunks/chunk-6V7ZLF34.js +387 -0
  12. package/dist/chunks/chunk-AC7KJWC5.js +358 -0
  13. package/dist/chunks/chunk-AJ62BGAW.js +29 -0
  14. package/dist/chunks/chunk-BUSC6UAE.js +43 -0
  15. package/dist/chunks/chunk-CVZ2JOSW.js +62 -0
  16. package/dist/chunks/chunk-D4LTJSJD.js +477 -0
  17. package/dist/chunks/chunk-EG6ZU444.js +654 -0
  18. package/dist/chunks/chunk-EZALDOQH.js +383 -0
  19. package/dist/chunks/chunk-FMPJJYYI.js +106 -0
  20. package/dist/chunks/chunk-G2JSIUWF.js +769 -0
  21. package/dist/chunks/chunk-GCG2NMWR.js +703 -0
  22. package/dist/chunks/chunk-GPMFD4RD.js +75 -0
  23. package/dist/chunks/chunk-H3WDBYJW.js +508 -0
  24. package/dist/chunks/chunk-H6XSTSLM.js +2357 -0
  25. package/dist/chunks/chunk-J7RJTZSV.js +5556 -0
  26. package/dist/chunks/chunk-LUSPKZKY.js +53 -0
  27. package/dist/chunks/chunk-NIAJ5ZCG.js +73 -0
  28. package/dist/chunks/chunk-NOODWMCL.js +584 -0
  29. package/dist/chunks/chunk-OOUTBLZ4.js +51 -0
  30. package/dist/chunks/chunk-P26ZWSKU.js +73 -0
  31. package/dist/chunks/chunk-QWY6PGL6.js +3472 -0
  32. package/dist/chunks/chunk-R2IJAK7T.js +464 -0
  33. package/dist/chunks/chunk-R7O5PUWO.js +205 -0
  34. package/dist/chunks/chunk-RCGYCOMG.js +6962 -0
  35. package/dist/chunks/chunk-VZTXRG6X.js +403 -0
  36. package/dist/chunks/chunk-WHD4XMRP.js +161 -0
  37. package/dist/chunks/chunk-YLCYX7LE.js +371 -0
  38. package/dist/chunks/chunk-ZEDWSH32.js +68 -0
  39. package/dist/chunks/continuation-2CKSBLA6.js +11 -0
  40. package/dist/chunks/continuation-HZIZRZBX.js +13 -0
  41. package/dist/chunks/continuation-NLECEJPX.js +11 -0
  42. package/dist/chunks/ebuilder-form-NJ4YK6CC.js +2276 -0
  43. package/dist/chunks/ehr-MMYN36VA.js +5241 -0
  44. package/dist/chunks/employee-SAK2QYXX.js +11 -0
  45. package/dist/chunks/esb-HIWHOU7Q.js +141 -0
  46. package/dist/chunks/fna-KBACQTA5.js +1444 -0
  47. package/dist/chunks/host-3OHS5QUX.js +21 -0
  48. package/dist/chunks/host-IG6KEH5Q.js +19 -0
  49. package/dist/chunks/host-KQL5N4HO.js +23 -0
  50. package/dist/chunks/host-LLSZQCWR.js +17 -0
  51. package/dist/chunks/hrm-LYEC67ZQ.js +244 -0
  52. package/dist/chunks/im-RN2UKAEU.js +252 -0
  53. package/dist/chunks/invoice-EEPHLAF6.js +5179 -0
  54. package/dist/chunks/jiuchuanhui-WVVZYSJE.js +1940 -0
  55. package/dist/chunks/lazy-dispatch-MDOFFL2S.js +28 -0
  56. package/dist/chunks/mail-LNQLTF6L.js +2695 -0
  57. package/dist/chunks/meeting-XSD3BDVQ.js +504 -0
  58. package/dist/chunks/msg-parse-4GSQ2WTO.js +41 -0
  59. package/dist/chunks/okr-TMWTIU2K.js +2482 -0
  60. package/dist/chunks/plan-A2FSFA7T.js +2025 -0
  61. package/dist/chunks/project-T26QLBQF.js +1361 -0
  62. package/dist/chunks/registry-2VLUAESA.js +14 -0
  63. package/dist/chunks/registry-JCFEYXOE.js +18 -0
  64. package/dist/chunks/registry-NYPYMZXG.js +16 -0
  65. package/dist/chunks/registry-Q7OQJ3SU.js +19 -0
  66. package/dist/chunks/registry-ZJMJVWFA.js +19 -0
  67. package/dist/chunks/room-recommend-UR4ZDZS6.js +20 -0
  68. package/dist/chunks/schedule-5L2PM4M6.js +24 -0
  69. package/dist/chunks/state-Y2XBP6YE.js +15 -0
  70. package/dist/chunks/workflow-TR6PG2SL.js +245 -0
  71. package/dist/cmd/argv.js +45 -0
  72. package/dist/cmd/docs/index.js +3 -3
  73. package/dist/cmd/lazy-dispatch.js +20 -0
  74. package/dist/cmd/root.js +2 -4
  75. package/dist/cmd/setup/index.js +2 -2
  76. package/dist/cmd/skills/index.js +2 -2
  77. package/dist/core/package-paths.js +47 -0
  78. package/dist/index.js +163 -7
  79. package/dist/internal/e10/auth/xiaoe.js +3 -3
  80. package/dist/internal/e10/request-runtime.js +18 -62
  81. package/dist/internal/skills/detector.js +2 -2
  82. package/dist/shortcuts/contract/data/performance-catalog.js +1306 -0
  83. package/dist/shortcuts/contract/host.js +91 -35
  84. package/dist/shortcuts/contract/index.js +213 -0
  85. package/dist/shortcuts/contract/manifest.js +928 -0
  86. package/dist/shortcuts/contract/ooxml/docx.js +295 -0
  87. package/dist/shortcuts/contract/ooxml/xlsx.js +221 -0
  88. package/dist/shortcuts/contract/ooxml/zip.js +158 -0
  89. package/dist/shortcuts/contract/operations/assistant.js +1156 -0
  90. package/dist/shortcuts/contract/operations/clause-library.js +522 -0
  91. package/dist/shortcuts/contract/operations/compare.js +193 -0
  92. package/dist/shortcuts/contract/operations/credit-page.js +34 -0
  93. package/dist/shortcuts/contract/operations/flow.js +98 -0
  94. package/dist/shortcuts/contract/operations/page.js +66 -0
  95. package/dist/shortcuts/contract/operations/performance.js +733 -0
  96. package/dist/shortcuts/contract/operations/quote.js +557 -0
  97. package/dist/shortcuts/contract/operations/recommend.js +78 -0
  98. package/dist/shortcuts/contract/operations/registry.js +18 -0
  99. package/dist/shortcuts/contract/operations/review.js +83 -0
  100. package/dist/shortcuts/contract/operations/shared.js +37 -0
  101. package/dist/shortcuts/contract/operations/upload.js +108 -0
  102. package/dist/shortcuts/contract/operations/write.js +3 -2
  103. package/dist/shortcuts/index.js +140 -40
  104. package/dist/shortcuts/invoice/manifest.js +34 -11
  105. package/dist/shortcuts/invoice/operations/reim-auto.js +9 -0
  106. package/dist/shortcuts/invoice/operations/reim.js +81 -5
  107. package/dist/shortcuts/invoice/operations/shared.js +12 -5
  108. package/dist/shortcuts/meeting/host.js +45 -3
  109. package/dist/shortcuts/meeting/invitation/docx.js +442 -0
  110. package/dist/shortcuts/meeting/operations/attachment-content.js +418 -0
  111. package/dist/shortcuts/meeting/operations/attachment.js +442 -0
  112. package/dist/shortcuts/meeting/operations/eb/cache.js +57 -0
  113. package/dist/shortcuts/meeting/operations/eb/catalog.js +343 -28
  114. package/dist/shortcuts/meeting/operations/eb/conflict.js +372 -11
  115. package/dist/shortcuts/meeting/operations/eb/create-flow.js +710 -0
  116. package/dist/shortcuts/meeting/operations/eb/decision.js +47 -1
  117. package/dist/shortcuts/meeting/operations/eb/factory.js +129 -11
  118. package/dist/shortcuts/meeting/operations/eb/index.js +122 -2
  119. package/dist/shortcuts/meeting/operations/eb/room-recommend.js +378 -0
  120. package/dist/shortcuts/meeting/operations/eb/room-summary.js +84 -0
  121. package/dist/shortcuts/meeting/operations/eb/schedule.js +450 -0
  122. package/dist/shortcuts/meeting/operations/from-group-chat.js +576 -0
  123. package/dist/shortcuts/meeting/operations/invitation.js +458 -0
  124. package/dist/shortcuts/meeting/operations/minute-content.js +266 -0
  125. package/dist/shortcuts/meeting/operations/pipeline.js +489 -0
  126. package/dist/shortcuts/meeting/operations/qrcode.js +131 -0
  127. package/dist/shortcuts/meeting/operations/receipts.js +1 -1
  128. package/dist/shortcuts/meeting/operations/registry.js +18 -0
  129. package/dist/shortcuts/meeting/operations/summary.js +276 -0
  130. package/dist/shortcuts/meeting/operations/task-notify.js +325 -0
  131. package/dist/shortcuts/meeting/operations.js +42 -1
  132. package/dist/shortcuts/meeting/qr/encoder.js +427 -0
  133. package/dist/shortcuts/meeting/qr/render.js +103 -0
  134. package/dist/shortcuts/workflow/operations/form.js +17 -5
  135. package/docs/contract.md +39 -4
  136. package/docs/invoice.md +5 -5
  137. package/package.json +3 -2
  138. package/scripts/bundle.mjs +63 -0
  139. package/skill-template/business-info.json +25 -4
  140. package/skill-template/domains/jinchengda.md +16 -3
  141. package/skill-template/domains/yepiaotong.md +1 -1
  142. package/skills/weaver-e10-jinchengda/SKILL.md +111 -31
  143. package/skills/weaver-e10-jinchengda/references/contract-agent-entry.md +7 -13
  144. package/skills/weaver-e10-jinchengda/references/contract-ai-assistant.md +150 -0
  145. package/skills/weaver-e10-jinchengda/references/contract-ai-review.md +87 -0
  146. package/skills/weaver-e10-jinchengda/references/contract-clause-library-crud.md +111 -0
  147. package/skills/weaver-e10-jinchengda/references/contract-clause-recommender.md +0 -15
  148. package/skills/weaver-e10-jinchengda/references/contract-clause-reference-save.md +0 -13
  149. package/skills/weaver-e10-jinchengda/references/contract-credit-page.md +50 -0
  150. package/skills/weaver-e10-jinchengda/references/contract-disabled-capabilities.md +4 -2
  151. package/skills/weaver-e10-jinchengda/references/contract-doc-compare.md +74 -0
  152. package/skills/weaver-e10-jinchengda/references/contract-flow-launch.md +84 -0
  153. package/skills/weaver-e10-jinchengda/references/contract-performance-query.md +29 -0
  154. package/skills/weaver-e10-jinchengda/references/contract-quote-to-contract.md +28 -0
  155. package/skills/weaver-e10-jinchengda/references/contract-request-status.md +2 -21
  156. package/skills/weaver-e10-jinchengda/references/contract-request-write.md +0 -24
  157. package/skills/weaver-e10-jinchengda/references/contract-signing-subjects.md +0 -15
  158. package/skills/weaver-e10-jinchengda/references/contract-subject-create.md +0 -17
  159. package/skills/weaver-e10-jinchengda/references/contract-template-recommend.md +70 -0
  160. package/skills/weaver-e10-jinchengda/references/contract-template-selector.md +0 -15
  161. package/skills/weaver-e10-jinchengda/references/contract-workflow-discovery.md +0 -15
  162. package/skills/weaver-e10-jinchengda/references/source-manifest.json +1168 -152
  163. package/skills/weaver-e10-meeting/SKILL.md +493 -17
  164. package/skills/weaver-e10-meeting/references/meeting-change.md +4 -4
  165. package/skills/weaver-e10-meeting/references/meeting-create.md +6 -6
  166. package/skills/weaver-e10-meeting/references/meeting-eb.md +503 -27
  167. package/skills/weaver-e10-meeting/references/meeting-invitation.md +113 -0
  168. package/skills/weaver-e10-meeting/references/meeting-lifecycle.md +6 -6
  169. package/skills/weaver-e10-meeting/references/meeting-minute-file.md +425 -0
  170. package/skills/weaver-e10-meeting/references/meeting-queries.md +4 -4
  171. package/skills/weaver-e10-meeting/references/meeting-receipts-signs.md +15 -15
  172. package/skills/weaver-e10-meeting/references/meeting-verification.md +19 -11
  173. package/skills/weaver-e10-meeting.zip +0 -0
  174. package/skills/weaver-e10-yepiaotong/SKILL.md +9 -5
  175. package/skills/weaver-e10-yepiaotong/references/invoice-enterprise-list.md +1 -1
  176. package/skills/weaver-e10-yepiaotong/references/invoice-personal-list.md +6 -5
  177. package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +14 -11
  178. package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +97 -56
  179. package/skills/weaver-e10-yepiaotong/references/reim-examples.md +84 -60
  180. package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +142 -105
  181. package/skills/weaver-e10-yepiaotong/references/reim-matching-rules.md +53 -194
  182. package/skills/weaver-e10-yepiaotong/references/reim-phone-subsidy-rules.md +7 -7
  183. package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +164 -72
  184. package/skills/weaver-e10-yepiaotong/references/source-manifest.json +6 -0
  185. package/templates/meeting/invitation.docx +0 -0
@@ -0,0 +1,442 @@
1
+ /**
2
+ * 邀请函 docx 渲染 —— WordPress 无关的纯 OOXML 处理,零第三方依赖。
3
+ *
4
+ * 为什么不用现成库:本 CLI 要对外分发(skill 随 CLI 走),依赖树越小越好;
5
+ * 而需要用到的能力只有「读 docx(zip) → 改 document.xml 文本 → 写回 docx(zip)」,
6
+ * 用 node 内置 zlib 即可完成,不引入 jszip 等包。
7
+ *
8
+ * ⚠️ 关键坑(2026-10-23 实测模板):Word 会把 `{mt_name}` 这样的占位符**拆成多个 run**:
9
+ * `会议名称:{` / `mt_name` / `}`
10
+ * 因此**不能**在 document.xml 上做字符串替换,必须按段落把 w:t 文本拼起来再替换,
11
+ * 否则任何占位符都替换不到(这是"模板看着没问题、一填就空"的根因)。
12
+ *
13
+ * 渲染策略:
14
+ * - 逐个段落处理(含表格单元格内的段落)。段落内合并文本 → 匹配 `{key}` → 按 run 回写。
15
+ * - 命中不了的占位符**不留字面量**:渲染成带黄色高亮的「【待补充】」,便于人工定位。
16
+ * - 只在段落内出现 `{` 时才重建该段;其余段落原样保留(避免无谓改动)。
17
+ */
18
+ import { deflateRawSync, inflateRawSync } from 'node:zlib';
19
+ // ---------------------------------------------------------------------------
20
+ // ZIP(docx 就是 zip)
21
+ // ---------------------------------------------------------------------------
22
+ const EOCD_SIG = 0x06054b50;
23
+ const CEN_SIG = 0x02014b50;
24
+ const LOC_SIG = 0x04034b50;
25
+ function findEocd(buf) {
26
+ const min = Math.max(0, buf.length - 0xffff - 22);
27
+ for (let i = buf.length - 22; i >= min; i -= 1) {
28
+ if (buf.readUInt32LE(i) === EOCD_SIG)
29
+ return i;
30
+ }
31
+ throw new Error('docx 解析失败:找不到 ZIP 结束记录(EOCD)');
32
+ }
33
+ /** 读取 docx(zip)的全部条目,保持原始顺序。 */
34
+ export function readDocxEntries(buf) {
35
+ const eocd = findEocd(buf);
36
+ const count = buf.readUInt16LE(eocd + 10);
37
+ let offset = buf.readUInt32LE(eocd + 16);
38
+ const entries = [];
39
+ for (let i = 0; i < count; i += 1) {
40
+ if (buf.readUInt32LE(offset) !== CEN_SIG) {
41
+ throw new Error('docx 解析失败:中央目录条目签名不匹配');
42
+ }
43
+ const method = buf.readUInt16LE(offset + 10);
44
+ const compSize = buf.readUInt32LE(offset + 20);
45
+ const nameLen = buf.readUInt16LE(offset + 28);
46
+ const extraLen = buf.readUInt16LE(offset + 30);
47
+ const commentLen = buf.readUInt16LE(offset + 32);
48
+ const localOffset = buf.readUInt32LE(offset + 42);
49
+ const name = buf.subarray(offset + 46, offset + 46 + nameLen).toString('utf8');
50
+ if (buf.readUInt32LE(localOffset) !== LOC_SIG) {
51
+ throw new Error(`docx 解析失败:${name} 的本地头签名不匹配`);
52
+ }
53
+ const localNameLen = buf.readUInt16LE(localOffset + 26);
54
+ const localExtraLen = buf.readUInt16LE(localOffset + 28);
55
+ const dataStart = localOffset + 30 + localNameLen + localExtraLen;
56
+ const raw = buf.subarray(dataStart, dataStart + compSize);
57
+ const data = method === 0 ? Buffer.from(raw) : inflateRawSync(raw);
58
+ entries.push({ name, data });
59
+ offset += 46 + nameLen + extraLen + commentLen;
60
+ }
61
+ return entries;
62
+ }
63
+ const CRC_TABLE = (() => {
64
+ const table = new Int32Array(256);
65
+ for (let i = 0; i < 256; i += 1) {
66
+ let c = i;
67
+ for (let k = 0; k < 8; k += 1)
68
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
69
+ table[i] = c;
70
+ }
71
+ return table;
72
+ })();
73
+ function crc32(buf) {
74
+ let crc = -1;
75
+ for (let i = 0; i < buf.length; i += 1)
76
+ crc = CRC_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);
77
+ return (crc ^ -1) >>> 0;
78
+ }
79
+ function dosDateTime(date) {
80
+ const time = ((date.getHours() << 11) | (date.getMinutes() << 5) | (date.getSeconds() >> 1)) & 0xffff;
81
+ const d = (((date.getFullYear() - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate()) & 0xffff;
82
+ return { time, date: d };
83
+ }
84
+ /** 把条目写回 docx(deflate,必要时降级 store)。 */
85
+ export function writeDocxEntries(entries, now = new Date()) {
86
+ const { time, date } = dosDateTime(now);
87
+ const locals = [];
88
+ const centrals = [];
89
+ let offset = 0;
90
+ for (const entry of entries) {
91
+ const nameBuf = Buffer.from(entry.name, 'utf8');
92
+ const crc = crc32(entry.data);
93
+ const deflated = deflateRawSync(entry.data);
94
+ const useDeflate = deflated.length < entry.data.length;
95
+ const payload = useDeflate ? deflated : entry.data;
96
+ const method = useDeflate ? 8 : 0;
97
+ const local = Buffer.alloc(30);
98
+ local.writeUInt32LE(LOC_SIG, 0);
99
+ local.writeUInt16LE(20, 4); // version needed
100
+ local.writeUInt16LE(0x0800, 6); // UTF-8 文件名
101
+ local.writeUInt16LE(method, 8);
102
+ local.writeUInt16LE(time, 10);
103
+ local.writeUInt16LE(date, 12);
104
+ local.writeUInt32LE(crc, 14);
105
+ local.writeUInt32LE(payload.length, 18);
106
+ local.writeUInt32LE(entry.data.length, 22);
107
+ local.writeUInt16LE(nameBuf.length, 26);
108
+ local.writeUInt16LE(0, 28);
109
+ const central = Buffer.alloc(46);
110
+ central.writeUInt32LE(CEN_SIG, 0);
111
+ central.writeUInt16LE(20, 4); // version made by
112
+ central.writeUInt16LE(20, 6);
113
+ central.writeUInt16LE(0x0800, 8);
114
+ central.writeUInt16LE(method, 10);
115
+ central.writeUInt16LE(time, 12);
116
+ central.writeUInt16LE(date, 14);
117
+ central.writeUInt32LE(crc, 16);
118
+ central.writeUInt32LE(payload.length, 20);
119
+ central.writeUInt32LE(entry.data.length, 24);
120
+ central.writeUInt16LE(nameBuf.length, 28);
121
+ central.writeUInt16LE(0, 30);
122
+ central.writeUInt16LE(0, 32);
123
+ central.writeUInt16LE(0, 34);
124
+ central.writeUInt16LE(0, 36);
125
+ central.writeUInt32LE(0, 38);
126
+ central.writeUInt32LE(offset, 42);
127
+ locals.push(local, nameBuf, payload);
128
+ centrals.push(central, nameBuf);
129
+ offset += local.length + nameBuf.length + payload.length;
130
+ }
131
+ const centralBuf = Buffer.concat(centrals);
132
+ const eocd = Buffer.alloc(22);
133
+ eocd.writeUInt32LE(EOCD_SIG, 0);
134
+ eocd.writeUInt16LE(0, 4);
135
+ eocd.writeUInt16LE(0, 6);
136
+ eocd.writeUInt16LE(entries.length, 8);
137
+ eocd.writeUInt16LE(entries.length, 10);
138
+ eocd.writeUInt32LE(centralBuf.length, 12);
139
+ eocd.writeUInt32LE(offset, 16);
140
+ eocd.writeUInt16LE(0, 20);
141
+ return Buffer.concat([...locals, centralBuf, eocd]);
142
+ }
143
+ export const PENDING_MARK = '【待补充】';
144
+ function unescapeXml(text) {
145
+ return text
146
+ .replace(/&lt;/g, '<')
147
+ .replace(/&gt;/g, '>')
148
+ .replace(/&quot;/g, '"')
149
+ .replace(/&apos;/g, "'")
150
+ .replace(/&amp;/g, '&');
151
+ }
152
+ function escapeXml(text) {
153
+ return text
154
+ .replace(/&/g, '&amp;')
155
+ .replace(/</g, '&lt;')
156
+ .replace(/>/g, '&gt;')
157
+ .replace(/"/g, '&quot;')
158
+ .replace(/'/g, '&apos;');
159
+ }
160
+ const PLACEHOLDER_RE = /\{([A-Za-z_][A-Za-z0-9_.]*)\}/g;
161
+ /** 非全局版本:只用来判断「这段文本里到底有没有占位符」。 */
162
+ const PLACEHOLDER_TEST = /\{[A-Za-z_][A-Za-z0-9_.]*\}/;
163
+ /**
164
+ * 段落内若存在绘图/超链接/域/内容控件等非纯文本结构,重建会破坏它们 ⇒ 跳过该段。
165
+ * 模板是纯文本段落 + 普通表格,正常不会命中;命中时如实计入 skippedParagraphs。
166
+ */
167
+ const UNSAFE_PARAGRAPH_RE = /<w:(drawing|pict|hyperlink|fldSimple|sdt|object|txbxContent)\b/;
168
+ /** 提取段落的 w:pPr(原样保留)与内容体。 */
169
+ function splitParagraph(paraXml) {
170
+ const openMatch = /^<w:p(?:\s[^>]*)?>/.exec(paraXml);
171
+ const open = openMatch ? openMatch[0] : '<w:p>';
172
+ let body = paraXml.slice(open.length).replace(/<\/w:p>\s*$/, '');
173
+ let pPr = '';
174
+ const pPrMatch = /^<w:pPr>[\s\S]*?<\/w:pPr>/.exec(body);
175
+ if (pPrMatch) {
176
+ pPr = pPrMatch[0];
177
+ body = body.slice(pPr.length);
178
+ }
179
+ return { open, pPr, body };
180
+ }
181
+ function parseRuns(body) {
182
+ const runs = [];
183
+ const runRe = /<w:r(?:\s[^>]*)?>([\s\S]*?)<\/w:r>/g;
184
+ let match;
185
+ while ((match = runRe.exec(body)) !== null) {
186
+ const inner = match[1];
187
+ const rPrMatch = /<w:rPr>[\s\S]*?<\/w:rPr>/.exec(inner);
188
+ const rPr = rPrMatch ? rPrMatch[0] : '';
189
+ let text = '';
190
+ const tRe = /<w:t(?:\s[^>]*)?>([\s\S]*?)<\/w:t>/g;
191
+ let tMatch;
192
+ while ((tMatch = tRe.exec(inner)) !== null)
193
+ text += unescapeXml(tMatch[1]);
194
+ runs.push({ rPr, text });
195
+ }
196
+ return runs;
197
+ }
198
+ function withHighlight(rPr) {
199
+ const highlight = '<w:highlight w:val="yellow"/>';
200
+ if (!rPr)
201
+ return `<w:rPr>${highlight}</w:rPr>`;
202
+ if (rPr.includes('<w:highlight'))
203
+ return rPr;
204
+ // 插入到 rPr 末尾(rPr 内元素顺序对 Word 不严格,highlight 放在最后可被正确识别)
205
+ return rPr.replace(/<\/w:rPr>$/, `${highlight}</w:rPr>`);
206
+ }
207
+ function emitRun(rPr, text) {
208
+ // xml:space="preserve" 防止首尾空格被吞
209
+ return `<w:r>${rPr}<w:t xml:space="preserve">${escapeXml(text)}</w:t></w:r>`;
210
+ }
211
+ /**
212
+ * 替换单个段落中的占位符。
213
+ * 文本按 run 顺序合并后匹配,替换结果尽量写回**占位符起点所在的 run**,
214
+ * 其余 run 仅删除占位符所占字符 —— 这样段落内其他文字的格式不受影响。
215
+ *
216
+ * 返回值语义:
217
+ * - `null`:该段无需/无法处理(不含占位符,或没有文本 run)
218
+ * - `'unsafe'`:含占位符但段落里有图片/超链接等结构,重建会破坏它 ⇒ 跳过并计数
219
+ * - `string`:替换后的段落 XML
220
+ */
221
+ function replaceInParagraph(paraXml, resolver, filled, pending) {
222
+ if (!paraXml.includes('{'))
223
+ return null;
224
+ const { open, pPr, body } = splitParagraph(paraXml);
225
+ const runs = parseRuns(body);
226
+ if (runs.length === 0)
227
+ return null;
228
+ const concat = runs.map((run) => run.text).join('');
229
+ // ⚠️ 必须在**合并后的文本**上判断占位符:Word 把 `{mt_name}` 拆成多个 run,
230
+ // 直接在原始 XML 上匹配 `\{key\}` 永远匹配不到(首版就踩了这个坑)。
231
+ if (!PLACEHOLDER_TEST.test(concat))
232
+ return null;
233
+ if (UNSAFE_PARAGRAPH_RE.test(paraXml))
234
+ return 'unsafe';
235
+ PLACEHOLDER_RE.lastIndex = 0;
236
+ const matches = [...concat.matchAll(PLACEHOLDER_RE)];
237
+ if (matches.length === 0)
238
+ return null;
239
+ // 每个 run 在 concat 中的区间
240
+ const spans = [];
241
+ let cursor = 0;
242
+ for (const run of runs) {
243
+ spans.push({ start: cursor, end: cursor + run.text.length });
244
+ cursor += run.text.length;
245
+ }
246
+ const runAt = (pos) => {
247
+ for (let i = 0; i < spans.length; i += 1)
248
+ if (pos >= spans[i].start && pos < spans[i].end)
249
+ return i;
250
+ return spans.length - 1;
251
+ };
252
+ // 用**字符级归属**做替换:每个字符记住它来自哪个 run、是否属于【待补充】。
253
+ // 这样"取不到值"时只把【待补充】这三个字标黄,而不会把整个 run(整句)染黄
254
+ // —— 2026-10-28 用户反馈:「【待补充】 诚邀您参加「…」,现将会议安排通知如下:」整行都是黄底,
255
+ // 实际只有召集人那一段该高亮。
256
+ const units = [];
257
+ runs.forEach((run, index) => {
258
+ for (const ch of run.text)
259
+ units.push({ ch, run: index, highlight: false });
260
+ });
261
+ // 从后往前替换,避免前面替换影响后面区间(units 的下标与 concat 一一对应)
262
+ for (const match of [...matches].reverse()) {
263
+ const key = match[1];
264
+ const start = match.index ?? 0;
265
+ const end = start + match[0].length;
266
+ const value = resolver(key);
267
+ // 取不到值时只渲染【待补充】(不回显 dataKey)。
268
+ // 2026-10-28 真机:模板「尊敬的 {guest_org}:」渲染成了「尊敬的 【待补充】:guest_org:」——
269
+ // 原因是 resolver 对缺失字段返回 `text=key`,拼进去把字段名漏进了**对外的邀请函**。
270
+ // 字段名仍然通过返回值 pendingFields 告诉调用方,不必写在文档正文里。
271
+ const text = value.pending ? PENDING_MARK : value.text;
272
+ (value.pending ? pending : filled).push(key);
273
+ const firstRun = runAt(start);
274
+ const replacement = [...text].map((ch) => ({ ch, run: firstRun, highlight: Boolean(value.pending) }));
275
+ units.splice(start, end - start, ...replacement);
276
+ }
277
+ // 相邻且(run 相同 / 格式与高亮相同)的字符合并回 run
278
+ const merged = [];
279
+ for (const unit of units) {
280
+ const rPr = runs[unit.run]?.rPr ?? '';
281
+ const last = merged[merged.length - 1];
282
+ if (last && last.rPr === rPr && last.highlight === unit.highlight)
283
+ last.text += unit.ch;
284
+ else
285
+ merged.push({ text: unit.ch, rPr, highlight: unit.highlight });
286
+ }
287
+ const rendered = merged
288
+ .map((run) => emitRun(run.highlight ? withHighlight(run.rPr) : run.rPr, run.text))
289
+ .join('');
290
+ return `${open}${pPr}${rendered}</w:p>`;
291
+ }
292
+ /** 在整份 XML(document.xml / header*.xml / footer*.xml)上替换占位符。 */
293
+ export function replacePlaceholdersInXml(xml, resolver) {
294
+ const filled = [];
295
+ const pending = [];
296
+ let skippedParagraphs = 0;
297
+ const output = xml.replace(/<w:p(?:\s[^>]*)?>[\s\S]*?<\/w:p>/g, (paraXml) => {
298
+ if (!paraXml.includes('{'))
299
+ return paraXml;
300
+ const replaced = replaceInParagraph(paraXml, resolver, filled, pending);
301
+ if (replaced === 'unsafe') {
302
+ skippedParagraphs += 1;
303
+ return paraXml;
304
+ }
305
+ if (replaced === null)
306
+ return paraXml;
307
+ return replaced;
308
+ });
309
+ return {
310
+ xml: output,
311
+ filled: [...new Set(filled)],
312
+ pending: [...new Set(pending)],
313
+ skippedParagraphs,
314
+ };
315
+ }
316
+ const TR_RE = /<w:tr(?:\s[^>]*)?>[\s\S]*?<\/w:tr>/g;
317
+ const TC_RE = /<w:tc(?:\s[^>]*)?>[\s\S]*?<\/w:tc>/g;
318
+ const WT_RE = /<w:t(?:\s[^>]*)?>([^<]*)<\/w:t>/g;
319
+ function cellText(tc) {
320
+ return [...tc.matchAll(WT_RE)].map((match) => unescapeXml(match[1])).join('');
321
+ }
322
+ /**
323
+ * 把单元格文字整体换掉,同时保留原有格式。
324
+ *
325
+ * Word 的单元格内可能有多个段落、每个段落多个 run。这里压成**一段一个 run**:
326
+ * 段落属性取原第一段,run 属性优先取原第一个 run(否则回退段落标记的 rPr)。
327
+ * 模板每格都是一段,所以压平不会丢版式。
328
+ */
329
+ function setCellText(tc, text) {
330
+ const tcOpen = /^<w:tc(?:\s[^>]*)?>/.exec(tc)?.[0] ?? '<w:tc>';
331
+ const tcPr = /<w:tcPr>[\s\S]*?<\/w:tcPr>/.exec(tc)?.[0] ?? '';
332
+ const paragraphs = tc.match(/<w:p(?:\s[^>]*)?>[\s\S]*?<\/w:p>/g) ?? [];
333
+ const firstParagraph = paragraphs[0] ?? '<w:p></w:p>';
334
+ const pOpen = /^<w:p(?:\s[^>]*)?>/.exec(firstParagraph)?.[0] ?? '<w:p>';
335
+ const pPr = /<w:pPr>[\s\S]*?<\/w:pPr>/.exec(firstParagraph)?.[0] ?? '';
336
+ const firstRun = /<w:r(?:\s[^>]*)?>[\s\S]*?<\/w:r>/.exec(firstParagraph)?.[0] ?? '';
337
+ const rPr = /<w:rPr>[\s\S]*?<\/w:rPr>/.exec(firstRun)?.[0] ??
338
+ /<w:rPr>[\s\S]*?<\/w:rPr>/.exec(pPr)?.[0] ??
339
+ '';
340
+ const run = text ? `<w:r>${rPr}<w:t xml:space="preserve">${escapeXml(text)}</w:t></w:r>` : '';
341
+ return `${tcOpen}${tcPr}${pOpen}${pPr}${run}</w:p></w:tc>`;
342
+ }
343
+ /**
344
+ * 在 document.xml 上按「表头关键字」定位表格,重建数据行。
345
+ *
346
+ * 没有匹配到表格、或 `rows` 为空时**原样返回**(`filled:false`)——
347
+ * 「没有议题就保持模板原样」是产品口径,不能把示例行清空。
348
+ */
349
+ export function fillTableRowsInXml(xml, option) {
350
+ const rows = option.rows.filter((row) => row.some((cell) => String(cell ?? '').trim() !== ''));
351
+ if (rows.length === 0)
352
+ return { xml, filled: false, rowCount: 0 };
353
+ const tables = [...xml.matchAll(/<w:tbl(?:\s[^>]*)?>[\s\S]*?<\/w:tbl>/g)];
354
+ for (const table of tables) {
355
+ const tableXml = table[0];
356
+ const tableRows = tableXml.match(TR_RE) ?? [];
357
+ if (tableRows.length < 2)
358
+ continue;
359
+ const headerRow = tableRows[0];
360
+ const headerText = (headerRow.match(TC_RE) ?? []).map(cellText).join(' ');
361
+ if (!headerText.includes(option.headerKeyword))
362
+ continue;
363
+ const dataRows = tableRows.slice(1);
364
+ const cloneSource = dataRows[dataRows.length - 1];
365
+ // 斑马纹:模板里的数据行通常给「1 行带底纹 + 1 行无底纹」两种样式。
366
+ // 原来按 `dataRows[index]` 取样式,超过模板行数后就一直克隆最后一行,
367
+ // 于是第 3 行起全是同一种底色、条纹断掉(2026-10-28 用户反馈:第一行淡蓝、后面全白)。
368
+ // 这里改成按**数据行序号奇偶**套用「有底纹 / 无底纹」两种模板行:
369
+ // 奇数行(index 0、2、4…)用带底纹那行,偶数行用无底纹那行;模板只有一种样式时保持原行为。
370
+ const shadingFill = (rowXml) => {
371
+ const matched = /<w:shd\b[^>]*w:fill="([^"]*)"/.exec(rowXml);
372
+ return (matched?.[1] ?? '').trim().toUpperCase();
373
+ };
374
+ const isShadedRow = (rowXml) => {
375
+ const fill = shadingFill(rowXml);
376
+ return fill !== '' && fill !== 'AUTO' && fill !== 'FFFFFF';
377
+ };
378
+ const shadedRow = dataRows.find((row) => isShadedRow(row));
379
+ const plainRow = dataRows.find((row) => !isShadedRow(row));
380
+ const hasZebra = Boolean(shadedRow && plainRow);
381
+ const rebuilt = [headerRow];
382
+ rows.forEach((values, index) => {
383
+ // 有斑马纹模板:奇数行(0-based 偶数)用带底纹行,偶数行用无底纹行
384
+ const base = hasZebra
385
+ ? (index % 2 === 0 ? shadedRow : plainRow)
386
+ : (dataRows[index] ?? cloneSource);
387
+ const trOpen = /^<w:tr(?:\s[^>]*)?>/.exec(base)?.[0] ?? '<w:tr>';
388
+ const trPr = /<w:trPr>[\s\S]*?<\/w:trPr>/.exec(base)?.[0] ?? '';
389
+ const cells = base.match(TC_RE) ?? [];
390
+ const rebuiltCells = cells.map((cell, cellIndex) => setCellText(cell, values[cellIndex] ?? ''));
391
+ rebuilt.push(`${trOpen}${trPr}${rebuiltCells.join('')}</w:tr>`);
392
+ });
393
+ // 表格根节点之后的部分(</w:tbl>)保留,之前的 tblPr / tblGrid 也保留
394
+ const firstRowIndex = tableXml.indexOf(headerRow);
395
+ const head = tableXml.slice(0, firstRowIndex);
396
+ const newTableXml = `${head}${rebuilt.join('')}</w:tbl>`;
397
+ const start = table.index ?? 0;
398
+ return {
399
+ xml: `${xml.slice(0, start)}${newTableXml}${xml.slice(start + tableXml.length)}`,
400
+ filled: true,
401
+ rowCount: rows.length,
402
+ };
403
+ }
404
+ return { xml, filled: false, rowCount: 0 };
405
+ }
406
+ /** 需要处理的 XML 部件(正文 + 页眉页脚 + 脚注尾注)。 */
407
+ const TEXT_PART_RE = /^word\/(document|header\d*|footer\d*|footnotes|endnotes)\.xml$/;
408
+ /** 渲染整个 docx:替换所有文本部件里的占位符,其余条目原样保留。 */
409
+ export function renderDocx(template, resolver, options = {}) {
410
+ const entries = readDocxEntries(template);
411
+ const filled = new Set();
412
+ const pending = new Set();
413
+ let skippedParagraphs = 0;
414
+ let agenda;
415
+ const updated = entries.map((entry) => {
416
+ if (!TEXT_PART_RE.test(entry.name))
417
+ return entry;
418
+ let xml = entry.data.toString('utf8');
419
+ // 表格行填充必须**先于**占位符替换:重建行会把单元格压成单 run,
420
+ // 若先替换再重建,占位符所在的 run 结构会被丢弃。
421
+ if (options.agenda && entry.name === 'word/document.xml') {
422
+ const tableResult = fillTableRowsInXml(xml, options.agenda);
423
+ agenda = { filled: tableResult.filled, rowCount: tableResult.rowCount };
424
+ xml = tableResult.xml;
425
+ }
426
+ if (!xml.includes('{')) {
427
+ return xml === entry.data.toString('utf8') ? entry : { name: entry.name, data: Buffer.from(xml, 'utf8') };
428
+ }
429
+ const result = replacePlaceholdersInXml(xml, resolver);
430
+ result.filled.forEach((key) => filled.add(key));
431
+ result.pending.forEach((key) => pending.add(key));
432
+ skippedParagraphs += result.skippedParagraphs;
433
+ return { name: entry.name, data: Buffer.from(result.xml, 'utf8') };
434
+ });
435
+ return {
436
+ buffer: writeDocxEntries(updated),
437
+ filled: [...filled],
438
+ pending: [...pending],
439
+ skippedParagraphs,
440
+ ...(options.agenda ? { agenda: agenda ?? { filled: false, rowCount: 0 } } : {}),
441
+ };
442
+ }