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,295 @@
1
+ /**
2
+ * 最小 docx 填充引擎(零依赖)。
3
+ *
4
+ * 只处理 word/document.xml,语义与源脚本 quote_to_contract.py 的 fill_docx 对齐:
5
+ * - 正文段落(不含表格内)先做具名占位符 + 匿名空白填充(低阈值)。
6
+ * - 表格单元格里「空值格」只认高阈值(≥95)的精确命中,避免误填签名栏。
7
+ * - 改写策略与 python-docx 的 set_para_text 一致:首个 w:t 写入新文本,其余清空。
8
+ * - 明细追加表自带 tblBorders,不依赖模板存在 Table Grid 样式。
9
+ *
10
+ * XML 处理为分段正则扫描(机器生成的 OOXML 结构稳定),不做通用 XML 解析。
11
+ */
12
+ import { readFileSync } from 'node:fs';
13
+ import { readZipEntry, rewriteZip } from './zip.js';
14
+ import { encodeXml, decodeXml } from './xlsx.js';
15
+ /** 注意:带 /g 的正则对象 .test() 有状态,统一用 SOURCE 现场构造。 */
16
+ export const PLACEHOLDER_SOURCE = '\\{\\{[^}]{1,60}\\}\\}|\\$\\{[^}]{1,60}\\}|【[^】]{1,60}】';
17
+ export const BLANK_SOURCE = '_{2,}|_{2,}|【\\s*】|\\{\\s*\\}';
18
+ export function hasPlaceholder(text) {
19
+ return new RegExp(PLACEHOLDER_SOURCE, 'u').test(text);
20
+ }
21
+ export function hasBlank(text) {
22
+ return new RegExp(BLANK_SOURCE, 'u').test(text);
23
+ }
24
+ /** 取占位符之前的标签(以 :/: 结尾),退化取段内第一个词组。 */
25
+ export function labelBefore(text, pos) {
26
+ const seg = text.slice(0, pos);
27
+ const matches = [...seg.matchAll(/([^\s,,;;。]{1,20})[::]\s*$/gu)];
28
+ if (matches.length)
29
+ return matches[matches.length - 1][1];
30
+ const m2 = /([^\s,,;;。]{2,20})[::]/u.exec(seg);
31
+ return m2 ? m2[1] : '';
32
+ }
33
+ function fillText(text, floor, filler, log) {
34
+ const orig = text;
35
+ // 1) 具名占位符
36
+ text = text.replace(new RegExp(PLACEHOLDER_SOURCE, 'gu'), (token) => {
37
+ const label = token.replace(/^[\{$【]+/u, '').replace(/[}\}】]+$/u, '').trim();
38
+ const { key, score } = filler.bestKey(label, floor);
39
+ const value = key ? filler.valOf(key) : '';
40
+ if (value) {
41
+ log.push({ label, key, score, type: 'named' });
42
+ return value;
43
+ }
44
+ log.push({ label, key, score, type: 'named_unfilled' });
45
+ return token;
46
+ });
47
+ // 2) 匿名空白
48
+ let out = '';
49
+ let idx = 0;
50
+ let changed = false;
51
+ for (const m of text.matchAll(new RegExp(BLANK_SOURCE, 'gu'))) {
52
+ const start = m.index ?? 0;
53
+ const label = labelBefore(text, start);
54
+ const { key, score } = filler.bestKey(label, floor);
55
+ const value = key ? filler.valOf(key) : '';
56
+ if (value) {
57
+ out += text.slice(idx, start) + value;
58
+ log.push({ label, key, score, type: 'blank' });
59
+ changed = true;
60
+ }
61
+ else {
62
+ out += text.slice(idx, start + m[0].length);
63
+ log.push({ label, key, score, type: 'blank_unfilled' });
64
+ }
65
+ idx = start + m[0].length;
66
+ }
67
+ out += text.slice(idx);
68
+ return { text: out, changed: changed || out !== orig };
69
+ }
70
+ function collectRanges(xml, tag) {
71
+ const open = new RegExp(`<${tag}(\\s[^>]*)?>`, 'gu');
72
+ const close = new RegExp(`</${tag}>`, 'gu');
73
+ const ranges = [];
74
+ const events = [];
75
+ for (const m of xml.matchAll(open))
76
+ events.push({ pos: m.index ?? 0, open: true });
77
+ for (const m of xml.matchAll(close))
78
+ events.push({ pos: m.index ?? 0, open: false });
79
+ events.sort((a, b) => a.pos - b.pos || (a.open ? -1 : 1));
80
+ let depth = 0;
81
+ let currentStart = -1;
82
+ for (const e of events) {
83
+ if (e.open) {
84
+ if (depth === 0)
85
+ currentStart = e.pos;
86
+ depth++;
87
+ }
88
+ else {
89
+ depth--;
90
+ if (depth === 0 && currentStart >= 0) {
91
+ const end = e.pos + tag.length + 3;
92
+ ranges.push({ start: currentStart, end });
93
+ currentStart = -1;
94
+ }
95
+ }
96
+ }
97
+ return ranges;
98
+ }
99
+ function paragraphFullText(pXml) {
100
+ let text = '';
101
+ for (const m of pXml.matchAll(/<w:t(?:\s[^>]*)?>([\s\S]*?)<\/w:t>/gu))
102
+ text += decodeXml(m[1]);
103
+ return text;
104
+ }
105
+ function setParagraphText(pXml, newText) {
106
+ let first = true;
107
+ return pXml.replace(/<w:t((?:\s[^>]*)?)>([\s\S]*?)<\/w:t>/gu, (_all, attrs) => {
108
+ if (first) {
109
+ first = false;
110
+ const spaceAttr = /xml:space/.test(attrs) ? attrs : attrs + ' xml:space="preserve"';
111
+ return `<w:t${spaceAttr}>${encodeXml(newText)}</w:t>`;
112
+ }
113
+ return '<w:t xml:space="preserve"></w:t>';
114
+ });
115
+ }
116
+ function topRanges(xml, tag) {
117
+ // 只取深度 0 的顶层范围
118
+ return collectRanges(xml, tag);
119
+ }
120
+ function isInside(pos, ranges) {
121
+ return ranges.some((r) => pos >= r.start && pos < r.end);
122
+ }
123
+ /** 填充 docx 模板:body 段落低阈值、表格空格高阈值、可选追加明细表。 */
124
+ export function fillDocx(templatePath, filler, options = {}) {
125
+ // 硬门禁与源脚本一致:没有真实模板 docx 不得生成合同
126
+ const entriesXml = readDocumentXml(templatePath);
127
+ const log = [];
128
+ let xml = entriesXml;
129
+ // ---- 表格处理(先做,避免被段落低阈值抢填)
130
+ const tableRanges = topRanges(xml, 'w:tbl');
131
+ for (let ti = tableRanges.length - 1; ti >= 0; ti--) {
132
+ const range = tableRanges[ti];
133
+ const tableXml = xml.slice(range.start, range.end);
134
+ const patched = fillTable(tableXml, filler, log);
135
+ xml = xml.slice(0, range.start) + patched + xml.slice(range.end);
136
+ }
137
+ // ---- 正文段落(表格之外的顶层段落)
138
+ const paraRanges = collectRanges(xml, 'w:p').filter((r) => !isInside(r.start, topRanges(xml, 'w:tbl')));
139
+ for (let i = paraRanges.length - 1; i >= 0; i--) {
140
+ const r = paraRanges[i];
141
+ const pXml = xml.slice(r.start, r.end);
142
+ const full = paragraphFullText(pXml);
143
+ if (!full)
144
+ continue;
145
+ const { text, changed } = fillText(full, 60, filler, log);
146
+ if (changed) {
147
+ xml = xml.slice(0, r.start) + setParagraphText(pXml, text) + xml.slice(r.end);
148
+ }
149
+ }
150
+ // ---- 追加费用明细
151
+ if (options.detail !== 'none' && options.sections?.length) {
152
+ xml = appendDetailTable(xml, options.sections, options.amount ?? null);
153
+ }
154
+ const filled = log.filter((m) => !m.type.endsWith('_unfilled'));
155
+ const unfilled = log.filter((m) => m.type.endsWith('_unfilled'));
156
+ return { buffer: rewriteDocument(templatePath, xml), result: { filled, unfilled } };
157
+ }
158
+ function fillTable(tableXml, filler, log) {
159
+ // 逐行处理单元格,收集右侧标签
160
+ const rowRanges = collectRanges(tableXml, 'w:tr');
161
+ let patched = tableXml;
162
+ // 从后往前替换,避免位移
163
+ const cellLabels = [];
164
+ for (const row of rowRanges) {
165
+ const rowXml = tableXml.slice(row.start, row.end);
166
+ const cellRanges = collectRanges(rowXml, 'w:tc');
167
+ const cells = cellRanges.map((cr) => ({
168
+ start: cr.start,
169
+ xml: rowXml.slice(cr.start, cr.end),
170
+ }));
171
+ for (let ci = cells.length - 1; ci >= 0; ci--) {
172
+ const cell = cells[ci];
173
+ const cellText = paragraphFullText(cell.xml);
174
+ if (cellText && !hasBlank(cellText) && !hasPlaceholder(cellText))
175
+ continue;
176
+ let label = '';
177
+ for (let j = ci - 1; j >= 0; j--) {
178
+ const cand = cells[j];
179
+ const candText = paragraphFullText(cand.xml).trim();
180
+ if (candText && !hasBlank(candText) && !hasPlaceholder(candText)) {
181
+ label = candText.replace(/[::]$/u, '');
182
+ break;
183
+ }
184
+ }
185
+ if (!label)
186
+ continue;
187
+ cellLabels.push({ cellStart: row.start + cell.start, cellXml: cell.xml, label });
188
+ }
189
+ }
190
+ // cellStart 是相对 tableXml 的绝对位置;从后往前替换
191
+ cellLabels.sort((a, b) => b.cellStart - a.cellStart);
192
+ for (const { cellStart, cellXml, label } of cellLabels) {
193
+ const patchedCell = fillTableCell(cellXml, label, filler, log);
194
+ patched = patched.slice(0, cellStart) + patchedCell + patched.slice(cellStart + cellXml.length);
195
+ }
196
+ return patched;
197
+ }
198
+ function fillTableCell(cellXml, label, filler, log) {
199
+ const paraRanges = collectRanges(cellXml, 'w:p');
200
+ let patched = cellXml;
201
+ let hit = false;
202
+ for (let i = paraRanges.length - 1; i >= 0; i--) {
203
+ const r = paraRanges[i];
204
+ const pXml = cellXml.slice(r.start, r.end);
205
+ const full = paragraphFullText(pXml);
206
+ const { key, score } = filler.bestKey(label, 95);
207
+ const value = key ? filler.valOf(key) : '';
208
+ if (!value)
209
+ continue;
210
+ if (full.trim() === '') {
211
+ log.push({ label, key, score, type: 'cell' });
212
+ patched = patched.slice(0, r.start) + addRunToParagraph(pXml, value) + patched.slice(r.end);
213
+ hit = true;
214
+ break;
215
+ }
216
+ const { text, changed } = fillText(full, 95, filler, log);
217
+ if (changed) {
218
+ log.push({ label, key, score, type: 'cell' });
219
+ patched = patched.slice(0, r.start) + setParagraphText(pXml, text) + patched.slice(r.end);
220
+ hit = true;
221
+ }
222
+ break;
223
+ }
224
+ void hit;
225
+ return patched;
226
+ }
227
+ function addRunToParagraph(pXml, value) {
228
+ const run = `<w:r><w:t xml:space="preserve">${encodeXml(value)}</w:t></w:r>`;
229
+ const closeIdx = pXml.lastIndexOf('</w:p>');
230
+ if (closeIdx < 0)
231
+ return pXml;
232
+ return pXml.slice(0, closeIdx) + run + pXml.slice(closeIdx);
233
+ }
234
+ function appendDetailTable(xml, sections, amount) {
235
+ const border = '<w:tblBorders>' +
236
+ ['top', 'left', 'bottom', 'right', 'insideH', 'insideV']
237
+ .map((s) => `<w:${s} w:val="single" w:sz="4" w:space="0" w:color="000000"/>`)
238
+ .join('') +
239
+ '</w:tblBorders>';
240
+ const cell = (text, bold = false) => '<w:tc><w:tcPr/><w:p><w:r>' +
241
+ (bold ? '<w:rPr><w:b/></w:rPr>' : '') +
242
+ `<w:t xml:space="preserve">${encodeXml(text)}</w:t></w:r></w:p></w:tc>`;
243
+ const row = (...cells) => `<w:tr>${cells.join('')}</w:tr>`;
244
+ const rows = [row(cell('费用类别', true), cell('项目', true), cell('数量', true), cell('金额(元)', true))];
245
+ for (const sec of sections) {
246
+ for (const item of sec.items) {
247
+ rows.push(row(cell(`${sec.code}、${sec.title}`), cell(String(item.name ?? item.desc ?? '')), cell(item.qty === null || item.qty === undefined || item.qty === '' ? '' : String(item.qty)), cell(formatAmount(item.amount))));
248
+ }
249
+ if (sec.total !== null && sec.total !== undefined) {
250
+ rows.push(row(cell('小计'), cell(`${sec.code}、${sec.title}`), cell(''), cell(formatAmount(sec.total))));
251
+ }
252
+ }
253
+ if (amount !== null && amount !== '') {
254
+ rows.push(row(cell('合计'), cell('合同总金额'), cell(''), cell(formatAmount(amount))));
255
+ }
256
+ const pageBreak = '<w:p><w:r><w:br w:type="page"/></w:r></w:p>';
257
+ const heading = '<w:p><w:pPr><w:outlineLvl w:val="0"/></w:pPr><w:r><w:rPr><w:b/><w:sz w:val="32"/></w:rPr>' +
258
+ '<w:t xml:space="preserve">附件:产品及服务费用明细</w:t></w:r></w:p>';
259
+ const table = `<w:tbl><w:tblPr><w:tblW w:w="0" w:type="auto"/>${border}</w:tblPr>${rows.join('')}</w:tbl>`;
260
+ const sectPr = xml.lastIndexOf('<w:sectPr');
261
+ const insertAt = sectPr >= 0 ? sectPr : xml.lastIndexOf('</w:body>');
262
+ if (insertAt < 0)
263
+ return xml + pageBreak + heading + table;
264
+ return xml.slice(0, insertAt) + pageBreak + heading + table + xml.slice(insertAt);
265
+ }
266
+ function formatAmount(v) {
267
+ if (v === null || v === undefined || v === '')
268
+ return '';
269
+ const s = String(v).replace(/[,¥¥]/gu, '').trim();
270
+ const f = Number(s);
271
+ if (!Number.isFinite(f))
272
+ return String(v);
273
+ return f === Math.trunc(f) ? f.toLocaleString('en-US') : f.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
274
+ }
275
+ function readDocumentXml(templatePath) {
276
+ let buffer;
277
+ try {
278
+ buffer = readFileSync(templatePath);
279
+ }
280
+ catch {
281
+ throw new Error('合同模板缺失:必须先取到真实的 .docx 模板才能生成合同。取不到就停止并请用户提供模板文件;禁止把报价单内容直接生成成一份合同。');
282
+ }
283
+ try {
284
+ return readDocumentXmlFrom(buffer);
285
+ }
286
+ catch {
287
+ throw new Error('模板不是有效的 .docx(可能拿到的是 HTML 或压缩包):请重新取模板;禁止用非 docx 文件当模板,也禁止绕过模板直接生成合同。');
288
+ }
289
+ }
290
+ function readDocumentXmlFrom(buffer) {
291
+ return readZipEntry(buffer, 'word/document.xml');
292
+ }
293
+ function rewriteDocument(templatePath, documentXml) {
294
+ return rewriteZip(templatePath, { 'word/document.xml': documentXml });
295
+ }
@@ -0,0 +1,221 @@
1
+ /**
2
+ * 最小 xlsx 读取器(零依赖,机器生成的常规工作簿)。
3
+ *
4
+ * 输出与源脚本 quote_to_contract.py 对齐:
5
+ * - sheetNames:工作表名列表(供用户选择,禁止自动选)。
6
+ * - readGrid:返回每行每列的原始值 + 纵向合并的 text/num 两张映射,
7
+ * 供 parseQuote 的「文本列可继承、数值列只归首行」语义使用。
8
+ */
9
+ import { readZip, readZipEntry } from './zip.js';
10
+ const DATE_BUILTIN = new Set([14, 15, 16, 17, 18, 19, 20, 21, 22, 27, 30, 36, 45, 46, 47]);
11
+ function colToIndex(ref) {
12
+ let n = 0;
13
+ for (const ch of ref) {
14
+ if (ch >= 'A' && ch <= 'Z')
15
+ n = n * 26 + (ch.charCodeAt(0) - 64);
16
+ else
17
+ break;
18
+ }
19
+ return n;
20
+ }
21
+ function rowToNumber(ref) {
22
+ const m = /\d+/.exec(ref);
23
+ return m ? parseInt(m[0], 10) : 0;
24
+ }
25
+ function excelSerialToIso(serial) {
26
+ // Excel 序列:1900-01-01 = 1,含 1900 闰年 bug 偏移
27
+ const days = Math.floor(serial) - 25569;
28
+ const ms = days * 86400000;
29
+ const d = new Date(ms);
30
+ const iso = d.toISOString().slice(0, 10);
31
+ return iso;
32
+ }
33
+ export function listSheetNames(buffer) {
34
+ const workbook = readZipText(buffer, 'xl/workbook.xml');
35
+ const rels = readZipText(buffer, 'xl/_rels/workbook.xml.rels');
36
+ const relMap = new Map();
37
+ for (const m of rels.matchAll(/<Relationship[^>]*Id="([^"]+)"[^>]*Target="([^"]+)"[^>]*\/?>/gu)) {
38
+ relMap.set(m[1], m[2]);
39
+ }
40
+ const names = [];
41
+ for (const m of workbook.matchAll(/<sheet[^>]*name="([^"]*)"[^>]*r:id="([^"]+)"[^>]*\/?>/gu)) {
42
+ names.push(decodeXml(m[1]));
43
+ void relMap.get(m[2]);
44
+ }
45
+ return names;
46
+ }
47
+ function readZipText(buffer, name) {
48
+ return readZipEntry(buffer, name);
49
+ }
50
+ /** 解析 styles.xml,返回 cellXfs 中每个 xf 的 numFmtId 及自定义格式串。 */
51
+ function loadDateStyles(buffer) {
52
+ let styles = '';
53
+ try {
54
+ styles = readZipText(buffer, 'xl/styles.xml');
55
+ }
56
+ catch {
57
+ return () => false;
58
+ }
59
+ const customFormats = new Map();
60
+ for (const m of styles.matchAll(/<numFmt[^>]*numFmtId="(\d+)"[^>]*formatCode="([^"]*)"[^>]*\/?>/gu)) {
61
+ customFormats.set(parseInt(m[1], 10), decodeXml(m[2]));
62
+ }
63
+ const xfIds = [];
64
+ const cellXfs = styles.indexOf('<cellXfs');
65
+ if (cellXfs >= 0) {
66
+ const section = styles.slice(cellXfs, styles.indexOf('</cellXfs>', cellXfs));
67
+ for (const m of section.matchAll(/<xf[^>]*numFmtId="(\d+)"[^>]*\/?>/gu)) {
68
+ xfIds.push(parseInt(m[1], 10));
69
+ }
70
+ }
71
+ return (styleIndex) => {
72
+ const id = xfIds[styleIndex] ?? 0;
73
+ if (DATE_BUILTIN.has(id))
74
+ return true;
75
+ const custom = customFormats.get(id);
76
+ if (!custom)
77
+ return false;
78
+ // 排除纯时间格式:必须含年/月/日标记之一
79
+ return /[ymd]/i.test(custom.replace(/\[[^\]]*\]/g, ''));
80
+ };
81
+ }
82
+ export function readGrid(buffer, sheetIndex) {
83
+ const workbook = readZipText(buffer, 'xl/workbook.xml');
84
+ const rels = readZipText(buffer, 'xl/_rels/workbook.xml.rels');
85
+ const relMap = new Map();
86
+ for (const m of rels.matchAll(/<Relationship[^>]*Id="([^"]+)"[^>]*Target="([^"]+)"[^>]*\/?>/gu)) {
87
+ relMap.set(m[1], m[2]);
88
+ }
89
+ const sheetMatches = [...workbook.matchAll(/<sheet[^>]*r:id="([^"]+)"[^>]*\/?>/gu)];
90
+ const hit = sheetMatches[sheetIndex];
91
+ if (!hit)
92
+ throw new Error(`sheet 序号越界:${sheetIndex}`);
93
+ let target = relMap.get(hit[1]);
94
+ if (!target)
95
+ throw new Error('workbook.xml.rels 缺少 sheet 关系,无法定位工作表。');
96
+ if (target.startsWith('/'))
97
+ target = target.slice(1);
98
+ else if (!target.startsWith('xl/'))
99
+ target = 'xl/' + target;
100
+ // 共享字符串
101
+ const shared = [];
102
+ try {
103
+ const sst = readZipText(buffer, 'xl/sharedStrings.xml');
104
+ for (const m of sst.matchAll(/<si>([\s\S]*?)<\/si>/gu)) {
105
+ shared.push([...m[1].matchAll(/<t[^>]*>([\s\S]*?)<\/t>/gu)].map((t) => decodeXml(t[1])).join(''));
106
+ }
107
+ }
108
+ catch {
109
+ // 无共享字符串表:全部 inline
110
+ }
111
+ const isDate = loadDateStyles(buffer);
112
+ const sheetXml = readZipText(buffer, sheetName(target, buffer));
113
+ const values = new Map();
114
+ let maxRow = 0;
115
+ let maxCol = 0;
116
+ for (const cm of sheetXml.matchAll(/<c([^>]*?)\/>|<c([^>]*?)>([\s\S]*?)<\/c>/gu)) {
117
+ const attrs = cm[1] || cm[2];
118
+ const inner = cm[3] ?? '';
119
+ const ref = /r="([A-Z]+\d+)"/u.exec(attrs)?.[1];
120
+ if (!ref)
121
+ continue;
122
+ const row = rowToNumber(ref);
123
+ const col = colToIndex(ref);
124
+ const type = /t="([^"]+)"/u.exec(attrs)?.[1] ?? 'n';
125
+ const styleIndex = parseInt(/s="(\d+)"/u.exec(attrs)?.[1] ?? '0', 10);
126
+ let text = '';
127
+ let num = null;
128
+ let dateIso = null;
129
+ const v = /<v[^>]*>([\s\S]*?)<\/v>/u.exec(inner)?.[1];
130
+ if (type === 'inlineStr') {
131
+ text = [...inner.matchAll(/<t[^>]*>([\s\S]*?)<\/t>/gu)].map((t) => decodeXml(t[1])).join('');
132
+ }
133
+ else if (v !== undefined) {
134
+ if (type === 's') {
135
+ text = shared[parseInt(v, 10)] ?? '';
136
+ }
137
+ else if (type === 'str') {
138
+ text = decodeXml(v);
139
+ }
140
+ else if (type === 'b') {
141
+ text = v.trim() === '1' ? '1' : '0';
142
+ }
143
+ else {
144
+ const parsed = Number(v);
145
+ if (Number.isFinite(parsed)) {
146
+ num = parsed;
147
+ if (isDate(styleIndex) && parsed > 59) {
148
+ dateIso = excelSerialToIso(parsed);
149
+ text = dateIso;
150
+ }
151
+ else {
152
+ text = formatNumber(parsed);
153
+ }
154
+ }
155
+ else {
156
+ text = decodeXml(v);
157
+ }
158
+ }
159
+ }
160
+ if (!values.has(row))
161
+ values.set(row, new Map());
162
+ values.get(row).set(col, { text, num, dateIso });
163
+ if (row > maxRow)
164
+ maxRow = row;
165
+ if (col > maxCol)
166
+ maxCol = col;
167
+ }
168
+ // 合并单元格:纵向 -> 文本列全继承;数值列只归首行
169
+ const textMerged = new Map();
170
+ const numMerged = new Map();
171
+ for (const m of sheetXml.matchAll(/<mergeCell[^>]*ref="([A-Z]+\d+):([A-Z]+\d+)"/gu)) {
172
+ const c1 = colToIndex(m[1]);
173
+ const r1 = rowToNumber(m[1]);
174
+ const c2 = colToIndex(m[2]);
175
+ const r2 = rowToNumber(m[2]);
176
+ if (r2 <= r1)
177
+ continue; // 纯横向合并,忽略
178
+ const top = values.get(r1)?.get(c1) ?? { text: '', num: null, dateIso: null };
179
+ for (let r = r1; r <= r2; r++) {
180
+ for (let c = c1; c <= c2; c++) {
181
+ if (!values.get(r)?.has(c)) {
182
+ textMerged.set(`${r}:${c}`, top);
183
+ }
184
+ }
185
+ }
186
+ for (let c = c1; c <= c2; c++)
187
+ numMerged.set(`${r1}:${c}`, top);
188
+ }
189
+ return { values, textMerged, numMerged, maxRow, maxCol };
190
+ }
191
+ function sheetName(target, buffer) {
192
+ const names = new Set(readZip(buffer).map((e) => e.name));
193
+ if (names.has(target))
194
+ return target;
195
+ // worksheets/sheetN.xml 大小写或路径差异兜底
196
+ const alt = [...names].find((n) => n.toLowerCase() === target.toLowerCase());
197
+ if (alt)
198
+ return alt;
199
+ throw new Error(`找不到工作表 XML:${target}`);
200
+ }
201
+ function formatNumber(n) {
202
+ if (Number.isInteger(n))
203
+ return String(n);
204
+ return String(parseFloat(n.toPrecision(15)));
205
+ }
206
+ export function decodeXml(s) {
207
+ return s
208
+ .replace(/&lt;/gu, '<')
209
+ .replace(/&gt;/gu, '>')
210
+ .replace(/&quot;/gu, '"')
211
+ .replace(/&apos;/gu, "'")
212
+ .replace(/&#(\d+);/gu, (_, d) => String.fromCodePoint(parseInt(d, 10)))
213
+ .replace(/&amp;/gu, '&');
214
+ }
215
+ export function encodeXml(s) {
216
+ return s
217
+ .replace(/&/gu, '&amp;')
218
+ .replace(/</gu, '&lt;')
219
+ .replace(/>/gu, '&gt;')
220
+ .replace(/"/gu, '&quot;');
221
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * 最小 OOXML ZIP 读写层(零运行时依赖)。
3
+ *
4
+ * 只覆盖 docx/xlsx 实际用到的形态:
5
+ * - 读:解析 central directory,STORED 与 DEFLATE 均支持(node:zlib inflateRawSync)。
6
+ * - 写:全部 STORED 无压缩写出(ZIP 规范允许,Word/Excel 正常打开)。
7
+ * - 改:读取原包全部条目,替换指定条目内容后重写,保留其余条目原样。
8
+ */
9
+ import { inflateRawSync } from 'node:zlib';
10
+ import { readFileSync } from 'node:fs';
11
+ const LOCAL_SIG = 0x04034b50;
12
+ const CENTRAL_SIG = 0x02014b50;
13
+ const EOCD_SIG = 0x06054b50;
14
+ function readDosDateTime(buf, offset) {
15
+ return { time: buf.readUInt16LE(offset), date: buf.readUInt16LE(offset + 2) };
16
+ }
17
+ export function readZip(buffer) {
18
+ // 定位 EOCD(容忍注释)
19
+ let eocd = -1;
20
+ for (let i = buffer.length - 22; i >= Math.max(0, buffer.length - 22 - 65536); i--) {
21
+ if (buffer.readUInt32LE(i) === EOCD_SIG) {
22
+ eocd = i;
23
+ break;
24
+ }
25
+ }
26
+ if (eocd < 0)
27
+ throw new Error('不是有效的 ZIP/OOXML 文件(找不到 EOCD)。');
28
+ const count = buffer.readUInt16LE(eocd + 10);
29
+ let offset = buffer.readUInt32LE(eocd + 16);
30
+ const entries = [];
31
+ for (let n = 0; n < count; n++) {
32
+ if (buffer.readUInt32LE(offset) !== CENTRAL_SIG)
33
+ throw new Error('ZIP central directory 损坏。');
34
+ const method = buffer.readUInt16LE(offset + 10);
35
+ const compressedSize = buffer.readUInt32LE(offset + 20);
36
+ const nameLen = buffer.readUInt16LE(offset + 28);
37
+ const extraLen = buffer.readUInt16LE(offset + 30);
38
+ const commentLen = buffer.readUInt16LE(offset + 32);
39
+ const localOffset = buffer.readUInt32LE(offset + 42);
40
+ const name = buffer.toString('utf8', offset + 46, offset + 46 + nameLen);
41
+ // 从 local header 取真实数据起点(local extra 长度可能不同)
42
+ const localNameLen = buffer.readUInt16LE(localOffset + 26);
43
+ const localExtraLen = buffer.readUInt16LE(localOffset + 28);
44
+ const dataStart = localOffset + 30 + localNameLen + localExtraLen;
45
+ const raw = buffer.subarray(dataStart, dataStart + compressedSize);
46
+ const data = method === 0 ? Buffer.from(raw) : inflateRawSync(raw);
47
+ entries.push({ name, data });
48
+ offset += 46 + nameLen + extraLen + commentLen;
49
+ }
50
+ return entries;
51
+ }
52
+ export function readZipEntry(buffer, name) {
53
+ const hit = readZip(buffer).find((e) => e.name === name);
54
+ if (!hit)
55
+ throw new Error(`OOXML 包缺少 ${name},不是有效的 Office 文档。`);
56
+ return hit.data.toString('utf8');
57
+ }
58
+ function crc32(data) {
59
+ let table = crc32Table;
60
+ let crc = 0xffffffff;
61
+ for (let i = 0; i < data.length; i++) {
62
+ crc = (crc >>> 8) ^ table[(crc ^ data[i]) & 0xff];
63
+ }
64
+ return (crc ^ 0xffffffff) >>> 0;
65
+ }
66
+ const crc32Table = (() => {
67
+ const table = new Int32Array(256);
68
+ for (let n = 0; n < 256; n++) {
69
+ let c = n;
70
+ for (let k = 0; k < 8; k++)
71
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
72
+ table[n] = c;
73
+ }
74
+ return table;
75
+ })();
76
+ function dosDateTime() {
77
+ const d = new Date();
78
+ return {
79
+ time: (d.getHours() << 11) | (d.getMinutes() << 5) | Math.floor(d.getSeconds() / 2),
80
+ date: (((d.getFullYear() - 1980) & 0x7f) << 9) | ((d.getMonth() + 1) << 5) | d.getDate(),
81
+ };
82
+ }
83
+ export function writeZip(entries) {
84
+ const { time, date } = dosDateTime();
85
+ const chunks = [];
86
+ const central = [];
87
+ let offset = 0;
88
+ for (const entry of entries) {
89
+ const nameBuf = Buffer.from(entry.name, 'utf8');
90
+ const crc = crc32(entry.data);
91
+ const local = Buffer.alloc(30);
92
+ local.writeUInt32LE(LOCAL_SIG, 0);
93
+ local.writeUInt16LE(20, 4); // version
94
+ local.writeUInt16LE(0, 6); // flags
95
+ local.writeUInt16LE(0, 8); // STORED
96
+ local.writeUInt16LE(time, 10);
97
+ local.writeUInt16LE(date, 12);
98
+ local.writeUInt32LE(crc, 14);
99
+ local.writeUInt32LE(entry.data.length, 18);
100
+ local.writeUInt32LE(entry.data.length, 22);
101
+ local.writeUInt16LE(nameBuf.length, 26);
102
+ local.writeUInt16LE(0, 28);
103
+ chunks.push(local, nameBuf, entry.data);
104
+ const cen = Buffer.alloc(46);
105
+ cen.writeUInt32LE(CENTRAL_SIG, 0);
106
+ cen.writeUInt16LE(20, 4);
107
+ cen.writeUInt16LE(20, 6);
108
+ cen.writeUInt16LE(0, 8);
109
+ cen.writeUInt16LE(0, 10);
110
+ cen.writeUInt16LE(time, 12);
111
+ cen.writeUInt16LE(date, 14);
112
+ cen.writeUInt32LE(crc, 16);
113
+ cen.writeUInt32LE(entry.data.length, 20);
114
+ cen.writeUInt32LE(entry.data.length, 24);
115
+ cen.writeUInt16LE(nameBuf.length, 28);
116
+ cen.writeUInt16LE(0, 30);
117
+ cen.writeUInt16LE(0, 32);
118
+ cen.writeUInt16LE(0, 34);
119
+ cen.writeUInt16LE(0, 36);
120
+ cen.writeUInt32LE(0, 38);
121
+ cen.writeUInt32LE(offset, 42);
122
+ central.push(cen, nameBuf);
123
+ offset += 30 + nameBuf.length + entry.data.length;
124
+ }
125
+ const centralBuf = Buffer.concat(central);
126
+ const eocd = Buffer.alloc(22);
127
+ eocd.writeUInt32LE(EOCD_SIG, 0);
128
+ eocd.writeUInt16LE(0, 4);
129
+ eocd.writeUInt16LE(0, 6);
130
+ eocd.writeUInt16LE(entries.length, 8);
131
+ eocd.writeUInt16LE(entries.length, 10);
132
+ eocd.writeUInt32LE(centralBuf.length, 12);
133
+ eocd.writeUInt32LE(offset, 16);
134
+ eocd.writeUInt16LE(0, 20);
135
+ return Buffer.concat([...chunks, centralBuf, eocd]);
136
+ }
137
+ /** 读原包、替换/追加条目、重写。 */
138
+ export function rewriteZip(sourcePath, replacements) {
139
+ const entries = readZip(readFileSync(sourcePath));
140
+ const out = [];
141
+ const done = new Set();
142
+ for (const entry of entries) {
143
+ if (entry.name in replacements) {
144
+ const data = replacements[entry.name];
145
+ out.push({ name: entry.name, data: typeof data === 'string' ? Buffer.from(data, 'utf8') : data });
146
+ done.add(entry.name);
147
+ }
148
+ else {
149
+ out.push(entry);
150
+ }
151
+ }
152
+ for (const [name, data] of Object.entries(replacements)) {
153
+ if (!done.has(name)) {
154
+ out.push({ name, data: typeof data === 'string' ? Buffer.from(data, 'utf8') : data });
155
+ }
156
+ }
157
+ return writeZip(out);
158
+ }