weaver-work-cli 0.1.31 → 0.1.33

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 (186) hide show
  1. package/README.md +2 -2
  2. package/dist/chunks/{archive-U766FTFH.js → archive-FVFY6GZN.js} +2 -2
  3. package/dist/chunks/{archive-oa-DMWEKX3L.js → archive-oa-NW3OD4UV.js} +2 -2
  4. package/dist/chunks/{asset-TCR7VDEV.js → asset-7QJKMIDV.js} +2 -2
  5. package/dist/chunks/{attachment-content-AQNX5N3S.js → attachment-content-OSAVA6K3.js} +3 -2
  6. package/dist/chunks/{blog-EVZWGDVM.js → blog-I7SDPNW4.js} +2 -2
  7. package/dist/chunks/{caizhilian-JOWS6SL5.js → caizhilian-O37ZIQNY.js} +2 -2
  8. package/dist/chunks/{calendar-5WNCV66Q.js → calendar-7KXE4ED3.js} +2 -2
  9. package/dist/chunks/{chunk-VZTXRG6X.js → chunk-2FLWXFVS.js} +4 -4
  10. package/dist/chunks/{chunk-QWY6PGL6.js → chunk-2LV74IWP.js} +361 -56
  11. package/dist/chunks/chunk-3SO6QL6U.js +73 -0
  12. package/dist/chunks/{chunk-H3WDBYJW.js → chunk-6ATGWRZM.js} +6 -4
  13. package/dist/chunks/chunk-BYATLLEF.js +68 -0
  14. package/dist/chunks/chunk-BYBSCN4X.js +932 -0
  15. package/dist/chunks/{chunk-324YPTC2.js → chunk-D223PTXU.js} +11 -2
  16. package/dist/chunks/{chunk-R2IJAK7T.js → chunk-EEWPQE3O.js} +2 -2
  17. package/dist/chunks/{chunk-J7RJTZSV.js → chunk-GFPXQIY2.js} +2 -2
  18. package/dist/chunks/{chunk-AC7KJWC5.js → chunk-HEL5XTYI.js} +12 -6
  19. package/dist/chunks/{chunk-CVZ2JOSW.js → chunk-IHUYP6LO.js} +1 -1
  20. package/dist/chunks/{chunk-R7O5PUWO.js → chunk-ILMBNVSG.js} +2 -2
  21. package/dist/chunks/{chunk-6V7ZLF34.js → chunk-KAS45KNR.js} +1 -1
  22. package/dist/chunks/{chunk-H6XSTSLM.js → chunk-MEEFGWTM.js} +54 -196
  23. package/dist/chunks/{chunk-GCG2NMWR.js → chunk-MQ2I5AA3.js} +4 -2
  24. package/dist/chunks/chunk-NHA62QD7.js +774 -0
  25. package/dist/chunks/{chunk-2DUV44UX.js → chunk-OCU4RLHC.js} +641 -309
  26. package/dist/chunks/{chunk-65QMVWHS.js → chunk-PRHJGPMA.js} +2 -2
  27. package/dist/chunks/{chunk-WHD4XMRP.js → chunk-Q6LWB5HX.js} +2 -2
  28. package/dist/chunks/{chunk-D4LTJSJD.js → chunk-RBGOMK4Y.js} +50 -33
  29. package/dist/chunks/chunk-VBCFFFGT.js +226 -0
  30. package/dist/chunks/{ebuilder-form-NJ4YK6CC.js → chunk-WRRJ4GG2.js} +22 -1148
  31. package/dist/chunks/{chunk-RCGYCOMG.js → chunk-WY74UBJ6.js} +264 -750
  32. package/dist/chunks/conflict-SFNEVJY5.js +21 -0
  33. package/dist/chunks/continuation-RN6PZN2M.js +11 -0
  34. package/dist/chunks/cowork-CKWYHXYB.js +2508 -0
  35. package/dist/chunks/ebuilder-form-KOLJRVZD.js +121 -0
  36. package/dist/chunks/{ehr-MMYN36VA.js → ehr-WFOC3B4D.js} +1174 -20
  37. package/dist/chunks/{esb-HIWHOU7Q.js → esb-LJZTN663.js} +4 -4
  38. package/dist/chunks/{fna-KBACQTA5.js → fna-F7QRIIZC.js} +2 -2
  39. package/dist/chunks/{host-3OHS5QUX.js → host-BCGUVEWX.js} +3 -3
  40. package/dist/chunks/host-BQBRHVR3.js +23 -0
  41. package/dist/chunks/{host-KQL5N4HO.js → host-DGE2TZD4.js} +3 -3
  42. package/dist/chunks/{host-LLSZQCWR.js → host-KXPTEWFS.js} +3 -3
  43. package/dist/chunks/{host-IG6KEH5Q.js → host-VQ6JJXGQ.js} +3 -3
  44. package/dist/chunks/{hrm-LYEC67ZQ.js → hrm-NI6XAKW5.js} +3 -3
  45. package/dist/chunks/{im-RN2UKAEU.js → im-7TRTF3KT.js} +88 -12
  46. package/dist/chunks/{invoice-EEPHLAF6.js → invoice-AOXWJKYT.js} +3 -3
  47. package/dist/chunks/{jiuchuanhui-WVVZYSJE.js → jiuchuanhui-7NQWEZAW.js} +2 -2
  48. package/dist/chunks/{lazy-dispatch-MDOFFL2S.js → lazy-dispatch-RNS4EE4S.js} +4 -4
  49. package/dist/chunks/{mail-LNQLTF6L.js → mail-VDHJAB4O.js} +3 -3
  50. package/dist/chunks/{meeting-XSD3BDVQ.js → meeting-GA7XCNNP.js} +214 -43
  51. package/dist/chunks/{okr-TMWTIU2K.js → okr-TQIRERPQ.js} +2 -2
  52. package/dist/chunks/operations-5QL3AECO.js +11 -0
  53. package/dist/chunks/{plan-A2FSFA7T.js → plan-4RY4LN2P.js} +2 -2
  54. package/dist/chunks/{project-T26QLBQF.js → project-D6XT2OC5.js} +2 -2
  55. package/dist/chunks/{registry-JCFEYXOE.js → registry-JHRIDQ3A.js} +4 -4
  56. package/dist/chunks/{registry-2VLUAESA.js → registry-OZSMDVPP.js} +2 -2
  57. package/dist/chunks/{registry-ZJMJVWFA.js → registry-QKMI43Q6.js} +8 -6
  58. package/dist/chunks/{registry-Q7OQJ3SU.js → registry-VOGKFFTR.js} +2 -2
  59. package/dist/chunks/{room-recommend-UR4ZDZS6.js → room-recommend-D4EUOOWY.js} +4 -3
  60. package/dist/chunks/{schedule-5L2PM4M6.js → schedule-XPFBHPCV.js} +4 -3
  61. package/dist/chunks/{workflow-TR6PG2SL.js → workflow-HLBZRWIY.js} +172 -6
  62. package/dist/core/package-paths.js +16 -2
  63. package/dist/index.js +30 -25
  64. package/dist/internal/e10/auth-recovery.js +14 -4
  65. package/dist/shortcuts/contract/host.js +34 -59
  66. package/dist/shortcuts/cowork/continuation.js +70 -0
  67. package/dist/shortcuts/cowork/errors.js +59 -0
  68. package/dist/shortcuts/cowork/host.js +172 -0
  69. package/dist/shortcuts/cowork/index.js +166 -0
  70. package/dist/shortcuts/cowork/manifest.js +65 -0
  71. package/dist/shortcuts/cowork/operations/discuss-write.js +277 -0
  72. package/dist/shortcuts/cowork/operations/item-write.js +380 -0
  73. package/dist/shortcuts/cowork/operations/member-write.js +283 -0
  74. package/dist/shortcuts/cowork/operations/permission-write.js +286 -0
  75. package/dist/shortcuts/cowork/operations/read.js +423 -0
  76. package/dist/shortcuts/cowork/operations/registry.js +39 -0
  77. package/dist/shortcuts/cowork/operations/review-write.js +157 -0
  78. package/dist/shortcuts/cowork/operations/shared.js +215 -0
  79. package/dist/shortcuts/cowork/operations/types.js +24 -0
  80. package/dist/shortcuts/cowork/operations.js +19 -0
  81. package/dist/shortcuts/ehr/host.js +4 -1
  82. package/dist/shortcuts/ehr/operations/recruit-candidate-import.js +757 -0
  83. package/dist/shortcuts/ehr/operations/recruit-position.js +367 -0
  84. package/dist/shortcuts/ehr/operations/recruit-stats.js +133 -0
  85. package/dist/shortcuts/ehr/operations/recruit-write.js +8 -16
  86. package/dist/shortcuts/ehr/operations/recruit.js +35 -10
  87. package/dist/shortcuts/ehr/operations/registry.js +6 -0
  88. package/dist/shortcuts/im/diagnostics.js +97 -0
  89. package/dist/shortcuts/im/host.js +58 -31
  90. package/dist/shortcuts/im/index.js +7 -2
  91. package/dist/shortcuts/im/operations/at-mention.js +127 -0
  92. package/dist/shortcuts/im/operations/file-user.js +10 -2
  93. package/dist/shortcuts/im/operations/group-read.js +26 -5
  94. package/dist/shortcuts/im/operations/group-write.js +35 -28
  95. package/dist/shortcuts/im/operations/msg-sync.js +7 -2
  96. package/dist/shortcuts/im/operations/msg-write.js +76 -10
  97. package/dist/shortcuts/im/operations/quote-mention.js +119 -0
  98. package/dist/shortcuts/im/operations/session.js +1 -1
  99. package/dist/shortcuts/im/operations/shared.js +10 -0
  100. package/dist/shortcuts/im/operations/types.js +13 -0
  101. package/dist/shortcuts/index.js +5 -0
  102. package/dist/shortcuts/mail/host.js +4 -1
  103. package/dist/shortcuts/meeting/host.js +27 -34
  104. package/dist/shortcuts/meeting/operations/create.js +4 -1
  105. package/dist/shortcuts/meeting/operations/eb/catalog.js +5 -0
  106. package/dist/shortcuts/meeting/operations/eb/conflict.js +78 -7
  107. package/dist/shortcuts/meeting/operations/eb/create-flow.js +168 -26
  108. package/dist/shortcuts/meeting/operations/eb/factory.js +69 -1
  109. package/dist/shortcuts/meeting/operations/eb/index.js +16 -3
  110. package/dist/shortcuts/meeting/operations/eb/room-recommend.js +14 -3
  111. package/dist/shortcuts/meeting/operations/eb-fallback.js +159 -0
  112. package/dist/shortcuts/meeting/operations/env.js +93 -46
  113. package/dist/shortcuts/meeting/operations/form-fields.js +110 -0
  114. package/dist/shortcuts/meeting/operations/registry.js +2 -0
  115. package/dist/shortcuts/meeting/operations/standard-guard.js +71 -0
  116. package/dist/shortcuts/meeting/operations.js +16 -0
  117. package/dist/shortcuts/workflow/index.js +110 -1
  118. package/dist/shortcuts/workflow/operations/form.js +24 -3
  119. package/dist/shortcuts/workflow/operations/read.js +104 -23
  120. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupRules.js +448 -0
  121. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupStore.js +106 -0
  122. package/dist/shortcuts/workflow/operations/todoSmartConvert/businessRules.js +47 -0
  123. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +30 -23
  124. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.core.js +1 -0
  125. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.group.js +1 -0
  126. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +2 -1
  127. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +2 -4
  128. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +138 -181
  129. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +12 -7
  130. package/docs/e10-auth.md +1 -1
  131. package/docs/im.md +8 -1
  132. package/docs/todo-smart-convert/README.md +1 -1
  133. package/docs/todo-smart-convert/data-contract.md +2 -2
  134. package/docs/todo-smart-convert/subgroup-rules.md +6 -0
  135. package/package.json +1 -1
  136. package/skill-template/domains/yimiaoban.md +3 -3
  137. package/skill-template/skill-template.md +1 -1
  138. package/skills/weaver-e10-blog/SKILL.md +5 -5
  139. package/skills/weaver-e10-caizhilian/SKILL.md +5 -5
  140. package/skills/weaver-e10-calendar/SKILL.md +5 -5
  141. package/skills/weaver-e10-esb/SKILL.md +5 -5
  142. package/skills/weaver-e10-hrm/SKILL.md +5 -5
  143. package/skills/weaver-e10-jinchengda/SKILL.md +2 -2
  144. package/skills/weaver-e10-jiuchuanhui/SKILL.md +5 -5
  145. package/skills/weaver-e10-jucailin/SKILL.md +18 -12
  146. package/skills/weaver-e10-jucailin/references/recruit-create.md +104 -0
  147. package/skills/weaver-e10-mail/SKILL.md +5 -5
  148. package/skills/weaver-e10-meeting/SKILL.md +171 -25
  149. package/skills/weaver-e10-meeting/references/meeting-eb.md +4 -3
  150. package/skills/weaver-e10-meeting/references/meeting-queries.md +9 -6
  151. package/skills/weaver-e10-meeting/references/meeting-verification.md +8 -6
  152. package/skills/weaver-e10-meeting.zip +0 -0
  153. package/skills/weaver-e10-okr/SKILL.md +5 -5
  154. package/skills/weaver-e10-okr/references/source-manifest.json +3 -3
  155. package/skills/weaver-e10-plan/SKILL.md +5 -5
  156. package/skills/weaver-e10-shared/SKILL.md +12 -12
  157. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +4 -4
  158. package/skills/weaver-e10-wenshuding/SKILL.md +5 -5
  159. package/skills/weaver-e10-workflow/SKILL.md +29 -5
  160. package/skills/weaver-e10-workflow/aigroup/default.json +88 -0
  161. package/skills/weaver-e10-workflow/references/todo-smart-convert.md +66 -14
  162. package/skills/weaver-e10-workflow/references/workflow-agent-entry.md +1 -1
  163. package/skills/weaver-e10-workflow/references/workflow-memory.md +1 -1
  164. package/skills/weaver-e10-workflow/references/workflow-query.md +12 -10
  165. package/skills/weaver-e10-workflow/references/workflow-view.md +2 -2
  166. package/skills/weaver-e10-workflow/tools/todo-serve.cjs +239 -0
  167. package/skills/weaver-e10-workflow/{scripts → tools}/todo-summary.cjs +218 -28
  168. package/skills/weaver-e10-yepiaotong/SKILL.md +10 -8
  169. package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +7 -7
  170. package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +10 -10
  171. package/skills/weaver-e10-yepiaotong/references/reim-examples.md +40 -37
  172. package/skills/weaver-e10-yepiaotong/references/reim-expense-subject-rules.md +4 -4
  173. package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +27 -38
  174. package/skills/weaver-e10-yepiaotong/references/reim-phone-subsidy-rules.md +4 -4
  175. package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +37 -29
  176. package/skills/weaver-e10-yimiaoban/SKILL.md +156 -33
  177. package/skills/weaver-e10-yimiaoban/references/ding-write.md +1 -1
  178. package/skills/weaver-e10-yimiaoban/references/error-codes.md +5 -1
  179. package/skills/weaver-e10-yimiaoban/references/group-read.md +5 -1
  180. package/skills/weaver-e10-yimiaoban/references/group-write.md +6 -4
  181. package/skills/weaver-e10-yimiaoban/references/msg-read.md +1 -0
  182. package/skills/weaver-e10-yimiaoban/references/msg-write.md +34 -2
  183. package/skills/weaver-e10-yimiaoban/references/safety-boundaries.md +5 -2
  184. package/skills/weaver-e10-yimiaoban/references/source-manifest.json +79 -58
  185. package/skills/weaver-e10-yimiaoban/references/user-remind.md +2 -2
  186. package/skills/weaver-e10-ziguanjia/SKILL.md +5 -5
@@ -1,12 +1,12 @@
1
+ import { activeRuleFor } from './aigroupRules.js';
1
2
  import { WORKFLOW_ENDPOINTS } from '../../endpoints.js';
2
3
  import { saveSearchIndex } from '../../state.js';
3
4
  import { buildSearchQuery } from '../read.js';
4
5
  import { projectDisplayRows } from './todoListRows.js';
5
6
  import { responseData } from '../shared.js';
6
7
  import { openSummaryCache, SUMMARY_CACHE_FILE_NAME } from './summaryCache.js';
7
- import { detectBusinessKind } from './businessRules.js';
8
8
  import { fetchDetailFacts, stampsOfRows } from './todoSmartConvert.detail.js';
9
- import { focusAppliesToKind, GROUP_MIN_ITEMS_FOR_SUBGROUP, isExpiringTodayRow, isOverdueRow, runGroup, } from './todoSmartConvert.group.js';
9
+ import { GROUP_MIN_ITEMS_FOR_SUBGROUP, isExpiringTodayRow, isOverdueRow, runGroup, } from './todoSmartConvert.group.js';
10
10
  import { DEFAULT_TABID, FETCH_PAGE_SIZE, LIST_ENDPOINT_BY_CATEGORY, MAX_FETCH_LIMIT, MAX_FETCH_PAGES, guardListOnlyHost, readBoundedInt, readCategory, readFetchLimit, readPageSize, rowFromAny, } from './todoSmartConvert.core.js';
11
11
  const DEFAULT_REQUEST_SUMMARY_MAX = 50;
12
12
  export async function collectRawTodos(args, env) {
@@ -170,7 +170,7 @@ export async function collectRawTodos(args, env) {
170
170
  const fieldsById = new Map();
171
171
  if (focusText) {
172
172
  const targetRows = wfIdGroup
173
- .filter((group) => !group.topPriority && focusAppliesToKind(focusText, detectBusinessKind(group.title)))
173
+ .filter((group) => !group.topPriority && activeRuleFor(group.title)?.aiGroup?.groupRule.aiContext === '流程详情')
174
174
  .flatMap((group) => group.requestDatas ?? []);
175
175
  const targetIds = [...new Set(targetRows.map((row) => row.requestId).filter(Boolean))];
176
176
  if (targetIds.length > 0) {
@@ -189,26 +189,33 @@ export async function collectRawTodos(args, env) {
189
189
  }
190
190
  }
191
191
  // 给模型的紧凑视图使用组内序号,aiGroups 可用 seq 引用。
192
- groupingBrief = wfIdGroup.map((group) => ({
193
- groupId: group.id,
194
- title: group.title,
195
- count: group.count,
196
- topPriority: group.topPriority,
197
- items: (group.requestDatas ?? []).map((row, index) => ({
198
- idx: index + 1,
199
- requestId: row.requestId,
200
- title: row.requestNameTitle ?? '',
201
- // 客户问题支持类无侧重点时按这两个布尔分组(模型不用自己解析日期文案)
202
- overdue: isOverdueRow(row),
203
- expiringToday: isExpiringTodayRow(row),
204
- ...(row.userOverTime ? { userOverTime: row.userOverTime } : {}),
205
- ...(row.summary ? { summary: row.summary } : {}),
206
- // `summaryCached:true` = 这条已有本地摘要(按 requestId__lastFeedBackDateTime 命中),
207
- // 分组仍要做(聚类看标题),但**不要再给它写摘要**。
208
- ...(cachedSummaryIds.has(row.requestId) ? { summaryCached: true } : {}),
209
- ...(fieldsById.has(row.requestId) ? { fields: fieldsById.get(row.requestId) } : {}),
210
- })),
211
- }));
192
+ groupingBrief = wfIdGroup.map((group) => {
193
+ const groupRule = activeRuleFor(group.title)?.aiGroup?.groupRule;
194
+ // 分组口径随素材一起下发:模型生成 aiGroups 时必须遵循(只有 focus=false 的语义分组才有意义)
195
+ const groupingPrompt = groupRule && groupRule.focus === false
196
+ && typeof groupRule.groupingPrompt === 'string' ? groupRule.groupingPrompt.trim() : '';
197
+ return {
198
+ groupId: group.id,
199
+ title: group.title,
200
+ count: group.count,
201
+ topPriority: group.topPriority,
202
+ ...(groupingPrompt ? { groupingPrompt } : {}),
203
+ items: (group.requestDatas ?? []).map((row, index) => ({
204
+ idx: index + 1,
205
+ requestId: row.requestId,
206
+ title: row.requestNameTitle ?? '',
207
+ // 客户问题支持类无侧重点时按这两个布尔分组(模型不用自己解析日期文案)
208
+ overdue: isOverdueRow(row),
209
+ expiringToday: isExpiringTodayRow(row),
210
+ ...(row.userOverTime ? { userOverTime: row.userOverTime } : {}),
211
+ ...(row.summary ? { summary: row.summary } : {}),
212
+ // `summaryCached:true` = 这条已有本地摘要(按 requestId__lastFeedBackDateTime 命中),
213
+ // 分组仍要做(聚类看标题),但**不要再给它写摘要**。
214
+ ...(cachedSummaryIds.has(row.requestId) ? { summaryCached: true } : {}),
215
+ ...(fieldsById.has(row.requestId) ? { fields: fieldsById.get(row.requestId) } : {}),
216
+ })),
217
+ };
218
+ });
212
219
  // 用户侧重点原样回显:分组/排序都按「标题语义 + 侧重点」,模型照它决定子组与顺序。
213
220
  const groupingFocus = typeof args.groupFocus === 'string' ? args.groupFocus.trim() : '';
214
221
  groupInfo = {
@@ -331,6 +331,7 @@ export function rowFromAny(raw) {
331
331
  lastFeedBackDateTime: pick([
332
332
  'lastFeedBackDateTime', 'lastfeedbackdatetime', 'lastFeedbackDateTime', 'lastfeedbacktime', 'lastFeedbackTime',
333
333
  ]) ?? '',
334
+ ...(pick(['planFinishDatetime', 'planfinishdatetime']) ? { planFinishDatetime: pick(['planFinishDatetime', 'planfinishdatetime']) } : {}),
334
335
  summary: pick(['summary']),
335
336
  workflowName: pick(['workflowName', 'workflowname', 'workflowTypeName'], true),
336
337
  receivedTime: pick(['receivedTime', 'receivedtime', 'receivedatetime', 'operatedatetime', 'stayStartAt']),
@@ -196,6 +196,7 @@ export function toRequestData(row) {
196
196
  creater: row.creater,
197
197
  createrName: row.createrName,
198
198
  lastFeedBackDateTime: row.lastFeedBackDateTime,
199
+ ...(row.planFinishDatetime ? { planFinishDatetime: row.planFinishDatetime } : {}),
199
200
  summary: row.summary ?? '',
200
201
  };
201
202
  }
@@ -1,3 +1,4 @@
1
+ import { activeRuleFor } from './aigroupRules.js';
1
2
  import { workflowValidationError } from '../../errors.js';
2
3
  import { asRecord, readBoundedInt } from './todoSmartConvert.core.js';
3
4
  import { detectBusinessKind, isDeterministicGroupKind, GROUP_SUMMARY_TAIL_TEXT, } from './businessRules.js';
@@ -191,7 +192,7 @@ export async function runGroupSummaryContext(args, env) {
191
192
  }
192
193
  const deterministicGroups = groups
193
194
  .filter((group) => !group.topPriority
194
- && isDeterministicGroupKind(detectBusinessKind(group.title)))
195
+ && activeRuleFor(group.title)?.aiGroup?.groupSummary.needAI === false)
195
196
  .reduce((sum, group) => sum + groupItemsOf(group).length, 0);
196
197
  return {
197
198
  data: {
@@ -25,7 +25,7 @@ const inputSchema = schemaOf({
25
25
  },
26
26
  aiGroups: {
27
27
  type: 'array',
28
- description: 'smartGroup 的二次分组计划。每项必须提供 groupId、title,以及 requestIds 与 seq 二选一。',
28
+ description: 'smartGroup 的二次分组计划。每项必须提供 groupId、title、requestIds。',
29
29
  items: {
30
30
  type: 'object',
31
31
  additionalProperties: false,
@@ -33,10 +33,8 @@ const inputSchema = schemaOf({
33
33
  groupId: { type: 'string' },
34
34
  title: { type: 'string' },
35
35
  requestIds: { type: 'array', minItems: 1, items: { type: 'string' } },
36
- seq: { type: 'array', minItems: 1, items: { type: 'integer', minimum: 1 } },
37
36
  },
38
- required: ['groupId', 'title'],
39
- oneOf: [{ required: ['requestIds'] }, { required: ['seq'] }],
37
+ required: ['groupId', 'title', 'requestIds'],
40
38
  },
41
39
  },
42
40
  aiSummaries: {
@@ -1,8 +1,9 @@
1
1
  import { workflowValidationError } from '../../errors.js';
2
- import { BUSINESS_KIND_LABEL, detectBusinessKind, FINANCE_AMOUNT_THRESHOLD, FINANCE_GROUP_TITLE, formatFinanceGroupSummary, formatIssueSupportGroupSummary, formatLeaveGroupSummary, isDeterministicGroupKind, ISSUE_SUPPORT_GROUP_TITLE, LEAVE_DAY_THRESHOLD, LEAVE_GROUP_TITLE, normalizeCreators, } from './businessRules.js';
2
+ import { renderGroupSummaryTemplate, } from './businessRules.js';
3
3
  import { asRecord, readBoundedInt, } from './todoSmartConvert.core.js';
4
- import { focusAppliesToKind, isExpiringTodayRow, isExpiredUserOverTime, 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, } from './todoSmartConvert.group.js';
5
5
  import { fetchDetailFacts, stampsOfRows } from './todoSmartConvert.detail.js';
6
+ import { activeRuleFor, evaluateFocusKeywords, primeRules } from './aigroupRules.js';
6
7
  import { openSummaryCache } from './summaryCache.js';
7
8
  const DEFAULT_REQUEST_SUMMARY_MAX = 50;
8
9
  /** 把一批条目包成「单独展示」项(优先组、以及普通组里没同类可合并的条目都用它)。 */
@@ -38,9 +39,9 @@ export function readAiGroups(value) {
38
39
  if (!record) {
39
40
  throw workflowValidationError('ai_groups_invalid', `${label} 必须是对象 { groupId, title, requestIds }`, { retryAllowed: false });
40
41
  }
41
- const unknownKeys = Object.keys(record).filter((key) => !['groupId', 'title', 'requestIds', 'seq'].includes(key));
42
+ const unknownKeys = Object.keys(record).filter((key) => !['groupId', 'title', 'requestIds'].includes(key));
42
43
  if (unknownKeys.length > 0) {
43
- throw workflowValidationError('ai_groups_invalid', `${label} 不接受字段:${unknownKeys.join(', ')};只支持 groupId / title / requestIds / seq`, { retryAllowed: false, details: { unknownKeys } });
44
+ throw workflowValidationError('ai_groups_invalid', `${label} 不接受字段:${unknownKeys.join(', ')};只支持 groupId / title / requestIds`, { retryAllowed: false, details: { unknownKeys } });
44
45
  }
45
46
  const groupId = typeof record.groupId === 'string' ? record.groupId.trim() : '';
46
47
  if (!groupId) {
@@ -51,25 +52,11 @@ export function readAiGroups(value) {
51
52
  throw workflowValidationError('ai_groups_invalid', `${label}.title 必填:这是 AI 给该子分组起的名字,结构里要原样输出`, { retryAllowed: false });
52
53
  }
53
54
  const hasIds = Array.isArray(record.requestIds) && record.requestIds.length > 0;
54
- const hasSeq = Array.isArray(record.seq) && record.seq.length > 0;
55
- if (hasIds && hasSeq) {
56
- throw workflowValidationError('ai_groups_invalid', `${label} 不能同时给 requestIds 与 seq:用 seq(组内序号,推荐)或 requestIds(显式 ID)二选一`, { retryAllowed: false });
57
- }
58
- if (!hasIds && !hasSeq) {
59
- throw workflowValidationError('ai_groups_invalid', `${label} 需要 requestIds 或 seq(组内 1-based 序号,见 collect 的 groupingBrief)`, { retryAllowed: false });
60
- }
61
- const seqNumbers = [];
62
- if (hasSeq) {
63
- for (const item of record.seq) {
64
- const n = typeof item === 'number' ? item : Number(item);
65
- if (!Number.isInteger(n) || n < 1) {
66
- throw workflowValidationError('ai_groups_invalid', `${label}.seq 只接受 ≥1 的整数序号,收到 ${String(item)}`, { retryAllowed: false });
67
- }
68
- seqNumbers.push(n);
69
- }
55
+ if (!hasIds) {
56
+ throw workflowValidationError('ai_groups_invalid', `${label} 必须给 requestIds(非空的 requestId 字符串数组)`, { retryAllowed: false });
70
57
  }
71
58
  const ids = [];
72
- for (const item of (hasIds ? record.requestIds : [])) {
59
+ for (const item of record.requestIds) {
73
60
  const id = typeof item === 'string' ? item.trim() : typeof item === 'number' ? String(item) : '';
74
61
  if (!id) {
75
62
  throw workflowValidationError('ai_groups_invalid', `${label}.requestIds 只接受非空的 requestId 字符串`, { retryAllowed: false });
@@ -77,7 +64,7 @@ export function readAiGroups(value) {
77
64
  ids.push(id);
78
65
  }
79
66
  const bucket = map.get(groupId) ?? [];
80
- bucket.push({ groupId, title, requestIds: [...new Set(ids)], seq: [...new Set(seqNumbers)] });
67
+ bucket.push({ groupId, title, requestIds: [...new Set(ids)] });
81
68
  map.set(groupId, bucket);
82
69
  });
83
70
  return map;
@@ -109,42 +96,32 @@ function rowMetricsOf(row, metrics) {
109
96
  * 金额算不出来时(表单没有金额字段、标题里也没有 `X元`)退回「标题清单」模板,
110
97
  * 而不是编一个 `¥0 ~ ¥0` —— 半截统计比不给更糟。
111
98
  */
112
- function deterministicGroupSummary(kind, rows, metrics, restCount = 0) {
113
- if (!isDeterministicGroupKind(kind))
114
- return null;
115
- const creators = normalizeCreators(rows.map((row) => row.createrName));
116
- // 客户问题支持类:固定「N 条已过期或今日到期 + 其余 M 条单条」口径,条数由 CLI 算。
117
- if (kind === 'issue-support') {
118
- return formatIssueSupportGroupSummary({ count: rows.length, restCount, creators });
99
+ /**
100
+ * 取一条流程在某个「指标名」上的值(规则 `focusKeyword` 求值用)。
101
+ *
102
+ * - `sysArg=true`:指标名就是 `getTodoListData` 返回的属性名(如 `planFinishDatetime`);
103
+ * - `sysArg=false`:先映射到结构化指标(金额 / 天数 / 假别),再退到「详情字段名包含指标名」;
104
+ * 都取不到返回 `undefined` —— 调用方按「不命中 + warning」处理(2026-10-28 用户定:
105
+ * 正则提不到就平铺并告警,不做 AI 兜底)。
106
+ */
107
+ function metricValueOf(row, metric, sysArg, facts, metrics) {
108
+ if (sysArg) {
109
+ const key = Object.keys(row).find((item) => item.toLowerCase() === metric.toLowerCase());
110
+ return key ? row[key] : undefined;
119
111
  }
120
- if (kind === 'leave') {
121
- const items = rows.map((row) => rowMetricsOf(row, metrics));
122
- return formatLeaveGroupSummary({
123
- count: rows.length,
124
- leaveTypes: items.map((item) => item.leaveType),
125
- creators,
126
- });
112
+ const rowMetrics = metrics?.get(row.requestId);
113
+ if (rowMetrics) {
114
+ if (/金额|费用|价|合计/u.test(metric) && typeof rowMetrics.amount === 'number')
115
+ return rowMetrics.amount;
116
+ if (/天数|时长/u.test(metric) && typeof rowMetrics.leaveDays === 'number')
117
+ return rowMetrics.leaveDays;
118
+ if (/假别|假种|假期/u.test(metric) && typeof rowMetrics.leaveType === 'string')
119
+ return rowMetrics.leaveType;
127
120
  }
128
- if (kind === 'finance') {
129
- const items = rows.map((row) => rowMetricsOf(row, metrics));
130
- const amounts = items
131
- .map((item) => item.amount)
132
- .filter((value) => typeof value === 'number' && value > 0);
133
- // 防御:能进阈值组的前提就是「金额 ≤100」,所以这里必然有金额;
134
- // 万一真的一个都没有,留空交 AI 写,绝不编一个 ¥0 ~ ¥0。
135
- if (amounts.length === 0)
136
- return null;
137
- const sum = Number(amounts.reduce((total, value) => total + value, 0).toFixed(2));
138
- return formatFinanceGroupSummary({
139
- min: Math.min(...amounts),
140
- max: Math.max(...amounts),
141
- sum,
142
- categories: items.map((item) => item.feeCategory),
143
- creators,
144
- });
145
- }
146
- // 其他类不再产汇总项(isDeterministicGroupKind 已排除),走到这里直接返回 null。
147
- return null;
121
+ const fact = facts?.get(row.requestId);
122
+ const field = (fact?.fields ?? []).find((item) => String(item.fieldName ?? '').includes(metric));
123
+ const value = field?.value;
124
+ return value === undefined || String(value).trim() === '' ? undefined : value;
148
125
  }
149
126
  /**
150
127
  * 最终结构里的 `totalCount`:**去重后的待办条数**(= collect 阶段取到的全部条数)。
@@ -229,6 +206,8 @@ export async function runSmartGroup(args, env, options = {}) {
229
206
  // markdown 是中间态的东西,最终结构里没有,相关提示一并丢掉,免得误导调用方。
230
207
  const warnings = (base.warnings ?? [])
231
208
  .filter((text) => !text.startsWith('markdown 只展示前'));
209
+ // aigroup 规则(default.json + user-intents.json):每个 operation 只加载一次。
210
+ warnings.push(...primeRules(env.ctx).warnings);
232
211
  if (skipDetail) {
233
212
  warnings.push('withDetailContext=false:本次未拉取任何详情——请假/财务阈值改为按标题识别、跳过整组空判定,也不产 summaryBatches');
234
213
  }
@@ -243,7 +222,7 @@ export async function runSmartGroup(args, env, options = {}) {
243
222
  topPriority: true,
244
223
  title: group.title,
245
224
  count: group.count,
246
- // 只有优先组默认展开,其余分组一律收缩。
225
+ // 大分组(含优先组)默认展开;只有汇总项(ai二次分组)收缩。
247
226
  defaultExpanded: true,
248
227
  displayItems: requestItemsOf(group.requestDatas),
249
228
  };
@@ -256,126 +235,79 @@ export async function runSmartGroup(args, env, options = {}) {
256
235
  * 注意:不存在 `other` 兜底汇总项(2026-10-26 用户定稿),漏分 / 子组解散一律落到这里。
257
236
  */
258
237
  const singles = [];
259
- const kind = detectBusinessKind(group.title);
260
- const rawPlans = plans?.get(group.id) ?? [];
261
- /** 意图是否适用于本组(Q15 专属/通用意图表):不适用 = 按「没有意图」处理。 */
262
- const groupHasFocus = hasFocus && focusAppliesToKind(groupFocus, kind);
263
- /**
264
- * aiGroups 什么时候被采纳:
265
- * - 意图适用于本组:只有本组有计划才分;没给计划 = 视为无意图,退回确定性规则(Q8)。
266
- * - 内部留言:有没有意图都采纳(无意图按流程标题分、有意图按详情+意图分,素材由 collect 准备)。
267
- * - 其余类型:意图不适用就一律忽略(Q9)。
268
- */
269
- const plansAllowed = groupHasFocus || kind === 'internal-message';
270
- const groupPlans = plansAllowed ? rawPlans : [];
271
- if (rawPlans.length > 0 && !plansAllowed) {
272
- // 没有意图时不要说成「意图不适用于本类型」——那是误导,真实原因是本类型默认不做二次分组。
273
- warnings.push(hasFocus
274
- ? `「${group.title}」意图「${groupFocus}」不适用于本类型,已忽略 aiGroups,按无意图规则处理`
275
- : `「${group.title}」${BUSINESS_KIND_LABEL[kind]}默认不按标题二次分组,已忽略 aiGroups(要按侧重点分组请在 groupFocus 里说明)`);
276
- }
277
238
  /** 该组整体平铺(不进入 ai二次分组)。 */
278
239
  const flatReturn = () => ({
279
240
  id: group.id,
280
241
  topPriority: false,
281
242
  title: group.title,
282
243
  count: group.count,
283
- defaultExpanded: false,
244
+ defaultExpanded: true,
284
245
  displayItems: requestItemsOf(group.requestDatas),
285
246
  });
286
- // D0 满意度类:不做 ai二次分组,也不与 AI 交互。
287
- if (kind === 'satisfaction') {
247
+ const rawPlans = plans?.get(group.id) ?? [];
248
+ /** 本组生效规则(`aigroup` JSON):没有 `aiGroup` ⇒ 不做二次分组。 */
249
+ const rule = activeRuleFor(group.title);
250
+ if (!rule?.aiGroup) {
288
251
  if (rawPlans.length > 0)
289
- warnings.push(`「${group.title}」满意度类不做 ai二次分组,已忽略 aiGroups`);
290
- warnings.push(`「${group.title}」属于满意度类,全部平铺单条;summary 由标题兜底(不调 AI)`);
252
+ warnings.push(`「${group.title}」未配置 aiGroup,已忽略 aiGroups`);
253
+ if (!rule?.needSummary)
254
+ warnings.push(`「${group.title}」不需要 AI 摘要,summary 用标题兜底`);
255
+ warnings.push(`「${group.title}」未配置 ai二次分组,${group.requestDatas.length} 条平铺`);
291
256
  return flatReturn();
292
257
  }
293
- // D1 硬门槛:组内 <3 条一律平铺(即使给了 aiGroups 也忽略,不与 AI 交互)。
258
+ const groupRule = rule.aiGroup.groupRule;
259
+ const focusMode = groupRule.focus === true;
260
+ const groupPlans = rawPlans;
261
+ // 全局硬门槛:组内 <3 条一律平铺(规则/计划都覆盖不了)。
294
262
  if (group.requestDatas.length < GROUP_MIN_ITEMS_FOR_SUBGROUP) {
295
263
  if (rawPlans.length > 0)
296
- warnings.push(`「${group.title}」不足 3 条(硬门槛),忽略 aiGroups 并平铺`);
264
+ warnings.push(`「${group.title}」不足 ${GROUP_MIN_ITEMS_FOR_SUBGROUP} 条(硬门槛),忽略 aiGroups 并平铺`);
297
265
  return flatReturn();
298
266
  }
299
- // D2 意图适用但没给本组计划 = 视为没有意图(Q8:按阈值组/过期组等确定性规则)。
300
- // 但**内部留言例外**:它 ≥3 条时没有「平铺」这个第三态(见下面的强制校验)。
301
- if (groupHasFocus && groupPlans.length === 0 && kind !== 'internal-message') {
302
- warnings.push(`「${group.title}」意图适用但模型未给本组计划,按无意图处理`);
303
- }
304
- /**
305
- * 内部留言(Q1/Q16,2026-10-27 用户明确):**≥3 条必然做 ai二次分组**,没有「平铺」这个第三态。
306
- *
307
- * 只有两种维度,且都必须由模型给出计划(CLI 不替模型分组):
308
- * - 侧重点适用于内部留言 ⇒ 按「侧重点 + 流程详情数据」分组;
309
- * - 没有侧重点,或侧重点不适用 ⇒ **按流程标题分组**。
310
- *
311
- * 「没给计划」不是合法输入:静默平铺会把 200+ 条留言堆成一片(五年那批实测 223 条全平铺),
312
- * 而且 warning 会被误读成「技术上无法分组」。这里直接要求补计划,保持规则单一确定性。
313
- *
314
- * 注意:门槛在前面 D1 已经判过(<3 条直接平铺),所以走到这里一定 ≥3 条。
315
- */
316
- if (kind === 'internal-message' && groupPlans.length === 0) {
317
- throw workflowValidationError('ai_groups_required', `「${group.title}」属于内部留言且不少于 ${GROUP_MIN_ITEMS_FOR_SUBGROUP} 条,必须给本组 aiGroups:`
318
- + (groupHasFocus
319
- ? `用户侧重点「${groupFocus}」适用于内部留言,请按侧重点结合流程详情数据分组`
320
- : '当前没有适用于内部留言的侧重点,请**按流程标题**分组(同主题的通知/公告归到同一子组)')
321
- + `;可用 groupId:${group.id},组内序号 1..${group.requestDatas.length}`, {
267
+ /** `focus:false` 必须给计划(没有「平铺」这个第三态);`focus:true` 没计划时按 focusKeyword 确定性成组。 */
268
+ if (!focusMode && groupPlans.length === 0) {
269
+ const groupingPrompt = typeof groupRule.groupingPrompt === 'string' ? groupRule.groupingPrompt.trim() : '';
270
+ throw workflowValidationError('ai_groups_required', `「${group.title}」按规则要求做 AI 二次分组(focus=false,上下文:${groupRule.aiContext ?? '流程标题'}),`
271
+ + `必须给本组 aiGroups;可用 groupId:${group.id},requestIds 取本组 ${group.requestDatas.length} 条`
272
+ + (groupingPrompt ? `。分组口径(groupingPrompt):${groupingPrompt}` : ''), {
322
273
  retryAllowed: true,
323
- details: { groupId: group.id, count: group.requestDatas.length, groupFocus: groupHasFocus ? groupFocus : '' },
274
+ details: {
275
+ groupId: group.id,
276
+ count: group.requestDatas.length,
277
+ aiContext: groupRule.aiContext ?? '',
278
+ ...(groupingPrompt ? { groupingPrompt } : {}),
279
+ },
324
280
  });
325
281
  }
326
282
  if (groupPlans.length === 0) {
327
- // 无意图(或退回无意图):只有四类确定性规则,其余一律平铺。
328
- if (kind === 'leave' || kind === 'finance') {
329
- // 请假/财务:最多一个固定组;指标**只来自流程详情数据**,取不到的条目单独平铺(Q3/Q11)。
330
- const eligible = [];
331
- let unknown = 0;
332
- for (const row of group.requestDatas) {
333
- const metric = rowMetricsOf(row, metrics);
334
- const value = kind === 'leave' ? metric.leaveDays : metric.amount;
335
- const hit = kind === 'leave'
336
- ? (typeof value === 'number' && value <= LEAVE_DAY_THRESHOLD)
337
- : (typeof value === 'number' && value > 0 && value <= FINANCE_AMOUNT_THRESHOLD);
338
- if (hit)
339
- eligible.push(row);
340
- else {
341
- singles.push(row);
342
- if (typeof value !== 'number')
343
- unknown += 1;
344
- }
345
- }
346
- if (eligible.length > 0) {
347
- subGroups.push({
348
- title: kind === 'leave' ? LEAVE_GROUP_TITLE : FINANCE_GROUP_TITLE,
349
- requestDatas: eligible,
350
- });
351
- warnings.push(`「${group.title}」按${kind === 'leave' ? '请假类 1天以内' : '财务类 100元以内'}确定性汇总,其余条目平铺`);
352
- }
353
- if (unknown > 0) {
354
- warnings.push(`「${group.title}」有 ${unknown} 条流程详情数据取不到${kind === 'leave' ? '请假天数' : '报销金额'},不做 ai二次分组、按单条平铺`);
355
- }
283
+ // 规则 focus=true 且没有计划:按 focusKeyword 确定性成一组(与业务类型无关)。
284
+ const conditions = groupRule.focusKeyword ?? [];
285
+ const sysArg = groupRule.sysArg === true;
286
+ const hit = [];
287
+ const missing = new Set();
288
+ for (const row of group.requestDatas) {
289
+ const resolve = (metric) => {
290
+ const value = metricValueOf(row, metric, sysArg, facts, metrics);
291
+ if (value === undefined)
292
+ missing.add(metric);
293
+ return value;
294
+ };
295
+ if (conditions.length > 0 && evaluateFocusKeywords(conditions, resolve))
296
+ hit.push(row);
297
+ else
298
+ singles.push(row);
356
299
  }
357
- else if (kind === 'issue-support') {
358
- // 问题支持类:判据是**原始列表数据**的超时字段 userOverTime(Q4);无命中则整组平铺。
359
- const urgentRows = [];
360
- const rest = [];
361
- for (const row of group.requestDatas) {
362
- if (isExpiredUserOverTime(row.userOverTime) || isExpiringTodayRow(row))
363
- urgentRows.push(row);
364
- else
365
- rest.push(row);
366
- }
367
- singles.push(...rest);
368
- if (urgentRows.length > 0) {
369
- subGroups.push({ title: ISSUE_SUPPORT_GROUP_TITLE, requestDatas: urgentRows });
370
- warnings.push(`「${group.title}」按客户问题支持类口径:${urgentRows.length} 条已过期或今日到期,其余 ${rest.length} 条平铺`);
371
- }
372
- else {
373
- warnings.push(`「${group.title}」没有已过期或今日到期的条目(userOverTime 未命中),全部平铺`);
374
- }
300
+ const conditionText = conditions.map((condition) => condition.join(' ')).join(' 且 ');
301
+ const title = groupRule.groupTitle?.trim() || conditionText || '汇总';
302
+ if (hit.length > 0) {
303
+ subGroups.push({ title, requestDatas: hit });
304
+ warnings.push(`「${group.title}」按规则「${title}」命中 ${hit.length} 条(${conditionText}),其余 ${group.requestDatas.length - hit.length} 条平铺`);
375
305
  }
376
306
  else {
377
- singles.push(...group.requestDatas);
378
- warnings.push(`「${group.title}」默认不做 ai二次分组,${group.requestDatas.length} 条平铺`);
307
+ warnings.push(`「${group.title}」规则「${title}」命中 0 条${conditionText ? `(${conditionText})` : ''},全部平铺`);
308
+ }
309
+ if (missing.size > 0) {
310
+ warnings.push(`「${group.title}」指标取不到:${[...missing].join('、')},相关条目按单条平铺;请检查规则里的指标名`);
379
311
  }
380
312
  }
381
313
  else {
@@ -385,22 +317,12 @@ export async function runSmartGroup(args, env, options = {}) {
385
317
  * 模型若拿它当子组名,CLI 必须逐条核阈值——否则 2000 元的报销会出现在「100元以内」里。
386
318
  * 超阈值的条目剔出、转单条展示(一条都不丢),并点名。
387
319
  */
388
- const reservedTitle = kind === 'leave'
389
- ? LEAVE_GROUP_TITLE
390
- : kind === 'finance' ? FINANCE_GROUP_TITLE : '';
320
+ /** 保留标题 = 规则里的 groupTitle:计划若用它当子组名,必须逐条核 focusKeyword。 */
321
+ const reservedTitle = groupRule.groupTitle ?? '';
391
322
  const droppedByThreshold = [];
392
323
  for (const plan of groupPlans) {
393
324
  const datas = [];
394
- // `seq`(组内 1-based 序号)→ requestId:映射口径与 `groupingBrief[].items[].idx` 一致。
395
- const planRequestIds = plan.requestIds.length > 0
396
- ? plan.requestIds
397
- : plan.seq.map((n) => {
398
- const row = group.requestDatas[n - 1];
399
- if (!row) {
400
- throw workflowValidationError('ai_group_seq_out_of_range', `aiGroups 给「${group.title}」的 seq 里有 ${n},但该组只有 ${group.requestDatas.length} 条(序号从 1 开始)`, { retryAllowed: true, details: { groupId: group.id, seq: n, count: group.requestDatas.length } });
401
- }
402
- return row.requestId;
403
- });
325
+ const planRequestIds = plan.requestIds;
404
326
  for (const requestId of planRequestIds) {
405
327
  const data = byRequestId.get(requestId);
406
328
  if (!data) {
@@ -417,13 +339,11 @@ export async function runSmartGroup(args, env, options = {}) {
417
339
  if (reservedTitle && plan.title === reservedTitle) {
418
340
  const kept = [];
419
341
  const dropped = [];
342
+ const conditions = groupRule.focusKeyword ?? [];
343
+ const sysArg = groupRule.sysArg === true;
420
344
  for (const row of datas) {
421
- const metric = rowMetricsOf(row, metrics);
422
- const value = kind === 'leave' ? metric.leaveDays : metric.amount;
423
- const hit = kind === 'leave'
424
- ? (typeof value === 'number' && value <= LEAVE_DAY_THRESHOLD)
425
- : (typeof value === 'number' && value > 0 && value <= FINANCE_AMOUNT_THRESHOLD);
426
- (hit ? kept : dropped).push(row);
345
+ const resolve = (metric) => metricValueOf(row, metric, sysArg, facts, metrics);
346
+ (conditions.length > 0 && evaluateFocusKeywords(conditions, resolve) ? kept : dropped).push(row);
427
347
  }
428
348
  if (dropped.length > 0) {
429
349
  droppedByThreshold.push(...dropped);
@@ -454,10 +374,50 @@ export async function runSmartGroup(args, env, options = {}) {
454
374
  singles.push(...uncovered);
455
375
  warnings.push(`「${group.title}」有 ${uncovered.length} 条未被 AI 分组,按单条平铺(不造 other 子组)`);
456
376
  }
377
+ /**
378
+ * `focus=true` 的组**最多只留一个汇总项**(2026-10-28 用户定:侧重点产出多个子组时,
379
+ * 只保留涉及数据最多的那个,其余条目平铺——多组会造成重复展示、批量处理口径有争议)。
380
+ */
381
+ if (focusMode && subGroups.length > 1) {
382
+ const sorted = [...subGroups].sort((a, b) => b.requestDatas.length - a.requestDatas.length);
383
+ const [keep] = sorted;
384
+ const dropped = sorted.slice(1);
385
+ const droppedCount = dropped.reduce((sum, sub) => sum + sub.requestDatas.length, 0);
386
+ for (const sub of dropped)
387
+ singles.push(...sub.requestDatas);
388
+ subGroups.length = 0;
389
+ subGroups.push(keep);
390
+ warnings.push(`「${group.title}」侧重点产出 ${sorted.length} 个子组,按规则只保留条数最多的「${keep.title}」`
391
+ + `(${keep.requestDatas.length} 条),其余 ${droppedCount} 条改单条平铺`);
392
+ }
457
393
  }
394
+ /** 组摘要:规则 `needAI:false` 时用 JSON `template` 渲染,否则留空交 AI。 */
395
+ const renderSubGroupSummary = (sub) => {
396
+ const groupSummary = rule.aiGroup?.groupSummary;
397
+ if (!groupSummary || groupSummary.needAI || !groupSummary.template)
398
+ return '';
399
+ if (groupRule.groupTitle && sub.title !== groupRule.groupTitle)
400
+ return '';
401
+ const rows = sub.requestDatas;
402
+ const items = rows.map((row) => rowMetricsOf(row, metrics));
403
+ const amounts = items
404
+ .map((item) => item.amount)
405
+ .filter((value) => typeof value === 'number' && value > 0);
406
+ if (groupSummary.template.includes('{{min}}') && amounts.length === 0)
407
+ return '';
408
+ return renderGroupSummaryTemplate({
409
+ template: groupSummary.template,
410
+ tail: groupSummary.tail,
411
+ count: rows.length,
412
+ restCount: singles.length,
413
+ categories: items.map((item) => item.leaveType ?? item.feeCategory),
414
+ amounts,
415
+ creators: rows.map((row) => row.createrName),
416
+ });
417
+ };
458
418
  // 条数降序;sort 稳定,同条数保持 AI 给出的先后顺序。
459
419
  // 有侧重点时沿用模型顺序;没有侧重点时按条数降序。
460
- if (!groupHasFocus)
420
+ if (!focusMode)
461
421
  subGroups.sort((a, b) => b.requestDatas.length - a.requestDatas.length);
462
422
  const groupItems = subGroups.map((sub, index) => ({
463
423
  // 键顺序照新目标结构:type → id → title → count → summary → defaultExpanded → requestDatas
@@ -466,11 +426,7 @@ export async function runSmartGroup(args, env, options = {}) {
466
426
  title: sub.title,
467
427
  count: sub.requestDatas.length,
468
428
  // 组摘要口径不变(Q4):只有 CLI 自己造的三类固定组用固定文案;AI 子组一律留空交 groupSummary。
469
- summary: ((kind === 'leave' && sub.title === LEAVE_GROUP_TITLE)
470
- || (kind === 'finance' && sub.title === FINANCE_GROUP_TITLE)
471
- || (kind === 'issue-support' && sub.title === ISSUE_SUPPORT_GROUP_TITLE))
472
- ? (deterministicGroupSummary(kind, sub.requestDatas, metrics, singles.length) ?? '')
473
- : '',
429
+ summary: renderSubGroupSummary(sub),
474
430
  // 汇总项自身默认收缩(与大分组的展开状态互相独立)
475
431
  defaultExpanded: false,
476
432
  requestDatas: sub.requestDatas,
@@ -480,7 +436,8 @@ export async function runSmartGroup(args, env, options = {}) {
480
436
  topPriority: false,
481
437
  title: group.title,
482
438
  count: group.count,
483
- defaultExpanded: false,
439
+ // 大分组默认展开(与汇总项自身的收缩状态互相独立)
440
+ defaultExpanded: true,
484
441
  // 汇总项在前、单独条目在后。
485
442
  displayItems: [...groupItems, ...requestItemsOf(singles)],
486
443
  };