weflow-cli 1.8.2 → 1.10.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/ARCHITECTURE.md +62 -26
- package/CHANGELOG.md +1290 -0
- package/OPERATIONS.md +144 -11
- package/README.en.md +20 -4
- package/README.md +30 -5
- package/bin/weflow-cli-electron.cjs +2 -0
- package/bin/weflow-cli.ts +996 -118
- package/dist/bin/weflow-cli.js +1038 -146
- 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 +58 -374
- package/dist/mcp-server/index.js.map +1 -1
- package/dist/src/core/dbPathService.d.ts +12 -0
- package/dist/src/core/dbPathService.d.ts.map +1 -1
- package/dist/src/core/dbPathService.js +20 -3
- package/dist/src/core/dbPathService.js.map +1 -1
- package/dist/src/panel/server.d.ts.map +1 -1
- package/dist/src/panel/server.js +61 -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 +33 -4
- package/dist/src/services/assistantService.d.ts.map +1 -1
- package/dist/src/services/assistantService.js +102 -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 +34 -1
- package/dist/src/services/assistantTools.d.ts.map +1 -1
- package/dist/src/services/assistantTools.js +931 -88
- 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 +9 -0
- package/dist/src/services/configService.d.ts.map +1 -1
- package/dist/src/services/configService.js +5 -2
- package/dist/src/services/configService.js.map +1 -1
- package/dist/src/services/messageQuery.d.ts +18 -0
- package/dist/src/services/messageQuery.d.ts.map +1 -1
- package/dist/src/services/messageQuery.js +29 -0
- package/dist/src/services/messageQuery.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/services/wechatClient.d.ts +125 -0
- package/dist/src/services/wechatClient.d.ts.map +1 -0
- package/dist/src/services/wechatClient.js +344 -0
- package/dist/src/services/wechatClient.js.map +1 -0
- package/dist/src/services/wechatMedia.d.ts +44 -0
- package/dist/src/services/wechatMedia.d.ts.map +1 -0
- package/dist/src/services/wechatMedia.js +155 -0
- package/dist/src/services/wechatMedia.js.map +1 -0
- package/dist/src/services/wechatMessageService.d.ts +11 -0
- package/dist/src/services/wechatMessageService.d.ts.map +1 -1
- package/dist/src/services/wechatMessageService.js +55 -11
- package/dist/src/services/wechatMessageService.js.map +1 -1
- package/dist/src/types.d.ts +4 -0
- package/dist/src/types.d.ts.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/CONTACT_DB_SCHEMA.md +1085 -0
- package/docs/DECISIONS.md +1624 -0
- package/docs/EXTENDING.md +98 -10
- package/docs/FUNCTIONAL-IMPROVEMENT-SPEC.md +6 -0
- package/docs/HEALTH-CHECK.md +12 -7
- package/docs/JEV/345/272/224/347/224/250/346/226/275/345/267/245/346/226/207/346/241/243.md +2 -2
- package/docs/MCP.md +58 -4
- package/docs/PROJECT_STATE.md +180 -11
- package/docs/RELEASING.md +115 -1
- package/docs/images/weflow-architecture-gpt-image-2.png +0 -0
- package/logo.png +0 -0
- package/mcp-server/http.ts +108 -0
- package/mcp-server/httpConfig.ts +126 -0
- package/mcp-server/index.ts +359 -681
- package/package.json +5 -3
- package/resources/js/graph3d/NOTICE.txt +35 -0
- package/resources/js/graph3d/d3-binarytree.min.js +2 -0
- package/resources/js/graph3d/d3-dispatch.min.js +2 -0
- package/resources/js/graph3d/d3-force-3d.min.js +2 -0
- package/resources/js/graph3d/d3-octree.min.js +2 -0
- package/resources/js/graph3d/d3-quadtree.min.js +2 -0
- package/resources/js/graph3d/d3-timer.min.js +2 -0
- package/resources/js/graph3d/layout.mjs +63 -0
- package/resources/js/graph3d/three.min.js +6 -0
- package/resources/js/graph3d/viewer2d.js +448 -0
- package/resources/panel/ball-position.cjs +48 -2
- package/resources/panel/index.html +18 -4
- package/resources/panel/main.cjs +137 -5
- package/resources/panel/mascot-base.png +0 -0
- package/resources/panel/mascot-blink-1.png +0 -0
- package/resources/panel/mascot-blink-2.png +0 -0
- package/resources/panel/mascot-iris.png +0 -0
- package/resources/panel/mascot-lift-down-1.png +0 -0
- package/resources/panel/mascot-lift-down-2.png +0 -0
- package/resources/panel/mascot-lift-down-3.png +0 -0
- package/resources/panel/mascot-lift-down-4.png +0 -0
- package/resources/panel/mascot-lift-down-5.png +0 -0
- package/resources/panel/mascot-lift-down-6.png +0 -0
- package/resources/panel/mascot-lift-up-1.png +0 -0
- package/resources/panel/mascot-lift-up-2.png +0 -0
- package/resources/panel/mascot-lift-up-3.png +0 -0
- package/resources/panel/mascot-lift-up-4.png +0 -0
- package/resources/panel/mascot-lift-up-5.png +0 -0
- package/resources/panel/mascot-lift-up-6.png +0 -0
- package/resources/panel/mascot-peek.png +0 -0
- package/resources/panel/mascot-tickle-1.png +0 -0
- package/resources/panel/mascot-tickle-2.png +0 -0
- package/resources/panel/mascot-tickle-3.png +0 -0
- package/resources/panel/mascot-tickle-4.png +0 -0
- package/resources/panel/panel.css +205 -59
- package/resources/panel/preload.cjs +26 -0
- package/resources/panel/renderer.js +646 -17
- package/scripts/_utils.py +1203 -931
- package/scripts/backfill_articles.py +202 -21
- package/scripts/biz_daily.py +26 -34
- package/scripts/bonds.py +331 -0
- package/scripts/chat_notes.py +561 -470
- package/scripts/chat_stats.py +8 -7
- package/scripts/check_readme_commands.py +426 -0
- package/scripts/compile_wiki.py +187 -59
- package/scripts/contact_schema.py +339 -0
- package/scripts/data/wechat_emoji.json +225 -0
- package/scripts/export_chat_html.py +3 -1
- package/scripts/fav_notes.py +4 -1
- package/scripts/graph_2d.py +151 -0
- package/scripts/graph_3d.py +646 -0
- package/scripts/health_check.py +163 -26
- package/scripts/mcp_bridge.py +6 -7
- package/scripts/nt_decrypt.py +3 -3
- package/scripts/panel_frames.py +683 -0
- package/scripts/pipeline.py +21 -10
- package/scripts/quality_eval.py +5 -0
- package/scripts/rag_chat.py +33 -16
- package/scripts/reply_debt.py +26 -6
- package/scripts/semantic_search.py +130 -22
- package/scripts/vault_rag.py +42 -16
- package/scripts/vault_search.py +44 -8
- package/scripts/wechat_emoticon.py +6 -0
- package/scripts/wechat_voice.py +9 -0
- package/scripts/wiki_lint.py +124 -24
- package/src/core/dbPathService.ts +22 -4
- package/src/panel/server.ts +58 -1
- package/src/services/assistantEval.ts +323 -12
- package/src/services/assistantScenes.ts +342 -0
- package/src/services/assistantService.ts +107 -6
- package/src/services/assistantSkills.ts +364 -0
- package/src/services/assistantTools.ts +2452 -1640
- package/src/services/assistantTrace.ts +9 -0
- package/src/services/configService.ts +415 -403
- package/src/services/messageQuery.ts +29 -0
- package/src/services/outboundSend.ts +126 -0
- package/src/services/pythonBridge.ts +1 -0
- package/src/services/wechatClient.ts +380 -0
- package/src/services/wechatMedia.ts +166 -0
- package/src/services/wechatMessageService.ts +62 -11
- package/src/types.ts +4 -0
- package/src/utils/python.ts +2 -2
- package/dist/src/utils/wechatEmoji.d.ts +0 -5
- package/dist/src/utils/wechatEmoji.d.ts.map +0 -1
- package/dist/src/utils/wechatEmoji.js +0 -35
- package/dist/src/utils/wechatEmoji.js.map +0 -1
package/dist/bin/weflow-cli.js
CHANGED
|
@@ -12,6 +12,7 @@ import { keyService } from '../src/core/keyService.js';
|
|
|
12
12
|
import { NtCore } from '../src/core/ntCore.js';
|
|
13
13
|
import { configService } from '../src/services/configService.js';
|
|
14
14
|
import { chatService } from '../src/services/chatService.js';
|
|
15
|
+
import { emptyMessagesNote, looksLikeTalkerId } from '../src/services/messageQuery.js';
|
|
15
16
|
import { exportService } from '../src/services/exportService.js';
|
|
16
17
|
import { writeEvidencePackage } from '../src/services/evidenceService.js';
|
|
17
18
|
import { resolveTalker as resolveTalkerCore } from '../src/utils/talkerUtils.js';
|
|
@@ -21,7 +22,11 @@ import { DateRangeError, parseLocalDateOrIso, resolveExportDateRange } from '../
|
|
|
21
22
|
import { resolvePackageRoot as resolvePackageRootFrom } from '../src/utils/packageRoot.js';
|
|
22
23
|
import { WechatMessageService } from '../src/services/wechatMessageService.js';
|
|
23
24
|
import { whitelistService, MAX_TEXT_LENGTH } from '../src/services/whitelistService.js';
|
|
25
|
+
import { authorizeOutbound, parseOutboundPolicy, performOutbound } from '../src/services/outboundSend.js';
|
|
24
26
|
import { applyDerivedNtKeys, enableFavorites, detectFavDbPath } from '../src/services/initKeyService.js';
|
|
27
|
+
import { disabledIds, readSkillBody, scanSkills, skillRoots, skillState } from '../src/services/assistantSkills.js';
|
|
28
|
+
import { assistantScenes, validateSceneId } from '../src/services/assistantScenes.js';
|
|
29
|
+
import { detectWechatClient, reportedDataDirs, samePath } from '../src/services/wechatClient.js';
|
|
25
30
|
const program = new Command();
|
|
26
31
|
function resolvePackageRoot() {
|
|
27
32
|
return resolvePackageRootFrom(import.meta.url);
|
|
@@ -59,6 +64,24 @@ function parseCliInteger(value, field, minimum, maximum, json = false) {
|
|
|
59
64
|
console.log(chalk.red(error));
|
|
60
65
|
process.exit(1);
|
|
61
66
|
}
|
|
67
|
+
/** 校验一个"只能是这几个值之一"的参数。与 `parseCliInteger` / `requireCliDate` 同一约定:
|
|
68
|
+
* `--json` 时错误也得是一份 JSON —— 否则机器调用方在 stdout 上拿到的是彩色散文,解析不了。
|
|
69
|
+
*
|
|
70
|
+
* 抽成一个是有理由的:`--line` 的校验原本在四个命令里各写了一遍,其中三处只打散文(`vault rag`、
|
|
71
|
+
* 顶层 `search`、`chat`)—— 同一份判断写四遍,就有三遍漏掉了 `--json` 分支。
|
|
72
|
+
* 错误文案由 choices 生成,保证与原来逐字一致('line 必须是 wiki、chat 或 all')。
|
|
73
|
+
*/
|
|
74
|
+
function requireCliChoice(value, choices, field, json = false) {
|
|
75
|
+
const text = String(value);
|
|
76
|
+
if (choices.includes(text))
|
|
77
|
+
return text;
|
|
78
|
+
const error = `${field} 必须是 ${choices.slice(0, -1).join('、')} 或 ${choices[choices.length - 1]}`;
|
|
79
|
+
if (json)
|
|
80
|
+
console.log(JSON.stringify({ success: false, code: 'INVALID_ARGUMENT', field, error }));
|
|
81
|
+
else
|
|
82
|
+
console.log(chalk.red(error));
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
62
85
|
function requireCliDate(value, json = false) {
|
|
63
86
|
try {
|
|
64
87
|
parseLocalDateOrIso(value);
|
|
@@ -264,7 +287,7 @@ program
|
|
|
264
287
|
read: {
|
|
265
288
|
sessions: { cli: 'sessions --json', mcp: 'wechat.list_sessions' },
|
|
266
289
|
messages: { cli: 'messages <talker> --json', mcp: 'wechat.export_messages' },
|
|
267
|
-
contacts: { cli: 'contacts --json' },
|
|
290
|
+
contacts: { cli: 'contacts --json', mcp: 'wechat.list_contacts', localIdentifiersInModelContext: false },
|
|
268
291
|
exports: {
|
|
269
292
|
cli: 'export <talker> <json|txt|html|excel>',
|
|
270
293
|
mcp: 'wechat.export_messages',
|
|
@@ -298,8 +321,36 @@ program
|
|
|
298
321
|
},
|
|
299
322
|
dailyReader: { cli: 'daily-server --status --json' },
|
|
300
323
|
dailyStats: { cli: 'daily-stats --json' },
|
|
324
|
+
// 只读、纯本地、不调模型。**故意没有 mcp 那一栏**(D-062):它给的是关于第三方的推断
|
|
325
|
+
// ——谁在被冷落、你会失去谁 —— 所以不进助手、不进 MCP、不喂模型。
|
|
326
|
+
bonds: { cli: 'bonds --json', mcp: null, leavesTheMachine: false },
|
|
301
327
|
diagnostics: { cli: 'check --json' },
|
|
302
328
|
todos: { cli: 'todos list --json' },
|
|
329
|
+
// 2026-09-29 三个界面对齐时新开的读取能力(D-058)。逐个写清它会不会出境,
|
|
330
|
+
// 因为其中两个会——`fetch_article` 白名单 + 逐跳复检,`search_public` 没有任何守卫。
|
|
331
|
+
review: { cli: 'review ...', mcp: 'wechat.get_review', writesReport: true },
|
|
332
|
+
concepts: { cli: 'wiki ...', mcp: 'wechat.get_concepts' },
|
|
333
|
+
articleFetch: {
|
|
334
|
+
cli: null, // 没有对应的 CLI 命令:它本来就是为模型准备的读取能力
|
|
335
|
+
mcp: 'wechat.fetch_article',
|
|
336
|
+
network: true,
|
|
337
|
+
allowlist: 'https only, host must be exactly mp.weixin.qq.com',
|
|
338
|
+
redirectsReValidatedPerHop: true,
|
|
339
|
+
confirmationRequired: false,
|
|
340
|
+
},
|
|
341
|
+
publicArticleSearch: {
|
|
342
|
+
mcp: 'wechat.search_public',
|
|
343
|
+
network: true,
|
|
344
|
+
// **这条是如实的**:搜索词由模型决定、发到第三方搜索页,没有白名单也没有预览。
|
|
345
|
+
// 它在该项目的边界内是被明确接受的残余风险(D-058),这里不能写成"无出境"。
|
|
346
|
+
allowlist: null,
|
|
347
|
+
modelAuthoredQueryLeavesTheMachine: true,
|
|
348
|
+
confirmationRequired: false,
|
|
349
|
+
},
|
|
350
|
+
articleFormatting: { mcp: 'wechat.format_article', cli: null, writesNothing: true, network: false },
|
|
351
|
+
themes: { mcp: 'wechat.list_themes', writesNothing: true },
|
|
352
|
+
wikiLint: { cli: 'wiki lint --json', mcp: 'wechat.lint_wiki', readsLocalChat: false, writesNothing: true },
|
|
353
|
+
skillCheck: { cli: 'skill check --json', mcp: 'wechat.check_skills', writesNothing: true },
|
|
303
354
|
awaiting: {
|
|
304
355
|
cli: 'awaiting --yes',
|
|
305
356
|
preview: 'awaiting --dry-run --json',
|
|
@@ -382,6 +433,33 @@ program
|
|
|
382
433
|
execute: 'todos <done|undone|rm> <id> --yes --json',
|
|
383
434
|
confirmationRequired: true,
|
|
384
435
|
},
|
|
436
|
+
skills: {
|
|
437
|
+
list: 'skill list [--query <q>] --json',
|
|
438
|
+
show: 'skill show <id> --json',
|
|
439
|
+
check: 'skill check --json',
|
|
440
|
+
readOnly: true,
|
|
441
|
+
// 技能是**资料**,不是插件:助手只把它当提示词/正文读,不执行里面的脚本,
|
|
442
|
+
// 也不因为装了技能就多出任何工具(P4 验收标准:扩展读不到配置与数据库)。
|
|
443
|
+
installsAnything: false,
|
|
444
|
+
executesSkillCode: false,
|
|
445
|
+
grantsTools: false,
|
|
446
|
+
directories: 'skillDirs config (default: ~/.claude/skills, ~/.weflow-cli/skills)',
|
|
447
|
+
disable: 'config set skillDisabled <id,...>(技能自己也能写 enabled: false)',
|
|
448
|
+
},
|
|
449
|
+
scenes: {
|
|
450
|
+
list: 'scene list --json',
|
|
451
|
+
show: 'scene show <id> --json',
|
|
452
|
+
preview: 'scene <add|remove|enable|disable|bind|unbind> … --dry-run --json',
|
|
453
|
+
execute: 'scene <add|remove|enable|disable|bind|unbind> … --yes --json',
|
|
454
|
+
confirmationRequired: true,
|
|
455
|
+
// 场景只改**本机提示词预设**,不发消息、不读聊天、不调模型
|
|
456
|
+
readsLocalChat: false,
|
|
457
|
+
invokesAI: false,
|
|
458
|
+
sendsNothing: true,
|
|
459
|
+
stateLocation: 'local-user-config-dir',
|
|
460
|
+
stateSchema: 'weflow-scenes/v1',
|
|
461
|
+
modelWritable: false,
|
|
462
|
+
},
|
|
385
463
|
accessControlMutations: {
|
|
386
464
|
preview: '<whitelist|blacklist> <add|rm> <target> --dry-run --json',
|
|
387
465
|
execute: '<whitelist|blacklist> <add|rm> <target> --yes --json',
|
|
@@ -553,9 +631,11 @@ program
|
|
|
553
631
|
// 读这一位的调用方按 false 走保守分支即可。
|
|
554
632
|
mcpDefaultReadOnly: false,
|
|
555
633
|
mcpSurface: {
|
|
556
|
-
//
|
|
557
|
-
//
|
|
558
|
-
|
|
634
|
+
// 这句话**手工补过两回**:先是只写 save_memory,后来补上 `look_at_image`,第三次加
|
|
635
|
+
// `set_todo_status` 时又漏了 —— 每次都是"表变了、这句话没变",而且没有任何东西会红。
|
|
636
|
+
// 现在名字**逐个与 `MCP_EXCLUDED` 对齐**,并由 `test/assistant-tools.test.ts` 从源码里核:
|
|
637
|
+
// 那张表再加一项而这里没跟,测试就红(同下面 requiresConfirm 的处理方式)。
|
|
638
|
+
derivedFrom: 'assistant tools minus save_memory, look_at_image and set_todo_status',
|
|
559
639
|
writesFiles: ['export_chat'],
|
|
560
640
|
// 逐个核实过(脚本里确实调云端模型的那些):
|
|
561
641
|
// who_owes_reply/search_chats → Jev;search_semantic → 阿里云百炼嵌入 + Jev 重排;draft_reply → Jev + DeepSeek。
|
|
@@ -700,8 +780,13 @@ program
|
|
|
700
780
|
console.log(chalk.gray(' 目录名未命中时将继续进行深度结构扫描,过程可能需要较长时间...'));
|
|
701
781
|
}
|
|
702
782
|
const detected = configuredDataRoot
|
|
703
|
-
? { success: true, path: configuredDataRoot }
|
|
704
|
-
: await dbPathService.autoDetect({
|
|
783
|
+
? { success: true, path: configuredDataRoot, source: 'known' }
|
|
784
|
+
: await dbPathService.autoDetect({
|
|
785
|
+
searchDrives: opts.searchDrives, fullScan: opts.fullScan,
|
|
786
|
+
// **先问客户端自己**(它命令行上带着 `--wechat-files-path`):这是唯一"来自
|
|
787
|
+
// 客户端自己"的来源,其余都是按目录名猜。取不到就是空数组,走老路。
|
|
788
|
+
hintedRoots: await reportedDataDirs(),
|
|
789
|
+
});
|
|
705
790
|
if (!detected.success || !detected.path) {
|
|
706
791
|
console.log(chalk.red('✗ 未检测到微信数据目录'));
|
|
707
792
|
if (!opts.searchDrives && !opts.fullScan) {
|
|
@@ -721,7 +806,10 @@ program
|
|
|
721
806
|
}
|
|
722
807
|
process.exit(1);
|
|
723
808
|
}
|
|
724
|
-
|
|
809
|
+
// **说清是怎么找到的**:来源不同,可信度与后续排查都不同("客户端自己说的"最硬)
|
|
810
|
+
const howFound = detected.source === 'client' ? '(客户端自己报的)'
|
|
811
|
+
: detected.source === 'scan' ? '(全盘扫描找到的)' : '';
|
|
812
|
+
console.log(chalk.green(`✓ 检测到数据目录: ${detected.path}${howFound}`));
|
|
725
813
|
configService.set('dbPath', detected.path);
|
|
726
814
|
configService.set('dataVersion', '4.x');
|
|
727
815
|
// Step 2: 扫描账号
|
|
@@ -978,8 +1066,10 @@ const configurableKeys = [
|
|
|
978
1066
|
'wereadApiKey',
|
|
979
1067
|
'assistantPrivacy', 'assistantWhitelist', 'assistantGroupWhitelist',
|
|
980
1068
|
'assistantGroupRequireMention', 'assistantFastRoute', 'assistantPanelUser', 'quickReplyContacts',
|
|
1069
|
+
'skillDirs', 'skillDisabled',
|
|
981
1070
|
'dailySources', 'dailySourceCategories',
|
|
982
1071
|
'dailyExcludeTopics', 'dailyAiEnabled',
|
|
1072
|
+
'wechatMediaDownload',
|
|
983
1073
|
'emoticonSeed',
|
|
984
1074
|
];
|
|
985
1075
|
function setConfigValue(key, value, quiet = false) {
|
|
@@ -1290,11 +1380,26 @@ program
|
|
|
1290
1380
|
? (await chatService.getMessagesInRange(talker, limit + offset, start, end)).slice(offset, offset + limit)
|
|
1291
1381
|
: await chatService.getMessages(talker, limit, offset);
|
|
1292
1382
|
if (messages.length === 0) {
|
|
1383
|
+
// 拼错的 ID 会被 resolveTalker 原样放行 ⇒ 取不到消息。这里补一句话把两种情况分开,
|
|
1384
|
+
// 但**不改退出码、也不改 success**("没有消息"不是错误,脚本与 MCP 依赖这一点)。
|
|
1385
|
+
let note = '';
|
|
1386
|
+
if (looksLikeTalkerId(talkerInput)) {
|
|
1387
|
+
const [sessions, contacts] = await Promise.all([
|
|
1388
|
+
chatService.listSessions(talkerInput, 5),
|
|
1389
|
+
chatService.listContacts(talkerInput, 5),
|
|
1390
|
+
]);
|
|
1391
|
+
note = emptyMessagesNote(talkerInput, {
|
|
1392
|
+
sessionUsernames: sessions.map(s => s.username),
|
|
1393
|
+
contactUsernames: contacts.map(c => c.username),
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1293
1396
|
if (opts.json) {
|
|
1294
|
-
console.log(JSON.stringify({ success: true, talker, messages: [] }));
|
|
1397
|
+
console.log(JSON.stringify({ success: true, talker, messages: [], ...(note ? { note } : {}) }));
|
|
1295
1398
|
return;
|
|
1296
1399
|
}
|
|
1297
1400
|
console.log(chalk.gray('未找到消息'));
|
|
1401
|
+
if (note)
|
|
1402
|
+
console.log(chalk.yellow(note));
|
|
1298
1403
|
return;
|
|
1299
1404
|
}
|
|
1300
1405
|
if (opts.json) {
|
|
@@ -1640,6 +1745,53 @@ syncCmd
|
|
|
1640
1745
|
process.exit(1);
|
|
1641
1746
|
});
|
|
1642
1747
|
// ==================== contacts ====================
|
|
1748
|
+
// contact-schema:读 contact.db 里**群**的 ext_buffer(本地只读:不联网、不调模型)
|
|
1749
|
+
//
|
|
1750
|
+
// 只解**实测验证过**的那几个字段(成员 userName/displayName/status/inviter + `#5`);
|
|
1751
|
+
// `#5` 曾经叫"附加参与者 id",实测是**成员 status 位 11 置位的那批 id**(本机 77/77 集合相等),
|
|
1752
|
+
// 所以输出改用 `statusBit11Ids`、不再说"额外参与者"(见 D-070)。
|
|
1753
|
+
// 顶层 `#3`/`#4` 的语义在本机被三次证伪(不是成员 id / 房间 id / 消息侧 id),所以脚本把它们
|
|
1754
|
+
// 原样放进 `unrecognized`,**不猜名字** —— 拿猜出来的名字写逻辑,就是拿不确定当事实。
|
|
1755
|
+
// 结构笔记见 `docs/CONTACT_DB_SCHEMA.md`,取舍见 D-067 / D-068 / D-070。
|
|
1756
|
+
//
|
|
1757
|
+
// `--contacts`(2026-10-07 加):同一个命令改读**联系人**的 `contact.extra_buffer`,输出这一轮验证的
|
|
1758
|
+
// 三个字段 —— `#5` 国家/地区码(ISO 3166-1 alpha-2)、`#13` = 该行 `biz_info.type`、`#41` = 资料最近更新时间。
|
|
1759
|
+
// `#4`/`#9` 已定性为"账号自身资料文本的汇集",是**内容**不是字段名,**故意不打印**(见 D-088)。
|
|
1760
|
+
program
|
|
1761
|
+
.command('contact-schema')
|
|
1762
|
+
.description('读 contact.db 的 ext_buffer:默认读群(成员 + #5);--contacts 读联系人(#5 地区码 / #13 服务类型 / #41 更新时间)—— 只解已验证字段,未识别的原样标注(本地、只读)')
|
|
1763
|
+
.option('-n, --limit <n>', '最多看几个(群 / 联系人)', '20')
|
|
1764
|
+
.option('--room <which>', '只看某个(群 / 联系人 的 username 或 id)')
|
|
1765
|
+
.option('--contacts', '改读联系人的 extra_buffer,而不是群')
|
|
1766
|
+
.option('--json', '输出机器可读结果')
|
|
1767
|
+
.action(async (opts) => {
|
|
1768
|
+
const { execFile } = await import('child_process');
|
|
1769
|
+
const { promisify } = await import('util');
|
|
1770
|
+
const execFileAsync = promisify(execFile);
|
|
1771
|
+
const script = join(resolvePackageRoot(), 'scripts', 'contact_schema.py');
|
|
1772
|
+
const limit = parseCliInteger(opts.limit, 'limit', 1, 100000, !!opts.json);
|
|
1773
|
+
const args = [script, '--limit', String(limit),
|
|
1774
|
+
...(opts.room ? ['--room', opts.room] : []),
|
|
1775
|
+
...(opts.contacts ? ['--contacts'] : []),
|
|
1776
|
+
...(opts.json ? ['--json'] : [])];
|
|
1777
|
+
try {
|
|
1778
|
+
const { stdout, stderr } = await execFileAsync(getPythonCommand(), args, {
|
|
1779
|
+
timeout: 300_000, maxBuffer: 64 * 1024 * 1024, env: pythonProcessEnv(),
|
|
1780
|
+
});
|
|
1781
|
+
process.stdout.write(stdout);
|
|
1782
|
+
if (stderr && !opts.json)
|
|
1783
|
+
process.stderr.write(stderr);
|
|
1784
|
+
}
|
|
1785
|
+
catch (error) {
|
|
1786
|
+
if (opts.json) {
|
|
1787
|
+
console.log(JSON.stringify({ success: false, code: 'CONTACT_SCHEMA_FAILED', action: 'contact-schema', error: safeSubprocessError(error) }));
|
|
1788
|
+
}
|
|
1789
|
+
else {
|
|
1790
|
+
console.error(chalk.red(`\n✗ ${safeSubprocessError(error)}`));
|
|
1791
|
+
}
|
|
1792
|
+
process.exit(1);
|
|
1793
|
+
}
|
|
1794
|
+
});
|
|
1643
1795
|
program
|
|
1644
1796
|
.command('contacts')
|
|
1645
1797
|
.description('查看联系人列表')
|
|
@@ -2006,7 +2158,10 @@ program
|
|
|
2006
2158
|
}
|
|
2007
2159
|
}
|
|
2008
2160
|
else {
|
|
2009
|
-
|
|
2161
|
+
// `opts.json` 也要传:这一行在 `--json` 下**不可达**(上面就 `INTERACTIVE_REQUIRED` 退出了),
|
|
2162
|
+
// 但少传一个是安静的 —— 规则统一成"校验 helper 一律带上它",判据才写得出来
|
|
2163
|
+
// (`test/json-error-surface.test.ts` 末尾那条静态检查)。
|
|
2164
|
+
const timeout = parseCliInteger(opts.timeout, 'timeout', 1000, 600000, opts.json);
|
|
2010
2165
|
const result = await keyService.autoGetDbKey(timeout, (msg) => {
|
|
2011
2166
|
console.log(chalk.gray(` ${msg}`));
|
|
2012
2167
|
});
|
|
@@ -2229,7 +2384,7 @@ whitelistCmd
|
|
|
2229
2384
|
}
|
|
2230
2385
|
else {
|
|
2231
2386
|
try {
|
|
2232
|
-
wxid = await resolveTalker(target);
|
|
2387
|
+
wxid = await resolveTalker(target, !!opts.json, !!opts.json);
|
|
2233
2388
|
const sessions = await chatService.listSessions(undefined, 50);
|
|
2234
2389
|
const match = sessions.find(s => s.username === wxid);
|
|
2235
2390
|
if (match)
|
|
@@ -2438,7 +2593,7 @@ blacklistCmd
|
|
|
2438
2593
|
}
|
|
2439
2594
|
else {
|
|
2440
2595
|
try {
|
|
2441
|
-
wxid = await resolveTalker(target);
|
|
2596
|
+
wxid = await resolveTalker(target, !!opts.json, !!opts.json);
|
|
2442
2597
|
const sessions = await chatService.listSessions(undefined, 50);
|
|
2443
2598
|
const match = sessions.find(s => s.username === wxid);
|
|
2444
2599
|
if (match)
|
|
@@ -3379,57 +3534,50 @@ program
|
|
|
3379
3534
|
});
|
|
3380
3535
|
process.exit(1);
|
|
3381
3536
|
}
|
|
3382
|
-
//
|
|
3383
|
-
|
|
3537
|
+
// 三道闸(黑名单 → 白名单 → 限速)与"每次尝试都写审计"在 `src/services/outboundSend.ts`
|
|
3538
|
+
// 里**只有一份**,因为 2026-10-01 起有了第二条发送路(`draft --pick N --send`,见 D-063)。
|
|
3539
|
+
// 两份写同一件事,分叉的后果具体是"某一条路绕过了白名单"。
|
|
3540
|
+
const policy = parseOutboundPolicy(Number(opts.rateWindow), Number(opts.rateMax));
|
|
3541
|
+
if ('code' in policy) {
|
|
3384
3542
|
if (opts.json)
|
|
3385
|
-
console.log(JSON.stringify({ success: false, code:
|
|
3386
|
-
else
|
|
3543
|
+
console.log(JSON.stringify({ success: false, code: policy.code, error: policy.error }));
|
|
3544
|
+
else
|
|
3545
|
+
console.log(chalk.red('速率参数无效:rate-window 必须为 1000-3600000,rate-max 必须为 1-100'));
|
|
3546
|
+
process.exit(1);
|
|
3547
|
+
}
|
|
3548
|
+
const attempt = {
|
|
3549
|
+
wxid, displayName, kind, preview, text: message,
|
|
3550
|
+
mediaPath: kind === 'text' ? undefined : String(opts.image || opts.file),
|
|
3551
|
+
};
|
|
3552
|
+
const decision = authorizeOutbound(attempt, policy);
|
|
3553
|
+
if (!decision.ok) {
|
|
3554
|
+
const { refusal } = decision;
|
|
3555
|
+
if (opts.json) {
|
|
3556
|
+
console.log(JSON.stringify({
|
|
3557
|
+
success: false, code: refusal.code, error: refusal.error,
|
|
3558
|
+
...(refusal.code === 'TARGET_BLOCKED' || refusal.code === 'TARGET_NOT_ALLOWED'
|
|
3559
|
+
? { target: { wxid, displayName } } : {}),
|
|
3560
|
+
...(refusal.rateLimit ? { rateLimit: refusal.rateLimit } : {}),
|
|
3561
|
+
}));
|
|
3562
|
+
}
|
|
3563
|
+
else if (refusal.code === 'TARGET_BLOCKED') {
|
|
3387
3564
|
console.log(chalk.red('\n❌ 目标在黑名单中,绝对禁止发送'));
|
|
3388
3565
|
console.log(chalk.gray(` wxid: ${wxid}`));
|
|
3389
3566
|
console.log(chalk.gray(` 解除: weflow-cli blacklist rm ${wxid}\n`));
|
|
3390
3567
|
}
|
|
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 {
|
|
3568
|
+
else if (refusal.code === 'TARGET_NOT_ALLOWED') {
|
|
3402
3569
|
console.log(chalk.red('\n❌ 目标不在白名单中,拒绝发送'));
|
|
3403
3570
|
console.log(chalk.gray(` 先运行: weflow-cli whitelist add ${target}`));
|
|
3404
3571
|
console.log(chalk.gray(` 查看名单: weflow-cli whitelist\n`));
|
|
3405
3572
|
}
|
|
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
3573
|
else {
|
|
3574
|
+
const rate = refusal.rateLimit;
|
|
3424
3575
|
console.log(chalk.red(`\n❌ 触发速率限制: 最近 ${rate.windowMs / 1000}s 内已发送 ${rate.count} 条 (上限 ${rate.max})`));
|
|
3425
3576
|
console.log(chalk.gray(' 请稍后再试, 或调整 --rate-window / --rate-max\n'));
|
|
3426
3577
|
}
|
|
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
3578
|
process.exit(1);
|
|
3432
3579
|
}
|
|
3580
|
+
const rate = decision.rate;
|
|
3433
3581
|
// 二次确认 — 同时显示 wxid + displayName + 消息预览
|
|
3434
3582
|
const actionPreview = {
|
|
3435
3583
|
action: 'send',
|
|
@@ -3480,41 +3628,8 @@ program
|
|
|
3480
3628
|
}
|
|
3481
3629
|
process.exit(1);
|
|
3482
3630
|
}
|
|
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
|
-
});
|
|
3631
|
+
// 真的发 + 按结果写审计,都在共享模块里(与 `draft --send` 同一份)
|
|
3632
|
+
const { success, error: errorMsg } = await performOutbound(attempt);
|
|
3518
3633
|
if (success) {
|
|
3519
3634
|
if (opts.json)
|
|
3520
3635
|
console.log(JSON.stringify({ success: true, ...actionPreview }));
|
|
@@ -3939,6 +4054,7 @@ program
|
|
|
3939
4054
|
.option('-o, --output <dir>', '概念页输出目录', './output/wechat-vault/Wiki/Concepts')
|
|
3940
4055
|
.option('--api-key <key>', 'DeepSeek API key')
|
|
3941
4056
|
.option('--refresh-sources', '只把已有概念页的"来源"段补齐成当前全部来源(本地重写,**不调用模型**)')
|
|
4057
|
+
.option('--qualify-links', '把跨线同名的概念链接改成带路径的形式(本地重写,**不调用模型**)')
|
|
3942
4058
|
.option('--cards <dir...>', '产卡目录,可写多个;默认所有 output/*-notes(与索引的引用数同一口径)')
|
|
3943
4059
|
.option('--dry-run', '仅预览,不读取文章、调用 AI 或写入概念页')
|
|
3944
4060
|
.option('--yes', '确认调用 AI 并生成概念页')
|
|
@@ -3962,6 +4078,22 @@ program
|
|
|
3962
4078
|
});
|
|
3963
4079
|
return;
|
|
3964
4080
|
}
|
|
4081
|
+
if (opts.qualifyLinks) {
|
|
4082
|
+
// 本地消歧:两条线同名的那些链接按名字带路径(`Chat/Concepts/DeepSeek|DeepSeek`),
|
|
4083
|
+
// 免得 Obsidian 里那条裸链接命中另一条线的那张。**不调模型**,重跑幂等。
|
|
4084
|
+
await runConfirmedPythonMutation({
|
|
4085
|
+
action: 'wiki.qualifyLinks',
|
|
4086
|
+
script,
|
|
4087
|
+
args: ['--qualify-links', '--output', opts.output],
|
|
4088
|
+
cliOptions: opts,
|
|
4089
|
+
preview: { readsLocalKnowledge: true, rewritesExistingPages: true, usesAi: false },
|
|
4090
|
+
confirmationMessage: `确认把 ${opts.output} 里跨线同名的概念链接改成带路径的形式?`
|
|
4091
|
+
+ '(本地重写,不调用 AI)',
|
|
4092
|
+
apiKey: opts.apiKey,
|
|
4093
|
+
timeout: 300_000,
|
|
4094
|
+
});
|
|
4095
|
+
return;
|
|
4096
|
+
}
|
|
3965
4097
|
const limit = parseCliInteger(opts.limit, 'limit', 1, 1000, opts.json);
|
|
3966
4098
|
const minRefs = parseCliInteger(opts.minRefs ?? '1', 'min-refs', 1, 50, opts.json);
|
|
3967
4099
|
await runConfirmedPythonMutation({
|
|
@@ -3989,8 +4121,9 @@ program
|
|
|
3989
4121
|
.option('--wiki-limit <n>', '概念编译数', '20')
|
|
3990
4122
|
.option('--source <name>', '仅处理指定公众号,可重复使用', (value, previous) => [...previous, value], [])
|
|
3991
4123
|
.option('--skip-classify', '跳过 AI 后处理')
|
|
3992
|
-
|
|
3993
|
-
.option('--
|
|
4124
|
+
// 这两个**默认不做**:Obsidian 自带的图谱视图是实时的,写一次 vault 图谱就重画一次。
|
|
4125
|
+
.option('--with-wiki', '把概念页写进 Obsidian Vault(默认不写:图谱只在你说了才变)')
|
|
4126
|
+
.option('--with-vault', '把当天内容副本同步进 Obsidian Vault(默认不写)')
|
|
3994
4127
|
.option('--skip-html', '跳过 HTML 阅读器生成')
|
|
3995
4128
|
.option('--skip-ai-report', '跳过 AI 深度阅读报告')
|
|
3996
4129
|
.option('--no-ai', '关闭全部 AI 调用,保留抓取和本地输出')
|
|
@@ -4082,10 +4215,10 @@ program
|
|
|
4082
4215
|
args.push('--source', source);
|
|
4083
4216
|
if (opts.skipClassify)
|
|
4084
4217
|
args.push('--skip-classify');
|
|
4085
|
-
if (opts.
|
|
4086
|
-
args.push('--
|
|
4087
|
-
if (opts.
|
|
4088
|
-
args.push('--
|
|
4218
|
+
if (opts.withWiki)
|
|
4219
|
+
args.push('--with-wiki');
|
|
4220
|
+
if (opts.withVault)
|
|
4221
|
+
args.push('--with-vault');
|
|
4089
4222
|
if (opts.skipHtml)
|
|
4090
4223
|
args.push('--skip-html');
|
|
4091
4224
|
if (opts.skipAiReport)
|
|
@@ -4327,16 +4460,30 @@ program.commands
|
|
|
4327
4460
|
.description('Vault 搜索 — 文章+概念+笔记')
|
|
4328
4461
|
.argument('<query>', '搜索关键词')
|
|
4329
4462
|
.option('--type <type>', 'all / article / concept / note', 'all')
|
|
4463
|
+
.option('--line <line>', 'wiki=文章线(默认) / chat=聊天线 / all=两条都搜', 'wiki')
|
|
4330
4464
|
.option('--top-k <n>', '返回数量', '10')
|
|
4465
|
+
// `vault_search.py` 一直有 `--days`(它的 docstring 自己就举 `--days 30`),CLI 从来没接 ——
|
|
4466
|
+
// 于是"只搜最近一周"这件事在这条路上做不到,窗口被脚本默认值(90)钉死。与 `search-index --days`
|
|
4467
|
+
// 同形:窗口的运行值由 CLI 说了算,默认值与脚本一致。
|
|
4468
|
+
.option('--days <n>', '日报文章取最近多少天(默认 90)', '90')
|
|
4469
|
+
// 这条 `--json` 曾经**不存在,而 JSON 又是唯一的输出**:参数里硬编码着 `'--json'`,
|
|
4470
|
+
// 人类模式虽然脚本里写着(`vault_search.py` 不带 `--json` 时打印带相关度的可读列表)
|
|
4471
|
+
// 却没有任何入口能到达,用户显式敲 `--json` 还会拿到 `unknown option` —— 与
|
|
4472
|
+
// `docs/EXTENDING.md` 那条"脚本或 AI 会驱动的命令都有 `--json` 模式"正相反。
|
|
4473
|
+
// 现在按兄弟命令(`vault rag`、顶层 `search`)的形状来:默认人类可读,`--json` 才转发。
|
|
4474
|
+
.option('--json', '输出 JSON 格式(含本地路径与标题,供本机脚本/AI 使用)')
|
|
4331
4475
|
.action(async (query, opts) => {
|
|
4332
4476
|
const pkgRoot = resolvePackageRoot();
|
|
4333
4477
|
const script = join(pkgRoot, 'scripts', 'vault_search.py');
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
const topK = parseCliInteger(opts.topK, 'top-k', 1, 100);
|
|
4339
|
-
|
|
4478
|
+
// 校验分支也要认 `--json`(`requireCliChoice` 里就带着这条约定):
|
|
4479
|
+
// 否则 JSON 模式下 stdout 是一行彩色散文,脚本解析不了。
|
|
4480
|
+
requireCliChoice(opts.type, ['all', 'article', 'concept', 'note'], 'type', opts.json);
|
|
4481
|
+
requireCliChoice(opts.line, ['wiki', 'chat', 'all'], 'line', opts.json);
|
|
4482
|
+
const topK = parseCliInteger(opts.topK, 'top-k', 1, 100, opts.json);
|
|
4483
|
+
const days = parseCliInteger(opts.days, 'days', 1, 3650, opts.json);
|
|
4484
|
+
await runPythonCmd(script, [query, '--type', opts.type, '--line', opts.line,
|
|
4485
|
+
'--top-k', String(topK), '--days', String(days),
|
|
4486
|
+
...(opts.json ? ['--json'] : [])]);
|
|
4340
4487
|
}));
|
|
4341
4488
|
// vault rag — RAG dialog over Vault
|
|
4342
4489
|
program.commands
|
|
@@ -4345,6 +4492,7 @@ program.commands
|
|
|
4345
4492
|
.description('Vault 问答 — 基于知识库的 AI 对话')
|
|
4346
4493
|
.argument('<question>', '问题')
|
|
4347
4494
|
.option('--top-k <n>', '检索条数', '8')
|
|
4495
|
+
.option('--line <line>', 'wiki=文章线(默认) / chat=聊天线 / all=两条都搜', 'wiki')
|
|
4348
4496
|
.option('--api-key <key>', 'DeepSeek API key')
|
|
4349
4497
|
.option('--dry-run', '仅预览,不读取知识库或调用 AI')
|
|
4350
4498
|
.option('--yes', '确认读取本地知识并发送筛选后的上下文到 AI')
|
|
@@ -4353,11 +4501,13 @@ program.commands
|
|
|
4353
4501
|
const pkgRoot = resolvePackageRoot();
|
|
4354
4502
|
const script = join(pkgRoot, 'scripts', 'vault_rag.py');
|
|
4355
4503
|
const topK = parseCliInteger(opts.topK, 'top-k', 1, 100, opts.json);
|
|
4504
|
+
requireCliChoice(opts.line, ['wiki', 'chat', 'all'], 'line', opts.json);
|
|
4356
4505
|
const preview = {
|
|
4357
4506
|
success: true,
|
|
4358
4507
|
dryRun: true,
|
|
4359
4508
|
action: 'vault.rag',
|
|
4360
4509
|
topK,
|
|
4510
|
+
line: opts.line,
|
|
4361
4511
|
readsLocalKnowledge: true,
|
|
4362
4512
|
usesAi: true,
|
|
4363
4513
|
sendsSelectedContextToAi: true,
|
|
@@ -4385,7 +4535,7 @@ program.commands
|
|
|
4385
4535
|
return;
|
|
4386
4536
|
}
|
|
4387
4537
|
}
|
|
4388
|
-
const args = ['--top-k', String(topK)];
|
|
4538
|
+
const args = ['--top-k', String(topK), '--line', opts.line];
|
|
4389
4539
|
if (opts.json)
|
|
4390
4540
|
args.push('--json');
|
|
4391
4541
|
const { execFile } = await import('child_process');
|
|
@@ -4694,6 +4844,7 @@ program
|
|
|
4694
4844
|
await openLocalUrl(`http://localhost:${port}`);
|
|
4695
4845
|
}
|
|
4696
4846
|
const child = spawn(getPythonCommand(), args, {
|
|
4847
|
+
windowsHide: true,
|
|
4697
4848
|
stdio: 'inherit',
|
|
4698
4849
|
env: pythonProcessEnv(),
|
|
4699
4850
|
});
|
|
@@ -4704,6 +4855,26 @@ program
|
|
|
4704
4855
|
}));
|
|
4705
4856
|
});
|
|
4706
4857
|
// ==================== weread ====================
|
|
4858
|
+
/** 微信读书服务失败时的统一出口。
|
|
4859
|
+
*
|
|
4860
|
+
* 9 个调用点(shelf / stats / notes×2 / search / book / review / discover / profile)原来都是一行式
|
|
4861
|
+
* 分支:把彩色散文打在 stdout 上再 `process.exit(1)`,**没有** json 支,而各命令的 `if (opts.json)`
|
|
4862
|
+
* 分支在它后面 ⇒ `weread shelf --json` 失败时 stdout 上只有一行 `✗ …`,调用方解析不了,也不知道
|
|
4863
|
+
* 是哪个命令失败了。全库其余同类出口都是"机器支发 JSON + exit"。
|
|
4864
|
+
*
|
|
4865
|
+
* (这里刻意**不**把那行旧代码原样抄出来:`test/json-error-surface.test.ts` 有一条绊线在扫那个形状,
|
|
4866
|
+
* 而注释里留一份字面量会让判据读到它 —— 今晚已经在另一条判据上踩过这个坑。)
|
|
4867
|
+
*
|
|
4868
|
+
* 人看的那一支保持原样(`console.log`,不动它的落点):把 CLI 的**错误**统一改走 stderr 是另一件事,
|
|
4869
|
+
* 现在库里两种都有(多数 `console.error`、少数 `console.log`),不在这条修复里顺手改。
|
|
4870
|
+
*/
|
|
4871
|
+
function failWeread(res, json = false) {
|
|
4872
|
+
if (json)
|
|
4873
|
+
console.log(JSON.stringify({ success: false, code: 'WEREAD_FAILED', error: res.error }));
|
|
4874
|
+
else
|
|
4875
|
+
console.log(chalk.red(`✗ ${res.error}`));
|
|
4876
|
+
process.exit(1);
|
|
4877
|
+
}
|
|
4707
4878
|
const wereadCmd = program
|
|
4708
4879
|
.command('weread')
|
|
4709
4880
|
.description('微信读书助手 — 书架、统计、笔记、搜索、书评、推荐');
|
|
@@ -4714,10 +4885,8 @@ wereadCmd
|
|
|
4714
4885
|
.action(async (opts) => {
|
|
4715
4886
|
const weread = await getWereadService();
|
|
4716
4887
|
const res = await weread.shelf();
|
|
4717
|
-
if (!res.ok)
|
|
4718
|
-
|
|
4719
|
-
process.exit(1);
|
|
4720
|
-
}
|
|
4888
|
+
if (!res.ok)
|
|
4889
|
+
failWeread(res, !!opts.json);
|
|
4721
4890
|
const d = res.data;
|
|
4722
4891
|
if (opts.json) {
|
|
4723
4892
|
console.log(JSON.stringify(d, null, 2));
|
|
@@ -4748,10 +4917,8 @@ wereadCmd
|
|
|
4748
4917
|
.action(async (opts) => {
|
|
4749
4918
|
const weread = await getWereadService();
|
|
4750
4919
|
const res = await weread.readData(opts.mode);
|
|
4751
|
-
if (!res.ok)
|
|
4752
|
-
|
|
4753
|
-
process.exit(1);
|
|
4754
|
-
}
|
|
4920
|
+
if (!res.ok)
|
|
4921
|
+
failWeread(res, !!opts.json);
|
|
4755
4922
|
const d = res.data;
|
|
4756
4923
|
if (opts.json) {
|
|
4757
4924
|
console.log(JSON.stringify(d, null, 2));
|
|
@@ -4800,10 +4967,8 @@ wereadCmd
|
|
|
4800
4967
|
const limit = parseCliInteger(opts.limit, 'limit', 1, 100, opts.json);
|
|
4801
4968
|
if (opts.bookId) {
|
|
4802
4969
|
const res = await weread.bookmarks(opts.bookId, limit);
|
|
4803
|
-
if (!res.ok)
|
|
4804
|
-
|
|
4805
|
-
process.exit(1);
|
|
4806
|
-
}
|
|
4970
|
+
if (!res.ok)
|
|
4971
|
+
failWeread(res, !!opts.json);
|
|
4807
4972
|
const d = res.data;
|
|
4808
4973
|
if (opts.json) {
|
|
4809
4974
|
console.log(JSON.stringify(d, null, 2));
|
|
@@ -4821,10 +4986,8 @@ wereadCmd
|
|
|
4821
4986
|
}
|
|
4822
4987
|
else {
|
|
4823
4988
|
const res = await weread.notebooks(50);
|
|
4824
|
-
if (!res.ok)
|
|
4825
|
-
|
|
4826
|
-
process.exit(1);
|
|
4827
|
-
}
|
|
4989
|
+
if (!res.ok)
|
|
4990
|
+
failWeread(res, !!opts.json);
|
|
4828
4991
|
const d = res.data;
|
|
4829
4992
|
if (opts.json) {
|
|
4830
4993
|
console.log(JSON.stringify(d, null, 2));
|
|
@@ -4849,10 +5012,8 @@ wereadCmd
|
|
|
4849
5012
|
const weread = await getWereadService();
|
|
4850
5013
|
const limit = parseCliInteger(opts.limit, 'limit', 1, 100, opts.json);
|
|
4851
5014
|
const res = await weread.search(keyword, limit);
|
|
4852
|
-
if (!res.ok)
|
|
4853
|
-
|
|
4854
|
-
process.exit(1);
|
|
4855
|
-
}
|
|
5015
|
+
if (!res.ok)
|
|
5016
|
+
failWeread(res, !!opts.json);
|
|
4856
5017
|
const d = res.data;
|
|
4857
5018
|
if (opts.json) {
|
|
4858
5019
|
console.log(JSON.stringify(d, null, 2));
|
|
@@ -4879,10 +5040,8 @@ wereadCmd
|
|
|
4879
5040
|
weread.bookInfo(bookId),
|
|
4880
5041
|
weread.getProgress(bookId),
|
|
4881
5042
|
]);
|
|
4882
|
-
if (!infoRes.ok)
|
|
4883
|
-
|
|
4884
|
-
process.exit(1);
|
|
4885
|
-
}
|
|
5043
|
+
if (!infoRes.ok)
|
|
5044
|
+
failWeread(infoRes, !!opts.json);
|
|
4886
5045
|
const b = infoRes.data;
|
|
4887
5046
|
if (opts.json) {
|
|
4888
5047
|
console.log(JSON.stringify(b, null, 2));
|
|
@@ -4915,10 +5074,8 @@ wereadCmd
|
|
|
4915
5074
|
const weread = await getWereadService();
|
|
4916
5075
|
const limit = parseCliInteger(opts.limit, 'limit', 1, 100, opts.json);
|
|
4917
5076
|
const res = await weread.reviews(bookId, limit);
|
|
4918
|
-
if (!res.ok)
|
|
4919
|
-
|
|
4920
|
-
process.exit(1);
|
|
4921
|
-
}
|
|
5077
|
+
if (!res.ok)
|
|
5078
|
+
failWeread(res, !!opts.json);
|
|
4922
5079
|
const d = res.data;
|
|
4923
5080
|
if (opts.json) {
|
|
4924
5081
|
console.log(JSON.stringify(d, null, 2));
|
|
@@ -4947,10 +5104,8 @@ wereadCmd
|
|
|
4947
5104
|
const res = opts.bookId
|
|
4948
5105
|
? await weread.similar(opts.bookId, limit)
|
|
4949
5106
|
: await weread.recommend(limit);
|
|
4950
|
-
if (!res.ok)
|
|
4951
|
-
|
|
4952
|
-
process.exit(1);
|
|
4953
|
-
}
|
|
5107
|
+
if (!res.ok)
|
|
5108
|
+
failWeread(res, !!opts.json);
|
|
4954
5109
|
const d = res.data;
|
|
4955
5110
|
if (opts.json) {
|
|
4956
5111
|
console.log(JSON.stringify(d, null, 2));
|
|
@@ -4974,10 +5129,8 @@ wereadCmd
|
|
|
4974
5129
|
.action(async (opts) => {
|
|
4975
5130
|
const weread = await getWereadService();
|
|
4976
5131
|
const res = await weread.profile();
|
|
4977
|
-
if (!res.ok)
|
|
4978
|
-
|
|
4979
|
-
process.exit(1);
|
|
4980
|
-
}
|
|
5132
|
+
if (!res.ok)
|
|
5133
|
+
failWeread(res, !!opts.json);
|
|
4981
5134
|
const p = res.data;
|
|
4982
5135
|
if (opts.json) {
|
|
4983
5136
|
console.log(JSON.stringify(p, null, 2));
|
|
@@ -5020,6 +5173,8 @@ program
|
|
|
5020
5173
|
.description('语义搜索知识库(聊天记录 + 文章)')
|
|
5021
5174
|
.argument('<query>', '搜索关键词')
|
|
5022
5175
|
.option('--top-k <n>', '返回数量', '10')
|
|
5176
|
+
.option('--line <line>', 'wiki=文章线(默认) / chat=聊天记录 / all=两条都搜', 'wiki')
|
|
5177
|
+
.option('--no-rerank', '不调用决策模型重排,只按向量/关键词相似度返回(回退到引入重排之前)')
|
|
5023
5178
|
.option('--api-key <key>', 'DashScope embedding API key')
|
|
5024
5179
|
.option('--dry-run', '仅预览,不读取索引或调用向量服务')
|
|
5025
5180
|
.option('--yes', '确认执行可能调用云端向量服务的搜索')
|
|
@@ -5031,11 +5186,14 @@ program
|
|
|
5031
5186
|
const pkgRoot = resolvePackageRoot();
|
|
5032
5187
|
const script = join(pkgRoot, 'scripts', 'semantic_search.py');
|
|
5033
5188
|
const topK = parseCliInteger(opts.topK, 'top-k', 1, 100, opts.json);
|
|
5189
|
+
requireCliChoice(opts.line, ['wiki', 'chat', 'all'], 'line', opts.json);
|
|
5034
5190
|
const preview = {
|
|
5035
5191
|
success: true,
|
|
5036
5192
|
dryRun: true,
|
|
5037
5193
|
action: 'semantic-search.query',
|
|
5038
5194
|
topK,
|
|
5195
|
+
line: opts.line,
|
|
5196
|
+
noRerank: opts.rerank === false,
|
|
5039
5197
|
readsLocalIndex: true,
|
|
5040
5198
|
mayUseCloudEmbedding: true,
|
|
5041
5199
|
};
|
|
@@ -5062,7 +5220,11 @@ program
|
|
|
5062
5220
|
return;
|
|
5063
5221
|
}
|
|
5064
5222
|
}
|
|
5065
|
-
|
|
5223
|
+
// `--no-rerank` 是**旗标**(不是密钥)⇒ 可以走 argv;查询本身仍然只走 env(见 `privacy-paths` 那条规矩)。
|
|
5224
|
+
// 接这条通道之前,`OPERATIONS.md` 一直在教用户敲 `search --no-rerank`,而 CLI 会答 `unknown option`
|
|
5225
|
+
// —— 同一份文档里 `--line` 是通的,所以它看起来像"能用的旗标"(2026-10-11 由文档命令面那道检查发现)。
|
|
5226
|
+
const args = [script, 'search', '--top-k', String(topK), '--line', opts.line,
|
|
5227
|
+
...(opts.rerank === false ? ['--no-rerank'] : [])];
|
|
5066
5228
|
try {
|
|
5067
5229
|
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
5068
5230
|
timeout: 60_000, maxBuffer: 10 * 1024 * 1024,
|
|
@@ -5165,6 +5327,7 @@ program
|
|
|
5165
5327
|
.description('RAG 智能助手 — 基于聊天记录和公众号的对话式问答')
|
|
5166
5328
|
.argument('[question]', '要问的问题(不传则进入交互模式)')
|
|
5167
5329
|
.option('--top-k <n>', '检索数量', '10')
|
|
5330
|
+
.option('--line <line>', 'wiki=文章/文章概念(默认) / chat=聊天记录 / all=两条都搜', 'wiki')
|
|
5168
5331
|
.option('--talker <name>', '限定联系人/群聊')
|
|
5169
5332
|
.option('--api-key <key>', 'AI API key')
|
|
5170
5333
|
.option('--dry-run', '仅预览,不读取知识库或调用 AI')
|
|
@@ -5177,11 +5340,13 @@ program
|
|
|
5177
5340
|
const pkgRoot = resolvePackageRoot();
|
|
5178
5341
|
const script = join(pkgRoot, 'scripts', 'rag_chat.py');
|
|
5179
5342
|
const topK = parseCliInteger(opts.topK, 'top-k', 1, 100, opts.json);
|
|
5343
|
+
requireCliChoice(opts.line, ['wiki', 'chat', 'all'], 'line', opts.json);
|
|
5180
5344
|
const preview = {
|
|
5181
5345
|
success: true,
|
|
5182
5346
|
dryRun: true,
|
|
5183
5347
|
action: 'rag-chat.query',
|
|
5184
5348
|
topK,
|
|
5349
|
+
line: opts.line,
|
|
5185
5350
|
interactiveRequired: !question,
|
|
5186
5351
|
readsLocalKnowledge: true,
|
|
5187
5352
|
usesCloudEmbedding: true,
|
|
@@ -5218,7 +5383,7 @@ program
|
|
|
5218
5383
|
}
|
|
5219
5384
|
const args = [script];
|
|
5220
5385
|
if (question) {
|
|
5221
|
-
args.push('--top-k', String(topK));
|
|
5386
|
+
args.push('--top-k', String(topK), '--line', opts.line);
|
|
5222
5387
|
if (opts.json)
|
|
5223
5388
|
args.push('--json');
|
|
5224
5389
|
try {
|
|
@@ -5239,8 +5404,9 @@ program
|
|
|
5239
5404
|
}
|
|
5240
5405
|
else {
|
|
5241
5406
|
// 交互模式:使用 spawn 保持终端交互
|
|
5242
|
-
args.push('--interactive', '--top-k', String(topK));
|
|
5407
|
+
args.push('--interactive', '--top-k', String(topK), '--line', opts.line);
|
|
5243
5408
|
const child = spawn(getPythonCommand(), args, {
|
|
5409
|
+
windowsHide: true,
|
|
5244
5410
|
stdio: 'inherit',
|
|
5245
5411
|
env: {
|
|
5246
5412
|
...pythonProcessEnv(opts.apiKey),
|
|
@@ -5512,8 +5678,12 @@ program
|
|
|
5512
5678
|
// draft:判断在前、起草在后、**绝不发送**
|
|
5513
5679
|
program
|
|
5514
5680
|
.command('draft <talker>')
|
|
5515
|
-
.description('帮我起草回复:先判断(意图/风险/该不该给实质),再起草候选 ——
|
|
5681
|
+
.description('帮我起草回复:先判断(意图/风险/该不该给实质),再起草候选 —— **默认只产出文本;给了 --pick N --send 才会替你发送**')
|
|
5516
5682
|
.option('--count <n>', '要几条候选', '3')
|
|
5683
|
+
.option('--pick <n>', '选第几条(从 1 数);不带 --send 时只把那条的完整原文打出来')
|
|
5684
|
+
.option('--send', '把选中的那条发出去 —— **必须同时给 --pick**(不允许"自己挑一条发"),仍需 --yes/--dry-run')
|
|
5685
|
+
.option('--rate-window <ms>', '发送速率窗口毫秒(与 send 同一份策略)', '60000')
|
|
5686
|
+
.option('--rate-max <n>', '窗口内最大发送条数', '10')
|
|
5517
5687
|
.option('--gate-commitment', '承诺未兑现时也拒绝起草(默认只在提示词里约束)')
|
|
5518
5688
|
.option('--dry-run', '仅预览会发多少字符给哪两个模型,不调用')
|
|
5519
5689
|
.option('--yes', '确认把这段对话发送到判断模型与生成模型')
|
|
@@ -5522,9 +5692,22 @@ program
|
|
|
5522
5692
|
const { execFile } = await import('child_process');
|
|
5523
5693
|
const { promisify } = await import('util');
|
|
5524
5694
|
const execFileAsync = promisify(execFile);
|
|
5695
|
+
// **用法错误先报**,而且都在调模型之前报:`--send` 少了 `--pick` 是"你没说发哪条",
|
|
5696
|
+
// `--pick 9` 而只有 3 条是"那条根本不存在" —— 两者都不该先问一遍"要不要把对话发给云端模型"、
|
|
5697
|
+
// 更不该先花掉模型调用再拒绝。
|
|
5698
|
+
const count = parseCliInteger(opts.count, 'count', 1, 5, opts.json);
|
|
5699
|
+
if (opts.send && opts.pick === undefined) {
|
|
5700
|
+
const message = '--send 必须同时给 --pick <序号>:先看候选,再点名要发哪一条';
|
|
5701
|
+
if (opts.json)
|
|
5702
|
+
console.log(JSON.stringify({ success: false, code: 'PICK_REQUIRED', error: message }));
|
|
5703
|
+
else
|
|
5704
|
+
console.log(chalk.red(`
|
|
5705
|
+
❌ ${message}`));
|
|
5706
|
+
process.exit(1);
|
|
5707
|
+
}
|
|
5708
|
+
const pick = opts.pick === undefined ? 0 : parseCliInteger(opts.pick, 'pick', 1, count, opts.json);
|
|
5525
5709
|
const pkgRoot = resolvePackageRoot();
|
|
5526
5710
|
const script = join(pkgRoot, 'scripts', 'draft_reply.py');
|
|
5527
|
-
const count = parseCliInteger(opts.count, 'count', 1, 5, opts.json);
|
|
5528
5711
|
// **"确认过了"只用一个变量表示**(`--yes` 或交互回答"是"),参数在确认之后才拼。
|
|
5529
5712
|
// 原来的写法是先拼参数、再问,于是**交互确认那条路是坏的**:答了"是",`args` 里
|
|
5530
5713
|
// 却从没补上 `--yes`,脚本照样拒绝(用户看到的是"我确认了它却说没确认")。
|
|
@@ -5552,6 +5735,170 @@ program
|
|
|
5552
5735
|
return;
|
|
5553
5736
|
}
|
|
5554
5737
|
}
|
|
5738
|
+
// ---------------- 选中并(可选)发送 ----------------
|
|
5739
|
+
//
|
|
5740
|
+
// **只有给了 `--pick` 或 `--send` 才走这条分支**;不带它们的调用与以前逐字相同(原样透传脚本输出)。
|
|
5741
|
+
//
|
|
5742
|
+
// 这条分支存在的理由:起草那一半早就有了(判断 → 候选 → 排序),缺的是"人选定之后能真的发出去"。
|
|
5743
|
+
// 而它的**要点是这个 `--pick`**:发送必须由人点名"第几条"。少了它,就成了"模型起草三条、
|
|
5744
|
+
// 模型挑一条、模型发出去" —— 那正是本仓库划掉的那件事
|
|
5745
|
+
// (docs/EXTENDING.md:sending is structurally unreachable from a model-driven path)。
|
|
5746
|
+
if (opts.pick !== undefined || opts.send) {
|
|
5747
|
+
// **`--dry-run` 的语义是"零出境"**,而候选必须由模型产出来 —— 两者不可兼得:不起草就没有
|
|
5748
|
+
// 候选,"预览某一条会发什么"这件事本身不成立。所以这里只报"会发多少字符给哪两个模型"
|
|
5749
|
+
// (脚本的 dry-run 本来就报这个),并把话说清楚,而不是假装预览了一条。
|
|
5750
|
+
if (opts.dryRun) {
|
|
5751
|
+
try {
|
|
5752
|
+
const { stdout } = await execFileAsync(getPythonCommand(), [script, '--talker', String(talker), '--count', String(count), '--dry-run',
|
|
5753
|
+
...(opts.json ? ['--json'] : [])], { timeout: 120_000, maxBuffer: 20 * 1024 * 1024, env: pythonProcessEnv() });
|
|
5754
|
+
process.stdout.write(stdout);
|
|
5755
|
+
}
|
|
5756
|
+
catch (error) {
|
|
5757
|
+
console.error(chalk.red(`\n✗ ${safeSubprocessError(error, '预览失败')}`));
|
|
5758
|
+
process.exit(1);
|
|
5759
|
+
}
|
|
5760
|
+
if (!opts.json) {
|
|
5761
|
+
console.log(chalk.gray('\n候选要由模型产出来,所以 --dry-run 下没有候选可挑。'
|
|
5762
|
+
+ '要预览"发第几条":先起草一次(会调用模型),再用 --pick N --send。\n'));
|
|
5763
|
+
}
|
|
5764
|
+
return;
|
|
5765
|
+
}
|
|
5766
|
+
// `--pick`/`--send` 的用法错误在 action 开头就报过了(那一段全在调模型之前)
|
|
5767
|
+
// 内部一律用 `--json` 拿结构化结果(用户那侧的 `--json` 管的是最终输出,两回事)
|
|
5768
|
+
const pickArgs = [script, '--talker', String(talker), '--count', String(count), '--json',
|
|
5769
|
+
...(confirmed ? ['--yes'] : []),
|
|
5770
|
+
...(opts.gateCommitment ? ['--gate-commitment'] : [])];
|
|
5771
|
+
let drafts = [];
|
|
5772
|
+
try {
|
|
5773
|
+
const { stdout } = await execFileAsync(getPythonCommand(), pickArgs, {
|
|
5774
|
+
timeout: 300_000, maxBuffer: 50 * 1024 * 1024, env: pythonProcessEnv(),
|
|
5775
|
+
});
|
|
5776
|
+
const parsed = JSON.parse(stdout);
|
|
5777
|
+
if (parsed.gate === 'refused') {
|
|
5778
|
+
// **不起草就不发**:那道闸(涉钱/风险高)拒绝时压根没有候选,这里也不能凭空发点什么
|
|
5779
|
+
const message = `这次不起草:${parsed.reason || '判断层拒绝了'}`;
|
|
5780
|
+
if (opts.json)
|
|
5781
|
+
console.log(JSON.stringify({ success: false, code: 'DRAFT_REFUSED', error: message, advice: parsed.advice || [] }));
|
|
5782
|
+
else {
|
|
5783
|
+
console.log(chalk.yellow(`\n${message}`));
|
|
5784
|
+
for (const item of parsed.advice || [])
|
|
5785
|
+
console.log(chalk.gray(` · ${item}`));
|
|
5786
|
+
}
|
|
5787
|
+
process.exit(1);
|
|
5788
|
+
}
|
|
5789
|
+
drafts = parsed.drafts || [];
|
|
5790
|
+
}
|
|
5791
|
+
catch (error) {
|
|
5792
|
+
const message = safeSubprocessError(error, '起草失败');
|
|
5793
|
+
if (opts.json)
|
|
5794
|
+
console.log(JSON.stringify({ success: false, code: 'DRAFT_FAILED', error: message }));
|
|
5795
|
+
else
|
|
5796
|
+
console.error(chalk.red(`\n✗ ${message}`));
|
|
5797
|
+
process.exit(1);
|
|
5798
|
+
}
|
|
5799
|
+
const chosen = drafts[pick - 1];
|
|
5800
|
+
if (!chosen) {
|
|
5801
|
+
const message = `没有第 ${pick} 条候选(这次只有 ${drafts.length} 条)`;
|
|
5802
|
+
if (opts.json)
|
|
5803
|
+
console.log(JSON.stringify({ success: false, code: 'PICK_OUT_OF_RANGE', error: message }));
|
|
5804
|
+
else
|
|
5805
|
+
console.log(chalk.red(`\n❌ ${message}`));
|
|
5806
|
+
process.exit(1);
|
|
5807
|
+
}
|
|
5808
|
+
if (!opts.send) {
|
|
5809
|
+
if (opts.json)
|
|
5810
|
+
console.log(JSON.stringify({ success: true, action: 'draft', sent: false, pick, text: chosen.text }));
|
|
5811
|
+
else
|
|
5812
|
+
console.log(`第 ${pick} 条:\n${chosen.text}\n\n${chalk.gray('(没有发送任何东西;要发加 --send)')}`);
|
|
5813
|
+
return;
|
|
5814
|
+
}
|
|
5815
|
+
// 发送这一半走**与 `send` 命令完全相同的那份策略**(黑名单 → 白名单 → 限速 → 审计)
|
|
5816
|
+
let wxid;
|
|
5817
|
+
try {
|
|
5818
|
+
wxid = await resolveTalker(String(talker), !!opts.json, !!opts.json);
|
|
5819
|
+
}
|
|
5820
|
+
catch (error) {
|
|
5821
|
+
if (opts.json)
|
|
5822
|
+
console.log(JSON.stringify({ success: false, code: 'TALKER_RESOLUTION_FAILED', error: error.message }));
|
|
5823
|
+
else
|
|
5824
|
+
console.log(chalk.red(`\n❌ ${error.message}`));
|
|
5825
|
+
process.exit(1);
|
|
5826
|
+
}
|
|
5827
|
+
let displayName = whitelistService.lookupName(wxid);
|
|
5828
|
+
if (displayName === wxid) {
|
|
5829
|
+
try {
|
|
5830
|
+
const sessions = await chatService.listSessions(undefined, 200);
|
|
5831
|
+
const match = sessions.find(s => s.username === wxid);
|
|
5832
|
+
if (match?.displayName)
|
|
5833
|
+
displayName = match.displayName;
|
|
5834
|
+
}
|
|
5835
|
+
catch { /* 拿不到名字就退回 wxid */ }
|
|
5836
|
+
}
|
|
5837
|
+
const policy = parseOutboundPolicy(Number(opts.rateWindow), Number(opts.rateMax));
|
|
5838
|
+
if ('code' in policy) {
|
|
5839
|
+
if (opts.json)
|
|
5840
|
+
console.log(JSON.stringify({ success: false, code: policy.code, error: policy.error }));
|
|
5841
|
+
else
|
|
5842
|
+
console.log(chalk.red(`\n❌ ${policy.error}`));
|
|
5843
|
+
process.exit(1);
|
|
5844
|
+
}
|
|
5845
|
+
const attempt = {
|
|
5846
|
+
wxid, displayName, kind: 'text',
|
|
5847
|
+
preview: chosen.text.slice(0, 80) + (chosen.text.length > 80 ? '...' : ''),
|
|
5848
|
+
text: chosen.text,
|
|
5849
|
+
};
|
|
5850
|
+
const decision = authorizeOutbound(attempt, policy);
|
|
5851
|
+
if (!decision.ok) {
|
|
5852
|
+
const { refusal } = decision;
|
|
5853
|
+
if (opts.json) {
|
|
5854
|
+
// 这里**还没有** sendPreview(它在闸门通过之后才建),所以逐字段写
|
|
5855
|
+
console.log(JSON.stringify({ success: false, code: refusal.code, error: refusal.error,
|
|
5856
|
+
...(refusal.rateLimit ? { rateLimit: refusal.rateLimit } : {}) }));
|
|
5857
|
+
}
|
|
5858
|
+
else {
|
|
5859
|
+
console.log(chalk.red(`\n❌ ${refusal.error}`));
|
|
5860
|
+
if (refusal.code === 'TARGET_NOT_ALLOWED')
|
|
5861
|
+
console.log(chalk.gray(` 先运行: weflow-cli whitelist add ${talker}\n`));
|
|
5862
|
+
if (refusal.code === 'TARGET_BLOCKED')
|
|
5863
|
+
console.log(chalk.gray(' 这个人在黑名单里,任何一条命令行都不会发给他\n'));
|
|
5864
|
+
if (refusal.rateLimit)
|
|
5865
|
+
console.log(chalk.gray(` 最近 ${refusal.rateLimit.windowMs / 1000}s 内已发 ${refusal.rateLimit.count} 条\n`));
|
|
5866
|
+
}
|
|
5867
|
+
process.exit(1);
|
|
5868
|
+
}
|
|
5869
|
+
const sendPreview = { success: true, action: 'draft-send', target: { wxid, displayName },
|
|
5870
|
+
pick, text: chosen.text, rateLimit: decision.rate };
|
|
5871
|
+
// 两段式在这里:不给 `--yes` 时,JSON 模式回 `CONFIRMATION_REQUIRED`(**带着要发的原文**),
|
|
5872
|
+
// 交互模式则问一句"确定把第 N 条发给 X?"。`--dry-run` 那条路在上面的分支入口就返回了
|
|
5873
|
+
// (它意味着零出境,与"必须由模型产出候选"不可兼得),所以这里没有第三个预览分支。
|
|
5874
|
+
if (opts.json && !opts.yes) {
|
|
5875
|
+
console.log(JSON.stringify({ ...sendPreview, success: false, code: 'CONFIRMATION_REQUIRED', error: '使用 --yes 确认发送' }));
|
|
5876
|
+
process.exit(1);
|
|
5877
|
+
}
|
|
5878
|
+
if (!opts.yes) {
|
|
5879
|
+
const answer = await inquirer.prompt([{
|
|
5880
|
+
type: 'confirm', name: 'send', default: false,
|
|
5881
|
+
message: `确定把第 ${pick} 条发给 ${displayName}?`,
|
|
5882
|
+
}]);
|
|
5883
|
+
if (!answer.send) {
|
|
5884
|
+
console.log(chalk.gray('已取消,没有发送'));
|
|
5885
|
+
return;
|
|
5886
|
+
}
|
|
5887
|
+
}
|
|
5888
|
+
const sent = await performOutbound(attempt);
|
|
5889
|
+
if (sent.success) {
|
|
5890
|
+
if (opts.json)
|
|
5891
|
+
console.log(JSON.stringify(sendPreview));
|
|
5892
|
+
else
|
|
5893
|
+
console.log(chalk.green('✓ 发送成功'));
|
|
5894
|
+
return;
|
|
5895
|
+
}
|
|
5896
|
+
if (opts.json)
|
|
5897
|
+
console.log(JSON.stringify({ ...sendPreview, success: false, code: 'SEND_FAILED', error: sent.error }));
|
|
5898
|
+
else
|
|
5899
|
+
console.log(chalk.red(`✗ 发送失败 — ${sent.error}`));
|
|
5900
|
+
process.exit(1);
|
|
5901
|
+
}
|
|
5555
5902
|
const args = [script, '--talker', String(talker), '--count', String(count),
|
|
5556
5903
|
// 脚本自己有第二道闸门(`--yes` 才真的把对话发出去),所以要透传
|
|
5557
5904
|
...(confirmed ? ['--yes'] : []),
|
|
@@ -5576,17 +5923,87 @@ program
|
|
|
5576
5923
|
process.exit(1);
|
|
5577
5924
|
}
|
|
5578
5925
|
});
|
|
5926
|
+
// wiki graph:把概念图谱导成一张**自包含**的 3D 页面(全本地:只读 Vault、不联网、不调用模型)
|
|
5927
|
+
//
|
|
5928
|
+
// 自包含是硬要求,不是顺手:页面用 `file://` 双击打开,那时 `fetch` 会被 CORS 挡死,
|
|
5929
|
+
// 所以库、数据、坐标全部内联进去 —— 也因此三个库随 npm 包发(见 resources/js/graph3d/NOTICE.txt)。
|
|
5930
|
+
// 布局(250 tick 实测 55 秒)在构建期用 Node 算完并缓存,打开页面即用。
|
|
5931
|
+
program.commands.find(c => c.name() === 'wiki')?.addCommand(new Command('graph')
|
|
5932
|
+
.description('把概念图谱导成一张自包含的图谱页面(只读本地、不联网;生成的 HTML 双击就能逛)。--flat 出 2D 版')
|
|
5933
|
+
.option('--vault <dir>', 'Vault 根目录(默认 output/wechat-vault)')
|
|
5934
|
+
.option('-o, --out <file>', '页面写到哪(默认 output/knowledge-graph-3d.html)')
|
|
5935
|
+
.option('--ticks <n>', '力导向迭代次数(默认 250,越多越舒展也越慢)')
|
|
5936
|
+
.option('--cache <dir>', '布局缓存目录(派生数据;测试/多份图分开放时用)')
|
|
5937
|
+
.option('--flat', '2D 版:平面图(更像 Obsidian 那种),canvas 渲染,输出默认 knowledge-graph-2d.html')
|
|
5938
|
+
.option('--min-degree <n>', '只画在原图里连接数 ≥N 的概念(去掉细枝看骨架;0 = 全画)')
|
|
5939
|
+
.option('--line <which>', '只画一条线:all / wiki / chat')
|
|
5940
|
+
.option('--dry-run', '只报概念与链接数:不写文件、也不算布局(本地,零改动)')
|
|
5941
|
+
.option('--open', '生成后用默认浏览器打开它')
|
|
5942
|
+
.option('--json', '输出机器可读结果')
|
|
5943
|
+
.action(async (opts) => {
|
|
5944
|
+
const { execFile } = await import('child_process');
|
|
5945
|
+
const { promisify } = await import('util');
|
|
5946
|
+
const execFileAsync = promisify(execFile);
|
|
5947
|
+
// 2D 与 3D 是**两个入口脚本**:图怎么建共用(graph_2d 直接 import graph_3d),
|
|
5948
|
+
// 换的是布局维度与渲染器 —— 两套渲染器塞一个文件就没人看得完了。
|
|
5949
|
+
const script = join(resolvePackageRoot(), 'scripts', opts.flat ? 'graph_2d.py' : 'graph_3d.py');
|
|
5950
|
+
const ticks = opts.ticks === undefined ? undefined : parseCliInteger(opts.ticks, 'ticks', 1, 100000, !!opts.json);
|
|
5951
|
+
// `--json` 一律要:CLI 要拿到 out 路径与计数,才决定怎么印。开关只决定**给谁看**。
|
|
5952
|
+
const minDegree = opts.minDegree === undefined
|
|
5953
|
+
? undefined : parseCliInteger(opts.minDegree, 'min-degree', 0, 100000, !!opts.json);
|
|
5954
|
+
const args = [script,
|
|
5955
|
+
...(opts.vault ? ['--vault', opts.vault] : []),
|
|
5956
|
+
...(opts.out ? ['--out', opts.out] : []),
|
|
5957
|
+
...(opts.cache ? ['--cache', opts.cache] : []),
|
|
5958
|
+
...(ticks === undefined ? [] : ['--ticks', String(ticks)]),
|
|
5959
|
+
...(minDegree === undefined || minDegree === 0 ? [] : ['--min-degree', String(minDegree)]),
|
|
5960
|
+
...(opts.line ? ['--line', opts.line] : []),
|
|
5961
|
+
...(opts.dryRun ? ['--dry-run'] : []),
|
|
5962
|
+
'--json'];
|
|
5963
|
+
try {
|
|
5964
|
+
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
5965
|
+
timeout: 900_000, maxBuffer: 64 * 1024 * 1024, env: pythonProcessEnv(),
|
|
5966
|
+
});
|
|
5967
|
+
const body = JSON.parse(stdout.slice(stdout.indexOf('{')));
|
|
5968
|
+
if (opts.json) {
|
|
5969
|
+
console.log(JSON.stringify(body));
|
|
5970
|
+
}
|
|
5971
|
+
else if (body.dryRun) {
|
|
5972
|
+
console.log(`概念 ${body.nodes} 个、链接 ${body.links} 条(--dry-run:没有写文件)`);
|
|
5973
|
+
}
|
|
5974
|
+
else {
|
|
5975
|
+
console.log(chalk.green(`✓ 已生成 ${body.out}`));
|
|
5976
|
+
console.log(chalk.gray(` 概念 ${body.nodes} / 链接 ${body.links} / ${body.mb} MB · ${opts.flat ? '2D' : '3D'} · 双击就能逛,不需要服务器`));
|
|
5977
|
+
}
|
|
5978
|
+
if (!opts.dryRun && opts.open && body.out)
|
|
5979
|
+
await openLocalUrl(body.out);
|
|
5980
|
+
}
|
|
5981
|
+
catch (error) {
|
|
5982
|
+
if (opts.json) {
|
|
5983
|
+
console.log(JSON.stringify({ success: false, code: 'GRAPH_FAILED', action: 'graph-3d', error: safeSubprocessError(error) }));
|
|
5984
|
+
}
|
|
5985
|
+
else {
|
|
5986
|
+
console.error(chalk.red(`\n✗ ${safeSubprocessError(error)}`));
|
|
5987
|
+
}
|
|
5988
|
+
process.exit(1);
|
|
5989
|
+
}
|
|
5990
|
+
}));
|
|
5579
5991
|
// wiki lint:知识库体检(全本地:不联网、不调用模型)
|
|
5580
5992
|
program.commands.find(c => c.name() === 'wiki')?.addCommand(new Command('lint')
|
|
5581
5993
|
.description('知识库体检:断链、孤儿、空页、同名(本地,不联网)')
|
|
5582
|
-
|
|
5994
|
+
// 不给默认值:不传 `--dir` 时体检脚本自己的默认(清单里的**全部线**)。
|
|
5995
|
+
// 这里曾经默认只给文章线,而脚本默认两条线都体检 ⇒"半开半合":命令行跑出来
|
|
5996
|
+
// 与脚本跑出来口径不同,而两边都不报错。
|
|
5997
|
+
.option('--dir <dirs...>', '概念页目录(可重复;不传则体检清单里的全部线)')
|
|
5583
5998
|
.option('--json', '输出机器可读结果')
|
|
5584
5999
|
.action(async (opts) => {
|
|
5585
6000
|
const { execFile } = await import('child_process');
|
|
5586
6001
|
const { promisify } = await import('util');
|
|
5587
6002
|
const execFileAsync = promisify(execFile);
|
|
5588
6003
|
const script = join(resolvePackageRoot(), 'scripts', 'wiki_lint.py');
|
|
5589
|
-
const
|
|
6004
|
+
const dirs = Array.isArray(opts.dir) ? opts.dir : (opts.dir ? [opts.dir] : []);
|
|
6005
|
+
const args = [script, ...dirs.flatMap((d) => ['--dir', String(d)]),
|
|
6006
|
+
...(opts.json ? ['--json'] : [])];
|
|
5590
6007
|
try {
|
|
5591
6008
|
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
5592
6009
|
timeout: 120_000, maxBuffer: 20 * 1024 * 1024, env: pythonProcessEnv(),
|
|
@@ -5610,6 +6027,7 @@ program
|
|
|
5610
6027
|
.option('--limit <n>', '最多处理几个会话', '40')
|
|
5611
6028
|
.option('--dry-run', '仅预览:几个会话、多少字符会发给生成模型(只读本地,零出境)')
|
|
5612
6029
|
.option('--yes', '确认把对话发送给生成模型')
|
|
6030
|
+
.option('--transcribe-voice', '把缺转写的语音**本机**识别掉再产卡(本地 whisper,慢、吃 CPU、不出网;配合 --dry-run 就是只补转写、不调模型)')
|
|
5613
6031
|
.option('--json', '输出机器可读结果;执行仍需 --yes')
|
|
5614
6032
|
.action(async (opts) => {
|
|
5615
6033
|
const { execFile } = await import('child_process');
|
|
@@ -5645,6 +6063,7 @@ program
|
|
|
5645
6063
|
}
|
|
5646
6064
|
const args = [script, '--days', String(days), '--limit', String(limit),
|
|
5647
6065
|
...(confirmed ? ['--yes'] : []),
|
|
6066
|
+
...(opts.transcribeVoice ? ['--transcribe-voice'] : []),
|
|
5648
6067
|
...(opts.json ? ['--json'] : []),
|
|
5649
6068
|
...(opts.dryRun ? ['--dry-run'] : [])];
|
|
5650
6069
|
try {
|
|
@@ -6018,7 +6437,7 @@ program
|
|
|
6018
6437
|
};
|
|
6019
6438
|
const isComplete = (targetDate) => Object.values(artifactStatus(targetDate)).every(Boolean);
|
|
6020
6439
|
const runPipeline = (targetDate) => new Promise((resolve) => {
|
|
6021
|
-
const args = [pipeline, '--date', targetDate, '--engine', noAi ? 'local' : 'deepseek', '--interest', 'AI'
|
|
6440
|
+
const args = [pipeline, '--date', targetDate, '--engine', noAi ? 'local' : 'deepseek', '--interest', 'AI'];
|
|
6022
6441
|
if (noAi)
|
|
6023
6442
|
args.push('--no-ai');
|
|
6024
6443
|
// 与 --no-ai 不同:这一条保留判断(Jev),只关掉 LLM 的文字生成。
|
|
@@ -6030,6 +6449,7 @@ program
|
|
|
6030
6449
|
args.push('--skip-classify');
|
|
6031
6450
|
dailyLog(chalk.cyan(`\n📰 正在生成 ${targetDate} 公众号日报${noAi ? '(AI 已关闭)' : ''}...\n`));
|
|
6032
6451
|
const child = spawn(getPythonCommand(), args, {
|
|
6452
|
+
windowsHide: true,
|
|
6033
6453
|
stdio: opts.json ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
6034
6454
|
env: pythonProcessEnv(apiKey),
|
|
6035
6455
|
});
|
|
@@ -6045,6 +6465,7 @@ program
|
|
|
6045
6465
|
for (const source of opts.source || [])
|
|
6046
6466
|
dryRunArgs.push('--source', source);
|
|
6047
6467
|
const child = spawn(getPythonCommand(), dryRunArgs, {
|
|
6468
|
+
windowsHide: true,
|
|
6048
6469
|
stdio: opts.json ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
6049
6470
|
env: pythonProcessEnv(),
|
|
6050
6471
|
});
|
|
@@ -6191,6 +6612,351 @@ dailyFavoritesCmd
|
|
|
6191
6612
|
await runDailyFavorites(merged.date || defaultDailyDate(), merged.root, ['--remove', ...articles], 'daily.favorites.remove', merged);
|
|
6192
6613
|
});
|
|
6193
6614
|
program.commands.find(c => c.name() === 'daily')?.addCommand(dailyFavoritesCmd);
|
|
6615
|
+
// ==================== skill (本机技能包) ====================
|
|
6616
|
+
// 技能就是磁盘上的 `SKILL.md`,所以这里**没有安装/卸载**:读、看、体检三件事。
|
|
6617
|
+
// 体检(`check`)是最有用的一个 —— 它把"读不出来""不合规范""跨目录撞名"这些
|
|
6618
|
+
// 平时完全静默的问题一次报出来(少一个技能不会报错,只会少一个技能)。
|
|
6619
|
+
const skillCmd = program
|
|
6620
|
+
.command('skill')
|
|
6621
|
+
.description('本机技能包(SKILL.md):列出、查看、体检');
|
|
6622
|
+
skillCmd
|
|
6623
|
+
.command('list')
|
|
6624
|
+
.description('列出扫描到的技能')
|
|
6625
|
+
.option('--query <q>', '按关键词过滤(匹配 id、名称、说明、触发词)')
|
|
6626
|
+
.option('--json', 'JSON 输出')
|
|
6627
|
+
.action((opts) => {
|
|
6628
|
+
const scan = scanSkills();
|
|
6629
|
+
const disabled = disabledIds();
|
|
6630
|
+
const q = String(opts.query || '').trim().toLowerCase();
|
|
6631
|
+
const rows = scan.skills.filter(s => !q
|
|
6632
|
+
|| [s.id, s.name, s.description, ...s.triggers].some(x => String(x).toLowerCase().includes(q)))
|
|
6633
|
+
.map(s => ({
|
|
6634
|
+
id: s.id,
|
|
6635
|
+
name: s.name,
|
|
6636
|
+
description: s.description,
|
|
6637
|
+
version: s.version,
|
|
6638
|
+
state: skillState(s, disabled),
|
|
6639
|
+
notes: s.notes,
|
|
6640
|
+
// **不给路径**:这是机器输出,路径只有在用户明确要 `show` 时才出现
|
|
6641
|
+
}));
|
|
6642
|
+
if (opts.json) {
|
|
6643
|
+
console.log(JSON.stringify({
|
|
6644
|
+
success: true,
|
|
6645
|
+
roots: skillRoots(),
|
|
6646
|
+
missingRoots: scan.missingRoots,
|
|
6647
|
+
total: rows.length,
|
|
6648
|
+
skills: rows,
|
|
6649
|
+
}));
|
|
6650
|
+
return;
|
|
6651
|
+
}
|
|
6652
|
+
if (!rows.length) {
|
|
6653
|
+
console.log(chalk.gray(scan.skills.length ? `没有匹配「${opts.query}」的技能` : '没有扫描到技能'));
|
|
6654
|
+
console.log(chalk.gray(`技能目录:${skillRoots().join('、')}`));
|
|
6655
|
+
for (const dir of scan.missingRoots)
|
|
6656
|
+
console.log(chalk.gray(` (不存在:${dir})`));
|
|
6657
|
+
return;
|
|
6658
|
+
}
|
|
6659
|
+
for (const r of rows) {
|
|
6660
|
+
const tag = r.state === 'disabled' ? chalk.gray('[已禁用] ') : '';
|
|
6661
|
+
console.log(`${tag}${chalk.cyan(r.id)}${r.version ? chalk.gray(' v' + r.version) : ''} ${r.name}`);
|
|
6662
|
+
if (r.description)
|
|
6663
|
+
console.log(chalk.gray(` ${r.description.slice(0, 100)}`));
|
|
6664
|
+
for (const n of r.notes)
|
|
6665
|
+
console.log(chalk.yellow(` ⚠ ${n}`));
|
|
6666
|
+
}
|
|
6667
|
+
console.log(chalk.gray(`\n共 ${rows.length} 个。正文:weflow-cli skill show <id>`));
|
|
6668
|
+
});
|
|
6669
|
+
skillCmd
|
|
6670
|
+
.command('show')
|
|
6671
|
+
.argument('<id>', '技能 id')
|
|
6672
|
+
.description('打印一个技能的 SKILL.md 正文')
|
|
6673
|
+
.option('--json', 'JSON 输出')
|
|
6674
|
+
.action((id, opts) => {
|
|
6675
|
+
const body = readSkillBody(id);
|
|
6676
|
+
if (!body.ok) {
|
|
6677
|
+
if (opts.json) {
|
|
6678
|
+
console.log(JSON.stringify({ success: false, code: 'SKILL_NOT_READABLE', error: body.text }));
|
|
6679
|
+
process.exit(1);
|
|
6680
|
+
}
|
|
6681
|
+
console.log(chalk.red(body.text));
|
|
6682
|
+
return;
|
|
6683
|
+
}
|
|
6684
|
+
if (opts.json) {
|
|
6685
|
+
const scan = scanSkills();
|
|
6686
|
+
const info = scan.skills.find(s => s.id === id);
|
|
6687
|
+
console.log(JSON.stringify({
|
|
6688
|
+
success: true,
|
|
6689
|
+
id,
|
|
6690
|
+
name: info?.name ?? id,
|
|
6691
|
+
version: info?.version ?? '',
|
|
6692
|
+
truncated: !!body.truncated,
|
|
6693
|
+
body: body.text,
|
|
6694
|
+
}));
|
|
6695
|
+
return;
|
|
6696
|
+
}
|
|
6697
|
+
console.log(body.text);
|
|
6698
|
+
});
|
|
6699
|
+
skillCmd
|
|
6700
|
+
.command('check')
|
|
6701
|
+
.description('体检:不可读的 SKILL.md、跨目录同名、非规范 id、被禁用的技能')
|
|
6702
|
+
.option('--json', 'JSON 输出')
|
|
6703
|
+
.action((opts) => {
|
|
6704
|
+
const scan = scanSkills();
|
|
6705
|
+
const disabled = disabledIds();
|
|
6706
|
+
const disabledRows = scan.skills.filter(s => skillState(s, disabled) === 'disabled').map(s => s.id);
|
|
6707
|
+
const warned = scan.skills.filter(s => s.notes.length).map(s => ({ id: s.id, notes: s.notes }));
|
|
6708
|
+
const payload = {
|
|
6709
|
+
success: true,
|
|
6710
|
+
roots: skillRoots(),
|
|
6711
|
+
missingRoots: scan.missingRoots,
|
|
6712
|
+
total: scan.skills.length,
|
|
6713
|
+
unreadable: scan.unreadable,
|
|
6714
|
+
conflicts: scan.conflicts,
|
|
6715
|
+
warned,
|
|
6716
|
+
disabled: disabledRows,
|
|
6717
|
+
};
|
|
6718
|
+
if (opts.json) {
|
|
6719
|
+
console.log(JSON.stringify(payload));
|
|
6720
|
+
// 不可读 = 真出事了(有文件却读不出来),体检失败;警告与冲突只提示,不算失败
|
|
6721
|
+
if (scan.unreadable.length)
|
|
6722
|
+
process.exit(1);
|
|
6723
|
+
return;
|
|
6724
|
+
}
|
|
6725
|
+
console.log(`技能目录:${skillRoots().join('、')}`);
|
|
6726
|
+
for (const dir of scan.missingRoots)
|
|
6727
|
+
console.log(chalk.gray(` 不存在:${dir}`));
|
|
6728
|
+
console.log(`扫描到 ${scan.skills.length} 个技能`);
|
|
6729
|
+
if (scan.unreadable.length) {
|
|
6730
|
+
console.log(chalk.red(`\n不可读 ${scan.unreadable.length} 个:`));
|
|
6731
|
+
for (const u of scan.unreadable)
|
|
6732
|
+
console.log(chalk.red(` ✗ ${u.file}\n ${u.reason}`));
|
|
6733
|
+
}
|
|
6734
|
+
if (scan.conflicts.length) {
|
|
6735
|
+
console.log(chalk.yellow(`\n跨目录同名 ${scan.conflicts.length} 个(靠前的目录胜出,不会覆盖):`));
|
|
6736
|
+
for (const c of scan.conflicts)
|
|
6737
|
+
console.log(chalk.yellow(` ! ${c.id}\n 用:${c.winner}\n 忽略:${c.loser}`));
|
|
6738
|
+
}
|
|
6739
|
+
if (warned.length) {
|
|
6740
|
+
console.log(chalk.yellow(`\n有提醒的 ${warned.length} 个:`));
|
|
6741
|
+
for (const w of warned)
|
|
6742
|
+
for (const n of w.notes)
|
|
6743
|
+
console.log(chalk.yellow(` · ${w.id}:${n}`));
|
|
6744
|
+
}
|
|
6745
|
+
if (disabledRows.length)
|
|
6746
|
+
console.log(chalk.gray(`\n已禁用:${disabledRows.join('、')}`));
|
|
6747
|
+
if (!scan.unreadable.length && !scan.conflicts.length)
|
|
6748
|
+
console.log(chalk.green('\n没有不可读或撞名的技能'));
|
|
6749
|
+
});
|
|
6750
|
+
// ==================== scene (会话场景) ====================
|
|
6751
|
+
const sceneCmd = program
|
|
6752
|
+
.command('scene')
|
|
6753
|
+
.description('会话场景:按会话生效的提示词预设(绑定 → 关键词 → 上次用过的)');
|
|
6754
|
+
/** 写场景是**本机文件写**,不是 python mutation:同样两段式,措辞照 todos 那条 */
|
|
6755
|
+
async function confirmSceneWrite(opts, preview, message, apply) {
|
|
6756
|
+
if (opts.dryRun) {
|
|
6757
|
+
if (opts.json)
|
|
6758
|
+
console.log(JSON.stringify({ success: true, dryRun: true, ...preview }));
|
|
6759
|
+
else
|
|
6760
|
+
console.log(chalk.cyan('预览:') + message);
|
|
6761
|
+
return;
|
|
6762
|
+
}
|
|
6763
|
+
if (!opts.yes) {
|
|
6764
|
+
if (opts.json) {
|
|
6765
|
+
console.log(JSON.stringify({
|
|
6766
|
+
success: false,
|
|
6767
|
+
code: 'CONFIRMATION_REQUIRED',
|
|
6768
|
+
error: '先使用 --dry-run --json 预览,再由用户确认后使用 --yes --json 执行',
|
|
6769
|
+
}));
|
|
6770
|
+
process.exit(1);
|
|
6771
|
+
}
|
|
6772
|
+
const { confirmed } = await inquirer.prompt([{ type: 'confirm', name: 'confirmed', message, default: false }]);
|
|
6773
|
+
if (!confirmed) {
|
|
6774
|
+
console.log(chalk.gray('已取消'));
|
|
6775
|
+
return;
|
|
6776
|
+
}
|
|
6777
|
+
}
|
|
6778
|
+
const result = apply();
|
|
6779
|
+
if (result.ok)
|
|
6780
|
+
assistantScenes.save();
|
|
6781
|
+
const saved = result.ok && !assistantScenes.lastSaveError;
|
|
6782
|
+
if (opts.json) {
|
|
6783
|
+
if (!result.ok) {
|
|
6784
|
+
console.log(JSON.stringify({ success: false, code: 'SCENE_OPERATION_FAILED', error: result.reason }));
|
|
6785
|
+
process.exit(1);
|
|
6786
|
+
}
|
|
6787
|
+
// 写失败要说出来:`save()` 不抛异常,代价是必须有地方把它讲出来
|
|
6788
|
+
console.log(JSON.stringify({
|
|
6789
|
+
success: saved,
|
|
6790
|
+
...(saved ? {} : { code: 'SCENE_SAVE_FAILED', error: assistantScenes.lastSaveError }),
|
|
6791
|
+
...preview,
|
|
6792
|
+
}));
|
|
6793
|
+
if (!saved)
|
|
6794
|
+
process.exit(1);
|
|
6795
|
+
return;
|
|
6796
|
+
}
|
|
6797
|
+
if (!result.ok) {
|
|
6798
|
+
console.log(chalk.red(`✗ ${result.reason}`));
|
|
6799
|
+
process.exit(1);
|
|
6800
|
+
}
|
|
6801
|
+
if (!saved)
|
|
6802
|
+
console.log(chalk.red(`⚠ 已改动内存,但落盘失败:${assistantScenes.lastSaveError}`));
|
|
6803
|
+
else
|
|
6804
|
+
console.log(chalk.green('✓ 完成'));
|
|
6805
|
+
}
|
|
6806
|
+
sceneCmd
|
|
6807
|
+
.command('list')
|
|
6808
|
+
.description('列出所有场景')
|
|
6809
|
+
.option('--json', 'JSON 输出')
|
|
6810
|
+
.action((opts) => {
|
|
6811
|
+
const scenes = assistantScenes.list();
|
|
6812
|
+
if (opts.json) {
|
|
6813
|
+
console.log(JSON.stringify({ success: true, total: scenes.length, scenes }));
|
|
6814
|
+
return;
|
|
6815
|
+
}
|
|
6816
|
+
if (!scenes.length) {
|
|
6817
|
+
console.log(chalk.gray('还没有场景。加一个:'));
|
|
6818
|
+
console.log(chalk.gray(' weflow-cli scene add --id 日报 --keywords 日报,推文 --instruction "按主题分组" --dry-run'));
|
|
6819
|
+
return;
|
|
6820
|
+
}
|
|
6821
|
+
for (const s of scenes) {
|
|
6822
|
+
console.log(`${s.enabled ? chalk.cyan(s.id) : chalk.gray(s.id + ' [已停用]')} ${s.name}`);
|
|
6823
|
+
if (s.keywords.length)
|
|
6824
|
+
console.log(chalk.gray(` 关键词:${s.keywords.join('、')}`));
|
|
6825
|
+
if (s.instruction)
|
|
6826
|
+
console.log(chalk.gray(` ${s.instruction.split('\n')[0].slice(0, 80)}`));
|
|
6827
|
+
}
|
|
6828
|
+
});
|
|
6829
|
+
sceneCmd
|
|
6830
|
+
.command('show')
|
|
6831
|
+
.argument('<id>', '场景 id')
|
|
6832
|
+
.description('打印一个场景的全部字段')
|
|
6833
|
+
.option('--json', 'JSON 输出')
|
|
6834
|
+
.action((id, opts) => {
|
|
6835
|
+
const scene = assistantScenes.get(id);
|
|
6836
|
+
if (!scene) {
|
|
6837
|
+
if (opts.json)
|
|
6838
|
+
console.log(JSON.stringify({ success: false, code: 'SCENE_NOT_FOUND', error: `没有场景「${id}」` }));
|
|
6839
|
+
else
|
|
6840
|
+
console.log(chalk.red(`没有场景「${id}」`));
|
|
6841
|
+
process.exit(1);
|
|
6842
|
+
}
|
|
6843
|
+
if (opts.json) {
|
|
6844
|
+
console.log(JSON.stringify({ success: true, scene }));
|
|
6845
|
+
return;
|
|
6846
|
+
}
|
|
6847
|
+
console.log(`id ${scene.id}`);
|
|
6848
|
+
console.log(`名称 ${scene.name}`);
|
|
6849
|
+
console.log(`状态 ${scene.enabled ? '启用' : '已停用'}`);
|
|
6850
|
+
console.log(`关键词 ${scene.keywords.join('、') || '(无)'}`);
|
|
6851
|
+
console.log(`所需技能 ${scene.skills.join('、') || '(无)'}`);
|
|
6852
|
+
console.log(`附加指令 ${scene.instruction || '(无)'}`);
|
|
6853
|
+
if (scene.outputSpec)
|
|
6854
|
+
console.log(`输出规范 ${scene.outputSpec}`);
|
|
6855
|
+
});
|
|
6856
|
+
sceneCmd
|
|
6857
|
+
.command('add')
|
|
6858
|
+
.description('新增一个场景')
|
|
6859
|
+
.requiredOption('--id <id>', '场景 id(字母数字开头,可用 . _ -)')
|
|
6860
|
+
.option('--name <name>', '显示名(默认用 id)')
|
|
6861
|
+
.option('--keywords <list>', '关键词,逗号分隔')
|
|
6862
|
+
.option('--instruction <text>', '附加指令(长文本用 --from-file)')
|
|
6863
|
+
.option('--from-file <path>', '从文件读附加指令(多行中文走这条)')
|
|
6864
|
+
.option('--output-spec <text>', '输出规范')
|
|
6865
|
+
.option('--skills <list>', '所需技能 id,逗号分隔')
|
|
6866
|
+
.option('--dry-run', '仅预览,不写入')
|
|
6867
|
+
.option('--yes', '确认写入')
|
|
6868
|
+
.option('--json', 'JSON 输出')
|
|
6869
|
+
.action(async (opts) => {
|
|
6870
|
+
const id = String(opts.id || '').trim();
|
|
6871
|
+
const idError = validateSceneId(id);
|
|
6872
|
+
if (idError) {
|
|
6873
|
+
if (opts.json)
|
|
6874
|
+
console.log(JSON.stringify({ success: false, code: 'INVALID_SCENE_ID', error: idError }));
|
|
6875
|
+
else
|
|
6876
|
+
console.log(chalk.red(idError));
|
|
6877
|
+
process.exit(1);
|
|
6878
|
+
}
|
|
6879
|
+
let instruction = String(opts.instruction || '');
|
|
6880
|
+
if (opts.fromFile) {
|
|
6881
|
+
const path = String(opts.fromFile);
|
|
6882
|
+
if (!existsSync(path)) {
|
|
6883
|
+
if (opts.json)
|
|
6884
|
+
console.log(JSON.stringify({ success: false, code: 'FILE_NOT_FOUND', error: `读不到 ${path}` }));
|
|
6885
|
+
else
|
|
6886
|
+
console.log(chalk.red(`读不到 ${path}`));
|
|
6887
|
+
process.exit(1);
|
|
6888
|
+
}
|
|
6889
|
+
instruction = readFileSync(path, 'utf8');
|
|
6890
|
+
}
|
|
6891
|
+
const splitList = (v) => String(v || '').split(/[,;]/).map(s => s.trim()).filter(Boolean);
|
|
6892
|
+
const scene = {
|
|
6893
|
+
id,
|
|
6894
|
+
name: String(opts.name || '').trim() || id,
|
|
6895
|
+
keywords: splitList(opts.keywords),
|
|
6896
|
+
instruction,
|
|
6897
|
+
outputSpec: String(opts.outputSpec || ''),
|
|
6898
|
+
skills: splitList(opts.skills),
|
|
6899
|
+
enabled: true,
|
|
6900
|
+
};
|
|
6901
|
+
await confirmSceneWrite(opts, { action: 'scene.add', scene: { ...scene, instruction: `(${instruction.length} 字)` } }, `确认新增场景「${scene.name}」?`, () => assistantScenes.add(scene));
|
|
6902
|
+
});
|
|
6903
|
+
sceneCmd
|
|
6904
|
+
.command('remove')
|
|
6905
|
+
.argument('<id>', '场景 id')
|
|
6906
|
+
.description('删除一个场景(连同指向它的绑定)')
|
|
6907
|
+
.option('--dry-run', '仅预览,不删除')
|
|
6908
|
+
.option('--yes', '确认删除')
|
|
6909
|
+
.option('--json', 'JSON 输出')
|
|
6910
|
+
.action(async (id, opts) => {
|
|
6911
|
+
const scene = assistantScenes.get(id);
|
|
6912
|
+
if (!scene) {
|
|
6913
|
+
if (opts.json)
|
|
6914
|
+
console.log(JSON.stringify({ success: false, code: 'SCENE_NOT_FOUND', error: `没有场景「${id}」` }));
|
|
6915
|
+
else
|
|
6916
|
+
console.log(chalk.red(`没有场景「${id}」`));
|
|
6917
|
+
process.exit(1);
|
|
6918
|
+
}
|
|
6919
|
+
await confirmSceneWrite(opts, { action: 'scene.remove', id }, `确认删除场景「${scene.name}」?`, () => { assistantScenes.remove(id); return { ok: true }; });
|
|
6920
|
+
});
|
|
6921
|
+
const toggleScene = (on) => async (id, opts) => {
|
|
6922
|
+
const scene = assistantScenes.get(id);
|
|
6923
|
+
if (!scene) {
|
|
6924
|
+
if (opts.json)
|
|
6925
|
+
console.log(JSON.stringify({ success: false, code: 'SCENE_NOT_FOUND', error: `没有场景「${id}」` }));
|
|
6926
|
+
else
|
|
6927
|
+
console.log(chalk.red(`没有场景「${id}」`));
|
|
6928
|
+
process.exit(1);
|
|
6929
|
+
}
|
|
6930
|
+
await confirmSceneWrite(opts, { action: on ? 'scene.enable' : 'scene.disable', id }, `确认${on ? '启用' : '停用'}场景「${scene.name}」?`, () => ({ ok: assistantScenes.setEnabled(id, on) }));
|
|
6931
|
+
};
|
|
6932
|
+
sceneCmd.command('enable')
|
|
6933
|
+
.argument('<id>', '场景 id')
|
|
6934
|
+
.description('启用一个场景')
|
|
6935
|
+
.option('--dry-run', '仅预览').option('--yes', '确认').option('--json', 'JSON 输出')
|
|
6936
|
+
.action(toggleScene(true));
|
|
6937
|
+
sceneCmd.command('disable')
|
|
6938
|
+
.argument('<id>', '场景 id')
|
|
6939
|
+
.description('停用一个场景(它不再参与匹配)')
|
|
6940
|
+
.option('--dry-run', '仅预览').option('--yes', '确认').option('--json', 'JSON 输出')
|
|
6941
|
+
.action(toggleScene(false));
|
|
6942
|
+
sceneCmd
|
|
6943
|
+
.command('bind')
|
|
6944
|
+
.argument('<conversation>', '会话 id(微信里就是 conversationId;本机面板用它的记忆桶)')
|
|
6945
|
+
.requiredOption('--scene <id>', '要绑定的场景 id')
|
|
6946
|
+
.description('把某个场景固定绑到一个会话')
|
|
6947
|
+
.option('--dry-run', '仅预览').option('--yes', '确认').option('--json', 'JSON 输出')
|
|
6948
|
+
.action(async (conversation, opts) => {
|
|
6949
|
+
const id = String(opts.scene || '').trim();
|
|
6950
|
+
await confirmSceneWrite(opts, { action: 'scene.bind', conversation, scene: id }, `确认把场景「${id}」绑到会话 ${conversation}?`, () => assistantScenes.bind(conversation, id));
|
|
6951
|
+
});
|
|
6952
|
+
sceneCmd
|
|
6953
|
+
.command('unbind')
|
|
6954
|
+
.argument('<conversation>', '会话 id')
|
|
6955
|
+
.description('解绑:这个会话回到按关键词自动挑')
|
|
6956
|
+
.option('--dry-run', '仅预览').option('--yes', '确认').option('--json', 'JSON 输出')
|
|
6957
|
+
.action(async (conversation, opts) => {
|
|
6958
|
+
await confirmSceneWrite(opts, { action: 'scene.unbind', conversation }, `确认解绑会话 ${conversation}?`, () => assistantScenes.bind(conversation, null));
|
|
6959
|
+
});
|
|
6194
6960
|
// ==================== assistant (第二大脑持久化 Agent) ====================
|
|
6195
6961
|
const assistantCmd = program
|
|
6196
6962
|
.command('assistant')
|
|
@@ -6643,6 +7409,52 @@ program
|
|
|
6643
7409
|
process.exit(1);
|
|
6644
7410
|
}
|
|
6645
7411
|
});
|
|
7412
|
+
// ==================== bonds ====================
|
|
7413
|
+
program
|
|
7414
|
+
.command('bonds')
|
|
7415
|
+
.description('关系温度:谁在冷却、谁在变热、谁只有微信这一条线(只读本地库,不联网、不调模型)')
|
|
7416
|
+
.option('--days <number>', '看最近多少天', '365')
|
|
7417
|
+
.option('--limit <number>', '最多扫几个会话', '400')
|
|
7418
|
+
.option('--top <number>', '每组列几条', '12')
|
|
7419
|
+
.option('--skip <name>', '排除某个名字或 id(可重复)', (value, acc) => {
|
|
7420
|
+
acc.push(value);
|
|
7421
|
+
return acc;
|
|
7422
|
+
}, [])
|
|
7423
|
+
.option('--json', '输出 JSON 格式')
|
|
7424
|
+
.action(async (opts) => {
|
|
7425
|
+
const { execFile } = await import('child_process');
|
|
7426
|
+
const { promisify } = await import('util');
|
|
7427
|
+
const execFileAsync = promisify(execFile);
|
|
7428
|
+
const script = join(resolvePackageRoot(), 'scripts', 'bonds.py');
|
|
7429
|
+
const days = parseCliInteger(opts.days, 'days', 1, 3650, opts.json);
|
|
7430
|
+
const limit = parseCliInteger(opts.limit, 'limit', 1, 5000, opts.json);
|
|
7431
|
+
const top = parseCliInteger(opts.top, 'top', 1, 200, opts.json);
|
|
7432
|
+
try {
|
|
7433
|
+
const args = [script, '--days', String(days), '--limit', String(limit), '--top', String(top)];
|
|
7434
|
+
for (const name of opts.skip || [])
|
|
7435
|
+
args.push('--skip', name);
|
|
7436
|
+
if (opts.json)
|
|
7437
|
+
args.push('--json');
|
|
7438
|
+
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
7439
|
+
timeout: 300_000,
|
|
7440
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
7441
|
+
env: pythonProcessEnv(),
|
|
7442
|
+
});
|
|
7443
|
+
console.log(stdout);
|
|
7444
|
+
}
|
|
7445
|
+
catch (e) {
|
|
7446
|
+
if (opts.json) {
|
|
7447
|
+
console.log(JSON.stringify({
|
|
7448
|
+
success: false,
|
|
7449
|
+
code: 'BONDS_FAILED',
|
|
7450
|
+
error: '关系温度不可用,请运行 check --json 检查配置和依赖',
|
|
7451
|
+
}));
|
|
7452
|
+
process.exit(1);
|
|
7453
|
+
}
|
|
7454
|
+
console.error(chalk.red(`\n${safeSubprocessError(e, '统计失败')}`));
|
|
7455
|
+
process.exit(1);
|
|
7456
|
+
}
|
|
7457
|
+
});
|
|
6646
7458
|
// ==================== daily-server ====================
|
|
6647
7459
|
program
|
|
6648
7460
|
.command('daily-server')
|
|
@@ -6724,6 +7536,7 @@ program
|
|
|
6724
7536
|
await openLocalUrl(`http://localhost:${port}`);
|
|
6725
7537
|
}
|
|
6726
7538
|
const child = spawn(getPythonCommand(), [favServer, '--date', date, '--port', String(port)], {
|
|
7539
|
+
windowsHide: true,
|
|
6727
7540
|
stdio: 'inherit',
|
|
6728
7541
|
env: pythonProcessEnv(),
|
|
6729
7542
|
});
|
|
@@ -6784,6 +7597,11 @@ program
|
|
|
6784
7597
|
}
|
|
6785
7598
|
else {
|
|
6786
7599
|
console.log(json);
|
|
7600
|
+
// 机器模式到这里就结束:下面那段工具清单与"复制以上 JSON…"是**给人看的**,原来无条件打在
|
|
7601
|
+
// stdout 上 ⇒ `mcp-config --json-result | jq` 断在第 14 行(2026-10-12 由只读驱动扫描发现;
|
|
7602
|
+
// 写路径本来就早退发 JSON,所以只有读路径这一处漏)。
|
|
7603
|
+
if (opts.jsonResult)
|
|
7604
|
+
return;
|
|
6787
7605
|
}
|
|
6788
7606
|
console.log(chalk.cyan('\n可用的 MCP 工具:'));
|
|
6789
7607
|
const STATIC_TOOLS = [
|
|
@@ -6822,20 +7640,42 @@ program
|
|
|
6822
7640
|
.description('检查运行环境(Python、pip 依赖、数据库配置)')
|
|
6823
7641
|
.option('--json', '输出 JSON 格式,不包含数据库路径或密钥')
|
|
6824
7642
|
.action(async (opts) => {
|
|
7643
|
+
// 微信客户端的**版本**与它自报的**数据目录**:两个分支都要用,所以在这里算一次。
|
|
7644
|
+
// 判据见 `wechatClient.ts`(版本取活进程的 `--user-lib-dir`;取不到就是"未知",不是"没问题")。
|
|
7645
|
+
const client = await detectWechatClient();
|
|
7646
|
+
// 客户端自报的数据目录 vs 配置里那个数据库推出来的根:**只比是不是同一份**,
|
|
7647
|
+
// 路径本身不进机器可读输出(`wechat-check/v1` 一直是不含路径的)。
|
|
7648
|
+
//
|
|
7649
|
+
// **只在 4.x 上比**:这个根是从 `ntDbPath`(4.x 的库)推的,而客户端自报的那条来自 4.x 客户端
|
|
7650
|
+
// —— 只用 3.x 的用户(`dbPath3x`)比出来的"不是同一份"没有意义,而这句话**只在 false 时打印**
|
|
7651
|
+
// ⇒ 那就是一条假警告。判据不成立时给 null("不知道"),不是 false。
|
|
7652
|
+
const { dirname } = await import('node:path');
|
|
7653
|
+
// **四层**:`…/xwechat_files/<wxid>/db_storage/message/message_0.db`
|
|
7654
|
+
// → message → db_storage → <wxid> → xwechat_files。少一层就退到 `<wxid>` 目录,
|
|
7655
|
+
// 于是两个路径永远不相等、永远报"不是同一份"(一个看着很合理的假警告)。
|
|
7656
|
+
const dbPathConfigured = String(configService.get('dataVersion') || '') === '4.x'
|
|
7657
|
+
? String(configService.get('ntDbPath') || '')
|
|
7658
|
+
: '';
|
|
7659
|
+
const configuredRoot = dbPathConfigured
|
|
7660
|
+
? dirname(dirname(dirname(dirname(dbPathConfigured))))
|
|
7661
|
+
: '';
|
|
7662
|
+
const dataDirMatchesConfig = (client.dataDirs.length && configuredRoot)
|
|
7663
|
+
? client.dataDirs.some(dir => samePath(dir, configuredRoot))
|
|
7664
|
+
: null;
|
|
6825
7665
|
if (opts.json) {
|
|
6826
7666
|
const { execFileSync } = await import('child_process');
|
|
6827
7667
|
const requiredDependencies = ['sqlcipher3', 'html2text', 'zstandard', 'cryptography'];
|
|
6828
7668
|
const optionalDependencies = ['scrapling'];
|
|
6829
7669
|
let pythonVersion = '';
|
|
6830
7670
|
try {
|
|
6831
|
-
pythonVersion = execFileSync(getPythonCommand(), ['--version'], { encoding: 'utf8', timeout: 5000 }).trim();
|
|
7671
|
+
pythonVersion = execFileSync(getPythonCommand(), ['--version'], { windowsHide: true, encoding: 'utf8', timeout: 5000 }).trim();
|
|
6832
7672
|
}
|
|
6833
7673
|
catch { }
|
|
6834
7674
|
const dependencyStatus = (name) => {
|
|
6835
7675
|
if (!pythonVersion)
|
|
6836
7676
|
return false;
|
|
6837
7677
|
try {
|
|
6838
|
-
execFileSync(getPythonCommand(), ['-c', `import ${name}`], { stdio: 'ignore', timeout: 5000 });
|
|
7678
|
+
execFileSync(getPythonCommand(), ['-c', `import ${name}`], { windowsHide: true, stdio: 'ignore', timeout: 5000 });
|
|
6839
7679
|
return true;
|
|
6840
7680
|
}
|
|
6841
7681
|
catch {
|
|
@@ -6879,6 +7719,26 @@ program
|
|
|
6879
7719
|
messageChannelLoggedIn: !!configService.get('wechatOcToken'),
|
|
6880
7720
|
daemonRunning: daemonAlive,
|
|
6881
7721
|
},
|
|
7722
|
+
// `version` 只从活进程取;取不到就是 null + 一句 `note`(**不是**"没问题")。
|
|
7723
|
+
// `unusableVersionDirs` 是磁盘上的残留版本目录(没有 `Weixin.dll`)—— 指向它们的加载会失败。
|
|
7724
|
+
wechatClient: {
|
|
7725
|
+
running: client.running,
|
|
7726
|
+
version: client.version,
|
|
7727
|
+
loadedDirs: client.loadedDirs,
|
|
7728
|
+
// 客户端自报的构建 token(并列的第二来源;**不拿它推版本号**,编码规则没解出来)
|
|
7729
|
+
versionTokens: client.versionTokens,
|
|
7730
|
+
note: client.note,
|
|
7731
|
+
unusableVersionDirs: client.unusableVersionDirs,
|
|
7732
|
+
// 客户端**自己说**数据目录在哪(与配置不一致时说明两边不是同一份库)
|
|
7733
|
+
dataDirs: client.dataDirs,
|
|
7734
|
+
// 只报结论(true/false/null),**不带路径** —— 这条 schema 一直是不含路径的
|
|
7735
|
+
dataDirMatchesConfig,
|
|
7736
|
+
// `update.data` 在场 ⇒ 加载路径可能不由命令行决定:这条不确定必须跟着版本一起出去
|
|
7737
|
+
updateData: client.updateData,
|
|
7738
|
+
// 只报条数与最近时间(不读崩溃内容)
|
|
7739
|
+
crashRecords: client.crashRecords,
|
|
7740
|
+
caveats: client.caveats,
|
|
7741
|
+
},
|
|
6882
7742
|
};
|
|
6883
7743
|
console.log(JSON.stringify(data, null, 2));
|
|
6884
7744
|
return;
|
|
@@ -6891,7 +7751,7 @@ program
|
|
|
6891
7751
|
const pythonCommand = getPythonCommand();
|
|
6892
7752
|
let pythonOk = false;
|
|
6893
7753
|
try {
|
|
6894
|
-
const pyVer = execFileSync(pythonCommand, ['--version'], { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
7754
|
+
const pyVer = execFileSync(pythonCommand, ['--version'], { windowsHide: true, encoding: 'utf-8', timeout: 5000 }).trim();
|
|
6895
7755
|
console.log(chalk.white('Python: '), chalk.green(pyVer));
|
|
6896
7756
|
pythonOk = true;
|
|
6897
7757
|
}
|
|
@@ -6905,7 +7765,7 @@ program
|
|
|
6905
7765
|
let allOk = true;
|
|
6906
7766
|
for (const dep of deps) {
|
|
6907
7767
|
try {
|
|
6908
|
-
execFileSync(pythonCommand, ['-c', `import ${dep}`], { stdio: 'ignore', timeout: 5000 });
|
|
7768
|
+
execFileSync(pythonCommand, ['-c', `import ${dep}`], { windowsHide: true, stdio: 'ignore', timeout: 5000 });
|
|
6909
7769
|
console.log(` ${dep.padEnd(20)} ${chalk.green('✓')}`);
|
|
6910
7770
|
}
|
|
6911
7771
|
catch {
|
|
@@ -6928,7 +7788,7 @@ program
|
|
|
6928
7788
|
console.log(chalk.white('\n可选依赖(提升抓取成功率):'));
|
|
6929
7789
|
for (const dep of optDeps) {
|
|
6930
7790
|
try {
|
|
6931
|
-
execFileSync(pythonCommand, ['-c', `import ${dep}`], { stdio: 'ignore', timeout: 5000 });
|
|
7791
|
+
execFileSync(pythonCommand, ['-c', `import ${dep}`], { windowsHide: true, stdio: 'ignore', timeout: 5000 });
|
|
6932
7792
|
console.log(` ${dep.padEnd(20)} ${chalk.green('✓')}`);
|
|
6933
7793
|
}
|
|
6934
7794
|
catch {
|
|
@@ -6963,7 +7823,38 @@ program
|
|
|
6963
7823
|
console.log(chalk.gray(` ${label}: 未配置${hint}`));
|
|
6964
7824
|
}
|
|
6965
7825
|
}
|
|
6966
|
-
// 6.
|
|
7826
|
+
// 6. 微信客户端版本(`client` / `dataDirMatchesConfig` 在 action 开头已经算过)
|
|
7827
|
+
console.log(chalk.white('\n微信客户端:'));
|
|
7828
|
+
if (client.version) {
|
|
7829
|
+
// 并列报上客户端自报的构建 token:这是"哪个构建"的第二个来源(**不用它推版本号**)
|
|
7830
|
+
const token = client.versionTokens.length ? chalk.gray(` · 自报构建 ${client.versionTokens.join('、')}`) : '';
|
|
7831
|
+
console.log(chalk.green(` ✓ 运行时加载 ${client.version}`) + chalk.gray(` (${client.loadedDirs[0]})`) + token);
|
|
7832
|
+
}
|
|
7833
|
+
else {
|
|
7834
|
+
// **说"未知"而不是"没问题"**:取不到就说取不到,别给人一个假的安心
|
|
7835
|
+
console.log(chalk.gray(` ○ 版本未知 — ${client.note}`));
|
|
7836
|
+
}
|
|
7837
|
+
for (const dead of client.unusableVersionDirs) {
|
|
7838
|
+
console.log(chalk.yellow(` ! 残留版本目录 ${dead.dir}:只剩 ${dead.files} 个文件(没有 Weixin.dll)`));
|
|
7839
|
+
console.log(chalk.gray(' 指向它的加载会失败;如果客户端起不来,先看这里'));
|
|
7840
|
+
}
|
|
7841
|
+
// `update.data` 在场时上面那个版本**未必**是加载的那个 —— 这句话要跟着它一起出现
|
|
7842
|
+
for (const caveat of client.caveats) {
|
|
7843
|
+
console.log(chalk.yellow(` ⚠ ${caveat}`));
|
|
7844
|
+
}
|
|
7845
|
+
if (client.dataDirs.length) {
|
|
7846
|
+
console.log(chalk.gray(` · 客户端自报数据目录: ${client.dataDirs.join('、')}`));
|
|
7847
|
+
if (dataDirMatchesConfig === false) {
|
|
7848
|
+
// 不一致 = 两边可能不是同一份库(本机数据目录还有 D 盘软链,这条已经按真身比过)
|
|
7849
|
+
console.log(chalk.yellow(' ⚠ 与配置里那个数据库推出来的根**不是同一份** —— 先确认你在读的是客户端正在写的那份'));
|
|
7850
|
+
}
|
|
7851
|
+
}
|
|
7852
|
+
const crashes = client.crashRecords.reports + client.crashRecords.records;
|
|
7853
|
+
if (crashes > 0) {
|
|
7854
|
+
const when = client.crashRecords.newest ? client.crashRecords.newest.slice(0, 16).replace('T', ' ') : '时间读不到';
|
|
7855
|
+
console.log(chalk.gray(` · 客户端崩溃记录 ${crashes} 份(最近 ${when})—— 客户端崩过时数据读不出来,先看这条`));
|
|
7856
|
+
}
|
|
7857
|
+
// 7. Assistant (第二大脑)
|
|
6967
7858
|
console.log(chalk.white('\n第二大脑 Agent:'));
|
|
6968
7859
|
const aiKey = configService.get('deepseekApiKey');
|
|
6969
7860
|
const aiBase = configService.get('aiBaseUrl');
|
|
@@ -7184,7 +8075,8 @@ async function showInteractiveMenu() {
|
|
|
7184
8075
|
const { spawn } = await import('child_process');
|
|
7185
8076
|
const pipeline = join(resolvePackageRoot(), 'scripts', 'pipeline.py');
|
|
7186
8077
|
console.log(chalk.cyan(`\n📰 正在生成 ${dateStr} 公众号日报...\n`));
|
|
7187
|
-
const child = spawn(getPythonCommand(), [pipeline, '--date', dateStr, '--interest', 'AI'
|
|
8078
|
+
const child = spawn(getPythonCommand(), [pipeline, '--date', dateStr, '--interest', 'AI'], {
|
|
8079
|
+
windowsHide: true,
|
|
7188
8080
|
stdio: 'inherit',
|
|
7189
8081
|
env: pythonProcessEnv(apiKey),
|
|
7190
8082
|
});
|