基于之前佬的油猴脚本二开
增加支持多个 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,例如: ca0e29ed-a54c-42d9-a50b-2ba5e065296d a65ebb2e-dd7c-4fdb-9a5d-6ccaf6ad00a3">${savedAccountValue.replace(/"/g, '"')}</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();
}
})();