Add Cockpit export format
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# ChatGPT Session to CPA / sub2api
|
||||
# ChatGPT Session to CPA / sub2api / Cockpit
|
||||
|
||||
纯前端单页面工具,用来把 ChatGPT Web 登录 session JSON 转换成 CPA 或 sub2api 可导入 JSON。
|
||||
纯前端单页面工具,用来把 ChatGPT Web 登录 session JSON 转换成 CPA、sub2api 或 Cockpit Tools 可导入 JSON。
|
||||
|
||||
## 在线使用
|
||||
|
||||
@@ -40,6 +40,7 @@ Plus 号可以用此方式导入中转站使用;Free 号的 access token 不
|
||||
|
||||
- `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`。
|
||||
- `Cockpit`:生成 Cockpit Tools Codex JSON 导入可识别的扁平 token 格式,包含 `id_token`、`access_token`、`refresh_token`、`account_id`、`email`、`expired` 等字段。
|
||||
ChatGPT Web session 通常不包含 CPA OAuth 文件里常见的 `refresh_token`,因此 access token 过期后不能自动刷新。
|
||||
|
||||
## 本地使用
|
||||
|
||||
+51
-5
@@ -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(3, minmax(0, 1fr));
|
||||
width: min(480px, 100%);
|
||||
padding: 4px;
|
||||
border: 1px solid var(--line);
|
||||
@@ -601,7 +621,12 @@
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="meta-row" aria-label="处理方式">
|
||||
<span class="meta-chip">浏览器本地解析</span>
|
||||
@@ -626,6 +651,7 @@
|
||||
<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>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="button button-secondary" id="copy-output" type="button" disabled>复制输出</button>
|
||||
@@ -687,7 +713,7 @@
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p class="notice" id="cpa-notice">
|
||||
ChatGPT Web session 一般没有 `refresh_token`;缺少真实 `id_token` 时会构造 CPA 可解析的占位 claims,实际调用仍依赖 access token。
|
||||
ChatGPT Web session 一般没有 `refresh_token`;缺少真实 `id_token` 时会构造 CPA/Cockpit 可解析的占位 claims,实际调用仍依赖 access token。
|
||||
</p>
|
||||
<div class="summary" aria-label="转换统计">
|
||||
<div class="stat">
|
||||
@@ -739,6 +765,7 @@
|
||||
const OUTPUT_LABELS = {
|
||||
sub2api: "sub2api",
|
||||
cpa: "CPA",
|
||||
cockpit: "Cockpit",
|
||||
};
|
||||
|
||||
const state = {
|
||||
@@ -1114,10 +1141,10 @@
|
||||
const idAuth = getOpenAIAuthSection(idPayload);
|
||||
const profile = getOpenAIProfileSection(payload);
|
||||
const expiresAt = firstNonEmpty(
|
||||
payload ? timestampFromUnixSeconds(payload.exp) : undefined,
|
||||
normalizeTimestamp(record.expires),
|
||||
normalizeTimestamp(record.expired),
|
||||
normalizeTimestamp(record.expires_at),
|
||||
payload ? timestampFromUnixSeconds(payload.exp) : undefined,
|
||||
);
|
||||
const email = firstNonEmpty(
|
||||
record.user?.email,
|
||||
@@ -1177,6 +1204,18 @@
|
||||
disabled: Boolean(record.disabled) || undefined,
|
||||
});
|
||||
|
||||
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"),
|
||||
platform: "openai",
|
||||
@@ -1209,6 +1248,7 @@
|
||||
name,
|
||||
expiresAt,
|
||||
cpa,
|
||||
cockpit,
|
||||
sub2apiAccount,
|
||||
};
|
||||
}
|
||||
@@ -1233,6 +1273,12 @@
|
||||
: 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);
|
||||
}
|
||||
|
||||
return buildSub2apiDocument(state.converted, now);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user