docs: 更新图片处理策略并新增调试日志系统
This commit is contained in:
10
SKILL.md
10
SKILL.md
@@ -12,6 +12,7 @@ description: 通过 Gemini 官网(gemini.google.com)执行问答与生图操
|
||||
3. 文本问答任务(如"问问Gemini xxx")走 Gemini 文本提问链路。
|
||||
4. 默认模型:可用列表中最强模型,优先 `Gemini 3.1 Pro`。
|
||||
5. 执行生图后先向用户回报"正在绘图中",完成后回传图片。
|
||||
6. **禁止使用浏览器截图(screenshot)获取生成图片**。默认通过右键图片另存为(Save Image As)保存到本地后发送给用户;仅当用户明确要求高清/原图时,才调用 `downloadLatestImage()` 走原图下载流程。
|
||||
|
||||
## 任务分流
|
||||
|
||||
@@ -50,13 +51,16 @@ Gemini 页面的操作按钮(`.send-button-container` 内)通过 `aria-label
|
||||
5. 发送后立即通知用户:正在绘图中。
|
||||
6. **分段轮询等待**(见下方"CDP 保活轮询策略",生图超时上限 120s)。
|
||||
7. 结果出现后,调用 `GeminiOps.getLatestImage()` 获取最新生成的图片(Gemini 一次只生成一张):
|
||||
- 返回 `{ok, src, alt, width, height, hasDownloadBtn}`。
|
||||
- 返回 `{ok, src, alt, width, height, hasDownloadBtn, debug}`。
|
||||
- 定位依据:`<img class="image loaded">` — 只有同时具有 `image` 和 `loaded` 两个 class 的才是已渲染完成的生成图片;DOM 中取最后一个即为最新。
|
||||
- `src` 为 `https://lh3.googleusercontent.com/...` 格式的原图 URL。
|
||||
- 若 `ok === false`,等几秒再调一次;连续两次失败则做 snapshot 排查页面状态。
|
||||
- 若 `hasDownloadBtn: true`,可调用 `GeminiOps.downloadLatestImage()` 点击原图下载按钮。
|
||||
- **默认**:通过 `src` URL 右键另存为(Save Image As)保存图片到本地,然后发送给用户。
|
||||
- **高清**:仅当用户明确要求高清/原图时,才调用 `GeminiOps.downloadLatestImage()` 走原图下载按钮流程。
|
||||
- 下载按钮定位:从 `img` 向上找到 `.image-container` 容器,容器内的 `mat-icon[fonticon="download"]` 即为下载原图按钮。
|
||||
8. 把图片返回用户。
|
||||
- ⚠️ **严禁使用浏览器截图(screenshot)代替保存图片**。
|
||||
8. 将保存到本地的图片文件发送给用户。
|
||||
9. **将每步操作返回的 `debug` 日志一并回传给用户**,方便排查定位失败和优化策略。所有函数(`probe`、`click`、`fillPrompt`、`pollStatus`、`getLatestImage`、`downloadLatestImage`)的返回值都包含 `debug` 字段。
|
||||
|
||||
## CDP 保活轮询策略
|
||||
|
||||
|
||||
@@ -37,6 +37,10 @@
|
||||
|
||||
## 4) 生图结果获取
|
||||
|
||||
> ⚠️ **严禁使用浏览器截图(screenshot)获取生成图片。**
|
||||
> - **默认流程**:通过 `src` URL 右键另存为(Save Image As)保存到本地,再发送给用户。
|
||||
> - **高清流程**:仅当用户明确要求高清/原图时,才调用 `downloadLatestImage()` 点击原图下载按钮。
|
||||
|
||||
Gemini 一次只生成一张图片,流程上只关心**最新生成的那张**,历史图片不做处理。
|
||||
|
||||
调用 `GeminiOps.getLatestImage()` 获取最新一张生成图片。
|
||||
@@ -70,6 +74,8 @@ Gemini 一次只生成一张图片,流程上只关心**最新生成的那张**
|
||||
|
||||
### API
|
||||
|
||||
所有操作函数的返回值都包含 `debug` 字段,记录该次调用每一步的日志(含时间戳、步骤名、成功/失败、上下文详情),方便排查问题和改进策略。
|
||||
|
||||
- `GeminiOps.getLatestImage()` → 获取最新一张图片信息
|
||||
|
||||
```json
|
||||
@@ -79,21 +85,64 @@ Gemini 一次只生成一张图片,流程上只关心**最新生成的那张**
|
||||
"alt": "AI 生成",
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"hasDownloadBtn": true
|
||||
"hasDownloadBtn": true,
|
||||
"debug": [
|
||||
{"ts": 1710000000000, "fn": "getLatestImage", "step": "start", "ok": true},
|
||||
{"ts": 1710000000001, "fn": "getLatestImage", "step": "query_imgs", "ok": true, "detail": {"totalFound": 1}},
|
||||
{"ts": 1710000000002, "fn": "getLatestImage", "step": "picked_latest", "ok": true, "detail": {"index": 0, "src": "https://lh3.google..."}},
|
||||
{"ts": 1710000000003, "fn": "getLatestImage", "step": "find_container", "ok": true},
|
||||
{"ts": 1710000000004, "fn": "getLatestImage", "step": "find_download_btn", "ok": true}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `GeminiOps.downloadLatestImage()` → 点击最新图片的下载原图按钮
|
||||
|
||||
```json
|
||||
{"ok": true, "src": "https://lh3.googleusercontent.com/..."}
|
||||
{"ok": true, "src": "https://lh3.googleusercontent.com/...", "debug": [...]}
|
||||
```
|
||||
|
||||
- `GeminiOps.probe()` / `click()` / `fillPrompt()` / `pollStatus()` → 同样携带 `debug` 字段
|
||||
|
||||
- `GeminiOps.getDebugLog()` → 获取完整累积日志(不清空),用于事后排查
|
||||
|
||||
```json
|
||||
{"log": [...], "count": 15}
|
||||
```
|
||||
|
||||
### debug 日志格式
|
||||
|
||||
每条日志条目:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `ts` | number | 毫秒级时间戳 |
|
||||
| `fn` | string | 函数名,如 `click`、`getLatestImage` |
|
||||
| `step` | string | 步骤名,如 `start`、`find_container`、`clicked` |
|
||||
| `ok` | boolean | 该步骤是否成功 |
|
||||
| `detail` | object? | 可选,上下文信息(匹配的选择器、找到的元素数量等) |
|
||||
|
||||
调用端应将 `debug` 数组回传给用户,便于分析定位失败原因和优化选择器策略。
|
||||
```
|
||||
|
||||
### 图片交付流程(重要)
|
||||
|
||||
**默认流程(右键另存):**
|
||||
1. 调用 `GeminiOps.getLatestImage()` 确认图片已渲染完成
|
||||
2. 通过返回的 `src` URL,右键图片另存为(Save Image As)保存到本地
|
||||
3. 将本地图片文件发送给用户
|
||||
|
||||
**高清流程(仅用户要求时):**
|
||||
1. 调用 `GeminiOps.getLatestImage()` 确认图片已渲染完成
|
||||
2. 调用 `GeminiOps.downloadLatestImage()` 点击原图下载按钮
|
||||
3. 将下载到本地的高清原图文件发送给用户
|
||||
|
||||
> **严禁**在任何环节使用浏览器截图(screenshot)代替保存图片。
|
||||
|
||||
### 回退
|
||||
|
||||
- `ok === false` → 页面可能还在渲染,等几秒再调一次
|
||||
- 连续两次失败 → 做 snapshot 排查页面状态
|
||||
- `hasDownloadBtn: false` → 回退到直接用 `src` URL 下载
|
||||
- 连续两次失败 → 做 snapshot 排查页面状态(snapshot 仅用于排查,不用于交付图片)
|
||||
|
||||
## 5) 用户提示文案(建议)
|
||||
|
||||
|
||||
@@ -22,6 +22,24 @@
|
||||
]
|
||||
};
|
||||
|
||||
/* ── 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();
|
||||
@@ -44,40 +62,66 @@
|
||||
function find(key){
|
||||
for(const s of (S[key]||[])){
|
||||
const el=q(s);
|
||||
if(el) return el;
|
||||
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) return {ok:false,key,error:'not_found'};
|
||||
if(!el){
|
||||
_d('click','element_not_found',false,{key:key});
|
||||
return {ok:false,key,error:'not_found',debug:_flush()};
|
||||
}
|
||||
el.click();
|
||||
return {ok:true,key};
|
||||
_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) return {ok:false,error:'prompt_not_found'};
|
||||
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};
|
||||
return {ok:true,debug:_flush()};
|
||||
}
|
||||
|
||||
function getStatus(){
|
||||
const btn=find('actionBtn');
|
||||
if(!btn) return {status:'unknown',error:'btn_not_found'};
|
||||
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)) return {status:'loading',label};
|
||||
if(/发送|Send|Submit/i.test(label)) return {status:'ready',label,disabled};
|
||||
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};
|
||||
}
|
||||
|
||||
@@ -88,8 +132,8 @@
|
||||
*/
|
||||
function pollStatus(){
|
||||
var s=getStatus();
|
||||
// 顺便返回页面可见性,帮助调用端判断 tab 是否还活着
|
||||
return {status:s.status, label:s.label, pageVisible:!document.hidden, ts:Date.now()};
|
||||
_d('pollStatus','polled',true,{status:s.status});
|
||||
return {status:s.status, label:s.label, pageVisible:!document.hidden, ts:Date.now(), debug:_flush()};
|
||||
}
|
||||
|
||||
/* ── 最新图片获取与下载 ──
|
||||
@@ -125,44 +169,72 @@
|
||||
|
||||
/** 获取最新生成的一张图片信息(DOM 中最后一个 img.image.loaded) */
|
||||
function getLatestImage(){
|
||||
_d('getLatestImage','start',true);
|
||||
var imgs=[...document.querySelectorAll('img.image.loaded')];
|
||||
if(!imgs.length) return {ok:false, error:'no_loaded_images'};
|
||||
_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
|
||||
hasDownloadBtn: !!dlBtn,
|
||||
debug: _flush()
|
||||
};
|
||||
}
|
||||
|
||||
/** 点击最新图片的"下载原图"按钮 */
|
||||
function downloadLatestImage(){
|
||||
_d('downloadLatestImage','start',true);
|
||||
var imgs=[...document.querySelectorAll('img.image.loaded')];
|
||||
if(!imgs.length) return {ok:false, error:'no_loaded_images'};
|
||||
_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) return {ok:false, error:'download_btn_not_found'};
|
||||
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();
|
||||
return {ok:true, src:img.src||''};
|
||||
_d('downloadLatestImage','clicked',true,{clickedTag:clickable.tagName});
|
||||
return {ok:true, src:img.src||'', debug:_flush()};
|
||||
}
|
||||
|
||||
function probe(){
|
||||
_d('probe','start',true);
|
||||
var s=getStatus();
|
||||
return {
|
||||
var result={
|
||||
promptInput: !!find('promptInput'),
|
||||
actionBtn: !!find('actionBtn'),
|
||||
newChatBtn: !!find('newChatBtn'),
|
||||
modelBtn: !!find('modelBtn'),
|
||||
status: s.status
|
||||
status: s.status,
|
||||
debug: _flush()
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
window.GeminiOps = {probe, click, fillPrompt, getStatus, pollStatus, getLatestImage, downloadLatestImage, selectors:S, version:'0.7.0'};
|
||||
/** 获取完整调试日志(不清空) */
|
||||
function getDebugLog(){
|
||||
return {log:_log.slice(), count:_log.length};
|
||||
}
|
||||
|
||||
window.GeminiOps = {probe, click, fillPrompt, getStatus, pollStatus, getLatestImage, downloadLatestImage, getDebugLog, selectors:S, version:'0.8.0'};
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user