mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-26 15:45:51 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a965890916 | ||
|
|
b07bbd68d7 | ||
|
|
3d4a79aac6 | ||
|
|
e30c4cc644 | ||
|
|
d317be3ad3 | ||
|
|
1b078bd2fd | ||
|
|
1d84ed1614 | ||
|
|
114476d74c |
27
README.md
27
README.md
@@ -41,7 +41,28 @@ WeFlow 是一个**完全本地**的微信**实时**聊天记录查看、分析
|
|||||||
- 年度报告与可视化概览
|
- 年度报告与可视化概览
|
||||||
- 导出聊天记录为 HTML 等格式
|
- 导出聊天记录为 HTML 等格式
|
||||||
- HTTP API 接口(供开发者集成)
|
- HTTP API 接口(供开发者集成)
|
||||||
|
- 查看完整能力清单:[详细功能](#详细功能清单)
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
若你只想使用成品版本,可前往 Release 下载并安装。
|
||||||
|
|
||||||
|
## 详细功能清单
|
||||||
|
|
||||||
|
当前版本已支持以下能力:
|
||||||
|
|
||||||
|
| 功能模块 | 说明 |
|
||||||
|
|---------|------|
|
||||||
|
| **聊天** | 解密聊天中的图片、视频、实况(仅支持谷歌协议拍摄的实况);支持**修改**、删除**本地**消息;实时刷新最新消息,无需生成解密中间数据库 |
|
||||||
|
| **实时弹窗通知** | 新消息到达时提供桌面弹窗提醒,便于及时查看重要会话,提供黑白名单功能 |
|
||||||
|
| **私聊分析** | 统计好友间消息数量;分析消息类型与发送比例;查看消息时段分布等 |
|
||||||
|
| **群聊分析** | 查看群成员详细信息;分析群内发言排行、活跃时段和媒体内容 |
|
||||||
|
| **年度报告** | 生成按年统计的年度报告,或跨年度的长期历史报告 |
|
||||||
|
| **双人报告** | 选择指定好友,基于双方聊天记录生成专属分析报告 |
|
||||||
|
| **消息导出** | 将微信聊天记录导出为多种格式:JSON、HTML、TXT、Excel、CSV、PGSQL、ChatLab专属格式等 |
|
||||||
|
| **朋友圈** | 解密朋友圈图片、视频、实况;导出朋友圈内容;拦截朋友圈的删除与隐藏操作;突破时间访问限制 |
|
||||||
|
| **联系人** | 导出微信好友、群聊、公众号信息;尝试找回曾经的好友(功能尚不完善) |
|
||||||
|
| **HTTP API 映射** | 将本地消息能力映射为 HTTP API,便于对接外部系统、自动化脚本与二次开发 |
|
||||||
|
|
||||||
## HTTP API
|
## HTTP API
|
||||||
|
|
||||||
@@ -55,13 +76,9 @@ WeFlow 提供本地 HTTP API 服务,支持通过接口查询消息数据,可
|
|||||||
- **访问地址**:`http://127.0.0.1:5031`
|
- **访问地址**:`http://127.0.0.1:5031`
|
||||||
- **支持格式**:原始 JSON 或 [ChatLab](https://chatlab.fun/) 标准格式
|
- **支持格式**:原始 JSON 或 [ChatLab](https://chatlab.fun/) 标准格式
|
||||||
|
|
||||||
📖 完整接口文档:[点击查看](docs/HTTP-API.md)
|
完整接口文档:[点击查看](docs/HTTP-API.md)
|
||||||
|
|
||||||
|
|
||||||
## 快速开始
|
|
||||||
|
|
||||||
若你只想使用成品版本,可前往 Release 下载并安装。
|
|
||||||
|
|
||||||
## 面向开发者
|
## 面向开发者
|
||||||
|
|
||||||
如果你想从源码构建或为项目贡献代码,请遵循以下步骤:
|
如果你想从源码构建或为项目贡献代码,请遵循以下步骤:
|
||||||
|
|||||||
@@ -1533,10 +1533,10 @@ function registerIpcHandlers() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('key:autoGetImageKey', async (event, manualDir?: string) => {
|
ipcMain.handle('key:autoGetImageKey', async (event, manualDir?: string, wxid?: string) => {
|
||||||
return keyService.autoGetImageKey(manualDir, (message) => {
|
return keyService.autoGetImageKey(manualDir, (message) => {
|
||||||
event.sender.send('key:imageKeyStatus', { message })
|
event.sender.send('key:imageKeyStatus', { message })
|
||||||
})
|
}, wxid)
|
||||||
})
|
})
|
||||||
|
|
||||||
// HTTP API 服务
|
// HTTP API 服务
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
// 密钥获取
|
// 密钥获取
|
||||||
key: {
|
key: {
|
||||||
autoGetDbKey: () => ipcRenderer.invoke('key:autoGetDbKey'),
|
autoGetDbKey: () => ipcRenderer.invoke('key:autoGetDbKey'),
|
||||||
autoGetImageKey: (manualDir?: string) => ipcRenderer.invoke('key:autoGetImageKey', manualDir),
|
autoGetImageKey: (manualDir?: string, wxid?: string) => ipcRenderer.invoke('key:autoGetImageKey', manualDir, wxid),
|
||||||
onDbKeyStatus: (callback: (payload: { message: string; level: number }) => void) => {
|
onDbKeyStatus: (callback: (payload: { message: string; level: number }) => void) => {
|
||||||
ipcRenderer.on('key:dbKeyStatus', (_, payload) => callback(payload))
|
ipcRenderer.on('key:dbKeyStatus', (_, payload) => callback(payload))
|
||||||
return () => ipcRenderer.removeAllListeners('key:dbKeyStatus')
|
return () => ipcRenderer.removeAllListeners('key:dbKeyStatus')
|
||||||
|
|||||||
@@ -15,8 +15,16 @@ function getStaticFfmpegPath(): string | null {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const ffmpegStatic = require('ffmpeg-static')
|
const ffmpegStatic = require('ffmpeg-static')
|
||||||
|
|
||||||
if (typeof ffmpegStatic === 'string' && existsSync(ffmpegStatic)) {
|
if (typeof ffmpegStatic === 'string') {
|
||||||
return ffmpegStatic
|
// 修复:如果路径包含 app.asar(打包后),自动替换为 app.asar.unpacked
|
||||||
|
let fixedPath = ffmpegStatic
|
||||||
|
if (fixedPath.includes('app.asar') && !fixedPath.includes('app.asar.unpacked')) {
|
||||||
|
fixedPath = fixedPath.replace('app.asar', 'app.asar.unpacked')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existsSync(fixedPath)) {
|
||||||
|
return fixedPath
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 方法2: 手动构建路径(开发环境)
|
// 方法2: 手动构建路径(开发环境)
|
||||||
|
|||||||
@@ -651,7 +651,8 @@ export class KeyService {
|
|||||||
|
|
||||||
async autoGetImageKey(
|
async autoGetImageKey(
|
||||||
manualDir?: string,
|
manualDir?: string,
|
||||||
onProgress?: (message: string) => void
|
onProgress?: (message: string) => void,
|
||||||
|
wxidParam?: string
|
||||||
): Promise<ImageKeyResult> {
|
): Promise<ImageKeyResult> {
|
||||||
if (!this.ensureWin32()) return { success: false, error: '仅支持 Windows' }
|
if (!this.ensureWin32()) return { success: false, error: '仅支持 Windows' }
|
||||||
if (!this.ensureLoaded()) return { success: false, error: 'wx_key.dll 未加载' }
|
if (!this.ensureLoaded()) return { success: false, error: 'wx_key.dll 未加载' }
|
||||||
@@ -683,20 +684,29 @@ export class KeyService {
|
|||||||
const codes: number[] = accounts[0].keys.map((k: any) => k.code)
|
const codes: number[] = accounts[0].keys.map((k: any) => k.code)
|
||||||
console.log('[ImageKey] codes:', codes, 'DLL wxids:', accounts.map((a: any) => a.wxid))
|
console.log('[ImageKey] codes:', codes, 'DLL wxids:', accounts.map((a: any) => a.wxid))
|
||||||
|
|
||||||
// 从 manualDir 提取前端已配置好的正确 wxid
|
// 优先级: 1. 直接传入的wxidParam 2. 从manualDir提取 3. DLL返回的wxid(可能是unknown)
|
||||||
// 格式: "D:\weixin\xwechat_files\wxid_xxx_1234" → "wxid_xxx_1234"
|
|
||||||
let targetWxid = ''
|
let targetWxid = ''
|
||||||
if (manualDir) {
|
|
||||||
|
// 方案1: 直接使用传入的wxidParam(最优先)
|
||||||
|
if (wxidParam && wxidParam.startsWith('wxid_')) {
|
||||||
|
targetWxid = wxidParam
|
||||||
|
console.log('[ImageKey] 使用直接传入的 wxid:', targetWxid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方案2: 从 manualDir 提取前端已配置好的正确 wxid
|
||||||
|
// 格式: "D:\weixin\xwechat_files\wxid_xxx_1234" → "wxid_xxx_1234"
|
||||||
|
if (!targetWxid && manualDir) {
|
||||||
const dirName = manualDir.replace(/[\\/]+$/, '').split(/[\\/]/).pop() ?? ''
|
const dirName = manualDir.replace(/[\\/]+$/, '').split(/[\\/]/).pop() ?? ''
|
||||||
if (dirName.startsWith('wxid_')) {
|
if (dirName.startsWith('wxid_')) {
|
||||||
targetWxid = dirName
|
targetWxid = dirName
|
||||||
|
console.log('[ImageKey] 从 manualDir 提取 wxid:', targetWxid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 方案3: 回退到 DLL 发现的第一个(可能是 unknown)
|
||||||
if (!targetWxid) {
|
if (!targetWxid) {
|
||||||
// 无法从 manualDir 提取 wxid,回退到 DLL 发现的第一个
|
|
||||||
targetWxid = accounts[0].wxid
|
targetWxid = accounts[0].wxid
|
||||||
console.log('[ImageKey] 无法从 manualDir 提取 wxid,使用 DLL 发现的:', targetWxid)
|
console.log('[ImageKey] 无法获取 wxid,使用 DLL 发现的:', targetWxid)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CleanWxid: 截断到第二个下划线,与 xkey 算法一致
|
// CleanWxid: 截断到第二个下划线,与 xkey 算法一致
|
||||||
|
|||||||
Binary file not shown.
@@ -779,7 +779,7 @@ function SettingsPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const accountPath = wxid ? `${dbPath}/${wxid}` : dbPath;
|
const accountPath = wxid ? `${dbPath}/${wxid}` : dbPath;
|
||||||
const result = await window.electronAPI.key.autoGetImageKey(accountPath)
|
const result = await window.electronAPI.key.autoGetImageKey(accountPath, wxid)
|
||||||
if (result.success && result.aesKey) {
|
if (result.success && result.aesKey) {
|
||||||
if (typeof result.xorKey === 'number') {
|
if (typeof result.xorKey === 'number') {
|
||||||
setImageXorKey(`0x${result.xorKey.toString(16).toUpperCase().padStart(2, '0')}`)
|
setImageXorKey(`0x${result.xorKey.toString(16).toUpperCase().padStart(2, '0')}`)
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
|||||||
try {
|
try {
|
||||||
// 拼接完整的账号目录,确保 KeyService 能准确找到模板文件
|
// 拼接完整的账号目录,确保 KeyService 能准确找到模板文件
|
||||||
const accountPath = wxid ? `${dbPath}/${wxid}` : dbPath
|
const accountPath = wxid ? `${dbPath}/${wxid}` : dbPath
|
||||||
const result = await window.electronAPI.key.autoGetImageKey(accountPath)
|
const result = await window.electronAPI.key.autoGetImageKey(accountPath, wxid)
|
||||||
if (result.success && result.aesKey) {
|
if (result.success && result.aesKey) {
|
||||||
if (typeof result.xorKey === 'number') {
|
if (typeof result.xorKey === 'number') {
|
||||||
setImageXorKey(`0x${result.xorKey.toString(16).toUpperCase().padStart(2, '0')}`)
|
setImageXorKey(`0x${result.xorKey.toString(16).toUpperCase().padStart(2, '0')}`)
|
||||||
|
|||||||
2
src/types/electron.d.ts
vendored
2
src/types/electron.d.ts
vendored
@@ -66,7 +66,7 @@ export interface ElectronAPI {
|
|||||||
}
|
}
|
||||||
key: {
|
key: {
|
||||||
autoGetDbKey: () => Promise<{ success: boolean; key?: string; error?: string; logs?: string[] }>
|
autoGetDbKey: () => Promise<{ success: boolean; key?: string; error?: string; logs?: string[] }>
|
||||||
autoGetImageKey: (manualDir?: string) => Promise<{ success: boolean; xorKey?: number; aesKey?: string; error?: string }>
|
autoGetImageKey: (manualDir?: string, wxid?: string) => Promise<{ success: boolean; xorKey?: number; aesKey?: string; error?: string }>
|
||||||
onDbKeyStatus: (callback: (payload: { message: string; level: number }) => void) => () => void
|
onDbKeyStatus: (callback: (payload: { message: string; level: number }) => void) => () => void
|
||||||
onImageKeyStatus: (callback: (payload: { message: string }) => void) => () => void
|
onImageKeyStatus: (callback: (payload: { message: string }) => void) => () => void
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user