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
@@ -115,29 +115,32 @@ export function isExpiringTodayRow(row, now = Date.now()) {
115
115
  return isExpiringTodayUserOverTime(row.userOverTime, now);
116
116
  }
117
117
  /**
118
- * 「建议优先处理的流程」筛选规则:
118
+ * 「建议优先处理的流程」筛选规则(2026-11-03 起可在 `generalRules.priorityGroup.rules` 配置启用子集):
119
119
  *
120
- * 1. **超时** 必收(`userOverTime` / `overTime` 判定,与紧急程度无关);
121
- * 2. **紧急程度**按 `requestLevel` 取:整批有 `=2`(紧急)就只要 2;没有 2 才退到 `=1`(重要);
120
+ * 1. **超时**(`overdue`)必收(`userOverTime` / `overTime` 判定,与紧急程度无关);
121
+ * 2. **紧急程度**(`levelFallback`)按 `requestLevel` 取:整批有 `=2`(紧急)就只要 2;没有 2 才退到 `=1`(重要);
122
122
  * `=0`(正常)**不因等级入选**——降级链到 1 就停,不倒到 0;
123
123
  * 3. 整批连 1 级都没有(全 0)时,等级条件不生效,只按超时筛;此时若一条都没超时,
124
124
  * 优先组为空("实在没有紧急的就算了"),不硬凑。
125
125
  *
126
126
  * 注意这是**筛选**,不是视图叠加:入选条目的 `requestLevel` 只有 2(或 2 缺失时的 1)与超时两类。
127
+ * `enabledRules` 缺省 = 全部启用(与历史行为一致);预留 `expiringToday`(今天到期)等后续扩展。
127
128
  */
128
- export function selectPriorityRows(rows) {
129
+ export function selectPriorityRows(rows, enabledRules) {
130
+ const rules = enabledRules ?? new Set(['overdue', 'levelFallback']);
129
131
  let maxLevel = 0;
130
132
  for (const row of rows) {
131
133
  if (row.requestLevel > maxLevel)
132
134
  maxLevel = row.requestLevel;
133
135
  }
134
- const targetLevel = maxLevel >= 2 ? 2 : maxLevel >= 1 ? 1 : undefined;
136
+ const targetLevel = rules.has('levelFallback') && maxLevel >= 2 ? 2
137
+ : rules.has('levelFallback') && maxLevel >= 1 ? 1 : undefined;
135
138
  const selected = [];
136
139
  let overdueCount = 0;
137
140
  let levelCount = 0;
138
141
  let bothCount = 0;
139
142
  for (const row of rows) {
140
- const overdue = isOverdueRow(row);
143
+ const overdue = rules.has('overdue') && isOverdueRow(row);
141
144
  const byLevel = targetLevel !== undefined && row.requestLevel >= targetLevel;
142
145
  if (overdue)
143
146
  overdueCount += 1;
@@ -323,12 +326,11 @@ export function runGroup(args) {
323
326
  const { rawCount, rows, duplicateCount, unreadableCount } = readItems(args.items);
324
327
  const groupBy = readGroupBy(args.groupBy);
325
328
  const topN = readTopN(args.topN);
326
- // 优先组是叠加视图:筛出的条目**不从**工作流分组剔除,两个视图各自完整。
327
- const priority = selectPriorityRows(rows);
328
- const priorityRows = sortPriorityRows(priority.rows);
329
- const restRows = rows;
329
+ // 2026-11-03 用户定:分组阶段**不再**产出「建议优先处理的流程」——它是最终结构上的一个置顶
330
+ // 视图,等摘要全部回填后用 `assemblePriorityGroup()` 按同一条规则从成品数据里筛出来。
331
+ // 这样前期(摘要单位、闸门、统计)都只需处理一份数据,不必到处特判 topPriority。
330
332
  const buckets = new Map();
331
- for (const row of restRows) {
333
+ for (const row of rows) {
332
334
  const key = groupKeyOf(row, groupBy);
333
335
  let bucket = buckets.get(key);
334
336
  if (!bucket) {
@@ -356,20 +358,8 @@ export function runGroup(args) {
356
358
  stats: statsOf(bucket.rows),
357
359
  ...(bucket.ids.length > 1 ? { workflowIds: bucket.ids } : {}),
358
360
  }));
359
- const groups = priorityRows.length > 0 && groupBy === 'workflow'
360
- ? [{
361
- id: PRIORITY_GROUP_ID,
362
- topPriority: true,
363
- title: PRIORITY_GROUP_TITLE,
364
- count: priorityRows.length,
365
- requestDatas: priorityRows.map((row) => toRequestData(row)),
366
- stats: statsOf(priorityRows),
367
- }, ...workflowGroups]
368
- : workflowGroups;
369
- const displayedWorkflow = workflowGroups.slice(0, topN);
370
- const displayed = priorityRows.length > 0 && groupBy === 'workflow'
371
- ? [groups[0], ...displayedWorkflow]
372
- : displayedWorkflow;
361
+ const groups = workflowGroups;
362
+ const displayed = workflowGroups.slice(0, topN);
373
363
  const rest = workflowGroups.slice(topN);
374
364
  const restCount = rest.reduce((sum, group) => sum + group.count, 0);
375
365
  const overall = statsOf(rows);
@@ -386,20 +376,6 @@ export function runGroup(args) {
386
376
  ? `有 ${unknownGroupCount} 条缺少工作流名称,已归入「${UNKNOWN_GROUP}」`
387
377
  : `有 ${unknownGroupCount} 条缺少分组字段,已归入「${UNKNOWN_GROUP}」`);
388
378
  }
389
- if (priorityRows.length > 0 && groupBy === 'workflow') {
390
- const levelText = priority.targetLevel === 2
391
- ? '紧急(requestLevel=2)'
392
- : priority.targetLevel === 1
393
- ? '重要(requestLevel=1;本次无 2 级数据,已按规则降级取 1,0 级不取)'
394
- : '无等级(本次整批没有 ≥1 级的条目,只按超时筛)';
395
- warnings.push(`「${PRIORITY_GROUP_TITLE}」筛出 ${priorityRows.length} 条:超时 ${priority.overdueCount} 条 / ` +
396
- `等级命中 ${priority.levelCount} 条(两者都满足 ${priority.bothCount} 条),等级口径=${levelText};` +
397
- `这 ${priorityRows.length} 条同时保留在各自工作流分组中(叠加视图:Σcount 会大于 totalCount,` +
398
- `totalCount 按去重条数计 = ${rows.length})`);
399
- }
400
- else if (priorityRows.length === 0 && groupBy === 'workflow') {
401
- warnings.push(`本次没有超时条目,也没有 requestLevel≥1 的条目,故不输出「${PRIORITY_GROUP_TITLE}」分组`);
402
- }
403
379
  if (rest.length > 0) {
404
380
  warnings.push(`markdown 只展示前 ${topN} 个工作流分组,其余 ${rest.length} 组共 ${restCount} 条仍在 wfIdGroup 中(未截断)`);
405
381
  }
@@ -434,7 +410,7 @@ export function runGroup(args) {
434
410
  },
435
411
  wfIdGroup: groups,
436
412
  /** 第二部分(非优先组)的总条数与组数,前端折叠/分页直接用它,不用自己减 */
437
- restTotal: restRows.length,
413
+ restTotal: rows.length,
438
414
  restGroupCount: workflowGroups.length,
439
415
  restGroups: rest.length > 0 ? { groupCount: rest.length, count: restCount } : null,
440
416
  warnings,
@@ -69,8 +69,9 @@ export function collectGroupSummaryBatches(groups, isCtxEmpty, maxCharsFor = ()
69
69
  continue;
70
70
  const kind = detectBusinessKind(group.title);
71
71
  for (const sub of groupItemsOf(group)) {
72
- // CLI 已经写好固定文案 ⇒ 不需要模型再写一遍
73
- if (isDeterministicGroupKind(kind) && (sub.summary ?? '').trim() !== '')
72
+ // 已有摘要就不产批次:①CLI 固定文案(needAI:false);②模型随 aiGroups 一起给的
73
+ // 子组摘要(2026-11-03 合并交互)——两者都不需要再问一次模型。
74
+ if ((sub.summary ?? '').trim() !== '')
74
75
  continue;
75
76
  const allRows = Array.isArray(sub.requestDatas) ? sub.requestDatas : [];
76
77
  const rows = isCtxEmpty ? allRows.filter((row) => !isCtxEmpty(row.requestId)) : allRows;
@@ -5,8 +5,9 @@ import { collectRawTodos } from './todoSmartConvert.collect.js';
5
5
  import { runSmartConvert } from './todoSmartConvert.aggregate.js';
6
6
  import { runGroup } from './todoSmartConvert.group.js';
7
7
  import { inferStage, readStage } from './todoSmartConvert.pipeline.js';
8
- import { runSmartGroup } from './todoSmartConvert.smartGroup.js';
8
+ import { runSmartGroup, assemblePriorityGroup } from './todoSmartConvert.smartGroup.js';
9
9
  import { runSummary, runSummaryContext } from './todoSmartConvert.summary.js';
10
+ import { runSubGroup } from './todoSmartConvert.subGroup.js';
10
11
  import { runGroupSummary, runGroupSummaryContext } from './todoSmartConvert.groupSummary.js';
11
12
  import { schemaOf, } from '../types.js';
12
13
  const inputSchema = schemaOf({
@@ -58,6 +59,7 @@ const inputSchema = schemaOf({
58
59
  },
59
60
  },
60
61
  withDetailContext: { type: 'boolean', description: 'smartGroup 是否同进程生成单条摘要上下文。' },
62
+ withSubGroup: { type: 'boolean', description: 'summary 回填后是否同进程接着做 ai 二次分组(stage=subGroup 等价)。' },
61
63
  withGroupSummaryContext: { type: 'boolean', description: 'summary 是否同进程生成组摘要上下文。' },
62
64
  detailPrefetch: { type: 'boolean', description: '废弃兼容参数,已忽略;collect 只在给了 groupFocus 且意图适用于本组时按需预取分组字段(走详情缓存)。' },
63
65
  groupBy: { type: 'string', enum: [...GROUP_BY_VALUES], default: 'workflow' },
@@ -80,6 +82,7 @@ const inputSchema = schemaOf({
80
82
  tabid: { type: 'string' },
81
83
  tabName: { type: 'string' },
82
84
  subCategory: { type: 'string' },
85
+ isremarks: { type: 'string', description: '备注筛选(getTodoListData params.isremarks),多个数字用英文逗号拼接,仅作用于本汇总取数' },
83
86
  workflow: { type: 'array', items: { type: 'string' } },
84
87
  creator: { type: 'string' },
85
88
  requestName: { type: 'string' },
@@ -139,6 +142,19 @@ export const workflowTodoSmartConvertOperations = [{
139
142
  : readStage(args.stage);
140
143
  const hasItems = Array.isArray(args.items);
141
144
  const resumed = stage === 'collect' && hasItems;
145
+ /**
146
+ * 把「建议优先处理的流程」拼到**最终结构**上(2026-11-03 用户定:放到最后生成)。
147
+ * 只在会产出成品结构的阶段(summary / groupSummary)调用;幂等,重复调用不会叠加。
148
+ */
149
+ const withPriorityView = (data, extraWarnings) => {
150
+ if ((args.groupBy ?? 'workflow') !== 'workflow')
151
+ return { data, warnings: extraWarnings };
152
+ const { groups, warning } = assemblePriorityGroup(data.wfIdGroup ?? []);
153
+ return {
154
+ data: { ...data, wfIdGroup: groups },
155
+ warnings: warning ? [...extraWarnings, warning] : extraWarnings,
156
+ };
157
+ };
142
158
  if ((stage === 'group' || stage === 'smartGroup' || stage === 'aggregate') && !hasItems) {
143
159
  throw workflowValidationError('items_required', 'stage=' + stage + ' 需要 items:先执行 stage=collect,再回灌 data.items', { retryAllowed: false });
144
160
  }
@@ -166,28 +182,48 @@ export const workflowTodoSmartConvertOperations = [{
166
182
  throw workflowValidationError('ai_summaries_required', 'stage=summary 需要 aiSummaries:先执行 stage=summaryContext', { retryAllowed: false });
167
183
  }
168
184
  const result = await runSummary(args, env);
185
+ // 2026-11-03 用户定:**ai 二次分组后移到单条摘要之后**(两者不再耦合)。
186
+ // `withSubGroup` 时在同一次进程内接着做二次分组,省掉一轮 CLI 启动(≈6~7s)。
187
+ // 顺序:summary(回填单条摘要) → subGroup(切子组) → priority 装配 → 组摘要批次。
188
+ let baseData = result.data;
189
+ let baseWarnings = result.warnings;
190
+ let subGrouped = false;
191
+ if (args.withSubGroup === true) {
192
+ const sub = await runSubGroup({ ...args, wfIdGroup: baseData.wfIdGroup }, env);
193
+ baseData = { ...baseData, wfIdGroup: sub.data.wfIdGroup };
194
+ baseWarnings = [...baseWarnings, ...sub.warnings];
195
+ subGrouped = true;
196
+ }
197
+ const final = withPriorityView(baseData, baseWarnings);
169
198
  if (args.withGroupSummaryContext === true) {
170
199
  const context = await runGroupSummaryContext({
171
- wfIdGroup: result.data.wfIdGroup,
172
- smartSummaryTitle: result.data.smartSummaryTitle,
173
- totalCount: result.data.totalCount,
200
+ wfIdGroup: final.data.wfIdGroup,
201
+ smartSummaryTitle: final.data.smartSummaryTitle,
202
+ totalCount: final.data.totalCount,
174
203
  }, env);
175
204
  if (Array.isArray(context.data.groupSummaryBatches) && context.data.groupSummaryBatches.length === 0) {
176
- return finish(env, args, startedAt, stage, result.data, {
177
- branch: 'todo-smart-convert', stage, aiSummarized: true, withGroupSummaryContext: true, ...result.meta,
178
- }, result.warnings);
205
+ return finish(env, args, startedAt, stage, final.data, {
206
+ branch: 'todo-smart-convert', stage, aiSummarized: true, subGrouped, withGroupSummaryContext: true, ...result.meta,
207
+ }, final.warnings);
179
208
  }
180
209
  return finish(env, args, startedAt, stage, {
181
- ...result.data,
210
+ ...final.data,
182
211
  groupSummaryBatches: context.data.groupSummaryBatches,
183
212
  groupSummaryStats: context.data.groupSummaryStats,
184
213
  }, {
185
- branch: 'todo-smart-convert', stage, aiSummarized: true, withGroupSummaryContext: true, ...result.meta,
186
- }, [...result.warnings, ...context.warnings]);
214
+ branch: 'todo-smart-convert', stage, aiSummarized: true, subGrouped, withGroupSummaryContext: true, ...result.meta,
215
+ }, [...final.warnings, ...context.warnings]);
187
216
  }
188
- return finish(env, args, startedAt, stage, result.data, {
189
- branch: 'todo-smart-convert', stage, aiSummarized: true, ...result.meta,
190
- }, result.warnings);
217
+ return finish(env, args, startedAt, stage, final.data, {
218
+ branch: 'todo-smart-convert', stage, aiSummarized: true, subGrouped, ...result.meta,
219
+ }, final.warnings);
220
+ }
221
+ if (stage === 'subGroup') {
222
+ const result = await runSubGroup(args, env);
223
+ const final = withPriorityView(result.data, result.warnings);
224
+ return finish(env, args, startedAt, stage, final.data, {
225
+ branch: 'todo-smart-convert', stage, aiGrouped: Array.isArray(args.aiGroups), subGrouped: true,
226
+ }, final.warnings);
191
227
  }
192
228
  if (stage === 'groupSummaryContext') {
193
229
  const result = await runGroupSummaryContext(args, env);
@@ -200,9 +236,10 @@ export const workflowTodoSmartConvertOperations = [{
200
236
  throw workflowValidationError('ai_group_summaries_required', 'stage=groupSummary 需要 aiGroupSummaries:先执行 stage=groupSummaryContext', { retryAllowed: false });
201
237
  }
202
238
  const result = await runGroupSummary(args, env);
203
- return finish(env, args, startedAt, stage, result.data, {
239
+ const final = withPriorityView(result.data, result.warnings);
240
+ return finish(env, args, startedAt, stage, final.data, {
204
241
  branch: 'todo-smart-convert', stage, aiSummarized: true, ...result.meta,
205
- }, result.warnings);
242
+ }, final.warnings);
206
243
  }
207
244
  if (stage === 'group' || resumed) {
208
245
  return finish(env, args, startedAt, 'group', runGroup(args), {
@@ -32,7 +32,22 @@ export function inferStage(args) {
32
32
  return 'groupSummary';
33
33
  if (Array.isArray(args.aiSummaries))
34
34
  return 'summary';
35
- return hasAnyRequestSummary(args.wfIdGroup) ? 'groupSummaryContext' : 'summaryContext';
35
+ if (hasAnyRequestSummary(args.wfIdGroup)) {
36
+ // 2026-11-03:ai 二次分组后移到单条摘要之后。
37
+ // 单条摘要已填、但结构里还没有汇总项(`type:"group"`)⇒ 下一步是 subGroup;
38
+ // 已经有汇总项 ⇒ 说明二次分组做过,下一步才是组摘要。
39
+ return hasAnySubGroup(args.wfIdGroup) ? 'groupSummaryContext' : 'subGroup';
40
+ }
41
+ return 'summaryContext';
42
+ }
43
+ /** 结构里是否已经存在汇总项(`displayItems[].type === "group"`)。 */
44
+ export function hasAnySubGroup(groups) {
45
+ for (const raw of groups) {
46
+ const items = raw?.displayItems;
47
+ if (Array.isArray(items) && items.some((item) => item?.type === 'group'))
48
+ return true;
49
+ }
50
+ return false;
36
51
  }
37
52
  /** 从**未经校验**的分组对象里摊平出条目(推断阶段用,同时兼容新旧两种结构)。 */
38
53
  export function rowsOfRawGroup(group) {
@@ -1,9 +1,9 @@
1
1
  import { workflowValidationError } from '../../errors.js';
2
2
  import { renderGroupSummaryTemplate, } from './businessRules.js';
3
3
  import { asRecord, readBoundedInt, } from './todoSmartConvert.core.js';
4
- import { PRIORITY_GROUP_ID, PRIORITY_GROUP_TITLE, runGroup, GROUP_MIN_ITEMS_FOR_SUBGROUP, } from './todoSmartConvert.group.js';
4
+ import { PRIORITY_GROUP_ID, PRIORITY_GROUP_TITLE, runGroup, GROUP_MIN_ITEMS_FOR_SUBGROUP, selectPriorityRows, sortPriorityRows, } from './todoSmartConvert.group.js';
5
5
  import { fetchDetailFacts, stampsOfRows } from './todoSmartConvert.detail.js';
6
- import { activeRuleFor, evaluateFocusKeywords, primeRules } from './aigroupRules.js';
6
+ import { activeRuleFor, evaluateFocusKeywords, generalRules, primeRules } from './aigroupRules.js';
7
7
  import { openSummaryCache, promptTagOf } from './summaryCache.js';
8
8
  import { openDetailCache, detailMaterialTagOf } from './todoDetailCache.js';
9
9
  const DEFAULT_REQUEST_SUMMARY_MAX = 50;
@@ -19,6 +19,54 @@ export function rowsOfGroup(group) {
19
19
  export function groupItemsOf(group) {
20
20
  return group.displayItems.filter((item) => item.type === 'group');
21
21
  }
22
+ /**
23
+ * 最终结构上的「建议优先处理的流程」置顶视图(2026-11-03 用户定:改到**最后一步**生成)。
24
+ *
25
+ * - 输入是**成品结构**(单条摘要、组摘要都已回填):按同一条规则(`generalRules.priorityGroup`)
26
+ * 从各工作流分组的 `displayItems` 里摊平条目、筛出「超时 / 紧急程度」命中项,复制一份成
27
+ * `id=priority`、`unshift` 到首位;
28
+ * - **幂等**:输入里已有 priority 组时先剔除再重建,stage 串联多次调用结果一致;
29
+ * - 叠加视图语义不变:入选条目仍保留在原工作流分组里 ⇒ `Σcount` 仍大于 `totalCount`
30
+ * (`totalCount` 始终按去重条数计);
31
+ * - 判定字段(`overTime` / `userOverTime` / `requestLevel`)都在 `requestData` 上,不必回查列表;
32
+ * 原排序用的 `receivedTime` 不在 requestData 中,同档退化为按 `requestId` 稳定排序。
33
+ */
34
+ export function assemblePriorityGroup(groups) {
35
+ const base = (groups ?? []).filter((group) => group.id !== PRIORITY_GROUP_ID && group.topPriority !== true);
36
+ const pgConfig = generalRules()?.priorityGroup ?? null;
37
+ const title = pgConfig?.title?.trim() || PRIORITY_GROUP_TITLE;
38
+ const rules = new Set(pgConfig?.rules ?? ['overdue', 'levelFallback']);
39
+ const rows = base.flatMap((group) => rowsOfGroup(group));
40
+ if (rows.length === 0)
41
+ return { groups: base };
42
+ const selection = selectPriorityRows(rows, rules);
43
+ const selected = sortPriorityRows(selection.rows);
44
+ if (selected.length === 0) {
45
+ return { groups: base, warning: `本次没有超时条目,也没有 requestLevel≥1 的条目,故不输出「${title}」分组` };
46
+ }
47
+ const levelText = selection.targetLevel === 2
48
+ ? '紧急(requestLevel=2)'
49
+ : selection.targetLevel === 1
50
+ ? '重要(requestLevel=1;本次无 2 级数据,已按规则降级取 1,0 级不取)'
51
+ : '无等级(本次整批没有 ≥1 级的条目,只按超时筛)';
52
+ const priorityGroup = {
53
+ id: PRIORITY_GROUP_ID,
54
+ topPriority: true,
55
+ title,
56
+ count: selected.length,
57
+ // 大分组(含优先组)默认展开;只有汇总项收缩
58
+ defaultExpanded: true,
59
+ // 复制一份:优先组是独立视图,确保两边互不影响
60
+ displayItems: requestItemsOf(selected.map((row) => ({ ...row }))),
61
+ };
62
+ return {
63
+ groups: [priorityGroup, ...base],
64
+ warning: `「${title}」筛出 ${selected.length} 条:超时 ${selection.overdueCount} 条 / `
65
+ + `等级命中 ${selection.levelCount} 条(两者都满足 ${selection.bothCount} 条),等级口径=${levelText};`
66
+ + `这 ${selected.length} 条同时保留在各自工作流分组中(叠加视图:Σcount 会大于 totalCount,`
67
+ + 'totalCount 按去重条数计)',
68
+ };
69
+ }
22
70
  /**
23
71
  * 校验并索引 `aiGroups`。
24
72
  *
@@ -40,10 +88,11 @@ export function readAiGroups(value) {
40
88
  if (!record) {
41
89
  throw workflowValidationError('ai_groups_invalid', `${label} 必须是对象 { groupId, title, requestIds }`, { retryAllowed: false });
42
90
  }
43
- const unknownKeys = Object.keys(record).filter((key) => !['groupId', 'title', 'requestIds'].includes(key));
91
+ const unknownKeys = Object.keys(record).filter((key) => !['groupId', 'title', 'requestIds', 'summary'].includes(key));
44
92
  if (unknownKeys.length > 0) {
45
- throw workflowValidationError('ai_groups_invalid', `${label} 不接受字段:${unknownKeys.join(', ')};只支持 groupId / title / requestIds`, { retryAllowed: false, details: { unknownKeys } });
93
+ throw workflowValidationError('ai_groups_invalid', `${label} 不接受字段:${unknownKeys.join(', ')};只支持 groupId / title / requestIds / summary`, { retryAllowed: false, details: { unknownKeys } });
46
94
  }
95
+ const planSummary = typeof record.summary === 'string' ? record.summary.trim() : '';
47
96
  const groupId = typeof record.groupId === 'string' ? record.groupId.trim() : '';
48
97
  if (!groupId) {
49
98
  throw workflowValidationError('ai_groups_invalid', `${label}.groupId 必填且必须是字符串(19 位 workflowId 用数字会失真;值取 stage=group 的 wfIdGroup[i].id)`, { retryAllowed: false });
@@ -65,7 +114,12 @@ export function readAiGroups(value) {
65
114
  ids.push(id);
66
115
  }
67
116
  const bucket = map.get(groupId) ?? [];
68
- bucket.push({ groupId, title, requestIds: [...new Set(ids)] });
117
+ bucket.push({
118
+ groupId,
119
+ title,
120
+ requestIds: [...new Set(ids)],
121
+ ...(planSummary ? { summary: planSummary } : {}),
122
+ });
69
123
  map.set(groupId, bucket);
70
124
  });
71
125
  return map;
@@ -159,48 +213,29 @@ function rowKeyForCount(row) {
159
213
  return `id:${id}`;
160
214
  return `tx:${row.requestNameTitle ?? ''}|${row.creater ?? ''}|${row.nodeId ?? ''}`;
161
215
  }
162
- export async function runSmartGroup(args, env, options = {}) {
163
- const base = runGroup(args);
164
- const groups = (base.wfIdGroup ?? []);
165
- /** `null` = 确定性模式;数组(可能为空)= 兼容模式 */
166
- const plans = args.aiGroups === undefined || args.aiGroups === null
167
- ? undefined
168
- : readAiGroups(args.aiGroups);
169
- // 注意:`aiGroups: []` 与「未传」现在等价(默认不分组),不再需要用空数组区分语义。
170
- // 有侧重点时分组 = 标题语义 + 侧重点;子组顺序沿用模型返回顺序。
171
- // 无侧重点时普通组按条数降序,请假/财务使用阈值规则。
172
- const groupFocus = typeof args.groupFocus === 'string' ? args.groupFocus.trim() : '';
173
- const hasFocus = groupFocus.length > 0;
174
- // 摘要字数口径:缓存命中要按口径过滤(10 字要求不能吃 50 字的旧摘要)。
175
- const summaryMaxChars = readBoundedInt(args.summaryMaxChars, "summaryMaxChars", DEFAULT_REQUEST_SUMMARY_MAX, 10, 500);
176
- // 详情用来算阈值指标 + 判定空上下文;摘要缓存只省 AI,不省详情(阈值指标只认流程详情数据)。
177
- // withDetailContext=false 时只生成结构、不获取详情(此时没有指标,阈值规则自然不生效)。
178
- const skipDetail = args.withDetailContext === false;
179
- const summaryCache = await openSummaryCache(env.ctx);
180
- // 摘要条目的素材特征值:详情素材变了 ⇒ 旧摘要视为不存在(该条重新写摘要)
181
- const detailCacheForTag = skipDetail ? null : await openDetailCache(env.ctx);
182
- const materialTagOfRow = (requestId, stamp) => {
183
- const entry = detailCacheForTag?.get(requestId, stamp);
184
- return entry ? detailMaterialTagOf(entry) : undefined;
185
- };
186
- const cachedIds = new Set();
187
- const allRows = [];
188
- for (const group of groups) {
189
- for (const row of group.requestDatas ?? []) {
190
- allRows.push(row);
191
- if (summaryCache.get(row.requestId, row.lastFeedBackDateTime, summaryMaxChars, promptTagOf(activeRuleFor(group.title)?.requestSummaryPrompt ?? ''), materialTagOfRow(row.requestId, row.lastFeedBackDateTime)))
192
- cachedIds.add(row.requestId);
193
- }
194
- }
195
- // 详情缓存已经按 `requestId__lastFeedBackDateTime` 落盘,命中时零请求;
196
- // 所以「摘要命中」不再等于「可以不拉详情」——否则阈值分组会因为没有指标而失效(回归 10.2)。
197
- const detailIds = [...new Set(allRows.map((row) => row.requestId).filter(Boolean))];
198
- // stamps 决定详情缓存的键:不传就永远 miss(缓存白建)。
199
- const stamps = stampsOfRows(allRows);
200
- const facts = skipDetail
201
- ? new Map()
202
- : (options.facts ?? (await fetchDetailFacts(env, detailIds, args, stamps)).facts);
203
- const metrics = skipDetail ? undefined : (options.metrics ?? metricsOfFacts(facts));
216
+ /** 平铺结构:不做 ai 二次分组,`displayItems` 全是单条。 */
217
+ export function flattenFinalGroups(groups) {
218
+ return groups.map((group) => ({
219
+ id: group.id,
220
+ topPriority: group.id === PRIORITY_GROUP_ID,
221
+ title: group.title,
222
+ count: group.count,
223
+ defaultExpanded: true,
224
+ displayItems: requestItemsOf(group.requestDatas),
225
+ }));
226
+ }
227
+ /**
228
+ * 在**已有结构**上应用 aigroup 规则的 ai 二次分组(2026-11-03 解耦)。
229
+ *
230
+ * 输入是首次 workflowName 分组产出的平铺结构(条目上可能已经带着单条摘要),
231
+ * 输出带汇总项的结构;既不重新分组、也不碰详情缓存,纯粹做「组内切子组」。
232
+ */
233
+ export function applySubGroups(groups, options) {
234
+ const plans = options.plans;
235
+ const facts = options.facts;
236
+ const metrics = options.metrics;
237
+ const warnings = options.warnings;
238
+ // 计划本身的合法性:未知 groupId / 对优先组做二次分组,都要硬失败(2026-11-03 迁移到本阶段)。
204
239
  const knownGroupIds = new Set(groups.map((group) => group.id));
205
240
  for (const groupId of plans?.keys() ?? []) {
206
241
  if (!knownGroupIds.has(groupId)) {
@@ -210,18 +245,6 @@ export async function runSmartGroup(args, env, options = {}) {
210
245
  throw workflowValidationError('ai_group_priority_unsupported', `「${PRIORITY_GROUP_TITLE}」不做二次分组:目标结构里它只有 displayItems(全是单条展示),没有汇总项;请把它的条目从 aiGroups 里去掉`, { retryAllowed: true });
211
246
  }
212
247
  }
213
- // markdown 是中间态的东西,最终结构里没有,相关提示一并丢掉,免得误导调用方。
214
- const warnings = (base.warnings ?? [])
215
- .filter((text) => !text.startsWith('markdown 只展示前'));
216
- // aigroup 规则(default.json + user-intents.json):每个 operation 只加载一次。
217
- warnings.push(...primeRules(env.ctx).warnings);
218
- if (skipDetail) {
219
- warnings.push('withDetailContext=false:本次未拉取任何详情——请假/财务阈值改为按标题识别、跳过整组空判定,也不产 summaryBatches');
220
- }
221
- else if (cachedIds.size > 0) {
222
- warnings.push(`有 ${cachedIds.size} 条命中本地摘要缓存(这些条目不用再调 AI);`
223
- + '详情仍按缓存优先取一次——阈值指标只认流程详情数据,跳过详情会让阈值分组失效');
224
- }
225
248
  const finalGroups = groups.map((group) => {
226
249
  if (group.id === PRIORITY_GROUP_ID) {
227
250
  return {
@@ -358,7 +381,11 @@ export async function runSmartGroup(args, env, options = {}) {
358
381
  }
359
382
  groupRows = kept;
360
383
  }
361
- subGroups.push({ title: plan.title, requestDatas: groupRows });
384
+ subGroups.push({
385
+ title: plan.title,
386
+ requestDatas: groupRows,
387
+ ...(plan.summary ? { summary: plan.summary } : {}),
388
+ });
362
389
  }
363
390
  // 子组 <2 不成立(含有意图的子组):解散,条目平铺(Q5)。
364
391
  const kept = subGroups.filter((sub) => sub.requestDatas.length >= 2);
@@ -400,6 +427,10 @@ export async function runSmartGroup(args, env, options = {}) {
400
427
  }
401
428
  /** 组摘要:规则 `needAI:false` 时用 JSON `template` 渲染,否则留空交 AI。 */
402
429
  const renderSubGroupSummary = (sub) => {
430
+ // ① 模型随 aiGroups 一起给的摘要优先(同一次交互产出,见 AiSmartGroupPlan.summary)
431
+ if ((sub.summary ?? '').trim())
432
+ return sub.summary;
433
+ // ② 否则走规则里的固定模板(needAI:false 的确定性组)
403
434
  const groupSummary = rule.aiGroup?.groupSummary;
404
435
  if (!groupSummary || groupSummary.needAI || !groupSummary.template)
405
436
  return '';
@@ -449,10 +480,67 @@ export async function runSmartGroup(args, env, options = {}) {
449
480
  displayItems: [...groupItems, ...requestItemsOf(singles)],
450
481
  };
451
482
  });
483
+ return finalGroups;
484
+ }
485
+ export async function runSmartGroup(args, env, options = {}) {
486
+ const base = runGroup(args);
487
+ const groups = (base.wfIdGroup ?? []);
488
+ /** `null` = 确定性模式;数组(可能为空)= 兼容模式 */
489
+ const plans = args.aiGroups === undefined || args.aiGroups === null
490
+ ? undefined
491
+ : readAiGroups(args.aiGroups);
492
+ // 注意:`aiGroups: []` 与「未传」现在等价(默认不分组),不再需要用空数组区分语义。
493
+ // 有侧重点时分组 = 标题语义 + 侧重点;子组顺序沿用模型返回顺序。
494
+ // 无侧重点时普通组按条数降序,请假/财务使用阈值规则。
495
+ const groupFocus = typeof args.groupFocus === 'string' ? args.groupFocus.trim() : '';
496
+ const hasFocus = groupFocus.length > 0;
497
+ // 摘要字数口径:缓存命中要按口径过滤(10 字要求不能吃 50 字的旧摘要)。
498
+ const summaryMaxChars = readBoundedInt(args.summaryMaxChars, "summaryMaxChars", DEFAULT_REQUEST_SUMMARY_MAX, 10, 500);
499
+ // 详情用来算阈值指标 + 判定空上下文;摘要缓存只省 AI,不省详情(阈值指标只认流程详情数据)。
500
+ // withDetailContext=false 时只生成结构、不获取详情(此时没有指标,阈值规则自然不生效)。
501
+ const skipDetail = args.withDetailContext === false;
502
+ const summaryCache = await openSummaryCache(env.ctx);
503
+ // 摘要条目的素材特征值:详情素材变了 ⇒ 旧摘要视为不存在(该条重新写摘要)
504
+ const detailCacheForTag = skipDetail ? null : await openDetailCache(env.ctx);
505
+ const materialTagOfRow = (requestId, stamp) => {
506
+ const entry = detailCacheForTag?.get(requestId, stamp);
507
+ return entry ? detailMaterialTagOf(entry) : undefined;
508
+ };
509
+ const cachedIds = new Set();
510
+ const allRows = [];
511
+ for (const group of groups) {
512
+ for (const row of group.requestDatas ?? []) {
513
+ allRows.push(row);
514
+ if (summaryCache.get(row.requestId, row.lastFeedBackDateTime, summaryMaxChars, promptTagOf(activeRuleFor(group.title)?.requestSummaryPrompt ?? ''), materialTagOfRow(row.requestId, row.lastFeedBackDateTime)))
515
+ cachedIds.add(row.requestId);
516
+ }
517
+ }
518
+ // 详情缓存已经按 `requestId__lastFeedBackDateTime` 落盘,命中时零请求;
519
+ // 所以「摘要命中」不再等于「可以不拉详情」——否则阈值分组会因为没有指标而失效(回归 10.2)。
520
+ const detailIds = [...new Set(allRows.map((row) => row.requestId).filter(Boolean))];
521
+ // stamps 决定详情缓存的键:不传就永远 miss(缓存白建)。
522
+ const stamps = stampsOfRows(allRows);
523
+ const facts = skipDetail
524
+ ? new Map()
525
+ : (options.facts ?? (await fetchDetailFacts(env, detailIds, args, stamps)).facts);
526
+ // markdown 是中间态的东西,最终结构里没有,相关提示一并丢掉,免得误导调用方。
527
+ const warnings = (base.warnings ?? [])
528
+ .filter((text) => !text.startsWith('markdown 只展示前'));
529
+ // aigroup 规则(default.json + user-intents.json):每个 operation 只加载一次。
530
+ warnings.push(...primeRules(env.ctx).warnings);
531
+ if (skipDetail) {
532
+ warnings.push('withDetailContext=false:本次未拉取任何详情——请假/财务阈值改为按标题识别、跳过整组空判定,也不产 summaryBatches');
533
+ }
534
+ else if (cachedIds.size > 0) {
535
+ warnings.push(`有 ${cachedIds.size} 条命中本地摘要缓存(这些条目不用再调 AI);`
536
+ + '详情仍按缓存优先取一次——阈值指标只认流程详情数据,跳过详情会让阈值分组失效');
537
+ }
452
538
  // 分组顺序完全沿用第一次 workflowName 分组顺序。
453
- const orderedGroups = finalGroups;
454
- if (plans === undefined) {
455
- warnings.push('smartGroup 未收到 aiGroups:已按业务类型执行确定性分组;需要语义侧重点时再传入本轮 aiGroups');
539
+ // 2026-11-03 用户定:本阶段**只做** workflowName 首次分组,产出平铺 displayItems。
540
+ // ai 二次分组一律由 `stage=subGroup` 调 `applySubGroups()` 完成(旧的就地分组分支已删除)。
541
+ const orderedGroups = flattenFinalGroups(groups);
542
+ if (plans !== undefined) {
543
+ warnings.push('aiGroups 已收到,但本阶段不做二次分组:留到 stage=subGroup 应用');
456
544
  }
457
545
  return {
458
546
  data: {
@@ -0,0 +1,56 @@
1
+ import { applySubGroups, metricsOfFacts, readAiGroups, rowsOfGroup, totalTodoCount, } from './todoSmartConvert.smartGroup.js';
2
+ import { fetchDetailFacts, stampsOfRows } from './todoSmartConvert.detail.js';
3
+ import { readFinalGroups, readSmartSummaryTitle } from './todoSmartConvert.summary.js';
4
+ /**
5
+ * `stage=subGroup`:**ai 二次分组**(2026-11-03 从 `smartGroup` 拆出来独立成阶段)。
6
+ *
7
+ * 为什么要拆:二次分组曾经和「workflowName 首次分组」绑在同一个 stage 里,导致
8
+ * `focus:false` 的组在**写单条摘要之前**就强制要求模型给出 `aiGroups`——摘要与分组互相耦合。
9
+ * 现在的链路:
10
+ *
11
+ * ```
12
+ * collect → smartGroup(仅首次分组 + 平铺) → summaryContext → summary(单条摘要)
13
+ * → subGroup(本阶段:ai 二次分组) → groupSummaryContext → groupSummary
14
+ * ```
15
+ *
16
+ * 本阶段只做「组内切子组」:不重新分组、不改摘要、详情全部走缓存(命中率 100%,
17
+ * 因为 collect 已经全量回填)。条目上已有的单条摘要随 `requestDatas` 一起带进子组。
18
+ */
19
+ export async function runSubGroup(args, env) {
20
+ const groups = await readFinalGroups(args, env);
21
+ const warnings = [];
22
+ // 优先级组是最终结构上的叠加视图,不参与二次分组(导出前由 assemblePriorityGroup 重建)。
23
+ const wfGroups = groups
24
+ .filter((group) => !group.topPriority)
25
+ .map((group) => ({
26
+ id: group.id,
27
+ title: group.title,
28
+ count: group.count,
29
+ requestDatas: rowsOfGroup(group),
30
+ }));
31
+ const plans = args.aiGroups === undefined || args.aiGroups === null
32
+ ? undefined
33
+ : readAiGroups(args.aiGroups);
34
+ // 阈值类二次分组(请假天数 / 报销金额 / 计划完成时间)要指标 ⇒ 仍要详情;
35
+ // collect 已全量回填,这里等价于纯缓存命中、零 HTTP。
36
+ const rows = wfGroups.flatMap((group) => group.requestDatas);
37
+ const detailIds = [...new Set(rows.map((row) => row.requestId).filter(Boolean))];
38
+ const facts = new Map();
39
+ let metrics;
40
+ if (args.withDetailContext !== false && detailIds.length > 0) {
41
+ const result = await fetchDetailFacts(env, detailIds, args, stampsOfRows(rows));
42
+ for (const [requestId, fact] of result.facts)
43
+ facts.set(requestId, fact);
44
+ metrics = metricsOfFacts(facts);
45
+ warnings.push(...result.warnings);
46
+ }
47
+ const finalGroups = applySubGroups(wfGroups, { plans, facts, metrics, warnings });
48
+ return {
49
+ data: {
50
+ smartSummaryTitle: readSmartSummaryTitle(args, `您当前有 ${totalTodoCount(groups)} 条待办,清单如下:`),
51
+ totalCount: totalTodoCount(groups),
52
+ wfIdGroup: finalGroups,
53
+ },
54
+ warnings,
55
+ };
56
+ }