Guest User

Untitled

a guest
Jul 4th, 2026
427
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 13.68 KB | None | 0 0
  1. // ==UserScript==
  2. // @name ChatGPT Workspace Join Request
  3. // @namespace https://chatgpt.com/
  4. // @version 4.0.0
  5. // @description 子号自动从 /api/auth/session 获取当前登录账号 AT,向母号 workspace 发送加入申请(request)或接受邀请(accept)。只需 workspace ID,UI 可编辑保存。
  6. // @author you
  7. // @match https://chatgpt.com/*
  8. // @run-at document-idle
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. (function () {
  13. "use strict";
  14.  
  15. // ===================== 默认配置 =====================
  16. const DEFAULTS = {
  17. workspaceIds: "ca0e29ed-a54c-42d9-a50b-2ba5e065296d",
  18. intervalMs: 1500,
  19. maxRetries: 3,
  20. retryBackoffMs: 5000,
  21. sessionPollMs: 20000,
  22. panelWidth: 400,
  23. };
  24.  
  25. const STORE_KEY = "jr_config_v4";
  26. function loadConfig() {
  27. let saved = {};
  28. try { saved = JSON.parse(localStorage.getItem(STORE_KEY) || "{}"); } catch (_) {}
  29. return Object.assign({}, DEFAULTS, saved);
  30. }
  31. function saveConfig(cfg) {
  32. try { localStorage.setItem(STORE_KEY, JSON.stringify(cfg)); } catch (_) {}
  33. }
  34.  
  35. let CONFIG = loadConfig();
  36.  
  37. // ---------- 状态 ----------
  38. const STATE = {
  39. at: "",
  40. session: null,
  41. deviceId: crypto.randomUUID(),
  42. autoRan: false,
  43. running: false,
  44. };
  45.  
  46. // ---------- /api/auth/session ----------
  47. async function fetchSession() {
  48. const res = await fetch("/api/auth/session", {
  49. headers: { accept: "*/*" },
  50. credentials: "include",
  51. });
  52. if (!res.ok) throw new Error(`session HTTP ${res.status}`);
  53. return res.json();
  54. }
  55.  
  56. function decodeJwt(at) {
  57. try {
  58. const p = at.split(".")[1];
  59. const j = JSON.parse(atob(p.replace(/-/g, "+").replace(/_/g, "/")));
  60. const auth = j["https://api.openai.com/auth"] || {};
  61. const prof = j["https://api.openai.com/profile"] || {};
  62. return {
  63. account_id: auth.chatgpt_account_id || "",
  64. email: prof.email || "",
  65. plan_type: auth.chatgpt_plan_type || "",
  66. exp: j.exp || 0,
  67. };
  68. } catch (_) { return {}; }
  69. }
  70.  
  71. function fmtExp(exp) {
  72. if (!exp) return "?";
  73. const min = Math.round((exp * 1000 - Date.now()) / 60000);
  74. if (min > 60) return `剩余 ${Math.round(min/60)} 小时`;
  75. return `剩余 ${min} 分钟`;
  76. }
  77.  
  78. async function refreshSession() {
  79. try {
  80. const s = await fetchSession();
  81. const at = s.accessToken || "";
  82. if (at && at !== STATE.at) {
  83. STATE.at = at;
  84. STATE.session = s;
  85. const info = decodeJwt(at);
  86. log(`子号 AT 已更新: ${info.email || "?"}`, "ok");
  87. updateUserBar(info, "ok");
  88. onATReady();
  89. } else if (!at) {
  90. updateUserBar(null, "warn");
  91. }
  92. } catch (e) {
  93. log(`session 获取失败: ${e.message}`, "warn");
  94. updateUserBar(null, "err");
  95. }
  96. }
  97.  
  98. // ---------- 发请求 ----------
  99. async function sendOne(wsId, route, attempt) {
  100. attempt = attempt || 0;
  101. const url = `/backend-api/accounts/${wsId}/invites/${route}`;
  102. const headers = {
  103. accept: "*/*",
  104. authorization: "Bearer " + STATE.at,
  105. "content-type": "application/json",
  106. "oai-device-id": STATE.deviceId,
  107. "oai-language": navigator.language || "en-US",
  108. };
  109. log(`→ POST /accounts/${wsId.slice(0,8)}/invites/${route} (第 ${attempt+1} 次)`);
  110. try {
  111. const res = await fetch(url, {
  112. method: "POST", headers, body: "", mode: "cors", credentials: "include",
  113. });
  114. const text = await res.text();
  115. if (res.ok) {
  116. log(`✓ ${wsId.slice(0,8)} HTTP ${res.status}: ${text}`, "ok");
  117. return true;
  118. }
  119. log(`✗ ${wsId.slice(0,8)} HTTP ${res.status}: ${text.slice(0,180)}`, "warn");
  120. if (res.status === 401 || res.status === 403) {
  121. log("子号 AT 失效,刷新 session...", "warn");
  122. STATE.at = "";
  123. await refreshSession();
  124. if (attempt < CONFIG.maxRetries) {
  125. await sleep(2000);
  126. return sendOne(wsId, route, attempt + 1);
  127. }
  128. return false;
  129. }
  130. if (attempt < CONFIG.maxRetries) {
  131. await sleep(CONFIG.retryBackoffMs * (attempt + 1));
  132. return sendOne(wsId, route, attempt + 1);
  133. }
  134. return false;
  135. } catch (e) {
  136. log(`网络错误: ${e.message}`, "err");
  137. if (attempt < CONFIG.maxRetries) {
  138. await sleep(CONFIG.retryBackoffMs);
  139. return sendOne(wsId, route, attempt + 1);
  140. }
  141. return false;
  142. }
  143. }
  144.  
  145. function parseWorkspaceIds() {
  146. return CONFIG.workspaceIds.split(/[\n,]+/).map(s => s.trim()).filter(Boolean);
  147. }
  148.  
  149. async function runAll(route) {
  150. if (STATE.running) { log("正在运行中,请稍候", "warn"); return; }
  151. if (!STATE.at) {
  152. log("无可用 AT,刷新 session...", "warn");
  153. await refreshSession();
  154. if (!STATE.at) { log("仍未取到 AT,请先登录 chatgpt.com", "err"); return; }
  155. }
  156. const ids = parseWorkspaceIds();
  157. if (!ids.length) { log("未配置 workspace ID", "err"); return; }
  158.  
  159. STATE.running = true;
  160. setBtns(false);
  161. log(`开始处理 ${ids.length} 个 workspace(${route})`, "info");
  162. let ok = 0;
  163. for (const ws of ids) {
  164. const r = await sendOne(ws, route);
  165. if (r) ok++;
  166. if (ids.length > 1) await sleep(CONFIG.intervalMs);
  167. }
  168. log(`完成:成功 ${ok}/${ids.length}`, ok === ids.length ? "ok" : "warn");
  169. STATE.running = false;
  170. setBtns(true);
  171. }
  172.  
  173. function onATReady() {
  174. if (!STATE.autoRan) {
  175. STATE.autoRan = true;
  176. runAll("request");
  177. }
  178. }
  179.  
  180. function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
  181.  
  182. // ---------- UI ----------
  183. let panelBody, userBarEl, reqBtnEl, accBtnEl, wsInputEl, saveBtnEl, dirty = false;
  184.  
  185. function setBtns(enabled) {
  186. [reqBtnEl, accBtnEl].forEach(b => {
  187. if (b) { b.disabled = !enabled; b.style.opacity = enabled ? "1" : "0.5"; }
  188. });
  189. }
  190.  
  191. function updateUserBar(info, status) {
  192. if (!userBarEl) return;
  193. const c = { ok: "#2f855a", warn: "#b7791f", err: "#c53030" };
  194. if (info && info.email) {
  195. userBarEl.innerHTML =
  196. `<span style="color:${c.ok}">●</span> <b>${info.email}</b> · ${info.plan_type||"?"} · ` +
  197. `<code style="background:#edf2f7;padding:1px 4px;border-radius:3px">${(info.account_id||"").slice(0,8)}</code> · ` +
  198. `<span style="color:#718096">${fmtExp(info.exp)}</span>`;
  199. } else {
  200. const msg = status === "err" ? "session 获取失败,请确认已登录" : "未检测到 AT,等待登录...";
  201. userBarEl.innerHTML = `<span style="color:${c[status]||c.warn}">●</span> ${msg}`;
  202. }
  203. }
  204.  
  205. function markDirty() {
  206. dirty = true;
  207. if (saveBtnEl) {
  208. saveBtnEl.textContent = "保存 *";
  209. saveBtnEl.style.background = "#d69e2e";
  210. saveBtnEl.style.color = "#fff";
  211. }
  212. }
  213.  
  214. function markClean() {
  215. dirty = false;
  216. if (saveBtnEl) {
  217. saveBtnEl.textContent = "已保存";
  218. saveBtnEl.style.background = "#38a169";
  219. saveBtnEl.style.color = "#fff";
  220. }
  221. setTimeout(() => {
  222. if (!dirty && saveBtnEl) {
  223. saveBtnEl.textContent = "保存";
  224. saveBtnEl.style.background = "#edf2f7";
  225. saveBtnEl.style.color = "#4a5568";
  226. }
  227. }, 1500);
  228. }
  229.  
  230. function buildPanel() {
  231. const css = `
  232. .jr-panel{position:fixed;top:14px;right:14px;width:${CONFIG.panelWidth}px;
  233. background:#fff;border:1px solid #e2e8f0;border-radius:14px;
  234. box-shadow:0 12px 32px rgba(0,0,0,.16);z-index:99999;
  235. font:13px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#1a202c;overflow:hidden}
  236. .jr-head{padding:11px 16px;background:linear-gradient(135deg,#3182ce,#2b6cb0);color:#fff;
  237. display:flex;justify-content:space-between;align-items:center}
  238. .jr-title{font-weight:600;font-size:13px;letter-spacing:.3px;display:flex;align-items:center;gap:6px}
  239. .jr-title svg{width:16px;height:16px;fill:#fff}
  240. .jr-ver{font-size:11px;opacity:.85;background:rgba(255,255,255,.2);padding:1px 6px;border-radius:8px}
  241. .jr-sub{padding:9px 16px;background:#f7fafc;border-bottom:1px solid #edf2f7;font-size:12px;color:#4a5568;min-height:22px}
  242. .jr-sec{padding:12px 16px;border-bottom:1px solid #edf2f7}
  243. .jr-label{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:#718096;
  244. margin-bottom:6px;font-weight:600;letter-spacing:.4px;text-transform:uppercase}
  245. .jr-label .jr-count{font-size:10px;color:#a0aec0;font-weight:400;text-transform:none;letter-spacing:0}
  246. .jr-ta{width:100%;box-sizing:border-box;border:1px solid #e2e8f0;border-radius:8px;
  247. padding:8px 10px;font:12px/1.5 monospace;color:#2d3748;background:#fff;transition:.15s;min-height:64px;resize:vertical}
  248. .jr-ta:focus{outline:0;border-color:#3182ce;box-shadow:0 0 0 3px rgba(49,130,206,.12)}
  249. .jr-save-row{display:flex;justify-content:flex-end;margin-top:8px}
  250. .jr-body{padding:10px 16px;max-height:36vh;overflow:auto}
  251. .jr-foot{padding:10px 16px;border-top:1px solid #edf2f7;display:flex;gap:8px;justify-content:flex-end;flex-wrap:wrap}
  252. .jr-line{padding:3px 0;word-break:break-all;border-bottom:1px dashed #f1f5f9}
  253. .jr-line:last-child{border-bottom:0}
  254. .jr-info{color:#2b6cb0}.jr-ok{color:#2f855a}.jr-warn{color:#b7791f}.jr-err{color:#c53030}
  255. .jr-btn{cursor:pointer;border:0;border-radius:7px;padding:7px 14px;font-size:12px;font-weight:500;transition:.15s}
  256. .jr-btn-primary{background:#3182ce;color:#fff}.jr-btn-primary:hover{background:#2b6cb0}
  257. .jr-btn-green{background:#38a169;color:#fff}.jr-btn-green:hover{background:#2f855a}
  258. .jr-btn-ghost{background:#edf2f7;color:#4a5568}.jr-btn-ghost:hover{background:#e2e8f0}
  259. .jr-btn:disabled{cursor:not-allowed;opacity:.5}
  260. .jr-hint{font-size:11px;color:#a0aec0;padding:6px 0 0 0;line-height:1.5}
  261. `;
  262. const style = document.createElement("style");
  263. style.textContent = css;
  264. document.head.appendChild(style);
  265.  
  266. const p = document.createElement("div");
  267. p.className = "jr-panel";
  268. p.innerHTML = `
  269. <div class="jr-head">
  270. <span class="jr-title">
  271. <svg viewBox="0 0 24 24"><path d="M15 4l6 6-10 10H5v-6L15 4zm-1 1L7 12l5 5 7-7-5-5z"/></svg>
  272. Workspace Join Request
  273. </span>
  274. <span class="jr-ver">v4.0</span>
  275. </div>
  276. <div class="jr-sub" id="jr-user">未检测到 AT,等待登录...</div>
  277.  
  278. <div class="jr-sec">
  279. <label class="jr-label">
  280. <span>母号 Workspace ID</span>
  281. <span class="jr-count" id="jr-count">0 个</span>
  282. </label>
  283. <textarea class="jr-ta" id="jr-ws" placeholder="一行一个 UUID&#10;acfb4e38-524c-4dc8-b4cf-fb3d0ce28b25"></textarea>
  284. <div class="jr-save-row">
  285. <button class="jr-btn jr-btn-ghost" id="jr-save" style="padding:5px 14px">保存</button>
  286. </div>
  287. <div class="jr-hint">只需子号 AT + workspace ID。request = 主动申请加入;accept = 接受已有邀请。</div>
  288. </div>
  289.  
  290. <div class="jr-body" id="jr-body"></div>
  291.  
  292. <div class="jr-foot">
  293. <button class="jr-btn jr-btn-ghost" id="jr-refresh">刷新 AT</button>
  294. <button class="jr-btn jr-btn-green" id="jr-accept">Accept</button>
  295. <button class="jr-btn jr-btn-primary" id="jr-run">Request</button>
  296. </div>
  297. `;
  298. document.body.appendChild(p);
  299.  
  300. panelBody = p.querySelector("#jr-body");
  301. userBarEl = p.querySelector("#jr-user");
  302. reqBtnEl = p.querySelector("#jr-run");
  303. accBtnEl = p.querySelector("#jr-accept");
  304. wsInputEl = p.querySelector("#jr-ws");
  305. saveBtnEl = p.querySelector("#jr-save");
  306. const countEl = p.querySelector("#jr-count");
  307.  
  308. // 填入已保存值
  309. wsInputEl.value = CONFIG.workspaceIds;
  310. const updateCount = () => {
  311. const n = wsInputEl.value.split(/[\n,]+/).map(s=>s.trim()).filter(Boolean).length;
  312. countEl.textContent = `${n} 个`;
  313. };
  314. updateCount();
  315.  
  316. // 编辑标记 dirty
  317. wsInputEl.addEventListener("input", () => {
  318. markDirty();
  319. updateCount();
  320. });
  321.  
  322. // 保存
  323. saveBtnEl.addEventListener("click", () => {
  324. CONFIG.workspaceIds = wsInputEl.value;
  325. saveConfig(CONFIG);
  326. markClean();
  327. log("workspace 已保存", "ok");
  328. });
  329.  
  330. // 按钮
  331. reqBtnEl.addEventListener("click", () => runAll("request"));
  332. accBtnEl.addEventListener("click", () => runAll("accept"));
  333. p.querySelector("#jr-refresh").addEventListener("click", async () => {
  334. log("手动刷新 session...", "info");
  335. await refreshSession();
  336. });
  337. }
  338.  
  339. function log(msg, level) {
  340. const styles = { info: "jr-info", ok: "jr-ok", warn: "jr-warn", err: "jr-err" };
  341. const time = new Date().toLocaleTimeString();
  342. console.log("%c[JoinReq]", "color:#3182ce;font-weight:bold", msg);
  343. if (panelBody) {
  344. const line = document.createElement("div");
  345. line.className = `jr-line ${styles[level] || "jr-info"}`;
  346. line.textContent = `[${time}] ${msg}`;
  347. panelBody.appendChild(line);
  348. panelBody.scrollTop = panelBody.scrollHeight;
  349. }
  350. }
  351.  
  352. // ---------- 启动 ----------
  353. function boot() {
  354. buildPanel();
  355. log("脚本已加载 v4.0", "info");
  356. log("只需子号 AT + workspace ID", "info");
  357. log("request = 主动申请 · accept = 接受邀请", "info");
  358. refreshSession();
  359. setInterval(refreshSession, CONFIG.sessionPollMs);
  360. window.addEventListener("focus", () => { if (!STATE.running) refreshSession(); });
  361. }
  362.  
  363. if (document.readyState === "loading") {
  364. document.addEventListener("DOMContentLoaded", boot);
  365. } else {
  366. boot();
  367. }
  368. })();
  369.  
Advertisement
Add Comment
Please, Sign In to add comment