diff --git a/package-lock.json b/package-lock.json index 43009ba..668f853 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,18 @@ "@modelcontextprotocol/sdk": "^1.27.1", "puppeteer-core": "^24.39.1", "puppeteer-extra": "^3.3.6", - "puppeteer-extra-plugin-stealth": "^2.11.2" + "puppeteer-extra-plugin-stealth": "^2.11.2", + "sharp": "^0.34.5" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@hono/node-server": { @@ -27,6 +38,471 @@ "hono": "^4" } }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.27.1", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz", @@ -622,6 +1098,15 @@ "node": ">= 0.8" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/devtools-protocol": { "version": "0.0.1581282", "resolved": "https://mirrors.cloud.tencent.com/npm/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz", @@ -2043,6 +2528,50 @@ "node": ">=0.10.0" } }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index 2d69362..3eb13da 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@modelcontextprotocol/sdk": "^1.27.1", "puppeteer-core": "^24.39.1", "puppeteer-extra": "^3.3.6", - "puppeteer-extra-plugin-stealth": "^2.11.2" + "puppeteer-extra-plugin-stealth": "^2.11.2", + "sharp": "^0.34.5" } } diff --git a/src/assets/bg_48.png b/src/assets/bg_48.png new file mode 100644 index 0000000..ed11f16 Binary files /dev/null and b/src/assets/bg_48.png differ diff --git a/src/assets/bg_96.png b/src/assets/bg_96.png new file mode 100644 index 0000000..6a73b58 Binary files /dev/null and b/src/assets/bg_96.png differ diff --git a/src/gemini-ops.js b/src/gemini-ops.js index a3633a8..2354553 100644 --- a/src/gemini-ops.js +++ b/src/gemini-ops.js @@ -9,6 +9,7 @@ import { createOperator } from './operator.js'; import { sleep } from './util.js'; import config from './config.js'; import { mkdirSync } from 'node:fs'; +import { removeWatermarkFromFile, removeWatermarkFromDataUrl } from './watermark-remover.js'; // ── Gemini 页面元素选择器 ── const SELECTORS = { @@ -658,6 +659,18 @@ export function createOps(page) { const dataUrl = `data:${mime};base64,${base64Full}`; console.log(`[extractImageBase64] ✅ CDP 提取成功 (mime=${mime}, size=${(base64Full.length * 0.75 / 1024).toFixed(1)}KB)`); + + // 去水印处理 + const wmResult = await removeWatermarkFromDataUrl(dataUrl); + if (wmResult.ok && !wmResult.skipped) { + console.log(`[extractImageBase64] 🍌 水印已移除 (${wmResult.width}×${wmResult.height}, logo=${wmResult.logoSize}px)`); + return { ok: true, dataUrl: wmResult.dataUrl, method: 'cdp' }; + } else if (wmResult.skipped) { + console.log(`[extractImageBase64] 跳过去水印: ${wmResult.reason}`); + } else { + console.warn(`[extractImageBase64] 去水印失败(不影响提取结果): ${wmResult.error}`); + } + return { ok: true, dataUrl, method: 'cdp' }; } catch (err) { const errMsg = err.message || String(err); @@ -833,6 +846,16 @@ export function createOps(page) { } } + // 去水印处理 + const wmResult = await removeWatermarkFromFile(filePath); + if (wmResult.ok && !wmResult.skipped) { + console.log(`[ops] 水印已移除 (${wmResult.width}×${wmResult.height}, logo=${wmResult.logoSize}px)`); + } else if (wmResult.skipped) { + console.log(`[ops] 跳过去水印: ${wmResult.reason}`); + } else { + console.warn(`[ops] 去水印失败(不影响下载结果): ${wmResult.error}`); + } + return { ok: true, filePath, diff --git a/src/watermark-remover.js b/src/watermark-remover.js new file mode 100644 index 0000000..10eed68 --- /dev/null +++ b/src/watermark-remover.js @@ -0,0 +1,287 @@ +/** + * watermark-remover.js — Gemini 图片水印移除 + * + * 基于反向 Alpha 混合算法,精确还原被 Gemini 添加水印的图片。 + * + * 算法移植自 gemini-watermark-remover(by journey-ad / Jad) + * 原始仓库:https://github.com/journey-ad/gemini-watermark-remover + * 许可证:MIT - Copyright (c) 2025 Jad + * + * 原理: + * Gemini 水印叠加公式: watermarked = α × 255 + (1 - α) × original + * 反向求解: original = (watermarked - α × 255) / (1 - α) + */ +import sharp from 'sharp'; +import { readFileSync } from 'node:fs'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +// ── 常量 ── +const ALPHA_THRESHOLD = 0.002; // 忽略极小的 alpha 值(噪声) +const MAX_ALPHA = 0.99; // 避免除以接近零的值 +const LOGO_VALUE = 255; // 白色水印的颜色值 + +// ── Alpha Map 缓存 ── +const alphaMapCache = {}; + +/** + * 从水印背景捕获图中计算 Alpha Map + * @param {Buffer} pngBuffer - 水印背景捕获图的 PNG 数据 + * @param {number} size - 水印尺寸(48 或 96) + * @returns {Promise} alpha 值数组(0.0 ~ 1.0) + */ +async function calculateAlphaMap(pngBuffer, size) { + const { data, info } = await sharp(pngBuffer) + .resize(size, size) + .raw() + .ensureAlpha() + .toBuffer({ resolveWithObject: true }); + + const pixelCount = info.width * info.height; + const alphaMap = new Float32Array(pixelCount); + const channels = info.channels; // 4 (RGBA) + + for (let i = 0; i < pixelCount; i++) { + const idx = i * channels; + const r = data[idx]; + const g = data[idx + 1]; + const b = data[idx + 2]; + // 取 RGB 三通道最大值归一化 + alphaMap[i] = Math.max(r, g, b) / 255.0; + } + + return alphaMap; +} + +/** + * 获取指定尺寸的 Alpha Map(带缓存) + * @param {number} size - 48 或 96 + * @returns {Promise} + */ +async function getAlphaMap(size) { + if (alphaMapCache[size]) return alphaMapCache[size]; + + const bgFile = size === 48 ? 'bg_48.png' : 'bg_96.png'; + const bgPath = join(__dirname, 'assets', bgFile); + const bgBuffer = readFileSync(bgPath); + + const alphaMap = await calculateAlphaMap(bgBuffer, size); + alphaMapCache[size] = alphaMap; + return alphaMap; +} + +/** + * 根据图片尺寸检测水印配置 + * @param {number} width - 图片宽度 + * @param {number} height - 图片高度 + * @returns {{ logoSize: number, marginRight: number, marginBottom: number }} + */ +function detectWatermarkConfig(width, height) { + // Gemini 规则:宽高都 > 1024 用 96×96,否则用 48×48 + if (width > 1024 && height > 1024) { + return { logoSize: 96, marginRight: 64, marginBottom: 64 }; + } + return { logoSize: 48, marginRight: 32, marginBottom: 32 }; +} + +/** + * 计算水印在图片中的位置(固定右下角) + * @param {number} imgWidth + * @param {number} imgHeight + * @param {{ logoSize: number, marginRight: number, marginBottom: number }} config + * @returns {{ x: number, y: number, width: number, height: number }} + */ +function calculateWatermarkPosition(imgWidth, imgHeight, config) { + const { logoSize, marginRight, marginBottom } = config; + return { + x: imgWidth - marginRight - logoSize, + y: imgHeight - marginBottom - logoSize, + width: logoSize, + height: logoSize, + }; +} + +/** + * 对原始像素数据执行反向 Alpha 混合,移除水印 + * + * @param {Buffer} pixels - RGBA 原始像素 Buffer(会被原地修改) + * @param {number} imgWidth - 图片宽度 + * @param {Float32Array} alphaMap - Alpha 通道数据 + * @param {{ x: number, y: number, width: number, height: number }} position - 水印位置 + */ +function removeWatermarkPixels(pixels, imgWidth, alphaMap, position) { + const { x, y, width, height } = position; + + for (let row = 0; row < height; row++) { + for (let col = 0; col < width; col++) { + const imgIdx = ((y + row) * imgWidth + (x + col)) * 4; + const alphaIdx = row * width + col; + + let alpha = alphaMap[alphaIdx]; + + // 跳过噪声 + if (alpha < ALPHA_THRESHOLD) continue; + + // 限制 alpha 避免除零 + alpha = Math.min(alpha, MAX_ALPHA); + const oneMinusAlpha = 1.0 - alpha; + + // 对 R / G / B 三通道分别反向混合 + for (let c = 0; c < 3; c++) { + const watermarked = pixels[imgIdx + c]; + const original = (watermarked - alpha * LOGO_VALUE) / oneMinusAlpha; + pixels[imgIdx + c] = Math.max(0, Math.min(255, Math.round(original))); + } + // Alpha 通道不动 + } + } +} + +/** + * 移除图片文件中的 Gemini 水印并覆盖保存 + * + * @param {string} filePath - 图片文件路径(会被原地覆盖) + * @returns {Promise<{ ok: boolean, width?: number, height?: number, logoSize?: number, error?: string }>} + */ +export async function removeWatermarkFromFile(filePath) { + try { + console.log(`[watermark-remover] 开始处理: ${filePath}`); + + // 1. 读取图片原始像素 + const image = sharp(filePath); + const metadata = await image.metadata(); + const { width, height } = metadata; + + if (!width || !height) { + return { ok: false, error: 'invalid_image_metadata' }; + } + + // 2. 检测水印配置 + const config = detectWatermarkConfig(width, height); + const position = calculateWatermarkPosition(width, height, config); + + // 校验水印位置合法性 + if (position.x < 0 || position.y < 0) { + console.log(`[watermark-remover] 图片太小(${width}×${height}),跳过去水印`); + return { ok: true, width, height, skipped: true, reason: 'image_too_small' }; + } + + // 3. 获取 Alpha Map + const alphaMap = await getAlphaMap(config.logoSize); + + // 4. 提取原始像素、执行反向混合 + const { data: pixels, info } = await sharp(filePath) + .raw() + .ensureAlpha() + .toBuffer({ resolveWithObject: true }); + + removeWatermarkPixels(pixels, info.width, alphaMap, position); + + // 5. 写回文件(保持原格式) + const ext = (filePath.match(/\.(\w+)$/)?.[1] || 'png').toLowerCase(); + let outputPipeline = sharp(pixels, { + raw: { width: info.width, height: info.height, channels: info.channels }, + }); + + switch (ext) { + case 'jpg': + case 'jpeg': + outputPipeline = outputPipeline.jpeg({ quality: 95 }); + break; + case 'webp': + outputPipeline = outputPipeline.webp({ quality: 95 }); + break; + default: + outputPipeline = outputPipeline.png(); + break; + } + + await outputPipeline.toFile(filePath); + + console.log(`[watermark-remover] ✅ 去水印完成: ${width}×${height}, logo=${config.logoSize}px`); + return { ok: true, width, height, logoSize: config.logoSize }; + } catch (err) { + console.error(`[watermark-remover] ❌ 去水印失败: ${err.message}`); + return { ok: false, error: err.message }; + } +} + +/** + * 移除 base64 图片数据中的 Gemini 水印 + * + * @param {string} dataUrl - data:image/xxx;base64,... 格式的图片 + * @returns {Promise<{ ok: boolean, dataUrl?: string, width?: number, height?: number, logoSize?: number, error?: string }>} + */ +export async function removeWatermarkFromDataUrl(dataUrl) { + try { + console.log('[watermark-remover] 开始处理 base64 图片'); + + // 1. 解析 dataUrl + const mimeMatch = dataUrl.match(/^data:(image\/\w+);base64,/); + if (!mimeMatch) { + return { ok: false, error: 'invalid_data_url' }; + } + const mime = mimeMatch[1]; + const base64Data = dataUrl.slice(mimeMatch[0].length); + const inputBuffer = Buffer.from(base64Data, 'base64'); + + // 2. 读取图片信息 + const metadata = await sharp(inputBuffer).metadata(); + const { width, height } = metadata; + + if (!width || !height) { + return { ok: false, error: 'invalid_image_metadata' }; + } + + // 3. 检测水印配置 + const config = detectWatermarkConfig(width, height); + const position = calculateWatermarkPosition(width, height, config); + + if (position.x < 0 || position.y < 0) { + console.log(`[watermark-remover] 图片太小(${width}×${height}),跳过去水印`); + return { ok: true, dataUrl, width, height, skipped: true, reason: 'image_too_small' }; + } + + // 4. 获取 Alpha Map + const alphaMap = await getAlphaMap(config.logoSize); + + // 5. 提取像素、反向混合 + const { data: pixels, info } = await sharp(inputBuffer) + .raw() + .ensureAlpha() + .toBuffer({ resolveWithObject: true }); + + removeWatermarkPixels(pixels, info.width, alphaMap, position); + + // 6. 编码回原格式 + const ext = mime.split('/')[1]; + let outputPipeline = sharp(pixels, { + raw: { width: info.width, height: info.height, channels: info.channels }, + }); + + switch (ext) { + case 'jpeg': + case 'jpg': + outputPipeline = outputPipeline.jpeg({ quality: 95 }); + break; + case 'webp': + outputPipeline = outputPipeline.webp({ quality: 95 }); + break; + default: + outputPipeline = outputPipeline.png(); + break; + } + + const outputBuffer = await outputPipeline.toBuffer(); + const outputBase64 = outputBuffer.toString('base64'); + const outputDataUrl = `data:${mime};base64,${outputBase64}`; + + console.log(`[watermark-remover] ✅ base64 去水印完成: ${width}×${height}, logo=${config.logoSize}px`); + return { ok: true, dataUrl: outputDataUrl, width, height, logoSize: config.logoSize }; + } catch (err) { + console.error(`[watermark-remover] ❌ base64 去水印失败: ${err.message}`); + return { ok: false, error: err.message }; + } +}