weflow-cli 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -0
  3. package/bin/weflow-cli-electron.cjs +59 -0
  4. package/bin/weflow-cli.ts +2311 -0
  5. package/cli.cjs +3 -0
  6. package/dist/bin/weflow-cli.d.ts +3 -0
  7. package/dist/bin/weflow-cli.d.ts.map +1 -0
  8. package/dist/bin/weflow-cli.js +2270 -0
  9. package/dist/bin/weflow-cli.js.map +1 -0
  10. package/dist/mcp-server/index.d.ts +3 -0
  11. package/dist/mcp-server/index.d.ts.map +1 -0
  12. package/dist/mcp-server/index.js +690 -0
  13. package/dist/mcp-server/index.js.map +1 -0
  14. package/dist/src/core/dbPathService.d.ts +40 -0
  15. package/dist/src/core/dbPathService.d.ts.map +1 -0
  16. package/dist/src/core/dbPathService.js +324 -0
  17. package/dist/src/core/dbPathService.js.map +1 -0
  18. package/dist/src/core/keyService.d.ts +59 -0
  19. package/dist/src/core/keyService.d.ts.map +1 -0
  20. package/dist/src/core/keyService.js +368 -0
  21. package/dist/src/core/keyService.js.map +1 -0
  22. package/dist/src/core/ntCore.d.ts +62 -0
  23. package/dist/src/core/ntCore.d.ts.map +1 -0
  24. package/dist/src/core/ntCore.js +191 -0
  25. package/dist/src/core/ntCore.js.map +1 -0
  26. package/dist/src/core/sqlcipherCore.d.ts +119 -0
  27. package/dist/src/core/sqlcipherCore.d.ts.map +1 -0
  28. package/dist/src/core/sqlcipherCore.js +693 -0
  29. package/dist/src/core/sqlcipherCore.js.map +1 -0
  30. package/dist/src/core/wcdbCore.d.ts +738 -0
  31. package/dist/src/core/wcdbCore.d.ts.map +1 -0
  32. package/dist/src/core/wcdbCore.js +4545 -0
  33. package/dist/src/core/wcdbCore.js.map +1 -0
  34. package/dist/src/core/wechatClient.d.ts +23 -0
  35. package/dist/src/core/wechatClient.d.ts.map +1 -0
  36. package/dist/src/core/wechatClient.js +166 -0
  37. package/dist/src/core/wechatClient.js.map +1 -0
  38. package/dist/src/services/chatService.d.ts +25 -0
  39. package/dist/src/services/chatService.d.ts.map +1 -0
  40. package/dist/src/services/chatService.js +291 -0
  41. package/dist/src/services/chatService.js.map +1 -0
  42. package/dist/src/services/configService.d.ts +43 -0
  43. package/dist/src/services/configService.d.ts.map +1 -0
  44. package/dist/src/services/configService.js +156 -0
  45. package/dist/src/services/configService.js.map +1 -0
  46. package/dist/src/services/exportService.d.ts +30 -0
  47. package/dist/src/services/exportService.d.ts.map +1 -0
  48. package/dist/src/services/exportService.js +273 -0
  49. package/dist/src/services/exportService.js.map +1 -0
  50. package/dist/src/services/wechat-formatter.d.ts +69 -0
  51. package/dist/src/services/wechat-formatter.d.ts.map +1 -0
  52. package/dist/src/services/wechat-formatter.js +248 -0
  53. package/dist/src/services/wechat-formatter.js.map +1 -0
  54. package/dist/src/services/wechatMessageService.d.ts +28 -0
  55. package/dist/src/services/wechatMessageService.d.ts.map +1 -0
  56. package/dist/src/services/wechatMessageService.js +341 -0
  57. package/dist/src/services/wechatMessageService.js.map +1 -0
  58. package/dist/src/services/wereadService.d.ts +206 -0
  59. package/dist/src/services/wereadService.d.ts.map +1 -0
  60. package/dist/src/services/wereadService.js +145 -0
  61. package/dist/src/services/wereadService.js.map +1 -0
  62. package/dist/src/services/whitelistService.d.ts +20 -0
  63. package/dist/src/services/whitelistService.d.ts.map +1 -0
  64. package/dist/src/services/whitelistService.js +60 -0
  65. package/dist/src/services/whitelistService.js.map +1 -0
  66. package/dist/src/types.d.ts +140 -0
  67. package/dist/src/types.d.ts.map +1 -0
  68. package/dist/src/types.js +2 -0
  69. package/dist/src/types.js.map +1 -0
  70. package/dist/src/utils/errors.d.ts +25 -0
  71. package/dist/src/utils/errors.d.ts.map +1 -0
  72. package/dist/src/utils/errors.js +43 -0
  73. package/dist/src/utils/errors.js.map +1 -0
  74. package/dist/src/utils/pathUtils.d.ts +5 -0
  75. package/dist/src/utils/pathUtils.d.ts.map +1 -0
  76. package/dist/src/utils/pathUtils.js +16 -0
  77. package/dist/src/utils/pathUtils.js.map +1 -0
  78. package/dist/src/utils/pythonRunner.d.ts +27 -0
  79. package/dist/src/utils/pythonRunner.d.ts.map +1 -0
  80. package/dist/src/utils/pythonRunner.js +62 -0
  81. package/dist/src/utils/pythonRunner.js.map +1 -0
  82. package/dist/src/utils/talkerUtils.d.ts +14 -0
  83. package/dist/src/utils/talkerUtils.d.ts.map +1 -0
  84. package/dist/src/utils/talkerUtils.js +73 -0
  85. package/dist/src/utils/talkerUtils.js.map +1 -0
  86. package/mcp-server/index.ts +757 -0
  87. package/package.json +72 -0
  88. package/resources/key/win32/x64/wx_key.dll +0 -0
  89. package/resources/wcdb/win32/x64/SDL2.dll +0 -0
  90. package/resources/wcdb/win32/x64/WCDB.dll +0 -0
  91. package/resources/wcdb/win32/x64/msvcp140.dll +0 -0
  92. package/resources/wcdb/win32/x64/msvcp140_1.dll +0 -0
  93. package/resources/wcdb/win32/x64/vcruntime140.dll +0 -0
  94. package/resources/wcdb/win32/x64/vcruntime140_1.dll +0 -0
  95. package/resources/wcdb/win32/x64/wcdb_api.dll +0 -0
  96. package/scripts/_batch_link_sources.py +73 -0
  97. package/scripts/_utils.py +531 -0
  98. package/scripts/annual_report.py +796 -0
  99. package/scripts/auto_tag.py +128 -0
  100. package/scripts/biz_daily.py +896 -0
  101. package/scripts/chat_report.py +397 -0
  102. package/scripts/chat_stats.py +702 -0
  103. package/scripts/classify_daily.py +411 -0
  104. package/scripts/compile_wiki.py +258 -0
  105. package/scripts/create_reading_notes.py +370 -0
  106. package/scripts/enrich_backlinks.py +163 -0
  107. package/scripts/export_chat_html.py +766 -0
  108. package/scripts/extract_3x_key.py +86 -0
  109. package/scripts/extract_todos.py +404 -0
  110. package/scripts/fav_server.py +392 -0
  111. package/scripts/fix_topics.py +111 -0
  112. package/scripts/generate_ai_report.py +367 -0
  113. package/scripts/generate_html.py +1372 -0
  114. package/scripts/generate_review.py +170 -0
  115. package/scripts/mcp_bridge.py +398 -0
  116. package/scripts/nt_decrypt.py +562 -0
  117. package/scripts/pipeline.py +145 -0
  118. package/scripts/promote_all.py +310 -0
  119. package/scripts/promote_ideas.py +297 -0
  120. package/scripts/rag_chat.py +191 -0
  121. package/scripts/scan_decrypt_4x.py +329 -0
  122. package/scripts/semantic_search.py +431 -0
  123. package/scripts/sync_fav.py +146 -0
  124. package/scripts/sync_weread.py +192 -0
  125. package/scripts/vault_rag.py +139 -0
  126. package/scripts/vault_search.py +141 -0
  127. package/src/core/dbPathService.ts +348 -0
  128. package/src/core/keyService.ts +409 -0
  129. package/src/core/ntCore.ts +224 -0
  130. package/src/core/sqlcipherCore.ts +764 -0
  131. package/src/core/wcdbCore.ts +4568 -0
  132. package/src/core/wechatClient.ts +204 -0
  133. package/src/lz4.d.ts +10 -0
  134. package/src/services/chatService.ts +296 -0
  135. package/src/services/configService.ts +197 -0
  136. package/src/services/exportService.ts +305 -0
  137. package/src/services/wechat-formatter.ts +348 -0
  138. package/src/services/wechatMessageService.ts +383 -0
  139. package/src/services/wereadService.ts +308 -0
  140. package/src/services/whitelistService.ts +72 -0
  141. package/src/types.ts +141 -0
  142. package/src/utils/errors.ts +42 -0
  143. package/src/utils/pathUtils.ts +16 -0
  144. package/src/utils/pythonRunner.ts +81 -0
  145. package/src/utils/talkerUtils.ts +84 -0
@@ -0,0 +1,4568 @@
1
+ import { join, dirname, basename } from 'path'
2
+ import { appendFileSync, existsSync, mkdirSync, readdirSync, statSync, readFileSync } from 'fs'
3
+ import { tmpdir } from 'os'
4
+ import { createRequire } from 'module'
5
+ import * as fzstd from 'fzstd'
6
+ import { expandHomePath } from '../utils/pathUtils.js'
7
+
8
+ const require = createRequire(import.meta.url)
9
+
10
+ //数据服务初始化错误信息,用于帮助用户诊断问题
11
+ let lastDllInitError: string | null = null
12
+
13
+ export function getLastDllInitError(): string | null {
14
+ return lastDllInitError
15
+ }
16
+
17
+ export class WcdbCore {
18
+ private resourcesPath: string | null = null
19
+ private userDataPath: string | null = null
20
+ private logEnabled = false
21
+ private lib: any = null
22
+ private koffi: any = null
23
+ private initialized = false
24
+ private handle: number | null = null
25
+ private currentPath: string | null = null
26
+ private currentKey: string | null = null
27
+ private currentWxid: string | null = null
28
+ private currentDbStoragePath: string | null = null
29
+
30
+ // 函数引用
31
+ private wcdbInitProtection: any = null
32
+ private wcdbInit: any = null
33
+ private wcdbShutdown: any = null
34
+ private wcdbOpenAccount: any = null
35
+ private wcdbCloseAccount: any = null
36
+ private wcdbSetMyWxid: any = null
37
+ private wcdbFreeString: any = null
38
+ private wcdbUpdateMessage: any = null
39
+ private wcdbDeleteMessage: any = null
40
+ private wcdbGetSessions: any = null
41
+ private wcdbGetMessages: any = null
42
+ private wcdbGetMessageCount: any = null
43
+ private wcdbGetDisplayNames: any = null
44
+ private wcdbGetAvatarUrls: any = null
45
+ private wcdbGetGroupMemberCount: any = null
46
+ private wcdbGetGroupMemberCounts: any = null
47
+ private wcdbGetGroupMembers: any = null
48
+ private wcdbGetGroupNicknames: any = null
49
+ private wcdbGetMessageTables: any = null
50
+ private wcdbGetMessageMeta: any = null
51
+ private wcdbGetContact: any = null
52
+ private wcdbGetContactStatus: any = null
53
+ private wcdbGetContactTypeCounts: any = null
54
+ private wcdbGetContactsCompact: any = null
55
+ private wcdbGetContactAliasMap: any = null
56
+ private wcdbGetContactFriendFlags: any = null
57
+ private wcdbGetChatRoomExtBuffer: any = null
58
+ private wcdbGetMessageTableStats: any = null
59
+ private wcdbGetAggregateStats: any = null
60
+ private wcdbGetAvailableYears: any = null
61
+ private wcdbGetAnnualReportStats: any = null
62
+ private wcdbGetAnnualReportExtras: any = null
63
+ private wcdbGetDualReportStats: any = null
64
+ private wcdbGetGroupStats: any = null
65
+ private wcdbGetMyFootprintStats: any = null
66
+ private wcdbGetMessageDates: any = null
67
+ private wcdbOpenMessageCursor: any = null
68
+ private wcdbOpenMessageCursorLite: any = null
69
+ private wcdbFetchMessageBatch: any = null
70
+ private wcdbCloseMessageCursor: any = null
71
+ private wcdbGetLogs: any = null
72
+ private wcdbExecQuery: any = null
73
+ private wcdbListMessageDbs: any = null
74
+ private wcdbListMediaDbs: any = null
75
+ private wcdbGetMessageById: any = null
76
+ private wcdbGetEmoticonCdnUrl: any = null
77
+ private wcdbGetEmoticonCaption: any = null
78
+ private wcdbGetEmoticonCaptionStrict: any = null
79
+ private wcdbGetDbStatus: any = null
80
+ private wcdbGetVoiceData: any = null
81
+ private wcdbGetVoiceDataBatch: any = null
82
+ private wcdbGetMediaSchemaSummary: any = null
83
+ private wcdbGetSessionMessageCounts: any = null
84
+ private wcdbGetSessionMessageTypeStats: any = null
85
+ private wcdbGetSessionMessageTypeStatsBatch: any = null
86
+ private wcdbGetSessionMessageDateCounts: any = null
87
+ private wcdbGetSessionMessageDateCountsBatch: any = null
88
+ private wcdbGetMessagesByType: any = null
89
+ private wcdbScanMediaStream: any = null
90
+ private wcdbGetHeadImageBuffers: any = null
91
+ private wcdbSearchMessages: any = null
92
+ private wcdbGetSnsTimeline: any = null
93
+ private wcdbGetSnsAnnualStats: any = null
94
+ private wcdbGetSnsUsernames: any = null
95
+ private wcdbGetSnsExportStats: any = null
96
+ private wcdbGetMessageTableColumns: any = null
97
+ private wcdbListTables: any = null
98
+ private wcdbGetTableSchema: any = null
99
+ private wcdbExportTableSnapshot: any = null
100
+ private wcdbImportTableSnapshot: any = null
101
+ private wcdbImportTableSnapshotWithSchema: any = null
102
+ private wcdbGetMessageTableTimeRange: any = null
103
+ private wcdbResolveImageHardlink: any = null
104
+ private wcdbResolveImageHardlinkBatch: any = null
105
+ private wcdbResolveVideoHardlinkMd5: any = null
106
+ private wcdbResolveVideoHardlinkMd5Batch: any = null
107
+ private wcdbInstallMessageAntiRevokeTrigger: any = null
108
+ private wcdbUninstallMessageAntiRevokeTrigger: any = null
109
+ private wcdbCheckMessageAntiRevokeTrigger: any = null
110
+ private wcdbInstallSnsBlockDeleteTrigger: any = null
111
+ private wcdbUninstallSnsBlockDeleteTrigger: any = null
112
+ private wcdbCheckSnsBlockDeleteTrigger: any = null
113
+ private wcdbDeleteSnsPost: any = null
114
+ private wcdbVerifyUser: any = null
115
+ private wcdbStartMonitorPipe: any = null
116
+ private wcdbStopMonitorPipe: any = null
117
+ private wcdbGetMonitorPipeName: any = null
118
+ private wcdbCloudInit: any = null
119
+ private wcdbCloudReport: any = null
120
+ private wcdbCloudStop: any = null
121
+
122
+ private monitorPipeClient: any = null
123
+ private monitorCallback: ((type: string, json: string) => void) | null = null
124
+ private monitorReconnectTimer: any = null
125
+ private monitorPipePath: string = ''
126
+
127
+
128
+ private avatarUrlCache: Map<string, { url?: string; updatedAt: number }> = new Map()
129
+ private readonly avatarCacheTtlMs = 10 * 60 * 1000
130
+ private imageHardlinkCache: Map<string, { result: { success: boolean; data?: any; error?: string }; updatedAt: number }> = new Map()
131
+ private videoHardlinkCache: Map<string, { result: { success: boolean; data?: any; error?: string }; updatedAt: number }> = new Map()
132
+ private readonly hardlinkCacheTtlMs = 10 * 60 * 1000
133
+ private readonly hardlinkCacheMaxEntries = 20000
134
+ private mediaStreamSessionCache: Array<{ sessionId: string; displayName: string; sortTimestamp: number }> | null = null
135
+ private mediaStreamSessionCacheAt = 0
136
+ private readonly mediaStreamSessionCacheTtlMs = 12 * 1000
137
+ private logTimer: NodeJS.Timeout | null = null
138
+ private lastLogTail: string | null = null
139
+ private lastResolvedLogPath: string | null = null
140
+ private lastCursorForceReopenAt = 0
141
+ private readonly cursorForceReopenCooldownMs = 15000
142
+
143
+ setPaths(resourcesPath: string, userDataPath: string): void {
144
+ this.resourcesPath = resourcesPath
145
+ this.userDataPath = userDataPath
146
+ this.writeLog(`[bootstrap] setPaths resourcesPath=${resourcesPath} userDataPath=${userDataPath}`, true)
147
+ }
148
+
149
+ getLastInitError(): string | null {
150
+ return lastDllInitError
151
+ }
152
+
153
+ setLogEnabled(enabled: boolean): void {
154
+ this.logEnabled = enabled
155
+ this.writeLog(`[bootstrap] setLogEnabled=${enabled ? '1' : '0'} env.WCDB_LOG_ENABLED=${process.env.WCDB_LOG_ENABLED || ''}`, true)
156
+ if (this.isLogEnabled() && this.initialized) {
157
+ this.startLogPolling()
158
+ } else {
159
+ this.stopLogPolling()
160
+ }
161
+ }
162
+
163
+ // 使用命名管道/socket IPC (Windows: Named Pipe, macOS: Unix Socket)
164
+ startMonitor(callback: (type: string, json: string) => void): boolean {
165
+ if (!this.wcdbStartMonitorPipe) {
166
+ return false
167
+ }
168
+
169
+ this.monitorCallback = callback
170
+
171
+ try {
172
+ const result = this.wcdbStartMonitorPipe()
173
+ if (result !== 0) {
174
+ return false
175
+ }
176
+
177
+ // 从数据服务获取动态管道名(含 PID)
178
+ let pipePath = '\\\\.\\pipe\\weflow_monitor'
179
+ if (this.wcdbGetMonitorPipeName) {
180
+ try {
181
+ const namePtr = [null as any]
182
+ if (this.wcdbGetMonitorPipeName(namePtr) === 0 && namePtr[0]) {
183
+ pipePath = this.koffi.decode(namePtr[0], 'char', -1)
184
+ this.wcdbFreeString(namePtr[0])
185
+ }
186
+ } catch { }
187
+ }
188
+ this.connectMonitorPipe(pipePath)
189
+ return true
190
+ } catch (e) {
191
+ console.error('[wcdbCore] startMonitor exception:', e)
192
+ return false
193
+ }
194
+ }
195
+
196
+ // 连接命名管道,支持断开后自动重连
197
+ private connectMonitorPipe(pipePath: string) {
198
+ this.monitorPipePath = pipePath
199
+ const net = require('net')
200
+
201
+ setTimeout(() => {
202
+ if (!this.monitorCallback) return
203
+
204
+ this.monitorPipeClient = net.createConnection(this.monitorPipePath, () => { })
205
+
206
+ let buffer = ''
207
+ this.monitorPipeClient.on('data', (data: Buffer) => {
208
+ const rawChunk = data.toString('utf8')
209
+ // macOS 侧可能使用 '\0' 或无换行分隔,统一归一化并兜底拆包
210
+ const normalizedChunk = rawChunk
211
+ .replace(/\u0000/g, '\n')
212
+ .replace(/}\s*{/g, '}\n{')
213
+
214
+ buffer += normalizedChunk
215
+ const lines = buffer.split(/\r?\n/)
216
+ buffer = lines.pop() || ''
217
+ for (const line of lines) {
218
+ if (line.trim()) {
219
+ try {
220
+ const parsed = JSON.parse(line)
221
+ this.monitorCallback?.(parsed.action || 'update', line)
222
+ } catch {
223
+ this.monitorCallback?.('update', line)
224
+ }
225
+ }
226
+ }
227
+
228
+ // 兜底:如果没有分隔符但已形成完整 JSON,则直接上报
229
+ const tail = buffer.trim()
230
+ if (tail.startsWith('{') && tail.endsWith('}')) {
231
+ try {
232
+ const parsed = JSON.parse(tail)
233
+ this.monitorCallback?.(parsed.action || 'update', tail)
234
+ buffer = ''
235
+ } catch {
236
+ // 不可解析则继续等待下一块数据
237
+ }
238
+ }
239
+ })
240
+
241
+ this.monitorPipeClient.on('error', () => {
242
+ // 保持静默,与现有错误处理策略一致
243
+ })
244
+
245
+ this.monitorPipeClient.on('close', () => {
246
+ this.monitorPipeClient = null
247
+ this.scheduleReconnect()
248
+ })
249
+ }, 100)
250
+ }
251
+
252
+ // 定时重连
253
+ private scheduleReconnect() {
254
+ if (this.monitorReconnectTimer || !this.monitorCallback) return
255
+ this.monitorReconnectTimer = setTimeout(() => {
256
+ this.monitorReconnectTimer = null
257
+ if (this.monitorCallback && !this.monitorPipeClient) {
258
+ this.connectMonitorPipe(this.monitorPipePath)
259
+ }
260
+ }, 3000)
261
+ }
262
+
263
+
264
+
265
+ stopMonitor(): void {
266
+ this.monitorCallback = null
267
+ if (this.monitorReconnectTimer) {
268
+ clearTimeout(this.monitorReconnectTimer)
269
+ this.monitorReconnectTimer = null
270
+ }
271
+ if (this.monitorPipeClient) {
272
+ this.monitorPipeClient.destroy()
273
+ this.monitorPipeClient = null
274
+ }
275
+ if (this.wcdbStopMonitorPipe) {
276
+ this.wcdbStopMonitorPipe()
277
+ }
278
+ }
279
+
280
+ // 保留旧方法签名以兼容
281
+ setMonitor(callback: (type: string, json: string) => void): boolean {
282
+ return this.startMonitor(callback)
283
+ }
284
+
285
+
286
+
287
+ /**
288
+ * 获取库文件路径(跨平台)
289
+ */
290
+ private getDllPath(): string {
291
+ const isMac = process.platform === 'darwin'
292
+ const isLinux = process.platform === 'linux'
293
+ const isArm64 = process.arch === 'arm64'
294
+ const libName = isMac ? 'libwcdb_api.dylib' : isLinux ? 'libwcdb_api.so' : 'wcdb_api.dll'
295
+ const legacySubDir = isMac ? 'macos' : isLinux ? 'linux' : (isArm64 ? 'arm64' : '')
296
+ const platformDir = isMac ? 'macos' : (isLinux ? 'linux' : 'win32')
297
+ const archDir = isMac ? 'universal' : (isArm64 ? 'arm64' : 'x64')
298
+
299
+ const envDllPath = process.env.WCDB_DLL_PATH
300
+ if (envDllPath && envDllPath.length > 0) {
301
+ return envDllPath
302
+ }
303
+
304
+ // 基础路径探测
305
+ const isPackaged = typeof (process as any)['resourcesPath'] !== 'undefined'
306
+ const resourcesPath = isPackaged ? (process as any).resourcesPath : join(process.cwd(), 'resources')
307
+ const roots = [
308
+ process.env.WCDB_RESOURCES_PATH || null,
309
+ this.resourcesPath || null,
310
+ join(resourcesPath, 'resources'),
311
+ resourcesPath,
312
+ join(process.cwd(), 'resources')
313
+ ].filter(Boolean) as string[]
314
+
315
+ const normalizedArch = process.arch === 'arm64' ? 'arm64' : 'x64'
316
+ const relativeCandidates = [
317
+ join('wcdb', platformDir, archDir, libName),
318
+ join('wcdb', platformDir, normalizedArch, libName),
319
+ join('wcdb', platformDir, 'x64', libName),
320
+ join('wcdb', platformDir, 'universal', libName),
321
+ join('wcdb', platformDir, libName)
322
+ ]
323
+
324
+ const candidates: string[] = []
325
+ for (const root of roots) {
326
+ for (const relativePath of relativeCandidates) {
327
+ candidates.push(join(root, relativePath))
328
+ }
329
+ // 兼容旧目录:resources/macos/libwcdb_api.dylib 或 resources/wcdb_api.dll
330
+ candidates.push(join(root, legacySubDir, libName))
331
+ candidates.push(join(root, libName))
332
+ }
333
+
334
+ for (const path of candidates) {
335
+ if (existsSync(path)) return path
336
+ }
337
+
338
+ return candidates[0] || libName
339
+ }
340
+
341
+ private formatInitProtectionError(code: number): string {
342
+ const messages: Record<number, string> = {
343
+ '-3001': '未找到数据库目录 (db_storage),请确认已选择正确的微信数据目录(应包含以 wxid_ 开头的子文件夹)',
344
+ '-3002': '未找到 session.db 文件,请确认微信已登录并且数据目录完整',
345
+ '-3003': '数据库句柄无效,请重试',
346
+ '-3004': '恢复数据库连接失败,请重试',
347
+ '-2301': '动态库加载失败,请检查安装是否完整',
348
+ '-2302': 'WCDB 初始化异常,请重试',
349
+ '-2303': 'WCDB 未能成功初始化',
350
+ }
351
+ const msg = messages[String(code) as unknown as keyof typeof messages]
352
+ return msg ? `${msg} (错误码: ${code})` : `操作失败,错误码: ${code}`
353
+ }
354
+
355
+ private isLogEnabled(): boolean {
356
+ // 移除 Worker 线程的日志禁用逻辑,允许在 Worker 中记录日志
357
+ if (process.env.WCDB_LOG_ENABLED === '1') return true
358
+ return this.logEnabled
359
+ }
360
+
361
+ private writeLog(message: string, force = false): void {
362
+ if (!force && !this.isLogEnabled()) return
363
+ const line = `[${new Date().toISOString()}] ${message}`
364
+
365
+ const candidates: string[] = []
366
+ if (this.userDataPath) candidates.push(join(this.userDataPath, 'logs', 'wcdb.log'))
367
+ if (process.env.WCDB_LOG_DIR) candidates.push(join(process.env.WCDB_LOG_DIR, 'logs', 'wcdb.log'))
368
+ candidates.push(join(process.cwd(), 'logs', 'wcdb.log'))
369
+ candidates.push(join(tmpdir(), 'weflow-wcdb.log'))
370
+
371
+ const uniq = Array.from(new Set(candidates))
372
+ for (const filePath of uniq) {
373
+ try {
374
+ const dir = dirname(filePath)
375
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
376
+ appendFileSync(filePath, line + '\n', { encoding: 'utf8' })
377
+ this.lastResolvedLogPath = filePath
378
+ return
379
+ } catch (e) {
380
+ console.error(`[wcdbCore] writeLog failed path=${filePath}:`, e)
381
+ }
382
+ }
383
+
384
+ console.error('[wcdbCore] writeLog failed for all candidates:', uniq.join(' | '))
385
+ }
386
+
387
+ private formatSqlForLog(sql: string, maxLen = 240): string {
388
+ const compact = String(sql || '').replace(/\s+/g, ' ').trim()
389
+ if (compact.length <= maxLen) return compact
390
+ return compact.slice(0, maxLen) + '...'
391
+ }
392
+
393
+ private async dumpDbStatus(tag: string): Promise<void> {
394
+ try {
395
+ if (!this.ensureReady()) {
396
+ this.writeLog(`[diag:${tag}] db_status skipped: not connected`, true)
397
+ return
398
+ }
399
+ if (!this.wcdbGetDbStatus) {
400
+ this.writeLog(`[diag:${tag}] db_status skipped: api not supported`, true)
401
+ return
402
+ }
403
+ const outPtr = [null as any]
404
+ const rc = this.wcdbGetDbStatus(this.handle, outPtr)
405
+ if (rc !== 0 || !outPtr[0]) {
406
+ this.writeLog(`[diag:${tag}] db_status failed rc=${rc} outPtr=${outPtr[0] ? 'set' : 'null'}`, true)
407
+ return
408
+ }
409
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
410
+ if (!jsonStr) {
411
+ this.writeLog(`[diag:${tag}] db_status decode failed`, true)
412
+ return
413
+ }
414
+ this.writeLog(`[diag:${tag}] db_status=${jsonStr}`, true)
415
+ } catch (e) {
416
+ this.writeLog(`[diag:${tag}] db_status exception: ${String(e)}`, true)
417
+ }
418
+ }
419
+
420
+ private async runPostOpenDiagnostics(dbPath: string, dbStoragePath: string | null, sessionDbPath: string | null, wxid: string): Promise<void> {
421
+ try {
422
+ this.writeLog(`[diag:open] input dbPath=${dbPath} wxid=${wxid}`, true)
423
+ this.writeLog(`[diag:open] resolved dbStorage=${dbStoragePath || 'null'}`, true)
424
+ this.writeLog(`[diag:open] resolved sessionDb=${sessionDbPath || 'null'}`, true)
425
+ if (!dbStoragePath) return
426
+ try {
427
+ const entries = readdirSync(dbStoragePath)
428
+ const sample = entries.slice(0, 20).join(',')
429
+ this.writeLog(`[diag:open] dbStorage entries(${entries.length}) sample=${sample}`, true)
430
+ } catch (e) {
431
+ this.writeLog(`[diag:open] list dbStorage failed: ${String(e)}`, true)
432
+ }
433
+
434
+ const contactProbe = await this.execQuery(
435
+ 'contact',
436
+ null,
437
+ "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name LIMIT 50"
438
+ )
439
+ if (contactProbe.success) {
440
+ const names = (contactProbe.rows || []).map((r: any) => String(r?.name || '')).filter(Boolean)
441
+ this.writeLog(`[diag:open] contact sqlite_master rows=${names.length} names=${names.join(',')}`, true)
442
+ } else {
443
+ this.writeLog(`[diag:open] contact sqlite_master failed: ${contactProbe.error || 'unknown'}`, true)
444
+ }
445
+
446
+ const contactCount = await this.execQuery('contact', null, 'SELECT COUNT(1) AS cnt FROM contact')
447
+ if (contactCount.success && Array.isArray(contactCount.rows) && contactCount.rows.length > 0) {
448
+ this.writeLog(`[diag:open] contact count=${String((contactCount.rows[0] as any)?.cnt ?? '')}`, true)
449
+ } else {
450
+ this.writeLog(`[diag:open] contact count failed: ${contactCount.error || 'unknown'}`, true)
451
+ }
452
+ } catch (e) {
453
+ this.writeLog(`[diag:open] post-open diagnostics exception: ${String(e)}`, true)
454
+ }
455
+ }
456
+
457
+ /**
458
+ * 递归查找 session.db 文件
459
+ */
460
+ private findSessionDb(dir: string, depth = 0): string | null {
461
+ if (depth > 5) return null
462
+
463
+ try {
464
+ const entries = readdirSync(dir)
465
+
466
+ for (const entry of entries) {
467
+ if (entry.toLowerCase() === 'session.db') {
468
+ const fullPath = join(dir, entry)
469
+ if (statSync(fullPath).isFile()) {
470
+ return fullPath
471
+ }
472
+ }
473
+ }
474
+
475
+ for (const entry of entries) {
476
+ const fullPath = join(dir, entry)
477
+ try {
478
+ if (statSync(fullPath).isDirectory()) {
479
+ const found = this.findSessionDb(fullPath, depth + 1)
480
+ if (found) return found
481
+ }
482
+ } catch { }
483
+ }
484
+ } catch (e) {
485
+ console.error('查找 session.db 失败:', e)
486
+ }
487
+
488
+ return null
489
+ }
490
+
491
+ private resolveDbStoragePath(basePath: string, wxid: string): string | null {
492
+ if (!basePath) return null
493
+ const normalized = expandHomePath(basePath).replace(/[\\\\/]+$/, '')
494
+ if (normalized.toLowerCase().endsWith('db_storage') && existsSync(normalized)) {
495
+ return normalized
496
+ }
497
+ const direct = join(normalized, 'db_storage')
498
+ if (existsSync(direct)) {
499
+ return direct
500
+ }
501
+ if (wxid) {
502
+ const viaWxid = join(normalized, wxid, 'db_storage')
503
+ if (existsSync(viaWxid)) {
504
+ return viaWxid
505
+ }
506
+ // 兼容目录名包含额外后缀(如 wxid_xxx_1234)
507
+ try {
508
+ const entries = readdirSync(normalized)
509
+ const lowerWxid = wxid.toLowerCase()
510
+ const candidates = entries.filter((entry) => {
511
+ const entryPath = join(normalized, entry)
512
+ try {
513
+ if (!statSync(entryPath).isDirectory()) return false
514
+ } catch {
515
+ return false
516
+ }
517
+ const lowerEntry = entry.toLowerCase()
518
+ return lowerEntry === lowerWxid || lowerEntry.startsWith(`${lowerWxid}_`)
519
+ })
520
+ for (const entry of candidates) {
521
+ const candidate = join(normalized, entry, 'db_storage')
522
+ if (existsSync(candidate)) {
523
+ return candidate
524
+ }
525
+ }
526
+ } catch { }
527
+ }
528
+ // 兜底:向上查找 db_storage(最多 2 级),处理用户选择了子目录的情况
529
+ try {
530
+ let parent = normalized
531
+ for (let i = 0; i < 2; i++) {
532
+ const up = join(parent, '..')
533
+ if (up === parent) break
534
+ parent = up
535
+ const candidateUp = join(parent, 'db_storage')
536
+ if (existsSync(candidateUp)) return candidateUp
537
+ if (wxid) {
538
+ const viaWxidUp = join(parent, wxid, 'db_storage')
539
+ if (existsSync(viaWxidUp)) return viaWxidUp
540
+ }
541
+ }
542
+ } catch { }
543
+ // 兜底:递归搜索 basePath 下的 db_storage 目录(最多 3 层深)
544
+ try {
545
+ const found = this.findDbStorageRecursive(normalized, 3)
546
+ if (found) return found
547
+ } catch { }
548
+ return null
549
+ }
550
+
551
+ private findDbStorageRecursive(dir: string, maxDepth: number): string | null {
552
+ if (maxDepth <= 0) return null
553
+ try {
554
+ const entries = readdirSync(dir)
555
+ for (const entry of entries) {
556
+ if (entry.toLowerCase() === 'db_storage') {
557
+ const candidate = join(dir, entry)
558
+ try { if (statSync(candidate).isDirectory()) return candidate } catch { }
559
+ }
560
+ }
561
+ for (const entry of entries) {
562
+ const entryPath = join(dir, entry)
563
+ try {
564
+ if (statSync(entryPath).isDirectory()) {
565
+ const found = this.findDbStorageRecursive(entryPath, maxDepth - 1)
566
+ if (found) return found
567
+ }
568
+ } catch { }
569
+ }
570
+ } catch { }
571
+ return null
572
+ }
573
+
574
+ private isRealDbFileName(name: string): boolean {
575
+ const lower = String(name || '').toLowerCase()
576
+ if (!lower.endsWith('.db')) return false
577
+ if (lower.endsWith('.db-shm')) return false
578
+ if (lower.endsWith('.db-wal')) return false
579
+ if (lower.endsWith('.db-journal')) return false
580
+ return true
581
+ }
582
+
583
+ private resolveContactDbPath(): string | null {
584
+ const dbStorage = this.currentDbStoragePath || this.resolveDbStoragePath(this.currentPath || '', this.currentWxid || '')
585
+ if (!dbStorage) return null
586
+ const contactDir = join(dbStorage, 'Contact')
587
+ if (!existsSync(contactDir)) return null
588
+
589
+ const preferred = [
590
+ join(contactDir, 'contact.db'),
591
+ join(contactDir, 'Contact.db')
592
+ ]
593
+ for (const p of preferred) {
594
+ if (existsSync(p)) return p
595
+ }
596
+
597
+ try {
598
+ const entries = readdirSync(contactDir)
599
+ const cands = entries
600
+ .filter((name) => this.isRealDbFileName(name))
601
+ .map((name) => join(contactDir, name))
602
+ if (cands.length > 0) return cands[0]
603
+ } catch { }
604
+ return null
605
+ }
606
+
607
+ private pickFirstStringField(row: Record<string, any>, candidates: string[]): string {
608
+ for (const key of candidates) {
609
+ const v = row[key]
610
+ if (typeof v === 'string' && v.trim()) return v
611
+ if (v !== null && v !== undefined) {
612
+ const s = String(v).trim()
613
+ if (s) return s
614
+ }
615
+ }
616
+ return ''
617
+ }
618
+
619
+ private escapeSqlString(value: string): string {
620
+ return String(value || '').replace(/'/g, "''")
621
+ }
622
+
623
+ private buildContactSelectSql(usernames: string[] = []): string {
624
+ const uniq = Array.from(new Set((usernames || []).map((item) => String(item || '').trim()).filter(Boolean)))
625
+ if (uniq.length === 0) return 'SELECT * FROM contact'
626
+ const inList = uniq.map((username) => `'${this.escapeSqlString(username)}'`).join(',')
627
+ return `SELECT * FROM contact WHERE username IN (${inList})`
628
+ }
629
+
630
+ private deriveContactTypeCounts(rows: Array<Record<string, any>>): { private: number; group: number; official: number; former_friend: number } {
631
+ const counts = {
632
+ private: 0,
633
+ group: 0,
634
+ official: 0,
635
+ former_friend: 0
636
+ }
637
+ const excludeNames = new Set(['medianote', 'floatbottle', 'qmessage', 'qqmail', 'fmessage'])
638
+
639
+ for (const row of rows || []) {
640
+ const username = this.pickFirstStringField(row, ['username', 'user_name', 'userName'])
641
+ if (!username) continue
642
+
643
+ const localTypeRaw = row.local_type ?? row.localType ?? row.WCDB_CT_local_type ?? 0
644
+ const localType = Number.isFinite(Number(localTypeRaw)) ? Math.floor(Number(localTypeRaw)) : 0
645
+ const quanPin = this.pickFirstStringField(row, ['quan_pin', 'quanPin', 'WCDB_CT_quan_pin'])
646
+
647
+ if (username.endsWith('@chatroom')) {
648
+ counts.group += 1
649
+ } else if (username.startsWith('gh_')) {
650
+ counts.official += 1
651
+ } else if (localType === 1 && !excludeNames.has(username)) {
652
+ counts.private += 1
653
+ } else if (localType === 0 && quanPin) {
654
+ counts.former_friend += 1
655
+ }
656
+ }
657
+
658
+ return counts
659
+ }
660
+
661
+ /**
662
+ * 初始化 WCDB
663
+ */
664
+ async initialize(): Promise<boolean> {
665
+ if (this.initialized) return true
666
+
667
+ try {
668
+ this.koffi = require('koffi')
669
+ const dllPath = this.getDllPath()
670
+ this.writeLog(`[bootstrap] initialize platform=${process.platform} dllPath=${dllPath} resourcesPath=${this.resourcesPath || ''} userDataPath=${this.userDataPath || ''}`, true)
671
+
672
+ if (!existsSync(dllPath)) {
673
+ console.error('WCDB数据服务不存在:', dllPath)
674
+ this.writeLog(`[bootstrap] initialize failed:数据服务not found path=${dllPath}`, true)
675
+ return false
676
+ }
677
+
678
+ const dllDir = dirname(dllPath)
679
+ const isMac = process.platform === 'darwin'
680
+ const isLinux = process.platform === 'linux'
681
+
682
+ // 预加载依赖库
683
+ if (isMac) {
684
+ const wcdbCorePath = join(dllDir, 'libWCDB.dylib')
685
+ if (existsSync(wcdbCorePath)) {
686
+ try {
687
+ this.koffi.load(wcdbCorePath)
688
+ this.writeLog('预加载 libWCDB.dylib 成功')
689
+ } catch (e) {
690
+ console.warn('预加载 libWCDB.dylib 失败(可能不是致命的):', e)
691
+ this.writeLog(`预加载 libWCDB.dylib 失败: ${String(e)}`)
692
+ }
693
+ }
694
+ } else if (isLinux) {
695
+ // 如果有libWCDB.so的话, 没有就算了
696
+ } else {
697
+ const wcdbCorePath = join(dllDir, 'WCDB.dll')
698
+ if (existsSync(wcdbCorePath)) {
699
+ try {
700
+ this.koffi.load(wcdbCorePath)
701
+ this.writeLog('预加载 WCDB.dll 成功')
702
+ } catch (e) {
703
+ console.warn('预加载 WCDB.dll 失败(可能不是致命的):', e)
704
+ this.writeLog(`预加载 WCDB.dll 失败: ${String(e)}`)
705
+ }
706
+ }
707
+ const sdl2Path = join(dllDir, 'SDL2.dll')
708
+ if (existsSync(sdl2Path)) {
709
+ try {
710
+ this.koffi.load(sdl2Path)
711
+ this.writeLog('预加载 SDL2.dll 成功')
712
+ } catch (e) {
713
+ console.warn('预加载 SDL2.dll 失败(可能不是致命的):', e)
714
+ this.writeLog(`预加载 SDL2.dll 失败: ${String(e)}`)
715
+ }
716
+ }
717
+ }
718
+
719
+ this.writeLog(`[bootstrap] koffi.load begin path=${dllPath}`, true)
720
+ this.lib = this.koffi.load(dllPath)
721
+ this.writeLog('[bootstrap] koffi.load ok', true)
722
+
723
+ // InitProtection (Added for security)
724
+ try {
725
+ this.wcdbInitProtection = this.lib.func('int32 InitProtection(const char* resourcePath)')
726
+
727
+ // 尝试多个可能的资源路径
728
+ const resourcePaths = [
729
+ this.resourcesPath, // 配置的资源路径(优先)
730
+ join(process.cwd(), 'resources'), // 开发环境
731
+ (process as any).resourcesPath, // 打包后 Contents/Resources
732
+ (process as any).resourcesPath ? join((process as any).resourcesPath, 'resources') : null, // Contents/Resources/resources
733
+ dirname(dllDir), // DLL 上级目录
734
+ dllDir // DLL 所在目录(兜底)
735
+ ].filter(Boolean)
736
+
737
+ let protectionOk = false
738
+ let protectionCode = -1
739
+ let bestFailCode: number | null = null
740
+ const scoreFailCode = (code: number): number => {
741
+ if (code >= -2212 && code <= -2201) return 0 // manifest/signature/hash failures
742
+ if (code === -102 || code === -101 || code === -1006) return 1
743
+ return 2
744
+ }
745
+ for (const resPath of resourcePaths) {
746
+ try {
747
+ this.writeLog(`[bootstrap] InitProtection call path=${resPath}`, true)
748
+ protectionCode = Number(this.wcdbInitProtection(resPath))
749
+ this.writeLog(`[bootstrap] InitProtection returned rc=${protectionCode} path=${resPath}`, true)
750
+ if (protectionCode === 0) {
751
+ protectionOk = true
752
+ break
753
+ }
754
+ if (bestFailCode === null || scoreFailCode(protectionCode) < scoreFailCode(bestFailCode)) {
755
+ bestFailCode = protectionCode
756
+ }
757
+ } catch (e) {
758
+ this.writeLog(`[bootstrap] InitProtection exception path=${resPath}: ${String(e)}`, true)
759
+ }
760
+ }
761
+
762
+ if (!protectionOk) {
763
+ const finalCode = bestFailCode ?? protectionCode
764
+ lastDllInitError = this.formatInitProtectionError(finalCode)
765
+ this.writeLog(`[bootstrap] InitProtection failed finalCode=${finalCode}`, true)
766
+ return false
767
+ }
768
+ } catch (e) {
769
+ lastDllInitError = this.formatInitProtectionError(-2301)
770
+ this.writeLog(`[bootstrap] InitProtection symbol load failed: ${String(e)}`, true)
771
+ return false
772
+ }
773
+
774
+ // 定义类型
775
+ // wcdb_status wcdb_init()
776
+ this.wcdbInit = this.lib.func('int32 wcdb_init()')
777
+
778
+ // wcdb_status wcdb_shutdown()
779
+ this.wcdbShutdown = this.lib.func('int32 wcdb_shutdown()')
780
+
781
+ // wcdb_status wcdb_open_account(const char* session_db_path, const char* hex_key, wcdb_handle* out_handle)
782
+ // wcdb_handle 是 int64_t
783
+ this.wcdbOpenAccount = this.lib.func('int32 wcdb_open_account(const char* path, const char* key, _Out_ int64* handle)')
784
+
785
+ // wcdb_status wcdb_close_account(wcdb_handle handle)
786
+ // C 接口是 int64, koffi 返回 handle 是 number 类型
787
+ this.wcdbCloseAccount = this.lib.func('int32 wcdb_close_account(int64 handle)')
788
+
789
+ // wcdb_status wcdb_set_my_wxid(wcdb_handle handle, const char* wxid)
790
+ try {
791
+ this.wcdbSetMyWxid = this.lib.func('int32 wcdb_set_my_wxid(int64 handle, const char* wxid)')
792
+ } catch {
793
+ this.wcdbSetMyWxid = null
794
+ }
795
+
796
+ // wcdb_status wcdb_update_message(wcdb_handle handle, const char* session_id, int64_t local_id, int32_t create_time, const char* new_content, char** out_error)
797
+ try {
798
+ this.wcdbUpdateMessage = this.lib.func('int32 wcdb_update_message(int64 handle, const char* sessionId, int64 localId, int32 createTime, const char* newContent, _Out_ void** outError)')
799
+ } catch {
800
+ this.wcdbUpdateMessage = null
801
+ }
802
+
803
+ // wcdb_status wcdb_delete_message(wcdb_handle handle, const char* session_id, int64_t local_id, char** out_error)
804
+ try {
805
+ this.wcdbDeleteMessage = this.lib.func('int32 wcdb_delete_message(int64 handle, const char* sessionId, int64 localId, int32 createTime, const char* dbPathHint, _Out_ void** outError)')
806
+ } catch {
807
+ this.wcdbDeleteMessage = null
808
+ }
809
+
810
+ // void wcdb_free_string(char* ptr)
811
+ this.wcdbFreeString = this.lib.func('void wcdb_free_string(void* ptr)')
812
+
813
+ // wcdb_status wcdb_get_sessions(wcdb_handle handle, char** out_json)
814
+ this.wcdbGetSessions = this.lib.func('int32 wcdb_get_sessions(int64 handle, _Out_ void** outJson)')
815
+
816
+ // wcdb_status wcdb_get_messages(wcdb_handle handle, const char* username, int32_t limit, int32_t offset, char** out_json)
817
+ this.wcdbGetMessages = this.lib.func('int32 wcdb_get_messages(int64 handle, const char* username, int32 limit, int32 offset, _Out_ void** outJson)')
818
+
819
+ // wcdb_status wcdb_get_message_count(wcdb_handle handle, const char* username, int32_t* out_count)
820
+ this.wcdbGetMessageCount = this.lib.func('int32 wcdb_get_message_count(int64 handle, const char* username, _Out_ int32* outCount)')
821
+
822
+ // wcdb_status wcdb_get_display_names(wcdb_handle handle, const char* usernames_json, char** out_json)
823
+ this.wcdbGetDisplayNames = this.lib.func('int32 wcdb_get_display_names(int64 handle, const char* usernamesJson, _Out_ void** outJson)')
824
+
825
+ // wcdb_status wcdb_get_avatar_urls(wcdb_handle handle, const char* usernames_json, char** out_json)
826
+ this.wcdbGetAvatarUrls = this.lib.func('int32 wcdb_get_avatar_urls(int64 handle, const char* usernamesJson, _Out_ void** outJson)')
827
+
828
+ // wcdb_status wcdb_get_group_member_count(wcdb_handle handle, const char* chatroom_id, int32_t* out_count)
829
+ this.wcdbGetGroupMemberCount = this.lib.func('int32 wcdb_get_group_member_count(int64 handle, const char* chatroomId, _Out_ int32* outCount)')
830
+
831
+ // wcdb_status wcdb_get_group_member_counts(wcdb_handle handle, const char* chatroom_ids_json, char** out_json)
832
+ try {
833
+ this.wcdbGetGroupMemberCounts = this.lib.func('int32 wcdb_get_group_member_counts(int64 handle, const char* chatroomIdsJson, _Out_ void** outJson)')
834
+ } catch {
835
+ this.wcdbGetGroupMemberCounts = null
836
+ }
837
+
838
+ // wcdb_status wcdb_get_group_members(wcdb_handle handle, const char* chatroom_id, char** out_json)
839
+ this.wcdbGetGroupMembers = this.lib.func('int32 wcdb_get_group_members(int64 handle, const char* chatroomId, _Out_ void** outJson)')
840
+
841
+ // wcdb_status wcdb_get_group_nicknames(wcdb_handle handle, const char* chatroom_id, char** out_json)
842
+ try {
843
+ this.wcdbGetGroupNicknames = this.lib.func('int32 wcdb_get_group_nicknames(int64 handle, const char* chatroomId, _Out_ void** outJson)')
844
+ } catch {
845
+ this.wcdbGetGroupNicknames = null
846
+ }
847
+
848
+ // wcdb_status wcdb_get_message_tables(wcdb_handle handle, const char* session_id, char** out_json)
849
+ this.wcdbGetMessageTables = this.lib.func('int32 wcdb_get_message_tables(int64 handle, const char* sessionId, _Out_ void** outJson)')
850
+
851
+ // wcdb_status wcdb_get_message_meta(wcdb_handle handle, const char* db_path, const char* table_name, int32_t limit, int32_t offset, char** out_json)
852
+ this.wcdbGetMessageMeta = this.lib.func('int32 wcdb_get_message_meta(int64 handle, const char* dbPath, const char* tableName, int32 limit, int32 offset, _Out_ void** outJson)')
853
+
854
+ // wcdb_status wcdb_get_contact(wcdb_handle handle, const char* username, char** out_json)
855
+ this.wcdbGetContact = this.lib.func('int32 wcdb_get_contact(int64 handle, const char* username, _Out_ void** outJson)')
856
+
857
+ // wcdb_status wcdb_get_contact_status(wcdb_handle handle, const char* usernames_json, char** out_json)
858
+ try {
859
+ this.wcdbGetContactStatus = this.lib.func('int32 wcdb_get_contact_status(int64 handle, const char* usernamesJson, _Out_ void** outJson)')
860
+ } catch {
861
+ this.wcdbGetContactStatus = null
862
+ }
863
+
864
+ try {
865
+ this.wcdbGetContactTypeCounts = this.lib.func('int32 wcdb_get_contact_type_counts(int64 handle, _Out_ void** outJson)')
866
+ } catch {
867
+ this.wcdbGetContactTypeCounts = null
868
+ }
869
+ try {
870
+ this.wcdbGetContactsCompact = this.lib.func('int32 wcdb_get_contacts_compact(int64 handle, const char* usernamesJson, _Out_ void** outJson)')
871
+ } catch {
872
+ this.wcdbGetContactsCompact = null
873
+ }
874
+ try {
875
+ this.wcdbGetContactAliasMap = this.lib.func('int32 wcdb_get_contact_alias_map(int64 handle, const char* usernamesJson, _Out_ void** outJson)')
876
+ } catch {
877
+ this.wcdbGetContactAliasMap = null
878
+ }
879
+ try {
880
+ this.wcdbGetContactFriendFlags = this.lib.func('int32 wcdb_get_contact_friend_flags(int64 handle, const char* usernamesJson, _Out_ void** outJson)')
881
+ } catch {
882
+ this.wcdbGetContactFriendFlags = null
883
+ }
884
+ try {
885
+ this.wcdbGetChatRoomExtBuffer = this.lib.func('int32 wcdb_get_chat_room_ext_buffer(int64 handle, const char* chatroomId, _Out_ void** outJson)')
886
+ } catch {
887
+ this.wcdbGetChatRoomExtBuffer = null
888
+ }
889
+
890
+ // wcdb_status wcdb_get_message_table_stats(wcdb_handle handle, const char* session_id, char** out_json)
891
+ this.wcdbGetMessageTableStats = this.lib.func('int32 wcdb_get_message_table_stats(int64 handle, const char* sessionId, _Out_ void** outJson)')
892
+
893
+ // wcdb_status wcdb_get_aggregate_stats(wcdb_handle handle, const char* session_ids_json, int32_t begin_timestamp, int32_t end_timestamp, char** out_json)
894
+ this.wcdbGetAggregateStats = this.lib.func('int32 wcdb_get_aggregate_stats(int64 handle, const char* sessionIdsJson, int32 begin, int32 end, _Out_ void** outJson)')
895
+
896
+ // wcdb_status wcdb_get_available_years(wcdb_handle handle, const char* session_ids_json, char** out_json)
897
+ try {
898
+ this.wcdbGetAvailableYears = this.lib.func('int32 wcdb_get_available_years(int64 handle, const char* sessionIdsJson, _Out_ void** outJson)')
899
+ } catch {
900
+ this.wcdbGetAvailableYears = null
901
+ }
902
+
903
+ // wcdb_status wcdb_get_annual_report_stats(wcdb_handle handle, const char* session_ids_json, int32_t begin_timestamp, int32_t end_timestamp, char** out_json)
904
+ try {
905
+ this.wcdbGetAnnualReportStats = this.lib.func('int32 wcdb_get_annual_report_stats(int64 handle, const char* sessionIdsJson, int32 begin, int32 end, _Out_ void** outJson)')
906
+ } catch {
907
+ this.wcdbGetAnnualReportStats = null
908
+ }
909
+
910
+ // wcdb_status wcdb_get_annual_report_extras(wcdb_handle handle, const char* session_ids_json, int32_t begin_timestamp, int32_t end_timestamp, int32_t peak_day_begin, int32_t peak_day_end, char** out_json)
911
+ try {
912
+ this.wcdbGetAnnualReportExtras = this.lib.func('int32 wcdb_get_annual_report_extras(int64 handle, const char* sessionIdsJson, int32 begin, int32 end, int32 peakBegin, int32 peakEnd, _Out_ void** outJson)')
913
+ } catch {
914
+ this.wcdbGetAnnualReportExtras = null
915
+ }
916
+
917
+ // wcdb_status wcdb_get_dual_report_stats(wcdb_handle handle, const char* session_id, int32_t begin_timestamp, int32_t end_timestamp, char** out_json)
918
+ try {
919
+ this.wcdbGetDualReportStats = this.lib.func('int32 wcdb_get_dual_report_stats(int64 handle, const char* sessionId, int32 begin, int32 end, _Out_ void** outJson)')
920
+ } catch {
921
+ this.wcdbGetDualReportStats = null
922
+ }
923
+
924
+ // wcdb_status wcdb_get_logs(char** out_json)
925
+ try {
926
+ this.wcdbGetLogs = this.lib.func('int32 wcdb_get_logs(_Out_ void** outJson)')
927
+ } catch {
928
+ this.wcdbGetLogs = null
929
+ }
930
+
931
+ // wcdb_status wcdb_get_group_stats(wcdb_handle handle, const char* chatroom_id, int32_t begin_timestamp, int32_t end_timestamp, char** out_json)
932
+ try {
933
+ this.wcdbGetGroupStats = this.lib.func('int32 wcdb_get_group_stats(int64 handle, const char* chatroomId, int32 begin, int32 end, _Out_ void** outJson)')
934
+ } catch {
935
+ this.wcdbGetGroupStats = null
936
+ }
937
+
938
+ // wcdb_status wcdb_get_my_footprint_stats(wcdb_handle handle, const char* options_json, char** out_json)
939
+ try {
940
+ this.wcdbGetMyFootprintStats = this.lib.func('int32 wcdb_get_my_footprint_stats(int64 handle, const char* optionsJson, _Out_ void** outJson)')
941
+ } catch {
942
+ this.wcdbGetMyFootprintStats = null
943
+ }
944
+
945
+ // wcdb_status wcdb_get_message_dates(wcdb_handle handle, const char* session_id, char** out_json)
946
+ try {
947
+ this.wcdbGetMessageDates = this.lib.func('int32 wcdb_get_message_dates(int64 handle, const char* sessionId, _Out_ void** outJson)')
948
+ } catch {
949
+ this.wcdbGetMessageDates = null
950
+ }
951
+
952
+ // wcdb_status wcdb_open_message_cursor(wcdb_handle handle, const char* session_id, int32_t batch_size, int32_t ascending, int32_t begin_timestamp, int32_t end_timestamp, wcdb_cursor* out_cursor)
953
+ this.wcdbOpenMessageCursor = this.lib.func('int32 wcdb_open_message_cursor(int64 handle, const char* sessionId, int32 batchSize, int32 ascending, int32 beginTimestamp, int32 endTimestamp, _Out_ int64* outCursor)')
954
+
955
+ // wcdb_status wcdb_open_message_cursor_lite(wcdb_handle handle, const char* session_id, int32_t batch_size, int32_t ascending, int32_t begin_timestamp, int32_t end_timestamp, wcdb_cursor* out_cursor)
956
+ try {
957
+ this.wcdbOpenMessageCursorLite = this.lib.func('int32 wcdb_open_message_cursor_lite(int64 handle, const char* sessionId, int32 batchSize, int32 ascending, int32 beginTimestamp, int32 endTimestamp, _Out_ int64* outCursor)')
958
+ } catch {
959
+ this.wcdbOpenMessageCursorLite = null
960
+ }
961
+
962
+ // wcdb_status wcdb_fetch_message_batch(wcdb_handle handle, wcdb_cursor cursor, char** out_json, int32_t* out_has_more)
963
+ this.wcdbFetchMessageBatch = this.lib.func('int32 wcdb_fetch_message_batch(int64 handle, int64 cursor, _Out_ void** outJson, _Out_ int32* outHasMore)')
964
+
965
+ // wcdb_status wcdb_close_message_cursor(wcdb_handle handle, wcdb_cursor cursor)
966
+ this.wcdbCloseMessageCursor = this.lib.func('int32 wcdb_close_message_cursor(int64 handle, int64 cursor)')
967
+
968
+ // wcdb_status wcdb_get_logs(char** out_json)
969
+ this.wcdbGetLogs = this.lib.func('int32 wcdb_get_logs(_Out_ void** outJson)')
970
+
971
+ // wcdb_status wcdb_exec_query(wcdb_handle handle, const char* db_kind, const char* db_path, const char* sql, char** out_json)
972
+ this.wcdbExecQuery = this.lib.func('int32 wcdb_exec_query(int64 handle, const char* kind, const char* path, const char* sql, _Out_ void** outJson)')
973
+
974
+ // wcdb_status wcdb_get_emoticon_cdn_url(wcdb_handle handle, const char* db_path, const char* md5, char** out_url)
975
+ this.wcdbGetEmoticonCdnUrl = this.lib.func('int32 wcdb_get_emoticon_cdn_url(int64 handle, const char* dbPath, const char* md5, _Out_ void** outUrl)')
976
+
977
+ // wcdb_status wcdb_get_emoticon_caption(wcdb_handle handle, const char* db_path, const char* md5, char** out_caption)
978
+ try {
979
+ this.wcdbGetEmoticonCaption = this.lib.func('int32 wcdb_get_emoticon_caption(int64 handle, const char* dbPath, const char* md5, _Out_ void** outCaption)')
980
+ } catch (e) {
981
+ this.wcdbGetEmoticonCaption = null
982
+ this.writeLog(`[diag:emoji] symbol missing wcdb_get_emoticon_caption: ${String(e)}`, true)
983
+ }
984
+
985
+ // wcdb_status wcdb_get_emoticon_caption_strict(wcdb_handle handle, const char* md5, char** out_caption)
986
+ try {
987
+ this.wcdbGetEmoticonCaptionStrict = this.lib.func('int32 wcdb_get_emoticon_caption_strict(int64 handle, const char* md5, _Out_ void** outCaption)')
988
+ } catch (e) {
989
+ this.wcdbGetEmoticonCaptionStrict = null
990
+ this.writeLog(`[diag:emoji] symbol missing wcdb_get_emoticon_caption_strict: ${String(e)}`, true)
991
+ }
992
+
993
+ // wcdb_status wcdb_list_message_dbs(wcdb_handle handle, char** out_json)
994
+ this.wcdbListMessageDbs = this.lib.func('int32 wcdb_list_message_dbs(int64 handle, _Out_ void** outJson)')
995
+
996
+ // wcdb_status wcdb_list_media_dbs(wcdb_handle handle, char** out_json)
997
+ this.wcdbListMediaDbs = this.lib.func('int32 wcdb_list_media_dbs(int64 handle, _Out_ void** outJson)')
998
+
999
+ // wcdb_status wcdb_get_message_by_id(wcdb_handle handle, const char* session_id, int32 local_id, char** out_json)
1000
+ this.wcdbGetMessageById = this.lib.func('int32 wcdb_get_message_by_id(int64 handle, const char* sessionId, int32 localId, _Out_ void** outJson)')
1001
+
1002
+ // wcdb_status wcdb_get_db_status(wcdb_handle handle, char** out_json)
1003
+ try {
1004
+ this.wcdbGetDbStatus = this.lib.func('int32 wcdb_get_db_status(int64 handle, _Out_ void** outJson)')
1005
+ } catch {
1006
+ this.wcdbGetDbStatus = null
1007
+ }
1008
+
1009
+ // wcdb_status wcdb_get_voice_data(wcdb_handle handle, const char* session_id, int32_t create_time, int32_t local_id, int64_t svr_id, const char* candidates_json, char** out_hex)
1010
+ try {
1011
+ this.wcdbGetVoiceData = this.lib.func('int32 wcdb_get_voice_data(int64 handle, const char* sessionId, int32 createTime, int32 localId, int64 svrId, const char* candidatesJson, _Out_ void** outHex)')
1012
+ } catch {
1013
+ this.wcdbGetVoiceData = null
1014
+ }
1015
+ try {
1016
+ this.wcdbGetVoiceDataBatch = this.lib.func('int32 wcdb_get_voice_data_batch(int64 handle, const char* requestsJson, _Out_ void** outJson)')
1017
+ } catch {
1018
+ this.wcdbGetVoiceDataBatch = null
1019
+ }
1020
+ try {
1021
+ this.wcdbGetMediaSchemaSummary = this.lib.func('int32 wcdb_get_media_schema_summary(int64 handle, const char* dbPath, _Out_ void** outJson)')
1022
+ } catch {
1023
+ this.wcdbGetMediaSchemaSummary = null
1024
+ }
1025
+ try {
1026
+ this.wcdbGetSessionMessageCounts = this.lib.func('int32 wcdb_get_session_message_counts(int64 handle, const char* sessionIdsJson, _Out_ void** outJson)')
1027
+ } catch {
1028
+ this.wcdbGetSessionMessageCounts = null
1029
+ }
1030
+ try {
1031
+ this.wcdbGetSessionMessageTypeStats = this.lib.func('int32 wcdb_get_session_message_type_stats(int64 handle, const char* sessionId, int32 beginTimestamp, int32 endTimestamp, _Out_ void** outJson)')
1032
+ } catch {
1033
+ this.wcdbGetSessionMessageTypeStats = null
1034
+ }
1035
+ try {
1036
+ this.wcdbGetSessionMessageTypeStatsBatch = this.lib.func('int32 wcdb_get_session_message_type_stats_batch(int64 handle, const char* sessionIdsJson, const char* optionsJson, _Out_ void** outJson)')
1037
+ } catch {
1038
+ this.wcdbGetSessionMessageTypeStatsBatch = null
1039
+ }
1040
+ try {
1041
+ this.wcdbGetSessionMessageDateCounts = this.lib.func('int32 wcdb_get_session_message_date_counts(int64 handle, const char* sessionId, _Out_ void** outJson)')
1042
+ } catch {
1043
+ this.wcdbGetSessionMessageDateCounts = null
1044
+ }
1045
+ try {
1046
+ this.wcdbGetSessionMessageDateCountsBatch = this.lib.func('int32 wcdb_get_session_message_date_counts_batch(int64 handle, const char* sessionIdsJson, _Out_ void** outJson)')
1047
+ } catch {
1048
+ this.wcdbGetSessionMessageDateCountsBatch = null
1049
+ }
1050
+ try {
1051
+ this.wcdbGetMessagesByType = this.lib.func('int32 wcdb_get_messages_by_type(int64 handle, const char* sessionId, int64 localType, int32 ascending, int32 limit, int32 offset, _Out_ void** outJson)')
1052
+ } catch {
1053
+ this.wcdbGetMessagesByType = null
1054
+ }
1055
+ try {
1056
+ this.wcdbScanMediaStream = this.lib.func('int32 wcdb_scan_media_stream(int64 handle, const char* sessionIdsJson, int32 mediaType, int32 beginTimestamp, int32 endTimestamp, int32 limit, int32 offset, _Out_ void** outJson, _Out_ int32* outHasMore)')
1057
+ } catch {
1058
+ this.wcdbScanMediaStream = null
1059
+ }
1060
+ try {
1061
+ this.wcdbGetHeadImageBuffers = this.lib.func('int32 wcdb_get_head_image_buffers(int64 handle, const char* usernamesJson, _Out_ void** outJson)')
1062
+ } catch {
1063
+ this.wcdbGetHeadImageBuffers = null
1064
+ }
1065
+
1066
+ // wcdb_status wcdb_search_messages(wcdb_handle handle, const char* session_id, const char* keyword, int32_t limit, int32_t offset, int32_t begin_timestamp, int32_t end_timestamp, char** out_json)
1067
+ try {
1068
+ this.wcdbSearchMessages = this.lib.func('int32 wcdb_search_messages(int64 handle, const char* sessionId, const char* keyword, int32 limit, int32 offset, int32 beginTimestamp, int32 endTimestamp, _Out_ void** outJson)')
1069
+ } catch {
1070
+ this.wcdbSearchMessages = null
1071
+ }
1072
+
1073
+ // wcdb_status wcdb_get_sns_timeline(wcdb_handle handle, int32_t limit, int32_t offset, const char* username, const char* keyword, int32_t start_time, int32_t end_time, char** out_json)
1074
+ try {
1075
+ this.wcdbGetSnsTimeline = this.lib.func('int32 wcdb_get_sns_timeline(int64 handle, int32 limit, int32 offset, const char* username, const char* keyword, int32 startTime, int32 endTime, _Out_ void** outJson)')
1076
+ } catch {
1077
+ this.wcdbGetSnsTimeline = null
1078
+ }
1079
+
1080
+ // wcdb_status wcdb_get_sns_annual_stats(wcdb_handle handle, int32_t begin_timestamp, int32_t end_timestamp, char** out_json)
1081
+ try {
1082
+ this.wcdbGetSnsAnnualStats = this.lib.func('int32 wcdb_get_sns_annual_stats(int64 handle, int32 begin, int32 end, _Out_ void** outJson)')
1083
+ } catch {
1084
+ this.wcdbGetSnsAnnualStats = null
1085
+ }
1086
+ try {
1087
+ this.wcdbGetSnsUsernames = this.lib.func('int32 wcdb_get_sns_usernames(int64 handle, _Out_ void** outJson)')
1088
+ } catch {
1089
+ this.wcdbGetSnsUsernames = null
1090
+ }
1091
+ try {
1092
+ this.wcdbGetSnsExportStats = this.lib.func('int32 wcdb_get_sns_export_stats(int64 handle, const char* myWxid, _Out_ void** outJson)')
1093
+ } catch {
1094
+ this.wcdbGetSnsExportStats = null
1095
+ }
1096
+ try {
1097
+ this.wcdbGetMessageTableColumns = this.lib.func('int32 wcdb_get_message_table_columns(int64 handle, const char* dbPath, const char* tableName, _Out_ void** outJson)')
1098
+ } catch {
1099
+ this.wcdbGetMessageTableColumns = null
1100
+ }
1101
+ try {
1102
+ this.wcdbListTables = this.lib.func('int32 wcdb_list_tables(int64 handle, const char* kind, const char* dbPath, _Out_ void** outJson)')
1103
+ } catch {
1104
+ this.wcdbListTables = null
1105
+ }
1106
+ try {
1107
+ this.wcdbGetTableSchema = this.lib.func('int32 wcdb_get_table_schema(int64 handle, const char* kind, const char* dbPath, const char* tableName, _Out_ void** outJson)')
1108
+ } catch {
1109
+ this.wcdbGetTableSchema = null
1110
+ }
1111
+ try {
1112
+ this.wcdbExportTableSnapshot = this.lib.func('int32 wcdb_export_table_snapshot(int64 handle, const char* kind, const char* dbPath, const char* tableName, const char* outputPath, _Out_ void** outJson)')
1113
+ } catch {
1114
+ this.wcdbExportTableSnapshot = null
1115
+ }
1116
+ try {
1117
+ this.wcdbImportTableSnapshot = this.lib.func('int32 wcdb_import_table_snapshot(int64 handle, const char* kind, const char* dbPath, const char* tableName, const char* inputPath, _Out_ void** outJson)')
1118
+ } catch {
1119
+ this.wcdbImportTableSnapshot = null
1120
+ }
1121
+ try {
1122
+ this.wcdbImportTableSnapshotWithSchema = this.lib.func('int32 wcdb_import_table_snapshot_with_schema(int64 handle, const char* kind, const char* dbPath, const char* tableName, const char* inputPath, const char* createTableSql, _Out_ void** outJson)')
1123
+ } catch {
1124
+ this.wcdbImportTableSnapshotWithSchema = null
1125
+ }
1126
+ try {
1127
+ this.wcdbGetMessageTableTimeRange = this.lib.func('int32 wcdb_get_message_table_time_range(int64 handle, const char* dbPath, const char* tableName, _Out_ void** outJson)')
1128
+ } catch {
1129
+ this.wcdbGetMessageTableTimeRange = null
1130
+ }
1131
+ try {
1132
+ this.wcdbResolveImageHardlink = this.lib.func('int32 wcdb_resolve_image_hardlink(int64 handle, const char* md5, const char* accountDir, _Out_ void** outJson)')
1133
+ } catch {
1134
+ this.wcdbResolveImageHardlink = null
1135
+ }
1136
+ try {
1137
+ this.wcdbResolveImageHardlinkBatch = this.lib.func('int32 wcdb_resolve_image_hardlink_batch(int64 handle, const char* requestsJson, _Out_ void** outJson)')
1138
+ } catch {
1139
+ this.wcdbResolveImageHardlinkBatch = null
1140
+ }
1141
+ try {
1142
+ this.wcdbResolveVideoHardlinkMd5 = this.lib.func('int32 wcdb_resolve_video_hardlink_md5(int64 handle, const char* md5, const char* dbPath, _Out_ void** outJson)')
1143
+ } catch {
1144
+ this.wcdbResolveVideoHardlinkMd5 = null
1145
+ }
1146
+ try {
1147
+ this.wcdbResolveVideoHardlinkMd5Batch = this.lib.func('int32 wcdb_resolve_video_hardlink_md5_batch(int64 handle, const char* requestsJson, _Out_ void** outJson)')
1148
+ } catch {
1149
+ this.wcdbResolveVideoHardlinkMd5Batch = null
1150
+ }
1151
+
1152
+ // wcdb_status wcdb_install_message_anti_revoke_trigger(wcdb_handle handle, const char* session_id, char** out_error)
1153
+ try {
1154
+ this.wcdbInstallMessageAntiRevokeTrigger = this.lib.func('int32 wcdb_install_message_anti_revoke_trigger(int64 handle, const char* sessionId, _Out_ void** outError)')
1155
+ } catch {
1156
+ this.wcdbInstallMessageAntiRevokeTrigger = null
1157
+ }
1158
+
1159
+ // wcdb_status wcdb_uninstall_message_anti_revoke_trigger(wcdb_handle handle, const char* session_id, char** out_error)
1160
+ try {
1161
+ this.wcdbUninstallMessageAntiRevokeTrigger = this.lib.func('int32 wcdb_uninstall_message_anti_revoke_trigger(int64 handle, const char* sessionId, _Out_ void** outError)')
1162
+ } catch {
1163
+ this.wcdbUninstallMessageAntiRevokeTrigger = null
1164
+ }
1165
+
1166
+ // wcdb_status wcdb_check_message_anti_revoke_trigger(wcdb_handle handle, const char* session_id, int32_t* out_installed)
1167
+ try {
1168
+ this.wcdbCheckMessageAntiRevokeTrigger = this.lib.func('int32 wcdb_check_message_anti_revoke_trigger(int64 handle, const char* sessionId, _Out_ int32* outInstalled)')
1169
+ } catch {
1170
+ this.wcdbCheckMessageAntiRevokeTrigger = null
1171
+ }
1172
+
1173
+ // wcdb_status wcdb_install_sns_block_delete_trigger(wcdb_handle handle, char** out_error)
1174
+ try {
1175
+ this.wcdbInstallSnsBlockDeleteTrigger = this.lib.func('int32 wcdb_install_sns_block_delete_trigger(int64 handle, _Out_ void** outError)')
1176
+ } catch {
1177
+ this.wcdbInstallSnsBlockDeleteTrigger = null
1178
+ }
1179
+
1180
+ // wcdb_status wcdb_uninstall_sns_block_delete_trigger(wcdb_handle handle, char** out_error)
1181
+ try {
1182
+ this.wcdbUninstallSnsBlockDeleteTrigger = this.lib.func('int32 wcdb_uninstall_sns_block_delete_trigger(int64 handle, _Out_ void** outError)')
1183
+ } catch {
1184
+ this.wcdbUninstallSnsBlockDeleteTrigger = null
1185
+ }
1186
+
1187
+ // wcdb_status wcdb_check_sns_block_delete_trigger(wcdb_handle handle, int32_t* out_installed)
1188
+ try {
1189
+ this.wcdbCheckSnsBlockDeleteTrigger = this.lib.func('int32 wcdb_check_sns_block_delete_trigger(int64 handle, _Out_ int32* outInstalled)')
1190
+ } catch {
1191
+ this.wcdbCheckSnsBlockDeleteTrigger = null
1192
+ }
1193
+
1194
+ // wcdb_status wcdb_delete_sns_post(wcdb_handle handle, const char* post_id, char** out_error)
1195
+ try {
1196
+ this.wcdbDeleteSnsPost = this.lib.func('int32 wcdb_delete_sns_post(int64 handle, const char* postId, _Out_ void** outError)')
1197
+ } catch {
1198
+ this.wcdbDeleteSnsPost = null
1199
+ }
1200
+
1201
+ // Named pipe IPC for monitoring (replaces callback)
1202
+ try {
1203
+ this.wcdbStartMonitorPipe = this.lib.func('int32 wcdb_start_monitor_pipe()')
1204
+ this.wcdbStopMonitorPipe = this.lib.func('void wcdb_stop_monitor_pipe()')
1205
+ this.wcdbGetMonitorPipeName = this.lib.func('int32 wcdb_get_monitor_pipe_name(_Out_ void** outName)')
1206
+ this.writeLog('Monitor pipe functions loaded')
1207
+ } catch (e) {
1208
+ console.warn('Failed to load monitor pipe functions:', e)
1209
+ this.wcdbStartMonitorPipe = null
1210
+ this.wcdbStopMonitorPipe = null
1211
+ this.wcdbGetMonitorPipeName = null
1212
+ }
1213
+
1214
+ // void VerifyUser(int64_t hwnd_ptr, const char* message, char* out_result, int max_len)
1215
+ try {
1216
+ this.wcdbVerifyUser = this.lib.func('void VerifyUser(int64 hwnd, const char* message, _Out_ char* outResult, int maxLen)')
1217
+ } catch {
1218
+ this.wcdbVerifyUser = null
1219
+ }
1220
+
1221
+ // wcdb_status wcdb_cloud_init(int32_t interval_seconds)
1222
+ try {
1223
+ this.wcdbCloudInit = this.lib.func('int32 wcdb_cloud_init(int32 intervalSeconds)')
1224
+ } catch {
1225
+ this.wcdbCloudInit = null
1226
+ }
1227
+
1228
+ // wcdb_status wcdb_cloud_report(const char* stats_json)
1229
+ try {
1230
+ this.wcdbCloudReport = this.lib.func('int32 wcdb_cloud_report(const char* statsJson)')
1231
+ } catch {
1232
+ this.wcdbCloudReport = null
1233
+ }
1234
+
1235
+ // void wcdb_cloud_stop()
1236
+ try {
1237
+ this.wcdbCloudStop = this.lib.func('void wcdb_cloud_stop()')
1238
+ } catch {
1239
+ this.wcdbCloudStop = null
1240
+ }
1241
+
1242
+
1243
+ // 初始化
1244
+ this.writeLog('[bootstrap] calling wcdb_init()', true)
1245
+ const initResult = this.wcdbInit()
1246
+ this.writeLog(`[bootstrap] wcdb_init returned: ${initResult}`, true)
1247
+ if (initResult !== 0) {
1248
+ console.error('WCDB 初始化失败:', initResult)
1249
+ lastDllInitError = this.formatInitProtectionError(initResult)
1250
+ return false
1251
+ }
1252
+
1253
+ this.initialized = true
1254
+ lastDllInitError = null
1255
+ return true
1256
+ } catch (e) {
1257
+ const errorMsg = e instanceof Error ? e.message : String(e)
1258
+ console.error('WCDB 初始化异常:', errorMsg)
1259
+ this.writeLog(`WCDB 初始化异常: ${errorMsg}`, true)
1260
+ lastDllInitError = this.formatInitProtectionError(-2302)
1261
+ return false
1262
+ }
1263
+ }
1264
+
1265
+ /**
1266
+ * 测试数据库连接
1267
+ */
1268
+ async testConnection(dbPath: string, hexKey: string, wxid: string): Promise<{ success: boolean; error?: string; sessionCount?: number }> {
1269
+ try {
1270
+ // 如果当前已经有相同参数的活动连接,直接返回成功
1271
+ if (this.handle !== null &&
1272
+ this.currentPath === dbPath &&
1273
+ this.currentKey === hexKey &&
1274
+ this.currentWxid === wxid) {
1275
+ return { success: true, sessionCount: 0 }
1276
+ }
1277
+
1278
+ // 记录当前活动连接,用于在测试结束后恢复(避免影响聊天页等正在使用的连接)
1279
+ const hadActiveConnection = this.handle !== null
1280
+ const prevPath = this.currentPath
1281
+ const prevKey = this.currentKey
1282
+ const prevWxid = this.currentWxid
1283
+
1284
+ if (!this.initialized) {
1285
+ const initOk = await this.initialize()
1286
+ if (!initOk) {
1287
+ const detailedError = lastDllInitError || this.formatInitProtectionError(-2303)
1288
+ return { success: false, error: detailedError }
1289
+ }
1290
+ }
1291
+
1292
+ // 构建 db_storage 目录路径
1293
+ const dbStoragePath = this.resolveDbStoragePath(dbPath, wxid)
1294
+ this.writeLog(`testConnection dbPath=${dbPath} wxid=${wxid} dbStorage=${dbStoragePath || 'null'}`)
1295
+
1296
+ if (!dbStoragePath || !existsSync(dbStoragePath)) {
1297
+ return { success: false, error: this.formatInitProtectionError(-3001) }
1298
+ }
1299
+
1300
+ // 递归查找 session.db
1301
+ const sessionDbPath = this.findSessionDb(dbStoragePath)
1302
+ this.writeLog(`testConnection sessionDb=${sessionDbPath || 'null'}`)
1303
+
1304
+ if (!sessionDbPath) {
1305
+ return { success: false, error: this.formatInitProtectionError(-3002) }
1306
+ }
1307
+
1308
+ // 分配输出参数内存
1309
+ const handleOut = [0]
1310
+ const result = this.wcdbOpenAccount(sessionDbPath, hexKey, handleOut)
1311
+
1312
+ if (result !== 0) {
1313
+ await this.printLogs()
1314
+ this.writeLog(`testConnection openAccount failed code=${result}`)
1315
+ return { success: false, error: this.formatInitProtectionError(result) }
1316
+ }
1317
+
1318
+ const tempHandle = handleOut[0]
1319
+ if (tempHandle <= 0) {
1320
+ return { success: false, error: this.formatInitProtectionError(-3003) }
1321
+ }
1322
+
1323
+ // 测试成功:使用 shutdown 清理资源(包括测试句柄)
1324
+ // 注意:shutdown 会断开当前活动连接,因此需要在测试后尝试恢复之前的连接
1325
+ try {
1326
+ this.wcdbShutdown()
1327
+ this.handle = null
1328
+ this.currentPath = null
1329
+ this.currentKey = null
1330
+ this.currentWxid = null
1331
+ this.initialized = false
1332
+ } catch (closeErr) {
1333
+ console.error('关闭测试数据库时出错:', closeErr)
1334
+ }
1335
+
1336
+ // 恢复测试前的连接(如果之前有活动连接)
1337
+ if (hadActiveConnection && prevPath && prevKey && prevWxid) {
1338
+ try {
1339
+ await this.open(prevPath, prevKey, prevWxid)
1340
+ } catch {
1341
+ // 恢复失败则保持断开,由调用方处理
1342
+ }
1343
+ }
1344
+
1345
+ return { success: true, sessionCount: 0 }
1346
+ } catch (e) {
1347
+ console.error('测试连接异常:', e)
1348
+ this.writeLog(`testConnection exception: ${String(e)}`)
1349
+ return { success: false, error: this.formatInitProtectionError(-3004) }
1350
+ }
1351
+ }
1352
+
1353
+ /**
1354
+ * 打印数据服务内部日志(仅在出错时调用)
1355
+ */
1356
+ private async printLogs(force = false): Promise<void> {
1357
+ try {
1358
+ if (!this.wcdbGetLogs) return
1359
+ const outPtr = [null as any]
1360
+ const result = this.wcdbGetLogs(outPtr)
1361
+ if (result === 0 && outPtr[0]) {
1362
+ try {
1363
+ const jsonStr = this.koffi.decode(outPtr[0], 'char', -1)
1364
+ this.writeLog(`wcdb_logs: ${jsonStr}`, force)
1365
+ this.wcdbFreeString(outPtr[0])
1366
+ } catch (e) {
1367
+ // ignore
1368
+ }
1369
+ }
1370
+ } catch (e) {
1371
+ console.error('获取日志失败:', e)
1372
+ this.writeLog(`wcdb_logs failed: ${String(e)}`, force)
1373
+ }
1374
+ }
1375
+
1376
+ private startLogPolling(): void {
1377
+ if (this.logTimer || !this.isLogEnabled()) return
1378
+ this.logTimer = setInterval(() => {
1379
+ void this.pollLogs()
1380
+ }, 2000)
1381
+ }
1382
+
1383
+ private stopLogPolling(): void {
1384
+ if (this.logTimer) {
1385
+ clearInterval(this.logTimer)
1386
+ this.logTimer = null
1387
+ }
1388
+ this.lastLogTail = null
1389
+ }
1390
+
1391
+ private async pollLogs(): Promise<void> {
1392
+ try {
1393
+ if (!this.wcdbGetLogs || !this.isLogEnabled()) return
1394
+ const outPtr = [null as any]
1395
+ const result = this.wcdbGetLogs(outPtr)
1396
+ if (result !== 0 || !outPtr[0]) return
1397
+ let jsonStr = ''
1398
+ try {
1399
+ jsonStr = this.koffi.decode(outPtr[0], 'char', -1)
1400
+ } finally {
1401
+ try { this.wcdbFreeString(outPtr[0]) } catch { }
1402
+ }
1403
+ const logs = JSON.parse(jsonStr) as string[]
1404
+ if (!Array.isArray(logs) || logs.length === 0) return
1405
+ let startIdx = 0
1406
+ if (this.lastLogTail) {
1407
+ const idx = logs.lastIndexOf(this.lastLogTail)
1408
+ if (idx >= 0) startIdx = idx + 1
1409
+ }
1410
+ for (let i = startIdx; i < logs.length; i += 1) {
1411
+ this.writeLog(`wcdb: ${logs[i]}`)
1412
+ }
1413
+ this.lastLogTail = logs[logs.length - 1]
1414
+ } catch (e) {
1415
+ // ignore polling errors
1416
+ }
1417
+ }
1418
+
1419
+ private decodeJsonPtr(outPtr: any): string | null {
1420
+ if (!outPtr) return null
1421
+ try {
1422
+ const jsonStr = this.koffi.decode(outPtr, 'char', -1)
1423
+ this.wcdbFreeString(outPtr)
1424
+ return jsonStr
1425
+ } catch (e) {
1426
+ try { this.wcdbFreeString(outPtr) } catch { }
1427
+ return null
1428
+ }
1429
+ }
1430
+
1431
+ private parseMessageJson(jsonStr: string): any {
1432
+ const raw = String(jsonStr || '')
1433
+ if (!raw) return []
1434
+ // 热路径优化:仅在检测到 16+ 位整数字段时才进行字符串包裹,避免每批次多轮全量 replace。
1435
+ const needsInt64Normalize = /"server_id"\s*:\s*-?\d{16,}/.test(raw)
1436
+ if (!needsInt64Normalize) {
1437
+ return JSON.parse(raw)
1438
+ }
1439
+ const normalized = raw.replace(
1440
+ /("server_id"\s*:\s*)(-?\d{16,})/g,
1441
+ '$1"$2"'
1442
+ )
1443
+ return JSON.parse(normalized)
1444
+ }
1445
+
1446
+ private ensureReady(): boolean {
1447
+ return this.initialized && this.handle !== null
1448
+ }
1449
+
1450
+ private normalizeTimestamp(input: number): number {
1451
+ if (!input || input <= 0) return 0
1452
+ const asNumber = Number(input)
1453
+ if (!Number.isFinite(asNumber)) return 0
1454
+ // Treat >1e12 as milliseconds.
1455
+ const seconds = asNumber > 1e12 ? Math.floor(asNumber / 1000) : Math.floor(asNumber)
1456
+ const maxInt32 = 2147483647
1457
+ return Math.min(Math.max(seconds, 0), maxInt32)
1458
+ }
1459
+
1460
+ private normalizeRange(beginTimestamp: number, endTimestamp: number): { begin: number; end: number } {
1461
+ const normalizedBegin = this.normalizeTimestamp(beginTimestamp)
1462
+ let normalizedEnd = this.normalizeTimestamp(endTimestamp)
1463
+ if (normalizedEnd <= 0) {
1464
+ normalizedEnd = this.normalizeTimestamp(Date.now())
1465
+ }
1466
+ if (normalizedBegin > 0 && normalizedEnd < normalizedBegin) {
1467
+ normalizedEnd = normalizedBegin
1468
+ }
1469
+ return { begin: normalizedBegin, end: normalizedEnd }
1470
+ }
1471
+
1472
+ private makeHardlinkCacheKey(primary: string, secondary?: string | null): string {
1473
+ const a = String(primary || '').trim().toLowerCase()
1474
+ const b = String(secondary || '').trim().toLowerCase()
1475
+ return `${a}\u001f${b}`
1476
+ }
1477
+
1478
+ private readHardlinkCache(
1479
+ cache: Map<string, { result: { success: boolean; data?: any; error?: string }; updatedAt: number }>,
1480
+ key: string
1481
+ ): { success: boolean; data?: any; error?: string } | null {
1482
+ const entry = cache.get(key)
1483
+ if (!entry) return null
1484
+ if (Date.now() - entry.updatedAt > this.hardlinkCacheTtlMs) {
1485
+ cache.delete(key)
1486
+ return null
1487
+ }
1488
+ return this.cloneHardlinkResult(entry.result)
1489
+ }
1490
+
1491
+ private writeHardlinkCache(
1492
+ cache: Map<string, { result: { success: boolean; data?: any; error?: string }; updatedAt: number }>,
1493
+ key: string,
1494
+ result: { success: boolean; data?: any; error?: string }
1495
+ ): void {
1496
+ cache.set(key, {
1497
+ result: this.cloneHardlinkResult(result),
1498
+ updatedAt: Date.now()
1499
+ })
1500
+ if (cache.size <= this.hardlinkCacheMaxEntries) return
1501
+
1502
+ const now = Date.now()
1503
+ for (const [cacheKey, entry] of cache) {
1504
+ if (now - entry.updatedAt > this.hardlinkCacheTtlMs) {
1505
+ cache.delete(cacheKey)
1506
+ }
1507
+ }
1508
+
1509
+ while (cache.size > this.hardlinkCacheMaxEntries) {
1510
+ const oldestKey = cache.keys().next().value as string | undefined
1511
+ if (!oldestKey) break
1512
+ cache.delete(oldestKey)
1513
+ }
1514
+ }
1515
+
1516
+ private cloneHardlinkResult(result: { success: boolean; data?: any; error?: string }): { success: boolean; data?: any; error?: string } {
1517
+ const data = result.data && typeof result.data === 'object'
1518
+ ? { ...result.data }
1519
+ : result.data
1520
+ return {
1521
+ success: result.success === true,
1522
+ data,
1523
+ error: result.error
1524
+ }
1525
+ }
1526
+
1527
+ private clearHardlinkCaches(): void {
1528
+ this.imageHardlinkCache.clear()
1529
+ this.videoHardlinkCache.clear()
1530
+ }
1531
+
1532
+ private clearMediaStreamSessionCache(): void {
1533
+ this.mediaStreamSessionCache = null
1534
+ this.mediaStreamSessionCacheAt = 0
1535
+ }
1536
+
1537
+ isReady(): boolean {
1538
+ return this.ensureReady()
1539
+ }
1540
+
1541
+ /**
1542
+ * 打开数据库
1543
+ */
1544
+ async open(dbPath: string, hexKey: string, wxid: string): Promise<boolean> {
1545
+ try {
1546
+ lastDllInitError = null
1547
+ if (!this.initialized) {
1548
+ const initOk = await this.initialize()
1549
+ if (!initOk) return false
1550
+ }
1551
+
1552
+ // 检查是否已经是当前连接的参数,如果是则直接返回成功,实现"始终保持链接"
1553
+ if (this.handle !== null &&
1554
+ this.currentPath === dbPath &&
1555
+ this.currentKey === hexKey &&
1556
+ this.currentWxid === wxid) {
1557
+ return true
1558
+ }
1559
+
1560
+ // 如果参数不同,则先关闭原来的连接
1561
+ if (this.handle !== null) {
1562
+ this.close()
1563
+ // 重新初始化,因为 close 呼叫了 shutdown
1564
+ const initOk = await this.initialize()
1565
+ if (!initOk) return false
1566
+ }
1567
+
1568
+ const dbStoragePath = this.resolveDbStoragePath(dbPath, wxid)
1569
+ this.writeLog(`open dbPath=${dbPath} wxid=${wxid} dbStorage=${dbStoragePath || 'null'}`, true)
1570
+
1571
+ if (!dbStoragePath || !existsSync(dbStoragePath)) {
1572
+ console.error('数据库目录不存在:', dbPath)
1573
+ this.writeLog(`open failed: dbStorage not found for ${dbPath}`)
1574
+ lastDllInitError = this.formatInitProtectionError(-3001)
1575
+ return false
1576
+ }
1577
+
1578
+ const sessionDbPath = this.findSessionDb(dbStoragePath)
1579
+ this.writeLog(`open sessionDb=${sessionDbPath || 'null'}`, true)
1580
+ if (!sessionDbPath) {
1581
+ console.error('未找到 session.db 文件')
1582
+ this.writeLog('open failed: session.db not found')
1583
+ lastDllInitError = this.formatInitProtectionError(-3002)
1584
+ return false
1585
+ }
1586
+
1587
+ const handleOut = [0]
1588
+ const result = this.wcdbOpenAccount(sessionDbPath, hexKey, handleOut)
1589
+
1590
+ if (result !== 0) {
1591
+ console.error('打开数据库失败:', result)
1592
+ await this.printLogs()
1593
+ this.writeLog(`open failed: openAccount code=${result}`)
1594
+ lastDllInitError = this.formatInitProtectionError(result)
1595
+ return false
1596
+ }
1597
+
1598
+ const handle = handleOut[0]
1599
+ if (handle <= 0) {
1600
+ lastDllInitError = this.formatInitProtectionError(-3003)
1601
+ return false
1602
+ }
1603
+
1604
+ this.handle = handle
1605
+ this.currentPath = dbPath
1606
+ this.currentKey = hexKey
1607
+ this.currentWxid = wxid
1608
+ this.currentDbStoragePath = dbStoragePath
1609
+ this.initialized = true
1610
+ lastDllInitError = null
1611
+ if (this.wcdbSetMyWxid && wxid) {
1612
+ try {
1613
+ this.wcdbSetMyWxid(this.handle, wxid)
1614
+ } catch (e) {
1615
+ // 静默失败
1616
+ }
1617
+ }
1618
+ if (this.isLogEnabled()) {
1619
+ this.startLogPolling()
1620
+ }
1621
+ this.writeLog(`open ok handle=${handle}`, true)
1622
+ await this.dumpDbStatus('open')
1623
+ await this.runPostOpenDiagnostics(dbPath, dbStoragePath, sessionDbPath, wxid)
1624
+ return true
1625
+ } catch (e) {
1626
+ console.error('打开数据库异常:', e)
1627
+ this.writeLog(`open exception: ${String(e)}`)
1628
+ lastDllInitError = this.formatInitProtectionError(-3004)
1629
+ return false
1630
+ }
1631
+ }
1632
+
1633
+ /**
1634
+ * 关闭数据库
1635
+ * 注意:wcdb_close_account 可能导致崩溃,使用 shutdown 代替
1636
+ */
1637
+ close(): void {
1638
+ if (this.handle !== null || this.initialized) {
1639
+ // 先停止监控与云控回调,避免 shutdown 后仍有 native 回调访问已释放资源。
1640
+ try { this.stopMonitor() } catch {}
1641
+ try { this.cloudStop() } catch {}
1642
+ try {
1643
+ // 不调用 closeAccount,直接 shutdown
1644
+ this.wcdbShutdown()
1645
+ } catch (e) {
1646
+ console.error('WCDB shutdown 出错:', e)
1647
+ }
1648
+ this.handle = null
1649
+ this.currentPath = null
1650
+ this.currentKey = null
1651
+ this.currentWxid = null
1652
+ this.currentDbStoragePath = null
1653
+ this.initialized = false
1654
+ this.clearHardlinkCaches()
1655
+ this.clearMediaStreamSessionCache()
1656
+ this.stopLogPolling()
1657
+ }
1658
+ }
1659
+
1660
+ /**
1661
+ * 关闭服务(与 close 相同)
1662
+ */
1663
+ shutdown(): void {
1664
+ this.close()
1665
+ }
1666
+
1667
+ /**
1668
+ * 检查是否已连接
1669
+ */
1670
+ isConnected(): boolean {
1671
+ return this.initialized && this.handle !== null
1672
+ }
1673
+
1674
+ async getSessions(): Promise<{ success: boolean; sessions?: any[]; error?: string }> {
1675
+ if (!this.ensureReady()) {
1676
+ this.writeLog('getSessions skipped: not connected')
1677
+ return { success: false, error: 'WCDB 未连接' }
1678
+ }
1679
+ try {
1680
+ // 使用 setImmediate 让事件循环有机会处理其他任务,避免长时间阻塞
1681
+ await new Promise(resolve => setImmediate(resolve))
1682
+
1683
+ const outPtr = [null as any]
1684
+ const result = this.wcdbGetSessions(this.handle, outPtr)
1685
+
1686
+ //数据服务调用后再次让出控制权
1687
+ await new Promise(resolve => setImmediate(resolve))
1688
+
1689
+ if (result !== 0 || !outPtr[0]) {
1690
+ this.writeLog(`getSessions failed: code=${result}`)
1691
+ return { success: false, error: `获取会话失败: ${result}` }
1692
+ }
1693
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
1694
+ if (!jsonStr) return { success: false, error: '解析会话失败' }
1695
+ this.writeLog(`getSessions ok size=${jsonStr.length}`)
1696
+ const sessions = JSON.parse(jsonStr)
1697
+ return { success: true, sessions }
1698
+ } catch (e) {
1699
+ this.writeLog(`getSessions exception: ${String(e)}`)
1700
+ return { success: false, error: String(e) }
1701
+ }
1702
+ }
1703
+
1704
+ async getMessages(sessionId: string, limit: number, offset: number): Promise<{ success: boolean; messages?: any[]; error?: string }> {
1705
+ if (!this.ensureReady()) {
1706
+ return { success: false, error: 'WCDB 未连接' }
1707
+ }
1708
+ try {
1709
+ // limit=0 means no limit, pass large number to native API
1710
+ const effectiveLimit = limit > 0 ? limit : 999999999
1711
+ const outPtr = [null as any]
1712
+ const result = this.wcdbGetMessages(this.handle, sessionId, effectiveLimit, offset, outPtr)
1713
+ if (result !== 0 || !outPtr[0]) {
1714
+ return { success: false, error: `获取消息失败: ${result}` }
1715
+ }
1716
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
1717
+ if (!jsonStr) return { success: false, error: '解析消息失败' }
1718
+ const messages = this.parseMessageJson(jsonStr)
1719
+ return { success: true, messages }
1720
+ } catch (e) {
1721
+ return { success: false, error: String(e) }
1722
+ }
1723
+ }
1724
+
1725
+ /**
1726
+ * 获取指定时间之后的新消息
1727
+ */
1728
+ async getNewMessages(sessionId: string, minTime: number, limit: number = 1000): Promise<{ success: boolean; messages?: any[]; error?: string }> {
1729
+ if (!this.ensureReady()) {
1730
+ return { success: false, error: 'WCDB 未连接' }
1731
+ }
1732
+ try {
1733
+ // 1. 打开游标 (使用 Ascending=1 从指定时间往后查)
1734
+ const openRes = await this.openMessageCursor(sessionId, limit, true, minTime, 0)
1735
+ if (!openRes.success || !openRes.cursor) {
1736
+ return { success: false, error: openRes.error }
1737
+ }
1738
+
1739
+ const cursor = openRes.cursor
1740
+ try {
1741
+ // 2. 获取批次
1742
+ const fetchRes = await this.fetchMessageBatch(cursor)
1743
+ if (!fetchRes.success) {
1744
+ return { success: false, error: fetchRes.error }
1745
+ }
1746
+ return { success: true, messages: fetchRes.rows }
1747
+ } finally {
1748
+ // 3. 关闭游标
1749
+ await this.closeMessageCursor(cursor)
1750
+ }
1751
+ } catch (e) {
1752
+ return { success: false, error: String(e) }
1753
+ }
1754
+ }
1755
+
1756
+ async getMessageCount(sessionId: string): Promise<{ success: boolean; count?: number; error?: string }> {
1757
+ if (!this.ensureReady()) {
1758
+ return { success: false, error: 'WCDB 未连接' }
1759
+ }
1760
+ try {
1761
+ const outCount = [0]
1762
+ const result = this.wcdbGetMessageCount(this.handle, sessionId, outCount)
1763
+ if (result !== 0) {
1764
+ if (result === -7) {
1765
+ return { success: false, error: 'message schema mismatch:当前账号消息表结构与程序要求不一致' }
1766
+ }
1767
+ return { success: false, error: `获取消息总数失败: ${result}` }
1768
+ }
1769
+ return { success: true, count: outCount[0] }
1770
+ } catch (e) {
1771
+ return { success: false, error: String(e) }
1772
+ }
1773
+ }
1774
+
1775
+ async getMessageCounts(sessionIds: string[]): Promise<{ success: boolean; counts?: Record<string, number>; error?: string }> {
1776
+ if (!this.ensureReady()) {
1777
+ return { success: false, error: 'WCDB 未连接' }
1778
+ }
1779
+
1780
+ const normalizedSessionIds = Array.from(
1781
+ new Set(
1782
+ (sessionIds || [])
1783
+ .map((id) => String(id || '').trim())
1784
+ .filter(Boolean)
1785
+ )
1786
+ )
1787
+ if (normalizedSessionIds.length === 0) {
1788
+ return { success: true, counts: {} }
1789
+ }
1790
+
1791
+ try {
1792
+ const counts: Record<string, number> = {}
1793
+ for (let i = 0; i < normalizedSessionIds.length; i += 1) {
1794
+ const sessionId = normalizedSessionIds[i]
1795
+ const outCount = [0]
1796
+ const result = this.wcdbGetMessageCount(this.handle, sessionId, outCount)
1797
+ if (result === -7) {
1798
+ return { success: false, error: `message schema mismatch:会话 ${sessionId} 的消息表结构不匹配` }
1799
+ }
1800
+ counts[sessionId] = result === 0 && Number.isFinite(outCount[0]) ? Math.max(0, Math.floor(outCount[0])) : 0
1801
+
1802
+ if (i > 0 && i % 160 === 0) {
1803
+ await new Promise(resolve => setImmediate(resolve))
1804
+ }
1805
+ }
1806
+ return { success: true, counts }
1807
+ } catch (e) {
1808
+ return { success: false, error: String(e) }
1809
+ }
1810
+ }
1811
+
1812
+ async getSessionMessageCounts(sessionIds: string[]): Promise<{ success: boolean; counts?: Record<string, number>; error?: string }> {
1813
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
1814
+ if (!this.wcdbGetSessionMessageCounts) return this.getMessageCounts(sessionIds)
1815
+ try {
1816
+ const outPtr = [null as any]
1817
+ const result = this.wcdbGetSessionMessageCounts(this.handle, JSON.stringify(sessionIds || []), outPtr)
1818
+ if (result !== 0 || !outPtr[0]) {
1819
+ if (result === -7) {
1820
+ return { success: false, error: 'message schema mismatch:当前账号消息表结构与程序要求不一致' }
1821
+ }
1822
+ return { success: false, error: `获取会话消息总数失败: ${result}` }
1823
+ }
1824
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
1825
+ if (!jsonStr) return { success: false, error: '解析会话消息总数失败' }
1826
+ const raw = JSON.parse(jsonStr) || {}
1827
+ const counts: Record<string, number> = {}
1828
+ for (const sid of sessionIds || []) {
1829
+ const value = Number(raw?.[sid] ?? 0)
1830
+ counts[sid] = Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0
1831
+ }
1832
+ return { success: true, counts }
1833
+ } catch (e) {
1834
+ return { success: false, error: String(e) }
1835
+ }
1836
+ }
1837
+
1838
+ async getSessionMessageTypeStats(
1839
+ sessionId: string,
1840
+ beginTimestamp: number = 0,
1841
+ endTimestamp: number = 0
1842
+ ): Promise<{ success: boolean; data?: any; error?: string }> {
1843
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
1844
+ if (!this.wcdbGetSessionMessageTypeStats) return { success: false, error: '接口未就绪' }
1845
+ try {
1846
+ const outPtr = [null as any]
1847
+ const result = this.wcdbGetSessionMessageTypeStats(
1848
+ this.handle,
1849
+ sessionId,
1850
+ this.normalizeTimestamp(beginTimestamp),
1851
+ this.normalizeTimestamp(endTimestamp),
1852
+ outPtr
1853
+ )
1854
+ if (result !== 0 || !outPtr[0]) {
1855
+ return { success: false, error: `获取会话类型统计失败: ${result}` }
1856
+ }
1857
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
1858
+ if (!jsonStr) return { success: false, error: '解析会话类型统计失败' }
1859
+ return { success: true, data: JSON.parse(jsonStr) || {} }
1860
+ } catch (e) {
1861
+ return { success: false, error: String(e) }
1862
+ }
1863
+ }
1864
+
1865
+ async getSessionMessageTypeStatsBatch(
1866
+ sessionIds: string[],
1867
+ options?: {
1868
+ beginTimestamp?: number
1869
+ endTimestamp?: number
1870
+ quickMode?: boolean
1871
+ includeGroupSenderCount?: boolean
1872
+ }
1873
+ ): Promise<{ success: boolean; data?: Record<string, any>; error?: string }> {
1874
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
1875
+ const normalizedSessionIds = Array.from(new Set((sessionIds || []).map((id) => String(id || '').trim()).filter(Boolean)))
1876
+ if (normalizedSessionIds.length === 0) return { success: true, data: {} }
1877
+
1878
+ if (!this.wcdbGetSessionMessageTypeStatsBatch) {
1879
+ const data: Record<string, any> = {}
1880
+ for (const sessionId of normalizedSessionIds) {
1881
+ const single = await this.getSessionMessageTypeStats(
1882
+ sessionId,
1883
+ options?.beginTimestamp || 0,
1884
+ options?.endTimestamp || 0
1885
+ )
1886
+ if (single.success) {
1887
+ data[sessionId] = single.data || {}
1888
+ }
1889
+ }
1890
+ return { success: true, data }
1891
+ }
1892
+
1893
+ try {
1894
+ const outPtr = [null as any]
1895
+ const optionsJson = JSON.stringify({
1896
+ begin: this.normalizeTimestamp(options?.beginTimestamp || 0),
1897
+ end: this.normalizeTimestamp(options?.endTimestamp || 0),
1898
+ quick_mode: options?.quickMode === true,
1899
+ include_group_sender_count: options?.includeGroupSenderCount !== false
1900
+ })
1901
+ const result = this.wcdbGetSessionMessageTypeStatsBatch(
1902
+ this.handle,
1903
+ JSON.stringify(normalizedSessionIds),
1904
+ optionsJson,
1905
+ outPtr
1906
+ )
1907
+ if (result !== 0 || !outPtr[0]) return { success: false, error: `批量获取会话类型统计失败: ${result}` }
1908
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
1909
+ if (!jsonStr) return { success: false, error: '解析批量会话类型统计失败' }
1910
+ return { success: true, data: JSON.parse(jsonStr) || {} }
1911
+ } catch (e) {
1912
+ return { success: false, error: String(e) }
1913
+ }
1914
+ }
1915
+
1916
+ async getSessionMessageDateCounts(sessionId: string): Promise<{ success: boolean; counts?: Record<string, number>; error?: string }> {
1917
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
1918
+ if (!this.wcdbGetSessionMessageDateCounts) return { success: false, error: '接口未就绪' }
1919
+ try {
1920
+ const outPtr = [null as any]
1921
+ const result = this.wcdbGetSessionMessageDateCounts(this.handle, sessionId, outPtr)
1922
+ if (result !== 0 || !outPtr[0]) return { success: false, error: `获取会话日消息统计失败: ${result}` }
1923
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
1924
+ if (!jsonStr) return { success: false, error: '解析会话日消息统计失败' }
1925
+ const raw = JSON.parse(jsonStr) || {}
1926
+ const counts: Record<string, number> = {}
1927
+ for (const [dateKey, value] of Object.entries(raw)) {
1928
+ const count = Number(value)
1929
+ if (!dateKey || !Number.isFinite(count) || count <= 0) continue
1930
+ counts[String(dateKey)] = Math.floor(count)
1931
+ }
1932
+ return { success: true, counts }
1933
+ } catch (e) {
1934
+ return { success: false, error: String(e) }
1935
+ }
1936
+ }
1937
+
1938
+ async getSessionMessageDateCountsBatch(sessionIds: string[]): Promise<{ success: boolean; data?: Record<string, Record<string, number>>; error?: string }> {
1939
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
1940
+ const normalizedSessionIds = Array.from(new Set((sessionIds || []).map((id) => String(id || '').trim()).filter(Boolean)))
1941
+ if (normalizedSessionIds.length === 0) return { success: true, data: {} }
1942
+
1943
+ if (!this.wcdbGetSessionMessageDateCountsBatch) {
1944
+ const data: Record<string, Record<string, number>> = {}
1945
+ for (const sessionId of normalizedSessionIds) {
1946
+ const single = await this.getSessionMessageDateCounts(sessionId)
1947
+ data[sessionId] = single.success && single.counts ? single.counts : {}
1948
+ }
1949
+ return { success: true, data }
1950
+ }
1951
+
1952
+ try {
1953
+ const outPtr = [null as any]
1954
+ const result = this.wcdbGetSessionMessageDateCountsBatch(this.handle, JSON.stringify(normalizedSessionIds), outPtr)
1955
+ if (result !== 0 || !outPtr[0]) return { success: false, error: `批量获取会话日消息统计失败: ${result}` }
1956
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
1957
+ if (!jsonStr) return { success: false, error: '解析批量会话日消息统计失败' }
1958
+ const raw = JSON.parse(jsonStr) || {}
1959
+ const data: Record<string, Record<string, number>> = {}
1960
+ for (const sessionId of normalizedSessionIds) {
1961
+ const source = raw?.[sessionId] || {}
1962
+ const next: Record<string, number> = {}
1963
+ for (const [dateKey, value] of Object.entries(source)) {
1964
+ const count = Number(value)
1965
+ if (!dateKey || !Number.isFinite(count) || count <= 0) continue
1966
+ next[String(dateKey)] = Math.floor(count)
1967
+ }
1968
+ data[sessionId] = next
1969
+ }
1970
+ return { success: true, data }
1971
+ } catch (e) {
1972
+ return { success: false, error: String(e) }
1973
+ }
1974
+ }
1975
+
1976
+ async getMessagesByType(
1977
+ sessionId: string,
1978
+ localType: number,
1979
+ ascending = false,
1980
+ limit = 0,
1981
+ offset = 0
1982
+ ): Promise<{ success: boolean; rows?: any[]; error?: string }> {
1983
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
1984
+ if (!this.wcdbGetMessagesByType) return { success: false, error: '接口未就绪' }
1985
+ try {
1986
+ const outPtr = [null as any]
1987
+ const result = this.wcdbGetMessagesByType(
1988
+ this.handle,
1989
+ sessionId,
1990
+ BigInt(localType),
1991
+ ascending ? 1 : 0,
1992
+ Math.max(0, Math.floor(limit || 0)),
1993
+ Math.max(0, Math.floor(offset || 0)),
1994
+ outPtr
1995
+ )
1996
+ if (result !== 0 || !outPtr[0]) return { success: false, error: `按类型读取消息失败: ${result}` }
1997
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
1998
+ if (!jsonStr) return { success: false, error: '解析按类型消息失败' }
1999
+ const rows = JSON.parse(jsonStr)
2000
+ return { success: true, rows: Array.isArray(rows) ? rows : [] }
2001
+ } catch (e) {
2002
+ return { success: false, error: String(e) }
2003
+ }
2004
+ }
2005
+
2006
+ async getMediaStream(options?: {
2007
+ sessionId?: string
2008
+ mediaType?: 'image' | 'video' | 'all'
2009
+ beginTimestamp?: number
2010
+ endTimestamp?: number
2011
+ limit?: number
2012
+ offset?: number
2013
+ }): Promise<{
2014
+ success: boolean
2015
+ items?: Array<{
2016
+ sessionId: string
2017
+ sessionDisplayName?: string
2018
+ mediaType: 'image' | 'video'
2019
+ localId: number
2020
+ serverId?: string
2021
+ createTime: number
2022
+ localType: number
2023
+ senderUsername?: string
2024
+ isSend?: number | null
2025
+ imageMd5?: string
2026
+ imageDatName?: string
2027
+ videoMd5?: string
2028
+ content?: string
2029
+ }>
2030
+ hasMore?: boolean
2031
+ nextOffset?: number
2032
+ error?: string
2033
+ }> {
2034
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
2035
+ if (!this.wcdbScanMediaStream) return { success: false, error: '当前数据服务版本不支持资源扫描,请先更新 wcdb 数据服务' }
2036
+ try {
2037
+ const toInt = (value: unknown): number => {
2038
+ const n = Number(value || 0)
2039
+ if (!Number.isFinite(n)) return 0
2040
+ return Math.floor(n)
2041
+ }
2042
+ const pickString = (row: Record<string, any>, keys: string[]): string => {
2043
+ for (const key of keys) {
2044
+ const value = row[key]
2045
+ if (value === null || value === undefined) continue
2046
+ const text = String(value).trim()
2047
+ if (text) return text
2048
+ }
2049
+ return ''
2050
+ }
2051
+ const pickRaw = (row: Record<string, any>, keys: string[]): unknown => {
2052
+ for (const key of keys) {
2053
+ const value = row[key]
2054
+ if (value === null || value === undefined) continue
2055
+ return value
2056
+ }
2057
+ return undefined
2058
+ }
2059
+ const extractXmlValue = (xml: string, tag: string): string => {
2060
+ if (!xml) return ''
2061
+ const regex = new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`, 'i')
2062
+ const match = regex.exec(xml)
2063
+ if (!match) return ''
2064
+ return String(match[1] || '').replace(/<!\[CDATA\[/g, '').replace(/\]\]>/g, '').trim()
2065
+ }
2066
+ const looksLikeHex = (text: string): boolean => {
2067
+ if (!text || text.length < 2 || text.length % 2 !== 0) return false
2068
+ return /^[0-9a-fA-F]+$/.test(text)
2069
+ }
2070
+ const looksLikeBase64 = (text: string): boolean => {
2071
+ if (!text || text.length < 16 || text.length % 4 !== 0) return false
2072
+ return /^[A-Za-z0-9+/]+={0,2}$/.test(text)
2073
+ }
2074
+ const decodeBinaryContent = (data: Buffer, fallbackValue?: string): string => {
2075
+ if (!data || data.length === 0) return ''
2076
+ try {
2077
+ if (data.length >= 4) {
2078
+ const magicLE = data.readUInt32LE(0)
2079
+ const magicBE = data.readUInt32BE(0)
2080
+ if (magicLE === 0xFD2FB528 || magicBE === 0xFD2FB528) {
2081
+ try {
2082
+ const decompressed = fzstd.decompress(data)
2083
+ return Buffer.from(decompressed).toString('utf-8')
2084
+ } catch {
2085
+ // ignore
2086
+ }
2087
+ }
2088
+ }
2089
+ const decoded = data.toString('utf-8')
2090
+ const replacementCount = (decoded.match(/\uFFFD/g) || []).length
2091
+ if (replacementCount < decoded.length * 0.2) {
2092
+ return decoded.replace(/\uFFFD/g, '')
2093
+ }
2094
+ if (fallbackValue && replacementCount > 0) return fallbackValue
2095
+ return data.toString('latin1')
2096
+ } catch {
2097
+ return fallbackValue || ''
2098
+ }
2099
+ }
2100
+ const decodeMaybeCompressed = (raw: unknown): string => {
2101
+ if (raw === null || raw === undefined) return ''
2102
+ if (Buffer.isBuffer(raw) || raw instanceof Uint8Array) {
2103
+ return decodeBinaryContent(Buffer.from(raw as any), String(raw))
2104
+ }
2105
+ const text = String(raw).trim()
2106
+ if (!text) return ''
2107
+
2108
+ if (text.length > 16 && looksLikeHex(text)) {
2109
+ try {
2110
+ const bytes = Buffer.from(text, 'hex')
2111
+ if (bytes.length > 0) return decodeBinaryContent(bytes, text)
2112
+ } catch {
2113
+ // ignore
2114
+ }
2115
+ }
2116
+ if (text.length > 16 && looksLikeBase64(text)) {
2117
+ try {
2118
+ const bytes = Buffer.from(text, 'base64')
2119
+ if (bytes.length > 0) return decodeBinaryContent(bytes, text)
2120
+ } catch {
2121
+ // ignore
2122
+ }
2123
+ }
2124
+ return text
2125
+ }
2126
+ const decodeMessageContent = (messageContent: unknown, compressContent: unknown): string => {
2127
+ const compressedDecoded = decodeMaybeCompressed(compressContent)
2128
+ if (compressedDecoded) return compressedDecoded
2129
+ return decodeMaybeCompressed(messageContent)
2130
+ }
2131
+ const extractImageMd5 = (xml: string): string => {
2132
+ const byTag = extractXmlValue(xml, 'md5') || extractXmlValue(xml, 'imgmd5')
2133
+ if (byTag) return byTag
2134
+ const byAttr = /(?:md5|imgmd5)\s*=\s*['"]?([a-fA-F0-9]{16,64})['"]?/i.exec(xml)
2135
+ return byAttr?.[1] || ''
2136
+ }
2137
+ const normalizeDatBase = (value: string): string => {
2138
+ const input = String(value || '').trim()
2139
+ if (!input) return ''
2140
+ const fileBase = input.replace(/^.*[\\/]/, '').replace(/\.(?:t\.)?dat$/i, '')
2141
+ const md5Like = /([0-9a-fA-F]{16,64})/.exec(fileBase)
2142
+ return String(md5Like?.[1] || fileBase || '').trim().toLowerCase()
2143
+ }
2144
+ const decodePackedInfoBuffer = (raw: unknown): Buffer | null => {
2145
+ if (!raw) return null
2146
+ if (Buffer.isBuffer(raw)) return raw
2147
+ if (raw instanceof Uint8Array) return Buffer.from(raw)
2148
+ if (Array.isArray(raw)) return Buffer.from(raw as any[])
2149
+ if (typeof raw === 'string') {
2150
+ const text = raw.trim()
2151
+ if (!text) return null
2152
+ const compactHex = text.replace(/\s+/g, '')
2153
+ if (/^[a-fA-F0-9]+$/.test(compactHex) && compactHex.length % 2 === 0) {
2154
+ try {
2155
+ return Buffer.from(compactHex, 'hex')
2156
+ } catch {
2157
+ // ignore
2158
+ }
2159
+ }
2160
+ try {
2161
+ const base64 = Buffer.from(text, 'base64')
2162
+ if (base64.length > 0) return base64
2163
+ } catch {
2164
+ // ignore
2165
+ }
2166
+ return null
2167
+ }
2168
+ if (typeof raw === 'object' && raw !== null && Array.isArray((raw as any).data)) {
2169
+ return Buffer.from((raw as any).data)
2170
+ }
2171
+ return null
2172
+ }
2173
+ const decodePackedToPrintable = (raw: unknown): string => {
2174
+ const buf = decodePackedInfoBuffer(raw)
2175
+ if (!buf || buf.length === 0) return ''
2176
+ const printable: number[] = []
2177
+ for (const byte of buf) {
2178
+ if (byte >= 0x20 && byte <= 0x7e) printable.push(byte)
2179
+ else printable.push(0x20)
2180
+ }
2181
+ return Buffer.from(printable).toString('utf-8')
2182
+ }
2183
+ const extractHexMd5 = (text: string): string => {
2184
+ const input = String(text || '')
2185
+ if (!input) return ''
2186
+ const match = /([a-fA-F0-9]{32})/.exec(input)
2187
+ return String(match?.[1] || '').toLowerCase()
2188
+ }
2189
+ const normalizeVideoFileToken = (value: unknown): string => {
2190
+ let text = String(value || '').trim().toLowerCase()
2191
+ if (!text) return ''
2192
+ text = text.replace(/^.*[\\/]/, '')
2193
+ text = text.replace(/\.(?:mp4|mov|m4v|avi|mkv|flv|jpg|jpeg|png|gif|dat)$/i, '')
2194
+ text = text.replace(/_thumb$/, '')
2195
+ const direct = /^([a-f0-9]{16,64})(?:_raw)?$/i.exec(text)
2196
+ if (direct) {
2197
+ const suffix = /_raw$/i.test(text) ? '_raw' : ''
2198
+ return `${direct[1].toLowerCase()}${suffix}`
2199
+ }
2200
+ const preferred32 = /([a-f0-9]{32})(?![a-f0-9])/i.exec(text)
2201
+ if (preferred32?.[1]) return preferred32[1].toLowerCase()
2202
+ const fallback = /([a-f0-9]{16,64})(?![a-f0-9])/i.exec(text)
2203
+ return String(fallback?.[1] || '').toLowerCase()
2204
+ }
2205
+ const extractVideoFileNameFromPackedRaw = (raw: unknown): string => {
2206
+ const buf = decodePackedInfoBuffer(raw)
2207
+ if (!buf || buf.length === 0) return ''
2208
+ const candidates: string[] = []
2209
+ let current = ''
2210
+ for (const byte of buf) {
2211
+ const isHex =
2212
+ (byte >= 0x30 && byte <= 0x39) ||
2213
+ (byte >= 0x41 && byte <= 0x46) ||
2214
+ (byte >= 0x61 && byte <= 0x66)
2215
+ if (isHex) {
2216
+ current += String.fromCharCode(byte)
2217
+ continue
2218
+ }
2219
+ if (current.length >= 16) candidates.push(current)
2220
+ current = ''
2221
+ }
2222
+ if (current.length >= 16) candidates.push(current)
2223
+ if (candidates.length === 0) return ''
2224
+ const exact32 = candidates.find((item) => item.length === 32)
2225
+ if (exact32) return exact32.toLowerCase()
2226
+ const fallback = candidates.find((item) => item.length >= 16 && item.length <= 64)
2227
+ return String(fallback || '').toLowerCase()
2228
+ }
2229
+ const extractImageDatName = (row: Record<string, any>, content: string): string => {
2230
+ const direct = pickString(row, [
2231
+ 'image_path',
2232
+ 'imagePath',
2233
+ 'image_dat_name',
2234
+ 'imageDatName',
2235
+ 'img_path',
2236
+ 'imgPath',
2237
+ 'img_name',
2238
+ 'imgName'
2239
+ ])
2240
+ const normalizedDirect = normalizeDatBase(direct)
2241
+ if (normalizedDirect) return normalizedDirect
2242
+
2243
+ const xmlCandidate = extractXmlValue(content, 'imgname') || extractXmlValue(content, 'cdnmidimgurl')
2244
+ const normalizedXml = normalizeDatBase(xmlCandidate)
2245
+ if (normalizedXml) return normalizedXml
2246
+
2247
+ const packedRaw = pickRaw(row, [
2248
+ 'packed_info_data',
2249
+ 'packedInfoData',
2250
+ 'packed_info_blob',
2251
+ 'packedInfoBlob',
2252
+ 'packed_info',
2253
+ 'packedInfo',
2254
+ 'BytesExtra',
2255
+ 'bytes_extra',
2256
+ 'WCDB_CT_packed_info',
2257
+ 'reserved0',
2258
+ 'Reserved0',
2259
+ 'WCDB_CT_Reserved0'
2260
+ ])
2261
+ const packedText = decodePackedToPrintable(packedRaw)
2262
+ if (packedText) {
2263
+ const datLike = /([0-9a-fA-F]{8,})(?:\.t)?\.dat/i.exec(packedText)
2264
+ if (datLike?.[1]) return String(datLike[1]).toLowerCase()
2265
+ const md5Like = /([0-9a-fA-F]{16,64})/.exec(packedText)
2266
+ if (md5Like?.[1]) return String(md5Like[1]).toLowerCase()
2267
+ }
2268
+
2269
+ return ''
2270
+ }
2271
+ const extractPackedPayload = (row: Record<string, any>): string => {
2272
+ const packedRaw = pickRaw(row, [
2273
+ 'packed_info_data',
2274
+ 'packedInfoData',
2275
+ 'packed_info_blob',
2276
+ 'packedInfoBlob',
2277
+ 'packed_info',
2278
+ 'packedInfo',
2279
+ 'BytesExtra',
2280
+ 'bytes_extra',
2281
+ 'WCDB_CT_packed_info',
2282
+ 'reserved0',
2283
+ 'Reserved0',
2284
+ 'WCDB_CT_Reserved0'
2285
+ ])
2286
+ return decodePackedToPrintable(packedRaw)
2287
+ }
2288
+ const extractVideoMd5 = (xml: string): string => {
2289
+ const byTag =
2290
+ extractXmlValue(xml, 'rawmd5') ||
2291
+ extractXmlValue(xml, 'videomd5') ||
2292
+ extractXmlValue(xml, 'newmd5') ||
2293
+ extractXmlValue(xml, 'md5')
2294
+ if (byTag) return byTag
2295
+ const byAttr = /(?:rawmd5|videomd5|newmd5|md5)\s*=\s*['"]?([a-fA-F0-9]{16,64})['"]?/i.exec(xml)
2296
+ return byAttr?.[1] || ''
2297
+ }
2298
+
2299
+ const requestedSessionId = String(options?.sessionId || '').trim()
2300
+ const mediaType = String(options?.mediaType || 'all').trim() as 'image' | 'video' | 'all'
2301
+ const beginTimestamp = Math.max(0, toInt(options?.beginTimestamp))
2302
+ const endTimestamp = Math.max(0, toInt(options?.endTimestamp))
2303
+ const offset = Math.max(0, toInt(options?.offset))
2304
+ const limit = Math.min(1200, Math.max(40, toInt(options?.limit) || 240))
2305
+
2306
+ const getSessionRows = async (): Promise<{
2307
+ success: boolean
2308
+ rows?: Array<{ sessionId: string; displayName: string; sortTimestamp: number }>
2309
+ error?: string
2310
+ }> => {
2311
+ const now = Date.now()
2312
+ const cachedRows = this.mediaStreamSessionCache
2313
+ if (
2314
+ cachedRows &&
2315
+ now - this.mediaStreamSessionCacheAt <= this.mediaStreamSessionCacheTtlMs
2316
+ ) {
2317
+ return { success: true, rows: cachedRows }
2318
+ }
2319
+
2320
+ const sessionsRes = await this.getSessions()
2321
+ if (!sessionsRes.success || !Array.isArray(sessionsRes.sessions)) {
2322
+ return { success: false, error: sessionsRes.error || '读取会话失败' }
2323
+ }
2324
+
2325
+ const rows = (sessionsRes.sessions || [])
2326
+ .map((row: any) => ({
2327
+ sessionId: String(
2328
+ row.username ||
2329
+ row.user_name ||
2330
+ row.userName ||
2331
+ row.usrName ||
2332
+ row.UsrName ||
2333
+ row.talker ||
2334
+ ''
2335
+ ).trim(),
2336
+ displayName: String(row.displayName || row.display_name || row.remark || '').trim(),
2337
+ sortTimestamp: toInt(
2338
+ row.sort_timestamp ||
2339
+ row.sortTimestamp ||
2340
+ row.last_timestamp ||
2341
+ row.lastTimestamp ||
2342
+ 0
2343
+ )
2344
+ }))
2345
+ .filter((row) => Boolean(row.sessionId))
2346
+ .sort((a, b) => b.sortTimestamp - a.sortTimestamp)
2347
+
2348
+ this.mediaStreamSessionCache = rows
2349
+ this.mediaStreamSessionCacheAt = now
2350
+ return { success: true, rows }
2351
+ }
2352
+
2353
+ let sessionRows: Array<{ sessionId: string; displayName: string; sortTimestamp: number }> = []
2354
+ if (requestedSessionId) {
2355
+ sessionRows = [{ sessionId: requestedSessionId, displayName: requestedSessionId, sortTimestamp: 0 }]
2356
+ } else {
2357
+ const sessionsRowsRes = await getSessionRows()
2358
+ if (!sessionsRowsRes.success || !Array.isArray(sessionsRowsRes.rows)) {
2359
+ return { success: false, error: sessionsRowsRes.error || '读取会话失败' }
2360
+ }
2361
+ sessionRows = sessionsRowsRes.rows
2362
+ }
2363
+
2364
+ if (sessionRows.length === 0) {
2365
+ return { success: true, items: [], hasMore: false, nextOffset: offset }
2366
+ }
2367
+ const sessionNameMap = new Map(sessionRows.map((row) => [row.sessionId, row.displayName || row.sessionId]))
2368
+
2369
+ const outPtr = [null as any]
2370
+ const outHasMore = [0]
2371
+ const mediaTypeCode = mediaType === 'image' ? 1 : mediaType === 'video' ? 2 : 0
2372
+ const result = this.wcdbScanMediaStream(
2373
+ this.handle,
2374
+ JSON.stringify(sessionRows.map((row) => row.sessionId)),
2375
+ mediaTypeCode,
2376
+ beginTimestamp,
2377
+ endTimestamp,
2378
+ limit,
2379
+ offset,
2380
+ outPtr,
2381
+ outHasMore
2382
+ )
2383
+ if (result !== 0 || !outPtr[0]) {
2384
+ return { success: false, error: `扫描资源失败: ${result}` }
2385
+ }
2386
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
2387
+ if (!jsonStr) return { success: false, error: '解析资源失败' }
2388
+ const rows = JSON.parse(jsonStr)
2389
+ const list = Array.isArray(rows) ? rows as Array<Record<string, any>> : []
2390
+
2391
+ let items = list.map((row) => {
2392
+ const sessionId = pickString(row, ['session_id', 'sessionId']) || requestedSessionId
2393
+ const localType = toInt(row.local_type ?? row.localType)
2394
+ const rawMessageContent = pickString(row, [
2395
+ 'message_content',
2396
+ 'messageContent',
2397
+ 'message_content_text',
2398
+ 'messageText',
2399
+ 'StrContent',
2400
+ 'str_content',
2401
+ 'msg_content',
2402
+ 'msgContent',
2403
+ 'strContent',
2404
+ 'content',
2405
+ 'rawContent',
2406
+ 'WCDB_CT_message_content'
2407
+ ])
2408
+ const rawCompressContent = pickString(row, [
2409
+ 'compress_content',
2410
+ 'compressContent',
2411
+ 'msg_compress_content',
2412
+ 'msgCompressContent',
2413
+ 'WCDB_CT_compress_content'
2414
+ ])
2415
+ const useRawMessageContent = Boolean(
2416
+ rawMessageContent &&
2417
+ (rawMessageContent.includes('<') || rawMessageContent.includes('md5') || rawMessageContent.includes('videomsg'))
2418
+ )
2419
+ const decodeContentIfNeeded = (): string => {
2420
+ if (useRawMessageContent) return rawMessageContent
2421
+ if (!rawMessageContent && !rawCompressContent) return ''
2422
+ return decodeMessageContent(rawMessageContent, rawCompressContent)
2423
+ }
2424
+ const packedPayload = extractPackedPayload(row)
2425
+ const imageMd5ByColumn = pickString(row, ['image_md5', 'imageMd5'])
2426
+ const videoMd5ByColumn = pickString(row, ['video_md5', 'videoMd5', 'raw_md5', 'rawMd5'])
2427
+ const packedRaw = pickRaw(row, [
2428
+ 'packed_info_data',
2429
+ 'packedInfoData',
2430
+ 'packed_info_blob',
2431
+ 'packedInfoBlob',
2432
+ 'packed_info',
2433
+ 'packedInfo',
2434
+ 'BytesExtra',
2435
+ 'bytes_extra',
2436
+ 'WCDB_CT_packed_info',
2437
+ 'reserved0',
2438
+ 'Reserved0',
2439
+ 'WCDB_CT_Reserved0'
2440
+ ])
2441
+
2442
+ let content = ''
2443
+ let imageMd5: string | undefined
2444
+ let imageDatName: string | undefined
2445
+ let videoMd5: string | undefined
2446
+
2447
+ if (localType === 3) {
2448
+ imageMd5 = imageMd5ByColumn || extractHexMd5(packedPayload) || undefined
2449
+ imageDatName = extractImageDatName(row, '') || undefined
2450
+ if (!imageMd5 || !imageDatName) {
2451
+ content = decodeContentIfNeeded()
2452
+ if (!imageMd5) imageMd5 = extractImageMd5(content) || extractHexMd5(packedPayload) || undefined
2453
+ if (!imageDatName) imageDatName = extractImageDatName(row, content) || undefined
2454
+ }
2455
+ } else if (localType === 43) {
2456
+ videoMd5 =
2457
+ extractVideoFileNameFromPackedRaw(packedRaw) ||
2458
+ normalizeVideoFileToken(videoMd5ByColumn) ||
2459
+ extractHexMd5(packedPayload) ||
2460
+ undefined
2461
+ if (!videoMd5) {
2462
+ content = decodeContentIfNeeded()
2463
+ videoMd5 =
2464
+ normalizeVideoFileToken(extractVideoMd5(content)) ||
2465
+ extractHexMd5(packedPayload) ||
2466
+ undefined
2467
+ } else if (useRawMessageContent) {
2468
+ // 占位态标题只依赖简单 XML,已带 md5 时不做额外解压
2469
+ content = rawMessageContent
2470
+ }
2471
+ }
2472
+
2473
+ return {
2474
+ sessionId,
2475
+ sessionDisplayName: sessionNameMap.get(sessionId) || sessionId,
2476
+ mediaType: localType === 43 ? 'video' as const : 'image' as const,
2477
+ localId: toInt(row.local_id ?? row.localId),
2478
+ serverId: pickString(row, ['server_id', 'serverId']) || undefined,
2479
+ createTime: toInt(row.create_time ?? row.createTime),
2480
+ localType,
2481
+ senderUsername: pickString(row, ['sender_username', 'senderUsername']) || undefined,
2482
+ isSend: row.is_send === null || row.is_send === undefined ? null : toInt(row.is_send),
2483
+ imageMd5,
2484
+ imageDatName,
2485
+ videoMd5,
2486
+ content: localType === 43 ? (content || undefined) : undefined
2487
+ }
2488
+ })
2489
+
2490
+ const unresolvedSessionIds = Array.from(
2491
+ new Set(
2492
+ items
2493
+ .map((item) => item.sessionId)
2494
+ .filter((sessionId) => {
2495
+ const name = String(sessionNameMap.get(sessionId) || '').trim()
2496
+ return !name || name === sessionId
2497
+ })
2498
+ )
2499
+ )
2500
+ if (unresolvedSessionIds.length > 0) {
2501
+ const displayNameRes = await this.getDisplayNames(unresolvedSessionIds)
2502
+ if (displayNameRes.success && displayNameRes.map) {
2503
+ unresolvedSessionIds.forEach((sessionId) => {
2504
+ const display = String(displayNameRes.map?.[sessionId] || '').trim()
2505
+ if (display) sessionNameMap.set(sessionId, display)
2506
+ })
2507
+ items = items.map((item) => ({
2508
+ ...item,
2509
+ sessionDisplayName: sessionNameMap.get(item.sessionId) || item.sessionId
2510
+ }))
2511
+ }
2512
+ }
2513
+
2514
+ return {
2515
+ success: true,
2516
+ items,
2517
+ hasMore: Number(outHasMore[0]) > 0,
2518
+ nextOffset: offset + items.length
2519
+ }
2520
+ } catch (e) {
2521
+ return { success: false, error: String(e) }
2522
+ }
2523
+ }
2524
+
2525
+ async getDisplayNames(usernames: string[]): Promise<{ success: boolean; map?: Record<string, string>; error?: string }> {
2526
+ if (!this.ensureReady()) {
2527
+ return { success: false, error: 'WCDB 未连接' }
2528
+ }
2529
+ if (usernames.length === 0) return { success: true, map: {} }
2530
+ try {
2531
+ if (process.platform === 'darwin') {
2532
+ const uniq = Array.from(new Set(usernames.map((x) => String(x || '').trim()).filter(Boolean)))
2533
+ if (uniq.length === 0) return { success: true, map: {} }
2534
+ const inList = uniq.map((u) => `'${u.replace(/'/g, "''")}'`).join(',')
2535
+ const sql = `SELECT * FROM contact WHERE username IN (${inList})`
2536
+ const q = await this.execQuery('contact', null, sql)
2537
+ if (!q.success) return { success: false, error: q.error || '获取昵称失败' }
2538
+ const map: Record<string, string> = {}
2539
+ for (const row of (q.rows || []) as Array<Record<string, any>>) {
2540
+ const username = this.pickFirstStringField(row, ['username', 'user_name', 'userName'])
2541
+ if (!username) continue
2542
+ const display = this.pickFirstStringField(row, [
2543
+ 'remark', 'Remark',
2544
+ 'nick_name', 'nickName', 'nickname', 'NickName',
2545
+ 'alias', 'Alias'
2546
+ ]) || username
2547
+ map[username] = display
2548
+ }
2549
+ // 保证每个请求用户名至少有回退值
2550
+ for (const u of uniq) {
2551
+ if (!map[u]) map[u] = u
2552
+ }
2553
+ return { success: true, map }
2554
+ }
2555
+
2556
+ // 让出控制权,避免阻塞事件循环
2557
+ await new Promise(resolve => setImmediate(resolve))
2558
+
2559
+ const outPtr = [null as any]
2560
+ const result = this.wcdbGetDisplayNames(this.handle, JSON.stringify(usernames), outPtr)
2561
+
2562
+ //数据服务调用后再次让出控制权
2563
+ await new Promise(resolve => setImmediate(resolve))
2564
+
2565
+ if (result !== 0 || !outPtr[0]) {
2566
+ return { success: false, error: `获取昵称失败: ${result}` }
2567
+ }
2568
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
2569
+ if (!jsonStr) return { success: false, error: '解析昵称失败' }
2570
+ const map = JSON.parse(jsonStr)
2571
+ return { success: true, map }
2572
+ } catch (e) {
2573
+ return { success: false, error: String(e) }
2574
+ }
2575
+ }
2576
+
2577
+ async getAvatarUrls(usernames: string[]): Promise<{ success: boolean; map?: Record<string, string>; error?: string }> {
2578
+ if (!this.ensureReady()) {
2579
+ return { success: false, error: 'WCDB 未连接' }
2580
+ }
2581
+ if (usernames.length === 0) return { success: true, map: {} }
2582
+ try {
2583
+ const now = Date.now()
2584
+ const resultMap: Record<string, string> = {}
2585
+ const toFetch: string[] = []
2586
+ const seen = new Set<string>()
2587
+
2588
+ for (const username of usernames) {
2589
+ if (!username || seen.has(username)) continue
2590
+ seen.add(username)
2591
+ const cached = this.avatarUrlCache.get(username)
2592
+ // 只使用有效的缓存(URL不为空)
2593
+ if (cached && cached.url && cached.url.trim() && now - cached.updatedAt < this.avatarCacheTtlMs) {
2594
+ resultMap[username] = cached.url
2595
+ continue
2596
+ }
2597
+ toFetch.push(username)
2598
+ }
2599
+
2600
+ if (toFetch.length === 0) {
2601
+ return { success: true, map: resultMap }
2602
+ }
2603
+
2604
+ if (process.platform === 'darwin') {
2605
+ const inList = toFetch.map((u) => `'${u.replace(/'/g, "''")}'`).join(',')
2606
+ const sql = `SELECT * FROM contact WHERE username IN (${inList})`
2607
+ const q = await this.execQuery('contact', null, sql)
2608
+ if (!q.success) {
2609
+ if (Object.keys(resultMap).length > 0) {
2610
+ return { success: true, map: resultMap, error: q.error || '获取头像失败' }
2611
+ }
2612
+ return { success: false, error: q.error || '获取头像失败' }
2613
+ }
2614
+
2615
+ for (const row of (q.rows || []) as Array<Record<string, any>>) {
2616
+ const username = this.pickFirstStringField(row, ['username', 'user_name', 'userName'])
2617
+ if (!username) continue
2618
+ const url = this.pickFirstStringField(row, [
2619
+ 'big_head_img_url', 'bigHeadImgUrl', 'bigHeadUrl', 'big_head_url',
2620
+ 'small_head_img_url', 'smallHeadImgUrl', 'smallHeadUrl', 'small_head_url',
2621
+ 'head_img_url', 'headImgUrl',
2622
+ 'avatar_url', 'avatarUrl'
2623
+ ])
2624
+ if (url) {
2625
+ resultMap[username] = url
2626
+ this.avatarUrlCache.set(username, { url, updatedAt: now })
2627
+ }
2628
+ }
2629
+ return { success: true, map: resultMap }
2630
+ }
2631
+
2632
+ // 让出控制权,避免阻塞事件循环
2633
+ const handle = this.handle
2634
+ await new Promise(resolve => setImmediate(resolve))
2635
+
2636
+ // await 后 handle 可能已被关闭,需重新检查
2637
+ if (handle === null || this.handle !== handle) {
2638
+ if (Object.keys(resultMap).length > 0) {
2639
+ return { success: true, map: resultMap, error: '连接已断开' }
2640
+ }
2641
+ return { success: false, error: '连接已断开' }
2642
+ }
2643
+
2644
+ const outPtr = [null as any]
2645
+ const result = this.wcdbGetAvatarUrls(handle, JSON.stringify(toFetch), outPtr)
2646
+
2647
+ //数据服务调用后再次让出控制权
2648
+ await new Promise(resolve => setImmediate(resolve))
2649
+
2650
+ if (result !== 0 || !outPtr[0]) {
2651
+ if (Object.keys(resultMap).length > 0) {
2652
+ return { success: true, map: resultMap, error: `获取头像失败: ${result}` }
2653
+ }
2654
+ return { success: false, error: `获取头像失败: ${result}` }
2655
+ }
2656
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
2657
+ if (!jsonStr) {
2658
+ return { success: false, error: '解析头像失败' }
2659
+ }
2660
+ const map = JSON.parse(jsonStr) as Record<string, string>
2661
+ for (const username of toFetch) {
2662
+ const url = map[username]
2663
+ if (url && url.trim()) {
2664
+ resultMap[username] = url
2665
+ // 只缓存有效的URL
2666
+ this.avatarUrlCache.set(username, { url, updatedAt: now })
2667
+ }
2668
+ // 不缓存空URL,下次可以重新尝试
2669
+ }
2670
+ return { success: true, map: resultMap }
2671
+ } catch (e) {
2672
+ console.error('[wcdbCore] getAvatarUrls 异常:', e)
2673
+ return { success: false, error: String(e) }
2674
+ }
2675
+ }
2676
+
2677
+ async getGroupMemberCount(chatroomId: string): Promise<{ success: boolean; count?: number; error?: string }> {
2678
+ if (!this.ensureReady()) {
2679
+ return { success: false, error: 'WCDB 未连接' }
2680
+ }
2681
+ try {
2682
+ const outCount = [0]
2683
+ const result = this.wcdbGetGroupMemberCount(this.handle, chatroomId, outCount)
2684
+ if (result !== 0) {
2685
+ return { success: false, error: `获取群成员数量失败: ${result}` }
2686
+ }
2687
+ return { success: true, count: outCount[0] }
2688
+ } catch (e) {
2689
+ return { success: false, error: String(e) }
2690
+ }
2691
+ }
2692
+
2693
+ async getGroupMemberCounts(chatroomIds: string[]): Promise<{ success: boolean; map?: Record<string, number>; error?: string }> {
2694
+ if (!this.ensureReady()) {
2695
+ return { success: false, error: 'WCDB 未连接' }
2696
+ }
2697
+ if (chatroomIds.length === 0) return { success: true, map: {} }
2698
+ if (!this.wcdbGetGroupMemberCounts) {
2699
+ const map: Record<string, number> = {}
2700
+ for (const chatroomId of chatroomIds) {
2701
+ const result = await this.getGroupMemberCount(chatroomId)
2702
+ if (result.success && typeof result.count === 'number') {
2703
+ map[chatroomId] = result.count
2704
+ }
2705
+ }
2706
+ return { success: true, map }
2707
+ }
2708
+ try {
2709
+ const outPtr = [null as any]
2710
+ const result = this.wcdbGetGroupMemberCounts(this.handle, JSON.stringify(chatroomIds), outPtr)
2711
+ if (result !== 0 || !outPtr[0]) {
2712
+ return { success: false, error: `获取群成员数量失败: ${result}` }
2713
+ }
2714
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
2715
+ if (!jsonStr) return { success: false, error: '解析群成员数量失败' }
2716
+ const map = JSON.parse(jsonStr)
2717
+ return { success: true, map }
2718
+ } catch (e) {
2719
+ return { success: false, error: String(e) }
2720
+ }
2721
+ }
2722
+
2723
+ async getGroupMembers(chatroomId: string): Promise<{ success: boolean; members?: any[]; error?: string }> {
2724
+ if (!this.ensureReady()) {
2725
+ return { success: false, error: 'WCDB 未连接' }
2726
+ }
2727
+ try {
2728
+ const outPtr = [null as any]
2729
+ const result = this.wcdbGetGroupMembers(this.handle, chatroomId, outPtr)
2730
+ if (result !== 0 || !outPtr[0]) {
2731
+ return { success: false, error: `获取群成员失败: ${result}` }
2732
+ }
2733
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
2734
+ if (!jsonStr) return { success: false, error: '解析群成员失败' }
2735
+ const members = JSON.parse(jsonStr)
2736
+ return { success: true, members }
2737
+ } catch (e) {
2738
+ return { success: false, error: String(e) }
2739
+ }
2740
+ }
2741
+
2742
+ async getGroupNicknames(chatroomId: string): Promise<{ success: boolean; nicknames?: Record<string, string>; error?: string }> {
2743
+ if (!this.ensureReady()) {
2744
+ return { success: false, error: 'WCDB 未连接' }
2745
+ }
2746
+ if (!this.wcdbGetGroupNicknames) {
2747
+ return { success: false, error: '当前数据服务版本不支持获取群昵称接口' }
2748
+ }
2749
+ try {
2750
+ const outPtr = [null as any]
2751
+ const result = this.wcdbGetGroupNicknames(this.handle, chatroomId, outPtr)
2752
+ if (result !== 0 || !outPtr[0]) {
2753
+ return { success: false, error: `获取群昵称失败: ${result}` }
2754
+ }
2755
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
2756
+ if (!jsonStr) return { success: false, error: '解析群昵称失败' }
2757
+ const nicknames = JSON.parse(jsonStr)
2758
+ return { success: true, nicknames }
2759
+ } catch (e) {
2760
+ return { success: false, error: String(e) }
2761
+ }
2762
+ }
2763
+
2764
+ async getMessageTables(sessionId: string): Promise<{ success: boolean; tables?: any[]; error?: string }> {
2765
+ if (!this.ensureReady()) {
2766
+ return { success: false, error: 'WCDB 未连接' }
2767
+ }
2768
+ try {
2769
+ const outPtr = [null as any]
2770
+ const result = this.wcdbGetMessageTables(this.handle, sessionId, outPtr)
2771
+ if (result !== 0 || !outPtr[0]) {
2772
+ return { success: false, error: `获取消息表失败: ${result}` }
2773
+ }
2774
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
2775
+ if (!jsonStr) return { success: false, error: '解析消息表失败' }
2776
+ const tables = JSON.parse(jsonStr)
2777
+ return { success: true, tables }
2778
+ } catch (e) {
2779
+ return { success: false, error: String(e) }
2780
+ }
2781
+ }
2782
+
2783
+ async getMessageDates(sessionId: string): Promise<{ success: boolean; dates?: string[]; error?: string }> {
2784
+ if (!this.ensureReady()) {
2785
+ return { success: false, error: 'WCDB 未连接' }
2786
+ }
2787
+ try {
2788
+ if (!this.wcdbGetMessageDates) {
2789
+ return { success: false, error: 'DLL 不支持 getMessageDates' }
2790
+ }
2791
+ const outPtr = [null as any]
2792
+ const result = this.wcdbGetMessageDates(this.handle, sessionId, outPtr)
2793
+ if (result !== 0 || !outPtr[0]) {
2794
+ // 空结果也可能是正常的(无消息)
2795
+ return { success: true, dates: [] }
2796
+ }
2797
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
2798
+ if (!jsonStr) return { success: false, error: '解析日期列表失败' }
2799
+ const dates = JSON.parse(jsonStr)
2800
+ return { success: true, dates }
2801
+ } catch (e) {
2802
+ return { success: false, error: String(e) }
2803
+ }
2804
+ }
2805
+
2806
+ async getMessageTableStats(sessionId: string): Promise<{ success: boolean; tables?: any[]; error?: string }> {
2807
+ if (!this.ensureReady()) {
2808
+ return { success: false, error: 'WCDB 未连接' }
2809
+ }
2810
+ try {
2811
+ const outPtr = [null as any]
2812
+ const result = this.wcdbGetMessageTableStats(this.handle, sessionId, outPtr)
2813
+ if (result !== 0 || !outPtr[0]) {
2814
+ return { success: false, error: `获取表统计失败: ${result}` }
2815
+ }
2816
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
2817
+ if (!jsonStr) return { success: false, error: '解析表统计失败' }
2818
+ const tables = JSON.parse(jsonStr)
2819
+ return { success: true, tables }
2820
+ } catch (e) {
2821
+ return { success: false, error: String(e) }
2822
+ }
2823
+ }
2824
+
2825
+ async getMessageMeta(dbPath: string, tableName: string, limit: number, offset: number): Promise<{ success: boolean; rows?: any[]; error?: string }> {
2826
+ if (!this.ensureReady()) {
2827
+ return { success: false, error: 'WCDB 未连接' }
2828
+ }
2829
+ try {
2830
+ const outPtr = [null as any]
2831
+ const result = this.wcdbGetMessageMeta(this.handle, dbPath, tableName, limit, offset, outPtr)
2832
+ if (result !== 0 || !outPtr[0]) {
2833
+ return { success: false, error: `获取消息元数据失败: ${result}` }
2834
+ }
2835
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
2836
+ if (!jsonStr) return { success: false, error: '解析消息元数据失败' }
2837
+ const rows = JSON.parse(jsonStr)
2838
+ return { success: true, rows }
2839
+ } catch (e) {
2840
+ return { success: false, error: String(e) }
2841
+ }
2842
+ }
2843
+
2844
+ async getContact(username: string): Promise<{ success: boolean; contact?: any; error?: string }> {
2845
+ if (!this.ensureReady()) {
2846
+ return { success: false, error: 'WCDB 未连接' }
2847
+ }
2848
+ try {
2849
+ if (process.platform === 'darwin') {
2850
+ const safe = String(username || '').replace(/'/g, "''")
2851
+ const sql = `SELECT * FROM contact WHERE username='${safe}' LIMIT 1`
2852
+ const q = await this.execQuery('contact', null, sql)
2853
+ if (!q.success) {
2854
+ return { success: false, error: q.error || '获取联系人失败' }
2855
+ }
2856
+ const row = Array.isArray(q.rows) && q.rows.length > 0 ? q.rows[0] : null
2857
+ if (!row) {
2858
+ return { success: false, error: `联系人不存在: ${username}` }
2859
+ }
2860
+ return { success: true, contact: row }
2861
+ }
2862
+
2863
+ const outPtr = [null as any]
2864
+ const result = this.wcdbGetContact(this.handle, username, outPtr)
2865
+ if (result !== 0 || !outPtr[0]) {
2866
+ this.writeLog(`[diag:getContact] primary api failed username=${username} code=${result} outPtr=${outPtr[0] ? 'set' : 'null'}`, true)
2867
+ await this.dumpDbStatus('getContact-primary-fail')
2868
+ await this.printLogs(true)
2869
+
2870
+ // Fallback: 直接查询 contact 表,便于区分是接口失败还是 contact 库本身不可读。
2871
+ const safe = String(username || '').replace(/'/g, "''")
2872
+ const fallbackSql = `SELECT * FROM contact WHERE username='${safe}' LIMIT 1`
2873
+ const fallback = await this.execQuery('contact', null, fallbackSql)
2874
+ if (fallback.success) {
2875
+ const row = Array.isArray(fallback.rows) ? fallback.rows[0] : null
2876
+ if (row) {
2877
+ this.writeLog(`[diag:getContact] fallback sql hit username=${username}`, true)
2878
+ return { success: true, contact: row }
2879
+ }
2880
+ this.writeLog(`[diag:getContact] fallback sql no row username=${username}`, true)
2881
+ return { success: false, error: `联系人不存在: ${username}` }
2882
+ }
2883
+ this.writeLog(`[diag:getContact] fallback sql failed username=${username} err=${fallback.error || 'unknown'}`, true)
2884
+ return { success: false, error: `获取联系人失败: ${result}; fallback=${fallback.error || 'unknown'}` }
2885
+ }
2886
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
2887
+ if (!jsonStr) return { success: false, error: '解析联系人失败' }
2888
+ const contact = JSON.parse(jsonStr)
2889
+ return { success: true, contact }
2890
+ } catch (e) {
2891
+ return { success: false, error: String(e) }
2892
+ }
2893
+ }
2894
+
2895
+ async getContactStatus(usernames: string[]): Promise<{ success: boolean; map?: Record<string, { isFolded: boolean; isMuted: boolean }>; error?: string }> {
2896
+ if (!this.ensureReady()) {
2897
+ return { success: false, error: 'WCDB 未连接' }
2898
+ }
2899
+ if (!this.wcdbGetContactStatus) {
2900
+ return { success: false, error: '接口未就绪' }
2901
+ }
2902
+ try {
2903
+ const outPtr = [null as any]
2904
+ const code = this.wcdbGetContactStatus(this.handle, JSON.stringify(usernames || []), outPtr)
2905
+ if (code !== 0 || !outPtr[0]) {
2906
+ return { success: false, error: `获取会话状态失败: ${code}` }
2907
+ }
2908
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
2909
+ if (!jsonStr) return { success: false, error: '解析会话状态失败' }
2910
+
2911
+ const rawMap = JSON.parse(jsonStr) || {}
2912
+ const map: Record<string, { isFolded: boolean; isMuted: boolean }> = {}
2913
+ for (const username of usernames || []) {
2914
+ const state = rawMap[username] || {}
2915
+ map[username] = {
2916
+ isFolded: Boolean(state.isFolded),
2917
+ isMuted: Boolean(state.isMuted)
2918
+ }
2919
+ }
2920
+ return { success: true, map }
2921
+ } catch (e) {
2922
+ return { success: false, error: String(e) }
2923
+ }
2924
+ }
2925
+
2926
+ async getMessageTableColumns(dbPath: string, tableName: string): Promise<{ success: boolean; columns?: string[]; error?: string }> {
2927
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
2928
+ if (!this.wcdbGetMessageTableColumns) return { success: false, error: '接口未就绪' }
2929
+ try {
2930
+ const outPtr = [null as any]
2931
+ const result = this.wcdbGetMessageTableColumns(this.handle, dbPath, tableName, outPtr)
2932
+ if (result !== 0 || !outPtr[0]) return { success: false, error: `获取消息表列失败: ${result}` }
2933
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
2934
+ if (!jsonStr) return { success: false, error: '解析消息表列失败' }
2935
+ const columns = JSON.parse(jsonStr)
2936
+ return { success: true, columns: Array.isArray(columns) ? columns.map((c: any) => String(c || '')) : [] }
2937
+ } catch (e) {
2938
+ return { success: false, error: String(e) }
2939
+ }
2940
+ }
2941
+
2942
+ async listTables(kind: string, dbPath: string = ''): Promise<{ success: boolean; tables?: string[]; error?: string }> {
2943
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
2944
+ if (!this.wcdbListTables) return { success: false, error: '接口未就绪' }
2945
+ try {
2946
+ const outPtr = [null as any]
2947
+ const result = this.wcdbListTables(this.handle, kind, dbPath || '', outPtr)
2948
+ if (result !== 0 || !outPtr[0]) return { success: false, error: `获取表列表失败: ${result}` }
2949
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
2950
+ if (!jsonStr) return { success: false, error: '解析表列表失败' }
2951
+ const tables = JSON.parse(jsonStr)
2952
+ return { success: true, tables: Array.isArray(tables) ? tables.map((c: any) => String(c || '')).filter(Boolean) : [] }
2953
+ } catch (e) {
2954
+ return { success: false, error: String(e) }
2955
+ }
2956
+ }
2957
+
2958
+ async getTableSchema(kind: string, dbPath: string, tableName: string): Promise<{ success: boolean; schema?: string; error?: string }> {
2959
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
2960
+ if (!this.wcdbGetTableSchema) return { success: false, error: '接口未就绪' }
2961
+ try {
2962
+ const outPtr = [null as any]
2963
+ const result = this.wcdbGetTableSchema(this.handle, kind, dbPath || '', tableName, outPtr)
2964
+ const jsonStr = outPtr[0] ? this.decodeJsonPtr(outPtr[0]) : ''
2965
+ const data = jsonStr ? JSON.parse(jsonStr) : {}
2966
+ if (result !== 0 || data?.success === false) return { success: false, error: data?.error || `获取表结构失败: ${result}` }
2967
+ return { success: true, schema: String(data?.schema || '') }
2968
+ } catch (e) {
2969
+ return { success: false, error: String(e) }
2970
+ }
2971
+ }
2972
+
2973
+ async exportTableSnapshot(kind: string, dbPath: string, tableName: string, outputPath: string): Promise<{ success: boolean; rows?: number; columns?: number; error?: string }> {
2974
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
2975
+ if (!this.wcdbExportTableSnapshot) return { success: false, error: '接口未就绪' }
2976
+ try {
2977
+ const outPtr = [null as any]
2978
+ const result = this.wcdbExportTableSnapshot(this.handle, kind, dbPath || '', tableName, outputPath, outPtr)
2979
+ const jsonStr = outPtr[0] ? this.decodeJsonPtr(outPtr[0]) : ''
2980
+ const data = jsonStr ? JSON.parse(jsonStr) : {}
2981
+ if (result !== 0 || data?.success === false) return { success: false, error: data?.error || `导出表快照失败: ${result}` }
2982
+ return { success: true, rows: Number(data?.rows || 0), columns: Number(data?.columns || 0) }
2983
+ } catch (e) {
2984
+ return { success: false, error: String(e) }
2985
+ }
2986
+ }
2987
+
2988
+ async importTableSnapshot(kind: string, dbPath: string, tableName: string, inputPath: string): Promise<{ success: boolean; rows?: number; inserted?: number; ignored?: number; malformed?: number; columns?: number; error?: string }> {
2989
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
2990
+ if (!this.wcdbImportTableSnapshot) return { success: false, error: '接口未就绪' }
2991
+ try {
2992
+ const outPtr = [null as any]
2993
+ const result = this.wcdbImportTableSnapshot(this.handle, kind, dbPath || '', tableName, inputPath, outPtr)
2994
+ const jsonStr = outPtr[0] ? this.decodeJsonPtr(outPtr[0]) : ''
2995
+ const data = jsonStr ? JSON.parse(jsonStr) : {}
2996
+ if (result !== 0 || data?.success === false) return { success: false, error: data?.error || `导入表快照失败: ${result}` }
2997
+ return {
2998
+ success: true,
2999
+ rows: Number(data?.rows || 0),
3000
+ inserted: Number(data?.inserted || 0),
3001
+ ignored: Number(data?.ignored || 0),
3002
+ malformed: Number(data?.malformed || 0),
3003
+ columns: Number(data?.columns || 0)
3004
+ }
3005
+ } catch (e) {
3006
+ return { success: false, error: String(e) }
3007
+ }
3008
+ }
3009
+
3010
+ async importTableSnapshotWithSchema(kind: string, dbPath: string, tableName: string, inputPath: string, createTableSql: string): Promise<{ success: boolean; rows?: number; inserted?: number; ignored?: number; malformed?: number; columns?: number; error?: string }> {
3011
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
3012
+ if (!this.wcdbImportTableSnapshotWithSchema) return { success: false, error: '接口未就绪' }
3013
+ try {
3014
+ const outPtr = [null as any]
3015
+ const result = this.wcdbImportTableSnapshotWithSchema(this.handle, kind, dbPath || '', tableName, inputPath, createTableSql || '', outPtr)
3016
+ const jsonStr = outPtr[0] ? this.decodeJsonPtr(outPtr[0]) : ''
3017
+ const data = jsonStr ? JSON.parse(jsonStr) : {}
3018
+ if (result !== 0 || data?.success === false) return { success: false, error: data?.error || `导入表快照失败: ${result}` }
3019
+ return {
3020
+ success: true,
3021
+ rows: Number(data?.rows || 0),
3022
+ inserted: Number(data?.inserted || 0),
3023
+ ignored: Number(data?.ignored || 0),
3024
+ malformed: Number(data?.malformed || 0),
3025
+ columns: Number(data?.columns || 0)
3026
+ }
3027
+ } catch (e) {
3028
+ return { success: false, error: String(e) }
3029
+ }
3030
+ }
3031
+
3032
+ async getMessageTableTimeRange(dbPath: string, tableName: string): Promise<{ success: boolean; data?: any; error?: string }> {
3033
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
3034
+ if (!this.wcdbGetMessageTableTimeRange) return { success: false, error: '接口未就绪' }
3035
+ try {
3036
+ const outPtr = [null as any]
3037
+ const result = this.wcdbGetMessageTableTimeRange(this.handle, dbPath, tableName, outPtr)
3038
+ if (result !== 0 || !outPtr[0]) return { success: false, error: `获取消息表时间范围失败: ${result}` }
3039
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
3040
+ if (!jsonStr) return { success: false, error: '解析消息表时间范围失败' }
3041
+ const data = JSON.parse(jsonStr) || {}
3042
+ return { success: true, data }
3043
+ } catch (e) {
3044
+ return { success: false, error: String(e) }
3045
+ }
3046
+ }
3047
+
3048
+ async getContactTypeCounts(): Promise<{ success: boolean; counts?: { private: number; group: number; official: number; former_friend: number }; error?: string }> {
3049
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
3050
+ const runFallback = async (reason: string) => {
3051
+ const contactsResult = await this.getContactsCompact()
3052
+ if (!contactsResult.success || !Array.isArray(contactsResult.contacts)) {
3053
+ return { success: false, error: `获取联系人分类统计失败: ${reason}; fallback=${contactsResult.error || 'unknown'}` }
3054
+ }
3055
+ const counts = this.deriveContactTypeCounts(contactsResult.contacts as Array<Record<string, any>>)
3056
+ this.writeLog(`[diag:getContactTypeCounts] fallback reason=${reason} private=${counts.private} group=${counts.group} official=${counts.official} former_friend=${counts.former_friend}`, true)
3057
+ return { success: true, counts }
3058
+ }
3059
+
3060
+ if (!this.wcdbGetContactTypeCounts) return await runFallback('api_missing')
3061
+ try {
3062
+ const outPtr = [null as any]
3063
+ const code = this.wcdbGetContactTypeCounts(this.handle, outPtr)
3064
+ if (code !== 0 || !outPtr[0]) return await runFallback(`code=${code}`)
3065
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
3066
+ if (!jsonStr) return await runFallback('decode_empty')
3067
+ const raw = JSON.parse(jsonStr) || {}
3068
+ return {
3069
+ success: true,
3070
+ counts: {
3071
+ private: Number(raw.private || 0),
3072
+ group: Number(raw.group || 0),
3073
+ official: Number(raw.official || 0),
3074
+ former_friend: Number(raw.former_friend || 0)
3075
+ }
3076
+ }
3077
+ } catch (e) {
3078
+ return await runFallback(`exception=${String(e)}`)
3079
+ }
3080
+ }
3081
+
3082
+ async getContactsCompact(usernames: string[] = []): Promise<{ success: boolean; contacts?: any[]; error?: string }> {
3083
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
3084
+ const runFallback = async (reason: string) => {
3085
+ const fallback = await this.execQuery('contact', null, this.buildContactSelectSql(usernames))
3086
+ if (!fallback.success) {
3087
+ return { success: false, error: `获取联系人列表失败: ${reason}; fallback=${fallback.error || 'unknown'}` }
3088
+ }
3089
+ const rows = Array.isArray(fallback.rows) ? fallback.rows : []
3090
+ this.writeLog(`[diag:getContactsCompact] fallback reason=${reason} usernames=${Array.isArray(usernames) ? usernames.length : 0} rows=${rows.length}`, true)
3091
+ return { success: true, contacts: rows }
3092
+ }
3093
+
3094
+ if (!this.wcdbGetContactsCompact) return await runFallback('api_missing')
3095
+ try {
3096
+ const outPtr = [null as any]
3097
+ const payload = Array.isArray(usernames) && usernames.length > 0 ? JSON.stringify(usernames) : null
3098
+ const code = this.wcdbGetContactsCompact(this.handle, payload, outPtr)
3099
+ if (code !== 0 || !outPtr[0]) return await runFallback(`code=${code}`)
3100
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
3101
+ if (!jsonStr) return await runFallback('decode_empty')
3102
+ const contacts = JSON.parse(jsonStr)
3103
+ return { success: true, contacts: Array.isArray(contacts) ? contacts : [] }
3104
+ } catch (e) {
3105
+ return await runFallback(`exception=${String(e)}`)
3106
+ }
3107
+ }
3108
+
3109
+ async getContactAliasMap(usernames: string[]): Promise<{ success: boolean; map?: Record<string, string>; error?: string }> {
3110
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
3111
+ if (!this.wcdbGetContactAliasMap) return { success: false, error: '接口未就绪' }
3112
+ try {
3113
+ const outPtr = [null as any]
3114
+ const code = this.wcdbGetContactAliasMap(this.handle, JSON.stringify(usernames || []), outPtr)
3115
+ if (code !== 0 || !outPtr[0]) return { success: false, error: `获取联系人 alias 失败: ${code}` }
3116
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
3117
+ if (!jsonStr) return { success: false, error: '解析联系人 alias 失败' }
3118
+ const map = JSON.parse(jsonStr)
3119
+ return { success: true, map }
3120
+ } catch (e) {
3121
+ return { success: false, error: String(e) }
3122
+ }
3123
+ }
3124
+
3125
+ async getContactFriendFlags(usernames: string[]): Promise<{ success: boolean; map?: Record<string, boolean>; error?: string }> {
3126
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
3127
+ if (!this.wcdbGetContactFriendFlags) return { success: false, error: '接口未就绪' }
3128
+ try {
3129
+ const outPtr = [null as any]
3130
+ const code = this.wcdbGetContactFriendFlags(this.handle, JSON.stringify(usernames || []), outPtr)
3131
+ if (code !== 0 || !outPtr[0]) return { success: false, error: `获取联系人好友标记失败: ${code}` }
3132
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
3133
+ if (!jsonStr) return { success: false, error: '解析联系人好友标记失败' }
3134
+ const map = JSON.parse(jsonStr)
3135
+ return { success: true, map }
3136
+ } catch (e) {
3137
+ return { success: false, error: String(e) }
3138
+ }
3139
+ }
3140
+
3141
+ async getChatRoomExtBuffer(chatroomId: string): Promise<{ success: boolean; extBuffer?: string; error?: string }> {
3142
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
3143
+ if (!this.wcdbGetChatRoomExtBuffer) return { success: false, error: '接口未就绪' }
3144
+ try {
3145
+ const outPtr = [null as any]
3146
+ const code = this.wcdbGetChatRoomExtBuffer(this.handle, chatroomId, outPtr)
3147
+ if (code !== 0 || !outPtr[0]) return { success: false, error: `获取群聊 ext_buffer 失败: ${code}` }
3148
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
3149
+ if (!jsonStr) return { success: false, error: '解析群聊 ext_buffer 失败' }
3150
+ const data = JSON.parse(jsonStr) || {}
3151
+ const extBuffer = String(data.ext_buffer || '').trim()
3152
+ return { success: true, extBuffer: extBuffer || undefined }
3153
+ } catch (e) {
3154
+ return { success: false, error: String(e) }
3155
+ }
3156
+ }
3157
+
3158
+ async getAggregateStats(sessionIds: string[], beginTimestamp: number = 0, endTimestamp: number = 0): Promise<{ success: boolean; data?: any; error?: string }> {
3159
+ if (!this.ensureReady()) {
3160
+ return { success: false, error: 'WCDB 未连接' }
3161
+ }
3162
+ try {
3163
+ const normalizedBegin = this.normalizeTimestamp(beginTimestamp)
3164
+ let normalizedEnd = this.normalizeTimestamp(endTimestamp)
3165
+ if (normalizedEnd <= 0) {
3166
+ normalizedEnd = this.normalizeTimestamp(Date.now())
3167
+ }
3168
+ if (normalizedBegin > 0 && normalizedEnd < normalizedBegin) {
3169
+ normalizedEnd = normalizedBegin
3170
+ }
3171
+
3172
+ const callAggregate = (ids: string[]) => {
3173
+ const idsAreNumeric = ids.length > 0 && ids.every((id) => /^\d+$/.test(id))
3174
+ const payloadIds = idsAreNumeric ? ids.map((id) => Number(id)) : ids
3175
+
3176
+ const outPtr = [null as any]
3177
+ const result = this.wcdbGetAggregateStats(this.handle, JSON.stringify(payloadIds), normalizedBegin, normalizedEnd, outPtr)
3178
+
3179
+ if (result !== 0 || !outPtr[0]) {
3180
+ return { success: false, error: `获取聚合统计失败: ${result}` }
3181
+ }
3182
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
3183
+ if (!jsonStr) {
3184
+ return { success: false, error: '解析聚合统计失败' }
3185
+ }
3186
+
3187
+ const data = JSON.parse(jsonStr)
3188
+ return { success: true, data }
3189
+ }
3190
+
3191
+ let result = callAggregate(sessionIds)
3192
+ if (result.success && result.data && result.data.total === 0 && result.data.idMap) {
3193
+ const idMap = result.data.idMap as Record<string, string>
3194
+ const reverseMap: Record<string, string> = {}
3195
+ for (const [id, name] of Object.entries(idMap)) {
3196
+ if (!name) continue
3197
+ reverseMap[name] = id
3198
+ }
3199
+ const numericIds = sessionIds
3200
+ .map((id) => reverseMap[id])
3201
+ .filter((id) => typeof id === 'string' && /^\d+$/.test(id))
3202
+ if (numericIds.length > 0) {
3203
+ const retry = callAggregate(numericIds)
3204
+ if (retry.success && retry.data) {
3205
+ result = retry
3206
+ }
3207
+ }
3208
+ }
3209
+
3210
+ return result
3211
+ } catch (e) {
3212
+ return { success: false, error: String(e) }
3213
+ }
3214
+ }
3215
+
3216
+ async getAvailableYears(sessionIds: string[]): Promise<{ success: boolean; data?: number[]; error?: string }> {
3217
+ if (!this.ensureReady()) {
3218
+ return { success: false, error: 'WCDB 未连接' }
3219
+ }
3220
+ if (!this.wcdbGetAvailableYears) {
3221
+ return { success: false, error: '未支持获取年度列表' }
3222
+ }
3223
+ if (sessionIds.length === 0) return { success: true, data: [] }
3224
+ try {
3225
+ const outPtr = [null as any]
3226
+ const result = this.wcdbGetAvailableYears(this.handle, JSON.stringify(sessionIds), outPtr)
3227
+ if (result !== 0 || !outPtr[0]) {
3228
+ return { success: false, error: `获取年度列表失败: ${result}` }
3229
+ }
3230
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
3231
+ if (!jsonStr) return { success: false, error: '解析年度列表失败' }
3232
+ const data = JSON.parse(jsonStr)
3233
+ return { success: true, data }
3234
+ } catch (e) {
3235
+ return { success: false, error: String(e) }
3236
+ }
3237
+ }
3238
+
3239
+ async getAnnualReportStats(sessionIds: string[], beginTimestamp: number = 0, endTimestamp: number = 0): Promise<{ success: boolean; data?: any; error?: string }> {
3240
+ if (!this.ensureReady()) {
3241
+ return { success: false, error: 'WCDB 未连接' }
3242
+ }
3243
+ if (!this.wcdbGetAnnualReportStats) {
3244
+ return this.getAggregateStats(sessionIds, beginTimestamp, endTimestamp)
3245
+ }
3246
+ try {
3247
+ const { begin, end } = this.normalizeRange(beginTimestamp, endTimestamp)
3248
+ const outPtr = [null as any]
3249
+ const result = this.wcdbGetAnnualReportStats(this.handle, JSON.stringify(sessionIds), begin, end, outPtr)
3250
+ if (result !== 0 || !outPtr[0]) {
3251
+ return { success: false, error: `获取年度统计失败: ${result}` }
3252
+ }
3253
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
3254
+ if (!jsonStr) return { success: false, error: '解析年度统计失败' }
3255
+ const data = JSON.parse(jsonStr)
3256
+ return { success: true, data }
3257
+ } catch (e) {
3258
+ return { success: false, error: String(e) }
3259
+ }
3260
+ }
3261
+
3262
+ async getAnnualReportExtras(
3263
+ sessionIds: string[],
3264
+ beginTimestamp: number = 0,
3265
+ endTimestamp: number = 0,
3266
+ peakDayBegin: number = 0,
3267
+ peakDayEnd: number = 0
3268
+ ): Promise<{ success: boolean; data?: any; error?: string }> {
3269
+ if (!this.ensureReady()) {
3270
+ return { success: false, error: 'WCDB 未连接' }
3271
+ }
3272
+ if (!this.wcdbGetAnnualReportExtras) {
3273
+ return { success: false, error: '未支持年度扩展统计' }
3274
+ }
3275
+ if (sessionIds.length === 0) return { success: true, data: {} }
3276
+ try {
3277
+ const { begin, end } = this.normalizeRange(beginTimestamp, endTimestamp)
3278
+ const outPtr = [null as any]
3279
+ const result = this.wcdbGetAnnualReportExtras(
3280
+ this.handle,
3281
+ JSON.stringify(sessionIds),
3282
+ begin,
3283
+ end,
3284
+ this.normalizeTimestamp(peakDayBegin),
3285
+ this.normalizeTimestamp(peakDayEnd),
3286
+ outPtr
3287
+ )
3288
+ if (result !== 0 || !outPtr[0]) {
3289
+ return { success: false, error: `获取年度扩展统计失败: ${result}` }
3290
+ }
3291
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
3292
+ if (!jsonStr) return { success: false, error: '解析年度扩展统计失败' }
3293
+ const data = JSON.parse(jsonStr)
3294
+ return { success: true, data }
3295
+ } catch (e) {
3296
+ return { success: false, error: String(e) }
3297
+ }
3298
+ }
3299
+
3300
+ async getGroupStats(chatroomId: string, beginTimestamp: number = 0, endTimestamp: number = 0): Promise<{ success: boolean; data?: any; error?: string }> {
3301
+ if (!this.ensureReady()) {
3302
+ return { success: false, error: 'WCDB 未连接' }
3303
+ }
3304
+ if (!this.wcdbGetGroupStats) {
3305
+ return this.getAggregateStats([chatroomId], beginTimestamp, endTimestamp)
3306
+ }
3307
+ try {
3308
+ const { begin, end } = this.normalizeRange(beginTimestamp, endTimestamp)
3309
+ const outPtr = [null as any]
3310
+ const result = this.wcdbGetGroupStats(this.handle, chatroomId, begin, end, outPtr)
3311
+ if (result !== 0 || !outPtr[0]) {
3312
+ return { success: false, error: `获取群聊统计失败: ${result}` }
3313
+ }
3314
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
3315
+ if (!jsonStr) return { success: false, error: '解析群聊统计失败' }
3316
+ const data = JSON.parse(jsonStr)
3317
+ return { success: true, data }
3318
+ } catch (e) {
3319
+ return { success: false, error: String(e) }
3320
+ }
3321
+ }
3322
+
3323
+ async getMyFootprintStats(options: {
3324
+ beginTimestamp?: number
3325
+ endTimestamp?: number
3326
+ myWxid?: string
3327
+ privateSessionIds?: string[]
3328
+ groupSessionIds?: string[]
3329
+ mentionLimit?: number
3330
+ privateLimit?: number
3331
+ mentionMode?: 'text_at_me' | string
3332
+ }): Promise<{ success: boolean; data?: any; error?: string }> {
3333
+ if (!this.ensureReady()) {
3334
+ return { success: false, error: 'WCDB 未连接' }
3335
+ }
3336
+ if (!this.wcdbGetMyFootprintStats) {
3337
+ return { success: false, error: '接口未就绪' }
3338
+ }
3339
+
3340
+ try {
3341
+ const normalizedPrivateSessions = Array.from(new Set(
3342
+ (options?.privateSessionIds || [])
3343
+ .map((value) => String(value || '').trim())
3344
+ .filter(Boolean)
3345
+ ))
3346
+ const normalizedGroupSessions = Array.from(new Set(
3347
+ (options?.groupSessionIds || [])
3348
+ .map((value) => String(value || '').trim())
3349
+ .filter(Boolean)
3350
+ ))
3351
+ const mentionLimitRaw = Number(options?.mentionLimit ?? 0)
3352
+ const privateLimitRaw = Number(options?.privateLimit ?? 0)
3353
+ const mentionLimit = Number.isFinite(mentionLimitRaw) && mentionLimitRaw >= 0 ? Math.floor(mentionLimitRaw) : 0
3354
+ const privateLimit = Number.isFinite(privateLimitRaw) && privateLimitRaw >= 0 ? Math.floor(privateLimitRaw) : 0
3355
+
3356
+ const payload = JSON.stringify({
3357
+ begin: this.normalizeTimestamp(options?.beginTimestamp || 0),
3358
+ end: this.normalizeTimestamp(options?.endTimestamp || 0),
3359
+ my_wxid: String(options?.myWxid || '').trim(),
3360
+ private_session_ids: normalizedPrivateSessions,
3361
+ group_session_ids: normalizedGroupSessions,
3362
+ mention_limit: mentionLimit,
3363
+ private_limit: privateLimit,
3364
+ mention_mode: options?.mentionMode || 'text_at_me'
3365
+ })
3366
+
3367
+ const outPtr = [null as any]
3368
+ const result = this.wcdbGetMyFootprintStats(this.handle, payload, outPtr)
3369
+ if (result !== 0 || !outPtr[0]) {
3370
+ return { success: false, error: `获取我的足迹统计失败: ${result}` }
3371
+ }
3372
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
3373
+ if (!jsonStr) {
3374
+ return { success: false, error: '解析我的足迹统计失败' }
3375
+ }
3376
+ return { success: true, data: JSON.parse(jsonStr) || {} }
3377
+ } catch (e) {
3378
+ return { success: false, error: String(e) }
3379
+ }
3380
+ }
3381
+
3382
+ /**
3383
+ * 强制重新打开账号连接(绕过路径缓存),用于微信重装后消息数据库刷新失败时的自动恢复。
3384
+ * 返回重新打开是否成功。
3385
+ */
3386
+ private async forceReopen(): Promise<boolean> {
3387
+ if (!this.currentPath || !this.currentKey || !this.currentWxid) return false
3388
+ const path = this.currentPath
3389
+ const key = this.currentKey
3390
+ const wxid = this.currentWxid
3391
+ this.writeLog('forceReopen: clearing cached handle and reopening...', true)
3392
+ // 清空缓存状态,让 open() 真正重新打开
3393
+ try { this.wcdbShutdown() } catch { }
3394
+ this.handle = null
3395
+ this.currentPath = null
3396
+ this.currentKey = null
3397
+ this.currentWxid = null
3398
+ this.currentDbStoragePath = null
3399
+ this.initialized = false
3400
+ return this.open(path, key, wxid)
3401
+ }
3402
+
3403
+ private shouldRetryCursorAfterNoDb(): boolean {
3404
+ const now = Date.now()
3405
+ if (now - this.lastCursorForceReopenAt < this.cursorForceReopenCooldownMs) {
3406
+ return false
3407
+ }
3408
+ this.lastCursorForceReopenAt = now
3409
+ return true
3410
+ }
3411
+
3412
+ async openMessageCursor(sessionId: string, batchSize: number, ascending: boolean, beginTimestamp: number, endTimestamp: number): Promise<{ success: boolean; cursor?: number; error?: string }> {
3413
+ if (!this.ensureReady()) {
3414
+ return { success: false, error: 'WCDB 未连接' }
3415
+ }
3416
+ try {
3417
+ const outCursor = [0]
3418
+ let result = this.wcdbOpenMessageCursor(
3419
+ this.handle,
3420
+ sessionId,
3421
+ batchSize,
3422
+ ascending ? 1 : 0,
3423
+ beginTimestamp,
3424
+ endTimestamp,
3425
+ outCursor
3426
+ )
3427
+ // result=-3 表示 WCDB_STATUS_NO_MESSAGE_DB:消息数据库缓存为空(常见于微信重装后)
3428
+ // 自动强制重连并重试一次
3429
+ if (result === -3 && outCursor[0] <= 0 && this.shouldRetryCursorAfterNoDb()) {
3430
+ this.writeLog('openMessageCursor: result=-3 (no message db), attempting forceReopen...', true)
3431
+ const reopened = await this.forceReopen()
3432
+ if (reopened && this.handle !== null) {
3433
+ outCursor[0] = 0
3434
+ result = this.wcdbOpenMessageCursor(
3435
+ this.handle,
3436
+ sessionId,
3437
+ batchSize,
3438
+ ascending ? 1 : 0,
3439
+ beginTimestamp,
3440
+ endTimestamp,
3441
+ outCursor
3442
+ )
3443
+ this.writeLog(`openMessageCursor retry after forceReopen: result=${result} cursor=${outCursor[0]}`, true)
3444
+ } else {
3445
+ this.writeLog('openMessageCursor forceReopen failed, giving up', true)
3446
+ }
3447
+ }
3448
+ if (result !== 0 || outCursor[0] <= 0) {
3449
+ if (result !== -3) {
3450
+ await this.printLogs(true)
3451
+ this.writeLog(
3452
+ `openMessageCursor failed: sessionId=${sessionId} batchSize=${batchSize} ascending=${ascending ? 1 : 0} begin=${beginTimestamp} end=${endTimestamp} result=${result} cursor=${outCursor[0]}`,
3453
+ true
3454
+ )
3455
+ }
3456
+ const hint = result === -3
3457
+ ? `创建游标失败: ${result}(消息数据库未找到)。如果你最近重装过微信,请尝试重新指定数据目录后重试`
3458
+ : result === -7
3459
+ ? 'message schema mismatch:当前账号消息表结构与程序要求不一致'
3460
+ : `创建游标失败: ${result},请查看日志`
3461
+ return { success: false, error: hint }
3462
+ }
3463
+ return { success: true, cursor: outCursor[0] }
3464
+ } catch (e) {
3465
+ await this.printLogs(true)
3466
+ this.writeLog(`openMessageCursor exception: ${String(e)}`, true)
3467
+ return { success: false, error: '创建游标异常,请查看日志' }
3468
+ }
3469
+ }
3470
+
3471
+ async openMessageCursorLite(sessionId: string, batchSize: number, ascending: boolean, beginTimestamp: number, endTimestamp: number): Promise<{ success: boolean; cursor?: number; error?: string }> {
3472
+ if (!this.ensureReady()) {
3473
+ return { success: false, error: 'WCDB 未连接' }
3474
+ }
3475
+ if (!this.wcdbOpenMessageCursorLite) {
3476
+ return this.openMessageCursor(sessionId, batchSize, ascending, beginTimestamp, endTimestamp)
3477
+ }
3478
+ try {
3479
+ const outCursor = [0]
3480
+ let result = this.wcdbOpenMessageCursorLite(
3481
+ this.handle,
3482
+ sessionId,
3483
+ batchSize,
3484
+ ascending ? 1 : 0,
3485
+ beginTimestamp,
3486
+ endTimestamp,
3487
+ outCursor
3488
+ )
3489
+
3490
+ // result=-3 表示 WCDB_STATUS_NO_MESSAGE_DB:消息数据库缓存为空
3491
+ // 自动强制重连并重试一次
3492
+ if (result === -3 && outCursor[0] <= 0 && this.shouldRetryCursorAfterNoDb()) {
3493
+ this.writeLog('openMessageCursorLite: result=-3 (no message db), attempting forceReopen...', true)
3494
+ const reopened = await this.forceReopen()
3495
+ if (reopened && this.handle !== null) {
3496
+ outCursor[0] = 0
3497
+ result = this.wcdbOpenMessageCursorLite(
3498
+ this.handle,
3499
+ sessionId,
3500
+ batchSize,
3501
+ ascending ? 1 : 0,
3502
+ beginTimestamp,
3503
+ endTimestamp,
3504
+ outCursor
3505
+ )
3506
+ this.writeLog(`openMessageCursorLite retry after forceReopen: result=${result} cursor=${outCursor[0]}`, true)
3507
+ } else {
3508
+ this.writeLog('openMessageCursorLite forceReopen failed, giving up', true)
3509
+ }
3510
+ }
3511
+
3512
+ if (result !== 0 || outCursor[0] <= 0) {
3513
+ if (result !== -3) {
3514
+ await this.printLogs(true)
3515
+ this.writeLog(
3516
+ `openMessageCursorLite failed: sessionId=${sessionId} batchSize=${batchSize} ascending=${ascending ? 1 : 0} begin=${beginTimestamp} end=${endTimestamp} result=${result} cursor=${outCursor[0]}`,
3517
+ true
3518
+ )
3519
+ }
3520
+ if (result === -7) {
3521
+ return { success: false, error: 'message schema mismatch:当前账号消息表结构与程序要求不一致' }
3522
+ }
3523
+ return { success: false, error: `创建游标失败: ${result},请查看日志` }
3524
+ }
3525
+ return { success: true, cursor: outCursor[0] }
3526
+ } catch (e) {
3527
+ await this.printLogs(true)
3528
+ this.writeLog(`openMessageCursorLite exception: ${String(e)}`, true)
3529
+ return { success: false, error: '创建游标异常,请查看日志' }
3530
+ }
3531
+ }
3532
+
3533
+ async fetchMessageBatch(cursor: number): Promise<{ success: boolean; rows?: any[]; hasMore?: boolean; error?: string }> {
3534
+ if (!this.ensureReady()) {
3535
+ return { success: false, error: 'WCDB 未连接' }
3536
+ }
3537
+ try {
3538
+ const outPtr = [null as any]
3539
+ const outHasMore = [0]
3540
+ const result = this.wcdbFetchMessageBatch(this.handle, cursor, outPtr, outHasMore)
3541
+ if (result !== 0 || !outPtr[0]) {
3542
+ return { success: false, error: `获取批次失败: ${result}` }
3543
+ }
3544
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
3545
+ if (!jsonStr) return { success: false, error: '解析批次失败' }
3546
+ const rows = this.parseMessageJson(jsonStr)
3547
+ return { success: true, rows, hasMore: outHasMore[0] === 1 }
3548
+ } catch (e) {
3549
+ return { success: false, error: String(e) }
3550
+ }
3551
+ }
3552
+
3553
+ async closeMessageCursor(cursor: number): Promise<{ success: boolean; error?: string }> {
3554
+ if (!this.ensureReady()) {
3555
+ return { success: false, error: 'WCDB 未连接' }
3556
+ }
3557
+ try {
3558
+ const result = this.wcdbCloseMessageCursor(this.handle, cursor)
3559
+ if (result !== 0) {
3560
+ return { success: false, error: `关闭游标失败: ${result}` }
3561
+ }
3562
+ return { success: true }
3563
+ } catch (e) {
3564
+ return { success: false, error: String(e) }
3565
+ }
3566
+ }
3567
+
3568
+ async getLogs(): Promise<{ success: boolean; logs?: string[]; error?: string }> {
3569
+ if (!this.lib) return { success: false, error: 'DLL 未加载' }
3570
+ if (!this.wcdbGetLogs) return { success: false, error: '接口未就绪' }
3571
+ try {
3572
+ const outPtr = [null as any]
3573
+ const result = this.wcdbGetLogs(outPtr)
3574
+ if (result !== 0 || !outPtr[0]) {
3575
+ return { success: false, error: `获取日志失败: ${result}` }
3576
+ }
3577
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
3578
+ if (!jsonStr) return { success: false, error: '解析日志失败' }
3579
+ return { success: true, logs: JSON.parse(jsonStr) }
3580
+ } catch (e) {
3581
+ return { success: false, error: String(e) }
3582
+ }
3583
+ }
3584
+
3585
+ async execQuery(kind: string, path: string | null, sql: string, params: any[] = []): Promise<{ success: boolean; rows?: any[]; error?: string }> {
3586
+ if (!this.ensureReady()) {
3587
+ return { success: false, error: 'WCDB 未连接' }
3588
+ }
3589
+ const startedAt = Date.now()
3590
+ try {
3591
+ if (!this.wcdbExecQuery) return { success: false, error: '接口未就绪' }
3592
+ const fallbackFlag = /fallback|diag|diagnostic/i.test(String(sql || ''))
3593
+ this.writeLog(`[audit:execQuery] kind=${kind} path=${path || ''} sql_len=${String(sql || '').length} fallback=${fallbackFlag ? 1 : 0}`)
3594
+
3595
+ // 如果提供了参数,使用参数化查询(需要 C++ 层支持)
3596
+ // 注意:当前 wcdbExecQuery 可能不支持参数化,这是一个占位符实现
3597
+ // TODO: 需要更新 C++ 层的 wcdb_exec_query 以支持参数绑定
3598
+ if (params && params.length > 0) {
3599
+ console.warn('[wcdbCore] execQuery: 参数化查询暂未在 C++ 层实现,将使用原始 SQL(可能存在注入风险)')
3600
+ }
3601
+
3602
+ const normalizedKind = String(kind || '').toLowerCase()
3603
+ const isContactQuery = normalizedKind === 'contact' || /\bfrom\s+contact\b/i.test(String(sql))
3604
+ let effectivePath = path || ''
3605
+ if (normalizedKind === 'contact' && !effectivePath) {
3606
+ const resolvedContactDb = this.resolveContactDbPath()
3607
+ if (resolvedContactDb) {
3608
+ effectivePath = resolvedContactDb
3609
+ this.writeLog(`[diag:execQuery] contact path override -> ${effectivePath}`, true)
3610
+ } else {
3611
+ this.writeLog('[diag:execQuery] contact path override miss: Contact/contact.db not found', true)
3612
+ }
3613
+ }
3614
+
3615
+ const outPtr = [null as any]
3616
+ const result = this.wcdbExecQuery(this.handle, kind, effectivePath, sql, outPtr)
3617
+ if (result !== 0 || !outPtr[0]) {
3618
+ if (isContactQuery) {
3619
+ this.writeLog(`[diag:execQuery] contact query failed code=${result} kind=${kind} path=${effectivePath} sql="${this.formatSqlForLog(sql)}"`, true)
3620
+ await this.dumpDbStatus('execQuery-contact-fail')
3621
+ await this.printLogs(true)
3622
+ }
3623
+ return { success: false, error: `执行查询失败: ${result}` }
3624
+ }
3625
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
3626
+ if (!jsonStr) return { success: false, error: '解析查询结果失败' }
3627
+ const rows = JSON.parse(jsonStr)
3628
+ this.writeLog(`[audit:execQuery] done kind=${kind} cost_ms=${Date.now() - startedAt} rows=${Array.isArray(rows) ? rows.length : -1}`)
3629
+ if (isContactQuery) {
3630
+ const count = Array.isArray(rows) ? rows.length : -1
3631
+ this.writeLog(`[diag:execQuery] contact query ok rows=${count} kind=${kind} path=${effectivePath} sql="${this.formatSqlForLog(sql)}"`, true)
3632
+ }
3633
+ return { success: true, rows }
3634
+ } catch (e) {
3635
+ this.writeLog(`[audit:execQuery] fail kind=${kind} cost_ms=${Date.now() - startedAt} err=${String(e)}`)
3636
+ const isContactQuery = String(kind).toLowerCase() === 'contact' || /\bfrom\s+contact\b/i.test(String(sql))
3637
+ if (isContactQuery) {
3638
+ this.writeLog(`[diag:execQuery] contact query exception kind=${kind} path=${path || ''} sql="${this.formatSqlForLog(sql)}" err=${String(e)}`, true)
3639
+ await this.dumpDbStatus('execQuery-contact-exception')
3640
+ }
3641
+ return { success: false, error: String(e) }
3642
+ }
3643
+ }
3644
+
3645
+ async getEmoticonCdnUrl(dbPath: string, md5: string): Promise<{ success: boolean; url?: string; error?: string }> {
3646
+ if (!this.ensureReady()) {
3647
+ return { success: false, error: 'WCDB 未连接' }
3648
+ }
3649
+ try {
3650
+ const outPtr = [null as any]
3651
+ const result = this.wcdbGetEmoticonCdnUrl(this.handle, dbPath, md5, outPtr)
3652
+ if (result !== 0 || !outPtr[0]) {
3653
+ return { success: false, error: `获取表情 URL 失败: ${result}` }
3654
+ }
3655
+ const urlStr = this.decodeJsonPtr(outPtr[0])
3656
+ if (urlStr === null) return { success: false, error: '解析表情 URL 失败' }
3657
+ return { success: true, url: urlStr || undefined }
3658
+ } catch (e) {
3659
+ return { success: false, error: String(e) }
3660
+ }
3661
+ }
3662
+
3663
+ async getEmoticonCaption(dbPath: string, md5: string): Promise<{ success: boolean; caption?: string; error?: string }> {
3664
+ if (!this.ensureReady()) {
3665
+ return { success: false, error: 'WCDB 未连接' }
3666
+ }
3667
+ if (!this.wcdbGetEmoticonCaption) {
3668
+ return { success: false, error: '接口未就绪: wcdb_get_emoticon_caption' }
3669
+ }
3670
+ try {
3671
+ const outPtr = [null as any]
3672
+ const result = this.wcdbGetEmoticonCaption(this.handle, dbPath || '', md5, outPtr)
3673
+ if (result !== 0 || !outPtr[0]) {
3674
+ return { success: false, error: `获取表情释义失败: ${result}` }
3675
+ }
3676
+ const captionStr = this.decodeJsonPtr(outPtr[0])
3677
+ if (captionStr === null) return { success: false, error: '解析表情释义失败' }
3678
+ return { success: true, caption: captionStr || undefined }
3679
+ } catch (e) {
3680
+ return { success: false, error: String(e) }
3681
+ }
3682
+ }
3683
+
3684
+ async getEmoticonCaptionStrict(md5: string): Promise<{ success: boolean; caption?: string; error?: string }> {
3685
+ if (!this.ensureReady()) {
3686
+ return { success: false, error: 'WCDB 未连接' }
3687
+ }
3688
+ if (!this.wcdbGetEmoticonCaptionStrict) {
3689
+ return { success: false, error: '接口未就绪: wcdb_get_emoticon_caption_strict' }
3690
+ }
3691
+ try {
3692
+ const outPtr = [null as any]
3693
+ const result = this.wcdbGetEmoticonCaptionStrict(this.handle, md5, outPtr)
3694
+ if (result !== 0 || !outPtr[0]) {
3695
+ return { success: false, error: `获取表情释义失败(strict): ${result}` }
3696
+ }
3697
+ const captionStr = this.decodeJsonPtr(outPtr[0])
3698
+ if (captionStr === null) return { success: false, error: '解析表情释义失败(strict)' }
3699
+ return { success: true, caption: captionStr || undefined }
3700
+ } catch (e) {
3701
+ return { success: false, error: String(e) }
3702
+ }
3703
+ }
3704
+
3705
+ async listMessageDbs(): Promise<{ success: boolean; data?: string[]; error?: string }> {
3706
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
3707
+ try {
3708
+ const outPtr = [null as any]
3709
+ const result = this.wcdbListMessageDbs(this.handle, outPtr)
3710
+ if (result !== 0 || !outPtr[0]) return { success: false, error: `获取消息库列表失败: ${result}` }
3711
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
3712
+ if (!jsonStr) return { success: false, error: '解析消息库列表失败' }
3713
+ const data = JSON.parse(jsonStr)
3714
+ return { success: true, data }
3715
+ } catch (e) {
3716
+ return { success: false, error: String(e) }
3717
+ }
3718
+ }
3719
+
3720
+ async listMediaDbs(): Promise<{ success: boolean; data?: string[]; error?: string }> {
3721
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
3722
+ try {
3723
+ const outPtr = [null as any]
3724
+ const result = this.wcdbListMediaDbs(this.handle, outPtr)
3725
+ if (result !== 0 || !outPtr[0]) return { success: false, error: `获取媒体库列表失败: ${result}` }
3726
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
3727
+ if (!jsonStr) return { success: false, error: '解析媒体库列表失败' }
3728
+ const data = JSON.parse(jsonStr)
3729
+ return { success: true, data }
3730
+ } catch (e) {
3731
+ return { success: false, error: String(e) }
3732
+ }
3733
+ } async getMessageById(sessionId: string, localId: number): Promise<{ success: boolean; message?: any; error?: string }> {
3734
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
3735
+ try {
3736
+ const outPtr = [null as any]
3737
+ const result = this.wcdbGetMessageById(this.handle, sessionId, localId, outPtr)
3738
+ if (result !== 0 || !outPtr[0]) return { success: false, error: `查询消息失败: ${result}` }
3739
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
3740
+ if (!jsonStr) return { success: false, error: '解析消息失败' }
3741
+ const message = this.parseMessageJson(jsonStr)
3742
+ // 处理 wcdb_get_message_by_id 返回空对象的情况
3743
+ if (Object.keys(message).length === 0) return { success: false, error: '未找到消息' }
3744
+ return { success: true, message }
3745
+ } catch (e) {
3746
+ return { success: false, error: String(e) }
3747
+ }
3748
+ }
3749
+
3750
+ async getVoiceData(sessionId: string, createTime: number, candidates: string[], localId: number = 0, svrId: string | number = 0): Promise<{ success: boolean; hex?: string; error?: string }> {
3751
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
3752
+ if (!this.wcdbGetVoiceData) return { success: false, error: '当前数据服务版本不支持获取语音数据' }
3753
+ try {
3754
+ const outPtr = [null as any]
3755
+ const result = this.wcdbGetVoiceData(this.handle, sessionId, createTime, localId, BigInt(svrId || 0), JSON.stringify(candidates), outPtr)
3756
+ if (result !== 0 || !outPtr[0]) {
3757
+ return { success: false, error: `获取语音数据失败: ${result}` }
3758
+ }
3759
+ const hex = this.decodeJsonPtr(outPtr[0])
3760
+ if (hex === null) return { success: false, error: '解析语音数据失败' }
3761
+ return { success: true, hex: hex || undefined }
3762
+ } catch (e) {
3763
+ return { success: false, error: String(e) }
3764
+ }
3765
+ }
3766
+
3767
+ async getVoiceDataBatch(
3768
+ requests: Array<{ session_id: string; create_time: number; local_id?: number; svr_id?: string | number; candidates?: string[] }>
3769
+ ): Promise<{ success: boolean; rows?: Array<{ index: number; hex?: string }>; error?: string }> {
3770
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
3771
+ if (!this.wcdbGetVoiceDataBatch) return { success: false, error: '接口未就绪' }
3772
+ try {
3773
+ const outPtr = [null as any]
3774
+ const payload = JSON.stringify(Array.isArray(requests) ? requests : [])
3775
+ const result = this.wcdbGetVoiceDataBatch(this.handle, payload, outPtr)
3776
+ if (result !== 0 || !outPtr[0]) return { success: false, error: `批量获取语音数据失败: ${result}` }
3777
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
3778
+ if (!jsonStr) return { success: false, error: '解析批量语音数据失败' }
3779
+ const rows = JSON.parse(jsonStr)
3780
+ const normalized = Array.isArray(rows) ? rows.map((row: any) => ({
3781
+ index: Number(row?.index ?? 0),
3782
+ hex: row?.hex ? String(row.hex) : undefined
3783
+ })) : []
3784
+ return { success: true, rows: normalized }
3785
+ } catch (e) {
3786
+ return { success: false, error: String(e) }
3787
+ }
3788
+ }
3789
+
3790
+ async getMediaSchemaSummary(dbPath: string): Promise<{ success: boolean; data?: any; error?: string }> {
3791
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
3792
+ if (!this.wcdbGetMediaSchemaSummary) return { success: false, error: '接口未就绪' }
3793
+ try {
3794
+ const outPtr = [null as any]
3795
+ const result = this.wcdbGetMediaSchemaSummary(this.handle, dbPath, outPtr)
3796
+ if (result !== 0 || !outPtr[0]) return { success: false, error: `获取媒体表结构摘要失败: ${result}` }
3797
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
3798
+ if (!jsonStr) return { success: false, error: '解析媒体表结构摘要失败' }
3799
+ const data = JSON.parse(jsonStr) || {}
3800
+ return { success: true, data }
3801
+ } catch (e) {
3802
+ return { success: false, error: String(e) }
3803
+ }
3804
+ }
3805
+
3806
+ async getHeadImageBuffers(usernames: string[]): Promise<{ success: boolean; map?: Record<string, string>; error?: string }> {
3807
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
3808
+ if (!this.wcdbGetHeadImageBuffers) return { success: false, error: '接口未就绪' }
3809
+ try {
3810
+ const outPtr = [null as any]
3811
+ const result = this.wcdbGetHeadImageBuffers(this.handle, JSON.stringify(usernames || []), outPtr)
3812
+ if (result !== 0 || !outPtr[0]) return { success: false, error: `获取头像二进制失败: ${result}` }
3813
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
3814
+ if (!jsonStr) return { success: false, error: '解析头像二进制失败' }
3815
+ const map = JSON.parse(jsonStr) || {}
3816
+ return { success: true, map }
3817
+ } catch (e) {
3818
+ return { success: false, error: String(e) }
3819
+ }
3820
+ }
3821
+
3822
+ async resolveImageHardlink(md5: string, accountDir?: string): Promise<{ success: boolean; data?: any; error?: string }> {
3823
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
3824
+ if (!this.wcdbResolveImageHardlink) return { success: false, error: '接口未就绪' }
3825
+ try {
3826
+ const normalizedMd5 = String(md5 || '').trim().toLowerCase()
3827
+ const normalizedAccountDir = String(accountDir || '').trim()
3828
+ if (!normalizedMd5) return { success: false, error: 'md5 为空' }
3829
+ const cacheKey = this.makeHardlinkCacheKey(normalizedMd5, normalizedAccountDir)
3830
+ const cached = this.readHardlinkCache(this.imageHardlinkCache, cacheKey)
3831
+ if (cached) return cached
3832
+
3833
+ const outPtr = [null as any]
3834
+ const result = this.wcdbResolveImageHardlink(this.handle, normalizedMd5, normalizedAccountDir || null, outPtr)
3835
+ if (result !== 0 || !outPtr[0]) return { success: false, error: `解析图片 hardlink 失败: ${result}` }
3836
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
3837
+ if (!jsonStr) return { success: false, error: '解析图片 hardlink 响应失败' }
3838
+ const data = JSON.parse(jsonStr) || {}
3839
+ const finalResult = { success: true, data }
3840
+ this.writeHardlinkCache(this.imageHardlinkCache, cacheKey, finalResult)
3841
+ return finalResult
3842
+ } catch (e) {
3843
+ return { success: false, error: String(e) }
3844
+ }
3845
+ }
3846
+
3847
+ async resolveVideoHardlinkMd5(md5: string, dbPath?: string): Promise<{ success: boolean; data?: any; error?: string }> {
3848
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
3849
+ if (!this.wcdbResolveVideoHardlinkMd5) return { success: false, error: '接口未就绪' }
3850
+ try {
3851
+ const normalizedMd5 = String(md5 || '').trim().toLowerCase()
3852
+ const normalizedDbPath = String(dbPath || '').trim()
3853
+ if (!normalizedMd5) return { success: false, error: 'md5 为空' }
3854
+ const cacheKey = this.makeHardlinkCacheKey(normalizedMd5, normalizedDbPath)
3855
+ const cached = this.readHardlinkCache(this.videoHardlinkCache, cacheKey)
3856
+ if (cached) return cached
3857
+
3858
+ const outPtr = [null as any]
3859
+ const result = this.wcdbResolveVideoHardlinkMd5(this.handle, normalizedMd5, normalizedDbPath || null, outPtr)
3860
+ if (result !== 0 || !outPtr[0]) return { success: false, error: `解析视频 hardlink 失败: ${result}` }
3861
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
3862
+ if (!jsonStr) return { success: false, error: '解析视频 hardlink 响应失败' }
3863
+ const data = JSON.parse(jsonStr) || {}
3864
+ const finalResult = { success: true, data }
3865
+ this.writeHardlinkCache(this.videoHardlinkCache, cacheKey, finalResult)
3866
+ return finalResult
3867
+ } catch (e) {
3868
+ return { success: false, error: String(e) }
3869
+ }
3870
+ }
3871
+
3872
+ async resolveImageHardlinkBatch(
3873
+ requests: Array<{ md5: string; accountDir?: string }>
3874
+ ): Promise<{ success: boolean; rows?: Array<{ index: number; md5: string; success: boolean; data?: any; error?: string }>; error?: string }> {
3875
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
3876
+ if (!Array.isArray(requests)) return { success: false, error: '参数错误: requests 必须是数组' }
3877
+ try {
3878
+ const normalizedRequests = requests.map((req) => ({
3879
+ md5: String(req?.md5 || '').trim().toLowerCase(),
3880
+ accountDir: String(req?.accountDir || '').trim()
3881
+ }))
3882
+ const rows: Array<{ index: number; md5: string; success: boolean; data?: any; error?: string }> = new Array(normalizedRequests.length)
3883
+ const unresolved: Array<{ index: number; md5: string; accountDir: string }> = []
3884
+
3885
+ for (let i = 0; i < normalizedRequests.length; i += 1) {
3886
+ const req = normalizedRequests[i]
3887
+ if (!req.md5) {
3888
+ rows[i] = { index: i, md5: '', success: false, error: 'md5 为空' }
3889
+ continue
3890
+ }
3891
+ const cacheKey = this.makeHardlinkCacheKey(req.md5, req.accountDir)
3892
+ const cached = this.readHardlinkCache(this.imageHardlinkCache, cacheKey)
3893
+ if (cached) {
3894
+ rows[i] = {
3895
+ index: i,
3896
+ md5: req.md5,
3897
+ success: cached.success === true,
3898
+ data: cached.data,
3899
+ error: cached.error
3900
+ }
3901
+ } else {
3902
+ unresolved.push({ index: i, md5: req.md5, accountDir: req.accountDir })
3903
+ }
3904
+ }
3905
+
3906
+ if (unresolved.length === 0) {
3907
+ return { success: true, rows }
3908
+ }
3909
+
3910
+ if (this.wcdbResolveImageHardlinkBatch) {
3911
+ try {
3912
+ const outPtr = [null as any]
3913
+ const payload = JSON.stringify(unresolved.map((req) => ({
3914
+ md5: req.md5,
3915
+ account_dir: req.accountDir || undefined
3916
+ })))
3917
+ const result = this.wcdbResolveImageHardlinkBatch(this.handle, payload, outPtr)
3918
+ if (result === 0 && outPtr[0]) {
3919
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
3920
+ if (jsonStr) {
3921
+ const nativeRows = JSON.parse(jsonStr)
3922
+ const mappedRows = Array.isArray(nativeRows) ? nativeRows.map((row: any, index: number) => {
3923
+ const rowIndexRaw = Number(row?.index)
3924
+ const rowIndex = Number.isFinite(rowIndexRaw) ? Math.floor(rowIndexRaw) : index
3925
+ const fallbackReq = rowIndex >= 0 && rowIndex < unresolved.length ? unresolved[rowIndex] : { md5: '', accountDir: '', index: -1 }
3926
+ const rowMd5 = String(row?.md5 || fallbackReq.md5 || '').trim().toLowerCase()
3927
+ const success = row?.success === true || row?.success === 1 || row?.success === '1'
3928
+ const data = row?.data && typeof row.data === 'object' ? row.data : {}
3929
+ const error = row?.error ? String(row.error) : undefined
3930
+ if (success && rowMd5) {
3931
+ const cacheKey = this.makeHardlinkCacheKey(rowMd5, fallbackReq.accountDir)
3932
+ this.writeHardlinkCache(this.imageHardlinkCache, cacheKey, { success: true, data })
3933
+ }
3934
+ return {
3935
+ index: rowIndex,
3936
+ md5: rowMd5,
3937
+ success,
3938
+ data,
3939
+ error
3940
+ }
3941
+ }) : []
3942
+ for (const row of mappedRows) {
3943
+ const fallbackReq = row.index >= 0 && row.index < unresolved.length ? unresolved[row.index] : null
3944
+ if (!fallbackReq) continue
3945
+ rows[fallbackReq.index] = {
3946
+ index: fallbackReq.index,
3947
+ md5: row.md5 || fallbackReq.md5,
3948
+ success: row.success,
3949
+ data: row.data,
3950
+ error: row.error
3951
+ }
3952
+ }
3953
+ }
3954
+ }
3955
+ } catch {
3956
+ // 回退到单条循环实现
3957
+ }
3958
+ }
3959
+
3960
+ for (const req of unresolved) {
3961
+ if (rows[req.index]) continue
3962
+ const result = await this.resolveImageHardlink(req.md5, req.accountDir)
3963
+ rows[req.index] = {
3964
+ index: req.index,
3965
+ md5: req.md5,
3966
+ success: result.success === true,
3967
+ data: result.data,
3968
+ error: result.error
3969
+ }
3970
+ }
3971
+ return { success: true, rows }
3972
+ } catch (e) {
3973
+ return { success: false, error: String(e) }
3974
+ }
3975
+ }
3976
+
3977
+ async resolveVideoHardlinkMd5Batch(
3978
+ requests: Array<{ md5: string; dbPath?: string }>
3979
+ ): Promise<{ success: boolean; rows?: Array<{ index: number; md5: string; success: boolean; data?: any; error?: string }>; error?: string }> {
3980
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
3981
+ if (!Array.isArray(requests)) return { success: false, error: '参数错误: requests 必须是数组' }
3982
+ try {
3983
+ const normalizedRequests = requests.map((req) => ({
3984
+ md5: String(req?.md5 || '').trim().toLowerCase(),
3985
+ dbPath: String(req?.dbPath || '').trim()
3986
+ }))
3987
+ const rows: Array<{ index: number; md5: string; success: boolean; data?: any; error?: string }> = new Array(normalizedRequests.length)
3988
+ const unresolved: Array<{ index: number; md5: string; dbPath: string }> = []
3989
+
3990
+ for (let i = 0; i < normalizedRequests.length; i += 1) {
3991
+ const req = normalizedRequests[i]
3992
+ if (!req.md5) {
3993
+ rows[i] = { index: i, md5: '', success: false, error: 'md5 为空' }
3994
+ continue
3995
+ }
3996
+ const cacheKey = this.makeHardlinkCacheKey(req.md5, req.dbPath)
3997
+ const cached = this.readHardlinkCache(this.videoHardlinkCache, cacheKey)
3998
+ if (cached) {
3999
+ rows[i] = {
4000
+ index: i,
4001
+ md5: req.md5,
4002
+ success: cached.success === true,
4003
+ data: cached.data,
4004
+ error: cached.error
4005
+ }
4006
+ } else {
4007
+ unresolved.push({ index: i, md5: req.md5, dbPath: req.dbPath })
4008
+ }
4009
+ }
4010
+
4011
+ if (unresolved.length === 0) {
4012
+ return { success: true, rows }
4013
+ }
4014
+
4015
+ if (this.wcdbResolveVideoHardlinkMd5Batch) {
4016
+ try {
4017
+ const outPtr = [null as any]
4018
+ const payload = JSON.stringify(unresolved.map((req) => ({
4019
+ md5: req.md5,
4020
+ db_path: req.dbPath || undefined
4021
+ })))
4022
+ const result = this.wcdbResolveVideoHardlinkMd5Batch(this.handle, payload, outPtr)
4023
+ if (result === 0 && outPtr[0]) {
4024
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
4025
+ if (jsonStr) {
4026
+ const nativeRows = JSON.parse(jsonStr)
4027
+ const mappedRows = Array.isArray(nativeRows) ? nativeRows.map((row: any, index: number) => {
4028
+ const rowIndexRaw = Number(row?.index)
4029
+ const rowIndex = Number.isFinite(rowIndexRaw) ? Math.floor(rowIndexRaw) : index
4030
+ const fallbackReq = rowIndex >= 0 && rowIndex < unresolved.length ? unresolved[rowIndex] : { md5: '', dbPath: '', index: -1 }
4031
+ const rowMd5 = String(row?.md5 || fallbackReq.md5 || '').trim().toLowerCase()
4032
+ const success = row?.success === true || row?.success === 1 || row?.success === '1'
4033
+ const data = row?.data && typeof row.data === 'object' ? row.data : {}
4034
+ const error = row?.error ? String(row.error) : undefined
4035
+ if (success && rowMd5) {
4036
+ const cacheKey = this.makeHardlinkCacheKey(rowMd5, fallbackReq.dbPath)
4037
+ this.writeHardlinkCache(this.videoHardlinkCache, cacheKey, { success: true, data })
4038
+ }
4039
+ return {
4040
+ index: rowIndex,
4041
+ md5: rowMd5,
4042
+ success,
4043
+ data,
4044
+ error
4045
+ }
4046
+ }) : []
4047
+ for (const row of mappedRows) {
4048
+ const fallbackReq = row.index >= 0 && row.index < unresolved.length ? unresolved[row.index] : null
4049
+ if (!fallbackReq) continue
4050
+ rows[fallbackReq.index] = {
4051
+ index: fallbackReq.index,
4052
+ md5: row.md5 || fallbackReq.md5,
4053
+ success: row.success,
4054
+ data: row.data,
4055
+ error: row.error
4056
+ }
4057
+ }
4058
+ }
4059
+ }
4060
+ } catch {
4061
+ // 回退到单条循环实现
4062
+ }
4063
+ }
4064
+
4065
+ for (const req of unresolved) {
4066
+ if (rows[req.index]) continue
4067
+ const result = await this.resolveVideoHardlinkMd5(req.md5, req.dbPath)
4068
+ rows[req.index] = {
4069
+ index: req.index,
4070
+ md5: req.md5,
4071
+ success: result.success === true,
4072
+ data: result.data,
4073
+ error: result.error
4074
+ }
4075
+ }
4076
+ return { success: true, rows }
4077
+ } catch (e) {
4078
+ return { success: false, error: String(e) }
4079
+ }
4080
+ }
4081
+
4082
+ /**
4083
+ * 数据收集初始化
4084
+ */
4085
+ async cloudInit(intervalSeconds: number = 600): Promise<{ success: boolean; error?: string }> {
4086
+ if (!this.initialized) {
4087
+ const initOk = await this.initialize()
4088
+ if (!initOk) return { success: false, error: 'WCDB init failed' }
4089
+ }
4090
+ if (!this.wcdbCloudInit) {
4091
+ return { success: false, error: 'Cloud init API not supported by DLL' }
4092
+ }
4093
+ try {
4094
+ const result = this.wcdbCloudInit(intervalSeconds)
4095
+ if (result !== 0) {
4096
+ return { success: false, error: `Cloud init failed: ${result}` }
4097
+ }
4098
+ return { success: true }
4099
+ } catch (e) {
4100
+ return { success: false, error: String(e) }
4101
+ }
4102
+ }
4103
+
4104
+ async cloudReport(statsJson: string): Promise<{ success: boolean; error?: string }> {
4105
+ if (!this.initialized) {
4106
+ const initOk = await this.initialize()
4107
+ if (!initOk) return { success: false, error: 'WCDB init failed' }
4108
+ }
4109
+ if (!this.wcdbCloudReport) {
4110
+ return { success: false, error: 'Cloud report API not supported by DLL' }
4111
+ }
4112
+ try {
4113
+ const result = this.wcdbCloudReport(statsJson || '')
4114
+ if (result !== 0) {
4115
+ return { success: false, error: `Cloud report failed: ${result}` }
4116
+ }
4117
+ return { success: true }
4118
+ } catch (e) {
4119
+ return { success: false, error: String(e) }
4120
+ }
4121
+ }
4122
+
4123
+ cloudStop(): { success: boolean; error?: string } {
4124
+ if (!this.wcdbCloudStop) {
4125
+ return { success: false, error: 'Cloud stop API not supported by DLL' }
4126
+ }
4127
+ try {
4128
+ this.wcdbCloudStop()
4129
+ return { success: true }
4130
+ } catch (e) {
4131
+ return { success: false, error: String(e) }
4132
+ }
4133
+ }
4134
+ async verifyUser(message: string, hwnd?: string): Promise<{ success: boolean; error?: string }> {
4135
+ if (!this.initialized) {
4136
+ const initOk = await this.initialize()
4137
+ if (!initOk) return { success: false, error: 'WCDB 初始化失败' }
4138
+ }
4139
+
4140
+ if (!this.wcdbVerifyUser) {
4141
+ return { success: false, error: 'Binding not found: VerifyUser' }
4142
+ }
4143
+
4144
+ return new Promise((resolve) => {
4145
+ try {
4146
+ // Allocate buffer for result JSON
4147
+ const maxLen = 1024
4148
+ const outBuf = Buffer.alloc(maxLen)
4149
+
4150
+ // Call native function
4151
+ const hwndVal = hwnd ? BigInt(hwnd) : BigInt(0)
4152
+ this.wcdbVerifyUser(hwndVal, message || '', outBuf, maxLen)
4153
+
4154
+ // Parse result
4155
+ const jsonStr = this.koffi.decode(outBuf, 'char', -1)
4156
+ const result = JSON.parse(jsonStr)
4157
+ resolve(result)
4158
+ } catch (e) {
4159
+ resolve({ success: false, error: String(e) })
4160
+ }
4161
+ })
4162
+ }
4163
+
4164
+ async searchMessages(keyword: string, sessionId?: string, limit?: number, offset?: number, beginTimestamp?: number, endTimestamp?: number): Promise<{ success: boolean; messages?: any[]; error?: string }> {
4165
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
4166
+ if (!this.wcdbSearchMessages) return { success: false, error: '当前数据服务版本不支持搜索消息' }
4167
+ try {
4168
+ const handle = this.handle
4169
+ await new Promise(resolve => setImmediate(resolve))
4170
+ if (handle === null || this.handle !== handle) return { success: false, error: '连接已断开' }
4171
+ const outPtr = [null as any]
4172
+ const result = this.wcdbSearchMessages(
4173
+ handle,
4174
+ sessionId || '',
4175
+ keyword,
4176
+ limit || 50,
4177
+ offset || 0,
4178
+ beginTimestamp || 0,
4179
+ endTimestamp || 0,
4180
+ outPtr
4181
+ )
4182
+ if (result !== 0 || !outPtr[0]) {
4183
+ return { success: false, error: `搜索消息失败: ${result}` }
4184
+ }
4185
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
4186
+ if (!jsonStr) return { success: false, error: '解析搜索结果失败' }
4187
+ const messages = this.parseMessageJson(jsonStr)
4188
+ return { success: true, messages }
4189
+ } catch (e) {
4190
+ return { success: false, error: String(e) }
4191
+ }
4192
+ }
4193
+
4194
+ async getSnsTimeline(limit: number, offset: number, usernames?: string[], keyword?: string, startTime?: number, endTime?: number): Promise<{ success: boolean; timeline?: any[]; error?: string }> {
4195
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
4196
+ if (!this.wcdbGetSnsTimeline) return { success: false, error: '当前数据服务版本不支持获取朋友圈' }
4197
+ try {
4198
+ const outPtr = [null as any]
4199
+ const usernamesJson = usernames && usernames.length > 0 ? JSON.stringify(usernames) : ''
4200
+ const result = this.wcdbGetSnsTimeline(
4201
+ this.handle,
4202
+ limit,
4203
+ offset,
4204
+ usernamesJson,
4205
+ keyword || '',
4206
+ startTime || 0,
4207
+ endTime || 0,
4208
+ outPtr
4209
+ )
4210
+ if (result !== 0 || !outPtr[0]) {
4211
+ return { success: false, error: `获取朋友圈失败: ${result}` }
4212
+ }
4213
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
4214
+ if (!jsonStr) return { success: false, error: '解析朋友圈数据失败' }
4215
+ const timeline = JSON.parse(jsonStr)
4216
+ return { success: true, timeline }
4217
+ } catch (e) {
4218
+ return { success: false, error: String(e) }
4219
+ }
4220
+ }
4221
+
4222
+ async getSnsAnnualStats(beginTimestamp: number, endTimestamp: number): Promise<{ success: boolean; data?: any; error?: string }> {
4223
+ if (!this.ensureReady()) {
4224
+ return { success: false, error: 'WCDB 未连接' }
4225
+ }
4226
+ try {
4227
+ if (!this.wcdbGetSnsAnnualStats) {
4228
+ return { success: false, error: 'wcdbGetSnsAnnualStats 未找到' }
4229
+ }
4230
+ await new Promise(resolve => setImmediate(resolve))
4231
+ const outPtr = [null as any]
4232
+ const result = this.wcdbGetSnsAnnualStats(this.handle, beginTimestamp, endTimestamp, outPtr)
4233
+ await new Promise(resolve => setImmediate(resolve))
4234
+
4235
+ if (result !== 0 || !outPtr[0]) {
4236
+ return { success: false, error: `getSnsAnnualStats failed: ${result}` }
4237
+ }
4238
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
4239
+ if (!jsonStr) return { success: false, error: 'Failed to decode JSON' }
4240
+ return { success: true, data: JSON.parse(jsonStr) }
4241
+ } catch (e) {
4242
+ console.error('getSnsAnnualStats 异常:', e)
4243
+ return { success: false, error: String(e) }
4244
+ }
4245
+ }
4246
+
4247
+ async getSnsUsernames(): Promise<{ success: boolean; usernames?: string[]; error?: string }> {
4248
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
4249
+ if (!this.wcdbGetSnsUsernames) return { success: false, error: '接口未就绪' }
4250
+ try {
4251
+ const outPtr = [null as any]
4252
+ const result = this.wcdbGetSnsUsernames(this.handle, outPtr)
4253
+ if (result !== 0 || !outPtr[0]) return { success: false, error: `获取朋友圈用户名失败: ${result}` }
4254
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
4255
+ if (!jsonStr) return { success: false, error: '解析朋友圈用户名失败' }
4256
+ const usernames = JSON.parse(jsonStr)
4257
+ return { success: true, usernames: Array.isArray(usernames) ? usernames.map((u: any) => String(u || '').trim()).filter(Boolean) : [] }
4258
+ } catch (e) {
4259
+ return { success: false, error: String(e) }
4260
+ }
4261
+ }
4262
+
4263
+ async getSnsExportStats(myWxid?: string): Promise<{ success: boolean; data?: { totalPosts: number; totalFriends: number; myPosts: number | null }; error?: string }> {
4264
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
4265
+ if (!this.wcdbGetSnsExportStats) return { success: false, error: '接口未就绪' }
4266
+ try {
4267
+ const outPtr = [null as any]
4268
+ const result = this.wcdbGetSnsExportStats(this.handle, myWxid || null, outPtr)
4269
+ if (result !== 0 || !outPtr[0]) return { success: false, error: `获取朋友圈导出统计失败: ${result}` }
4270
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
4271
+ if (!jsonStr) return { success: false, error: '解析朋友圈导出统计失败' }
4272
+ const raw = JSON.parse(jsonStr) || {}
4273
+ return {
4274
+ success: true,
4275
+ data: {
4276
+ totalPosts: Number(raw.total_posts || 0),
4277
+ totalFriends: Number(raw.total_friends || 0),
4278
+ myPosts: raw.my_posts === null || raw.my_posts === undefined ? null : Number(raw.my_posts || 0)
4279
+ }
4280
+ }
4281
+ } catch (e) {
4282
+ return { success: false, error: String(e) }
4283
+ }
4284
+ }
4285
+
4286
+ async installMessageAntiRevokeTrigger(sessionId: string): Promise<{ success: boolean; alreadyInstalled?: boolean; error?: string }> {
4287
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
4288
+ if (!this.wcdbInstallMessageAntiRevokeTrigger) return { success: false, error: '当前数据服务版本不支持此功能' }
4289
+ const normalizedSessionId = String(sessionId || '').trim()
4290
+ if (!normalizedSessionId) return { success: false, error: 'sessionId 不能为空' }
4291
+ try {
4292
+ const outPtr = [null]
4293
+ const status = this.wcdbInstallMessageAntiRevokeTrigger(this.handle, normalizedSessionId, outPtr)
4294
+ let msg = ''
4295
+ if (outPtr[0]) {
4296
+ try { msg = this.koffi.decode(outPtr[0], 'char', -1) } catch { }
4297
+ try { this.wcdbFreeString(outPtr[0]) } catch { }
4298
+ }
4299
+ if (status === 1) {
4300
+ return { success: true, alreadyInstalled: true }
4301
+ }
4302
+ if (status !== 0) {
4303
+ return { success: false, error: msg || `DLL error ${status}` }
4304
+ }
4305
+ return { success: true, alreadyInstalled: false }
4306
+ } catch (e) {
4307
+ return { success: false, error: String(e) }
4308
+ }
4309
+ }
4310
+
4311
+ async uninstallMessageAntiRevokeTrigger(sessionId: string): Promise<{ success: boolean; error?: string }> {
4312
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
4313
+ if (!this.wcdbUninstallMessageAntiRevokeTrigger) return { success: false, error: '当前数据服务版本不支持此功能' }
4314
+ const normalizedSessionId = String(sessionId || '').trim()
4315
+ if (!normalizedSessionId) return { success: false, error: 'sessionId 不能为空' }
4316
+ try {
4317
+ const outPtr = [null]
4318
+ const status = this.wcdbUninstallMessageAntiRevokeTrigger(this.handle, normalizedSessionId, outPtr)
4319
+ let msg = ''
4320
+ if (outPtr[0]) {
4321
+ try { msg = this.koffi.decode(outPtr[0], 'char', -1) } catch { }
4322
+ try { this.wcdbFreeString(outPtr[0]) } catch { }
4323
+ }
4324
+ if (status !== 0) {
4325
+ return { success: false, error: msg || `DLL error ${status}` }
4326
+ }
4327
+ return { success: true }
4328
+ } catch (e) {
4329
+ return { success: false, error: String(e) }
4330
+ }
4331
+ }
4332
+
4333
+ async checkMessageAntiRevokeTrigger(sessionId: string): Promise<{ success: boolean; installed?: boolean; error?: string }> {
4334
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
4335
+ if (!this.wcdbCheckMessageAntiRevokeTrigger) return { success: false, error: '当前数据服务版本不支持此功能' }
4336
+ const normalizedSessionId = String(sessionId || '').trim()
4337
+ if (!normalizedSessionId) return { success: false, error: 'sessionId 不能为空' }
4338
+ try {
4339
+ const outInstalled = [0]
4340
+ const status = this.wcdbCheckMessageAntiRevokeTrigger(this.handle, normalizedSessionId, outInstalled)
4341
+ if (status !== 0) {
4342
+ return { success: false, error: `DLL error ${status}` }
4343
+ }
4344
+ return { success: true, installed: outInstalled[0] === 1 }
4345
+ } catch (e) {
4346
+ return { success: false, error: String(e) }
4347
+ }
4348
+ }
4349
+
4350
+ async checkMessageAntiRevokeTriggers(sessionIds: string[]): Promise<{
4351
+ success: boolean
4352
+ rows?: Array<{ sessionId: string; success: boolean; installed?: boolean; error?: string }>
4353
+ error?: string
4354
+ }> {
4355
+ if (!Array.isArray(sessionIds) || sessionIds.length === 0) {
4356
+ return { success: true, rows: [] }
4357
+ }
4358
+ const uniqueIds = Array.from(new Set(sessionIds.map((id) => String(id || '').trim()).filter(Boolean)))
4359
+ const rows: Array<{ sessionId: string; success: boolean; installed?: boolean; error?: string }> = []
4360
+ for (const sessionId of uniqueIds) {
4361
+ const result = await this.checkMessageAntiRevokeTrigger(sessionId)
4362
+ rows.push({ sessionId, success: result.success, installed: result.installed, error: result.error })
4363
+ }
4364
+ return { success: true, rows }
4365
+ }
4366
+
4367
+ async installMessageAntiRevokeTriggers(sessionIds: string[]): Promise<{
4368
+ success: boolean
4369
+ rows?: Array<{ sessionId: string; success: boolean; alreadyInstalled?: boolean; error?: string }>
4370
+ error?: string
4371
+ }> {
4372
+ if (!Array.isArray(sessionIds) || sessionIds.length === 0) {
4373
+ return { success: true, rows: [] }
4374
+ }
4375
+ const uniqueIds = Array.from(new Set(sessionIds.map((id) => String(id || '').trim()).filter(Boolean)))
4376
+ const rows: Array<{ sessionId: string; success: boolean; alreadyInstalled?: boolean; error?: string }> = []
4377
+ for (const sessionId of uniqueIds) {
4378
+ const result = await this.installMessageAntiRevokeTrigger(sessionId)
4379
+ rows.push({ sessionId, success: result.success, alreadyInstalled: result.alreadyInstalled, error: result.error })
4380
+ }
4381
+ return { success: true, rows }
4382
+ }
4383
+
4384
+ async uninstallMessageAntiRevokeTriggers(sessionIds: string[]): Promise<{
4385
+ success: boolean
4386
+ rows?: Array<{ sessionId: string; success: boolean; error?: string }>
4387
+ error?: string
4388
+ }> {
4389
+ if (!Array.isArray(sessionIds) || sessionIds.length === 0) {
4390
+ return { success: true, rows: [] }
4391
+ }
4392
+ const uniqueIds = Array.from(new Set(sessionIds.map((id) => String(id || '').trim()).filter(Boolean)))
4393
+ const rows: Array<{ sessionId: string; success: boolean; error?: string }> = []
4394
+ for (const sessionId of uniqueIds) {
4395
+ const result = await this.uninstallMessageAntiRevokeTrigger(sessionId)
4396
+ rows.push({ sessionId, success: result.success, error: result.error })
4397
+ }
4398
+ return { success: true, rows }
4399
+ }
4400
+
4401
+ /**
4402
+ * 为朋友圈安装删除
4403
+ */
4404
+ async installSnsBlockDeleteTrigger(): Promise<{ success: boolean; alreadyInstalled?: boolean; error?: string }> {
4405
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
4406
+ if (!this.wcdbInstallSnsBlockDeleteTrigger) return { success: false, error: '当前数据服务版本不支持此功能' }
4407
+ try {
4408
+ const outPtr = [null]
4409
+ const status = this.wcdbInstallSnsBlockDeleteTrigger(this.handle, outPtr)
4410
+ let msg = ''
4411
+ if (outPtr[0]) {
4412
+ try { msg = this.koffi.decode(outPtr[0], 'char', -1) } catch { }
4413
+ try { this.wcdbFreeString(outPtr[0]) } catch { }
4414
+ }
4415
+ if (status === 1) {
4416
+ //数据服务返回 1 表示已安装
4417
+ return { success: true, alreadyInstalled: true }
4418
+ }
4419
+ if (status !== 0) {
4420
+ return { success: false, error: msg || `DLL error ${status}` }
4421
+ }
4422
+ return { success: true, alreadyInstalled: false }
4423
+ } catch (e) {
4424
+ return { success: false, error: String(e) }
4425
+ }
4426
+ }
4427
+
4428
+ /**
4429
+ * 关闭朋友圈删除拦截
4430
+ */
4431
+ async uninstallSnsBlockDeleteTrigger(): Promise<{ success: boolean; error?: string }> {
4432
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
4433
+ if (!this.wcdbUninstallSnsBlockDeleteTrigger) return { success: false, error: '当前数据服务版本不支持此功能' }
4434
+ try {
4435
+ const outPtr = [null]
4436
+ const status = this.wcdbUninstallSnsBlockDeleteTrigger(this.handle, outPtr)
4437
+ let msg = ''
4438
+ if (outPtr[0]) {
4439
+ try { msg = this.koffi.decode(outPtr[0], 'char', -1) } catch { }
4440
+ try { this.wcdbFreeString(outPtr[0]) } catch { }
4441
+ }
4442
+ if (status !== 0) {
4443
+ return { success: false, error: msg || `DLL error ${status}` }
4444
+ }
4445
+ return { success: true }
4446
+ } catch (e) {
4447
+ return { success: false, error: String(e) }
4448
+ }
4449
+ }
4450
+
4451
+ /**
4452
+ * 查询朋友圈删除拦截是否已安装
4453
+ */
4454
+ async checkSnsBlockDeleteTrigger(): Promise<{ success: boolean; installed?: boolean; error?: string }> {
4455
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
4456
+ if (!this.wcdbCheckSnsBlockDeleteTrigger) return { success: false, error: '当前数据服务版本不支持此功能' }
4457
+ try {
4458
+ const outInstalled = [0]
4459
+ const status = this.wcdbCheckSnsBlockDeleteTrigger(this.handle, outInstalled)
4460
+ if (status !== 0) {
4461
+ return { success: false, error: `DLL error ${status}` }
4462
+ }
4463
+ return { success: true, installed: outInstalled[0] === 1 }
4464
+ } catch (e) {
4465
+ return { success: false, error: String(e) }
4466
+ }
4467
+ }
4468
+
4469
+ async deleteSnsPost(postId: string): Promise<{ success: boolean; error?: string }> {
4470
+ if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
4471
+ if (!this.wcdbDeleteSnsPost) return { success: false, error: '当前数据服务版本不支持此功能' }
4472
+ try {
4473
+ const outPtr = [null]
4474
+ const status = this.wcdbDeleteSnsPost(this.handle, postId, outPtr)
4475
+ let msg = ''
4476
+ if (outPtr[0]) {
4477
+ try { msg = this.koffi.decode(outPtr[0], 'char', -1) } catch { }
4478
+ try { this.wcdbFreeString(outPtr[0]) } catch { }
4479
+ }
4480
+ if (status !== 0) {
4481
+ return { success: false, error: msg || `DLL error ${status}` }
4482
+ }
4483
+ return { success: true }
4484
+ } catch (e) {
4485
+ return { success: false, error: String(e) }
4486
+ }
4487
+ }
4488
+
4489
+ async getDualReportStats(sessionId: string, beginTimestamp: number = 0, endTimestamp: number = 0): Promise<{ success: boolean; data?: any; error?: string }> {
4490
+ if (!this.ensureReady()) {
4491
+ return { success: false, error: 'WCDB 未连接' }
4492
+ }
4493
+ if (!this.wcdbGetDualReportStats) {
4494
+ return { success: false, error: '未支持双人报告统计' }
4495
+ }
4496
+ try {
4497
+ const { begin, end } = this.normalizeRange(beginTimestamp, endTimestamp)
4498
+ const outPtr = [null as any]
4499
+ const result = this.wcdbGetDualReportStats(this.handle, sessionId, begin, end, outPtr)
4500
+ if (result !== 0 || !outPtr[0]) {
4501
+ return { success: false, error: `获取双人报告统计失败: ${result}` }
4502
+ }
4503
+ const jsonStr = this.decodeJsonPtr(outPtr[0])
4504
+ if (!jsonStr) return { success: false, error: '解析双人报告统计失败' }
4505
+ const data = JSON.parse(jsonStr)
4506
+ return { success: true, data }
4507
+ } catch (e) {
4508
+ return { success: false, error: String(e) }
4509
+ }
4510
+ }
4511
+ /**
4512
+ * 修改消息内容
4513
+ */
4514
+ async updateMessage(sessionId: string, localId: number, createTime: number, newContent: string): Promise<{ success: boolean; error?: string }> {
4515
+ if (!this.initialized || !this.wcdbUpdateMessage) return { success: false, error: 'WCDB Not Initialized or Method Missing' }
4516
+ if (!this.handle) return { success: false, error: 'Not Connected' }
4517
+
4518
+ return new Promise((resolve) => {
4519
+ try {
4520
+ const outError = [null as any]
4521
+ const result = this.wcdbUpdateMessage(this.handle, sessionId, localId, createTime, newContent, outError)
4522
+
4523
+ if (result !== 0) {
4524
+ let errorMsg = 'Unknown Error'
4525
+ if (outError[0]) {
4526
+ errorMsg = this.decodeJsonPtr(outError[0]) || 'Unknown Error (Decode Failed)'
4527
+ }
4528
+ resolve({ success: false, error: errorMsg })
4529
+ return
4530
+ }
4531
+
4532
+ resolve({ success: true })
4533
+ } catch (e) {
4534
+ resolve({ success: false, error: String(e) })
4535
+ }
4536
+ })
4537
+ }
4538
+
4539
+ /**
4540
+ * 删除消息
4541
+ */
4542
+ async deleteMessage(sessionId: string, localId: number, createTime: number, dbPathHint?: string): Promise<{ success: boolean; error?: string }> {
4543
+ if (!this.initialized || !this.wcdbDeleteMessage) return { success: false, error: 'WCDB Not Initialized or Method Missing' }
4544
+ if (!this.handle) return { success: false, error: 'Not Connected' }
4545
+
4546
+ return new Promise((resolve) => {
4547
+ try {
4548
+ const outError = [null as any]
4549
+ const result = this.wcdbDeleteMessage(this.handle, sessionId, localId, createTime || 0, dbPathHint || '', outError)
4550
+
4551
+ if (result !== 0) {
4552
+ let errorMsg = 'Unknown Error'
4553
+ if (outError[0]) {
4554
+ errorMsg = this.decodeJsonPtr(outError[0]) || 'Unknown Error (Decode Failed)'
4555
+ }
4556
+ console.error(`[WcdbCore] deleteMessage fail: code=${result}, error=${errorMsg}`)
4557
+ resolve({ success: false, error: errorMsg })
4558
+ return
4559
+ }
4560
+
4561
+ resolve({ success: true })
4562
+ } catch (e) {
4563
+ console.error(`[WcdbCore] deleteMessage exception:`, e)
4564
+ resolve({ success: false, error: String(e) })
4565
+ }
4566
+ })
4567
+ }
4568
+ }