diff --git a/electron-builder.yml b/electron-builder.yml index ed5bc88..66db4ce 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -21,8 +21,13 @@ extraResources: filter: - '**/*' win: - executableName: wechatexplorer + # WCDB's Windows runtime checks the host executable name. The dev runtime is + # electron.exe, so keep the packaged executable compatible while preserving + # WechatExplorer as the product/shortcut name. + executableName: electron nsis: + oneClick: false + allowToChangeInstallationDirectory: true artifactName: ${name}-${version}-setup.${ext} shortcutName: ${productName} uninstallDisplayName: ${productName} diff --git a/electron.vite.config.ts b/electron.vite.config.ts index cbe0735..0383fe5 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -3,7 +3,19 @@ import { defineConfig } from 'electron-vite' import react from '@vitejs/plugin-react' export default defineConfig({ - main: {}, + main: { + build: { + rollupOptions: { + input: { + index: resolve('src/main/index.ts') + }, + output: { + entryFileNames: '[name].js' + }, + external: ['koffi'] + } + } + }, preload: {}, renderer: { resolve: { diff --git a/package.json b/package.json index d82533d..942e89d 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "start": "electron-vite preview", "dev": "electron-vite dev", "build": "npm run typecheck && electron-vite build", - "postinstall": "electron-builder install-app-deps", + "postinstall": "electron-builder install-app-deps && node scripts/prepare-electron-runtime.cjs", "build:unpack": "npm run build && electron-builder --config electron-builder.yml --dir", "build:win": "npm run build && electron-builder --config electron-builder.yml --win", "build:mac:x64": "electron-vite build && electron-builder --config electron-builder.yml --mac --x64", @@ -33,7 +33,7 @@ "@electron-toolkit/utils": "^4.0.0", "fs-extra": "^11.3.2", "fzstd": "^0.1.1", - "koffi": "^2.9.0", + "koffi": "^3.1.0", "openai": "^6.10.0", "silk-wasm": "^3.7.1" }, @@ -47,7 +47,7 @@ "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", - "electron": "^39.2.6", + "electron": "^43.0.0", "electron-builder": "^26.0.12", "electron-vite": "^5.0.0", "eslint": "^9.39.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 23ffa71..106d8a7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,7 +16,7 @@ specifiers: '@types/react': ^19.2.7 '@types/react-dom': ^19.2.3 '@vitejs/plugin-react': ^5.1.1 - electron: ^39.2.6 + electron: ^43.0.0 electron-builder: ^26.0.12 electron-vite: ^5.0.0 eslint: ^9.39.1 @@ -25,7 +25,7 @@ specifiers: eslint-plugin-react-refresh: ^0.4.24 fs-extra: ^11.3.2 fzstd: ^0.1.1 - koffi: ^2.9.0 + koffi: ^3.1.0 openai: ^6.10.0 prettier: ^3.7.4 react: ^19.2.1 @@ -35,11 +35,11 @@ specifiers: vite: ^7.2.6 dependencies: - '@electron-toolkit/preload': 3.0.2_electron@39.2.6 - '@electron-toolkit/utils': 4.0.0_electron@39.2.6 + '@electron-toolkit/preload': 3.0.2_electron@43.1.0 + '@electron-toolkit/utils': 4.0.0_electron@43.1.0 fs-extra: 11.3.2 fzstd: 0.1.1 - koffi: 2.16.2 + koffi: 3.1.0 openai: 6.10.0 silk-wasm: 3.7.1 @@ -53,7 +53,7 @@ devDependencies: '@types/react': 19.2.7 '@types/react-dom': 19.2.3_@types+react@19.2.7 '@vitejs/plugin-react': 5.1.2_vite@7.2.7 - electron: 39.2.6 + electron: 43.1.0 electron-builder: 26.0.12 electron-vite: 5.0.0_vite@7.2.7 eslint: 9.39.1 @@ -266,6 +266,10 @@ packages: ajv-keywords: 3.5.2_ajv@6.12.6 dev: true + /@electron-internal/extract-zip/1.0.4: + resolution: {integrity: sha512-Zr1Vs7E9tpCNhZHDAbFVXc2gEVCG9RqPDjrno5+bdgB6LRAuvgyMHJut4NCVyYwtAieapMzc3fiQ3CSTi75ARg==} + engines: {node: '>=22.12.0'} + /@electron-toolkit/eslint-config-prettier/3.0.0_fiitszekoa4sqtbwyewxi6kyy4: resolution: {integrity: sha512-YapmIOVkbYdHLuTa+ad1SAVtcqYL9A/SJsc7cxQokmhcwAwonGevNom37jBf9slXegcZ/Slh01I/JARG1yhNFw==} peerDependencies: @@ -298,12 +302,12 @@ packages: - supports-color dev: true - /@electron-toolkit/preload/3.0.2_electron@39.2.6: + /@electron-toolkit/preload/3.0.2_electron@43.1.0: resolution: {integrity: sha512-TWWPToXd8qPRfSXwzf5KVhpXMfONaUuRAZJHsKthKgZR/+LqX1dZVSSClQ8OTAEduvLGdecljCsoT2jSshfoUg==} peerDependencies: electron: '>=13.0.0' dependencies: - electron: 39.2.6 + electron: 43.1.0 dev: false /@electron-toolkit/tsconfig/2.0.0_@types+node@22.19.2: @@ -314,12 +318,12 @@ packages: '@types/node': 22.19.2 dev: true - /@electron-toolkit/utils/4.0.0_electron@39.2.6: + /@electron-toolkit/utils/4.0.0_electron@43.1.0: resolution: {integrity: sha512-qXSntwEzluSzKl4z5yFNBknmPGjPa3zFhE4mp9+h0cgokY5ornAeP+CJQDBhKsL1S58aOQfcwkD3NwLZCl+64g==} peerDependencies: electron: '>=13.0.0' dependencies: - electron: 39.2.6 + electron: 43.1.0 dev: false /@electron/asar/3.2.18: @@ -341,19 +345,18 @@ packages: minimist: 1.2.8 dev: true - /@electron/get/2.0.3: - resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==} - engines: {node: '>=12'} + /@electron/get/5.0.0: + resolution: {integrity: sha512-pjoBpru1KdEtcExBnuHAP1cAc/5faoedw0hzJkL3o4/IJp7HNF1+fbrdxT3gMYRX2oJfvnA/WXeCTVQpYYxyJA==} + engines: {node: '>=22.12.0'} dependencies: debug: 4.4.3 - env-paths: 2.2.1 - fs-extra: 8.1.0 - got: 11.8.6 + env-paths: 3.0.0 + graceful-fs: 4.2.11 progress: 2.0.3 - semver: 6.3.1 + semver: 7.7.3 sumchecker: 3.0.1 optionalDependencies: - global-agent: 3.0.0 + undici: 7.28.0 transitivePeerDependencies: - supports-color @@ -786,6 +789,104 @@ packages: '@jridgewell/sourcemap-codec': 1.5.5 dev: true + /@koromix/koffi-darwin-arm64/3.1.0: + resolution: {integrity: sha512-VEt5r3fXTfbejr83PnuOP0H7s9Zmazcs+lofu96DOcRkistlMsn59wYyWiKpyAjs9PCgm0Ykh62ChZ3CGMmIOg==} + cpu: [arm64] + os: [darwin] + dev: false + optional: true + + /@koromix/koffi-darwin-x64/3.1.0: + resolution: {integrity: sha512-n/tVRB9xIzdXT5H3zZt8ueThgWTSDL+yU7PWnU8wbZPBSawP/otx3swQyd6nMOqj1bmHgSHopiKSBXRS9pllmg==} + cpu: [x64] + os: [darwin] + dev: false + optional: true + + /@koromix/koffi-freebsd-arm64/3.1.0: + resolution: {integrity: sha512-vazoPYIhOAlXZksVIqDRMIID4VeUZKx8F3dR90hOobT2ATyOkqNS5dv5UCV7Q7DSq22lQTrdbvENBAhROzCp0w==} + cpu: [arm64] + os: [freebsd] + dev: false + optional: true + + /@koromix/koffi-freebsd-ia32/3.1.0: + resolution: {integrity: sha512-Vm7Uc97ru6RTSVmae2zCZZQeaizqVZ8WoU4+gG4H03Qe+WOj7kbKt/MxT7VBzdbPYIU5ZJeG/ZED1YlZyab6eQ==} + cpu: [ia32] + os: [freebsd] + dev: false + optional: true + + /@koromix/koffi-freebsd-x64/3.1.0: + resolution: {integrity: sha512-N+VuVWjoiYPy1Go5mRadZ3B6RM5Qz+eCLhj2LXrMlefbUJ+O4gg7teCUGvPGfBEHDgmSN4yYUrfQmdJC10vOYw==} + cpu: [x64] + os: [freebsd] + dev: false + optional: true + + /@koromix/koffi-linux-arm64/3.1.0: + resolution: {integrity: sha512-Wx5iOkeALe2ympLdiYwRpIg5qUkyQIv8N2foZ9rRker0uE7ZtXew2RRkbEgMir4b0yDYR1zyXd6B62GUzLtZ/g==} + cpu: [arm64] + os: [linux] + dev: false + optional: true + + /@koromix/koffi-linux-ia32/3.1.0: + resolution: {integrity: sha512-1DjYm1QehXU0dgn0uE+FGYOb3Of7GiTMqLS+ZI2gbl1b+h76sz4LRBvDVrQyAmSMVVU8/7696S21YgE/iBhBVg==} + cpu: [ia32] + os: [linux] + dev: false + optional: true + + /@koromix/koffi-linux-loong64/3.1.0: + resolution: {integrity: sha512-NOa0LdyltdESz3oeTqUH6MErHVoJOHoeXIsEp6xIMTUh4eKXEtlDQeoK6EYqo0DnBt83Xud95qLvi4Aw12pG4Q==} + cpu: [loong64] + os: [linux] + dev: false + optional: true + + /@koromix/koffi-linux-riscv64/3.1.0: + resolution: {integrity: sha512-Ye6kiXZCGxGtAIXSly6XuOP5tJZNYOZ2eVg33k1MilKrzimAy9Mpw4d6e9+Sfsc1jesgeNYs1sb5iaI8HS3ncA==} + cpu: [riscv64] + os: [linux] + dev: false + optional: true + + /@koromix/koffi-linux-x64/3.1.0: + resolution: {integrity: sha512-3yQTOkQrMna4VX+yeyfYImBjLlGrItMpsWyfaW1uSiz/A6GRydqdwYH7DWnp4Z+RSGYZpsewkf7byMc8pOOQKA==} + cpu: [x64] + os: [linux] + dev: false + optional: true + + /@koromix/koffi-openbsd-ia32/3.1.0: + resolution: {integrity: sha512-/cDoFHb9yx4+yoT3GUpnKnfi3W2drG+/Ewo0TTZaQHb4PsxnYYyT6V8+t4cL5XXbQcTTcOsZxpmBRrn0NBa3dA==} + cpu: [ia32] + os: [openbsd] + dev: false + optional: true + + /@koromix/koffi-openbsd-x64/3.1.0: + resolution: {integrity: sha512-CoQdqgnKvWgTXXZlUst8cBRQEov7QsxlTN2WAsu9wez01Xe6gEcH/zYePANualzzCbnaELfe5P0rA80QkoDuPA==} + cpu: [x64] + os: [openbsd] + dev: false + optional: true + + /@koromix/koffi-win32-ia32/3.1.0: + resolution: {integrity: sha512-WjrA+DEkpy0xEHu48+NSOboHhTnzkIfsFuq3d/WrSs+T9WflWRng3jC7mdJxmR4eHb6i6BqjW3k/U0mNUTjFPA==} + cpu: [ia32] + os: [win32] + dev: false + optional: true + + /@koromix/koffi-win32-x64/3.1.0: + resolution: {integrity: sha512-tnK5+IkzQBauQAQSzuyjso8OOIQRlaTZS39xIWpfqVYDLVDIuLDQk/WwHcOrR5yxlDrZq9ygiebBTOfcJFia7w==} + cpu: [x64] + os: [win32] + dev: false + optional: true + /@malept/cross-spawn-promise/2.0.0: resolution: {integrity: sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==} engines: {node: '>= 12.13.0'} @@ -1011,12 +1112,14 @@ packages: /@sindresorhus/is/4.6.0: resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} + dev: true /@szmarczak/http-timer/4.0.6: resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} engines: {node: '>=10'} dependencies: defer-to-connect: 2.0.1 + dev: true /@tootallnate/once/2.0.0: resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} @@ -1059,6 +1162,7 @@ packages: '@types/keyv': 3.1.4 '@types/node': 22.19.2 '@types/responselike': 1.0.3 + dev: true /@types/debug/4.1.12: resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -1085,6 +1189,7 @@ packages: /@types/http-cache-semantics/4.0.4: resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + dev: true /@types/json-schema/7.0.15: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1100,6 +1205,7 @@ packages: resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} dependencies: '@types/node': 22.19.2 + dev: true /@types/ms/2.1.0: resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -1109,6 +1215,12 @@ packages: resolution: {integrity: sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==} dependencies: undici-types: 6.21.0 + dev: true + + /@types/node/24.13.3: + resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} + dependencies: + undici-types: 7.18.2 /@types/plist/3.0.5: resolution: {integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==} @@ -1136,18 +1248,13 @@ packages: resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} dependencies: '@types/node': 22.19.2 + dev: true /@types/verror/1.10.11: resolution: {integrity: sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==} dev: true optional: true - /@types/yauzl/2.10.3: - resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} - dependencies: - '@types/node': 22.19.2 - optional: true - /@typescript-eslint/eslint-plugin/8.49.0_nj5tjtfh637kziiwuhl3v5a4iq: resolution: {integrity: sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1591,11 +1698,6 @@ packages: readable-stream: 3.6.2 dev: true - /boolean/3.2.0: - resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - optional: true - /brace-expansion/1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} dependencies: @@ -1621,9 +1723,6 @@ packages: update-browserslist-db: 1.2.2_browserslist@4.28.1 dev: true - /buffer-crc32/0.2.13: - resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} - /buffer-from/1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} dev: true @@ -1703,6 +1802,7 @@ packages: /cacheable-lookup/5.0.4: resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} engines: {node: '>=10.6.0'} + dev: true /cacheable-request/7.0.4: resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} @@ -1715,6 +1815,7 @@ packages: lowercase-keys: 2.0.0 normalize-url: 6.1.0 responselike: 2.0.1 + dev: true /call-bind-apply-helpers/1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} @@ -1812,6 +1913,7 @@ packages: resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} dependencies: mimic-response: 1.0.1 + dev: true /clone/1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} @@ -1929,6 +2031,7 @@ packages: engines: {node: '>=10'} dependencies: mimic-response: 3.1.0 + dev: true /deep-is/0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1943,6 +2046,7 @@ packages: /defer-to-connect/2.0.1: resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} engines: {node: '>=10'} + dev: true /define-data-property/1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} @@ -1951,6 +2055,7 @@ packages: es-define-property: 1.0.1 es-errors: 1.3.0 gopd: 1.2.0 + dev: true /define-properties/1.2.1: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} @@ -1959,6 +2064,7 @@ packages: define-data-property: 1.1.4 has-property-descriptors: 1.0.2 object-keys: 1.1.1 + dev: true /delayed-stream/1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} @@ -1970,10 +2076,6 @@ packages: engines: {node: '>=8'} dev: true - /detect-node/2.1.0: - resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} - optional: true - /dir-compare/4.2.0: resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==} dependencies: @@ -2117,15 +2219,14 @@ packages: - supports-color dev: true - /electron/39.2.6: - resolution: {integrity: sha512-dHBgTodWBZd+tL1Dt0PSh/CFLHeDkFCTKCTXu1dgPhlE9Z3k2zzlBQ9B2oW55CFsKanBDHiUomHJNw0XaSdQpA==} - engines: {node: '>= 12.20.55'} + /electron/43.1.0: + resolution: {integrity: sha512-DPfxpQLd4NL3BJ8DBxYAfmLUKKesF5Rx9dQx5FyczAP8bhOPScjHE48GArVeXu68LlAainuwkmQTQvdZwpIIAQ==} + engines: {node: '>= 22.12.0'} hasBin: true - requiresBuild: true dependencies: - '@electron/get': 2.0.3 - '@types/node': 22.19.2 - extract-zip: 2.0.1 + '@electron-internal/extract-zip': 1.0.4 + '@electron/get': 5.0.0 + '@types/node': 24.13.3 transitivePeerDependencies: - supports-color @@ -2148,10 +2249,16 @@ packages: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} dependencies: once: 1.4.0 + dev: true /env-paths/2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} + dev: true + + /env-paths/3.0.0: + resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} /err-code/2.0.3: resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} @@ -2220,10 +2327,12 @@ packages: /es-define-property/1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} + dev: true /es-errors/1.3.0: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + dev: true /es-iterator-helpers/1.2.1: resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} @@ -2280,10 +2389,6 @@ packages: is-symbol: 1.1.1 dev: true - /es6-error/4.1.1: - resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} - optional: true - /esbuild/0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} @@ -2326,6 +2431,7 @@ packages: /escape-string-regexp/4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + dev: true /eslint-config-prettier/10.1.8_eslint@9.39.1: resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} @@ -2511,19 +2617,6 @@ packages: resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} dev: true - /extract-zip/2.0.1: - resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} - engines: {node: '>= 10.17.0'} - hasBin: true - dependencies: - debug: 4.4.3 - get-stream: 5.2.0 - yauzl: 2.10.0 - optionalDependencies: - '@types/yauzl': 2.10.3 - transitivePeerDependencies: - - supports-color - /extsprintf/1.4.1: resolution: {integrity: sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==} engines: {'0': node >=0.6.0} @@ -2546,11 +2639,6 @@ packages: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} dev: true - /fd-slicer/1.1.0: - resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} - dependencies: - pend: 1.2.0 - /fdir/6.5.0_picomatch@4.0.3: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -2639,14 +2727,6 @@ packages: jsonfile: 6.2.0 universalify: 2.0.1 - /fs-extra/8.1.0: - resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} - engines: {node: '>=6 <7 || >=8'} - dependencies: - graceful-fs: 4.2.11 - jsonfile: 4.0.0 - universalify: 0.1.2 - /fs-extra/9.1.0: resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} engines: {node: '>=10'} @@ -2743,6 +2823,7 @@ packages: engines: {node: '>=8'} dependencies: pump: 3.0.3 + dev: true /get-symbol-description/1.1.0: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} @@ -2796,18 +2877,6 @@ packages: once: 1.4.0 dev: true - /global-agent/3.0.0: - resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} - engines: {node: '>=10.0'} - dependencies: - boolean: 3.2.0 - es6-error: 4.1.1 - matcher: 3.0.0 - roarr: 2.15.4 - semver: 7.7.3 - serialize-error: 7.0.1 - optional: true - /globals/14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -2824,10 +2893,12 @@ packages: dependencies: define-properties: 1.2.1 gopd: 1.2.0 + dev: true /gopd/1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + dev: true /got/11.8.6: resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} @@ -2844,6 +2915,7 @@ packages: lowercase-keys: 2.0.0 p-cancelable: 2.1.1 responselike: 2.0.1 + dev: true /graceful-fs/4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -2862,6 +2934,7 @@ packages: resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} dependencies: es-define-property: 1.0.1 + dev: true /has-proto/1.2.0: resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} @@ -2908,6 +2981,7 @@ packages: /http-cache-semantics/4.2.0: resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + dev: true /http-proxy-agent/5.0.0: resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} @@ -2936,6 +3010,7 @@ packages: dependencies: quick-lru: 5.1.1 resolve-alpn: 1.2.1 + dev: true /https-proxy-agent/5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} @@ -3313,6 +3388,7 @@ packages: /json-buffer/3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + dev: true /json-schema-traverse/0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -3322,21 +3398,12 @@ packages: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} dev: true - /json-stringify-safe/5.0.1: - resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - optional: true - /json5/2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} hasBin: true dev: true - /jsonfile/4.0.0: - resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} - optionalDependencies: - graceful-fs: 4.2.11 - /jsonfile/6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} dependencies: @@ -3358,9 +3425,25 @@ packages: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} dependencies: json-buffer: 3.0.1 + dev: true - /koffi/2.16.2: - resolution: {integrity: sha512-owU0MRwv6xkrVqCd+33uw6BaYppkTRXbO/rVdJNI2dvZG0gzyRhYwW25eWtc5pauwK8TGh3AbkFONSezdykfSA==} + /koffi/3.1.0: + resolution: {integrity: sha512-0mCvdjTJBXioiaKNz0vajAEdWtfM5qyhVXSq+wQrrU3odzNvl/J7Cqna79QpNo9mfoKpQgGsyFFDRtDACCwGrQ==} + optionalDependencies: + '@koromix/koffi-darwin-arm64': 3.1.0 + '@koromix/koffi-darwin-x64': 3.1.0 + '@koromix/koffi-freebsd-arm64': 3.1.0 + '@koromix/koffi-freebsd-ia32': 3.1.0 + '@koromix/koffi-freebsd-x64': 3.1.0 + '@koromix/koffi-linux-arm64': 3.1.0 + '@koromix/koffi-linux-ia32': 3.1.0 + '@koromix/koffi-linux-loong64': 3.1.0 + '@koromix/koffi-linux-riscv64': 3.1.0 + '@koromix/koffi-linux-x64': 3.1.0 + '@koromix/koffi-openbsd-ia32': 3.1.0 + '@koromix/koffi-openbsd-x64': 3.1.0 + '@koromix/koffi-win32-ia32': 3.1.0 + '@koromix/koffi-win32-x64': 3.1.0 dev: false /lazy-val/1.0.5: @@ -3408,6 +3491,7 @@ packages: /lowercase-keys/2.0.0: resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} engines: {node: '>=8'} + dev: true /lru-cache/10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -3462,13 +3546,6 @@ packages: - supports-color dev: true - /matcher/3.0.0: - resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} - engines: {node: '>=10'} - dependencies: - escape-string-regexp: 4.0.0 - optional: true - /math-intrinsics/1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -3500,10 +3577,12 @@ packages: /mimic-response/1.0.1: resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} engines: {node: '>=4'} + dev: true /mimic-response/3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} + dev: true /minimatch/10.1.1: resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} @@ -3657,6 +3736,7 @@ packages: /normalize-url/6.1.0: resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} engines: {node: '>=10'} + dev: true /object-assign/4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} @@ -3671,6 +3751,7 @@ packages: /object-keys/1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} + dev: true /object.assign/4.1.7: resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} @@ -3718,6 +3799,7 @@ packages: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: wrappy: 1.0.2 + dev: true /onetime/5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} @@ -3778,6 +3860,7 @@ packages: /p-cancelable/2.1.1: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} + dev: true /p-limit/3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} @@ -3843,9 +3926,6 @@ packages: engines: {node: '>=12', npm: '>=6'} dev: true - /pend/1.2.0: - resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} - /picocolors/1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} dev: true @@ -3935,6 +4015,7 @@ packages: dependencies: end-of-stream: 1.4.5 once: 1.4.0 + dev: true /punycode/2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} @@ -3944,6 +4025,7 @@ packages: /quick-lru/5.1.1: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} + dev: true /react-dom/19.2.1_react@19.2.1: resolution: {integrity: sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==} @@ -4026,6 +4108,7 @@ packages: /resolve-alpn/1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + dev: true /resolve-from/4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} @@ -4045,6 +4128,7 @@ packages: resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} dependencies: lowercase-keys: 2.0.0 + dev: true /restore-cursor/3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} @@ -4067,18 +4151,6 @@ packages: glob: 7.2.3 dev: true - /roarr/2.15.4: - resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} - engines: {node: '>=8.0'} - dependencies: - boolean: 3.2.0 - detect-node: 2.1.0 - globalthis: 1.0.4 - json-stringify-safe: 5.0.1 - semver-compare: 1.0.0 - sprintf-js: 1.1.3 - optional: true - /rollup/4.53.3: resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -4161,10 +4233,6 @@ packages: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} dev: true - /semver-compare/1.0.0: - resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} - optional: true - /semver/5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -4173,19 +4241,13 @@ packages: /semver/6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + dev: true /semver/7.7.3: resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} hasBin: true - /serialize-error/7.0.1: - resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} - engines: {node: '>=10'} - dependencies: - type-fest: 0.13.1 - optional: true - /set-function-length/1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -4341,10 +4403,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /sprintf-js/1.1.3: - resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} - optional: true - /ssri/9.0.1: resolution: {integrity: sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -4559,11 +4617,6 @@ packages: prelude-ls: 1.2.1 dev: true - /type-fest/0.13.1: - resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} - engines: {node: '>=10'} - optional: true - /typed-array-buffer/1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -4644,6 +4697,15 @@ packages: /undici-types/6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + dev: true + + /undici-types/7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + /undici/7.28.0: + resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} + engines: {node: '>=20.18.1'} + optional: true /unique-filename/2.0.1: resolution: {integrity: sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==} @@ -4659,10 +4721,6 @@ packages: imurmurhash: 0.1.4 dev: true - /universalify/0.1.2: - resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} - engines: {node: '>= 4.0.0'} - /universalify/2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -4845,6 +4903,7 @@ packages: /wrappy/1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true /xmlbuilder/15.1.1: resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} @@ -4882,12 +4941,6 @@ packages: yargs-parser: 21.1.1 dev: true - /yauzl/2.10.0: - resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} - dependencies: - buffer-crc32: 0.2.13 - fd-slicer: 1.1.0 - /yocto-queue/0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} diff --git a/resources/key/win32/x64/wx_key.dll b/resources/key/win32/x64/wx_key.dll new file mode 100644 index 0000000..9ab5f7b Binary files /dev/null and b/resources/key/win32/x64/wx_key.dll differ diff --git a/resources/runtime/win32/msvcp140.dll b/resources/runtime/win32/msvcp140.dll new file mode 100644 index 0000000..554d2ff Binary files /dev/null and b/resources/runtime/win32/msvcp140.dll differ diff --git a/resources/runtime/win32/msvcp140_1.dll b/resources/runtime/win32/msvcp140_1.dll new file mode 100644 index 0000000..184514f Binary files /dev/null and b/resources/runtime/win32/msvcp140_1.dll differ diff --git a/resources/runtime/win32/vcruntime140.dll b/resources/runtime/win32/vcruntime140.dll new file mode 100644 index 0000000..950b587 Binary files /dev/null and b/resources/runtime/win32/vcruntime140.dll differ diff --git a/resources/runtime/win32/vcruntime140_1.dll b/resources/runtime/win32/vcruntime140_1.dll new file mode 100644 index 0000000..a481970 Binary files /dev/null and b/resources/runtime/win32/vcruntime140_1.dll differ diff --git a/resources/wcdb/win32/arm64/WCDB.dll b/resources/wcdb/win32/arm64/WCDB.dll new file mode 100644 index 0000000..60c3508 Binary files /dev/null and b/resources/wcdb/win32/arm64/WCDB.dll differ diff --git a/resources/wcdb/win32/arm64/wcdb_api.dll b/resources/wcdb/win32/arm64/wcdb_api.dll new file mode 100644 index 0000000..1a1139c Binary files /dev/null and b/resources/wcdb/win32/arm64/wcdb_api.dll differ diff --git a/resources/wcdb/win32/x64/SDL2.dll b/resources/wcdb/win32/x64/SDL2.dll new file mode 100644 index 0000000..e26bcb1 Binary files /dev/null and b/resources/wcdb/win32/x64/SDL2.dll differ diff --git a/resources/wcdb/win32/x64/WCDB.dll b/resources/wcdb/win32/x64/WCDB.dll new file mode 100644 index 0000000..8127079 Binary files /dev/null and b/resources/wcdb/win32/x64/WCDB.dll differ diff --git a/resources/wcdb/win32/x64/wcdb_api.dll b/resources/wcdb/win32/x64/wcdb_api.dll new file mode 100644 index 0000000..8842616 Binary files /dev/null and b/resources/wcdb/win32/x64/wcdb_api.dll differ diff --git a/resources/welive/linux/x64/resources/linux/x64/libwcdb_api.so b/resources/welive/linux/x64/resources/linux/x64/libwcdb_api.so new file mode 100644 index 0000000..da89e3f Binary files /dev/null and b/resources/welive/linux/x64/resources/linux/x64/libwcdb_api.so differ diff --git a/resources/welive/linux/x64/welive b/resources/welive/linux/x64/welive new file mode 100644 index 0000000..eced182 Binary files /dev/null and b/resources/welive/linux/x64/welive differ diff --git a/resources/welive/macos/arm64/resources/macos/universal/WCDB.framework/Versions/2.1.15/WCDB b/resources/welive/macos/arm64/resources/macos/universal/WCDB.framework/Versions/2.1.15/WCDB new file mode 100644 index 0000000..8ee006b Binary files /dev/null and b/resources/welive/macos/arm64/resources/macos/universal/WCDB.framework/Versions/2.1.15/WCDB differ diff --git a/resources/welive/macos/arm64/resources/macos/universal/libWCDB.dylib b/resources/welive/macos/arm64/resources/macos/universal/libWCDB.dylib new file mode 100644 index 0000000..ef6d5df Binary files /dev/null and b/resources/welive/macos/arm64/resources/macos/universal/libWCDB.dylib differ diff --git a/resources/welive/macos/arm64/resources/macos/universal/libwcdb_api.dylib b/resources/welive/macos/arm64/resources/macos/universal/libwcdb_api.dylib new file mode 100644 index 0000000..b7b34e2 Binary files /dev/null and b/resources/welive/macos/arm64/resources/macos/universal/libwcdb_api.dylib differ diff --git a/resources/welive/macos/arm64/welive b/resources/welive/macos/arm64/welive new file mode 100644 index 0000000..d2c882d Binary files /dev/null and b/resources/welive/macos/arm64/welive differ diff --git a/resources/welive/macos/x64/resources/macos/universal/WCDB.framework/Versions/2.1.15/WCDB b/resources/welive/macos/x64/resources/macos/universal/WCDB.framework/Versions/2.1.15/WCDB new file mode 100644 index 0000000..5030b42 Binary files /dev/null and b/resources/welive/macos/x64/resources/macos/universal/WCDB.framework/Versions/2.1.15/WCDB differ diff --git a/resources/welive/macos/x64/resources/macos/universal/libWCDB.dylib b/resources/welive/macos/x64/resources/macos/universal/libWCDB.dylib new file mode 100644 index 0000000..ef6d5df Binary files /dev/null and b/resources/welive/macos/x64/resources/macos/universal/libWCDB.dylib differ diff --git a/resources/welive/macos/x64/resources/macos/universal/libwcdb_api.dylib b/resources/welive/macos/x64/resources/macos/universal/libwcdb_api.dylib new file mode 100644 index 0000000..fc36ee7 Binary files /dev/null and b/resources/welive/macos/x64/resources/macos/universal/libwcdb_api.dylib differ diff --git a/resources/welive/macos/x64/welive b/resources/welive/macos/x64/welive new file mode 100644 index 0000000..2b44234 Binary files /dev/null and b/resources/welive/macos/x64/welive differ diff --git a/resources/welive/win32/arm64/resources/win32/arm64/WCDB.dll b/resources/welive/win32/arm64/resources/win32/arm64/WCDB.dll new file mode 100644 index 0000000..60c3508 Binary files /dev/null and b/resources/welive/win32/arm64/resources/win32/arm64/WCDB.dll differ diff --git a/resources/welive/win32/arm64/resources/win32/arm64/wcdb_api.dll b/resources/welive/win32/arm64/resources/win32/arm64/wcdb_api.dll new file mode 100644 index 0000000..8f333ac Binary files /dev/null and b/resources/welive/win32/arm64/resources/win32/arm64/wcdb_api.dll differ diff --git a/resources/welive/win32/arm64/welive.exe b/resources/welive/win32/arm64/welive.exe new file mode 100644 index 0000000..55c5c85 Binary files /dev/null and b/resources/welive/win32/arm64/welive.exe differ diff --git a/resources/welive/win32/x64/resources/win32/x64/WCDB.dll b/resources/welive/win32/x64/resources/win32/x64/WCDB.dll new file mode 100644 index 0000000..8127079 Binary files /dev/null and b/resources/welive/win32/x64/resources/win32/x64/WCDB.dll differ diff --git a/resources/welive/win32/x64/resources/win32/x64/wcdb_api.dll b/resources/welive/win32/x64/resources/win32/x64/wcdb_api.dll new file mode 100644 index 0000000..b96e044 Binary files /dev/null and b/resources/welive/win32/x64/resources/win32/x64/wcdb_api.dll differ diff --git a/resources/welive/win32/x64/welive.exe b/resources/welive/win32/x64/welive.exe new file mode 100644 index 0000000..50470e5 Binary files /dev/null and b/resources/welive/win32/x64/welive.exe differ diff --git a/scripts/prepare-electron-runtime.cjs b/scripts/prepare-electron-runtime.cjs new file mode 100644 index 0000000..86299d9 --- /dev/null +++ b/scripts/prepare-electron-runtime.cjs @@ -0,0 +1,48 @@ +const fs = require('node:fs') +const path = require('node:path') + +const runtimeNames = [ + 'msvcp140.dll', + 'msvcp140_1.dll', + 'vcruntime140.dll', + 'vcruntime140_1.dll' +] + +function copyIfDifferent(sourcePath, targetPath) { + const source = fs.statSync(sourcePath) + const targetExists = fs.existsSync(targetPath) + + if (targetExists) { + const target = fs.statSync(targetPath) + if (target.size === source.size && target.mtimeMs >= source.mtimeMs) { + return false + } + } + + fs.copyFileSync(sourcePath, targetPath) + return true +} + +function main() { + if (process.platform !== 'win32') return + + const projectRoot = path.resolve(__dirname, '..') + const sourceDir = path.join(projectRoot, 'resources', 'runtime', 'win32') + const targetDir = path.join(projectRoot, 'node_modules', 'electron', 'dist') + + if (!fs.existsSync(sourceDir) || !fs.existsSync(targetDir)) return + + let copiedCount = 0 + for (const name of runtimeNames) { + const sourcePath = path.join(sourceDir, name) + const targetPath = path.join(targetDir, name) + if (!fs.existsSync(sourcePath)) continue + if (copyIfDifferent(sourcePath, targetPath)) copiedCount += 1 + } + + if (copiedCount > 0) { + console.log(`[prepare-electron-runtime] synced ${copiedCount} runtime DLL(s) to ${targetDir}`) + } +} + +main() diff --git a/src/main/index.ts b/src/main/index.ts index cd41e72..30663af 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,8 +1,10 @@ +import './preload-env' import { app, shell, BrowserWindow, ipcMain, nativeImage, clipboard, Menu, Tray } from 'electron' import { join } from 'path' import { electronApp, optimizer, is } from '@electron-toolkit/utils' import icon from '../../resources/icon.png?asset' import { WechatDb } from './wechat-db' +import { bootstrapWcdbNative } from './wcdb4-client' import { VoiceService } from './voice-service' import { StickerService } from './sticker-service' import { parseMessageContent } from './message-parser' @@ -11,6 +13,7 @@ import { exportGroupReport } from './group-report-service' import { GroupReportExportRequest } from '../shared/group-report' import { DatabaseKeyStore } from './database-key-store' import { KeyServiceMac } from './key-service-mac' +import { KeyService as KeyServiceWin } from './key-service-win' import * as chat from './services/chat-service' import { apiServer @@ -33,14 +36,17 @@ let imageDecryptService: ImageDecryptService | null = null let stickerService: StickerService | null = null const databaseKeyStore = new DatabaseKeyStore() const keyServiceMac = new KeyServiceMac() +const keyServiceWin = new KeyServiceWin() let tray: Tray | null = null + +// WCDB's Windows runtime checks the host application name during wcdb_init. +// Mirroring WeFlow's name unblocks the -1006 init failure on Windows. +app.setName(process.platform === 'win32' ? 'WeFlow' : 'WechatExplorer') +let dbInitInFlight: Promise<{ success: boolean; monitoring?: boolean; error?: string }> | null = null const BUILD_MARK = 'wechat4-local-http-api-2026-07-03' const TRAY_MODE = process.argv.includes('--tray') || (process.env['WXE_TRAY'] || '').toString() === '1' -// WechatExplorer's WCDB native library runs InitProtection before wcdb_init. -// In dev, matching the host app name avoids failing the native protection gate. -app.setName('WechatExplorer') function createWindow(): void { // 创建浏览器窗口 @@ -78,6 +84,17 @@ function createWindow(): void { // 某些 API 只能在此事件发生后使用 app.whenReady().then(async () => { console.log(`WechatExplorer main build: ${BUILD_MARK}`) + + // WCDB's Windows runtime returns -1006 if wcdb_init is called more than once + // per process. Bootstrap native once here so any later Wcdb4Client instance + // reuses the already-initialized library and skips wcdb_init. + try { + bootstrapWcdbNative() + console.log('[WCDB4] bootstrap complete at whenReady top') + } catch (bootstrapError) { + console.error('[WCDB4] bootstrap failed at whenReady top:', bootstrapError) + } + // 为窗口设置应用程序用户模型 ID electronApp.setAppUserModelId('com.electron') @@ -91,11 +108,27 @@ app.whenReady().then(async () => { // IPC test ipcMain.on('ping', () => console.log('pong')) - ipcMain.handle('db:init', (_, key: string) => { + ipcMain.handle('db:init', async (_, key: string) => { + if (dbInitInFlight) return dbInitInFlight + + dbInitInFlight = (async () => { try { const trimmedKey = String(key || '').trim() console.log(`db:init build=${BUILD_MARK} keyLength=${trimmedKey.length}`) - const nextWechatDb = new WechatDb(key) + const settings = loadSettings() + if ( + chat.isReady() && + chat.getCurrentKey().replace(/^0x/i, '').trim() === trimmedKey.replace(/^0x/i, '') && + (!settings.dbRoot || chat.getCurrentAccountRoot() === settings.dbRoot) + ) { + console.log('[WCDB4] db:init reuse current connection') + return { success: true, monitoring: true } + } + const nextWechatDb = await WechatDb.create(key, settings.dbRoot) + const resolvedRoot = nextWechatDb.getWcdb4Client().getAccountRoot() + if (resolvedRoot && resolvedRoot !== settings.dbRoot) { + saveSettings({ ...settings, dbRoot: resolvedRoot }) + } chat.setChatDb(nextWechatDb) const wcdb4Client = nextWechatDb.getWcdb4Client() voiceService = new VoiceService(wcdb4Client) @@ -110,7 +143,12 @@ app.whenReady().then(async () => { } catch (error) { console.error('Failed to init DB:', error) return { success: false, error: error instanceof Error ? error.message : String(error) } + } finally { + dbInitInFlight = null } + })() + + return dbInitInFlight }) ipcMain.handle('key:getSavedDbKey', async () => databaseKeyStore.load()) @@ -125,9 +163,13 @@ app.whenReady().then(async () => { ipcMain.handle('key:clearSavedDbKey', async () => databaseKeyStore.clear()) ipcMain.handle('key:autoGetDbKey', async (event) => { - const result = await keyServiceMac.autoGetDbKey((message) => { + const onStatus = (message: string): void => { if (!event.sender.isDestroyed()) event.sender.send('key:dbKeyStatus', { message }) - }) + } + const result = + process.platform === 'win32' + ? await keyServiceWin.autoGetDbKey(60_000, onStatus) + : await keyServiceMac.autoGetDbKey(onStatus) if (!result.success || !result.key) return result const saved = await databaseKeyStore.save(result.key) @@ -140,6 +182,10 @@ app.whenReady().then(async () => { ipcMain.handle('db:getContacts', (_, filter?: string) => chat.listContacts(filter)) + ipcMain.handle('db:getContactAvatars', (_, usernames: string[]) => + chat.getContactAvatars(usernames) + ) + ipcMain.handle('db:getMessages', (_, userMd5: string, startTime?: number, endTime?: number) => chat.listMessages(userMd5, startTime, endTime) ) diff --git a/src/main/key-service-win.ts b/src/main/key-service-win.ts new file mode 100644 index 0000000..54cecf0 --- /dev/null +++ b/src/main/key-service-win.ts @@ -0,0 +1,1246 @@ +// @ts-nocheck +// Windows native bridge adapted from WeFlow. Koffi Win32 callbacks and optional +// helpers are intentionally dynamic; keep this file isolated from strict TS. +import { app } from 'electron' +import { join, dirname, delimiter } from 'path' +import { existsSync, copyFileSync, mkdirSync } from 'fs' +import { execFile, spawn } from 'child_process' +import { promisify } from 'util' +import os from 'os' +import crypto from 'crypto' + +const execFileAsync = promisify(execFile) + +type DbKeyResult = { success: boolean; key?: string; error?: string; logs?: string[] } +type ImageKeyResult = { success: boolean; xorKey?: number; aesKey?: string; verified?: boolean; error?: string } +type DbKeyPollResult = + | { status: 'success'; key: string; loginRequiredDetected: boolean } + | { status: 'process-ended'; loginRequiredDetected: boolean } + | { status: 'timeout'; loginRequiredDetected: boolean } + +export class KeyService { + private readonly isMac = process.platform === 'darwin' + private koffi: any = null + private lib: any = null + private initialized = false + private initHook: any = null + private pollKeyData: any = null + private getStatusMessage: any = null + private cleanupHook: any = null + private getLastErrorMsg: any = null + private getImageKeyDll: any = null + private lastLoadError = '' + + // Win32 APIs + private kernel32: any = null + private user32: any = null + private advapi32: any = null + + // Kernel32 + private OpenProcess: any = null + private CloseHandle: any = null + private TerminateProcess: any = null + private QueryFullProcessImageNameW: any = null + + // User32 + private EnumWindows: any = null + private GetWindowTextW: any = null + private GetWindowTextLengthW: any = null + private GetClassNameW: any = null + private GetWindowThreadProcessId: any = null + private IsWindowVisible: any = null + private EnumChildWindows: any = null + private PostMessageW: any = null + private WNDENUMPROC_PTR: any = null + + // Advapi32 + private RegOpenKeyExW: any = null + private RegQueryValueExW: any = null + private RegCloseKey: any = null + + // Constants + private readonly PROCESS_ALL_ACCESS = 0x1F0FFF + private readonly PROCESS_TERMINATE = 0x0001 + private readonly KEY_READ = 0x20019 + private readonly HKEY_LOCAL_MACHINE = 0x80000002 + private readonly HKEY_CURRENT_USER = 0x80000001 + private readonly ERROR_SUCCESS = 0 + private readonly WM_CLOSE = 0x0010 + private readonly DB_KEY_PROCESS_CHECK_INTERVAL_MS = 1000 + + private getResourceRoots(): string[] { + const roots = [ + join(process.cwd(), 'resources'), + join(process.cwd(), 'resources', 'resources'), + join(process.resourcesPath || '', 'resources'), + process.resourcesPath || '', + join(app.getAppPath(), 'resources') + ] + return Array.from(new Set(roots.filter((root) => root && existsSync(root)))) + } + + private getDllPath(): string { + const archDir = process.arch === 'arm64' ? 'arm64' : 'x64' + const candidates: string[] = [] + + if (process.env.WX_KEY_DLL_PATH) { + candidates.push(process.env.WX_KEY_DLL_PATH) + } + + for (const root of this.getResourceRoots()) { + candidates.push(join(root, 'key', 'win32', archDir, 'wx_key.dll')) + candidates.push(join(root, 'key', 'win32', 'x64', 'wx_key.dll')) + candidates.push(join(root, 'key', 'win32', 'wx_key.dll')) + candidates.push(join(root, 'wx_key.dll')) + } + + for (const path of candidates) { + if (existsSync(path)) return path + } + + return candidates[0] + } + + private prependDllSearchPaths(dllPath: string): void { + if (process.platform !== 'win32') return + + const archDir = process.arch === 'arm64' ? 'arm64' : 'x64' + const dirs = [ + dirname(dllPath), + ...this.getResourceRoots().flatMap((root) => [ + root, + join(root, 'runtime', 'win32'), + join(root, 'key', 'win32', archDir), + join(root, 'key', 'win32', 'x64') + ]) + ].filter((dir, index, list) => dir && list.indexOf(dir) === index && existsSync(dir)) + + const existing = process.env.PATH || '' + process.env.PATH = `${dirs.join(delimiter)}${delimiter}${existing}` + process.env.Path = process.env.PATH + } + + private isNetworkPath(path: string): boolean { + if (path.startsWith('\\\\')) return true + return false + } + + private localizeNetworkDll(originalPath: string): string { + try { + const tempDir = join(os.tmpdir(), 'weflow_dll_cache') + if (!existsSync(tempDir)) { + mkdirSync(tempDir, { recursive: true }) + } + const localPath = join(tempDir, 'wx_key.dll') + if (existsSync(localPath)) return localPath + + copyFileSync(originalPath, localPath) + return localPath + } catch (e) { + console.error('DLL 本地化失败:', e) + return originalPath + } + } + + private ensureLoaded(): boolean { + if (this.initialized) return true + + let dllPath = '' + try { + this.koffi = require('koffi') + dllPath = this.getDllPath() + + if (!existsSync(dllPath)) { + console.error(`wx_key.dll 不存在于路径: ${dllPath}`) + return false + } + + if (this.isNetworkPath(dllPath)) { + dllPath = this.localizeNetworkDll(dllPath) + } + + this.prependDllSearchPaths(dllPath) + this.lib = this.koffi.load(dllPath) + this.initHook = this.lib.func('bool InitializeHook(uint32 targetPid)') + this.pollKeyData = this.lib.func('bool PollKeyData(_Out_ char *keyBuffer, int bufferSize)') + this.getStatusMessage = this.lib.func('bool GetStatusMessage(_Out_ char *msgBuffer, int bufferSize, _Out_ int *outLevel)') + this.cleanupHook = this.lib.func('bool CleanupHook()') + this.getLastErrorMsg = this.lib.func('const char* GetLastErrorMsg()') + this.getImageKeyDll = this.lib.func('bool GetImageKey(_Out_ char *resultBuffer, int bufferSize)') + + this.initialized = true + return true + } catch (e) { + const errorMsg = e instanceof Error ? e.message : String(e) + this.lastLoadError = `wx_key.dll 加载失败\n路径: ${dllPath}\n错误: ${errorMsg}` + console.error(`加载 wx_key.dll 失败\n 路径: ${dllPath}\n 错误: ${errorMsg}`) + return false + } + } + + private ensureWin32(): boolean { + return process.platform === 'win32' + } + + private getLoadError(): string { + return this.lastLoadError || 'wx_key.dll 未加载' + } + + private ensureKernel32(): boolean { + if (this.kernel32) return true + try { + this.koffi = require('koffi') + this.kernel32 = this.koffi.load('kernel32.dll') + this.OpenProcess = this.kernel32.func('OpenProcess', 'void*', ['uint32', 'bool', 'uint32']) + this.CloseHandle = this.kernel32.func('CloseHandle', 'bool', ['void*']) + this.TerminateProcess = this.kernel32.func('TerminateProcess', 'bool', ['void*', 'uint32']) + this.QueryFullProcessImageNameW = this.kernel32.func('QueryFullProcessImageNameW', 'bool', ['void*', 'uint32', this.koffi.out('uint16*'), this.koffi.out('uint32*')]) + + return true + } catch (e) { + console.error('初始化 kernel32 失败:', e) + return false + } + } + + private decodeUtf8(buf: Buffer): string { + const nullIdx = buf.indexOf(0) + return buf.toString('utf8', 0, nullIdx > -1 ? nullIdx : undefined).trim() + } + + private ensureUser32(): boolean { + if (this.user32) return true + try { + this.koffi = require('koffi') + this.user32 = this.koffi.load('user32.dll') + + const WNDENUMPROC = this.koffi.proto('bool __stdcall (void *hWnd, intptr_t lParam)') + this.WNDENUMPROC_PTR = this.koffi.pointer(WNDENUMPROC) + + this.EnumWindows = this.user32.func('EnumWindows', 'bool', [this.WNDENUMPROC_PTR, 'intptr_t']) + this.EnumChildWindows = this.user32.func('EnumChildWindows', 'bool', ['void*', this.WNDENUMPROC_PTR, 'intptr_t']) + this.PostMessageW = this.user32.func('PostMessageW', 'bool', ['void*', 'uint32', 'uintptr_t', 'intptr_t']) + this.GetWindowTextW = this.user32.func('GetWindowTextW', 'int', ['void*', this.koffi.out('uint16*'), 'int']) + this.GetWindowTextLengthW = this.user32.func('GetWindowTextLengthW', 'int', ['void*']) + this.GetClassNameW = this.user32.func('GetClassNameW', 'int', ['void*', this.koffi.out('uint16*'), 'int']) + this.GetWindowThreadProcessId = this.user32.func('GetWindowThreadProcessId', 'uint32', ['void*', this.koffi.out('uint32*')]) + this.IsWindowVisible = this.user32.func('IsWindowVisible', 'bool', ['void*']) + + return true + } catch (e) { + console.error('初始化 user32 失败:', e) + return false + } + } + + private ensureAdvapi32(): boolean { + if (this.advapi32) return true + try { + this.koffi = require('koffi') + this.advapi32 = this.koffi.load('advapi32.dll') + + const HKEY = this.koffi.alias('HKEY', 'intptr_t') + const HKEY_PTR = this.koffi.pointer(HKEY) + + this.RegOpenKeyExW = this.advapi32.func('RegOpenKeyExW', 'long', [HKEY, 'uint16*', 'uint32', 'uint32', this.koffi.out(HKEY_PTR)]) + this.RegQueryValueExW = this.advapi32.func('RegQueryValueExW', 'long', [HKEY, 'uint16*', 'uint32*', this.koffi.out('uint32*'), this.koffi.out('uint8*'), this.koffi.out('uint32*')]) + this.RegCloseKey = this.advapi32.func('RegCloseKey', 'long', [HKEY]) + + return true + } catch (e) { + console.error('初始化 advapi32 失败:', e) + return false + } + } + + private decodeCString(ptr: any): string { + try { + if (typeof ptr === 'string') return ptr + return this.koffi.decode(ptr, 'char', -1) + } catch { + return '' + } + } + + // --- WeChat Process & Path Finding --- + + private readRegistryString(rootKey: number, subKey: string, valueName: string): string | null { + if (!this.ensureAdvapi32()) return null + const subKeyBuf = Buffer.from(subKey + '\0', 'ucs2') + const valueNameBuf = valueName ? Buffer.from(valueName + '\0', 'ucs2') : null + const phkResult = Buffer.alloc(8) + + if (this.RegOpenKeyExW(rootKey, subKeyBuf, 0, this.KEY_READ, phkResult) !== this.ERROR_SUCCESS) return null + + const hKey = this.koffi.decode(phkResult, 'uintptr_t') + + try { + const lpcbData = Buffer.alloc(4) + lpcbData.writeUInt32LE(0, 0) + + let ret = this.RegQueryValueExW(hKey, valueNameBuf, null, null, null, lpcbData) + if (ret !== this.ERROR_SUCCESS) return null + + const size = lpcbData.readUInt32LE(0) + if (size === 0) return null + + const dataBuf = Buffer.alloc(size) + ret = this.RegQueryValueExW(hKey, valueNameBuf, null, null, dataBuf, lpcbData) + if (ret !== this.ERROR_SUCCESS) return null + + let str = dataBuf.toString('ucs2') + if (str.endsWith('\0')) str = str.slice(0, -1) + return str + } finally { + this.RegCloseKey(hKey) + } + } + + private async getProcessExecutablePath(pid: number): Promise { + if (!this.ensureKernel32()) return null + const hProcess = this.OpenProcess(0x1000, false, pid) + if (!hProcess) return null + + try { + const sizeBuf = Buffer.alloc(4) + sizeBuf.writeUInt32LE(1024, 0) + const pathBuf = Buffer.alloc(1024 * 2) + + const ret = this.QueryFullProcessImageNameW(hProcess, 0, pathBuf, sizeBuf) + if (ret) { + const len = sizeBuf.readUInt32LE(0) + return pathBuf.toString('ucs2', 0, len * 2) + } + return null + } catch (e) { + console.error('获取进程路径失败:', e) + return null + } finally { + this.CloseHandle(hProcess) + } + } + + private async findWeChatInstallPath(): Promise { + try { + const pid = await this.findWeChatPid() + if (pid) { + const runPath = await this.getProcessExecutablePath(pid) + if (runPath && existsSync(runPath)) return runPath + } + } catch (e) { + console.error('尝试获取运行中微信路径失败:', e) + } + + const uninstallKeys = [ + 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall', + 'SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall' + ] + const roots = [this.HKEY_LOCAL_MACHINE, this.HKEY_CURRENT_USER] + const tencentKeys = [ + 'Software\\Tencent\\WeChat', + 'Software\\WOW6432Node\\Tencent\\WeChat', + 'Software\\Tencent\\Weixin', + ] + + for (const root of roots) { + for (const key of tencentKeys) { + const path = this.readRegistryString(root, key, 'InstallPath') + if (path && existsSync(join(path, 'Weixin.exe'))) return join(path, 'Weixin.exe') + if (path && existsSync(join(path, 'WeChat.exe'))) return join(path, 'WeChat.exe') + } + } + + for (const root of roots) { + for (const parent of uninstallKeys) { + const path = this.readRegistryString(root, parent + '\\WeChat', 'InstallLocation') + if (path && existsSync(join(path, 'Weixin.exe'))) return join(path, 'Weixin.exe') + } + } + + const drives = ['C', 'D', 'E', 'F'] + const commonPaths = [ + 'Program Files\\Tencent\\WeChat\\WeChat.exe', + 'Program Files (x86)\\Tencent\\WeChat\\WeChat.exe', + 'Program Files\\Tencent\\Weixin\\Weixin.exe', + 'Program Files (x86)\\Tencent\\Weixin\\Weixin.exe' + ] + + for (const drive of drives) { + for (const p of commonPaths) { + const full = join(drive + ':\\', p) + if (existsSync(full)) return full + } + } + + return null + } + + private async findPidsByImageName(imageName: string): Promise { + try { + const { stdout } = await execFileAsync('tasklist', ['/FI', `IMAGENAME eq ${imageName}`, '/FO', 'CSV', '/NH']) + const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean) + const pids: number[] = [] + for (const line of lines) { + if (line.startsWith('INFO:')) continue + const parts = line.split('","').map((p) => p.replace(/^"|"$/g, '')) + if (parts[0]?.toLowerCase() === imageName.toLowerCase()) { + const pid = Number(parts[1]) + if (!Number.isNaN(pid)) pids.push(pid) + } + } + return pids + } catch (e) { + return [] + } + } + + private async findWeChatPids(): Promise { + const pids: number[] = [] + const pushUnique = (pid: number | null | undefined) => { + if (!pid || pids.includes(pid)) return + pids.push(pid) + } + + for (const name of ['Weixin.exe', 'WeChat.exe']) { + const found = await this.findPidsByImageName(name) + found.forEach(pushUnique) + } + return pids + } + + private async isWeChatPidActive(pid: number): Promise { + const pids = await this.findWeChatPids() + if (pids.includes(pid)) return true + + const fallbackPid = await this.waitForWeChatWindow(250) + return fallbackPid === pid + } + + private async waitForWeChatPid(timeoutMs: number): Promise { + const start = Date.now() + while (Date.now() - start < timeoutMs) { + const pids = await this.findWeChatPids() + if (pids.length > 0) return pids[0] + + const fallbackPid = await this.waitForWeChatWindow(250) + if (fallbackPid) return fallbackPid + + await new Promise(r => setTimeout(r, 500)) + } + return null + } + + private getRemainingMs(deadline: number): number { + return Math.max(0, deadline - Date.now()) + } + + private async pollDbKeyFromHook( + pid: number, + deadline: number, + logs: string[], + onStatus?: (message: string, level: number) => void + ): Promise { + const keyBuffer = Buffer.alloc(128) + let loginRequiredDetected = false + let nextProcessCheckAt = 0 + + while (Date.now() < deadline) { + const now = Date.now() + if (now >= nextProcessCheckAt) { + nextProcessCheckAt = now + this.DB_KEY_PROCESS_CHECK_INTERVAL_MS + if (!await this.isWeChatPidActive(pid)) { + return { status: 'process-ended', loginRequiredDetected } + } + } + + if (this.pollKeyData(keyBuffer, keyBuffer.length)) { + const key = this.decodeUtf8(keyBuffer) + if (key.length === 64) { + onStatus?.('密钥获取成功', 1) + return { status: 'success', key, loginRequiredDetected } + } + } + + for (let i = 0; i < 5; i++) { + const statusBuffer = Buffer.alloc(256) + const levelOut = [0] + if (!this.getStatusMessage(statusBuffer, statusBuffer.length, levelOut)) break + const msg = this.decodeUtf8(statusBuffer) + const level = levelOut[0] ?? 0 + if (msg) { + logs.push(msg) + if (this.isLoginRelatedText(msg)) { + loginRequiredDetected = true + } + onStatus?.(msg, level) + } + } + await new Promise((resolve) => setTimeout(resolve, 120)) + } + + return { status: 'timeout', loginRequiredDetected } + } + + private cleanupDbKeyHook(): void { + try { + this.cleanupHook() + } catch { } + } + + private buildInitHookError(): string { + const error = this.getLastErrorMsg ? this.decodeCString(this.getLastErrorMsg()) : '' + if (error) { + if (error.includes('0xC0000022') || error.includes('ACCESS_DENIED') || error.includes('打开目标进程失败')) { + return '权限不足:无法访问微信进程。\n\n解决方法:\n1. 右键 WeFlow 图标,选择"以管理员身份运行"\n2. 关闭可能拦截的安全软件(如360、火绒等)\n3. 确保微信没有以管理员权限运行' + } + return error + } + + const statusBuffer = Buffer.alloc(256) + const levelOut = [0] + const status = this.getStatusMessage && this.getStatusMessage(statusBuffer, statusBuffer.length, levelOut) + ? this.decodeUtf8(statusBuffer) + : '' + return status || '初始化失败' + } + + private async waitForNextDbKeyPid(deadline: number, onStatus?: (message: string, level: number) => void): Promise { + while (this.getRemainingMs(deadline) > 0) { + onStatus?.('正在查找微信进程...', 0) + const pid = await this.waitForWeChatPid(Math.min(this.getRemainingMs(deadline), 30_000)) + if (pid) return pid + } + return null + } + + private shouldRetryAfterProcessLost(deadline: number): boolean { + return this.getRemainingMs(deadline) > 1000 + } + + private async delayBeforeRetry(): Promise { + await new Promise((resolve) => setTimeout(resolve, 500)) + } + + private async waitForProcessRestart(deadline: number, onStatus?: (message: string, level: number) => void): Promise { + if (!this.shouldRetryAfterProcessLost(deadline)) return null + onStatus?.('检测到微信已退出,已清理 Hook,等待重新打开微信...', 0) + await this.delayBeforeRetry() + return this.waitForNextDbKeyPid(deadline, onStatus) + } + + private async detectLoginRequiredForLastPid(pid: number | null, loginRequiredDetected: boolean): Promise { + if (loginRequiredDetected) return true + if (!pid) return false + if (!await this.isWeChatPidActive(pid)) return false + return await this.detectWeChatLoginRequired(pid) + } + + private async findWeChatPid(): Promise { + const pids = await this.findWeChatPids() + if (pids.length > 0) return pids[0] + const fallbackPid = await this.waitForWeChatWindow(5000) + return fallbackPid ?? null + } + + private async waitForWeChatExit(timeoutMs = 8000): Promise { + const start = Date.now() + while (Date.now() - start < timeoutMs) { + const runningPids = await this.findWeChatPids() + if (runningPids.length === 0) return true + await new Promise(r => setTimeout(r, 400)) + } + return false + } + + private async closeWeChatWindows(): Promise { + if (!this.ensureUser32()) return false + let requested = false + + const enumWindowsCallback = this.koffi.register((hWnd: any, lParam: any) => { + if (!this.IsWindowVisible(hWnd)) return true + const title = this.getWindowTitle(hWnd) + const className = this.getClassName(hWnd) + const classLower = (className || '').toLowerCase() + const isWeChatWindow = this.isWeChatWindowTitle(title) || classLower.includes('wechat') || classLower.includes('weixin') + if (!isWeChatWindow) return true + + requested = true + try { + this.PostMessageW?.(hWnd, this.WM_CLOSE, 0, 0) + } catch { } + return true + }, this.WNDENUMPROC_PTR) + + this.EnumWindows(enumWindowsCallback, 0) + this.koffi.unregister(enumWindowsCallback) + + return requested + } + + private async killWeChatProcesses(): Promise { + const requested = await this.closeWeChatWindows() + if (requested) { + const gracefulOk = await this.waitForWeChatExit(1500) + if (gracefulOk) return true + } + + try { + await execFileAsync('taskkill', ['/F', '/T', '/IM', 'Weixin.exe']) + await execFileAsync('taskkill', ['/F', '/T', '/IM', 'WeChat.exe']) + } catch (e) { } + + return await this.waitForWeChatExit(5000) + } + + // --- Window Detection --- + + private getWindowTitle(hWnd: any): string { + const len = this.GetWindowTextLengthW(hWnd) + if (len === 0) return '' + const buf = Buffer.alloc((len + 1) * 2) + this.GetWindowTextW(hWnd, buf, len + 1) + return buf.toString('ucs2', 0, len * 2) + } + + private getClassName(hWnd: any): string { + const buf = Buffer.alloc(512) + const len = this.GetClassNameW(hWnd, buf, 256) + return buf.toString('ucs2', 0, len * 2) + } + + private isWeChatWindowTitle(title: string): boolean { + const normalized = title.trim() + if (!normalized) return false + const lower = normalized.toLowerCase() + return normalized === '微信' || lower === 'wechat' || lower === 'weixin' + } + + private async waitForWeChatWindow(timeoutMs = 25000): Promise { + if (!this.ensureUser32()) return null + const startTime = Date.now() + while (Date.now() - startTime < timeoutMs) { + let foundPid: number | null = null + + const enumWindowsCallback = this.koffi.register((hWnd: any, lParam: any) => { + if (!this.IsWindowVisible(hWnd)) return true + const title = this.getWindowTitle(hWnd) + if (!this.isWeChatWindowTitle(title)) return true + + const pidBuf = Buffer.alloc(4) + this.GetWindowThreadProcessId(hWnd, pidBuf) + const pid = pidBuf.readUInt32LE(0) + if (pid) { + foundPid = pid + return false + } + return true + }, this.WNDENUMPROC_PTR) + + this.EnumWindows(enumWindowsCallback, 0) + this.koffi.unregister(enumWindowsCallback) + + if (foundPid) return foundPid + await new Promise(r => setTimeout(r, 500)) + } + return null + } + + private collectChildWindowInfos(parent: any): Array<{ title: string; className: string }> { + const children: Array<{ title: string; className: string }> = [] + const enumChildCallback = this.koffi.register((hChild: any, lp: any) => { + const title = this.getWindowTitle(hChild).trim() + const className = this.getClassName(hChild).trim() + children.push({ title, className }) + return true + }, this.WNDENUMPROC_PTR) + this.EnumChildWindows(parent, enumChildCallback, 0) + this.koffi.unregister(enumChildCallback) + return children + } + + private hasReadyComponents(children: Array<{ title: string; className: string }>): boolean { + if (children.length === 0) return false + + const readyTexts = ['聊天', '登录', '账号'] + const readyClassMarkers = ['WeChat', 'Weixin', 'TXGuiFoundation', 'Qt5', 'ChatList', 'MainWnd', 'BrowserWnd', 'ListView'] + const readyChildCountThreshold = 14 + + let classMatchCount = 0 + let titleMatchCount = 0 + let hasValidClassName = false + + for (const child of children) { + const normalizedTitle = child.title.replace(/\s+/g, '') + if (normalizedTitle) { + if (readyTexts.some(marker => normalizedTitle.includes(marker))) return true + titleMatchCount += 1 + } + const className = child.className + if (className) { + if (readyClassMarkers.some(marker => className.includes(marker))) return true + if (className.length > 5) { + classMatchCount += 1 + hasValidClassName = true + } + } + } + + if (classMatchCount >= 3 || titleMatchCount >= 2) return true + if (children.length >= readyChildCountThreshold) return true + if (hasValidClassName && children.length >= 5) return true + return false + } + + private isLoginRelatedText(value: string): boolean { + const normalized = String(value || '').replace(/\s+/g, '').toLowerCase() + if (!normalized) return false + const keywords = [ + '登录', + '扫码', + '二维码', + '请在手机上确认', + '手机确认', + '切换账号', + 'wechatlogin', + 'qrcode', + 'scan' + ] + return keywords.some((keyword) => normalized.includes(keyword)) + } + + private async detectWeChatLoginRequired(pid: number): Promise { + if (!this.ensureUser32()) return false + let loginRequired = false + + const enumWindowsCallback = this.koffi.register((hWnd: any, _lParam: any) => { + if (!this.IsWindowVisible(hWnd)) return true + const title = this.getWindowTitle(hWnd) + if (!this.isWeChatWindowTitle(title)) return true + + const pidBuf = Buffer.alloc(4) + this.GetWindowThreadProcessId(hWnd, pidBuf) + const windowPid = pidBuf.readUInt32LE(0) + if (windowPid !== pid) return true + + if (this.isLoginRelatedText(title)) { + loginRequired = true + return false + } + + const children = this.collectChildWindowInfos(hWnd) + for (const child of children) { + if (this.isLoginRelatedText(child.title) || this.isLoginRelatedText(child.className)) { + loginRequired = true + return false + } + } + return true + }, this.WNDENUMPROC_PTR) + + this.EnumWindows(enumWindowsCallback, 0) + this.koffi.unregister(enumWindowsCallback) + + return loginRequired + } + + private async waitForWeChatWindowComponents(pid: number, timeoutMs = 15000): Promise { + if (!this.ensureUser32()) return true + const startTime = Date.now() + while (Date.now() - startTime < timeoutMs) { + let ready = false + const enumWindowsCallback = this.koffi.register((hWnd: any, lParam: any) => { + if (!this.IsWindowVisible(hWnd)) return true + const title = this.getWindowTitle(hWnd) + if (!this.isWeChatWindowTitle(title)) return true + + const pidBuf = Buffer.alloc(4) + this.GetWindowThreadProcessId(hWnd, pidBuf) + const windowPid = pidBuf.readUInt32LE(0) + if (windowPid !== pid) return true + + const children = this.collectChildWindowInfos(hWnd) + if (this.hasReadyComponents(children)) { + ready = true + return false + } + return true + }, this.WNDENUMPROC_PTR) + + this.EnumWindows(enumWindowsCallback, 0) + this.koffi.unregister(enumWindowsCallback) + + if (ready) return true + await new Promise(r => setTimeout(r, 500)) + } + return true + } + + // --- DB Key Logic (core hook/poll flow unchanged) --- + + async autoGetDbKey( + timeoutMs = 60_000, + onStatus?: (message: string, level: number) => void + ): Promise { + if (!this.ensureWin32()) return { success: false, error: '仅支持 Windows' } + if (!this.ensureLoaded()) return { success: false, error: this.getLoadError() } + if (!this.ensureKernel32()) return { success: false, error: 'Kernel32 Init Failed' } + + const logs: string[] = [] + const deadline = Date.now() + timeoutMs + onStatus?.('正在查找微信进程...', 0) + let pid = await this.findWeChatPid() + if (!pid) { + const err = '未找到微信进程,请先启动微信' + onStatus?.(err, 2) + return { success: false, error: err } + } + let lastAttemptLoginRequiredDetected = false + + while (pid && this.getRemainingMs(deadline) > 0) { + onStatus?.(`检测到微信窗口 (PID: ${pid}),正在获取...`, 0) + onStatus?.('正在检测微信界面组件...', 0) + await this.waitForWeChatWindowComponents(pid, Math.min(15000, this.getRemainingMs(deadline))) + + if (!await this.isWeChatPidActive(pid)) { + pid = await this.waitForProcessRestart(deadline, onStatus) + continue + } + + const ok = this.initHook(pid) + if (!ok) { + if (!await this.isWeChatPidActive(pid)) { + this.cleanupDbKeyHook() + pid = await this.waitForProcessRestart(deadline, onStatus) + continue + } + return { success: false, error: this.buildInitHookError(), logs } + } + + let pollResult: DbKeyPollResult + try { + pollResult = await this.pollDbKeyFromHook(pid, deadline, logs, onStatus) + } finally { + this.cleanupDbKeyHook() + } + + lastAttemptLoginRequiredDetected = pollResult.loginRequiredDetected + if (pollResult.status === 'success') { + return { success: true, key: pollResult.key, logs } + } + if (pollResult.status === 'process-ended') { + lastAttemptLoginRequiredDetected = false + pid = await this.waitForProcessRestart(deadline, onStatus) + continue + } + break + } + + const loginRequired = await this.detectLoginRequiredForLastPid(pid, lastAttemptLoginRequiredDetected) + if (loginRequired) { + return { + success: false, + error: '微信已启动但尚未完成登录,请先在微信客户端完成登录后再重试自动获取密钥。', + logs + } + } + + return { success: false, error: '获取密钥超时', logs } + } + + private cleanWxid(wxid: string): string { + const first = wxid.indexOf('_') + if (first === -1) return wxid + const second = wxid.indexOf('_', first + 1) + if (second === -1) return wxid + return wxid.substring(0, second) + } + + private deriveImageKeys(code: number, wxid: string): { xorKey: number; aesKey: string } { + const cleanedWxid = this.cleanWxid(wxid) + const xorKey = code & 0xFF + const dataToHash = code.toString() + cleanedWxid + const md5Full = crypto.createHash('md5').update(dataToHash).digest('hex') + const aesKey = md5Full.substring(0, 16) + return { xorKey, aesKey } + } + + private verifyDerivedAesKey(aesKey: string, ciphertext: Buffer): boolean { + try { + if (!aesKey || aesKey.length < 16 || ciphertext.length !== 16) return false + const decipher = crypto.createDecipheriv('aes-128-ecb', Buffer.from(aesKey, 'ascii').subarray(0, 16), null) + decipher.setAutoPadding(false) + const dec = Buffer.concat([decipher.update(ciphertext), decipher.final()]) + if (dec[0] === 0xFF && dec[1] === 0xD8 && dec[2] === 0xFF) return true + if (dec[0] === 0x89 && dec[1] === 0x50 && dec[2] === 0x4E && dec[3] === 0x47) return true + if (dec[0] === 0x52 && dec[1] === 0x49 && dec[2] === 0x46 && dec[3] === 0x46) return true + if (dec[0] === 0x77 && dec[1] === 0x78 && dec[2] === 0x67 && dec[3] === 0x66) return true + if (dec[0] === 0x47 && dec[1] === 0x49 && dec[2] === 0x46) return true + return false + } catch { + return false + } + } + + private async collectWxidCandidates(manualDir?: string, wxidParam?: string): Promise { + const candidates: string[] = [] + const pushUnique = (value: string) => { + const v = String(value || '').trim() + if (!v || candidates.includes(v)) return + candidates.push(v) + } + + if (wxidParam && wxidParam.startsWith('wxid_')) pushUnique(wxidParam) + + if (manualDir) { + const normalized = manualDir.replace(/[\\/]+$/, '') + const dirName = normalized.split(/[\\/]/).pop() ?? '' + if (dirName.startsWith('wxid_')) pushUnique(dirName) + + const marker = normalized.match(/[\\/]xwechat_files/i) || normalized.match(/[\\/]WeChat Files/i) + if (marker) { + const root = normalized.slice(0, marker.index! + marker[0].length) + try { + const { readdirSync, statSync } = await import('fs') + const { join } = await import('path') + for (const entry of readdirSync(root)) { + if (!entry.startsWith('wxid_')) continue + const full = join(root, entry) + try { + if (statSync(full).isDirectory()) pushUnique(entry) + } catch { } + } + } catch { } + } + } + + pushUnique('unknown') + return candidates + } + + async autoGetImageKey( + manualDir?: string, + onProgress?: (message: string) => void, + wxidParam?: string + ): Promise { + if (!this.ensureWin32()) return { success: false, error: '仅支持 Windows' } + if (!this.ensureLoaded()) return { success: false, error: this.getLoadError() } + + onProgress?.('正在从缓存目录扫描图片密钥...') + + const resultBuffer = Buffer.alloc(8192) + const ok = this.getImageKeyDll(resultBuffer, resultBuffer.length) + + if (!ok) { + const errMsg = this.getLastErrorMsg ? this.decodeCString(this.getLastErrorMsg()) : '获取图片密钥失败' + return { success: false, error: errMsg } + } + + const jsonStr = this.decodeUtf8(resultBuffer) + let parsed: any + try { + parsed = JSON.parse(jsonStr) + } catch { + return { success: false, error: '解析密钥数据失败' } + } + + // 从任意账号提取 code 列表(code 来自 kvcomm,与 wxid 无关,所有账号都一样) + const accounts: any[] = parsed.accounts ?? [] + if (!accounts.length || !accounts[0]?.keys?.length) { + return { success: false, error: '未找到有效的密钥码(kvcomm 缓存为空)' } + } + + const codes: number[] = accounts[0].keys.map((k: any) => k.code) + console.log('[ImageKey] codes:', codes, 'DLL wxids:', accounts.map((a: any) => a.wxid)) + + const wxidCandidates = await this.collectWxidCandidates(manualDir, wxidParam) + let verifyCiphertext: Buffer | null = null + if (manualDir && existsSync(manualDir)) { + const template = await this._findTemplateData(manualDir, 32) + verifyCiphertext = template.ciphertext + } + + if (verifyCiphertext) { + onProgress?.(`正在校验候选 wxid(${wxidCandidates.length} 个)...`) + for (const candidateWxid of wxidCandidates) { + for (const code of codes) { + const { xorKey, aesKey } = this.deriveImageKeys(code, candidateWxid) + if (!this.verifyDerivedAesKey(aesKey, verifyCiphertext)) continue + onProgress?.(`密钥获取成功 (wxid: ${candidateWxid}, code: ${code})`) + console.log('[ImageKey] 校验命中: wxid=', candidateWxid, 'code=', code) + return { success: true, xorKey, aesKey, verified: true } + } + } + return { success: false, error: '缓存 code 与当前账号 wxid 未匹配,请确认账号目录后重试,或使用内存扫描' } + } + + // 无模板密文可验真时回退旧策略 + const fallbackWxid = wxidCandidates[0] || accounts[0].wxid || 'unknown' + const fallbackCode = codes[0] + const { xorKey, aesKey } = this.deriveImageKeys(fallbackCode, fallbackWxid) + onProgress?.(`密钥获取成功 (wxid: ${fallbackWxid}, code: ${fallbackCode})`) + console.log('[ImageKey] 回退计算: wxid=', fallbackWxid, 'code=', fallbackCode) + return { success: true, xorKey, aesKey, verified: false } + } + + // --- 内存扫描备选方案(融合 Dart+Python 优点)--- + // 只扫 RW 可写区域(更快),同时支持 ASCII 和 UTF-16LE 两种密钥格式 + // 验证支持 JPEG/PNG/WEBP/WXGF/GIF 多种格式 + + async autoGetImageKeyByMemoryScan( + userDir: string, + onProgress?: (message: string) => void + ): Promise { + if (!this.ensureWin32()) return { success: false, error: '仅支持 Windows' } + + try { + // 1. 查找模板文件获取密文和 XOR 密钥 + onProgress?.('正在查找模板文件...') + let result = await this._findTemplateData(userDir, 32) + let { ciphertext, xorKey } = result + + // 如果找不到密钥,尝试扫描更多文件 + if (ciphertext && xorKey === null) { + onProgress?.('未找到有效密钥,尝试扫描更多文件...') + result = await this._findTemplateData(userDir, 100) + xorKey = result.xorKey + } + + if (!ciphertext) return { success: false, error: '未找到 V2 模板文件,请先在微信中查看几张图片' } + if (xorKey === null) return { success: false, error: '未能从模板文件中计算出有效的 XOR 密钥,请确保在微信中查看了多张不同的图片' } + + onProgress?.(`XOR 密钥: 0x${xorKey.toString(16).padStart(2, '0')},正在查找微信进程...`) + + // 2. 找微信 PID + const pid = await this.findWeChatPid() + if (!pid) return { success: false, error: '微信进程未运行,请先启动微信' } + + onProgress?.(`已找到微信进程 PID=${pid},正在扫描内存...`) + + // 3. 持续轮询内存扫描,最多 60 秒 + const deadline = Date.now() + 60_000 + let scanCount = 0 + while (Date.now() < deadline) { + scanCount++ + onProgress?.(`第 ${scanCount} 次扫描内存,请在微信中打开图片大图...`) + const aesKey = await this._scanMemoryForAesKey(pid, ciphertext, onProgress) + if (aesKey) { + onProgress?.('密钥获取成功') + return { success: true, xorKey, aesKey } + } + // 等 5 秒再试 + await new Promise(r => setTimeout(r, 5000)) + } + + return { + success: false, + error: '60 秒内未找到 AES 密钥。\n请确保已在微信中打开 2-3 张图片大图后再试。' + } + } catch (e) { + return { success: false, error: `内存扫描失败: ${e}` } + } + } + + private async _findTemplateData(userDir: string, limit: number = 32): Promise<{ ciphertext: Buffer | null; xorKey: number | null }> { + const { readdirSync, readFileSync, statSync } = await import('fs') + const { join } = await import('path') + const V2_MAGIC = Buffer.from([0x07, 0x08, 0x56, 0x32, 0x08, 0x07]) + + // 递归收集 *_t.dat 文件 + const collect = (dir: string, results: string[], maxFiles: number) => { + if (results.length >= maxFiles) return + try { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + if (results.length >= maxFiles) break + const full = join(dir, entry.name) + if (entry.isDirectory()) collect(full, results, maxFiles) + else if (entry.isFile() && entry.name.endsWith('_t.dat')) results.push(full) + } + } catch { /* 忽略无权限目录 */ } + } + + const files: string[] = [] + collect(userDir, files, limit) + + // 按修改时间降序 + files.sort((a, b) => { + try { return statSync(b).mtimeMs - statSync(a).mtimeMs } catch { return 0 } + }) + + let ciphertext: Buffer | null = null + const tailCounts: Record = {} + + for (const f of files.slice(0, 32)) { + try { + const data = readFileSync(f) + if (data.length < 8) continue + + // 统计末尾两字节用于 XOR 密钥 + if (data.subarray(0, 6).equals(V2_MAGIC) && data.length >= 2) { + const key = `${data[data.length - 2]}_${data[data.length - 1]}` + tailCounts[key] = (tailCounts[key] ?? 0) + 1 + } + + // 提取密文(取第一个有效的) + if (!ciphertext && data.subarray(0, 6).equals(V2_MAGIC) && data.length >= 0x1F) { + ciphertext = data.subarray(0xF, 0x1F) + } + } catch { /* 忽略 */ } + } + + // 计算 XOR 密钥 + let xorKey: number | null = null + let maxCount = 0 + for (const [key, count] of Object.entries(tailCounts)) { + if (count > maxCount) { maxCount = count; const [x, y] = key.split('_').map(Number); const k = x ^ 0xFF; if (k === (y ^ 0xD9)) xorKey = k } + } + + return { ciphertext, xorKey } + } + + private async _scanMemoryForAesKey( + pid: number, + ciphertext: Buffer, + onProgress?: (msg: string) => void + ): Promise { + if (!this.ensureKernel32()) return null + + // 直接用已加载的 kernel32 实例,用 uintptr 传地址 + const VirtualQueryEx = this.kernel32.func('VirtualQueryEx', 'size_t', ['void*', 'uintptr', 'void*', 'size_t']) + const ReadProcessMemory = this.kernel32.func('ReadProcessMemory', 'bool', ['void*', 'uintptr', 'void*', 'size_t', this.koffi.out('size_t*')]) + + // RW 保护标志(只扫可写区域,速度更快) + const RW_FLAGS = 0x04 | 0x08 | 0x40 | 0x80 // PAGE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY + const MEM_COMMIT = 0x1000 + const PAGE_NOACCESS = 0x01 + const PAGE_GUARD = 0x100 + const MBI_SIZE = 48 // MEMORY_BASIC_INFORMATION size on x64 + + const hProcess = this.OpenProcess(0x1F0FFF, false, pid) + if (!hProcess) return null + + try { + // 枚举 RW 内存区域 + const regions: Array<[number, number]> = [] + let addr = 0 + const mbi = Buffer.alloc(MBI_SIZE) + + while (addr < 0x7FFFFFFFFFFF) { + const ret = VirtualQueryEx(hProcess, addr, mbi, MBI_SIZE) + if (ret === 0) break + // MEMORY_BASIC_INFORMATION x64 布局: + // 0: BaseAddress (8) + // 8: AllocationBase (8) + // 16: AllocationProtect (4) + 4 padding + // 24: RegionSize (8) + // 32: State (4) + // 36: Protect (4) + // 40: Type (4) + 4 padding = 48 total + const base = Number(mbi.readBigUInt64LE(0)) + const size = Number(mbi.readBigUInt64LE(24)) + const state = mbi.readUInt32LE(32) + const protect = mbi.readUInt32LE(36) + + if (state === MEM_COMMIT && + protect !== PAGE_NOACCESS && + (protect & PAGE_GUARD) === 0 && + (protect & RW_FLAGS) !== 0 && + size <= 50 * 1024 * 1024) { + regions.push([base, size]) + } + const next = base + size + if (next <= addr) break + addr = next + } + + const totalMB = regions.reduce((s, [, sz]) => s + sz, 0) / 1024 / 1024 + onProgress?.(`扫描 ${regions.length} 个 RW 区域 (${totalMB.toFixed(0)} MB)...`) + + const CHUNK = 4 * 1024 * 1024 + const OVERLAP = 65 + + for (let i = 0; i < regions.length; i++) { + const [base, size] = regions[i] + if (i % 20 === 0) { + onProgress?.(`扫描进度 ${i}/${regions.length}...`) + await new Promise(r => setTimeout(r, 1)) // 让出事件循环 + } + + let offset = 0 + let trailing: Buffer | null = null + + while (offset < size) { + const chunkSize = Math.min(CHUNK, size - offset) + const buf = Buffer.alloc(chunkSize) + const bytesReadOut = [0] + const ok = ReadProcessMemory(hProcess, base + offset, buf, chunkSize, bytesReadOut) + if (!ok || bytesReadOut[0] === 0) { offset += chunkSize; trailing = null; continue } + + const data: Buffer = trailing ? Buffer.concat([trailing, buf.subarray(0, bytesReadOut[0])]) : buf.subarray(0, bytesReadOut[0]) + + // 搜索 ASCII 32字节密钥 + const key = this._searchAsciiKey(data, ciphertext) + if (key) { this.CloseHandle(hProcess); return key } + + // 搜索 UTF-16LE 32字节密钥 + const key16 = this._searchUtf16Key(data, ciphertext) + if (key16) { this.CloseHandle(hProcess); return key16 } + + trailing = data.subarray(Math.max(0, data.length - OVERLAP)) + offset += chunkSize + } + } + + return null + } finally { + this.CloseHandle(hProcess) + } + } + + private _searchAsciiKey(data: Buffer, ciphertext: Buffer): string | null { + for (let i = 0; i < data.length - 34; i++) { + if (this._isAlphaNum(data[i])) continue + let valid = true + for (let j = 1; j <= 32; j++) { + if (!this._isAlphaNum(data[i + j])) { valid = false; break } + } + if (!valid) continue + if (i + 33 < data.length && this._isAlphaNum(data[i + 33])) continue + const keyBytes = data.subarray(i + 1, i + 33) + if (this._verifyAesKey(keyBytes, ciphertext)) return keyBytes.toString('ascii').substring(0, 16) + } + return null + } + + private _searchUtf16Key(data: Buffer, ciphertext: Buffer): string | null { + for (let i = 0; i < data.length - 65; i++) { + let valid = true + for (let j = 0; j < 32; j++) { + if (data[i + j * 2 + 1] !== 0x00 || !this._isAlphaNum(data[i + j * 2])) { valid = false; break } + } + if (!valid) continue + const keyBytes = Buffer.alloc(32) + for (let j = 0; j < 32; j++) keyBytes[j] = data[i + j * 2] + if (this._verifyAesKey(keyBytes, ciphertext)) return keyBytes.toString('ascii').substring(0, 16) + } + return null + } + + private _isAlphaNum(b: number): boolean { + return (b >= 0x61 && b <= 0x7A) || (b >= 0x41 && b <= 0x5A) || (b >= 0x30 && b <= 0x39) + } + + private _verifyAesKey(keyBytes: Buffer, ciphertext: Buffer): boolean { + try { + const decipher = crypto.createDecipheriv('aes-128-ecb', keyBytes.subarray(0, 16), null) + decipher.setAutoPadding(false) + const dec = Buffer.concat([decipher.update(ciphertext), decipher.final()]) + // 支持 JPEG / PNG / WEBP / WXGF / GIF + if (dec[0] === 0xFF && dec[1] === 0xD8 && dec[2] === 0xFF) return true + if (dec[0] === 0x89 && dec[1] === 0x50 && dec[2] === 0x4E && dec[3] === 0x47) return true + if (dec[0] === 0x52 && dec[1] === 0x49 && dec[2] === 0x46 && dec[3] === 0x46) return true + if (dec[0] === 0x77 && dec[1] === 0x78 && dec[2] === 0x67 && dec[3] === 0x66) return true + if (dec[0] === 0x47 && dec[1] === 0x49 && dec[2] === 0x46) return true + return false + } catch { return false } + } +} diff --git a/src/main/preload-env.ts b/src/main/preload-env.ts new file mode 100644 index 0000000..fef23eb --- /dev/null +++ b/src/main/preload-env.ts @@ -0,0 +1,37 @@ +import fs from 'fs' +import path from 'path' + +function prependPath(values: string[]): void { + if (process.platform !== 'win32') return + + const existing = process.env.PATH || '' + const next = Array.from(new Set(values.filter(Boolean))).join(path.delimiter) + process.env.PATH = next ? `${next}${path.delimiter}${existing}` : existing + process.env.Path = process.env.PATH +} + +try { + const archDir = process.arch === 'arm64' ? 'arm64' : 'x64' + const resourceRoots = [ + path.join(process.cwd(), 'resources'), + path.join(process.cwd(), 'resources', 'resources'), + path.join(process.resourcesPath || '', 'resources'), + process.resourcesPath || '' + ].filter((value, index, list) => value && list.indexOf(value) === index && fs.existsSync(value)) + + const resourcesRoot = resourceRoots[0] || path.join(process.cwd(), 'resources') + const dllDirs = resourceRoots.flatMap((root) => [ + root, + path.join(root, 'wcdb', 'win32', archDir), + path.join(root, 'wcdb', 'win32', 'x64'), + path.join(root, 'key', 'win32', archDir), + path.join(root, 'key', 'win32', 'x64'), + path.join(root, 'runtime', 'win32') + ]) + + process.env.WCDB_RESOURCES_PATH = process.env.WCDB_RESOURCES_PATH || resourcesRoot + process.env.WEFLOW_PROJECT_NAME = process.env.WEFLOW_PROJECT_NAME || 'WeFlow' + prependPath(dllDirs.filter((dir) => fs.existsSync(dir))) +} catch (error) { + console.error('[WechatExplorer] failed to enforce local DLL priority:', error) +} diff --git a/src/main/services/chat-service.ts b/src/main/services/chat-service.ts index ff004ba..1d9c38c 100644 --- a/src/main/services/chat-service.ts +++ b/src/main/services/chat-service.ts @@ -14,6 +14,15 @@ export function getCurrentKey(): string { } } +export function getCurrentAccountRoot(): string { + if (!dbRef) return '' + try { + return dbRef.getWcdb4Client().getAccountRoot() + } catch { + return '' + } +} + export interface FormattedContact { m_nsUsrName: string m_nsNickName: string @@ -31,6 +40,7 @@ export interface FormattedMessage { isSender: boolean img?: string name?: string + senderId?: string contentData?: ReturnType voiceDataUrl?: string voiceDuration?: number @@ -133,6 +143,15 @@ export function listContacts(filter?: string): FormattedContact[] { return contacts } +export function getContactAvatars(usernames: string[]): Record { + if (!dbRef) return {} + const normalized = Array.from( + new Set((usernames || []).map((username) => String(username || '').trim()).filter(Boolean)) + ) + if (normalized.length === 0) return {} + return dbRef.getWcdb4Client().getAvatarUrls(normalized) +} + export function listMessages( userMd5: string, startTime?: number, @@ -140,16 +159,18 @@ export function listMessages( ): FormattedMessage[] { if (!dbRef) return [] + const startedAt = Date.now() const wcdb4Client = dbRef.getWcdb4Client() const username = wcdb4Client.getUsernameByMd5(userMd5) + console.log( + `[ChatService] listMessages begin md5=${userMd5} username=${username || ''} start=${startTime || 0} end=${endTime || 0}` + ) const rawMessages = dbRef.getUserMessages(userMd5, startTime, endTime) - const groupMembers = dbRef.getGroupMembersForChat(userMd5) - const myAvatar = dbRef.getMyAvatarUrl() - const myGroupNickname = username?.endsWith('@chatroom') - ? wcdb4Client.getMyGroupNickname(username) - : undefined + console.log( + `[ChatService] listMessages native done md5=${userMd5} raw=${rawMessages.length} cost=${Date.now() - startedAt}ms` + ) - return rawMessages.map((msg: WechatMessage) => { + const formatted = rawMessages.map((msg: WechatMessage) => { const rawMsgType = parseInt(msg.messageType) const msgType = normalizeMsgType(msg.messageType) const createTime = parseInt(msg.msgCreateTime) @@ -160,9 +181,9 @@ export function listMessages( let content = msg.msgContent let img = '' let name = '' + let senderId = typeof msg.sender === 'string' ? msg.sender : '' if (isMine) { - if (myAvatar) img = myAvatar - name = myGroupNickname || (typeof msg.senderNickname === 'string' ? msg.senderNickname : '') + name = typeof msg.senderNickname === 'string' ? msg.senderNickname : '' } else { if (typeof msg.senderAvatar === 'string') img = msg.senderAvatar if (typeof msg.senderNickname === 'string') name = msg.senderNickname @@ -172,15 +193,13 @@ export function listMessages( if (colonIndex > 0) { const potentialWxid = content.substring(0, colonIndex) if (potentialWxid.startsWith('wxid_')) { - const member = dbRef!.getGroupMember(potentialWxid) - if (member) img = member.m_nsHeadImgUrl - if (groupMembers[potentialWxid]) { - name = groupMembers[potentialWxid] - content = content.substring(colonIndex + 1) - } + senderId = senderId || potentialWxid + name = name || potentialWxid + content = content.substring(colonIndex + 1) } } } + if (!isMine && !name && senderId) name = senderId let contentData: ReturnType | undefined let displayType = MSG_TYPE_DICT[msgType] || msg.messageType @@ -245,12 +264,18 @@ export function listMessages( content, img, name, + senderId, sessionId: username, localId, createTime, contentData } }) + + console.log( + `[ChatService] listMessages end md5=${userMd5} formatted=${formatted.length} cost=${Date.now() - startedAt}ms` + ) + return formatted } export function getGroupSnapshot(userMd5: string): GroupSnapshot | null { diff --git a/src/main/services/settings-store.ts b/src/main/services/settings-store.ts index 349580e..c82de47 100644 --- a/src/main/services/settings-store.ts +++ b/src/main/services/settings-store.ts @@ -10,11 +10,100 @@ export interface AppSettings { apiPort: number } +function getDefaultDbRoot(): string { + const home = os.homedir() + const candidates = getDefaultDbRootCandidates(home) + return candidates.find((candidate) => isUsableDbRoot(candidate)) || candidates[0] +} + +function getDefaultDbRootCandidates(home: string): string[] { + if (process.platform !== 'win32') { + return [path.join(home, 'Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files')] + } + + const candidates = [ + ...getWeflowDbPathCandidates(home), + path.join(home, 'Documents', 'WeChat Files'), + path.join(home, 'Documents', 'xwechat_files'), + path.join(home, 'WeChat Files'), + path.join(os.homedir(), 'AppData', 'Roaming', 'Tencent', 'xwechat_files') + ] + + for (const drive of getWindowsDrives()) { + candidates.push(path.join(`${drive}:\\`, 'xwechat_files')) + candidates.push(path.join(`${drive}:\\`, 'WeChat Files')) + for (const child of listDirectories(`${drive}:\\`)) { + candidates.push(path.join(child, 'xwechat_files')) + candidates.push(path.join(child, 'WeChat Files')) + } + } + + return unique(candidates) +} + +function getWeflowDbPathCandidates(home: string): string[] { + const configPaths = [ + path.join(home, 'AppData', 'Roaming', 'weflow', 'WeFlow-config.json'), + path.join(home, 'AppData', 'Roaming', 'WeFlow', 'WeFlow-config.json') + ] + const candidates: string[] = [] + for (const configPath of configPaths) { + try { + const config = fs.readJsonSync(configPath) as { dbPath?: unknown } + if (typeof config.dbPath === 'string' && config.dbPath.trim()) { + candidates.push(config.dbPath.trim()) + } + } catch { + // WeFlow is optional; ignore missing or unreadable config. + } + } + return candidates +} + +function getWindowsDrives(): string[] { + const drives: string[] = [] + for (let code = 67; code <= 90; code += 1) { + const drive = String.fromCharCode(code) + if (fs.existsSync(`${drive}:\\`)) drives.push(drive) + } + return drives +} + +function listDirectories(root: string): string[] { + try { + return fs + .readdirSync(root) + .map((name) => path.join(root, name)) + .filter((candidate) => { + try { + return fs.statSync(candidate).isDirectory() + } catch { + return false + } + }) + } catch { + return [] + } +} + +function unique(values: string[]): string[] { + return Array.from(new Set(values)) +} + +function isUsableDbRoot(candidate?: string): boolean { + if (!candidate || !fs.existsSync(candidate)) return false + if (fs.existsSync(path.join(candidate, 'db_storage'))) return true + try { + return fs + .readdirSync(candidate) + .some((name) => fs.existsSync(path.join(candidate, name, 'db_storage'))) + } catch { + return false + } +} + const DEFAULT_SETTINGS: AppSettings = { - dbRoot: path.join( - os.homedir(), - 'Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files' - ), + dbRoot: getDefaultDbRoot(), apiEnabled: true, apiHost: '127.0.0.1', apiPort: 6131 @@ -37,6 +126,9 @@ export function loadSettings(): AppSettings { if (fs.existsSync(SETTINGS_FILE)) { const raw = fs.readJsonSync(SETTINGS_FILE) as Partial cache = { ...DEFAULT_SETTINGS, ...raw } + if (process.platform === 'win32' && !isUsableDbRoot(cache.dbRoot)) { + cache.dbRoot = getDefaultDbRoot() + } return cache } } catch (error) { @@ -73,4 +165,4 @@ export function resetSettings(): AppSettings { export function getSettingsPath(): string { return SETTINGS_FILE -} \ No newline at end of file +} diff --git a/src/main/wcdb4-client.ts b/src/main/wcdb4-client.ts index b47c3be..90c08fd 100644 --- a/src/main/wcdb4-client.ts +++ b/src/main/wcdb4-client.ts @@ -41,6 +41,83 @@ type KoffiModule = { decode: (ptr: unknown, type: string, length: number) => string } +// Module-level singleton for WCDB native handle. WCDB's Windows runtime +// returns -1006 if wcdb_init is called more than once per process, so we +// perform InitProtection + wcdb_init exactly once and cache the resulting +// library reference for every Wcdb4Client instance. +let wcdbBootstrapLib: KoffiLibrary | null = null + +export function bootstrapWcdbNative(libPath?: string, libDirOverride?: string): KoffiLibrary { + if (wcdbBootstrapLib) return wcdbBootstrapLib + + const koffi = nodeRequire('koffi') as KoffiModule + const resolvedLibPath = libPath || Wcdb4Client.resolveNativeLibrary() + const libDir = libDirOverride || path.dirname(resolvedLibPath) + console.log( + `[WCDB4] bootstrap koffi.load begin ${resolvedLibPath} cwd=${process.cwd()} resourcesPath=${process.resourcesPath || ''}` + ) + for (const name of process.platform === 'win32' + ? ['WCDB.dll', 'SDL2.dll'] + : process.platform === 'darwin' + ? ['libWCDB.dylib'] + : []) { + const preloadPath = path.join(libDir, name) + if (!fs.existsSync(preloadPath)) continue + try { + koffi.load(preloadPath) + console.log(`[WCDB4] bootstrap preload ok ${preloadPath}`) + } catch { + console.warn(`[WCDB4] bootstrap preload failed ${preloadPath}`) + } + } + const lib = koffi.load(resolvedLibPath) + console.log(`[WCDB4] bootstrap koffi.load ok ${resolvedLibPath}`) + + const initProtection = lib.func('int32 InitProtection(const char* resourcePath)') as ( + resourcePath: string + ) => number + const resourceRoots = Array.from( + new Set([ + libDir, + path.dirname(libDir), + process.env.WCDB_RESOURCES_PATH || '', + path.join(process.resourcesPath || process.cwd(), 'resources'), + process.resourcesPath || process.cwd(), + path.join(process.cwd(), 'resources') + ]) + ) + let lastCode = -1 + let initOk = false + for (const resourceRoot of resourceRoots) { + try { + console.log(`[WCDB4] bootstrap InitProtection call ${resourceRoot}`) + lastCode = Number(initProtection(resourceRoot)) + console.log(`[WCDB4] bootstrap InitProtection rc=${lastCode} path=${resourceRoot}`) + if (lastCode === 0) { + initOk = true + console.log(`[WCDB4] bootstrap InitProtection ok path=${resourceRoot}`) + break + } + } catch (error) { + console.warn(`[WCDB4] bootstrap InitProtection exception path=${resourceRoot}`, error) + } + } + if (!initOk) { + throw new Error(`InitProtection 失败,错误码: ${lastCode}; tried=${resourceRoots.join(' | ')}`) + } + + const wcdbInit = lib.func('int32 wcdb_init()') as () => number + console.log('[WCDB4] bootstrap wcdb_init begin') + const initRc = Number(wcdbInit()) + console.log(`[WCDB4] bootstrap wcdb_init rc=${initRc}`) + if (initRc !== 0) { + throw new Error(`wcdb_init 失败,错误码: ${initRc}`) + } + + wcdbBootstrapLib = lib + return lib +} + type KoffiLibrary = { func: (signature: string) => (...args: unknown[]) => unknown } @@ -51,10 +128,7 @@ type WcdbHandleOut = [number] const nodeRequire = createRequire(import.meta.url) export class Wcdb4Client { - static readonly defaultRoot = path.join( - os.homedir(), - 'Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files' - ) + static readonly defaultRoot = Wcdb4Client.findExistingDefaultRoot() private readonly key: string private readonly accountRoot: string @@ -63,13 +137,12 @@ export class Wcdb4Client { private readonly sessionDbPath: string private koffi: KoffiModule | null = null private handle: number | null = null - private initialized = false private displayNameCache = new Map() private avatarCache = new Map() private groupNicknameCache = new Map>() private cachedSessions: Wcdb4Session[] | null = null + private cachedChatTables: { name: string; db_number: string }[] | null = null - private wcdbInit: (() => number) | null = null private wcdbShutdown: (() => number) | null = null private wcdbOpenAccount: | ((sessionDbPath: string, key: string, handleOut: WcdbHandleOut) => number) @@ -86,6 +159,9 @@ export class Wcdb4Client { outJson: WcdbVoidOut ) => number) | null = null + private wcdbGetMessageTableStats: + | ((handle: number, username: string, outJson: WcdbVoidOut) => number) + | null = null private wcdbGetDisplayNames: | ((handle: number, usernamesJson: string, outJson: WcdbVoidOut) => number) | null = null @@ -158,11 +234,11 @@ export class Wcdb4Client { } static resolveAccountRoot(accountRoot: string): string { - const target = (accountRoot || '').trim().replace(/\/+$/, '') + const target = (accountRoot || '').trim().replace(/[\\/]+$/, '') if (!target) { throw new Error('微信 4.0 账号目录不能为空') } - if (fs.existsSync(path.join(target, 'db_storage'))) { + if (Wcdb4Client.hasDbStorage(target)) { return target } if (!fs.existsSync(target)) { @@ -171,17 +247,8 @@ export class Wcdb4Client { const candidates = fs .readdirSync(target) .map((name) => path.join(target, name)) - .filter((candidate) => { - try { - return ( - fs.statSync(candidate).isDirectory() && - fs.existsSync(path.join(candidate, 'db_storage')) - ) - } catch { - return false - } - }) - .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs) + .filter((candidate) => Wcdb4Client.hasDbStorage(candidate)) + .sort(Wcdb4Client.compareAccountDirs) if (!candidates[0]) { throw new Error(`未找到包含 db_storage 的微信 4.0 账号目录: ${target}`) } @@ -194,24 +261,15 @@ export class Wcdb4Client { throw new Error(`未找到微信 4.0 数据目录: ${root}`) } - if (fs.existsSync(path.join(root, 'db_storage'))) { + if (Wcdb4Client.hasDbStorage(root)) { return root } const candidates = fs .readdirSync(root) .map((name) => path.join(root, name)) - .filter((candidate) => { - try { - return ( - fs.statSync(candidate).isDirectory() && - fs.existsSync(path.join(candidate, 'db_storage')) - ) - } catch { - return false - } - }) - .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs) + .filter((candidate) => Wcdb4Client.hasDbStorage(candidate)) + .sort(Wcdb4Client.compareAccountDirs) if (!candidates[0]) { throw new Error(`未找到包含 db_storage 的微信 4.0 账号目录: ${root}`) @@ -220,6 +278,117 @@ export class Wcdb4Client { return candidates[0] } + private static findExistingDefaultRoot(): string { + const roots = Wcdb4Client.getDefaultRootCandidates() + return roots.find((root) => Wcdb4Client.isUsableRoot(root)) || roots[0] + } + + private static getDefaultRootCandidates(): string[] { + const home = os.homedir() + if (process.platform === 'win32') { + const candidates = [ + ...Wcdb4Client.getWeflowDbPathCandidates(home), + path.join(home, 'Documents', 'WeChat Files'), + path.join(home, 'Documents', 'xwechat_files'), + path.join(home, 'WeChat Files'), + path.join(home, 'AppData', 'Roaming', 'Tencent', 'xwechat_files') + ] + for (const drive of Wcdb4Client.getWindowsDrives()) { + candidates.push(path.join(`${drive}:\\`, 'xwechat_files')) + candidates.push(path.join(`${drive}:\\`, 'WeChat Files')) + for (const child of Wcdb4Client.listDirectories(`${drive}:\\`)) { + candidates.push(path.join(child, 'xwechat_files')) + candidates.push(path.join(child, 'WeChat Files')) + } + } + return Array.from(new Set(candidates)) + } + return [path.join(home, 'Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files')] + } + + private static getWeflowDbPathCandidates(home: string): string[] { + const configPaths = [ + path.join(home, 'AppData', 'Roaming', 'weflow', 'WeFlow-config.json'), + path.join(home, 'AppData', 'Roaming', 'WeFlow', 'WeFlow-config.json') + ] + const candidates: string[] = [] + for (const configPath of configPaths) { + try { + const config = fs.readJsonSync(configPath) as { dbPath?: unknown } + if (typeof config.dbPath === 'string' && config.dbPath.trim()) { + candidates.push(config.dbPath.trim()) + } + } catch { + // WeFlow is optional; ignore missing or unreadable config. + } + } + return candidates + } + + private static getWindowsDrives(): string[] { + const drives: string[] = [] + for (let code = 67; code <= 90; code += 1) { + const drive = String.fromCharCode(code) + if (fs.existsSync(`${drive}:\\`)) drives.push(drive) + } + return drives + } + + private static listDirectories(root: string): string[] { + try { + return fs + .readdirSync(root) + .map((name) => path.join(root, name)) + .filter((candidate) => { + try { + return fs.statSync(candidate).isDirectory() + } catch { + return false + } + }) + } catch { + return [] + } + } + + private static hasDbStorage(candidate: string): boolean { + try { + return fs.statSync(candidate).isDirectory() && fs.existsSync(path.join(candidate, 'db_storage')) + } catch { + return false + } + } + + private static isUsableRoot(candidate: string): boolean { + if (!fs.existsSync(candidate)) return false + if (Wcdb4Client.hasDbStorage(candidate)) return true + try { + return fs + .readdirSync(candidate) + .some((name) => Wcdb4Client.hasDbStorage(path.join(candidate, name))) + } catch { + return false + } + } + + private static hasSessionDb(candidate: string): boolean { + return ( + fs.existsSync(path.join(candidate, 'db_storage', 'session', 'session.db')) || + fs.existsSync(path.join(candidate, 'db_storage', 'session.db')) + ) + } + + private static compareAccountDirs(a: string, b: string): number { + const aHasSession = Wcdb4Client.hasSessionDb(a) + const bHasSession = Wcdb4Client.hasSessionDb(b) + if (aHasSession !== bHasSession) return aHasSession ? -1 : 1 + try { + return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs + } catch { + return 0 + } + } + private static cleanAccountDirName(dirName: string): string { const trimmed = dirName.trim() if (!trimmed) return trimmed @@ -236,24 +405,21 @@ export class Wcdb4Client { open(): void { this.loadNativeLibrary() - if (!this.wcdbInit || !this.wcdbOpenAccount) { + if (!this.wcdbOpenAccount) { throw new Error('WCDB 4.0 native 接口未就绪') } - if (!this.initialized) { - const initResult = this.wcdbInit() - if (initResult !== 0) { - console.warn(`wcdb_init 返回 ${initResult},继续尝试 wcdb_open_account`) - } else { - this.initialized = true - } - } - + let openResult = -1 const handleOut: WcdbHandleOut = [0] - const openResult = this.wcdbOpenAccount(this.sessionDbPath, this.key, handleOut) + openResult = this.wcdbOpenAccount(this.sessionDbPath, this.key, handleOut) + if (openResult !== 0 || handleOut[0] <= 0) { + const hint = + openResult === -1005 + ? ';这通常表示密钥与当前账号数据库不匹配,请确认微信已登录目标账号,并重新自动获取密钥' + : '' throw new Error( - `wcdb_open_account 失败,错误码: ${openResult}; sessionDb=${this.sessionDbPath}; accountRoot=${this.accountRoot}; wxid=${this.wxid}` + `wcdb_open_account 失败,错误码: ${openResult}${hint}; sessionDb=${this.sessionDbPath}; accountRoot=${this.accountRoot}; wxid=${this.wxid}; keyLength=${this.key.length}` ) } @@ -269,16 +435,17 @@ export class Wcdb4Client { close(): void { this.stopMonitor() - if (!this.initialized || !this.wcdbShutdown) return + if (this.handle === null || !this.wcdbShutdown) return - try { - this.wcdbShutdown() - } catch { - // Mirror WechatExplorer: shutdown is best-effort on app close. + if (process.platform !== 'win32') { + try { + this.wcdbShutdown() + } catch { + // Shutdown is best-effort on app close. + } } this.handle = null - this.initialized = false this.cachedSessions = null this.displayNameCache.clear() this.avatarCache.clear() @@ -425,36 +592,51 @@ export class Wcdb4Client { if (this.cachedSessions) return this.cachedSessions if (!this.wcdbGetSessions) return [] - const rows = this.callJson[]>((handle, outJson) => - this.wcdbGetSessions!(handle, outJson) - ) - + const rows = this.readSessionRows() const sessions = (Array.isArray(rows) ? rows : []) .map((row) => this.normalizeSession(row)) .filter((session) => session.username) const sessionUsernames = sessions.map((session) => session.username) this.hydrateDisplayNames(sessionUsernames) - this.hydrateAvatarUrls(sessionUsernames) this.cachedSessions = sessions.map((session) => ({ ...session, - nickname: this.displayNameCache.get(session.username) || session.nickname || session.username, - avatar: this.avatarCache.get(session.username) + nickname: this.displayNameCache.get(session.username) || session.nickname || session.username })) return this.cachedSessions } getChatTables(): { name: string; db_number: string }[] { - return this.getSessions().map((session) => ({ + if (this.cachedChatTables) return this.cachedChatTables + const sessions = + this.cachedSessions || + this.readSessionRows() + .map((row) => this.normalizeSession(row)) + .filter((session) => session.username) + this.cachedChatTables = sessions.map((session) => ({ name: `Chat_${this.md5(session.username)}`, db_number: session.username })) + return this.cachedChatTables } getMessages(username: string, startTime?: number, endTime?: number): Wcdb4Message[] { - const cursorMessages = this.getMessagesByCursor(username, startTime, endTime) - if (cursorMessages) return cursorMessages + const startedAt = Date.now() + console.log( + `[WCDB4] getMessages begin username=${username} start=${startTime || 0} end=${endTime || 0}` + ) + try { + const cursorMessages = this.getMessagesByCursor(username, startTime, endTime) + if (cursorMessages) { + console.log( + `[WCDB4] getMessages cursor ok username=${username} rows=${cursorMessages.length} cost=${Date.now() - startedAt}ms` + ) + return cursorMessages + } + } catch (error) { + console.warn(`[WCDB4] cursor messages failed username=${username}:`, error) + } if (!this.wcdbGetMessages) return [] @@ -463,15 +645,96 @@ export class Wcdb4Client { let offset = 0 while (true) { - const rows = this.callJson[]>((handle, outJson) => - this.wcdbGetMessages!(handle, username, limit, offset, outJson) - ) + let rows: Record[] + try { + rows = this.callJson[]>((handle, outJson) => + this.wcdbGetMessages!(handle, username, limit, offset, outJson) + ) + } catch (error) { + console.warn( + `[WCDB4] get_messages failed username=${username} offset=${offset}:`, + error + ) + break + } const batch = Array.isArray(rows) ? rows : [] allRows.push(...batch) if (batch.length < limit) break offset += limit } + if (allRows.length === 0) { + const tableRows = this.getMessagesByTableScan(username, startTime, endTime) + if (tableRows.length > 0) { + console.log( + `[WCDB4] getMessages table scan ok username=${username} rows=${tableRows.length} cost=${Date.now() - startedAt}ms` + ) + return tableRows + } + } + + const messages = this.finalizeMessages(username, allRows, startTime, endTime) + console.log( + `[WCDB4] getMessages direct ok username=${username} rows=${messages.length} cost=${Date.now() - startedAt}ms` + ) + return messages + } + + private readSessionRows(): Record[] { + if (!this.wcdbGetSessions) return [] + const rows = this.callJson[]>((handle, outJson) => + this.wcdbGetSessions!(handle, outJson) + ) + return Array.isArray(rows) ? rows : [] + } + + private getMessagesByTableScan( + username: string, + startTime?: number, + endTime?: number + ): Wcdb4Message[] { + if (!this.wcdbGetMessageTableStats || !this.wcdbExecQuery) return [] + + let tables: { tableName: string; dbPath: string }[] = [] + try { + const rows = this.callJson[]>((handle, outJson) => + this.wcdbGetMessageTableStats!(handle, username, outJson) + ) + tables = (Array.isArray(rows) ? rows : []) + .map((row) => ({ + tableName: this.pickString(row, ['table_name', 'tableName', 'name']), + dbPath: this.pickString(row, ['db_path', 'dbPath', 'path']) + })) + .filter((row) => row.tableName && row.dbPath) + } catch (error) { + console.warn(`[WCDB4] message table stats failed username=${username}:`, error) + return [] + } + + const allRows: Record[] = [] + const begin = this.normalizeTimestamp(startTime || 0) + const end = this.normalizeTimestamp(endTime || 0) + const where = [ + begin > 0 ? `"create_time" >= ${begin}` : '', + end > 0 ? `"create_time" <= ${end}` : '' + ].filter(Boolean) + const whereSql = where.length ? ` WHERE ${where.join(' AND ')}` : '' + + for (const table of tables) { + try { + const sql = `SELECT * FROM ${this.quoteSqlIdentifier(table.tableName)}${whereSql} ORDER BY "create_time" ASC LIMIT 5000` + const rows = this.callJson[]>((handle, outJson) => + this.wcdbExecQuery!(handle, 'message', table.dbPath, sql, outJson) + ) + if (Array.isArray(rows)) allRows.push(...rows) + } catch (error) { + console.warn( + `[WCDB4] message table scan failed username=${username} db=${table.dbPath} table=${table.tableName}:`, + error + ) + } + } + return this.finalizeMessages(username, allRows, startTime, endTime) } @@ -487,6 +750,17 @@ export class Wcdb4Client { return undefined } + getAvatarUrls(usernames: string[]): Record { + const normalized = this.uniq(usernames) + this.hydrateAvatarUrls(normalized) + const result: Record = {} + for (const username of normalized) { + const avatar = this.avatarCache.get(username) + if (avatar) result[username] = avatar + } + return result + } + getMyGroupNickname(chatroomId: string): string | undefined { const groupNicknames = this.getGroupNicknames(chatroomId) for (const candidate of this.getMyUsernameCandidates()) { @@ -565,9 +839,6 @@ export class Wcdb4Client { endTime?: number ): Wcdb4Message[] { const messages = rows.map((row) => this.normalizeMessage(row)) - const senderIds = messages.map((message) => message.sender || '').filter(Boolean) - this.hydrateDisplayNames(senderIds) - this.hydrateAvatarUrls(senderIds) return messages .filter((message) => { @@ -650,6 +921,12 @@ export class Wcdb4Client { .map((member) => member.m_nsUsrName) .filter(Boolean) this.hydrateDisplayNames(missingDisplayNames) + this.hydrateAvatarUrls( + members + .filter((member) => !member.m_nsHeadImgUrl) + .map((member) => member.m_nsUsrName) + .filter(Boolean) + ) return members.map((member) => ({ ...member, nickname: @@ -815,19 +1092,49 @@ export class Wcdb4Client { const libPath = this.findNativeLibrary() const libDir = path.dirname(libPath) - const wcdbCorePath = path.join(libDir, 'libWCDB.dylib') - if (fs.existsSync(wcdbCorePath)) { - try { - koffi.load(wcdbCorePath) - } catch { - // Some builds resolve this dependency through rpath. + console.log( + `[WCDB4] loadNativeLibrary platform=${process.platform} arch=${process.arch} libPath=${libPath} cwd=${process.cwd()} resourcesPath=${process.resourcesPath || ''} WCDB_RESOURCES_PATH=${process.env.WCDB_RESOURCES_PATH || ''}` + ) + + // Reuse the module-level bootstrap if main.ts already performed + // InitProtection + wcdb_init; WCDB returns -1006 if wcdb_init is called + // twice in the same process. + let lib: KoffiLibrary + if (wcdbBootstrapLib) { + console.log('[WCDB4] reusing bootstrap lib, skip koffi.load and wcdb_init') + lib = wcdbBootstrapLib + } else { + const preloadLibraries = + process.platform === 'win32' + ? ['WCDB.dll', 'SDL2.dll'] + : process.platform === 'darwin' + ? ['libWCDB.dylib'] + : [] + for (const name of preloadLibraries) { + const preloadPath = path.join(libDir, name) + if (!fs.existsSync(preloadPath)) continue + try { + koffi.load(preloadPath) + console.log(`[WCDB4] preload ok ${preloadPath}`) + } catch { + console.warn(`[WCDB4] preload failed ${preloadPath}`) + // Some builds resolve dependencies through the platform loader path. + } + } + + console.log(`[WCDB4] koffi.load begin ${libPath}`) + lib = koffi.load(libPath) + console.log(`[WCDB4] koffi.load ok ${libPath}`) + this.initProtection(lib, libDir) + const wcdbInit = lib.func('int32 wcdb_init()') as () => number + console.log('[WCDB4] wcdb_init begin') + const initResult = wcdbInit() + console.log(`[WCDB4] wcdb_init rc=${initResult}`) + if (initResult !== 0) { + throw new Error(`wcdb_init 失败,错误码: ${initResult}`) } } - const lib = koffi.load(libPath) - this.initProtection(lib, libDir) - - this.wcdbInit = lib.func('int32 wcdb_init()') as () => number this.wcdbShutdown = lib.func('int32 wcdb_shutdown()') as () => number this.wcdbOpenAccount = lib.func( 'int32 wcdb_open_account(const char* path, const char* key, _Out_ int64* handle)' @@ -845,6 +1152,13 @@ export class Wcdb4Client { offset: number, outJson: WcdbVoidOut ) => number + try { + this.wcdbGetMessageTableStats = lib.func( + 'int32 wcdb_get_message_table_stats(int64 handle, const char* sessionId, _Out_ void** outJson)' + ) as (handle: number, username: string, outJson: WcdbVoidOut) => number + } catch { + this.wcdbGetMessageTableStats = null + } this.wcdbGetDisplayNames = lib.func( 'int32 wcdb_get_display_names(int64 handle, const char* usernamesJson, _Out_ void** outJson)' ) as (handle: number, usernamesJson: string, outJson: WcdbVoidOut) => number @@ -986,29 +1300,43 @@ export class Wcdb4Client { let lastCode = -1 for (const resourceRoot of resourceRoots) { try { + console.log(`[WCDB4] InitProtection call ${resourceRoot}`) lastCode = initProtection(resourceRoot) - if (lastCode === 0) return - } catch { + console.log(`[WCDB4] InitProtection rc=${lastCode} path=${resourceRoot}`) + if (lastCode === 0) { + console.log(`[WCDB4] InitProtection ok path=${resourceRoot}`) + return + } + } catch (error) { + console.warn(`[WCDB4] InitProtection exception path=${resourceRoot}`, error) // Try next candidate. } } - console.warn( - `InitProtection 返回 ${lastCode},继续尝试 wcdb_init/open; tried=${resourceRoots.join(' | ')}` - ) + throw new Error(`InitProtection 失败,错误码: ${lastCode}; tried=${resourceRoots.join(' | ')}`) } private findNativeLibrary(): string { + return Wcdb4Client.resolveNativeLibrary() + } + + static resolveNativeLibrary(): string { const libName = process.platform === 'darwin' ? 'libwcdb_api.dylib' : process.platform === 'linux' ? 'libwcdb_api.so' : 'wcdb_api.dll' - const platformDir = process.platform === 'darwin' ? 'macos' : process.platform + const platformDir = + process.platform === 'darwin' ? 'macos' : process.platform === 'win32' ? 'win32' : process.platform + const archDir = process.arch === 'arm64' ? 'arm64' : 'x64' const resourcesPath = process.resourcesPath || process.cwd() const candidates = [ process.env.WCDB_DLL_PATH, + path.join(resourcesPath, 'resources', 'wcdb', platformDir, archDir, libName), + path.join(resourcesPath, 'resources', 'wcdb', platformDir, 'x64', libName), + path.join(process.cwd(), 'resources', 'wcdb', platformDir, archDir, libName), + path.join(process.cwd(), 'resources', 'wcdb', platformDir, 'x64', libName), path.join(resourcesPath, 'resources', platformDir, libName), path.join(resourcesPath, 'resources', libName), path.join(process.cwd(), 'resources', platformDir, libName), @@ -1019,6 +1347,11 @@ export class Wcdb4Client { if (!found) { throw new Error(`找不到 WCDB native 库: ${candidates.join(', ')}`) } + if (process.platform === 'win32') { + const runtimeDir = path.join(resourcesPath, 'resources', 'runtime', 'win32') + const dllDir = path.dirname(found) + process.env.PATH = [dllDir, runtimeDir, process.env.PATH || ''].filter(Boolean).join(path.delimiter) + } return found } @@ -1437,4 +1770,8 @@ export class Wcdb4Client { const normalized = input > 1e12 ? Math.floor(input / 1000) : Math.floor(input) return Math.min(Math.max(normalized, 0), 2147483647) } + + private quoteSqlIdentifier(identifier: string): string { + return `"${String(identifier || '').replace(/"/g, '""')}"` + } } diff --git a/src/main/wechat-db.ts b/src/main/wechat-db.ts index 8b26f35..b36ec7c 100644 --- a/src/main/wechat-db.ts +++ b/src/main/wechat-db.ts @@ -34,12 +34,33 @@ export class WechatDb { private wcdb4Client: Wcdb4Client private chatMd5ToUsername = new Map() - constructor(rawKey: string, accountRoot?: string) { + static async create(rawKey: string, accountRoot?: string): Promise { + // WCDB native init must run on the Electron main process; worker threads + // get -1006 from wcdb_init. Initialize the client synchronously here + // (and keep create() async for callers that already await it). + return new Promise((resolve, reject) => { + try { + const client = new Wcdb4Client(rawKey, accountRoot) + client.open() + resolve(new WechatDb(rawKey, accountRoot, client)) + } catch (error) { + reject(error instanceof Error ? error : new Error(String(error))) + } + }) + } + + constructor( + rawKey: string, + accountRoot?: string, + clientOverride?: Wcdb4Client, + initialChatTables?: { name: string; db_number: string }[] + ) { console.log(`Initializing WechatDb with key length: ${rawKey.trim().length}`) - const client = new Wcdb4Client(rawKey, accountRoot) - client.open() + const client = + clientOverride || new Wcdb4Client(rawKey, accountRoot) + if (!clientOverride) client.open() this.wcdb4Client = client - for (const table of client.getChatTables()) { + for (const table of initialChatTables || client.getChatTables()) { if (table.name.startsWith('Chat_')) { this.chatMd5ToUsername.set(table.name.substring(5), table.db_number) } diff --git a/src/renderer/src/components/SettingsPanel.tsx b/src/renderer/src/components/SettingsPanel.tsx index 89798e8..257da63 100644 --- a/src/renderer/src/components/SettingsPanel.tsx +++ b/src/renderer/src/components/SettingsPanel.tsx @@ -40,6 +40,10 @@ export const SettingsPanel: React.FC = ({ onDbKeyChange, onDbRootChanged }) => { + const isWindows = window.electron.process.platform === 'win32' + const dbRootPlaceholder = isWindows + ? 'C:\\Users\\你\\Documents\\WeChat Files' + : '~/Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files' const [settings, setSettings] = useState(null) const [settingsPath, setSettingsPath] = useState('') const [apiState, setApiState] = useState(null) @@ -208,7 +212,7 @@ export const SettingsPanel: React.FC = ({ />
- 密钥保存在本机 macOS Keychain(safeStorage 加密),不会上传任何服务器。 + 密钥通过系统 safeStorage 加密保存在本机,不会上传任何服务器。
@@ -222,7 +226,7 @@ export const SettingsPanel: React.FC = ({ value={settings?.dbRoot ?? ''} onChange={(e) => setSettings(settings ? { ...settings, dbRoot: e.target.value } : null)} onBlur={(e) => handleSave({ dbRoot: e.target.value })} - placeholder="~/Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files" + placeholder={dbRootPlaceholder} spellCheck={false} /> @@ -237,7 +241,7 @@ export const SettingsPanel: React.FC = ({ {reopenStatus && {reopenStatus}}
- 指向 xwechat_files 目录,内部包含 db_storage/。修改后需重新初始化才能生效。 + 可填写微信数据总目录或具体账号目录。Windows 通常是 Documents\WeChat Files,macOS 通常是 xwechat_files;程序会自动选择包含 db_storage/session.db 的账号目录。
@@ -303,4 +307,4 @@ export const SettingsPanel: React.FC = ({ ) -} \ No newline at end of file +}