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..de57f23 --- /dev/null +++ b/browser-extension/MineAssistant/package.json @@ -0,0 +1,21 @@ +{ + "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" + }, + "dependencies": { + "axios": "^1.13.2" + } +} diff --git a/browser-extension/MineAssistant/pnpm-lock.yaml b/browser-extension/MineAssistant/pnpm-lock.yaml new file mode 100644 index 0000000..86a71a8 --- /dev/null +++ b/browser-extension/MineAssistant/pnpm-lock.yaml @@ -0,0 +1,2199 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + axios: + specifier: ^1.13.2 + version: 1.13.2 + 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==} + + 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'} + + 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==} + + 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==} + + 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'} + + 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'} + + 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'} + + 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'} + + 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==} + + 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'} + + 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==} + + 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==} + + 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==} + + 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 + + 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'} + + 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] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + 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'} + + 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==} + + global-directory@4.0.1: + 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==} + + 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==} + + 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==} + + 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==} + + 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==} + + 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==} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + 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: {} + + asynckit@0.4.0: {} + + atomic-sleep@1.0.0: {} + + atomically@2.1.0: + dependencies: + 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: {} + + 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: {} + + 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: {} + + 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: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.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: {} + + delayed-stream@1.0.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 + + 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: {} + + entities@4.5.0: {} + + error-ex@1.3.4: + 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: + 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 + + 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 + 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 + + function-bind@1.1.2: {} + + 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: {} + + 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: {} + + graceful-readlink@1.0.1: {} + + 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: + 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: {} + + 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 + + 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: {} + + proxy-from-env@1.1.0: {} + + 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 0000000..53e9353 Binary files /dev/null and b/browser-extension/MineAssistant/public/icon/128.png differ diff --git a/browser-extension/MineAssistant/public/icon/16.png b/browser-extension/MineAssistant/public/icon/16.png new file mode 100644 index 0000000..3fe3674 Binary files /dev/null and b/browser-extension/MineAssistant/public/icon/16.png differ diff --git a/browser-extension/MineAssistant/public/icon/32.png b/browser-extension/MineAssistant/public/icon/32.png new file mode 100644 index 0000000..d1063e5 Binary files /dev/null and b/browser-extension/MineAssistant/public/icon/32.png differ diff --git a/browser-extension/MineAssistant/public/icon/48.png b/browser-extension/MineAssistant/public/icon/48.png new file mode 100644 index 0000000..6c5e5e1 Binary files /dev/null and b/browser-extension/MineAssistant/public/icon/48.png differ diff --git a/browser-extension/MineAssistant/public/icon/96.png b/browser-extension/MineAssistant/public/icon/96.png new file mode 100644 index 0000000..c71c80e Binary files /dev/null and b/browser-extension/MineAssistant/public/icon/96.png differ diff --git a/browser-extension/MineAssistant/src/background/index.ts b/browser-extension/MineAssistant/src/background/index.ts new file mode 100644 index 0000000..d853565 --- /dev/null +++ b/browser-extension/MineAssistant/src/background/index.ts @@ -0,0 +1,101 @@ +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; + } + + // 清除现有计时器 + if (timerState.intervalId !== null) { + clearInterval(timerState.intervalId); + timerState.intervalId = null; + } + + // 如果需要激活计时器,设置新的计时器 + if (timerState.isActive) { + console.log(`[Timer] Starting timer with interval: ${timerState.captureInterval}ms`); + + // 设置定时捕获 + timerState.intervalId = setInterval(() => { + performTimedCapture(); + }, timerState.captureInterval) as unknown as number; + } + + sendResponse({ success: true, isActive: timerState.isActive }); + } + + if (message.type === MessageTypeEnum.UI_SAVE_SETTINGS) { + // 更新计时配置 + const { settings } = message.data || {}; + + if (settings && settings.captureInterval) { + timerState.captureInterval = settings.captureInterval; + + // 如果计时器正在运行,重新设置计时器 + if (timerState.isActive && timerState.intervalId !== null) { + clearInterval(timerState.intervalId); + timerState.intervalId = setInterval(() => { + performTimedCapture(); + }, timerState.captureInterval) as unknown as number; + + console.log(`[Timer] Updated capture interval to: ${timerState.captureInterval}ms`); + } + } + + sendResponse({ success: true }); + } + + if (message.type === MessageTypeEnum.ON_TIME_CAPTURE) { + // 执行定时捕获 + performTimedCapture(); + sendResponse({ success: true }); + } + + return true; // 保持消息通道开放以支持异步响应 +}); + +/** + * 执行定时捕获 + */ +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; + } + + timerState.lastCaptureTime = now; + + console.log('[Timer] Performing timed capture'); + + // 发送捕获通知到background script + browser.runtime.sendMessage({ + type: MessageTypeEnum.ON_TIME_CAPTURE, + data: { + timestamp: now, + url: 'test', + title: 'test title', + } + }).catch(error => { + console.error('[Timer] Failed to send capture notification:', error); + }); +} diff --git a/browser-extension/MineAssistant/src/constants/api.ts b/browser-extension/MineAssistant/src/constants/api.ts new file mode 100644 index 0000000..f9b1744 --- /dev/null +++ b/browser-extension/MineAssistant/src/constants/api.ts @@ -0,0 +1,5 @@ +// 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/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..f535f95 --- /dev/null +++ b/browser-extension/MineAssistant/src/constants/storage.ts @@ -0,0 +1,21 @@ +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; + +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/BrowserContextExtractor.ts b/browser-extension/MineAssistant/src/content/BrowserContextExtractor.ts new file mode 100644 index 0000000..b99eea4 --- /dev/null +++ b/browser-extension/MineAssistant/src/content/BrowserContextExtractor.ts @@ -0,0 +1,299 @@ +import { + BrowserContext, + ContentType, + ExtractionMode, + PageMetadata, + StructuredContent, + ArticleContent, + CodeContent, + MultimediaContent, + DataContent, +} from "../types"; +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( + mode: ExtractionMode = ExtractionMode.SMART + ): Promise { + const contentType = detectContentType(); + const metadata = this.extractMetadata(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: generateContextId(), + url: location.href, + title: document.title, + timestamp: Date.now(), + metadata, + structuredContent, + rawDom, + screenshot, + extractionMode: mode, + }; + } + + /** + * 提取页面元数据 + */ + public extractMetadata(contentType?: ContentType): PageMetadata { + contentType = contentType || detectContentType(); + + const metaTags = extractMetaTags(); + const wordCount = 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 extractBasicStructuredContent( + contentType: ContentType + ): StructuredContent { + const title = document.title; + const summary = generateSummary(contentType); + + return { + title, + summary, + contentType, + // 基础模式不提取详细的结构化内容 + }; + } + + /** + * 提取结构化内容(智能模式使用) + */ + private extractStructuredContent( + contentType: ContentType + ): StructuredContent { + const title = document.title; + const summary = 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 = extractHeadings(); + const paragraphs = extractParagraphs(); + const images = extractImages(); + + return { + headings, + paragraphs, + images, + }; + } + + /** + * 提取代码内容 + */ + // todo: 完善代码内容提取,包括文件结构分析 + private extractCodeContent(): CodeContent { + const codeBlocks = extractCodeBlocks(); + const languageStats = analyzeCodeLanguages(codeBlocks); + + return { + codeBlocks, + languageStats, + fileStructure: [], + }; + } + + /** + * 提取多媒体内容 - 完善的多媒体内容提取 + */ + private extractMultimediaContent(): MultimediaContent { + const mediaElements = extractMediaElements(); + const duration = estimateMediaDuration(mediaElements); + const transcript = extractTranscript(); + + return { + mediaElements, + duration, + transcript, + }; + } + + /** + * 提取数据内容 - 完善的数据内容提取 + */ + private extractDataContent(): DataContent { + const tables = extractTables(); + const charts = detectCharts(); + const dataPoints = extractDataPoints(); + + return { + tables, + charts, + dataPoints, + }; + } + + + + + + + + /** + * 捕获屏幕截图 + */ + private async captureScreenshot(): Promise { + try { + // 检查是否在浏览器扩展环境中 + if (browser?.tabs?.captureVisibleTab) { + const dataUrl = await browser.tabs.captureVisibleTab(undefined, { + format: "png", + quality: 80, + }); + return dataUrl; + } + + // 检查是否支持 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 createPagePreview(); + } catch (error) { + console.error("截图捕获失败:", error); + return undefined; + } + } + + +} diff --git a/browser-extension/MineAssistant/src/content/index.ts b/browser-extension/MineAssistant/src/content/index.ts new file mode 100644 index 0000000..cabebf3 --- /dev/null +++ b/browser-extension/MineAssistant/src/content/index.ts @@ -0,0 +1,53 @@ +import { initializePopupManager } from '../popup/popupManager'; +import { initializeApiClient } from '../services/core/apiClient'; +import { initializeStorage } from '../storage'; + +// 初始化状态跟踪 +const initializationState = { + storage: false, + apiClient: false, + popupManager: false, + allComplete: false +}; + + +// 初始化队列 - 按顺序定义初始化函数 +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 { + 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('[Initialization] Main initialization sequence failed:', error); + } +} + + +main(); + +export { initializationState }; + diff --git a/browser-extension/MineAssistant/src/manifest.json b/browser-extension/MineAssistant/src/manifest.json new file mode 100644 index 0000000..6a17678 --- /dev/null +++ b/browser-extension/MineAssistant/src/manifest.json @@ -0,0 +1,36 @@ +{ + "{{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/index.html" + }, + "{{chrome}}.content_scripts": [ + { + "matches": [""], + "js": ["src/content/index.ts"] + } + ], + "{{firefox}}.browser_action": { + "default_popup": "src/popup/index.html" + }, + "background": { + "{{chrome}}.service_worker": "src/background/index.ts", + "{{firefox}}.scripts": ["src/background/index.ts"] + }, + "permissions": [ + "storage", + "tabs", + "activeTab", + "webRequest" + ], + "host_permissions": [ + "" + ] +} diff --git a/browser-extension/MineAssistant/src/popup/index.css b/browser-extension/MineAssistant/src/popup/index.css new file mode 100644 index 0000000..c46874e --- /dev/null +++ b/browser-extension/MineAssistant/src/popup/index.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/index.html b/browser-extension/MineAssistant/src/popup/index.html new file mode 100644 index 0000000..f365203 --- /dev/null +++ b/browser-extension/MineAssistant/src/popup/index.html @@ -0,0 +1,99 @@ + + + + + + + MineAssistant + + + + +
+
+

MineAssistant

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

设置

+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + +
+ + + + + \ No newline at end of file diff --git a/browser-extension/MineAssistant/src/popup/popupManager.ts b/browser-extension/MineAssistant/src/popup/popupManager.ts new file mode 100644 index 0000000..9e99686 --- /dev/null +++ b/browser-extension/MineAssistant/src/popup/popupManager.ts @@ -0,0 +1,468 @@ +import browser from 'webextension-polyfill'; +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(); + } + + /** + * 初始化popup管理器 + */ + private async initialize(): Promise { + console.log("initialize popup manager", browser.storage); + try { + // 从存储中加载状态 + this.storageManager = storageManager; + const savedState = await storageManager.settingsManager.getSettings(); + + 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 { + console.log('Received message from background:', message); + 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}`); + } + } + + /** + * 保存状态到存储 + */ + private async saveState(): Promise { + try { + await this.storageManager.saveSettings(this.state.settings); + } 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 + }; + + this.saveState(); + this.state = newState; + + // 通知background script + try { + 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); + } + } + + /** + * 处理立即捕获 + */ + private async handleCaptureNow() { + 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 || '', + }; + } + + return response; + } + } 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) { + console.log('updateSettings', newSettings); + const updatedSettings = { + ...this.state.settings, + ...newSettings + }; + + this.state = { + ...this.state, + settings: updatedSettings + }; + + await this.saveState(); + + // 通知background script设置已更新 + try { + await browser.runtime.sendMessage({ + type: MessageTypeEnum.UI_SAVE_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; + + + // 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; + } +} + +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/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..f74a31a --- /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客户端管理器 - 使用单例模式确保只有一个实例 + */ +export 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 new file mode 100644 index 0000000..35de2fb --- /dev/null +++ b/browser-extension/MineAssistant/src/services/index.ts @@ -0,0 +1,11 @@ +// Core API client +export { initializeApiClient, updateBackendUrl, getRequest } 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/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/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/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..6303bd4 --- /dev/null +++ b/browser-extension/MineAssistant/src/storage/index.ts @@ -0,0 +1,128 @@ + +import { BrowserContext, ExtensionState } from "../types"; + +import { SettingsManager } from './settings'; +import { ContextManager } from './context'; + +export class StorageManager { + private static instance: StorageManager; + + public settingsManager: SettingsManager; + private contextManager: ContextManager; + + private isInitialized = false; + + constructor() { + this.settingsManager = SettingsManager.getInstance(); + this.contextManager = ContextManager.getInstance(); + } + + static getInstance() { + if (!this.instance) { + this.instance = new 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 { + this.ensureInitialized(); + return await this.settingsManager.getSettings(); + } + + 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 new file mode 100644 index 0000000..2a6bdee --- /dev/null +++ b/browser-extension/MineAssistant/src/storage/settings.ts @@ -0,0 +1,105 @@ +import browser from 'webextension-polyfill'; + +import { ExtensionSettings, ExtensionState } from "../types"; +import { STORAGE_KEYS, DEFAULT_SETTINGS, DEFAULT_STATE } 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 ExtensionState; + + if (!settings) { + // 如果没有设置,使用默认值 + return DEFAULT_STATE; + } + + return settings; + } catch (error) { + console.error('Failed to get settings:', error); + return DEFAULT_STATE; + } + } + + // 保存设置 + async saveSettings(settings: Partial): Promise { + try { + // 验证设置 + const validatedSettings = this.validateSettings(settings); + const originalSetting = await this.getSettings(); + + await browser.storage.local.set({ + [STORAGE_KEYS.SETTINGS]: { + ...originalSetting, + ...validatedSettings, + settings: { + ...originalSetting.settings, + ...validatedSettings.settings, + } + } + }); + + 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: Partial) { + const validated = { ...settings }; + + return validated; + } + + // 重置为默认设置 + async resetToDefaults(): Promise { + return await this.saveSettings(DEFAULT_STATE); + } + + // 监听设置变化 + 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 new file mode 100644 index 0000000..792a78b --- /dev/null +++ b/browser-extension/MineAssistant/src/types/browserContext.ts @@ -0,0 +1,145 @@ +export enum ContentType { + ARTICLE = 'article', + CODE = 'code', + MULTIMEDIA = 'multimedia', + DATA = 'data', + UNKNOWN = 'unknown' +} + +export enum ExtractionMode { + SMART = 'smart', // 智能模式 - 提取结构化数据+截屏 + BASIC = 'basic' // 基础模式 - 只提取基础信息+rawContext +} + +// 核心类型定义 +export interface BrowserContext { + id: string; + url: string; + title: string; + timestamp: number; + metadata: PageMetadata; + structuredContent: StructuredContent; + rawDom?: string; // 可选:存储body的innerHTML + screenshot?: string; // 可选:Base64缩略图 + extractionMode: ExtractionMode; // 提取模式 +} + +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; +} diff --git a/browser-extension/MineAssistant/src/types/index.ts b/browser-extension/MineAssistant/src/types/index.ts new file mode 100644 index 0000000..2e31eae --- /dev/null +++ b/browser-extension/MineAssistant/src/types/index.ts @@ -0,0 +1,3 @@ +export * from "./browserContext" +export * from "./message" +export * from "./storage" \ 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..05fc71e --- /dev/null +++ b/browser-extension/MineAssistant/src/types/message.ts @@ -0,0 +1,76 @@ +// 消息类型枚举 +export enum MessageTypeEnum { + //todo: 是否要这么设计 当插件配置 ready 时,发出信号, storage 的插件值允许读取 + // SETTING_READY = 'SETTING_READY', + + // 对应 UI 操作 + UI_START_RECORDING = 'UI_START_RECORDING', + UI_CAPTURE_NOW = 'UI_CAPTURE_NOW', + UI_SAVE_SETTINGS = 'UI_SAVE_SETTINGS', + + // 对应 background 后台操作 + START_RECODING = 'START_RECODING',// 开始计时 + ON_TIME_CAPTURE = 'ON_TIME_CAPTURE', + ON_TIME_UPLOAD = 'ON_TIME_UPLOAD', +} + +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/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 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 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": {