weaver-work-cli 0.1.28 → 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 (219) 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/host.js +2 -1
  105. package/dist/shortcuts/invoice/manifest.js +36 -12
  106. package/dist/shortcuts/invoice/operations/issuing-qys.js +132 -36
  107. package/dist/shortcuts/invoice/operations/reim-auto.js +9 -0
  108. package/dist/shortcuts/invoice/operations/reim.js +81 -5
  109. package/dist/shortcuts/invoice/operations/shared.js +12 -5
  110. package/dist/shortcuts/meeting/host.js +45 -3
  111. package/dist/shortcuts/meeting/invitation/docx.js +442 -0
  112. package/dist/shortcuts/meeting/operations/attachment-content.js +418 -0
  113. package/dist/shortcuts/meeting/operations/attachment.js +442 -0
  114. package/dist/shortcuts/meeting/operations/eb/cache.js +57 -0
  115. package/dist/shortcuts/meeting/operations/eb/catalog.js +343 -28
  116. package/dist/shortcuts/meeting/operations/eb/conflict.js +372 -11
  117. package/dist/shortcuts/meeting/operations/eb/create-flow.js +710 -0
  118. package/dist/shortcuts/meeting/operations/eb/decision.js +47 -1
  119. package/dist/shortcuts/meeting/operations/eb/factory.js +129 -11
  120. package/dist/shortcuts/meeting/operations/eb/index.js +122 -2
  121. package/dist/shortcuts/meeting/operations/eb/room-recommend.js +378 -0
  122. package/dist/shortcuts/meeting/operations/eb/room-summary.js +84 -0
  123. package/dist/shortcuts/meeting/operations/eb/schedule.js +450 -0
  124. package/dist/shortcuts/meeting/operations/from-group-chat.js +576 -0
  125. package/dist/shortcuts/meeting/operations/invitation.js +458 -0
  126. package/dist/shortcuts/meeting/operations/minute-content.js +266 -0
  127. package/dist/shortcuts/meeting/operations/pipeline.js +489 -0
  128. package/dist/shortcuts/meeting/operations/qrcode.js +131 -0
  129. package/dist/shortcuts/meeting/operations/receipts.js +1 -1
  130. package/dist/shortcuts/meeting/operations/registry.js +18 -0
  131. package/dist/shortcuts/meeting/operations/summary.js +276 -0
  132. package/dist/shortcuts/meeting/operations/task-notify.js +325 -0
  133. package/dist/shortcuts/meeting/operations.js +42 -1
  134. package/dist/shortcuts/meeting/qr/encoder.js +427 -0
  135. package/dist/shortcuts/meeting/qr/render.js +103 -0
  136. package/dist/shortcuts/workflow/index.js +3 -0
  137. package/dist/shortcuts/workflow/operations/form.js +17 -5
  138. package/dist/shortcuts/workflow/operations/read.js +13 -6
  139. package/dist/shortcuts/workflow/operations/registry.js +2 -0
  140. package/dist/shortcuts/workflow/operations/todoSmartConvert/businessRules.js +331 -0
  141. package/dist/shortcuts/workflow/operations/todoSmartConvert/detailFieldRules.js +133 -0
  142. package/dist/shortcuts/workflow/operations/todoSmartConvert/summaryCache.js +326 -0
  143. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailCache.js +113 -0
  144. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailFetcher.js +259 -0
  145. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoListRows.js +64 -0
  146. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoOutputPlanner.js +28 -0
  147. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.aggregate.js +70 -0
  148. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +267 -0
  149. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.core.js +392 -0
  150. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.detail.js +165 -0
  151. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.group.js +448 -0
  152. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +348 -0
  153. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +222 -0
  154. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.pipeline.js +66 -0
  155. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +502 -0
  156. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +540 -0
  157. package/docs/_catalog.md +1 -0
  158. package/docs/contract.md +39 -4
  159. package/docs/invoice.md +7 -7
  160. package/docs/todo-smart-convert/README.md +171 -0
  161. package/docs/todo-smart-convert/data-contract.md +236 -0
  162. package/docs/todo-smart-convert/mock/ai-todo-query-target.json +209 -0
  163. package/docs/todo-smart-convert/subgroup-rules.md +245 -0
  164. package/package.json +3 -2
  165. package/scripts/bundle.mjs +63 -0
  166. package/scripts/package-skill.mjs +1 -1
  167. package/skill-template/business-info.json +25 -4
  168. package/skill-template/domains/jinchengda.md +16 -3
  169. package/skill-template/domains/yepiaotong.md +3 -3
  170. package/skills/weaver-e10-jinchengda/SKILL.md +111 -31
  171. package/skills/weaver-e10-jinchengda/references/contract-agent-entry.md +7 -13
  172. package/skills/weaver-e10-jinchengda/references/contract-ai-assistant.md +150 -0
  173. package/skills/weaver-e10-jinchengda/references/contract-ai-review.md +87 -0
  174. package/skills/weaver-e10-jinchengda/references/contract-clause-library-crud.md +111 -0
  175. package/skills/weaver-e10-jinchengda/references/contract-clause-recommender.md +0 -15
  176. package/skills/weaver-e10-jinchengda/references/contract-clause-reference-save.md +0 -13
  177. package/skills/weaver-e10-jinchengda/references/contract-credit-page.md +50 -0
  178. package/skills/weaver-e10-jinchengda/references/contract-disabled-capabilities.md +4 -2
  179. package/skills/weaver-e10-jinchengda/references/contract-doc-compare.md +74 -0
  180. package/skills/weaver-e10-jinchengda/references/contract-flow-launch.md +84 -0
  181. package/skills/weaver-e10-jinchengda/references/contract-performance-query.md +29 -0
  182. package/skills/weaver-e10-jinchengda/references/contract-quote-to-contract.md +28 -0
  183. package/skills/weaver-e10-jinchengda/references/contract-request-status.md +2 -21
  184. package/skills/weaver-e10-jinchengda/references/contract-request-write.md +0 -24
  185. package/skills/weaver-e10-jinchengda/references/contract-signing-subjects.md +0 -15
  186. package/skills/weaver-e10-jinchengda/references/contract-subject-create.md +0 -17
  187. package/skills/weaver-e10-jinchengda/references/contract-template-recommend.md +70 -0
  188. package/skills/weaver-e10-jinchengda/references/contract-template-selector.md +0 -15
  189. package/skills/weaver-e10-jinchengda/references/contract-workflow-discovery.md +0 -15
  190. package/skills/weaver-e10-jinchengda/references/source-manifest.json +1168 -152
  191. package/skills/weaver-e10-meeting/SKILL.md +493 -17
  192. package/skills/weaver-e10-meeting/references/meeting-change.md +4 -4
  193. package/skills/weaver-e10-meeting/references/meeting-create.md +6 -6
  194. package/skills/weaver-e10-meeting/references/meeting-eb.md +503 -27
  195. package/skills/weaver-e10-meeting/references/meeting-invitation.md +113 -0
  196. package/skills/weaver-e10-meeting/references/meeting-lifecycle.md +6 -6
  197. package/skills/weaver-e10-meeting/references/meeting-minute-file.md +425 -0
  198. package/skills/weaver-e10-meeting/references/meeting-queries.md +4 -4
  199. package/skills/weaver-e10-meeting/references/meeting-receipts-signs.md +15 -15
  200. package/skills/weaver-e10-meeting/references/meeting-verification.md +19 -11
  201. package/skills/weaver-e10-meeting.zip +0 -0
  202. package/skills/weaver-e10-workflow/SKILL.md +23 -1
  203. package/skills/weaver-e10-workflow/references/todo-smart-convert.md +282 -0
  204. package/skills/weaver-e10-workflow/scripts/todo-summary.cjs +695 -0
  205. package/skills/weaver-e10-yepiaotong/SKILL.md +15 -11
  206. package/skills/weaver-e10-yepiaotong/references/invoice-enterprise-list.md +1 -1
  207. package/skills/weaver-e10-yepiaotong/references/invoice-issuing-examples.md +10 -7
  208. package/skills/weaver-e10-yepiaotong/references/invoice-issuing-form-fill-rules.md +41 -14
  209. package/skills/weaver-e10-yepiaotong/references/invoice-issuing-workflow.md +54 -26
  210. package/skills/weaver-e10-yepiaotong/references/invoice-personal-list.md +6 -5
  211. package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +14 -11
  212. package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +97 -56
  213. package/skills/weaver-e10-yepiaotong/references/reim-examples.md +84 -60
  214. package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +142 -105
  215. package/skills/weaver-e10-yepiaotong/references/reim-matching-rules.md +53 -194
  216. package/skills/weaver-e10-yepiaotong/references/reim-phone-subsidy-rules.md +7 -7
  217. package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +164 -72
  218. package/skills/weaver-e10-yepiaotong/references/source-manifest.json +8 -2
  219. package/templates/meeting/invitation.docx +0 -0
@@ -0,0 +1,695 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 待办智能汇总 —— 固定流水线脚本(供 weaver-e10-workflow skill 使用)
4
+ *
5
+ * 一句话:把「取数 → 过滤 → (模型分组) → 组装 → (模型写摘要) → 导出校验」固化成三段命令,
6
+ * 避免每次现场拼脚本,也避免多跑一次 stage=group 进程(每次 CLI 启动约 6~7s)。
7
+ *
8
+ * 用法:
9
+ * node todo-summary.cjs prep --limit 100 [--exclude 内部留言] [--days 365] [--out DIR]
10
+ * → 取数、按 workflowName 剔除指定流程、打印分组素材(模型据此做二次分组)
11
+ * node todo-summary.cjs build --dir DIR --plan aiGroups.json
12
+ * → 校验 1..count 全覆盖 → smartGroup;若还有待写摘要则打印,否则直接导出终态
13
+ * node todo-summary.cjs finish --dir DIR [--summaries aiSummaries.json]
14
+ * → 回填摘要(可选)→ 导出终态 JSON → 打印结构自检与耗时
15
+ *
16
+ * 铁律:二次分组每轮都必须由模型现场产出 aiGroups,本脚本只做校验与组装,不做任何复用/缓存。
17
+ */
18
+ const { execFileSync } = require('node:child_process');
19
+ const fs = require('node:fs');
20
+ const path = require('node:path');
21
+
22
+ const CLI = process.env.WEAVER_CLI || 'weaver-work-cli';
23
+
24
+ /**
25
+ * 宿主(WorkBuddy)会给每个 node 子进程注入
26
+ * `NODE_OPTIONS=--require=.../node-language-shim.cjs`,该 shim 对**每次 fs 读取**加约 10~16ms 审计:
27
+ * 本 CLI 启动要读 387 个模块 ⇒ 每次进程 ~3.9s,3 个阶段就是 12s+(实测 22~28s 的异常也来自它)。
28
+ *
29
+ * 这里启动 CLI 时只摘掉 shim 这一项,其它 NODE_OPTIONS 原样保留。
30
+ * 实测:`--version` 4.96s → 0.16s,`workflow schema` 7.23s → 0.19s(2026-10-27)。
31
+ * 需要保留 shim 时设 `WEAVER_WORK_CLI_KEEP_NODE_SHIM=1`。
32
+ */
33
+ function cliEnv() {
34
+ const env = { ...process.env };
35
+ if (env.WEAVER_WORK_CLI_KEEP_NODE_SHIM) return env;
36
+ const raw = env.NODE_OPTIONS || '';
37
+ if (!raw.includes('node-language-shim')) return env;
38
+ const cleaned = raw
39
+ // `--require="…/node-language-shim.cjs"` 或 `--require …/node-language-shim.cjs` 两种写法
40
+ .replace(/--require\s*=?\s*("[^"]*node-language-shim\.cjs"|'[^']*node-language-shim\.cjs'|[^\s"']*node-language-shim\.cjs)/g, '')
41
+ .trim();
42
+ if (cleaned) env.NODE_OPTIONS = cleaned;
43
+ else delete env.NODE_OPTIONS;
44
+ return env;
45
+ }
46
+
47
+ function parseArgs(argv) {
48
+ const out = { _: [] };
49
+ for (let i = 0; i < argv.length; i += 1) {
50
+ const token = argv[i];
51
+ if (token.startsWith('--')) {
52
+ const key = token.slice(2);
53
+ const next = argv[i + 1];
54
+ if (next === undefined || next.startsWith('--')) out[key] = true;
55
+ else { out[key] = next; i += 1; }
56
+ } else out._.push(token);
57
+ }
58
+ return out;
59
+ }
60
+
61
+ function cli(args) {
62
+ const raw = execFileSync(CLI, ['--json', 'workflow', 'run', 'workflow.todoSmartConvert', ...args], {
63
+ encoding: 'utf8',
64
+ maxBuffer: 512 * 1024 * 1024,
65
+ env: cliEnv(),
66
+ });
67
+ const parsed = JSON.parse(raw);
68
+ if (!parsed.ok) {
69
+ const error = parsed.error || {};
70
+ console.error(`✖ ${error.subtype || error.type || 'error'}: ${error.message || JSON.stringify(parsed)}`);
71
+ process.exit(1);
72
+ }
73
+ return parsed;
74
+ }
75
+
76
+ function runStage(input, dir, name) {
77
+ const file = path.join(dir, `in-${name}.json`);
78
+ fs.writeFileSync(file, JSON.stringify(input));
79
+ return cli(['--input', file, '--allow-file-input']);
80
+ }
81
+
82
+ function readJson(file) {
83
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
84
+ }
85
+
86
+ function fmt(ms) {
87
+ return `${(ms / 1000).toFixed(1)}s`;
88
+ }
89
+
90
+ /** 从结果信封里取某个 stage 的 **CLI 内部**耗时(不含 node 启动);取不到返回 null。 */
91
+ function cliStageMs(result, stage) {
92
+ const steps = (result && result.meta && result.meta.timing && result.meta.timing.steps) || [];
93
+ const hit = steps.filter((step) => step && step.name === stage).pop();
94
+ return hit && Number.isFinite(hit.durationMs) ? hit.durationMs : null;
95
+ }
96
+
97
+ function nowIso() {
98
+ return new Date().toISOString();
99
+ }
100
+
101
+ function groupStats(wfIdGroup) {
102
+ let covered = 0;
103
+ let singles = 0;
104
+ let empty = 0;
105
+ const rows = [];
106
+ for (const group of wfIdGroup) {
107
+ const items = group.displayItems || [];
108
+ const subs = items.filter((x) => x.type === 'group');
109
+ const one = items.filter((x) => x.type === 'request');
110
+ covered += subs.reduce((sum, x) => sum + x.count, 0);
111
+ singles += one.length;
112
+ for (const item of items) {
113
+ const summary = item.type === 'group' ? item.summary : item.requestData.summary;
114
+ if (!summary) empty += 1;
115
+ }
116
+ rows.push({
117
+ title: group.title,
118
+ count: group.count,
119
+ display: subs.map((x) => `${x.title}(${x.count})`).join('、') || '-',
120
+ singles: one.length,
121
+ });
122
+ }
123
+ return { covered, singles, empty, rows };
124
+ }
125
+
126
+ /**
127
+ * 导出最终结构。
128
+ *
129
+ * 交付形态(用户 2026-10-22 硬规则):**只在对话里输出 JSON + 耗时,不产物理文件**。
130
+ * 所以默认走「stdout-only」:把最终结构的 JSON 打到 stdout 的 `[JSON_BEGIN]/[JSON_END]` 之间,
131
+ * 由 Agent 原样贴进回复;只有显式传 `--file` 才写成 `待办汇总-*.json`(兼容旧用法/调试)。
132
+ *
133
+ * 顶层键固定三个:`smartSummaryTitle` / `totalCount` / `wfIdGroup`(用户 2026-10-25 要求加
134
+ * `totalCount` = 当前这批数据的总条数,与各大分组 `count` 之和一致)。
135
+ */
136
+ /**
137
+ * 生成本轮耗时说明(markdown 文本)。数据全部来自 `state.json`(各阶段墙钟 / CLI 内部耗时)
138
+ * 与最终结果,所以「落 JSON + 耗时摘要」可以**一次调用**完成,不必再单独跑一轮查询。
139
+ *
140
+ * 2026-10-27 起:文本**不再落盘成 md 交付物**,只在 `--timing-report` 时取「结论」段打到控制台;
141
+ * 同日起**请求级埋点已移除**(`requestTiming` 模块删除),只报「墙钟 vs CLI 内部」两个口径。
142
+ *
143
+ * 两个口径必须分开报,否则会误判瓶颈:
144
+ * - 墙钟:含 node 启动 + 模块加载;
145
+ * - CLI 内部:只含业务(请求 + 计算)。
146
+ */
147
+ function buildTimingReport(state, result) {
148
+ const stages = [
149
+ { name: 'collect(取数)', wall: state.collectWallMs, inner: state.collectMs },
150
+ { name: 'smartGroup(分组)', wall: state.smartGroupWallMs, inner: state.smartGroupInternalMs },
151
+ { name: 'summary(摘要)', wall: state.summaryWallMs, inner: state.summaryInternalMs },
152
+ { name: 'groupSummary(组摘要回填)', wall: state.groupSummaryWallMs, inner: state.groupSummaryInternalMs },
153
+ ].filter((stage) => Number.isFinite(stage.wall) && stage.wall > 0);
154
+ const total = (pick) => stages.reduce((acc, stage) => acc + (Number(pick(stage)) || 0), 0);
155
+ const wallMs = total((stage) => stage.wall);
156
+ const innerMs = total((stage) => stage.inner);
157
+ const pct = (part) => (wallMs > 0 ? `${((part / wallMs) * 100).toFixed(0)}%` : '-');
158
+ const scope = state.days ? `近 ${state.days} 天`
159
+ : (state.from && state.to ? `${state.from} ~ ${state.to}` : `前 ${state.limit} 条`);
160
+ const data = result.data;
161
+ const lines = [];
162
+ lines.push('# 待办汇总耗时说明', '');
163
+ lines.push(`> 范围:${scope}|${data.totalCount} 条待办 / ${data.wfIdGroup.length} 组`);
164
+ lines.push(`> 生成:${new Date().toISOString().slice(0, 19).replace('T', ' ')} UTC`);
165
+ lines.push('', '## 1. 结论', '');
166
+ lines.push(`- 总墙钟 **${fmt(wallMs)}**,其中**真实业务 ${fmt(innerMs)}(${pct(innerMs)})**,进程启动与模块加载占 ${pct(wallMs - innerMs)}。`);
167
+ lines.push('- ⚠️ 本报告**只统计 CLI 执行链路**。端到端(用户看到的「已完成 X」)还要叠加模型往返——每步 20~35s,通常远大于这里的总墙钟。');
168
+ lines.push('', '## 2. 分阶段', '');
169
+ lines.push('| 阶段 | 墙钟 | CLI 内部 | 业务占比 |');
170
+ lines.push('|---|---|---|---|');
171
+ for (const stage of stages) {
172
+ lines.push(`| ${stage.name} | ${fmt(stage.wall)} | ${Number.isFinite(stage.inner) ? fmt(stage.inner) : '—'} | ${pct(stage.inner)} |`);
173
+ }
174
+ lines.push(`| **合计** | **${fmt(wallMs)}** | **${fmt(innerMs)}** | **${pct(innerMs)}** |`);
175
+ lines.push('', '## 3. 优化提示', '');
176
+ lines.push('- 若「业务占比」很低:瓶颈在**CLI 进程启动**。根因是宿主注入的 node shim 对每次 fs 读取加 ~10ms 审计(387 个模块 ≈ 4s/次);本脚本已自动摘掉 shim,若这里仍看到 4s+,检查是否设了 `WEAVER_WORK_CLI_KEEP_NODE_SHIM=1`。');
177
+ lines.push('- 「待写摘要为 0」时脚本会直接导出,不会白跑一次 finish。');
178
+ lines.push('', '## 4. 复现', '');
179
+ lines.push('```bash');
180
+ lines.push('node todo-summary.cjs run --days 30 --limit 500 --out /tmp/t --file --timing-report');
181
+ lines.push('```');
182
+ return `${lines.join('\n')}\n`;
183
+ }
184
+
185
+ /**
186
+ * 取当前认证环境的 `baseUrl`(`weaver-work-cli --json auth status`),优先用 state.json 缓存,
187
+ * 避免同一次流水线里重复启动 CLI。browsertool 载荷的 `src` 必须跟着认证环境走,**禁止写死租户域名**。
188
+ */
189
+ function resolveBaseUrl(state, dir) {
190
+ if (state.baseUrl) return state.baseUrl;
191
+ let baseUrl = '';
192
+ try {
193
+ const raw = execFileSync(CLI, ['--json', 'auth', 'status'], {
194
+ encoding: 'utf8',
195
+ maxBuffer: 16 * 1024 * 1024,
196
+ env: cliEnv(),
197
+ });
198
+ const parsed = JSON.parse(raw);
199
+ baseUrl = (parsed && ((parsed.data && parsed.data.baseUrl) || parsed.baseUrl)) || '';
200
+ } catch (error) {
201
+ baseUrl = '';
202
+ }
203
+ if (!baseUrl) {
204
+ console.error('✖ 取不到认证环境的 baseUrl(weaver-work-cli --json auth status),src 不能写死租户');
205
+ process.exit(1);
206
+ }
207
+ state.baseUrl = String(baseUrl).replace(/\/+$/, '');
208
+ fs.writeFileSync(path.join(dir, 'state.json'), JSON.stringify(state));
209
+ return state.baseUrl;
210
+ }
211
+
212
+ /**
213
+ * 把最终三键对象包成 browsertool 组件载荷(用户 2026-10-27 要求)。
214
+ * - `params.data` 放三键本身;`src` = 当前认证环境 baseUrl + `/workflow/list/todo`;
215
+ * - 数据段沿用调用方的紧凑文本(`requestData` 单行),只加缩进前缀、不重新序列化,
216
+ * 避免几千行 requestData 被展开成 4 倍体积;
217
+ * - 包裹完成后立刻 `JSON.parse` 自检,坏载荷不交给用户。
218
+ */
219
+ function wrapBrowseToolPayload(dataText, baseUrl) {
220
+ const indentTail = (text, spaces) => text.split('\n')
221
+ .map((line, index) => (index === 0 ? line : ' '.repeat(spaces) + line))
222
+ .join('\n');
223
+ const src = `${String(baseUrl).replace(/\/+$/, '')}/workflow/list/todo`;
224
+ const text = [
225
+ '{',
226
+ ' "version": "1.0",',
227
+ ' "callId": "call-1",',
228
+ ' "toolId": "todo_summary",',
229
+ ' "arguments": {',
230
+ ' "type": "e10",',
231
+ ' "title": "待办摘要汇总",',
232
+ ` "src": ${JSON.stringify(src)},`,
233
+ ' "sdkRenderType": "drawer",',
234
+ ' "sdkOptions": {',
235
+ ' "compName": "ui.CorsComponent",',
236
+ ' "routerType": "MemoryRouter",',
237
+ ' "routerProps_pc": { "initialEntries": ["/"] },',
238
+ ' "routerProps_mobile": { "initialEntries": ["/"] },',
239
+ ' "params_pc": { "app": "@weapp/workflow", "compName": "Comp.WflTodoSummary", "clientType": "pc" },',
240
+ ' "params_mobile": { "app": "@weapp/workflow", "compName": "Comp.WflTodoSummary", "clientType": "mobile" },',
241
+ ' "params": {',
242
+ ` "data": ${indentTail(dataText, 8)}`,
243
+ ' }',
244
+ ' }',
245
+ ' }',
246
+ '}',
247
+ ].join('\n');
248
+ JSON.parse(text);
249
+ return `${text}\n`;
250
+ }
251
+
252
+ function exportFinal(result, dir, state, kept, opts = {}) {
253
+ const { limit, from, to } = state;
254
+ const excluded = (state.exclude || []).join('/');
255
+ const data = result.data;
256
+ const groupTotal = (data.wfIdGroup || []).reduce((sum, g) => sum + (Number(g.count) || 0), 0);
257
+ // CLI 已给 totalCount;缺了(旧版 CLI)就按各组 count 求和兜底,保证三个键恒定存在。
258
+ const totalCount = Number(data.totalCount ?? groupTotal) || 0;
259
+ const payload = {
260
+ smartSummaryTitle: data.smartSummaryTitle,
261
+ totalCount,
262
+ wfIdGroup: data.wfIdGroup,
263
+ };
264
+ const stamp = new Date().toISOString().slice(0, 10);
265
+ const scope = from && to
266
+ ? `${from}~${to}`
267
+ : limit
268
+ ? (kept === limit ? `前${limit}条` : `前${limit}条-剩${kept}条`)
269
+ : `全部${kept}条`;
270
+ const stampPart = from && to ? '' : `-${stamp}`;
271
+ const finalFile = path.join(dir, `待办汇总-${scope}${excluded ? `-排除${excluded}` : ''}${stampPart}.json`);
272
+ // browsertool 载荷的 src 跟着当前认证环境走(用户 2026-10-27 要求不写死租户)。
273
+ const baseUrl = resolveBaseUrl(state, dir);
274
+ const stats = groupStats(data.wfIdGroup);
275
+ const total = groupTotal;
276
+ console.log('\n最终结构(共 %d 条 / %d 组|totalCount=%d):', total, data.wfIdGroup.length, totalCount);
277
+ for (const row of stats.rows) {
278
+ console.log(` ${row.title} | ${row.count} | ${row.display}${row.singles ? ` +单条${row.singles}` : ''}`);
279
+ }
280
+ // 叠加视图:优先组的条目同时留在工作流组里,所以 Σcount 会大于 totalCount(去重条数)。
281
+ console.log('\n自检:totalCount=%d(去重)|Σcount=%d(含优先组叠加 %d)|汇总项覆盖 %d + 单条 %d | 空摘要 %d | 文件 %s',
282
+ totalCount, total, Math.max(0, total - totalCount), stats.covered, stats.singles, stats.empty,
283
+ opts.writeFile ? finalFile : '(未生成,按交付规则只输出 <browsertool> 载荷)');
284
+ const meta = result.meta || {};
285
+ const steps = (meta.timing && meta.timing.steps) || [];
286
+ console.log('CLI 自报分段:%s', steps.map((s) => `${s.name}:${s.kind}=${fmt(s.durationMs)}`).join(' '));
287
+ // 交付用 JSON:requestData 压成单行、其余缩进,既合法又可读(10KB 级)
288
+ const lines = [
289
+ '{',
290
+ ` "smartSummaryTitle": ${JSON.stringify(payload.smartSummaryTitle)},`,
291
+ ` "totalCount": ${totalCount},`,
292
+ ' "wfIdGroup": [',
293
+ ];
294
+ payload.wfIdGroup.forEach((g, gi) => {
295
+ lines.push(' {');
296
+ lines.push(` "id": ${JSON.stringify(g.id)},`);
297
+ lines.push(` "topPriority": ${g.topPriority},`);
298
+ lines.push(` "title": ${JSON.stringify(g.title)},`);
299
+ lines.push(` "count": ${g.count},`);
300
+ lines.push(` "defaultExpanded": ${g.defaultExpanded},`);
301
+ lines.push(' "displayItems": [');
302
+ g.displayItems.forEach((item, ii) => {
303
+ const tail = ii < g.displayItems.length - 1 ? ',' : '';
304
+ if (item.type === 'request') {
305
+ lines.push(` {"type":"request","requestData":${JSON.stringify(item.requestData)}}${tail}`);
306
+ return;
307
+ }
308
+ lines.push(' {');
309
+ lines.push(' "type": "group",');
310
+ lines.push(` "id": ${JSON.stringify(item.id)},`);
311
+ lines.push(` "title": ${JSON.stringify(item.title)},`);
312
+ lines.push(` "count": ${item.count},`);
313
+ lines.push(` "summary": ${JSON.stringify(item.summary)},`);
314
+ lines.push(` "defaultExpanded": ${item.defaultExpanded},`);
315
+ lines.push(' "requestDatas": [');
316
+ item.requestDatas.forEach((row, ri) => {
317
+ lines.push(` ${JSON.stringify(row)}${ri < item.requestDatas.length - 1 ? ',' : ''}`);
318
+ });
319
+ lines.push(' ]');
320
+ lines.push(` }${tail}`);
321
+ });
322
+ lines.push(' ]');
323
+ lines.push(` }${gi < payload.wfIdGroup.length - 1 ? ',' : ''}`);
324
+ });
325
+ lines.push(' ]', '}');
326
+ // 落盘与 stdout 共用同一份载荷;data 段只保留三个业务键。
327
+ const finalText = wrapBrowseToolPayload(lines.join('\n'), baseUrl);
328
+ // 用户 2026-10-27 定稿:交付 = 回复里内联 <browsertool> 整块,**不再生成 JSON 文件**。
329
+ // `--file` 仅保留作调试(写一份纯载荷 JSON),交付流程不再使用它。
330
+ if (opts.writeFile) fs.writeFileSync(finalFile, finalText);
331
+ const delivery = `<browsertool>\n${finalText.trimEnd()}\n</browsertool>`;
332
+ console.log('\n[JSON_BEGIN]');
333
+ console.log(delivery);
334
+ console.log('[JSON_END]');
335
+ console.log('载荷字节数:%d(含 <browsertool> 标签)', Buffer.byteLength(delivery));
336
+ // 用户 2026-10-27 要求:不再产出「耗时说明 md」交付物,耗时只在控制台打一段摘要。
337
+ // 计时与统计逻辑(state 各阶段耗时、buildTimingReport 的算法)保持不变,只是输出形态变了。
338
+ if (opts.timingReport) {
339
+ const conclusion = buildTimingReport(state, result)
340
+ .split('## 2. 分阶段')[0]
341
+ .split('\n')
342
+ .filter((line) => line.startsWith('- '))
343
+ .map((line) => ` ${line.slice(2).replace(/\*\*/g, '')}`)
344
+ .join('\n');
345
+ console.log('\n耗时摘要(仅 CLI 执行链路,不含模型往返):');
346
+ console.log(conclusion);
347
+ }
348
+ return opts.writeFile ? finalFile : null;
349
+ }
350
+
351
+ /**
352
+ * 默认由 CLI 走确定性分组的类型(请假 / 财务)。没有侧重点时它们不需要 AI 计划,
353
+ * 但**用户给了适用于本类型的侧重点时必须给计划**,否则这里会把组藏起来、白丢一次分组。
354
+ * 2026-10-27 修:原实现只看组名就跳过,导致 `--focus "请假类型、报销类型"` 下
355
+ * 请假/报销组仍被标成「无需分组」并把条目藏掉。
356
+ */
357
+ const RULE_KINDS = [
358
+ { kind: 'leave', pattern: /(请假|休假|年假|调休|病假|事假|婚假|产假|陪产|丧假|探亲假|工伤假|哺乳假|公休)/u },
359
+ { kind: 'finance', pattern: /(报销|费用|付款|请款|借款|发票|差旅|对公|资金|核销)/u },
360
+ ];
361
+
362
+ /** 侧重点命中了哪些「默认确定性」类型的维度(与 businessRules 的意图表同口径:只认类型词)。 */
363
+ function focusCoversKinds(focusText) {
364
+ const text = String(focusText || '').trim();
365
+ if (!text) return new Set();
366
+ return new Set(RULE_KINDS.filter((rule) => rule.pattern.test(text)).map((rule) => rule.kind));
367
+ }
368
+
369
+ /** 本组是否属于某个默认确定性类型。 */
370
+ function kindOfTitle(title) {
371
+ const hit = RULE_KINDS.find((rule) => rule.pattern.test(String(title || '')));
372
+ return hit ? hit.kind : '';
373
+ }
374
+
375
+ function cmdPrep(args) {
376
+ const limit = Number(args.limit || 0);
377
+ const days = Number(args.days || 0);
378
+ const exclude = args.exclude && args.exclude !== true ? String(args.exclude).split(',').map((x) => x.trim()).filter(Boolean) : [];
379
+ const dir = args.out && args.out !== true ? String(args.out) : `/tmp/todo-summary-${new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14)}`;
380
+ fs.mkdirSync(dir, { recursive: true });
381
+
382
+ const startedAt = nowIso();
383
+ const input = { stage: 'collect', limit: limit || 500 };
384
+ // 小分类(待处理 / 待阅 / 全部…):该环境 `category=todo` 默认是「全部」,会把待阅也算进来;
385
+ // 要「待阅除外」必须显式传 `--sub-category 待处理`。
386
+ if (args['sub-category'] && args['sub-category'] !== true) {
387
+ input.subCategory = String(args['sub-category']);
388
+ }
389
+ // 摘要字数口径要带进 collect:否则缓存命中按默认 50 字判定,
390
+ // 用户要求「10 字以内」时仍会回填旧的长摘要(2026-10-22 实测踩到)。
391
+ if (args['summary-chars'] && args['summary-chars'] !== true) {
392
+ input.summaryMaxChars = Number(args['summary-chars']);
393
+ }
394
+ if (args.focus && args.focus !== true) input.groupFocus = String(args.focus);
395
+ if (args.from && args.to) {
396
+ // 指定区间(含两端),如 --from 2026-10-01 --to 2026-10-31
397
+ input.receiveTime = `${args.from} ${args.to}`;
398
+ } else if (days > 0) {
399
+ const end = new Date();
400
+ const start = new Date(end.getTime() - days * 24 * 3600 * 1000);
401
+ const fmtDate = (d) => d.toISOString().slice(0, 10);
402
+ input.receiveTime = `${fmtDate(start)} ${fmtDate(end)}`;
403
+ }
404
+ const t0 = Date.now();
405
+ const collect = runStage(input, dir, 'collect');
406
+ const collectWallMs = Date.now() - t0;
407
+ // **两个口径必须分开**(性能调优时最容易看错的地方):
408
+ // - collectWallMs:脚本进程视角的墙钟,含 node 启动 + 模块加载(沙箱下 5~7s);
409
+ // - collectMs:CLI 自报的 collect 阶段内部耗时,只含真实业务(取数 + 归一)。
410
+ const collectStep = ((collect.meta && collect.meta.timing && collect.meta.timing.steps) || [])
411
+ .find((step) => step && step.name === 'collect');
412
+ const collectMs = collectStep && Number.isFinite(collectStep.durationMs) ? collectStep.durationMs : collectWallMs;
413
+
414
+ const all = collect.data.items || [];
415
+ const kept = exclude.length ? all.filter((row) => !exclude.includes(String(row.workflowName || ''))) : all;
416
+ const dropped = all.length - kept.length;
417
+
418
+ // 组结构直接用 CLI 返回的 wfIdGroup(已做 priority 抽取,不能再自己按 workflowName 复算,
419
+ // 否则会把被抽进「建议优先处理」的条目重复计入原流程)。
420
+ // 注意:wfIdGroup[].requestDatas 里**没有 workflowName**,要按 requestId 从 items 回查,
421
+ // 否则「按流程名剔除」对被 priority 抽走的条目会失效。
422
+ const workflowNameById = new Map();
423
+ for (const row of all) workflowNameById.set(String(row.requestId || ''), String(row.workflowName || ''));
424
+ const isExcluded = (row) => exclude.includes(workflowNameById.get(String(row.requestId || '')) || '');
425
+
426
+ const planGroups = [];
427
+ for (const group of collect.data.wfIdGroup || []) {
428
+ if (exclude.includes(String(group.title))) continue;
429
+ const rows = (group.requestDatas || []).filter((row) => !isExcluded(row));
430
+ if (rows.length === 0) continue;
431
+ planGroups.push({
432
+ groupId: String(group.id),
433
+ title: group.title,
434
+ count: rows.length,
435
+ rows: rows.map((row, index) => ({
436
+ idx: index + 1,
437
+ requestId: String(row.requestId || ''),
438
+ title: String(row.requestNameTitle || ''),
439
+ })),
440
+ });
441
+ }
442
+
443
+ // 有侧重点时 collect 已经把流程详情数据作为分组素材放进 `groupingBrief[].items[].fields`。
444
+ // prep 直接把它挑几类关键字段打出来:模型据此分桶,不必再另起一轮去翻详情缓存。
445
+ const HINT_FIELD = /(类型|类别|假别|假种|天数|金额|说明)/u;
446
+ const fieldHintById = new Map();
447
+ for (const brief of collect.data.groupingBrief || []) {
448
+ for (const item of brief.items || []) {
449
+ const fields = Array.isArray(item.fields) ? item.fields : [];
450
+ const seenValues = new Set();
451
+ const picked = fields
452
+ .filter((f) => HINT_FIELD.test(String(f.fieldName || '')))
453
+ .filter((f) => {
454
+ const value = String(f.value || '').replace(/\s+/g, ' ').trim();
455
+ if (!value || seenValues.has(value)) return false;
456
+ seenValues.add(value);
457
+ return true;
458
+ })
459
+ .slice(0, 3)
460
+ .map((f) => `${f.fieldName}=${String(f.value || '').replace(/\s+/g, ' ').trim().slice(0, 20)}`);
461
+ if (picked.length > 0) fieldHintById.set(String(item.requestId || ''), picked.join(' · '));
462
+ }
463
+ }
464
+
465
+ fs.writeFileSync(path.join(dir, 'state.json'), JSON.stringify({
466
+ dir, startedAt, limit, days, from: args.from || null, to: args.to || null, exclude, subCategory: input.subCategory || null, collectMs, collectWallMs,
467
+ groupFocus: args.focus && args.focus !== true ? String(args.focus) : null,
468
+ summaryMaxChars: args["summary-chars"] ? Number(args["summary-chars"]) : null,
469
+ groupSummaryMaxChars: args["group-summary-chars"] ? Number(args["group-summary-chars"]) : null,
470
+ fetched: all.length, kept: kept.length, dropped,
471
+ planGroups,
472
+ }));
473
+ fs.writeFileSync(path.join(dir, 'items.json'), JSON.stringify(kept, null, 2));
474
+
475
+ console.log('取数:%d 条(%s)|剔除 %d 条(%s)|保留 %d 条|collect 内部 %s / 含进程启动 %s',
476
+ all.length, limit ? `前 ${limit} 条` : '全部', dropped, exclude.join('/') || '无', kept.length,
477
+ fmt(collectMs), fmt(collectWallMs));
478
+ console.log('输出目录:%s\n', dir);
479
+ const focusKinds = focusCoversKinds(args.focus && args.focus !== true ? args.focus : '');
480
+ for (const group of planGroups) {
481
+ // 只有「默认确定性类型 + 侧重点没覆盖本类型」才算无需分组;被覆盖时照常列出条目。
482
+ const kind = kindOfTitle(group.title);
483
+ const skip = group.groupId === 'priority' || group.count <= 1 || (kind !== '' && !focusKinds.has(kind));
484
+ console.log(`=== ${group.groupId} | ${group.title} | ${group.count}${skip ? ' [无需分组]' : ''}`);
485
+ if (skip) continue;
486
+ for (const row of group.rows) {
487
+ const hint = fieldHintById.get(row.requestId) || '';
488
+ // 提示放在**同一行**:两行格式容易让人(和模型)回头去匹配标题,而标题里往往没有类型词。
489
+ console.log(`${row.idx}\t${hint ? `[${hint}] ` : ''}${row.title.replace(/\s+/g, ' ').slice(0, 46)}`);
490
+ }
491
+ }
492
+ console.log('\n下一步:node %s build --dir %s', path.basename(__filename), dir);
493
+ console.log('(只有内部留言组、或用户给了侧重点时才需要再补 --plan <aiGroups.json>;其余走确定性分组)');
494
+ }
495
+
496
+ function cmdBuild(args) {
497
+ const dir = String(args.dir);
498
+ const planFile = args.plan !== undefined && args.plan !== true ? String(args.plan) : '';
499
+ const state = readJson(path.join(dir, 'state.json'));
500
+ const kept = readJson(path.join(dir, 'items.json'));
501
+ const planGroups = state.planGroups || [];
502
+ const countById = new Map(planGroups.map((g) => [g.groupId, g.count]));
503
+ // `--plan` 可选:其他类默认不按标题二次分组,很多批次根本不需要 AI 计划。
504
+ // 不传时直接走 CLI 确定性分组(等价于 aiGroups 未传)。
505
+ let aiGroups;
506
+ if (planFile) {
507
+ aiGroups = readJson(planFile);
508
+
509
+ // 覆盖校验:每个 groupId 的 1..count 全覆盖、无重复;count 取 CLI 口径(priority 已抽取)
510
+ const byGroup = new Map();
511
+ for (const entry of aiGroups) {
512
+ if (!entry || !entry.groupId || !Array.isArray(entry.seq)) {
513
+ console.error('✖ aiGroups 元素必须是 {groupId,title,seq:[...]}'); process.exit(1);
514
+ }
515
+ if (!countById.has(String(entry.groupId))) {
516
+ console.error(`✖ 未知 groupId ${entry.groupId}(合法值见 state.json 的 planGroups)`); process.exit(1);
517
+ }
518
+ const set = byGroup.get(entry.groupId) || new Set();
519
+ for (const n of entry.seq) {
520
+ if (set.has(n)) { console.error(`✖ groupId ${entry.groupId} 的 seq ${n} 重复`); process.exit(1); }
521
+ set.add(n);
522
+ }
523
+ byGroup.set(entry.groupId, set);
524
+ }
525
+ for (const [groupId, seqs] of byGroup) {
526
+ const count = countById.get(groupId);
527
+ const miss = [];
528
+ for (let i = 1; i <= count; i += 1) if (!seqs.has(i)) miss.push(i);
529
+ if (miss.length) {
530
+ const name = (planGroups.find((g) => g.groupId === groupId) || {}).title || groupId;
531
+ console.error(`✖ ${name} 缺 ${miss.length} 条:${miss.slice(0, 12).join(',')}`);
532
+ process.exit(1);
533
+ }
534
+ }
535
+ const focusKinds = focusCoversKinds(state.groupFocus);
536
+ for (const group of planGroups) {
537
+ if (countById.get(group.groupId) <= 1 || group.groupId === 'priority') continue;
538
+ // 请假/财务默认由 CLI 阈值兜底,不需要模型给子组;但**侧重点覆盖了本类型时必须给**,
539
+ // 否则这里静默跳过,用户要的「按请假类型/报销类型分组」就白丢了。
540
+ const kind = kindOfTitle(group.title);
541
+ if (kind !== '' && !focusKinds.has(kind)) continue;
542
+ if (!byGroup.has(group.groupId)) console.log('⚠ %s(%d 条)没有给子组,将整体作为一个汇总项', group.title, group.count);
543
+ }
544
+ fs.writeFileSync(path.join(dir, 'aiGroups.json'), JSON.stringify(aiGroups, null, 2));
545
+ }
546
+
547
+ const t = Date.now();
548
+ const sg = runStage({
549
+ stage: 'smartGroup',
550
+ items: kept,
551
+ ...(aiGroups !== undefined ? { aiGroups } : {}),
552
+ withDetailContext: true,
553
+ ...(state.groupFocus ? { groupFocus: state.groupFocus } : {}),
554
+ ...(state.summaryMaxChars ? { summaryMaxChars: state.summaryMaxChars } : {}),
555
+ skillTiming: { startedAt: state.startedAt, steps: [{ name: 'collect', kind: 'cli', durationMs: state.collectMs }] },
556
+ }, dir, 'smartgroup');
557
+ fs.writeFileSync(path.join(dir, 'smartgroup.json'), JSON.stringify(sg));
558
+ const smartGroupMs = Date.now() - t;
559
+
560
+ const batches = sg.data.summaryBatches || [];
561
+ const pending = batches.flatMap((b) => b.items || []);
562
+ const stats = sg.data.summaryStats || {};
563
+ const sgInternal = cliStageMs(sg, 'smartGroup');
564
+ console.log('smartGroup 内部 %s / 含进程启动 %s|%d 条 / %d 组|prefilled=%s|待写摘要 %d 条',
565
+ sgInternal === null ? '-' : fmt(sgInternal), fmt(smartGroupMs),
566
+ sg.data.wfIdGroup.reduce((a, g) => a + g.count, 0), sg.data.wfIdGroup.length, stats.prefilled, pending.length);
567
+ // 回写耗时(供 `finish --timing-report` 一次性生成耗时说明,免得多跑一轮查询)
568
+ state.smartGroupWallMs = smartGroupMs;
569
+ state.smartGroupInternalMs = sgInternal;
570
+ fs.writeFileSync(path.join(dir, 'state.json'), JSON.stringify(state));
571
+
572
+ if (pending.length === 0) {
573
+ exportFinal(sg, dir, state, kept.length, { writeFile: args.file === true, timingReport: args['timing-report'] });
574
+ return;
575
+ }
576
+ const summaryTarget = pending[0] && pending[0].suggestedMaxChars ? pending[0].suggestedMaxChars : 50;
577
+ console.log(`\n待写摘要(把结果写成 aiSummaries.json,元素 {requestId,summary},约 ${summaryTarget} 字):`);
578
+ for (const item of pending) {
579
+ console.log(`### ${item.requestId}`);
580
+ console.log(`T: ${String(item.requestNameTitle || '').slice(0, 70)}`);
581
+ console.log(`C: ${String(item.context || '').replace(/\s+/g, ' ').slice(0, 240)}`);
582
+ }
583
+ console.log('\n下一步:node %s finish --dir %s --summaries <aiSummaries.json>', path.basename(__filename), dir);
584
+ }
585
+
586
+ function cmdFinish(args) {
587
+ const dir = String(args.dir);
588
+ const state = readJson(path.join(dir, 'state.json'));
589
+ const kept = readJson(path.join(dir, 'items.json'));
590
+ // `aiGroups.json` 可能不存在(build 没传 --plan 时就是确定性分组),此时按「未传 aiGroups」处理。
591
+ const planPath = path.join(dir, 'aiGroups.json');
592
+ const aiGroups = fs.existsSync(planPath) ? readJson(planPath) : undefined;
593
+ const sg = readJson(path.join(dir, 'smartgroup.json'));
594
+
595
+ // 即使没传 --summaries 也要跑一次 summary 阶段:这样才能让**摘要缓存**回填
596
+ // (否则直接导出 smartgroup.json 的半成品,会出现一堆空摘要)。传了 aiSummaries 就额外落位。
597
+ const aiSummaries = args.summaries && args.summaries !== true ? readJson(String(args.summaries)) : [];
598
+ const t = Date.now();
599
+ const result = runStage({
600
+ stage: 'summary',
601
+ items: kept,
602
+ ...(aiGroups !== undefined ? { aiGroups } : {}),
603
+ withDetailContext: true,
604
+ wfIdGroup: sg.data.wfIdGroup,
605
+ aiSummaries,
606
+ withGroupSummaryContext: true,
607
+ ...(state.summaryMaxChars ? { summaryMaxChars: state.summaryMaxChars } : {}),
608
+ ...(state.groupSummaryMaxChars ? { groupSummaryMaxChars: state.groupSummaryMaxChars } : {}),
609
+ skillTiming: { startedAt: state.startedAt, steps: [{ name: 'collect', kind: 'cli', durationMs: state.collectMs }] },
610
+ }, dir, 'summary');
611
+ fs.writeFileSync(path.join(dir, 'summary.json'), JSON.stringify(result));
612
+ const summaryInternal = cliStageMs(result, 'summary');
613
+ const summaryWallMs = Date.now() - t;
614
+ state.summaryWallMs = summaryWallMs;
615
+ state.summaryInternalMs = summaryInternal;
616
+ fs.writeFileSync(path.join(dir, 'state.json'), JSON.stringify(state));
617
+ console.log('summary 内部 %s / 含进程启动 %s(回填 %d 条,其余走缓存预填)',
618
+ summaryInternal === null ? '-' : fmt(summaryInternal), fmt(summaryWallMs), aiSummaries.length);
619
+ let finalResult = result;
620
+ const groupBatches = result.data.groupSummaryBatches || [];
621
+ if (groupBatches.length > 0) {
622
+ const groupSummariesFile = args['group-summaries'] && args['group-summaries'] !== true
623
+ ? String(args['group-summaries'])
624
+ : null;
625
+ if (groupSummariesFile) {
626
+ // 组摘要必须由模型写、且每轮重算;这里只是把「回填」并进同一次命令,省掉一次模型往返。
627
+ const aiGroupSummaries = readJson(groupSummariesFile);
628
+ const t2 = Date.now();
629
+ const gs = runStage({
630
+ smartSummaryTitle: result.data.smartSummaryTitle,
631
+ totalCount: result.data.totalCount,
632
+ wfIdGroup: result.data.wfIdGroup,
633
+ aiGroupSummaries,
634
+ }, dir, 'groupsummary');
635
+ state.groupSummaryWallMs = Date.now() - t2;
636
+ state.groupSummaryInternalMs = cliStageMs(gs, 'groupSummary');
637
+ fs.writeFileSync(path.join(dir, 'state.json'), JSON.stringify(state));
638
+ fs.writeFileSync(path.join(dir, 'groupsummary.json'), JSON.stringify(gs));
639
+ const filled = (gs.meta && gs.meta.groupSummarized) || 0;
640
+ const empty = (gs.meta && gs.meta.groupEmpty) || 0;
641
+ console.log('groupSummary 内部 %s / 含进程启动 %s(回填 %d 个汇总项,空 %d)',
642
+ state.groupSummaryInternalMs === null ? '-' : fmt(state.groupSummaryInternalMs), fmt(state.groupSummaryWallMs), filled, empty);
643
+ const gsWarnings = (gs.meta && gs.meta.warnings) || [];
644
+ for (const text of gsWarnings) console.log(' ⚠ %s', text);
645
+ finalResult = gs;
646
+ } else {
647
+ console.log('\n⚠ 还有 %d 个组摘要素材待模型生成(groupSummary 阶段):', groupBatches.length);
648
+ for (const batch of groupBatches) console.log(JSON.stringify(batch).slice(0, 400));
649
+ console.log(' → 写成 [{groupId,summary}] 后加 `--group-summaries <file>` 重跑本命令,可在同一次调用里回填');
650
+ }
651
+ }
652
+ exportFinal(finalResult, dir, state, kept.length, { writeFile: args.file === true, timingReport: args['timing-report'] });
653
+ console.log('总耗时(脚本口径):%s', fmt(Date.now() - Date.parse(state.startedAt)));
654
+ }
655
+
656
+ /**
657
+ * `run`:把 prep + build(+ 无待写摘要时自动 finish)串成**一次调用**。
658
+ *
659
+ * 为什么要这个命令:每一步都要模型读输出再决定下一步,每次往返在长上下文下要 20~35s。
660
+ * 一次汇总原本要 9 次往返(实测 4m58s),其中 CLI 只占 25s —— 合并阶段是端到端提速的关键。
661
+ *
662
+ * 有待写摘要时会停下并打印素材:那部分必须模型写,写完跑 `finish` 即可,不用重跑取数与分组。
663
+ */
664
+ function cmdRun(args) {
665
+ const dir = args.out && args.out !== true
666
+ ? String(args.out)
667
+ : `/tmp/todo-summary-${new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14)}`;
668
+ cmdPrep({ ...args, out: dir });
669
+ cmdBuild({ ...args, dir });
670
+ const sg = readJson(path.join(dir, 'smartgroup.json'));
671
+ const pending = ((sg.data.summaryBatches || []).flatMap((batch) => batch.items || []));
672
+ if (pending.length > 0) {
673
+ console.log('\n⚠ 有 %d 条待写摘要:写成 aiSummaries.json 后跑 finish(**不用重跑** collect/smartGroup)', pending.length);
674
+ console.log(' node %s finish --dir %s --summaries <aiSummaries.json> --file --timing-report', path.basename(__filename), dir);
675
+ return;
676
+ }
677
+ // 没有待写摘要 ⇒ 终态可以立刻出,省掉一次约 7s 的进程启动
678
+ cmdFinish({ dir, file: args.file === true, 'timing-report': args['timing-report'], 'group-summaries': args['group-summaries'] });
679
+ }
680
+
681
+ const args = parseArgs(process.argv.slice(2));
682
+ const cmd = args._[0];
683
+ if (cmd === 'prep') cmdPrep(args);
684
+ else if (cmd === 'build') cmdBuild(args);
685
+ else if (cmd === 'finish') cmdFinish(args);
686
+ else if (cmd === 'run') cmdRun(args);
687
+ else {
688
+ console.log('用法:node ' + path.basename(__filename) + ' <run|prep|build|finish> [options]');
689
+ console.log(' run --days 30 --limit 500 [--out DIR] [--file] [--timing-report] # 一次跑完 prep+build(无待写摘要时连 finish 一起)');
690
+ console.log(' prep --limit 100 [--exclude 内部留言] [--days 1460] [--sub-category 待处理] [--out DIR]');
691
+ console.log(' build --dir DIR [--plan aiGroups.json] # 不传 plan = 直接走确定性分组');
692
+ console.log(' finish --dir DIR [--summaries aiSummaries.json] [--group-summaries aiGroupSummaries.json] [--file] [--timing-report]');
693
+ console.log(' 提示:`--timing-report` 会在控制台打一段耗时摘要(阶段墙钟 / CLI 内部),**不产出 md**');
694
+ process.exit(2);
695
+ }