mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-18 03:57:02 +08:00
Compare commits
7
Commits
v1.0.0
...
fix-issue-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02baed9bb6 | ||
|
|
55b792b23c | ||
|
|
7c3fa728f7 | ||
|
|
755d11f71c | ||
|
|
eeded5fc46 | ||
|
|
ac14864655 | ||
|
|
80316b1db7 |
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 WechatExplorer Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -5,10 +5,11 @@ MAC系统 获取微信聊天记录 AI一键生成群聊总结
|
||||
|
||||
## 📸 预览
|
||||
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<img src="./public/WX20251215-155324.png" alt="预览图1" width="48%" />
|
||||
<img src="./public/WX20251215-155524.png" alt="预览图2" width="48%" />
|
||||
</div>
|
||||
<img src="./public/example1.png" alt="预览图1" />
|
||||
<img src="./public/example2.png" alt="预览图2" />
|
||||
<img src="./public/example3.png" alt="预览图3" />
|
||||
|
||||
## [点击这里下载](https://github.com/Wxw-Gu/WechatExplorer/releases/tag/v1.0.0)
|
||||
|
||||
## ✨ 功能特性
|
||||
|
||||
@@ -19,74 +20,22 @@ MAC系统 获取微信聊天记录 AI一键生成群聊总结
|
||||
- **数据导出**: 支持导出聊天记录为 CSV 文件(今日、昨日、近7天或全部)。
|
||||
- **安全隐私**: 所有数据仅在本地处理,AI 功能需自行配置 API Key。
|
||||
|
||||
## 🛠️ 技术栈
|
||||
|
||||
- **Electron**: 跨平台桌面应用框架
|
||||
- **React**: 用于构建用户界面
|
||||
- **TypeScript**: 提供类型安全
|
||||
- **Vite**: 极速构建工具
|
||||
- **Better-SQLite3**: 高性能 SQLite 数据库操作
|
||||
- **OpenAI SDK**: 用于调用 DeepSeek API
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 前置要求
|
||||
### 使用前置要求
|
||||
|
||||
- 微信>=4.0 无法使用
|
||||
- 微信<=4.0 需要获取自己微信本地数据库的密码, 获取方式参考: [Mac 导出微信聊天记录](https://blog.vcvit.me/2024/08/02/mac-export-wechat-chat-records/)
|
||||
- 如果无法获取本地数据库密码 则无法使用当前项目
|
||||
- Node.js (推荐 v16+)
|
||||
- pnpm (推荐) 或 npm
|
||||
- pnpm@7
|
||||
- 解密后的微信数据库文件 (`.db`) 和对应的密钥
|
||||
- [DeepSeek API Key](https://www.deepseek.com/) (用于 AI 总结功能)
|
||||
|
||||
### 安装
|
||||
|
||||
```bash
|
||||
# 克隆项目
|
||||
git clone https://github.com/your-username/WechatExplorer.git
|
||||
|
||||
# 进入目录
|
||||
cd WechatExplorer
|
||||
|
||||
# 安装依赖
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### 配置
|
||||
|
||||
复制 `.env.example` 文件为 `.env`,并填写相关配置(可选,也可以在应用内设置):
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
### 开发模式运行
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### 打包构建
|
||||
|
||||
```bash
|
||||
# Windows
|
||||
pnpm build:win
|
||||
|
||||
# macOS
|
||||
pnpm build:mac
|
||||
|
||||
# Linux
|
||||
pnpm build:linux
|
||||
```
|
||||
|
||||
## ⚠️ 免责声明
|
||||
|
||||
本项目仅供学习和研究使用。请勿用于非法用途。开发者不对使用本项目造成的任何后果负责。请遵守相关法律法规和微信使用协议。
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
[MIT](./LICENSE) License
|
||||
|
||||
## 🔗 参考
|
||||
|
||||
- [WechatMessageExplorer](https://github.com/svcvit/WechatMessageExplorer)
|
||||
|
||||
+8
-2
@@ -1,8 +1,14 @@
|
||||
{
|
||||
"name": "wechatexplorer",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.2",
|
||||
"description": "mac 版本获取微信聊天记录, AI群聊总结助手",
|
||||
"keywords": ["wechat", "chat", "mac微信", "微信聊天记录", "AI群聊总结助手"],
|
||||
"keywords": [
|
||||
"wechat",
|
||||
"chat",
|
||||
"mac微信",
|
||||
"微信聊天记录",
|
||||
"AI群聊总结助手"
|
||||
],
|
||||
"author": "Qingmao",
|
||||
"main": "./out/main/index.js",
|
||||
"scripts": {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 138 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 463 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 378 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 319 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 700 KiB |
+13
-15
@@ -6,6 +6,17 @@ import { WechatDb, Contact, WechatMessage } from './wechat-db'
|
||||
|
||||
let wechatDb: WechatDb | null = null
|
||||
|
||||
const MSG_TYPE_DICT: Record<number, string> = {
|
||||
1: '普通文本',
|
||||
3: '图片',
|
||||
34: '语音',
|
||||
43: '视频',
|
||||
47: '表情包',
|
||||
48: '位置',
|
||||
49: '分享消息',
|
||||
10000: '系统消息'
|
||||
}
|
||||
|
||||
function createWindow(): void {
|
||||
// 创建浏览器窗口
|
||||
const mainWindow = new BrowserWindow({
|
||||
@@ -117,17 +128,6 @@ app.whenReady().then(() => {
|
||||
const groupMembers = wechatDb.getAllGroupMembers()
|
||||
|
||||
return rawMessages.map((msg: WechatMessage) => {
|
||||
const typeDict: Record<number, string> = {
|
||||
1: '普通文本',
|
||||
3: '图片',
|
||||
34: '语音',
|
||||
43: '视频',
|
||||
47: '表情包',
|
||||
48: '位置',
|
||||
49: '分享消息',
|
||||
10000: '系统消息'
|
||||
}
|
||||
|
||||
const msgType = parseInt(msg.messageType)
|
||||
const createTime = parseInt(msg.msgCreateTime)
|
||||
const date = new Date(createTime * 1000)
|
||||
@@ -161,14 +161,12 @@ app.whenReady().then(() => {
|
||||
|
||||
return {
|
||||
id: msg.mesLocalID || Math.random().toString(),
|
||||
from: msg.mesDes === '1' ? 'user' : 'assistant', // 1 通常是接收到的,0 是发送的?需要验证。Swift 说:[1: "user", 0: "assistant"]
|
||||
type: typeDict[msgType] || msg.messageType,
|
||||
from: msg.mesDes === 1 ? 'user' : 'assistant', // 1 通常是接收到的,0 是发送的?需要验证。Swift 说:[1: "user", 0: "assistant"]
|
||||
type: MSG_TYPE_DICT[msgType] || msg.messageType,
|
||||
datetime: date.toLocaleString('zh-CN', { hour12: false }),
|
||||
content: content,
|
||||
isSender: msg.mesDes === '0',
|
||||
img: img,
|
||||
name: name
|
||||
// 实际上让我们坚持我们观察到的:1=用户(其他人), 0=助手(我)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
+5
-25
@@ -13,7 +13,7 @@ export interface UserContact {
|
||||
|
||||
export interface WechatMessage {
|
||||
mesLocalID: string
|
||||
mesDes: string
|
||||
mesDes: number
|
||||
messageType: string
|
||||
msgCreateTime: string
|
||||
msgContent: string
|
||||
@@ -62,7 +62,6 @@ export class WechatDb {
|
||||
}
|
||||
|
||||
private connectDb(dbPath: string): Database | null {
|
||||
// 1. 路径预检查
|
||||
const targetPath = path.resolve(dbPath)
|
||||
if (!fs.existsSync(targetPath)) {
|
||||
console.error(`❌ 错误:数据库文件不存在于路径:${targetPath}`)
|
||||
@@ -70,45 +69,26 @@ export class WechatDb {
|
||||
}
|
||||
|
||||
try {
|
||||
// console.log(`🔌 正在尝试连接至:${path.basename(targetPath)}`)
|
||||
// console.log('路径是', targetPath)
|
||||
// 2. 初始化数据库句柄
|
||||
// 关键:使用 better-sqlite3-multiple-ciphers 的构造函数
|
||||
// verbose 选项有助于调试 Pragma 的执行情况
|
||||
const db = new DatabaseConstructor(targetPath, {
|
||||
// verbose: console.log
|
||||
})
|
||||
|
||||
// 此时 db 实例已创建,但因为未提供密钥,任何数据读取都会失败。
|
||||
// 必须立即配置加密参数。
|
||||
|
||||
// 3. 配置加密方案 (Cipher Scheme)
|
||||
// 必须显式切换到 'sqlcipher',因为该库默认可能使用 'chacha20'
|
||||
// 配置加密方案 (Cipher Scheme)
|
||||
db.pragma("cipher='sqlcipher'")
|
||||
|
||||
// 4. 配置页大小 (Page Size)
|
||||
// macOS 版 WeChat 使用 1024 字节。这必须在设置密钥前(或紧随其后)生效。
|
||||
db.pragma('cipher_page_size = 1024')
|
||||
|
||||
// 5. 配置兼容性与算法细节
|
||||
// 显式指定 SQLCipher 3 的行为
|
||||
db.pragma('legacy=3')
|
||||
db.pragma('kdf_iter = 64000')
|
||||
db.pragma('cipher_hmac_algorithm = HMAC_SHA1')
|
||||
db.pragma('cipher_kdf_algorithm = PBKDF2_HMAC_SHA1')
|
||||
|
||||
// 6. 应用密钥
|
||||
// 使用正确的 hex 语法: key = "x'HEXSTRING'"
|
||||
const processedKey = this.processRawKey(this.rawKey)
|
||||
// console.log(`🔑 应用密钥 (前6位): ${processedKey.substring(0, 6)}...`)
|
||||
|
||||
db.pragma(`key = "x'${processedKey}'"`)
|
||||
|
||||
// 7. 验证连接
|
||||
// 如果参数错误,这里将抛出 "file is not a database" 或 "HMAC check failed"
|
||||
// 验证连接
|
||||
db.prepare('SELECT count(*) as count FROM sqlite_master').get()
|
||||
|
||||
// console.log(`✅ 连接成功!当前数据库 ${path.basename(targetPath)} 包含 ${row.count} 张表。`)
|
||||
return db
|
||||
} catch (error) {
|
||||
console.error(`❌ 连接失败:`, error)
|
||||
@@ -241,7 +221,7 @@ export class WechatDb {
|
||||
public getGroupMember(wxid: string): GroupMemberInfo | null {
|
||||
if (!this.correctUserId) return null
|
||||
|
||||
// 1. 检查缓存
|
||||
// 检查缓存
|
||||
if (this.groupMemberCache.has(wxid)) {
|
||||
return this.groupMemberCache.get(wxid) || null
|
||||
}
|
||||
@@ -261,7 +241,7 @@ export class WechatDb {
|
||||
|
||||
db.close()
|
||||
|
||||
// 2. 写入缓存
|
||||
// 写入缓存
|
||||
this.groupMemberCache.set(wxid, result || null)
|
||||
|
||||
return result || null
|
||||
|
||||
@@ -28,6 +28,7 @@ const systemPrompt = `你是一个中文的群聊总结的助手,你可以为
|
||||
2. 使用中文冒号
|
||||
3. 无需大标题
|
||||
4. 开始给出本群讨论风格的整体评价,例如活跃、太水、太黄、太暴力、话题不集中、无聊诸如此类
|
||||
5. 每个话题详细写出参与者
|
||||
|
||||
最后总结下今日最活跃的前五个发言者`
|
||||
|
||||
@@ -38,9 +39,11 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
||||
onRefresh,
|
||||
onRefreshData
|
||||
}) => {
|
||||
const isGroupChat = contact?.type === 'group' || contact?.m_nsUsrName?.endsWith('@chatroom')
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||
const imageContainerRef = useRef<HTMLDivElement>(null)
|
||||
const [generatedImage, setGeneratedImage] = useState<string | null>(null)
|
||||
const [showAvatar, setShowAvatar] = useState(false)
|
||||
|
||||
const [colWidths, setColWidths] = useState([150, 100, 180, 400])
|
||||
const [resizingColIndex, setResizingColIndex] = useState<number | null>(null)
|
||||
@@ -113,11 +116,16 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
||||
// 昨天
|
||||
const startOfYesterday = startOfDay - 86400000
|
||||
return parsed >= startOfYesterday && parsed < startOfDay
|
||||
} else {
|
||||
} else if (days === 7) {
|
||||
// 过去 7 天
|
||||
const startOf7DaysAgo = startOfDay - 7 * 86400000
|
||||
return parsed >= startOf7DaysAgo
|
||||
} else if (days === 30) {
|
||||
// 过去 30 天
|
||||
const startOf30DaysAgo = startOfDay - 30 * 86400000
|
||||
return parsed >= startOf30DaysAgo
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -125,7 +133,15 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
||||
const csvContent = [
|
||||
headers.join(','),
|
||||
...filtered.map((m) => {
|
||||
const content = m.content.replace(/"/g, '""').replace(/\n/g, ' ')
|
||||
let prefix = ''
|
||||
if (isGroupChat) {
|
||||
prefix = m.name ? `${m.name}: ` : ''
|
||||
} else {
|
||||
const name = m.from === 'user' ? contact?.m_nsNickName || '未知' : '我'
|
||||
prefix = `${name}: `
|
||||
}
|
||||
const fullContent = `${prefix}${m.content}`
|
||||
const content = fullContent.replace(/"/g, '""').replace(/\n/g, ' ')
|
||||
return `"${m.from}","${m.type}","${m.datetime}","${content}"`
|
||||
})
|
||||
].join('\n')
|
||||
@@ -148,16 +164,12 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
||||
alert('当前没有消息可供总结')
|
||||
return
|
||||
}
|
||||
const filteredMessages = messages.filter((msg) => {
|
||||
delete msg.img
|
||||
// @ts-ignore 暂时去除
|
||||
delete msg.from
|
||||
// @ts-ignore 暂时去除
|
||||
delete msg.id
|
||||
// @ts-ignore 暂时去除
|
||||
delete msg.isSender
|
||||
return !'分享消息,图片,表情包,视频'.split(',').includes(msg.type)
|
||||
})
|
||||
const filteredMessages = messages
|
||||
.filter((msg) => !'分享消息,图片,表情包,视频'.split(',').includes(msg.type))
|
||||
.map((msg) => {
|
||||
const { img, id, isSender, ...rest } = msg
|
||||
return rest
|
||||
})
|
||||
const recentMessages = filteredMessages
|
||||
.map((msg) => {
|
||||
return `${msg.datetime} ${msg.from}: ${msg.content}`
|
||||
@@ -292,14 +304,34 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
||||
{msg.datetime}
|
||||
</td>
|
||||
<td style={{ wordBreak: 'break-all', display: 'flex', alignItems: 'center' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', marginRight: 12 }}>
|
||||
<span>{msg.name}</span>
|
||||
{msg?.img && (
|
||||
<img style={{ width: '50px', height: '50px' }} src={msg?.img}></img>
|
||||
{showAvatar && msg?.img && (
|
||||
<img style={{ width: '40px', height: '40px' }} src={msg?.img}></img>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
display: !showAvatar ? 'flex' : 'block'
|
||||
}}
|
||||
>
|
||||
{(msg.name || contact.m_nsNickName) && (
|
||||
<div style={{ display: 'flex', fontSize: 18 }}>
|
||||
{isGroupChat ? msg.name : msg.from === 'user' ? contact.m_nsNickName : '我'}
|
||||
<span>{isGroupChat ? (msg.name ? ':' : '') : ':'} </span>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
fontSize: 18,
|
||||
background: '#fff',
|
||||
margin: 4,
|
||||
padding: 4,
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
>
|
||||
{msg.content}
|
||||
</div>
|
||||
</div>
|
||||
{/* <span style={{ marginRight: '24px' }}>:</span> */}
|
||||
<div>{msg.content}</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -325,6 +357,17 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="chat-toolbar">
|
||||
<label
|
||||
style={{ marginRight: '10px', display: 'flex', alignItems: 'center', cursor: 'pointer' }}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showAvatar}
|
||||
onChange={(e) => setShowAvatar(e.target.checked)}
|
||||
style={{ marginRight: '5px' }}
|
||||
/>
|
||||
显示头像
|
||||
</label>
|
||||
<button className="toolbar-btn" onClick={onRefresh}>
|
||||
🔄 刷新聊天记录
|
||||
</button>
|
||||
@@ -343,6 +386,9 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
||||
<button className="toolbar-btn" onClick={() => handleExport(7)}>
|
||||
📅 导出近7天
|
||||
</button>
|
||||
<button className="toolbar-btn" onClick={() => handleExport(30)}>
|
||||
📅 导出近30天
|
||||
</button>
|
||||
<button className="toolbar-btn" onClick={() => setShowSettingsModal(true)}>
|
||||
🤖 AI总结群聊
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user