From e8c32945c31285d27a7d5f43d377ec3595bfc0e4 Mon Sep 17 00:00:00 2001 From: down <37741552+Facefall@users.noreply.github.com> Date: Sun, 7 Dec 2025 13:16:13 +0800 Subject: [PATCH 1/6] feat: browser extension init --- browser-extension/MineAssistant/.gitignore | 28 + browser-extension/MineAssistant/package.json | 18 + .../MineAssistant/pnpm-lock.yaml | 2008 +++++++++++++++++ .../MineAssistant/public/icon-with-shadow.svg | 29 + .../MineAssistant/public/icon/128.png | Bin 0 -> 12669 bytes .../MineAssistant/public/icon/16.png | Bin 0 -> 698 bytes .../MineAssistant/public/icon/32.png | Bin 0 -> 1630 bytes .../MineAssistant/public/icon/48.png | Bin 0 -> 2941 bytes .../MineAssistant/public/icon/96.png | Bin 0 -> 8080 bytes .../MineAssistant/src/background.ts | 5 + .../src/content/BrowserContextExtractor.ts | 481 ++++ .../MineAssistant/src/content/index.ts | 210 ++ .../MineAssistant/src/manifest.json | 35 + browser-extension/MineAssistant/src/popup.css | 232 ++ .../MineAssistant/src/popup.html | 99 + browser-extension/MineAssistant/src/popup.ts | 43 + .../MineAssistant/src/popupManager.ts | 435 ++++ .../MineAssistant/src/types/browserContext.ts | 165 ++ .../MineAssistant/src/types/index.ts | 3 + .../MineAssistant/src/types/message.ts | 76 + .../MineAssistant/src/vite-env.d.ts | 1 + browser-extension/MineAssistant/tsconfig.json | 19 + .../MineAssistant/vite.config.ts | 23 + browser-extension/readme.md | 156 ++ .../electron.vite.config.1764942152772.mjs | 81 + package-lock.json | 2 +- 26 files changed, 4148 insertions(+), 1 deletion(-) create mode 100644 browser-extension/MineAssistant/.gitignore create mode 100644 browser-extension/MineAssistant/package.json create mode 100644 browser-extension/MineAssistant/pnpm-lock.yaml create mode 100644 browser-extension/MineAssistant/public/icon-with-shadow.svg create mode 100644 browser-extension/MineAssistant/public/icon/128.png create mode 100644 browser-extension/MineAssistant/public/icon/16.png create mode 100644 browser-extension/MineAssistant/public/icon/32.png create mode 100644 browser-extension/MineAssistant/public/icon/48.png create mode 100644 browser-extension/MineAssistant/public/icon/96.png create mode 100644 browser-extension/MineAssistant/src/background.ts create mode 100644 browser-extension/MineAssistant/src/content/BrowserContextExtractor.ts create mode 100644 browser-extension/MineAssistant/src/content/index.ts create mode 100644 browser-extension/MineAssistant/src/manifest.json create mode 100644 browser-extension/MineAssistant/src/popup.css create mode 100644 browser-extension/MineAssistant/src/popup.html create mode 100644 browser-extension/MineAssistant/src/popup.ts create mode 100644 browser-extension/MineAssistant/src/popupManager.ts create mode 100644 browser-extension/MineAssistant/src/types/browserContext.ts create mode 100644 browser-extension/MineAssistant/src/types/index.ts create mode 100644 browser-extension/MineAssistant/src/types/message.ts create mode 100644 browser-extension/MineAssistant/src/vite-env.d.ts create mode 100644 browser-extension/MineAssistant/tsconfig.json create mode 100644 browser-extension/MineAssistant/vite.config.ts create mode 100644 browser-extension/readme.md create mode 100644 frontend/electron.vite.config.1764942152772.mjs diff --git a/browser-extension/MineAssistant/.gitignore b/browser-extension/MineAssistant/.gitignore new file mode 100644 index 0000000..49053f3 --- /dev/null +++ b/browser-extension/MineAssistant/.gitignore @@ -0,0 +1,28 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Config files +.webextrc +.webextrc.* diff --git a/browser-extension/MineAssistant/package.json b/browser-extension/MineAssistant/package.json new file mode 100644 index 0000000..fe9b8c8 --- /dev/null +++ b/browser-extension/MineAssistant/package.json @@ -0,0 +1,18 @@ +{ + "name": "MineAssistant", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "compile": "tsc --noEmit", + "dev": "vite", + "build": "vite build" + }, + "devDependencies": { + "@types/webextension-polyfill": "^0.10.0", + "typescript": "~5.6.3", + "vite": "^5.0.0", + "vite-plugin-web-extension": "^4.0.0", + "webextension-polyfill": "^0.10.0" + } +} diff --git a/browser-extension/MineAssistant/pnpm-lock.yaml b/browser-extension/MineAssistant/pnpm-lock.yaml new file mode 100644 index 0000000..619f71d --- /dev/null +++ b/browser-extension/MineAssistant/pnpm-lock.yaml @@ -0,0 +1,2008 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@types/webextension-polyfill': + specifier: ^0.10.0 + version: 0.10.7 + typescript: + specifier: ~5.6.3 + version: 5.6.3 + vite: + specifier: ^5.0.0 + version: 5.4.21(@types/node@24.10.1) + vite-plugin-web-extension: + specifier: ^4.0.0 + version: 4.5.0(@types/node@24.10.1) + webextension-polyfill: + specifier: ^0.10.0 + version: 0.10.0 + +packages: + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/runtime@7.28.2': + resolution: {integrity: sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==} + engines: {node: '>=6.9.0'} + + '@devicefarmer/adbkit-logcat@2.1.3': + resolution: {integrity: sha512-yeaGFjNBc/6+svbDeul1tNHtNChw6h8pSHAt5D+JsedUrMTN7tla7B15WLDyekxsuS2XlZHRxpuC6m92wiwCNw==} + engines: {node: '>= 4'} + + '@devicefarmer/adbkit-monkey@1.2.1': + resolution: {integrity: sha512-ZzZY/b66W2Jd6NHbAhLyDWOEIBWC11VizGFk7Wx7M61JZRz7HR9Cq5P+65RKWUU7u6wgsE8Lmh9nE4Mz+U2eTg==} + engines: {node: '>= 0.10.4'} + + '@devicefarmer/adbkit@3.3.8': + resolution: {integrity: sha512-7rBLLzWQnBwutH2WZ0EWUkQdihqrnLYCUMaB44hSol9e0/cdIhuNFcqZO0xNheAU6qqHVA8sMiLofkYTgb+lmw==} + engines: {node: '>= 0.10.4'} + hasBin: true + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@pnpm/config.env-replace@1.1.0': + resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} + engines: {node: '>=12.22.0'} + + '@pnpm/network.ca-file@1.0.2': + resolution: {integrity: sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==} + engines: {node: '>=12.22.0'} + + '@pnpm/npm-conf@2.3.1': + resolution: {integrity: sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==} + engines: {node: '>=12'} + + '@rollup/rollup-android-arm-eabi@4.53.3': + resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.53.3': + resolution: {integrity: sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.53.3': + resolution: {integrity: sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.53.3': + resolution: {integrity: sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.53.3': + resolution: {integrity: sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.53.3': + resolution: {integrity: sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.53.3': + resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.53.3': + resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.53.3': + resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.53.3': + resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.53.3': + resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-gnu@4.53.3': + resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-gnu@4.53.3': + resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.53.3': + resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.53.3': + resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.53.3': + resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.53.3': + resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openharmony-arm64@4.53.3': + resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.53.3': + resolution: {integrity: sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.53.3': + resolution: {integrity: sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.53.3': + resolution: {integrity: sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.53.3': + resolution: {integrity: sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==} + cpu: [x64] + os: [win32] + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/minimatch@3.0.5': + resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} + + '@types/node@24.10.1': + resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} + + '@types/webextension-polyfill@0.10.7': + resolution: {integrity: sha512-10ql7A0qzBmFB+F+qAke/nP1PIonS0TXZAOMVOxEUsm+lGSW6uwVcISFNa0I4Oyj0884TZVWGGMIWeXOVSNFHw==} + + adm-zip@0.5.16: + resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==} + engines: {node: '>=12.0'} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + array-differ@4.0.0: + resolution: {integrity: sha512-Q6VPTLMsmXZ47ENG3V+wQyZS1ZxXMxFyYzA+Z/GMrJ6yIutAIEf9wTyroTzmGjNfox9/h3GdGBCVh43GVFx4Uw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + array-union@3.0.1: + resolution: {integrity: sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==} + engines: {node: '>=12'} + + async-lock@1.4.1: + resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + atomically@2.1.0: + resolution: {integrity: sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + boxen@8.0.1: + resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==} + engines: {node: '>=18'} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + camelcase@8.0.0: + resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} + engines: {node: '>=16'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + charenc@0.0.2: + resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + + chrome-launcher@1.2.0: + resolution: {integrity: sha512-JbuGuBNss258bvGil7FT4HKdC3SC2K7UAEUqiPy3ACS3Yxo3hAW6bvFpCu2HsIJLgTqxgEX6BkujvzZfLpUD0Q==} + engines: {node: '>=12.13.0'} + hasBin: true + + cli-boxes@3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + + commander@2.9.0: + resolution: {integrity: sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A==} + engines: {node: '>= 0.6.x'} + + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concat-stream@1.6.2: + resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} + engines: {'0': node >= 0.8} + + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + + configstore@7.1.0: + resolution: {integrity: sha512-N4oog6YJWbR9kGyXvS7jEykLDXIE2C0ILYqNBZBp9iwiJpoCBWYsuAdW6PPFn6w06jjnC+3JstVvWHO4cZqvRg==} + engines: {node: '>=18'} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + crypt@0.0.2: + resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} + + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + + cssom@0.5.0: + resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} + + debounce@1.2.1: + resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dot-prop@9.0.0: + resolution: {integrity: sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==} + engines: {node: '>=18'} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es6-error@4.1.1: + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escape-goat@4.0.0: + resolution: {integrity: sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==} + engines: {node: '>=12'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-redact@3.5.0: + resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} + engines: {node: '>=6'} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + firefox-profile@4.7.0: + resolution: {integrity: sha512-aGApEu5bfCNbA4PGUZiRJAIU6jKmghV2UVdklXAofnNtiDjqYw0czLS46W7IfFqVKgKhFB8Ao2YoNGHY4BoIMQ==} + engines: {node: '>=18'} + hasBin: true + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-extra@11.3.2: + resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==} + engines: {node: '>=14.14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fx-runner@1.4.0: + resolution: {integrity: sha512-rci1g6U0rdTg6bAaBboP7XdRu01dzTAaKXxFf+PUqGuCv6Xu7o8NZdY1D5MvKGIjb6EdS1g3VlXOgksir1uGkg==} + hasBin: true + + get-east-asian-width@1.4.0: + resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} + engines: {node: '>=18'} + + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + + global-directory@4.0.1: + resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} + engines: {node: '>=18'} + + graceful-fs@4.2.10: + resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graceful-readlink@1.0.1: + resolution: {integrity: sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==} + + growly@1.3.0: + resolution: {integrity: sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==} + + html-escaper@3.0.3: + resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} + + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ini@4.1.1: + resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + ini@4.1.3: + resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + is-absolute@0.1.7: + resolution: {integrity: sha512-Xi9/ZSn4NFapG8RP98iNPMOeaV3mXPisxKxzKtHVqr3g56j/fBn+yZmnxSVAA8lmZbl2J9b/a4kJvfU3hqQYgA==} + engines: {node: '>=0.10.0'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-in-ci@1.0.0: + resolution: {integrity: sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==} + engines: {node: '>=18'} + hasBin: true + + is-installed-globally@1.0.0: + resolution: {integrity: sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==} + engines: {node: '>=18'} + + is-npm@6.1.0: + resolution: {integrity: sha512-O2z4/kNgyjhQwVR1Wpkbfc19JIhggF97NZNCpWTnjH7kVcZMUrnut9XSN7txI7VdyIYk5ZatOq3zvSuWpU8hoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-path-inside@4.0.0: + resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} + engines: {node: '>=12'} + + is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + + is-primitive@3.0.1: + resolution: {integrity: sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w==} + engines: {node: '>=0.10.0'} + + is-relative@0.1.3: + resolution: {integrity: sha512-wBOr+rNM4gkAZqoLRJI4myw5WzzIdQosFAAbnvfXP5z1LyzgAI3ivOKehC5KfqlQJZoihVhirgtCBj378Eg8GA==} + engines: {node: '>=0.10.0'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isexe@1.1.2: + resolution: {integrity: sha512-d2eJzK691yZwPHcv1LbeAOa91yMJ9QmfTgSO1oXB65ezVhXQsxBac2vEB4bMVms9cGzaA99n6V2viHMq82VLDw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + json-parse-even-better-errors@3.0.2: + resolution: {integrity: sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + + ky@1.14.0: + resolution: {integrity: sha512-Rczb6FMM6JT0lvrOlP5WUOCB7s9XKxzwgErzhKlKde1bEV90FXplV1o87fpt4PU/asJFiqjYJxAJyzJhcrxOsQ==} + engines: {node: '>=18'} + + latest-version@9.0.0: + resolution: {integrity: sha512-7W0vV3rqv5tokqkBAFV1LbR7HPOWzXQDpDgEuib/aJ1jsZZx6x3c2mBI+TJhJzOhkGeaLbCKEHXEXLfirtG2JA==} + engines: {node: '>=18'} + + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + + lighthouse-logger@2.0.2: + resolution: {integrity: sha512-vWl2+u5jgOQuZR55Z1WM0XDdrJT6mzMP8zHUct7xTlWhuQs+eV0g+QL0RQdFjT54zVmbhLCP8vIVpy1wGn/gCg==} + + lines-and-columns@2.0.4: + resolution: {integrity: sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + linkedom@0.14.26: + resolution: {integrity: sha512-mK6TrydfFA7phrnp+1j57ycBwFI5bGSW6YXlw9acHoqF+mP/y+FooEYYyniOt5Ot57FSKB3iwmnuQ1UUyNLm5A==} + + lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + + lodash.uniqby@4.7.0: + resolution: {integrity: sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + marky@1.3.0: + resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} + + md5@2.3.0: + resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + multimatch@6.0.0: + resolution: {integrity: sha512-I7tSVxHGPlmPN/enE3mS1aOSo6bWBfls+3HmuEeCUBCE7gWnm3cBXCBkpurzFjVRwC6Kld8lLaZ1Iv5vOcjvcQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-forge@1.3.3: + resolution: {integrity: sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==} + engines: {node: '>= 6.13.0'} + + node-notifier@10.0.1: + resolution: {integrity: sha512-YX7TSyDukOZ0g+gmzjB6abKu+hTGvO8+8+gIFDsRCU2t8fLV/P2unmt+LGFaIa4y64aX98Qksa97rgz4vMNeLQ==} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + os-shim@0.1.3: + resolution: {integrity: sha512-jd0cvB8qQ5uVt0lvCIexBaROw1KyKm5sbulg2fWOHjETisuCzWyt+eTZKEMs8v6HwzoGs8xik26jg7eCM6pS+A==} + engines: {node: '>= 0.4.0'} + + package-json@10.0.1: + resolution: {integrity: sha512-ua1L4OgXSBdsu1FPb7F3tYH0F48a6kxvod4pLUlGY9COeJAJQNX/sNH2IiEmsxw7lqYiAwrdHMjz1FctOsyDQg==} + engines: {node: '>=18'} + + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + + parse-json@7.1.1: + resolution: {integrity: sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw==} + engines: {node: '>=16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + + pino-std-serializers@7.0.0: + resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} + + pino@9.7.0: + resolution: {integrity: sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg==} + hasBin: true + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + + promise-toolbox@0.21.0: + resolution: {integrity: sha512-NV8aTmpwrZv+Iys54sSFOBx3tuVaOBvvrft5PNppnxy9xpU/akHbaWIril22AB22zaPgrgwKdD0KsrM0ptUtpg==} + engines: {node: '>=6'} + + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + + pupa@3.3.0: + resolution: {integrity: sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA==} + engines: {node: '>=12.20'} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + registry-auth-token@5.1.0: + resolution: {integrity: sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw==} + engines: {node: '>=14'} + + registry-url@6.0.1: + resolution: {integrity: sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==} + engines: {node: '>=12'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + rollup@4.53.3: + resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + sax@1.4.3: + resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==} + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + set-value@4.1.0: + resolution: {integrity: sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw==} + engines: {node: '>=11.0'} + + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + + shell-quote@1.7.3: + resolution: {integrity: sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==} + + shellwords@0.1.1: + resolution: {integrity: sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==} + + sonic-boom@4.2.0: + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + spawn-sync@1.0.15: + resolution: {integrity: sha512-9DWBgrgYZzNghseho0JOuh+5fg9u6QWhAWa51QC7+U5rCheZ/j1DrEZnyE0RBBRqZ9uEXGPgSSM0nky6burpVw==} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + split@1.0.1: + resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + + strip-bom@5.0.0: + resolution: {integrity: sha512-p+byADHF7SzEcVnLvc/r3uognM1hUhObuHXxJcgLCfD194XAkaLbjq3Wzb0N5G2tgIjH0dgT708Z51QxMeu60A==} + engines: {node: '>=12'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strip-json-comments@5.0.2: + resolution: {integrity: sha512-4X2FR3UwhNUE9G49aIsJW5hRRR3GXGTBTZRMfv568O60ojM8HcWjV/VxAxCDW3SUND33O6ZY66ZuRcdkj73q2g==} + engines: {node: '>=14.16'} + + stubborn-fs@2.0.0: + resolution: {integrity: sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==} + + stubborn-utils@1.0.2: + resolution: {integrity: sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==} + + thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tmp@0.2.5: + resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} + engines: {node: '>=14.14'} + + type-fest@3.13.1: + resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} + engines: {node: '>=14.16'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + engines: {node: '>=14.17'} + hasBin: true + + uhyphen@0.2.0: + resolution: {integrity: sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==} + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + update-notifier@7.3.1: + resolution: {integrity: sha512-+dwUY4L35XFYEzE+OAL3sarJdUioVovq+8f7lcIJ7wnmnYQV5UD1Y/lcwaMSyaQ6Bj3JMj1XSTjZbNLHn/19yA==} + engines: {node: '>=18'} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + vite-plugin-web-extension@4.5.0: + resolution: {integrity: sha512-5e48v9GApUqIPl9Pgyp1baZfDSTiGkotnR7hH52S9us5f4OxK/JueeEfn1E/fTV/cSlhYu/0qTCpIINeTQVnog==} + engines: {node: '>=16'} + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + watchpack@2.4.4: + resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==} + engines: {node: '>=10.13.0'} + + web-ext-option-types@8.3.1: + resolution: {integrity: sha512-mKG1fplVXMKYaEeSs35v/x9YIx7FJJDCBQNoLoMvUXeFck0rNC2qnHsYaRnVXXd1XL7o/hz+5+T7YqpTVyEK3w==} + + web-ext-run@0.2.4: + resolution: {integrity: sha512-rQicL7OwuqWdQWI33JkSXKcp7cuv1mJG8u3jRQwx/8aDsmhbTHs9ZRmNYOL+LX0wX8edIEQX8jj4bB60GoXtKA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + + webextension-polyfill@0.10.0: + resolution: {integrity: sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g==} + + when-exit@2.1.5: + resolution: {integrity: sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==} + + when@3.7.7: + resolution: {integrity: sha512-9lFZp/KHoqH6bPKjbWqa+3Dg/K/r2v0X/3/G2x4DBGchVS2QX2VXL3cZV994WQVnTM1/PD71Az25nAzryEUugw==} + + which@1.2.4: + resolution: {integrity: sha512-zDRAqDSBudazdfM9zpiI30Fu9ve47htYXcGi3ln0wfKu2a7SmrT6F3VDoYONu//48V8Vz4TdCRNPjtvyRO3yBA==} + hasBin: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + widest-line@5.0.0: + resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} + engines: {node: '>=18'} + + winreg@0.0.12: + resolution: {integrity: sha512-typ/+JRmi7RqP1NanzFULK36vczznSNN8kWVA9vIqXyv8GhghUlwhGp1Xj3Nms1FsPcNnsQrJOR10N58/nQ9hQ==} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + xdg-basedir@5.1.0: + resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} + engines: {node: '>=12'} + + xml2js@0.6.2: + resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + engines: {node: '>= 14.6'} + hasBin: true + + zip-dir@2.0.0: + resolution: {integrity: sha512-uhlsJZWz26FLYXOD6WVuq+fIcZ3aBPGo/cFdiLlv3KNwpa52IF3ISV8fLhQLiqVu5No3VhlqlgthN6gehil1Dg==} + +snapshots: + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/runtime@7.28.2': {} + + '@devicefarmer/adbkit-logcat@2.1.3': {} + + '@devicefarmer/adbkit-monkey@1.2.1': {} + + '@devicefarmer/adbkit@3.3.8': + dependencies: + '@devicefarmer/adbkit-logcat': 2.1.3 + '@devicefarmer/adbkit-monkey': 1.2.1 + bluebird: 3.7.2 + commander: 9.5.0 + debug: 4.3.7 + node-forge: 1.3.3 + split: 1.0.1 + transitivePeerDependencies: + - supports-color + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@pnpm/config.env-replace@1.1.0': {} + + '@pnpm/network.ca-file@1.0.2': + dependencies: + graceful-fs: 4.2.10 + + '@pnpm/npm-conf@2.3.1': + dependencies: + '@pnpm/config.env-replace': 1.1.0 + '@pnpm/network.ca-file': 1.0.2 + config-chain: 1.1.13 + + '@rollup/rollup-android-arm-eabi@4.53.3': + optional: true + + '@rollup/rollup-android-arm64@4.53.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.53.3': + optional: true + + '@rollup/rollup-darwin-x64@4.53.3': + optional: true + + '@rollup/rollup-freebsd-arm64@4.53.3': + optional: true + + '@rollup/rollup-freebsd-x64@4.53.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.53.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.53.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.53.3': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.53.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-x64-musl@4.53.3': + optional: true + + '@rollup/rollup-openharmony-arm64@4.53.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.53.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.53.3': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.53.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.53.3': + optional: true + + '@types/estree@1.0.8': {} + + '@types/minimatch@3.0.5': {} + + '@types/node@24.10.1': + dependencies: + undici-types: 7.16.0 + + '@types/webextension-polyfill@0.10.7': {} + + adm-zip@0.5.16: {} + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@6.2.3: {} + + array-differ@4.0.0: {} + + array-union@3.0.1: {} + + async-lock@1.4.1: {} + + async@3.2.6: {} + + atomic-sleep@1.0.0: {} + + atomically@2.1.0: + dependencies: + stubborn-fs: 2.0.0 + when-exit: 2.1.5 + + balanced-match@1.0.2: {} + + bluebird@3.7.2: {} + + boolbase@1.0.0: {} + + boxen@8.0.1: + dependencies: + ansi-align: 3.0.1 + camelcase: 8.0.0 + chalk: 5.6.2 + cli-boxes: 3.0.0 + string-width: 7.2.0 + type-fest: 4.41.0 + widest-line: 5.0.0 + wrap-ansi: 9.0.2 + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + buffer-from@1.1.2: {} + + camelcase@8.0.0: {} + + chalk@5.6.2: {} + + charenc@0.0.2: {} + + chrome-launcher@1.2.0: + dependencies: + '@types/node': 24.10.1 + escape-string-regexp: 4.0.0 + is-wsl: 2.2.0 + lighthouse-logger: 2.0.2 + transitivePeerDependencies: + - supports-color + + cli-boxes@3.0.0: {} + + commander@2.9.0: + dependencies: + graceful-readlink: 1.0.1 + + commander@9.5.0: {} + + concat-map@0.0.1: {} + + concat-stream@1.6.2: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 2.3.8 + typedarray: 0.0.6 + + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + + configstore@7.1.0: + dependencies: + atomically: 2.1.0 + dot-prop: 9.0.0 + graceful-fs: 4.2.11 + xdg-basedir: 5.1.0 + + core-util-is@1.0.3: {} + + crypt@0.0.2: {} + + css-select@5.2.2: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-what@6.2.2: {} + + cssom@0.5.0: {} + + debounce@1.2.1: {} + + debug@4.3.7: + dependencies: + ms: 2.1.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-extend@0.6.0: {} + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dot-prop@9.0.0: + dependencies: + type-fest: 4.41.0 + + emoji-regex@10.6.0: {} + + emoji-regex@8.0.0: {} + + entities@4.5.0: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es6-error@4.1.1: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escape-goat@4.0.0: {} + + escape-string-regexp@4.0.0: {} + + fast-deep-equal@3.1.3: {} + + fast-redact@3.5.0: {} + + fast-uri@3.1.0: {} + + firefox-profile@4.7.0: + dependencies: + adm-zip: 0.5.16 + fs-extra: 11.3.2 + ini: 4.1.3 + minimist: 1.2.8 + xml2js: 0.6.2 + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs-extra@11.3.2: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fsevents@2.3.3: + optional: true + + fx-runner@1.4.0: + dependencies: + commander: 2.9.0 + shell-quote: 1.7.3 + spawn-sync: 1.0.15 + when: 3.7.7 + which: 1.2.4 + winreg: 0.0.12 + + get-east-asian-width@1.4.0: {} + + glob-to-regexp@0.4.1: {} + + global-directory@4.0.1: + dependencies: + ini: 4.1.1 + + graceful-fs@4.2.10: {} + + graceful-fs@4.2.11: {} + + graceful-readlink@1.0.1: {} + + growly@1.3.0: {} + + html-escaper@3.0.3: {} + + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + + immediate@3.0.6: {} + + inherits@2.0.4: {} + + ini@1.3.8: {} + + ini@4.1.1: {} + + ini@4.1.3: {} + + is-absolute@0.1.7: + dependencies: + is-relative: 0.1.3 + + is-arrayish@0.2.1: {} + + is-buffer@1.1.6: {} + + is-docker@2.2.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-in-ci@1.0.0: {} + + is-installed-globally@1.0.0: + dependencies: + global-directory: 4.0.1 + is-path-inside: 4.0.0 + + is-npm@6.1.0: {} + + is-path-inside@4.0.0: {} + + is-plain-object@2.0.4: + dependencies: + isobject: 3.0.1 + + is-primitive@3.0.1: {} + + is-relative@0.1.3: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + isarray@1.0.0: {} + + isexe@1.1.2: {} + + isexe@2.0.0: {} + + isobject@3.0.1: {} + + js-tokens@4.0.0: {} + + json-parse-even-better-errors@3.0.2: {} + + json-schema-traverse@1.0.0: {} + + json5@2.2.3: {} + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + + ky@1.14.0: {} + + latest-version@9.0.0: + dependencies: + package-json: 10.0.1 + + lie@3.3.0: + dependencies: + immediate: 3.0.6 + + lighthouse-logger@2.0.2: + dependencies: + debug: 4.4.3 + marky: 1.3.0 + transitivePeerDependencies: + - supports-color + + lines-and-columns@2.0.4: {} + + linkedom@0.14.26: + dependencies: + css-select: 5.2.2 + cssom: 0.5.0 + html-escaper: 3.0.3 + htmlparser2: 8.0.2 + uhyphen: 0.2.0 + + lodash.uniq@4.5.0: {} + + lodash.uniqby@4.7.0: {} + + make-error@1.3.6: {} + + marky@1.3.0: {} + + md5@2.3.0: + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + is-buffer: 1.1.6 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimist@1.2.8: {} + + ms@2.1.3: {} + + multimatch@6.0.0: + dependencies: + '@types/minimatch': 3.0.5 + array-differ: 4.0.0 + array-union: 3.0.1 + minimatch: 3.1.2 + + nanoid@3.3.11: {} + + node-forge@1.3.3: {} + + node-notifier@10.0.1: + dependencies: + growly: 1.3.0 + is-wsl: 2.2.0 + semver: 7.7.3 + shellwords: 0.1.1 + uuid: 8.3.2 + which: 2.0.2 + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + on-exit-leak-free@2.1.2: {} + + os-shim@0.1.3: {} + + package-json@10.0.1: + dependencies: + ky: 1.14.0 + registry-auth-token: 5.1.0 + registry-url: 6.0.1 + semver: 7.7.3 + + pako@1.0.11: {} + + parse-json@7.1.1: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.4 + json-parse-even-better-errors: 3.0.2 + lines-and-columns: 2.0.4 + type-fest: 3.13.1 + + picocolors@1.1.1: {} + + pino-abstract-transport@2.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.0.0: {} + + pino@9.7.0: + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.5.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.0.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.0 + thread-stream: 3.1.0 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + process-nextick-args@2.0.1: {} + + process-warning@5.0.0: {} + + promise-toolbox@0.21.0: + dependencies: + make-error: 1.3.6 + + proto-list@1.2.4: {} + + pupa@3.3.0: + dependencies: + escape-goat: 4.0.0 + + quick-format-unescaped@4.0.4: {} + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + 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 + + real-require@0.2.0: {} + + registry-auth-token@5.1.0: + dependencies: + '@pnpm/npm-conf': 2.3.1 + + registry-url@6.0.1: + dependencies: + rc: 1.2.8 + + require-from-string@2.0.2: {} + + rollup@4.53.3: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.53.3 + '@rollup/rollup-android-arm64': 4.53.3 + '@rollup/rollup-darwin-arm64': 4.53.3 + '@rollup/rollup-darwin-x64': 4.53.3 + '@rollup/rollup-freebsd-arm64': 4.53.3 + '@rollup/rollup-freebsd-x64': 4.53.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.53.3 + '@rollup/rollup-linux-arm-musleabihf': 4.53.3 + '@rollup/rollup-linux-arm64-gnu': 4.53.3 + '@rollup/rollup-linux-arm64-musl': 4.53.3 + '@rollup/rollup-linux-loong64-gnu': 4.53.3 + '@rollup/rollup-linux-ppc64-gnu': 4.53.3 + '@rollup/rollup-linux-riscv64-gnu': 4.53.3 + '@rollup/rollup-linux-riscv64-musl': 4.53.3 + '@rollup/rollup-linux-s390x-gnu': 4.53.3 + '@rollup/rollup-linux-x64-gnu': 4.53.3 + '@rollup/rollup-linux-x64-musl': 4.53.3 + '@rollup/rollup-openharmony-arm64': 4.53.3 + '@rollup/rollup-win32-arm64-msvc': 4.53.3 + '@rollup/rollup-win32-ia32-msvc': 4.53.3 + '@rollup/rollup-win32-x64-gnu': 4.53.3 + '@rollup/rollup-win32-x64-msvc': 4.53.3 + fsevents: 2.3.3 + + safe-buffer@5.1.2: {} + + safe-stable-stringify@2.5.0: {} + + sax@1.4.3: {} + + semver@7.7.3: {} + + set-value@4.1.0: + dependencies: + is-plain-object: 2.0.4 + is-primitive: 3.0.1 + + setimmediate@1.0.5: {} + + shell-quote@1.7.3: {} + + shellwords@0.1.1: {} + + sonic-boom@4.2.0: + dependencies: + atomic-sleep: 1.0.0 + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + spawn-sync@1.0.15: + dependencies: + concat-stream: 1.6.2 + os-shim: 0.1.3 + + split2@4.2.0: {} + + split@1.0.1: + dependencies: + through: 2.3.8 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.4.0 + strip-ansi: 7.1.2 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + + strip-bom@5.0.0: {} + + strip-json-comments@2.0.1: {} + + strip-json-comments@5.0.2: {} + + stubborn-fs@2.0.0: + dependencies: + stubborn-utils: 1.0.2 + + stubborn-utils@1.0.2: {} + + thread-stream@3.1.0: + dependencies: + real-require: 0.2.0 + + through@2.3.8: {} + + tmp@0.2.5: {} + + type-fest@3.13.1: {} + + type-fest@4.41.0: {} + + typedarray@0.0.6: {} + + typescript@5.6.3: {} + + uhyphen@0.2.0: {} + + undici-types@7.16.0: {} + + universalify@2.0.1: {} + + update-notifier@7.3.1: + dependencies: + boxen: 8.0.1 + chalk: 5.6.2 + configstore: 7.1.0 + is-in-ci: 1.0.0 + is-installed-globally: 1.0.0 + is-npm: 6.1.0 + latest-version: 9.0.0 + pupa: 3.3.0 + semver: 7.7.3 + xdg-basedir: 5.1.0 + + util-deprecate@1.0.2: {} + + uuid@8.3.2: {} + + vite-plugin-web-extension@4.5.0(@types/node@24.10.1): + dependencies: + ajv: 8.17.1 + async-lock: 1.4.1 + fs-extra: 10.1.0 + json5: 2.2.3 + linkedom: 0.14.26 + lodash.uniq: 4.5.0 + lodash.uniqby: 4.7.0 + md5: 2.3.0 + vite: 5.4.21(@types/node@24.10.1) + web-ext-option-types: 8.3.1 + web-ext-run: 0.2.4 + webextension-polyfill: 0.10.0 + yaml: 2.8.2 + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.21(@types/node@24.10.1): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.53.3 + optionalDependencies: + '@types/node': 24.10.1 + fsevents: 2.3.3 + + watchpack@2.4.4: + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + + web-ext-option-types@8.3.1: {} + + web-ext-run@0.2.4: + dependencies: + '@babel/runtime': 7.28.2 + '@devicefarmer/adbkit': 3.3.8 + chrome-launcher: 1.2.0 + debounce: 1.2.1 + es6-error: 4.1.1 + firefox-profile: 4.7.0 + fx-runner: 1.4.0 + multimatch: 6.0.0 + node-notifier: 10.0.1 + parse-json: 7.1.1 + pino: 9.7.0 + promise-toolbox: 0.21.0 + set-value: 4.1.0 + source-map-support: 0.5.21 + strip-bom: 5.0.0 + strip-json-comments: 5.0.2 + tmp: 0.2.5 + update-notifier: 7.3.1 + watchpack: 2.4.4 + zip-dir: 2.0.0 + transitivePeerDependencies: + - supports-color + + webextension-polyfill@0.10.0: {} + + when-exit@2.1.5: {} + + when@3.7.7: {} + + which@1.2.4: + dependencies: + is-absolute: 0.1.7 + isexe: 1.1.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + widest-line@5.0.0: + dependencies: + string-width: 7.2.0 + + winreg@0.0.12: {} + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.1.2 + + xdg-basedir@5.1.0: {} + + xml2js@0.6.2: + dependencies: + sax: 1.4.3 + xmlbuilder: 11.0.1 + + xmlbuilder@11.0.1: {} + + yaml@2.8.2: {} + + zip-dir@2.0.0: + dependencies: + async: 3.2.6 + jszip: 3.10.1 diff --git a/browser-extension/MineAssistant/public/icon-with-shadow.svg b/browser-extension/MineAssistant/public/icon-with-shadow.svg new file mode 100644 index 0000000..9a36a6f --- /dev/null +++ b/browser-extension/MineAssistant/public/icon-with-shadow.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser-extension/MineAssistant/public/icon/128.png b/browser-extension/MineAssistant/public/icon/128.png new file mode 100644 index 0000000000000000000000000000000000000000..53e9353acf1060645a8d1569fc33a0e93d172135 GIT binary patch literal 12669 zcmV-@F@nyCP)He2?8IQLSdnK){2@vu0?L(2WW|~x zl}h3-0lp-$T`{9ll5#nA7*bZH%2vXF76d{t2(T6_MY9PB1PsGATfdY0?mgSR@BMqa zr+d0)#=6k+f6IM$JLmk)a__s3@T0V0i66gl0rQ8pVGCb@W!eV9c5uIdIoeI{`0d+p z3MRsj(g`Jgd}ku;S=%w+FM{ccbZL9~OUHd0RZbbwAJTC?#}Zxgxp(0dO!N3rI&R`; z|M4P@_S?`MTSPZ^C3txoI1w915{+4j3?q2om*f0!zPBHzX!;QYG>M&@pAHUOqJBatR-fhreErGx3KEWO;hJ6Q^kU3(WwT*vkWSC!NjxptHAO zd3gb}%!!6nL3x&LAvOZMqk{S-Z(;-<%AtKYMblpx2H?j(x`??g+Y?c@Wr7o~z_BA2 zj)+*Kn~aDYq+BIFJ`>M9FUk49MEmJo_b=fTO=lzn@Z-O-4RcAFIT5?VmB_|fl58Au zDel!+i^Nmtjsyl=!W)LDm;*4?KqU1Hm*{Ss!s)bT0Dk&24oM%70d_XE3xevI8lw@a*gUs;pXuFUYx?|v~mM8!MAMPnTb8)Wz0Q5cfWY~peZ zIVKSnC0|#L?Ybl;|NHH~@z)n!vVlrB6Q+qvLOE6@XqW-0pIwQ#pynCONLlye*IxWz z2fo)|a)6{?edw=_0Xpw<4;zC6X*>=jt@Us^+q>_lPM#Zi!U*s$6XAItx2Fzl&qlQj z40l8O$vSHP$l#iags9!&H$BsF5IPh@9tOwqwsS68jF+|f%vgQnV`h31UG0i8hdYGR`-MIc_FhYkDNT2>K z-a+$tD7AMx2|Lpe?->5$qEF>I>%D=%lu^3eBk6hm?ODoziANR;8JywSw{3kEX3?8v zk1+x4zWVV9A{R!x%8xN~^xCeeI#Hj;dgb{?55KSU6R^JNANtVOXwGn*H;)77A{LX7 z+;#OMybBwg*31B(`nP;d>ffF;I13~LWCXQi1R@GESY8@+k|gMs^A5}tJTXMH=Lv#D zOP*s!8WF|IkTUyFILF^6#d0ulTrooVdGzS}jKCCl%NPO8FV_MP0iiC(M+%= z2H2foza#vfH=c5cyp`Z`-H-LR{HOU8DJU?Gs|iKJ%tUE0-n#XBbz^Mc$tPs;U#^!b zJAoTwF-sr_HE~;skBp=E9f;}2^>fcH|2Q5y^nM=`=|dL?jKJiRe`pv3=Be(!`eD8i z80Lhcyl^StjCLE|HxWIqS#Y|*y$3+duUzZU=8GQc47n4su`fC z{>Dmwi}co=DI-#t1eNrW_hgbhfmb^|+fSFjMCOG8r# zCP94kX{e}Pz2<>6$`C^$KH$iqV=m*P2(ta&Vd86jY4w316_9}*!!&mZhH%01q5p+lMfzDGlSJBCFOk>_ znOBD1&`^r!4fPG@eO{PBe9qiKn$+J>I{SWw_Vdb8KPBzGax9HeUUPmrDvFdc~EnA;~+W-^9U+H{`uv0UC8b?^|c$i))KZEbDkR9%n zYb1OYbUPP$s-}}80(M7cA00 z3_xapR+^)32FB8<21>

Vt|%(S+eJRY%k0nh^+p_Qp@+jsAQ$06Q2JVMen&p}ZTk zP?fJ-AatnUFe4{w^|IcyHU#KGON4L)_4rDYM0|(95a)L!ELPCsdnrTX1A-fFEal=< z#c+uP@{De4{)+Gz>wXT#p3VjI#nY@QG+m7~zQ>3O9(et4+VxmeL`q`}G+m(@tkCv} z8a8g!!+O%1YydVUAXjy2(Q7Yc5LlRbKs!f{NO}rr5;5OwqM%C%=gc3JmI#VOu~>;< zRXf#gm>{&;1jifPR$WN?R)K#Tg4`<#@Exkr1tL~VrT6l^;x=;qtPF*@%^Sf3YjXpu z4yklTeGlVSa}_mWXap~?-pfg1kn9%L%pp|@(a4MpO;ZGHhGPWN2n>Po^HV7_X1b2T zP|qKGC!Rd~9?PmQZAAqBi_JR+2Qx3fhVyG>)XQ+x#*#f(6|&BB0)e0+f>EONhFLDs zdf{fGfMJRvIY&l+HjlvRR9r$WcOkog+KyEcaMk%x4`jPIlqd363%!eVtP7f{JqzIgFb&`r#FWhfpWvp!pFn;GS;8g90)>O z@72O3HR^X5%(;-a2jbGOA@I|A^l$JUsjrn?vLl!#^?w#-K0ahu&oso1WyRmZ$rBlx zOTswk(!rqf)5KMG5bsU|zG(jGMDRlFx6MD}%+oN~)o0y-&mZ2I&%_R5a?p*M*3q@2 z%|`&&{3`Ae&>7m&1>;)H)$IQ`_m(SBu~Ere%2b`_jKIZ!Ja=9^0APkh#`E? zqt!G0anhO)z=>ZPF+fud-;$#%(v3*?qU8|XWH$gKZVp@}F#!tCG9nk1G|MX(N$*P3 z^B2uMgNx32dWANOtJUBC@y0)^Y^*R77`3OjQJZ`S>rQKn1eP!s zXUi?qQVmjXBAL#TCaYLyOjARTD=(mmhK9#FaiQLF_A?k+`y&;^vs-S^i@>OQJC0~m zZWR!kB4U*86(*wEXMzgYclZN&4#C0_u}_UHkeeC2icO6KlKQ?vEsOXu+}EW7`6YlM zD~BZRZv@XB3Cc@VhV(Y8l6-@65CJL)k#Lb%F-1w9@*1=cRT&d6gXnLSf5dUq>9`Du zDx&#^uYWQNhTx5XVif?AYqW}@*P7P!UO=AY5!WvNbYd5@@!KHjvJ#+*>Jf!sBIOa3Y3rXaHXDBMaY>R1c&>opPKw__F4DSrMnM*5I8jTSLcvbacK1N0cDr1vMz3}X3FoxDfck8XHA|CajK9>ikq?Bim!;0lr z)Wl52y=RdAW0&MpHKbMb**eKkB#qPpqn*Myk+k+WKx~vYrPpW=%m&EHq31fpDN?YdpDEYWr>E>G#u>pT1u0>jD+mPM zI=mDDV77Y}uB}FrtLJ%d}8uN_YW6S%Ek^VMmP{x7}pAr@6i@YqTozdV9rp_9!^ zdCT%Yee?*{nO42BC^c*;wPeZGv=433w|XHVY`qu$I4TIB z@9{y)=J6kREE-jN*njO}o0nmmK33(hUmMkpz%ZfQrtUAj{vo_@^b*qXXx^19c3;VF z+E%Z{cM0sxqIoP{^S{^kZomog(JlXkxAk+&+q!OUM{4QzJdY#VyjLHlbJs?D!NS_* zn)ID{ z{ve^irQ;n_)Z&+jkdINh-YbxLzEXWCC;jbD$NT#ZckaE4R=qRXx`E`D8#4#)N|o=_ zMzg;7);Yno;l3@55IQo3coUf^q-9&rI*5O9)fG*x6GWT3J*AV|W25g1%%cY@x|6_c z+k|lE!5eYstG`~ZR^y-qm2n765%YxcD?wBzeqjz&jI@-Zw0qLoXA?6lVTH8r#icjh zN=xbQ`c&_`Qek__D`FDWlh?fz*6r1?9`Gn18MGa907P@2(;^K2y}Gj#po734J!(=#0g5 zeRul1>ze<=JF!CAsMW>B2+7#HxzpFh1D^RVAmmKI-bvjE!aG-D4?<9MJlQTOHjZJO%C&gk zkL`YG)?zxl{g?0JTQDPS7z1ef=eN%7PIdpoXt;FXrosL~C>LhnIKUcS9`c-{3{XiG z&n8AI`IyK*(@+B{G_eDkk)J)^=o!vPGZ6kOuYAa)U7Q29$R`I!0L~yfUYS6>p0kZh zz8CV@Gra_7*fydV;wd*?^MCoFYwj9vDzV`VFr;6)2Nn|zVhma@Zu2lV}b867gCdhC{ZXdAg zHbsS^78t&}2w~yur)Lmlg1ErSd2KTXueU;p2vb%zdAXVo6ZP}B4TJQ5_0-=VB*9Q4?pY=TN1oHPa)QbK~=sge5( z_O;`>2N1*_FjQcmg*BEF24=%GHxh~o0>iESNmK~etX{{)AQz8MvP;SxG6 z$*9WI(<)6#e*-yRJ@{d~m{fo=ezeXbVIBQym^X=y&-AT<=z^n+C5B<#fsB-?ijkPF zE&j5&hnuolbea>mHC6me*&-$HfCha^(j~AYi%v_%t$89y96d4! zH-n6K{pA-eqU?wQG|MGmA-_(odc^etKGPZ&j9Pb;284De+#Ts>&!#ZIfC9ICl9w<9 zD5PmJX;`LwWok^KgQSMiz^xA~{TeQN%e`qr=w&>a2>skZ^z{2|lJ*io%Sw)Ql;VWb zFvtsApK=75px+3}r*0dM>PrrBBx!_5_Z|F5W`d~+8(a0H3nN4MRaK}7$=*c?k#qev zF=F>*873+KEvwDFx!VU@VDq+%@B2HG^0`5$cp=lmWgi@6+t7WA51Qt4$ zUy6^gklr4bVNMr*?A%9!6wl;>hCz6I51|YdyI}BX8espvS3X+Q7@)jv%o*fzv`138 z8bQT3mrz;>pg|8F$IErt{aNo-x!AnW{0TVxx)iJP( zfoOSEZRgFX5kS{V*n5^`%2=MYH5qV~;tW!EQ^&H%kD?H++&l(IGgZgf*MC*}3)$E> zam&gf_>M`^;jA}_M8c$uG6JStwA=4E?=d*>w5|!lmkXFw6P4wUah@>BFCM!L_aFSI ztcBD%L({@PL;LMj$v}K1B*FR1BwODamex|-R?QdZ`g#(wGFXF99FR8sY#ss}>2Y9g z&V}9n)fKTp$V~ddvU!AGb2^yVL*F?lBVE+ru$|H3`Cs-IN}`^gp#`Kc)VMlBUBnYP z|5}p%14l1|uZWl4M2HWUiJh7tS6c|+;sE5M6R^Y|*RI*lBw<}q*=n!m70!UVuo-cH zoNoHhv_!qnW2zez-SEL>iqWl{Upm)@hwDuzsmv!pmJ{T70w^N@Pr)}(yM*nZ=4+3+ zKGyq#-=6|L_rH7{S1xPt&*S#FRXo6cJs#rYf#CWPs;Hs-+TxHXMIoLL?`eJEdkzbm zh5)zRm?k0)tE;CxD_VT?2C*tBRIp$RN_)07Y9S*jS|~QW{k%uYvphrGhw+AY1vEp`Gt{u8M3h)mGRv z1~}^o7V9yM`dd^1%gZJmiT%VrDB4Zp@Ya!bj1fV0I6QyrlgR*l5;G0EDOI3#^>ukO z8A#G#^nLB6kGS!Lf>0WhFjcK@L77faOTjG{T?x&hJzb~nGSy*+Xje#atgZs`#F6h# z+QocUz|y8MKnh+gvQ&5s=x8t*AnIFaRG}LGl}DukN+K zTpNxp)1>sZ1J@x3>;($v=IL~BdFdw6*tbuLO(hus zwl}S;NyV1xLOx`E+&l*8`hL6mJ1Pa-zV2d@Ub>_)1FB46ksuKnec6<#>@}n-ZQpU; zqi`{j)Q03LLdKKkengn&+Dk_+!`Bl75LZtsK-nI@Y`Z@?qvn$#Ox%%a#^T<=#XB@9 zGtXcb$53Ic)Cz~$7&ZiA99l%M4CTX{$N=4y7Uxl&7`_TtI!r-7s_#p#`#~{tNjB28 z^jyC1WsF4CdXD43dV~SOq#-G{2jVB)FT7PoxY9<(+XDK+#?{N7Y-r%X2k)xJ0sz>K zAvG|u>HR7as-vRb^&mH$cn9g9Ex^7_VSxX%lNXZ}yd9xuxoLuZr9JIC14Wl{(D|&_ zLo!BDn1Es!T|Ffb}WMoCUsz=()O^BS-d=$ zubuSNUdIp#>owRy<&6_&TZT>-B;#+GJE&4rH#yCc>L^-pCgLW~ApI|i?iaDlFfXB9 z=M}bKmWKn#-}=xOLKObV^?lhIwqX>6JPmKQlS1MJ6t%(5bizRUGHq~@(;)q+uRCp{ zWL8)b!uP80xmc==-d9-FowEyPz>L4_@&>DY_<8#*15jVz5({W%ntHae171pmfAGM^ zx%-zJdfEszp7=N{fO*uuDZ$a2yd zV7^b&jvX(mL?}$kX(g1zLGPF8K|@5Aw`b#E0-*rW1H-spw(uo=YPwN8F#A;Q1j z^MUfdz)59*-`UCA5?k*sge2)SBgQ=1@NRaabU$6cEkOdrfOZfH(X_+PCF$_(Z+Zk{ zE(59srC0FC{6d;M8E5%`k?qsrG2^jG;aaD`9@Y6_pFxTe zA-yySVwPzxB8{e8eEuVD0Ge39@bJl(e-__);TQ1j7k>foNJ#OEKk)RzxFNIsD0N{xwzDMyO{1Jx=h- z>zCv3k@K;2OCjpvV+;7s5C1NnN`(LO7k&|M9J|2YzdLyNa(w&3KgLg9{NKhMF@v!d zr+a_+3A~)1bEfuLNl1TX(Qh!;9cjpNIr3G{Q<%Vvdn>SIFyM<`l?-UbB$_`REhW$Q zs+Rae+E51g+=uy^W#-!xt1ncplnw!x*2X;s>u&{>uML$kC-VfR5rh!u&UG)s!$;4@ zA3XSZ{H3@3F`ho~vsvPG6;Usilx;qNVI%4%yaQaL1yB?Jz>&*w-*f-e$V#mRCq+jB9t3m zwM4!B+{Rvz_F2Ip-TmyRoT@%eOMu#Faz9v4KJL97?W{@;(i!G(PCi%=cn3l_ zv~-87880%1aEJ$upwM8n<{xt@MKPa_4AQ^=weOdyqh_ofFv`#aaKOn6PJW`N)S zd&@VbR$rbPzK|NUEmgNI2d}HCQ#Xt{VFDwIw2)-b^0oL~9o1=uyp9~i`ifi`<=W68 z&#%K56>uGYEaND33`Ss;X1R&!+CAU<4aD_W-eY55cLVL)eq=_N6k~dwo4u2iS*w7t z98jeR+zU+UwltPYDaLSMnXvzNuc!T3V_L&r!QBmsV2e5Ctsu-U)!FkwwZ;kRKIJ9Q z9S1gnNolL#&H5*~*XP|sXn0)K3IJ6>WzhP{jRJi2v4SYv^b+Q|^v(B7!+*m7FDIAp z8!vpk-0wVO4G!((2J3C$s79FPoTbfWa!k=L_Ce6iG$=t8&fGk1z4iK)H-I=^TH|iv zTs7JVRx*(5C^oE?sb4c~HbgB@GK*;KoIQjEX+5s{_zO+TKg-c4EmNV4W%`cjO$ z5JwResB(-TW;{9=d>B;etvkVQM*i>RxFl$+IbsrE)=nC(((xRI4C z{~{8D?WWt!6&!5Br>bv>P5=7(IO&A+VjzAF08K><;av5_?|r&3^#B_B5{Dpxz1>rw z${r70_vAbU{r)nU=?W19mv~bp6uc^m>JuB1*7(&ha)O!;L{w1fUz2xG8VtB$H=OwL zWz7NuNKp$yP|_JUt{h_y&jjWRP+99+3WgTS3nP7_|09(k(*OL&O;WpT+=aLUm2<5M z5qWYnID8tIeH`00r2YG1Yf*PGV()iCwH1D8g3tc;8qMrO#^8!YT2 z)l-mQ1m<#=ibB*YBM6znX?5#)g#3IQVozj|axeRmg}dP*35-RS@ujE!mD?jhtq~l~ zV-bk92r+em_{xjH#Whc$n&(gtCK!O_atV1y&l($$)`S2$lT~8TB9Fardgolh9E~y# zM=BlS6&RjH*poq)0e8Z}7Iox%^@!>_hE^SeDZnU2BpCj`_?-PU+5n$;9f?@I;~g{=E9B3O?R@bnCT)cnv{y`#{oeKVT+x{Q?`LK(ja-4 zEZl>!4EcqxJpG%dr?oF-exj7obF}2Oeh*@msp*2#Qw#kf=n#3`j(|Z0gm)@s8D-Pj z6HjYH0FoDT%F-uP(omu}MBEUYOedCe(eH@YG=JvwTpKillYlDq1K2>r0xocPRii8p ze6A@C_th&}$XhLwl(4HNZEu>jl;w}vt>_oS*FbHg+ zr!rCKU_CWjCWRE*+4Lh|x>n8`h8jw})X@&>@VsMb$xTtOt>{zYqFwM(t-spM2RK-Pa+HXxZJtNZpXV2{j8k?F-O(Uy95RB9+ zFl|Q_+?WTUOfC5$QZ!89bpW*4Dj@G#No0hLh7QZ*w^*;ioC*=SBATL6A)U~J2DZV~ z7fDLTh0vnstC=)ml*({IjZP}L@4R4NdBdlA4LF@<$vg1_~s9GNF>>GF}XGlj8uhe$@-de zSKmNX6U6;_mgT_`TrSHQS+Ryqon*B~&fL|8P%;K*Zzcn1y@vEWzF!!c>rcj*q(y=T znAdw0Iu6GWllTHXM&jremfOl$aFJvwIxN0;=fb|&S}WlGrOy<1&naHl4|12?5TIPK z2^$Vz2L!Tj*U+(PF+jYw{JdsNqb~e&~-}?T}!n_*H$?l%=7}*jZ zuKC7|KUMwJtJ#6#s^SSR8f@yXejPLnlhR?qAv^%q4I&hkv?OlSSP^{LtD-cFy zvpcnewgA=8LxA66|6zKWf~Hb_aU?71toLvi)sZ}M+lN`g;8Y8j3X(=%g;R!Tdmm}B zjHE1eo_isb-eoC9#xqzu~0N@iya>zS=pi!+P5eQq>Hi=6=p zKU2Mp%sSKB0zoqHI$>e^;I!0J{V5P8cg&cvuVYdj!)ME|+K5Tf!78sEW_C zOvQmCQvyGB;l3cUgje3U9N&Is2W;tXT$L13L=>Ci4FglqO>?3SU;$J`IHj+BfjC&i zQwU#*6sGS~y~j~kai+6tx+ zp%YfR$d+UM#`NT|B@H=CO=n~MX>B%uhD>{Q6bX0DmMsVHrgN6GCwMjK{_i}!LyEI_qE$oMHF9)lD^8k z4uisHXVB&5nOMSj!HCcesowgghY=s(#9x) zwWxptBIe$}j0OnPv89u74M1&)E0SdKwn=U;G2~H6Aww6CSw2EwpPjKO+djI5CW$&; zWmsQ*V-ep?(y!GI!*BBzqN;r0*$p!^XoCPos`x1kKzM0IrCWzknkM$AqguJA`KUq+ zDY7tSS($PG>r87<0sY;nLECa$rO_->o~6!q?}XWAMyqJ)TSu8}<1KgN!SC(@#jX|L@+hQE(NdTJv?*VTK;p-`a8)OB z)MB*2P#xgb&~9|~COlf_qre1macTGNO=JLG?)E0Qy+J2{BDdSXFAJmvB3LjZ4qAEL zQ2|5-Sq8r09`%7u8WGn1OI$%-$A*fExvLZaD;4nFCwJhxPwgmmcCI;0qWi{plO|Iz z|8om$#*QpwwdsXkW)^fp0{^YgLZ@^LG1d97bsmKw=YnrwhgHctAbwETUafeUmeP`O@xTuas! zY9N8iGs7q0ZpH?rwe1$ZjGN*P*pU#UKg9FWfwJ3mAXIN}d)udDeV1*5eP)(J_*Lo$ zv<_+rP~aL(-O8x3eH}LpT71qd@r1|-&QzVuX6H6b``RyfcKT^_7J=w<9${bkPwR%~ z(f;VzLB=b51X3-}+Yg z4B3_o%6656Gh8AI0~INasA4gvIv*&Ylh!X#TiVSvQ*dAgCTC)qBQyj8MJ7^v!I@C< zD5Dzk6qQGYYylxLB`7G!meC6KM+JpjrA8}u-AT0 z^s%%VB{vnxVn70E}Q191F%Cdo|=P{o_be1Tgy0*=r)~G^e;0e@2 z?F@*;jv&oktD~TPiia$AjL@CNDl_&S73!&NkRnO9CF1|oMiL)5K?Klr*I(tu{up+o zQm)~Sx0$5LZ;c8lOUi;YkM9e|ceT{!q3-m_G!~s`4^r2%OrcstA(l0;o80pQSMvt=L7 zfV62JjoPS0J?)RWQ!ReA0bJK3*dsvwmGXR#M($yD9|=@>FZwHX2oJMx^K0IV=TuFn z9Rs9vcQmxP#0^*tw8S9g*a%+x|A+yiYzV>(G$lIBKqafknSu6Szi0g+*i$8)eh9F9 zY;Watpm-Kg2?i520UlDXTOxSTBal0lM^H(A@FwKV*gJ6srPGc9uKmK6eTg9ZEtn*~ zMXCeQdsIOl4@}jNuhBdU%Jkw6_NZLWPMr7ouG@n%D4mWfU?9kCIbuc@dBYr3vV$!E zxSx+E)?dmo1kgA@9hTK&64lY~N$|gfGbo*w3~=qA(w;=feKrZy7h8a2qNP&ZbFJUSXl%WPDh^)|U_T#9 z)8J=lI;|OCNY{R0Zg(QtZe2Li>7oiaL32`~>TyvYtm>kCg`3>5O53 zA>Hw}d2#M2cBP4$ofsU0YEr14`!DkXVP;Mzg@AntNN)PrUOHpKy(^_NiUBkYj6loS zm2iD~iV`ojQ_j^iZJ88$nLFCcbnLU&?L9Llc~+awNCt@Mj-Tgk==$x$$1aj&FC@Y* rQimmBhW&{J9)5Km`_E)Ozf$`Dmu9Jh2kAnq00000NkvXXu0mjf;E-lX literal 0 HcmV?d00001 diff --git a/browser-extension/MineAssistant/public/icon/16.png b/browser-extension/MineAssistant/public/icon/16.png new file mode 100644 index 0000000000000000000000000000000000000000..3fe36745faf20616d43a67ac083bd5889f7f5ae7 GIT binary patch literal 698 zcmV;r0!96aP)+@@Pv*Ibkp{}o53mKT zHT7W&+!6t@mI`EyL3m0Sbd-+N6Y32q@wVf_^l?gG1rD-G)&n>kFr*R_Qw|KvWk84s z5PP+G9HYipwD<^`7)eS90cJ;2!c0Q|xXyq_7ct;!`UDzc6tnHSSn2ivMqEP03Rv9^ z)VB|8F+M8@k03nuAi!cPht}#CY>ss98Re2DYMU!iMi^A%4$>Eo?8B;hCG9LF8T=OG zxZZe(@2PR(DHA-EHmxEn+-N?gsq8^1F)gJ*KLX8-^I literal 0 HcmV?d00001 diff --git a/browser-extension/MineAssistant/public/icon/32.png b/browser-extension/MineAssistant/public/icon/32.png new file mode 100644 index 0000000000000000000000000000000000000000..d1063e5b1bcd21f825d5e24f5b984a22cebb45b3 GIT binary patch literal 1630 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 e_&In3w*Lhs_|9Q$&b4v?0000 { + const contentType = this.detectContentType(); + const metadata = this.extractMetadata(contentType); + const structuredContent = this.extractStructuredContent(contentType); + + return { + id: this.generateContextId(), + url: location.href, + title: document.title, + timestamp: Date.now(), + metadata, + structuredContent, + rawDom: this.shouldStoreRawDom(contentType) ? document.body.innerHTML : undefined, + }; + } + + /** + * 检测内容类型 + */ + public detectContentType(): ContentType { + const url = location.href; + const html = document.documentElement.innerHTML; + + // 代码相关页面检测 + if (this.isCodePage(url, html)) { + return ContentType.CODE; + } + + // 多媒体页面检测 + if (this.isMultimediaPage(html)) { + return ContentType.MULTIMEDIA; + } + + // 数据表格页面检测 + if (this.isDataPage(html)) { + return ContentType.DATA; + } + + // 文章页面检测 + if (this.isArticlePage(html)) { + return ContentType.ARTICLE; + } + + return ContentType.UNKNOWN; + } + + /** + * 提取页面元数据 + */ + public extractMetadata(contentType?: ContentType): PageMetadata { + contentType = contentType || this.detectContentType(); + + const metaTags = this.extractMetaTags(); + const wordCount = this.countWords(document.body.textContent || ''); + + return { + domain: location.hostname, + language: document.documentElement.lang || 'en', + description: metaTags.description || '', + keywords: metaTags.keywords ? metaTags.keywords.split(',').map(k => k.trim()) : [], + author: metaTags.author || '', + publishedTime: metaTags['article:published_time'] || metaTags.publishDate || '', + modifiedTime: metaTags['article:modified_time'] || metaTags.modifiedDate || '', + wordCount, + contentType, + metaTags + }; + } + + /** + * 提取结构化内容 + */ + private extractStructuredContent(contentType: ContentType): StructuredContent { + const title = document.title; + const summary = this.generateSummary(contentType); + + const structuredContent: StructuredContent = { + title, + summary, + contentType + }; + + // 根据内容类型提取特定内容 + switch (contentType) { + case ContentType.ARTICLE: + structuredContent.articleContent = this.extractArticleContent(); + break; + case ContentType.CODE: + structuredContent.codeContent = this.extractCodeContent(); + break; + case ContentType.MULTIMEDIA: + structuredContent.multimediaContent = this.extractMultimediaContent(); + break; + case ContentType.DATA: + structuredContent.dataContent = this.extractDataContent(); + break; + } + + return structuredContent; + } + + /** + * 提取文章内容 + */ + private extractArticleContent(): ArticleContent { + const headings = this.extractHeadings(); + const paragraphs = this.extractParagraphs(); + const images = this.extractImages(); + + return { + headings, + paragraphs, + images, + }; + } + + /** + * 提取代码内容 + */ + private extractCodeContent(): CodeContent { + const codeBlocks = this.extractCodeBlocks(); + const languageStats = this.analyzeCodeLanguages(codeBlocks); + + return { + codeBlocks, + languageStats, + fileStructure: [] // 简化实现 + }; + } + + /** + * 提取多媒体内容 + */ + // TODO: 实现提取多媒体内容的逻辑 + private extractMultimediaContent(): MultimediaContent { + const mediaElements = this.extractMediaElements(); + + return { + mediaElements, + duration: undefined, + transcript: undefined + }; + } + + /** + * 提取数据内容 + * todo: 实现提取数据内容的逻辑 + */ + private extractDataContent(): DataContent { + const tables = this.extractTables(); + + return { + tables, + charts: [], // 简化实现 + dataPoints: [] // 简化实现 + }; + } + + /** + * 检测代码页面 + */ + private isCodePage(url: string, html: string): boolean { + const codeIndicators = [ + /github\.com/i, + /gitlab\.com/i, + /bitbucket\.org/i, + /stackoverflow\.com/i, + /codepen\.io/i, + /jsfiddle\.net/i, + /]*>|]*>/i, + /class=\"highlight\"/i, + /syntaxhighlighter/i + ]; + + return codeIndicators.some(indicator => indicator.test(url) || indicator.test(html)); + } + + /** + * 检测多媒体页面 + */ + private isMultimediaPage(html: string): boolean { + const mediaIndicators = [ + /]*>/i, + /]*>/i, + /]*>/i, + /youtube\.com/i, + /vimeo\.com/i, + /soundcloud\.com/i + ]; + + return mediaIndicators.some(indicator => indicator.test(html)); + } + + /** + * 检测数据页面 + */ + private isDataPage(html: string): boolean { + const dataIndicators = [ + /]*>/i, + /class=\"table\"/i, + /data-grid/i, + /chart/i, + /graph/i, + /dataset/i + ]; + + return dataIndicators.some(indicator => indicator.test(html)); + } + + /** + * 检测文章页面 + */ + private isArticlePage(html: string): boolean { + const articleIndicators = [ + /]*>/i, + /class=\"article\"/i, + /blog-post/i, + /news-article/i, + /entry-content/i + ]; + + return articleIndicators.some(indicator => indicator.test(html)); + } + + /** + * 提取meta标签 + */ + public extractMetaTags(): Record { + const metaTags: Record = {}; + const metaElements = document.querySelectorAll('meta'); + + metaElements.forEach(meta => { + const name = meta.getAttribute('name') || meta.getAttribute('property'); + const content = meta.getAttribute('content'); + + if (name && content) { + metaTags[name] = content; + } + }); + + return metaTags; + } + + /** + * 生成摘要 + */ + private generateSummary(contentType: ContentType): string { + const description = document.querySelector('meta[name="description"]')?.getAttribute('content'); + + if (description) return description; + + // 根据内容类型生成不同摘要 + switch (contentType) { + case ContentType.ARTICLE: + return this.extractFirstParagraph(); + case ContentType.CODE: + return '代码页面,包含多个代码片段'; + case ContentType.MULTIMEDIA: + return '多媒体内容页面'; + case ContentType.DATA: + return '数据表格页面'; + default: + return document.title; + } + } + + /** + * 提取标题 + */ + private extractHeadings(): Heading[] { + const headings: Heading[] = []; + const headingElements = document.querySelectorAll('h1, h2, h3, h4, h5, h6'); + + headingElements.forEach(heading => { + const level = parseInt(heading.tagName.charAt(1)); + headings.push({ + level, + text: heading.textContent?.trim() || '', + id: heading.id || undefined + }); + }); + + return headings; + } + + /** + * 提取段落 + */ + private extractParagraphs(): Paragraph[] { + const paragraphs: Paragraph[] = []; + const paragraphElements = document.querySelectorAll('p'); + + paragraphElements.forEach(p => { + const text = p.textContent?.trim(); + if (text && text.length > 10) { + paragraphs.push({ + text, + wordCount: this.countWords(text) + }); + } + }); + + return paragraphs; + } + + /** + * 提取图片 + */ + private extractImages(): Image[] { + const images: Image[] = []; + const imgElements = document.querySelectorAll('img'); + + imgElements.forEach(img => { + images.push({ + src: img.src, + alt: img.alt, + title: img.title || undefined + }); + }); + + return images; + } + + /** + * 提取代码块 + */ + private extractCodeBlocks(): CodeBlock[] { + const codeBlocks: CodeBlock[] = []; + const codeElements = document.querySelectorAll('pre code, code'); + + codeElements.forEach(code => { + const language = this.detectCodeLanguage(code); + codeBlocks.push({ + language, + code: code.textContent?.trim() || '' + }); + }); + + return codeBlocks; + } + + /** + * 提取媒体元素 + */ + private extractMediaElements(): MediaElement[] { + const mediaElements: MediaElement[] = []; + + // 视频元素 + document.querySelectorAll('video').forEach(video => { + mediaElements.push({ + type: 'video', + src: video.src, + duration: video.duration || undefined, + width: video.videoWidth, + height: video.videoHeight + }); + }); + + // 音频元素 + document.querySelectorAll('audio').forEach(audio => { + mediaElements.push({ + type: 'audio', + src: audio.src, + duration: audio.duration || undefined + }); + }); + + return mediaElements; + } + + /** + * 提取表格 + */ + private extractTables(): Table[] { + const tables: Table[] = []; + const tableElements = document.querySelectorAll('table'); + + tableElements.forEach(table => { + const headers: string[] = []; + const rows: string[][] = []; + + // 提取表头 + table.querySelectorAll('th').forEach(th => { + headers.push(th.textContent?.trim() || ''); + }); + + // 提取行数据 + table.querySelectorAll('tr').forEach(tr => { + const row: string[] = []; + tr.querySelectorAll('td').forEach(td => { + row.push(td.textContent?.trim() || ''); + }); + if (row.length > 0) rows.push(row); + }); + + if (headers.length > 0 || rows.length > 0) { + tables.push({ + headers, + rows, + caption: table.querySelector('caption')?.textContent?.trim() || undefined + }); + } + }); + + return tables; + } + + /** + * 辅助方法 + */ + private countWords(text: string): number { + return text.split(/\s+/).filter(word => word.length > 0).length; + } + + private detectCodeLanguage(code: Element): string | undefined { + const className = code.className; + if (className.includes('language-')) { + return className.split('language-')[1]?.split(' ')[0]; + } + return undefined; + } + + private analyzeCodeLanguages(codeBlocks: CodeBlock[]): Record { + const stats: Record = {}; + + codeBlocks.forEach(block => { + const lang = block.language || 'unknown'; + stats[lang] = (stats[lang] || 0) + 1; + }); + + return stats; + } + + private extractFirstParagraph(): string { + const firstP = document.querySelector('p'); + return firstP?.textContent?.trim().slice(0, 200) + '...' || ''; + } + + private generateContextId(): string { + return `${location.href}_${Date.now()}`; + } + + private shouldStoreRawDom(contentType: ContentType): boolean { + // 只在文章页面和未知页面存储原始DOM + return contentType === ContentType.ARTICLE || contentType === ContentType.UNKNOWN; + } + + // need update: 更新实现方案 + private async captureScreenshot(): Promise { + // 简化实现 - 实际应该使用chrome.tabs.captureVisibleTab + + return undefined + } +} \ No newline at end of file diff --git a/browser-extension/MineAssistant/src/content/index.ts b/browser-extension/MineAssistant/src/content/index.ts new file mode 100644 index 0000000..45cfde0 --- /dev/null +++ b/browser-extension/MineAssistant/src/content/index.ts @@ -0,0 +1,210 @@ +import { BrowserContextExtractor } from "./BrowserContextExtractor"; +import { BrowserContext, MessageTypeEnum } from "../types"; +import browser from 'webextension-polyfill'; + +/** + * 内容脚本主类 + * 负责与页面交互,捕获上下文内容 + */ +class ContentScript { + private extractor: BrowserContextExtractor; + private isActive: boolean = false; + private observer: MutationObserver | null = null; + + constructor() { + this.extractor = new BrowserContextExtractor(); + } + + /** + * 初始化内容脚本 + */ + async init() { + try { + await this.initMessageListener(); + // await this.initObserver(); + this.isActive = true; + console.log('MineAssistant Content Script initialized successfully'); + } catch (error) { + console.error('Failed to initialize content script:', error); + } + } + + /** + * 初始化消息监听器 + */ + private async initMessageListener() { + // 添加消息监听器 + browser.runtime.onMessage.addListener((message, sender, sendResponse) => { + this.handleMessage(message, sender, sendResponse); + return true; // 保持异步响应 + }); + } + + /** + * 处理接收到的消息 + */ + + private async handleMessage(message: any, sender: browser.Runtime.MessageSender, sendResponse: (response?: any) => void) { + console.log('Received message:', message); + try { + switch (message.type) { + case MessageTypeEnum.CAPTURE_CONTEXT: + const context = await this.captureContext(message.data?.mode || 'smart'); + sendResponse({ success: true, context }); + break; + case MessageTypeEnum.GET_STATE: + sendResponse({ success: true, active: this.isActive }); + break; + case MessageTypeEnum.ANALYZE_PAGE_TYPE: + const _context = await this.extractor.extractContext(); + sendResponse({ success: true, pageType: _context.metadata.contentType }); + break; + case MessageTypeEnum.GET_METADATA: + const metadata = await this.getMetadata(); + sendResponse({ success: true, metadata }); + break; + default: + sendResponse({ success: false, error: 'Unknown message type' }); + } + } catch (error) { + console.error('Error handling message:', error); + sendResponse({ success: false, error: error instanceof Error ? error.message : 'Unknown error' }); + } + } + + /** + * 初始化页面变化观察器 + * todo: 实现页面变化观察器的逻辑 + */ + private async initObserver() { + // 监听页面变化 + this.observer = new MutationObserver((mutations) => { + if (this.isActive) { + this.handlePageChange(mutations); + } + }); + + this.observer.observe(document.body, { + childList: true, + subtree: true, + characterData: true, + attributes: true + }); + } + + /** + * 处理页面变化 + */ + private async handlePageChange(mutations: MutationRecord[]) { + // 检查是否有实质性变化 + const hasSignificantChange = mutations.some(mutation => { + // 检查是否是内容变化 + if (mutation.type === 'characterData' || mutation.type === 'childList') { + return true; + } + + // 检查是否是重要属性变化 + if (mutation.type === 'attributes') { + const importantAttributes = ['title', 'src', 'href', 'content']; + return importantAttributes.includes(mutation.attributeName || ''); + } + + return false; + }); + + if (hasSignificantChange) { + console.log('Page content changed, notifying background script'); + + // 通知background script页面内容已更新 + browser.runtime.sendMessage({ + type: MessageTypeEnum.PAGE_CONTENT_UPDATED, + url: location.href, + title: document.title + }).catch(error => { + console.log('Failed to send page update notification:', error); + }); + } + } + + /** + * 捕获上下文 + */ + private async captureContext(mode: 'smart' | 'basic' = 'smart'): Promise { + const contentType = this.extractor.detectContentType(); + if (mode === 'basic') { + // 基础模式:只提取基本页面信息 + return { + id: `${location.href}_${Date.now()}`, + url: location.href, + title: document.title, + timestamp: Date.now(), + metadata: this.extractor.extractMetadata(contentType), + structuredContent: { + title: document.title, + contentType: contentType + } + }; + } + + // 智能模式:使用完整的内容提取器 + return await this.extractor.extractContext(); + } + + /** + * 提取内容 + */ + private async extractContent(options?: any): Promise { + const context = await this.extractor.extractContext(); + + // 根据选项过滤内容 + if (options?.contentType) { + return this.filterContentByType(context, options.contentType); + } + + return context; + } + + /** + * 获取页面元数据 + */ + private async getMetadata(): Promise { + const context = await this.extractor.extractContext(); + return context.metadata; + } + + /** + * 根据类型过滤内容 + */ + private filterContentByType(context: BrowserContext, contentType: string): any { + switch (contentType) { + case 'article': + return context.structuredContent.articleContent; + case 'code': + return context.structuredContent.codeContent; + case 'multimedia': + return context.structuredContent.multimediaContent; + case 'data': + return context.structuredContent.dataContent; + default: + return context; + } + } + + /** + * 清理资源 + */ + destroy() { + if (this.observer) { + this.observer.disconnect(); + this.observer = null; + } + this.isActive = false; + } +} + +// 初始化内容脚本 +const contentScript = new ContentScript(); +contentScript.init(); + +// 导出供测试使用 +export { ContentScript }; \ No newline at end of file diff --git a/browser-extension/MineAssistant/src/manifest.json b/browser-extension/MineAssistant/src/manifest.json new file mode 100644 index 0000000..cef340e --- /dev/null +++ b/browser-extension/MineAssistant/src/manifest.json @@ -0,0 +1,35 @@ +{ + "{{chrome}}.manifest_version": 3, + "{{firefox}}.manifest_version": 2, + "icons": { + "16": "icon/16.png", + "32": "icon/32.png", + "48": "icon/48.png", + "96": "icon/96.png", + "128": "icon/128.png" + }, + "{{chrome}}.action": { + "default_popup": "src/popup.html" + }, + "{{chrome}}.content_scripts": [ + { + "matches": [""], + "js": ["src/content/index.ts"] + } + ], + "{{firefox}}.browser_action": { + "default_popup": "src/popup.html" + }, + "background": { + "{{chrome}}.service_worker": "src/background.ts", + "{{firefox}}.scripts": ["src/background.ts"] + }, + "permissions": [ + "storage", + "tabs", + "activeTab" + ], + "host_permissions": [ + "" + ] +} diff --git a/browser-extension/MineAssistant/src/popup.css b/browser-extension/MineAssistant/src/popup.css new file mode 100644 index 0000000..c46874e --- /dev/null +++ b/browser-extension/MineAssistant/src/popup.css @@ -0,0 +1,232 @@ +.app { + padding: 16px; + min-height: 400px; + min-width: 240px; +} + +.app-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding-bottom: 12px; + border-bottom: 1px solid #e0e0e0; +} + +.app-header h1 { + font-size: 18px; + font-weight: 600; + color: #2c3e50; +} + +.status-indicator { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: #666; +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + transition: background-color 0.3s; +} + +.status-dot.active { + background-color: #4CAF50; + box-shadow: 0 0 4px rgba(76, 175, 80, 0.5); +} + +.status-dot.inactive { + background-color: #f44336; +} + +.stats { + background: white; + border-radius: 8px; + padding: 16px; + margin-bottom: 16px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.stat-item { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.stat-item:last-child { + margin-bottom: 0; +} + +.stat-label { + font-size: 12px; + color: #666; +} + +.stat-value { + font-size: 18px; + font-weight: 600; + color: #2c3e50; +} + +.stat-url { + font-size: 10px; + color: #666; + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.controls { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 16px; +} + +.btn { + padding: 10px 16px; + border: none; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s; + text-align: center; +} + +.btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.btn-start { + background-color: #4CAF50; + color: white; +} + +.btn-start:hover:not(:disabled) { + background-color: #45a049; +} + +.btn-stop { + background-color: #f44336; + color: white; +} + +.btn-stop:hover:not(:disabled) { + background-color: #da190b; +} + +.btn-secondary { + background-color: #2196F3; + color: white; +} + +.btn-secondary:hover:not(:disabled) { + background-color: #0b7dda; +} + +.btn-sync { + background-color: #FF9800; + color: white; +} + +.btn-sync:hover:not(:disabled) { + background-color: #e68900; +} + +.settings { + background: white; + border-radius: 8px; + padding: 16px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.settings h3 { + font-size: 14px; + font-weight: 600; + margin-bottom: 12px; + color: #2c3e50; +} + +.setting-item { + margin-bottom: 12px; +} + +.setting-item:last-child { + margin-bottom: 0; +} + +.setting-item label { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + cursor: pointer; +} + +.setting-item input[type="checkbox"] { + width: 16px; + height: 16px; +} + +.setting-item input[type="number"] { + width: 60px; + padding: 4px 8px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 12px; + margin-left: 8px; +} + +.setting-item input[type="text"] { + width: 120px; + padding: 4px 8px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 12px; + margin-left: 8px; +} + +.setting-item select { + width: 100px; + padding: 4px 8px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 12px; + margin-left: 8px; + background-color: white; +} + +.loading-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.8); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.loading-spinner { + width: 32px; + height: 32px; + border: 3px solid #f3f3f3; + border-top: 3px solid #4CAF50; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} \ No newline at end of file diff --git a/browser-extension/MineAssistant/src/popup.html b/browser-extension/MineAssistant/src/popup.html new file mode 100644 index 0000000..8b82cdb --- /dev/null +++ b/browser-extension/MineAssistant/src/popup.html @@ -0,0 +1,99 @@ + + + + + + + MineAssistant + + + + +

+
+

MineAssistant

+
+ + 已停止 +
+
+ +
+
+
+ 已捕获 + 0 +
+
+ 当前URL + +
+
+ +
+ + + + + +
+ +
+

设置

+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + +
+ + + + + \ No newline at end of file diff --git a/browser-extension/MineAssistant/src/popup.ts b/browser-extension/MineAssistant/src/popup.ts new file mode 100644 index 0000000..db67393 --- /dev/null +++ b/browser-extension/MineAssistant/src/popup.ts @@ -0,0 +1,43 @@ +import { PopupManager } from './popupManager'; + +let popupManager: PopupManager | null = null; + +console.log("log popupManager"); +/** + * 初始化popup应用 + */ +async function initializePopup(): Promise { + try { + // 创建popup管理器实例 + popupManager = new PopupManager(); + + // 等待初始化完成 + const waitForInit = (): Promise => { + return new Promise((resolve) => { + const checkReady = () => { + if (popupManager && popupManager.isReady()) { + resolve(); + } else { + setTimeout(checkReady, 100); + } + }; + checkReady(); + }); + }; + + await waitForInit(); + console.log('Popup application initialized successfully'); + } catch (error) { + console.error('Failed to initialize popup application:', error); + } +} + +// 页面加载完成后初始化 +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initializePopup); +} else { + initializePopup(); +} + +// 导出popup管理器供外部使用(如测试) +export { popupManager }; \ No newline at end of file diff --git a/browser-extension/MineAssistant/src/popupManager.ts b/browser-extension/MineAssistant/src/popupManager.ts new file mode 100644 index 0000000..7fc6de4 --- /dev/null +++ b/browser-extension/MineAssistant/src/popupManager.ts @@ -0,0 +1,435 @@ +import browser from 'webextension-polyfill'; +import { ExtensionState, ExtensionSettings } from './types'; + +// 默认设置 +const DEFAULT_SETTINGS: ExtensionSettings = { + autoCapture: true, + captureInterval: 30000, // 30秒 + maxContexts: 100, + syncEnabled: true, + backendUrl: 'http://localhost:1733' +}; + +// 默认状态 +const DEFAULT_STATE: ExtensionState = { + isActive: false, + currentUrl: '', + contextCount: 0, + settings: DEFAULT_SETTINGS +}; + +console.debug("[default state]", DEFAULT_STATE); + +export class PopupManager { + private state: ExtensionState = { ...DEFAULT_STATE }; + private isLoading = false; + private captureMode: 'smart' | 'basic' = 'smart'; + private isInitialized = false; + + constructor() { + this.initialize(); + } + + /** + * 初始化popup管理器 + */ + private async initialize(): Promise { + console.log("initialize popup manager", browser.storage); + try { + // 从存储中加载状态 + const result = await browser.storage.local.get(['extensionState']); + + const savedState = result.extensionState; + + if (savedState) { + this.state = { + ...DEFAULT_STATE, + ...savedState, + settings: { + ...DEFAULT_SETTINGS, + ...savedState.settings + } + }; + console.info("Loaded state:", this.state); + } else { + this.state = DEFAULT_STATE; + console.info("No saved state found, using default state", DEFAULT_STATE); + } + + this.isInitialized = true; + this.setupEventListeners(); + this.updateUI(); + } catch (error) { + console.error('Failed to initialize popup manager:', error); + this.state = DEFAULT_STATE; + this.isInitialized = true; + this.setupEventListeners(); + this.updateUI(); + } + } + + /** + * 设置事件监听器 + */ + private setupEventListeners(): void { + // 开始/停止捕获按钮 + const toggleButton = document.getElementById('toggle-capture-btn') as HTMLButtonElement; + if (toggleButton) { + toggleButton.addEventListener('click', this.handleToggleCapture.bind(this)); + } + + // 立即捕获按钮 + const captureButton = document.getElementById('capture-now-btn') as HTMLButtonElement; + if (captureButton) { + captureButton.addEventListener('click', this.handleCaptureNow.bind(this)); + } + + // 同步数据按钮 + const syncButton = document.getElementById('sync-data-btn') as HTMLButtonElement; + if (syncButton) { + syncButton.addEventListener('click', this.handleSync.bind(this)); + } + + // 自动捕获设置 + const autoCaptureCheckbox = document.getElementById('auto-capture-checkbox') as HTMLInputElement; + if (autoCaptureCheckbox) { + autoCaptureCheckbox.addEventListener('change', (e) => { + const target = e?.target as HTMLInputElement; + this.updateSettings({ autoCapture: target?.checked || false }); + + }); + } + + // 自动同步设置 + const syncEnabledCheckbox = document.getElementById('sync-enabled-checkbox') as HTMLInputElement; + if (syncEnabledCheckbox) { + syncEnabledCheckbox.addEventListener('change', (e) => { + const target = e?.target as HTMLInputElement; + this.updateSettings({ syncEnabled: target?.checked || false }); + }); + } + + // 捕获间隔设置 + const captureIntervalInput = document.getElementById('capture-interval-input') as HTMLInputElement; + if (captureIntervalInput) { + captureIntervalInput.addEventListener('change', (e) => { + const target = e?.target as HTMLInputElement; + const value = parseInt(target.value) || 30; + + this.updateSettings({ captureInterval: value * 1000 }); + }); + } + + // 最大存储数量设置 + const maxContextsInput = document.getElementById('max-contexts-input') as HTMLInputElement; + if (maxContextsInput) { + maxContextsInput.addEventListener('change', (e) => { + const target = e?.target as HTMLInputElement; + const value = parseInt(target.value) || 100; + this.updateSettings({ maxContexts: value }); + }); + } + + // 后端URL设置 + const backendUrlInput = document.getElementById('backend-url-input') as HTMLInputElement; + if (backendUrlInput) { + backendUrlInput.addEventListener('change', (e) => { + const target = e?.target as HTMLInputElement; + this.updateSettings({ backendUrl: target.value }); + }); + } + + // 捕获模式设置 + const captureModeSelect = document.getElementById('capture-mode-select') as HTMLSelectElement; + if (captureModeSelect) { + captureModeSelect.addEventListener('change', (e) => { + const target = e?.target as HTMLInputElement; + this.captureMode = target.value as 'smart' | 'basic'; + }); + } + + // 监听来自background script的消息 + browser.runtime.onMessage.addListener(this.handleMessage.bind(this)); + } + + /** + * 处理消息 + */ + private handleMessage(message: any, sender: browser.Runtime.MessageSender, sendResponse: (response?: any) => void): true | void | Promise { + if (message.type === 'STATE_UPDATE') { + this.state = message.data; + this.updateUI(); + sendResponse({ success: true }); + } + + if (message.type === 'CONTEXT_CAPTURED') { + this.state = { + ...this.state, + contextCount: this.state.contextCount + 1 + }; + this.updateUI(); + sendResponse({ success: true }); + } + } + + /** + * 保存状态到存储 + */ + private async saveState(): Promise { + try { + await browser.storage.local.set({ extensionState: this.state }); + } catch (error) { + console.error('Failed to save extension state:', error); + } + } + + /** + * 处理开始/停止捕获 + */ + private async handleToggleCapture(): Promise { + this.setLoading(true); + try { + const newState = { + ...this.state, + isActive: !this.state.isActive + }; + + await this.saveState(); + this.state = newState; + + // 通知background script + try { + await browser.runtime.sendMessage({ + type: 'TOGGLE_CAPTURE', + data: { isActive: this.state.isActive } + }); + } catch (error) { + console.error('Failed to send toggle capture message:', error); + } + + this.updateUI(); + } finally { + this.setLoading(false); + } + } + + /** + * 处理立即捕获 + */ + private async handleCaptureNow(): Promise { + console.info("Capturing context now..."); + + this.setLoading(true); + try { + // 获取当前活动标签页 + const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); + + if (tab && tab.id) { + // 发送捕获消息到content script + const screenshot = await browser.tabs.captureVisibleTab(); + const response = await browser.tabs.sendMessage(tab.id, { + type: 'CAPTURE_CONTEXT', + data: { mode: this.captureMode, screenshot } + }); + + console.log('response', response); + console.log('screenshot', screenshot) + + if (response && response.success) { + // 更新上下文计数和当前URL + this.state = { + ...this.state, + contextCount: this.state.contextCount + 1, + currentUrl: tab.url || '', + }; + await this.saveState(); + this.updateUI(); + } + } + } catch (error) { + console.error('Failed to capture context:', error); + } finally { + this.setLoading(false); + } + } + + /** + * 处理同步数据 + */ + private async handleSync(): Promise { + this.setLoading(true); + try { + // 获取所有存储的上下文数据 + const result = await browser.storage.local.get(['contexts']); + const contexts = result.contexts || []; + + if (contexts.length === 0) { + console.log('No data to sync'); + return; + } + + // 发送到后端API + const response = await fetch(`${this.state.settings.backendUrl}/api/contexts/sync`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ contexts }) + }); + + if (response.ok) { + // 更新状态 + this.state = { + ...this.state, + lastSyncTime: Date.now() + }; + await this.saveState(); + this.updateUI(); + } else { + console.error(`Sync failed: ${response.status} ${response.statusText}`); + } + } catch (error) { + console.error('Failed to sync data:', error); + } finally { + this.setLoading(false); + } + } + + /** + * 更新设置 + */ + private async updateSettings(newSettings: Partial): Promise { + const updatedSettings = { + ...this.state.settings, + ...newSettings + }; + + this.state = { + ...this.state, + settings: updatedSettings + }; + + await this.saveState(); + + // 通知background script设置已更新 + try { + await browser.runtime.sendMessage({ + type: 'UPDATE_SETTINGS', + data: { settings: updatedSettings } + }); + } catch (error) { + console.error('Failed to send settings update message:', error); + } + + this.updateUI(); + } + + /** + * 设置加载状态 + */ + private setLoading(loading: boolean): void { + this.isLoading = loading; + this.updateUI(); + } + + /** + * 更新UI + */ + private updateUI(): void { + // 更新状态指示器 + const statusDot = document.querySelector('.status-dot') as HTMLElement; + const statusText = document.querySelector('.status-text') as HTMLElement; + + if (statusDot && statusText) { + if (this.state.isActive) { + statusDot.classList.add('active'); + statusDot.classList.remove('inactive'); + statusText.textContent = '运行中'; + } else { + statusDot.classList.add('inactive'); + statusDot.classList.remove('active'); + statusText.textContent = '已停止'; + } + } + + // 更新统计信息 + const contextCountElement = document.querySelector('.stat-value') as HTMLElement; + if (contextCountElement) { + contextCountElement.textContent = this.state.contextCount.toString(); + } + + const currentUrlElement = document.querySelector('.stat-url') as HTMLElement; + if (currentUrlElement) { + currentUrlElement.textContent = this.state.currentUrl || '无'; + } + + // 更新控制按钮 + const toggleButton = document.getElementById('toggle-capture-btn') as HTMLButtonElement; + if (toggleButton) { + toggleButton.textContent = this.state.isActive ? '停止捕获' : '开始捕获'; + toggleButton.className = this.state.isActive ? 'btn btn-stop' : 'btn btn-start'; + toggleButton.disabled = this.isLoading; + } + + const captureButton = document.getElementById('capture-now-btn') as HTMLButtonElement; + if (captureButton) { + captureButton.disabled = this.isLoading || !this.state.isActive; + } + + const syncButton = document.getElementById('sync-data-btn') as HTMLButtonElement; + if (syncButton) { + syncButton.disabled = this.isLoading; + } + + // 更新设置输入框 + const autoCaptureCheckbox = document.getElementById('auto-capture-checkbox') as HTMLInputElement; + if (autoCaptureCheckbox) { + autoCaptureCheckbox.checked = this.state.settings.autoCapture; + } + + const syncEnabledCheckbox = document.getElementById('sync-enabled-checkbox') as HTMLInputElement; + if (syncEnabledCheckbox) { + syncEnabledCheckbox.checked = this.state.settings.syncEnabled; + } + + const captureIntervalInput = document.getElementById('capture-interval-input') as HTMLInputElement; + if (captureIntervalInput) { + captureIntervalInput.value = (this.state.settings.captureInterval / 1000).toString(); + } + + const maxContextsInput = document.getElementById('max-contexts-input') as HTMLInputElement; + if (maxContextsInput) { + maxContextsInput.value = this.state.settings.maxContexts.toString(); + } + + const backendUrlInput = document.getElementById('backend-url-input') as HTMLInputElement; + if (backendUrlInput) { + backendUrlInput.value = this.state.settings.backendUrl; + } + + const captureModeSelect = document.getElementById('capture-mode-select') as HTMLSelectElement; + if (captureModeSelect) { + captureModeSelect.value = this.captureMode; + } + + // 更新加载状态 + const loadingOverlay = document.querySelector('.loading-overlay') as HTMLElement; + if (loadingOverlay) { + loadingOverlay.style.display = this.isLoading ? 'flex' : 'none'; + } + } + + /** + * 获取当前状态 + */ + public getState(): ExtensionState { + return { ...this.state }; + } + + /** + * 获取是否初始化完成 + */ + public isReady(): boolean { + return this.isInitialized; + } +} \ No newline at end of file diff --git a/browser-extension/MineAssistant/src/types/browserContext.ts b/browser-extension/MineAssistant/src/types/browserContext.ts new file mode 100644 index 0000000..37b40a6 --- /dev/null +++ b/browser-extension/MineAssistant/src/types/browserContext.ts @@ -0,0 +1,165 @@ +export enum ContentType { + ARTICLE = 'article', + CODE = 'code', + MULTIMEDIA = 'multimedia', + DATA = 'data', + UNKNOWN = 'unknown' +} + +// 核心类型定义 +export interface BrowserContext { + id: string; + url: string; + title: string; + timestamp: number; + metadata: PageMetadata; + structuredContent: StructuredContent; + rawDom?: string; // 可选:存储body的innerHTML + screenshot?: string; // 可选:Base64缩略图 +} + +export interface PageMetadata { + domain: string; + language: string; + description: string; + keywords: string[]; + author: string; + publishedTime?: string; + modifiedTime?: string; + wordCount: number; + contentType: ContentType; + // 从meta标签提取的关键信息 + metaTags: Record; +} + +// 通用结构化内容 +export interface StructuredContent { + title: string; + summary?: string; + contentType: ContentType; + + // 按类型分化的内容(互斥) + articleContent?: ArticleContent; + codeContent?: CodeContent; + multimediaContent?: MultimediaContent; + dataContent?: DataContent; +} + +// 文章类型内容 +export interface ArticleContent { + headings: Heading[]; + paragraphs: Paragraph[]; + images: Image[]; +} + +// 代码类型内容 +export interface CodeContent { + codeBlocks: CodeBlock[]; + languageStats: Record; + fileStructure: FileStructure[]; +} + +// 多媒体类型内容 +export interface MultimediaContent { + mediaElements: MediaElement[]; + duration?: number; + transcript?: string; +} + +// 数据表格类型内容 +export interface DataContent { + tables: Table[]; + charts: Chart[]; + dataPoints: DataPoint[]; +} + +export interface Heading { + level: number; + text: string; + id?: string; +} + +export interface Paragraph { + text: string; + wordCount: number; +} + +export interface Link { + text: string; + href: string; + title?: string; +} + +export interface Image { + src: string; + alt: string; + title?: string; +} + +export interface Table { + headers: string[]; + rows: string[][]; + caption?: string; +} + +export interface List { + type: 'ordered' | 'unordered'; + items: string[]; +} + +export interface CodeBlock { + language?: string; + code: string; +} + +export interface FileStructure { + type: string; + name: string; + path?: string; +} + +export interface MediaElement { + type: 'video' | 'audio' | 'canvas'; + src?: string; + duration?: number; + width?: number; + height?: number; +} + +export interface Chart { + type: string; + data: any; + title?: string; +} + +export interface DataPoint { + type: string; + value: number; + unit?: string; +} + +// 扩展状态类型 +export interface ExtensionState { + isActive: boolean; + currentUrl: string; + contextCount: number; + lastSyncTime?: number; + settings: ExtensionSettings; +} + +export interface ExtensionSettings { + autoCapture: boolean; + captureInterval: number; + maxContexts: number; + syncEnabled: boolean; + backendUrl: string; +} + + + +// 存储类型 +export interface StorageData { + contexts: BrowserContext[]; + settings: ExtensionSettings; + state: ExtensionState; +} \ No newline at end of file diff --git a/browser-extension/MineAssistant/src/types/index.ts b/browser-extension/MineAssistant/src/types/index.ts new file mode 100644 index 0000000..4dba7aa --- /dev/null +++ b/browser-extension/MineAssistant/src/types/index.ts @@ -0,0 +1,3 @@ +export * from "./browserContext" +// export * from "./payload" +export * from "./message" \ No newline at end of file diff --git a/browser-extension/MineAssistant/src/types/message.ts b/browser-extension/MineAssistant/src/types/message.ts new file mode 100644 index 0000000..b594d6f --- /dev/null +++ b/browser-extension/MineAssistant/src/types/message.ts @@ -0,0 +1,76 @@ +// 消息类型枚举 +export enum MessageTypeEnum { + // 捕获相关 + CAPTURE_CONTEXT = 'CAPTURE_CONTEXT', + PAGE_CONTENT_UPDATED = 'PAGE_CONTENT_UPDATED', + + // 截图实现放在 popup 即可, 需要考虑实现 + CAPTURE_SCREENSHOT = 'CAPTURE_SCREENSHOT', + + GET_STATE = 'GET_STATE', + + ANALYZE_PAGE_TYPE = 'ANALYZE_PAGE_TYPE', + + GET_METADATA = 'GET_METADATA' +} + +export type MessageType = keyof typeof MessageTypeEnum; + +// 消息数据接口 +export interface MessageData { + mode?: CaptureMode; + isActive?: boolean; + settings?: any; + contexts?: any[]; + url?: string; + content?: any; + [key: string]: any; +} + + + +// 统一消息接口 +export interface Message { + type: MessageType; + data: MessageData; +} + +// 消息响应接口 +export interface MessageResponse { + success: boolean; + data?: any; + error?: string; +} + +// 消息目标类型 +export enum MessageTarget { + BACKGROUND = 'background', + CONTENT_SCRIPT = 'content_script', + POPUP = 'popup', + ACTIVE_TAB = 'ACTIVE_TAB', + ALL_TABS = 'ALL_TABS' +} + +// 消息传递选项 +export interface MessageOptions { + target: MessageTarget; + tabId?: number; + timeout?: number; +} + +// 捕获模式类型 +export type CaptureMode = 'smart' | 'basic'; + +// 捕获上下文数据 +export interface CaptureContext { + url: string; + title: string; + content: string; + mode: CaptureMode; + timestamp: number; + metadata?: { + wordCount?: number; + elementCount?: number; + imageCount?: number; + }; +} \ No newline at end of file diff --git a/browser-extension/MineAssistant/src/vite-env.d.ts b/browser-extension/MineAssistant/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/browser-extension/MineAssistant/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/browser-extension/MineAssistant/tsconfig.json b/browser-extension/MineAssistant/tsconfig.json new file mode 100644 index 0000000..f0074d3 --- /dev/null +++ b/browser-extension/MineAssistant/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "strict": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "skipLibCheck": true + }, + "include": ["src/*"] +} diff --git a/browser-extension/MineAssistant/vite.config.ts b/browser-extension/MineAssistant/vite.config.ts new file mode 100644 index 0000000..20a07fa --- /dev/null +++ b/browser-extension/MineAssistant/vite.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from "vite"; +import webExtension, { readJsonFile } from "vite-plugin-web-extension"; + +function generateManifest() { + const manifest = readJsonFile("src/manifest.json"); + const pkg = readJsonFile("package.json"); + console.log("manifest", manifest); + return { + name: pkg.name, + description: pkg.description, + version: pkg.version, + ...manifest, + }; +} + +export default defineConfig({ + plugins: [ + webExtension({ + manifest: generateManifest, + watchFilePaths: ["package.json", "manifest.json"], + }), + ], +}); diff --git a/browser-extension/readme.md b/browser-extension/readme.md new file mode 100644 index 0000000..0229ffb --- /dev/null +++ b/browser-extension/readme.md @@ -0,0 +1,156 @@ + +# MineCapture 方案 +MineContext 当前阶段对于浏览器的信息采集主要是基于 screenshot + vlm 分析. +这种方式虽然通用,但存在明显问题: +1. 信息密度低且噪音大:截图包含大量冗余 UI 元素、无关内容例如网页footer, sidebar 广告 等等无关内容,这些内容不需要捕获,但截屏分析一股脑装进来,即增加了存储成本,也增加了模型理解成本,加大 token 消耗. +2. 上下文理解依赖强视觉模型:准确提取文本、理解结构(如表格、列表)成本高, token 消耗非常大. +3. 无法进行深度交互: 仅能"看", 不能"操作". + +而基于 browser-use 的浏览器插件开发设计希望做到 + +* 精准数据采集:可直接获取网页的纯净HTML、结构化数据(JSON-LD)、文本内容, (与截图 + vlm 模型识别 方案对比, 优势: 数据精确+低成本) +* 理解页面结构与语义:通过DOM树,能天然理解元素的层次、角色(按钮、输入框)和交互逻辑。 +* 实现主动服务闭环:不仅能“记录”你看了什么,未来还能“替你做”。例如,记录到你反复查看某航班比价页面,未来可授权它自动执行比价查询并推送结果,实现真正的“伙伴”级主动服务。 + + +## 架构设计 + +核心思想如下图所示: + +```mermaid +flowchart TD + subgraph A[浏览器内部 - MineContext扩展] + B[“内容脚本
(Content Script)”] + C[“扩展后台服务
(Background Service)”] + end + + subgraph D[用户本地计算机] + E[“MineContext 主应用
(数据存储/主逻辑)”] + F[“browser-use 服务
(Playwright + LLM)”] + G[“MCP协议网关
(协调通信)”] + end + + B -- “1. 注入脚本,监听导航与DOM变化” --> C + C -- “2. 封装结构化上下文数据” --> G + G -- “3. 路由请求” --> E + G -- “4a. 若需深度分析/自动化” --> F + F -- “4b. 执行深度解析或操作” --> H[外部浏览器实例] + H -- “返回操作结果/提取内容” --> F + F -- “5. 返回增强的上下文 List[ProcessedContext]” --> G + E & G -- “6. 存储与推送 RawContext --> E +``` + +在这个架构中,各组件分工如下: + +1. 浏览器扩展层:负责无感知的初始采集。 + 1. 通过内容脚本监听页面导航、DOM变化,轻量级地捕获URL、标题、主要文本和用户交互事件。 + 2. 当需要更深度的分析(如整个页面的知识图谱提取)或执行任务(如“帮我保存这篇文章”)时,它将请求通过MCP协议转发给本地服务。 +2. 本地 browser-use 服务层。 + 1. 收到扩展的请求后,可启动一个无头浏览器实例,精准导航到相同页面,利用其AI能力执行复杂的解析、总结、问答或自动化操作。 +3. MineContext 主应用:作为数据中枢与呈现层。接收来自扩展的原始上下文和来自 browser-use 服务的增强型分析结果,进行统一存储、索引和管理。并基于这些丰富的上下文,生成高质量的日报、周报、待办事项和主动洞察。 + +## 实现方案 + +1. 基础框架: + 1. 浏览器扩展: + 其内容脚本使用 MutationObserver 监听页面主要内容区域的变化 + 2. opencontext: + 负责与本地 MCP服务器 建立 WebSocket 长连接。数据格式设计为: + ```json + { + "type": "page_context", + "url": "...", + "title": "...", + "timestamp": "...", + "simple_text": "...", + "action": "summarize" // 或 "extract_links", "save_to_notion" + } + ``` + +2. 集成 browser-use 作为 MCP 服务器: + · 这正是 mcp-browser-use 项目()所解决的问题。你可以直接使用或参考它,将 browser-use 的能力(导航、点击、提取、执行JS)封装成标准的MCP工具。 + · 在MineContext本地主应用中,集成这个MCP客户端。当扩展发来一个 {"action": "summarize"} 的请求时,主应用通过MCP协议调用 browser-use 工具,对目标URL进行深度访问和总结。 +3. 实现上下文增强与任务自动化: + · 增强采集:对于重要的文章页或文档页,扩展可触发 browser-use 进行深度解析,提取核心论点、参考文献、数据表格等,形成远超截图的优质上下文。 + · 任务自动化:用户可以通过与MineContext的对话界面(如“帮我把这篇文章的要点保存到我的笔记里”),触发一个由 browser-use 执行的自动化工作流:导航到文章页->提取要点->打开笔记网站->填写并提交。这实现了从“记录”到“执行”的跨越。 + +四、 挑战与前瞻 + +1. 性能与复杂度:为每个标签页维持完整的 browser-use 实例开销巨大。必须采用按需启动、池化管理、任务队列等策略,确保资源占用可控。 +2. 权限与安全:浏览器扩展权限与本地自动化能力的结合,力量强大也意味着风险。必须设计明确的用户授权机制(例如“是否允许MineContext自动为您填写表单?”),并且所有操作都应可审计、可撤销。 +3. 与 MineContext 开源路线的协同:MineContext的底层框架未来将开源为 OpenContext。基于 browser-use 的扩展开发,完全可以遵循OpenContext的架构规范,使其成为一个专注于“浏览器上下文捕获与交互”的插件模块,贡献到其开源生态中。 + +结论:将 browser-use 作为 MineContext 浏览器扩展开发的基点,不仅在技术上完全可行,而且是实现其“主动式上下文感知伙伴”愿景的强力助推器。它能解决当前截图方案的诸多痛点,提供结构化、高质量的数据输入,并首次为 MineContext 赋予了在浏览器环境中“动手操作”的能力,为未来实现真正智能的人机异步协作奠定了坚实的技术基础。如果你作为开发者启动这个项目,建议从实现一个基于MCP协议的、能够执行简单页面总结和内容保存的浏览器扩展原型开始。 + + + + +本次讨论聚焦在 MineContext 对 浏览器上下文 信息提取, 处理, 工作流执行的方向. + +## 问题 + +MineContext 当前阶段对于浏览器的信息处理主要是基于 screenshot 截屏采集 + vlm 视觉模型分析识别. + +这种方式通用,但存在明显弊端: + +1. 信息密度低且噪音大:截图包含大量冗余 UI 元素、无关内容例如网页 footer, sidebar 广告等无关内容, 这些内容不需要捕获, 但截屏分析不会考虑这样. 即增加了存储成本, 也增加了模型理解成本, 加大非必要的 token 消耗. + +2. 上下文理解依赖强视觉模型:准确提取文本、理解结构(如表格、列表)成本高, 尤其是在某些特定 case , 截屏会非常大. 例如当表格或者文档在页面展示很长, 或者无线下拉滚动加载新内容时, 屏幕截图会非常大. 参考 case 1 和 case 2 截屏 pdf + +[case1-news.pdf](https://github.com/user-attachments/files/23849832/case1-news.pdf) +[case2-bilibili.pdf](https://github.com/user-attachments/files/23849844/case2-bilibili.pdf) + + +3. 目前 MineContext 暂时无法对浏览器进行深度交互: 仅能"看", 不能"操作". + + +## 解决方案: AI 浏览器自动化工具 + +说到 AI 浏览器自动化工具, 最著名的就是 browser-use + +如果基于 browser-use 开发,可以看到的明显优势有: +1. 精准数据采集:可直接获取网页的纯净HTML、结构化数据(JSON-LD)、文本内容, (与截图 + vlm 模型识别 方案对比, 优势: 数据精确+低成本) +2. 准确理解页面结构与语义:通过DOM树,能天然理解元素的层次、角色(按钮、输入框)和交互逻辑。 +3. 实现主动服务闭环:不仅能“记录”你看了什么,未来还能“替你做”。例如,记录到你反复查看某航班比价页面,未来可授权它自动执行比价查询并推送结果,实现真正的“伙伴”级主动服务。 + +```mermaid +flowchart TD + subgraph A[浏览器内部 - MineContext扩展] + B[“内容脚本
(Content Script)”] + C[“扩展后台服务
(Background Service)”] + end + + subgraph D[用户本地计算机] + E[“MineContext 主应用
(数据存储/主逻辑)”] + F[“browser-use 服务
(Playwright + LLM)”] + G[“MCP协议网关
(协调通信)”] + end + + B -- “1. 注入脚本,监听导航与DOM变化” --> C + C -- “2. 封装结构化上下文数据” --> G + G -- “3. 路由请求” --> E + G -- “4a. 若需深度分析/自动化” --> F + F -- “4b. 执行深度解析或操作” --> H[外部浏览器实例] + H -- “返回操作结果/提取内容” --> F + F -- “5. 返回增强的上下文 List[ProcessedContext]” --> G + E & G -- “6. 存储与推送 RawContext --> E +``` + +### 竞品方案对比 + + +|方案名称 | 技术路线 / 核心特点 | 优势 | 主要劣势 / 适用场景| +| ---- | ---- |---- |---- | +| 基于MCP的成熟方案 | +| ---- | ---- |---- |---- | +|Browser Use | 技术路线 / 核心特点 | 优势 | 主要劣势 / 适用场景| +|Stagehand | 技术路线 / 核心特点 | 优势 | 主要劣势 / 适用场景| +|其他技术路线方案 | +| Skyvern | 单元格 | +| Midscene.js | 单元格 | +| 传统自动化框架 如 Playwright| + + +ExtractedData 提取 对比 + + diff --git a/frontend/electron.vite.config.1764942152772.mjs b/frontend/electron.vite.config.1764942152772.mjs new file mode 100644 index 0000000..bf16ec9 --- /dev/null +++ b/frontend/electron.vite.config.1764942152772.mjs @@ -0,0 +1,81 @@ +// electron.vite.config.ts +import react from "@vitejs/plugin-react-swc"; +import { CodeInspectorPlugin } from "code-inspector-plugin"; +import { resolve } from "path"; +import { defineConfig, externalizeDepsPlugin } from "electron-vite"; +import { visualizer } from "rollup-plugin-visualizer"; +import tailwindcss from "@tailwindcss/vite"; +var visualizerPlugin = (type) => { + return process.env[`VISUALIZER_${type.toUpperCase()}`] ? [visualizer({ open: true })] : []; +}; +var isDev = process.env.NODE_ENV === "development"; +var electron_vite_config_default = defineConfig({ + main: { + plugins: [externalizeDepsPlugin()], + resolve: { + alias: { + "@main": resolve("src/main"), + "@types": resolve("src/renderer/src/types"), + "@shared": resolve("packages/shared"), + "@logger": resolve("src/main/services/LoggerService") + } + } + }, + preload: { + plugins: [ + react({ + tsDecorators: true + }), + externalizeDepsPlugin() + ], + resolve: { + alias: { + "@shared": resolve("packages/shared"), + "@types": resolve("src/renderer/src/types") + } + }, + build: { + sourcemap: isDev + } + }, + renderer: { + resolve: { + alias: { + "@renderer": resolve("src/renderer/src"), + "@shared": resolve("packages/shared"), + "@logger": resolve("src/renderer/src/services/LoggerService"), + "@types": resolve("src/renderer/src/types") + } + }, + css: { + preprocessorOptions: { + less: { + javascriptEnabled: true + } + } + }, + plugins: [ + tailwindcss(), + react({}), + ...isDev ? [CodeInspectorPlugin({ bundler: "vite" })] : [], + // 只在开发环境下启用 CodeInspectorPlugin + ...visualizerPlugin("renderer"), + { + name: "force-arco-adapter-side-effect", + transform(code, id) { + if (id.includes("react-19-adapter")) { + return { + code, + map: null, + moduleSideEffects: true + }; + } + return null; + } + } + ] + } +}); +export { + electron_vite_config_default as default +}; diff --git a/package-lock.json b/package-lock.json index 2c31edc..215e504 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "fix_24c7e1db-e102-46cb-8db2-96f0f3dc9ae6_110_bz9u4ywd", + "name": "MineContext", "lockfileVersion": 3, "requires": true, "packages": { From 0b5cc7774dd4a92b7abcf186f23c585f8f41325e Mon Sep 17 00:00:00 2001 From: down <37741552+Facefall@users.noreply.github.com> Date: Sun, 7 Dec 2025 13:32:05 +0800 Subject: [PATCH 2/6] feat: add timeout todo --- .../MineAssistant/src/content/index.ts | 15 ++++++++++++++- .../MineAssistant/src/popupManager.ts | 11 ++++++----- .../MineAssistant/src/types/message.ts | 4 +++- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/browser-extension/MineAssistant/src/content/index.ts b/browser-extension/MineAssistant/src/content/index.ts index 45cfde0..fb9ba45 100644 --- a/browser-extension/MineAssistant/src/content/index.ts +++ b/browser-extension/MineAssistant/src/content/index.ts @@ -1,5 +1,5 @@ import { BrowserContextExtractor } from "./BrowserContextExtractor"; -import { BrowserContext, MessageTypeEnum } from "../types"; +import { BrowserContext, ExtensionState, MessageTypeEnum } from "../types"; import browser from 'webextension-polyfill'; /** @@ -63,6 +63,10 @@ class ContentScript { const metadata = await this.getMetadata(); sendResponse({ success: true, metadata }); break; + case MessageTypeEnum.UPDATE_STATE: + await this.updateState(message); + sendResponse({ success: true }); + break; default: sendResponse({ success: false, error: 'Unknown message type' }); } @@ -72,6 +76,15 @@ class ContentScript { } } + private async updateState(state: ExtensionState) { + setTimeout(() => { + browser.runtime.sendMessage({ + type: MessageTypeEnum.UPDATE_STATE, + data: { 11: 11, ...state } + }); + }, 5000); + } + /** * 初始化页面变化观察器 * todo: 实现页面变化观察器的逻辑 diff --git a/browser-extension/MineAssistant/src/popupManager.ts b/browser-extension/MineAssistant/src/popupManager.ts index 7fc6de4..b6c3a69 100644 --- a/browser-extension/MineAssistant/src/popupManager.ts +++ b/browser-extension/MineAssistant/src/popupManager.ts @@ -1,5 +1,5 @@ import browser from 'webextension-polyfill'; -import { ExtensionState, ExtensionSettings } from './types'; +import { ExtensionState, ExtensionSettings, MessageTypeEnum } from './types'; // 默认设置 const DEFAULT_SETTINGS: ExtensionSettings = { @@ -156,10 +156,11 @@ export class PopupManager { * 处理消息 */ private handleMessage(message: any, sender: browser.Runtime.MessageSender, sendResponse: (response?: any) => void): true | void | Promise { - if (message.type === 'STATE_UPDATE') { + console.log('Received message from background:', message); + if (message.type === MessageTypeEnum.UPDATE_STATE) { this.state = message.data; - this.updateUI(); - sendResponse({ success: true }); + // TODO: + console.log("定时保存 Context 提醒") } if (message.type === 'CONTEXT_CAPTURED') { @@ -314,7 +315,7 @@ export class PopupManager { // 通知background script设置已更新 try { await browser.runtime.sendMessage({ - type: 'UPDATE_SETTINGS', + type: MessageTypeEnum.UPDATE_STATE, data: { settings: updatedSettings } }); } catch (error) { diff --git a/browser-extension/MineAssistant/src/types/message.ts b/browser-extension/MineAssistant/src/types/message.ts index b594d6f..9e3cc76 100644 --- a/browser-extension/MineAssistant/src/types/message.ts +++ b/browser-extension/MineAssistant/src/types/message.ts @@ -11,7 +11,9 @@ export enum MessageTypeEnum { ANALYZE_PAGE_TYPE = 'ANALYZE_PAGE_TYPE', - GET_METADATA = 'GET_METADATA' + GET_METADATA = 'GET_METADATA', + + UPDATE_STATE = 'UPDATE_SETTINGS', } export type MessageType = keyof typeof MessageTypeEnum; From d19eb8c8cd0a9222d3f7813d1723df2015f21fe8 Mon Sep 17 00:00:00 2001 From: down <37741552+Facefall@users.noreply.github.com> Date: Sun, 7 Dec 2025 20:05:20 +0800 Subject: [PATCH 3/6] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=E8=AE=BE=E8=AE=A1;=E6=96=B0=E5=A2=9E=20storage=20?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E5=AE=9E=E7=8E=B0,=E5=8C=85=E6=8B=AC=20setti?= =?UTF-8?q?ngs=20=E5=92=8C=20context=20=E5=AD=98=E5=82=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MineAssistant/src/background.ts | 5 - .../MineAssistant/src/background/index.ts | 223 ++++++++++++++++ .../MineAssistant/src/constants/api.ts | 6 + .../MineAssistant/src/constants/index.ts | 3 + .../MineAssistant/src/constants/settings.ts | 19 ++ .../MineAssistant/src/constants/storage.ts | 14 + .../contentManager.ts} | 23 +- .../MineAssistant/src/content/index.ts | 247 +++--------------- .../MineAssistant/src/manifest.json | 8 +- browser-extension/MineAssistant/src/popup.ts | 43 --- .../src/{popup.css => popup/index.css} | 0 .../src/{popup.html => popup/index.html} | 4 +- .../MineAssistant/src/services/api.ts | 0 .../MineAssistant/src/services/index.ts | 0 .../MineAssistant/src/services/screenshot.ts | 0 .../MineAssistant/src/services/sync/index.ts | 1 + .../MineAssistant/src/storage/context.ts | 216 +++++++++++++++ .../MineAssistant/src/storage/index.ts | 64 +++++ .../MineAssistant/src/storage/settings.ts | 98 +++++++ .../MineAssistant/src/types/browserContext.ts | 26 -- .../MineAssistant/src/types/index.ts | 4 +- .../MineAssistant/src/types/storage.ts | 26 ++ 22 files changed, 714 insertions(+), 316 deletions(-) delete mode 100644 browser-extension/MineAssistant/src/background.ts create mode 100644 browser-extension/MineAssistant/src/background/index.ts create mode 100644 browser-extension/MineAssistant/src/constants/api.ts create mode 100644 browser-extension/MineAssistant/src/constants/index.ts create mode 100644 browser-extension/MineAssistant/src/constants/settings.ts create mode 100644 browser-extension/MineAssistant/src/constants/storage.ts rename browser-extension/MineAssistant/src/{popupManager.ts => content/contentManager.ts} (97%) delete mode 100644 browser-extension/MineAssistant/src/popup.ts rename browser-extension/MineAssistant/src/{popup.css => popup/index.css} (100%) rename browser-extension/MineAssistant/src/{popup.html => popup/index.html} (96%) create mode 100644 browser-extension/MineAssistant/src/services/api.ts create mode 100644 browser-extension/MineAssistant/src/services/index.ts create mode 100644 browser-extension/MineAssistant/src/services/screenshot.ts create mode 100644 browser-extension/MineAssistant/src/services/sync/index.ts create mode 100644 browser-extension/MineAssistant/src/storage/context.ts create mode 100644 browser-extension/MineAssistant/src/storage/index.ts create mode 100644 browser-extension/MineAssistant/src/storage/settings.ts create mode 100644 browser-extension/MineAssistant/src/types/storage.ts diff --git a/browser-extension/MineAssistant/src/background.ts b/browser-extension/MineAssistant/src/background.ts deleted file mode 100644 index 7a49d48..0000000 --- a/browser-extension/MineAssistant/src/background.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { ContentScript } from './content'; - -console.log('ContentScript loaded') - -export default ContentScript \ No newline at end of file diff --git a/browser-extension/MineAssistant/src/background/index.ts b/browser-extension/MineAssistant/src/background/index.ts new file mode 100644 index 0000000..fbc948d --- /dev/null +++ b/browser-extension/MineAssistant/src/background/index.ts @@ -0,0 +1,223 @@ +import { BrowserContextExtractor } from "../content/BrowserContextExtractor"; +import { BrowserContext, ExtensionState, MessageTypeEnum } from "../types"; +import browser from 'webextension-polyfill'; + +/** + * 内容脚本主类 + * 负责与页面交互,捕获上下文内容 + */ +class BackgroundScript { + private extractor: BrowserContextExtractor; + private isActive: boolean = false; + private observer: MutationObserver | null = null; + + constructor() { + this.extractor = new BrowserContextExtractor(); + } + + /** + * 初始化内容脚本 + */ + async init() { + try { + await this.initMessageListener(); + // await this.initObserver(); + this.isActive = true; + console.log('MineAssistant Content Script initialized successfully'); + } catch (error) { + console.error('Failed to initialize content script:', error); + } + } + + /** + * 初始化消息监听器 + */ + private async initMessageListener() { + // 添加消息监听器 + browser.runtime.onMessage.addListener((message, sender, sendResponse) => { + this.handleMessage(message, sender, sendResponse); + return true; // 保持异步响应 + }); + } + + /** + * 处理接收到的消息 + */ + + private async handleMessage(message: any, sender: browser.Runtime.MessageSender, sendResponse: (response?: any) => void) { + console.log('Received message:', message); + try { + switch (message.type) { + case MessageTypeEnum.CAPTURE_CONTEXT: + const context = await this.captureContext(message.data?.mode || 'smart'); + sendResponse({ success: true, context }); + break; + case MessageTypeEnum.GET_STATE: + sendResponse({ success: true, active: this.isActive }); + break; + case MessageTypeEnum.ANALYZE_PAGE_TYPE: + const _context = await this.extractor.extractContext(); + sendResponse({ success: true, pageType: _context.metadata.contentType }); + break; + case MessageTypeEnum.GET_METADATA: + const metadata = await this.getMetadata(); + sendResponse({ success: true, metadata }); + break; + case MessageTypeEnum.UPDATE_STATE: + await this.updateState(message); + sendResponse({ success: true }); + break; + default: + sendResponse({ success: false, error: 'Unknown message type' }); + } + } catch (error) { + console.error('Error handling message:', error); + sendResponse({ success: false, error: error instanceof Error ? error.message : 'Unknown error' }); + } + } + + private async updateState(state: ExtensionState) { + setTimeout(() => { + browser.runtime.sendMessage({ + type: MessageTypeEnum.UPDATE_STATE, + data: { 11: 11, ...state } + }); + }, 5000); + } + + /** + * 初始化页面变化观察器 + * todo: 实现页面变化观察器的逻辑 + */ + private async initObserver() { + // 监听页面变化 + this.observer = new MutationObserver((mutations) => { + if (this.isActive) { + this.handlePageChange(mutations); + } + }); + + this.observer.observe(document.body, { + childList: true, + subtree: true, + characterData: true, + attributes: true + }); + } + + /** + * 处理页面变化 + */ + private async handlePageChange(mutations: MutationRecord[]) { + // 检查是否有实质性变化 + const hasSignificantChange = mutations.some(mutation => { + // 检查是否是内容变化 + if (mutation.type === 'characterData' || mutation.type === 'childList') { + return true; + } + + // 检查是否是重要属性变化 + if (mutation.type === 'attributes') { + const importantAttributes = ['title', 'src', 'href', 'content']; + return importantAttributes.includes(mutation.attributeName || ''); + } + + return false; + }); + + if (hasSignificantChange) { + console.log('Page content changed, notifying background script'); + + // 通知background script页面内容已更新 + browser.runtime.sendMessage({ + type: MessageTypeEnum.PAGE_CONTENT_UPDATED, + url: location.href, + title: document.title + }).catch(error => { + console.log('Failed to send page update notification:', error); + }); + } + } + + /** + * 捕获上下文 + */ + private async captureContext(mode: 'smart' | 'basic' = 'smart'): Promise { + const contentType = this.extractor.detectContentType(); + if (mode === 'basic') { + // 基础模式:只提取基本页面信息 + return { + id: `${location.href}_${Date.now()}`, + url: location.href, + title: document.title, + timestamp: Date.now(), + metadata: this.extractor.extractMetadata(contentType), + structuredContent: { + title: document.title, + contentType: contentType + } + }; + } + + // 智能模式:使用完整的内容提取器 + return await this.extractor.extractContext(); + } + + /** + * 提取内容 + */ + private async extractContent(options?: any): Promise { + const context = await this.extractor.extractContext(); + + // 根据选项过滤内容 + if (options?.contentType) { + return this.filterContentByType(context, options.contentType); + } + + return context; + } + + /** + * 获取页面元数据 + */ + private async getMetadata(): Promise { + const context = await this.extractor.extractContext(); + return context.metadata; + } + + /** + * 根据类型过滤内容 + */ + private filterContentByType(context: BrowserContext, contentType: string): any { + switch (contentType) { + case 'article': + return context.structuredContent.articleContent; + case 'code': + return context.structuredContent.codeContent; + case 'multimedia': + return context.structuredContent.multimediaContent; + case 'data': + return context.structuredContent.dataContent; + default: + return context; + } + } + + /** + * 清理资源 + */ + destroy() { + if (this.observer) { + this.observer.disconnect(); + this.observer = null; + } + this.isActive = false; + } +} + +// 初始化内容脚本 +const contentScript = new BackgroundScript(); +contentScript.init(); + +// 导出供测试使用 +export { BackgroundScript }; \ No newline at end of file diff --git a/browser-extension/MineAssistant/src/constants/api.ts b/browser-extension/MineAssistant/src/constants/api.ts new file mode 100644 index 0000000..22ef6d6 --- /dev/null +++ b/browser-extension/MineAssistant/src/constants/api.ts @@ -0,0 +1,6 @@ +export const API_ENDPOINT = { + SYNC_CONTEXTS: '/api/contexts/sync', + HEALTH_CHECK: '/api/health' +} + +export const API_TIMEOUT = 30000; // 30秒 diff --git a/browser-extension/MineAssistant/src/constants/index.ts b/browser-extension/MineAssistant/src/constants/index.ts new file mode 100644 index 0000000..94c8f17 --- /dev/null +++ b/browser-extension/MineAssistant/src/constants/index.ts @@ -0,0 +1,3 @@ +export * from "./settings" +export * from "./storage" +export * from "./api" \ No newline at end of file diff --git a/browser-extension/MineAssistant/src/constants/settings.ts b/browser-extension/MineAssistant/src/constants/settings.ts new file mode 100644 index 0000000..e4dba5d --- /dev/null +++ b/browser-extension/MineAssistant/src/constants/settings.ts @@ -0,0 +1,19 @@ +import { ExtensionSettings, ExtensionState } from "../types"; + + +// 默认设置 +export const DEFAULT_SETTINGS: ExtensionSettings = { + autoCapture: true, + captureInterval: 30000, // 30秒 + maxContexts: 100, + syncEnabled: true, + backendUrl: 'http://localhost:1733' +} as const; + +// 默认状态 +export const DEFAULT_STATE: ExtensionState = { + isActive: false, + currentUrl: '', + contextCount: 0, + settings: DEFAULT_SETTINGS +} as const; diff --git a/browser-extension/MineAssistant/src/constants/storage.ts b/browser-extension/MineAssistant/src/constants/storage.ts new file mode 100644 index 0000000..a4fe5c1 --- /dev/null +++ b/browser-extension/MineAssistant/src/constants/storage.ts @@ -0,0 +1,14 @@ +export const STORAGE_KEYS = { + SETTINGS: 'mineAssistant_settings', + CONTEXTS: 'mineAssistant_contexts', + SYNC_STATE: 'mineAssistant_sync_state', + LAST_SYNC_TIME: 'mineAssistant_last_sync_time' +} as const; + +export const STORAGE_LIMITS = { + MAX_CONTEXTS: 100, + MAX_CONTEXT_SIZE: 1024 * 1024, // 1MB + MAX_TOTAL_SIZE: 5 * 1024 * 1024 // 5MB +} + +export const GET_RECENT_CONTEXTS_LIMIT = 10; \ No newline at end of file diff --git a/browser-extension/MineAssistant/src/popupManager.ts b/browser-extension/MineAssistant/src/content/contentManager.ts similarity index 97% rename from browser-extension/MineAssistant/src/popupManager.ts rename to browser-extension/MineAssistant/src/content/contentManager.ts index b6c3a69..93909d4 100644 --- a/browser-extension/MineAssistant/src/popupManager.ts +++ b/browser-extension/MineAssistant/src/content/contentManager.ts @@ -1,24 +1,7 @@ import browser from 'webextension-polyfill'; -import { ExtensionState, ExtensionSettings, MessageTypeEnum } from './types'; - -// 默认设置 -const DEFAULT_SETTINGS: ExtensionSettings = { - autoCapture: true, - captureInterval: 30000, // 30秒 - maxContexts: 100, - syncEnabled: true, - backendUrl: 'http://localhost:1733' -}; - -// 默认状态 -const DEFAULT_STATE: ExtensionState = { - isActive: false, - currentUrl: '', - contextCount: 0, - settings: DEFAULT_SETTINGS -}; - -console.debug("[default state]", DEFAULT_STATE); +import { ExtensionState, ExtensionSettings, MessageTypeEnum } from '../types'; +import { DEFAULT_SETTINGS, DEFAULT_STATE } from '../constants'; + export class PopupManager { private state: ExtensionState = { ...DEFAULT_STATE }; diff --git a/browser-extension/MineAssistant/src/content/index.ts b/browser-extension/MineAssistant/src/content/index.ts index fb9ba45..5afa392 100644 --- a/browser-extension/MineAssistant/src/content/index.ts +++ b/browser-extension/MineAssistant/src/content/index.ts @@ -1,223 +1,42 @@ -import { BrowserContextExtractor } from "./BrowserContextExtractor"; -import { BrowserContext, ExtensionState, MessageTypeEnum } from "../types"; -import browser from 'webextension-polyfill'; +import { PopupManager } from './contentManager'; + +let popupManager: PopupManager | null = null; /** - * 内容脚本主类 - * 负责与页面交互,捕获上下文内容 + * 初始化popup应用 */ -class ContentScript { - private extractor: BrowserContextExtractor; - private isActive: boolean = false; - private observer: MutationObserver | null = null; - - constructor() { - this.extractor = new BrowserContextExtractor(); - } - - /** - * 初始化内容脚本 - */ - async init() { - try { - await this.initMessageListener(); - // await this.initObserver(); - this.isActive = true; - console.log('MineAssistant Content Script initialized successfully'); - } catch (error) { - console.error('Failed to initialize content script:', error); - } - } - - /** - * 初始化消息监听器 - */ - private async initMessageListener() { - // 添加消息监听器 - browser.runtime.onMessage.addListener((message, sender, sendResponse) => { - this.handleMessage(message, sender, sendResponse); - return true; // 保持异步响应 - }); - } - - /** - * 处理接收到的消息 - */ - - private async handleMessage(message: any, sender: browser.Runtime.MessageSender, sendResponse: (response?: any) => void) { - console.log('Received message:', message); - try { - switch (message.type) { - case MessageTypeEnum.CAPTURE_CONTEXT: - const context = await this.captureContext(message.data?.mode || 'smart'); - sendResponse({ success: true, context }); - break; - case MessageTypeEnum.GET_STATE: - sendResponse({ success: true, active: this.isActive }); - break; - case MessageTypeEnum.ANALYZE_PAGE_TYPE: - const _context = await this.extractor.extractContext(); - sendResponse({ success: true, pageType: _context.metadata.contentType }); - break; - case MessageTypeEnum.GET_METADATA: - const metadata = await this.getMetadata(); - sendResponse({ success: true, metadata }); - break; - case MessageTypeEnum.UPDATE_STATE: - await this.updateState(message); - sendResponse({ success: true }); - break; - default: - sendResponse({ success: false, error: 'Unknown message type' }); - } - } catch (error) { - console.error('Error handling message:', error); - sendResponse({ success: false, error: error instanceof Error ? error.message : 'Unknown error' }); - } - } - - private async updateState(state: ExtensionState) { - setTimeout(() => { - browser.runtime.sendMessage({ - type: MessageTypeEnum.UPDATE_STATE, - data: { 11: 11, ...state } - }); - }, 5000); - } - - /** - * 初始化页面变化观察器 - * todo: 实现页面变化观察器的逻辑 - */ - private async initObserver() { - // 监听页面变化 - this.observer = new MutationObserver((mutations) => { - if (this.isActive) { - this.handlePageChange(mutations); - } - }); - - this.observer.observe(document.body, { - childList: true, - subtree: true, - characterData: true, - attributes: true - }); - } - - /** - * 处理页面变化 - */ - private async handlePageChange(mutations: MutationRecord[]) { - // 检查是否有实质性变化 - const hasSignificantChange = mutations.some(mutation => { - // 检查是否是内容变化 - if (mutation.type === 'characterData' || mutation.type === 'childList') { - return true; - } - - // 检查是否是重要属性变化 - if (mutation.type === 'attributes') { - const importantAttributes = ['title', 'src', 'href', 'content']; - return importantAttributes.includes(mutation.attributeName || ''); - } - - return false; - }); - - if (hasSignificantChange) { - console.log('Page content changed, notifying background script'); - - // 通知background script页面内容已更新 - browser.runtime.sendMessage({ - type: MessageTypeEnum.PAGE_CONTENT_UPDATED, - url: location.href, - title: document.title - }).catch(error => { - console.log('Failed to send page update notification:', error); +async function initializePopup(): Promise { + try { + // 创建popup管理器实例 + popupManager = new PopupManager(); + + // 等待初始化完成 + const waitForInit = (): Promise => { + return new Promise((resolve) => { + const checkReady = () => { + if (popupManager && popupManager.isReady()) { + resolve(); + } else { + setTimeout(checkReady, 100); + } + }; + checkReady(); }); - } - } + }; - /** - * 捕获上下文 - */ - private async captureContext(mode: 'smart' | 'basic' = 'smart'): Promise { - const contentType = this.extractor.detectContentType(); - if (mode === 'basic') { - // 基础模式:只提取基本页面信息 - return { - id: `${location.href}_${Date.now()}`, - url: location.href, - title: document.title, - timestamp: Date.now(), - metadata: this.extractor.extractMetadata(contentType), - structuredContent: { - title: document.title, - contentType: contentType - } - }; - } - - // 智能模式:使用完整的内容提取器 - return await this.extractor.extractContext(); - } - - /** - * 提取内容 - */ - private async extractContent(options?: any): Promise { - const context = await this.extractor.extractContext(); - - // 根据选项过滤内容 - if (options?.contentType) { - return this.filterContentByType(context, options.contentType); - } - - return context; - } - - /** - * 获取页面元数据 - */ - private async getMetadata(): Promise { - const context = await this.extractor.extractContext(); - return context.metadata; - } - - /** - * 根据类型过滤内容 - */ - private filterContentByType(context: BrowserContext, contentType: string): any { - switch (contentType) { - case 'article': - return context.structuredContent.articleContent; - case 'code': - return context.structuredContent.codeContent; - case 'multimedia': - return context.structuredContent.multimediaContent; - case 'data': - return context.structuredContent.dataContent; - default: - return context; - } - } - - /** - * 清理资源 - */ - destroy() { - if (this.observer) { - this.observer.disconnect(); - this.observer = null; - } - this.isActive = false; + await waitForInit(); + console.log('Popup application initialized successfully'); + } catch (error) { + console.error('Failed to initialize popup application:', error); } } -// 初始化内容脚本 -const contentScript = new ContentScript(); -contentScript.init(); +// 页面加载完成后初始化 +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initializePopup); +} else { + initializePopup(); +} -// 导出供测试使用 -export { ContentScript }; \ No newline at end of file +// 导出popup管理器供外部使用(如测试) +export { popupManager }; \ No newline at end of file diff --git a/browser-extension/MineAssistant/src/manifest.json b/browser-extension/MineAssistant/src/manifest.json index cef340e..7cee5e7 100644 --- a/browser-extension/MineAssistant/src/manifest.json +++ b/browser-extension/MineAssistant/src/manifest.json @@ -9,7 +9,7 @@ "128": "icon/128.png" }, "{{chrome}}.action": { - "default_popup": "src/popup.html" + "default_popup": "src/popup/index.html" }, "{{chrome}}.content_scripts": [ { @@ -18,11 +18,11 @@ } ], "{{firefox}}.browser_action": { - "default_popup": "src/popup.html" + "default_popup": "src/popup/index.html" }, "background": { - "{{chrome}}.service_worker": "src/background.ts", - "{{firefox}}.scripts": ["src/background.ts"] + "{{chrome}}.service_worker": "src/background/index.ts", + "{{firefox}}.scripts": ["src/background/index.ts"] }, "permissions": [ "storage", diff --git a/browser-extension/MineAssistant/src/popup.ts b/browser-extension/MineAssistant/src/popup.ts deleted file mode 100644 index db67393..0000000 --- a/browser-extension/MineAssistant/src/popup.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { PopupManager } from './popupManager'; - -let popupManager: PopupManager | null = null; - -console.log("log popupManager"); -/** - * 初始化popup应用 - */ -async function initializePopup(): Promise { - try { - // 创建popup管理器实例 - popupManager = new PopupManager(); - - // 等待初始化完成 - const waitForInit = (): Promise => { - return new Promise((resolve) => { - const checkReady = () => { - if (popupManager && popupManager.isReady()) { - resolve(); - } else { - setTimeout(checkReady, 100); - } - }; - checkReady(); - }); - }; - - await waitForInit(); - console.log('Popup application initialized successfully'); - } catch (error) { - console.error('Failed to initialize popup application:', error); - } -} - -// 页面加载完成后初始化 -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initializePopup); -} else { - initializePopup(); -} - -// 导出popup管理器供外部使用(如测试) -export { popupManager }; \ No newline at end of file diff --git a/browser-extension/MineAssistant/src/popup.css b/browser-extension/MineAssistant/src/popup/index.css similarity index 100% rename from browser-extension/MineAssistant/src/popup.css rename to browser-extension/MineAssistant/src/popup/index.css diff --git a/browser-extension/MineAssistant/src/popup.html b/browser-extension/MineAssistant/src/popup/index.html similarity index 96% rename from browser-extension/MineAssistant/src/popup.html rename to browser-extension/MineAssistant/src/popup/index.html index 8b82cdb..da57c4d 100644 --- a/browser-extension/MineAssistant/src/popup.html +++ b/browser-extension/MineAssistant/src/popup/index.html @@ -5,7 +5,7 @@ MineAssistant - + @@ -93,7 +93,7 @@

设置

- + \ No newline at end of file diff --git a/browser-extension/MineAssistant/src/services/api.ts b/browser-extension/MineAssistant/src/services/api.ts new file mode 100644 index 0000000..e69de29 diff --git a/browser-extension/MineAssistant/src/services/index.ts b/browser-extension/MineAssistant/src/services/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/browser-extension/MineAssistant/src/services/screenshot.ts b/browser-extension/MineAssistant/src/services/screenshot.ts new file mode 100644 index 0000000..e69de29 diff --git a/browser-extension/MineAssistant/src/services/sync/index.ts b/browser-extension/MineAssistant/src/services/sync/index.ts new file mode 100644 index 0000000..30071a2 --- /dev/null +++ b/browser-extension/MineAssistant/src/services/sync/index.ts @@ -0,0 +1 @@ +// 负责同步逻辑 diff --git a/browser-extension/MineAssistant/src/storage/context.ts b/browser-extension/MineAssistant/src/storage/context.ts new file mode 100644 index 0000000..261eb3b --- /dev/null +++ b/browser-extension/MineAssistant/src/storage/context.ts @@ -0,0 +1,216 @@ +import browser from 'webextension-polyfill'; + +import { BrowserContext } from "../types"; +import { GET_RECENT_CONTEXTS_LIMIT, STORAGE_KEYS, STORAGE_LIMITS } from "../constants"; + +// 上下文存储管理器 +// todo 暂时存储再 browser.storage 中 +// 后续扩容考虑存储到 IndexedDB 中 +export interface ContextStorageOptions { + maxContexts?: number; + maxContextSize?: number; +} + +export class ContextManager { + private static instance: ContextManager; + private options: ContextStorageOptions; + + static getInstance(options: ContextStorageOptions = {}): ContextManager { + if (!ContextManager.instance) { + ContextManager.instance = new ContextManager(options); + } + return ContextManager.instance; + } + + private constructor(options: ContextStorageOptions = {}) { + this.options = { + maxContexts: options?.maxContexts || STORAGE_LIMITS.MAX_CONTEXTS, + maxContextSize: options?.maxContextSize || STORAGE_LIMITS.MAX_CONTEXT_SIZE + }; + } + + // 获取所有上下文 + async getContexts(): Promise { + try { + const result = await browser.storage.local.get(STORAGE_KEYS.CONTEXTS); + return result[STORAGE_KEYS.CONTEXTS] || []; + } catch (error) { + console.error('Failed to get contexts:', error); + return []; + } + } + + // 添加新上下文 + async addContext(context: BrowserContext): Promise { + try { + // 验证上下文大小 + const contextSize = JSON.stringify(context).length; + if (contextSize > this.options.maxContextSize!) { + console.warn('Context size exceeds limit, skipping'); + return false; + } + + const contexts = await this.getContexts(); + + // 检查是否已经存在相同的上下文(基于URL和时间戳) + const existingIndex = contexts.findIndex(c => + c.url === context.url && c.timestamp === context.timestamp + ); + + if (existingIndex !== -1) { + // 更新现有上下文 + contexts[existingIndex] = context; + } else { + // 添加新上下文 + contexts.unshift(context); // 最新的在前面 + + // 限制数量 + if (contexts.length > this.options.maxContexts!) { + contexts.splice(this.options.maxContexts!); + } + } + + await browser.storage.local.set({ + [STORAGE_KEYS.CONTEXTS]: contexts + }); + + return true; + } catch (error) { + console.error('Failed to add context:', error); + return false; + } + } + + // 删除上下文 + async removeContext(url: string, timestamp: number): Promise { + try { + const contexts = await this.getContexts(); + const filteredContexts = contexts.filter(c => + !(c.url === url && c.timestamp === timestamp) + ); + + if (filteredContexts.length === contexts.length) { + return false; // 没有找到要删除的上下文 + } + + await browser.storage.local.set({ + [STORAGE_KEYS.CONTEXTS]: filteredContexts + }); + + return true; + } catch (error) { + console.error('Failed to remove context:', error); + return false; + } + } + + async removeContextById(id: string): Promise { + try { + const contexts = await this.getContexts(); + const filteredContexts = contexts.filter(c => + !(c.id === id) + ); + + if (filteredContexts.length === contexts.length) { + return false; // 没有找到要删除的上下文 + } + + await browser.storage.local.set({ + [STORAGE_KEYS.CONTEXTS]: filteredContexts + }); + + return true; + } catch (error) { + console.error('Failed to remove context:', error); + return false; + } + } + + // 清空所有上下文 + async clearContexts(): Promise { + try { + await browser.storage.local.remove(STORAGE_KEYS.CONTEXTS); + return true; + } catch (error) { + console.error('Failed to clear contexts:', error); + return false; + } + } + + // 获取最近的上下文 + async getRecentContexts(limit: number = GET_RECENT_CONTEXTS_LIMIT): Promise { + try { + const contexts = await this.getContexts(); + return contexts.slice(0, limit); + } catch (error) { + console.error('Failed to get recent contexts:', error); + return []; + } + } + + // 按域名获取上下文 + async getContextsByDomain(domain: string): Promise { + try { + const contexts = await this.getContexts(); + return contexts.filter(context => { + try { + const url = new URL(context.url); + return url.hostname === domain; + } catch { + return false; + } + }); + } catch (error) { + console.error('Failed to get contexts by domain:', error); + return []; + } + } + + // 获取存储统计信息 + async getStorageStats(): Promise<{ + totalContexts: number; + totalSize: number; + oldestContext?: BrowserContext; + newestContext?: BrowserContext; + }> { + try { + const contexts = await this.getContexts(); + const totalSize = contexts.reduce((size, context) => + size + JSON.stringify(context).length, 0 + ); + + return { + totalContexts: contexts.length, + totalSize, + oldestContext: contexts.length > 0 ? contexts[contexts.length - 1] : undefined, + newestContext: contexts.length > 0 ? contexts[0] : undefined + }; + } catch (error) { + console.error('Failed to get storage stats:', error); + return { + totalContexts: 0, + totalSize: 0 + }; + } + } + + // 监听上下文变化 + // perf: 考虑频繁触发时进行防抖, 300ms + onContextsChanged(callback: (contexts: BrowserContext[]) => void) { + const listener = (changes: { [key: string]: browser.Storage.StorageChange }) => { + if (changes[STORAGE_KEYS.CONTEXTS]) { + const newContexts = changes[STORAGE_KEYS.CONTEXTS].newValue as BrowserContext[]; + if (newContexts) { + callback(newContexts); + } + } + }; + + browser.storage.onChanged.addListener(listener); + + // 返回取消监听的函数 + return () => { + browser.storage.onChanged.removeListener(listener); + }; + } +} \ No newline at end of file diff --git a/browser-extension/MineAssistant/src/storage/index.ts b/browser-extension/MineAssistant/src/storage/index.ts new file mode 100644 index 0000000..cffd6f0 --- /dev/null +++ b/browser-extension/MineAssistant/src/storage/index.ts @@ -0,0 +1,64 @@ + +import { BrowserContext, ExtensionSettings } from "../types"; + +import { SettingsManager } from './settings'; +import { ContextManager } from './context'; + +export class StorageManager { + private static instance: StorageManager; + private settingsManager: SettingsManager; + private contextManager: ContextManager; + + constructor() { + this.settingsManager = SettingsManager.getInstance(); + this.contextManager = ContextManager.getInstance(); + } + + static getInstance() { + if (!this.instance) { + this.instance = new StorageManager(); + } + return this.instance; + } + + // 设置管理 + async getSettings(): Promise { + return await this.settingsManager.getSettings(); + } + + async saveSettings(settings: ExtensionSettings) { + return await this.settingsManager.saveSettings(settings); + } + + // 上下文管理 + async getContexts() { + return await this.contextManager.getContexts(); + } + + async addContext(context: BrowserContext) { + return await this.contextManager.addContext(context); + } + + async removeContextById(contextId: string) { + return await this.contextManager.removeContextById(contextId); + } + + async removeContext(url: string, timestamp: number) { + return await this.contextManager.removeContext(url, timestamp); + } + + async clearContexts() { + return await this.contextManager.clearContexts(); + } + + async getRecentContexts(count: number) { + return await this.contextManager.getRecentContexts(count); + } + async getRecentContextsByDomain(domain: string) { + return await this.contextManager.getContextsByDomain(domain); + } +} + +const storageManager = StorageManager.getInstance(); + +export default storageManager; diff --git a/browser-extension/MineAssistant/src/storage/settings.ts b/browser-extension/MineAssistant/src/storage/settings.ts new file mode 100644 index 0000000..f2dd2d3 --- /dev/null +++ b/browser-extension/MineAssistant/src/storage/settings.ts @@ -0,0 +1,98 @@ +import browser from 'webextension-polyfill'; + +import { ExtensionSettings } from "../types"; +import { STORAGE_KEYS, DEFAULT_SETTINGS } from "../constants"; + +export class SettingsManager { + private static instance: SettingsManager; + + static getInstance(): SettingsManager { + if (!SettingsManager.instance) { + SettingsManager.instance = new SettingsManager(); + } + return SettingsManager.instance; + } + + // 获取设置 + async getSettings(): Promise { + try { + const result = await browser.storage.local.get(STORAGE_KEYS.SETTINGS); + const settings = result[STORAGE_KEYS.SETTINGS] as ExtensionSettings; + + if (!settings) { + // 如果没有设置,使用默认值 + await this.saveSettings(DEFAULT_SETTINGS); + return DEFAULT_SETTINGS; + } + + return settings; + } catch (error) { + console.error('Failed to get settings:', error); + return DEFAULT_SETTINGS; + } + } + + // 保存设置 + async saveSettings(settings: ExtensionSettings): Promise { + try { + // 验证设置 + const validatedSettings = this.validateSettings(settings); + + await browser.storage.local.set({ + [STORAGE_KEYS.SETTINGS]: validatedSettings + }); + + return true; + } catch (error) { + console.error('Failed to save settings:', error); + return false; + } + } + + // 更新部分设置 + async updateSettings(updates: Partial): Promise { + try { + const currentSettings = await this.getSettings(); + const newSettings = { ...currentSettings, ...updates }; + + return await this.saveSettings(newSettings); + } catch (error) { + console.error('Failed to update settings:', error); + return false; + } + } + + // 验证设置 todo: 完善验证逻辑 + // 考虑是否用 react, arco-design 提供的组件,支持控件输入源头的validation + // 优点: 使用简单 + // 缺点: 体积变大; 重构项目为 react 项目 + private validateSettings(settings: ExtensionSettings): ExtensionSettings { + const validated = { ...settings }; + + return validated; + } + + // 重置为默认设置 + async resetToDefaults(): Promise { + return await this.saveSettings(DEFAULT_SETTINGS); + } + + // 监听设置变化 + onSettingsChanged(callback: (newSettings: ExtensionSettings) => void): () => void { + const listener = (changes: { [key: string]: browser.Storage.StorageChange }) => { + if (changes[STORAGE_KEYS.SETTINGS]) { + const newSettings = changes[STORAGE_KEYS.SETTINGS].newValue as ExtensionSettings; + if (newSettings) { + callback(newSettings); + } + } + }; + + browser.storage.onChanged.addListener(listener); + + // 返回取消监听的函数 + return () => { + browser.storage.onChanged.removeListener(listener); + }; + } +} diff --git a/browser-extension/MineAssistant/src/types/browserContext.ts b/browser-extension/MineAssistant/src/types/browserContext.ts index 37b40a6..5c46dd7 100644 --- a/browser-extension/MineAssistant/src/types/browserContext.ts +++ b/browser-extension/MineAssistant/src/types/browserContext.ts @@ -137,29 +137,3 @@ export interface DataPoint { value: number; unit?: string; } - -// 扩展状态类型 -export interface ExtensionState { - isActive: boolean; - currentUrl: string; - contextCount: number; - lastSyncTime?: number; - settings: ExtensionSettings; -} - -export interface ExtensionSettings { - autoCapture: boolean; - captureInterval: number; - maxContexts: number; - syncEnabled: boolean; - backendUrl: string; -} - - - -// 存储类型 -export interface StorageData { - contexts: BrowserContext[]; - settings: ExtensionSettings; - state: ExtensionState; -} \ No newline at end of file diff --git a/browser-extension/MineAssistant/src/types/index.ts b/browser-extension/MineAssistant/src/types/index.ts index 4dba7aa..2e31eae 100644 --- a/browser-extension/MineAssistant/src/types/index.ts +++ b/browser-extension/MineAssistant/src/types/index.ts @@ -1,3 +1,3 @@ export * from "./browserContext" -// export * from "./payload" -export * from "./message" \ No newline at end of file +export * from "./message" +export * from "./storage" \ No newline at end of file diff --git a/browser-extension/MineAssistant/src/types/storage.ts b/browser-extension/MineAssistant/src/types/storage.ts new file mode 100644 index 0000000..5748482 --- /dev/null +++ b/browser-extension/MineAssistant/src/types/storage.ts @@ -0,0 +1,26 @@ +import { BrowserContext } from "./"; + + +// 扩展状态类型 +export interface ExtensionState { + isActive: boolean; + currentUrl: string; + contextCount: number; + lastSyncTime?: number; + settings: ExtensionSettings; +} + +export interface ExtensionSettings { + autoCapture: boolean; + captureInterval: number; + maxContexts: number; + syncEnabled: boolean; + backendUrl: string; +} + +// 存储类型 +export interface StorageData { + contexts: BrowserContext[]; + settings: ExtensionSettings; + state: ExtensionState; +} \ No newline at end of file From f7967677f1af0e04cec7b168eaa0ef7fe1bcd4ba Mon Sep 17 00:00:00 2001 From: down <37741552+Facefall@users.noreply.github.com> Date: Sun, 7 Dec 2025 21:36:13 +0800 Subject: [PATCH 4/6] refactor: BrowserContext structure, store the single function to utils file --- .../src/content/BrowserContextExtractor.ts | 520 ++++++------------ .../MineAssistant/src/types/browserContext.ts | 8 +- .../MineAssistant/src/utils/common.ts | 7 + .../src/utils/contentAnalyzer.ts | 221 ++++++++ .../src/utils/contentDetector.ts | 188 +++++++ .../src/utils/contentExtractor.ts | 185 +++++++ .../src/utils/contentSummarizer.ts | 75 +++ .../MineAssistant/src/utils/index.ts | 5 + 8 files changed, 857 insertions(+), 352 deletions(-) create mode 100644 browser-extension/MineAssistant/src/utils/common.ts create mode 100644 browser-extension/MineAssistant/src/utils/contentAnalyzer.ts create mode 100644 browser-extension/MineAssistant/src/utils/contentDetector.ts create mode 100644 browser-extension/MineAssistant/src/utils/contentExtractor.ts create mode 100644 browser-extension/MineAssistant/src/utils/contentSummarizer.ts create mode 100644 browser-extension/MineAssistant/src/utils/index.ts diff --git a/browser-extension/MineAssistant/src/content/BrowserContextExtractor.ts b/browser-extension/MineAssistant/src/content/BrowserContextExtractor.ts index f6c1f6f..b99eea4 100644 --- a/browser-extension/MineAssistant/src/content/BrowserContextExtractor.ts +++ b/browser-extension/MineAssistant/src/content/BrowserContextExtractor.ts @@ -1,110 +1,142 @@ import { BrowserContext, ContentType, + ExtractionMode, PageMetadata, StructuredContent, ArticleContent, CodeContent, MultimediaContent, DataContent, - Heading, - Paragraph, - Image, - CodeBlock, - MediaElement, - Table, } from "../types"; -import browser from 'webextension-polyfill'; +import browser from "webextension-polyfill"; +import { + countWords, + extractCodeBlocks, + extractHeadings, + extractImages, + extractMediaElements, + extractParagraphs, + extractTables, + estimateMediaDuration, + extractTranscript, + detectCharts, + extractDataPoints, + generateSummary, + detectContentType, + extractMetaTags, + analyzeCodeLanguages, + shouldStoreRawDom, + createPagePreview, + generateContextId +} from "../utils"; /** * 浏览器上下文提取器 * 负责从当前页面提取结构化内容 + * 支持两种提取模式: + * - SMART模式:提取结构化数据+截屏+详细分析 + * - BASIC模式:只提取基础信息+rawContext */ export class BrowserContextExtractor { - /** * 提取完整的浏览器上下文 + * @param mode 提取模式,默认为智能模式 */ - async extractContext(): Promise { - const contentType = this.detectContentType(); + async extractContext( + mode: ExtractionMode = ExtractionMode.SMART + ): Promise { + const contentType = detectContentType(); const metadata = this.extractMetadata(contentType); - const structuredContent = this.extractStructuredContent(contentType); + + let structuredContent: StructuredContent; + let screenshot: string | undefined; + let rawDom: string | undefined; + + if (mode === ExtractionMode.SMART) { + // 智能模式:提取结构化内容+截屏+详细分析 + structuredContent = this.extractStructuredContent(contentType); + screenshot = await this.captureScreenshot(); + rawDom = shouldStoreRawDom(contentType) + ? document.body.innerHTML + : undefined; + } else { + // 基础模式:只提取基础信息+rawContext + structuredContent = this.extractBasicStructuredContent(contentType); + screenshot = undefined; + rawDom = document.body.innerHTML; // 基础模式总是存储rawContext + } return { - id: this.generateContextId(), + id: generateContextId(), url: location.href, title: document.title, timestamp: Date.now(), metadata, structuredContent, - rawDom: this.shouldStoreRawDom(contentType) ? document.body.innerHTML : undefined, + rawDom, + screenshot, + extractionMode: mode, }; } /** - * 检测内容类型 + * 提取页面元数据 */ - public detectContentType(): ContentType { - const url = location.href; - const html = document.documentElement.innerHTML; - - // 代码相关页面检测 - if (this.isCodePage(url, html)) { - return ContentType.CODE; - } - - // 多媒体页面检测 - if (this.isMultimediaPage(html)) { - return ContentType.MULTIMEDIA; - } - - // 数据表格页面检测 - if (this.isDataPage(html)) { - return ContentType.DATA; - } + public extractMetadata(contentType?: ContentType): PageMetadata { + contentType = contentType || detectContentType(); - // 文章页面检测 - if (this.isArticlePage(html)) { - return ContentType.ARTICLE; - } + const metaTags = extractMetaTags(); + const wordCount = countWords(document.body.textContent || ""); - return ContentType.UNKNOWN; + return { + domain: location.hostname, + language: document.documentElement.lang || "en", + description: metaTags.description || "", + keywords: metaTags.keywords + ? metaTags.keywords.split(",").map((k) => k.trim()) + : [], + author: metaTags.author || "", + publishedTime: + metaTags["article:published_time"] || metaTags.publishDate || "", + modifiedTime: + metaTags["article:modified_time"] || metaTags.modifiedDate || "", + wordCount, + contentType, + metaTags, + }; } /** - * 提取页面元数据 + * 提取基础结构化内容(基础模式使用) */ - public extractMetadata(contentType?: ContentType): PageMetadata { - contentType = contentType || this.detectContentType(); - - const metaTags = this.extractMetaTags(); - const wordCount = this.countWords(document.body.textContent || ''); + private extractBasicStructuredContent( + contentType: ContentType + ): StructuredContent { + const title = document.title; + const summary = generateSummary(contentType); return { - domain: location.hostname, - language: document.documentElement.lang || 'en', - description: metaTags.description || '', - keywords: metaTags.keywords ? metaTags.keywords.split(',').map(k => k.trim()) : [], - author: metaTags.author || '', - publishedTime: metaTags['article:published_time'] || metaTags.publishDate || '', - modifiedTime: metaTags['article:modified_time'] || metaTags.modifiedDate || '', - wordCount, + title, + summary, contentType, - metaTags + // 基础模式不提取详细的结构化内容 }; } /** - * 提取结构化内容 + * 提取结构化内容(智能模式使用) */ - private extractStructuredContent(contentType: ContentType): StructuredContent { + private extractStructuredContent( + contentType: ContentType + ): StructuredContent { const title = document.title; - const summary = this.generateSummary(contentType); + const summary = generateSummary(contentType); const structuredContent: StructuredContent = { title, summary, - contentType + contentType, }; // 根据内容类型提取特定内容 @@ -130,9 +162,9 @@ export class BrowserContextExtractor { * 提取文章内容 */ private extractArticleContent(): ArticleContent { - const headings = this.extractHeadings(); - const paragraphs = this.extractParagraphs(); - const images = this.extractImages(); + const headings = extractHeadings(); + const paragraphs = extractParagraphs(); + const images = extractImages(); return { headings, @@ -144,338 +176,124 @@ export class BrowserContextExtractor { /** * 提取代码内容 */ + // todo: 完善代码内容提取,包括文件结构分析 private extractCodeContent(): CodeContent { - const codeBlocks = this.extractCodeBlocks(); - const languageStats = this.analyzeCodeLanguages(codeBlocks); + const codeBlocks = extractCodeBlocks(); + const languageStats = analyzeCodeLanguages(codeBlocks); return { codeBlocks, languageStats, - fileStructure: [] // 简化实现 + fileStructure: [], }; } /** - * 提取多媒体内容 + * 提取多媒体内容 - 完善的多媒体内容提取 */ - // TODO: 实现提取多媒体内容的逻辑 private extractMultimediaContent(): MultimediaContent { - const mediaElements = this.extractMediaElements(); + const mediaElements = extractMediaElements(); + const duration = estimateMediaDuration(mediaElements); + const transcript = extractTranscript(); return { mediaElements, - duration: undefined, - transcript: undefined + duration, + transcript, }; } /** - * 提取数据内容 - * todo: 实现提取数据内容的逻辑 + * 提取数据内容 - 完善的数据内容提取 */ private extractDataContent(): DataContent { - const tables = this.extractTables(); + const tables = extractTables(); + const charts = detectCharts(); + const dataPoints = extractDataPoints(); return { tables, - charts: [], // 简化实现 - dataPoints: [] // 简化实现 + charts, + dataPoints, }; } - /** - * 检测代码页面 - */ - private isCodePage(url: string, html: string): boolean { - const codeIndicators = [ - /github\.com/i, - /gitlab\.com/i, - /bitbucket\.org/i, - /stackoverflow\.com/i, - /codepen\.io/i, - /jsfiddle\.net/i, - /]*>|]*>/i, - /class=\"highlight\"/i, - /syntaxhighlighter/i - ]; - - return codeIndicators.some(indicator => indicator.test(url) || indicator.test(html)); - } - - /** - * 检测多媒体页面 - */ - private isMultimediaPage(html: string): boolean { - const mediaIndicators = [ - /]*>/i, - /]*>/i, - /]*>/i, - /youtube\.com/i, - /vimeo\.com/i, - /soundcloud\.com/i - ]; - - return mediaIndicators.some(indicator => indicator.test(html)); - } - - /** - * 检测数据页面 - */ - private isDataPage(html: string): boolean { - const dataIndicators = [ - /]*>/i, - /class=\"table\"/i, - /data-grid/i, - /chart/i, - /graph/i, - /dataset/i - ]; - - return dataIndicators.some(indicator => indicator.test(html)); - } - - /** - * 检测文章页面 - */ - private isArticlePage(html: string): boolean { - const articleIndicators = [ - /]*>/i, - /class=\"article\"/i, - /blog-post/i, - /news-article/i, - /entry-content/i - ]; - - return articleIndicators.some(indicator => indicator.test(html)); - } - - /** - * 提取meta标签 - */ - public extractMetaTags(): Record { - const metaTags: Record = {}; - const metaElements = document.querySelectorAll('meta'); - - metaElements.forEach(meta => { - const name = meta.getAttribute('name') || meta.getAttribute('property'); - const content = meta.getAttribute('content'); - if (name && content) { - metaTags[name] = content; - } - }); - - return metaTags; - } - /** - * 生成摘要 - */ - private generateSummary(contentType: ContentType): string { - const description = document.querySelector('meta[name="description"]')?.getAttribute('content'); - if (description) return description; - // 根据内容类型生成不同摘要 - switch (contentType) { - case ContentType.ARTICLE: - return this.extractFirstParagraph(); - case ContentType.CODE: - return '代码页面,包含多个代码片段'; - case ContentType.MULTIMEDIA: - return '多媒体内容页面'; - case ContentType.DATA: - return '数据表格页面'; - default: - return document.title; - } - } - /** - * 提取标题 - */ - private extractHeadings(): Heading[] { - const headings: Heading[] = []; - const headingElements = document.querySelectorAll('h1, h2, h3, h4, h5, h6'); - - headingElements.forEach(heading => { - const level = parseInt(heading.tagName.charAt(1)); - headings.push({ - level, - text: heading.textContent?.trim() || '', - id: heading.id || undefined - }); - }); - - return headings; - } /** - * 提取段落 + * 捕获屏幕截图 */ - private extractParagraphs(): Paragraph[] { - const paragraphs: Paragraph[] = []; - const paragraphElements = document.querySelectorAll('p'); - - paragraphElements.forEach(p => { - const text = p.textContent?.trim(); - if (text && text.length > 10) { - paragraphs.push({ - text, - wordCount: this.countWords(text) + private async captureScreenshot(): Promise { + try { + // 检查是否在浏览器扩展环境中 + if (browser?.tabs?.captureVisibleTab) { + const dataUrl = await browser.tabs.captureVisibleTab(undefined, { + format: "png", + quality: 80, }); + return dataUrl; } - }); - return paragraphs; - } - - /** - * 提取图片 - */ - private extractImages(): Image[] { - const images: Image[] = []; - const imgElements = document.querySelectorAll('img'); - - imgElements.forEach(img => { - images.push({ - src: img.src, - alt: img.alt, - title: img.title || undefined - }); - }); - - return images; - } - - /** - * 提取代码块 - */ - private extractCodeBlocks(): CodeBlock[] { - const codeBlocks: CodeBlock[] = []; - const codeElements = document.querySelectorAll('pre code, code'); - - codeElements.forEach(code => { - const language = this.detectCodeLanguage(code); - codeBlocks.push({ - language, - code: code.textContent?.trim() || '' - }); - }); - - return codeBlocks; - } - - /** - * 提取媒体元素 - */ - private extractMediaElements(): MediaElement[] { - const mediaElements: MediaElement[] = []; - - // 视频元素 - document.querySelectorAll('video').forEach(video => { - mediaElements.push({ - type: 'video', - src: video.src, - duration: video.duration || undefined, - width: video.videoWidth, - height: video.videoHeight - }); - }); - - // 音频元素 - document.querySelectorAll('audio').forEach(audio => { - mediaElements.push({ - type: 'audio', - src: audio.src, - duration: audio.duration || undefined - }); - }); - - return mediaElements; - } - - /** - * 提取表格 - */ - private extractTables(): Table[] { - const tables: Table[] = []; - const tableElements = document.querySelectorAll('table'); - - tableElements.forEach(table => { - const headers: string[] = []; - const rows: string[][] = []; - - // 提取表头 - table.querySelectorAll('th').forEach(th => { - headers.push(th.textContent?.trim() || ''); - }); - - // 提取行数据 - table.querySelectorAll('tr').forEach(tr => { - const row: string[] = []; - tr.querySelectorAll('td').forEach(td => { - row.push(td.textContent?.trim() || ''); - }); - if (row.length > 0) rows.push(row); - }); - - if (headers.length > 0 || rows.length > 0) { - tables.push({ - headers, - rows, - caption: table.querySelector('caption')?.textContent?.trim() || undefined - }); + // 检查是否支持 HTML2Canvas 或其他截图库 + // if (typeof (window as any).html2canvas !== 'undefined') { + // const canvas = await (window as any).html2canvas(document.body, { + // scale: 0.5, // 降低分辨率以减少数据大小 + // useCORS: true, + // logging: false + // }); + // return canvas.toDataURL('image/png', 0.8); + // } + + // 降级方案:使用浏览器原生 API + if ( + "mediaDevices" in navigator && + "getDisplayMedia" in navigator.mediaDevices + ) { + try { + const stream = await navigator.mediaDevices.getDisplayMedia({ + video: true, + // todo: figure out there option + //{ mediaSource: 'screen' }, + audio: false, + }); + + const video = document.createElement("video"); + video.srcObject = stream; + video.play(); + + return new Promise((resolve) => { + video.onloadedmetadata = () => { + const canvas = document.createElement("canvas"); + canvas.width = Math.min(video.videoWidth, 800); // 限制宽度 + canvas.height = Math.min(video.videoHeight, 600); // 限制高度 + + const ctx = canvas.getContext("2d"); + ctx?.drawImage(video, 0, 0, canvas.width, canvas.height); + + const dataUrl = canvas.toDataURL("image/png", 0.7); + + // 停止媒体流 + stream.getTracks().forEach((track: any) => track.stop()); + + resolve(dataUrl); + }; + }); + } catch (error) { + console.warn("屏幕捕获失败:", error); + } } - }); - - return tables; - } - - /** - * 辅助方法 - */ - private countWords(text: string): number { - return text.split(/\s+/).filter(word => word.length > 0).length; - } - private detectCodeLanguage(code: Element): string | undefined { - const className = code.className; - if (className.includes('language-')) { - return className.split('language-')[1]?.split(' ')[0]; + // 最后降级:创建页面预览 + return createPagePreview(); + } catch (error) { + console.error("截图捕获失败:", error); + return undefined; } - return undefined; } - private analyzeCodeLanguages(codeBlocks: CodeBlock[]): Record { - const stats: Record = {}; - codeBlocks.forEach(block => { - const lang = block.language || 'unknown'; - stats[lang] = (stats[lang] || 0) + 1; - }); - - return stats; - } - - private extractFirstParagraph(): string { - const firstP = document.querySelector('p'); - return firstP?.textContent?.trim().slice(0, 200) + '...' || ''; - } - - private generateContextId(): string { - return `${location.href}_${Date.now()}`; - } - - private shouldStoreRawDom(contentType: ContentType): boolean { - // 只在文章页面和未知页面存储原始DOM - return contentType === ContentType.ARTICLE || contentType === ContentType.UNKNOWN; - } - - // need update: 更新实现方案 - private async captureScreenshot(): Promise { - // 简化实现 - 实际应该使用chrome.tabs.captureVisibleTab - - return undefined - } -} \ No newline at end of file +} diff --git a/browser-extension/MineAssistant/src/types/browserContext.ts b/browser-extension/MineAssistant/src/types/browserContext.ts index 5c46dd7..792a78b 100644 --- a/browser-extension/MineAssistant/src/types/browserContext.ts +++ b/browser-extension/MineAssistant/src/types/browserContext.ts @@ -6,6 +6,11 @@ export enum ContentType { UNKNOWN = 'unknown' } +export enum ExtractionMode { + SMART = 'smart', // 智能模式 - 提取结构化数据+截屏 + BASIC = 'basic' // 基础模式 - 只提取基础信息+rawContext +} + // 核心类型定义 export interface BrowserContext { id: string; @@ -16,6 +21,7 @@ export interface BrowserContext { structuredContent: StructuredContent; rawDom?: string; // 可选:存储body的innerHTML screenshot?: string; // 可选:Base64缩略图 + extractionMode: ExtractionMode; // 提取模式 } export interface PageMetadata { @@ -56,7 +62,7 @@ export interface ArticleContent { export interface CodeContent { codeBlocks: CodeBlock[]; languageStats: Record; - fileStructure: FileStructure[]; + fileStructure?: FileStructure[]; } // 多媒体类型内容 diff --git a/browser-extension/MineAssistant/src/utils/common.ts b/browser-extension/MineAssistant/src/utils/common.ts new file mode 100644 index 0000000..4d7c838 --- /dev/null +++ b/browser-extension/MineAssistant/src/utils/common.ts @@ -0,0 +1,7 @@ +export function generateContextId() { + return `${location.href}_${Date.now()}`; +} + +export function countWords(text: string): number { + return text.split(/\s+/).filter(word => word.length > 0).length; +} \ No newline at end of file diff --git a/browser-extension/MineAssistant/src/utils/contentAnalyzer.ts b/browser-extension/MineAssistant/src/utils/contentAnalyzer.ts new file mode 100644 index 0000000..475a79a --- /dev/null +++ b/browser-extension/MineAssistant/src/utils/contentAnalyzer.ts @@ -0,0 +1,221 @@ +import { Chart, CodeBlock, DataPoint, MediaElement } from "../types"; + +/** + * 检测图表 + */ +export function detectCharts(): Chart[] { + const charts: Chart[] = []; + + // 查找常见的图表容器 + const chartSelectors = [ + "canvas", + ".chart", + ".graph", + "[data-chart]", + ".plotly-graph-div", + ".highcharts-container", + ".chart-container", + ]; + + chartSelectors.forEach((selector) => { + const elements = document.querySelectorAll(selector); + elements.forEach((element, index) => { + const chartType = inferChartType(element); + charts.push({ + type: chartType, + data: extractChartData(element), + title: extractChartTitle(element), + }); + }); + }); + + return charts; +} + +/** + * 推断图表类型 + */ +export function inferChartType(element: Element): string { + const className = element.className; + const id = element.id; + + if (className.includes("pie") || id.includes("pie")) return "pie"; + if (className.includes("bar") || id.includes("bar")) return "bar"; + if (className.includes("line") || id.includes("line")) return "line"; + if (className.includes("scatter") || id.includes("scatter")) + return "scatter"; + if (element.tagName.toLowerCase() === "canvas") return "canvas"; + + return "unknown"; +} + +/** + * 提取图表数据(简化实现) + */ +export function extractChartData(element: Element): any { + // 这里可以实现更复杂的数据提取逻辑 + // 简化实现:返回元素的基本信息 + return { + elementType: element.tagName, + className: element.className, + id: element.id, + textContent: element.textContent?.trim().slice(0, 100), + }; +} + +/** + * 提取图表标题 + */ +export function extractChartTitle(element: Element): string | undefined { + // 查找附近的标题元素 + const titleSelectors = [ + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + ".title", + ".chart-title", + ]; + + let current: Element | null = element; + while (current) { + const parent = current.parentElement as any; + if (parent) { + for (const selector of titleSelectors) { + const titleElement = parent.querySelector(selector); + if (titleElement) { + return titleElement.textContent?.trim(); + } + } + } + current = parent; + } + + return undefined; +} + +/** + * 提取数据点 + */ +export function extractDataPoints(): DataPoint[] { + const dataPoints: DataPoint[] = []; + + // 从表格中提取数据点 + const tables = document.querySelectorAll("table"); + tables.forEach((table) => { + const rows = table.querySelectorAll("tr"); + rows.forEach((row, rowIndex) => { + if (rowIndex === 0) return; // 跳过表头 + + const cells = row.querySelectorAll("td"); + cells.forEach((cell, cellIndex) => { + const text = cell.textContent?.trim(); + if (text) { + const number = parseFloat(text.replace(/[^\d.-]/g, "")); + if (!isNaN(number)) { + dataPoints.push({ + type: "table_numeric", + value: number, + unit: extractUnit(text), + }); + } + } + }); + }); + }); + + // 从页面文本中提取数值 + const numberRegex = /(-?\d+(?:\.\d+)?)\s*([a-zA-Z%°]+)?/g; + const textContent = document.body.textContent || ""; + let match; + + while ((match = numberRegex.exec(textContent)) !== null) { + const value = parseFloat(match[1]); + const unit = match[2]; + + dataPoints.push({ + type: "text_numeric", + value: value, + unit: unit, + }); + } + + return dataPoints; +} + +/** + * 提取单位 + */ +export function extractUnit(text: string): string | undefined { + const unitMatch = text.match(/[a-zA-Z%°]+$/); + return unitMatch ? unitMatch[0] : undefined; +} + +/** + * 估算媒体时长 + */ +export function estimateMediaDuration(mediaElements: MediaElement[]): number { + if (mediaElements.length === 0) return 0; + + // 计算所有媒体元素的平均时长 + const validDurations = mediaElements + .map((element) => element.duration) + .filter((duration) => duration !== undefined && duration > 0) as number[]; + + if (validDurations.length === 0) return 0; + + return Math.round( + validDurations.reduce((sum, duration) => sum + duration, 0) / + validDurations.length + ); +} + +/** + * 提取转录文本(简化实现) + */ +export function extractTranscript(): string | undefined { + // 查找页面中的转录文本或字幕 + const transcriptSelectors = [ + ".transcript", + ".subtitle", + ".caption", + "[data-transcript]", + ".video-transcript", + ".audio-transcript", + ]; + + for (const selector of transcriptSelectors) { + const element = document.querySelector(selector); + if (element) { + return element.textContent?.trim(); + } + } + + // 如果没有找到专门的转录文本,尝试提取页面主要内容作为转录 + const mainContent = document.querySelector( + "main, article, .content, .main-content" + ); + if (mainContent) { + const text = mainContent.textContent?.trim(); + return text + ? text.slice(0, 1000) + (text.length > 1000 ? "..." : "") + : undefined; + } + + return undefined; +} + +export function analyzeCodeLanguages( + codeBlocks: CodeBlock[] +): Record { + const stats: Record = {}; + + codeBlocks.forEach((block) => { + const lang = block.language || "unknown"; + stats[lang] = (stats[lang] || 0) + 1; + }); + + return stats; +} \ No newline at end of file diff --git a/browser-extension/MineAssistant/src/utils/contentDetector.ts b/browser-extension/MineAssistant/src/utils/contentDetector.ts new file mode 100644 index 0000000..0549460 --- /dev/null +++ b/browser-extension/MineAssistant/src/utils/contentDetector.ts @@ -0,0 +1,188 @@ +import { ContentType } from "../types"; + +/** + * 检测内容类型 - 更精细的页面特征判断 + */ +export function detectContentType(): ContentType { + const url = location.href; + const html = document.documentElement.innerHTML; + const hostname = location.hostname; + + // 1. 基于URL的快速判断(高置信度) + const quickType = detectByUrl(hostname, url); + if (quickType !== ContentType.UNKNOWN) { + return quickType; + } + + // 2. 基于DOM结构的详细分析 + return detectByDomStructure(html); +} + +/** + * 基于URL的快速内容类型检测 + */ +export function detectByUrl(hostname: string, url: string): ContentType { + // 代码平台 + const codePlatforms = [ + "github.com", + "gitlab.com", + "bitbucket.org", + "stackoverflow.com", + "codepen.io", + "jsfiddle.net", + "codesandbox.io", + "repl.it", + "gitee.com", + "coding.net", + "csdn.net", + ]; + if (codePlatforms.some((platform) => hostname.includes(platform))) { + return ContentType.CODE; + } + + // 多媒体平台 + const mediaPlatforms = [ + "youtube.com", + "vimeo.com", + "soundcloud.com", + "spotify.com", + "bilibili.com", + "youku.com", + "iqiyi.com", + "qq.com", + ]; + if (mediaPlatforms.some((platform) => hostname.includes(platform))) { + return ContentType.MULTIMEDIA; + } + + // 数据/图表平台 + const dataPlatforms = [ + "docs.google.com/spreadsheets", + "excel", + "tableau", + "plotly", + ]; + if (dataPlatforms.some((platform) => url.includes(platform))) { + return ContentType.DATA; + } + + // 文章/博客平台 + const articlePlatforms = [ + "medium.com", + "zhihu.com", + "juejin.cn", + "segmentfault.com", + "cnblogs.com", + "wordpress.com", + "blogspot.com", + ]; + if (articlePlatforms.some((platform) => hostname.includes(platform))) { + return ContentType.ARTICLE; + } + + return ContentType.UNKNOWN; +} + +/** + * 基于DOM结构的内容类型检测 + */ +export function detectByDomStructure(html: string): ContentType { + // 代码相关页面检测 + if (isCodePage(location.href, html)) { + return ContentType.CODE; + } + + // 多媒体页面检测 + if (isMultimediaPage(html)) { + return ContentType.MULTIMEDIA; + } + + // 数据表格页面检测 + if (isDataPage(html)) { + return ContentType.DATA; + } + + // 文章页面检测 + if (isArticlePage(html)) { + return ContentType.ARTICLE; + } + + return ContentType.UNKNOWN; +} + + +/** + * 检测代码页面 + */ +export function isCodePage(url: string, html: string): boolean { + const codeIndicators = [ + /github\.com/i, + /gitlab\.com/i, + /bitbucket\.org/i, + /stackoverflow\.com/i, + /codepen\.io/i, + /jsfiddle\.net/i, + /]*>|]*>/i, + /class=\"highlight\"/i, + /syntaxhighlighter/i, + ]; + + return codeIndicators.some( + (indicator) => indicator.test(url) || indicator.test(html) + ); +} + +/** + * 检测多媒体页面 + */ +export function isMultimediaPage(html: string): boolean { + const mediaIndicators = [ + /]*>/i, + /]*>/i, + /]*>/i, + /youtube\.com/i, + /vimeo\.com/i, + /soundcloud\.com/i, + ]; + + return mediaIndicators.some((indicator) => indicator.test(html)); +} + +/** + * 检测数据页面 + */ +export function isDataPage(html: string): boolean { + const dataIndicators = [ + /]*>/i, + /class=\"table\"/i, + /data-grid/i, + /chart/i, + /graph/i, + /dataset/i, + ]; + + return dataIndicators.some((indicator) => indicator.test(html)); +} + +/** + * 检测文章页面 + */ +export function isArticlePage(html: string): boolean { + const articleIndicators = [ + /]*>/i, + /class=\"article\"/i, + /blog-post/i, + /news-article/i, + /entry-content/i, + ]; + + return articleIndicators.some((indicator) => indicator.test(html)); +} + + +export function shouldStoreRawDom(contentType: ContentType): boolean { + // 只在文章页面和未知页面存储原始DOM + return ( + contentType === ContentType.ARTICLE || contentType === ContentType.UNKNOWN + ); +} \ No newline at end of file diff --git a/browser-extension/MineAssistant/src/utils/contentExtractor.ts b/browser-extension/MineAssistant/src/utils/contentExtractor.ts new file mode 100644 index 0000000..65bb920 --- /dev/null +++ b/browser-extension/MineAssistant/src/utils/contentExtractor.ts @@ -0,0 +1,185 @@ +import { + Heading, + Paragraph, + CodeBlock, + Image, + MediaElement, + Table, +} from "../types"; + +import { countWords } from "./common"; + +/** + * 提取meta标签 + */ +export function extractMetaTags(): Record { + const metaTags: Record = {}; + const metaElements = document.querySelectorAll("meta"); + + metaElements.forEach((meta) => { + const name = meta.getAttribute("name") || meta.getAttribute("property"); + const content = meta.getAttribute("content"); + + if (name && content) { + metaTags[name] = content; + } + }); + + return metaTags; +} + +/** + * 提取标题 + */ +export function extractHeadings(): Heading[] { + const headings: Heading[] = []; + const headingElements = document.querySelectorAll('h1, h2, h3, h4, h5, h6'); + + headingElements.forEach(heading => { + const level = parseInt(heading.tagName.charAt(1)); + headings.push({ + level, + text: heading.textContent?.trim() || '', + id: heading.id || undefined + }); + }); + + return headings; +} + +/** + * 提取段落 + */ +export function extractParagraphs(): Paragraph[] { + const paragraphs: Paragraph[] = []; + const paragraphElements = document.querySelectorAll('p'); + + paragraphElements.forEach(p => { + const text = p.textContent?.trim(); + if (text && text.length > 10) { + paragraphs.push({ + text, + wordCount: countWords(text) + }); + } + }); + + return paragraphs; +} + +export function extractFirstParagraph() { + const firstP = document.querySelector("p"); + return firstP?.textContent?.trim().slice(0, 200) + "..." || ""; +} + +/** + * 提取图片 + */ +export function extractImages(): Image[] { + const images: Image[] = []; + const imgElements = document.querySelectorAll('img'); + + imgElements.forEach(img => { + images.push({ + src: img.src, + alt: img.alt, + title: img.title || undefined + }); + }); + + return images; +} + +/** + * 提取代码块 + */ +export function extractCodeBlocks(): CodeBlock[] { + const codeBlocks: CodeBlock[] = []; + const codeElements = document.querySelectorAll('pre code, code'); + + codeElements.forEach(code => { + const language = detectCodeLanguage(code); + codeBlocks.push({ + language, + code: code.textContent?.trim() || '' + }); + }); + + return codeBlocks; +} + +/** + * 提取媒体元素 + */ +export function extractMediaElements(): MediaElement[] { + const mediaElements: MediaElement[] = []; + + // 视频元素 + document.querySelectorAll('video').forEach(video => { + mediaElements.push({ + type: 'video', + src: video.src, + duration: video.duration || undefined, + width: video.videoWidth, + height: video.videoHeight + }); + }); + + // 音频元素 + document.querySelectorAll('audio').forEach(audio => { + mediaElements.push({ + type: 'audio', + src: audio.src, + duration: audio.duration || undefined + }); + }); + + return mediaElements; +} + +/** + * 提取表格 + */ +export function extractTables(): Table[] { + const tables: Table[] = []; + const tableElements = document.querySelectorAll('table'); + + tableElements.forEach(table => { + const headers: string[] = []; + const rows: string[][] = []; + + // 提取表头 + table.querySelectorAll('th').forEach(th => { + headers.push(th.textContent?.trim() || ''); + }); + + // 提取行数据 + table.querySelectorAll('tr').forEach(tr => { + const row: string[] = []; + tr.querySelectorAll('td').forEach(td => { + row.push(td.textContent?.trim() || ''); + }); + if (row.length > 0) rows.push(row); + }); + + if (headers.length > 0 || rows.length > 0) { + tables.push({ + headers, + rows, + caption: table.querySelector('caption')?.textContent?.trim() || undefined + }); + } + }); + + return tables; +} + + + +export function detectCodeLanguage(code: Element): string | undefined { + const className = code.className; + if (className.includes('language-')) { + return className.split('language-')[1]?.split(' ')[0]; + } + return undefined; +} \ No newline at end of file diff --git a/browser-extension/MineAssistant/src/utils/contentSummarizer.ts b/browser-extension/MineAssistant/src/utils/contentSummarizer.ts new file mode 100644 index 0000000..d7621b5 --- /dev/null +++ b/browser-extension/MineAssistant/src/utils/contentSummarizer.ts @@ -0,0 +1,75 @@ +import { ContentType } from "../types"; +import { extractFirstParagraph } from "./contentExtractor"; + +/** + * 生成摘要 + */ +export function generateSummary(contentType: ContentType) { + const description = document + .querySelector('meta[name="description"]') + ?.getAttribute("content"); + + if (description) return description; + + // 根据内容类型生成不同摘要 + switch (contentType) { + case ContentType.ARTICLE: + return extractFirstParagraph(); + case ContentType.CODE: + return "代码页面,包含多个代码片段"; + case ContentType.MULTIMEDIA: + return "多媒体内容页面"; + case ContentType.DATA: + return "数据表格页面"; + default: + return document.title; + } +} + + + + +/** + * 创建页面预览(降级方案) + */ +export function createPagePreview() { + // 创建一个简化的页面预览 + const canvas = document.createElement("canvas"); + canvas.width = 400; + canvas.height = 300; + + const ctx = canvas.getContext("2d"); + if (!ctx) return ""; + + // 白色背景 + ctx.fillStyle = "#ffffff"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // 页面标题 + ctx.fillStyle = "#333333"; + ctx.font = "16px Arial"; + ctx.fillText(document.title.slice(0, 30), 20, 40); + + // URL + ctx.fillStyle = "#666666"; + ctx.font = "12px Arial"; + ctx.fillText(location.hostname, 20, 70); + + // 内容预览 + const contentPreview = + document.body.textContent?.slice(0, 200) || "No content"; + ctx.fillStyle = "#444444"; + ctx.font = "10px Arial"; + + const lines = contentPreview.split("\n").slice(0, 5); + lines.forEach((line, index) => { + ctx.fillText(line.slice(0, 50), 20, 100 + index * 15); + }); + + // 时间戳 + ctx.fillStyle = "#999999"; + ctx.font = "10px Arial"; + ctx.fillText(new Date().toLocaleString(), 20, 270); + + return canvas.toDataURL("image/png"); +} \ No newline at end of file diff --git a/browser-extension/MineAssistant/src/utils/index.ts b/browser-extension/MineAssistant/src/utils/index.ts new file mode 100644 index 0000000..70173a4 --- /dev/null +++ b/browser-extension/MineAssistant/src/utils/index.ts @@ -0,0 +1,5 @@ +export * from "./contentExtractor" +export * from "./contentDetector" +export * from "./contentAnalyzer" +export * from "./contentSummarizer" +export * from "./common" \ No newline at end of file From 33539828c6428447f5bf5e29690d882e7a12c7d7 Mon Sep 17 00:00:00 2001 From: down <37741552+Facefall@users.noreply.github.com> Date: Mon, 8 Dec 2025 00:06:21 +0800 Subject: [PATCH 5/6] refactor: refactor request part; setting application init queue --- browser-extension/MineAssistant/package.json | 3 + .../MineAssistant/pnpm-lock.yaml | 191 ++++++++++++++ .../MineAssistant/src/background/index.ts | 10 +- .../MineAssistant/src/constants/api.ts | 11 +- .../src/content/contentManager.ts | 35 ++- .../MineAssistant/src/content/index.ts | 78 +++--- .../MineAssistant/src/manifest.json | 3 +- .../MineAssistant/src/services/api.ts | 0 .../src/services/context/index.ts | 48 ++++ .../src/services/core/apiClient.ts | 249 ++++++++++++++++++ .../MineAssistant/src/services/index.ts | 11 + .../MineAssistant/src/services/screenshot.ts | 0 .../src/services/screenshot/index.ts | 41 +++ .../MineAssistant/src/services/sync/index.ts | 1 - .../src/services/system/index.ts | 48 ++++ .../MineAssistant/src/services/types.ts | 170 ++++++++++++ .../MineAssistant/src/storage/index.ts | 70 ++++- .../MineAssistant/src/storage/settings.ts | 20 +- 18 files changed, 928 insertions(+), 61 deletions(-) delete mode 100644 browser-extension/MineAssistant/src/services/api.ts create mode 100644 browser-extension/MineAssistant/src/services/context/index.ts create mode 100644 browser-extension/MineAssistant/src/services/core/apiClient.ts delete mode 100644 browser-extension/MineAssistant/src/services/screenshot.ts create mode 100644 browser-extension/MineAssistant/src/services/screenshot/index.ts delete mode 100644 browser-extension/MineAssistant/src/services/sync/index.ts create mode 100644 browser-extension/MineAssistant/src/services/system/index.ts create mode 100644 browser-extension/MineAssistant/src/services/types.ts diff --git a/browser-extension/MineAssistant/package.json b/browser-extension/MineAssistant/package.json index fe9b8c8..de57f23 100644 --- a/browser-extension/MineAssistant/package.json +++ b/browser-extension/MineAssistant/package.json @@ -14,5 +14,8 @@ "vite": "^5.0.0", "vite-plugin-web-extension": "^4.0.0", "webextension-polyfill": "^0.10.0" + }, + "dependencies": { + "axios": "^1.13.2" } } diff --git a/browser-extension/MineAssistant/pnpm-lock.yaml b/browser-extension/MineAssistant/pnpm-lock.yaml index 619f71d..86a71a8 100644 --- a/browser-extension/MineAssistant/pnpm-lock.yaml +++ b/browser-extension/MineAssistant/pnpm-lock.yaml @@ -7,6 +7,10 @@ settings: importers: .: + dependencies: + axios: + specifier: ^1.13.2 + version: 1.13.2 devDependencies: '@types/webextension-polyfill': specifier: ^0.10.0 @@ -370,6 +374,9 @@ packages: async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + atomic-sleep@1.0.0: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} @@ -377,6 +384,9 @@ packages: atomically@2.1.0: resolution: {integrity: sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q==} + axios@1.13.2: + resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -396,6 +406,10 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + camelcase@8.0.0: resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} engines: {node: '>=16'} @@ -416,6 +430,10 @@ packages: resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} engines: {node: '>=10'} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commander@2.9.0: resolution: {integrity: sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A==} engines: {node: '>= 0.6.x'} @@ -479,6 +497,10 @@ packages: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} @@ -496,6 +518,10 @@ packages: resolution: {integrity: sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==} engines: {node: '>=18'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -509,6 +535,22 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + es6-error@4.1.1: resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} @@ -540,6 +582,19 @@ packages: engines: {node: '>=18'} hasBin: true + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + fs-extra@10.1.0: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} @@ -553,6 +608,9 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + fx-runner@1.4.0: resolution: {integrity: sha512-rci1g6U0rdTg6bAaBboP7XdRu01dzTAaKXxFf+PUqGuCv6Xu7o8NZdY1D5MvKGIjb6EdS1g3VlXOgksir1uGkg==} hasBin: true @@ -561,6 +619,14 @@ packages: resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} engines: {node: '>=18'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} @@ -568,6 +634,10 @@ packages: resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} engines: {node: '>=18'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.10: resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} @@ -580,6 +650,18 @@ packages: growly@1.3.0: resolution: {integrity: sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + html-escaper@3.0.3: resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} @@ -722,9 +804,21 @@ packages: marky@1.3.0: resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + md5@2.3.0: resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -802,6 +896,9 @@ packages: proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + pupa@3.3.0: resolution: {integrity: sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA==} engines: {node: '>=12.20'} @@ -1280,6 +1377,8 @@ snapshots: async@3.2.6: {} + asynckit@0.4.0: {} + atomic-sleep@1.0.0: {} atomically@2.1.0: @@ -1287,6 +1386,14 @@ snapshots: stubborn-fs: 2.0.0 when-exit: 2.1.5 + axios@1.13.2: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + balanced-match@1.0.2: {} bluebird@3.7.2: {} @@ -1311,6 +1418,11 @@ snapshots: buffer-from@1.1.2: {} + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + camelcase@8.0.0: {} chalk@5.6.2: {} @@ -1328,6 +1440,10 @@ snapshots: cli-boxes@3.0.0: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@2.9.0: dependencies: graceful-readlink: 1.0.1 @@ -1383,6 +1499,8 @@ snapshots: deep-extend@0.6.0: {} + delayed-stream@1.0.0: {} + dom-serializer@2.0.0: dependencies: domelementtype: 2.3.0 @@ -1405,6 +1523,12 @@ snapshots: dependencies: type-fest: 4.41.0 + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + emoji-regex@10.6.0: {} emoji-regex@8.0.0: {} @@ -1415,6 +1539,21 @@ snapshots: dependencies: is-arrayish: 0.2.1 + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + es6-error@4.1.1: {} esbuild@0.21.5: @@ -1461,6 +1600,16 @@ snapshots: minimist: 1.2.8 xml2js: 0.6.2 + follow-redirects@1.15.11: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + fs-extra@10.1.0: dependencies: graceful-fs: 4.2.11 @@ -1476,6 +1625,8 @@ snapshots: fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + fx-runner@1.4.0: dependencies: commander: 2.9.0 @@ -1487,12 +1638,32 @@ snapshots: get-east-asian-width@1.4.0: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + glob-to-regexp@0.4.1: {} global-directory@4.0.1: dependencies: ini: 4.1.1 + gopd@1.2.0: {} + graceful-fs@4.2.10: {} graceful-fs@4.2.11: {} @@ -1501,6 +1672,16 @@ snapshots: growly@1.3.0: {} + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + html-escaper@3.0.3: {} htmlparser2@8.0.2: @@ -1619,12 +1800,20 @@ snapshots: marky@1.3.0: {} + math-intrinsics@1.1.0: {} + md5@2.3.0: dependencies: charenc: 0.0.2 crypt: 0.0.2 is-buffer: 1.1.6 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -1716,6 +1905,8 @@ snapshots: proto-list@1.2.4: {} + proxy-from-env@1.1.0: {} + pupa@3.3.0: dependencies: escape-goat: 4.0.0 diff --git a/browser-extension/MineAssistant/src/background/index.ts b/browser-extension/MineAssistant/src/background/index.ts index fbc948d..bea36aa 100644 --- a/browser-extension/MineAssistant/src/background/index.ts +++ b/browser-extension/MineAssistant/src/background/index.ts @@ -1,6 +1,7 @@ import { BrowserContextExtractor } from "../content/BrowserContextExtractor"; -import { BrowserContext, ExtensionState, MessageTypeEnum } from "../types"; +import { BrowserContext, ExtensionState, ExtractionMode, MessageTypeEnum } from "../types"; import browser from 'webextension-polyfill'; +import { detectContentType } from "../utils"; /** * 内容脚本主类 @@ -142,8 +143,8 @@ class BackgroundScript { /** * 捕获上下文 */ - private async captureContext(mode: 'smart' | 'basic' = 'smart'): Promise { - const contentType = this.extractor.detectContentType(); + private async captureContext(mode: ExtractionMode = ExtractionMode.SMART): Promise { + const contentType = detectContentType(); if (mode === 'basic') { // 基础模式:只提取基本页面信息 return { @@ -155,7 +156,8 @@ class BackgroundScript { structuredContent: { title: document.title, contentType: contentType - } + }, + extractionMode: mode }; } diff --git a/browser-extension/MineAssistant/src/constants/api.ts b/browser-extension/MineAssistant/src/constants/api.ts index 22ef6d6..f9b1744 100644 --- a/browser-extension/MineAssistant/src/constants/api.ts +++ b/browser-extension/MineAssistant/src/constants/api.ts @@ -1,6 +1,5 @@ -export const API_ENDPOINT = { - SYNC_CONTEXTS: '/api/contexts/sync', - HEALTH_CHECK: '/api/health' -} - -export const API_TIMEOUT = 30000; // 30秒 +// API 请求路径 +export const PATH = { + UPLOAD_CONTEXT: '/context/upload', + SCREENSHOT_UPLOAD: '/screenshot/upload', +} as const; \ No newline at end of file diff --git a/browser-extension/MineAssistant/src/content/contentManager.ts b/browser-extension/MineAssistant/src/content/contentManager.ts index 93909d4..d406d42 100644 --- a/browser-extension/MineAssistant/src/content/contentManager.ts +++ b/browser-extension/MineAssistant/src/content/contentManager.ts @@ -1,7 +1,7 @@ import browser from 'webextension-polyfill'; import { ExtensionState, ExtensionSettings, MessageTypeEnum } from '../types'; import { DEFAULT_SETTINGS, DEFAULT_STATE } from '../constants'; - +import storageManager from '../storage'; export class PopupManager { private state: ExtensionState = { ...DEFAULT_STATE }; @@ -20,9 +20,7 @@ export class PopupManager { console.log("initialize popup manager", browser.storage); try { // 从存储中加载状态 - const result = await browser.storage.local.get(['extensionState']); - - const savedState = result.extensionState; + const savedState = await storageManager.settingsManager.getSettings(); if (savedState) { this.state = { @@ -416,4 +414,33 @@ export class PopupManager { public isReady(): boolean { return this.isInitialized; } +} + +let popupManager: PopupManager | null; + + +export async function initializePopupManager(): Promise { + try { + // 创建popup管理器实例 + popupManager = new PopupManager(); + + // 等待初始化完成 + const waitForInit = (): Promise => { + return new Promise((resolve) => { + const checkReady = () => { + if (popupManager && popupManager.isReady()) { + resolve(); + } else { + setTimeout(checkReady, 100); + } + }; + checkReady(); + }); + }; + + await waitForInit(); + console.log('Popup application initialized successfully', popupManager); + } catch (error) { + console.error('Failed to initialize popup application:', error); + } } \ No newline at end of file diff --git a/browser-extension/MineAssistant/src/content/index.ts b/browser-extension/MineAssistant/src/content/index.ts index 5afa392..51becba 100644 --- a/browser-extension/MineAssistant/src/content/index.ts +++ b/browser-extension/MineAssistant/src/content/index.ts @@ -1,42 +1,58 @@ -import { PopupManager } from './contentManager'; +import { initializePopupManager } from './contentManager'; +import { initializeApiClient } from '../services/core/apiClient'; +import { initializeStorage } from '../storage'; -let popupManager: PopupManager | null = null; +// 初始化状态跟踪 +const initializationState = { + storage: false, + apiClient: false, + popupManager: false, + allComplete: false +}; -/** - * 初始化popup应用 - */ -async function initializePopup(): Promise { + +// 初始化队列 - 按顺序定义初始化函数 +const initQueue = [ + initializeStorage, + initializeApiClient, + initializePopupManager +]; + +// 执行初始化队列 +async function executeInitQueue(queue: Array<() => Promise>): Promise { + for (const [index, initFunction] of queue.entries()) { + const stepName = initFunction.name || `step_${index + 1}`; + try { + console.log(`[Initialization] Executing ${stepName} (${index + 1}/${queue.length})`); + await initFunction(); + console.log(`[Initialization] Completed ${stepName}`); + } catch (error) { + console.error(`[Initialization] Failed to execute ${stepName}:`, error); + console.log(`[Initialization] Continuing with remaining initialization steps...`); + } + } +} + +async function main() { try { - // 创建popup管理器实例 - popupManager = new PopupManager(); - - // 等待初始化完成 - const waitForInit = (): Promise => { - return new Promise((resolve) => { - const checkReady = () => { - if (popupManager && popupManager.isReady()) { - resolve(); - } else { - setTimeout(checkReady, 100); - } - }; - checkReady(); - }); - }; - - await waitForInit(); - console.log('Popup application initialized successfully'); + console.log('[Initialization] Starting main initialization sequence with queue...'); + + await executeInitQueue(initQueue); + + initializationState.allComplete = true; + console.log('[Initialization] All components initialized successfully!'); } catch (error) { - console.error('Failed to initialize popup application:', error); + console.error('[Initialization] Main initialization sequence failed:', error); } } -// 页面加载完成后初始化 if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initializePopup); + document.addEventListener('DOMContentLoaded', async () => { + await main(); + }); } else { - initializePopup(); + main(); } -// 导出popup管理器供外部使用(如测试) -export { popupManager }; \ No newline at end of file +export { initializationState }; + diff --git a/browser-extension/MineAssistant/src/manifest.json b/browser-extension/MineAssistant/src/manifest.json index 7cee5e7..6a17678 100644 --- a/browser-extension/MineAssistant/src/manifest.json +++ b/browser-extension/MineAssistant/src/manifest.json @@ -27,7 +27,8 @@ "permissions": [ "storage", "tabs", - "activeTab" + "activeTab", + "webRequest" ], "host_permissions": [ "" diff --git a/browser-extension/MineAssistant/src/services/api.ts b/browser-extension/MineAssistant/src/services/api.ts deleted file mode 100644 index e69de29..0000000 diff --git a/browser-extension/MineAssistant/src/services/context/index.ts b/browser-extension/MineAssistant/src/services/context/index.ts new file mode 100644 index 0000000..612b080 --- /dev/null +++ b/browser-extension/MineAssistant/src/services/context/index.ts @@ -0,0 +1,48 @@ +import { PATH } from '../../constants'; +import { getRequest } from '../core/apiClient'; +import { ContextUploadRequest, ContextUploadResponse } from '../types'; + +// 获取请求实例 +const request = getRequest(); + +/** + * 上下文相关API服务 + */ +export const contextService = { + /** + * 上传上下文 + */ + uploadContext: async (context: ContextUploadRequest['context']): Promise => { + return request.post(PATH.UPLOAD_CONTEXT, { context }); + }, + + /** + * 获取上下文列表 + */ + // getContexts: async (page: number = 1, limit: number = 20): Promise => { + // return request.get(PATH.CONTEXT_LIST, { params: { page, limit } }); + // }, + + /** + * 搜索上下文 + */ + // searchContexts: async (searchRequest: SearchRequest): Promise => { + // return request.post(PATH.SEARCH_CONTEXT, searchRequest); + // }, + + /** + * 删除上下文 + */ + // deleteContext: async (contextId: string): Promise<{ success: boolean }> => { + // return request.delete<{ success: boolean }>(`${PATH.CONTEXT_DELETE}/${contextId}`); + // }, + + /** + * 获取上下文详情 + */ + // getContextDetail: async (contextId: string): Promise => { + // return request.get(`${PATH.CONTEXT_DETAIL}/${contextId}`); + // }, +}; + +export default contextService; diff --git a/browser-extension/MineAssistant/src/services/core/apiClient.ts b/browser-extension/MineAssistant/src/services/core/apiClient.ts new file mode 100644 index 0000000..553380f --- /dev/null +++ b/browser-extension/MineAssistant/src/services/core/apiClient.ts @@ -0,0 +1,249 @@ +import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; +import { DEFAULT_SETTINGS } from '../../constants'; +import { getStorageManager } from '../../storage'; + +/** + * API客户端管理器 - 使用单例模式确保只有一个实例 + */ +class ApiClientManager { + private static instance: ApiClientManager; + private apiClient: AxiosInstance; + private settingsManager: any = null; + private isBackendUrlUpdating: boolean = false; + private lastBackendUrl: string = DEFAULT_SETTINGS.backendUrl; + private isInitialized: boolean = false; + private initializationPromise: Promise | null = null; + + private constructor() { + // 私有构造函数,防止外部直接实例化 + this.apiClient = axios.create({ + baseURL: this.lastBackendUrl, + timeout: 10000, + headers: { + 'Content-Type': 'application/json', + } + }); + + this.setupInterceptors(); + } + + /** + * 获取单例实例 + */ + public static getInstance(): ApiClientManager { + if (!ApiClientManager.instance) { + ApiClientManager.instance = new ApiClientManager(); + } + return ApiClientManager.instance; + } + + /** + * 设置拦截器 + */ + private setupInterceptors(): void { + // 请求拦截器 + this.apiClient.interceptors.request.use( + async (config) => { + // 检查 backendUrl 是否正在更新 + if (this.isBackendUrlUpdating) { + const error = new Error('Backend URL is currently updating, request blocked'); + console.warn('[API] Request blocked:', error.message); + return Promise.reject(error); + } + + // 检查是否需要更新 baseURL + const currentBackendUrl = await this.getBackendUrl(); + if (currentBackendUrl !== this.lastBackendUrl) { + const error = new Error('Backend URL has changed, please update and retry'); + console.warn('[API] Request blocked - URL changed:', { + lastUrl: this.lastBackendUrl, + currentUrl: currentBackendUrl + }); + return Promise.reject(error); + } + + console.log(`[API] ${config.method?.toUpperCase()} ${config.url}`, { + params: config.params, + data: config.data, + baseURL: config.baseURL + }); + return config; + }, + (error) => { + console.error('[API] Request error:', error); + return Promise.reject(error); + } + ); + + // 响应拦截器 + this.apiClient.interceptors.response.use( + (response) => { + console.log(`[API] Success: ${response.config.method?.toUpperCase()} ${response.config.url}`, { + status: response.status, + data: response.data + }); + return response; + }, + (error) => { + console.error('[API] Response error:', { + url: error.config?.url, + status: error.response?.status, + message: error.message, + data: error.response?.data + }); + return Promise.reject(error); + } + ); + } + + /** + * 确保获取到设置管理器 + */ + private async getSettingsManager(): Promise { + if (!this.settingsManager) { + const storageManager = await getStorageManager(); + this.settingsManager = storageManager.settingsManager; + } + return this.settingsManager; + } + + /** + * 获取当前 backendUrl + */ + private async getBackendUrl(): Promise { + try { + const manager = await this.getSettingsManager(); + const settings = await manager.getSettings(); + return settings.backendUrl; + } catch (error) { + console.error('Failed to get backendUrl from settings:', error); + return DEFAULT_SETTINGS.backendUrl; + } + } + + /** + * 更新 axios baseURL + */ + private async updateAxiosBaseUrl(): Promise { + const newBackendUrl = await this.getBackendUrl(); + const apiBaseUrl = `${newBackendUrl}/api`; + + if (this.apiClient.defaults.baseURL !== apiBaseUrl) { + this.apiClient.defaults.baseURL = apiBaseUrl; + console.log(`[API] Updated baseURL to: ${apiBaseUrl}`); + } + + this.lastBackendUrl = newBackendUrl; + this.isBackendUrlUpdating = false; + } + + /** + * 初始化API客户端 + */ + public async initialize(): Promise { + // 防止重复初始化 + if (this.isInitialized) { + return; + } + + // 如果已经有初始化过程在进行,等待其完成 + if (this.initializationPromise) { + return this.initializationPromise; + } + + // 创建初始化promise + this.initializationPromise = (async () => { + try { + console.log('[API] Initializing API client...'); + + // 确保 storage 已初始化 + const storageManager = await getStorageManager(); + + this.isBackendUrlUpdating = true; + await this.updateAxiosBaseUrl(); + + // 设置设置变化监听器 + await this.setupSettingsListener(); + + this.isInitialized = true; + console.log('[API] API client initialization completed'); + } catch (error) { + console.error('[API] Failed to initialize API client:', error); + throw error; + } finally { + this.initializationPromise = null; + } + })(); + + return this.initializationPromise; + } + + /** + * 监听设置变化 + */ + private async setupSettingsListener(): Promise { + try { + const manager = await this.getSettingsManager(); + manager.onSettingsChanged((newSettings: any) => { + console.log('[API] Settings changed, updating backendUrl...', newSettings); + this.updateBackendUrl(); + }); + } catch (error) { + console.error('[API] Failed to setup settings listener:', error); + } + } + + /** + * 更新后端URL + */ + public async updateBackendUrl(): Promise { + this.isBackendUrlUpdating = true; + await this.updateAxiosBaseUrl(); + } + + /** + * 获取axios实例(谨慎使用) + */ + public getAxiosInstance(): AxiosInstance { + return this.apiClient; + } + + /** + * 检查是否已初始化 + */ + public getIsInitialized(): boolean { + return this.isInitialized; + } + + /** + * 通用请求方法 + */ + public request = { + get: (url: string, config?: AxiosRequestConfig) => + this.apiClient.get(url, config).then(res => res.data), + + post: (url: string, data?: any, config?: AxiosRequestConfig) => + this.apiClient.post(url, data, config).then(res => res.data), + + put: (url: string, data?: any, config?: AxiosRequestConfig) => + this.apiClient.put(url, data, config).then(res => res.data), + + delete: (url: string, config?: AxiosRequestConfig) => + this.apiClient.delete(url, config).then(res => res.data), + }; +} + +// 导出单例实例的方法和请求接口 +const apiClientManager = ApiClientManager.getInstance(); + +export const initializeApiClient = async () => { + return apiClientManager.initialize(); +}; + +export const updateBackendUrl = async () => { + return apiClientManager.updateBackendUrl(); +}; + +export const getRequest = (): typeof apiClientManager.request => { + return apiClientManager.request; +}; diff --git a/browser-extension/MineAssistant/src/services/index.ts b/browser-extension/MineAssistant/src/services/index.ts index e69de29..f204055 100644 --- a/browser-extension/MineAssistant/src/services/index.ts +++ b/browser-extension/MineAssistant/src/services/index.ts @@ -0,0 +1,11 @@ +// Core API client +export { initializeApiClient, updateBackendUrl } from './core/apiClient'; + +// Business services +export { contextService } from './context'; +export { screenshotService } from './screenshot'; +export { systemService } from './system'; + +// Types +export * from './types'; + diff --git a/browser-extension/MineAssistant/src/services/screenshot.ts b/browser-extension/MineAssistant/src/services/screenshot.ts deleted file mode 100644 index e69de29..0000000 diff --git a/browser-extension/MineAssistant/src/services/screenshot/index.ts b/browser-extension/MineAssistant/src/services/screenshot/index.ts new file mode 100644 index 0000000..2711fe8 --- /dev/null +++ b/browser-extension/MineAssistant/src/services/screenshot/index.ts @@ -0,0 +1,41 @@ +import { PATH } from '../../constants'; +import { getRequest } from '../core/apiClient'; +import { ApiResponse } from '../types'; + +// 获取请求实例 +const request = getRequest(); + +/** + * 截图相关API服务 + */ +export const screenshotService = { + /** + * 上传截图 + */ + uploadScreenshot: async (screenshotData: { data: string; metadata?: any }): Promise => { + return request.post(PATH.SCREENSHOT_UPLOAD, screenshotData); + }, + + // /** + // * 获取截图列表 + // */ + // getScreenshots: async (page: number = 1, limit: number = 20): Promise => { + // return request.get(PATH.SCREENSHOT_LIST, { params: { page, limit } }); + // }, + + // /** + // * 删除截图 + // */ + // deleteScreenshot: async (screenshotId: string): Promise => { + // return request.delete(`${PATH.SCREENSHOT_DELETE}/${screenshotId}`); + // }, + + // /** + // * 分析截图内容 + // */ + // analyzeScreenshot: async (screenshotId: string, prompt?: string): Promise => { + // return request.post(`${PATH.SCREENSHOT_ANALYZE}/${screenshotId}`, { prompt }); + // }, +}; + +export default screenshotService; diff --git a/browser-extension/MineAssistant/src/services/sync/index.ts b/browser-extension/MineAssistant/src/services/sync/index.ts deleted file mode 100644 index 30071a2..0000000 --- a/browser-extension/MineAssistant/src/services/sync/index.ts +++ /dev/null @@ -1 +0,0 @@ -// 负责同步逻辑 diff --git a/browser-extension/MineAssistant/src/services/system/index.ts b/browser-extension/MineAssistant/src/services/system/index.ts new file mode 100644 index 0000000..2762344 --- /dev/null +++ b/browser-extension/MineAssistant/src/services/system/index.ts @@ -0,0 +1,48 @@ +// import { PATH } from '../../constants'; +// import { getRequest } from '../core/apiClient'; +// import { HealthCheckResponse, StatisticsResponse, ConfigSyncResponse } from '../types'; + +// 获取请求实例 +// const request = getRequest(); + +/** + * 系统相关API服务 + */ +export const systemService = { + // /** + // * 健康检查 + // */ + // healthCheck: async (): Promise => { + // return request.get(PATH.HEALTH_CHECK); + // }, + + // /** + // * 获取统计信息 + // */ + // getStatistics: async (): Promise => { + // return request.get(PATH.STATISTICS); + // }, + + // /** + // * 同步配置 + // */ + // syncConfig: async (): Promise => { + // return request.get(PATH.SYNC_CONFIG); + // }, + + // /** + // * 测试连接 + // */ + // testConnection: async (): Promise<{ success: boolean; latency?: number }> => { + // const startTime = Date.now(); + // try { + // await request.get(PATH.TEST_CONNECTION); + // const latency = Date.now() - startTime; + // return { success: true, latency }; + // } catch (error) { + // return { success: false }; + // } + // }, +}; + +export default systemService; diff --git a/browser-extension/MineAssistant/src/services/types.ts b/browser-extension/MineAssistant/src/services/types.ts new file mode 100644 index 0000000..3ea2978 --- /dev/null +++ b/browser-extension/MineAssistant/src/services/types.ts @@ -0,0 +1,170 @@ +// API服务类型定义 + +/** + * HTTP方法类型 + */ +export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; + +/** + * 请求配置 + */ +export interface RequestConfig { + baseURL: string; + timeout?: number; + headers?: Record; +} + +/** + * 请求选项 + */ +export interface RequestOptions { + method?: HttpMethod; + headers?: Record; + data?: any; + timeout?: number; +} + +/** + * API响应格式 + */ +export interface ApiResponse { + success: boolean; + data?: T; + message?: string; + error?: string; + timestamp: number; +} + +/** + * 健康检查响应 + */ +export interface HealthCheckResponse { + status: 'healthy' | 'unhealthy'; + version: string; + uptime: number; + services: { + storage: boolean; + llm: boolean; + embedding: boolean; + }; +} + +/** + * 上下文相关类型 + */ +export interface ContextUploadRequest { + context: { + id?: string; + title: string; + content: string; + url?: string; + type: 'article' | 'code' | 'multimedia' | 'data'; + metadata?: Record; + timestamp: number; + }; +} + +export interface ContextUploadResponse { + id: string; + processed: boolean; + embeddingsGenerated: boolean; + processingTime: number; + chunks?: number; +} + +export interface ContextListResponse { + contexts: Array<{ + id: string; + title: string; + content: string; + url?: string; + type: string; + timestamp: number; + size: number; + }>; + total: number; + page: number; + pages: number; +} + +/** + * 搜索相关类型 + */ +export interface SearchRequest { + query: string; + limit?: number; + filters?: { + type?: string; + dateRange?: { + start: number; + end: number; + }; + }; +} + +export interface SearchResult { + context: { + id: string; + title: string; + content: string; + url?: string; + type: string; + timestamp: number; + }; + score: number; + highlights: string[]; +} + +/** + * 统计信息类型 + */ +export interface StatisticsResponse { + totalContexts: number; + totalSize: number; + storageUsed: number; + storageLimit: number; + lastSyncTime?: number; +} + +/** + * 配置同步类型 + */ +export interface ConfigSyncResponse { + settings: Record; + prompts: Record; + version: string; +} + +/** + * 错误类型 + */ +export class ApiError extends Error { + constructor( + message: string, + public statusCode?: number, + public code?: string + ) { + super(message); + this.name = 'ApiError'; + } +} + +/** + * 网络错误类型 + */ +export class NetworkError extends Error { + constructor(message: string, public originalError?: any) { + super(message); + this.name = 'NetworkError'; + } +} + +/** + * 超时错误类型 + */ +export class TimeoutError extends Error { + constructor(message: string = 'Request timeout') { + super(message); + this.name = 'TimeoutError'; + } +} \ No newline at end of file diff --git a/browser-extension/MineAssistant/src/storage/index.ts b/browser-extension/MineAssistant/src/storage/index.ts index cffd6f0..6256288 100644 --- a/browser-extension/MineAssistant/src/storage/index.ts +++ b/browser-extension/MineAssistant/src/storage/index.ts @@ -1,13 +1,14 @@ -import { BrowserContext, ExtensionSettings } from "../types"; +import { BrowserContext, ExtensionState } from "../types"; import { SettingsManager } from './settings'; import { ContextManager } from './context'; export class StorageManager { private static instance: StorageManager; - private settingsManager: SettingsManager; + public settingsManager: SettingsManager; private contextManager: ContextManager; + private isInitialized = false; constructor() { this.settingsManager = SettingsManager.getInstance(); @@ -21,44 +22,105 @@ export class StorageManager { return this.instance; } + // 初始化函数 - 确保在使用前完成初始化 + async initialize(): Promise { + if (this.isInitialized) { + console.log('[Storage] Already initialized'); + return; + } + + try { + console.log('[Storage] Initializing...'); + + // 初始化设置管理器 + await this.settingsManager.getSettings(); + + // 初始化上下文管理器 + await this.contextManager.getContexts(); + + this.isInitialized = true; + console.log('[Storage] Initialization completed'); + } catch (error) { + console.error('[Storage] Initialization failed:', error); + throw error; + } + } + + // 检查是否已初始化 + isReady(): boolean { + return this.isInitialized; + } + + // 确保已初始化的装饰器 + private ensureInitialized() { + if (!this.isInitialized) { + throw new Error('StorageManager is not initialized. Call initialize() first.'); + } + } + // 设置管理 - async getSettings(): Promise { + async getSettings(): Promise { + this.ensureInitialized(); return await this.settingsManager.getSettings(); } - async saveSettings(settings: ExtensionSettings) { + async saveSettings(settings: ExtensionState) { + this.ensureInitialized(); return await this.settingsManager.saveSettings(settings); } // 上下文管理 async getContexts() { + this.ensureInitialized(); return await this.contextManager.getContexts(); } async addContext(context: BrowserContext) { + this.ensureInitialized(); return await this.contextManager.addContext(context); } async removeContextById(contextId: string) { + this.ensureInitialized(); return await this.contextManager.removeContextById(contextId); } async removeContext(url: string, timestamp: number) { + this.ensureInitialized(); return await this.contextManager.removeContext(url, timestamp); } async clearContexts() { + this.ensureInitialized(); return await this.contextManager.clearContexts(); } async getRecentContexts(count: number) { + this.ensureInitialized(); return await this.contextManager.getRecentContexts(count); } async getRecentContextsByDomain(domain: string) { + this.ensureInitialized(); return await this.contextManager.getContextsByDomain(domain); } } const storageManager = StorageManager.getInstance(); +export const initializeStorage = async (): Promise => { + await storageManager.initialize(); + return storageManager; +}; + +// 便捷的获取已初始化实例的方式 +let initializedInstance: StorageManager | null = null; + +export const getStorageManager = async (): Promise => { + if (!initializedInstance) { + initializedInstance = await initializeStorage(); + } + return initializedInstance; +}; + +// 保持向后兼容的默认导出(但不推荐直接使用) export default storageManager; diff --git a/browser-extension/MineAssistant/src/storage/settings.ts b/browser-extension/MineAssistant/src/storage/settings.ts index f2dd2d3..0996d60 100644 --- a/browser-extension/MineAssistant/src/storage/settings.ts +++ b/browser-extension/MineAssistant/src/storage/settings.ts @@ -1,7 +1,7 @@ import browser from 'webextension-polyfill'; -import { ExtensionSettings } from "../types"; -import { STORAGE_KEYS, DEFAULT_SETTINGS } from "../constants"; +import { ExtensionSettings, ExtensionState } from "../types"; +import { STORAGE_KEYS, DEFAULT_SETTINGS, DEFAULT_STATE } from "../constants"; export class SettingsManager { private static instance: SettingsManager; @@ -14,26 +14,26 @@ export class SettingsManager { } // 获取设置 - async getSettings(): Promise { + async getSettings(): Promise { try { const result = await browser.storage.local.get(STORAGE_KEYS.SETTINGS); - const settings = result[STORAGE_KEYS.SETTINGS] as ExtensionSettings; + const settings = result[STORAGE_KEYS.SETTINGS] as ExtensionState; if (!settings) { // 如果没有设置,使用默认值 - await this.saveSettings(DEFAULT_SETTINGS); - return DEFAULT_SETTINGS; + await this.saveSettings(DEFAULT_STATE); + return DEFAULT_STATE; } return settings; } catch (error) { console.error('Failed to get settings:', error); - return DEFAULT_SETTINGS; + return DEFAULT_STATE; } } // 保存设置 - async saveSettings(settings: ExtensionSettings): Promise { + async saveSettings(settings: ExtensionState): Promise { try { // 验证设置 const validatedSettings = this.validateSettings(settings); @@ -66,7 +66,7 @@ export class SettingsManager { // 考虑是否用 react, arco-design 提供的组件,支持控件输入源头的validation // 优点: 使用简单 // 缺点: 体积变大; 重构项目为 react 项目 - private validateSettings(settings: ExtensionSettings): ExtensionSettings { + private validateSettings(settings: ExtensionState): ExtensionState { const validated = { ...settings }; return validated; @@ -74,7 +74,7 @@ export class SettingsManager { // 重置为默认设置 async resetToDefaults(): Promise { - return await this.saveSettings(DEFAULT_SETTINGS); + return await this.saveSettings(DEFAULT_STATE); } // 监听设置变化 From 15227d2b34e89569976f624091dbd52fbaf9457c Mon Sep 17 00:00:00 2001 From: down <37741552+Facefall@users.noreply.github.com> Date: Mon, 8 Dec 2025 03:09:07 +0800 Subject: [PATCH 6/6] feat: mvp 0.1 version --- .../MineAssistant/src/background/index.ts | 280 +++++------------- .../MineAssistant/src/constants/storage.ts | 17 +- .../MineAssistant/src/content/index.ts | 11 +- .../MineAssistant/src/popup/index.html | 2 +- .../popupManager.ts} | 86 ++++-- .../src/services/core/apiClient.ts | 2 +- .../MineAssistant/src/services/index.ts | 2 +- .../MineAssistant/src/storage/index.ts | 4 +- .../MineAssistant/src/storage/settings.ts | 15 +- .../MineAssistant/src/types/message.ts | 22 +- 10 files changed, 174 insertions(+), 267 deletions(-) rename browser-extension/MineAssistant/src/{content/contentManager.ts => popup/popupManager.ts} (86%) diff --git a/browser-extension/MineAssistant/src/background/index.ts b/browser-extension/MineAssistant/src/background/index.ts index bea36aa..d853565 100644 --- a/browser-extension/MineAssistant/src/background/index.ts +++ b/browser-extension/MineAssistant/src/background/index.ts @@ -1,225 +1,101 @@ -import { BrowserContextExtractor } from "../content/BrowserContextExtractor"; -import { BrowserContext, ExtensionState, ExtractionMode, MessageTypeEnum } from "../types"; -import browser from 'webextension-polyfill'; -import { detectContentType } from "../utils"; - -/** - * 内容脚本主类 - * 负责与页面交互,捕获上下文内容 - */ -class BackgroundScript { - private extractor: BrowserContextExtractor; - private isActive: boolean = false; - private observer: MutationObserver | null = null; - - constructor() { - this.extractor = new BrowserContextExtractor(); - } +import browser from "webextension-polyfill"; +import { MessageTypeEnum } from "../types"; + +// 计时器状态 +let timerState = { + isActive: false, + intervalId: null as number | null, + captureInterval: 2000, // 默认30秒 + lastCaptureTime: 0 +}; + +browser.runtime.onMessage.addListener((message, sender, sendResponse: (data?: any) => void) => { + if (message.type === MessageTypeEnum.START_RECODING) { + // 开始计时 + const { isActive, captureInterval } = message.data || {}; + console.log('start recording', message.data, timerState); + // 更新计时状态 + timerState.isActive = isActive !== undefined ? isActive : true; + + if (captureInterval) { + timerState.captureInterval = captureInterval; + } - /** - * 初始化内容脚本 - */ - async init() { - try { - await this.initMessageListener(); - // await this.initObserver(); - this.isActive = true; - console.log('MineAssistant Content Script initialized successfully'); - } catch (error) { - console.error('Failed to initialize content script:', error); + // 清除现有计时器 + if (timerState.intervalId !== null) { + clearInterval(timerState.intervalId); + timerState.intervalId = null; } - } - /** - * 初始化消息监听器 - */ - private async initMessageListener() { - // 添加消息监听器 - browser.runtime.onMessage.addListener((message, sender, sendResponse) => { - this.handleMessage(message, sender, sendResponse); - return true; // 保持异步响应 - }); - } + // 如果需要激活计时器,设置新的计时器 + if (timerState.isActive) { + console.log(`[Timer] Starting timer with interval: ${timerState.captureInterval}ms`); - /** - * 处理接收到的消息 - */ - - private async handleMessage(message: any, sender: browser.Runtime.MessageSender, sendResponse: (response?: any) => void) { - console.log('Received message:', message); - try { - switch (message.type) { - case MessageTypeEnum.CAPTURE_CONTEXT: - const context = await this.captureContext(message.data?.mode || 'smart'); - sendResponse({ success: true, context }); - break; - case MessageTypeEnum.GET_STATE: - sendResponse({ success: true, active: this.isActive }); - break; - case MessageTypeEnum.ANALYZE_PAGE_TYPE: - const _context = await this.extractor.extractContext(); - sendResponse({ success: true, pageType: _context.metadata.contentType }); - break; - case MessageTypeEnum.GET_METADATA: - const metadata = await this.getMetadata(); - sendResponse({ success: true, metadata }); - break; - case MessageTypeEnum.UPDATE_STATE: - await this.updateState(message); - sendResponse({ success: true }); - break; - default: - sendResponse({ success: false, error: 'Unknown message type' }); - } - } catch (error) { - console.error('Error handling message:', error); - sendResponse({ success: false, error: error instanceof Error ? error.message : 'Unknown error' }); + // 设置定时捕获 + timerState.intervalId = setInterval(() => { + performTimedCapture(); + }, timerState.captureInterval) as unknown as number; } - } - private async updateState(state: ExtensionState) { - setTimeout(() => { - browser.runtime.sendMessage({ - type: MessageTypeEnum.UPDATE_STATE, - data: { 11: 11, ...state } - }); - }, 5000); + sendResponse({ success: true, isActive: timerState.isActive }); } - /** - * 初始化页面变化观察器 - * todo: 实现页面变化观察器的逻辑 - */ - private async initObserver() { - // 监听页面变化 - this.observer = new MutationObserver((mutations) => { - if (this.isActive) { - this.handlePageChange(mutations); - } - }); - - this.observer.observe(document.body, { - childList: true, - subtree: true, - characterData: true, - attributes: true - }); - } - - /** - * 处理页面变化 - */ - private async handlePageChange(mutations: MutationRecord[]) { - // 检查是否有实质性变化 - const hasSignificantChange = mutations.some(mutation => { - // 检查是否是内容变化 - if (mutation.type === 'characterData' || mutation.type === 'childList') { - return true; - } - - // 检查是否是重要属性变化 - if (mutation.type === 'attributes') { - const importantAttributes = ['title', 'src', 'href', 'content']; - return importantAttributes.includes(mutation.attributeName || ''); - } - - return false; - }); + if (message.type === MessageTypeEnum.UI_SAVE_SETTINGS) { + // 更新计时配置 + const { settings } = message.data || {}; - if (hasSignificantChange) { - console.log('Page content changed, notifying background script'); + if (settings && settings.captureInterval) { + timerState.captureInterval = settings.captureInterval; - // 通知background script页面内容已更新 - browser.runtime.sendMessage({ - type: MessageTypeEnum.PAGE_CONTENT_UPDATED, - url: location.href, - title: document.title - }).catch(error => { - console.log('Failed to send page update notification:', error); - }); - } - } + // 如果计时器正在运行,重新设置计时器 + if (timerState.isActive && timerState.intervalId !== null) { + clearInterval(timerState.intervalId); + timerState.intervalId = setInterval(() => { + performTimedCapture(); + }, timerState.captureInterval) as unknown as number; - /** - * 捕获上下文 - */ - private async captureContext(mode: ExtractionMode = ExtractionMode.SMART): Promise { - const contentType = detectContentType(); - if (mode === 'basic') { - // 基础模式:只提取基本页面信息 - return { - id: `${location.href}_${Date.now()}`, - url: location.href, - title: document.title, - timestamp: Date.now(), - metadata: this.extractor.extractMetadata(contentType), - structuredContent: { - title: document.title, - contentType: contentType - }, - extractionMode: mode - }; + console.log(`[Timer] Updated capture interval to: ${timerState.captureInterval}ms`); + } } - // 智能模式:使用完整的内容提取器 - return await this.extractor.extractContext(); + sendResponse({ success: true }); } - /** - * 提取内容 - */ - private async extractContent(options?: any): Promise { - const context = await this.extractor.extractContext(); + if (message.type === MessageTypeEnum.ON_TIME_CAPTURE) { + // 执行定时捕获 + performTimedCapture(); + sendResponse({ success: true }); + } - // 根据选项过滤内容 - if (options?.contentType) { - return this.filterContentByType(context, options.contentType); - } + return true; // 保持消息通道开放以支持异步响应 +}); - return context; +/** + * 执行定时捕获 + */ +function performTimedCapture(): void { + console.log('performTimedCapture', timerState, performTimedCapture); + const now = Date.now(); + + // 防止过于频繁的捕获 + if (now - timerState.lastCaptureTime < timerState.captureInterval / 2) { + console.log('[Timer] Skipping capture - too soon since last capture'); + return; } - /** - * 获取页面元数据 - */ - private async getMetadata(): Promise { - const context = await this.extractor.extractContext(); - return context.metadata; - } + timerState.lastCaptureTime = now; - /** - * 根据类型过滤内容 - */ - private filterContentByType(context: BrowserContext, contentType: string): any { - switch (contentType) { - case 'article': - return context.structuredContent.articleContent; - case 'code': - return context.structuredContent.codeContent; - case 'multimedia': - return context.structuredContent.multimediaContent; - case 'data': - return context.structuredContent.dataContent; - default: - return context; - } - } + console.log('[Timer] Performing timed capture'); - /** - * 清理资源 - */ - destroy() { - if (this.observer) { - this.observer.disconnect(); - this.observer = null; + // 发送捕获通知到background script + browser.runtime.sendMessage({ + type: MessageTypeEnum.ON_TIME_CAPTURE, + data: { + timestamp: now, + url: 'test', + title: 'test title', } - this.isActive = false; - } + }).catch(error => { + console.error('[Timer] Failed to send capture notification:', error); + }); } - -// 初始化内容脚本 -const contentScript = new BackgroundScript(); -contentScript.init(); - -// 导出供测试使用 -export { BackgroundScript }; \ No newline at end of file diff --git a/browser-extension/MineAssistant/src/constants/storage.ts b/browser-extension/MineAssistant/src/constants/storage.ts index a4fe5c1..f535f95 100644 --- a/browser-extension/MineAssistant/src/constants/storage.ts +++ b/browser-extension/MineAssistant/src/constants/storage.ts @@ -1,8 +1,8 @@ export const STORAGE_KEYS = { - SETTINGS: 'mineAssistant_settings', - CONTEXTS: 'mineAssistant_contexts', - SYNC_STATE: 'mineAssistant_sync_state', - LAST_SYNC_TIME: 'mineAssistant_last_sync_time' + SETTINGS: 'MineAssistant_settings', + CONTEXTS: 'MineAssistant_contexts', + SYNC_STATE: 'MineAssistant_sync_state', + LAST_SYNC_TIME: 'MineAssistant_last_sync_time' } as const; export const STORAGE_LIMITS = { @@ -11,4 +11,11 @@ export const STORAGE_LIMITS = { MAX_TOTAL_SIZE: 5 * 1024 * 1024 // 5MB } -export const GET_RECENT_CONTEXTS_LIMIT = 10; \ No newline at end of file +export const GET_RECENT_CONTEXTS_LIMIT = 10; + +export const LOCAL_STORAGE_KEYS = { + SETTINGS: 'MineAssistant_settings', + CONTEXTS: 'MineAssistant_contexts', + SYNC_STATE: 'MineAssistant_sync_state', + LAST_SYNC_TIME: 'MineAssistant_last_sync_time' +} as const; diff --git a/browser-extension/MineAssistant/src/content/index.ts b/browser-extension/MineAssistant/src/content/index.ts index 51becba..cabebf3 100644 --- a/browser-extension/MineAssistant/src/content/index.ts +++ b/browser-extension/MineAssistant/src/content/index.ts @@ -1,4 +1,4 @@ -import { initializePopupManager } from './contentManager'; +import { initializePopupManager } from '../popup/popupManager'; import { initializeApiClient } from '../services/core/apiClient'; import { initializeStorage } from '../storage'; @@ -46,13 +46,8 @@ async function main() { } } -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', async () => { - await main(); - }); -} else { - main(); -} + +main(); export { initializationState }; diff --git a/browser-extension/MineAssistant/src/popup/index.html b/browser-extension/MineAssistant/src/popup/index.html index da57c4d..f365203 100644 --- a/browser-extension/MineAssistant/src/popup/index.html +++ b/browser-extension/MineAssistant/src/popup/index.html @@ -35,7 +35,7 @@

MineAssistant

开始捕获 - diff --git a/browser-extension/MineAssistant/src/content/contentManager.ts b/browser-extension/MineAssistant/src/popup/popupManager.ts similarity index 86% rename from browser-extension/MineAssistant/src/content/contentManager.ts rename to browser-extension/MineAssistant/src/popup/popupManager.ts index d406d42..9e99686 100644 --- a/browser-extension/MineAssistant/src/content/contentManager.ts +++ b/browser-extension/MineAssistant/src/popup/popupManager.ts @@ -3,11 +3,14 @@ import { ExtensionState, ExtensionSettings, MessageTypeEnum } from '../types'; import { DEFAULT_SETTINGS, DEFAULT_STATE } from '../constants'; import storageManager from '../storage'; + +// TODO: 接入 storage 模块 和 api request 模块,移除多余 state code export class PopupManager { private state: ExtensionState = { ...DEFAULT_STATE }; private isLoading = false; private captureMode: 'smart' | 'basic' = 'smart'; private isInitialized = false; + storageManager: any; constructor() { this.initialize(); @@ -20,6 +23,7 @@ export class PopupManager { console.log("initialize popup manager", browser.storage); try { // 从存储中加载状态 + this.storageManager = storageManager; const savedState = await storageManager.settingsManager.getSettings(); if (savedState) { @@ -77,7 +81,6 @@ export class PopupManager { autoCaptureCheckbox.addEventListener('change', (e) => { const target = e?.target as HTMLInputElement; this.updateSettings({ autoCapture: target?.checked || false }); - }); } @@ -138,19 +141,36 @@ export class PopupManager { */ private handleMessage(message: any, sender: browser.Runtime.MessageSender, sendResponse: (response?: any) => void): true | void | Promise { console.log('Received message from background:', message); - if (message.type === MessageTypeEnum.UPDATE_STATE) { - this.state = message.data; - // TODO: - console.log("定时保存 Context 提醒") - } - - if (message.type === 'CONTEXT_CAPTURED') { - this.state = { - ...this.state, - contextCount: this.state.contextCount + 1 - }; - this.updateUI(); - sendResponse({ success: true }); + switch (message.type) { + // 来自 UI 的 action + case MessageTypeEnum.UI_CAPTURE_NOW: + const response = this.handleCaptureNow(); + this.updateUI(); + console.log('[MessageTypeEnum.UI_CAPTURE_NOW] response', response) + sendResponse(response); + break; + case MessageTypeEnum.START_RECODING: + this.state = { + ...this.state, + contextCount: this.state.contextCount + 1 + }; + this.updateUI(); + sendResponse({ success: true }); + break; + case MessageTypeEnum.UI_SAVE_SETTINGS: + this.updateUI(); + sendResponse({ success: true }); + break; + + // 来自 background 的通讯 + case MessageTypeEnum.ON_TIME_CAPTURE: + this.handleCaptureNow(); + break; + case MessageTypeEnum.ON_TIME_UPLOAD: + this.handleSync(); + break; + default: + console.warn(`Unhandled message type: ${message.type}`); } } @@ -159,7 +179,7 @@ export class PopupManager { */ private async saveState(): Promise { try { - await browser.storage.local.set({ extensionState: this.state }); + await this.storageManager.saveSettings(this.state.settings); } catch (error) { console.error('Failed to save extension state:', error); } @@ -176,20 +196,23 @@ export class PopupManager { isActive: !this.state.isActive }; - await this.saveState(); + this.saveState(); this.state = newState; // 通知background script try { - await browser.runtime.sendMessage({ - type: 'TOGGLE_CAPTURE', - data: { isActive: this.state.isActive } + browser.runtime.sendMessage({ + type: MessageTypeEnum.START_RECODING, + data: { isActive: this.state.isActive, captureInterval: this.state.settings.captureInterval } }); } catch (error) { console.error('Failed to send toggle capture message:', error); } + console.log('handleToggleCapture', this.state); this.updateUI(); + this.setLoading(false); + } finally { this.setLoading(false); } @@ -198,7 +221,7 @@ export class PopupManager { /** * 处理立即捕获 */ - private async handleCaptureNow(): Promise { + private async handleCaptureNow() { console.info("Capturing context now..."); this.setLoading(true); @@ -224,9 +247,9 @@ export class PopupManager { contextCount: this.state.contextCount + 1, currentUrl: tab.url || '', }; - await this.saveState(); - this.updateUI(); } + + return response; } } catch (error) { console.error('Failed to capture context:', error); @@ -280,7 +303,8 @@ export class PopupManager { /** * 更新设置 */ - private async updateSettings(newSettings: Partial): Promise { + private async updateSettings(newSettings: Partial) { + console.log('updateSettings', newSettings); const updatedSettings = { ...this.state.settings, ...newSettings @@ -296,7 +320,7 @@ export class PopupManager { // 通知background script设置已更新 try { await browser.runtime.sendMessage({ - type: MessageTypeEnum.UPDATE_STATE, + type: MessageTypeEnum.UI_SAVE_SETTINGS, data: { settings: updatedSettings } }); } catch (error) { @@ -353,15 +377,13 @@ export class PopupManager { toggleButton.disabled = this.isLoading; } - const captureButton = document.getElementById('capture-now-btn') as HTMLButtonElement; - if (captureButton) { - captureButton.disabled = this.isLoading || !this.state.isActive; - } + // const captureButton = document.getElementById('capture-now-btn') as HTMLButtonElement; - const syncButton = document.getElementById('sync-data-btn') as HTMLButtonElement; - if (syncButton) { - syncButton.disabled = this.isLoading; - } + + // const syncButton = document.getElementById('sync-data-btn') as HTMLButtonElement; + // if (syncButton) { + // syncButton.disabled = this.isLoading; + // } // 更新设置输入框 const autoCaptureCheckbox = document.getElementById('auto-capture-checkbox') as HTMLInputElement; diff --git a/browser-extension/MineAssistant/src/services/core/apiClient.ts b/browser-extension/MineAssistant/src/services/core/apiClient.ts index 553380f..f74a31a 100644 --- a/browser-extension/MineAssistant/src/services/core/apiClient.ts +++ b/browser-extension/MineAssistant/src/services/core/apiClient.ts @@ -5,7 +5,7 @@ import { getStorageManager } from '../../storage'; /** * API客户端管理器 - 使用单例模式确保只有一个实例 */ -class ApiClientManager { +export class ApiClientManager { private static instance: ApiClientManager; private apiClient: AxiosInstance; private settingsManager: any = null; diff --git a/browser-extension/MineAssistant/src/services/index.ts b/browser-extension/MineAssistant/src/services/index.ts index f204055..35de2fb 100644 --- a/browser-extension/MineAssistant/src/services/index.ts +++ b/browser-extension/MineAssistant/src/services/index.ts @@ -1,5 +1,5 @@ // Core API client -export { initializeApiClient, updateBackendUrl } from './core/apiClient'; +export { initializeApiClient, updateBackendUrl, getRequest } from './core/apiClient'; // Business services export { contextService } from './context'; diff --git a/browser-extension/MineAssistant/src/storage/index.ts b/browser-extension/MineAssistant/src/storage/index.ts index 6256288..6303bd4 100644 --- a/browser-extension/MineAssistant/src/storage/index.ts +++ b/browser-extension/MineAssistant/src/storage/index.ts @@ -6,8 +6,10 @@ import { ContextManager } from './context'; export class StorageManager { private static instance: StorageManager; + public settingsManager: SettingsManager; private contextManager: ContextManager; + private isInitialized = false; constructor() { @@ -36,7 +38,7 @@ export class StorageManager { await this.settingsManager.getSettings(); // 初始化上下文管理器 - await this.contextManager.getContexts(); + // await this.contextManager.getContexts(); this.isInitialized = true; console.log('[Storage] Initialization completed'); diff --git a/browser-extension/MineAssistant/src/storage/settings.ts b/browser-extension/MineAssistant/src/storage/settings.ts index 0996d60..2a6bdee 100644 --- a/browser-extension/MineAssistant/src/storage/settings.ts +++ b/browser-extension/MineAssistant/src/storage/settings.ts @@ -21,7 +21,6 @@ export class SettingsManager { if (!settings) { // 如果没有设置,使用默认值 - await this.saveSettings(DEFAULT_STATE); return DEFAULT_STATE; } @@ -33,13 +32,21 @@ export class SettingsManager { } // 保存设置 - async saveSettings(settings: ExtensionState): Promise { + async saveSettings(settings: Partial): Promise { try { // 验证设置 const validatedSettings = this.validateSettings(settings); + const originalSetting = await this.getSettings(); await browser.storage.local.set({ - [STORAGE_KEYS.SETTINGS]: validatedSettings + [STORAGE_KEYS.SETTINGS]: { + ...originalSetting, + ...validatedSettings, + settings: { + ...originalSetting.settings, + ...validatedSettings.settings, + } + } }); return true; @@ -66,7 +73,7 @@ export class SettingsManager { // 考虑是否用 react, arco-design 提供的组件,支持控件输入源头的validation // 优点: 使用简单 // 缺点: 体积变大; 重构项目为 react 项目 - private validateSettings(settings: ExtensionState): ExtensionState { + private validateSettings(settings: Partial) { const validated = { ...settings }; return validated; diff --git a/browser-extension/MineAssistant/src/types/message.ts b/browser-extension/MineAssistant/src/types/message.ts index 9e3cc76..05fc71e 100644 --- a/browser-extension/MineAssistant/src/types/message.ts +++ b/browser-extension/MineAssistant/src/types/message.ts @@ -1,19 +1,17 @@ // 消息类型枚举 export enum MessageTypeEnum { - // 捕获相关 - CAPTURE_CONTEXT = 'CAPTURE_CONTEXT', - PAGE_CONTENT_UPDATED = 'PAGE_CONTENT_UPDATED', + //todo: 是否要这么设计 当插件配置 ready 时,发出信号, storage 的插件值允许读取 + // SETTING_READY = 'SETTING_READY', - // 截图实现放在 popup 即可, 需要考虑实现 - CAPTURE_SCREENSHOT = 'CAPTURE_SCREENSHOT', + // 对应 UI 操作 + UI_START_RECORDING = 'UI_START_RECORDING', + UI_CAPTURE_NOW = 'UI_CAPTURE_NOW', + UI_SAVE_SETTINGS = 'UI_SAVE_SETTINGS', - GET_STATE = 'GET_STATE', - - ANALYZE_PAGE_TYPE = 'ANALYZE_PAGE_TYPE', - - GET_METADATA = 'GET_METADATA', - - UPDATE_STATE = 'UPDATE_SETTINGS', + // 对应 background 后台操作 + START_RECODING = 'START_RECODING',// 开始计时 + ON_TIME_CAPTURE = 'ON_TIME_CAPTURE', + ON_TIME_UPLOAD = 'ON_TIME_UPLOAD', } export type MessageType = keyof typeof MessageTypeEnum;