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.
- package/README.md +7 -3
- package/dist/chunks/{archive-PDV724YK.js → archive-JHTONRST.js} +556 -66
- package/dist/chunks/{archive-oa-4XE4MNXQ.js → archive-oa-FCHLCPHC.js} +2 -2
- package/dist/chunks/{asset-PDL2QE2N.js → asset-RLIHC7DB.js} +2 -2
- package/dist/chunks/asset-idle-KVOFTLCP.js +670 -0
- package/dist/chunks/{attachment-content-MDDGOE5E.js → attachment-content-RZYDB3MB.js} +3 -3
- package/dist/chunks/{blog-UYR3EUXW.js → blog-42OZTJNP.js} +2 -2
- package/dist/chunks/{caizhilian-LG2X5NMF.js → caizhilian-KRK547K5.js} +2 -2
- package/dist/chunks/{calendar-FG4BAZHE.js → calendar-RTWAMKHD.js} +2 -2
- package/dist/chunks/{chunk-J7USS3ED.js → chunk-4CTESSKI.js} +4 -4
- package/dist/chunks/{chunk-OJQUJZQF.js → chunk-4TEDNCON.js} +45 -5
- package/dist/chunks/chunk-57IECMWX.js +487 -0
- package/dist/chunks/{chunk-ELYDWBZJ.js → chunk-7BRU4EFH.js} +13 -7
- package/dist/chunks/{chunk-6LTFIFLM.js → chunk-7E6MUOEK.js} +2 -2
- package/dist/chunks/{chunk-KKRL3REV.js → chunk-7U577Q7U.js} +2 -2
- package/dist/chunks/{chunk-UJJD4QWF.js → chunk-CAQNBBQA.js} +2 -2
- package/dist/chunks/{chunk-CORFRD7Q.js → chunk-CXRQWOZ5.js} +2 -2
- package/dist/chunks/{chunk-GGJFMNJM.js → chunk-FLS4DPGP.js} +110 -16
- package/dist/chunks/{chunk-JSMFPO43.js → chunk-I566ZYNF.js} +1 -1
- package/dist/chunks/{chunk-6FTTSA45.js → chunk-IZSSVYAG.js} +2 -2
- package/dist/chunks/{chunk-XJY7G4DD.js → chunk-JWRZM2H2.js} +3 -3
- package/dist/chunks/{chunk-NI6GIPIM.js → chunk-LFMSDBZN.js} +529 -395
- package/dist/chunks/{chunk-VRYHGG6O.js → chunk-LN447PY2.js} +92 -51
- package/dist/chunks/{chunk-NR6RKXPA.js → chunk-NETCCARD.js} +4 -2
- package/dist/chunks/{chunk-Z32KWY6G.js → chunk-NJKREDA3.js} +1 -1
- package/dist/chunks/{chunk-FUWVHMVV.js → chunk-SPRHHSVC.js} +2 -2
- package/dist/chunks/{chunk-IZUDA353.js → chunk-T6EGVBKU.js} +1 -1
- package/dist/chunks/{chunk-DMV2TOMH.js → chunk-VNBU55SL.js} +428 -145
- package/dist/chunks/{chunk-UZZBA6PV.js → chunk-WFFNM65B.js} +47 -6
- package/dist/chunks/{chunk-EG6ZU444.js → chunk-ZUX3P6BQ.js} +10 -5
- package/dist/chunks/{conflict-BQZEQ4SP.js → conflict-VR55AOOO.js} +2 -2
- package/dist/chunks/{contract-XIRAFTEV.js → contract-HI4FNPYE.js} +2 -2
- package/dist/chunks/{cowork-BLG62BL2.js → cowork-HJLI353D.js} +12 -5
- package/dist/chunks/{ebuilder-form-NUKG5QW4.js → ebuilder-form-HTGI63UU.js} +3 -3
- package/dist/chunks/{ehr-2STW5ILH.js → ehr-GYXIYH3A.js} +2 -2
- package/dist/chunks/env-DW3G4HUO.js +19 -0
- package/dist/chunks/{esb-UUDQUNKY.js → esb-I5INCEFE.js} +4 -4
- package/dist/chunks/factory-TKXGXS6L.js +30 -0
- package/dist/chunks/{fna-6RCY4NU5.js → fna-K2MHTQ24.js} +2 -2
- package/dist/chunks/group-chat-VYBNMJLG.js +18 -0
- package/dist/chunks/{host-FWDOSRH5.js → host-256GVCAH.js} +3 -3
- package/dist/chunks/{host-EJZFROU3.js → host-D5SERMAJ.js} +3 -3
- package/dist/chunks/{host-RHM3NPXV.js → host-JM4YVHFM.js} +3 -3
- package/dist/chunks/{host-7RXEDMAH.js → host-L3VLITMT.js} +3 -3
- package/dist/chunks/{host-UFF33TWJ.js → host-ZUQTJYYK.js} +3 -3
- package/dist/chunks/{hrm-PKJ6ELSH.js → hrm-VLRBKTZB.js} +3 -3
- package/dist/chunks/{im-YJAXS7CO.js → im-QK3VTLXK.js} +8 -8
- package/dist/chunks/{invoice-HWVSXGX6.js → invoice-T5JTA32H.js} +3 -3
- package/dist/chunks/{jiuchuanhui-L7VKILZB.js → jiuchuanhui-P2FNEQVZ.js} +2 -2
- package/dist/chunks/{lazy-dispatch-NMYEIHRH.js → lazy-dispatch-XRRI46M6.js} +5 -5
- package/dist/chunks/{mail-B42RRU7G.js → mail-3Y7MJ6PS.js} +9 -4
- package/dist/chunks/{meeting-CFYVBJSS.js → meeting-M7LTXGJE.js} +11 -10
- package/dist/chunks/{okr-GCLF6V7W.js → okr-OBC6S67L.js} +2 -2
- package/dist/chunks/{plan-ZVL7GBRI.js → plan-5H6I53SK.js} +2 -2
- package/dist/chunks/{project-4YWAAE2K.js → project-L75QZV4S.js} +2 -2
- package/dist/chunks/{registry-EP4VTHZS.js → registry-KMHCPAAD.js} +9 -8
- package/dist/chunks/{registry-OZSMDVPP.js → registry-QGHFNUWS.js} +2 -2
- package/dist/chunks/{registry-FRJ6H7BT.js → registry-RXPN6BBT.js} +4 -4
- package/dist/chunks/{registry-SG2PGVCF.js → registry-UGRBCBHO.js} +3 -3
- package/dist/chunks/{room-recommend-R3JVUUAC.js → room-recommend-A3UDC3CA.js} +4 -4
- package/dist/chunks/{schedule-NIJBTYDO.js → schedule-5GCACCK7.js} +4 -4
- package/dist/chunks/{workflow-BB24DFBY.js → workflow-M5ZJ7DWF.js} +11 -8
- package/dist/core/output.js +16 -6
- package/dist/index.js +33 -28
- package/dist/internal/e10/auth/commands.js +109 -56
- package/dist/internal/e10/auth/messages.js +1 -0
- package/dist/internal/e10/auth/session.js +11 -1
- package/dist/internal/e10/auth/xiaoe.js +29 -4
- package/dist/internal/e10/auth-recovery.js +13 -6
- package/dist/internal/e10/client.js +13 -0
- package/dist/internal/e10/context.js +24 -3
- package/dist/internal/e10/request-runtime.js +12 -1
- package/dist/shortcuts/archive/host.js +2 -0
- package/dist/shortcuts/archive/manifest.js +1 -1
- package/dist/shortcuts/archive/operations/borrow-list.js +13 -1
- package/dist/shortcuts/archive/operations/flow-url.js +1 -1
- package/dist/shortcuts/archive/operations/prelib.js +472 -0
- package/dist/shortcuts/archive/operations/registry.js +2 -0
- package/dist/shortcuts/archive/operations/upload.js +1 -1
- package/dist/shortcuts/asset-idle/errors.js +44 -0
- package/dist/shortcuts/asset-idle/host.js +161 -0
- package/dist/shortcuts/asset-idle/index.js +114 -0
- package/dist/shortcuts/asset-idle/manifest.js +159 -0
- package/dist/shortcuts/asset-idle/operations/idle.js +47 -0
- package/dist/shortcuts/asset-idle/operations/registry.js +33 -0
- package/dist/shortcuts/asset-idle/operations/shared.js +142 -0
- package/dist/shortcuts/asset-idle/operations/types.js +9 -0
- package/dist/shortcuts/asset-idle/operations.js +15 -0
- package/dist/shortcuts/cowork/operations/item-write.js +10 -2
- package/dist/shortcuts/index.js +5 -0
- package/dist/shortcuts/mail/operations/mail-send.js +8 -2
- package/dist/shortcuts/meeting/operations/eb/catalog.js +5 -1
- package/dist/shortcuts/meeting/operations/eb/create-flow.js +381 -104
- package/dist/shortcuts/meeting/operations/eb/crm-search.js +87 -5
- package/dist/shortcuts/meeting/operations/eb/flow-memory.js +49 -0
- package/dist/shortcuts/meeting/operations/eb/group-chat.js +583 -0
- package/dist/shortcuts/meeting/operations/eb/index.js +2 -0
- package/dist/shortcuts/meeting/operations/eb/room-recommend.js +19 -3
- package/dist/shortcuts/meeting/operations/eb/room-summary.js +2 -0
- package/dist/shortcuts/meeting/operations/eb/usage-cache.js +99 -0
- package/dist/shortcuts/meeting/operations/from-group-chat.js +35 -12
- package/dist/shortcuts/workflow/host.js +3 -0
- package/dist/shortcuts/workflow/index.js +1 -1
- package/dist/shortcuts/workflow/operations/form.js +65 -171
- package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupRules.js +129 -27
- package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupStore.js +6 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/summaryCache.js +60 -117
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailCache.js +44 -7
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailFetcher.js +37 -29
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +48 -8
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.core.js +1 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.detail.js +22 -18
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.group.js +17 -9
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +34 -10
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +5 -2
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +9 -2
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +122 -20
- package/docs/_catalog.md +3 -0
- package/docs/asset-idle.md +45 -0
- package/docs/e10-auth.md +16 -1
- package/docs/todo-smart-convert/subgroup-rules.md +1 -1
- package/package.json +1 -1
- package/scripts/mcp-meeting.mjs +234 -0
- package/scripts/package-skill.mjs +2 -0
- package/skill-template/business-info.json +14 -1
- package/skill-template/domains/asset-idle.md +13 -0
- package/skill-template/domains/wenshuding.md +3 -0
- package/skills/weaver-e10-archive-oa/SKILL.md +1 -0
- package/skills/weaver-e10-archive-oa/references/my-borrow.md +49 -0
- package/skills/weaver-e10-asset-idle/SKILL.md +73 -0
- package/skills/weaver-e10-asset-idle/references/idle-list.md +62 -0
- package/skills/weaver-e10-asset-idle/references/idle-stats-and-charts.md +65 -0
- package/skills/weaver-e10-asset-idle/references/source-manifest.json +193 -0
- package/skills/weaver-e10-asset-idle/references/widget-style.md +205 -0
- package/skills/weaver-e10-caizhilian/SKILL.md +29 -11
- package/skills/weaver-e10-caizhilian/references/auth-and-install.md +12 -0
- package/skills/weaver-e10-caizhilian/references/files.md +1 -1
- package/skills/weaver-e10-caizhilian/references/setting-encrypt.md +1 -1
- package/skills/weaver-e10-caizhilian/references/source-manifest.json +45 -45
- package/skills/weaver-e10-cowork/SKILL.md +2 -0
- package/skills/weaver-e10-cowork/references/cowork-write.md +1 -1
- package/skills/weaver-e10-jucailin/SKILL.md +2 -0
- package/skills/weaver-e10-jucailin/references/recruit-read.md +2 -0
- package/skills/weaver-e10-jucailin/references/recruit-write.md +1 -1
- package/skills/weaver-e10-mail/SKILL.md +3 -0
- package/skills/weaver-e10-mail/references/mail-send.md +2 -0
- package/skills/weaver-e10-meeting/SKILL.md +88 -25
- package/skills/weaver-e10-meeting/references/meeting-minute-file.md +25 -11
- package/skills/weaver-e10-meeting.zip +0 -0
- package/skills/weaver-e10-shared/SKILL.md +5 -5
- package/skills/weaver-e10-shared/references/e10-auth-and-session.md +3 -3
- package/skills/weaver-e10-wenshuding/SKILL.md +29 -6
- package/skills/weaver-e10-wenshuding/references/archive-borrow-list.md +14 -0
- package/skills/weaver-e10-wenshuding/references/archive-download.md +2 -0
- package/skills/weaver-e10-wenshuding/references/archive-flow.md +1 -1
- package/skills/weaver-e10-wenshuding/references/archive-prelib-arrange.md +76 -0
- package/skills/weaver-e10-wenshuding/references/archive-prelib-list.md +107 -0
- package/skills/weaver-e10-wenshuding/references/archive-prelib-open.md +59 -0
- package/skills/weaver-e10-wenshuding/references/archive-upload.md +4 -1
- package/skills/weaver-e10-wenshuding/references/source-manifest.json +108 -65
- package/skills/weaver-e10-workflow/SKILL.md +9 -2
- package/skills/weaver-e10-workflow/aigroup/default.json +166 -19
- package/skills/weaver-e10-workflow/references/todo-smart-convert.md +72 -34
- package/skills/weaver-e10-workflow/tools/todo-serve.cjs +113 -25
- package/skills/weaver-e10-workflow/tools/todo-summary.cjs +350 -50
- package/dist/chunks/{chunk-2LV74IWP.js → chunk-E7OK5WD5.js} +5 -5
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 避免每次现场拼脚本,也避免多跑一次 stage=group 进程(每次 CLI 启动约 6~7s)。
|
|
7
7
|
*
|
|
8
8
|
* 用法:
|
|
9
|
-
* node todo-summary.cjs prep --limit
|
|
9
|
+
* node todo-summary.cjs prep [--limit 500] [--exclude 内部留言] [--days 365 | --from A --to B] [--out DIR] # 用户没说时间段就都不传 = 不限时间取前 500 条
|
|
10
10
|
* → 取数、按 workflowName 剔除指定流程、打印分组素材(模型据此做二次分组)
|
|
11
11
|
* node todo-summary.cjs build --dir DIR --plan aiGroups.json
|
|
12
12
|
* → 校验 1..count 全覆盖 → smartGroup;若还有待写摘要则打印,否则直接导出终态
|
|
@@ -17,8 +17,17 @@
|
|
|
17
17
|
*/
|
|
18
18
|
const { execFileSync } = require('node:child_process');
|
|
19
19
|
const fs = require('node:fs');
|
|
20
|
+
const os = require('node:os');
|
|
20
21
|
const path = require('node:path');
|
|
21
22
|
|
|
23
|
+
// 缓存与 aigroup 规则都锚定「当前 skill」(2026-11-01 用户定稿):本脚本就住在
|
|
24
|
+
// <skillDir>/tools/ 下,把自身 skill 目录显式指给 CLI——生成与提取一律用当前 skill
|
|
25
|
+
// 的 cache/,不再走多安装发现排名(否则改名的副本会被旧安装的缓存抢走)。
|
|
26
|
+
// 用户已显式设置时不覆盖。
|
|
27
|
+
if (!process.env.WEAVER_WORK_CLI_SKILL_DIR) {
|
|
28
|
+
process.env.WEAVER_WORK_CLI_SKILL_DIR = path.resolve(__dirname, '..');
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
/**
|
|
23
32
|
* CLI 可执行入口(跨平台)。
|
|
24
33
|
*
|
|
@@ -169,9 +178,10 @@ function groupStats(wfIdGroup) {
|
|
|
169
178
|
/**
|
|
170
179
|
* 导出最终结构。
|
|
171
180
|
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
* 由 Agent
|
|
181
|
+
* 交付形态:stdout 输出 `<browsertool>` 载荷(`[JSON_BEGIN]/[JSON_END]` 之间),
|
|
182
|
+
* 由 Agent 原样贴进回复;三键数据本体另存 `final.data.json`(交付文件复制它的,
|
|
183
|
+
* 命名 `<用户名>_<时间戳>.json` 由 Agent 侧按 references §6.1 拼写)。
|
|
184
|
+
* 旧 `--file`(写 `待办汇总-*.json` 外框归档)已于 2026-11-01 删除:文件名与内容都是旧口径,易误当交付物。
|
|
175
185
|
*
|
|
176
186
|
* 顶层键固定三个:`smartSummaryTitle` / `totalCount` / `wfIdGroup`(用户 2026-10-25 要求加
|
|
177
187
|
* `totalCount` = 当前这批数据的总条数,与各大分组 `count` 之和一致)。
|
|
@@ -220,7 +230,7 @@ function buildTimingReport(state, result) {
|
|
|
220
230
|
lines.push('- 「待写摘要为 0」时脚本会直接导出,不会白跑一次 finish。');
|
|
221
231
|
lines.push('', '## 4. 复现', '');
|
|
222
232
|
lines.push('```bash');
|
|
223
|
-
lines.push('node todo-summary.cjs run --days 30 --limit 500 --out /tmp/t --
|
|
233
|
+
lines.push('node todo-summary.cjs run --days 30 --limit 500 --out /tmp/t --timing-report');
|
|
224
234
|
lines.push('```');
|
|
225
235
|
return `${lines.join('\n')}\n`;
|
|
226
236
|
}
|
|
@@ -311,7 +321,9 @@ function sleepSync(ms) {
|
|
|
311
321
|
* - 快照按目录 mtime 计空闲,超时由服务端自己删;进程在所有快照过期且无请求后才退出。
|
|
312
322
|
*/
|
|
313
323
|
const SERVE_ROOT = path.join(require('node:os').tmpdir(), 'weaver-work-cli-serve');
|
|
314
|
-
const SERVE_VERSION = '
|
|
324
|
+
const SERVE_VERSION = '4';
|
|
325
|
+
/** v4 服务的发布密钥:ensureServeServer 从 current.json 读出,publishSnapshot 发布时回传。 */
|
|
326
|
+
let servePublishKey = '';
|
|
315
327
|
|
|
316
328
|
/**
|
|
317
329
|
* 同步探一次本地数据源的 `/health`:通就返回解析后的 JSON,不通返回 null。
|
|
@@ -362,6 +374,7 @@ function ensureServeServer(portOption) {
|
|
|
362
374
|
// 先探活:既验「真有人在这端口上应答」,也验「应答的是不是我们这个版本的自己」。
|
|
363
375
|
const health = probeHealth(cur.port);
|
|
364
376
|
if (health && health.ok === true && String(health.version) === SERVE_VERSION) {
|
|
377
|
+
servePublishKey = String(cur.publishKey || '');
|
|
365
378
|
return cur.port;
|
|
366
379
|
}
|
|
367
380
|
if (health && health.ok === true) {
|
|
@@ -390,7 +403,10 @@ function ensureServeServer(portOption) {
|
|
|
390
403
|
if (fs.existsSync(registryFile)) {
|
|
391
404
|
try {
|
|
392
405
|
const info = JSON.parse(fs.readFileSync(registryFile, 'utf8'));
|
|
393
|
-
if (info && info.version === SERVE_VERSION && info.port && info.pid === child.pid)
|
|
406
|
+
if (info && info.version === SERVE_VERSION && info.port && info.pid === child.pid) {
|
|
407
|
+
servePublishKey = String(info.publishKey || '');
|
|
408
|
+
return info.port;
|
|
409
|
+
}
|
|
394
410
|
} catch { /* 文件正在写入,稍后重试 */ }
|
|
395
411
|
}
|
|
396
412
|
sleepSync(120);
|
|
@@ -405,6 +421,48 @@ function publishSnapshot(dir, portOption) {
|
|
|
405
421
|
const port = ensureServeServer(portOption);
|
|
406
422
|
if (!port) return null;
|
|
407
423
|
const token = require('node:crypto').randomBytes(12).toString('hex');
|
|
424
|
+
const url = `http://127.0.0.1:${port}/s/${token}/todo.json`;
|
|
425
|
+
const body = fs.readFileSync(dataFile, 'utf8');
|
|
426
|
+
|
|
427
|
+
// v4 主通路:把 JSON 原文 POST 进服务内存(带发布密钥)。
|
|
428
|
+
// 服务端不再读磁盘 ⇒ 不受「常驻进程读权限被沙箱回收」的影响,链接跨命令/跨轮次稳定。
|
|
429
|
+
// POST 用独立的极短 node 子进程同步完成(与 probeHealth 同款手法:主进程 Atomics 阻塞时
|
|
430
|
+
// async 回调排不上队,只能用子进程);body 走 stdin 传给子进程,避免命令行长度问题。
|
|
431
|
+
const postCode = [
|
|
432
|
+
"const http=require('node:http');",
|
|
433
|
+
"const fs=require('node:fs');",
|
|
434
|
+
'const key=process.argv[1];',
|
|
435
|
+
'const port=Number(process.argv[2]);',
|
|
436
|
+
'const token=process.argv[3];',
|
|
437
|
+
"const body=fs.readFileSync(0,'utf8');",
|
|
438
|
+
"const req=http.request({host:'127.0.0.1',port,path:'/s/'+token+'/todo.json',method:'POST',",
|
|
439
|
+
" headers:{'content-type':'application/json','x-publish-key':key,'content-length':Buffer.byteLength(body)},timeout:10000},",
|
|
440
|
+
' (res)=>{let b="";res.setEncoding("utf8");',
|
|
441
|
+
' res.on("data",(d)=>{b+=d;});',
|
|
442
|
+
' res.on("end",()=>{process.stdout.write(b);process.exit(res.statusCode===200?0:3);});',
|
|
443
|
+
' });',
|
|
444
|
+
"req.on('timeout',()=>{req.destroy();process.exit(2);});",
|
|
445
|
+
"req.on('error',()=>process.exit(2));",
|
|
446
|
+
'req.end(body);',
|
|
447
|
+
].join('');
|
|
448
|
+
try {
|
|
449
|
+
const result = execFileSync(
|
|
450
|
+
process.execPath,
|
|
451
|
+
['-e', postCode, servePublishKey, String(port), token],
|
|
452
|
+
{ input: body, encoding: 'utf8', timeout: 20000, maxBuffer: 16 * 1024 * 1024, env: cliEnv() },
|
|
453
|
+
);
|
|
454
|
+
const parsed = JSON.parse(result);
|
|
455
|
+
if (parsed && parsed.ok === true) {
|
|
456
|
+
try {
|
|
457
|
+
fs.writeFileSync(path.join(dir, 'serve.json'), JSON.stringify({
|
|
458
|
+
url, token, port, mode: 'memory', bytes: parsed.bytes,
|
|
459
|
+
}, null, 2));
|
|
460
|
+
} catch { /* 只是排错用的旁路文件 */ }
|
|
461
|
+
return url;
|
|
462
|
+
}
|
|
463
|
+
} catch { /* POST 失败 ⇒ 回落写文件方式(旧服务兼容) */ }
|
|
464
|
+
|
|
465
|
+
// 回落:老路径——写快照文件,由服务端读磁盘(v3 服务或 POST 失败时才走这里)。
|
|
408
466
|
const snapDir = path.join(SERVE_ROOT, 'snapshots', token);
|
|
409
467
|
fs.mkdirSync(snapDir, { recursive: true });
|
|
410
468
|
// 先拷到临时名再拷成正式名:沙箱会拒绝对家目录做 rename(EPERM),copyFile 不受限;
|
|
@@ -413,16 +471,57 @@ function publishSnapshot(dir, portOption) {
|
|
|
413
471
|
fs.copyFileSync(dataFile, tmp);
|
|
414
472
|
fs.copyFileSync(tmp, path.join(snapDir, 'todo.json'));
|
|
415
473
|
try { fs.unlinkSync(tmp); } catch { /* 残留也无害 */ }
|
|
416
|
-
const url = `http://127.0.0.1:${port}/s/${token}/todo.json`;
|
|
417
474
|
try {
|
|
418
|
-
fs.writeFileSync(path.join(dir, 'serve.json'), JSON.stringify({ url, token, port,
|
|
475
|
+
fs.writeFileSync(path.join(dir, 'serve.json'), JSON.stringify({ url, token, port, mode: 'file' }, null, 2));
|
|
419
476
|
} catch { /* 只是排错用的旁路文件 */ }
|
|
420
477
|
return url;
|
|
421
478
|
}
|
|
422
479
|
|
|
480
|
+
/**
|
|
481
|
+
* 交付文件名的用户名(2026-11-01 用户拍板):`auth whoami` 拿 employeeId →
|
|
482
|
+
* `hrm run hrm.employee.detail` 取 `username`(认证系统当前登录人,不是本机账号)。
|
|
483
|
+
* 结果缓存在 state.json——同一次流水线只查一次;查不到返回 null(调用方退回手工命名)。
|
|
484
|
+
*/
|
|
485
|
+
function resolveDeliverUsername(state, dir) {
|
|
486
|
+
if (state.deliveryUsername) return state.deliveryUsername;
|
|
487
|
+
try {
|
|
488
|
+
const who = JSON.parse(runCli(['--json', 'auth', 'whoami'], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, env: cliEnv() }));
|
|
489
|
+
const employeeId = String((who.data || who).employeeId || '').trim();
|
|
490
|
+
if (!employeeId) return null;
|
|
491
|
+
const raw = runCli(['--json', 'hrm', 'run', 'hrm.employee.detail', '--input-json', JSON.stringify({ id: employeeId })], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, env: cliEnv() });
|
|
492
|
+
const detail = JSON.parse(raw);
|
|
493
|
+
const username = String((detail.data || {}).username || '').trim();
|
|
494
|
+
if (!username) return null;
|
|
495
|
+
state.deliveryUsername = username;
|
|
496
|
+
fs.writeFileSync(path.join(dir, 'state.json'), JSON.stringify(state));
|
|
497
|
+
return username;
|
|
498
|
+
} catch {
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** 规则文件缓存(同一进程只读一次)。 */
|
|
504
|
+
let aigroupRuleFileCache = null;
|
|
505
|
+
function loadAigroupRuleFile() {
|
|
506
|
+
if (aigroupRuleFileCache) return aigroupRuleFileCache;
|
|
507
|
+
try {
|
|
508
|
+
aigroupRuleFileCache = JSON.parse(fs.readFileSync(path.join(path.resolve(__dirname, '..'), 'aigroup', 'default.json'), 'utf8'));
|
|
509
|
+
} catch {
|
|
510
|
+
aigroupRuleFileCache = { rules: [] };
|
|
511
|
+
}
|
|
512
|
+
return aigroupRuleFileCache;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** 组名 → 该规则的 summaryRequiredFields(needSummary:false 的组不校验)。 */
|
|
516
|
+
function requiredFieldsFor(title) {
|
|
517
|
+
const rules = loadAigroupRuleFile().rules || [];
|
|
518
|
+
const rule = rules.find((r) => (Array.isArray(r.workflowName) ? r.workflowName : [r.workflowName])
|
|
519
|
+
.some((name) => name && name !== '*' && (title.includes(String(name)) || String(name).includes(title))));
|
|
520
|
+
if (!rule || rule.needSummary === false) return [];
|
|
521
|
+
return Array.isArray(rule.summaryRequiredFields) ? rule.summaryRequiredFields.filter((f) => typeof f === 'string' && f.trim()) : [];
|
|
522
|
+
}
|
|
523
|
+
|
|
423
524
|
function exportFinal(result, dir, state, kept, opts = {}) {
|
|
424
|
-
const { limit, from, to } = state;
|
|
425
|
-
const excluded = (state.exclude || []).join('/');
|
|
426
525
|
const data = result.data;
|
|
427
526
|
const groupTotal = (data.wfIdGroup || []).reduce((sum, g) => sum + (Number(g.count) || 0), 0);
|
|
428
527
|
// CLI 已给 totalCount;缺了(旧版 CLI)就按各组 count 求和兜底,保证三个键恒定存在。
|
|
@@ -432,26 +531,89 @@ function exportFinal(result, dir, state, kept, opts = {}) {
|
|
|
432
531
|
totalCount,
|
|
433
532
|
wfIdGroup: data.wfIdGroup,
|
|
434
533
|
};
|
|
435
|
-
const stamp = new Date().toISOString().slice(0, 10);
|
|
436
|
-
const scope = from && to
|
|
437
|
-
? `${from}~${to}`
|
|
438
|
-
: limit
|
|
439
|
-
? (kept === limit ? `前${limit}条` : `前${limit}条-剩${kept}条`)
|
|
440
|
-
: `全部${kept}条`;
|
|
441
|
-
const stampPart = from && to ? '' : `-${stamp}`;
|
|
442
|
-
const finalFile = path.join(dir, `待办汇总-${scope}${excluded ? `-排除${excluded}` : ''}${stampPart}.json`);
|
|
443
534
|
// browsertool 载荷的 src 跟着当前认证环境走(用户 2026-10-27 要求不写死租户)。
|
|
444
535
|
const baseUrl = resolveBaseUrl(state, dir);
|
|
445
536
|
const stats = groupStats(data.wfIdGroup);
|
|
446
537
|
const total = groupTotal;
|
|
538
|
+
// ── 交付硬闸门(2026-11-01 用户拍板):缺环节 / 静默降级出不了交付物,只有显式豁免才放行 ──
|
|
539
|
+
const allowEmpty = opts.allowEmptySummary === true;
|
|
540
|
+
const detailFailed = Number(opts.detailFailed) || 0;
|
|
541
|
+
const groupEmpty = Number(opts.groupEmpty) || 0;
|
|
542
|
+
if (!allowEmpty && stats.empty > 0) {
|
|
543
|
+
const emptyIds = [];
|
|
544
|
+
for (const group of data.wfIdGroup || []) {
|
|
545
|
+
for (const item of group.displayItems || []) {
|
|
546
|
+
for (const row of item.type === 'request' ? [item.requestData] : (item.requestDatas || [])) {
|
|
547
|
+
if (row && row.requestId && !String(row.summary || '').trim() && !emptyIds.includes(row.requestId)) emptyIds.push(row.requestId);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
console.error('\n✖ 交付拦截:仍有 %d 条单条摘要为空(多为 --summaries 漏 id / 摘要缓存未回填)。缺失 requestId 前 10 个:%s%s',
|
|
552
|
+
stats.empty, emptyIds.slice(0, 10).join(', '), emptyIds.length > 10 ? ' …' : '');
|
|
553
|
+
console.error(' → 补全 --summaries 后重跑 finish,或加 --allow-empty-summary 显式豁免。');
|
|
554
|
+
process.exit(1);
|
|
555
|
+
}
|
|
556
|
+
if (!allowEmpty && groupEmpty > 0) {
|
|
557
|
+
console.error('\n✖ 交付拦截:仍有 %d 个组摘要为空(--group-summaries 漏了这些 groupId)。补全后重跑,或加 --allow-empty-summary 显式豁免。', groupEmpty);
|
|
558
|
+
process.exit(1);
|
|
559
|
+
}
|
|
560
|
+
if (!(opts.allowDetailFail === true) && detailFailed > 0) {
|
|
561
|
+
console.error('\n✖ 交付拦截:详情取数失败 %d 条——这些条目的摘要只能靠标题兜底,内容可能不完整(防"静默降级+单样本外推")。排查登录态/详情缓存后重跑,或加 --allow-detail-fail 显式豁免。', detailFailed);
|
|
562
|
+
process.exit(1);
|
|
563
|
+
}
|
|
564
|
+
// ⑤ 降级话术黑名单(2026-11-01):摘要正文出现"未在标题体现/详见流程"这类表述,
|
|
565
|
+
// 说明执行方把「自己没取到」写成了「业务对象没有」——按模板回查详情补齐后重写。
|
|
566
|
+
const DEGRADE_PHRASES = ['未在标题体现', '标题未体现', '标题里没有', '详见流程', '详见详情', '待确认'];
|
|
567
|
+
const degraded = [];
|
|
568
|
+
for (const group of data.wfIdGroup || []) {
|
|
569
|
+
for (const item of group.displayItems || []) {
|
|
570
|
+
for (const row of item.type === 'request' ? [item.requestData] : (item.requestDatas || [])) {
|
|
571
|
+
const text = String((row && row.summary) || '');
|
|
572
|
+
const hit = DEGRADE_PHRASES.find((phrase) => text.includes(phrase));
|
|
573
|
+
if (hit && row.requestId) degraded.push({ requestId: String(row.requestId), phrase: hit });
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (!allowEmpty && degraded.length > 0) {
|
|
578
|
+
console.error('\n✖ 交付拦截:%d 条摘要含降级话术(把取数失败写成业务信息缺失):%s%s',
|
|
579
|
+
degraded.length,
|
|
580
|
+
degraded.slice(0, 5).map((x) => `${x.requestId.slice(-6)}[${x.phrase}]`).join(' '),
|
|
581
|
+
degraded.length > 5 ? ' …' : '');
|
|
582
|
+
console.error(' → 按模板回查详情缓存或 workflow.detail 补齐要素后重写,或加 --allow-empty-summary 显式豁免。');
|
|
583
|
+
process.exit(1);
|
|
584
|
+
}
|
|
585
|
+
// ⑥ 要素级校验(2026-11-01 用户拍板 P2):规则声明了 summaryRequiredFields 的组,
|
|
586
|
+
// 每条摘要必须包含全部要素关键字,缺任一即拒绝导出("模板要素优先于字数偏好"的机器化)。
|
|
587
|
+
const missingFields = [];
|
|
588
|
+
for (const group of data.wfIdGroup || []) {
|
|
589
|
+
const required = requiredFieldsFor(String(group.title || ''));
|
|
590
|
+
if (!required.length) continue;
|
|
591
|
+
for (const item of group.displayItems || []) {
|
|
592
|
+
for (const row of item.type === 'request' ? [item.requestData] : (item.requestDatas || [])) {
|
|
593
|
+
if (!row || !row.requestId) continue;
|
|
594
|
+
const text = String(row.summary || '');
|
|
595
|
+
if (!text.trim()) continue; // 空摘要由闸门①负责
|
|
596
|
+
const missing = required.filter((keyword) => !text.includes(keyword));
|
|
597
|
+
if (missing.length > 0) missingFields.push({ requestId: String(row.requestId), missing });
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (!allowEmpty && missingFields.length > 0) {
|
|
602
|
+
console.error('\n✖ 交付拦截:%d 条摘要缺模板要素(要素级校验):%s%s',
|
|
603
|
+
missingFields.length,
|
|
604
|
+
missingFields.slice(0, 5).map((x) => `${x.requestId.slice(-6)}缺[${x.missing.join(',')}]`).join(' '),
|
|
605
|
+
missingFields.length > 5 ? ' …' : '');
|
|
606
|
+
console.error(' → 按模板回查详情补齐要素后重写,或加 --allow-empty-summary 显式豁免。');
|
|
607
|
+
process.exit(1);
|
|
608
|
+
}
|
|
447
609
|
console.log('\n最终结构(共 %d 条 / %d 组|totalCount=%d):', total, data.wfIdGroup.length, totalCount);
|
|
448
610
|
for (const row of stats.rows) {
|
|
449
611
|
console.log(` ${row.title} | ${row.count} | ${row.display}${row.singles ? ` +单条${row.singles}` : ''}`);
|
|
450
612
|
}
|
|
451
613
|
// 叠加视图:优先组的条目同时留在工作流组里,所以 Σcount 会大于 totalCount(去重条数)。
|
|
452
|
-
console.log('\n自检:totalCount=%d(去重)|Σcount=%d(含优先组叠加 %d)|汇总项覆盖 %d + 单条 %d | 空摘要 %d |
|
|
614
|
+
console.log('\n自检:totalCount=%d(去重)|Σcount=%d(含优先组叠加 %d)|汇总项覆盖 %d + 单条 %d | 空摘要 %d | 三键文件 %s',
|
|
453
615
|
totalCount, total, Math.max(0, total - totalCount), stats.covered, stats.singles, stats.empty,
|
|
454
|
-
|
|
616
|
+
path.join(dir, 'final.data.json'));
|
|
455
617
|
const meta = result.meta || {};
|
|
456
618
|
const steps = (meta.timing && meta.timing.steps) || [];
|
|
457
619
|
console.log('CLI 自报分段:%s', steps.map((s) => `${s.name}:${s.kind}=${fmt(s.durationMs)}`).join(' '));
|
|
@@ -496,6 +658,26 @@ function exportFinal(result, dir, state, kept, opts = {}) {
|
|
|
496
658
|
lines.push(' ]', '}');
|
|
497
659
|
// 供 todo-serve.cjs 读取的同构数据(只有三键,无外框):服务与载荷永远读同一份,不会出现两份数据。
|
|
498
660
|
fs.writeFileSync(path.join(dir, 'final.data.json'), JSON.stringify(payload));
|
|
661
|
+
// 交付文件(2026-11-01 用户拍板):由**脚本**按「认证用户名_YYYYmmddHHMMSS.json」落到 ~/Downloads/,
|
|
662
|
+
// 执行方默认不展示该文件(正文只有组件块)——仅当用户明确要求输出 json 时才交付。
|
|
663
|
+
// 用户名 = auth whoami → hrm.employee.detail.username,state 缓存免重复查询;失败则不生成并在输出里说明。
|
|
664
|
+
let deliveredFile = null;
|
|
665
|
+
const username = resolveDeliverUsername(state, dir);
|
|
666
|
+
if (username) {
|
|
667
|
+
try {
|
|
668
|
+
const d = new Date();
|
|
669
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
670
|
+
const stamp = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
671
|
+
const downloads = path.join(os.homedir(), 'Downloads');
|
|
672
|
+
fs.mkdirSync(downloads, { recursive: true });
|
|
673
|
+
deliveredFile = path.join(downloads, `${username}_${stamp}.json`);
|
|
674
|
+
fs.writeFileSync(deliveredFile, JSON.stringify(payload));
|
|
675
|
+
} catch {
|
|
676
|
+
deliveredFile = null;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (deliveredFile) console.log('已生成备用文件:%s(默认不展示;仅当用户明确要求输出 json 时再交付——交付口径以 references §6.1 顶部为准)', deliveredFile);
|
|
680
|
+
else console.log('备用文件未生成(用户名查询失败或 Downloads 不可写):如用户要求输出 json,把 %s 复制为 ~/Downloads/<用户名>_<YYYYmmddHHMMSS>.json 后交付', path.join(dir, 'final.data.json'));
|
|
499
681
|
// 数据源模式:`--data-source <url>` 直接用给定地址;`--serve [端口]` 现起一个本地服务拿地址。
|
|
500
682
|
let dataSourceUrl = opts.dataSource && opts.dataSource !== true ? String(opts.dataSource) : null;
|
|
501
683
|
if (!dataSourceUrl && opts.serve) dataSourceUrl = publishSnapshot(dir, opts.serve);
|
|
@@ -505,9 +687,8 @@ function exportFinal(result, dir, state, kept, opts = {}) {
|
|
|
505
687
|
console.log('\n数据源模式:params.dataSource = %s', dataSourceUrl);
|
|
506
688
|
console.log(' 组件改为 GET 该地址(响应体就是 params.data 的三键对象);单进程多快照,快照空闲 30 分钟后自动清理');
|
|
507
689
|
}
|
|
508
|
-
//
|
|
509
|
-
// `--file`
|
|
510
|
-
if (opts.writeFile) fs.writeFileSync(finalFile, finalText);
|
|
690
|
+
// 交付 = 回复里内联 <browsertool> 整块 + 另存 final.data.json(Agent 复制它按 §6.1 命名交付)。
|
|
691
|
+
// 旧 `--file` 分支已删(2026-11-01):它写的 `待办汇总-*.json` 是外框归档,命名/内容均为旧口径,易误当交付物。
|
|
511
692
|
const delivery = `<browsertool>\n${finalText.trimEnd()}\n</browsertool>`;
|
|
512
693
|
console.log('\n[JSON_BEGIN]');
|
|
513
694
|
console.log(delivery);
|
|
@@ -525,7 +706,6 @@ function exportFinal(result, dir, state, kept, opts = {}) {
|
|
|
525
706
|
console.log('\n耗时摘要(仅 CLI 执行链路,不含模型往返):');
|
|
526
707
|
console.log(conclusion);
|
|
527
708
|
}
|
|
528
|
-
return opts.writeFile ? finalFile : null;
|
|
529
709
|
}
|
|
530
710
|
|
|
531
711
|
/**
|
|
@@ -566,6 +746,17 @@ function cmdPrep(args) {
|
|
|
566
746
|
if (args['sub-category'] && args['sub-category'] !== true) {
|
|
567
747
|
input.subCategory = String(args['sub-category']);
|
|
568
748
|
}
|
|
749
|
+
// 2026-11-02:小分类的等价直传入口。显式 tabid 优先级高于 subCategory/tabName
|
|
750
|
+
// (CLI buildSearchQuery:先解析 subCategory/tabName,再用显式 tabid 覆盖)。
|
|
751
|
+
// 仅影响本次汇总取数范围,不触碰 workflow.search 的默认行为。
|
|
752
|
+
if (args.tabid && args.tabid !== true) {
|
|
753
|
+
input.tabid = String(args.tabid);
|
|
754
|
+
}
|
|
755
|
+
// 2026-11-02:备注筛选(getTodoListData params.isremarks),多个数字英文逗号拼接。
|
|
756
|
+
// 仅汇总链路生效(CLI collect 内部注入 params),workflow.search 不受影响。
|
|
757
|
+
if (args.isremarks && args.isremarks !== true) {
|
|
758
|
+
input.isremarks = String(args.isremarks);
|
|
759
|
+
}
|
|
569
760
|
// 摘要字数口径要带进 collect:否则缓存命中按默认 50 字判定,
|
|
570
761
|
// 用户要求「10 字以内」时仍会回填旧的长摘要(2026-10-22 实测踩到)。
|
|
571
762
|
if (args['summary-chars'] && args['summary-chars'] !== true) {
|
|
@@ -595,6 +786,29 @@ function cmdPrep(args) {
|
|
|
595
786
|
const kept = exclude.length ? all.filter((row) => !exclude.includes(String(row.workflowName || ''))) : all;
|
|
596
787
|
const dropped = all.length - kept.length;
|
|
597
788
|
|
|
789
|
+
// 2026-11-03 用户定稿:collect 空结果按「是否带显式限制条件」给不同文案,直接结束(exit 0),
|
|
790
|
+
// 不进 build(CLI 会抛 items_empty 校验错误)。
|
|
791
|
+
// - 无限制条件(如「汇总我的待办」)⇒ 「您当前没有待办。」
|
|
792
|
+
// - 带限制条件(时间范围 / 小分类 / tabid / 参与身份 / 排除清单 / 侧重点)⇒
|
|
793
|
+
// 「您当前没有符合条件的待办,可以尝试调整限制数据的范围后重试。」并回显条件。
|
|
794
|
+
if (all.length === 0 || kept.length === 0) {
|
|
795
|
+
const constraints = [];
|
|
796
|
+
if (days > 0) constraints.push(`近 ${days} 天`);
|
|
797
|
+
if (args.from && args.to) constraints.push(`${args.from} 至 ${args.to}`);
|
|
798
|
+
if (input.subCategory) constraints.push(`小分类「${input.subCategory}」`);
|
|
799
|
+
if (input.tabid) constraints.push(`tabid=${input.tabid}`);
|
|
800
|
+
if (input.isremarks) constraints.push(`参与身份 ${input.isremarks}`);
|
|
801
|
+
if (exclude.length) constraints.push(`排除流程「${exclude.join('、')}」`);
|
|
802
|
+
if (input.groupFocus) constraints.push(`侧重点「${input.groupFocus}」`);
|
|
803
|
+
if (all.length === 0 && constraints.length === 0) {
|
|
804
|
+
console.log('您当前没有待办。');
|
|
805
|
+
} else {
|
|
806
|
+
console.log('您当前没有符合条件的待办,可以尝试调整限制数据的范围后重试。%s',
|
|
807
|
+
constraints.length ? `(当前条件:${constraints.join('、')})` : '');
|
|
808
|
+
}
|
|
809
|
+
return 'EMPTY';
|
|
810
|
+
}
|
|
811
|
+
|
|
598
812
|
// 组结构直接用 CLI 返回的 wfIdGroup(已做 priority 抽取,不能再自己按 workflowName 复算,
|
|
599
813
|
// 否则会把被抽进「建议优先处理」的条目重复计入原流程)。
|
|
600
814
|
// 注意:wfIdGroup[].requestDatas 里**没有 workflowName**,要按 requestId 从 items 回查,
|
|
@@ -657,12 +871,44 @@ function cmdPrep(args) {
|
|
|
657
871
|
fmt(collectMs), fmt(collectWallMs));
|
|
658
872
|
console.log('输出目录:%s\n', dir);
|
|
659
873
|
const focusKinds = focusCoversKinds(args.focus && args.focus !== true ? args.focus : '');
|
|
874
|
+
// 规则里 focus:false 的组**必须**由模型给 aiGroups(如 安全问题处理工单),prep 阶段就标出来,
|
|
875
|
+
// 不要等 build 报 ai_groups_required 才知道(2026-11-01 用户确认的规范缺陷)。
|
|
876
|
+
// 同时**回显该组的 groupingPrompt(组名词表/分法)与 requestSummaryPrompt(摘要模板)**——
|
|
877
|
+
// 这两样只存在于 aigroup/default.json,规则要求"生成 aiGroups 前必须先读它",
|
|
878
|
+
// 但 prep 原本不输出 ⇒ 执行方只能凭正文推断(自造组名/漏摘要要素的根因,2026-11-01 复盘定稿)。
|
|
879
|
+
const mustPlanRules = [];
|
|
880
|
+
try {
|
|
881
|
+
const ruleFile = JSON.parse(fs.readFileSync(path.join(path.resolve(__dirname, '..'), 'aigroup', 'default.json'), 'utf8'));
|
|
882
|
+
for (const rule of ruleFile.rules || []) {
|
|
883
|
+
if (rule.aiGroup && rule.aiGroup.groupRule && rule.aiGroup.groupRule.focus === false) {
|
|
884
|
+
const names = Array.isArray(rule.workflowName) ? rule.workflowName : [rule.workflowName];
|
|
885
|
+
for (const name of names) {
|
|
886
|
+
if (name && name !== '*') {
|
|
887
|
+
mustPlanRules.push({
|
|
888
|
+
name: String(name),
|
|
889
|
+
groupingPrompt: String((rule.aiGroup.groupRule || {}).groupingPrompt || ''),
|
|
890
|
+
requestSummaryPrompt: String(rule.requestSummaryPrompt || ''),
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
} catch { /* 规则文件读不到就不标注(build 仍会硬校验兜底) */ }
|
|
897
|
+
const matchRule = (title) => mustPlanRules.find((r) => title.includes(r.name) || r.name.includes(title));
|
|
898
|
+
const needsPlan = (title, count) => count >= 3 && (title.includes('内部留言') || Boolean(matchRule(title)));
|
|
660
899
|
for (const group of planGroups) {
|
|
661
900
|
// 只有「默认确定性类型 + 侧重点没覆盖本类型」才算无需分组;被覆盖时照常列出条目。
|
|
662
901
|
const kind = kindOfTitle(group.title);
|
|
663
902
|
const skip = group.groupId === 'priority' || group.count <= 1 || (kind !== '' && !focusKinds.has(kind));
|
|
664
|
-
|
|
903
|
+
const rule = skip ? null : matchRule(group.title);
|
|
904
|
+
const marker = skip ? ' [无需分组]' : (rule || group.title.includes('内部留言') ? ' [必须给 plan]' : '');
|
|
905
|
+
console.log(`=== ${group.groupId} | ${group.title} | ${group.count}${marker}`);
|
|
665
906
|
if (skip) continue;
|
|
907
|
+
// 回显规则内容:组名/分法只按 groupingPrompt 来(词表不得自造),摘要按 requestSummaryPrompt 写
|
|
908
|
+
if (rule) {
|
|
909
|
+
if (rule.groupingPrompt) console.log(` ▸ 分组口径(groupingPrompt,组名/分法只按它来,不得自造):${rule.groupingPrompt}`);
|
|
910
|
+
if (rule.requestSummaryPrompt) console.log(` ▸ 摘要模板(requestSummaryPrompt,本组单条摘要照它写):${rule.requestSummaryPrompt}`);
|
|
911
|
+
}
|
|
666
912
|
for (const row of group.rows) {
|
|
667
913
|
const hint = fieldHintById.get(row.requestId) || '';
|
|
668
914
|
// 提示放在**同一行**:两行格式容易让人(和模型)回头去匹配标题,而标题里往往没有类型词。
|
|
@@ -670,7 +916,7 @@ function cmdPrep(args) {
|
|
|
670
916
|
}
|
|
671
917
|
}
|
|
672
918
|
console.log('\n下一步:node %s build --dir %s', path.basename(__filename), dir);
|
|
673
|
-
console.log('
|
|
919
|
+
console.log('(标了 [必须给 plan] 的组——内部留言、以及规则 focus:false 的组如 安全问题处理工单——build 时必须带 --plan <aiGroups.json>;组名/分法/摘要口径见各组下回显的规则内容;其余走确定性分组)');
|
|
674
920
|
}
|
|
675
921
|
|
|
676
922
|
function cmdBuild(args) {
|
|
@@ -680,7 +926,7 @@ function cmdBuild(args) {
|
|
|
680
926
|
const kept = readJson(path.join(dir, 'items.json'));
|
|
681
927
|
const planGroups = state.planGroups || [];
|
|
682
928
|
const countById = new Map(planGroups.map((g) => [g.groupId, g.count]));
|
|
683
|
-
// `--plan
|
|
929
|
+
// `--plan`:确定性类型可不传;prep 标了 [必须给 plan] 的组(内部留言 / focus:false 组)不传会被 CLI 拒(ai_groups_required)。
|
|
684
930
|
// 不传时直接走 CLI 确定性分组(等价于 aiGroups 未传)。
|
|
685
931
|
let aiGroups;
|
|
686
932
|
if (planFile) {
|
|
@@ -742,17 +988,36 @@ function cmdBuild(args) {
|
|
|
742
988
|
fs.writeFileSync(path.join(dir, 'state.json'), JSON.stringify(state));
|
|
743
989
|
|
|
744
990
|
if (pending.length === 0) {
|
|
745
|
-
|
|
991
|
+
// 即使无待写摘要也必须走 finish:summary 阶段还负责满意度类(needSummary:false)的标题
|
|
992
|
+
// 填充与交付闸门——smartGroup 半成品直接导出会让这类条目空着(2026-11-01 闸门真机首跑抓到)。
|
|
993
|
+
cmdFinish({ ...args, dir });
|
|
746
994
|
return;
|
|
747
995
|
}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
996
|
+
// 2026-11-02 P3-B:素材无损落盘(此前整段吐控制台,268 条时 build 输出达 850KB,
|
|
997
|
+
// 模型要多轮读长文本还易被截断);控制台只留批次索引,模型用 Read 按需读 need-summaries.json。
|
|
998
|
+
const needFile = path.join(dir, 'need-summaries.json');
|
|
999
|
+
fs.writeFileSync(needFile, JSON.stringify(batches, null, 2));
|
|
1000
|
+
console.log(`\n⚠ NEEDS_SUMMARIES:%d 条单条摘要待写(完整素材已写入 %s):`, pending.length, needFile);
|
|
1001
|
+
for (const b of batches) {
|
|
1002
|
+
const items = b.items || [];
|
|
1003
|
+
if (items.length === 0) continue;
|
|
1004
|
+
console.log(` - ${b.batchId}|${b.groupTitle}${b.subGroupTitle && b.subGroupTitle !== b.groupTitle ? ` → ${b.subGroupTitle}` : ''}|${items.length} 条|目标字数 ${b.suggestedMaxChars}${b.prompt ? `|口径:${b.prompt}` : ''}`);
|
|
1005
|
+
if (b.hint) console.log(` 提示:${b.hint}`);
|
|
1006
|
+
}
|
|
1007
|
+
console.log(' → 读 need-summaries.json(items[].requestId + context),摘要写成 aiSummaries.json(元素 {requestId,summary})后重跑:');
|
|
1008
|
+
console.log(' node %s finish --dir %s --summaries <aiSummaries.json> --timing-report', path.basename(__filename), dir);
|
|
1009
|
+
// 组摘要(aiGroup.groupSummary.needAI=true 的汇总项):口径随批次下发,模型按它写 aiGroupSummaries
|
|
1010
|
+
const gsBatches = sg.data.groupSummaryBatches || [];
|
|
1011
|
+
if (gsBatches.length > 0) {
|
|
1012
|
+
console.log('\n待写组摘要(写成 aiGroupSummaries.json,元素 {groupId,summary}):');
|
|
1013
|
+
for (const b of gsBatches) {
|
|
1014
|
+
console.log(`### 组摘要 ${b.groupId}|${b.parentTitle} → ${b.title}|${b.count} 条|目标字数 ${b.suggestedMaxChars}`);
|
|
1015
|
+
if (b.prompt) console.log(`口径:${b.prompt}`);
|
|
1016
|
+
if (b.hint) console.log(`提示:${b.hint}`);
|
|
1017
|
+
if (b.stats && b.stats.tail) console.log(`tail:${b.stats.tail}`);
|
|
1018
|
+
}
|
|
1019
|
+
console.log('\nfinish 时加:--group-summaries <aiGroupSummaries.json>');
|
|
754
1020
|
}
|
|
755
|
-
console.log('\n下一步:node %s finish --dir %s --summaries <aiSummaries.json>', path.basename(__filename), dir);
|
|
756
1021
|
}
|
|
757
1022
|
|
|
758
1023
|
function cmdFinish(args) {
|
|
@@ -788,6 +1053,9 @@ function cmdFinish(args) {
|
|
|
788
1053
|
fs.writeFileSync(path.join(dir, 'state.json'), JSON.stringify(state));
|
|
789
1054
|
console.log('summary 内部 %s / 含进程启动 %s(回填 %d 条,其余走缓存预填)',
|
|
790
1055
|
summaryInternal === null ? '-' : fmt(summaryInternal), fmt(summaryWallMs), aiSummaries.length);
|
|
1056
|
+
// 未回填的缺失 id 不在这里单列:summary 阶段的信封不带 summaryBatches/summaryStats,
|
|
1057
|
+
// 空 id 列表统一由 exportFinal 的闸门①从最终结构里就地提取(能直接给出 requestId)。
|
|
1058
|
+
// 未知 requestId 的 aiSummaries 由 CLI 侧硬校验拒绝(ai_summary_unknown_request,非零退出)。
|
|
791
1059
|
let finalResult = result;
|
|
792
1060
|
const groupBatches = result.data.groupSummaryBatches || [];
|
|
793
1061
|
if (groupBatches.length > 0) {
|
|
@@ -796,7 +1064,10 @@ function cmdFinish(args) {
|
|
|
796
1064
|
: null;
|
|
797
1065
|
if (groupSummariesFile) {
|
|
798
1066
|
// 组摘要必须由模型写、且每轮重算;这里只是把「回填」并进同一次命令,省掉一次模型往返。
|
|
799
|
-
|
|
1067
|
+
// need-group-summaries.json 可直接指回:这里投影成 CLI 需要的 {groupId,summary},
|
|
1068
|
+
// 多余字段(title/items/stats 等)不外传,避免 CLI 白名单校验拒绝。
|
|
1069
|
+
const aiGroupSummaries = readJson(groupSummariesFile)
|
|
1070
|
+
.map((x) => ({ groupId: x.groupId || x.id, summary: x.summary }));
|
|
800
1071
|
const t2 = Date.now();
|
|
801
1072
|
const gs = runStage({
|
|
802
1073
|
smartSummaryTitle: result.data.smartSummaryTitle,
|
|
@@ -816,9 +1087,22 @@ function cmdFinish(args) {
|
|
|
816
1087
|
for (const text of gsWarnings) console.log(' ⚠ %s', text);
|
|
817
1088
|
finalResult = gs;
|
|
818
1089
|
} else {
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
1090
|
+
// 2026-11-02 P2 定稿:组摘要未回填 ≠ 校验失败。完整素材落盘(此前控制台每批只打
|
|
1091
|
+
// JSON.stringify(batch).slice(0, 400) 的预览,等于没有素材),以 exit 0 + NEEDS_GROUP_SUMMARIES
|
|
1092
|
+
// 返回、不导出;真失败(空单条摘要/降级话术/要素缺失/详情失败)仍由 exportFinal 以 exit 1 拦截。
|
|
1093
|
+
const needFile = path.join(dir, 'need-group-summaries.json');
|
|
1094
|
+
fs.writeFileSync(needFile, JSON.stringify(groupBatches, null, 2));
|
|
1095
|
+
console.log('\n⚠ NEEDS_GROUP_SUMMARIES:%d 个组摘要待回填(完整素材已写入 %s):', groupBatches.length, needFile);
|
|
1096
|
+
for (const batch of groupBatches) {
|
|
1097
|
+
console.log(' - %s|%s → %s|%d 条', batch.groupId, batch.parentTitle, batch.title, batch.count);
|
|
1098
|
+
}
|
|
1099
|
+
if (args['allow-empty-summary'] === true) {
|
|
1100
|
+
console.log(' --allow-empty-summary 已指定:跳过回填、按空组摘要继续导出。');
|
|
1101
|
+
} else {
|
|
1102
|
+
console.log(' → 在该文件的每个元素里补 "summary" 字段(脚本会自动只取 groupId/summary),重跑:');
|
|
1103
|
+
console.log(' node %s finish --dir %s --group-summaries %s --timing-report', path.basename(__filename), dir, needFile);
|
|
1104
|
+
return; // 不导出、退出码 0:素材已备齐、等回填,不是失败
|
|
1105
|
+
}
|
|
822
1106
|
}
|
|
823
1107
|
}
|
|
824
1108
|
// 2026-10-28 用户 OA 页面实测通过:默认走本地数据源(params.dataSource),
|
|
@@ -827,10 +1111,15 @@ function cmdFinish(args) {
|
|
|
827
1111
|
? false
|
|
828
1112
|
: (args.serve === undefined ? true : args.serve);
|
|
829
1113
|
exportFinal(finalResult, dir, state, kept.length, {
|
|
830
|
-
writeFile: args.file === true,
|
|
831
1114
|
timingReport: args['timing-report'],
|
|
832
1115
|
dataSource: args['data-source'],
|
|
833
1116
|
serve: serveOption,
|
|
1117
|
+
// 交付硬闸门(2026-11-01):空摘要 / 详情取数失败都要显式豁免才能出文件。
|
|
1118
|
+
// 详情取数发生在 smartGroup 阶段 ⇒ failed 从 smartgroup.json 的 data.summaryStats 取。
|
|
1119
|
+
allowEmptySummary: args['allow-empty-summary'] === true,
|
|
1120
|
+
allowDetailFail: args['allow-detail-fail'] === true,
|
|
1121
|
+
detailFailed: ((sg.data || {}).summaryStats || {}).failed || 0,
|
|
1122
|
+
groupEmpty: (finalResult.meta && finalResult.meta.groupEmpty) || 0,
|
|
834
1123
|
});
|
|
835
1124
|
console.log('总耗时(脚本口径):%s', fmt(Date.now() - Date.parse(state.startedAt)));
|
|
836
1125
|
}
|
|
@@ -847,23 +1136,30 @@ function cmdRun(args) {
|
|
|
847
1136
|
const dir = args.out && args.out !== true
|
|
848
1137
|
? String(args.out)
|
|
849
1138
|
: `/tmp/todo-summary-${new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14)}`;
|
|
850
|
-
|
|
1139
|
+
// collect 空结果已在 prep 里给出用户文案并结束(EMPTY),不再进 build/finish
|
|
1140
|
+
if (cmdPrep({ ...args, out: dir }) === 'EMPTY') return;
|
|
851
1141
|
cmdBuild({ ...args, dir });
|
|
1142
|
+
// 无待写摘要时 cmdBuild 已经内部走完 cmdFinish(含满意度标题填充与交付闸门),这里直接收工
|
|
1143
|
+
if (fs.existsSync(path.join(dir, 'final.data.json'))) return;
|
|
1144
|
+
// build→finish 已把组摘要素材落盘并停下(NEEDS_GROUP_SUMMARIES,exit 0)⇒ 不再重复跑 finish,
|
|
1145
|
+
// 也不再重复打印(cmdFinish 刚打过完整指引),静默收工(2026-11-02 P2)。
|
|
1146
|
+
if (fs.existsSync(path.join(dir, 'need-group-summaries.json'))) return;
|
|
852
1147
|
const sg = readJson(path.join(dir, 'smartgroup.json'));
|
|
853
1148
|
const pending = ((sg.data.summaryBatches || []).flatMap((batch) => batch.items || []));
|
|
854
1149
|
if (pending.length > 0) {
|
|
855
|
-
console.log('\n⚠ 有 %d
|
|
856
|
-
console.log(' node %s finish --dir %s --summaries <aiSummaries.json> --
|
|
1150
|
+
console.log('\n⚠ 有 %d 条待写摘要:素材见 %s;写成 aiSummaries.json 后跑 finish(**不用重跑** collect/smartGroup)', pending.length, path.join(dir, 'need-summaries.json'));
|
|
1151
|
+
console.log(' node %s finish --dir %s --summaries <aiSummaries.json> --timing-report', path.basename(__filename), dir);
|
|
857
1152
|
return;
|
|
858
1153
|
}
|
|
859
|
-
//
|
|
1154
|
+
// build 停下但没导出(如组摘要待回填)⇒ 把 finish 跑完(它自己会按闸门决定放行/拦截)
|
|
860
1155
|
cmdFinish({
|
|
861
1156
|
dir,
|
|
862
|
-
file: args.file === true,
|
|
863
1157
|
'timing-report': args['timing-report'],
|
|
864
1158
|
'group-summaries': args['group-summaries'],
|
|
865
1159
|
serve: args.serve,
|
|
866
1160
|
'no-serve': args['no-serve'],
|
|
1161
|
+
'allow-empty-summary': args['allow-empty-summary'] === true,
|
|
1162
|
+
'allow-detail-fail': args['allow-detail-fail'] === true,
|
|
867
1163
|
});
|
|
868
1164
|
}
|
|
869
1165
|
|
|
@@ -875,10 +1171,14 @@ else if (cmd === 'finish') cmdFinish(args);
|
|
|
875
1171
|
else if (cmd === 'run') cmdRun(args);
|
|
876
1172
|
else {
|
|
877
1173
|
console.log('用法:node ' + path.basename(__filename) + ' <run|prep|build|finish> [options]');
|
|
878
|
-
console.log(' run --days
|
|
879
|
-
console.log(' prep --limit 100 [--exclude 内部留言] [--days 1460] [--sub-category 待处理] [--out DIR]');
|
|
880
|
-
console.log(' build --dir DIR [--plan aiGroups.json] #
|
|
881
|
-
console.log(' finish --dir DIR [--summaries aiSummaries.json] [--group-summaries aiGroupSummaries.json] [--
|
|
1174
|
+
console.log(' run [--days N | --from A --to B] [--limit 500] [--out DIR] [--timing-report] # 一次跑完 prep+build;用户没说时间段就都不传 = 不限时间取前 500 条');
|
|
1175
|
+
console.log(' prep --limit 100 [--exclude 内部留言] [--days 1460] [--sub-category 待处理] [--tabid 7] [--isremarks 1,2] [--out DIR] # 小分类:--sub-category/--tabid(显式 tabid 优先);isremarks=备注筛选(数字逗号拼接)');
|
|
1176
|
+
console.log(' build --dir DIR [--plan aiGroups.json] # prep 标注 [必须给 plan] 的组必须传;其余不传走确定性分组');
|
|
1177
|
+
console.log(' finish --dir DIR [--summaries aiSummaries.json] [--group-summaries aiGroupSummaries.json] [--timing-report]');
|
|
1178
|
+
console.log(' 待写摘要素材:build/ run 停下时写 <out>/need-summaries.json(无损),控制台只留批次索引;写 aiSummaries.json 后 finish --summaries 回填');
|
|
1179
|
+
console.log(' 组摘要未回填:以 exit 0 + NEEDS_GROUP_SUMMARIES 返回(不导出),完整素材在 <out>/need-group-summaries.json;');
|
|
1180
|
+
console.log(' 在该文件补 summary 后用 --group-summaries 指回它重跑。真失败(空摘要/降级话术/要素缺失/详情失败)仍 exit 1;');
|
|
1181
|
+
console.log(' 豁免:--allow-empty-summary / --allow-detail-fail');
|
|
882
1182
|
console.log(' 默认:载荷只带 params.dataSource(现起本地服务,空闲 30 分钟自动退出);--no-serve 退回内联 data');
|
|
883
1183
|
console.log(' 提示:`--timing-report` 会在控制台打一段耗时摘要(阶段墙钟 / CLI 内部),**不产出 md**');
|
|
884
1184
|
process.exit(2);
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import { createRequire as __esbuildCreateRequire } from 'node:module';
|
|
2
2
|
const require = __esbuildCreateRequire(import.meta.url);
|
|
3
|
-
import {
|
|
4
|
-
imBusinessError,
|
|
5
|
-
imConfirmationError,
|
|
6
|
-
imValidationError
|
|
7
|
-
} from "./chunk-ZEDWSH32.js";
|
|
8
3
|
import {
|
|
9
4
|
collapseNewlines,
|
|
10
5
|
collectI18nIds,
|
|
@@ -17,6 +12,11 @@ import {
|
|
|
17
12
|
parseMsg,
|
|
18
13
|
senderName
|
|
19
14
|
} from "./chunk-YLCYX7LE.js";
|
|
15
|
+
import {
|
|
16
|
+
imBusinessError,
|
|
17
|
+
imConfirmationError,
|
|
18
|
+
imValidationError
|
|
19
|
+
} from "./chunk-ZEDWSH32.js";
|
|
20
20
|
|
|
21
21
|
// src/shortcuts/im/operations/types.ts
|
|
22
22
|
var READ = "read";
|