在 ChatGPT 管理页的助手。
// ==UserScript==
// @name ChatGPT-Workspace-Helper
// @namespace https://chatgpt.com
// @version 1.5.1
// @description 在 ChatGPT 管理页的助手。
// @author Marx
// @license MIT
// @icon https://chatgpt.com/favicon.ico
// @match https://chatgpt.com/*
// @grant GM_addStyle
// @run-at document-idle
// ==/UserScript==
(function () {
const css = `
:root{--bg:#ffffff;--panel:#f7f9fc;--text:#111827;--muted:#6b7280;--border:#e5e7eb;--accent:#60a5fa;--danger:#ef4444;--success:#16a34a}
#wdd-fab{position:fixed;top:2.2rem;right:2.2rem;width:4rem;height:4rem;border-radius:999rem;border:none;background:transparent;cursor:pointer;z-index:999999;display:grid;place-items:center;box-shadow:0 .5rem 1.25rem rgba(0,0,0,.12);transition:transform .2s ease,box-shadow .2s ease}
#wdd-fab:hover{transform:translateY(-.1rem);box-shadow:0 .75rem 1.75rem rgba(0,0,0,.16)}
#wdd-fab img{width:2.8rem;height:2.8rem;display:block}
#wdd-modal{position:fixed;inset:0;display:none;align-items:flex-start;justify-content:flex-end;background:transparent;z-index:999998}
#wdd-modal.open #wdd-card{opacity:1;transform:translateY(0) scale(.9)}
#wdd-card{width:28rem;max-width:92vw;margin:6rem 2.2rem 0 0;background:var(--panel);color:var(--text);border:0.0625rem solid var(--border);border-radius:1.25rem;box-shadow:0 1.5rem 4rem rgba(0,0,0,.12);overflow:hidden;opacity:0;transform-origin:top right;transform:translateY(-.5rem) scale(.86);transition:opacity .2s ease,transform .2s ease}
.wdd-hd{display:flex;align-items:center;justify-content:space-between;padding:.9rem 1rem;border-bottom:0.0625rem solid var(--border);background:#fff}
.wdd-ttl{font-weight:600;font-size:1rem;letter-spacing:.02em}
.wdd-x{appearance:none;border:none;background:transparent;color:var(--muted);font-size:1.2rem;cursor:pointer;padding:.3rem .6rem;border-radius:.6rem}
.wdd-x:hover{background:#f3f4f6;color:#111827}
.wdd-bd{padding:1rem;display:grid;gap:1rem;background:var(--panel)}
.wdd-row{display:grid;gap:.6rem}
.wdd-lbl{font-size:.8rem;color:var(--muted)}
.wdd-inp,.wdd-sel{width:100%;padding:.8rem 1rem;border-radius:.9rem;border:0.0625rem solid var(--border);background:#fff;color:var(--text);outline:none}
.wdd-inp::placeholder{color:#9ca3af}
.wdd-kv{position:relative;display:flex;align-items:center;gap:.6rem;border:0.0625rem dashed var(--border);border-radius:.9rem;padding:1.8rem 1rem 1rem 1rem;background:#fff}
.wdd-kv .kv-hint{position:absolute;top:.55rem;left:.8rem;font-size:.8rem;color:var(--muted)}
.wdd-kv .kv-row{display:flex;align-items:center;gap:.6rem;width:100%}
.wdd-kv input{flex:1;min-width:0;background:transparent;border:none;color:var(--text);outline:none;font-size:.95rem}
.wdd-actions{display:flex;gap:.6rem;flex-wrap:wrap}
.wdd-btn{flex:1;min-width:0;appearance:none;border:none;border-radius:1rem;padding:.85rem 1.1rem;background:var(--accent);color:#0b1220;font-weight:700;cursor:pointer}
.wdd-btn.secondary{background:#fff;color:var(--text);border:0.0625rem solid var(--border);font-weight:600}
.wdd-btn.warn{background:var(--danger);color:#fff}
.wdd-btn:disabled{opacity:.6;cursor:not-allowed}
.wdd-note{font-size:.8rem;color:var(--muted);line-height:1.45}
.wdd-status{display:flex;align-items:center;gap:.5rem;padding:.7rem 1rem;border-radius:.8rem;border:0.0625rem solid var(--border);background:#f3f4f6;color:#374151;font-weight:600}
.wdd-status.ok{background:#ecfdf5;border-color:#a7f3d0;color:#065f46;font-weight:800}
.wdd-status.err{background:#fef2f2;border-color:#fecaca;color:#991b1b;font-weight:800}
.wdd-grid{display:grid;grid-template-columns:1fr 1fr;gap:.6rem}
#wdd-toast{position:fixed;right:2.2rem;top:10.2rem;background:#111827;color:#fff;padding:.55rem .8rem;border-radius:.6rem;font-size:.85rem;opacity:0;transform:translateY(-.3rem);transition:opacity .15s ease,transform .15s ease;pointer-events:none;z-index:999999}
#wdd-toast.show{opacity:1;transform:translateY(0)}
.wdd-switch-btn{appearance:none;border:none;background:#e5e7eb;width:2.8rem;height:1.6rem;border-radius:999rem;position:relative;cursor:pointer;transition:background .2s ease;border:0.0625rem solid var(--border)}
.wdd-switch-btn::after{content:'';position:absolute;top:.1rem;left:.1rem;width:1.4rem;height:1.4rem;background:#fff;border-radius:50%;transition:transform .2s ease;box-shadow:0 .1rem .25rem rgba(0,0,0,.15)}
.wdd-switch-btn.on{background:var(--accent)}
.wdd-switch-btn.on::after{transform:translateX(1.2rem)}
.wdd-switch-btn:disabled{opacity:.6;cursor:not-allowed}
.wdd-list{display:grid;gap:.5rem;max-height:12.5rem;overflow:auto}
.wdd-item{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.6rem .8rem;border:0.0625rem solid var(--border);border-radius:.7rem;background:#fff;cursor:pointer}
.wdd-item:hover{box-shadow:0 .25rem .75rem rgba(0,0,0,.06)}
.wdd-host{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:16rem}
.wdd-pill{padding:.2rem .55rem;border-radius:999rem;font-size:.75rem;border:0.0625rem solid var(--border);background:#f3f4f6;color:#374151}
.wdd-pill.ok{background:#ecfdf5;border-color:#a7f3d0;color:#065f46}
.wdd-pill.wait{background:#fff7ed;border-color:#fed7aa;color:#9a3412}
.wdd-pill.err{background:#fef2f2;border-color:#fecaca;color:#991b1b}
.wdd-spin{width:1rem;height:1rem;border:.15rem solid #e5e7eb;border-top-color:var(--accent);border-radius:50%;animation:wddspin 1s linear infinite;flex:0 0 auto}
@keyframes wddspin{to{transform:rotate(360deg)}}
.wdd-legacy-row{max-height:0;opacity:0;transform:translateY(-.15rem);overflow:hidden;transition:max-height .25s ease,opacity .2s ease,transform .2s ease}
.wdd-legacy-row.active{max-height:4rem;opacity:1;transform:translateY(0)}
.wdd-legacy-wrap{border-radius:.8rem;border:0.0625rem solid var(--border);background:#fff;padding:.55rem .75rem;display:grid;gap:.25rem}
.wdd-legacy-bar{position:relative;width:100%;height:.45rem;border-radius:999rem;background:#e5e7eb;overflow:hidden}
.wdd-legacy-bar-inner{position:absolute;left:0;top:0;height:100%;width:0;background:var(--accent);transition:width .2s ease}
.wdd-legacy-info{display:flex;justify-content:space-between;font-size:.75rem;color:var(--muted);align-items:center}
`;
if (typeof GM_addStyle === 'function') GM_addStyle(css); else { const st = document.createElement('style'); st.textContent = css; document.head.appendChild(st); }
const fab = document.createElement('button'); fab.id = 'wdd-fab'; fab.innerHTML = `<img src="https://chatgpt.com/favicon.ico" alt="gpt">`;
const modal = document.createElement('div'); modal.id = 'wdd-modal';
const card = document.createElement('div'); card.id = 'wdd-card';
const hd = document.createElement('div'); hd.className = 'wdd-hd';
const ttl = document.createElement('div'); ttl.className = 'wdd-ttl'; ttl.textContent = '域验证助手';
const btnX = document.createElement('button'); btnX.className = 'wdd-x'; btnX.textContent = '✕';
hd.appendChild(ttl); hd.appendChild(btnX);
const bd = document.createElement('div'); bd.className = 'wdd-bd';
const rowWs = document.createElement('div'); rowWs.className = 'wdd-row';
const lblWs = document.createElement('div'); lblWs.className = 'wdd-lbl'; lblWs.textContent = '选择工作区';
const selWs = document.createElement('select'); selWs.className = 'wdd-sel';
const wsBar = document.createElement('div'); wsBar.className = 'wdd-grid';
const wsId = document.createElement('input'); wsId.className = 'wdd-inp'; wsId.readOnly = true; wsId.placeholder = 'account_id';
const orgId = document.createElement('input'); orgId.className = 'wdd-inp'; orgId.readOnly = true; orgId.placeholder = 'organization_id';
wsBar.append(wsId, orgId);
const wsActions = document.createElement('div'); wsActions.className = 'wdd-actions';
const btnRefresh = document.createElement('button'); btnRefresh.className = 'wdd-btn secondary'; btnRefresh.textContent = '刷新工作区';
const btnReloadDomains = document.createElement('button'); btnReloadDomains.className = 'wdd-btn secondary'; btnReloadDomains.textContent = '刷新域名';
const btnLegacyAll = document.createElement('button'); btnLegacyAll.className = 'wdd-btn secondary'; btnLegacyAll.textContent = '开启全部 Legacy 模型';
wsActions.append(btnRefresh, btnReloadDomains, btnLegacyAll);
rowWs.append(lblWs, selWs, wsBar, wsActions);
const rowHost = document.createElement('div'); rowHost.className = 'wdd-row';
const lblHost = document.createElement('div'); lblHost.className = 'wdd-lbl'; lblHost.textContent = '域名';
const inpHost = document.createElement('input'); inpHost.className = 'wdd-inp'; inpHost.placeholder = 'example.com';
rowHost.append(lblHost, inpHost);
const rowTxt = document.createElement('div'); rowTxt.className = 'wdd-row';
const lblTxt = document.createElement('div'); lblTxt.className = 'wdd-lbl'; lblTxt.textContent = 'TXT 记录值';
const txtKV = document.createElement('div'); txtKV.className = 'wdd-kv';
const kvHint = document.createElement('div'); kvHint.className = 'kv-hint'; kvHint.textContent = 'openai-domain-verification=';
const kvRow = document.createElement('div'); kvRow.className = 'kv-row';
const txtVal = document.createElement('input'); txtVal.placeholder = 'dv-xxxxxxxx'; txtVal.readOnly = true;
const btnCopy = document.createElement('button'); btnCopy.className = 'wdd-btn secondary'; btnCopy.style.flex = '0 0 auto'; btnCopy.textContent = '复制';
kvRow.append(txtVal, btnCopy);
txtKV.append(kvHint, kvRow);
rowTxt.append(lblTxt, txtKV);
const rowInvite = document.createElement('div'); rowInvite.className = 'wdd-row';
const lblInvite = document.createElement('div'); lblInvite.className = 'wdd-lbl'; lblInvite.textContent = '允许外部域邀请';
const inviteWrap = document.createElement('div'); inviteWrap.style.display='flex'; inviteWrap.style.alignItems='center'; inviteWrap.style.gap='.6rem';
const btnInviteSwitch = document.createElement('button'); btnInviteSwitch.className='wdd-switch-btn on'; btnInviteSwitch.setAttribute('aria-pressed','true');
const inviteStateText = document.createElement('div'); inviteStateText.className='wdd-note'; inviteStateText.textContent='开启';
inviteWrap.append(btnInviteSwitch, inviteStateText);
rowInvite.append(lblInvite, inviteWrap);
const rowLegacy = document.createElement('div'); rowLegacy.className = 'wdd-row wdd-legacy-row';
const lblLegacy = document.createElement('div'); lblLegacy.className = 'wdd-lbl'; lblLegacy.textContent = 'Legacy 模型批量开启';
const legacyWrap = document.createElement('div'); legacyWrap.className = 'wdd-legacy-wrap';
const legacyBar = document.createElement('div'); legacyBar.className = 'wdd-legacy-bar';
const legacyBarInner = document.createElement('div'); legacyBarInner.className = 'wdd-legacy-bar-inner';
legacyBar.append(legacyBarInner);
const legacyInfo = document.createElement('div'); legacyInfo.className = 'wdd-legacy-info';
const legacyCount = document.createElement('div'); legacyCount.textContent = '0/0';
const legacyMsg = document.createElement('div'); legacyMsg.textContent = '准备就绪';
legacyInfo.append(legacyCount, legacyMsg);
legacyWrap.append(legacyBar, legacyInfo);
rowLegacy.append(lblLegacy, legacyWrap);
const rowList = document.createElement('div'); rowList.className = 'wdd-row';
const lblList = document.createElement('div'); lblList.className = 'wdd-lbl'; lblList.textContent = '已添加域名';
const domainList = document.createElement('div'); domainList.className = 'wdd-list';
rowList.append(lblList, domainList);
const note = document.createElement('div'); note.className = 'wdd-note'; note.textContent = '选择工作区后提交域名以获取TXT,复制后到DNS添加记录,稍候点击“检查”。';
const actions = document.createElement('div'); actions.className = 'wdd-actions';
const btnSubmit = document.createElement('button'); btnSubmit.className = 'wdd-btn'; btnSubmit.textContent = '提交域名';
const btnCheck = document.createElement('button'); btnCheck.className = 'wdd-btn secondary'; btnCheck.textContent = '检查';
const btnRemove = document.createElement('button'); btnRemove.className = 'wdd-btn warn'; btnRemove.textContent = '移除域';
actions.append(btnSubmit, btnCheck, btnRemove);
const status = document.createElement('div'); status.className = 'wdd-status'; status.textContent = '等待操作...';
const toast = document.createElement('div'); toast.id = 'wdd-toast'; toast.textContent = '已复制TXT';
const frag = document.createDocumentFragment();
frag.append(rowWs, rowHost, rowTxt, rowInvite, rowLegacy, rowList, note, actions, status);
bd.append(frag);
card.append(hd, bd); modal.appendChild(card);
document.body.append(fab, modal, toast);
function showToast(){ toast.classList.add('show'); setTimeout(()=>toast.classList.remove('show'), 400); }
function openUI(){ modal.style.display='flex'; requestAnimationFrame(()=>modal.classList.add('open')); }
function closeUI(){ modal.classList.remove('open'); setTimeout(()=>{ modal.style.display='none'; }, 200); }
fab.addEventListener('click', openUI);
btnX.addEventListener('click', closeUI);
modal.addEventListener('click', e=>{ if(e.target===modal) closeUI(); });
const store = {
get k(){ return JSON.parse(localStorage.getItem('wdd_store')||'{}'); },
set v(obj){ localStorage.setItem('wdd_store', JSON.stringify(obj)); },
upd(p){ const cur=this.k; Object.assign(cur,p); this.v=cur; }
};
let token = null;
let tokenTs = 0;
let accounts = [];
let selected = null;
let lastDomain = null;
let inviteState = true;
const domainsCache = new Map();
let legacyHideTimer = null;
function setStatus(msg, kind, spin){
status.classList.remove('ok','err');
if(kind==='ok') status.classList.add('ok');
if(kind==='err') status.classList.add('err');
status.innerHTML = (spin ? `<span class="wdd-spin"></span>` : '') + (msg || '');
}
function enableOps(flag){
btnSubmit.disabled = !flag;
btnCheck.disabled = !flag || !lastDomain?.id;
btnRemove.disabled = !flag || !lastDomain?.id;
btnInviteSwitch.disabled = !flag || !selected;
btnLegacyAll.disabled = !flag || !accounts?.length;
}
function setWsInfo(a){
if(!a){ wsId.value=''; orgId.value=''; selected=null; enableOps(false); renderDomainList([]); return; }
wsId.value = a.account.account_id || '';
orgId.value = a.account.organization_id || '';
selected = { account_id: a.account.account_id, organization_id: a.account.organization_id, name: a.account.name };
store.upd({ sel: selected });
enableOps(true);
loadAllowExternal();
loadDomains();
}
async function getAccessToken(){
const r = await fetch('/api/auth/session', { credentials: 'include', cache: 'no-store' });
if(!r.ok) throw new Error('获取登录会话失败');
const j = await r.json();
const t = j && j.accessToken;
if(!t) throw new Error('未获取到 accessToken');
return t;
}
async function ensureToken(force){
const now = Date.now();
if(!token || force || (now - tokenTs) > 4*60*1000){
token = await getAccessToken();
tokenTs = Date.now();
}
return token;
}
async function fetchAccounts(){
const tz = new Date().getTimezoneOffset();
const u = `/backend-api/accounts/check/v4-2023-04-27?timezone_offset_min=${encodeURIComponent(tz)}`;
await ensureToken();
const r = await fetch(u, { credentials:'include', cache:'no-store', headers:{ 'authorization': 'Bearer '+token, 'accept': '*/*' }});
if(!r.ok) throw new Error('获取工作区失败');
const j = await r.json();
const map = j && j.accounts ? j.accounts : {};
return Object.entries(map).map(([id, obj]) => ({ id, ...obj }));
}
function renderWsOptions(){
selWs.innerHTML = '';
accounts.forEach((it)=>{
const o = document.createElement('option');
const name = it.account?.name || '(未命名)';
o.value = it.id;
o.textContent = `${name} — ${it.id}`;
selWs.appendChild(o);
});
const pref = store.k.sel?.account_id;
if (pref && accounts.find(x=>x.id===pref)) selWs.value = pref;
const cur = accounts.find(x=>x.id===selWs.value) || accounts[0];
setWsInfo(cur);
}
async function initWorkspaceList(){
setStatus('载入工作区中...', null);
try{
await ensureToken();
accounts = await fetchAccounts();
accounts = accounts.filter(it=>{
const name = (it.account?.name || '').toLowerCase();
const id = String(it.id || '').toLowerCase();
return name !== 'default' && id !== 'default';
});
if(!accounts.length) throw new Error('无可用工作区');
renderWsOptions();
setStatus('已载入工作区', null);
}catch(e){ setStatus(String(e.message||e), 'err'); enableOps(false); }
}
function legacySetBar(p){
const v = Math.max(0, Math.min(100, typeof p==='number'?p:0));
legacyBarInner.style.width = v + '%';
}
function legacyShowRow(){
rowLegacy.classList.add('active');
}
function legacyHideRowLater(ms){
if(legacyHideTimer) clearTimeout(legacyHideTimer);
legacyHideTimer = setTimeout(()=>{ rowLegacy.classList.remove('active'); }, ms);
}
function legacyStart(total){
if(legacyHideTimer) { clearTimeout(legacyHideTimer); legacyHideTimer=null; }
legacyCount.textContent = total ? `0/${total}` : '0/0';
legacyMsg.textContent = total ? '开始批量开启...' : '无可处理工作区';
legacySetBar(0);
legacyShowRow();
}
function getAccountName(a){
return (a && a.account && a.account.name) || a?.id || '未知工作区';
}
async function updateLegacyModelsFeature(account){
if(!account || !account.account || !account.account.account_id) throw new Error('无效工作区');
const accountId = account.account.account_id;
const u = `/backend-api/accounts/${encodeURIComponent(accountId)}/beta_features`;
const doFetch = async ()=> fetch(u, { method:'POST', credentials:'include', headers:{
'accept':'*/*','content-type':'application/json','authorization':'Bearer '+token,'chatgpt-account-id': accountId
}, body: JSON.stringify({ feature:'legacy_models', value:true }) });
await ensureToken();
for(let i=0;i<2;i++){
let r;
try{ r = await doFetch(); }
catch(e){
if(i===0){
setStatus('网络错误,重试中...', null, true);
await new Promise(rs=>setTimeout(rs,800));
continue;
}
throw e;
}
if(r.status===401 && i===0){
await ensureToken(true);
setStatus('登录过期,重试中...', null, true);
continue;
}
const txt = await r.text(); let j; try{ j=JSON.parse(txt);}catch{ throw new Error('响应解析失败'); }
if(!r.ok){
const m = j?.error?.message || '更新失败';
throw new Error(m);
}
if(j && j.legacy_models !== true) throw new Error('服务器未确认开启 legacy_models');
return j;
}
throw new Error('请求失败');
}
async function enableLegacyAllWorkspaces(){
if(!accounts || !accounts.length){
setStatus('没有可用工作区', 'err');
return;
}
const workspaces = accounts.filter(a=>a?.account?.account_id);
if(!workspaces.length){
setStatus('没有可用工作区', 'err');
legacyStart(0);
legacyHideRowLater(2000);
return;
}
btnLegacyAll.disabled = true;
enableOps(false);
setStatus('正在为所有工作区开启 Legacy 模型...', null, true);
legacyStart(workspaces.length);
const ok = [];
const fail = [];
let processed = 0;
for(const a of workspaces){
const name = getAccountName(a);
try{
await updateLegacyModelsFeature(a);
ok.push(a);
processed++;
legacyCount.textContent = `${processed}/${workspaces.length}`;
legacyMsg.textContent = `已开启:${name}`;
legacySetBar(processed/workspaces.length*100);
}catch(e){
const msg = e && e.message ? String(e.message) : String(e);
fail.push({ account:a, error:e });
processed++;
legacyCount.textContent = `${processed}/${workspaces.length}`;
legacyMsg.textContent = `失败:${name}(${msg})`;
legacySetBar(processed/workspaces.length*100);
}
}
enableOps(true);
btnLegacyAll.disabled = false;
if(fail.length){
const failNames = fail.map(x=>getAccountName(x.account)).join('、') || '未知';
setStatus(`成功 ${ok.length} 个工作区,失败 ${fail.length} 个:${failNames}`, 'err');
}else{
setStatus(`已为 ${ok.length} 个工作区开启 Legacy 模型`, 'ok');
}
legacyMsg.textContent = fail.length
? `完成:成功 ${ok.length},失败 ${fail.length}`
: `完成:已为 ${ok.length} 个工作区开启 Legacy 模型`;
legacySetBar(100);
legacyHideRowLater(5000);
}
selWs.addEventListener('change', ()=>{
const cur = accounts.find(x=>x.id===selWs.value);
setWsInfo(cur);
});
btnRefresh.addEventListener('click', initWorkspaceList);
btnReloadDomains.addEventListener('click', ()=>{ if(selected) loadDomains(true); });
btnLegacyAll.addEventListener('click', ()=>{
if(!accounts || !accounts.length){ setStatus('请先载入工作区', 'err'); return; }
enableLegacyAllWorkspaces();
});
function parseDomainsPayload(j){
const raw = j?.domains || j?.identity?.domains || j?.domain_whitelist || j?.domain_list || [];
return (Array.isArray(raw)?raw:[]).map(d=>({
id: d.id || d.domain_id || d.uuid || null,
hostname: d.hostname || d.domain || d.name || '',
status: d.status || (typeof d.verified==='boolean' ? (d.verified?'verified':'unverified') : (d.is_verified?'verified':'unverified')),
token: d.dns_verification_token || d.token || d.dns_token || ''
})).filter(x=>x.hostname);
}
function renderDomainList(arr){
domainList.innerHTML = '';
if(!arr.length){ const empty=document.createElement('div'); empty.className='wdd-note'; empty.textContent='暂无域名'; domainList.appendChild(empty); return; }
arr.forEach(d=>{
const it = document.createElement('div'); it.className='wdd-item';
const host = document.createElement('div'); host.className='wdd-host'; host.textContent = d.hostname;
const pill = document.createElement('div'); pill.className='wdd-pill';
const s = String(d.status||'').toLowerCase();
if(s==='verified') pill.classList.add('ok');
else if(s==='pending' || s==='unverified') pill.classList.add('wait');
else pill.classList.add('err');
pill.textContent = s || 'unknown';
it.append(host, pill);
it.addEventListener('click', ()=>{
lastDomain = { id: d.id || null, hostname: d.hostname || '', token: d.token || '', tokenStr: d.token || '' };
txtVal.value = lastDomain.token || '';
inpHost.value = lastDomain.hostname || '';
store.upd({ lastDomain, lastHost: inpHost.value });
btnCheck.disabled = !lastDomain?.id;
btnRemove.disabled = !lastDomain?.id;
});
domainList.appendChild(it);
});
}
async function fetchIdentity({onRetry}={}){
if(!selected) throw new Error('未选择工作区');
const u = `/backend-api/accounts/${encodeURIComponent(selected.account_id)}/identity`;
const doFetch = async ()=> fetch(u, { credentials:'include', headers:{
'accept':'*/*','authorization':'Bearer '+token,'chatgpt-account-id': selected.account_id
}});
try{
await ensureToken();
let r;
try{ r = await doFetch(); }
catch(e){ if(onRetry) onRetry(); setStatus('网络错误,重试中...', null, true); await new Promise(rs=>setTimeout(rs,800)); r = await doFetch(); }
if(r.status===401){ await ensureToken(true); if(onRetry) onRetry(); setStatus('登录过期,重试中...', null, true); r = await doFetch(); }
const txt = await r.text(); let j; try{ j=JSON.parse(txt);}catch{ throw new Error('响应解析失败'); }
if(!r.ok){ const m = j?.error?.message || '获取域名失败'; throw new Error(m); }
return j;
}catch(e){ throw e; }
}
async function loadDomains(force){
if(!selected) return;
const key = selected.account_id;
if(!force && domainsCache.has(key) && (Date.now()-domainsCache.get(key).ts<30*1000)){
renderDomainList(domainsCache.get(key).list); return;
}
setStatus('加载域名列表中...', null, true);
try{
const j = await fetchIdentity({onRetry:()=>{}});
const list = parseDomainsPayload(j);
domainsCache.set(key, { ts: Date.now(), list });
renderDomainList(list);
setStatus('域名列表已更新', null);
}catch(e){
setStatus(String(e.message||e), 'err');
renderDomainList([]);
}
}
async function getAllowExternalSetting(){
const u = `/backend-api/accounts/${encodeURIComponent(selected.account_id)}/settings`;
const doFetch = async ()=> fetch(u, { credentials:'include', headers:{
'accept':'*/*','authorization':'Bearer '+token,'chatgpt-account-id': selected.account_id
}});
await ensureToken();
let r;
try{ r = await doFetch(); }
catch(e){ setStatus('网络错误,重试中...', null, true); await new Promise(rs=>setTimeout(rs,800)); r = await doFetch(); }
if(r.status===401){ await ensureToken(true); setStatus('登录过期,重试中...', null, true); r = await doFetch(); }
const txt = await r.text(); let j; try{ j=JSON.parse(txt);}catch{ throw new Error('响应解析失败'); }
if(!r.ok){ const m = j?.error?.message || '获取设置失败'; throw new Error(m); }
return j.allow_external_domain_invites;
}
async function updateAllowExternalSetting(val){
const u = `/backend-api/accounts/${encodeURIComponent(selected.account_id)}/settings/allow_external_domain_invites`;
const doFetch = async ()=> fetch(u, { method:'POST', credentials:'include', headers:{
'accept':'*/*','content-type':'application/json','authorization':'Bearer '+token,'chatgpt-account-id': selected.account_id
}, body: JSON.stringify({ value: !!val }) });
await ensureToken();
let r;
try{ r = await doFetch(); }
catch(e){ setStatus('网络错误,重试中...', null, true); await new Promise(rs=>setTimeout(rs,800)); r = await doFetch(); }
if(r.status===401){ await ensureToken(true); setStatus('登录过期,重试中...', null, true); r = await doFetch(); }
const txt = await r.text(); let j; try{ j=JSON.parse(txt);}catch{ throw new Error('响应解析失败'); }
if(!r.ok){ const m = j?.error?.message || '更新失败'; throw new Error(m); }
return j.allow_external_domain_invites;
}
async function loadAllowExternal(){
btnInviteSwitch.disabled = true;
try{
await ensureToken();
const val = await getAllowExternalSetting();
setInviteUI(typeof val==='boolean' ? val : true);
}catch{ setInviteUI(true); }
btnInviteSwitch.disabled = false;
}
function setInviteUI(flag){
inviteState = !!flag;
if(inviteState){ btnInviteSwitch.classList.add('on'); btnInviteSwitch.setAttribute('aria-pressed','true'); inviteStateText.textContent='开启'; }
else { btnInviteSwitch.classList.remove('on'); btnInviteSwitch.setAttribute('aria-pressed','false'); inviteStateText.textContent='关闭'; }
}
btnInviteSwitch.addEventListener('click', async ()=>{
if(!selected){ setStatus('请选择工作区', 'err'); return; }
const next = !inviteState;
btnInviteSwitch.disabled = true;
setStatus('更新设置中...', null, true);
try{
await ensureToken();
const serverVal = await updateAllowExternalSetting(next);
setInviteUI(serverVal);
setStatus(serverVal ? '已开启外部域邀请' : '已关闭外部域邀请', 'ok');
}catch(e){
setStatus(String(e.message||e), 'err');
setInviteUI(inviteState);
}
btnInviteSwitch.disabled = false;
});
btnCopy.addEventListener('click', async ()=>{
const v = txtVal.value ? ('openai-domain-verification='+txtVal.value) : '';
if(!v){ setStatus('无TXT可复制', 'err'); return; }
try{ await navigator.clipboard.writeText(v); showToast(); }catch{ setStatus('复制失败', 'err'); }
});
function hostValid(h){ return /^([a-z0-9-]+\.)+[a-z]{2,}$/i.test(h); }
async function createDomain(hostname){
const u = `/backend-api/accounts/${encodeURIComponent(selected.account_id)}/domains`;
const doFetch = async ()=> fetch(u, { method:'POST', credentials:'include', headers:{
'accept':'*/*','content-type':'application/json','authorization':'Bearer '+token,'chatgpt-account-id': selected.account_id
}, body: JSON.stringify({ hostname }) });
await ensureToken();
let r;
try{ r = await doFetch(); }
catch(e){ setStatus('网络错误,重试中...', null, true); await new Promise(rs=>setTimeout(rs,800)); r = await doFetch(); }
if(r.status===401){ await ensureToken(true); setStatus('登录过期,重试中...', null, true); r = await doFetch(); }
const txt = await r.text(); let j; try{ j=JSON.parse(txt);}catch{ throw new Error('响应解析失败'); }
if(!r.ok){ const m = j?.error?.message || '提交失败'; throw new Error(m); }
return j;
}
async function checkDomain(domainId){
const u = `/backend-api/accounts/${encodeURIComponent(selected.account_id)}/domains/${encodeURIComponent(domainId)}/check`;
const doFetch = async ()=> fetch(u, { method:'POST', credentials:'include', headers:{
'accept':'*/*','authorization':'Bearer '+token,'chatgpt-account-id': selected.account_id
}});
await ensureToken();
let r;
try{ r = await doFetch(); }
catch(e){ setStatus('网络错误,重试中...', null, true); await new Promise(rs=>setTimeout(rs,800)); r = await doFetch(); }
if(r.status===401){ await ensureToken(true); setStatus('登录过期,重试中...', null, true); r = await doFetch(); }
const txt = await r.text(); let j; try{ j=JSON.parse(txt);}catch{ throw new Error('响应解析失败'); }
if(!r.ok){ const m = j?.error?.message || '检查失败'; throw new Error(m); }
return j;
}
async function removeDomain(domainId){
const u = `/backend-api/accounts/${encodeURIComponent(selected.account_id)}/domains/${encodeURIComponent(domainId)}`;
const doFetch = async ()=> fetch(u, { method:'DELETE', credentials:'include', headers:{
'accept':'*/*','authorization':'Bearer '+token,'chatgpt-account-id': selected.account_id
}});
await ensureToken();
let r;
try{ r = await doFetch(); }
catch(e){ setStatus('网络错误,重试中...', null, true); await new Promise(rs=>setTimeout(rs,800)); r = await doFetch(); }
if(r.status===401){ await ensureToken(true); setStatus('登录过期,重试中...', null, true); r = await doFetch(); }
if(!r.ok) throw new Error('移除失败');
return true;
}
async function onSubmit(){
const host = (inpHost.value||'').trim();
if(!host){ setStatus('请填写域名', 'err'); return; }
if(!hostValid(host)){ setStatus('域名格式不正确', 'err'); return; }
if(!selected){ setStatus('请选择工作区', 'err'); return; }
setStatus('提交中...', null, true);
enableOps(false);
try{
const j = await createDomain(host);
lastDomain = { id: j.id, hostname: j.hostname, token: j.dns_verification_token, tokenStr: j.dns_verification_token_string };
txtVal.value = j.dns_verification_token || '';
store.upd({ lastDomain, lastHost: host });
btnCheck.disabled = false; btnRemove.disabled = false;
const v = txtVal.value ? ('openai-domain-verification='+txtVal.value) : '';
if(v){ try{ await navigator.clipboard.writeText(v); showToast(); }catch{} }
setStatus('已获取TXT,请到DNS添加记录后再“检查”', null);
await loadDomains(true);
}catch(e){ setStatus(String(e.message||e), 'err'); }
enableOps(true);
}
btnSubmit.addEventListener('click', onSubmit);
inpHost.addEventListener('keydown', e=>{ if(e.key==='Enter') onSubmit(); });
btnCheck.addEventListener('click', async ()=>{
if(!lastDomain?.id){ setStatus('没有可检查的域', 'err'); return; }
setStatus('检查中...', null, true);
enableOps(false);
try{
const j = await checkDomain(lastDomain.id);
if(j.status==='verified'){
setStatus('🎉 域名已验证成功!', 'ok');
}else{
const s = j.status ? String(j.status) : '未验证';
setStatus(`未验证:当前状态 ${s}。请确认已添加 TXT 并等待 DNS 传播。`, 'err');
}
await loadDomains(true);
}catch(e){
setStatus(String(e.message||e), 'err');
}
enableOps(true);
});
btnRemove.addEventListener('click', async ()=>{
if(!lastDomain?.id){ setStatus('没有可移除的域', 'err'); return; }
setStatus('移除中...', null, true);
enableOps(false);
try{
await removeDomain(lastDomain.id);
lastDomain=null; txtVal.value=''; store.upd({ lastDomain:null });
btnCheck.disabled = true; btnRemove.disabled = true;
setStatus('已移除该域', null);
await loadDomains(true);
}catch(e){ setStatus(String(e.message||e), 'err'); }
enableOps(true);
});
function tick(){
const p = location.pathname || '';
if(/^\/admin(?:\/.*)?$/.test(p)){ fab.style.display='grid'; } else { fab.style.display='none'; closeUI(); }
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
(function hydrate(){
const s = store.k;
if(s.lastHost) inpHost.value = s.lastHost;
if(s.lastDomain){ lastDomain = s.lastDomain; if(lastDomain?.token) txtVal.value = lastDomain.token; }
btnCheck.disabled = !lastDomain?.id; btnRemove.disabled = !lastDomain?.id;
})();
(function boot(){
initWorkspaceList();
})();
})();