mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-18 03:57:02 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a740a50e79 | ||
|
|
ffe797b3e4 | ||
|
|
9cd6061c0f | ||
|
|
67d86f7834 | ||
|
|
a6138fd62b | ||
|
|
d1763757be | ||
|
|
a4dfa542d1 | ||
|
|
9fb1d14c41 | ||
|
|
eb60ce4984 | ||
|
|
061086f0ad | ||
|
|
d1cb6277f9 | ||
|
|
02baed9bb6 | ||
|
|
55b792b23c | ||
|
|
7c3fa728f7 | ||
|
|
755d11f71c | ||
|
|
eeded5fc46 | ||
|
|
ac14864655 | ||
|
|
80316b1db7 |
+3
-1
@@ -1,8 +1,10 @@
|
|||||||
# WeChat Database Key (Optional, can be entered in UI)
|
# WeChat Database Key (Optional, can be entered in UI)
|
||||||
VITE_DB_KEY=
|
VITE_DB_KEY=
|
||||||
|
|
||||||
# DeepSeek API Key (Optional, can be entered in UI)
|
# AI API Configuration (Optional, can be entered in UI)
|
||||||
VITE_DEEPSEEK_API_KEY=
|
VITE_DEEPSEEK_API_KEY=
|
||||||
|
VITE_AI_BASE_URL=https://api.deepseek.com
|
||||||
|
VITE_AI_MODEL=deepseek-chat
|
||||||
|
|
||||||
# Message types to filter out (comma separated)
|
# Message types to filter out (comma separated)
|
||||||
VITE_FILTER_MSG_TYPES=分享消息,图片,表情包,视频
|
VITE_FILTER_MSG_TYPES=分享消息,图片,表情包,视频
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
name: Build & Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: macos-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
platform: [x64, arm64]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: '7'
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: pnpm build
|
||||||
|
|
||||||
|
- name: Build DMG
|
||||||
|
run: pnpm run build:mac:${{ matrix.platform }}
|
||||||
|
|
||||||
|
- name: Upload DMG
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: dmg-${{ matrix.platform }}
|
||||||
|
path: out/mac-*/WechatExplorer-*.dmg
|
||||||
|
|
||||||
|
release:
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Download artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
pattern: dmg-*
|
||||||
|
path: artifacts
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: artifacts/**/*
|
||||||
|
generate_release_notes: true
|
||||||
@@ -5,3 +5,4 @@ out
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.eslintcache
|
.eslintcache
|
||||||
*.log*
|
*.log*
|
||||||
|
.omc
|
||||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"[typescript]": {
|
"[typescript]": {
|
||||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
},
|
},
|
||||||
"[javascript]": {
|
"[javascript]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"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,88 +5,46 @@ MAC系统 获取微信聊天记录 AI一键生成群聊总结
|
|||||||
|
|
||||||
## 📸 预览
|
## 📸 预览
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-between;">
|
<img src="./public/example1.png" alt="预览图1" />
|
||||||
<img src="./public/WX20251215-155324.png" alt="预览图1" width="48%" />
|
<img src="./public/example2.png" alt="预览图2" />
|
||||||
<img src="./public/WX20251215-155524.png" alt="预览图2" width="48%" />
|
<img src="./public/example3.png" alt="预览图3" />
|
||||||
</div>
|
|
||||||
|
## [点击这里下载](https://github.com/Wxw-Gu/WechatExplorer/releases)
|
||||||
|
|
||||||
|
## 📦 安装说明
|
||||||
|
|
||||||
|
1. 下载下方的 `xxx.dmg` 文件。
|
||||||
|
2. 打开 DMG 并将应用拖动到 **Applications** (应用程序) 文件夹。
|
||||||
|
3. 如果遇到“无法打开,因为开发者无法验证”的提示,请前往:
|
||||||
|
`系统设置 -> 隐私与安全性 -> 仍要打开`。
|
||||||
|
|
||||||
## ✨ 功能特性
|
## ✨ 功能特性
|
||||||
|
|
||||||
- **聊天记录查看**: 浏览微信好友和群聊的聊天记录。
|
- **聊天记录查看**: 浏览微信好友和群聊的聊天记录。
|
||||||
- **全局搜索**: 快速搜索聊天内容。
|
- **全局搜索**: 快速搜索聊天内容。
|
||||||
- **AI 智能总结**: 集成 DeepSeek AI,一键总结群聊精华内容,生成话题报告。
|
- **AI 智能总结**: 支持多模型服务配置(DeepSeek/GPT-4o/Claude/Moonshot),一键总结群聊精华内容,生成话题报告。
|
||||||
- **图片生成**: 将 AI 总结的内容生成精美图片,方便分享。
|
- **图片生成**: 将 AI 总结的内容生成精美图片,方便分享。
|
||||||
- **数据导出**: 支持导出聊天记录为 CSV 文件(今日、昨日、近7天或全部)。
|
- **数据导出**: 支持导出聊天记录为 CSV 文件(今日、昨日、近7天或全部)。
|
||||||
- **安全隐私**: 所有数据仅在本地处理,AI 功能需自行配置 API Key。
|
- **安全隐私**: 所有数据仅在本地处理,AI 功能需自行配置 API Key。
|
||||||
|
|
||||||
## 🛠️ 技术栈
|
|
||||||
|
|
||||||
- **Electron**: 跨平台桌面应用框架
|
|
||||||
- **React**: 用于构建用户界面
|
|
||||||
- **TypeScript**: 提供类型安全
|
|
||||||
- **Vite**: 极速构建工具
|
|
||||||
- **Better-SQLite3**: 高性能 SQLite 数据库操作
|
|
||||||
- **OpenAI SDK**: 用于调用 DeepSeek API
|
|
||||||
|
|
||||||
## 🚀 快速开始
|
## 🚀 快速开始
|
||||||
|
|
||||||
### 前置要求
|
### 使用前置要求
|
||||||
|
|
||||||
- 微信<=4.0 需要获取自己微信本地数据库的密码, 获取方式参考: [Mac 导出微信聊天记录](https://blog.vcvit.me/2024/08/02/mac-export-wechat-chat-records/)
|
- **微信版本**: 推荐使用微信 **4.0 以下**版本(4.0以上版本不支持数据库解密)
|
||||||
- 如果无法获取本地数据库密码 则无法使用当前项目
|
- 微信 <= 4.0: 可正常使用,获取数据库密码方式参考:[Mac 导出微信聊天记录](https://blog.vcvit.me/2024/08/02/mac-export-wechat-chat-records/)
|
||||||
|
- 微信 >= 4.0: 如需使用,推荐使用 [WeFlow](https://github.com/hicccc77/WeFlow)
|
||||||
|
- 如无法获取本地数据库密码,则无法使用当前项目
|
||||||
- Node.js (推荐 v16+)
|
- Node.js (推荐 v16+)
|
||||||
- pnpm (推荐) 或 npm
|
- pnpm@7
|
||||||
- 解密后的微信数据库文件 (`.db`) 和对应的密钥
|
- 解密后的微信数据库文件 (`.db`) 和对应的密钥
|
||||||
- [DeepSeek API Key](https://www.deepseek.com/) (用于 AI 总结功能)
|
- AI API Key(支持 OpenAI 兼容 API,可选 DeepSeek/GPT/Claude/Moonshot 等)
|
||||||
|
|
||||||
### 安装
|
|
||||||
|
|
||||||
```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)
|
- [WechatMessageExplorer](https://github.com/svcvit/WechatMessageExplorer)
|
||||||
|
- [WeFlow](https://github.com/hicccc77/WeFlow)
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 获取当前版本号(从 package.json 读取)
|
||||||
|
VERSION=$(jq -r '.version' package.json)
|
||||||
|
|
||||||
|
# 格式化为新的 tag(在版本号前加上 "v")
|
||||||
|
TAG="v${VERSION}"
|
||||||
|
|
||||||
|
# 输出当前生成的 tag
|
||||||
|
echo "生成的 tag: $TAG"
|
||||||
|
|
||||||
|
# 创建并推送 tag 到远程
|
||||||
|
git tag $TAG
|
||||||
|
git push origin $TAG
|
||||||
|
|
||||||
|
echo "Tag $TAG 已经推送到远程仓库!"
|
||||||
+20
-3
@@ -1,8 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "wechatexplorer",
|
"name": "wechatexplorer",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"description": "mac 版本获取微信聊天记录, AI群聊总结助手",
|
"description": "mac 版本获取微信聊天记录, AI群聊总结助手",
|
||||||
"keywords": ["wechat", "chat", "mac微信", "微信聊天记录", "AI群聊总结助手"],
|
"keywords": [
|
||||||
|
"wechat",
|
||||||
|
"chat",
|
||||||
|
"mac微信",
|
||||||
|
"微信聊天记录",
|
||||||
|
"AI群聊总结助手"
|
||||||
|
],
|
||||||
"author": "Qingmao",
|
"author": "Qingmao",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -17,7 +23,9 @@
|
|||||||
"postinstall": "electron-builder install-app-deps",
|
"postinstall": "electron-builder install-app-deps",
|
||||||
"build:unpack": "npm run build && electron-builder --dir",
|
"build:unpack": "npm run build && electron-builder --dir",
|
||||||
"build:win": "npm run build && electron-builder --win",
|
"build:win": "npm run build && electron-builder --win",
|
||||||
"build:mac": "electron-vite build && electron-builder --mac",
|
"build:mac:x64": "electron-vite build && electron-builder --mac --x64",
|
||||||
|
"build:mac:arm64": "electron-vite build && electron-builder --mac --arm64",
|
||||||
|
"release:mac": "electron-vite build && electron-builder --mac --x64 --arm64 --publish always",
|
||||||
"build:linux": "electron-vite build && electron-builder --linux"
|
"build:linux": "electron-vite build && electron-builder --linux"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -55,5 +63,14 @@
|
|||||||
"electron",
|
"electron",
|
||||||
"esbuild"
|
"esbuild"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"artifactName": "${productName}-${version}-${os}-${arch}.${ext}",
|
||||||
|
"mac": {
|
||||||
|
"target": [
|
||||||
|
"dmg",
|
||||||
|
"zip"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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 |
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_DEEPSEEK_API_KEY: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
+21
-21
@@ -6,6 +6,17 @@ import { WechatDb, Contact, WechatMessage } from './wechat-db'
|
|||||||
|
|
||||||
let wechatDb: WechatDb | null = null
|
let wechatDb: WechatDb | null = null
|
||||||
|
|
||||||
|
const MSG_TYPE_DICT: Record<number, string> = {
|
||||||
|
1: '普通文本',
|
||||||
|
3: '图片',
|
||||||
|
34: '语音',
|
||||||
|
43: '视频',
|
||||||
|
47: '表情包',
|
||||||
|
48: '位置',
|
||||||
|
49: '分享消息',
|
||||||
|
10000: '系统消息'
|
||||||
|
}
|
||||||
|
|
||||||
function createWindow(): void {
|
function createWindow(): void {
|
||||||
// 创建浏览器窗口
|
// 创建浏览器窗口
|
||||||
const mainWindow = new BrowserWindow({
|
const mainWindow = new BrowserWindow({
|
||||||
@@ -117,17 +128,6 @@ app.whenReady().then(() => {
|
|||||||
const groupMembers = wechatDb.getAllGroupMembers()
|
const groupMembers = wechatDb.getAllGroupMembers()
|
||||||
|
|
||||||
return rawMessages.map((msg: WechatMessage) => {
|
return rawMessages.map((msg: WechatMessage) => {
|
||||||
const typeDict: Record<number, string> = {
|
|
||||||
1: '普通文本',
|
|
||||||
3: '图片',
|
|
||||||
34: '语音',
|
|
||||||
43: '视频',
|
|
||||||
47: '表情包',
|
|
||||||
48: '位置',
|
|
||||||
49: '分享消息',
|
|
||||||
10000: '系统消息'
|
|
||||||
}
|
|
||||||
|
|
||||||
const msgType = parseInt(msg.messageType)
|
const msgType = parseInt(msg.messageType)
|
||||||
const createTime = parseInt(msg.msgCreateTime)
|
const createTime = parseInt(msg.msgCreateTime)
|
||||||
const date = new Date(createTime * 1000)
|
const date = new Date(createTime * 1000)
|
||||||
@@ -161,14 +161,12 @@ app.whenReady().then(() => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: msg.mesLocalID || Math.random().toString(),
|
id: msg.mesLocalID || Math.random().toString(),
|
||||||
from: msg.mesDes === '1' ? 'user' : 'assistant', // 1 通常是接收到的,0 是发送的?需要验证。Swift 说:[1: "user", 0: "assistant"]
|
from: msg.mesDes === 1 ? 'user' : 'assistant', // 1 通常是接收到的,0 是发送的?需要验证。Swift 说:[1: "user", 0: "assistant"]
|
||||||
type: typeDict[msgType] || msg.messageType,
|
type: MSG_TYPE_DICT[msgType] || msg.messageType,
|
||||||
datetime: date.toLocaleString('zh-CN', { hour12: false }),
|
datetime: date.toLocaleString('zh-CN', { hour12: false }),
|
||||||
content: content,
|
content: content,
|
||||||
isSender: msg.mesDes === '0',
|
|
||||||
img: img,
|
img: img,
|
||||||
name: name
|
name: name
|
||||||
// 实际上让我们坚持我们观察到的:1=用户(其他人), 0=助手(我)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -183,22 +181,24 @@ app.whenReady().then(() => {
|
|||||||
async (
|
async (
|
||||||
_,
|
_,
|
||||||
messages: { role: string; content: string }[],
|
messages: { role: string; content: string }[],
|
||||||
options?: { apiKey?: string; model?: string }
|
options?: { apiKey?: string; model?: string; baseURL?: string }
|
||||||
) => {
|
) => {
|
||||||
// @ts-ignore: vite env
|
// @ts-ignore: vite env
|
||||||
const apiKey = options?.apiKey || import.meta.env.VITE_DEEPSEEK_API_KEY
|
const apiKey = options?.apiKey || import.meta.env.VITE_DEEPSEEK_API_KEY
|
||||||
const model = options?.model || 'deepseek-chat'
|
const model = options?.model || import.meta.env.VITE_AI_MODEL || 'deepseek-chat'
|
||||||
|
const baseURL =
|
||||||
|
options?.baseURL || import.meta.env.VITE_AI_BASE_URL || 'https://api.deepseek.com'
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
return { success: false, error: '未配置 DeepSeek API Key' }
|
return { success: false, error: '未配置 API Key' }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 动态导入以避免如果未安装或初始类型缺失的问题
|
// 动态导入以避免如果未安装或初始类型缺失的问题
|
||||||
const { OpenAI } = await import('openai')
|
const { OpenAI } = await import('openai')
|
||||||
|
|
||||||
const openai = new OpenAI({
|
const openai = new OpenAI({
|
||||||
baseURL: 'https://api.deepseek.com',
|
baseURL,
|
||||||
apiKey: apiKey
|
apiKey
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
const completion = await openai.chat.completions.create({
|
const completion = await openai.chat.completions.create({
|
||||||
@@ -208,7 +208,7 @@ app.whenReady().then(() => {
|
|||||||
})
|
})
|
||||||
return { success: true, data: completion.choices[0].message.content }
|
return { success: true, data: completion.choices[0].message.content }
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.error('DeepSeek API Error:', error)
|
console.error('AI API Error:', error)
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||||
return { success: false, error: errorMessage }
|
return { success: false, error: errorMessage }
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-25
@@ -13,7 +13,7 @@ export interface UserContact {
|
|||||||
|
|
||||||
export interface WechatMessage {
|
export interface WechatMessage {
|
||||||
mesLocalID: string
|
mesLocalID: string
|
||||||
mesDes: string
|
mesDes: number
|
||||||
messageType: string
|
messageType: string
|
||||||
msgCreateTime: string
|
msgCreateTime: string
|
||||||
msgContent: string
|
msgContent: string
|
||||||
@@ -62,7 +62,6 @@ export class WechatDb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private connectDb(dbPath: string): Database | null {
|
private connectDb(dbPath: string): Database | null {
|
||||||
// 1. 路径预检查
|
|
||||||
const targetPath = path.resolve(dbPath)
|
const targetPath = path.resolve(dbPath)
|
||||||
if (!fs.existsSync(targetPath)) {
|
if (!fs.existsSync(targetPath)) {
|
||||||
console.error(`❌ 错误:数据库文件不存在于路径:${targetPath}`)
|
console.error(`❌ 错误:数据库文件不存在于路径:${targetPath}`)
|
||||||
@@ -70,45 +69,26 @@ export class WechatDb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// console.log(`🔌 正在尝试连接至:${path.basename(targetPath)}`)
|
|
||||||
// console.log('路径是', targetPath)
|
|
||||||
// 2. 初始化数据库句柄
|
|
||||||
// 关键:使用 better-sqlite3-multiple-ciphers 的构造函数
|
|
||||||
// verbose 选项有助于调试 Pragma 的执行情况
|
|
||||||
const db = new DatabaseConstructor(targetPath, {
|
const db = new DatabaseConstructor(targetPath, {
|
||||||
// verbose: console.log
|
// verbose: console.log
|
||||||
})
|
})
|
||||||
|
|
||||||
// 此时 db 实例已创建,但因为未提供密钥,任何数据读取都会失败。
|
// 配置加密方案 (Cipher Scheme)
|
||||||
// 必须立即配置加密参数。
|
|
||||||
|
|
||||||
// 3. 配置加密方案 (Cipher Scheme)
|
|
||||||
// 必须显式切换到 'sqlcipher',因为该库默认可能使用 'chacha20'
|
|
||||||
db.pragma("cipher='sqlcipher'")
|
db.pragma("cipher='sqlcipher'")
|
||||||
|
|
||||||
// 4. 配置页大小 (Page Size)
|
|
||||||
// macOS 版 WeChat 使用 1024 字节。这必须在设置密钥前(或紧随其后)生效。
|
|
||||||
db.pragma('cipher_page_size = 1024')
|
db.pragma('cipher_page_size = 1024')
|
||||||
|
|
||||||
// 5. 配置兼容性与算法细节
|
|
||||||
// 显式指定 SQLCipher 3 的行为
|
|
||||||
db.pragma('legacy=3')
|
db.pragma('legacy=3')
|
||||||
db.pragma('kdf_iter = 64000')
|
db.pragma('kdf_iter = 64000')
|
||||||
db.pragma('cipher_hmac_algorithm = HMAC_SHA1')
|
db.pragma('cipher_hmac_algorithm = HMAC_SHA1')
|
||||||
db.pragma('cipher_kdf_algorithm = PBKDF2_HMAC_SHA1')
|
db.pragma('cipher_kdf_algorithm = PBKDF2_HMAC_SHA1')
|
||||||
|
|
||||||
// 6. 应用密钥
|
|
||||||
// 使用正确的 hex 语法: key = "x'HEXSTRING'"
|
|
||||||
const processedKey = this.processRawKey(this.rawKey)
|
const processedKey = this.processRawKey(this.rawKey)
|
||||||
// console.log(`🔑 应用密钥 (前6位): ${processedKey.substring(0, 6)}...`)
|
// console.log(`🔑 应用密钥 (前6位): ${processedKey.substring(0, 6)}...`)
|
||||||
|
|
||||||
db.pragma(`key = "x'${processedKey}'"`)
|
db.pragma(`key = "x'${processedKey}'"`)
|
||||||
|
|
||||||
// 7. 验证连接
|
// 验证连接
|
||||||
// 如果参数错误,这里将抛出 "file is not a database" 或 "HMAC check failed"
|
|
||||||
db.prepare('SELECT count(*) as count FROM sqlite_master').get()
|
db.prepare('SELECT count(*) as count FROM sqlite_master').get()
|
||||||
|
|
||||||
// console.log(`✅ 连接成功!当前数据库 ${path.basename(targetPath)} 包含 ${row.count} 张表。`)
|
|
||||||
return db
|
return db
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ 连接失败:`, error)
|
console.error(`❌ 连接失败:`, error)
|
||||||
@@ -241,7 +221,7 @@ export class WechatDb {
|
|||||||
public getGroupMember(wxid: string): GroupMemberInfo | null {
|
public getGroupMember(wxid: string): GroupMemberInfo | null {
|
||||||
if (!this.correctUserId) return null
|
if (!this.correctUserId) return null
|
||||||
|
|
||||||
// 1. 检查缓存
|
// 检查缓存
|
||||||
if (this.groupMemberCache.has(wxid)) {
|
if (this.groupMemberCache.has(wxid)) {
|
||||||
return this.groupMemberCache.get(wxid) || null
|
return this.groupMemberCache.get(wxid) || null
|
||||||
}
|
}
|
||||||
@@ -261,7 +241,7 @@ export class WechatDb {
|
|||||||
|
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
// 2. 写入缓存
|
// 写入缓存
|
||||||
this.groupMemberCache.set(wxid, result || null)
|
this.groupMemberCache.set(wxid, result || null)
|
||||||
|
|
||||||
return result || null
|
return result || null
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const systemPrompt = `你是一个中文的群聊总结的助手,你可以为
|
|||||||
2. 使用中文冒号
|
2. 使用中文冒号
|
||||||
3. 无需大标题
|
3. 无需大标题
|
||||||
4. 开始给出本群讨论风格的整体评价,例如活跃、太水、太黄、太暴力、话题不集中、无聊诸如此类
|
4. 开始给出本群讨论风格的整体评价,例如活跃、太水、太黄、太暴力、话题不集中、无聊诸如此类
|
||||||
|
5. 每个话题详细写出参与者
|
||||||
|
|
||||||
最后总结下今日最活跃的前五个发言者`
|
最后总结下今日最活跃的前五个发言者`
|
||||||
|
|
||||||
@@ -38,9 +39,11 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
|||||||
onRefresh,
|
onRefresh,
|
||||||
onRefreshData
|
onRefreshData
|
||||||
}) => {
|
}) => {
|
||||||
|
const isGroupChat = contact?.type === 'group' || contact?.m_nsUsrName?.endsWith('@chatroom')
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||||
const imageContainerRef = useRef<HTMLDivElement>(null)
|
const imageContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const [generatedImage, setGeneratedImage] = useState<string | null>(null)
|
const [generatedImage, setGeneratedImage] = useState<string | null>(null)
|
||||||
|
const [showAvatar, setShowAvatar] = useState(false)
|
||||||
|
|
||||||
const [colWidths, setColWidths] = useState([150, 100, 180, 400])
|
const [colWidths, setColWidths] = useState([150, 100, 180, 400])
|
||||||
const [resizingColIndex, setResizingColIndex] = useState<number | null>(null)
|
const [resizingColIndex, setResizingColIndex] = useState<number | null>(null)
|
||||||
@@ -49,11 +52,16 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
|||||||
|
|
||||||
// AI Settings
|
// AI Settings
|
||||||
const [showSettingsModal, setShowSettingsModal] = useState(false)
|
const [showSettingsModal, setShowSettingsModal] = useState(false)
|
||||||
const [apiKey, setApiKey] = useState(() => localStorage.getItem('deepseek_api_key') || '')
|
const [apiKey, setApiKey] = useState(() => localStorage.getItem('ai_api_key') || '')
|
||||||
const [model, setModel] = useState('deepseek-chat')
|
const [baseURL, setBaseURL] = useState(
|
||||||
|
() => localStorage.getItem('ai_base_url') || 'https://api.deepseek.com'
|
||||||
|
)
|
||||||
|
const [model, setModel] = useState(() => localStorage.getItem('ai_model') || 'deepseek-chat')
|
||||||
|
|
||||||
const handleSaveSettings = (): void => {
|
const handleSaveSettings = (): void => {
|
||||||
localStorage.setItem('deepseek_api_key', apiKey)
|
localStorage.setItem('ai_api_key', apiKey)
|
||||||
|
localStorage.setItem('ai_base_url', baseURL)
|
||||||
|
localStorage.setItem('ai_model', model)
|
||||||
setShowSettingsModal(false)
|
setShowSettingsModal(false)
|
||||||
AIChat()
|
AIChat()
|
||||||
}
|
}
|
||||||
@@ -113,11 +121,16 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
|||||||
// 昨天
|
// 昨天
|
||||||
const startOfYesterday = startOfDay - 86400000
|
const startOfYesterday = startOfDay - 86400000
|
||||||
return parsed >= startOfYesterday && parsed < startOfDay
|
return parsed >= startOfYesterday && parsed < startOfDay
|
||||||
} else {
|
} else if (days === 7) {
|
||||||
// 过去 7 天
|
// 过去 7 天
|
||||||
const startOf7DaysAgo = startOfDay - 7 * 86400000
|
const startOf7DaysAgo = startOfDay - 7 * 86400000
|
||||||
return parsed >= startOf7DaysAgo
|
return parsed >= startOf7DaysAgo
|
||||||
|
} else if (days === 30) {
|
||||||
|
// 过去 30 天
|
||||||
|
const startOf30DaysAgo = startOfDay - 30 * 86400000
|
||||||
|
return parsed >= startOf30DaysAgo
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +138,15 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
|||||||
const csvContent = [
|
const csvContent = [
|
||||||
headers.join(','),
|
headers.join(','),
|
||||||
...filtered.map((m) => {
|
...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}"`
|
return `"${m.from}","${m.type}","${m.datetime}","${content}"`
|
||||||
})
|
})
|
||||||
].join('\n')
|
].join('\n')
|
||||||
@@ -148,16 +169,12 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
|||||||
alert('当前没有消息可供总结')
|
alert('当前没有消息可供总结')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const filteredMessages = messages.filter((msg) => {
|
const filteredMessages = messages
|
||||||
delete msg.img
|
.filter((msg) => !'分享消息,图片,表情包,视频'.split(',').includes(msg.type))
|
||||||
// @ts-ignore 暂时去除
|
.map((msg) => {
|
||||||
delete msg.from
|
const { img, id, isSender, ...rest } = msg
|
||||||
// @ts-ignore 暂时去除
|
return rest
|
||||||
delete msg.id
|
})
|
||||||
// @ts-ignore 暂时去除
|
|
||||||
delete msg.isSender
|
|
||||||
return !'分享消息,图片,表情包,视频'.split(',').includes(msg.type)
|
|
||||||
})
|
|
||||||
const recentMessages = filteredMessages
|
const recentMessages = filteredMessages
|
||||||
.map((msg) => {
|
.map((msg) => {
|
||||||
return `${msg.datetime} ${msg.from}: ${msg.content}`
|
return `${msg.datetime} ${msg.from}: ${msg.content}`
|
||||||
@@ -174,7 +191,7 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
|||||||
{ role: 'system', content: systemPrompt },
|
{ role: 'system', content: systemPrompt },
|
||||||
{ role: 'user', content: prompt }
|
{ role: 'user', content: prompt }
|
||||||
],
|
],
|
||||||
{ apiKey, model }
|
{ apiKey, model, baseURL }
|
||||||
)
|
)
|
||||||
|
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
@@ -292,14 +309,34 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
|||||||
{msg.datetime}
|
{msg.datetime}
|
||||||
</td>
|
</td>
|
||||||
<td style={{ wordBreak: 'break-all', display: 'flex', alignItems: 'center' }}>
|
<td style={{ wordBreak: 'break-all', display: 'flex', alignItems: 'center' }}>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', marginRight: 12 }}>
|
{showAvatar && msg?.img && (
|
||||||
<span>{msg.name}</span>
|
<img style={{ width: '40px', height: '40px' }} src={msg?.img}></img>
|
||||||
{msg?.img && (
|
)}
|
||||||
<img style={{ width: '50px', height: '50px' }} 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>
|
</div>
|
||||||
{/* <span style={{ marginRight: '24px' }}>:</span> */}
|
|
||||||
<div>{msg.content}</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
@@ -325,6 +362,17 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="chat-toolbar">
|
<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 className="toolbar-btn" onClick={onRefresh}>
|
||||||
🔄 刷新聊天记录
|
🔄 刷新聊天记录
|
||||||
</button>
|
</button>
|
||||||
@@ -343,6 +391,9 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
|||||||
<button className="toolbar-btn" onClick={() => handleExport(7)}>
|
<button className="toolbar-btn" onClick={() => handleExport(7)}>
|
||||||
📅 导出近7天
|
📅 导出近7天
|
||||||
</button>
|
</button>
|
||||||
|
<button className="toolbar-btn" onClick={() => handleExport(30)}>
|
||||||
|
📅 导出近30天
|
||||||
|
</button>
|
||||||
<button className="toolbar-btn" onClick={() => setShowSettingsModal(true)}>
|
<button className="toolbar-btn" onClick={() => setShowSettingsModal(true)}>
|
||||||
🤖 AI总结群聊
|
🤖 AI总结群聊
|
||||||
</button>
|
</button>
|
||||||
@@ -436,22 +487,37 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
|||||||
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
||||||
<h3>AI 设置</h3>
|
<h3>AI 设置</h3>
|
||||||
<div className="form-group" style={{ marginBottom: '15px' }}>
|
<div className="form-group" style={{ marginBottom: '15px' }}>
|
||||||
<label style={{ display: 'block', marginBottom: '5px' }}>模型:</label>
|
<label style={{ display: 'block', marginBottom: '5px' }}>模型服务:</label>
|
||||||
<select
|
<select
|
||||||
value={model}
|
value={model}
|
||||||
onChange={(e) => setModel(e.target.value)}
|
onChange={(e) => setModel(e.target.value)}
|
||||||
style={{ width: '100%', padding: '8px' }}
|
style={{ width: '100%', padding: '8px' }}
|
||||||
>
|
>
|
||||||
<option value="deepseek-chat">DeepSeek Chat</option>
|
<option value="deepseek-chat">DeepSeek Chat</option>
|
||||||
|
<option value="gpt-4o">GPT-4o</option>
|
||||||
|
<option value="gpt-4o-mini">GPT-4o Mini</option>
|
||||||
|
<option value="gpt-4-turbo">GPT-4 Turbo</option>
|
||||||
|
<option value="claude-3-5-sonnet-20240620">Claude 3.5 Sonnet</option>
|
||||||
|
<option value="moonshot-v1-8k">Moonshot V1</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="form-group" style={{ marginBottom: '15px' }}>
|
||||||
|
<label style={{ display: 'block', marginBottom: '5px' }}>Base URL:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={baseURL}
|
||||||
|
onChange={(e) => setBaseURL(e.target.value)}
|
||||||
|
placeholder="https://api.deepseek.com"
|
||||||
|
style={{ width: '95%', padding: '8px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="form-group" style={{ marginBottom: '20px' }}>
|
<div className="form-group" style={{ marginBottom: '20px' }}>
|
||||||
<label style={{ display: 'block', marginBottom: '5px' }}>API Key:</label>
|
<label style={{ display: 'block', marginBottom: '5px' }}>API Key:</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={apiKey}
|
value={apiKey}
|
||||||
onChange={(e) => setApiKey(e.target.value)}
|
onChange={(e) => setApiKey(e.target.value)}
|
||||||
placeholder="Enter your DeepSeek API Key"
|
placeholder="Enter your API Key"
|
||||||
style={{ width: '95%', padding: '8px' }}
|
style={{ width: '95%', padding: '8px' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"files": [],
|
"files": [],
|
||||||
"references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }]
|
"references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }],
|
||||||
|
"include": ["src", "vite-env.d.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
|
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
|
||||||
"include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*", "src/shared/**/*"],
|
"include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*", "src/shared/**/*", "src/env.d.ts"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"types": ["electron-vite/node"]
|
"types": ["electron-vite/node"]
|
||||||
|
|||||||
Reference in New Issue
Block a user