weaver-work-cli 0.1.32 → 0.1.36

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 (211) hide show
  1. package/README.md +2 -2
  2. package/dist/chunks/{archive-UMGX72K6.js → archive-XBVH7JU7.js} +2 -2
  3. package/dist/chunks/{archive-oa-N2IYBLF3.js → archive-oa-MZM3EKFQ.js} +2 -2
  4. package/dist/chunks/{asset-GLEGKFG5.js → asset-4XO7UOUW.js} +2 -2
  5. package/dist/chunks/{attachment-content-AQNX5N3S.js → attachment-content-VXSWANAC.js} +3 -2
  6. package/dist/chunks/{blog-E44OTVDA.js → blog-PGZKOPSC.js} +2 -2
  7. package/dist/chunks/{caizhilian-DQUFZXZL.js → caizhilian-BH4ZVGUN.js} +2 -2
  8. package/dist/chunks/{calendar-JKPIRKFD.js → calendar-MXEMTIO4.js} +2 -2
  9. package/dist/chunks/{chunk-QWY6PGL6.js → chunk-2LV74IWP.js} +361 -56
  10. package/dist/chunks/chunk-3SO6QL6U.js +73 -0
  11. package/dist/chunks/chunk-5DQEEGCC.js +229 -0
  12. package/dist/chunks/{chunk-G3QJULO2.js → chunk-6LLJFKJE.js} +1 -1
  13. package/dist/chunks/{chunk-C6R3B2ML.js → chunk-BPE37FPB.js} +35 -7
  14. package/dist/chunks/chunk-BYATLLEF.js +68 -0
  15. package/dist/chunks/chunk-BYBSCN4X.js +932 -0
  16. package/dist/chunks/{chunk-CVZ2JOSW.js → chunk-F746NKNF.js} +1 -1
  17. package/dist/chunks/{chunk-GCG2NMWR.js → chunk-HXJQI5NM.js} +4 -2
  18. package/dist/chunks/{chunk-YRVDW4WN.js → chunk-I6QGIBDM.js} +2 -2
  19. package/dist/chunks/{chunk-H7J4DC5R.js → chunk-JCSN3JGM.js} +2 -2
  20. package/dist/chunks/{chunk-2GA5ZIQL.js → chunk-JNG42JKG.js} +30 -1
  21. package/dist/chunks/{chunk-DW6AUUKS.js → chunk-JUXVYKII.js} +9 -4
  22. package/dist/chunks/{chunk-6RKD4DRZ.js → chunk-JYKABYCV.js} +2 -2
  23. package/dist/chunks/{chunk-6EFWDL7Y.js → chunk-NCOVUAZK.js} +2 -2
  24. package/dist/chunks/{chunk-E4Z23GHM.js → chunk-QHFANTX7.js} +1114 -904
  25. package/dist/chunks/{ebuilder-form-V5FG2EHC.js → chunk-QYGO5EE2.js} +22 -1148
  26. package/dist/chunks/{chunk-IURUFGZK.js → chunk-T72N74ZT.js} +50 -33
  27. package/dist/chunks/{chunk-H6XSTSLM.js → chunk-TC6MZAO3.js} +137 -204
  28. package/dist/chunks/{chunk-WKI2FBIQ.js → chunk-VPUTVEMC.js} +737 -341
  29. package/dist/chunks/{chunk-WSKBSQ2U.js → chunk-VPXOPQJI.js} +2 -2
  30. package/dist/chunks/chunk-WXFPWCXG.js +774 -0
  31. package/dist/chunks/{chunk-4YC43QQ4.js → chunk-XMGSYD6S.js} +4 -4
  32. package/dist/chunks/conflict-JAB2LJEZ.js +21 -0
  33. package/dist/chunks/continuation-RN6PZN2M.js +11 -0
  34. package/dist/chunks/contract-XI2EEIZU.js +9110 -0
  35. package/dist/chunks/cowork-SCPGLBA4.js +2527 -0
  36. package/dist/chunks/ebuilder-form-EYLHNOTA.js +121 -0
  37. package/dist/chunks/{ehr-HMFY6VIL.js → ehr-EAP77IMT.js} +1344 -27
  38. package/dist/chunks/{esb-ZYSVB7SG.js → esb-2TGGXEE7.js} +4 -4
  39. package/dist/chunks/{fna-B7RKPAZD.js → fna-2X7ZE7BU.js} +13 -8
  40. package/dist/chunks/{host-OCH2BCCV.js → host-C6EL2RKL.js} +3 -3
  41. package/dist/chunks/{host-45WED2KK.js → host-I4NRK4MX.js} +3 -3
  42. package/dist/chunks/{host-I4Z6BFYX.js → host-Q4LZGCRU.js} +3 -3
  43. package/dist/chunks/host-UEI5FGDM.js +23 -0
  44. package/dist/chunks/{host-KNZ52LJZ.js → host-XCLDDXKQ.js} +3 -3
  45. package/dist/chunks/{hrm-2KAZGUHI.js → hrm-YXERJM76.js} +3 -3
  46. package/dist/chunks/{im-ACJJWH4U.js → im-A4VZN2TQ.js} +88 -12
  47. package/dist/chunks/{invoice-ZK3WCX2U.js → invoice-2Z6YPMIS.js} +3 -3
  48. package/dist/chunks/{jiuchuanhui-5PQD3IL2.js → jiuchuanhui-AR5GMKZB.js} +2 -2
  49. package/dist/chunks/{lazy-dispatch-JQXBW3GM.js → lazy-dispatch-QXW7P4X3.js} +4 -4
  50. package/dist/chunks/{mail-S2HDDXO2.js → mail-7GZ6CPBJ.js} +45 -24
  51. package/dist/chunks/{meeting-QVN3I37L.js → meeting-3NMGA3MB.js} +278 -43
  52. package/dist/chunks/{okr-QNG4VC2Q.js → okr-BZ6XUOAG.js} +2 -2
  53. package/dist/chunks/operations-5QL3AECO.js +11 -0
  54. package/dist/chunks/{plan-A25EMW6U.js → plan-DR5CMUYP.js} +2 -2
  55. package/dist/chunks/{project-G6XV66DZ.js → project-6J63MK33.js} +2 -2
  56. package/dist/chunks/{registry-DC6KR3NH.js → registry-KDD7DHNK.js} +4 -4
  57. package/dist/chunks/{registry-2VLUAESA.js → registry-OZSMDVPP.js} +2 -2
  58. package/dist/chunks/{registry-7C5UMIPL.js → registry-TKX2E7C7.js} +2 -2
  59. package/dist/chunks/{registry-TI3CIUXF.js → registry-VRTNC6UR.js} +8 -6
  60. package/dist/chunks/{room-recommend-WRTHO6Y2.js → room-recommend-XCWRMB5P.js} +4 -3
  61. package/dist/chunks/{schedule-KFIITGSK.js → schedule-IVBICP32.js} +4 -3
  62. package/dist/chunks/{workflow-DJ7SYWAF.js → workflow-D74NFK3C.js} +179 -6
  63. package/dist/index.js +35 -25
  64. package/dist/internal/e10/auth-recovery.js +14 -4
  65. package/dist/internal/e10/fetch-timing.js +31 -0
  66. package/dist/internal/e10/request-runtime.js +5 -0
  67. package/dist/shortcuts/contract/host.js +34 -59
  68. package/dist/shortcuts/cowork/continuation.js +70 -0
  69. package/dist/shortcuts/cowork/errors.js +59 -0
  70. package/dist/shortcuts/cowork/host.js +180 -0
  71. package/dist/shortcuts/cowork/index.js +166 -0
  72. package/dist/shortcuts/cowork/manifest.js +65 -0
  73. package/dist/shortcuts/cowork/operations/discuss-write.js +277 -0
  74. package/dist/shortcuts/cowork/operations/item-write.js +380 -0
  75. package/dist/shortcuts/cowork/operations/member-write.js +283 -0
  76. package/dist/shortcuts/cowork/operations/permission-write.js +286 -0
  77. package/dist/shortcuts/cowork/operations/read.js +423 -0
  78. package/dist/shortcuts/cowork/operations/registry.js +39 -0
  79. package/dist/shortcuts/cowork/operations/review-write.js +157 -0
  80. package/dist/shortcuts/cowork/operations/shared.js +215 -0
  81. package/dist/shortcuts/cowork/operations/types.js +39 -0
  82. package/dist/shortcuts/cowork/operations.js +19 -0
  83. package/dist/shortcuts/ehr/host.js +4 -1
  84. package/dist/shortcuts/ehr/operations/recruit-candidate-import.js +757 -0
  85. package/dist/shortcuts/ehr/operations/recruit-position.js +367 -0
  86. package/dist/shortcuts/ehr/operations/recruit-stats.js +133 -0
  87. package/dist/shortcuts/ehr/operations/recruit-write.js +43 -16
  88. package/dist/shortcuts/ehr/operations/recruit.js +182 -10
  89. package/dist/shortcuts/ehr/operations/registry.js +6 -0
  90. package/dist/shortcuts/fna/operations/expense-chart.js +19 -7
  91. package/dist/shortcuts/im/diagnostics.js +97 -0
  92. package/dist/shortcuts/im/host.js +58 -31
  93. package/dist/shortcuts/im/index.js +7 -2
  94. package/dist/shortcuts/im/operations/at-mention.js +127 -0
  95. package/dist/shortcuts/im/operations/file-user.js +10 -2
  96. package/dist/shortcuts/im/operations/group-read.js +26 -5
  97. package/dist/shortcuts/im/operations/group-write.js +35 -28
  98. package/dist/shortcuts/im/operations/msg-sync.js +7 -2
  99. package/dist/shortcuts/im/operations/msg-write.js +76 -10
  100. package/dist/shortcuts/im/operations/quote-mention.js +119 -0
  101. package/dist/shortcuts/im/operations/session.js +1 -1
  102. package/dist/shortcuts/im/operations/shared.js +10 -0
  103. package/dist/shortcuts/im/operations/types.js +13 -0
  104. package/dist/shortcuts/index.js +10 -0
  105. package/dist/shortcuts/mail/host.js +12 -1
  106. package/dist/shortcuts/mail/operations/types.js +17 -2
  107. package/dist/shortcuts/meeting/host.js +27 -34
  108. package/dist/shortcuts/meeting/index.js +79 -1
  109. package/dist/shortcuts/meeting/manifest.js +10 -0
  110. package/dist/shortcuts/meeting/operations/create.js +4 -1
  111. package/dist/shortcuts/meeting/operations/eb/catalog.js +10 -0
  112. package/dist/shortcuts/meeting/operations/eb/conflict.js +84 -8
  113. package/dist/shortcuts/meeting/operations/eb/create-flow.js +1195 -140
  114. package/dist/shortcuts/meeting/operations/eb/factory.js +170 -8
  115. package/dist/shortcuts/meeting/operations/eb/index.js +16 -3
  116. package/dist/shortcuts/meeting/operations/eb/room-recommend.js +50 -5
  117. package/dist/shortcuts/meeting/operations/eb-fallback.js +163 -0
  118. package/dist/shortcuts/meeting/operations/env.js +99 -46
  119. package/dist/shortcuts/meeting/operations/form-fields.js +110 -0
  120. package/dist/shortcuts/meeting/operations/invitation.js +79 -20
  121. package/dist/shortcuts/meeting/operations/registry.js +2 -0
  122. package/dist/shortcuts/meeting/operations/standard-guard.js +71 -0
  123. package/dist/shortcuts/meeting/operations.js +16 -0
  124. package/dist/shortcuts/workflow/endpoints.js +8 -3
  125. package/dist/shortcuts/workflow/index.js +110 -1
  126. package/dist/shortcuts/workflow/operations/form.js +204 -36
  127. package/dist/shortcuts/workflow/operations/read.js +104 -23
  128. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupRules.js +448 -0
  129. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupStore.js +106 -0
  130. package/dist/shortcuts/workflow/operations/todoSmartConvert/businessRules.js +47 -0
  131. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +30 -23
  132. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.core.js +1 -0
  133. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.group.js +1 -0
  134. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +2 -1
  135. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +2 -4
  136. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +138 -181
  137. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +12 -7
  138. package/dist/shortcuts/workflow/operations.js +23 -1
  139. package/docs/e10-auth.md +1 -1
  140. package/docs/ehr.md +1 -1
  141. package/docs/fna.md +2 -1
  142. package/docs/im.md +8 -1
  143. package/docs/todo-smart-convert/README.md +1 -1
  144. package/docs/todo-smart-convert/data-contract.md +2 -2
  145. package/docs/todo-smart-convert/subgroup-rules.md +6 -0
  146. package/package.json +1 -1
  147. package/skill-template/business-info.json +18 -0
  148. package/skill-template/domains/cowork.md +14 -0
  149. package/skill-template/domains/yimiaoban.md +3 -3
  150. package/skill-template/skill-template.md +1 -1
  151. package/skills/weaver-e10-blog/SKILL.md +5 -5
  152. package/skills/weaver-e10-caizhilian/SKILL.md +5 -5
  153. package/skills/weaver-e10-calendar/SKILL.md +5 -5
  154. package/skills/weaver-e10-cowork/SKILL.md +101 -0
  155. package/skills/weaver-e10-cowork/references/cowork-read.md +90 -0
  156. package/skills/weaver-e10-cowork/references/cowork-write.md +131 -0
  157. package/skills/weaver-e10-cowork/references/safety-boundaries.md +45 -0
  158. package/skills/weaver-e10-esb/SKILL.md +5 -5
  159. package/skills/weaver-e10-hrm/SKILL.md +5 -5
  160. package/skills/weaver-e10-jinchengda/SKILL.md +2 -2
  161. package/skills/weaver-e10-jiuchuanhui/SKILL.md +5 -5
  162. package/skills/weaver-e10-jucailin/SKILL.md +38 -14
  163. package/skills/weaver-e10-jucailin/references/recruit-create.md +104 -0
  164. package/skills/weaver-e10-jucailin/references/recruit-read.md +25 -0
  165. package/skills/weaver-e10-jucailin/references/recruit-write.md +13 -0
  166. package/skills/weaver-e10-jucailin/references/salary.md +15 -11
  167. package/skills/weaver-e10-mail/SKILL.md +17 -14
  168. package/skills/weaver-e10-meeting/SKILL.md +442 -46
  169. package/skills/weaver-e10-meeting/references/meeting-eb.md +135 -3
  170. package/skills/weaver-e10-meeting/references/meeting-invitation.md +28 -2
  171. package/skills/weaver-e10-meeting/references/meeting-queries.md +9 -6
  172. package/skills/weaver-e10-meeting/references/meeting-verification.md +8 -6
  173. package/skills/weaver-e10-meeting.zip +0 -0
  174. package/skills/weaver-e10-okr/SKILL.md +5 -5
  175. package/skills/weaver-e10-okr/references/source-manifest.json +3 -3
  176. package/skills/weaver-e10-plan/SKILL.md +5 -5
  177. package/skills/weaver-e10-qiyecheng/SKILL.md +5 -2
  178. package/skills/weaver-e10-qiyecheng/references/catalog.md +2 -2
  179. package/skills/weaver-e10-qiyecheng/references/expense-chart.md +15 -1
  180. package/skills/weaver-e10-qiyecheng/references/source-manifest.json +22 -21
  181. package/skills/weaver-e10-shared/SKILL.md +12 -12
  182. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +4 -4
  183. package/skills/weaver-e10-wenshuding/SKILL.md +5 -5
  184. package/skills/weaver-e10-workflow/SKILL.md +29 -5
  185. package/skills/weaver-e10-workflow/aigroup/default.json +88 -0
  186. package/skills/weaver-e10-workflow/references/todo-smart-convert.md +66 -14
  187. package/skills/weaver-e10-workflow/references/workflow-agent-entry.md +1 -1
  188. package/skills/weaver-e10-workflow/references/workflow-memory.md +1 -1
  189. package/skills/weaver-e10-workflow/references/workflow-query.md +12 -10
  190. package/skills/weaver-e10-workflow/references/workflow-view.md +2 -2
  191. package/skills/weaver-e10-workflow/tools/todo-serve.cjs +239 -0
  192. package/skills/weaver-e10-workflow/{scripts → tools}/todo-summary.cjs +218 -28
  193. package/skills/weaver-e10-yepiaotong/SKILL.md +10 -8
  194. package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +7 -7
  195. package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +10 -10
  196. package/skills/weaver-e10-yepiaotong/references/reim-examples.md +40 -37
  197. package/skills/weaver-e10-yepiaotong/references/reim-expense-subject-rules.md +4 -4
  198. package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +27 -38
  199. package/skills/weaver-e10-yepiaotong/references/reim-phone-subsidy-rules.md +4 -4
  200. package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +37 -29
  201. package/skills/weaver-e10-yimiaoban/SKILL.md +156 -33
  202. package/skills/weaver-e10-yimiaoban/references/ding-write.md +1 -1
  203. package/skills/weaver-e10-yimiaoban/references/error-codes.md +5 -1
  204. package/skills/weaver-e10-yimiaoban/references/group-read.md +5 -1
  205. package/skills/weaver-e10-yimiaoban/references/group-write.md +6 -4
  206. package/skills/weaver-e10-yimiaoban/references/msg-read.md +1 -0
  207. package/skills/weaver-e10-yimiaoban/references/msg-write.md +34 -2
  208. package/skills/weaver-e10-yimiaoban/references/safety-boundaries.md +5 -2
  209. package/skills/weaver-e10-yimiaoban/references/source-manifest.json +79 -58
  210. package/skills/weaver-e10-yimiaoban/references/user-remind.md +2 -2
  211. package/skills/weaver-e10-ziguanjia/SKILL.md +5 -5
@@ -0,0 +1,239 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * todo-serve.cjs —— 待办汇总载荷的本地 GET 数据源(单进程多快照版,2026-10-29)。
4
+ *
5
+ * 设计:
6
+ * 一个常驻进程,按 token 路由到文件系统里的快照目录,进程数恒为 1、端口恒定。
7
+ *
8
+ * ~/.weaver-work-cli/serve/
9
+ * ├── current.json # { pid, port, version, startedAt }
10
+ * └── snapshots/<token>/todo.json
11
+ *
12
+ * GET /s/<token>/todo.json -> 该快照的三键数据
13
+ * GET /todo.json?token=<token> -> 同上(兼容旧链接形态)
14
+ * GET /health -> 存活信息与快照数
15
+ *
16
+ * 生命周期:
17
+ * - 快照各自按目录 mtime 计空闲,超时即删(请求命中过期快照也会顺手删掉)。
18
+ * - 所有快照都过期且全局无请求超过 idle 分钟 -> 进程退出并删 current.json。
19
+ *
20
+ * 为什么必须自己写这个服务(而不是 python -m http.server / http-server):
21
+ * https 页面取 http://127.0.0.1 会先发 OPTIONS 私有网络预检,
22
+ * 必须回 `Access-Control-Allow-Private-Network: true` 且回显 Origin,否则浏览器直接拦
23
+ * (python 的 http.server 对 OPTIONS 直接 501)。
24
+ */
25
+ const http = require('node:http');
26
+ const fs = require('node:fs');
27
+ const os = require('node:os');
28
+ const path = require('node:path');
29
+
30
+ function parseArgs(argv) {
31
+ const out = {};
32
+ for (let i = 0; i < argv.length; i += 1) {
33
+ const token = argv[i];
34
+ if (!token.startsWith('--')) continue;
35
+ const key = token.slice(2);
36
+ const next = argv[i + 1];
37
+ if (next === undefined || next.startsWith('--')) out[key] = true;
38
+ else { out[key] = next; i += 1; }
39
+ }
40
+ return out;
41
+ }
42
+
43
+ /** 与服务端约定一致的版本号:客户端只复用同版本实例。 */
44
+ const SERVE_VERSION = '3';
45
+
46
+ const args = parseArgs(process.argv.slice(2));
47
+ // 默认放系统临时目录:快照只有 30 分钟寿命,本就不该写进家目录;
48
+ // 而且宿主沙箱对家目录的删除操作更严(unlink/rmdir 会被 EPERM 拦),临时目录不受影响。
49
+ const root = path.resolve(String(
50
+ args.root && args.root !== true ? args.root : path.join(os.tmpdir(), 'weaver-work-cli-serve'),
51
+ ));
52
+ const snapshotsDir = path.join(root, 'snapshots');
53
+ const registryFile = path.join(root, 'current.json');
54
+ const idleMinutes = Number(args.idle && args.idle !== true ? args.idle : 30);
55
+ const port = args.port !== undefined && args.port !== true ? Number(args.port) : 0;
56
+ const allowOrigin = args['allow-origin'] && args['allow-origin'] !== true ? String(args['allow-origin']) : '';
57
+
58
+ // token 只允许十六进制,避免 `../` 之类的目录穿越
59
+ const TOKEN_RE = /^[a-f0-9]{8,64}$/;
60
+ const IDLE_MS = idleMinutes * 60000;
61
+
62
+ fs.mkdirSync(snapshotsDir, { recursive: true });
63
+
64
+ let hits = 0;
65
+ let lastHit = Date.now();
66
+
67
+ function touchDir(dir) {
68
+ try {
69
+ const now = new Date();
70
+ fs.utimesSync(dir, now, now);
71
+ } catch { /* 目录已被删,忽略 */ }
72
+ }
73
+
74
+ function dropSnapshot(dir) {
75
+ // 不用 rmSync(recursive):宿主沙箱会拦后台进程的批量删除(EPERM),
76
+ // 逐文件 unlink 再 rmdir 可以通过;任一步失败都不影响「过期即 404」的安全性。
77
+ try {
78
+ for (const name of fs.readdirSync(dir)) {
79
+ try { fs.unlinkSync(path.join(dir, name)); } catch { /* 忽略 */ }
80
+ }
81
+ fs.rmdirSync(dir);
82
+ } catch { /* 忽略 */ }
83
+ }
84
+
85
+ /** 读一份快照;不存在或已过期都返回 404(过期的顺手清掉)。 */
86
+ function readSnapshot(token) {
87
+ if (!TOKEN_RE.test(String(token || ''))) return { status: 400, error: 'invalid_token' };
88
+ const dir = path.join(snapshotsDir, token);
89
+ const file = path.join(dir, 'todo.json');
90
+ let mtime = 0;
91
+ try {
92
+ mtime = fs.statSync(dir).mtimeMs;
93
+ } catch {
94
+ return { status: 404, error: 'snapshot_not_found' };
95
+ }
96
+ if (Date.now() - mtime > IDLE_MS) {
97
+ dropSnapshot(dir);
98
+ return { status: 404, error: 'snapshot_expired' };
99
+ }
100
+ let body = '';
101
+ try {
102
+ body = fs.readFileSync(file, 'utf8');
103
+ } catch {
104
+ return { status: 404, error: 'snapshot_empty' };
105
+ }
106
+ touchDir(dir);
107
+ return { status: 200, body };
108
+ }
109
+
110
+ /** 清掉过期快照,返回仍在有效期内的数量。 */
111
+ function sweepSnapshots() {
112
+ let names = [];
113
+ try {
114
+ names = fs.readdirSync(snapshotsDir);
115
+ } catch {
116
+ return 0;
117
+ }
118
+ let alive = 0;
119
+ for (const name of names) {
120
+ const dir = path.join(snapshotsDir, name);
121
+ let mtime = 0;
122
+ try {
123
+ mtime = fs.statSync(dir).mtimeMs;
124
+ } catch {
125
+ continue;
126
+ }
127
+ if (Date.now() - mtime > IDLE_MS) dropSnapshot(dir);
128
+ else alive += 1;
129
+ }
130
+ return alive;
131
+ }
132
+
133
+ function corsHeaders(req) {
134
+ const origin = allowOrigin || req.headers.origin || '*';
135
+ return {
136
+ 'Access-Control-Allow-Origin': origin,
137
+ 'Access-Control-Allow-Methods': 'GET, OPTIONS',
138
+ 'Access-Control-Allow-Headers': req.headers['access-control-request-headers'] || '*',
139
+ 'Access-Control-Allow-Private-Network': 'true',
140
+ 'Access-Control-Max-Age': '600',
141
+ Vary: 'Origin',
142
+ };
143
+ }
144
+
145
+ function send(res, status, headers, body) {
146
+ res.writeHead(status, { ...headers, 'Content-Type': 'application/json; charset=utf-8' });
147
+ res.end(body);
148
+ }
149
+
150
+ const server = http.createServer((req, res) => {
151
+ lastHit = Date.now();
152
+ const url = new URL(req.url || '/', 'http://127.0.0.1');
153
+ const headers = corsHeaders(req);
154
+
155
+ if (req.method === 'OPTIONS') {
156
+ res.writeHead(204, headers);
157
+ res.end();
158
+ return;
159
+ }
160
+ if (req.method !== 'GET') {
161
+ send(res, 405, headers, JSON.stringify({ ok: false, error: 'method_not_allowed' }));
162
+ return;
163
+ }
164
+ if (url.pathname === '/health') {
165
+ const alive = sweepSnapshots();
166
+ const address = server.address();
167
+ send(res, 200, { ...headers, 'Cache-Control': 'no-store' }, JSON.stringify({
168
+ ok: true,
169
+ version: SERVE_VERSION,
170
+ pid: process.pid,
171
+ port: address ? address.port : port,
172
+ snapshots: alive,
173
+ idleMinutes,
174
+ hits,
175
+ }));
176
+ return;
177
+ }
178
+
179
+ // 新形态 /s/<token>/todo.json,兼容旧形态 /todo.json?token=<token>
180
+ let token = '';
181
+ const matched = url.pathname.match(/^\/s\/([^/]+)\/todo\.json$/);
182
+ if (matched) token = matched[1];
183
+ else if (url.pathname === '/todo.json' || url.pathname === '/') token = url.searchParams.get('token') || '';
184
+
185
+ if (!token) {
186
+ send(res, 404, headers, JSON.stringify({ ok: false, error: 'not_found' }));
187
+ return;
188
+ }
189
+ const result = readSnapshot(token);
190
+ if (result.status !== 200) {
191
+ send(res, result.status, { ...headers, 'Cache-Control': 'no-store' }, JSON.stringify({ ok: false, error: result.error }));
192
+ return;
193
+ }
194
+ hits += 1;
195
+ send(res, 200, { ...headers, 'Cache-Control': 'no-store' }, result.body);
196
+ });
197
+
198
+ let retries = 0;
199
+ server.on('error', (error) => {
200
+ if (error && error.code === 'EADDRINUSE' && retries < 10) {
201
+ retries += 1;
202
+ console.error('[todo-serve] 端口 %d 被占用,换下一个', port + retries);
203
+ server.listen(port + retries, '127.0.0.1');
204
+ return;
205
+ }
206
+ console.error('[todo-serve] 监听失败:%s', error && error.message);
207
+ cleanup(1);
208
+ });
209
+
210
+ server.listen(port, '127.0.0.1', () => {
211
+ const actualPort = server.address().port;
212
+ fs.writeFileSync(registryFile, JSON.stringify({
213
+ pid: process.pid,
214
+ port: actualPort,
215
+ version: SERVE_VERSION,
216
+ root,
217
+ startedAt: new Date().toISOString(),
218
+ idleMinutes,
219
+ }, null, 2));
220
+ console.log('[todo-serve] listening http://127.0.0.1:%d/s/<token>/todo.json|空闲 %d 分钟自动退出|pid=%d', actualPort, idleMinutes, process.pid);
221
+ });
222
+
223
+ function cleanup(code) {
224
+ try { fs.unlinkSync(registryFile); } catch { /* 已删 */ }
225
+ process.exit(code);
226
+ }
227
+
228
+ const sweeper = setInterval(() => {
229
+ const alive = sweepSnapshots();
230
+ if (alive > 0) return;
231
+ if (Date.now() - lastHit < IDLE_MS) return;
232
+ console.log('[todo-serve] 无快照且连续 %d 分钟无访问,退出', idleMinutes);
233
+ cleanup(0);
234
+ }, 15000);
235
+ sweeper.unref();
236
+
237
+ for (const sig of ['SIGINT', 'SIGTERM']) {
238
+ process.on(sig, () => cleanup(0));
239
+ }
@@ -19,7 +19,42 @@ const { execFileSync } = require('node:child_process');
19
19
  const fs = require('node:fs');
20
20
  const path = require('node:path');
21
21
 
22
- const CLI = process.env.WEAVER_CLI || 'weaver-work-cli';
22
+ /**
23
+ * CLI 可执行入口(跨平台)。
24
+ *
25
+ * - POSIX:直接跑 PATH 里的 `weaver-work-cli`;
26
+ * - Windows:npm 全局装出来的是 `weaver-work-cli.cmd`,**不能用 `execFile` 直接跑 `.cmd`**
27
+ * (Node ≥ 18.20 / 20.12 起会拒绝,报 `EINVAL`;老版本则 `ENOENT`)——这是本脚本原先在
28
+ * Windows 上第一步就崩的原因。所以优先定位包内的 `dist/index.js`,用当前 node 去跑;
29
+ * 实在找不到才退回 `.cmd`(此时必须 `shell:true`,参数里的引号会被 cmd 重新解释,仅作兜底)。
30
+ *
31
+ * `WEAVER_CLI` 环境变量优先级最高(值按原样当可执行文件用)。
32
+ */
33
+ const CLI_LAUNCH = (() => {
34
+ const override = process.env.WEAVER_CLI;
35
+ if (override) return { file: override, prefix: [], shell: false };
36
+ if (process.platform !== 'win32') return { file: 'weaver-work-cli', prefix: [], shell: false };
37
+ const entry = path.join('node_modules', 'weaver-work-cli', 'dist', 'index.js');
38
+ const bases = [
39
+ process.env.APPDATA ? path.join(process.env.APPDATA, 'npm') : '',
40
+ path.dirname(process.execPath),
41
+ process.env.ProgramFiles ? path.join(process.env.ProgramFiles, 'nodejs') : '',
42
+ process.env['ProgramFiles(x86)'] ? path.join(process.env['ProgramFiles(x86)'], 'nodejs') : '',
43
+ ].filter(Boolean);
44
+ for (const base of bases) {
45
+ const candidate = path.join(base, entry);
46
+ if (fs.existsSync(candidate)) return { file: process.execPath, prefix: [candidate], shell: false };
47
+ }
48
+ return { file: 'weaver-work-cli.cmd', prefix: [], shell: true };
49
+ })();
50
+
51
+ /** 按平台拼好的入口调 CLI;其余选项由调用方给。 */
52
+ function runCli(args, options) {
53
+ return execFileSync(CLI_LAUNCH.file, [...CLI_LAUNCH.prefix, ...args], {
54
+ shell: CLI_LAUNCH.shell,
55
+ ...options,
56
+ });
57
+ }
23
58
 
24
59
  /**
25
60
  * 宿主(WorkBuddy)会给每个 node 子进程注入
@@ -59,7 +94,7 @@ function parseArgs(argv) {
59
94
  }
60
95
 
61
96
  function cli(args) {
62
- const raw = execFileSync(CLI, ['--json', 'workflow', 'run', 'workflow.todoSmartConvert', ...args], {
97
+ const raw = runCli(['--json', 'workflow', 'run', 'workflow.todoSmartConvert', ...args], {
63
98
  encoding: 'utf8',
64
99
  maxBuffer: 512 * 1024 * 1024,
65
100
  env: cliEnv(),
@@ -110,8 +145,16 @@ function groupStats(wfIdGroup) {
110
145
  covered += subs.reduce((sum, x) => sum + x.count, 0);
111
146
  singles += one.length;
112
147
  for (const item of items) {
113
- const summary = item.type === 'group' ? item.summary : item.requestData.summary;
114
- if (!summary) empty += 1;
148
+ // 汇总项除了自身的组摘要,还要算它内部每条的单条摘要——否则「组内明细全空」会被漏报
149
+ // (2026-10-28 实测:223 条内部留言单条摘要全空,旧统计只报 84,实际 302)。
150
+ if (item.type === 'group') {
151
+ if (!item.summary) empty += 1;
152
+ for (const row of item.requestDatas || []) {
153
+ if (!row.summary) empty += 1;
154
+ }
155
+ } else if (!item.requestData.summary) {
156
+ empty += 1;
157
+ }
115
158
  }
116
159
  rows.push({
117
160
  title: group.title,
@@ -190,7 +233,7 @@ function resolveBaseUrl(state, dir) {
190
233
  if (state.baseUrl) return state.baseUrl;
191
234
  let baseUrl = '';
192
235
  try {
193
- const raw = execFileSync(CLI, ['--json', 'auth', 'status'], {
236
+ const raw = runCli(['--json', 'auth', 'status'], {
194
237
  encoding: 'utf8',
195
238
  maxBuffer: 16 * 1024 * 1024,
196
239
  env: cliEnv(),
@@ -211,21 +254,26 @@ function resolveBaseUrl(state, dir) {
211
254
 
212
255
  /**
213
256
  * 把最终三键对象包成 browsertool 组件载荷(用户 2026-10-27 要求)。
257
+ * - 外框固定:`callId` 写死 `todo_summary`、`toolId` 写死 `fill_form`(2026-10-28 规范);
214
258
  * - `params.data` 放三键本身;`src` = 当前认证环境 baseUrl + `/workflow/list/todo`;
215
259
  * - 数据段沿用调用方的紧凑文本(`requestData` 单行),只加缩进前缀、不重新序列化,
216
260
  * 避免几千行 requestData 被展开成 4 倍体积;
217
261
  * - 包裹完成后立刻 `JSON.parse` 自检,坏载荷不交给用户。
218
262
  */
219
- function wrapBrowseToolPayload(dataText, baseUrl) {
263
+ function wrapBrowseToolPayload(dataText, baseUrl, dataSourceUrl) {
220
264
  const indentTail = (text, spaces) => text.split('\n')
221
265
  .map((line, index) => (index === 0 ? line : ' '.repeat(spaces) + line))
222
266
  .join('\n');
223
267
  const src = `${String(baseUrl).replace(/\/+$/, '')}/workflow/list/todo`;
268
+ // 用户 2026-10-28 定稿的载荷结构:数据源地址放在 `params.data.dataSource`(不是 `params.dataSource`)。
269
+ const paramsBody = dataSourceUrl
270
+ ? ` "data": {\n "dataSource": ${JSON.stringify(dataSourceUrl)}\n }`
271
+ : ` "data": ${indentTail(dataText, 8)}`;
224
272
  const text = [
225
273
  '{',
226
274
  ' "version": "1.0",',
227
- ' "callId": "call-1",',
228
- ' "toolId": "todo_summary",',
275
+ ' "callId": "todo_summary",',
276
+ ' "toolId": "fill_form",',
229
277
  ' "arguments": {',
230
278
  ' "type": "e10",',
231
279
  ' "title": "待办摘要汇总",',
@@ -239,7 +287,7 @@ function wrapBrowseToolPayload(dataText, baseUrl) {
239
287
  ' "params_pc": { "app": "@weapp/workflow", "compName": "Comp.WflTodoSummary", "clientType": "pc" },',
240
288
  ' "params_mobile": { "app": "@weapp/workflow", "compName": "Comp.WflTodoSummary", "clientType": "mobile" },',
241
289
  ' "params": {',
242
- ` "data": ${indentTail(dataText, 8)}`,
290
+ paramsBody,
243
291
  ' }',
244
292
  ' }',
245
293
  ' }',
@@ -249,6 +297,129 @@ function wrapBrowseToolPayload(dataText, baseUrl) {
249
297
  return `${text}\n`;
250
298
  }
251
299
 
300
+ /** 同步 sleep(跨平台,不启动子进程):只在等服务写 serve.json 的这几百毫秒里用。 */
301
+ function sleepSync(ms) {
302
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
303
+ }
304
+
305
+ /**
306
+ * 发布一份快照到「单进程多快照」的本地数据源,返回带 token 的完整 URL(2026-10-29 方案 B)。
307
+ *
308
+ * - 进程恒为 1、端口恒定:`~/.weaver-work-cli/serve/current.json` 里记录 pid/port/version,
309
+ * **只有 `/health` 探通且版本一致**才复用(只看 pid 存活会被 Windows 的 pid 复用骗到);
310
+ * - 每份汇总 = 一个快照目录 `<root>/snapshots/<token>/todo.json`,旧链接在各自空闲期内继续可用;
311
+ * - 快照按目录 mtime 计空闲,超时由服务端自己删;进程在所有快照过期且无请求后才退出。
312
+ */
313
+ const SERVE_ROOT = path.join(require('node:os').tmpdir(), 'weaver-work-cli-serve');
314
+ const SERVE_VERSION = '3';
315
+
316
+ /**
317
+ * 同步探一次本地数据源的 `/health`:通就返回解析后的 JSON,不通返回 null。
318
+ *
319
+ * 为什么不用 async `http.get`:`ensureServeServer` 整条调用链是**同步**的(端口要同步返回),
320
+ * 而 `Atomics.wait` 阻塞主线程后 http 回调永远排不上来。所以用一个极短的 node 子进程发这一发请求:
321
+ * env 走 `cliEnv()` 摘掉宿主 shim,实测子进程约 0.1~0.2s(不摘 shim 会到 4s)。
322
+ *
323
+ * 这一发探测解决两件事:
324
+ * 1. `current.json` 里的 pid 被系统复用给无关进程时,`process.kill(pid,0)` 会误判「还活着」;
325
+ * 2. 服务进程被强杀(Windows 的 TerminateProcess)后注册文件残留,端口已无人监听。
326
+ */
327
+ function probeHealth(port) {
328
+ if (!port) return null;
329
+ const code = [
330
+ "const http=require('node:http');",
331
+ 'const port=Number(process.argv[1]);',
332
+ "const req=http.get({host:'127.0.0.1',port,path:'/health',timeout:800},(res)=>{",
333
+ " let b='';res.setEncoding('utf8');",
334
+ " res.on('data',(d)=>{b+=d;});",
335
+ " res.on('end',()=>{try{process.stdout.write(b);}catch{}process.exit(0);});",
336
+ '});',
337
+ "req.on('timeout',()=>{req.destroy();process.exit(2);});",
338
+ "req.on('error',()=>process.exit(2));",
339
+ ].join('');
340
+ try {
341
+ const raw = execFileSync(process.execPath, ['-e', code, String(port)], {
342
+ encoding: 'utf8',
343
+ timeout: 5000,
344
+ maxBuffer: 1024 * 1024,
345
+ env: cliEnv(),
346
+ });
347
+ const parsed = JSON.parse(raw);
348
+ return parsed && typeof parsed === 'object' ? parsed : null;
349
+ } catch {
350
+ return null;
351
+ }
352
+ }
353
+
354
+ function ensureServeServer(portOption) {
355
+ const registryFile = path.join(SERVE_ROOT, 'current.json');
356
+ const wanted = portOption === true ? 0 : (Number(portOption) || 0);
357
+ fs.mkdirSync(path.join(SERVE_ROOT, 'snapshots'), { recursive: true });
358
+ if (fs.existsSync(registryFile)) {
359
+ try {
360
+ const cur = JSON.parse(fs.readFileSync(registryFile, 'utf8'));
361
+ if (cur && cur.pid && cur.port) {
362
+ // 先探活:既验「真有人在这端口上应答」,也验「应答的是不是我们这个版本的自己」。
363
+ const health = probeHealth(cur.port);
364
+ if (health && health.ok === true && String(health.version) === SERVE_VERSION) {
365
+ return cur.port;
366
+ }
367
+ if (health && health.ok === true) {
368
+ // 确认是本项目的**旧版本**实例:收掉再起新的(此时杀掉是安全的,端口已自证身份)。
369
+ // pid 取 /health 自己报的(authoritative),注册文件里的 pid 可能已过期或被复用。
370
+ const target = Number(health.pid) || cur.pid;
371
+ try { process.kill(target, 'SIGTERM'); } catch { /* 已退出 */ }
372
+ sleepSync(300);
373
+ }
374
+ // 应答不通时**绝不发信号**:pid 可能已被系统复用给无关进程,发 SIGTERM 会误杀别人的进程。
375
+ // 只清注册表、另起一个(旧实例若真活着,会按自己的空闲超时退出)。
376
+ // Windows 上 process.kill 走 TerminateProcess,服务端 SIGTERM 处理器不执行,
377
+ // 残留的 current.json 也由这里一并清掉。
378
+ try { fs.unlinkSync(registryFile); } catch { /* 已删或被杀进程清理过 */ }
379
+ }
380
+ } catch { /* 注册文件损坏,当不存在 */ }
381
+ }
382
+ const child = require('node:child_process').spawn(
383
+ process.execPath,
384
+ [path.join(__dirname, 'todo-serve.cjs'), '--root', SERVE_ROOT, '--port', String(wanted)],
385
+ { detached: true, stdio: 'ignore' },
386
+ );
387
+ child.unref();
388
+ const deadline = Date.now() + 6000;
389
+ while (Date.now() < deadline) {
390
+ if (fs.existsSync(registryFile)) {
391
+ try {
392
+ const info = JSON.parse(fs.readFileSync(registryFile, 'utf8'));
393
+ if (info && info.version === SERVE_VERSION && info.port && info.pid === child.pid) return info.port;
394
+ } catch { /* 文件正在写入,稍后重试 */ }
395
+ }
396
+ sleepSync(120);
397
+ }
398
+ console.log('⚠ 本地数据源启动超时,改用内联 data(检查 %s)', registryFile);
399
+ return null;
400
+ }
401
+
402
+ function publishSnapshot(dir, portOption) {
403
+ const dataFile = path.join(dir, 'final.data.json');
404
+ if (!fs.existsSync(dataFile)) return null;
405
+ const port = ensureServeServer(portOption);
406
+ if (!port) return null;
407
+ const token = require('node:crypto').randomBytes(12).toString('hex');
408
+ const snapDir = path.join(SERVE_ROOT, 'snapshots', token);
409
+ fs.mkdirSync(snapDir, { recursive: true });
410
+ // 先拷到临时名再拷成正式名:沙箱会拒绝对家目录做 rename(EPERM),copyFile 不受限;
411
+ // 这份 URL 此时还没交给任何人,所以不存在「读到半份」的窗口。
412
+ const tmp = path.join(snapDir, 'todo.json.tmp');
413
+ fs.copyFileSync(dataFile, tmp);
414
+ fs.copyFileSync(tmp, path.join(snapDir, 'todo.json'));
415
+ try { fs.unlinkSync(tmp); } catch { /* 残留也无害 */ }
416
+ const url = `http://127.0.0.1:${port}/s/${token}/todo.json`;
417
+ try {
418
+ fs.writeFileSync(path.join(dir, 'serve.json'), JSON.stringify({ url, token, port, snapshotDir: snapDir }, null, 2));
419
+ } catch { /* 只是排错用的旁路文件 */ }
420
+ return url;
421
+ }
422
+
252
423
  function exportFinal(result, dir, state, kept, opts = {}) {
253
424
  const { limit, from, to } = state;
254
425
  const excluded = (state.exclude || []).join('/');
@@ -323,8 +494,17 @@ function exportFinal(result, dir, state, kept, opts = {}) {
323
494
  lines.push(` }${gi < payload.wfIdGroup.length - 1 ? ',' : ''}`);
324
495
  });
325
496
  lines.push(' ]', '}');
497
+ // 供 todo-serve.cjs 读取的同构数据(只有三键,无外框):服务与载荷永远读同一份,不会出现两份数据。
498
+ fs.writeFileSync(path.join(dir, 'final.data.json'), JSON.stringify(payload));
499
+ // 数据源模式:`--data-source <url>` 直接用给定地址;`--serve [端口]` 现起一个本地服务拿地址。
500
+ let dataSourceUrl = opts.dataSource && opts.dataSource !== true ? String(opts.dataSource) : null;
501
+ if (!dataSourceUrl && opts.serve) dataSourceUrl = publishSnapshot(dir, opts.serve);
326
502
  // 落盘与 stdout 共用同一份载荷;data 段只保留三个业务键。
327
- const finalText = wrapBrowseToolPayload(lines.join('\n'), baseUrl);
503
+ const finalText = wrapBrowseToolPayload(lines.join('\n'), baseUrl, dataSourceUrl);
504
+ if (dataSourceUrl) {
505
+ console.log('\n数据源模式:params.dataSource = %s', dataSourceUrl);
506
+ console.log(' 组件改为 GET 该地址(响应体就是 params.data 的三键对象);单进程多快照,快照空闲 30 分钟后自动清理');
507
+ }
328
508
  // 用户 2026-10-27 定稿:交付 = 回复里内联 <browsertool> 整块,**不再生成 JSON 文件**。
329
509
  // `--file` 仅保留作调试(写一份纯载荷 JSON),交付流程不再使用它。
330
510
  if (opts.writeFile) fs.writeFileSync(finalFile, finalText);
@@ -506,32 +686,24 @@ function cmdBuild(args) {
506
686
  if (planFile) {
507
687
  aiGroups = readJson(planFile);
508
688
 
509
- // 覆盖校验:每个 groupId 的 1..count 全覆盖、无重复;count 取 CLI 口径(priority 已抽取)
689
+ // 只做「结构 + groupId + 组内重复」的前置友好检查;
690
+ // 覆盖度与合法性由 CLI 裁定(`aiGroups.seq` 已废弃,只认 requestIds;漏分的条目 CLI 会落成单条并告警)。
510
691
  const byGroup = new Map();
511
692
  for (const entry of aiGroups) {
512
- if (!entry || !entry.groupId || !Array.isArray(entry.seq)) {
513
- console.error('✖ aiGroups 元素必须是 {groupId,title,seq:[...]}'); process.exit(1);
693
+ if (!entry || !entry.groupId || !Array.isArray(entry.requestIds)) {
694
+ console.error('✖ aiGroups 元素必须是 {groupId,title,requestIds:[...]}(seq 已废弃)'); process.exit(1);
514
695
  }
515
696
  if (!countById.has(String(entry.groupId))) {
516
697
  console.error(`✖ 未知 groupId ${entry.groupId}(合法值见 state.json 的 planGroups)`); process.exit(1);
517
698
  }
518
699
  const set = byGroup.get(entry.groupId) || new Set();
519
- for (const n of entry.seq) {
520
- if (set.has(n)) { console.error(`✖ groupId ${entry.groupId} 的 seq ${n} 重复`); process.exit(1); }
521
- set.add(n);
700
+ for (const id of entry.requestIds) {
701
+ const key = String(id);
702
+ if (set.has(key)) { console.error(`✖ groupId ${entry.groupId} 的 requestId ${key} 重复`); process.exit(1); }
703
+ set.add(key);
522
704
  }
523
705
  byGroup.set(entry.groupId, set);
524
706
  }
525
- for (const [groupId, seqs] of byGroup) {
526
- const count = countById.get(groupId);
527
- const miss = [];
528
- for (let i = 1; i <= count; i += 1) if (!seqs.has(i)) miss.push(i);
529
- if (miss.length) {
530
- const name = (planGroups.find((g) => g.groupId === groupId) || {}).title || groupId;
531
- console.error(`✖ ${name} 缺 ${miss.length} 条:${miss.slice(0, 12).join(',')}`);
532
- process.exit(1);
533
- }
534
- }
535
707
  const focusKinds = focusCoversKinds(state.groupFocus);
536
708
  for (const group of planGroups) {
537
709
  if (countById.get(group.groupId) <= 1 || group.groupId === 'priority') continue;
@@ -649,7 +821,17 @@ function cmdFinish(args) {
649
821
  console.log(' → 写成 [{groupId,summary}] 后加 `--group-summaries <file>` 重跑本命令,可在同一次调用里回填');
650
822
  }
651
823
  }
652
- exportFinal(finalResult, dir, state, kept.length, { writeFile: args.file === true, timingReport: args['timing-report'] });
824
+ // 2026-10-28 用户 OA 页面实测通过:默认走本地数据源(params.dataSource),
825
+ // 载荷只带 URL、不带明细;`--no-serve` 退回内联 data,`--serve <端口>` 换端口。
826
+ const serveOption = args['no-serve'] === true
827
+ ? false
828
+ : (args.serve === undefined ? true : args.serve);
829
+ exportFinal(finalResult, dir, state, kept.length, {
830
+ writeFile: args.file === true,
831
+ timingReport: args['timing-report'],
832
+ dataSource: args['data-source'],
833
+ serve: serveOption,
834
+ });
653
835
  console.log('总耗时(脚本口径):%s', fmt(Date.now() - Date.parse(state.startedAt)));
654
836
  }
655
837
 
@@ -675,7 +857,14 @@ function cmdRun(args) {
675
857
  return;
676
858
  }
677
859
  // 没有待写摘要 ⇒ 终态可以立刻出,省掉一次约 7s 的进程启动
678
- cmdFinish({ dir, file: args.file === true, 'timing-report': args['timing-report'], 'group-summaries': args['group-summaries'] });
860
+ cmdFinish({
861
+ dir,
862
+ file: args.file === true,
863
+ 'timing-report': args['timing-report'],
864
+ 'group-summaries': args['group-summaries'],
865
+ serve: args.serve,
866
+ 'no-serve': args['no-serve'],
867
+ });
679
868
  }
680
869
 
681
870
  const args = parseArgs(process.argv.slice(2));
@@ -690,6 +879,7 @@ else {
690
879
  console.log(' prep --limit 100 [--exclude 内部留言] [--days 1460] [--sub-category 待处理] [--out DIR]');
691
880
  console.log(' build --dir DIR [--plan aiGroups.json] # 不传 plan = 直接走确定性分组');
692
881
  console.log(' finish --dir DIR [--summaries aiSummaries.json] [--group-summaries aiGroupSummaries.json] [--file] [--timing-report]');
882
+ console.log(' 默认:载荷只带 params.dataSource(现起本地服务,空闲 30 分钟自动退出);--no-serve 退回内联 data');
693
883
  console.log(' 提示:`--timing-report` 会在控制台打一段耗时摘要(阶段墙钟 / CLI 内部),**不产出 md**');
694
884
  process.exit(2);
695
885
  }
@@ -91,11 +91,11 @@ cliHelp: "weaver-work-cli invoice --help"
91
91
  | 报销自动填单(发票、事前申请、工作流、预填数据) | `invoice run invoice.reim.`* | `[invoice-reim.md](references/invoice-reim.md)` |
92
92
  | 报销单创建 / 修改 | `workflow run workflow.createForm/createRequest`、`workflow.editLoad/editPrepare/editApply`(`weaver-e10-workflow` 技能) | `[reim-workflow.md](references/reim-workflow.md)` Step 3~6;命令契约见 `[workflow-create.md](../weaver-e10-workflow/references/workflow-create.md)` |
93
93
  | 报销完整工作流编排、行为准则 | AI 内部 | `[reim-workflow.md](references/reim-workflow.md)` |
94
- | 批量获取多张发票预填信息 | `invoice run invoice.reim.row-info-batch` | `[reim-api-reference.md](references/reim-api-reference.md)` |
94
+ | 批量获取多张发票预填信息(报销建单不再调用) | `invoice run invoice.reim.row-info-batch` | `[reim-api-reference.md](references/reim-api-reference.md)` |
95
95
  | 相关客户 / 相关项目模糊搜索 | `invoice run invoice.reim.field.search` | `[reim-form-fill-rules.md](references/reim-form-fill-rules.md)`「相关客户模糊搜索」 |
96
96
  | 报销 CLI operation 入参与返回值 | AI 内部 | `[reim-api-reference.md](references/reim-api-reference.md)` |
97
97
  | 发票与事前申请匹配规则 | AI 内部 | `[reim-matching-rules.md](references/reim-matching-rules.md)` |
98
- | 费用科目自动匹配规则 | AI 内部 | `[reim-expense-subject-rules.md](references/reim-expense-subject-rules.md)` |
98
+ | 费用科目匹配规则(建单不用,仅用户要求改科目时) | AI 内部 | `[reim-expense-subject-rules.md](references/reim-expense-subject-rules.md)` |
99
99
  | 报销表单字段填充与 JSON 组装规则 | AI 内部 | `[reim-form-fill-rules.md](references/reim-form-fill-rules.md)` |
100
100
  | 话费补贴 / 话费报销硬拦截规则 | AI 内部 | `[reim-phone-subsidy-rules.md](references/reim-phone-subsidy-rules.md)` |
101
101
  | 报销端到端示例 | 参考 | `[reim-examples.md](references/reim-examples.md)` |
@@ -118,11 +118,13 @@ cliHelp: "weaver-work-cli invoice --help"
118
118
  - 编辑发票:`invoice.update.prepare` -> 用户确认 -> `invoice.update.apply`
119
119
  - 删除发票:`invoice.delete.prepare` -> 用户确认 -> `invoice.delete.apply`
120
120
  - 流程发票字段:已有 `fid` 或详情时,用 `invoice.browse-field.data` 生成浏览按钮对象;不要运行旧源码脚本或直接调接口。
121
- - 报销填单(完整工作流见 `[reim-workflow.md](references/reim-workflow.md)`):先用 `invoice.reim.candidates.list`(按日期批量)/`invoice.list`/`invoice.get`/`invoice.reim.file-ocr.preview` 获取发票(日期筛选默认 `consumption_tm_begin` / `consumption_tm_end`)-> 展示汇总并确认 -> `invoice.reim.requests.list` 只按开票日期和事前申请发起日期关联(只比日期不比钟点,发起日与开票日同一天算对上,默认绑定),一张发票同时落在多条上时让用户选 -> `invoice.reim.workflow.resolve` 得到报销工作流 -> 并行 `workflow.createForm`(`weaver-e10-workflow`,传 `{workFlowName}({workFlowId})`,**不传 `userQuestion`**,也不用 `workflow.createRequestDirect`)、`invoice.reim.row-info-batch`、`invoice.browse-field.data`(只传发票)-> 按规则组装 `data`(**`requestName` 与「报销事由」必传**,按 form-fill-rules 的标题/事由公式生成,禁止留用表单默认标题或留空;发票与事前申请只按时间关联,见 `[reim-matching-rules.md](references/reim-matching-rules.md)`,填单规则见 `[reim-form-fill-rules.md](references/reim-form-fill-rules.md)`,科目匹配见 `[reim-expense-subject-rules.md](references/reim-expense-subject-rules.md)`;命中话费补贴/话费报销时先读 `[reim-phone-subsidy-rules.md](references/reim-phone-subsidy-rules.md)`;工作流是「总部费用报销」时,用本次 `createForm` 的 `historyRequestData` 在项目确认后带出合同号,**不要询问合同号**;相关客户和相关项目需要填写时同一次让用户一起输入,未确认不建单,见 `[reim-workflow.md](references/reim-workflow.md)` Step 4.4、Step 4.5;明细「费用说明」「费用明细」不写入 `data`,由发票联动带出)-> 直接 `workflow.createRequest` 存草稿(`action` 固定 `"draft"`,**不问用户「存草稿 / 提交审批」**,草稿由用户在 E10 核对后自己提交)-> 原样粘贴 `finalMarkdown` -> **必须做 Step 5.3**:查客户 / 项目是否被联动冲空(已确认值直接写回,不要先建空草稿再问)。创建成功后修改只能用 `workflow.editLoad` -> `workflow.editPrepare` -> 用户确认 -> `workflow.editApply`。
121
+ - 报销填单(完整工作流见 `[reim-workflow.md](references/reim-workflow.md)`):先用 `invoice.reim.candidates.list`(按日期批量)/`invoice.list`/`invoice.get`/`invoice.reim.file-ocr.preview` 获取发票(日期筛选默认 `consumption_tm_begin` / `consumption_tm_end`;出差 / 去某城市报销时出发城市没说默认上海,消费日期落在出差时间内的出发城市打车票也一起筛进来,见 `[reim-workflow.md](references/reim-workflow.md)`「出差 / 城市意图查票」)-> 展示汇总并确认 -> `invoice.reim.requests.list` 只按开票日期和事前申请发起日期关联(只比日期不比钟点,发起日与开票日同一天算对上,默认绑定),一张发票同时落在多条上时让用户选 -> `invoice.reim.workflow.resolve` 得到报销工作流 -> 并行 `workflow.createForm`(`weaver-e10-workflow`,传 `{workFlowName}({workFlowId})`,**不传 `userQuestion`**,也不用 `workflow.createRequestDirect`)、`invoice.browse-field.data`(只传发票)-> 按规则组装 `data`(**`requestName` 与「报销事由」必传**,按 form-fill-rules 的标题/事由公式生成,禁止留用表单默认标题或留空;发票与事前申请只按时间关联,见 `[reim-matching-rules.md](references/reim-matching-rules.md)`,填单规则见 `[reim-form-fill-rules.md](references/reim-form-fill-rules.md)`;明细「费用类型(科目)」不传,由发票联动带出;命中话费补贴/话费报销时先读 `[reim-phone-subsidy-rules.md](references/reim-phone-subsidy-rules.md)`;工作流是「总部费用报销」时,用本次 `createForm` 的 `historyRequestData` 在项目确认后带出合同号,**不要询问合同号**;相关客户和相关项目需要填写时同一次让用户一起输入,未确认不建单,见 `[reim-workflow.md](references/reim-workflow.md)` Step 4.4、Step 4.5;明细「费用说明」「费用明细」不写入 `data`,由发票联动带出)-> 直接 `workflow.createRequest` 存草稿(`action` 固定 `"draft"`,**不问用户「存草稿 / 提交审批」**,草稿由用户在 E10 核对后自己提交)-> 原样粘贴 `finalMarkdown` -> **必须做 Step 5.3**:查客户 / 项目是否被联动冲空(已确认值直接写回,不要先建空草稿再问)。创建成功后修改只能用 `workflow.editLoad` -> `workflow.editPrepare` -> 用户确认 -> `workflow.editApply`。
122
122
  - 报销单创建/修改依赖同级的 `weaver-e10-workflow` 技能(`../weaver-e10-workflow/`);该技能不存在时停止并告知用户需要安装,不要改用其他方式创建报销单。
123
123
  - **⚠️ 报销单不询问合同号。** 默认 `20260999`。用户本轮自己说了才用用户的;否则按已确认的相关项目,从 `historyRequestData` 里该项目最近一条带出。对不上或项目「不填」则用默认。带出的合同号不是 `20260999` 时,创建阶段承担主体不传。**相关客户和相关项目同一次让用户一起输入**,确认后再写入 `data`,禁止空着先建草稿,也不要拆成两轮。名称先 `invoice.reim.field.search`。明细「费用说明」不自己生成,交给发票联动。口径见 `[reim-form-fill-rules.md](references/reim-form-fill-rules.md)`「总部费用报销历史参考」「费用明细字段规则」。
124
124
  - **⚠️ 明细列「费用明细」禁止填值**:`createForm` 的 `fields` 里名称是「费用明细」的明细列不要写入 `data`。只传「相关发票」,由发票联动带出。用户给了文字也不要写入。建单后这一列仍空,就说明联动没带出,不要补写。见 `[reim-form-fill-rules.md](references/reim-form-fill-rules.md)`「费用明细字段规则」。
125
125
  - **⚠️ 明细「费用说明」禁止自己生成**:`createForm` 的 `fields` 里名称是「费用说明」或「摘要」的明细列不要写入 `data`。不要用 `fysm`、起止点、消费内容或话费账单说明去填。只传「相关发票」,由发票联动带出。用户给了文字也不要写入。建单后仍空,只说明联动没带出,不要补写。见 `[reim-form-fill-rules.md](references/reim-form-fill-rules.md)`「费用明细字段规则」。
126
+ - **⚠️ 明细每行只写「相关发票」**:有绑定的事前申请时再写「相关流程」(差旅 / 外出每行必须有)。费用日期、金额、币种、费用类型(科目)、费用说明、费用明细、相关凭证、明细相关客户 / 项目、附件数等其余明细列**一律不传**,由「相关发票」联动带出;表单默认值、`row-info` 返回值、用户口述都不写进去。建单后仍空只告知联动没带出,不补写。建单不再调用 `invoice.reim.row-info-batch`。见 `[reim-form-fill-rules.md](references/reim-form-fill-rules.md)`「费用明细字段规则」。
127
+ - **⚠️ 明细「费用类型(科目)」不允许 AI 填值**:创建时不写入 `data`,保留空值,由「相关发票」联动带出。不要用 `row-info` 的 `subjectId`、科目匹配规则或兜底科目「其他-管理费用」自己填。建单后仍空只告知联动没带出,不补写,也不让用户选科目;只有用户明确要求改某行科目时才走修改链。见 `[reim-form-fill-rules.md](references/reim-form-fill-rules.md)`「费用明细字段规则」。
126
128
  - **⚠️ 明细「相关凭证」不允许赋值**:创建和修改都不写入 `data`,由「相关发票」联动带出。不做凭证配对,`invoice.browse-field.data` 只传发票。建单后仍空只告知联动没带出,不补写,也不让用户挂行程单。见 `[reim-form-fill-rules.md](references/reim-form-fill-rules.md)`「3. 相关凭证不赋值」。
127
129
  - **⚠️ 主表「事前申请流程」不允许赋值**:创建报销单时不写入 `data.main`,保留空值。匹配到的事前申请只写到明细每行「相关流程」(`{requestname}({requestid})`)。见 `[reim-form-fill-rules.md](references/reim-form-fill-rules.md)`「主表字段规则」。
128
130
  - **AI 开票(默认主链路,所有开票意图先走这条)**:`invoice.issuing.qys.list`(合同号传 `contractNo`,公司名等传 `keyword`;没有线索只问一句合同号或客户名)-> 按返回的 `groups`(一组 = 合同号 + 购方 = 一张流程)让用户选组 -> **购方无税号却是专票(`specialInvoiceWithoutTaxNo=true`)时硬拦截:没有税号只能开普票,请用户先在台账改为普票,不发起流程** -> 展示确认卡片(明确"只建草稿,不会直接开票";必填的「预计收款时间」在卡片里请用户从本月底 / 下月底 / 30 天后候选里选或直接给日期,不自己定默认值,**用户没给日期就不建草稿**)-> 用户确认 -> `workflow.createForm`(`weaver-e10-workflow`,按台账「核算公司」`sellerNames` 选择,名称必须完全相同:只有「上海亘岩网络科技有限公司」时传 `契约锁合同收款发票开具及快递申请(100003460000001852)`;是泛微网络、泛微软件、上海田亩、成都泛微、泛微国际、北京泛微、浙江泛微之一时传 `合同收款发票开具及快递申请(100003460000000214)`;为空或不在这份名单里时先问用户,选定前不建草稿。用户要退票、冲红或红冲时改为 `合同开票冲红流程(887410479139758085)`,不套用开票填单规则。**不传 `userQuestion`**,**不要按宽词“开票”检索**)-> 按 `[invoice-issuing-form-fill-rules.md](references/invoice-issuing-form-fill-rules.md)` 组装 `data`(用户说了就按用户的,没说就按默认值,例如本次开票金额默认 = 总金额、开票事由自动生成、所售产品版本号取明细规格类型(与下拉选项完全一致才填);「开票内容」只在 `契约锁合同收款发票开具及快递申请(100003460000001852)` 上按明细货物名称推断后传入,其他流程忽略该字段;明细每行只传「金额(含税)」,其余列(货物名称、明细行id、单价、规格类型、税率、总开票金额 / 已开票金额 / 未开票金额等)由合同号联动带出,多传会让总开票金额带不出来;明细「数量」和主表「开户行名称」「开户行银行帐号」创建时不传(数量会被联动写成税率选项 id;开户行会被写成名称账号混写串);其余购方信息由合同号联动带出,不传);`requestName` 按 `开票申请-{购方名称}-{合同号}` 必传 -> `workflow.createRequest`(`action` 固定 `"draft"`)-> 原样粘贴 `finalMarkdown`,再用 `linkageResult` 的「是否存在合同明细数据」「总开票金额」和「本次开票总金额」核对明细是否带出、本次填的开票金额是否一致(台账开票金额为含税金额)-> **创建后补正**:`workflow.editLoad` 读草稿。开户行名称 / 账号取分组里 CLI 拆好的 `purchaserBankName` / `purchaserBankAccount`(为空时按「,账号:」拆只读字段「开户行及帐号」),`workflow.editPrepare` **只传主表这两个字段**,用户确认后 `workflow.editApply`。只有契约锁开票流程才在创建时写入「开票内容」,不要再用编辑链改(`editPrepare` 会报错);没有匹配到选项时才提醒用户在 E10 选择。其他流程没有这个字段,不推断、不提醒。明细数量不经编辑链改(会多出一行):回复末尾把每行数量改成台账 `items[].quantity`(按「明细行id」对应)。创建成功后修改只走 `workflow.editLoad/editPrepare/editApply`,禁止重复创建。详见 `[invoice-issuing-workflow.md](references/invoice-issuing-workflow.md)`。
@@ -163,11 +165,11 @@ cliHelp: "weaver-work-cli invoice --help"
163
165
  - 写请求已经发出后,遇到结果不确定必须停止;不要把 `.apply` 当成可重试读操作
164
166
  - **错误为 `authentication`(未登录,如 `E10 auth not found`)或 `session_expired`(登录态失效)时:立即停止当前操作,不要用示例占位域名或自行猜域名登录,也不要盲目重试**。
165
167
  - 认证失败后先读取共享规则 `../weaver-e10-shared/references/e10-auth-and-session.md`:
166
- - 登录失效的认证方式必须由 Agent 复用本任务开始时的宿主判定,禁止调用任何其他登录 Skill。
167
- - 明确为 XiaoE 时,只执行 `weaver-work-cli auth xiaoe`,不得改用 `auth login`。
168
- - 统一登录返回 `authentication/xiaoe_credential_unavailable` 时停止并反馈宿主凭据不可用,不自动回退浏览器登录。
169
- - 非 XiaoE 时不得执行 `auth xiaoe`;取得用户确认的真实 E10 登录域名后,执行 `weaver-work-cli auth login --base-url "<用户确认的 E10 登录域名>" --agent_type "<当前智能体名称>"`。环境不能完成本机浏览器回调时停止并反馈,不得改调其他登录 Skill。
170
- - 登录恢复后读操作可重新执行;写操作不得复用旧 continuation,必须重新 prepare 并重新确认。已返回 `partial/write_uncertain` 时先只读回查,禁止直接重放写入。
168
+ - 已有 XiaoE 会话的 `session_expired` 已由 CLI 底层自动换票并重放一次;若错误最终仍返回,Agent 只反馈并停止,不执行任何登录命令、其他登录 Skill 或原业务命令。
169
+ - 仅首次无会话或明确的 `auth not found` 才由 Agent 复用任务开始时的宿主判定。
170
+ - 首次认证明确为 XiaoE 时,执行一次 `weaver-work-cli auth xiaoe`;返回 `authentication/xiaoe_credential_unavailable` 时停止,不回退浏览器登录。
171
+ - 首次认证且非 XiaoE 时不得执行 `auth xiaoe`;取得用户确认的真实 E10 登录域名后,执行 `weaver-work-cli auth login --base-url "<用户确认的 E10 登录域名>" --agent_type "<当前智能体名称>"`。环境不能完成本机浏览器回调时停止并反馈。
172
+ - 显式登录后读操作可重新执行;写操作不得复用旧 continuation,必须重新 prepare 并重新确认。已返回 `partial/write_uncertain` 时先只读回查,禁止直接重放写入。
171
173
  - 登录类命令必须带 timeout 限时(上限 300 秒 / 5 分钟,后台运行 + 限时观察):输出一旦出现 `Open browser login`、`Waiting for E10 authorization callback` 或 `Could not open browser automatically`,即判定已降级到浏览器 OAuth,立即终止该命令并反馈,不得继续等待。登录失败不自动重试。
172
174
  - 需要浏览器登录时必须携带发起方标识 `--agent_type "<当前智能体名称>"`(授权确认页展示用;宿主无法确认名称时才可省略,交由 CLI 从环境变量推断)。
173
175