Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01c66e4b23 | ||
|
|
ac8ee327e3 | ||
|
|
81b6595306 | ||
|
|
1aceaa83fd | ||
|
|
59367033cb | ||
|
|
6181594dc0 | ||
|
|
b8bfdfb823 | ||
|
|
83f3e60462 | ||
|
|
d2398014ea | ||
|
|
8b628a1a76 |
@@ -1,15 +1,22 @@
|
||||
# ChatGPT Session to CPA / sub2api
|
||||
# ChatGPT Session to CPA / sub2api / Cockpit / 9router
|
||||
|
||||
纯前端单页面工具,用来把 ChatGPT Web 登录 session JSON 转换成 CPA 或 sub2api 可导入 JSON。
|
||||
纯前端单页面工具,用来把 ChatGPT Web 登录 session JSON 转换成 CPA、sub2api、Cockpit Tools 或 9router 可导入 JSON。
|
||||
|
||||
## 在线使用
|
||||
|
||||
### [**》》 点我直接使用 《《**](https://gtxx3600.github.io/GPTSession2CPAandSub2API/)
|
||||
### [**》》 点我直接使用 《《**](https://json.chatai.codes/)
|
||||
|
||||
## 使用提示
|
||||
|
||||
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,例如包含:
|
||||
@@ -21,13 +28,17 @@ Plus 号可以用此方式导入中转站使用;Free 号的 access token 不
|
||||
- `account.id`
|
||||
- `account.planType`
|
||||
|
||||
也支持粘贴或拖入 9router Codex OAuth JSON,例如包含 `accessToken`、`refreshToken`、`expiresAt`、`providerSpecificData.chatgptAccountId` 和 `providerSpecificData.chatgptPlanType`。
|
||||
|
||||
页面也会尝试从 `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`。
|
||||
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
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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" />
|
||||
<link rel="icon" href="./favicon.svg" type="image/svg+xml" sizes="any" />
|
||||
<style>
|
||||
@@ -90,6 +90,26 @@
|
||||
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 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -138,7 +158,7 @@
|
||||
|
||||
.segmented {
|
||||
display: inline-grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
width: min(480px, 100%);
|
||||
padding: 4px;
|
||||
border: 1px solid var(--line);
|
||||
@@ -245,30 +265,6 @@
|
||||
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 {
|
||||
margin: 0 0 12px;
|
||||
padding: 13px 14px;
|
||||
@@ -306,6 +302,71 @@
|
||||
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 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -530,6 +591,10 @@
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.telegram-card {
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.col-expiry,
|
||||
.col-type,
|
||||
td:nth-child(3),
|
||||
@@ -556,7 +621,13 @@
|
||||
<header class="topbar">
|
||||
<div class="title-block">
|
||||
<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 class="meta-row" aria-label="处理方式">
|
||||
<span class="meta-chip">浏览器本地解析</span>
|
||||
@@ -564,7 +635,7 @@
|
||||
<span class="meta-chip">不写入存储</span>
|
||||
<a
|
||||
class="meta-chip github-link"
|
||||
href="https://github.com/gtxx3600/GPTSession2CPAandSub2API"
|
||||
href="https://github.com/yynxxxxx/GPTSession2CPAandSub2API"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label="本页面代码开源"
|
||||
@@ -581,6 +652,8 @@
|
||||
<div class="segmented" role="group" aria-label="输出格式">
|
||||
<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="cockpit" aria-pressed="false">Cockpit</button>
|
||||
<button type="button" data-format="9router" aria-pressed="false">9router</button>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="button button-secondary" id="copy-output" type="button" disabled>复制输出</button>
|
||||
@@ -606,12 +679,18 @@
|
||||
</p>
|
||||
<p class="session-warning">这段 JSON 包含 accessToken 和 sessionToken,等同敏感登录凭证,不要发给别人。</p>
|
||||
</div>
|
||||
<div class="dropzone" id="dropzone" tabindex="0" role="button">
|
||||
<div>
|
||||
<strong>拖入 JSON 文件</strong>
|
||||
<span>也可以点击选择文件</span>
|
||||
</div>
|
||||
</div>
|
||||
<a class="telegram-card" href="https://t.me/Geminivip1" target="_blank" rel="noreferrer">
|
||||
<span class="telegram-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<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" />
|
||||
</svg>
|
||||
</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 />
|
||||
<div class="input-actions">
|
||||
<button class="button button-secondary" id="pick-files" type="button">选择文件</button>
|
||||
@@ -636,7 +715,7 @@
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<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>
|
||||
<div class="summary" aria-label="转换统计">
|
||||
<div class="stat">
|
||||
@@ -688,6 +767,8 @@
|
||||
const OUTPUT_LABELS = {
|
||||
sub2api: "sub2api",
|
||||
cpa: "CPA",
|
||||
cockpit: "Cockpit",
|
||||
"9router": "9router",
|
||||
};
|
||||
|
||||
const state = {
|
||||
@@ -704,7 +785,6 @@
|
||||
copyOutput: document.querySelector("#copy-output"),
|
||||
cpaNotice: document.querySelector("#cpa-notice"),
|
||||
downloadOutput: document.querySelector("#download-output"),
|
||||
dropzone: document.querySelector("#dropzone"),
|
||||
fileInput: document.querySelector("#file-input"),
|
||||
formatButtons: Array.from(document.querySelectorAll("[data-format]")),
|
||||
input: document.querySelector("#session-input"),
|
||||
@@ -765,6 +845,18 @@
|
||||
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) {
|
||||
if (typeof token !== "string" || token.trim() === "") {
|
||||
return undefined;
|
||||
@@ -829,6 +921,51 @@
|
||||
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()) {
|
||||
if (!expiresAt) {
|
||||
return undefined;
|
||||
@@ -927,9 +1064,22 @@
|
||||
}
|
||||
visited.add(item);
|
||||
|
||||
const token = firstNonEmpty(item.accessToken, item.access_token, item.token?.accessToken, item.credentials?.access_token);
|
||||
const hasUser = isPlainObject(item.user) || firstNonEmpty(item.email, item.name);
|
||||
if (token && hasUser) {
|
||||
const token = firstNonEmpty(
|
||||
item.accessToken,
|
||||
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 });
|
||||
return;
|
||||
}
|
||||
@@ -974,6 +1124,8 @@
|
||||
record.accessToken,
|
||||
record.access_token,
|
||||
record.token?.accessToken,
|
||||
record.token?.access_token,
|
||||
record.credentials?.accessToken,
|
||||
record.credentials?.access_token,
|
||||
);
|
||||
if (!accessToken) {
|
||||
@@ -986,58 +1138,114 @@
|
||||
record.token?.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 idPayload = parseJwtPayload(inputIdToken);
|
||||
const auth = getOpenAIAuthSection(payload);
|
||||
const idAuth = getOpenAIAuthSection(idPayload);
|
||||
const profile = getOpenAIProfileSection(payload);
|
||||
const expiresAt = firstNonEmpty(
|
||||
payload ? timestampFromUnixSeconds(payload.exp) : undefined,
|
||||
normalizeTimestamp(record.expires),
|
||||
normalizeTimestamp(record.expiresAt),
|
||||
normalizeTimestamp(record.expired),
|
||||
normalizeTimestamp(record.expires_at),
|
||||
payload ? timestampFromUnixSeconds(payload.exp) : undefined,
|
||||
);
|
||||
const email = firstNonEmpty(
|
||||
record.user?.email,
|
||||
record.email,
|
||||
record.credentials?.email,
|
||||
record.providerSpecificData?.email,
|
||||
profile.email,
|
||||
idPayload?.email,
|
||||
payload?.email,
|
||||
);
|
||||
const accountId = firstNonEmpty(
|
||||
record.account?.id,
|
||||
record.account_id,
|
||||
record.chatgptAccountId,
|
||||
record.providerSpecificData?.chatgptAccountId,
|
||||
record.providerSpecificData?.chatgpt_account_id,
|
||||
record.credentials?.chatgpt_account_id,
|
||||
auth.chatgpt_account_id,
|
||||
idAuth.chatgpt_account_id,
|
||||
record.provider === "codex" ? record.id : undefined,
|
||||
);
|
||||
const userId = firstNonEmpty(
|
||||
record.user?.id,
|
||||
record.user_id,
|
||||
record.chatgptUserId,
|
||||
record.providerSpecificData?.chatgptUserId,
|
||||
record.providerSpecificData?.chatgpt_user_id,
|
||||
auth.chatgpt_user_id,
|
||||
auth.user_id,
|
||||
idAuth.chatgpt_user_id,
|
||||
idAuth.user_id,
|
||||
);
|
||||
const planType = firstNonEmpty(
|
||||
record.account?.planType,
|
||||
record.account?.plan_type,
|
||||
record.planType,
|
||||
record.plan_type,
|
||||
record.providerSpecificData?.chatgptPlanType,
|
||||
record.providerSpecificData?.chatgpt_plan_type,
|
||||
record.credentials?.plan_type,
|
||||
auth.chatgpt_plan_type,
|
||||
idAuth.chatgpt_plan_type,
|
||||
);
|
||||
const exportedAt = normalizeTimestamp(options.now || new Date());
|
||||
const expiresIn = getExpiresIn(expiresAt, options.now || new Date());
|
||||
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 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",
|
||||
access_token: accessToken,
|
||||
session_token: sessionToken,
|
||||
account_id: accountId,
|
||||
chatgpt_account_id: accountId,
|
||||
email,
|
||||
name,
|
||||
expired: expiresAt,
|
||||
last_refresh: exportedAt,
|
||||
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({
|
||||
name: firstNonEmpty(name, email, sourceName, "ChatGPT Account"),
|
||||
@@ -1059,10 +1267,34 @@
|
||||
email_key: toEmailKey(email),
|
||||
name,
|
||||
auth_provider: firstNonEmpty(record.authProvider, record.auth_provider),
|
||||
source: "chatgpt_web_session",
|
||||
source: sourceType,
|
||||
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 {
|
||||
sourceName,
|
||||
@@ -1071,6 +1303,8 @@
|
||||
name,
|
||||
expiresAt,
|
||||
cpa,
|
||||
cockpit,
|
||||
nineRouter,
|
||||
sub2apiAccount,
|
||||
};
|
||||
}
|
||||
@@ -1095,6 +1329,18 @@
|
||||
: 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);
|
||||
}
|
||||
|
||||
@@ -1156,7 +1402,7 @@
|
||||
elements.statErrors.textContent = String(state.skipped.length);
|
||||
elements.statFormat.textContent = OUTPUT_LABELS[state.format];
|
||||
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();
|
||||
renderIssues();
|
||||
@@ -1337,7 +1583,6 @@
|
||||
elements.copyOutput.addEventListener("click", copyOutput);
|
||||
elements.downloadOutput.addEventListener("click", downloadOutput);
|
||||
elements.pickFiles.addEventListener("click", () => elements.fileInput.click());
|
||||
elements.dropzone.addEventListener("click", () => elements.fileInput.click());
|
||||
elements.fileInput.addEventListener("change", (event) => {
|
||||
readFiles(event.target.files);
|
||||
event.target.value = "";
|
||||
@@ -1353,31 +1598,6 @@
|
||||
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();
|
||||
})();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user