weaver-work-cli 0.1.39 → 0.1.42

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 (197) hide show
  1. package/README.md +4 -2
  2. package/dist/chunks/{archive-ENZYBBME.js → archive-J4I6MCKF.js} +2 -2
  3. package/dist/chunks/{archive-oa-YWLEC5V4.js → archive-oa-BRZKKTO4.js} +2 -2
  4. package/dist/chunks/{asset-OOV6DWPA.js → asset-L7D4FAOZ.js} +2 -2
  5. package/dist/chunks/{asset-idle-RE632MY5.js → asset-idle-DADLLV75.js} +2 -2
  6. package/dist/chunks/{attachment-content-RWN2FCLL.js → attachment-content-K4C7VLWQ.js} +3 -3
  7. package/dist/chunks/{blog-PEPOJAL7.js → blog-BVGQ7NZL.js} +2 -2
  8. package/dist/chunks/{caizhilian-WYIQDVPJ.js → caizhilian-SAADNFD6.js} +2 -2
  9. package/dist/chunks/{calendar-CIARBAOX.js → calendar-Q7NQY5Z2.js} +61 -23
  10. package/dist/chunks/{chunk-F2EQXUEE.js → chunk-3KTFS4IA.js} +397 -159
  11. package/dist/chunks/{chunk-G2JSIUWF.js → chunk-4PD5NWAX.js} +89 -0
  12. package/dist/chunks/{chunk-HJYVHXUQ.js → chunk-4RAE6JXP.js} +1 -1
  13. package/dist/chunks/{chunk-CQHGNCK2.js → chunk-72CKAUAG.js} +41 -8
  14. package/dist/chunks/{chunk-K3RUEEOL.js → chunk-APVFJUQH.js} +2 -2
  15. package/dist/chunks/{chunk-BJGXMFGD.js → chunk-BBKDXQEC.js} +2 -2
  16. package/dist/chunks/chunk-BIPYLLVS.js +501 -0
  17. package/dist/chunks/{chunk-GVPJZXJ2.js → chunk-EKWAQSLU.js} +92 -50
  18. package/dist/chunks/{chunk-XXJOSZTQ.js → chunk-FOAA27U5.js} +381 -57
  19. package/dist/chunks/{chunk-I2HB737U.js → chunk-G4UR6NXN.js} +2 -2
  20. package/dist/chunks/{chunk-YS5DPN4T.js → chunk-GEWYMQME.js} +1 -1
  21. package/dist/chunks/{chunk-P2DAISYW.js → chunk-GHXCDPTM.js} +1 -1
  22. package/dist/chunks/{chunk-L4N7B7ZD.js → chunk-IZ7S7QUS.js} +4 -4
  23. package/dist/chunks/{chunk-NLK2RZKG.js → chunk-JPRO6ER2.js} +122 -2214
  24. package/dist/chunks/{chunk-36DIFZAH.js → chunk-LADPYOWC.js} +2 -2
  25. package/dist/chunks/chunk-LDZQPUIA.js +1238 -0
  26. package/dist/chunks/chunk-NKZRQXST.js +2546 -0
  27. package/dist/chunks/{chunk-EINRJHQ3.js → chunk-QKNKR4RH.js} +3 -3
  28. package/dist/chunks/{chunk-T7JDJJ7H.js → chunk-R4CHUSKC.js} +126 -19
  29. package/dist/chunks/{chunk-EZGFHJFW.js → chunk-SA6VBIHY.js} +88 -15
  30. package/dist/chunks/{chunk-ZXRNHZTE.js → chunk-TTPJJJC4.js} +2 -2
  31. package/dist/chunks/{chunk-KTCB5R5H.js → chunk-W7BEHOSR.js} +2 -2
  32. package/dist/chunks/{chunk-MUQUHKPG.js → chunk-YRSZQUD2.js} +2 -2
  33. package/dist/chunks/{conflict-DRMDOL56.js → conflict-F2OMZRTO.js} +2 -2
  34. package/dist/chunks/{contract-73MERZWN.js → contract-5BETHRSG.js} +2 -2
  35. package/dist/chunks/{cowork-HNGBPD7A.js → cowork-3MN6EQQO.js} +27 -10
  36. package/dist/chunks/create-flow-5FQNNMVO.js +55 -0
  37. package/dist/chunks/{ebuilder-form-OY6DVNRK.js → ebuilder-form-SBHOYBUD.js} +3 -3
  38. package/dist/chunks/{ehr-RAQNEQO7.js → ehr-KMOJULKG.js} +432 -1234
  39. package/dist/chunks/env-LB7NBL2O.js +19 -0
  40. package/dist/chunks/{esb-6XV7C7YV.js → esb-W2PJCFCX.js} +4 -4
  41. package/dist/chunks/factory-6ZFQXQ46.js +30 -0
  42. package/dist/chunks/{fna-XTSRB7G7.js → fna-ZQPCUV4R.js} +2 -2
  43. package/dist/chunks/group-chat-2HHAJN46.js +20 -0
  44. package/dist/chunks/{host-YMBXWFWX.js → host-4CBHGOHT.js} +3 -3
  45. package/dist/chunks/{host-A7J6GDUP.js → host-ATKVNZAT.js} +3 -3
  46. package/dist/chunks/{host-3F5UM5GN.js → host-BQH22NTQ.js} +3 -3
  47. package/dist/chunks/{host-YAQAPYNP.js → host-X2CSLFM6.js} +3 -3
  48. package/dist/chunks/{host-VGPVP7YF.js → host-YTODKMSL.js} +3 -3
  49. package/dist/chunks/{hrm-TLHKC545.js → hrm-FFM4UJFS.js} +4 -4
  50. package/dist/chunks/{im-YOUCFMUK.js → im-VEHKDQPB.js} +8 -8
  51. package/dist/chunks/{invoice-WNBS2B4C.js → invoice-LVIKSIIF.js} +3 -3
  52. package/dist/chunks/{jiuchuanhui-XABWS5RN.js → jiuchuanhui-X6DINO5P.js} +2 -2
  53. package/dist/chunks/{lazy-dispatch-NLZ5UQQI.js → lazy-dispatch-KSWTN5L6.js} +4 -4
  54. package/dist/chunks/{mail-LRYMBFNL.js → mail-VDKRGUD3.js} +9 -4
  55. package/dist/chunks/{meeting-ESXL6MFI.js → meeting-CLC6HP3Z.js} +12 -10
  56. package/dist/chunks/{okr-EOV65SSF.js → okr-CHG4OO3U.js} +2 -2
  57. package/dist/chunks/{plan-YFPVKW6Y.js → plan-XWR75667.js} +2 -2
  58. package/dist/chunks/{project-MDVZUYQW.js → project-5BXOOKMG.js} +40 -23
  59. package/dist/chunks/recruit-stage-DGMFNNDE.js +4507 -0
  60. package/dist/chunks/{registry-VT5Y2HLA.js → registry-7FFOXIAW.js} +4 -4
  61. package/dist/chunks/{registry-QCLZEDAP.js → registry-BKBZPBAJ.js} +10 -8
  62. package/dist/chunks/{registry-YX4CSEMD.js → registry-RHMW6ANW.js} +2 -2
  63. package/dist/chunks/{registry-NYPYMZXG.js → registry-Z43434VG.js} +1 -1
  64. package/dist/chunks/{room-recommend-L3MWH4JW.js → room-recommend-62H7ZQQ6.js} +4 -4
  65. package/dist/chunks/{schedule-UASAVDVD.js → schedule-FNXKH3DQ.js} +4 -4
  66. package/dist/chunks/{workflow-KDGIUCNW.js → workflow-ADHKCAJK.js} +4 -4
  67. package/dist/index.js +33 -28
  68. package/dist/internal/e10/auth/commands.js +110 -56
  69. package/dist/internal/e10/auth/messages.js +6 -0
  70. package/dist/internal/e10/auth/session.js +28 -1
  71. package/dist/internal/e10/auth/xiaoe.js +315 -48
  72. package/dist/internal/e10/auth-recovery.js +36 -5
  73. package/dist/internal/e10/client.js +13 -0
  74. package/dist/internal/e10/context.js +61 -9
  75. package/dist/internal/e10/index.js +1 -1
  76. package/dist/internal/e10/network-error.js +93 -0
  77. package/dist/internal/e10/request-runtime.js +15 -0
  78. package/dist/shortcuts/calendar/operations/read.js +31 -22
  79. package/dist/shortcuts/calendar/operations/write.js +45 -1
  80. package/dist/shortcuts/cowork/operations/discuss-write.js +5 -1
  81. package/dist/shortcuts/cowork/operations/item-write.js +5 -3
  82. package/dist/shortcuts/cowork/operations/member-write.js +3 -1
  83. package/dist/shortcuts/cowork/operations/permission-write.js +5 -1
  84. package/dist/shortcuts/cowork/operations/shared.js +7 -0
  85. package/dist/shortcuts/ehr/operations/attend-write.js +43 -1
  86. package/dist/shortcuts/ehr/operations/hr-contact.js +7 -4
  87. package/dist/shortcuts/ehr/operations/hr-report.js +13 -7
  88. package/dist/shortcuts/ehr/operations/recruit-candidate-import.js +134 -59
  89. package/dist/shortcuts/ehr/operations/recruit-write.js +262 -17
  90. package/dist/shortcuts/ehr/operations/recruit.js +10 -4
  91. package/dist/shortcuts/ehr/operations/shared.js +47 -0
  92. package/dist/shortcuts/hrm/operations/org.js +118 -0
  93. package/dist/shortcuts/index.js +5 -0
  94. package/dist/shortcuts/mail/operations/mail-send.js +8 -2
  95. package/dist/shortcuts/meeting/operations/eb/catalog.js +9 -2
  96. package/dist/shortcuts/meeting/operations/eb/create-flow.js +461 -127
  97. package/dist/shortcuts/meeting/operations/eb/crm-search.js +87 -5
  98. package/dist/shortcuts/meeting/operations/eb/factory.js +33 -0
  99. package/dist/shortcuts/meeting/operations/eb/flow-memory.js +49 -0
  100. package/dist/shortcuts/meeting/operations/eb/group-chat.js +605 -0
  101. package/dist/shortcuts/meeting/operations/eb/index.js +2 -0
  102. package/dist/shortcuts/meeting/operations/eb/person-cache.js +96 -0
  103. package/dist/shortcuts/meeting/operations/eb/room-recommend.js +44 -6
  104. package/dist/shortcuts/meeting/operations/eb/room-summary.js +2 -0
  105. package/dist/shortcuts/meeting/operations/eb/usage-cache.js +99 -0
  106. package/dist/shortcuts/meeting/operations/from-group-chat.js +35 -12
  107. package/dist/shortcuts/project/operations/read.js +6 -25
  108. package/dist/shortcuts/project/operations/shared.js +22 -0
  109. package/dist/shortcuts/project/operations/write.js +28 -2
  110. package/dist/shortcuts/recruit-stage/context.js +119 -0
  111. package/dist/shortcuts/recruit-stage/continuation.js +73 -0
  112. package/dist/shortcuts/recruit-stage/errors.js +65 -0
  113. package/dist/shortcuts/recruit-stage/host.js +109 -0
  114. package/dist/shortcuts/recruit-stage/index.js +115 -0
  115. package/dist/shortcuts/recruit-stage/manifest.js +29 -0
  116. package/dist/shortcuts/recruit-stage/operations/interview-arrange.js +722 -0
  117. package/dist/shortcuts/recruit-stage/operations/interview-feedback.js +269 -0
  118. package/dist/shortcuts/recruit-stage/operations/jd-create.js +630 -0
  119. package/dist/shortcuts/recruit-stage/operations/list.js +431 -0
  120. package/dist/shortcuts/recruit-stage/operations/registry.js +38 -0
  121. package/dist/shortcuts/recruit-stage/operations/resume-import.js +1198 -0
  122. package/dist/shortcuts/recruit-stage/operations/search.js +158 -0
  123. package/dist/shortcuts/recruit-stage/operations/types.js +25 -0
  124. package/dist/shortcuts/recruit-stage/school-tiers.json +318 -0
  125. package/dist/shortcuts/recruit-stage/shared.js +608 -0
  126. package/dist/shortcuts/workflow/operations/form.js +21 -2
  127. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupRules.js +52 -1
  128. package/dist/shortcuts/workflow/operations/todoSmartConvert/detailFieldRules.js +4 -2
  129. package/dist/shortcuts/workflow/operations/todoSmartConvert/summaryCache.js +19 -11
  130. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailCache.js +17 -24
  131. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailFetcher.js +14 -9
  132. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +78 -10
  133. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.core.js +3 -1
  134. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.detail.js +17 -11
  135. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.group.js +16 -40
  136. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +3 -2
  137. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +52 -15
  138. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.pipeline.js +16 -1
  139. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +151 -63
  140. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.subGroup.js +56 -0
  141. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +53 -16
  142. package/docs/e10-auth.md +22 -1
  143. package/docs/operation-manual.md +8 -0
  144. package/package.json +1 -1
  145. package/scripts/install-skill-to-xiaoe.mjs +105 -0
  146. package/scripts/mcp-meeting.mjs +234 -0
  147. package/scripts/package-skill.mjs +4 -2
  148. package/skill-template/business-info.json +24 -0
  149. package/skill-template/domains/recruit-stage.md +29 -0
  150. package/skills/weaver-e10-calendar/SKILL.md +2 -1
  151. package/skills/weaver-e10-calendar/references/calendar-write.md +54 -0
  152. package/skills/weaver-e10-cowork/SKILL.md +1 -1
  153. package/skills/weaver-e10-cowork/references/cowork-write.md +3 -1
  154. package/skills/weaver-e10-jucailin/SKILL.md +1 -1
  155. package/skills/weaver-e10-jucailin/references/attend-write.md +8 -0
  156. package/skills/weaver-e10-jucailin/references/hr-contact.md +8 -0
  157. package/skills/weaver-e10-jucailin/references/hr-report.md +8 -0
  158. package/skills/weaver-e10-jucailin/references/recruit-create.md +2 -2
  159. package/skills/weaver-e10-jucailin/references/recruit-read.md +1 -1
  160. package/skills/weaver-e10-jucailin/references/recruit-write.md +4 -3
  161. package/skills/weaver-e10-mail/SKILL.md +1 -1
  162. package/skills/weaver-e10-mail/references/mail-send.md +2 -0
  163. package/skills/weaver-e10-meeting/SKILL.md +119 -26
  164. package/skills/weaver-e10-meeting/references/meeting-minute-file.md +25 -11
  165. package/skills/weaver-e10-meeting.zip +0 -0
  166. package/skills/weaver-e10-recruit-stage/SKILL.md +226 -0
  167. package/skills/weaver-e10-recruit-stage/agents/openai.yaml +119 -0
  168. package/skills/weaver-e10-recruit-stage/lib/recruit-xiaoe-file.mjs +188 -0
  169. package/skills/weaver-e10-recruit-stage/references/recruit-arrange-interview.md +322 -0
  170. package/skills/weaver-e10-recruit-stage/references/recruit-capability-check.md +127 -0
  171. package/skills/weaver-e10-recruit-stage/references/recruit-check-install.md +65 -0
  172. package/skills/weaver-e10-recruit-stage/references/recruit-detail-process.md +63 -0
  173. package/skills/weaver-e10-recruit-stage/references/recruit-edit-interview-feedback.md +142 -0
  174. package/skills/weaver-e10-recruit-stage/references/recruit-jd-create.md +73 -0
  175. package/skills/weaver-e10-recruit-stage/references/recruit-jd-template.md +56 -0
  176. package/skills/weaver-e10-recruit-stage/references/recruit-list-candidate.md +101 -0
  177. package/skills/weaver-e10-recruit-stage/references/recruit-list-interview.md +148 -0
  178. package/skills/weaver-e10-recruit-stage/references/recruit-match-candidates.md +77 -0
  179. package/skills/weaver-e10-recruit-stage/references/recruit-output-links.md +59 -0
  180. package/skills/weaver-e10-recruit-stage/references/recruit-resolve-creator.md +69 -0
  181. package/skills/weaver-e10-recruit-stage/references/recruit-resolve-metadata.md +241 -0
  182. package/skills/weaver-e10-recruit-stage/references/recruit-resume-import.md +438 -0
  183. package/skills/weaver-e10-recruit-stage/references/recruit-resume-parse.md +128 -0
  184. package/skills/weaver-e10-recruit-stage/references/recruit-search-candidate.md +146 -0
  185. package/skills/weaver-e10-recruit-stage/references/recruit-urge-feedback.md +69 -0
  186. package/skills/weaver-e10-shared/SKILL.md +2 -2
  187. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +4 -2
  188. package/skills/weaver-e10-shijingran/SKILL.md +3 -2
  189. package/skills/weaver-e10-shijingran/references/operations.md +3 -1
  190. package/skills/weaver-e10-shijingran/references/source-manifest.json +41 -40
  191. package/skills/weaver-e10-shijingran/references/write-operations.md +2 -0
  192. package/skills/weaver-e10-workflow/SKILL.md +7 -1
  193. package/skills/weaver-e10-workflow/aigroup/default.json +23 -9
  194. package/skills/weaver-e10-workflow/references/todo-smart-convert.md +37 -21
  195. package/skills/weaver-e10-workflow/tools/todo-summary.cjs +135 -43
  196. package/skills/weaver-e10-yepiaotong/SKILL.md +9 -10
  197. package/skills/weaver-e10-yepiaotong/references/invoice-agent-entry.md +6 -3
@@ -1,5 +1,17 @@
1
1
  {
2
2
  "schemaVersion": 3,
3
+ "generalRules": {
4
+ "description": "通用规则区(2026-11-03):不与任何工作流绑定的全局行为定义;旧版本 CLI 忽略本区块。后续新的通用规则以新键扩展(预留)。",
5
+ "priorityGroup": {
6
+ "description": "第一分组「建议优先处理的流程」的入选规则(叠加视图,入选条目仍保留在原工作流分组)。",
7
+ "title": "建议优先处理的流程",
8
+ "rules": [
9
+ "overdue",
10
+ "levelFallback"
11
+ ],
12
+ "rulesNote": "overdue=已超时(overTime/userOverTime 判定,与紧急程度无关);levelFallback=紧急程度降级链(有 2 取 2,无 2 取 1,全 0 不因等级入选)。预留:expiringToday(今天到期)等后续扩展,未启用。"
13
+ }
14
+ },
3
15
  "defaults": {
4
16
  "weight": 999,
5
17
  "needSummary": true,
@@ -15,10 +27,13 @@
15
27
  "客户问题支持"
16
28
  ],
17
29
  "weight": 100,
18
- "requestSummaryPrompt": "根据以下模板进行摘要:{总结问题描述} 【{完结状态}】【{客户名称}】",
30
+ "requestSummaryPrompt": "根据以下模板进行摘要:{总结问题描述} 【{完结状态}】最晚解决时间:{最晚解决时间}【{客户名称}】",
19
31
  "excludeOriginFormDataFields": [
20
32
  "备案环境",
21
- "推荐答案"
33
+ "推荐答案",
34
+ "小E回复结果",
35
+ "解决方案",
36
+ "问题相关日志(ecology/emobile及resin日志)"
22
37
  ],
23
38
  "aiGroup": {
24
39
  "groupSummary": {
@@ -38,11 +53,7 @@
38
53
  ],
39
54
  "groupTitle": "已过期及今天即将过期的流程"
40
55
  }
41
- },
42
- "summaryRequiredFields": [
43
- "【",
44
- "】"
45
- ]
56
+ }
46
57
  },
47
58
  {
48
59
  "workflowName": [
@@ -63,7 +74,8 @@
63
74
  "意见建议"
64
75
  ],
65
76
  "weight": 300,
66
- "requestSummaryPrompt": "根据流程详情信息(非流程标题信息),言简意赅,禁止过度思考,追求极致速度,不追求摘要质量。",
77
+
78
+ "needSummary": false,
67
79
  "aiGroup": {
68
80
  "groupSummary": {
69
81
  "needAI": true,
@@ -94,7 +106,7 @@
94
106
  "公休"
95
107
  ],
96
108
  "weight": 400,
97
- "requestSummaryPrompt": "根据以下模板进行摘要:{申请人}于{起始时间}开始,请假:{请假天数}天,请假事由:{对:【请假原因】进行总结} ",
109
+ "requestSummaryPrompt": "根据以下模板进行摘要:{起始时间}开始,请假:{请假天数}天,请假事由:{对:【请假原因】进行总结} ",
98
110
  "excludeOriginFormDataFields": [
99
111
  "契约锁人员所有上级",
100
112
  "所有上级",
@@ -192,6 +204,8 @@
192
204
  "安全问题处理工单"
193
205
  ],
194
206
  "weight": 700,
207
+
208
+ "needSummary": true,
195
209
  "requestSummaryPrompt": "根据以下模板进行摘要:漏洞文件:{漏洞地址的文件(完整的文件,不能去掉数据库方言)及行号,行号是指地址最后的L{数字}-L{数字}} 漏洞类型:{漏洞类型} 最晚完成时间:{最晚完成日期}",
196
210
  "excludeOriginFormDataFields": [
197
211
  "修复方案"
@@ -5,13 +5,13 @@
5
5
  **触发分流(2026-10-28 用户定稿,与 `SKILL.md`「待办『汇总』vs『统计』分流」同口径)**:原话含 **「汇总待办」/「批量处理待办」/「摘要」** 等归拢说法 → 走本 operation;原话含 **「统计待办」** → 走 `workflow.todoStat`(不是本 operation);两类词同时出现按本 operation(归拢优先);三类词都没有("有哪些待办 / 列出来 / 多少条")→ `workflow.search`。**不要**为了"先看一眼有哪些"先去调 `todoStat` / `search` 再回头走本链路——那会多花一轮,且交付形态会被带偏。
6
6
 
7
7
  > **⚠️ 2026-10-28 起:分组规则已外置到 JSON,代码不再按业务类型分支。**
8
- > 唯一真源是 `skills/weaver-e10-workflow/aigroup/default.json`(`{defaults, rules[]}`,随 skill 分发、只读),用户意图另存 **CLI 状态目录**下的 `aigroup/user-intents.json`(`aigroup set` 写入,不落 skill 目录)。
8
+ > 唯一真源是 `skills/weaver-e10-workflow/aigroup/default.json`(`{defaults, rules[]}`,随 skill 分发)——**它同时是用户本机可直接编辑的规则文件**:用户调试、增删规则是常态,CLI 与执行方**只管读**,内容是什么就按什么跑;**禁止**与仓库版本比对、禁止覆盖「恢复」、禁止探究改动来源。用户意图另存同目录 `aigroup/user-intents.json`(`aigroup set` 写入;旧位置在 CLI 状态目录,兼容回读)。
9
9
  > 现行规则要点:①没有 `aiGroup` ⇒ 全平铺;②`focus:true` ⇒ 按 `focusKeyword`(`[指标, 算子, 值]`,十种算子)确定性成一组,`groupTitle` 为组名,`sysArg` 指明指标取自列表属性还是流程详情;③`focus:false` ⇒ `aiContext`(流程标题/流程详情)交 AI 分组,**必须**给 `aiGroups`;④`focus:true` 收到多个计划子组时**只保留条数最多的一个**,其余平铺;⑤`aiGroups.seq` 已删除,只认 `requestIds`;⑥组摘要 `groupSummary.needAI:false` 时由 CLI 按 JSON `template` 渲染,`true` 时交 AI;⑦单条摘要由 `needSummary` 决定(false ⇒ 用标题);⑧`focus:false` 还可给 `groupingPrompt`(二次分组口径,见下节);⑨**`summaryRequiredFields`(要素级交付闸门,2026-11-01)**:规则声明的关键字清单,本组每条摘要必须逐项包含,缺任一项 ⇒ 导出闸门 `exit 1`(见 §6.1)。
10
10
 
11
11
  **`groupingPrompt`(2026-10-29 新增,schemaVersion 3)**
12
12
  - 位置 `aiGroup.groupRule.groupingPrompt`,**只在 `focus:false` 时有意义**;`focus:true` 给它会直接校验失败。可选,不给就按素材自由分组。
13
13
  - 分工:`aiContext` 决定**看什么**(是否预取流程详情),`groupingPrompt` 决定**怎么切**(组名词表、归属规则)。
14
- - 下发方式:`collect` 会把该字段原样放进 `groupingBrief[].groupingPrompt`,`smartGroup` 的 `ai_groups_required` 报错文案也会带上。**模型生成 `aiGroups` 前必须先读它并照做**(组名只能取它给的词表,不得新造)。
14
+ - 下发方式:`collect` 会把该字段原样放进 `groupingBrief[].groupingPrompt`,`subGroup` 阶段的 `ai_groups_required` 报错文案也会带上。**模型生成 `aiGroups` 前必须先读它并照做**(组名只能取它给的词表,不得新造)。
15
15
  - 限制:非空字符串、≤500 字(会原样进模型上下文)。
16
16
  > 本文档只描述**机制与执行规则**(aiGroups 格式与校验、子组解散/平铺机制、意图判定、素材与缓存、交付形态);**分组阈值、组名词表、摘要模板、排除清单等业务口径一律以 `aigroup/default.json` 为准**(`prep`/`build` 输出会回显相关内容),与本文冲突时以 JSON 为准。
17
17
 
@@ -38,7 +38,7 @@ weaver-work-cli --json workflow aigroup set --input-json '{"workflowName":["费
38
38
  | --- | --- | --- |
39
39
  | `workflowName` | 工作流名称 | 大组的 `title` 就是它 |
40
40
  | **第一次分组** | 按 `workflowName` 把条目分到各个工作流;得到的每个组叫**工作流分组** | `runGroup` → `wfIdGroup[]` |
41
- | **ai二次分组** | 在工作流分组**内部**再切子组的过程;**只有这一步需要 AI 计划** | `smartGroup` + `aiGroups` |
41
+ | **ai二次分组** | 在工作流分组**内部**再切子组的过程;**只有这一步需要 AI 计划**。**位于单条摘要之后**(2026-11-03 解耦) | `subGroup` + `aiGroups` |
42
42
  | **原始列表数据** | `/getTodoListData` 返回的待办条目集合,**整条链路的唯一数据源**(每条对应一个 `requestId`) | `collect` 的 `items[]` |
43
43
  | **流程标题** | 条目标题 `requestNameTitle`;没有**流程详情数据**时,AI 只能靠它做语义判断,也是摘要的兜底文案 | `requestData.requestNameTitle` |
44
44
  | **流程详情数据** | `/getWorkflowRequest` 返回的表单字段 + 基础信息;用于 a) 分组素材 b) 摘要素材 c) 阈值指标(请假天数/报销金额/到期时间) | `DetailFact { context, metrics }` |
@@ -54,9 +54,10 @@ weaver-work-cli --json workflow aigroup set --input-json '{"workflowName":["费
54
54
  | --- | --- |
55
55
  | `collect` | 调列表接口取数,并生成首次分组和 `groupingBrief` |
56
56
  | `group` | 对已传入的 `items` 重新做首次分组 |
57
- | `smartGroup` | 校验 `aiGroups`,组装最终三键结构 |
58
- | `summaryContext` | 拉详情并生成单条摘要素材 |
59
- | `summary` | 回填 `aiSummaries`,可同时生成组摘要素材 |
57
+ | `smartGroup` | 首次分组 + 详情回填 + **平铺**结构(**不做** ai 二次分组) |
58
+ | `summaryContext` | 生成单条摘要素材(按平铺结构切批) |
59
+ | `summary` | 回填 `aiSummaries`;`withSubGroup:true` 时同进程接着做二次分组 |
60
+ | `subGroup` | **ai 二次分组**:在已回填摘要的成品结构上应用 `aiGroups`,切出汇总项 |
60
61
  | `groupSummaryContext` | 生成汇总项摘要素材 |
61
62
  | `groupSummary` | 回填 `aiGroupSummaries` |
62
63
  | `aggregate` | 兼容旧调用的本地去重、分组和金额合计 |
@@ -65,11 +66,15 @@ weaver-work-cli --json workflow aigroup set --input-json '{"workflowName":["费
65
66
 
66
67
  ```text
67
68
  collect
68
- -> smartGroup(可传 withDetailContext:true)
69
- -> summary(可传 withGroupSummaryContext:true)
69
+ -> smartGroup(可传 withDetailContext:true;只做首次分组,平铺)
70
+ -> summary(回填单条摘要;withSubGroup:true 时接着做二次分组)
70
71
  -> groupSummary(仅存在待回填组摘要时)
71
72
  ```
72
73
 
74
+ 阶段划分(2026-11-03 用户定:**分组与摘要不耦合**):
75
+ `collect` → `smartGroup`(workflowName 分组)→ 单条摘要 → `subGroup`(ai 二次分组)→ 组摘要 → 导出。
76
+ `focus:false` 的组只在 **`subGroup`** 阶段要求 `aiGroups`;摘要阶段不再被它阻塞。
77
+
73
78
  未传 `stage` 时,没有 `items` 进入 `collect`;传入 `items` 进入兼容的本地汇总;回灌带有最终结构的数据时由 CLI 推断后续阶段。显式 `stage` 优先。
74
79
 
75
80
  每次模型调用由调用方把 AI 耗时追加到 `skillTiming`;CLI 会把当前阶段耗时追加到 `meta.timing.steps`。不要为没有执行的 AI 阶段填入耗时。
@@ -79,6 +84,11 @@ collect
79
84
  ### 取数
80
85
 
81
86
  - 默认 `category=todo`,只调用对应分类的列表接口;待办接口为 `getTodoListData`。collect 不调用总数、分组统计或详情接口。
87
+ - **小分类 / tabid(2026-11-02)**:用户明确提到小分类词(待处理 / 待阅 / 超时 / 未读 / 被退回 / 草稿 / 抄送 / 转发 / 传阅 / 关注 / 被督办 / 被转办 / 新反馈 / 已查阅)时,`prep`/`run` 传 `--sub-category <词>` 或 `--tabid <tabid>`(等价入口,**显式 tabid 优先**)。todo 词表:全部=0、待处理=7、未读=1、新反馈=2、已查阅=3、待阅=5、超时=6、被退回=8、转发=4、传阅=11、抄送=9、关注=10、被督办=12、被转办=13、草稿=14。用户未表达小分类时**不要传**(默认 tabid=0 = 全部待办,保住「汇总下待办 = 全量」的历史语义)。小分类与时间范围(`--days`/`--from`/`--to`)可叠加:接口先按 tab 圈定、再按时间过滤。**此参数只影响汇总取数**,`workflow.search` 等链路不受影响。
88
+ - **isremarks(2026-11-02,词表已按 wfc_operate.isremark 补齐)**:待办**参与身份**筛选,多个 code 用英文逗号拼接(如 `0,2,20`),放 `getTodoListData` 的 **`params` 层**;`prep`/`run` 传 `--isremarks`,多值取**并集**。**仅汇总链路生效**——注入点在 todoSmartConvert 的 collect 内部,`workflow.search` 的代码路径与 schema 均未改动。词表:`0`=节点操作者、`1`=节点操作者(代理)、`2`=节点协办人、`5`=超时指定干预人、`6`=超时指定干预人(代理)、`7`=异常处理指定干预人、`8`=异常处理指定干预人(代理)、`10`=意见征询接收人、`11`=意见征询接收人(代理)、`20`=传阅接收人、`21`=传阅接收人(代理)、`30`=抄送需反馈、`31`=抄送需反馈(代理)、`33`=抄送协办人、`40`=转发接收人、`41`=转发接收人(代理)、`50`=协办人、`60`=传阅接收人无需反馈、`61`=传阅接收人无需反馈(代理)、`70`=抄送无需反馈、`71`=抄送无需反馈(代理)、`80`=转发接收人无需反馈、`81`=转发接收人无需反馈(代理)。实测(近一年):`0`→54 条(节点操作者,当前待办的绝对主体),传阅/协办/抄送类身份→0 条;**不传 = 不过滤**(tabid 圈定的全部)。典型用法:「汇总下需要我反馈的抄送」→ `--isremarks 30,31`(抄送需反馈)。
89
+ - **标题摘要模式(2026-11-03 用户定稿)**:内部留言、公司重要通知、安全问题处理工单、产品开发计划制定及任务跟踪(e-todo)、E10项目版本发布申请流程 五类 `needSummary:false`(摘要=流程标题,**不取详情、不进模型**);安全问题/内部留言的 `aiContext` 改为「流程标题」(分组仍按标题分桶,模型 1 轮);e-todo 与安全工单的 `summaryRequiredFields` 已删除(标题模式下要素校验会误拦)。收益:单条摘要 282→0 条进模型、素材 194KB→~10KB、详情取数 -93%,冷缓存全量 35 分钟 → **2 轮模型往返(约 1 分钟)**。备份:`/tmp/aigroup-default-backup-20261103.json`(改前快照,含 generalRules)。
90
+ - **通用规则区 `generalRules`(2026-11-03)**:default.json 顶层可选区块(与具体工作流无关的全局行为;schemaVersion 仍为 3,旧版本 CLI 忽略)。当前仅启用 **`priorityGroup`**——第一分组标题与入选规则(`overdue` 已超时 / `levelFallback` 紧急程度降级链;预留 `expiringToday` 未启用)。`identityFilter`(默认排除抄送类身份)已于 11-03 下午移除:汇总默认**包含**抄送数据,需要排除时由调用方显式传 `--isremarks`(非抄送 18 码白名单见 git 历史/本文件 §2 词表)。后续新的通用规则以新键扩展(预留)。
91
+ - **空结果文案(2026-11-03 用户定稿)**:collect 取到 0 条(或排除条件滤光)时,`prep`/`run` **不进 build**(CLI 会抛 `items_empty`),直接以 `exit 0` 输出用户文案并结束——①无任何显式限制条件 ⇒ 「您当前没有待办。」;②带显式限制条件(时间范围/小分类/tabid/参与身份/排除清单/侧重点)⇒ 「您当前没有符合条件的待办,可以尝试调整限制数据的范围后重试。(当前条件:…)」并回显命中的条件。执行方原样转述即可,**不要自动放宽范围重查**。
82
92
  - 时间范围一次传入,通过 `current` 递增翻页。默认 `limit=200`,最大 `limit=500`;单页最多 100 条,最多 5 页。`pageSize` 是兼容参数,实际大小由 `limit` 推导。
83
93
  - 达到 limit、页内没有新增数据或返回空页时停止;跨页按 `requestId` 去重。超过上限会在 warnings 中说明。
84
94
  - 只使用响应中的 `displayData`,不把技术行当作回退数据。长 ID 始终按字符串处理。
@@ -105,11 +115,10 @@ collect
105
115
  - 详情缓存(条目格式 v3):过滤后的 formData 字段 / basicInfo(已剔除标题类字段)/ 阈值指标 / `excludeTag`,
106
116
  **详情为空也写**;旧版本条目(v2 及更早,basicInfo 里还带标题)读取时视为无效,自动重拉;
107
117
  `excludeOriginFormDataFields` 变了 ⇒ `excludeTag` 对不上 ⇒ 旧条目自动失效重拉(与摘要缓存的 pk 机制同思路);
108
- - 摘要缓存:单条摘要文本。条目除 `maxChars`/`pk`(口径)外还带 **`dv`(详情素材特征值,
109
- 2026-11-01 根治)**:写入时对当前详情素材(basicInfo+fields 内容哈希)取特征值,
110
- 读取时与当前素材比对——素材升级/排除清单调整/详情缓存被清(无法核验)⇒ 旧摘要失效重写;
111
- 无 `dv` 的旧条目按旧规则兼容命中(7 天 TTL 内自然消化)。
118
+ - 摘要缓存:单条摘要文本。条目除 `maxChars`/`pk`(口径)外还带 **`dv`(详情素材特征值,2026-11-01 根治)**:写入时对当前详情素材(basicInfo+fields 内容哈希)取特征值,读取时与当前素材比对——素材升级/排除清单调整/详情缓存被清(无法核验)⇒ 旧摘要失效重写;无 `dv` 的旧条目按旧规则兼容命中。
119
+ **失效条件:由 `requestSummaryPrompt`(default.json / 用户意图)调整触发**——①`pk` **严格比对**(2026-11-04 收紧,取代 11-02 的「条目缺 pk 兼容命中」):调用方传了 pk 时,条目**缺 pk** 或 **pk 不等** ⇒ 一律失效重写。此前「缺 pk 就放过」等于「改 prompt 对老条目永远不生效」——缺 pk 只能说明口径未知,不能证明口径没变。空口径(`pk===''`)照常落库,因此改空 / 改回非空同样能失效,空口径组也不会每次 miss;②`maxChars` 仅在用户显式传 `--summary-chars` 时参与比对,规则/默认字数变化(如 50→150)**不作废**历史摘要;③`dv` 仍比对素材变化,**条目缺 `dv` 依旧兼容命中**(素材侧无法核验,未随本次收紧)。
112
120
  记录里带 `lastFeedBackDateTime` 就是为了这个 key:**流程有新反馈 ⇒ key 变 ⇒ 缓存自然失效**。
121
+ - **缓存缺失/未命中 = 冷缓存(2026-11-03 用户定稿)**:`cache/` 下文件不存在、被清理或条目 miss,一律按「从未生成过」处理,直接走待写摘要流程;**禁止**追溯缓存丢失或被删的原因、**禁止**从备份恢复缓存、也不作为异常上报用户。
113
122
  - 详情取数时机:`collect` 只为「用户给了 `groupFocus` 且意图适用于本组」的组预取字段;`smartGroup` 对全部条目走一次「缓存优先」的取数(命中零请求)。因此**冷缓存下详情请求集中在 `smartGroup`,`summary` 阶段通常 0 请求**。
114
123
  - 摘要缓存**只省 AI、不省详情**:阈值指标只认流程详情数据,所以命中摘要的条目仍会查一次详情缓存;摘要为空的条目用标题兜底。
115
124
 
@@ -117,27 +126,31 @@ collect
117
126
 
118
127
  - `groupBy` 支持 `workflow`、`urgency`、`date`、`none`,默认 `workflow`。
119
128
  - 优先组固定为 `id=priority`、标题“建议优先处理的流程”,排在第一位。超时条目必入;没有级别 2 时才使用级别 1,级别 0 不因等级入选。
129
+ - **生成时机(2026-11-03 用户定)**:优先组**不再在分组阶段产出**,而是在**最终结构**上由 `assemblePriorityGroup()` 生成——摘要与组摘要全部回填后,按 `generalRules.priorityGroup` 从各工作流分组的 `displayItems` 里筛出条目、复制一份置顶。因此 `prep`/`build` 的组清单里**没有**这一组(组数比其他阶段少 1),只有最终导出结构才有;判据字段(`overTime`/`userOverTime`/`requestLevel`)都在 `requestData` 上,无需回查列表。该函数**幂等**(输入已有 priority 会先剔除再重建)。
120
130
  - 优先组是**叠加视图**,不是筛选:入选条目**仍保留在各自的工作流分组里**(优先组只是置顶入口)。因此 `Σcount` 会大于 `totalCount`,**`totalCount` 一律按去重条数计(= collect 取到的全部条数),不做分组求和**。
121
131
  - 普通组保持接口返回的首次出现顺序。只有调用方明确给出排序意图时才传 `groupOrder`;`topN` 只影响中间 Markdown,不裁剪最终数组。
122
- - `groupingBrief` 用组内 1-based `idx` 标识条目。AI 生成 `aiGroups` 时优先用 `seq`,不要手抄长 `requestId`。
132
+ - `groupingBrief` 用组内 1-based `idx` 标识条目;`idx` 与 `prep` 输出目录 `state.json` 里 `planGroups[].rows[].requestId` 一一对应。生成 `aiGroups` 时一律用 `requestId`:需要映射就从 `state.json` 一次性提取,不要手抄,更不要用 `seq`(已废弃)。
123
133
 
124
134
  ## 4. `smartGroup` 和 `aiGroups`
125
135
 
126
- `aiGroups` 是扁平数组,每项只允许 `groupId`、`title`、`requestIds` 或 `seq`:
136
+ `aiGroups` 是扁平数组,每项允许 `groupId`、`title`、`requestIds`、`summary` 四个键(**`seq` 已废弃**;`summary` 是 2026-11-03 新增的可选键):
127
137
 
128
138
  ```json
129
139
  [
130
- { "groupId": "workflow-id", "title": "安全通知", "seq": [1, 3] },
131
- { "groupId": "workflow-id", "title": "制度通知", "seq": [2, 4] }
140
+ { "groupId": "workflow-id", "title": "安全通知", "requestIds": ["101", "103"], "summary": "本组为安全补丁升级通告,含 2 条补丁安装提醒,建议优先处理。" },
141
+ { "groupId": "workflow-id", "title": "制度通知", "requestIds": ["102", "104"] }
132
142
  ]
133
143
  ```
134
144
 
135
- - `requestIds` 与 `seq` 二选一,不能同时出现,也不能都为空。
145
+ - **`summary`(组摘要)与分组在同一次交互产出**:给了就原样落到 `displayItems[].summary`,`groupSummaryBatches` 不再为它产批次 ⇒ **不必再跑一轮组摘要**。规则 `aiGroup.groupSummary.needAI === true` 的组(如内部留言、安全问题处理工单)**建议一定带**;`needAI:false` 的确定性组(请假/报销/问题支持)由 CLI 模板渲染,带了也以你给的为准。
146
+ - 写 `summary` 时手上有本组的标题 + 每条 `summary`(单条摘要已在前一步写进结构),直接概括即可;字数按 `groupSummaryMaxChars`(默认 100)。
147
+
148
+ - `requestIds` 必填且非空,成员必须属于本组;未知 `groupId`、未知 `requestId`、重复分配直接失败。
136
149
  - 优先组不可二次分组。
137
150
  - 组内少于 3 条时**一律平铺单条**,不产生任何汇总项——阈值组(1天以内 / 100元以内)、问题支持过期组、内部留言整组都不例外;**即使给了 `aiGroups` 也忽略**(CLI 写 warning)。这条是硬门槛,没有例外。
138
151
  - **计划子组不足 2 条会被解散平铺**(CLI 写 warning「N 个子组不足 2 条,已解散」)——这是机制行为,与上一条相互独立:若 `groupingPrompt` 的自然粒度每组只有 1 条(如 安全问题处理工单 按 git 文件,11 条对应 11 个文件),按它给的计划会全部解散、整组平铺,**这是该计划的必然结果,不是错误**;要真正成组,就得按能凑够每组 ≥2 条的粒度给计划(如按文件族),且组名仍必须来自 `groupingPrompt` 口径、**不得为凑组虚构更粗粒度的新组名**(如"XML/Java 文件类型")。
139
- - 组内至少 3 条时,完全省略 `aiGroups` 走 CLI 确定性分组;显式传入后,未知 `groupId`、未知 `requestId`、重复分配和越界序号直接失败(**仅在本组计划会被采纳时校验**:意图适用于本类型,或本组是内部留言)。
140
- - `aiGroups` 何时该给——**三种情形**(`prep` 输出会给组标 `[必须给 plan]`,照标注执行即可):
152
+ - 组内至少 3 条时,完全省略 `aiGroups` 走 CLI 确定性分组;显式传入后,未知 `groupId`、未知 `requestId`、重复分配直接失败(**仅在本组计划会被采纳时校验**:意图适用于本类型,或本组是内部留言)。
153
+ - `aiGroups` 何时该给——**三种情形**(`prep` 输出会给组标 `[需二次分组]`,照标注执行即可):
141
154
  - ① 用户给了侧重点(`groupFocus`),且侧重点**按意图表适用于本组类型**;
142
155
  - ② 本组是**内部留言** —— 而且**≥3 条时是必须给**:没有侧重点(或侧重点不适用)就按**流程标题**分组,缺计划直接报 `ai_groups_required`;
143
156
  - ③ 规则里 `aiGroup.groupRule.focus === false` 的组(如 **安全问题处理工单**,上下文=流程详情)——**≥3 条时必须给**,缺计划同样报 `ai_groups_required`。这类组的分组素材(如漏洞地址里的 git 文件)来自流程详情,prep 无侧重点时不带字段注记 ⇒ 按 references 上文的「单条查询详情」兜底取得,**不要用标题推断**。
@@ -278,7 +291,8 @@ collect
278
291
  - `params_pc` / `params_mobile` 必须放在 `sdkOptions` 内、与 `routerProps_pc/mobile` 同级,**不要放进 `arguments` 层**。
279
292
  - `app` 的值按原始字符写 **`@weapp/workflow`**(`@` 后面**没有**斜杠,共 15 个字符):**不要写成 `@//weapp/workflow`、不要转义成 `@\/\/weapp\/workflow`、也不要加前导空格**——带前导空格会导致 app 解析失败(用户 2026-10-27 以截图校正)。
280
293
  - 脚本 `exportFinal` 已在 stdout 的 `[JSON_BEGIN]`/`[JSON_END]` 之间直接输出**带标签的整块载荷**,原样粘贴即可;交付 JSON 文件(`~/Downloads/<用户名>_<时间戳>.json`)由脚本自动落盘并打印路径,**默认不 `present_files`(见 §6.1 顶部)**。旧 `--file` 分支(写 `待办汇总-*.json` 外框归档)已于 2026-11-01 **删除**——传了也会被忽略,不要再依赖它。
281
- - **交付硬闸门(2026-11-01)**:`finish`/`build` 导出前强制校验——①空单条摘要 >0、②空组摘要 >0、③待写摘要未全部回填(列出缺失 requestId)、④详情取数失败(`failed>0`,防"静默降级+标题编内容")、⑤**降级话术黑名单**(摘要含「未在标题体现 / 标题未体现 / 标题里没有 / 详见流程 / 详见详情 / 待确认」即拦截——把取数失败写成业务信息缺失)、⑥**要素级校验**(规则 `summaryRequiredFields` 声明的关键字,本组每条摘要必须逐项包含,缺任一即拦截并列出缺失项)——任一命中即 `exit 1` 拒绝导出。显式豁免:`--allow-empty-summary`(豁免①②③⑤⑥)、`--allow-detail-fail`(豁免④)。**豁免是给"确认要带瑕疵出文件"用的,不要拿它当常规手段绕过校验。**
294
+ - **交付硬闸门(2026-11-01)**:`finish`/`build` 导出前强制校验——①空单条摘要 >0、②空组摘要 >0、③待写摘要未全部回填(列出缺失 requestId)、④详情取数失败(`failed>0`,防"静默降级+标题编内容")、⑤**降级话术黑名单**(摘要含「未在标题体现 / 标题未体现 / 标题里没有 / 详见流程 / 详见详情」即拦截——把取数失败写成业务信息缺失;2026-11-03 按用户要求去掉「待确认」:摘要里出现"待确认"属于正常业务表述)、⑥**要素级校验**(规则 `summaryRequiredFields` 声明的关键字,本组每条摘要必须逐项包含,缺任一即拦截并列出缺失项)——任一命中即 `exit 1` 拒绝导出。显式豁免:`--allow-empty-summary`(豁免①②③⑤⑥)、`--allow-detail-fail`(豁免④)。**豁免是给"确认要带瑕疵出文件"用的,不要拿它当常规手段绕过校验。**
295
+ - **组摘要未回填 ≠ 失败(2026-11-02 P2 定稿)**:`finish`/`build` 遇到组摘要待回填时,把**完整素材**(含每个成员的 `requestId`、标题、发起人、单条摘要、`prompt`)写 `<out>/need-group-summaries.json`,以 **`exit 0` + `NEEDS_GROUP_SUMMARIES`** 返回、**不导出**(控制台只给紧凑清单,此前每批 400 字符的截断预览已废除);在该文件每个元素里补 `summary` 后用 `--group-summaries` **指回同一个文件**重跑即可(脚本自动只取 `groupId`/`summary`,`items` 等多余字段不外传)。`--allow-empty-summary` 豁免后才按空组摘要继续导出。上方①②④⑤⑥等真失败仍 `exit 1`。
282
296
  - 体积见 §6.2:**不能按组拆成多个 `<browsertool>` 块**(拆开就不是合法载荷);超限时先在正文说明条数与预计体积,建议缩小范围,用户坚持再照做。
283
297
 
284
298
 
@@ -308,6 +322,8 @@ collect
308
322
 
309
323
  ⇒ 端到端提速最有效的不是优化 CLI,而是**减少往返次数**:
310
324
 
325
+ - **第一原则(2026-11-02 用户定稿,先看这条)**:**不截断脚本输出、不探查中间产物**。`todo-summary.cjs` 打的是纯文本素材,`| head -c` 不会报错、只会让「待写摘要」悄悄缺条(实测 17.6 KB / 9 条被截成 5 条,缓存随后已写、重跑也补不回,只能整套重跑);`<out>/` 下的 `items.json` / `state.json` / `smartgroup.json` / `*.log` 更不能为"确认一下"去翻。遵守这两条,同一次汇总可从 12 轮压到 1 轮(脚本口径 71.9s → 3.0s);违反其中任一条,必然多 2~3 轮、约 25~60s。细则见 SKILL.md「5) 命令输出读取纪律」。
326
+ - **素材落盘(2026-11-02 P3-B)**:`build`/`run` 停在待写摘要时,完整素材(batches 数组,含 `items[].requestId/context` 等,**无损**)写 `<out>/need-summaries.json`,控制台只留批次索引(`NEEDS_SUMMARIES`);模型用 Read 按需读该文件写 `aiSummaries.json` 后 `finish --summaries` 回填,**不再解析控制台长日志**(冷缓存 268 条时旧输出达 850KB)。
311
327
  - 用 `run --days N --limit N --out DIR --timing-report` 一次跑完 `prep + build`(无待写摘要时连 `finish` 一起),别一步步手动跑。
312
328
  - **时间段默认(2026-11-01 用户定稿)**:用户**没有说明数据的时间段**时,**不加任何时间条件**——不传 `--days` / `--from` / `--to`,取数范围就是全部待办,默认取**前 500 条**(`--limit` 不传即为 500);只有用户明确说了时段("近一年""近 30 天""某区间")才传 `--days` 或 `--from/--to`。不要替用户默认加时间范围。
313
329
  - **有侧重点(`--focus`)时的最短路径是 4 轮**,多一轮就多 5~7s:
@@ -356,7 +372,7 @@ weaver-work-cli --json workflow run workflow.todoSmartConvert --input-json '{"st
356
372
  | --- | --- |
357
373
  | `stage_invalid` / `input_unknown_key` | 按错误中的允许字段修正 |
358
374
  | `items_required` / `items_invalid` / `items_too_many` | 先执行 `collect`,再回灌原始 `items` |
359
- | `ai_groups_invalid` / `ai_group_*` | 检查 `groupId`、`title`、`seq`/`requestIds` 和覆盖范围 |
375
+ | `ai_groups_invalid` / `ai_group_*` | 检查 `groupId`、`title`、`requestIds`(`seq` 已废弃,改用 `requestIds`)和覆盖范围 |
360
376
  | `final_groups_required` / `wfidgroup_invalid` | 原样回灌上一步 `wfIdGroup` |
361
377
  | `ai_summaries_required` / `ai_summary_*` | 只提交本轮有效 `requestId`,不能重复 |
362
378
  | `ai_group_summaries_required` / `ai_group_summary_*` | 使用汇总项 id,不使用父组 id |
@@ -512,6 +512,23 @@ function loadAigroupRuleFile() {
512
512
  return aigroupRuleFileCache;
513
513
  }
514
514
 
515
+ // ── 规则指纹(2026-11-04 新增:交付纪律强化)────────────────────────────
516
+ // 对影响取数/分组/摘要口径的规则文件算指纹:aigroup/default.json + aigroup/user-intents.json(若存在)。
517
+ // prep 时写入 state,finish 导出前比对——规则在取数之后被改过 ⇒ 数据是旧口径产出,拒绝导出,
518
+ // 防「新规则 + 旧数据」的静默交付(2026-11-04 实测:快照 09:49 导出、规则 09:50 修改)。
519
+ function ruleFingerprint() {
520
+ const { createHash } = require('node:crypto');
521
+ const aigroupDir = path.join(path.resolve(__dirname, '..'), 'aigroup');
522
+ const parts = [];
523
+ for (const name of ['default.json', 'user-intents.json']) {
524
+ const file = path.join(aigroupDir, name);
525
+ if (!fs.existsSync(file)) continue;
526
+ const buf = fs.readFileSync(file);
527
+ parts.push(`${name}#${createHash('sha256').update(buf).digest('hex').slice(0, 16)}(${buf.length}B)`);
528
+ }
529
+ return parts.join(' | ');
530
+ }
531
+
515
532
  /** 组名 → 该规则的 summaryRequiredFields(needSummary:false 的组不校验)。 */
516
533
  function requiredFieldsFor(title) {
517
534
  const rules = loadAigroupRuleFile().rules || [];
@@ -536,6 +553,20 @@ function exportFinal(result, dir, state, kept, opts = {}) {
536
553
  const stats = groupStats(data.wfIdGroup);
537
554
  const total = groupTotal;
538
555
  // ── 交付硬闸门(2026-11-01 用户拍板):缺环节 / 静默降级出不了交付物,只有显式豁免才放行 ──
556
+ // ⓪ 规则新鲜度(2026-11-04 新增):取数之后规则文件被改过 ⇒ 本次数据是旧口径产出,
557
+ // 拒绝导出。旧 state(无 ruleFp)仅提示不拦截,保持向后兼容。
558
+ if (state.ruleFp) {
559
+ const nowFp = ruleFingerprint();
560
+ if (nowFp !== state.ruleFp && opts.allowStaleRules !== true) {
561
+ console.error('\n✖ 交付拦截:规则文件在取数之后被修改,本次数据是旧口径产出。');
562
+ console.error(' 取数时指纹:%s', state.ruleFp);
563
+ console.error(' 当前指纹 :%s', nowFp);
564
+ console.error(' → 请重新执行 run 取数后再 finish;确要带旧口径出文件,加 --allow-stale-rules 显式豁免。');
565
+ process.exit(1);
566
+ }
567
+ } else {
568
+ console.error('⚠ 本次 state 无规则指纹(旧版本产出),跳过规则新鲜度校验。');
569
+ }
539
570
  const allowEmpty = opts.allowEmptySummary === true;
540
571
  const detailFailed = Number(opts.detailFailed) || 0;
541
572
  const groupEmpty = Number(opts.groupEmpty) || 0;
@@ -563,7 +594,7 @@ function exportFinal(result, dir, state, kept, opts = {}) {
563
594
  }
564
595
  // ⑤ 降级话术黑名单(2026-11-01):摘要正文出现"未在标题体现/详见流程"这类表述,
565
596
  // 说明执行方把「自己没取到」写成了「业务对象没有」——按模板回查详情补齐后重写。
566
- const DEGRADE_PHRASES = ['未在标题体现', '标题未体现', '标题里没有', '详见流程', '详见详情', '待确认'];
597
+ const DEGRADE_PHRASES = ['未在标题体现', '标题未体现', '标题里没有', '详见流程', '详见详情'];
567
598
  const degraded = [];
568
599
  for (const group of data.wfIdGroup || []) {
569
600
  for (const item of group.displayItems || []) {
@@ -746,6 +777,17 @@ function cmdPrep(args) {
746
777
  if (args['sub-category'] && args['sub-category'] !== true) {
747
778
  input.subCategory = String(args['sub-category']);
748
779
  }
780
+ // 2026-11-02:小分类的等价直传入口。显式 tabid 优先级高于 subCategory/tabName
781
+ // (CLI buildSearchQuery:先解析 subCategory/tabName,再用显式 tabid 覆盖)。
782
+ // 仅影响本次汇总取数范围,不触碰 workflow.search 的默认行为。
783
+ if (args.tabid && args.tabid !== true) {
784
+ input.tabid = String(args.tabid);
785
+ }
786
+ // 2026-11-02:备注筛选(getTodoListData params.isremarks),多个数字英文逗号拼接。
787
+ // 仅汇总链路生效(CLI collect 内部注入 params),workflow.search 不受影响。
788
+ if (args.isremarks && args.isremarks !== true) {
789
+ input.isremarks = String(args.isremarks);
790
+ }
749
791
  // 摘要字数口径要带进 collect:否则缓存命中按默认 50 字判定,
750
792
  // 用户要求「10 字以内」时仍会回填旧的长摘要(2026-10-22 实测踩到)。
751
793
  if (args['summary-chars'] && args['summary-chars'] !== true) {
@@ -775,6 +817,29 @@ function cmdPrep(args) {
775
817
  const kept = exclude.length ? all.filter((row) => !exclude.includes(String(row.workflowName || ''))) : all;
776
818
  const dropped = all.length - kept.length;
777
819
 
820
+ // 2026-11-03 用户定稿:collect 空结果按「是否带显式限制条件」给不同文案,直接结束(exit 0),
821
+ // 不进 build(CLI 会抛 items_empty 校验错误)。
822
+ // - 无限制条件(如「汇总我的待办」)⇒ 「您当前没有待办。」
823
+ // - 带限制条件(时间范围 / 小分类 / tabid / 参与身份 / 排除清单 / 侧重点)⇒
824
+ // 「您当前没有符合条件的待办,可以尝试调整限制数据的范围后重试。」并回显条件。
825
+ if (all.length === 0 || kept.length === 0) {
826
+ const constraints = [];
827
+ if (days > 0) constraints.push(`近 ${days} 天`);
828
+ if (args.from && args.to) constraints.push(`${args.from} 至 ${args.to}`);
829
+ if (input.subCategory) constraints.push(`小分类「${input.subCategory}」`);
830
+ if (input.tabid) constraints.push(`tabid=${input.tabid}`);
831
+ if (input.isremarks) constraints.push(`参与身份 ${input.isremarks}`);
832
+ if (exclude.length) constraints.push(`排除流程「${exclude.join('、')}」`);
833
+ if (input.groupFocus) constraints.push(`侧重点「${input.groupFocus}」`);
834
+ if (all.length === 0 && constraints.length === 0) {
835
+ console.log('您当前没有待办。');
836
+ } else {
837
+ console.log('您当前没有符合条件的待办,可以尝试调整限制数据的范围后重试。%s',
838
+ constraints.length ? `(当前条件:${constraints.join('、')})` : '');
839
+ }
840
+ return 'EMPTY';
841
+ }
842
+
778
843
  // 组结构直接用 CLI 返回的 wfIdGroup(已做 priority 抽取,不能再自己按 workflowName 复算,
779
844
  // 否则会把被抽进「建议优先处理」的条目重复计入原流程)。
780
845
  // 注意:wfIdGroup[].requestDatas 里**没有 workflowName**,要按 requestId 从 items 回查,
@@ -827,6 +892,7 @@ function cmdPrep(args) {
827
892
  groupFocus: args.focus && args.focus !== true ? String(args.focus) : null,
828
893
  summaryMaxChars: args["summary-chars"] ? Number(args["summary-chars"]) : null,
829
894
  groupSummaryMaxChars: args["group-summary-chars"] ? Number(args["group-summary-chars"]) : null,
895
+ ruleFp: ruleFingerprint(),
830
896
  fetched: all.length, kept: kept.length, dropped,
831
897
  planGroups,
832
898
  }));
@@ -867,7 +933,7 @@ function cmdPrep(args) {
867
933
  const kind = kindOfTitle(group.title);
868
934
  const skip = group.groupId === 'priority' || group.count <= 1 || (kind !== '' && !focusKinds.has(kind));
869
935
  const rule = skip ? null : matchRule(group.title);
870
- const marker = skip ? ' [无需分组]' : (rule || group.title.includes('内部留言') ? ' [必须给 plan]' : '');
936
+ const marker = skip ? ' [无需分组]' : (rule || group.title.includes('内部留言') ? ' [需二次分组]' : '');
871
937
  console.log(`=== ${group.groupId} | ${group.title} | ${group.count}${marker}`);
872
938
  if (skip) continue;
873
939
  // 回显规则内容:组名/分法只按 groupingPrompt 来(词表不得自造),摘要按 requestSummaryPrompt 写
@@ -882,7 +948,8 @@ function cmdPrep(args) {
882
948
  }
883
949
  }
884
950
  console.log('\n下一步:node %s build --dir %s', path.basename(__filename), dir);
885
- console.log('(标了 [必须给 plan] 的组——内部留言、以及规则 focus:false 的组如 安全问题处理工单——build 时必须带 --plan <aiGroups.json>;组名/分法/摘要口径见各组下回显的规则内容;其余走确定性分组)');
951
+ console.log('(标了 [需二次分组] 的组——内部留言、以及规则 focus:false 的组如 安全问题处理工单——在**单条摘要之后**的二次分组阶段才需要 aiGroups;build 阶段不需要,先把摘要写完)');
952
+ console.log('(写 aiGroups 时给 needAI:true 的组带上 summary 字段,分组+组摘要在同一轮拿到,不用再跑一次组摘要)');
886
953
  }
887
954
 
888
955
  function cmdBuild(args) {
@@ -892,7 +959,8 @@ function cmdBuild(args) {
892
959
  const kept = readJson(path.join(dir, 'items.json'));
893
960
  const planGroups = state.planGroups || [];
894
961
  const countById = new Map(planGroups.map((g) => [g.groupId, g.count]));
895
- // `--plan`:确定性类型可不传;prep 标了 [必须给 plan] 的组(内部留言 / focus:false 组)不传会被 CLI 拒(ai_groups_required)。
962
+ // `--plan`:2026-11-03 起 build 阶段**不再消费** aiGroups(二次分组后移到 subGroup);
963
+ // 这里只做校验与落盘,finish 阶段自动读取目录里的 aiGroups.json。
896
964
  // 不传时直接走 CLI 确定性分组(等价于 aiGroups 未传)。
897
965
  let aiGroups;
898
966
  if (planFile) {
@@ -903,7 +971,7 @@ function cmdBuild(args) {
903
971
  const byGroup = new Map();
904
972
  for (const entry of aiGroups) {
905
973
  if (!entry || !entry.groupId || !Array.isArray(entry.requestIds)) {
906
- console.error('✖ aiGroups 元素必须是 {groupId,title,requestIds:[...]}(seq 已废弃)'); process.exit(1);
974
+ console.error('✖ aiGroups 元素必须是 {groupId,title,requestIds:[...],summary?}(seq 已废弃)'); process.exit(1);
907
975
  }
908
976
  if (!countById.has(String(entry.groupId))) {
909
977
  console.error(`✖ 未知 groupId ${entry.groupId}(合法值见 state.json 的 planGroups)`); process.exit(1);
@@ -931,8 +999,8 @@ function cmdBuild(args) {
931
999
  const t = Date.now();
932
1000
  const sg = runStage({
933
1001
  stage: 'smartGroup',
1002
+ // 2026-11-03:本阶段只做 workflowName 首次分组 + 平铺结构(二次分组在 finish 的 subGroup)。
934
1003
  items: kept,
935
- ...(aiGroups !== undefined ? { aiGroups } : {}),
936
1004
  withDetailContext: true,
937
1005
  ...(state.groupFocus ? { groupFocus: state.groupFocus } : {}),
938
1006
  ...(state.summaryMaxChars ? { summaryMaxChars: state.summaryMaxChars } : {}),
@@ -959,34 +1027,19 @@ function cmdBuild(args) {
959
1027
  cmdFinish({ ...args, dir });
960
1028
  return;
961
1029
  }
962
- console.log(`\n待写摘要(把结果写成 aiSummaries.json,元素 {requestId,summary};每批的「口径」必须遵守):`);
1030
+ // 2026-11-02 P3-B:素材无损落盘(此前整段吐控制台,268 条时 build 输出达 850KB,
1031
+ // 模型要多轮读长文本还易被截断);控制台只留批次索引,模型用 Read 按需读 need-summaries.json。
1032
+ const needFile = path.join(dir, 'need-summaries.json');
1033
+ fs.writeFileSync(needFile, JSON.stringify(batches, null, 2));
1034
+ console.log(`\n⚠ NEEDS_SUMMARIES:%d 条单条摘要待写(完整素材已写入 %s):`, pending.length, needFile);
963
1035
  for (const b of batches) {
964
1036
  const items = b.items || [];
965
1037
  if (items.length === 0) continue;
966
- console.log(`## 批次 ${b.batchId}|${b.groupTitle}${b.subGroupTitle && b.subGroupTitle !== b.groupTitle ? ` → ${b.subGroupTitle}` : ''}|目标字数 ${b.suggestedMaxChars}`);
967
- if (b.prompt) console.log(`口径:${b.prompt}`);
968
- if (b.hint) console.log(`提示:${b.hint}`);
969
- for (const item of items) {
970
- console.log(`### ${item.requestId}`);
971
- console.log(`T: ${String(item.requestNameTitle || '').slice(0, 160)}`);
972
- // 素材**几乎不截断**(4000 字符保险丝):2026-11-01 实测 240 字截断把 e-todo 模板的
973
- // 「预估开发总人天/需求概述」等尾部要素裁掉,模型只能靠标题凑数(要素缺失复盘定稿)。
974
- console.log(`C: ${String(item.context || '').replace(/\s+/g, ' ').slice(0, 4000)}`);
975
- }
976
- }
977
- console.log('\n下一步:node %s finish --dir %s --summaries <aiSummaries.json>', path.basename(__filename), dir);
978
- // 组摘要(aiGroup.groupSummary.needAI=true 的汇总项):口径随批次下发,模型按它写 aiGroupSummaries
979
- const gsBatches = sg.data.groupSummaryBatches || [];
980
- if (gsBatches.length > 0) {
981
- console.log('\n待写组摘要(写成 aiGroupSummaries.json,元素 {groupId,summary}):');
982
- for (const b of gsBatches) {
983
- console.log(`### 组摘要 ${b.groupId}|${b.parentTitle} → ${b.title}|${b.count} 条|目标字数 ${b.suggestedMaxChars}`);
984
- if (b.prompt) console.log(`口径:${b.prompt}`);
985
- if (b.hint) console.log(`提示:${b.hint}`);
986
- if (b.stats && b.stats.tail) console.log(`tail:${b.stats.tail}`);
987
- }
988
- console.log('\nfinish 时加:--group-summaries <aiGroupSummaries.json>');
1038
+ console.log(` - ${b.batchId}|${b.groupTitle}${b.subGroupTitle && b.subGroupTitle !== b.groupTitle ? ` → ${b.subGroupTitle}` : ''}|${items.length} 条|目标字数 ${b.suggestedMaxChars}${b.prompt ? `|口径:${b.prompt}` : ''}`);
1039
+ if (b.hint) console.log(` 提示:${b.hint}`);
989
1040
  }
1041
+ console.log(' → 读 need-summaries.json(items[].requestId + context),摘要写成 aiSummaries.json(元素 {requestId,summary})后重跑:');
1042
+ console.log(' node %s finish --dir %s --summaries <aiSummaries.json> --timing-report', path.basename(__filename), dir);
990
1043
  }
991
1044
 
992
1045
  function cmdFinish(args) {
@@ -995,7 +1048,14 @@ function cmdFinish(args) {
995
1048
  const kept = readJson(path.join(dir, 'items.json'));
996
1049
  // `aiGroups.json` 可能不存在(build 没传 --plan 时就是确定性分组),此时按「未传 aiGroups」处理。
997
1050
  const planPath = path.join(dir, 'aiGroups.json');
1051
+ // `--plan` 给了就以它为准(落到目录里,后续 stage=subGroup 直接读)
1052
+ if (args.plan && args.plan !== true) {
1053
+ const src = String(args.plan);
1054
+ if (src !== planPath && fs.existsSync(src)) fs.copyFileSync(src, planPath);
1055
+ }
998
1056
  const aiGroups = fs.existsSync(planPath) ? readJson(planPath) : undefined;
1057
+ /** 有没有二次分组计划:没有就只回填单条摘要,不做 subGroup(也就不产组摘要、不导出)。 */
1058
+ const hasPlan = aiGroups !== undefined;
999
1059
  const sg = readJson(path.join(dir, 'smartgroup.json'));
1000
1060
 
1001
1061
  // 即使没传 --summaries 也要跑一次 summary 阶段:这样才能让**摘要缓存**回填
@@ -1009,7 +1069,9 @@ function cmdFinish(args) {
1009
1069
  withDetailContext: true,
1010
1070
  wfIdGroup: sg.data.wfIdGroup,
1011
1071
  aiSummaries,
1012
- withGroupSummaryContext: true,
1072
+ // 单条摘要回填完 → 同进程做 ai 二次分组(stage=subGroup)→ 再产组摘要批次;
1073
+ // 没有 aiGroups 时只回填单条摘要,二次分组留到下一步(两条链路完全解耦)。
1074
+ ...(hasPlan ? { withSubGroup: true, withGroupSummaryContext: true } : {}),
1013
1075
  ...(state.summaryMaxChars ? { summaryMaxChars: state.summaryMaxChars } : {}),
1014
1076
  ...(state.groupSummaryMaxChars ? { groupSummaryMaxChars: state.groupSummaryMaxChars } : {}),
1015
1077
  skillTiming: { startedAt: state.startedAt, steps: [{ name: 'collect', kind: 'cli', durationMs: state.collectMs }] },
@@ -1022,6 +1084,15 @@ function cmdFinish(args) {
1022
1084
  fs.writeFileSync(path.join(dir, 'state.json'), JSON.stringify(state));
1023
1085
  console.log('summary 内部 %s / 含进程启动 %s(回填 %d 条,其余走缓存预填)',
1024
1086
  summaryInternal === null ? '-' : fmt(summaryInternal), fmt(summaryWallMs), aiSummaries.length);
1087
+ if (!hasPlan) {
1088
+ // 2026-11-03:ai 二次分组与单条摘要解耦——没有计划就停在这里,不产组摘要、不导出。
1089
+ const filled = (result.meta && result.meta.summarized) || 0;
1090
+ const emptyN = (result.meta && result.meta.empty) || 0;
1091
+ console.log('单条摘要已回填 %d 条(空 %d)。ai 二次分组是下一步:', filled, emptyN);
1092
+ console.log(' 把 aiGroups.json 放到 %s(或 --plan <path>),再跑:', path.join(dir, 'aiGroups.json'));
1093
+ console.log(' node %s finish --dir %s --plan <aiGroups.json> --timing-report', path.basename(__filename), dir);
1094
+ return;
1095
+ }
1025
1096
  // 未回填的缺失 id 不在这里单列:summary 阶段的信封不带 summaryBatches/summaryStats,
1026
1097
  // 空 id 列表统一由 exportFinal 的闸门①从最终结构里就地提取(能直接给出 requestId)。
1027
1098
  // 未知 requestId 的 aiSummaries 由 CLI 侧硬校验拒绝(ai_summary_unknown_request,非零退出)。
@@ -1033,7 +1104,10 @@ function cmdFinish(args) {
1033
1104
  : null;
1034
1105
  if (groupSummariesFile) {
1035
1106
  // 组摘要必须由模型写、且每轮重算;这里只是把「回填」并进同一次命令,省掉一次模型往返。
1036
- const aiGroupSummaries = readJson(groupSummariesFile);
1107
+ // need-group-summaries.json 可直接指回:这里投影成 CLI 需要的 {groupId,summary},
1108
+ // 多余字段(title/items/stats 等)不外传,避免 CLI 白名单校验拒绝。
1109
+ const aiGroupSummaries = readJson(groupSummariesFile)
1110
+ .map((x) => ({ groupId: x.groupId || x.id, summary: x.summary }));
1037
1111
  const t2 = Date.now();
1038
1112
  const gs = runStage({
1039
1113
  smartSummaryTitle: result.data.smartSummaryTitle,
@@ -1053,12 +1127,21 @@ function cmdFinish(args) {
1053
1127
  for (const text of gsWarnings) console.log(' ⚠ %s', text);
1054
1128
  finalResult = gs;
1055
1129
  } else {
1056
- console.log('\n⚠ 还有 %d 个组摘要素材待模型生成(groupSummary 阶段):', groupBatches.length);
1057
- for (const batch of groupBatches) console.log(JSON.stringify(batch).slice(0, 400));
1058
- console.log(' → 写成 [{groupId,summary}] 后加 `--group-summaries <file>` 重跑本命令,可在同一次调用里回填');
1059
- if (args['allow-empty-summary'] !== true) {
1060
- console.error('✖ 交付拦截:%d 个组摘要未回填,导出会是空组摘要。补齐 --group-summaries 后重跑,或加 --allow-empty-summary 显式豁免。', groupBatches.length);
1061
- process.exit(1);
1130
+ // 2026-11-02 P2 定稿:组摘要未回填 ≠ 校验失败。完整素材落盘(此前控制台每批只打
1131
+ // JSON.stringify(batch).slice(0, 400) 的预览,等于没有素材),以 exit 0 + NEEDS_GROUP_SUMMARIES
1132
+ // 返回、不导出;真失败(空单条摘要/降级话术/要素缺失/详情失败)仍由 exportFinal 以 exit 1 拦截。
1133
+ const needFile = path.join(dir, 'need-group-summaries.json');
1134
+ fs.writeFileSync(needFile, JSON.stringify(groupBatches, null, 2));
1135
+ console.log('\n⚠ NEEDS_GROUP_SUMMARIES:%d 个组摘要待回填(完整素材已写入 %s):', groupBatches.length, needFile);
1136
+ for (const batch of groupBatches) {
1137
+ console.log(' - %s|%s → %s|%d 条', batch.groupId, batch.parentTitle, batch.title, batch.count);
1138
+ }
1139
+ if (args['allow-empty-summary'] === true) {
1140
+ console.log(' --allow-empty-summary 已指定:跳过回填、按空组摘要继续导出。');
1141
+ } else {
1142
+ console.log(' → 在该文件的每个元素里补 "summary" 字段(脚本会自动只取 groupId/summary),重跑:');
1143
+ console.log(' node %s finish --dir %s --group-summaries %s --timing-report', path.basename(__filename), dir, needFile);
1144
+ return; // 不导出、退出码 0:素材已备齐、等回填,不是失败
1062
1145
  }
1063
1146
  }
1064
1147
  }
@@ -1075,6 +1158,7 @@ function cmdFinish(args) {
1075
1158
  // 详情取数发生在 smartGroup 阶段 ⇒ failed 从 smartgroup.json 的 data.summaryStats 取。
1076
1159
  allowEmptySummary: args['allow-empty-summary'] === true,
1077
1160
  allowDetailFail: args['allow-detail-fail'] === true,
1161
+ allowStaleRules: args['allow-stale-rules'] === true,
1078
1162
  detailFailed: ((sg.data || {}).summaryStats || {}).failed || 0,
1079
1163
  groupEmpty: (finalResult.meta && finalResult.meta.groupEmpty) || 0,
1080
1164
  });
@@ -1093,14 +1177,18 @@ function cmdRun(args) {
1093
1177
  const dir = args.out && args.out !== true
1094
1178
  ? String(args.out)
1095
1179
  : `/tmp/todo-summary-${new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14)}`;
1096
- cmdPrep({ ...args, out: dir });
1180
+ // collect 空结果已在 prep 里给出用户文案并结束(EMPTY),不再进 build/finish
1181
+ if (cmdPrep({ ...args, out: dir }) === 'EMPTY') return;
1097
1182
  cmdBuild({ ...args, dir });
1098
1183
  // 无待写摘要时 cmdBuild 已经内部走完 cmdFinish(含满意度标题填充与交付闸门),这里直接收工
1099
1184
  if (fs.existsSync(path.join(dir, 'final.data.json'))) return;
1185
+ // build→finish 已把组摘要素材落盘并停下(NEEDS_GROUP_SUMMARIES,exit 0)⇒ 不再重复跑 finish,
1186
+ // 也不再重复打印(cmdFinish 刚打过完整指引),静默收工(2026-11-02 P2)。
1187
+ if (fs.existsSync(path.join(dir, 'need-group-summaries.json'))) return;
1100
1188
  const sg = readJson(path.join(dir, 'smartgroup.json'));
1101
1189
  const pending = ((sg.data.summaryBatches || []).flatMap((batch) => batch.items || []));
1102
1190
  if (pending.length > 0) {
1103
- console.log('\n⚠ 有 %d 条待写摘要:写成 aiSummaries.json 后跑 finish(**不用重跑** collect/smartGroup)', pending.length);
1191
+ console.log('\n⚠ 有 %d 条待写摘要:素材见 %s;写成 aiSummaries.json 后跑 finish(**不用重跑** collect/smartGroup)', pending.length, path.join(dir, 'need-summaries.json'));
1104
1192
  console.log(' node %s finish --dir %s --summaries <aiSummaries.json> --timing-report', path.basename(__filename), dir);
1105
1193
  return;
1106
1194
  }
@@ -1125,10 +1213,14 @@ else if (cmd === 'run') cmdRun(args);
1125
1213
  else {
1126
1214
  console.log('用法:node ' + path.basename(__filename) + ' <run|prep|build|finish> [options]');
1127
1215
  console.log(' run [--days N | --from A --to B] [--limit 500] [--out DIR] [--timing-report] # 一次跑完 prep+build;用户没说时间段就都不传 = 不限时间取前 500 条');
1128
- console.log(' prep --limit 100 [--exclude 内部留言] [--days 1460] [--sub-category 待处理] [--out DIR]');
1129
- console.log(' build --dir DIR [--plan aiGroups.json] # prep 标注 [必须给 plan] 的组必须传;其余不传走确定性分组');
1216
+ console.log(' prep --limit 100 [--exclude 内部留言] [--days 1460] [--sub-category 待处理] [--tabid 7] [--isremarks 1,2] [--out DIR] # 小分类:--sub-category/--tabid(显式 tabid 优先);isremarks=备注筛选(数字逗号拼接)');
1217
+ console.log(' build --dir DIR [--plan aiGroups.json] # aiGroups 可先落盘(不传也行):build 只做首次分组+平铺,二次分组在 finish');
1130
1218
  console.log(' finish --dir DIR [--summaries aiSummaries.json] [--group-summaries aiGroupSummaries.json] [--timing-report]');
1131
- console.log(' 交付闸门:空摘要 / 组摘要缺失 / 详情取数失败都会 exit 1 拒绝导出;显式豁免:--allow-empty-summary / --allow-detail-fail');
1219
+ console.log(' 规则新鲜度:finish 导出前比对 aigroup 指纹——规则在取数后被改过即拦截(新规则+旧数据出不了文件);确要带旧口径出文件加 --allow-stale-rules');
1220
+ console.log(' 待写摘要素材:build/ run 停下时写 <out>/need-summaries.json(无损),控制台只留批次索引;写 aiSummaries.json 后 finish --summaries 回填');
1221
+ console.log(' 组摘要未回填:以 exit 0 + NEEDS_GROUP_SUMMARIES 返回(不导出),完整素材在 <out>/need-group-summaries.json;');
1222
+ console.log(' 在该文件补 summary 后用 --group-summaries 指回它重跑。真失败(空摘要/降级话术/要素缺失/详情失败)仍 exit 1;');
1223
+ console.log(' 豁免:--allow-empty-summary / --allow-detail-fail');
1132
1224
  console.log(' 默认:载荷只带 params.dataSource(现起本地服务,空闲 30 分钟自动退出);--no-serve 退回内联 data');
1133
1225
  console.log(' 提示:`--timing-report` 会在控制台打一段耗时摘要(阶段墙钟 / CLI 内部),**不产出 md**');
1134
1226
  process.exit(2);