weflow-cli 1.17.0 → 1.19.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 +30 -0
- package/README.md +3 -0
- package/bin/weflow-cli.ts +167 -0
- package/dist/bin/weflow-cli.js +199 -0
- package/dist/bin/weflow-cli.js.map +1 -1
- package/dist/src/services/diagnosticReport.d.ts +6 -0
- package/dist/src/services/diagnosticReport.d.ts.map +1 -1
- package/dist/src/services/diagnosticReport.js +16 -0
- package/dist/src/services/diagnosticReport.js.map +1 -1
- package/dist/src/services/wechatCompat.d.ts +29 -0
- package/dist/src/services/wechatCompat.d.ts.map +1 -0
- package/dist/src/services/wechatCompat.js +65 -0
- package/dist/src/services/wechatCompat.js.map +1 -0
- package/docs/DATA_CONTRACT.md +25 -25
- package/docs/DECISIONS.md +26 -0
- 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/NT_DATABASES.md +47 -0
- package/docs/POSITIONING.md +68 -68
- package/docs/PROJECT_STATE.md +2 -1
- package/docs/RELEASING.md +4 -0
- package/package.json +1 -1
- 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/panel/quick-menu.cjs +197 -197
- package/scripts/_utils.py +1338 -1338
- package/scripts/annual_report.py +3 -1
- package/scripts/article_notes.py +423 -423
- package/scripts/contact_schema.py +339 -339
- package/scripts/create_reading_notes.py +11 -11
- package/scripts/daily_task.ps1 +213 -213
- package/scripts/extract_todos.py +3 -1
- package/scripts/fav_notes.py +321 -321
- package/scripts/health_check.py +473 -473
- package/scripts/imported_chats.py +68 -68
- package/scripts/install_tasks.ps1 +105 -105
- package/scripts/leads.py +179 -179
- package/scripts/mcp_bridge.py +3 -1
- package/scripts/media_index.py +150 -0
- package/scripts/network.py +206 -206
- package/scripts/nt_common.py +32 -0
- package/scripts/nt_decrypt.py +19 -15
- package/scripts/opinion_timeline.py +138 -138
- package/scripts/replay.py +803 -803
- package/scripts/run_hidden.vbs +24 -24
- package/scripts/semantic_search.py +5 -1
- 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/scripts/wechat_avatar.py +185 -0
- package/scripts/wechat_ledger.py +190 -0
- package/src/services/cursorProbe.ts +222 -222
- package/src/services/diagnosticReport.ts +22 -0
- package/src/services/qchatImport.ts +327 -327
- package/src/services/setupVenv.ts +238 -238
- package/src/services/syncService.ts +363 -363
- package/src/services/syncState.ts +275 -275
- package/src/services/wechatCompat.ts +67 -0
- package/src/services/wechatMedia.ts +166 -166
package/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,36 @@ 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.19.0
|
|
21
|
+
|
|
22
|
+
- **修复 issue #12:`local_type == 1` 的压缩正文导出为空,普通文本被截断到 200 字。**
|
|
23
|
+
`message_content` 若是 zstd 压缩的 BLOB,旧代码只认 `str`,正文被置空后落进
|
|
24
|
+
`non_text_display()`,最终输出 `[未识别的消息类型 1]`——**消息还在、正文没了**,
|
|
25
|
+
下游按 `parsedContent` 做的关键词 / `@所有人` 筛选于是漏掉这些通知。
|
|
26
|
+
现在类型 1 一律走已有的 `_decode_content()`(str / zstd frame / 普通 bytes 三种都处理),
|
|
27
|
+
且 `parsedContent` 对文本**不再截断**:末尾关键词正是筛选要用的,截掉等于静默漏消息。
|
|
28
|
+
非文本仍保留 200 字展示上限——那是标签与标题,不是正文。
|
|
29
|
+
- **`media` — md5 直接对到本地文件(读 `hardlink.db`,本地只读)。** 图片、文件、视频三张表各记
|
|
30
|
+
`md5 → (dir1, dir2, file_name)`,`dir2id` 把 id 映射成目录名,路径是**确定的**,不用再扫整个
|
|
31
|
+
`msg/` 目录去猜。每一行都标 `✓/✗`:`✗` 是"库里有记录但文件已被微信清理",不是路径算错。
|
|
32
|
+
**图片的 dir1/dir2 顺序与另两张表相反**:实测 image 行是 `dir1=会话hash, dir2=月份`
|
|
33
|
+
(`attach/<会话>/<月份>/Img/`),file/video 行是 `dir1=月份`。按统一顺序拼,4647 条图片会全指到
|
|
34
|
+
一个空目录,而文件/视频那条仍然正确——只看某一类能打开就会漏掉这个错。
|
|
35
|
+
|
|
36
|
+
## 1.18.0
|
|
37
|
+
|
|
38
|
+
- **`ledger` — 红包与转账流水(读 `general.db`,本地只读)。** 把两张表归一成一条按时间排的
|
|
39
|
+
流水:`kind`(红包/转账)、方向(我付/我收/我发/对方发/未知)、对方、`messageServerId`。
|
|
40
|
+
方向判定用前缀容忍的自己 id 比对,双方都是自己时如实给「未知」而不是猜一个。
|
|
41
|
+
**两处已知的"不猜"**:红包表**没有时间列也没有金额列**——真实时间与金额在消息正文的
|
|
42
|
+
`wcpayinfo` 里,这一步没做,所以红包按 `messageServerId` 排先后并标注"无时间列";
|
|
43
|
+
`hb_status`/`receive_status`/`pay_sub_type` 的枚举语义**未逐值验证**,原样输出(本机观测
|
|
44
|
+
`hb_status ∈ {1,4}`、`receive_status ∈ {0,1,2}`),不给"已领取/已过期"这类翻译。
|
|
45
|
+
- **测试隔离了两处会读真实数据的路径。** `qchat_import_test` 的 `build_index` 用例只把
|
|
46
|
+
`INDEX_DIR` 指到临时目录,`OUTPUT_ROOT` 仍是真实值,于是它扫到本机 10,981 篇日报文章,
|
|
47
|
+
`new` 断言 5 变成 10981;`wechat_ledger_test` 的红包 fixture 按 9 列建表却只给 7 个值,
|
|
48
|
+
`hb_status` 整体左移两格落进 `send_id`。两处都改成显式隔离/对齐,不是调期望值。
|
|
49
|
+
|
|
20
50
|
## 1.17.0
|
|
21
51
|
|
|
22
52
|
- **`sync cursor-probe`:稳定游标从"一句 false"变成一条看得见的路径(D-027)。**
|
package/README.md
CHANGED
|
@@ -325,12 +325,15 @@ weflow-cli config set assistantGroupRequireMention true
|
|
|
325
325
|
| 人脉图谱 | `weflow-cli network --groups` · `--with <名字>`(共同群)· `--intro <名字>`(谁能引荐;只读 contact.db)|
|
|
326
326
|
| 某号观点演变 | `weflow-cli timeline --list` · `--source <公众号名>`(按时间看它说法的变化)|
|
|
327
327
|
| 关系网络时间重放 | `weflow-cli replay`(按月展开,生成可播放的单文件 HTML;看哪条边断了,不只是图变大)|
|
|
328
|
+
| 红包与转账流水 | `weflow-cli ledger --days 365`(读 `general.db`;红包表无时间/金额列,只给先后与原始状态,不翻译语义)|
|
|
329
|
+
| 媒体精确定位 | `weflow-cli media --md5 <前缀>` · `--type image\|file\|video`(读 `hardlink.db`,md5 直接对到本地路径,不扫目录)|
|
|
328
330
|
| 本机判断层 | `weflow-cli decide --request <file> --dry-run` · `--yes` |
|
|
329
331
|
| 导出聊天记录 | `weflow-cli export <联系人> <json\|txt\|html\|excel>` |
|
|
330
332
|
| 同步检查点 | `weflow-cli sync run <联系人> --since <日期>` · `sync status` · `sync verify` · `sync cursor-probe`(离稳定增量游标还差什么,实测当前后端)|
|
|
331
333
|
| QQ 聊天导入 | `weflow-cli import qchat <qchat导出>` · `import list` · `import show <会话>` (读 qchat-cli 的 JSON 导出;之后 `chat-notes --source qq` 产知识卡、`search-index --include-imports` 进搜索)|
|
|
332
334
|
| 公众号日报与阅读器 | `weflow-cli daily` · `weflow-cli daily-server` · `weflow-cli review` |
|
|
333
335
|
| 朋友圈缓存 | `weflow-cli sns timeline` · `weflow-cli sns users` · `weflow-cli sns stats` |
|
|
336
|
+
| 联系人头像 | `weflow-cli avatar <联系人>` · `avatar --list` · `avatar --all`(读 head_image.db,头像本体在库里,缓存清理不丢) |
|
|
334
337
|
| 微信收藏 | `weflow-cli fav list` · `weflow-cli fav export markdown` · `weflow-cli fav set-key` |
|
|
335
338
|
| 微信读书 | `weflow-cli weread shelf` · `notes` · `search` · `stats`(key 从 https://weread.qq.com/r/weread-skills 拿,见 [OPERATIONS.md](./OPERATIONS.md)) |
|
|
336
339
|
| 知识库 | `weflow-cli vault` · `weflow-cli wiki` · `weflow-cli search <query>` · `weflow-cli chat`(语义检索需先 `config set dashscopeApiKey`;`search --phrase <词> --line all` 连语音转写一起搜)|
|
package/bin/weflow-cli.ts
CHANGED
|
@@ -33,6 +33,7 @@ import type { ChatSession } from '../src/types.js'
|
|
|
33
33
|
import { disabledIds, describeSkill, readSkillBody, scanSkills, skillRoots, skillState } from '../src/services/assistantSkills.js'
|
|
34
34
|
import { assistantScenes, validateSceneId, type Scene } from '../src/services/assistantScenes.js'
|
|
35
35
|
import { detectWechatClient, reportedDataDirs, samePath } from '../src/services/wechatClient.js'
|
|
36
|
+
import { WECHAT_VERIFIED, compareWechatVersions, describeCompatVerdict } from '../src/services/wechatCompat.js'
|
|
36
37
|
|
|
37
38
|
const program = new Command()
|
|
38
39
|
|
|
@@ -305,6 +306,14 @@ program
|
|
|
305
306
|
const data = {
|
|
306
307
|
schema: 'weflow-capabilities/v1',
|
|
307
308
|
version: CLI_VERSION,
|
|
309
|
+
// D-130:本包最后用真实数据验证过的微信版本(单一事实源 wechatCompat.ts)。
|
|
310
|
+
// check 的判定也从同一处读;发布清单要求 CHANGELOG 每版记录(来源就是这里)。
|
|
311
|
+
compatibility: {
|
|
312
|
+
wechat: {
|
|
313
|
+
verifiedVersion: WECHAT_VERIFIED.version,
|
|
314
|
+
verifiedAt: WECHAT_VERIFIED.verifiedAt,
|
|
315
|
+
},
|
|
316
|
+
},
|
|
308
317
|
read: {
|
|
309
318
|
sessions: { cli: 'sessions --json', mcp: 'wechat.list_sessions' },
|
|
310
319
|
messages: { cli: 'messages <talker> --json', mcp: 'wechat.export_messages' },
|
|
@@ -2105,6 +2114,108 @@ syncCmd
|
|
|
2105
2114
|
console.log('')
|
|
2106
2115
|
})
|
|
2107
2116
|
|
|
2117
|
+
// ==================== ledger(资金流水:红包/转账,读 general.db) ====================
|
|
2118
|
+
// 2026-10-26 NT 普查产物(docs/NT_DATABASES.md)。如实边界写在脚本头:红包表无
|
|
2119
|
+
// 时间/金额列(在消息正文里,按 messageServerId 溯源是后续工作);状态枚举语义
|
|
2120
|
+
// 未逐值验证,原样输出不翻译。本地只读、不联网。
|
|
2121
|
+
program
|
|
2122
|
+
.command('ledger')
|
|
2123
|
+
.description('资金流水:红包与转账(读 general.db;本地只读)')
|
|
2124
|
+
.option('--talker <contact>', '只看某个会话(id 或备注/昵称子串)')
|
|
2125
|
+
.option('--days <n>', '只看最近 N 天(按转账时间;红包无时间列不受此限)')
|
|
2126
|
+
.option('--json', '输出 JSON 格式')
|
|
2127
|
+
.action(async (opts) => {
|
|
2128
|
+
const { execFile } = await import('child_process')
|
|
2129
|
+
const { promisify } = await import('util')
|
|
2130
|
+
const execFileAsync = promisify(execFile)
|
|
2131
|
+
const script = join(resolvePackageRoot(), 'scripts', 'wechat_ledger.py')
|
|
2132
|
+
const args = [script, '--json']
|
|
2133
|
+
if (opts.days !== undefined) {
|
|
2134
|
+
const days = parseCliInteger(opts.days, 'days', 1, 36_500, opts.json)
|
|
2135
|
+
args.push('--days', String(days))
|
|
2136
|
+
}
|
|
2137
|
+
try {
|
|
2138
|
+
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
2139
|
+
timeout: 120_000, maxBuffer: 10 * 1024 * 1024,
|
|
2140
|
+
// D-020:会话标识走环境变量,不进 argv
|
|
2141
|
+
env: createPythonProcessEnv(opts.talker ? { WEFLOW_TALKER: String(opts.talker) } : {}),
|
|
2142
|
+
})
|
|
2143
|
+
const payload = JSON.parse(stdout.slice(stdout.indexOf('{')))
|
|
2144
|
+
if (opts.json) { console.log(JSON.stringify(payload)); if (!payload.success) process.exit(1); return }
|
|
2145
|
+
if (!payload.success) { console.error(chalk.red(`✗ ${payload.error}`)); process.exit(1) }
|
|
2146
|
+
console.log(chalk.cyan(`
|
|
2147
|
+
资金流水:红包 ${payload.redEnvelopes} 笔、转账 ${payload.transfers} 笔`))
|
|
2148
|
+
console.log(chalk.gray(' (红包无时间/金额列——在消息正文里,按 messageServerId 看先后;状态枚举原样,语义未验证)'))
|
|
2149
|
+
for (const r of payload.rows.slice(0, 40)) {
|
|
2150
|
+
const when = r.time ? new Date(r.time * 1000).toLocaleString('zh-CN', { hour12: false }) : '(无时间列)'
|
|
2151
|
+
const extra = r.kind === '红包'
|
|
2152
|
+
? `status=${r.hbStatus}/${r.receiveStatus}`
|
|
2153
|
+
: `对方=${r.counterpartyDisplay || r.counterparty || '?'}${r.invalidTime ? ` invalid=${r.invalidTime}` : ''}`
|
|
2154
|
+
console.log(` [${when}] ${r.kind} · ${r.sessionDisplay} · ${r.direction} · ${extra}`)
|
|
2155
|
+
}
|
|
2156
|
+
if (payload.rows.length > 40) console.log(chalk.gray(` …共 ${payload.rows.length} 行(--json 看全表)`))
|
|
2157
|
+
console.log('')
|
|
2158
|
+
} catch (e: any) {
|
|
2159
|
+
if (opts.json) { console.log(JSON.stringify({ success: false, code: 'LEDGER_FAILED', error: safeSubprocessError(e) })); process.exit(1) }
|
|
2160
|
+
console.error(chalk.red(`✗ ${safeSubprocessError(e)}`))
|
|
2161
|
+
process.exit(1)
|
|
2162
|
+
}
|
|
2163
|
+
})
|
|
2164
|
+
|
|
2165
|
+
// ==================== avatar(联系人头像,读 head_image.db) ====================
|
|
2166
|
+
// 2026-10-26 NT 数据库普查的直接产物(docs/NT_DATABASES.md):头像本体内嵌在
|
|
2167
|
+
// head_image.db 里(JPEG/PNG 完整字节,按 username 键控,一人多行=变更历史),
|
|
2168
|
+
// 比文件缓存可靠(清理不丢)。本地只读、不联网。
|
|
2169
|
+
program
|
|
2170
|
+
.command('avatar [contact]')
|
|
2171
|
+
.description('导出联系人头像(读 head_image.db;--list 看谁有头像,--all 批量导出最新头像)')
|
|
2172
|
+
.option('--list', '只列出有头像的 username 与头像数')
|
|
2173
|
+
.option('--all', '导出所有 username 的最新头像到 --out 目录')
|
|
2174
|
+
.option('--out <path>', '输出文件(单联系人)或目录(--all),默认 output/avatars/')
|
|
2175
|
+
.option('--json', '输出 JSON 格式')
|
|
2176
|
+
.action(async (contact: string | undefined, opts) => {
|
|
2177
|
+
if (!contact && !opts.list && !opts.all) {
|
|
2178
|
+
if (opts.json) { console.log(JSON.stringify({ success: false, code: 'INVALID_ARGUMENT', error: '要给联系人,或用 --list / --all' })); process.exit(1) }
|
|
2179
|
+
console.error(chalk.red('✗ 要给联系人,或用 --list / --all'))
|
|
2180
|
+
process.exit(1)
|
|
2181
|
+
}
|
|
2182
|
+
if (contact && (opts.list || opts.all)) {
|
|
2183
|
+
if (opts.json) { console.log(JSON.stringify({ success: false, code: 'INVALID_ARGUMENT', error: '联系人与 --list/--all 互斥' })); process.exit(1) }
|
|
2184
|
+
console.error(chalk.red('✗ 联系人与 --list/--all 互斥'))
|
|
2185
|
+
process.exit(1)
|
|
2186
|
+
}
|
|
2187
|
+
const { execFile } = await import('child_process')
|
|
2188
|
+
const { promisify } = await import('util')
|
|
2189
|
+
const execFileAsync = promisify(execFile)
|
|
2190
|
+
const script = join(resolvePackageRoot(), 'scripts', 'wechat_avatar.py')
|
|
2191
|
+
const args = [script, ...(opts.list ? ['--list'] : opts.all ? ['--all'] : []), '--json']
|
|
2192
|
+
// D-020:联系人标识走环境变量,不进 argv;输出路径同理(脚本 --out 缺省读它)
|
|
2193
|
+
const env = {
|
|
2194
|
+
...(contact ? { WEFLOW_TALKER: String(contact) } : {}),
|
|
2195
|
+
...(opts.out ? { WEFLOW_AVATAR_OUT: String(opts.out) } : {}),
|
|
2196
|
+
}
|
|
2197
|
+
try {
|
|
2198
|
+
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
2199
|
+
timeout: 120_000, maxBuffer: 10 * 1024 * 1024,
|
|
2200
|
+
env: createPythonProcessEnv(env),
|
|
2201
|
+
})
|
|
2202
|
+
const payload = JSON.parse(stdout.slice(stdout.indexOf('{')))
|
|
2203
|
+
if (opts.json) { console.log(JSON.stringify(payload)); if (!payload.success) process.exit(1); return }
|
|
2204
|
+
if (!payload.success) { console.error(chalk.red(`✗ ${payload.error}`)); process.exit(1) }
|
|
2205
|
+
if (payload.action === 'avatar.export') {
|
|
2206
|
+
console.log(chalk.green(`✓ 已导出「${payload.username}」的最新头像(${payload.bytes} 字节,历史 ${payload.historyCount} 张)→ ${payload.out}`))
|
|
2207
|
+
} else if (payload.action === 'avatar.export-all') {
|
|
2208
|
+
console.log(chalk.green(`✓ 已导出 ${payload.written} 个最新头像 → ${payload.dir}`))
|
|
2209
|
+
} else {
|
|
2210
|
+
console.log(payload.error || `共 ${payload.users?.length ?? '?'} 个 username 有头像(用 --json 看全表)`)
|
|
2211
|
+
}
|
|
2212
|
+
} catch (e: any) {
|
|
2213
|
+
if (opts.json) { console.log(JSON.stringify({ success: false, code: 'AVATAR_FAILED', error: safeSubprocessError(e) })); process.exit(1) }
|
|
2214
|
+
console.error(chalk.red(`✗ ${safeSubprocessError(e)}`))
|
|
2215
|
+
process.exit(1)
|
|
2216
|
+
}
|
|
2217
|
+
})
|
|
2218
|
+
|
|
2108
2219
|
// ==================== contacts ====================
|
|
2109
2220
|
// contact-schema:读 contact.db 里**群**的 ext_buffer(本地只读:不联网、不调模型)
|
|
2110
2221
|
//
|
|
@@ -7907,6 +8018,47 @@ program
|
|
|
7907
8018
|
}
|
|
7908
8019
|
})
|
|
7909
8020
|
|
|
8021
|
+
// ==================== media(媒体精确匹配)====================
|
|
8022
|
+
program
|
|
8023
|
+
.command('media')
|
|
8024
|
+
.description('媒体精确匹配:用 hardlink.db 把 md5 直接对到本地文件(图片/文件/视频;本地只读)')
|
|
8025
|
+
.option('--md5 <prefix>', '按 md5 前缀查')
|
|
8026
|
+
.option('--type <kind>', 'image | file | video | all', 'all')
|
|
8027
|
+
.option('--limit <n>', '最多列几条', '20')
|
|
8028
|
+
.option('--missing-only', '只列文件已不在本地的(微信会清理)')
|
|
8029
|
+
.option('--json', '输出 JSON 格式')
|
|
8030
|
+
.action(async (opts) => {
|
|
8031
|
+
const { execFile } = await import('child_process')
|
|
8032
|
+
const { promisify } = await import('util')
|
|
8033
|
+
const execFileAsync = promisify(execFile)
|
|
8034
|
+
const script = join(resolvePackageRoot(), 'scripts', 'media_index.py')
|
|
8035
|
+
const limit = parseCliInteger(opts.limit, 'limit', 1, 5000, opts.json)
|
|
8036
|
+
|
|
8037
|
+
try {
|
|
8038
|
+
const args = [script, '--type', String(opts.type || 'all'), '--limit', String(limit)]
|
|
8039
|
+
if (opts.md5) args.push('--md5', String(opts.md5))
|
|
8040
|
+
if (opts.missingOnly) args.push('--missing-only')
|
|
8041
|
+
if (opts.json) args.push('--json')
|
|
8042
|
+
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
8043
|
+
timeout: 300_000,
|
|
8044
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
8045
|
+
env: pythonProcessEnv(),
|
|
8046
|
+
})
|
|
8047
|
+
console.log(stdout)
|
|
8048
|
+
} catch (e: any) {
|
|
8049
|
+
if (opts.json) {
|
|
8050
|
+
console.log(JSON.stringify({
|
|
8051
|
+
success: false,
|
|
8052
|
+
code: 'MEDIA_FAILED',
|
|
8053
|
+
error: '媒体索引不可用,请运行 check --json 检查配置和依赖',
|
|
8054
|
+
}))
|
|
8055
|
+
process.exit(1)
|
|
8056
|
+
}
|
|
8057
|
+
console.error(chalk.red(`\n${safeSubprocessError(e, '查询失败')}`))
|
|
8058
|
+
process.exit(1)
|
|
8059
|
+
}
|
|
8060
|
+
})
|
|
8061
|
+
|
|
7910
8062
|
// ==================== replay(关系时间重放)====================
|
|
7911
8063
|
program
|
|
7912
8064
|
.command('replay')
|
|
@@ -8263,6 +8415,12 @@ program
|
|
|
8263
8415
|
// 只报条数与最近时间(不读崩溃内容)
|
|
8264
8416
|
crashRecords: client.crashRecords,
|
|
8265
8417
|
caveats: client.caveats,
|
|
8418
|
+
// D-130:本机实际加载的版本 vs 本包验证过的版本(判定只有这一份实现)
|
|
8419
|
+
compatibility: {
|
|
8420
|
+
verifiedVersion: WECHAT_VERIFIED.version,
|
|
8421
|
+
verifiedAt: WECHAT_VERIFIED.verifiedAt,
|
|
8422
|
+
verdict: compareWechatVersions(client.version, WECHAT_VERIFIED.version),
|
|
8423
|
+
},
|
|
8266
8424
|
},
|
|
8267
8425
|
}
|
|
8268
8426
|
return data
|
|
@@ -8302,6 +8460,7 @@ program
|
|
|
8302
8460
|
dataDirMatchesConfig: data.wechatClient.dataDirMatchesConfig,
|
|
8303
8461
|
dataDirs: data.wechatClient.dataDirs,
|
|
8304
8462
|
unusableVersionDirs: data.wechatClient.unusableVersionDirs,
|
|
8463
|
+
compatibility: data.wechatClient.compatibility,
|
|
8305
8464
|
},
|
|
8306
8465
|
}))
|
|
8307
8466
|
return
|
|
@@ -8360,6 +8519,14 @@ program
|
|
|
8360
8519
|
}
|
|
8361
8520
|
}
|
|
8362
8521
|
|
|
8522
|
+
// 3.5 微信版本兼容(D-130):本机加载的 vs 本包验证过的
|
|
8523
|
+
{
|
|
8524
|
+
const verdict = compareWechatVersions(client.version, WECHAT_VERIFIED.version)
|
|
8525
|
+
const mark = verdict === 'verified' ? chalk.green('✓') : verdict === 'newer-than-verified' ? chalk.yellow('⚠') : chalk.gray('·')
|
|
8526
|
+
console.log(chalk.white('\n微信版本兼容:'))
|
|
8527
|
+
console.log(` ${mark} ${client.version ?? '未知(微信没在运行)'} · ${describeCompatVerdict(verdict, WECHAT_VERIFIED.version)}`)
|
|
8528
|
+
}
|
|
8529
|
+
|
|
8363
8530
|
// 4. Config
|
|
8364
8531
|
console.log(chalk.white('\n配置状态:'))
|
|
8365
8532
|
const configured = configService.isConfigured()
|
package/dist/bin/weflow-cli.js
CHANGED
|
@@ -31,6 +31,7 @@ import { applyDerivedNtKeys, enableFavorites, detectFavDbPath } from '../src/ser
|
|
|
31
31
|
import { disabledIds, readSkillBody, scanSkills, skillRoots, skillState } from '../src/services/assistantSkills.js';
|
|
32
32
|
import { assistantScenes, validateSceneId } from '../src/services/assistantScenes.js';
|
|
33
33
|
import { detectWechatClient, reportedDataDirs, samePath } from '../src/services/wechatClient.js';
|
|
34
|
+
import { WECHAT_VERIFIED, compareWechatVersions, describeCompatVerdict } from '../src/services/wechatCompat.js';
|
|
34
35
|
const program = new Command();
|
|
35
36
|
function resolvePackageRoot() {
|
|
36
37
|
return resolvePackageRootFrom(import.meta.url);
|
|
@@ -288,6 +289,14 @@ program
|
|
|
288
289
|
const data = {
|
|
289
290
|
schema: 'weflow-capabilities/v1',
|
|
290
291
|
version: CLI_VERSION,
|
|
292
|
+
// D-130:本包最后用真实数据验证过的微信版本(单一事实源 wechatCompat.ts)。
|
|
293
|
+
// check 的判定也从同一处读;发布清单要求 CHANGELOG 每版记录(来源就是这里)。
|
|
294
|
+
compatibility: {
|
|
295
|
+
wechat: {
|
|
296
|
+
verifiedVersion: WECHAT_VERIFIED.version,
|
|
297
|
+
verifiedAt: WECHAT_VERIFIED.verifiedAt,
|
|
298
|
+
},
|
|
299
|
+
},
|
|
291
300
|
read: {
|
|
292
301
|
sessions: { cli: 'sessions --json', mcp: 'wechat.list_sessions' },
|
|
293
302
|
messages: { cli: 'messages <talker> --json', mcp: 'wechat.export_messages' },
|
|
@@ -2160,6 +2169,139 @@ syncCmd
|
|
|
2160
2169
|
}
|
|
2161
2170
|
console.log('');
|
|
2162
2171
|
});
|
|
2172
|
+
// ==================== ledger(资金流水:红包/转账,读 general.db) ====================
|
|
2173
|
+
// 2026-10-26 NT 普查产物(docs/NT_DATABASES.md)。如实边界写在脚本头:红包表无
|
|
2174
|
+
// 时间/金额列(在消息正文里,按 messageServerId 溯源是后续工作);状态枚举语义
|
|
2175
|
+
// 未逐值验证,原样输出不翻译。本地只读、不联网。
|
|
2176
|
+
program
|
|
2177
|
+
.command('ledger')
|
|
2178
|
+
.description('资金流水:红包与转账(读 general.db;本地只读)')
|
|
2179
|
+
.option('--talker <contact>', '只看某个会话(id 或备注/昵称子串)')
|
|
2180
|
+
.option('--days <n>', '只看最近 N 天(按转账时间;红包无时间列不受此限)')
|
|
2181
|
+
.option('--json', '输出 JSON 格式')
|
|
2182
|
+
.action(async (opts) => {
|
|
2183
|
+
const { execFile } = await import('child_process');
|
|
2184
|
+
const { promisify } = await import('util');
|
|
2185
|
+
const execFileAsync = promisify(execFile);
|
|
2186
|
+
const script = join(resolvePackageRoot(), 'scripts', 'wechat_ledger.py');
|
|
2187
|
+
const args = [script, '--json'];
|
|
2188
|
+
if (opts.days !== undefined) {
|
|
2189
|
+
const days = parseCliInteger(opts.days, 'days', 1, 36_500, opts.json);
|
|
2190
|
+
args.push('--days', String(days));
|
|
2191
|
+
}
|
|
2192
|
+
try {
|
|
2193
|
+
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
2194
|
+
timeout: 120_000, maxBuffer: 10 * 1024 * 1024,
|
|
2195
|
+
// D-020:会话标识走环境变量,不进 argv
|
|
2196
|
+
env: createPythonProcessEnv(opts.talker ? { WEFLOW_TALKER: String(opts.talker) } : {}),
|
|
2197
|
+
});
|
|
2198
|
+
const payload = JSON.parse(stdout.slice(stdout.indexOf('{')));
|
|
2199
|
+
if (opts.json) {
|
|
2200
|
+
console.log(JSON.stringify(payload));
|
|
2201
|
+
if (!payload.success)
|
|
2202
|
+
process.exit(1);
|
|
2203
|
+
return;
|
|
2204
|
+
}
|
|
2205
|
+
if (!payload.success) {
|
|
2206
|
+
console.error(chalk.red(`✗ ${payload.error}`));
|
|
2207
|
+
process.exit(1);
|
|
2208
|
+
}
|
|
2209
|
+
console.log(chalk.cyan(`
|
|
2210
|
+
资金流水:红包 ${payload.redEnvelopes} 笔、转账 ${payload.transfers} 笔`));
|
|
2211
|
+
console.log(chalk.gray(' (红包无时间/金额列——在消息正文里,按 messageServerId 看先后;状态枚举原样,语义未验证)'));
|
|
2212
|
+
for (const r of payload.rows.slice(0, 40)) {
|
|
2213
|
+
const when = r.time ? new Date(r.time * 1000).toLocaleString('zh-CN', { hour12: false }) : '(无时间列)';
|
|
2214
|
+
const extra = r.kind === '红包'
|
|
2215
|
+
? `status=${r.hbStatus}/${r.receiveStatus}`
|
|
2216
|
+
: `对方=${r.counterpartyDisplay || r.counterparty || '?'}${r.invalidTime ? ` invalid=${r.invalidTime}` : ''}`;
|
|
2217
|
+
console.log(` [${when}] ${r.kind} · ${r.sessionDisplay} · ${r.direction} · ${extra}`);
|
|
2218
|
+
}
|
|
2219
|
+
if (payload.rows.length > 40)
|
|
2220
|
+
console.log(chalk.gray(` …共 ${payload.rows.length} 行(--json 看全表)`));
|
|
2221
|
+
console.log('');
|
|
2222
|
+
}
|
|
2223
|
+
catch (e) {
|
|
2224
|
+
if (opts.json) {
|
|
2225
|
+
console.log(JSON.stringify({ success: false, code: 'LEDGER_FAILED', error: safeSubprocessError(e) }));
|
|
2226
|
+
process.exit(1);
|
|
2227
|
+
}
|
|
2228
|
+
console.error(chalk.red(`✗ ${safeSubprocessError(e)}`));
|
|
2229
|
+
process.exit(1);
|
|
2230
|
+
}
|
|
2231
|
+
});
|
|
2232
|
+
// ==================== avatar(联系人头像,读 head_image.db) ====================
|
|
2233
|
+
// 2026-10-26 NT 数据库普查的直接产物(docs/NT_DATABASES.md):头像本体内嵌在
|
|
2234
|
+
// head_image.db 里(JPEG/PNG 完整字节,按 username 键控,一人多行=变更历史),
|
|
2235
|
+
// 比文件缓存可靠(清理不丢)。本地只读、不联网。
|
|
2236
|
+
program
|
|
2237
|
+
.command('avatar [contact]')
|
|
2238
|
+
.description('导出联系人头像(读 head_image.db;--list 看谁有头像,--all 批量导出最新头像)')
|
|
2239
|
+
.option('--list', '只列出有头像的 username 与头像数')
|
|
2240
|
+
.option('--all', '导出所有 username 的最新头像到 --out 目录')
|
|
2241
|
+
.option('--out <path>', '输出文件(单联系人)或目录(--all),默认 output/avatars/')
|
|
2242
|
+
.option('--json', '输出 JSON 格式')
|
|
2243
|
+
.action(async (contact, opts) => {
|
|
2244
|
+
if (!contact && !opts.list && !opts.all) {
|
|
2245
|
+
if (opts.json) {
|
|
2246
|
+
console.log(JSON.stringify({ success: false, code: 'INVALID_ARGUMENT', error: '要给联系人,或用 --list / --all' }));
|
|
2247
|
+
process.exit(1);
|
|
2248
|
+
}
|
|
2249
|
+
console.error(chalk.red('✗ 要给联系人,或用 --list / --all'));
|
|
2250
|
+
process.exit(1);
|
|
2251
|
+
}
|
|
2252
|
+
if (contact && (opts.list || opts.all)) {
|
|
2253
|
+
if (opts.json) {
|
|
2254
|
+
console.log(JSON.stringify({ success: false, code: 'INVALID_ARGUMENT', error: '联系人与 --list/--all 互斥' }));
|
|
2255
|
+
process.exit(1);
|
|
2256
|
+
}
|
|
2257
|
+
console.error(chalk.red('✗ 联系人与 --list/--all 互斥'));
|
|
2258
|
+
process.exit(1);
|
|
2259
|
+
}
|
|
2260
|
+
const { execFile } = await import('child_process');
|
|
2261
|
+
const { promisify } = await import('util');
|
|
2262
|
+
const execFileAsync = promisify(execFile);
|
|
2263
|
+
const script = join(resolvePackageRoot(), 'scripts', 'wechat_avatar.py');
|
|
2264
|
+
const args = [script, ...(opts.list ? ['--list'] : opts.all ? ['--all'] : []), '--json'];
|
|
2265
|
+
// D-020:联系人标识走环境变量,不进 argv;输出路径同理(脚本 --out 缺省读它)
|
|
2266
|
+
const env = {
|
|
2267
|
+
...(contact ? { WEFLOW_TALKER: String(contact) } : {}),
|
|
2268
|
+
...(opts.out ? { WEFLOW_AVATAR_OUT: String(opts.out) } : {}),
|
|
2269
|
+
};
|
|
2270
|
+
try {
|
|
2271
|
+
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
2272
|
+
timeout: 120_000, maxBuffer: 10 * 1024 * 1024,
|
|
2273
|
+
env: createPythonProcessEnv(env),
|
|
2274
|
+
});
|
|
2275
|
+
const payload = JSON.parse(stdout.slice(stdout.indexOf('{')));
|
|
2276
|
+
if (opts.json) {
|
|
2277
|
+
console.log(JSON.stringify(payload));
|
|
2278
|
+
if (!payload.success)
|
|
2279
|
+
process.exit(1);
|
|
2280
|
+
return;
|
|
2281
|
+
}
|
|
2282
|
+
if (!payload.success) {
|
|
2283
|
+
console.error(chalk.red(`✗ ${payload.error}`));
|
|
2284
|
+
process.exit(1);
|
|
2285
|
+
}
|
|
2286
|
+
if (payload.action === 'avatar.export') {
|
|
2287
|
+
console.log(chalk.green(`✓ 已导出「${payload.username}」的最新头像(${payload.bytes} 字节,历史 ${payload.historyCount} 张)→ ${payload.out}`));
|
|
2288
|
+
}
|
|
2289
|
+
else if (payload.action === 'avatar.export-all') {
|
|
2290
|
+
console.log(chalk.green(`✓ 已导出 ${payload.written} 个最新头像 → ${payload.dir}`));
|
|
2291
|
+
}
|
|
2292
|
+
else {
|
|
2293
|
+
console.log(payload.error || `共 ${payload.users?.length ?? '?'} 个 username 有头像(用 --json 看全表)`);
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
catch (e) {
|
|
2297
|
+
if (opts.json) {
|
|
2298
|
+
console.log(JSON.stringify({ success: false, code: 'AVATAR_FAILED', error: safeSubprocessError(e) }));
|
|
2299
|
+
process.exit(1);
|
|
2300
|
+
}
|
|
2301
|
+
console.error(chalk.red(`✗ ${safeSubprocessError(e)}`));
|
|
2302
|
+
process.exit(1);
|
|
2303
|
+
}
|
|
2304
|
+
});
|
|
2163
2305
|
// ==================== contacts ====================
|
|
2164
2306
|
// contact-schema:读 contact.db 里**群**的 ext_buffer(本地只读:不联网、不调模型)
|
|
2165
2307
|
//
|
|
@@ -8226,6 +8368,49 @@ program
|
|
|
8226
8368
|
process.exit(1);
|
|
8227
8369
|
}
|
|
8228
8370
|
});
|
|
8371
|
+
// ==================== media(媒体精确匹配)====================
|
|
8372
|
+
program
|
|
8373
|
+
.command('media')
|
|
8374
|
+
.description('媒体精确匹配:用 hardlink.db 把 md5 直接对到本地文件(图片/文件/视频;本地只读)')
|
|
8375
|
+
.option('--md5 <prefix>', '按 md5 前缀查')
|
|
8376
|
+
.option('--type <kind>', 'image | file | video | all', 'all')
|
|
8377
|
+
.option('--limit <n>', '最多列几条', '20')
|
|
8378
|
+
.option('--missing-only', '只列文件已不在本地的(微信会清理)')
|
|
8379
|
+
.option('--json', '输出 JSON 格式')
|
|
8380
|
+
.action(async (opts) => {
|
|
8381
|
+
const { execFile } = await import('child_process');
|
|
8382
|
+
const { promisify } = await import('util');
|
|
8383
|
+
const execFileAsync = promisify(execFile);
|
|
8384
|
+
const script = join(resolvePackageRoot(), 'scripts', 'media_index.py');
|
|
8385
|
+
const limit = parseCliInteger(opts.limit, 'limit', 1, 5000, opts.json);
|
|
8386
|
+
try {
|
|
8387
|
+
const args = [script, '--type', String(opts.type || 'all'), '--limit', String(limit)];
|
|
8388
|
+
if (opts.md5)
|
|
8389
|
+
args.push('--md5', String(opts.md5));
|
|
8390
|
+
if (opts.missingOnly)
|
|
8391
|
+
args.push('--missing-only');
|
|
8392
|
+
if (opts.json)
|
|
8393
|
+
args.push('--json');
|
|
8394
|
+
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
8395
|
+
timeout: 300_000,
|
|
8396
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
8397
|
+
env: pythonProcessEnv(),
|
|
8398
|
+
});
|
|
8399
|
+
console.log(stdout);
|
|
8400
|
+
}
|
|
8401
|
+
catch (e) {
|
|
8402
|
+
if (opts.json) {
|
|
8403
|
+
console.log(JSON.stringify({
|
|
8404
|
+
success: false,
|
|
8405
|
+
code: 'MEDIA_FAILED',
|
|
8406
|
+
error: '媒体索引不可用,请运行 check --json 检查配置和依赖',
|
|
8407
|
+
}));
|
|
8408
|
+
process.exit(1);
|
|
8409
|
+
}
|
|
8410
|
+
console.error(chalk.red(`\n${safeSubprocessError(e, '查询失败')}`));
|
|
8411
|
+
process.exit(1);
|
|
8412
|
+
}
|
|
8413
|
+
});
|
|
8229
8414
|
// ==================== replay(关系时间重放)====================
|
|
8230
8415
|
program
|
|
8231
8416
|
.command('replay')
|
|
@@ -8598,6 +8783,12 @@ program
|
|
|
8598
8783
|
// 只报条数与最近时间(不读崩溃内容)
|
|
8599
8784
|
crashRecords: client.crashRecords,
|
|
8600
8785
|
caveats: client.caveats,
|
|
8786
|
+
// D-130:本机实际加载的版本 vs 本包验证过的版本(判定只有这一份实现)
|
|
8787
|
+
compatibility: {
|
|
8788
|
+
verifiedVersion: WECHAT_VERIFIED.version,
|
|
8789
|
+
verifiedAt: WECHAT_VERIFIED.verifiedAt,
|
|
8790
|
+
verdict: compareWechatVersions(client.version, WECHAT_VERIFIED.version),
|
|
8791
|
+
},
|
|
8601
8792
|
},
|
|
8602
8793
|
};
|
|
8603
8794
|
return data;
|
|
@@ -8635,6 +8826,7 @@ program
|
|
|
8635
8826
|
dataDirMatchesConfig: data.wechatClient.dataDirMatchesConfig,
|
|
8636
8827
|
dataDirs: data.wechatClient.dataDirs,
|
|
8637
8828
|
unusableVersionDirs: data.wechatClient.unusableVersionDirs,
|
|
8829
|
+
compatibility: data.wechatClient.compatibility,
|
|
8638
8830
|
},
|
|
8639
8831
|
}));
|
|
8640
8832
|
return;
|
|
@@ -8692,6 +8884,13 @@ program
|
|
|
8692
8884
|
}
|
|
8693
8885
|
}
|
|
8694
8886
|
}
|
|
8887
|
+
// 3.5 微信版本兼容(D-130):本机加载的 vs 本包验证过的
|
|
8888
|
+
{
|
|
8889
|
+
const verdict = compareWechatVersions(client.version, WECHAT_VERIFIED.version);
|
|
8890
|
+
const mark = verdict === 'verified' ? chalk.green('✓') : verdict === 'newer-than-verified' ? chalk.yellow('⚠') : chalk.gray('·');
|
|
8891
|
+
console.log(chalk.white('\n微信版本兼容:'));
|
|
8892
|
+
console.log(` ${mark} ${client.version ?? '未知(微信没在运行)'} · ${describeCompatVerdict(verdict, WECHAT_VERIFIED.version)}`);
|
|
8893
|
+
}
|
|
8695
8894
|
// 4. Config
|
|
8696
8895
|
console.log(chalk.white('\n配置状态:'));
|
|
8697
8896
|
const configured = configService.isConfigured();
|