weaver-work-cli 0.1.38 → 0.1.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/chunks/{archive-PDV724YK.js → archive-ENZYBBME.js} +556 -66
- package/dist/chunks/{archive-oa-4XE4MNXQ.js → archive-oa-YWLEC5V4.js} +2 -2
- package/dist/chunks/{asset-PDL2QE2N.js → asset-OOV6DWPA.js} +2 -2
- package/dist/chunks/asset-idle-RE632MY5.js +670 -0
- package/dist/chunks/{attachment-content-MDDGOE5E.js → attachment-content-RWN2FCLL.js} +3 -3
- package/dist/chunks/{blog-UYR3EUXW.js → blog-PEPOJAL7.js} +2 -2
- package/dist/chunks/{caizhilian-LG2X5NMF.js → caizhilian-WYIQDVPJ.js} +2 -2
- package/dist/chunks/{calendar-FG4BAZHE.js → calendar-CIARBAOX.js} +2 -2
- package/dist/chunks/{chunk-6FTTSA45.js → chunk-36DIFZAH.js} +2 -2
- package/dist/chunks/{chunk-UJJD4QWF.js → chunk-BJGXMFGD.js} +2 -2
- package/dist/chunks/{chunk-ELYDWBZJ.js → chunk-CQHGNCK2.js} +1 -1
- package/dist/chunks/{chunk-XJY7G4DD.js → chunk-EINRJHQ3.js} +3 -3
- package/dist/chunks/{chunk-UZZBA6PV.js → chunk-EZGFHJFW.js} +28 -6
- package/dist/chunks/{chunk-NI6GIPIM.js → chunk-F2EQXUEE.js} +438 -387
- package/dist/chunks/{chunk-VRYHGG6O.js → chunk-GVPJZXJ2.js} +3 -3
- package/dist/chunks/{chunk-IZUDA353.js → chunk-HJYVHXUQ.js} +1 -1
- package/dist/chunks/{chunk-KKRL3REV.js → chunk-I2HB737U.js} +2 -2
- package/dist/chunks/{chunk-NR6RKXPA.js → chunk-K3RUEEOL.js} +4 -2
- package/dist/chunks/{chunk-6LTFIFLM.js → chunk-KTCB5R5H.js} +2 -2
- package/dist/chunks/{chunk-J7USS3ED.js → chunk-L4N7B7ZD.js} +4 -4
- package/dist/chunks/{chunk-CORFRD7Q.js → chunk-MUQUHKPG.js} +2 -2
- package/dist/chunks/{chunk-DMV2TOMH.js → chunk-NLK2RZKG.js} +46 -46
- package/dist/chunks/{chunk-JSMFPO43.js → chunk-P2DAISYW.js} +1 -1
- package/dist/chunks/{chunk-GGJFMNJM.js → chunk-T7JDJJ7H.js} +3 -3
- package/dist/chunks/{chunk-OJQUJZQF.js → chunk-XXJOSZTQ.js} +2 -0
- package/dist/chunks/{chunk-Z32KWY6G.js → chunk-YS5DPN4T.js} +1 -1
- package/dist/chunks/{chunk-EG6ZU444.js → chunk-ZUX3P6BQ.js} +10 -5
- package/dist/chunks/{chunk-FUWVHMVV.js → chunk-ZXRNHZTE.js} +2 -2
- package/dist/chunks/{conflict-BQZEQ4SP.js → conflict-DRMDOL56.js} +2 -2
- package/dist/chunks/{contract-XIRAFTEV.js → contract-73MERZWN.js} +2 -2
- package/dist/chunks/{cowork-BLG62BL2.js → cowork-HNGBPD7A.js} +2 -2
- package/dist/chunks/{ebuilder-form-NUKG5QW4.js → ebuilder-form-OY6DVNRK.js} +3 -3
- package/dist/chunks/{ehr-2STW5ILH.js → ehr-RAQNEQO7.js} +2 -2
- package/dist/chunks/{esb-UUDQUNKY.js → esb-6XV7C7YV.js} +4 -4
- package/dist/chunks/{fna-6RCY4NU5.js → fna-XTSRB7G7.js} +2 -2
- package/dist/chunks/{host-RHM3NPXV.js → host-3F5UM5GN.js} +3 -3
- package/dist/chunks/{host-EJZFROU3.js → host-A7J6GDUP.js} +3 -3
- package/dist/chunks/{host-FWDOSRH5.js → host-VGPVP7YF.js} +3 -3
- package/dist/chunks/{host-7RXEDMAH.js → host-YAQAPYNP.js} +3 -3
- package/dist/chunks/{host-UFF33TWJ.js → host-YMBXWFWX.js} +3 -3
- package/dist/chunks/{hrm-PKJ6ELSH.js → hrm-TLHKC545.js} +3 -3
- package/dist/chunks/{im-YJAXS7CO.js → im-YOUCFMUK.js} +3 -3
- package/dist/chunks/{invoice-HWVSXGX6.js → invoice-WNBS2B4C.js} +3 -3
- package/dist/chunks/{jiuchuanhui-L7VKILZB.js → jiuchuanhui-XABWS5RN.js} +2 -2
- package/dist/chunks/{lazy-dispatch-NMYEIHRH.js → lazy-dispatch-NLZ5UQQI.js} +5 -5
- package/dist/chunks/{mail-B42RRU7G.js → mail-LRYMBFNL.js} +2 -2
- package/dist/chunks/{meeting-CFYVBJSS.js → meeting-ESXL6MFI.js} +10 -10
- package/dist/chunks/{okr-GCLF6V7W.js → okr-EOV65SSF.js} +2 -2
- package/dist/chunks/{plan-ZVL7GBRI.js → plan-YFPVKW6Y.js} +2 -2
- package/dist/chunks/{project-4YWAAE2K.js → project-MDVZUYQW.js} +2 -2
- package/dist/chunks/{registry-EP4VTHZS.js → registry-QCLZEDAP.js} +8 -8
- package/dist/chunks/{registry-FRJ6H7BT.js → registry-VT5Y2HLA.js} +4 -4
- package/dist/chunks/{registry-SG2PGVCF.js → registry-YX4CSEMD.js} +3 -3
- package/dist/chunks/{room-recommend-R3JVUUAC.js → room-recommend-L3MWH4JW.js} +4 -4
- package/dist/chunks/{schedule-NIJBTYDO.js → schedule-UASAVDVD.js} +4 -4
- package/dist/chunks/{workflow-BB24DFBY.js → workflow-KDGIUCNW.js} +9 -6
- package/dist/core/output.js +16 -6
- package/dist/index.js +33 -28
- package/dist/internal/e10/auth/messages.js +1 -0
- package/dist/internal/e10/auth-recovery.js +13 -6
- package/dist/internal/e10/context.js +16 -3
- package/dist/internal/e10/request-runtime.js +3 -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/index.js +5 -0
- package/dist/shortcuts/workflow/host.js +3 -0
- package/dist/shortcuts/workflow/index.js +1 -1
- package/dist/shortcuts/workflow/operations/form.js +46 -171
- package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupRules.js +77 -26
- package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupStore.js +6 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/summaryCache.js +59 -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 +20 -6
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.detail.js +22 -18
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +34 -10
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +4 -2
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +9 -2
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +110 -20
- package/docs/_catalog.md +3 -0
- package/docs/asset-idle.md +45 -0
- package/docs/e10-auth.md +1 -1
- package/docs/todo-smart-convert/subgroup-rules.md +1 -1
- package/package.json +1 -1
- 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-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-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 +5 -2
- package/skills/weaver-e10-workflow/aigroup/default.json +154 -19
- package/skills/weaver-e10-workflow/references/todo-smart-convert.md +59 -27
- package/skills/weaver-e10-workflow/tools/todo-serve.cjs +113 -25
- package/skills/weaver-e10-workflow/tools/todo-summary.cjs +292 -42
|
@@ -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
|
/**
|
|
@@ -657,12 +837,44 @@ function cmdPrep(args) {
|
|
|
657
837
|
fmt(collectMs), fmt(collectWallMs));
|
|
658
838
|
console.log('输出目录:%s\n', dir);
|
|
659
839
|
const focusKinds = focusCoversKinds(args.focus && args.focus !== true ? args.focus : '');
|
|
840
|
+
// 规则里 focus:false 的组**必须**由模型给 aiGroups(如 安全问题处理工单),prep 阶段就标出来,
|
|
841
|
+
// 不要等 build 报 ai_groups_required 才知道(2026-11-01 用户确认的规范缺陷)。
|
|
842
|
+
// 同时**回显该组的 groupingPrompt(组名词表/分法)与 requestSummaryPrompt(摘要模板)**——
|
|
843
|
+
// 这两样只存在于 aigroup/default.json,规则要求"生成 aiGroups 前必须先读它",
|
|
844
|
+
// 但 prep 原本不输出 ⇒ 执行方只能凭正文推断(自造组名/漏摘要要素的根因,2026-11-01 复盘定稿)。
|
|
845
|
+
const mustPlanRules = [];
|
|
846
|
+
try {
|
|
847
|
+
const ruleFile = JSON.parse(fs.readFileSync(path.join(path.resolve(__dirname, '..'), 'aigroup', 'default.json'), 'utf8'));
|
|
848
|
+
for (const rule of ruleFile.rules || []) {
|
|
849
|
+
if (rule.aiGroup && rule.aiGroup.groupRule && rule.aiGroup.groupRule.focus === false) {
|
|
850
|
+
const names = Array.isArray(rule.workflowName) ? rule.workflowName : [rule.workflowName];
|
|
851
|
+
for (const name of names) {
|
|
852
|
+
if (name && name !== '*') {
|
|
853
|
+
mustPlanRules.push({
|
|
854
|
+
name: String(name),
|
|
855
|
+
groupingPrompt: String((rule.aiGroup.groupRule || {}).groupingPrompt || ''),
|
|
856
|
+
requestSummaryPrompt: String(rule.requestSummaryPrompt || ''),
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
} catch { /* 规则文件读不到就不标注(build 仍会硬校验兜底) */ }
|
|
863
|
+
const matchRule = (title) => mustPlanRules.find((r) => title.includes(r.name) || r.name.includes(title));
|
|
864
|
+
const needsPlan = (title, count) => count >= 3 && (title.includes('内部留言') || Boolean(matchRule(title)));
|
|
660
865
|
for (const group of planGroups) {
|
|
661
866
|
// 只有「默认确定性类型 + 侧重点没覆盖本类型」才算无需分组;被覆盖时照常列出条目。
|
|
662
867
|
const kind = kindOfTitle(group.title);
|
|
663
868
|
const skip = group.groupId === 'priority' || group.count <= 1 || (kind !== '' && !focusKinds.has(kind));
|
|
664
|
-
|
|
869
|
+
const rule = skip ? null : matchRule(group.title);
|
|
870
|
+
const marker = skip ? ' [无需分组]' : (rule || group.title.includes('内部留言') ? ' [必须给 plan]' : '');
|
|
871
|
+
console.log(`=== ${group.groupId} | ${group.title} | ${group.count}${marker}`);
|
|
665
872
|
if (skip) continue;
|
|
873
|
+
// 回显规则内容:组名/分法只按 groupingPrompt 来(词表不得自造),摘要按 requestSummaryPrompt 写
|
|
874
|
+
if (rule) {
|
|
875
|
+
if (rule.groupingPrompt) console.log(` ▸ 分组口径(groupingPrompt,组名/分法只按它来,不得自造):${rule.groupingPrompt}`);
|
|
876
|
+
if (rule.requestSummaryPrompt) console.log(` ▸ 摘要模板(requestSummaryPrompt,本组单条摘要照它写):${rule.requestSummaryPrompt}`);
|
|
877
|
+
}
|
|
666
878
|
for (const row of group.rows) {
|
|
667
879
|
const hint = fieldHintById.get(row.requestId) || '';
|
|
668
880
|
// 提示放在**同一行**:两行格式容易让人(和模型)回头去匹配标题,而标题里往往没有类型词。
|
|
@@ -670,7 +882,7 @@ function cmdPrep(args) {
|
|
|
670
882
|
}
|
|
671
883
|
}
|
|
672
884
|
console.log('\n下一步:node %s build --dir %s', path.basename(__filename), dir);
|
|
673
|
-
console.log('
|
|
885
|
+
console.log('(标了 [必须给 plan] 的组——内部留言、以及规则 focus:false 的组如 安全问题处理工单——build 时必须带 --plan <aiGroups.json>;组名/分法/摘要口径见各组下回显的规则内容;其余走确定性分组)');
|
|
674
886
|
}
|
|
675
887
|
|
|
676
888
|
function cmdBuild(args) {
|
|
@@ -680,7 +892,7 @@ function cmdBuild(args) {
|
|
|
680
892
|
const kept = readJson(path.join(dir, 'items.json'));
|
|
681
893
|
const planGroups = state.planGroups || [];
|
|
682
894
|
const countById = new Map(planGroups.map((g) => [g.groupId, g.count]));
|
|
683
|
-
// `--plan
|
|
895
|
+
// `--plan`:确定性类型可不传;prep 标了 [必须给 plan] 的组(内部留言 / focus:false 组)不传会被 CLI 拒(ai_groups_required)。
|
|
684
896
|
// 不传时直接走 CLI 确定性分组(等价于 aiGroups 未传)。
|
|
685
897
|
let aiGroups;
|
|
686
898
|
if (planFile) {
|
|
@@ -742,17 +954,39 @@ function cmdBuild(args) {
|
|
|
742
954
|
fs.writeFileSync(path.join(dir, 'state.json'), JSON.stringify(state));
|
|
743
955
|
|
|
744
956
|
if (pending.length === 0) {
|
|
745
|
-
|
|
957
|
+
// 即使无待写摘要也必须走 finish:summary 阶段还负责满意度类(needSummary:false)的标题
|
|
958
|
+
// 填充与交付闸门——smartGroup 半成品直接导出会让这类条目空着(2026-11-01 闸门真机首跑抓到)。
|
|
959
|
+
cmdFinish({ ...args, dir });
|
|
746
960
|
return;
|
|
747
961
|
}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
console.log(`
|
|
753
|
-
|
|
962
|
+
console.log(`\n待写摘要(把结果写成 aiSummaries.json,元素 {requestId,summary};每批的「口径」必须遵守):`);
|
|
963
|
+
for (const b of batches) {
|
|
964
|
+
const items = b.items || [];
|
|
965
|
+
if (items.length === 0) continue;
|
|
966
|
+
console.log(`## 批次 ${b.batchId}|${b.groupTitle}${b.subGroupTitle && b.subGroupTitle !== b.groupTitle ? ` → ${b.subGroupTitle}` : ''}|目标字数 ${b.suggestedMaxChars}`);
|
|
967
|
+
if (b.prompt) console.log(`口径:${b.prompt}`);
|
|
968
|
+
if (b.hint) console.log(`提示:${b.hint}`);
|
|
969
|
+
for (const item of items) {
|
|
970
|
+
console.log(`### ${item.requestId}`);
|
|
971
|
+
console.log(`T: ${String(item.requestNameTitle || '').slice(0, 160)}`);
|
|
972
|
+
// 素材**几乎不截断**(4000 字符保险丝):2026-11-01 实测 240 字截断把 e-todo 模板的
|
|
973
|
+
// 「预估开发总人天/需求概述」等尾部要素裁掉,模型只能靠标题凑数(要素缺失复盘定稿)。
|
|
974
|
+
console.log(`C: ${String(item.context || '').replace(/\s+/g, ' ').slice(0, 4000)}`);
|
|
975
|
+
}
|
|
754
976
|
}
|
|
755
977
|
console.log('\n下一步:node %s finish --dir %s --summaries <aiSummaries.json>', path.basename(__filename), dir);
|
|
978
|
+
// 组摘要(aiGroup.groupSummary.needAI=true 的汇总项):口径随批次下发,模型按它写 aiGroupSummaries
|
|
979
|
+
const gsBatches = sg.data.groupSummaryBatches || [];
|
|
980
|
+
if (gsBatches.length > 0) {
|
|
981
|
+
console.log('\n待写组摘要(写成 aiGroupSummaries.json,元素 {groupId,summary}):');
|
|
982
|
+
for (const b of gsBatches) {
|
|
983
|
+
console.log(`### 组摘要 ${b.groupId}|${b.parentTitle} → ${b.title}|${b.count} 条|目标字数 ${b.suggestedMaxChars}`);
|
|
984
|
+
if (b.prompt) console.log(`口径:${b.prompt}`);
|
|
985
|
+
if (b.hint) console.log(`提示:${b.hint}`);
|
|
986
|
+
if (b.stats && b.stats.tail) console.log(`tail:${b.stats.tail}`);
|
|
987
|
+
}
|
|
988
|
+
console.log('\nfinish 时加:--group-summaries <aiGroupSummaries.json>');
|
|
989
|
+
}
|
|
756
990
|
}
|
|
757
991
|
|
|
758
992
|
function cmdFinish(args) {
|
|
@@ -788,6 +1022,9 @@ function cmdFinish(args) {
|
|
|
788
1022
|
fs.writeFileSync(path.join(dir, 'state.json'), JSON.stringify(state));
|
|
789
1023
|
console.log('summary 内部 %s / 含进程启动 %s(回填 %d 条,其余走缓存预填)',
|
|
790
1024
|
summaryInternal === null ? '-' : fmt(summaryInternal), fmt(summaryWallMs), aiSummaries.length);
|
|
1025
|
+
// 未回填的缺失 id 不在这里单列:summary 阶段的信封不带 summaryBatches/summaryStats,
|
|
1026
|
+
// 空 id 列表统一由 exportFinal 的闸门①从最终结构里就地提取(能直接给出 requestId)。
|
|
1027
|
+
// 未知 requestId 的 aiSummaries 由 CLI 侧硬校验拒绝(ai_summary_unknown_request,非零退出)。
|
|
791
1028
|
let finalResult = result;
|
|
792
1029
|
const groupBatches = result.data.groupSummaryBatches || [];
|
|
793
1030
|
if (groupBatches.length > 0) {
|
|
@@ -819,6 +1056,10 @@ function cmdFinish(args) {
|
|
|
819
1056
|
console.log('\n⚠ 还有 %d 个组摘要素材待模型生成(groupSummary 阶段):', groupBatches.length);
|
|
820
1057
|
for (const batch of groupBatches) console.log(JSON.stringify(batch).slice(0, 400));
|
|
821
1058
|
console.log(' → 写成 [{groupId,summary}] 后加 `--group-summaries <file>` 重跑本命令,可在同一次调用里回填');
|
|
1059
|
+
if (args['allow-empty-summary'] !== true) {
|
|
1060
|
+
console.error('✖ 交付拦截:%d 个组摘要未回填,导出会是空组摘要。补齐 --group-summaries 后重跑,或加 --allow-empty-summary 显式豁免。', groupBatches.length);
|
|
1061
|
+
process.exit(1);
|
|
1062
|
+
}
|
|
822
1063
|
}
|
|
823
1064
|
}
|
|
824
1065
|
// 2026-10-28 用户 OA 页面实测通过:默认走本地数据源(params.dataSource),
|
|
@@ -827,10 +1068,15 @@ function cmdFinish(args) {
|
|
|
827
1068
|
? false
|
|
828
1069
|
: (args.serve === undefined ? true : args.serve);
|
|
829
1070
|
exportFinal(finalResult, dir, state, kept.length, {
|
|
830
|
-
writeFile: args.file === true,
|
|
831
1071
|
timingReport: args['timing-report'],
|
|
832
1072
|
dataSource: args['data-source'],
|
|
833
1073
|
serve: serveOption,
|
|
1074
|
+
// 交付硬闸门(2026-11-01):空摘要 / 详情取数失败都要显式豁免才能出文件。
|
|
1075
|
+
// 详情取数发生在 smartGroup 阶段 ⇒ failed 从 smartgroup.json 的 data.summaryStats 取。
|
|
1076
|
+
allowEmptySummary: args['allow-empty-summary'] === true,
|
|
1077
|
+
allowDetailFail: args['allow-detail-fail'] === true,
|
|
1078
|
+
detailFailed: ((sg.data || {}).summaryStats || {}).failed || 0,
|
|
1079
|
+
groupEmpty: (finalResult.meta && finalResult.meta.groupEmpty) || 0,
|
|
834
1080
|
});
|
|
835
1081
|
console.log('总耗时(脚本口径):%s', fmt(Date.now() - Date.parse(state.startedAt)));
|
|
836
1082
|
}
|
|
@@ -849,21 +1095,24 @@ function cmdRun(args) {
|
|
|
849
1095
|
: `/tmp/todo-summary-${new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14)}`;
|
|
850
1096
|
cmdPrep({ ...args, out: dir });
|
|
851
1097
|
cmdBuild({ ...args, dir });
|
|
1098
|
+
// 无待写摘要时 cmdBuild 已经内部走完 cmdFinish(含满意度标题填充与交付闸门),这里直接收工
|
|
1099
|
+
if (fs.existsSync(path.join(dir, 'final.data.json'))) return;
|
|
852
1100
|
const sg = readJson(path.join(dir, 'smartgroup.json'));
|
|
853
1101
|
const pending = ((sg.data.summaryBatches || []).flatMap((batch) => batch.items || []));
|
|
854
1102
|
if (pending.length > 0) {
|
|
855
1103
|
console.log('\n⚠ 有 %d 条待写摘要:写成 aiSummaries.json 后跑 finish(**不用重跑** collect/smartGroup)', pending.length);
|
|
856
|
-
console.log(' node %s finish --dir %s --summaries <aiSummaries.json> --
|
|
1104
|
+
console.log(' node %s finish --dir %s --summaries <aiSummaries.json> --timing-report', path.basename(__filename), dir);
|
|
857
1105
|
return;
|
|
858
1106
|
}
|
|
859
|
-
//
|
|
1107
|
+
// build 停下但没导出(如组摘要待回填)⇒ 把 finish 跑完(它自己会按闸门决定放行/拦截)
|
|
860
1108
|
cmdFinish({
|
|
861
1109
|
dir,
|
|
862
|
-
file: args.file === true,
|
|
863
1110
|
'timing-report': args['timing-report'],
|
|
864
1111
|
'group-summaries': args['group-summaries'],
|
|
865
1112
|
serve: args.serve,
|
|
866
1113
|
'no-serve': args['no-serve'],
|
|
1114
|
+
'allow-empty-summary': args['allow-empty-summary'] === true,
|
|
1115
|
+
'allow-detail-fail': args['allow-detail-fail'] === true,
|
|
867
1116
|
});
|
|
868
1117
|
}
|
|
869
1118
|
|
|
@@ -875,10 +1124,11 @@ else if (cmd === 'finish') cmdFinish(args);
|
|
|
875
1124
|
else if (cmd === 'run') cmdRun(args);
|
|
876
1125
|
else {
|
|
877
1126
|
console.log('用法:node ' + path.basename(__filename) + ' <run|prep|build|finish> [options]');
|
|
878
|
-
console.log(' run --days
|
|
1127
|
+
console.log(' run [--days N | --from A --to B] [--limit 500] [--out DIR] [--timing-report] # 一次跑完 prep+build;用户没说时间段就都不传 = 不限时间取前 500 条');
|
|
879
1128
|
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] [--
|
|
1129
|
+
console.log(' build --dir DIR [--plan aiGroups.json] # prep 标注 [必须给 plan] 的组必须传;其余不传走确定性分组');
|
|
1130
|
+
console.log(' finish --dir DIR [--summaries aiSummaries.json] [--group-summaries aiGroupSummaries.json] [--timing-report]');
|
|
1131
|
+
console.log(' 交付闸门:空摘要 / 组摘要缺失 / 详情取数失败都会 exit 1 拒绝导出;显式豁免:--allow-empty-summary / --allow-detail-fail');
|
|
882
1132
|
console.log(' 默认:载荷只带 params.dataSource(现起本地服务,空闲 30 分钟自动退出);--no-serve 退回内联 data');
|
|
883
1133
|
console.log(' 提示:`--timing-report` 会在控制台打一段耗时摘要(阶段墙钟 / CLI 内部),**不产出 md**');
|
|
884
1134
|
process.exit(2);
|