weflow-cli 1.15.1 → 1.17.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.
Files changed (100) hide show
  1. package/CHANGELOG.md +59 -1
  2. package/OPERATIONS.md +16 -1
  3. package/README.en.md +21 -2
  4. package/README.md +34 -6
  5. package/bin/weflow-cli.ts +389 -6
  6. package/dist/bin/weflow-cli.js +422 -8
  7. package/dist/bin/weflow-cli.js.map +1 -1
  8. package/dist/src/core/ntCore.d.ts +47 -0
  9. package/dist/src/core/ntCore.d.ts.map +1 -1
  10. package/dist/src/core/ntCore.js +24 -0
  11. package/dist/src/core/ntCore.js.map +1 -1
  12. package/dist/src/services/assistantService.d.ts +8 -1
  13. package/dist/src/services/assistantService.d.ts.map +1 -1
  14. package/dist/src/services/assistantService.js +47 -8
  15. package/dist/src/services/assistantService.js.map +1 -1
  16. package/dist/src/services/assistantTools.d.ts +8 -0
  17. package/dist/src/services/assistantTools.d.ts.map +1 -1
  18. package/dist/src/services/assistantTools.js +67 -11
  19. package/dist/src/services/assistantTools.js.map +1 -1
  20. package/dist/src/services/configService.d.ts.map +1 -1
  21. package/dist/src/services/configService.js +3 -2
  22. package/dist/src/services/configService.js.map +1 -1
  23. package/dist/src/services/cursorProbe.d.ts +44 -0
  24. package/dist/src/services/cursorProbe.d.ts.map +1 -0
  25. package/dist/src/services/cursorProbe.js +183 -0
  26. package/dist/src/services/cursorProbe.js.map +1 -0
  27. package/dist/src/services/qchatImport.d.ts +89 -0
  28. package/dist/src/services/qchatImport.d.ts.map +1 -0
  29. package/dist/src/services/qchatImport.js +273 -0
  30. package/dist/src/services/qchatImport.js.map +1 -0
  31. package/dist/src/services/setupVenv.d.ts +43 -0
  32. package/dist/src/services/setupVenv.d.ts.map +1 -0
  33. package/dist/src/services/setupVenv.js +198 -0
  34. package/dist/src/services/setupVenv.js.map +1 -0
  35. package/dist/src/utils/python.d.ts +10 -0
  36. package/dist/src/utils/python.d.ts.map +1 -1
  37. package/dist/src/utils/python.js +27 -4
  38. package/dist/src/utils/python.js.map +1 -1
  39. package/docs/DATA_CONTRACT.md +25 -0
  40. package/docs/DECISIONS.md +92 -30
  41. package/docs/FUNCTIONAL-IMPROVEMENT-SPEC.md +365 -365
  42. package/docs/HEALTH-CHECK.md +104 -104
  43. package/docs/JEV/345/272/224/347/224/250/346/226/275/345/267/245/346/226/207/346/241/243.md +310 -310
  44. package/docs/NEXT_ACTIONS.md +236 -236
  45. package/docs/NPM-PUBLISH.md +130 -130
  46. package/docs/POSITIONING.md +68 -68
  47. package/docs/PROJECT_STATE.md +6 -3
  48. package/docs/REPLAY.md +169 -0
  49. package/docs/SYNC_CONTRACT.md +197 -180
  50. package/package.json +89 -88
  51. package/requirements-3x.txt +4 -4
  52. package/requirements-voice.txt +15 -15
  53. package/resources/js/graph3d/NOTICE.txt +35 -35
  54. package/resources/js/graph3d/d3-binarytree.min.js +2 -2
  55. package/resources/js/graph3d/d3-dispatch.min.js +2 -2
  56. package/resources/js/graph3d/d3-force-3d.min.js +2 -2
  57. package/resources/js/graph3d/d3-octree.min.js +2 -2
  58. package/resources/js/graph3d/d3-quadtree.min.js +2 -2
  59. package/resources/js/graph3d/d3-timer.min.js +2 -2
  60. package/resources/js/graph3d/three.min.js +6 -6
  61. package/resources/js/graph3d/viewer2d.js +550 -448
  62. package/resources/panel/quick-menu.cjs +197 -197
  63. package/scripts/_utils.py +1338 -1338
  64. package/scripts/article_notes.py +423 -423
  65. package/scripts/bonds.py +11 -0
  66. package/scripts/chat_notes.py +86 -3
  67. package/scripts/check_readme_commands.py +427 -426
  68. package/scripts/contact_schema.py +339 -339
  69. package/scripts/create_reading_notes.py +11 -11
  70. package/scripts/daily_task.ps1 +213 -177
  71. package/scripts/fav_notes.py +321 -321
  72. package/scripts/graph_2d.py +156 -151
  73. package/scripts/graph_3d.py +672 -646
  74. package/scripts/health_check.py +473 -473
  75. package/scripts/imported_chats.py +68 -0
  76. package/scripts/install_tasks.ps1 +105 -91
  77. package/scripts/leads.py +179 -0
  78. package/scripts/network.py +206 -206
  79. package/scripts/nt_decrypt.py +135 -1
  80. package/scripts/on_this_day.py +178 -178
  81. package/scripts/opinion_timeline.py +138 -138
  82. package/scripts/promises.py +185 -185
  83. package/scripts/replay.py +803 -0
  84. package/scripts/run_hidden.vbs +25 -0
  85. package/scripts/semantic_search.py +73 -5
  86. package/scripts/user_notes.py +315 -315
  87. package/scripts/voice_transcribe.py +139 -139
  88. package/scripts/watch_issues.py +212 -212
  89. package/scripts/watch_mail.py +131 -131
  90. package/src/core/ntCore.ts +59 -0
  91. package/src/services/assistantService.ts +48 -8
  92. package/src/services/assistantTools.ts +73 -10
  93. package/src/services/configService.ts +3 -2
  94. package/src/services/cursorProbe.ts +222 -0
  95. package/src/services/qchatImport.ts +327 -0
  96. package/src/services/setupVenv.ts +238 -0
  97. package/src/services/syncService.ts +363 -363
  98. package/src/services/syncState.ts +275 -275
  99. package/src/services/wechatMedia.ts +166 -166
  100. package/src/utils/python.ts +33 -4
@@ -7,6 +7,9 @@ import { existsSync, readFileSync, statSync, writeFileSync } from 'fs';
7
7
  import { homedir } from 'os';
8
8
  import { SYNC_SCHEMA, SYNC_SOURCE, syncStateStore } from '../src/services/syncState.js';
9
9
  import { SyncRangeRequiredError, runSync } from '../src/services/syncService.js';
10
+ import { probeCursorReadiness } from '../src/services/cursorProbe.js';
11
+ import { planSetup, runSetup } from '../src/services/setupVenv.js';
12
+ import { planImportQchat, runImportQchat, listImports, readImportedSession } from '../src/services/qchatImport.js';
10
13
  import { dbPathService } from '../src/core/dbPathService.js';
11
14
  import { keyService } from '../src/core/keyService.js';
12
15
  import { NtCore } from '../src/core/ntCore.js';
@@ -295,7 +298,30 @@ program
295
298
  versionedContract: 'weflow-message/v1',
296
299
  rawContractPreserved: true,
297
300
  coverage: ['requestedFrom', 'requestedTo', 'requestedLimit', 'returned', 'mayHaveMore', 'oldestCreateTime', 'newestCreateTime'],
298
- incrementalRead: { mode: 'overlapping-time-window', stableCursor: false },
301
+ incrementalRead: {
302
+ mode: 'overlapping-time-window',
303
+ stableCursor: false,
304
+ // D-027 的另一半:光写 false 是句结论,这里把 false → true 的**路径**
305
+ // 摊开。probe 指向的那条命令会实测 NT 数据侧判据(采样有界、结果里
306
+ // 自带 exhaustive=false);backends 里的字段是代码事实,改读取器时同步改。
307
+ cursorReadiness: {
308
+ probe: 'sync cursor-probe --json',
309
+ criteria: [
310
+ 'stable-identity: 每条消息都有非零且不重复的 server_id(缺失时身份退化为按分片的 localId+createTime,不能跨分片续读)',
311
+ 'globally-comparable-order: 跨分片合并的顺序键可比(现在是 createTime+localId,localId 不跨分片可比)',
312
+ 'window-pushdown: 时间窗下推进读取(现在只有 NT 后端)',
313
+ 'coverage-report: 后端能报告本次读取是否完整(现在只有 NT 的分片报告)',
314
+ 'every-backend: 上述各项在所有消息后端同时成立(D-027 的总门)',
315
+ ],
316
+ backends: {
317
+ 'nt': { identity: 'server_id(退化为 localId+createTime)', windowPushdown: true, coverageReport: true, liveProbe: true },
318
+ 'raw-sqlite-4x': { identity: 'MsgSvrID 列被读取、覆盖率未实测', windowPushdown: false, coverageReport: false, liveProbe: false },
319
+ 'wcdb-4x': { identity: 'server_id 随原生 API 返回、未实测', windowPushdown: false, coverageReport: false, liveProbe: false },
320
+ 'legacy-3x': { identity: 'MsgSvrID 列被读取、覆盖率未实测', windowPushdown: false, coverageReport: false, liveProbe: false },
321
+ },
322
+ advertisedWhen: 'every backend reports true on every criterion above',
323
+ },
324
+ },
299
325
  },
300
326
  favorites: {
301
327
  cli: 'fav list --json',
@@ -657,6 +683,256 @@ program
657
683
  };
658
684
  console.log(JSON.stringify(data, null, 2));
659
685
  });
686
+ // ==================== setup(自管 Python venv) ====================
687
+ // Issues 里最大的两类环境噪音(PowerShell 策略 / 商店占位 python 的 9009)里,
688
+ // 后者的根因是"没有能 import sqlcipher3 的 Python"。这条命令一次性解决:
689
+ // 在 ~/.weflow-cli/venv 建 venv 并装 requirements,之后 getPythonCommand() 优先用它。
690
+ program
691
+ .command('setup')
692
+ .description('创建自管 Python 虚拟环境 (~/.weflow-cli/venv) 并安装依赖;之后所有 Python 工作流优先用它')
693
+ .option('--dry-run', '只看计划:venv 位置、用哪个 Python 建、装哪些包(不动任何东西)')
694
+ .option('--yes', '跳过交互确认(机器执行路径)')
695
+ .option('--recreate', '删除现有 venv 后重建(默认已存在则复用)')
696
+ .option('--with-3x', '同时安装 requirements-3x.txt(微信 3.x 旧版密钥提取)')
697
+ .option('--with-voice', '同时安装 requirements-voice.txt(本机语音转写)')
698
+ .option('--non-interactive', '禁止交互提问;与 --yes 配合用于脚本')
699
+ .option('--json', '输出 JSON 格式')
700
+ .action(async (opts) => {
701
+ let plan;
702
+ try {
703
+ plan = await planSetup({ recreate: !!opts.recreate, with3x: !!opts.with3x, withVoice: !!opts.withVoice });
704
+ }
705
+ catch (error) {
706
+ if (opts.json) {
707
+ console.log(JSON.stringify({ success: false, code: 'SETUP_PLAN_FAILED', error: String(error?.message || error).slice(0, 200) }));
708
+ process.exit(1);
709
+ }
710
+ console.error(chalk.red(`✗ 计划失败: ${error?.message || error}`));
711
+ process.exit(1);
712
+ }
713
+ if (opts.dryRun || !opts.yes) {
714
+ const preview = {
715
+ success: true,
716
+ dryRun: true,
717
+ action: 'setup.venv',
718
+ venvDir: plan.venvDir,
719
+ venvExists: existsSync(plan.venvDir),
720
+ creator: plan.creator,
721
+ requirementFiles: plan.requirementFiles,
722
+ packages: plan.packages,
723
+ substitutions: plan.substitutions,
724
+ recreate: plan.recreate,
725
+ createsUnder: plan.createsUnder,
726
+ network: plan.network,
727
+ // 密钥/数据库一个都不碰:这条命令只写 ~/.weflow-cli/venv
728
+ readsLocalData: false,
729
+ };
730
+ if (opts.dryRun) {
731
+ if (opts.json) {
732
+ console.log(JSON.stringify(preview));
733
+ return;
734
+ }
735
+ console.log(chalk.cyan('\nsetup 计划(--dry-run,不执行)'));
736
+ console.log(chalk.gray(` venv: ${plan.venvDir}${existsSync(plan.venvDir) ? '(已存在,默认复用)' : ''}`));
737
+ console.log(chalk.gray(` 创建者: ${plan.creator ?? '未找到可运行的 Python(先装 Python 3.10+)'}`));
738
+ console.log(chalk.gray(` 安装: ${plan.packages.join(' ')}`));
739
+ for (const sub of plan.substitutions)
740
+ console.log(chalk.gray(` 替换: ${sub}`));
741
+ console.log(chalk.gray(' 网络请求: pip 访问 PyPI;不读聊天数据、不碰密钥\n'));
742
+ return;
743
+ }
744
+ if (opts.json) {
745
+ console.log(JSON.stringify({ ...preview, success: false, dryRun: false, code: 'CONFIRMATION_REQUIRED' }));
746
+ process.exit(1);
747
+ }
748
+ if (process.env.WEFLOW_NON_INTERACTIVE || opts.nonInteractive) {
749
+ console.error(chalk.red('✗ 非交互模式需要 --yes'));
750
+ process.exit(1);
751
+ }
752
+ const { confirmed } = await inquirer.prompt([{
753
+ type: 'confirm',
754
+ name: 'confirmed',
755
+ message: `将在 ${plan.venvDir} 创建虚拟环境并安装 ${plan.packages.length} 个包(pip 需要访问 PyPI)。继续吗?`,
756
+ default: false,
757
+ }]);
758
+ if (!confirmed) {
759
+ console.log(chalk.gray('已取消'));
760
+ return;
761
+ }
762
+ }
763
+ const result = await runSetup(plan, (line) => console.log(chalk.gray(` ${line}`)));
764
+ if (opts.json) {
765
+ console.log(JSON.stringify(result));
766
+ if (!result.success)
767
+ process.exit(1);
768
+ return;
769
+ }
770
+ console.log(chalk.cyan('\nPython 环境 setup'));
771
+ for (const step of result.steps) {
772
+ console.log(` ${step.ok ? '✓' : '✗'} ${step.name}${step.detail ? chalk.gray(` — ${step.detail}`) : ''}`);
773
+ }
774
+ if (result.success) {
775
+ console.log(chalk.green(`\n完成。之后的 Python 工作流会优先用: ${result.venvPython}`));
776
+ console.log(chalk.gray(' 下一步: weflow-cli check 验证环境,weflow-cli init 初始化本地数据'));
777
+ }
778
+ else {
779
+ console.log(chalk.red(`\n失败(${result.error ?? '见上方步骤'})。把 --json 输出贴到 issue 里最好排查。`));
780
+ process.exit(1);
781
+ }
782
+ });
783
+ // ==================== import(外部 IM 数据导入,D-129) ====================
784
+ // 边界是**文件契约**:读 qchat-cli `qce export --format json` 的产物,归一化落到
785
+ // output/imports/qq/,之后 chat-notes / search-index 就能把 QQ 会话当普通聊天线处理。
786
+ const importCmd = program
787
+ .command('import')
788
+ .description('导入外部 IM 的聊天记录(当前支持 qchat-cli 的 JSON 导出),并入本地知识管道');
789
+ importCmd
790
+ .command('qchat <path>')
791
+ .description('导入 qchat-cli 的 JSON 导出(单文件或目录;按 messageId 去重,重复导入安全)')
792
+ .option('--dry-run', '只看会导入什么:文件、会话、条数(不写任何东西)')
793
+ .option('--yes', '确认执行(写本地存储 output/imports/qq/)')
794
+ .option('--own-id <qq>', '可选:自己的 QQ 号,用来标 isSend(不给则该字段缺省为未知)')
795
+ .option('--json', '输出 JSON 格式')
796
+ .action(async (inputPath, opts) => {
797
+ let plan;
798
+ try {
799
+ plan = planImportQchat(inputPath);
800
+ }
801
+ catch (error) {
802
+ if (opts.json) {
803
+ console.log(JSON.stringify({ success: false, code: 'IMPORT_PLAN_FAILED', error: String(error?.message || error).slice(0, 200) }));
804
+ process.exit(1);
805
+ }
806
+ console.error(chalk.red(`✗ 计划失败: ${error?.message || error}`));
807
+ process.exit(1);
808
+ }
809
+ const preview = {
810
+ success: true,
811
+ dryRun: true,
812
+ action: 'import.qchat',
813
+ input: inputPath,
814
+ files: plan.files.map(f => ({
815
+ file: basename(f.file), ok: f.ok,
816
+ ...(f.reason ? { reason: f.reason } : {}),
817
+ sessions: f.sessions.map(s => ({ id: s.id, name: s.name, type: s.type, messages: s.messages })),
818
+ })),
819
+ sessionsFound: plan.sessionsFound,
820
+ messagesTotal: plan.messagesTotal,
821
+ storeRoot: plan.storeRoot,
822
+ ownIdProvided: Boolean(opts.ownId),
823
+ readsLocalData: true,
824
+ writesLocalStore: true,
825
+ invokesAI: false,
826
+ network: false,
827
+ };
828
+ if (opts.dryRun) {
829
+ if (opts.json) {
830
+ console.log(JSON.stringify(preview));
831
+ return;
832
+ }
833
+ console.log(chalk.cyan('\nimport qchat 计划(--dry-run,不执行)'));
834
+ console.log(chalk.gray(` 存储: ${plan.storeRoot}`));
835
+ for (const f of plan.files) {
836
+ if (f.ok) {
837
+ for (const sess of f.sessions) {
838
+ console.log(chalk.gray(` ✓ ${basename(f.file)} → 会话「${sess.name}」(${sess.type},${sess.messages} 条)`));
839
+ }
840
+ }
841
+ else {
842
+ console.log(chalk.yellow(` ✗ ${basename(f.file)} — ${f.reason}`));
843
+ }
844
+ }
845
+ if (opts.ownId)
846
+ console.log(chalk.gray(` isSend 判定: 按自己的 QQ 号 ${String(opts.ownId).slice(0, 4)}…`));
847
+ console.log('');
848
+ return;
849
+ }
850
+ if (!opts.yes) {
851
+ if (opts.json) {
852
+ console.log(JSON.stringify({ ...preview, success: false, dryRun: false, code: 'CONFIRMATION_REQUIRED' }));
853
+ process.exit(1);
854
+ }
855
+ console.error(chalk.red(`✗ 需要确认:将把 ${plan.messagesTotal} 条消息写入本地存储 ${plan.storeRoot}。加 --yes 执行`));
856
+ process.exit(1);
857
+ }
858
+ const result = runImportQchat(inputPath, opts.ownId ? String(opts.ownId) : undefined);
859
+ if (opts.json) {
860
+ console.log(JSON.stringify(result));
861
+ if (!result.success)
862
+ process.exit(1);
863
+ return;
864
+ }
865
+ if (!result.success) {
866
+ console.error(chalk.red(`✗ 没有可导入的文件(${result.filesSkipped} 个被跳过,原因见 --dry-run)`));
867
+ process.exit(1);
868
+ }
869
+ console.log(chalk.cyan('\n导入完成'));
870
+ for (const sess of result.sessions) {
871
+ console.log(chalk.green(` ✓ 「${sess.name}」共 ${sess.total} 条(新增 ${sess.added}、更新 ${sess.updated}、原样 ${sess.unchanged})`));
872
+ }
873
+ if (result.filesSkipped)
874
+ console.log(chalk.yellow(` ⚠ 跳过 ${result.filesSkipped} 个不认识的文件`));
875
+ console.log(chalk.gray(` 存储: ${result.storeRoot}`));
876
+ console.log(chalk.gray(' 下一步: weflow-cli import list 查看;chat-notes --source qq 产知识卡;search-index --include-imports 进搜索'));
877
+ });
878
+ importCmd
879
+ .command('list')
880
+ .description('列出已导入的会话')
881
+ .option('--json', '输出 JSON 格式')
882
+ .action((opts) => {
883
+ const entries = listImports();
884
+ if (opts.json) {
885
+ console.log(JSON.stringify({ success: true, sessions: entries }));
886
+ return;
887
+ }
888
+ if (!entries.length) {
889
+ console.log(chalk.gray('\n还没有导入任何会话。先跑 weflow-cli import qchat <qchat 导出文件>'));
890
+ return;
891
+ }
892
+ console.log(chalk.cyan(`\n已导入 ${entries.length} 个会话`));
893
+ for (const e of entries) {
894
+ const span = e.newestCreateTime
895
+ ? `${new Date((e.oldestCreateTime ?? 0) * 1000).toLocaleDateString('zh-CN')} ~ ${new Date(e.newestCreateTime * 1000).toLocaleDateString('zh-CN')}`
896
+ : '(无消息)';
897
+ console.log(` 「${e.name}」(${e.type},${e.messages} 条,${span})`);
898
+ }
899
+ console.log('');
900
+ });
901
+ importCmd
902
+ .command('show <sessionId>')
903
+ .description('读回一个已导入会话的消息(验证导入)')
904
+ .option('-n, --limit <n>', '只看最近 N 条(0 = 全部)', '20')
905
+ .option('--json', '输出 JSON 格式')
906
+ .action((sessionId, opts) => {
907
+ const limit = Number(opts.limit);
908
+ if (!Number.isInteger(limit) || limit < 0) {
909
+ if (opts.json) {
910
+ console.log(JSON.stringify({ success: false, code: 'INVALID_ARGUMENT', error: 'limit 必须是 ≥ 0 的整数' }));
911
+ process.exit(1);
912
+ }
913
+ console.error(chalk.red('✗ limit 必须是 ≥ 0 的整数'));
914
+ process.exit(1);
915
+ }
916
+ const data = readImportedSession(sessionId, limit);
917
+ if (!data) {
918
+ if (opts.json) {
919
+ console.log(JSON.stringify({ success: false, code: 'IMPORT_SESSION_NOT_FOUND' }));
920
+ process.exit(1);
921
+ }
922
+ console.error(chalk.red(`✗ 没有会话 ${sessionId} 的导入记录`));
923
+ process.exit(1);
924
+ }
925
+ if (opts.json) {
926
+ console.log(JSON.stringify({ success: true, ...data }));
927
+ return;
928
+ }
929
+ console.log(chalk.cyan(`\n「${data.session.name}」共 ${data.messages.length} 条(存储里全部 ${listImports().find(e => e.id === sessionId)?.messages ?? '?'} 条)`));
930
+ for (const m of data.messages) {
931
+ const who = m.isSend === undefined ? m.senderName : (m.isSend ? '我' : m.senderName);
932
+ console.log(chalk.gray(`[${new Date(m.createTime * 1000).toLocaleString('zh-CN')}] ${who}(${m.messageType}): `) + m.parsedContent.slice(0, 80));
933
+ }
934
+ console.log('');
935
+ });
660
936
  // ==================== init ====================
661
937
  program
662
938
  .command('init')
@@ -1829,6 +2105,61 @@ syncCmd
1829
2105
  if (!success)
1830
2106
  process.exit(1);
1831
2107
  });
2108
+ // 稳定游标的"路径可见"探针(D-027)。只读、本地、无确认门——与 `check` 同一副姿态:
2109
+ // 它是诊断,不是副作用。`stableCursor` 的结论**不会**因为它翻 true;它做的是把
2110
+ // "还差什么"逐条摊开给下游(she-love-me、MCP 客户端)看。
2111
+ syncCmd
2112
+ .command('cursor-probe')
2113
+ .description('探测当前消息后端离稳定增量游标还差什么(只读,NT 后端带实测采样)')
2114
+ .option('--json', '输出 JSON 格式')
2115
+ .option('--tables-per-shard <n>', '每分片采样的会话表数量(schema 检查始终全量)', '5')
2116
+ .option('--rows-per-table <n>', '每张表两端各采样的行数', '500')
2117
+ .action(async (opts) => {
2118
+ const parseBound = (raw, label) => {
2119
+ const n = Number(raw);
2120
+ if (!Number.isInteger(n) || n < 1 || n > 10_000) {
2121
+ if (opts.json) {
2122
+ console.log(JSON.stringify({ success: false, code: 'INVALID_ARGUMENT', error: `${label} 必须是 1-10000 的整数` }));
2123
+ process.exit(1);
2124
+ }
2125
+ console.error(chalk.red(`✗ ${label} 必须是 1-10000 的整数`));
2126
+ process.exit(1);
2127
+ }
2128
+ return n;
2129
+ };
2130
+ let report;
2131
+ try {
2132
+ report = await probeCursorReadiness({
2133
+ tablesPerShard: parseBound(opts.tablesPerShard, '--tables-per-shard'),
2134
+ rowsPerTable: parseBound(opts.rowsPerTable, '--rows-per-table'),
2135
+ });
2136
+ }
2137
+ catch (error) {
2138
+ if (opts.json) {
2139
+ console.log(JSON.stringify({ success: false, code: 'CURSOR_PROBE_FAILED', error: error?.message || '探测失败' }));
2140
+ process.exit(1);
2141
+ }
2142
+ console.error(chalk.red(`✗ 探测失败: ${error?.message || error}`));
2143
+ process.exit(1);
2144
+ }
2145
+ if (opts.json) {
2146
+ console.log(JSON.stringify(report));
2147
+ return;
2148
+ }
2149
+ console.log(chalk.cyan('\n稳定游标探针(结论不变:stableCursor = false,D-027)'));
2150
+ console.log(chalk.gray(` 后端: ${report.backend ?? '(未配置)'} — ${report.backendNote}`));
2151
+ console.log(chalk.gray(` 实测: ${report.probedLive ? `已采样(exhaustive=false)` : '未运行(该后端无实测探针或未初始化)'}`));
2152
+ for (const c of report.criteria) {
2153
+ const mark = c.met === null ? '?' : c.met ? '✓' : '✗';
2154
+ console.log(` ${mark} ${c.id}`);
2155
+ console.log(chalk.gray(` ${c.detail}`));
2156
+ }
2157
+ if (report.sample) {
2158
+ const sm = report.sample;
2159
+ console.log(chalk.gray(` 采样: ${sm.rowsSampled} 行 / ${sm.tablesSampled} 表(共 ${sm.tablesTotal} 表,每表两端各 ${sm.rowsPerTable} 行)`));
2160
+ }
2161
+ console.log('');
2162
+ });
1832
2163
  // ==================== contacts ====================
1833
2164
  // contact-schema:读 contact.db 里**群**的 ext_buffer(本地只读:不联网、不调模型)
1834
2165
  //
@@ -5433,6 +5764,7 @@ program
5433
5764
  .option('--api-key <key>', 'DashScope embedding API key')
5434
5765
  .option('--days <n>', '聊天记录收最近多少天(默认 90)')
5435
5766
  .option('--article-days <n>', '日报文章收最近多少个日期目录(默认 30)')
5767
+ .option('--include-imports', '把 weflow-cli import qchat 导入的 QQ 会话也收进索引(聊天半边)')
5436
5768
  .option('--dry-run', '仅预览,不读取数据库、调用网络或写入索引')
5437
5769
  .option('--yes', '确认构建或重建索引')
5438
5770
  .option('--json', '输出 JSON 格式')
@@ -5482,6 +5814,7 @@ program
5482
5814
  mode: opts.full ? 'full' : 'incremental',
5483
5815
  chatDays,
5484
5816
  articleDays,
5817
+ includeImports: !!opts.includeImports,
5485
5818
  readsLocalData: true,
5486
5819
  usesCloudEmbedding: true,
5487
5820
  replacesExistingIndex: !!opts.full,
@@ -5522,6 +5855,8 @@ program
5522
5855
  '--article-days', String(articleDays)];
5523
5856
  if (opts.full)
5524
5857
  args.push('--full');
5858
+ if (opts.includeImports)
5859
+ args.push('--include-imports');
5525
5860
  try {
5526
5861
  const { stdout } = await execFileAsync(getPythonCommand(), args, {
5527
5862
  // 30 分钟:全量重建实测要跑二十分钟以上(16k 条 / 1500+ 批),而这里原来给的是 5 分钟 --
@@ -6244,6 +6579,7 @@ program
6244
6579
  .description('把最近一段时间的会话整理成知识卡(之后用 wiki compile 聚成概念页)—— **只写本地文件,不发送**')
6245
6580
  .option('--days <n>', '看最近多少天', '30')
6246
6581
  .option('--limit <n>', '最多处理几个会话', '40')
6582
+ .option('--source <source>', '聊天来源:wechat(默认)/ qq(只读 import qchat 导入的会话,不碰微信库)/ all(都要)', 'wechat')
6247
6583
  .option('--dry-run', '仅预览:几个会话、多少字符会发给生成模型(只读本地,零出境)')
6248
6584
  .option('--yes', '确认把对话发送给生成模型')
6249
6585
  .option('--transcribe-voice', '把缺转写的语音**本机**识别掉再产卡(本地 whisper,慢、吃 CPU、不出网;配合 --dry-run 就是只补转写、不调模型)')
@@ -6256,12 +6592,13 @@ program
6256
6592
  const script = join(pkgRoot, 'scripts', 'chat_notes.py');
6257
6593
  const days = parseCliInteger(opts.days, 'days', 1, 3650, opts.json);
6258
6594
  const limit = parseCliInteger(opts.limit, 'limit', 1, 200, opts.json);
6595
+ requireCliChoice(opts.source, ['wechat', 'qq', 'all'], 'source', opts.json);
6259
6596
  // 与 `draft` 同一条纪律:**"确认过了"只用一个变量**,参数在确认之后才拼
6260
6597
  let confirmed = !!opts.yes;
6261
6598
  if (!opts.dryRun && !confirmed) {
6262
6599
  const preview = {
6263
6600
  success: false, dryRun: false, action: 'chat-notes',
6264
- code: 'CONFIRMATION_REQUIRED', days, limit,
6601
+ code: 'CONFIRMATION_REQUIRED', days, limit, source: opts.source,
6265
6602
  readsLocalChat: true, invokesAI: true, writesLocalFiles: true, sendsNothing: true,
6266
6603
  };
6267
6604
  if (opts.json) {
@@ -6281,6 +6618,7 @@ program
6281
6618
  }
6282
6619
  }
6283
6620
  const args = [script, '--days', String(days), '--limit', String(limit),
6621
+ '--source', String(opts.source),
6284
6622
  ...(confirmed ? ['--yes'] : []),
6285
6623
  ...(opts.transcribeVoice ? ['--transcribe-voice'] : []),
6286
6624
  ...(opts.json ? ['--json'] : []),
@@ -7737,14 +8075,13 @@ program
7737
8075
  const limit = parseCliInteger(opts.limit, 'limit', 1, 200, opts.json);
7738
8076
  try {
7739
8077
  const args = [script, '--years', String(opts.years || '1,3,5'), '--limit', String(limit)];
7740
- if (opts.talker)
7741
- args.push('--talker', String(opts.talker));
7742
8078
  if (opts.json)
7743
8079
  args.push('--json');
8080
+ // D-020:会话标识不进子进程 argv(进程列表里看得见),走 WEFLOW_TALKER。
7744
8081
  const { stdout } = await execFileAsync(getPythonCommand(), args, {
7745
8082
  timeout: 300_000,
7746
8083
  maxBuffer: 10 * 1024 * 1024,
7747
- env: pythonProcessEnv(),
8084
+ env: createPythonProcessEnv(opts.talker ? { WEFLOW_TALKER: String(opts.talker) } : {}),
7748
8085
  });
7749
8086
  console.log(stdout);
7750
8087
  }
@@ -7825,14 +8162,13 @@ program
7825
8162
  const limit = parseCliInteger(opts.limit, 'limit', 1, 500, opts.json);
7826
8163
  try {
7827
8164
  const args = [script, '--days', String(days), '--limit', String(limit)];
7828
- if (opts.talker)
7829
- args.push('--talker', String(opts.talker));
7830
8165
  if (opts.json)
7831
8166
  args.push('--json');
8167
+ // D-020:同 today——会话标识走 WEFLOW_TALKER,不进 argv。
7832
8168
  const { stdout } = await execFileAsync(getPythonCommand(), args, {
7833
8169
  timeout: 300_000,
7834
8170
  maxBuffer: 10 * 1024 * 1024,
7835
- env: pythonProcessEnv(),
8171
+ env: createPythonProcessEnv(opts.talker ? { WEFLOW_TALKER: String(opts.talker) } : {}),
7836
8172
  });
7837
8173
  console.log(stdout);
7838
8174
  }
@@ -7849,6 +8185,84 @@ program
7849
8185
  process.exit(1);
7850
8186
  }
7851
8187
  });
8188
+ // ==================== leads(线索雷达)====================
8189
+ program
8190
+ .command('leads')
8191
+ .description('线索雷达:聊天里的报价/采购/合作/需求信号,带原话与跟进状态(规则判定,不调模型)')
8192
+ .option('--days <number>', '看最近多少天', '30')
8193
+ .option('--min-score <number>', '最低分,4 分以上更接近真实生意', '2')
8194
+ .option('--limit <number>', '最多列几条', '20')
8195
+ .option('--json', '输出 JSON 格式')
8196
+ .action(async (opts) => {
8197
+ const { execFile } = await import('child_process');
8198
+ const { promisify } = await import('util');
8199
+ const execFileAsync = promisify(execFile);
8200
+ const script = join(resolvePackageRoot(), 'scripts', 'leads.py');
8201
+ const days = parseCliInteger(opts.days, 'days', 1, 3650, opts.json);
8202
+ const minScore = parseCliInteger(opts.minScore, 'min-score', 1, 50, opts.json);
8203
+ const limit = parseCliInteger(opts.limit, 'limit', 1, 500, opts.json);
8204
+ try {
8205
+ const args = [script, '--days', String(days), '--min-score', String(minScore),
8206
+ '--limit', String(limit)];
8207
+ if (opts.json)
8208
+ args.push('--json');
8209
+ const { stdout } = await execFileAsync(getPythonCommand(), args, {
8210
+ timeout: 300_000,
8211
+ maxBuffer: 10 * 1024 * 1024,
8212
+ env: pythonProcessEnv(),
8213
+ });
8214
+ console.log(stdout);
8215
+ }
8216
+ catch (e) {
8217
+ if (opts.json) {
8218
+ console.log(JSON.stringify({
8219
+ success: false,
8220
+ code: 'LEADS_FAILED',
8221
+ error: '线索雷达不可用,请运行 check --json 检查配置和依赖',
8222
+ }));
8223
+ process.exit(1);
8224
+ }
8225
+ console.error(chalk.red(`\n${safeSubprocessError(e, '统计失败')}`));
8226
+ process.exit(1);
8227
+ }
8228
+ });
8229
+ // ==================== replay(关系时间重放)====================
8230
+ program
8231
+ .command('replay')
8232
+ .description('关系网络时间重放:按月展开,生成可播放的单文件 HTML(本地、离线、不调模型)')
8233
+ .option('--out <file>', '输出路径', 'output/replay.html')
8234
+ .option('--min-weight <number>', '当月互动量门槛,低于此不计入', '10')
8235
+ .option('--json', '只输出聚合结果(验收用)')
8236
+ .action(async (opts) => {
8237
+ const { execFile } = await import('child_process');
8238
+ const { promisify } = await import('util');
8239
+ const execFileAsync = promisify(execFile);
8240
+ const script = join(resolvePackageRoot(), 'scripts', 'replay.py');
8241
+ const minWeight = parseCliInteger(opts.minWeight, 'min-weight', 1, 10000, opts.json);
8242
+ try {
8243
+ const args = [script, '--min-weight', String(minWeight), '--out', String(opts.out)];
8244
+ if (opts.json)
8245
+ args.push('--json');
8246
+ const { stdout } = await execFileAsync(getPythonCommand(), args, {
8247
+ timeout: 600_000,
8248
+ maxBuffer: 64 * 1024 * 1024,
8249
+ env: pythonProcessEnv(),
8250
+ });
8251
+ console.log(stdout);
8252
+ }
8253
+ catch (e) {
8254
+ if (opts.json) {
8255
+ console.log(JSON.stringify({
8256
+ success: false,
8257
+ code: 'REPLAY_FAILED',
8258
+ error: '时间重放不可用,请运行 check --json 检查配置和依赖',
8259
+ }));
8260
+ process.exit(1);
8261
+ }
8262
+ console.error(chalk.red(`\n${safeSubprocessError(e, '生成失败')}`));
8263
+ process.exit(1);
8264
+ }
8265
+ });
7852
8266
  // ==================== timeline(某号观点演变)====================
7853
8267
  program
7854
8268
  .command('timeline')