diff --git a/README.md b/README.md index 27661a4..d9bb797 100644 --- a/README.md +++ b/README.md @@ -50,8 +50,7 @@ MAC系统 获取微信聊天记录 AI一键生成群聊总结 ### 使用前置要求 - **微信版本**: - - 微信 <= 4.0: 可正常使用,获取数据库密码方式参考:[Mac 导出微信聊天记录](https://blog.vcvit.me/2024/08/02/mac-export-wechat-chat-records/) - - 微信 >= 4.0: 已支持部分能力,仍在持续迭代与兼容性验证中;如需更成熟的完整方案,推荐使用 [WeFlow](https://github.com/hicccc77/WeFlow) [Chatlog](https://github.com/sjzar/chatlog) + - 微信 4.0+: 已支持部分能力,仍在持续迭代与兼容性验证中;如需更成熟的完整方案,推荐使用 [WeFlow](https://github.com/hicccc77/WeFlow) [Chatlog](https://github.com/sjzar/chatlog) - 如无法获取本地数据库密码,则无法使用当前项目 - Node.js (推荐 v16+) - pnpm@7 diff --git a/package.json b/package.json index 700d31e..d82533d 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,6 @@ "dependencies": { "@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/utils": "^4.0.0", - "better-sqlite3-multiple-ciphers": "^12.5.0", "fs-extra": "^11.3.2", "fzstd": "^0.1.1", "koffi": "^2.9.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be04697..23ffa71 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,7 +16,6 @@ specifiers: '@types/react': ^19.2.7 '@types/react-dom': ^19.2.3 '@vitejs/plugin-react': ^5.1.1 - better-sqlite3-multiple-ciphers: ^12.5.0 electron: ^39.2.6 electron-builder: ^26.0.12 electron-vite: ^5.0.0 @@ -38,7 +37,6 @@ specifiers: dependencies: '@electron-toolkit/preload': 3.0.2_electron@39.2.6 '@electron-toolkit/utils': 4.0.0_electron@39.2.6 - better-sqlite3-multiple-ciphers: 12.5.0 fs-extra: 11.3.2 fzstd: 0.1.1 koffi: 2.16.2 @@ -1578,32 +1576,20 @@ packages: /base64-js/1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: true /baseline-browser-mapping/2.9.5: resolution: {integrity: sha512-D5vIoztZOq1XM54LUdttJVc96ggEsIfju2JBvht06pSzpckp3C7HReun67Bghzrtdsq9XdMGbSSB3v3GhMNmAA==} hasBin: true dev: true - /better-sqlite3-multiple-ciphers/12.5.0: - resolution: {integrity: sha512-bYbiV0TdCIQFqlI122+wIFnc3yXWHyz8A/026Y/YfiA3vKcC0wLnHqqociNehpFmbRSowuY3Ko8SyUhBXI9+4A==} - engines: {node: 20.x || 22.x || 23.x || 24.x} - dependencies: - bindings: 1.5.0 - prebuild-install: 7.1.3 - dev: false - - /bindings/1.5.0: - resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} - dependencies: - file-uri-to-path: 1.0.0 - dev: false - /bl/4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} dependencies: buffer: 5.7.1 inherits: 2.0.4 readable-stream: 3.6.2 + dev: true /boolean/3.2.0: resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} @@ -1647,6 +1633,7 @@ packages: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 + dev: true /builder-util-runtime/9.3.1: resolution: {integrity: sha512-2/egrNDDnRaxVwK3A+cJq6UOlqOdedGA7JPqCeJjN2Zjk1/QB/6QUi3b714ScIGS7HafFXTyzJEOr5b44I3kvQ==} @@ -1772,10 +1759,6 @@ packages: supports-color: 7.2.0 dev: true - /chownr/1.1.4: - resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} - dev: false - /chownr/2.0.0: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} engines: {node: '>=10'} @@ -1947,11 +1930,6 @@ packages: dependencies: mimic-response: 3.1.0 - /deep-extend/0.6.0: - resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} - engines: {node: '>=4.0.0'} - dev: false - /deep-is/0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true @@ -1990,6 +1968,7 @@ packages: /detect-libc/2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + dev: true /detect-node/2.1.0: resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} @@ -2528,11 +2507,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /expand-template/2.0.3: - resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} - engines: {node: '>=6'} - dev: false - /exponential-backoff/3.1.3: resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} dev: true @@ -2596,10 +2570,6 @@ packages: flat-cache: 4.0.1 dev: true - /file-uri-to-path/1.0.0: - resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} - dev: false - /filelist/1.0.4: resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} dependencies: @@ -2652,10 +2622,6 @@ packages: mime-types: 2.1.35 dev: true - /fs-constants/1.0.0: - resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} - dev: false - /fs-extra/10.1.0: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} @@ -2787,10 +2753,6 @@ packages: get-intrinsic: 1.3.0 dev: true - /github-from-package/0.0.0: - resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} - dev: false - /glob-parent/6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -3020,6 +2982,7 @@ packages: /ieee754/1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: true /ignore/5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} @@ -3063,10 +3026,7 @@ packages: /inherits/2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - /ini/1.3.8: - resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - dev: false + dev: true /internal-slot/1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} @@ -3574,6 +3534,7 @@ packages: /minimist/1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: true /minipass-collect/1.0.2: resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} @@ -3639,10 +3600,6 @@ packages: yallist: 4.0.0 dev: true - /mkdirp-classic/0.5.3: - resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} - dev: false - /mkdirp/1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} @@ -3658,10 +3615,6 @@ packages: hasBin: true dev: true - /napi-build-utils/2.0.0: - resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} - dev: false - /natural-compare/1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true @@ -3676,6 +3629,7 @@ packages: engines: {node: '>=10'} dependencies: semver: 7.7.3 + dev: true /node-addon-api/1.7.2: resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==} @@ -3924,25 +3878,6 @@ packages: source-map-js: 1.2.1 dev: true - /prebuild-install/7.1.3: - resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} - engines: {node: '>=10'} - hasBin: true - dependencies: - detect-libc: 2.1.2 - expand-template: 2.0.3 - github-from-package: 0.0.0 - minimist: 1.2.8 - mkdirp-classic: 0.5.3 - napi-build-utils: 2.0.0 - node-abi: 3.85.0 - pump: 3.0.3 - rc: 1.2.8 - simple-get: 4.0.1 - tar-fs: 2.1.4 - tunnel-agent: 0.6.0 - dev: false - /prelude-ls/1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -4010,16 +3945,6 @@ packages: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} - /rc/1.2.8: - resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} - hasBin: true - dependencies: - deep-extend: 0.6.0 - ini: 1.3.8 - minimist: 1.2.8 - strip-json-comments: 2.0.1 - dev: false - /react-dom/19.2.1_react@19.2.1: resolution: {integrity: sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==} peerDependencies: @@ -4059,6 +3984,7 @@ packages: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 + dev: true /reflect.getprototypeof/1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} @@ -4198,6 +4124,7 @@ packages: /safe-buffer/5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: true /safe-push-apply/1.0.0: resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} @@ -4356,18 +4283,6 @@ packages: engines: {node: '>=16.11.0'} dev: false - /simple-concat/1.0.1: - resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} - dev: false - - /simple-get/4.0.1: - resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} - dependencies: - decompress-response: 6.0.0 - once: 1.4.0 - simple-concat: 1.0.1 - dev: false - /simple-update-notifier/2.0.0: resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} engines: {node: '>=10'} @@ -4530,6 +4445,7 @@ packages: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} dependencies: safe-buffer: 5.2.1 + dev: true /strip-ansi/6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} @@ -4545,11 +4461,6 @@ packages: ansi-regex: 6.2.2 dev: true - /strip-json-comments/2.0.1: - resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} - engines: {node: '>=0.10.0'} - dev: false - /strip-json-comments/3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -4582,26 +4493,6 @@ packages: '@pkgr/core': 0.2.9 dev: true - /tar-fs/2.1.4: - resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} - dependencies: - chownr: 1.1.4 - mkdirp-classic: 0.5.3 - pump: 3.0.3 - tar-stream: 2.2.0 - dev: false - - /tar-stream/2.2.0: - resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} - engines: {node: '>=6'} - dependencies: - bl: 4.1.0 - end-of-stream: 1.4.5 - fs-constants: 1.0.0 - inherits: 2.0.4 - readable-stream: 3.6.2 - dev: false - /tar/6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} @@ -4661,12 +4552,6 @@ packages: typescript: 5.9.3 dev: true - /tunnel-agent/0.6.0: - resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} - dependencies: - safe-buffer: 5.2.1 - dev: false - /type-check/0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -4805,6 +4690,7 @@ packages: /util-deprecate/1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: true /verror/1.10.1: resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==} diff --git a/src/main/image-decrypt-service.ts b/src/main/image-decrypt-service.ts index 4582218..9265401 100644 --- a/src/main/image-decrypt-service.ts +++ b/src/main/image-decrypt-service.ts @@ -241,15 +241,11 @@ export class ImageDecryptService { ) let decrypted: Buffer - if (version === 0) { - console.log('[ImageDecrypt] using V3 (XOR only)') - decrypted = this.decryptDatV3(datPath) - } else if (version === 1) { + if (version === 1) { console.log('[ImageDecrypt] using V1 (default AES key)') const key = Buffer.from(this.defaultV1AesKey, 'ascii') decrypted = this.decryptDatV4(datPath, key) - } else { - // version === 2 + } else if (version === 2) { console.log('[ImageDecrypt] using V2 (user AES key)') if (!this.aesKey) { console.log('[ImageDecrypt] no AES key configured') @@ -257,6 +253,9 @@ export class ImageDecryptService { } const key = Buffer.from(this.aesKey, 'ascii').slice(0, 16) decrypted = this.decryptDatV4(datPath, key) + } else { + console.log('[ImageDecrypt] unsupported dat version:', version) + return null } return decrypted @@ -310,18 +309,6 @@ export class ImageDecryptService { return 0 } - /** - * V3 解密 - 仅 XOR - */ - private decryptDatV3(inputPath: string): Buffer { - const data = readFileSync(inputPath) - const out = Buffer.alloc(data.length) - for (let i = 0; i < data.length; i += 1) { - out[i] = data[i] ^ this.xorKey - } - return out - } - /** * V4 解密 - AES + XOR */ diff --git a/src/main/index.ts b/src/main/index.ts index 42981ce..1c36e33 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -113,16 +113,13 @@ app.whenReady().then(() => { wechatDb?.close() wechatDb = nextWechatDb const wcdb4Client = nextWechatDb.getWcdb4Client() - let monitoring = false - if (wcdb4Client) { - voiceService = new VoiceService(wcdb4Client) - stickerService = new StickerService(wcdb4Client) - monitoring = wcdb4Client.startMonitor((type, json) => { - for (const window of BrowserWindow.getAllWindows()) { - if (!window.isDestroyed()) window.webContents.send('wcdb-change', { type, json }) - } - }) - } + voiceService = new VoiceService(wcdb4Client) + stickerService = new StickerService(wcdb4Client) + const monitoring = wcdb4Client.startMonitor((type, json) => { + for (const window of BrowserWindow.getAllWindows()) { + if (!window.isDestroyed()) window.webContents.send('wcdb-change', { type, json }) + } + }) imageDecryptService = null return { success: true, monitoring } } catch (error) { @@ -206,14 +203,13 @@ app.whenReady().then(() => { ipcMain.handle('db:getMessages', (_, userMd5: string, startTime?: number, endTime?: number) => { if (!wechatDb) return [] const wcdb4Client = wechatDb.getWcdb4Client() - const username = wcdb4Client?.getUsernameByMd5(userMd5) + const username = wcdb4Client.getUsernameByMd5(userMd5) const rawMessages = wechatDb.getUserMessages(userMd5, startTime, endTime) const groupMembers = wechatDb.getGroupMembersForChat(userMd5) const myAvatar = wechatDb.getMyAvatarUrl() - const myGroupNickname = - username?.endsWith('@chatroom') && wcdb4Client - ? wcdb4Client.getMyGroupNickname(username) - : undefined + const myGroupNickname = username?.endsWith('@chatroom') + ? wcdb4Client.getMyGroupNickname(username) + : undefined return rawMessages.map((msg: WechatMessage) => { const rawMsgType = parseInt(msg.messageType) @@ -281,7 +277,7 @@ app.whenReady().then(() => { const imageDatName = parseImageDatNameFromRow(msg) contentData = { ...parsed, datName: parsed.datName || imageDatName } } else if (parsed.type !== 'system') { - if (parsed.type === 'sticker' && !parsed.url && parsed.md5 && wcdb4Client) { + if (parsed.type === 'sticker' && !parsed.url && parsed.md5) { parsed.url = wcdb4Client.resolveEmoticonCdnUrl(parsed.md5) } contentData = parsed @@ -301,7 +297,7 @@ app.whenReady().then(() => { ) { const parsed = parseStickerMessageFromRow(msg, content) if (parsed.type === 'sticker') { - if (!parsed.url && parsed.md5 && wcdb4Client) { + if (!parsed.url && parsed.md5) { parsed.url = wcdb4Client.resolveEmoticonCdnUrl(parsed.md5) } content = '' @@ -333,7 +329,6 @@ app.whenReady().then(() => { ipcMain.handle('db:getGroupSnapshot', (_, userMd5: string) => { if (!wechatDb) return null const wcdb4Client = wechatDb.getWcdb4Client() - if (!wcdb4Client) return null const roomId = wcdb4Client.getUsernameByMd5(userMd5) if (!roomId || !roomId.endsWith('@chatroom')) return null diff --git a/src/main/wechat-db.ts b/src/main/wechat-db.ts index 1baf3fe..8a282f9 100644 --- a/src/main/wechat-db.ts +++ b/src/main/wechat-db.ts @@ -1,12 +1,5 @@ -import DatabaseConstructor from 'better-sqlite3-multiple-ciphers' -import fs from 'fs-extra' -import path from 'path' -import crypto from 'crypto' -import os from 'os' import { Wcdb4Client } from './wcdb4-client' -type Database = import('better-sqlite3-multiple-ciphers').Database - export interface UserContact { m_nsUsrName: string nickname: string @@ -38,444 +31,122 @@ export interface GroupMemberInfo { } export class WechatDb { - private static WECHAT_DIR = path.join( - os.homedir(), - 'Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/2.0b4.0.9' - ) - - private rawKey: string - private correctUserId: string | null = null - private chatDb: { name: string; db_number: string }[] | null = null - private groupMemberCache = new Map() - private wcdb4Client: Wcdb4Client | null = null + private wcdb4Client: Wcdb4Client private chatMd5ToUsername = new Map() - private wechat4OpenError: string | null = null constructor(rawKey: string) { - this.rawKey = rawKey console.log(`Initializing WechatDb with key length: ${rawKey.trim().length}`) - - if (this.tryOpenWechat4()) { - this.chatDb = this.getChatDbNumber() - return - } - - if (!fs.existsSync(WechatDb.WECHAT_DIR)) { - throw new Error(`WeChat directory not found at ${WechatDb.WECHAT_DIR}`) - } - - if (this.getUser()) { - console.log('User found, getting chat DB number') - this.chatDb = this.getChatDbNumber() - } else { - throw new Error( - `No valid user found or invalid key${this.wechat4OpenError ? `; WeChat 4.0 error: ${this.wechat4OpenError}` : ''}` - ) - } - } - - private tryOpenWechat4(): boolean { - try { - const client = new Wcdb4Client(this.rawKey) - client.open() - this.wcdb4Client = client - console.log('Opened WeChat 4.0 database with WechatExplorer WCDB native adapter') - return true - } catch (error) { - this.wechat4OpenError = error instanceof Error ? error.message : String(error) - console.warn('WeChat 4.0 open failed, fallback to 3.0 SQLCipher mode:', error) - this.wcdb4Client = null - return false - } - } - - private connectDb(dbPath: string): Database | null { - const targetPath = path.resolve(dbPath) - if (!fs.existsSync(targetPath)) { - console.error(`❌ 错误:数据库文件不存在于路径:${targetPath}`) - return null - } - - try { - const db = new DatabaseConstructor(targetPath, { - // verbose: console.log - }) - - // 配置加密方案 (Cipher Scheme) - db.pragma("cipher='sqlcipher'") - db.pragma('cipher_page_size = 1024') - db.pragma('legacy=3') - db.pragma('kdf_iter = 64000') - db.pragma('cipher_hmac_algorithm = HMAC_SHA1') - db.pragma('cipher_kdf_algorithm = PBKDF2_HMAC_SHA1') - - const processedKey = this.processRawKey(this.rawKey) - // console.log(`🔑 应用密钥 (前6位): ${processedKey.substring(0, 6)}...`) - - db.pragma(`key = "x'${processedKey}'"`) - - // 验证连接 - db.prepare('SELECT count(*) as count FROM sqlite_master').get() - - return db - } catch (error) { - console.error(`❌ 连接失败:`, error) - - const errString = String(error) - if (errString.includes('file is not a database')) { - console.error( - "💡 诊断建议:\n1. 请确认是否已安装 'better-sqlite3-multiple-ciphers'。\n2. 检查密钥是否正确(WeChat密钥与设备强绑定)。\n3. 尝试将页大小改为 4096 (db.pragma('cipher_page_size = 4096'))。" - ) - } else if (errString.includes('HMAC')) { - console.error( - '💡 诊断建议:密钥可能正确,但HMAC算法或页大小不匹配。尝试调整 cipher_hmac_algorithm。' - ) - } - - return null - } - } - - private processRawKey(key: string): string { - // 移除可能的 '0x' 前缀并修剪空格 - return key.replace(/^0x/i, '').trim() - } - - private getUser(): boolean { - const keyValuePath = path.join(WechatDb.WECHAT_DIR, 'KeyValue') - if (!fs.existsSync(keyValuePath)) return false - - const contents = fs.readdirSync(keyValuePath) - const potentialUsers = contents.filter((name) => !name.startsWith('.')) - - for (const user of potentialUsers) { - const dbPath = path.join(keyValuePath, user, 'KeyValue.db') - if (fs.existsSync(dbPath)) { - const db = this.connectDb(dbPath) - if (db) { - this.correctUserId = user - console.log(`Valid user found: ${user}`) - db.close() - return true - } + const client = new Wcdb4Client(rawKey) + client.open() + this.wcdb4Client = client + for (const table of client.getChatTables()) { + if (table.name.startsWith('Chat_')) { + this.chatMd5ToUsername.set(table.name.substring(5), table.db_number) } } - return false - } - - private getChatDbNumber(): { name: string; db_number: string }[] { - if (this.wcdb4Client) { - const chatDb = this.wcdb4Client.getChatTables() - this.chatMd5ToUsername.clear() - for (const table of chatDb) { - if (table.name.startsWith('Chat_')) { - this.chatMd5ToUsername.set(table.name.substring(5), table.db_number) - } - } - return chatDb - } - - const chatDb: { name: string; db_number: string }[] = [] - if (!this.correctUserId) return [] - - for (let i = 0; i < 10; i++) { - const dbPath = path.join(WechatDb.WECHAT_DIR, this.correctUserId, 'Message', `msg_${i}.db`) - if (!fs.existsSync(dbPath)) continue - - const db = this.connectDb(dbPath) - if (db) { - const tables = db - .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'Chat_%'") - .all() as { name: string }[] - for (const table of tables) { - chatDb.push({ name: table.name, db_number: `msg_${i}.db` }) - } - db.close() - } - } - return chatDb } public getUserList(nicknameFilter?: string): UserContact[] { - if (this.wcdb4Client) { - const keyword = (nicknameFilter || '').trim().toLowerCase() - return this.wcdb4Client - .getSessions() - .map((session) => ({ - m_nsUsrName: session.username, - nickname: session.nickname || session.username, - avatar: session.avatar - })) - .filter((contact) => { - if (!keyword) return true - return ( - contact.m_nsUsrName.toLowerCase().includes(keyword) || - contact.nickname.toLowerCase().includes(keyword) - ) - }) - } - - if (!this.correctUserId) return [] - const dbPath = path.join(WechatDb.WECHAT_DIR, this.correctUserId, 'Contact/wccontact_new2.db') - const db = this.connectDb(dbPath) - if (!db) return [] - - let query = "SELECT m_nsUsrName, nickname FROM WCContact WHERE m_nsUsrName NOT LIKE 'gh_%'" - if (nicknameFilter) { - query += ` AND nickname LIKE '%${nicknameFilter}%'` - } - - const results = db.prepare(query).all() as unknown as UserContact[] - db.close() - return results + const keyword = (nicknameFilter || '').trim().toLowerCase() + return this.wcdb4Client + .getSessions() + .map((session) => ({ + m_nsUsrName: session.username, + nickname: session.nickname || session.username, + avatar: session.avatar + })) + .filter((contact) => { + if (!keyword) return true + return ( + contact.m_nsUsrName.toLowerCase().includes(keyword) || + contact.nickname.toLowerCase().includes(keyword) + ) + }) } public getAllGroupContacts(): Record { - if (this.wcdb4Client) { - const groupContacts: Record = {} - for (const session of this.wcdb4Client.getSessions()) { - if (session.username.endsWith('@chatroom')) { - groupContacts[this.md5(session.username)] = session.nickname || session.username - } - } - return groupContacts - } - - if (!this.correctUserId) return {} - const dbPath = path.join(WechatDb.WECHAT_DIR, this.correctUserId, 'Group/group_new.db') - const db = this.connectDb(dbPath) - if (!db) return {} - - const results = db.prepare('SELECT m_nsUsrName, nickname FROM GroupContact').all() as { - m_nsUsrName: string - nickname: string - }[] - db.close() - const groupContacts: Record = {} - for (const row of results) { - if (row.m_nsUsrName && row.nickname) { - const md5 = this.md5(row.m_nsUsrName) - groupContacts[md5] = row.nickname + for (const session of this.wcdb4Client.getSessions()) { + if (session.username.endsWith('@chatroom')) { + groupContacts[this.md5(session.username)] = session.nickname || session.username } } return groupContacts } public getAllGroupMembers(): Record { - if (this.wcdb4Client) { - const members: Record = {} - for (const session of this.wcdb4Client.getSessions()) { - if (!session.username.endsWith('@chatroom')) continue - for (const member of this.wcdb4Client.getGroupMembers(session.username)) { - if (member.m_nsUsrName) { - members[member.m_nsUsrName] = member.nickname || member.m_nsUsrName - } - } - } - return members - } - - if (!this.correctUserId) return {} - const dbPath = path.join(WechatDb.WECHAT_DIR, this.correctUserId, 'Group/group_new.db') - const db = this.connectDb(dbPath) - if (!db) return {} - - // 查询 GroupMember 表 - const results = db.prepare('SELECT m_nsUsrName, nickname FROM GroupMember').all() as { - m_nsUsrName: string - nickname: string - }[] - db.close() - - const groupMembers: Record = {} - for (const row of results) { - if (row.m_nsUsrName && row.nickname) { - // 将 wxid (m_nsUsrName) 映射到昵称 - groupMembers[row.m_nsUsrName] = row.nickname - } - } - return groupMembers - } - - public getGroupMembersForChat(userMd5: string): Record { - if (this.wcdb4Client) { - const username = this.chatMd5ToUsername.get(userMd5) - if (!username || !username.endsWith('@chatroom')) return {} - - const members: Record = {} - for (const member of this.wcdb4Client.getGroupMembers(username)) { + const members: Record = {} + for (const session of this.wcdb4Client.getSessions()) { + if (!session.username.endsWith('@chatroom')) continue + for (const member of this.wcdb4Client.getGroupMembers(session.username)) { if (member.m_nsUsrName) { members[member.m_nsUsrName] = member.nickname || member.m_nsUsrName } } - return members } + return members + } - return this.getAllGroupMembers() + public getGroupMembersForChat(userMd5: string): Record { + const username = this.chatMd5ToUsername.get(userMd5) + if (!username || !username.endsWith('@chatroom')) return {} + + const members: Record = {} + for (const member of this.wcdb4Client.getGroupMembers(username)) { + if (member.m_nsUsrName) { + members[member.m_nsUsrName] = member.nickname || member.m_nsUsrName + } + } + return members } public getGroupMember(wxid: string, chatroomId?: string): GroupMemberInfo | null { - if (this.wcdb4Client && chatroomId) { - return ( - this.wcdb4Client - .getGroupMembers(chatroomId) - .find((member) => member.m_nsUsrName === wxid) || null - ) - } - - if (!this.correctUserId) return null - - // 检查缓存 - if (this.groupMemberCache.has(wxid)) { - return this.groupMemberCache.get(wxid) || null - } - - const dbPath = path.join(WechatDb.WECHAT_DIR, this.correctUserId, 'Group/group_new.db') - const db = this.connectDb(dbPath) - if (!db) { - this.groupMemberCache.set(wxid, null) - return null - } - - const result = db - .prepare( - 'SELECT m_nsUsrName, nickname, m_nsHeadImgUrl FROM GroupMember WHERE m_nsUsrName = ?' - ) - .get(wxid) as GroupMemberInfo - - db.close() - - // 写入缓存 - this.groupMemberCache.set(wxid, result || null) - - return result || null + if (!chatroomId) return null + return ( + this.wcdb4Client + .getGroupMembers(chatroomId) + .find((member) => member.m_nsUsrName === wxid) || null + ) } public getAllChatTables(): { name: string; db_number: string }[] { - return this.chatDb || [] + return this.wcdb4Client.getChatTables() } public getMyAvatarUrl(): string | undefined { - return this.wcdb4Client?.getMyAvatarUrl() + return this.wcdb4Client.getMyAvatarUrl() } - public getWcdb4Client(): Wcdb4Client | null { + public getWcdb4Client(): Wcdb4Client { return this.wcdb4Client } public close(): void { - this.wcdb4Client?.close() - this.wcdb4Client = null + this.wcdb4Client.close() } public getUserMessages(userMd5: string, startTime?: number, endTime?: number): WechatMessage[] { - if (this.wcdb4Client) { - const username = this.chatMd5ToUsername.get(userMd5) - if (!username) return [] - return this.wcdb4Client.getMessages(username, startTime, endTime).map((message) => ({ - ...message, - ...message.raw - })) - } - - if (!this.chatDb || !this.correctUserId) return [] - - const tableName = `Chat_${userMd5}` - const tableInfo = this.chatDb.find((t) => t.name === tableName) - - if (!tableInfo) { - console.error(`No table found for ${tableName}`) - return [] - } - - const dbPath = path.join( - WechatDb.WECHAT_DIR, - this.correctUserId, - 'Message', - tableInfo.db_number - ) - const db = this.connectDb(dbPath) - if (!db) return [] - - try { - let query = `SELECT * FROM ${tableName}` - const conditions: string[] = [] - - // console.log(startTime, endTime, 'time range') - - if (startTime) { - conditions.push(`msgCreateTime >= ${startTime}`) - } - if (endTime) { - conditions.push(`msgCreateTime <= ${endTime}`) - } - - if (conditions.length > 0) { - query += ` WHERE ${conditions.join(' AND ')}` - } - - // 添加排序以确保存储正确的顺序 - query += ` ORDER BY msgCreateTime ASC` - - const results = db.prepare(query).all() as unknown as WechatMessage[] - db.close() - return results - } catch (e) { - console.error(`Error querying messages for ${tableName}:`, e) - db.close() - return [] - } + const username = this.chatMd5ToUsername.get(userMd5) + if (!username) return [] + return this.wcdb4Client.getMessages(username, startTime, endTime).map((message) => ({ + ...message, + ...message.raw + })) } public searchAllMessages(keyword: string): string | null { - if (this.wcdb4Client) { - const lowerKeyword = keyword.trim().toLowerCase() - if (!lowerKeyword) return null - for (const session of this.wcdb4Client.getSessions()) { - const found = this.wcdb4Client - .getMessages(session.username) - .some((message) => message.msgContent.toLowerCase().includes(lowerKeyword)) - if (found) return `Chat_${this.md5(session.username)}` - } - return null - } - - if (!this.correctUserId) return null - - for (let i = 0; i < 10; i++) { - const dbName = `msg_${i}.db` - const dbPath = path.join(WechatDb.WECHAT_DIR, this.correctUserId, 'Message', dbName) - if (!fs.existsSync(dbPath)) continue - - const db = this.connectDb(dbPath) - if (!db) continue - - const tables = db - .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'Chat_%'") - .all() as { name: string }[] - for (const table of tables) { - try { - // 首先检查 msgContent 列是否存在以避免错误? - // 或者像 Swift 代码中那样使用 try/catch "抑制错误" - const results = db - .prepare(`SELECT 1 FROM ${table.name} WHERE msgContent LIKE '%${keyword}%' LIMIT 1`) - .get() - if (results) { - console.log(`Found keyword in ${table.name} (${dbName})`) - db.close() - return table.name - } - } catch { - // 忽略没有 msgContent 的表 - } - } - db.close() + const lowerKeyword = keyword.trim().toLowerCase() + if (!lowerKeyword) return null + for (const session of this.wcdb4Client.getSessions()) { + const found = this.wcdb4Client + .getMessages(session.username) + .some((message) => message.msgContent.toLowerCase().includes(lowerKeyword)) + if (found) return `Chat_${this.md5(session.username)}` } return null } public md5(str: string): string { - return crypto.createHash('md5').update(str).digest('hex') + return this.wcdb4Client.md5(str) } }