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.
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/bin/weflow-cli-electron.cjs +59 -0
- package/bin/weflow-cli.ts +2311 -0
- package/cli.cjs +3 -0
- package/dist/bin/weflow-cli.d.ts +3 -0
- package/dist/bin/weflow-cli.d.ts.map +1 -0
- package/dist/bin/weflow-cli.js +2270 -0
- package/dist/bin/weflow-cli.js.map +1 -0
- package/dist/mcp-server/index.d.ts +3 -0
- package/dist/mcp-server/index.d.ts.map +1 -0
- package/dist/mcp-server/index.js +690 -0
- package/dist/mcp-server/index.js.map +1 -0
- package/dist/src/core/dbPathService.d.ts +40 -0
- package/dist/src/core/dbPathService.d.ts.map +1 -0
- package/dist/src/core/dbPathService.js +324 -0
- package/dist/src/core/dbPathService.js.map +1 -0
- package/dist/src/core/keyService.d.ts +59 -0
- package/dist/src/core/keyService.d.ts.map +1 -0
- package/dist/src/core/keyService.js +368 -0
- package/dist/src/core/keyService.js.map +1 -0
- package/dist/src/core/ntCore.d.ts +62 -0
- package/dist/src/core/ntCore.d.ts.map +1 -0
- package/dist/src/core/ntCore.js +191 -0
- package/dist/src/core/ntCore.js.map +1 -0
- package/dist/src/core/sqlcipherCore.d.ts +119 -0
- package/dist/src/core/sqlcipherCore.d.ts.map +1 -0
- package/dist/src/core/sqlcipherCore.js +693 -0
- package/dist/src/core/sqlcipherCore.js.map +1 -0
- package/dist/src/core/wcdbCore.d.ts +738 -0
- package/dist/src/core/wcdbCore.d.ts.map +1 -0
- package/dist/src/core/wcdbCore.js +4545 -0
- package/dist/src/core/wcdbCore.js.map +1 -0
- package/dist/src/core/wechatClient.d.ts +23 -0
- package/dist/src/core/wechatClient.d.ts.map +1 -0
- package/dist/src/core/wechatClient.js +166 -0
- package/dist/src/core/wechatClient.js.map +1 -0
- package/dist/src/services/chatService.d.ts +25 -0
- package/dist/src/services/chatService.d.ts.map +1 -0
- package/dist/src/services/chatService.js +291 -0
- package/dist/src/services/chatService.js.map +1 -0
- package/dist/src/services/configService.d.ts +43 -0
- package/dist/src/services/configService.d.ts.map +1 -0
- package/dist/src/services/configService.js +156 -0
- package/dist/src/services/configService.js.map +1 -0
- package/dist/src/services/exportService.d.ts +30 -0
- package/dist/src/services/exportService.d.ts.map +1 -0
- package/dist/src/services/exportService.js +273 -0
- package/dist/src/services/exportService.js.map +1 -0
- package/dist/src/services/wechat-formatter.d.ts +69 -0
- package/dist/src/services/wechat-formatter.d.ts.map +1 -0
- package/dist/src/services/wechat-formatter.js +248 -0
- package/dist/src/services/wechat-formatter.js.map +1 -0
- package/dist/src/services/wechatMessageService.d.ts +28 -0
- package/dist/src/services/wechatMessageService.d.ts.map +1 -0
- package/dist/src/services/wechatMessageService.js +341 -0
- package/dist/src/services/wechatMessageService.js.map +1 -0
- package/dist/src/services/wereadService.d.ts +206 -0
- package/dist/src/services/wereadService.d.ts.map +1 -0
- package/dist/src/services/wereadService.js +145 -0
- package/dist/src/services/wereadService.js.map +1 -0
- package/dist/src/services/whitelistService.d.ts +20 -0
- package/dist/src/services/whitelistService.d.ts.map +1 -0
- package/dist/src/services/whitelistService.js +60 -0
- package/dist/src/services/whitelistService.js.map +1 -0
- package/dist/src/types.d.ts +140 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/errors.d.ts +25 -0
- package/dist/src/utils/errors.d.ts.map +1 -0
- package/dist/src/utils/errors.js +43 -0
- package/dist/src/utils/errors.js.map +1 -0
- package/dist/src/utils/pathUtils.d.ts +5 -0
- package/dist/src/utils/pathUtils.d.ts.map +1 -0
- package/dist/src/utils/pathUtils.js +16 -0
- package/dist/src/utils/pathUtils.js.map +1 -0
- package/dist/src/utils/pythonRunner.d.ts +27 -0
- package/dist/src/utils/pythonRunner.d.ts.map +1 -0
- package/dist/src/utils/pythonRunner.js +62 -0
- package/dist/src/utils/pythonRunner.js.map +1 -0
- package/dist/src/utils/talkerUtils.d.ts +14 -0
- package/dist/src/utils/talkerUtils.d.ts.map +1 -0
- package/dist/src/utils/talkerUtils.js +73 -0
- package/dist/src/utils/talkerUtils.js.map +1 -0
- package/mcp-server/index.ts +757 -0
- package/package.json +72 -0
- package/resources/key/win32/x64/wx_key.dll +0 -0
- package/resources/wcdb/win32/x64/SDL2.dll +0 -0
- package/resources/wcdb/win32/x64/WCDB.dll +0 -0
- package/resources/wcdb/win32/x64/msvcp140.dll +0 -0
- package/resources/wcdb/win32/x64/msvcp140_1.dll +0 -0
- package/resources/wcdb/win32/x64/vcruntime140.dll +0 -0
- package/resources/wcdb/win32/x64/vcruntime140_1.dll +0 -0
- package/resources/wcdb/win32/x64/wcdb_api.dll +0 -0
- package/scripts/_batch_link_sources.py +73 -0
- package/scripts/_utils.py +531 -0
- package/scripts/annual_report.py +796 -0
- package/scripts/auto_tag.py +128 -0
- package/scripts/biz_daily.py +896 -0
- package/scripts/chat_report.py +397 -0
- package/scripts/chat_stats.py +702 -0
- package/scripts/classify_daily.py +411 -0
- package/scripts/compile_wiki.py +258 -0
- package/scripts/create_reading_notes.py +370 -0
- package/scripts/enrich_backlinks.py +163 -0
- package/scripts/export_chat_html.py +766 -0
- package/scripts/extract_3x_key.py +86 -0
- package/scripts/extract_todos.py +404 -0
- package/scripts/fav_server.py +392 -0
- package/scripts/fix_topics.py +111 -0
- package/scripts/generate_ai_report.py +367 -0
- package/scripts/generate_html.py +1372 -0
- package/scripts/generate_review.py +170 -0
- package/scripts/mcp_bridge.py +398 -0
- package/scripts/nt_decrypt.py +562 -0
- package/scripts/pipeline.py +145 -0
- package/scripts/promote_all.py +310 -0
- package/scripts/promote_ideas.py +297 -0
- package/scripts/rag_chat.py +191 -0
- package/scripts/scan_decrypt_4x.py +329 -0
- package/scripts/semantic_search.py +431 -0
- package/scripts/sync_fav.py +146 -0
- package/scripts/sync_weread.py +192 -0
- package/scripts/vault_rag.py +139 -0
- package/scripts/vault_search.py +141 -0
- package/src/core/dbPathService.ts +348 -0
- package/src/core/keyService.ts +409 -0
- package/src/core/ntCore.ts +224 -0
- package/src/core/sqlcipherCore.ts +764 -0
- package/src/core/wcdbCore.ts +4568 -0
- package/src/core/wechatClient.ts +204 -0
- package/src/lz4.d.ts +10 -0
- package/src/services/chatService.ts +296 -0
- package/src/services/configService.ts +197 -0
- package/src/services/exportService.ts +305 -0
- package/src/services/wechat-formatter.ts +348 -0
- package/src/services/wechatMessageService.ts +383 -0
- package/src/services/wereadService.ts +308 -0
- package/src/services/whitelistService.ts +72 -0
- package/src/types.ts +141 -0
- package/src/utils/errors.ts +42 -0
- package/src/utils/pathUtils.ts +16 -0
- package/src/utils/pythonRunner.ts +81 -0
- 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
|
+
}
|