weflow-cli 1.9.0 → 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 +948 -0
- package/OPERATIONS.md +65 -11
- package/README.en.md +14 -5
- package/README.md +21 -6
- package/bin/weflow-cli.ts +348 -39
- package/dist/bin/weflow-cli.js +363 -68
- package/dist/bin/weflow-cli.js.map +1 -1
- package/dist/mcp-server/index.d.ts.map +1 -1
- package/dist/mcp-server/index.js +19 -1
- 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 +35 -0
- package/dist/src/panel/server.js.map +1 -1
- package/dist/src/services/assistantService.d.ts +19 -0
- package/dist/src/services/assistantService.d.ts.map +1 -1
- package/dist/src/services/assistantService.js +20 -0
- package/dist/src/services/assistantService.js.map +1 -1
- package/dist/src/services/assistantTools.d.ts +27 -1
- package/dist/src/services/assistantTools.d.ts.map +1 -1
- package/dist/src/services/assistantTools.js +169 -35
- package/dist/src/services/assistantTools.js.map +1 -1
- package/dist/src/services/configService.d.ts +2 -0
- package/dist/src/services/configService.d.ts.map +1 -1
- package/dist/src/services/configService.js +3 -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/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/docs/CONTACT_DB_SCHEMA.md +1085 -0
- package/docs/DECISIONS.md +1274 -0
- package/docs/EXTENDING.md +69 -7
- 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 +2 -2
- package/docs/PROJECT_STATE.md +62 -5
- 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/index.ts +18 -3
- 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/index.html +12 -0
- package/resources/panel/mascot-blink-1.png +0 -0
- package/resources/panel/mascot-blink-2.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-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 +111 -0
- package/resources/panel/renderer.js +480 -8
- package/scripts/_utils.py +1203 -977
- package/scripts/biz_daily.py +25 -10
- package/scripts/chat_notes.py +15 -5
- package/scripts/check_readme_commands.py +426 -0
- package/scripts/compile_wiki.py +163 -57
- package/scripts/contact_schema.py +339 -0
- package/scripts/data/wechat_emoji.json +225 -0
- 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/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/wiki_lint.py +94 -17
- package/src/core/dbPathService.ts +22 -4
- package/src/panel/server.ts +33 -0
- package/src/services/assistantService.ts +26 -0
- package/src/services/assistantTools.ts +2452 -2319
- package/src/services/configService.ts +5 -2
- package/src/services/messageQuery.ts +29 -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/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';
|
|
@@ -25,6 +26,7 @@ import { authorizeOutbound, parseOutboundPolicy, performOutbound } from '../src/
|
|
|
25
26
|
import { applyDerivedNtKeys, enableFavorites, detectFavDbPath } from '../src/services/initKeyService.js';
|
|
26
27
|
import { disabledIds, readSkillBody, scanSkills, skillRoots, skillState } from '../src/services/assistantSkills.js';
|
|
27
28
|
import { assistantScenes, validateSceneId } from '../src/services/assistantScenes.js';
|
|
29
|
+
import { detectWechatClient, reportedDataDirs, samePath } from '../src/services/wechatClient.js';
|
|
28
30
|
const program = new Command();
|
|
29
31
|
function resolvePackageRoot() {
|
|
30
32
|
return resolvePackageRootFrom(import.meta.url);
|
|
@@ -62,6 +64,24 @@ function parseCliInteger(value, field, minimum, maximum, json = false) {
|
|
|
62
64
|
console.log(chalk.red(error));
|
|
63
65
|
process.exit(1);
|
|
64
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
|
+
}
|
|
65
85
|
function requireCliDate(value, json = false) {
|
|
66
86
|
try {
|
|
67
87
|
parseLocalDateOrIso(value);
|
|
@@ -611,9 +631,11 @@ program
|
|
|
611
631
|
// 读这一位的调用方按 false 走保守分支即可。
|
|
612
632
|
mcpDefaultReadOnly: false,
|
|
613
633
|
mcpSurface: {
|
|
614
|
-
//
|
|
615
|
-
//
|
|
616
|
-
|
|
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',
|
|
617
639
|
writesFiles: ['export_chat'],
|
|
618
640
|
// 逐个核实过(脚本里确实调云端模型的那些):
|
|
619
641
|
// who_owes_reply/search_chats → Jev;search_semantic → 阿里云百炼嵌入 + Jev 重排;draft_reply → Jev + DeepSeek。
|
|
@@ -758,8 +780,13 @@ program
|
|
|
758
780
|
console.log(chalk.gray(' 目录名未命中时将继续进行深度结构扫描,过程可能需要较长时间...'));
|
|
759
781
|
}
|
|
760
782
|
const detected = configuredDataRoot
|
|
761
|
-
? { success: true, path: configuredDataRoot }
|
|
762
|
-
: 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
|
+
});
|
|
763
790
|
if (!detected.success || !detected.path) {
|
|
764
791
|
console.log(chalk.red('✗ 未检测到微信数据目录'));
|
|
765
792
|
if (!opts.searchDrives && !opts.fullScan) {
|
|
@@ -779,7 +806,10 @@ program
|
|
|
779
806
|
}
|
|
780
807
|
process.exit(1);
|
|
781
808
|
}
|
|
782
|
-
|
|
809
|
+
// **说清是怎么找到的**:来源不同,可信度与后续排查都不同("客户端自己说的"最硬)
|
|
810
|
+
const howFound = detected.source === 'client' ? '(客户端自己报的)'
|
|
811
|
+
: detected.source === 'scan' ? '(全盘扫描找到的)' : '';
|
|
812
|
+
console.log(chalk.green(`✓ 检测到数据目录: ${detected.path}${howFound}`));
|
|
783
813
|
configService.set('dbPath', detected.path);
|
|
784
814
|
configService.set('dataVersion', '4.x');
|
|
785
815
|
// Step 2: 扫描账号
|
|
@@ -1039,6 +1069,7 @@ const configurableKeys = [
|
|
|
1039
1069
|
'skillDirs', 'skillDisabled',
|
|
1040
1070
|
'dailySources', 'dailySourceCategories',
|
|
1041
1071
|
'dailyExcludeTopics', 'dailyAiEnabled',
|
|
1072
|
+
'wechatMediaDownload',
|
|
1042
1073
|
'emoticonSeed',
|
|
1043
1074
|
];
|
|
1044
1075
|
function setConfigValue(key, value, quiet = false) {
|
|
@@ -1349,11 +1380,26 @@ program
|
|
|
1349
1380
|
? (await chatService.getMessagesInRange(talker, limit + offset, start, end)).slice(offset, offset + limit)
|
|
1350
1381
|
: await chatService.getMessages(talker, limit, offset);
|
|
1351
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
|
+
}
|
|
1352
1396
|
if (opts.json) {
|
|
1353
|
-
console.log(JSON.stringify({ success: true, talker, messages: [] }));
|
|
1397
|
+
console.log(JSON.stringify({ success: true, talker, messages: [], ...(note ? { note } : {}) }));
|
|
1354
1398
|
return;
|
|
1355
1399
|
}
|
|
1356
1400
|
console.log(chalk.gray('未找到消息'));
|
|
1401
|
+
if (note)
|
|
1402
|
+
console.log(chalk.yellow(note));
|
|
1357
1403
|
return;
|
|
1358
1404
|
}
|
|
1359
1405
|
if (opts.json) {
|
|
@@ -1699,6 +1745,53 @@ syncCmd
|
|
|
1699
1745
|
process.exit(1);
|
|
1700
1746
|
});
|
|
1701
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
|
+
});
|
|
1702
1795
|
program
|
|
1703
1796
|
.command('contacts')
|
|
1704
1797
|
.description('查看联系人列表')
|
|
@@ -2065,7 +2158,10 @@ program
|
|
|
2065
2158
|
}
|
|
2066
2159
|
}
|
|
2067
2160
|
else {
|
|
2068
|
-
|
|
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);
|
|
2069
2165
|
const result = await keyService.autoGetDbKey(timeout, (msg) => {
|
|
2070
2166
|
console.log(chalk.gray(` ${msg}`));
|
|
2071
2167
|
});
|
|
@@ -2288,7 +2384,7 @@ whitelistCmd
|
|
|
2288
2384
|
}
|
|
2289
2385
|
else {
|
|
2290
2386
|
try {
|
|
2291
|
-
wxid = await resolveTalker(target);
|
|
2387
|
+
wxid = await resolveTalker(target, !!opts.json, !!opts.json);
|
|
2292
2388
|
const sessions = await chatService.listSessions(undefined, 50);
|
|
2293
2389
|
const match = sessions.find(s => s.username === wxid);
|
|
2294
2390
|
if (match)
|
|
@@ -2497,7 +2593,7 @@ blacklistCmd
|
|
|
2497
2593
|
}
|
|
2498
2594
|
else {
|
|
2499
2595
|
try {
|
|
2500
|
-
wxid = await resolveTalker(target);
|
|
2596
|
+
wxid = await resolveTalker(target, !!opts.json, !!opts.json);
|
|
2501
2597
|
const sessions = await chatService.listSessions(undefined, 50);
|
|
2502
2598
|
const match = sessions.find(s => s.username === wxid);
|
|
2503
2599
|
if (match)
|
|
@@ -3958,6 +4054,7 @@ program
|
|
|
3958
4054
|
.option('-o, --output <dir>', '概念页输出目录', './output/wechat-vault/Wiki/Concepts')
|
|
3959
4055
|
.option('--api-key <key>', 'DeepSeek API key')
|
|
3960
4056
|
.option('--refresh-sources', '只把已有概念页的"来源"段补齐成当前全部来源(本地重写,**不调用模型**)')
|
|
4057
|
+
.option('--qualify-links', '把跨线同名的概念链接改成带路径的形式(本地重写,**不调用模型**)')
|
|
3961
4058
|
.option('--cards <dir...>', '产卡目录,可写多个;默认所有 output/*-notes(与索引的引用数同一口径)')
|
|
3962
4059
|
.option('--dry-run', '仅预览,不读取文章、调用 AI 或写入概念页')
|
|
3963
4060
|
.option('--yes', '确认调用 AI 并生成概念页')
|
|
@@ -3981,6 +4078,22 @@ program
|
|
|
3981
4078
|
});
|
|
3982
4079
|
return;
|
|
3983
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
|
+
}
|
|
3984
4097
|
const limit = parseCliInteger(opts.limit, 'limit', 1, 1000, opts.json);
|
|
3985
4098
|
const minRefs = parseCliInteger(opts.minRefs ?? '1', 'min-refs', 1, 50, opts.json);
|
|
3986
4099
|
await runConfirmedPythonMutation({
|
|
@@ -4008,8 +4121,9 @@ program
|
|
|
4008
4121
|
.option('--wiki-limit <n>', '概念编译数', '20')
|
|
4009
4122
|
.option('--source <name>', '仅处理指定公众号,可重复使用', (value, previous) => [...previous, value], [])
|
|
4010
4123
|
.option('--skip-classify', '跳过 AI 后处理')
|
|
4011
|
-
|
|
4012
|
-
.option('--
|
|
4124
|
+
// 这两个**默认不做**:Obsidian 自带的图谱视图是实时的,写一次 vault 图谱就重画一次。
|
|
4125
|
+
.option('--with-wiki', '把概念页写进 Obsidian Vault(默认不写:图谱只在你说了才变)')
|
|
4126
|
+
.option('--with-vault', '把当天内容副本同步进 Obsidian Vault(默认不写)')
|
|
4013
4127
|
.option('--skip-html', '跳过 HTML 阅读器生成')
|
|
4014
4128
|
.option('--skip-ai-report', '跳过 AI 深度阅读报告')
|
|
4015
4129
|
.option('--no-ai', '关闭全部 AI 调用,保留抓取和本地输出')
|
|
@@ -4101,10 +4215,10 @@ program
|
|
|
4101
4215
|
args.push('--source', source);
|
|
4102
4216
|
if (opts.skipClassify)
|
|
4103
4217
|
args.push('--skip-classify');
|
|
4104
|
-
if (opts.
|
|
4105
|
-
args.push('--
|
|
4106
|
-
if (opts.
|
|
4107
|
-
args.push('--
|
|
4218
|
+
if (opts.withWiki)
|
|
4219
|
+
args.push('--with-wiki');
|
|
4220
|
+
if (opts.withVault)
|
|
4221
|
+
args.push('--with-vault');
|
|
4108
4222
|
if (opts.skipHtml)
|
|
4109
4223
|
args.push('--skip-html');
|
|
4110
4224
|
if (opts.skipAiReport)
|
|
@@ -4346,16 +4460,30 @@ program.commands
|
|
|
4346
4460
|
.description('Vault 搜索 — 文章+概念+笔记')
|
|
4347
4461
|
.argument('<query>', '搜索关键词')
|
|
4348
4462
|
.option('--type <type>', 'all / article / concept / note', 'all')
|
|
4463
|
+
.option('--line <line>', 'wiki=文章线(默认) / chat=聊天线 / all=两条都搜', 'wiki')
|
|
4349
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 使用)')
|
|
4350
4475
|
.action(async (query, opts) => {
|
|
4351
4476
|
const pkgRoot = resolvePackageRoot();
|
|
4352
4477
|
const script = join(pkgRoot, 'scripts', 'vault_search.py');
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
const topK = parseCliInteger(opts.topK, 'top-k', 1, 100);
|
|
4358
|
-
|
|
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'] : [])]);
|
|
4359
4487
|
}));
|
|
4360
4488
|
// vault rag — RAG dialog over Vault
|
|
4361
4489
|
program.commands
|
|
@@ -4364,6 +4492,7 @@ program.commands
|
|
|
4364
4492
|
.description('Vault 问答 — 基于知识库的 AI 对话')
|
|
4365
4493
|
.argument('<question>', '问题')
|
|
4366
4494
|
.option('--top-k <n>', '检索条数', '8')
|
|
4495
|
+
.option('--line <line>', 'wiki=文章线(默认) / chat=聊天线 / all=两条都搜', 'wiki')
|
|
4367
4496
|
.option('--api-key <key>', 'DeepSeek API key')
|
|
4368
4497
|
.option('--dry-run', '仅预览,不读取知识库或调用 AI')
|
|
4369
4498
|
.option('--yes', '确认读取本地知识并发送筛选后的上下文到 AI')
|
|
@@ -4372,11 +4501,13 @@ program.commands
|
|
|
4372
4501
|
const pkgRoot = resolvePackageRoot();
|
|
4373
4502
|
const script = join(pkgRoot, 'scripts', 'vault_rag.py');
|
|
4374
4503
|
const topK = parseCliInteger(opts.topK, 'top-k', 1, 100, opts.json);
|
|
4504
|
+
requireCliChoice(opts.line, ['wiki', 'chat', 'all'], 'line', opts.json);
|
|
4375
4505
|
const preview = {
|
|
4376
4506
|
success: true,
|
|
4377
4507
|
dryRun: true,
|
|
4378
4508
|
action: 'vault.rag',
|
|
4379
4509
|
topK,
|
|
4510
|
+
line: opts.line,
|
|
4380
4511
|
readsLocalKnowledge: true,
|
|
4381
4512
|
usesAi: true,
|
|
4382
4513
|
sendsSelectedContextToAi: true,
|
|
@@ -4404,7 +4535,7 @@ program.commands
|
|
|
4404
4535
|
return;
|
|
4405
4536
|
}
|
|
4406
4537
|
}
|
|
4407
|
-
const args = ['--top-k', String(topK)];
|
|
4538
|
+
const args = ['--top-k', String(topK), '--line', opts.line];
|
|
4408
4539
|
if (opts.json)
|
|
4409
4540
|
args.push('--json');
|
|
4410
4541
|
const { execFile } = await import('child_process');
|
|
@@ -4724,6 +4855,26 @@ program
|
|
|
4724
4855
|
}));
|
|
4725
4856
|
});
|
|
4726
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
|
+
}
|
|
4727
4878
|
const wereadCmd = program
|
|
4728
4879
|
.command('weread')
|
|
4729
4880
|
.description('微信读书助手 — 书架、统计、笔记、搜索、书评、推荐');
|
|
@@ -4734,10 +4885,8 @@ wereadCmd
|
|
|
4734
4885
|
.action(async (opts) => {
|
|
4735
4886
|
const weread = await getWereadService();
|
|
4736
4887
|
const res = await weread.shelf();
|
|
4737
|
-
if (!res.ok)
|
|
4738
|
-
|
|
4739
|
-
process.exit(1);
|
|
4740
|
-
}
|
|
4888
|
+
if (!res.ok)
|
|
4889
|
+
failWeread(res, !!opts.json);
|
|
4741
4890
|
const d = res.data;
|
|
4742
4891
|
if (opts.json) {
|
|
4743
4892
|
console.log(JSON.stringify(d, null, 2));
|
|
@@ -4768,10 +4917,8 @@ wereadCmd
|
|
|
4768
4917
|
.action(async (opts) => {
|
|
4769
4918
|
const weread = await getWereadService();
|
|
4770
4919
|
const res = await weread.readData(opts.mode);
|
|
4771
|
-
if (!res.ok)
|
|
4772
|
-
|
|
4773
|
-
process.exit(1);
|
|
4774
|
-
}
|
|
4920
|
+
if (!res.ok)
|
|
4921
|
+
failWeread(res, !!opts.json);
|
|
4775
4922
|
const d = res.data;
|
|
4776
4923
|
if (opts.json) {
|
|
4777
4924
|
console.log(JSON.stringify(d, null, 2));
|
|
@@ -4820,10 +4967,8 @@ wereadCmd
|
|
|
4820
4967
|
const limit = parseCliInteger(opts.limit, 'limit', 1, 100, opts.json);
|
|
4821
4968
|
if (opts.bookId) {
|
|
4822
4969
|
const res = await weread.bookmarks(opts.bookId, limit);
|
|
4823
|
-
if (!res.ok)
|
|
4824
|
-
|
|
4825
|
-
process.exit(1);
|
|
4826
|
-
}
|
|
4970
|
+
if (!res.ok)
|
|
4971
|
+
failWeread(res, !!opts.json);
|
|
4827
4972
|
const d = res.data;
|
|
4828
4973
|
if (opts.json) {
|
|
4829
4974
|
console.log(JSON.stringify(d, null, 2));
|
|
@@ -4841,10 +4986,8 @@ wereadCmd
|
|
|
4841
4986
|
}
|
|
4842
4987
|
else {
|
|
4843
4988
|
const res = await weread.notebooks(50);
|
|
4844
|
-
if (!res.ok)
|
|
4845
|
-
|
|
4846
|
-
process.exit(1);
|
|
4847
|
-
}
|
|
4989
|
+
if (!res.ok)
|
|
4990
|
+
failWeread(res, !!opts.json);
|
|
4848
4991
|
const d = res.data;
|
|
4849
4992
|
if (opts.json) {
|
|
4850
4993
|
console.log(JSON.stringify(d, null, 2));
|
|
@@ -4869,10 +5012,8 @@ wereadCmd
|
|
|
4869
5012
|
const weread = await getWereadService();
|
|
4870
5013
|
const limit = parseCliInteger(opts.limit, 'limit', 1, 100, opts.json);
|
|
4871
5014
|
const res = await weread.search(keyword, limit);
|
|
4872
|
-
if (!res.ok)
|
|
4873
|
-
|
|
4874
|
-
process.exit(1);
|
|
4875
|
-
}
|
|
5015
|
+
if (!res.ok)
|
|
5016
|
+
failWeread(res, !!opts.json);
|
|
4876
5017
|
const d = res.data;
|
|
4877
5018
|
if (opts.json) {
|
|
4878
5019
|
console.log(JSON.stringify(d, null, 2));
|
|
@@ -4899,10 +5040,8 @@ wereadCmd
|
|
|
4899
5040
|
weread.bookInfo(bookId),
|
|
4900
5041
|
weread.getProgress(bookId),
|
|
4901
5042
|
]);
|
|
4902
|
-
if (!infoRes.ok)
|
|
4903
|
-
|
|
4904
|
-
process.exit(1);
|
|
4905
|
-
}
|
|
5043
|
+
if (!infoRes.ok)
|
|
5044
|
+
failWeread(infoRes, !!opts.json);
|
|
4906
5045
|
const b = infoRes.data;
|
|
4907
5046
|
if (opts.json) {
|
|
4908
5047
|
console.log(JSON.stringify(b, null, 2));
|
|
@@ -4935,10 +5074,8 @@ wereadCmd
|
|
|
4935
5074
|
const weread = await getWereadService();
|
|
4936
5075
|
const limit = parseCliInteger(opts.limit, 'limit', 1, 100, opts.json);
|
|
4937
5076
|
const res = await weread.reviews(bookId, limit);
|
|
4938
|
-
if (!res.ok)
|
|
4939
|
-
|
|
4940
|
-
process.exit(1);
|
|
4941
|
-
}
|
|
5077
|
+
if (!res.ok)
|
|
5078
|
+
failWeread(res, !!opts.json);
|
|
4942
5079
|
const d = res.data;
|
|
4943
5080
|
if (opts.json) {
|
|
4944
5081
|
console.log(JSON.stringify(d, null, 2));
|
|
@@ -4967,10 +5104,8 @@ wereadCmd
|
|
|
4967
5104
|
const res = opts.bookId
|
|
4968
5105
|
? await weread.similar(opts.bookId, limit)
|
|
4969
5106
|
: await weread.recommend(limit);
|
|
4970
|
-
if (!res.ok)
|
|
4971
|
-
|
|
4972
|
-
process.exit(1);
|
|
4973
|
-
}
|
|
5107
|
+
if (!res.ok)
|
|
5108
|
+
failWeread(res, !!opts.json);
|
|
4974
5109
|
const d = res.data;
|
|
4975
5110
|
if (opts.json) {
|
|
4976
5111
|
console.log(JSON.stringify(d, null, 2));
|
|
@@ -4994,10 +5129,8 @@ wereadCmd
|
|
|
4994
5129
|
.action(async (opts) => {
|
|
4995
5130
|
const weread = await getWereadService();
|
|
4996
5131
|
const res = await weread.profile();
|
|
4997
|
-
if (!res.ok)
|
|
4998
|
-
|
|
4999
|
-
process.exit(1);
|
|
5000
|
-
}
|
|
5132
|
+
if (!res.ok)
|
|
5133
|
+
failWeread(res, !!opts.json);
|
|
5001
5134
|
const p = res.data;
|
|
5002
5135
|
if (opts.json) {
|
|
5003
5136
|
console.log(JSON.stringify(p, null, 2));
|
|
@@ -5040,6 +5173,8 @@ program
|
|
|
5040
5173
|
.description('语义搜索知识库(聊天记录 + 文章)')
|
|
5041
5174
|
.argument('<query>', '搜索关键词')
|
|
5042
5175
|
.option('--top-k <n>', '返回数量', '10')
|
|
5176
|
+
.option('--line <line>', 'wiki=文章线(默认) / chat=聊天记录 / all=两条都搜', 'wiki')
|
|
5177
|
+
.option('--no-rerank', '不调用决策模型重排,只按向量/关键词相似度返回(回退到引入重排之前)')
|
|
5043
5178
|
.option('--api-key <key>', 'DashScope embedding API key')
|
|
5044
5179
|
.option('--dry-run', '仅预览,不读取索引或调用向量服务')
|
|
5045
5180
|
.option('--yes', '确认执行可能调用云端向量服务的搜索')
|
|
@@ -5051,11 +5186,14 @@ program
|
|
|
5051
5186
|
const pkgRoot = resolvePackageRoot();
|
|
5052
5187
|
const script = join(pkgRoot, 'scripts', 'semantic_search.py');
|
|
5053
5188
|
const topK = parseCliInteger(opts.topK, 'top-k', 1, 100, opts.json);
|
|
5189
|
+
requireCliChoice(opts.line, ['wiki', 'chat', 'all'], 'line', opts.json);
|
|
5054
5190
|
const preview = {
|
|
5055
5191
|
success: true,
|
|
5056
5192
|
dryRun: true,
|
|
5057
5193
|
action: 'semantic-search.query',
|
|
5058
5194
|
topK,
|
|
5195
|
+
line: opts.line,
|
|
5196
|
+
noRerank: opts.rerank === false,
|
|
5059
5197
|
readsLocalIndex: true,
|
|
5060
5198
|
mayUseCloudEmbedding: true,
|
|
5061
5199
|
};
|
|
@@ -5082,7 +5220,11 @@ program
|
|
|
5082
5220
|
return;
|
|
5083
5221
|
}
|
|
5084
5222
|
}
|
|
5085
|
-
|
|
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'] : [])];
|
|
5086
5228
|
try {
|
|
5087
5229
|
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
5088
5230
|
timeout: 60_000, maxBuffer: 10 * 1024 * 1024,
|
|
@@ -5185,6 +5327,7 @@ program
|
|
|
5185
5327
|
.description('RAG 智能助手 — 基于聊天记录和公众号的对话式问答')
|
|
5186
5328
|
.argument('[question]', '要问的问题(不传则进入交互模式)')
|
|
5187
5329
|
.option('--top-k <n>', '检索数量', '10')
|
|
5330
|
+
.option('--line <line>', 'wiki=文章/文章概念(默认) / chat=聊天记录 / all=两条都搜', 'wiki')
|
|
5188
5331
|
.option('--talker <name>', '限定联系人/群聊')
|
|
5189
5332
|
.option('--api-key <key>', 'AI API key')
|
|
5190
5333
|
.option('--dry-run', '仅预览,不读取知识库或调用 AI')
|
|
@@ -5197,11 +5340,13 @@ program
|
|
|
5197
5340
|
const pkgRoot = resolvePackageRoot();
|
|
5198
5341
|
const script = join(pkgRoot, 'scripts', 'rag_chat.py');
|
|
5199
5342
|
const topK = parseCliInteger(opts.topK, 'top-k', 1, 100, opts.json);
|
|
5343
|
+
requireCliChoice(opts.line, ['wiki', 'chat', 'all'], 'line', opts.json);
|
|
5200
5344
|
const preview = {
|
|
5201
5345
|
success: true,
|
|
5202
5346
|
dryRun: true,
|
|
5203
5347
|
action: 'rag-chat.query',
|
|
5204
5348
|
topK,
|
|
5349
|
+
line: opts.line,
|
|
5205
5350
|
interactiveRequired: !question,
|
|
5206
5351
|
readsLocalKnowledge: true,
|
|
5207
5352
|
usesCloudEmbedding: true,
|
|
@@ -5238,7 +5383,7 @@ program
|
|
|
5238
5383
|
}
|
|
5239
5384
|
const args = [script];
|
|
5240
5385
|
if (question) {
|
|
5241
|
-
args.push('--top-k', String(topK));
|
|
5386
|
+
args.push('--top-k', String(topK), '--line', opts.line);
|
|
5242
5387
|
if (opts.json)
|
|
5243
5388
|
args.push('--json');
|
|
5244
5389
|
try {
|
|
@@ -5259,7 +5404,7 @@ program
|
|
|
5259
5404
|
}
|
|
5260
5405
|
else {
|
|
5261
5406
|
// 交互模式:使用 spawn 保持终端交互
|
|
5262
|
-
args.push('--interactive', '--top-k', String(topK));
|
|
5407
|
+
args.push('--interactive', '--top-k', String(topK), '--line', opts.line);
|
|
5263
5408
|
const child = spawn(getPythonCommand(), args, {
|
|
5264
5409
|
windowsHide: true,
|
|
5265
5410
|
stdio: 'inherit',
|
|
@@ -5533,7 +5678,7 @@ program
|
|
|
5533
5678
|
// draft:判断在前、起草在后、**绝不发送**
|
|
5534
5679
|
program
|
|
5535
5680
|
.command('draft <talker>')
|
|
5536
|
-
.description('帮我起草回复:先判断(意图/风险/该不该给实质),再起草候选 ——
|
|
5681
|
+
.description('帮我起草回复:先判断(意图/风险/该不该给实质),再起草候选 —— **默认只产出文本;给了 --pick N --send 才会替你发送**')
|
|
5537
5682
|
.option('--count <n>', '要几条候选', '3')
|
|
5538
5683
|
.option('--pick <n>', '选第几条(从 1 数);不带 --send 时只把那条的完整原文打出来')
|
|
5539
5684
|
.option('--send', '把选中的那条发出去 —— **必须同时给 --pick**(不允许"自己挑一条发"),仍需 --yes/--dry-run')
|
|
@@ -5778,17 +5923,87 @@ program
|
|
|
5778
5923
|
process.exit(1);
|
|
5779
5924
|
}
|
|
5780
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
|
+
}));
|
|
5781
5991
|
// wiki lint:知识库体检(全本地:不联网、不调用模型)
|
|
5782
5992
|
program.commands.find(c => c.name() === 'wiki')?.addCommand(new Command('lint')
|
|
5783
5993
|
.description('知识库体检:断链、孤儿、空页、同名(本地,不联网)')
|
|
5784
|
-
|
|
5994
|
+
// 不给默认值:不传 `--dir` 时体检脚本自己的默认(清单里的**全部线**)。
|
|
5995
|
+
// 这里曾经默认只给文章线,而脚本默认两条线都体检 ⇒"半开半合":命令行跑出来
|
|
5996
|
+
// 与脚本跑出来口径不同,而两边都不报错。
|
|
5997
|
+
.option('--dir <dirs...>', '概念页目录(可重复;不传则体检清单里的全部线)')
|
|
5785
5998
|
.option('--json', '输出机器可读结果')
|
|
5786
5999
|
.action(async (opts) => {
|
|
5787
6000
|
const { execFile } = await import('child_process');
|
|
5788
6001
|
const { promisify } = await import('util');
|
|
5789
6002
|
const execFileAsync = promisify(execFile);
|
|
5790
6003
|
const script = join(resolvePackageRoot(), 'scripts', 'wiki_lint.py');
|
|
5791
|
-
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'] : [])];
|
|
5792
6007
|
try {
|
|
5793
6008
|
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
5794
6009
|
timeout: 120_000, maxBuffer: 20 * 1024 * 1024, env: pythonProcessEnv(),
|
|
@@ -5812,6 +6027,7 @@ program
|
|
|
5812
6027
|
.option('--limit <n>', '最多处理几个会话', '40')
|
|
5813
6028
|
.option('--dry-run', '仅预览:几个会话、多少字符会发给生成模型(只读本地,零出境)')
|
|
5814
6029
|
.option('--yes', '确认把对话发送给生成模型')
|
|
6030
|
+
.option('--transcribe-voice', '把缺转写的语音**本机**识别掉再产卡(本地 whisper,慢、吃 CPU、不出网;配合 --dry-run 就是只补转写、不调模型)')
|
|
5815
6031
|
.option('--json', '输出机器可读结果;执行仍需 --yes')
|
|
5816
6032
|
.action(async (opts) => {
|
|
5817
6033
|
const { execFile } = await import('child_process');
|
|
@@ -5847,6 +6063,7 @@ program
|
|
|
5847
6063
|
}
|
|
5848
6064
|
const args = [script, '--days', String(days), '--limit', String(limit),
|
|
5849
6065
|
...(confirmed ? ['--yes'] : []),
|
|
6066
|
+
...(opts.transcribeVoice ? ['--transcribe-voice'] : []),
|
|
5850
6067
|
...(opts.json ? ['--json'] : []),
|
|
5851
6068
|
...(opts.dryRun ? ['--dry-run'] : [])];
|
|
5852
6069
|
try {
|
|
@@ -6220,7 +6437,7 @@ program
|
|
|
6220
6437
|
};
|
|
6221
6438
|
const isComplete = (targetDate) => Object.values(artifactStatus(targetDate)).every(Boolean);
|
|
6222
6439
|
const runPipeline = (targetDate) => new Promise((resolve) => {
|
|
6223
|
-
const args = [pipeline, '--date', targetDate, '--engine', noAi ? 'local' : 'deepseek', '--interest', 'AI'
|
|
6440
|
+
const args = [pipeline, '--date', targetDate, '--engine', noAi ? 'local' : 'deepseek', '--interest', 'AI'];
|
|
6224
6441
|
if (noAi)
|
|
6225
6442
|
args.push('--no-ai');
|
|
6226
6443
|
// 与 --no-ai 不同:这一条保留判断(Jev),只关掉 LLM 的文字生成。
|
|
@@ -7380,6 +7597,11 @@ program
|
|
|
7380
7597
|
}
|
|
7381
7598
|
else {
|
|
7382
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;
|
|
7383
7605
|
}
|
|
7384
7606
|
console.log(chalk.cyan('\n可用的 MCP 工具:'));
|
|
7385
7607
|
const STATIC_TOOLS = [
|
|
@@ -7418,6 +7640,28 @@ program
|
|
|
7418
7640
|
.description('检查运行环境(Python、pip 依赖、数据库配置)')
|
|
7419
7641
|
.option('--json', '输出 JSON 格式,不包含数据库路径或密钥')
|
|
7420
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;
|
|
7421
7665
|
if (opts.json) {
|
|
7422
7666
|
const { execFileSync } = await import('child_process');
|
|
7423
7667
|
const requiredDependencies = ['sqlcipher3', 'html2text', 'zstandard', 'cryptography'];
|
|
@@ -7475,6 +7719,26 @@ program
|
|
|
7475
7719
|
messageChannelLoggedIn: !!configService.get('wechatOcToken'),
|
|
7476
7720
|
daemonRunning: daemonAlive,
|
|
7477
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
|
+
},
|
|
7478
7742
|
};
|
|
7479
7743
|
console.log(JSON.stringify(data, null, 2));
|
|
7480
7744
|
return;
|
|
@@ -7559,7 +7823,38 @@ program
|
|
|
7559
7823
|
console.log(chalk.gray(` ${label}: 未配置${hint}`));
|
|
7560
7824
|
}
|
|
7561
7825
|
}
|
|
7562
|
-
// 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 (第二大脑)
|
|
7563
7858
|
console.log(chalk.white('\n第二大脑 Agent:'));
|
|
7564
7859
|
const aiKey = configService.get('deepseekApiKey');
|
|
7565
7860
|
const aiBase = configService.get('aiBaseUrl');
|
|
@@ -7780,7 +8075,7 @@ async function showInteractiveMenu() {
|
|
|
7780
8075
|
const { spawn } = await import('child_process');
|
|
7781
8076
|
const pipeline = join(resolvePackageRoot(), 'scripts', 'pipeline.py');
|
|
7782
8077
|
console.log(chalk.cyan(`\n📰 正在生成 ${dateStr} 公众号日报...\n`));
|
|
7783
|
-
const child = spawn(getPythonCommand(), [pipeline, '--date', dateStr, '--interest', 'AI'
|
|
8078
|
+
const child = spawn(getPythonCommand(), [pipeline, '--date', dateStr, '--interest', 'AI'], {
|
|
7784
8079
|
windowsHide: true,
|
|
7785
8080
|
stdio: 'inherit',
|
|
7786
8081
|
env: pythonProcessEnv(apiKey),
|