weaver-work-cli 0.1.38 → 0.1.40

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 (166) hide show
  1. package/README.md +7 -3
  2. package/dist/chunks/{archive-PDV724YK.js → archive-JHTONRST.js} +556 -66
  3. package/dist/chunks/{archive-oa-4XE4MNXQ.js → archive-oa-FCHLCPHC.js} +2 -2
  4. package/dist/chunks/{asset-PDL2QE2N.js → asset-RLIHC7DB.js} +2 -2
  5. package/dist/chunks/asset-idle-KVOFTLCP.js +670 -0
  6. package/dist/chunks/{attachment-content-MDDGOE5E.js → attachment-content-RZYDB3MB.js} +3 -3
  7. package/dist/chunks/{blog-UYR3EUXW.js → blog-42OZTJNP.js} +2 -2
  8. package/dist/chunks/{caizhilian-LG2X5NMF.js → caizhilian-KRK547K5.js} +2 -2
  9. package/dist/chunks/{calendar-FG4BAZHE.js → calendar-RTWAMKHD.js} +2 -2
  10. package/dist/chunks/{chunk-J7USS3ED.js → chunk-4CTESSKI.js} +4 -4
  11. package/dist/chunks/{chunk-OJQUJZQF.js → chunk-4TEDNCON.js} +45 -5
  12. package/dist/chunks/chunk-57IECMWX.js +487 -0
  13. package/dist/chunks/{chunk-ELYDWBZJ.js → chunk-7BRU4EFH.js} +13 -7
  14. package/dist/chunks/{chunk-6LTFIFLM.js → chunk-7E6MUOEK.js} +2 -2
  15. package/dist/chunks/{chunk-KKRL3REV.js → chunk-7U577Q7U.js} +2 -2
  16. package/dist/chunks/{chunk-UJJD4QWF.js → chunk-CAQNBBQA.js} +2 -2
  17. package/dist/chunks/{chunk-CORFRD7Q.js → chunk-CXRQWOZ5.js} +2 -2
  18. package/dist/chunks/{chunk-GGJFMNJM.js → chunk-FLS4DPGP.js} +110 -16
  19. package/dist/chunks/{chunk-JSMFPO43.js → chunk-I566ZYNF.js} +1 -1
  20. package/dist/chunks/{chunk-6FTTSA45.js → chunk-IZSSVYAG.js} +2 -2
  21. package/dist/chunks/{chunk-XJY7G4DD.js → chunk-JWRZM2H2.js} +3 -3
  22. package/dist/chunks/{chunk-NI6GIPIM.js → chunk-LFMSDBZN.js} +529 -395
  23. package/dist/chunks/{chunk-VRYHGG6O.js → chunk-LN447PY2.js} +92 -51
  24. package/dist/chunks/{chunk-NR6RKXPA.js → chunk-NETCCARD.js} +4 -2
  25. package/dist/chunks/{chunk-Z32KWY6G.js → chunk-NJKREDA3.js} +1 -1
  26. package/dist/chunks/{chunk-FUWVHMVV.js → chunk-SPRHHSVC.js} +2 -2
  27. package/dist/chunks/{chunk-IZUDA353.js → chunk-T6EGVBKU.js} +1 -1
  28. package/dist/chunks/{chunk-DMV2TOMH.js → chunk-VNBU55SL.js} +428 -145
  29. package/dist/chunks/{chunk-UZZBA6PV.js → chunk-WFFNM65B.js} +47 -6
  30. package/dist/chunks/{chunk-EG6ZU444.js → chunk-ZUX3P6BQ.js} +10 -5
  31. package/dist/chunks/{conflict-BQZEQ4SP.js → conflict-VR55AOOO.js} +2 -2
  32. package/dist/chunks/{contract-XIRAFTEV.js → contract-HI4FNPYE.js} +2 -2
  33. package/dist/chunks/{cowork-BLG62BL2.js → cowork-HJLI353D.js} +12 -5
  34. package/dist/chunks/{ebuilder-form-NUKG5QW4.js → ebuilder-form-HTGI63UU.js} +3 -3
  35. package/dist/chunks/{ehr-2STW5ILH.js → ehr-GYXIYH3A.js} +2 -2
  36. package/dist/chunks/env-DW3G4HUO.js +19 -0
  37. package/dist/chunks/{esb-UUDQUNKY.js → esb-I5INCEFE.js} +4 -4
  38. package/dist/chunks/factory-TKXGXS6L.js +30 -0
  39. package/dist/chunks/{fna-6RCY4NU5.js → fna-K2MHTQ24.js} +2 -2
  40. package/dist/chunks/group-chat-VYBNMJLG.js +18 -0
  41. package/dist/chunks/{host-FWDOSRH5.js → host-256GVCAH.js} +3 -3
  42. package/dist/chunks/{host-EJZFROU3.js → host-D5SERMAJ.js} +3 -3
  43. package/dist/chunks/{host-RHM3NPXV.js → host-JM4YVHFM.js} +3 -3
  44. package/dist/chunks/{host-7RXEDMAH.js → host-L3VLITMT.js} +3 -3
  45. package/dist/chunks/{host-UFF33TWJ.js → host-ZUQTJYYK.js} +3 -3
  46. package/dist/chunks/{hrm-PKJ6ELSH.js → hrm-VLRBKTZB.js} +3 -3
  47. package/dist/chunks/{im-YJAXS7CO.js → im-QK3VTLXK.js} +8 -8
  48. package/dist/chunks/{invoice-HWVSXGX6.js → invoice-T5JTA32H.js} +3 -3
  49. package/dist/chunks/{jiuchuanhui-L7VKILZB.js → jiuchuanhui-P2FNEQVZ.js} +2 -2
  50. package/dist/chunks/{lazy-dispatch-NMYEIHRH.js → lazy-dispatch-XRRI46M6.js} +5 -5
  51. package/dist/chunks/{mail-B42RRU7G.js → mail-3Y7MJ6PS.js} +9 -4
  52. package/dist/chunks/{meeting-CFYVBJSS.js → meeting-M7LTXGJE.js} +11 -10
  53. package/dist/chunks/{okr-GCLF6V7W.js → okr-OBC6S67L.js} +2 -2
  54. package/dist/chunks/{plan-ZVL7GBRI.js → plan-5H6I53SK.js} +2 -2
  55. package/dist/chunks/{project-4YWAAE2K.js → project-L75QZV4S.js} +2 -2
  56. package/dist/chunks/{registry-EP4VTHZS.js → registry-KMHCPAAD.js} +9 -8
  57. package/dist/chunks/{registry-OZSMDVPP.js → registry-QGHFNUWS.js} +2 -2
  58. package/dist/chunks/{registry-FRJ6H7BT.js → registry-RXPN6BBT.js} +4 -4
  59. package/dist/chunks/{registry-SG2PGVCF.js → registry-UGRBCBHO.js} +3 -3
  60. package/dist/chunks/{room-recommend-R3JVUUAC.js → room-recommend-A3UDC3CA.js} +4 -4
  61. package/dist/chunks/{schedule-NIJBTYDO.js → schedule-5GCACCK7.js} +4 -4
  62. package/dist/chunks/{workflow-BB24DFBY.js → workflow-M5ZJ7DWF.js} +11 -8
  63. package/dist/core/output.js +16 -6
  64. package/dist/index.js +33 -28
  65. package/dist/internal/e10/auth/commands.js +109 -56
  66. package/dist/internal/e10/auth/messages.js +1 -0
  67. package/dist/internal/e10/auth/session.js +11 -1
  68. package/dist/internal/e10/auth/xiaoe.js +29 -4
  69. package/dist/internal/e10/auth-recovery.js +13 -6
  70. package/dist/internal/e10/client.js +13 -0
  71. package/dist/internal/e10/context.js +24 -3
  72. package/dist/internal/e10/request-runtime.js +12 -1
  73. package/dist/shortcuts/archive/host.js +2 -0
  74. package/dist/shortcuts/archive/manifest.js +1 -1
  75. package/dist/shortcuts/archive/operations/borrow-list.js +13 -1
  76. package/dist/shortcuts/archive/operations/flow-url.js +1 -1
  77. package/dist/shortcuts/archive/operations/prelib.js +472 -0
  78. package/dist/shortcuts/archive/operations/registry.js +2 -0
  79. package/dist/shortcuts/archive/operations/upload.js +1 -1
  80. package/dist/shortcuts/asset-idle/errors.js +44 -0
  81. package/dist/shortcuts/asset-idle/host.js +161 -0
  82. package/dist/shortcuts/asset-idle/index.js +114 -0
  83. package/dist/shortcuts/asset-idle/manifest.js +159 -0
  84. package/dist/shortcuts/asset-idle/operations/idle.js +47 -0
  85. package/dist/shortcuts/asset-idle/operations/registry.js +33 -0
  86. package/dist/shortcuts/asset-idle/operations/shared.js +142 -0
  87. package/dist/shortcuts/asset-idle/operations/types.js +9 -0
  88. package/dist/shortcuts/asset-idle/operations.js +15 -0
  89. package/dist/shortcuts/cowork/operations/item-write.js +10 -2
  90. package/dist/shortcuts/index.js +5 -0
  91. package/dist/shortcuts/mail/operations/mail-send.js +8 -2
  92. package/dist/shortcuts/meeting/operations/eb/catalog.js +5 -1
  93. package/dist/shortcuts/meeting/operations/eb/create-flow.js +381 -104
  94. package/dist/shortcuts/meeting/operations/eb/crm-search.js +87 -5
  95. package/dist/shortcuts/meeting/operations/eb/flow-memory.js +49 -0
  96. package/dist/shortcuts/meeting/operations/eb/group-chat.js +583 -0
  97. package/dist/shortcuts/meeting/operations/eb/index.js +2 -0
  98. package/dist/shortcuts/meeting/operations/eb/room-recommend.js +19 -3
  99. package/dist/shortcuts/meeting/operations/eb/room-summary.js +2 -0
  100. package/dist/shortcuts/meeting/operations/eb/usage-cache.js +99 -0
  101. package/dist/shortcuts/meeting/operations/from-group-chat.js +35 -12
  102. package/dist/shortcuts/workflow/host.js +3 -0
  103. package/dist/shortcuts/workflow/index.js +1 -1
  104. package/dist/shortcuts/workflow/operations/form.js +65 -171
  105. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupRules.js +129 -27
  106. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupStore.js +6 -0
  107. package/dist/shortcuts/workflow/operations/todoSmartConvert/summaryCache.js +60 -117
  108. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailCache.js +44 -7
  109. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailFetcher.js +37 -29
  110. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +48 -8
  111. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.core.js +1 -0
  112. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.detail.js +22 -18
  113. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.group.js +17 -9
  114. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +34 -10
  115. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +5 -2
  116. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +9 -2
  117. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +122 -20
  118. package/docs/_catalog.md +3 -0
  119. package/docs/asset-idle.md +45 -0
  120. package/docs/e10-auth.md +16 -1
  121. package/docs/todo-smart-convert/subgroup-rules.md +1 -1
  122. package/package.json +1 -1
  123. package/scripts/mcp-meeting.mjs +234 -0
  124. package/scripts/package-skill.mjs +2 -0
  125. package/skill-template/business-info.json +14 -1
  126. package/skill-template/domains/asset-idle.md +13 -0
  127. package/skill-template/domains/wenshuding.md +3 -0
  128. package/skills/weaver-e10-archive-oa/SKILL.md +1 -0
  129. package/skills/weaver-e10-archive-oa/references/my-borrow.md +49 -0
  130. package/skills/weaver-e10-asset-idle/SKILL.md +73 -0
  131. package/skills/weaver-e10-asset-idle/references/idle-list.md +62 -0
  132. package/skills/weaver-e10-asset-idle/references/idle-stats-and-charts.md +65 -0
  133. package/skills/weaver-e10-asset-idle/references/source-manifest.json +193 -0
  134. package/skills/weaver-e10-asset-idle/references/widget-style.md +205 -0
  135. package/skills/weaver-e10-caizhilian/SKILL.md +29 -11
  136. package/skills/weaver-e10-caizhilian/references/auth-and-install.md +12 -0
  137. package/skills/weaver-e10-caizhilian/references/files.md +1 -1
  138. package/skills/weaver-e10-caizhilian/references/setting-encrypt.md +1 -1
  139. package/skills/weaver-e10-caizhilian/references/source-manifest.json +45 -45
  140. package/skills/weaver-e10-cowork/SKILL.md +2 -0
  141. package/skills/weaver-e10-cowork/references/cowork-write.md +1 -1
  142. package/skills/weaver-e10-jucailin/SKILL.md +2 -0
  143. package/skills/weaver-e10-jucailin/references/recruit-read.md +2 -0
  144. package/skills/weaver-e10-jucailin/references/recruit-write.md +1 -1
  145. package/skills/weaver-e10-mail/SKILL.md +3 -0
  146. package/skills/weaver-e10-mail/references/mail-send.md +2 -0
  147. package/skills/weaver-e10-meeting/SKILL.md +88 -25
  148. package/skills/weaver-e10-meeting/references/meeting-minute-file.md +25 -11
  149. package/skills/weaver-e10-meeting.zip +0 -0
  150. package/skills/weaver-e10-shared/SKILL.md +5 -5
  151. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +3 -3
  152. package/skills/weaver-e10-wenshuding/SKILL.md +29 -6
  153. package/skills/weaver-e10-wenshuding/references/archive-borrow-list.md +14 -0
  154. package/skills/weaver-e10-wenshuding/references/archive-download.md +2 -0
  155. package/skills/weaver-e10-wenshuding/references/archive-flow.md +1 -1
  156. package/skills/weaver-e10-wenshuding/references/archive-prelib-arrange.md +76 -0
  157. package/skills/weaver-e10-wenshuding/references/archive-prelib-list.md +107 -0
  158. package/skills/weaver-e10-wenshuding/references/archive-prelib-open.md +59 -0
  159. package/skills/weaver-e10-wenshuding/references/archive-upload.md +4 -1
  160. package/skills/weaver-e10-wenshuding/references/source-manifest.json +108 -65
  161. package/skills/weaver-e10-workflow/SKILL.md +9 -2
  162. package/skills/weaver-e10-workflow/aigroup/default.json +166 -19
  163. package/skills/weaver-e10-workflow/references/todo-smart-convert.md +72 -34
  164. package/skills/weaver-e10-workflow/tools/todo-serve.cjs +113 -25
  165. package/skills/weaver-e10-workflow/tools/todo-summary.cjs +350 -50
  166. package/dist/chunks/{chunk-2LV74IWP.js → chunk-E7OK5WD5.js} +5 -5
@@ -6,7 +6,7 @@
6
6
  * 避免每次现场拼脚本,也避免多跑一次 stage=group 进程(每次 CLI 启动约 6~7s)。
7
7
  *
8
8
  * 用法:
9
- * node todo-summary.cjs prep --limit 100 [--exclude 内部留言] [--days 365] [--out DIR]
9
+ * node todo-summary.cjs prep [--limit 500] [--exclude 内部留言] [--days 365 | --from A --to B] [--out DIR] # 用户没说时间段就都不传 = 不限时间取前 500 条
10
10
  * → 取数、按 workflowName 剔除指定流程、打印分组素材(模型据此做二次分组)
11
11
  * node todo-summary.cjs build --dir DIR --plan aiGroups.json
12
12
  * → 校验 1..count 全覆盖 → smartGroup;若还有待写摘要则打印,否则直接导出终态
@@ -17,8 +17,17 @@
17
17
  */
18
18
  const { execFileSync } = require('node:child_process');
19
19
  const fs = require('node:fs');
20
+ const os = require('node:os');
20
21
  const path = require('node:path');
21
22
 
23
+ // 缓存与 aigroup 规则都锚定「当前 skill」(2026-11-01 用户定稿):本脚本就住在
24
+ // <skillDir>/tools/ 下,把自身 skill 目录显式指给 CLI——生成与提取一律用当前 skill
25
+ // 的 cache/,不再走多安装发现排名(否则改名的副本会被旧安装的缓存抢走)。
26
+ // 用户已显式设置时不覆盖。
27
+ if (!process.env.WEAVER_WORK_CLI_SKILL_DIR) {
28
+ process.env.WEAVER_WORK_CLI_SKILL_DIR = path.resolve(__dirname, '..');
29
+ }
30
+
22
31
  /**
23
32
  * CLI 可执行入口(跨平台)。
24
33
  *
@@ -169,9 +178,10 @@ function groupStats(wfIdGroup) {
169
178
  /**
170
179
  * 导出最终结构。
171
180
  *
172
- * 交付形态(用户 2026-10-22 硬规则):**只在对话里输出 JSON + 耗时,不产物理文件**。
173
- * 所以默认走「stdout-only」:把最终结构的 JSON 打到 stdout 的 `[JSON_BEGIN]/[JSON_END]` 之间,
174
- * 由 Agent 原样贴进回复;只有显式传 `--file` 才写成 `待办汇总-*.json`(兼容旧用法/调试)。
181
+ * 交付形态:stdout 输出 `<browsertool>` 载荷(`[JSON_BEGIN]/[JSON_END]` 之间),
182
+ * 由 Agent 原样贴进回复;三键数据本体另存 `final.data.json`(交付文件复制它的,
183
+ * 命名 `<用户名>_<时间戳>.json` 由 Agent 侧按 references §6.1 拼写)。
184
+ * 旧 `--file`(写 `待办汇总-*.json` 外框归档)已于 2026-11-01 删除:文件名与内容都是旧口径,易误当交付物。
175
185
  *
176
186
  * 顶层键固定三个:`smartSummaryTitle` / `totalCount` / `wfIdGroup`(用户 2026-10-25 要求加
177
187
  * `totalCount` = 当前这批数据的总条数,与各大分组 `count` 之和一致)。
@@ -220,7 +230,7 @@ function buildTimingReport(state, result) {
220
230
  lines.push('- 「待写摘要为 0」时脚本会直接导出,不会白跑一次 finish。');
221
231
  lines.push('', '## 4. 复现', '');
222
232
  lines.push('```bash');
223
- lines.push('node todo-summary.cjs run --days 30 --limit 500 --out /tmp/t --file --timing-report');
233
+ lines.push('node todo-summary.cjs run --days 30 --limit 500 --out /tmp/t --timing-report');
224
234
  lines.push('```');
225
235
  return `${lines.join('\n')}\n`;
226
236
  }
@@ -311,7 +321,9 @@ function sleepSync(ms) {
311
321
  * - 快照按目录 mtime 计空闲,超时由服务端自己删;进程在所有快照过期且无请求后才退出。
312
322
  */
313
323
  const SERVE_ROOT = path.join(require('node:os').tmpdir(), 'weaver-work-cli-serve');
314
- const SERVE_VERSION = '3';
324
+ const SERVE_VERSION = '4';
325
+ /** v4 服务的发布密钥:ensureServeServer 从 current.json 读出,publishSnapshot 发布时回传。 */
326
+ let servePublishKey = '';
315
327
 
316
328
  /**
317
329
  * 同步探一次本地数据源的 `/health`:通就返回解析后的 JSON,不通返回 null。
@@ -362,6 +374,7 @@ function ensureServeServer(portOption) {
362
374
  // 先探活:既验「真有人在这端口上应答」,也验「应答的是不是我们这个版本的自己」。
363
375
  const health = probeHealth(cur.port);
364
376
  if (health && health.ok === true && String(health.version) === SERVE_VERSION) {
377
+ servePublishKey = String(cur.publishKey || '');
365
378
  return cur.port;
366
379
  }
367
380
  if (health && health.ok === true) {
@@ -390,7 +403,10 @@ function ensureServeServer(portOption) {
390
403
  if (fs.existsSync(registryFile)) {
391
404
  try {
392
405
  const info = JSON.parse(fs.readFileSync(registryFile, 'utf8'));
393
- if (info && info.version === SERVE_VERSION && info.port && info.pid === child.pid) return info.port;
406
+ if (info && info.version === SERVE_VERSION && info.port && info.pid === child.pid) {
407
+ servePublishKey = String(info.publishKey || '');
408
+ return info.port;
409
+ }
394
410
  } catch { /* 文件正在写入,稍后重试 */ }
395
411
  }
396
412
  sleepSync(120);
@@ -405,6 +421,48 @@ function publishSnapshot(dir, portOption) {
405
421
  const port = ensureServeServer(portOption);
406
422
  if (!port) return null;
407
423
  const token = require('node:crypto').randomBytes(12).toString('hex');
424
+ const url = `http://127.0.0.1:${port}/s/${token}/todo.json`;
425
+ const body = fs.readFileSync(dataFile, 'utf8');
426
+
427
+ // v4 主通路:把 JSON 原文 POST 进服务内存(带发布密钥)。
428
+ // 服务端不再读磁盘 ⇒ 不受「常驻进程读权限被沙箱回收」的影响,链接跨命令/跨轮次稳定。
429
+ // POST 用独立的极短 node 子进程同步完成(与 probeHealth 同款手法:主进程 Atomics 阻塞时
430
+ // async 回调排不上队,只能用子进程);body 走 stdin 传给子进程,避免命令行长度问题。
431
+ const postCode = [
432
+ "const http=require('node:http');",
433
+ "const fs=require('node:fs');",
434
+ 'const key=process.argv[1];',
435
+ 'const port=Number(process.argv[2]);',
436
+ 'const token=process.argv[3];',
437
+ "const body=fs.readFileSync(0,'utf8');",
438
+ "const req=http.request({host:'127.0.0.1',port,path:'/s/'+token+'/todo.json',method:'POST',",
439
+ " headers:{'content-type':'application/json','x-publish-key':key,'content-length':Buffer.byteLength(body)},timeout:10000},",
440
+ ' (res)=>{let b="";res.setEncoding("utf8");',
441
+ ' res.on("data",(d)=>{b+=d;});',
442
+ ' res.on("end",()=>{process.stdout.write(b);process.exit(res.statusCode===200?0:3);});',
443
+ ' });',
444
+ "req.on('timeout',()=>{req.destroy();process.exit(2);});",
445
+ "req.on('error',()=>process.exit(2));",
446
+ 'req.end(body);',
447
+ ].join('');
448
+ try {
449
+ const result = execFileSync(
450
+ process.execPath,
451
+ ['-e', postCode, servePublishKey, String(port), token],
452
+ { input: body, encoding: 'utf8', timeout: 20000, maxBuffer: 16 * 1024 * 1024, env: cliEnv() },
453
+ );
454
+ const parsed = JSON.parse(result);
455
+ if (parsed && parsed.ok === true) {
456
+ try {
457
+ fs.writeFileSync(path.join(dir, 'serve.json'), JSON.stringify({
458
+ url, token, port, mode: 'memory', bytes: parsed.bytes,
459
+ }, null, 2));
460
+ } catch { /* 只是排错用的旁路文件 */ }
461
+ return url;
462
+ }
463
+ } catch { /* POST 失败 ⇒ 回落写文件方式(旧服务兼容) */ }
464
+
465
+ // 回落:老路径——写快照文件,由服务端读磁盘(v3 服务或 POST 失败时才走这里)。
408
466
  const snapDir = path.join(SERVE_ROOT, 'snapshots', token);
409
467
  fs.mkdirSync(snapDir, { recursive: true });
410
468
  // 先拷到临时名再拷成正式名:沙箱会拒绝对家目录做 rename(EPERM),copyFile 不受限;
@@ -413,16 +471,57 @@ function publishSnapshot(dir, portOption) {
413
471
  fs.copyFileSync(dataFile, tmp);
414
472
  fs.copyFileSync(tmp, path.join(snapDir, 'todo.json'));
415
473
  try { fs.unlinkSync(tmp); } catch { /* 残留也无害 */ }
416
- const url = `http://127.0.0.1:${port}/s/${token}/todo.json`;
417
474
  try {
418
- fs.writeFileSync(path.join(dir, 'serve.json'), JSON.stringify({ url, token, port, snapshotDir: snapDir }, null, 2));
475
+ fs.writeFileSync(path.join(dir, 'serve.json'), JSON.stringify({ url, token, port, mode: 'file' }, null, 2));
419
476
  } catch { /* 只是排错用的旁路文件 */ }
420
477
  return url;
421
478
  }
422
479
 
480
+ /**
481
+ * 交付文件名的用户名(2026-11-01 用户拍板):`auth whoami` 拿 employeeId →
482
+ * `hrm run hrm.employee.detail` 取 `username`(认证系统当前登录人,不是本机账号)。
483
+ * 结果缓存在 state.json——同一次流水线只查一次;查不到返回 null(调用方退回手工命名)。
484
+ */
485
+ function resolveDeliverUsername(state, dir) {
486
+ if (state.deliveryUsername) return state.deliveryUsername;
487
+ try {
488
+ const who = JSON.parse(runCli(['--json', 'auth', 'whoami'], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, env: cliEnv() }));
489
+ const employeeId = String((who.data || who).employeeId || '').trim();
490
+ if (!employeeId) return null;
491
+ const raw = runCli(['--json', 'hrm', 'run', 'hrm.employee.detail', '--input-json', JSON.stringify({ id: employeeId })], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, env: cliEnv() });
492
+ const detail = JSON.parse(raw);
493
+ const username = String((detail.data || {}).username || '').trim();
494
+ if (!username) return null;
495
+ state.deliveryUsername = username;
496
+ fs.writeFileSync(path.join(dir, 'state.json'), JSON.stringify(state));
497
+ return username;
498
+ } catch {
499
+ return null;
500
+ }
501
+ }
502
+
503
+ /** 规则文件缓存(同一进程只读一次)。 */
504
+ let aigroupRuleFileCache = null;
505
+ function loadAigroupRuleFile() {
506
+ if (aigroupRuleFileCache) return aigroupRuleFileCache;
507
+ try {
508
+ aigroupRuleFileCache = JSON.parse(fs.readFileSync(path.join(path.resolve(__dirname, '..'), 'aigroup', 'default.json'), 'utf8'));
509
+ } catch {
510
+ aigroupRuleFileCache = { rules: [] };
511
+ }
512
+ return aigroupRuleFileCache;
513
+ }
514
+
515
+ /** 组名 → 该规则的 summaryRequiredFields(needSummary:false 的组不校验)。 */
516
+ function requiredFieldsFor(title) {
517
+ const rules = loadAigroupRuleFile().rules || [];
518
+ const rule = rules.find((r) => (Array.isArray(r.workflowName) ? r.workflowName : [r.workflowName])
519
+ .some((name) => name && name !== '*' && (title.includes(String(name)) || String(name).includes(title))));
520
+ if (!rule || rule.needSummary === false) return [];
521
+ return Array.isArray(rule.summaryRequiredFields) ? rule.summaryRequiredFields.filter((f) => typeof f === 'string' && f.trim()) : [];
522
+ }
523
+
423
524
  function exportFinal(result, dir, state, kept, opts = {}) {
424
- const { limit, from, to } = state;
425
- const excluded = (state.exclude || []).join('/');
426
525
  const data = result.data;
427
526
  const groupTotal = (data.wfIdGroup || []).reduce((sum, g) => sum + (Number(g.count) || 0), 0);
428
527
  // CLI 已给 totalCount;缺了(旧版 CLI)就按各组 count 求和兜底,保证三个键恒定存在。
@@ -432,26 +531,89 @@ function exportFinal(result, dir, state, kept, opts = {}) {
432
531
  totalCount,
433
532
  wfIdGroup: data.wfIdGroup,
434
533
  };
435
- const stamp = new Date().toISOString().slice(0, 10);
436
- const scope = from && to
437
- ? `${from}~${to}`
438
- : limit
439
- ? (kept === limit ? `前${limit}条` : `前${limit}条-剩${kept}条`)
440
- : `全部${kept}条`;
441
- const stampPart = from && to ? '' : `-${stamp}`;
442
- const finalFile = path.join(dir, `待办汇总-${scope}${excluded ? `-排除${excluded}` : ''}${stampPart}.json`);
443
534
  // browsertool 载荷的 src 跟着当前认证环境走(用户 2026-10-27 要求不写死租户)。
444
535
  const baseUrl = resolveBaseUrl(state, dir);
445
536
  const stats = groupStats(data.wfIdGroup);
446
537
  const total = groupTotal;
538
+ // ── 交付硬闸门(2026-11-01 用户拍板):缺环节 / 静默降级出不了交付物,只有显式豁免才放行 ──
539
+ const allowEmpty = opts.allowEmptySummary === true;
540
+ const detailFailed = Number(opts.detailFailed) || 0;
541
+ const groupEmpty = Number(opts.groupEmpty) || 0;
542
+ if (!allowEmpty && stats.empty > 0) {
543
+ const emptyIds = [];
544
+ for (const group of data.wfIdGroup || []) {
545
+ for (const item of group.displayItems || []) {
546
+ for (const row of item.type === 'request' ? [item.requestData] : (item.requestDatas || [])) {
547
+ if (row && row.requestId && !String(row.summary || '').trim() && !emptyIds.includes(row.requestId)) emptyIds.push(row.requestId);
548
+ }
549
+ }
550
+ }
551
+ console.error('\n✖ 交付拦截:仍有 %d 条单条摘要为空(多为 --summaries 漏 id / 摘要缓存未回填)。缺失 requestId 前 10 个:%s%s',
552
+ stats.empty, emptyIds.slice(0, 10).join(', '), emptyIds.length > 10 ? ' …' : '');
553
+ console.error(' → 补全 --summaries 后重跑 finish,或加 --allow-empty-summary 显式豁免。');
554
+ process.exit(1);
555
+ }
556
+ if (!allowEmpty && groupEmpty > 0) {
557
+ console.error('\n✖ 交付拦截:仍有 %d 个组摘要为空(--group-summaries 漏了这些 groupId)。补全后重跑,或加 --allow-empty-summary 显式豁免。', groupEmpty);
558
+ process.exit(1);
559
+ }
560
+ if (!(opts.allowDetailFail === true) && detailFailed > 0) {
561
+ console.error('\n✖ 交付拦截:详情取数失败 %d 条——这些条目的摘要只能靠标题兜底,内容可能不完整(防"静默降级+单样本外推")。排查登录态/详情缓存后重跑,或加 --allow-detail-fail 显式豁免。', detailFailed);
562
+ process.exit(1);
563
+ }
564
+ // ⑤ 降级话术黑名单(2026-11-01):摘要正文出现"未在标题体现/详见流程"这类表述,
565
+ // 说明执行方把「自己没取到」写成了「业务对象没有」——按模板回查详情补齐后重写。
566
+ const DEGRADE_PHRASES = ['未在标题体现', '标题未体现', '标题里没有', '详见流程', '详见详情', '待确认'];
567
+ const degraded = [];
568
+ for (const group of data.wfIdGroup || []) {
569
+ for (const item of group.displayItems || []) {
570
+ for (const row of item.type === 'request' ? [item.requestData] : (item.requestDatas || [])) {
571
+ const text = String((row && row.summary) || '');
572
+ const hit = DEGRADE_PHRASES.find((phrase) => text.includes(phrase));
573
+ if (hit && row.requestId) degraded.push({ requestId: String(row.requestId), phrase: hit });
574
+ }
575
+ }
576
+ }
577
+ if (!allowEmpty && degraded.length > 0) {
578
+ console.error('\n✖ 交付拦截:%d 条摘要含降级话术(把取数失败写成业务信息缺失):%s%s',
579
+ degraded.length,
580
+ degraded.slice(0, 5).map((x) => `${x.requestId.slice(-6)}[${x.phrase}]`).join(' '),
581
+ degraded.length > 5 ? ' …' : '');
582
+ console.error(' → 按模板回查详情缓存或 workflow.detail 补齐要素后重写,或加 --allow-empty-summary 显式豁免。');
583
+ process.exit(1);
584
+ }
585
+ // ⑥ 要素级校验(2026-11-01 用户拍板 P2):规则声明了 summaryRequiredFields 的组,
586
+ // 每条摘要必须包含全部要素关键字,缺任一即拒绝导出("模板要素优先于字数偏好"的机器化)。
587
+ const missingFields = [];
588
+ for (const group of data.wfIdGroup || []) {
589
+ const required = requiredFieldsFor(String(group.title || ''));
590
+ if (!required.length) continue;
591
+ for (const item of group.displayItems || []) {
592
+ for (const row of item.type === 'request' ? [item.requestData] : (item.requestDatas || [])) {
593
+ if (!row || !row.requestId) continue;
594
+ const text = String(row.summary || '');
595
+ if (!text.trim()) continue; // 空摘要由闸门①负责
596
+ const missing = required.filter((keyword) => !text.includes(keyword));
597
+ if (missing.length > 0) missingFields.push({ requestId: String(row.requestId), missing });
598
+ }
599
+ }
600
+ }
601
+ if (!allowEmpty && missingFields.length > 0) {
602
+ console.error('\n✖ 交付拦截:%d 条摘要缺模板要素(要素级校验):%s%s',
603
+ missingFields.length,
604
+ missingFields.slice(0, 5).map((x) => `${x.requestId.slice(-6)}缺[${x.missing.join(',')}]`).join(' '),
605
+ missingFields.length > 5 ? ' …' : '');
606
+ console.error(' → 按模板回查详情补齐要素后重写,或加 --allow-empty-summary 显式豁免。');
607
+ process.exit(1);
608
+ }
447
609
  console.log('\n最终结构(共 %d 条 / %d 组|totalCount=%d):', total, data.wfIdGroup.length, totalCount);
448
610
  for (const row of stats.rows) {
449
611
  console.log(` ${row.title} | ${row.count} | ${row.display}${row.singles ? ` +单条${row.singles}` : ''}`);
450
612
  }
451
613
  // 叠加视图:优先组的条目同时留在工作流组里,所以 Σcount 会大于 totalCount(去重条数)。
452
- console.log('\n自检:totalCount=%d(去重)|Σcount=%d(含优先组叠加 %d)|汇总项覆盖 %d + 单条 %d | 空摘要 %d | 文件 %s',
614
+ console.log('\n自检:totalCount=%d(去重)|Σcount=%d(含优先组叠加 %d)|汇总项覆盖 %d + 单条 %d | 空摘要 %d | 三键文件 %s',
453
615
  totalCount, total, Math.max(0, total - totalCount), stats.covered, stats.singles, stats.empty,
454
- opts.writeFile ? finalFile : '(未生成,按交付规则只输出 <browsertool> 载荷)');
616
+ path.join(dir, 'final.data.json'));
455
617
  const meta = result.meta || {};
456
618
  const steps = (meta.timing && meta.timing.steps) || [];
457
619
  console.log('CLI 自报分段:%s', steps.map((s) => `${s.name}:${s.kind}=${fmt(s.durationMs)}`).join(' '));
@@ -496,6 +658,26 @@ function exportFinal(result, dir, state, kept, opts = {}) {
496
658
  lines.push(' ]', '}');
497
659
  // 供 todo-serve.cjs 读取的同构数据(只有三键,无外框):服务与载荷永远读同一份,不会出现两份数据。
498
660
  fs.writeFileSync(path.join(dir, 'final.data.json'), JSON.stringify(payload));
661
+ // 交付文件(2026-11-01 用户拍板):由**脚本**按「认证用户名_YYYYmmddHHMMSS.json」落到 ~/Downloads/,
662
+ // 执行方默认不展示该文件(正文只有组件块)——仅当用户明确要求输出 json 时才交付。
663
+ // 用户名 = auth whoami → hrm.employee.detail.username,state 缓存免重复查询;失败则不生成并在输出里说明。
664
+ let deliveredFile = null;
665
+ const username = resolveDeliverUsername(state, dir);
666
+ if (username) {
667
+ try {
668
+ const d = new Date();
669
+ const pad = (n) => String(n).padStart(2, '0');
670
+ const stamp = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
671
+ const downloads = path.join(os.homedir(), 'Downloads');
672
+ fs.mkdirSync(downloads, { recursive: true });
673
+ deliveredFile = path.join(downloads, `${username}_${stamp}.json`);
674
+ fs.writeFileSync(deliveredFile, JSON.stringify(payload));
675
+ } catch {
676
+ deliveredFile = null;
677
+ }
678
+ }
679
+ if (deliveredFile) console.log('已生成备用文件:%s(默认不展示;仅当用户明确要求输出 json 时再交付——交付口径以 references §6.1 顶部为准)', deliveredFile);
680
+ else console.log('备用文件未生成(用户名查询失败或 Downloads 不可写):如用户要求输出 json,把 %s 复制为 ~/Downloads/<用户名>_<YYYYmmddHHMMSS>.json 后交付', path.join(dir, 'final.data.json'));
499
681
  // 数据源模式:`--data-source <url>` 直接用给定地址;`--serve [端口]` 现起一个本地服务拿地址。
500
682
  let dataSourceUrl = opts.dataSource && opts.dataSource !== true ? String(opts.dataSource) : null;
501
683
  if (!dataSourceUrl && opts.serve) dataSourceUrl = publishSnapshot(dir, opts.serve);
@@ -505,9 +687,8 @@ function exportFinal(result, dir, state, kept, opts = {}) {
505
687
  console.log('\n数据源模式:params.dataSource = %s', dataSourceUrl);
506
688
  console.log(' 组件改为 GET 该地址(响应体就是 params.data 的三键对象);单进程多快照,快照空闲 30 分钟后自动清理');
507
689
  }
508
- // 用户 2026-10-27 定稿:交付 = 回复里内联 <browsertool> 整块,**不再生成 JSON 文件**。
509
- // `--file` 仅保留作调试(写一份纯载荷 JSON),交付流程不再使用它。
510
- if (opts.writeFile) fs.writeFileSync(finalFile, finalText);
690
+ // 交付 = 回复里内联 <browsertool> 整块 + 另存 final.data.json(Agent 复制它按 §6.1 命名交付)。
691
+ // 旧 `--file` 分支已删(2026-11-01):它写的 `待办汇总-*.json` 是外框归档,命名/内容均为旧口径,易误当交付物。
511
692
  const delivery = `<browsertool>\n${finalText.trimEnd()}\n</browsertool>`;
512
693
  console.log('\n[JSON_BEGIN]');
513
694
  console.log(delivery);
@@ -525,7 +706,6 @@ function exportFinal(result, dir, state, kept, opts = {}) {
525
706
  console.log('\n耗时摘要(仅 CLI 执行链路,不含模型往返):');
526
707
  console.log(conclusion);
527
708
  }
528
- return opts.writeFile ? finalFile : null;
529
709
  }
530
710
 
531
711
  /**
@@ -566,6 +746,17 @@ function cmdPrep(args) {
566
746
  if (args['sub-category'] && args['sub-category'] !== true) {
567
747
  input.subCategory = String(args['sub-category']);
568
748
  }
749
+ // 2026-11-02:小分类的等价直传入口。显式 tabid 优先级高于 subCategory/tabName
750
+ // (CLI buildSearchQuery:先解析 subCategory/tabName,再用显式 tabid 覆盖)。
751
+ // 仅影响本次汇总取数范围,不触碰 workflow.search 的默认行为。
752
+ if (args.tabid && args.tabid !== true) {
753
+ input.tabid = String(args.tabid);
754
+ }
755
+ // 2026-11-02:备注筛选(getTodoListData params.isremarks),多个数字英文逗号拼接。
756
+ // 仅汇总链路生效(CLI collect 内部注入 params),workflow.search 不受影响。
757
+ if (args.isremarks && args.isremarks !== true) {
758
+ input.isremarks = String(args.isremarks);
759
+ }
569
760
  // 摘要字数口径要带进 collect:否则缓存命中按默认 50 字判定,
570
761
  // 用户要求「10 字以内」时仍会回填旧的长摘要(2026-10-22 实测踩到)。
571
762
  if (args['summary-chars'] && args['summary-chars'] !== true) {
@@ -595,6 +786,29 @@ function cmdPrep(args) {
595
786
  const kept = exclude.length ? all.filter((row) => !exclude.includes(String(row.workflowName || ''))) : all;
596
787
  const dropped = all.length - kept.length;
597
788
 
789
+ // 2026-11-03 用户定稿:collect 空结果按「是否带显式限制条件」给不同文案,直接结束(exit 0),
790
+ // 不进 build(CLI 会抛 items_empty 校验错误)。
791
+ // - 无限制条件(如「汇总我的待办」)⇒ 「您当前没有待办。」
792
+ // - 带限制条件(时间范围 / 小分类 / tabid / 参与身份 / 排除清单 / 侧重点)⇒
793
+ // 「您当前没有符合条件的待办,可以尝试调整限制数据的范围后重试。」并回显条件。
794
+ if (all.length === 0 || kept.length === 0) {
795
+ const constraints = [];
796
+ if (days > 0) constraints.push(`近 ${days} 天`);
797
+ if (args.from && args.to) constraints.push(`${args.from} 至 ${args.to}`);
798
+ if (input.subCategory) constraints.push(`小分类「${input.subCategory}」`);
799
+ if (input.tabid) constraints.push(`tabid=${input.tabid}`);
800
+ if (input.isremarks) constraints.push(`参与身份 ${input.isremarks}`);
801
+ if (exclude.length) constraints.push(`排除流程「${exclude.join('、')}」`);
802
+ if (input.groupFocus) constraints.push(`侧重点「${input.groupFocus}」`);
803
+ if (all.length === 0 && constraints.length === 0) {
804
+ console.log('您当前没有待办。');
805
+ } else {
806
+ console.log('您当前没有符合条件的待办,可以尝试调整限制数据的范围后重试。%s',
807
+ constraints.length ? `(当前条件:${constraints.join('、')})` : '');
808
+ }
809
+ return 'EMPTY';
810
+ }
811
+
598
812
  // 组结构直接用 CLI 返回的 wfIdGroup(已做 priority 抽取,不能再自己按 workflowName 复算,
599
813
  // 否则会把被抽进「建议优先处理」的条目重复计入原流程)。
600
814
  // 注意:wfIdGroup[].requestDatas 里**没有 workflowName**,要按 requestId 从 items 回查,
@@ -657,12 +871,44 @@ function cmdPrep(args) {
657
871
  fmt(collectMs), fmt(collectWallMs));
658
872
  console.log('输出目录:%s\n', dir);
659
873
  const focusKinds = focusCoversKinds(args.focus && args.focus !== true ? args.focus : '');
874
+ // 规则里 focus:false 的组**必须**由模型给 aiGroups(如 安全问题处理工单),prep 阶段就标出来,
875
+ // 不要等 build 报 ai_groups_required 才知道(2026-11-01 用户确认的规范缺陷)。
876
+ // 同时**回显该组的 groupingPrompt(组名词表/分法)与 requestSummaryPrompt(摘要模板)**——
877
+ // 这两样只存在于 aigroup/default.json,规则要求"生成 aiGroups 前必须先读它",
878
+ // 但 prep 原本不输出 ⇒ 执行方只能凭正文推断(自造组名/漏摘要要素的根因,2026-11-01 复盘定稿)。
879
+ const mustPlanRules = [];
880
+ try {
881
+ const ruleFile = JSON.parse(fs.readFileSync(path.join(path.resolve(__dirname, '..'), 'aigroup', 'default.json'), 'utf8'));
882
+ for (const rule of ruleFile.rules || []) {
883
+ if (rule.aiGroup && rule.aiGroup.groupRule && rule.aiGroup.groupRule.focus === false) {
884
+ const names = Array.isArray(rule.workflowName) ? rule.workflowName : [rule.workflowName];
885
+ for (const name of names) {
886
+ if (name && name !== '*') {
887
+ mustPlanRules.push({
888
+ name: String(name),
889
+ groupingPrompt: String((rule.aiGroup.groupRule || {}).groupingPrompt || ''),
890
+ requestSummaryPrompt: String(rule.requestSummaryPrompt || ''),
891
+ });
892
+ }
893
+ }
894
+ }
895
+ }
896
+ } catch { /* 规则文件读不到就不标注(build 仍会硬校验兜底) */ }
897
+ const matchRule = (title) => mustPlanRules.find((r) => title.includes(r.name) || r.name.includes(title));
898
+ const needsPlan = (title, count) => count >= 3 && (title.includes('内部留言') || Boolean(matchRule(title)));
660
899
  for (const group of planGroups) {
661
900
  // 只有「默认确定性类型 + 侧重点没覆盖本类型」才算无需分组;被覆盖时照常列出条目。
662
901
  const kind = kindOfTitle(group.title);
663
902
  const skip = group.groupId === 'priority' || group.count <= 1 || (kind !== '' && !focusKinds.has(kind));
664
- console.log(`=== ${group.groupId} | ${group.title} | ${group.count}${skip ? ' [无需分组]' : ''}`);
903
+ const rule = skip ? null : matchRule(group.title);
904
+ const marker = skip ? ' [无需分组]' : (rule || group.title.includes('内部留言') ? ' [必须给 plan]' : '');
905
+ console.log(`=== ${group.groupId} | ${group.title} | ${group.count}${marker}`);
665
906
  if (skip) continue;
907
+ // 回显规则内容:组名/分法只按 groupingPrompt 来(词表不得自造),摘要按 requestSummaryPrompt 写
908
+ if (rule) {
909
+ if (rule.groupingPrompt) console.log(` ▸ 分组口径(groupingPrompt,组名/分法只按它来,不得自造):${rule.groupingPrompt}`);
910
+ if (rule.requestSummaryPrompt) console.log(` ▸ 摘要模板(requestSummaryPrompt,本组单条摘要照它写):${rule.requestSummaryPrompt}`);
911
+ }
666
912
  for (const row of group.rows) {
667
913
  const hint = fieldHintById.get(row.requestId) || '';
668
914
  // 提示放在**同一行**:两行格式容易让人(和模型)回头去匹配标题,而标题里往往没有类型词。
@@ -670,7 +916,7 @@ function cmdPrep(args) {
670
916
  }
671
917
  }
672
918
  console.log('\n下一步:node %s build --dir %s', path.basename(__filename), dir);
673
- console.log('(只有内部留言组、或用户给了侧重点时才需要再补 --plan <aiGroups.json>;其余走确定性分组)');
919
+ console.log('(标了 [必须给 plan] 的组——内部留言、以及规则 focus:false 的组如 安全问题处理工单——build 时必须带 --plan <aiGroups.json>;组名/分法/摘要口径见各组下回显的规则内容;其余走确定性分组)');
674
920
  }
675
921
 
676
922
  function cmdBuild(args) {
@@ -680,7 +926,7 @@ function cmdBuild(args) {
680
926
  const kept = readJson(path.join(dir, 'items.json'));
681
927
  const planGroups = state.planGroups || [];
682
928
  const countById = new Map(planGroups.map((g) => [g.groupId, g.count]));
683
- // `--plan` 可选:其他类默认不按标题二次分组,很多批次根本不需要 AI 计划。
929
+ // `--plan`:确定性类型可不传;prep 标了 [必须给 plan] 的组(内部留言 / focus:false 组)不传会被 CLI 拒(ai_groups_required)。
684
930
  // 不传时直接走 CLI 确定性分组(等价于 aiGroups 未传)。
685
931
  let aiGroups;
686
932
  if (planFile) {
@@ -742,17 +988,36 @@ function cmdBuild(args) {
742
988
  fs.writeFileSync(path.join(dir, 'state.json'), JSON.stringify(state));
743
989
 
744
990
  if (pending.length === 0) {
745
- exportFinal(sg, dir, state, kept.length, { writeFile: args.file === true, timingReport: args['timing-report'] });
991
+ // 即使无待写摘要也必须走 finish:summary 阶段还负责满意度类(needSummary:false)的标题
992
+ // 填充与交付闸门——smartGroup 半成品直接导出会让这类条目空着(2026-11-01 闸门真机首跑抓到)。
993
+ cmdFinish({ ...args, dir });
746
994
  return;
747
995
  }
748
- const summaryTarget = pending[0] && pending[0].suggestedMaxChars ? pending[0].suggestedMaxChars : 50;
749
- console.log(`\n待写摘要(把结果写成 aiSummaries.json,元素 {requestId,summary},约 ${summaryTarget} 字):`);
750
- for (const item of pending) {
751
- console.log(`### ${item.requestId}`);
752
- console.log(`T: ${String(item.requestNameTitle || '').slice(0, 70)}`);
753
- console.log(`C: ${String(item.context || '').replace(/\s+/g, ' ').slice(0, 240)}`);
996
+ // 2026-11-02 P3-B:素材无损落盘(此前整段吐控制台,268 条时 build 输出达 850KB,
997
+ // 模型要多轮读长文本还易被截断);控制台只留批次索引,模型用 Read 按需读 need-summaries.json。
998
+ const needFile = path.join(dir, 'need-summaries.json');
999
+ fs.writeFileSync(needFile, JSON.stringify(batches, null, 2));
1000
+ console.log(`\n⚠ NEEDS_SUMMARIES:%d 条单条摘要待写(完整素材已写入 %s):`, pending.length, needFile);
1001
+ for (const b of batches) {
1002
+ const items = b.items || [];
1003
+ if (items.length === 0) continue;
1004
+ console.log(` - ${b.batchId}|${b.groupTitle}${b.subGroupTitle && b.subGroupTitle !== b.groupTitle ? ` → ${b.subGroupTitle}` : ''}|${items.length} 条|目标字数 ${b.suggestedMaxChars}${b.prompt ? `|口径:${b.prompt}` : ''}`);
1005
+ if (b.hint) console.log(` 提示:${b.hint}`);
1006
+ }
1007
+ console.log(' → 读 need-summaries.json(items[].requestId + context),摘要写成 aiSummaries.json(元素 {requestId,summary})后重跑:');
1008
+ console.log(' node %s finish --dir %s --summaries <aiSummaries.json> --timing-report', path.basename(__filename), dir);
1009
+ // 组摘要(aiGroup.groupSummary.needAI=true 的汇总项):口径随批次下发,模型按它写 aiGroupSummaries
1010
+ const gsBatches = sg.data.groupSummaryBatches || [];
1011
+ if (gsBatches.length > 0) {
1012
+ console.log('\n待写组摘要(写成 aiGroupSummaries.json,元素 {groupId,summary}):');
1013
+ for (const b of gsBatches) {
1014
+ console.log(`### 组摘要 ${b.groupId}|${b.parentTitle} → ${b.title}|${b.count} 条|目标字数 ${b.suggestedMaxChars}`);
1015
+ if (b.prompt) console.log(`口径:${b.prompt}`);
1016
+ if (b.hint) console.log(`提示:${b.hint}`);
1017
+ if (b.stats && b.stats.tail) console.log(`tail:${b.stats.tail}`);
1018
+ }
1019
+ console.log('\nfinish 时加:--group-summaries <aiGroupSummaries.json>');
754
1020
  }
755
- console.log('\n下一步:node %s finish --dir %s --summaries <aiSummaries.json>', path.basename(__filename), dir);
756
1021
  }
757
1022
 
758
1023
  function cmdFinish(args) {
@@ -788,6 +1053,9 @@ function cmdFinish(args) {
788
1053
  fs.writeFileSync(path.join(dir, 'state.json'), JSON.stringify(state));
789
1054
  console.log('summary 内部 %s / 含进程启动 %s(回填 %d 条,其余走缓存预填)',
790
1055
  summaryInternal === null ? '-' : fmt(summaryInternal), fmt(summaryWallMs), aiSummaries.length);
1056
+ // 未回填的缺失 id 不在这里单列:summary 阶段的信封不带 summaryBatches/summaryStats,
1057
+ // 空 id 列表统一由 exportFinal 的闸门①从最终结构里就地提取(能直接给出 requestId)。
1058
+ // 未知 requestId 的 aiSummaries 由 CLI 侧硬校验拒绝(ai_summary_unknown_request,非零退出)。
791
1059
  let finalResult = result;
792
1060
  const groupBatches = result.data.groupSummaryBatches || [];
793
1061
  if (groupBatches.length > 0) {
@@ -796,7 +1064,10 @@ function cmdFinish(args) {
796
1064
  : null;
797
1065
  if (groupSummariesFile) {
798
1066
  // 组摘要必须由模型写、且每轮重算;这里只是把「回填」并进同一次命令,省掉一次模型往返。
799
- const aiGroupSummaries = readJson(groupSummariesFile);
1067
+ // need-group-summaries.json 可直接指回:这里投影成 CLI 需要的 {groupId,summary},
1068
+ // 多余字段(title/items/stats 等)不外传,避免 CLI 白名单校验拒绝。
1069
+ const aiGroupSummaries = readJson(groupSummariesFile)
1070
+ .map((x) => ({ groupId: x.groupId || x.id, summary: x.summary }));
800
1071
  const t2 = Date.now();
801
1072
  const gs = runStage({
802
1073
  smartSummaryTitle: result.data.smartSummaryTitle,
@@ -816,9 +1087,22 @@ function cmdFinish(args) {
816
1087
  for (const text of gsWarnings) console.log(' ⚠ %s', text);
817
1088
  finalResult = gs;
818
1089
  } else {
819
- console.log('\n⚠ 还有 %d 个组摘要素材待模型生成(groupSummary 阶段):', groupBatches.length);
820
- for (const batch of groupBatches) console.log(JSON.stringify(batch).slice(0, 400));
821
- console.log(' → 写成 [{groupId,summary}] 后加 `--group-summaries <file>` 重跑本命令,可在同一次调用里回填');
1090
+ // 2026-11-02 P2 定稿:组摘要未回填 ≠ 校验失败。完整素材落盘(此前控制台每批只打
1091
+ // JSON.stringify(batch).slice(0, 400) 的预览,等于没有素材),以 exit 0 + NEEDS_GROUP_SUMMARIES
1092
+ // 返回、不导出;真失败(空单条摘要/降级话术/要素缺失/详情失败)仍由 exportFinal 以 exit 1 拦截。
1093
+ const needFile = path.join(dir, 'need-group-summaries.json');
1094
+ fs.writeFileSync(needFile, JSON.stringify(groupBatches, null, 2));
1095
+ console.log('\n⚠ NEEDS_GROUP_SUMMARIES:%d 个组摘要待回填(完整素材已写入 %s):', groupBatches.length, needFile);
1096
+ for (const batch of groupBatches) {
1097
+ console.log(' - %s|%s → %s|%d 条', batch.groupId, batch.parentTitle, batch.title, batch.count);
1098
+ }
1099
+ if (args['allow-empty-summary'] === true) {
1100
+ console.log(' --allow-empty-summary 已指定:跳过回填、按空组摘要继续导出。');
1101
+ } else {
1102
+ console.log(' → 在该文件的每个元素里补 "summary" 字段(脚本会自动只取 groupId/summary),重跑:');
1103
+ console.log(' node %s finish --dir %s --group-summaries %s --timing-report', path.basename(__filename), dir, needFile);
1104
+ return; // 不导出、退出码 0:素材已备齐、等回填,不是失败
1105
+ }
822
1106
  }
823
1107
  }
824
1108
  // 2026-10-28 用户 OA 页面实测通过:默认走本地数据源(params.dataSource),
@@ -827,10 +1111,15 @@ function cmdFinish(args) {
827
1111
  ? false
828
1112
  : (args.serve === undefined ? true : args.serve);
829
1113
  exportFinal(finalResult, dir, state, kept.length, {
830
- writeFile: args.file === true,
831
1114
  timingReport: args['timing-report'],
832
1115
  dataSource: args['data-source'],
833
1116
  serve: serveOption,
1117
+ // 交付硬闸门(2026-11-01):空摘要 / 详情取数失败都要显式豁免才能出文件。
1118
+ // 详情取数发生在 smartGroup 阶段 ⇒ failed 从 smartgroup.json 的 data.summaryStats 取。
1119
+ allowEmptySummary: args['allow-empty-summary'] === true,
1120
+ allowDetailFail: args['allow-detail-fail'] === true,
1121
+ detailFailed: ((sg.data || {}).summaryStats || {}).failed || 0,
1122
+ groupEmpty: (finalResult.meta && finalResult.meta.groupEmpty) || 0,
834
1123
  });
835
1124
  console.log('总耗时(脚本口径):%s', fmt(Date.now() - Date.parse(state.startedAt)));
836
1125
  }
@@ -847,23 +1136,30 @@ function cmdRun(args) {
847
1136
  const dir = args.out && args.out !== true
848
1137
  ? String(args.out)
849
1138
  : `/tmp/todo-summary-${new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14)}`;
850
- cmdPrep({ ...args, out: dir });
1139
+ // collect 空结果已在 prep 里给出用户文案并结束(EMPTY),不再进 build/finish
1140
+ if (cmdPrep({ ...args, out: dir }) === 'EMPTY') return;
851
1141
  cmdBuild({ ...args, dir });
1142
+ // 无待写摘要时 cmdBuild 已经内部走完 cmdFinish(含满意度标题填充与交付闸门),这里直接收工
1143
+ if (fs.existsSync(path.join(dir, 'final.data.json'))) return;
1144
+ // build→finish 已把组摘要素材落盘并停下(NEEDS_GROUP_SUMMARIES,exit 0)⇒ 不再重复跑 finish,
1145
+ // 也不再重复打印(cmdFinish 刚打过完整指引),静默收工(2026-11-02 P2)。
1146
+ if (fs.existsSync(path.join(dir, 'need-group-summaries.json'))) return;
852
1147
  const sg = readJson(path.join(dir, 'smartgroup.json'));
853
1148
  const pending = ((sg.data.summaryBatches || []).flatMap((batch) => batch.items || []));
854
1149
  if (pending.length > 0) {
855
- console.log('\n⚠ 有 %d 条待写摘要:写成 aiSummaries.json 后跑 finish(**不用重跑** collect/smartGroup)', pending.length);
856
- console.log(' node %s finish --dir %s --summaries <aiSummaries.json> --file --timing-report', path.basename(__filename), dir);
1150
+ console.log('\n⚠ 有 %d 条待写摘要:素材见 %s;写成 aiSummaries.json 后跑 finish(**不用重跑** collect/smartGroup)', pending.length, path.join(dir, 'need-summaries.json'));
1151
+ console.log(' node %s finish --dir %s --summaries <aiSummaries.json> --timing-report', path.basename(__filename), dir);
857
1152
  return;
858
1153
  }
859
- // 没有待写摘要 ⇒ 终态可以立刻出,省掉一次约 7s 的进程启动
1154
+ // build 停下但没导出(如组摘要待回填)⇒ 把 finish 跑完(它自己会按闸门决定放行/拦截)
860
1155
  cmdFinish({
861
1156
  dir,
862
- file: args.file === true,
863
1157
  'timing-report': args['timing-report'],
864
1158
  'group-summaries': args['group-summaries'],
865
1159
  serve: args.serve,
866
1160
  'no-serve': args['no-serve'],
1161
+ 'allow-empty-summary': args['allow-empty-summary'] === true,
1162
+ 'allow-detail-fail': args['allow-detail-fail'] === true,
867
1163
  });
868
1164
  }
869
1165
 
@@ -875,10 +1171,14 @@ else if (cmd === 'finish') cmdFinish(args);
875
1171
  else if (cmd === 'run') cmdRun(args);
876
1172
  else {
877
1173
  console.log('用法:node ' + path.basename(__filename) + ' <run|prep|build|finish> [options]');
878
- console.log(' run --days 30 --limit 500 [--out DIR] [--file] [--timing-report] # 一次跑完 prep+build(无待写摘要时连 finish 一起)');
879
- console.log(' prep --limit 100 [--exclude 内部留言] [--days 1460] [--sub-category 待处理] [--out DIR]');
880
- console.log(' build --dir DIR [--plan aiGroups.json] # 不传 plan = 直接走确定性分组');
881
- console.log(' finish --dir DIR [--summaries aiSummaries.json] [--group-summaries aiGroupSummaries.json] [--file] [--timing-report]');
1174
+ console.log(' run [--days N | --from A --to B] [--limit 500] [--out DIR] [--timing-report] # 一次跑完 prep+build;用户没说时间段就都不传 = 不限时间取前 500 条');
1175
+ console.log(' prep --limit 100 [--exclude 内部留言] [--days 1460] [--sub-category 待处理] [--tabid 7] [--isremarks 1,2] [--out DIR] # 小分类:--sub-category/--tabid(显式 tabid 优先);isremarks=备注筛选(数字逗号拼接)');
1176
+ console.log(' build --dir DIR [--plan aiGroups.json] # prep 标注 [必须给 plan] 的组必须传;其余不传走确定性分组');
1177
+ console.log(' finish --dir DIR [--summaries aiSummaries.json] [--group-summaries aiGroupSummaries.json] [--timing-report]');
1178
+ console.log(' 待写摘要素材:build/ run 停下时写 <out>/need-summaries.json(无损),控制台只留批次索引;写 aiSummaries.json 后 finish --summaries 回填');
1179
+ console.log(' 组摘要未回填:以 exit 0 + NEEDS_GROUP_SUMMARIES 返回(不导出),完整素材在 <out>/need-group-summaries.json;');
1180
+ console.log(' 在该文件补 summary 后用 --group-summaries 指回它重跑。真失败(空摘要/降级话术/要素缺失/详情失败)仍 exit 1;');
1181
+ console.log(' 豁免:--allow-empty-summary / --allow-detail-fail');
882
1182
  console.log(' 默认:载荷只带 params.dataSource(现起本地服务,空闲 30 分钟自动退出);--no-serve 退回内联 data');
883
1183
  console.log(' 提示:`--timing-report` 会在控制台打一段耗时摘要(阶段墙钟 / CLI 内部),**不产出 md**');
884
1184
  process.exit(2);
@@ -1,10 +1,5 @@
1
1
  import { createRequire as __esbuildCreateRequire } from 'node:module';
2
2
  const require = __esbuildCreateRequire(import.meta.url);
3
- import {
4
- imBusinessError,
5
- imConfirmationError,
6
- imValidationError
7
- } from "./chunk-ZEDWSH32.js";
8
3
  import {
9
4
  collapseNewlines,
10
5
  collectI18nIds,
@@ -17,6 +12,11 @@ import {
17
12
  parseMsg,
18
13
  senderName
19
14
  } from "./chunk-YLCYX7LE.js";
15
+ import {
16
+ imBusinessError,
17
+ imConfirmationError,
18
+ imValidationError
19
+ } from "./chunk-ZEDWSH32.js";
20
20
 
21
21
  // src/shortcuts/im/operations/types.ts
22
22
  var READ = "read";