weflow-cli 1.9.0 → 1.11.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 +1051 -0
- package/OPERATIONS.md +99 -13
- package/README.en.md +14 -5
- package/README.md +23 -8
- package/bin/weflow-cli.ts +364 -40
- package/dist/bin/weflow-cli.js +381 -69
- 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/exportService.d.ts +43 -0
- package/dist/src/services/exportService.d.ts.map +1 -1
- package/dist/src/services/exportService.js +116 -11
- package/dist/src/services/exportService.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 +27 -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 +64 -5
- package/docs/RELEASING.md +128 -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/export_chat_html.py +346 -6
- 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 +212 -58
- 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/exportService.ts +129 -12
- 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('查看联系人列表')
|
|
@@ -1730,8 +1821,9 @@ program
|
|
|
1730
1821
|
.option('--json', '输出机器可读的导出结果,不改变导出文件格式')
|
|
1731
1822
|
.option('--non-interactive', '禁止交互选择;匹配不唯一时返回错误')
|
|
1732
1823
|
.option('--full-images', '使用原图而非微信缓存缩略图(更清晰,但导出慢数倍)')
|
|
1824
|
+
.option('--media', 'json/txt/excel 导出时一并抽出图片与文档(落盘到 <输出>/<会话>_assets,并在导出物里给出相对路径)')
|
|
1733
1825
|
.action(async (talkerInput: string, format: string, opts) => {
|
|
1734
|
-
const validFormats = ['json', 'txt', 'html', 'excel']
|
|
1826
|
+
const validFormats = ['json', 'txt', 'html', 'md', 'excel']
|
|
1735
1827
|
if (!validFormats.includes(format)) {
|
|
1736
1828
|
if (opts.json) console.log(JSON.stringify({ success: false, code: 'INVALID_FORMAT', error: '导出格式无效', supported: validFormats }))
|
|
1737
1829
|
else {
|
|
@@ -1780,6 +1872,16 @@ program
|
|
|
1780
1872
|
}
|
|
1781
1873
|
process.exit(1)
|
|
1782
1874
|
}
|
|
1875
|
+
// 图片要靠 Python 那条路匹配(缓存缩略图、资源 md5、原图那档)。`--media` 就是"先抽一次",
|
|
1876
|
+
// 抽完 json/txt/excel 各自消费同一份清单 —— 匹配只跑一遍,也不会在 TS 里重写一遍。
|
|
1877
|
+
if (opts.media && (format === 'json' || format === 'txt' || format === 'excel')) {
|
|
1878
|
+
const media = await exportService.extractMedia(talker, opts.output, !!opts.json, { fullImages: opts.fullImages === true }, limit, opts.date || '')
|
|
1879
|
+
if (!media.success) {
|
|
1880
|
+
if (opts.json) console.log(JSON.stringify({ success: false, code: 'MEDIA_EXPORT_FAILED', error: media.error || '媒体抽取失败' }))
|
|
1881
|
+
else console.log(chalk.red(`✗ 媒体抽取失败: ${media.error}`))
|
|
1882
|
+
process.exit(1)
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1783
1885
|
switch (format) {
|
|
1784
1886
|
case 'json':
|
|
1785
1887
|
result = await exportService.exportJson(talker, opts.output, limit, from, to, opts.contract)
|
|
@@ -1790,6 +1892,10 @@ program
|
|
|
1790
1892
|
case 'html':
|
|
1791
1893
|
result = await exportService.exportHtml(talker, opts.output, limit, opts.date || '', from, to, !!opts.json, { fullImages: opts.fullImages === true })
|
|
1792
1894
|
break
|
|
1895
|
+
case 'md':
|
|
1896
|
+
// markdown + 图片落盘(`<输出>/<talker>_assets/`);json/txt/excel 之后会自动带上那些图。
|
|
1897
|
+
result = await exportService.exportMarkdown(talker, opts.output, limit, opts.date || '', from, to, !!opts.json, { fullImages: opts.fullImages === true })
|
|
1898
|
+
break
|
|
1793
1899
|
case 'excel':
|
|
1794
1900
|
result = await exportService.exportExcel(talker, opts.output, limit, from, to)
|
|
1795
1901
|
break
|
|
@@ -2027,7 +2133,10 @@ program
|
|
|
2027
2133
|
process.exit(1)
|
|
2028
2134
|
}
|
|
2029
2135
|
} else {
|
|
2030
|
-
|
|
2136
|
+
// `opts.json` 也要传:这一行在 `--json` 下**不可达**(上面就 `INTERACTIVE_REQUIRED` 退出了),
|
|
2137
|
+
// 但少传一个是安静的 —— 规则统一成"校验 helper 一律带上它",判据才写得出来
|
|
2138
|
+
// (`test/json-error-surface.test.ts` 末尾那条静态检查)。
|
|
2139
|
+
const timeout = parseCliInteger(opts.timeout, 'timeout', 1000, 600000, opts.json)
|
|
2031
2140
|
const result = await keyService.autoGetDbKey(timeout, (msg) => {
|
|
2032
2141
|
console.log(chalk.gray(` ${msg}`))
|
|
2033
2142
|
})
|
|
@@ -2250,7 +2359,7 @@ whitelistCmd
|
|
|
2250
2359
|
wxid = target
|
|
2251
2360
|
} else {
|
|
2252
2361
|
try {
|
|
2253
|
-
wxid = await resolveTalker(target)
|
|
2362
|
+
wxid = await resolveTalker(target, !!opts.json, !!opts.json)
|
|
2254
2363
|
const sessions = await chatService.listSessions(undefined, 50)
|
|
2255
2364
|
const match = sessions.find(s => s.username === wxid)
|
|
2256
2365
|
if (match) displayName = match.displayName || target
|
|
@@ -2437,7 +2546,7 @@ blacklistCmd
|
|
|
2437
2546
|
wxid = target
|
|
2438
2547
|
} else {
|
|
2439
2548
|
try {
|
|
2440
|
-
wxid = await resolveTalker(target)
|
|
2549
|
+
wxid = await resolveTalker(target, !!opts.json, !!opts.json)
|
|
2441
2550
|
const sessions = await chatService.listSessions(undefined, 50)
|
|
2442
2551
|
const match = sessions.find(s => s.username === wxid)
|
|
2443
2552
|
if (match) displayName = match.displayName || target
|
|
@@ -3809,6 +3918,8 @@ program
|
|
|
3809
3918
|
.option('--api-key <key>', 'DeepSeek API key')
|
|
3810
3919
|
.option('--refresh-sources',
|
|
3811
3920
|
'只把已有概念页的"来源"段补齐成当前全部来源(本地重写,**不调用模型**)')
|
|
3921
|
+
.option('--qualify-links',
|
|
3922
|
+
'把跨线同名的概念链接改成带路径的形式(本地重写,**不调用模型**)')
|
|
3812
3923
|
.option('--cards <dir...>', '产卡目录,可写多个;默认所有 output/*-notes(与索引的引用数同一口径)')
|
|
3813
3924
|
.option('--dry-run', '仅预览,不读取文章、调用 AI 或写入概念页')
|
|
3814
3925
|
.option('--yes', '确认调用 AI 并生成概念页')
|
|
@@ -3834,6 +3945,22 @@ program
|
|
|
3834
3945
|
return
|
|
3835
3946
|
}
|
|
3836
3947
|
|
|
3948
|
+
if (opts.qualifyLinks) {
|
|
3949
|
+
// 本地消歧:两条线同名的那些链接按名字带路径(`Chat/Concepts/DeepSeek|DeepSeek`),
|
|
3950
|
+
// 免得 Obsidian 里那条裸链接命中另一条线的那张。**不调模型**,重跑幂等。
|
|
3951
|
+
await runConfirmedPythonMutation({
|
|
3952
|
+
action: 'wiki.qualifyLinks',
|
|
3953
|
+
script,
|
|
3954
|
+
args: ['--qualify-links', '--output', opts.output],
|
|
3955
|
+
cliOptions: opts,
|
|
3956
|
+
preview: { readsLocalKnowledge: true, rewritesExistingPages: true, usesAi: false },
|
|
3957
|
+
confirmationMessage: `确认把 ${opts.output} 里跨线同名的概念链接改成带路径的形式?`
|
|
3958
|
+
+ '(本地重写,不调用 AI)',
|
|
3959
|
+
apiKey: opts.apiKey,
|
|
3960
|
+
timeout: 300_000,
|
|
3961
|
+
})
|
|
3962
|
+
return
|
|
3963
|
+
}
|
|
3837
3964
|
const limit = parseCliInteger(opts.limit, 'limit', 1, 1000, opts.json)
|
|
3838
3965
|
const minRefs = parseCliInteger(opts.minRefs ?? '1', 'min-refs', 1, 50, opts.json)
|
|
3839
3966
|
await runConfirmedPythonMutation({
|
|
@@ -3864,8 +3991,9 @@ program
|
|
|
3864
3991
|
.option('--wiki-limit <n>', '概念编译数', '20')
|
|
3865
3992
|
.option('--source <name>', '仅处理指定公众号,可重复使用', (value: string, previous: string[]) => [...previous, value], [] as string[])
|
|
3866
3993
|
.option('--skip-classify', '跳过 AI 后处理')
|
|
3867
|
-
|
|
3868
|
-
.option('--
|
|
3994
|
+
// 这两个**默认不做**:Obsidian 自带的图谱视图是实时的,写一次 vault 图谱就重画一次。
|
|
3995
|
+
.option('--with-wiki', '把概念页写进 Obsidian Vault(默认不写:图谱只在你说了才变)')
|
|
3996
|
+
.option('--with-vault', '把当天内容副本同步进 Obsidian Vault(默认不写)')
|
|
3869
3997
|
.option('--skip-html', '跳过 HTML 阅读器生成')
|
|
3870
3998
|
.option('--skip-ai-report', '跳过 AI 深度阅读报告')
|
|
3871
3999
|
.option('--no-ai', '关闭全部 AI 调用,保留抓取和本地输出')
|
|
@@ -3947,8 +4075,8 @@ program
|
|
|
3947
4075
|
if (opts.date) args.push('--date', opts.date)
|
|
3948
4076
|
for (const source of sources) args.push('--source', source)
|
|
3949
4077
|
if (opts.skipClassify) args.push('--skip-classify')
|
|
3950
|
-
if (opts.
|
|
3951
|
-
if (opts.
|
|
4078
|
+
if (opts.withWiki) args.push('--with-wiki')
|
|
4079
|
+
if (opts.withVault) args.push('--with-vault')
|
|
3952
4080
|
if (opts.skipHtml) args.push('--skip-html')
|
|
3953
4081
|
if (opts.skipAiReport) args.push('--skip-ai-report')
|
|
3954
4082
|
if (noAi) args.push('--no-ai')
|
|
@@ -4184,16 +4312,30 @@ program
|
|
|
4184
4312
|
.description('Vault 搜索 — 文章+概念+笔记')
|
|
4185
4313
|
.argument('<query>', '搜索关键词')
|
|
4186
4314
|
.option('--type <type>', 'all / article / concept / note', 'all')
|
|
4315
|
+
.option('--line <line>', 'wiki=文章线(默认) / chat=聊天线 / all=两条都搜', 'wiki')
|
|
4187
4316
|
.option('--top-k <n>', '返回数量', '10')
|
|
4317
|
+
// `vault_search.py` 一直有 `--days`(它的 docstring 自己就举 `--days 30`),CLI 从来没接 ——
|
|
4318
|
+
// 于是"只搜最近一周"这件事在这条路上做不到,窗口被脚本默认值(90)钉死。与 `search-index --days`
|
|
4319
|
+
// 同形:窗口的运行值由 CLI 说了算,默认值与脚本一致。
|
|
4320
|
+
.option('--days <n>', '日报文章取最近多少天(默认 90)', '90')
|
|
4321
|
+
// 这条 `--json` 曾经**不存在,而 JSON 又是唯一的输出**:参数里硬编码着 `'--json'`,
|
|
4322
|
+
// 人类模式虽然脚本里写着(`vault_search.py` 不带 `--json` 时打印带相关度的可读列表)
|
|
4323
|
+
// 却没有任何入口能到达,用户显式敲 `--json` 还会拿到 `unknown option` —— 与
|
|
4324
|
+
// `docs/EXTENDING.md` 那条"脚本或 AI 会驱动的命令都有 `--json` 模式"正相反。
|
|
4325
|
+
// 现在按兄弟命令(`vault rag`、顶层 `search`)的形状来:默认人类可读,`--json` 才转发。
|
|
4326
|
+
.option('--json', '输出 JSON 格式(含本地路径与标题,供本机脚本/AI 使用)')
|
|
4188
4327
|
.action(async (query, opts) => {
|
|
4189
4328
|
const pkgRoot = resolvePackageRoot()
|
|
4190
4329
|
const script = join(pkgRoot, 'scripts', 'vault_search.py')
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
const topK = parseCliInteger(opts.topK, 'top-k', 1, 100)
|
|
4196
|
-
|
|
4330
|
+
// 校验分支也要认 `--json`(`requireCliChoice` 里就带着这条约定):
|
|
4331
|
+
// 否则 JSON 模式下 stdout 是一行彩色散文,脚本解析不了。
|
|
4332
|
+
requireCliChoice(opts.type, ['all', 'article', 'concept', 'note'], 'type', opts.json)
|
|
4333
|
+
requireCliChoice(opts.line, ['wiki', 'chat', 'all'], 'line', opts.json)
|
|
4334
|
+
const topK = parseCliInteger(opts.topK, 'top-k', 1, 100, opts.json)
|
|
4335
|
+
const days = parseCliInteger(opts.days, 'days', 1, 3650, opts.json)
|
|
4336
|
+
await runPythonCmd(script, [query, '--type', opts.type, '--line', opts.line,
|
|
4337
|
+
'--top-k', String(topK), '--days', String(days),
|
|
4338
|
+
...(opts.json ? ['--json'] : [])])
|
|
4197
4339
|
})
|
|
4198
4340
|
)
|
|
4199
4341
|
|
|
@@ -4205,6 +4347,7 @@ program
|
|
|
4205
4347
|
.description('Vault 问答 — 基于知识库的 AI 对话')
|
|
4206
4348
|
.argument('<question>', '问题')
|
|
4207
4349
|
.option('--top-k <n>', '检索条数', '8')
|
|
4350
|
+
.option('--line <line>', 'wiki=文章线(默认) / chat=聊天线 / all=两条都搜', 'wiki')
|
|
4208
4351
|
.option('--api-key <key>', 'DeepSeek API key')
|
|
4209
4352
|
.option('--dry-run', '仅预览,不读取知识库或调用 AI')
|
|
4210
4353
|
.option('--yes', '确认读取本地知识并发送筛选后的上下文到 AI')
|
|
@@ -4213,11 +4356,13 @@ program
|
|
|
4213
4356
|
const pkgRoot = resolvePackageRoot()
|
|
4214
4357
|
const script = join(pkgRoot, 'scripts', 'vault_rag.py')
|
|
4215
4358
|
const topK = parseCliInteger(opts.topK, 'top-k', 1, 100, opts.json)
|
|
4359
|
+
requireCliChoice(opts.line, ['wiki', 'chat', 'all'], 'line', opts.json)
|
|
4216
4360
|
const preview = {
|
|
4217
4361
|
success: true,
|
|
4218
4362
|
dryRun: true,
|
|
4219
4363
|
action: 'vault.rag',
|
|
4220
4364
|
topK,
|
|
4365
|
+
line: opts.line,
|
|
4221
4366
|
readsLocalKnowledge: true,
|
|
4222
4367
|
usesAi: true,
|
|
4223
4368
|
sendsSelectedContextToAi: true,
|
|
@@ -4243,7 +4388,7 @@ program
|
|
|
4243
4388
|
return
|
|
4244
4389
|
}
|
|
4245
4390
|
}
|
|
4246
|
-
const args = ['--top-k', String(topK)]
|
|
4391
|
+
const args = ['--top-k', String(topK), '--line', opts.line]
|
|
4247
4392
|
if (opts.json) args.push('--json')
|
|
4248
4393
|
const { execFile } = await import('child_process')
|
|
4249
4394
|
const { promisify } = await import('util')
|
|
@@ -4548,6 +4693,25 @@ program
|
|
|
4548
4693
|
})
|
|
4549
4694
|
|
|
4550
4695
|
// ==================== weread ====================
|
|
4696
|
+
|
|
4697
|
+
/** 微信读书服务失败时的统一出口。
|
|
4698
|
+
*
|
|
4699
|
+
* 9 个调用点(shelf / stats / notes×2 / search / book / review / discover / profile)原来都是一行式
|
|
4700
|
+
* 分支:把彩色散文打在 stdout 上再 `process.exit(1)`,**没有** json 支,而各命令的 `if (opts.json)`
|
|
4701
|
+
* 分支在它后面 ⇒ `weread shelf --json` 失败时 stdout 上只有一行 `✗ …`,调用方解析不了,也不知道
|
|
4702
|
+
* 是哪个命令失败了。全库其余同类出口都是"机器支发 JSON + exit"。
|
|
4703
|
+
*
|
|
4704
|
+
* (这里刻意**不**把那行旧代码原样抄出来:`test/json-error-surface.test.ts` 有一条绊线在扫那个形状,
|
|
4705
|
+
* 而注释里留一份字面量会让判据读到它 —— 今晚已经在另一条判据上踩过这个坑。)
|
|
4706
|
+
*
|
|
4707
|
+
* 人看的那一支保持原样(`console.log`,不动它的落点):把 CLI 的**错误**统一改走 stderr 是另一件事,
|
|
4708
|
+
* 现在库里两种都有(多数 `console.error`、少数 `console.log`),不在这条修复里顺手改。
|
|
4709
|
+
*/
|
|
4710
|
+
function failWeread(res: { error?: string }, json = false): never {
|
|
4711
|
+
if (json) console.log(JSON.stringify({ success: false, code: 'WEREAD_FAILED', error: res.error }))
|
|
4712
|
+
else console.log(chalk.red(`✗ ${res.error}`))
|
|
4713
|
+
process.exit(1)
|
|
4714
|
+
}
|
|
4551
4715
|
const wereadCmd = program
|
|
4552
4716
|
.command('weread')
|
|
4553
4717
|
.description('微信读书助手 — 书架、统计、笔记、搜索、书评、推荐')
|
|
@@ -4559,7 +4723,7 @@ program
|
|
|
4559
4723
|
.action(async (opts) => {
|
|
4560
4724
|
const weread = await getWereadService()
|
|
4561
4725
|
const res = await weread.shelf()
|
|
4562
|
-
if (!res.ok)
|
|
4726
|
+
if (!res.ok) failWeread(res, !!opts.json)
|
|
4563
4727
|
const d = res.data!
|
|
4564
4728
|
if (opts.json) { console.log(JSON.stringify(d, null, 2)); return }
|
|
4565
4729
|
|
|
@@ -4588,7 +4752,7 @@ program
|
|
|
4588
4752
|
.action(async (opts) => {
|
|
4589
4753
|
const weread = await getWereadService()
|
|
4590
4754
|
const res = await weread.readData(opts.mode)
|
|
4591
|
-
if (!res.ok)
|
|
4755
|
+
if (!res.ok) failWeread(res, !!opts.json)
|
|
4592
4756
|
const d = res.data!
|
|
4593
4757
|
if (opts.json) { console.log(JSON.stringify(d, null, 2)); return }
|
|
4594
4758
|
|
|
@@ -4636,7 +4800,7 @@ program
|
|
|
4636
4800
|
const limit = parseCliInteger(opts.limit, 'limit', 1, 100, opts.json)
|
|
4637
4801
|
if (opts.bookId) {
|
|
4638
4802
|
const res = await weread.bookmarks(opts.bookId, limit)
|
|
4639
|
-
if (!res.ok)
|
|
4803
|
+
if (!res.ok) failWeread(res, !!opts.json)
|
|
4640
4804
|
const d = res.data!
|
|
4641
4805
|
if (opts.json) { console.log(JSON.stringify(d, null, 2)); return }
|
|
4642
4806
|
console.log(chalk.cyan(`📝 划线笔记 (${d.updated?.length || 0} 条):\n`))
|
|
@@ -4649,7 +4813,7 @@ program
|
|
|
4649
4813
|
}
|
|
4650
4814
|
} else {
|
|
4651
4815
|
const res = await weread.notebooks(50)
|
|
4652
|
-
if (!res.ok)
|
|
4816
|
+
if (!res.ok) failWeread(res, !!opts.json)
|
|
4653
4817
|
const d = res.data!
|
|
4654
4818
|
if (opts.json) { console.log(JSON.stringify(d, null, 2)); return }
|
|
4655
4819
|
console.log(chalk.cyan(`📝 笔记本 (${d.books?.length || 0} 本有笔记):\n`))
|
|
@@ -4672,7 +4836,7 @@ program
|
|
|
4672
4836
|
const weread = await getWereadService()
|
|
4673
4837
|
const limit = parseCliInteger(opts.limit, 'limit', 1, 100, opts.json)
|
|
4674
4838
|
const res = await weread.search(keyword, limit)
|
|
4675
|
-
if (!res.ok)
|
|
4839
|
+
if (!res.ok) failWeread(res, !!opts.json)
|
|
4676
4840
|
const d = res.data!
|
|
4677
4841
|
if (opts.json) { console.log(JSON.stringify(d, null, 2)); return }
|
|
4678
4842
|
console.log(chalk.cyan(`🔍 搜索: "${keyword}" (${d.books?.length || 0} 条):\n`))
|
|
@@ -4696,7 +4860,7 @@ program
|
|
|
4696
4860
|
weread.bookInfo(bookId),
|
|
4697
4861
|
weread.getProgress(bookId),
|
|
4698
4862
|
])
|
|
4699
|
-
if (!infoRes.ok)
|
|
4863
|
+
if (!infoRes.ok) failWeread(infoRes, !!opts.json)
|
|
4700
4864
|
const b = infoRes.data!
|
|
4701
4865
|
if (opts.json) { console.log(JSON.stringify(b, null, 2)); return }
|
|
4702
4866
|
|
|
@@ -4722,7 +4886,7 @@ program
|
|
|
4722
4886
|
const weread = await getWereadService()
|
|
4723
4887
|
const limit = parseCliInteger(opts.limit, 'limit', 1, 100, opts.json)
|
|
4724
4888
|
const res = await weread.reviews(bookId, limit)
|
|
4725
|
-
if (!res.ok)
|
|
4889
|
+
if (!res.ok) failWeread(res, !!opts.json)
|
|
4726
4890
|
const d = res.data!
|
|
4727
4891
|
if (opts.json) { console.log(JSON.stringify(d, null, 2)); return }
|
|
4728
4892
|
console.log(chalk.cyan(`💬 书评 (${d.reviews?.length || 0} 条):\n`))
|
|
@@ -4748,7 +4912,7 @@ program
|
|
|
4748
4912
|
const res = opts.bookId
|
|
4749
4913
|
? await weread.similar(opts.bookId, limit)
|
|
4750
4914
|
: await weread.recommend(limit)
|
|
4751
|
-
if (!res.ok)
|
|
4915
|
+
if (!res.ok) failWeread(res, !!opts.json)
|
|
4752
4916
|
const d = res.data!
|
|
4753
4917
|
if (opts.json) { console.log(JSON.stringify(d, null, 2)); return }
|
|
4754
4918
|
const label = opts.bookId ? '相似推荐' : '个性化推荐'
|
|
@@ -4769,7 +4933,7 @@ program
|
|
|
4769
4933
|
.action(async (opts) => {
|
|
4770
4934
|
const weread = await getWereadService()
|
|
4771
4935
|
const res = await weread.profile()
|
|
4772
|
-
if (!res.ok)
|
|
4936
|
+
if (!res.ok) failWeread(res, !!opts.json)
|
|
4773
4937
|
const p = res.data!
|
|
4774
4938
|
if (opts.json) { console.log(JSON.stringify(p, null, 2)); return }
|
|
4775
4939
|
|
|
@@ -4812,6 +4976,8 @@ program
|
|
|
4812
4976
|
.description('语义搜索知识库(聊天记录 + 文章)')
|
|
4813
4977
|
.argument('<query>', '搜索关键词')
|
|
4814
4978
|
.option('--top-k <n>', '返回数量', '10')
|
|
4979
|
+
.option('--line <line>', 'wiki=文章线(默认) / chat=聊天记录 / all=两条都搜', 'wiki')
|
|
4980
|
+
.option('--no-rerank', '不调用决策模型重排,只按向量/关键词相似度返回(回退到引入重排之前)')
|
|
4815
4981
|
.option('--api-key <key>', 'DashScope embedding API key')
|
|
4816
4982
|
.option('--dry-run', '仅预览,不读取索引或调用向量服务')
|
|
4817
4983
|
.option('--yes', '确认执行可能调用云端向量服务的搜索')
|
|
@@ -4823,11 +4989,14 @@ program
|
|
|
4823
4989
|
const pkgRoot = resolvePackageRoot()
|
|
4824
4990
|
const script = join(pkgRoot, 'scripts', 'semantic_search.py')
|
|
4825
4991
|
const topK = parseCliInteger(opts.topK, 'top-k', 1, 100, opts.json)
|
|
4992
|
+
requireCliChoice(opts.line, ['wiki', 'chat', 'all'], 'line', opts.json)
|
|
4826
4993
|
const preview = {
|
|
4827
4994
|
success: true,
|
|
4828
4995
|
dryRun: true,
|
|
4829
4996
|
action: 'semantic-search.query',
|
|
4830
4997
|
topK,
|
|
4998
|
+
line: opts.line,
|
|
4999
|
+
noRerank: opts.rerank === false,
|
|
4831
5000
|
readsLocalIndex: true,
|
|
4832
5001
|
mayUseCloudEmbedding: true,
|
|
4833
5002
|
}
|
|
@@ -4852,7 +5021,11 @@ program
|
|
|
4852
5021
|
return
|
|
4853
5022
|
}
|
|
4854
5023
|
}
|
|
4855
|
-
|
|
5024
|
+
// `--no-rerank` 是**旗标**(不是密钥)⇒ 可以走 argv;查询本身仍然只走 env(见 `privacy-paths` 那条规矩)。
|
|
5025
|
+
// 接这条通道之前,`OPERATIONS.md` 一直在教用户敲 `search --no-rerank`,而 CLI 会答 `unknown option`
|
|
5026
|
+
// —— 同一份文档里 `--line` 是通的,所以它看起来像"能用的旗标"(2026-10-11 由文档命令面那道检查发现)。
|
|
5027
|
+
const args: string[] = [script, 'search', '--top-k', String(topK), '--line', opts.line,
|
|
5028
|
+
...(opts.rerank === false ? ['--no-rerank'] : [])]
|
|
4856
5029
|
try {
|
|
4857
5030
|
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
4858
5031
|
timeout: 60_000, maxBuffer: 10 * 1024 * 1024,
|
|
@@ -4950,6 +5123,7 @@ program
|
|
|
4950
5123
|
.description('RAG 智能助手 — 基于聊天记录和公众号的对话式问答')
|
|
4951
5124
|
.argument('[question]', '要问的问题(不传则进入交互模式)')
|
|
4952
5125
|
.option('--top-k <n>', '检索数量', '10')
|
|
5126
|
+
.option('--line <line>', 'wiki=文章/文章概念(默认) / chat=聊天记录 / all=两条都搜', 'wiki')
|
|
4953
5127
|
.option('--talker <name>', '限定联系人/群聊')
|
|
4954
5128
|
.option('--api-key <key>', 'AI API key')
|
|
4955
5129
|
.option('--dry-run', '仅预览,不读取知识库或调用 AI')
|
|
@@ -4962,11 +5136,13 @@ program
|
|
|
4962
5136
|
const pkgRoot = resolvePackageRoot()
|
|
4963
5137
|
const script = join(pkgRoot, 'scripts', 'rag_chat.py')
|
|
4964
5138
|
const topK = parseCliInteger(opts.topK, 'top-k', 1, 100, opts.json)
|
|
5139
|
+
requireCliChoice(opts.line, ['wiki', 'chat', 'all'], 'line', opts.json)
|
|
4965
5140
|
const preview = {
|
|
4966
5141
|
success: true,
|
|
4967
5142
|
dryRun: true,
|
|
4968
5143
|
action: 'rag-chat.query',
|
|
4969
5144
|
topK,
|
|
5145
|
+
line: opts.line,
|
|
4970
5146
|
interactiveRequired: !question,
|
|
4971
5147
|
readsLocalKnowledge: true,
|
|
4972
5148
|
usesCloudEmbedding: true,
|
|
@@ -5001,7 +5177,7 @@ program
|
|
|
5001
5177
|
}
|
|
5002
5178
|
const args: string[] = [script]
|
|
5003
5179
|
if (question) {
|
|
5004
|
-
args.push('--top-k', String(topK))
|
|
5180
|
+
args.push('--top-k', String(topK), '--line', opts.line)
|
|
5005
5181
|
if (opts.json) args.push('--json')
|
|
5006
5182
|
try {
|
|
5007
5183
|
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
@@ -5019,7 +5195,7 @@ program
|
|
|
5019
5195
|
}
|
|
5020
5196
|
} else {
|
|
5021
5197
|
// 交互模式:使用 spawn 保持终端交互
|
|
5022
|
-
args.push('--interactive', '--top-k', String(topK))
|
|
5198
|
+
args.push('--interactive', '--top-k', String(topK), '--line', opts.line)
|
|
5023
5199
|
const child = spawn(getPythonCommand(), args, {
|
|
5024
5200
|
windowsHide: true,
|
|
5025
5201
|
stdio: 'inherit',
|
|
@@ -5288,7 +5464,7 @@ program
|
|
|
5288
5464
|
// draft:判断在前、起草在后、**绝不发送**
|
|
5289
5465
|
program
|
|
5290
5466
|
.command('draft <talker>')
|
|
5291
|
-
.description('帮我起草回复:先判断(意图/风险/该不该给实质),再起草候选 ——
|
|
5467
|
+
.description('帮我起草回复:先判断(意图/风险/该不该给实质),再起草候选 —— **默认只产出文本;给了 --pick N --send 才会替你发送**')
|
|
5292
5468
|
.option('--count <n>', '要几条候选', '3')
|
|
5293
5469
|
.option('--pick <n>', '选第几条(从 1 数);不带 --send 时只把那条的完整原文打出来')
|
|
5294
5470
|
.option('--send', '把选中的那条发出去 —— **必须同时给 --pick**(不允许"自己挑一条发"),仍需 --yes/--dry-run')
|
|
@@ -5510,18 +5686,86 @@ program
|
|
|
5510
5686
|
}
|
|
5511
5687
|
})
|
|
5512
5688
|
|
|
5689
|
+
// wiki graph:把概念图谱导成一张**自包含**的 3D 页面(全本地:只读 Vault、不联网、不调用模型)
|
|
5690
|
+
//
|
|
5691
|
+
// 自包含是硬要求,不是顺手:页面用 `file://` 双击打开,那时 `fetch` 会被 CORS 挡死,
|
|
5692
|
+
// 所以库、数据、坐标全部内联进去 —— 也因此三个库随 npm 包发(见 resources/js/graph3d/NOTICE.txt)。
|
|
5693
|
+
// 布局(250 tick 实测 55 秒)在构建期用 Node 算完并缓存,打开页面即用。
|
|
5694
|
+
program.commands.find(c => c.name() === 'wiki')?.addCommand(
|
|
5695
|
+
new Command('graph')
|
|
5696
|
+
.description('把概念图谱导成一张自包含的图谱页面(只读本地、不联网;生成的 HTML 双击就能逛)。--flat 出 2D 版')
|
|
5697
|
+
.option('--vault <dir>', 'Vault 根目录(默认 output/wechat-vault)')
|
|
5698
|
+
.option('-o, --out <file>', '页面写到哪(默认 output/knowledge-graph-3d.html)')
|
|
5699
|
+
.option('--ticks <n>', '力导向迭代次数(默认 250,越多越舒展也越慢)')
|
|
5700
|
+
.option('--cache <dir>', '布局缓存目录(派生数据;测试/多份图分开放时用)')
|
|
5701
|
+
.option('--flat', '2D 版:平面图(更像 Obsidian 那种),canvas 渲染,输出默认 knowledge-graph-2d.html')
|
|
5702
|
+
.option('--min-degree <n>', '只画在原图里连接数 ≥N 的概念(去掉细枝看骨架;0 = 全画)')
|
|
5703
|
+
.option('--line <which>', '只画一条线:all / wiki / chat')
|
|
5704
|
+
.option('--dry-run', '只报概念与链接数:不写文件、也不算布局(本地,零改动)')
|
|
5705
|
+
.option('--open', '生成后用默认浏览器打开它')
|
|
5706
|
+
.option('--json', '输出机器可读结果')
|
|
5707
|
+
.action(async (opts) => {
|
|
5708
|
+
const { execFile } = await import('child_process')
|
|
5709
|
+
const { promisify } = await import('util')
|
|
5710
|
+
const execFileAsync = promisify(execFile)
|
|
5711
|
+
// 2D 与 3D 是**两个入口脚本**:图怎么建共用(graph_2d 直接 import graph_3d),
|
|
5712
|
+
// 换的是布局维度与渲染器 —— 两套渲染器塞一个文件就没人看得完了。
|
|
5713
|
+
const script = join(resolvePackageRoot(), 'scripts', opts.flat ? 'graph_2d.py' : 'graph_3d.py')
|
|
5714
|
+
const ticks = opts.ticks === undefined ? undefined : parseCliInteger(opts.ticks, 'ticks', 1, 100000, !!opts.json)
|
|
5715
|
+
// `--json` 一律要:CLI 要拿到 out 路径与计数,才决定怎么印。开关只决定**给谁看**。
|
|
5716
|
+
const minDegree = opts.minDegree === undefined
|
|
5717
|
+
? undefined : parseCliInteger(opts.minDegree, 'min-degree', 0, 100000, !!opts.json)
|
|
5718
|
+
const args = [script,
|
|
5719
|
+
...(opts.vault ? ['--vault', opts.vault] : []),
|
|
5720
|
+
...(opts.out ? ['--out', opts.out] : []),
|
|
5721
|
+
...(opts.cache ? ['--cache', opts.cache] : []),
|
|
5722
|
+
...(ticks === undefined ? [] : ['--ticks', String(ticks)]),
|
|
5723
|
+
...(minDegree === undefined || minDegree === 0 ? [] : ['--min-degree', String(minDegree)]),
|
|
5724
|
+
...(opts.line ? ['--line', opts.line] : []),
|
|
5725
|
+
...(opts.dryRun ? ['--dry-run'] : []),
|
|
5726
|
+
'--json']
|
|
5727
|
+
try {
|
|
5728
|
+
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
5729
|
+
timeout: 900_000, maxBuffer: 64 * 1024 * 1024, env: pythonProcessEnv(),
|
|
5730
|
+
})
|
|
5731
|
+
const body = JSON.parse(stdout.slice(stdout.indexOf('{')))
|
|
5732
|
+
if (opts.json) {
|
|
5733
|
+
console.log(JSON.stringify(body))
|
|
5734
|
+
} else if (body.dryRun) {
|
|
5735
|
+
console.log(`概念 ${body.nodes} 个、链接 ${body.links} 条(--dry-run:没有写文件)`)
|
|
5736
|
+
} else {
|
|
5737
|
+
console.log(chalk.green(`✓ 已生成 ${body.out}`))
|
|
5738
|
+
console.log(chalk.gray(` 概念 ${body.nodes} / 链接 ${body.links} / ${body.mb} MB · ${opts.flat ? '2D' : '3D'} · 双击就能逛,不需要服务器`))
|
|
5739
|
+
}
|
|
5740
|
+
if (!opts.dryRun && opts.open && body.out) await openLocalUrl(body.out)
|
|
5741
|
+
} catch (error) {
|
|
5742
|
+
if (opts.json) {
|
|
5743
|
+
console.log(JSON.stringify({ success: false, code: 'GRAPH_FAILED', action: 'graph-3d', error: safeSubprocessError(error) }))
|
|
5744
|
+
} else {
|
|
5745
|
+
console.error(chalk.red(`\n✗ ${safeSubprocessError(error)}`))
|
|
5746
|
+
}
|
|
5747
|
+
process.exit(1)
|
|
5748
|
+
}
|
|
5749
|
+
})
|
|
5750
|
+
)
|
|
5751
|
+
|
|
5513
5752
|
// wiki lint:知识库体检(全本地:不联网、不调用模型)
|
|
5514
5753
|
program.commands.find(c => c.name() === 'wiki')?.addCommand(
|
|
5515
5754
|
new Command('lint')
|
|
5516
5755
|
.description('知识库体检:断链、孤儿、空页、同名(本地,不联网)')
|
|
5517
|
-
|
|
5756
|
+
// 不给默认值:不传 `--dir` 时体检脚本自己的默认(清单里的**全部线**)。
|
|
5757
|
+
// 这里曾经默认只给文章线,而脚本默认两条线都体检 ⇒"半开半合":命令行跑出来
|
|
5758
|
+
// 与脚本跑出来口径不同,而两边都不报错。
|
|
5759
|
+
.option('--dir <dirs...>', '概念页目录(可重复;不传则体检清单里的全部线)')
|
|
5518
5760
|
.option('--json', '输出机器可读结果')
|
|
5519
5761
|
.action(async (opts) => {
|
|
5520
5762
|
const { execFile } = await import('child_process')
|
|
5521
5763
|
const { promisify } = await import('util')
|
|
5522
5764
|
const execFileAsync = promisify(execFile)
|
|
5523
5765
|
const script = join(resolvePackageRoot(), 'scripts', 'wiki_lint.py')
|
|
5524
|
-
const
|
|
5766
|
+
const dirs = Array.isArray(opts.dir) ? opts.dir : (opts.dir ? [opts.dir] : [])
|
|
5767
|
+
const args = [script, ...dirs.flatMap((d: string) => ['--dir', String(d)]),
|
|
5768
|
+
...(opts.json ? ['--json'] : [])]
|
|
5525
5769
|
try {
|
|
5526
5770
|
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
5527
5771
|
timeout: 120_000, maxBuffer: 20 * 1024 * 1024, env: pythonProcessEnv(),
|
|
@@ -5543,6 +5787,8 @@ program
|
|
|
5543
5787
|
.option('--limit <n>', '最多处理几个会话', '40')
|
|
5544
5788
|
.option('--dry-run', '仅预览:几个会话、多少字符会发给生成模型(只读本地,零出境)')
|
|
5545
5789
|
.option('--yes', '确认把对话发送给生成模型')
|
|
5790
|
+
.option('--transcribe-voice',
|
|
5791
|
+
'把缺转写的语音**本机**识别掉再产卡(本地 whisper,慢、吃 CPU、不出网;配合 --dry-run 就是只补转写、不调模型)')
|
|
5546
5792
|
.option('--json', '输出机器可读结果;执行仍需 --yes')
|
|
5547
5793
|
.action(async (opts) => {
|
|
5548
5794
|
const { execFile } = await import('child_process')
|
|
@@ -5578,6 +5824,7 @@ program
|
|
|
5578
5824
|
}
|
|
5579
5825
|
const args = [script, '--days', String(days), '--limit', String(limit),
|
|
5580
5826
|
...(confirmed ? ['--yes'] : []),
|
|
5827
|
+
...(opts.transcribeVoice ? ['--transcribe-voice'] : []),
|
|
5581
5828
|
...(opts.json ? ['--json'] : []),
|
|
5582
5829
|
...(opts.dryRun ? ['--dry-run'] : [])]
|
|
5583
5830
|
try {
|
|
@@ -5951,7 +6198,7 @@ program
|
|
|
5951
6198
|
const isComplete = (targetDate: string): boolean => Object.values(artifactStatus(targetDate)).every(Boolean)
|
|
5952
6199
|
|
|
5953
6200
|
const runPipeline = (targetDate: string): Promise<number> => new Promise((resolve) => {
|
|
5954
|
-
const args = [pipeline, '--date', targetDate, '--engine', noAi ? 'local' : 'deepseek', '--interest', 'AI'
|
|
6201
|
+
const args = [pipeline, '--date', targetDate, '--engine', noAi ? 'local' : 'deepseek', '--interest', 'AI']
|
|
5955
6202
|
if (noAi) args.push('--no-ai')
|
|
5956
6203
|
// 与 --no-ai 不同:这一条保留判断(Jev),只关掉 LLM 的文字生成。
|
|
5957
6204
|
if (opts.noSummary && !noAi) args.push('--no-summary')
|
|
@@ -7095,6 +7342,10 @@ program
|
|
|
7095
7342
|
console.log(chalk.gray('AI 助手现在可以使用以下工具:'))
|
|
7096
7343
|
} else {
|
|
7097
7344
|
console.log(json)
|
|
7345
|
+
// 机器模式到这里就结束:下面那段工具清单与"复制以上 JSON…"是**给人看的**,原来无条件打在
|
|
7346
|
+
// stdout 上 ⇒ `mcp-config --json-result | jq` 断在第 14 行(2026-10-12 由只读驱动扫描发现;
|
|
7347
|
+
// 写路径本来就早退发 JSON,所以只有读路径这一处漏)。
|
|
7348
|
+
if (opts.jsonResult) return
|
|
7098
7349
|
}
|
|
7099
7350
|
console.log(chalk.cyan('\n可用的 MCP 工具:'))
|
|
7100
7351
|
const STATIC_TOOLS: Array<[string, string]> = [
|
|
@@ -7133,6 +7384,28 @@ program
|
|
|
7133
7384
|
.description('检查运行环境(Python、pip 依赖、数据库配置)')
|
|
7134
7385
|
.option('--json', '输出 JSON 格式,不包含数据库路径或密钥')
|
|
7135
7386
|
.action(async (opts) => {
|
|
7387
|
+
// 微信客户端的**版本**与它自报的**数据目录**:两个分支都要用,所以在这里算一次。
|
|
7388
|
+
// 判据见 `wechatClient.ts`(版本取活进程的 `--user-lib-dir`;取不到就是"未知",不是"没问题")。
|
|
7389
|
+
const client = await detectWechatClient()
|
|
7390
|
+
// 客户端自报的数据目录 vs 配置里那个数据库推出来的根:**只比是不是同一份**,
|
|
7391
|
+
// 路径本身不进机器可读输出(`wechat-check/v1` 一直是不含路径的)。
|
|
7392
|
+
//
|
|
7393
|
+
// **只在 4.x 上比**:这个根是从 `ntDbPath`(4.x 的库)推的,而客户端自报的那条来自 4.x 客户端
|
|
7394
|
+
// —— 只用 3.x 的用户(`dbPath3x`)比出来的"不是同一份"没有意义,而这句话**只在 false 时打印**
|
|
7395
|
+
// ⇒ 那就是一条假警告。判据不成立时给 null("不知道"),不是 false。
|
|
7396
|
+
const { dirname } = await import('node:path')
|
|
7397
|
+
// **四层**:`…/xwechat_files/<wxid>/db_storage/message/message_0.db`
|
|
7398
|
+
// → message → db_storage → <wxid> → xwechat_files。少一层就退到 `<wxid>` 目录,
|
|
7399
|
+
// 于是两个路径永远不相等、永远报"不是同一份"(一个看着很合理的假警告)。
|
|
7400
|
+
const dbPathConfigured = String(configService.get('dataVersion') || '') === '4.x'
|
|
7401
|
+
? String(configService.get('ntDbPath') || '')
|
|
7402
|
+
: ''
|
|
7403
|
+
const configuredRoot = dbPathConfigured
|
|
7404
|
+
? dirname(dirname(dirname(dirname(dbPathConfigured))))
|
|
7405
|
+
: ''
|
|
7406
|
+
const dataDirMatchesConfig = (client.dataDirs.length && configuredRoot)
|
|
7407
|
+
? client.dataDirs.some(dir => samePath(dir, configuredRoot))
|
|
7408
|
+
: null
|
|
7136
7409
|
if (opts.json) {
|
|
7137
7410
|
const { execFileSync } = await import('child_process')
|
|
7138
7411
|
const requiredDependencies = ['sqlcipher3', 'html2text', 'zstandard', 'cryptography']
|
|
@@ -7186,6 +7459,26 @@ program
|
|
|
7186
7459
|
messageChannelLoggedIn: !!configService.get('wechatOcToken'),
|
|
7187
7460
|
daemonRunning: daemonAlive,
|
|
7188
7461
|
},
|
|
7462
|
+
// `version` 只从活进程取;取不到就是 null + 一句 `note`(**不是**"没问题")。
|
|
7463
|
+
// `unusableVersionDirs` 是磁盘上的残留版本目录(没有 `Weixin.dll`)—— 指向它们的加载会失败。
|
|
7464
|
+
wechatClient: {
|
|
7465
|
+
running: client.running,
|
|
7466
|
+
version: client.version,
|
|
7467
|
+
loadedDirs: client.loadedDirs,
|
|
7468
|
+
// 客户端自报的构建 token(并列的第二来源;**不拿它推版本号**,编码规则没解出来)
|
|
7469
|
+
versionTokens: client.versionTokens,
|
|
7470
|
+
note: client.note,
|
|
7471
|
+
unusableVersionDirs: client.unusableVersionDirs,
|
|
7472
|
+
// 客户端**自己说**数据目录在哪(与配置不一致时说明两边不是同一份库)
|
|
7473
|
+
dataDirs: client.dataDirs,
|
|
7474
|
+
// 只报结论(true/false/null),**不带路径** —— 这条 schema 一直是不含路径的
|
|
7475
|
+
dataDirMatchesConfig,
|
|
7476
|
+
// `update.data` 在场 ⇒ 加载路径可能不由命令行决定:这条不确定必须跟着版本一起出去
|
|
7477
|
+
updateData: client.updateData,
|
|
7478
|
+
// 只报条数与最近时间(不读崩溃内容)
|
|
7479
|
+
crashRecords: client.crashRecords,
|
|
7480
|
+
caveats: client.caveats,
|
|
7481
|
+
},
|
|
7189
7482
|
}
|
|
7190
7483
|
console.log(JSON.stringify(data, null, 2))
|
|
7191
7484
|
return
|
|
@@ -7270,7 +7563,38 @@ program
|
|
|
7270
7563
|
}
|
|
7271
7564
|
}
|
|
7272
7565
|
|
|
7273
|
-
// 6.
|
|
7566
|
+
// 6. 微信客户端版本(`client` / `dataDirMatchesConfig` 在 action 开头已经算过)
|
|
7567
|
+
console.log(chalk.white('\n微信客户端:'))
|
|
7568
|
+
if (client.version) {
|
|
7569
|
+
// 并列报上客户端自报的构建 token:这是"哪个构建"的第二个来源(**不用它推版本号**)
|
|
7570
|
+
const token = client.versionTokens.length ? chalk.gray(` · 自报构建 ${client.versionTokens.join('、')}`) : ''
|
|
7571
|
+
console.log(chalk.green(` ✓ 运行时加载 ${client.version}`) + chalk.gray(` (${client.loadedDirs[0]})`) + token)
|
|
7572
|
+
} else {
|
|
7573
|
+
// **说"未知"而不是"没问题"**:取不到就说取不到,别给人一个假的安心
|
|
7574
|
+
console.log(chalk.gray(` ○ 版本未知 — ${client.note}`))
|
|
7575
|
+
}
|
|
7576
|
+
for (const dead of client.unusableVersionDirs) {
|
|
7577
|
+
console.log(chalk.yellow(` ! 残留版本目录 ${dead.dir}:只剩 ${dead.files} 个文件(没有 Weixin.dll)`))
|
|
7578
|
+
console.log(chalk.gray(' 指向它的加载会失败;如果客户端起不来,先看这里'))
|
|
7579
|
+
}
|
|
7580
|
+
// `update.data` 在场时上面那个版本**未必**是加载的那个 —— 这句话要跟着它一起出现
|
|
7581
|
+
for (const caveat of client.caveats) {
|
|
7582
|
+
console.log(chalk.yellow(` ⚠ ${caveat}`))
|
|
7583
|
+
}
|
|
7584
|
+
if (client.dataDirs.length) {
|
|
7585
|
+
console.log(chalk.gray(` · 客户端自报数据目录: ${client.dataDirs.join('、')}`))
|
|
7586
|
+
if (dataDirMatchesConfig === false) {
|
|
7587
|
+
// 不一致 = 两边可能不是同一份库(本机数据目录还有 D 盘软链,这条已经按真身比过)
|
|
7588
|
+
console.log(chalk.yellow(' ⚠ 与配置里那个数据库推出来的根**不是同一份** —— 先确认你在读的是客户端正在写的那份'))
|
|
7589
|
+
}
|
|
7590
|
+
}
|
|
7591
|
+
const crashes = client.crashRecords.reports + client.crashRecords.records
|
|
7592
|
+
if (crashes > 0) {
|
|
7593
|
+
const when = client.crashRecords.newest ? client.crashRecords.newest.slice(0, 16).replace('T', ' ') : '时间读不到'
|
|
7594
|
+
console.log(chalk.gray(` · 客户端崩溃记录 ${crashes} 份(最近 ${when})—— 客户端崩过时数据读不出来,先看这条`))
|
|
7595
|
+
}
|
|
7596
|
+
|
|
7597
|
+
// 7. Assistant (第二大脑)
|
|
7274
7598
|
console.log(chalk.white('\n第二大脑 Agent:'))
|
|
7275
7599
|
const aiKey = configService.get('deepseekApiKey')
|
|
7276
7600
|
const aiBase = configService.get('aiBaseUrl')
|
|
@@ -7491,7 +7815,7 @@ async function showInteractiveMenu() {
|
|
|
7491
7815
|
const { spawn } = await import('child_process')
|
|
7492
7816
|
const pipeline = join(resolvePackageRoot(), 'scripts', 'pipeline.py')
|
|
7493
7817
|
console.log(chalk.cyan(`\n📰 正在生成 ${dateStr} 公众号日报...\n`))
|
|
7494
|
-
const child = spawn(getPythonCommand(), [pipeline, '--date', dateStr, '--interest', 'AI'
|
|
7818
|
+
const child = spawn(getPythonCommand(), [pipeline, '--date', dateStr, '--interest', 'AI'], {
|
|
7495
7819
|
windowsHide: true,
|
|
7496
7820
|
stdio: 'inherit',
|
|
7497
7821
|
env: pythonProcessEnv(apiKey),
|