Compare commits

...
10 Commits
Author SHA1 Message Date
yyyyyynnn 01c66e4b23 Update README.md 2026-05-16 23:37:03 +08:00
yyyyyynnn ac8ee327e3 Add files via upload 2026-05-16 21:26:32 +08:00
Dehujiaogeli 81b6595306 Update README with Discord channel information
Added section about Discord channel for GPT information.
2026-05-12 18:19:25 +08:00
dave 1aceaa83fd Update CPA and Discord guidance 2026-05-12 17:13:32 +08:00
Long 59367033cb Add 9router OAuth export support
Adds 9router import/export support while preserving access-token-derived expiration and email-based account names.
2026-05-11 18:54:09 +08:00
dave 6181594dc0 Add Cockpit export format 2026-05-11 02:56:29 +08:00
dave b8bfdfb823 Update usage notes 2026-05-09 20:09:00 +08:00
dave 83f3e60462 Document Discord invite 2026-05-09 18:10:59 +08:00
dave d2398014ea Add Discord invite card 2026-05-09 18:05:27 +08:00
dave 8b628a1a76 Construct CPA id token when missing 2026-05-09 17:33:00 +08:00
2 changed files with 310 additions and 79 deletions
+16 -5
View File
@@ -1,15 +1,22 @@
# ChatGPT Session to CPA / sub2api # ChatGPT Session to CPA / sub2api / Cockpit / 9router
纯前端单页面工具,用来把 ChatGPT Web 登录 session JSON 转换成 CPAsub2api 可导入 JSON。 纯前端单页面工具,用来把 ChatGPT Web 登录 session JSON 转换成 CPAsub2api、Cockpit Tools 或 9router 可导入 JSON。
## 在线使用 ## 在线使用
### [**》》 点我直接使用 《《**](https://gtxx3600.github.io/GPTSession2CPAandSub2API/) ### [**》》 点我直接使用 《《**](https://json.chatai.codes/)
## 使用提示 ## 使用提示
Plus 号可以用此方式导入中转站使用;Free 号的 access token 不能用于调用接口。 Plus 号可以用此方式导入中转站使用;Free 号的 access token 不能用于调用接口。
本工具可用来解决 Codex OAuth 登录需要绑定手机的问题。Plus 账号通过 Web 登录后的 session 就能生成可导入中转站的账号 JSON 数据;这类数据没有 `refresh_token`,但 `access_token` 有效期通常足够长。
解释一下: plus激活前(free状态)或激活后(plus状态)获取的session在使用上没有区别(free时拿到的session, 激活plus后就可以调模型了),只是账号级别标识有点区别(标识为free or plus),不影响调模型。 换句话讲,不管你啥时候拿到的session, 用本项目转换导入中转站,只要账号当前激活了plus, 就能正常调模型接口。
本工具主要针对 Plus 账号适用,Free 账号即使转换了也没有权限调用 GPT 模型。GoPay 拉闸了,没法每天发 Plus 了;加入 Discord 频道免费获取 GPT 撸羊毛信息,然后配合本工具导入 CPA or Sub2API 使用。
## 支持输入 ## 支持输入
支持粘贴或拖入 ChatGPT Web session JSON,例如包含: 支持粘贴或拖入 ChatGPT Web session JSON,例如包含:
@@ -21,13 +28,17 @@ Plus 号可以用此方式导入中转站使用;Free 号的 access token 不
- `account.id` - `account.id`
- `account.planType` - `account.planType`
也支持粘贴或拖入 9router Codex OAuth JSON,例如包含 `accessToken``refreshToken``expiresAt``providerSpecificData.chatgptAccountId``providerSpecificData.chatgptPlanType`
页面也会尝试从 `accessToken` 的 JWT payload 中补充邮箱、账号 ID、用户 ID、计划类型和过期时间。 页面也会尝试从 `accessToken` 的 JWT payload 中补充邮箱、账号 ID、用户 ID、计划类型和过期时间。
## 输出格式 ## 输出格式
- `CPA`:生成最小 Codex CPA JSON,包含 `type: "codex"``access_token``session_token``email``name`、过期时间等可推导字段 - `CPA`:生成 Codex CPA auth JSON,包含 `type: "codex"``access_token``session_token``id_token``email``account_id`、套餐和过期时间等字段;缺少真实 `id_token` 时会根据 session 与 access token claims 构造 CPA 可解析的占位 claims
- `sub2api`:生成参考 `CPA2sub2API` 项目的 `exported_at/proxies/accounts` 结构,账号平台为 `openai`,类型为 `oauth` - `sub2api`:生成参考 `CPA2sub2API` 项目的 `exported_at/proxies/accounts` 结构,账号平台为 `openai`,类型为 `oauth`
ChatGPT Web session 通常不包含 CPA OAuth 文件里常见的 `refresh_token``id_token`,因此 CPA 输出不会伪造这些字段。 - `Cockpit`:生成 Cockpit Tools Codex JSON 导入可识别的扁平 token 格式,包含 `id_token``access_token``refresh_token``account_id``email``expired`字段。
- `9router`:生成 9router Codex OAuth JSON,包含 `accessToken``refreshToken``expiresAt``providerSpecificData``provider``authType``priority``isActive``createdAt``updatedAt` 等字段。
ChatGPT Web session 通常不包含 CPA OAuth 文件里常见的 `refresh_token`,因此 access token 过期后不能自动刷新。
## 本地使用 ## 本地使用
+294 -74
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ChatGPT Session -> CPA / sub2api</title> <title>ChatGPT Session -> CPA / sub2api / Cockpit</title>
<meta name="theme-color" content="#24505a" /> <meta name="theme-color" content="#24505a" />
<link rel="icon" href="./favicon.svg" type="image/svg+xml" sizes="any" /> <link rel="icon" href="./favicon.svg" type="image/svg+xml" sizes="any" />
<style> <style>
@@ -90,6 +90,26 @@
letter-spacing: 0; letter-spacing: 0;
} }
.title-targets {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
.title-targets span {
display: inline-flex;
align-items: center;
min-height: 30px;
padding: 5px 10px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--surface);
color: var(--accent);
font-size: 0.86rem;
font-weight: 800;
}
.meta-row { .meta-row {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -138,7 +158,7 @@
.segmented { .segmented {
display: inline-grid; display: inline-grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
width: min(480px, 100%); width: min(480px, 100%);
padding: 4px; padding: 4px;
border: 1px solid var(--line); border: 1px solid var(--line);
@@ -245,30 +265,6 @@
padding: 14px 16px 16px; padding: 14px 16px 16px;
} }
.dropzone {
display: grid;
min-height: 128px;
place-items: center;
margin-bottom: 12px;
border: 1px dashed var(--line-strong);
border-radius: var(--radius);
background: var(--surface-soft);
color: var(--muted);
text-align: center;
}
.dropzone.is-dragover {
border-color: var(--accent);
background: var(--accent-soft);
color: var(--accent);
}
.dropzone strong {
display: block;
margin-bottom: 4px;
color: var(--text);
}
.session-guide { .session-guide {
margin: 0 0 12px; margin: 0 0 12px;
padding: 13px 14px; padding: 13px 14px;
@@ -306,6 +302,71 @@
font-weight: 700; font-weight: 700;
} }
.telegram-card {
position: relative;
display: grid;
grid-template-columns: auto minmax(0, 1fr);
gap: 12px 14px;
align-items: start;
margin-bottom: 12px;
padding: 14px 14px 52px;
border: 1px solid rgba(0, 136, 204, 0.28);
border-radius: var(--radius);
background: #f1f9ff;
color: #173d52;
text-decoration: none;
}
.telegram-card:hover {
border-color: rgba(0, 136, 204, 0.5);
background: #eaf6ff;
}
.telegram-icon {
display: grid;
width: 44px;
height: 44px;
place-items: center;
border-radius: var(--radius);
background: #0088cc;
color: #fff;
}
.telegram-icon svg {
width: 27px;
height: 27px;
fill: currentColor;
}
.telegram-copy strong {
display: block;
margin-bottom: 4px;
color: #12384d;
font-size: 0.98rem;
line-height: 1.35;
}
.telegram-copy span {
display: block;
color: #3f5f70;
font-size: 0.88rem;
line-height: 1.45;
}
.telegram-action {
position: absolute;
right: 14px;
bottom: 14px;
min-height: 32px;
padding: 6px 12px;
border-radius: 6px;
background: #0088cc;
color: #fff;
font-weight: 800;
white-space: nowrap;
box-shadow: 0 8px 18px rgba(0, 136, 204, 0.22);
}
.input-actions { .input-actions {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -530,6 +591,10 @@
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.telegram-card {
grid-template-columns: auto minmax(0, 1fr);
}
.col-expiry, .col-expiry,
.col-type, .col-type,
td:nth-child(3), td:nth-child(3),
@@ -556,7 +621,13 @@
<header class="topbar"> <header class="topbar">
<div class="title-block"> <div class="title-block">
<p class="eyebrow">Local JSON Converter</p> <p class="eyebrow">Local JSON Converter</p>
<h1>ChatGPT Session -> CPA / sub2api</h1> <h1>ChatGPT Session Converter</h1>
<div class="title-targets" aria-label="支持输出格式">
<span>CPA</span>
<span>sub2api</span>
<span>Cockpit</span>
<span>9router</span>
</div>
</div> </div>
<div class="meta-row" aria-label="处理方式"> <div class="meta-row" aria-label="处理方式">
<span class="meta-chip">浏览器本地解析</span> <span class="meta-chip">浏览器本地解析</span>
@@ -564,7 +635,7 @@
<span class="meta-chip">不写入存储</span> <span class="meta-chip">不写入存储</span>
<a <a
class="meta-chip github-link" class="meta-chip github-link"
href="https://github.com/gtxx3600/GPTSession2CPAandSub2API" href="https://github.com/yynxxxxx/GPTSession2CPAandSub2API"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
aria-label="本页面代码开源" aria-label="本页面代码开源"
@@ -581,6 +652,8 @@
<div class="segmented" role="group" aria-label="输出格式"> <div class="segmented" role="group" aria-label="输出格式">
<button type="button" data-format="sub2api" aria-pressed="true">sub2api</button> <button type="button" data-format="sub2api" aria-pressed="true">sub2api</button>
<button type="button" data-format="cpa" aria-pressed="false">CPA</button> <button type="button" data-format="cpa" aria-pressed="false">CPA</button>
<button type="button" data-format="cockpit" aria-pressed="false">Cockpit</button>
<button type="button" data-format="9router" aria-pressed="false">9router</button>
</div> </div>
<div class="actions"> <div class="actions">
<button class="button button-secondary" id="copy-output" type="button" disabled>复制输出</button> <button class="button button-secondary" id="copy-output" type="button" disabled>复制输出</button>
@@ -606,12 +679,18 @@
</p> </p>
<p class="session-warning">这段 JSON 包含 accessToken 和 sessionToken,等同敏感登录凭证,不要发给别人。</p> <p class="session-warning">这段 JSON 包含 accessToken 和 sessionToken,等同敏感登录凭证,不要发给别人。</p>
</div> </div>
<div class="dropzone" id="dropzone" tabindex="0" role="button"> <a class="telegram-card" href="https://t.me/Geminivip1" target="_blank" rel="noreferrer">
<div> <span class="telegram-icon" aria-hidden="true">
<strong>拖入 JSON 文件</strong> <svg viewBox="0 0 24 24">
<span>也可以点击选择文件</span> <path d="M22.44 2.13c.42-.17.85.19.74.63l-3.28 15.47c-.1.49-.66.69-1.05.38l-4.98-3.67-2.4 2.31c-.27.27-.49.49-1.01.49l.36-5.1 9.29-8.4c.4-.36-.09-.56-.63-.2L7.99 11.27 3.04 9.72c-1.08-.34-1.1-1.08.22-1.59l19.18-6Z" />
</div> </svg>
</div> </span>
<span class="telegram-copy">
<strong>加入 Telegram 群免费获取 GPT 撸羊毛信息</strong>
<span>本工具主要针对 Plus 账号适用,Free 账号即使转换了也没有权限调用 GPT 模型。GoPay 拉闸了,没法每天发 Plus 了;加入 Telegram 群免费获取 GPT 撸羊毛信息,然后配合本工具导入 CPA or Sub2API 使用。</span>
</span>
<span class="telegram-action">加入群组</span>
</a>
<input class="hidden" id="file-input" type="file" accept=".json,application/json" multiple /> <input class="hidden" id="file-input" type="file" accept=".json,application/json" multiple />
<div class="input-actions"> <div class="input-actions">
<button class="button button-secondary" id="pick-files" type="button">选择文件</button> <button class="button button-secondary" id="pick-files" type="button">选择文件</button>
@@ -636,7 +715,7 @@
</div> </div>
<div class="panel-body"> <div class="panel-body">
<p class="notice" id="cpa-notice"> <p class="notice" id="cpa-notice">
ChatGPT Web session 一般没有 `refresh_token` `id_token`CPA 输出只包含 access token 和可推导字段 ChatGPT Web session 一般没有 `refresh_token`;缺少真实 `id_token` 时会构造 CPA/Cockpit 可解析的占位 claims,实际调用仍依赖 access token。
</p> </p>
<div class="summary" aria-label="转换统计"> <div class="summary" aria-label="转换统计">
<div class="stat"> <div class="stat">
@@ -688,6 +767,8 @@
const OUTPUT_LABELS = { const OUTPUT_LABELS = {
sub2api: "sub2api", sub2api: "sub2api",
cpa: "CPA", cpa: "CPA",
cockpit: "Cockpit",
"9router": "9router",
}; };
const state = { const state = {
@@ -704,7 +785,6 @@
copyOutput: document.querySelector("#copy-output"), copyOutput: document.querySelector("#copy-output"),
cpaNotice: document.querySelector("#cpa-notice"), cpaNotice: document.querySelector("#cpa-notice"),
downloadOutput: document.querySelector("#download-output"), downloadOutput: document.querySelector("#download-output"),
dropzone: document.querySelector("#dropzone"),
fileInput: document.querySelector("#file-input"), fileInput: document.querySelector("#file-input"),
formatButtons: Array.from(document.querySelectorAll("[data-format]")), formatButtons: Array.from(document.querySelectorAll("[data-format]")),
input: document.querySelector("#session-input"), input: document.querySelector("#session-input"),
@@ -765,6 +845,18 @@
return new TextDecoder().decode(bytes); return new TextDecoder().decode(bytes);
} }
function bytesToBase64Url(bytes) {
let binary = "";
for (let index = 0; index < bytes.length; index += 0x8000) {
binary += String.fromCharCode(...bytes.subarray(index, index + 0x8000));
}
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
}
function encodeBase64UrlJson(value) {
return bytesToBase64Url(new TextEncoder().encode(JSON.stringify(value)));
}
function parseJwtPayload(token) { function parseJwtPayload(token) {
if (typeof token !== "string" || token.trim() === "") { if (typeof token !== "string" || token.trim() === "") {
return undefined; return undefined;
@@ -829,6 +921,51 @@
return Number.isNaN(date.getTime()) ? undefined : date.toISOString(); return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
} }
function epochSecondsFromValue(value) {
if (value === undefined || value === null || value === "") {
return 0;
}
const numeric = Number(value);
if (Number.isFinite(numeric)) {
return Math.trunc(numeric > 1e11 ? numeric / 1000 : numeric);
}
const parsed = Date.parse(String(value));
return Number.isFinite(parsed) ? Math.trunc(parsed / 1000) : 0;
}
function buildSyntheticCodexIdToken(email, accountId, planType, userId, expiresAt) {
if (!accountId) {
return undefined;
}
const now = Math.trunc(Date.now() / 1000);
const authInfo = { chatgpt_account_id: accountId };
const expires = epochSecondsFromValue(expiresAt) || now + 90 * 24 * 60 * 60;
if (planType) {
authInfo.chatgpt_plan_type = planType;
}
if (userId) {
authInfo.chatgpt_user_id = userId;
authInfo.user_id = userId;
}
const payload = {
iat: now,
exp: expires,
"https://api.openai.com/auth": authInfo,
};
if (email) {
payload.email = email;
}
return `${encodeBase64UrlJson({ alg: "none", typ: "JWT", cpa_synthetic: true })}.${encodeBase64UrlJson(payload)}.`;
}
function getExpiresIn(expiresAt, now = new Date()) { function getExpiresIn(expiresAt, now = new Date()) {
if (!expiresAt) { if (!expiresAt) {
return undefined; return undefined;
@@ -927,9 +1064,22 @@
} }
visited.add(item); visited.add(item);
const token = firstNonEmpty(item.accessToken, item.access_token, item.token?.accessToken, item.credentials?.access_token); const token = firstNonEmpty(
const hasUser = isPlainObject(item.user) || firstNonEmpty(item.email, item.name); item.accessToken,
if (token && hasUser) { item.access_token,
item.token?.accessToken,
item.token?.access_token,
item.credentials?.accessToken,
item.credentials?.access_token,
);
const hasIdentity = isPlainObject(item.user) || firstNonEmpty(
item.email,
item.name,
item.providerSpecificData?.chatgptAccountId,
item.providerSpecificData?.chatgpt_account_id,
item.id,
);
if (token && hasIdentity) {
found.push({ value: item, sourceName, path }); found.push({ value: item, sourceName, path });
return; return;
} }
@@ -974,6 +1124,8 @@
record.accessToken, record.accessToken,
record.access_token, record.access_token,
record.token?.accessToken, record.token?.accessToken,
record.token?.access_token,
record.credentials?.accessToken,
record.credentials?.access_token, record.credentials?.access_token,
); );
if (!accessToken) { if (!accessToken) {
@@ -986,58 +1138,114 @@
record.token?.session_token, record.token?.session_token,
record.credentials?.session_token, record.credentials?.session_token,
); );
const refreshToken = firstNonEmpty(
record.refreshToken,
record.refresh_token,
record.token?.refreshToken,
record.token?.refresh_token,
record.credentials?.refresh_token,
);
const inputIdToken = firstNonEmpty(
record.idToken,
record.id_token,
record.token?.idToken,
record.token?.id_token,
record.credentials?.id_token,
);
const payload = parseJwtPayload(accessToken); const payload = parseJwtPayload(accessToken);
const idPayload = parseJwtPayload(inputIdToken);
const auth = getOpenAIAuthSection(payload); const auth = getOpenAIAuthSection(payload);
const idAuth = getOpenAIAuthSection(idPayload);
const profile = getOpenAIProfileSection(payload); const profile = getOpenAIProfileSection(payload);
const expiresAt = firstNonEmpty( const expiresAt = firstNonEmpty(
payload ? timestampFromUnixSeconds(payload.exp) : undefined,
normalizeTimestamp(record.expires), normalizeTimestamp(record.expires),
normalizeTimestamp(record.expiresAt),
normalizeTimestamp(record.expired), normalizeTimestamp(record.expired),
normalizeTimestamp(record.expires_at), normalizeTimestamp(record.expires_at),
payload ? timestampFromUnixSeconds(payload.exp) : undefined,
); );
const email = firstNonEmpty( const email = firstNonEmpty(
record.user?.email, record.user?.email,
record.email, record.email,
record.credentials?.email, record.credentials?.email,
record.providerSpecificData?.email,
profile.email, profile.email,
idPayload?.email,
payload?.email, payload?.email,
); );
const accountId = firstNonEmpty( const accountId = firstNonEmpty(
record.account?.id, record.account?.id,
record.account_id, record.account_id,
record.chatgptAccountId,
record.providerSpecificData?.chatgptAccountId,
record.providerSpecificData?.chatgpt_account_id,
record.credentials?.chatgpt_account_id, record.credentials?.chatgpt_account_id,
auth.chatgpt_account_id, auth.chatgpt_account_id,
idAuth.chatgpt_account_id,
record.provider === "codex" ? record.id : undefined,
); );
const userId = firstNonEmpty( const userId = firstNonEmpty(
record.user?.id, record.user?.id,
record.user_id, record.user_id,
record.chatgptUserId,
record.providerSpecificData?.chatgptUserId,
record.providerSpecificData?.chatgpt_user_id,
auth.chatgpt_user_id, auth.chatgpt_user_id,
auth.user_id, auth.user_id,
idAuth.chatgpt_user_id,
idAuth.user_id,
); );
const planType = firstNonEmpty( const planType = firstNonEmpty(
record.account?.planType, record.account?.planType,
record.account?.plan_type, record.account?.plan_type,
record.planType,
record.plan_type, record.plan_type,
record.providerSpecificData?.chatgptPlanType,
record.providerSpecificData?.chatgpt_plan_type,
record.credentials?.plan_type, record.credentials?.plan_type,
auth.chatgpt_plan_type, auth.chatgpt_plan_type,
idAuth.chatgpt_plan_type,
); );
const exportedAt = normalizeTimestamp(options.now || new Date()); const exportedAt = normalizeTimestamp(options.now || new Date());
const expiresIn = getExpiresIn(expiresAt, options.now || new Date()); const expiresIn = getExpiresIn(expiresAt, options.now || new Date());
const sourceName = firstNonEmpty(options.sourceName, "pasted-json"); const sourceName = firstNonEmpty(options.sourceName, "pasted-json");
const sourceType = record.provider === "codex" && record.authType === "oauth" ? "9router" : "chatgpt_web_session";
const name = firstNonEmpty(email, sourceName, "ChatGPT Account"); const name = firstNonEmpty(email, sourceName, "ChatGPT Account");
const syntheticIdToken = !inputIdToken
? buildSyntheticCodexIdToken(email, accountId, planType, userId, expiresAt)
: undefined;
const idToken = firstNonEmpty(inputIdToken, syntheticIdToken);
const cpa = stripUnavailable({ const cpa = Object.fromEntries(Object.entries({
type: "codex", type: "codex",
access_token: accessToken,
session_token: sessionToken,
account_id: accountId, account_id: accountId,
chatgpt_account_id: accountId,
email, email,
name, name,
expired: expiresAt,
last_refresh: exportedAt,
plan_type: planType, plan_type: planType,
}); chatgpt_plan_type: planType,
id_token: idToken,
id_token_synthetic: Boolean(syntheticIdToken) || undefined,
access_token: accessToken,
refresh_token: refreshToken || "",
session_token: sessionToken,
last_refresh: exportedAt,
expired: expiresAt,
disabled: Boolean(record.disabled) || undefined,
}).filter(([, value]) => value !== undefined && value !== null));
const cockpit = {
type: "codex",
id_token: idToken,
access_token: accessToken,
refresh_token: refreshToken || "",
account_id: accountId,
last_refresh: exportedAt,
email,
expired: expiresAt,
account_note: firstNonEmpty(record.account_note, record.accountInfo, record.account_info, record.note, record.notes, record.remark),
};
const sub2apiAccount = stripUnavailable({ const sub2apiAccount = stripUnavailable({
name: firstNonEmpty(name, email, sourceName, "ChatGPT Account"), name: firstNonEmpty(name, email, sourceName, "ChatGPT Account"),
@@ -1059,10 +1267,34 @@
email_key: toEmailKey(email), email_key: toEmailKey(email),
name, name,
auth_provider: firstNonEmpty(record.authProvider, record.auth_provider), auth_provider: firstNonEmpty(record.authProvider, record.auth_provider),
source: "chatgpt_web_session", source: sourceType,
last_refresh: exportedAt, last_refresh: exportedAt,
}, },
}); });
const priority = Number.isFinite(Number(record.priority)) ? Number(record.priority) : 9;
const isActive = typeof record.isActive === "boolean" ? record.isActive : !Boolean(record.disabled);
const createdAt = normalizeTimestamp(record.createdAt) || exportedAt;
const updatedAt = normalizeTimestamp(record.updatedAt) || exportedAt;
const nineRouter = stripUnavailable({
accessToken,
refreshToken,
expiresAt,
testStatus: firstNonEmpty(record.testStatus, record.test_status, "active"),
expiresIn,
providerSpecificData: {
chatgptAccountId: accountId,
chatgptPlanType: planType,
},
id: accountId,
provider: "codex",
authType: "oauth",
name,
email,
priority,
isActive,
createdAt,
updatedAt,
});
return { return {
sourceName, sourceName,
@@ -1071,6 +1303,8 @@
name, name,
expiresAt, expiresAt,
cpa, cpa,
cockpit,
nineRouter,
sub2apiAccount, sub2apiAccount,
}; };
} }
@@ -1095,6 +1329,18 @@
: state.converted.map((item) => item.cpa); : state.converted.map((item) => item.cpa);
} }
if (state.format === "cockpit") {
return state.converted.length === 1
? state.converted[0].cockpit
: state.converted.map((item) => item.cockpit);
}
if (state.format === "9router") {
return state.converted.length === 1
? state.converted[0].nineRouter
: state.converted.map((item) => item.nineRouter);
}
return buildSub2apiDocument(state.converted, now); return buildSub2apiDocument(state.converted, now);
} }
@@ -1156,7 +1402,7 @@
elements.statErrors.textContent = String(state.skipped.length); elements.statErrors.textContent = String(state.skipped.length);
elements.statFormat.textContent = OUTPUT_LABELS[state.format]; elements.statFormat.textContent = OUTPUT_LABELS[state.format];
elements.outputSubtitle.textContent = `当前输出为 ${OUTPUT_LABELS[state.format]} 导入 JSON。`; elements.outputSubtitle.textContent = `当前输出为 ${OUTPUT_LABELS[state.format]} 导入 JSON。`;
elements.cpaNotice.style.display = state.format === "sub2api" ? "none" : "block"; elements.cpaNotice.style.display = state.format === "cpa" || state.format === "cockpit" ? "block" : "none";
renderAccounts(); renderAccounts();
renderIssues(); renderIssues();
@@ -1337,7 +1583,6 @@
elements.copyOutput.addEventListener("click", copyOutput); elements.copyOutput.addEventListener("click", copyOutput);
elements.downloadOutput.addEventListener("click", downloadOutput); elements.downloadOutput.addEventListener("click", downloadOutput);
elements.pickFiles.addEventListener("click", () => elements.fileInput.click()); elements.pickFiles.addEventListener("click", () => elements.fileInput.click());
elements.dropzone.addEventListener("click", () => elements.fileInput.click());
elements.fileInput.addEventListener("change", (event) => { elements.fileInput.addEventListener("change", (event) => {
readFiles(event.target.files); readFiles(event.target.files);
event.target.value = ""; event.target.value = "";
@@ -1353,31 +1598,6 @@
scheduleConvert(); scheduleConvert();
}); });
["dragenter", "dragover"].forEach((eventName) => {
elements.dropzone.addEventListener(eventName, (event) => {
event.preventDefault();
elements.dropzone.classList.add("is-dragover");
});
});
["dragleave", "drop"].forEach((eventName) => {
elements.dropzone.addEventListener(eventName, (event) => {
event.preventDefault();
elements.dropzone.classList.remove("is-dragover");
});
});
elements.dropzone.addEventListener("drop", (event) => {
readFiles(event.dataTransfer.files);
});
elements.dropzone.addEventListener("keydown", (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
elements.fileInput.click();
}
});
updateOutput(); updateOutput();
})(); })();
</script> </script>