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,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeChat ilink API HTTP client.
|
|
3
|
+
*
|
|
4
|
+
* Communicates with WeChat's bridge service (default https://ilinkai.weixin.qq.com)
|
|
5
|
+
* for QR login, message send/receive, and CDN media upload/download.
|
|
6
|
+
*
|
|
7
|
+
* References:
|
|
8
|
+
* AstrBot - weixin_oc_client.py / weixin_oc_adapter.py
|
|
9
|
+
*/
|
|
10
|
+
import crypto from 'crypto'
|
|
11
|
+
import fs from 'fs/promises'
|
|
12
|
+
import type { WechatOCConfig } from '../types.js'
|
|
13
|
+
|
|
14
|
+
const DEFAULT_BASE_URL = 'https://ilinkai.weixin.qq.com'
|
|
15
|
+
const DEFAULT_CDN_BASE_URL = 'https://novac2c.cdn.weixin.qq.com/c2c'
|
|
16
|
+
const DEFAULT_TIMEOUT_MS = 15_000
|
|
17
|
+
|
|
18
|
+
export class WechatClient {
|
|
19
|
+
private baseUrl: string
|
|
20
|
+
private cdnBaseUrl: string
|
|
21
|
+
private apiTimeoutMs: number
|
|
22
|
+
token?: string
|
|
23
|
+
|
|
24
|
+
constructor(config: WechatOCConfig = {}) {
|
|
25
|
+
this.baseUrl = config.baseUrl || DEFAULT_BASE_URL
|
|
26
|
+
this.cdnBaseUrl = config.cdnBaseUrl || DEFAULT_CDN_BASE_URL
|
|
27
|
+
this.apiTimeoutMs = config.apiTimeoutMs || DEFAULT_TIMEOUT_MS
|
|
28
|
+
this.token = config.token
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ====== Core HTTP ======
|
|
32
|
+
|
|
33
|
+
async requestJson(
|
|
34
|
+
method: 'GET' | 'POST',
|
|
35
|
+
endpoint: string,
|
|
36
|
+
options: {
|
|
37
|
+
payload?: Record<string, any>
|
|
38
|
+
params?: Record<string, any>
|
|
39
|
+
tokenRequired?: boolean
|
|
40
|
+
timeoutMs?: number
|
|
41
|
+
extraHeaders?: Record<string, string>
|
|
42
|
+
} = {},
|
|
43
|
+
): Promise<Record<string, any>> {
|
|
44
|
+
const url = new URL(`${this.baseUrl}/${endpoint}`)
|
|
45
|
+
if (options.params) {
|
|
46
|
+
for (const [k, v] of Object.entries(options.params)) {
|
|
47
|
+
url.searchParams.set(k, String(v))
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const headers: Record<string, string> = {
|
|
52
|
+
'Content-Type': 'application/json',
|
|
53
|
+
AuthorizationType: 'ilink_bot_token',
|
|
54
|
+
'X-WECHAT-UIN': Buffer.from(String(crypto.randomInt(2 ** 32))).toString('base64'),
|
|
55
|
+
...options.extraHeaders,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (options.tokenRequired && this.token) {
|
|
59
|
+
headers['Authorization'] = `Bearer ${this.token}`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const controller = new AbortController()
|
|
63
|
+
const timeout = options.timeoutMs || this.apiTimeoutMs
|
|
64
|
+
const timer = setTimeout(() => controller.abort(), timeout)
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
let response: Response
|
|
68
|
+
if (method === 'GET') {
|
|
69
|
+
response = await fetch(url.toString(), { headers, signal: controller.signal })
|
|
70
|
+
} else {
|
|
71
|
+
response = await fetch(url.toString(), {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers,
|
|
74
|
+
body: JSON.stringify(options.payload || {}),
|
|
75
|
+
signal: controller.signal,
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (response.status >= 400) {
|
|
80
|
+
const text = await response.text().catch(() => '')
|
|
81
|
+
throw new Error(`HTTP ${response.status}: ${text.slice(0, 200)}`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const text = await response.text()
|
|
85
|
+
if (!text) return {}
|
|
86
|
+
return JSON.parse(text)
|
|
87
|
+
} catch (e: any) {
|
|
88
|
+
if (e.name === 'AbortError') {
|
|
89
|
+
throw new Error(`Request timeout: ${method} ${endpoint}`)
|
|
90
|
+
}
|
|
91
|
+
throw e
|
|
92
|
+
} finally {
|
|
93
|
+
clearTimeout(timer)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ====== CDN Media ======
|
|
98
|
+
|
|
99
|
+
async uploadToCdn(
|
|
100
|
+
uploadFullUrl: string,
|
|
101
|
+
uploadParam: string,
|
|
102
|
+
fileKey: string,
|
|
103
|
+
aesKeyHex: string,
|
|
104
|
+
fileBuffer: Buffer,
|
|
105
|
+
): Promise<string> {
|
|
106
|
+
const padded = WechatClient.pkcs7Pad(fileBuffer)
|
|
107
|
+
const keyBuffer = Buffer.from(aesKeyHex, 'hex')
|
|
108
|
+
const cipher = crypto.createCipheriv('aes-128-ecb', keyBuffer, null)
|
|
109
|
+
cipher.setAutoPadding(false)
|
|
110
|
+
const encrypted = Buffer.concat([cipher.update(padded), cipher.final()])
|
|
111
|
+
|
|
112
|
+
const url = `${uploadFullUrl}?${uploadParam}`
|
|
113
|
+
const controller = new AbortController()
|
|
114
|
+
const timer = setTimeout(() => controller.abort(), this.apiTimeoutMs)
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const response = await fetch(url, {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: { 'Content-Type': 'application/octet-stream' },
|
|
120
|
+
body: encrypted,
|
|
121
|
+
signal: controller.signal,
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
if (response.status !== 200) {
|
|
125
|
+
const text = await response.text().catch(() => '')
|
|
126
|
+
throw new Error(`CDN upload failed: HTTP ${response.status} ${text.slice(0, 200)}`)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const encryptedParam = response.headers.get('x-encrypted-param')
|
|
130
|
+
if (!encryptedParam) {
|
|
131
|
+
throw new Error('CDN upload response missing x-encrypted-param header')
|
|
132
|
+
}
|
|
133
|
+
return encryptedParam
|
|
134
|
+
} finally {
|
|
135
|
+
clearTimeout(timer)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async downloadMedia(
|
|
140
|
+
encryptedQueryParam: string,
|
|
141
|
+
aesKeyValue: string,
|
|
142
|
+
): Promise<Buffer> {
|
|
143
|
+
const url = `${this.cdnBaseUrl}/download?encrypted_query_param=${encodeURIComponent(encryptedQueryParam)}`
|
|
144
|
+
const keyBuffer = WechatClient.parseMediaAesKey(aesKeyValue)
|
|
145
|
+
|
|
146
|
+
const controller = new AbortController()
|
|
147
|
+
const timer = setTimeout(() => controller.abort(), this.apiTimeoutMs)
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const response = await fetch(url.toString(), { signal: controller.signal })
|
|
151
|
+
if (response.status !== 200) {
|
|
152
|
+
throw new Error(`CDN download failed: HTTP ${response.status}`)
|
|
153
|
+
}
|
|
154
|
+
const encrypted = Buffer.from(await response.arrayBuffer())
|
|
155
|
+
|
|
156
|
+
const decipher = crypto.createDecipheriv('aes-128-ecb', keyBuffer, null)
|
|
157
|
+
decipher.setAutoPadding(false)
|
|
158
|
+
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()])
|
|
159
|
+
return WechatClient.pkcs7Unpad(decrypted)
|
|
160
|
+
} finally {
|
|
161
|
+
clearTimeout(timer)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ====== Static Utils ======
|
|
166
|
+
|
|
167
|
+
static pkcs7Pad(data: Buffer, blockSize: number = 16): Buffer {
|
|
168
|
+
const padLen = blockSize - (data.length % blockSize)
|
|
169
|
+
const pad = Buffer.alloc(padLen, padLen)
|
|
170
|
+
return Buffer.concat([data, pad])
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
static pkcs7Unpad(data: Buffer, blockSize: number = 16): Buffer {
|
|
174
|
+
if (data.length === 0) return data
|
|
175
|
+
const padLen = data[data.length - 1]
|
|
176
|
+
if (padLen < 1 || padLen > blockSize) return data
|
|
177
|
+
return data.subarray(0, data.length - padLen)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
static parseMediaAesKey(aesKeyStr: string): Buffer {
|
|
181
|
+
// Try base64 first
|
|
182
|
+
try {
|
|
183
|
+
let b64 = aesKeyStr.replace(/-/g, '+').replace(/_/g, '/')
|
|
184
|
+
while (b64.length % 4 !== 0) b64 += '='
|
|
185
|
+
const buf = Buffer.from(b64, 'base64')
|
|
186
|
+
if (buf.length === 16) return buf
|
|
187
|
+
} catch {}
|
|
188
|
+
|
|
189
|
+
// Try hex (32 chars = 16 bytes)
|
|
190
|
+
if (aesKeyStr.length === 32 && /^[0-9a-fA-F]+$/.test(aesKeyStr)) {
|
|
191
|
+
return Buffer.from(aesKeyStr, 'hex')
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
throw new Error(`Cannot parse AES key: ${aesKeyStr.slice(0, 20)}...`)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
static aesPaddedSize(size: number): number {
|
|
198
|
+
return size + (16 - (size % 16)) % 16
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async close(): Promise<void> {
|
|
202
|
+
// no persistent connection to close with fetch API
|
|
203
|
+
}
|
|
204
|
+
}
|
package/src/lz4.d.ts
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { join, dirname } from 'path'
|
|
2
|
+
import { fileURLToPath } from 'url'
|
|
3
|
+
import { existsSync } from 'fs'
|
|
4
|
+
import { WcdbCore } from '../core/wcdbCore.js'
|
|
5
|
+
import { SqlcipherCore } from '../core/sqlcipherCore.js'
|
|
6
|
+
import { NtCore } from '../core/ntCore.js'
|
|
7
|
+
import { configService } from './configService.js'
|
|
8
|
+
import type { ChatSession, Message, Contact, DataVersion } from '../types.js'
|
|
9
|
+
|
|
10
|
+
// 创建单例
|
|
11
|
+
const wcdbCore = new WcdbCore()
|
|
12
|
+
const sqlcipherCore = new SqlcipherCore()
|
|
13
|
+
|
|
14
|
+
export class ChatService {
|
|
15
|
+
private connected = false
|
|
16
|
+
private activeVersion: DataVersion | null = null
|
|
17
|
+
/** 4.x 是否通过原始 SQLite 连接 (而非 WCDB API) */
|
|
18
|
+
private useRawSqlite4x = false
|
|
19
|
+
/** NT 格式数据库访问 (通过 Python sqlcipher3) */
|
|
20
|
+
private ntCore: NtCore | null = null
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 检测数据版本: 3.x 或 4.x
|
|
24
|
+
*/
|
|
25
|
+
private detectVersion(): DataVersion | null {
|
|
26
|
+
const version = configService.get('dataVersion')
|
|
27
|
+
if (version === '3.x' || version === '4.x') return version as DataVersion
|
|
28
|
+
|
|
29
|
+
// 自动检测: 检查已配置的路径
|
|
30
|
+
const dbPath4x = configService.get('dbPath')
|
|
31
|
+
const dbPath3x = configService.get('dbPath3x')
|
|
32
|
+
const key4x = configService.get('decryptKey')
|
|
33
|
+
const key3x = configService.get('decryptKey3x')
|
|
34
|
+
|
|
35
|
+
if (dbPath3x && key3x) return '3.x'
|
|
36
|
+
if (dbPath4x && key4x) return '4.x'
|
|
37
|
+
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async connect(): Promise<{ success: boolean; error?: string }> {
|
|
42
|
+
if (this.connected) return { success: true }
|
|
43
|
+
|
|
44
|
+
const version = this.detectVersion()
|
|
45
|
+
if (!version) {
|
|
46
|
+
return { success: false, error: '请先运行 weflow-cli init 完成配置' }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (version === '4.x') {
|
|
50
|
+
return this.connect4x()
|
|
51
|
+
} else {
|
|
52
|
+
return this.connect3x()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private async connect4x(): Promise<{ success: boolean; error?: string }> {
|
|
57
|
+
const dbPath = configService.get('dbPath')
|
|
58
|
+
const wxid = configService.get('wxid')
|
|
59
|
+
const decryptKey = configService.get('decryptKey')
|
|
60
|
+
|
|
61
|
+
if (!dbPath || !decryptKey) {
|
|
62
|
+
return { success: false, error: '4.x 配置不完整,请运行 weflow-cli init' }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 方案 1: 使用已解密的 MSG0_decrypted.db (纯 SQLite)
|
|
66
|
+
const rawMsg0Path = join(dbPath, wxid || '', 'Msg', 'Multi', 'MSG0_decrypted.db')
|
|
67
|
+
if (existsSync(rawMsg0Path)) {
|
|
68
|
+
const result = await sqlcipherCore.openRaw(rawMsg0Path, wxid || '')
|
|
69
|
+
if (result.success) {
|
|
70
|
+
this.connected = true
|
|
71
|
+
this.activeVersion = '4.x'
|
|
72
|
+
this.useRawSqlite4x = true
|
|
73
|
+
return { success: true }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 方案 2: 尝试直接解密 MSG0.db (需要正确的密钥和盐值)
|
|
78
|
+
const msg0Path = join(dbPath, wxid || '', 'Msg', 'Multi', 'MSG0.db')
|
|
79
|
+
if (existsSync(msg0Path) && decryptKey) {
|
|
80
|
+
const result = await sqlcipherCore.open4x(msg0Path, decryptKey, wxid || '')
|
|
81
|
+
if (result.success) {
|
|
82
|
+
this.connected = true
|
|
83
|
+
this.activeVersion = '4.x'
|
|
84
|
+
this.useRawSqlite4x = true
|
|
85
|
+
return { success: true }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 方案 3: 尝试 NT 格式数据库 (xwechat_files/*/db_storage/message/message_0.db)
|
|
90
|
+
const ntDbPath = configService.get('ntDbPath')
|
|
91
|
+
const ntKey = configService.get('ntKey')
|
|
92
|
+
const ntSalt = configService.get('ntSalt')
|
|
93
|
+
if (ntDbPath && ntKey && ntSalt && existsSync(ntDbPath)) {
|
|
94
|
+
this.ntCore = new NtCore(ntDbPath, ntKey, ntSalt)
|
|
95
|
+
// 自动发现 contact.db 路径
|
|
96
|
+
if (this.ntCore.autoDetectContactDb()) {
|
|
97
|
+
const allConfig = configService.getAll()
|
|
98
|
+
if (allConfig.contactKey && allConfig.contactSalt) {
|
|
99
|
+
this.ntCore.contactKey = allConfig.contactKey
|
|
100
|
+
this.ntCore.contactSalt = allConfig.contactSalt
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// 快速验证: 尝试获取会话列表
|
|
104
|
+
const testResult = await this.ntCore.getSessions()
|
|
105
|
+
if (testResult.success) {
|
|
106
|
+
this.connected = true
|
|
107
|
+
this.activeVersion = '4.x'
|
|
108
|
+
return { success: true }
|
|
109
|
+
}
|
|
110
|
+
this.ntCore = null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 方案 4: 尝试 WCDB API 连接 (需要 db_storage/session.db 结构,仅 3.x 兼容)
|
|
114
|
+
// Use package root instead of cwd for global install support
|
|
115
|
+
const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..')
|
|
116
|
+
const resourcesPath = join(pkgRoot, 'resources')
|
|
117
|
+
wcdbCore.setPaths(resourcesPath, '')
|
|
118
|
+
|
|
119
|
+
const ok = await wcdbCore.open(dbPath, decryptKey, wxid || '')
|
|
120
|
+
if (ok) {
|
|
121
|
+
this.connected = true
|
|
122
|
+
this.activeVersion = '4.x'
|
|
123
|
+
this.useRawSqlite4x = false
|
|
124
|
+
return { success: true }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 所有方案失败,给出明确的指导
|
|
128
|
+
const msgDir = join(dbPath, wxid || '', 'Msg', 'Multi')
|
|
129
|
+
return {
|
|
130
|
+
success: false,
|
|
131
|
+
error: [
|
|
132
|
+
'4.x 数据库连接失败,请尝试以下方案之一:',
|
|
133
|
+
'',
|
|
134
|
+
'方案 A - 使用解密工具 (推荐):',
|
|
135
|
+
` python scripts/scan_decrypt_4x.py --db "${msg0Path}" --out "${rawMsg0Path}"`,
|
|
136
|
+
'',
|
|
137
|
+
'方案 B - NT 格式数据库 (微信 4.x 新版):',
|
|
138
|
+
' 1. 确保微信正在运行',
|
|
139
|
+
' 2. 运行: weflow-cli init (将自动扫描 NT 数据库)',
|
|
140
|
+
'',
|
|
141
|
+
'方案 C - 重新提取密钥 (需要重启微信):',
|
|
142
|
+
' 1. 关闭微信 4.x (Weixin.exe)',
|
|
143
|
+
' 2. 运行: weflow-cli dbkey --timeout 120000',
|
|
144
|
+
' 3. 启动微信并登录,密钥将在登录时自动捕获',
|
|
145
|
+
' 4. 保存密钥: weflow-cli config set decryptKey <64位密钥>',
|
|
146
|
+
'',
|
|
147
|
+
'方案 D - 手动指定已解密数据库路径:',
|
|
148
|
+
` 将已解密的数据库放到: ${rawMsg0Path}`,
|
|
149
|
+
].join('\n'),
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private async connect3x(): Promise<{ success: boolean; error?: string }> {
|
|
154
|
+
const dbPath = configService.get('dbPath3x')
|
|
155
|
+
const wxid = configService.get('wxid')
|
|
156
|
+
const decryptKey = configService.get('decryptKey3x')
|
|
157
|
+
|
|
158
|
+
if (!dbPath || !decryptKey) {
|
|
159
|
+
return { success: false, error: '3.x 配置不完整,请运行 weflow-cli init' }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const ok = await sqlcipherCore.open(dbPath, decryptKey, wxid || '')
|
|
163
|
+
if (ok.success) {
|
|
164
|
+
this.connected = true
|
|
165
|
+
this.activeVersion = '3.x'
|
|
166
|
+
return { success: true }
|
|
167
|
+
}
|
|
168
|
+
return { success: false, error: ok.error || '3.x 数据库连接失败' }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async listSessions(keyword?: string, limit = 50): Promise<ChatSession[]> {
|
|
172
|
+
const conn = await this.connect()
|
|
173
|
+
if (!conn.success) return []
|
|
174
|
+
|
|
175
|
+
let sessions: ChatSession[] = []
|
|
176
|
+
|
|
177
|
+
if (this.ntCore) {
|
|
178
|
+
const result = await this.ntCore.getSessions(keyword)
|
|
179
|
+
if (!result.success || !result.sessions) return []
|
|
180
|
+
sessions = result.sessions
|
|
181
|
+
} else if (this.activeVersion === '4.x') {
|
|
182
|
+
if (this.useRawSqlite4x) {
|
|
183
|
+
const result = await sqlcipherCore.getSessions()
|
|
184
|
+
if (!result.success || !result.sessions) return []
|
|
185
|
+
sessions = result.sessions
|
|
186
|
+
} else {
|
|
187
|
+
const result = await wcdbCore.getSessions()
|
|
188
|
+
if (!result.success || !result.sessions) return []
|
|
189
|
+
sessions = result.sessions
|
|
190
|
+
}
|
|
191
|
+
} else if (this.activeVersion === '3.x') {
|
|
192
|
+
const result = await sqlcipherCore.getSessions()
|
|
193
|
+
if (!result.success || !result.sessions) return []
|
|
194
|
+
sessions = result.sessions
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (keyword) {
|
|
198
|
+
const kw = keyword.toLowerCase()
|
|
199
|
+
sessions = sessions.filter(s =>
|
|
200
|
+
(s.username || '').toLowerCase().includes(kw) ||
|
|
201
|
+
(s.displayName || '').toLowerCase().includes(kw) ||
|
|
202
|
+
(s.summary || '').toLowerCase().includes(kw)
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return sessions.slice(0, limit)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async getMessages(
|
|
210
|
+
talker: string,
|
|
211
|
+
limit = 0,
|
|
212
|
+
offset = 0
|
|
213
|
+
): Promise<Message[]> {
|
|
214
|
+
const conn = await this.connect()
|
|
215
|
+
if (!conn.success) return []
|
|
216
|
+
|
|
217
|
+
if (this.ntCore) {
|
|
218
|
+
const result = await this.ntCore.getMessages(talker, limit, offset)
|
|
219
|
+
if (!result.success || !result.messages) return []
|
|
220
|
+
return result.messages
|
|
221
|
+
} else if (this.activeVersion === '4.x') {
|
|
222
|
+
if (this.useRawSqlite4x) {
|
|
223
|
+
const result = await sqlcipherCore.getMessages(talker, limit, offset)
|
|
224
|
+
if (!result.success || !result.messages) return []
|
|
225
|
+
return result.messages
|
|
226
|
+
}
|
|
227
|
+
const result = await wcdbCore.getMessages(talker, limit, offset)
|
|
228
|
+
if (!result.success || !result.messages) return []
|
|
229
|
+
return result.messages
|
|
230
|
+
} else if (this.activeVersion === '3.x') {
|
|
231
|
+
const result = await sqlcipherCore.getMessages(talker, limit, offset)
|
|
232
|
+
if (!result.success || !result.messages) return []
|
|
233
|
+
return result.messages
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return []
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async listContacts(keyword?: string, limit = 200): Promise<Contact[]> {
|
|
240
|
+
const conn = await this.connect()
|
|
241
|
+
if (!conn.success) return []
|
|
242
|
+
|
|
243
|
+
let contacts: Contact[] = []
|
|
244
|
+
|
|
245
|
+
if (this.ntCore) {
|
|
246
|
+
const result = await this.ntCore.getContacts(keyword, limit)
|
|
247
|
+
if (!result.success || !result.contacts) return []
|
|
248
|
+
contacts = result.contacts
|
|
249
|
+
} else if (this.activeVersion === '4.x') {
|
|
250
|
+
if (this.useRawSqlite4x) {
|
|
251
|
+
const result = await sqlcipherCore.getContacts()
|
|
252
|
+
if (!result.success || !result.contacts) return []
|
|
253
|
+
contacts = result.contacts
|
|
254
|
+
} else {
|
|
255
|
+
const result = await wcdbCore.getContactsCompact()
|
|
256
|
+
if (!result.success || !result.contacts) return []
|
|
257
|
+
contacts = result.contacts
|
|
258
|
+
}
|
|
259
|
+
} else if (this.activeVersion === '3.x') {
|
|
260
|
+
const result = await sqlcipherCore.getContacts()
|
|
261
|
+
if (!result.success || !result.contacts) return []
|
|
262
|
+
contacts = result.contacts
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (keyword) {
|
|
266
|
+
const kw = keyword.toLowerCase()
|
|
267
|
+
contacts = contacts.filter(c =>
|
|
268
|
+
(c.username || '').toLowerCase().includes(kw) ||
|
|
269
|
+
(c.displayName || '').toLowerCase().includes(kw) ||
|
|
270
|
+
(c.remark || '').toLowerCase().includes(kw) ||
|
|
271
|
+
(c.nickname || '').toLowerCase().includes(kw)
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return contacts.slice(0, limit)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
disconnect(): void {
|
|
279
|
+
if (this.ntCore) {
|
|
280
|
+
this.ntCore = null
|
|
281
|
+
}
|
|
282
|
+
if (this.activeVersion === '4.x') {
|
|
283
|
+
if (this.useRawSqlite4x) {
|
|
284
|
+
try { sqlcipherCore.close() } catch { }
|
|
285
|
+
}
|
|
286
|
+
try { wcdbCore.close() } catch { }
|
|
287
|
+
} else if (this.activeVersion === '3.x') {
|
|
288
|
+
try { sqlcipherCore.close() } catch { }
|
|
289
|
+
}
|
|
290
|
+
this.connected = false
|
|
291
|
+
this.activeVersion = null
|
|
292
|
+
this.useRawSqlite4x = false
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export const chatService = new ChatService()
|