weaver-work-cli 0.1.36 → 0.1.39

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 (169) hide show
  1. package/README.md +4 -2
  2. package/dist/chunks/{archive-XBVH7JU7.js → archive-ENZYBBME.js} +559 -68
  3. package/dist/chunks/{archive-oa-MZM3EKFQ.js → archive-oa-YWLEC5V4.js} +4 -3
  4. package/dist/chunks/{asset-4XO7UOUW.js → asset-OOV6DWPA.js} +8 -7
  5. package/dist/chunks/asset-idle-RE632MY5.js +670 -0
  6. package/dist/chunks/{attachment-content-VXSWANAC.js → attachment-content-RWN2FCLL.js} +3 -3
  7. package/dist/chunks/{blog-PGZKOPSC.js → blog-PEPOJAL7.js} +5 -4
  8. package/dist/chunks/{caizhilian-BH4ZVGUN.js → caizhilian-WYIQDVPJ.js} +5 -4
  9. package/dist/chunks/{calendar-MXEMTIO4.js → calendar-CIARBAOX.js} +4 -3
  10. package/dist/chunks/{chunk-NCOVUAZK.js → chunk-36DIFZAH.js} +4 -3
  11. package/dist/chunks/{chunk-I6QGIBDM.js → chunk-BJGXMFGD.js} +4 -3
  12. package/dist/chunks/{chunk-TC6MZAO3.js → chunk-CQHGNCK2.js} +52 -2
  13. package/dist/chunks/{chunk-WXFPWCXG.js → chunk-EINRJHQ3.js} +3 -3
  14. package/dist/chunks/{chunk-JUXVYKII.js → chunk-EZGFHJFW.js} +201 -19
  15. package/dist/chunks/{chunk-VPUTVEMC.js → chunk-F2EQXUEE.js} +436 -290
  16. package/dist/chunks/{chunk-JYKABYCV.js → chunk-GVPJZXJ2.js} +3 -3
  17. package/dist/chunks/{chunk-6LLJFKJE.js → chunk-HJYVHXUQ.js} +1 -1
  18. package/dist/chunks/{chunk-T72N74ZT.js → chunk-I2HB737U.js} +5 -4
  19. package/dist/chunks/{chunk-VPXOPQJI.js → chunk-K3RUEEOL.js} +4 -2
  20. package/dist/chunks/{chunk-HXJQI5NM.js → chunk-KTCB5R5H.js} +3 -2
  21. package/dist/chunks/{chunk-XMGSYD6S.js → chunk-L4N7B7ZD.js} +4 -4
  22. package/dist/chunks/{chunk-QYGO5EE2.js → chunk-MUQUHKPG.js} +4 -3
  23. package/dist/chunks/{chunk-QHFANTX7.js → chunk-NLK2RZKG.js} +1348 -90
  24. package/dist/chunks/{chunk-F746NKNF.js → chunk-P2DAISYW.js} +1 -1
  25. package/dist/chunks/{chunk-BPE37FPB.js → chunk-T7JDJJ7H.js} +3 -3
  26. package/dist/chunks/{chunk-JNG42JKG.js → chunk-XXJOSZTQ.js} +6 -6
  27. package/dist/chunks/{chunk-5DQEEGCC.js → chunk-YS5DPN4T.js} +1 -1
  28. package/dist/chunks/{chunk-EG6ZU444.js → chunk-ZUX3P6BQ.js} +10 -5
  29. package/dist/chunks/{chunk-JCSN3JGM.js → chunk-ZXRNHZTE.js} +7 -6
  30. package/dist/chunks/{conflict-JAB2LJEZ.js → conflict-DRMDOL56.js} +2 -2
  31. package/dist/chunks/{contract-XI2EEIZU.js → contract-73MERZWN.js} +4 -3
  32. package/dist/chunks/{cowork-SCPGLBA4.js → cowork-HNGBPD7A.js} +4 -3
  33. package/dist/chunks/{ebuilder-form-EYLHNOTA.js → ebuilder-form-OY6DVNRK.js} +3 -3
  34. package/dist/chunks/{ehr-EAP77IMT.js → ehr-RAQNEQO7.js} +4 -3
  35. package/dist/chunks/{esb-2TGGXEE7.js → esb-6XV7C7YV.js} +4 -4
  36. package/dist/chunks/{fna-2X7ZE7BU.js → fna-XTSRB7G7.js} +4 -3
  37. package/dist/chunks/{host-Q4LZGCRU.js → host-3F5UM5GN.js} +3 -3
  38. package/dist/chunks/{host-XCLDDXKQ.js → host-A7J6GDUP.js} +3 -3
  39. package/dist/chunks/{host-I4NRK4MX.js → host-VGPVP7YF.js} +3 -3
  40. package/dist/chunks/{host-C6EL2RKL.js → host-YAQAPYNP.js} +3 -3
  41. package/dist/chunks/{host-UEI5FGDM.js → host-YMBXWFWX.js} +3 -3
  42. package/dist/chunks/{hrm-YXERJM76.js → hrm-TLHKC545.js} +3 -3
  43. package/dist/chunks/{im-A4VZN2TQ.js → im-YOUCFMUK.js} +3 -3
  44. package/dist/chunks/{invoice-2Z6YPMIS.js → invoice-WNBS2B4C.js} +3 -3
  45. package/dist/chunks/{jiuchuanhui-AR5GMKZB.js → jiuchuanhui-XABWS5RN.js} +4 -3
  46. package/dist/chunks/{lazy-dispatch-QXW7P4X3.js → lazy-dispatch-NLZ5UQQI.js} +5 -5
  47. package/dist/chunks/{mail-7GZ6CPBJ.js → mail-LRYMBFNL.js} +4 -3
  48. package/dist/chunks/{meeting-3NMGA3MB.js → meeting-ESXL6MFI.js} +13 -12
  49. package/dist/chunks/{okr-BZ6XUOAG.js → okr-EOV65SSF.js} +4 -3
  50. package/dist/chunks/{plan-DR5CMUYP.js → plan-YFPVKW6Y.js} +4 -3
  51. package/dist/chunks/{project-6J63MK33.js → project-MDVZUYQW.js} +4 -3
  52. package/dist/chunks/{registry-VRTNC6UR.js → registry-QCLZEDAP.js} +8 -8
  53. package/dist/chunks/{registry-KDD7DHNK.js → registry-VT5Y2HLA.js} +4 -4
  54. package/dist/chunks/{registry-TKX2E7C7.js → registry-YX4CSEMD.js} +3 -3
  55. package/dist/chunks/{room-recommend-XCWRMB5P.js → room-recommend-L3MWH4JW.js} +4 -4
  56. package/dist/chunks/{schedule-IVBICP32.js → schedule-UASAVDVD.js} +4 -4
  57. package/dist/chunks/{workflow-D74NFK3C.js → workflow-KDGIUCNW.js} +9 -6
  58. package/dist/core/output.js +16 -6
  59. package/dist/index.js +33 -28
  60. package/dist/internal/e10/auth/messages.js +1 -0
  61. package/dist/internal/e10/auth-recovery.js +127 -7
  62. package/dist/internal/e10/client.js +16 -2
  63. package/dist/internal/e10/context.js +16 -3
  64. package/dist/internal/e10/request-runtime.js +106 -8
  65. package/dist/shortcuts/archive/host.js +5 -2
  66. package/dist/shortcuts/archive/manifest.js +1 -1
  67. package/dist/shortcuts/archive/operations/borrow-list.js +13 -1
  68. package/dist/shortcuts/archive/operations/flow-url.js +1 -1
  69. package/dist/shortcuts/archive/operations/prelib.js +472 -0
  70. package/dist/shortcuts/archive/operations/registry.js +2 -0
  71. package/dist/shortcuts/archive/operations/upload.js +1 -1
  72. package/dist/shortcuts/archive-oa/host.js +2 -1
  73. package/dist/shortcuts/asset/host.js +6 -5
  74. package/dist/shortcuts/asset-idle/errors.js +44 -0
  75. package/dist/shortcuts/asset-idle/host.js +161 -0
  76. package/dist/shortcuts/asset-idle/index.js +114 -0
  77. package/dist/shortcuts/asset-idle/manifest.js +159 -0
  78. package/dist/shortcuts/asset-idle/operations/idle.js +47 -0
  79. package/dist/shortcuts/asset-idle/operations/registry.js +33 -0
  80. package/dist/shortcuts/asset-idle/operations/shared.js +142 -0
  81. package/dist/shortcuts/asset-idle/operations/types.js +9 -0
  82. package/dist/shortcuts/asset-idle/operations.js +15 -0
  83. package/dist/shortcuts/blog/host.js +3 -2
  84. package/dist/shortcuts/caizhilian/host.js +3 -2
  85. package/dist/shortcuts/calendar/host.js +2 -1
  86. package/dist/shortcuts/contract/host.js +2 -1
  87. package/dist/shortcuts/cowork/host.js +2 -1
  88. package/dist/shortcuts/ebuilder-form/host.js +3 -1
  89. package/dist/shortcuts/ehr/host.js +2 -1
  90. package/dist/shortcuts/esb/host.js +2 -1
  91. package/dist/shortcuts/fna/host.js +2 -1
  92. package/dist/shortcuts/hrm/host.js +2 -1
  93. package/dist/shortcuts/im/host.js +3 -2
  94. package/dist/shortcuts/index.js +5 -0
  95. package/dist/shortcuts/invoice/host.js +5 -4
  96. package/dist/shortcuts/jiuchuanhui/host.js +2 -1
  97. package/dist/shortcuts/mail/host.js +2 -1
  98. package/dist/shortcuts/meeting/host.js +3 -2
  99. package/dist/shortcuts/meeting/invitation/text.js +71 -0
  100. package/dist/shortcuts/meeting/operations/eb/create-flow.js +1073 -50
  101. package/dist/shortcuts/meeting/operations/eb/crm-search.js +478 -0
  102. package/dist/shortcuts/meeting/operations/eb/factory.js +60 -1
  103. package/dist/shortcuts/meeting/operations/eb/index.js +2 -0
  104. package/dist/shortcuts/meeting/operations/invitation.js +122 -9
  105. package/dist/shortcuts/okr/host.js +2 -1
  106. package/dist/shortcuts/plan/host.js +2 -1
  107. package/dist/shortcuts/project/host.js +2 -1
  108. package/dist/shortcuts/workflow/host.js +3 -0
  109. package/dist/shortcuts/workflow/index.js +1 -1
  110. package/dist/shortcuts/workflow/operations/form.js +44 -11
  111. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupRules.js +77 -26
  112. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupStore.js +6 -0
  113. package/dist/shortcuts/workflow/operations/todoSmartConvert/summaryCache.js +59 -117
  114. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailCache.js +44 -7
  115. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailFetcher.js +37 -29
  116. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +20 -6
  117. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.detail.js +22 -18
  118. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +34 -10
  119. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +4 -2
  120. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +9 -2
  121. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +110 -20
  122. package/docs/_catalog.md +3 -0
  123. package/docs/asset-idle.md +45 -0
  124. package/docs/e10-auth.md +1 -1
  125. package/docs/todo-smart-convert/subgroup-rules.md +1 -1
  126. package/package.json +1 -1
  127. package/scripts/package-skill.mjs +2 -0
  128. package/skill-template/business-info.json +14 -1
  129. package/skill-template/domains/asset-idle.md +13 -0
  130. package/skill-template/domains/wenshuding.md +3 -0
  131. package/skills/weaver-e10-archive-oa/SKILL.md +1 -0
  132. package/skills/weaver-e10-archive-oa/references/my-borrow.md +49 -0
  133. package/skills/weaver-e10-asset-idle/SKILL.md +73 -0
  134. package/skills/weaver-e10-asset-idle/references/idle-list.md +62 -0
  135. package/skills/weaver-e10-asset-idle/references/idle-stats-and-charts.md +65 -0
  136. package/skills/weaver-e10-asset-idle/references/source-manifest.json +193 -0
  137. package/skills/weaver-e10-asset-idle/references/widget-style.md +205 -0
  138. package/skills/weaver-e10-caizhilian/SKILL.md +29 -11
  139. package/skills/weaver-e10-caizhilian/references/auth-and-install.md +12 -0
  140. package/skills/weaver-e10-caizhilian/references/files.md +1 -1
  141. package/skills/weaver-e10-caizhilian/references/setting-encrypt.md +1 -1
  142. package/skills/weaver-e10-caizhilian/references/source-manifest.json +45 -45
  143. package/skills/weaver-e10-cowork/SKILL.md +2 -0
  144. package/skills/weaver-e10-jucailin/SKILL.md +2 -0
  145. package/skills/weaver-e10-jucailin/references/recruit-read.md +2 -0
  146. package/skills/weaver-e10-jucailin/references/recruit-write.md +1 -1
  147. package/skills/weaver-e10-mail/SKILL.md +3 -0
  148. package/skills/weaver-e10-meeting/SKILL.md +250 -301
  149. package/skills/weaver-e10-meeting/references/meeting-eb.md +203 -0
  150. package/skills/weaver-e10-meeting/references/meeting-invitation.md +37 -5
  151. package/skills/weaver-e10-meeting.zip +0 -0
  152. package/skills/weaver-e10-okr/references/source-manifest.json +3 -3
  153. package/skills/weaver-e10-shared/SKILL.md +5 -5
  154. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +3 -3
  155. package/skills/weaver-e10-wenshuding/SKILL.md +29 -6
  156. package/skills/weaver-e10-wenshuding/references/archive-borrow-list.md +14 -0
  157. package/skills/weaver-e10-wenshuding/references/archive-download.md +2 -0
  158. package/skills/weaver-e10-wenshuding/references/archive-flow.md +1 -1
  159. package/skills/weaver-e10-wenshuding/references/archive-prelib-arrange.md +76 -0
  160. package/skills/weaver-e10-wenshuding/references/archive-prelib-list.md +107 -0
  161. package/skills/weaver-e10-wenshuding/references/archive-prelib-open.md +59 -0
  162. package/skills/weaver-e10-wenshuding/references/archive-upload.md +4 -1
  163. package/skills/weaver-e10-wenshuding/references/source-manifest.json +108 -65
  164. package/skills/weaver-e10-workflow/SKILL.md +5 -2
  165. package/skills/weaver-e10-workflow/aigroup/default.json +154 -19
  166. package/skills/weaver-e10-workflow/references/todo-smart-convert.md +59 -27
  167. package/skills/weaver-e10-workflow/tools/todo-serve.cjs +113 -25
  168. package/skills/weaver-e10-workflow/tools/todo-summary.cjs +292 -42
  169. package/skills/weaver-e10-yimiaoban/references/source-manifest.json +3 -3
@@ -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,28 @@ 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
+ * - 不传 `maxChars` 时不按字数过滤;传入后必须与写入口径一致;
129
+ * - `pk`(口径特征值)同理:规则改了 `requestSummaryPrompt` ⇒ pk 变化 ⇒ 旧摘要自动失效;
130
+ * - `dv`(详情素材特征值,2026-11-01 根治):
131
+ * 条目没有 dv = 旧条目,兼容命中;
132
+ * 条目有 dv 时必须与当前素材特征值一致——当前**没有**详情素材(无法核验)也视为失效
133
+ * (与「缓存没了就当从未生成过、直接走实时逻辑」的口径一致)。
206
134
  */
207
- function entryMatchesMaxChars(entry, maxChars) {
208
- if (maxChars === undefined)
209
- return true;
210
- return (entry.maxChars ?? 50) === maxChars;
135
+ function entryMatches(entry, maxChars, pk, dv) {
136
+ if (maxChars !== undefined && (entry.maxChars ?? 50) !== maxChars)
137
+ return false;
138
+ if (pk !== undefined && (entry.pk ?? '') !== pk)
139
+ return false;
140
+ if (entry.dv !== undefined && entry.dv !== dv)
141
+ return false;
142
+ return true;
211
143
  }
212
144
  function normalizeSummaryEntry(raw) {
213
145
  if (!raw || typeof raw !== 'object')
@@ -218,7 +150,15 @@ function normalizeSummaryEntry(raw) {
218
150
  if (typeof entry.cachedAt !== 'number' || !Number.isFinite(entry.cachedAt))
219
151
  return null;
220
152
  const maxChars = typeof entry.maxChars === "number" && Number.isFinite(entry.maxChars) ? entry.maxChars : undefined;
221
- return { summary: entry.summary, cachedAt: entry.cachedAt, ...(maxChars === undefined ? {} : { maxChars }) };
153
+ const pk = typeof entry.pk === 'string' && entry.pk !== '' ? entry.pk : undefined;
154
+ const dv = typeof entry.dv === 'string' && entry.dv !== '' ? entry.dv : undefined;
155
+ return {
156
+ summary: entry.summary,
157
+ cachedAt: entry.cachedAt,
158
+ ...(maxChars === undefined ? {} : { maxChars }),
159
+ ...(pk === undefined ? {} : { pk }),
160
+ ...(dv === undefined ? {} : { dv }),
161
+ };
222
162
  }
223
163
  /**
224
164
  * 打开摘要缓存(读 + 按需清理)。
@@ -273,21 +213,21 @@ export async function openSummaryCache(ctx) {
273
213
  misses: 0,
274
214
  pruned,
275
215
  dirty: false,
276
- get(requestId, lastFeedBackDateTime, maxChars) {
216
+ get(requestId, lastFeedBackDateTime, maxChars, pk, dv) {
277
217
  const key = summaryCacheKey(requestId, lastFeedBackDateTime);
278
218
  const entry = entries.get(key);
279
- if (entry && now - entry.cachedAt <= SUMMARY_CACHE_TTL_MS && entryMatchesMaxChars(entry, maxChars)) {
219
+ if (entry && now - entry.cachedAt <= SUMMARY_CACHE_TTL_MS && entryMatches(entry, maxChars, pk, dv)) {
280
220
  cache.hits += 1;
281
221
  return entry.summary;
282
222
  }
283
223
  cache.misses += 1;
284
224
  return null;
285
225
  },
286
- has(requestId, lastFeedBackDateTime, maxChars) {
226
+ has(requestId, lastFeedBackDateTime, maxChars, pk, dv) {
287
227
  const entry = entries.get(summaryCacheKey(requestId, lastFeedBackDateTime));
288
- return entry !== undefined && entryMatchesMaxChars(entry, maxChars);
228
+ return entry !== undefined && entryMatches(entry, maxChars, pk, dv);
289
229
  },
290
- set(requestId, lastFeedBackDateTime, summary, maxChars) {
230
+ set(requestId, lastFeedBackDateTime, summary, maxChars, pk, dv) {
291
231
  const text = String(summary ?? '').trim();
292
232
  if (!requestId || !text)
293
233
  return false;
@@ -295,6 +235,8 @@ export async function openSummaryCache(ctx) {
295
235
  summary: text,
296
236
  cachedAt: Date.now(),
297
237
  ...(maxChars === undefined ? {} : { maxChars }),
238
+ ...(pk === undefined || pk === '' ? {} : { pk }),
239
+ ...(dv === undefined || dv === '' ? {} : { dv }),
298
240
  });
299
241
  dirty = true;
300
242
  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
  }
@@ -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 实体 → 字符。`&ldquo;` 这类 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
  });
@@ -4,7 +4,8 @@ import { saveSearchIndex } from '../../state.js';
4
4
  import { buildSearchQuery } from '../read.js';
5
5
  import { projectDisplayRows } from './todoListRows.js';
6
6
  import { responseData } from '../shared.js';
7
- import { openSummaryCache, SUMMARY_CACHE_FILE_NAME } from './summaryCache.js';
7
+ import { openSummaryCache, promptTagOf, SUMMARY_CACHE_FILE_NAME } from './summaryCache.js';
8
+ import { openDetailCache, detailMaterialTagOf } from './todoDetailCache.js';
8
9
  import { fetchDetailFacts, stampsOfRows } from './todoSmartConvert.detail.js';
9
10
  import { GROUP_MIN_ITEMS_FOR_SUBGROUP, isExpiringTodayRow, isOverdueRow, runGroup, } from './todoSmartConvert.group.js';
10
11
  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';
@@ -144,11 +145,24 @@ export async function collectRawTodos(args, env) {
144
145
  wfIdGroup = (grouped.wfIdGroup ?? []);
145
146
  // 摘要缓存 key 为 requestId 与最后反馈时间;反馈变化会自然失效。
146
147
  const summaryCache = await openSummaryCache(env.ctx);
147
- // 调用方指定摘要字数时不能命中其他字数口径的旧条目。
148
- const collectSummaryMaxChars = readBoundedInt(args.summaryMaxChars, "summaryMaxChars", DEFAULT_REQUEST_SUMMARY_MAX, 10, 500);
148
+ // 字数口径按每条所属工作流的规则算(入参 > 规则 summaryMaxChars > 兜底),
149
+ // 否则不同口径的旧摘要会被错命中/错判失效。
150
+ const collectMaxCharsByGroup = new Map();
151
+ const collectMaxCharsFor = (workflowName) => {
152
+ const key = workflowName ?? '';
153
+ const hit = collectMaxCharsByGroup.get(key);
154
+ if (hit !== undefined)
155
+ return hit;
156
+ const value = readBoundedInt(args.summaryMaxChars ?? activeRuleFor(key)?.summaryMaxChars, "summaryMaxChars", DEFAULT_REQUEST_SUMMARY_MAX, 10, 500);
157
+ collectMaxCharsByGroup.set(key, value);
158
+ return value;
159
+ };
149
160
  const cachedSummaryIds = new Set();
161
+ // 摘要条目的素材特征值:详情素材变了 ⇒ 旧摘要失效、重新走 AI(2026-11-01 根治)
162
+ const detailCacheForTag = await openDetailCache(env.ctx);
150
163
  for (const row of items) {
151
- const cached = summaryCache.get(row.requestId, row.lastFeedBackDateTime, collectSummaryMaxChars);
164
+ const detailEntry = detailCacheForTag.get(row.requestId, row.lastFeedBackDateTime);
165
+ const cached = summaryCache.get(row.requestId, row.lastFeedBackDateTime, collectMaxCharsFor(row.workflowName), promptTagOf(activeRuleFor(row.workflowName ?? '')?.requestSummaryPrompt ?? ''), detailEntry ? detailMaterialTagOf(detailEntry) : undefined);
152
166
  if (!cached)
153
167
  continue;
154
168
  row.summary = cached;
@@ -237,8 +251,8 @@ export async function collectRawTodos(args, env) {
237
251
  + `(缓存文件:${summaryCache.file ?? SUMMARY_CACHE_FILE_NAME})`);
238
252
  }
239
253
  if (!summaryCache.inSkillDir) {
240
- warnings.push('没有找到本机 Agent 的 weaver-e10-workflow skill 目录,摘要缓存已退回 CLI 状态目录;'
241
- + '如需缓存落在 skill 目录,可用 WEAVER_WORK_CLI_SKILL_DIR 指定');
254
+ warnings.push('无当前 skill 上下文(WEAVER_WORK_CLI_SKILL_DIR 未设置):摘要缓存与 aigroup 规则均未读写,'
255
+ + '本次按「从未生成过」走实时逻辑;请经 skill 的 tools 脚本调用');
242
256
  }
243
257
  }
244
258
  return {
@@ -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
  }