weflow-cli 1.17.0 → 1.19.1
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 +46 -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 +3 -2
- 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_mail.py +51 -5
- package/scripts/daily_task.ps1 +216 -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/key_cache.py +232 -0
- 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 +2023 -1954
- 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
|
@@ -1,222 +1,222 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 稳定增量游标的"路径可见"层(D-027)。
|
|
3
|
-
*
|
|
4
|
-
* `capabilities --json` 的 `stableCursor: false` 一直是一句结论;这里把它背后的
|
|
5
|
-
* 判据拆开,逐条给出当前状态与差在哪,让下游(she-love-me、MCP 客户端)能看见
|
|
6
|
-
* false → true 要满足什么,而不是等它某天悄悄变 true。
|
|
7
|
-
*
|
|
8
|
-
* 两类事实,来源不同、都必须如实标注:
|
|
9
|
-
* - **代码事实**(静态):每个后端有没有时间窗下推、有没有覆盖报告、合并顺序键是什么。
|
|
10
|
-
* 这些写在下面的表里,改读取器时要同步改。
|
|
11
|
-
* - **数据事实**(实测):NT 库里每张消息表有没有锚点列、采样的行带不带非零
|
|
12
|
-
* `server_id`。这些只有开库才知道,由 `sync cursor-probe` 现场 probing,采样
|
|
13
|
-
* 有界且结果里写明 `exhaustive: false`。
|
|
14
|
-
*/
|
|
15
|
-
import { existsSync } from 'fs'
|
|
16
|
-
import { join } from 'path'
|
|
17
|
-
import { configService } from './configService.js'
|
|
18
|
-
import { NtCore } from '../core/ntCore.js'
|
|
19
|
-
import type { NtCursorProbeResult, NtCursorProbeShard, NtCursorProbeSample } from '../core/ntCore.js'
|
|
20
|
-
|
|
21
|
-
export type MessageBackend = 'nt' | 'raw-sqlite-4x' | 'wcdb-4x' | 'legacy-3x'
|
|
22
|
-
|
|
23
|
-
export interface CursorCriterion {
|
|
24
|
-
id: string
|
|
25
|
-
/** 这条判据满足没有:true/false 是已核实的结论,null 是"当前无法判定" */
|
|
26
|
-
met: boolean | null
|
|
27
|
-
detail: string
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface CursorProbeReport {
|
|
31
|
-
success: boolean
|
|
32
|
-
schema: 'weflow-cursor-probe/v1'
|
|
33
|
-
/** 结论不动:探针是让路径可见,不是替 D-027 翻案 */
|
|
34
|
-
stableCursor: false
|
|
35
|
-
backend: MessageBackend | null
|
|
36
|
-
backendNote: string
|
|
37
|
-
/** 数据事实那一半跑了没有(未初始化 / 非 NT 后端 = false) */
|
|
38
|
-
probedLive: boolean
|
|
39
|
-
criteria: CursorCriterion[]
|
|
40
|
-
/** NT 实测明细;probedLive=false 时为 undefined */
|
|
41
|
-
shards?: NtCursorProbeShard[]
|
|
42
|
-
sample?: NtCursorProbeSample
|
|
43
|
-
error?: string
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* 静态判断"这台机器会走哪条消息后端"。**镜像 `chatService.connect4x` 的尝试顺序**
|
|
48
|
-
* (先已解密 MSG0,再 NT,最后 WCDB),但只看配置与文件存在性、不开库——真正的
|
|
49
|
-
* 开库判定在 connect 里,而探测命令不该为了报一个名字去连数据库。
|
|
50
|
-
*/
|
|
51
|
-
export function likelyMessageBackend(): { backend: MessageBackend | null; note: string } {
|
|
52
|
-
const version = configService.get('dataVersion')
|
|
53
|
-
const dbPath = configService.get('dbPath')
|
|
54
|
-
const wxid = configService.get('wxid')
|
|
55
|
-
const dbPath3x = configService.get('dbPath3x')
|
|
56
|
-
const key3x = configService.get('decryptKey3x')
|
|
57
|
-
const ntDbPath = configService.get('ntDbPath')
|
|
58
|
-
const ntKey = configService.get('ntKey')
|
|
59
|
-
const ntSalt = configService.get('ntSalt')
|
|
60
|
-
|
|
61
|
-
if (version === '3.x' || (!version && dbPath3x && key3x)) {
|
|
62
|
-
return { backend: 'legacy-3x', note: '按 3.x 配置判定(sqlcipher + MSG 库)' }
|
|
63
|
-
}
|
|
64
|
-
if (version !== '4.x' && !dbPath && !(ntDbPath && ntKey && ntSalt)) {
|
|
65
|
-
return { backend: null, note: '未初始化:没有可判定的消息后端(先运行 weflow-cli init)' }
|
|
66
|
-
}
|
|
67
|
-
if (dbPath && wxid && existsSync(join(dbPath, wxid, 'Msg', 'Multi', 'MSG0_decrypted.db'))) {
|
|
68
|
-
return { backend: 'raw-sqlite-4x', note: '发现已解密的 MSG0_decrypted.db(connect4x 的第一优先)' }
|
|
69
|
-
}
|
|
70
|
-
if (ntDbPath && ntKey && ntSalt && existsSync(ntDbPath)) {
|
|
71
|
-
return { backend: 'nt', note: 'NT 配置完整(db_storage/message 分片)' }
|
|
72
|
-
}
|
|
73
|
-
if (dbPath) {
|
|
74
|
-
return { backend: 'wcdb-4x', note: '无解密 MSG0、无 NT 配置,connect4x 的最后兜底是 WCDB API' }
|
|
75
|
-
}
|
|
76
|
-
return { backend: null, note: '4.x 配置不完整:既无 dbPath 也无 NT 三件套' }
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** 各后端的**代码事实**。改读取器(下推 / 报告 / 顺序键)时同步改这张表。 */
|
|
80
|
-
export const BACKEND_CAPS: Record<MessageBackend, {
|
|
81
|
-
identity: string
|
|
82
|
-
windowPushdown: boolean
|
|
83
|
-
coverageReport: boolean
|
|
84
|
-
liveProbe: boolean
|
|
85
|
-
}> = {
|
|
86
|
-
'nt': {
|
|
87
|
-
identity: 'server_id(缺失时退化为 localId+createTime,后者不跨分片)',
|
|
88
|
-
windowPushdown: true, // D-030: --from/--to 推进 SQL
|
|
89
|
-
coverageReport: true, // 分片报告(connect_message_shards_detailed)
|
|
90
|
-
liveProbe: true,
|
|
91
|
-
},
|
|
92
|
-
'raw-sqlite-4x': {
|
|
93
|
-
identity: 'MsgSvrID 列被读取,但覆盖率从未实测',
|
|
94
|
-
windowPushdown: false,
|
|
95
|
-
coverageReport: false,
|
|
96
|
-
liveProbe: false,
|
|
97
|
-
},
|
|
98
|
-
'wcdb-4x': {
|
|
99
|
-
identity: 'server_id 随原生 API 返回,未实测;另有按时间的原生游标未接入 chatService',
|
|
100
|
-
windowPushdown: false,
|
|
101
|
-
coverageReport: false,
|
|
102
|
-
liveProbe: false,
|
|
103
|
-
},
|
|
104
|
-
'legacy-3x': {
|
|
105
|
-
identity: 'MsgSvrID 列被读取,但覆盖率从未实测',
|
|
106
|
-
windowPushdown: false,
|
|
107
|
-
coverageReport: false,
|
|
108
|
-
liveProbe: false,
|
|
109
|
-
},
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export async function probeCursorReadiness(opts: {
|
|
113
|
-
tablesPerShard?: number
|
|
114
|
-
rowsPerTable?: number
|
|
115
|
-
} = {}): Promise<CursorProbeReport> {
|
|
116
|
-
const { backend, note } = likelyMessageBackend()
|
|
117
|
-
|
|
118
|
-
const criteria: CursorCriterion[] = []
|
|
119
|
-
let live: NtCursorProbeResult | null = null
|
|
120
|
-
let shards: NtCursorProbeShard[] | undefined
|
|
121
|
-
let sample: NtCursorProbeSample | undefined
|
|
122
|
-
|
|
123
|
-
if (backend === 'nt') {
|
|
124
|
-
const ntDbPath = configService.get('ntDbPath')!
|
|
125
|
-
const ntKey = configService.get('ntKey')!
|
|
126
|
-
const ntSalt = configService.get('ntSalt')!
|
|
127
|
-
live = await new NtCore(ntDbPath, ntKey, ntSalt).cursorProbe(opts)
|
|
128
|
-
if (live.success) {
|
|
129
|
-
shards = live.shards
|
|
130
|
-
sample = live.sample
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const caps = backend ? BACKEND_CAPS[backend] : null
|
|
135
|
-
|
|
136
|
-
// 1. stable-identity
|
|
137
|
-
if (backend === null) {
|
|
138
|
-
criteria.push({
|
|
139
|
-
id: 'stable-identity',
|
|
140
|
-
met: null,
|
|
141
|
-
detail: '没有可判定的后端',
|
|
142
|
-
})
|
|
143
|
-
} else if (backend === 'nt') {
|
|
144
|
-
if (!live?.success || !sample) {
|
|
145
|
-
criteria.push({
|
|
146
|
-
id: 'stable-identity',
|
|
147
|
-
met: null,
|
|
148
|
-
detail: `NT 实测没有跑成:${live?.error ?? '无结果'}`,
|
|
149
|
-
})
|
|
150
|
-
} else {
|
|
151
|
-
const failed = (live.shards ?? []).filter(s => !s.opened)
|
|
152
|
-
const anchorBad = live.schemaAnchorMissingTables ?? 0
|
|
153
|
-
const parts: string[] = []
|
|
154
|
-
if (failed.length) parts.push(`${failed.length} 个分片没打开`)
|
|
155
|
-
if (anchorBad > 0) parts.push(`${anchorBad} 组消息表缺锚点列`)
|
|
156
|
-
if (sample.serverIdMissing > 0) parts.push(`采样 ${sample.rowsSampled} 行里 ${sample.serverIdMissing} 行没有非零 server_id(这些行的身份退化为按分片的 localId+createTime)`)
|
|
157
|
-
if (sample.duplicateServerIds > 0) parts.push(`采样里 ${sample.duplicateServerIds} 个 server_id 出现在多个位置`)
|
|
158
|
-
criteria.push({
|
|
159
|
-
id: 'stable-identity',
|
|
160
|
-
met: parts.length === 0,
|
|
161
|
-
detail: parts.length
|
|
162
|
-
? parts.join(';')
|
|
163
|
-
: `采样 ${sample.rowsSampled} 行(${sample.tablesSampled}/${sample.tablesTotal} 张表)全部带非零 server_id 且无重复;schema 全量检查 ${anchorBad} 组缺列。注意 exhaustive=false,这是采样结论`,
|
|
164
|
-
})
|
|
165
|
-
}
|
|
166
|
-
} else {
|
|
167
|
-
criteria.push({
|
|
168
|
-
id: 'stable-identity',
|
|
169
|
-
met: null,
|
|
170
|
-
detail: `${backend}:${caps!.identity};该后端没有实测探针`,
|
|
171
|
-
})
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// 2. globally-comparable-order(代码事实,对所有后端同为 false)
|
|
175
|
-
criteria.push({
|
|
176
|
-
id: 'globally-comparable-order',
|
|
177
|
-
met: false,
|
|
178
|
-
detail: '读取器跨分片合并的顺序键是 (createTime, localId),localId 是分片内编号、不可跨分片比较;换成 (createTime, serverId) 是稳定游标在读取器侧的前提',
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
// 3. window-pushdown
|
|
182
|
-
criteria.push({
|
|
183
|
-
id: 'window-pushdown',
|
|
184
|
-
met: caps ? caps.windowPushdown : null,
|
|
185
|
-
detail: caps
|
|
186
|
-
? (caps.windowPushdown
|
|
187
|
-
? `${backend}:--from/--to 已下推进 SQL(D-030)`
|
|
188
|
-
: `${backend}:没有时间窗下推,窗口过滤全靠调用方拉全量后自筛`)
|
|
189
|
-
: '没有可判定的后端',
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
// 4. coverage-report
|
|
193
|
-
criteria.push({
|
|
194
|
-
id: 'coverage-report',
|
|
195
|
-
met: caps ? caps.coverageReport : null,
|
|
196
|
-
detail: caps
|
|
197
|
-
? (caps.coverageReport
|
|
198
|
-
? `${backend}:每分片读取结果都带覆盖报告(complete/unverified/partial)`
|
|
199
|
-
: `${backend}:无法报告读取是否完整,sync 在它上面永远是 unverified`)
|
|
200
|
-
: '没有可判定的后端',
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
// 5. every-backend(D-027 的总门)
|
|
204
|
-
const others = (Object.keys(BACKEND_CAPS) as MessageBackend[]).filter(b => b !== backend)
|
|
205
|
-
criteria.push({
|
|
206
|
-
id: 'every-backend',
|
|
207
|
-
met: false,
|
|
208
|
-
detail: `D-027:稳定游标要所有消息后端同时满足上述判据。当前后端 ${backend ?? '(未配置)'};其余后端(${others.join('、')})各自缺下推与覆盖报告`,
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
return {
|
|
212
|
-
success: true,
|
|
213
|
-
schema: 'weflow-cursor-probe/v1',
|
|
214
|
-
stableCursor: false,
|
|
215
|
-
backend,
|
|
216
|
-
backendNote: note,
|
|
217
|
-
probedLive: !!(backend === 'nt' && live?.success),
|
|
218
|
-
criteria,
|
|
219
|
-
...(shards ? { shards } : {}),
|
|
220
|
-
...(sample ? { sample } : {}),
|
|
221
|
-
}
|
|
222
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 稳定增量游标的"路径可见"层(D-027)。
|
|
3
|
+
*
|
|
4
|
+
* `capabilities --json` 的 `stableCursor: false` 一直是一句结论;这里把它背后的
|
|
5
|
+
* 判据拆开,逐条给出当前状态与差在哪,让下游(she-love-me、MCP 客户端)能看见
|
|
6
|
+
* false → true 要满足什么,而不是等它某天悄悄变 true。
|
|
7
|
+
*
|
|
8
|
+
* 两类事实,来源不同、都必须如实标注:
|
|
9
|
+
* - **代码事实**(静态):每个后端有没有时间窗下推、有没有覆盖报告、合并顺序键是什么。
|
|
10
|
+
* 这些写在下面的表里,改读取器时要同步改。
|
|
11
|
+
* - **数据事实**(实测):NT 库里每张消息表有没有锚点列、采样的行带不带非零
|
|
12
|
+
* `server_id`。这些只有开库才知道,由 `sync cursor-probe` 现场 probing,采样
|
|
13
|
+
* 有界且结果里写明 `exhaustive: false`。
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync } from 'fs'
|
|
16
|
+
import { join } from 'path'
|
|
17
|
+
import { configService } from './configService.js'
|
|
18
|
+
import { NtCore } from '../core/ntCore.js'
|
|
19
|
+
import type { NtCursorProbeResult, NtCursorProbeShard, NtCursorProbeSample } from '../core/ntCore.js'
|
|
20
|
+
|
|
21
|
+
export type MessageBackend = 'nt' | 'raw-sqlite-4x' | 'wcdb-4x' | 'legacy-3x'
|
|
22
|
+
|
|
23
|
+
export interface CursorCriterion {
|
|
24
|
+
id: string
|
|
25
|
+
/** 这条判据满足没有:true/false 是已核实的结论,null 是"当前无法判定" */
|
|
26
|
+
met: boolean | null
|
|
27
|
+
detail: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CursorProbeReport {
|
|
31
|
+
success: boolean
|
|
32
|
+
schema: 'weflow-cursor-probe/v1'
|
|
33
|
+
/** 结论不动:探针是让路径可见,不是替 D-027 翻案 */
|
|
34
|
+
stableCursor: false
|
|
35
|
+
backend: MessageBackend | null
|
|
36
|
+
backendNote: string
|
|
37
|
+
/** 数据事实那一半跑了没有(未初始化 / 非 NT 后端 = false) */
|
|
38
|
+
probedLive: boolean
|
|
39
|
+
criteria: CursorCriterion[]
|
|
40
|
+
/** NT 实测明细;probedLive=false 时为 undefined */
|
|
41
|
+
shards?: NtCursorProbeShard[]
|
|
42
|
+
sample?: NtCursorProbeSample
|
|
43
|
+
error?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 静态判断"这台机器会走哪条消息后端"。**镜像 `chatService.connect4x` 的尝试顺序**
|
|
48
|
+
* (先已解密 MSG0,再 NT,最后 WCDB),但只看配置与文件存在性、不开库——真正的
|
|
49
|
+
* 开库判定在 connect 里,而探测命令不该为了报一个名字去连数据库。
|
|
50
|
+
*/
|
|
51
|
+
export function likelyMessageBackend(): { backend: MessageBackend | null; note: string } {
|
|
52
|
+
const version = configService.get('dataVersion')
|
|
53
|
+
const dbPath = configService.get('dbPath')
|
|
54
|
+
const wxid = configService.get('wxid')
|
|
55
|
+
const dbPath3x = configService.get('dbPath3x')
|
|
56
|
+
const key3x = configService.get('decryptKey3x')
|
|
57
|
+
const ntDbPath = configService.get('ntDbPath')
|
|
58
|
+
const ntKey = configService.get('ntKey')
|
|
59
|
+
const ntSalt = configService.get('ntSalt')
|
|
60
|
+
|
|
61
|
+
if (version === '3.x' || (!version && dbPath3x && key3x)) {
|
|
62
|
+
return { backend: 'legacy-3x', note: '按 3.x 配置判定(sqlcipher + MSG 库)' }
|
|
63
|
+
}
|
|
64
|
+
if (version !== '4.x' && !dbPath && !(ntDbPath && ntKey && ntSalt)) {
|
|
65
|
+
return { backend: null, note: '未初始化:没有可判定的消息后端(先运行 weflow-cli init)' }
|
|
66
|
+
}
|
|
67
|
+
if (dbPath && wxid && existsSync(join(dbPath, wxid, 'Msg', 'Multi', 'MSG0_decrypted.db'))) {
|
|
68
|
+
return { backend: 'raw-sqlite-4x', note: '发现已解密的 MSG0_decrypted.db(connect4x 的第一优先)' }
|
|
69
|
+
}
|
|
70
|
+
if (ntDbPath && ntKey && ntSalt && existsSync(ntDbPath)) {
|
|
71
|
+
return { backend: 'nt', note: 'NT 配置完整(db_storage/message 分片)' }
|
|
72
|
+
}
|
|
73
|
+
if (dbPath) {
|
|
74
|
+
return { backend: 'wcdb-4x', note: '无解密 MSG0、无 NT 配置,connect4x 的最后兜底是 WCDB API' }
|
|
75
|
+
}
|
|
76
|
+
return { backend: null, note: '4.x 配置不完整:既无 dbPath 也无 NT 三件套' }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 各后端的**代码事实**。改读取器(下推 / 报告 / 顺序键)时同步改这张表。 */
|
|
80
|
+
export const BACKEND_CAPS: Record<MessageBackend, {
|
|
81
|
+
identity: string
|
|
82
|
+
windowPushdown: boolean
|
|
83
|
+
coverageReport: boolean
|
|
84
|
+
liveProbe: boolean
|
|
85
|
+
}> = {
|
|
86
|
+
'nt': {
|
|
87
|
+
identity: 'server_id(缺失时退化为 localId+createTime,后者不跨分片)',
|
|
88
|
+
windowPushdown: true, // D-030: --from/--to 推进 SQL
|
|
89
|
+
coverageReport: true, // 分片报告(connect_message_shards_detailed)
|
|
90
|
+
liveProbe: true,
|
|
91
|
+
},
|
|
92
|
+
'raw-sqlite-4x': {
|
|
93
|
+
identity: 'MsgSvrID 列被读取,但覆盖率从未实测',
|
|
94
|
+
windowPushdown: false,
|
|
95
|
+
coverageReport: false,
|
|
96
|
+
liveProbe: false,
|
|
97
|
+
},
|
|
98
|
+
'wcdb-4x': {
|
|
99
|
+
identity: 'server_id 随原生 API 返回,未实测;另有按时间的原生游标未接入 chatService',
|
|
100
|
+
windowPushdown: false,
|
|
101
|
+
coverageReport: false,
|
|
102
|
+
liveProbe: false,
|
|
103
|
+
},
|
|
104
|
+
'legacy-3x': {
|
|
105
|
+
identity: 'MsgSvrID 列被读取,但覆盖率从未实测',
|
|
106
|
+
windowPushdown: false,
|
|
107
|
+
coverageReport: false,
|
|
108
|
+
liveProbe: false,
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function probeCursorReadiness(opts: {
|
|
113
|
+
tablesPerShard?: number
|
|
114
|
+
rowsPerTable?: number
|
|
115
|
+
} = {}): Promise<CursorProbeReport> {
|
|
116
|
+
const { backend, note } = likelyMessageBackend()
|
|
117
|
+
|
|
118
|
+
const criteria: CursorCriterion[] = []
|
|
119
|
+
let live: NtCursorProbeResult | null = null
|
|
120
|
+
let shards: NtCursorProbeShard[] | undefined
|
|
121
|
+
let sample: NtCursorProbeSample | undefined
|
|
122
|
+
|
|
123
|
+
if (backend === 'nt') {
|
|
124
|
+
const ntDbPath = configService.get('ntDbPath')!
|
|
125
|
+
const ntKey = configService.get('ntKey')!
|
|
126
|
+
const ntSalt = configService.get('ntSalt')!
|
|
127
|
+
live = await new NtCore(ntDbPath, ntKey, ntSalt).cursorProbe(opts)
|
|
128
|
+
if (live.success) {
|
|
129
|
+
shards = live.shards
|
|
130
|
+
sample = live.sample
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const caps = backend ? BACKEND_CAPS[backend] : null
|
|
135
|
+
|
|
136
|
+
// 1. stable-identity
|
|
137
|
+
if (backend === null) {
|
|
138
|
+
criteria.push({
|
|
139
|
+
id: 'stable-identity',
|
|
140
|
+
met: null,
|
|
141
|
+
detail: '没有可判定的后端',
|
|
142
|
+
})
|
|
143
|
+
} else if (backend === 'nt') {
|
|
144
|
+
if (!live?.success || !sample) {
|
|
145
|
+
criteria.push({
|
|
146
|
+
id: 'stable-identity',
|
|
147
|
+
met: null,
|
|
148
|
+
detail: `NT 实测没有跑成:${live?.error ?? '无结果'}`,
|
|
149
|
+
})
|
|
150
|
+
} else {
|
|
151
|
+
const failed = (live.shards ?? []).filter(s => !s.opened)
|
|
152
|
+
const anchorBad = live.schemaAnchorMissingTables ?? 0
|
|
153
|
+
const parts: string[] = []
|
|
154
|
+
if (failed.length) parts.push(`${failed.length} 个分片没打开`)
|
|
155
|
+
if (anchorBad > 0) parts.push(`${anchorBad} 组消息表缺锚点列`)
|
|
156
|
+
if (sample.serverIdMissing > 0) parts.push(`采样 ${sample.rowsSampled} 行里 ${sample.serverIdMissing} 行没有非零 server_id(这些行的身份退化为按分片的 localId+createTime)`)
|
|
157
|
+
if (sample.duplicateServerIds > 0) parts.push(`采样里 ${sample.duplicateServerIds} 个 server_id 出现在多个位置`)
|
|
158
|
+
criteria.push({
|
|
159
|
+
id: 'stable-identity',
|
|
160
|
+
met: parts.length === 0,
|
|
161
|
+
detail: parts.length
|
|
162
|
+
? parts.join(';')
|
|
163
|
+
: `采样 ${sample.rowsSampled} 行(${sample.tablesSampled}/${sample.tablesTotal} 张表)全部带非零 server_id 且无重复;schema 全量检查 ${anchorBad} 组缺列。注意 exhaustive=false,这是采样结论`,
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
criteria.push({
|
|
168
|
+
id: 'stable-identity',
|
|
169
|
+
met: null,
|
|
170
|
+
detail: `${backend}:${caps!.identity};该后端没有实测探针`,
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 2. globally-comparable-order(代码事实,对所有后端同为 false)
|
|
175
|
+
criteria.push({
|
|
176
|
+
id: 'globally-comparable-order',
|
|
177
|
+
met: false,
|
|
178
|
+
detail: '读取器跨分片合并的顺序键是 (createTime, localId),localId 是分片内编号、不可跨分片比较;换成 (createTime, serverId) 是稳定游标在读取器侧的前提',
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
// 3. window-pushdown
|
|
182
|
+
criteria.push({
|
|
183
|
+
id: 'window-pushdown',
|
|
184
|
+
met: caps ? caps.windowPushdown : null,
|
|
185
|
+
detail: caps
|
|
186
|
+
? (caps.windowPushdown
|
|
187
|
+
? `${backend}:--from/--to 已下推进 SQL(D-030)`
|
|
188
|
+
: `${backend}:没有时间窗下推,窗口过滤全靠调用方拉全量后自筛`)
|
|
189
|
+
: '没有可判定的后端',
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
// 4. coverage-report
|
|
193
|
+
criteria.push({
|
|
194
|
+
id: 'coverage-report',
|
|
195
|
+
met: caps ? caps.coverageReport : null,
|
|
196
|
+
detail: caps
|
|
197
|
+
? (caps.coverageReport
|
|
198
|
+
? `${backend}:每分片读取结果都带覆盖报告(complete/unverified/partial)`
|
|
199
|
+
: `${backend}:无法报告读取是否完整,sync 在它上面永远是 unverified`)
|
|
200
|
+
: '没有可判定的后端',
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
// 5. every-backend(D-027 的总门)
|
|
204
|
+
const others = (Object.keys(BACKEND_CAPS) as MessageBackend[]).filter(b => b !== backend)
|
|
205
|
+
criteria.push({
|
|
206
|
+
id: 'every-backend',
|
|
207
|
+
met: false,
|
|
208
|
+
detail: `D-027:稳定游标要所有消息后端同时满足上述判据。当前后端 ${backend ?? '(未配置)'};其余后端(${others.join('、')})各自缺下推与覆盖报告`,
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
success: true,
|
|
213
|
+
schema: 'weflow-cursor-probe/v1',
|
|
214
|
+
stableCursor: false,
|
|
215
|
+
backend,
|
|
216
|
+
backendNote: note,
|
|
217
|
+
probedLive: !!(backend === 'nt' && live?.success),
|
|
218
|
+
criteria,
|
|
219
|
+
...(shards ? { shards } : {}),
|
|
220
|
+
...(sample ? { sample } : {}),
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -49,6 +49,12 @@ export interface DiagnosticInput {
|
|
|
49
49
|
dataDirMatchesConfig?: boolean | null
|
|
50
50
|
dataDirs?: string[]
|
|
51
51
|
unusableVersionDirs?: Array<{ dir: string; files: number }>
|
|
52
|
+
/** D-130:本机加载版本 vs 本包验证版本(check 那边算好传进来) */
|
|
53
|
+
compatibility?: {
|
|
54
|
+
verifiedVersion: string
|
|
55
|
+
verifiedAt: string
|
|
56
|
+
verdict: 'verified' | 'newer-than-verified' | 'older-than-verified' | 'unknown'
|
|
57
|
+
}
|
|
52
58
|
}
|
|
53
59
|
/** 最近一次失败的原文(可选;`redactForReport` 会再过一遍) */
|
|
54
60
|
lastError?: string | null
|
|
@@ -157,6 +163,14 @@ export function diagnose(input: DiagnosticInput): DiagnosticFinding[] {
|
|
|
157
163
|
hint: '指向它的加载会失败。如果客户端起不来,先看这里;正常情况下可以忽略。',
|
|
158
164
|
})
|
|
159
165
|
}
|
|
166
|
+
if (client.compatibility?.verdict === 'newer-than-verified') {
|
|
167
|
+
out.push({
|
|
168
|
+
level: 'warning',
|
|
169
|
+
title: `本机微信(${client.version})比本版本验证过的 ${client.compatibility.verifiedVersion} 更新`,
|
|
170
|
+
hint: '如果读不出数据,先怀疑版本变化——去 GitHub 看有没有更新版本的 weflow-cli,'
|
|
171
|
+
+ '或把这份报告贴进 issue(版本差就是第一条线索)。',
|
|
172
|
+
})
|
|
173
|
+
}
|
|
160
174
|
}
|
|
161
175
|
|
|
162
176
|
if (!out.length) {
|
|
@@ -209,6 +223,14 @@ export function renderDiagnosticReport(input: DiagnosticInput): string {
|
|
|
209
223
|
} else {
|
|
210
224
|
say(`运行中: ${yesNo(client.running)}`)
|
|
211
225
|
say(`加载版本: ${client.version || `未知${client.note ? `(${client.note})` : ''}`}`)
|
|
226
|
+
if (client.compatibility) {
|
|
227
|
+
const v = client.compatibility.verdict
|
|
228
|
+
const word = v === 'verified' ? '就是验证过的版本'
|
|
229
|
+
: v === 'newer-than-verified' ? '比验证过的更新(读不出数据先怀疑这里)'
|
|
230
|
+
: v === 'older-than-verified' ? '比验证过的更旧(一般兼容)'
|
|
231
|
+
: '无法判定(微信没在运行或探测失败)'
|
|
232
|
+
say(`与本版本的关系: ${word}(本版本验证过 ${client.compatibility.verifiedVersion},${client.compatibility.verifiedAt})`)
|
|
233
|
+
}
|
|
212
234
|
if (client.dataDirs?.length) say(`客户端自报数据目录: ${client.dataDirs.join('、')}`)
|
|
213
235
|
say(`与配置推出来的同一份: ${yesNo(client.dataDirMatchesConfig)}`)
|
|
214
236
|
if (client.unusableVersionDirs?.length) {
|