Initial commit: Open source chat interface based on EchoTrace
64
.gitignore
vendored
Normal 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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,127 @@
|
||||
# 密语 CipherTalk
|
||||
|
||||
[](LICENSE)
|
||||
[](package.json)
|
||||
[]()
|
||||
|
||||
基于 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
@@ -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
@@ -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
@@ -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
@@ -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
|
After Width: | Height: | Size: 87 KiB |
BIN
public/logo.png
Normal file
|
After Width: | Height: | Size: 193 KiB |
BIN
public/wechat-emojis/animal/发抖.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/wechat-emojis/animal/猪头.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/wechat-emojis/animal/跳跳.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/wechat-emojis/animal/转圈.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/wechat-emojis/blessing/庆祝.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
public/wechat-emojis/blessing/烟花.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
public/wechat-emojis/blessing/爆竹.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
public/wechat-emojis/blessing/發.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
public/wechat-emojis/blessing/礼物.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
public/wechat-emojis/blessing/福.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
public/wechat-emojis/blessing/红包.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
public/wechat-emojis/face/666.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
public/wechat-emojis/face/Emm.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
public/wechat-emojis/face/亲亲.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
public/wechat-emojis/face/偷笑.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
public/wechat-emojis/face/傲慢.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
public/wechat-emojis/face/再见.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
public/wechat-emojis/face/加油.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/wechat-emojis/face/发呆.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
public/wechat-emojis/face/发怒.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
public/wechat-emojis/face/可怜.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
public/wechat-emojis/face/右哼哼.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
public/wechat-emojis/face/叹气.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
public/wechat-emojis/face/吃瓜.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
public/wechat-emojis/face/吐.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
public/wechat-emojis/face/呲牙.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
public/wechat-emojis/face/咒骂.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
public/wechat-emojis/face/哇.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
public/wechat-emojis/face/嘘.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
public/wechat-emojis/face/嘿哈.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
public/wechat-emojis/face/囧.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
public/wechat-emojis/face/困.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
public/wechat-emojis/face/坏笑.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
public/wechat-emojis/face/大哭.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
public/wechat-emojis/face/天啊.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
public/wechat-emojis/face/失望.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
public/wechat-emojis/face/奸笑.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/wechat-emojis/face/好的.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
public/wechat-emojis/face/委屈.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
public/wechat-emojis/face/害羞.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
public/wechat-emojis/face/尴尬.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
public/wechat-emojis/face/得意.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
public/wechat-emojis/face/微笑.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
public/wechat-emojis/face/快哭了.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
public/wechat-emojis/face/恐惧.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
public/wechat-emojis/face/悠闲.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
public/wechat-emojis/face/惊恐.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
public/wechat-emojis/face/惊讶.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
public/wechat-emojis/face/愉快.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
public/wechat-emojis/face/憨笑.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
public/wechat-emojis/face/打脸.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
public/wechat-emojis/face/抓狂.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
public/wechat-emojis/face/抠鼻.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
public/wechat-emojis/face/捂脸.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
public/wechat-emojis/face/撇嘴.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
public/wechat-emojis/face/擦汗.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
public/wechat-emojis/face/敲打.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
public/wechat-emojis/face/无语.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
public/wechat-emojis/face/旺柴.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
public/wechat-emojis/face/晕.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
public/wechat-emojis/face/机智.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
public/wechat-emojis/face/汗.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
public/wechat-emojis/face/流泪.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
public/wechat-emojis/face/生病.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
public/wechat-emojis/face/疑问.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
public/wechat-emojis/face/白眼.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
public/wechat-emojis/face/皱眉.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
public/wechat-emojis/face/睡.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
public/wechat-emojis/face/破涕为笑.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
public/wechat-emojis/face/社会社会.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
public/wechat-emojis/face/笑脸.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
public/wechat-emojis/face/翻白眼.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
public/wechat-emojis/face/耶.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
public/wechat-emojis/face/脸红.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
public/wechat-emojis/face/色.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
public/wechat-emojis/face/苦涩.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
public/wechat-emojis/face/衰.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
public/wechat-emojis/face/裂开.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
public/wechat-emojis/face/让我看看.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
public/wechat-emojis/face/调皮.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
public/wechat-emojis/face/鄙视.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
public/wechat-emojis/face/闭嘴.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
public/wechat-emojis/face/阴险.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
public/wechat-emojis/face/难过.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
public/wechat-emojis/face/骷髅.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
public/wechat-emojis/face/鼓掌.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
public/wechat-emojis/gesture/OK.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/wechat-emojis/gesture/勾引.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
public/wechat-emojis/gesture/合十.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |