/* * ChatGPT Team/Business single-seat cost checker. * * Usage: * 1. Sign in at https://chatgpt.com/ with the workspace owner account. * 2. Paste this entire file into DevTools Console and run it. * * This script only calls the subscription preview endpoint. It never submits * a seat update or changes the subscription. */ (async () => { "use strict"; const CHATGPT_ORIGIN = "https://chatgpt.com"; const AUTH_CLAIM = "https://api.openai.com/auth"; const DIALOG_ID = "team-workflow-seat-cost-dialog"; const ZERO_DECIMAL_CURRENCIES = new Set([ "BIF", "CLP", "DJF", "GNF", "JPY", "KMF", "KRW", "MGA", "PYG", "RWF", "VND", "VUV", "XAF", "XOF", "XPF", ]); const THREE_DECIMAL_CURRENCIES = new Set([ "BHD", "IQD", "JOD", "KWD", "LYD", "OMR", "TND", ]); const FOUR_DECIMAL_CURRENCIES = new Set(["CLF", "UYW"]); function firstText(...values) { for (const value of values) { if (value !== undefined && value !== null && String(value).trim()) { return String(value).trim(); } } return ""; } function decodeJwtClaims(token) { const part = String(token || "").split(".")[1]; if (!part) return {}; try { const normalized = part.replace(/-/g, "+").replace(/_/g, "/"); const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4); const bytes = Uint8Array.from(atob(padded), (value) => value.charCodeAt(0)); return JSON.parse(new TextDecoder().decode(bytes)); } catch (_) { return {}; } } function authContext(session) { const account = session?.account && typeof session.account === "object" ? session.account : {}; const claims = decodeJwtClaims(session?.accessToken); const auth = claims?.[AUTH_CLAIM] && typeof claims[AUTH_CLAIM] === "object" ? claims[AUTH_CLAIM] : {}; return { accessToken: firstText(session?.accessToken, session?.access_token), accountId: firstText( auth.chatgpt_account_id, auth.poid, session?.chatgpt_account_id, session?.account_id, session?.workspace_id, account.id, account.account_id, ), }; } function cookieValue(name) { const escaped = String(name).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const match = document.cookie.match(new RegExp(`(?:^|;\\s*)${escaped}=([^;]*)`)); return match ? decodeURIComponent(match[1]) : ""; } function setCookie(name, value) { document.cookie = `${name}=${encodeURIComponent(value)}; Path=/; Secure; SameSite=Lax`; } function clearCookie(name) { document.cookie = `${name}=; Path=/; Max-Age=0; Secure; SameSite=Lax`; } async function requestJson(url, options = {}, label = "请求") { const response = await fetch(url, { credentials: "include", cache: "no-store", ...options, headers: { Accept: "application/json", ...(options.headers || {}), }, }); const text = await response.text(); let data = {}; if (text.trim()) { try { data = JSON.parse(text); } catch (_) { data = {}; } } if (!response.ok) { const error = data?.error && typeof data.error === "object" ? data.error : {}; const detail = firstText( error.message, error.code, data?.message, data?.detail, text.slice(0, 240), ); throw new Error(`${label}失败:HTTP ${response.status}${detail ? ` · ${detail}` : ""}`); } if (!data || typeof data !== "object" || Array.isArray(data)) { throw new Error(`${label}失败:响应不是 JSON 对象`); } return data; } function workspaceRows(payload) { const accounts = payload?.accounts && typeof payload.accounts === "object" ? payload.accounts : {}; const ordering = Array.isArray(payload?.account_ordering) ? payload.account_ordering.map(String) : Object.keys(accounts); const ids = [...new Set([...ordering, ...Object.keys(accounts)])]; return ids.map((key) => { const wrapper = accounts[key] && typeof accounts[key] === "object" ? accounts[key] : {}; const account = wrapper.account && typeof wrapper.account === "object" ? wrapper.account : wrapper; return { id: firstText(account.account_id, account.id, wrapper.account_id, key), name: firstText( account.name, account.workspace_name, wrapper.name, wrapper.workspace_name, key, ), structure: firstText(account.structure, wrapper.structure).toLowerCase(), deactivated: account.is_deactivated === true || wrapper.is_deactivated === true, }; }).filter((row) => row.id && !row.deactivated); } function chooseWorkspace(rows, currentId) { const current = rows.find((row) => row.id === currentId); if (current && current.structure && current.structure !== "personal") return current; const workspaces = rows.filter((row) => row.structure !== "personal"); if (workspaces.length === 1) return workspaces[0]; if (!workspaces.length) { throw new Error("当前账号没有可用的 Team/Business Workspace"); } const options = workspaces .map((row, index) => `${index + 1}. ${row.name} (${row.id})`) .join("\n"); const answer = window.prompt(`请选择要检测的 Workspace:\n\n${options}`, "1"); if (answer === null) throw new Error("已取消检测"); const index = Number(answer) - 1; if (!Number.isInteger(index) || !workspaces[index]) { throw new Error("Workspace 序号无效"); } return workspaces[index]; } async function workspaceSession(workspaceId, initialSession) { const initial = authContext(initialSession); if (initial.accountId === workspaceId && initial.accessToken) return initial; const query = new URLSearchParams({ exchange_workspace_token: "true", workspace_id: workspaceId, reason: "setCurrentAccount", }); const contextCookies = [ ["_account", workspaceId], ["_account_is_fedramp", "false"], ["_account_residency_region", "no_constraint"], ]; const previousCookies = new Map( contextCookies.map(([name]) => [name, cookieValue(name)]), ); let session; try { for (const [name, value] of contextCookies) setCookie(name, value); session = await requestJson( `/api/auth/session?${query}`, { headers: { "X-OpenAI-Target-Path": "/api/auth/session", "X-OpenAI-Target-Route": "/api/auth/session", }, }, "切换 Workspace 上下文", ); } finally { for (const [name] of contextCookies) { const previous = previousCookies.get(name); if (previous) setCookie(name, previous); else clearCookie(name); } } const context = authContext(session); if (!context.accessToken) throw new Error("Workspace 会话未返回 Access Token"); if (context.accountId && context.accountId !== workspaceId) { throw new Error(`Workspace 切换结果不匹配:${context.accountId}`); } return {...context, accountId: workspaceId}; } async function previewSeatCost(accessToken, workspaceId, updatedSeats) { const query = new URLSearchParams({ account_id: workspaceId, updated_seats: String(updatedSeats), }); return requestJson( `/backend-api/subscriptions/update/preview?${query}`, { headers: { Authorization: `Bearer ${accessToken}`, "chatgpt-account-id": workspaceId, }, }, "读取席位费用", ); } async function singleSeatPreview(accessToken, workspaceId) { let targetSeats = 3; for (let attempt = 0; attempt < 3; attempt += 1) { const preview = await previewSeatCost(accessToken, workspaceId, targetSeats); const currentSeats = Number(preview?.current_seat_quantity); if (!Number.isInteger(currentSeats) || currentSeats < 1) { throw new Error("费用预览未返回当前席位数"); } const desiredSeats = currentSeats + 1; if (targetSeats === desiredSeats) { return {preview, currentSeats, updatedSeats: targetSeats}; } targetSeats = desiredSeats; } throw new Error("检测期间当前席位数连续变化,请稍后重试"); } function currencyMinorUnit(currency) { if (ZERO_DECIMAL_CURRENCIES.has(currency)) return 0; if (THREE_DECIMAL_CURRENCIES.has(currency)) return 3; if (FOUR_DECIMAL_CURRENCIES.has(currency)) return 4; return 2; } function previewAmount(preview) { const amountDue = preview?.amount_due && typeof preview.amount_due === "object" ? preview.amount_due : {}; const rawAmount = amountDue.amount ?? preview?.total_amount; const currency = firstText(preview?.currency, amountDue.currency).toUpperCase(); if (rawAmount === undefined || rawAmount === null || !/^[A-Z]{3}$/.test(currency)) { throw new Error("费用预览未返回有效的金额或币种"); } const minorUnit = currencyMinorUnit(currency); const amount = Number(rawAmount) / 10 ** minorUnit; if (!Number.isFinite(amount)) throw new Error("费用预览金额无效"); let formatted; try { formatted = new Intl.NumberFormat("zh-CN", { style: "currency", currency, minimumFractionDigits: minorUnit, maximumFractionDigits: minorUnit, }).format(amount); } catch (_) { formatted = `${currency} ${amount.toFixed(minorUnit)}`; } return {amount, currency, minorUnit, formatted}; } function billingTime(value) { const raw = firstText(value); if (!raw) return {formatted: "未返回", raw: ""}; const date = new Date(raw); if (Number.isNaN(date.getTime())) return {formatted: raw, raw}; return { formatted: new Intl.DateTimeFormat("zh-CN", { timeZone: "Asia/Shanghai", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false, }).format(date), raw, }; } function showDialog({title, tone = "normal", rows}) { document.getElementById(DIALOG_ID)?.remove(); const dialog = document.createElement("dialog"); dialog.id = DIALOG_ID; dialog.style.cssText = [ "width:min(460px,calc(100% - 40px))", "max-width:460px", "padding:0", "border:0", "border-radius:8px", "background:transparent", "overflow:visible", "z-index:2147483647", "font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif", ].join(";"); const panel = document.createElement("section"); panel.setAttribute("role", "dialog"); panel.setAttribute("aria-modal", "true"); panel.style.cssText = [ "width:min(460px,100%)", "background:#fff", "color:#171717", "border:1px solid #d8d8d8", "border-radius:8px", "box-shadow:0 24px 70px rgba(0,0,0,.32)", "overflow:hidden", ].join(";"); const heading = document.createElement("h2"); heading.textContent = title; heading.style.cssText = [ "margin:0", "padding:18px 20px", "font-size:18px", "line-height:1.35", `border-top:4px solid ${tone === "error" ? "#c83b3b" : "#16845b"}`, "border-bottom:1px solid #e6e6e6", ].join(";"); const list = document.createElement("dl"); list.style.cssText = "margin:0;padding:8px 20px 4px"; for (const row of rows) { const wrap = document.createElement("div"); wrap.style.cssText = "display:grid;grid-template-columns:120px minmax(0,1fr);gap:12px;padding:10px 0;border-bottom:1px solid #eee"; const label = document.createElement("dt"); label.textContent = row.label; label.style.cssText = "color:#666;font-size:13px"; const value = document.createElement("dd"); value.textContent = row.value; value.style.cssText = `margin:0;font-size:${row.primary ? "20px" : "14px"};font-weight:${row.primary ? "700" : "500"};overflow-wrap:anywhere`; wrap.append(label, value); list.append(wrap); } const footer = document.createElement("footer"); footer.style.cssText = "display:flex;justify-content:flex-end;padding:14px 20px"; const close = document.createElement("button"); close.type = "button"; close.textContent = "关闭"; close.style.cssText = "border:1px solid #bbb;border-radius:6px;background:#fff;color:#171717;padding:8px 18px;font:inherit;cursor:pointer"; close.addEventListener("click", () => dialog.close()); dialog.addEventListener("click", (event) => { if (event.target === dialog) dialog.close(); }); dialog.addEventListener("close", () => dialog.remove(), {once: true}); footer.append(close); panel.append(heading, list, footer); dialog.append(panel); document.body.append(dialog); dialog.showModal(); close.focus(); } try { if (window.location.origin !== CHATGPT_ORIGIN) { throw new Error("请在 https://chatgpt.com/ 页面运行此脚本"); } const initialSession = await requestJson("/api/auth/session", {}, "读取登录会话"); const initialContext = authContext(initialSession); if (!initialContext.accessToken) throw new Error("当前页面没有有效登录会话"); const accounts = await requestJson( "/backend-api/accounts/check/v4-2023-04-27", {headers: {Authorization: `Bearer ${initialContext.accessToken}`}}, "读取 Workspace 列表", ); const workspace = chooseWorkspace(workspaceRows(accounts), initialContext.accountId); const context = await workspaceSession(workspace.id, initialSession); const result = await singleSeatPreview(context.accessToken, workspace.id); const amount = previewAmount(result.preview); const renewal = billingTime(result.preview?.renewal_date); showDialog({ title: "单个席位费用", rows: [ {label: "新增 1 席费用", value: amount.formatted, primary: true}, {label: "账单时间", value: `${renewal.formatted}(北京时间)`}, ...(renewal.raw ? [{label: "原始账单时间", value: renewal.raw}] : []), {label: "席位变化", value: `${result.currentSeats} → ${result.updatedSeats}`}, {label: "Workspace", value: `${workspace.name} (${workspace.id})`}, ], }); } catch (error) { showDialog({ title: "席位费用检测失败", tone: "error", rows: [ {label: "错误", value: firstText(error?.message, error)}, ], }); console.error("[seat-cost-check]", error); } })(); 查看Team到期的脚本 @nyxarrival #6 发布于2026/9/2 22:43:05 时间怎么看?
1500收
要几个,怎么联系
@wechatpay #2 LKH200__
顶楼
顶楼
时间怎么看?
这有什么用
1
查看Team到期的脚本
收9月4日的team 要求晚上9点到12点过期 或者9月5日凌晨 其他时间再议 1.9K秒款