ChatGPT Workspace 管理一体化面板
// ==UserScript==
// @name ChatGPT-Workspace-Helper
// @namespace https://chatgpt.com
// @version 1.8.0
// @description ChatGPT Workspace 管理一体化面板
// @author Marx
// @license MIT
// @icon https://chatgpt.com/favicon.ico
// @match https://chatgpt.com/*
// @grant GM_addStyle
// @run-at document-idle
// ==/UserScript==
(function () {
if (window !== window.top) return;
const PERMISSIONS_MEMBER = [
"chatgpt.workspace.model.GPT-4.5.access",
"chatgpt.workspace.model.o1-pro.access",
"chatgpt.workspace.model.o3-pro.access",
"chatgpt.workspace.model.o3.access",
"chatgpt.workspace.model.o4-mini.access",
"chatgpt.workspace.model.o4-mini-high.access",
"chatgpt.workspace.model.GPT-4.1.access",
"chatgpt.workspace.model.GPT-4.1-mini.access",
"chatgpt.workspace.model.GPT-5-reasoning.access",
"chatgpt.workspace.model.GPT-5-pro.access",
"chatgpt.workspace.feature.deep-research.access",
"chatgpt.workspace.feature.image-gen.access",
"chatgpt.workspace.feature.voice.access",
"chatgpt.workspace.feature.aura-browser-memories.access",
"chatgpt.workspace.gpt.crud",
"chatgpt.workspace.feature.search.access",
"chatgpt.workspace.feature.allow-codex-access.access",
"chatgpt.workspace.feature.allow-codex-local-access.access",
"chatgpt.workspace.feature.codex-agent-network-access.access",
"chatgpt.workspace.feature.hive.access",
"chatgpt.workspace.feature.hive-knowledge-retrieval.access",
"chatgpt.workspace.project.crud",
"chatgpt.workspace.project.share",
"chatgpt.workspace.feature.canvas-code-execution.access",
"chatgpt.workspace.feature.canvas-code-network-access.access",
"chatgpt.workspace.feature.video-screen-sharing.access",
"chatgpt.workspace.feature.share-chat-with-workspace.access",
"chatgpt.workspace.member.role.view",
"chatgpt.workspace.model.GPT-5.1.access",
"chatgpt.workspace.model.GPT-5.1-reasoning.access",
"chatgpt.workspace.model.GPT-5.1-pro.access",
"chatgpt.workspace.gpt.allow_all_third_party",
"chatgpt.workspace.model.GPT-5.2.access",
"chatgpt.workspace.model.GPT-5.2.instant.access",
"chatgpt.workspace.model.GPT-5.2-reasoning.access",
"chatgpt.workspace.feature.aura.access",
"chatgpt.workspace.model.GPT-5.2-pro.access",
"chatgpt.workspace.feature.agent-mode.access",
"chatgpt.workspace.feature.codex-admin.access",
"chatgpt.workspace.feature.codex-slack-posting.access",
"chatgpt.workspace.gpt.share_workspace",
"chatgpt.workspace.gpt.share_external",
"chatgpt.workspace.gpt.allow_specific_third_party"
];
const css = `
:root{--bg:#ffffff;--panel:#f7f9fc;--text:#111827;--muted:#6b7280;--border:#e5e7eb;--accent:#60a5fa;--danger:#ef4444;--success:#16a34a;--warn:#f59e0b}
#wdd-fab{position:fixed;top:1.3rem;right:1.3rem;width:3.2rem;height:3.2rem;border-radius:999rem;border:none;background:transparent;cursor:pointer;z-index:999999;display:grid;place-items:center;box-shadow:0 .42rem 1.05rem rgba(0,0,0,.12);transition:transform .2s ease,box-shadow .2s ease}
#wdd-fab:hover{transform:translateY(-.07rem);box-shadow:0 .62rem 1.45rem rgba(0,0,0,.16)}
#wdd-fab img{width:2.05rem;height:2.05rem;display:block}
#wdd-team-float{position:fixed;top:4.9rem;right:1.3rem;z-index:999999;padding:.46rem .72rem;border-radius:999rem;border:1px solid rgba(0,0,0,.08);background:#e5e7eb;color:#111827;font-size:11.5px;font-weight:650;letter-spacing:.02em;cursor:pointer;box-shadow:0 .3rem .9rem rgba(0,0,0,.06);backdrop-filter:blur(6px);transition:transform .12s ease,box-shadow .2s ease,opacity .2s ease}
#wdd-team-float:hover{transform:translateY(-1px);box-shadow:0 .45rem 1.1rem rgba(0,0,0,.08);opacity:.96}
#wdd-team-float[data-loading="1"]{cursor:default;opacity:.7}
#wdd-team-float.tm-success{background:#a7f3d0;color:#064e3b}
#wdd-team-float.tm-error{background:#fee2e2;color:#991b1b}
#wdd-modal{position:fixed;inset:0;display:none;align-items:flex-start;justify-content:flex-end;background:transparent;z-index:999998;padding:1rem}
#wdd-modal.open #wdd-card{opacity:1;transform:translateY(0) scale(.98)}
#wdd-card{width:28rem;max-width:92vw;margin:0;background:var(--panel);color:var(--text);border:0.0625rem solid var(--border);border-radius:1.05rem;box-shadow:0 1.15rem 3rem rgba(0,0,0,.12);overflow:hidden;opacity:0;transform-origin:top right;transform:translateY(-.3rem) scale(.92);transition:opacity .2s ease,transform .2s ease;max-height:calc(100vh - 2rem);display:flex;flex-direction:column;font-size:11.5px}
.wdd-hd{display:flex;align-items:center;justify-content:space-between;padding:.6rem .75rem;border-bottom:0.0625rem solid var(--border);background:#fff}
.wdd-ttl{font-weight:800;font-size:12px;letter-spacing:.02em}
.wdd-x{appearance:none;border:none;background:transparent;color:var(--muted);font-size:16px;cursor:pointer;padding:.18rem .48rem;border-radius:.5rem}
.wdd-x:hover{background:#f3f4f6;color:#111827}
.wdd-bd{padding:.7rem;display:grid;gap:.62rem;background:var(--panel);overflow:auto;flex:1;min-height:0}
.wdd-row{display:grid;gap:.4rem}
.wdd-lbl{font-size:10.5px;color:var(--muted);display:flex;align-items:center;justify-content:space-between;gap:.55rem}
.wdd-inp,.wdd-sel{width:100%;padding:.5rem .65rem;border-radius:.68rem;border:0.0625rem solid var(--border);background:#fff;color:var(--text);outline:none;font-size:11.5px}
.wdd-inp::placeholder{color:#9ca3af}
.wdd-kv{position:relative;display:flex;align-items:center;gap:.5rem;border:0.0625rem dashed var(--border);border-radius:.68rem;padding:1.22rem .65rem .65rem .65rem;background:#fff}
.wdd-kv .kv-hint{position:absolute;top:.35rem;left:.55rem;font-size:10.5px;color:var(--muted)}
.wdd-kv .kv-row{display:flex;align-items:center;gap:.5rem;width:100%}
.wdd-kv input{flex:1;min-width:0;background:transparent;border:none;color:var(--text);outline:none;font-size:11.5px}
.wdd-actions{display:flex;gap:.42rem;flex-wrap:wrap}
.wdd-actions.grid2{display:grid;grid-template-columns:1fr 1fr;gap:.42rem}
.wdd-actions.grid3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:.42rem}
.wdd-btn{appearance:none;border:none;border-radius:.75rem;padding:.5rem .62rem;background:var(--accent);color:#0b1220;font-weight:850;cursor:pointer;font-size:11.5px}
.wdd-btn.secondary{background:#fff;color:var(--text);border:0.0625rem solid var(--border);font-weight:750}
.wdd-btn.warn{background:var(--danger);color:#fff}
.wdd-btn.ok{background:var(--success);color:#fff}
.wdd-btn:disabled{opacity:.6;cursor:not-allowed}
.wdd-note{font-size:10.5px;color:var(--muted);line-height:1.45}
.wdd-status{display:flex;align-items:center;gap:.45rem;padding:.55rem .7rem;border-radius:.72rem;border:0.0625rem solid var(--border);background:#f3f4f6;color:#374151;font-weight:750;position:sticky;bottom:0;z-index:3}
.wdd-status.ok{background:#ecfdf5;border-color:#a7f3d0;color:#065f46;font-weight:900}
.wdd-status.err{background:#fef2f2;border-color:#fecaca;color:#991b1b;font-weight:900}
.wdd-grid{display:grid;grid-template-columns:1fr 1fr;gap:.42rem}
#wdd-toast{position:fixed;right:1.3rem;top:7.6rem;background:#111827;color:#fff;padding:.42rem .6rem;border-radius:.5rem;font-size:11.5px;opacity:0;transform:translateY(-.22rem);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.35rem;height:1.38rem;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:.085rem;left:.085rem;width:1.21rem;height:1.21rem;background:#fff;border-radius:50%;transition:transform .2s ease;box-shadow:0 .08rem .2rem rgba(0,0,0,.15)}
.wdd-switch-btn.on{background:var(--accent)}
.wdd-switch-btn.on::after{transform:translateX(1rem)}
.wdd-switch-btn:disabled{opacity:.6;cursor:not-allowed}
.wdd-list{display:grid;gap:.36rem;max-height:10.2rem;overflow:auto}
.wdd-item{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.46rem .6rem;border:0.0625rem solid var(--border);border-radius:.62rem;background:#fff;cursor:pointer}
.wdd-item:hover{box-shadow:0 .2rem .55rem rgba(0,0,0,.06)}
.wdd-host{font-weight:850;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:15.4rem;font-size:11.5px}
.wdd-pill{padding:.14rem .48rem;border-radius:999rem;font-size:10.5px;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:12px;height:12px;border:2px 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(-.1rem);overflow:hidden;transition:max-height .25s ease,opacity .2s ease,transform .2s ease}
.wdd-legacy-row.active{max-height:3.25rem;opacity:1;transform:translateY(0)}
.wdd-legacy-wrap{border-radius:.68rem;border:0.0625rem solid var(--border);background:#fff;padding:.42rem .55rem;display:grid;gap:.18rem}
.wdd-legacy-bar{position:relative;width:100%;height:.36rem;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:10.5px;color:var(--muted);align-items:center}
.wdd-hr{height:1px;background:var(--border);border:none;margin:.1rem 0}
.wdd-log{background:#0b1220;color:#e5e7eb;border-radius:.72rem;border:1px solid rgba(148,163,184,.2);padding:.52rem .6rem;max-height:9.2rem;overflow:auto;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:10.5px;line-height:1.35;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}
.wdd-log .i{color:#e5e7eb}
.wdd-log .s{color:#6ee7b7}
.wdd-log .e{color:#fca5a5}
.wdd-inline{display:flex;gap:.42rem;align-items:center}
.wdd-inline .wdd-inp{flex:1}
`;
if (typeof GM_addStyle === 'function') GM_addStyle(css);
else { const st = document.createElement('style'); st.textContent = css; document.head.appendChild(st); }
const store = {
get k(){ try{ return JSON.parse(localStorage.getItem('wdd_store')||'{}'); }catch{ return {}; } },
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 userId = null;
let accounts = [];
let selected = null;
let lastDomain = null;
let inviteState = true;
const domainsCache = new Map();
let legacyHideTimer = null;
const fab = document.createElement('button');
fab.id = 'wdd-fab';
fab.innerHTML = `<img src="https://chatgpt.com/favicon.ico" alt="gpt">`;
const teamFloat = document.createElement('button');
teamFloat.id = 'wdd-team-float';
teamFloat.type = 'button';
teamFloat.textContent = '生成团队支付链接';
teamFloat.dataset.loading = '0';
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 = 'Workspace Helper';
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 wsBtns = document.createElement('div');
wsBtns.className = 'wdd-actions grid3';
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 全部';
const btnPatchMember = document.createElement('button');
btnPatchMember.className = 'wdd-btn secondary';
btnPatchMember.textContent = '更新 Member 权限';
const btnEnableApple = document.createElement('button');
btnEnableApple.className = 'wdd-btn secondary';
btnEnableApple.textContent = '开启 Apple 客户端';
const btnEnableLegacyCurrent = document.createElement('button');
btnEnableLegacyCurrent.className = 'wdd-btn secondary';
btnEnableLegacyCurrent.textContent = 'Legacy 当前';
const btnQuickCur = document.createElement('button');
btnQuickCur.className = 'wdd-btn secondary';
btnQuickCur.textContent = 'Quick 当前';
const btnQuickAll = document.createElement('button');
btnQuickAll.className = 'wdd-btn secondary';
btnQuickAll.textContent = 'Quick 全部';
const btnOnboardCur = document.createElement('button');
btnOnboardCur.className = 'wdd-btn secondary';
btnOnboardCur.textContent = 'Onboarding 当前';
const btnOnboardAll = document.createElement('button');
btnOnboardAll.className = 'wdd-btn secondary';
btnOnboardAll.textContent = 'Onboarding 全部';
const btnSeat1000n = document.createElement('button');
btnSeat1000n.className = 'wdd-btn secondary';
btnSeat1000n.textContent = 'SEAT 1000→n';
const btnLeaveCur = document.createElement('button');
btnLeaveCur.className = 'wdd-btn warn';
btnLeaveCur.textContent = '退出当前工作区';
wsBtns.append(
btnRefresh, btnReloadDomains, btnPatchMember,
btnEnableLegacyCurrent, btnEnableApple, btnSeat1000n,
btnQuickCur, btnQuickAll, btnOnboardCur,
btnOnboardAll, btnLegacyAll, btnLeaveCur
);
rowWs.append(lblWs, selWs, wsBar, wsBtns);
const rowTeam = document.createElement('div');
rowTeam.className = 'wdd-row';
const lblTeam = document.createElement('div');
lblTeam.className = 'wdd-lbl';
lblTeam.textContent = 'Team Checkout(生成并复制支付链接)';
const teamGrid1 = document.createElement('div');
teamGrid1.className = 'wdd-grid';
const inpTeamName = document.createElement('input');
inpTeamName.className = 'wdd-inp';
inpTeamName.placeholder = 'workspace_name';
inpTeamName.value = store.k.team_workspace_name || 'OAI';
const selTeamInterval = document.createElement('select');
selTeamInterval.className = 'wdd-sel';
const optM = document.createElement('option'); optM.value='month'; optM.textContent='month';
const optY = document.createElement('option'); optY.value='year'; optY.textContent='year';
selTeamInterval.append(optM, optY);
selTeamInterval.value = store.k.team_price_interval || 'month';
teamGrid1.append(inpTeamName, selTeamInterval);
const teamGrid2 = document.createElement('div');
teamGrid2.className = 'wdd-grid';
const inpTeamSeats = document.createElement('input');
inpTeamSeats.className = 'wdd-inp';
inpTeamSeats.placeholder = 'seat_quantity';
inpTeamSeats.value = String(store.k.team_seat_quantity ?? 2);
const inpTeamCountry = document.createElement('input');
inpTeamCountry.className = 'wdd-inp';
inpTeamCountry.placeholder = 'country';
inpTeamCountry.value = store.k.team_country || 'JP';
teamGrid2.append(inpTeamSeats, inpTeamCountry);
const teamGrid3 = document.createElement('div');
teamGrid3.className = 'wdd-grid';
const inpTeamCurrency = document.createElement('input');
inpTeamCurrency.className = 'wdd-inp';
inpTeamCurrency.placeholder = 'currency';
inpTeamCurrency.value = store.k.team_currency || 'USD';
const inpTeamPromo = document.createElement('input');
inpTeamPromo.className = 'wdd-inp';
inpTeamPromo.placeholder = 'promo_campaign';
inpTeamPromo.value = store.k.team_promo_campaign || 'team-1-month-free';
teamGrid3.append(inpTeamCurrency, inpTeamPromo);
const teamBtns = document.createElement('div');
teamBtns.className = 'wdd-actions grid2';
const btnTeamCheckout = document.createElement('button');
btnTeamCheckout.className = 'wdd-btn ok';
btnTeamCheckout.textContent = '生成并复制';
const btnTeamOpen = document.createElement('button');
btnTeamOpen.className = 'wdd-btn secondary';
btnTeamOpen.textContent = '仅生成(不复制)';
teamBtns.append(btnTeamCheckout, btnTeamOpen);
rowTeam.append(lblTeam, teamGrid1, teamGrid2, teamGrid3, teamBtns);
const rowFree = document.createElement('div');
rowFree.className = 'wdd-row';
const lblFree = document.createElement('div');
lblFree.className = 'wdd-lbl';
lblFree.textContent = '创建 Freemium Workspace';
const freeLine = document.createElement('div');
freeLine.className = 'wdd-inline';
const inpFreeName = document.createElement('input');
inpFreeName.className = 'wdd-inp';
inpFreeName.value = store.k.freeName || 'OAI-free';
const btnCreateFree = document.createElement('button');
btnCreateFree.className = 'wdd-btn ok';
btnCreateFree.style.flex = '0 0 auto';
btnCreateFree.textContent = '创建';
freeLine.append(inpFreeName, btnCreateFree);
rowFree.append(lblFree, freeLine);
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='.45rem';
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 = '批量进度';
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 grid3';
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 rowK12 = document.createElement('div');
rowK12.className = 'wdd-row';
const lblK12 = document.createElement('div');
lblK12.className = 'wdd-lbl';
lblK12.textContent = 'K12 批量创建(仅 /k12-create-workspace 可用)';
const k12Grid = document.createElement('div');
k12Grid.className = 'wdd-grid';
const inpK12Count = document.createElement('input');
inpK12Count.className = 'wdd-inp';
inpK12Count.placeholder = '数量';
inpK12Count.value = store.k.k12Count || '5';
const inpK12Prefix = document.createElement('input');
inpK12Prefix.className = 'wdd-inp';
inpK12Prefix.placeholder = '名称前缀';
inpK12Prefix.value = store.k.k12Prefix || 'OAI ';
k12Grid.append(inpK12Count, inpK12Prefix);
const k12Btns = document.createElement('div');
k12Btns.className = 'wdd-actions grid2';
const btnK12Run = document.createElement('button');
btnK12Run.className = 'wdd-btn ok';
btnK12Run.textContent = '开始创建';
const btnLogClear = document.createElement('button');
btnLogClear.className = 'wdd-btn secondary';
btnLogClear.textContent = '清空日志';
k12Btns.append(btnK12Run, btnLogClear);
rowK12.append(lblK12, k12Grid, k12Btns);
const rowLog = document.createElement('div');
rowLog.className = 'wdd-row';
const lblLog = document.createElement('div');
lblLog.className = 'wdd-lbl';
lblLog.textContent = '日志';
const logBox = document.createElement('div');
logBox.className = 'wdd-log';
logBox.id = 'wdd-log';
rowLog.append(lblLog, logBox);
const status = document.createElement('div');
status.className = 'wdd-status';
status.textContent = '等待操作...';
const toast = document.createElement('div');
toast.id = 'wdd-toast';
toast.textContent = '已复制';
bd.append(
rowWs,
rowTeam,
document.createElement('hr'),
rowFree,
document.createElement('hr'),
rowHost,
rowTxt,
rowInvite,
rowLegacy,
rowList,
note,
actions,
document.createElement('hr'),
rowK12,
rowLog,
status
);
bd.querySelectorAll('hr').forEach(hr=>{ hr.className='wdd-hr'; });
card.append(hd, bd);
modal.appendChild(card);
document.body.append(fab, teamFloat, modal, toast);
function showToast(msg){
if(msg) toast.textContent = msg;
toast.classList.add('show');
setTimeout(()=>toast.classList.remove('show'), 450);
}
function isPanelOpen(){ return modal.classList.contains('open'); }
function updateTeamFloatVisibility(){
const shouldShow = (location.pathname === '/') && !isPanelOpen();
teamFloat.style.display = shouldShow ? 'inline-flex' : 'none';
}
function openUI(){
modal.style.display='flex';
requestAnimationFrame(()=>{
modal.classList.add('open');
updateTeamFloatVisibility();
});
}
function closeUI(){
modal.classList.remove('open');
setTimeout(()=>{
modal.style.display='none';
updateTeamFloatVisibility();
}, 210);
}
fab.addEventListener('click', openUI);
btnX.addEventListener('click', closeUI);
modal.addEventListener('click', e=>{ if(e.target===modal) closeUI(); });
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 log(message, type='i'){
const line = document.createElement('div');
line.className = type;
const t = new Date();
const ts = String(t.getHours()).padStart(2,'0')+':'+String(t.getMinutes()).padStart(2,'0')+':'+String(t.getSeconds()).padStart(2,'0');
line.textContent = `[${ts}] ${message}`;
logBox.appendChild(line);
logBox.scrollTop = logBox.scrollHeight;
}
btnLogClear.addEventListener('click', ()=>{ logBox.textContent=''; });
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;
btnPatchMember.disabled = !flag || !selected;
btnEnableApple.disabled = !flag || !selected;
btnEnableLegacyCurrent.disabled = !flag || !selected;
btnQuickCur.disabled = !flag || !selected;
btnQuickAll.disabled = !flag || !accounts?.length;
btnOnboardCur.disabled = !flag || !selected;
btnOnboardAll.disabled = !flag || !accounts?.length;
btnSeat1000n.disabled = !flag || !selected;
btnLeaveCur.disabled = !flag || !selected;
btnReloadDomains.disabled = !flag || !selected;
btnCreateFree.disabled = !flag;
btnK12Run.disabled = !flag;
btnTeamCheckout.disabled = !flag;
btnTeamOpen.disabled = !flag;
}
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');
userId = j?.user?.id || j?.user_id || userId || null;
return t;
}
function decodeJwtUserId(t){
try{
const parts = String(t||'').split('.');
if(parts.length < 2) return null;
const b64 = parts[1].replace(/-/g,'+').replace(/_/g,'/');
const raw = atob(b64 + '==='.slice((b64.length+3)%4));
const json = decodeURIComponent(Array.from(raw).map(c=>'%' + c.charCodeAt(0).toString(16).padStart(2,'0')).join(''));
const payload = JSON.parse(json);
const auth = payload['https://api.openai.com/auth'] || {};
return auth.user_id || payload.user_id || null;
}catch{
return null;
}
}
async function ensureToken(force){
const now = Date.now();
if(!token || force || (now - tokenTs) > 4*60*1000){
token = await getAccessToken();
tokenTs = Date.now();
if(!userId) userId = decodeJwtUserId(token);
}
return token;
}
async function sleep(ms){ return new Promise(r=>setTimeout(r, ms)); }
async function fetchJsonWithRetry(url, init, {retry=1, retryDelay=800, allowEmpty=true}={}){
await ensureToken();
let lastErr = null;
for(let i=0;i<=retry;i++){
try{
const r = await fetch(url, init);
if(r.status===401 && i<retry){
await ensureToken(true);
const init2 = { ...init, headers: { ...(init.headers||{}), authorization: 'Bearer '+token } };
const r2 = await fetch(url, init2);
return await parseResp(r2, allowEmpty);
}
return await parseResp(r, allowEmpty);
}catch(e){
lastErr = e;
if(i<retry){ await sleep(retryDelay); continue; }
throw e;
}
}
throw lastErr || new Error('请求失败');
}
async function parseResp(r, allowEmpty){
const txt = await r.text().catch(()=> '');
let j = null;
if(txt){
try{ j = JSON.parse(txt); }catch{ j = null; }
}
if(!r.ok){
const m = j?.error?.message || j?.message || (txt && txt.slice(0,200)) || `HTTP ${r.status}`;
throw new Error(m);
}
if(j !== null) return j;
if(allowEmpty) return {};
throw new Error('响应解析失败');
}
async function copyToClipboard(text){
if(navigator.clipboard && navigator.clipboard.writeText){
await navigator.clipboard.writeText(text);
return;
}
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.top = '-9999px';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
}
function teamConfig(){
const name = (inpTeamName.value||'').trim() || 'OAI';
const interval = (selTeamInterval.value||'month').trim() || 'month';
const seats = Math.max(1, parseInt((inpTeamSeats.value||'2').trim(), 10) || 2);
const country = (inpTeamCountry.value||'JP').trim() || 'JP';
const currency = (inpTeamCurrency.value||'USD').trim() || 'USD';
const promo = (inpTeamPromo.value||'team-1-month-free').trim() || 'team-1-month-free';
store.upd({
team_workspace_name: name,
team_price_interval: interval,
team_seat_quantity: seats,
team_country: country,
team_currency: currency,
team_promo_campaign: promo
});
return { name, interval, seats, country, currency, promo };
}
async function createTeamCheckoutUrl({name, interval, seats, country, currency, promo}){
const u = `/backend-api/payments/checkout`;
const cancel_url = `https://chatgpt.com/?numSeats=${encodeURIComponent(seats)}&selectedPlan=${encodeURIComponent(interval)}&referrer=https%3A%2F%2Fauth.openai.com%2F#team-pricing-seat-selection`;
const init = {
method:'POST',
credentials:'include',
headers:{
'Accept':'*/*',
'Content-Type':'application/json',
'Authorization': `Bearer ${token}`,
'oai-language': 'zh-CN'
},
body: JSON.stringify({
plan_name: 'chatgptteamplan',
team_plan_data: {
workspace_name: name,
price_interval: interval,
seat_quantity: seats
},
billing_details: {
country,
currency
},
cancel_url,
promo_campaign: promo,
checkout_ui_mode: 'redirect'
})
};
const data = await fetchJsonWithRetry(u, init, {retry:1});
const url = data?.url;
if(!url) throw new Error('响应缺少 checkout 链接');
return url;
}
async function runTeamCheckout({copy=true, from='panel'}={}){
const cfg = teamConfig();
setStatus('Team Checkout 生成中...', null, true);
enableOps(false);
try{
await ensureToken();
const url = await createTeamCheckoutUrl(cfg);
if(copy){
await copyToClipboard(url);
showToast('已复制支付链接');
setStatus('Team Checkout:已复制 ✅', 'ok');
}else{
setStatus('Team Checkout:已生成 ✅', 'ok');
}
log(`Team Checkout OK (${from}) seats=${cfg.seats} interval=${cfg.interval} country=${cfg.country} currency=${cfg.currency}`, 's');
log(url, 'i');
return url;
}catch(e){
setStatus(`Team Checkout 失败:${e.message||e}`, 'err');
log(`Team Checkout FAIL (${from}):${e.message||e}`, 'e');
throw e;
}finally{
enableOps(true);
}
}
btnTeamCheckout.addEventListener('click', ()=>runTeamCheckout({copy:true, from:'panel'}).catch(()=>{}));
btnTeamOpen.addEventListener('click', ()=>runTeamCheckout({copy:false, from:'panel'}).catch(()=>{}));
teamFloat.addEventListener('click', async ()=>{
if(teamFloat.dataset.loading === '1') return;
teamFloat.dataset.loading = '1';
const originalText = teamFloat.textContent;
teamFloat.classList.remove('tm-success','tm-error');
teamFloat.textContent = '生成中...';
try{
await runTeamCheckout({copy:true, from:'float'});
teamFloat.textContent = '已复制支付链接';
teamFloat.classList.add('tm-success');
}catch(e){
teamFloat.textContent = '生成失败';
teamFloat.classList.add('tm-error');
alert(e?.message ? e.message : String(e));
}finally{
setTimeout(()=>{
teamFloat.textContent = originalText;
teamFloat.dataset.loading = '0';
teamFloat.classList.remove('tm-success','tm-error');
updateTeamFloatVisibility();
}, 2000);
}
});
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 || '(未命名)';
const role = it.account?.account_user_role || it.account?.role || '';
o.value = it.id;
o.textContent = role ? `${name} (${role}) — ${it.id}` : `${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);
}
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: { account_id: a.id, organization_id: selected.organization_id, name: selected.name } });
enableOps(true);
loadAllowExternal();
loadDomains();
}
async function initWorkspaceList(){
setStatus('载入工作区中...', null, true);
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);
log(`workspace 已加载:${accounts.length} 个`, 's');
}catch(e){
setStatus(String(e.message||e), 'err');
log(`workspace 加载失败:${e.message||e}`, 'e');
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 updateBetaFeature(accountId, feature, value){
const u = `/backend-api/accounts/${encodeURIComponent(accountId)}/beta_features`;
const init = {
method:'POST',
credentials:'include',
headers:{
'accept':'*/*',
'content-type':'application/json',
'authorization':'Bearer '+token,
'chatgpt-account-id': accountId
},
body: JSON.stringify({ feature, value: !!value })
};
return fetchJsonWithRetry(u, init, {retry:1});
}
async function enableLegacyAllWorkspaces(){
if(!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);
log('开始批量开启 legacy_models', 'i');
let processed = 0, ok = 0, fail = 0;
for(const a of workspaces){
const accountId = a.account.account_id;
const name = getAccountName(a);
try{
await updateBetaFeature(accountId, 'legacy_models', true);
ok++;
log(`Legacy OK: ${name}`, 's');
}catch(e){
fail++;
log(`Legacy FAIL: ${name} (${e.message||e})`, 'e');
}finally{
processed++;
legacyCount.textContent = `${processed}/${workspaces.length}`;
legacyMsg.textContent = `完成 ${processed}/${workspaces.length}`;
legacySetBar(processed/workspaces.length*100);
}
}
enableOps(true);
btnLegacyAll.disabled = false;
legacyMsg.textContent = `完成:成功 ${ok},失败 ${fail}`;
legacySetBar(100);
legacyHideRowLater(5000);
setStatus(fail ? `Legacy:成功 ${ok},失败 ${fail}` : `Legacy:全部成功(${ok})`, fail ? 'err' : 'ok');
}
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 || '' };
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(){
if(!selected) throw new Error('未选择工作区');
const u = `/backend-api/accounts/${encodeURIComponent(selected.account_id)}/identity`;
const init = {
credentials:'include',
headers:{
'accept':'*/*',
'authorization':'Bearer '+token,
'chatgpt-account-id': selected.account_id
}
};
return fetchJsonWithRetry(u, init, {retry:1});
}
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();
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(){
if(!selected) throw new Error('未选择工作区');
const u = `/backend-api/accounts/${encodeURIComponent(selected.account_id)}/settings`;
const init = {
credentials:'include',
headers:{
'accept':'*/*',
'authorization':'Bearer '+token,
'chatgpt-account-id': selected.account_id
}
};
const j = await fetchJsonWithRetry(u, init, {retry:1});
return j.allow_external_domain_invites;
}
async function updateAllowExternalSetting(val){
if(!selected) throw new Error('未选择工作区');
const u = `/backend-api/accounts/${encodeURIComponent(selected.account_id)}/settings/allow_external_domain_invites`;
const init = {
method:'POST',
credentials:'include',
headers:{
'accept':'*/*',
'content-type':'application/json',
'authorization':'Bearer '+token,
'chatgpt-account-id': selected.account_id
},
body: JSON.stringify({ value: !!val })
};
const j = await fetchJsonWithRetry(u, init, {retry:1});
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 prev = inviteState;
const next = !inviteState;
btnInviteSwitch.disabled = true;
setStatus('更新设置中...', null, true);
try{
await ensureToken();
const serverVal = await updateAllowExternalSetting(next);
setInviteUI(serverVal);
setStatus(serverVal ? '已开启外部域邀请' : '已关闭外部域邀请', 'ok');
log(`allow_external_domain_invites=${serverVal}`, 's');
}catch(e){
setStatus(String(e.message||e), 'err');
setInviteUI(prev);
log(`更新 allow_external_domain_invites 失败:${e.message||e}`, 'e');
}
btnInviteSwitch.disabled = false;
});
btnCopy.addEventListener('click', async ()=>{
const v = txtVal.value ? ('openai-domain-verification='+txtVal.value) : '';
if(!v){ setStatus('无TXT可复制', 'err'); return; }
try{ await copyToClipboard(v); showToast('已复制TXT'); }catch{ setStatus('复制失败', 'err'); }
});
function hostValid(h){ return /^([a-z0-9-]+\.)+[a-z]{2,}$/i.test(h); }
async function createDomain(hostname){
if(!selected) throw new Error('未选择工作区');
const u = `/backend-api/accounts/${encodeURIComponent(selected.account_id)}/domains`;
const init = {
method:'POST',
credentials:'include',
headers:{
'accept':'*/*',
'content-type':'application/json',
'authorization':'Bearer '+token,
'chatgpt-account-id': selected.account_id
},
body: JSON.stringify({ hostname })
};
return fetchJsonWithRetry(u, init, {retry:1});
}
async function checkDomain(domainId){
if(!selected) throw new Error('未选择工作区');
const u = `/backend-api/accounts/${encodeURIComponent(selected.account_id)}/domains/${encodeURIComponent(domainId)}/check`;
const init = {
method:'POST',
credentials:'include',
headers:{
'accept':'*/*',
'authorization':'Bearer '+token,
'chatgpt-account-id': selected.account_id
}
};
return fetchJsonWithRetry(u, init, {retry:1});
}
async function removeDomain(domainId){
if(!selected) throw new Error('未选择工作区');
const u = `/backend-api/accounts/${encodeURIComponent(selected.account_id)}/domains/${encodeURIComponent(domainId)}`;
const init = {
method:'DELETE',
credentials:'include',
headers:{
'accept':'*/*',
'authorization':'Bearer '+token,
'chatgpt-account-id': selected.account_id
}
};
await fetchJsonWithRetry(u, init, {retry:1});
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 };
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 copyToClipboard(v); showToast('已复制TXT'); }catch{} }
setStatus('已获取TXT,请到DNS添加记录后再“检查”', null);
log(`提交域名:${host}`, 's');
await loadDomains(true);
}catch(e){
setStatus(String(e.message||e), 'err');
log(`提交域名失败:${e.message||e}`, 'e');
}
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');
log(`域名验证成功:${lastDomain.hostname}`, 's');
}else{
const s = j.status ? String(j.status) : '未验证';
setStatus(`未验证:当前状态 ${s}`, 'err');
log(`域名未验证:${lastDomain.hostname} (${s})`, 'e');
}
await loadDomains(true);
}catch(e){
setStatus(String(e.message||e), 'err');
log(`检查失败:${e.message||e}`, 'e');
}
enableOps(true);
});
btnRemove.addEventListener('click', async ()=>{
if(!lastDomain?.id){ setStatus('没有可移除的域', 'err'); return; }
setStatus('移除中...', null, true);
enableOps(false);
try{
await removeDomain(lastDomain.id);
log(`移除域名:${lastDomain.hostname}`, 's');
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');
log(`移除失败:${e.message||e}`, 'e');
}
enableOps(true);
});
async function patchMemberRolePermissions(){
if(!selected) throw new Error('未选择工作区');
const accountId = selected.account_id;
const u = `/backend-api/rbac/workspace/${encodeURIComponent(accountId)}/roles/role-workspace-member__chatgpt-workspace__${encodeURIComponent(accountId)}_overridden?account_id=${encodeURIComponent(accountId)}`;
const init = {
method:'PATCH',
credentials:'include',
headers:{
'accept':'*/*',
'content-type':'application/json',
'authorization':'Bearer '+token,
'chatgpt-account-id': accountId
},
body: JSON.stringify({ role_name:"member", description:"ChatGPT workspace member role", permissions: PERMISSIONS_MEMBER })
};
return fetchJsonWithRetry(u, init, {retry:1});
}
btnPatchMember.addEventListener('click', async ()=>{
if(!selected){ setStatus('请选择工作区', 'err'); return; }
setStatus('更新 Member 权限中...', null, true);
enableOps(false);
try{
await ensureToken();
await patchMemberRolePermissions();
setStatus('已更新 Member 角色权限', 'ok');
log('已更新 Member 权限', 's');
showToast('已更新权限');
}catch(e){
setStatus(String(e.message||e), 'err');
log(`更新 Member 权限失败:${e.message||e}`, 'e');
}
enableOps(true);
});
btnEnableApple.addEventListener('click', async ()=>{
if(!selected){ setStatus('请选择工作区', 'err'); return; }
setStatus('开启 Apple 客户端中...', null, true);
enableOps(false);
try{
await ensureToken();
await updateBetaFeature(selected.account_id, 'client_application_apple', true);
setStatus('已开启 client_application_apple', 'ok');
log('已开启 client_application_apple', 's');
showToast('已开启 Apple');
}catch(e){
setStatus(String(e.message||e), 'err');
log(`开启 Apple 失败:${e.message||e}`, 'e');
}
enableOps(true);
});
btnEnableLegacyCurrent.addEventListener('click', async ()=>{
if(!selected){ setStatus('请选择工作区', 'err'); return; }
setStatus('开启 Legacy(当前) 中...', null, true);
enableOps(false);
try{
await ensureToken();
await updateBetaFeature(selected.account_id, 'legacy_models', true);
setStatus('已开启 legacy_models(当前)', 'ok');
log('已开启 legacy_models(当前)', 's');
showToast('已开启 Legacy');
}catch(e){
setStatus(String(e.message||e), 'err');
log(`开启 Legacy(当前) 失败:${e.message||e}`, 'e');
}
enableOps(true);
});
btnLegacyAll.addEventListener('click', ()=>{
if(!accounts?.length){ setStatus('请先载入工作区', 'err'); return; }
enableLegacyAllWorkspaces();
});
async function requestSetting(accountId, settingKey, payloadObj){
const url = `/backend-api/accounts/${encodeURIComponent(accountId)}/settings/${encodeURIComponent(settingKey)}`;
const body = JSON.stringify(payloadObj);
const doFetch = async (method)=> fetchJsonWithRetry(url, {
method,
credentials:'include',
headers:{
'accept':'*/*',
'content-type':'application/json',
'authorization':'Bearer '+token,
'chatgpt-account-id': accountId
},
body
}, {retry:1});
try{
return await doFetch('POST');
}catch(e){
const msg = String(e.message||e);
if(/405|404/.test(msg)) return doFetch('PATCH');
throw e;
}
}
const QUICK_SETTINGS = [
{ key:'workspace_discoverable', body:{ value:true, public_display_name:null, use_workspace_name_for_discovery:true } },
{ key:'auto_accept_requests', body:{ value:true } },
{ key:'auto_provision', body:{ value:true } }
];
async function runQuickSettingsFor(accountId){
for(const it of QUICK_SETTINGS) await requestSetting(accountId, it.key, it.body);
}
btnQuickCur.addEventListener('click', async ()=>{
if(!selected){ setStatus('请选择工作区', 'err'); return; }
setStatus('Quick(当前) 执行中...', null, true);
enableOps(false);
try{
await ensureToken();
await runQuickSettingsFor(selected.account_id);
setStatus('Quick(当前) 完成 ✅', 'ok');
log('Quick(当前) 完成', 's');
}catch(e){
setStatus(`Quick 失败:${e.message||e}`, 'err');
log(`Quick(当前) 失败:${e.message||e}`, 'e');
}
enableOps(true);
});
btnQuickAll.addEventListener('click', async ()=>{
if(!accounts?.length){ setStatus('请先载入工作区', 'err'); return; }
setStatus('Quick(全部) 执行中...', null, true);
enableOps(false);
legacyStart(accounts.length);
log('Quick(全部) 开始', 'i');
let processed=0, ok=0, fail=0;
for(const a of accounts){
const accountId = a.account?.account_id;
if(!accountId) continue;
try{
await ensureToken();
await runQuickSettingsFor(accountId);
ok++;
log(`Quick OK: ${getAccountName(a)}`, 's');
}catch(e){
fail++;
log(`Quick FAIL: ${getAccountName(a)} (${e.message||e})`, 'e');
}finally{
processed++;
legacyCount.textContent = `${processed}/${accounts.length}`;
legacyMsg.textContent = `Quick 全部:${processed}/${accounts.length}`;
legacySetBar(processed/accounts.length*100);
}
}
legacyMsg.textContent = `Quick 完成:成功 ${ok},失败 ${fail}`;
legacySetBar(100);
legacyHideRowLater(5000);
setStatus(fail ? `Quick 全部:成功 ${ok},失败 ${fail}` : `Quick 全部:全部成功(${ok})`, fail ? 'err' : 'ok');
enableOps(true);
});
async function markOnboardingViewed(accountId){
const u = `/backend-api/settings/announcement_viewed?announcement_id=oai%2Fapps%2FhasSeenOnboarding`;
const init = {
method:'POST',
credentials:'include',
headers:{
'accept':'*/*',
'authorization':'Bearer '+token,
'chatgpt-account-id': accountId
}
};
return fetchJsonWithRetry(u, init, {retry:1});
}
btnOnboardCur.addEventListener('click', async ()=>{
if(!selected){ setStatus('请选择工作区', 'err'); return; }
setStatus('Onboarding(当前) 标记中...', null, true);
enableOps(false);
try{
await ensureToken();
await markOnboardingViewed(selected.account_id);
setStatus('Onboarding(当前) 已标记', 'ok');
log('Onboarding(当前) 已标记', 's');
}catch(e){
setStatus(`Onboarding 失败:${e.message||e}`, 'err');
log(`Onboarding(当前) 失败:${e.message||e}`, 'e');
}
enableOps(true);
});
btnOnboardAll.addEventListener('click', async ()=>{
if(!accounts?.length){ setStatus('请先载入工作区', 'err'); return; }
setStatus('Onboarding(全部) 标记中...', null, true);
enableOps(false);
legacyStart(accounts.length);
log('Onboarding(全部) 开始', 'i');
let processed=0, ok=0, fail=0;
for(const a of accounts){
const accountId = a.account?.account_id;
if(!accountId) continue;
try{
await ensureToken();
await markOnboardingViewed(accountId);
ok++;
log(`Onboarding OK: ${getAccountName(a)}`, 's');
}catch(e){
fail++;
log(`Onboarding FAIL: ${getAccountName(a)} (${e.message||e})`, 'e');
}finally{
processed++;
legacyCount.textContent = `${processed}/${accounts.length}`;
legacyMsg.textContent = `Onboarding 全部:${processed}/${accounts.length}`;
legacySetBar(processed/accounts.length*100);
}
}
legacyMsg.textContent = `Onboarding 完成:成功 ${ok},失败 ${fail}`;
legacySetBar(100);
legacyHideRowLater(5000);
setStatus(fail ? `Onboarding 全部:成功 ${ok},失败 ${fail}` : `Onboarding 全部:全部成功(${ok})`, fail ? 'err' : 'ok');
enableOps(true);
});
async function createFreemiumWorkspace(name){
const u = `/backend-api/accounts/create_freemium_workspace`;
const init = {
method:'POST',
credentials:'include',
headers:{
'accept':'*/*',
'content-type':'application/json',
'authorization':'Bearer '+token
},
body: JSON.stringify({ workspace_name: name })
};
return fetchJsonWithRetry(u, init, {retry:1});
}
btnCreateFree.addEventListener('click', async ()=>{
const name = (inpFreeName.value||'').trim();
if(!name){ setStatus('workspace_name 不能为空', 'err'); return; }
store.upd({ freeName: name });
setStatus('创建 Freemium 中...', null, true);
enableOps(false);
try{
await ensureToken();
const res = await createFreemiumWorkspace(name);
log(`Freemium 创建请求已发送:${name}`, 's');
setStatus(`已创建:${name}`, 'ok');
showToast('已创建');
await initWorkspaceList();
if(res?.account_id){
const hit = accounts.find(a=>a?.account?.account_id===res.account_id);
if(hit){ selWs.value = hit.id; setWsInfo(hit); }
}
}catch(e){
setStatus(`创建失败:${e.message||e}`, 'err');
log(`Freemium 创建失败:${e.message||e}`, 'e');
}
enableOps(true);
});
async function fetchSeatsEntitled(accountId){
const u = `/backend-api/subscriptions?account_id=${encodeURIComponent(accountId)}`;
const init = {
method:'GET',
credentials:'include',
headers:{
'accept':'*/*',
'authorization':'Bearer '+token,
'chatgpt-account-id': accountId
}
};
const j = await fetchJsonWithRetry(u, init, {retry:1});
const seats = typeof j?.seats_entitled === 'number' ? j.seats_entitled : null;
if(seats == null) throw new Error('未获取到 seats_entitled');
return seats;
}
async function updateSeats(accountId, updatedSeats){
const u = `/backend-api/subscriptions/update`;
const init = {
method:'POST',
credentials:'include',
headers:{
'accept':'*/*',
'content-type':'application/json',
'authorization':'Bearer '+token,
'chatgpt-account-id': accountId
},
body: JSON.stringify({ account_id: accountId, updated_seats: updatedSeats })
};
return fetchJsonWithRetry(u, init, {retry:1});
}
btnSeat1000n.addEventListener('click', async ()=>{
if(!selected){ setStatus('请选择工作区', 'err'); return; }
setStatus('SEAT 1000→n 执行中...', null, true);
enableOps(false);
try{
await ensureToken();
const seats = await fetchSeatsEntitled(selected.account_id);
const target = seats + 1;
if(target > 5){
setStatus(`不执行:seats_entitled=${seats},目标=${target} > 5`, 'err');
log(`SEAT 不执行:seats_entitled=${seats},目标=${target} > 5`, 'e');
enableOps(true);
return;
}
log(`SEAT 当前 seats_entitled=${seats},目标 n=${target}`, 'i');
await updateSeats(selected.account_id, 1000);
await sleep(12);
await updateSeats(selected.account_id, target);
setStatus(`已触发:1000 → ${target}`, 'ok');
log(`SEAT 已触发:1000 → ${target}`, 's');
}catch(e){
setStatus(`SEAT 失败:${e.message||e}`, 'err');
log(`SEAT 失败:${e.message||e}`, 'e');
}
enableOps(true);
});
async function leaveWorkspace(accountId){
await ensureToken();
if(!userId) userId = decodeJwtUserId(token);
if(!userId) throw new Error('未获取到 userId');
const u = `/backend-api/accounts/${encodeURIComponent(accountId)}/users/${encodeURIComponent(userId)}`;
const init = {
method:'DELETE',
credentials:'include',
headers:{
'accept':'*/*',
'authorization':'Bearer '+token,
'chatgpt-account-id': accountId
}
};
return fetchJsonWithRetry(u, init, {retry:1, allowEmpty:true});
}
btnLeaveCur.addEventListener('click', async ()=>{
if(!selected){ setStatus('请选择工作区', 'err'); return; }
const name = selected.name || selected.account_id;
const ok = window.confirm(`确定退出工作区?\n${name}\n\n将对自己执行 DELETE /accounts/{accountId}/users/{userId}`);
if(!ok) return;
setStatus('退出中...', null, true);
enableOps(false);
try{
await leaveWorkspace(selected.account_id);
setStatus('退出请求已发送(建议刷新页面)', 'ok');
log(`已退出:${name}`, 's');
await initWorkspaceList();
}catch(e){
setStatus(`退出失败:${e.message||e}`, 'err');
log(`退出失败:${e.message||e}`, 'e');
}
enableOps(true);
});
async function createK12Workspace(name){
const u = `/backend-api/accounts/create_workspace_without_subscription`;
const init = {
method:'POST',
credentials:'include',
headers:{
'accept':'*/*',
'content-type':'application/json',
'authorization':'Bearer '+token
},
body: JSON.stringify({ workspace_name:name, agreed_to_dpa:true, plan_type:'k12' })
};
return fetchJsonWithRetry(u, init, {retry:1});
}
async function runK12Batch(){
if(location.pathname !== '/k12-create-workspace'){
setStatus('仅 /k12-create-workspace 可用', 'err');
return;
}
const count = parseInt((inpK12Count.value||'').trim(), 10);
const prefix = (inpK12Prefix.value||'').trim();
store.upd({ k12Count: String(count||''), k12Prefix: prefix });
if(!Number.isFinite(count) || count<=0){ setStatus('K12 数量无效', 'err'); return; }
if(!prefix){ setStatus('K12 名称前缀不能为空', 'err'); return; }
setStatus('K12 批量创建中...', null, true);
enableOps(false);
try{
await ensureToken();
const names = Array.from({length:count}, (_,i)=> `${prefix}${i+1}`);
let ok=0, fail=0;
const concurrency = 3;
let idx = 0;
log(`K12 批量开始:${count} 个`, 'i');
async function worker(){
while(idx < names.length){
const i = idx++;
const name = names[i];
try{
await createK12Workspace(name);
ok++;
log(`K12 OK: ${name}`, 's');
}catch(e){
fail++;
log(`K12 FAIL: ${name} (${e.message||e})`, 'e');
}
}
}
const workers = Array.from({length:Math.min(concurrency, names.length)}, ()=>worker());
await Promise.all(workers);
setStatus(fail ? `K12 完成:成功 ${ok},失败 ${fail}` : `K12 完成:全部成功(${ok})`, fail ? 'err' : 'ok');
log(`K12 完成:成功 ${ok},失败 ${fail}`, fail ? 'e' : 's');
}catch(e){
setStatus(`K12 失败:${e.message||e}`, 'err');
log(`K12 失败:${e.message||e}`, 'e');
}
enableOps(true);
}
btnK12Run.addEventListener('click', runK12Batch);
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); });
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 applyK12Enabled(){
const ok = location.pathname === '/k12-create-workspace';
btnK12Run.disabled = !ok;
lblK12.textContent = ok ? 'K12 批量创建' : 'K12 批量创建(仅 /k12-create-workspace 可用)';
}
function hookHistory(){
const _ps = history.pushState;
const _rs = history.replaceState;
history.pushState = function(){
const r = _ps.apply(this, arguments);
window.dispatchEvent(new Event('wdd:locationchange'));
return r;
};
history.replaceState = function(){
const r = _rs.apply(this, arguments);
window.dispatchEvent(new Event('wdd:locationchange'));
return r;
};
window.addEventListener('popstate', ()=>window.dispatchEvent(new Event('wdd:locationchange')));
window.addEventListener('wdd:locationchange', ()=>{
applyK12Enabled();
updateTeamFloatVisibility();
});
}
function bindTeamInputs(){
const onChange = ()=>teamConfig();
inpTeamName.addEventListener('change', onChange);
selTeamInterval.addEventListener('change', onChange);
inpTeamSeats.addEventListener('change', onChange);
inpTeamCountry.addEventListener('change', onChange);
inpTeamCurrency.addEventListener('change', onChange);
inpTeamPromo.addEventListener('change', onChange);
}
(function boot(){
enableOps(false);
hydrate();
hookHistory();
bindTeamInputs();
applyK12Enabled();
updateTeamFloatVisibility();
initWorkspaceList();
setStatus('就绪', null);
log('面板已加载', 'i');
})();
})();