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
@@ -12,7 +12,7 @@
12
12
  import { existsSync, readFileSync } from 'node:fs';
13
13
  import { join } from 'node:path';
14
14
  import { stateRoot } from '../../../../core/state.js';
15
- import { cacheRoots } from './summaryCache.js';
15
+ import { resolveSkillDir } from './summaryCache.js';
16
16
  export const AIGROUP_DIR_NAME = 'aigroup';
17
17
  export const DEFAULT_RULE_FILE = 'default.json';
18
18
  export const USER_INTENT_RULE_FILE = 'user-intents.json';
@@ -30,6 +30,7 @@ export const FALLBACK_DEFAULTS = {
30
30
  summaryMaxChars: 150,
31
31
  groupSummaryMaxChars: 100,
32
32
  groupSummaryTail: '可展开逐条查看或直接汇总处理',
33
+ excludeOriginFormDataFields: [],
33
34
  };
34
35
  export function emptyRuleFile(error = null) {
35
36
  return {
@@ -66,16 +67,79 @@ function validateFocusKeyword(raw, index) {
66
67
  return `focusKeyword[${index}] 必须是 [指标, 算子, 值] 三元组`;
67
68
  return null;
68
69
  }
70
+ /** 校验排除字段清单:缺省放行;给了就必须是字符串或字符串数组(允许空数组)。 */
71
+ function validateExcludeFields(raw, where) {
72
+ if (raw === undefined)
73
+ return null;
74
+ if (typeof raw === 'string') {
75
+ return raw.trim() === '' ? `${where} 不能是空字符串(不排除就别给这个键)` : null;
76
+ }
77
+ if (!Array.isArray(raw))
78
+ return `${where} 必须是字符串数组`;
79
+ for (const item of raw) {
80
+ if (typeof item !== 'string' || item.trim() === '')
81
+ return `${where} 的每一项都必须是非空字符串`;
82
+ }
83
+ return null;
84
+ }
69
85
  /** 校验整份规则文件;返回错误列表(空数组 = 合法)。 */
70
86
  export function validateRuleFile(raw) {
71
87
  const errors = [];
72
88
  const file = raw;
73
89
  if (!file || typeof file !== 'object')
74
90
  return ['规则文件必须是 JSON 对象'];
91
+ // schemaVersion:不能是废字段。给值时必须受支持(2 = aigroup set 写入的意图文件,3 = default.json);缺省允许(规则级校验兼容)。
92
+ if (file.schemaVersion !== undefined && file.schemaVersion !== 2 && file.schemaVersion !== 3) {
93
+ errors.push(`schemaVersion 只支持 2 或 3,收到 ${JSON.stringify(file.schemaVersion ?? null)}`);
94
+ }
75
95
  if (!Array.isArray(file.rules)) {
76
96
  errors.push('rules 必须是数组');
77
97
  return errors;
78
98
  }
99
+ // generalRules(2026-11-03):可选区块,只做宽松类型校验;未知键忽略(向后兼容预留扩展)。
100
+ if (file.generalRules !== undefined) {
101
+ if (!file.generalRules || typeof file.generalRules !== 'object' || Array.isArray(file.generalRules)) {
102
+ errors.push('generalRules 必须是对象');
103
+ }
104
+ else {
105
+ const identity = file.generalRules.identityFilter;
106
+ if (identity !== undefined) {
107
+ if (!identity || typeof identity !== 'object' || Array.isArray(identity)) {
108
+ errors.push('generalRules.identityFilter 必须是对象');
109
+ }
110
+ else if (identity.defaultIsremarks !== undefined
111
+ && !/^\d+(,\d+)*$/.test(String(identity.defaultIsremarks).trim())) {
112
+ errors.push('generalRules.identityFilter.defaultIsremarks 必须是「数字逗号拼接」格式,如 0,1,2');
113
+ }
114
+ if (identity?.excludedCodes !== undefined
115
+ && (!Array.isArray(identity.excludedCodes)
116
+ || identity.excludedCodes.some((c) => !/^\d+$/.test(String(c))))) {
117
+ errors.push('generalRules.identityFilter.excludedCodes 必须是数字码数组');
118
+ }
119
+ }
120
+ const priority = file.generalRules.priorityGroup;
121
+ if (priority !== undefined) {
122
+ if (!priority || typeof priority !== 'object' || Array.isArray(priority)) {
123
+ errors.push('generalRules.priorityGroup 必须是对象');
124
+ }
125
+ else {
126
+ if (priority.title !== undefined && (typeof priority.title !== 'string' || !priority.title.trim())) {
127
+ errors.push('generalRules.priorityGroup.title 必须是非空字符串');
128
+ }
129
+ if (priority.rules !== undefined) {
130
+ const KNOWN_PRIORITY_RULES = ['overdue', 'levelFallback'];
131
+ if (!Array.isArray(priority.rules) || priority.rules.length === 0
132
+ || priority.rules.some((r) => !KNOWN_PRIORITY_RULES.includes(String(r)))) {
133
+ errors.push(`generalRules.priorityGroup.rules 只支持 ${KNOWN_PRIORITY_RULES.join('、')}`);
134
+ }
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
140
+ const defaultsExclude = validateExcludeFields(file.defaults?.excludeOriginFormDataFields, 'defaults.excludeOriginFormDataFields');
141
+ if (defaultsExclude)
142
+ errors.push(defaultsExclude);
79
143
  const seen = new Set();
80
144
  file.rules.forEach((rule, index) => {
81
145
  if (!rule || typeof rule !== 'object') {
@@ -98,6 +162,16 @@ export function validateRuleFile(raw) {
98
162
  if (rule.summaryMaxChars !== undefined && (typeof rule.summaryMaxChars !== 'number' || rule.summaryMaxChars <= 0)) {
99
163
  errors.push(`rules[${index}].summaryMaxChars 必须是正数`);
100
164
  }
165
+ const excludeReason = validateExcludeFields(rule.excludeOriginFormDataFields, `rules[${index}].excludeOriginFormDataFields`);
166
+ if (excludeReason)
167
+ errors.push(excludeReason);
168
+ if (rule.summaryRequiredFields !== undefined) {
169
+ if (!Array.isArray(rule.summaryRequiredFields) || rule.summaryRequiredFields.length === 0
170
+ || rule.summaryRequiredFields.some((f) => typeof f !== 'string' || !f.trim() || f.trim().length > 50)
171
+ || rule.summaryRequiredFields.length > 20) {
172
+ errors.push(`rules[${index}].summaryRequiredFields 必须是非空字符串数组(1~20 项、每项 1~50 字)`);
173
+ }
174
+ }
101
175
  const groupRule = rule.aiGroup?.groupRule;
102
176
  if (groupRule) {
103
177
  if (typeof groupRule.focus !== 'boolean')
@@ -162,47 +236,42 @@ export function loadRuleFile(file) {
162
236
  return emptyRuleFile(`规则文件校验失败:${errors.join(';')}`);
163
237
  return { file: parsed, error: null };
164
238
  }
165
- /** 默认规则目录(**随 skill 分发、只读**):`WEAVER_WORK_CLI_AIGROUP_DIR` → skill 目录下的 `aigroup/` → CLI 状态目录。 */
166
- export function aigroupDir(ctx) {
167
- const explicit = process.env.WEAVER_WORK_CLI_AIGROUP_DIR?.trim();
168
- if (explicit)
169
- return join(explicit);
170
- const roots = cacheRoots(ctx);
171
- return roots.skillDir ? join(roots.skillDir, AIGROUP_DIR_NAME) : join(stateRoot(ctx), AIGROUP_DIR_NAME);
172
- }
173
239
  /**
174
- * 用户意图目录(**运行时产物,绝不写进 skill 目录**):`WEAVER_WORK_CLI_AIGROUP_DIR` → CLI 状态目录。
175
- *
176
- * 刻意与 `aigroupDir()` 分开:意图文件是本机产生的,一旦落在 skill 目录里,
177
- * 打包/安装时会跟着 skill 跑到别人机器上(既是泄漏,也会污染别人的分组口径)。
240
+ * 当前 skill 的 aigroup 目录(`default.json` 与 `user-intents.json` 同目录;
241
+ * 2026-11-01 用户定稿:规则与意图都锚定**当前 agent 的 skill 目录**)。
242
+ * 裸 CLI(无当前 skill 上下文)返回 null ⇒ 规则不加载、意图不可读写。
178
243
  */
244
+ export function aigroupDir(ctx) {
245
+ const skillDir = resolveSkillDir();
246
+ return skillDir ? join(skillDir, AIGROUP_DIR_NAME) : null;
247
+ }
248
+ /** 用户意图目录:与 `default.json` 同目录(见 `aigroupDir`)。 */
179
249
  export function aigroupUserDir(ctx) {
180
- const explicit = process.env.WEAVER_WORK_CLI_AIGROUP_DIR?.trim();
181
- if (explicit)
182
- return join(explicit);
183
- return join(stateRoot(ctx), AIGROUP_DIR_NAME);
250
+ return aigroupDir(ctx);
184
251
  }
185
252
  /**
186
253
  * 加载并合并规则:默认文件 +(可选)用户意图文件(**同 workflowName 整条覆盖**,系统默认不被覆盖)。
187
254
  * 用户意图文件里的条目按 `workflowName` 粒度替换默认条目,缺字段由 defaults 补。
255
+ * 裸 CLI(无当前 skill 上下文)⇒ 按无规则处理 + warning(缓存与规则都拒绝,见 cacheRoots)。
188
256
  */
189
257
  export function loadRules(options = {}) {
190
- const dir = options.dir ?? aigroupDir(options.ctx);
191
- // 显式 `dir` 时(测试/现场)默认文件与意图文件放一起;否则意图文件只从状态目录读。
192
- const userDir = options.dir ?? aigroupUserDir(options.ctx);
193
258
  const warnings = [];
259
+ const dir = options.dir ?? aigroupDir(options.ctx);
260
+ if (!dir) {
261
+ warnings.push('无当前 skill 上下文(WEAVER_WORK_CLI_SKILL_DIR 未设置):aigroup 规则未加载,按无规则处理');
262
+ return { file: emptyRuleFile(null).file, warnings, dir: null };
263
+ }
194
264
  const base = loadRuleFile(join(dir, DEFAULT_RULE_FILE));
195
265
  if (base.error)
196
266
  warnings.push(base.error);
197
267
  const rules = [...base.file.rules];
198
- // 兼容迁移:意图文件原先写在 skill 目录里,改到状态目录后老文件不会被自动搬走;
199
- // 状态目录没有时就回读老位置,避免用户已有的意图「凭空消失」。
200
- const userIntentFile = join(userDir, USER_INTENT_RULE_FILE);
201
- const legacyIntentFile = join(dir, USER_INTENT_RULE_FILE);
268
+ // 意图文件与 default.json 同目录;兼容迁移:老位置(CLI 状态目录)若存在且新位置没有 ⇒ 回读 + 提示
269
+ const userIntentFile = join(dir, USER_INTENT_RULE_FILE);
270
+ const legacyIntentFile = join(stateRoot(options.ctx), AIGROUP_DIR_NAME, USER_INTENT_RULE_FILE);
202
271
  let intentFile = userIntentFile;
203
272
  if (!options.dir && !existsSync(userIntentFile) && existsSync(legacyIntentFile)) {
204
273
  intentFile = legacyIntentFile;
205
- warnings.push(`用户意图仍读自 skill 目录(${legacyIntentFile}):新位置是 ${userIntentFile},重新执行 aigroup set 即可迁移`);
274
+ warnings.push(`用户意图仍读自 CLI 状态目录(${legacyIntentFile}):新位置是 ${userIntentFile},重新执行 aigroup set 即可迁移`);
206
275
  }
207
276
  const intent = loadRuleFile(intentFile);
208
277
  if (intent.error) {
@@ -225,7 +294,13 @@ export function loadRules(options = {}) {
225
294
  }
226
295
  }
227
296
  return {
228
- file: { schemaVersion: base.file.schemaVersion, defaults: { ...FALLBACK_DEFAULTS, ...base.file.defaults }, rules },
297
+ file: {
298
+ schemaVersion: base.file.schemaVersion,
299
+ defaults: { ...FALLBACK_DEFAULTS, ...base.file.defaults },
300
+ rules,
301
+ // 通用规则区(2026-11-03):仅取 default.json 的定义;用户意图文件不参与通用规则
302
+ ...(base.file.generalRules ? { generalRules: base.file.generalRules } : {}),
303
+ },
229
304
  warnings,
230
305
  dir,
231
306
  };
@@ -274,13 +349,16 @@ export function resolveRule(file, workflowName) {
274
349
  summaryMaxChars: raw.summaryMaxChars ?? defaults.summaryMaxChars,
275
350
  groupSummaryMaxChars: defaults.groupSummaryMaxChars,
276
351
  groupSummaryTail: defaults.groupSummaryTail,
352
+ // 显式给 `[]` = 规则级覆盖(清空 defaults 的清单);未给才落 defaults
353
+ excludeOriginFormDataFields: raw.excludeOriginFormDataFields === undefined
354
+ ? defaults.excludeOriginFormDataFields
355
+ : asStringArray(raw.excludeOriginFormDataFields) ?? [],
277
356
  aiGroup: raw.aiGroup
278
357
  ? {
279
358
  groupSummary: {
280
359
  needAI: groupSummary?.needAI ?? false,
281
360
  ...(groupSummary?.prompt === undefined ? {} : { prompt: groupSummary.prompt }),
282
361
  ...(groupSummary?.template === undefined ? {} : { template: groupSummary.template }),
283
- maxChars: defaults.groupSummaryMaxChars,
284
362
  tail: defaults.groupSummaryTail,
285
363
  },
286
364
  groupRule: groupRule ?? EMPTY_GROUP_RULE,
@@ -441,8 +519,32 @@ export function activeRuleFor(workflowName) {
441
519
  return null;
442
520
  return resolveRule(activeRules(), name)?.rule ?? null;
443
521
  }
522
+ /** 通用规则区(2026-11-03):未配置返回 null,消费方按内置默认兜底。 */
523
+ export function generalRules() {
524
+ return activeRules().generalRules ?? null;
525
+ }
444
526
  /** 清空进程内缓存(单测用)。 */
445
527
  export function resetActiveRules() {
446
528
  activeFile = null;
447
529
  activeDir = null;
448
530
  }
531
+ // ---------------------------------------------------------------------------
532
+ // formData 排除字段(2026-10-30 用户定稿:excludeOriginFormDataFields)
533
+ // ---------------------------------------------------------------------------
534
+ /**
535
+ * 当前生效规则的 formData 排除字段清单(空数组 = 不排除)。
536
+ *
537
+ * 消费方:`todoDetailFetcher.fetchOneDetail` —— 拿到接口 formData 后按 fieldName 精确匹配剔除;
538
+ * 详情缓存用 `excludeTagFor()` 做**口径失效**:清单一变,旧缓存条目整体 miss。
539
+ */
540
+ export function excludeFieldsFor(workflowName) {
541
+ return activeRuleFor(workflowName)?.excludeOriginFormDataFields ?? [];
542
+ }
543
+ /** 排除清单的特征串(原文拼接)。规则没配 = 空串,与「无口径」条目互相匹配。 */
544
+ export function excludeTagOf(fields) {
545
+ return [...fields].join('\u0001');
546
+ }
547
+ /** 便捷组合:某流程当前生效的排除口径特征串。 */
548
+ export function excludeTagFor(workflowName) {
549
+ return excludeTagOf(excludeFieldsFor(workflowName));
550
+ }
@@ -44,6 +44,8 @@ function namesOf(rule) {
44
44
  */
45
45
  export function saveUserIntents(payload, options = {}) {
46
46
  const dir = options.dir ?? aigroupUserDir(options.ctx);
47
+ if (!dir)
48
+ throw workflowValidationError('aigroup_no_skill_context', '无当前 skill 上下文(WEAVER_WORK_CLI_SKILL_DIR 未设置):用户意图不可读写');
47
49
  const warnings = [];
48
50
  const entries = normalizeIntentPayload(payload);
49
51
  if (entries.length === 0)
@@ -86,6 +88,8 @@ export function saveUserIntents(payload, options = {}) {
86
88
  /** 读用户意图文件(诊断/展示用)。 */
87
89
  export function readUserIntents(options = {}) {
88
90
  const dir = options.dir ?? aigroupUserDir(options.ctx);
91
+ if (!dir)
92
+ return { dir: null, file: '', exists: false, error: '无当前 skill 上下文(WEAVER_WORK_CLI_SKILL_DIR 未设置)', rules: [] };
89
93
  const file = join(dir, USER_INTENT_RULE_FILE);
90
94
  if (!existsSync(file))
91
95
  return { dir, file, exists: false, error: null, rules: [] };
@@ -95,6 +99,8 @@ export function readUserIntents(options = {}) {
95
99
  /** 清空用户意图(只删内容、不删文件,保持目录结构)。 */
96
100
  export function clearUserIntents(options = {}) {
97
101
  const dir = options.dir ?? aigroupUserDir(options.ctx);
102
+ if (!dir)
103
+ throw workflowValidationError('aigroup_no_skill_context', '无当前 skill 上下文(WEAVER_WORK_CLI_SKILL_DIR 未设置):用户意图不可读写');
98
104
  const file = join(dir, USER_INTENT_RULE_FILE);
99
105
  mkdirSync(dir, { recursive: true, mode: 0o700 });
100
106
  writeFileSync(file, `${JSON.stringify({ schemaVersion: 2, rules: [] }, null, 2)}\n`, { mode: 0o600 });
@@ -1,8 +1,6 @@
1
- import { existsSync, readdirSync, statSync } from 'node:fs';
1
+ import { existsSync } from 'node:fs';
2
2
  import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
3
- import { homedir } from 'node:os';
4
3
  import { join, resolve } from 'node:path';
5
- import { stateRoot } from '../../../../core/state.js';
6
4
  /**
7
5
  * `todoSmartConvert` 的**单条摘要缓存**(放在 Agent 真实 skill 目录下)。
8
6
  *
@@ -18,21 +16,13 @@ import { stateRoot } from '../../../../core/state.js';
18
16
  * 流程有了新反馈(lastFeedBackDateTime 变化)key 就变了,摘要自然失效、重新生成——
19
17
  * 用业务时间做版本号,比记录「摘要生成时间」更准。
20
18
  *
21
- * 目录定位顺序:`WEAVER_WORK_CLI_CACHE_DIR`(直接指定缓存目录,测试用)→
22
- * `WEAVER_WORK_CLI_SKILL_DIR`(直接指定 skill 目录)→ 已知 Agent 的全局 skill 根目录
23
- * (`~/.workbuddy/skills`、`~/.claude/skills` …)→ 全部不存在时退回 CLI 状态目录(并在 warnings 里说明)。
19
+ * 目录定位(2026-11-01 用户定稿:**缓存的生成与提取都取自当前 skill 自己的 `cache/`**):
20
+ * `WEAVER_WORK_CLI_SKILL_DIR`(tools 脚本把自身所在 skill 目录显式指进来)⇒ 直接用 `<该目录>/cache`;
21
+ * 未设置(裸 CLI,无当前 skill 上下文)⇒ **拒绝读写缓存**(refused),调用方按「没有缓存」处理。
22
+ * 用户已不要求多安装发现与排名——各自 agent 独立安装一份 skill,脚本锚定自身目录即可。
24
23
  */
25
- /** 与 `skills/` 下发布的目录名一致。 */
26
- const SKILL_DIR_NAME = 'weaver-e10-workflow';
27
24
  /** skill 内的缓存子目录。 */
28
25
  const CACHE_DIR_NAME = 'cache';
29
- /**
30
- * 已知 Agent 产物的用户级根目录(按优先级)。
31
- *
32
- * 只看 `~/<agent>/skills/<skill>/SKILL.md` 是否存在——存在即认为「这个 Agent 装了这个 skill」。
33
- * **不看工作区里的 `.workbuddy/skills`**:项目内目录属于「本地项目」,用户明确不让缓存放那儿。
34
- */
35
- const AGENT_HOME_DIRS = ['.workbuddy', '.claude', '.codebuddy', '.codex', '.cursor', '.gemini'];
36
26
  /** 单条摘要缓存(跨任务,7 天)。 */
37
27
  export const SUMMARY_CACHE_FILE_NAME = 'todo-summary-cache.json';
38
28
  /**
@@ -42,105 +32,31 @@ export const SUMMARY_CACHE_FILE_NAME = 'todo-summary-cache.json';
42
32
  */
43
33
  export const SUMMARY_CACHE_VERSION = 2;
44
34
  function uniqueDirs(dirs) {
45
- return [...new Set(dirs.filter(Boolean))];
46
- }
47
- function stateCacheDir(ctx) {
48
- return resolve(stateRoot(ctx), CACHE_DIR_NAME);
35
+ return [...new Set(dirs.filter((dir) => Boolean(dir)))];
49
36
  }
50
37
  /**
51
- * 扫已知 Agent 的 skills 根,找「本 skill 的目录」——判据是 `references/todo-smart-convert.md` 存在。
52
- * 用于 skill 目录被改名(例如本地测试副本)后仍能定位到已攒的摘要缓存。
38
+ * 解析「当前 skill 目录」:只认 `WEAVER_WORK_CLI_SKILL_DIR`(tools 脚本锚定自身目录);
39
+ * 未设置 = 裸 CLI,无当前 skill 上下文。
53
40
  */
54
- function discoverThisSkillDirs(home) {
55
- const marker = join('references', 'todo-smart-convert.md');
56
- const found = [];
57
- for (const agent of AGENT_HOME_DIRS) {
58
- const root = join(home, agent, 'skills');
59
- const entries = (() => {
60
- try {
61
- return readdirSync(root, { withFileTypes: true });
62
- }
63
- catch {
64
- return [];
65
- }
66
- })();
67
- for (const entry of entries) {
68
- if (!entry.isDirectory())
69
- continue;
70
- const dir = join(root, entry.name);
71
- if (existsSync(join(dir, marker)))
72
- found.push(dir);
73
- }
74
- }
75
- return found;
76
- }
77
- /** 读文件 mtime(毫秒);读不到(不存在/无权限)返回 0。 */
78
- function mtimeOf(file) {
79
- try {
80
- return statSync(file).mtimeMs;
81
- }
82
- catch {
83
- return 0;
84
- }
85
- }
86
- /**
87
- * 多个候选 skill 目录同时存在时,优先选择已有缓存且安装完整的目录。
88
- *
89
- * 排序键(依次比较,越小越优先):
90
- * 1. 目录里**已有摘要缓存** ⇒ 优先(连续性:别把攒好的摘要换到另一份安装上)
91
- * 2. 有 `references/todo-smart-convert.md` ⇒ 优先(完整安装优于半解压的残目录)
92
- * 3. `SKILL.md` 的 mtime 更新 ⇒ 优先(刚复制/刚升级的那份大概率就是正在用的)
93
- * 4. 原始顺序(`WEAVER_WORK_CLI_SKILL_DIR` 指定的永远排最前)
94
- */
95
- function rankSkillDirs(dirs) {
96
- const scored = dirs.map((dir, index) => ({
97
- dir,
98
- index,
99
- hasCache: existsSync(join(dir, CACHE_DIR_NAME, SUMMARY_CACHE_FILE_NAME)) ? 1 : 0,
100
- cacheMtime: mtimeOf(join(dir, CACHE_DIR_NAME, SUMMARY_CACHE_FILE_NAME)),
101
- hasMarker: existsSync(join(dir, 'references', 'todo-smart-convert.md')) ? 1 : 0,
102
- skillMtime: mtimeOf(join(dir, 'SKILL.md')),
103
- }));
104
- scored.sort((a, b) => b.hasCache - a.hasCache
105
- || b.cacheMtime - a.cacheMtime
106
- || b.hasMarker - a.hasMarker
107
- || b.skillMtime - a.skillMtime
108
- || a.index - b.index);
109
- return scored.map((item) => item.dir);
41
+ export function resolveSkillDir() {
42
+ const explicit = process.env.WEAVER_WORK_CLI_SKILL_DIR?.trim();
43
+ return explicit ? resolve(explicit) : null;
110
44
  }
111
45
  /** 定位缓存目录(不创建目录,纯路径计算)。 */
112
46
  export function cacheRoots(ctx) {
113
- const fallback = stateCacheDir(ctx);
114
- const explicitCache = process.env.WEAVER_WORK_CLI_CACHE_DIR?.trim();
115
- if (explicitCache) {
116
- const dir = resolve(explicitCache);
117
- return { preferred: dir, fallback: dir, inSkillDir: false, skillDir: null };
118
- }
119
- const home = homedir();
120
- const explicitSkill = process.env.WEAVER_WORK_CLI_SKILL_DIR?.trim();
121
- const candidates = [];
122
- if (explicitSkill)
123
- candidates.push(resolve(explicitSkill));
124
- for (const agent of AGENT_HOME_DIRS) {
125
- candidates.push(join(home, agent, 'skills', SKILL_DIR_NAME));
47
+ void ctx;
48
+ const skillDir = resolveSkillDir();
49
+ if (!skillDir) {
50
+ // 裸 CLI:无当前 skill 上下文 ⇒ 缓存拒绝读写(用户 2026-11-01 定稿:拒绝执行)
51
+ return { preferred: null, fallback: null, inSkillDir: false, skillDir: null, refused: true };
126
52
  }
127
- // 目录名可能被改(如本地测试副本 `todoSmartGroupTest`):再按「本 skill 的特征文件」扫一遍,
128
- // 避免改名后缓存落到 CLI 状态目录、把已攒的摘要全丢掉。
129
- candidates.push(...discoverThisSkillDirs(home));
130
- // 只认「装了本 skill」的目录,再在多份安装之间择优(见 rankSkillDirs)。
131
- const installed = uniqueDirs(candidates).filter((dir) => existsSync(join(dir, 'SKILL.md')));
132
- const [skillDir] = rankSkillDirs(installed);
133
- if (skillDir) {
134
- return {
135
- preferred: join(skillDir, CACHE_DIR_NAME),
136
- fallback,
137
- inSkillDir: true,
138
- skillDir,
139
- };
140
- }
141
- return { preferred: fallback, fallback, inSkillDir: false, skillDir: null };
53
+ const cacheDir = join(skillDir, CACHE_DIR_NAME);
54
+ return { preferred: cacheDir, fallback: cacheDir, inSkillDir: true, skillDir, refused: false };
142
55
  }
143
56
  export async function readCacheFile(roots, fileName) {
57
+ // 裸 CLI(无当前 skill 上下文):拒绝读缓存(用户 2026-11-01 定稿)
58
+ if (roots.refused || !roots.preferred)
59
+ return null;
144
60
  for (const dir of uniqueDirs([roots.preferred, roots.fallback])) {
145
61
  const file = join(dir, fileName);
146
62
  if (!existsSync(file))
@@ -161,6 +77,9 @@ export async function readCacheFile(roots, fileName) {
161
77
  * 缓存是加速设施,写不进去也绝不能让主流程失败。
162
78
  */
163
79
  export async function writeCacheFile(roots, fileName, payload) {
80
+ // 裸 CLI(无当前 skill 上下文):拒绝写缓存(用户 2026-11-01 定稿);目录不可写时静默放弃(不另设兜底)
81
+ if (roots.refused || !roots.preferred)
82
+ return null;
164
83
  for (const dir of uniqueDirs([roots.preferred, roots.fallback])) {
165
84
  const file = join(dir, fileName);
166
85
  try {
@@ -199,15 +118,29 @@ export function summaryCacheKey(requestId, lastFeedBackDateTime) {
199
118
  const stamp = String(lastFeedBackDateTime ?? '').trim();
200
119
  return `${requestId}__${stamp}`;
201
120
  }
121
+ /** 口径提示的入库形式:trim 后原样存储;空口径返回空串(条目不落 pk 字段)。 */
122
+ export function promptTagOf(prompt) {
123
+ return String(prompt ?? '').trim();
124
+ }
202
125
  /**
203
- * 缓存条目是否与当前目标字数口径匹配。
126
+ * 缓存条目是否与当前口径/素材匹配。
204
127
  *
205
- * 不传 `maxChars` 时不按字数过滤;传入后必须与写入口径一致。
128
+ * **失效条件(2026-11-02 用户定稿):只由 `requestSummaryPrompt`(default.json / 用户意图)调整触发**——
129
+ * - `pk`:双方都存在时才比对;**条目缺 pk = 旧口径条目,兼容命中**(无法证明口径变过就不作废);
130
+ * - `maxChars`:双方都存在时才比对,且调用方只在用户**显式**传字数时才传入
131
+ * (`summary.ts` 的 `explicitMaxCharsFor`)——规则/默认字数变化不作废历史摘要;
132
+ * 用户显式要求「10 字以内」时仍会重写(2026-10-22 实测踩坑的保护保留);
133
+ * - `dv`:详情素材特征值仍比对——素材升级 / 排除清单调整后旧摘要缺信息(如安全工单缺行号),
134
+ * 不能继续命中;条目缺 dv = 旧条目,兼容命中。
206
135
  */
207
- function entryMatchesMaxChars(entry, maxChars) {
208
- if (maxChars === undefined)
209
- return true;
210
- return (entry.maxChars ?? 50) === maxChars;
136
+ function entryMatches(entry, maxChars, pk, dv) {
137
+ if (pk !== undefined && entry.pk !== undefined && entry.pk !== pk)
138
+ return false;
139
+ if (maxChars !== undefined && entry.maxChars !== undefined && entry.maxChars !== maxChars)
140
+ return false;
141
+ if (entry.dv !== undefined && entry.dv !== dv)
142
+ return false;
143
+ return true;
211
144
  }
212
145
  function normalizeSummaryEntry(raw) {
213
146
  if (!raw || typeof raw !== 'object')
@@ -218,7 +151,15 @@ function normalizeSummaryEntry(raw) {
218
151
  if (typeof entry.cachedAt !== 'number' || !Number.isFinite(entry.cachedAt))
219
152
  return null;
220
153
  const maxChars = typeof entry.maxChars === "number" && Number.isFinite(entry.maxChars) ? entry.maxChars : undefined;
221
- return { summary: entry.summary, cachedAt: entry.cachedAt, ...(maxChars === undefined ? {} : { maxChars }) };
154
+ const pk = typeof entry.pk === 'string' && entry.pk !== '' ? entry.pk : undefined;
155
+ const dv = typeof entry.dv === 'string' && entry.dv !== '' ? entry.dv : undefined;
156
+ return {
157
+ summary: entry.summary,
158
+ cachedAt: entry.cachedAt,
159
+ ...(maxChars === undefined ? {} : { maxChars }),
160
+ ...(pk === undefined ? {} : { pk }),
161
+ ...(dv === undefined ? {} : { dv }),
162
+ };
222
163
  }
223
164
  /**
224
165
  * 打开摘要缓存(读 + 按需清理)。
@@ -273,21 +214,21 @@ export async function openSummaryCache(ctx) {
273
214
  misses: 0,
274
215
  pruned,
275
216
  dirty: false,
276
- get(requestId, lastFeedBackDateTime, maxChars) {
217
+ get(requestId, lastFeedBackDateTime, maxChars, pk, dv) {
277
218
  const key = summaryCacheKey(requestId, lastFeedBackDateTime);
278
219
  const entry = entries.get(key);
279
- if (entry && now - entry.cachedAt <= SUMMARY_CACHE_TTL_MS && entryMatchesMaxChars(entry, maxChars)) {
220
+ if (entry && now - entry.cachedAt <= SUMMARY_CACHE_TTL_MS && entryMatches(entry, maxChars, pk, dv)) {
280
221
  cache.hits += 1;
281
222
  return entry.summary;
282
223
  }
283
224
  cache.misses += 1;
284
225
  return null;
285
226
  },
286
- has(requestId, lastFeedBackDateTime, maxChars) {
227
+ has(requestId, lastFeedBackDateTime, maxChars, pk, dv) {
287
228
  const entry = entries.get(summaryCacheKey(requestId, lastFeedBackDateTime));
288
- return entry !== undefined && entryMatchesMaxChars(entry, maxChars);
229
+ return entry !== undefined && entryMatches(entry, maxChars, pk, dv);
289
230
  },
290
- set(requestId, lastFeedBackDateTime, summary, maxChars) {
231
+ set(requestId, lastFeedBackDateTime, summary, maxChars, pk, dv) {
291
232
  const text = String(summary ?? '').trim();
292
233
  if (!requestId || !text)
293
234
  return false;
@@ -295,6 +236,8 @@ export async function openSummaryCache(ctx) {
295
236
  summary: text,
296
237
  cachedAt: Date.now(),
297
238
  ...(maxChars === undefined ? {} : { maxChars }),
239
+ ...(pk === undefined || pk === '' ? {} : { pk }),
240
+ ...(dv === undefined || dv === '' ? {} : { dv }),
298
241
  });
299
242
  dirty = true;
300
243
  cache.dirty = true;
@@ -2,11 +2,12 @@ import { cacheRoots, readCacheFile, writeCacheFile, SUMMARY_CACHE_PRUNE_INTERVAL
2
2
  /**
3
3
  * **流程详情数据缓存**(与摘要缓存并存,互不替代)。
4
4
  *
5
- * 规则(用户 2026-10-26 定稿):
5
+ * 规则(用户 2026-10-26 定稿;条目格式 2026-10-30 升 v2):
6
6
  * - 键:`requestId__lastFeedBackDateTime`(与该条待办的业务时间绑定——流程一动就换键);
7
7
  * - 有效期:7 天,与摘要缓存一致;读的时候逻辑过期,超过清理间隔(3 天)时物理剔除;
8
- * - **不设版本号**:过期条目逐条剔除、新条目直接加入,不做整份失效;
9
- * - 详情为空的条目**也写**(`contextAvailable:false`),否则这类条目每次都要重拉;
8
+ * - **条目带格式版本 `v`**:v2 起素材 = 排除清单过滤后的 formData + 全量 basicInfo;
9
+ * 旧格式条目(无 `v`)读取时视为无效,被新条目逐步覆盖;排除清单变化靠 `excludeTag` 失效;
10
+ * - 详情为空的条目**也写**(`fields: []`),否则这类条目每次都要重拉;
10
11
  * - 缓存文件缺失 ⇒ 视为从未缓存过,正常走接口。
11
12
  *
12
13
  * 位置:与摘要缓存同一个 `cache/` 目录(skill 目录优先,不可写时退回 CLI 状态目录)。
@@ -16,18 +17,53 @@ export const DETAIL_CACHE_FILE_NAME = 'todo-detail-cache.json';
16
17
  export const DETAIL_CACHE_TTL_MS = SUMMARY_CACHE_TTL_MS;
17
18
  /** 与摘要缓存一致:清理间隔 3 天。 */
18
19
  export const DETAIL_CACHE_PRUNE_INTERVAL_MS = SUMMARY_CACHE_PRUNE_INTERVAL_MS;
20
+ /**
21
+ * 缓存条目格式版本。v2(2026-10-30):
22
+ * - 素材改为「排除清单过滤后的 formData `fieldName:value` + 全量 basicInfo」,
23
+ * 取代旧版「selectSummaryFields 投影 + 8 字段/80 字截断」;
24
+ * - 新增 `excludeTag`(排除清单原文拼接)——清单一变旧条目整体失效;
25
+ * - v1 条目(无 `v` 键)读取时直接视为无效并逐步被覆盖。
26
+ */
27
+ export const DETAIL_CACHE_ENTRY_VERSION = 3;
19
28
  export function detailCacheKey(requestId, lastFeedBackDateTime) {
20
29
  const stamp = String(lastFeedBackDateTime ?? '').trim();
21
30
  return `${requestId}__${stamp}`;
22
31
  }
32
+ /**
33
+ * 详情素材特征值(2026-11-01 根治「素材升级后旧摘要继续命中」):对 `basicInfo + fields`
34
+ * 的稳定序列化做 djb2 哈希。任何素材变化——详情管线升级、`excludeOriginFormDataFields`
35
+ * 调整、字段增减——都会让它改变;摘要缓存条目的 `dv` 与之比对即可自动失效重写。
36
+ */
37
+ 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
+ ]);
44
+ let hash = 5381;
45
+ for (let i = 0; i < material.length; i += 1)
46
+ hash = ((hash << 5) + hash + material.charCodeAt(i)) >>> 0;
47
+ return hash.toString(16);
48
+ }
23
49
  function normalizeEntry(raw) {
24
50
  if (!raw || typeof raw !== 'object')
25
51
  return null;
26
52
  const value = raw;
53
+ // 旧格式(v1:context + 8 字段/80 字截断)与新口径不兼容,直接视为无效
54
+ if (value.v !== DETAIL_CACHE_ENTRY_VERSION)
55
+ return null;
27
56
  if (typeof value.cachedAt !== 'number' || !Number.isFinite(value.cachedAt))
28
57
  return null;
29
- if (typeof value.context !== 'string')
58
+ if (typeof value.excludeTag !== 'string')
30
59
  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
+ }
31
67
  const fields = Array.isArray(value.fields)
32
68
  ? value.fields
33
69
  .filter((item) => Boolean(item) && typeof item.fieldName === 'string'
@@ -36,10 +72,11 @@ function normalizeEntry(raw) {
36
72
  : [];
37
73
  const metrics = value.metrics && typeof value.metrics === 'object' ? value.metrics : {};
38
74
  return {
39
- context: value.context,
40
- contextAvailable: value.contextAvailable === true,
41
- metrics,
75
+ v: DETAIL_CACHE_ENTRY_VERSION,
76
+ basicInfo,
42
77
  fields,
78
+ metrics,
79
+ excludeTag: value.excludeTag,
43
80
  cachedAt: value.cachedAt,
44
81
  };
45
82
  }