// ==UserScript==
// @name Copilot 增强助手
// @namespace js-hub/copilot-enhance
// @version 1.2.4
// @description 为 Microsoft Copilot 增加地区解锁与附件上传网络增强——3 个开关、2 组分类,菜单打开设置面板,零依赖
// @author EFate
// @license MIT
// @match https://m365.cloud.microsoft/*
// @match https://copilot.cloud.microsoft/*
// @match https://copilot.microsoft.com/*
// @match https://copilot.com/*
// @match https://www.bing.com/chat*
// @match https://bing.com/chat*
// @match https://outlook.office.com/*
// @match https://microsoft365.microsoftonline.cn/*
// @run-at document-start
// @noframes
// @grant unsafeWindow
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// ==/UserScript==
(function () {
'use strict';
// ======================================================================
// L1 配置层 · 版本、存储前缀、功能开关目录、选项定义表(一切开关的唯一来源)
// ======================================================================
var VERSION = '1.2.4';
var PREFIX = 'cp'; // 存储键前缀:cp.<开关名>
var MARK_ATTR = 'data-cp-retry';
var GROUPS = ['地区', '上传'];
// 设置面板的唯一数据源:只保留 2026-11-06 在 copilot.com 实测可用的能力。
var OPT_DEFS = {
// —— 地区 ——
regionUnlock: { group: '地区', type: 'bool', label: '地区解锁(美国)', tip: '把页面配置中的地区、资格与地区可用标记改写为美国可用状态;服务端按 IP 拒绝时仍需配合可用网络区域', def: true },
// —— 上传 ——
uploadRetry: { group: '上传', type: 'bool', label: '附件上传自动重试', tip: '上传接口断连 / 429 / 5xx 时自动退避重试,最多 3 次', def: true },
uploadFailureWatch: { group: '上传', type: 'bool', label: '上传失败自动再试', tip: '页面出现「上传失败」提示时自动点其中的重试按钮,同一提示只点一次', def: true }
};
// ======================================================================
// L2 数据结构层 · OPT 单一数据源(loadOpts 唯一入口,saveOpt 唯一出口,readOpt 唯一归一)
// ======================================================================
var OPT = {};
var STATE = {
uploads: { retried: 0, failed: 0, healing: 0 },
regionPatches: 0
};
// 单值归一(唯一合法判据):布尔项非布尔回落默认值,文本项非字符串回落默认值。
function readOpt(name, v) {
var d = OPT_DEFS[name];
if (!d) return undefined;
if (d.type === 'text') {
if (typeof v !== 'string') return d.def;
return v.replace(/\s+/g, ' ').trim(); // 文本项允许空串=自动
}
return (typeof v === 'boolean') ? v : d.def;
}
function loadOpts() {
for (var name in OPT_DEFS) {
OPT[name] = readOpt(name, GM_getValue(PREFIX + '.' + name));
}
return OPT;
}
// 管理器存储异常时用默认值补齐,保证模型逻辑仍然能启动
function ensureOpts() {
for (var name in OPT_DEFS) {
if (!(name in OPT)) OPT[name] = readOpt(name, undefined);
}
return OPT;
}
function saveOpt(name, value) {
if (!(name in OPT_DEFS)) return undefined;
var v = readOpt(name, value); // 写前归一:坏值不落盘
OPT[name] = v;
GM_setValue(PREFIX + '.' + name, v);
return v;
}
// ======================================================================
// L3 识别 / 解析层 · 文本归一、宿主判定、模型档位与存储值识别
// ======================================================================
var norm = function (s) { return (s || '').replace(/\s+/g, ' ').trim().toLowerCase(); };
// ======================================================================
// L4 核心逻辑层 · 上传判定与重试策略(纯函数,UMD 导出)
// ======================================================================
// 地区解锁:只改资格/地区相关字段,不动账号、语言、主题等无关数据。
var REGION_TRUE_KEYS = {
isCopilotEnabledRegion: 1, isCodexEnabledRegion: 1, isCopilotEligible: 1,
isUserConsumerCopilotEligible: 1, isOpalEligible: 1, HasCopilotAI: 1,
HasCopilotBusinessChat: 1, userCopilotUserSetting: 1, IsLicensed: 1
};
var REGION_FALSE_KEYS = {
isEURegion: 1, isOptedOutByAdmin: 1, isCopilotExtensibilityDisabled: 1,
isCoworkCreditPacksRestrictedRegion: 1
};
var COUNTRY_KEYS = {
userCountryCode: 1, countryCode: 1, clientCountryCode: 1, geoCountryCode: 1,
signInCountryCode: 1, ipCountryCode: 1
};
// 把一个 JSON 对象里的地区相关字段改成美国可用状态;返回改动数。
function patchRegionData(data) {
var n = 0, seen = 0;
var visited = (typeof WeakSet === 'function') ? new WeakSet() : null;
function walk(obj, depth) {
if (!obj || typeof obj !== 'object' || depth > 12 || seen++ > 12000) return;
if (visited) { if (visited.has(obj)) return; visited.add(obj); }
if (Array.isArray(obj)) {
for (var i = 0; i < obj.length; i++) walk(obj[i], depth + 1);
return;
}
for (var k in obj) {
if (!Object.prototype.hasOwnProperty.call(obj, k)) continue;
var v = obj[k];
if (REGION_TRUE_KEYS[k] && v !== true) { obj[k] = true; n++; continue; }
if (REGION_FALSE_KEYS[k] && v !== false) { obj[k] = false; n++; continue; }
if (COUNTRY_KEYS[k] && typeof v === 'string' && /^[a-z]{2}$/i.test(v) && v.toUpperCase() !== 'US') {
obj[k] = 'US'; n++; continue;
}
if ((k === 'm365CopilotAcquisitionState' || k === 'acquisitionState') && v !== 'acquired') {
obj[k] = 'acquired'; n++; continue;
}
if (k === 'cohort' && (!v || v === 'None')) { obj[k] = 'bcais'; n++; continue; }
if (v && typeof v === 'object') walk(v, depth + 1);
}
}
walk(data, 0);
return n;
}
function patchRegionJsonText(text) {
if (typeof text !== 'string' || text.indexOf('{') < 0) return 0;
if (!/Region|Country|eligib|License/i.test(text)) return 0;
try {
var data = JSON.parse(text);
return patchRegionData(data);
} catch (e) { return 0; }
}
function patchRegionDom() {
var n = 0;
['identity', 'userEntitlements', 'm365ChatFeatureSettings'].forEach(function (id) {
var el = document.getElementById(id);
if (!el || !el.textContent) return;
var old = el.textContent;
var k = patchRegionJsonText(old);
if (k > 0) {
try {
var d = JSON.parse(old);
patchRegionData(d);
el.textContent = JSON.stringify(d);
n += k;
} catch (e) { /* 非法 JSON 保持原样 */ }
}
});
var scripts = document.querySelectorAll('script[type="application/json"],script[type="application/ld+json"]');
for (var i = 0; i < scripts.length; i++) {
var txt = scripts[i].textContent || '';
var k2 = patchRegionJsonText(txt);
if (k2 > 0) {
try {
var d2 = JSON.parse(txt);
patchRegionData(d2);
scripts[i].textContent = JSON.stringify(d2);
n += k2;
} catch (e) { }
}
}
return n;
}
function patchRegionStorage() {
var n = 0;
var stores = [];
try { if (window.localStorage) stores.push(window.localStorage); } catch (e) { }
try { if (window.sessionStorage) stores.push(window.sessionStorage); } catch (e) { }
stores.forEach(function (st) {
var len = 0;
try { len = st.length; } catch (e) { return; }
for (var i = 0; i < len && i < 300; i++) {
var key = st.key(i);
if (!key) continue;
var raw;
try { raw = st.getItem(key); } catch (e) { continue; }
if (!raw || raw.length > 200000) continue;
var k2 = patchRegionJsonText(raw);
if (k2 > 0) {
try {
var d = JSON.parse(raw);
patchRegionData(d);
st.setItem(key, JSON.stringify(d));
n += k2;
} catch (e) { }
} else if (/country|region/i.test(key) && /^[a-z]{2}$/i.test(raw) && raw.toUpperCase() !== 'US') {
try { st.setItem(key, 'US'); n++; } catch (e) { }
}
}
});
return n;
}
function patchRegionRuntime() {
var w = pageWin(), n = 0;
['__staticRouterHydrationData', '__INITIAL_STATE__', '__PRELOADED_STATE__', '__COPILOT_STATE__'].forEach(function (k) {
try { if (w && w[k]) n += patchRegionData(w[k]); } catch (e) { }
});
return n;
}
function applyRegionUnlock() {
if (!OPT.regionUnlock) return 0;
var n = 0;
try { n += patchRegionDom(); } catch (e) { }
try { n += patchRegionStorage(); } catch (e) { }
try { n += patchRegionRuntime(); } catch (e) { }
STATE.regionPatches += n;
return n;
}
// 在页面读取 JSON 之前挂钩,覆盖水合数据与后续接口响应。
function installRegionUnlock() {
var w = pageWin();
if (!w || !w.JSON || w.JSON.__cpRegionHooked) return applyRegionUnlock();
try {
var origParse = w.JSON.parse;
w.JSON.parse = function (text, reviver) {
var data = origParse.call(this, text, reviver);
try {
if (OPT.regionUnlock) {
var k = patchRegionData(data);
STATE.regionPatches += k;
}
} catch (e) { }
return data;
};
w.JSON.__cpRegionHooked = true;
} catch (e) { return applyRegionUnlock(); }
return applyRegionUnlock();
}
// 上传接口判定:只要「上传/附件」语义的端点,避免误包对话请求。
// 段名必须是独立的 upload/uploads,或 files|attachments + attach|upload 组合。
function isUploadUrl(url) {
var u = (url || '').toLowerCase();
if (!u || u.indexOf('http') !== 0) return false;
if (/(^|[\/\-_.])uploads?([\/\-_.?]|$)/.test(u)) return true;
if (/\/(attachments?|files?)[\/\-_]?(attach|upload)/.test(u)) return true;
if (/attachment/.test(u) && /file/.test(u)) return true;
// OneDrive / SharePoint / Graph 常见上传会话
if (/createuploadsession|uploadsession|uploadfile|fileupload/i.test(u)) return true;
if (/(sharepoint|onedrive|graph\.microsoft\.com)/.test(u) && /\/content(?:[\/?]|$)/.test(u)) return true;
return false;
}
// 该状态码是否值得重试:断连 / 超时 / 429 / 5xx 重试,4xx 业务错误不重试
function isTransientStatus(status) {
if (status === 0) return true;
if (!status) return false;
if (status === 408 || status === 429) return true;
return status >= 500;
}
function backoffDelay(attempt) {
var n = Math.max(1, attempt | 0);
return Math.min(4000, 300 * Math.pow(2, n - 1));
}
// 从提示文字里找重试按钮:文案命中 + 不是模型行
function pickRetryText(texts) {
for (var i = 0; i < (texts || []).length; i++) {
var t = texts[i];
if (typeof t !== 'string') continue;
var s = t.trim();
if (!s || s.length > 24) continue;
if (/重试|再试|重新上传|重新尝试|retry|try again/i.test(s)) return s;
}
return '';
}
// ======================================================================
// L5 执行层 · 存储预写、驱动模型菜单、上传重试
// ======================================================================
function pageWin() {
if (typeof unsafeWindow !== 'undefined' && unsafeWindow) return unsafeWindow;
if (typeof window !== 'undefined' && window) return window;
return typeof globalThis !== 'undefined' ? globalThis : null;
}
// 包一层 XHR.send:上传请求遇断连/超时/429/5xx 自动退避重试。
// 关键:宿主自己的 load/error 监听器是先注册的,原生事件会先到它们手里——
// 所以必须在 send 期间拦下原生事件,由我们决定何时把「最终结果」重放出去,
// 否则重试的中间响应会被应用当成一次真实上传结果(可能直接报失败)。
var MAX_RETRY = 3;
function installUploadRetry() {
var w = pageWin();
var X = w && w.XMLHttpRequest;
if (!X || !X.prototype || X.prototype.__cpWrapped) return false;
var origSend = X.prototype.send;
X.prototype.__cpWrapped = true;
X.prototype.send = function (body) {
var xhr = this;
// 未 boot 时(测试直调、或脚本被晚注入)OPT 还没装载:此时按默认值处理
var method = String(xhr.__cpMethod || 'POST').toUpperCase();
var uploadMethod = method === 'POST' || method === 'PUT' || method === 'PATCH';
if (OPT.uploadRetry === false || !uploadMethod || !isUploadUrl(xhr.__cpUrl)) {
return origSend.apply(xhr, arguments);
}
var args = arguments;
var attempt = 0;
var settled = false; // 已对调用方交付最终结果
var delivered = false; // 最终结果只重放一次
var gated = false; // 正在拦截原生事件
var origMethod = xhr.__cpMethod || 'POST';
var origUrl = xhr.__cpUrl;
var origHeaders = (xhr.__cpHeaders || []).slice();
var origWithCreds = !!xhr.__cpWithCreds;
var gateTimer = null;
// 原生事件在捕获阶段被截停,不冒泡到应用自己的监听器
var blockers = {};
function gate(on) {
if (on === gated) return;
gated = on;
['load', 'error', 'abort', 'timeout', 'progress', 'loadend', 'readystatechange'].forEach(function (t) {
if (on) {
if (!blockers[t]) {
blockers[t] = function (ev) {
if (ev.stopImmediatePropagation) ev.stopImmediatePropagation();
};
}
xhr.addEventListener(t, blockers[t], true);
} else if (blockers[t]) {
xhr.removeEventListener(t, blockers[t], true);
}
});
}
// 重放只给一次:先解除闸门,再派发一次最终结果;
// 同时打上已重放的标记,避免后续任何路径重复通知调用方。
function deliver(type) {
if (delivered) return;
delivered = true;
gate(false);
try { xhr.dispatchEvent(new w.Event(type)); } catch (e) { /* 派发失败不影响上传本身 */ }
}
function onSettle(type) {
if (settled) return;
settled = true;
if (gateTimer) { clearTimeout(gateTimer); gateTimer = null; }
detachAll();
deliver(type);
}
// 自己的监听也在捕获阶段:必须早于闸门注册,才能在拦截之前拿到结果
function detachAll() {
xhr.removeEventListener('load', onLoad, true);
xhr.removeEventListener('error', onError, true);
xhr.removeEventListener('timeout', onTimeout, true);
}
function retryOr(type) {
if (settled) return;
if (attempt >= MAX_RETRY) {
STATE.uploads.failed++;
return onSettle(type);
}
attempt++;
STATE.uploads.retried++;
var wait = backoffDelay(attempt);
setTimeout(function () {
if (settled) return;
// 同一个 xhr 不能再 send 第二次(readyState 已非 OPENED,会抛 InvalidStateError),
// 必须重建连接再发:按原方法/地址/头重新 open 一遍。
try {
xhr.open(origMethod, origUrl, true);
applyHeaders(xhr, origHeaders);
if (origWithCreds) xhr.withCredentials = true;
} catch (e) {
STATE.uploads.failed++;
return onSettle('error');
}
arm();
}, wait);
}
function onLoad() {
if (settled) return;
if (isTransientStatus(xhr.status) && attempt < MAX_RETRY) return retryOr('load');
onSettle('load');
}
function onError() {
if (settled) return;
retryOr('error');
}
function onTimeout() {
if (settled) return;
retryOr('timeout');
}
function arm() {
// 顺序至关重要:捕获阶段按注册顺序触发,
// 必须先撤闸门 → 注册自己的监听 → 再上闸门,
// 否则重试时自己的监听会排在闸门之后,被自己拦掉。
gate(false);
detachAll();
xhr.addEventListener('load', onLoad, true);
xhr.addEventListener('error', onError, true);
xhr.addEventListener('timeout', onTimeout, true);
gate(true);
try {
origSend.apply(xhr, args);
} catch (e) {
retryOr('error');
}
}
arm();
};
return true;
}
// 记录请求 URL(open 是 send 之前唯一能看到地址的地方)
function installUrlCapture() {
var w = pageWin();
var X = w && w.XMLHttpRequest;
if (!X || !X.prototype || X.prototype.__cpUrlHooked) return false;
var origOpen = X.prototype.open;
var origSetHeader = X.prototype.setRequestHeader;
X.prototype.__cpUrlHooked = true;
X.prototype.open = function (method, url) {
try {
this.__cpMethod = String(method || 'POST');
this.__cpUrl = String(url || '');
this.__cpHeaders = [];
this.__cpWithCreds = false;
} catch (e) { /* 只读属性时跳过 */ }
return origOpen.apply(this, arguments);
};
// 头也记下来:重试要重建连接,缺了 Content-Type 服务端会拒收
if (typeof origSetHeader === 'function') {
X.prototype.setRequestHeader = function (k, v) {
try {
if (this.__cpHeaders) this.__cpHeaders.push([String(k), String(v)]);
} catch (e) { /* 忽略 */ }
return origSetHeader.apply(this, arguments);
};
}
return true;
}
function applyHeaders(xhr, list) {
for (var i = 0; i < (list || []).length; i++) {
try { xhr.setRequestHeader(list[i][0], list[i][1]); } catch (e) { /* 头不可设置时跳过 */ }
}
}
// 上传失败提示里的重试按钮:页面自己给了出口就替用户点一下(同一提示只点一次)
var FAIL_RE = /上传失败|上传未完成|upload failed|failed to upload|couldn't upload|上传出错/;
function healUploadFailure() {
var hits = [];
var all = document.querySelectorAll('div,span,p,li,section,[role="alert"],[role="status"]');
for (var i = 0; i < all.length; i++) {
var el = all[i];
if (!isVisible(el)) continue;
var t = norm(el.textContent);
if (!t || t.length > 60) continue; // 只看短提示块
if (!FAIL_RE.test(t)) continue;
if (el.querySelector('button,[role="button"]')) hits.push(el);
}
var n = 0;
for (var j = 0; j < hits.length && j < 3; j++) {
var box = hits[j];
if (box.getAttribute(MARK_ATTR) === '1') continue; // 同一提示只处理一次
var btns = box.querySelectorAll('button,[role="button"]');
for (var k = 0; k < btns.length; k++) {
var bt = btns[k];
if (!isVisible(bt)) continue;
if (!/重试|再试|重新上传|retry|try again/i.test(norm(bt.textContent))) continue;
box.setAttribute(MARK_ATTR, '1');
bt.click();
n++;
break;
}
}
if (n) STATE.uploads.healing += n;
return n;
}
// ======================================================================
// L6 UI 层 · 设置面板(ghb 视觉规范,cp- 前缀;懒挂载,菜单唯一入口)
// ======================================================================
var PANEL_CSS = [
'html{--cp-accent:#2da44e;--cp-accent-2:#1a7f37;--cp-accent-fg:#ffffff;',
'--cp-good:#2da44e;--cp-warn:#d29922;--cp-bad:#f85149;}',
'.cp-scope{',
'--cp-bg:#0d1117;--cp-bg-2:#161b22;--cp-bg-3:#21262d;',
'--cp-bd:#30363d;--cp-bd-2:#21262d;',
'--cp-fg:#e6edf3;--cp-fg-2:#8b949e;--cp-fg-3:#6e7681;',
'--cp-shadow:0 16px 44px rgba(0,0,0,.5);',
'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;',
'color:var(--cp-fg);font-size:13px;line-height:1.5;}',
'@media (prefers-color-scheme: light){',
'.cp-scope{--cp-bg:#ffffff;--cp-bg-2:#f6f8fa;--cp-bg-3:#eaeef2;',
'--cp-bd:#d0d7de;--cp-bd-2:#d8dee4;',
'--cp-fg:#1f2328;--cp-fg-2:#59636e;--cp-fg-3:#818b98;',
'--cp-shadow:0 16px 44px rgba(31,35,40,.16);}}',
'#cp-overlay{position:fixed;inset:0;z-index:2147483001;background:rgba(0,0,0,.5);opacity:0;pointer-events:none;transition:opacity .2s;}',
'#cp-overlay.cp-open{opacity:1;pointer-events:auto;}',
'#cp-panel{position:fixed;left:50%;top:50%;z-index:2147483002;width:460px;max-width:calc(100vw - 32px);max-height:84vh;',
'background:var(--cp-bg);border:1px solid var(--cp-bd);border-radius:14px;box-shadow:var(--cp-shadow);',
'display:flex;flex-direction:column;overflow:hidden;opacity:0;transform:translate(-50%,-46%) scale(.97);pointer-events:none;',
'transition:opacity .22s,transform .22s cubic-bezier(.4,0,.2,1);}',
'#cp-panel.cp-open{opacity:1;transform:translate(-50%,-50%) scale(1);pointer-events:auto;}',
'.cp-head{display:flex;align-items:center;gap:10px;padding:14px 16px;border-bottom:1px solid var(--cp-bd-2);flex:none;}',
'.cp-head h2{margin:0;font-size:15px;font-weight:600;color:var(--cp-fg);}',
'.cp-head .cp-ver{font-size:11px;color:var(--cp-fg-2);border:1px solid var(--cp-bd);border-radius:999px;padding:1px 7px;}',
'.cp-tabs{display:flex;border-bottom:1px solid var(--cp-bd-2);flex:none;background:var(--cp-bg-2);}',
'.cp-tab{flex:1;padding:10px 0;border:none;background:transparent;cursor:pointer;font-family:inherit;font-size:13px;',
'color:var(--cp-fg-2);border-bottom:2px solid transparent;transition:color .15s,background .15s;}',
'.cp-tab:hover{color:var(--cp-fg);background:var(--cp-bg-3);}',
'.cp-tab.cp-on{color:var(--cp-fg);font-weight:600;border-bottom-color:var(--cp-accent);}',
'.cp-body{flex:1;overflow-y:auto;min-height:180px;}',
'.cp-body::-webkit-scrollbar{width:8px;}',
'.cp-body::-webkit-scrollbar-thumb{background:var(--cp-bd);border-radius:4px;}',
'.cp-page{display:none;} .cp-page.cp-on{display:block;}',
'.cp-setting{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 16px;transition:background .12s;}',
'.cp-setting:hover{background:var(--cp-bg-2);}',
'.cp-st-t{font-size:13px;color:var(--cp-fg);}',
'.cp-st-d{font-size:11.5px;color:var(--cp-fg-2);margin-top:2px;}',
'.cp-switch{position:relative;width:40px;height:22px;flex:none;cursor:pointer;}',
'.cp-switch input{position:absolute;inset:0;width:100%;height:100%;margin:0;opacity:0;cursor:pointer;z-index:1;}',
'.cp-switch i{display:block;width:40px;height:22px;border-radius:11px;background:var(--cp-bd);transition:background .22s;position:relative;}',
'.cp-switch i::after{content:"";position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:#fff;',
'transition:transform .22s cubic-bezier(.4,0,.2,1);}',
'.cp-switch input:checked + i{background:var(--cp-accent);}',
'.cp-switch input:checked + i::after{transform:translateX(18px);}',
'.cp-input{flex:none;width:210px;padding:6px 9px;border:1px solid var(--cp-bd);border-radius:6px;background:var(--cp-bg-2);',
'color:var(--cp-fg);font-family:inherit;font-size:12px;}',
'.cp-input:focus{outline:none;border-color:var(--cp-accent);}',
'.cp-btn{display:inline-flex;align-items:center;gap:5px;padding:5px 10px;border:1px solid var(--cp-bd);border-radius:6px;',
'background:var(--cp-bg-3);color:var(--cp-fg);font-family:inherit;font-size:12px;cursor:pointer;white-space:nowrap;',
'transition:background .15s,border-color .15s;}',
'.cp-btn:hover{background:var(--cp-bd);border-color:var(--cp-fg-3);}',
'.cp-btn.cp-primary{background:var(--cp-accent);border-color:var(--cp-accent);color:var(--cp-accent-fg);}',
'.cp-btn.cp-primary:hover{background:var(--cp-accent-2);}',
'.cp-icon-btn{width:28px;height:28px;display:flex;align-items:center;justify-content:center;border:none;border-radius:6px;',
'background:transparent;color:var(--cp-fg-2);cursor:pointer;font-size:14px;transition:background .15s,color .15s;}',
'.cp-icon-btn:hover{background:var(--cp-bg-3);color:var(--cp-fg);}',
'.cp-status{margin:0 16px 12px;padding:9px 11px;border:1px solid var(--cp-bd);border-radius:8px;background:var(--cp-bg-2);',
'font-size:11.5px;color:var(--cp-fg-2);line-height:1.6;}',
'.cp-status b{color:var(--cp-fg);font-weight:600;}',
'.cp-foot{display:flex;gap:8px;justify-content:flex-end;padding:10px 16px;border-top:1px solid var(--cp-bd-2);flex:none;background:var(--cp-bg-2);}',
'#cp-toasts{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);z-index:2147483004;display:flex;',
'flex-direction:column;gap:8px;align-items:center;pointer-events:none;}',
'.cp-toast{display:flex;align-items:center;gap:8px;max-width:min(560px,calc(100vw - 32px));padding:9px 14px;',
'border-radius:8px;font-family:inherit;font-size:13px;color:#fff;box-shadow:0 6px 20px rgba(0,0,0,.35);',
'opacity:0;transform:translateY(12px);transition:opacity .22s,transform .22s;}',
'.cp-toast.cp-show{opacity:1;transform:none;}',
'.cp-toast.cp-info{background:#1f6feb;} .cp-toast.cp-ok{background:var(--cp-good);}',
'.cp-toast.cp-warn{background:#9e6a03;} .cp-toast.cp-err{background:#b62324;}'
].join('\n');
var uiBuilt = false;
var raf = (typeof requestAnimationFrame === 'function')
? requestAnimationFrame
: function (fn) { setTimeout(fn, 16); };
function toast(msg, kind) {
if (!document || !document.body) return;
var host = document.getElementById('cp-toasts');
if (!host) {
host = document.createElement('div');
host.id = 'cp-toasts';
document.body.appendChild(host);
}
var el = document.createElement('div');
el.className = 'cp-toast cp-' + (kind || 'info');
el.textContent = msg;
host.appendChild(el);
raf(function () { el.classList.add('cp-show'); });
setTimeout(function () {
el.classList.remove('cp-show');
setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 250);
}, 3200);
}
// 注入判定与自动选模型结论必须明示——用户不该对着一个没反应的按钮猜原因
function statusText() {
var t = OPT.regionUnlock ? '地区解锁已启用' : '地区解锁已关闭';
t += '
上传增强已启用';
if (!OPT.uploadRetry && !OPT.uploadFailureWatch) return t + ';上传两项均已关闭';
t += ';地区改写 ' + STATE.regionPatches + ' 处';
t += '
已重试 ' + STATE.uploads.retried + ' 次 / 自动再试 ' +
STATE.uploads.healing + ' 次 / 最终失败 ' + STATE.uploads.failed + ' 次';
return t;
}
function makeSettingRow(name) {
var d = OPT_DEFS[name];
var row = document.createElement('div');
row.className = 'cp-setting';
var left = document.createElement('div');
var t = document.createElement('div');
t.className = 'cp-st-t';
t.textContent = d.label;
var tip = document.createElement('div');
tip.className = 'cp-st-d';
tip.textContent = d.tip;
left.appendChild(t);
left.appendChild(tip);
row.appendChild(left);
if (d.type === 'text') {
var inp = document.createElement('input');
inp.className = 'cp-input';
inp.type = 'text';
inp.setAttribute('data-opt', name);
inp.setAttribute('placeholder', '自动(留空)');
inp.value = OPT[name];
inp.setAttribute('aria-label', d.label);
row.appendChild(inp);
return row;
}
var lab = document.createElement('label');
lab.className = 'cp-switch';
var input = document.createElement('input');
input.type = 'checkbox';
input.setAttribute('data-opt', name);
input.checked = OPT[name];
var i = document.createElement('i');
lab.appendChild(input);
lab.appendChild(i);
row.appendChild(lab);
return row;
}
function buildUI() {
if (uiBuilt) return;
var style = document.createElement('style');
style.id = 'cp-panel-style';
style.textContent = PANEL_CSS;
(document.head || document.documentElement).appendChild(style);
var overlay = document.createElement('div');
overlay.id = 'cp-overlay';
var panel = document.createElement('div');
panel.id = 'cp-panel';
panel.className = 'cp-scope';
panel.setAttribute('role', 'dialog');
panel.setAttribute('aria-label', 'Copilot 增强助手设置');
var head = document.createElement('div');
head.className = 'cp-head';
var h2 = document.createElement('h2');
h2.textContent = 'Copilot 增强助手';
var ver = document.createElement('span');
ver.className = 'cp-ver';
ver.textContent = 'v' + VERSION;
var closeBtn = document.createElement('button');
closeBtn.className = 'cp-icon-btn';
closeBtn.id = 'cp-close';
closeBtn.textContent = '✕';
closeBtn.setAttribute('aria-label', '关闭');
head.appendChild(h2);
head.appendChild(ver);
head.appendChild(closeBtn);
var tabs = document.createElement('div');
tabs.className = 'cp-tabs';
var body = document.createElement('div');
body.className = 'cp-body';
var byGroup = {};
for (var name in OPT_DEFS) {
var g = OPT_DEFS[name].group;
(byGroup[g] = byGroup[g] || []).push(name);
}
GROUPS.forEach(function (g, idx) {
var tab = document.createElement('button');
tab.className = 'cp-tab' + (idx === 0 ? ' cp-on' : '');
tab.setAttribute('data-tab', g);
tab.textContent = g;
tabs.appendChild(tab);
var page = document.createElement('div');
page.className = 'cp-page' + (idx === 0 ? ' cp-on' : '');
page.setAttribute('data-page', g);
(byGroup[g] || []).forEach(function (n) { page.appendChild(makeSettingRow(n)); });
if (g === '上传') {
var st = document.createElement('div');
st.className = 'cp-status';
st.id = 'cp-status';
st.innerHTML = statusText();
page.appendChild(st);
}
body.appendChild(page);
});
panel.appendChild(head);
panel.appendChild(tabs);
panel.appendChild(body);
document.body.appendChild(overlay);
document.body.appendChild(panel);
uiBuilt = true;
}
function refreshStatus() {
var el = document && document.getElementById('cp-status');
if (el) el.innerHTML = statusText();
}
function openPanel() {
if (!document.body) return;
buildUI();
refreshStatus();
document.getElementById('cp-overlay').classList.add('cp-open');
document.getElementById('cp-panel').classList.add('cp-open');
}
function closePanel() {
var o = document.getElementById('cp-overlay');
var p = document.getElementById('cp-panel');
if (o) o.classList.remove('cp-open');
if (p) p.classList.remove('cp-open');
}
// 面板事件:委托一次;开关即时生效,文本框在 change 时落盘
function bindPanelEvents() {
document.addEventListener('click', function (ev) {
if (!uiBuilt) return;
var t = ev.target;
if (!t || typeof t.closest !== 'function') return;
if (t.id === 'cp-overlay') { closePanel(); return; }
if (t.closest('#cp-close')) { closePanel(); return; }
var tab = t.closest('.cp-tab');
if (tab) {
var g = tab.getAttribute('data-tab');
var all = document.querySelectorAll('.cp-tab');
for (var i = 0; i < all.length; i++) all[i].classList.remove('cp-on');
tab.classList.add('cp-on');
var pages = document.querySelectorAll('.cp-page');
for (var j = 0; j < pages.length; j++) {
pages[j].classList.toggle('cp-on', pages[j].getAttribute('data-page') === g);
}
}
}, false);
document.addEventListener('change', function (ev) {
var t = ev.target;
if (!t || !t.getAttribute || !t.hasAttribute('data-opt')) return;
var name = t.getAttribute('data-opt');
if (!(name in OPT_DEFS)) return;
var d = OPT_DEFS[name];
var v = d.type === 'text' ? saveOpt(name, t.value) : saveOpt(name, t.checked);
if (d.type === 'text') t.value = v;
if (name === 'regionUnlock' && v === true) {
tryInstall(installRegionUnlock);
if (typeof toast === 'function') toast('地区解锁已启用', 'ok');
}
refreshStatus();
}, false);
document.addEventListener('keydown', function (ev) {
if (ev.key === 'Escape') closePanel(); // 面板规范:Esc 关闭(唯一允许的快捷键)
}, false);
}
// ======================================================================
// L7 Watcher 层 · 唯一监听者:等待帮手 + 路由钩子 + DOM 变更防抖复检
// ======================================================================
var _debTimer = null;
function watch() {
if (typeof MutationObserver === 'undefined') return;
var mo = new MutationObserver(function () {
if (_debTimer) clearTimeout(_debTimer);
_debTimer = setTimeout(function () {
_debTimer = null;
if (OPT.regionUnlock) { try { applyRegionUnlock(); } catch (e) { } }
if (OPT.uploadFailureWatch) healUploadFailure();
if (uiBuilt) refreshStatus();
}, 400);
});
mo.observe(document.documentElement, { childList: true, subtree: true });
}
// ======================================================================
// 启动序列 · loadOpts → 预写模型偏好 → (DOMContentLoaded) 面板事件/监听 → 自动选模型
// ======================================================================
function registerMenu() {
if (typeof GM_registerMenuCommand !== 'function') return;
GM_registerMenuCommand('打开设置面板', openPanel);
}
// 上传增强安装失败只影响该项,不影响页面本身。
function tryInstall(fn) {
try { return fn(); } catch (e) { return false; }
}
function start() {
tryInstall(bindPanelEvents);
tryInstall(watch);
tryInstall(installUrlCapture);
tryInstall(installUploadRetry);
tryInstall(applyRegionUnlock);
}
function boot() {
try { loadOpts(); } catch (e) { /* 管理器读取异常时继续用默认值 */ }
ensureOpts();
// document-start 先挂钩 JSON.parse,尽量早于宿主读取资格数据
tryInstall(installRegionUnlock);
try { registerMenu(); } catch (e) { /* 菜单注册失败不影响上传增强 */ }
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', start);
} else {
start();
}
}
// ======================================================================
// UMD 导出 · 测试 require 真实代码;浏览器环境照常启动
// ======================================================================
var API = {
VERSION: VERSION, PREFIX: PREFIX, GROUPS: GROUPS, OPT_DEFS: OPT_DEFS, STATE: STATE,
readOpt: readOpt, saveOpt: saveOpt, loadOpts: loadOpts, ensureOpts: ensureOpts, norm: norm,
patchRegionData: patchRegionData, patchRegionJsonText: patchRegionJsonText,
bindPanelEvents: bindPanelEvents,
applyRegionUnlock: applyRegionUnlock, installRegionUnlock: installRegionUnlock,
isUploadUrl: isUploadUrl, isTransientStatus: isTransientStatus, backoffDelay: backoffDelay,
pickRetryText: pickRetryText, installUploadRetry: installUploadRetry,
healUploadFailure: healUploadFailure, statusText: statusText,
openPanel: openPanel, closePanel: closePanel, buildUI: buildUI,
__setOpt: function (name, value) { OPT[name] = value; },
__getOpt: function (name) { return OPT[name]; }
};
if (typeof module !== 'undefined' && module.exports) {
module.exports = API;
} else {
boot();
}
})();