From b8649e56991c2e7d5adea4a05246295d9459cdb2 Mon Sep 17 00:00:00 2001 From: WJZ_P <110795301+WJZ-P@users.noreply.github.com> Date: Sun, 15 Mar 2026 23:46:55 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E6=B7=BB=E5=8A=A0=20.gitignore=20?= =?UTF-8?q?=E5=B9=B6=E6=9B=B4=E6=96=B0=E4=BE=9D=E8=B5=96=E4=B8=8E=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 + SKILL.md | 24 +- package-lock.json | 1414 ++++++++++++++++++++++++++++++++ package.json | 24 + scripts/gemini_ui_shortcuts.js | 304 ------- src/browser.js | 238 ++++++ src/demo.js | 47 ++ src/gemini-ops.js | 350 ++++++++ src/index.js | 37 + src/operator.js | 326 ++++++++ 10 files changed, 2453 insertions(+), 316 deletions(-) create mode 100644 .gitignore create mode 100644 package-lock.json create mode 100644 package.json delete mode 100644 scripts/gemini_ui_shortcuts.js create mode 100644 src/browser.js create mode 100644 src/demo.js create mode 100644 src/gemini-ops.js create mode 100644 src/index.js create mode 100644 src/operator.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7e4d0b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +*.log +.DS_Store +dist/ +output/ diff --git a/SKILL.md b/SKILL.md index 98431e9..bcbd21b 100644 --- a/SKILL.md +++ b/SKILL.md @@ -12,7 +12,8 @@ description: 通过 Gemini 官网(gemini.google.com)执行问答与生图操 3. 文本问答任务(如"问问Gemini xxx")走 Gemini 文本提问链路。 4. 默认模型:可用列表中最强模型,优先 `Gemini 3.1 Pro`。 5. 执行生图后先向用户回报"正在绘图中",完成后回传图片。 -6. **禁止使用浏览器截图(screenshot)获取生成图片**。默认通过 `extractImageBase64()` 从已渲染的 DOM 直接提取图片 Base64 数据,解码后保存到本地再发送给用户;仅当用户明确要求高清/原图时,才调用 `downloadLatestImage()` 走原图下载流程。 +6. **禁止使用浏览器截图(screenshot)获取生成图片**。默认通过 `ops.extractImageBase64()` 从已渲染的 DOM 直接提取图片 Base64 数据,解码后保存到本地再发送给用户;仅当用户明确要求高清/原图时,才调用 `ops.downloadLatestImage()` 走原图下载流程。 +7. **只调封装好的方法,禁止自己写 `page.evaluate()`**。所有操作通过 `ops.xxx`(高层业务)或 `operator.xxx`(底层原子)完成。底层已全部走 CDP 协议,无需关心实现细节。直接写 evaluate 既浪费 token 又容易出错。 ## 任务分流 @@ -32,7 +33,7 @@ Gemini 页面的操作按钮(`.send-button-container` 内)通过 `aria-label | 发送 / Send | `ready` | 输入框有内容,可发送 | | 停止 / Stop | `loading` | 已发送,正在生成回答 | -可通过 `GeminiOps.getStatus()` 获取当前状态,通过 `GeminiOps.pollStatus()` 分段轮询等待生成完毕。 +可通过 `ops.getStatus()` 获取当前状态,通过 `ops.pollStatus()` 分段轮询等待生成完毕。 ### A. 文本问答 1. 打开 `https://gemini.google.com`。 @@ -50,25 +51,24 @@ Gemini 页面的操作按钮(`.send-button-container` 内)通过 `aria-label 4. 将用户提示词原样输入。 5. 发送后立即通知用户:正在绘图中。 6. **分段轮询等待**(见下方"CDP 保活轮询策略",生图超时上限 120s)。 -7. 结果出现后,调用 `GeminiOps.getLatestImage()` 获取最新生成的图片(Gemini 一次只生成一张): - - 返回 `{ok, src, alt, width, height, hasDownloadBtn, debug}`。 +7. 结果出现后,调用 `ops.getLatestImage()` 获取最新生成的图片(Gemini 一次只生成一张): + - 返回 `{ok, src, alt, width, height, hasDownloadBtn}`。 - 定位依据:`` — 只有同时具有 `image` 和 `loaded` 两个 class 的才是已渲染完成的生成图片;DOM 中取最后一个即为最新。 - `src` 为 `https://lh3.googleusercontent.com/...` 格式的原图 URL。 - - 若 `ok === false`,等几秒再调一次;连续两次失败则做 snapshot 排查页面状态。 - - **默认**:调用 `GeminiOps.extractImageBase64()` 从 DOM 直接提取图片 Base64(Canvas 优先,跨域污染时 fallback 到 fetch),解码后保存为本地文件发送给用户。注意该函数返回 Promise,CDP 调用时需设置 `awaitPromise:true`。 - - **高清**:仅当用户明确要求高清/原图时,才调用 `GeminiOps.downloadLatestImage()` 走原图下载按钮流程。 + - 若 `ok === false`,等几秒再调一次;连续两次失败则做 screenshot 排查页面状态。 + - **默认**:调用 `ops.extractImageBase64()` 从 DOM 直接提取图片 Base64(Canvas 优先,跨域污染时 fallback 到 fetch),解码后保存为本地文件发送给用户。 + - **高清**:仅当用户明确要求高清/原图时,才调用 `ops.downloadLatestImage()` 走原图下载按钮流程。 - 下载按钮定位:从 `img` 向上找到 `.image-container` 容器,容器内的 `mat-icon[fonticon="download"]` 即为下载原图按钮。 - ⚠️ **严禁使用浏览器截图(screenshot)代替保存图片**。 8. 将保存到本地的图片文件发送给用户。 -9. **将每步操作返回的 `debug` 日志一并回传给用户**,方便排查定位失败和优化策略。所有函数(`probe`、`click`、`fillPrompt`、`pollStatus`、`getLatestImage`、`extractImageBase64`、`downloadLatestImage`)的返回值都包含 `debug` 字段。 ## CDP 保活轮询策略 -> **核心原则**:绝不在页面内做长时间 Promise 等待。每次 `evaluate` 必须毫秒级返回,由调用端控制循环。 +> **核心原则**:通过 `ops.pollStatus()` 分段轮询,不要试图一次性长时间等待结果。 生图/问答发送后,按以下方式等待结果: -1. 每隔 **8~10 秒**调用一次 `GeminiOps.pollStatus()`。 +1. 每隔 **8~10 秒**调用一次 `ops.pollStatus()`。 2. 该函数立即返回 `{status, label, pageVisible, ts}`。 3. 调用端根据 `status` 判断: - `loading` → 继续等待,累计已耗时。 @@ -86,8 +86,8 @@ Gemini 页面的操作按钮(`.send-button-container` 内)通过 `aria-label ## 低 token 优先策略 -- 优先使用 `scripts/gemini_ui_shortcuts.js` 的快捷选择器。 -- 先 evaluate 批量动作,再 snapshot 精准兜底。 +- **只调封装好的 `ops.xxx` / `operator.xxx` 方法**,不要自己拼 `page.evaluate()` 代码——既省 token 又不容易出错。 +- 先调方法执行动作,再用 `operator.screenshot()` 精准兜底排查。 - 避免高频全量快照。 ## 参考 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6128931 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1414 @@ +{ + "name": "gemini-skill", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gemini-skill", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "puppeteer-core": "^24.39.1", + "puppeteer-extra": "^3.3.6", + "puppeteer-extra-plugin-stealth": "^2.11.2" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.13.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/@puppeteer/browsers/-/browsers-2.13.0.tgz", + "integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://mirrors.cloud.tencent.com/npm/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "license": "MIT", + "optional": true, + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/b4a": { + "version": "1.8.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.5.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/bare-fs/-/bare-fs-4.5.5.tgz", + "integrity": "sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.8.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/bare-os/-/bare-os-3.8.0.tgz", + "integrity": "sha512-Dc9/SlwfxkXIGYhvMQNUtKaXCaGkZYGcd1vuNUUADVqzu4/vQfvnMkYYOUnt2VwQ2AqKr/8qAVFRtwETljgeFg==", + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.8.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/bare-stream/-/bare-stream-2.8.1.tgz", + "integrity": "sha512-bSeR8RfvbRwDpD7HWZvn8M3uYNDrk7m9DQjYOFkENZlXW8Ju/MPaqUPQq5LqJ3kyjEm07siTaAQ7wBKCU59oHg==", + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.21.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.3.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/bare-url/-/bare-url-2.3.2.tgz", + "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/basic-ftp": { + "version": "5.2.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/basic-ftp/-/basic-ftp-5.2.0.tgz", + "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://mirrors.cloud.tencent.com/npm/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://mirrors.cloud.tencent.com/npm/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/chromium-bidi": { + "version": "14.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/chromium-bidi/-/chromium-bidi-14.0.0.tgz", + "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-deep": { + "version": "0.2.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/clone-deep/-/clone-deep-0.2.4.tgz", + "integrity": "sha512-we+NuQo2DHhSl+DP6jlUiAhyAjBQrYnpOk15rN6c6JSPScjiCLh8IbSU+VTcph6YS3o7mASE8a0+gbZ7ChLpgg==", + "license": "MIT", + "dependencies": { + "for-own": "^0.1.3", + "is-plain-object": "^2.0.1", + "kind-of": "^3.0.2", + "lazy-cache": "^1.0.3", + "shallow-clone": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1581282", + "resolved": "https://mirrors.cloud.tencent.com/npm/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz", + "integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==", + "license": "BSD-3-Clause" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/for-own": { + "version": "0.1.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw==", + "license": "MIT", + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://mirrors.cloud.tencent.com/npm/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://mirrors.cloud.tencent.com/npm/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://mirrors.cloud.tencent.com/npm/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://mirrors.cloud.tencent.com/npm/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "license": "MIT" + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lazy-cache": { + "version": "1.0.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/merge-deep": { + "version": "3.0.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/merge-deep/-/merge-deep-3.0.3.tgz", + "integrity": "sha512-qtmzAS6t6grwEkNrunqTBdn0qKwFgNWvlxUbAV8es9M7Ot1EbyApytCnvE0jALPa46ZpKDUo527kKiaWplmlFA==", + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "clone-deep": "^0.2.4", + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/mixin-object": { + "version": "2.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/mixin-object/-/mixin-object-2.0.1.tgz", + "integrity": "sha512-ALGF1Jt9ouehcaXaHhn6t1yGWRqGaHkPFndtFVHfZXOvkIZ/yoGaSi0AHVTafb3ZBGg4dr/bDwnaEKqCXzchMA==", + "license": "MIT", + "dependencies": { + "for-in": "^0.1.3", + "is-extendable": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mixin-object/node_modules/for-in": { + "version": "0.1.8", + "resolved": "https://mirrors.cloud.tencent.com/npm/for-in/-/for-in-0.1.8.tgz", + "integrity": "sha512-F0to7vbBSHP8E3l6dCjxNOLuSFAACIxFy3UehTUlG7svlXi37HHsDkyVcHo0Pq8QwrE+pXvWSVX3ZT1T9wAZ9g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/puppeteer-core": { + "version": "24.39.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/puppeteer-core/-/puppeteer-core-24.39.1.tgz", + "integrity": "sha512-AMqQIKoEhPS6CilDzw0Gd1brLri3emkC+1N2J6ZCCuY1Cglo56M63S0jOeBZDQlemOiRd686MYVMl9ELJBzN3A==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.0", + "chromium-bidi": "14.0.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1581282", + "typed-query-selector": "^2.12.1", + "webdriver-bidi-protocol": "0.4.1", + "ws": "^8.19.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-extra": { + "version": "3.3.6", + "resolved": "https://mirrors.cloud.tencent.com/npm/puppeteer-extra/-/puppeteer-extra-3.3.6.tgz", + "integrity": "sha512-rsLBE/6mMxAjlLd06LuGacrukP2bqbzKCLzV1vrhHFavqQE/taQ2UXv3H5P0Ls7nsrASa+6x3bDbXHpqMwq+7A==", + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.0", + "debug": "^4.1.1", + "deepmerge": "^4.2.2" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "@types/puppeteer": "*", + "puppeteer": "*", + "puppeteer-core": "*" + }, + "peerDependenciesMeta": { + "@types/puppeteer": { + "optional": true + }, + "puppeteer": { + "optional": true + }, + "puppeteer-core": { + "optional": true + } + } + }, + "node_modules/puppeteer-extra-plugin": { + "version": "3.2.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/puppeteer-extra-plugin/-/puppeteer-extra-plugin-3.2.3.tgz", + "integrity": "sha512-6RNy0e6pH8vaS3akPIKGg28xcryKscczt4wIl0ePciZENGE2yoaQJNd17UiEbdmh5/6WW6dPcfRWT9lxBwCi2Q==", + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.0", + "debug": "^4.1.1", + "merge-deep": "^3.0.1" + }, + "engines": { + "node": ">=9.11.2" + }, + "peerDependencies": { + "playwright-extra": "*", + "puppeteer-extra": "*" + }, + "peerDependenciesMeta": { + "playwright-extra": { + "optional": true + }, + "puppeteer-extra": { + "optional": true + } + } + }, + "node_modules/puppeteer-extra-plugin-stealth": { + "version": "2.11.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/puppeteer-extra-plugin-stealth/-/puppeteer-extra-plugin-stealth-2.11.2.tgz", + "integrity": "sha512-bUemM5XmTj9i2ZerBzsk2AN5is0wHMNE6K0hXBzBXOzP5m5G3Wl0RHhiqKeHToe/uIH8AoZiGhc1tCkLZQPKTQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "puppeteer-extra-plugin": "^3.2.3", + "puppeteer-extra-plugin-user-preferences": "^2.4.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "playwright-extra": "*", + "puppeteer-extra": "*" + }, + "peerDependenciesMeta": { + "playwright-extra": { + "optional": true + }, + "puppeteer-extra": { + "optional": true + } + } + }, + "node_modules/puppeteer-extra-plugin-user-data-dir": { + "version": "2.4.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/puppeteer-extra-plugin-user-data-dir/-/puppeteer-extra-plugin-user-data-dir-2.4.1.tgz", + "integrity": "sha512-kH1GnCcqEDoBXO7epAse4TBPJh9tEpVEK/vkedKfjOVOhZAvLkHGc9swMs5ChrJbRnf8Hdpug6TJlEuimXNQ+g==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^10.0.0", + "puppeteer-extra-plugin": "^3.2.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "playwright-extra": "*", + "puppeteer-extra": "*" + }, + "peerDependenciesMeta": { + "playwright-extra": { + "optional": true + }, + "puppeteer-extra": { + "optional": true + } + } + }, + "node_modules/puppeteer-extra-plugin-user-preferences": { + "version": "2.4.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/puppeteer-extra-plugin-user-preferences/-/puppeteer-extra-plugin-user-preferences-2.4.1.tgz", + "integrity": "sha512-i1oAZxRbc1bk8MZufKCruCEC3CCafO9RKMkkodZltI4OqibLFXF3tj6HZ4LZ9C5vCXZjYcDWazgtY69mnmrQ9A==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "deepmerge": "^4.2.2", + "puppeteer-extra-plugin": "^3.2.3", + "puppeteer-extra-plugin-user-data-dir": "^2.4.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "playwright-extra": "*", + "puppeteer-extra": "*" + }, + "peerDependenciesMeta": { + "playwright-extra": { + "optional": true + }, + "puppeteer-extra": { + "optional": true + } + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://mirrors.cloud.tencent.com/npm/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shallow-clone": { + "version": "0.1.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/shallow-clone/-/shallow-clone-0.1.2.tgz", + "integrity": "sha512-J1zdXCky5GmNnuauESROVu31MQSnLoYvlyEn6j2Ztk6Q5EHFIhxkMhYcv6vuDzl2XEzoRr856QwzMgWM/TmZgw==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.1", + "kind-of": "^2.0.1", + "lazy-cache": "^0.2.3", + "mixin-object": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shallow-clone/node_modules/kind-of": { + "version": "2.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/kind-of/-/kind-of-2.0.1.tgz", + "integrity": "sha512-0u8i1NZ/mg0b+W3MGGw5I7+6Eib2nx72S/QvXa0hYjEkjTknYmEYQJwGu3mLC0BrhtJjtQafTkyRUQ75Kx0LVg==", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shallow-clone/node_modules/lazy-cache": { + "version": "0.2.7", + "resolved": "https://mirrors.cloud.tencent.com/npm/lazy-cache/-/lazy-cache-0.2.7.tgz", + "integrity": "sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://mirrors.cloud.tencent.com/npm/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://mirrors.cloud.tencent.com/npm/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "3.1.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/tar-fs/-/tar-fs-3.1.2.tgz", + "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.8", + "resolved": "https://mirrors.cloud.tencent.com/npm/tar-stream/-/tar-stream-3.1.8.tgz", + "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://mirrors.cloud.tencent.com/npm/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typed-query-selector": { + "version": "2.12.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/typed-query-selector/-/typed-query-selector-2.12.1.tgz", + "integrity": "sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT", + "optional": true + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", + "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://mirrors.cloud.tencent.com/npm/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://mirrors.cloud.tencent.com/npm/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://mirrors.cloud.tencent.com/npm/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b45a6c6 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "gemini-skill", + "version": "1.0.0", + "type": "module", + "main": "src/index.js", + "scripts": { + "demo": "node src/demo.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "gemini", + "cdp", + "puppeteer", + "ai-image-generation" + ], + "author": "", + "license": "ISC", + "description": "通过 CDP 操控 Gemini 网页进行 AI 问答与生图", + "dependencies": { + "puppeteer-core": "^24.39.1", + "puppeteer-extra": "^3.3.6", + "puppeteer-extra-plugin-stealth": "^2.11.2" + } +} diff --git a/scripts/gemini_ui_shortcuts.js b/scripts/gemini_ui_shortcuts.js deleted file mode 100644 index f9b5d51..0000000 --- a/scripts/gemini_ui_shortcuts.js +++ /dev/null @@ -1,304 +0,0 @@ -(function initGeminiOps(){ - const S = { - promptInput: [ - 'div.ql-editor[contenteditable="true"][role="textbox"]', - '[contenteditable="true"][aria-label*="Gemini"]', - '[contenteditable="true"][data-placeholder*="Gemini"]', - 'div[contenteditable="true"][role="textbox"]' - ], - actionBtn: [ - '.send-button-container button.send-button', - '.send-button-container button' - ], - newChatBtn: [ - '[data-test-id="new-chat-button"] a', - '[data-test-id="new-chat-button"]', - 'a[aria-label="发起新对话"]', - 'a[aria-label*="new chat" i]' - ], - modelBtn: [ - 'button:has-text("Gemini")', - '[role="button"][aria-haspopup="menu"]' - ] - }; - - /* ── Debug 日志系统 ── */ - var _log = []; - var _MAX_LOG = 200; - - function _d(fn, step, ok, detail){ - var entry = {ts:Date.now(), fn:fn, step:step, ok:ok}; - if(detail!==undefined) entry.detail=detail; - _log.push(entry); - if(_log.length>_MAX_LOG) _log.splice(0, _log.length-_MAX_LOG); - } - - /** 取出并清空日志 */ - function _flush(){ - var out=_log.slice(); - _log=[]; - return out; - } - - function visible(el){ - if(!el) return false; - const r=el.getBoundingClientRect(); - const st=getComputedStyle(el); - return r.width>0 && r.height>0 && st.display!=='none' && st.visibility!=='hidden'; - } - - function q(sel){ - try{ - if(sel.includes(':has-text(')){ - const m=sel.match(/^(.*):has-text\("(.*)"\)$/); - if(!m) return null; - const nodes=[...document.querySelectorAll(m[1]||'*')]; - return nodes.find(n=>visible(n)&&n.textContent?.includes(m[2]))||null; - } - return [...document.querySelectorAll(sel)].find(visible)||null; - }catch{return null;} - } - - function find(key){ - for(const s of (S[key]||[])){ - const el=q(s); - if(el){ - _d('find','matched',true,{key:key,selector:s}); - return el; - } - } - _d('find','no_match',false,{key:key,tried:S[key]||[]}); - return null; - } - - function click(key){ - _d('click','start',true,{key:key}); - const el=find(key); - if(!el){ - _d('click','element_not_found',false,{key:key}); - return {ok:false,key,error:'not_found',debug:_flush()}; - } - el.click(); - _d('click','clicked',true,{key:key}); - return {ok:true,key,debug:_flush()}; - } - - function fillPrompt(text){ - _d('fillPrompt','start',true,{textLen:text.length}); - const el=find('promptInput'); - if(!el){ - _d('fillPrompt','input_not_found',false); - return {ok:false,error:'prompt_not_found',debug:_flush()}; - } - _d('fillPrompt','input_found',true,{tag:el.tagName}); - el.focus(); - if(el.tagName==='TEXTAREA'){ - el.value=text; - el.dispatchEvent(new Event('input',{bubbles:true})); - _d('fillPrompt','set_textarea',true); - }else{ - document.execCommand('selectAll',false,null); - document.execCommand('insertText',false,text); - el.dispatchEvent(new Event('input',{bubbles:true})); - _d('fillPrompt','exec_insertText',true); - } - return {ok:true,debug:_flush()}; - } - - function getStatus(){ - const btn=find('actionBtn'); - if(!btn){ - _d('getStatus','btn_not_found',false); - return {status:'unknown',error:'btn_not_found'}; - } - const label=(btn.getAttribute('aria-label')||'').trim(); - const disabled=btn.getAttribute('aria-disabled')==='true'; - if(/停止|Stop/i.test(label)){ - _d('getStatus','detected',true,{status:'loading',label:label}); - return {status:'loading',label}; - } - if(/发送|Send|Submit/i.test(label)){ - _d('getStatus','detected',true,{status:'ready',label:label,disabled:disabled}); - return {status:'ready',label,disabled}; - } - _d('getStatus','detected',true,{status:'idle',label:label,disabled:disabled}); - return {status:'idle',label,disabled}; - } - - /* ── 保活式轮询 ── - * 不在页面内做长 Promise 等待(会导致 CDP 连接因长时间无消息被网关判定空闲断开)。 - * 改为:调用端每 8-10s evaluate 一次 GeminiOps.pollStatus(),立即拿到结果。 - * 调用端自行累计耗时并判断超时。 - */ - function pollStatus(){ - var s=getStatus(); - _d('pollStatus','polled',true,{status:s.status}); - return {status:s.status, label:s.label, pageVisible:!document.hidden, ts:Date.now(), debug:_flush()}; - } - - /* ── 最新图片获取与下载 ── - * Gemini 一次只生成一张图片,流程上只关心最新生成的那张。 - * DOM 中 img.image.loaded 按顺序排列,最后一个即为最新生成。 - * - * DOM 结构: - *
- * - *
- * - *
- *
- */ - - function _findContainer(img){ - var el=img; - while(el&&el!==document.body){ - if(el.classList&&el.classList.contains('image-container')) return el; - el=el.parentElement; - } - return null; - } - - function _findDownloadBtn(container){ - if(!container) return null; - return container.querySelector('mat-icon[fonticon="download"]') - || container.querySelector('mat-icon[data-mat-icon-name="download"]') - || null; - } - - /** 获取最新生成的一张图片信息(DOM 中最后一个 img.image.loaded) */ - function getLatestImage(){ - _d('getLatestImage','start',true); - var imgs=[...document.querySelectorAll('img.image.loaded')]; - _d('getLatestImage','query_imgs',true,{totalFound:imgs.length}); - if(!imgs.length){ - _d('getLatestImage','no_images',false); - return {ok:false, error:'no_loaded_images', debug:_flush()}; - } - var img=imgs[imgs.length-1]; - _d('getLatestImage','picked_latest',true,{index:imgs.length-1, src:(img.src||'').slice(0,80)}); - var container=_findContainer(img); - _d('getLatestImage','find_container',!!container); - var dlBtn=_findDownloadBtn(container); - _d('getLatestImage','find_download_btn',!!dlBtn); - return { - ok: true, - src: img.src||'', - alt: img.alt||'', - width: img.naturalWidth||0, - height: img.naturalHeight||0, - hasDownloadBtn: !!dlBtn, - debug: _flush() - }; - } - - /** 点击最新图片的"下载原图"按钮(仅用户要求高清时调用) */ - function downloadLatestImage(){ - _d('downloadLatestImage','start',true); - var imgs=[...document.querySelectorAll('img.image.loaded')]; - _d('downloadLatestImage','query_imgs',true,{totalFound:imgs.length}); - if(!imgs.length){ - _d('downloadLatestImage','no_images',false); - return {ok:false, error:'no_loaded_images', debug:_flush()}; - } - var img=imgs[imgs.length-1]; - var container=_findContainer(img); - _d('downloadLatestImage','find_container',!!container); - var dlBtn=_findDownloadBtn(container); - if(!dlBtn){ - _d('downloadLatestImage','download_btn_not_found',false); - return {ok:false, error:'download_btn_not_found', debug:_flush()}; - } - _d('downloadLatestImage','find_download_btn',true); - var clickable=dlBtn.closest('button,[role="button"],.button-icon-wrapper')||dlBtn; - clickable.click(); - _d('downloadLatestImage','clicked',true,{clickedTag:clickable.tagName}); - return {ok:true, src:img.src||'', debug:_flush()}; - } - - /* ── 图片 Base64 提取 ── - * 默认获取图片的方式。直接从已渲染的 DOM 提取,不走网络请求,不触发下载对话框。 - * - * 策略: - * 1. Canvas 提取(同步,零网络,最快) - * 2. 若 Canvas 被 tainted(跨域污染),fallback 到页面内 fetch → blob → Base64 - * - * 返回 data:image/png;base64,... 格式字符串,调用端直接解码存文件即可。 - * 注意:fetch fallback 是异步的,因此本函数返回 Promise。 - * 调用端需用 CDP Runtime.evaluate + awaitPromise:true 来获取结果。 - */ - function extractImageBase64(){ - _d('extractImageBase64','start',true); - var imgs=[...document.querySelectorAll('img.image.loaded')]; - _d('extractImageBase64','query_imgs',true,{totalFound:imgs.length}); - if(!imgs.length){ - _d('extractImageBase64','no_images',false); - var dbg=_flush(); - return Promise.resolve({ok:false, error:'no_loaded_images', debug:dbg}); - } - var img=imgs[imgs.length-1]; - var w=img.naturalWidth||img.width; - var h=img.naturalHeight||img.height; - _d('extractImageBase64','picked_latest',true,{index:imgs.length-1, w:w, h:h, src:(img.src||'').slice(0,80)}); - - // 尝试 Canvas 同步提取 - try{ - var canvas=document.createElement('canvas'); - canvas.width=w; - canvas.height=h; - var ctx=canvas.getContext('2d'); - ctx.drawImage(img,0,0); - var dataUrl=canvas.toDataURL('image/png'); - _d('extractImageBase64','canvas_ok',true,{size:dataUrl.length}); - var dbg=_flush(); - return Promise.resolve({ok:true, dataUrl:dataUrl, width:w, height:h, method:'canvas', debug:dbg}); - }catch(e){ - _d('extractImageBase64','canvas_tainted',false,{error:e.message||String(e)}); - } - - // Fallback: 页面内 fetch → blob → Base64 - _d('extractImageBase64','fetch_fallback_start',true,{src:(img.src||'').slice(0,80)}); - var debugSnapshot=_flush(); - return fetch(img.src) - .then(function(r){ - if(!r.ok) throw new Error('fetch_status_'+r.status); - return r.blob(); - }) - .then(function(blob){ - return new Promise(function(resolve){ - var reader=new FileReader(); - reader.onloadend=function(){ - _d('extractImageBase64','fetch_ok',true,{size:reader.result.length}); - resolve({ok:true, dataUrl:reader.result, width:w, height:h, method:'fetch', debug:debugSnapshot.concat(_flush())}); - }; - reader.readAsDataURL(blob); - }); - }) - .catch(function(err){ - _d('extractImageBase64','fetch_failed',false,{error:err.message||String(err)}); - return {ok:false, error:'extract_failed', detail:err.message||String(err), debug:debugSnapshot.concat(_flush())}; - }); - } - - function probe(){ - _d('probe','start',true); - var s=getStatus(); - var result={ - promptInput: !!find('promptInput'), - actionBtn: !!find('actionBtn'), - newChatBtn: !!find('newChatBtn'), - modelBtn: !!find('modelBtn'), - status: s.status, - debug: _flush() - }; - return result; - } - - /** 获取完整调试日志(不清空) */ - function getDebugLog(){ - return {log:_log.slice(), count:_log.length}; - } - - window.GeminiOps = {probe, click, fillPrompt, getStatus, pollStatus, getLatestImage, extractImageBase64, downloadLatestImage, getDebugLog, selectors:S, version:'0.9.0'}; -})(); diff --git a/src/browser.js b/src/browser.js new file mode 100644 index 0000000..f46a25f --- /dev/null +++ b/src/browser.js @@ -0,0 +1,238 @@ +/** + * browser.js — 浏览器生命周期管理(内部模块,不对外暴露) + * + * 设计思路: + * Skill 内部自己管理 Chrome 进程,对外只暴露 getSession()。 + * 调用方不需要关心 launch/connect/端口/CDP 等细节。 + * + * 流程: + * 1. 先检查指定端口是否已有 Chrome 在跑 → 有就 connect + * 2. 没有 → 启动新 Chrome(需要 executablePath) + * 3. 找到 / 新开 Gemini 标签页 + * 4. 返回 { browser, page } + */ +import puppeteerCore from 'puppeteer-core'; +import { addExtra } from 'puppeteer-extra'; +import StealthPlugin from 'puppeteer-extra-plugin-stealth'; +import { homedir } from 'node:os'; +import { join } from 'node:path'; +import { createConnection } from 'node:net'; + +// ── 用 puppeteer-extra 包装 puppeteer-core,注入 stealth 插件 ── +const puppeteer = addExtra(puppeteerCore); +puppeteer.use(StealthPlugin()); + +// ── 模块级单例:跨调用复用同一个浏览器 ── +let _browser = null; + +/** 默认配置 */ +const DEFAULTS = { + port: 9222, + userDataDir: join(homedir(), '.gemini-skill', 'chrome-data'), + headless: false, + protocolTimeout: 60_000, +}; + +/** + * 探测指定端口是否有 Chrome 在监听 + * @param {number} port + * @param {string} [host='127.0.0.1'] + * @param {number} [timeout=1500] + * @returns {Promise} + */ +function isPortAlive(port, host = '127.0.0.1', timeout = 1500) { + return new Promise((resolve) => { + const socket = createConnection({ host, port }); + const timer = setTimeout(() => { + socket.destroy(); + resolve(false); + }, timeout); + socket.on('connect', () => { + clearTimeout(timer); + socket.destroy(); + resolve(true); + }); + socket.on('error', () => { + clearTimeout(timer); + resolve(false); + }); + }); +} + +/** Chrome 启动参数 */ +const CHROME_ARGS = [ + // ── 基础 ── + '--no-first-run', + '--disable-default-apps', + '--disable-popup-blocking', + + // ── 渲染稳定性(无头 / 无显卡服务器) ── + '--disable-gpu', + '--disable-software-rasterizer', + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + + // ── 反检测(配合 stealth 插件 + ignoreDefaultArgs) ── + '--disable-blink-features=AutomationControlled', + + // ── 网络 / 性能 ── + '--disable-background-networking', + '--disable-background-timer-throttling', + '--disable-backgrounding-occluded-windows', + '--disable-renderer-backgrounding', + + // ── UI 纯净度 ── + '--disable-features=Translate', + '--no-default-browser-check', + '--disable-crash-reporter', + '--hide-crash-restore-bubble', +]; + +/** + * 连接到已运行的 Chrome + * @param {number} port + * @returns {Promise} + */ +async function connectToChrome(port) { + const browserURL = `http://127.0.0.1:${port}`; + const browser = await puppeteer.connect({ + browserURL, + defaultViewport: null, + protocolTimeout: DEFAULTS.protocolTimeout, + }); + console.log('[browser] connected to existing Chrome on port', port); + return browser; +} + +/** + * 启动新的 Chrome 实例 + * @param {object} opts + * @param {string} opts.executablePath + * @param {number} opts.port + * @param {string} opts.userDataDir + * @param {boolean} opts.headless + * @returns {Promise} + */ +async function launchChrome({ executablePath, port, userDataDir, headless }) { + const browser = await puppeteer.launch({ + executablePath, + headless, + userDataDir, + defaultViewport: null, + args: [ + ...CHROME_ARGS, + `--remote-debugging-port=${port}`, + ], + ignoreDefaultArgs: ['--enable-automation'], + protocolTimeout: DEFAULTS.protocolTimeout, + }); + console.log('[browser] launched Chrome, pid:', browser.process()?.pid, 'port:', port, 'dataDir:', userDataDir); + return browser; +} + +/** + * 在浏览器中找到 Gemini 标签页,或新开一个 + * @param {import('puppeteer-core').Browser} browser + * @returns {Promise} + */ +async function findOrCreateGeminiPage(browser) { + const pages = await browser.pages(); + + // 优先复用已有的 Gemini 标签页 + for (const page of pages) { + const url = page.url(); + if (url.includes('gemini.google.com')) { + console.log('[browser] reusing existing Gemini tab:', url); + await page.bringToFront(); + return page; + } + } + + // 没找到,新开一个 + const page = pages.length > 0 ? pages[0] : await browser.newPage(); + await page.goto('https://gemini.google.com/app', { + waitUntil: 'networkidle2', + timeout: 30_000, + }); + console.log('[browser] opened new Gemini tab'); + return page; +} + +/** + * 确保浏览器可用 — Skill 唯一的对外浏览器管理入口 + * + * 逻辑: + * 1. 如果已有 _browser 且未断开 → 直接复用 + * 2. 检查端口是否有 Chrome → connect + * 3. 否则 launch 新 Chrome(需要 executablePath) + * + * @param {object} [opts] + * @param {string} [opts.executablePath] - Chrome 路径(仅 launch 时需要) + * @param {number} [opts.port=9222] - 调试端口 + * @param {string} [opts.userDataDir] - 用户数据目录 + * @param {boolean} [opts.headless=false] + * @returns {Promise<{browser: import('puppeteer-core').Browser, page: import('puppeteer-core').Page}>} + */ +export async function ensureBrowser(opts = {}) { + const { + executablePath, + port = DEFAULTS.port, + userDataDir = DEFAULTS.userDataDir, + headless = DEFAULTS.headless, + } = opts; + + // 1. 复用已有连接 + if (_browser && _browser.isConnected()) { + console.log('[browser] reusing existing connection'); + const page = await findOrCreateGeminiPage(_browser); + return { browser: _browser, page }; + } + + // 2. 尝试连接已在运行的 Chrome + const alive = await isPortAlive(port); + if (alive) { + try { + _browser = await connectToChrome(port); + const page = await findOrCreateGeminiPage(_browser); + return { browser: _browser, page }; + } catch (err) { + console.warn('[browser] connect failed, will try launch:', err.message); + } + } + + // 3. 启动新 Chrome + if (!executablePath) { + throw new Error( + `[browser] 端口 ${port} 无可用 Chrome,且未提供 executablePath。\n` + + `请先手动启动 Chrome:chrome --remote-debugging-port=${port} --user-data-dir="${userDataDir}"\n` + + `或传入 executablePath 让 skill 自动启动。` + ); + } + + _browser = await launchChrome({ executablePath, port, userDataDir, headless }); + const page = await findOrCreateGeminiPage(_browser); + return { browser: _browser, page }; +} + +/** + * 断开浏览器连接(不杀 Chrome 进程,方便下次复用) + */ +export function disconnect() { + if (_browser) { + _browser.disconnect(); + _browser = null; + console.log('[browser] disconnected'); + } +} + +/** + * 关闭浏览器(杀 Chrome 进程) + */ +export async function close() { + if (_browser) { + await _browser.close(); + _browser = null; + console.log('[browser] closed'); + } +} diff --git a/src/demo.js b/src/demo.js new file mode 100644 index 0000000..e1ad62d --- /dev/null +++ b/src/demo.js @@ -0,0 +1,47 @@ +/** + * demo.js — 使用示例 + * + * 两种启动方式: + * + * 方式 1(推荐):先手动启动 Chrome,再运行 demo + * chrome --remote-debugging-port=9222 --user-data-dir="~/.gemini-skill/chrome-data" + * node src/demo.js + * + * 方式 2:让 skill 自动启动 Chrome + * CHROME_PATH="C:/Program Files/Google/Chrome/Application/chrome.exe" node src/demo.js + */ +import { createGeminiSession, disconnect } from './index.js'; + +async function main() { + console.log('=== Gemini Skill Demo ===\n'); + + // 创建会话(自动 connect 或 launch) + const { ops } = await createGeminiSession({ + executablePath: process.env.CHROME_PATH || undefined, + }); + + try { + // 1. 探测页面状态 + console.log('[1] 探测页面元素...'); + const probe = await ops.probe(); + console.log('probe:', JSON.stringify(probe, null, 2)); + + // 2. 发送一句话 + console.log('\n[2] 发送提示词...'); + const result = await ops.sendAndWait('Hello Gemini!', { + timeout: 60_000, + onPoll(poll) { + console.log(` polling... status=${poll.status}`); + }, + }); + console.log('result:', JSON.stringify(result, null, 2)); + + } catch (err) { + console.error('Error:', err); + } finally { + disconnect(); + console.log('\n[done]'); + } +} + +main().catch(console.error); diff --git a/src/gemini-ops.js b/src/gemini-ops.js new file mode 100644 index 0000000..b3fe74c --- /dev/null +++ b/src/gemini-ops.js @@ -0,0 +1,350 @@ +/** + * gemini-ops.js — Gemini 操作高层 API + * + * 职责: + * 基于 operator.js 的底层原子操作,编排 Gemini 特定的业务流程。 + * 全部通过 CDP 实现,不往页面注入任何对象。 + */ +import { createOperator } from './operator.js'; + +// ── Gemini 页面元素选择器 ── +const SELECTORS = { + promptInput: [ + 'div.ql-editor[contenteditable="true"][role="textbox"]', + '[contenteditable="true"][aria-label*="Gemini"]', + '[contenteditable="true"][data-placeholder*="Gemini"]', + 'div[contenteditable="true"][role="textbox"]', + ], + actionBtn: [ + '.send-button-container button.send-button', + '.send-button-container button', + ], + newChatBtn: [ + '[data-test-id="new-chat-button"] a', + '[data-test-id="new-chat-button"]', + 'a[aria-label="发起新对话"]', + 'a[aria-label*="new chat" i]', + ], + modelBtn: [ + 'button:has-text("Gemini")', + '[role="button"][aria-haspopup="menu"]', + ], +}; + +/** + * 创建 GeminiOps 操控实例 + * @param {import('puppeteer-core').Page} page + */ +export function createOps(page) { + const op = createOperator(page); + + return { + /** 暴露底层 operator,供高级用户直接使用 */ + operator: op, + + /** 暴露选择器定义,方便调试和外部扩展 */ + selectors: SELECTORS, + + /** + * 探测页面各元素是否就位 + * @returns {Promise<{promptInput: boolean, actionBtn: boolean, newChatBtn: boolean, modelBtn: boolean, status: object}>} + */ + async probe() { + const [promptInput, actionBtn, newChatBtn, modelBtn] = await Promise.all([ + op.locate(SELECTORS.promptInput), + op.locate(SELECTORS.actionBtn), + op.locate(SELECTORS.newChatBtn), + op.locate(SELECTORS.modelBtn), + ]); + const status = await this.getStatus(); + return { + promptInput: promptInput.found, + actionBtn: actionBtn.found, + newChatBtn: newChatBtn.found, + modelBtn: modelBtn.found, + status, + }; + }, + + /** + * 点击指定按钮 + * @param {'actionBtn'|'newChatBtn'|'modelBtn'} key + */ + async click(key) { + const sels = SELECTORS[key]; + if (!sels) { + return { ok: false, error: `unknown_key: ${key}` }; + } + return op.click(sels); + }, + + /** + * 填写提示词(快速填充,非逐字输入) + * @param {string} text + */ + async fillPrompt(text) { + return op.fill(SELECTORS.promptInput, text); + }, + + /** + * 获取当前按钮状态(通过一次性 evaluate 读取,不注入任何东西) + */ + async getStatus() { + return op.query((sels) => { + // 在页面上下文中查找 actionBtn + let btn = null; + for (const sel of sels) { + try { + const all = [...document.querySelectorAll(sel)]; + btn = all.find(n => { + const r = n.getBoundingClientRect(); + const st = getComputedStyle(n); + return r.width > 0 && r.height > 0 + && st.display !== 'none' && st.visibility !== 'hidden'; + }) || null; + } catch { /* skip */ } + if (btn) break; + } + + if (!btn) return { status: 'unknown', error: 'btn_not_found' }; + + const label = (btn.getAttribute('aria-label') || '').trim(); + const disabled = btn.getAttribute('aria-disabled') === 'true'; + + if (/停止|Stop/i.test(label)) { + return { status: 'loading', label }; + } + if (/发送|Send|Submit/i.test(label)) { + return { status: 'ready', label, disabled }; + } + return { status: 'idle', label, disabled }; + }, SELECTORS.actionBtn); + }, + + /** + * 单次轮询状态(保活式,不阻塞) + */ + async pollStatus() { + const status = await this.getStatus(); + const pageVisible = await op.query(() => !document.hidden); + return { ...status, pageVisible, ts: Date.now() }; + }, + + /** + * 获取最新生成的图片信息 + */ + async getLatestImage() { + return op.query(() => { + const imgs = [...document.querySelectorAll('img.image.loaded')]; + if (!imgs.length) { + return { ok: false, error: 'no_loaded_images' }; + } + const img = imgs[imgs.length - 1]; + // 查找下载按钮 + let container = img; + while (container && container !== document.body) { + if (container.classList?.contains('image-container')) break; + container = container.parentElement; + } + const dlBtn = container + ? (container.querySelector('mat-icon[fonticon="download"]') + || container.querySelector('mat-icon[data-mat-icon-name="download"]')) + : null; + + return { + ok: true, + src: img.src || '', + alt: img.alt || '', + width: img.naturalWidth || 0, + height: img.naturalHeight || 0, + hasDownloadBtn: !!dlBtn, + }; + }); + }, + + /** + * 提取最新图片的 Base64 数据(Canvas 优先,fetch 兜底) + */ + async extractImageBase64() { + return op.query(() => { + const imgs = [...document.querySelectorAll('img.image.loaded')]; + if (!imgs.length) { + return { ok: false, error: 'no_loaded_images' }; + } + const img = imgs[imgs.length - 1]; + const w = img.naturalWidth || img.width; + const h = img.naturalHeight || img.height; + + // 尝试 Canvas 同步提取 + try { + const canvas = document.createElement('canvas'); + canvas.width = w; + canvas.height = h; + canvas.getContext('2d').drawImage(img, 0, 0); + const dataUrl = canvas.toDataURL('image/png'); + return { ok: true, dataUrl, width: w, height: h, method: 'canvas' }; + } catch { /* canvas tainted, fallback */ } + + // 标记需要 fetch fallback + return { ok: false, needFetch: true, src: img.src, width: w, height: h }; + }).then(async (result) => { + if (result.ok || !result.needFetch) return result; + + // Fetch fallback: 在页面上下文中异步执行 + return page.evaluate(async (src, w, h) => { + try { + const r = await fetch(src); + if (!r.ok) throw new Error(`fetch_status_${r.status}`); + const blob = await r.blob(); + return await new Promise((resolve) => { + const reader = new FileReader(); + reader.onloadend = () => resolve({ + ok: true, dataUrl: reader.result, width: w, height: h, method: 'fetch', + }); + reader.readAsDataURL(blob); + }); + } catch (err) { + return { ok: false, error: 'extract_failed', detail: err.message || String(err) }; + } + }, result.src, result.width, result.height); + }); + }, + + /** + * 点击最新图片的下载按钮 + */ + async downloadLatestImage() { + return op.query(() => { + const imgs = [...document.querySelectorAll('img.image.loaded')]; + if (!imgs.length) return { ok: false, error: 'no_loaded_images' }; + + const img = imgs[imgs.length - 1]; + let container = img; + while (container && container !== document.body) { + if (container.classList?.contains('image-container')) break; + container = container.parentElement; + } + const dlBtn = container + ? (container.querySelector('mat-icon[fonticon="download"]') + || container.querySelector('mat-icon[data-mat-icon-name="download"]')) + : null; + + if (!dlBtn) return { ok: false, error: 'download_btn_not_found' }; + + const clickable = dlBtn.closest('button,[role="button"],.button-icon-wrapper') || dlBtn; + clickable.click(); + return { ok: true, src: img.src || '' }; + }); + }, + + // ─── 高层组合操作 ─── + + /** + * 发送提示词并等待生成完成 + * @param {string} prompt + * @param {object} [opts] + * @param {number} [opts.timeout=120000] + * @param {number} [opts.interval=8000] + * @param {(status: object) => void} [opts.onPoll] + * @returns {Promise<{ok: boolean, elapsed: number, finalStatus?: object, error?: string}>} + */ + async sendAndWait(prompt, opts = {}) { + const { timeout = 120_000, interval = 8_000, onPoll } = opts; + + // 1. 填写 + const fillResult = await this.fillPrompt(prompt); + if (!fillResult.ok) { + return { ok: false, error: 'fill_failed', detail: fillResult, elapsed: 0 }; + } + + // 短暂等待 UI 响应 + await sleep(300); + + // 2. 点击发送 + const clickResult = await this.click('actionBtn'); + if (!clickResult.ok) { + return { ok: false, error: 'send_click_failed', detail: clickResult, elapsed: 0 }; + } + + // 3. 轮询等待 + const start = Date.now(); + let lastStatus = null; + + while (Date.now() - start < timeout) { + await sleep(interval); + + const poll = await this.pollStatus(); + lastStatus = poll; + onPoll?.(poll); + + if (poll.status === 'idle') { + return { ok: true, elapsed: Date.now() - start, finalStatus: poll }; + } + if (poll.status === 'unknown') { + console.warn('[ops] unknown status, may need screenshot to debug'); + } + } + + return { ok: false, error: 'timeout', elapsed: Date.now() - start, finalStatus: lastStatus }; + }, + + /** + * 完整生图流程:新建会话 → 发送提示词 → 等待 → 提取图片 + * @param {string} prompt + * @param {object} [opts] + * @param {number} [opts.timeout=120000] + * @param {boolean} [opts.newChat=true] + * @param {boolean} [opts.highRes=false] + * @param {(status: object) => void} [opts.onPoll] + */ + async generateImage(prompt, opts = {}) { + const { timeout = 120_000, newChat = true, highRes = false, onPoll } = opts; + + // 1. 可选:新建会话 + if (newChat) { + const newChatResult = await this.click('newChatBtn'); + if (!newChatResult.ok) { + console.warn('[ops] newChatBtn click failed, continuing anyway'); + } + await sleep(1500); + } + + // 2. 发送并等待 + const waitResult = await this.sendAndWait(prompt, { timeout, onPoll }); + if (!waitResult.ok) { + return { ...waitResult, step: 'sendAndWait' }; + } + + // 3. 等图片渲染完成 + await sleep(2000); + + // 4. 获取图片 + const imgInfo = await this.getLatestImage(); + if (!imgInfo.ok) { + await sleep(3000); + const retry = await this.getLatestImage(); + if (!retry.ok) { + return { ok: false, error: 'no_image_found', elapsed: waitResult.elapsed, imgInfo: retry }; + } + } + + // 5. 提取 / 下载 + if (highRes) { + const dlResult = await this.downloadLatestImage(); + return { ok: dlResult.ok, method: 'download', elapsed: waitResult.elapsed, ...dlResult }; + } else { + const b64Result = await this.extractImageBase64(); + return { ok: b64Result.ok, method: b64Result.method, elapsed: waitResult.elapsed, ...b64Result }; + } + }, + + /** 底层 page 引用 */ + get page() { + return page; + }, + }; +} + +function sleep(ms) { + return new Promise(r => setTimeout(r, ms)); +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..a3a231d --- /dev/null +++ b/src/index.js @@ -0,0 +1,37 @@ +/** + * gemini-skill — 统一入口 + * + * 对外只暴露高层 API,浏览器管理在内部自动完成。 + * + * 用法: + * import { createGeminiSession, disconnect } from './index.js'; + * + * const { ops } = await createGeminiSession(); + * await ops.generateImage('画一只猫'); + * disconnect(); + */ +import { ensureBrowser, disconnect, close } from './browser.js'; +import { createOps } from './gemini-ops.js'; + +export { disconnect, close }; + +/** + * 创建 Gemini 操控会话 + * + * 内部自动管理浏览器连接: + * 1. 端口有 Chrome → 直接 connect + * 2. 无 Chrome + 提供了 executablePath → 自动 launch + * 3. 无 Chrome + 无 executablePath → 报错并提示手动启动 + * + * @param {object} [opts] + * @param {string} [opts.executablePath] - Chrome 路径(可选,仅自动启动时需要) + * @param {number} [opts.port=9222] - 调试端口 + * @param {string} [opts.userDataDir] - 用户数据目录(默认 ~/.gemini-skill/chrome-data) + * @param {boolean} [opts.headless=false] + * @returns {Promise<{ops: ReturnType, page: import('puppeteer-core').Page, browser: import('puppeteer-core').Browser}>} + */ +export async function createGeminiSession(opts = {}) { + const { browser, page } = await ensureBrowser(opts); + const ops = createOps(page); + return { ops, page, browser }; +} diff --git a/src/operator.js b/src/operator.js new file mode 100644 index 0000000..634fe54 --- /dev/null +++ b/src/operator.js @@ -0,0 +1,326 @@ +/** + * operator.js — 纯 CDP 底层操作封装 + * + * 职责: + * 封装最基础的浏览器交互原语(点击、输入、查询、等待等), + * 全部通过 CDP 协议实现,不往页面注入任何对象。 + * + * 设计原则: + * - 所有 DOM 操作通过 page.evaluate() 一次性执行,执行完即走,不留痕迹 + * - 鼠标 / 键盘事件通过 CDP Input 域发送,生成 isTrusted=true 的原生事件 + * - 每个方法都是独立的原子操作,上层 gemini-ops.js 负责编排组合 + */ + +/** + * 创建 operator 实例 + * @param {import('puppeteer-core').Page} page + */ +export function createOperator(page) { + + // ─── 内部工具 ─── + + /** + * 通过 CSS 选择器列表查找第一个可见元素,返回其中心坐标和边界信息 + * 在页面上下文中执行,执行完即走 + * @param {string[]} selectors - 候选选择器,按优先级排列 + * @returns {Promise<{found: boolean, x?: number, y?: number, width?: number, height?: number, selector?: string, tagName?: string}>} + */ + async function locate(selectors) { + return page.evaluate((sels) => { + for (const sel of sels) { + let el = null; + try { + // 支持 :has-text("xxx") 伪选择器 + if (sel.includes(':has-text(')) { + const m = sel.match(/^(.*):has-text\("(.*)"\)$/); + if (m) { + const candidates = [...document.querySelectorAll(m[1] || '*')]; + el = candidates.find(n => { + const r = n.getBoundingClientRect(); + const st = getComputedStyle(n); + return r.width > 0 && r.height > 0 + && st.display !== 'none' && st.visibility !== 'hidden' + && n.textContent?.includes(m[2]); + }) || null; + } + } else { + const all = [...document.querySelectorAll(sel)]; + el = all.find(n => { + const r = n.getBoundingClientRect(); + const st = getComputedStyle(n); + return r.width > 0 && r.height > 0 + && st.display !== 'none' && st.visibility !== 'hidden'; + }) || null; + } + } catch { /* 选择器语法错误,跳过 */ } + + if (el) { + const rect = el.getBoundingClientRect(); + return { + found: true, + x: rect.x + rect.width / 2, + y: rect.y + rect.height / 2, + width: rect.width, + height: rect.height, + selector: sel, + tagName: el.tagName.toLowerCase(), + }; + } + } + return { found: false }; + }, selectors); + } + + /** + * 给坐标加一点随机偏移,模拟人类鼠标不精确的特征 + * @param {number} x + * @param {number} y + * @param {number} [jitter=3] - 最大偏移像素 + * @returns {{x: number, y: number}} + */ + function humanize(x, y, jitter = 3) { + return { + x: x + (Math.random() * 2 - 1) * jitter, + y: y + (Math.random() * 2 - 1) * jitter, + }; + } + + /** + * 随机延迟(毫秒),模拟人类反应时间 + * @param {number} min + * @param {number} max + */ + function randomDelay(min, max) { + const ms = min + Math.random() * (max - min); + return new Promise(r => setTimeout(r, ms)); + } + + // ─── 公开 API ─── + + return { + + /** + * 定位元素 — 通过选择器列表查找第一个可见元素 + * @param {string|string[]} selectors - 单个选择器或候选列表 + * @returns {Promise<{found: boolean, x?: number, y?: number, width?: number, height?: number, selector?: string, tagName?: string}>} + */ + async locate(selectors) { + const sels = Array.isArray(selectors) ? selectors : [selectors]; + return locate(sels); + }, + + /** + * 点击元素 — 通过 CDP Input.dispatchMouseEvent 发送真实鼠标事件 + * + * 生成 isTrusted=true 的原生事件,比 element.click() 更真实 + * + * @param {string|string[]} selectors - 候选选择器 + * @param {object} [opts] + * @param {number} [opts.jitter=3] - 坐标随机偏移像素 + * @param {number} [opts.delayBeforeClick=50] - 移动到元素后、点击前的等待(ms) + * @param {number} [opts.clickDuration=80] - mousedown 到 mouseup 的间隔(ms) + * @returns {Promise<{ok: boolean, selector?: string, x?: number, y?: number, error?: string}>} + */ + async click(selectors, opts = {}) { + const { jitter = 3, delayBeforeClick = 50, clickDuration = 80 } = opts; + + const sels = Array.isArray(selectors) ? selectors : [selectors]; + const loc = await locate(sels); + if (!loc.found) { + return { ok: false, error: 'element_not_found', triedSelectors: sels }; + } + + const { x, y } = humanize(loc.x, loc.y, jitter); + + // 先移动鼠标到目标位置 + await page.mouse.move(x, y); + await randomDelay(delayBeforeClick * 0.5, delayBeforeClick * 1.5); + + // mousedown → 短暂停留 → mouseup(模拟真实点击节奏) + await page.mouse.down(); + await randomDelay(clickDuration * 0.5, clickDuration * 1.5); + await page.mouse.up(); + + return { ok: true, selector: loc.selector, x, y }; + }, + + /** + * 输入文本 — 支持两种模式 + * + * - `'paste'`(默认):通过剪贴板粘贴,整段文本一次性输入,人类也经常这样操作 + * - `'typeChar'`:逐字符键盘输入,每个字符间有随机延迟,模拟打字节奏 + * + * @param {string} text - 要输入的文本 + * @param {object} [opts] + * @param {'paste'|'typeChar'} [opts.mode='paste'] - 输入模式 + * @param {number} [opts.minDelay=30] - typeChar 模式下字符间最小间隔(ms) + * @param {number} [opts.maxDelay=80] - typeChar 模式下字符间最大间隔(ms) + * @returns {Promise<{ok: boolean, length: number, mode: string}>} + */ + async type(text, opts = {}) { + const { mode = 'paste', minDelay = 30, maxDelay = 80 } = opts; + + if (mode === 'typeChar') { + // 逐字符输入,模拟真实打字 + for (const char of text) { + await page.keyboard.type(char); + await randomDelay(minDelay, maxDelay); + } + } else { + // 粘贴模式:通过 CDP Input.insertText 一次性输入整段文本 + // 等价于用户从剪贴板粘贴,但不依赖 clipboard API 权限 + const client = page._client(); + await client.send('Input.insertText', { text }); + } + + return { ok: true, length: text.length, mode }; + }, + + /** + * 快速设置文本 — 对 contenteditable 元素,用 Ctrl+A → 粘贴的方式填充 + * + * 比逐字输入快得多,适合长文本(如 prompt) + * 同样不注入任何对象,通过 evaluate 执行一次性 DOM 操作 + * + * @param {string|string[]} selectors - 目标输入框选择器 + * @param {string} text - 要填入的文本 + * @returns {Promise<{ok: boolean, selector?: string, error?: string}>} + */ + async fill(selectors, text) { + const sels = Array.isArray(selectors) ? selectors : [selectors]; + const loc = await locate(sels); + if (!loc.found) { + return { ok: false, error: 'element_not_found', triedSelectors: sels }; + } + + // 先点击聚焦目标元素 + const { x, y } = humanize(loc.x, loc.y, 2); + await page.mouse.click(x, y); + await randomDelay(100, 200); + + // 在页面上下文中执行文本填充(一次性,不留痕迹) + const result = await page.evaluate((selsInner, textInner) => { + // 重新查找元素(因为 click 后 DOM 可能有变化) + let el = null; + for (const sel of selsInner) { + try { + const all = [...document.querySelectorAll(sel)]; + el = all.find(n => { + const r = n.getBoundingClientRect(); + return r.width > 0 && r.height > 0; + }) || null; + } catch { /* skip */ } + if (el) break; + } + + if (!el) return { ok: false, error: 'element_lost_after_click' }; + + el.focus(); + + if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') { + // 原生表单元素 + el.value = textInner; + el.dispatchEvent(new Event('input', { bubbles: true })); + } else { + // contenteditable 元素(如 Gemini 的富文本输入框) + document.execCommand('selectAll', false, null); + document.execCommand('insertText', false, textInner); + } + return { ok: true }; + }, sels, text); + + return { ...result, selector: loc.selector }; + }, + + /** + * 在页面上下文中执行一次性查询(不注入任何对象) + * + * @param {((...args: any[]) => any)} fn - 要在页面中执行的函数 + * @param {...any} args - 传入函数的参数 + * @returns {Promise} + */ + async query(fn, ...args) { + return page.evaluate(fn, ...args); + }, + + /** + * 等待某个条件满足(轮询式) + * + * @param {((...args: any[]) => any)} conditionFn - 在页面中执行的判断函数,返回 truthy 值表示满足 + * @param {object} [opts] + * @param {number} [opts.timeout=30000] - 最大等待时间(ms) + * @param {number} [opts.interval=500] - 轮询间隔(ms) + * @param {any[]} [opts.args=[]] - 传入 conditionFn 的参数 + * @returns {Promise<{ok: boolean, result?: any, elapsed: number, error?: string}>} + */ + async waitFor(conditionFn, opts = {}) { + const { timeout = 30_000, interval = 500, args = [] } = opts; + const start = Date.now(); + + while (Date.now() - start < timeout) { + try { + const result = await page.evaluate(conditionFn, ...args); + if (result) { + return { ok: true, result, elapsed: Date.now() - start }; + } + } catch { /* 页面可能还在加载 */ } + await new Promise(r => setTimeout(r, interval)); + } + + return { ok: false, error: 'timeout', elapsed: Date.now() - start }; + }, + + /** + * 等待导航完成 + * + * @param {object} [opts] + * @param {string} [opts.waitUntil='networkidle2'] + * @param {number} [opts.timeout=30000] + * @returns {Promise} + */ + async waitForNavigation(opts = {}) { + const { waitUntil = 'networkidle2', timeout = 30_000 } = opts; + await page.waitForNavigation({ waitUntil, timeout }); + }, + + /** + * 按下键盘快捷键 + * + * @param {string} key - 键名(如 'Enter'、'Tab'、'Escape') + * @param {object} [opts] + * @param {number} [opts.delay=50] - keydown 到 keyup 的间隔 + * @returns {Promise<{ok: boolean, key: string}>} + */ + async press(key, opts = {}) { + const { delay = 50 } = opts; + await page.keyboard.press(key, { delay }); + return { ok: true, key }; + }, + + /** + * 页面截图(用于调试或状态验证) + * + * @param {object} [opts] + * @param {boolean} [opts.fullPage=false] + * @param {'png'|'jpeg'|'webp'} [opts.type='png'] + * @param {string} [opts.path] - 保存路径(不传则返回 Buffer) + * @returns {Promise} + */ + async screenshot(opts = {}) { + return page.screenshot(opts); + }, + + /** + * 获取页面当前 URL + * @returns {string} + */ + url() { + return page.url(); + }, + + /** 底层 page 对象引用 */ + get page() { + return page; + }, + }; +}