diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5e5507c..440b60f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,6 +34,7 @@ jobs: run: pnpm install --frozen-lockfile - name: Install Playwright Chromium + if: runner.os == 'macOS' run: pnpm exec playwright install chromium - name: Type check diff --git a/package.json b/package.json index b1bc64c..9638455 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "wechatexplorer", "version": "2.1.8", + "packageManager": "pnpm@7.33.7", "description": "macOS / Windows 微信聊天记录查看与 AI 群聊总结助手", "keywords": [ "wechat", @@ -59,6 +60,7 @@ "@electron-toolkit/utils": "^4.0.0", "@koromix/koffi-win32-x64": "3.1.0", "@tanstack/react-virtual": "^3.14.6", + "archiver": "^8.0.0", "cross-env": "^10.1.0", "electron-updater": "^6.6.2", "ffmpeg-static": "5.3.0", @@ -81,6 +83,7 @@ "@testing-library/jest-dom": "^7.0.0", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", + "@types/archiver": "^8.0.0", "@types/fs-extra": "^11.0.4", "@types/node": "^22.19.1", "@types/react": "^19.2.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a41dc85..3348e1c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,12 +19,14 @@ specifiers: '@testing-library/jest-dom': ^7.0.0 '@testing-library/react': ^16.3.2 '@testing-library/user-event': ^14.6.1 + '@types/archiver': ^8.0.0 '@types/fs-extra': ^11.0.4 '@types/node': ^22.19.1 '@types/react': ^19.2.7 '@types/react-dom': ^19.2.3 '@vitejs/plugin-react': ^5.1.1 '@vitest/coverage-v8': ^4.1.10 + archiver: ^8.0.0 cross-env: ^10.1.0 electron: ^43.0.0 electron-builder: ^26.0.12 @@ -57,6 +59,7 @@ dependencies: '@electron-toolkit/utils': 4.0.0_electron@43.1.0 '@koromix/koffi-win32-x64': 3.1.0 '@tanstack/react-virtual': 3.14.6_bokjwhiew3ov3ffvbmafuwoalq + archiver: 8.0.0 cross-env: 10.1.0 electron-updater: 6.8.9 ffmpeg-static: 5.3.0 @@ -79,6 +82,7 @@ devDependencies: '@testing-library/jest-dom': 7.0.0_rwcxf7kajngkz26znmtz643644 '@testing-library/react': 16.3.2_ifbmwgqdqpqwyvhobr7vc7fmyu '@testing-library/user-event': 14.6.1_rwcxf7kajngkz26znmtz643644 + '@types/archiver': 8.0.0 '@types/fs-extra': 11.0.4 '@types/node': 22.19.2 '@types/react': 19.2.7 @@ -1515,6 +1519,13 @@ packages: engines: {node: '>= 10'} dev: true + /@types/archiver/8.0.0: + resolution: {integrity: sha512-YpXPbEuv9+eUIPPQWUPahj3cvs9isWRuF+J4z+KbdYVDO3rWorWQFxUVHnwPu2AgKwvgpki5F2VMX0Xx+mX45A==} + dependencies: + '@types/node': 22.19.2 + '@types/readdir-glob': 1.1.5 + dev: true + /@types/aria-query/5.0.4: resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} dev: true @@ -1652,6 +1663,12 @@ packages: csstype: 3.2.3 dev: true + /@types/readdir-glob/1.1.5: + resolution: {integrity: sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==} + dependencies: + '@types/node': 22.19.2 + dev: true + /@types/responselike/1.0.3: resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} dependencies: @@ -1913,6 +1930,13 @@ packages: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} dev: true + /abort-controller/3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + dependencies: + event-target-shim: 5.0.1 + dev: false + /acorn-jsx/5.3.2_acorn@8.15.0: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -2049,6 +2073,25 @@ packages: - supports-color dev: true + /archiver/8.0.0: + resolution: {integrity: sha512-fV1orZfsnPn9BaSByR/qE67rJCLJEy2Ox5bq7nJh+jquWaNh6Sfec75kJ2T6PtdGUbPQlrVoSVCEOa5SdiTQ1g==} + engines: {node: '>=18'} + dependencies: + async: 3.2.6 + buffer-crc32: 1.0.0 + is-stream: 4.0.1 + lazystream: 1.0.1 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + readdir-glob: 3.0.0 + tar-stream: 3.2.0 + zip-stream: 7.0.5 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + dev: false + /argparse/2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -2178,7 +2221,6 @@ packages: /async/3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} - dev: true /asynckit/0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -2196,13 +2238,86 @@ packages: possible-typed-array-names: 1.1.0 dev: true + /b4a/1.8.1: + resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + dev: false + /balanced-match/1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true + /balanced-match/4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + dev: false + + /bare-events/2.9.1: + resolution: {integrity: sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + dev: false + + /bare-fs/4.8.0: + resolution: {integrity: sha512-fM+MhCvdQhZ7NV6S95a07gPSqjIYKn6mFaXfx266wN3ajZGl/+1AzH+ubkXQ0fFZvOe2nk9VHkzdYkQE5zMV3Q==} + engines: {bare: '>=1.28.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + dependencies: + bare-events: 2.9.1 + bare-path: 3.1.1 + bare-stream: 2.13.3_bare-events@2.9.1 + bare-url: 2.4.7 + fast-fifo: 1.3.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + dev: false + + /bare-path/3.1.1: + resolution: {integrity: sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==} + dev: false + + /bare-stream/2.13.3_bare-events@2.9.1: + resolution: {integrity: sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==} + peerDependencies: + bare-abort-controller: '*' + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + bare-buffer: + optional: true + bare-events: + optional: true + dependencies: + b4a: 1.8.1 + bare-events: 2.9.1 + streamx: 2.28.0 + teex: 1.0.1 + transitivePeerDependencies: + - react-native-b4a + dev: false + + /bare-url/2.4.7: + resolution: {integrity: sha512-o8CRCiJtib+ycO3mE4A5UChtGX4dDP2XxsWVu9P+Zc3H8tcmKwNVEDoDTXmwN+uuMhfKeT7/i7Y26xS8W7ohoA==} + dependencies: + bare-path: 3.1.1 + dev: false + /base64-js/1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - dev: true /baseline-browser-mapping/2.9.5: resolution: {integrity: sha512-D5vIoztZOq1XM54LUdttJVc96ggEsIfju2JBvht06pSzpckp3C7HReun67Bghzrtdsq9XdMGbSSB3v3GhMNmAA==} @@ -2236,6 +2351,13 @@ packages: balanced-match: 1.0.2 dev: true + /brace-expansion/5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} + dependencies: + balanced-match: 4.0.4 + dev: false + /browserslist/4.28.1: resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -2248,6 +2370,11 @@ packages: update-browserslist-db: 1.2.2_browserslist@4.28.1 dev: true + /buffer-crc32/1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + dev: false + /buffer-from/1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -2258,6 +2385,13 @@ packages: ieee754: 1.2.1 dev: true + /buffer/6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: false + /builder-util-runtime/9.3.1: resolution: {integrity: sha512-2/egrNDDnRaxVwK3A+cJq6UOlqOdedGA7JPqCeJjN2Zjk1/QB/6QUi3b714ScIGS7HafFXTyzJEOr5b44I3kvQ==} engines: {node: '>=12.0.0'} @@ -2498,6 +2632,17 @@ packages: engines: {node: '>=0.10.0'} dev: true + /compress-commons/7.0.1: + resolution: {integrity: sha512-g0S8KAD8qf4+V//pr3BfB1aBnARLXNz2Gx+jmHU0LEriUuoQUOPOulVquHKTJ8+EAIIO7fhseNDr9wK5Q9FKBQ==} + engines: {node: '>=18'} + dependencies: + crc-32: 1.2.2 + crc32-stream: 7.0.1 + is-stream: 4.0.1 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + dev: false + /concat-map/0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true @@ -2525,8 +2670,12 @@ packages: /core-util-is/1.0.2: resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} - dev: true - optional: true + + /crc-32/1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + dev: false /crc/3.8.0: resolution: {integrity: sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==} @@ -2535,6 +2684,14 @@ packages: dev: true optional: true + /crc32-stream/7.0.1: + resolution: {integrity: sha512-IBWsY8xznyQrcHn8h4bC8/4ErNke5elzgG8GcqF4RFPw6aHkWWRc7Tgw6upjaTX/CT/yQgqYENkxYsTYN+hW2g==} + engines: {node: '>=18'} + dependencies: + crc-32: 1.2.2 + readable-stream: 4.7.0 + dev: false + /cross-env/10.1.0: resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==} engines: {node: '>=20'} @@ -3249,6 +3406,24 @@ packages: engines: {node: '>=0.10.0'} dev: true + /event-target-shim/5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + dev: false + + /events-universal/1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + dependencies: + bare-events: 2.9.1 + transitivePeerDependencies: + - bare-abort-controller + dev: false + + /events/3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + dev: false + /expect-type/1.4.0: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} @@ -3272,6 +3447,10 @@ packages: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} dev: true + /fast-fifo/1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + dev: false + /fast-json-stable-stringify/2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} dev: true @@ -3540,7 +3719,7 @@ packages: /glob/8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 @@ -3747,7 +3926,6 @@ packages: /ieee754/1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - dev: true /ignore/5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} @@ -3969,6 +4147,11 @@ packages: call-bound: 1.0.4 dev: true + /is-stream/4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + dev: false + /is-string/1.1.1: resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} engines: {node: '>= 0.4'} @@ -4018,6 +4201,10 @@ packages: get-intrinsic: 1.3.0 dev: true + /isarray/1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + dev: false + /isarray/2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} dev: true @@ -4209,6 +4396,13 @@ packages: /lazy-val/1.0.5: resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==} + /lazystream/1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + dependencies: + readable-stream: 2.3.8 + dev: false + /levn/0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -4393,6 +4587,13 @@ packages: '@isaacs/brace-expansion': 5.0.0 dev: true + /minimatch/10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + dependencies: + brace-expansion: 5.0.9 + dev: false + /minimatch/3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: @@ -4540,6 +4741,11 @@ packages: abbrev: 1.1.1 dev: true + /normalize-path/3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: false + /normalize-url/6.1.0: resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} engines: {node: '>=10'} @@ -4836,6 +5042,15 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} dev: true + /process-nextick-args/2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + dev: false + + /process/0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + dev: false + /progress/2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} @@ -4916,6 +5131,18 @@ packages: - supports-color dev: true + /readable-stream/2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + dependencies: + core-util-is: 1.0.2 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + dev: false + /readable-stream/3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -4924,6 +5151,24 @@ packages: string_decoder: 1.3.0 util-deprecate: 1.0.2 + /readable-stream/4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + dev: false + + /readdir-glob/3.0.0: + resolution: {integrity: sha512-AhNB2KgKeVJr16nK9LLZbJNWnYoT23ZrumNKFDebHBdkC8KHSqWo871JAUhoWC/RtjEVdqNMFpM6qrwRbaUqpw==} + engines: {node: '>=18'} + dependencies: + minimatch: 10.2.6 + dev: false + /readdirp/5.0.0: resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} engines: {node: '>= 20.19.0'} @@ -5068,6 +5313,10 @@ packages: isarray: 2.0.5 dev: true + /safe-buffer/5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + dev: false + /safe-buffer/5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -5376,6 +5625,17 @@ packages: internal-slot: 1.1.0 dev: true + /streamx/2.28.0: + resolution: {integrity: sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==} + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.7 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + dev: false + /string-width/4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -5452,6 +5712,12 @@ packages: es-object-atoms: 1.1.1 dev: true + /string_decoder/1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + dependencies: + safe-buffer: 5.1.2 + dev: false + /string_decoder/1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} dependencies: @@ -5514,6 +5780,19 @@ packages: '@pkgr/core': 0.2.9 dev: true + /tar-stream/3.2.0: + resolution: {integrity: sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==} + dependencies: + b4a: 1.8.1 + bare-fs: 4.8.0 + fast-fifo: 1.3.2 + streamx: 2.28.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + dev: false + /tar/6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} @@ -5526,6 +5805,15 @@ packages: yallist: 4.0.0 dev: true + /teex/1.0.1: + resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} + dependencies: + streamx: 2.28.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + dev: false + /temp-file/3.4.0: resolution: {integrity: sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==} dependencies: @@ -5533,6 +5821,14 @@ packages: fs-extra: 10.1.0 dev: true + /text-decoder/1.2.7: + resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} + dependencies: + b4a: 1.8.1 + transitivePeerDependencies: + - react-native-b4a + dev: false + /tiny-async-pool/1.3.0: resolution: {integrity: sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==} dependencies: @@ -6090,6 +6386,15 @@ packages: engines: {node: '>=10'} dev: true + /zip-stream/7.0.5: + resolution: {integrity: sha512-dSvYKdvLsAHCDqPOhIwk/q5CvuWtTB3Dgpoe0uVEFjTzIOAmsQpprX25InCvrvJsirEbu1OHyy67n/kAj1Sw/w==} + engines: {node: '>=18'} + dependencies: + compress-commons: 7.0.1 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + dev: false + /zod-validation-error/4.0.2_zod@4.1.13: resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} engines: {node: '>=18.0.0'} diff --git a/scripts/after-pack.cjs b/scripts/after-pack.cjs index d50c86c..3390cd7 100644 --- a/scripts/after-pack.cjs +++ b/scripts/after-pack.cjs @@ -1,9 +1,21 @@ +/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/explicit-function-return-type */ const { chmodSync, existsSync, renameSync } = require('node:fs') const { execFileSync } = require('node:child_process') const path = require('node:path') +const asar = require('@electron/asar') const COMPATIBILITY_NAME = 'Electron' const HELPER_SUFFIXES = ['', ' (Plugin)', ' (Renderer)', ' (GPU)'] +const REQUIRED_RUNTIME_PACKAGES = [ + '@electron-toolkit/preload', + '@electron-toolkit/utils', + 'archiver', + 'electron-updater', + 'ffmpeg-static', + 'fs-extra', + 'jsonrepair', + 'koffi' +] function getRuntimeResources(context) { const productName = context.packager.appInfo.productFilename @@ -72,12 +84,28 @@ function normalizeBuilderArch(arch) { return { 0: 'ia32', 1: 'x64', 2: 'armv7l', 3: 'arm64', 4: 'universal' }[arch] || String(arch) } +function validateAsarRuntimeDependencies(runtimeResources) { + const asarPath = path.join(runtimeResources, 'app.asar') + if (!existsSync(asarPath)) throw new Error(`Missing packaged application archive: ${asarPath}`) + + const entries = new Set(asar.listPackage(asarPath)) + const missingPackages = REQUIRED_RUNTIME_PACKAGES.filter( + (packageName) => !entries.has(`/node_modules/${packageName}/package.json`) + ) + if (missingPackages.length > 0) { + throw new Error( + `Missing packaged runtime dependencies: ${missingPackages.join(', ')}. ` + + 'Use pnpm 7.33.7 so electron-builder can read pnpm-lock.yaml.' + ) + } +} function setPlistValue(plistPath, key, value) { execFileSync('/usr/libexec/PlistBuddy', ['-c', `Set :${key} ${value}`, plistPath]) } exports.default = async function afterPack(context) { const runtimeResources = getRuntimeResources(context) + validateAsarRuntimeDependencies(runtimeResources) validateSilkWasmRuntime(runtimeResources) const ffmpegPath = validateFfmpegRuntime(runtimeResources, context.electronPlatformName) validateSherpaRuntime( @@ -153,6 +181,7 @@ exports.default = async function afterPack(context) { } exports.getRuntimeResources = getRuntimeResources +exports.validateAsarRuntimeDependencies = validateAsarRuntimeDependencies exports.validateFfmpegRuntime = validateFfmpegRuntime exports.validateSilkWasmRuntime = validateSilkWasmRuntime exports.validateSherpaRuntime = validateSherpaRuntime diff --git a/src/main/export-html-template.ts b/src/main/export-html-template.ts index 98e2356..2f6453a 100644 --- a/src/main/export-html-template.ts +++ b/src/main/export-html-template.ts @@ -20,8 +20,50 @@ body { color: var(--text); font: 14px system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; } +.archive-loading { + position: fixed; + inset: 0; + z-index: 20; + display: grid; + place-content: center; + justify-items: center; + gap: 10px; + padding: 24px; + background: var(--page); + color: var(--text); + text-align: center; + transition: opacity .2s ease, visibility .2s ease; +} +.archive-loading.complete { + visibility: hidden; + opacity: 0; + pointer-events: none; +} +.archive-loading-indicator { + width: 34px; + height: 34px; + margin-bottom: 4px; + border: 3px solid #c9d9d1; + border-top-color: var(--accent); + border-radius: 50%; + animation: archive-loading-spin .8s linear infinite; +} +.archive-loading strong { font-size: 16px; } +.archive-loading span { color: var(--muted); font-size: 13px; } +.archive-loading.error .archive-loading-indicator { + display: grid; + border: 0; + animation: none; + place-items: center; + color: #a33b32; + font-size: 28px; +} +.archive-loading.error .archive-loading-indicator::before { content: '!'; } +@keyframes archive-loading-spin { to { transform: rotate(360deg); } } .page { + width: 100%; max-width: 1380px; + min-width: 0; height: 100vh; margin: auto; padding: 20px 24px; @@ -30,6 +72,8 @@ body { } .toolbar { display: grid; + width: 100%; + min-width: 0; grid-template-columns: minmax(220px, 1fr) auto; gap: 14px 24px; align-items: center; @@ -39,8 +83,14 @@ body { padding: 16px 20px; box-shadow: 0 8px 24px #29483b12; } +.archive-heading { + display: flex; + align-items: center; + gap: 8px 12px; + min-width: 0; + flex-wrap: wrap; +} .title { font-size: 18px; font-weight: 750; } -.meta { color: var(--muted); margin-left: 12px; font-size: 13px; } .controls { display: flex; gap: 8px; align-items: center; justify-content: flex-end; } .controls input, .filter-button { border: 1px solid var(--border); @@ -63,13 +113,165 @@ body { .count { margin-left: auto; color: var(--muted); font-size: 13px; } .archive-layout { display: grid; - grid-template-columns: 150px minmax(0, 1fr); + width: 100%; + max-width: 100%; + min-width: 0; + grid-template-columns: 168px minmax(0, 1fr); gap: 18px; min-height: 0; flex: 1; margin-top: 16px; } +.archive-layout.single-conversation { grid-template-columns: 168px minmax(0, 1fr); } +.archive-navigation { + display: flex; + width: 100%; + max-width: 100%; + flex-direction: column; + gap: 10px; + min-width: 0; + min-height: 0; +} +.conversation-filter { + position: relative; + display: inline-flex; + flex: 0 1 auto; + min-width: 0; + max-width: 100%; + height: 42px; + border-radius: 10px; + transition: background .15s ease; +} +.conversation-filter[hidden] { display: none; } +.conversation-trigger { + display: flex; + width: auto; + max-width: 280px; + min-width: 0; + align-items: center; + gap: 9px; + border: 0; + border-radius: 10px; + padding: 5px 9px 5px 6px; + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; + transition: background .15s ease; +} +.conversation-trigger:hover, .conversation-trigger[aria-expanded=true] { background: #edf5f1; } +.conversation-trigger:focus-visible { outline: 2px solid #5b9785; outline-offset: 1px; } +.conversation-trigger-avatar { + position: relative; + display: grid; + width: 30px; + height: 30px; + flex: 0 0 30px; + overflow: hidden; + border-radius: 8px; + place-items: center; + background: #dcebe4; + color: var(--accent); + font-size: 12px; + font-weight: 700; +} +.conversation-trigger-avatar img { width: 100%; height: 100%; object-fit: cover; } +.conversation-trigger-avatar.multiple { overflow: visible; background: transparent; } +.conversation-trigger-avatar.multiple img { + position: absolute; + width: 21px; + height: 21px; + border: 2px solid #f7fbf9; + border-radius: 6px; +} +.conversation-trigger-avatar.multiple img:first-child { top: 0; left: 0; } +.conversation-trigger-avatar.multiple img:last-child { right: 0; bottom: 0; } +.conversation-trigger-name { + flex: 0 1 auto; + min-width: 0; + max-width: 220px; + overflow: hidden; + color: var(--text); + font-size: 15px; + font-weight: 700; + text-overflow: ellipsis; + white-space: nowrap; +} +.conversation-switch-icon { + width: 14px; + height: 14px; + flex: 0 0 14px; + color: #5c6963; + stroke-width: 2; +} +.conversation-menu { + position: absolute; + z-index: 12; + top: calc(100% + 7px); + left: 0; + width: max(100%, 240px); + max-width: calc(100vw - 32px); + max-height: min(360px, 55vh); + overflow-y: auto; + border: 0; + border-radius: 10px; + padding: 6px; + background: #fff; + box-shadow: 0 10px 30px #19392d2b, 0 2px 8px #19392d14; +} +.conversation-menu[hidden] { display: none; } +.conversation-option { + display: flex; + width: 100%; + min-width: 0; + align-items: center; + gap: 10px; + border: 0; + border-radius: 7px; + padding: 8px; + background: transparent; + color: var(--text); + font: inherit; + text-align: left; + cursor: pointer; +} +.conversation-option:hover, .conversation-option:focus-visible { background: #f0f6f3; outline: 0; } +.conversation-option[aria-selected=true] { background: var(--accent-soft); color: var(--accent); } +.conversation-option-avatar { + display: grid; + width: 30px; + height: 30px; + flex: 0 0 30px; + overflow: hidden; + border-radius: 8px; + place-items: center; + background: #dcebe4; + color: var(--accent); + font-size: 12px; + font-weight: 700; +} +.conversation-option-avatar img { width: 100%; height: 100%; object-fit: cover; } +.conversation-option-avatar.multiple { position: relative; overflow: visible; background: transparent; } +.conversation-option-avatar.multiple img { + position: absolute; + width: 21px; + height: 21px; + border: 2px solid #fff; + border-radius: 6px; +} +.conversation-option-avatar.multiple img:first-child { top: 0; left: 0; } +.conversation-option-avatar.multiple img:last-child { right: 0; bottom: 0; } +.conversation-option-name { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.conversation-option-check { width: 16px; flex: 0 0 16px; color: var(--accent); text-align: center; } .timeline { + flex: 1; overflow: auto; background: #f7faf8; border: 1px solid var(--border); @@ -77,7 +279,36 @@ body { padding: 12px 9px; } .timeline-empty { padding: 10px; color: var(--muted); font-size: 12px; } -.timeline-year { margin: 6px 7px 5px; color: var(--text); font-size: 13px; font-weight: 700; } +.timeline-year-group + .timeline-year-group { margin-top: 4px; } +.timeline-year { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + margin: 0; + padding: 7px 8px; + border: 0; + border-radius: 6px; + background: transparent; + color: var(--text); + font: inherit; + font-size: 13px; + font-weight: 700; + cursor: pointer; + text-align: left; +} +.timeline-year::after { + content: '⌄'; + color: var(--muted); + font-size: 16px; + font-weight: 400; + line-height: 1; + transform: rotate(-90deg); + transition: transform .15s ease; +} +.timeline-year:hover { background: #eaf2ee; } +.timeline-year[aria-expanded=true]::after { transform: rotate(0); } +.timeline-months[hidden] { display: none; } .timeline-month { width: 100%; display: flex; @@ -98,7 +329,7 @@ body { color: var(--accent); } .timeline-month small { color: inherit; } -.scroll { overflow: auto; min-width: 0; padding: 10px 8px 36px; } +.scroll { width: 100%; max-width: 100%; overflow: auto; min-width: 0; padding: 10px 8px 36px; } .lazy-hint { width: min(100%, 820px); margin: 0 auto 12px; @@ -114,16 +345,92 @@ body { flex-direction: column; gap: 6px; width: min(100%, 820px); + min-width: 0; margin: 0 auto 22px; } +.locate-all { + position: relative; + display: grid; + width: 28px; + height: 28px; + flex: 0 0 auto; + border: 1px solid #b8d4c7; + border-radius: 50%; + padding: 0; + place-items: center; + background: #fff; + color: var(--accent); + font: inherit; + cursor: pointer; + opacity: 0; + pointer-events: none; + box-shadow: 0 3px 10px #29483b1a; + transition: opacity .15s ease, background .15s ease; +} +.locate-icon { font-size: 16px; line-height: 1; } +.locate-label { + position: absolute; + z-index: 3; + top: 50%; + left: calc(100% + 7px); + border: 1px solid #b8d4c7; + border-radius: 7px; + padding: 5px 8px; + background: #fff; + color: var(--accent); + font-size: 11px; + font-weight: 650; + line-height: 1.4; + white-space: nowrap; + opacity: 0; + pointer-events: none; + transform: translate(-2px, -50%); + box-shadow: 0 3px 10px #29483b1a; + transition: opacity .15s ease, transform .15s ease; +} +.message.sent .locate-label { right: calc(100% + 7px); left: auto; transform: translate(2px, -50%); } +.message:hover .locate-all, .locate-all:focus-visible { + opacity: 1; + pointer-events: auto; +} +.locate-all:hover, .locate-all:focus-visible { background: var(--accent-soft); outline: none; } +.locate-all:hover .locate-label, .locate-all:focus-visible .locate-label { + opacity: 1; + transform: translate(0, -50%); +} +@media (hover: none) { + .locate-all { opacity: 1; pointer-events: auto; } + .locate-label { display: none; } +} +.message.located .bubble { animation: locate-message 1.5s ease-out; } +@keyframes locate-message { + 0%, 32% { outline: 3px solid #36a477; outline-offset: 3px; } + 100% { outline: 3px solid transparent; outline-offset: 3px; } +} +@media (prefers-reduced-motion: reduce) { + .message.located .bubble { animation: none; outline: 3px solid #36a477; outline-offset: 3px; } + .archive-loading-indicator { animation: none; } +} .message.sent { align-items: flex-end; } .message.system { align-items: center; } -.message.system .row { justify-content: center; } +.message.system .row { + position: relative; + width: fit-content; + max-width: 92%; + justify-content: center; +} +.message.system .locate-all { + position: absolute; + top: 50%; + left: calc(100% + 10px); + transform: translateY(-50%); +} .message.system .avatar { display: none; } .message.system .message-stack { max-width: 92%; } .message.system .bubble { + max-width: 100%; padding: 5px 10px; border: 0; border-radius: 5px; @@ -135,7 +442,15 @@ body { } .message.system .sender { display: none; } .time { color: var(--muted); font-size: 11px; margin: 0 12px; } -.row { display: flex; gap: 12px; align-items: flex-end; max-width: 100%; } +.conversation-source { + display: inline-block; + margin-left: 8px; + padding: 2px 6px; + border-radius: 5px; + background: var(--accent-soft); + color: var(--accent); +} +.row { display: flex; width: 100%; min-width: 0; gap: 12px; align-items: flex-end; max-width: 100%; } .sent .row { flex-direction: row-reverse; } .avatar { width: 38px; @@ -167,6 +482,14 @@ body { .sender { color: var(--muted); font-size: 12px; margin-bottom: 5px; } .content { line-height: 1.7; word-break: break-word; white-space: pre-wrap; } .audio-wrap { width: 380px; max-width: 100%; min-width: 0; } +.search-highlight { + border-radius: 3px; + padding: 0 1px; + background: #ffe58f; + color: inherit; + box-decoration-break: clone; + -webkit-box-decoration-break: clone; +} .audio { display: block; width: 100%; max-width: 100%; height: 38px; } .voice-transcript { width: 100%; @@ -217,6 +540,95 @@ body { } .quote-reference strong { font-weight: 650; color: var(--text); } .quote-reference span { white-space: pre-wrap; } +.structured-content { + display: grid; + gap: 7px; + width: min(420px, 100%); + min-width: min(240px, 100%); +} +.structured-kicker { + color: var(--accent); + font-size: 11px; + font-weight: 700; +} +.structured-title { + color: var(--text); + font-size: 15px; + font-weight: 700; + line-height: 1.45; + white-space: pre-wrap; + word-break: break-word; +} +.structured-description { + color: var(--muted); + font-size: 12px; + line-height: 1.55; + white-space: pre-wrap; + word-break: break-word; +} +.structured-footer { + display: flex; + gap: 6px; + align-items: center; + padding-top: 6px; + border-top: 1px solid var(--border); + color: var(--muted); + font-size: 11px; +} +.structured-link { color: inherit; text-decoration: none; } +.structured-link:hover .structured-title { color: var(--accent); } +.location-content { + gap: 8px; + padding-top: 8px; + border-top: 3px solid #3f8f76; +} +.location-content .structured-footer { justify-content: space-between; } +.location-coordinates { + color: var(--muted); + font-size: 11px; + font-variant-numeric: tabular-nums; +} +.location-action { color: var(--accent); font-weight: 650; } +.mini-program-content { border-top: 3px solid #4d9d75; padding-top: 8px; } +.mini-program-preview { + display: block; + width: 100%; + max-height: 260px; + border-radius: 8px; + object-fit: cover; + cursor: zoom-in; +} +.mini-program-icon { + width: 20px; + height: 20px; + flex: 0 0 auto; + border-radius: 4px; + object-fit: cover; +} +.forward-content details { margin-top: 2px; } +.forward-content summary { + color: var(--accent); + cursor: pointer; + font-size: 12px; + font-weight: 650; +} +.forward-list, .forward-list ol { + display: grid; + gap: 7px; + margin: 8px 0 0; + padding-left: 18px; +} +.forward-item { color: var(--muted); font-size: 12px; line-height: 1.5; } +.forward-item strong { color: var(--text); font-weight: 650; } +.forward-item time { margin-left: 6px; font-size: 10px; } +.forward-item span { display: block; white-space: pre-wrap; word-break: break-word; } +.payment-content { + gap: 4px; + padding-left: 12px; + border-left: 4px solid #d15246; +} +.payment-content.transfer { border-left-color: var(--accent); } +.payment-content .structured-title { font-size: 16px; } .media-image { display: block; max-width: 100%; @@ -264,15 +676,102 @@ body { cursor: pointer; } @media (max-width: 760px) { + html, body { width: 100%; max-width: 100%; overflow-x: hidden; overscroll-behavior-x: none; } .page { padding: 10px; } - .toolbar { grid-template-columns: 1fr; padding: 13px; } - .controls { justify-content: flex-start; } - .controls input[type=search] { width: 100%; } - .filters { grid-column: 1; } - .count { width: 100%; margin-left: 0; } - .archive-layout { grid-template-columns: 1fr; margin-top: 10px; } - .timeline { display: flex; gap: 6px; overflow: auto; padding: 8px; } - .timeline-year { display: none; } + .toolbar { + grid-template-columns: minmax(112px, .7fr) minmax(0, 1.3fr); + gap: 8px; + padding: 10px; + border-radius: 14px; + } + .archive-heading { + grid-column: 1; + min-height: 36px; + gap: 4px; + align-items: center; + flex-wrap: nowrap; + } + .archive-heading .title { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .conversation-filter:not([hidden]) { + display: inline-flex; + width: 100%; + min-width: 0; + max-width: 100%; + flex: 1 1 auto; + height: 36px; + } + .conversation-trigger { width: 100%; max-width: none; gap: 6px; padding: 4px 6px 4px 4px; } + .conversation-trigger-avatar { width: 26px; height: 26px; flex-basis: 26px; border-radius: 7px; } + .conversation-trigger-avatar.multiple img { width: 18px; height: 18px; border-radius: 5px; } + .conversation-trigger-name { flex: 1; max-width: none; font-size: 12px; } + .conversation-switch-icon { width: 13px; height: 13px; flex-basis: 13px; } + .conversation-menu { + width: min(250px, calc(100vw - 20px)); + max-width: none; + } + .controls { grid-column: 2; min-width: 0; justify-content: flex-start; } + .controls input[type=search] { + width: 100%; + min-width: 0; + height: 36px; + padding: 0 10px; + font-size: 16px; + } + .filters { + grid-column: 1 / -1; + display: flex; + gap: 4px; + flex-wrap: wrap; + } + .filter-button { + min-height: 30px; + border-radius: 8px; + padding: 4px 6px; + font-size: 12px; + line-height: 1.2; + } + .count { + display: block; + width: 100%; + flex: 0 0 100%; + margin-left: 0; + color: var(--muted); + font-size: 11px; + line-height: 1.4; + } + .archive-layout { grid-template-columns: 1fr; align-content: start; margin-top: 10px; } + .archive-layout.single-conversation { grid-template-columns: 1fr; } + .archive-navigation { align-self: start; gap: 8px; } + .timeline { + align-self: stretch; + display: flex; + width: 100%; + max-width: 100%; + gap: 6px; + overflow: auto; + padding: 8px; + } + .timeline-year-group { + flex: 0 0 auto; + display: flex; + align-items: center; + gap: 6px; + } + .timeline-year-group + .timeline-year-group { margin-top: 0; } + .timeline-year { + flex: 0 0 auto; + width: auto; + gap: 6px; + padding: 7px 8px; + white-space: nowrap; + } + .timeline-months { display: flex; gap: 6px; } + .timeline-months[hidden] { display: none; } .timeline-month { flex: 0 0 auto; width: auto; @@ -283,8 +782,14 @@ body { border-left-color: transparent; border-bottom-color: var(--accent); } - .bubble { max-width: calc(100vw - 92px); } - .audio-wrap { width: min(260px, calc(100vw - 130px)); } + .scroll { + overflow-x: hidden; + overscroll-behavior-x: none; + touch-action: pan-y; + } + .message:not(.system) .bubble { max-width: calc(100% - 50px); } + .message:not(.system) .row.has-locate .bubble { max-width: calc(100% - 90px); } + .audio-wrap { width: min(260px, 100%); } } ` @@ -299,26 +804,67 @@ const safe = (value: unknown): string => const renderExportScript = (name: string): string => ` (() => { 'use strict' + const loadingOverlay = document.querySelector('#archive-loading') + const loadingTitle = document.querySelector('#archive-loading-title') + const loadingDetail = document.querySelector('#archive-loading-detail') + const finishLoading = () => { + if (!loadingOverlay) return + loadingOverlay.setAttribute('aria-busy', 'false') + loadingOverlay.classList.add('complete') + window.setTimeout(() => loadingOverlay.remove(), 250) + } + const failLoading = (message) => { + if (!loadingOverlay) return + loadingOverlay.setAttribute('aria-busy', 'false') + loadingOverlay.setAttribute('role', 'alert') + loadingOverlay.classList.add('error') + if (loadingTitle) loadingTitle.textContent = '无法加载聊天档案' + if (loadingDetail) loadingDetail.textContent = message + } + const initializeArchive = () => { + try { const PAGE_SIZE = ${EXPORT_PAGE_SIZE} const WINDOW_STEP = Math.floor(PAGE_SIZE / 2) const archive = window.__WECHAT_EXPORT__ || { name: ${JSON.stringify(name)}, messages: [] } - const allMessages = Array.isArray(archive.messages) ? archive.messages : [] + const legacyConversation = archive.sourceId + ? [{ id: archive.sourceId, name: archive.name || ${JSON.stringify(name)}, type: 'user', messageCount: 0 }] + : [] + const conversations = Array.isArray(archive.conversations) && archive.conversations.length + ? archive.conversations + : legacyConversation + const allMessages = (Array.isArray(archive.messages) ? archive.messages : []).map((message) => + message.exportConversationId || conversations.length !== 1 + ? message + : Object.assign({}, message, { + exportConversationId: conversations[0].id, + exportConversationName: conversations[0].name + }) + ) const list = document.querySelector('#messages') + const layout = document.querySelector('.archive-layout') + const conversationFilter = document.querySelector('#conversation-filter') + const conversationTrigger = document.querySelector('#conversation-trigger') + const conversationTriggerAvatar = document.querySelector('#conversation-trigger-avatar') + const conversationTriggerName = document.querySelector('#conversation-trigger-name') + const conversationMenu = document.querySelector('#conversation-menu') const timeline = document.querySelector('#timeline') const query = document.querySelector('#query') const count = document.querySelector('#count') - const meta = document.querySelector('#archive-meta') const title = document.querySelector('#archive-title') const filters = document.querySelector('#filters') const box = document.querySelector('#lightbox') const preview = document.querySelector('#lightbox-image') const closeButton = document.querySelector('#lightbox-close') let activeKind = 'all' + let activeConversation = 'all' let filtered = [] let windowStart = 0 let windowEnd = 0 let scrollLoadPending = false let scrollLoadSuppressed = false + let activeMonthUpdatePending = false + let expandedTimelineYear = '' + const tabPositions = new Map() let lastScrollTop = 0 let zoom = 1 @@ -334,6 +880,37 @@ const renderExportScript = (name: string): string => ` /[&<>"']/g, (character) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[character] || character ) + const decodeEntities = (value) => { + const textarea = document.createElement('textarea') + textarea.innerHTML = String(value ?? '') + return textarea.value + } + const displayText = (value) => esc(decodeEntities(value)) + const externalUrl = (value) => { + const decoded = decodeEntities(value).trim() + if (!decoded) return '' + try { + const parsed = new URL(decoded) + return parsed.protocol === 'http:' || parsed.protocol === 'https:' ? esc(decoded) : '' + } catch { + return '' + } + } + const imageUrl = (value) => { + const decoded = decodeEntities(value).trim() + if (decoded.toLowerCase().startsWith('data:image/')) return esc(decoded) + return '' + } + const urlHost = (value) => { + const decoded = decodeEntities(value).trim() + if (!decoded) return '' + try { + const parsed = new URL(decoded) + return parsed.protocol === 'http:' || parsed.protocol === 'https:' ? parsed.hostname : '' + } catch { + return '' + } + } const pad = (value) => String(value).padStart(2, '0') const fullTime = (message) => { const timestamp = Number(message.createTime || 0) @@ -351,32 +928,191 @@ const renderExportScript = (name: string): string => ` } const kindOf = (message) => { const data = message.contentData || {} - if (message.exportMediaType === 'file' || (data.type === 'share' && String(data.typeVal) === '6')) return 'file' + const shareType = String(data.typeVal || '') + if ( + message.exportMediaType === 'file' || + (data.type === 'share' && (shareType === '6' || shareType === '74')) + ) return 'file' if ( message.exportMediaType === 'image' || message.exportMediaType === 'video' || message.exportMediaType === 'sticker' || data.type === 'image' || data.type === 'video' || data.type === 'sticker' ) return 'media' if (message.voiceDataUrl || data.type === 'voice' || message.type === '语音') return 'voice' + if ( + data.type === 'system' || data.type === 'unknown' || data.type === 'redPacket' || + data.type === 'voip' || data.type === 'card' || message.from === 'system' || + (data.type === 'share' && shareType === '2000') || + message.type === '微信红包' || message.type === '转账' + ) return 'system' if ( data.type === 'share' || data.type === 'location' || data.type === 'miniProgram' || data.type === 'forwardBundle' ) return 'share' - if (data.type === 'system' || data.type === 'unknown' || message.from === 'system') return 'system' return 'text' } + const forwardedSearchText = (items) => Array.isArray(items) + ? items.flatMap((item) => [ + item.sender, + item.sentAt, + item.text, + forwardedSearchText(item.nested) + ]).flat().filter(Boolean) + : [] const searchText = (message) => [ + message.exportConversationName, message.name, - message.senderId, message.content, message.type, message.contentData && message.contentData.title, + message.contentData && message.contentData.des, + message.contentData && message.contentData.description, + message.contentData && message.contentData.appname, + message.contentData && message.contentData.appName, message.contentData && message.contentData.quotedSender, message.contentData && message.contentData.quotedContent, message.voiceTranscript, + message.contentData && message.contentData.nickname, + message.contentData && message.contentData.username, + message.contentData && message.contentData.poiname, + message.contentData && message.contentData.label, + message.contentData && message.contentData.status, + message.contentData && forwardedSearchText(message.contentData.items), message.exportMediaName ].filter(Boolean).join(' ').toLowerCase() + const shareLabel = (typeVal) => { + if (String(typeVal) === '5') return '公众号链接' + if (String(typeVal) === '51') return '视频号' + return '分享' + } + const renderShareContent = (data) => { + const label = shareLabel(data.typeVal) + let title = decodeEntities(data.title || label) + let description = decodeEntities(data.des || '') + if (String(data.typeVal) === '51' && /当前微信版本不支持展示该内容/.test(title) && description) { + const lines = description.split(/\\n+/).map((line) => line.trim()).filter(Boolean) + title = lines.shift() || label + description = lines.join('\\n') + } + const href = externalUrl(data.url) + const host = urlHost(data.url) + const appName = displayText(data.appname || label) + const titleMarkup = '