k12半自动油猴脚本

基于之前佬的油猴脚本二开
增加支持多个 Workspace ID
增加直接复制session,因为我用的codex2api
增加下载json,方便批量导入。
非大批量注册账户自用足够了

// ==UserScript==
// @name         ChatGPT K12 Invite Helper
// @namespace    http://tampermonkey.net/
// @version      0.4.0
// @description  Visual helper for requesting and switching to a ChatGPT K12 workspace account (stop on first success)
// @match        https://chatgpt.com/*
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  const PANEL_ID = 'chatgpt-k12-helper-panel';
  const BODY_ID = 'chatgpt-k12-helper-body';
  const STATUS_ID = 'chatgpt-k12-helper-status';
  const RESULT_ID = 'chatgpt-k12-helper-result';
  const ACCOUNT_INPUT_ID = 'chatgpt-k12-helper-account-input';
  const RUN_BUTTON_ID = 'chatgpt-k12-helper-run';
  const COPY_BUTTON_ID = 'chatgpt-k12-helper-copy';
  const COPY_SESSION_BUTTON_ID = 'chatgpt-k12-helper-copy-session';
  const DOWNLOAD_BUTTON_ID = 'chatgpt-k12-helper-download';
  const CLEAR_BUTTON_ID = 'chatgpt-k12-helper-clear';
  const TOGGLE_BUTTON_ID = 'chatgpt-k12-helper-toggle';
  const STORAGE_KEY = 'chatgpt-k12-helper-account-ids';

  let isRunning = false;
  let lastResultText = '';
  let lastCodexJson = null; // Store the last successful codex JSON
  let processedResults = [];

  function formatResult(value) {
    if (typeof value === 'string') {
      return value;
    }

    try {
      return JSON.stringify(value, null, 2);
    } catch (error) {
      return String(value);
    }
  }

  function setStatus(type, text) {
    const statusEl = document.getElementById(STATUS_ID);
    if (!statusEl) {
      return;
    }

    statusEl.textContent = text;
    statusEl.setAttribute('data-state', type);
  }

  function setResult(value) {
    lastResultText = formatResult(value);

    const resultEl = document.getElementById(RESULT_ID);
    if (!resultEl) {
      return;
    }

    resultEl.textContent = lastResultText || '暂无输出。';

    // Update buttons state
    updateButtonsState();
  }

  function appendResult(text) {
    const resultEl = document.getElementById(RESULT_ID);
    if (!resultEl) {
      return;
    }

    const currentText = resultEl.textContent || '';
    const newText = currentText + '\n' + text;
    resultEl.textContent = newText;
    lastResultText = newText;

    // Auto scroll to bottom
    resultEl.scrollTop = resultEl.scrollHeight;
  }

  function updateButtonsState() {
    const copyButton = document.getElementById(COPY_BUTTON_ID);
    const copySessionButton = document.getElementById(COPY_SESSION_BUTTON_ID);
    const downloadButton = document.getElementById(DOWNLOAD_BUTTON_ID);

    const hasResult = !!lastResultText && lastResultText !== '暂无输出。';
    const hasCodex = !!lastCodexJson;

    if (copyButton) copyButton.disabled = !hasResult || isRunning;
    if (copySessionButton) copySessionButton.disabled = !hasCodex || isRunning;
    if (downloadButton) downloadButton.disabled = !hasCodex || isRunning;
  }

  function setRunningState(running) {
    isRunning = running;

    const runButton = document.getElementById(RUN_BUTTON_ID);
    const input = document.getElementById(ACCOUNT_INPUT_ID);

    if (runButton) {
      runButton.disabled = running;
      runButton.textContent = running ? '执行中...' : '批量执行';
    }

    if (input) {
      input.disabled = running;
    }

    updateButtonsState();
  }

  function extractAccountIds(text) {
    const lines = text.split('\n').filter(line => line.trim());
    const ids = [];
    const uuidRegex = /([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i;

    for (const line of lines) {
      const match = line.match(uuidRegex);
      if (match) {
        ids.push(match[1].toLowerCase());
      }
    }

    return ids;
  }

  function getAccountInputValue() {
    const input = document.getElementById(ACCOUNT_INPUT_ID);
    return input ? input.value : '';
  }

  function saveAccountInputValue(value) {
    try {
      localStorage.setItem(STORAGE_KEY, value);
    } catch (error) {
      console.warn('保存 Account IDs 失败:', error);
    }
  }

  function getSavedAccountInputValue() {
    try {
      return localStorage.getItem(STORAGE_KEY) || '';
    } catch (error) {
      return '';
    }
  }

  async function safeReadResponse(response) {
    const contentType = response.headers.get('content-type') || '';

    if (contentType.includes('application/json')) {
      return response.json();
    }

    return response.text();
  }

  function formatCstDate(value) {
    const date = value ? new Date(value) : new Date();
    const cstDate = new Date(date.getTime() + 8 * 60 * 60 * 1000);
    const pad = (number) => String(number).padStart(2, '0');

    return `${cstDate.getUTCFullYear()}-${pad(cstDate.getUTCMonth() + 1)}-${pad(cstDate.getUTCDate())} ${pad(cstDate.getUTCHours())}:${pad(cstDate.getUTCMinutes())}:${pad(cstDate.getUTCSeconds())} +0800`;
  }

  function buildCodexJson(accountId, accessToken, sessionData) {
    const codex = {
      type: 'codex',
      email: sessionData?.user?.email || '',
      expired: formatCstDate(sessionData?.expires),
      id_token: accessToken,
      account_id: accountId,
      disabled: false,
      access_token: accessToken,
      session_token: sessionData?.sessionToken || '',
      last_refresh: formatCstDate(),
      refresh_token: '',
    };
    return codex;
  }

  function setChatGptAccountCookie(accountId) {
    const encoded = encodeURIComponent(accountId);
    document.cookie = `_account=${encoded}; path=/; domain=.chatgpt.com; SameSite=Lax; Secure`;
    document.cookie = `_account=${encoded}; path=/; SameSite=Lax; Secure`;
  }

  async function tryInviteAccount(accountId, accessToken) {
    try {
      const inviteUrl = `https://chatgpt.com/backend-api/accounts/${accountId}/invites/request`;
      const inviteRes = await fetch(inviteUrl, {
        method: 'POST',
        credentials: 'include',
        mode: 'cors',
        referrer: 'https://chatgpt.com/k12-verification',
        referrerPolicy: 'strict-origin-when-cross-origin',
        headers: {
          accept: '*/*',
          'accept-language': 'en-US,en;q=0.9',
          authorization: `Bearer ${accessToken}`,
          'cache-control': 'no-cache',
          'oai-language': 'en-US',
          pragma: 'no-cache',
        },
        body: null,
      });
      const inviteResult = await safeReadResponse(inviteRes);

      if (!inviteRes.ok) {
        throw new Error(`邀请请求失败: ${inviteRes.status} ${inviteRes.statusText}\n${formatResult(inviteResult)}`);
      }

      return { success: true, data: inviteResult };
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      return { success: false, error: message };
    }
  }

  async function switchToAccount(accountId, sessionData, accessToken) {
    try {
      // Switch workspace
      setChatGptAccountCookie(accountId);

      const switchedSessionRes = await fetch('https://chatgpt.com/api/auth/session', {
        method: 'GET',
        credentials: 'include',
        headers: {
          accept: 'application/json',
        },
      });
      const switchedSessionData = await safeReadResponse(switchedSessionRes);

      const outputSessionData = switchedSessionData?.accessToken ? switchedSessionData : sessionData;
      const outputAccessToken = outputSessionData?.accessToken || accessToken;
      const codexJson = buildCodexJson(accountId, outputAccessToken, outputSessionData);
      const switched = switchedSessionData?.account?.id === accountId;

      return {
        success: true,
        switched,
        codexJson,
        sessionData: outputSessionData,
        accessToken: outputAccessToken
      };
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      return { success: false, error: message };
    }
  }

  async function runFlow() {
    if (isRunning) {
      return;
    }

    const rawAccountValue = getAccountInputValue();
    const accountIds = extractAccountIds(rawAccountValue);

    if (accountIds.length === 0) {
      setStatus('error', '请至少输入一个有效的 Account ID');
      return;
    }

    if (rawAccountValue.trim()) {
      saveAccountInputValue(rawAccountValue.trim());
    }

    setRunningState(true);
    processedResults = [];
    lastCodexJson = null;
    setStatus('running', '正在获取 ChatGPT session...');
    setResult('');

    try {
      // Get session first
      const sessionRes = await fetch('https://chatgpt.com/api/auth/session', {
        method: 'GET',
        credentials: 'include',
        headers: {
          accept: 'application/json',
        },
      });
      const sessionData = await safeReadResponse(sessionRes);

      if (!sessionRes.ok) {
        throw new Error(`获取 session 失败: ${sessionRes.status} ${sessionRes.statusText}\n${formatResult(sessionData)}`);
      }

      const accessToken = sessionData?.accessToken;
      if (!accessToken) {
        throw new Error('未能获取到 accessToken,请确认当前浏览器已登录 chatgpt.com。');
      }

      // Process accounts one by one until success
      let successAccountId = null;
      let successSwitchResult = null;
      let failedAccounts = [];
      const totalCount = accountIds.length;

      setResult(`开始尝试 ${totalCount} 个账号,找到第一个可用的...\n`);

      for (let i = 0; i < accountIds.length; i++) {
        const accountId = accountIds[i];

        // Try to invite
        setStatus('running', `正在尝试第 ${i + 1}/${totalCount} 个: ${accountId}...`);
        appendResult(`[${i + 1}/${totalCount}] 正在尝试: ${accountId}`);

        const inviteResult = await tryInviteAccount(accountId, accessToken);

        if (inviteResult.success) {
          // Invite successful, now switch
          appendResult(`[${i + 1}/${totalCount}] ✅ 邀请成功!正在切换...`);
          setStatus('running', `邀请成功,正在切换到 ${accountId}...`);

          const switchResult = await switchToAccount(accountId, sessionData, accessToken);

          if (switchResult.success) {
            // Success! Stop here
            successAccountId = accountId;
            successSwitchResult = switchResult;
            lastCodexJson = switchResult.codexJson;
            appendResult(`[${i + 1}/${totalCount}] 🎉 成功切换到 ${accountId}`);
            break; // Exit loop on first success
          } else {
            // Switch failed, but invite succeeded
            appendResult(`[${i + 1}/${totalCount}] ⚠️ 邀请成功但切换失败: ${switchResult.error}`);
            failedAccounts.push({ accountId, error: switchResult.error, stage: 'switch' });
          }
        } else {
          // Invite failed
          appendResult(`[${i + 1}/${totalCount}] ❌ 邀请失败: ${inviteResult.error}`);
          failedAccounts.push({ accountId, error: inviteResult.error, stage: 'invite' });
        }
      }

      // Handle results
      if (successAccountId && successSwitchResult) {
        // Found a successful account
        const successMessage = `\n===== 成功!=====\n找到可用账号: ${successAccountId}`;
        appendResult(successMessage);

        setResult(successSwitchResult.codexJson);
        setStatus('success', `✅ 成功切换到 ${successAccountId}`);
        console.log('ChatGPT K12 Helper - 成功切换到账号:', successAccountId);
        console.log('ChatGPT K12 Helper - Codex JSON:', successSwitchResult.codexJson);

        // Update buttons
        updateButtonsState();
      } else {
        // All failed
        const failureMessage = `\n===== 全部失败 =====\n共尝试了 ${totalCount} 个账号,均未成功`;
        appendResult(failureMessage);

        setStatus('error', `❌ 所有 ${totalCount} 个账号均失败`);
        setResult({
          success: false,
          error: '所有账号处理失败',
          totalAttempted: totalCount,
          failedAccounts: failedAccounts.map(f => ({ id: f.accountId, error: f.error }))
        });
        console.error('ChatGPT K12 Helper - 所有账号均失败:', failedAccounts);
      }

    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      setStatus('error', '执行失败');
      setResult({ success: false, error: message });
      console.error('ChatGPT K12 Helper 执行失败:', error);
    } finally {
      setRunningState(false);
    }
  }

  async function copyText(text, emptyMessage, successMessage) {
    if (!text) {
      setStatus('idle', emptyMessage);
      return;
    }

    try {
      await navigator.clipboard.writeText(text);
      setStatus('success', successMessage);
      setTimeout(() => {
        if (!isRunning) {
          setStatus('idle', '就绪');
        }
      }, 2000);
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      setStatus('error', '复制失败');
      console.error('复制失败:', message);
    }
  }

  function copyResult() {
    return copyText(
      lastResultText,
      '暂无可复制结果',
      '✅ 结果已复制到剪贴板'
    );
  }

  function copySessionToken() {
    if (!lastCodexJson) {
      setStatus('error', '没有可用的 session_token');
      return;
    }

    const sessionToken = lastCodexJson.session_token || '';
    if (!sessionToken) {
      setStatus('error', 'session_token 为空');
      return;
    }

    return copyText(
      sessionToken,
      'session_token 为空',
      '✅ session_token 已复制到剪贴板'
    );
  }

  function downloadJson() {
    if (!lastCodexJson) {
      setStatus('error', '没有可下载的数据');
      return;
    }

    try {
      const email = lastCodexJson.email || 'unknown';
      const fileName = `k12-${email}.json`;
      const jsonString = JSON.stringify(lastCodexJson, null, 2);
      const blob = new Blob([jsonString], { type: 'application/json' });
      const url = URL.createObjectURL(blob);

      const link = document.createElement('a');
      link.href = url;
      link.download = fileName;
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
      URL.revokeObjectURL(url);

      setStatus('success', `✅ 已下载: ${fileName}`);
      setTimeout(() => {
        if (!isRunning) {
          setStatus('idle', '就绪');
        }
      }, 2000);
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      setStatus('error', `下载失败: ${message}`);
      console.error('下载失败:', error);
    }
  }

  function clearResult() {
    lastResultText = '';
    lastCodexJson = null;
    processedResults = [];
    setResult('');
    setStatus('idle', '已清空结果');
    setRunningState(false);
  }

  function togglePanelBody() {
    const bodyEl = document.getElementById(BODY_ID);
    const toggleButton = document.getElementById(TOGGLE_BUTTON_ID);
    if (!bodyEl || !toggleButton) {
      return;
    }

    const isHidden = bodyEl.hidden;
    bodyEl.hidden = !isHidden;
    toggleButton.textContent = isHidden ? '收起' : '展开';
  }

  function createStyles() {
    const style = document.createElement('style');
    style.textContent = `
      #${PANEL_ID} {
        position: fixed;
        top: 50%;
        right: 20px;
        transform: translateY(-50%);
        z-index: 999999;
        width: 480px;
        max-height: 90vh;
        color: #e5e7eb;
        background: rgba(15, 23, 42, 0.96);
        border: 1px solid rgba(148, 163, 184, 0.25);
        border-radius: 16px;
        box-shadow: 0 18px 50px rgba(0, 0, 0, 0.35);
        backdrop-filter: blur(12px);
        font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      }

      #${PANEL_ID} * {
        box-sizing: border-box;
      }

      #${PANEL_ID} button,
      #${PANEL_ID} textarea {
        font-family: inherit;
      }

      #${PANEL_ID} button {
        border: 0;
        border-radius: 10px;
        padding: 10px 14px;
        font-size: 13px;
        font-weight: 600;
        cursor: pointer;
        transition: opacity 0.2s ease, transform 0.2s ease, background 0.2s ease;
      }

      #${PANEL_ID} button:hover:not(:disabled) {
        transform: translateY(-1px);
      }

      #${PANEL_ID} button:disabled,
      #${PANEL_ID} textarea:disabled {
        cursor: not-allowed;
        opacity: 0.55;
      }

      #${PANEL_ID} .helper-header {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 12px;
        padding: 14px 16px;
        border-bottom: 1px solid rgba(148, 163, 184, 0.18);
        flex-shrink: 0;
      }

      #${PANEL_ID} .helper-title {
        font-size: 15px;
        font-weight: 700;
      }

      #${PANEL_ID} .helper-status {
        display: inline-flex;
        align-items: center;
        gap: 8px;
        font-size: 12px;
        color: #cbd5e1;
        margin-top: 6px;
      }

      #${PANEL_ID} .helper-status::before {
        content: '';
        width: 8px;
        height: 8px;
        border-radius: 999px;
        background: #94a3b8;
      }

      #${STATUS_ID}[data-state="idle"]::before {
        background: #94a3b8;
      }

      #${STATUS_ID}[data-state="running"]::before {
        background: #f59e0b;
        animation: pulse 1s infinite;
      }

      @keyframes pulse {
        0%, 100% { opacity: 1; }
        50% { opacity: 0.3; }
      }

      #${STATUS_ID}[data-state="success"]::before {
        background: #22c55e;
      }

      #${STATUS_ID}[data-state="error"]::before {
        background: #ef4444;
      }

      #${BODY_ID} {
        padding: 16px;
        overflow-y: auto;
        max-height: calc(90vh - 80px);
      }

      #${PANEL_ID} .helper-field {
        margin-bottom: 14px;
      }

      #${PANEL_ID} .helper-label {
        display: block;
        margin-bottom: 7px;
        font-size: 12px;
        color: #cbd5e1;
        font-weight: 600;
      }

      #${ACCOUNT_INPUT_ID} {
        width: 100%;
        min-height: 80px;
        border: 1px solid rgba(148, 163, 184, 0.22);
        border-radius: 10px;
        outline: none;
        padding: 10px 12px;
        color: #e5e7eb;
        background: rgba(2, 6, 23, 0.72);
        font-size: 13px;
        resize: vertical;
        font-family: 'Courier New', monospace;
        line-height: 1.6;
      }

      #${ACCOUNT_INPUT_ID}:focus {
        border-color: rgba(96, 165, 250, 0.75);
        box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.18);
      }

      #${PANEL_ID} .helper-actions {
        display: grid;
        grid-template-columns: repeat(4, 1fr);
        gap: 8px;
        margin-bottom: 14px;
      }

      #${RUN_BUTTON_ID} {
        background: linear-gradient(135deg, #2563eb, #3b82f6);
        color: #fff;
        grid-column: span 1;
      }

      #${COPY_BUTTON_ID} {
        background: rgba(59, 130, 246, 0.14);
        color: #bfdbfe;
        grid-column: span 1;
      }

      #${COPY_SESSION_BUTTON_ID} {
        background: rgba(34, 197, 94, 0.14);
        color: #86efac;
        grid-column: span 1;
      }

      #${DOWNLOAD_BUTTON_ID} {
        background: rgba(251, 146, 60, 0.14);
        color: #fdba74;
        grid-column: span 1;
      }

      #${CLEAR_BUTTON_ID} {
        background: rgba(148, 163, 184, 0.14);
        color: #e2e8f0;
        grid-column: span 2;
      }

      #${TOGGLE_BUTTON_ID} {
        background: rgba(148, 163, 184, 0.14);
        color: #e2e8f0;
        padding: 8px 12px;
      }

      #${RESULT_ID} {
        min-height: 200px;
        max-height: 350px;
        overflow: auto;
        margin: 0;
        padding: 14px;
        border-radius: 12px;
        background: rgba(2, 6, 23, 0.75);
        border: 1px solid rgba(148, 163, 184, 0.12);
        color: #dbeafe;
        font-size: 12px;
        line-height: 1.6;
        white-space: pre-wrap;
        word-break: break-word;
        font-family: 'Courier New', monospace;
      }

      #${PANEL_ID} .helper-tip {
        margin-top: 10px;
        font-size: 12px;
        line-height: 1.5;
        color: #94a3b8;
      }

      #${PANEL_ID} .button-icon {
        margin-right: 4px;
      }
    `;

    document.head.appendChild(style);
  }

  function createPanel() {
    if (document.getElementById(PANEL_ID)) {
      return;
    }

    createStyles();

    const savedAccountValue = getSavedAccountInputValue();
    const panel = document.createElement('section');
    panel.id = PANEL_ID;
    panel.innerHTML = `
      <div class="helper-header">
        <div>
          <div class="helper-title">ChatGPT K12 Invite Helper</div>
          <div id="${STATUS_ID}" class="helper-status" data-state="idle">等待输入 account ID</div>
        </div>
        <button id="${TOGGLE_BUTTON_ID}" type="button">收起</button>
      </div>
      <div id="${BODY_ID}">
        <div class="helper-field">
          <label class="helper-label" for="${ACCOUNT_INPUT_ID}">Account / Workspace ID (每行一个,找到第一个可用即停止)</label>
          <textarea id="${ACCOUNT_INPUT_ID}" placeholder="每行输入一个 Account ID,例如:&#10;ca0e29ed-a54c-42d9-a50b-2ba5e065296d&#10;a65ebb2e-dd7c-4fdb-9a5d-6ccaf6ad00a3">${savedAccountValue.replace(/"/g, '&quot;')}</textarea>
        </div>
        <div class="helper-actions">
          <button id="${RUN_BUTTON_ID}" type="button">▶ 批量执行</button>
          <button id="${COPY_BUTTON_ID}" type="button" disabled>📋 复制JSON</button>
          <button id="${COPY_SESSION_BUTTON_ID}" type="button" disabled>🔑 复制Session</button>
          <button id="${DOWNLOAD_BUTTON_ID}" type="button" disabled>💾 下载JSON</button>
          <button id="${CLEAR_BUTTON_ID}" type="button">🗑 清空</button>
        </div>
        <pre id="${RESULT_ID}">按顺序尝试每个 ID,一旦成功加入并切换,立即停止尝试。</pre>
        <div class="helper-tip">📌 找到第一个可用 ID 后停止尝试 | 支持复制完整JSON、session_token或下载为JSON文件</div>
      </div>
    `;

    document.body.appendChild(panel);

    document.getElementById(RUN_BUTTON_ID)?.addEventListener('click', runFlow);
    document.getElementById(COPY_BUTTON_ID)?.addEventListener('click', copyResult);
    document.getElementById(COPY_SESSION_BUTTON_ID)?.addEventListener('click', copySessionToken);
    document.getElementById(DOWNLOAD_BUTTON_ID)?.addEventListener('click', downloadJson);
    document.getElementById(CLEAR_BUTTON_ID)?.addEventListener('click', clearResult);
    document.getElementById(TOGGLE_BUTTON_ID)?.addEventListener('click', togglePanelBody);
  }

  function init() {
    createPanel();
  }

  if (document.readyState === 'loading') {
    window.addEventListener('DOMContentLoaded', init, { once: true });
  } else {
    init();
  }
})();
21 个赞

佬们有效的空间id私聊分享个呀,今天注册的五个号都封了 :rofl:

求一个outlook的id​:smiley:gmail太珍贵了

2 个赞

这个导出的json文件支持cpa直接导入使用吗

1 个赞

貌似不太行吧,不都得接码吗,我反正都要接码

1 个赞

可以吧,我把session转json直接导入sub2api就可以使用了。登陆codex要接码

1 个赞

蹲一个。outlook是不是都拉闸了。

还有能用的空间吗?求好心的大佬私聊分享下:folded_hands:

佬,这个代码添加到油猴里,但是没有gpt页面没有这个弹窗怎么操作呢?

auth登录模式下才用接码,这种导入json文件的不需要,等你需要二验的时候,再登录获取这些信息就行。
主要没尝试过直接保存这个文件导入,都是转换了cpa格式再导入的,既然做了导出功能,我觉得再加个导出所需格式会更舒服些,比如cpa sub2api的对应格式

ff598c4d-ccaf-40c1-bfaa-cb94565764b1

7 个赞

留了几天大口水,终于蹬到了,感谢分享

佬,我用这导出的json格式,导入进sub2,显示不支持数据类型codex,该怎么转一下格式啊

转换一下

但是没有可以用的空间,好像还是不行吧?求空间

好家伙,这不得填几十个id进入试试水