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。
|
- `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`。
|
||||||
|
- `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 过期后不能自动刷新。
|
ChatGPT Web session 通常不包含 CPA OAuth 文件里常见的 `refresh_token`,因此 access token 过期后不能自动刷新。
|
||||||
|
|
||||||
## 本地使用
|
## 本地使用
|
||||||
|
|||||||
+51
-5
@@ -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(3, 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);
|
||||||
@@ -601,7 +621,12 @@
|
|||||||
<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>
|
||||||
|
</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>
|
||||||
@@ -626,6 +651,7 @@
|
|||||||
<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>
|
||||||
</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>
|
||||||
@@ -687,7 +713,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 可解析的占位 claims,实际调用仍依赖 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">
|
||||||
@@ -739,6 +765,7 @@
|
|||||||
const OUTPUT_LABELS = {
|
const OUTPUT_LABELS = {
|
||||||
sub2api: "sub2api",
|
sub2api: "sub2api",
|
||||||
cpa: "CPA",
|
cpa: "CPA",
|
||||||
|
cockpit: "Cockpit",
|
||||||
};
|
};
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
@@ -1114,10 +1141,10 @@
|
|||||||
const idAuth = getOpenAIAuthSection(idPayload);
|
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.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,
|
||||||
@@ -1177,6 +1204,18 @@
|
|||||||
disabled: Boolean(record.disabled) || undefined,
|
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({
|
const sub2apiAccount = stripUnavailable({
|
||||||
name: firstNonEmpty(name, email, sourceName, "ChatGPT Account"),
|
name: firstNonEmpty(name, email, sourceName, "ChatGPT Account"),
|
||||||
platform: "openai",
|
platform: "openai",
|
||||||
@@ -1209,6 +1248,7 @@
|
|||||||
name,
|
name,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
cpa,
|
cpa,
|
||||||
|
cockpit,
|
||||||
sub2apiAccount,
|
sub2apiAccount,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1233,6 +1273,12 @@
|
|||||||
: 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);
|
||||||
|
}
|
||||||
|
|
||||||
return buildSub2apiDocument(state.converted, now);
|
return buildSub2apiDocument(state.converted, now);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user