// ==UserScript== // @name GPT Image 内部网页跳转抠图 // @namespace https://local.codex/gpt-img-to-koukoutu // @version 1.1.3 // @description Send images from custom source websites to koukoutu remove-bg page, then auto start. // @author 木木 // @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 // @run-at document-start // ==/UserScript== (function () { 'use strict'; const STORE_KEY = 'gpt_img_to_koukoutu_pending_image'; const VTRACER_STORE_KEY = 'gpt_img_to_vtracer_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 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 isTargetPage = isKoukoutuPage || isVTracerPage; let targetImporting = false; 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 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 openKoukoutu() { location.assign(`${KOUKOUTU_BASE_URL}#from-gpt-img-${Date.now()}`); } function openVTracer() { location.assign(`${VTRACER_BASE_URL}#from-gpt-img-${Date.now()}`); } 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 findBestImage() { const viewportArea = Math.max(1, innerWidth * innerHeight); const candidates = []; for (const img of document.querySelectorAll('img')) { const area = visibleArea(img); const src = img.currentSrc || img.src || ''; if (!area || !src) continue; const lower = src.toLowerCase(); if (/\b(logo|favicon|icon|avatar)\b/.test(lower)) continue; if (img.naturalWidth < 128 || img.naturalHeight < 128) continue; const rect = img.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; let score = area - (centerDx + centerDy) * 100000; const alt = (img.alt || '').toLowerCase(); if (alt.includes('generated result')) score += 100000000; if (area / viewportArea > 0.22) score += 80000000; if (src.startsWith('data:image/')) score += 50000000; if (img.closest('button[aria-label*="\u9884\u89c8"], button[class*="cursor-zoom"]')) score += 1000000; if (/\/images\/|logo|favicon|icon/.test(lower)) score -= 50000000; candidates.push({ type: 'img', el: img, src: normalizeUrl(src), score, area, width: img.naturalWidth, height: img.naturalHeight }); } for (const canvas of document.querySelectorAll('canvas')) { const area = visibleArea(canvas); if (!area || canvas.width < 128 || canvas.height < 128) continue; candidates.push({ type: 'canvas', el: canvas, score: area + 100000, area, width: canvas.width, height: canvas.height }); } 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; try { panel.setStatus(TXT.finding); const candidate = findBestImage(); if (!candidate) throw new Error(TXT.noImage); panel.setStatus(`${TXT.reading} ${candidate.width}x${candidate.height}`); const blob = await candidateToBlob(candidate); const dataUrl = await blobToDataUrl(blob); await GM_setValue(target === 'vtracer' ? VTRACER_STORE_KEY : STORE_KEY, { dataUrl, name: `gpt-image-${new Date().toISOString().replace(/[:.]/g, '-')}.png`, type: blob.type || 'image/png', createdAt: Date.now() }); panel.setStatus(TXT.saved); if (target === 'vtracer') openVTracer(); else openKoukoutu(); } catch (err) { 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 }; const onShortcut = event => { const isAltK = event.altKey && !event.ctrlKey && !event.metaKey && event.key.toLowerCase() === 'k'; const isCtrlShiftK = event.ctrlKey && event.shiftKey && event.key.toLowerCase() === 'k'; const isAltL = event.altKey && !event.ctrlKey && !event.metaKey && event.key.toLowerCase() === 'l'; const isCtrlShiftL = event.ctrlKey && event.shiftKey && event.key.toLowerCase() === 'l'; if (!isAltK && !isCtrlShiftK && !isAltL && !isCtrlShiftL) return; event.preventDefault(); event.stopPropagation(); sendCurrentImage(panel, sendBtn, (isAltL || isCtrlShiftL) ? 'vtracer' : 'koukoutu'); }; window.addEventListener('keydown', onShortcut, true); document.addEventListener('keydown', onShortcut, true); } function getImageInputs() { return Array.from(document.querySelectorAll('input[type="file"]')) .filter(input => String(input.accept || '').includes('image') || 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 waiting input: ${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 image imported.'); await GM_deleteValue(VTRACER_STORE_KEY); return true; } await sleep(250); } panel.setStatus('VTracer input not found.'); 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 < 30; 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 < 30; i += 1) { const started = await triggerImport(); if (started) break; await sleep(500); } } ready(async () => { console.log('[GPT Image to Koukoutu] userscript loaded:', location.href); if (isKoukoutuPage) { startTargetUI(); } else if (isVTracerPage) { startVTracerUI(); } else { startSourceUI(); } }); })();