Initial commit: Open source chat interface based on EchoTrace

This commit is contained in:
ILoveBingLu
2026-01-08 18:38:13 +08:00
commit 7f115a7a78
178 changed files with 17150 additions and 0 deletions

64
.gitignore vendored Normal file
View File

@@ -0,0 +1,64 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Dependencies
node_modules
dist
dist-electron
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Build output
out
release
# Database
*.db
*.db-shm
*.db-wal
# Environment
.env
.env.local
.env.production
# OS
Thumbs.db
# ========================================
# 半开源项目 - 核心敏感代码文件
# ========================================
# 核心解密服务 - 包含微信数据库解密算法
electron/services/
# 核心资源文件 - 包含关键的DLL和二进制文件
resources/
# 安装脚本 - 包含安装逻辑
installer.nsh
# 构建脚本 - 包含构建和发布逻辑
scripts/add-size-to-yml.js
# 配置文件 - 可能包含敏感配置
.npmrc
TODO.md
package-lock.json

73
CHANGELOG.md Normal file
View File

@@ -0,0 +1,73 @@
# 更新日志
本文档记录了密语 CipherTalk 的所有重要更改。
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)
并且本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
## [未发布]
### 新增
- 完善的项目文档和贡献指南
- 标准化的开源项目结构
- MIT 许可证
### 变更
- 更新 README.md 以反映项目定位
- 优化 .gitignore 配置
- 改进代码组织结构
## [1.0.1] - 2024-01-08
### 新增
- 💬 现代化聊天记录查看界面
- 🎨 多主题支持(浅色/深色模式)
- 📊 数据可视化图表界面
- 🔍 搜索功能界面
- 📤 数据导出界面
- 🌍 国际化框架
### 技术特性
- 基于 Electron 39 + React 19
- TypeScript 严格模式
- Zustand 状态管理
- SCSS 样式系统
- ECharts 图表库
### 修复
- 修复了界面响应性问题
- 优化了组件渲染性能
- 改进了错误处理机制
## [1.0.0] - 2024-01-01
### 新增
- 🎉 项目初始版本
- 基础的聊天记录查看界面
- 简单的主题切换功能
- 基础 UI 组件库
### 技术栈
- Electron + React + TypeScript
- 基础的文件结构和构建配置
---
## 版本说明
### 版本号规则
- **主版本号**: 不兼容的 API 修改
- **次版本号**: 向下兼容的功能性新增
- **修订号**: 向下兼容的问题修正
### 更新类型
- `新增` - 新功能
- `变更` - 对现有功能的变更
- `弃用` - 即将移除的功能
- `移除` - 已移除的功能
- `修复` - 问题修复
- `安全` - 安全相关的修复
---
更多详细信息请查看 [GitHub Releases](https://github.com/your-repo/releases)。

248
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,248 @@
# 贡献指南
感谢你对密语 CipherTalk 项目的关注!我们欢迎社区贡献,让这个项目变得更好。
## 🤝 如何贡献
### 贡献领域
#### ✅ 欢迎贡献的领域
- **前端界面优化**
- React 组件改进
- UI/UX 优化
- 响应式设计
- 无障碍访问性改进
- **样式和主题**
- 新主题色彩方案
- CSS 动画效果
- 图标和视觉元素
- 深色模式优化
- **用户体验**
- 交互流程优化
- 错误提示改进
- 加载状态优化
- 快捷键支持
- **文档完善**
- README 文档
- 代码注释
- 使用教程
- API 文档
- **国际化**
- 多语言支持
- 本地化适配
- 文本翻译
- **性能优化**
- 组件渲染优化
- 内存使用优化
- 打包体积优化
## 📋 贡献流程
### 1. 准备工作
```bash
# Fork 项目到你的 GitHub 账号
# 克隆你的 fork
git clone https://github.com/ILoveBingLu/miyu.git
cd miyu
# 安装依赖
npm install
# 创建新分支
git checkout -b feature/your-feature-name
```
### 2. 开发环境
```bash
# 启动开发服务器
npm run dev
# 运行类型检查
npx tsc --noEmit
# 运行代码格式化
npx prettier --write src/
```
### 3. 提交代码
```bash
# 添加修改的文件
git add .
# 提交代码(请使用有意义的提交信息)
git commit -m "feat: 添加新的主题色彩方案"
# 推送到你的 fork
git push origin feature/your-feature-name
```
### 4. 创建 Pull Request
1. 在 GitHub 上打开你的 fork
2. 点击 "New Pull Request"
3. 选择目标分支(通常是 `main`
4. 填写 PR 描述,说明你的修改内容
5. 提交 PR 等待审核
## 📝 代码规范
### TypeScript 规范
- 使用 TypeScript 严格模式
- 为所有函数和变量提供类型注解
- 使用接口定义复杂对象类型
- 避免使用 `any` 类型
```typescript
// ✅ 好的示例
interface UserInfo {
id: string
name: string
avatar?: string
}
const getUserInfo = (userId: string): Promise<UserInfo> => {
// 实现
}
// ❌ 避免的写法
const getUserInfo = (userId: any): any => {
// 实现
}
```
### React 组件规范
- 使用函数组件和 Hooks
- 组件名使用 PascalCase
- Props 接口以组件名 + Props 命名
- 使用 memo 优化性能关键组件
```typescript
// ✅ 好的示例
interface ChatMessageProps {
message: string
timestamp: number
sender: string
}
const ChatMessage: React.FC<ChatMessageProps> = ({ message, timestamp, sender }) => {
return (
<div className="chat-message">
<span className="sender">{sender}</span>
<p className="content">{message}</p>
<time className="timestamp">{new Date(timestamp).toLocaleString()}</time>
</div>
)
}
export default React.memo(ChatMessage)
```
### CSS/SCSS 规范
- 使用 BEM 命名规范
- 优先使用 CSS 变量
- 避免深层嵌套(最多 3 层)
- 使用语义化的类名
```scss
// ✅ 好的示例
.chat-message {
padding: var(--spacing-md);
border-radius: var(--border-radius);
&__sender {
font-weight: bold;
color: var(--color-primary);
}
&__content {
margin: var(--spacing-sm) 0;
line-height: 1.5;
}
&--highlighted {
background-color: var(--color-highlight);
}
}
```
## 🐛 报告问题
### 提交 Issue 前请检查
1. 搜索现有 Issues避免重复提交
2. 确保问题与开源部分相关
3. 提供详细的复现步骤
4. 包含系统环境信息
### Issue 模板
```markdown
## 问题描述
简要描述遇到的问题
## 复现步骤
1. 打开应用
2. 点击某个按钮
3. 看到错误信息
## 期望行为
描述你期望发生的情况
## 实际行为
描述实际发生的情况
## 环境信息
- 操作系统: Windows 11
- Node.js 版本: 18.17.0
- 应用版本: 1.0.1
## 截图
如果适用,请添加截图来帮助解释问题
```
## 🎯 优先级指南
我们特别欢迎以下类型的贡献:
### 高优先级
- 🐛 修复 UI 相关的 bug
- ♿ 无障碍访问性改进
- 🌍 国际化和本地化
- 📱 响应式设计优化
### 中优先级
- ✨ 新的 UI 组件
- 🎨 主题和样式改进
- 📖 文档完善
- 🔧 开发工具改进
### 低优先级
- 🧹 代码重构(需要充分理由)
- 📦 依赖更新
- 🎯 性能优化(需要基准测试)
## 📞 联系我们
如果你有任何问题或建议,可以通过以下方式联系我们:
- 🐛 GitHub Issues
## 🙏 致谢
感谢所有贡献者的努力!你们的贡献让这个项目变得更好。
---
再次感谢你的贡献!让我们一起打造更好的密语 CipherTalk

30
LICENSE Normal file
View File

@@ -0,0 +1,30 @@
MIT License
Copyright (c) 2026 CipherTalk
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.
## 免责声明
本软件仅供学习和研究使用。使用者应当遵守所在地区的法律法规,
不得将本软件用于任何违法违规的活动。因使用本软件而产生的任何
法律责任由使用者自行承担。
作者不对软件的完整性、准确性或可用性做出任何保证,也不对因
使用本软件而导致的任何直接或间接损失承担责任。

127
README.md Normal file
View File

@@ -0,0 +1,127 @@
# 密语 CipherTalk
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Version](https://img.shields.io/badge/version-1.0.1-green.svg)](package.json)
[![Platform](https://img.shields.io/badge/platform-Windows-lightgrey.svg)]()
基于 Electron + React + TypeScript 构建的聊天记录查看工具界面,基于原项目 [EchoTrace](https://github.com/ycccccccy/echotrace) 重构。
## 🚀 功能特性
- 💬 **聊天记录界面** - 现代化的聊天记录查看界面
- 🎨 **主题切换** - 支持浅色/深色模式,多种主题色可选
- 📊 **数据可视化** - 图表展示和数据分析界面
- <20> **搜索功能** - 全文搜索界面和交互
- 📤 **导出界面** - 数据导出功能的用户界面
- 🌍 **国际化** - 多语言支持框架
## 🛠️ 技术栈
- **前端框架**: React 19 + TypeScript + Zustand
- **桌面应用**: Electron 39
- **构建工具**: Vite + electron-builder
- **样式方案**: SCSS + CSS Variables
- **图表库**: ECharts
- **其他**: jieba-wasm (分词), lucide-react (图标)
## 📁 项目结构
```
ciphertalk/
├── src/ # React 前端
│ ├── components/ # 通用组件
│ ├── pages/ # 页面组件
│ ├── stores/ # Zustand 状态管理
│ ├── services/ # 前端服务
│ ├── types/ # TypeScript 类型定义
│ ├── utils/ # 工具函数
│ └── styles/ # 样式文件
├── public/ # 静态资源
├── electron/ # Electron 配置
│ ├── main.ts # 主进程入口
│ └── preload.ts # 预加载脚本
└── docs/ # 项目文档
```
## 🚀 快速开始
### 环境要求
- Node.js 18+
- Windows 10/11
### 安装依赖
```bash
npm install
```
### 开发模式
```bash
npm run dev
```
### 构建应用
```bash
npm run build
```
## 📖 开发指南
### 前端开发
本项目使用现代化的前端技术栈:
1. **React 19** - 最新的 React 版本,支持并发特性
2. **TypeScript** - 类型安全的 JavaScript
3. **Zustand** - 轻量级状态管理
4. **SCSS** - 强大的 CSS 预处理器
5. **Vite** - 快速的构建工具
### 组件开发
- 使用函数组件和 Hooks
- 遵循 TypeScript 最佳实践
- 组件名使用 PascalCase
- 样式使用 BEM 命名规范
### 主题系统
项目支持多主题切换:
- 浅色/深色模式
- 多种主题色彩
- CSS 变量驱动
- 响应式设计
## 🤝 贡献指南
欢迎贡献代码!请查看 [CONTRIBUTING.md](CONTRIBUTING.md) 了解详细信息。
### 贡献领域
- <20> 修复 UI 相关的 bug
- ✨ 改进用户界面和交互
- 📝 完善文档和注释
- 🎨 优化样式和主题
- 🌍 国际化和本地化
## 📄 许可证
本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。
## ⚠️ 免责声明
- 本项目仅供学习和研究使用
- 请遵守相关法律法规
- 使用本项目产生的任何后果由用户自行承担
## 📞 联系方式
- 🐛 问题反馈: [GitHub Issues](https://github.com/ILoveBingLu/miyu/issues)
## 联致谢
感谢所有为开源社区做出贡献的开发者们!

946
electron/main.ts Normal file
View File

@@ -0,0 +1,946 @@
import { app, BrowserWindow, ipcMain, nativeTheme } from 'electron'
import { join } from 'path'
import { autoUpdater } from 'electron-updater'
import { DatabaseService } from './services/database'
import { DecryptService } from './services/decrypt'
import { ConfigService } from './services/config'
import { wxKeyService } from './services/wxKeyService'
import { dbPathService } from './services/dbPathService'
import { wcdbService } from './services/wcdbService'
import { dataManagementService } from './services/dataManagementService'
import { imageDecryptService } from './services/imageDecryptService'
import { imageKeyService } from './services/imageKeyService'
import { chatService } from './services/chatService'
import { analyticsService } from './services/analyticsService'
import { groupAnalyticsService } from './services/groupAnalyticsService'
import { annualReportService } from './services/annualReportService'
import { exportService, ExportOptions } from './services/exportService'
import { activationService } from './services/activationService'
// 配置自动更新
autoUpdater.autoDownload = false
autoUpdater.autoInstallOnAppQuit = true
autoUpdater.disableDifferentialDownload = true // 禁用差分更新,强制全量下载
// 单例服务
let dbService: DatabaseService | null = null
let decryptService: DecryptService | null = null
let configService: ConfigService | null = null
// 聊天窗口实例
let chatWindow: BrowserWindow | null = null
// 群聊分析窗口实例
let groupAnalyticsWindow: BrowserWindow | null = null
// 年度报告窗口实例
let annualReportWindow: BrowserWindow | null = null
// 协议窗口实例
let agreementWindow: BrowserWindow | null = null
// 购买窗口实例
let purchaseWindow: BrowserWindow | null = null
function createWindow() {
// 获取图标路径 - 打包后在 resources 目录
const isDev = !!process.env.VITE_DEV_SERVER_URL
const iconPath = isDev
? join(__dirname, '../public/icon.ico')
: join(process.resourcesPath, 'icon.ico')
const win = new BrowserWindow({
width: 1400,
height: 900,
minWidth: 1000,
minHeight: 700,
icon: iconPath,
webPreferences: {
preload: join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
},
titleBarStyle: 'hidden',
titleBarOverlay: {
color: '#00000000',
symbolColor: '#1a1a1a',
height: 40
},
show: false
})
// 初始化服务
configService = new ConfigService()
dbService = new DatabaseService()
decryptService = new DecryptService()
// 窗口准备好后显示
win.once('ready-to-show', () => {
win.show()
})
// 开发环境加载 vite 服务器
if (process.env.VITE_DEV_SERVER_URL) {
win.loadURL(process.env.VITE_DEV_SERVER_URL)
// 开发环境下按 F12 或 Ctrl+Shift+I 打开开发者工具
win.webContents.on('before-input-event', (event, input) => {
if (input.key === 'F12' || (input.control && input.shift && input.key === 'I')) {
if (win.webContents.isDevToolsOpened()) {
win.webContents.closeDevTools()
} else {
win.webContents.openDevTools()
}
event.preventDefault()
}
})
} else {
win.loadFile(join(__dirname, '../dist/index.html'))
}
return win
}
/**
* 创建独立的聊天窗口(仿微信风格)
*/
function createChatWindow() {
// 如果已存在,聚焦到现有窗口
if (chatWindow && !chatWindow.isDestroyed()) {
chatWindow.focus()
return chatWindow
}
const isDev = !!process.env.VITE_DEV_SERVER_URL
const iconPath = isDev
? join(__dirname, '../public/icon.ico')
: join(process.resourcesPath, 'icon.ico')
chatWindow = new BrowserWindow({
width: 1000,
height: 700,
minWidth: 800,
minHeight: 600,
icon: iconPath,
webPreferences: {
preload: join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
},
titleBarStyle: 'hidden',
titleBarOverlay: {
color: '#00000000',
symbolColor: '#666666',
height: 32
},
show: false,
backgroundColor: '#F0F0F0'
})
chatWindow.once('ready-to-show', () => {
chatWindow?.show()
})
// 加载聊天页面
if (process.env.VITE_DEV_SERVER_URL) {
chatWindow.loadURL(`${process.env.VITE_DEV_SERVER_URL}#/chat-window`)
chatWindow.webContents.on('before-input-event', (event, input) => {
if (input.key === 'F12' || (input.control && input.shift && input.key === 'I')) {
if (chatWindow?.webContents.isDevToolsOpened()) {
chatWindow.webContents.closeDevTools()
} else {
chatWindow?.webContents.openDevTools()
}
event.preventDefault()
}
})
} else {
chatWindow.loadFile(join(__dirname, '../dist/index.html'), { hash: '/chat-window' })
}
chatWindow.on('closed', () => {
chatWindow = null
})
return chatWindow
}
/**
* 创建独立的群聊分析窗口
*/
function createGroupAnalyticsWindow() {
// 如果已存在,聚焦到现有窗口
if (groupAnalyticsWindow && !groupAnalyticsWindow.isDestroyed()) {
groupAnalyticsWindow.focus()
return groupAnalyticsWindow
}
const isDev = !!process.env.VITE_DEV_SERVER_URL
const iconPath = isDev
? join(__dirname, '../public/icon.ico')
: join(process.resourcesPath, 'icon.ico')
groupAnalyticsWindow = new BrowserWindow({
width: 1100,
height: 750,
minWidth: 900,
minHeight: 600,
icon: iconPath,
webPreferences: {
preload: join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
},
titleBarStyle: 'hidden',
titleBarOverlay: {
color: '#00000000',
symbolColor: '#666666',
height: 32
},
show: false,
backgroundColor: '#F0F0F0'
})
groupAnalyticsWindow.once('ready-to-show', () => {
groupAnalyticsWindow?.show()
})
// 加载群聊分析页面
if (process.env.VITE_DEV_SERVER_URL) {
groupAnalyticsWindow.loadURL(`${process.env.VITE_DEV_SERVER_URL}#/group-analytics-window`)
groupAnalyticsWindow.webContents.on('before-input-event', (event, input) => {
if (input.key === 'F12' || (input.control && input.shift && input.key === 'I')) {
if (groupAnalyticsWindow?.webContents.isDevToolsOpened()) {
groupAnalyticsWindow.webContents.closeDevTools()
} else {
groupAnalyticsWindow?.webContents.openDevTools()
}
event.preventDefault()
}
})
} else {
groupAnalyticsWindow.loadFile(join(__dirname, '../dist/index.html'), { hash: '/group-analytics-window' })
}
groupAnalyticsWindow.on('closed', () => {
groupAnalyticsWindow = null
})
return groupAnalyticsWindow
}
/**
* 创建独立的年度报告窗口
*/
function createAnnualReportWindow(year: number) {
// 如果已存在,关闭旧窗口
if (annualReportWindow && !annualReportWindow.isDestroyed()) {
annualReportWindow.close()
annualReportWindow = null
}
const isDev = !!process.env.VITE_DEV_SERVER_URL
const iconPath = isDev
? join(__dirname, '../public/icon.ico')
: join(process.resourcesPath, 'icon.ico')
const isDark = nativeTheme.shouldUseDarkColors
annualReportWindow = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 900,
minHeight: 650,
icon: iconPath,
webPreferences: {
preload: join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
},
titleBarStyle: 'hidden',
titleBarOverlay: {
color: '#00000000',
symbolColor: isDark ? '#FFFFFF' : '#333333',
height: 32
},
show: false,
backgroundColor: isDark ? '#1A1A1A' : '#F9F8F6'
})
annualReportWindow.once('ready-to-show', () => {
annualReportWindow?.show()
})
// 加载年度报告页面,带年份参数
if (process.env.VITE_DEV_SERVER_URL) {
annualReportWindow.loadURL(`${process.env.VITE_DEV_SERVER_URL}#/annual-report-window?year=${year}`)
annualReportWindow.webContents.on('before-input-event', (event, input) => {
if (input.key === 'F12' || (input.control && input.shift && input.key === 'I')) {
if (annualReportWindow?.webContents.isDevToolsOpened()) {
annualReportWindow.webContents.closeDevTools()
} else {
annualReportWindow?.webContents.openDevTools()
}
event.preventDefault()
}
})
} else {
annualReportWindow.loadFile(join(__dirname, '../dist/index.html'), { hash: `/annual-report-window?year=${year}` })
}
annualReportWindow.on('closed', () => {
annualReportWindow = null
})
return annualReportWindow
}
/**
* 创建用户协议窗口
*/
function createAgreementWindow() {
// 如果已存在,聚焦
if (agreementWindow && !agreementWindow.isDestroyed()) {
agreementWindow.focus()
return agreementWindow
}
const isDev = !!process.env.VITE_DEV_SERVER_URL
const iconPath = isDev
? join(__dirname, '../public/icon.ico')
: join(process.resourcesPath, 'icon.ico')
const isDark = nativeTheme.shouldUseDarkColors
agreementWindow = new BrowserWindow({
width: 800,
height: 700,
minWidth: 600,
minHeight: 500,
icon: iconPath,
webPreferences: {
preload: join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
},
titleBarStyle: 'hidden',
titleBarOverlay: {
color: '#00000000',
symbolColor: isDark ? '#FFFFFF' : '#333333',
height: 32
},
show: false,
backgroundColor: isDark ? '#1A1A1A' : '#FFFFFF'
})
agreementWindow.once('ready-to-show', () => {
agreementWindow?.show()
})
if (process.env.VITE_DEV_SERVER_URL) {
agreementWindow.loadURL(`${process.env.VITE_DEV_SERVER_URL}#/agreement-window`)
} else {
agreementWindow.loadFile(join(__dirname, '../dist/index.html'), { hash: '/agreement-window' })
}
agreementWindow.on('closed', () => {
agreementWindow = null
})
return agreementWindow
}
/**
* 创建购买窗口
*/
function createPurchaseWindow() {
// 如果已存在,聚焦
if (purchaseWindow && !purchaseWindow.isDestroyed()) {
purchaseWindow.focus()
return purchaseWindow
}
const isDev = !!process.env.VITE_DEV_SERVER_URL
const iconPath = isDev
? join(__dirname, '../public/icon.ico')
: join(process.resourcesPath, 'icon.ico')
purchaseWindow = new BrowserWindow({
width: 1000,
height: 700,
minWidth: 800,
minHeight: 600,
icon: iconPath,
webPreferences: {
contextIsolation: true,
nodeIntegration: false
},
title: '获取激活码 - 密语',
show: false,
backgroundColor: '#FFFFFF',
autoHideMenuBar: true
})
purchaseWindow.once('ready-to-show', () => {
purchaseWindow?.show()
})
// 加载购买页面
purchaseWindow.loadURL('https://pay.ldxp.cn/shop/aiqiji')
purchaseWindow.on('closed', () => {
purchaseWindow = null
})
return purchaseWindow
}
// 注册 IPC 处理器
function registerIpcHandlers() {
// 配置相关
ipcMain.handle('config:get', async (_, key: string) => {
return configService?.get(key as any)
})
ipcMain.handle('config:set', async (_, key: string, value: any) => {
return configService?.set(key as any, value)
})
// TLD 缓存相关
ipcMain.handle('config:getTldCache', async () => {
return configService?.getTldCache()
})
ipcMain.handle('config:setTldCache', async (_, tlds: string[]) => {
return configService?.setTldCache(tlds)
})
// 数据库相关
ipcMain.handle('db:open', async (_, dbPath: string) => {
return dbService?.open(dbPath)
})
ipcMain.handle('db:query', async (_, sql: string, params?: any[]) => {
return dbService?.query(sql, params)
})
ipcMain.handle('db:close', async () => {
return dbService?.close()
})
// 解密相关
ipcMain.handle('decrypt:database', async (_, sourcePath: string, key: string, outputPath: string) => {
return decryptService?.decryptDatabase(sourcePath, key, outputPath)
})
ipcMain.handle('decrypt:image', async (_, imagePath: string) => {
return decryptService?.decryptImage(imagePath)
})
// 文件对话框
ipcMain.handle('dialog:openFile', async (_, options) => {
const { dialog } = await import('electron')
return dialog.showOpenDialog(options)
})
ipcMain.handle('dialog:saveFile', async (_, options) => {
const { dialog } = await import('electron')
return dialog.showSaveDialog(options)
})
ipcMain.handle('shell:openPath', async (_, path: string) => {
const { shell } = await import('electron')
return shell.openPath(path)
})
ipcMain.handle('shell:openExternal', async (_, url: string) => {
const { shell } = await import('electron')
return shell.openExternal(url)
})
ipcMain.handle('app:getDownloadsPath', async () => {
return app.getPath('downloads')
})
ipcMain.handle('app:getVersion', async () => {
return app.getVersion()
})
ipcMain.handle('app:checkForUpdates', async () => {
try {
const result = await autoUpdater.checkForUpdates()
if (result && result.updateInfo) {
const currentVersion = app.getVersion()
const latestVersion = result.updateInfo.version
if (latestVersion !== currentVersion) {
return {
hasUpdate: true,
version: latestVersion,
releaseNotes: result.updateInfo.releaseNotes as string || ''
}
}
}
return { hasUpdate: false }
} catch (error) {
console.error('检查更新失败:', error)
return { hasUpdate: false }
}
})
ipcMain.handle('app:downloadAndInstall', async (event) => {
const win = BrowserWindow.fromWebContents(event.sender)
// 监听下载进度
autoUpdater.on('download-progress', (progress) => {
win?.webContents.send('app:downloadProgress', progress.percent)
})
// 下载完成后自动安装
autoUpdater.on('update-downloaded', () => {
autoUpdater.quitAndInstall(false, true)
})
try {
await autoUpdater.downloadUpdate()
} catch (error) {
console.error('下载更新失败:', error)
throw error
}
})
// 窗口控制
ipcMain.on('window:minimize', (event) => {
BrowserWindow.fromWebContents(event.sender)?.minimize()
})
ipcMain.on('window:maximize', (event) => {
const win = BrowserWindow.fromWebContents(event.sender)
if (win?.isMaximized()) {
win.unmaximize()
} else {
win?.maximize()
}
})
ipcMain.on('window:close', (event) => {
BrowserWindow.fromWebContents(event.sender)?.close()
})
// 更新窗口控件主题色
ipcMain.on('window:setTitleBarOverlay', (event, options: { symbolColor: string }) => {
const win = BrowserWindow.fromWebContents(event.sender)
if (win) {
win.setTitleBarOverlay({
color: '#00000000',
symbolColor: options.symbolColor,
height: 40
})
}
})
// 密钥获取相关
ipcMain.handle('wxkey:isWeChatRunning', async () => {
return wxKeyService.isWeChatRunning()
})
ipcMain.handle('wxkey:getWeChatPid', async () => {
return wxKeyService.getWeChatPid()
})
ipcMain.handle('wxkey:killWeChat', async () => {
return wxKeyService.killWeChat()
})
ipcMain.handle('wxkey:launchWeChat', async () => {
return wxKeyService.launchWeChat()
})
ipcMain.handle('wxkey:waitForWindow', async (_, maxWaitSeconds?: number) => {
return wxKeyService.waitForWeChatWindow(maxWaitSeconds)
})
ipcMain.handle('wxkey:startGetKey', async (event) => {
try {
// 初始化 DLL
const initSuccess = await wxKeyService.initialize()
if (!initSuccess) {
return { success: false, error: 'DLL 初始化失败' }
}
// 获取微信 PID
const pid = wxKeyService.getWeChatPid()
if (!pid) {
return { success: false, error: '未找到微信进程' }
}
// 创建 Promise 等待密钥
return new Promise((resolve) => {
const timeout = setTimeout(() => {
wxKeyService.dispose()
resolve({ success: false, error: '获取密钥超时' })
}, 60000)
const success = wxKeyService.installHook(
pid,
(key) => {
clearTimeout(timeout)
wxKeyService.dispose()
resolve({ success: true, key })
},
(status, level) => {
// 发送状态到渲染进程
event.sender.send('wxkey:status', { status, level })
}
)
if (!success) {
clearTimeout(timeout)
const error = wxKeyService.getLastError()
wxKeyService.dispose()
resolve({ success: false, error: `Hook 安装失败: ${error}` })
}
})
} catch (e) {
wxKeyService.dispose()
return { success: false, error: String(e) }
}
})
ipcMain.handle('wxkey:cancel', async () => {
wxKeyService.dispose()
return true
})
// 数据库路径相关
ipcMain.handle('dbpath:autoDetect', async () => {
return dbPathService.autoDetect()
})
ipcMain.handle('dbpath:scanWxids', async (_, rootPath: string) => {
return dbPathService.scanWxids(rootPath)
})
ipcMain.handle('dbpath:getDefault', async () => {
return dbPathService.getDefaultPath()
})
// WCDB 数据库相关
ipcMain.handle('wcdb:testConnection', async (_, dbPath: string, hexKey: string, wxid: string) => {
return wcdbService.testConnection(dbPath, hexKey, wxid)
})
ipcMain.handle('wcdb:open', async (_, dbPath: string, hexKey: string, wxid: string) => {
return wcdbService.open(dbPath, hexKey, wxid)
})
ipcMain.handle('wcdb:close', async () => {
wcdbService.close()
return true
})
// 数据管理相关
ipcMain.handle('dataManagement:scanDatabases', async () => {
return dataManagementService.scanDatabases()
})
ipcMain.handle('dataManagement:decryptAll', async () => {
return dataManagementService.decryptAll()
})
ipcMain.handle('dataManagement:incrementalUpdate', async () => {
return dataManagementService.incrementalUpdate()
})
ipcMain.handle('dataManagement:getCurrentCachePath', async () => {
return dataManagementService.getCurrentCachePath()
})
ipcMain.handle('dataManagement:getDefaultCachePath', async () => {
return dataManagementService.getDefaultCachePath()
})
ipcMain.handle('dataManagement:migrateCache', async (_, newCachePath: string) => {
return dataManagementService.migrateCache(newCachePath)
})
ipcMain.handle('dataManagement:scanImages', async (_, dirPath: string) => {
return dataManagementService.scanImages(dirPath)
})
ipcMain.handle('dataManagement:decryptImages', async (_, dirPath: string) => {
return dataManagementService.decryptImages(dirPath)
})
ipcMain.handle('dataManagement:getImageDirectories', async () => {
return dataManagementService.getImageDirectories()
})
ipcMain.handle('dataManagement:decryptSingleImage', async (_, filePath: string) => {
return dataManagementService.decryptSingleImage(filePath)
})
// 图片解密相关
ipcMain.handle('imageDecrypt:batchDetectXorKey', async (_, dirPath: string) => {
try {
const key = await imageDecryptService.batchDetectXorKey(dirPath)
return { success: true, key }
} catch (e) {
return { success: false, error: String(e) }
}
})
ipcMain.handle('imageDecrypt:decryptImage', async (_, inputPath: string, outputPath: string, xorKey: number, aesKey?: string) => {
try {
const aesKeyBuffer = aesKey ? imageDecryptService.asciiKey16(aesKey) : undefined
await imageDecryptService.decryptToFile(inputPath, outputPath, xorKey, aesKeyBuffer)
return { success: true }
} catch (e) {
return { success: false, error: String(e) }
}
})
// 图片密钥获取(从内存)
ipcMain.handle('imageKey:getImageKeys', async (event, userDir: string) => {
try {
// 获取微信 PID
const pid = wxKeyService.getWeChatPid()
if (!pid) {
return { success: false, error: '微信进程未运行,请先启动微信并登录' }
}
const result = await imageKeyService.getImageKeys(
userDir,
pid,
(msg) => {
event.sender.send('imageKey:progress', msg)
}
)
return result
} catch (e) {
return { success: false, error: String(e) }
}
})
// 聊天相关
ipcMain.handle('chat:connect', async () => {
return chatService.connect()
})
ipcMain.handle('chat:getSessions', async () => {
return chatService.getSessions()
})
ipcMain.handle('chat:getMessages', async (_, sessionId: string, offset?: number, limit?: number) => {
return chatService.getMessages(sessionId, offset, limit)
})
ipcMain.handle('chat:getContact', async (_, username: string) => {
return chatService.getContact(username)
})
ipcMain.handle('chat:getContactAvatar', async (_, username: string) => {
return chatService.getContactAvatar(username)
})
ipcMain.handle('chat:getMyAvatarUrl', async () => {
return chatService.getMyAvatarUrl()
})
ipcMain.handle('chat:getMyUserInfo', async () => {
return chatService.getMyUserInfo()
})
ipcMain.handle('chat:downloadEmoji', async (_, cdnUrl: string, md5?: string) => {
return chatService.downloadEmoji(cdnUrl, md5)
})
ipcMain.handle('chat:close', async () => {
chatService.close()
return true
})
ipcMain.handle('chat:refreshCache', async () => {
chatService.refreshMessageDbCache()
return true
})
ipcMain.handle('chat:getSessionDetail', async (_, sessionId: string) => {
return chatService.getSessionDetail(sessionId)
})
// 导出相关
ipcMain.handle('export:exportSessions', async (_, sessionIds: string[], outputDir: string, options: ExportOptions) => {
return exportService.exportSessions(sessionIds, outputDir, options)
})
ipcMain.handle('export:exportSession', async (_, sessionId: string, outputPath: string, options: ExportOptions) => {
return exportService.exportSessionToChatLab(sessionId, outputPath, options)
})
// 数据分析相关
ipcMain.handle('analytics:getOverallStatistics', async () => {
return analyticsService.getOverallStatistics()
})
ipcMain.handle('analytics:getContactRankings', async (_, limit?: number) => {
return analyticsService.getContactRankings(limit)
})
ipcMain.handle('analytics:getTimeDistribution', async () => {
return analyticsService.getTimeDistribution()
})
// 群聊分析相关
ipcMain.handle('groupAnalytics:getGroupChats', async () => {
return groupAnalyticsService.getGroupChats()
})
ipcMain.handle('groupAnalytics:getGroupMembers', async (_, chatroomId: string) => {
return groupAnalyticsService.getGroupMembers(chatroomId)
})
ipcMain.handle('groupAnalytics:getGroupMessageRanking', async (_, chatroomId: string, limit?: number, startTime?: number, endTime?: number) => {
return groupAnalyticsService.getGroupMessageRanking(chatroomId, limit, startTime, endTime)
})
ipcMain.handle('groupAnalytics:getGroupActiveHours', async (_, chatroomId: string, startTime?: number, endTime?: number) => {
return groupAnalyticsService.getGroupActiveHours(chatroomId, startTime, endTime)
})
ipcMain.handle('groupAnalytics:getGroupMediaStats', async (_, chatroomId: string, startTime?: number, endTime?: number) => {
return groupAnalyticsService.getGroupMediaStats(chatroomId, startTime, endTime)
})
// 打开独立聊天窗口
ipcMain.handle('window:openChatWindow', async () => {
createChatWindow()
return true
})
// 打开群聊分析窗口
ipcMain.handle('window:openGroupAnalyticsWindow', async () => {
createGroupAnalyticsWindow()
return true
})
// 打开年度报告窗口
ipcMain.handle('window:openAnnualReportWindow', async (_, year: number) => {
createAnnualReportWindow(year)
return true
})
// 打开协议窗口
ipcMain.handle('window:openAgreementWindow', async () => {
createAgreementWindow()
return true
})
// 打开购买窗口
ipcMain.handle('window:openPurchaseWindow', async () => {
createPurchaseWindow()
return true
})
// 年度报告相关
ipcMain.handle('annualReport:getAvailableYears', async () => {
return annualReportService.getAvailableYears()
})
ipcMain.handle('annualReport:generateReport', async (_, year: number) => {
return annualReportService.generateReport(year)
})
// 检查聊天窗口是否打开
ipcMain.handle('window:isChatWindowOpen', async () => {
return chatWindow !== null && !chatWindow.isDestroyed()
})
// 关闭聊天窗口
ipcMain.handle('window:closeChatWindow', async () => {
if (chatWindow && !chatWindow.isDestroyed()) {
chatWindow.close()
chatWindow = null
}
return true
})
// 激活相关
ipcMain.handle('activation:getDeviceId', async () => {
return activationService.getDeviceId()
})
ipcMain.handle('activation:verifyCode', async (_, code: string) => {
return activationService.verifyCode(code)
})
ipcMain.handle('activation:activate', async (_, code: string) => {
return activationService.activate(code)
})
ipcMain.handle('activation:checkStatus', async () => {
return activationService.checkActivation()
})
ipcMain.handle('activation:getTypeDisplayName', async (_, type: string | null) => {
return activationService.getTypeDisplayName(type)
})
ipcMain.handle('activation:clearCache', async () => {
activationService.clearCache()
return true
})
}
// 主窗口引用
let mainWindow: BrowserWindow | null = null
// 启动时自动检测更新
function checkForUpdatesOnStartup() {
// 开发环境不检测更新
if (process.env.VITE_DEV_SERVER_URL) return
// 延迟3秒检测等待窗口完全加载
setTimeout(async () => {
try {
const result = await autoUpdater.checkForUpdates()
if (result && result.updateInfo) {
const currentVersion = app.getVersion()
const latestVersion = result.updateInfo.version
if (latestVersion !== currentVersion && mainWindow) {
// 通知渲染进程有新版本
mainWindow.webContents.send('app:updateAvailable', {
version: latestVersion,
releaseNotes: result.updateInfo.releaseNotes || ''
})
}
}
} catch (error) {
console.error('启动时检查更新失败:', error)
}
}, 3000)
}
app.whenReady().then(() => {
registerIpcHandlers()
mainWindow = createWindow()
// 启动时检测更新
checkForUpdatesOnStartup()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
mainWindow = createWindow()
}
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('before-quit', () => {
// 关闭配置数据库连接
configService?.close()
})

190
electron/preload.ts Normal file
View File

@@ -0,0 +1,190 @@
import { contextBridge, ipcRenderer } from 'electron'
// 暴露给渲染进程的 API
contextBridge.exposeInMainWorld('electronAPI', {
// 配置
config: {
get: (key: string) => ipcRenderer.invoke('config:get', key),
set: (key: string, value: any) => ipcRenderer.invoke('config:set', key, value),
getTldCache: () => ipcRenderer.invoke('config:getTldCache'),
setTldCache: (tlds: string[]) => ipcRenderer.invoke('config:setTldCache', tlds)
},
// 数据库操作
db: {
open: (dbPath: string, key?: string) => ipcRenderer.invoke('db:open', dbPath, key),
query: (sql: string, params?: any[]) => ipcRenderer.invoke('db:query', sql, params),
close: () => ipcRenderer.invoke('db:close')
},
// 解密
decrypt: {
database: (sourcePath: string, key: string, outputPath: string) =>
ipcRenderer.invoke('decrypt:database', sourcePath, key, outputPath),
image: (imagePath: string) => ipcRenderer.invoke('decrypt:image', imagePath)
},
// 对话框
dialog: {
openFile: (options: any) => ipcRenderer.invoke('dialog:openFile', options),
saveFile: (options: any) => ipcRenderer.invoke('dialog:saveFile', options)
},
// Shell
shell: {
openPath: (path: string) => ipcRenderer.invoke('shell:openPath', path),
openExternal: (url: string) => ipcRenderer.invoke('shell:openExternal', url)
},
// App
app: {
getDownloadsPath: () => ipcRenderer.invoke('app:getDownloadsPath'),
getVersion: () => ipcRenderer.invoke('app:getVersion'),
checkForUpdates: () => ipcRenderer.invoke('app:checkForUpdates'),
downloadAndInstall: () => ipcRenderer.invoke('app:downloadAndInstall'),
onDownloadProgress: (callback: (progress: number) => void) => {
ipcRenderer.on('app:downloadProgress', (_, progress) => callback(progress))
return () => ipcRenderer.removeAllListeners('app:downloadProgress')
},
onUpdateAvailable: (callback: (info: { version: string; releaseNotes: string }) => void) => {
ipcRenderer.on('app:updateAvailable', (_, info) => callback(info))
return () => ipcRenderer.removeAllListeners('app:updateAvailable')
}
},
// 窗口控制
window: {
minimize: () => ipcRenderer.send('window:minimize'),
maximize: () => ipcRenderer.send('window:maximize'),
close: () => ipcRenderer.send('window:close'),
openChatWindow: () => ipcRenderer.invoke('window:openChatWindow'),
openGroupAnalyticsWindow: () => ipcRenderer.invoke('window:openGroupAnalyticsWindow'),
openAnnualReportWindow: (year: number) => ipcRenderer.invoke('window:openAnnualReportWindow', year),
openAgreementWindow: () => ipcRenderer.invoke('window:openAgreementWindow'),
openPurchaseWindow: () => ipcRenderer.invoke('window:openPurchaseWindow'),
isChatWindowOpen: () => ipcRenderer.invoke('window:isChatWindowOpen'),
closeChatWindow: () => ipcRenderer.invoke('window:closeChatWindow'),
setTitleBarOverlay: (options: { symbolColor: string }) => ipcRenderer.send('window:setTitleBarOverlay', options)
},
// 密钥获取
wxKey: {
isWeChatRunning: () => ipcRenderer.invoke('wxkey:isWeChatRunning'),
getWeChatPid: () => ipcRenderer.invoke('wxkey:getWeChatPid'),
killWeChat: () => ipcRenderer.invoke('wxkey:killWeChat'),
launchWeChat: () => ipcRenderer.invoke('wxkey:launchWeChat'),
waitForWindow: (maxWaitSeconds?: number) => ipcRenderer.invoke('wxkey:waitForWindow', maxWaitSeconds),
startGetKey: () => ipcRenderer.invoke('wxkey:startGetKey'),
cancel: () => ipcRenderer.invoke('wxkey:cancel'),
onStatus: (callback: (data: { status: string; level: number }) => void) => {
ipcRenderer.on('wxkey:status', (_, data) => callback(data))
return () => ipcRenderer.removeAllListeners('wxkey:status')
}
},
// 数据库路径
dbPath: {
autoDetect: () => ipcRenderer.invoke('dbpath:autoDetect'),
scanWxids: (rootPath: string) => ipcRenderer.invoke('dbpath:scanWxids', rootPath),
getDefault: () => ipcRenderer.invoke('dbpath:getDefault')
},
// WCDB 数据库
wcdb: {
testConnection: (dbPath: string, hexKey: string, wxid: string) =>
ipcRenderer.invoke('wcdb:testConnection', dbPath, hexKey, wxid),
open: (dbPath: string, hexKey: string, wxid: string) =>
ipcRenderer.invoke('wcdb:open', dbPath, hexKey, wxid),
close: () => ipcRenderer.invoke('wcdb:close')
},
// 数据管理
dataManagement: {
scanDatabases: () => ipcRenderer.invoke('dataManagement:scanDatabases'),
decryptAll: () => ipcRenderer.invoke('dataManagement:decryptAll'),
incrementalUpdate: () => ipcRenderer.invoke('dataManagement:incrementalUpdate'),
getCurrentCachePath: () => ipcRenderer.invoke('dataManagement:getCurrentCachePath'),
getDefaultCachePath: () => ipcRenderer.invoke('dataManagement:getDefaultCachePath'),
migrateCache: (newCachePath: string) => ipcRenderer.invoke('dataManagement:migrateCache', newCachePath),
scanImages: (dirPath: string) => ipcRenderer.invoke('dataManagement:scanImages', dirPath),
decryptImages: (dirPath: string) => ipcRenderer.invoke('dataManagement:decryptImages', dirPath),
getImageDirectories: () => ipcRenderer.invoke('dataManagement:getImageDirectories'),
decryptSingleImage: (filePath: string) => ipcRenderer.invoke('dataManagement:decryptSingleImage', filePath),
onProgress: (callback: (data: any) => void) => {
ipcRenderer.on('dataManagement:progress', (_, data) => callback(data))
return () => ipcRenderer.removeAllListeners('dataManagement:progress')
}
},
// 图片解密
imageDecrypt: {
batchDetectXorKey: (dirPath: string) => ipcRenderer.invoke('imageDecrypt:batchDetectXorKey', dirPath),
decryptImage: (inputPath: string, outputPath: string, xorKey: number, aesKey?: string) =>
ipcRenderer.invoke('imageDecrypt:decryptImage', inputPath, outputPath, xorKey, aesKey)
},
// 图片密钥获取
imageKey: {
getImageKeys: (userDir: string) => ipcRenderer.invoke('imageKey:getImageKeys', userDir),
onProgress: (callback: (msg: string) => void) => {
ipcRenderer.on('imageKey:progress', (_, msg) => callback(msg))
return () => ipcRenderer.removeAllListeners('imageKey:progress')
}
},
// 聊天
chat: {
connect: () => ipcRenderer.invoke('chat:connect'),
getSessions: () => ipcRenderer.invoke('chat:getSessions'),
getMessages: (sessionId: string, offset?: number, limit?: number) =>
ipcRenderer.invoke('chat:getMessages', sessionId, offset, limit),
getContact: (username: string) => ipcRenderer.invoke('chat:getContact', username),
getContactAvatar: (username: string) => ipcRenderer.invoke('chat:getContactAvatar', username),
getMyAvatarUrl: () => ipcRenderer.invoke('chat:getMyAvatarUrl'),
getMyUserInfo: () => ipcRenderer.invoke('chat:getMyUserInfo'),
downloadEmoji: (cdnUrl: string, md5?: string) => ipcRenderer.invoke('chat:downloadEmoji', cdnUrl, md5),
close: () => ipcRenderer.invoke('chat:close'),
refreshCache: () => ipcRenderer.invoke('chat:refreshCache'),
getSessionDetail: (sessionId: string) => ipcRenderer.invoke('chat:getSessionDetail', sessionId)
},
// 数据分析
analytics: {
getOverallStatistics: () => ipcRenderer.invoke('analytics:getOverallStatistics'),
getContactRankings: (limit?: number) => ipcRenderer.invoke('analytics:getContactRankings', limit),
getTimeDistribution: () => ipcRenderer.invoke('analytics:getTimeDistribution')
},
// 群聊分析
groupAnalytics: {
getGroupChats: () => ipcRenderer.invoke('groupAnalytics:getGroupChats'),
getGroupMembers: (chatroomId: string) => ipcRenderer.invoke('groupAnalytics:getGroupMembers', chatroomId),
getGroupMessageRanking: (chatroomId: string, limit?: number, startTime?: number, endTime?: number) => ipcRenderer.invoke('groupAnalytics:getGroupMessageRanking', chatroomId, limit, startTime, endTime),
getGroupActiveHours: (chatroomId: string, startTime?: number, endTime?: number) => ipcRenderer.invoke('groupAnalytics:getGroupActiveHours', chatroomId, startTime, endTime),
getGroupMediaStats: (chatroomId: string, startTime?: number, endTime?: number) => ipcRenderer.invoke('groupAnalytics:getGroupMediaStats', chatroomId, startTime, endTime)
},
// 年度报告
annualReport: {
getAvailableYears: () => ipcRenderer.invoke('annualReport:getAvailableYears'),
generateReport: (year: number) => ipcRenderer.invoke('annualReport:generateReport', year)
},
// 导出
export: {
exportSessions: (sessionIds: string[], outputDir: string, options: any) =>
ipcRenderer.invoke('export:exportSessions', sessionIds, outputDir, options),
exportSession: (sessionId: string, outputPath: string, options: any) =>
ipcRenderer.invoke('export:exportSession', sessionId, outputPath, options)
},
// 激活
activation: {
getDeviceId: () => ipcRenderer.invoke('activation:getDeviceId'),
verifyCode: (code: string) => ipcRenderer.invoke('activation:verifyCode', code),
activate: (code: string) => ipcRenderer.invoke('activation:activate', code),
checkStatus: () => ipcRenderer.invoke('activation:checkStatus'),
getTypeDisplayName: (type: string | null) => ipcRenderer.invoke('activation:getTypeDisplayName', type),
clearCache: () => ipcRenderer.invoke('activation:clearCache')
}
})

12
index.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>密语</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

100
package.json Normal file
View File

@@ -0,0 +1,100 @@
{
"name": "ciphertalk",
"version": "1.0.1",
"description": "密语 - 微信聊天记录查看工具",
"main": "dist-electron/main.js",
"scripts": {
"dev": "vite",
"build": "tsc && vite build && electron-builder && node scripts/add-size-to-yml.js",
"preview": "vite preview",
"electron:dev": "vite --mode electron",
"electron:build": "npm run build",
"postinstall": "electron-rebuild"
},
"dependencies": {
"better-sqlite3": "^12.5.0",
"echarts": "^5.5.1",
"echarts-for-react": "^3.0.2",
"electron-store": "^10.0.0",
"electron-updater": "^6.3.9",
"fzstd": "^0.1.1",
"html2canvas": "^1.4.1",
"jieba-wasm": "^2.2.0",
"jszip": "^3.10.1",
"koffi": "^2.9.0",
"lucide-react": "^0.562.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-router-dom": "^7.1.1",
"wechat-emojis": "^1.0.2",
"zustand": "^5.0.2"
},
"devDependencies": {
"@electron/rebuild": "^4.0.2",
"@types/better-sqlite3": "^7.6.13",
"@types/react": "^19.1.0",
"@types/react-dom": "^19.1.0",
"@vitejs/plugin-react": "^4.3.4",
"electron": "^39.2.7",
"electron-builder": "^25.1.8",
"sass": "^1.83.0",
"sharp": "^0.34.5",
"typescript": "^5.6.3",
"vite": "^6.0.5",
"vite-plugin-electron": "^0.28.8",
"vite-plugin-electron-renderer": "^0.14.6"
},
"build": {
"appId": "com.ciphertalk.app",
"productName": "CipherTalk",
"artifactName": "${productName}-${version}-Setup.${ext}",
"directories": {
"output": "release"
},
"publish": {
"provider": "generic",
"url": "https://miyuapp.aiqji.com"
},
"win": {
"target": [
"nsis"
],
"icon": "public/icon.ico"
},
"nsis": {
"differentialPackage": false,
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"unicode": true,
"installerLanguages": [
"zh_CN",
"en_US"
],
"language": "2052",
"displayLanguageSelector": false,
"include": "installer.nsh",
"installerIcon": "public/icon.ico",
"uninstallerIcon": "public/icon.ico",
"installerHeaderIcon": "public/icon.ico",
"perMachine": false,
"allowElevation": true,
"installerSidebar": null,
"uninstallerSidebar": null
},
"extraResources": [
{
"from": "resources/",
"to": "resources/"
},
{
"from": "public/icon.ico",
"to": "icon.ico"
}
],
"files": [
"dist/**/*",
"dist-electron/**/*"
]
}
}

BIN
public/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Some files were not shown because too many files have changed in this diff Show More