├── .editorconfig ├── .eslintignore ├── .eslintrc.cjs ├── .gitignore ├── .prettierignore ├── .prettierrc.yaml ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── electron-builder.yml ├── electron.vite.config.ts ├── main.spec ├── mychat.spec ├── package-lock.json ├── package.json ├── py ├── mychat.py ├── requirements.txt ├── utils │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-310.pyc │ │ ├── _loger.cpython-310.pyc │ │ ├── common_utils.cpython-310.pyc │ │ ├── ctypes_utils.cpython-310.pyc │ │ └── memory_search.cpython-310.pyc │ ├── _loger.py │ ├── common_utils.py │ ├── ctypes_utils.py │ └── memory_search.py └── wx │ ├── __init__.py │ ├── __pycache__ │ ├── __init__.cpython-310.pyc │ ├── decryption.cpython-310.pyc │ ├── parse.cpython-310.pyc │ ├── realtimeMsg.cpython-310.pyc │ └── wx_info.cpython-310.pyc │ ├── decryption.py │ ├── parse.py │ ├── realtimeMsg.py │ ├── tools │ ├── libcrypto-1_1-x64.dll │ └── realTime.exe │ └── wx_info.py ├── resources ├── icon.png ├── preview1.png ├── preview2.png ├── preview3.png ├── preview4.png └── statistics.png ├── src ├── main │ ├── dbUtils │ │ ├── util.ts │ │ └── wxDatabse.ts │ ├── index.ts │ ├── ipcFunction.ts │ └── pyServer.ts ├── preload │ ├── index.d.ts │ └── index.ts └── renderer │ ├── index.html │ └── src │ ├── App.vue │ ├── assets │ ├── HarmonyOS_Sans_SC_Regular.woff2 │ ├── base.css │ ├── electron.svg │ ├── icon.png │ ├── main.css │ └── wavy-lines.svg │ ├── components │ ├── pulseLoading.vue │ └── sidebar.vue │ ├── env.d.ts │ ├── main.ts │ ├── page.d.ts │ ├── pages │ ├── chat │ │ ├── components │ │ │ ├── actions.vue │ │ │ ├── actions │ │ │ │ ├── AIAnaly.vue │ │ │ │ ├── _com_ │ │ │ │ │ ├── chatDistribution.vue │ │ │ │ │ ├── chatSender.vue │ │ │ │ │ ├── chatTime.vue │ │ │ │ │ ├── chatTypeChart.vue │ │ │ │ │ ├── export.ts │ │ │ │ │ └── exportHtml.vue │ │ │ │ ├── export.vue │ │ │ │ ├── statistic.vue │ │ │ │ └── suggest.vue │ │ │ ├── content.vue │ │ │ ├── largeStatistics.vue │ │ │ ├── largeStatistics │ │ │ │ ├── chatTypeChart.vue │ │ │ │ ├── dailyChatDistribution.vue │ │ │ │ ├── dailyChatPeriods.vue │ │ │ │ ├── useAI.ts │ │ │ │ └── wordDistribution.vue │ │ │ ├── session.vue │ │ │ └── singleMsg.vue │ │ ├── index.vue │ │ └── store │ │ │ ├── index.ts │ │ │ ├── msg.ts │ │ │ └── session.ts │ ├── contact │ │ ├── components │ │ │ └── info.vue │ │ ├── index.vue │ │ └── store │ │ │ └── index.ts │ ├── launch.vue │ └── setting │ │ ├── components │ │ ├── AISetting.vue │ │ ├── account.vue │ │ └── statement.vue │ │ └── index.vue │ ├── plugins │ └── elLoading.ts │ ├── store │ ├── ai.ts │ └── wx.ts │ └── util │ ├── indexDB.js │ ├── momentCh.js │ ├── router.ts │ └── util.js ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.web.json ├── type.ts └── visualizer.html /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out 4 | .gitignore 5 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution') 3 | 4 | module.exports = { 5 | extends: [ 6 | 'eslint:recommended', 7 | 'plugin:vue/vue3-recommended', 8 | '@electron-toolkit', 9 | '@electron-toolkit/eslint-config-ts/eslint-recommended', 10 | '@vue/eslint-config-typescript/recommended', 11 | '@vue/eslint-config-prettier' 12 | ], 13 | rules: { 14 | 'vue/require-default-prop': 'off', 15 | 'vue/multi-word-component-names': 'off' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out 4 | .DS_Store 5 | *.log* 6 | build 7 | pydist 8 | wxDb 9 | **/__pycache__/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | pnpm-lock.yaml 4 | LICENSE.md 5 | tsconfig.json 6 | tsconfig.*.json 7 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | singleQuote: true 2 | semi: false 3 | printWidth: 100 4 | trailingComma: none 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug Main Process", 6 | "type": "node", 7 | "request": "launch", 8 | "cwd": "${workspaceRoot}", 9 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite", 10 | "windows": { 11 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd" 12 | }, 13 | "runtimeArgs": ["--sourcemap"], 14 | "env": { 15 | "REMOTE_DEBUGGING_PORT": "9222" 16 | } 17 | }, 18 | { 19 | "name": "Debug Renderer Process", 20 | "port": 9222, 21 | "request": "attach", 22 | "type": "chrome", 23 | "webRoot": "${workspaceFolder}/src/renderer", 24 | "timeout": 60000, 25 | "presentation": { 26 | "hidden": true 27 | } 28 | } 29 | ], 30 | "compounds": [ 31 | { 32 | "name": "Debug All", 33 | "configurations": ["Debug Main Process", "Debug Renderer Process"], 34 | "presentation": { 35 | "order": 1 36 | } 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.defaultFormatter": "esbenp.prettier-vscode" 4 | }, 5 | "[javascript]": { 6 | "editor.defaultFormatter": "esbenp.prettier-vscode" 7 | }, 8 | "[json]": { 9 | "editor.defaultFormatter": "esbenp.prettier-vscode" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 DingHui-Li 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ##### `欢迎为项目点亮⭐️,您的支持是我持续改进的动力!` 2 | 3 | # mychat 4 | 5 | 使用Electron(vue3+ts)和python开发的具有现代化UI和友好交互的**微信聊天数据智能管理分析工具**。 6 | 7 | ## 功能 8 | 9 | 1. 统计信息 10 | 11 | - ✅聊天日期分布图 12 | - ✅消息类型占比 13 | - ✅发送者消息数量统计 14 | - ✅聊天时间统计 15 | - ✅导出统计图表(v1.1) 16 | - ✅AI解读统计图表(v1.1.1) 17 | 18 | 2. 聊天导出 19 | 20 | - ✅导出为JSON 21 | - ✅导出为HTML 22 | - ✅导出为TXT(v1.1.2) 23 | - ✅导出为长图(v1.1.3) 24 | - ✅导出文字类型消息 25 | - ❌导出图片类型消息 26 | - ❌导出语音类型消息 27 | 28 | 3. AI赋能 29 | 30 | - ✅AI回复建议 31 | 通过分析上下文,为用户提供智能、高效、符合语境的回复建议,提升沟通效率。 32 | - ✅AI分析 33 | 对聊天内容进行从核心话题与事件、 关系分析、聊天节奏、互动方式、情感表达、用户画像、隐含需求与建议等方面的深度分析,帮助用户更好地理解对话内容。 34 | - ❌AI搜索 35 | 快速、精准地搜索聊天记录中的特定内容,帮助用户快速定位关键信息,提升信息检索效率。 36 | 37 | 4. 其他功能 38 | 39 | - ✅获取最新微信消息 40 | - ✅查看所有联系人 41 | - ✅黑暗模式 42 | 43 | ## 预览 44 | 45 | 统计分析报告: 46 | 47 | 运行截图: 48 | 49 | 50 | 51 | 52 | 53 | ## 构建 54 | 55 | - 开发环境 56 | windows11,node18,python3 57 | - 步骤1.`npm install` 2.`pip install -r ./py/requirements.txt` 3. 开发 58 | `npm run dev ` 4. 打包 # For windows 59 | `npm run build:win` 60 | 61 | ###### 只测试了在64位windows环境的开发打包运行流程 62 | 63 | ## 感谢 64 | 65 | 本项目中的微信数据库解密python脚本来自[PyWxDump](https://github.com/xaoyaoo/PyWxDump)项目。 66 | -------------------------------------------------------------------------------- /electron-builder.yml: -------------------------------------------------------------------------------- 1 | appId: com.electron.app 2 | productName: myChat 3 | directories: 4 | buildResources: build 5 | files: 6 | - '!**/.vscode/*' 7 | - '!src/*' 8 | - '!electron.vite.config.{js,ts,mjs,cjs}' 9 | - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' 10 | - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' 11 | - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}' 12 | asarUnpack: 13 | - resources/** 14 | win: 15 | executableName: myChat 16 | nsis: 17 | artifactName: ${name}-${version}-setup.${ext} 18 | shortcutName: ${productName} 19 | uninstallDisplayName: ${productName} 20 | createDesktopShortcut: always 21 | installerIcon:'build/icons/icon.ico' 22 | uninstallerIcon:'build/icons/icon.ico' 23 | mac: 24 | entitlementsInherit: build/entitlements.mac.plist 25 | extendInfo: 26 | - NSCameraUsageDescription: Application requests access to the device's camera. 27 | - NSMicrophoneUsageDescription: Application requests access to the device's microphone. 28 | - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. 29 | - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder. 30 | notarize: false 31 | dmg: 32 | artifactName: ${name}-${version}.${ext} 33 | linux: 34 | target: 35 | - AppImage 36 | - snap 37 | - deb 38 | maintainer: electronjs.org 39 | category: Utility 40 | appImage: 41 | artifactName: ${name}-${version}.${ext} 42 | npmRebuild: false 43 | publish: 44 | provider: generic 45 | url: https://example.com/auto-updates 46 | -------------------------------------------------------------------------------- /electron.vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { defineConfig, externalizeDepsPlugin } from 'electron-vite' 3 | import vue from '@vitejs/plugin-vue' 4 | import { visualizer } from 'rollup-plugin-visualizer' 5 | 6 | export default defineConfig({ 7 | main: { 8 | plugins: [externalizeDepsPlugin()] 9 | }, 10 | preload: { 11 | plugins: [externalizeDepsPlugin()] 12 | }, 13 | renderer: { 14 | resolve: { 15 | alias: { 16 | '@renderer': resolve('src/renderer/src') 17 | } 18 | }, 19 | plugins: [ 20 | vue() 21 | // visualizer({ 22 | // open: true, 23 | // filename: 'visualizer.html' 24 | // }) 25 | ] 26 | } 27 | }) 28 | -------------------------------------------------------------------------------- /main.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | 4 | a = Analysis( 5 | ['py\\main.py'], 6 | pathex=[], 7 | binaries=[], 8 | datas=[('py/wx/tools/realTime.exe', 'wx/tools'), ('py/wx/tools/libcrypto-1_1-x64.dll', 'wx/tools')], 9 | hiddenimports=[], 10 | hookspath=[], 11 | hooksconfig={}, 12 | runtime_hooks=[], 13 | excludes=[], 14 | noarchive=False, 15 | optimize=0, 16 | ) 17 | pyz = PYZ(a.pure) 18 | 19 | exe = EXE( 20 | pyz, 21 | a.scripts, 22 | [], 23 | exclude_binaries=True, 24 | name='main', 25 | debug=False, 26 | bootloader_ignore_signals=False, 27 | strip=False, 28 | upx=True, 29 | console=True, 30 | disable_windowed_traceback=False, 31 | argv_emulation=False, 32 | target_arch=None, 33 | codesign_identity=None, 34 | entitlements_file=None, 35 | ) 36 | coll = COLLECT( 37 | exe, 38 | a.binaries, 39 | a.datas, 40 | strip=False, 41 | upx=True, 42 | upx_exclude=[], 43 | name='main', 44 | ) 45 | -------------------------------------------------------------------------------- /mychat.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | 4 | a = Analysis( 5 | ['py\\mychat.py'], 6 | pathex=[], 7 | binaries=[], 8 | datas=[('py/wx/tools/realTime.exe', 'wx/tools'), ('py/wx/tools/libcrypto-1_1-x64.dll', 'wx/tools')], 9 | hiddenimports=[], 10 | hookspath=[], 11 | hooksconfig={}, 12 | runtime_hooks=[], 13 | excludes=[], 14 | noarchive=False, 15 | optimize=0, 16 | ) 17 | pyz = PYZ(a.pure) 18 | 19 | exe = EXE( 20 | pyz, 21 | a.scripts, 22 | [], 23 | exclude_binaries=True, 24 | name='mychat', 25 | debug=False, 26 | bootloader_ignore_signals=False, 27 | strip=False, 28 | upx=True, 29 | console=True, 30 | disable_windowed_traceback=False, 31 | argv_emulation=False, 32 | target_arch=None, 33 | codesign_identity=None, 34 | entitlements_file=None, 35 | ) 36 | coll = COLLECT( 37 | exe, 38 | a.binaries, 39 | a.datas, 40 | strip=False, 41 | upx=True, 42 | upx_exclude=[], 43 | name='mychat', 44 | ) 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mychat", 3 | "version": "1.1.5", 4 | "description": "An Electron application with Vue and TypeScript", 5 | "main": "./out/main/index.js", 6 | "author": "linkssr.cn", 7 | "homepage": "https://electron-vite.org", 8 | "build": { 9 | "asar": true, 10 | "asarUnpack": [ 11 | "out/pydist/**/*" 12 | ], 13 | "compression": "maximum", 14 | "icon": "build/icons/icon.ico", 15 | "win": { 16 | "icon": "build/icons/icon.ico" 17 | }, 18 | "files": [ 19 | "out/**/*" 20 | ], 21 | "extraResources": [ 22 | { 23 | "from": "resources/", 24 | "to": "resources/", 25 | "filter": [ 26 | "!**/*.pak" 27 | ] 28 | } 29 | ] 30 | }, 31 | "scripts": { 32 | "format": "prettier --write .", 33 | "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix", 34 | "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", 35 | "typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false", 36 | "typecheck": "npm run typecheck:node && npm run typecheck:web", 37 | "start": "electron-vite preview", 38 | "dev": "electron-vite dev --watch", 39 | "build": "electron-vite build && npm run build-python && npm run generate-icon", 40 | "postinstall": "electron-builder install-app-deps", 41 | "build:unpack": "npm run build && electron-builder --dir", 42 | "build:win": "npm run build && electron-builder --win", 43 | "build:mac": "npm run build && electron-builder --mac", 44 | "build:linux": "npm run build && electron-builder --linux", 45 | "build-python": "pyinstaller --add-data py/wx/tools/realTime.exe;wx/tools --add-data py/wx/tools/libcrypto-1_1-x64.dll;wx/tools ./py/mychat.py --clean --distpath ./out/pydist", 46 | "generate-icon": "electron-icon-builder --input=./resources/icon.png --output=build --flatten" 47 | }, 48 | "dependencies": { 49 | "@electron-toolkit/preload": "^3.0.0", 50 | "@electron-toolkit/utils": "^3.0.0", 51 | "@element-plus/icons-vue": "^2.3.1", 52 | "@vue/server-renderer": "^3.5.13", 53 | "buffer": "^6.0.3", 54 | "echarts": "^5.6.0", 55 | "echarts-wordcloud": "^2.1.0", 56 | "electron-icon-builder": "^2.0.1", 57 | "element-plus": "^2.9.4", 58 | "fast-xml-parser": "^4.5.1", 59 | "html2canvas": "^1.4.1", 60 | "less": "^4.2.2", 61 | "less-loader": "^12.2.0", 62 | "marked": "^15.0.7", 63 | "moment": "^2.30.1", 64 | "openai": "^4.84.0", 65 | "sql.js": "^1.12.0", 66 | "virtua": "^0.40.0", 67 | "vue-router": "^4.5.0" 68 | }, 69 | "devDependencies": { 70 | "@electron-toolkit/eslint-config": "^1.0.2", 71 | "@electron-toolkit/eslint-config-ts": "^2.0.0", 72 | "@electron-toolkit/tsconfig": "^1.0.1", 73 | "@rushstack/eslint-patch": "^1.10.3", 74 | "@types/node": "^20.14.8", 75 | "@vitejs/plugin-vue": "^5.0.5", 76 | "@vue/eslint-config-prettier": "^9.0.0", 77 | "@vue/eslint-config-typescript": "^13.0.0", 78 | "electron": "^31.0.2", 79 | "electron-builder": "^24.13.3", 80 | "electron-vite": "^2.3.0", 81 | "eslint": "^8.57.0", 82 | "eslint-plugin-vue": "^9.26.0", 83 | "prettier": "^3.3.2", 84 | "rollup-plugin-visualizer": "^5.14.0", 85 | "typescript": "^5.5.2", 86 | "vite": "^5.3.1", 87 | "vue": "^3.4.30", 88 | "vue-tsc": "^2.0.22" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /py/mychat.py: -------------------------------------------------------------------------------- 1 | import glob 2 | from http.server import BaseHTTPRequestHandler, HTTPServer 3 | import json 4 | import os 5 | import sys 6 | from urllib.parse import parse_qs, urlparse 7 | from wx import get_wx_info,batch_decrypt,merge_real_time_db 8 | import jieba 9 | 10 | class ApiHTTPRequestHandler(BaseHTTPRequestHandler): 11 | 12 | def do_GET(self): 13 | if self.path == '/api/wxinfo': 14 | self.send_response(200) 15 | self.send_header('Content-type', 'application/json') 16 | self.send_header('Access-Control-Allow-Origin', '*') 17 | self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') 18 | self.send_header('Access-Control-Allow-Headers', 'Content-Type') 19 | self.end_headers() 20 | wxinfo=get_wx_info() 21 | response = {'data': {'wxinfo':wxinfo}} 22 | self.wfile.write(json.dumps(response).encode()) 23 | return 24 | if self.path.find('/api/syncrealtimedb')!=-1: 25 | self.send_response(200) 26 | self.send_header('Content-type', 'application/json') 27 | self.send_header('Access-Control-Allow-Origin', '*') 28 | self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') 29 | self.send_header('Access-Control-Allow-Headers', 'Content-Type') 30 | self.end_headers() 31 | parsed_path = urlparse(self.path) 32 | query_params = parse_qs(parsed_path.query) 33 | response = {'msg':"success"} 34 | try: 35 | filtered_files = glob.glob(query_params['db_path'][0]+'/**/MSG*.db', recursive=True) 36 | db_name=filtered_files[-2].replace(query_params['db_path'][0]+'\Msg','').replace("MSG", "de_MSG") 37 | out_path=query_params['out_path'][0]+db_name 38 | # merge_real_time_db(query_params['key'][0], 39 | # out_path,{"db_path":filtered_files[-2]} 40 | # ) 41 | for path in filtered_files: 42 | db_name=path.replace(query_params['db_path'][0]+'\Msg','').replace("MSG", "de_MSG") 43 | out_path=query_params['out_path'][0]+db_name 44 | merge_real_time_db(query_params['key'][0], 45 | out_path,{"db_path":path} 46 | ) 47 | # merge_real_time_db(query_params['key'][0], 48 | # out_path, 49 | # list(map(lambda item:{"db_path":item},filtered_files)) 50 | # ) 51 | # merge_real_time_db(query_params['key'][0],query_params['out_path'][0]+'/de_MicroMsg.db',{"db_path":query_params['db_path'][0]+'/Msg/MicroMsg.db'}) 52 | response['out_path']=out_path 53 | except Exception as e: 54 | self.send_response(400) 55 | self.end_headers() 56 | self.wfile.write('') 57 | return 58 | self.wfile.write(json.dumps(response).encode()) 59 | else: 60 | self.send_response(404) 61 | self.send_header('Content-type', 'application/json') 62 | self.end_headers() 63 | response = {'error': 'Not Found'} 64 | self.wfile.write(json.dumps(response).encode()) 65 | def do_POST(self): 66 | if self.path == '/api/asyncwxdb': 67 | self.send_response(200) 68 | self.send_header('Content-type', 'application/json') 69 | self.send_header('Access-Control-Allow-Origin', '*') 70 | self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') 71 | self.send_header('Access-Control-Allow-Headers', 'Content-Type') 72 | self.end_headers() 73 | content_length = int(self.headers.get('Content-Length', 0)) 74 | post_data = self.rfile.read(content_length) 75 | response={} 76 | try: 77 | params = json.loads(post_data.decode('utf-8')) 78 | if params['wxPath']: 79 | outPath=decryptionWxDb(params['key'],params['wxPath'],params['wxid']) 80 | response={'data':{'outDbPath':outPath}} 81 | except Exception as e: 82 | self.send_response(400) 83 | self.end_headers() 84 | self.wfile.write(e) 85 | return 86 | self.wfile.write(json.dumps(response).encode()) 87 | # if self.path == '/api/parseextra': 88 | # self.send_response(200) 89 | # self.send_header('Content-type', 'application/json') 90 | # self.send_header('Access-Control-Allow-Origin', '*') 91 | # self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') 92 | # self.send_header('Access-Control-Allow-Headers', 'Content-Type') 93 | # self.end_headers() 94 | # content_length = int(self.headers.get('Content-Length', 0)) 95 | # post_data = self.rfile.read(content_length) 96 | # result="" 97 | # try: 98 | # # data = json.loads(post_data.decode('utf-8')) 99 | # # print(data) 100 | # result=decompress_CompressContent(post_data) 101 | # except json.JSONDecodeError: 102 | # self.send_response(400) 103 | # self.end_headers() 104 | # self.wfile.write(b'Invalid JSON') 105 | # return 106 | # self.wfile.write(json.dumps({"data":result}).encode()) 107 | if self.path == '/api/wordcut': 108 | self.send_response(200) 109 | self.send_header('Content-type', 'application/json') 110 | self.send_header('Access-Control-Allow-Origin', '*') 111 | self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') 112 | self.send_header('Access-Control-Allow-Headers', 'Content-Type') 113 | self.end_headers() 114 | content_length = int(self.headers.get('Content-Length', 0)) 115 | post_data = self.rfile.read(content_length) 116 | result="" 117 | try: 118 | params = json.loads(post_data.decode('utf-8')) 119 | result=jieba.cut(params['content']) 120 | def is_not_special_char(s): 121 | if s.isalpha() or '\u4e00' <= s <= '\u9fff': 122 | return True 123 | result=list(filter(is_not_special_char,result)) 124 | except Exception as e: 125 | self.send_response(400) 126 | self.end_headers() 127 | self.wfile.write('') 128 | return 129 | self.wfile.write(json.dumps({"data":result}).encode()) 130 | def do_OPTIONS(self): 131 | # 处理预检请求 132 | self.send_response(200) 133 | self.send_header('Access-Control-Allow-Origin', '*') 134 | self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') 135 | self.send_header('Access-Control-Allow-Headers', 'Content-Type') 136 | self.end_headers() 137 | 138 | def run(server_class=HTTPServer, handler_class=ApiHTTPRequestHandler, port=4556): 139 | server_address = ('', port) 140 | httpd = server_class(server_address, handler_class) 141 | print(f'Starting httpd server on port {port}...') 142 | httpd.serve_forever() 143 | 144 | def decryptionWxDb(key,dbPath,wxid): 145 | outPath=os.getcwd()+'/wxDb/'+wxid 146 | if not os.path.exists(outPath): 147 | os.makedirs(outPath) 148 | if not os.path.exists(outPath+'/Multi'): 149 | os.makedirs(outPath+'/Multi') 150 | batch_decrypt(key, dbPath+'/Msg',outPath ) 151 | batch_decrypt(key,dbPath+'/Msg/Multi', outPath+'/Multi' ) 152 | return outPath 153 | 154 | if __name__ == '__main__': 155 | run() 156 | -------------------------------------------------------------------------------- /py/requirements.txt: -------------------------------------------------------------------------------- 1 | pycryptodomex 2 | pywin32 3 | blackboxprotobuf 4 | lz4 5 | jieba -------------------------------------------------------------------------------- /py/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*-# 2 | # ------------------------------------------------------------------------------- 3 | # Name: __init__.py.py 4 | # Description: 5 | # Author: xaoyaoo 6 | # Date: 2024/07/23 7 | # ------------------------------------------------------------------------------- 8 | 9 | from .common_utils import verify_key, get_exe_version, get_exe_bit, wx_core_error 10 | from .ctypes_utils import get_process_list, get_memory_maps, get_process_exe_path, \ 11 | get_file_version_info 12 | from .memory_search import search_memory 13 | from ._loger import wx_core_loger 14 | 15 | CORE_DB_TYPE = ["MicroMsg", "MSG", "MediaMSG", "OpenIMContact", "OpenIMMsg", "PublicMsg", "OpenIMMedia", 16 | "Favorite", "Sns"] 17 | -------------------------------------------------------------------------------- /py/utils/__pycache__/__init__.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DingHui-Li/mychat/5128f174ba4cb6baf77f95714cd2efbfc962b66f/py/utils/__pycache__/__init__.cpython-310.pyc -------------------------------------------------------------------------------- /py/utils/__pycache__/_loger.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DingHui-Li/mychat/5128f174ba4cb6baf77f95714cd2efbfc962b66f/py/utils/__pycache__/_loger.cpython-310.pyc -------------------------------------------------------------------------------- /py/utils/__pycache__/common_utils.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DingHui-Li/mychat/5128f174ba4cb6baf77f95714cd2efbfc962b66f/py/utils/__pycache__/common_utils.cpython-310.pyc -------------------------------------------------------------------------------- /py/utils/__pycache__/ctypes_utils.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DingHui-Li/mychat/5128f174ba4cb6baf77f95714cd2efbfc962b66f/py/utils/__pycache__/ctypes_utils.cpython-310.pyc -------------------------------------------------------------------------------- /py/utils/__pycache__/memory_search.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DingHui-Li/mychat/5128f174ba4cb6baf77f95714cd2efbfc962b66f/py/utils/__pycache__/memory_search.cpython-310.pyc -------------------------------------------------------------------------------- /py/utils/_loger.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*-# 2 | # ------------------------------------------------------------------------------- 3 | # Name: _loger.py 4 | # Description: 5 | # Author: xaoyaoo 6 | # Date: 2024/07/23 7 | # ------------------------------------------------------------------------------- 8 | import logging 9 | 10 | wx_core_loger = logging.getLogger("wx_core") 11 | -------------------------------------------------------------------------------- /py/utils/common_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*-# 2 | # ------------------------------------------------------------------------------- 3 | # Name: utils.py 4 | # Description: 5 | # Author: xaoyaoo 6 | # Date: 2023/12/25 7 | # ------------------------------------------------------------------------------- 8 | import os 9 | import re 10 | import hmac 11 | import sys 12 | import traceback 13 | import hashlib 14 | from ._loger import wx_core_loger 15 | 16 | if sys.platform == "win32": 17 | from win32com.client import Dispatch 18 | else: 19 | Dispatch = None 20 | 21 | 22 | def wx_core_error(func): 23 | """ 24 | 错误处理装饰器 25 | :param func: 26 | :return: 27 | """ 28 | def wrapper(*args, **kwargs): 29 | try: 30 | return func(*args, **kwargs) 31 | except Exception as e: 32 | wx_core_loger.error(f"wx_core_error: {e}", exc_info=True) 33 | return None 34 | return wrapper 35 | 36 | 37 | def verify_key(key, wx_db_path): 38 | """ 39 | 验证key是否正确 40 | """ 41 | KEY_SIZE = 32 42 | DEFAULT_PAGESIZE = 4096 43 | DEFAULT_ITER = 64000 44 | with open(wx_db_path, "rb") as file: 45 | blist = file.read(5000) 46 | salt = blist[:16] 47 | pk = hashlib.pbkdf2_hmac("sha1", key, salt, DEFAULT_ITER, KEY_SIZE) 48 | first = blist[16:DEFAULT_PAGESIZE] 49 | mac_salt = bytes([(salt[i] ^ 58) for i in range(16)]) 50 | pk = hashlib.pbkdf2_hmac("sha1", pk, mac_salt, 2, KEY_SIZE) 51 | hash_mac = hmac.new(pk, first[:-32], hashlib.sha1) 52 | hash_mac.update(b'\x01\x00\x00\x00') 53 | if hash_mac.digest() != first[-32:-12]: 54 | return False 55 | return True 56 | 57 | @wx_core_error 58 | def get_exe_version(file_path): 59 | """ 60 | 获取 PE 文件的版本号 61 | :param file_path: PE 文件路径(可执行文件) 62 | :return: 如果遇到错误则返回 63 | """ 64 | if not os.path.exists(file_path): 65 | return "None" 66 | file_version = Dispatch("Scripting.FileSystemObject").GetFileVersion(file_path) 67 | return file_version 68 | 69 | 70 | def find_all(c: bytes, string: bytes, base_addr=0): 71 | """ 72 | 查找字符串中所有子串的位置 73 | :param c: 子串 b'123' 74 | :param string: 字符串 b'123456789123' 75 | :return: 76 | """ 77 | return [base_addr + m.start() for m in re.finditer(re.escape(c), string)] 78 | 79 | 80 | def get_exe_bit(file_path): 81 | """ 82 | # 获取exe文件的位数 83 | PE 文件的位数: 32 位或 64 位 84 | :param file_path: PE 文件路径(可执行文件) 85 | :return: 如果遇到错误则返回 64 86 | """ 87 | try: 88 | with open(file_path, 'rb') as f: 89 | dos_header = f.read(2) 90 | if dos_header != b'MZ': 91 | print('get exe bit error: Invalid PE file') 92 | return 64 93 | # Seek to the offset of the PE signature 94 | f.seek(60) 95 | pe_offset_bytes = f.read(4) 96 | pe_offset = int.from_bytes(pe_offset_bytes, byteorder='little') 97 | 98 | # Seek to the Machine field in the PE header 99 | f.seek(pe_offset + 4) 100 | machine_bytes = f.read(2) 101 | machine = int.from_bytes(machine_bytes, byteorder='little') 102 | 103 | if machine == 0x14c: 104 | return 32 105 | elif machine == 0x8664: 106 | return 64 107 | else: 108 | print('get exe bit error: Unknown architecture: %s' % hex(machine)) 109 | return 64 110 | except IOError: 111 | print('get exe bit error: File not found or cannot be opened') 112 | return 64 113 | 114 | -------------------------------------------------------------------------------- /py/utils/memory_search.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import ctypes.wintypes as wintypes 3 | import logging 4 | import re 5 | import sys 6 | 7 | # 定义常量 8 | PROCESS_QUERY_INFORMATION = 0x0400 9 | PROCESS_VM_READ = 0x0010 10 | 11 | PAGE_EXECUTE = 0x10 12 | PAGE_EXECUTE_READ = 0x20 13 | PAGE_EXECUTE_READWRITE = 0x40 14 | PAGE_EXECUTE_WRITECOPY = 0x80 15 | PAGE_NOACCESS = 0x01 16 | PAGE_READONLY = 0x02 17 | PAGE_READWRITE = 0x04 18 | PAGE_WRITECOPY = 0x08 19 | PAGE_GUARD = 0x100 20 | PAGE_NOCACHE = 0x200 21 | PAGE_WRITECOMBINE = 0x400 22 | 23 | MEM_COMMIT = 0x1000 24 | MEM_FREE = 0x10000 25 | MEM_RESERVE = 0x2000 26 | MEM_DECOMMIT = 0x4000 27 | MEM_RELEASE = 0x8000 28 | 29 | 30 | # 定义结构体 31 | class MEMORY_BASIC_INFORMATION(ctypes.Structure): 32 | _fields_ = [ 33 | ("BaseAddress", ctypes.c_void_p), 34 | ("AllocationBase", ctypes.c_void_p), 35 | ("AllocationProtect", wintypes.DWORD), 36 | ("RegionSize", ctypes.c_size_t), 37 | ("State", wintypes.DWORD), 38 | ("Protect", wintypes.DWORD), 39 | ("Type", wintypes.DWORD), 40 | ] 41 | 42 | 43 | # 加载Windows API函数 44 | kernel32 = ctypes.WinDLL('kernel32', use_last_error=True) 45 | 46 | OpenProcess = kernel32.OpenProcess 47 | OpenProcess.restype = wintypes.HANDLE 48 | OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD] 49 | 50 | ReadProcessMemory = kernel32.ReadProcessMemory 51 | 52 | VirtualQueryEx = kernel32.VirtualQueryEx 53 | VirtualQueryEx.restype = ctypes.c_size_t 54 | VirtualQueryEx.argtypes = [wintypes.HANDLE, ctypes.c_void_p, ctypes.POINTER(MEMORY_BASIC_INFORMATION), ctypes.c_size_t] 55 | 56 | CloseHandle = kernel32.CloseHandle 57 | CloseHandle.restype = wintypes.BOOL 58 | CloseHandle.argtypes = [wintypes.HANDLE] 59 | 60 | 61 | def search_memory(hProcess, pattern=br'\\Msg\\FTSContact', max_num=100,start_address=0x0,end_address=0x7FFFFFFFFFFFFFFF): 62 | """ 63 | 在进程内存中搜索字符串 64 | :param p: 进程ID或者进程句柄 65 | :param pattern: 要搜索的字符串 66 | :param max_num: 最多找到的数量 67 | """ 68 | result = [] 69 | # 打开进程 70 | if not hProcess: 71 | raise ctypes.WinError(ctypes.get_last_error()) 72 | 73 | mbi = MEMORY_BASIC_INFORMATION() 74 | 75 | address = start_address 76 | max_address = end_address if sys.maxsize > 2 ** 32 else 0x7fff0000 77 | pattern = re.compile(pattern) 78 | 79 | while address < max_address: 80 | if VirtualQueryEx(hProcess, address, ctypes.byref(mbi), ctypes.sizeof(mbi)) == 0: 81 | break 82 | # 读取内存数据 83 | allowed_protections = [PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE, PAGE_READWRITE, PAGE_READONLY, ] 84 | if mbi.State != MEM_COMMIT or mbi.Protect not in allowed_protections: 85 | address += mbi.RegionSize 86 | continue 87 | 88 | # 使用正确的类型来避免OverflowError 89 | base_address_c = ctypes.c_ulonglong(mbi.BaseAddress) 90 | region_size_c = ctypes.c_size_t(mbi.RegionSize) 91 | 92 | page_bytes = ctypes.create_string_buffer(mbi.RegionSize) 93 | bytes_read = ctypes.c_size_t() 94 | 95 | if ReadProcessMemory(hProcess, base_address_c, page_bytes, region_size_c, ctypes.byref(bytes_read)) == 0: 96 | address += mbi.RegionSize 97 | continue 98 | # 搜索字符串 re print(page_bytes.raw) 99 | find = [address + match.start() for match in pattern.finditer(page_bytes, re.DOTALL)] 100 | if find: 101 | result.extend(find) 102 | if len(result) >= max_num: 103 | break 104 | address += mbi.RegionSize 105 | return result 106 | 107 | 108 | if __name__ == '__main__': 109 | # 示例用法 110 | pid = 29320 # 将此替换为你要查询的进程ID 111 | try: 112 | maps = search_memory(pid) 113 | print(len(maps)) 114 | for m in maps: 115 | print(hex(m)) 116 | except Exception as e: 117 | logging.error(e, exc_info=True) 118 | -------------------------------------------------------------------------------- /py/wx/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .wx_info import get_wx_info, get_wx_db, get_core_db 3 | from .decryption import batch_decrypt, decrypt 4 | from .parse import get_BytesExtra,decompress_CompressContent 5 | from .realtimeMsg import merge_real_time_db -------------------------------------------------------------------------------- /py/wx/__pycache__/__init__.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DingHui-Li/mychat/5128f174ba4cb6baf77f95714cd2efbfc962b66f/py/wx/__pycache__/__init__.cpython-310.pyc -------------------------------------------------------------------------------- /py/wx/__pycache__/decryption.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DingHui-Li/mychat/5128f174ba4cb6baf77f95714cd2efbfc962b66f/py/wx/__pycache__/decryption.cpython-310.pyc -------------------------------------------------------------------------------- /py/wx/__pycache__/parse.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DingHui-Li/mychat/5128f174ba4cb6baf77f95714cd2efbfc962b66f/py/wx/__pycache__/parse.cpython-310.pyc -------------------------------------------------------------------------------- /py/wx/__pycache__/realtimeMsg.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DingHui-Li/mychat/5128f174ba4cb6baf77f95714cd2efbfc962b66f/py/wx/__pycache__/realtimeMsg.cpython-310.pyc -------------------------------------------------------------------------------- /py/wx/__pycache__/wx_info.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DingHui-Li/mychat/5128f174ba4cb6baf77f95714cd2efbfc962b66f/py/wx/__pycache__/wx_info.cpython-310.pyc -------------------------------------------------------------------------------- /py/wx/decryption.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*-# 2 | # ------------------------------------------------------------------------------- 3 | # Name: getwxinfo.py 4 | # Description: 5 | # Author: xaoyaoo 6 | # Date: 2023/08/21 7 | # 微信数据库采用的加密算法是256位的AES-CBC。数据库的默认的页大小是4096字节即4KB,其中每一个页都是被单独加解密的。 8 | # 加密文件的每一个页都有一个随机的初始化向量,它被保存在每一页的末尾。 9 | # 加密文件的每一页都存有着消息认证码,算法使用的是HMAC-SHA1(安卓数据库使用的是SHA512)。它也被保存在每一页的末尾。 10 | # 每一个数据库文件的开头16字节都保存了一段唯一且随机的盐值,作为HMAC的验证和数据的解密。 11 | # 为了保证数据部分长度是16字节即AES块大小的整倍数,每一页的末尾将填充一段空字节,使得保留字段的长度为48字节。 12 | # 综上,加密文件结构为第一页4KB数据前16字节为盐值,紧接着4032字节数据,再加上16字节IV和20字节HMAC以及12字节空字节;而后的页均是4048字节长度的加密数据段和48字节的保留段。 13 | # ------------------------------------------------------------------------------- 14 | import hmac 15 | import hashlib 16 | import os 17 | from typing import Union, List 18 | from Cryptodome.Cipher import AES 19 | # from Crypto.Cipher import AES # 如果上面的导入失败,可以尝试使用这个 20 | 21 | from utils import wx_core_error, wx_core_loger 22 | 23 | SQLITE_FILE_HEADER = "SQLite format 3\x00" # SQLite文件头 24 | 25 | KEY_SIZE = 32 26 | DEFAULT_PAGESIZE = 4096 27 | 28 | 29 | # 通过密钥解密数据库 30 | @wx_core_error 31 | def decrypt(key: str, db_path: str, out_path: str): 32 | """ 33 | 通过密钥解密数据库 34 | :param key: 密钥 64位16进制字符串 35 | :param db_path: 待解密的数据库路径(必须是文件) 36 | :param out_path: 解密后的数据库输出路径(必须是文件) 37 | :return: 38 | """ 39 | if not os.path.exists(db_path) or not os.path.isfile(db_path): 40 | return False, f"[-] db_path:'{db_path}' File not found!" 41 | if not os.path.exists(os.path.dirname(out_path)): 42 | return False, f"[-] out_path:'{out_path}' File not found!" 43 | 44 | if len(key) != 64: 45 | return False, f"[-] key:'{key}' Len Error!" 46 | 47 | password = bytes.fromhex(key.strip()) 48 | 49 | try: 50 | with open(db_path, "rb") as file: 51 | blist = file.read() 52 | except Exception as e: 53 | return False, f"[-] db_path:'{db_path}' {e}!" 54 | 55 | salt = blist[:16] 56 | first = blist[16:4096] 57 | if len(salt) != 16: 58 | return False, f"[-] db_path:'{db_path}' File Error!" 59 | mac_salt = bytes([(salt[i] ^ 58) for i in range(16)]) 60 | byteHmac = hashlib.pbkdf2_hmac("sha1", password, salt, 64000, KEY_SIZE) 61 | mac_key = hashlib.pbkdf2_hmac("sha1", byteHmac, mac_salt, 2, KEY_SIZE) 62 | hash_mac = hmac.new(mac_key, blist[16:4064], hashlib.sha1) 63 | hash_mac.update(b'\x01\x00\x00\x00') 64 | 65 | if hash_mac.digest() != first[-32:-12]: 66 | return False, f"[-] Key Error! (key:'{key}'; db_path:'{db_path}'; out_path:'{out_path}' )" 67 | 68 | with open(out_path, "wb") as deFile: 69 | deFile.write(SQLITE_FILE_HEADER.encode()) 70 | for i in range(0, len(blist), 4096): 71 | tblist = blist[i:i + 4096] if i > 0 else blist[16:i + 4096] 72 | deFile.write(AES.new(byteHmac, AES.MODE_CBC, tblist[-48:-32]).decrypt(tblist[:-48])) 73 | deFile.write(tblist[-48:]) 74 | 75 | return True, [db_path, out_path, key] 76 | 77 | @wx_core_error 78 | def batch_decrypt(key: str, db_path: Union[str, List[str]], out_path: str, is_print: bool = False): 79 | """ 80 | 批量解密数据库 81 | :param key: 密钥 64位16进制字符串 82 | :param db_path: 待解密的数据库路径(文件或文件夹) 83 | :param out_path: 解密后的数据库输出路径(文件夹) 84 | :param is_logging: 是否打印日志 85 | :return: (bool, [[input_db_path, output_db_path, key],...]) 86 | """ 87 | if not isinstance(key, str) or not isinstance(out_path, str) or not os.path.exists(out_path) or len(key) != 64: 88 | error = f"[-] (key:'{key}' or out_path:'{out_path}') Error!" 89 | wx_core_loger.error(error, exc_info=True) 90 | return False, error 91 | 92 | process_list = [] 93 | 94 | if isinstance(db_path, str): 95 | if not os.path.exists(db_path): 96 | error = f"[-] db_path:'{db_path}' not found!" 97 | wx_core_loger.error(error, exc_info=True) 98 | return False, error 99 | 100 | if os.path.isfile(db_path): 101 | inpath = db_path 102 | outpath = os.path.join(out_path, 'de_' + os.path.basename(db_path)) 103 | process_list.append([key, inpath, outpath]) 104 | 105 | elif os.path.isdir(db_path): 106 | for root, dirs, files in os.walk(db_path): 107 | for file in files: 108 | inpath = os.path.join(root, file) 109 | rel = os.path.relpath(root, db_path) 110 | outpath = os.path.join(out_path, rel, 'de_' + file) 111 | 112 | if not os.path.exists(os.path.dirname(outpath)): 113 | os.makedirs(os.path.dirname(outpath)) 114 | process_list.append([key, inpath, outpath]) 115 | else: 116 | error = f"[-] db_path:'{db_path}' Error " 117 | wx_core_loger.error(error, exc_info=True) 118 | return False, error 119 | 120 | elif isinstance(db_path, list): 121 | rt_path = os.path.commonprefix(db_path) 122 | if not os.path.exists(rt_path): 123 | rt_path = os.path.dirname(rt_path) 124 | 125 | for inpath in db_path: 126 | if not os.path.exists(inpath): 127 | error = f"[-] db_path:'{db_path}' not found!" 128 | wx_core_loger.error(error, exc_info=True) 129 | return False, error 130 | 131 | inpath = os.path.normpath(inpath) 132 | rel = os.path.relpath(os.path.dirname(inpath), rt_path) 133 | outpath = os.path.join(out_path, rel, 'de_' + os.path.basename(inpath)) 134 | if not os.path.exists(os.path.dirname(outpath)): 135 | os.makedirs(os.path.dirname(outpath)) 136 | process_list.append([key, inpath, outpath]) 137 | else: 138 | error = f"[-] db_path:'{db_path}' Error " 139 | wx_core_loger.error(error, exc_info=True) 140 | return False, error 141 | 142 | result = [] 143 | for i in process_list: 144 | result.append(decrypt(*i)) # 解密 145 | 146 | # 删除空文件夹 147 | for root, dirs, files in os.walk(out_path, topdown=False): 148 | for dir in dirs: 149 | if not os.listdir(os.path.join(root, dir)): 150 | os.rmdir(os.path.join(root, dir)) 151 | 152 | if is_print: 153 | print("=" * 32) 154 | success_count = 0 155 | fail_count = 0 156 | for code, ret in result: 157 | if code == False: 158 | print(ret) 159 | fail_count += 1 160 | else: 161 | print(f'[+] "{ret[0]}" -> "{ret[1]}"') 162 | success_count += 1 163 | print("-" * 32) 164 | print(f"[+] 共 {len(result)} 个文件, 成功 {success_count} 个, 失败 {fail_count} 个") 165 | print("=" * 32) 166 | return True, result 167 | -------------------------------------------------------------------------------- /py/wx/parse.py: -------------------------------------------------------------------------------- 1 | import blackboxprotobuf 2 | import lz4.block 3 | 4 | def get_BytesExtra(BytesExtra): 5 | if BytesExtra is None or not isinstance(BytesExtra, bytes): 6 | return None 7 | try: 8 | deserialize_data, message_type = blackboxprotobuf.decode_message(BytesExtra) 9 | return deserialize_data 10 | except Exception as e: 11 | print(e) 12 | 13 | def decompress_CompressContent(data): 14 | """ 15 | 解压缩Msg:CompressContent内容 16 | :param data: CompressContent内容 bytes 17 | :return: 18 | """ 19 | if data is None or not isinstance(data, bytes): 20 | return None 21 | try: 22 | dst = lz4.block.decompress(data, uncompressed_size=len(data) << 8) 23 | dst = dst.replace(b'\x00', b'') # 已经解码完成后,还含有0x00的部分,要删掉,要不后面ET识别的时候会报错 24 | uncompressed_data = dst.decode('utf-8', errors='ignore') 25 | return uncompressed_data 26 | except Exception as e: 27 | return data.decode('utf-8', errors='ignore') -------------------------------------------------------------------------------- /py/wx/realtimeMsg.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import os 4 | import subprocess 5 | 6 | def merge_real_time_db(key, merge_path: str, db_paths: [dict] or dict, real_time_exe_path: str = None): 7 | """ 8 | 合并实时数据库消息,暂时只支持64位系统 9 | :param key: 解密密钥 10 | :param merge_path: 合并后的数据库路径 11 | :param db_paths: [dict] or dict eg: {'wxid': 'wxid_***', 'db_type': 'MicroMsg', 12 | 'db_path': 'C:\**\wxid_***\Msg\MicroMsg.db', 'wxid_dir': 'C:\***\wxid_***'} 13 | :param real_time_exe_path: 实时数据库合并工具路径 14 | :return: 15 | """ 16 | try: 17 | import platform 18 | except: 19 | raise ImportError("未找到模块 platform") 20 | # 判断系统位数是否为64位,如果不是则抛出异常 21 | if platform.architecture()[0] != '64bit': 22 | raise Exception("System is not 64-bit.") 23 | 24 | if isinstance(db_paths, dict): 25 | db_paths = [db_paths] 26 | 27 | merge_path = os.path.abspath(merge_path) # 合并后的数据库路径,必须为绝对路径 28 | merge_path_base = os.path.dirname(merge_path) # 合并后的数据库路径 29 | if not os.path.exists(merge_path_base): 30 | os.makedirs(merge_path_base) 31 | 32 | endbs = [] 33 | for db_info in db_paths: 34 | db_path = os.path.abspath(db_info['db_path']) 35 | if not os.path.exists(db_path): 36 | # raise FileNotFoundError("数据库不存在") 37 | continue 38 | endbs.append(os.path.abspath(db_path)) 39 | endbs = '" "'.join(list(set(endbs))) 40 | 41 | if not os.path.exists(real_time_exe_path if real_time_exe_path else ""): 42 | current_path = os.path.dirname(__file__) # 获取当前文件夹路径 43 | real_time_exe_path = os.path.join(current_path, "tools", "realTime.exe") 44 | if not os.path.exists(real_time_exe_path): 45 | raise FileNotFoundError("未找到实时数据库合并工具") 46 | real_time_exe_path = os.path.abspath(real_time_exe_path) 47 | 48 | # 调用cmd命令 49 | cmd = f'{real_time_exe_path} "{key}" "{merge_path}" "{endbs}"' 50 | # os.system(cmd) 51 | # wx_core_loger.info(f"合并实时数据库命令:{cmd}") 52 | p = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=merge_path_base, 53 | creationflags=subprocess.CREATE_NO_WINDOW) 54 | out, err = p.communicate() # 查看返回值 55 | if out and out.decode("utf-8").find("SUCCESS") >= 0: 56 | print(f"合并实时数据库成功{out}") 57 | return True, merge_path 58 | else: 59 | print(f"合并实时数据库失败\n{out}\n{err}") 60 | return False, (out, err) -------------------------------------------------------------------------------- /py/wx/tools/libcrypto-1_1-x64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DingHui-Li/mychat/5128f174ba4cb6baf77f95714cd2efbfc962b66f/py/wx/tools/libcrypto-1_1-x64.dll -------------------------------------------------------------------------------- /py/wx/tools/realTime.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DingHui-Li/mychat/5128f174ba4cb6baf77f95714cd2efbfc962b66f/py/wx/tools/realTime.exe -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DingHui-Li/mychat/5128f174ba4cb6baf77f95714cd2efbfc962b66f/resources/icon.png -------------------------------------------------------------------------------- /resources/preview1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DingHui-Li/mychat/5128f174ba4cb6baf77f95714cd2efbfc962b66f/resources/preview1.png -------------------------------------------------------------------------------- /resources/preview2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DingHui-Li/mychat/5128f174ba4cb6baf77f95714cd2efbfc962b66f/resources/preview2.png -------------------------------------------------------------------------------- /resources/preview3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DingHui-Li/mychat/5128f174ba4cb6baf77f95714cd2efbfc962b66f/resources/preview3.png -------------------------------------------------------------------------------- /resources/preview4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DingHui-Li/mychat/5128f174ba4cb6baf77f95714cd2efbfc962b66f/resources/preview4.png -------------------------------------------------------------------------------- /resources/statistics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DingHui-Li/mychat/5128f174ba4cb6baf77f95714cd2efbfc962b66f/resources/statistics.png -------------------------------------------------------------------------------- /src/main/dbUtils/util.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | const path = require('path') 3 | 4 | //解析查询结果为对象 5 | export function parseSQLResult(result) { 6 | let list: Array = [] 7 | try { 8 | let keys = result[0].columns 9 | for (const item of result[0].values) { 10 | let t = {} 11 | for (const i in item) { 12 | t[keys[i]] = item[i] 13 | } 14 | list.push(t) 15 | } 16 | } catch {} 17 | return list 18 | } 19 | 20 | //按照正则查找文件 21 | export function findFiles(dir, pattern) { 22 | let results: Array = [] 23 | const items = fs.readdirSync(dir) 24 | for (const item of items) { 25 | const fullPath = path.join(dir, item) 26 | const stat = fs.statSync(fullPath) 27 | 28 | if (stat.isDirectory()) { 29 | results = results.concat(findFiles(fullPath, pattern)) 30 | } else if (pattern.test(item)) { 31 | results.push(fullPath) 32 | } 33 | } 34 | return results 35 | } 36 | 37 | //解析消息类型 38 | export function parseMsgType(type, subtype) { 39 | return { 40 | '1,0': '文本', 41 | '3,0': '图片', 42 | '34,0': '语音', 43 | '37,0': '添加好友', 44 | '42,0': '推荐公众号', 45 | '43,0': '视频', 46 | '47,0': '动画表情', 47 | '48,0': '位置', 48 | 49 | '49,0': '文件', 50 | '49,1': '粘贴的文本', 51 | '49,3': '(分享)音乐', 52 | '49,4': '(分享)卡片式链接', 53 | '49,5': '(分享)卡片式链接', 54 | '49,6': '文件', 55 | '49,7': '游戏相关', 56 | '49,8': '用户上传的GIF表情', 57 | '49,15': '未知-49,15', 58 | '49,17': '位置共享', 59 | '49,19': '合并转发的聊天记录', 60 | '49,24': '(分享)笔记', 61 | '49,33': '(分享)小程序', 62 | '49,36': '(分享)小程序', 63 | '49,40': '(分享)收藏夹', 64 | '49,44': '(分享)小说(猜)', 65 | '49,50': '(分享)视频号名片', 66 | '49,51': '(分享)视频号视频', 67 | '49,53': '接龙', 68 | '49,57': '引用回复', 69 | '49,63': '视频号直播或直播回放', 70 | '49,74': '文件(猜)', 71 | '49,87': '群公告', 72 | '49,88': '视频号直播或直播回放等', 73 | '49,2000': '转账', 74 | '49,2003': '赠送红包封面', 75 | 76 | '50,0': '语音通话', 77 | '65,0': '企业微信打招呼(猜)', 78 | '66,0': '企业微信添加好友(猜)', 79 | 80 | '10000,0': '系统通知', 81 | '10000,1': '消息撤回1', 82 | '10000,4': '拍一拍', 83 | '10000,5': '消息撤回5', 84 | '10000,6': '消息撤回6', 85 | '10000,33': '消息撤回33', 86 | '10000,36': '消息撤回36', 87 | '10000,57': '消息撤回57', 88 | '10000,8000': '邀请加群', 89 | '11000,0': '未知-11000,0' 90 | }[`${type},${subtype}`] 91 | } 92 | -------------------------------------------------------------------------------- /src/main/dbUtils/wxDatabse.ts: -------------------------------------------------------------------------------- 1 | //微信数据库分析:https://github.com/xaoyaoo/PyWxDump/blob/master/doc/wx%E6%95%B0%E6%8D%AE%E5%BA%93%E7%AE%80%E8%BF%B0.md 2 | import Database from 'sql.js/dist/sql-wasm.js' 3 | import { ipcMain } from 'electron' 4 | import fs from 'fs' 5 | import { findFiles, parseSQLResult, parseMsgType } from './util' 6 | import { XMLParser } from 'fast-xml-parser' 7 | 8 | let MicroMsgDb: Database 9 | let MsgDbPool: Array = [] 10 | 11 | let wxDBPath: string 12 | let SQLInstance: Database 13 | 14 | export async function init(path) { 15 | SQLInstance = await new Database() 16 | wxDBPath = path 17 | MicroMsgDb = new SQLInstance.Database(fs.readFileSync(path + '/de_MicroMsg.db')) 18 | await initMsgDb() 19 | console.log('wxDB init success') 20 | } 21 | export async function initMsgDb() { 22 | MsgDbPool = [] 23 | let allMsgDbFileName = findFiles(wxDBPath, /^de_MSG\d+\.db$/) 24 | allMsgDbFileName.forEach((filePath) => { 25 | const db = new SQLInstance.Database(fs.readFileSync(filePath)) 26 | MsgDbPool.push(db) 27 | }) 28 | console.log('msgDB init success;total:', allMsgDbFileName) 29 | } 30 | 31 | ipcMain.handle('initWxDb', async (event, query) => { 32 | try { 33 | return init(query) 34 | } catch (err) { 35 | console.log(err) 36 | return false 37 | } 38 | }) 39 | ipcMain.handle('initWxMsgDb', async (event, query) => { 40 | try { 41 | return initMsgDb() 42 | } catch (err) { 43 | console.log(err) 44 | return false 45 | } 46 | }) 47 | 48 | ipcMain.handle('dbQuery', (event, { query = '', dbname = 'de_MicroMsg' }) => { 49 | try { 50 | const db = { de_MicroMsg: MicroMsgDb }[dbname] 51 | if (!db) { 52 | throw `${dbname}不存在或未读取` 53 | } 54 | console.log('exec query:' + query) 55 | let result = db.exec(query) 56 | return { result: parseSQLResult(result) } 57 | } catch (err) { 58 | return { error: err } 59 | } 60 | }) 61 | 62 | //由于聊天数据是分库的,所以单独查询 63 | ipcMain.handle('findMsgDb', async (event, username = '') => { 64 | if (!MsgDbPool.length) { 65 | throw `聊天数据库不存在或未读取` 66 | } 67 | if (!username) { 68 | throw `查询对象不能为空` 69 | } 70 | let list: Array = [] 71 | let msgIds = {} 72 | let tempUserInfo = {} 73 | for (const db of MsgDbPool) { 74 | console.log(`查询msgDB,`, username) 75 | const sql = ` 76 | SELECT MsgSvrID,Type,SubType,IsSender,CreateTime,StrTalker,StrContent,DisplayContent,CompressContent,BytesExtra 77 | FROM MSG 78 | WHERE StrTalker=='${username}' 79 | ORDER BY CreateTime ASC 80 | ` 81 | let result = db.exec(sql) 82 | result = parseSQLResult(result) 83 | let xmlParser = new XMLParser({ 84 | ignoreAttributes: false 85 | }) 86 | for (const index in result) { 87 | let item = result[index] 88 | if (msgIds[item.MsgSvrID]) { 89 | continue //去重 90 | } 91 | msgIds[item.MsgSvrID] = true 92 | item.TypeName = parseMsgType(item.Type, item.SubType) 93 | if (!item.talker && item.BytesExtra && !item.IsSender) { 94 | try { 95 | item.talker = getTalker(item.BytesExtra) 96 | if (tempUserInfo[item.talker]) { 97 | item.talkerInfo = tempUserInfo[item.talker] 98 | } else { 99 | item.talkerInfo = parseSQLResult( 100 | MicroMsgDb.exec(` 101 | SELECT A.smallHeadImgUrl,A.bigHeadImgUrl,B.UserName,B.Remark,B.NickName 102 | FROM ContactHeadImgUrl A INNER JOIN Contact B 103 | ON A.usrName=B.UserName 104 | WHERE A.UsrName=='${item.talker}' 105 | `) 106 | )[0] 107 | if (item.talkerInfo) { 108 | item.talkerInfo.avatar = 109 | item.talkerInfo.smallHeadImgUrl || item.talkerInfo.bigHeadImgUrl 110 | item.talkerInfo.strNickName = item.talkerInfo.Remark || item.talkerInfo.NickName 111 | delete item.talkerInfo.smallHeadImgUrl 112 | delete item.talkerInfo.bigHeadImgUrl 113 | } 114 | tempUserInfo[item.talker] = item.talkerInfo 115 | delete item.CompressContent 116 | } 117 | } catch (e) { 118 | console.log(e) 119 | } 120 | } 121 | try { 122 | if (item.StrContent) { 123 | let xmlParseResult = xmlParser.parse(item.StrContent) 124 | item.StrContent = xmlParseResult.revokemsg || item.StrContent 125 | if (xmlParseResult.msg?.emoji) { 126 | item.StrContent = xmlParseResult.msg?.emoji['@_cdnurl'] 127 | } 128 | // item.xmlParseResult = xmlParseResult 129 | } 130 | } catch (err) { 131 | console.log(err) 132 | } 133 | delete item.BytesExtra 134 | list.push(item) 135 | } 136 | } 137 | // console.log(list) 138 | return list.sort((a, b) => a.CreateTime - b.CreateTime) 139 | }) 140 | 141 | function getTalker(BytesExtra) { 142 | if (!BytesExtra || !(BytesExtra instanceof Uint8Array)) { 143 | return null 144 | } 145 | try { 146 | let str = new TextDecoder('utf-8').decode(BytesExtra) 147 | str = str.replace(/[^a-zA-Z0-9\s.,_!?@-\\<\\>$""''=\\/]/g, '').trim() 148 | if (str.includes('')) { 149 | return str.split('')[0] 150 | } else { 151 | return str.split('$')[0] 152 | } 153 | } catch { 154 | return null 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/main/index.ts: -------------------------------------------------------------------------------- 1 | import { app, shell, BrowserWindow, ipcMain } from 'electron' 2 | import { join } from 'path' 3 | import { electronApp, optimizer, is } from '@electron-toolkit/utils' 4 | import { createPyServer, exitPyServer } from './pyServer' 5 | import './dbUtils/wxDatabse' 6 | import './ipcFunction' 7 | 8 | function createWindow(): void { 9 | // Create the browser window. 10 | const mainWindow = new BrowserWindow({ 11 | width: 1080, 12 | height: 870, 13 | show: false, 14 | autoHideMenuBar: true, 15 | icon: join(__dirname, '../../resources/icon.png'), 16 | webPreferences: { 17 | preload: join(__dirname, '../preload/index.js'), 18 | sandbox: false, 19 | devTools: true 20 | } 21 | }) 22 | 23 | mainWindow.on('ready-to-show', () => { 24 | mainWindow.show() 25 | }) 26 | 27 | mainWindow.webContents.setWindowOpenHandler((details) => { 28 | shell.openExternal(details.url) 29 | return { action: 'deny' } 30 | }) 31 | 32 | // HMR for renderer base on electron-vite cli. 33 | // Load the remote URL for development or the local html file for production. 34 | if (is.dev && process.env['ELECTRON_RENDERER_URL']) { 35 | mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) 36 | } else { 37 | mainWindow.loadFile(join(__dirname, '../renderer/index.html')) 38 | } 39 | } 40 | 41 | // This method will be called when Electron has finished 42 | // initialization and is ready to create browser windows. 43 | // Some APIs can only be used after this event occurs. 44 | app.whenReady().then(() => { 45 | // Set app user model id for windows 46 | electronApp.setAppUserModelId('com.electron') 47 | 48 | // Default open or close DevTools by F12 in development 49 | // and ignore CommandOrControl + R in production. 50 | // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils 51 | app.on('browser-window-created', (_, window) => { 52 | optimizer.watchWindowShortcuts(window) 53 | }) 54 | 55 | // IPC test 56 | ipcMain.on('ping', () => console.log('pong')) 57 | 58 | createPyServer() 59 | createWindow() 60 | 61 | app.on('activate', function () { 62 | // On macOS it's common to re-create a window in the app when the 63 | // dock icon is clicked and there are no other windows open. 64 | if (BrowserWindow.getAllWindows().length === 0) createWindow() 65 | }) 66 | }) 67 | 68 | // Quit when all windows are closed, except on macOS. There, it's common 69 | // for applications and their menu bar to stay active until the user quits 70 | // explicitly with Cmd + Q. 71 | app.on('window-all-closed', () => { 72 | exitPyServer() 73 | if (process.platform !== 'darwin') { 74 | app.quit() 75 | } 76 | }) 77 | 78 | // In this file you can include the rest of your app"s specific main process 79 | // code. You can also put them in separate files and require them here. 80 | -------------------------------------------------------------------------------- /src/main/ipcFunction.ts: -------------------------------------------------------------------------------- 1 | import { ipcMain, app, shell, dialog } from 'electron' 2 | import path, { resolve } from 'path' 3 | import { createPyServer, exitPyServer } from '../main/pyServer' 4 | const fs = require('fs') 5 | 6 | ipcMain.handle('appQuit', async (event, query) => { 7 | app.quit() 8 | }) 9 | 10 | ipcMain.handle('openUrl', async (event, url) => { 11 | if (url) { 12 | shell.openExternal(url) 13 | } 14 | }) 15 | 16 | ipcMain.handle('exportFile', async (event, { content = '', name = '*.json', type = ['json'] }) => { 17 | return new Promise((resolve, reject) => { 18 | if (!content || typeof content != 'string') return reject('json为空或不为字符串') 19 | dialog 20 | .showSaveDialog({ 21 | title: '保存 JSON 文件', 22 | defaultPath: path.join(__dirname, name), 23 | filters: [ 24 | { name: 'JSON 文件', extensions: type }, 25 | { name: '所有文件', extensions: ['*'] } 26 | ] 27 | }) 28 | .then((result) => { 29 | if (!result.canceled && result.filePath) { 30 | // 将 JSON 数据写入文件 31 | fs.writeFile(result.filePath, content, (err) => { 32 | if (err) { 33 | dialog.showErrorBox('错误', '保存文件失败') 34 | reject('保存文件失败:') 35 | } else { 36 | console.log('文件保存成功:', result.filePath) 37 | resolve(true) 38 | } 39 | }) 40 | } else { 41 | reject('已取消') 42 | } 43 | }) 44 | .catch((err) => { 45 | reject('文件保存对话框出错') 46 | }) 47 | }) 48 | }) 49 | 50 | ipcMain.handle('initPyServer', async (event, url) => { 51 | exitPyServer() 52 | return createPyServer() 53 | }) 54 | -------------------------------------------------------------------------------- /src/main/pyServer.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'electron' 2 | 3 | const path = require('path') 4 | let pyServer: any = null 5 | 6 | export async function createPyServer() { 7 | try { 8 | let pythonExecutable = 'mychat' 9 | if (process.platform === 'win32') { 10 | pythonExecutable = 'mychat.exe' 11 | } 12 | let script = '' 13 | if (app.isPackaged) { 14 | script = path.join(__dirname, '../pydist/mychat', pythonExecutable) 15 | pyServer = require('child_process').execFile(script) 16 | } else { 17 | script = path.join(__dirname, '../../py', 'mychat.py') 18 | pyServer = require('child_process').spawn('python', [script]) 19 | } 20 | if (pyServer != null) { 21 | console.log('py process success') 22 | return script 23 | } 24 | } catch { 25 | return false 26 | } 27 | return false 28 | } 29 | 30 | export function exitPyServer() { 31 | pyServer?.kill() 32 | pyServer = null 33 | } 34 | -------------------------------------------------------------------------------- /src/preload/index.d.ts: -------------------------------------------------------------------------------- 1 | import { ElectronAPI } from '@electron-toolkit/preload' 2 | 3 | declare global { 4 | interface Window { 5 | electron: ElectronAPI 6 | api: unknown 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/preload/index.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge, ipcRenderer } from 'electron' 2 | import { electronAPI } from '@electron-toolkit/preload' 3 | 4 | // Use `contextBridge` APIs to expose Electron APIs to 5 | // renderer only if context isolation is enabled, otherwise 6 | // just add to the DOM global. 7 | if (process.contextIsolated) { 8 | //默认 9 | try { 10 | contextBridge.exposeInMainWorld('electron', electronAPI) 11 | contextBridge.exposeInMainWorld('appQuit', () => ipcRenderer.invoke('appQuit')) 12 | contextBridge.exposeInMainWorld('initWxDb', (path) => ipcRenderer.invoke('initWxDb', path)) 13 | contextBridge.exposeInMainWorld('dbQuery', (params) => ipcRenderer.invoke('dbQuery', params)) 14 | contextBridge.exposeInMainWorld('initPyServer', (params) => 15 | ipcRenderer.invoke('initPyServer', params) 16 | ) 17 | contextBridge.exposeInMainWorld('initWxMsgDb', (params) => 18 | ipcRenderer.invoke('initWxMsgDb', params) 19 | ) 20 | contextBridge.exposeInMainWorld('exportFile', (params) => 21 | ipcRenderer.invoke('exportFile', params) 22 | ) 23 | contextBridge.exposeInMainWorld('findMsgDb', (params) => 24 | ipcRenderer.invoke('findMsgDb', params) 25 | ) 26 | contextBridge.exposeInMainWorld('openUrl', (url) => ipcRenderer.invoke('openUrl', url)) 27 | } catch (error) { 28 | console.error(error) 29 | } 30 | } else { 31 | // @ts-ignore (define in dts) 32 | window.electron = electronAPI 33 | } 34 | -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MyChat-v1.1.5 6 | 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/renderer/src/App.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 23 | -------------------------------------------------------------------------------- /src/renderer/src/assets/HarmonyOS_Sans_SC_Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DingHui-Li/mychat/5128f174ba4cb6baf77f95714cd2efbfc962b66f/src/renderer/src/assets/HarmonyOS_Sans_SC_Regular.woff2 -------------------------------------------------------------------------------- /src/renderer/src/assets/base.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --ev-c-white: #ffffff; 3 | --ev-c-white-soft: #f8f8f8; 4 | --ev-c-white-mute: #f2f2f2; 5 | 6 | --ev-c-black: #1b1b1f; 7 | --ev-c-black-soft: #222222; 8 | --ev-c-black-mute: #282828; 9 | 10 | --ev-c-gray-1: #515c67; 11 | --ev-c-gray-2: #414853; 12 | --ev-c-gray-3: #32363f; 13 | 14 | --ev-c-text-1: rgba(255, 255, 245, 0.86); 15 | --ev-c-text-2: rgba(235, 235, 245, 0.6); 16 | --ev-c-text-3: rgba(235, 235, 245, 0.38); 17 | 18 | --ev-button-alt-border: transparent; 19 | --ev-button-alt-text: var(--ev-c-text-1); 20 | --ev-button-alt-bg: var(--ev-c-gray-3); 21 | --ev-button-alt-hover-border: transparent; 22 | --ev-button-alt-hover-text: var(--ev-c-text-1); 23 | --ev-button-alt-hover-bg: var(--ev-c-gray-2); 24 | } 25 | 26 | :root { 27 | --color-background: var(--ev-c-black); 28 | --color-background-soft: var(--ev-c-black-soft); 29 | --color-background-mute: var(--ev-c-black-mute); 30 | 31 | --color-text: var(--ev-c-text-1); 32 | } 33 | 34 | *, 35 | *::before, 36 | *::after { 37 | box-sizing: border-box; 38 | margin: 0; 39 | font-weight: normal; 40 | } 41 | 42 | ul { 43 | list-style: none; 44 | } 45 | 46 | body { 47 | min-height: 100vh; 48 | color: var(--color-text); 49 | background: var(--color-background); 50 | line-height: 1.6; 51 | font-family: 52 | Inter, 53 | -apple-system, 54 | BlinkMacSystemFont, 55 | 'Segoe UI', 56 | Roboto, 57 | Oxygen, 58 | Ubuntu, 59 | Cantarell, 60 | 'Fira Sans', 61 | 'Droid Sans', 62 | 'Helvetica Neue', 63 | sans-serif; 64 | text-rendering: optimizeLegibility; 65 | -webkit-font-smoothing: antialiased; 66 | -moz-osx-font-smoothing: grayscale; 67 | } 68 | -------------------------------------------------------------------------------- /src/renderer/src/assets/electron.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/renderer/src/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DingHui-Li/mychat/5128f174ba4cb6baf77f95714cd2efbfc962b66f/src/renderer/src/assets/icon.png -------------------------------------------------------------------------------- /src/renderer/src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import './base.css'; 2 | @font-face { 3 | font-family: 'HarmonyOS_Sans_SC_Regular'; 4 | font-weight: normal; 5 | src: url('./HarmonyOS_Sans_SC_Regular.woff2') format('truetype'); 6 | } 7 | 8 | * { 9 | font-family: HarmonyOS_Sans_SC_Regular !important; 10 | } 11 | 12 | body { 13 | display: flex; 14 | align-items: center; 15 | justify-content: center; 16 | overflow: hidden; 17 | background-image: url('./wavy-lines.svg'); 18 | background-size: cover; 19 | user-select: none; 20 | --el-color-primary: #7879ef; 21 | --el-color-primary-light-3: #7879ef90; 22 | --el-color-primary-light-5: #7879ef80; 23 | } 24 | 25 | code { 26 | font-weight: 600; 27 | padding: 3px 5px; 28 | border-radius: 2px; 29 | background-color: #eee; 30 | font-family: 31 | ui-monospace, 32 | SFMono-Regular, 33 | SF Mono, 34 | Menlo, 35 | Consolas, 36 | Liberation Mono, 37 | monospace; 38 | font-size: 85%; 39 | } 40 | 41 | #app { 42 | display: flex; 43 | align-items: center; 44 | justify-content: center; 45 | flex-direction: column; 46 | margin-bottom: 80px; 47 | } 48 | .vue { 49 | background: -webkit-linear-gradient(315deg, #42d392 25%, #647eff); 50 | background-clip: text; 51 | -webkit-background-clip: text; 52 | -webkit-text-fill-color: transparent; 53 | font-weight: 700; 54 | } 55 | 56 | .ts { 57 | background: -webkit-linear-gradient(315deg, #3178c6 45%, #f0dc4e); 58 | background-clip: text; 59 | -webkit-background-clip: text; 60 | -webkit-text-fill-color: transparent; 61 | font-weight: 700; 62 | } 63 | 64 | @media (max-width: 720px) { 65 | .text { 66 | font-size: 20px; 67 | } 68 | } 69 | 70 | @media (max-width: 620px) { 71 | .versions { 72 | display: none; 73 | } 74 | } 75 | 76 | @media (max-width: 350px) { 77 | .tip, 78 | .actions { 79 | display: none; 80 | } 81 | } 82 | 83 | ::-webkit-scrollbar { 84 | width: 5px; 85 | height: 10px; 86 | } 87 | 88 | ::-webkit-scrollbar-track { 89 | background: #fff; 90 | } 91 | 92 | ::-webkit-scrollbar-thumb { 93 | background: #eee; 94 | border-radius: 10px; 95 | } 96 | 97 | ::-webkit-scrollbar-thumb:hover { 98 | background: #e0e0e0; 99 | width: 10px; 100 | height: 10px; 101 | } 102 | -------------------------------------------------------------------------------- /src/renderer/src/assets/wavy-lines.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/renderer/src/components/pulseLoading.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /src/renderer/src/components/sidebar.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | MyChat 5 | 6 | 7 | 8 | 9 | 10 | {{ item.label }} 11 | 12 | 13 | 15 | 16 | 17 | 18 | 设置 19 | 20 | 21 | 22 | 黑暗模式 23 | 24 | 25 | 26 | 27 | 28 | 29 | AI 30 | 31 | 32 | 33 | 34 | 90 | -------------------------------------------------------------------------------- /src/renderer/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { ComponentOptions, DefineComponent } from 'vue' 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | // const component: DefineComponent<{}, {}, any> 7 | // export default component 8 | const componentOptions: ComponentOptions 9 | export default componentOptions 10 | } 11 | -------------------------------------------------------------------------------- /src/renderer/src/main.ts: -------------------------------------------------------------------------------- 1 | import './assets/main.css' 2 | import router from './util/router' 3 | import ElementPlus from 'element-plus' 4 | import 'element-plus/dist/index.css' 5 | import WrapElLoading from './plugins/elLoading' 6 | import './util/indexDb.js' 7 | import './util/util.js' 8 | 9 | import { createApp } from 'vue' 10 | import App from './App.vue' 11 | 12 | const app = createApp(App) 13 | app.use(router) 14 | app.use(ElementPlus) 15 | app.use(WrapElLoading) 16 | app.mount('#app') 17 | -------------------------------------------------------------------------------- /src/renderer/src/page.d.ts: -------------------------------------------------------------------------------- 1 | type Msg = { 2 | index: number 3 | MsgSvrID: string 4 | CreateTime: number 5 | DisplayContent: string 6 | IsSender: number 7 | StrContent: string 8 | StrTalker: string 9 | Type: number 10 | SubType: number 11 | TypeName: string 12 | talker: string 13 | talkerInfo: { 14 | Remark: string 15 | avatar: string 16 | strNickName: string 17 | UserName: string 18 | } 19 | } 20 | 21 | type Session = { 22 | Remark: string 23 | strUsrName: string 24 | strNickName: string 25 | strContent: string 26 | nMsgType: number 27 | nTime: number 28 | avatar: string 29 | } 30 | 31 | interface Window { 32 | dbQuery: Function 33 | findMsgDb: Function 34 | } 35 | interface Date { 36 | format: Function 37 | } 38 | 39 | type Contact = { 40 | FirstLetter: string 41 | UserName: string 42 | Alias: string 43 | avatar: string 44 | Remark: string 45 | NickName: string 46 | displayName: string 47 | QuanPin: string 48 | RemarkQuanPin: string 49 | LabelIDList: string 50 | ChatRoomType: number 51 | extraInfo: { 52 | 个性签名: string 53 | 国: string 54 | 省: string 55 | 市: string 56 | '性别[1男2女]': number 57 | 手机号: string 58 | 朋友圈背景: string 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/renderer/src/pages/chat/components/actions.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | {{ item.label }} 11 | 12 | 13 | 14 | 19 | -------------------------------------------------------------------------------- /src/renderer/src/pages/chat/components/actions/_com_/chatDistribution.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ selectYear }}年共有 {{ totalCountOfYear }} 条聊天 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 周一 13 | 周四 14 | 周日 15 | 16 | 18 | 19 | 20 | {{ week[0].month }}月 21 | 22 | 23 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 109 | -------------------------------------------------------------------------------- /src/renderer/src/pages/chat/components/actions/_com_/chatSender.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 发送者消息数量 5 | 6 | 7 | 8 | 9 | 10 | {{ name }} 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 53 | -------------------------------------------------------------------------------- /src/renderer/src/pages/chat/components/actions/_com_/chatTime.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 聊天时间分布 5 | 6 | 7 | 8 | 9 | 94 | -------------------------------------------------------------------------------- /src/renderer/src/pages/chat/components/actions/_com_/chatTypeChart.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 消息类型占比 4 | 5 | 6 | 7 | 8 | {{ type }} 9 | 10 | 11 | 12 | 13 | 86 | -------------------------------------------------------------------------------- /src/renderer/src/pages/chat/components/actions/_com_/export.ts: -------------------------------------------------------------------------------- 1 | export const shortcuts = [ 2 | { 3 | text: '近一周', 4 | value: () => { 5 | const end = new Date() 6 | const start = new Date() 7 | start.setTime(start.getTime() - 3600 * 1000 * 24 * 7) 8 | return [start, end] 9 | } 10 | }, 11 | { 12 | text: '近一月', 13 | value: () => { 14 | const end = new Date() 15 | const start = new Date() 16 | start.setTime(start.getTime() - 3600 * 1000 * 24 * 30) 17 | return [start, end] 18 | } 19 | }, 20 | { 21 | text: '近3月', 22 | value: () => { 23 | const end = new Date() 24 | const start = new Date() 25 | start.setTime(start.getTime() - 3600 * 1000 * 24 * 90) 26 | return [start, end] 27 | } 28 | }, 29 | { 30 | text: '今年至今', 31 | value: () => { 32 | const start = new Date(`${new Date().getFullYear()}-1-1`) 33 | const end = new Date() 34 | return [start, end] 35 | } 36 | } 37 | ] 38 | 39 | export const msgTypeList = [ 40 | { 41 | label: '文本', 42 | key: '文本' 43 | }, 44 | { 45 | label: '图片(暂未支持)', 46 | key: '图片', 47 | disabled: true 48 | }, 49 | { 50 | label: '语音(暂未支持)', 51 | key: '语音', 52 | disabled: true 53 | }, 54 | { 55 | label: '文件(暂未支持)', 56 | key: '文件', 57 | disabled: true 58 | } 59 | ] 60 | 61 | export const msgStyle = ` 62 | .export-html { 63 | height: 100vh; 64 | width:100vw; 65 | background:#fff; 66 | overflow: hidden; 67 | 68 | .list { 69 | width:100%; 70 | height: 100%; 71 | overflow: auto; 72 | max-width:1000px; 73 | margin:0 auto; 74 | padding:30px 15px; 75 | background:#f5f5f5; 76 | box-sizing: border-box; 77 | } 78 | } 79 | .msg-date { 80 | background-color: #eee; 81 | border-radius: 5px; 82 | padding: 2px 5px; 83 | font-size: 12px; 84 | width: fit-content; 85 | margin: 0 auto; 86 | margin-bottom: 10px; 87 | color: #999; 88 | } 89 | 90 | .system-msg { 91 | text-align: center; 92 | font-size: 12px; 93 | color: #999; 94 | margin-bottom: 30px; 95 | } 96 | 97 | .single-msg { 98 | display: flex; 99 | align-items: center; 100 | margin-bottom: 10px; 101 | 102 | &.special { 103 | align-items: flex-start; 104 | } 105 | 106 | .avatar-box { 107 | width: 35px; 108 | height: 35px; 109 | overflow: hidden; 110 | font-size: 20px; 111 | font-weight: bold; 112 | color: #e0e0e0; 113 | 114 | .avatar { 115 | width: 100%; 116 | height: 100%; 117 | display: flex; 118 | align-items: center; 119 | justify-content: center; 120 | align-items: center; 121 | // border-radius: 8px; 122 | background-color: #eee; 123 | overflow: hidden; 124 | } 125 | 126 | img { 127 | width: 100%; 128 | height: 100%; 129 | } 130 | } 131 | 132 | .msg-content-box { 133 | position: relative; 134 | flex: 1; 135 | overflow: hidden; 136 | padding: 0 5px; 137 | 138 | .nickname { 139 | position: relative; 140 | font-size: 12px; 141 | color: #999; 142 | overflow: hidden; 143 | text-overflow: ellipsis; 144 | padding-left: 10px; 145 | } 146 | 147 | .emoji { 148 | max-width: 200px; 149 | height: fit-content; 150 | margin: 0 10px; 151 | } 152 | 153 | .msg-box { 154 | display: inline-block; 155 | position: relative; 156 | 157 | .triangle { 158 | position: absolute; 159 | left: -7px; 160 | top: 50%; 161 | transform: translateY(-50%); 162 | width: 0; 163 | height: 0; 164 | border: 8px solid transparent; 165 | border-right: 10px solid #fff; 166 | } 167 | 168 | .msg-content { 169 | position: relative; 170 | width: fit-content; 171 | max-width: 100%; 172 | border-radius: 4px; 173 | padding: 6px 8px; 174 | background-color: #fff; 175 | margin-left: 8px; 176 | overflow: hidden; 177 | cursor: text; 178 | 179 | .text { 180 | font-size: 14px; 181 | color: #333; 182 | overflow: hidden; 183 | user-select: text; 184 | } 185 | } 186 | } 187 | 188 | &.isSender { 189 | text-align: right; 190 | 191 | .triangle { 192 | left: calc(100% - 10px); 193 | border-right: none; 194 | border-left: 8px solid #95EC69; 195 | } 196 | 197 | .msg-content { 198 | margin-left: 0; 199 | margin-right: 8px; 200 | border-bottom-right-radius: 0; 201 | background-color: #95EC69; 202 | 203 | .text {} 204 | } 205 | } 206 | 207 | .other-msg { 208 | font-size: 12px; 209 | } 210 | } 211 | } 212 | ` 213 | -------------------------------------------------------------------------------- /src/renderer/src/pages/chat/components/actions/_com_/exportHtml.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 27 | -------------------------------------------------------------------------------- /src/renderer/src/pages/chat/components/actions/export.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 导出 4 | 5 | 6 | 7 | 选择时间范围 8 | 9 | 11 | 12 | 选择消息类型 13 | 14 | 16 | 17 | 所选消息数:{{ exportList.length }} 18 | 导出为 19 | JSON 20 | 21 | 导出为 23 | 图片 24 | 25 | 26 | 导出为 27 | TXT 28 | 导出为 29 | HTML 30 | 31 | 32 | 33 | 34 | 35 | 取消 36 | 导出 37 | 38 | 39 | 40 | 41 | 183 | ./_com_/export -------------------------------------------------------------------------------- /src/renderer/src/pages/chat/components/actions/statistic.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 统计 4 | 5 | 6 | 7 | 8 | 生成报告 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 28 | -------------------------------------------------------------------------------- /src/renderer/src/pages/chat/components/actions/suggest.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | AI回复建议 4 | 5 | 6 | 7 | 8 | 9 | 10 | by {{ result.model }}.{{ moment(result.time).format('LLL') }}.仅供参考 11 | 重新建议 12 | 13 | 14 | 小建议:先进行"AI分析"再获取回复建议会更准确 15 | 16 | 17 | 18 | 19 | 20 | 115 | -------------------------------------------------------------------------------- /src/renderer/src/pages/chat/components/content.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ activeSession.Remark || activeSession.strNickName || "-" }} 6 | ({{ chatRoomInfo.UserNameList.length }}) 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | 74 | -------------------------------------------------------------------------------- /src/renderer/src/pages/chat/components/largeStatistics/chatTypeChart.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 消息类型占比 4 | 5 | 6 | 7 | 8 | 9 | {{ typeCountList[0].type }} 类型消息以 {{ typeCountList[0].rate }} 占比主导交互场景, 10 | 11 | {{ typeCountList[1].type }}({{ typeCountList[1].rate }}) 12 | 13 | 与 {{ typeCountList[2].type }}({{ typeCountList[2].rate }}) 14 | 15 | 类型消息构成补充生态。 16 | 17 | 18 | 其中 {{ maxCountOfType[typeCountList[0].type].user }} 在 {{ typeCountList[0].type }} 19 | 消息维度发送最多.达到 {{ maxCountOfType[typeCountList[0].type].max }}条。 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 229 | -------------------------------------------------------------------------------- /src/renderer/src/pages/chat/components/largeStatistics/dailyChatPeriods.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 每日聊天时间段 4 | 5 | 6 | 7 | 8 | 在 {{ Object.keys(dailyPeriodData).length }} 个活跃时段中, 9 | 有{{ (dailyPeriodData[maxCountPeriod] / msgList.length * 100).toFixed(1) }}% 的互动发生在 10 | {{ maxCountPeriod }}:00~{{ maxCountPeriod }}:59 ({{ maxCountPeriodDesc?.name }}). 11 | 12 | 13 | 14 | 15 | 16 | 17 | 199 | -------------------------------------------------------------------------------- /src/renderer/src/pages/chat/components/largeStatistics/useAI.ts: -------------------------------------------------------------------------------- 1 | import { activeSession } from '../../store/session' 2 | import { ref } from 'vue' 3 | import { marked } from 'marked' 4 | import { getAiReply } from '@renderer/store/ai' 5 | 6 | export const AISetting = ref({ 7 | show: false, 8 | style: '专业分析师', 9 | content: '简短', 10 | timeRange: [new Date(), new Date()] 11 | }) 12 | export const AIingCount = ref(0) 13 | 14 | export default function (name) { 15 | const AIResult = ref('') 16 | const AIing = ref(false) 17 | const cacheKey = `AI-${name}-${activeSession.value?.strUsrName}` 18 | 19 | AIResult.value = localStorage[cacheKey] 20 | 21 | function callAI(prompts: Array) { 22 | if (AIing.value) return 23 | AIing.value = true 24 | AIingCount.value += 1 25 | getAiReply( 26 | [ 27 | `以${AISetting.value.style}风格回答以下问题。 28 | 以下数据跨度为:${AISetting.value.timeRange[0].format('yyyy-MM-dd')}至${AISetting.value.timeRange[1].format('yyyy-MM-dd')}。 29 | 要求:不要标题,结果内容密度${AISetting.value.content}。 30 | `, 31 | ...prompts 32 | ], 33 | true 34 | ) 35 | .then(async (res) => { 36 | let str = '' 37 | // @ts-ignore (define in dts) 38 | for await (const chunk of res.stream) { 39 | str += chunk.choices[0]?.delta?.content || '' 40 | if (str) { 41 | AIResult.value = await marked(str) 42 | } 43 | } 44 | localStorage[cacheKey] = AIResult.value 45 | }) 46 | .finally(() => { 47 | AIing.value = false 48 | AIingCount.value -= 1 49 | }) 50 | } 51 | 52 | function clearAICache() { 53 | localStorage[cacheKey] = '' 54 | AIResult.value = '' 55 | } 56 | 57 | return { 58 | AIResult, 59 | AIing, 60 | callAI, 61 | clearAICache 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/renderer/src/pages/chat/components/largeStatistics/wordDistribution.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 常用词 4 | 5 | 6 | 7 | 8 | 9 | 10 | {{ name }} 11 | 12 | 13 | 14 | 15 | 157 | -------------------------------------------------------------------------------- /src/renderer/src/pages/chat/components/session.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | {{ item.strNickName[0] }} 10 | 11 | 12 | 13 | 14 | {{ item.Remark || item.strNickName }} 15 | 16 | {{ moment(item.nTime * 1000).fromNow() }} 17 | 18 | {{ item.strContent }} 19 | 20 | 21 | 22 | 23 | 24 | 25 | 49 | -------------------------------------------------------------------------------- /src/renderer/src/pages/chat/components/singleMsg.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ moment(data.CreateTime * 1000).format("LLLL") }} 4 | 5 | {{ data.StrContent }} 6 | 7 | 8 | 9 | 10 | 11 | 12 | {{ talkerDisplayName[0] }} 13 | 14 | 15 | 16 | 17 | {{ talkerDisplayName }} 18 | 19 | 20 | 21 | 22 | {{ props.data.StrContent }} 23 | [{{ props.data.TypeName }}](暂未支持此类型消息) 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {{ data.talkerInfo.nickName[0] }} 35 | 36 | 37 | 38 | 39 | 40 | 41 | 59 | -------------------------------------------------------------------------------- /src/renderer/src/pages/chat/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 35 | -------------------------------------------------------------------------------- /src/renderer/src/pages/chat/store/index.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { Wallet, PieChart, Lollipop, MagicStick } from '@element-plus/icons-vue' 3 | import comAIAnaly from '../components/actions/AIAnaly.vue' 4 | import comStatistic from '../components/actions/statistic.vue' 5 | import comExport from '../components/actions/export.vue' 6 | import comSuggest from '../components/actions/suggest.vue' 7 | 8 | export const actions = [ 9 | { 10 | icon: PieChart, 11 | label: '统计', 12 | key: 'statistic', 13 | color: '#3F51B5', 14 | component: comStatistic 15 | }, 16 | { 17 | icon: Wallet, 18 | label: '导出', 19 | key: 'export', 20 | color: '#4CAF50', 21 | component: comExport 22 | }, 23 | { 24 | icon: MagicStick, 25 | label: '回复建议', 26 | key: 'suggest', 27 | color: '#E91E63', 28 | component: comSuggest 29 | }, 30 | { 31 | icon: Lollipop, 32 | label: 'AI分析', 33 | key: 'AIAnaly', 34 | color: '#FF9800', 35 | component: comAIAnaly 36 | } 37 | ] 38 | 39 | export const activeAction = ref<{ 40 | icon 41 | label: '' 42 | key: '' 43 | component: any 44 | }>() 45 | 46 | export const showLargeStatistics = ref(false) 47 | 48 | export function handleChooseAction(action) { 49 | if (action?.key == activeAction.value?.key) return 50 | if (activeAction.value) { 51 | handleRemoveAction() 52 | setTimeout(() => { 53 | activeAction.value = action 54 | }, 300) 55 | } else { 56 | activeAction.value = action 57 | } 58 | } 59 | 60 | export function handleRemoveAction() { 61 | activeAction.value = undefined 62 | } 63 | -------------------------------------------------------------------------------- /src/renderer/src/pages/chat/store/msg.ts: -------------------------------------------------------------------------------- 1 | import { ref, nextTick } from 'vue' 2 | import { activeSession } from './session' 3 | 4 | export const msgList = ref>([]) 5 | export const chatRoomInfo = ref<{ 6 | UserNameList: [] 7 | DisplayNameList: [] 8 | SelfDisplayName: '' 9 | }>() 10 | export const loading = ref(false) 11 | export const listEl = ref() 12 | 13 | export function getChatRoomInfo(id) { 14 | chatRoomInfo.value = undefined 15 | msgList.value = [] 16 | return window 17 | .dbQuery({ 18 | query: ` 19 | SELECT UserNameList,DisplayNameList,SelfDisplayName 20 | FROM ChatRoom 21 | WHERE ChatRoomName=='${id}' 22 | `, 23 | dbname: 'de_MicroMsg' 24 | }) 25 | .then((res) => { 26 | if (res.result?.length) { 27 | let t = res.result[0] 28 | chatRoomInfo.value = { 29 | UserNameList: t.UserNameList?.split('^G'), 30 | DisplayNameList: t.DisplayNameList?.split('^G'), 31 | SelfDisplayName: t.SelfDisplayName 32 | } 33 | } 34 | }) 35 | } 36 | 37 | export function getMsgList(username) { 38 | loading.value = true 39 | msgList.value = [] 40 | return window 41 | .findMsgDb(username) 42 | .then(async (res) => { 43 | res = res.map((item, index) => { 44 | item.index = index 45 | if ((!item.talker || !item.talkerInfo) && !item.IsSender) { 46 | item.talker = activeSession.value?.strUsrName 47 | item.talkerInfo = { ...activeSession.value } 48 | } 49 | if (chatRoomInfo.value) { 50 | let index = chatRoomInfo.value.UserNameList.findIndex((name) => name == item.talker) 51 | if (index >= 0) { 52 | item.talkerInfo.Remark = 53 | chatRoomInfo.value.DisplayNameList[index] || item.talkerInfo?.strNickName 54 | } 55 | } 56 | return item 57 | }) 58 | msgList.value = res 59 | setTimeout(() => { 60 | nextTick(() => { 61 | listEl.value?.scrollTo(listEl.value?.scrollSize) 62 | }) 63 | }, 300) 64 | }) 65 | .finally(() => { 66 | loading.value = false 67 | }) 68 | } 69 | 70 | export function scrollToMsg(index = 0) { 71 | nextTick(() => { 72 | listEl.value?.scrollToIndex(index) 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /src/renderer/src/pages/chat/store/session.ts: -------------------------------------------------------------------------------- 1 | import { ComputedRef, computed, ref } from 'vue' 2 | import { handleRemoveAction } from '../store/index' 3 | import { getMsgList, getChatRoomInfo } from '../store/msg' 4 | import { XMLParser } from 'fast-xml-parser' 5 | 6 | export const sessionList = ref>([]) 7 | export const activeSessionIndex = ref(-1) 8 | export const listEl = ref() 9 | 10 | export const activeSession: ComputedRef = computed(() => { 11 | if (activeSessionIndex.value >= 0 && sessionList.value?.length) { 12 | return sessionList.value[activeSessionIndex.value] 13 | } 14 | return null 15 | }) 16 | 17 | export async function getList() { 18 | let res = await window.dbQuery({ 19 | query: `SELECT 20 | A.strUsrName,A.strNickName,A.strContent,A.nMsgType,A.nTime, 21 | B.smallHeadImgUrl,B.bigHeadImgUrl,C.Remark 22 | FROM Session A 23 | LEFT JOIN ContactHeadImgUrl B ON A.strUsrName=B.usrName 24 | LEFT JOIN Contact C ON A.strUsrName=C.UserName 25 | WHERE A.strUsrName NOT LIKE 'gh_%' 26 | AND A.strUsrName != '@publicUser' 27 | AND A.strUsrName != 'notification_messages' 28 | AND A.strUsrName != 'notifymessage' 29 | AND A.strUsrName != 'weixin' 30 | AND A.strNickName != '' 31 | ORDER BY A.nTime DESC;`, 32 | dbname: 'de_MicroMsg' 33 | }) 34 | let xmlParser = new XMLParser() 35 | if (res.result) { 36 | for (let i = 0; i < res.result.length; i++) { 37 | res.result[i].avatar = res.result[i].smallHeadImgUrl || res.result[i].bigHeadImgUrl 38 | delete res.result[i].smallHeadImgUrl 39 | delete res.result[i].bigHeadImgUrl 40 | } 41 | sessionList.value = res.result.map((item) => { 42 | if (item.strContent) { 43 | item.strContent = xmlParser.parse(item.strContent)?.revokemsg || item.strContent 44 | } 45 | return item 46 | }) 47 | // console.log(sessionList.value) 48 | } 49 | } 50 | 51 | export async function handleChooseSession(index) { 52 | if (activeSessionIndex.value == index || !sessionList.value) { 53 | return 54 | } 55 | handleRemoveAction() 56 | let item = sessionList.value[index] 57 | if (activeSessionIndex.value >= 0) { 58 | activeSessionIndex.value = -1 59 | setTimeout(async () => { 60 | activeSessionIndex.value = index 61 | await getChatRoomInfo(item.strUsrName) 62 | getMsgList(item.strUsrName) 63 | }, 300) 64 | } else { 65 | activeSessionIndex.value = index 66 | await getChatRoomInfo(item.strUsrName) 67 | getMsgList(item.strUsrName) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/renderer/src/pages/contact/components/info.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | {{ activeContact.displayName[0] }} 13 | 14 | 15 | 16 | {{ activeContact.displayName }} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 昵称: {{ activeContact.NickName }} 27 | 微信号: {{ activeContact.Alias }} 28 | 地区: {{ area }} 29 | 30 | 31 | {{ activeContact.extraInfo['个性签名'] }} 32 | 33 | 34 | 35 | 微信ID 36 | {{ activeContact.UserName }} 37 | 38 | 39 | 40 | 41 | 标签 42 | {{ labelMap[activeContact.LabelIDList] }} 43 | 44 | 45 | 46 | 47 | 手机号 48 | {{ activeContact.extraInfo['手机号'] }} 49 | 50 | 51 | 52 | 聊天数据 53 | 54 | 55 | 56 | 57 | 83 | -------------------------------------------------------------------------------- /src/renderer/src/pages/contact/store/index.ts: -------------------------------------------------------------------------------- 1 | import { ref, watch } from 'vue' 2 | import { Buffer } from 'buffer' 3 | 4 | export const list = ref>([]) 5 | export const activeContact = ref() 6 | export const labelMap = ref() 7 | export const type = ref('') 8 | 9 | watch(type, () => { 10 | getList() 11 | }) 12 | 13 | export function getList() { 14 | let query = ` 15 | SELECT A.UserName,A.Alias,A.ReMark,A.NickName,A.QuanPin,A.RemarkQuanPin,A.LabelIdList,A.ChatRoomType,A.ExtraBuf, 16 | B.smallHeadImgUrl,B.bigHeadImgUrl 17 | FROM Contact A 18 | JOIN ContactHeadImgUrl B on A.UserName=B.usrName 19 | WHERE A.UserName NOT LIKE 'gh_%' 20 | AND A.UserName != '@publicUser' 21 | AND A.UserName != 'notification_messages' 22 | AND A.UserName != "notifymessage" 23 | AND A.UserName != "weixin" 24 | ` 25 | if (type.value) { 26 | query += ` AND A.Type IN ${type.value}` 27 | } 28 | return window 29 | .dbQuery({ 30 | query, 31 | dbname: 'de_MicroMsg' 32 | }) 33 | .then((res) => { 34 | if (res.result) { 35 | res.result.forEach((item) => { 36 | item.avatar = item.smallHeadImgUrl || item.bigHeadImgUrl 37 | item.FirstLetter = (item.RemarkQuanPin || item.QuanPin)[0]?.toUpperCase() 38 | delete item.smallHeadImgUrl 39 | delete item.bigHeadImgUrl 40 | item.displayName = item.Remark || item.NickName 41 | if (item.ExtraBuf) { 42 | item.extraInfo = getExtraBuf(item.ExtraBuf) 43 | delete item.ExtraBuf 44 | } 45 | }) 46 | list.value = res.result 47 | .filter((item) => item.FirstLetter !== undefined) 48 | .sort((a, b) => a.FirstLetter?.localeCompare(b.FirstLetter)) 49 | } 50 | }) 51 | } 52 | 53 | export function getLabelMap() { 54 | window 55 | .dbQuery({ 56 | query: ` 57 | SELECT LabelId,LabelName 58 | FROM ContactLabel 59 | `, 60 | dbname: 'de_MicroMsg' 61 | }) 62 | .then((res) => { 63 | if (res.result) { 64 | let t = {} 65 | res.result.forEach((item) => { 66 | t[item.LabelId] = item.LabelName 67 | }) 68 | labelMap.value = t 69 | } 70 | }) 71 | } 72 | 73 | function getExtraBuf(ExtraBuf) { 74 | if (!ExtraBuf || ExtraBuf.length === 0) { 75 | return null 76 | } 77 | ExtraBuf = Buffer.from(ExtraBuf) 78 | 79 | const bufDict = { 80 | '74752C06': '性别[1男2女]', 81 | '46CF10C4': '个性签名', 82 | A4D9024A: '国', 83 | E2EAA8D1: '省', 84 | '1D025BBF': '市', 85 | F917BCC0: '公司名称', 86 | '759378AD': '手机号', 87 | '4EB96D85': '企微属性', 88 | '81AE19B4': '朋友圈背景', 89 | '0E719F13': '备注图片', 90 | '945f3190': '备注图片2', 91 | DDF32683: '0', 92 | '88E28FCE': '1', 93 | '761A1D2D': '2', 94 | '0263A0CB': '3', 95 | '0451FF12': '4', 96 | '228C66A8': '5', 97 | '4D6C4570': '6', 98 | '4335DFDD': '7', 99 | DE4CDAEB: '8', 100 | A72BC20A: '9', 101 | '069FED52': '10', 102 | '9B0F4299': '11', 103 | '3D641E22': '12', 104 | '1249822C': '13', 105 | B4F73ACB: '14', 106 | '0959EB92': '15', 107 | '3CF4A315': '16', 108 | C9477AC60201E44CD0E8: '17', 109 | B7ACF0F5: '18', 110 | '57A7B5A8': '19', 111 | '695F3170': '20', 112 | FB083DD9: '21', 113 | '0240E37F': '22', 114 | '315D02A3': '23', 115 | '7DEC0BC3': '24', 116 | '16791C90': '25' 117 | } 118 | 119 | const rdata = {} 120 | 121 | for (const bufName in bufDict) { 122 | const rdataName = bufDict[bufName] 123 | const bufBytes = Buffer.from(bufName, 'hex') 124 | let offset = ExtraBuf.indexOf(bufBytes) 125 | 126 | if (offset === -1) { 127 | rdata[rdataName] = '' 128 | continue 129 | } 130 | 131 | offset += bufBytes.length 132 | const typeId = ExtraBuf[offset] 133 | offset += 1 134 | 135 | if (typeId === 0x04) { 136 | rdata[rdataName] = ExtraBuf.readUInt32LE(offset) 137 | } else if (typeId === 0x18) { 138 | const length = ExtraBuf.readUInt32LE(offset) 139 | const start = offset + 4 140 | const strBuffer = ExtraBuf.subarray(start, start + length) 141 | rdata[rdataName] = strBuffer.toString('utf16le').replace(/\x00+$/, '') 142 | } else if (typeId === 0x17) { 143 | const length = ExtraBuf.readUInt32LE(offset) 144 | const start = offset + 4 145 | const strBuffer = ExtraBuf.subarray(start, start + length) 146 | rdata[rdataName] = strBuffer.toString('utf8').replace(/\x00+$/, '') 147 | } else if (typeId === 0x05) { 148 | const hexStr = ExtraBuf.subarray(offset, offset + 8).toString('hex') 149 | rdata[rdataName] = `0x${hexStr}` 150 | } 151 | } 152 | 153 | return rdata 154 | } 155 | 156 | export function handleChooseContact(contact: Contact) { 157 | activeContact.value = undefined 158 | setTimeout(() => { 159 | activeContact.value = contact 160 | }, 300) 161 | } 162 | -------------------------------------------------------------------------------- /src/renderer/src/pages/launch.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | MyChat 5 | 6 | 获取微信数据中 7 | 8 | {{ wxinfo.msg }} 9 | 第一次使用请先打开并登录微信后点击重试 10 | 11 | 重试 12 | 13 | 14 | 15 | 声明 16 | 17 | 18 | 19 | 20 | 退出 21 | 确认 22 | 23 | 24 | 25 | 26 | 27 | 99 | -------------------------------------------------------------------------------- /src/renderer/src/pages/setting/components/AISetting.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | AI配置 4 | 6 | 7 | 8 | AI模型 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | API密钥 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 推荐使用 硅基流动 平台,api服务更加稳定。 33 | 34 | 35 | 36 | API域名 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | AI服务{{ aiInstance ? "可用" : "不可用" }} 48 | 49 | AI服务初始化 50 | 51 | 52 | 使用AI服务时须了解以下信息: 53 | 54 | 55 | 56 | 57 | 105 | -------------------------------------------------------------------------------- /src/renderer/src/pages/setting/components/account.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ myWxUserinfo.nickName }} 7 | 微信ID: {{ myWxUserinfo.userName }} 8 | 9 | 10 | 11 | 同步数据 12 | 13 | 14 | 退出 15 | 16 | 17 | 18 | 35 | -------------------------------------------------------------------------------- /src/renderer/src/pages/setting/components/statement.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1.数据处理原则 4 | 5 | 所有用户数据(包括聊天记录、交互内容)均100%存储于本地设备 6 | 不会将任何用户数据上传至云端服务器或第三方存储平台 7 | 8 | 2.若使用AI功能 9 | 10 | AI交互过程通过openai库直接连接第三方服务 11 | 发送至第三方AI的数据将受其隐私政策约束 12 | 建议您在使用前阅读并确认第三方AI的隐私条款 13 | 我们无法控制第三方对已接收数据的后续处理 14 | 与AI对话时已过滤手机号,邮箱,身份证号,微信ID等个人信息 15 | 16 | 3.免责条款 17 | 18 | 因设备丢失/损坏导致的本地数据泄露不属于责任范围 19 | 因用户主动分享屏幕/导出记录导致的泄露不承担责任 20 | 因第三方AI服务商导致的数据问题不承担连带责任 21 | 22 | 23 | 24 | 27 | -------------------------------------------------------------------------------- /src/renderer/src/pages/setting/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 设置 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 34 | -------------------------------------------------------------------------------- /src/renderer/src/plugins/elLoading.ts: -------------------------------------------------------------------------------- 1 | import { ElLoading } from 'element-plus' 2 | 3 | /** 4 | * @description 扩展ElLoading,传入默认值 5 | */ 6 | export default { 7 | install(app: any) { 8 | const svg: string = `` 9 | const loadingDir: any = ElLoading.directive 10 | const originDirMounted = loadingDir.mounted 11 | loadingDir.mounted = function (el: any, binding: any, vnode: any, prevVnode: any) { 12 | // 需要覆盖哪些默认属性值在这里设置,具体属性名参考官网loading指令用法 13 | el.setAttribute('element-loading-svg', svg) 14 | el.setAttribute('element-loading-svg-view-box', '0 0 24 24') 15 | originDirMounted.call(this, el, binding, vnode, prevVnode) 16 | } 17 | const originService = ElLoading.service 18 | ElLoading.service = function (options: any = {}) { 19 | return originService.call(this, Object.assign({ svg }, options)) 20 | } 21 | app.config.globalProperties.$loading = ElLoading.service 22 | // 如果在main.ts中全局使用了ElementPlus —> app.use(ElementPlus),则下面这行代码不需要 23 | // app.use(ElLoading); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/renderer/src/store/ai.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai' 2 | import { ref } from 'vue' 3 | 4 | export let aiInstance = ref() 5 | export let creatingInstance = ref(false) 6 | export const aiConfig = ref({ 7 | model: 'Pro/deepseek-ai/DeepSeek-V3', 8 | apiKey: '', 9 | baseURL: 'https://api.siliconflow.cn/v1' 10 | }) 11 | 12 | try { 13 | aiConfig.value = JSON.parse(localStorage['aiConfig']) 14 | } catch {} 15 | 16 | export async function init() { 17 | try { 18 | if (!aiConfig.value.apiKey) { 19 | throw { msg: 'ApiKey不能为空' } 20 | } 21 | creatingInstance.value = true 22 | aiInstance.value = new OpenAI({ 23 | baseURL: aiConfig.value.baseURL, 24 | apiKey: aiConfig.value.apiKey, 25 | dangerouslyAllowBrowser: true 26 | }) 27 | console.log('ai初始化成功') 28 | } catch (err) { 29 | throw err 30 | } finally { 31 | creatingInstance.value = false 32 | } 33 | } 34 | 35 | export function changeAiConfig(form) { 36 | aiConfig.value = form 37 | localStorage['aiConfig'] = JSON.stringify(form) 38 | } 39 | 40 | export async function getAiReply(prompts: Array, stream = false) { 41 | if (!aiInstance.value) { 42 | throw { msg: 'AI未初始化' } 43 | } 44 | if (!prompt) { 45 | throw { msg: '提示词不能为空' } 46 | } 47 | try { 48 | const completion = await aiInstance.value.chat.completions.create({ 49 | messages: prompts.map((prompt) => { 50 | return { role: 'user', content: filterPersonalInfo(prompt) } 51 | }), 52 | model: aiConfig.value.model, 53 | stream: stream 54 | }) 55 | if (stream) { 56 | return { 57 | model: aiConfig.value.model, 58 | stream: completion 59 | } 60 | } else { 61 | return { 62 | model: aiConfig.value.model, 63 | // @ts-ignore (define in dts) 64 | result: completion.choices[0].message.content 65 | } 66 | } 67 | } catch (err) { 68 | // @ts-ignore (define in dts) 69 | throw { msg: err?.message } 70 | } 71 | } 72 | 73 | const patterns = { 74 | phone: /\b(\+?86[-\s]?)?1[3-9]\d{9}\b/g, // 手机号 75 | email: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, // 邮箱 76 | id_card: /\b\d{15}|\d{18}|\d{17}X\b/g, // 身份证号 77 | password: /\b(?:password|pwd|pass|psw|密码)[\s=:]*\S+\b/g, // 密码 78 | wechat: /\b(?:wechat|wx)[\s=:]*\S+\b/g // 微信号 79 | } 80 | function filterPersonalInfo(text) { 81 | let filteredText = text 82 | for (const key in patterns) { 83 | filteredText = filteredText.replace(patterns[key], `[${key.toUpperCase()}_FILTERED]`) 84 | } 85 | return filteredText 86 | } 87 | -------------------------------------------------------------------------------- /src/renderer/src/store/wx.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { clear as clearIndexDB } from '../util/indexDB' 3 | 4 | export const readingWxinfo = ref(false) 5 | export const wxinfo = ref({ pid: '', wxid: '', wx_dir: '', key: '', msg: '' }) 6 | export const wxDbPath = ref('') //解密后的微信数据库路径 7 | export const myWxUserinfo = ref({ 8 | userName: '', 9 | nickName: '', 10 | avatar: '' 11 | }) 12 | 13 | try { 14 | wxinfo.value = JSON.parse(localStorage['wxinfo']) 15 | wxDbPath.value = localStorage['wxDbPath'] 16 | } catch {} 17 | 18 | export function logout() { 19 | localStorage.clear() 20 | clearIndexDB() 21 | // @ts-ignore (define in dts) 22 | window.appQuit() 23 | } 24 | 25 | export function getWxinfo() { 26 | readingWxinfo.value = true 27 | return fetch('http://127.0.0.1:4556/api/wxinfo') 28 | .then(async (response: Response) => { 29 | const res = await response.json() 30 | let info = res.data?.wxinfo 31 | let result = { msg: '' } 32 | if (Array.isArray(info) && info.length) { 33 | if (info[0]?.wxid && info[0]?.wxid != 'None') { 34 | return info[0] 35 | } else { 36 | result.msg = '微信未登录' 37 | } 38 | } else { 39 | result.msg = '微信未运行' 40 | } 41 | return result 42 | }) 43 | .finally(() => { 44 | readingWxinfo.value = false 45 | }) 46 | } 47 | 48 | export function syncWxDb() { 49 | readingWxinfo.value = true 50 | return fetch('http://127.0.0.1:4556/api/asyncwxdb', { 51 | method: 'POST', 52 | headers: { 53 | 'Content-Type': 'application/json' 54 | }, 55 | body: JSON.stringify({ 56 | wxid: wxinfo.value.wxid, 57 | key: wxinfo.value.key, 58 | wxPath: wxinfo.value.wx_dir 59 | }) 60 | }) 61 | .then(async (response) => { 62 | const res = await response.json() 63 | if (res?.data) { 64 | wxDbPath.value = res.data.outDbPath 65 | localStorage['wxDbPath'] = wxDbPath.value 66 | } 67 | }) 68 | .catch(() => { 69 | wxinfo.value.msg = '同步微信数据库失败' 70 | }) 71 | .finally(() => { 72 | readingWxinfo.value = false 73 | }) 74 | } 75 | 76 | export async function getWxUserinfo(username = '') { 77 | let nickName = '' 78 | let avatar = '' 79 | try { 80 | let res = await window.dbQuery({ 81 | query: ` 82 | SELECT A.NickName,B.smallHeadImgUrl,B.bigHeadImgUrl 83 | FROM Contact A LEFT JOIN ContactHeadImgUrl B ON A.UserName==B.usrName 84 | WHERE A.UserName=='${username}' 85 | `, 86 | dbname: 'de_MicroMsg' 87 | }) 88 | if (res?.result) { 89 | nickName = res.result[0]?.NickName 90 | avatar = res.result[0].smallHeadImgUrl || res.result[0].bigHeadImgUrl 91 | } else { 92 | throw '查询用户信息为空:' + username 93 | } 94 | } catch (err) { 95 | throw err 96 | } 97 | return { 98 | nickName: nickName || '', 99 | avatar: avatar || '' 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/renderer/src/util/indexDB.js: -------------------------------------------------------------------------------- 1 | var request = window.indexedDB.open('MyWeChat', 1) 2 | var db 3 | const tables = ['AI-analy', 'AI-suggest'] 4 | 5 | request.onerror = function (err) { 6 | console.log('indexDB打开失败:', err) 7 | } 8 | request.onsuccess = function (event) { 9 | db = request.result 10 | console.log('indexDB打开成功') 11 | } 12 | request.onupgradeneeded = function (event) { 13 | db = request.result 14 | //建表 15 | tables.forEach((table) => { 16 | if (!db.objectStoreNames.contains(table)) { 17 | db.createObjectStore(table, { keyPath: 'userName' }) 18 | } 19 | }) 20 | } 21 | 22 | export function add(tb = '', data = {}) { 23 | return new Promise((resolve, reject) => { 24 | if (!tb) { 25 | return reject('表名不能为空') 26 | } 27 | if (!data?.userName) { 28 | return reject('userName不能为空') 29 | } 30 | var request = db 31 | .transaction([tb], 'readwrite') //新建事务,readwrite, readonly(默认), versionchange 32 | .objectStore(tb) //拿到IDBObjectStore 对象 33 | .add(data) 34 | request.onsuccess = function (event) { 35 | resolve(event) 36 | } 37 | request.onerror = function (err) { 38 | reject(err) 39 | } 40 | request.onabort = function (err) { 41 | reject(err) 42 | } 43 | }) 44 | } 45 | 46 | export function remove(tb = '', id = '') { 47 | return new Promise((resolve, reject) => { 48 | if (!tb) { 49 | return reject('表名不能为空') 50 | } 51 | if (!id) { 52 | return reject('id不能为空') 53 | } 54 | var request = db.transaction([tb], 'readwrite').objectStore(tb).delete(id) 55 | request.onsuccess = function (event) { 56 | resolve(event) 57 | } 58 | request.onerror = function (err) { 59 | reject(err) 60 | } 61 | }) 62 | } 63 | 64 | export function readAll(tb = '') { 65 | return new Promise((resolve, reject) => { 66 | if (!tb) { 67 | return reject('表名不能为空') 68 | } 69 | var objectStore = db.transaction([tb]).objectStore(tb) 70 | let list = [] 71 | objectStore.openCursor().onsuccess = function (event) { 72 | var cursor = event.target.result 73 | if (cursor) { 74 | list.push(cursor.value) 75 | cursor.continue() 76 | } else { 77 | resolve(list) 78 | } 79 | } 80 | }) 81 | } 82 | 83 | export function read(tb, id) { 84 | return new Promise((resolve, reject) => { 85 | var request = db.transaction([tb]).objectStore(tb).get(id) 86 | request.onsuccess = function (event) { 87 | resolve(request.result) 88 | } 89 | request.onerror = function (event) { 90 | reject() 91 | } 92 | }) 93 | } 94 | 95 | export function update(tb = '', data = {}) { 96 | return new Promise(async (resolve, reject) => { 97 | if (!tb) { 98 | return reject('表名不能为空') 99 | } 100 | let t = await read(tb, data.userName) 101 | if (t) { 102 | var request = db 103 | .transaction([tb], 'readwrite') 104 | .objectStore(tb) 105 | .put({ ...t, ...data }) 106 | request.onsuccess = function (event) { 107 | resolve(event) 108 | } 109 | request.onerror = function (err) { 110 | reject(err) 111 | } 112 | } else { 113 | add(tb, data) 114 | .then((res) => resolve(res)) 115 | .catch((err) => reject(err)) 116 | } 117 | }) 118 | } 119 | 120 | export function clear() { 121 | tables.forEach((table) => { 122 | clearIndexedDB(table) 123 | }) 124 | } 125 | 126 | function clearIndexedDB(databaseName) { 127 | return new Promise((resolve, reject) => { 128 | const request = indexedDB.deleteDatabase(databaseName) 129 | 130 | request.onsuccess = () => { 131 | console.log(`IndexedDB "${databaseName}" deleted successfully.`) 132 | resolve() 133 | } 134 | 135 | request.onerror = (event) => { 136 | console.error(`Error deleting IndexedDB "${databaseName}".`, event) 137 | reject(event) 138 | } 139 | 140 | request.onblocked = () => { 141 | console.warn(`Deletion of IndexedDB "${databaseName}" is blocked.`) 142 | } 143 | }) 144 | } 145 | -------------------------------------------------------------------------------- /src/renderer/src/util/momentCh.js: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | 3 | moment.locale("zh-cn", { 4 | months: 5 | "一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月".split( 6 | "_" 7 | ), 8 | monthsShort: "1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"), 9 | weekdays: "星期日_星期一_星期二_星期三_星期四_星期五_星期六".split("_"), 10 | weekdaysShort: "周日_周一_周二_周三_周四_周五_周六".split("_"), 11 | weekdaysMin: "日_一_二_三_四_五_六".split("_"), 12 | longDateFormat: { 13 | LT: "Ah点mm分", 14 | LTS: "Ah点m分s秒", 15 | L: "YYYY-MM-DD", 16 | LL: "YYYY年MMMD日", 17 | LLL: "YYYY年MMMD日Ah点mm分", 18 | LLLL: "YYYY年MMMD日ddddAh点mm分", 19 | l: "YYYY-MM-DD", 20 | ll: "YYYY年MMMD日", 21 | lll: "YYYY年MMMD日Ah点mm分", 22 | llll: "YYYY年MMMD日ddddAh点mm分", 23 | }, 24 | meridiemParse: /凌晨|早上|上午|中午|下午|晚上/, 25 | meridiemHour: function (hour, meridiem) { 26 | if (hour === 12) { 27 | hour = 0; 28 | } 29 | if (meridiem === "凌晨" || meridiem === "早上" || meridiem === "上午") { 30 | return hour; 31 | } else if (meridiem === "下午" || meridiem === "晚上") { 32 | return hour + 12; 33 | } else { 34 | // '中午' 35 | return hour >= 11 ? hour : hour + 12; 36 | } 37 | }, 38 | meridiem: function (hour, minute, isLower) { 39 | var hm = hour * 100 + minute; 40 | if (hm < 600) { 41 | return "凌晨"; 42 | } else if (hm < 900) { 43 | return "早上"; 44 | } else if (hm < 1130) { 45 | return "上午"; 46 | } else if (hm < 1230) { 47 | return "中午"; 48 | } else if (hm < 1800) { 49 | return "下午"; 50 | } else { 51 | return "晚上"; 52 | } 53 | }, 54 | calendar: { 55 | sameDay: function () { 56 | return this.minutes() === 0 ? "[今天]Ah[点整]" : "[今天]LT"; 57 | }, 58 | nextDay: function () { 59 | return this.minutes() === 0 ? "[明天]Ah[点整]" : "[明天]LT"; 60 | }, 61 | lastDay: function () { 62 | return this.minutes() === 0 ? "[昨天]Ah[点整]" : "[昨天]LT"; 63 | }, 64 | nextWeek: function () { 65 | var startOfWeek, prefix; 66 | startOfWeek = moment().startOf("week"); 67 | prefix = 68 | this.unix() - startOfWeek.unix() >= 7 * 24 * 3600 ? "[下]" : "[本]"; 69 | return this.minutes() === 0 ? prefix + "dddAh点整" : prefix + "dddAh点mm"; 70 | }, 71 | lastWeek: function () { 72 | var startOfWeek, prefix; 73 | startOfWeek = moment().startOf("week"); 74 | prefix = this.unix() < startOfWeek.unix() ? "[上]" : "[本]"; 75 | return this.minutes() === 0 ? prefix + "dddAh点整" : prefix + "dddAh点mm"; 76 | }, 77 | sameElse: "LL", 78 | }, 79 | ordinalParse: /\d{1,2}(日|月|周)/, 80 | ordinal: function (number, period) { 81 | switch (period) { 82 | case "d": 83 | case "D": 84 | case "DDD": 85 | return number + "日"; 86 | case "M": 87 | return number + "月"; 88 | case "w": 89 | case "W": 90 | return number + "周"; 91 | default: 92 | return number; 93 | } 94 | }, 95 | relativeTime: { 96 | future: "%s内", 97 | past: "%s前", 98 | s: "几秒", 99 | m: "1 分钟", 100 | mm: "%d 分钟", 101 | h: "1 小时", 102 | hh: "%d 小时", 103 | d: "1 天", 104 | dd: "%d 天", 105 | M: "1 个月", 106 | MM: "%d 个月", 107 | y: "1 年", 108 | yy: "%d 年", 109 | }, 110 | week: { 111 | // GB/T 7408-1994《数据元和交换格式·信息交换·日期和时间表示法》与ISO 8601:1988等效 112 | dow: 1, // Monday is the first day of the week. 113 | doy: 4, // The week that contains Jan 4th is the first week of the year. 114 | }, 115 | }); 116 | -------------------------------------------------------------------------------- /src/renderer/src/util/router.ts: -------------------------------------------------------------------------------- 1 | import { createWebHashHistory, createRouter, RouteRecordRaw } from 'vue-router' 2 | 3 | const pages = import.meta.glob('../pages/**/**.vue') 4 | const routes: Array = [] 5 | Object.keys(pages).map(function (key: string) { 6 | const component = pages[key] 7 | const path = key 8 | .toLowerCase() 9 | .replace(/\.vue$/, '') 10 | .replace(/.+\/pages/g, '') 11 | if (!path.includes('/components')) { 12 | routes.push({ 13 | path, 14 | name: path, 15 | component 16 | }) 17 | } 18 | }) 19 | 20 | export default createRouter({ 21 | history: createWebHashHistory(), 22 | routes: [ 23 | { 24 | path: '/', 25 | redirect: '/launch' 26 | }, 27 | ...routes 28 | ] 29 | }) 30 | -------------------------------------------------------------------------------- /src/renderer/src/util/util.js: -------------------------------------------------------------------------------- 1 | export function throttle(func, interval = 100) { 2 | let sign = true 3 | return function () { 4 | if (!sign) return 5 | console.log('run') 6 | sign = false 7 | setTimeout(() => { 8 | func.apply(this, arguments) 9 | sign = true 10 | }, interval) 11 | } 12 | } 13 | export function debounce(func, interval = 100) { 14 | var timer = '' 15 | return function () { 16 | clearTimeout(timer) 17 | timer = setTimeout(() => { 18 | func.apply(this, arguments) 19 | }, interval) 20 | } 21 | } 22 | 23 | Date.prototype.format = function (fmt) { 24 | var arr_week = new Array('星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六') 25 | var o = { 26 | 'M+': this.getMonth() + 1, //月份 27 | 'd+': this.getDate(), //日 28 | 'h+': this.getHours(), //小时 29 | 'm+': this.getMinutes(), //分 30 | 's+': this.getSeconds(), //秒 31 | 'q+': Math.floor((this.getMonth() + 3) / 3), //季度 32 | S: this.getMilliseconds(), //毫秒 33 | W: arr_week[this.getDay()] 34 | } 35 | if (/(y+)/.test(fmt)) { 36 | fmt = fmt.replace(RegExp.$1, (this.getFullYear() + '').substr(4 - RegExp.$1.length)) 37 | } 38 | for (var k in o) { 39 | if (new RegExp('(' + k + ')').test(fmt)) { 40 | fmt = fmt.replace( 41 | RegExp.$1, 42 | RegExp.$1.length == 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length) 43 | ) 44 | } 45 | } 46 | return fmt 47 | } 48 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.node.json" }, 5 | { "path": "./tsconfig.web.json" } 6 | ], 7 | "compilerOptions": { 8 | // ... 9 | "types": ["element-plus/global"], 10 | "typeRoots": ["node_modules/@types", "./types"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", 3 | "include": ["electron.vite.config.*", 4 | "src/main/**/*", 5 | "src/preload/**/*", 6 | "src/renderer/util/**/*" 7 | ], 8 | "compilerOptions": { 9 | "composite": true, 10 | "types": ["electron-vite/node"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.web.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@electron-toolkit/tsconfig/tsconfig.web.json", 3 | "include": [ 4 | "src/renderer/src/env.d.ts", 5 | "src/renderer/src/page.d.ts", 6 | "src/renderer/src/**/*", 7 | "src/renderer/src/**/*.vue", 8 | "src/renderer/src/components/**/*.vue", 9 | "src/preload/*.d.ts" 10 | ], 11 | "compilerOptions": { 12 | "composite": true, 13 | "baseUrl": ".", 14 | "paths": { 15 | "@renderer/*": [ 16 | "src/renderer/src/*" 17 | ] 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /type.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DingHui-Li/mychat/5128f174ba4cb6baf77f95714cd2efbfc962b66f/type.ts --------------------------------------------------------------------------------