Add Cockpit export format

This commit is contained in:
dave
2026-05-11 02:56:29 +08:00
parent b8bfdfb823
commit 6181594dc0
2 changed files with 54 additions and 7 deletions
+3 -2
View File
@@ -1,6 +1,6 @@
# ChatGPT Session to CPA / sub2api
# ChatGPT Session to CPA / sub2api / Cockpit
纯前端单页面工具,用来把 ChatGPT Web 登录 session JSON 转换成 CPAsub2api 可导入 JSON。
纯前端单页面工具,用来把 ChatGPT Web 登录 session JSON 转换成 CPAsub2api 或 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
View File
@@ -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);
}