weaver-work-cli 0.1.40 → 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 (185) hide show
  1. package/dist/chunks/{archive-JHTONRST.js → archive-J4I6MCKF.js} +2 -2
  2. package/dist/chunks/{archive-oa-FCHLCPHC.js → archive-oa-BRZKKTO4.js} +2 -2
  3. package/dist/chunks/{asset-RLIHC7DB.js → asset-L7D4FAOZ.js} +2 -2
  4. package/dist/chunks/{asset-idle-KVOFTLCP.js → asset-idle-DADLLV75.js} +2 -2
  5. package/dist/chunks/{attachment-content-RZYDB3MB.js → attachment-content-K4C7VLWQ.js} +3 -3
  6. package/dist/chunks/{blog-42OZTJNP.js → blog-BVGQ7NZL.js} +2 -2
  7. package/dist/chunks/{caizhilian-KRK547K5.js → caizhilian-SAADNFD6.js} +2 -2
  8. package/dist/chunks/{calendar-RTWAMKHD.js → calendar-Q7NQY5Z2.js} +61 -23
  9. package/dist/chunks/{chunk-LFMSDBZN.js → chunk-3KTFS4IA.js} +308 -153
  10. package/dist/chunks/{chunk-G2JSIUWF.js → chunk-4PD5NWAX.js} +89 -0
  11. package/dist/chunks/{chunk-T6EGVBKU.js → chunk-4RAE6JXP.js} +1 -1
  12. package/dist/chunks/{chunk-7BRU4EFH.js → chunk-72CKAUAG.js} +29 -2
  13. package/dist/chunks/{chunk-NETCCARD.js → chunk-APVFJUQH.js} +2 -2
  14. package/dist/chunks/{chunk-CAQNBBQA.js → chunk-BBKDXQEC.js} +2 -2
  15. package/dist/chunks/{chunk-57IECMWX.js → chunk-BIPYLLVS.js} +26 -12
  16. package/dist/chunks/{chunk-LN447PY2.js → chunk-EKWAQSLU.js} +3 -2
  17. package/dist/chunks/{chunk-4TEDNCON.js → chunk-FOAA27U5.js} +338 -52
  18. package/dist/chunks/{chunk-7U577Q7U.js → chunk-G4UR6NXN.js} +2 -2
  19. package/dist/chunks/{chunk-NJKREDA3.js → chunk-GEWYMQME.js} +1 -1
  20. package/dist/chunks/{chunk-I566ZYNF.js → chunk-GHXCDPTM.js} +1 -1
  21. package/dist/chunks/{chunk-4CTESSKI.js → chunk-IZ7S7QUS.js} +4 -4
  22. package/dist/chunks/{chunk-VNBU55SL.js → chunk-JPRO6ER2.js} +57 -2432
  23. package/dist/chunks/{chunk-IZSSVYAG.js → chunk-LADPYOWC.js} +2 -2
  24. package/dist/chunks/chunk-LDZQPUIA.js +1238 -0
  25. package/dist/chunks/chunk-NKZRQXST.js +2546 -0
  26. package/dist/chunks/{chunk-JWRZM2H2.js → chunk-QKNKR4RH.js} +3 -3
  27. package/dist/chunks/{chunk-FLS4DPGP.js → chunk-R4CHUSKC.js} +28 -15
  28. package/dist/chunks/{chunk-WFFNM65B.js → chunk-SA6VBIHY.js} +73 -19
  29. package/dist/chunks/{chunk-SPRHHSVC.js → chunk-TTPJJJC4.js} +2 -2
  30. package/dist/chunks/{chunk-7E6MUOEK.js → chunk-W7BEHOSR.js} +2 -2
  31. package/dist/chunks/{chunk-CXRQWOZ5.js → chunk-YRSZQUD2.js} +2 -2
  32. package/dist/chunks/{conflict-VR55AOOO.js → conflict-F2OMZRTO.js} +2 -2
  33. package/dist/chunks/{contract-HI4FNPYE.js → contract-5BETHRSG.js} +2 -2
  34. package/dist/chunks/{cowork-HJLI353D.js → cowork-3MN6EQQO.js} +22 -12
  35. package/dist/chunks/create-flow-5FQNNMVO.js +55 -0
  36. package/dist/chunks/{ebuilder-form-HTGI63UU.js → ebuilder-form-SBHOYBUD.js} +3 -3
  37. package/dist/chunks/{ehr-GYXIYH3A.js → ehr-KMOJULKG.js} +432 -1234
  38. package/dist/chunks/{env-DW3G4HUO.js → env-LB7NBL2O.js} +2 -2
  39. package/dist/chunks/{esb-I5INCEFE.js → esb-W2PJCFCX.js} +4 -4
  40. package/dist/chunks/{factory-TKXGXS6L.js → factory-6ZFQXQ46.js} +1 -1
  41. package/dist/chunks/{fna-K2MHTQ24.js → fna-ZQPCUV4R.js} +2 -2
  42. package/dist/chunks/{group-chat-VYBNMJLG.js → group-chat-2HHAJN46.js} +5 -3
  43. package/dist/chunks/{host-ZUQTJYYK.js → host-4CBHGOHT.js} +3 -3
  44. package/dist/chunks/{host-D5SERMAJ.js → host-ATKVNZAT.js} +3 -3
  45. package/dist/chunks/{host-JM4YVHFM.js → host-BQH22NTQ.js} +3 -3
  46. package/dist/chunks/{host-L3VLITMT.js → host-X2CSLFM6.js} +3 -3
  47. package/dist/chunks/{host-256GVCAH.js → host-YTODKMSL.js} +3 -3
  48. package/dist/chunks/{hrm-VLRBKTZB.js → hrm-FFM4UJFS.js} +4 -4
  49. package/dist/chunks/{im-QK3VTLXK.js → im-VEHKDQPB.js} +7 -7
  50. package/dist/chunks/{invoice-T5JTA32H.js → invoice-LVIKSIIF.js} +3 -3
  51. package/dist/chunks/{jiuchuanhui-P2FNEQVZ.js → jiuchuanhui-X6DINO5P.js} +2 -2
  52. package/dist/chunks/{lazy-dispatch-XRRI46M6.js → lazy-dispatch-KSWTN5L6.js} +4 -4
  53. package/dist/chunks/{mail-3Y7MJ6PS.js → mail-VDKRGUD3.js} +2 -2
  54. package/dist/chunks/{meeting-M7LTXGJE.js → meeting-CLC6HP3Z.js} +12 -11
  55. package/dist/chunks/{okr-OBC6S67L.js → okr-CHG4OO3U.js} +2 -2
  56. package/dist/chunks/{plan-5H6I53SK.js → plan-XWR75667.js} +2 -2
  57. package/dist/chunks/{project-L75QZV4S.js → project-5BXOOKMG.js} +40 -23
  58. package/dist/chunks/recruit-stage-DGMFNNDE.js +4507 -0
  59. package/dist/chunks/{registry-RXPN6BBT.js → registry-7FFOXIAW.js} +4 -4
  60. package/dist/chunks/{registry-KMHCPAAD.js → registry-BKBZPBAJ.js} +10 -9
  61. package/dist/chunks/{registry-QGHFNUWS.js → registry-OZSMDVPP.js} +2 -2
  62. package/dist/chunks/{registry-UGRBCBHO.js → registry-RHMW6ANW.js} +2 -2
  63. package/dist/chunks/{registry-NYPYMZXG.js → registry-Z43434VG.js} +1 -1
  64. package/dist/chunks/{room-recommend-A3UDC3CA.js → room-recommend-62H7ZQQ6.js} +4 -4
  65. package/dist/chunks/{schedule-5GCACCK7.js → schedule-FNXKH3DQ.js} +4 -4
  66. package/dist/chunks/{workflow-M5ZJ7DWF.js → workflow-ADHKCAJK.js} +6 -6
  67. package/dist/index.js +33 -28
  68. package/dist/internal/e10/auth/commands.js +1 -0
  69. package/dist/internal/e10/auth/messages.js +6 -0
  70. package/dist/internal/e10/auth/session.js +17 -0
  71. package/dist/internal/e10/auth/xiaoe.js +286 -44
  72. package/dist/internal/e10/auth-recovery.js +36 -5
  73. package/dist/internal/e10/context.js +56 -12
  74. package/dist/internal/e10/index.js +1 -1
  75. package/dist/internal/e10/network-error.js +93 -0
  76. package/dist/internal/e10/request-runtime.js +6 -0
  77. package/dist/shortcuts/calendar/operations/read.js +31 -22
  78. package/dist/shortcuts/calendar/operations/write.js +45 -1
  79. package/dist/shortcuts/cowork/operations/discuss-write.js +5 -1
  80. package/dist/shortcuts/cowork/operations/item-write.js +1 -7
  81. package/dist/shortcuts/cowork/operations/member-write.js +3 -1
  82. package/dist/shortcuts/cowork/operations/permission-write.js +5 -1
  83. package/dist/shortcuts/cowork/operations/shared.js +7 -0
  84. package/dist/shortcuts/ehr/operations/attend-write.js +43 -1
  85. package/dist/shortcuts/ehr/operations/hr-contact.js +7 -4
  86. package/dist/shortcuts/ehr/operations/hr-report.js +13 -7
  87. package/dist/shortcuts/ehr/operations/recruit-candidate-import.js +134 -59
  88. package/dist/shortcuts/ehr/operations/recruit-write.js +262 -17
  89. package/dist/shortcuts/ehr/operations/recruit.js +10 -4
  90. package/dist/shortcuts/ehr/operations/shared.js +47 -0
  91. package/dist/shortcuts/hrm/operations/org.js +118 -0
  92. package/dist/shortcuts/index.js +5 -0
  93. package/dist/shortcuts/meeting/operations/eb/catalog.js +4 -1
  94. package/dist/shortcuts/meeting/operations/eb/create-flow.js +85 -28
  95. package/dist/shortcuts/meeting/operations/eb/factory.js +33 -0
  96. package/dist/shortcuts/meeting/operations/eb/group-chat.js +25 -3
  97. package/dist/shortcuts/meeting/operations/eb/person-cache.js +96 -0
  98. package/dist/shortcuts/meeting/operations/eb/room-recommend.js +33 -11
  99. package/dist/shortcuts/project/operations/read.js +6 -25
  100. package/dist/shortcuts/project/operations/shared.js +22 -0
  101. package/dist/shortcuts/project/operations/write.js +28 -2
  102. package/dist/shortcuts/recruit-stage/context.js +119 -0
  103. package/dist/shortcuts/recruit-stage/continuation.js +73 -0
  104. package/dist/shortcuts/recruit-stage/errors.js +65 -0
  105. package/dist/shortcuts/recruit-stage/host.js +109 -0
  106. package/dist/shortcuts/recruit-stage/index.js +115 -0
  107. package/dist/shortcuts/recruit-stage/manifest.js +29 -0
  108. package/dist/shortcuts/recruit-stage/operations/interview-arrange.js +722 -0
  109. package/dist/shortcuts/recruit-stage/operations/interview-feedback.js +269 -0
  110. package/dist/shortcuts/recruit-stage/operations/jd-create.js +630 -0
  111. package/dist/shortcuts/recruit-stage/operations/list.js +431 -0
  112. package/dist/shortcuts/recruit-stage/operations/registry.js +38 -0
  113. package/dist/shortcuts/recruit-stage/operations/resume-import.js +1198 -0
  114. package/dist/shortcuts/recruit-stage/operations/search.js +158 -0
  115. package/dist/shortcuts/recruit-stage/operations/types.js +25 -0
  116. package/dist/shortcuts/recruit-stage/school-tiers.json +318 -0
  117. package/dist/shortcuts/recruit-stage/shared.js +608 -0
  118. package/dist/shortcuts/workflow/operations/todoSmartConvert/detailFieldRules.js +4 -2
  119. package/dist/shortcuts/workflow/operations/todoSmartConvert/summaryCache.js +13 -6
  120. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailCache.js +17 -24
  121. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailFetcher.js +14 -9
  122. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +50 -8
  123. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.core.js +2 -1
  124. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.detail.js +17 -11
  125. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.group.js +7 -39
  126. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +3 -2
  127. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +51 -15
  128. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.pipeline.js +16 -1
  129. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +151 -63
  130. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.subGroup.js +56 -0
  131. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +39 -14
  132. package/docs/e10-auth.md +7 -1
  133. package/docs/operation-manual.md +8 -0
  134. package/package.json +1 -1
  135. package/scripts/install-skill-to-xiaoe.mjs +105 -0
  136. package/scripts/package-skill.mjs +4 -2
  137. package/skill-template/business-info.json +24 -0
  138. package/skill-template/domains/recruit-stage.md +29 -0
  139. package/skills/weaver-e10-calendar/SKILL.md +2 -1
  140. package/skills/weaver-e10-calendar/references/calendar-write.md +54 -0
  141. package/skills/weaver-e10-cowork/SKILL.md +1 -1
  142. package/skills/weaver-e10-cowork/references/cowork-write.md +3 -1
  143. package/skills/weaver-e10-jucailin/SKILL.md +1 -1
  144. package/skills/weaver-e10-jucailin/references/attend-write.md +8 -0
  145. package/skills/weaver-e10-jucailin/references/hr-contact.md +8 -0
  146. package/skills/weaver-e10-jucailin/references/hr-report.md +8 -0
  147. package/skills/weaver-e10-jucailin/references/recruit-create.md +2 -2
  148. package/skills/weaver-e10-jucailin/references/recruit-read.md +1 -1
  149. package/skills/weaver-e10-jucailin/references/recruit-write.md +4 -3
  150. package/skills/weaver-e10-mail/SKILL.md +1 -1
  151. package/skills/weaver-e10-meeting/SKILL.md +32 -2
  152. package/skills/weaver-e10-meeting.zip +0 -0
  153. package/skills/weaver-e10-recruit-stage/SKILL.md +226 -0
  154. package/skills/weaver-e10-recruit-stage/agents/openai.yaml +119 -0
  155. package/skills/weaver-e10-recruit-stage/lib/recruit-xiaoe-file.mjs +188 -0
  156. package/skills/weaver-e10-recruit-stage/references/recruit-arrange-interview.md +322 -0
  157. package/skills/weaver-e10-recruit-stage/references/recruit-capability-check.md +127 -0
  158. package/skills/weaver-e10-recruit-stage/references/recruit-check-install.md +65 -0
  159. package/skills/weaver-e10-recruit-stage/references/recruit-detail-process.md +63 -0
  160. package/skills/weaver-e10-recruit-stage/references/recruit-edit-interview-feedback.md +142 -0
  161. package/skills/weaver-e10-recruit-stage/references/recruit-jd-create.md +73 -0
  162. package/skills/weaver-e10-recruit-stage/references/recruit-jd-template.md +56 -0
  163. package/skills/weaver-e10-recruit-stage/references/recruit-list-candidate.md +101 -0
  164. package/skills/weaver-e10-recruit-stage/references/recruit-list-interview.md +148 -0
  165. package/skills/weaver-e10-recruit-stage/references/recruit-match-candidates.md +77 -0
  166. package/skills/weaver-e10-recruit-stage/references/recruit-output-links.md +59 -0
  167. package/skills/weaver-e10-recruit-stage/references/recruit-resolve-creator.md +69 -0
  168. package/skills/weaver-e10-recruit-stage/references/recruit-resolve-metadata.md +241 -0
  169. package/skills/weaver-e10-recruit-stage/references/recruit-resume-import.md +438 -0
  170. package/skills/weaver-e10-recruit-stage/references/recruit-resume-parse.md +128 -0
  171. package/skills/weaver-e10-recruit-stage/references/recruit-search-candidate.md +146 -0
  172. package/skills/weaver-e10-recruit-stage/references/recruit-urge-feedback.md +69 -0
  173. package/skills/weaver-e10-shared/SKILL.md +1 -1
  174. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +4 -2
  175. package/skills/weaver-e10-shijingran/SKILL.md +3 -2
  176. package/skills/weaver-e10-shijingran/references/operations.md +3 -1
  177. package/skills/weaver-e10-shijingran/references/source-manifest.json +41 -40
  178. package/skills/weaver-e10-shijingran/references/write-operations.md +2 -0
  179. package/skills/weaver-e10-workflow/SKILL.md +4 -2
  180. package/skills/weaver-e10-workflow/aigroup/default.json +11 -9
  181. package/skills/weaver-e10-workflow/references/todo-smart-convert.md +23 -13
  182. package/skills/weaver-e10-workflow/tools/todo-summary.cjs +62 -20
  183. package/skills/weaver-e10-yepiaotong/SKILL.md +9 -10
  184. package/skills/weaver-e10-yepiaotong/references/invoice-agent-entry.md +6 -3
  185. package/dist/chunks/{chunk-E7OK5WD5.js → chunk-2LV74IWP.js} +5 -5
@@ -5,7 +5,7 @@ import { cacheRoots, readCacheFile, writeCacheFile, SUMMARY_CACHE_PRUNE_INTERVAL
5
5
  * 规则(用户 2026-10-26 定稿;条目格式 2026-10-30 升 v2):
6
6
  * - 键:`requestId__lastFeedBackDateTime`(与该条待办的业务时间绑定——流程一动就换键);
7
7
  * - 有效期:7 天,与摘要缓存一致;读的时候逻辑过期,超过清理间隔(3 天)时物理剔除;
8
- * - **条目带格式版本 `v`**:v2 起素材 = 排除清单过滤后的 formData + 全量 basicInfo;
8
+ * - **条目带格式版本 `v`**:v4(2026-11-03)起素材 = 排除清单过滤后的 formData **+ 仅 workflowName**;
9
9
  * 旧格式条目(无 `v`)读取时视为无效,被新条目逐步覆盖;排除清单变化靠 `excludeTag` 失效;
10
10
  * - 详情为空的条目**也写**(`fields: []`),否则这类条目每次都要重拉;
11
11
  * - 缓存文件缺失 ⇒ 视为从未缓存过,正常走接口。
@@ -18,29 +18,28 @@ export const DETAIL_CACHE_TTL_MS = SUMMARY_CACHE_TTL_MS;
18
18
  /** 与摘要缓存一致:清理间隔 3 天。 */
19
19
  export const DETAIL_CACHE_PRUNE_INTERVAL_MS = SUMMARY_CACHE_PRUNE_INTERVAL_MS;
20
20
  /**
21
- * 缓存条目格式版本。v2(2026-10-30):
22
- * - 素材改为「排除清单过滤后的 formData `fieldName:value` + 全量 basicInfo」,
23
- * 取代旧版「selectSummaryFields 投影 + 8 字段/80 字截断」;
24
- * - 新增 `excludeTag`(排除清单原文拼接)——清单一变旧条目整体失效;
25
- * - v1 条目(无 `v` 键)读取时直接视为无效并逐步被覆盖。
21
+ * 缓存条目格式版本。
22
+ * - v2(2026-10-30):素材 = 排除清单过滤后的 formData + 全量 basicInfo,新增 `excludeTag`;
23
+ * - v3(2026-11-01):basicInfo 剔除 `requestTitle` / `flowName`;
24
+ * - **v4(2026-11-03,用户定)**:基础信息**只留 `workflowName`**,其余 basic 字段(creatorName /
25
+ * createdAt / currentNode / flowStatus / requestLevel …)既不入缓存也不入素材;
26
+ * `dv` 相应改为**只对 fields 哈希**。v3 及更早条目读取时视为无效、自动重拉。
26
27
  */
27
- export const DETAIL_CACHE_ENTRY_VERSION = 3;
28
+ export const DETAIL_CACHE_ENTRY_VERSION = 4;
28
29
  export function detailCacheKey(requestId, lastFeedBackDateTime) {
29
30
  const stamp = String(lastFeedBackDateTime ?? '').trim();
30
31
  return `${requestId}__${stamp}`;
31
32
  }
32
33
  /**
33
- * 详情素材特征值(2026-11-01 根治「素材升级后旧摘要继续命中」):对 `basicInfo + fields`
34
- * 的稳定序列化做 djb2 哈希。任何素材变化——详情管线升级、`excludeOriginFormDataFields`
35
- * 调整、字段增减——都会让它改变;摘要缓存条目的 `dv` 与之比对即可自动失效重写。
34
+ * 详情素材特征值 `dv`(2026-11-01 根治「素材升级后旧摘要继续命中」):对**清洗后的 fields**
35
+ * 做稳定序列化 + djb2 哈希。任何素材变化——详情管线升级、`excludeOriginFormDataFields` 调整、
36
+ * 字段增减——都会让它改变;摘要缓存条目的 `dv` 与之比对即可自动失效重写。
37
+ * (v4 起 basic 只剩 workflowName 且不进素材,故不再参与哈希。)
36
38
  */
37
39
  export function detailMaterialTagOf(entry) {
38
- const basic = entry.basicInfo ?? {};
39
- const basicKeys = Object.keys(basic).sort();
40
- const material = JSON.stringify([
41
- basicKeys.map((key) => [key, basic[key]]),
42
- (entry.fields ?? []).map((field) => [field.fieldName, field.value]),
43
- ]);
40
+ // v4 起 basic 只剩 workflowName 且不进素材 ⇒ dv 只对**清洗后的 fields** 哈希;
41
+ // workflowName 的变化不影响摘要内容,故不参与(改由 pk 侧的口径与分组处理)。
42
+ const material = JSON.stringify((entry.fields ?? []).map((field) => [field.fieldName, field.value]));
44
43
  let hash = 5381;
45
44
  for (let i = 0; i < material.length; i += 1)
46
45
  hash = ((hash << 5) + hash + material.charCodeAt(i)) >>> 0;
@@ -57,13 +56,7 @@ function normalizeEntry(raw) {
57
56
  return null;
58
57
  if (typeof value.excludeTag !== 'string')
59
58
  return null;
60
- const basicInfo = {};
61
- if (value.basicInfo && typeof value.basicInfo === 'object') {
62
- for (const [key, item] of Object.entries(value.basicInfo)) {
63
- if (typeof item === 'string')
64
- basicInfo[key] = item;
65
- }
66
- }
59
+ const workflowName = typeof value.workflowName === 'string' ? value.workflowName : '';
67
60
  const fields = Array.isArray(value.fields)
68
61
  ? value.fields
69
62
  .filter((item) => Boolean(item) && typeof item.fieldName === 'string'
@@ -73,7 +66,7 @@ function normalizeEntry(raw) {
73
66
  const metrics = value.metrics && typeof value.metrics === 'object' ? value.metrics : {};
74
67
  return {
75
68
  v: DETAIL_CACHE_ENTRY_VERSION,
76
- basicInfo,
69
+ workflowName,
77
70
  fields,
78
71
  metrics,
79
72
  excludeTag: value.excludeTag,
@@ -110,6 +110,12 @@ function isRetryable(error) {
110
110
  return true;
111
111
  return structured?.retryable === true && structured?.type !== 'authentication';
112
112
  }
113
+ /** 列表行上的发起人(**只用于内部留言的「发起人」行**,basic 已丢弃后由它兜底)。 */
114
+ function userIdToCreatorName(options, requestId) {
115
+ const name = options.rowMeta?.get(requestId)?.createrName;
116
+ const text = String(name ?? '').trim();
117
+ return text === '' ? undefined : text;
118
+ }
113
119
  /**
114
120
  * 单条详情取数。后续切换批量接口时,保持本文件对外的批量结果契约即可。
115
121
  */
@@ -134,15 +140,14 @@ async function fetchOneDetail(env, requestId, userId, options) {
134
140
  const normalized = normalizeDetailResponse(response);
135
141
  const base = (normalized?.basicInfo ?? {});
136
142
  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'));
143
+ // ⓪ 基础信息**只留 workflowName**(2026-11-03 用户定):其余 basic 字段丢弃,不进缓存也不进素材。
144
+ // `requestTitle` / `flowName` 这类标题字段自然也在丢弃之列。
145
+ const workflowName = String(base.workflowName ?? '').trim();
141
146
  // ① 分发类字段(`主送` …)**无条件丢弃**:值是几十个部门的收件人清单,对摘要零信息量,
142
147
  // 还会撑大喂给模型的上下文。必须在排除清单之前丢(技术卫生,用户不可在 JSON 里恢复)。
143
148
  const usableFields = dropDistributionDetailFields(rawFields);
144
149
  // ② 内部留言类:只保留 留言内容 / 发起人 / 发起时间(其余是分发元数据,见 `detailFieldRules.ts`)。
145
- const internalMessage = isInternalMessageDetail(base.workflowName, usableFields.map((field) => field?.fieldName));
150
+ const internalMessage = isInternalMessageDetail(workflowName, usableFields.map((field) => field?.fieldName));
146
151
  // ③ 字段值只清洗(实体解码 / 富文本残留 / 压缩空白),不在这一步截断。
147
152
  const cleanedFields = usableFields.map((field) => ({
148
153
  ...field,
@@ -150,10 +155,10 @@ async function fetchOneDetail(env, requestId, userId, options) {
150
155
  }));
151
156
  // ④ 规则排除清单(aigroup JSON `excludeOriginFormDataFields`):按 fieldName 精确匹配剔除,
152
157
  // 两侧都过 `normalizeDetailFieldName`,防【】包裹 / 空白等排版差异绕过精确匹配。
153
- const excludeFields = excludeFieldsFor(base.workflowName);
158
+ const excludeFields = excludeFieldsFor(workflowName);
154
159
  const excludeSet = new Set(excludeFields.map((name) => normalizeDetailFieldName(name)));
155
160
  const picked = (internalMessage
156
- ? pickInternalMessageFieldPairs(cleanedFields, base)
161
+ ? pickInternalMessageFieldPairs(cleanedFields, {}, userIdToCreatorName(options, requestId))
157
162
  : cleanedFields.filter((field) => !excludeSet.has(normalizeDetailFieldName(field?.fieldName))))
158
163
  .slice(0, DETAIL_MATERIAL_FIELD_LIMIT)
159
164
  .map((field) => ({
@@ -167,7 +172,7 @@ async function fetchOneDetail(env, requestId, userId, options) {
167
172
  const metrics = extractRowMetrics(usableFields);
168
173
  return {
169
174
  requestId,
170
- basicInfo,
175
+ workflowName,
171
176
  formFields: picked,
172
177
  contextAvailable: picked.length > 0,
173
178
  excludeTag: excludeTagOf(excludeFields),
@@ -240,7 +245,7 @@ export async function fetchRequestDetailsBatch(env, requestIds, options = {}) {
240
245
  failures.push({ requestId, error: reason });
241
246
  details.set(requestId, {
242
247
  requestId,
243
- basicInfo: {},
248
+ workflowName: '',
244
249
  formFields: [],
245
250
  contextAvailable: false,
246
251
  excludeTag: '',
@@ -124,6 +124,46 @@ export async function collectRawTodos(args, env) {
124
124
  if (pageRows.length < pageSize)
125
125
  break;
126
126
  }
127
+ // ① 详情回填(2026-11-03 用户定:取完列表**紧接着**拉流程详情)
128
+ // 目的:`getTodoListData.displayData` 可能没有 `workflowName`,缺了分组会整片落到 UNKNOWN_GROUP;
129
+ // 这里全量拉一遍详情(缓存优先,热缓存 0 请求),列表缺名时用详情 basic 的 workflowName 回填。
130
+ // 清洗口径照旧(分发字段 / excludeOriginFormDataFields / 200 字段 / 500 字),
131
+ // 但 basic 只保留 workflowName,其余字段丢弃(见 todoDetailFetcher + todoDetailCache v4)。
132
+ const workflowNames = new Map();
133
+ const createrNames = new Map();
134
+ for (const row of items) {
135
+ if (!row.requestId)
136
+ continue;
137
+ const name = String(row.workflowName ?? '').trim();
138
+ if (name)
139
+ workflowNames.set(row.requestId, name);
140
+ if (row.createrName)
141
+ createrNames.set(row.requestId, { createrName: row.createrName });
142
+ }
143
+ const detailBackfill = { requested: 0, cacheHit: 0, fetched: 0, failed: 0, nameFilled: 0 };
144
+ let allFacts = new Map();
145
+ if (items.length > 0) {
146
+ const detailIds = [...new Set(items.map((row) => row.requestId).filter(Boolean))];
147
+ const { facts, stats } = await fetchDetailFacts(env, detailIds, args, stampsOfRows(items), workflowNames, createrNames);
148
+ allFacts = facts;
149
+ detailBackfill.requested = stats.requested;
150
+ detailBackfill.cacheHit = stats.cacheHit;
151
+ detailBackfill.fetched = stats.fetched;
152
+ detailBackfill.failed = stats.failed;
153
+ for (const row of items) {
154
+ const fact = facts.get(row.requestId);
155
+ if (!fact)
156
+ continue;
157
+ if (!String(row.workflowName ?? '').trim() && fact.workflowName) {
158
+ row.workflowName = fact.workflowName;
159
+ workflowNames.set(row.requestId, fact.workflowName);
160
+ detailBackfill.nameFilled += 1;
161
+ }
162
+ }
163
+ if (detailBackfill.nameFilled > 0) {
164
+ warnings.push(`有 ${detailBackfill.nameFilled} 条列表数据缺少工作流名称,已用流程详情数据(basic.workflowName)回填`);
165
+ }
166
+ }
127
167
  // 刷新本地查询快照(本地写文件,不联网):批量写门禁以「最近一次查询结果」为准,
128
168
  // 不写会让 todoSmartConvert 取到的 ID 在 workflow.batch* 那里被判成「不在快照里」。
129
169
  await saveSearchIndex({
@@ -197,8 +237,9 @@ export async function collectRawTodos(args, env) {
197
237
  // 详情是流水线中间数据,smartGroup / summaryContext 需要时现场拉取(带缓存)。
198
238
  // 例外:**有意图时**,ai二次分组要先看到表单字段,这里就把素材准备好(见下)。
199
239
  detailStats = {
200
- deferred: true,
201
- note: '详情不预取:smartGroup / summaryContext 会在需要时现场拉取(详情缓存优先)',
240
+ deferred: false,
241
+ note: '详情已在 collect 阶段全量回填(缓存优先):仅 basic.workflowName 保留,其余 basic 字段与素材无关',
242
+ detailBackfill,
202
243
  summaryCached: cachedSummaryIds.size,
203
244
  summaryCacheFile: summaryCache.file,
204
245
  summaryCachePruned: summaryCache.pruned,
@@ -214,16 +255,17 @@ export async function collectRawTodos(args, env) {
214
255
  .flatMap((group) => group.requestDatas ?? []);
215
256
  const targetIds = [...new Set(targetRows.map((row) => row.requestId).filter(Boolean))];
216
257
  if (targetIds.length > 0) {
217
- const { facts, stats } = await fetchDetailFacts(env, targetIds, args, stampsOfRows(targetRows));
218
- for (const [requestId, fact] of facts) {
219
- if (fact.fields.length > 0)
258
+ // 复用 ① 已回填的 facts(不再二次取数)
259
+ for (const requestId of targetIds) {
260
+ const fact = allFacts.get(requestId);
261
+ if (fact && fact.fields.length > 0)
220
262
  fieldsById.set(requestId, fact.fields);
221
263
  }
222
264
  detailStats.groupingMaterial = {
223
265
  requested: targetIds.length,
224
- cacheHit: stats.cacheHit,
225
- fetched: stats.fetched,
226
- failed: stats.failed,
266
+ cacheHit: detailBackfill.cacheHit,
267
+ fetched: detailBackfill.fetched,
268
+ failed: detailBackfill.failed,
227
269
  withFields: fieldsById.size,
228
270
  };
229
271
  }
@@ -7,7 +7,7 @@ export const UNKNOWN_DATE = '未知日期';
7
7
  export const DEFAULT_TOP_N = 10;
8
8
  export const STAGE_VALUES = [
9
9
  'auto', 'collect', 'group', 'smartGroup',
10
- 'summaryContext', 'summary', 'groupSummaryContext', 'groupSummary',
10
+ 'summaryContext', 'summary', 'subGroup', 'groupSummaryContext', 'groupSummary',
11
11
  'aggregate',
12
12
  ];
13
13
  export function asRecord(value) {
@@ -137,6 +137,7 @@ export const ALLOWED_INPUT_KEYS = [
137
137
  'aiGroupSummaries',
138
138
  'withDetailContext',
139
139
  'withGroupSummaryContext',
140
+ 'withSubGroup',
140
141
  'detailPrefetch',
141
142
  'maxCharsPerBatch',
142
143
  'maxItemsPerBatch',
@@ -3,17 +3,16 @@ import { readBoundedInt } from './todoSmartConvert.core.js';
3
3
  import { openDetailCache, DETAIL_CACHE_ENTRY_VERSION } from './todoDetailCache.js';
4
4
  import { excludeTagFor } from './aigroupRules.js';
5
5
  /**
6
- * 把「全量 basicInfo + 排除清单过滤后的 formData 字段」拼成喂模型的文本(2026-10-30 用户定稿)。
6
+ * 把「排除清单过滤后的 formData 字段」拼成喂模型的文本。
7
7
  *
8
- * 每行一条 `fieldName:value`——字段归属一目了然;basicInfo 的键值同样按 `key:value` 逐行放在最前。
8
+ * 每行一条 `fieldName:value`;2026-11-03 起基础信息(basic)整体丢弃、只有 `workflowName` 保留在
9
+ * `DetailFact` 上用于分组回填,**不进素材**。
9
10
  * 字段来源与截断口径见 `todoDetailFetcher`(排除清单 → ≤200 个字段 → 单值 ≤500 字)。
10
11
  */
11
12
  export function contextOfDetail(detail) {
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');
13
+ // 2026-11-03 用户定:流程详情接口返回的 basic 基础信息直接丢弃(只留 workflowName 用于分组回填,
14
+ // 且不进素材)。因此素材就是清洗后的表单字段,逐行 `fieldName:value`。
15
+ return (detail.formFields ?? []).map((field) => `${field.fieldName}:${field.value}`).join('\n');
17
16
  }
18
17
  const EMPTY_DETAIL_STATS = {
19
18
  requested: 0,
@@ -57,7 +56,11 @@ function materialFieldsOf(detail) {
57
56
  *
58
57
  * 失败不致命:返回已拿到的部分 + warning,调用方按「无详情」降级。
59
58
  */
60
- export async function fetchDetailFacts(env, requestIds, args, stamps) {
59
+ export async function fetchDetailFacts(env, requestIds, args, stamps,
60
+ /** 列表行的 `requestId → workflowName`(详情回填与 excludeTag 校验用) */
61
+ names,
62
+ /** 列表行的 `requestId → createrName`(内部留言「发起人」行兜底) */
63
+ createrNames) {
61
64
  const unique = [...new Set(requestIds.filter(Boolean))];
62
65
  const facts = new Map();
63
66
  const warnings = [];
@@ -72,15 +75,16 @@ export async function fetchDetailFacts(env, requestIds, args, stamps) {
72
75
  const missing = [];
73
76
  for (const requestId of unique) {
74
77
  const hit = cache.get(requestId, stamps?.get(requestId));
75
- if (!hit || hit.excludeTag !== excludeTagFor(hit.basicInfo.workflowName ?? '')) {
78
+ if (!hit || hit.excludeTag !== excludeTagFor(hit.workflowName ?? names?.get(requestId) ?? '')) {
76
79
  missing.push(requestId);
77
80
  continue;
78
81
  }
79
82
  facts.set(requestId, {
80
- context: contextOfDetail({ basicInfo: hit.basicInfo, formFields: hit.fields }),
83
+ context: contextOfDetail({ formFields: hit.fields }),
81
84
  contextAvailable: hit.fields.length > 0,
82
85
  metrics: hit.metrics,
83
86
  fields: hit.fields,
87
+ workflowName: hit.workflowName || (names?.get(requestId) ?? ''),
84
88
  });
85
89
  stats.cacheHit += 1;
86
90
  }
@@ -91,6 +95,7 @@ export async function fetchDetailFacts(env, requestIds, args, stamps) {
91
95
  const { details, stats: fetchStats } = await fetchRequestDetailsBatch(env, missing, {
92
96
  concurrency: readBoundedInt(args.detailConcurrency, 'detailConcurrency', 10, 1, 32),
93
97
  timeoutMs: readBoundedInt(args.detailTimeoutMs, 'detailTimeoutMs', 10_000, 1_000, 60_000),
98
+ ...(createrNames ? { rowMeta: createrNames } : {}),
94
99
  });
95
100
  stats.failed = fetchStats.failed;
96
101
  stats.concurrency = fetchStats.concurrency;
@@ -100,6 +105,7 @@ export async function fetchDetailFacts(env, requestIds, args, stamps) {
100
105
  contextAvailable: detail.formFields.length > 0,
101
106
  metrics: detail.metrics ?? {},
102
107
  fields: materialFieldsOf(detail),
108
+ workflowName: detail.workflowName || (names?.get(requestId) ?? ''),
103
109
  };
104
110
  facts.set(requestId, fact);
105
111
  stats.fetched += 1;
@@ -110,7 +116,7 @@ export async function fetchDetailFacts(env, requestIds, args, stamps) {
110
116
  // 详情为空也写缓存:否则这类条目每次都要重拉
111
117
  if (cache.set(requestId, stamps?.get(requestId), {
112
118
  v: DETAIL_CACHE_ENTRY_VERSION,
113
- basicInfo: detail.basicInfo,
119
+ workflowName: fact.workflowName,
114
120
  metrics: fact.metrics,
115
121
  fields: fact.fields,
116
122
  excludeTag: detail.excludeTag,
@@ -1,5 +1,4 @@
1
1
  import { workflowValidationError } from '../../errors.js';
2
- import { generalRules } from './aigroupRules.js';
3
2
  import { UNKNOWN_GROUP, groupKeyOf, readGroupBy, readItems, readTopN, assertKnownKeys, } from './todoSmartConvert.core.js';
4
3
  import { intentAppliesToKind, } from './businessRules.js';
5
4
  /** 紧急程度以 requestLevel=2 或显示文本「紧急」为准;1 级是「重要」。 */
@@ -327,16 +326,11 @@ export function runGroup(args) {
327
326
  const { rawCount, rows, duplicateCount, unreadableCount } = readItems(args.items);
328
327
  const groupBy = readGroupBy(args.groupBy);
329
328
  const topN = readTopN(args.topN);
330
- // 优先组是叠加视图:筛出的条目**不从**工作流分组剔除,两个视图各自完整。
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);
336
- const priorityRows = sortPriorityRows(priority.rows);
337
- const restRows = rows;
329
+ // 2026-11-03 用户定:分组阶段**不再**产出「建议优先处理的流程」——它是最终结构上的一个置顶
330
+ // 视图,等摘要全部回填后用 `assemblePriorityGroup()` 按同一条规则从成品数据里筛出来。
331
+ // 这样前期(摘要单位、闸门、统计)都只需处理一份数据,不必到处特判 topPriority。
338
332
  const buckets = new Map();
339
- for (const row of restRows) {
333
+ for (const row of rows) {
340
334
  const key = groupKeyOf(row, groupBy);
341
335
  let bucket = buckets.get(key);
342
336
  if (!bucket) {
@@ -364,20 +358,8 @@ export function runGroup(args) {
364
358
  stats: statsOf(bucket.rows),
365
359
  ...(bucket.ids.length > 1 ? { workflowIds: bucket.ids } : {}),
366
360
  }));
367
- const groups = priorityRows.length > 0 && groupBy === 'workflow'
368
- ? [{
369
- id: PRIORITY_GROUP_ID,
370
- topPriority: true,
371
- title: pgTitle,
372
- count: priorityRows.length,
373
- requestDatas: priorityRows.map((row) => toRequestData(row)),
374
- stats: statsOf(priorityRows),
375
- }, ...workflowGroups]
376
- : workflowGroups;
377
- const displayedWorkflow = workflowGroups.slice(0, topN);
378
- const displayed = priorityRows.length > 0 && groupBy === 'workflow'
379
- ? [groups[0], ...displayedWorkflow]
380
- : displayedWorkflow;
361
+ const groups = workflowGroups;
362
+ const displayed = workflowGroups.slice(0, topN);
381
363
  const rest = workflowGroups.slice(topN);
382
364
  const restCount = rest.reduce((sum, group) => sum + group.count, 0);
383
365
  const overall = statsOf(rows);
@@ -394,20 +376,6 @@ export function runGroup(args) {
394
376
  ? `有 ${unknownGroupCount} 条缺少工作流名称,已归入「${UNKNOWN_GROUP}」`
395
377
  : `有 ${unknownGroupCount} 条缺少分组字段,已归入「${UNKNOWN_GROUP}」`);
396
378
  }
397
- if (priorityRows.length > 0 && groupBy === 'workflow') {
398
- const levelText = priority.targetLevel === 2
399
- ? '紧急(requestLevel=2)'
400
- : priority.targetLevel === 1
401
- ? '重要(requestLevel=1;本次无 2 级数据,已按规则降级取 1,0 级不取)'
402
- : '无等级(本次整批没有 ≥1 级的条目,只按超时筛)';
403
- warnings.push(`「${pgTitle}」筛出 ${priorityRows.length} 条:超时 ${priority.overdueCount} 条 / ` +
404
- `等级命中 ${priority.levelCount} 条(两者都满足 ${priority.bothCount} 条),等级口径=${levelText};` +
405
- `这 ${priorityRows.length} 条同时保留在各自工作流分组中(叠加视图:Σcount 会大于 totalCount,` +
406
- `totalCount 按去重条数计 = ${rows.length})`);
407
- }
408
- else if (priorityRows.length === 0 && groupBy === 'workflow') {
409
- warnings.push(`本次没有超时条目,也没有 requestLevel≥1 的条目,故不输出「${PRIORITY_GROUP_TITLE}」分组`);
410
- }
411
379
  if (rest.length > 0) {
412
380
  warnings.push(`markdown 只展示前 ${topN} 个工作流分组,其余 ${rest.length} 组共 ${restCount} 条仍在 wfIdGroup 中(未截断)`);
413
381
  }
@@ -442,7 +410,7 @@ export function runGroup(args) {
442
410
  },
443
411
  wfIdGroup: groups,
444
412
  /** 第二部分(非优先组)的总条数与组数,前端折叠/分页直接用它,不用自己减 */
445
- restTotal: restRows.length,
413
+ restTotal: rows.length,
446
414
  restGroupCount: workflowGroups.length,
447
415
  restGroups: rest.length > 0 ? { groupCount: rest.length, count: restCount } : null,
448
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' },
@@ -140,6 +142,19 @@ export const workflowTodoSmartConvertOperations = [{
140
142
  : readStage(args.stage);
141
143
  const hasItems = Array.isArray(args.items);
142
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
+ };
143
158
  if ((stage === 'group' || stage === 'smartGroup' || stage === 'aggregate') && !hasItems) {
144
159
  throw workflowValidationError('items_required', 'stage=' + stage + ' 需要 items:先执行 stage=collect,再回灌 data.items', { retryAllowed: false });
145
160
  }
@@ -167,28 +182,48 @@ export const workflowTodoSmartConvertOperations = [{
167
182
  throw workflowValidationError('ai_summaries_required', 'stage=summary 需要 aiSummaries:先执行 stage=summaryContext', { retryAllowed: false });
168
183
  }
169
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);
170
198
  if (args.withGroupSummaryContext === true) {
171
199
  const context = await runGroupSummaryContext({
172
- wfIdGroup: result.data.wfIdGroup,
173
- smartSummaryTitle: result.data.smartSummaryTitle,
174
- totalCount: result.data.totalCount,
200
+ wfIdGroup: final.data.wfIdGroup,
201
+ smartSummaryTitle: final.data.smartSummaryTitle,
202
+ totalCount: final.data.totalCount,
175
203
  }, env);
176
204
  if (Array.isArray(context.data.groupSummaryBatches) && context.data.groupSummaryBatches.length === 0) {
177
- return finish(env, args, startedAt, stage, result.data, {
178
- branch: 'todo-smart-convert', stage, aiSummarized: true, withGroupSummaryContext: true, ...result.meta,
179
- }, 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);
180
208
  }
181
209
  return finish(env, args, startedAt, stage, {
182
- ...result.data,
210
+ ...final.data,
183
211
  groupSummaryBatches: context.data.groupSummaryBatches,
184
212
  groupSummaryStats: context.data.groupSummaryStats,
185
213
  }, {
186
- branch: 'todo-smart-convert', stage, aiSummarized: true, withGroupSummaryContext: true, ...result.meta,
187
- }, [...result.warnings, ...context.warnings]);
214
+ branch: 'todo-smart-convert', stage, aiSummarized: true, subGrouped, withGroupSummaryContext: true, ...result.meta,
215
+ }, [...final.warnings, ...context.warnings]);
188
216
  }
189
- return finish(env, args, startedAt, stage, result.data, {
190
- branch: 'todo-smart-convert', stage, aiSummarized: true, ...result.meta,
191
- }, 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);
192
227
  }
193
228
  if (stage === 'groupSummaryContext') {
194
229
  const result = await runGroupSummaryContext(args, env);
@@ -201,9 +236,10 @@ export const workflowTodoSmartConvertOperations = [{
201
236
  throw workflowValidationError('ai_group_summaries_required', 'stage=groupSummary 需要 aiGroupSummaries:先执行 stage=groupSummaryContext', { retryAllowed: false });
202
237
  }
203
238
  const result = await runGroupSummary(args, env);
204
- 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, {
205
241
  branch: 'todo-smart-convert', stage, aiSummarized: true, ...result.meta,
206
- }, result.warnings);
242
+ }, final.warnings);
207
243
  }
208
244
  if (stage === 'group' || resumed) {
209
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) {