// ==UserScript== // @name 元宝优化 // @namespace bysun.yuanbao.opt // @version 1.0 // @description 元宝(yuanbao.tencent.com)页面优化 + 图生图增强:①一键简洁(默认开启):隐藏左下角"前往下载中心"与右上角"安装电脑版"按钮;②多图生成:上传图片并输入修改指令后自动生成多张(2/3/4 张可选)不重复的修改结果并排展示,手动点击"重新生成"时同样自动补足为多张不同结果,每张可点击查看无水印大图并下载,带真实生成进度显示。两个功能均可在设置面板独立开关。纯文生图不干预。 // @author bysun // @match https://yuanbao.tencent.com/* // @grant none // @run-at document-idle // @license MIT // ==/UserScript== (function () { 'use strict'; // ============ XHR 劫持:捕获 /api/chat/repeat 真实进度(替代假动画) ============ var progressHook = { active: false, // 是否正在监听某个 repeat 请求 done: false, // 收到 [DONE] lastPos: 0, // 上次解析位置(responseText 分片增长,避免重复解析) current: 0, // 当前真实百分比(0~100;未捕获到真实事件时为 0) desc: '' // 最近一次 step 阶段描述 }; // 将真实进度/阶段同步到所有激活的面板占位符 function notifyProgress(pct, desc) { stateMap.forEach(function (st, container) { if (!st || !st.placeholder || !st.placeholder.isConnected) return; var bar = st.placeholder.querySelector('.my-x3-ph-bar'); var txt = st.placeholder.querySelector('.my-x3-ph-text'); var nth = (st.collected || 0) + 1; if (bar) bar.style.width = pct + '%'; if (txt) { txt.textContent = '正在生成第 ' + nth + ' 张 · ' + pct + '%'; } }); } // 清洗阶段文案里的模型名等杂讯(如 "Hy Image3.5 preview"),保持占位提示干净 function cleanDesc(desc) { if (!desc) return ''; return String(desc).replace(/Hy\s*Image\s*3\.5\s*preview/gi, '').replace(/\s{2,}/g, ' ').trim(); } // 容错解析 SSE 分片:逐行识别 'data: {' 开头,命中 progress 取 value×100,命中 step 取 desc function parseProgressChunk(text) { if (!text || text.length <= progressHook.lastPos) return; var chunk = text.substring(progressHook.lastPos); progressHook.lastPos = text.length; var lines = chunk.split('\n'); for (var i = 0; i < lines.length; i++) { var line = lines[i]; if (line.indexOf('[DONE]') >= 0) { progressHook.done = true; progressHook.active = false; continue; } var di = line.indexOf('data: {'); if (di < 0) { di = line.indexOf('data:{"'); if (di < 0) continue; } var payload = line.substring(di + 6); try { var obj = JSON.parse(payload); if (!obj) continue; if (obj.type === 'progress' && typeof obj.value === 'number') { progressHook.active = true; progressHook.done = false; progressHook.current = Math.min(100, Math.max(0, Math.round(obj.value * 100))); if (obj.desc) progressHook.desc = cleanDesc(obj.desc); notifyProgress(progressHook.current, obj.desc || progressHook.desc); } else if (obj.type === 'step' && obj.desc) { progressHook.desc = cleanDesc(obj.desc); notifyProgress(progressHook.current, obj.desc); } } catch (e) { /* 分片边界可能截断 JSON 行,忽略该行 */ } } } // 尽早 hook XHR open/send:仅接管 URL 含 /api/chat/repeat 的请求,不影响页面其他请求 (function hookXhrProgress() { var origOpen = XMLHttpRequest.prototype.open; var origSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url) { this.__myX3IsRepeat = typeof url === 'string' && url.indexOf('/api/chat/repeat') >= 0; return origOpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function () { if (this.__myX3IsRepeat) { var xhr = this; progressHook.active = true; progressHook.done = false; progressHook.lastPos = 0; var prevReady = xhr.onreadystatechange; xhr.onreadystatechange = function () { if (xhr.readyState >= 3 && xhr.responseText) parseProgressChunk(xhr.responseText); if (xhr.readyState === 4 && xhr.responseText) parseProgressChunk(xhr.responseText); if (prevReady) { try { prevReady.apply(this, arguments); } catch (e) {} } }; xhr.addEventListener('load', function () { if (xhr.responseText) parseProgressChunk(xhr.responseText); }); } return origSend.apply(this, arguments); }; })(); // ============ fetch 劫持:直接触发 repeat 时的 SSE 进度(兜底链路) ============ var origFetch = window.fetch; window.fetch = function (input, init) { var url = typeof input === 'string' ? input : (input && input.url) || ''; var p = origFetch.apply(this, arguments); if (typeof url === 'string' && url.indexOf('/api/chat/repeat') >= 0) { p.then(function (res) { try { if (!res.body || !res.body.getReader) return res; var reader = res.body.getReader(); var decoder = new TextDecoder('utf-8'); var buf = ''; (function pump() { return reader.read().then(function (r) { if (r.done) { parseProgressChunk(buf); return; } buf += decoder.decode(r.value, { stream: true }); parseProgressChunk(buf); return pump(); }).catch(function () {}); })(); } catch (e) {} return res; }).catch(function () {}); } return p; }; // 从当前 URL 提取会话 cid(/chat/xxx、/bot/xxx/chat/xxx 等),用于直接触发 repeat 请求 function parseConversationId() { var m = window.location.pathname.match(/\/(?:chat|session|bot\/[^/]+\/chat)\/([A-Za-z0-9_-]+)/); if (m) return m[1]; m = window.location.pathname.match(/\/([A-Za-z0-9_-]{20,})/); if (m) return m[1]; return null; } // 绕过浮层菜单,直接向 /api/chat/repeat/{cid} 发真实请求触发重新生成(等同点击确认项的效果)。 // 走被包裹后的 window.fetch,以便上方 fetch 劫持继续解析 SSE 进度。 function fireRepeatDirect(container) { var cid = parseConversationId(); if (!cid) return false; scriptClick = true; try { window.fetch('/api/chat/repeat/' + cid, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' } }).catch(function () {}); } catch (e) { scriptClick = false; return false; } setTimeout(function () { scriptClick = false; }, 80); return true; } // ============ 设置(多图生成 + 一键简洁),localStorage 持久化 ============ var SETTINGS_KEY = 'myX3Settings'; var settings = { enabled: true, target: 3, clean: true }; function loadSettings() { try { var raw = localStorage.getItem(SETTINGS_KEY); if (raw) { var o = JSON.parse(raw); if (typeof o.enabled === 'boolean') settings.enabled = o.enabled; if (o.target === 2 || o.target === 3 || o.target === 4) settings.target = o.target; if (typeof o.clean === 'boolean') settings.clean = o.clean; } } catch (e) {} } function saveSettings() { try { localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); } catch (e) {} } // ============ 可配置 ============ // 生图数量由设置面板控制(settings.target:2/3/4),不再使用固定 TARGET 常量 var MAX_CLICKS = 6; // 自动点击"重新生成"总次数上限(含重试),防死循环 var TIMEOUT_MS = 180000; // 单轮等待超时(毫秒) var TICK_MS = 500; // 主循环间隔(毫秒) var PENDING_TTL = 180000; // 发送标记最长保留时间(毫秒) var STABLE_TICKS = 2; // 新图需连续 N 次观察稳定才算数(防闪烁误判) var NO_NEW_MS = 45000; // 点击重生成后这么久仍是旧图才重试(图生图生成耗时长,给足宽限防打断) // ============ 选择器 ============ var SEL_RESULT_BOX = '.hyc-card-box-card-list, .hyc-media-box, [data-box-type="loadingImage"], [class*="dic-list"]'; var SEL_RESULT_CARD = '.dic-card-item'; var SEL_RESULT_IMG = '.dic-card-item__img--loaded, .dic-card-item img'; var SEL_REGEN = '[aria-label="重新生成"], .Repeat_iconBox__GUh3i'; var SEL_UPLOAD_PREVIEW = '[data-input-resource-area="true"] img, .creation-reference-resource-list_boundResource__TWUfX img, .FileList_inputFileListItem__sOOlP img'; var SEL_AI_BUBBLE = '.agent-chat__bubble--ai, .agent-chat__conv--ai, [class*="bubble--ai"]'; // ============ 状态 ============ var pendingImg2Img = false; var pendingSetAt = 0; var candidateBox = null; // 明确锁定的当前容器(用户点重新生成 / 新插入容器) var scriptClick = false; // 脚本自己点击重新生成时置位,避免误判为用户操作 var preSnapshot = null; // 发送前容器快照:container -> 当时的图片 src(需先声明,否则严格模式抛错) var stateMap = new Map(); var boxRegistry = new Map(); // container -> { src, seenAt }:每个结果容器图片出现时间 var globalSeen = {}; // 页面已出现过的图片链接全集:触发重新生成前快照,防止把"上次的旧图"当本轮新图收集 // 触发重新生成前,把页面当前所有图片链接(img src + 卡片无水印原图属性)加入全集, // 之后 repeat 过程中这些旧图再出现一律不算新结果,只认真正新生成的图。 function snapshotGlobalSeen() { var imgs = document.querySelectorAll('img'); for (var i = 0; i < imgs.length; i++) { var s = imgs[i].currentSrc || imgs[i].src || ''; if (s) globalSeen[s] = 1; } var cards = document.querySelectorAll(SEL_RESULT_CARD); for (var j = 0; j < cards.length; j++) { var d = cards[j].getAttribute('data-download-url'); if (d) globalSeen[d] = 1; var c = cards[j].getAttribute('data-card-url'); if (c) globalSeen[c] = 1; } } // ============ CSS ============ function injectStyle() { var css = [ '.my-x3-panel{display:flex;gap:10px;flex-wrap:wrap;margin:10px 0 6px;padding:10px;border:1px solid rgba(0,0,0,.06);border-radius:8px;background:#fff;box-shadow:0 1px 4px rgba(0,0,0,.04);box-sizing:border-box;align-items:stretch;}', '.my-x3-title{width:100%;font-size:12px;color:#1f2329;line-height:1;font-weight:500;}', '.my-x3-placeholder{width:210px;min-height:150px;border:1.5px dashed rgba(0,0,0,.15);border-radius:8px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;color:#8a8f99;font-size:12px;background:rgba(0,0,0,.02);padding:12px;box-sizing:border-box;}', '.my-x3-ph-text{white-space:nowrap;}', '.my-x3-ph-progress{width:100%;height:5px;background:rgba(0,0,0,.06);border-radius:3px;overflow:hidden;}', '.my-x3-ph-bar{height:100%;width:0%;background:linear-gradient(90deg,#0052d9,#4a90d9);border-radius:3px;transition:width 1s linear;}', '.my-x3-clone{position:relative;width:210px!important;flex:0 0 auto;border-radius:8px;overflow:hidden;cursor:pointer;box-shadow:0 2px 8px rgba(0,0,0,.12);animation:my-x3-pop .5s cubic-bezier(.2,.9,.3,1.2);background:#fff;border:1px solid rgba(0,0,0,.05);}', '.my-x3-clone img{width:100%!important;height:auto!important;display:block!important;}', '.my-x3-tag{position:absolute;top:6px;left:6px;z-index:5;background:rgba(0,0,0,.6);color:#fff;font-size:12px;line-height:1;padding:3px 8px;border-radius:5px;pointer-events:none;}', '.my-x3-dl{position:absolute;right:6px;bottom:6px;z-index:5;background:#0052d9;color:#fff;font-size:12px;line-height:1;padding:5px 9px;border-radius:5px;border:0;cursor:pointer;} .my-x3-dl:hover{background:#0a5ee6;}', '.my-x3-dlall{margin-left:auto;align-self:flex-end;background:#0052d9;color:#fff;font-size:12px;line-height:1;padding:6px 12px;border-radius:5px;border:0;cursor:pointer;} .my-x3-dlall:hover{background:#0a5ee6;}', '@keyframes my-x3-pop{from{opacity:0;transform:scale(.9) translateY(10px)}to{opacity:1;transform:none}}', '.my-x3-overlay{position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,.88);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;}', '.my-x3-overlay img{max-width:92vw;max-height:78vh;object-fit:contain;border-radius:8px;background:#fff;box-shadow:0 10px 50px rgba(0,0,0,.5);}', '.my-x3-overlay .my-x3-bar{display:flex;gap:10px;align-items:center;}', '.my-x3-overlay .my-x3-btn{padding:8px 20px;border:0;border-radius:6px;font-size:14px;cursor:pointer;text-decoration:none;display:inline-block;}', '.my-x3-overlay .my-x3-btn-dl{background:#0052d9;color:#fff;} .my-x3-overlay .my-x3-btn-dl:hover{background:#0a5ee6;}', '.my-x3-overlay .my-x3-btn-close{background:rgba(255,255,255,.14);color:#fff;border:1px solid rgba(255,255,255,.2);} .my-x3-overlay .my-x3-btn-close:hover{background:rgba(255,255,255,.22);}', '.my-x3-overlay .my-x3-hint{color:#9aa0a6;font-size:12px;}', '.my-x3-setbtn{position:fixed;right:18px;bottom:18px;z-index:99990;padding:8px 14px;border:0;border-radius:20px;background:#0052d9;color:#fff;font-size:13px;cursor:pointer;box-shadow:0 2px 8px rgba(0,82,217,.35);} .my-x3-setbtn:hover{background:#0a5ee6;}', '.my-x3-setpanel{position:fixed;right:18px;bottom:58px;z-index:99991;width:230px;background:#fff;color:#1f2329;border-radius:8px;border:1px solid rgba(0,0,0,.06);box-shadow:0 6px 20px rgba(0,0,0,.12);padding:14px 16px;font-size:13px;display:none;box-sizing:border-box;}', '.my-x3-setpanel.open{display:block;}', '.my-x3-set-title{font-weight:600;margin-bottom:10px;font-size:14px;}', '.my-x3-set-group{font-size:11px;color:#8a8f99;margin:2px 0 4px;font-weight:500;}', '.my-x3-badge{display:inline-block;margin-left:6px;padding:1px 6px;border-radius:4px;background:rgba(0,82,217,.1);color:#0052d9;font-size:10px;line-height:1.5;vertical-align:middle;font-weight:400;white-space:nowrap;}', '.my-x3-set-row{display:flex;align-items:center;gap:6px;margin:8px 0;cursor:pointer;}', '.my-x3-set-sep{height:1px;background:#eef0f2;margin:10px 0;}', '.my-x3-set-targets{display:flex;gap:8px;margin:6px 0 2px;}', '.my-x3-set-targets label{cursor:pointer;}', '.my-x3-set-tip{color:#8a8f99;font-size:11px;line-height:1.4;margin-top:8px;font-style:italic;}', '.my-x3-setpanel input{accent-color:#0052d9;}' ].join('\n'); var style = document.createElement('style'); style.textContent = css; document.head.appendChild(style); document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeViewer(); }); } // ============ 小工具 ============ function hasUploadPreview() { return !!document.querySelector(SEL_UPLOAD_PREVIEW); } function currentCard(container) { return container.querySelector(SEL_RESULT_CARD); } function currentImg(container) { var card = currentCard(container); if (card) { var i = card.querySelector(SEL_RESULT_IMG) || card.querySelector('img'); if (i) return i; } return container.querySelector(SEL_RESULT_IMG); } function imgLoaded(imgEl) { return imgEl && imgEl.complete && imgEl.naturalWidth > 0; } function srcOfImg(imgEl) { return imgEl ? (imgEl.currentSrc || imgEl.src || '') : ''; } // 取无水印原图链接:data-download-url > data-card-url > img src function fullSrcOf(card, imgEl) { if (card) { var dl = card.getAttribute('data-download-url'); if (dl) return dl; var cu = card.getAttribute('data-card-url'); if (cu) return cu; } return srcOfImg(imgEl); } // 是否真正在生成中:结果结构未渲染 / 无图片地址。 // 注意:不再把"容器带 loading 类"视为生成中——元宝的 loading 类可能是持久属性, // 若据此拦截,新图出现后也会被提前 return 挡掉,导致第二、三张永远收集不到。 // 有真实已加载的图(complete && naturalWidth>0)即视为结果就绪。 function isGenerating(container) { if (!container) return true; var card = currentCard(container); var imgEl = currentImg(container); if (!card && !imgEl) return true; if (imgEl && imgLoaded(imgEl)) return false; return !srcOfImg(imgEl); } // 是否有生成中的外观迹象(无已加载图 / aria-busy),有则继续等待、不触发重试。 // 仅用可靠信号,避免持久 loading 类导致重试永远不触发。 function hasLoadingSign(container) { if (isGenerating(container)) return true; var bubble = getBubble(container); if (!bubble) return false; if (bubble.getAttribute && bubble.getAttribute('aria-busy') === 'true') return true; return false; } function getBubble(container) { return container.closest(SEL_AI_BUBBLE) || container.parentElement; } function getRegenBtn(container) { var bubble = getBubble(container); var btn = bubble ? bubble.querySelector(SEL_REGEN) : null; if (!btn) { var all = document.querySelectorAll(SEL_REGEN); if (all.length) btn = all[all.length - 1]; } return btn; } function regenDisabled(btn) { return !btn || btn.getAttribute('aria-disabled') === 'true' || (typeof btn.className === 'string' && /disabled/i.test(btn.className)); } // 点击"重新生成"图标后,TDesign 只弹出浮层菜单。v2.1 不再依赖"点弹窗里的确认项": // 1) 优先直接调用菜单项内部的 React props onClick(绕过事件模拟失败); // 2) 拿不到 props 就模拟完整鼠标事件序列(pointerdown→mousedown→pointerup→mouseup→click); // 3) 上述都无效或浮层迟迟不出现时,直接绕过弹窗发 /api/chat/repeat/{cid} 真实请求。 // 全程置位 scriptClick 防被误判为用户手动操作。 function findRegenMenuItem() { var scopes = document.querySelectorAll('.t-popup, [class*="repeat-option-dropdown"], [class*="t-dropdown"]'); var candidates = []; for (var s = 0; s < scopes.length; s++) { if (!scopes[s].offsetParent) continue; var inside = scopes[s].querySelectorAll('li, [class*="item"]'); candidates = candidates.concat(Array.prototype.slice.call(inside)); } candidates = candidates.concat(Array.prototype.slice.call(document.querySelectorAll('li.t-dropdown__item, [class*="dropdown"] li, [class*="menu"] li'))); for (var i = 0; i < candidates.length; i++) { var el = candidates[i]; if (!el.offsetParent) continue; var txt = (el.innerText || el.textContent || '').trim(); if (txt.indexOf('重新生成') >= 0 && txt.length <= 20) return el; } return null; } function invokeMenuItem(el) { // 1) React 内部 props onClick 直接调用(最接近真实点击,不受事件模拟失败影响) var rk = null; for (var k in el) { if (k.indexOf('__reactProps$') === 0) { rk = k; break; } } if (rk && el[rk] && typeof el[rk].onClick === 'function') { try { el[rk].onClick.call(el, { stopPropagation: function () {}, preventDefault: function () {}, nativeEvent: new MouseEvent('click', { bubbles: true }) }); return true; } catch (e) {} } // 2) 完整鼠标事件序列 try { ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click'].forEach(function (type) { var ev; try { ev = new PointerEvent(type, { bubbles: true, cancelable: true, view: window }); } catch (e2) { ev = new MouseEvent(type, { bubbles: true, cancelable: true, view: window }); } el.dispatchEvent(ev); }); return true; } catch (e) { return false; } } function confirmRegenAction(trigger, container) { var started = Date.now(); var TIMEOUT = 8000; var fired = false; var directFired = false; function attempt() { if (fired) return; var el = findRegenMenuItem(); if (el) { fired = true; clearInterval(timer); scriptClick = true; var ok = invokeMenuItem(el); setTimeout(function () { scriptClick = false; }, 150); if (!ok) { directFired = true; fireRepeatDirect(container); } return; } if (Date.now() - started > TIMEOUT) { fired = true; clearInterval(timer); directFired = true; fireRepeatDirect(container); } } var timer = setInterval(attempt, 200); // 菜单项已触发但 3.5s 内页面自身 repeat 请求未发出(progressHook 无动静)→ 直接发请求兜底 setTimeout(function () { if (directFired) return; if (progressHook.active || progressHook.done || progressHook.lastPos > 0) return; directFired = true; fireRepeatDirect(container); }, 3500); setTimeout(function () { if (!fired) { fired = true; clearInterval(timer); } }, TIMEOUT + 500); } function clickRegen(container) { var btn = getRegenBtn(container); if (regenDisabled(btn)) return false; try { // 先关闭可能残留的浮层,避免再次点击图标把刚弹出的菜单又关掉 var popups = document.querySelectorAll('.t-popup, [class*="repeat-option-dropdown"]'); for (var i = 0; i < popups.length; i++) { if (popups[i].offsetParent) { try { document.body.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true })); } catch (e) {} break; } } // 点击前快照页面所有已有图片链接:之后这些旧图再出现一律不算本轮新结果 snapshotGlobalSeen(); scriptClick = true; btn.click(); confirmRegenAction(btn, container); setTimeout(function () { scriptClick = false; }, 80); } catch (e) { scriptClick = false; return false; } return true; } // ============ 面板 ============ function getPanel(container) { var st = stateMap.get(container); if (st && st.panel && st.panel.isConnected) return st.panel; var bubble = getBubble(container); var panel = document.createElement('div'); panel.className = 'my-x3-panel'; panel.innerHTML = '
生成多图(' + settings.target + ' 张不重复,右下角可一键下载)
'; if (bubble && bubble.parentNode) { bubble.insertAdjacentElement('afterend', panel); } else { container.insertAdjacentElement('afterend', panel); } var dlAll = document.createElement('button'); dlAll.className = 'my-x3-dlall'; dlAll.textContent = '一键下载'; dlAll.addEventListener('click', function (e) { e.stopPropagation(); downloadAll(panel); }); panel.appendChild(dlAll); if (st) st.panel = panel; return panel; } // 一键下载:遍历面板内全部结果图,取固化 dataURL 或原图链接,逐个错开触发下载 function downloadAll(panel) { var wrappers = Array.prototype.slice.call(panel.querySelectorAll('.my-x3-clone')); if (!wrappers.length) return; wrappers.forEach(function (w, idx) { var img = w.querySelector('img'); var url = w.dataset.stableUrl || (img && img.src) || ''; if (!url) return; setTimeout(function () { downloadImage(url, '元宝-结果-' + (idx + 1) + '.png'); }, idx * 400); }); } function clearPlaceholder(container) { var st = stateMap.get(container); if (st && st.progressTimer) { clearInterval(st.progressTimer); st.progressTimer = null; } if (st && st.placeholder && st.placeholder.parentNode) { st.placeholder.remove(); st.placeholder = null; } } function addPlaceholder(container, nth) { var st = stateMap.get(container); var panel = getPanel(container); clearPlaceholder(container); var ph = document.createElement('div'); ph.className = 'my-x3-placeholder'; ph.innerHTML = '
正在生成第 ' + nth + ' 张...
' + '
'; var dlAllBtn = panel.querySelector('.my-x3-dlall'); if (dlAllBtn) { panel.insertBefore(ph, dlAllBtn); } else { panel.appendChild(ph); } if (st) { st.placeholder = ph; st.phStarted = Date.now(); st.progressTimer = null; // 不再用时间推算假动画:进度由 XHR 劫持的真实 progress 事件驱动 // 未捕获到真实进度事件时保持当前真实值或暂停,不回退假动画 if (progressHook.current > 0 && !progressHook.done) { var bar = ph.querySelector('.my-x3-ph-bar'); var txt = ph.querySelector('.my-x3-ph-text'); if (bar) bar.style.width = progressHook.current + '%'; if (txt) txt.textContent = '正在生成第 ' + nth + ' 张 · ' + progressHook.current + '%'; } } return ph; } // ============ 图片固化与下载 ============ // 把图片 URL 固化为 dataURL,避免元宝 revoke 旧 blob 后面板图/大图白屏。 // data: 直接返回;http(s)/blob 用 XHR 拉取转 blob 再 FileReader 读 dataURL; // 跨域失败时回调 null,调用方继续用原 URL 显示即可。 function solidifyUrl(url, cb) { if (!url) { cb(null); return; } if (url.indexOf('data:') === 0) { cb(url); return; } try { var xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.responseType = 'blob'; xhr.onload = function () { try { if (xhr.status >= 200 && xhr.status < 300 && xhr.response) { var fr = new FileReader(); fr.onload = function () { cb(fr.result); }; fr.onerror = function () { cb(null); }; fr.readAsDataURL(xhr.response); return; } } catch (e) {} cb(null); }; xhr.onerror = function () { cb(null); }; xhr.send(); } catch (e) { cb(null); } } // 触发 下载 function triggerDownload(href, filename) { var a = document.createElement('a'); a.href = href; a.download = filename; a.rel = 'noopener'; document.body.appendChild(a); a.click(); setTimeout(function () { a.remove(); }, 1000); } // 下载失败时回退:新标签打开原图 function fallbackOpen(url) { var w = window.open(url, '_blank'); if (w) w.opener = null; } // 下载图片:dataURL 直接 a[download];http(s)/blob 先 XHR 转 blob 再触发下载,失败回退新标签。 // 这样能绕过 a[download] 跨域限制,点击"下载"必有反馈。 function downloadImage(url, filename) { if (!url) return; if (url.indexOf('data:') === 0) { triggerDownload(url, filename); return; } try { var xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.responseType = 'blob'; xhr.onload = function () { try { if (xhr.status >= 200 && xhr.status < 300 && xhr.response) { var objUrl = URL.createObjectURL(xhr.response); triggerDownload(objUrl, filename); setTimeout(function () { URL.revokeObjectURL(objUrl); }, 10000); return; } } catch (e) {} fallbackOpen(url); }; xhr.onerror = function () { fallbackOpen(url); }; xhr.send(); } catch (e) { fallbackOpen(url); } } // ============ 大图查看器 ============ function closeViewer() { var ov = document.querySelector('.my-x3-overlay'); if (ov) ov.remove(); } function openViewer(src) { if (!src) return; closeViewer(); var ov = document.createElement('div'); ov.className = 'my-x3-overlay'; var img = document.createElement('img'); img.src = src; // 固化大图 URL,避免 blob 被页面 revoke 后大图白屏 solidifyUrl(src, function (stable) { if (stable && stable !== src && img.isConnected) img.src = stable; }); var bar = document.createElement('div'); bar.className = 'my-x3-bar'; var dl = document.createElement('a'); dl.className = 'my-x3-btn my-x3-btn-dl'; dl.href = '#'; dl.textContent = '下载原图'; dl.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); downloadImage(img.src || src, 'yuanbao-image-' + Date.now() + '.png'); }); var close = document.createElement('button'); close.className = 'my-x3-btn my-x3-btn-close'; close.textContent = '关闭'; var hint = document.createElement('div'); hint.className = 'my-x3-hint'; hint.textContent = '若下载按钮未生效,可右键大图另存为'; bar.appendChild(dl); bar.appendChild(close); ov.appendChild(img); ov.appendChild(bar); ov.appendChild(hint); close.onclick = closeViewer; ov.addEventListener('click', function (e) { if (e.target === ov) closeViewer(); }); document.body.appendChild(ov); } // ============ 收集 ============ function collectImage(container) { var st = stateMap.get(container); if (!st || st.done) return; var card = currentCard(container); var imgEl = currentImg(container); var imgUrl = srcOfImg(imgEl); var full = fullSrcOf(card, imgEl) || imgUrl; // 同一张图可能以不同 src 出现(缩略图→原图),用无水印原图链接去重,避免面板重复 if (!imgUrl || st.collectedSrcs.indexOf(full) >= 0) return; // 第二张起:重复出现的"旧图"(触发重新生成前页面已存在)一律不收集,只认真正新生成的图 if (st.collected > 0 && globalSeen[full]) return; var panel = getPanel(container); clearPlaceholder(container); // 一律用全新 img 承载:不克隆元宝卡片,避免继承其 blob URL / 懒加载占位 / 布局 // 样式导致面板图随 blob 被 revoke 一起消失或错位 var wrapper = document.createElement('div'); wrapper.className = 'my-x3-clone'; var im = document.createElement('img'); im.src = full; im.alt = '图生图结果 ' + (st.collected + 1); im.addEventListener('error', function () { // 无水印原图链接失效时,回退到容器当前 src(可能是水印缩略图),保证面板有图 if (imgUrl && im.src !== imgUrl) im.src = imgUrl; }); wrapper.appendChild(im); // 固化 dataURL:成功则覆盖 img.src 并记入 dataset.stableUrl,防元宝 revoke blob 后消失 solidifyUrl(full, function (stable) { if (stable && stable !== full) { wrapper.dataset.stableUrl = stable; if (im.isConnected && im.src === full) im.src = stable; } }); var tag = document.createElement('span'); tag.className = 'my-x3-tag'; tag.textContent = st.collected + 1; wrapper.appendChild(tag); var dlBtn = document.createElement('button'); dlBtn.className = 'my-x3-dl'; dlBtn.textContent = '下载'; dlBtn.addEventListener('click', function (e) { e.stopPropagation(); downloadImage(wrapper.dataset.stableUrl || im.src || full, 'yuanbao-image-' + Date.now() + '.png'); }); wrapper.appendChild(dlBtn); wrapper.addEventListener('click', function (e) { if (e.target === dlBtn) return; openViewer(wrapper.dataset.stableUrl || im.src || full); }); // 插入到"一键下载"按钮之前,保证按钮始终固定在面板右下角 var dlAllBtn = panel.querySelector('.my-x3-dlall'); if (dlAllBtn) { panel.insertBefore(wrapper, dlAllBtn); } else { panel.appendChild(wrapper); } st.collected++; st.lastSrc = imgUrl; st.collectedSrcs.push(full); st.lastNode = imgEl; globalSeen[full] = 1; // 本轮新图也记入全集,避免后续 repeat 时它被当成"旧图"或重复收集 } // ============ 状态管理 ============ function newState() { return { collected: 0, clicks: 0, done: false, phase: '', lastSrc: null, lastNode: null, panel: null, placeholder: null, progressTimer: null, phStarted: 0, collectedSrcs: [], pendingSrc: null, stableCount: 0, since: Date.now(), clickFailAt: 0, manualSrc: null, manualCount: 0 }; } function resetState(container) { var old = stateMap.get(container); var st = newState(); st.panel = old ? old.panel : null; if (old && old.panel) { Array.prototype.slice.call(old.panel.querySelectorAll('.my-x3-clone')).forEach(function (n) { n.remove(); }); } stateMap.set(container, st); } // 作废所有进行中的多图生成任务(用户发出新图生图时调用): // 上一张的重新生成按钮已消失,继续补足只会瞎等/瞎点,直接终止并移除其面板。 function cancelAllTasks() { stateMap.forEach(function (st, container) { if (!st || st.done) return; st.done = true; clearPlaceholder(container); if (st.panel && st.panel.parentNode) st.panel.remove(); st.panel = null; stateMap.delete(container); }); } // ============ 流程控制 ============ function doClick(container) { var st = stateMap.get(container); if (!st || st.done) return; if (st.clicks >= MAX_CLICKS) { st.done = true; clearPlaceholder(container); return; } if (clickRegen(container)) { st.clicks++; st.since = Date.now(); st.pendingSrc = null; st.stableCount = 0; addPlaceholder(container, st.collected + 1); } else { st.clickFailAt = Date.now() + 3000; } } function processFirst(container) { var st = stateMap.get(container); if (!st || st.done) return; var imgEl = currentImg(container); var src = srcOfImg(imgEl); if (!src) return; collectImage(container); if (st.collected >= settings.target) { st.done = true; return; } st.phase = 'waiting'; st.since = Date.now(); doClick(container); } function tickWaiting(container) { var st = stateMap.get(container); if (!st || st.done) return; if (!container.isConnected) return; // 容器已重建/被替换:由主循环统一做任务迁移 if (Date.now() - st.since > TIMEOUT_MS) { st.done = true; clearPlaceholder(container); return; } if (st.clickFailAt && Date.now() > st.clickFailAt && st.clicks < MAX_CLICKS) { st.clickFailAt = 0; doClick(container); return; } var imgEl = currentImg(container); var curSrc = srcOfImg(imgEl); // 新 src 出现即尝试收集(不要求原图加载完成、不看 loading 类:元宝新图可能懒加载未触发, // 收集时用无水印原图链接自行加载;loading 类可能是持久属性,不能据此拦截收集) if (curSrc && curSrc !== st.lastSrc) { // repeat 后容器可能先展示"上次的旧图":触发重新生成前页面已存在的 src 一律不当作新结果 if (st.collected > 0 && globalSeen[curSrc]) { st.pendingSrc = null; st.stableCount = 0; return; } if (st.pendingSrc === curSrc) { st.stableCount++; if (st.stableCount >= STABLE_TICKS) { st.pendingSrc = null; st.stableCount = 0; collectImage(container); if (st.collected >= settings.target) { st.done = true; clearPlaceholder(container); return; } doClick(container); } } else { st.pendingSrc = curSrc; st.stableCount = 1; } } else if (curSrc === st.lastSrc || !curSrc) { st.pendingSrc = null; st.stableCount = 0; // 太久没等到新图且无明确生成迹象 → 重试点击(NO_NEW_MS 已放宽,防误打断) if (Date.now() - st.since > NO_NEW_MS && st.clicks < MAX_CLICKS && !st.pendingSrc && !hasLoadingSign(container)) { doClick(container); return; } } } // ============ 容器注册表:记录每个结果容器图片出现时间 ============ function scanRegistry() { var boxes = document.querySelectorAll(SEL_RESULT_BOX); for (var i = 0; i < boxes.length; i++) { var box = boxes[i]; var src = srcOfImg(currentImg(box)); var reg = boxRegistry.get(box); if (!reg) { reg = { src: null, seenAt: 0, addedAt: Date.now() }; boxRegistry.set(box, reg); } if (src && src !== reg.src) { reg.src = src; reg.seenAt = Date.now(); } } // 清理已移除的容器 boxRegistry.forEach(function (reg, c) { if (!c.isConnected) boxRegistry.delete(c); }); } // 发送前已存在的容器快照:记录容器集合,用于排除历史容器。 // 注意:不依赖 src 快照判断新旧——发送前 src 为空的旧容器在发送后出图会被误判成新结果(接错容器根因之一)。 function snapshotBoxes() { preSnapshot = new Set(); var boxes = document.querySelectorAll(SEL_RESULT_BOX); for (var i = 0; i < boxes.length; i++) { preSnapshot.add(boxes[i]); } } // 是否为"发送后新鲜出现"的容器:仅发送后新插入的容器算数。 // 发送前已存在的容器(即使 src 变化)一律不接管,避免多图生成作用到上一张旧图。 function isFreshCandidate(c) { if (!preSnapshot) return true; return !preSnapshot.has(c); } // 选择一个"发送后才出现"的容器作为当前接管对象。 // 严格只认新容器:绝不兜底选"最后一个容器",否则发送后旧容器还在生成中/无图时会被误选(接错容器根因之二)。 function pickCurrentBox() { var best = null, bestAt = 0; boxRegistry.forEach(function (reg, c) { if (!c.isConnected) return; var st = stateMap.get(c); if (st && st.done) return; // 已完成的历史容器不参与竞选 if (!isFreshCandidate(c)) return; // 发送前就存在 = 历史旧容器,排除 var t = Math.max(reg.addedAt, reg.seenAt); if (t >= pendingSetAt && t > bestAt) { best = c; bestAt = t; } }); return best; } // 任务容器失联(点击"重新生成"后元宝可能重建/替换容器 DOM)时, // 寻找后继容器接管当前任务:优先带"与已收集图不同的新图"的容器,其次最新生成的容器。 function findSuccessorBox(st, oldBox) { var best = null, bestAt = 0; boxRegistry.forEach(function (reg, c) { if (!c.isConnected || c === oldBox) return; if (stateMap.has(c)) { var o = stateMap.get(c); if (!o || o.done) return; return; } // 已被占用/已完成的不抢 var s = srcOfImg(currentImg(c)); if (!s || s === st.lastSrc) return; if (st.collected > 0 && globalSeen[s]) return; // 只是旧图复现的容器不迁移 var t = Math.max(reg.addedAt, reg.seenAt); if (t > bestAt) { best = c; bestAt = t; } }); if (best) return best; boxRegistry.forEach(function (reg, c) { if (!c.isConnected || c === oldBox) return; if (stateMap.has(c)) { var o = stateMap.get(c); if (!o || o.done) return; return; } if (isGenerating(c) && reg.addedAt > bestAt) { best = c; bestAt = reg.addedAt; } }); return best; } // ============ 主循环 ============ function tick() { if (!settings.enabled) { pendingImg2Img = false; candidateBox = null; return; } var now = Date.now(); scanRegistry(); if (pendingImg2Img) { if (now - pendingSetAt > PENDING_TTL) { pendingImg2Img = false; candidateBox = null; } else { var usingCandidate = !!(candidateBox && candidateBox.isConnected); var box = usingCandidate ? candidateBox : pickCurrentBox(); if (box) { var st = stateMap.get(box); if (st && st.done) { if (usingCandidate) { // 用户手动锁定的容器已完成过一轮:由 done 分支重新接管 pendingImg2Img = false; candidateBox = null; } // pickCurrentBox 选到已完成历史容器:忽略,继续等待真正的新容器 } else { if (!st) { st = newState(); st.started = true; stateMap.set(box, st); } if (!st.done && !st.lastSrc) { processFirst(box); if (st.collected > 0) { pendingImg2Img = false; candidateBox = null; } } } } } } var rebinds = []; stateMap.forEach(function (st, container) { if (!container.isConnected) { // 容器失联(元宝重建/替换容器 DOM):尝试迁移到后继容器,保留面板与收集进度 var nb = findSuccessorBox(st, container); if (nb) rebinds.push([container, nb, st]); return; } if (st.done) { var imgEl = currentImg(container); var curSrc = srcOfImg(imgEl); // 手动重新生成判定:只认"此前页面没有的"新 src,旧图(globalSeen 内)不触发 if (imgLoaded(imgEl) && curSrc && curSrc !== st.lastSrc && !globalSeen[curSrc]) { if (st.manualSrc === curSrc) { st.manualCount++; if (st.manualCount >= STABLE_TICKS) { st.manualSrc = null; st.manualCount = 0; resetState(container); st = stateMap.get(container); processFirst(container); } } else { st.manualSrc = curSrc; st.manualCount = 1; } } else if (curSrc === st.lastSrc || !curSrc) { st.manualSrc = null; st.manualCount = 0; } return; } if (!st.lastSrc) { processFirst(container); return; } if (st.phase === 'waiting') { tickWaiting(container); } }); // 容器失联任务的迁移统一在此执行(迁移目标若已被占用则放弃) rebinds.forEach(function (r) { if (stateMap.has(r[1])) return; stateMap.delete(r[0]); stateMap.set(r[1], r[2]); }); } setInterval(tick, TICK_MS); // ============ 用户操作监听 ============ function watchSend() { // 用户手动点击"重新生成" → 直接锁定该结果容器为当前目标 document.addEventListener('click', function (e) { if (scriptClick) return; if (!settings.enabled) return; var regenBtn = e.target.closest(SEL_REGEN); if (regenBtn) { var bubble = regenBtn.closest(SEL_AI_BUBBLE) || regenBtn.parentElement; var box = bubble ? bubble.querySelector(SEL_RESULT_BOX) : null; if (box) { var oldSt = stateMap.get(box); // 手动重新生成 = 该容器重新开始一轮:丢弃进行中的旧进度(面板保留,结果清空重收) if (oldSt && !oldSt.done) resetState(box); pendingImg2Img = true; pendingSetAt = Date.now(); candidateBox = box; preSnapshot = null; // 手动重生成:不排除任何容器,直接锁定所在容器 } return; } var btn = e.target.closest('[aria-label="发送"], .SendButton_sendButton__g3Lcb, [class*="sendButton"]'); if (btn && hasUploadPreview()) { cancelAllTasks(); // 新图已发出:上一张的多图生成作废,不再补足(重新生成按钮已消失) pendingImg2Img = true; pendingSetAt = Date.now(); candidateBox = null; snapshotBoxes(); } }, true); document.addEventListener('keydown', function (e) { if (!settings.enabled) return; if (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey && !e.metaKey) { var ed = document.querySelector('.ql-editor'); if (ed && (document.activeElement === ed || ed.contains(document.activeElement)) && hasUploadPreview()) { setTimeout(function () { if (hasUploadPreview()) { cancelAllTasks(); // 新图已发出:上一张的多图生成作废,不再补足(重新生成按钮已消失) pendingImg2Img = true; pendingSetAt = Date.now(); candidateBox = null; snapshotBoxes(); } }, 0); } } }, true); } // ============ DOM 监听 ============ function watchDom() { var observer = new MutationObserver(function (mutations) { if (!settings.enabled) return; mutations.forEach(function (m) { if (m.addedNodes && m.addedNodes.length) { m.addedNodes.forEach(function (n) { if (n.nodeType !== 1) return; var box = null; if (n.matches && n.matches(SEL_RESULT_BOX)) box = n; else if (n.querySelector) box = n.querySelector(SEL_RESULT_BOX); if (box) { if (pendingImg2Img) { var boxImg = currentImg(box); var boxSrc = srcOfImg(boxImg); var fresh = isGenerating(box) || !boxSrc || (boxImg && !imgLoaded(boxImg)); // 新增容器:仅锁定"发送后新插入"且加载中/无图的新结果;旧容器重挂载(fresh=false)不抢锁。 // candidateBox 已失联时允许新容器顶替,避免盯着一去不返的旧节点。 if (isFreshCandidate(box) && fresh && (!candidateBox || !candidateBox.isConnected)) candidateBox = box; } else { // 非发送场景:若存在"容器失联且正在等待补足"的任务,把新容器分配给它 // (点击"重新生成"后元宝重建容器 DOM 的兜底,解决第二张图收集不到) var assigned = false; stateMap.forEach(function (st2, c2) { if (assigned || !st2 || st2.done || st2.phase !== 'waiting') return; if (c2.isConnected) return; // 原容器还在,不需要迁移 if (stateMap.has(box)) return; // 新容器已被占用 var s2 = srcOfImg(currentImg(box)); if (isGenerating(box) || (s2 && s2 !== st2.lastSrc && !(st2.collected > 0 && globalSeen[s2]))) { stateMap.delete(c2); stateMap.set(box, st2); assigned = true; } }); if (assigned) return; } var st = stateMap.get(box); if (st && !st.done && st.phase === 'waiting') { var imgEl = currentImg(box); var curSrc = srcOfImg(imgEl); // 旧图复现(globalSeen 内)不推进 pending,避免把上次的图当新结果 if (imgLoaded(imgEl) && curSrc && curSrc !== st.lastSrc && !(st.collected > 0 && globalSeen[curSrc])) { st.pendingSrc = curSrc; st.stableCount = (st.stableCount || 0) + 1; } } } }); } }); }); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['src'] }); } // ============ 一键简洁:隐藏下载/安装按钮、加"优化版"标识、改底部免责声明 ============ var cleanTimer = null; // 遍历文本节点,返回文本命中正则的父元素列表(trim 后匹配,避免多余空白干扰) function scanTextByRegex(re) { var hits = []; if (!document.body) return hits; var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null); var t; while ((t = walker.nextNode())) { var txt = (t.textContent || '').trim(); if (txt && re.test(txt)) hits.push(t.parentNode); } return hits; } // 隐藏元素并记录原 display,关闭功能时可恢复;幂等(已隐藏/标记过的跳过)。 // 保护自身 UI:设置面板/结果面板/大图浮层等 my-x3-* 元素永不隐藏,避免扫描误伤。 function hideEl(el) { if (!el) return; if (el.closest && el.closest('.my-x3-setpanel,.my-x3-setbtn,.my-x3-panel,.my-x3-overlay')) return; if (el.style.display === 'none') return; if (!el.hasAttribute('data-my-x3-hidden')) { el.setAttribute('data-my-x3-hidden', el.style.display || ''); } el.style.display = 'none'; } // 隐藏包含指定文案的按钮/链接(优先向上收敛到可点击元素,兜底隐藏文本父元素) function hideBanner(textRe) { scanTextByRegex(textRe).forEach(function (parent) { var target = null; if (parent && parent.closest) { target = parent.closest('button,a,[class*="btn"],[class*="Btn"],[class*="download"],[class*="Download"],[class*="install"],[class*="Install"],[class*="entry"],[class*="Entry"]'); } hideEl(target || parent); }); } // 按稳定选择器隐藏(覆盖无文字图标按钮/残留箭头等文本匹配不到的角落) function hideBySelector(selectors) { selectors.forEach(function (sel) { document.querySelectorAll(sel).forEach(hideEl); }); } // 左上角"元宝"后追加小字"优化版"(优先页面上部区域,避免误挂到页脚) function addBadge() { if (document.querySelector('.my-x3-badge')) return; var hits = scanTextByRegex(/^元宝$/); if (!hits.length) return; var target = null; for (var i = 0; i < hits.length; i++) { var el = hits[i]; if (!el || !el.getBoundingClientRect) continue; var r = el.getBoundingClientRect(); if (r.top >= 0 && r.top < 200 && r.bottom > r.top) { target = el; break; } } if (!target) { for (var j = 0; j < hits.length; j++) { var el2 = hits[j]; if (!el2 || !el2.getBoundingClientRect) continue; var r2 = el2.getBoundingClientRect(); if (r2.top >= 0 && r2.bottom > r2.top) { target = el2; break; } } } if (!target) return; var badge = document.createElement('span'); badge.className = 'my-x3-badge'; badge.textContent = '优化版'; target.appendChild(badge); } // 底部免责声明文案替换;记录原始文本与处理标记,可恢复 function replaceDisclaimer() { scanTextByRegex(/内容由AI生成,仅供参考|内容由AI生成/).forEach(function (el) { if (!el || el.getAttribute('data-my-x3-done') === '1') return; if (!el.hasAttribute('data-my-x3-orig')) el.setAttribute('data-my-x3-orig', el.textContent); el.setAttribute('data-my-x3-done', '1'); el.textContent = el.textContent.replace(/内容由AI生成,仅供参考/g, '元宝:优化版已启动'); }); } // 一键简洁整体生效(幂等,可重复调用) function applyCleanUI() { if (!settings.clean) return; hideBanner(/前往下载中心|下载中心/); hideBanner(/安装电脑版/); // 右上角下载按钮组容器(含"安装电脑版"主体与残留的纯图标下拉箭头)、 // 侧边栏"前往下载中心"(未登录态)——按稳定 class 前缀隐藏,兼顾哈希变化 hideBySelector([ '[class*="agent-dialogue__tool__download"]', '[class*="index_download-multi-btn"]', '[class*="index_pc_download__"]' ]); addBadge(); replaceDisclaimer(); } // 关闭一键简洁时恢复页面原状(还原被隐藏元素、移除标识、还原免责声明) function revertCleanUI() { if (cleanTimer) { clearTimeout(cleanTimer); cleanTimer = null; } document.querySelectorAll('[data-my-x3-hidden]').forEach(function (el) { el.style.display = el.getAttribute('data-my-x3-hidden'); el.removeAttribute('data-my-x3-hidden'); }); var badge = document.querySelector('.my-x3-badge'); if (badge) badge.remove(); document.querySelectorAll('[data-my-x3-done]').forEach(function (el) { var orig = el.getAttribute('data-my-x3-orig'); if (orig) el.textContent = orig; el.removeAttribute('data-my-x3-done'); el.removeAttribute('data-my-x3-orig'); }); } // 监听 DOM 动态变化:元宝页面常重渲染,新出现的按钮/文案需重新处理(防抖合并) function watchCleanUI() { var observer = new MutationObserver(function () { if (!settings.clean) return; if (cleanTimer) clearTimeout(cleanTimer); cleanTimer = setTimeout(function () { cleanTimer = null; applyCleanUI(); }, 300); }); observer.observe(document.body, { childList: true, subtree: true }); } // ============ 设置面板(页面右下角悬浮,多图生成 + 一键简洁独立开关) ============ function buildSettingsUI() { var btn = document.createElement('button'); btn.className = 'my-x3-setbtn'; btn.textContent = '元宝优化'; btn.title = '元宝优化设置'; document.body.appendChild(btn); var panel = document.createElement('div'); panel.className = 'my-x3-setpanel'; panel.innerHTML = '
元宝优化设置
' + '
一键简洁
' + '' + '
隐藏下载按钮,让画面更简洁
' + '
' + '
多图生成
' + '' + '
生图数量:
' + '
' + '' + '' + '' + '
'; document.body.appendChild(panel); var cb = panel.querySelector('#my-x3-enable'); cb.checked = settings.enabled; cb.addEventListener('change', function () { settings.enabled = cb.checked; saveSettings(); }); var radios = panel.querySelectorAll('input[name="my-x3-target"]'); Array.prototype.forEach.call(radios, function (r) { if (parseInt(r.value, 10) === settings.target) r.checked = true; r.addEventListener('change', function () { settings.target = parseInt(r.value, 10); saveSettings(); }); }); var cbClean = panel.querySelector('#my-x3-clean'); cbClean.checked = settings.clean; cbClean.addEventListener('change', function () { settings.clean = cbClean.checked; saveSettings(); if (settings.clean) { applyCleanUI(); watchCleanUI(); } else { revertCleanUI(); } }); btn.addEventListener('click', function (e) { e.stopPropagation(); panel.classList.toggle('open'); }); document.addEventListener('click', function (e) { if (panel.classList.contains('open') && !panel.contains(e.target) && e.target !== btn) { panel.classList.remove('open'); } }); } // ============ 启动 ============ function boot() { if (!document.body) { setTimeout(boot, 400); return; } injectStyle(); loadSettings(); buildSettingsUI(); watchSend(); watchDom(); if (settings.clean) { applyCleanUI(); watchCleanUI(); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } })();