From 9b7c484ab937d70d134566532a797083eb43f311 Mon Sep 17 00:00:00 2001 From: WJZ_P <110795301+WJZ-P@users.noreply.github.com> Date: Sat, 14 Mar 2026 21:27:12 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E5=A4=84=E7=90=86=E7=AD=96=E7=95=A5=E5=B9=B6=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E8=B0=83=E8=AF=95=E6=97=A5=E5=BF=97=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SKILL.md | 10 ++- references/gemini-flow.md | 57 +++++++++++++++-- scripts/gemini_ui_shortcuts.js | 108 +++++++++++++++++++++++++++------ 3 files changed, 150 insertions(+), 25 deletions(-) diff --git a/SKILL.md b/SKILL.md index aceac09..7c1d288 100644 --- a/SKILL.md +++ b/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}`。 - 定位依据:`` — 只有同时具有 `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 保活轮询策略 diff --git a/references/gemini-flow.md b/references/gemini-flow.md index 891902c..1dd6e65 100644 --- a/references/gemini-flow.md +++ b/references/gemini-flow.md @@ -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) 用户提示文案(建议) diff --git a/scripts/gemini_ui_shortcuts.js b/scripts/gemini_ui_shortcuts.js index 1cf0b98..d988d83 100644 --- a/scripts/gemini_ui_shortcuts.js +++ b/scripts/gemini_ui_shortcuts.js @@ -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'}; })();