28 |
29 |
30 | {/* 状态图标 */}
31 | {isConnecting ? (
32 |
33 | ) : error ? (
34 |
35 | ) : (
36 |
37 | )}
38 |
39 | {/* 状态信息 */}
40 |
41 | {isConnecting && progress ? (
42 |
43 |
44 |
45 | {progress.message}
46 |
47 |
48 | ({progress.current}/{progress.total})
49 |
50 |
51 |
52 | {/* 进度条 */}
53 |
54 |
0 ? (progress.current / progress.total) * 100 : 0}%`
58 | }}
59 | />
60 |
61 |
62 | ) : isConnecting ? (
63 |
64 | 正在自动连接数据库...
65 |
66 | ) : error ? (
67 |
68 |
69 | 自动连接失败
70 |
71 |
72 | {error}
73 |
74 |
75 | ) : null}
76 |
77 |
78 |
79 | {/* 操作按钮 */}
80 |
81 | {error && onRetry && (
82 |
89 | )}
90 |
91 | {(error || !isConnecting) && onDismiss && (
92 |
98 | )}
99 |
100 |
101 |
102 | );
103 | }
--------------------------------------------------------------------------------
/packages/wechat-db-manager/THREE-COLUMN-CONTACTS.md:
--------------------------------------------------------------------------------
1 | # 三列布局联系人页面实施报告
2 |
3 | ## 🎯 功能概述
4 |
5 | 成功实现了用户要求的三列布局联系人页面,提供高效的微信数据浏览体验:
6 |
7 | ```
8 | ┌──────────────┬───────────────────────┬──────────────┐
9 | │ 联系人列表 │ 聊天记录 │ 联系人属性 │
10 | │ │ │ │
11 | │ 🔍 搜索框 │ 💬 实时聊天显示 │ 📋 基本信息 │
12 | │ 📱 张三 │ [张三] 你好 │ 显示名: 张三 │
13 | │ 💼 李四 │ [我] 在吗? │ 微信号: xxx │
14 | │ 👥 群聊1 │ [张三] 在的 │ 数据库: msg_1 │
15 | │ │ [我] 明天见面吧 │ 表名: Chat_ab │
16 | │ │ │ 消息数: 156条 │
17 | └──────────────┴───────────────────────┴──────────────┘
18 | ```
19 |
20 | ## 🛠️ 技术实现
21 |
22 | ### 1. 核心组件
23 |
24 | **文件:** `src/pages/ContactsThreeColumnPage.tsx`
25 |
26 | **架构设计:**
27 | - **第一列 (25%)**: 联系人列表 + 搜索功能
28 | - **第二列 (50%)**: 聊天记录显示
29 | - **第三列 (25%)**: 联系人详细属性
30 |
31 | **状态管理:**
32 | ```typescript
33 | interface LoadingState {
34 | contacts: boolean;
35 | messages: boolean;
36 | }
37 |
38 | interface ContactStats {
39 | databaseSource: string;
40 | chatTables: Array<{tableName: string; databaseFilename: string}>;
41 | messageCount: number;
42 | lastActiveTime?: string;
43 | }
44 | ```
45 |
46 | ### 2. 导航集成
47 |
48 | **文件:** `src/components/Navigation.tsx`
49 |
50 | - 添加新导航选项:"联系人详情"
51 | - 使用 `UserCheck` 图标区分传统联系人页面
52 | - 响应式设计适配6个导航选项
53 |
54 | **路由处理:** `src/App.tsx`
55 | ```typescript
56 | case 'contacts-pro':
57 | return
;
58 | ```
59 |
60 | ### 3. 数据流架构
61 |
62 | #### 联系人加载流程
63 | 1. **自动检测**: 扫描联系人数据库 (`*contact*`, `*wccontact*`)
64 | 2. **批量加载**: 遍历所有联系人数据库
65 | 3. **去重排序**: 按显示名中文排序
66 | 4. **实时搜索**: 支持多字段模糊搜索
67 |
68 | #### 聊天记录加载流程
69 | 1. **表映射服务**: 利用修复后的表映射系统
70 | 2. **MD5定位**: 精确定位联系人对应的聊天表
71 | 3. **优化加载**: 使用 `loadMessagesOptimized` 方法
72 | 4. **实时显示**: 气泡式聊天界面
73 |
74 | #### 属性面板内容
75 | - **基本信息**: 显示名、昵称、备注、微信号
76 | - **数据统计**: 消息数量、最后活跃时间
77 | - **技术信息**: 数据库来源、表名映射、联系人ID
78 | - **调试工具**: 一键调试映射关系
79 |
80 | ## 🎨 用户体验设计
81 |
82 | ### 1. 视觉设计
83 | - **清晰分列**: 明确的边界线和背景色区分
84 | - **选中状态**: 蓝色高亮显示当前选中联系人
85 | - **加载状态**: 旋转指示器和进度反馈
86 | - **空状态**: 友好的提示信息和操作指导
87 |
88 | ### 2. 交互设计
89 | - **即点即显**: 点击联系人立即加载聊天记录
90 | - **搜索筛选**: 实时搜索联系人姓名、昵称、备注
91 | - **消息气泡**: 区分发送方和接收方的气泡样式
92 | - **时间格式**: 智能时间显示(今天/本周/具体日期)
93 |
94 | ### 3. 状态管理
95 | - **联系人同步**: 数据库变化时自动重新加载
96 | - **选中保持**: 智能处理选中状态的重置
97 | - **错误恢复**: 完善的错误处理和重试机制
98 |
99 | ## 🚀 性能优化
100 |
101 | ### 1. 数据加载优化
102 | - **表映射加速**: 利用全局表映射服务,避免重复扫描
103 | - **精确定位**: MD5映射直接定位聊天表,无需遍历
104 | - **批量去重**: 高效的联系人去重算法
105 |
106 | ### 2. 渲染优化
107 | - **条件渲染**: 按需渲染聊天记录和属性面板
108 | - **时间缓存**: 优化时间格式化计算
109 | - **状态分离**: 独立的加载状态管理
110 |
111 | ### 3. 内存管理
112 | - **状态重置**: 切换联系人时清理前一个联系人的数据
113 | - **懒加载**: 仅在选中时加载聊天记录
114 | - **垃圾回收**: 及时清理不需要的状态
115 |
116 | ## 📱 响应式适配
117 |
118 | ### 1. 导航栏适配
119 | - **图标缩放**: 小屏幕使用较小图标
120 | - **间距调整**: 动态调整按钮间距
121 | - **文字截断**: 防止导航文字溢出
122 |
123 | ### 2. 布局适配
124 | - **三列保持**: 在合理屏幕尺寸下保持三列布局
125 | - **最小宽度**: 设置列的最小宽度防止挤压
126 | - **滚动处理**: 各列独立滚动,防止内容丢失
127 |
128 | ## 🔧 技术特性
129 |
130 | ### 1. 集成表映射服务
131 | - 使用修复后的 `TableMappingService`
132 | - 支持 MD5 哈希表名映射
133 | - 提供详细的映射调试信息
134 |
135 | ### 2. 完善错误处理
136 | ```typescript
137 | // 空数据库状态
138 | if (contactDbs.length === 0) {
139 | return
;
140 | }
141 |
142 | // 加载错误恢复
143 | catch (error) {
144 | console.error('❌ 加载联系人失败:', error);
145 | setContacts([]);
146 | }
147 | ```
148 |
149 | ### 3. 调试工具集成
150 | - **控制台日志**: 详细的加载过程日志
151 | - **一键调试**: 快速调试联系人映射关系
152 | - **状态透明**: 显示技术细节方便问题排查
153 |
154 | ## 📊 功能对比
155 |
156 | | 功能特性 | 传统联系人页面 | 三列布局页面 |
157 | |---------|--------------|------------|
158 | | 联系人浏览 | ✅ 卡片式 | ✅ 列表式 |
159 | | 聊天记录 | ⚠️ 弹窗模式 | ✅ 即时显示 |
160 | | 技术信息 | ❌ 无 | ✅ 详细属性 |
161 | | 搜索功能 | ✅ 基础搜索 | ✅ 多字段搜索 |
162 | | 数据库信息 | ❌ 隐藏 | ✅ 透明显示 |
163 | | 调试工具 | ❌ 无 | ✅ 集成调试 |
164 | | 效率 | ⚠️ 多步操作 | ✅ 一屏完成 |
165 |
166 | ## 🎉 用户价值
167 |
168 | ### 1. 效率提升
169 | - **快速浏览**: 一屏内完成联系人选择和聊天查看
170 | - **无需弹窗**: 避免频繁的模态框开关操作
171 | - **即时反馈**: 点击即显示,无等待时间
172 |
173 | ### 2. 信息透明
174 | - **数据来源**: 清楚显示数据来自哪个数据库文件
175 | - **表结构**: 展示底层表名和映射关系
176 | - **统计信息**: 直观的消息数量和活跃度
177 |
178 | ### 3. 调试友好
179 | - **技术细节**: 显示联系人ID、原始ID等技术信息
180 | - **映射诊断**: 一键检查联系人到聊天表的映射
181 | - **错误定位**: 详细的错误日志和状态提示
182 |
183 | ## 🔮 未来扩展
184 |
185 | ### 短期优化
186 | 1. **虚拟滚动**: 支持大量联系人的性能优化
187 | 2. **消息分页**: 分页加载历史聊天记录
188 | 3. **快捷键**: 添加键盘导航支持
189 |
190 | ### 中期功能
191 | 1. **消息搜索**: 在聊天记录中搜索特定内容
192 | 2. **导出功能**: 导出选中联系人的聊天记录
193 | 3. **统计图表**: 联系人活跃度分析图表
194 |
195 | ### 长期规划
196 | 1. **多选操作**: 批量处理多个联系人
197 | 2. **自定义列宽**: 用户可调整三列的宽度比例
198 | 3. **布局记忆**: 保存用户的布局偏好设置
199 |
200 | ## ✅ 验收标准
201 |
202 | 用户要求的功能已全部实现:
203 |
204 | - ✅ **第一列联系人**: 完整的联系人列表 + 搜索功能
205 | - ✅ **第二列聊天记录**: 选中联系人后即时显示聊天内容
206 | - ✅ **第三列属性信息**: 基本属性 + 数据库名 + 表名 + 技术信息
207 |
208 | 用户现在可以通过底部导航的"联系人详情"选项体验全新的三列布局界面!
--------------------------------------------------------------------------------
/packages/wechat-db-manager/src/App.tsx:
--------------------------------------------------------------------------------
1 | import {useEffect, useState} from 'react';
2 | import {Navigation, NavigationTab} from './components/Navigation';
3 | import {SettingsPage} from './pages/SettingsPage';
4 | import {OverviewPage} from './pages/OverviewPage';
5 | import {DatabasePage} from './pages/DatabasePage';
6 | import {ChatPage} from './pages/ChatPageOptimized';
7 | import {ContactsThreeColumnPage} from './pages/ContactsThreeColumnPage';
8 | import {AutoConnectIndicator} from './components/AutoConnectIndicator';
9 | import {useAtom} from 'jotai';
10 | import {initializePersistedStateAtom} from './store/atoms';
11 | import {useAutoConnect} from './hooks/useAutoConnect';
12 | import {useTableMapping} from './hooks/useTableMapping';
13 | import './App.css';
14 |
15 | function App() {
16 | const [activeTab, setActiveTab] = useState
('settings');
17 | const [, initializeState] = useAtom(initializePersistedStateAtom);
18 | const autoConnect = useAutoConnect();
19 | const tableMapping = useTableMapping();
20 |
21 | // 初始化持久化状态
22 | useEffect(() => {
23 | initializeState();
24 | }, [initializeState]);
25 |
26 | const renderActiveTab = () => {
27 | switch (activeTab) {
28 | case 'chat':
29 | return ;
30 | case 'contacts-pro':
31 | return ;
32 | case 'database':
33 | return ;
34 | case 'overview':
35 | return ;
36 | case 'settings':
37 | return ;
38 | default:
39 | return (
40 |
41 |
页面加载中...
42 |
43 | );
44 | }
45 | };
46 |
47 | return (
48 |
49 | {/* Auto Connect Indicator - 顶部状态条 */}
50 | {(autoConnect.isAutoConnecting || autoConnect.autoConnectError) && (
51 |
60 | )}
61 |
62 | {/* Table Mapping Status - 表映射状态 */}
63 | {tableMapping.isInitializing && (
64 |
65 |
66 |
67 | 正在初始化表映射服务...
68 |
69 |
70 | )}
71 |
72 | {tableMapping.isInitialized && tableMapping.stats && (
73 |
74 |
75 |
76 |
77 | 表映射就绪 - {tableMapping.stats.totalTables} 个表,{tableMapping.stats.chatTables} 个聊天表
78 |
79 |
80 | {tableMapping.stats.databaseCount} 个数据库
81 |
82 |
83 |
84 | )}
85 |
86 | {!tableMapping.isInitializing && !tableMapping.isInitialized && (
87 |
88 |
89 |
90 | 等待数据库加载...请先在设置页面加载keys文件
91 |
92 |
93 | )}
94 |
95 | {/* Main Content Area - 确保可以收缩并包含滚动 */}
96 |
97 | {renderActiveTab()}
98 |
99 |
100 | {/* Bottom Navigation - 固定在底部,不允许收缩 */}
101 |
102 |
106 |
107 |
108 | );
109 | }
110 |
111 | export default App;
--------------------------------------------------------------------------------
/packages/wechat-db-manager/src/components/WelcomeGuide.tsx:
--------------------------------------------------------------------------------
1 | import {useState} from 'react';
2 | import {useAtom} from 'jotai';
3 | import {databasesAtom, keysFilePathAtom} from '../store/atoms';
4 | import {Database, Download, FileText, Table, X} from 'lucide-react';
5 |
6 | export function WelcomeGuide() {
7 | const [keysPath] = useAtom(keysFilePathAtom);
8 | const [databases] = useAtom(databasesAtom);
9 | const [isVisible, setIsVisible] = useState(true);
10 |
11 | // 如果已经有文件和数据库,就隐藏引导
12 | if (keysPath && databases.length > 0) {
13 | return null;
14 | }
15 |
16 | if (!isVisible) {
17 | return null;
18 | }
19 |
20 | return (
21 |
22 |
23 |
24 |
25 |
Welcome to WeChat DB Manager
26 |
27 |
33 |
34 |
35 |
36 |
37 | This tool helps you manage and browse WeChat SQLCipher databases. Here's how to get started:
38 |
39 |
40 |
41 |
42 |
44 | 1
45 |
46 |
47 |
Select your keys file
48 |
49 | Use the "Browse Files" button to select your .keys file,
51 | or click "Enter Path" to type the full path manually.
52 |
53 |
54 |
55 |
56 |
57 |
59 | 2
60 |
61 |
62 |
Browse databases
63 |
64 | Once loaded, select a database from the list to view its tables and information.
65 |
66 |
67 |
68 |
69 |
70 |
72 | 3
73 |
74 |
75 |
Explore data
76 |
77 | Click on tables to view their contents, execute custom queries, and export data as CSV.
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | Keys File
87 |
88 |
89 |
90 | Databases
91 |
92 |
96 |
97 |
98 | Export
99 |
100 |
101 |
102 |
103 | );
104 | }
--------------------------------------------------------------------------------
/docs/config-sqlcipher.bak.md:
--------------------------------------------------------------------------------
1 | # python driver for cracking/hacking wechat on macOS
2 |
3 | - version: 0.0.2
4 | - date: 2022/07/04
5 | - author: markshawn
6 |
7 | ## environment preparation
8 |
9 | ### init sqlcipher
10 |
11 | 1. check where is your `libcrypto.a`
12 |
13 | ```shell
14 | find /usr/local/Cellar -name libcrypto.a
15 | ```
16 |
17 | 2. use the libcrypto.a with openssl version >= 3
18 |
19 | ```shell
20 | LIBCRYPTO={YOUR-libcrypto.a}
21 | ```
22 |
23 | 3. install
24 |
25 | ```shell
26 |
27 | git clone https://github.com/sqlcipher/sqlcipher
28 | cd sqlcipher
29 |
30 | ./configure --enable-tempstore=yes CFLAGS="-DSQLITE_HAS_CODEC" \
31 | LDFLAGS=$LIBCRYPTO --with-crypto-lib=none
32 |
33 | make && make install
34 |
35 | cd ..
36 | ```
37 |
38 | ### init pysqlcipher
39 |
40 | ```shell
41 |
42 | git clone https://github.com/rigglemania/pysqlcipher3
43 | cd pysqlcipher3
44 |
45 | mkdir amalgamation && cp ../sqlcipher/sqlite3.[hc] amalgamation/
46 | mkdir src/python3/sqlcipher && cp amalgamation/sqlite3.h src/python3/sqlcipher/
47 |
48 | python setup.py build_amalgamation
49 | python setup.py install
50 |
51 | cd ..
52 | ```
53 |
54 | ### disable SIP, otherwise the dtrace can't be used
55 |
56 | ```shell
57 | # check SIP
58 | csrutil status
59 |
60 | # disable SIP, need in recovery mode (hold on shift+R when rebooting)
61 | csrutil disable
62 | ```
63 |
64 | ## hook to get wechat database secret keys
65 |
66 | ### 1. 打开mac微信,保持登录页面
67 |
68 | ### 2. 运行监控程序
69 |
70 | ```shell
71 | # comparing to `wechat-decipher-macos`, I make the script more robust.
72 | pgrep -f '^/Applications/WeChat.app/Contents/MacOS/WeChat' | xargs sudo wechat-decipher-macos/macos/dbcracker.d -p
73 | ```
74 |
75 | ### 3. 登录账号,确认是否有各种数据库键的输出
76 |
77 | 类似如下:
78 |
79 | ```text
80 | sqlcipher db path: '/Users/mark/Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/2.0b4.0.9/KeyValue/1d35a41b3adb8b335cc59362ad55ee88/KeyValue.db'
81 | PRAGMA key = "x'b95e58f5e48a455f935963f7f8bdec37a0205f799d8c4465b4c00b7138f516263363959d13f82ce5b9e0c3a74af1df1e'"; PRAGMA cipher_compatibility = 3;
82 |
83 | sqlcipher db path: '/Users/mark/Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/2.0b4.0.9/1d35a41b3adb8b335cc59362ad55ee88/Contact/wccontact_new2.db'
84 | PRAGMA key = "x'b95e58f5e48a455f935963f7f8bdec37a0205f799d8c4465b4c00b7138f51626b07475fbaa4b375dbc932419c1ee54d2'"; PRAGMA cipher_compatibility = 3;
85 |
86 | ...
87 | ```
88 |
89 | 如果没有,提示SIP,则参见之前的步骤;
90 |
91 | 如果没有,也不是SIP,则我也不知道啥原因,请联系我 :)
92 |
93 | 如果有,则说明运行成功,你可以把输出内容拷贝到`data/dbcracker.log`文件内(没有就新建一个)。
94 |
95 | 以后,可以直接使用以下自动往目标文件写入关键信息(而无需手动拷贝):
96 |
97 | ```shell
98 | # monitor into log file, so that to be read by our programme
99 | pgrep -f '^/Applications/WeChat.app/Contents/MacOS/WeChat' | xargs sudo wechat-decipher-macos/macos/dbcracker.d -p > data/dbcracker.log
100 | ```
101 |
102 | ## python sdk
103 |
104 | 在有了`data/dbcracker.log`文件之后,就可以使用我们封装的sdk,它会自动解析数据库,并提供我们的日常使用功能。
105 |
106 | ### inspect all your local wechat databases
107 |
108 | ```shell
109 | # change to your app data path
110 | cd '/Users/mark/Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/2.0b4.0.9/'
111 | ```
112 |
113 | ```text
114 | (venv) 2022/07/04 11:27:23 (base) ➜ 2.0b4.0.9 git:(master) ✗ tree --prune -P "*.db"
115 | .
116 | ├── 1d35a41b3adb8b335cc59362ad55ee88
117 | │ ├── Account
118 | │ │ └── Beta.db
119 | │ ├── ChatSync
120 | │ │ └── ChatSync.db
121 | │ ├── Contact
122 | │ │ └── wccontact_new2.db
123 | │ ├── Favorites
124 | │ │ └── favorites.db
125 | │ ├── FileStateSync
126 | │ │ └── filestatesync.db
127 | │ ├── Group
128 | │ │ └── group_new.db
129 | │ ├── MMLive
130 | │ │ └── live_main.db
131 | │ ├── Message
132 | │ │ ├── fileMsg.db
133 | │ │ ├── fts
134 | │ │ │ └── ftsmessage.db
135 | │ │ ├── ftsfile
136 | │ │ │ └── ftsfilemessage.db
137 | │ │ ├── msg_0.db
138 | │ │ ├── msg_1.db
139 | │ │ ├── msg_2.db
140 | │ │ ├── msg_3.db
141 | │ │ ├── msg_4.db
142 | │ │ ├── msg_5.db
143 | │ │ ├── msg_6.db
144 | │ │ ├── msg_7.db
145 | │ │ ├── msg_8.db
146 | │ │ └── msg_9.db
147 | │ ├── RevokeMsg
148 | │ │ └── revokemsg.db
149 | │ ├── Session
150 | │ │ └── session_new.db
151 | │ ├── Stickers
152 | │ │ └── stickers.db
153 | │ ├── Sync
154 | │ │ ├── openim_oplog.db
155 | │ │ └── oplog_1.1.db
156 | │ ├── solitaire
157 | │ │ └── solitaire_chat.db
158 | │ └── voip
159 | │ └── multiTalk
160 | │ └── multiTalk.db
161 | ├── Backup
162 | │ └── 1d35a41b3adb8b335cc59362ad55ee88
163 | │ ├── A2158f8233bc48b5
164 | │ │ └── Backup.db
165 | │ └── F10A43B8-5032-4E21-A627-F26663F39304
166 | │ └── Backup.db
167 | └── KeyValue
168 | └── 1d35a41b3adb8b335cc59362ad55ee88
169 | └── KeyValue.db
170 |
171 | 24 directories, 30 files
172 | ```
173 |
174 | ### python environment preparation
175 |
176 | ```shell
177 | pip install virtualenv
178 | virtualenv venv
179 | source venv/bin/python
180 | pip install -r requirements.txt
181 | ```
182 |
183 | ### test all the database keys
184 |
185 | ```shell
186 | python src/main.py
187 | ```
188 |
189 | ## ref
190 |
191 | - https://github.com/nalzok/wechat-decipher-macos
192 | - https://github.com/sqlcipher/sqlcipher
193 | - https://github.com/rigglemania/pysqlcipher3
194 | - [Mac终端使用Sqlcipher加解密基础过程详解_Martin.Mu `s Special Column-CSDN博客_mac sqlcipher](https://blog.csdn.net/u011195398/article/details/85266214)
195 |
--------------------------------------------------------------------------------
/packages/wechat-db-manager/src/components/TableList.tsx:
--------------------------------------------------------------------------------
1 | import {useEffect, useState} from 'react';
2 | import {DatabaseInfo, TableInfo} from '../types';
3 | import {dbManager} from '../api';
4 | import {AlertCircle, Hash, Loader, Table} from 'lucide-react';
5 | import {clsx} from 'clsx';
6 |
7 | interface TableListProps {
8 | database: DatabaseInfo;
9 | onSelectTable: (table: TableInfo) => void;
10 | selectedTableName?: string;
11 | }
12 |
13 | export function TableList({database, onSelectTable, selectedTableName}: TableListProps) {
14 | const [tables, setTables] = useState([]);
15 | const [loading, setLoading] = useState(false);
16 | const [error, setError] = useState(null);
17 | const [connected, setConnected] = useState(false);
18 |
19 | useEffect(() => {
20 | if (database) {
21 | connectAndLoadTables();
22 | }
23 | }, [database]);
24 |
25 | const connectAndLoadTables = async () => {
26 | try {
27 | setLoading(true);
28 | setError(null);
29 |
30 | // Connect to the database
31 | await dbManager.connectDatabase(database.id);
32 | setConnected(true);
33 |
34 | // Load tables
35 | const tableList = await dbManager.getTables(database.id);
36 | setTables(tableList);
37 | } catch (err) {
38 | setError(`Failed to connect to database: ${err}`);
39 | setConnected(false);
40 | } finally {
41 | setLoading(false);
42 | }
43 | };
44 |
45 | const formatRowCount = (count?: number): string => {
46 | if (count === undefined) return 'Unknown';
47 | if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`;
48 | if (count >= 1000) return `${(count / 1000).toFixed(1)}K`;
49 | return count.toString();
50 | };
51 |
52 | if (loading) {
53 | return (
54 |
55 |
56 |
57 | );
58 | }
59 |
60 | if (error) {
61 | return (
62 |
63 |
64 |
65 |
66 |
Connection Error
67 |
{error}
68 |
74 |
75 |
76 |
77 | );
78 | }
79 |
80 | if (!connected) {
81 | return (
82 |
83 |
84 |
Not connected to database
85 |
86 | );
87 | }
88 |
89 | if (tables.length === 0) {
90 | return (
91 |
92 |
93 |
No tables found in this database
94 |
95 | );
96 | }
97 |
98 | return (
99 |
100 | {/* 连接状态指示 */}
101 |
105 |
106 |
107 | {tables.map((table) => (
108 |
onSelectTable(table)}
111 | className={clsx(
112 | 'p-3 rounded-xl border cursor-pointer transition-all duration-200 hover:shadow-sm',
113 | selectedTableName === table.name
114 | ? 'border-blue-500 bg-blue-50 shadow-sm ring-1 ring-blue-100'
115 | : 'border-gray-200 hover:border-gray-300 hover:bg-gray-50'
116 | )}
117 | >
118 |
119 |
120 |
123 |
{table.name}
124 |
125 |
126 |
127 |
128 |
129 | {formatRowCount(table.row_count)}
130 |
131 |
{table.columns.length} 列
132 |
133 |
134 |
135 | ))}
136 |
137 |
138 | );
139 | }
--------------------------------------------------------------------------------
/packages/wechat-db-manager/src/components/Avatar.tsx:
--------------------------------------------------------------------------------
1 | import {useMemo} from 'react';
2 | import {User} from 'lucide-react';
3 |
4 | interface AvatarProps {
5 | name: string;
6 | size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
7 | className?: string;
8 | showFallback?: boolean;
9 | }
10 |
11 | export function Avatar({name, size = 'md', className = '', showFallback = true}: AvatarProps) {
12 | const {displayChar, backgroundColor, textColor} = useMemo(() => {
13 | // 生成显示字符
14 | const getDisplayChar = (name: string): string => {
15 | if (!name || name.trim() === '') return '?';
16 |
17 | const trimmedName = name.trim();
18 |
19 | // 中文名字:取最后一个字符(通常是名)
20 | if (/[\u4e00-\u9fa5]/.test(trimmedName)) {
21 | return trimmedName.charAt(trimmedName.length - 1);
22 | }
23 |
24 | // 英文名字:取首字母
25 | if (/[a-zA-Z]/.test(trimmedName)) {
26 | return trimmedName.charAt(0).toUpperCase();
27 | }
28 |
29 | // 数字或特殊字符:取第一个字符
30 | return trimmedName.charAt(0);
31 | };
32 |
33 | // 基于名字生成一致的颜色
34 | const getAvatarColors = (name: string): { backgroundColor: string; textColor: string } => {
35 | if (!name) return {backgroundColor: 'bg-gray-500', textColor: 'text-white'};
36 |
37 | // 预定义的颜色方案
38 | const colorSchemes = [
39 | {bg: 'bg-blue-500', text: 'text-white'},
40 | {bg: 'bg-green-500', text: 'text-white'},
41 | {bg: 'bg-purple-500', text: 'text-white'},
42 | {bg: 'bg-red-500', text: 'text-white'},
43 | {bg: 'bg-orange-500', text: 'text-white'},
44 | {bg: 'bg-teal-500', text: 'text-white'},
45 | {bg: 'bg-pink-500', text: 'text-white'},
46 | {bg: 'bg-indigo-500', text: 'text-white'},
47 | {bg: 'bg-cyan-500', text: 'text-white'},
48 | {bg: 'bg-emerald-500', text: 'text-white'},
49 | {bg: 'bg-amber-500', text: 'text-white'},
50 | {bg: 'bg-lime-500', text: 'text-white'},
51 | {bg: 'bg-rose-500', text: 'text-white'},
52 | {bg: 'bg-violet-500', text: 'text-white'},
53 | {bg: 'bg-fuchsia-500', text: 'text-white'},
54 | ];
55 |
56 | // 基于名字的hash生成一致的颜色索引
57 | let hash = 0;
58 | for (let i = 0; i < name.length; i++) {
59 | const char = name.charCodeAt(i);
60 | hash = ((hash << 5) - hash) + char;
61 | hash = hash & hash; // Convert to 32-bit integer
62 | }
63 |
64 | const colorIndex = Math.abs(hash) % colorSchemes.length;
65 | return colorSchemes[colorIndex];
66 | };
67 |
68 | const displayChar = getDisplayChar(name);
69 | const colors = getAvatarColors(name);
70 |
71 | return {
72 | displayChar,
73 | backgroundColor: colors.bg,
74 | textColor: colors.text
75 | };
76 | }, [name]);
77 |
78 | const sizeClasses = {
79 | xs: 'w-6 h-6 text-xs',
80 | sm: 'w-8 h-8 text-sm',
81 | md: 'w-10 h-10 text-base',
82 | lg: 'w-12 h-12 text-lg',
83 | xl: 'w-16 h-16 text-xl'
84 | };
85 |
86 | return (
87 |
99 | {showFallback && (!name || name.trim() === '') ? (
100 |
101 | ) : (
102 | displayChar
103 | )}
104 |
105 | );
106 | }
107 |
108 | // 头像和名字组合组件
109 | interface AvatarWithNameProps {
110 | name: string;
111 | subtitle?: string;
112 | avatarSize?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
113 | layout?: 'horizontal' | 'vertical';
114 | className?: string;
115 | showSubtitle?: boolean;
116 | }
117 |
118 | export function AvatarWithName({
119 | name,
120 | subtitle,
121 | avatarSize = 'md',
122 | layout = 'horizontal',
123 | className = '',
124 | showSubtitle = true
125 | }: AvatarWithNameProps) {
126 | const layoutClasses = layout === 'horizontal'
127 | ? 'flex items-center space-x-3'
128 | : 'flex flex-col items-center space-y-2';
129 |
130 | const textAlignClass = layout === 'horizontal' ? 'text-left' : 'text-center';
131 |
132 | return (
133 |
134 |
135 |
136 |
137 | {name || '未知用户'}
138 |
139 | {showSubtitle && subtitle && (
140 |
141 | {subtitle}
142 |
143 | )}
144 |
145 |
146 | );
147 | }
148 |
149 | // 用于消息气泡的小头像组件
150 | interface MessageAvatarProps {
151 | name: string;
152 | isOwn?: boolean;
153 | className?: string;
154 | }
155 |
156 | export function MessageAvatar({name, isOwn = false, className = ''}: MessageAvatarProps) {
157 | return (
158 |
163 | );
164 | }
--------------------------------------------------------------------------------
/packages/wechat-db-manager/src/hooks/useAutoConnect.ts:
--------------------------------------------------------------------------------
1 | import {useEffect, useState} from 'react';
2 | import {useAtom} from 'jotai';
3 | import {databasesAtom} from '../store/atoms';
4 | import {AutoConnectService} from '../services/autoConnectService';
5 | import {DatabaseInfo} from '../types';
6 |
7 | export interface AutoConnectState {
8 | isAutoConnecting: boolean;
9 | autoConnectProgress: {
10 | message: string;
11 | current: number;
12 | total: number;
13 | } | null;
14 | autoConnectError: string | null;
15 | connectedDatabases: DatabaseInfo[];
16 | }
17 |
18 | export function useAutoConnect() {
19 | const [databases, setDatabases] = useAtom(databasesAtom);
20 | const [state, setState] = useState({
21 | isAutoConnecting: false,
22 | autoConnectProgress: null,
23 | autoConnectError: null,
24 | connectedDatabases: []
25 | });
26 |
27 | // 在组件挂载时自动连接
28 | useEffect(() => {
29 | let mounted = true;
30 |
31 | const performAutoConnect = async () => {
32 | if (!AutoConnectService.shouldShowAutoConnectPrompt()) {
33 | return;
34 | }
35 |
36 | setState(prev => ({
37 | ...prev,
38 | isAutoConnecting: true,
39 | autoConnectError: null
40 | }));
41 |
42 | try {
43 | const connectedDatabases = await AutoConnectService.autoConnect(
44 | (message, current, total) => {
45 | if (mounted) {
46 | setState(prev => ({
47 | ...prev,
48 | autoConnectProgress: {message, current, total}
49 | }));
50 | }
51 | },
52 | (error) => {
53 | if (mounted) {
54 | setState(prev => ({
55 | ...prev,
56 | autoConnectError: error
57 | }));
58 | }
59 | }
60 | );
61 |
62 | if (mounted) {
63 | setState(prev => ({
64 | ...prev,
65 | connectedDatabases,
66 | isAutoConnecting: false,
67 | autoConnectProgress: null
68 | }));
69 |
70 | // 更新全局数据库状态
71 | if (connectedDatabases.length > 0) {
72 | setDatabases(connectedDatabases);
73 | }
74 | }
75 |
76 | } catch (error) {
77 | if (mounted) {
78 | const errorMessage = error instanceof Error ? error.message : '自动连接失败';
79 | setState(prev => ({
80 | ...prev,
81 | isAutoConnecting: false,
82 | autoConnectError: errorMessage,
83 | autoConnectProgress: null
84 | }));
85 | }
86 | }
87 | };
88 |
89 | // 延迟500ms开始自动连接,让UI先渲染
90 | const timer = setTimeout(performAutoConnect, 500);
91 |
92 | return () => {
93 | mounted = false;
94 | clearTimeout(timer);
95 | };
96 | }, [setDatabases]);
97 |
98 | // 手动触发自动连接
99 | const triggerAutoConnect = async () => {
100 | setState(prev => ({
101 | ...prev,
102 | isAutoConnecting: true,
103 | autoConnectError: null,
104 | autoConnectProgress: null
105 | }));
106 |
107 | try {
108 | const connectedDatabases = await AutoConnectService.autoConnect(
109 | (message, current, total) => {
110 | setState(prev => ({
111 | ...prev,
112 | autoConnectProgress: {message, current, total}
113 | }));
114 | },
115 | (error) => {
116 | setState(prev => ({
117 | ...prev,
118 | autoConnectError: error
119 | }));
120 | }
121 | );
122 |
123 | setState(prev => ({
124 | ...prev,
125 | connectedDatabases,
126 | isAutoConnecting: false,
127 | autoConnectProgress: null
128 | }));
129 |
130 | // 更新全局数据库状态
131 | if (connectedDatabases.length > 0) {
132 | setDatabases(connectedDatabases);
133 | }
134 |
135 | } catch (error) {
136 | const errorMessage = error instanceof Error ? error.message : '自动连接失败';
137 | setState(prev => ({
138 | ...prev,
139 | isAutoConnecting: false,
140 | autoConnectError: errorMessage,
141 | autoConnectProgress: null
142 | }));
143 | }
144 | };
145 |
146 | // 更新已连接数据库列表
147 | const updateConnectedDatabases = (newDatabases: DatabaseInfo[]) => {
148 | AutoConnectService.updateConnectedDatabases(newDatabases);
149 | setState(prev => ({
150 | ...prev,
151 | connectedDatabases: newDatabases
152 | }));
153 | };
154 |
155 | // 清除错误
156 | const clearError = () => {
157 | setState(prev => ({
158 | ...prev,
159 | autoConnectError: null
160 | }));
161 | };
162 |
163 | // 启用/禁用自动连接
164 | const toggleAutoConnect = (enabled: boolean) => {
165 | if (enabled) {
166 | AutoConnectService.enableAutoConnect();
167 | } else {
168 | AutoConnectService.disableAutoConnect();
169 | }
170 | };
171 |
172 | return {
173 | ...state,
174 | triggerAutoConnect,
175 | updateConnectedDatabases,
176 | clearError,
177 | toggleAutoConnect
178 | };
179 | }
--------------------------------------------------------------------------------
/packages/wechat-db-manager/src/services/autoConnectService.ts:
--------------------------------------------------------------------------------
1 | import {DatabaseInfo} from '../types';
2 | import {dbManager} from '../api';
3 |
4 | export interface AutoConnectSettings {
5 | enableAutoConnect: boolean;
6 | lastConnectedDatabases: DatabaseInfo[];
7 | autoConnectTimeout: number;
8 | }
9 |
10 | export class AutoConnectService {
11 | private static readonly STORAGE_KEY = 'wechat-db-manager-autoconnect';
12 | private static readonly DEFAULT_TIMEOUT = 30000; // 30秒超时
13 |
14 | /**
15 | * 保存自动连接设置
16 | */
17 | static saveSettings(settings: AutoConnectSettings): void {
18 | try {
19 | localStorage.setItem(this.STORAGE_KEY, JSON.stringify(settings));
20 | console.log('✅ 自动连接设置已保存');
21 | } catch (error) {
22 | console.error('❌ 保存自动连接设置失败:', error);
23 | }
24 | }
25 |
26 | /**
27 | * 加载自动连接设置
28 | */
29 | static loadSettings(): AutoConnectSettings {
30 | try {
31 | const saved = localStorage.getItem(this.STORAGE_KEY);
32 | if (saved) {
33 | const settings = JSON.parse(saved) as AutoConnectSettings;
34 | return {
35 | enableAutoConnect: settings.enableAutoConnect ?? true,
36 | lastConnectedDatabases: settings.lastConnectedDatabases ?? [],
37 | autoConnectTimeout: settings.autoConnectTimeout ?? this.DEFAULT_TIMEOUT
38 | };
39 | }
40 | } catch (error) {
41 | console.error('❌ 加载自动连接设置失败:', error);
42 | }
43 |
44 | return {
45 | enableAutoConnect: true,
46 | lastConnectedDatabases: [],
47 | autoConnectTimeout: this.DEFAULT_TIMEOUT
48 | };
49 | }
50 |
51 | /**
52 | * 更新已连接数据库列表
53 | */
54 | static updateConnectedDatabases(databases: DatabaseInfo[]): void {
55 | const settings = this.loadSettings();
56 | settings.lastConnectedDatabases = databases;
57 | this.saveSettings(settings);
58 | }
59 |
60 | /**
61 | * 自动连接到之前连接的数据库
62 | */
63 | static async autoConnect(
64 | onProgress?: (message: string, current: number, total: number) => void,
65 | onError?: (error: string) => void
66 | ): Promise {
67 | const settings = this.loadSettings();
68 |
69 | if (!settings.enableAutoConnect) {
70 | console.log('🚫 自动连接已禁用');
71 | return [];
72 | }
73 |
74 | const databases = settings.lastConnectedDatabases;
75 |
76 | if (databases.length === 0) {
77 | console.log('ℹ️ 没有需要自动连接的数据库');
78 | return [];
79 | }
80 |
81 | console.log(`🚀 开始自动连接 ${databases.length} 个数据库`);
82 |
83 | const successfulConnections: DatabaseInfo[] = [];
84 | const failedConnections: string[] = [];
85 |
86 | for (let i = 0; i < databases.length; i++) {
87 | const database = databases[i];
88 |
89 | onProgress?.(
90 | `正在连接 ${database.filename}...`,
91 | i,
92 | databases.length
93 | );
94 |
95 | try {
96 | // 检查文件是否仍然存在和可访问
97 | if (!database.accessible) {
98 | console.warn(`⚠️ 数据库 ${database.filename} 不可访问,跳过`);
99 | failedConnections.push(`${database.filename}: 文件不可访问`);
100 | continue;
101 | }
102 |
103 | // 尝试连接
104 | const connectPromise = dbManager.connectDatabase(database.id);
105 | const timeoutPromise = new Promise((_, reject) => {
106 | setTimeout(() => reject(new Error('连接超时')), settings.autoConnectTimeout);
107 | });
108 |
109 | await Promise.race([connectPromise, timeoutPromise]);
110 |
111 | successfulConnections.push(database);
112 | console.log(`✅ 成功连接数据库: ${database.filename}`);
113 |
114 | } catch (error) {
115 | const errorMessage = error instanceof Error ? error.message : '未知错误';
116 | console.error(`❌ 连接数据库 ${database.filename} 失败:`, errorMessage);
117 | failedConnections.push(`${database.filename}: ${errorMessage}`);
118 | }
119 | }
120 |
121 | onProgress?.(
122 | `自动连接完成`,
123 | databases.length,
124 | databases.length
125 | );
126 |
127 | // 报告结果
128 | if (successfulConnections.length > 0) {
129 | console.log(`✅ 成功自动连接 ${successfulConnections.length}/${databases.length} 个数据库`);
130 | }
131 |
132 | if (failedConnections.length > 0) {
133 | const errorMessage = `部分数据库连接失败:\n${failedConnections.join('\n')}`;
134 | console.warn('⚠️ 自动连接部分失败:', errorMessage);
135 | onError?.(errorMessage);
136 | }
137 |
138 | return successfulConnections;
139 | }
140 |
141 | /**
142 | * 禁用自动连接
143 | */
144 | static disableAutoConnect(): void {
145 | const settings = this.loadSettings();
146 | settings.enableAutoConnect = false;
147 | this.saveSettings(settings);
148 | }
149 |
150 | /**
151 | * 启用自动连接
152 | */
153 | static enableAutoConnect(): void {
154 | const settings = this.loadSettings();
155 | settings.enableAutoConnect = true;
156 | this.saveSettings(settings);
157 | }
158 |
159 | /**
160 | * 清除自动连接设置
161 | */
162 | static clearSettings(): void {
163 | try {
164 | localStorage.removeItem(this.STORAGE_KEY);
165 | console.log('✅ 自动连接设置已清除');
166 | } catch (error) {
167 | console.error('❌ 清除自动连接设置失败:', error);
168 | }
169 | }
170 |
171 | /**
172 | * 检查是否应该显示自动连接提示
173 | */
174 | static shouldShowAutoConnectPrompt(): boolean {
175 | const settings = this.loadSettings();
176 | return settings.enableAutoConnect && settings.lastConnectedDatabases.length > 0;
177 | }
178 | }
--------------------------------------------------------------------------------
/packages/wechat-db-manager/README.md:
--------------------------------------------------------------------------------
1 | # WeChat Database Manager
2 |
3 | A comprehensive Tauri application for managing and browsing WeChat SQLCipher databases. This application provides a user-friendly interface to explore WeChat database contents, execute queries, and export data.
4 |
5 | ## Features
6 |
7 | ### Database Management
8 | - **Automatic Key Parsing**: Parses `.keys` files generated by the WeChat database cracker
9 | - **Database Connection**: Securely connects to SQLCipher databases using extracted keys
10 | - **Database Browser**: Browse all available databases with metadata (size, type, accessibility)
11 | - **Connection Pool**: Manages multiple database connections efficiently
12 |
13 | ### Data Exploration
14 | - **Table Browser**: Navigate through database tables with column information
15 | - **Data Viewer**: Browse table contents with pagination and search
16 | - **Custom Queries**: Execute custom SQL queries with result visualization
17 | - **Schema Inspector**: View table structures and column details
18 |
19 | ### Export & Analysis
20 | - **CSV Export**: Export table data to CSV format
21 | - **Query Results**: Export custom query results
22 | - **Data Visualization**: View row counts, data types, and table statistics
23 |
24 | ### User Interface
25 | - **Modern UI**: Clean, responsive interface built with React and Tailwind CSS
26 | - **Database Types**: Color-coded database types (Messages, Contacts, Groups, etc.)
27 | - **Real-time Updates**: Live data loading with loading states
28 | - **Error Handling**: Comprehensive error handling with user-friendly messages
29 |
30 | ## Architecture
31 |
32 | ### Backend (Rust/Tauri)
33 | - **SQLCipher Integration**: Uses `rusqlite` with SQLCipher support
34 | - **Database Module**: Handles database parsing, connection, and queries
35 | - **API Endpoints**: Exposes database operations to frontend
36 | - **Security**: Secure key handling and database access
37 |
38 | ### Frontend (React/TypeScript)
39 | - **Component Architecture**: Modular React components for different views
40 | - **State Management**: React hooks for application state
41 | - **API Integration**: Tauri invoke API for backend communication
42 | - **Responsive Design**: Mobile-friendly interface with Tailwind CSS
43 |
44 | ## Usage
45 |
46 | ### 1. Prerequisites
47 | - Rust development environment
48 | - Node.js and pnpm
49 | - SQLCipher libraries
50 | - WeChat `.keys` file from the database cracker
51 |
52 | ### 2. Development Setup
53 |
54 | ```bash
55 | # Install dependencies
56 | pnpm install
57 |
58 | # Start development server
59 | pnpm tauri dev
60 | ```
61 |
62 | ### 3. Loading Database Keys
63 |
64 | 1. **Generate Keys**: First, use the WeChat database cracker to generate a `.keys` file
65 | 2. **Load Keys**: In the application, click "Load Keys File" to import your database configurations
66 | 3. **Browse Databases**: Select databases from the sidebar to view their tables
67 |
68 | ### 4. Exploring Data
69 |
70 | 1. **Select Database**: Click on a database in the left sidebar
71 | 2. **Connect**: The application will automatically connect to the selected database
72 | 3. **Browse Tables**: Select tables from the table list to view their contents
73 | 4. **Query Data**: Use the search button to execute custom SQL queries
74 | 5. **Export Data**: Click the download button to export table data as CSV
75 |
76 | ### 5. Database Types
77 |
78 | The application recognizes these WeChat database types:
79 | - **Messages**: Chat messages (msg_0.db to msg_9.db)
80 | - **Contacts**: Contact information (wccontact_new2.db)
81 | - **Groups**: Group information (group_new.db)
82 | - **Sessions**: Session data (session_new.db)
83 | - **Favorites**: Bookmarked content (favorites.db)
84 | - **Media**: Media file metadata (mediaData.db)
85 | - **Search**: Full-text search indexes (ftsmessage.db)
86 |
87 | ### 6. Security Considerations
88 |
89 | - Database keys are handled securely in memory
90 | - No persistent storage of encryption keys
91 | - Read-only access to database files
92 | - Connection pooling with proper cleanup
93 |
94 | ## File Structure
95 |
96 | ```
97 | src/
98 | ├── components/
99 | │ ├── DatabaseList.tsx # Database browser component
100 | │ ├── TableList.tsx # Table browser component
101 | │ └── TableView.tsx # Data viewer component
102 | ├── api.ts # Backend API interface
103 | ├── types.ts # TypeScript type definitions
104 | ├── App.tsx # Main application component
105 | └── main.tsx # Application entry point
106 |
107 | src-tauri/
108 | ├── src/
109 | │ ├── database.rs # Database management module
110 | │ ├── lib.rs # Tauri commands and setup
111 | │ └── main.rs # Application entry point
112 | ├── Cargo.toml # Rust dependencies
113 | └── tauri.conf.json # Tauri configuration
114 | ```
115 |
116 | ## Build for Production
117 |
118 | ```bash
119 | # Build the application
120 | pnpm build
121 |
122 | # Create distribution package
123 | pnpm tauri build
124 | ```
125 |
126 | ## Troubleshooting
127 |
128 | ### Common Issues
129 |
130 | 1. **SQLCipher Connection Error**
131 | - Ensure SQLCipher is properly installed
132 | - Verify database file accessibility
133 | - Check if the database keys are correct
134 |
135 | 2. **Database Not Found**
136 | - Verify the `.keys` file path is correct
137 | - Check file permissions
138 | - Ensure the database files exist at the specified paths
139 |
140 | 3. **Table Loading Issues**
141 | - Verify database connection is established
142 | - Check if the database is encrypted correctly
143 | - Ensure proper SQLCipher compatibility settings
144 |
145 | ### Development Tips
146 |
147 | 1. **Adding New Database Types**
148 | - Update the `DB_TYPE_LABELS` and `DB_TYPE_COLORS` in `types.ts`
149 | - Modify the `extract_db_type` function in `database.rs`
150 |
151 | 2. **Custom Query Features**
152 | - Use the custom query input to explore database schemas
153 | - Common queries: `SELECT * FROM sqlite_master` to view all tables
154 | - Use `PRAGMA table_info(table_name)` to inspect table structure
155 |
156 | 3. **Performance Optimization**
157 | - Use pagination for large datasets
158 | - Limit query results with `LIMIT` clause
159 | - Index frequently queried columns
160 |
161 | ## Security Notes
162 |
163 | This application is designed for legitimate database recovery and analysis purposes. It:
164 | - Handles sensitive personal data with appropriate security measures
165 | - Provides read-only access to database contents
166 | - Implements proper encryption key management
167 | - Follows security best practices for data handling
168 |
169 | ## License
170 |
171 | This project is part of the WeChat Database Cracker suite and follows the same licensing terms.
--------------------------------------------------------------------------------
/packages/wechat-db-manager/src/components/FilePathInput.tsx:
--------------------------------------------------------------------------------
1 | import {useState} from 'react';
2 | import {useAtom} from 'jotai';
3 | import {databasesAtom, errorAtom, loadingAtom, persistedKeysPathAtom} from '../store/atoms';
4 | import {dbManager} from '../api';
5 | import {AlertCircle, Check, File, X} from 'lucide-react';
6 |
7 | interface FilePathInputProps {
8 | onFileLoaded?: () => void;
9 | }
10 |
11 | export function FilePathInput({onFileLoaded}: FilePathInputProps) {
12 | const [keysPath, setKeysPath] = useAtom(persistedKeysPathAtom);
13 | const [loading, setLoading] = useAtom(loadingAtom);
14 | const [error, setError] = useAtom(errorAtom);
15 | const [, setDatabases] = useAtom(databasesAtom);
16 | const [inputPath, setInputPath] = useState(keysPath || '');
17 | const [isEditing, setIsEditing] = useState(false);
18 |
19 | const loadFile = async (path: string) => {
20 | try {
21 | setLoading(true);
22 | setError(null);
23 |
24 | const databases = await dbManager.loadKeysFile(path);
25 | setDatabases(databases);
26 | setKeysPath(path);
27 | setInputPath(path);
28 | setIsEditing(false);
29 | onFileLoaded?.();
30 | } catch (err) {
31 | setError(`Failed to load keys file: ${err}`);
32 | } finally {
33 | setLoading(false);
34 | }
35 | };
36 |
37 | const handleSubmit = async (e: React.FormEvent) => {
38 | e.preventDefault();
39 | if (inputPath.trim()) {
40 | await loadFile(inputPath.trim());
41 | }
42 | };
43 |
44 | const handleCancel = () => {
45 | setInputPath(keysPath || '');
46 | setIsEditing(false);
47 | };
48 |
49 | const clearFile = () => {
50 | setKeysPath(null);
51 | setInputPath('');
52 | setDatabases([]);
53 | setError(null);
54 | setIsEditing(false);
55 | };
56 |
57 | return (
58 |
59 |
60 |
Keys File Path
61 | {keysPath && !isEditing && (
62 |
69 | )}
70 |
71 |
72 | {isEditing ? (
73 |
106 | ) : keysPath ? (
107 |
108 |
109 |
110 |
111 | {keysPath}
112 |
113 |
114 |
115 |
116 |
123 |
124 |
131 |
132 |
133 | ) : (
134 |
135 |
136 |
137 |
No keys file selected
138 |
139 |
140 |
146 |
147 | )}
148 |
149 | {error && (
150 |
151 | {error}
152 |
153 | )}
154 |
155 | );
156 | }
--------------------------------------------------------------------------------
/packages/wechat-db-manager/src/utils/contactParser.tsx:
--------------------------------------------------------------------------------
1 | import {QueryResult} from '../types';
2 |
3 | export interface EnhancedContact {
4 | id: string;
5 | displayName: string; // 优先显示的名字
6 | nickname?: string; // 昵称
7 | remark?: string; // 备注名
8 | username?: string; // 用户名/微信号
9 | realName?: string; // 真实姓名
10 | avatar?: string; // 头像数据
11 | phoneNumber?: string; // 电话号码
12 | email?: string; // 邮箱
13 | contactType?: 'user' | 'group' | 'official' | 'unknown';
14 | lastActiveTime?: string; // 最后活跃时间
15 | isBlocked?: boolean; // 是否被屏蔽
16 | isFriend?: boolean; // 是否为好友
17 | originalId?: string; // 原始ID(M_NSUSRNAME等)用于MD5映射
18 | }
19 |
20 | export class ContactParser {
21 | /**
22 | * 解析联系人数据,智能提取各种字段
23 | */
24 | static parseContacts(result: QueryResult): EnhancedContact[] {
25 | const {columns, rows} = result;
26 |
27 | // 智能字段映射
28 | const fieldMapping = this.createFieldMapping(columns);
29 |
30 | return rows
31 | .map((row, index) => this.parseContact(row, fieldMapping, index))
32 | .filter(contact => this.isValidContact(contact));
33 | }
34 |
35 | /**
36 | * 搜索联系人
37 | */
38 | static searchContacts(contacts: EnhancedContact[], searchTerm: string): EnhancedContact[] {
39 | if (!searchTerm.trim()) return contacts;
40 |
41 | const term = searchTerm.toLowerCase();
42 |
43 | return contacts.filter(contact =>
44 | contact.displayName.toLowerCase().includes(term) ||
45 | contact.nickname?.toLowerCase().includes(term) ||
46 | contact.remark?.toLowerCase().includes(term) ||
47 | contact.username?.toLowerCase().includes(term) ||
48 | contact.realName?.toLowerCase().includes(term)
49 | );
50 | }
51 |
52 | /**
53 | * 按类型过滤联系人
54 | */
55 | static filterByType(contacts: EnhancedContact[], type: EnhancedContact['contactType']): EnhancedContact[] {
56 | return contacts.filter(contact => contact.contactType === type);
57 | }
58 |
59 | /**
60 | * 获取联系人的最佳显示信息
61 | */
62 | static getDisplayInfo(contact: EnhancedContact): { name: string; subtitle: string } {
63 | const name = contact.displayName;
64 |
65 | // 构建副标题
66 | const subtitleParts: string[] = [];
67 |
68 | if (contact.remark && contact.remark !== contact.displayName) {
69 | subtitleParts.push(`备注: ${contact.remark}`);
70 | } else if (contact.nickname && contact.nickname !== contact.displayName) {
71 | subtitleParts.push(`昵称: ${contact.nickname}`);
72 | }
73 |
74 | if (contact.username && contact.username !== contact.displayName) {
75 | subtitleParts.push(`微信号: ${contact.username}`);
76 | }
77 |
78 | const subtitle = subtitleParts.length > 0
79 | ? subtitleParts.join(' • ')
80 | : contact.contactType === 'group'
81 | ? '群聊'
82 | : contact.contactType === 'official'
83 | ? '公众号'
84 | : '点击查看聊天记录';
85 |
86 | return {name, subtitle};
87 | }
88 |
89 | /**
90 | * 创建字段映射
91 | */
92 | private static createFieldMapping(columns: string[]) {
93 | const mapping: Record = {};
94 |
95 | // 定义字段匹配规则
96 | const fieldRules = {
97 | // 名字相关字段(按优先级排序)
98 | remark: ['remark', 'remarkname', 'remark_name', 'contact_remark'],
99 | nickname: ['nickname', 'nick_name', 'displayname', 'display_name', 'name'],
100 | realname: ['realname', 'real_name', 'fullname', 'full_name'],
101 |
102 | // ID相关字段 - 添加M_NSUSRNAME支持
103 | username: ['M_NSUSRNAME', 'm_nsusrname', 'MUSRNAME', 'musrname', 'username', 'user_name', 'wxid', 'wx_id', 'userid', 'user_id'],
104 | contactid: ['contactid', 'contact_id', 'id', 'talker'],
105 |
106 | // 头像相关字段
107 | avatar: ['avatar', 'headimg', 'headimgurl', 'head_img_url', 'portrait', 'photo'],
108 |
109 | // 联系方式
110 | phone: ['phone', 'phonenumber', 'phone_number', 'mobile', 'tel'],
111 | email: ['email', 'mail', 'email_address'],
112 |
113 | // 状态字段
114 | type: ['type', 'contact_type', 'user_type', 'contacttype'],
115 | status: ['status', 'contact_status', 'friend_status'],
116 | blocked: ['blocked', 'is_blocked', 'blacklist'],
117 |
118 | // 时间字段
119 | lastactive: ['lastactive', 'last_active', 'lastseen', 'last_seen', 'updatetime', 'update_time']
120 | };
121 |
122 | // 为每个字段找到最匹配的列
123 | Object.entries(fieldRules).forEach(([field, patterns]) => {
124 | for (const pattern of patterns) {
125 | const index = columns.findIndex(col =>
126 | col.toLowerCase().includes(pattern.toLowerCase())
127 | );
128 | if (index !== -1) {
129 | mapping[field] = index;
130 | break;
131 | }
132 | }
133 | });
134 |
135 | return mapping;
136 | }
137 |
138 | /**
139 | * 解析单个联系人
140 | */
141 | private static parseContact(row: any[], mapping: Record, index: number): EnhancedContact {
142 | const getValue = (field: string): string | undefined => {
143 | const colIndex = mapping[field];
144 | if (colIndex === undefined || colIndex === -1) return undefined;
145 | const value = row[colIndex];
146 | return value && String(value).trim() !== '' && String(value) !== 'null'
147 | ? String(value).trim()
148 | : undefined;
149 | };
150 |
151 | // 提取各种名字字段
152 | const remark = getValue('remark');
153 | const nickname = getValue('nickname');
154 | const realname = getValue('realname');
155 | const username = getValue('username');
156 |
157 | // 确定显示名字的优先级:备注名 > 昵称 > 真实姓名 > 用户名
158 | const displayName = remark || nickname || realname || username || `联系人${index + 1}`;
159 |
160 | // 生成唯一ID
161 | const contactId = getValue('contactid') || getValue('username') || displayName || `contact_${index}`;
162 |
163 | // 保留原始ID和M_NSUSRNAME用于调试和MD5映射
164 | const originalId = getValue('username') || getValue('contactid');
165 |
166 | // 判断联系人类型
167 | const contactType = this.determineContactType(displayName, username);
168 |
169 | return {
170 | id: contactId,
171 | displayName,
172 | nickname,
173 | remark,
174 | username,
175 | realName: realname,
176 | avatar: getValue('avatar'),
177 | phoneNumber: getValue('phone'),
178 | email: getValue('email'),
179 | contactType,
180 | lastActiveTime: getValue('lastactive'),
181 | isBlocked: this.parseBoolean(getValue('blocked')),
182 | isFriend: contactType === 'user',
183 | originalId: originalId // 添加原始ID用于MD5映射
184 | };
185 | }
186 |
187 | /**
188 | * 判断联系人类型
189 | */
190 | private static determineContactType(displayName?: string, username?: string): EnhancedContact['contactType'] {
191 | if (!displayName && !username) return 'unknown';
192 |
193 | const name = displayName || username || '';
194 |
195 | // 群聊识别
196 | if (name.includes('@chatroom') || name.startsWith('群聊') || name.includes('群')) {
197 | return 'group';
198 | }
199 |
200 | // 公众号识别
201 | if (name.startsWith('gh_') || name.includes('公众号') || name.includes('服务号')) {
202 | return 'official';
203 | }
204 |
205 | // 系统账号识别
206 | if (name.includes('微信') || name.includes('系统') || name.startsWith('wx')) {
207 | return 'official';
208 | }
209 |
210 | return 'user';
211 | }
212 |
213 | /**
214 | * 解析布尔值
215 | */
216 | private static parseBoolean(value?: string): boolean {
217 | if (!value) return false;
218 | return value.toLowerCase() === 'true' || value === '1' || value === 'yes';
219 | }
220 |
221 | /**
222 | * 验证联系人是否有效
223 | */
224 | private static isValidContact(contact: EnhancedContact): boolean {
225 | return !!(
226 | contact.id &&
227 | contact.displayName &&
228 | contact.displayName.trim() !== '' &&
229 | !contact.displayName.includes('null')
230 | );
231 | }
232 | }
--------------------------------------------------------------------------------
/packages/wechat-db-manager/src/utils/contactParser.ts:
--------------------------------------------------------------------------------
1 | import {QueryResult} from '../types';
2 |
3 | export interface EnhancedContact {
4 | id: string;
5 | originalId?: string; // 原始数据库标识符,用于消息匹配
6 | mNsUsrName?: string; // m_nsUsrName字段值,用于MD5表映射
7 | displayName: string; // 优先显示的名字
8 | nickname?: string; // 昵称
9 | remark?: string; // 备注名
10 | username?: string; // 用户名/微信号
11 | realName?: string; // 真实姓名
12 | avatar?: string; // 头像数据
13 | phoneNumber?: string; // 电话号码
14 | email?: string; // 邮箱
15 | contactType?: 'user' | 'group' | 'official' | 'unknown';
16 | lastActiveTime?: string; // 最后活跃时间
17 | isBlocked?: boolean; // 是否被屏蔽
18 | isFriend?: boolean; // 是否为好友
19 | }
20 |
21 | export class ContactParser {
22 | /**
23 | * 解析联系人数据,智能提取各种字段
24 | */
25 | static parseContacts(result: QueryResult): EnhancedContact[] {
26 | const {columns, rows} = result;
27 |
28 | // 智能字段映射
29 | const fieldMapping = this.createFieldMapping(columns);
30 |
31 | return rows
32 | .map((row, index) => this.parseContact(row, fieldMapping, index))
33 | .filter(contact => this.isValidContact(contact));
34 | }
35 |
36 | /**
37 | * 搜索联系人
38 | */
39 | static searchContacts(contacts: EnhancedContact[], searchTerm: string): EnhancedContact[] {
40 | if (!searchTerm.trim()) return contacts;
41 |
42 | const term = searchTerm.toLowerCase();
43 |
44 | return contacts.filter(contact =>
45 | contact.displayName.toLowerCase().includes(term) ||
46 | contact.nickname?.toLowerCase().includes(term) ||
47 | contact.remark?.toLowerCase().includes(term) ||
48 | contact.username?.toLowerCase().includes(term) ||
49 | contact.realName?.toLowerCase().includes(term)
50 | );
51 | }
52 |
53 | /**
54 | * 按类型过滤联系人
55 | */
56 | static filterByType(contacts: EnhancedContact[], type: EnhancedContact['contactType']): EnhancedContact[] {
57 | return contacts.filter(contact => contact.contactType === type);
58 | }
59 |
60 | /**
61 | * 获取联系人的最佳显示信息
62 | */
63 | static getDisplayInfo(contact: EnhancedContact): { name: string; subtitle: string } {
64 | const name = contact.displayName;
65 |
66 | // 构建副标题
67 | const subtitleParts: string[] = [];
68 |
69 | if (contact.remark && contact.remark !== contact.displayName) {
70 | subtitleParts.push(`备注: ${contact.remark}`);
71 | } else if (contact.nickname && contact.nickname !== contact.displayName) {
72 | subtitleParts.push(`昵称: ${contact.nickname}`);
73 | }
74 |
75 | if (contact.username && contact.username !== contact.displayName) {
76 | subtitleParts.push(`微信号: ${contact.username}`);
77 | }
78 |
79 | const subtitle = subtitleParts.length > 0
80 | ? subtitleParts.join(' • ')
81 | : contact.contactType === 'group'
82 | ? '群聊'
83 | : contact.contactType === 'official'
84 | ? '公众号'
85 | : '点击查看聊天记录';
86 |
87 | return {name, subtitle};
88 | }
89 |
90 | /**
91 | * 创建字段映射
92 | */
93 | private static createFieldMapping(columns: string[]) {
94 | const mapping: Record = {};
95 |
96 | // 定义字段匹配规则
97 | const fieldRules = {
98 | // 名字相关字段(按优先级排序)
99 | remark: ['remark', 'remarkname', 'remark_name', 'contact_remark'],
100 | nickname: ['nickname', 'nick_name', 'displayname', 'display_name', 'name'],
101 | realname: ['realname', 'real_name', 'fullname', 'full_name'],
102 |
103 | // ID相关字段
104 | username: ['username', 'user_name', 'wxid', 'wx_id', 'userid', 'user_id'],
105 | contactid: ['contactid', 'contact_id', 'id', 'talker'],
106 | m_nsusrname: ['m_nsusrname', 'nsusrname', 'usr_name', 'usrname', 'nsuser'],
107 |
108 | // 头像相关字段
109 | avatar: ['avatar', 'headimg', 'headimgurl', 'head_img_url', 'portrait', 'photo'],
110 |
111 | // 联系方式
112 | phone: ['phone', 'phonenumber', 'phone_number', 'mobile', 'tel'],
113 | email: ['email', 'mail', 'email_address'],
114 |
115 | // 状态字段
116 | type: ['type', 'contact_type', 'user_type', 'contacttype'],
117 | status: ['status', 'contact_status', 'friend_status'],
118 | blocked: ['blocked', 'is_blocked', 'blacklist'],
119 |
120 | // 时间字段
121 | lastactive: ['lastactive', 'last_active', 'lastseen', 'last_seen', 'updatetime', 'update_time']
122 | };
123 |
124 | // 为每个字段找到最匹配的列
125 | Object.entries(fieldRules).forEach(([field, patterns]) => {
126 | for (const pattern of patterns) {
127 | const index = columns.findIndex(col =>
128 | col.toLowerCase().includes(pattern.toLowerCase())
129 | );
130 | if (index !== -1) {
131 | mapping[field] = index;
132 | break;
133 | }
134 | }
135 | });
136 |
137 | return mapping;
138 | }
139 |
140 | /**
141 | * 解析单个联系人
142 | */
143 | private static parseContact(row: any[], mapping: Record, index: number): EnhancedContact {
144 | const getValue = (field: string): string | undefined => {
145 | const colIndex = mapping[field];
146 | if (colIndex === undefined || colIndex === -1) return undefined;
147 | const value = row[colIndex];
148 | return value && String(value).trim() !== '' && String(value) !== 'null'
149 | ? String(value).trim()
150 | : undefined;
151 | };
152 |
153 | // 提取各种名字字段
154 | const remark = getValue('remark');
155 | const nickname = getValue('nickname');
156 | const realname = getValue('realname');
157 | const username = getValue('username');
158 |
159 | // 确定显示名字的优先级:备注名 > 昵称 > 真实姓名 > 用户名
160 | const displayName = remark || nickname || realname || username || `联系人${index + 1}`;
161 |
162 | // 生成唯一ID - 保持原始标识符用于消息匹配
163 | const primaryId = getValue('contactid') || getValue('username');
164 | const contactId = primaryId || `contact_${index}`;
165 |
166 | // 保存原始标识符用于消息匹配 - 优先使用m_nsUsrName
167 | const mNsUsrName = getValue('m_nsusrname');
168 | const originalId = mNsUsrName || primaryId;
169 |
170 | // 判断联系人类型
171 | const contactType = this.determineContactType(displayName, username);
172 |
173 | return {
174 | id: contactId,
175 | originalId,
176 | mNsUsrName,
177 | displayName,
178 | nickname,
179 | remark,
180 | username,
181 | realName: realname,
182 | avatar: getValue('avatar'),
183 | phoneNumber: getValue('phone'),
184 | email: getValue('email'),
185 | contactType,
186 | lastActiveTime: getValue('lastactive'),
187 | isBlocked: this.parseBoolean(getValue('blocked')),
188 | isFriend: contactType === 'user'
189 | };
190 | }
191 |
192 | /**
193 | * 判断联系人类型
194 | */
195 | private static determineContactType(displayName?: string, username?: string): EnhancedContact['contactType'] {
196 | if (!displayName && !username) return 'unknown';
197 |
198 | const name = displayName || username || '';
199 |
200 | // 群聊识别
201 | if (name.includes('@chatroom') || name.startsWith('群聊') || name.includes('群')) {
202 | return 'group';
203 | }
204 |
205 | // 公众号识别
206 | if (name.startsWith('gh_') || name.includes('公众号') || name.includes('服务号')) {
207 | return 'official';
208 | }
209 |
210 | // 系统账号识别
211 | if (name.includes('微信') || name.includes('系统') || name.startsWith('wx')) {
212 | return 'official';
213 | }
214 |
215 | return 'user';
216 | }
217 |
218 | /**
219 | * 解析布尔值
220 | */
221 | private static parseBoolean(value?: string): boolean {
222 | if (!value) return false;
223 | return value.toLowerCase() === 'true' || value === '1' || value === 'yes';
224 | }
225 |
226 | /**
227 | * 验证联系人是否有效
228 | */
229 | private static isValidContact(contact: EnhancedContact): boolean {
230 | return !!(
231 | contact.id &&
232 | contact.displayName &&
233 | contact.displayName.trim() !== '' &&
234 | !contact.displayName.includes('null')
235 | );
236 | }
237 | }
--------------------------------------------------------------------------------
/packages/wechat-db-manager/src/utils/PerformanceOptimizer.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 可取消的操作类
3 | */
4 | export class CancellableOperation {
5 | private cancelled = false;
6 | private timeoutId?: NodeJS.Timeout;
7 |
8 | constructor(private timeoutMs: number = 10000) {
9 | } // 默认10秒超时
10 |
11 | cancel() {
12 | this.cancelled = true;
13 | if (this.timeoutId) {
14 | clearTimeout(this.timeoutId);
15 | }
16 | }
17 |
18 | isCancelled() {
19 | return this.cancelled;
20 | }
21 |
22 | withTimeout(promise: Promise): Promise {
23 | return Promise.race([
24 | promise,
25 | new Promise((_, reject) => {
26 | this.timeoutId = setTimeout(() => {
27 | if (!this.cancelled) {
28 | reject(new Error('Operation timed out'));
29 | }
30 | }, this.timeoutMs);
31 | })
32 | ]);
33 | }
34 | }
35 |
36 | /**
37 | * 性能优化工具类
38 | * 处理大数据集的查询和处理
39 | */
40 | export class PerformanceOptimizer {
41 | private static readonly BATCH_SIZE = 100; // 每批处理的记录数
42 | private static readonly MAX_SAMPLE_SIZE = 1000; // 最大采样数量
43 | private static readonly QUERY_TIMEOUT = 10000; // 查询超时时间(毫秒)
44 |
45 | /**
46 | * 高效的表采样查询
47 | * 使用时间范围和LIMIT来避免全表扫描
48 | */
49 | static async efficientTableSample(
50 | dbManager: any,
51 | dbId: string,
52 | tableName: string,
53 | operation: CancellableOperation,
54 | sampleSize: number = PerformanceOptimizer.MAX_SAMPLE_SIZE
55 | ): Promise {
56 | if (operation.isCancelled()) {
57 | throw new Error('Operation was cancelled');
58 | }
59 |
60 | console.log(`⚡ 开始高效采样表 ${tableName},目标样本数: ${sampleSize}`);
61 |
62 | // 策略1:尝试按时间范围查询最新数据
63 | const timeBasedQueries = [
64 | // 最近一周的数据
65 | `SELECT * FROM ${tableName} WHERE timestamp > ${Date.now() - 7 * 24 * 60 * 60 * 1000} ORDER BY timestamp DESC LIMIT ${sampleSize}`,
66 | // 最近一个月的数据
67 | `SELECT * FROM ${tableName} WHERE timestamp > ${Date.now() - 30 * 24 * 60 * 60 * 1000} ORDER BY timestamp DESC LIMIT ${sampleSize}`,
68 | // 如果没有timestamp字段,尝试createtime
69 | `SELECT * FROM ${tableName} WHERE createtime > ${Date.now() - 7 * 24 * 60 * 60 * 1000} ORDER BY createtime DESC LIMIT ${sampleSize}`,
70 | ];
71 |
72 | // 策略2:如果时间查询失败,使用ROWID或其他方法
73 | const fallbackQueries = [
74 | `SELECT * FROM ${tableName} ORDER BY ROWID DESC LIMIT ${sampleSize}`,
75 | `SELECT * FROM ${tableName} LIMIT ${sampleSize}`,
76 | ];
77 |
78 | const allQueries = [...timeBasedQueries, ...fallbackQueries];
79 |
80 | for (const query of allQueries) {
81 | if (operation.isCancelled()) {
82 | throw new Error('Operation was cancelled');
83 | }
84 |
85 | try {
86 | console.log(`🔍 尝试查询: ${query.substring(0, 100)}...`);
87 | const result = await operation.withTimeout(
88 | dbManager.executeQuery(dbId, query)
89 | );
90 |
91 | if (result.rows.length > 0) {
92 | console.log(`✅ 查询成功,获得 ${result.rows.length} 条记录`);
93 | return result.rows;
94 | }
95 | } catch (error) {
96 | console.warn(`❌ 查询失败: ${error.message}`);
97 |
98 | }
99 | }
100 |
101 | console.warn(`⚠️ 所有查询策略都失败了,表 ${tableName} 可能为空或不可访问`);
102 | return [];
103 | }
104 |
105 | /**
106 | * 分批处理大数据集
107 | */
108 | static async processBatches(
109 | items: T[],
110 | batchProcessor: (batch: T[], batchIndex: number) => Promise,
111 | operation: CancellableOperation,
112 | onProgress?: (processed: number, total: number) => void
113 | ): Promise {
114 | const results: R[] = [];
115 | const batchSize = this.BATCH_SIZE;
116 | const totalBatches = Math.ceil(items.length / batchSize);
117 |
118 | for (let i = 0; i < totalBatches; i++) {
119 | if (operation.isCancelled()) {
120 | throw new Error('Operation was cancelled');
121 | }
122 |
123 | const batchStart = i * batchSize;
124 | const batchEnd = Math.min(batchStart + batchSize, items.length);
125 | const batch = items.slice(batchStart, batchEnd);
126 |
127 | try {
128 | const batchResults = await batchProcessor(batch, i);
129 | results.push(...batchResults);
130 |
131 | if (onProgress) {
132 | onProgress(batchEnd, items.length);
133 | }
134 | } catch (error) {
135 | console.warn(`❌ 批次 ${i + 1}/${totalBatches} 处理失败:`, error);
136 |
137 | }
138 | }
139 |
140 | return results;
141 | }
142 |
143 | /**
144 | * 内存友好的联系人活跃度检测
145 | */
146 | static async memoryFriendlyActivityDetection(
147 | dbManager: any,
148 | messageDbs: any[],
149 | operation: CancellableOperation,
150 | onProgress?: (current: number, total: number, message: string) => void
151 | ): Promise