plus订阅弹手机号问题

订阅了plus以为可以爽用了,结果codex登录弹了手机号 没有refresh_token free号是没法使用的,但是plus号可以正常调用,只是到期不能刷新 所以叫codex糊了一个,获取到session后转为cpa/sub2的json格式文件,方便导入使用 worker.js如下: const...
plus订阅弹手机号问题
plus订阅弹手机号问题

订阅plus以为可以爽用了,结果codex登录弹了手机号

没有refresh_token free号是没法使用的,但是plus号可以正常调用,只是到期不能刷新

所以叫codex糊了一个,获取到session后转为cpa/sub2的json格式文件,方便导入使用

worker.js如下:

const HTML = String.raw`<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>CPA / sub2api JSON Converter</title>
  <style>
    :root {
      color-scheme: light;
      --bg: #f4f7fb;
      --panel: #ffffff;
      --line: #d7e0ed;
      --line-soft: #e8eef6;
      --text: #0f172a;
      --muted: #62708a;
      --blue: #1f73b7;
      --blue-dark: #155f99;
      --green: #178a55;
      --amber-bg: #fff8e8;
      --amber-line: #f3be62;
      --amber-text: #8a570f;
      --red: #b42318;
    }

    * {
      box-sizing: border-box;
    }

    body {
      margin: 0;
      min-height: 100vh;
      background: var(--bg);
      color: var(--text);
      font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      font-size: 14px;
    }

    main {
      width: min(1180px, calc(100vw - 32px));
      margin: 0 auto;
      padding: 22px 0 34px;
    }

    header {
      margin-bottom: 18px;
    }

    h1 {
      margin: 0 0 4px;
      font-size: 26px;
      line-height: 1.2;
      letter-spacing: 0;
    }

    .subtitle {
      display: flex;
      flex-wrap: wrap;
      gap: 10px;
      color: var(--muted);
      font-size: 14px;
    }

    .subtitle a {
      color: #075cab;
      font-weight: 700;
      text-decoration: none;
    }

    .layout {
      display: grid;
      grid-template-columns: minmax(0, 1.12fr) minmax(360px, 0.88fr);
      gap: 18px;
      align-items: start;
    }

    .panel {
      overflow: hidden;
      background: var(--panel);
      border: 1px solid var(--line);
      border-radius: 8px;
      box-shadow: 0 16px 42px rgba(15, 23, 42, 0.08);
    }

    .panel-head {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 12px;
      min-height: 58px;
      padding: 12px 18px;
      border-bottom: 1px solid var(--line);
    }

    .panel-title {
      font-weight: 800;
    }

    .actions {
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      justify-content: flex-end;
    }

    button,
    select,
    input,
    textarea {
      font: inherit;
    }

    button {
      min-height: 40px;
      border: 1px solid var(--line);
      border-radius: 7px;
      background: #fff;
      color: var(--text);
      cursor: pointer;
      font-weight: 750;
      transition: background .15s, border-color .15s, transform .15s;
    }

    button:hover {
      border-color: #adc0d7;
      background: #f8fbff;
    }

    button:active {
      transform: translateY(1px);
    }

    .btn {
      padding: 0 14px;
    }

    .btn-primary {
      border-color: var(--blue);
      background: var(--blue);
      color: #fff;
    }

    .btn-primary:hover {
      border-color: var(--blue-dark);
      background: var(--blue-dark);
    }

    .btn-wide {
      width: 100%;
    }

    .body {
      padding: 18px;
    }

    label {
      display: block;
      margin-bottom: 8px;
      font-weight: 800;
    }

    textarea {
      width: 100%;
      min-height: 460px;
      resize: vertical;
      padding: 12px;
      border: 1px solid var(--line);
      border-radius: 7px;
      outline: none;
      color: #0b1220;
      background: #fff;
      font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
      font-size: 12px;
      line-height: 1.55;
      tab-size: 2;
    }

    textarea:focus,
    input:focus,
    select:focus {
      border-color: #71a9da;
      box-shadow: 0 0 0 3px rgba(31, 115, 183, 0.13);
    }

    .field {
      margin-top: 16px;
    }

    input,
    select {
      width: 100%;
      min-height: 40px;
      padding: 0 12px;
      border: 1px solid var(--line);
      border-radius: 7px;
      outline: none;
      background: #fff;
    }

    .grid-2 {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 12px;
    }

    .hint {
      margin-top: 8px;
      color: var(--muted);
      font-size: 12px;
      line-height: 1.5;
    }

    .status {
      display: inline-flex;
      align-items: center;
      min-height: 24px;
      padding: 0 10px;
      border-radius: 999px;
      background: #eff6ff;
      color: #175a96;
      font-size: 12px;
      font-weight: 800;
      white-space: nowrap;
    }

    .status.ok {
      background: #eaf8f0;
      color: var(--green);
    }

    .status.warn {
      background: #fff1d6;
      color: #9a6208;
    }

    .kv {
      margin: 12px 0 14px;
      border-top: 1px solid var(--line-soft);
    }

    .row {
      display: grid;
      grid-template-columns: 132px minmax(0, 1fr);
      gap: 10px;
      padding: 12px 0;
      border-bottom: 1px solid var(--line-soft);
      align-items: center;
    }

    .key {
      color: var(--muted);
      font-size: 14px;
    }

    .value {
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
      font-size: 12px;
    }

    .notice {
      margin-top: 8px;
      padding: 10px 12px;
      border: 1px solid var(--amber-line);
      border-radius: 7px;
      background: var(--amber-bg);
      color: var(--amber-text);
      line-height: 1.5;
    }

    .notice.error {
      border-color: #ffb4ab;
      background: #fff0ee;
      color: var(--red);
    }

    .download-grid {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 10px;
      margin-top: 16px;
    }

    .preview {
      margin-top: 16px;
    }

    .preview-head {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 12px;
      margin-bottom: 8px;
    }

    pre {
      max-height: 260px;
      overflow: auto;
      margin: 0;
      padding: 12px;
      border: 1px solid var(--line);
      border-radius: 7px;
      background: #fbfdff;
      color: #0b1220;
      font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
      font-size: 12px;
      line-height: 1.55;
      white-space: pre-wrap;
      word-break: break-word;
    }

    @media (max-width: 900px) {
      main {
        width: min(100vw - 24px, 760px);
        padding-top: 16px;
      }

      .layout,
      .grid-2,
      .download-grid {
        grid-template-columns: 1fr;
      }

      textarea {
        min-height: 340px;
      }
    }
  </style>
</head>
<body>
  <main>
    <header>
      <h1>CPA / sub2api JSON 下载</h1>
      <div class="subtitle">
        <span>ChatGPT Session JSON 转 CPA Auth JSON / sub2api 导入 JSON</span>
        <a href="https://chatgpt.com/api/auth/session" target="_blank" rel="noopener noreferrer">https://chatgpt.com/api/auth/session</a>
      </div>
    </header>

    <div class="layout">
      <section class="panel">
        <div class="panel-head">
          <div class="panel-title">输入</div>
          <div class="actions">
            <button class="btn" id="pasteBtn" type="button">粘贴</button>
            <button class="btn" id="clearBtn" type="button">清空</button>
            <button class="btn btn-primary" id="generateBtn" type="button">生成</button>
          </div>
        </div>
        <div class="body">
          <label for="source">OpenAI / Codex Session JSON</label>
          <textarea id="source" spellcheck="false" placeholder='粘贴 https://chatgpt.com/api/auth/session 返回的 JSON'></textarea>

          <div class="grid-2">
            <div class="field">
              <label for="namePrefix">账号名前缀</label>
              <select id="namePrefix">
                <option value="codex">codex</option>
                <option value="chatgpt">chatgpt</option>
                <option value="openai">openai</option>
              </select>
            </div>
            <div class="field">
              <label for="fileName">文件名</label>
              <input id="fileName" placeholder="可留空自动生成">
            </div>
          </div>
          <div class="hint">转换逻辑在浏览器本地执行,Worker 不接收、不保存你的 token。下载后的 JSON 仍是敏感凭据,请勿公开分享。</div>
        </div>
      </section>

      <section class="panel">
        <div class="panel-head">
          <div class="panel-title">结果</div>
          <span class="status" id="status">待生成</span>
        </div>
        <div class="body">
          <div class="kv">
            <div class="row"><div class="key">CPA 文件名</div><div class="value" id="cpaName">-</div></div>
            <div class="row"><div class="key">sub2 文件名</div><div class="value" id="sub2Name">-</div></div>
            <div class="row"><div class="key">邮箱</div><div class="value" id="email">-</div></div>
            <div class="row"><div class="key">账号 ID</div><div class="value" id="accountId">-</div></div>
            <div class="row"><div class="key">套餐</div><div class="value" id="planType">-</div></div>
            <div class="row"><div class="key">过期时间</div><div class="value" id="expires">-</div></div>
            <div class="row"><div class="key">access_token</div><div class="value" id="accessToken">-</div></div>
            <div class="row"><div class="key">session_token</div><div class="value" id="sessionToken">-</div></div>
            <div class="row"><div class="key">refresh_token</div><div class="value" id="refreshToken">-</div></div>
            <div class="row"><div class="key">id_token</div><div class="value" id="idToken">-</div></div>
          </div>

          <div id="messages"></div>

          <div class="download-grid">
            <button class="btn btn-primary" id="downloadCpa" type="button" disabled>下载 CPA JSON</button>
            <button class="btn btn-primary" id="downloadSub2" type="button" disabled>下载 sub2 JSON</button>
            <button class="btn" id="downloadBoth" type="button" disabled>下载两个文件</button>
            <button class="btn" id="copyFileNames" type="button" disabled>复制文件名</button>
          </div>

          <div class="preview">
            <div class="preview-head">
              <label for="previewType" style="margin:0">预览</label>
              <select id="previewType" style="width:150px">
                <option value="cpa">CPA</option>
                <option value="sub2">sub2api</option>
              </select>
            </div>
            <pre id="preview">尚未生成</pre>
          </div>
        </div>
      </section>
    </div>
  </main>

  <script>
    "use strict";

    const $ = (id) => document.getElementById(id);
    const state = {
      cpa: null,
      sub2: null,
      cpaFileName: "",
      sub2FileName: "",
    };

    function parseJson(text) {
      const trimmed = text.trim();
      if (!trimmed) throw new Error("请先粘贴 session JSON。");
      try {
        return JSON.parse(trimmed);
      } catch (error) {
        const first = trimmed.indexOf("{");
        const last = trimmed.lastIndexOf("}");
        if (first >= 0 && last > first) {
          return JSON.parse(trimmed.slice(first, last + 1));
        }
        throw new Error("JSON 格式无效,请确认粘贴的是完整 session JSON。");
      }
    }

    function base64UrlToString(input) {
      const base64 = input.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(input.length / 4) * 4, "=");
      const binary = atob(base64);
      const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
      return new TextDecoder().decode(bytes);
    }

    function stringToBase64Url(input) {
      const bytes = new TextEncoder().encode(input);
      let binary = "";
      for (const byte of bytes) binary += String.fromCharCode(byte);
      return btoa(binary).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
    }

    function decodeJwtPayload(token) {
      if (!token || typeof token !== "string") return {};
      const parts = token.split(".");
      if (parts.length < 2 || !parts[1]) return {};
      try {
        return JSON.parse(base64UrlToString(parts[1]));
      } catch {
        return {};
      }
    }

    function createUnsignedJwt(payload) {
      const header = { alg: "none", typ: "JWT", cpa_synthetic: true };
      return stringToBase64Url(JSON.stringify(header)) + "." + stringToBase64Url(JSON.stringify(payload)) + ".";
    }

    function getAuthClaims(accessPayload) {
      return accessPayload["https://api.openai.com/auth"] || {};
    }

    function getProfile(accessPayload) {
      return accessPayload["https://api.openai.com/profile"] || {};
    }

    function cleanNamePart(value) {
      return String(value || "")
        .trim()
        .replace(/[^a-zA-Z0-9@._-]+/g, "-")
        .replace(/-+/g, "-")
        .replace(/^-|-$/g, "");
    }

    function shortId(accountId) {
      return String(accountId || "").slice(0, 8) || "account";
    }

    function isoToUnix(iso) {
      const time = Date.parse(iso || "");
      return Number.isFinite(time) ? Math.floor(time / 1000) : undefined;
    }

    function unixToIso(seconds) {
      return Number.isFinite(seconds) ? new Date(seconds * 1000).toISOString() : "";
    }

    function valueStatus(value) {
      return value ? "已包含" : "缺失";
    }

    function buildOutputs(session) {
      const accessToken = session.accessToken || session.access_token || "";
      const sessionToken = session.sessionToken || session.session_token || "";
      const refreshToken = session.refreshToken || session.refresh_token || "";
      const accessPayload = decodeJwtPayload(accessToken);
      const authClaims = getAuthClaims(accessPayload);
      const profile = getProfile(accessPayload);

      const user = session.user || {};
      const account = session.account || {};
      const email = user.email || profile.email || session.email || "";
      const accountId = account.id || authClaims.chatgpt_account_id || session.account_id || session.chatgpt_account_id || "";
      const planType = account.planType || account.plan_type || authClaims.chatgpt_plan_type || session.plan_type || "unknown";
      const chatgptUserId = user.id || authClaims.chatgpt_user_id || authClaims.user_id || session.chatgpt_user_id || "";
      const clientId = accessPayload.client_id || session.client_id || "";
      const accessExp = Number(accessPayload.exp) || isoToUnix(session.expires) || isoToUnix(session.expired) || undefined;
      const idTokenExp = isoToUnix(session.expires) || accessExp;
      const issuedAt = Math.floor(Date.now() / 1000);
      const idToken = session.id_token || createUnsignedJwt({
        iat: issuedAt,
        exp: idTokenExp,
        "https://api.openai.com/auth": {
          chatgpt_account_id: accountId,
          chatgpt_plan_type: planType,
          chatgpt_user_id: chatgptUserId,
          user_id: chatgptUserId,
        },
        email,
      });

      if (!email) throw new Error("无法识别邮箱字段:需要 user.email 或 access_token profile.email。");
      if (!accountId) throw new Error("无法识别账号 ID:需要 account.id 或 access_token claims。");
      if (!accessToken) throw new Error("缺少 accessToken / access_token。");

      const prefix = $("namePrefix").value || "codex";
      const baseName = cleanNamePart($("fileName").value) || cleanNamePart(prefix + "-" + email + "-" + shortId(accountId));
      const cpaFileName = baseName + ".json";
      const sub2FileName = "sub2api-" + baseName + ".json";
      const nowIso = new Date().toISOString();

      const cpa = {
        type: "codex",
        email,
        account_id: accountId,
        chatgpt_account_id: accountId,
        plan_type: planType,
        chatgpt_plan_type: planType,
        id_token: idToken,
        access_token: accessToken,
        refresh_token: refreshToken,
        session_token: sessionToken,
        last_refresh: nowIso,
        expired: session.expires || session.expired || unixToIso(accessExp),
        disabled: false,
        id_token_synthetic: !session.id_token,
      };

      const sub2 = {
        exported_at: nowIso.replace(/\.\d{3}Z$/, "Z"),
        proxies: [],
        accounts: [
          {
            name: baseName,
            platform: "openai",
            type: "oauth",
            credentials: {
              access_token: accessToken,
              chatgpt_account_id: accountId,
              chatgpt_user_id: chatgptUserId,
              client_id: clientId,
              email,
              expires_at: accessExp || null,
              id_token: idToken,
              organization_id: session.organization_id || "",
              plan_type: planType,
              refresh_token: refreshToken,
            },
            extra: { email },
            concurrency: 10,
            priority: 1,
            rate_multiplier: 1,
            auto_pause_on_expired: true,
          },
        ],
      };

      return {
        cpa,
        sub2,
        cpaFileName,
        sub2FileName,
        summary: {
          email,
          accountId,
          planType,
          expires: cpa.expired || "-",
          accessToken: valueStatus(accessToken),
          sessionToken: valueStatus(sessionToken),
          refreshToken: valueStatus(refreshToken),
          idToken: session.id_token ? "已包含" : "占位 claims",
        },
        warnings: [
          !refreshToken ? "缺少 refresh_token,access_token 过期后 CPA 不能自动刷新。" : "",
          !session.id_token ? "缺少真实 id_token,已根据 account_id / plan_type 写入 CPA 额度面板可解析的占位 claims;上游认证仍使用 access_token。" : "",
          !sessionToken ? "缺少 session_token,部分依赖网页会话的工具可能不可用。" : "",
        ].filter(Boolean),
      };
    }

    function renderMessages(warnings, error) {
      const box = $("messages");
      if (error) {
        box.innerHTML = '<div class="notice error">' + escapeHtml(error) + "</div>";
        return;
      }
      box.innerHTML = warnings.map((text) => '<div class="notice">' + escapeHtml(text) + "</div>").join("");
    }

    function escapeHtml(input) {
      return String(input)
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#39;");
    }

    function setText(id, text) {
      $(id).textContent = text || "-";
      $(id).title = text || "";
    }

    function setReady(enabled) {
      $("downloadCpa").disabled = !enabled;
      $("downloadSub2").disabled = !enabled;
      $("downloadBoth").disabled = !enabled;
      $("copyFileNames").disabled = !enabled;
    }

    function refreshPreview() {
      if (!state.cpa || !state.sub2) {
        $("preview").textContent = "尚未生成";
        return;
      }
      const value = $("previewType").value === "sub2" ? state.sub2 : state.cpa;
      $("preview").textContent = JSON.stringify(value, null, 2);
    }

    function generate() {
      try {
        const session = parseJson($("source").value);
        const result = buildOutputs(session);
        Object.assign(state, result);

        setText("cpaName", result.cpaFileName);
        setText("sub2Name", result.sub2FileName);
        setText("email", result.summary.email);
        setText("accountId", result.summary.accountId);
        setText("planType", result.summary.planType);
        setText("expires", result.summary.expires);
        setText("accessToken", result.summary.accessToken);
        setText("sessionToken", result.summary.sessionToken);
        setText("refreshToken", result.summary.refreshToken);
        setText("idToken", result.summary.idToken);
        $("status").textContent = "已生成";
        $("status").className = result.warnings.length ? "status warn" : "status ok";
        renderMessages(result.warnings);
        setReady(true);
        refreshPreview();
      } catch (error) {
        $("status").textContent = "生成失败";
        $("status").className = "status warn";
        renderMessages([], error.message || String(error));
        setReady(false);
      }
    }

    function downloadJson(fileName, payload) {
      const blob = new Blob([JSON.stringify(payload, null, 2) + "\n"], { type: "application/json;charset=utf-8" });
      const url = URL.createObjectURL(blob);
      const anchor = document.createElement("a");
      anchor.href = url;
      anchor.download = fileName;
      document.body.append(anchor);
      anchor.click();
      anchor.remove();
      URL.revokeObjectURL(url);
    }

    $("generateBtn").addEventListener("click", generate);
    $("clearBtn").addEventListener("click", () => {
      $("source").value = "";
      $("fileName").value = "";
      setReady(false);
      $("status").textContent = "待生成";
      $("status").className = "status";
      $("messages").innerHTML = "";
      for (const id of ["cpaName", "sub2Name", "email", "accountId", "planType", "expires", "accessToken", "sessionToken", "refreshToken", "idToken"]) {
        setText(id, "-");
      }
      state.cpa = null;
      state.sub2 = null;
      refreshPreview();
    });
    $("pasteBtn").addEventListener("click", async () => {
      try {
        $("source").value = await navigator.clipboard.readText();
        generate();
      } catch {
        renderMessages([], "浏览器未允许读取剪贴板,请手动粘贴。");
      }
    });
    $("downloadCpa").addEventListener("click", () => downloadJson(state.cpaFileName, state.cpa));
    $("downloadSub2").addEventListener("click", () => downloadJson(state.sub2FileName, state.sub2));
    $("downloadBoth").addEventListener("click", () => {
      downloadJson(state.cpaFileName, state.cpa);
      setTimeout(() => downloadJson(state.sub2FileName, state.sub2), 150);
    });
    $("copyFileNames").addEventListener("click", async () => {
      const text = state.cpaFileName + "\n" + state.sub2FileName;
      await navigator.clipboard.writeText(text);
      $("status").textContent = "已复制";
      $("status").className = "status ok";
    });
    $("previewType").addEventListener("change", refreshPreview);
  </script>
</body>
</html>`;

export default {
  async fetch(request) {
    if (request.method !== "GET" && request.method !== "HEAD") {
      return new Response("Method Not Allowed", {
        status: 405,
        headers: { Allow: "GET, HEAD" },
      });
    }

    return new Response(request.method === "HEAD" ? null : HTML, {
      headers: {
        "content-type": "text/html; charset=utf-8",
        "cache-control": "no-store",
        "x-content-type-options": "nosniff",
        "referrer-policy": "no-referrer",
      },
    });
  },
};

嫌麻烦也可以直接用我这个 https://sub-cpa.112525.xyz/

2 个帖子 - 2 位参与者

阅读完整话题

来源: LinuxDo 最新话题查看原文