From 120e00b1889153dd4bcbdb89e0c16b2aeaffd6e9 Mon Sep 17 00:00:00 2001 From: WJZ_P <110795301+WJZ-P@users.noreply.github.com> Date: Sat, 21 Mar 2026 20:34:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(gemini-ops):=20=E6=96=B0=E5=A2=9E=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E5=8E=BB=E6=B0=B4=E5=8D=B0=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=96=87=E4=BB=B6=E5=92=8C=20base64=20?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 531 ++++++++++++++++++++++++++++++++++++++- package.json | 3 +- src/assets/bg_48.png | Bin 0 -> 1677 bytes src/assets/bg_96.png | Bin 0 -> 8165 bytes src/gemini-ops.js | 23 ++ src/watermark-remover.js | 287 +++++++++++++++++++++ 6 files changed, 842 insertions(+), 2 deletions(-) create mode 100644 src/assets/bg_48.png create mode 100644 src/assets/bg_96.png create mode 100644 src/watermark-remover.js 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 0000000000000000000000000000000000000000..ed11f16110fa5eb2da90fcf37384db4e1783c054 GIT binary patch literal 1677 zcmV;826Fj{P)c5j5dIeOj97`+ncbZ$vlBbJBlQB7OS-cB2KJ-)4VWr)CnkM#cBOQeDol6hNg;Jd zxiFQ9uqhS*_i#s=O6+*YBZD{q`{COkU_r(hW(FXCMTjW+83ssO|77MjZ{9HT+qZ9_ z#4o}`U?u=eAR^e7B16nf3;@Pj%gk-{T7qtwFA|15di1E@? zKCEW8sh1^;F~*pHLoh|-A3uI9jp1@p#;ZesXdWb{|;K z3wB0^RutPyvl{$2pp;GknjWp-`1m+YQ?(QTl4vsKfXG-2%LGF(uHX5PjL{59JLnG1 z8~SzU%a<>si9sflNtmep&&^e?(_xB0piab5dhXB2eI=4C%bq-W;u9#C07DGd zCRjsE3Z5uh*MSr-z4!#E^gz0*E25FMe5@$vp>L?ktwpPz3VK-_R60$_{@p{@Z1pofQt)9EzJvc|mll-wm(S6B1- zyuMip0wsosn5@g`Ar0!|DU20NgTY`royLB1Pe1_bx}NC?$%((Co^r+mA-|L`Hd_yqt*Tv$sWW_Bq@dyMp?X*!uq8fN-RQrGotHmh$| z$}#-;B0i)&20)S|CnqQW!ha))mW`w+iagJ6Zf+!-BbW>-rm`#>kH>9Rb}#*3q$rA_ zD9Wk?(FRbKWy9fcG#Y(HU%8jmb-i3Jm&@hHkAKqZ*RS`?y#IsL^{V4?_+5pGh|)A2 z4u`|x@Ox3UUayOyST2`URk@q;-QAra8;wTMHTj!LS(bU8mme!cQ3jt9jWI_@M^V`C z8)f%N-^Fi5p69E21%D;lWa{V;m7b%cqqDQKZ(&4XrK+mVuZ;pAs-B2wJRX1Xa=5>& zZ`XOA|9biBjxn1Hb#&!`zXc>o^6J&Ae!t%}`yMu`>v}$)UteGQsZyu8R}pBFwE?vc zlj(Fi7!3AR-jcE`=kxjP?XB9&RzP914d-nakf*1o9Rb))k}T>z6h*OE{0xA}!r%6t zdNxd8veM&f`=EXc8i#4ISQJIk(71;bMX^{cHYHT3J+LV`!j+APe5Ur501*L(=s+q< z7-E$a7K=q$R%-Lxe2C@4)z#JcPYWhkW-D)G*59Lv8aG#lyo$s}hrTp+77X+GySiR= zU}$kBUL36utrpd#01T7k0KS5>_HX9921@k`w*Kq${N0c1^}2(Urp#uux~`o*zJp)W zy)**EPM1e%0jn7DW{!&Ms4M|+b$y-Zc?U^K@;qOyR;1R9SRrmG+T-X;+;;<{@jp}My|9TPa*2u5j+wIsN(GHSkvzbN{t)LSx zTF*tsaR7?kGWZQM>(Z0i5;MSMS#Gj`lAfZ{S+%qL~ znW8IBse&&qE~Fy>3M)i!srxe2#!EiWy3Ua7$EkH>nPk-WAE3M5*V+=D_RV7D6RhI1|t#QHHgQb4} XU4ODwQOM~T00000NkvXXu0mjfF<~++ literal 0 HcmV?d00001 diff --git a/src/assets/bg_96.png b/src/assets/bg_96.png new file mode 100644 index 0000000000000000000000000000000000000000..6a73b5869113103cc6c6ba731bb679ff6bd26ac0 GIT binary patch literal 8165 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 LNkvXXu0mjfkxT%s literal 0 HcmV?d00001 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 }; + } +}