weaver-work-cli 0.1.38 → 0.1.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/README.md +7 -3
  2. package/dist/chunks/{archive-PDV724YK.js → archive-JHTONRST.js} +556 -66
  3. package/dist/chunks/{archive-oa-4XE4MNXQ.js → archive-oa-FCHLCPHC.js} +2 -2
  4. package/dist/chunks/{asset-PDL2QE2N.js → asset-RLIHC7DB.js} +2 -2
  5. package/dist/chunks/asset-idle-KVOFTLCP.js +670 -0
  6. package/dist/chunks/{attachment-content-MDDGOE5E.js → attachment-content-RZYDB3MB.js} +3 -3
  7. package/dist/chunks/{blog-UYR3EUXW.js → blog-42OZTJNP.js} +2 -2
  8. package/dist/chunks/{caizhilian-LG2X5NMF.js → caizhilian-KRK547K5.js} +2 -2
  9. package/dist/chunks/{calendar-FG4BAZHE.js → calendar-RTWAMKHD.js} +2 -2
  10. package/dist/chunks/{chunk-J7USS3ED.js → chunk-4CTESSKI.js} +4 -4
  11. package/dist/chunks/{chunk-OJQUJZQF.js → chunk-4TEDNCON.js} +45 -5
  12. package/dist/chunks/chunk-57IECMWX.js +487 -0
  13. package/dist/chunks/{chunk-ELYDWBZJ.js → chunk-7BRU4EFH.js} +13 -7
  14. package/dist/chunks/{chunk-6LTFIFLM.js → chunk-7E6MUOEK.js} +2 -2
  15. package/dist/chunks/{chunk-KKRL3REV.js → chunk-7U577Q7U.js} +2 -2
  16. package/dist/chunks/{chunk-UJJD4QWF.js → chunk-CAQNBBQA.js} +2 -2
  17. package/dist/chunks/{chunk-CORFRD7Q.js → chunk-CXRQWOZ5.js} +2 -2
  18. package/dist/chunks/{chunk-GGJFMNJM.js → chunk-FLS4DPGP.js} +110 -16
  19. package/dist/chunks/{chunk-JSMFPO43.js → chunk-I566ZYNF.js} +1 -1
  20. package/dist/chunks/{chunk-6FTTSA45.js → chunk-IZSSVYAG.js} +2 -2
  21. package/dist/chunks/{chunk-XJY7G4DD.js → chunk-JWRZM2H2.js} +3 -3
  22. package/dist/chunks/{chunk-NI6GIPIM.js → chunk-LFMSDBZN.js} +529 -395
  23. package/dist/chunks/{chunk-VRYHGG6O.js → chunk-LN447PY2.js} +92 -51
  24. package/dist/chunks/{chunk-NR6RKXPA.js → chunk-NETCCARD.js} +4 -2
  25. package/dist/chunks/{chunk-Z32KWY6G.js → chunk-NJKREDA3.js} +1 -1
  26. package/dist/chunks/{chunk-FUWVHMVV.js → chunk-SPRHHSVC.js} +2 -2
  27. package/dist/chunks/{chunk-IZUDA353.js → chunk-T6EGVBKU.js} +1 -1
  28. package/dist/chunks/{chunk-DMV2TOMH.js → chunk-VNBU55SL.js} +428 -145
  29. package/dist/chunks/{chunk-UZZBA6PV.js → chunk-WFFNM65B.js} +47 -6
  30. package/dist/chunks/{chunk-EG6ZU444.js → chunk-ZUX3P6BQ.js} +10 -5
  31. package/dist/chunks/{conflict-BQZEQ4SP.js → conflict-VR55AOOO.js} +2 -2
  32. package/dist/chunks/{contract-XIRAFTEV.js → contract-HI4FNPYE.js} +2 -2
  33. package/dist/chunks/{cowork-BLG62BL2.js → cowork-HJLI353D.js} +12 -5
  34. package/dist/chunks/{ebuilder-form-NUKG5QW4.js → ebuilder-form-HTGI63UU.js} +3 -3
  35. package/dist/chunks/{ehr-2STW5ILH.js → ehr-GYXIYH3A.js} +2 -2
  36. package/dist/chunks/env-DW3G4HUO.js +19 -0
  37. package/dist/chunks/{esb-UUDQUNKY.js → esb-I5INCEFE.js} +4 -4
  38. package/dist/chunks/factory-TKXGXS6L.js +30 -0
  39. package/dist/chunks/{fna-6RCY4NU5.js → fna-K2MHTQ24.js} +2 -2
  40. package/dist/chunks/group-chat-VYBNMJLG.js +18 -0
  41. package/dist/chunks/{host-FWDOSRH5.js → host-256GVCAH.js} +3 -3
  42. package/dist/chunks/{host-EJZFROU3.js → host-D5SERMAJ.js} +3 -3
  43. package/dist/chunks/{host-RHM3NPXV.js → host-JM4YVHFM.js} +3 -3
  44. package/dist/chunks/{host-7RXEDMAH.js → host-L3VLITMT.js} +3 -3
  45. package/dist/chunks/{host-UFF33TWJ.js → host-ZUQTJYYK.js} +3 -3
  46. package/dist/chunks/{hrm-PKJ6ELSH.js → hrm-VLRBKTZB.js} +3 -3
  47. package/dist/chunks/{im-YJAXS7CO.js → im-QK3VTLXK.js} +8 -8
  48. package/dist/chunks/{invoice-HWVSXGX6.js → invoice-T5JTA32H.js} +3 -3
  49. package/dist/chunks/{jiuchuanhui-L7VKILZB.js → jiuchuanhui-P2FNEQVZ.js} +2 -2
  50. package/dist/chunks/{lazy-dispatch-NMYEIHRH.js → lazy-dispatch-XRRI46M6.js} +5 -5
  51. package/dist/chunks/{mail-B42RRU7G.js → mail-3Y7MJ6PS.js} +9 -4
  52. package/dist/chunks/{meeting-CFYVBJSS.js → meeting-M7LTXGJE.js} +11 -10
  53. package/dist/chunks/{okr-GCLF6V7W.js → okr-OBC6S67L.js} +2 -2
  54. package/dist/chunks/{plan-ZVL7GBRI.js → plan-5H6I53SK.js} +2 -2
  55. package/dist/chunks/{project-4YWAAE2K.js → project-L75QZV4S.js} +2 -2
  56. package/dist/chunks/{registry-EP4VTHZS.js → registry-KMHCPAAD.js} +9 -8
  57. package/dist/chunks/{registry-OZSMDVPP.js → registry-QGHFNUWS.js} +2 -2
  58. package/dist/chunks/{registry-FRJ6H7BT.js → registry-RXPN6BBT.js} +4 -4
  59. package/dist/chunks/{registry-SG2PGVCF.js → registry-UGRBCBHO.js} +3 -3
  60. package/dist/chunks/{room-recommend-R3JVUUAC.js → room-recommend-A3UDC3CA.js} +4 -4
  61. package/dist/chunks/{schedule-NIJBTYDO.js → schedule-5GCACCK7.js} +4 -4
  62. package/dist/chunks/{workflow-BB24DFBY.js → workflow-M5ZJ7DWF.js} +11 -8
  63. package/dist/core/output.js +16 -6
  64. package/dist/index.js +33 -28
  65. package/dist/internal/e10/auth/commands.js +109 -56
  66. package/dist/internal/e10/auth/messages.js +1 -0
  67. package/dist/internal/e10/auth/session.js +11 -1
  68. package/dist/internal/e10/auth/xiaoe.js +29 -4
  69. package/dist/internal/e10/auth-recovery.js +13 -6
  70. package/dist/internal/e10/client.js +13 -0
  71. package/dist/internal/e10/context.js +24 -3
  72. package/dist/internal/e10/request-runtime.js +12 -1
  73. package/dist/shortcuts/archive/host.js +2 -0
  74. package/dist/shortcuts/archive/manifest.js +1 -1
  75. package/dist/shortcuts/archive/operations/borrow-list.js +13 -1
  76. package/dist/shortcuts/archive/operations/flow-url.js +1 -1
  77. package/dist/shortcuts/archive/operations/prelib.js +472 -0
  78. package/dist/shortcuts/archive/operations/registry.js +2 -0
  79. package/dist/shortcuts/archive/operations/upload.js +1 -1
  80. package/dist/shortcuts/asset-idle/errors.js +44 -0
  81. package/dist/shortcuts/asset-idle/host.js +161 -0
  82. package/dist/shortcuts/asset-idle/index.js +114 -0
  83. package/dist/shortcuts/asset-idle/manifest.js +159 -0
  84. package/dist/shortcuts/asset-idle/operations/idle.js +47 -0
  85. package/dist/shortcuts/asset-idle/operations/registry.js +33 -0
  86. package/dist/shortcuts/asset-idle/operations/shared.js +142 -0
  87. package/dist/shortcuts/asset-idle/operations/types.js +9 -0
  88. package/dist/shortcuts/asset-idle/operations.js +15 -0
  89. package/dist/shortcuts/cowork/operations/item-write.js +10 -2
  90. package/dist/shortcuts/index.js +5 -0
  91. package/dist/shortcuts/mail/operations/mail-send.js +8 -2
  92. package/dist/shortcuts/meeting/operations/eb/catalog.js +5 -1
  93. package/dist/shortcuts/meeting/operations/eb/create-flow.js +381 -104
  94. package/dist/shortcuts/meeting/operations/eb/crm-search.js +87 -5
  95. package/dist/shortcuts/meeting/operations/eb/flow-memory.js +49 -0
  96. package/dist/shortcuts/meeting/operations/eb/group-chat.js +583 -0
  97. package/dist/shortcuts/meeting/operations/eb/index.js +2 -0
  98. package/dist/shortcuts/meeting/operations/eb/room-recommend.js +19 -3
  99. package/dist/shortcuts/meeting/operations/eb/room-summary.js +2 -0
  100. package/dist/shortcuts/meeting/operations/eb/usage-cache.js +99 -0
  101. package/dist/shortcuts/meeting/operations/from-group-chat.js +35 -12
  102. package/dist/shortcuts/workflow/host.js +3 -0
  103. package/dist/shortcuts/workflow/index.js +1 -1
  104. package/dist/shortcuts/workflow/operations/form.js +65 -171
  105. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupRules.js +129 -27
  106. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupStore.js +6 -0
  107. package/dist/shortcuts/workflow/operations/todoSmartConvert/summaryCache.js +60 -117
  108. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailCache.js +44 -7
  109. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailFetcher.js +37 -29
  110. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +48 -8
  111. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.core.js +1 -0
  112. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.detail.js +22 -18
  113. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.group.js +17 -9
  114. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +34 -10
  115. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +5 -2
  116. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +9 -2
  117. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +122 -20
  118. package/docs/_catalog.md +3 -0
  119. package/docs/asset-idle.md +45 -0
  120. package/docs/e10-auth.md +16 -1
  121. package/docs/todo-smart-convert/subgroup-rules.md +1 -1
  122. package/package.json +1 -1
  123. package/scripts/mcp-meeting.mjs +234 -0
  124. package/scripts/package-skill.mjs +2 -0
  125. package/skill-template/business-info.json +14 -1
  126. package/skill-template/domains/asset-idle.md +13 -0
  127. package/skill-template/domains/wenshuding.md +3 -0
  128. package/skills/weaver-e10-archive-oa/SKILL.md +1 -0
  129. package/skills/weaver-e10-archive-oa/references/my-borrow.md +49 -0
  130. package/skills/weaver-e10-asset-idle/SKILL.md +73 -0
  131. package/skills/weaver-e10-asset-idle/references/idle-list.md +62 -0
  132. package/skills/weaver-e10-asset-idle/references/idle-stats-and-charts.md +65 -0
  133. package/skills/weaver-e10-asset-idle/references/source-manifest.json +193 -0
  134. package/skills/weaver-e10-asset-idle/references/widget-style.md +205 -0
  135. package/skills/weaver-e10-caizhilian/SKILL.md +29 -11
  136. package/skills/weaver-e10-caizhilian/references/auth-and-install.md +12 -0
  137. package/skills/weaver-e10-caizhilian/references/files.md +1 -1
  138. package/skills/weaver-e10-caizhilian/references/setting-encrypt.md +1 -1
  139. package/skills/weaver-e10-caizhilian/references/source-manifest.json +45 -45
  140. package/skills/weaver-e10-cowork/SKILL.md +2 -0
  141. package/skills/weaver-e10-cowork/references/cowork-write.md +1 -1
  142. package/skills/weaver-e10-jucailin/SKILL.md +2 -0
  143. package/skills/weaver-e10-jucailin/references/recruit-read.md +2 -0
  144. package/skills/weaver-e10-jucailin/references/recruit-write.md +1 -1
  145. package/skills/weaver-e10-mail/SKILL.md +3 -0
  146. package/skills/weaver-e10-mail/references/mail-send.md +2 -0
  147. package/skills/weaver-e10-meeting/SKILL.md +88 -25
  148. package/skills/weaver-e10-meeting/references/meeting-minute-file.md +25 -11
  149. package/skills/weaver-e10-meeting.zip +0 -0
  150. package/skills/weaver-e10-shared/SKILL.md +5 -5
  151. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +3 -3
  152. package/skills/weaver-e10-wenshuding/SKILL.md +29 -6
  153. package/skills/weaver-e10-wenshuding/references/archive-borrow-list.md +14 -0
  154. package/skills/weaver-e10-wenshuding/references/archive-download.md +2 -0
  155. package/skills/weaver-e10-wenshuding/references/archive-flow.md +1 -1
  156. package/skills/weaver-e10-wenshuding/references/archive-prelib-arrange.md +76 -0
  157. package/skills/weaver-e10-wenshuding/references/archive-prelib-list.md +107 -0
  158. package/skills/weaver-e10-wenshuding/references/archive-prelib-open.md +59 -0
  159. package/skills/weaver-e10-wenshuding/references/archive-upload.md +4 -1
  160. package/skills/weaver-e10-wenshuding/references/source-manifest.json +108 -65
  161. package/skills/weaver-e10-workflow/SKILL.md +9 -2
  162. package/skills/weaver-e10-workflow/aigroup/default.json +166 -19
  163. package/skills/weaver-e10-workflow/references/todo-smart-convert.md +72 -34
  164. package/skills/weaver-e10-workflow/tools/todo-serve.cjs +113 -25
  165. package/skills/weaver-e10-workflow/tools/todo-summary.cjs +350 -50
  166. package/dist/chunks/{chunk-2LV74IWP.js → chunk-E7OK5WD5.js} +5 -5
@@ -20,7 +20,8 @@
20
20
  *
21
21
  * 归一再交给 `normalizeDetailResponse`(`read.ts`)→ `formData.fields[]` → 业务裁剪
22
22
  * (`detailFieldRules.ts`:丢掉 `主送` 这类分发字段;内部留言类只留 `留言内容/发起人/发起时间`)
23
- * → `selectSummaryFields`(业务字段筛选,剔除流程名称/发起人/时间/状态/节点等技术字段)。
23
+ * → aigroup 规则排除清单(`excludeOriginFormDataFields`,fieldName 精确匹配)
24
+ * → 字段名额(≤200 个)与单值截断(≤500 字,2026-10-30 用户定稿)。
24
25
  *
25
26
  * ★ `isReadRequest:false` 是硬要求:批量取数**不得**把用户的待办标成已读。`workflow.detail`
26
27
  * 传的是 `true`,所以它不能被这里复用。
@@ -33,16 +34,23 @@
33
34
  */
34
35
  import { WORKFLOW_ENDPOINTS } from '../../endpoints.js';
35
36
  import { extractRowMetrics } from './businessRules.js';
36
- import { dropDistributionDetailFields, isInternalMessageDetail, pickInternalMessageFieldPairs, } from './detailFieldRules.js';
37
- import { normalizeDetailResponse, selectSummaryFields } from '../read.js';
37
+ import { dropDistributionDetailFields, isInternalMessageDetail, normalizeDetailFieldName, pickInternalMessageFieldPairs, } from './detailFieldRules.js';
38
+ import { excludeFieldsFor, excludeTagOf } from './aigroupRules.js';
39
+ import { normalizeDetailResponse } from '../read.js';
38
40
  /** 默认并发上限。 */
39
41
  export const DETAIL_BATCH_CONCURRENCY = 10;
40
42
  /** 单条详情默认超时。批量取数不是「补卡片」,不套用 5s 的短超时。 */
41
43
  export const DETAIL_BATCH_TIMEOUT_MS = 10_000;
42
44
  /** 默认重试次数(仅网络/超时;业务错误与登录失效不重试)。 */
43
45
  export const DETAIL_BATCH_RETRIES = 1;
44
- /** 单条最多取多少个业务字段(与 `workflow.summary` 的默认一致)。 */
45
- export const DETAIL_BATCH_FIELD_LIMIT = 12;
46
+ /**
47
+ * 详情素材的字段裁剪口径(2026-10-30 用户定稿,取代旧 selectSummaryFields 投影):
48
+ * - 排除清单(aigroup JSON `excludeOriginFormDataFields`)按 fieldName 精确匹配剔除后,
49
+ * **最多保留 200 个字段**(取前 200 个);
50
+ * - 单个字段值超过 **500 字只保留前 500 字**。
51
+ */
52
+ export const DETAIL_MATERIAL_FIELD_LIMIT = 200;
53
+ export const DETAIL_MATERIAL_VALUE_LIMIT = 500;
46
54
  /** 常见 HTML 实体 → 字符。`“` 这类 OA 富文本里到处都是,不解码会原样带进摘要。 */
47
55
  const HTML_ENTITIES = {
48
56
  amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ',
@@ -126,41 +134,43 @@ async function fetchOneDetail(env, requestId, userId, options) {
126
134
  const normalized = normalizeDetailResponse(response);
127
135
  const base = (normalized?.basicInfo ?? {});
128
136
  const rawFields = Array.isArray(normalized?.formData?.fields) ? normalized.formData.fields : [];
137
+ // ⓪ basicInfo 里**无条件剔除** requestTitle / flowName(2026-11-01 用户定稿:给 AI 的素材与
138
+ // 本地缓存都不允许出现这两个字段)——标题在素材里出现会诱导模型"按标题写摘要"、
139
+ // 甚至产出"标题未体现"这类降级话术;标题本身已由列表行(requestNameTitle)承载,素材里不需要。
140
+ const basicInfo = Object.fromEntries(Object.entries(base).filter(([key]) => key !== 'requestTitle' && key !== 'flowName'));
129
141
  // ① 分发类字段(`主送` …)**无条件丢弃**:值是几十个部门的收件人清单,对摘要零信息量,
130
- // 还会挤占 `summaryFields` 的字段名额。必须在挑选之前丢,名额才不会被它吃掉。
142
+ // 还会撑大喂给模型的上下文。必须在排除清单之前丢(技术卫生,用户不可在 JSON 里恢复)。
131
143
  const usableFields = dropDistributionDetailFields(rawFields);
132
144
  // ② 内部留言类:只保留 留言内容 / 发起人 / 发起时间(其余是分发元数据,见 `detailFieldRules.ts`)。
133
145
  const internalMessage = isInternalMessageDetail(base.workflowName, usableFields.map((field) => field?.fieldName));
134
- // ③ 内部留言类只保留留言内容、发起人、发起时间;字段值只清洗不裁剪。
146
+ // ③ 字段值只清洗(实体解码 / 富文本残留 / 压缩空白),不在这一步截断。
135
147
  const cleanedFields = usableFields.map((field) => ({
136
148
  ...field,
137
149
  value: cleanFieldText(String(field?.value ?? '')),
138
150
  }));
139
- const picked = internalMessage
140
- ? pickInternalMessageFieldPairs(cleanedFields, { creatorName: base.creatorName, createdAt: base.createdAt })
141
- : selectSummaryFields(cleanedFields, options.fieldLimit ?? DETAIL_BATCH_FIELD_LIMIT, undefined, { fullValue: true });
142
- const summaryFields = picked
151
+ // ④ 规则排除清单(aigroup JSON `excludeOriginFormDataFields`):按 fieldName 精确匹配剔除,
152
+ // 两侧都过 `normalizeDetailFieldName`,防【】包裹 / 空白等排版差异绕过精确匹配。
153
+ const excludeFields = excludeFieldsFor(base.workflowName);
154
+ const excludeSet = new Set(excludeFields.map((name) => normalizeDetailFieldName(name)));
155
+ const picked = (internalMessage
156
+ ? pickInternalMessageFieldPairs(cleanedFields, base)
157
+ : cleanedFields.filter((field) => !excludeSet.has(normalizeDetailFieldName(field?.fieldName))))
158
+ .slice(0, DETAIL_MATERIAL_FIELD_LIMIT)
143
159
  .map((field) => ({
144
160
  fieldName: String(field?.fieldName ?? '').trim(),
145
- value: String(field?.value ?? ''),
161
+ value: String(field?.value ?? '').slice(0, DETAIL_MATERIAL_VALUE_LIMIT),
146
162
  }))
147
163
  .filter((field) => field.fieldName && field.value);
148
- // ④ 结构化指标(请假天数 / 假别 / 金额 / 费用类别):**只从流程详情数据的字段里取**,
164
+ // ⑤ 结构化指标(请假天数 / 假别 / 金额 / 费用类别):**只从流程详情数据的字段里取**,
149
165
  // 不做标题兜底(2026-10-26 用户口径:详情取不到指标就不进阈值组,标题不能当证据)。
166
+ // 在排除清单**之前**的可用字段上取——排除只管「喂模型的素材」,不影响阈值判定。
150
167
  const metrics = extractRowMetrics(usableFields);
151
168
  return {
152
169
  requestId,
153
- basicInfo: {
154
- requestTitle: String(base.requestTitle ?? ''),
155
- workflowName: String(base.workflowName ?? ''),
156
- creatorName: String(base.creatorName ?? ''),
157
- createdAt: String(base.createdAt ?? ''),
158
- currentNode: String(base.currentNode ?? ''),
159
- requestLevel: String(base.requestLevel ?? ''),
160
- flowStatus: String(base.flowStatus ?? ''),
161
- },
162
- summaryFields,
163
- contextAvailable: summaryFields.length > 0,
170
+ basicInfo,
171
+ formFields: picked,
172
+ contextAvailable: picked.length > 0,
173
+ excludeTag: excludeTagOf(excludeFields),
164
174
  metrics,
165
175
  };
166
176
  }
@@ -230,12 +240,10 @@ export async function fetchRequestDetailsBatch(env, requestIds, options = {}) {
230
240
  failures.push({ requestId, error: reason });
231
241
  details.set(requestId, {
232
242
  requestId,
233
- basicInfo: {
234
- requestTitle: '', workflowName: '', creatorName: '', createdAt: '',
235
- currentNode: '', requestLevel: '', flowStatus: '',
236
- },
237
- summaryFields: [],
243
+ basicInfo: {},
244
+ formFields: [],
238
245
  contextAvailable: false,
246
+ excludeTag: '',
239
247
  metrics: {},
240
248
  error: reason,
241
249
  });
@@ -1,10 +1,14 @@
1
- import { activeRuleFor } from './aigroupRules.js';
1
+ import { activeRuleFor, generalRules } from './aigroupRules.js';
2
+ import { writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
2
5
  import { WORKFLOW_ENDPOINTS } from '../../endpoints.js';
3
6
  import { saveSearchIndex } from '../../state.js';
4
7
  import { buildSearchQuery } from '../read.js';
5
8
  import { projectDisplayRows } from './todoListRows.js';
6
9
  import { responseData } from '../shared.js';
7
- import { openSummaryCache, SUMMARY_CACHE_FILE_NAME } from './summaryCache.js';
10
+ import { openSummaryCache, promptTagOf, SUMMARY_CACHE_FILE_NAME } from './summaryCache.js';
11
+ import { openDetailCache, detailMaterialTagOf } from './todoDetailCache.js';
8
12
  import { fetchDetailFacts, stampsOfRows } from './todoSmartConvert.detail.js';
9
13
  import { GROUP_MIN_ITEMS_FOR_SUBGROUP, isExpiringTodayRow, isOverdueRow, runGroup, } from './todoSmartConvert.group.js';
10
14
  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';
@@ -18,16 +22,26 @@ export async function collectRawTodos(args, env) {
18
22
  // 取数阶段只允许 getTodoListData,getTodo* 分页/统计接口一律拦掉。
19
23
  const listEnv = guardListOnlyHost(env);
20
24
  const searchArgs = { category };
21
- for (const key of ['tabid', 'tabName', 'subCategory', 'workflow', 'creator', 'requestName', 'receiveTime', 'requestLevel']) {
25
+ for (const key of ['tabid', 'tabName', 'subCategory', 'isremarks', 'workflow', 'creator', 'requestName', 'receiveTime', 'requestLevel']) {
22
26
  const value = args[key];
23
27
  if (value !== undefined && value !== null && value !== '')
24
28
  searchArgs[key] = value;
25
29
  }
30
+ // 通用规则(2026-11-03):默认排除抄送类身份(generalRules.identityFilter.defaultIsremarks)。
31
+ // 用户显式传 isremarks 时不覆盖;「包含抄送 / 只要抄送」等诉求由调用方按提示词显式传 --isremarks。
32
+ // 注意:warnings 在下方声明,默认值注入放到声明之后(见「通用规则默认身份过滤」块)。
26
33
  // 显式给 tabid 才走平铺路径(见 DEFAULT_TABID 注释);用户另有子分类条件时原样尊重。
27
34
  const explicitTab = ['tabid', 'tabName', 'subCategory'].some((key) => searchArgs[key] !== undefined && String(searchArgs[key]).trim() !== '');
28
35
  if (!explicitTab)
29
36
  searchArgs.tabid = DEFAULT_TABID;
30
37
  const warnings = [];
38
+ if (searchArgs.isremarks === undefined) {
39
+ const defaultIsremarks = generalRules()?.identityFilter?.defaultIsremarks?.trim();
40
+ if (defaultIsremarks) {
41
+ searchArgs.isremarks = defaultIsremarks;
42
+ warnings.push(`已按通用规则默认排除抄送类身份(isremarks=${defaultIsremarks});如需包含抄送,请在提示词中说明`);
43
+ }
44
+ }
31
45
  if (cappedFrom !== undefined) {
32
46
  // 超过上限时显式告知,避免调用方误以为已取全量。
33
47
  warnings.push(`单次最多只能取 ${MAX_FETCH_LIMIT} 条(${MAX_FETCH_PAGES} 次请求 × ${FETCH_PAGE_SIZE} 条),`
@@ -49,6 +63,19 @@ export async function collectRawTodos(args, env) {
49
63
  const listPath = LIST_ENDPOINT_BY_CATEGORY[category] ?? WORKFLOW_ENDPOINTS.TODO_LIST;
50
64
  for (let pageNo = 1; pageNo <= plannedPages; pageNo += 1) {
51
65
  const { query } = await buildSearchQuery(listEnv, { ...searchArgs, current: pageNo, pageSize }, 'list');
66
+ // 2026-11-02:isremarks(备注筛选,多个数字英文逗号拼接)只由汇总链路透传,放 params 层。
67
+ // 注入点故意放在 collect 内部而非共享的 buildSearchQuery,保证 workflow.search 的代码路径零改动。
68
+ const isremarks = String(searchArgs.isremarks ?? '').trim();
69
+ if (isremarks !== '') {
70
+ query.params = { ...(query.params || {}), isremarks };
71
+ }
72
+ // 调试留痕(环境变量开启才写):把最终发往列表接口的 body 原样落盘,便于核对拼接结果。
73
+ if (process.env.WEAVER_WORK_CLI_DUMP_LIST_BODY) {
74
+ try {
75
+ writeFileSync(join(tmpdir(), `weaver-list-body-page${pageNo}.json`), JSON.stringify({ path: listPath, body: query }, null, 2));
76
+ }
77
+ catch { /* 留痕失败不影响主流程 */ }
78
+ }
52
79
  const response = await listEnv.host.workflowRequest({
53
80
  method: 'POST',
54
81
  path: listPath,
@@ -144,11 +171,24 @@ export async function collectRawTodos(args, env) {
144
171
  wfIdGroup = (grouped.wfIdGroup ?? []);
145
172
  // 摘要缓存 key 为 requestId 与最后反馈时间;反馈变化会自然失效。
146
173
  const summaryCache = await openSummaryCache(env.ctx);
147
- // 调用方指定摘要字数时不能命中其他字数口径的旧条目。
148
- const collectSummaryMaxChars = readBoundedInt(args.summaryMaxChars, "summaryMaxChars", DEFAULT_REQUEST_SUMMARY_MAX, 10, 500);
174
+ // 字数口径按每条所属工作流的规则算(入参 > 规则 summaryMaxChars > 兜底),
175
+ // 否则不同口径的旧摘要会被错命中/错判失效。
176
+ const collectMaxCharsByGroup = new Map();
177
+ const collectMaxCharsFor = (workflowName) => {
178
+ const key = workflowName ?? '';
179
+ const hit = collectMaxCharsByGroup.get(key);
180
+ if (hit !== undefined)
181
+ return hit;
182
+ const value = readBoundedInt(args.summaryMaxChars ?? activeRuleFor(key)?.summaryMaxChars, "summaryMaxChars", DEFAULT_REQUEST_SUMMARY_MAX, 10, 500);
183
+ collectMaxCharsByGroup.set(key, value);
184
+ return value;
185
+ };
149
186
  const cachedSummaryIds = new Set();
187
+ // 摘要条目的素材特征值:详情素材变了 ⇒ 旧摘要失效、重新走 AI(2026-11-01 根治)
188
+ const detailCacheForTag = await openDetailCache(env.ctx);
150
189
  for (const row of items) {
151
- const cached = summaryCache.get(row.requestId, row.lastFeedBackDateTime, collectSummaryMaxChars);
190
+ const detailEntry = detailCacheForTag.get(row.requestId, row.lastFeedBackDateTime);
191
+ const cached = summaryCache.get(row.requestId, row.lastFeedBackDateTime, collectMaxCharsFor(row.workflowName), promptTagOf(activeRuleFor(row.workflowName ?? '')?.requestSummaryPrompt ?? ''), detailEntry ? detailMaterialTagOf(detailEntry) : undefined);
152
192
  if (!cached)
153
193
  continue;
154
194
  row.summary = cached;
@@ -237,8 +277,8 @@ export async function collectRawTodos(args, env) {
237
277
  + `(缓存文件:${summaryCache.file ?? SUMMARY_CACHE_FILE_NAME})`);
238
278
  }
239
279
  if (!summaryCache.inSkillDir) {
240
- warnings.push('没有找到本机 Agent 的 weaver-e10-workflow skill 目录,摘要缓存已退回 CLI 状态目录;'
241
- + '如需缓存落在 skill 目录,可用 WEAVER_WORK_CLI_SKILL_DIR 指定');
280
+ warnings.push('无当前 skill 上下文(WEAVER_WORK_CLI_SKILL_DIR 未设置):摘要缓存与 aigroup 规则均未读写,'
281
+ + '本次按「从未生成过」走实时逻辑;请经 skill 的 tools 脚本调用');
242
282
  }
243
283
  }
244
284
  return {
@@ -154,6 +154,7 @@ export const ALLOWED_INPUT_KEYS = [
154
154
  'tabid',
155
155
  'tabName',
156
156
  'subCategory',
157
+ 'isremarks',
157
158
  'workflow',
158
159
  'creator',
159
160
  'requestName',
@@ -1,13 +1,19 @@
1
1
  import { fetchRequestDetailsBatch } from './todoDetailFetcher.js';
2
2
  import { readBoundedInt } from './todoSmartConvert.core.js';
3
- import { openDetailCache } from './todoDetailCache.js';
3
+ import { openDetailCache, DETAIL_CACHE_ENTRY_VERSION } from './todoDetailCache.js';
4
+ import { excludeTagFor } from './aigroupRules.js';
4
5
  /**
5
- * 把详情字段拼成喂模型的文本。
6
+ * 把「全量 basicInfo + 排除清单过滤后的 formData 字段」拼成喂模型的文本(2026-10-30 用户定稿)。
6
7
  *
7
- * 直接拼装详情取数阶段已经清洗过的字段值。
8
+ * 每行一条 `fieldName:value`——字段归属一目了然;basicInfo 的键值同样按 `key:value` 逐行放在最前。
9
+ * 字段来源与截断口径见 `todoDetailFetcher`(排除清单 → ≤200 个字段 → 单值 ≤500 字)。
8
10
  */
9
11
  export function contextOfDetail(detail) {
10
- return detail.summaryFields.map((field) => `${field.fieldName}:${field.value}`).join(';');
12
+ const basicLines = Object.entries(detail.basicInfo ?? {})
13
+ .filter(([, value]) => String(value ?? '').trim() !== '')
14
+ .map(([key, value]) => `${key}:${String(value).trim()}`);
15
+ const fieldLines = (detail.formFields ?? []).map((field) => `${field.fieldName}:${field.value}`);
16
+ return [...basicLines, ...fieldLines].join('\n');
11
17
  }
12
18
  const EMPTY_DETAIL_STATS = {
13
19
  requested: 0,
@@ -19,9 +25,8 @@ const EMPTY_DETAIL_STATS = {
19
25
  concurrency: 0,
20
26
  durationMs: 0,
21
27
  };
22
- /** 缓存里存的、也是 `DetailFact` 用的字段裁剪口径:最多 8 个、每个值 80 字。 */
23
- export const DETAIL_MATERIAL_FIELD_LIMIT = 8;
24
- export const DETAIL_MATERIAL_VALUE_LIMIT = 80;
28
+ /** 缓存里存的、也是 `DetailFact` 用的字段裁剪口径:排除清单 → ≤200 个字段、单值 ≤500 字(定义在 `todoDetailFetcher.ts`)。 */
29
+ export { DETAIL_MATERIAL_FIELD_LIMIT, DETAIL_MATERIAL_VALUE_LIMIT } from './todoDetailFetcher.js';
25
30
  /**
26
31
  * 从条目集合里建 `requestId → lastFeedBackDateTime`。
27
32
  *
@@ -44,10 +49,7 @@ export function stampsOfRows(rows) {
44
49
  return stamps;
45
50
  }
46
51
  function materialFieldsOf(detail) {
47
- return detail.summaryFields
48
- .filter((field) => field.value.length <= 200)
49
- .slice(0, DETAIL_MATERIAL_FIELD_LIMIT)
50
- .map((field) => ({ fieldName: field.fieldName, value: field.value.slice(0, DETAIL_MATERIAL_VALUE_LIMIT) }));
52
+ return detail.formFields.map((field) => ({ fieldName: field.fieldName, value: field.value }));
51
53
  }
52
54
  /**
53
55
  * 取流程详情数据:**缓存优先**(键 = `requestId__lastFeedBackDateTime`,7 天),
@@ -65,17 +67,18 @@ export async function fetchDetailFacts(env, requestIds, args, stamps) {
65
67
  const startedAt = Date.now();
66
68
  const cache = await openDetailCache(env.ctx);
67
69
  stats.cacheFile = cache.file;
68
- // ① 缓存命中:不发请求
70
+ // ① 缓存命中:不发请求。命中还要核对排除口径:JSON 里的 excludeOriginFormDataFields
71
+ // 变了 ⇒ excludeTag 对不上 ⇒ 视为 miss,按新口径重拉重写(与摘要缓存的 pk 机制同思路)。
69
72
  const missing = [];
70
73
  for (const requestId of unique) {
71
74
  const hit = cache.get(requestId, stamps?.get(requestId));
72
- if (!hit) {
75
+ if (!hit || hit.excludeTag !== excludeTagFor(hit.basicInfo.workflowName ?? '')) {
73
76
  missing.push(requestId);
74
77
  continue;
75
78
  }
76
79
  facts.set(requestId, {
77
- context: hit.context,
78
- contextAvailable: hit.contextAvailable,
80
+ context: contextOfDetail({ basicInfo: hit.basicInfo, formFields: hit.fields }),
81
+ contextAvailable: hit.fields.length > 0,
79
82
  metrics: hit.metrics,
80
83
  fields: hit.fields,
81
84
  });
@@ -94,7 +97,7 @@ export async function fetchDetailFacts(env, requestIds, args, stamps) {
94
97
  for (const [requestId, detail] of details) {
95
98
  const fact = {
96
99
  context: contextOfDetail(detail),
97
- contextAvailable: detail.summaryFields.length > 0,
100
+ contextAvailable: detail.formFields.length > 0,
98
101
  metrics: detail.metrics ?? {},
99
102
  fields: materialFieldsOf(detail),
100
103
  };
@@ -106,10 +109,11 @@ export async function fetchDetailFacts(env, requestIds, args, stamps) {
106
109
  stats.contextEmpty += 1;
107
110
  // 详情为空也写缓存:否则这类条目每次都要重拉
108
111
  if (cache.set(requestId, stamps?.get(requestId), {
109
- context: fact.context,
110
- contextAvailable: fact.contextAvailable,
112
+ v: DETAIL_CACHE_ENTRY_VERSION,
113
+ basicInfo: detail.basicInfo,
111
114
  metrics: fact.metrics,
112
115
  fields: fact.fields,
116
+ excludeTag: detail.excludeTag,
113
117
  }))
114
118
  written += 1;
115
119
  }
@@ -1,4 +1,5 @@
1
1
  import { workflowValidationError } from '../../errors.js';
2
+ import { generalRules } from './aigroupRules.js';
2
3
  import { UNKNOWN_GROUP, groupKeyOf, readGroupBy, readItems, readTopN, assertKnownKeys, } from './todoSmartConvert.core.js';
3
4
  import { intentAppliesToKind, } from './businessRules.js';
4
5
  /** 紧急程度以 requestLevel=2 或显示文本「紧急」为准;1 级是「重要」。 */
@@ -115,29 +116,32 @@ export function isExpiringTodayRow(row, now = Date.now()) {
115
116
  return isExpiringTodayUserOverTime(row.userOverTime, now);
116
117
  }
117
118
  /**
118
- * 「建议优先处理的流程」筛选规则:
119
+ * 「建议优先处理的流程」筛选规则(2026-11-03 起可在 `generalRules.priorityGroup.rules` 配置启用子集):
119
120
  *
120
- * 1. **超时** 必收(`userOverTime` / `overTime` 判定,与紧急程度无关);
121
- * 2. **紧急程度**按 `requestLevel` 取:整批有 `=2`(紧急)就只要 2;没有 2 才退到 `=1`(重要);
121
+ * 1. **超时**(`overdue`)必收(`userOverTime` / `overTime` 判定,与紧急程度无关);
122
+ * 2. **紧急程度**(`levelFallback`)按 `requestLevel` 取:整批有 `=2`(紧急)就只要 2;没有 2 才退到 `=1`(重要);
122
123
  * `=0`(正常)**不因等级入选**——降级链到 1 就停,不倒到 0;
123
124
  * 3. 整批连 1 级都没有(全 0)时,等级条件不生效,只按超时筛;此时若一条都没超时,
124
125
  * 优先组为空("实在没有紧急的就算了"),不硬凑。
125
126
  *
126
127
  * 注意这是**筛选**,不是视图叠加:入选条目的 `requestLevel` 只有 2(或 2 缺失时的 1)与超时两类。
128
+ * `enabledRules` 缺省 = 全部启用(与历史行为一致);预留 `expiringToday`(今天到期)等后续扩展。
127
129
  */
128
- export function selectPriorityRows(rows) {
130
+ export function selectPriorityRows(rows, enabledRules) {
131
+ const rules = enabledRules ?? new Set(['overdue', 'levelFallback']);
129
132
  let maxLevel = 0;
130
133
  for (const row of rows) {
131
134
  if (row.requestLevel > maxLevel)
132
135
  maxLevel = row.requestLevel;
133
136
  }
134
- const targetLevel = maxLevel >= 2 ? 2 : maxLevel >= 1 ? 1 : undefined;
137
+ const targetLevel = rules.has('levelFallback') && maxLevel >= 2 ? 2
138
+ : rules.has('levelFallback') && maxLevel >= 1 ? 1 : undefined;
135
139
  const selected = [];
136
140
  let overdueCount = 0;
137
141
  let levelCount = 0;
138
142
  let bothCount = 0;
139
143
  for (const row of rows) {
140
- const overdue = isOverdueRow(row);
144
+ const overdue = rules.has('overdue') && isOverdueRow(row);
141
145
  const byLevel = targetLevel !== undefined && row.requestLevel >= targetLevel;
142
146
  if (overdue)
143
147
  overdueCount += 1;
@@ -324,7 +328,11 @@ export function runGroup(args) {
324
328
  const groupBy = readGroupBy(args.groupBy);
325
329
  const topN = readTopN(args.topN);
326
330
  // 优先组是叠加视图:筛出的条目**不从**工作流分组剔除,两个视图各自完整。
327
- const priority = selectPriorityRows(rows);
331
+ // 2026-11-03:标题与入选规则可在 default.json 的 generalRules.priorityGroup 配置;缺省 = 内置全量规则。
332
+ const pgConfig = generalRules()?.priorityGroup ?? null;
333
+ const pgTitle = pgConfig?.title?.trim() || PRIORITY_GROUP_TITLE;
334
+ const pgRules = new Set(pgConfig?.rules ?? ['overdue', 'levelFallback']);
335
+ const priority = selectPriorityRows(rows, pgRules);
328
336
  const priorityRows = sortPriorityRows(priority.rows);
329
337
  const restRows = rows;
330
338
  const buckets = new Map();
@@ -360,7 +368,7 @@ export function runGroup(args) {
360
368
  ? [{
361
369
  id: PRIORITY_GROUP_ID,
362
370
  topPriority: true,
363
- title: PRIORITY_GROUP_TITLE,
371
+ title: pgTitle,
364
372
  count: priorityRows.length,
365
373
  requestDatas: priorityRows.map((row) => toRequestData(row)),
366
374
  stats: statsOf(priorityRows),
@@ -392,7 +400,7 @@ export function runGroup(args) {
392
400
  : priority.targetLevel === 1
393
401
  ? '重要(requestLevel=1;本次无 2 级数据,已按规则降级取 1,0 级不取)'
394
402
  : '无等级(本次整批没有 ≥1 级的条目,只按超时筛)';
395
- warnings.push(`「${PRIORITY_GROUP_TITLE}」筛出 ${priorityRows.length} 条:超时 ${priority.overdueCount} 条 / ` +
403
+ warnings.push(`「${pgTitle}」筛出 ${priorityRows.length} 条:超时 ${priority.overdueCount} 条 / ` +
396
404
  `等级命中 ${priority.levelCount} 条(两者都满足 ${priority.bothCount} 条),等级口径=${levelText};` +
397
405
  `这 ${priorityRows.length} 条同时保留在各自工作流分组中(叠加视图:Σcount 会大于 totalCount,` +
398
406
  `totalCount 按去重条数计 = ${rows.length})`);
@@ -13,6 +13,23 @@ const INTERNAL_MESSAGE_SUMMARY_HINT = '内部留言:只按原文直接概括
13
13
  const GROUP_SUMMARY_TAIL = GROUP_SUMMARY_TAIL_TEXT;
14
14
  /** 组级摘要建议长度上限。 */
15
15
  const DEFAULT_GROUP_SUMMARY_MAX = 100;
16
+ /**
17
+ * 组摘要目标字数解析器:**入参 > aigroup 规则(`groupSummaryMaxChars`)> 兜底 100**。
18
+ *
19
+ * 规则是主要来源(2026-10-30 用户要求「摘要规则必须从 JSON 取」),入参只在用户当轮明确给了
20
+ * 口径时覆盖它。按工作流名缓存,避免逐组重复解析规则。
21
+ */
22
+ function makeGroupMaxCharsResolver(args) {
23
+ const cache = new Map();
24
+ return (workflowName) => {
25
+ const hit = cache.get(workflowName);
26
+ if (hit !== undefined)
27
+ return hit;
28
+ const value = readBoundedInt(args.groupSummaryMaxChars ?? activeRuleFor(workflowName)?.groupSummaryMaxChars, 'groupSummaryMaxChars', DEFAULT_GROUP_SUMMARY_MAX, 20, 1000);
29
+ cache.set(workflowName, value);
30
+ return value;
31
+ };
32
+ }
16
33
  /** `¥123` / `¥1,280.50` / `123元` 三种写法;每条只取第一个命中,避免把「14 小时」当金额。 */
17
34
  const GROUP_AMOUNT_PATTERNS = [
18
35
  /[¥¥]\s*(\d[\d,]*(?:\.\d{1,2})?)/u,
@@ -45,7 +62,7 @@ export function createrPhraseOf(creators) {
45
62
  * `smartGroup` 已为确定性路径的二次分组汇总项写入固定 `summary`;AI 路径和内部留言仍可进入
46
63
  * `groupSummaryBatches`,由后续阶段补写组摘要。
47
64
  */
48
- export function collectGroupSummaryBatches(groups, isCtxEmpty, maxChars = DEFAULT_GROUP_SUMMARY_MAX) {
65
+ export function collectGroupSummaryBatches(groups, isCtxEmpty, maxCharsFor = () => DEFAULT_GROUP_SUMMARY_MAX) {
49
66
  const batches = [];
50
67
  for (const group of groups) {
51
68
  if (group.topPriority)
@@ -82,6 +99,8 @@ export function collectGroupSummaryBatches(groups, isCtxEmpty, maxChars = DEFAUL
82
99
  amounts.push(amount);
83
100
  }
84
101
  const amountReliable = items.length >= 2 && amounts.length === items.length;
102
+ /** 组摘要口径(规则 `groupSummary.prompt`):`needAI:true` 的组必有,没配则不下发 */
103
+ const groupPrompt = (activeRuleFor(group.title)?.aiGroup?.groupSummary?.prompt ?? '').trim();
85
104
  batches.push({
86
105
  groupId: sub.id,
87
106
  parentGroupId: group.id,
@@ -91,7 +110,8 @@ export function collectGroupSummaryBatches(groups, isCtxEmpty, maxChars = DEFAUL
91
110
  totalCount: allRows.length,
92
111
  kind,
93
112
  ...(kind === 'internal-message' ? { hint: INTERNAL_MESSAGE_SUMMARY_HINT } : {}),
94
- suggestedMaxChars: maxChars,
113
+ ...(groupPrompt ? { prompt: groupPrompt } : {}),
114
+ suggestedMaxChars: maxCharsFor(group.title),
95
115
  ...(emptySkipped > 0 ? { emptySkipped } : {}),
96
116
  items,
97
117
  stats: {
@@ -174,8 +194,9 @@ export async function runGroupSummaryContext(args, env) {
174
194
  const groups = await readFinalGroups(args, env);
175
195
  const { isCtxEmpty, warnings: emptyWarnings } = await loadCtxEmptyPredicate(env, groups, args);
176
196
  // 组摘要目标字数:用户给了就用用户的,没给才用默认 100。
177
- const groupSummaryMaxChars = readBoundedInt(args.groupSummaryMaxChars, 'groupSummaryMaxChars', DEFAULT_GROUP_SUMMARY_MAX, 20, 1000);
178
- const batches = collectGroupSummaryBatches(groups, isCtxEmpty, groupSummaryMaxChars);
197
+ const groupMaxCharsFor = makeGroupMaxCharsResolver(args);
198
+ const batches = collectGroupSummaryBatches(groups, isCtxEmpty, groupMaxCharsFor);
199
+ const distinctChars = [...new Set(batches.map((batch) => batch.suggestedMaxChars))].sort((left, right) => left - right);
179
200
  const totalCount = totalTodoCount(groups);
180
201
  const warnings = [...emptyWarnings];
181
202
  const skippedEmpty = batches.reduce((sum, batch) => sum + (batch.emptySkipped ?? 0), 0);
@@ -208,7 +229,7 @@ export async function runGroupSummaryContext(args, env) {
208
229
  /** CLI 固定文案的汇总项个数(不进批次,`smartGroup` 阶段已写好) */
209
230
  deterministicGroups,
210
231
  tail: GROUP_SUMMARY_TAIL,
211
- suggestedMaxChars: groupSummaryMaxChars,
232
+ ...(distinctChars.length === 1 ? { suggestedMaxChars: distinctChars[0] } : { suggestedMaxCharsList: distinctChars }),
212
233
  },
213
234
  },
214
235
  warnings,
@@ -259,8 +280,9 @@ export async function runGroupSummary(args, env) {
259
280
  const summaries = readAiGroupSummaries(args.aiGroupSummaries);
260
281
  const { isCtxEmpty, warnings: emptyWarnings } = await loadCtxEmptyPredicate(env, groups, args);
261
282
  // 组摘要目标字数:与 `runGroupSummaryContext` 保持同一口径(用户给了就用用户的)。
262
- const groupSummaryMaxChars = readBoundedInt(args.groupSummaryMaxChars, 'groupSummaryMaxChars', DEFAULT_GROUP_SUMMARY_MAX, 20, 1000);
263
- const batches = collectGroupSummaryBatches(groups, isCtxEmpty, groupSummaryMaxChars);
283
+ const groupMaxCharsFor = makeGroupMaxCharsResolver(args);
284
+ const batches = collectGroupSummaryBatches(groups, isCtxEmpty, groupMaxCharsFor);
285
+ const distinctChars = [...new Set(batches.map((batch) => batch.suggestedMaxChars))].sort((left, right) => left - right);
264
286
  const knownGroupIds = new Set(batches.map((batch) => batch.groupId));
265
287
  for (const groupId of summaries.keys()) {
266
288
  if (!knownGroupIds.has(groupId)) {
@@ -282,6 +304,8 @@ export async function runGroupSummary(args, env) {
282
304
  if (group.topPriority)
283
305
  continue;
284
306
  const kind = detectBusinessKind(group.title);
307
+ /** 本组的组摘要字数口径(入参 > 规则 > 兜底) */
308
+ const groupMaxChars = groupMaxCharsFor(group.title);
285
309
  for (const sub of groupItemsOf(group)) {
286
310
  if (isDeterministicGroupKind(kind)) {
287
311
  // ★ 固定文案:`smartGroup` 阶段已写好,这里**不覆盖**。
@@ -294,7 +318,7 @@ export async function runGroupSummary(args, env) {
294
318
  }
295
319
  filled += 1;
296
320
  summarizedGroupCount += 1;
297
- if (summary.length > groupSummaryMaxChars)
321
+ if (summary.length > groupMaxChars)
298
322
  tooLong += 1;
299
323
  const batch = batches.find((item) => item.groupId === sub.id);
300
324
  if (batch && batch.stats.createrPhrase && !summary.includes(batch.stats.createrPhrase)) {
@@ -314,7 +338,7 @@ export async function runGroupSummary(args, env) {
314
338
  }
315
339
  filled += 1;
316
340
  summarizedGroupCount += 1;
317
- if (summary.length > groupSummaryMaxChars)
341
+ if (summary.length > groupMaxChars)
318
342
  tooLong += 1;
319
343
  const batch = batches.find((item) => item.groupId === sub.id);
320
344
  if (batch && batch.stats.createrPhrase && !summary.includes(batch.stats.createrPhrase)) {
@@ -325,7 +349,7 @@ export async function runGroupSummary(args, env) {
325
349
  if (empty > 0)
326
350
  warnings.push(`有 ${empty} 个汇总项没有拿到组摘要,summary 保持空串(未编造)`);
327
351
  if (tooLong > 0) {
328
- warnings.push(`有 ${tooLong} 个组摘要超过建议的 ${groupSummaryMaxChars} 字,已原样保留(CLI 不改写文案)`);
352
+ warnings.push(`有 ${tooLong} 个组摘要超过建议的 ${distinctChars.length > 0 ? distinctChars.join('/') : DEFAULT_GROUP_SUMMARY_MAX} 字,已原样保留(CLI 不改写文案)`);
329
353
  }
330
354
  if (missingCreater > 0) {
331
355
  warnings.push(`有 ${missingCreater} 个组摘要没有包含算好的「由 … 发起」短语(发起人由 CLI 统计,模型不该自己数),`
@@ -80,6 +80,7 @@ const inputSchema = schemaOf({
80
80
  tabid: { type: 'string' },
81
81
  tabName: { type: 'string' },
82
82
  subCategory: { type: 'string' },
83
+ isremarks: { type: 'string', description: '备注筛选(getTodoListData params.isremarks),多个数字用英文逗号拼接,仅作用于本汇总取数' },
83
84
  workflow: { type: 'array', items: { type: 'string' } },
84
85
  creator: { type: 'string' },
85
86
  requestName: { type: 'string' },
@@ -88,8 +89,10 @@ const inputSchema = schemaOf({
88
89
  groupFocus: { type: 'string' },
89
90
  maxCharsPerBatch: { type: 'integer', minimum: 500, maximum: 200000, default: 24000 },
90
91
  maxItemsPerBatch: { type: 'integer', minimum: 1, maximum: 500, default: 100 },
91
- summaryMaxChars: { type: 'integer', minimum: 10, maximum: 500, default: 50 },
92
- groupSummaryMaxChars: { type: 'integer', minimum: 20, maximum: 1000, default: 100 },
92
+ // 字数口径不设 schema 默认:默认值来自 aigroup 规则(default.json 的 defaults),
93
+ // 入参只在用户当轮明确给口径时覆盖规则(2026-10-30 用户要求「摘要规则必须从 JSON 取」)。
94
+ summaryMaxChars: { type: 'integer', minimum: 10, maximum: 500 },
95
+ groupSummaryMaxChars: { type: 'integer', minimum: 20, maximum: 1000 },
93
96
  detailConcurrency: { type: 'integer', minimum: 1, maximum: 32, default: 10 },
94
97
  detailTimeoutMs: { type: 'integer', minimum: 1000, maximum: 60000, default: 10000 },
95
98
  skillTiming: { type: 'object' },
@@ -4,7 +4,8 @@ import { asRecord, readBoundedInt, } from './todoSmartConvert.core.js';
4
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
6
  import { activeRuleFor, evaluateFocusKeywords, primeRules } from './aigroupRules.js';
7
- import { openSummaryCache } from './summaryCache.js';
7
+ import { openSummaryCache, promptTagOf } from './summaryCache.js';
8
+ import { openDetailCache, detailMaterialTagOf } from './todoDetailCache.js';
8
9
  const DEFAULT_REQUEST_SUMMARY_MAX = 50;
9
10
  /** 把一批条目包成「单独展示」项(优先组、以及普通组里没同类可合并的条目都用它)。 */
10
11
  export function requestItemsOf(rows) {
@@ -176,12 +177,18 @@ export async function runSmartGroup(args, env, options = {}) {
176
177
  // withDetailContext=false 时只生成结构、不获取详情(此时没有指标,阈值规则自然不生效)。
177
178
  const skipDetail = args.withDetailContext === false;
178
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
+ };
179
186
  const cachedIds = new Set();
180
187
  const allRows = [];
181
188
  for (const group of groups) {
182
189
  for (const row of group.requestDatas ?? []) {
183
190
  allRows.push(row);
184
- if (summaryCache.get(row.requestId, row.lastFeedBackDateTime, summaryMaxChars))
191
+ if (summaryCache.get(row.requestId, row.lastFeedBackDateTime, summaryMaxChars, promptTagOf(activeRuleFor(group.title)?.requestSummaryPrompt ?? ''), materialTagOfRow(row.requestId, row.lastFeedBackDateTime)))
185
192
  cachedIds.add(row.requestId);
186
193
  }
187
194
  }