Files
gemini-skill/scripts/gemini_ui_shortcuts.js

241 lines
7.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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 结构:
* <div class="image-container ...">
* <button class="image-button ...">
* <img class="image loaded" src="https://lh3.googleusercontent.com/..." alt="AI 生成">
* </button>
* <div class="button-icon-wrapper">
* <mat-icon fonticon="download" data-mat-icon-name="download" ...></mat-icon>
* </div>
* </div>
*/
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()};
}
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, downloadLatestImage, getDebugLog, selectors:S, version:'0.8.0'};
})();