Compare commits

...
7 Commits
12 changed files with 99 additions and 141 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"[typescript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
-21
View File
@@ -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.
+8 -59
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+64 -18
View File
@@ -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>