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.
- package/CHANGELOG.md +59 -1
- package/OPERATIONS.md +16 -1
- package/README.en.md +21 -2
- package/README.md +34 -6
- package/bin/weflow-cli.ts +389 -6
- package/dist/bin/weflow-cli.js +422 -8
- package/dist/bin/weflow-cli.js.map +1 -1
- package/dist/src/core/ntCore.d.ts +47 -0
- package/dist/src/core/ntCore.d.ts.map +1 -1
- package/dist/src/core/ntCore.js +24 -0
- package/dist/src/core/ntCore.js.map +1 -1
- package/dist/src/services/assistantService.d.ts +8 -1
- package/dist/src/services/assistantService.d.ts.map +1 -1
- package/dist/src/services/assistantService.js +47 -8
- package/dist/src/services/assistantService.js.map +1 -1
- package/dist/src/services/assistantTools.d.ts +8 -0
- package/dist/src/services/assistantTools.d.ts.map +1 -1
- package/dist/src/services/assistantTools.js +67 -11
- package/dist/src/services/assistantTools.js.map +1 -1
- package/dist/src/services/configService.d.ts.map +1 -1
- package/dist/src/services/configService.js +3 -2
- package/dist/src/services/configService.js.map +1 -1
- package/dist/src/services/cursorProbe.d.ts +44 -0
- package/dist/src/services/cursorProbe.d.ts.map +1 -0
- package/dist/src/services/cursorProbe.js +183 -0
- package/dist/src/services/cursorProbe.js.map +1 -0
- package/dist/src/services/qchatImport.d.ts +89 -0
- package/dist/src/services/qchatImport.d.ts.map +1 -0
- package/dist/src/services/qchatImport.js +273 -0
- package/dist/src/services/qchatImport.js.map +1 -0
- package/dist/src/services/setupVenv.d.ts +43 -0
- package/dist/src/services/setupVenv.d.ts.map +1 -0
- package/dist/src/services/setupVenv.js +198 -0
- package/dist/src/services/setupVenv.js.map +1 -0
- package/dist/src/utils/python.d.ts +10 -0
- package/dist/src/utils/python.d.ts.map +1 -1
- package/dist/src/utils/python.js +27 -4
- package/dist/src/utils/python.js.map +1 -1
- package/docs/DATA_CONTRACT.md +25 -0
- package/docs/DECISIONS.md +92 -30
- package/docs/FUNCTIONAL-IMPROVEMENT-SPEC.md +365 -365
- package/docs/HEALTH-CHECK.md +104 -104
- package/docs/JEV/345/272/224/347/224/250/346/226/275/345/267/245/346/226/207/346/241/243.md +310 -310
- package/docs/NEXT_ACTIONS.md +236 -236
- package/docs/NPM-PUBLISH.md +130 -130
- package/docs/POSITIONING.md +68 -68
- package/docs/PROJECT_STATE.md +6 -3
- package/docs/REPLAY.md +169 -0
- package/docs/SYNC_CONTRACT.md +197 -180
- package/package.json +89 -88
- package/requirements-3x.txt +4 -4
- package/requirements-voice.txt +15 -15
- package/resources/js/graph3d/NOTICE.txt +35 -35
- package/resources/js/graph3d/d3-binarytree.min.js +2 -2
- package/resources/js/graph3d/d3-dispatch.min.js +2 -2
- package/resources/js/graph3d/d3-force-3d.min.js +2 -2
- package/resources/js/graph3d/d3-octree.min.js +2 -2
- package/resources/js/graph3d/d3-quadtree.min.js +2 -2
- package/resources/js/graph3d/d3-timer.min.js +2 -2
- package/resources/js/graph3d/three.min.js +6 -6
- package/resources/js/graph3d/viewer2d.js +550 -448
- package/resources/panel/quick-menu.cjs +197 -197
- package/scripts/_utils.py +1338 -1338
- package/scripts/article_notes.py +423 -423
- package/scripts/bonds.py +11 -0
- package/scripts/chat_notes.py +86 -3
- package/scripts/check_readme_commands.py +427 -426
- package/scripts/contact_schema.py +339 -339
- package/scripts/create_reading_notes.py +11 -11
- package/scripts/daily_task.ps1 +213 -177
- package/scripts/fav_notes.py +321 -321
- package/scripts/graph_2d.py +156 -151
- package/scripts/graph_3d.py +672 -646
- package/scripts/health_check.py +473 -473
- package/scripts/imported_chats.py +68 -0
- package/scripts/install_tasks.ps1 +105 -91
- package/scripts/leads.py +179 -0
- package/scripts/network.py +206 -206
- package/scripts/nt_decrypt.py +135 -1
- package/scripts/on_this_day.py +178 -178
- package/scripts/opinion_timeline.py +138 -138
- package/scripts/promises.py +185 -185
- package/scripts/replay.py +803 -0
- package/scripts/run_hidden.vbs +25 -0
- package/scripts/semantic_search.py +73 -5
- package/scripts/user_notes.py +315 -315
- package/scripts/voice_transcribe.py +139 -139
- package/scripts/watch_issues.py +212 -212
- package/scripts/watch_mail.py +131 -131
- package/src/core/ntCore.ts +59 -0
- package/src/services/assistantService.ts +48 -8
- package/src/services/assistantTools.ts +73 -10
- package/src/services/configService.ts +3 -2
- package/src/services/cursorProbe.ts +222 -0
- package/src/services/qchatImport.ts +327 -0
- package/src/services/setupVenv.ts +238 -0
- package/src/services/syncService.ts +363 -363
- package/src/services/syncState.ts +275 -275
- package/src/services/wechatMedia.ts +166 -166
- package/src/utils/python.ts +33 -4
package/bin/weflow-cli.ts
CHANGED
|
@@ -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'
|
|
@@ -312,7 +315,30 @@ program
|
|
|
312
315
|
versionedContract: 'weflow-message/v1',
|
|
313
316
|
rawContractPreserved: true,
|
|
314
317
|
coverage: ['requestedFrom', 'requestedTo', 'requestedLimit', 'returned', 'mayHaveMore', 'oldestCreateTime', 'newestCreateTime'],
|
|
315
|
-
incrementalRead: {
|
|
318
|
+
incrementalRead: {
|
|
319
|
+
mode: 'overlapping-time-window',
|
|
320
|
+
stableCursor: false,
|
|
321
|
+
// D-027 的另一半:光写 false 是句结论,这里把 false → true 的**路径**
|
|
322
|
+
// 摊开。probe 指向的那条命令会实测 NT 数据侧判据(采样有界、结果里
|
|
323
|
+
// 自带 exhaustive=false);backends 里的字段是代码事实,改读取器时同步改。
|
|
324
|
+
cursorReadiness: {
|
|
325
|
+
probe: 'sync cursor-probe --json',
|
|
326
|
+
criteria: [
|
|
327
|
+
'stable-identity: 每条消息都有非零且不重复的 server_id(缺失时身份退化为按分片的 localId+createTime,不能跨分片续读)',
|
|
328
|
+
'globally-comparable-order: 跨分片合并的顺序键可比(现在是 createTime+localId,localId 不跨分片可比)',
|
|
329
|
+
'window-pushdown: 时间窗下推进读取(现在只有 NT 后端)',
|
|
330
|
+
'coverage-report: 后端能报告本次读取是否完整(现在只有 NT 的分片报告)',
|
|
331
|
+
'every-backend: 上述各项在所有消息后端同时成立(D-027 的总门)',
|
|
332
|
+
],
|
|
333
|
+
backends: {
|
|
334
|
+
'nt': { identity: 'server_id(退化为 localId+createTime)', windowPushdown: true, coverageReport: true, liveProbe: true },
|
|
335
|
+
'raw-sqlite-4x': { identity: 'MsgSvrID 列被读取、覆盖率未实测', windowPushdown: false, coverageReport: false, liveProbe: false },
|
|
336
|
+
'wcdb-4x': { identity: 'server_id 随原生 API 返回、未实测', windowPushdown: false, coverageReport: false, liveProbe: false },
|
|
337
|
+
'legacy-3x': { identity: 'MsgSvrID 列被读取、覆盖率未实测', windowPushdown: false, coverageReport: false, liveProbe: false },
|
|
338
|
+
},
|
|
339
|
+
advertisedWhen: 'every backend reports true on every criterion above',
|
|
340
|
+
},
|
|
341
|
+
},
|
|
316
342
|
},
|
|
317
343
|
favorites: {
|
|
318
344
|
cli: 'fav list --json',
|
|
@@ -675,6 +701,232 @@ program
|
|
|
675
701
|
console.log(JSON.stringify(data, null, 2))
|
|
676
702
|
})
|
|
677
703
|
|
|
704
|
+
// ==================== setup(自管 Python venv) ====================
|
|
705
|
+
// Issues 里最大的两类环境噪音(PowerShell 策略 / 商店占位 python 的 9009)里,
|
|
706
|
+
// 后者的根因是"没有能 import sqlcipher3 的 Python"。这条命令一次性解决:
|
|
707
|
+
// 在 ~/.weflow-cli/venv 建 venv 并装 requirements,之后 getPythonCommand() 优先用它。
|
|
708
|
+
program
|
|
709
|
+
.command('setup')
|
|
710
|
+
.description('创建自管 Python 虚拟环境 (~/.weflow-cli/venv) 并安装依赖;之后所有 Python 工作流优先用它')
|
|
711
|
+
.option('--dry-run', '只看计划:venv 位置、用哪个 Python 建、装哪些包(不动任何东西)')
|
|
712
|
+
.option('--yes', '跳过交互确认(机器执行路径)')
|
|
713
|
+
.option('--recreate', '删除现有 venv 后重建(默认已存在则复用)')
|
|
714
|
+
.option('--with-3x', '同时安装 requirements-3x.txt(微信 3.x 旧版密钥提取)')
|
|
715
|
+
.option('--with-voice', '同时安装 requirements-voice.txt(本机语音转写)')
|
|
716
|
+
.option('--non-interactive', '禁止交互提问;与 --yes 配合用于脚本')
|
|
717
|
+
.option('--json', '输出 JSON 格式')
|
|
718
|
+
.action(async (opts) => {
|
|
719
|
+
let plan
|
|
720
|
+
try {
|
|
721
|
+
plan = await planSetup({ recreate: !!opts.recreate, with3x: !!opts.with3x, withVoice: !!opts.withVoice })
|
|
722
|
+
} catch (error: any) {
|
|
723
|
+
if (opts.json) { console.log(JSON.stringify({ success: false, code: 'SETUP_PLAN_FAILED', error: String(error?.message || error).slice(0, 200) })); process.exit(1) }
|
|
724
|
+
console.error(chalk.red(`✗ 计划失败: ${error?.message || error}`))
|
|
725
|
+
process.exit(1)
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (opts.dryRun || !opts.yes) {
|
|
729
|
+
const preview = {
|
|
730
|
+
success: true,
|
|
731
|
+
dryRun: true,
|
|
732
|
+
action: 'setup.venv',
|
|
733
|
+
venvDir: plan.venvDir,
|
|
734
|
+
venvExists: existsSync(plan.venvDir),
|
|
735
|
+
creator: plan.creator,
|
|
736
|
+
requirementFiles: plan.requirementFiles,
|
|
737
|
+
packages: plan.packages,
|
|
738
|
+
substitutions: plan.substitutions,
|
|
739
|
+
recreate: plan.recreate,
|
|
740
|
+
createsUnder: plan.createsUnder,
|
|
741
|
+
network: plan.network,
|
|
742
|
+
// 密钥/数据库一个都不碰:这条命令只写 ~/.weflow-cli/venv
|
|
743
|
+
readsLocalData: false,
|
|
744
|
+
}
|
|
745
|
+
if (opts.dryRun) {
|
|
746
|
+
if (opts.json) { console.log(JSON.stringify(preview)); return }
|
|
747
|
+
console.log(chalk.cyan('\nsetup 计划(--dry-run,不执行)'))
|
|
748
|
+
console.log(chalk.gray(` venv: ${plan.venvDir}${existsSync(plan.venvDir) ? '(已存在,默认复用)' : ''}`))
|
|
749
|
+
console.log(chalk.gray(` 创建者: ${plan.creator ?? '未找到可运行的 Python(先装 Python 3.10+)'}`))
|
|
750
|
+
console.log(chalk.gray(` 安装: ${plan.packages.join(' ')}`))
|
|
751
|
+
for (const sub of plan.substitutions) console.log(chalk.gray(` 替换: ${sub}`))
|
|
752
|
+
console.log(chalk.gray(' 网络请求: pip 访问 PyPI;不读聊天数据、不碰密钥\n'))
|
|
753
|
+
return
|
|
754
|
+
}
|
|
755
|
+
if (opts.json) {
|
|
756
|
+
console.log(JSON.stringify({ ...preview, success: false, dryRun: false, code: 'CONFIRMATION_REQUIRED' }))
|
|
757
|
+
process.exit(1)
|
|
758
|
+
}
|
|
759
|
+
if (process.env.WEFLOW_NON_INTERACTIVE || opts.nonInteractive) {
|
|
760
|
+
console.error(chalk.red('✗ 非交互模式需要 --yes'))
|
|
761
|
+
process.exit(1)
|
|
762
|
+
}
|
|
763
|
+
const { confirmed } = await inquirer.prompt([{
|
|
764
|
+
type: 'confirm',
|
|
765
|
+
name: 'confirmed',
|
|
766
|
+
message: `将在 ${plan.venvDir} 创建虚拟环境并安装 ${plan.packages.length} 个包(pip 需要访问 PyPI)。继续吗?`,
|
|
767
|
+
default: false,
|
|
768
|
+
}])
|
|
769
|
+
if (!confirmed) {
|
|
770
|
+
console.log(chalk.gray('已取消'))
|
|
771
|
+
return
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const result = await runSetup(plan, (line) => console.log(chalk.gray(` ${line}`)))
|
|
776
|
+
if (opts.json) {
|
|
777
|
+
console.log(JSON.stringify(result))
|
|
778
|
+
if (!result.success) process.exit(1)
|
|
779
|
+
return
|
|
780
|
+
}
|
|
781
|
+
console.log(chalk.cyan('\nPython 环境 setup'))
|
|
782
|
+
for (const step of result.steps) {
|
|
783
|
+
console.log(` ${step.ok ? '✓' : '✗'} ${step.name}${step.detail ? chalk.gray(` — ${step.detail}`) : ''}`)
|
|
784
|
+
}
|
|
785
|
+
if (result.success) {
|
|
786
|
+
console.log(chalk.green(`\n完成。之后的 Python 工作流会优先用: ${result.venvPython}`))
|
|
787
|
+
console.log(chalk.gray(' 下一步: weflow-cli check 验证环境,weflow-cli init 初始化本地数据'))
|
|
788
|
+
} else {
|
|
789
|
+
console.log(chalk.red(`\n失败(${result.error ?? '见上方步骤'})。把 --json 输出贴到 issue 里最好排查。`))
|
|
790
|
+
process.exit(1)
|
|
791
|
+
}
|
|
792
|
+
})
|
|
793
|
+
|
|
794
|
+
// ==================== import(外部 IM 数据导入,D-129) ====================
|
|
795
|
+
// 边界是**文件契约**:读 qchat-cli `qce export --format json` 的产物,归一化落到
|
|
796
|
+
// output/imports/qq/,之后 chat-notes / search-index 就能把 QQ 会话当普通聊天线处理。
|
|
797
|
+
const importCmd = program
|
|
798
|
+
.command('import')
|
|
799
|
+
.description('导入外部 IM 的聊天记录(当前支持 qchat-cli 的 JSON 导出),并入本地知识管道')
|
|
800
|
+
|
|
801
|
+
importCmd
|
|
802
|
+
.command('qchat <path>')
|
|
803
|
+
.description('导入 qchat-cli 的 JSON 导出(单文件或目录;按 messageId 去重,重复导入安全)')
|
|
804
|
+
.option('--dry-run', '只看会导入什么:文件、会话、条数(不写任何东西)')
|
|
805
|
+
.option('--yes', '确认执行(写本地存储 output/imports/qq/)')
|
|
806
|
+
.option('--own-id <qq>', '可选:自己的 QQ 号,用来标 isSend(不给则该字段缺省为未知)')
|
|
807
|
+
.option('--json', '输出 JSON 格式')
|
|
808
|
+
.action(async (inputPath: string, opts) => {
|
|
809
|
+
let plan
|
|
810
|
+
try {
|
|
811
|
+
plan = planImportQchat(inputPath)
|
|
812
|
+
} catch (error: any) {
|
|
813
|
+
if (opts.json) { console.log(JSON.stringify({ success: false, code: 'IMPORT_PLAN_FAILED', error: String(error?.message || error).slice(0, 200) })); process.exit(1) }
|
|
814
|
+
console.error(chalk.red(`✗ 计划失败: ${error?.message || error}`))
|
|
815
|
+
process.exit(1)
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const preview = {
|
|
819
|
+
success: true,
|
|
820
|
+
dryRun: true,
|
|
821
|
+
action: 'import.qchat',
|
|
822
|
+
input: inputPath,
|
|
823
|
+
files: plan.files.map(f => ({
|
|
824
|
+
file: basename(f.file), ok: f.ok,
|
|
825
|
+
...(f.reason ? { reason: f.reason } : {}),
|
|
826
|
+
sessions: f.sessions.map(s => ({ id: s.id, name: s.name, type: s.type, messages: s.messages })),
|
|
827
|
+
})),
|
|
828
|
+
sessionsFound: plan.sessionsFound,
|
|
829
|
+
messagesTotal: plan.messagesTotal,
|
|
830
|
+
storeRoot: plan.storeRoot,
|
|
831
|
+
ownIdProvided: Boolean(opts.ownId),
|
|
832
|
+
readsLocalData: true,
|
|
833
|
+
writesLocalStore: true,
|
|
834
|
+
invokesAI: false,
|
|
835
|
+
network: false,
|
|
836
|
+
}
|
|
837
|
+
if (opts.dryRun) {
|
|
838
|
+
if (opts.json) { console.log(JSON.stringify(preview)); return }
|
|
839
|
+
console.log(chalk.cyan('\nimport qchat 计划(--dry-run,不执行)'))
|
|
840
|
+
console.log(chalk.gray(` 存储: ${plan.storeRoot}`))
|
|
841
|
+
for (const f of plan.files) {
|
|
842
|
+
if (f.ok) {
|
|
843
|
+
for (const sess of f.sessions) {
|
|
844
|
+
console.log(chalk.gray(` ✓ ${basename(f.file)} → 会话「${sess.name}」(${sess.type},${sess.messages} 条)`))
|
|
845
|
+
}
|
|
846
|
+
} else {
|
|
847
|
+
console.log(chalk.yellow(` ✗ ${basename(f.file)} — ${f.reason}`))
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
if (opts.ownId) console.log(chalk.gray(` isSend 判定: 按自己的 QQ 号 ${String(opts.ownId).slice(0, 4)}…`))
|
|
851
|
+
console.log('')
|
|
852
|
+
return
|
|
853
|
+
}
|
|
854
|
+
if (!opts.yes) {
|
|
855
|
+
if (opts.json) {
|
|
856
|
+
console.log(JSON.stringify({ ...preview, success: false, dryRun: false, code: 'CONFIRMATION_REQUIRED' }))
|
|
857
|
+
process.exit(1)
|
|
858
|
+
}
|
|
859
|
+
console.error(chalk.red(`✗ 需要确认:将把 ${plan.messagesTotal} 条消息写入本地存储 ${plan.storeRoot}。加 --yes 执行`))
|
|
860
|
+
process.exit(1)
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const result = runImportQchat(inputPath, opts.ownId ? String(opts.ownId) : undefined)
|
|
864
|
+
if (opts.json) {
|
|
865
|
+
console.log(JSON.stringify(result))
|
|
866
|
+
if (!result.success) process.exit(1)
|
|
867
|
+
return
|
|
868
|
+
}
|
|
869
|
+
if (!result.success) {
|
|
870
|
+
console.error(chalk.red(`✗ 没有可导入的文件(${result.filesSkipped} 个被跳过,原因见 --dry-run)`))
|
|
871
|
+
process.exit(1)
|
|
872
|
+
}
|
|
873
|
+
console.log(chalk.cyan('\n导入完成'))
|
|
874
|
+
for (const sess of result.sessions) {
|
|
875
|
+
console.log(chalk.green(` ✓ 「${sess.name}」共 ${sess.total} 条(新增 ${sess.added}、更新 ${sess.updated}、原样 ${sess.unchanged})`))
|
|
876
|
+
}
|
|
877
|
+
if (result.filesSkipped) console.log(chalk.yellow(` ⚠ 跳过 ${result.filesSkipped} 个不认识的文件`))
|
|
878
|
+
console.log(chalk.gray(` 存储: ${result.storeRoot}`))
|
|
879
|
+
console.log(chalk.gray(' 下一步: weflow-cli import list 查看;chat-notes --source qq 产知识卡;search-index --include-imports 进搜索'))
|
|
880
|
+
})
|
|
881
|
+
|
|
882
|
+
importCmd
|
|
883
|
+
.command('list')
|
|
884
|
+
.description('列出已导入的会话')
|
|
885
|
+
.option('--json', '输出 JSON 格式')
|
|
886
|
+
.action((opts) => {
|
|
887
|
+
const entries = listImports()
|
|
888
|
+
if (opts.json) { console.log(JSON.stringify({ success: true, sessions: entries })); return }
|
|
889
|
+
if (!entries.length) {
|
|
890
|
+
console.log(chalk.gray('\n还没有导入任何会话。先跑 weflow-cli import qchat <qchat 导出文件>'))
|
|
891
|
+
return
|
|
892
|
+
}
|
|
893
|
+
console.log(chalk.cyan(`\n已导入 ${entries.length} 个会话`))
|
|
894
|
+
for (const e of entries) {
|
|
895
|
+
const span = e.newestCreateTime
|
|
896
|
+
? `${new Date((e.oldestCreateTime ?? 0) * 1000).toLocaleDateString('zh-CN')} ~ ${new Date(e.newestCreateTime * 1000).toLocaleDateString('zh-CN')}`
|
|
897
|
+
: '(无消息)'
|
|
898
|
+
console.log(` 「${e.name}」(${e.type},${e.messages} 条,${span})`)
|
|
899
|
+
}
|
|
900
|
+
console.log('')
|
|
901
|
+
})
|
|
902
|
+
|
|
903
|
+
importCmd
|
|
904
|
+
.command('show <sessionId>')
|
|
905
|
+
.description('读回一个已导入会话的消息(验证导入)')
|
|
906
|
+
.option('-n, --limit <n>', '只看最近 N 条(0 = 全部)', '20')
|
|
907
|
+
.option('--json', '输出 JSON 格式')
|
|
908
|
+
.action((sessionId: string, opts) => {
|
|
909
|
+
const limit = Number(opts.limit)
|
|
910
|
+
if (!Number.isInteger(limit) || limit < 0) {
|
|
911
|
+
if (opts.json) { console.log(JSON.stringify({ success: false, code: 'INVALID_ARGUMENT', error: 'limit 必须是 ≥ 0 的整数' })); process.exit(1) }
|
|
912
|
+
console.error(chalk.red('✗ limit 必须是 ≥ 0 的整数'))
|
|
913
|
+
process.exit(1)
|
|
914
|
+
}
|
|
915
|
+
const data = readImportedSession(sessionId, limit)
|
|
916
|
+
if (!data) {
|
|
917
|
+
if (opts.json) { console.log(JSON.stringify({ success: false, code: 'IMPORT_SESSION_NOT_FOUND' })); process.exit(1) }
|
|
918
|
+
console.error(chalk.red(`✗ 没有会话 ${sessionId} 的导入记录`))
|
|
919
|
+
process.exit(1)
|
|
920
|
+
}
|
|
921
|
+
if (opts.json) { console.log(JSON.stringify({ success: true, ...data })); return }
|
|
922
|
+
console.log(chalk.cyan(`\n「${data.session.name}」共 ${data.messages.length} 条(存储里全部 ${listImports().find(e => e.id === sessionId)?.messages ?? '?'} 条)`))
|
|
923
|
+
for (const m of data.messages) {
|
|
924
|
+
const who = m.isSend === undefined ? m.senderName : (m.isSend ? '我' : m.senderName)
|
|
925
|
+
console.log(chalk.gray(`[${new Date(m.createTime * 1000).toLocaleString('zh-CN')}] ${who}(${m.messageType}): `) + m.parsedContent.slice(0, 80))
|
|
926
|
+
}
|
|
927
|
+
console.log('')
|
|
928
|
+
})
|
|
929
|
+
|
|
678
930
|
// ==================== init ====================
|
|
679
931
|
program
|
|
680
932
|
.command('init')
|
|
@@ -1806,6 +2058,53 @@ syncCmd
|
|
|
1806
2058
|
if (!success) process.exit(1)
|
|
1807
2059
|
})
|
|
1808
2060
|
|
|
2061
|
+
// 稳定游标的"路径可见"探针(D-027)。只读、本地、无确认门——与 `check` 同一副姿态:
|
|
2062
|
+
// 它是诊断,不是副作用。`stableCursor` 的结论**不会**因为它翻 true;它做的是把
|
|
2063
|
+
// "还差什么"逐条摊开给下游(she-love-me、MCP 客户端)看。
|
|
2064
|
+
syncCmd
|
|
2065
|
+
.command('cursor-probe')
|
|
2066
|
+
.description('探测当前消息后端离稳定增量游标还差什么(只读,NT 后端带实测采样)')
|
|
2067
|
+
.option('--json', '输出 JSON 格式')
|
|
2068
|
+
.option('--tables-per-shard <n>', '每分片采样的会话表数量(schema 检查始终全量)', '5')
|
|
2069
|
+
.option('--rows-per-table <n>', '每张表两端各采样的行数', '500')
|
|
2070
|
+
.action(async (opts) => {
|
|
2071
|
+
const parseBound = (raw: string, label: string): number | undefined => {
|
|
2072
|
+
const n = Number(raw)
|
|
2073
|
+
if (!Number.isInteger(n) || n < 1 || n > 10_000) {
|
|
2074
|
+
if (opts.json) { console.log(JSON.stringify({ success: false, code: 'INVALID_ARGUMENT', error: `${label} 必须是 1-10000 的整数` })); process.exit(1) }
|
|
2075
|
+
console.error(chalk.red(`✗ ${label} 必须是 1-10000 的整数`))
|
|
2076
|
+
process.exit(1)
|
|
2077
|
+
}
|
|
2078
|
+
return n
|
|
2079
|
+
}
|
|
2080
|
+
let report
|
|
2081
|
+
try {
|
|
2082
|
+
report = await probeCursorReadiness({
|
|
2083
|
+
tablesPerShard: parseBound(opts.tablesPerShard, '--tables-per-shard'),
|
|
2084
|
+
rowsPerTable: parseBound(opts.rowsPerTable, '--rows-per-table'),
|
|
2085
|
+
})
|
|
2086
|
+
} catch (error: any) {
|
|
2087
|
+
if (opts.json) { console.log(JSON.stringify({ success: false, code: 'CURSOR_PROBE_FAILED', error: error?.message || '探测失败' })); process.exit(1) }
|
|
2088
|
+
console.error(chalk.red(`✗ 探测失败: ${error?.message || error}`))
|
|
2089
|
+
process.exit(1)
|
|
2090
|
+
}
|
|
2091
|
+
if (opts.json) { console.log(JSON.stringify(report)); return }
|
|
2092
|
+
|
|
2093
|
+
console.log(chalk.cyan('\n稳定游标探针(结论不变:stableCursor = false,D-027)'))
|
|
2094
|
+
console.log(chalk.gray(` 后端: ${report.backend ?? '(未配置)'} — ${report.backendNote}`))
|
|
2095
|
+
console.log(chalk.gray(` 实测: ${report.probedLive ? `已采样(exhaustive=false)` : '未运行(该后端无实测探针或未初始化)'}`))
|
|
2096
|
+
for (const c of report.criteria) {
|
|
2097
|
+
const mark = c.met === null ? '?' : c.met ? '✓' : '✗'
|
|
2098
|
+
console.log(` ${mark} ${c.id}`)
|
|
2099
|
+
console.log(chalk.gray(` ${c.detail}`))
|
|
2100
|
+
}
|
|
2101
|
+
if (report.sample) {
|
|
2102
|
+
const sm = report.sample
|
|
2103
|
+
console.log(chalk.gray(` 采样: ${sm.rowsSampled} 行 / ${sm.tablesSampled} 表(共 ${sm.tablesTotal} 表,每表两端各 ${sm.rowsPerTable} 行)`))
|
|
2104
|
+
}
|
|
2105
|
+
console.log('')
|
|
2106
|
+
})
|
|
2107
|
+
|
|
1809
2108
|
// ==================== contacts ====================
|
|
1810
2109
|
// contact-schema:读 contact.db 里**群**的 ext_buffer(本地只读:不联网、不调模型)
|
|
1811
2110
|
//
|
|
@@ -5204,6 +5503,7 @@ program
|
|
|
5204
5503
|
.option('--api-key <key>', 'DashScope embedding API key')
|
|
5205
5504
|
.option('--days <n>', '聊天记录收最近多少天(默认 90)')
|
|
5206
5505
|
.option('--article-days <n>', '日报文章收最近多少个日期目录(默认 30)')
|
|
5506
|
+
.option('--include-imports', '把 weflow-cli import qchat 导入的 QQ 会话也收进索引(聊天半边)')
|
|
5207
5507
|
.option('--dry-run', '仅预览,不读取数据库、调用网络或写入索引')
|
|
5208
5508
|
.option('--yes', '确认构建或重建索引')
|
|
5209
5509
|
.option('--json', '输出 JSON 格式')
|
|
@@ -5252,6 +5552,7 @@ program
|
|
|
5252
5552
|
mode: opts.full ? 'full' : 'incremental',
|
|
5253
5553
|
chatDays,
|
|
5254
5554
|
articleDays,
|
|
5555
|
+
includeImports: !!opts.includeImports,
|
|
5255
5556
|
readsLocalData: true,
|
|
5256
5557
|
usesCloudEmbedding: true,
|
|
5257
5558
|
replacesExistingIndex: !!opts.full,
|
|
@@ -5289,6 +5590,7 @@ program
|
|
|
5289
5590
|
const args: string[] = [script, 'build', '--days', String(chatDays),
|
|
5290
5591
|
'--article-days', String(articleDays)]
|
|
5291
5592
|
if (opts.full) args.push('--full')
|
|
5593
|
+
if (opts.includeImports) args.push('--include-imports')
|
|
5292
5594
|
try {
|
|
5293
5595
|
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
5294
5596
|
// 30 分钟:全量重建实测要跑二十分钟以上(16k 条 / 1500+ 批),而这里原来给的是 5 分钟 --
|
|
@@ -5973,6 +6275,7 @@ program
|
|
|
5973
6275
|
.description('把最近一段时间的会话整理成知识卡(之后用 wiki compile 聚成概念页)—— **只写本地文件,不发送**')
|
|
5974
6276
|
.option('--days <n>', '看最近多少天', '30')
|
|
5975
6277
|
.option('--limit <n>', '最多处理几个会话', '40')
|
|
6278
|
+
.option('--source <source>', '聊天来源:wechat(默认)/ qq(只读 import qchat 导入的会话,不碰微信库)/ all(都要)', 'wechat')
|
|
5976
6279
|
.option('--dry-run', '仅预览:几个会话、多少字符会发给生成模型(只读本地,零出境)')
|
|
5977
6280
|
.option('--yes', '确认把对话发送给生成模型')
|
|
5978
6281
|
.option('--transcribe-voice',
|
|
@@ -5986,12 +6289,13 @@ program
|
|
|
5986
6289
|
const script = join(pkgRoot, 'scripts', 'chat_notes.py')
|
|
5987
6290
|
const days = parseCliInteger(opts.days, 'days', 1, 3650, opts.json)
|
|
5988
6291
|
const limit = parseCliInteger(opts.limit, 'limit', 1, 200, opts.json)
|
|
6292
|
+
requireCliChoice(opts.source, ['wechat', 'qq', 'all'], 'source', opts.json)
|
|
5989
6293
|
// 与 `draft` 同一条纪律:**"确认过了"只用一个变量**,参数在确认之后才拼
|
|
5990
6294
|
let confirmed = !!opts.yes
|
|
5991
6295
|
if (!opts.dryRun && !confirmed) {
|
|
5992
6296
|
const preview = {
|
|
5993
6297
|
success: false, dryRun: false, action: 'chat-notes',
|
|
5994
|
-
code: 'CONFIRMATION_REQUIRED', days, limit,
|
|
6298
|
+
code: 'CONFIRMATION_REQUIRED', days, limit, source: opts.source,
|
|
5995
6299
|
readsLocalChat: true, invokesAI: true, writesLocalFiles: true, sendsNothing: true,
|
|
5996
6300
|
}
|
|
5997
6301
|
if (opts.json) {
|
|
@@ -6011,6 +6315,7 @@ program
|
|
|
6011
6315
|
}
|
|
6012
6316
|
}
|
|
6013
6317
|
const args = [script, '--days', String(days), '--limit', String(limit),
|
|
6318
|
+
'--source', String(opts.source),
|
|
6014
6319
|
...(confirmed ? ['--yes'] : []),
|
|
6015
6320
|
...(opts.transcribeVoice ? ['--transcribe-voice'] : []),
|
|
6016
6321
|
...(opts.json ? ['--json'] : []),
|
|
@@ -7455,12 +7760,12 @@ program
|
|
|
7455
7760
|
|
|
7456
7761
|
try {
|
|
7457
7762
|
const args = [script, '--years', String(opts.years || '1,3,5'), '--limit', String(limit)]
|
|
7458
|
-
if (opts.talker) args.push('--talker', String(opts.talker))
|
|
7459
7763
|
if (opts.json) args.push('--json')
|
|
7764
|
+
// D-020:会话标识不进子进程 argv(进程列表里看得见),走 WEFLOW_TALKER。
|
|
7460
7765
|
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
7461
7766
|
timeout: 300_000,
|
|
7462
7767
|
maxBuffer: 10 * 1024 * 1024,
|
|
7463
|
-
env:
|
|
7768
|
+
env: createPythonProcessEnv(opts.talker ? { WEFLOW_TALKER: String(opts.talker) } : {}),
|
|
7464
7769
|
})
|
|
7465
7770
|
console.log(stdout)
|
|
7466
7771
|
} catch (e: any) {
|
|
@@ -7539,12 +7844,12 @@ program
|
|
|
7539
7844
|
|
|
7540
7845
|
try {
|
|
7541
7846
|
const args = [script, '--days', String(days), '--limit', String(limit)]
|
|
7542
|
-
if (opts.talker) args.push('--talker', String(opts.talker))
|
|
7543
7847
|
if (opts.json) args.push('--json')
|
|
7848
|
+
// D-020:同 today——会话标识走 WEFLOW_TALKER,不进 argv。
|
|
7544
7849
|
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
7545
7850
|
timeout: 300_000,
|
|
7546
7851
|
maxBuffer: 10 * 1024 * 1024,
|
|
7547
|
-
env:
|
|
7852
|
+
env: createPythonProcessEnv(opts.talker ? { WEFLOW_TALKER: String(opts.talker) } : {}),
|
|
7548
7853
|
})
|
|
7549
7854
|
console.log(stdout)
|
|
7550
7855
|
} catch (e: any) {
|
|
@@ -7561,6 +7866,84 @@ program
|
|
|
7561
7866
|
}
|
|
7562
7867
|
})
|
|
7563
7868
|
|
|
7869
|
+
// ==================== leads(线索雷达)====================
|
|
7870
|
+
program
|
|
7871
|
+
.command('leads')
|
|
7872
|
+
.description('线索雷达:聊天里的报价/采购/合作/需求信号,带原话与跟进状态(规则判定,不调模型)')
|
|
7873
|
+
.option('--days <number>', '看最近多少天', '30')
|
|
7874
|
+
.option('--min-score <number>', '最低分,4 分以上更接近真实生意', '2')
|
|
7875
|
+
.option('--limit <number>', '最多列几条', '20')
|
|
7876
|
+
.option('--json', '输出 JSON 格式')
|
|
7877
|
+
.action(async (opts) => {
|
|
7878
|
+
const { execFile } = await import('child_process')
|
|
7879
|
+
const { promisify } = await import('util')
|
|
7880
|
+
const execFileAsync = promisify(execFile)
|
|
7881
|
+
const script = join(resolvePackageRoot(), 'scripts', 'leads.py')
|
|
7882
|
+
const days = parseCliInteger(opts.days, 'days', 1, 3650, opts.json)
|
|
7883
|
+
const minScore = parseCliInteger(opts.minScore, 'min-score', 1, 50, opts.json)
|
|
7884
|
+
const limit = parseCliInteger(opts.limit, 'limit', 1, 500, opts.json)
|
|
7885
|
+
|
|
7886
|
+
try {
|
|
7887
|
+
const args = [script, '--days', String(days), '--min-score', String(minScore),
|
|
7888
|
+
'--limit', String(limit)]
|
|
7889
|
+
if (opts.json) args.push('--json')
|
|
7890
|
+
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
7891
|
+
timeout: 300_000,
|
|
7892
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
7893
|
+
env: pythonProcessEnv(),
|
|
7894
|
+
})
|
|
7895
|
+
console.log(stdout)
|
|
7896
|
+
} catch (e: any) {
|
|
7897
|
+
if (opts.json) {
|
|
7898
|
+
console.log(JSON.stringify({
|
|
7899
|
+
success: false,
|
|
7900
|
+
code: 'LEADS_FAILED',
|
|
7901
|
+
error: '线索雷达不可用,请运行 check --json 检查配置和依赖',
|
|
7902
|
+
}))
|
|
7903
|
+
process.exit(1)
|
|
7904
|
+
}
|
|
7905
|
+
console.error(chalk.red(`\n${safeSubprocessError(e, '统计失败')}`))
|
|
7906
|
+
process.exit(1)
|
|
7907
|
+
}
|
|
7908
|
+
})
|
|
7909
|
+
|
|
7910
|
+
// ==================== replay(关系时间重放)====================
|
|
7911
|
+
program
|
|
7912
|
+
.command('replay')
|
|
7913
|
+
.description('关系网络时间重放:按月展开,生成可播放的单文件 HTML(本地、离线、不调模型)')
|
|
7914
|
+
.option('--out <file>', '输出路径', 'output/replay.html')
|
|
7915
|
+
.option('--min-weight <number>', '当月互动量门槛,低于此不计入', '10')
|
|
7916
|
+
.option('--json', '只输出聚合结果(验收用)')
|
|
7917
|
+
.action(async (opts) => {
|
|
7918
|
+
const { execFile } = await import('child_process')
|
|
7919
|
+
const { promisify } = await import('util')
|
|
7920
|
+
const execFileAsync = promisify(execFile)
|
|
7921
|
+
const script = join(resolvePackageRoot(), 'scripts', 'replay.py')
|
|
7922
|
+
const minWeight = parseCliInteger(opts.minWeight, 'min-weight', 1, 10000, opts.json)
|
|
7923
|
+
|
|
7924
|
+
try {
|
|
7925
|
+
const args = [script, '--min-weight', String(minWeight), '--out', String(opts.out)]
|
|
7926
|
+
if (opts.json) args.push('--json')
|
|
7927
|
+
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
7928
|
+
timeout: 600_000,
|
|
7929
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
7930
|
+
env: pythonProcessEnv(),
|
|
7931
|
+
})
|
|
7932
|
+
console.log(stdout)
|
|
7933
|
+
} catch (e: any) {
|
|
7934
|
+
if (opts.json) {
|
|
7935
|
+
console.log(JSON.stringify({
|
|
7936
|
+
success: false,
|
|
7937
|
+
code: 'REPLAY_FAILED',
|
|
7938
|
+
error: '时间重放不可用,请运行 check --json 检查配置和依赖',
|
|
7939
|
+
}))
|
|
7940
|
+
process.exit(1)
|
|
7941
|
+
}
|
|
7942
|
+
console.error(chalk.red(`\n${safeSubprocessError(e, '生成失败')}`))
|
|
7943
|
+
process.exit(1)
|
|
7944
|
+
}
|
|
7945
|
+
})
|
|
7946
|
+
|
|
7564
7947
|
// ==================== timeline(某号观点演变)====================
|
|
7565
7948
|
program
|
|
7566
7949
|
.command('timeline')
|