weflow-cli 1.8.1 → 1.9.0
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/CHANGELOG.md +359 -0
- package/OPERATIONS.md +79 -0
- package/README.en.md +9 -2
- package/README.md +12 -2
- package/bin/weflow-cli-electron.cjs +2 -0
- package/bin/weflow-cli.ts +648 -79
- package/dist/bin/weflow-cli.js +675 -78
- package/dist/bin/weflow-cli.js.map +1 -1
- package/dist/mcp-server/http.d.ts +14 -0
- package/dist/mcp-server/http.d.ts.map +1 -0
- package/dist/mcp-server/http.js +84 -0
- package/dist/mcp-server/http.js.map +1 -0
- package/dist/mcp-server/httpConfig.d.ts +50 -0
- package/dist/mcp-server/httpConfig.d.ts.map +1 -0
- package/dist/mcp-server/httpConfig.js +104 -0
- package/dist/mcp-server/httpConfig.js.map +1 -0
- package/dist/mcp-server/index.d.ts +13 -1
- package/dist/mcp-server/index.d.ts.map +1 -1
- package/dist/mcp-server/index.js +39 -373
- package/dist/mcp-server/index.js.map +1 -1
- package/dist/src/panel/server.d.ts.map +1 -1
- package/dist/src/panel/server.js +26 -1
- package/dist/src/panel/server.js.map +1 -1
- package/dist/src/services/assistantEval.d.ts +69 -0
- package/dist/src/services/assistantEval.d.ts.map +1 -1
- package/dist/src/services/assistantEval.js +270 -8
- package/dist/src/services/assistantEval.js.map +1 -1
- package/dist/src/services/assistantScenes.d.ts +81 -0
- package/dist/src/services/assistantScenes.d.ts.map +1 -0
- package/dist/src/services/assistantScenes.js +322 -0
- package/dist/src/services/assistantScenes.js.map +1 -0
- package/dist/src/services/assistantService.d.ts +14 -4
- package/dist/src/services/assistantService.d.ts.map +1 -1
- package/dist/src/services/assistantService.js +82 -6
- package/dist/src/services/assistantService.js.map +1 -1
- package/dist/src/services/assistantSkills.d.ts +94 -0
- package/dist/src/services/assistantSkills.d.ts.map +1 -0
- package/dist/src/services/assistantSkills.js +330 -0
- package/dist/src/services/assistantSkills.js.map +1 -0
- package/dist/src/services/assistantTools.d.ts +7 -0
- package/dist/src/services/assistantTools.d.ts.map +1 -1
- package/dist/src/services/assistantTools.js +770 -61
- package/dist/src/services/assistantTools.js.map +1 -1
- package/dist/src/services/assistantTrace.d.ts +4 -0
- package/dist/src/services/assistantTrace.d.ts.map +1 -1
- package/dist/src/services/assistantTrace.js +9 -0
- package/dist/src/services/assistantTrace.js.map +1 -1
- package/dist/src/services/configService.d.ts +7 -0
- package/dist/src/services/configService.d.ts.map +1 -1
- package/dist/src/services/configService.js +4 -2
- package/dist/src/services/configService.js.map +1 -1
- package/dist/src/services/outboundSend.d.ts +53 -0
- package/dist/src/services/outboundSend.d.ts.map +1 -0
- package/dist/src/services/outboundSend.js +90 -0
- package/dist/src/services/outboundSend.js.map +1 -0
- package/dist/src/services/pythonBridge.d.ts.map +1 -1
- package/dist/src/services/pythonBridge.js +1 -0
- package/dist/src/services/pythonBridge.js.map +1 -1
- package/dist/src/utils/python.js +2 -2
- package/dist/src/utils/python.js.map +1 -1
- package/docs/BRANCHES.md +37 -0
- package/docs/DECISIONS.md +350 -0
- package/docs/EXTENDING.md +29 -3
- package/docs/MCP.md +56 -2
- package/docs/PROJECT_STATE.md +121 -9
- package/mcp-server/http.ts +108 -0
- package/mcp-server/httpConfig.ts +126 -0
- package/mcp-server/index.ts +344 -681
- package/package.json +1 -1
- package/resources/panel/ball-position.cjs +48 -2
- package/resources/panel/index.html +6 -4
- package/resources/panel/main.cjs +137 -5
- package/resources/panel/mascot-base.png +0 -0
- package/resources/panel/mascot-iris.png +0 -0
- package/resources/panel/mascot-peek.png +0 -0
- package/resources/panel/panel.css +107 -63
- package/resources/panel/preload.cjs +26 -0
- package/resources/panel/renderer.js +167 -10
- package/scripts/_utils.py +47 -1
- package/scripts/backfill_articles.py +202 -21
- package/scripts/biz_daily.py +8 -31
- package/scripts/bonds.py +331 -0
- package/scripts/chat_notes.py +551 -470
- package/scripts/chat_stats.py +8 -7
- package/scripts/compile_wiki.py +24 -2
- package/scripts/export_chat_html.py +3 -1
- package/scripts/mcp_bridge.py +6 -7
- package/scripts/wechat_voice.py +9 -0
- package/scripts/wiki_lint.py +30 -7
- package/src/panel/server.ts +25 -1
- package/src/services/assistantEval.ts +323 -12
- package/src/services/assistantScenes.ts +342 -0
- package/src/services/assistantService.ts +81 -6
- package/src/services/assistantSkills.ts +364 -0
- package/src/services/assistantTools.ts +738 -59
- package/src/services/assistantTrace.ts +9 -0
- package/src/services/configService.ts +412 -403
- package/src/services/outboundSend.ts +126 -0
- package/src/services/pythonBridge.ts +1 -0
- package/src/utils/python.ts +2 -2
package/dist/bin/weflow-cli.js
CHANGED
|
@@ -21,7 +21,10 @@ import { DateRangeError, parseLocalDateOrIso, resolveExportDateRange } from '../
|
|
|
21
21
|
import { resolvePackageRoot as resolvePackageRootFrom } from '../src/utils/packageRoot.js';
|
|
22
22
|
import { WechatMessageService } from '../src/services/wechatMessageService.js';
|
|
23
23
|
import { whitelistService, MAX_TEXT_LENGTH } from '../src/services/whitelistService.js';
|
|
24
|
+
import { authorizeOutbound, parseOutboundPolicy, performOutbound } from '../src/services/outboundSend.js';
|
|
24
25
|
import { applyDerivedNtKeys, enableFavorites, detectFavDbPath } from '../src/services/initKeyService.js';
|
|
26
|
+
import { disabledIds, readSkillBody, scanSkills, skillRoots, skillState } from '../src/services/assistantSkills.js';
|
|
27
|
+
import { assistantScenes, validateSceneId } from '../src/services/assistantScenes.js';
|
|
25
28
|
const program = new Command();
|
|
26
29
|
function resolvePackageRoot() {
|
|
27
30
|
return resolvePackageRootFrom(import.meta.url);
|
|
@@ -264,7 +267,7 @@ program
|
|
|
264
267
|
read: {
|
|
265
268
|
sessions: { cli: 'sessions --json', mcp: 'wechat.list_sessions' },
|
|
266
269
|
messages: { cli: 'messages <talker> --json', mcp: 'wechat.export_messages' },
|
|
267
|
-
contacts: { cli: 'contacts --json' },
|
|
270
|
+
contacts: { cli: 'contacts --json', mcp: 'wechat.list_contacts', localIdentifiersInModelContext: false },
|
|
268
271
|
exports: {
|
|
269
272
|
cli: 'export <talker> <json|txt|html|excel>',
|
|
270
273
|
mcp: 'wechat.export_messages',
|
|
@@ -298,8 +301,36 @@ program
|
|
|
298
301
|
},
|
|
299
302
|
dailyReader: { cli: 'daily-server --status --json' },
|
|
300
303
|
dailyStats: { cli: 'daily-stats --json' },
|
|
304
|
+
// 只读、纯本地、不调模型。**故意没有 mcp 那一栏**(D-062):它给的是关于第三方的推断
|
|
305
|
+
// ——谁在被冷落、你会失去谁 —— 所以不进助手、不进 MCP、不喂模型。
|
|
306
|
+
bonds: { cli: 'bonds --json', mcp: null, leavesTheMachine: false },
|
|
301
307
|
diagnostics: { cli: 'check --json' },
|
|
302
308
|
todos: { cli: 'todos list --json' },
|
|
309
|
+
// 2026-09-29 三个界面对齐时新开的读取能力(D-058)。逐个写清它会不会出境,
|
|
310
|
+
// 因为其中两个会——`fetch_article` 白名单 + 逐跳复检,`search_public` 没有任何守卫。
|
|
311
|
+
review: { cli: 'review ...', mcp: 'wechat.get_review', writesReport: true },
|
|
312
|
+
concepts: { cli: 'wiki ...', mcp: 'wechat.get_concepts' },
|
|
313
|
+
articleFetch: {
|
|
314
|
+
cli: null, // 没有对应的 CLI 命令:它本来就是为模型准备的读取能力
|
|
315
|
+
mcp: 'wechat.fetch_article',
|
|
316
|
+
network: true,
|
|
317
|
+
allowlist: 'https only, host must be exactly mp.weixin.qq.com',
|
|
318
|
+
redirectsReValidatedPerHop: true,
|
|
319
|
+
confirmationRequired: false,
|
|
320
|
+
},
|
|
321
|
+
publicArticleSearch: {
|
|
322
|
+
mcp: 'wechat.search_public',
|
|
323
|
+
network: true,
|
|
324
|
+
// **这条是如实的**:搜索词由模型决定、发到第三方搜索页,没有白名单也没有预览。
|
|
325
|
+
// 它在该项目的边界内是被明确接受的残余风险(D-058),这里不能写成"无出境"。
|
|
326
|
+
allowlist: null,
|
|
327
|
+
modelAuthoredQueryLeavesTheMachine: true,
|
|
328
|
+
confirmationRequired: false,
|
|
329
|
+
},
|
|
330
|
+
articleFormatting: { mcp: 'wechat.format_article', cli: null, writesNothing: true, network: false },
|
|
331
|
+
themes: { mcp: 'wechat.list_themes', writesNothing: true },
|
|
332
|
+
wikiLint: { cli: 'wiki lint --json', mcp: 'wechat.lint_wiki', readsLocalChat: false, writesNothing: true },
|
|
333
|
+
skillCheck: { cli: 'skill check --json', mcp: 'wechat.check_skills', writesNothing: true },
|
|
303
334
|
awaiting: {
|
|
304
335
|
cli: 'awaiting --yes',
|
|
305
336
|
preview: 'awaiting --dry-run --json',
|
|
@@ -382,6 +413,33 @@ program
|
|
|
382
413
|
execute: 'todos <done|undone|rm> <id> --yes --json',
|
|
383
414
|
confirmationRequired: true,
|
|
384
415
|
},
|
|
416
|
+
skills: {
|
|
417
|
+
list: 'skill list [--query <q>] --json',
|
|
418
|
+
show: 'skill show <id> --json',
|
|
419
|
+
check: 'skill check --json',
|
|
420
|
+
readOnly: true,
|
|
421
|
+
// 技能是**资料**,不是插件:助手只把它当提示词/正文读,不执行里面的脚本,
|
|
422
|
+
// 也不因为装了技能就多出任何工具(P4 验收标准:扩展读不到配置与数据库)。
|
|
423
|
+
installsAnything: false,
|
|
424
|
+
executesSkillCode: false,
|
|
425
|
+
grantsTools: false,
|
|
426
|
+
directories: 'skillDirs config (default: ~/.claude/skills, ~/.weflow-cli/skills)',
|
|
427
|
+
disable: 'config set skillDisabled <id,...>(技能自己也能写 enabled: false)',
|
|
428
|
+
},
|
|
429
|
+
scenes: {
|
|
430
|
+
list: 'scene list --json',
|
|
431
|
+
show: 'scene show <id> --json',
|
|
432
|
+
preview: 'scene <add|remove|enable|disable|bind|unbind> … --dry-run --json',
|
|
433
|
+
execute: 'scene <add|remove|enable|disable|bind|unbind> … --yes --json',
|
|
434
|
+
confirmationRequired: true,
|
|
435
|
+
// 场景只改**本机提示词预设**,不发消息、不读聊天、不调模型
|
|
436
|
+
readsLocalChat: false,
|
|
437
|
+
invokesAI: false,
|
|
438
|
+
sendsNothing: true,
|
|
439
|
+
stateLocation: 'local-user-config-dir',
|
|
440
|
+
stateSchema: 'weflow-scenes/v1',
|
|
441
|
+
modelWritable: false,
|
|
442
|
+
},
|
|
385
443
|
accessControlMutations: {
|
|
386
444
|
preview: '<whitelist|blacklist> <add|rm> <target> --dry-run --json',
|
|
387
445
|
execute: '<whitelist|blacklist> <add|rm> <target> --yes --json',
|
|
@@ -978,6 +1036,7 @@ const configurableKeys = [
|
|
|
978
1036
|
'wereadApiKey',
|
|
979
1037
|
'assistantPrivacy', 'assistantWhitelist', 'assistantGroupWhitelist',
|
|
980
1038
|
'assistantGroupRequireMention', 'assistantFastRoute', 'assistantPanelUser', 'quickReplyContacts',
|
|
1039
|
+
'skillDirs', 'skillDisabled',
|
|
981
1040
|
'dailySources', 'dailySourceCategories',
|
|
982
1041
|
'dailyExcludeTopics', 'dailyAiEnabled',
|
|
983
1042
|
'emoticonSeed',
|
|
@@ -3379,57 +3438,50 @@ program
|
|
|
3379
3438
|
});
|
|
3380
3439
|
process.exit(1);
|
|
3381
3440
|
}
|
|
3382
|
-
//
|
|
3383
|
-
|
|
3441
|
+
// 三道闸(黑名单 → 白名单 → 限速)与"每次尝试都写审计"在 `src/services/outboundSend.ts`
|
|
3442
|
+
// 里**只有一份**,因为 2026-10-01 起有了第二条发送路(`draft --pick N --send`,见 D-063)。
|
|
3443
|
+
// 两份写同一件事,分叉的后果具体是"某一条路绕过了白名单"。
|
|
3444
|
+
const policy = parseOutboundPolicy(Number(opts.rateWindow), Number(opts.rateMax));
|
|
3445
|
+
if ('code' in policy) {
|
|
3384
3446
|
if (opts.json)
|
|
3385
|
-
console.log(JSON.stringify({ success: false, code:
|
|
3386
|
-
else
|
|
3447
|
+
console.log(JSON.stringify({ success: false, code: policy.code, error: policy.error }));
|
|
3448
|
+
else
|
|
3449
|
+
console.log(chalk.red('速率参数无效:rate-window 必须为 1000-3600000,rate-max 必须为 1-100'));
|
|
3450
|
+
process.exit(1);
|
|
3451
|
+
}
|
|
3452
|
+
const attempt = {
|
|
3453
|
+
wxid, displayName, kind, preview, text: message,
|
|
3454
|
+
mediaPath: kind === 'text' ? undefined : String(opts.image || opts.file),
|
|
3455
|
+
};
|
|
3456
|
+
const decision = authorizeOutbound(attempt, policy);
|
|
3457
|
+
if (!decision.ok) {
|
|
3458
|
+
const { refusal } = decision;
|
|
3459
|
+
if (opts.json) {
|
|
3460
|
+
console.log(JSON.stringify({
|
|
3461
|
+
success: false, code: refusal.code, error: refusal.error,
|
|
3462
|
+
...(refusal.code === 'TARGET_BLOCKED' || refusal.code === 'TARGET_NOT_ALLOWED'
|
|
3463
|
+
? { target: { wxid, displayName } } : {}),
|
|
3464
|
+
...(refusal.rateLimit ? { rateLimit: refusal.rateLimit } : {}),
|
|
3465
|
+
}));
|
|
3466
|
+
}
|
|
3467
|
+
else if (refusal.code === 'TARGET_BLOCKED') {
|
|
3387
3468
|
console.log(chalk.red('\n❌ 目标在黑名单中,绝对禁止发送'));
|
|
3388
3469
|
console.log(chalk.gray(` wxid: ${wxid}`));
|
|
3389
3470
|
console.log(chalk.gray(` 解除: weflow-cli blacklist rm ${wxid}\n`));
|
|
3390
3471
|
}
|
|
3391
|
-
|
|
3392
|
-
timestamp: Date.now(), action: 'send', targetWxid: wxid, targetName: displayName,
|
|
3393
|
-
kind, success: false, preview, error: 'blocked by blacklist',
|
|
3394
|
-
});
|
|
3395
|
-
process.exit(1);
|
|
3396
|
-
}
|
|
3397
|
-
// Whitelist check
|
|
3398
|
-
if (!whitelistService.isAllowed(wxid)) {
|
|
3399
|
-
if (opts.json)
|
|
3400
|
-
console.log(JSON.stringify({ success: false, code: 'TARGET_NOT_ALLOWED', error: '目标不在白名单中', target: { wxid, displayName } }));
|
|
3401
|
-
else {
|
|
3472
|
+
else if (refusal.code === 'TARGET_NOT_ALLOWED') {
|
|
3402
3473
|
console.log(chalk.red('\n❌ 目标不在白名单中,拒绝发送'));
|
|
3403
3474
|
console.log(chalk.gray(` 先运行: weflow-cli whitelist add ${target}`));
|
|
3404
3475
|
console.log(chalk.gray(` 查看名单: weflow-cli whitelist\n`));
|
|
3405
3476
|
}
|
|
3406
|
-
process.exit(1);
|
|
3407
|
-
}
|
|
3408
|
-
// 速率限制 (dry-run 不计入, 不检查也行, 但保持一致检查)
|
|
3409
|
-
const windowMs = Number(opts.rateWindow);
|
|
3410
|
-
const max = Number(opts.rateMax);
|
|
3411
|
-
if (!Number.isInteger(windowMs) || windowMs < 1000 || windowMs > 3_600_000 ||
|
|
3412
|
-
!Number.isInteger(max) || max < 1 || max > 100) {
|
|
3413
|
-
if (opts.json)
|
|
3414
|
-
console.log(JSON.stringify({ success: false, code: 'INVALID_RATE_LIMIT', error: 'rate-window 必须为 1000-3600000,rate-max 必须为 1-100' }));
|
|
3415
|
-
else
|
|
3416
|
-
console.log(chalk.red('速率参数无效:rate-window 必须为 1000-3600000,rate-max 必须为 1-100'));
|
|
3417
|
-
process.exit(1);
|
|
3418
|
-
}
|
|
3419
|
-
const rate = whitelistService.checkRateLimit(windowMs, max);
|
|
3420
|
-
if (!rate.allowed) {
|
|
3421
|
-
if (opts.json)
|
|
3422
|
-
console.log(JSON.stringify({ success: false, code: 'RATE_LIMITED', error: '触发发送速率限制', rateLimit: rate }));
|
|
3423
3477
|
else {
|
|
3478
|
+
const rate = refusal.rateLimit;
|
|
3424
3479
|
console.log(chalk.red(`\n❌ 触发速率限制: 最近 ${rate.windowMs / 1000}s 内已发送 ${rate.count} 条 (上限 ${rate.max})`));
|
|
3425
3480
|
console.log(chalk.gray(' 请稍后再试, 或调整 --rate-window / --rate-max\n'));
|
|
3426
3481
|
}
|
|
3427
|
-
whitelistService.auditSend({
|
|
3428
|
-
timestamp: Date.now(), action: 'send', targetWxid: wxid, targetName: displayName,
|
|
3429
|
-
kind, success: false, preview, error: `rate limited (${rate.count}/${rate.max})`,
|
|
3430
|
-
});
|
|
3431
3482
|
process.exit(1);
|
|
3432
3483
|
}
|
|
3484
|
+
const rate = decision.rate;
|
|
3433
3485
|
// 二次确认 — 同时显示 wxid + displayName + 消息预览
|
|
3434
3486
|
const actionPreview = {
|
|
3435
3487
|
action: 'send',
|
|
@@ -3480,41 +3532,8 @@ program
|
|
|
3480
3532
|
}
|
|
3481
3533
|
process.exit(1);
|
|
3482
3534
|
}
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
let errorMsg;
|
|
3486
|
-
try {
|
|
3487
|
-
if (opts.image) {
|
|
3488
|
-
success = await service.sendImage(wxid, opts.image);
|
|
3489
|
-
}
|
|
3490
|
-
else if (opts.file) {
|
|
3491
|
-
success = await service.sendFile(wxid, opts.file);
|
|
3492
|
-
}
|
|
3493
|
-
else {
|
|
3494
|
-
success = await service.sendText(wxid, message);
|
|
3495
|
-
}
|
|
3496
|
-
}
|
|
3497
|
-
catch {
|
|
3498
|
-
errorMsg = '消息通道调用失败';
|
|
3499
|
-
}
|
|
3500
|
-
if (!success && !errorMsg) {
|
|
3501
|
-
// 检查是否因缺少 context_token
|
|
3502
|
-
const hasToken = !!(configService.getContextTokens()[wxid]);
|
|
3503
|
-
errorMsg = hasToken
|
|
3504
|
-
? '发送失败 (接口返回错误, 可能 token 过期)'
|
|
3505
|
-
: '缺少 context_token — 需先收到对方一条消息 (或运行 weflow-cli listen 等待对方消息)';
|
|
3506
|
-
}
|
|
3507
|
-
// 审计日志
|
|
3508
|
-
whitelistService.auditSend({
|
|
3509
|
-
timestamp: Date.now(),
|
|
3510
|
-
action: 'send',
|
|
3511
|
-
targetWxid: wxid,
|
|
3512
|
-
targetName: displayName,
|
|
3513
|
-
kind,
|
|
3514
|
-
success,
|
|
3515
|
-
preview,
|
|
3516
|
-
error: success ? undefined : errorMsg,
|
|
3517
|
-
});
|
|
3535
|
+
// 真的发 + 按结果写审计,都在共享模块里(与 `draft --send` 同一份)
|
|
3536
|
+
const { success, error: errorMsg } = await performOutbound(attempt);
|
|
3518
3537
|
if (success) {
|
|
3519
3538
|
if (opts.json)
|
|
3520
3539
|
console.log(JSON.stringify({ success: true, ...actionPreview }));
|
|
@@ -4694,6 +4713,7 @@ program
|
|
|
4694
4713
|
await openLocalUrl(`http://localhost:${port}`);
|
|
4695
4714
|
}
|
|
4696
4715
|
const child = spawn(getPythonCommand(), args, {
|
|
4716
|
+
windowsHide: true,
|
|
4697
4717
|
stdio: 'inherit',
|
|
4698
4718
|
env: pythonProcessEnv(),
|
|
4699
4719
|
});
|
|
@@ -5241,6 +5261,7 @@ program
|
|
|
5241
5261
|
// 交互模式:使用 spawn 保持终端交互
|
|
5242
5262
|
args.push('--interactive', '--top-k', String(topK));
|
|
5243
5263
|
const child = spawn(getPythonCommand(), args, {
|
|
5264
|
+
windowsHide: true,
|
|
5244
5265
|
stdio: 'inherit',
|
|
5245
5266
|
env: {
|
|
5246
5267
|
...pythonProcessEnv(opts.apiKey),
|
|
@@ -5514,6 +5535,10 @@ program
|
|
|
5514
5535
|
.command('draft <talker>')
|
|
5515
5536
|
.description('帮我起草回复:先判断(意图/风险/该不该给实质),再起草候选 —— **只产出文本,不发送**')
|
|
5516
5537
|
.option('--count <n>', '要几条候选', '3')
|
|
5538
|
+
.option('--pick <n>', '选第几条(从 1 数);不带 --send 时只把那条的完整原文打出来')
|
|
5539
|
+
.option('--send', '把选中的那条发出去 —— **必须同时给 --pick**(不允许"自己挑一条发"),仍需 --yes/--dry-run')
|
|
5540
|
+
.option('--rate-window <ms>', '发送速率窗口毫秒(与 send 同一份策略)', '60000')
|
|
5541
|
+
.option('--rate-max <n>', '窗口内最大发送条数', '10')
|
|
5517
5542
|
.option('--gate-commitment', '承诺未兑现时也拒绝起草(默认只在提示词里约束)')
|
|
5518
5543
|
.option('--dry-run', '仅预览会发多少字符给哪两个模型,不调用')
|
|
5519
5544
|
.option('--yes', '确认把这段对话发送到判断模型与生成模型')
|
|
@@ -5522,9 +5547,22 @@ program
|
|
|
5522
5547
|
const { execFile } = await import('child_process');
|
|
5523
5548
|
const { promisify } = await import('util');
|
|
5524
5549
|
const execFileAsync = promisify(execFile);
|
|
5550
|
+
// **用法错误先报**,而且都在调模型之前报:`--send` 少了 `--pick` 是"你没说发哪条",
|
|
5551
|
+
// `--pick 9` 而只有 3 条是"那条根本不存在" —— 两者都不该先问一遍"要不要把对话发给云端模型"、
|
|
5552
|
+
// 更不该先花掉模型调用再拒绝。
|
|
5553
|
+
const count = parseCliInteger(opts.count, 'count', 1, 5, opts.json);
|
|
5554
|
+
if (opts.send && opts.pick === undefined) {
|
|
5555
|
+
const message = '--send 必须同时给 --pick <序号>:先看候选,再点名要发哪一条';
|
|
5556
|
+
if (opts.json)
|
|
5557
|
+
console.log(JSON.stringify({ success: false, code: 'PICK_REQUIRED', error: message }));
|
|
5558
|
+
else
|
|
5559
|
+
console.log(chalk.red(`
|
|
5560
|
+
❌ ${message}`));
|
|
5561
|
+
process.exit(1);
|
|
5562
|
+
}
|
|
5563
|
+
const pick = opts.pick === undefined ? 0 : parseCliInteger(opts.pick, 'pick', 1, count, opts.json);
|
|
5525
5564
|
const pkgRoot = resolvePackageRoot();
|
|
5526
5565
|
const script = join(pkgRoot, 'scripts', 'draft_reply.py');
|
|
5527
|
-
const count = parseCliInteger(opts.count, 'count', 1, 5, opts.json);
|
|
5528
5566
|
// **"确认过了"只用一个变量表示**(`--yes` 或交互回答"是"),参数在确认之后才拼。
|
|
5529
5567
|
// 原来的写法是先拼参数、再问,于是**交互确认那条路是坏的**:答了"是",`args` 里
|
|
5530
5568
|
// 却从没补上 `--yes`,脚本照样拒绝(用户看到的是"我确认了它却说没确认")。
|
|
@@ -5552,6 +5590,170 @@ program
|
|
|
5552
5590
|
return;
|
|
5553
5591
|
}
|
|
5554
5592
|
}
|
|
5593
|
+
// ---------------- 选中并(可选)发送 ----------------
|
|
5594
|
+
//
|
|
5595
|
+
// **只有给了 `--pick` 或 `--send` 才走这条分支**;不带它们的调用与以前逐字相同(原样透传脚本输出)。
|
|
5596
|
+
//
|
|
5597
|
+
// 这条分支存在的理由:起草那一半早就有了(判断 → 候选 → 排序),缺的是"人选定之后能真的发出去"。
|
|
5598
|
+
// 而它的**要点是这个 `--pick`**:发送必须由人点名"第几条"。少了它,就成了"模型起草三条、
|
|
5599
|
+
// 模型挑一条、模型发出去" —— 那正是本仓库划掉的那件事
|
|
5600
|
+
// (docs/EXTENDING.md:sending is structurally unreachable from a model-driven path)。
|
|
5601
|
+
if (opts.pick !== undefined || opts.send) {
|
|
5602
|
+
// **`--dry-run` 的语义是"零出境"**,而候选必须由模型产出来 —— 两者不可兼得:不起草就没有
|
|
5603
|
+
// 候选,"预览某一条会发什么"这件事本身不成立。所以这里只报"会发多少字符给哪两个模型"
|
|
5604
|
+
// (脚本的 dry-run 本来就报这个),并把话说清楚,而不是假装预览了一条。
|
|
5605
|
+
if (opts.dryRun) {
|
|
5606
|
+
try {
|
|
5607
|
+
const { stdout } = await execFileAsync(getPythonCommand(), [script, '--talker', String(talker), '--count', String(count), '--dry-run',
|
|
5608
|
+
...(opts.json ? ['--json'] : [])], { timeout: 120_000, maxBuffer: 20 * 1024 * 1024, env: pythonProcessEnv() });
|
|
5609
|
+
process.stdout.write(stdout);
|
|
5610
|
+
}
|
|
5611
|
+
catch (error) {
|
|
5612
|
+
console.error(chalk.red(`\n✗ ${safeSubprocessError(error, '预览失败')}`));
|
|
5613
|
+
process.exit(1);
|
|
5614
|
+
}
|
|
5615
|
+
if (!opts.json) {
|
|
5616
|
+
console.log(chalk.gray('\n候选要由模型产出来,所以 --dry-run 下没有候选可挑。'
|
|
5617
|
+
+ '要预览"发第几条":先起草一次(会调用模型),再用 --pick N --send。\n'));
|
|
5618
|
+
}
|
|
5619
|
+
return;
|
|
5620
|
+
}
|
|
5621
|
+
// `--pick`/`--send` 的用法错误在 action 开头就报过了(那一段全在调模型之前)
|
|
5622
|
+
// 内部一律用 `--json` 拿结构化结果(用户那侧的 `--json` 管的是最终输出,两回事)
|
|
5623
|
+
const pickArgs = [script, '--talker', String(talker), '--count', String(count), '--json',
|
|
5624
|
+
...(confirmed ? ['--yes'] : []),
|
|
5625
|
+
...(opts.gateCommitment ? ['--gate-commitment'] : [])];
|
|
5626
|
+
let drafts = [];
|
|
5627
|
+
try {
|
|
5628
|
+
const { stdout } = await execFileAsync(getPythonCommand(), pickArgs, {
|
|
5629
|
+
timeout: 300_000, maxBuffer: 50 * 1024 * 1024, env: pythonProcessEnv(),
|
|
5630
|
+
});
|
|
5631
|
+
const parsed = JSON.parse(stdout);
|
|
5632
|
+
if (parsed.gate === 'refused') {
|
|
5633
|
+
// **不起草就不发**:那道闸(涉钱/风险高)拒绝时压根没有候选,这里也不能凭空发点什么
|
|
5634
|
+
const message = `这次不起草:${parsed.reason || '判断层拒绝了'}`;
|
|
5635
|
+
if (opts.json)
|
|
5636
|
+
console.log(JSON.stringify({ success: false, code: 'DRAFT_REFUSED', error: message, advice: parsed.advice || [] }));
|
|
5637
|
+
else {
|
|
5638
|
+
console.log(chalk.yellow(`\n${message}`));
|
|
5639
|
+
for (const item of parsed.advice || [])
|
|
5640
|
+
console.log(chalk.gray(` · ${item}`));
|
|
5641
|
+
}
|
|
5642
|
+
process.exit(1);
|
|
5643
|
+
}
|
|
5644
|
+
drafts = parsed.drafts || [];
|
|
5645
|
+
}
|
|
5646
|
+
catch (error) {
|
|
5647
|
+
const message = safeSubprocessError(error, '起草失败');
|
|
5648
|
+
if (opts.json)
|
|
5649
|
+
console.log(JSON.stringify({ success: false, code: 'DRAFT_FAILED', error: message }));
|
|
5650
|
+
else
|
|
5651
|
+
console.error(chalk.red(`\n✗ ${message}`));
|
|
5652
|
+
process.exit(1);
|
|
5653
|
+
}
|
|
5654
|
+
const chosen = drafts[pick - 1];
|
|
5655
|
+
if (!chosen) {
|
|
5656
|
+
const message = `没有第 ${pick} 条候选(这次只有 ${drafts.length} 条)`;
|
|
5657
|
+
if (opts.json)
|
|
5658
|
+
console.log(JSON.stringify({ success: false, code: 'PICK_OUT_OF_RANGE', error: message }));
|
|
5659
|
+
else
|
|
5660
|
+
console.log(chalk.red(`\n❌ ${message}`));
|
|
5661
|
+
process.exit(1);
|
|
5662
|
+
}
|
|
5663
|
+
if (!opts.send) {
|
|
5664
|
+
if (opts.json)
|
|
5665
|
+
console.log(JSON.stringify({ success: true, action: 'draft', sent: false, pick, text: chosen.text }));
|
|
5666
|
+
else
|
|
5667
|
+
console.log(`第 ${pick} 条:\n${chosen.text}\n\n${chalk.gray('(没有发送任何东西;要发加 --send)')}`);
|
|
5668
|
+
return;
|
|
5669
|
+
}
|
|
5670
|
+
// 发送这一半走**与 `send` 命令完全相同的那份策略**(黑名单 → 白名单 → 限速 → 审计)
|
|
5671
|
+
let wxid;
|
|
5672
|
+
try {
|
|
5673
|
+
wxid = await resolveTalker(String(talker), !!opts.json, !!opts.json);
|
|
5674
|
+
}
|
|
5675
|
+
catch (error) {
|
|
5676
|
+
if (opts.json)
|
|
5677
|
+
console.log(JSON.stringify({ success: false, code: 'TALKER_RESOLUTION_FAILED', error: error.message }));
|
|
5678
|
+
else
|
|
5679
|
+
console.log(chalk.red(`\n❌ ${error.message}`));
|
|
5680
|
+
process.exit(1);
|
|
5681
|
+
}
|
|
5682
|
+
let displayName = whitelistService.lookupName(wxid);
|
|
5683
|
+
if (displayName === wxid) {
|
|
5684
|
+
try {
|
|
5685
|
+
const sessions = await chatService.listSessions(undefined, 200);
|
|
5686
|
+
const match = sessions.find(s => s.username === wxid);
|
|
5687
|
+
if (match?.displayName)
|
|
5688
|
+
displayName = match.displayName;
|
|
5689
|
+
}
|
|
5690
|
+
catch { /* 拿不到名字就退回 wxid */ }
|
|
5691
|
+
}
|
|
5692
|
+
const policy = parseOutboundPolicy(Number(opts.rateWindow), Number(opts.rateMax));
|
|
5693
|
+
if ('code' in policy) {
|
|
5694
|
+
if (opts.json)
|
|
5695
|
+
console.log(JSON.stringify({ success: false, code: policy.code, error: policy.error }));
|
|
5696
|
+
else
|
|
5697
|
+
console.log(chalk.red(`\n❌ ${policy.error}`));
|
|
5698
|
+
process.exit(1);
|
|
5699
|
+
}
|
|
5700
|
+
const attempt = {
|
|
5701
|
+
wxid, displayName, kind: 'text',
|
|
5702
|
+
preview: chosen.text.slice(0, 80) + (chosen.text.length > 80 ? '...' : ''),
|
|
5703
|
+
text: chosen.text,
|
|
5704
|
+
};
|
|
5705
|
+
const decision = authorizeOutbound(attempt, policy);
|
|
5706
|
+
if (!decision.ok) {
|
|
5707
|
+
const { refusal } = decision;
|
|
5708
|
+
if (opts.json) {
|
|
5709
|
+
// 这里**还没有** sendPreview(它在闸门通过之后才建),所以逐字段写
|
|
5710
|
+
console.log(JSON.stringify({ success: false, code: refusal.code, error: refusal.error,
|
|
5711
|
+
...(refusal.rateLimit ? { rateLimit: refusal.rateLimit } : {}) }));
|
|
5712
|
+
}
|
|
5713
|
+
else {
|
|
5714
|
+
console.log(chalk.red(`\n❌ ${refusal.error}`));
|
|
5715
|
+
if (refusal.code === 'TARGET_NOT_ALLOWED')
|
|
5716
|
+
console.log(chalk.gray(` 先运行: weflow-cli whitelist add ${talker}\n`));
|
|
5717
|
+
if (refusal.code === 'TARGET_BLOCKED')
|
|
5718
|
+
console.log(chalk.gray(' 这个人在黑名单里,任何一条命令行都不会发给他\n'));
|
|
5719
|
+
if (refusal.rateLimit)
|
|
5720
|
+
console.log(chalk.gray(` 最近 ${refusal.rateLimit.windowMs / 1000}s 内已发 ${refusal.rateLimit.count} 条\n`));
|
|
5721
|
+
}
|
|
5722
|
+
process.exit(1);
|
|
5723
|
+
}
|
|
5724
|
+
const sendPreview = { success: true, action: 'draft-send', target: { wxid, displayName },
|
|
5725
|
+
pick, text: chosen.text, rateLimit: decision.rate };
|
|
5726
|
+
// 两段式在这里:不给 `--yes` 时,JSON 模式回 `CONFIRMATION_REQUIRED`(**带着要发的原文**),
|
|
5727
|
+
// 交互模式则问一句"确定把第 N 条发给 X?"。`--dry-run` 那条路在上面的分支入口就返回了
|
|
5728
|
+
// (它意味着零出境,与"必须由模型产出候选"不可兼得),所以这里没有第三个预览分支。
|
|
5729
|
+
if (opts.json && !opts.yes) {
|
|
5730
|
+
console.log(JSON.stringify({ ...sendPreview, success: false, code: 'CONFIRMATION_REQUIRED', error: '使用 --yes 确认发送' }));
|
|
5731
|
+
process.exit(1);
|
|
5732
|
+
}
|
|
5733
|
+
if (!opts.yes) {
|
|
5734
|
+
const answer = await inquirer.prompt([{
|
|
5735
|
+
type: 'confirm', name: 'send', default: false,
|
|
5736
|
+
message: `确定把第 ${pick} 条发给 ${displayName}?`,
|
|
5737
|
+
}]);
|
|
5738
|
+
if (!answer.send) {
|
|
5739
|
+
console.log(chalk.gray('已取消,没有发送'));
|
|
5740
|
+
return;
|
|
5741
|
+
}
|
|
5742
|
+
}
|
|
5743
|
+
const sent = await performOutbound(attempt);
|
|
5744
|
+
if (sent.success) {
|
|
5745
|
+
if (opts.json)
|
|
5746
|
+
console.log(JSON.stringify(sendPreview));
|
|
5747
|
+
else
|
|
5748
|
+
console.log(chalk.green('✓ 发送成功'));
|
|
5749
|
+
return;
|
|
5750
|
+
}
|
|
5751
|
+
if (opts.json)
|
|
5752
|
+
console.log(JSON.stringify({ ...sendPreview, success: false, code: 'SEND_FAILED', error: sent.error }));
|
|
5753
|
+
else
|
|
5754
|
+
console.log(chalk.red(`✗ 发送失败 — ${sent.error}`));
|
|
5755
|
+
process.exit(1);
|
|
5756
|
+
}
|
|
5555
5757
|
const args = [script, '--talker', String(talker), '--count', String(count),
|
|
5556
5758
|
// 脚本自己有第二道闸门(`--yes` 才真的把对话发出去),所以要透传
|
|
5557
5759
|
...(confirmed ? ['--yes'] : []),
|
|
@@ -6030,6 +6232,7 @@ program
|
|
|
6030
6232
|
args.push('--skip-classify');
|
|
6031
6233
|
dailyLog(chalk.cyan(`\n📰 正在生成 ${targetDate} 公众号日报${noAi ? '(AI 已关闭)' : ''}...\n`));
|
|
6032
6234
|
const child = spawn(getPythonCommand(), args, {
|
|
6235
|
+
windowsHide: true,
|
|
6033
6236
|
stdio: opts.json ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
6034
6237
|
env: pythonProcessEnv(apiKey),
|
|
6035
6238
|
});
|
|
@@ -6045,6 +6248,7 @@ program
|
|
|
6045
6248
|
for (const source of opts.source || [])
|
|
6046
6249
|
dryRunArgs.push('--source', source);
|
|
6047
6250
|
const child = spawn(getPythonCommand(), dryRunArgs, {
|
|
6251
|
+
windowsHide: true,
|
|
6048
6252
|
stdio: opts.json ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
6049
6253
|
env: pythonProcessEnv(),
|
|
6050
6254
|
});
|
|
@@ -6191,6 +6395,351 @@ dailyFavoritesCmd
|
|
|
6191
6395
|
await runDailyFavorites(merged.date || defaultDailyDate(), merged.root, ['--remove', ...articles], 'daily.favorites.remove', merged);
|
|
6192
6396
|
});
|
|
6193
6397
|
program.commands.find(c => c.name() === 'daily')?.addCommand(dailyFavoritesCmd);
|
|
6398
|
+
// ==================== skill (本机技能包) ====================
|
|
6399
|
+
// 技能就是磁盘上的 `SKILL.md`,所以这里**没有安装/卸载**:读、看、体检三件事。
|
|
6400
|
+
// 体检(`check`)是最有用的一个 —— 它把"读不出来""不合规范""跨目录撞名"这些
|
|
6401
|
+
// 平时完全静默的问题一次报出来(少一个技能不会报错,只会少一个技能)。
|
|
6402
|
+
const skillCmd = program
|
|
6403
|
+
.command('skill')
|
|
6404
|
+
.description('本机技能包(SKILL.md):列出、查看、体检');
|
|
6405
|
+
skillCmd
|
|
6406
|
+
.command('list')
|
|
6407
|
+
.description('列出扫描到的技能')
|
|
6408
|
+
.option('--query <q>', '按关键词过滤(匹配 id、名称、说明、触发词)')
|
|
6409
|
+
.option('--json', 'JSON 输出')
|
|
6410
|
+
.action((opts) => {
|
|
6411
|
+
const scan = scanSkills();
|
|
6412
|
+
const disabled = disabledIds();
|
|
6413
|
+
const q = String(opts.query || '').trim().toLowerCase();
|
|
6414
|
+
const rows = scan.skills.filter(s => !q
|
|
6415
|
+
|| [s.id, s.name, s.description, ...s.triggers].some(x => String(x).toLowerCase().includes(q)))
|
|
6416
|
+
.map(s => ({
|
|
6417
|
+
id: s.id,
|
|
6418
|
+
name: s.name,
|
|
6419
|
+
description: s.description,
|
|
6420
|
+
version: s.version,
|
|
6421
|
+
state: skillState(s, disabled),
|
|
6422
|
+
notes: s.notes,
|
|
6423
|
+
// **不给路径**:这是机器输出,路径只有在用户明确要 `show` 时才出现
|
|
6424
|
+
}));
|
|
6425
|
+
if (opts.json) {
|
|
6426
|
+
console.log(JSON.stringify({
|
|
6427
|
+
success: true,
|
|
6428
|
+
roots: skillRoots(),
|
|
6429
|
+
missingRoots: scan.missingRoots,
|
|
6430
|
+
total: rows.length,
|
|
6431
|
+
skills: rows,
|
|
6432
|
+
}));
|
|
6433
|
+
return;
|
|
6434
|
+
}
|
|
6435
|
+
if (!rows.length) {
|
|
6436
|
+
console.log(chalk.gray(scan.skills.length ? `没有匹配「${opts.query}」的技能` : '没有扫描到技能'));
|
|
6437
|
+
console.log(chalk.gray(`技能目录:${skillRoots().join('、')}`));
|
|
6438
|
+
for (const dir of scan.missingRoots)
|
|
6439
|
+
console.log(chalk.gray(` (不存在:${dir})`));
|
|
6440
|
+
return;
|
|
6441
|
+
}
|
|
6442
|
+
for (const r of rows) {
|
|
6443
|
+
const tag = r.state === 'disabled' ? chalk.gray('[已禁用] ') : '';
|
|
6444
|
+
console.log(`${tag}${chalk.cyan(r.id)}${r.version ? chalk.gray(' v' + r.version) : ''} ${r.name}`);
|
|
6445
|
+
if (r.description)
|
|
6446
|
+
console.log(chalk.gray(` ${r.description.slice(0, 100)}`));
|
|
6447
|
+
for (const n of r.notes)
|
|
6448
|
+
console.log(chalk.yellow(` ⚠ ${n}`));
|
|
6449
|
+
}
|
|
6450
|
+
console.log(chalk.gray(`\n共 ${rows.length} 个。正文:weflow-cli skill show <id>`));
|
|
6451
|
+
});
|
|
6452
|
+
skillCmd
|
|
6453
|
+
.command('show')
|
|
6454
|
+
.argument('<id>', '技能 id')
|
|
6455
|
+
.description('打印一个技能的 SKILL.md 正文')
|
|
6456
|
+
.option('--json', 'JSON 输出')
|
|
6457
|
+
.action((id, opts) => {
|
|
6458
|
+
const body = readSkillBody(id);
|
|
6459
|
+
if (!body.ok) {
|
|
6460
|
+
if (opts.json) {
|
|
6461
|
+
console.log(JSON.stringify({ success: false, code: 'SKILL_NOT_READABLE', error: body.text }));
|
|
6462
|
+
process.exit(1);
|
|
6463
|
+
}
|
|
6464
|
+
console.log(chalk.red(body.text));
|
|
6465
|
+
return;
|
|
6466
|
+
}
|
|
6467
|
+
if (opts.json) {
|
|
6468
|
+
const scan = scanSkills();
|
|
6469
|
+
const info = scan.skills.find(s => s.id === id);
|
|
6470
|
+
console.log(JSON.stringify({
|
|
6471
|
+
success: true,
|
|
6472
|
+
id,
|
|
6473
|
+
name: info?.name ?? id,
|
|
6474
|
+
version: info?.version ?? '',
|
|
6475
|
+
truncated: !!body.truncated,
|
|
6476
|
+
body: body.text,
|
|
6477
|
+
}));
|
|
6478
|
+
return;
|
|
6479
|
+
}
|
|
6480
|
+
console.log(body.text);
|
|
6481
|
+
});
|
|
6482
|
+
skillCmd
|
|
6483
|
+
.command('check')
|
|
6484
|
+
.description('体检:不可读的 SKILL.md、跨目录同名、非规范 id、被禁用的技能')
|
|
6485
|
+
.option('--json', 'JSON 输出')
|
|
6486
|
+
.action((opts) => {
|
|
6487
|
+
const scan = scanSkills();
|
|
6488
|
+
const disabled = disabledIds();
|
|
6489
|
+
const disabledRows = scan.skills.filter(s => skillState(s, disabled) === 'disabled').map(s => s.id);
|
|
6490
|
+
const warned = scan.skills.filter(s => s.notes.length).map(s => ({ id: s.id, notes: s.notes }));
|
|
6491
|
+
const payload = {
|
|
6492
|
+
success: true,
|
|
6493
|
+
roots: skillRoots(),
|
|
6494
|
+
missingRoots: scan.missingRoots,
|
|
6495
|
+
total: scan.skills.length,
|
|
6496
|
+
unreadable: scan.unreadable,
|
|
6497
|
+
conflicts: scan.conflicts,
|
|
6498
|
+
warned,
|
|
6499
|
+
disabled: disabledRows,
|
|
6500
|
+
};
|
|
6501
|
+
if (opts.json) {
|
|
6502
|
+
console.log(JSON.stringify(payload));
|
|
6503
|
+
// 不可读 = 真出事了(有文件却读不出来),体检失败;警告与冲突只提示,不算失败
|
|
6504
|
+
if (scan.unreadable.length)
|
|
6505
|
+
process.exit(1);
|
|
6506
|
+
return;
|
|
6507
|
+
}
|
|
6508
|
+
console.log(`技能目录:${skillRoots().join('、')}`);
|
|
6509
|
+
for (const dir of scan.missingRoots)
|
|
6510
|
+
console.log(chalk.gray(` 不存在:${dir}`));
|
|
6511
|
+
console.log(`扫描到 ${scan.skills.length} 个技能`);
|
|
6512
|
+
if (scan.unreadable.length) {
|
|
6513
|
+
console.log(chalk.red(`\n不可读 ${scan.unreadable.length} 个:`));
|
|
6514
|
+
for (const u of scan.unreadable)
|
|
6515
|
+
console.log(chalk.red(` ✗ ${u.file}\n ${u.reason}`));
|
|
6516
|
+
}
|
|
6517
|
+
if (scan.conflicts.length) {
|
|
6518
|
+
console.log(chalk.yellow(`\n跨目录同名 ${scan.conflicts.length} 个(靠前的目录胜出,不会覆盖):`));
|
|
6519
|
+
for (const c of scan.conflicts)
|
|
6520
|
+
console.log(chalk.yellow(` ! ${c.id}\n 用:${c.winner}\n 忽略:${c.loser}`));
|
|
6521
|
+
}
|
|
6522
|
+
if (warned.length) {
|
|
6523
|
+
console.log(chalk.yellow(`\n有提醒的 ${warned.length} 个:`));
|
|
6524
|
+
for (const w of warned)
|
|
6525
|
+
for (const n of w.notes)
|
|
6526
|
+
console.log(chalk.yellow(` · ${w.id}:${n}`));
|
|
6527
|
+
}
|
|
6528
|
+
if (disabledRows.length)
|
|
6529
|
+
console.log(chalk.gray(`\n已禁用:${disabledRows.join('、')}`));
|
|
6530
|
+
if (!scan.unreadable.length && !scan.conflicts.length)
|
|
6531
|
+
console.log(chalk.green('\n没有不可读或撞名的技能'));
|
|
6532
|
+
});
|
|
6533
|
+
// ==================== scene (会话场景) ====================
|
|
6534
|
+
const sceneCmd = program
|
|
6535
|
+
.command('scene')
|
|
6536
|
+
.description('会话场景:按会话生效的提示词预设(绑定 → 关键词 → 上次用过的)');
|
|
6537
|
+
/** 写场景是**本机文件写**,不是 python mutation:同样两段式,措辞照 todos 那条 */
|
|
6538
|
+
async function confirmSceneWrite(opts, preview, message, apply) {
|
|
6539
|
+
if (opts.dryRun) {
|
|
6540
|
+
if (opts.json)
|
|
6541
|
+
console.log(JSON.stringify({ success: true, dryRun: true, ...preview }));
|
|
6542
|
+
else
|
|
6543
|
+
console.log(chalk.cyan('预览:') + message);
|
|
6544
|
+
return;
|
|
6545
|
+
}
|
|
6546
|
+
if (!opts.yes) {
|
|
6547
|
+
if (opts.json) {
|
|
6548
|
+
console.log(JSON.stringify({
|
|
6549
|
+
success: false,
|
|
6550
|
+
code: 'CONFIRMATION_REQUIRED',
|
|
6551
|
+
error: '先使用 --dry-run --json 预览,再由用户确认后使用 --yes --json 执行',
|
|
6552
|
+
}));
|
|
6553
|
+
process.exit(1);
|
|
6554
|
+
}
|
|
6555
|
+
const { confirmed } = await inquirer.prompt([{ type: 'confirm', name: 'confirmed', message, default: false }]);
|
|
6556
|
+
if (!confirmed) {
|
|
6557
|
+
console.log(chalk.gray('已取消'));
|
|
6558
|
+
return;
|
|
6559
|
+
}
|
|
6560
|
+
}
|
|
6561
|
+
const result = apply();
|
|
6562
|
+
if (result.ok)
|
|
6563
|
+
assistantScenes.save();
|
|
6564
|
+
const saved = result.ok && !assistantScenes.lastSaveError;
|
|
6565
|
+
if (opts.json) {
|
|
6566
|
+
if (!result.ok) {
|
|
6567
|
+
console.log(JSON.stringify({ success: false, code: 'SCENE_OPERATION_FAILED', error: result.reason }));
|
|
6568
|
+
process.exit(1);
|
|
6569
|
+
}
|
|
6570
|
+
// 写失败要说出来:`save()` 不抛异常,代价是必须有地方把它讲出来
|
|
6571
|
+
console.log(JSON.stringify({
|
|
6572
|
+
success: saved,
|
|
6573
|
+
...(saved ? {} : { code: 'SCENE_SAVE_FAILED', error: assistantScenes.lastSaveError }),
|
|
6574
|
+
...preview,
|
|
6575
|
+
}));
|
|
6576
|
+
if (!saved)
|
|
6577
|
+
process.exit(1);
|
|
6578
|
+
return;
|
|
6579
|
+
}
|
|
6580
|
+
if (!result.ok) {
|
|
6581
|
+
console.log(chalk.red(`✗ ${result.reason}`));
|
|
6582
|
+
process.exit(1);
|
|
6583
|
+
}
|
|
6584
|
+
if (!saved)
|
|
6585
|
+
console.log(chalk.red(`⚠ 已改动内存,但落盘失败:${assistantScenes.lastSaveError}`));
|
|
6586
|
+
else
|
|
6587
|
+
console.log(chalk.green('✓ 完成'));
|
|
6588
|
+
}
|
|
6589
|
+
sceneCmd
|
|
6590
|
+
.command('list')
|
|
6591
|
+
.description('列出所有场景')
|
|
6592
|
+
.option('--json', 'JSON 输出')
|
|
6593
|
+
.action((opts) => {
|
|
6594
|
+
const scenes = assistantScenes.list();
|
|
6595
|
+
if (opts.json) {
|
|
6596
|
+
console.log(JSON.stringify({ success: true, total: scenes.length, scenes }));
|
|
6597
|
+
return;
|
|
6598
|
+
}
|
|
6599
|
+
if (!scenes.length) {
|
|
6600
|
+
console.log(chalk.gray('还没有场景。加一个:'));
|
|
6601
|
+
console.log(chalk.gray(' weflow-cli scene add --id 日报 --keywords 日报,推文 --instruction "按主题分组" --dry-run'));
|
|
6602
|
+
return;
|
|
6603
|
+
}
|
|
6604
|
+
for (const s of scenes) {
|
|
6605
|
+
console.log(`${s.enabled ? chalk.cyan(s.id) : chalk.gray(s.id + ' [已停用]')} ${s.name}`);
|
|
6606
|
+
if (s.keywords.length)
|
|
6607
|
+
console.log(chalk.gray(` 关键词:${s.keywords.join('、')}`));
|
|
6608
|
+
if (s.instruction)
|
|
6609
|
+
console.log(chalk.gray(` ${s.instruction.split('\n')[0].slice(0, 80)}`));
|
|
6610
|
+
}
|
|
6611
|
+
});
|
|
6612
|
+
sceneCmd
|
|
6613
|
+
.command('show')
|
|
6614
|
+
.argument('<id>', '场景 id')
|
|
6615
|
+
.description('打印一个场景的全部字段')
|
|
6616
|
+
.option('--json', 'JSON 输出')
|
|
6617
|
+
.action((id, opts) => {
|
|
6618
|
+
const scene = assistantScenes.get(id);
|
|
6619
|
+
if (!scene) {
|
|
6620
|
+
if (opts.json)
|
|
6621
|
+
console.log(JSON.stringify({ success: false, code: 'SCENE_NOT_FOUND', error: `没有场景「${id}」` }));
|
|
6622
|
+
else
|
|
6623
|
+
console.log(chalk.red(`没有场景「${id}」`));
|
|
6624
|
+
process.exit(1);
|
|
6625
|
+
}
|
|
6626
|
+
if (opts.json) {
|
|
6627
|
+
console.log(JSON.stringify({ success: true, scene }));
|
|
6628
|
+
return;
|
|
6629
|
+
}
|
|
6630
|
+
console.log(`id ${scene.id}`);
|
|
6631
|
+
console.log(`名称 ${scene.name}`);
|
|
6632
|
+
console.log(`状态 ${scene.enabled ? '启用' : '已停用'}`);
|
|
6633
|
+
console.log(`关键词 ${scene.keywords.join('、') || '(无)'}`);
|
|
6634
|
+
console.log(`所需技能 ${scene.skills.join('、') || '(无)'}`);
|
|
6635
|
+
console.log(`附加指令 ${scene.instruction || '(无)'}`);
|
|
6636
|
+
if (scene.outputSpec)
|
|
6637
|
+
console.log(`输出规范 ${scene.outputSpec}`);
|
|
6638
|
+
});
|
|
6639
|
+
sceneCmd
|
|
6640
|
+
.command('add')
|
|
6641
|
+
.description('新增一个场景')
|
|
6642
|
+
.requiredOption('--id <id>', '场景 id(字母数字开头,可用 . _ -)')
|
|
6643
|
+
.option('--name <name>', '显示名(默认用 id)')
|
|
6644
|
+
.option('--keywords <list>', '关键词,逗号分隔')
|
|
6645
|
+
.option('--instruction <text>', '附加指令(长文本用 --from-file)')
|
|
6646
|
+
.option('--from-file <path>', '从文件读附加指令(多行中文走这条)')
|
|
6647
|
+
.option('--output-spec <text>', '输出规范')
|
|
6648
|
+
.option('--skills <list>', '所需技能 id,逗号分隔')
|
|
6649
|
+
.option('--dry-run', '仅预览,不写入')
|
|
6650
|
+
.option('--yes', '确认写入')
|
|
6651
|
+
.option('--json', 'JSON 输出')
|
|
6652
|
+
.action(async (opts) => {
|
|
6653
|
+
const id = String(opts.id || '').trim();
|
|
6654
|
+
const idError = validateSceneId(id);
|
|
6655
|
+
if (idError) {
|
|
6656
|
+
if (opts.json)
|
|
6657
|
+
console.log(JSON.stringify({ success: false, code: 'INVALID_SCENE_ID', error: idError }));
|
|
6658
|
+
else
|
|
6659
|
+
console.log(chalk.red(idError));
|
|
6660
|
+
process.exit(1);
|
|
6661
|
+
}
|
|
6662
|
+
let instruction = String(opts.instruction || '');
|
|
6663
|
+
if (opts.fromFile) {
|
|
6664
|
+
const path = String(opts.fromFile);
|
|
6665
|
+
if (!existsSync(path)) {
|
|
6666
|
+
if (opts.json)
|
|
6667
|
+
console.log(JSON.stringify({ success: false, code: 'FILE_NOT_FOUND', error: `读不到 ${path}` }));
|
|
6668
|
+
else
|
|
6669
|
+
console.log(chalk.red(`读不到 ${path}`));
|
|
6670
|
+
process.exit(1);
|
|
6671
|
+
}
|
|
6672
|
+
instruction = readFileSync(path, 'utf8');
|
|
6673
|
+
}
|
|
6674
|
+
const splitList = (v) => String(v || '').split(/[,;]/).map(s => s.trim()).filter(Boolean);
|
|
6675
|
+
const scene = {
|
|
6676
|
+
id,
|
|
6677
|
+
name: String(opts.name || '').trim() || id,
|
|
6678
|
+
keywords: splitList(opts.keywords),
|
|
6679
|
+
instruction,
|
|
6680
|
+
outputSpec: String(opts.outputSpec || ''),
|
|
6681
|
+
skills: splitList(opts.skills),
|
|
6682
|
+
enabled: true,
|
|
6683
|
+
};
|
|
6684
|
+
await confirmSceneWrite(opts, { action: 'scene.add', scene: { ...scene, instruction: `(${instruction.length} 字)` } }, `确认新增场景「${scene.name}」?`, () => assistantScenes.add(scene));
|
|
6685
|
+
});
|
|
6686
|
+
sceneCmd
|
|
6687
|
+
.command('remove')
|
|
6688
|
+
.argument('<id>', '场景 id')
|
|
6689
|
+
.description('删除一个场景(连同指向它的绑定)')
|
|
6690
|
+
.option('--dry-run', '仅预览,不删除')
|
|
6691
|
+
.option('--yes', '确认删除')
|
|
6692
|
+
.option('--json', 'JSON 输出')
|
|
6693
|
+
.action(async (id, opts) => {
|
|
6694
|
+
const scene = assistantScenes.get(id);
|
|
6695
|
+
if (!scene) {
|
|
6696
|
+
if (opts.json)
|
|
6697
|
+
console.log(JSON.stringify({ success: false, code: 'SCENE_NOT_FOUND', error: `没有场景「${id}」` }));
|
|
6698
|
+
else
|
|
6699
|
+
console.log(chalk.red(`没有场景「${id}」`));
|
|
6700
|
+
process.exit(1);
|
|
6701
|
+
}
|
|
6702
|
+
await confirmSceneWrite(opts, { action: 'scene.remove', id }, `确认删除场景「${scene.name}」?`, () => { assistantScenes.remove(id); return { ok: true }; });
|
|
6703
|
+
});
|
|
6704
|
+
const toggleScene = (on) => async (id, opts) => {
|
|
6705
|
+
const scene = assistantScenes.get(id);
|
|
6706
|
+
if (!scene) {
|
|
6707
|
+
if (opts.json)
|
|
6708
|
+
console.log(JSON.stringify({ success: false, code: 'SCENE_NOT_FOUND', error: `没有场景「${id}」` }));
|
|
6709
|
+
else
|
|
6710
|
+
console.log(chalk.red(`没有场景「${id}」`));
|
|
6711
|
+
process.exit(1);
|
|
6712
|
+
}
|
|
6713
|
+
await confirmSceneWrite(opts, { action: on ? 'scene.enable' : 'scene.disable', id }, `确认${on ? '启用' : '停用'}场景「${scene.name}」?`, () => ({ ok: assistantScenes.setEnabled(id, on) }));
|
|
6714
|
+
};
|
|
6715
|
+
sceneCmd.command('enable')
|
|
6716
|
+
.argument('<id>', '场景 id')
|
|
6717
|
+
.description('启用一个场景')
|
|
6718
|
+
.option('--dry-run', '仅预览').option('--yes', '确认').option('--json', 'JSON 输出')
|
|
6719
|
+
.action(toggleScene(true));
|
|
6720
|
+
sceneCmd.command('disable')
|
|
6721
|
+
.argument('<id>', '场景 id')
|
|
6722
|
+
.description('停用一个场景(它不再参与匹配)')
|
|
6723
|
+
.option('--dry-run', '仅预览').option('--yes', '确认').option('--json', 'JSON 输出')
|
|
6724
|
+
.action(toggleScene(false));
|
|
6725
|
+
sceneCmd
|
|
6726
|
+
.command('bind')
|
|
6727
|
+
.argument('<conversation>', '会话 id(微信里就是 conversationId;本机面板用它的记忆桶)')
|
|
6728
|
+
.requiredOption('--scene <id>', '要绑定的场景 id')
|
|
6729
|
+
.description('把某个场景固定绑到一个会话')
|
|
6730
|
+
.option('--dry-run', '仅预览').option('--yes', '确认').option('--json', 'JSON 输出')
|
|
6731
|
+
.action(async (conversation, opts) => {
|
|
6732
|
+
const id = String(opts.scene || '').trim();
|
|
6733
|
+
await confirmSceneWrite(opts, { action: 'scene.bind', conversation, scene: id }, `确认把场景「${id}」绑到会话 ${conversation}?`, () => assistantScenes.bind(conversation, id));
|
|
6734
|
+
});
|
|
6735
|
+
sceneCmd
|
|
6736
|
+
.command('unbind')
|
|
6737
|
+
.argument('<conversation>', '会话 id')
|
|
6738
|
+
.description('解绑:这个会话回到按关键词自动挑')
|
|
6739
|
+
.option('--dry-run', '仅预览').option('--yes', '确认').option('--json', 'JSON 输出')
|
|
6740
|
+
.action(async (conversation, opts) => {
|
|
6741
|
+
await confirmSceneWrite(opts, { action: 'scene.unbind', conversation }, `确认解绑会话 ${conversation}?`, () => assistantScenes.bind(conversation, null));
|
|
6742
|
+
});
|
|
6194
6743
|
// ==================== assistant (第二大脑持久化 Agent) ====================
|
|
6195
6744
|
const assistantCmd = program
|
|
6196
6745
|
.command('assistant')
|
|
@@ -6643,6 +7192,52 @@ program
|
|
|
6643
7192
|
process.exit(1);
|
|
6644
7193
|
}
|
|
6645
7194
|
});
|
|
7195
|
+
// ==================== bonds ====================
|
|
7196
|
+
program
|
|
7197
|
+
.command('bonds')
|
|
7198
|
+
.description('关系温度:谁在冷却、谁在变热、谁只有微信这一条线(只读本地库,不联网、不调模型)')
|
|
7199
|
+
.option('--days <number>', '看最近多少天', '365')
|
|
7200
|
+
.option('--limit <number>', '最多扫几个会话', '400')
|
|
7201
|
+
.option('--top <number>', '每组列几条', '12')
|
|
7202
|
+
.option('--skip <name>', '排除某个名字或 id(可重复)', (value, acc) => {
|
|
7203
|
+
acc.push(value);
|
|
7204
|
+
return acc;
|
|
7205
|
+
}, [])
|
|
7206
|
+
.option('--json', '输出 JSON 格式')
|
|
7207
|
+
.action(async (opts) => {
|
|
7208
|
+
const { execFile } = await import('child_process');
|
|
7209
|
+
const { promisify } = await import('util');
|
|
7210
|
+
const execFileAsync = promisify(execFile);
|
|
7211
|
+
const script = join(resolvePackageRoot(), 'scripts', 'bonds.py');
|
|
7212
|
+
const days = parseCliInteger(opts.days, 'days', 1, 3650, opts.json);
|
|
7213
|
+
const limit = parseCliInteger(opts.limit, 'limit', 1, 5000, opts.json);
|
|
7214
|
+
const top = parseCliInteger(opts.top, 'top', 1, 200, opts.json);
|
|
7215
|
+
try {
|
|
7216
|
+
const args = [script, '--days', String(days), '--limit', String(limit), '--top', String(top)];
|
|
7217
|
+
for (const name of opts.skip || [])
|
|
7218
|
+
args.push('--skip', name);
|
|
7219
|
+
if (opts.json)
|
|
7220
|
+
args.push('--json');
|
|
7221
|
+
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
7222
|
+
timeout: 300_000,
|
|
7223
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
7224
|
+
env: pythonProcessEnv(),
|
|
7225
|
+
});
|
|
7226
|
+
console.log(stdout);
|
|
7227
|
+
}
|
|
7228
|
+
catch (e) {
|
|
7229
|
+
if (opts.json) {
|
|
7230
|
+
console.log(JSON.stringify({
|
|
7231
|
+
success: false,
|
|
7232
|
+
code: 'BONDS_FAILED',
|
|
7233
|
+
error: '关系温度不可用,请运行 check --json 检查配置和依赖',
|
|
7234
|
+
}));
|
|
7235
|
+
process.exit(1);
|
|
7236
|
+
}
|
|
7237
|
+
console.error(chalk.red(`\n${safeSubprocessError(e, '统计失败')}`));
|
|
7238
|
+
process.exit(1);
|
|
7239
|
+
}
|
|
7240
|
+
});
|
|
6646
7241
|
// ==================== daily-server ====================
|
|
6647
7242
|
program
|
|
6648
7243
|
.command('daily-server')
|
|
@@ -6724,6 +7319,7 @@ program
|
|
|
6724
7319
|
await openLocalUrl(`http://localhost:${port}`);
|
|
6725
7320
|
}
|
|
6726
7321
|
const child = spawn(getPythonCommand(), [favServer, '--date', date, '--port', String(port)], {
|
|
7322
|
+
windowsHide: true,
|
|
6727
7323
|
stdio: 'inherit',
|
|
6728
7324
|
env: pythonProcessEnv(),
|
|
6729
7325
|
});
|
|
@@ -6828,14 +7424,14 @@ program
|
|
|
6828
7424
|
const optionalDependencies = ['scrapling'];
|
|
6829
7425
|
let pythonVersion = '';
|
|
6830
7426
|
try {
|
|
6831
|
-
pythonVersion = execFileSync(getPythonCommand(), ['--version'], { encoding: 'utf8', timeout: 5000 }).trim();
|
|
7427
|
+
pythonVersion = execFileSync(getPythonCommand(), ['--version'], { windowsHide: true, encoding: 'utf8', timeout: 5000 }).trim();
|
|
6832
7428
|
}
|
|
6833
7429
|
catch { }
|
|
6834
7430
|
const dependencyStatus = (name) => {
|
|
6835
7431
|
if (!pythonVersion)
|
|
6836
7432
|
return false;
|
|
6837
7433
|
try {
|
|
6838
|
-
execFileSync(getPythonCommand(), ['-c', `import ${name}`], { stdio: 'ignore', timeout: 5000 });
|
|
7434
|
+
execFileSync(getPythonCommand(), ['-c', `import ${name}`], { windowsHide: true, stdio: 'ignore', timeout: 5000 });
|
|
6839
7435
|
return true;
|
|
6840
7436
|
}
|
|
6841
7437
|
catch {
|
|
@@ -6891,7 +7487,7 @@ program
|
|
|
6891
7487
|
const pythonCommand = getPythonCommand();
|
|
6892
7488
|
let pythonOk = false;
|
|
6893
7489
|
try {
|
|
6894
|
-
const pyVer = execFileSync(pythonCommand, ['--version'], { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
7490
|
+
const pyVer = execFileSync(pythonCommand, ['--version'], { windowsHide: true, encoding: 'utf-8', timeout: 5000 }).trim();
|
|
6895
7491
|
console.log(chalk.white('Python: '), chalk.green(pyVer));
|
|
6896
7492
|
pythonOk = true;
|
|
6897
7493
|
}
|
|
@@ -6905,7 +7501,7 @@ program
|
|
|
6905
7501
|
let allOk = true;
|
|
6906
7502
|
for (const dep of deps) {
|
|
6907
7503
|
try {
|
|
6908
|
-
execFileSync(pythonCommand, ['-c', `import ${dep}`], { stdio: 'ignore', timeout: 5000 });
|
|
7504
|
+
execFileSync(pythonCommand, ['-c', `import ${dep}`], { windowsHide: true, stdio: 'ignore', timeout: 5000 });
|
|
6909
7505
|
console.log(` ${dep.padEnd(20)} ${chalk.green('✓')}`);
|
|
6910
7506
|
}
|
|
6911
7507
|
catch {
|
|
@@ -6928,7 +7524,7 @@ program
|
|
|
6928
7524
|
console.log(chalk.white('\n可选依赖(提升抓取成功率):'));
|
|
6929
7525
|
for (const dep of optDeps) {
|
|
6930
7526
|
try {
|
|
6931
|
-
execFileSync(pythonCommand, ['-c', `import ${dep}`], { stdio: 'ignore', timeout: 5000 });
|
|
7527
|
+
execFileSync(pythonCommand, ['-c', `import ${dep}`], { windowsHide: true, stdio: 'ignore', timeout: 5000 });
|
|
6932
7528
|
console.log(` ${dep.padEnd(20)} ${chalk.green('✓')}`);
|
|
6933
7529
|
}
|
|
6934
7530
|
catch {
|
|
@@ -7185,6 +7781,7 @@ async function showInteractiveMenu() {
|
|
|
7185
7781
|
const pipeline = join(resolvePackageRoot(), 'scripts', 'pipeline.py');
|
|
7186
7782
|
console.log(chalk.cyan(`\n📰 正在生成 ${dateStr} 公众号日报...\n`));
|
|
7187
7783
|
const child = spawn(getPythonCommand(), [pipeline, '--date', dateStr, '--interest', 'AI', '--skip-wiki'], {
|
|
7784
|
+
windowsHide: true,
|
|
7188
7785
|
stdio: 'inherit',
|
|
7189
7786
|
env: pythonProcessEnv(apiKey),
|
|
7190
7787
|
});
|