weflow-cli 1.16.0 → 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 +42 -0
- package/README.md +16 -4
- package/bin/weflow-cli.ts +348 -6
- package/dist/bin/weflow-cli.js +381 -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/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 -213
- 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 -105
- package/scripts/leads.py +179 -179
- 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/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,48 @@ purpose** - a hand-written "latest is X" is exactly the claim that goes stale he
|
|
|
17
17
|
|
|
18
18
|
All notable user-facing changes are recorded here. This project follows [Semantic Versioning](https://semver.org/).
|
|
19
19
|
|
|
20
|
+
## 1.17.0
|
|
21
|
+
|
|
22
|
+
- **`sync cursor-probe`:稳定游标从"一句 false"变成一条看得见的路径(D-027)。**
|
|
23
|
+
`capabilities --json` 新增 `cursorReadiness`(五条判据 × 四个消息后端的代码事实),
|
|
24
|
+
`sync cursor-probe --json` 实测 NT 数据侧——schema 全量检查、身份有界采样(自带
|
|
25
|
+
`exhaustive: false`)。本机实测:211 张表 0 缺列、采样 5,398 行全部带非零 server_id 且
|
|
26
|
+
0 重复;`stableCursor` 仍为 false,差的在哪、下一块是什么,逐条摊开给下游看。
|
|
27
|
+
|
|
28
|
+
- **`weflow-cli setup`:一条命令装好 Python 环境(自管 venv)。** 在 `~/.weflow-cli/venv`
|
|
29
|
+
建虚拟环境并安装依赖,之后所有 Python 工作流优先用它——PATH 上只有商店占位 python
|
|
30
|
+
(exit 9009)的那类 issue 从根上消掉。Windows 上自动用 `sqlcipher3-wheels` 预编译轮
|
|
31
|
+
(`sqlcipher3-binary` 根本没有 Windows 发行版);两段式(`--dry-run` 计划 / `--yes` 执行),
|
|
32
|
+
pip 瞬时网络失败重试一次,装完验证 `import sqlcipher3` 与运行时确实优先 venv。
|
|
33
|
+
|
|
34
|
+
- **2D 概念图谱:点一条边看它凭什么连着。** 每条边算出两页 `## 来源` 的交集(共同来源卡片),
|
|
35
|
+
点边高亮并列出;没有共同来源的边如实说明"来自页面正文里的 [[链接]]"。每边最多带 2 个名字
|
|
36
|
+
(页面体积有界),证据只进页面载荷——布局缓存键不变,旧坐标不作废。3D 不带。
|
|
37
|
+
|
|
38
|
+
- **助手能看对方发来的图片了(D-128,决策先行)。** 开了 `wechatMediaDownload` 的来信图片
|
|
39
|
+
从"整条忽略"变成一轮:模型可用 `look_at_image` 的 `#信1` 直接看落盘文件。门与看聊天图片
|
|
40
|
+
同构(strict 双层拦截、每轮两张上限、审计照记);超过 4 MB 不缩放直接说明(证据形态);
|
|
41
|
+
语音/视频/文件不在本轮。未开下载或没落盘的图片消息行为不变。
|
|
42
|
+
|
|
43
|
+
- **修了主分支上四处红的回归测试**(`npm test` 在 master 上本来就挂着 4 条,stash 验证过与
|
|
44
|
+
新特性无关):`today` / `promises` 曾把 `--talker` 放进子进程 argv(进程列表可见),改走
|
|
45
|
+
`WEFLOW_TALKER` 环境变量(D-020,`test/privacy-paths` 转绿);`dailyMailTo` 补齐默认值三处
|
|
46
|
+
登记(`test/config-keys` 三条转绿);`scripts/**/*.vbs` 进 npm 包——`run_hidden.vbs` 之前被
|
|
47
|
+
files allowlist 漏掉(`test/pack-contents` 转绿)。
|
|
48
|
+
|
|
49
|
+
- **`weflow-cli import qchat`:QQ 聊天进知识库与搜索(qchat-cli 一行不用改,D-129)。**
|
|
50
|
+
读 `qce export --format json` 的导出(单文件或目录),按全局唯一的 messageId 去重合并、
|
|
51
|
+
重复导入安全;两段式命令面。落盘后两条管道各开一个显式开关:`chat-notes --source qq|all`
|
|
52
|
+
产 QQ 知识卡(`QQ-` 前缀,与微信卡同形状、同下游),`search-index --include-imports` 把
|
|
53
|
+
QQ 会话收进语义与短语索引(id 带 `chat:qq:` 段)。没有微信库的机器也能用(纯 QQ 场景,
|
|
54
|
+
端到端有测试)。字段映射表见 `docs/DATA_CONTRACT.md`。
|
|
55
|
+
|
|
56
|
+
- **`replay`:关系网络的时间重放。** `weflow-cli replay` 出按月文本回放(哪些边断了、
|
|
57
|
+
哪些新长出来、哪些最持久),`--out out.html` 出可播放的单文件动画:按天推进、边用 30 天
|
|
58
|
+
滚动窗口、节点标签自动避让、播放速度可调;守恒校验内建,稀薄月份显式标注而非假装数据
|
|
59
|
+
完整;另带静态总结页(断了哪些 / 最持久 / 新出现 + 月度柱状图)。数据全部来自本地库,
|
|
60
|
+
不联网、不调模型。
|
|
61
|
+
|
|
20
62
|
## 1.16.0
|
|
21
63
|
|
|
22
64
|
- **`leads` - the offers and asks buried in chat, with the sentence that said them.** Scans recent
|
package/README.md
CHANGED
|
@@ -107,7 +107,7 @@ Linux 的自动初始化能力取决于微信发行版、Python 依赖和当前
|
|
|
107
107
|
| --- | --- |
|
|
108
108
|
| 聊天记录 | 查询会话、联系人和消息;导出 **Markdown / JSON / TXT / HTML / Excel**。Markdown 导出把图片**落盘**到 `<会话>_assets/`、正文用相对路径引用(喂给编辑器或别的工具时这才是能用的形态);**文档本体**(微信里的 Word/Excel/PDF/PPT 等)按原名与扩展名拷进 `<会话>_assets/files/`,正文里按原名链接到 `files/` 下;HTML 导出内嵌图片并给出一份媒体覆盖报告(哪些没嵌上、为什么)。**匹配只做一遍**:Markdown 导出写下清单,之后 JSON/TXT/Excel 导出(或 `--media`)会自动带上同样的图片与文档路径。 |
|
|
109
109
|
| 公众号日报 | 抓取文章、AI 摘要与分类、生成本地阅读页,保留收藏和已读状态。 |
|
|
110
|
-
| 个人知识库 | 同步微信读书笔记、构建 Obsidian Vault、语义搜索、RAG 问答和概念 Wiki,以及**双击就能打开**的概念图谱页面(`weflow-cli wiki graph --open` 出 3D、加 `--flat` 出 2D;自包含、不联网、不需要服务器;`--min-degree N` 只看枢纽、`--line wiki\|chat`
|
|
110
|
+
| 个人知识库 | 同步微信读书笔记、构建 Obsidian Vault、语义搜索、RAG 问答和概念 Wiki,以及**双击就能打开**的概念图谱页面(`weflow-cli wiki graph --open` 出 3D、加 `--flat` 出 2D;自包含、不联网、不需要服务器;`--min-degree N` 只看枢纽、`--line wiki\|chat` 只看一条线;**2D 页里点一条边能看到它凭什么连着**——两页的共同来源卡片,没有共同来源的边也会如实说来自正文链接)。 |
|
|
111
111
|
| AI 协作 | 通过 MCP 把文章、知识库、日报和受控的本地数据能力交给兼容客户端;工具清单以 `weflow-cli mcp-config` 和 `docs/MCP.md` 为准。 |
|
|
112
112
|
| 个人回顾 | 聊天月报、年度报告、待办提取与朋友圈本地缓存查询。 |
|
|
113
113
|
| 微信收藏 | 读取微信"收藏"内容(公众号文章、文字、图片、视频、聊天记录),支持类型过滤、关键词搜索与 Markdown/JSON 导出。 |
|
|
@@ -137,7 +137,15 @@ weflow-cli check
|
|
|
137
137
|
> - 马上能用:换成 `weflow-cli.cmd check`(`.cmd` 那份包装不走 PowerShell 策略);
|
|
138
138
|
> - 一次性解决:`Set-ExecutionPolicy -Scope CurrentUser RemoteSigned`,然后重开终端。
|
|
139
139
|
|
|
140
|
-
|
|
140
|
+
一条命令装好 Python 依赖(推荐)——它会在 `~/.weflow-cli/venv` 建一个本工具**自管**的虚拟环境
|
|
141
|
+
并安装依赖,之后的 Python 工作流会优先用它,不用再管 PATH 上哪个 Python 可用
|
|
142
|
+
(Windows 上自动改用 `sqlcipher3-wheels` 预编译轮,省去装编译器的麻烦):
|
|
143
|
+
|
|
144
|
+
```powershell
|
|
145
|
+
weflow-cli setup
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
也可以手动安装(效果相同;从源码运行时在仓库根目录执行):
|
|
141
149
|
|
|
142
150
|
```powershell
|
|
143
151
|
python -m pip install -r requirements.txt
|
|
@@ -278,6 +286,8 @@ weflow-cli panel # 打开本机面板窗口 (不扫码也能用)
|
|
|
278
286
|
- 「知识库里怎么讲 RAG 的?」— 概念 Wiki 检索
|
|
279
287
|
- 「帮我回老王那句话」— 起草回复:先判断对方要什么、风险和该不该给实质内容,再给几条候选。
|
|
280
288
|
**只产出文本,不会替你发送**;涉钱或风险很高时不给草稿,只说明该先确认什么
|
|
289
|
+
- 「(对方直接发来一张图片)」— 开了 `wechatMediaDownload` 后图片会落盘,助手能直接看图回答
|
|
290
|
+
(隐私模式 strict 下不出境,见下段与 D-128)
|
|
281
291
|
- 「记住:我的项目叫 weflow-cli」— 写入长期记忆
|
|
282
292
|
|
|
283
293
|
运行机制:守护进程通过微信官方 Bot 通道(iLink)长轮询收发消息,Agent 循环、三层记忆(工作窗口 / 滚动摘要 / 长期事实)、数据库查询全部在本机执行;仅最终提问与回复文本会发送给所配置的 LLM,新装默认就是 `assistantPrivacy=strict`:工具输出中的电话/邮箱/链接等 PII 自动脱敏,且第三方聊天正文不出境(`balanced` 放宽到正文可出境、只对 PII 打码,`open` 全不打码;`ollama` 本地引擎则完全不出网)。会话 24 小时未活跃需重新扫码,单窗口内主动回复有官方条数限制。
|
|
@@ -303,7 +313,7 @@ weflow-cli config set assistantGroupRequireMention true
|
|
|
303
313
|
|
|
304
314
|
| 目标 | 命令 |
|
|
305
315
|
| --- | --- |
|
|
306
|
-
| 检查环境与配置 | `weflow-cli check` · `weflow-cli config show` |
|
|
316
|
+
| 检查环境与配置 | `weflow-cli check` · `weflow-cli setup`(自管 Python venv,一条命令装依赖) · `weflow-cli config show` |
|
|
307
317
|
| 初始化或手动指定路径 | `weflow-cli init [--path <目录>]` |
|
|
308
318
|
| 浏览聊天数据 | `weflow-cli sessions` · `weflow-cli messages <联系人>` · `weflow-cli contacts` |
|
|
309
319
|
| 谁在等我回话 | `weflow-cli awaiting --dry-run` · `weflow-cli awaiting --yes` |
|
|
@@ -314,9 +324,11 @@ weflow-cli config set assistantGroupRequireMention true
|
|
|
314
324
|
| 那年今日 | `weflow-cli today --years 1,3,5`(几年前的今天在聊什么)|
|
|
315
325
|
| 人脉图谱 | `weflow-cli network --groups` · `--with <名字>`(共同群)· `--intro <名字>`(谁能引荐;只读 contact.db)|
|
|
316
326
|
| 某号观点演变 | `weflow-cli timeline --list` · `--source <公众号名>`(按时间看它说法的变化)|
|
|
327
|
+
| 关系网络时间重放 | `weflow-cli replay`(按月展开,生成可播放的单文件 HTML;看哪条边断了,不只是图变大)|
|
|
317
328
|
| 本机判断层 | `weflow-cli decide --request <file> --dry-run` · `--yes` |
|
|
318
329
|
| 导出聊天记录 | `weflow-cli export <联系人> <json\|txt\|html\|excel>` |
|
|
319
|
-
| 同步检查点 | `weflow-cli sync run <联系人> --since <日期>` · `sync status` · `sync verify`
|
|
330
|
+
| 同步检查点 | `weflow-cli sync run <联系人> --since <日期>` · `sync status` · `sync verify` · `sync cursor-probe`(离稳定增量游标还差什么,实测当前后端)|
|
|
331
|
+
| QQ 聊天导入 | `weflow-cli import qchat <qchat导出>` · `import list` · `import show <会话>` (读 qchat-cli 的 JSON 导出;之后 `chat-notes --source qq` 产知识卡、`search-index --include-imports` 进搜索)|
|
|
320
332
|
| 公众号日报与阅读器 | `weflow-cli daily` · `weflow-cli daily-server` · `weflow-cli review` |
|
|
321
333
|
| 朋友圈缓存 | `weflow-cli sns timeline` · `weflow-cli sns users` · `weflow-cli sns stats` |
|
|
322
334
|
| 微信收藏 | `weflow-cli fav list` · `weflow-cli fav export markdown` · `weflow-cli fav set-key` |
|
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) {
|
|
@@ -7602,6 +7907,43 @@ program
|
|
|
7602
7907
|
}
|
|
7603
7908
|
})
|
|
7604
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
|
+
|
|
7605
7947
|
// ==================== timeline(某号观点演变)====================
|
|
7606
7948
|
program
|
|
7607
7949
|
.command('timeline')
|