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,409 @@
1
+ import { join } from 'path'
2
+ import { existsSync, copyFileSync, mkdirSync } from 'fs'
3
+ import { execFile, execSync } from 'child_process'
4
+ import { promisify } from 'util'
5
+ import { createRequire } from 'module'
6
+ import os from 'os'
7
+ import type { DbKeyResult } from '../types.js'
8
+
9
+ const require = createRequire(import.meta.url)
10
+ const execFileAsync = promisify(execFile)
11
+
12
+ export class KeyService {
13
+ private koffi: any = null
14
+ private lib: any = null
15
+ private initialized = false
16
+ private initHook: any = null
17
+ private pollKeyData: any = null
18
+ private getStatusMessage: any = null
19
+ private cleanupHook: any = null
20
+ private getLastErrorMsg: any = null
21
+
22
+ // Win32 APIs
23
+ private kernel32: any = null
24
+ private OpenProcess: any = null
25
+ private CloseHandle: any = null
26
+ private QueryFullProcessImageNameW: any = null
27
+
28
+ private readonly PROCESS_ALL_ACCESS = 0x1F0FFF
29
+
30
+ private getDllPath(): string {
31
+ const archDir = process.arch === 'arm64' ? 'arm64' : 'x64'
32
+ const candidates: string[] = []
33
+
34
+ if (process.env.WX_KEY_DLL_PATH) {
35
+ candidates.push(process.env.WX_KEY_DLL_PATH)
36
+ }
37
+
38
+ const cwd = process.cwd()
39
+ candidates.push(join(cwd, 'resources', 'key', 'win32', archDir, 'wx_key.dll'))
40
+ candidates.push(join(cwd, 'resources', 'key', 'win32', 'x64', 'wx_key.dll'))
41
+ candidates.push(join(cwd, 'resources', 'key', 'win32', 'wx_key.dll'))
42
+ candidates.push(join(cwd, 'resources', 'wx_key.dll'))
43
+
44
+ for (const path of candidates) {
45
+ if (existsSync(path)) return path
46
+ }
47
+
48
+ return candidates[0]
49
+ }
50
+
51
+ private isNetworkPath(path: string): boolean {
52
+ return path.startsWith('\\\\')
53
+ }
54
+
55
+ private localizeNetworkDll(originalPath: string): string {
56
+ try {
57
+ const tempDir = join(os.tmpdir(), 'weflow_dll_cache')
58
+ if (!existsSync(tempDir)) {
59
+ mkdirSync(tempDir, { recursive: true })
60
+ }
61
+ const localPath = join(tempDir, 'wx_key.dll')
62
+ if (existsSync(localPath)) return localPath
63
+ copyFileSync(originalPath, localPath)
64
+ return localPath
65
+ } catch (e) {
66
+ console.error('DLL 本地化失败:', e)
67
+ return originalPath
68
+ }
69
+ }
70
+
71
+ private ensureLoaded(): boolean {
72
+ if (this.initialized) return true
73
+
74
+ let dllPath = ''
75
+ try {
76
+ this.koffi = require('koffi')
77
+ dllPath = this.getDllPath()
78
+
79
+ if (!existsSync(dllPath)) {
80
+ console.error(`wx_key.dll 不存在: ${dllPath}`)
81
+ return false
82
+ }
83
+
84
+ if (this.isNetworkPath(dllPath)) {
85
+ dllPath = this.localizeNetworkDll(dllPath)
86
+ }
87
+
88
+ this.lib = this.koffi.load(dllPath)
89
+ this.initHook = this.lib.func('bool InitializeHook(uint32 targetPid)')
90
+ this.pollKeyData = this.lib.func('bool PollKeyData(_Out_ char *keyBuffer, int bufferSize)')
91
+ this.getStatusMessage = this.lib.func('bool GetStatusMessage(_Out_ char *msgBuffer, int bufferSize, _Out_ int *outLevel)')
92
+ this.cleanupHook = this.lib.func('bool CleanupHook()')
93
+ this.getLastErrorMsg = this.lib.func('const char* GetLastErrorMsg()')
94
+
95
+ this.initialized = true
96
+ return true
97
+ } catch (e) {
98
+ const errorMsg = e instanceof Error ? e.message : String(e)
99
+ console.error(`加载 wx_key.dll 失败: ${dllPath} - ${errorMsg}`)
100
+ return false
101
+ }
102
+ }
103
+
104
+ private ensureKernel32(): boolean {
105
+ if (this.kernel32) return true
106
+ try {
107
+ this.koffi = require('koffi')
108
+ this.kernel32 = this.koffi.load('kernel32.dll')
109
+ this.OpenProcess = this.kernel32.func('OpenProcess', 'void*', ['uint32', 'bool', 'uint32'])
110
+ this.CloseHandle = this.kernel32.func('CloseHandle', 'bool', ['void*'])
111
+ this.QueryFullProcessImageNameW = this.kernel32.func('QueryFullProcessImageNameW', 'bool', ['void*', 'uint32', this.koffi.out('uint16*'), this.koffi.out('uint32*')])
112
+ return true
113
+ } catch (e) {
114
+ console.error('初始化 kernel32 失败:', e)
115
+ return false
116
+ }
117
+ }
118
+
119
+ private decodeUtf8(buf: Buffer): string {
120
+ const nullIdx = buf.indexOf(0)
121
+ return buf.toString('utf8', 0, nullIdx > -1 ? nullIdx : undefined).trim()
122
+ }
123
+
124
+ private decodeCString(ptr: any): string {
125
+ try {
126
+ if (typeof ptr === 'string') return ptr
127
+ return this.koffi.decode(ptr, 'char', -1)
128
+ } catch {
129
+ return ''
130
+ }
131
+ }
132
+
133
+ private async findPidByImageName(imageName: string): Promise<number | null> {
134
+ try {
135
+ const { stdout } = await execFileAsync('tasklist', ['/FI', `IMAGENAME eq ${imageName}`, '/FO', 'CSV', '/NH'])
136
+ const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
137
+ for (const line of lines) {
138
+ if (line.startsWith('INFO:')) continue
139
+ const parts = line.split('","').map((p) => p.replace(/^"|"$/g, ''))
140
+ if (parts[0]?.toLowerCase() === imageName.toLowerCase()) {
141
+ const pid = Number(parts[1])
142
+ if (!Number.isNaN(pid)) return pid
143
+ }
144
+ }
145
+ return null
146
+ } catch {
147
+ return null
148
+ }
149
+ }
150
+
151
+ async findWeChatPid(): Promise<number | null> {
152
+ const names = ['Weixin.exe', 'WeChat.exe']
153
+ for (const name of names) {
154
+ const pid = await this.findPidByImageName(name)
155
+ if (pid) return pid
156
+ }
157
+ return null
158
+ }
159
+
160
+ private async getProcessExecutablePath(pid: number): Promise<string | null> {
161
+ if (!this.ensureKernel32()) return null
162
+ const hProcess = this.OpenProcess(0x1000, false, pid)
163
+ if (!hProcess) return null
164
+
165
+ try {
166
+ const sizeBuf = Buffer.alloc(4)
167
+ sizeBuf.writeUInt32LE(1024, 0)
168
+ const pathBuf = Buffer.alloc(1024 * 2)
169
+ const ret = this.QueryFullProcessImageNameW(hProcess, 0, pathBuf, sizeBuf)
170
+ if (ret) {
171
+ const len = sizeBuf.readUInt32LE(0)
172
+ return pathBuf.toString('ucs2', 0, len * 2)
173
+ }
174
+ return null
175
+ } catch {
176
+ return null
177
+ } finally {
178
+ this.CloseHandle(hProcess)
179
+ }
180
+ }
181
+
182
+ /**
183
+ * 等待微信进程出现,立即 hook 并提取密钥
184
+ * 用户应在调用此方法后启动微信
185
+ */
186
+ async waitForKey(
187
+ timeoutMs = 120_000,
188
+ onStatus?: (message: string) => void
189
+ ): Promise<DbKeyResult> {
190
+ if (process.platform !== 'win32') {
191
+ return { success: false, error: '自动提取 key 仅支持 Windows' }
192
+ }
193
+ if (!this.ensureLoaded()) {
194
+ return { success: false, error: 'wx_key.dll 未加载,请确保 resources/key/ 目录存在' }
195
+ }
196
+ if (!this.ensureKernel32()) {
197
+ return { success: false, error: 'Kernel32 初始化失败' }
198
+ }
199
+
200
+ const logs: string[] = []
201
+
202
+ // 等待微信进程出现
203
+ onStatus?.('等待微信进程出现...')
204
+ let pid: number | null = null
205
+ const waitStart = Date.now()
206
+ while (Date.now() - waitStart < timeoutMs) {
207
+ pid = await this.findWeChatPid()
208
+ if (pid) break
209
+ await new Promise(r => setTimeout(r, 500))
210
+ process.stdout.write('.')
211
+ }
212
+ console.log('')
213
+
214
+ if (!pid) {
215
+ return { success: false, error: '等待超时,未检测到微信进程' }
216
+ }
217
+
218
+ onStatus?.(`检测到微信进程 (PID: ${pid}),立即 hook...`)
219
+ return this.extractKeyFromPid(pid, logs, onStatus)
220
+ }
221
+
222
+ /**
223
+ * 自动从运行中的微信进程提取数据库解密 key
224
+ */
225
+ async autoGetDbKey(
226
+ timeoutMs = 60_000,
227
+ onStatus?: (message: string) => void
228
+ ): Promise<DbKeyResult> {
229
+ if (process.platform !== 'win32') {
230
+ return { success: false, error: '自动提取 key 仅支持 Windows' }
231
+ }
232
+ if (!this.ensureLoaded()) {
233
+ return { success: false, error: 'wx_key.dll 未加载,请确保 resources/key/ 目录存在' }
234
+ }
235
+ if (!this.ensureKernel32()) {
236
+ return { success: false, error: 'Kernel32 初始化失败' }
237
+ }
238
+
239
+ const logs: string[] = []
240
+
241
+ onStatus?.('正在查找微信进程...')
242
+ const pid = await this.findWeChatPid()
243
+ if (!pid) {
244
+ return { success: false, error: '未找到微信进程,请先启动微信并登录' }
245
+ }
246
+
247
+ onStatus?.(`检测到微信进程 (PID: ${pid}),正在提取 key...`)
248
+ return this.extractKeyFromPid(pid, logs, onStatus)
249
+ }
250
+
251
+ private async extractKeyFromPid(
252
+ pid: number,
253
+ logs: string[],
254
+ onStatus?: (message: string) => void,
255
+ timeoutMs = 90_000
256
+ ): Promise<DbKeyResult> {
257
+
258
+ const ok = this.initHook(pid)
259
+ if (!ok) {
260
+ const error = this.getLastErrorMsg ? this.decodeCString(this.getLastErrorMsg()) : ''
261
+ if (error) {
262
+ if (error.includes('0xC0000022') || error.includes('ACCESS_DENIED')) {
263
+ return { success: false, error: '权限不足,请以管理员身份运行 CLI' }
264
+ }
265
+ return { success: false, error }
266
+ }
267
+ return { success: false, error: '初始化 Hook 失败' }
268
+ }
269
+
270
+ const keyBuffer = Buffer.alloc(128)
271
+ const start = Date.now()
272
+ let pollCount = 0
273
+
274
+ try {
275
+ while (Date.now() - start < timeoutMs) {
276
+ pollCount++
277
+ const pollResult = this.pollKeyData(keyBuffer, keyBuffer.length)
278
+ if (pollResult) {
279
+ const key = this.decodeUtf8(keyBuffer)
280
+ onStatus?.(`[调试] pollKeyData=true, key长度=${key.length}, 内容=${key.slice(0, 20)}...`)
281
+ if (key.length === 64) {
282
+ onStatus?.('密钥获取成功!')
283
+ return { success: true, key, logs }
284
+ }
285
+ } else if (pollCount <= 5 || pollCount % 20 === 0) {
286
+ // 前5次和每20次输出一次调试信息
287
+ const hexSample = keyBuffer.slice(0, 16).toString('hex')
288
+ onStatus?.(`[调试] poll #${pollCount}: pollResult=${pollResult}, buffer=${hexSample}`)
289
+ }
290
+
291
+ // 读取状态消息
292
+ for (let i = 0; i < 5; i++) {
293
+ const statusBuffer = Buffer.alloc(256)
294
+ const levelOut = [0]
295
+ if (!this.getStatusMessage(statusBuffer, statusBuffer.length, levelOut)) break
296
+ const msg = this.decodeUtf8(statusBuffer)
297
+ if (msg) {
298
+ logs.push(msg)
299
+ onStatus?.(msg)
300
+ }
301
+ }
302
+
303
+ await new Promise(resolve => setTimeout(resolve, 120))
304
+ }
305
+ } finally {
306
+ try { this.cleanupHook() } catch { }
307
+ }
308
+
309
+ return { success: false, error: '获取密钥超时,请确保微信已登录', logs }
310
+ }
311
+
312
+ /**
313
+ * 检测当前运行的微信版本
314
+ */
315
+ async detectWeChatVersion(): Promise<'3.x' | '4.x' | null> {
316
+ const pid3x = await this.findPidByImageName('WeChat.exe')
317
+ if (pid3x) return '3.x'
318
+
319
+ const pid4x = await this.findPidByImageName('Weixin.exe')
320
+ if (pid4x) return '4.x'
321
+
322
+ return null
323
+ }
324
+
325
+ /**
326
+ * 提取 3.x 数据库密钥 (通过 Python/pywxdump 内存搜索)
327
+ */
328
+ async extract3xKey(
329
+ onStatus?: (message: string) => void
330
+ ): Promise<DbKeyResult & { wxid?: string; msgDir?: string; wxDir?: string }> {
331
+ if (process.platform !== 'win32') {
332
+ return { success: false, error: '3.x 密钥提取仅支持 Windows' }
333
+ }
334
+
335
+ onStatus?.('正在查找 3.x 微信进程...')
336
+
337
+ const pid = await this.findPidByImageName('WeChat.exe')
338
+ if (!pid) {
339
+ return { success: false, error: '未找到 WeChat 3.x (WeChat.exe) 进程,请先启动 3.x 微信并登录' }
340
+ }
341
+
342
+ onStatus?.(`找到 3.x 进程 (PID: ${pid}),正在通过内存搜索提取密钥...`)
343
+
344
+ try {
345
+ const scriptPath = join(process.cwd(), 'scripts', 'extract_3x_key.py')
346
+
347
+ if (!existsSync(scriptPath)) {
348
+ return { success: false, error: `Python 脚本不存在: ${scriptPath}` }
349
+ }
350
+
351
+ const { stdout } = await execFileAsync('python', [scriptPath], {
352
+ timeout: 120_000,
353
+ maxBuffer: 1024 * 1024
354
+ })
355
+
356
+ // 过滤掉可能的警告信息,只取最后一行 JSON
357
+ const lines = stdout.split('\n').filter((l: string) => l.trim().startsWith('{'))
358
+ if (lines.length === 0) {
359
+ return { success: false, error: `Python 脚本无有效输出: ${stdout.slice(0, 200)}` }
360
+ }
361
+
362
+ const result = JSON.parse(lines[lines.length - 1])
363
+
364
+ if (result.success) {
365
+ onStatus?.(`3.x 密钥提取成功! wxid=${result.wxid}`)
366
+ return {
367
+ success: true,
368
+ key: result.key,
369
+ wxid: result.wxid,
370
+ msgDir: result.msg_dir,
371
+ wxDir: result.wx_dir
372
+ }
373
+ } else {
374
+ return { success: false, error: result.error || '3.x 密钥提取失败' }
375
+ }
376
+ } catch (e: any) {
377
+ const msg = e?.message || String(e)
378
+ if (msg.includes('ETIMEDOUT') || msg.includes('killed')) {
379
+ return { success: false, error: '3.x 密钥提取超时' }
380
+ }
381
+ if (msg.includes('ENOENT') || msg.includes('python')) {
382
+ return { success: false, error: '未找到 Python,请确保 Python 已安装并在 PATH 中' }
383
+ }
384
+ return { success: false, error: `3.x 密钥提取失败: ${msg}` }
385
+ }
386
+ }
387
+
388
+ /**
389
+ * 自动检测并提取密钥 (支持 3.x 和 4.x)
390
+ */
391
+ async autoExtractKey(
392
+ onStatus?: (message: string) => void
393
+ ): Promise<DbKeyResult & { version?: string; wxid?: string; msgDir?: string; wxDir?: string }> {
394
+ const version = await this.detectWeChatVersion()
395
+
396
+ if (version === '3.x') {
397
+ onStatus?.('检测到 WeChat 3.x 版本,使用内存搜索方式提取密钥...')
398
+ return this.extract3xKey(onStatus)
399
+ } else if (version === '4.x') {
400
+ onStatus?.('检测到 WeChat 4.x 版本,使用 Hook 方式提取密钥...')
401
+ const result = await this.autoGetDbKey(60000, onStatus)
402
+ return { ...result, version: '4.x' }
403
+ } else {
404
+ return { success: false, error: '未检测到微信进程 (WeChat.exe 或 Weixin.exe)' }
405
+ }
406
+ }
407
+ }
408
+
409
+ export const keyService = new KeyService()
@@ -0,0 +1,224 @@
1
+ /**
2
+ * NT Core - 4.x NT database access layer
3
+ *
4
+ * NT databases (xwechat_files/* /db_storage/message/message_0.db) use
5
+ * SQLCipher 4 with plaintext_header_size=0. Decryption requires the
6
+ * sqlcipher3 Python package. This module wraps scripts/nt_decrypt.py.
7
+ */
8
+ import { execFile } from 'child_process'
9
+ import { promisify } from 'util'
10
+ import { existsSync } from 'fs'
11
+ import { join, dirname } from 'path'
12
+ import { fileURLToPath } from 'url'
13
+ import { configService } from '../services/configService.js'
14
+ import type { ChatSession, Message, Contact } from '../types.js'
15
+
16
+ const execFileAsync = promisify(execFile)
17
+ const __filename = fileURLToPath(import.meta.url)
18
+ const __dirname = dirname(__filename)
19
+
20
+ export interface NtResult {
21
+ success: boolean
22
+ error?: string
23
+ }
24
+
25
+ export interface NtSessionsResult extends NtResult {
26
+ sessions?: ChatSession[]
27
+ }
28
+
29
+ export interface NtMessagesResult extends NtResult {
30
+ messages?: Message[]
31
+ }
32
+
33
+ export interface NtContactsResult extends NtResult {
34
+ contacts?: Contact[]
35
+ }
36
+
37
+ export interface NtScanResult {
38
+ success: boolean
39
+ error?: string
40
+ keys?: Array<{ key: string; salt: string }>
41
+ databases?: Array<{ path: string; name: string; salt: string; size: number; wxid: string }>
42
+ matched?: Array<{ path: string; name: string; salt: string; size: number; wxid: string; key: string }>
43
+ }
44
+
45
+ export class NtCore {
46
+ private dbPath: string
47
+ private keyHex: string
48
+ private saltHex: string
49
+ /** contact.db 路径 (用于显示备注名/昵称) */
50
+ contactDbPath: string | null = null
51
+ /** contact.db 解密密钥 */
52
+ contactKey: string | null = null
53
+ /** contact.db 盐值 */
54
+ contactSalt: string | null = null
55
+
56
+ constructor(dbPath: string, keyHex: string, saltHex: string) {
57
+ this.dbPath = dbPath
58
+ this.keyHex = keyHex
59
+ this.saltHex = saltHex
60
+ }
61
+
62
+ /** 尝试自动发现并连接 contact.db */
63
+ autoDetectContactDb(): boolean {
64
+ // 从 message_0.db 路径推导 contact.db
65
+ // message_0.db: <xwechat>/<wxid>/db_storage/message/message_0.db
66
+ // contact.db: <xwechat>/<wxid>/db_storage/contact/contact.db
67
+ const msgDir = this.dbPath.replace(/\\/g, '/')
68
+ const parts = msgDir.split('/')
69
+ // 找到 db_storage 位置
70
+ const dbStorageIdx = parts.lastIndexOf('db_storage')
71
+ if (dbStorageIdx < 0) return false
72
+
73
+ const wxidDir = parts.slice(0, dbStorageIdx).join('/')
74
+ const contactDbPath = `${wxidDir}/db_storage/contact/contact.db`
75
+
76
+ if (existsSync(contactDbPath)) {
77
+ this.contactDbPath = contactDbPath
78
+ return true
79
+ }
80
+ return false
81
+ }
82
+
83
+ private get scriptPath(): string {
84
+ // __dirname = dist/src/core, so ../../.. = package root
85
+ const pkgRoot = join(__dirname, '..', '..', '..')
86
+ return join(pkgRoot, 'scripts', 'nt_decrypt.py')
87
+ }
88
+
89
+ private async callPython(args: string[]): Promise<any> {
90
+ try {
91
+ const { stdout } = await execFileAsync('python', [this.scriptPath, ...args], {
92
+ timeout: 120_000,
93
+ maxBuffer: 50 * 1024 * 1024,
94
+ env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
95
+ encoding: 'utf-8',
96
+ })
97
+
98
+ const lines = stdout.split('\n').filter((l: string) => l.trim())
99
+ for (let i = lines.length - 1; i >= 0; i--) {
100
+ const trimmed = lines[i].trim()
101
+ if (trimmed.startsWith('{')) {
102
+ return JSON.parse(trimmed)
103
+ }
104
+ }
105
+ return { error: 'No valid JSON output from Python script' }
106
+ } catch (e: any) {
107
+ const msg = e?.message || String(e)
108
+ if (msg.includes('ENOENT') || msg.includes('python')) {
109
+ return { error: 'Python not found. Install Python and sqlcipher3: pip install sqlcipher3' }
110
+ }
111
+ if (msg.includes('ETIMEDOUT') || msg.includes('killed')) {
112
+ return { error: 'NT database query timed out' }
113
+ }
114
+ return { error: `NT database query failed: ${msg}` }
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Scan memory for NT keys and match to databases.
120
+ * Requires WeChat to be running.
121
+ */
122
+ static async scan(): Promise<NtScanResult> {
123
+ try {
124
+ const pkgRoot = join(__dirname, '..', '..', '..')
125
+ const scriptPath = join(pkgRoot, 'scripts', 'nt_decrypt.py')
126
+ const { stdout } = await execFileAsync('python', [scriptPath, 'scan', '--json'], {
127
+ timeout: 300_000,
128
+ maxBuffer: 10 * 1024 * 1024,
129
+ env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
130
+ encoding: 'utf-8',
131
+ })
132
+
133
+ const lines = stdout.split('\n').filter((l: string) => l.trim())
134
+ for (let i = lines.length - 1; i >= 0; i--) {
135
+ const trimmed = lines[i].trim()
136
+ if (trimmed.startsWith('{')) {
137
+ const result = JSON.parse(trimmed)
138
+ if (result.error) {
139
+ return { success: false, error: result.error }
140
+ }
141
+ return {
142
+ success: true,
143
+ keys: result.keys || [],
144
+ databases: result.databases || [],
145
+ matched: result.matched || [],
146
+ }
147
+ }
148
+ }
149
+ return { success: false, error: 'Scan produced no valid output' }
150
+ } catch (e: any) {
151
+ const msg = e?.message || String(e)
152
+ if (msg.includes('ENOENT') || msg.includes('python')) {
153
+ return { success: false, error: 'Python not found. Install Python and add to PATH.' }
154
+ }
155
+ return { success: false, error: `NT scan failed: ${msg}` }
156
+ }
157
+ }
158
+
159
+ async getSessions(keyword?: string): Promise<NtSessionsResult> {
160
+ const args: string[] = [
161
+ 'sessions',
162
+ '--db', this.dbPath,
163
+ '--key', this.keyHex,
164
+ '--salt', this.saltHex,
165
+ ]
166
+ if (keyword) args.push('--keyword', keyword)
167
+ if (this.contactDbPath && this.contactKey && this.contactSalt) {
168
+ args.push('--contact-db', this.contactDbPath)
169
+ args.push('--contact-key', this.contactKey)
170
+ args.push('--contact-salt', this.contactSalt)
171
+ }
172
+ const result = await this.callPython(args)
173
+ if (result.error) {
174
+ return { success: false, error: result.error }
175
+ }
176
+ return { success: true, sessions: result.sessions || [] }
177
+ }
178
+
179
+ async getMessages(talker: string, limit = 100, offset = 0): Promise<NtMessagesResult> {
180
+ const args: string[] = [
181
+ 'messages',
182
+ '--db', this.dbPath,
183
+ '--key', this.keyHex,
184
+ '--salt', this.saltHex,
185
+ '--talker', talker,
186
+ '--limit', String(limit),
187
+ '--offset', String(offset),
188
+ ]
189
+ if (this.contactDbPath && this.contactKey && this.contactSalt) {
190
+ args.push('--contact-db', this.contactDbPath)
191
+ args.push('--contact-key', this.contactKey)
192
+ args.push('--contact-salt', this.contactSalt)
193
+ }
194
+ // Pass own wxid from config for self-message detection
195
+ const ownWxid = configService.get('wxid')
196
+ if (ownWxid) args.push('--own-wxid', ownWxid)
197
+ const result = await this.callPython(args)
198
+ if (result.error) {
199
+ return { success: false, error: result.error }
200
+ }
201
+ return { success: true, messages: result.messages || [] }
202
+ }
203
+
204
+ async getContacts(keyword?: string, limit = 200): Promise<NtContactsResult> {
205
+ const args: string[] = [
206
+ 'contacts',
207
+ '--db', this.dbPath,
208
+ '--key', this.keyHex,
209
+ '--salt', this.saltHex,
210
+ '--limit', String(limit),
211
+ ]
212
+ if (keyword) args.push('--keyword', keyword)
213
+ if (this.contactDbPath && this.contactKey && this.contactSalt) {
214
+ args.push('--contact-db', this.contactDbPath)
215
+ args.push('--contact-key', this.contactKey)
216
+ args.push('--contact-salt', this.contactSalt)
217
+ }
218
+ const result = await this.callPython(args)
219
+ if (result.error) {
220
+ return { success: false, error: result.error }
221
+ }
222
+ return { success: true, contacts: result.contacts || [] }
223
+ }
224
+ }