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,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,10 @@
1
+ declare module 'lz4' {
2
+ export function decodeBlock(
3
+ input: Buffer,
4
+ output?: Buffer,
5
+ sIdx?: number,
6
+ eIdx?: number
7
+ ): number
8
+
9
+ export function decodeBound(size: number): number
10
+ }
@@ -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()