weflow-cli 1.11.1 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +378 -0
- package/OPERATIONS.md +359 -11
- package/README.md +4 -0
- package/bin/weflow-cli.ts +289 -37
- package/dist/bin/weflow-cli.js +300 -35
- package/dist/bin/weflow-cli.js.map +1 -1
- package/dist/src/core/dbPathService.d.ts.map +1 -1
- package/dist/src/core/dbPathService.js +5 -0
- package/dist/src/core/dbPathService.js.map +1 -1
- package/dist/src/core/ntCore.d.ts +12 -1
- package/dist/src/core/ntCore.d.ts.map +1 -1
- package/dist/src/core/ntCore.js +23 -3
- package/dist/src/core/ntCore.js.map +1 -1
- package/dist/src/core/sqlcipherCore.d.ts +8 -0
- package/dist/src/core/sqlcipherCore.d.ts.map +1 -1
- package/dist/src/core/sqlcipherCore.js +46 -11
- package/dist/src/core/sqlcipherCore.js.map +1 -1
- package/dist/src/services/assistantEval.d.ts.map +1 -1
- package/dist/src/services/assistantEval.js +3 -1
- package/dist/src/services/assistantEval.js.map +1 -1
- package/dist/src/services/assistantTools.d.ts.map +1 -1
- package/dist/src/services/assistantTools.js +25 -7
- package/dist/src/services/assistantTools.js.map +1 -1
- package/dist/src/services/configService.d.ts +3 -0
- package/dist/src/services/configService.d.ts.map +1 -1
- package/dist/src/services/configService.js +6 -3
- package/dist/src/services/configService.js.map +1 -1
- package/dist/src/services/diagnosticReport.d.ts +80 -0
- package/dist/src/services/diagnosticReport.d.ts.map +1 -0
- package/dist/src/services/diagnosticReport.js +191 -0
- package/dist/src/services/diagnosticReport.js.map +1 -0
- package/dist/src/services/exportService.d.ts +7 -0
- package/dist/src/services/exportService.d.ts.map +1 -1
- package/dist/src/services/exportService.js +1 -1
- package/dist/src/services/exportService.js.map +1 -1
- package/dist/src/services/wechatClient.d.ts +9 -3
- package/dist/src/services/wechatClient.d.ts.map +1 -1
- package/dist/src/services/wechatClient.js +55 -22
- package/dist/src/services/wechatClient.js.map +1 -1
- package/docs/DECISIONS.md +662 -1
- package/docs/MCP.md +1 -1
- package/docs/NEXT_ACTIONS.md +236 -0
- package/docs/POSITIONING.md +68 -0
- package/docs/PROJECT_STATE.md +119 -17
- package/docs/RELEASING.md +14 -0
- package/package.json +88 -86
- package/scripts/_utils.py +120 -4
- package/scripts/backfill_articles.py +64 -14
- package/scripts/biz_daily.py +118 -11
- package/scripts/daily_task.ps1 +132 -0
- package/scripts/export_chat_html.py +79 -31
- package/scripts/install_tasks.cmd +32 -0
- package/scripts/install_tasks.ps1 +91 -0
- package/scripts/jev_client.py +335 -3
- package/scripts/nt_decrypt.py +79 -14
- package/scripts/pipeline.py +21 -0
- package/scripts/recall_eval.py +247 -0
- package/scripts/repro_customer_init.cmd +86 -0
- package/scripts/semantic_search.py +926 -51
- package/src/core/dbPathService.ts +5 -0
- package/src/core/ntCore.ts +34 -3
- package/src/core/sqlcipherCore.ts +47 -11
- package/src/services/assistantEval.ts +3 -1
- package/src/services/assistantTools.ts +2471 -2452
- package/src/services/configService.ts +12 -3
- package/src/services/diagnosticReport.ts +237 -0
- package/src/services/exportService.ts +14 -2
- package/src/services/wechatClient.ts +418 -380
package/bin/weflow-cli.ts
CHANGED
|
@@ -11,9 +11,11 @@ import { dbPathService } from '../src/core/dbPathService.js'
|
|
|
11
11
|
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
|
+
import { renderDiagnosticReport } from '../src/services/diagnosticReport.js'
|
|
14
15
|
import { chatService } from '../src/services/chatService.js'
|
|
15
16
|
import { emptyMessagesNote, looksLikeTalkerId } from '../src/services/messageQuery.js'
|
|
16
17
|
import { exportService } from '../src/services/exportService.js'
|
|
18
|
+
import type { ExportResult } from '../src/services/exportService.js'
|
|
17
19
|
import { writeEvidencePackage } from '../src/services/evidenceService.js'
|
|
18
20
|
import { resolveTalker as resolveTalkerCore } from '../src/utils/talkerUtils.js'
|
|
19
21
|
import { getPythonCommand } from '../src/utils/python.js'
|
|
@@ -340,7 +342,9 @@ program
|
|
|
340
342
|
// 只读、纯本地、不调模型。**故意没有 mcp 那一栏**(D-062):它给的是关于第三方的推断
|
|
341
343
|
// ——谁在被冷落、你会失去谁 —— 所以不进助手、不进 MCP、不喂模型。
|
|
342
344
|
bonds: { cli: 'bonds --json', mcp: null, leavesTheMachine: false },
|
|
343
|
-
|
|
345
|
+
// `check --report` 是同一份事实的**给人版**(已脱敏、可直接贴给 AI 或 issue),
|
|
346
|
+
// 所以它跟 `--json` 并列在这里:外部程序按这条能力去找"怎么拿诊断",不该只知道 JSON 那条。
|
|
347
|
+
diagnostics: { cli: 'check --json', report: 'check --report', leavesTheMachine: false },
|
|
344
348
|
todos: { cli: 'todos list --json' },
|
|
345
349
|
// 2026-09-29 三个界面对齐时新开的读取能力(D-058)。逐个写清它会不会出境,
|
|
346
350
|
// 因为其中两个会——`fetch_article` 白名单 + 逐跳复检,`search_public` 没有任何守卫。
|
|
@@ -854,6 +858,26 @@ program
|
|
|
854
858
|
// Step 3: 提取密钥
|
|
855
859
|
console.log(chalk.yellow('\n步骤 3/3: 提取数据库解密密钥...'))
|
|
856
860
|
|
|
861
|
+
// **先看 Python 在不在**(Windows):它决定"读库"这一整步能不能做。
|
|
862
|
+
// 以前不看,于是 Python 不可用时还要先等 120 秒 hook 超时,然后把用户引向
|
|
863
|
+
// "退出微信重试"——而真正的原因(商店占位程序 / 缺 sqlcipher3)一个字都没提。
|
|
864
|
+
// 2026-10-19 用一台"PATH 里没有 python"的机器实测,2 分钟换来一条误导性建议。
|
|
865
|
+
let pythonUsable = process.platform !== 'win32'
|
|
866
|
+
if (process.platform === 'win32' && !opts.testMissingKeys) {
|
|
867
|
+
try {
|
|
868
|
+
const { execFileSync } = await import('node:child_process')
|
|
869
|
+
execFileSync(getPythonCommand(), ['--version'], { windowsHide: true, stdio: 'ignore', timeout: 5000 })
|
|
870
|
+
pythonUsable = true
|
|
871
|
+
} catch {
|
|
872
|
+
pythonUsable = false
|
|
873
|
+
console.log(chalk.red(' ✗ 没有可用的 Python —— 读微信库这一步跑不起来'))
|
|
874
|
+
console.log(chalk.gray(' NT 数据库扫描与密钥派生都靠它(需要 Python 3.10+ 且能 import sqlcipher3)。'))
|
|
875
|
+
console.log(chalk.gray(' Windows 上最常见的原因是只装了应用商店的 python 占位程序:'))
|
|
876
|
+
console.log(chalk.gray(' 在终端里运行 `python --version`,能打出版本号才算装对。'))
|
|
877
|
+
console.log(chalk.gray(' 跑 `weflow-cli check --report` 可以看到完整判定(那段话可直接贴给 AI)。'))
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
857
881
|
// 优先尝试从 WeFlow 配置读取
|
|
858
882
|
console.log(chalk.gray(' 尝试从 WeFlow 桌面版配置读取...'))
|
|
859
883
|
let extractedKey = opts.testMissingKeys ? '' : tryReadWeFlowKey()
|
|
@@ -921,6 +945,22 @@ program
|
|
|
921
945
|
process.exit(1)
|
|
922
946
|
}
|
|
923
947
|
}
|
|
948
|
+
} else if (!pythonUsable) {
|
|
949
|
+
// Python 不可用时**不去等 hook**:就算 hook 抓到了密钥,Step 4 的扫描/派生照样跑不起来,
|
|
950
|
+
// 而那 120 秒的等待只会把用户引向"退出微信重试"这种无效动作。
|
|
951
|
+
const fallback = configService.get('favPassphrase') || configService.get('decryptKey') || ''
|
|
952
|
+
if (fallback) {
|
|
953
|
+
console.log(chalk.gray(' 跳过 hook 等待:已配置的密钥会原样保留,但要先修好 Python 才读得动'))
|
|
954
|
+
extractedKey = fallback
|
|
955
|
+
} else {
|
|
956
|
+
console.log(chalk.red('\n✗ 这台机器现在无法读取微信数据库(缺 Python),因此没有可用密钥'))
|
|
957
|
+
console.log(chalk.gray('\n按顺序做:'))
|
|
958
|
+
console.log(chalk.gray(' 1) 装一个真正的 Python 3.10+,确认 `python --version` 能打出版本号'))
|
|
959
|
+
console.log(chalk.gray(' 2) `pip install sqlcipher3`'))
|
|
960
|
+
console.log(chalk.gray(' 3) 重新 `weflow-cli init`'))
|
|
961
|
+
console.log(chalk.gray(' 卡住时跑 `weflow-cli check --report`,那段文字可直接贴给 AI 或贴到 issue'))
|
|
962
|
+
process.exit(1)
|
|
963
|
+
}
|
|
924
964
|
} else if (process.platform !== 'win32') {
|
|
925
965
|
// macOS:自动提取不可用,引导手动提供密钥
|
|
926
966
|
console.log(chalk.yellow(' ⚠ 当前平台无法自动提取密钥(自动提取支持 Windows/Linux)'))
|
|
@@ -931,17 +971,23 @@ program
|
|
|
931
971
|
console.log(chalk.gray(' 4. 数据目录与账号已完成配置,设置密钥后即可使用 sessions/messages 等命令'))
|
|
932
972
|
process.exit(1)
|
|
933
973
|
} else {
|
|
934
|
-
//
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
974
|
+
// hook 只在**登录那一瞬**触发。所以要不要等,取决于微信现在在不在跑:
|
|
975
|
+
// · 已在运行 → 用户早就登录了,hook 永远不会再触发,等满 2 分钟纯属浪费
|
|
976
|
+
// (2026-10-19 用一个空配置实测:每次 init 都白等)
|
|
977
|
+
// · 没在运行 → 这正是 README「三分钟上手」第 2 步要求的姿势(关掉微信再 init,
|
|
978
|
+
// 然后按提示启动并登录)。人要打开客户端、等它起来、扫码/点确认,
|
|
979
|
+
// 30 秒根本不够,**必须留足**,否则按文档操作的人会被那句
|
|
980
|
+
// "hook 没抓到" 误导。所以就按文档给满 120 秒。
|
|
981
|
+
let keyResult: { success: boolean; key?: string; error?: string } = { success: false }
|
|
982
|
+
if (version) {
|
|
983
|
+
console.log(chalk.gray(' 微信已在运行:hook 只在登录那一瞬触发,不再等待,直接尝试扫描/派生'))
|
|
984
|
+
} else {
|
|
985
|
+
console.log(chalk.cyan('\n 请现在启动微信并登录(登录那一瞬会自动抓取密钥)'))
|
|
986
|
+
console.log(chalk.cyan(' 最多等 2 分钟;不启动也没关系,之后会继续尝试 NT 扫描 / passphrase 派生\n'))
|
|
987
|
+
keyResult = await keyService.waitForKey(120000, (msg) => {
|
|
988
|
+
console.log(chalk.gray(` ${msg}`))
|
|
989
|
+
})
|
|
939
990
|
}
|
|
940
|
-
console.log(chalk.gray(' 等待微信进程出现(启动后会自动 hook)...'))
|
|
941
|
-
|
|
942
|
-
const keyResult = await keyService.waitForKey(120000, (msg) => {
|
|
943
|
-
console.log(chalk.gray(` ${msg}`))
|
|
944
|
-
})
|
|
945
991
|
|
|
946
992
|
if (keyResult.success && keyResult.key) {
|
|
947
993
|
extractedKey = keyResult.key
|
|
@@ -958,12 +1004,13 @@ program
|
|
|
958
1004
|
console.log(chalk.gray(' 检测到已配置的密钥, 沿用它并重新验证各库...'))
|
|
959
1005
|
extractedKey = fallback
|
|
960
1006
|
} else {
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1007
|
+
// **不退出**:hook 只在"登录那一瞬"触发,而一个正常用户装完工具时微信早就登录了,
|
|
1008
|
+
// 于是这条 120 秒的等待**必然**超时。以前这里 process.exit(1),用户就被卡在
|
|
1009
|
+
// 「退出微信后重新运行 init」——而下一步(Step 4 的 NT 扫描 / 从 passphrase 派生)
|
|
1010
|
+
// 才是新版本真正能拿到密钥的地方,却永远不会被执行(2026-10-19 用空配置实测)。
|
|
1011
|
+
// 现在改成继续往下走:拿得到就成功,拿不到由结尾统一判"未完成"。
|
|
1012
|
+
console.log(chalk.yellow('\n Hook 没抓到密钥(它只在登录那一瞬触发,已登录时不会再有)'))
|
|
1013
|
+
console.log(chalk.gray(' 继续尝试 NT 扫描 / passphrase 派生 —— 这是新版本主要的取密钥方式...'))
|
|
967
1014
|
}
|
|
968
1015
|
}
|
|
969
1016
|
}
|
|
@@ -971,9 +1018,13 @@ program
|
|
|
971
1018
|
if (extractedKey) {
|
|
972
1019
|
configService.set('decryptKey', extractedKey)
|
|
973
1020
|
if (keyCaptured) console.log(chalk.green('\n✓ 密钥获取成功!'))
|
|
974
|
-
else console.log(chalk.green('\n✓ 已沿用配置中的密钥 (本次未重新捕获)'))
|
|
1021
|
+
else if (pythonUsable) console.log(chalk.green('\n✓ 已沿用配置中的密钥 (本次未重新捕获)'))
|
|
1022
|
+
// 缺 Python 时**不能说"已沿用"**:那句话在这里会被读成"密钥没问题",
|
|
1023
|
+
// 而下面紧接着就会说"密钥未就绪",两句自相矛盾(2026-10-19 实跑看到)。
|
|
1024
|
+
else console.log(chalk.yellow('\n· 配置里的密钥原样保留,但**这次验证不了**(缺 Python)——它可能仍然可用'))
|
|
975
1025
|
} else {
|
|
976
|
-
|
|
1026
|
+
// 空密钥时不能说"配置完成":下一句就是"密钥未就绪",两句打架。
|
|
1027
|
+
console.log(chalk.yellow('\n· 本次没有拿到密钥,继续尝试 NT 扫描 / passphrase 派生'))
|
|
977
1028
|
}
|
|
978
1029
|
|
|
979
1030
|
// Step 4: 尝试扫描 NT 格式数据库 (xwechat_files)
|
|
@@ -981,6 +1032,11 @@ program
|
|
|
981
1032
|
// 进入这里已经确认是 4.x 初始化流程。不要依赖步骤 0 的 version:
|
|
982
1033
|
// 微信可能是在 init 等待期间才启动,初始 version 会保持为 null。
|
|
983
1034
|
// Linux 分支已在步骤 3 完成配置,跳过。
|
|
1035
|
+
// 这次 init 到底有没有**取得可用的密钥** —— 只有它决定结尾能不能说"初始化完成"。
|
|
1036
|
+
// 扫描失败 + 沿用旧密钥 = 一次看着成功、实际什么也读不到的初始化:2026-10-18 一位用户
|
|
1037
|
+
// 的 init 打在 "NT scan failed (exit 9009)" 上(Python 是商店占位程序),结尾照样报
|
|
1038
|
+
// "初始化完成!"、"密钥: 已安全保存",然后 sessions 一条都没有。
|
|
1039
|
+
let ntReady = process.platform === 'linux' // Linux 分支已在步骤 3 配好
|
|
984
1040
|
if (process.platform !== 'linux') {
|
|
985
1041
|
console.log(chalk.yellow('\n步骤 4/4: 扫描 NT 格式数据库...'))
|
|
986
1042
|
console.log(chalk.gray(' 正在从微信内存中匹配数据库密钥...'))
|
|
@@ -998,6 +1054,7 @@ program
|
|
|
998
1054
|
}
|
|
999
1055
|
|
|
1000
1056
|
if (primaryDb) {
|
|
1057
|
+
ntReady = true
|
|
1001
1058
|
configService.set('ntDbPath', primaryDb.path)
|
|
1002
1059
|
configService.set('ntKey', primaryDb.key)
|
|
1003
1060
|
configService.set('ntSalt', primaryDb.salt)
|
|
@@ -1047,6 +1104,7 @@ program
|
|
|
1047
1104
|
}
|
|
1048
1105
|
|
|
1049
1106
|
if (derivedMessage) {
|
|
1107
|
+
ntReady = true
|
|
1050
1108
|
console.log(chalk.green(' ✓ 派生模式初始化成功 (聊天/联系人/朋友圈库已配置)'))
|
|
1051
1109
|
} else {
|
|
1052
1110
|
// 即使密钥未匹配,仍尝试自动发现 sns.db 路径和盐值
|
|
@@ -1065,23 +1123,40 @@ program
|
|
|
1065
1123
|
}
|
|
1066
1124
|
|
|
1067
1125
|
console.log(chalk.cyan('\n=============================='))
|
|
1068
|
-
|
|
1126
|
+
// 只有**这次真的拿到可用密钥**才叫"完成"。否则说清楚"没就绪"以及下一步做什么 ——
|
|
1127
|
+
// 报"完成"再让 sessions 空着,是最费用户时间的失败方式(他会以为是自己微信的问题)。
|
|
1128
|
+
console.log(chalk[ntReady ? 'cyan' : 'yellow'](
|
|
1129
|
+
ntReady ? '初始化完成! (4.x 模式)' : '初始化未完成:数据库密钥没有就绪'))
|
|
1069
1130
|
console.log(chalk.cyan('=============================='))
|
|
1070
1131
|
console.log(chalk.white(`数据目录: ${detected.path}`))
|
|
1071
1132
|
console.log(chalk.white(`账号: ${selectedWxid.wxid}${selectedWxid.nickname ? ` (${selectedWxid.nickname})` : ''}`))
|
|
1072
|
-
|
|
1073
|
-
console.log(chalk.white('密钥: 已安全保存'))
|
|
1133
|
+
console.log(chalk.white(`密钥: ${ntReady ? '已安全保存' : '未就绪(配置里的旧密钥未必能解开这个库)'}`))
|
|
1074
1134
|
const ntDbPath = configService.get('ntDbPath')
|
|
1075
1135
|
if (ntDbPath) {
|
|
1076
1136
|
console.log(chalk.white(`NT 数据库: ${ntDbPath}`))
|
|
1077
1137
|
}
|
|
1138
|
+
if (!ntReady) {
|
|
1139
|
+
console.log(chalk.yellow('\n现在 sessions / messages 会读不到数据。按顺序做这三步:'))
|
|
1140
|
+
console.log(chalk.yellow(' 1) 在这个终端里运行 `python --version`,要能打出版本号'))
|
|
1141
|
+
console.log(chalk.yellow(' (Windows 上最常见的问题是只装了商店的 python 占位程序)'))
|
|
1142
|
+
console.log(chalk.yellow(' 2) `pip install sqlcipher3`'))
|
|
1143
|
+
console.log(chalk.yellow(' 3) `weflow-cli config forget-keys`,然后重新 `weflow-cli init`'))
|
|
1144
|
+
// 这三步是"最常见的原因",不是诊断结论。真卡住时让用户拿到一份**已经判定过**的事实,
|
|
1145
|
+
// 而不是再让他把现象口头描述一遍(那正是 issue #3/#9/#10 每次都要重来一遍的原因)。
|
|
1146
|
+
console.log(chalk.yellow('\n想先看清到底卡在哪一步,运行:'))
|
|
1147
|
+
console.log(chalk.cyan(' weflow-cli check --report'))
|
|
1148
|
+
console.log(chalk.gray(' 它会给出结论(Python / sqlcipher3 / 密钥 / 数据目录),那段文字可以直接复制给 AI 或贴到 issue。'))
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// 未就绪时说"现在可以使用"是骗人的:那几条命令都会返回空。
|
|
1152
|
+
console.log(chalk.gray(ntReady
|
|
1153
|
+
? '\n现在可以使用以下命令:'
|
|
1154
|
+
: '\n修好上面那几步之后,这些命令才会返回数据:'))
|
|
1155
|
+
console.log(chalk.gray(' weflow-cli sessions 查看会话列表'))
|
|
1156
|
+
console.log(chalk.gray(' weflow-cli messages <talker> 查看消息'))
|
|
1157
|
+
console.log(chalk.gray(' weflow-cli contacts 查看联系人'))
|
|
1158
|
+
console.log(chalk.gray(' weflow-cli export <talker> <format> 导出聊天记录'))
|
|
1078
1159
|
}
|
|
1079
|
-
|
|
1080
|
-
console.log(chalk.gray('\n现在可以使用以下命令:'))
|
|
1081
|
-
console.log(chalk.gray(' weflow-cli sessions 查看会话列表'))
|
|
1082
|
-
console.log(chalk.gray(' weflow-cli messages <talker> 查看消息'))
|
|
1083
|
-
console.log(chalk.gray(' weflow-cli contacts 查看联系人'))
|
|
1084
|
-
console.log(chalk.gray(' weflow-cli export <talker> <format> 导出聊天记录'))
|
|
1085
1160
|
})
|
|
1086
1161
|
|
|
1087
1162
|
// ==================== config ====================
|
|
@@ -1094,6 +1169,7 @@ const configurableKeys = [
|
|
|
1094
1169
|
'ntDbPath', 'ntKey', 'ntSalt', 'contactDbPath', 'contactKey', 'contactSalt',
|
|
1095
1170
|
'vaultRepo', 'aiEngine', 'aiBaseUrl', 'aiModel', 'deepseekApiKey', 'typesafeApiKey',
|
|
1096
1171
|
'dashscopeApiKey', 'favPassphrase',
|
|
1172
|
+
'judgeProvider', 'openrouterApiKey', 'openrouterJudgeModel',
|
|
1097
1173
|
'wereadApiKey',
|
|
1098
1174
|
'assistantPrivacy', 'assistantWhitelist', 'assistantGroupWhitelist',
|
|
1099
1175
|
'assistantGroupRequireMention', 'assistantFastRoute', 'assistantPanelUser', 'quickReplyContacts',
|
|
@@ -1343,6 +1419,12 @@ program
|
|
|
1343
1419
|
if (sessions.length === 0) {
|
|
1344
1420
|
if (opts.json) { console.log(JSON.stringify({ success: true, sessions: [] })); return }
|
|
1345
1421
|
console.log(chalk.gray('未找到会话'))
|
|
1422
|
+
// "0 条"有两种完全不同的原因:真的没有会话,和**初始化/密钥坏了读不出来**。
|
|
1423
|
+
// 后者以前只显示这一句,用户就去猜"是不是我微信坏了 / 版本太新"(issue #3/#9/#10 的老路:
|
|
1424
|
+
// 同一个根因被描述成三种现象)。给一条能立刻分清的动作,比让他猜有用。
|
|
1425
|
+
console.log(chalk.yellow('\n如果微信里明明有聊天记录,那多半是读取这一步没成功。运行这条看结论:'))
|
|
1426
|
+
console.log(chalk.cyan(' weflow-cli check --report'))
|
|
1427
|
+
console.log(chalk.gray(' 它会指出卡在哪一步(Python / sqlcipher3 / 密钥 / 数据目录),那段文字可直接复制给 AI 或贴到 issue。'))
|
|
1346
1428
|
return
|
|
1347
1429
|
}
|
|
1348
1430
|
|
|
@@ -1874,8 +1956,12 @@ program
|
|
|
1874
1956
|
}
|
|
1875
1957
|
// 图片要靠 Python 那条路匹配(缓存缩略图、资源 md5、原图那档)。`--media` 就是"先抽一次",
|
|
1876
1958
|
// 抽完 json/txt/excel 各自消费同一份清单 —— 匹配只跑一遍,也不会在 TS 里重写一遍。
|
|
1959
|
+
// 抽取那一步的媒体摘要留在这里:`--media` 的调用者(机器调用)应当能从结果里直接读到
|
|
1960
|
+
// "各状态多少条、报告叫什么",而不必去翻 Python 的 stdout。
|
|
1961
|
+
let extracted: ExportResult | undefined
|
|
1877
1962
|
if (opts.media && (format === 'json' || format === 'txt' || format === 'excel')) {
|
|
1878
1963
|
const media = await exportService.extractMedia(talker, opts.output, !!opts.json, { fullImages: opts.fullImages === true }, limit, opts.date || '')
|
|
1964
|
+
extracted = media
|
|
1879
1965
|
if (!media.success) {
|
|
1880
1966
|
if (opts.json) console.log(JSON.stringify({ success: false, code: 'MEDIA_EXPORT_FAILED', error: media.error || '媒体抽取失败' }))
|
|
1881
1967
|
else console.log(chalk.red(`✗ 媒体抽取失败: ${media.error}`))
|
|
@@ -1909,7 +1995,16 @@ program
|
|
|
1909
1995
|
}
|
|
1910
1996
|
|
|
1911
1997
|
if (result?.success) {
|
|
1912
|
-
if (opts.json) console.log(JSON.stringify({
|
|
1998
|
+
if (opts.json) console.log(JSON.stringify({
|
|
1999
|
+
success: true,
|
|
2000
|
+
format,
|
|
2001
|
+
contract: format === 'json' ? opts.contract : null,
|
|
2002
|
+
path: result.path,
|
|
2003
|
+
count: result.count ?? null,
|
|
2004
|
+
// 媒体摘要:html/md 那条由导出自己产出(Python 的 media_json_summary),
|
|
2005
|
+
// json/txt/excel 的 `--media` 则由上面那次抽取产出 —— 两条都是同一份词汇。
|
|
2006
|
+
media: result.media ?? extracted?.media ?? null,
|
|
2007
|
+
}, null, 2))
|
|
1913
2008
|
else console.log(chalk.green(`✓ 导出成功: ${result.path}`))
|
|
1914
2009
|
} else {
|
|
1915
2010
|
if (opts.json) console.log(JSON.stringify({ success: false, code: 'EXPORT_FAILED', error: result?.error || '导出失败' }))
|
|
@@ -4985,6 +5080,14 @@ program
|
|
|
4985
5080
|
.option('--top-k <n>', '返回数量', '10')
|
|
4986
5081
|
.option('--line <line>', 'wiki=文章线(默认) / chat=聊天记录 / all=两条都搜', 'wiki')
|
|
4987
5082
|
.option('--no-rerank', '不调用决策模型重排,只按向量/关键词相似度返回(回退到引入重排之前)')
|
|
5083
|
+
.option('--no-hybrid', '不融合本地 BM25 名次(默认融合;门控后实测只增不减)')
|
|
5084
|
+
.option('--rerank-pool <n>', '重排看多少条候选(默认 100;越大越准也越贵)')
|
|
5085
|
+
.option('--no-dedup', '不折叠同文本的多号转载(默认折叠,结果里带 duplicateCount)')
|
|
5086
|
+
.option('--with-text [n]', '给每条命中挂上本地正文(fullText / sourceFile / url);n 为每条最多多少字')
|
|
5087
|
+
.option('--fetch-missing [n]', '正文太薄时按 url 抓一次(最多 n 条,默认 3;复用日报缓存)')
|
|
5088
|
+
.option('--mmr', '对结果做 MMR 去冗余(压掉近似的同领域邻居,可能牺牲单点召回)')
|
|
5089
|
+
.option('--from <account>', '只查这个公众号的文章(按文件名匹配,不走语义)')
|
|
5090
|
+
.option('--phrase', '把这串当**原样短语**在本地定位(FTS5/trigram,不走向量、不出境、不花钱):回答"哪个号在哪天原样写过它"')
|
|
4988
5091
|
.option('--api-key <key>', 'DashScope embedding API key')
|
|
4989
5092
|
.option('--dry-run', '仅预览,不读取索引或调用向量服务')
|
|
4990
5093
|
.option('--yes', '确认执行可能调用云端向量服务的搜索')
|
|
@@ -4996,23 +5099,59 @@ program
|
|
|
4996
5099
|
const pkgRoot = resolvePackageRoot()
|
|
4997
5100
|
const script = join(pkgRoot, 'scripts', 'semantic_search.py')
|
|
4998
5101
|
const topK = parseCliInteger(opts.topK, 'top-k', 1, 100, opts.json)
|
|
5102
|
+
// 没传就**不转发**(默认值只在 Python 侧定义一处):commander 对未提供的选项给 undefined,
|
|
5103
|
+
// 直接 parseCliInteger 会被自己的校验判成非法参数 —— 实测就是这么红的。
|
|
5104
|
+
const rerankPool = opts.rerankPool
|
|
5105
|
+
? parseCliInteger(opts.rerankPool, 'rerank-pool', 5, 500, opts.json) : null
|
|
4999
5106
|
requireCliChoice(opts.line, ['wiki', 'chat', 'all'], 'line', opts.json)
|
|
5107
|
+
// `--phrase` 走的是**纯本地**那条路(FTS5):不出境、不花钱,所以它既不需要向量 key,
|
|
5108
|
+
// 也不需要确认门。把它当成"向量搜索的降级"会让人以为它也花钱、也发数据。
|
|
5109
|
+
const phrase = opts.phrase === true
|
|
5110
|
+
// 下面这些旗标只属于**向量检索**那条路;`--phrase`(FTS5 原样定位)一个都用不上。
|
|
5111
|
+
// 以前它们会被原样转发,然后在 Python 的 phrase 分支里被**静默忽略** —— 用户以为自己在调参,
|
|
5112
|
+
// 实际上什么都没发生。这类"接受但不生效"的开关比报错更贵:它会让人把默认行为当成调过的结果。
|
|
5113
|
+
if (phrase) {
|
|
5114
|
+
const irrelevant: string[] = []
|
|
5115
|
+
if (opts.rerank === false) irrelevant.push('--no-rerank')
|
|
5116
|
+
if (opts.hybrid === false) irrelevant.push('--no-hybrid')
|
|
5117
|
+
if (rerankPool != null) irrelevant.push('--rerank-pool')
|
|
5118
|
+
if (opts.dedup === false) irrelevant.push('--no-dedup')
|
|
5119
|
+
if (opts.withText) irrelevant.push('--with-text')
|
|
5120
|
+
if (opts.fetchMissing) irrelevant.push('--fetch-missing')
|
|
5121
|
+
if (opts.mmr) irrelevant.push('--mmr')
|
|
5122
|
+
if (irrelevant.length) {
|
|
5123
|
+
const error = `--phrase 只做原样定位,不接受 ${irrelevant.join('、')}(它们只对向量检索有意义)`
|
|
5124
|
+
if (opts.json) console.log(JSON.stringify({ success: false, code: 'INVALID_ARGUMENT', error }))
|
|
5125
|
+
else console.log(chalk.red(error))
|
|
5126
|
+
process.exit(1)
|
|
5127
|
+
}
|
|
5128
|
+
}
|
|
5000
5129
|
const preview = {
|
|
5001
5130
|
success: true,
|
|
5002
5131
|
dryRun: true,
|
|
5003
5132
|
action: 'semantic-search.query',
|
|
5133
|
+
phrase,
|
|
5004
5134
|
topK,
|
|
5005
5135
|
line: opts.line,
|
|
5006
5136
|
noRerank: opts.rerank === false,
|
|
5137
|
+
noHybrid: opts.hybrid === false,
|
|
5138
|
+
rerankPool,
|
|
5139
|
+
noDedup: opts.dedup === false,
|
|
5140
|
+
withText: opts.withText || null,
|
|
5141
|
+
fetchMissing: opts.fetchMissing || null,
|
|
5142
|
+
mmr: opts.mmr === true,
|
|
5143
|
+
from: opts.from || null,
|
|
5007
5144
|
readsLocalIndex: true,
|
|
5008
|
-
mayUseCloudEmbedding:
|
|
5145
|
+
mayUseCloudEmbedding: !phrase,
|
|
5009
5146
|
}
|
|
5010
5147
|
if (opts.dryRun) {
|
|
5011
5148
|
if (opts.json) console.log(JSON.stringify(preview))
|
|
5012
|
-
else console.log(chalk.cyan(
|
|
5149
|
+
else console.log(chalk.cyan(phrase
|
|
5150
|
+
? '预览:原样短语定位 —— 只读本地短语索引,不联网、不花钱。'
|
|
5151
|
+
: '预览:将读取本地语义索引,并可能把查询发送到已配置的向量服务。'))
|
|
5013
5152
|
return
|
|
5014
5153
|
}
|
|
5015
|
-
if (!opts.yes) {
|
|
5154
|
+
if (!phrase && !opts.yes) {
|
|
5016
5155
|
if (opts.json) {
|
|
5017
5156
|
console.log(JSON.stringify({ ...preview, success: false, dryRun: false, code: 'CONFIRMATION_REQUIRED' }))
|
|
5018
5157
|
process.exit(1)
|
|
@@ -5032,7 +5171,16 @@ program
|
|
|
5032
5171
|
// 接这条通道之前,`OPERATIONS.md` 一直在教用户敲 `search --no-rerank`,而 CLI 会答 `unknown option`
|
|
5033
5172
|
// —— 同一份文档里 `--line` 是通的,所以它看起来像"能用的旗标"(2026-10-11 由文档命令面那道检查发现)。
|
|
5034
5173
|
const args: string[] = [script, 'search', '--top-k', String(topK), '--line', opts.line,
|
|
5035
|
-
...(
|
|
5174
|
+
...(phrase ? ['--phrase'] : []),
|
|
5175
|
+
...(opts.rerank === false ? ['--no-rerank'] : []),
|
|
5176
|
+
...(opts.hybrid === false ? ['--no-hybrid'] : []),
|
|
5177
|
+
...(opts.from ? ['--from', opts.from] : []),
|
|
5178
|
+
...(rerankPool ? ['--rerank-pool', String(rerankPool)] : []),
|
|
5179
|
+
...(opts.dedup === false ? ['--no-dedup'] : []),
|
|
5180
|
+
...(opts.withText ? ['--with-text', opts.withText === true ? '4000' : String(opts.withText)] : []),
|
|
5181
|
+
...(opts.fetchMissing ? ['--fetch-missing',
|
|
5182
|
+
opts.fetchMissing === true ? '3' : String(opts.fetchMissing)] : []),
|
|
5183
|
+
...(opts.mmr ? ['--mmr'] : [])]
|
|
5036
5184
|
try {
|
|
5037
5185
|
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
5038
5186
|
timeout: 60_000, maxBuffer: 10 * 1024 * 1024,
|
|
@@ -5052,6 +5200,7 @@ program
|
|
|
5052
5200
|
.command('search-index')
|
|
5053
5201
|
.description('构建语义搜索索引')
|
|
5054
5202
|
.option('--full', '全量重建')
|
|
5203
|
+
.option('--phrase', '只建**精确短语索引**(本地 FTS5/trigram,不调用向量服务、不花钱、不出境)')
|
|
5055
5204
|
.option('--api-key <key>', 'DashScope embedding API key')
|
|
5056
5205
|
.option('--days <n>', '聊天记录收最近多少天(默认 90)')
|
|
5057
5206
|
.option('--article-days <n>', '日报文章收最近多少个日期目录(默认 30)')
|
|
@@ -5059,6 +5208,35 @@ program
|
|
|
5059
5208
|
.option('--yes', '确认构建或重建索引')
|
|
5060
5209
|
.option('--json', '输出 JSON 格式')
|
|
5061
5210
|
.action(async (opts) => {
|
|
5211
|
+
// `--phrase`:短语索引那条路完全不碰向量服务(也不碰聊天库——它从语义索引的 meta.json
|
|
5212
|
+
// 派生),所以它不走窗口参数、不需要确认门,也不校验 key。混进下面那条路会让用户以为
|
|
5213
|
+
// "建个本地的检索也要花 vector 的钱"。
|
|
5214
|
+
if (opts.phrase) {
|
|
5215
|
+
const phrasePreview = {
|
|
5216
|
+
success: true, dryRun: true, action: 'search-index.build-phrase',
|
|
5217
|
+
readsLocalData: true, usesCloudEmbedding: false, rebuildsPhraseIndex: true,
|
|
5218
|
+
}
|
|
5219
|
+
if (opts.dryRun) {
|
|
5220
|
+
if (opts.json) console.log(JSON.stringify(phrasePreview))
|
|
5221
|
+
else console.log(chalk.cyan('短语索引预览:全量重建本地 FTS5 索引(含两条线),不联网、不花钱。'))
|
|
5222
|
+
return
|
|
5223
|
+
}
|
|
5224
|
+
const { execFile } = await import('child_process')
|
|
5225
|
+
const { promisify } = await import('util')
|
|
5226
|
+
const execFileAsync = promisify(execFile)
|
|
5227
|
+
const pkgRoot = resolvePackageRoot()
|
|
5228
|
+
const script = join(pkgRoot, 'scripts', 'semantic_search.py')
|
|
5229
|
+
try {
|
|
5230
|
+
const { stdout } = await execFileAsync(getPythonCommand(),
|
|
5231
|
+
[script, 'phrase-build', '--line', 'all'],
|
|
5232
|
+
{ timeout: 600_000, maxBuffer: 10 * 1024 * 1024, env: pythonProcessEnv() })
|
|
5233
|
+
console.log(opts.json ? stdout.trim() : stdout)
|
|
5234
|
+
} catch (e: any) {
|
|
5235
|
+
console.error(chalk.red(`\n✗ ${safeSubprocessError(e, '建短语索引失败')}`))
|
|
5236
|
+
process.exit(1)
|
|
5237
|
+
}
|
|
5238
|
+
return
|
|
5239
|
+
}
|
|
5062
5240
|
// 窗口的**运行值由这里说了算**:默认值 + 校验 + 透传都在这一处,预览报的就是真跑的那些
|
|
5063
5241
|
// (脚本文档里另有自己的默认值,只供手动直接跑脚本时用;CLI 这条路永远显式传参,
|
|
5064
5242
|
// 所以两边万一漂了也不会影响实际行为)。
|
|
@@ -5113,7 +5291,10 @@ program
|
|
|
5113
5291
|
if (opts.full) args.push('--full')
|
|
5114
5292
|
try {
|
|
5115
5293
|
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
5116
|
-
|
|
5294
|
+
// 30 分钟:全量重建实测要跑二十分钟以上(16k 条 / 1500+ 批),而这里原来给的是 5 分钟 --
|
|
5295
|
+
// 那个值让 `--full` 在真实索引上**从来没跑完过**(超时被杀;且脚本改成了边嵌边落盘之前,
|
|
5296
|
+
// 中途被杀等于连已经嵌好的那部分也一起丢)。
|
|
5297
|
+
timeout: 1_800_000, maxBuffer: 10 * 1024 * 1024,
|
|
5117
5298
|
env: pythonProcessEnv(opts.apiKey, 'DASHSCOPE_API_KEY'),
|
|
5118
5299
|
})
|
|
5119
5300
|
if (opts.json) console.log(stdout.trim())
|
|
@@ -6169,12 +6350,14 @@ program
|
|
|
6169
6350
|
.option('--source <name>', '仅处理指定公众号,可重复或用逗号分隔', (value, previous: string[] = []) => [...previous, value], [])
|
|
6170
6351
|
.description('一键生成公众号日报(抓取 + AI 摘要 + HTML 阅读器 + 行动建议)')
|
|
6171
6352
|
.option('-d, --date <YYYY-MM-DD>', '日期')
|
|
6353
|
+
.option('--yesterday', '跑昨天(等价于 --date 昨天)。公众号发到 24:00 才停,所以当晚跑的那一天永远不全;"跑昨天"与时刻无关,计划任务用这个')
|
|
6172
6354
|
.option('--api-key <key>', 'DeepSeek API key(或设环境变量 DEEPSEEK_API_KEY)')
|
|
6173
6355
|
.option('--skip-classify', '跳过后处理')
|
|
6174
6356
|
.option('--dry-run', '仅预览文章,不调用 AI 或写入日报')
|
|
6175
6357
|
.option('--yes', '确认执行日报生成;JSON 模式需要此选项')
|
|
6176
6358
|
.option('--no-ai', '关闭本次日报的所有 AI 处理')
|
|
6177
6359
|
.option('--no-summary', '只判断不生成:不调 LLM 写摘要/标签/简报(主题与相关度仍由 Jev 判断),无需 DeepSeek key')
|
|
6360
|
+
.option('--prefetch-bodies [n]', '抓取之前先并发把当天正文抓进缓存(n=并发数,默认 8):抓取阶段于是全部命中缓存、不再逐篇 sleep 8-12 秒(450 篇约 75 分钟 → 秒级)。对上游的请求次数不变,只是压进更短的时间窗')
|
|
6178
6361
|
.option('--json', '输出机器可读的最终结果;运行日志写入 stderr')
|
|
6179
6362
|
.action(async (opts) => {
|
|
6180
6363
|
const { spawn } = await import('child_process')
|
|
@@ -6185,7 +6368,16 @@ program
|
|
|
6185
6368
|
|
|
6186
6369
|
const now = new Date()
|
|
6187
6370
|
const localDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
|
|
6188
|
-
const
|
|
6371
|
+
const isoDate = (when: Date) => `${when.getFullYear()}-${String(when.getMonth() + 1).padStart(2, '0')}-${String(when.getDate()).padStart(2, '0')}`
|
|
6372
|
+
if (opts.date && opts.yesterday) {
|
|
6373
|
+
const error = '--date 与 --yesterday 不能同时给(前者是明确的一天,后者是"昨天")'
|
|
6374
|
+
if (opts.json) console.log(JSON.stringify({ success: false, code: 'INVALID_ARGUMENT', field: 'date', error }))
|
|
6375
|
+
else console.log(chalk.red(error))
|
|
6376
|
+
process.exit(1)
|
|
6377
|
+
}
|
|
6378
|
+
const date = opts.date || (opts.yesterday
|
|
6379
|
+
? isoDate(new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1))
|
|
6380
|
+
: localDate)
|
|
6189
6381
|
const apiKey = opts.apiKey || process.env.DEEPSEEK_API_KEY || ''
|
|
6190
6382
|
const noAi = opts.ai === false || configService.get('dailyAiEnabled') === 'false'
|
|
6191
6383
|
const dailyLog = (...values: any[]) => opts.json ? console.error(...values) : console.log(...values)
|
|
@@ -6211,6 +6403,14 @@ program
|
|
|
6211
6403
|
if (opts.noSummary && !noAi) args.push('--no-summary')
|
|
6212
6404
|
for (const source of opts.source || []) args.push('--source', source)
|
|
6213
6405
|
if (opts.skipClassify) args.push('--skip-classify')
|
|
6406
|
+
// 只在**给了**这个旗标时才解析数字:`--prefetch-bodies`(不带数字)是合法的,
|
|
6407
|
+
// 而默认并发数只在 python 侧写一份(这里再写一份迟早漂移)。
|
|
6408
|
+
if (opts.prefetchBodies !== undefined) {
|
|
6409
|
+
args.push('--prefetch-bodies')
|
|
6410
|
+
if (opts.prefetchBodies !== true) {
|
|
6411
|
+
args.push(String(parseCliInteger(opts.prefetchBodies, 'prefetch-bodies', 1, 64, !!opts.json)))
|
|
6412
|
+
}
|
|
6413
|
+
}
|
|
6214
6414
|
|
|
6215
6415
|
dailyLog(chalk.cyan(`\n📰 正在生成 ${targetDate} 公众号日报${noAi ? '(AI 已关闭)' : ''}...\n`))
|
|
6216
6416
|
const child = spawn(getPythonCommand(), args, {
|
|
@@ -7390,6 +7590,7 @@ program
|
|
|
7390
7590
|
.command('check')
|
|
7391
7591
|
.description('检查运行环境(Python、pip 依赖、数据库配置)')
|
|
7392
7592
|
.option('--json', '输出 JSON 格式,不包含数据库路径或密钥')
|
|
7593
|
+
.option('--report', '输出一段可直接粘贴给 AI / 贴进 issue 的诊断结论(已脱敏:用户名与 wxid 换占位符,不含密钥与聊天内容)')
|
|
7393
7594
|
.action(async (opts) => {
|
|
7394
7595
|
// 微信客户端的**版本**与它自报的**数据目录**:两个分支都要用,所以在这里算一次。
|
|
7395
7596
|
// 判据见 `wechatClient.ts`(版本取活进程的 `--user-lib-dir`;取不到就是"未知",不是"没问题")。
|
|
@@ -7413,7 +7614,9 @@ program
|
|
|
7413
7614
|
const dataDirMatchesConfig = (client.dataDirs.length && configuredRoot)
|
|
7414
7615
|
? client.dataDirs.some(dir => samePath(dir, configuredRoot))
|
|
7415
7616
|
: null
|
|
7416
|
-
|
|
7617
|
+
// `--json`(给机器)与 `--report`(给人/AI)用的是**同一份事实**,所以只探测一次:
|
|
7618
|
+
// 两个分支各写一份探测,迟早会出现"JSON 说 Python 可用、报告说不可用"这种自相矛盾。
|
|
7619
|
+
const collectCheckData = async () => {
|
|
7417
7620
|
const { execFileSync } = await import('child_process')
|
|
7418
7621
|
const requiredDependencies = ['sqlcipher3', 'html2text', 'zstandard', 'cryptography']
|
|
7419
7622
|
const optionalDependencies = ['scrapling']
|
|
@@ -7487,9 +7690,48 @@ program
|
|
|
7487
7690
|
caveats: client.caveats,
|
|
7488
7691
|
},
|
|
7489
7692
|
}
|
|
7490
|
-
|
|
7693
|
+
return data
|
|
7694
|
+
}
|
|
7695
|
+
|
|
7696
|
+
if (opts.json) {
|
|
7697
|
+
console.log(JSON.stringify(await collectCheckData(), null, 2))
|
|
7491
7698
|
return
|
|
7492
7699
|
}
|
|
7700
|
+
|
|
7701
|
+
// `--report`:一段**可整段复制**的诊断块(脱敏后)。这是"用户说不清、我们又猜不准"
|
|
7702
|
+
// 那个循环的出口 —— issue #3/#9/#10 与 2026-10-18 那封邮件都是同一个根因被描述成四种现象。
|
|
7703
|
+
if (opts.report) {
|
|
7704
|
+
const data = await collectCheckData()
|
|
7705
|
+
console.log(renderDiagnosticReport({
|
|
7706
|
+
cliVersion: CLI_VERSION,
|
|
7707
|
+
// 本地时间:用户对照日志/截图时要能直接对上,UTC 会多一步换算
|
|
7708
|
+
generatedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
|
|
7709
|
+
platform: process.platform,
|
|
7710
|
+
arch: process.arch,
|
|
7711
|
+
nodeVersion: process.versions.node,
|
|
7712
|
+
python: {
|
|
7713
|
+
command: getPythonCommand(),
|
|
7714
|
+
available: !!data.runtime.python.available,
|
|
7715
|
+
version: String(data.runtime.python.version || ''),
|
|
7716
|
+
},
|
|
7717
|
+
dependencies: data.dependencies,
|
|
7718
|
+
configuration: data.configuration,
|
|
7719
|
+
keys: {
|
|
7720
|
+
messageKeyConfigured: !!configService.get('ntKey') || !!configService.get('decryptKey'),
|
|
7721
|
+
passphraseConfigured: !!configService.get('favPassphrase'),
|
|
7722
|
+
},
|
|
7723
|
+
wechatClient: {
|
|
7724
|
+
running: data.wechatClient.running,
|
|
7725
|
+
version: data.wechatClient.version,
|
|
7726
|
+
note: data.wechatClient.note,
|
|
7727
|
+
dataDirMatchesConfig: data.wechatClient.dataDirMatchesConfig,
|
|
7728
|
+
dataDirs: data.wechatClient.dataDirs,
|
|
7729
|
+
unusableVersionDirs: data.wechatClient.unusableVersionDirs,
|
|
7730
|
+
},
|
|
7731
|
+
}))
|
|
7732
|
+
return
|
|
7733
|
+
}
|
|
7734
|
+
|
|
7493
7735
|
console.log(chalk.cyan('🔍 WeFlow CLI 环境检查\n'))
|
|
7494
7736
|
|
|
7495
7737
|
// 1. Node.js
|
|
@@ -7564,6 +7806,16 @@ program
|
|
|
7564
7806
|
const stat = (await import('fs')).statSync(path)
|
|
7565
7807
|
const mb = (stat.size / 1024 / 1024).toFixed(0)
|
|
7566
7808
|
console.log(chalk.white(` ${label}: ${path} (${mb}MB)`))
|
|
7809
|
+
// 微信"存储位置"设到别的盘时,用户目录下那个是**junction**:显示的是 C 盘路径,
|
|
7810
|
+
// 数据其实在 D 盘。不点出来的话,用户会以为配置指错了地方(2026-10-19 实测:
|
|
7811
|
+
// `C:\Users\…\xwechat_files` → `D:\微信\xwechat_files`)。
|
|
7812
|
+
try {
|
|
7813
|
+
const { realpathSync } = await import('fs')
|
|
7814
|
+
const real = realpathSync(path)
|
|
7815
|
+
if (real.toLowerCase() !== path.toLowerCase()) {
|
|
7816
|
+
console.log(chalk.gray(` ↳ 软链指向 ${real}`))
|
|
7817
|
+
}
|
|
7818
|
+
} catch { /* 解析不了就不说,别拿它当错误 */ }
|
|
7567
7819
|
} else {
|
|
7568
7820
|
const hint = keyRequired ? ' (可选, init 或 sns/fav 命令配置)' : ''
|
|
7569
7821
|
console.log(chalk.gray(` ${label}: 未配置${hint}`))
|