feat(gemini): 新增 extractImageBase64 方法优化图片获取流程

This commit is contained in:
WJZ_P
2026-03-15 00:32:43 +08:00
parent 9b7c484ab9
commit f4beac1db2
3 changed files with 100 additions and 10 deletions

View File

@@ -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
- 定位依据:`<img class="image loaded">` — 只有同时具有 `image``loaded` 两个 class 的才是已渲染完成的生成图片DOM 中取最后一个即为最新。
- `src``https://lh3.googleusercontent.com/...` 格式的原图 URL。
-`ok === false`,等几秒再调一次;连续两次失败则做 snapshot 排查页面状态。
- **默认**通过 `src` URL 右键另存为Save Image As保存图片到本地然后发送给用户
- **默认**调用 `GeminiOps.extractImageBase64()` 从 DOM 直接提取图片 Base64Canvas 优先,跨域污染时 fallback 到 fetch解码后保存为本地文件发送给用户。注意该函数返回 PromiseCDP 调用时需设置 `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 保活轮询策略

View File

@@ -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 提取**(优先):将已渲染的 `<img>` 绘制到虚拟 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()` 确认图片已渲染完成

View File

@@ -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'};
})();