// ==UserScript== // @name ChatGPT 多账号一键切换(本地 ST) // @namespace codex-plus // @version 1.1.0 // @description 在 chatgpt.com 保存多个账号的 sessionToken(ST),悬浮球/菜单一键切换,支持备份导入导出。原理:网页登录态 = HttpOnly cookie `__Secure-next-auth.session-token`,用 GM_cookie 删旧写新后刷新。需要 Tampermonkey(GM_cookie 仅 TM 支持)。 // @author codex_plus // @match https://chatgpt.com/* // @match https://chat.openai.com/* // @grant GM_cookie // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @run-at document-idle // @noframes // ==/UserScript== /* * 安全须知: * - ST 是账号钥匙,明文存在 Tampermonkey 脚本存储里(仅本机本浏览器)。勿导出分享;「备份」导出的 JSON 同样含明文 ST。 * - 在 chatgpt.com 执行“登出所有设备”会使所有已存 ST 全部失效。 * - ST 过期(约 30 天滚动)后需在该账号登录状态下点“保存当前账号”重新捕获。 * - 本脚本只读写你自己的会话 cookie,不外发任何数据。 * * v1.1.0 * - UI:悬浮球入口;面板深/浅色自适应(跟随系统),账号列表 + 当前高亮,两步删除确认, * 导入改为面板内文本框(不再用 prompt),toast 堆叠显示,Esc/遮罩点击关闭 * - 逻辑:捕获即标记为当前账号;页面加载时按实际会话校正 active(手动换号后不再显示陈旧标记); * 切换/写 cookie 失败可见(toast);会话探测 5s 超时;分块序号解析加 NaN 防御 * - 新增:备份导出/导入(导入兼容本脚本导出的备份 JSON) */ (function () { "use strict"; if (typeof GM_cookie === "undefined" || typeof GM_cookie.list !== "function") { // 必须在 Tampermonkey 下运行;测试环境由 harness 预置 GM_cookie shim if (!window.__CAS_TEST__) return; } // ---------- 常量 / 存储 ---------- const COOKIE_BASE = "__Secure-next-auth.session-token"; const CHUNK_SIZE = 3500; // NextAuth 单 cookie 上限约 4KB,超长按 .0/.1/.2 分块 const SECURE = COOKIE_BASE.startsWith("__Secure-"); const store = { get: (k, d) => (GM_getValue(k) === undefined ? d : GM_getValue(k)), set: (k, v) => GM_setValue(k, v), }; const getAccounts = () => store.get("cas_accounts", {}); const putAccounts = (a) => store.set("cas_accounts", a); const getActive = () => store.get("cas_active", null); const setActive = (n) => store.set("cas_active", n); // ---------- 工具 ---------- // 所有动态文案一律走 textContent,杜绝把账号名拼进 HTML(账号名来自外部 JSON) const el = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text !== undefined) n.textContent = text; return n; }; function deepFind(obj, key) { if (Array.isArray(obj)) { for (const v of obj) { const r = deepFind(v, key); if (r !== undefined) return r; } return undefined; } if (obj && typeof obj === "object") { for (const [k, v] of Object.entries(obj)) { if (k === key && typeof v === "string" && v) return v; } for (const v of Object.values(obj)) { const r = deepFind(v, key); if (r !== undefined) return r; } } return undefined; } // ---------- 样式(注入