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/bin/weflow-cli.ts
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'
|
|
@@ -26,6 +27,7 @@ import { applyDerivedNtKeys, enableFavorites, detectFavDbPath } from '../src/ser
|
|
|
26
27
|
import type { ChatSession } from '../src/types.js'
|
|
27
28
|
import { disabledIds, describeSkill, readSkillBody, scanSkills, skillRoots, skillState } from '../src/services/assistantSkills.js'
|
|
28
29
|
import { assistantScenes, validateSceneId, type Scene } from '../src/services/assistantScenes.js'
|
|
30
|
+
import { detectWechatClient, reportedDataDirs, samePath } from '../src/services/wechatClient.js'
|
|
29
31
|
|
|
30
32
|
const program = new Command()
|
|
31
33
|
|
|
@@ -66,6 +68,22 @@ function parseCliInteger(value: unknown, field: string, minimum: number, maximum
|
|
|
66
68
|
process.exit(1)
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
/** 校验一个"只能是这几个值之一"的参数。与 `parseCliInteger` / `requireCliDate` 同一约定:
|
|
72
|
+
* `--json` 时错误也得是一份 JSON —— 否则机器调用方在 stdout 上拿到的是彩色散文,解析不了。
|
|
73
|
+
*
|
|
74
|
+
* 抽成一个是有理由的:`--line` 的校验原本在四个命令里各写了一遍,其中三处只打散文(`vault rag`、
|
|
75
|
+
* 顶层 `search`、`chat`)—— 同一份判断写四遍,就有三遍漏掉了 `--json` 分支。
|
|
76
|
+
* 错误文案由 choices 生成,保证与原来逐字一致('line 必须是 wiki、chat 或 all')。
|
|
77
|
+
*/
|
|
78
|
+
function requireCliChoice(value: unknown, choices: readonly string[], field: string, json = false): string {
|
|
79
|
+
const text = String(value)
|
|
80
|
+
if (choices.includes(text)) return text
|
|
81
|
+
const error = `${field} 必须是 ${choices.slice(0, -1).join('、')} 或 ${choices[choices.length - 1]}`
|
|
82
|
+
if (json) console.log(JSON.stringify({ success: false, code: 'INVALID_ARGUMENT', field, error }))
|
|
83
|
+
else console.log(chalk.red(error))
|
|
84
|
+
process.exit(1)
|
|
85
|
+
}
|
|
86
|
+
|
|
69
87
|
function requireCliDate(value: string, json = false): string {
|
|
70
88
|
try {
|
|
71
89
|
parseLocalDateOrIso(value)
|
|
@@ -629,9 +647,11 @@ program
|
|
|
629
647
|
// 读这一位的调用方按 false 走保守分支即可。
|
|
630
648
|
mcpDefaultReadOnly: false,
|
|
631
649
|
mcpSurface: {
|
|
632
|
-
//
|
|
633
|
-
//
|
|
634
|
-
|
|
650
|
+
// 这句话**手工补过两回**:先是只写 save_memory,后来补上 `look_at_image`,第三次加
|
|
651
|
+
// `set_todo_status` 时又漏了 —— 每次都是"表变了、这句话没变",而且没有任何东西会红。
|
|
652
|
+
// 现在名字**逐个与 `MCP_EXCLUDED` 对齐**,并由 `test/assistant-tools.test.ts` 从源码里核:
|
|
653
|
+
// 那张表再加一项而这里没跟,测试就红(同下面 requiresConfirm 的处理方式)。
|
|
654
|
+
derivedFrom: 'assistant tools minus save_memory, look_at_image and set_todo_status',
|
|
635
655
|
writesFiles: ['export_chat'],
|
|
636
656
|
// 逐个核实过(脚本里确实调云端模型的那些):
|
|
637
657
|
// who_owes_reply/search_chats → Jev;search_semantic → 阿里云百炼嵌入 + Jev 重排;draft_reply → Jev + DeepSeek。
|
|
@@ -783,8 +803,13 @@ program
|
|
|
783
803
|
console.log(chalk.gray(' 目录名未命中时将继续进行深度结构扫描,过程可能需要较长时间...'))
|
|
784
804
|
}
|
|
785
805
|
const detected = configuredDataRoot
|
|
786
|
-
? { success: true, path: configuredDataRoot }
|
|
787
|
-
: await dbPathService.autoDetect({
|
|
806
|
+
? { success: true, path: configuredDataRoot, source: 'known' as const }
|
|
807
|
+
: await dbPathService.autoDetect({
|
|
808
|
+
searchDrives: opts.searchDrives, fullScan: opts.fullScan,
|
|
809
|
+
// **先问客户端自己**(它命令行上带着 `--wechat-files-path`):这是唯一"来自
|
|
810
|
+
// 客户端自己"的来源,其余都是按目录名猜。取不到就是空数组,走老路。
|
|
811
|
+
hintedRoots: await reportedDataDirs(),
|
|
812
|
+
})
|
|
788
813
|
if (!detected.success || !detected.path) {
|
|
789
814
|
console.log(chalk.red('✗ 未检测到微信数据目录'))
|
|
790
815
|
if (!opts.searchDrives && !opts.fullScan) {
|
|
@@ -802,7 +827,10 @@ program
|
|
|
802
827
|
}
|
|
803
828
|
process.exit(1)
|
|
804
829
|
}
|
|
805
|
-
|
|
830
|
+
// **说清是怎么找到的**:来源不同,可信度与后续排查都不同("客户端自己说的"最硬)
|
|
831
|
+
const howFound = detected.source === 'client' ? '(客户端自己报的)'
|
|
832
|
+
: detected.source === 'scan' ? '(全盘扫描找到的)' : ''
|
|
833
|
+
console.log(chalk.green(`✓ 检测到数据目录: ${detected.path}${howFound}`))
|
|
806
834
|
configService.set('dbPath', detected.path)
|
|
807
835
|
configService.set('dataVersion', '4.x')
|
|
808
836
|
|
|
@@ -1072,6 +1100,7 @@ const configurableKeys = [
|
|
|
1072
1100
|
'skillDirs', 'skillDisabled',
|
|
1073
1101
|
'dailySources', 'dailySourceCategories',
|
|
1074
1102
|
'dailyExcludeTopics', 'dailyAiEnabled',
|
|
1103
|
+
'wechatMediaDownload',
|
|
1075
1104
|
'emoticonSeed',
|
|
1076
1105
|
] as const
|
|
1077
1106
|
|
|
@@ -1369,8 +1398,25 @@ program
|
|
|
1369
1398
|
: await chatService.getMessages(talker, limit, offset)
|
|
1370
1399
|
|
|
1371
1400
|
if (messages.length === 0) {
|
|
1372
|
-
|
|
1401
|
+
// 拼错的 ID 会被 resolveTalker 原样放行 ⇒ 取不到消息。这里补一句话把两种情况分开,
|
|
1402
|
+
// 但**不改退出码、也不改 success**("没有消息"不是错误,脚本与 MCP 依赖这一点)。
|
|
1403
|
+
let note = ''
|
|
1404
|
+
if (looksLikeTalkerId(talkerInput)) {
|
|
1405
|
+
const [sessions, contacts] = await Promise.all([
|
|
1406
|
+
chatService.listSessions(talkerInput, 5),
|
|
1407
|
+
chatService.listContacts(talkerInput, 5),
|
|
1408
|
+
])
|
|
1409
|
+
note = emptyMessagesNote(talkerInput, {
|
|
1410
|
+
sessionUsernames: sessions.map(s => s.username),
|
|
1411
|
+
contactUsernames: contacts.map(c => c.username),
|
|
1412
|
+
})
|
|
1413
|
+
}
|
|
1414
|
+
if (opts.json) {
|
|
1415
|
+
console.log(JSON.stringify({ success: true, talker, messages: [], ...(note ? { note } : {}) }))
|
|
1416
|
+
return
|
|
1417
|
+
}
|
|
1373
1418
|
console.log(chalk.gray('未找到消息'))
|
|
1419
|
+
if (note) console.log(chalk.yellow(note))
|
|
1374
1420
|
return
|
|
1375
1421
|
}
|
|
1376
1422
|
|
|
@@ -1679,6 +1725,51 @@ syncCmd
|
|
|
1679
1725
|
})
|
|
1680
1726
|
|
|
1681
1727
|
// ==================== contacts ====================
|
|
1728
|
+
// contact-schema:读 contact.db 里**群**的 ext_buffer(本地只读:不联网、不调模型)
|
|
1729
|
+
//
|
|
1730
|
+
// 只解**实测验证过**的那几个字段(成员 userName/displayName/status/inviter + `#5`);
|
|
1731
|
+
// `#5` 曾经叫"附加参与者 id",实测是**成员 status 位 11 置位的那批 id**(本机 77/77 集合相等),
|
|
1732
|
+
// 所以输出改用 `statusBit11Ids`、不再说"额外参与者"(见 D-070)。
|
|
1733
|
+
// 顶层 `#3`/`#4` 的语义在本机被三次证伪(不是成员 id / 房间 id / 消息侧 id),所以脚本把它们
|
|
1734
|
+
// 原样放进 `unrecognized`,**不猜名字** —— 拿猜出来的名字写逻辑,就是拿不确定当事实。
|
|
1735
|
+
// 结构笔记见 `docs/CONTACT_DB_SCHEMA.md`,取舍见 D-067 / D-068 / D-070。
|
|
1736
|
+
//
|
|
1737
|
+
// `--contacts`(2026-10-07 加):同一个命令改读**联系人**的 `contact.extra_buffer`,输出这一轮验证的
|
|
1738
|
+
// 三个字段 —— `#5` 国家/地区码(ISO 3166-1 alpha-2)、`#13` = 该行 `biz_info.type`、`#41` = 资料最近更新时间。
|
|
1739
|
+
// `#4`/`#9` 已定性为"账号自身资料文本的汇集",是**内容**不是字段名,**故意不打印**(见 D-088)。
|
|
1740
|
+
program
|
|
1741
|
+
.command('contact-schema')
|
|
1742
|
+
.description('读 contact.db 的 ext_buffer:默认读群(成员 + #5);--contacts 读联系人(#5 地区码 / #13 服务类型 / #41 更新时间)—— 只解已验证字段,未识别的原样标注(本地、只读)')
|
|
1743
|
+
.option('-n, --limit <n>', '最多看几个(群 / 联系人)', '20')
|
|
1744
|
+
.option('--room <which>', '只看某个(群 / 联系人 的 username 或 id)')
|
|
1745
|
+
.option('--contacts', '改读联系人的 extra_buffer,而不是群')
|
|
1746
|
+
.option('--json', '输出机器可读结果')
|
|
1747
|
+
.action(async (opts) => {
|
|
1748
|
+
const { execFile } = await import('child_process')
|
|
1749
|
+
const { promisify } = await import('util')
|
|
1750
|
+
const execFileAsync = promisify(execFile)
|
|
1751
|
+
const script = join(resolvePackageRoot(), 'scripts', 'contact_schema.py')
|
|
1752
|
+
const limit = parseCliInteger(opts.limit, 'limit', 1, 100000, !!opts.json)
|
|
1753
|
+
const args = [script, '--limit', String(limit),
|
|
1754
|
+
...(opts.room ? ['--room', opts.room] : []),
|
|
1755
|
+
...(opts.contacts ? ['--contacts'] : []),
|
|
1756
|
+
...(opts.json ? ['--json'] : [])]
|
|
1757
|
+
try {
|
|
1758
|
+
const { stdout, stderr } = await execFileAsync(getPythonCommand(), args, {
|
|
1759
|
+
timeout: 300_000, maxBuffer: 64 * 1024 * 1024, env: pythonProcessEnv(),
|
|
1760
|
+
})
|
|
1761
|
+
process.stdout.write(stdout)
|
|
1762
|
+
if (stderr && !opts.json) process.stderr.write(stderr)
|
|
1763
|
+
} catch (error) {
|
|
1764
|
+
if (opts.json) {
|
|
1765
|
+
console.log(JSON.stringify({ success: false, code: 'CONTACT_SCHEMA_FAILED', action: 'contact-schema', error: safeSubprocessError(error) }))
|
|
1766
|
+
} else {
|
|
1767
|
+
console.error(chalk.red(`\n✗ ${safeSubprocessError(error)}`))
|
|
1768
|
+
}
|
|
1769
|
+
process.exit(1)
|
|
1770
|
+
}
|
|
1771
|
+
})
|
|
1772
|
+
|
|
1682
1773
|
program
|
|
1683
1774
|
.command('contacts')
|
|
1684
1775
|
.description('查看联系人列表')
|
|
@@ -2027,7 +2118,10 @@ program
|
|
|
2027
2118
|
process.exit(1)
|
|
2028
2119
|
}
|
|
2029
2120
|
} else {
|
|
2030
|
-
|
|
2121
|
+
// `opts.json` 也要传:这一行在 `--json` 下**不可达**(上面就 `INTERACTIVE_REQUIRED` 退出了),
|
|
2122
|
+
// 但少传一个是安静的 —— 规则统一成"校验 helper 一律带上它",判据才写得出来
|
|
2123
|
+
// (`test/json-error-surface.test.ts` 末尾那条静态检查)。
|
|
2124
|
+
const timeout = parseCliInteger(opts.timeout, 'timeout', 1000, 600000, opts.json)
|
|
2031
2125
|
const result = await keyService.autoGetDbKey(timeout, (msg) => {
|
|
2032
2126
|
console.log(chalk.gray(` ${msg}`))
|
|
2033
2127
|
})
|
|
@@ -2250,7 +2344,7 @@ whitelistCmd
|
|
|
2250
2344
|
wxid = target
|
|
2251
2345
|
} else {
|
|
2252
2346
|
try {
|
|
2253
|
-
wxid = await resolveTalker(target)
|
|
2347
|
+
wxid = await resolveTalker(target, !!opts.json, !!opts.json)
|
|
2254
2348
|
const sessions = await chatService.listSessions(undefined, 50)
|
|
2255
2349
|
const match = sessions.find(s => s.username === wxid)
|
|
2256
2350
|
if (match) displayName = match.displayName || target
|
|
@@ -2437,7 +2531,7 @@ blacklistCmd
|
|
|
2437
2531
|
wxid = target
|
|
2438
2532
|
} else {
|
|
2439
2533
|
try {
|
|
2440
|
-
wxid = await resolveTalker(target)
|
|
2534
|
+
wxid = await resolveTalker(target, !!opts.json, !!opts.json)
|
|
2441
2535
|
const sessions = await chatService.listSessions(undefined, 50)
|
|
2442
2536
|
const match = sessions.find(s => s.username === wxid)
|
|
2443
2537
|
if (match) displayName = match.displayName || target
|
|
@@ -3809,6 +3903,8 @@ program
|
|
|
3809
3903
|
.option('--api-key <key>', 'DeepSeek API key')
|
|
3810
3904
|
.option('--refresh-sources',
|
|
3811
3905
|
'只把已有概念页的"来源"段补齐成当前全部来源(本地重写,**不调用模型**)')
|
|
3906
|
+
.option('--qualify-links',
|
|
3907
|
+
'把跨线同名的概念链接改成带路径的形式(本地重写,**不调用模型**)')
|
|
3812
3908
|
.option('--cards <dir...>', '产卡目录,可写多个;默认所有 output/*-notes(与索引的引用数同一口径)')
|
|
3813
3909
|
.option('--dry-run', '仅预览,不读取文章、调用 AI 或写入概念页')
|
|
3814
3910
|
.option('--yes', '确认调用 AI 并生成概念页')
|
|
@@ -3834,6 +3930,22 @@ program
|
|
|
3834
3930
|
return
|
|
3835
3931
|
}
|
|
3836
3932
|
|
|
3933
|
+
if (opts.qualifyLinks) {
|
|
3934
|
+
// 本地消歧:两条线同名的那些链接按名字带路径(`Chat/Concepts/DeepSeek|DeepSeek`),
|
|
3935
|
+
// 免得 Obsidian 里那条裸链接命中另一条线的那张。**不调模型**,重跑幂等。
|
|
3936
|
+
await runConfirmedPythonMutation({
|
|
3937
|
+
action: 'wiki.qualifyLinks',
|
|
3938
|
+
script,
|
|
3939
|
+
args: ['--qualify-links', '--output', opts.output],
|
|
3940
|
+
cliOptions: opts,
|
|
3941
|
+
preview: { readsLocalKnowledge: true, rewritesExistingPages: true, usesAi: false },
|
|
3942
|
+
confirmationMessage: `确认把 ${opts.output} 里跨线同名的概念链接改成带路径的形式?`
|
|
3943
|
+
+ '(本地重写,不调用 AI)',
|
|
3944
|
+
apiKey: opts.apiKey,
|
|
3945
|
+
timeout: 300_000,
|
|
3946
|
+
})
|
|
3947
|
+
return
|
|
3948
|
+
}
|
|
3837
3949
|
const limit = parseCliInteger(opts.limit, 'limit', 1, 1000, opts.json)
|
|
3838
3950
|
const minRefs = parseCliInteger(opts.minRefs ?? '1', 'min-refs', 1, 50, opts.json)
|
|
3839
3951
|
await runConfirmedPythonMutation({
|
|
@@ -3864,8 +3976,9 @@ program
|
|
|
3864
3976
|
.option('--wiki-limit <n>', '概念编译数', '20')
|
|
3865
3977
|
.option('--source <name>', '仅处理指定公众号,可重复使用', (value: string, previous: string[]) => [...previous, value], [] as string[])
|
|
3866
3978
|
.option('--skip-classify', '跳过 AI 后处理')
|
|
3867
|
-
|
|
3868
|
-
.option('--
|
|
3979
|
+
// 这两个**默认不做**:Obsidian 自带的图谱视图是实时的,写一次 vault 图谱就重画一次。
|
|
3980
|
+
.option('--with-wiki', '把概念页写进 Obsidian Vault(默认不写:图谱只在你说了才变)')
|
|
3981
|
+
.option('--with-vault', '把当天内容副本同步进 Obsidian Vault(默认不写)')
|
|
3869
3982
|
.option('--skip-html', '跳过 HTML 阅读器生成')
|
|
3870
3983
|
.option('--skip-ai-report', '跳过 AI 深度阅读报告')
|
|
3871
3984
|
.option('--no-ai', '关闭全部 AI 调用,保留抓取和本地输出')
|
|
@@ -3947,8 +4060,8 @@ program
|
|
|
3947
4060
|
if (opts.date) args.push('--date', opts.date)
|
|
3948
4061
|
for (const source of sources) args.push('--source', source)
|
|
3949
4062
|
if (opts.skipClassify) args.push('--skip-classify')
|
|
3950
|
-
if (opts.
|
|
3951
|
-
if (opts.
|
|
4063
|
+
if (opts.withWiki) args.push('--with-wiki')
|
|
4064
|
+
if (opts.withVault) args.push('--with-vault')
|
|
3952
4065
|
if (opts.skipHtml) args.push('--skip-html')
|
|
3953
4066
|
if (opts.skipAiReport) args.push('--skip-ai-report')
|
|
3954
4067
|
if (noAi) args.push('--no-ai')
|
|
@@ -4184,16 +4297,30 @@ program
|
|
|
4184
4297
|
.description('Vault 搜索 — 文章+概念+笔记')
|
|
4185
4298
|
.argument('<query>', '搜索关键词')
|
|
4186
4299
|
.option('--type <type>', 'all / article / concept / note', 'all')
|
|
4300
|
+
.option('--line <line>', 'wiki=文章线(默认) / chat=聊天线 / all=两条都搜', 'wiki')
|
|
4187
4301
|
.option('--top-k <n>', '返回数量', '10')
|
|
4302
|
+
// `vault_search.py` 一直有 `--days`(它的 docstring 自己就举 `--days 30`),CLI 从来没接 ——
|
|
4303
|
+
// 于是"只搜最近一周"这件事在这条路上做不到,窗口被脚本默认值(90)钉死。与 `search-index --days`
|
|
4304
|
+
// 同形:窗口的运行值由 CLI 说了算,默认值与脚本一致。
|
|
4305
|
+
.option('--days <n>', '日报文章取最近多少天(默认 90)', '90')
|
|
4306
|
+
// 这条 `--json` 曾经**不存在,而 JSON 又是唯一的输出**:参数里硬编码着 `'--json'`,
|
|
4307
|
+
// 人类模式虽然脚本里写着(`vault_search.py` 不带 `--json` 时打印带相关度的可读列表)
|
|
4308
|
+
// 却没有任何入口能到达,用户显式敲 `--json` 还会拿到 `unknown option` —— 与
|
|
4309
|
+
// `docs/EXTENDING.md` 那条"脚本或 AI 会驱动的命令都有 `--json` 模式"正相反。
|
|
4310
|
+
// 现在按兄弟命令(`vault rag`、顶层 `search`)的形状来:默认人类可读,`--json` 才转发。
|
|
4311
|
+
.option('--json', '输出 JSON 格式(含本地路径与标题,供本机脚本/AI 使用)')
|
|
4188
4312
|
.action(async (query, opts) => {
|
|
4189
4313
|
const pkgRoot = resolvePackageRoot()
|
|
4190
4314
|
const script = join(pkgRoot, 'scripts', 'vault_search.py')
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
const topK = parseCliInteger(opts.topK, 'top-k', 1, 100)
|
|
4196
|
-
|
|
4315
|
+
// 校验分支也要认 `--json`(`requireCliChoice` 里就带着这条约定):
|
|
4316
|
+
// 否则 JSON 模式下 stdout 是一行彩色散文,脚本解析不了。
|
|
4317
|
+
requireCliChoice(opts.type, ['all', 'article', 'concept', 'note'], 'type', opts.json)
|
|
4318
|
+
requireCliChoice(opts.line, ['wiki', 'chat', 'all'], 'line', opts.json)
|
|
4319
|
+
const topK = parseCliInteger(opts.topK, 'top-k', 1, 100, opts.json)
|
|
4320
|
+
const days = parseCliInteger(opts.days, 'days', 1, 3650, opts.json)
|
|
4321
|
+
await runPythonCmd(script, [query, '--type', opts.type, '--line', opts.line,
|
|
4322
|
+
'--top-k', String(topK), '--days', String(days),
|
|
4323
|
+
...(opts.json ? ['--json'] : [])])
|
|
4197
4324
|
})
|
|
4198
4325
|
)
|
|
4199
4326
|
|
|
@@ -4205,6 +4332,7 @@ program
|
|
|
4205
4332
|
.description('Vault 问答 — 基于知识库的 AI 对话')
|
|
4206
4333
|
.argument('<question>', '问题')
|
|
4207
4334
|
.option('--top-k <n>', '检索条数', '8')
|
|
4335
|
+
.option('--line <line>', 'wiki=文章线(默认) / chat=聊天线 / all=两条都搜', 'wiki')
|
|
4208
4336
|
.option('--api-key <key>', 'DeepSeek API key')
|
|
4209
4337
|
.option('--dry-run', '仅预览,不读取知识库或调用 AI')
|
|
4210
4338
|
.option('--yes', '确认读取本地知识并发送筛选后的上下文到 AI')
|
|
@@ -4213,11 +4341,13 @@ program
|
|
|
4213
4341
|
const pkgRoot = resolvePackageRoot()
|
|
4214
4342
|
const script = join(pkgRoot, 'scripts', 'vault_rag.py')
|
|
4215
4343
|
const topK = parseCliInteger(opts.topK, 'top-k', 1, 100, opts.json)
|
|
4344
|
+
requireCliChoice(opts.line, ['wiki', 'chat', 'all'], 'line', opts.json)
|
|
4216
4345
|
const preview = {
|
|
4217
4346
|
success: true,
|
|
4218
4347
|
dryRun: true,
|
|
4219
4348
|
action: 'vault.rag',
|
|
4220
4349
|
topK,
|
|
4350
|
+
line: opts.line,
|
|
4221
4351
|
readsLocalKnowledge: true,
|
|
4222
4352
|
usesAi: true,
|
|
4223
4353
|
sendsSelectedContextToAi: true,
|
|
@@ -4243,7 +4373,7 @@ program
|
|
|
4243
4373
|
return
|
|
4244
4374
|
}
|
|
4245
4375
|
}
|
|
4246
|
-
const args = ['--top-k', String(topK)]
|
|
4376
|
+
const args = ['--top-k', String(topK), '--line', opts.line]
|
|
4247
4377
|
if (opts.json) args.push('--json')
|
|
4248
4378
|
const { execFile } = await import('child_process')
|
|
4249
4379
|
const { promisify } = await import('util')
|
|
@@ -4548,6 +4678,25 @@ program
|
|
|
4548
4678
|
})
|
|
4549
4679
|
|
|
4550
4680
|
// ==================== weread ====================
|
|
4681
|
+
|
|
4682
|
+
/** 微信读书服务失败时的统一出口。
|
|
4683
|
+
*
|
|
4684
|
+
* 9 个调用点(shelf / stats / notes×2 / search / book / review / discover / profile)原来都是一行式
|
|
4685
|
+
* 分支:把彩色散文打在 stdout 上再 `process.exit(1)`,**没有** json 支,而各命令的 `if (opts.json)`
|
|
4686
|
+
* 分支在它后面 ⇒ `weread shelf --json` 失败时 stdout 上只有一行 `✗ …`,调用方解析不了,也不知道
|
|
4687
|
+
* 是哪个命令失败了。全库其余同类出口都是"机器支发 JSON + exit"。
|
|
4688
|
+
*
|
|
4689
|
+
* (这里刻意**不**把那行旧代码原样抄出来:`test/json-error-surface.test.ts` 有一条绊线在扫那个形状,
|
|
4690
|
+
* 而注释里留一份字面量会让判据读到它 —— 今晚已经在另一条判据上踩过这个坑。)
|
|
4691
|
+
*
|
|
4692
|
+
* 人看的那一支保持原样(`console.log`,不动它的落点):把 CLI 的**错误**统一改走 stderr 是另一件事,
|
|
4693
|
+
* 现在库里两种都有(多数 `console.error`、少数 `console.log`),不在这条修复里顺手改。
|
|
4694
|
+
*/
|
|
4695
|
+
function failWeread(res: { error?: string }, json = false): never {
|
|
4696
|
+
if (json) console.log(JSON.stringify({ success: false, code: 'WEREAD_FAILED', error: res.error }))
|
|
4697
|
+
else console.log(chalk.red(`✗ ${res.error}`))
|
|
4698
|
+
process.exit(1)
|
|
4699
|
+
}
|
|
4551
4700
|
const wereadCmd = program
|
|
4552
4701
|
.command('weread')
|
|
4553
4702
|
.description('微信读书助手 — 书架、统计、笔记、搜索、书评、推荐')
|
|
@@ -4559,7 +4708,7 @@ program
|
|
|
4559
4708
|
.action(async (opts) => {
|
|
4560
4709
|
const weread = await getWereadService()
|
|
4561
4710
|
const res = await weread.shelf()
|
|
4562
|
-
if (!res.ok)
|
|
4711
|
+
if (!res.ok) failWeread(res, !!opts.json)
|
|
4563
4712
|
const d = res.data!
|
|
4564
4713
|
if (opts.json) { console.log(JSON.stringify(d, null, 2)); return }
|
|
4565
4714
|
|
|
@@ -4588,7 +4737,7 @@ program
|
|
|
4588
4737
|
.action(async (opts) => {
|
|
4589
4738
|
const weread = await getWereadService()
|
|
4590
4739
|
const res = await weread.readData(opts.mode)
|
|
4591
|
-
if (!res.ok)
|
|
4740
|
+
if (!res.ok) failWeread(res, !!opts.json)
|
|
4592
4741
|
const d = res.data!
|
|
4593
4742
|
if (opts.json) { console.log(JSON.stringify(d, null, 2)); return }
|
|
4594
4743
|
|
|
@@ -4636,7 +4785,7 @@ program
|
|
|
4636
4785
|
const limit = parseCliInteger(opts.limit, 'limit', 1, 100, opts.json)
|
|
4637
4786
|
if (opts.bookId) {
|
|
4638
4787
|
const res = await weread.bookmarks(opts.bookId, limit)
|
|
4639
|
-
if (!res.ok)
|
|
4788
|
+
if (!res.ok) failWeread(res, !!opts.json)
|
|
4640
4789
|
const d = res.data!
|
|
4641
4790
|
if (opts.json) { console.log(JSON.stringify(d, null, 2)); return }
|
|
4642
4791
|
console.log(chalk.cyan(`📝 划线笔记 (${d.updated?.length || 0} 条):\n`))
|
|
@@ -4649,7 +4798,7 @@ program
|
|
|
4649
4798
|
}
|
|
4650
4799
|
} else {
|
|
4651
4800
|
const res = await weread.notebooks(50)
|
|
4652
|
-
if (!res.ok)
|
|
4801
|
+
if (!res.ok) failWeread(res, !!opts.json)
|
|
4653
4802
|
const d = res.data!
|
|
4654
4803
|
if (opts.json) { console.log(JSON.stringify(d, null, 2)); return }
|
|
4655
4804
|
console.log(chalk.cyan(`📝 笔记本 (${d.books?.length || 0} 本有笔记):\n`))
|
|
@@ -4672,7 +4821,7 @@ program
|
|
|
4672
4821
|
const weread = await getWereadService()
|
|
4673
4822
|
const limit = parseCliInteger(opts.limit, 'limit', 1, 100, opts.json)
|
|
4674
4823
|
const res = await weread.search(keyword, limit)
|
|
4675
|
-
if (!res.ok)
|
|
4824
|
+
if (!res.ok) failWeread(res, !!opts.json)
|
|
4676
4825
|
const d = res.data!
|
|
4677
4826
|
if (opts.json) { console.log(JSON.stringify(d, null, 2)); return }
|
|
4678
4827
|
console.log(chalk.cyan(`🔍 搜索: "${keyword}" (${d.books?.length || 0} 条):\n`))
|
|
@@ -4696,7 +4845,7 @@ program
|
|
|
4696
4845
|
weread.bookInfo(bookId),
|
|
4697
4846
|
weread.getProgress(bookId),
|
|
4698
4847
|
])
|
|
4699
|
-
if (!infoRes.ok)
|
|
4848
|
+
if (!infoRes.ok) failWeread(infoRes, !!opts.json)
|
|
4700
4849
|
const b = infoRes.data!
|
|
4701
4850
|
if (opts.json) { console.log(JSON.stringify(b, null, 2)); return }
|
|
4702
4851
|
|
|
@@ -4722,7 +4871,7 @@ program
|
|
|
4722
4871
|
const weread = await getWereadService()
|
|
4723
4872
|
const limit = parseCliInteger(opts.limit, 'limit', 1, 100, opts.json)
|
|
4724
4873
|
const res = await weread.reviews(bookId, limit)
|
|
4725
|
-
if (!res.ok)
|
|
4874
|
+
if (!res.ok) failWeread(res, !!opts.json)
|
|
4726
4875
|
const d = res.data!
|
|
4727
4876
|
if (opts.json) { console.log(JSON.stringify(d, null, 2)); return }
|
|
4728
4877
|
console.log(chalk.cyan(`💬 书评 (${d.reviews?.length || 0} 条):\n`))
|
|
@@ -4748,7 +4897,7 @@ program
|
|
|
4748
4897
|
const res = opts.bookId
|
|
4749
4898
|
? await weread.similar(opts.bookId, limit)
|
|
4750
4899
|
: await weread.recommend(limit)
|
|
4751
|
-
if (!res.ok)
|
|
4900
|
+
if (!res.ok) failWeread(res, !!opts.json)
|
|
4752
4901
|
const d = res.data!
|
|
4753
4902
|
if (opts.json) { console.log(JSON.stringify(d, null, 2)); return }
|
|
4754
4903
|
const label = opts.bookId ? '相似推荐' : '个性化推荐'
|
|
@@ -4769,7 +4918,7 @@ program
|
|
|
4769
4918
|
.action(async (opts) => {
|
|
4770
4919
|
const weread = await getWereadService()
|
|
4771
4920
|
const res = await weread.profile()
|
|
4772
|
-
if (!res.ok)
|
|
4921
|
+
if (!res.ok) failWeread(res, !!opts.json)
|
|
4773
4922
|
const p = res.data!
|
|
4774
4923
|
if (opts.json) { console.log(JSON.stringify(p, null, 2)); return }
|
|
4775
4924
|
|
|
@@ -4812,6 +4961,8 @@ program
|
|
|
4812
4961
|
.description('语义搜索知识库(聊天记录 + 文章)')
|
|
4813
4962
|
.argument('<query>', '搜索关键词')
|
|
4814
4963
|
.option('--top-k <n>', '返回数量', '10')
|
|
4964
|
+
.option('--line <line>', 'wiki=文章线(默认) / chat=聊天记录 / all=两条都搜', 'wiki')
|
|
4965
|
+
.option('--no-rerank', '不调用决策模型重排,只按向量/关键词相似度返回(回退到引入重排之前)')
|
|
4815
4966
|
.option('--api-key <key>', 'DashScope embedding API key')
|
|
4816
4967
|
.option('--dry-run', '仅预览,不读取索引或调用向量服务')
|
|
4817
4968
|
.option('--yes', '确认执行可能调用云端向量服务的搜索')
|
|
@@ -4823,11 +4974,14 @@ program
|
|
|
4823
4974
|
const pkgRoot = resolvePackageRoot()
|
|
4824
4975
|
const script = join(pkgRoot, 'scripts', 'semantic_search.py')
|
|
4825
4976
|
const topK = parseCliInteger(opts.topK, 'top-k', 1, 100, opts.json)
|
|
4977
|
+
requireCliChoice(opts.line, ['wiki', 'chat', 'all'], 'line', opts.json)
|
|
4826
4978
|
const preview = {
|
|
4827
4979
|
success: true,
|
|
4828
4980
|
dryRun: true,
|
|
4829
4981
|
action: 'semantic-search.query',
|
|
4830
4982
|
topK,
|
|
4983
|
+
line: opts.line,
|
|
4984
|
+
noRerank: opts.rerank === false,
|
|
4831
4985
|
readsLocalIndex: true,
|
|
4832
4986
|
mayUseCloudEmbedding: true,
|
|
4833
4987
|
}
|
|
@@ -4852,7 +5006,11 @@ program
|
|
|
4852
5006
|
return
|
|
4853
5007
|
}
|
|
4854
5008
|
}
|
|
4855
|
-
|
|
5009
|
+
// `--no-rerank` 是**旗标**(不是密钥)⇒ 可以走 argv;查询本身仍然只走 env(见 `privacy-paths` 那条规矩)。
|
|
5010
|
+
// 接这条通道之前,`OPERATIONS.md` 一直在教用户敲 `search --no-rerank`,而 CLI 会答 `unknown option`
|
|
5011
|
+
// —— 同一份文档里 `--line` 是通的,所以它看起来像"能用的旗标"(2026-10-11 由文档命令面那道检查发现)。
|
|
5012
|
+
const args: string[] = [script, 'search', '--top-k', String(topK), '--line', opts.line,
|
|
5013
|
+
...(opts.rerank === false ? ['--no-rerank'] : [])]
|
|
4856
5014
|
try {
|
|
4857
5015
|
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
4858
5016
|
timeout: 60_000, maxBuffer: 10 * 1024 * 1024,
|
|
@@ -4950,6 +5108,7 @@ program
|
|
|
4950
5108
|
.description('RAG 智能助手 — 基于聊天记录和公众号的对话式问答')
|
|
4951
5109
|
.argument('[question]', '要问的问题(不传则进入交互模式)')
|
|
4952
5110
|
.option('--top-k <n>', '检索数量', '10')
|
|
5111
|
+
.option('--line <line>', 'wiki=文章/文章概念(默认) / chat=聊天记录 / all=两条都搜', 'wiki')
|
|
4953
5112
|
.option('--talker <name>', '限定联系人/群聊')
|
|
4954
5113
|
.option('--api-key <key>', 'AI API key')
|
|
4955
5114
|
.option('--dry-run', '仅预览,不读取知识库或调用 AI')
|
|
@@ -4962,11 +5121,13 @@ program
|
|
|
4962
5121
|
const pkgRoot = resolvePackageRoot()
|
|
4963
5122
|
const script = join(pkgRoot, 'scripts', 'rag_chat.py')
|
|
4964
5123
|
const topK = parseCliInteger(opts.topK, 'top-k', 1, 100, opts.json)
|
|
5124
|
+
requireCliChoice(opts.line, ['wiki', 'chat', 'all'], 'line', opts.json)
|
|
4965
5125
|
const preview = {
|
|
4966
5126
|
success: true,
|
|
4967
5127
|
dryRun: true,
|
|
4968
5128
|
action: 'rag-chat.query',
|
|
4969
5129
|
topK,
|
|
5130
|
+
line: opts.line,
|
|
4970
5131
|
interactiveRequired: !question,
|
|
4971
5132
|
readsLocalKnowledge: true,
|
|
4972
5133
|
usesCloudEmbedding: true,
|
|
@@ -5001,7 +5162,7 @@ program
|
|
|
5001
5162
|
}
|
|
5002
5163
|
const args: string[] = [script]
|
|
5003
5164
|
if (question) {
|
|
5004
|
-
args.push('--top-k', String(topK))
|
|
5165
|
+
args.push('--top-k', String(topK), '--line', opts.line)
|
|
5005
5166
|
if (opts.json) args.push('--json')
|
|
5006
5167
|
try {
|
|
5007
5168
|
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
@@ -5019,7 +5180,7 @@ program
|
|
|
5019
5180
|
}
|
|
5020
5181
|
} else {
|
|
5021
5182
|
// 交互模式:使用 spawn 保持终端交互
|
|
5022
|
-
args.push('--interactive', '--top-k', String(topK))
|
|
5183
|
+
args.push('--interactive', '--top-k', String(topK), '--line', opts.line)
|
|
5023
5184
|
const child = spawn(getPythonCommand(), args, {
|
|
5024
5185
|
windowsHide: true,
|
|
5025
5186
|
stdio: 'inherit',
|
|
@@ -5288,7 +5449,7 @@ program
|
|
|
5288
5449
|
// draft:判断在前、起草在后、**绝不发送**
|
|
5289
5450
|
program
|
|
5290
5451
|
.command('draft <talker>')
|
|
5291
|
-
.description('帮我起草回复:先判断(意图/风险/该不该给实质),再起草候选 ——
|
|
5452
|
+
.description('帮我起草回复:先判断(意图/风险/该不该给实质),再起草候选 —— **默认只产出文本;给了 --pick N --send 才会替你发送**')
|
|
5292
5453
|
.option('--count <n>', '要几条候选', '3')
|
|
5293
5454
|
.option('--pick <n>', '选第几条(从 1 数);不带 --send 时只把那条的完整原文打出来')
|
|
5294
5455
|
.option('--send', '把选中的那条发出去 —— **必须同时给 --pick**(不允许"自己挑一条发"),仍需 --yes/--dry-run')
|
|
@@ -5510,18 +5671,86 @@ program
|
|
|
5510
5671
|
}
|
|
5511
5672
|
})
|
|
5512
5673
|
|
|
5674
|
+
// wiki graph:把概念图谱导成一张**自包含**的 3D 页面(全本地:只读 Vault、不联网、不调用模型)
|
|
5675
|
+
//
|
|
5676
|
+
// 自包含是硬要求,不是顺手:页面用 `file://` 双击打开,那时 `fetch` 会被 CORS 挡死,
|
|
5677
|
+
// 所以库、数据、坐标全部内联进去 —— 也因此三个库随 npm 包发(见 resources/js/graph3d/NOTICE.txt)。
|
|
5678
|
+
// 布局(250 tick 实测 55 秒)在构建期用 Node 算完并缓存,打开页面即用。
|
|
5679
|
+
program.commands.find(c => c.name() === 'wiki')?.addCommand(
|
|
5680
|
+
new Command('graph')
|
|
5681
|
+
.description('把概念图谱导成一张自包含的图谱页面(只读本地、不联网;生成的 HTML 双击就能逛)。--flat 出 2D 版')
|
|
5682
|
+
.option('--vault <dir>', 'Vault 根目录(默认 output/wechat-vault)')
|
|
5683
|
+
.option('-o, --out <file>', '页面写到哪(默认 output/knowledge-graph-3d.html)')
|
|
5684
|
+
.option('--ticks <n>', '力导向迭代次数(默认 250,越多越舒展也越慢)')
|
|
5685
|
+
.option('--cache <dir>', '布局缓存目录(派生数据;测试/多份图分开放时用)')
|
|
5686
|
+
.option('--flat', '2D 版:平面图(更像 Obsidian 那种),canvas 渲染,输出默认 knowledge-graph-2d.html')
|
|
5687
|
+
.option('--min-degree <n>', '只画在原图里连接数 ≥N 的概念(去掉细枝看骨架;0 = 全画)')
|
|
5688
|
+
.option('--line <which>', '只画一条线:all / wiki / chat')
|
|
5689
|
+
.option('--dry-run', '只报概念与链接数:不写文件、也不算布局(本地,零改动)')
|
|
5690
|
+
.option('--open', '生成后用默认浏览器打开它')
|
|
5691
|
+
.option('--json', '输出机器可读结果')
|
|
5692
|
+
.action(async (opts) => {
|
|
5693
|
+
const { execFile } = await import('child_process')
|
|
5694
|
+
const { promisify } = await import('util')
|
|
5695
|
+
const execFileAsync = promisify(execFile)
|
|
5696
|
+
// 2D 与 3D 是**两个入口脚本**:图怎么建共用(graph_2d 直接 import graph_3d),
|
|
5697
|
+
// 换的是布局维度与渲染器 —— 两套渲染器塞一个文件就没人看得完了。
|
|
5698
|
+
const script = join(resolvePackageRoot(), 'scripts', opts.flat ? 'graph_2d.py' : 'graph_3d.py')
|
|
5699
|
+
const ticks = opts.ticks === undefined ? undefined : parseCliInteger(opts.ticks, 'ticks', 1, 100000, !!opts.json)
|
|
5700
|
+
// `--json` 一律要:CLI 要拿到 out 路径与计数,才决定怎么印。开关只决定**给谁看**。
|
|
5701
|
+
const minDegree = opts.minDegree === undefined
|
|
5702
|
+
? undefined : parseCliInteger(opts.minDegree, 'min-degree', 0, 100000, !!opts.json)
|
|
5703
|
+
const args = [script,
|
|
5704
|
+
...(opts.vault ? ['--vault', opts.vault] : []),
|
|
5705
|
+
...(opts.out ? ['--out', opts.out] : []),
|
|
5706
|
+
...(opts.cache ? ['--cache', opts.cache] : []),
|
|
5707
|
+
...(ticks === undefined ? [] : ['--ticks', String(ticks)]),
|
|
5708
|
+
...(minDegree === undefined || minDegree === 0 ? [] : ['--min-degree', String(minDegree)]),
|
|
5709
|
+
...(opts.line ? ['--line', opts.line] : []),
|
|
5710
|
+
...(opts.dryRun ? ['--dry-run'] : []),
|
|
5711
|
+
'--json']
|
|
5712
|
+
try {
|
|
5713
|
+
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
5714
|
+
timeout: 900_000, maxBuffer: 64 * 1024 * 1024, env: pythonProcessEnv(),
|
|
5715
|
+
})
|
|
5716
|
+
const body = JSON.parse(stdout.slice(stdout.indexOf('{')))
|
|
5717
|
+
if (opts.json) {
|
|
5718
|
+
console.log(JSON.stringify(body))
|
|
5719
|
+
} else if (body.dryRun) {
|
|
5720
|
+
console.log(`概念 ${body.nodes} 个、链接 ${body.links} 条(--dry-run:没有写文件)`)
|
|
5721
|
+
} else {
|
|
5722
|
+
console.log(chalk.green(`✓ 已生成 ${body.out}`))
|
|
5723
|
+
console.log(chalk.gray(` 概念 ${body.nodes} / 链接 ${body.links} / ${body.mb} MB · ${opts.flat ? '2D' : '3D'} · 双击就能逛,不需要服务器`))
|
|
5724
|
+
}
|
|
5725
|
+
if (!opts.dryRun && opts.open && body.out) await openLocalUrl(body.out)
|
|
5726
|
+
} catch (error) {
|
|
5727
|
+
if (opts.json) {
|
|
5728
|
+
console.log(JSON.stringify({ success: false, code: 'GRAPH_FAILED', action: 'graph-3d', error: safeSubprocessError(error) }))
|
|
5729
|
+
} else {
|
|
5730
|
+
console.error(chalk.red(`\n✗ ${safeSubprocessError(error)}`))
|
|
5731
|
+
}
|
|
5732
|
+
process.exit(1)
|
|
5733
|
+
}
|
|
5734
|
+
})
|
|
5735
|
+
)
|
|
5736
|
+
|
|
5513
5737
|
// wiki lint:知识库体检(全本地:不联网、不调用模型)
|
|
5514
5738
|
program.commands.find(c => c.name() === 'wiki')?.addCommand(
|
|
5515
5739
|
new Command('lint')
|
|
5516
5740
|
.description('知识库体检:断链、孤儿、空页、同名(本地,不联网)')
|
|
5517
|
-
|
|
5741
|
+
// 不给默认值:不传 `--dir` 时体检脚本自己的默认(清单里的**全部线**)。
|
|
5742
|
+
// 这里曾经默认只给文章线,而脚本默认两条线都体检 ⇒"半开半合":命令行跑出来
|
|
5743
|
+
// 与脚本跑出来口径不同,而两边都不报错。
|
|
5744
|
+
.option('--dir <dirs...>', '概念页目录(可重复;不传则体检清单里的全部线)')
|
|
5518
5745
|
.option('--json', '输出机器可读结果')
|
|
5519
5746
|
.action(async (opts) => {
|
|
5520
5747
|
const { execFile } = await import('child_process')
|
|
5521
5748
|
const { promisify } = await import('util')
|
|
5522
5749
|
const execFileAsync = promisify(execFile)
|
|
5523
5750
|
const script = join(resolvePackageRoot(), 'scripts', 'wiki_lint.py')
|
|
5524
|
-
const
|
|
5751
|
+
const dirs = Array.isArray(opts.dir) ? opts.dir : (opts.dir ? [opts.dir] : [])
|
|
5752
|
+
const args = [script, ...dirs.flatMap((d: string) => ['--dir', String(d)]),
|
|
5753
|
+
...(opts.json ? ['--json'] : [])]
|
|
5525
5754
|
try {
|
|
5526
5755
|
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
5527
5756
|
timeout: 120_000, maxBuffer: 20 * 1024 * 1024, env: pythonProcessEnv(),
|
|
@@ -5543,6 +5772,8 @@ program
|
|
|
5543
5772
|
.option('--limit <n>', '最多处理几个会话', '40')
|
|
5544
5773
|
.option('--dry-run', '仅预览:几个会话、多少字符会发给生成模型(只读本地,零出境)')
|
|
5545
5774
|
.option('--yes', '确认把对话发送给生成模型')
|
|
5775
|
+
.option('--transcribe-voice',
|
|
5776
|
+
'把缺转写的语音**本机**识别掉再产卡(本地 whisper,慢、吃 CPU、不出网;配合 --dry-run 就是只补转写、不调模型)')
|
|
5546
5777
|
.option('--json', '输出机器可读结果;执行仍需 --yes')
|
|
5547
5778
|
.action(async (opts) => {
|
|
5548
5779
|
const { execFile } = await import('child_process')
|
|
@@ -5578,6 +5809,7 @@ program
|
|
|
5578
5809
|
}
|
|
5579
5810
|
const args = [script, '--days', String(days), '--limit', String(limit),
|
|
5580
5811
|
...(confirmed ? ['--yes'] : []),
|
|
5812
|
+
...(opts.transcribeVoice ? ['--transcribe-voice'] : []),
|
|
5581
5813
|
...(opts.json ? ['--json'] : []),
|
|
5582
5814
|
...(opts.dryRun ? ['--dry-run'] : [])]
|
|
5583
5815
|
try {
|
|
@@ -5951,7 +6183,7 @@ program
|
|
|
5951
6183
|
const isComplete = (targetDate: string): boolean => Object.values(artifactStatus(targetDate)).every(Boolean)
|
|
5952
6184
|
|
|
5953
6185
|
const runPipeline = (targetDate: string): Promise<number> => new Promise((resolve) => {
|
|
5954
|
-
const args = [pipeline, '--date', targetDate, '--engine', noAi ? 'local' : 'deepseek', '--interest', 'AI'
|
|
6186
|
+
const args = [pipeline, '--date', targetDate, '--engine', noAi ? 'local' : 'deepseek', '--interest', 'AI']
|
|
5955
6187
|
if (noAi) args.push('--no-ai')
|
|
5956
6188
|
// 与 --no-ai 不同:这一条保留判断(Jev),只关掉 LLM 的文字生成。
|
|
5957
6189
|
if (opts.noSummary && !noAi) args.push('--no-summary')
|
|
@@ -7095,6 +7327,10 @@ program
|
|
|
7095
7327
|
console.log(chalk.gray('AI 助手现在可以使用以下工具:'))
|
|
7096
7328
|
} else {
|
|
7097
7329
|
console.log(json)
|
|
7330
|
+
// 机器模式到这里就结束:下面那段工具清单与"复制以上 JSON…"是**给人看的**,原来无条件打在
|
|
7331
|
+
// stdout 上 ⇒ `mcp-config --json-result | jq` 断在第 14 行(2026-10-12 由只读驱动扫描发现;
|
|
7332
|
+
// 写路径本来就早退发 JSON,所以只有读路径这一处漏)。
|
|
7333
|
+
if (opts.jsonResult) return
|
|
7098
7334
|
}
|
|
7099
7335
|
console.log(chalk.cyan('\n可用的 MCP 工具:'))
|
|
7100
7336
|
const STATIC_TOOLS: Array<[string, string]> = [
|
|
@@ -7133,6 +7369,28 @@ program
|
|
|
7133
7369
|
.description('检查运行环境(Python、pip 依赖、数据库配置)')
|
|
7134
7370
|
.option('--json', '输出 JSON 格式,不包含数据库路径或密钥')
|
|
7135
7371
|
.action(async (opts) => {
|
|
7372
|
+
// 微信客户端的**版本**与它自报的**数据目录**:两个分支都要用,所以在这里算一次。
|
|
7373
|
+
// 判据见 `wechatClient.ts`(版本取活进程的 `--user-lib-dir`;取不到就是"未知",不是"没问题")。
|
|
7374
|
+
const client = await detectWechatClient()
|
|
7375
|
+
// 客户端自报的数据目录 vs 配置里那个数据库推出来的根:**只比是不是同一份**,
|
|
7376
|
+
// 路径本身不进机器可读输出(`wechat-check/v1` 一直是不含路径的)。
|
|
7377
|
+
//
|
|
7378
|
+
// **只在 4.x 上比**:这个根是从 `ntDbPath`(4.x 的库)推的,而客户端自报的那条来自 4.x 客户端
|
|
7379
|
+
// —— 只用 3.x 的用户(`dbPath3x`)比出来的"不是同一份"没有意义,而这句话**只在 false 时打印**
|
|
7380
|
+
// ⇒ 那就是一条假警告。判据不成立时给 null("不知道"),不是 false。
|
|
7381
|
+
const { dirname } = await import('node:path')
|
|
7382
|
+
// **四层**:`…/xwechat_files/<wxid>/db_storage/message/message_0.db`
|
|
7383
|
+
// → message → db_storage → <wxid> → xwechat_files。少一层就退到 `<wxid>` 目录,
|
|
7384
|
+
// 于是两个路径永远不相等、永远报"不是同一份"(一个看着很合理的假警告)。
|
|
7385
|
+
const dbPathConfigured = String(configService.get('dataVersion') || '') === '4.x'
|
|
7386
|
+
? String(configService.get('ntDbPath') || '')
|
|
7387
|
+
: ''
|
|
7388
|
+
const configuredRoot = dbPathConfigured
|
|
7389
|
+
? dirname(dirname(dirname(dirname(dbPathConfigured))))
|
|
7390
|
+
: ''
|
|
7391
|
+
const dataDirMatchesConfig = (client.dataDirs.length && configuredRoot)
|
|
7392
|
+
? client.dataDirs.some(dir => samePath(dir, configuredRoot))
|
|
7393
|
+
: null
|
|
7136
7394
|
if (opts.json) {
|
|
7137
7395
|
const { execFileSync } = await import('child_process')
|
|
7138
7396
|
const requiredDependencies = ['sqlcipher3', 'html2text', 'zstandard', 'cryptography']
|
|
@@ -7186,6 +7444,26 @@ program
|
|
|
7186
7444
|
messageChannelLoggedIn: !!configService.get('wechatOcToken'),
|
|
7187
7445
|
daemonRunning: daemonAlive,
|
|
7188
7446
|
},
|
|
7447
|
+
// `version` 只从活进程取;取不到就是 null + 一句 `note`(**不是**"没问题")。
|
|
7448
|
+
// `unusableVersionDirs` 是磁盘上的残留版本目录(没有 `Weixin.dll`)—— 指向它们的加载会失败。
|
|
7449
|
+
wechatClient: {
|
|
7450
|
+
running: client.running,
|
|
7451
|
+
version: client.version,
|
|
7452
|
+
loadedDirs: client.loadedDirs,
|
|
7453
|
+
// 客户端自报的构建 token(并列的第二来源;**不拿它推版本号**,编码规则没解出来)
|
|
7454
|
+
versionTokens: client.versionTokens,
|
|
7455
|
+
note: client.note,
|
|
7456
|
+
unusableVersionDirs: client.unusableVersionDirs,
|
|
7457
|
+
// 客户端**自己说**数据目录在哪(与配置不一致时说明两边不是同一份库)
|
|
7458
|
+
dataDirs: client.dataDirs,
|
|
7459
|
+
// 只报结论(true/false/null),**不带路径** —— 这条 schema 一直是不含路径的
|
|
7460
|
+
dataDirMatchesConfig,
|
|
7461
|
+
// `update.data` 在场 ⇒ 加载路径可能不由命令行决定:这条不确定必须跟着版本一起出去
|
|
7462
|
+
updateData: client.updateData,
|
|
7463
|
+
// 只报条数与最近时间(不读崩溃内容)
|
|
7464
|
+
crashRecords: client.crashRecords,
|
|
7465
|
+
caveats: client.caveats,
|
|
7466
|
+
},
|
|
7189
7467
|
}
|
|
7190
7468
|
console.log(JSON.stringify(data, null, 2))
|
|
7191
7469
|
return
|
|
@@ -7270,7 +7548,38 @@ program
|
|
|
7270
7548
|
}
|
|
7271
7549
|
}
|
|
7272
7550
|
|
|
7273
|
-
// 6.
|
|
7551
|
+
// 6. 微信客户端版本(`client` / `dataDirMatchesConfig` 在 action 开头已经算过)
|
|
7552
|
+
console.log(chalk.white('\n微信客户端:'))
|
|
7553
|
+
if (client.version) {
|
|
7554
|
+
// 并列报上客户端自报的构建 token:这是"哪个构建"的第二个来源(**不用它推版本号**)
|
|
7555
|
+
const token = client.versionTokens.length ? chalk.gray(` · 自报构建 ${client.versionTokens.join('、')}`) : ''
|
|
7556
|
+
console.log(chalk.green(` ✓ 运行时加载 ${client.version}`) + chalk.gray(` (${client.loadedDirs[0]})`) + token)
|
|
7557
|
+
} else {
|
|
7558
|
+
// **说"未知"而不是"没问题"**:取不到就说取不到,别给人一个假的安心
|
|
7559
|
+
console.log(chalk.gray(` ○ 版本未知 — ${client.note}`))
|
|
7560
|
+
}
|
|
7561
|
+
for (const dead of client.unusableVersionDirs) {
|
|
7562
|
+
console.log(chalk.yellow(` ! 残留版本目录 ${dead.dir}:只剩 ${dead.files} 个文件(没有 Weixin.dll)`))
|
|
7563
|
+
console.log(chalk.gray(' 指向它的加载会失败;如果客户端起不来,先看这里'))
|
|
7564
|
+
}
|
|
7565
|
+
// `update.data` 在场时上面那个版本**未必**是加载的那个 —— 这句话要跟着它一起出现
|
|
7566
|
+
for (const caveat of client.caveats) {
|
|
7567
|
+
console.log(chalk.yellow(` ⚠ ${caveat}`))
|
|
7568
|
+
}
|
|
7569
|
+
if (client.dataDirs.length) {
|
|
7570
|
+
console.log(chalk.gray(` · 客户端自报数据目录: ${client.dataDirs.join('、')}`))
|
|
7571
|
+
if (dataDirMatchesConfig === false) {
|
|
7572
|
+
// 不一致 = 两边可能不是同一份库(本机数据目录还有 D 盘软链,这条已经按真身比过)
|
|
7573
|
+
console.log(chalk.yellow(' ⚠ 与配置里那个数据库推出来的根**不是同一份** —— 先确认你在读的是客户端正在写的那份'))
|
|
7574
|
+
}
|
|
7575
|
+
}
|
|
7576
|
+
const crashes = client.crashRecords.reports + client.crashRecords.records
|
|
7577
|
+
if (crashes > 0) {
|
|
7578
|
+
const when = client.crashRecords.newest ? client.crashRecords.newest.slice(0, 16).replace('T', ' ') : '时间读不到'
|
|
7579
|
+
console.log(chalk.gray(` · 客户端崩溃记录 ${crashes} 份(最近 ${when})—— 客户端崩过时数据读不出来,先看这条`))
|
|
7580
|
+
}
|
|
7581
|
+
|
|
7582
|
+
// 7. Assistant (第二大脑)
|
|
7274
7583
|
console.log(chalk.white('\n第二大脑 Agent:'))
|
|
7275
7584
|
const aiKey = configService.get('deepseekApiKey')
|
|
7276
7585
|
const aiBase = configService.get('aiBaseUrl')
|
|
@@ -7491,7 +7800,7 @@ async function showInteractiveMenu() {
|
|
|
7491
7800
|
const { spawn } = await import('child_process')
|
|
7492
7801
|
const pipeline = join(resolvePackageRoot(), 'scripts', 'pipeline.py')
|
|
7493
7802
|
console.log(chalk.cyan(`\n📰 正在生成 ${dateStr} 公众号日报...\n`))
|
|
7494
|
-
const child = spawn(getPythonCommand(), [pipeline, '--date', dateStr, '--interest', 'AI'
|
|
7803
|
+
const child = spawn(getPythonCommand(), [pipeline, '--date', dateStr, '--interest', 'AI'], {
|
|
7495
7804
|
windowsHide: true,
|
|
7496
7805
|
stdio: 'inherit',
|
|
7497
7806
|
env: pythonProcessEnv(apiKey),
|