// ==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();
}
})();