feat(gemini-ops): 新增图片去水印功能,支持文件和 base64 数据处理

This commit is contained in:
WJZ_P
2026-03-21 20:34:03 +08:00
parent a4f7f0433f
commit 120e00b188
6 changed files with 842 additions and 2 deletions

531
package-lock.json generated
View File

@@ -12,7 +12,18 @@
"@modelcontextprotocol/sdk": "^1.27.1", "@modelcontextprotocol/sdk": "^1.27.1",
"puppeteer-core": "^24.39.1", "puppeteer-core": "^24.39.1",
"puppeteer-extra": "^3.3.6", "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": { "node_modules/@hono/node-server": {
@@ -27,6 +38,471 @@
"hono": "^4" "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": { "node_modules/@modelcontextprotocol/sdk": {
"version": "1.27.1", "version": "1.27.1",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz",
@@ -622,6 +1098,15 @@
"node": ">= 0.8" "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": { "node_modules/devtools-protocol": {
"version": "0.0.1581282", "version": "0.0.1581282",
"resolved": "https://mirrors.cloud.tencent.com/npm/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz",
@@ -2043,6 +2528,50 @@
"node": ">=0.10.0" "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": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",

View File

@@ -21,6 +21,7 @@
"@modelcontextprotocol/sdk": "^1.27.1", "@modelcontextprotocol/sdk": "^1.27.1",
"puppeteer-core": "^24.39.1", "puppeteer-core": "^24.39.1",
"puppeteer-extra": "^3.3.6", "puppeteer-extra": "^3.3.6",
"puppeteer-extra-plugin-stealth": "^2.11.2" "puppeteer-extra-plugin-stealth": "^2.11.2",
"sharp": "^0.34.5"
} }
} }

BIN
src/assets/bg_48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
src/assets/bg_96.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

@@ -9,6 +9,7 @@ import { createOperator } from './operator.js';
import { sleep } from './util.js'; import { sleep } from './util.js';
import config from './config.js'; import config from './config.js';
import { mkdirSync } from 'node:fs'; import { mkdirSync } from 'node:fs';
import { removeWatermarkFromFile, removeWatermarkFromDataUrl } from './watermark-remover.js';
// ── Gemini 页面元素选择器 ── // ── Gemini 页面元素选择器 ──
const SELECTORS = { const SELECTORS = {
@@ -658,6 +659,18 @@ export function createOps(page) {
const dataUrl = `data:${mime};base64,${base64Full}`; const dataUrl = `data:${mime};base64,${base64Full}`;
console.log(`[extractImageBase64] ✅ CDP 提取成功 (mime=${mime}, size=${(base64Full.length * 0.75 / 1024).toFixed(1)}KB)`); 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' }; return { ok: true, dataUrl, method: 'cdp' };
} catch (err) { } catch (err) {
const errMsg = err.message || String(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 { return {
ok: true, ok: true,
filePath, filePath,

287
src/watermark-remover.js Normal file
View File

@@ -0,0 +1,287 @@
/**
* watermark-remover.js — Gemini 图片水印移除
*
* 基于反向 Alpha 混合算法,精确还原被 Gemini 添加水印的图片。
*
* 算法移植自 gemini-watermark-removerby 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<Float32Array>} 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<Float32Array>}
*/
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 };
}
}