// ==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 = `
设置首选项
点击输入框后按下新的组合键,保存并刷新当前网页后生效。
`;
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();
}
});
})();