// ==UserScript== // @name 图片快捷跳转处理 // @namespace https://local.codex/gpt-img-to-koukoutu // @version 1.3.4 // @description 在任意网页按快捷键抓取当前图片,并跳转到抠抠图、VTracer、Gemini 或 Imagvio 页面自动上传处理。 // @author 木木 // @license MIT // @match https://*/* // @match http://*/* // @match https://www.koukoutu.com/* // @noframes // @connect * // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_xmlhttpRequest // @grant GM_addValueChangeListener // @grant GM_registerMenuCommand // @run-at document-start // ==/UserScript== /* ==UserConfig== shortcuts: koukoutu: title: 跳转抠图快捷键 type: text default: "Alt+K" vtracer: title: 跳转 矢量 快捷键 type: text default: "Alt+L" gemini: title: 跳转 高清 快捷键 type: text default: "Alt+O" imagvio: title: 跳转 放大 快捷键 type: text default: "Alt+P" ==/UserConfig== */ (function () { 'use strict'; const STORE_KEY = 'gpt_img_to_koukoutu_pending_image'; const VTRACER_STORE_KEY = 'gpt_img_to_vtracer_pending_image'; const GEMINI_STORE_KEY = 'gpt_img_to_gemini_pending_image'; const IMAGVIO_STORE_KEY = 'gpt_img_to_imagvio_pending_image'; const SOURCE_HOSTS_KEY = 'gpt_img_to_koukoutu_source_hosts'; const KOUKOUTU_BASE_URL = 'https://www.koukoutu.com/removebgtool/all'; const VTRACER_BASE_URL = 'https://www.visioncortex.org/vtracer/'; const GEMINI_BASE_URL = 'https://aistudio.google.com/prompts/new_chat?model=gemini-3-pro-image'; const IMAGVIO_BASE_URL = 'https://imagvio.ai/zh/ai-image-upscaler'; const KOUKOUTU_TAB_NAME = 'gpt_image_to_koukoutu_tab'; const VTRACER_TAB_NAME = 'gpt_image_to_vtracer_tab'; const GEMINI_TAB_NAME = 'gpt_image_to_gemini_tab'; const IMAGVIO_TAB_NAME = 'gpt_image_to_imagvio_tab'; const MAX_AGE_MS = 30 * 60 * 1000; const DEFAULT_SOURCE_HOSTS = [ 'gpt-img-2.hf.space', 'www.xiaohongshu.com', 'xiaohongshu.com' ]; const TXT = { ready: '\u51c6\u5907\u5c31\u7eea', sourceTitle: '\u53d1\u9001\u5230\u62a0\u62a0\u56fe', sourceHint: 'Alt+K \u53ef\u5728\u5927\u56fe\u9884\u89c8\u65f6\u76f4\u63a5\u53d1\u9001', sendCurrent: '\u53d1\u9001\u5f53\u524d\u56fe', overlaySend: '\u53d1\u9001\u5230\u62a0\u62a0\u56fe', openKoukoutu: '\u6253\u5f00\u62a0\u56fe', finding: '\u6b63\u5728\u5bfb\u627e\u751f\u6210\u56fe...', noImage: '\u6ca1\u627e\u5230\u751f\u6210\u56fe\u3002\u8bf7\u7b49\u56fe\u7247\u751f\u6210\u5b8c\u6210\u540e\u518d\u8bd5\u3002', reading: '\u6b63\u5728\u8bfb\u53d6\u56fe\u7247...', saved: '\u5df2\u4fdd\u5b58\uff0c\u6b63\u5728\u6253\u5f00\u62a0\u56fe\u9875...', candidate: '\u5df2\u8bc6\u522b\u5019\u9009\u56fe', targetTitle: '\u63a5\u6536 GPT \u56fe\u7247', importPending: '\u5bfc\u5165\u5f85\u5904\u7406', clear: '\u6e05\u7a7a', waitingInput: '\u7b49\u5f85\u4e0a\u4f20\u5165\u53e3', imported: '\u5df2\u5bfc\u5165\u56fe\u7247\uff0c\u6b63\u5728\u5c1d\u8bd5\u81ea\u52a8\u70b9\u51fb\u5f00\u59cb\u62a0\u56fe...', noInput: '\u6ca1\u627e\u5230\u4e0a\u4f20\u5165\u53e3\uff0c\u8bf7\u5237\u65b0\u9875\u9762\u540e\u518d\u70b9\u5bfc\u5165\u3002', noPending: '\u6ca1\u6709\u5f85\u5904\u7406\u56fe\u7247\uff0c\u6216\u56fe\u7247\u5df2\u8d85\u8fc7 30 \u5206\u949f\u3002', pendingFound: '\u68c0\u6d4b\u5230\u5f85\u5904\u7406\u56fe\u7247\uff0c\u51c6\u5907\u81ea\u52a8\u5bfc\u5165...', cleared: '\u5df2\u6e05\u7a7a\u5f85\u5904\u7406\u56fe\u7247\u3002', startClicked: '\u5df2\u70b9\u51fb\u5f00\u59cb\u62a0\u56fe\u3002', noStart: '\u5df2\u5bfc\u5165\uff0c\u4f46\u6ca1\u627e\u5230\u53ef\u70b9\u51fb\u7684\u5f00\u59cb\u6309\u94ae\u3002', loginPopup: '\u68c0\u6d4b\u5230\u767b\u5f55\u5f39\u7a97\uff0c\u5df2\u5c1d\u8bd5\u5173\u95ed\u5e76\u7b49\u5f85\u9875\u9762\u6062\u590d...', loginStill: '\u767b\u5f55\u5f39\u7a97\u4ecd\u7136\u51fa\u73b0\uff0c\u8bf7\u624b\u52a8\u5173\u95ed\u540e\u70b9\u201c\u5f00\u59cb\u62a0\u56fe\u201d\u3002', addTitle: '\u6dfb\u52a0\u5230\u62a0\u56fe\u6765\u6e90', addHint: '\u6dfb\u52a0\u672c\u7ad9\u540e\uff0c\u6b64\u7f51\u7ad9\u4f1a\u663e\u793a\u53d1\u9001\u9762\u677f', addSite: '\u6dfb\u52a0\u672c\u7ad9', hidePanel: '\u9690\u85cf', siteAdded: '\u5df2\u6dfb\u52a0\u672c\u7ad9\uff0c\u5373\u5c06\u5207\u6362\u5230\u53d1\u9001\u9762\u677f\u3002' }; const isKoukoutuPage = location.hostname === 'www.koukoutu.com'; const isVTracerPage = location.hostname === 'www.visioncortex.org' && location.pathname.startsWith('/vtracer'); const isGeminiPage = location.hostname === 'aistudio.google.com' && location.pathname.startsWith('/prompts/new_chat'); const isImagvioPage = location.hostname === 'imagvio.ai' && location.pathname.startsWith('/zh/ai-image-upscaler'); const isTargetPage = isKoukoutuPage || isVTracerPage || isGeminiPage || isImagvioPage; let targetImporting = false; let lastPointer = { x: 0, y: 0, at: 0 }; function ready(fn) { if (document.body) { fn(); return; } const timer = setInterval(() => { if (!document.body) return; clearInterval(timer); fn(); }, 100); } function normalizeHost(host) { return String(host || '').trim().toLowerCase().replace(/^https?:\/\//, '').replace(/\/.*$/, ''); } function uniqueHosts(hosts) { return Array.from(new Set(hosts.map(normalizeHost).filter(Boolean))).sort(); } async function getSourceHosts() { const saved = await GM_getValue(SOURCE_HOSTS_KEY, null); if (Array.isArray(saved) && saved.length) return uniqueHosts(saved); return uniqueHosts(DEFAULT_SOURCE_HOSTS); } async function setSourceHosts(hosts) { const next = uniqueHosts(hosts); await GM_setValue(SOURCE_HOSTS_KEY, next); return next; } function splitShortcutList(value) { return String(value || '') .split(/[,;,;\n]/) .map(item => item.trim()) .filter(Boolean); } function parseShortcut(value) { const raw = String(value || '').trim(); if (!raw) return null; const parts = raw.split('+').map(part => part.trim()).filter(Boolean); const shortcut = { ctrl: false, shift: false, alt: false, meta: false, key: '' }; for (const part of parts) { const token = part.toLowerCase(); if (token === 'ctrl' || token === 'control') shortcut.ctrl = true; else if (token === 'shift') shortcut.shift = true; else if (token === 'alt' || token === 'option') shortcut.alt = true; else if (token === 'meta' || token === 'cmd' || token === 'command' || token === 'win') shortcut.meta = true; else shortcut.key = part; } if (!shortcut.key) return null; const keyMap = { space: ' ', comma: ',', period: '.', slash: '/', backslash: '\\', escape: 'Escape', esc: 'Escape', enter: 'Enter', tab: 'Tab' }; shortcut.key = keyMap[shortcut.key.toLowerCase()] || shortcut.key; return shortcut; } function parseShortcutList(value, fallback) { const parsed = splitShortcutList(value) .map(parseShortcut) .filter(Boolean); if (parsed.length) return parsed; return splitShortcutList(fallback).map(parseShortcut).filter(Boolean); } function shortcutMatches(event, shortcut) { if (!shortcut) return false; const eventKey = event.key.length === 1 ? event.key.toLowerCase() : event.key; const shortcutKey = shortcut.key.length === 1 ? shortcut.key.toLowerCase() : shortcut.key; return event.ctrlKey === shortcut.ctrl && event.shiftKey === shortcut.shift && event.altKey === shortcut.alt && event.metaKey === shortcut.meta && eventKey === shortcutKey; } function eventToShortcutText(event) { if (event.key === 'Backspace' || event.key === 'Delete') return ''; if (['Control', 'Shift', 'Alt', 'Meta'].includes(event.key)) return ''; const parts = []; if (event.ctrlKey) parts.push('Ctrl'); if (event.shiftKey) parts.push('Shift'); if (event.altKey) parts.push('Alt'); if (event.metaKey) parts.push('Meta'); const keyMap = { ' ': 'Space', Escape: 'Esc', ArrowUp: 'ArrowUp', ArrowDown: 'ArrowDown', ArrowLeft: 'ArrowLeft', ArrowRight: 'ArrowRight' }; const key = keyMap[event.key] || (event.key.length === 1 ? event.key.toUpperCase() : event.key); parts.push(key); return parts.join('+'); } async function getShortcutSettings() { return { koukoutu: parseShortcutList(await GM_getValue('shortcuts.koukoutu', 'Alt+K'), 'Alt+K'), vtracer: parseShortcutList(await GM_getValue('shortcuts.vtracer', 'Alt+L'), 'Alt+L'), gemini: parseShortcutList(await GM_getValue('shortcuts.gemini', 'Alt+O'), 'Alt+O'), imagvio: parseShortcutList(await GM_getValue('shortcuts.imagvio', 'Alt+P'), 'Alt+P') }; } function findShortcutTarget(event, shortcuts) { if (shortcuts.koukoutu.some(shortcut => shortcutMatches(event, shortcut))) return 'koukoutu'; if (shortcuts.vtracer.some(shortcut => shortcutMatches(event, shortcut))) return 'vtracer'; if (shortcuts.gemini.some(shortcut => shortcutMatches(event, shortcut))) return 'gemini'; if (shortcuts.imagvio.some(shortcut => shortcutMatches(event, shortcut))) return 'imagvio'; return ''; } async function createPreferenceUI() { document.getElementById('g2k-preference-mask')?.remove(); const currentKoukoutu = await GM_getValue('shortcuts.koukoutu', 'Alt+K'); const currentVTracer = await GM_getValue('shortcuts.vtracer', 'Alt+L'); const currentGemini = await GM_getValue('shortcuts.gemini', 'Alt+O'); const currentImagvio = await GM_getValue('shortcuts.imagvio', 'Alt+P'); const mask = document.createElement('div'); mask.id = 'g2k-preference-mask'; mask.style.cssText = ` position: fixed; inset: 0; z-index: 2147483647; background: rgba(15, 23, 42, .42); display: flex; align-items: center; justify-content: center; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; `; const dialog = document.createElement('div'); dialog.style.cssText = ` width: min(720px, calc(100vw - 32px)); border-radius: 18px; background: #fff; box-shadow: 0 28px 90px rgba(15, 23, 42, .28); overflow: hidden; color: #111827; `; dialog.innerHTML = `
设置首选项
点击输入框后按下新的组合键,保存并刷新当前网页后生效。
点击输入框后直接按组合键。
BackspaceDelete 可清空当前项。
`; mask.appendChild(dialog); document.body.appendChild(mask); const close = () => mask.remove(); mask.addEventListener('click', event => { if (event.target === mask) close(); }); dialog.querySelector('[data-close]').addEventListener('click', close); dialog.querySelectorAll('input[data-key]').forEach(input => { input.dataset.originalValue = input.value; input.addEventListener('focus', () => { input.dataset.originalValue = input.value; input.value = ''; input.placeholder = '请按下新的快捷键组合...'; input.style.borderColor = '#111827'; input.style.boxShadow = '0 0 0 3px rgba(17, 24, 39, .12)'; }); input.addEventListener('blur', () => { if (!input.value && input.dataset.originalValue) input.value = input.dataset.originalValue; input.style.borderColor = '#d1d5db'; input.style.boxShadow = 'none'; }); input.addEventListener('keydown', event => { event.preventDefault(); event.stopPropagation(); const value = eventToShortcutText(event); if (value || event.key === 'Backspace' || event.key === 'Delete') { input.value = value; input.dataset.originalValue = value; input.blur(); } }); }); dialog.querySelector('[data-reset]').addEventListener('click', async () => { dialog.querySelector('[data-key="koukoutu"]').value = 'Alt+K'; dialog.querySelector('[data-key="vtracer"]').value = 'Alt+L'; dialog.querySelector('[data-key="gemini"]').value = 'Alt+O'; dialog.querySelector('[data-key="imagvio"]').value = 'Alt+P'; }); dialog.querySelector('[data-save]').addEventListener('click', async () => { const koukoutu = dialog.querySelector('[data-key="koukoutu"]').value.trim() || 'Alt+K'; const vtracer = dialog.querySelector('[data-key="vtracer"]').value.trim() || 'Alt+L'; const gemini = dialog.querySelector('[data-key="gemini"]').value.trim() || 'Alt+O'; const imagvio = dialog.querySelector('[data-key="imagvio"]').value.trim() || 'Alt+P'; await GM_setValue('shortcuts.koukoutu', koukoutu); await GM_setValue('shortcuts.vtracer', vtracer); await GM_setValue('shortcuts.gemini', gemini); await GM_setValue('shortcuts.imagvio', imagvio); alert('快捷键已保存,当前页面会尽量立即生效;如果没有生效,请刷新网页。'); close(); }); } function registerShortcutMenu() { if (typeof GM_registerMenuCommand !== 'function') return; GM_registerMenuCommand('⚙️ 设置首选项', createPreferenceUI); GM_registerMenuCommand('恢复默认快捷键', async () => { await GM_setValue('shortcuts.koukoutu', 'Alt+K'); await GM_setValue('shortcuts.vtracer', 'Alt+L'); await GM_setValue('shortcuts.gemini', 'Alt+O'); await GM_setValue('shortcuts.imagvio', 'Alt+P'); alert('\u5df2\u6062\u590d\u9ed8\u8ba4\u5feb\u6377\u952e\uff0c\u5237\u65b0\u9875\u9762\u540e\u751f\u6548\u3002'); }); } function hostMatchesPattern(host, pattern) { const cleanHost = normalizeHost(host); const cleanPattern = normalizeHost(pattern); if (!cleanHost || !cleanPattern) return false; if (cleanPattern === '*') return true; if (cleanPattern.startsWith('*.')) { const root = cleanPattern.slice(2); return cleanHost === root || cleanHost.endsWith(`.${root}`); } return cleanHost === cleanPattern; } async function isAllowedSourceHost(host) { if (isTargetPage) return false; const hosts = await getSourceHosts(); return hosts.some(pattern => hostMatchesPattern(host, pattern)); } function registerMenuCommands() { if (typeof GM_registerMenuCommand !== 'function') return; const currentHost = normalizeHost(location.hostname); if (!currentHost || isTargetPage) return; GM_registerMenuCommand('\u6dfb\u52a0\u5f53\u524d\u7f51\u7ad9\u4e3a\u6765\u6e90', async () => { const hosts = await getSourceHosts(); if (!hosts.some(pattern => hostMatchesPattern(currentHost, pattern))) { await setSourceHosts([...hosts, currentHost]); } alert(`\u5df2\u6dfb\u52a0\uff1a${currentHost}\n\u5237\u65b0\u9875\u9762\u540e\u663e\u793a\u53d1\u9001\u9762\u677f\u3002`); }); GM_registerMenuCommand('\u79fb\u9664\u5f53\u524d\u7f51\u7ad9\u6765\u6e90', async () => { const hosts = await getSourceHosts(); const next = hosts.filter(pattern => !hostMatchesPattern(currentHost, pattern)); await setSourceHosts(next); alert(`\u5df2\u79fb\u9664\uff1a${currentHost}\n\u5237\u65b0\u9875\u9762\u540e\u751f\u6548\u3002`); }); GM_registerMenuCommand('\u67e5\u770b\u5df2\u6dfb\u52a0\u7f51\u7ad9', async () => { const hosts = await getSourceHosts(); alert(`\u5df2\u6dfb\u52a0\u7684\u6765\u6e90\u7f51\u7ad9\uff1a\n\n${hosts.join('\n')}`); }); GM_registerMenuCommand('\u91cd\u7f6e\u9ed8\u8ba4\u6765\u6e90\u7f51\u7ad9', async () => { await setSourceHosts(DEFAULT_SOURCE_HOSTS); alert('\u5df2\u91cd\u7f6e\u4e3a\u9ed8\u8ba4\u6765\u6e90\u7f51\u7ad9\u3002'); }); } function installStyle() { const css = ` .g2k-panel { position: fixed !important; right: 16px !important; bottom: 18px !important; z-index: 2147483647 !important; width: 248px; padding: 12px; border: 1px solid rgba(28, 25, 23, .12); border-radius: 14px; background: rgba(255, 255, 255, .97); box-shadow: 0 18px 56px rgba(15, 23, 42, .22); color: #1c1917; font: 13px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; backdrop-filter: blur(10px); pointer-events: auto !important; } .g2k-title { margin: 0 0 6px; font-size: 13px; font-weight: 700; } .g2k-hint { margin: 0 0 8px; color: #78716c; font-size: 11px; } .g2k-row { display: flex; gap: 8px; margin-top: 8px; } .g2k-btn { flex: 1; border: 0; border-radius: 10px; padding: 8px 9px; background: #18181b; color: #fff; cursor: pointer; font-size: 12px; font-weight: 650; } .g2k-btn.secondary { background: #f4f4f5; color: #27272a; } .g2k-btn:disabled { cursor: not-allowed; opacity: .55; } .g2k-status { min-height: 18px; color: #57534e; word-break: break-word; } .g2k-overlay-send { position: fixed !important; right: 16px !important; top: 52px !important; z-index: 2147483647 !important; border: 0; border-radius: 999px; padding: 9px 14px; background: #ffffff; color: #18181b; box-shadow: 0 10px 34px rgba(0, 0, 0, .28); cursor: pointer; font: 12px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; font-weight: 700; pointer-events: auto !important; } `; try { GM_addStyle(css); } catch { const style = document.createElement('style'); style.textContent = css; document.documentElement.appendChild(style); } } function makePanel(title, hint) { installStyle(); document.querySelectorAll('.g2k-panel').forEach(el => el.remove()); const panel = document.createElement('div'); panel.className = 'g2k-panel'; panel.innerHTML = `
${title}
${hint ? `
${hint}
` : ''}
${TXT.ready}
`; document.body.appendChild(panel); return { root: panel, status: panel.querySelector('.g2k-status'), row: panel.querySelector('.g2k-row'), setStatus(text) { this.status.textContent = text; }, addButton(text, className, onClick) { const btn = document.createElement('button'); btn.type = 'button'; btn.className = `g2k-btn ${className || ''}`.trim(); btn.textContent = text; btn.addEventListener('click', onClick); this.row.appendChild(btn); return btn; } }; } function makeSilentStatus(scope) { return { silent: true, scope, setStatus(text) { console.log(`[GPT Image 内部网页跳转抠图][${scope}]`, text); } }; } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function getTargetUrl(target) { const stamp = Date.now(); if (target === 'vtracer') return `${VTRACER_BASE_URL}?g2k_ts=${stamp}#from-gpt-img-${stamp}`; if (target === 'gemini') return `${GEMINI_BASE_URL}&g2k_ts=${stamp}#from-gpt-img-${stamp}`; if (target === 'imagvio') return `${IMAGVIO_BASE_URL}?g2k_ts=${stamp}#from-gpt-img-${stamp}`; return `${KOUKOUTU_BASE_URL}?g2k_ts=${stamp}#from-gpt-img-${stamp}`; } function getTargetTabName(target) { if (target === 'vtracer') return VTRACER_TAB_NAME; if (target === 'gemini') return GEMINI_TAB_NAME; if (target === 'imagvio') return IMAGVIO_TAB_NAME; return KOUKOUTU_TAB_NAME; } function openTargetShell(target) { return window.open('about:blank', getTargetTabName(target)); } function navigateTargetPage(targetWindow, target) { const url = getTargetUrl(target); if (targetWindow && !targetWindow.closed) { targetWindow.location.href = url; try { targetWindow.focus(); } catch {} return; } window.open(url, getTargetTabName(target)); } function isVisible(el) { if (!el || !el.isConnected) return false; const rect = el.getBoundingClientRect(); const style = getComputedStyle(el); return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden' && Number(style.opacity || 1) > 0; } function visibleArea(el) { const rect = el.getBoundingClientRect(); if (rect.width < 80 || rect.height < 80) return 0; if (rect.bottom < 0 || rect.right < 0 || rect.top > innerHeight || rect.left > innerWidth) return 0; return Math.round(rect.width * rect.height); } function normalizeUrl(url) { try { return new URL(url, location.href).toString(); } catch { return url; } } function queryAllDeep(selector, root = document) { const results = Array.from(root.querySelectorAll(selector)); for (const el of root.querySelectorAll('*')) { if (el.shadowRoot) results.push(...queryAllDeep(selector, el.shadowRoot)); } return results; } function bestFromSrcset(srcset) { return String(srcset || '') .split(',') .map(item => { const parts = item.trim().split(/\s+/); const url = parts[0] || ''; const mark = parts[1] || '1x'; const score = mark.endsWith('w') ? parseFloat(mark) : mark.endsWith('x') ? parseFloat(mark) * 10000 : 1; return { url, score: Number.isFinite(score) ? score : 1 }; }) .filter(item => item.url) .sort((a, b) => b.score - a.score)[0]?.url || ''; } function cleanImageUrl(url) { const raw = String(url || '').trim(); if (!raw || raw === 'none') return ''; const unwrapped = raw.replace(/^url\((['"]?)(.*?)\1\)$/i, '$2').trim(); if (!unwrapped || unwrapped.startsWith('chrome-extension:')) return ''; return normalizeUrl(unwrapped); } function looksLikeBlockedImageUrl(url) { const lower = String(url || '').toLowerCase(); return /\b(logo|favicon|icon|avatar|emoji|sprite|placeholder|loading)\b/.test(lower) || lower.endsWith('.svg') || lower.includes('data:image/svg'); } function extractCssImageUrls(value) { const urls = []; String(value || '').replace(/url\((['"]?)(.*?)\1\)/gi, (_all, _quote, url) => { const clean = cleanImageUrl(url); if (clean) urls.push(clean); return ''; }); return urls; } function getImgSource(img) { const attrs = [ 'data-src', 'data-original', 'data-origin', 'data-url', 'data-image', 'data-img', 'data-large', 'data-full', 'data-fullsrc', 'data-actualsrc', 'data-lazy-src', 'data-raw', 'origin-src' ]; const urls = [ img.currentSrc, img.src, bestFromSrcset(img.srcset) ]; for (const attr of attrs) urls.push(img.getAttribute(attr)); for (const source of img.closest('picture')?.querySelectorAll('source') || []) { urls.push(bestFromSrcset(source.getAttribute('srcset'))); } return urls.map(cleanImageUrl).find(url => url && !looksLikeBlockedImageUrl(url)) || ''; } function pointerScore(el) { if (!lastPointer.at || Date.now() - lastPointer.at > 15000) return 0; const rect = el.getBoundingClientRect(); if (lastPointer.x >= rect.left && lastPointer.x <= rect.right && lastPointer.y >= rect.top && lastPointer.y <= rect.bottom) { return 90000000; } const cx = rect.left + rect.width / 2; const cy = rect.top + rect.height / 2; const dx = Math.abs(cx - lastPointer.x) / Math.max(1, innerWidth); const dy = Math.abs(cy - lastPointer.y) / Math.max(1, innerHeight); return Math.max(0, 1800000 - (dx + dy) * 1800000); } function makeImageCandidate(type, el, src, baseScore, naturalWidth, naturalHeight) { const area = visibleArea(el); if (!area) return null; const rect = el.getBoundingClientRect(); const centerDx = Math.abs(rect.left + rect.width / 2 - innerWidth / 2) / innerWidth; const centerDy = Math.abs(rect.top + rect.height / 2 - innerHeight / 2) / innerHeight; const viewportArea = Math.max(1, innerWidth * innerHeight); let score = area + baseScore - (centerDx + centerDy) * 100000 + pointerScore(el); if (area / viewportArea > 0.22) score += 80000000; if (src && src.startsWith('data:image/')) score += 50000000; if (el.closest('[role="dialog"], [aria-modal="true"], [class*="modal"], [class*="preview"], [class*="lightbox"]')) score += 3000000; return { type, el, src, score, area, width: Math.round(naturalWidth || rect.width), height: Math.round(naturalHeight || rect.height) }; } function findBestImage() { const candidates = []; for (const img of queryAllDeep('img')) { const src = getImgSource(img); if (!src) continue; if ((img.naturalWidth && img.naturalWidth < 80) || (img.naturalHeight && img.naturalHeight < 80)) continue; const alt = (img.alt || '').toLowerCase(); let baseScore = 0; if (alt.includes('generated result') || alt.includes('生成') || alt.includes('result')) baseScore += 100000000; if (img.closest('button[aria-label*="\u9884\u89c8"], button[class*="cursor-zoom"]')) baseScore += 1000000; if (/\/images\//.test(src.toLowerCase())) baseScore -= 5000000; const candidate = makeImageCandidate('img', img, src, baseScore, img.naturalWidth, img.naturalHeight); if (candidate) candidates.push(candidate); } for (const canvas of queryAllDeep('canvas')) { const area = visibleArea(canvas); if (!area || canvas.width < 128 || canvas.height < 128) continue; candidates.push({ type: 'canvas', el: canvas, score: area + 100000 + pointerScore(canvas), area, width: canvas.width, height: canvas.height }); } for (const el of queryAllDeep('a[href], [style], [role="img"], div, section, article')) { const area = visibleArea(el); if (!area) continue; const rect = el.getBoundingClientRect(); if (rect.width < 120 || rect.height < 120) continue; const href = el.matches('a[href]') ? cleanImageUrl(el.getAttribute('href')) : ''; if (href && !looksLikeBlockedImageUrl(href) && /\.(png|jpe?g|webp|avif|gif)([?#].*)?$/i.test(href)) { const candidate = makeImageCandidate('url', el, href, 500000, rect.width, rect.height); if (candidate) candidates.push(candidate); } const style = getComputedStyle(el); for (const src of extractCssImageUrls(style.backgroundImage || style.content)) { if (!src || looksLikeBlockedImageUrl(src)) continue; const candidate = makeImageCandidate('url', el, src, 250000, rect.width, rect.height); if (candidate) candidates.push(candidate); } } candidates.sort((a, b) => b.score - a.score); return candidates[0] || null; } function gmBlob(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url, responseType: 'blob', onload(resp) { if (resp.status >= 200 && resp.status < 300 && resp.response) { resolve(resp.response); } else { reject(new Error(`HTTP ${resp.status}`)); } }, onerror() { reject(new Error('network error')); }, ontimeout() { reject(new Error('timeout')); } }); }); } function dataUrlToBlob(dataUrl) { const [head, body] = dataUrl.split(','); const mime = (head.match(/data:([^;]+)/) || [])[1] || 'image/png'; const bin = atob(body); const bytes = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i += 1) bytes[i] = bin.charCodeAt(i); return new Blob([bytes], { type: mime }); } function blobToDataUrl(blob) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(String(reader.result)); reader.onerror = () => reject(reader.error || new Error('read image failed')); reader.readAsDataURL(blob); }); } function canvasToBlob(canvas) { return new Promise((resolve, reject) => { canvas.toBlob(blob => { if (blob) resolve(blob); else reject(new Error('canvas is blocked by browser security')); }, 'image/png'); }); } async function candidateToBlob(candidate) { if (candidate.type === 'canvas') return canvasToBlob(candidate.el); const src = candidate.src; if (src.startsWith('data:image/')) return dataUrlToBlob(src); if (src.startsWith('blob:')) return fetch(src).then(resp => resp.blob()); return gmBlob(src); } function dataUrlToFile(dataUrl, name) { const blob = dataUrlToBlob(dataUrl); const ext = blob.type.includes('jpeg') ? 'jpg' : blob.type.includes('webp') ? 'webp' : 'png'; return new File([blob], name || `gpt-image-${Date.now()}.${ext}`, { type: blob.type || 'image/png' }); } async function getPendingImage(storeKey = STORE_KEY) { const payload = await GM_getValue(storeKey, null); if (!payload || !payload.dataUrl || Date.now() - payload.createdAt > MAX_AGE_MS) return null; return payload; } async function sendCurrentImage(panel, btn, target = 'koukoutu') { if (btn) btn.disabled = true; let targetWindow = null; try { panel.setStatus(TXT.finding); const candidate = findBestImage(); if (!candidate) throw new Error(TXT.noImage); targetWindow = openTargetShell(target); panel.setStatus(`${TXT.reading} ${candidate.width}x${candidate.height}`); const blob = await candidateToBlob(candidate); const dataUrl = await blobToDataUrl(blob); const targetStoreKey = target === 'vtracer' ? VTRACER_STORE_KEY : target === 'gemini' ? GEMINI_STORE_KEY : target === 'imagvio' ? IMAGVIO_STORE_KEY : STORE_KEY; await GM_setValue(targetStoreKey, { dataUrl, name: `gpt-image-${new Date().toISOString().replace(/[:.]/g, '-')}.png`, type: blob.type || 'image/png', createdAt: Date.now() }); panel.setStatus(TXT.saved); navigateTargetPage(targetWindow, target); } catch (err) { if (targetWindow && !targetWindow.closed && targetWindow.location.href === 'about:blank') { targetWindow.close(); } const message = err && err.message ? err.message : String(err); panel.setStatus(message); if (panel.silent && panel.scope === 'source') alert(message); } finally { if (btn) btn.disabled = false; } } function findLightboxHost(img) { if (!img) return null; const minArea = innerWidth * innerHeight * 0.22; if (visibleArea(img.el) < minArea) return null; let host = img.el.parentElement; let best = null; while (host && host !== document.body) { const rect = host.getBoundingClientRect(); const style = getComputedStyle(host); const coversScreen = rect.width > innerWidth * 0.65 && rect.height > innerHeight * 0.65; if (style.position === 'fixed' || style.position === 'sticky' || coversScreen || host.getAttribute('role') === 'dialog') { best = host; } host = host.parentElement; } return best || document.body; } function ensureOverlayButton(panel) { const img = findBestImage(); const host = img ? findLightboxHost(img) : null; const existing = document.querySelector('.g2k-overlay-send'); if (!host) { if (existing) existing.remove(); return; } if (existing) return; const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'g2k-overlay-send'; btn.textContent = TXT.overlaySend; btn.addEventListener('click', event => { event.preventDefault(); event.stopPropagation(); sendCurrentImage(panel, btn); }); host.appendChild(btn); } async function startSourceUI() { if (window.__g2kSourceHotkeyReady) return; window.__g2kSourceHotkeyReady = true; const panel = makeSilentStatus('source'); const sendBtn = { disabled: false }; let shortcuts = await getShortcutSettings(); const rememberPointer = event => { lastPointer = { x: event.clientX, y: event.clientY, at: Date.now() }; }; const refreshShortcuts = async () => { shortcuts = await getShortcutSettings(); }; if (typeof GM_addValueChangeListener === 'function') { GM_addValueChangeListener('shortcuts.koukoutu', refreshShortcuts); GM_addValueChangeListener('shortcuts.vtracer', refreshShortcuts); GM_addValueChangeListener('shortcuts.gemini', refreshShortcuts); GM_addValueChangeListener('shortcuts.imagvio', refreshShortcuts); } const onShortcut = event => { const target = findShortcutTarget(event, shortcuts); if (!target) return; event.preventDefault(); event.stopPropagation(); sendCurrentImage(panel, sendBtn, target); }; window.addEventListener('keydown', onShortcut, true); document.addEventListener('keydown', onShortcut, true); window.addEventListener('pointermove', rememberPointer, true); document.addEventListener('pointermove', rememberPointer, true); window.addEventListener('pointerdown', rememberPointer, true); document.addEventListener('pointerdown', rememberPointer, true); } function getImageInputs() { return Array.from(document.querySelectorAll('input[type="file"]')) .filter(input => { const accept = String(input.accept || '').toLowerCase(); return !accept || accept.includes('image') || accept.includes('*/*') || accept.includes('.png') || accept.includes('.jpg') || accept.includes('.jpeg') || accept.includes('.webp') || accept.includes('.avif') || input.multiple; }); } function normalizedText(el) { return (el.innerText || el.textContent || '').replace(/\s+/g, ' ').trim(); } function findLoginDialog() { const nodes = Array.from(document.querySelectorAll('body *')); return nodes.find(el => { if (!isVisible(el)) return false; const rect = el.getBoundingClientRect(); if (rect.width < 260 || rect.height < 180) return false; const text = normalizedText(el); return text.includes('\u8bf7\u767b\u5f55') && (text.includes('\u5fae\u4fe1\u626b\u7801\u767b\u5f55') || text.includes('\u5207\u6362\u4e3a\u624b\u673a\u53f7\u767b\u5f55')); }) || null; } function closeLoginDialog() { const dialog = findLoginDialog(); if (!dialog) return false; const closeCandidates = Array.from(document.querySelectorAll('button, [role="button"], a, div, span')).filter(el => { if (!isVisible(el)) return false; const text = normalizedText(el); const aria = `${el.getAttribute('aria-label') || ''} ${el.getAttribute('title') || ''}`.toLowerCase(); if (text === '\u00d7' || text === 'x' || text === 'X' || text.includes('\u5173\u95ed') || aria.includes('close')) return true; const d = dialog.getBoundingClientRect(); const r = el.getBoundingClientRect(); const cx = r.left + r.width / 2; const cy = r.top + r.height / 2; return cx >= d.right - 35 && cx <= d.right + 45 && cy >= d.top - 45 && cy <= d.top + 35; }); const target = closeCandidates[0]; if (target) { target.click(); return true; } document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', bubbles: true, cancelable: true })); return true; } function isDisabledLike(el) { const className = String(el.className || ''); return Boolean( el.disabled || el.getAttribute('disabled') !== null || el.getAttribute('aria-disabled') === 'true' || /\b(disabled|cursor-not-allowed|opacity-50|opacity-40)\b/.test(className) ); } function findStartButton() { const terms = [ '\u5f00\u59cb\u62a0\u56fe', '\u5f00\u59cb\u5904\u7406', '\u7acb\u5373\u62a0\u56fe', '\u4e00\u952e\u62a0\u56fe', '\u5f00\u59cb\u53bb\u80cc\u666f', '\u5f00\u59cb' ]; const blockTerms = [ '\u5386\u53f2', '\u4e0a\u4f20', '\u4e0b\u8f7d', '\u6e05\u7a7a', '\u5220\u9664', '\u767b\u5f55' ]; const nodes = Array.from(document.querySelectorAll('button, a, [role="button"], div[class*="cursor-pointer"]')); return nodes.find(el => { if (!isVisible(el) || isDisabledLike(el)) return false; const text = normalizedText(el); if (!text || text.length > 30) return false; if (blockTerms.some(term => text.includes(term))) return false; return terms.some(term => text.includes(term)); }) || null; } async function waitAndClickStartButton(panel) { for (let i = 0; i < 90; i += 1) { if (findLoginDialog()) { closeLoginDialog(); panel.setStatus(TXT.loginPopup); await sleep(2500); continue; } const btn = findStartButton(); if (btn) { btn.scrollIntoView({ block: 'center', inline: 'center' }); await sleep(200); btn.click(); await sleep(1400); if (findLoginDialog()) { closeLoginDialog(); panel.setStatus(TXT.loginPopup); await sleep(2000); const retryBtn = findStartButton(); if (retryBtn && !findLoginDialog()) { retryBtn.scrollIntoView({ block: 'center', inline: 'center' }); await sleep(200); retryBtn.click(); await sleep(1400); } if (findLoginDialog()) { closeLoginDialog(); panel.setStatus(TXT.loginStill); return false; } } panel.setStatus(TXT.startClicked); return true; } await sleep(500); } panel.setStatus(TXT.noStart); return false; } async function importToKoukoutu(payload, panel) { const file = dataUrlToFile(payload.dataUrl, payload.name); panel.setStatus(`${TXT.waitingInput}: ${file.name}`); for (let i = 0; i < 80; i += 1) { const input = getImageInputs()[0]; if (input) { const dt = new DataTransfer(); dt.items.add(file); input.files = dt.files; input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); panel.setStatus(TXT.imported); await GM_deleteValue(STORE_KEY); await sleep(1500); await waitAndClickStartButton(panel); return true; } await sleep(250); } panel.setStatus(TXT.noInput); return false; } async function importToVTracer(payload, panel) { const file = dataUrlToFile(payload.dataUrl, payload.name); panel.setStatus(`VTracer 等待上传入口:${file.name}`); for (let i = 0; i < 80; i += 1) { const input = document.querySelector('#imageInput') || getImageInputs()[0]; if (input) { await sleep(1500); const dt = new DataTransfer(); dt.items.add(file); input.files = dt.files; input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); panel.setStatus('VTracer 图片已导入。'); await GM_deleteValue(VTRACER_STORE_KEY); return true; } await sleep(250); } panel.setStatus('VTracer 未找到上传入口。'); return false; } function clickPossibleImagvioUploadButton() { const terms = ['上传图片', '浏览', '选择图片', '选择文件', 'upload', 'browse', 'select']; const blockTerms = ['download', '下载', 'pricing', '价格', 'login', '登录']; const nodes = Array.from(document.querySelectorAll('button, a, label, [role="button"], div[tabindex], div[class*="cursor-pointer"]')); const target = nodes.find(el => { if (!isVisible(el) || isDisabledLike(el)) return false; const text = `${normalizedText(el)} ${el.getAttribute('aria-label') || ''} ${el.getAttribute('title') || ''}`.toLowerCase(); if (!text.trim()) return false; if (blockTerms.some(term => text.includes(term))) return false; return terms.some(term => text.includes(term.toLowerCase())); }); if (!target) return false; target.click(); return true; } function findImagvioUpscaleButton() { const terms = ['放大图片', '开始放大', '立即放大', '开始处理', 'upscale', 'enhance']; const blockTerms = ['上传', 'upload', 'download', '下载', '重新', 'remove', '删除']; const nodes = Array.from(document.querySelectorAll('button, a, [role="button"], div[class*="cursor-pointer"]')); return nodes.find(el => { if (!isVisible(el) || isDisabledLike(el)) return false; const text = `${normalizedText(el)} ${el.getAttribute('aria-label') || ''} ${el.getAttribute('title') || ''}`.toLowerCase(); if (!text || text.length > 80) return false; if (blockTerms.some(term => text.includes(term))) return false; return terms.some(term => text.includes(term.toLowerCase())); }) || null; } async function waitAndClickImagvioUpscaleButton(panel) { for (let i = 0; i < 80; i += 1) { const button = findImagvioUpscaleButton(); if (button) { button.click(); panel.setStatus('Imagvio 已点击放大图片。'); return true; } await sleep(250); } panel.setStatus('Imagvio 图片已导入,但没找到可点击的放大按钮。'); return false; } async function importToImagvio(payload, panel) { const file = dataUrlToFile(payload.dataUrl, payload.name); panel.setStatus(`Imagvio 等待上传入口:${file.name}`); let uploadButtonClicked = false; for (let i = 0; i < 100; i += 1) { if (!uploadButtonClicked && i >= 4) { uploadButtonClicked = clickPossibleImagvioUploadButton(); } const input = getImageInputs()[0]; if (input) { await sleep(600); const dt = new DataTransfer(); dt.items.add(file); try { input.value = ''; } catch {} input.files = dt.files; input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); panel.setStatus('Imagvio 图片已导入,正在尝试点击放大图片。'); await GM_deleteValue(IMAGVIO_STORE_KEY); await sleep(1200); await waitAndClickImagvioUpscaleButton(panel); return true; } await sleep(250); } panel.setStatus('Imagvio 未找到上传入口。'); return false; } function getGeminiUploadInputs() { return Array.from(document.querySelectorAll('input[type="file"]')).filter(input => { const accept = String(input.accept || '').toLowerCase(); return !accept || accept.includes('image') || accept.includes('*/*') || accept.includes('.png') || accept.includes('.jpg') || accept.includes('.jpeg') || accept.includes('.webp'); }); } function clickPossibleGeminiUploadButton() { const terms = [ 'upload', 'attach', '\u4e0a\u4f20', '\u9644\u4ef6', '\u6dfb\u52a0' ]; const blockTerms = ['send', 'run', 'example', 'sample', 'prompt', '\u53d1\u9001', '\u8fd0\u884c', '\u793a\u4f8b']; const nodes = Array.from(document.querySelectorAll('button, [role="button"], a, div[aria-label], span[aria-label]')); const target = nodes.find(el => { if (!isVisible(el) || isDisabledLike(el)) return false; const text = `${normalizedText(el)} ${el.getAttribute('aria-label') || ''} ${el.getAttribute('title') || ''}`.toLowerCase(); if (!text.trim()) return false; if (blockTerms.some(term => text.includes(term))) return false; const rect = el.getBoundingClientRect(); const looksLikeIconButton = rect.width <= 64 && rect.height <= 64; const looksLikePlus = text.trim() === '+' || text.includes('add') || text.includes('\u6dfb\u52a0'); return terms.some(term => text.includes(term)) || (looksLikeIconButton && looksLikePlus); }); if (!target) return false; target.click(); return true; } function findElementByText(selector, matcher) { return Array.from(document.querySelectorAll(selector)).find(el => isVisible(el) && matcher(normalizedText(el), el)) || null; } async function setGeminiResolution4K(panel) { const nativeSelect = Array.from(document.querySelectorAll('select')).find(select => { const text = normalizedText(select.closest('label') || select.parentElement || select); return /resolution/i.test(text) || Array.from(select.options || []).some(option => option.textContent.trim() === '4K'); }); if (nativeSelect) { const option = Array.from(nativeSelect.options || []).find(option => option.textContent.trim() === '4K' || option.value === '4K'); if (option && nativeSelect.value !== option.value) { nativeSelect.value = option.value; nativeSelect.dispatchEvent(new Event('input', { bubbles: true })); nativeSelect.dispatchEvent(new Event('change', { bubbles: true })); panel.setStatus('Gemini 分辨率已设置为 4K。'); } return true; } const resolutionLabel = findElementByText('label, div, span, mat-label', text => /^Resolution$/i.test(text.trim())); let trigger = null; if (resolutionLabel) { let root = resolutionLabel.parentElement; for (let i = 0; root && i < 5; i += 1, root = root.parentElement) { trigger = Array.from(root.querySelectorAll('button, [role="button"], [role="combobox"], mat-select, .mat-mdc-select-trigger, [aria-haspopup="listbox"]')) .find(el => isVisible(el) && !isDisabledLike(el)); if (trigger) break; } } trigger = trigger || Array.from(document.querySelectorAll('[role="combobox"], mat-select, .mat-mdc-select-trigger, [aria-haspopup="listbox"]')) .find(el => { if (!isVisible(el) || isDisabledLike(el)) return false; const text = normalizedText(el); return text === '1K' || text === '2K' || text === '4K' || /resolution/i.test(`${text} ${el.getAttribute('aria-label') || ''}`); }); trigger = trigger || Array.from(document.querySelectorAll('button, [role="button"], div, span')) .find(el => { if (!isVisible(el) || isDisabledLike(el)) return false; const text = normalizedText(el).trim(); if (text !== '1K' && text !== '2K') return false; const rect = el.getBoundingClientRect(); return rect.width >= 30 && rect.width <= 420 && rect.height >= 20 && rect.height <= 80; }); if (!trigger) return false; const currentText = normalizedText(trigger); if (currentText === '4K') return true; trigger.click(); await sleep(250); const option = Array.from(document.querySelectorAll('[role="option"], mat-option, button, .mat-mdc-option')) .find(el => isVisible(el) && normalizedText(el).trim() === '4K'); if (!option) return false; option.click(); panel.setStatus('Gemini 分辨率已设置为 4K。'); return true; } function setNativeInputValue(el, value) { const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set; if (setter) setter.call(el, value); else el.value = value; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); } function clearGeminiPromptText() { const fields = Array.from(document.querySelectorAll('textarea, input[type="text"], [contenteditable="true"], [role="textbox"]')) .filter(el => isVisible(el)); for (const field of fields) { const text = field.matches('textarea, input') ? field.value : normalizedText(field); if (!text || text.length < 10) continue; field.focus(); if (field.matches('textarea, input')) { setNativeInputValue(field, ''); } else { field.textContent = ''; field.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'deleteContentBackward', data: null })); } } } function getGeminiComposerTarget() { const active = document.activeElement; if (active && isVisible(active) && active.matches?.('textarea, [contenteditable="true"], [role="textbox"]')) { return active; } const candidates = Array.from(document.querySelectorAll('textarea, [contenteditable="true"], [role="textbox"]')) .filter(el => isVisible(el)) .map(el => { const rect = el.getBoundingClientRect(); const text = el.matches('textarea, input') ? el.value : normalizedText(el); return { el, score: rect.width * rect.height + (rect.top > innerHeight * 0.45 ? 1000000 : 0) - Math.min(text.length, 5000) }; }) .sort((a, b) => b.score - a.score); return candidates[0]?.el || null; } function countGeminiVisibleImageItems() { return Array.from(document.querySelectorAll('[data-file-id], [data-test-id*="attachment"], [class*="attachment"], [class*="thumbnail"], img')) .filter(el => { if (!isVisible(el)) return false; const rect = el.getBoundingClientRect(); if (rect.top < innerHeight * 0.25) return false; if (el.tagName.toLowerCase() === 'img' && (rect.width < 24 || rect.height < 24 || rect.width > 260 || rect.height > 260)) return false; return true; }) .length; } async function pasteGeminiImage(file, panel) { const before = countGeminiVisibleImageItems(); const target = getGeminiComposerTarget(); if (!target) return false; const dt = new DataTransfer(); dt.items.add(file); try { target.focus?.(); } catch {} const event = new ClipboardEvent('paste', { bubbles: true, cancelable: true, clipboardData: dt }); target.dispatchEvent(event); await sleep(1500); const after = countGeminiVisibleImageItems(); if (after > before) { panel.setStatus('Gemini 图片已通过粘贴导入。'); return true; } return false; } function removeGeminiExistingAttachments() { const candidates = Array.from(document.querySelectorAll('button, [role="button"], mat-icon-button, mat-icon, div, span')) .filter(el => { if (!isVisible(el) || isDisabledLike(el)) return false; const text = `${normalizedText(el)} ${el.getAttribute('aria-label') || ''} ${el.getAttribute('title') || ''}`.toLowerCase(); if (text.includes('remove') || text.includes('delete') || text.includes('clear') || text.includes('close') || text.includes('移除') || text.includes('删除') || text.includes('清除') || text === '×' || text === 'x') { const rect = el.getBoundingClientRect(); return rect.width <= 80 && rect.height <= 80; } return false; }); let clicked = 0; for (const el of candidates.slice(0, 8)) { const rect = el.getBoundingClientRect(); if (rect.top < innerHeight * 0.25) continue; el.click(); clicked += 1; } return clicked; } async function resetGeminiComposer(panel) { clearGeminiPromptText(); let removed = 0; for (let i = 0; i < 4; i += 1) { removed += removeGeminiExistingAttachments(); await sleep(180); } if (removed) panel.setStatus(`Gemini 已清理旧附件:${removed}`); } async function importToGemini(payload, panel) { const file = dataUrlToFile(payload.dataUrl, payload.name); panel.setStatus(`Gemini 等待上传入口:${file.name}`); let uploadButtonClicked = false; let resolutionSet = false; await sleep(4500); await resetGeminiComposer(panel); for (let i = 0; i < 8 && !resolutionSet; i += 1) { resolutionSet = await setGeminiResolution4K(panel); if (!resolutionSet) await sleep(500); } if (await pasteGeminiImage(file, panel)) { await sleep(500); if (!resolutionSet) await setGeminiResolution4K(panel); clearGeminiPromptText(); await GM_deleteValue(GEMINI_STORE_KEY); return true; } for (let i = 0; i < 120; i += 1) { if (i > 0 && i <= 24 && i % 8 === 0) { await resetGeminiComposer(panel); } if (!resolutionSet && i % 4 === 0) { resolutionSet = await setGeminiResolution4K(panel); } if (!uploadButtonClicked && i >= 2) { uploadButtonClicked = clickPossibleGeminiUploadButton(); } const input = getGeminiUploadInputs()[0]; if (input && (uploadButtonClicked || i >= 20)) { const dt = new DataTransfer(); dt.items.add(file); try { input.value = ''; } catch {} input.files = dt.files; input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); await sleep(1200); clearGeminiPromptText(); panel.setStatus('Gemini 图片已导入。'); await GM_deleteValue(GEMINI_STORE_KEY); return true; } await sleep(250); } panel.setStatus('Gemini 未找到上传入口。'); return false; } async function startTargetUI() { const panel = makeSilentStatus('target'); let lastImportedAt = 0; const runImport = async () => { if (targetImporting) return; targetImporting = true; try { const payload = await getPendingImage(); if (!payload) { panel.setStatus(TXT.noPending); return; } if (payload.createdAt && payload.createdAt === lastImportedAt) return; lastImportedAt = payload.createdAt || Date.now(); await importToKoukoutu(payload, panel); } catch (err) { panel.setStatus(err && err.message ? err.message : String(err)); } finally { targetImporting = false; } }; const triggerImport = async () => { const payload = await getPendingImage(); if (!payload) return false; if (payload.createdAt && payload.createdAt === lastImportedAt) return false; panel.setStatus(TXT.pendingFound); await sleep(500); runImport(); return true; }; if (typeof GM_addValueChangeListener === 'function') { GM_addValueChangeListener(STORE_KEY, (_name, _oldValue, newValue) => { if (!newValue || !newValue.dataUrl) return; triggerImport(); }); } window.addEventListener('focus', () => { triggerImport(); }); window.addEventListener('hashchange', () => { triggerImport(); }); for (let i = 0; i < 120; i += 1) { const started = await triggerImport(); if (started) break; await sleep(500); } } async function startVTracerUI() { const panel = makeSilentStatus('vtracer'); let lastImportedAt = 0; const runImport = async () => { if (targetImporting) return; targetImporting = true; try { const payload = await getPendingImage(VTRACER_STORE_KEY); if (!payload) return; if (payload.createdAt && payload.createdAt === lastImportedAt) return; lastImportedAt = payload.createdAt || Date.now(); await importToVTracer(payload, panel); } catch (err) { panel.setStatus(err && err.message ? err.message : String(err)); } finally { targetImporting = false; } }; const triggerImport = async () => { const payload = await getPendingImage(VTRACER_STORE_KEY); if (!payload) return false; if (payload.createdAt && payload.createdAt === lastImportedAt) return false; await sleep(500); runImport(); return true; }; if (typeof GM_addValueChangeListener === 'function') { GM_addValueChangeListener(VTRACER_STORE_KEY, (_name, _oldValue, newValue) => { if (!newValue || !newValue.dataUrl) return; triggerImport(); }); } window.addEventListener('focus', () => { triggerImport(); }); window.addEventListener('hashchange', () => { triggerImport(); }); for (let i = 0; i < 120; i += 1) { const started = await triggerImport(); if (started) break; await sleep(500); } } async function startImagvioUI() { const panel = makeSilentStatus('imagvio'); let lastImportedAt = 0; const runImport = async () => { if (targetImporting) return; targetImporting = true; try { const payload = await getPendingImage(IMAGVIO_STORE_KEY); if (!payload) return; if (payload.createdAt && payload.createdAt === lastImportedAt) return; lastImportedAt = payload.createdAt || Date.now(); await importToImagvio(payload, panel); } catch (err) { panel.setStatus(err && err.message ? err.message : String(err)); } finally { targetImporting = false; } }; const triggerImport = async () => { const payload = await getPendingImage(IMAGVIO_STORE_KEY); if (!payload) return false; if (payload.createdAt && payload.createdAt === lastImportedAt) return false; await sleep(500); runImport(); return true; }; if (typeof GM_addValueChangeListener === 'function') { GM_addValueChangeListener(IMAGVIO_STORE_KEY, (_name, _oldValue, newValue) => { if (!newValue || !newValue.dataUrl) return; triggerImport(); }); } window.addEventListener('focus', () => { triggerImport(); }); window.addEventListener('hashchange', () => { triggerImport(); }); for (let i = 0; i < 120; i += 1) { const started = await triggerImport(); if (started) break; await sleep(500); } } async function startGeminiUI() { const panel = makeSilentStatus('gemini'); let lastImportedAt = 0; const runImport = async () => { if (targetImporting) return; targetImporting = true; try { const payload = await getPendingImage(GEMINI_STORE_KEY); if (!payload) return; if (payload.createdAt && payload.createdAt === lastImportedAt) return; lastImportedAt = payload.createdAt || Date.now(); await importToGemini(payload, panel); } catch (err) { panel.setStatus(err && err.message ? err.message : String(err)); } finally { targetImporting = false; } }; const triggerImport = async () => { const payload = await getPendingImage(GEMINI_STORE_KEY); if (!payload) return false; if (payload.createdAt && payload.createdAt === lastImportedAt) return false; await sleep(500); runImport(); return true; }; if (typeof GM_addValueChangeListener === 'function') { GM_addValueChangeListener(GEMINI_STORE_KEY, (_name, _oldValue, newValue) => { if (!newValue || !newValue.dataUrl) return; triggerImport(); }); } window.addEventListener('focus', () => { triggerImport(); }); window.addEventListener('hashchange', () => { triggerImport(); }); for (let i = 0; i < 120; i += 1) { const started = await triggerImport(); if (started) break; await sleep(500); } } ready(async () => { console.log('[GPT Image to Koukoutu] userscript loaded:', location.href); registerShortcutMenu(); if (isKoukoutuPage) { startTargetUI(); } else if (isVTracerPage) { startVTracerUI(); } else if (isGeminiPage) { startGeminiUI(); } else if (isImagvioPage) { startImagvioUI(); } else { startSourceUI(); } }); })();