← 返回首页
教程2026 · 07 · 2431 分钟阅读

【焚决】ChatGPT K12 空间母号换绑焚决

原文:【焚决】 gpt k12 空间母号换绑焚决 作者:whistle · 发布于 2026-07-24

换绑方法

我这边换绑老哥不做这生意了把焚决丢给我了,想着造福各位还是直接开源了(我这只是一种换绑的方法)~

换绑其实很简单,原理是脚本申请只会检查你的母号的邮箱域名:

  1. 登录你的母号
  2. 注册你的母号同域名的子号(使用手机号注册然后走 codex 登录逻辑绑定母号域名邮箱)
  3. 子号通过申请脚本加入工作区
  4. 母号批准为 owner(如果出现批准不了或者点邮件没用多试几个号)
  5. 进入 2-3 个子号,以防 openai 杀 free 号(都设置为 owner)
  6. 子号给母号移出工作区
  7. 母号退出了工作区才有修改邮箱的选项
  8. 修改为要改的其他域名的邮箱
  9. 重新登录
  10. 母号使用申请脚本进入工作区
  11. 子号批准母号进入工作区
  12. 换绑完成

焚决 — ChatGPT Workspace Join Request v4.0

Tampermonkey 用户脚本,子号自动从 /api/auth/session 获取当前登录账号 AT,向母号 workspace 发送加入申请(request)或接受邀请(accept)。只需 workspace ID,UI 可编辑保存。

// ==UserScript==
// @name         ChatGPT Workspace Join Request
// @namespace    https://chatgpt.com/
// @version      4.0.0
// @description  子号自动从 /api/auth/session 获取当前登录账号 AT,向母号 workspace 发送加入申请(request)或接受邀请(accept)。只需 workspace ID,UI 可编辑保存。
// @author       whistle
// @match        https://chatgpt.com/*
// @run-at       document-idle
// @grant        none
// ==/UserScript==

(function () {
"use strict";

// ===================== 默认配置 =====================
const DEFAULTS = {
  workspaceIds: "你的工作区id",
  intervalMs: 1500,
  maxRetries: 3,
  retryBackoffMs: 5000,
  sessionPollMs: 20000,
  panelWidth: 400,
};

const STORE_KEY = "jr_config_v4";
function loadConfig() {
  let saved = {};
  try { saved = JSON.parse(localStorage.getItem(STORE_KEY) || "{}"); } catch (_) {}
  return Object.assign({}, DEFAULTS, saved);
}
function saveConfig(cfg) {
  try { localStorage.setItem(STORE_KEY, JSON.stringify(cfg)); } catch (_) {}
}

let CONFIG = loadConfig();

// ---------- 状态 ----------
const STATE = {
  at: "",
  session: null,
  deviceId: crypto.randomUUID(),
  autoRan: false,
  running: false,
};

// ---------- /api/auth/session ----------
async function fetchSession() {
  const res = await fetch("/api/auth/session", {
    headers: { accept: "*/*" },
    credentials: "include",
  });
  if (!res.ok) throw new Error(`session HTTP ${res.status}`);
  return res.json();
}

function decodeJwt(at) {
  try {
    const p = at.split(".")[1];
    const j = JSON.parse(atob(p.replace(/-/g, "+").replace(/_/g, "/")));
    const auth = j["https://api.openai.com/auth"] || {};
    const prof = j["https://api.openai.com/profile"] || {};
    return {
      account_id: auth.chatgpt_account_id || "",
      email: prof.email || "",
      plan_type: auth.chatgpt_plan_type || "",
      exp: j.exp || 0,
    };
  } catch (_) { return {}; }
}

function fmtExp(exp) {
  if (!exp) return "?";
  const min = Math.round((exp * 1000 - Date.now()) / 60000);
  if (min > 60) return `剩余 ${Math.round(min/60)} 小时`;
  return `剩余 ${min} 分钟`;
}

async function refreshSession() {
  try {
    const s = await fetchSession();
    const at = s.accessToken || "";
    if (at && at !== STATE.at) {
      STATE.at = at;
      STATE.session = s;
      const info = decodeJwt(at);
      log(`子号 AT 已更新: ${info.email || "?"}`, "ok");
      updateUserBar(info, "ok");
      onATReady();
    } else if (!at) {
      updateUserBar(null, "warn");
    }
  } catch (e) {
    log(`session 获取失败: ${e.message}`, "warn");
    updateUserBar(null, "err");
  }
}

// ---------- 发请求 ----------
async function sendOne(wsId, route, attempt) {
  attempt = attempt || 0;
  const url = `/backend-api/accounts/${wsId}/invites/${route}`;
  const headers = {
    accept: "*/*",
    authorization: `Bearer ${STATE.at}`,
    "content-type": "application/json",
    "oai-device-id": STATE.deviceId,
    "oai-language": navigator.language || "en-US",
  };
  log(`→ POST /accounts/${wsId.slice(0,8)}/invites/${route} (第 ${attempt+1} 次)`);
  try {
    const res = await fetch(url, {
      method: "POST", headers, body: "", mode: "cors", credentials: "include",
    });
    const text = await res.text();
    if (res.ok) {
      log(`✓ ${wsId.slice(0,8)} HTTP ${res.status}: ${text}`, "ok");
      return true;
    }
    log(`✗ ${wsId.slice(0,8)} HTTP ${res.status}: ${text.slice(0,180)}`, "warn");
    if (res.status === 401 || res.status === 403) {
      log("子号 AT 失效,刷新 session...", "warn");
      STATE.at = "";
      await refreshSession();
      if (attempt < CONFIG.maxRetries) {
        await sleep(2000);
        return sendOne(wsId, route, attempt + 1);
      }
      return false;
    }
    if (attempt < CONFIG.maxRetries) {
      await sleep(CONFIG.retryBackoffMs * (attempt + 1));
      return sendOne(wsId, route, attempt + 1);
    }
    return false;
  } catch (e) {
    log(`网络错误: ${e.message}`, "err");
    if (attempt < CONFIG.maxRetries) {
      await sleep(CONFIG.retryBackoffMs);
      return sendOne(wsId, route, attempt + 1);
    }
    return false;
  }
}

function parseWorkspaceIds() {
  return CONFIG.workspaceIds.split(/[\n,]+/).map(s => s.trim()).filter(Boolean);
}

async function runAll(route) {
  if (STATE.running) { log("正在运行中,请稍候", "warn"); return; }
  if (!STATE.at) {
    log("无可用 AT,刷新 session...", "warn");
    await refreshSession();
    if (!STATE.at) { log("仍未取到 AT,请先登录 chatgpt.com", "err"); return; }
  }
  const ids = parseWorkspaceIds();
  if (!ids.length) { log("未配置 workspace ID", "err"); return; }

  STATE.running = true;
  setBtns(false);
  log(`开始处理 ${ids.length} 个 workspace(${route})`, "info");
  let ok = 0;
  for (const ws of ids) {
    const r = await sendOne(ws, route);
    if (r) ok++;
    if (ids.length > 1) await sleep(CONFIG.intervalMs);
  }
  log(`完成:成功 ${ok}/${ids.length}`, ok === ids.length ? "ok" : "warn");
  STATE.running = false;
  setBtns(true);
}

function onATReady() {
  if (!STATE.autoRan) {
    STATE.autoRan = true;
    runAll("request");
  }
}

function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }

// ---------- UI ----------
let panelBody, userBarEl, reqBtnEl, accBtnEl, wsInputEl, saveBtnEl, dirty = false;

function setBtns(enabled) {
  [reqBtnEl, accBtnEl].forEach(b => {
    if (b) { b.disabled = !enabled; b.style.opacity = enabled ? "1" : "0.5"; }
  });
}

function updateUserBar(info, status) {
  if (!userBarEl) return;
  const c = { ok: "#2f855a", warn: "#b7791f", err: "#c53030" };
  if (info && info.email) {
    userBarEl.innerHTML =
      `<span style="color:${c.ok}">●</span> <b>${info.email}</b> · ${info.plan_type||"?"} · ` +
      `<code style="background:#edf2f7;padding:1px 4px;border-radius:3px">${(info.account_id||"").slice(0,8)}</code> · ` +
      `<span style="color:#718096">${fmtExp(info.exp)}</span>`;
  } else {
    const msg = status === "err" ? "session 获取失败,请确认已登录" : "未检测到 AT,等待登录...";
    userBarEl.innerHTML = `<span style="color:${c[status]||c.warn}">●</span> ${msg}`;
  }
}

function markDirty() {
  dirty = true;
  if (saveBtnEl) {
    saveBtnEl.textContent = "保存 *";
    saveBtnEl.style.background = "#d69e2e";
    saveBtnEl.style.color = "#fff";
  }
}

function markClean() {
  dirty = false;
  if (saveBtnEl) {
    saveBtnEl.textContent = "已保存";
    saveBtnEl.style.background = "#38a169";
    saveBtnEl.style.color = "#fff";
  }
  setTimeout(() => {
    if (!dirty && saveBtnEl) {
      saveBtnEl.textContent = "保存";
      saveBtnEl.style.background = "#edf2f7";
      saveBtnEl.style.color = "#4a5568";
    }
  }, 1500);
}

function buildPanel() {
  const css = `
      .jr-panel{position:fixed;top:14px;right:14px;width:${CONFIG.panelWidth}px;
         background:#fff;border:1px solid #e2e8f0;border-radius:14px;
         box-shadow:0 12px 32px rgba(0,0,0,.16);z-index:99999;
         font:13px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#1a202c;overflow:hidden}
      .jr-head{padding:11px 16px;background:linear-gradient(135deg,#3182ce,#2b6cb0);color:#fff;
         display:flex;justify-content:space-between;align-items:center}
      .jr-title{font-weight:600;font-size:13px;letter-spacing:.3px;display:flex;align-items:center;gap:6px}
      .jr-title svg{width:16px;height:16px;fill:#fff}
      .jr-ver{font-size:11px;opacity:.85;background:rgba(255,255,255,.2);padding:1px 6px;border-radius:8px}
      .jr-sub{padding:9px 16px;background:#f7fafc;border-bottom:1px solid #edf2f7;font-size:12px;color:#4a5568;min-height:22px}
      .jr-sec{padding:12px 16px;border-bottom:1px solid #edf2f7}
      .jr-label{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:#718096;
         margin-bottom:6px;font-weight:600;letter-spacing:.4px;text-transform:uppercase}
      .jr-label .jr-count{font-size:10px;color:#a0aec0;font-weight:400;text-transform:none;letter-spacing:0}
      .jr-ta{width:100%;box-sizing:border-box;border:1px solid #e2e8f0;border-radius:8px;
         padding:8px 10px;font:12px/1.5 monospace;color:#2d3748;background:#fff;transition:.15s;min-height:64px;resize:vertical}
      .jr-ta:focus{outline:0;border-color:#3182ce;box-shadow:0 0 0 3px rgba(49,130,206,.12)}
      .jr-save-row{display:flex;justify-content:flex-end;margin-top:8px}
      .jr-body{padding:10px 16px;max-height:36vh;overflow:auto}
      .jr-foot{padding:10px 16px;border-top:1px solid #edf2f7;display:flex;gap:8px;justify-content:flex-end;flex-wrap:wrap}
      .jr-line{padding:3px 0;word-break:break-all;border-bottom:1px dashed #f1f5f9}
      .jr-line:last-child{border-bottom:0}
      .jr-info{color:#2b6cb0}.jr-ok{color:#2f855a}.jr-warn{color:#b7791f}.jr-err{color:#c53030}
      .jr-btn{cursor:pointer;border:0;border-radius:7px;padding:7px 14px;font-size:12px;font-weight:500;transition:.15s}
      .jr-btn-primary{background:#3182ce;color:#fff}.jr-btn-primary:hover{background:#2b6cb0}
      .jr-btn-green{background:#38a169;color:#fff}.jr-btn-green:hover{background:#2f855a}
      .jr-btn-ghost{background:#edf2f7;color:#4a5568}.jr-btn-ghost:hover{background:#e2e8f0}
      .jr-btn:disabled{cursor:not-allowed;opacity:.5}
      .jr-hint{font-size:11px;color:#a0aec0;padding:6px 0 0 0;line-height:1.5}
    `;
  const style = document.createElement("style");
  style.textContent = css;
  document.head.appendChild(style);

  const p = document.createElement("div");
  p.className = "jr-panel";
  p.innerHTML = `
    <div class="jr-head">
      <span class="jr-title">
        <svg viewBox="0 0 24 24"><path d="M15 4l6 6-10 10H5v-6L15 4zm-1 1L7 12l5 5 7-7-5-5z"/></svg>
        Workspace Join Request
      </span>
      <span class="jr-ver">v4.0</span>
    </div>
    <div class="jr-sub" id="jr-user">未检测到 AT,等待登录...</div>

    <div class="jr-sec">
      <label class="jr-label">
        <span>母号 Workspace ID</span>
        <span class="jr-count" id="jr-count">0 个</span>
      </label>
      <textarea class="jr-ta" id="jr-ws" placeholder="一行一个 UUID&#10;acfb4e38-524c-4dc8-b4cf-fb3d0ce28b25"></textarea>
      <div class="jr-save-row">
        <button class="jr-btn jr-btn-ghost" id="jr-save" style="padding:5px 14px">保存</button>
      </div>
      <div class="jr-hint">只需子号 AT + workspace ID。request = 主动申请加入;accept = 接受已有邀请。</div>
    </div>

    <div class="jr-body" id="jr-body"></div>

    <div class="jr-foot">
      <button class="jr-btn jr-btn-ghost" id="jr-refresh">刷新 AT</button>
      <button class="jr-btn jr-btn-green" id="jr-accept">Accept</button>
      <button class="jr-btn jr-btn-primary" id="jr-run">Request</button>
    </div>
  `;
  document.body.appendChild(p);

  panelBody = p.querySelector("#jr-body");
  userBarEl = p.querySelector("#jr-user");
  reqBtnEl = p.querySelector("#jr-run");
  accBtnEl = p.querySelector("#jr-accept");
  wsInputEl = p.querySelector("#jr-ws");
  saveBtnEl = p.querySelector("#jr-save");
  const countEl = p.querySelector("#jr-count");

  wsInputEl.value = CONFIG.workspaceIds;
  const updateCount = () => {
    const n = wsInputEl.value.split(/[\n,]+/).map(s=>s.trim()).filter(Boolean).length;
    countEl.textContent = `${n} 个`;
  };
  updateCount();

  wsInputEl.addEventListener("input", () => {
    markDirty();
    updateCount();
  });

  saveBtnEl.addEventListener("click", () => {
    CONFIG.workspaceIds = wsInputEl.value;
    saveConfig(CONFIG);
    markClean();
    log("workspace 已保存", "ok");
  });

  reqBtnEl.addEventListener("click", () => runAll("request"));
  accBtnEl.addEventListener("click", () => runAll("accept"));
  p.querySelector("#jr-refresh").addEventListener("click", async () => {
    log("手动刷新 session...", "info");
    await refreshSession();
  });
}

function log(msg, level) {
  const styles = { info: "jr-info", ok: "jr-ok", warn: "jr-warn", err: "jr-err" };
  const time = new Date().toLocaleTimeString();
  console.log("%c[JoinReq]", "color:#3182ce;font-weight:bold", msg);
  if (panelBody) {
    const line = document.createElement("div");
    line.className = `jr-line ${styles[level] || "jr-info"}`;
    line.textContent = `[${time}] ${msg}`;
    panelBody.appendChild(line);
    panelBody.scrollTop = panelBody.scrollHeight;
  }
}

// ---------- 启动 ----------
function boot() {
  buildPanel();
  log("脚本已加载 v4.0", "info");
  log("只需子号 AT + workspace ID", "info");
  log("request = 主动申请 · accept = 接受邀请", "info");
  refreshSession();
  setInterval(refreshSession, CONFIG.sessionPollMs);
  window.addEventListener("focus", () => { if (!STATE.running) refreshSession(); });
}

if (document.readyState === "loading") {
  document.addEventListener("DOMContentLoaded", boot);
} else {
  boot();
}
})();
原文出处www.nodeseek.com