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/dist/bin/weflow-cli.js
CHANGED
|
@@ -11,6 +11,7 @@ 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';
|
|
@@ -324,7 +325,9 @@ program
|
|
|
324
325
|
// 只读、纯本地、不调模型。**故意没有 mcp 那一栏**(D-062):它给的是关于第三方的推断
|
|
325
326
|
// ——谁在被冷落、你会失去谁 —— 所以不进助手、不进 MCP、不喂模型。
|
|
326
327
|
bonds: { cli: 'bonds --json', mcp: null, leavesTheMachine: false },
|
|
327
|
-
|
|
328
|
+
// `check --report` 是同一份事实的**给人版**(已脱敏、可直接贴给 AI 或 issue),
|
|
329
|
+
// 所以它跟 `--json` 并列在这里:外部程序按这条能力去找"怎么拿诊断",不该只知道 JSON 那条。
|
|
330
|
+
diagnostics: { cli: 'check --json', report: 'check --report', leavesTheMachine: false },
|
|
328
331
|
todos: { cli: 'todos list --json' },
|
|
329
332
|
// 2026-09-29 三个界面对齐时新开的读取能力(D-058)。逐个写清它会不会出境,
|
|
330
333
|
// 因为其中两个会——`fetch_article` 白名单 + 逐跳复检,`search_public` 没有任何守卫。
|
|
@@ -829,6 +832,26 @@ program
|
|
|
829
832
|
configService.set('wxid', selectedWxid.wxid);
|
|
830
833
|
// Step 3: 提取密钥
|
|
831
834
|
console.log(chalk.yellow('\n步骤 3/3: 提取数据库解密密钥...'));
|
|
835
|
+
// **先看 Python 在不在**(Windows):它决定"读库"这一整步能不能做。
|
|
836
|
+
// 以前不看,于是 Python 不可用时还要先等 120 秒 hook 超时,然后把用户引向
|
|
837
|
+
// "退出微信重试"——而真正的原因(商店占位程序 / 缺 sqlcipher3)一个字都没提。
|
|
838
|
+
// 2026-10-19 用一台"PATH 里没有 python"的机器实测,2 分钟换来一条误导性建议。
|
|
839
|
+
let pythonUsable = process.platform !== 'win32';
|
|
840
|
+
if (process.platform === 'win32' && !opts.testMissingKeys) {
|
|
841
|
+
try {
|
|
842
|
+
const { execFileSync } = await import('node:child_process');
|
|
843
|
+
execFileSync(getPythonCommand(), ['--version'], { windowsHide: true, stdio: 'ignore', timeout: 5000 });
|
|
844
|
+
pythonUsable = true;
|
|
845
|
+
}
|
|
846
|
+
catch {
|
|
847
|
+
pythonUsable = false;
|
|
848
|
+
console.log(chalk.red(' ✗ 没有可用的 Python —— 读微信库这一步跑不起来'));
|
|
849
|
+
console.log(chalk.gray(' NT 数据库扫描与密钥派生都靠它(需要 Python 3.10+ 且能 import sqlcipher3)。'));
|
|
850
|
+
console.log(chalk.gray(' Windows 上最常见的原因是只装了应用商店的 python 占位程序:'));
|
|
851
|
+
console.log(chalk.gray(' 在终端里运行 `python --version`,能打出版本号才算装对。'));
|
|
852
|
+
console.log(chalk.gray(' 跑 `weflow-cli check --report` 可以看到完整判定(那段话可直接贴给 AI)。'));
|
|
853
|
+
}
|
|
854
|
+
}
|
|
832
855
|
// 优先尝试从 WeFlow 配置读取
|
|
833
856
|
console.log(chalk.gray(' 尝试从 WeFlow 桌面版配置读取...'));
|
|
834
857
|
let extractedKey = opts.testMissingKeys ? '' : tryReadWeFlowKey();
|
|
@@ -898,6 +921,24 @@ program
|
|
|
898
921
|
}
|
|
899
922
|
}
|
|
900
923
|
}
|
|
924
|
+
else if (!pythonUsable) {
|
|
925
|
+
// Python 不可用时**不去等 hook**:就算 hook 抓到了密钥,Step 4 的扫描/派生照样跑不起来,
|
|
926
|
+
// 而那 120 秒的等待只会把用户引向"退出微信重试"这种无效动作。
|
|
927
|
+
const fallback = configService.get('favPassphrase') || configService.get('decryptKey') || '';
|
|
928
|
+
if (fallback) {
|
|
929
|
+
console.log(chalk.gray(' 跳过 hook 等待:已配置的密钥会原样保留,但要先修好 Python 才读得动'));
|
|
930
|
+
extractedKey = fallback;
|
|
931
|
+
}
|
|
932
|
+
else {
|
|
933
|
+
console.log(chalk.red('\n✗ 这台机器现在无法读取微信数据库(缺 Python),因此没有可用密钥'));
|
|
934
|
+
console.log(chalk.gray('\n按顺序做:'));
|
|
935
|
+
console.log(chalk.gray(' 1) 装一个真正的 Python 3.10+,确认 `python --version` 能打出版本号'));
|
|
936
|
+
console.log(chalk.gray(' 2) `pip install sqlcipher3`'));
|
|
937
|
+
console.log(chalk.gray(' 3) 重新 `weflow-cli init`'));
|
|
938
|
+
console.log(chalk.gray(' 卡住时跑 `weflow-cli check --report`,那段文字可直接贴给 AI 或贴到 issue'));
|
|
939
|
+
process.exit(1);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
901
942
|
else if (process.platform !== 'win32') {
|
|
902
943
|
// macOS:自动提取不可用,引导手动提供密钥
|
|
903
944
|
console.log(chalk.yellow(' ⚠ 当前平台无法自动提取密钥(自动提取支持 Windows/Linux)'));
|
|
@@ -909,16 +950,24 @@ program
|
|
|
909
950
|
process.exit(1);
|
|
910
951
|
}
|
|
911
952
|
else {
|
|
912
|
-
//
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
953
|
+
// hook 只在**登录那一瞬**触发。所以要不要等,取决于微信现在在不在跑:
|
|
954
|
+
// · 已在运行 → 用户早就登录了,hook 永远不会再触发,等满 2 分钟纯属浪费
|
|
955
|
+
// (2026-10-19 用一个空配置实测:每次 init 都白等)
|
|
956
|
+
// · 没在运行 → 这正是 README「三分钟上手」第 2 步要求的姿势(关掉微信再 init,
|
|
957
|
+
// 然后按提示启动并登录)。人要打开客户端、等它起来、扫码/点确认,
|
|
958
|
+
// 30 秒根本不够,**必须留足**,否则按文档操作的人会被那句
|
|
959
|
+
// "hook 没抓到" 误导。所以就按文档给满 120 秒。
|
|
960
|
+
let keyResult = { success: false };
|
|
961
|
+
if (version) {
|
|
962
|
+
console.log(chalk.gray(' 微信已在运行:hook 只在登录那一瞬触发,不再等待,直接尝试扫描/派生'));
|
|
963
|
+
}
|
|
964
|
+
else {
|
|
965
|
+
console.log(chalk.cyan('\n 请现在启动微信并登录(登录那一瞬会自动抓取密钥)'));
|
|
966
|
+
console.log(chalk.cyan(' 最多等 2 分钟;不启动也没关系,之后会继续尝试 NT 扫描 / passphrase 派生\n'));
|
|
967
|
+
keyResult = await keyService.waitForKey(120000, (msg) => {
|
|
968
|
+
console.log(chalk.gray(` ${msg}`));
|
|
969
|
+
});
|
|
917
970
|
}
|
|
918
|
-
console.log(chalk.gray(' 等待微信进程出现(启动后会自动 hook)...'));
|
|
919
|
-
const keyResult = await keyService.waitForKey(120000, (msg) => {
|
|
920
|
-
console.log(chalk.gray(` ${msg}`));
|
|
921
|
-
});
|
|
922
971
|
if (keyResult.success && keyResult.key) {
|
|
923
972
|
extractedKey = keyResult.key;
|
|
924
973
|
keyCaptured = true;
|
|
@@ -936,12 +985,13 @@ program
|
|
|
936
985
|
extractedKey = fallback;
|
|
937
986
|
}
|
|
938
987
|
else {
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
988
|
+
// **不退出**:hook 只在"登录那一瞬"触发,而一个正常用户装完工具时微信早就登录了,
|
|
989
|
+
// 于是这条 120 秒的等待**必然**超时。以前这里 process.exit(1),用户就被卡在
|
|
990
|
+
// 「退出微信后重新运行 init」——而下一步(Step 4 的 NT 扫描 / 从 passphrase 派生)
|
|
991
|
+
// 才是新版本真正能拿到密钥的地方,却永远不会被执行(2026-10-19 用空配置实测)。
|
|
992
|
+
// 现在改成继续往下走:拿得到就成功,拿不到由结尾统一判"未完成"。
|
|
993
|
+
console.log(chalk.yellow('\n Hook 没抓到密钥(它只在登录那一瞬触发,已登录时不会再有)'));
|
|
994
|
+
console.log(chalk.gray(' 继续尝试 NT 扫描 / passphrase 派生 —— 这是新版本主要的取密钥方式...'));
|
|
945
995
|
}
|
|
946
996
|
}
|
|
947
997
|
}
|
|
@@ -949,17 +999,27 @@ program
|
|
|
949
999
|
configService.set('decryptKey', extractedKey);
|
|
950
1000
|
if (keyCaptured)
|
|
951
1001
|
console.log(chalk.green('\n✓ 密钥获取成功!'));
|
|
952
|
-
else
|
|
1002
|
+
else if (pythonUsable)
|
|
953
1003
|
console.log(chalk.green('\n✓ 已沿用配置中的密钥 (本次未重新捕获)'));
|
|
1004
|
+
// 缺 Python 时**不能说"已沿用"**:那句话在这里会被读成"密钥没问题",
|
|
1005
|
+
// 而下面紧接着就会说"密钥未就绪",两句自相矛盾(2026-10-19 实跑看到)。
|
|
1006
|
+
else
|
|
1007
|
+
console.log(chalk.yellow('\n· 配置里的密钥原样保留,但**这次验证不了**(缺 Python)——它可能仍然可用'));
|
|
954
1008
|
}
|
|
955
1009
|
else {
|
|
956
|
-
|
|
1010
|
+
// 空密钥时不能说"配置完成":下一句就是"密钥未就绪",两句打架。
|
|
1011
|
+
console.log(chalk.yellow('\n· 本次没有拿到密钥,继续尝试 NT 扫描 / passphrase 派生'));
|
|
957
1012
|
}
|
|
958
1013
|
// Step 4: 尝试扫描 NT 格式数据库 (xwechat_files)
|
|
959
1014
|
// 即使检测到传统路径,也可能存在 NT 格式数据库
|
|
960
1015
|
// 进入这里已经确认是 4.x 初始化流程。不要依赖步骤 0 的 version:
|
|
961
1016
|
// 微信可能是在 init 等待期间才启动,初始 version 会保持为 null。
|
|
962
1017
|
// Linux 分支已在步骤 3 完成配置,跳过。
|
|
1018
|
+
// 这次 init 到底有没有**取得可用的密钥** —— 只有它决定结尾能不能说"初始化完成"。
|
|
1019
|
+
// 扫描失败 + 沿用旧密钥 = 一次看着成功、实际什么也读不到的初始化:2026-10-18 一位用户
|
|
1020
|
+
// 的 init 打在 "NT scan failed (exit 9009)" 上(Python 是商店占位程序),结尾照样报
|
|
1021
|
+
// "初始化完成!"、"密钥: 已安全保存",然后 sessions 一条都没有。
|
|
1022
|
+
let ntReady = process.platform === 'linux'; // Linux 分支已在步骤 3 配好
|
|
963
1023
|
if (process.platform !== 'linux') {
|
|
964
1024
|
console.log(chalk.yellow('\n步骤 4/4: 扫描 NT 格式数据库...'));
|
|
965
1025
|
console.log(chalk.gray(' 正在从微信内存中匹配数据库密钥...'));
|
|
@@ -974,6 +1034,7 @@ program
|
|
|
974
1034
|
primaryDb = ntResult.matched.sort((a, b) => b.size - a.size)[0];
|
|
975
1035
|
}
|
|
976
1036
|
if (primaryDb) {
|
|
1037
|
+
ntReady = true;
|
|
977
1038
|
configService.set('ntDbPath', primaryDb.path);
|
|
978
1039
|
configService.set('ntKey', primaryDb.key);
|
|
979
1040
|
configService.set('ntSalt', primaryDb.salt);
|
|
@@ -1019,6 +1080,7 @@ program
|
|
|
1019
1080
|
derivedMessage = await applyDerivedNtKeys(passphrase, deriveDbs, (l) => console.log(l));
|
|
1020
1081
|
}
|
|
1021
1082
|
if (derivedMessage) {
|
|
1083
|
+
ntReady = true;
|
|
1022
1084
|
console.log(chalk.green(' ✓ 派生模式初始化成功 (聊天/联系人/朋友圈库已配置)'));
|
|
1023
1085
|
}
|
|
1024
1086
|
else {
|
|
@@ -1037,22 +1099,38 @@ program
|
|
|
1037
1099
|
}
|
|
1038
1100
|
}
|
|
1039
1101
|
console.log(chalk.cyan('\n=============================='));
|
|
1040
|
-
|
|
1102
|
+
// 只有**这次真的拿到可用密钥**才叫"完成"。否则说清楚"没就绪"以及下一步做什么 ——
|
|
1103
|
+
// 报"完成"再让 sessions 空着,是最费用户时间的失败方式(他会以为是自己微信的问题)。
|
|
1104
|
+
console.log(chalk[ntReady ? 'cyan' : 'yellow'](ntReady ? '初始化完成! (4.x 模式)' : '初始化未完成:数据库密钥没有就绪'));
|
|
1041
1105
|
console.log(chalk.cyan('=============================='));
|
|
1042
1106
|
console.log(chalk.white(`数据目录: ${detected.path}`));
|
|
1043
1107
|
console.log(chalk.white(`账号: ${selectedWxid.wxid}${selectedWxid.nickname ? ` (${selectedWxid.nickname})` : ''}`));
|
|
1044
|
-
|
|
1045
|
-
console.log(chalk.white('密钥: 已安全保存'));
|
|
1108
|
+
console.log(chalk.white(`密钥: ${ntReady ? '已安全保存' : '未就绪(配置里的旧密钥未必能解开这个库)'}`));
|
|
1046
1109
|
const ntDbPath = configService.get('ntDbPath');
|
|
1047
1110
|
if (ntDbPath) {
|
|
1048
1111
|
console.log(chalk.white(`NT 数据库: ${ntDbPath}`));
|
|
1049
1112
|
}
|
|
1113
|
+
if (!ntReady) {
|
|
1114
|
+
console.log(chalk.yellow('\n现在 sessions / messages 会读不到数据。按顺序做这三步:'));
|
|
1115
|
+
console.log(chalk.yellow(' 1) 在这个终端里运行 `python --version`,要能打出版本号'));
|
|
1116
|
+
console.log(chalk.yellow(' (Windows 上最常见的问题是只装了商店的 python 占位程序)'));
|
|
1117
|
+
console.log(chalk.yellow(' 2) `pip install sqlcipher3`'));
|
|
1118
|
+
console.log(chalk.yellow(' 3) `weflow-cli config forget-keys`,然后重新 `weflow-cli init`'));
|
|
1119
|
+
// 这三步是"最常见的原因",不是诊断结论。真卡住时让用户拿到一份**已经判定过**的事实,
|
|
1120
|
+
// 而不是再让他把现象口头描述一遍(那正是 issue #3/#9/#10 每次都要重来一遍的原因)。
|
|
1121
|
+
console.log(chalk.yellow('\n想先看清到底卡在哪一步,运行:'));
|
|
1122
|
+
console.log(chalk.cyan(' weflow-cli check --report'));
|
|
1123
|
+
console.log(chalk.gray(' 它会给出结论(Python / sqlcipher3 / 密钥 / 数据目录),那段文字可以直接复制给 AI 或贴到 issue。'));
|
|
1124
|
+
}
|
|
1125
|
+
// 未就绪时说"现在可以使用"是骗人的:那几条命令都会返回空。
|
|
1126
|
+
console.log(chalk.gray(ntReady
|
|
1127
|
+
? '\n现在可以使用以下命令:'
|
|
1128
|
+
: '\n修好上面那几步之后,这些命令才会返回数据:'));
|
|
1129
|
+
console.log(chalk.gray(' weflow-cli sessions 查看会话列表'));
|
|
1130
|
+
console.log(chalk.gray(' weflow-cli messages <talker> 查看消息'));
|
|
1131
|
+
console.log(chalk.gray(' weflow-cli contacts 查看联系人'));
|
|
1132
|
+
console.log(chalk.gray(' weflow-cli export <talker> <format> 导出聊天记录'));
|
|
1050
1133
|
}
|
|
1051
|
-
console.log(chalk.gray('\n现在可以使用以下命令:'));
|
|
1052
|
-
console.log(chalk.gray(' weflow-cli sessions 查看会话列表'));
|
|
1053
|
-
console.log(chalk.gray(' weflow-cli messages <talker> 查看消息'));
|
|
1054
|
-
console.log(chalk.gray(' weflow-cli contacts 查看联系人'));
|
|
1055
|
-
console.log(chalk.gray(' weflow-cli export <talker> <format> 导出聊天记录'));
|
|
1056
1134
|
});
|
|
1057
1135
|
// ==================== config ====================
|
|
1058
1136
|
const configCmd = program
|
|
@@ -1063,6 +1141,7 @@ const configurableKeys = [
|
|
|
1063
1141
|
'ntDbPath', 'ntKey', 'ntSalt', 'contactDbPath', 'contactKey', 'contactSalt',
|
|
1064
1142
|
'vaultRepo', 'aiEngine', 'aiBaseUrl', 'aiModel', 'deepseekApiKey', 'typesafeApiKey',
|
|
1065
1143
|
'dashscopeApiKey', 'favPassphrase',
|
|
1144
|
+
'judgeProvider', 'openrouterApiKey', 'openrouterJudgeModel',
|
|
1066
1145
|
'wereadApiKey',
|
|
1067
1146
|
'assistantPrivacy', 'assistantWhitelist', 'assistantGroupWhitelist',
|
|
1068
1147
|
'assistantGroupRequireMention', 'assistantFastRoute', 'assistantPanelUser', 'quickReplyContacts',
|
|
@@ -1326,6 +1405,12 @@ program
|
|
|
1326
1405
|
return;
|
|
1327
1406
|
}
|
|
1328
1407
|
console.log(chalk.gray('未找到会话'));
|
|
1408
|
+
// "0 条"有两种完全不同的原因:真的没有会话,和**初始化/密钥坏了读不出来**。
|
|
1409
|
+
// 后者以前只显示这一句,用户就去猜"是不是我微信坏了 / 版本太新"(issue #3/#9/#10 的老路:
|
|
1410
|
+
// 同一个根因被描述成三种现象)。给一条能立刻分清的动作,比让他猜有用。
|
|
1411
|
+
console.log(chalk.yellow('\n如果微信里明明有聊天记录,那多半是读取这一步没成功。运行这条看结论:'));
|
|
1412
|
+
console.log(chalk.cyan(' weflow-cli check --report'));
|
|
1413
|
+
console.log(chalk.gray(' 它会指出卡在哪一步(Python / sqlcipher3 / 密钥 / 数据目录),那段文字可直接复制给 AI 或贴到 issue。'));
|
|
1329
1414
|
return;
|
|
1330
1415
|
}
|
|
1331
1416
|
if (opts.json) {
|
|
@@ -1902,8 +1987,12 @@ program
|
|
|
1902
1987
|
}
|
|
1903
1988
|
// 图片要靠 Python 那条路匹配(缓存缩略图、资源 md5、原图那档)。`--media` 就是"先抽一次",
|
|
1904
1989
|
// 抽完 json/txt/excel 各自消费同一份清单 —— 匹配只跑一遍,也不会在 TS 里重写一遍。
|
|
1990
|
+
// 抽取那一步的媒体摘要留在这里:`--media` 的调用者(机器调用)应当能从结果里直接读到
|
|
1991
|
+
// "各状态多少条、报告叫什么",而不必去翻 Python 的 stdout。
|
|
1992
|
+
let extracted;
|
|
1905
1993
|
if (opts.media && (format === 'json' || format === 'txt' || format === 'excel')) {
|
|
1906
1994
|
const media = await exportService.extractMedia(talker, opts.output, !!opts.json, { fullImages: opts.fullImages === true }, limit, opts.date || '');
|
|
1995
|
+
extracted = media;
|
|
1907
1996
|
if (!media.success) {
|
|
1908
1997
|
if (opts.json)
|
|
1909
1998
|
console.log(JSON.stringify({ success: false, code: 'MEDIA_EXPORT_FAILED', error: media.error || '媒体抽取失败' }));
|
|
@@ -1940,7 +2029,16 @@ program
|
|
|
1940
2029
|
}
|
|
1941
2030
|
if (result?.success) {
|
|
1942
2031
|
if (opts.json)
|
|
1943
|
-
console.log(JSON.stringify({
|
|
2032
|
+
console.log(JSON.stringify({
|
|
2033
|
+
success: true,
|
|
2034
|
+
format,
|
|
2035
|
+
contract: format === 'json' ? opts.contract : null,
|
|
2036
|
+
path: result.path,
|
|
2037
|
+
count: result.count ?? null,
|
|
2038
|
+
// 媒体摘要:html/md 那条由导出自己产出(Python 的 media_json_summary),
|
|
2039
|
+
// json/txt/excel 的 `--media` 则由上面那次抽取产出 —— 两条都是同一份词汇。
|
|
2040
|
+
media: result.media ?? extracted?.media ?? null,
|
|
2041
|
+
}, null, 2));
|
|
1944
2042
|
else
|
|
1945
2043
|
console.log(chalk.green(`✓ 导出成功: ${result.path}`));
|
|
1946
2044
|
}
|
|
@@ -5200,6 +5298,14 @@ program
|
|
|
5200
5298
|
.option('--top-k <n>', '返回数量', '10')
|
|
5201
5299
|
.option('--line <line>', 'wiki=文章线(默认) / chat=聊天记录 / all=两条都搜', 'wiki')
|
|
5202
5300
|
.option('--no-rerank', '不调用决策模型重排,只按向量/关键词相似度返回(回退到引入重排之前)')
|
|
5301
|
+
.option('--no-hybrid', '不融合本地 BM25 名次(默认融合;门控后实测只增不减)')
|
|
5302
|
+
.option('--rerank-pool <n>', '重排看多少条候选(默认 100;越大越准也越贵)')
|
|
5303
|
+
.option('--no-dedup', '不折叠同文本的多号转载(默认折叠,结果里带 duplicateCount)')
|
|
5304
|
+
.option('--with-text [n]', '给每条命中挂上本地正文(fullText / sourceFile / url);n 为每条最多多少字')
|
|
5305
|
+
.option('--fetch-missing [n]', '正文太薄时按 url 抓一次(最多 n 条,默认 3;复用日报缓存)')
|
|
5306
|
+
.option('--mmr', '对结果做 MMR 去冗余(压掉近似的同领域邻居,可能牺牲单点召回)')
|
|
5307
|
+
.option('--from <account>', '只查这个公众号的文章(按文件名匹配,不走语义)')
|
|
5308
|
+
.option('--phrase', '把这串当**原样短语**在本地定位(FTS5/trigram,不走向量、不出境、不花钱):回答"哪个号在哪天原样写过它"')
|
|
5203
5309
|
.option('--api-key <key>', 'DashScope embedding API key')
|
|
5204
5310
|
.option('--dry-run', '仅预览,不读取索引或调用向量服务')
|
|
5205
5311
|
.option('--yes', '确认执行可能调用云端向量服务的搜索')
|
|
@@ -5211,25 +5317,70 @@ program
|
|
|
5211
5317
|
const pkgRoot = resolvePackageRoot();
|
|
5212
5318
|
const script = join(pkgRoot, 'scripts', 'semantic_search.py');
|
|
5213
5319
|
const topK = parseCliInteger(opts.topK, 'top-k', 1, 100, opts.json);
|
|
5320
|
+
// 没传就**不转发**(默认值只在 Python 侧定义一处):commander 对未提供的选项给 undefined,
|
|
5321
|
+
// 直接 parseCliInteger 会被自己的校验判成非法参数 —— 实测就是这么红的。
|
|
5322
|
+
const rerankPool = opts.rerankPool
|
|
5323
|
+
? parseCliInteger(opts.rerankPool, 'rerank-pool', 5, 500, opts.json) : null;
|
|
5214
5324
|
requireCliChoice(opts.line, ['wiki', 'chat', 'all'], 'line', opts.json);
|
|
5325
|
+
// `--phrase` 走的是**纯本地**那条路(FTS5):不出境、不花钱,所以它既不需要向量 key,
|
|
5326
|
+
// 也不需要确认门。把它当成"向量搜索的降级"会让人以为它也花钱、也发数据。
|
|
5327
|
+
const phrase = opts.phrase === true;
|
|
5328
|
+
// 下面这些旗标只属于**向量检索**那条路;`--phrase`(FTS5 原样定位)一个都用不上。
|
|
5329
|
+
// 以前它们会被原样转发,然后在 Python 的 phrase 分支里被**静默忽略** —— 用户以为自己在调参,
|
|
5330
|
+
// 实际上什么都没发生。这类"接受但不生效"的开关比报错更贵:它会让人把默认行为当成调过的结果。
|
|
5331
|
+
if (phrase) {
|
|
5332
|
+
const irrelevant = [];
|
|
5333
|
+
if (opts.rerank === false)
|
|
5334
|
+
irrelevant.push('--no-rerank');
|
|
5335
|
+
if (opts.hybrid === false)
|
|
5336
|
+
irrelevant.push('--no-hybrid');
|
|
5337
|
+
if (rerankPool != null)
|
|
5338
|
+
irrelevant.push('--rerank-pool');
|
|
5339
|
+
if (opts.dedup === false)
|
|
5340
|
+
irrelevant.push('--no-dedup');
|
|
5341
|
+
if (opts.withText)
|
|
5342
|
+
irrelevant.push('--with-text');
|
|
5343
|
+
if (opts.fetchMissing)
|
|
5344
|
+
irrelevant.push('--fetch-missing');
|
|
5345
|
+
if (opts.mmr)
|
|
5346
|
+
irrelevant.push('--mmr');
|
|
5347
|
+
if (irrelevant.length) {
|
|
5348
|
+
const error = `--phrase 只做原样定位,不接受 ${irrelevant.join('、')}(它们只对向量检索有意义)`;
|
|
5349
|
+
if (opts.json)
|
|
5350
|
+
console.log(JSON.stringify({ success: false, code: 'INVALID_ARGUMENT', error }));
|
|
5351
|
+
else
|
|
5352
|
+
console.log(chalk.red(error));
|
|
5353
|
+
process.exit(1);
|
|
5354
|
+
}
|
|
5355
|
+
}
|
|
5215
5356
|
const preview = {
|
|
5216
5357
|
success: true,
|
|
5217
5358
|
dryRun: true,
|
|
5218
5359
|
action: 'semantic-search.query',
|
|
5360
|
+
phrase,
|
|
5219
5361
|
topK,
|
|
5220
5362
|
line: opts.line,
|
|
5221
5363
|
noRerank: opts.rerank === false,
|
|
5364
|
+
noHybrid: opts.hybrid === false,
|
|
5365
|
+
rerankPool,
|
|
5366
|
+
noDedup: opts.dedup === false,
|
|
5367
|
+
withText: opts.withText || null,
|
|
5368
|
+
fetchMissing: opts.fetchMissing || null,
|
|
5369
|
+
mmr: opts.mmr === true,
|
|
5370
|
+
from: opts.from || null,
|
|
5222
5371
|
readsLocalIndex: true,
|
|
5223
|
-
mayUseCloudEmbedding:
|
|
5372
|
+
mayUseCloudEmbedding: !phrase,
|
|
5224
5373
|
};
|
|
5225
5374
|
if (opts.dryRun) {
|
|
5226
5375
|
if (opts.json)
|
|
5227
5376
|
console.log(JSON.stringify(preview));
|
|
5228
5377
|
else
|
|
5229
|
-
console.log(chalk.cyan(
|
|
5378
|
+
console.log(chalk.cyan(phrase
|
|
5379
|
+
? '预览:原样短语定位 —— 只读本地短语索引,不联网、不花钱。'
|
|
5380
|
+
: '预览:将读取本地语义索引,并可能把查询发送到已配置的向量服务。'));
|
|
5230
5381
|
return;
|
|
5231
5382
|
}
|
|
5232
|
-
if (!opts.yes) {
|
|
5383
|
+
if (!phrase && !opts.yes) {
|
|
5233
5384
|
if (opts.json) {
|
|
5234
5385
|
console.log(JSON.stringify({ ...preview, success: false, dryRun: false, code: 'CONFIRMATION_REQUIRED' }));
|
|
5235
5386
|
process.exit(1);
|
|
@@ -5249,7 +5400,16 @@ program
|
|
|
5249
5400
|
// 接这条通道之前,`OPERATIONS.md` 一直在教用户敲 `search --no-rerank`,而 CLI 会答 `unknown option`
|
|
5250
5401
|
// —— 同一份文档里 `--line` 是通的,所以它看起来像"能用的旗标"(2026-10-11 由文档命令面那道检查发现)。
|
|
5251
5402
|
const args = [script, 'search', '--top-k', String(topK), '--line', opts.line,
|
|
5252
|
-
...(
|
|
5403
|
+
...(phrase ? ['--phrase'] : []),
|
|
5404
|
+
...(opts.rerank === false ? ['--no-rerank'] : []),
|
|
5405
|
+
...(opts.hybrid === false ? ['--no-hybrid'] : []),
|
|
5406
|
+
...(opts.from ? ['--from', opts.from] : []),
|
|
5407
|
+
...(rerankPool ? ['--rerank-pool', String(rerankPool)] : []),
|
|
5408
|
+
...(opts.dedup === false ? ['--no-dedup'] : []),
|
|
5409
|
+
...(opts.withText ? ['--with-text', opts.withText === true ? '4000' : String(opts.withText)] : []),
|
|
5410
|
+
...(opts.fetchMissing ? ['--fetch-missing',
|
|
5411
|
+
opts.fetchMissing === true ? '3' : String(opts.fetchMissing)] : []),
|
|
5412
|
+
...(opts.mmr ? ['--mmr'] : [])];
|
|
5253
5413
|
try {
|
|
5254
5414
|
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
5255
5415
|
timeout: 60_000, maxBuffer: 10 * 1024 * 1024,
|
|
@@ -5269,6 +5429,7 @@ program
|
|
|
5269
5429
|
.command('search-index')
|
|
5270
5430
|
.description('构建语义搜索索引')
|
|
5271
5431
|
.option('--full', '全量重建')
|
|
5432
|
+
.option('--phrase', '只建**精确短语索引**(本地 FTS5/trigram,不调用向量服务、不花钱、不出境)')
|
|
5272
5433
|
.option('--api-key <key>', 'DashScope embedding API key')
|
|
5273
5434
|
.option('--days <n>', '聊天记录收最近多少天(默认 90)')
|
|
5274
5435
|
.option('--article-days <n>', '日报文章收最近多少个日期目录(默认 30)')
|
|
@@ -5276,6 +5437,36 @@ program
|
|
|
5276
5437
|
.option('--yes', '确认构建或重建索引')
|
|
5277
5438
|
.option('--json', '输出 JSON 格式')
|
|
5278
5439
|
.action(async (opts) => {
|
|
5440
|
+
// `--phrase`:短语索引那条路完全不碰向量服务(也不碰聊天库——它从语义索引的 meta.json
|
|
5441
|
+
// 派生),所以它不走窗口参数、不需要确认门,也不校验 key。混进下面那条路会让用户以为
|
|
5442
|
+
// "建个本地的检索也要花 vector 的钱"。
|
|
5443
|
+
if (opts.phrase) {
|
|
5444
|
+
const phrasePreview = {
|
|
5445
|
+
success: true, dryRun: true, action: 'search-index.build-phrase',
|
|
5446
|
+
readsLocalData: true, usesCloudEmbedding: false, rebuildsPhraseIndex: true,
|
|
5447
|
+
};
|
|
5448
|
+
if (opts.dryRun) {
|
|
5449
|
+
if (opts.json)
|
|
5450
|
+
console.log(JSON.stringify(phrasePreview));
|
|
5451
|
+
else
|
|
5452
|
+
console.log(chalk.cyan('短语索引预览:全量重建本地 FTS5 索引(含两条线),不联网、不花钱。'));
|
|
5453
|
+
return;
|
|
5454
|
+
}
|
|
5455
|
+
const { execFile } = await import('child_process');
|
|
5456
|
+
const { promisify } = await import('util');
|
|
5457
|
+
const execFileAsync = promisify(execFile);
|
|
5458
|
+
const pkgRoot = resolvePackageRoot();
|
|
5459
|
+
const script = join(pkgRoot, 'scripts', 'semantic_search.py');
|
|
5460
|
+
try {
|
|
5461
|
+
const { stdout } = await execFileAsync(getPythonCommand(), [script, 'phrase-build', '--line', 'all'], { timeout: 600_000, maxBuffer: 10 * 1024 * 1024, env: pythonProcessEnv() });
|
|
5462
|
+
console.log(opts.json ? stdout.trim() : stdout);
|
|
5463
|
+
}
|
|
5464
|
+
catch (e) {
|
|
5465
|
+
console.error(chalk.red(`\n✗ ${safeSubprocessError(e, '建短语索引失败')}`));
|
|
5466
|
+
process.exit(1);
|
|
5467
|
+
}
|
|
5468
|
+
return;
|
|
5469
|
+
}
|
|
5279
5470
|
// 窗口的**运行值由这里说了算**:默认值 + 校验 + 透传都在这一处,预览报的就是真跑的那些
|
|
5280
5471
|
// (脚本文档里另有自己的默认值,只供手动直接跑脚本时用;CLI 这条路永远显式传参,
|
|
5281
5472
|
// 所以两边万一漂了也不会影响实际行为)。
|
|
@@ -5333,7 +5524,10 @@ program
|
|
|
5333
5524
|
args.push('--full');
|
|
5334
5525
|
try {
|
|
5335
5526
|
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
5336
|
-
|
|
5527
|
+
// 30 分钟:全量重建实测要跑二十分钟以上(16k 条 / 1500+ 批),而这里原来给的是 5 分钟 --
|
|
5528
|
+
// 那个值让 `--full` 在真实索引上**从来没跑完过**(超时被杀;且脚本改成了边嵌边落盘之前,
|
|
5529
|
+
// 中途被杀等于连已经嵌好的那部分也一起丢)。
|
|
5530
|
+
timeout: 1_800_000, maxBuffer: 10 * 1024 * 1024,
|
|
5337
5531
|
env: pythonProcessEnv(opts.apiKey, 'DASHSCOPE_API_KEY'),
|
|
5338
5532
|
});
|
|
5339
5533
|
if (opts.json)
|
|
@@ -6424,12 +6618,14 @@ program
|
|
|
6424
6618
|
.option('--source <name>', '仅处理指定公众号,可重复或用逗号分隔', (value, previous = []) => [...previous, value], [])
|
|
6425
6619
|
.description('一键生成公众号日报(抓取 + AI 摘要 + HTML 阅读器 + 行动建议)')
|
|
6426
6620
|
.option('-d, --date <YYYY-MM-DD>', '日期')
|
|
6621
|
+
.option('--yesterday', '跑昨天(等价于 --date 昨天)。公众号发到 24:00 才停,所以当晚跑的那一天永远不全;"跑昨天"与时刻无关,计划任务用这个')
|
|
6427
6622
|
.option('--api-key <key>', 'DeepSeek API key(或设环境变量 DEEPSEEK_API_KEY)')
|
|
6428
6623
|
.option('--skip-classify', '跳过后处理')
|
|
6429
6624
|
.option('--dry-run', '仅预览文章,不调用 AI 或写入日报')
|
|
6430
6625
|
.option('--yes', '确认执行日报生成;JSON 模式需要此选项')
|
|
6431
6626
|
.option('--no-ai', '关闭本次日报的所有 AI 处理')
|
|
6432
6627
|
.option('--no-summary', '只判断不生成:不调 LLM 写摘要/标签/简报(主题与相关度仍由 Jev 判断),无需 DeepSeek key')
|
|
6628
|
+
.option('--prefetch-bodies [n]', '抓取之前先并发把当天正文抓进缓存(n=并发数,默认 8):抓取阶段于是全部命中缓存、不再逐篇 sleep 8-12 秒(450 篇约 75 分钟 → 秒级)。对上游的请求次数不变,只是压进更短的时间窗')
|
|
6433
6629
|
.option('--json', '输出机器可读的最终结果;运行日志写入 stderr')
|
|
6434
6630
|
.action(async (opts) => {
|
|
6435
6631
|
const { spawn } = await import('child_process');
|
|
@@ -6439,7 +6635,18 @@ program
|
|
|
6439
6635
|
const bizDaily = join(pkgRoot, 'scripts', 'biz_daily.py');
|
|
6440
6636
|
const now = new Date();
|
|
6441
6637
|
const localDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
|
6442
|
-
const
|
|
6638
|
+
const isoDate = (when) => `${when.getFullYear()}-${String(when.getMonth() + 1).padStart(2, '0')}-${String(when.getDate()).padStart(2, '0')}`;
|
|
6639
|
+
if (opts.date && opts.yesterday) {
|
|
6640
|
+
const error = '--date 与 --yesterday 不能同时给(前者是明确的一天,后者是"昨天")';
|
|
6641
|
+
if (opts.json)
|
|
6642
|
+
console.log(JSON.stringify({ success: false, code: 'INVALID_ARGUMENT', field: 'date', error }));
|
|
6643
|
+
else
|
|
6644
|
+
console.log(chalk.red(error));
|
|
6645
|
+
process.exit(1);
|
|
6646
|
+
}
|
|
6647
|
+
const date = opts.date || (opts.yesterday
|
|
6648
|
+
? isoDate(new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1))
|
|
6649
|
+
: localDate);
|
|
6443
6650
|
const apiKey = opts.apiKey || process.env.DEEPSEEK_API_KEY || '';
|
|
6444
6651
|
const noAi = opts.ai === false || configService.get('dailyAiEnabled') === 'false';
|
|
6445
6652
|
const dailyLog = (...values) => opts.json ? console.error(...values) : console.log(...values);
|
|
@@ -6472,6 +6679,14 @@ program
|
|
|
6472
6679
|
args.push('--source', source);
|
|
6473
6680
|
if (opts.skipClassify)
|
|
6474
6681
|
args.push('--skip-classify');
|
|
6682
|
+
// 只在**给了**这个旗标时才解析数字:`--prefetch-bodies`(不带数字)是合法的,
|
|
6683
|
+
// 而默认并发数只在 python 侧写一份(这里再写一份迟早漂移)。
|
|
6684
|
+
if (opts.prefetchBodies !== undefined) {
|
|
6685
|
+
args.push('--prefetch-bodies');
|
|
6686
|
+
if (opts.prefetchBodies !== true) {
|
|
6687
|
+
args.push(String(parseCliInteger(opts.prefetchBodies, 'prefetch-bodies', 1, 64, !!opts.json)));
|
|
6688
|
+
}
|
|
6689
|
+
}
|
|
6475
6690
|
dailyLog(chalk.cyan(`\n📰 正在生成 ${targetDate} 公众号日报${noAi ? '(AI 已关闭)' : ''}...\n`));
|
|
6476
6691
|
const child = spawn(getPythonCommand(), args, {
|
|
6477
6692
|
windowsHide: true,
|
|
@@ -7664,6 +7879,7 @@ program
|
|
|
7664
7879
|
.command('check')
|
|
7665
7880
|
.description('检查运行环境(Python、pip 依赖、数据库配置)')
|
|
7666
7881
|
.option('--json', '输出 JSON 格式,不包含数据库路径或密钥')
|
|
7882
|
+
.option('--report', '输出一段可直接粘贴给 AI / 贴进 issue 的诊断结论(已脱敏:用户名与 wxid 换占位符,不含密钥与聊天内容)')
|
|
7667
7883
|
.action(async (opts) => {
|
|
7668
7884
|
// 微信客户端的**版本**与它自报的**数据目录**:两个分支都要用,所以在这里算一次。
|
|
7669
7885
|
// 判据见 `wechatClient.ts`(版本取活进程的 `--user-lib-dir`;取不到就是"未知",不是"没问题")。
|
|
@@ -7687,7 +7903,9 @@ program
|
|
|
7687
7903
|
const dataDirMatchesConfig = (client.dataDirs.length && configuredRoot)
|
|
7688
7904
|
? client.dataDirs.some(dir => samePath(dir, configuredRoot))
|
|
7689
7905
|
: null;
|
|
7690
|
-
|
|
7906
|
+
// `--json`(给机器)与 `--report`(给人/AI)用的是**同一份事实**,所以只探测一次:
|
|
7907
|
+
// 两个分支各写一份探测,迟早会出现"JSON 说 Python 可用、报告说不可用"这种自相矛盾。
|
|
7908
|
+
const collectCheckData = async () => {
|
|
7691
7909
|
const { execFileSync } = await import('child_process');
|
|
7692
7910
|
const requiredDependencies = ['sqlcipher3', 'html2text', 'zstandard', 'cryptography'];
|
|
7693
7911
|
const optionalDependencies = ['scrapling'];
|
|
@@ -7765,7 +7983,43 @@ program
|
|
|
7765
7983
|
caveats: client.caveats,
|
|
7766
7984
|
},
|
|
7767
7985
|
};
|
|
7768
|
-
|
|
7986
|
+
return data;
|
|
7987
|
+
};
|
|
7988
|
+
if (opts.json) {
|
|
7989
|
+
console.log(JSON.stringify(await collectCheckData(), null, 2));
|
|
7990
|
+
return;
|
|
7991
|
+
}
|
|
7992
|
+
// `--report`:一段**可整段复制**的诊断块(脱敏后)。这是"用户说不清、我们又猜不准"
|
|
7993
|
+
// 那个循环的出口 —— issue #3/#9/#10 与 2026-10-18 那封邮件都是同一个根因被描述成四种现象。
|
|
7994
|
+
if (opts.report) {
|
|
7995
|
+
const data = await collectCheckData();
|
|
7996
|
+
console.log(renderDiagnosticReport({
|
|
7997
|
+
cliVersion: CLI_VERSION,
|
|
7998
|
+
// 本地时间:用户对照日志/截图时要能直接对上,UTC 会多一步换算
|
|
7999
|
+
generatedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
|
|
8000
|
+
platform: process.platform,
|
|
8001
|
+
arch: process.arch,
|
|
8002
|
+
nodeVersion: process.versions.node,
|
|
8003
|
+
python: {
|
|
8004
|
+
command: getPythonCommand(),
|
|
8005
|
+
available: !!data.runtime.python.available,
|
|
8006
|
+
version: String(data.runtime.python.version || ''),
|
|
8007
|
+
},
|
|
8008
|
+
dependencies: data.dependencies,
|
|
8009
|
+
configuration: data.configuration,
|
|
8010
|
+
keys: {
|
|
8011
|
+
messageKeyConfigured: !!configService.get('ntKey') || !!configService.get('decryptKey'),
|
|
8012
|
+
passphraseConfigured: !!configService.get('favPassphrase'),
|
|
8013
|
+
},
|
|
8014
|
+
wechatClient: {
|
|
8015
|
+
running: data.wechatClient.running,
|
|
8016
|
+
version: data.wechatClient.version,
|
|
8017
|
+
note: data.wechatClient.note,
|
|
8018
|
+
dataDirMatchesConfig: data.wechatClient.dataDirMatchesConfig,
|
|
8019
|
+
dataDirs: data.wechatClient.dataDirs,
|
|
8020
|
+
unusableVersionDirs: data.wechatClient.unusableVersionDirs,
|
|
8021
|
+
},
|
|
8022
|
+
}));
|
|
7769
8023
|
return;
|
|
7770
8024
|
}
|
|
7771
8025
|
console.log(chalk.cyan('🔍 WeFlow CLI 环境检查\n'));
|
|
@@ -7842,6 +8096,17 @@ program
|
|
|
7842
8096
|
const stat = (await import('fs')).statSync(path);
|
|
7843
8097
|
const mb = (stat.size / 1024 / 1024).toFixed(0);
|
|
7844
8098
|
console.log(chalk.white(` ${label}: ${path} (${mb}MB)`));
|
|
8099
|
+
// 微信"存储位置"设到别的盘时,用户目录下那个是**junction**:显示的是 C 盘路径,
|
|
8100
|
+
// 数据其实在 D 盘。不点出来的话,用户会以为配置指错了地方(2026-10-19 实测:
|
|
8101
|
+
// `C:\Users\…\xwechat_files` → `D:\微信\xwechat_files`)。
|
|
8102
|
+
try {
|
|
8103
|
+
const { realpathSync } = await import('fs');
|
|
8104
|
+
const real = realpathSync(path);
|
|
8105
|
+
if (real.toLowerCase() !== path.toLowerCase()) {
|
|
8106
|
+
console.log(chalk.gray(` ↳ 软链指向 ${real}`));
|
|
8107
|
+
}
|
|
8108
|
+
}
|
|
8109
|
+
catch { /* 解析不了就不说,别拿它当错误 */ }
|
|
7845
8110
|
}
|
|
7846
8111
|
else {
|
|
7847
8112
|
const hint = keyRequired ? ' (可选, init 或 sns/fav 命令配置)' : '';
|