From f4beac1db2ebd801f58ab2259087daa71710d0cf Mon Sep 17 00:00:00 2001 From: WJZ_P <110795301+WJZ-P@users.noreply.github.com> Date: Sun, 15 Mar 2026 00:32:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(gemini):=20=E6=96=B0=E5=A2=9E=20extractIma?= =?UTF-8?q?geBase64=20=E6=96=B9=E6=B3=95=E4=BC=98=E5=8C=96=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E8=8E=B7=E5=8F=96=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SKILL.md | 6 +-- references/gemini-flow.md | 36 +++++++++++++++--- scripts/gemini_ui_shortcuts.js | 68 +++++++++++++++++++++++++++++++++- 3 files changed, 100 insertions(+), 10 deletions(-) diff --git a/SKILL.md b/SKILL.md index 7c1d288..98431e9 100644 --- a/SKILL.md +++ b/SKILL.md @@ -12,7 +12,7 @@ description: 通过 Gemini 官网(gemini.google.com)执行问答与生图操 3. 文本问答任务(如"问问Gemini xxx")走 Gemini 文本提问链路。 4. 默认模型:可用列表中最强模型,优先 `Gemini 3.1 Pro`。 5. 执行生图后先向用户回报"正在绘图中",完成后回传图片。 -6. **禁止使用浏览器截图(screenshot)获取生成图片**。默认通过右键图片另存为(Save Image As)保存到本地后发送给用户;仅当用户明确要求高清/原图时,才调用 `downloadLatestImage()` 走原图下载流程。 +6. **禁止使用浏览器截图(screenshot)获取生成图片**。默认通过 `extractImageBase64()` 从已渲染的 DOM 直接提取图片 Base64 数据,解码后保存到本地再发送给用户;仅当用户明确要求高清/原图时,才调用 `downloadLatestImage()` 走原图下载流程。 ## 任务分流 @@ -55,12 +55,12 @@ Gemini 页面的操作按钮(`.send-button-container` 内)通过 `aria-label - 定位依据:`` — 只有同时具有 `image` 和 `loaded` 两个 class 的才是已渲染完成的生成图片;DOM 中取最后一个即为最新。 - `src` 为 `https://lh3.googleusercontent.com/...` 格式的原图 URL。 - 若 `ok === false`,等几秒再调一次;连续两次失败则做 snapshot 排查页面状态。 - - **默认**:通过 `src` URL 右键另存为(Save Image As)保存图片到本地,然后发送给用户。 + - **默认**:调用 `GeminiOps.extractImageBase64()` 从 DOM 直接提取图片 Base64(Canvas 优先,跨域污染时 fallback 到 fetch),解码后保存为本地文件发送给用户。注意该函数返回 Promise,CDP 调用时需设置 `awaitPromise:true`。 - **高清**:仅当用户明确要求高清/原图时,才调用 `GeminiOps.downloadLatestImage()` 走原图下载按钮流程。 - 下载按钮定位:从 `img` 向上找到 `.image-container` 容器,容器内的 `mat-icon[fonticon="download"]` 即为下载原图按钮。 - ⚠️ **严禁使用浏览器截图(screenshot)代替保存图片**。 8. 将保存到本地的图片文件发送给用户。 -9. **将每步操作返回的 `debug` 日志一并回传给用户**,方便排查定位失败和优化策略。所有函数(`probe`、`click`、`fillPrompt`、`pollStatus`、`getLatestImage`、`downloadLatestImage`)的返回值都包含 `debug` 字段。 +9. **将每步操作返回的 `debug` 日志一并回传给用户**,方便排查定位失败和优化策略。所有函数(`probe`、`click`、`fillPrompt`、`pollStatus`、`getLatestImage`、`extractImageBase64`、`downloadLatestImage`)的返回值都包含 `debug` 字段。 ## CDP 保活轮询策略 diff --git a/references/gemini-flow.md b/references/gemini-flow.md index 1dd6e65..e2435cd 100644 --- a/references/gemini-flow.md +++ b/references/gemini-flow.md @@ -96,12 +96,37 @@ Gemini 一次只生成一张图片,流程上只关心**最新生成的那张** } ``` -- `GeminiOps.downloadLatestImage()` → 点击最新图片的下载原图按钮 +- `GeminiOps.downloadLatestImage()` → 点击最新图片的下载原图按钮(仅用户要求高清时) ```json {"ok": true, "src": "https://lh3.googleusercontent.com/...", "debug": [...]} ``` +- `GeminiOps.extractImageBase64()` → **默认图片获取方式**,从 DOM 直接提取 Base64 + +```json +{ + "ok": true, + "dataUrl": "data:image/png;base64,iVBORw0KGgo...", + "width": 1024, + "height": 1024, + "method": "canvas", + "debug": [...] +} +``` + + 提取策略(自动选择,无需调用端干预): + 1. **Canvas 提取**(优先):将已渲染的 `` 绘制到虚拟 Canvas,同步导出 `toDataURL('image/png')`。零网络请求,毫秒级完成。`method` 返回 `"canvas"`。 + 2. **Fetch fallback**:若 Canvas 因跨域 tainted 而报错,自动回退到页面内 `fetch(img.src)` → `blob` → `FileReader.readAsDataURL()`。`method` 返回 `"fetch"`。 + + > ⚠️ 该函数返回 **Promise**。CDP 调用时必须设置 `awaitPromise: true`: + > ```js + > // CDP Runtime.evaluate 示例 + > { expression: "GeminiOps.extractImageBase64()", awaitPromise: true, returnByValue: true } + > ``` + + 调用端拿到 `dataUrl` 后,去掉 `data:image/png;base64,` 前缀,解码为二进制存为 `.png` 文件即可。 + - `GeminiOps.probe()` / `click()` / `fillPrompt()` / `pollStatus()` → 同样携带 `debug` 字段 - `GeminiOps.getDebugLog()` → 获取完整累积日志(不清空),用于事后排查 @@ -127,10 +152,11 @@ Gemini 一次只生成一张图片,流程上只关心**最新生成的那张** ### 图片交付流程(重要) -**默认流程(右键另存):** -1. 调用 `GeminiOps.getLatestImage()` 确认图片已渲染完成 -2. 通过返回的 `src` URL,右键图片另存为(Save Image As)保存到本地 -3. 将本地图片文件发送给用户 +**默认流程(Base64 提取):** +1. 调用 `GeminiOps.getLatestImage()` 确认图片已渲染完成(`ok: true`) +2. 调用 `GeminiOps.extractImageBase64()` 提取图片数据(需 `awaitPromise: true`) +3. 去掉 `dataUrl` 的 `data:image/png;base64,` 前缀,解码为二进制,保存为 `.png` 文件 +4. 将本地图片文件发送给用户 **高清流程(仅用户要求时):** 1. 调用 `GeminiOps.getLatestImage()` 确认图片已渲染完成 diff --git a/scripts/gemini_ui_shortcuts.js b/scripts/gemini_ui_shortcuts.js index d988d83..f9b5d51 100644 --- a/scripts/gemini_ui_shortcuts.js +++ b/scripts/gemini_ui_shortcuts.js @@ -193,7 +193,7 @@ }; } - /** 点击最新图片的"下载原图"按钮 */ + /** 点击最新图片的"下载原图"按钮(仅用户要求高清时调用) */ function downloadLatestImage(){ _d('downloadLatestImage','start',true); var imgs=[...document.querySelectorAll('img.image.loaded')]; @@ -217,6 +217,70 @@ 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(); @@ -236,5 +300,5 @@ return {log:_log.slice(), count:_log.length}; } - window.GeminiOps = {probe, click, fillPrompt, getStatus, pollStatus, getLatestImage, downloadLatestImage, getDebugLog, selectors:S, version:'0.8.0'}; + window.GeminiOps = {probe, click, fillPrompt, getStatus, pollStatus, getLatestImage, extractImageBase64, downloadLatestImage, getDebugLog, selectors:S, version:'0.9.0'}; })();