feat: migrate account usage to CLIProxyAPI
Frontend / quality (push) Canceled after 0s

This commit is contained in:
2026-09-08 09:56:08 +08:00
parent 0b2f969c6d
commit 8997886eef
35 changed files with 2778 additions and 1651 deletions
+7 -2
View File
@@ -85,8 +85,13 @@ export const post = <T>(
{ method: "POST", body: JSON.stringify(value), signal },
timeoutMs,
);
export const put = <T>(path: string, decoder: Decoder<T>, value: unknown) =>
api(path, decoder, { method: "PUT", body: JSON.stringify(value) });
export const put = <T>(
path: string,
decoder: Decoder<T>,
value: unknown,
timeoutMs = 30_000,
) =>
api(path, decoder, { method: "PUT", body: JSON.stringify(value) }, timeoutMs);
export const del = <T>(path: string, decoder: Decoder<T>) =>
api(path, decoder, { method: "DELETE" });
+147 -160
View File
@@ -39,14 +39,12 @@ import {
decodeAction,
decodeCode,
decodeDashboard,
decodeDeviceLogin,
decodeGeneral,
decodeOK,
decodeSMTP,
decodeTelegram,
type Account,
type Dashboard as Dash,
type DeviceLogin,
type GeneralSettings,
type Limit,
type SMTPSettingsForm,
@@ -1083,8 +1081,8 @@ function General() {
}
function CodexSettings() {
const [xs, setXs] = useState<Account[]>([]),
[deviceLogin, setDeviceLogin] = useState<DeviceLogin | null>(null),
[active, setActive] = useState(0),
[newAuthIndex, setNewAuthIndex] = useState(""),
[newKind, setNewKind] = useState<"personal" | "team">("team"),
[newPublicVisible, setNewPublicVisible] = useState(false),
[busy, setBusy] = useState(false),
@@ -1097,81 +1095,62 @@ function CodexSettings() {
useEffect(() => {
void load().catch((error) => setErr(toErrorMessage(error)));
}, []);
useEffect(() => {
if (!deviceLogin) return;
const started = Date.now();
const controller = new AbortController();
let timer = 0;
const poll = async () => {
try {
const accounts = await load(controller.signal);
const account = accounts.find((x) => x.id === deviceLogin.accountId);
if (!account) {
setDeviceLogin(null);
return;
}
if (["matched", "mismatch"].includes(account.validationStatus)) return;
if (Date.now() - started >= 120_000) {
setErr("设备码登录检测已超时,请重新生成设备码");
return;
}
timer = window.setTimeout(poll, 2000);
} catch (error) {
if (!controller.signal.aborted) {
setErr(toErrorMessage(error));
timer = window.setTimeout(poll, 2000);
}
}
};
timer = window.setTimeout(poll, 2000);
return () => {
controller.abort();
window.clearTimeout(timer);
};
}, [deviceLogin]);
const login = async (id: number) => {
const updateAccount = async (
account: Account,
changes: Partial<
Pick<
Account,
"displayName" | "authIndex" | "expectedKind" | "publicVisible"
>
>,
) => {
try {
setBusy(true);
setActive(id);
setErr("");
setDeviceLogin(null);
const result = await post(
`accounts/${id}/login/device`,
decodeDeviceLogin,
{},
undefined,
60_000,
await put(
`accounts/${account.id}`,
decodeOK,
{
displayName: changes.displayName ?? account.displayName,
authIndex: changes.authIndex ?? account.authIndex,
expectedKind: changes.expectedKind ?? account.expectedKind,
publicVisible: changes.publicVisible ?? account.publicVisible,
},
changes.authIndex === undefined ? 30_000 : 60_000,
);
setDeviceLogin({ accountId: id, ...result });
} catch (q) {
setErr(toErrorMessage(q));
} finally {
setBusy(false);
await load();
return true;
} catch (error) {
setErr(toErrorMessage(error));
return false;
}
};
const add = async () => {
const authIndex = newAuthIndex.trim();
if (!authIndex) {
setErr("请输入 CPA authIndex");
return;
}
try {
setBusy(true);
setActive(0);
setErr("");
setDeviceLogin(null);
const x = await post("accounts", (value) => decodeAccounts([value])[0], {
displayName: `账号 ${xs.length + 1}`,
expectedKind: newKind,
publicVisible: newPublicVisible,
});
await load();
setActive(x.id);
const result = await post(
`accounts/${x.id}/login/device`,
decodeDeviceLogin,
{},
const account = await post(
"accounts",
(value) => decodeAccounts([value])[0],
{
displayName: "",
authIndex,
expectedKind: newKind,
publicVisible: newPublicVisible,
},
undefined,
60_000,
);
setDeviceLogin({ accountId: x.id, ...result });
} catch (q) {
setErr(toErrorMessage(q));
setNewAuthIndex("");
await load();
setActive(account.id);
} catch (error) {
setErr(toErrorMessage(error));
} finally {
setBusy(false);
}
@@ -1181,21 +1160,32 @@ function CodexSettings() {
<div className="codex-heading">
<h2>Codex 账户与工作区</h2>
<p>
个人订阅和 Team 工作区请分别添加为独立连接;同一邮箱可以添加多次。
账号凭据和 OAuth 刷新由 CLIProxyAPI 管理;这里通过 CPA authIndex
绑定需要展示的 Codex 账号。
</p>
</div>
<section className="panel add-account-card">
<div>
<h3>添加新连接</h3>
<p>选择要连接的订阅类型,然后使用设备码完成授权。</p>
<h3>添加 CPA 账号</h3>
<p>authIndex 可从 CLIProxyAPI 的认证文件列表中取得。</p>
</div>
<div className="add-account-controls">
<label>
连接类型
CPA authIndex
<input
aria-label="CPA authIndex"
autoComplete="off"
placeholder="例如 6c4894e0cb312814"
value={newAuthIndex}
onChange={(event) => setNewAuthIndex(event.target.value)}
/>
</label>
<label>
预期连接类型
<select
value={newKind}
onChange={(e) =>
setNewKind(e.target.value as "personal" | "team")
onChange={(event) =>
setNewKind(event.target.value as "personal" | "team")
}
>
<option value="team">Team / Business 工作区</option>
@@ -1206,73 +1196,97 @@ function CodexSettings() {
<input
type="checkbox"
checked={newPublicVisible}
onChange={(e) => setNewPublicVisible(e.target.checked)}
onChange={(event) => setNewPublicVisible(event.target.checked)}
/>
公开显示到未登录总览
</label>
<button disabled={busy} onClick={add}>
{busy && active === 0 ? "服务启动中…" : "添加账号"}
{busy && active === 0 ? "正在验证并添加…" : "添加账号"}
</button>
</div>
</section>
<div className="account-list">
{xs.length === 0 && (
<div className="panel empty account-empty">
尚未添加 Codex 账号,请使用上方的“添加账号”创建连接。
尚未绑定 CPA Codex 账号,请在上方输入 authIndex。
</div>
)}
{xs.map((x) => (
<section className="panel account-card" key={x.id}>
{xs.map((account) => (
<section className="panel account-card" key={account.id}>
<div className="account-card-main">
<span className={"dot " + (x.connected ? "ok" : "")} />
<span className={"dot " + (account.connected ? "ok" : "")} />
<div className="account-details">
<input
aria-label="连接名称"
defaultValue={x.displayName}
onBlur={async (e) => {
const name = e.target.value.trim();
if (name && name !== x.displayName) {
await put(`accounts/${x.id}`, decodeOK, {
displayName: name,
expectedKind: x.expectedKind,
publicVisible: x.publicVisible,
});
void load().catch((error) =>
setErr(toErrorMessage(error)),
);
}
defaultValue={account.displayName}
onBlur={async (event) => {
const input = event.currentTarget;
const displayName = input.value.trim();
if (
displayName &&
displayName !== account.displayName &&
!(await updateAccount(account, { displayName }))
)
input.value = account.displayName;
}}
/>
<div className="account-meta">
<span>{x.email ? maskEmail(x.email) : "尚未登录"}</span>
<span>{planLabel(x.planType)}</span>
<span>
{account.email
? maskEmail(account.email)
: "账号信息暂不可用"}
</span>
<span>{planLabel(account.planType)}</span>
<span
className={x.validationStatus === "mismatch" ? "error" : ""}
className={
account.validationStatus === "mismatch" ? "error" : ""
}
>
{validationLabel(x)}
{validationLabel(account)}
</span>
</div>
{x.possibleDuplicate && (
{account.possibleDuplicate && (
<small className="hint">
同一邮箱已有相同类型连接,请确认没有重复授权同一工作区
同一邮箱已有相同类型绑定,请确认 authIndex 没有指向重复凭据
</small>
)}
</div>
</div>
<div className="account-card-actions">
<label>
CPA authIndex
<input
aria-label={`“${account.displayName}”的 CPA authIndex`}
autoComplete="off"
defaultValue={account.authIndex}
onBlur={async (event) => {
const input = event.currentTarget;
const authIndex = input.value.trim();
if (!authIndex || authIndex === account.authIndex) return;
if (
!confirm(
`更换“${account.displayName}”的 authIndex 会清除该本地账号已有的 Token 历史、限额快照和提醒记录。继续吗?`,
)
) {
input.value = account.authIndex;
return;
}
if (!(await updateAccount(account, { authIndex })))
input.value = account.authIndex;
}}
/>
</label>
<label>
预期连接类型
<select
aria-label="预期连接类型"
value={x.expectedKind}
onChange={async (e) => {
await put(`accounts/${x.id}`, decodeOK, {
displayName: x.displayName,
expectedKind: e.target.value,
publicVisible: x.publicVisible,
});
void load().catch((error) => setErr(toErrorMessage(error)));
}}
value={account.expectedKind}
onChange={(event) =>
void updateAccount(account, {
expectedKind: event.target
.value as Account["expectedKind"],
})
}
>
<option value="any">不校验</option>
<option value="personal">个人订阅</option>
@@ -1282,73 +1296,62 @@ function CodexSettings() {
<label className="checks">
<input
type="checkbox"
checked={x.publicVisible}
onChange={async (e) => {
try {
setErr("");
await put(`accounts/${x.id}`, decodeOK, {
displayName: x.displayName,
expectedKind: x.expectedKind,
publicVisible: e.target.checked,
});
await load();
} catch (q) {
setErr(toErrorMessage(q));
}
}}
checked={account.publicVisible}
onChange={(event) =>
void updateAccount(account, {
publicVisible: event.target.checked,
})
}
/>
公开显示到未登录总览
</label>
<div className="account-buttons">
<button
className="secondary"
disabled={busy}
onClick={() => login(x.id)}
>
{busy && active === x.id ? "服务启动中…" : "设备码登录"}
</button>
<button
className="secondary"
disabled={busy}
onClick={async () => {
if (
!confirm(`确定要退出“${x.displayName}”的 Codex 账号吗?`)
)
return;
try {
setBusy(true);
setActive(account.id);
setErr("");
await post(`accounts/${x.id}/logout`, decodeOK);
if (deviceLogin?.accountId === x.id) setDeviceLogin(null);
await post(
`accounts/${account.id}/sync`,
decodeOK,
{},
undefined,
60_000,
);
await load();
} catch (q) {
setErr(toErrorMessage(q));
} catch (error) {
setErr(toErrorMessage(error));
} finally {
setBusy(false);
}
}}
>
退出
{busy && active === account.id ? "同步中…" : "立即同步"}
</button>
<button
className="icon danger"
disabled={busy}
title="删除连接"
aria-label={`删除“${x.displayName}”`}
title="删除本地绑定"
aria-label={`删除“${account.displayName}”`}
onClick={async () => {
if (
!confirm(
`删除“${x.displayName}”及其全部凭据和历史数据?此操作无法撤销。`,
`删除“${account.displayName}”的本地绑定及全部历史数据?此操作不会删除 CLIProxyAPI 中的凭据,且无法撤销。`,
)
)
return;
try {
setErr("");
await del(`accounts/${x.id}`, decodeOK);
if (deviceLogin?.accountId === x.id) setDeviceLogin(null);
if (active === x.id) setActive(0);
if (+localStorage.accountId === x.id)
await del(`accounts/${account.id}`, decodeOK);
if (active === account.id) setActive(0);
if (+localStorage.accountId === account.id)
localStorage.removeItem("accountId");
await load();
} catch (q) {
setErr(toErrorMessage(q));
} catch (error) {
setErr(toErrorMessage(error));
}
}}
>
@@ -1356,22 +1359,6 @@ function CodexSettings() {
</button>
</div>
</div>
{deviceLogin?.accountId === x.id && (
<div className="codebox">
<span>
在浏览器中访问{" "}
<a
href={deviceLogin.verificationUrl}
target="_blank"
rel="noreferrer"
>
{deviceLogin.verificationUrl}
</a>
,然后输入设备码
</span>
<strong>{deviceLogin.userCode}</strong>
</div>
)}
</section>
))}
</div>
+14 -1
View File
@@ -1339,12 +1339,20 @@ html[data-theme="dark"] .codebox {
.add-account-controls {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
align-items: end;
gap: 10px;
}
.add-account-controls label {
min-width: 250px;
flex: 1 1 220px;
min-width: 220px;
}
.add-account-controls .checks {
flex: 0 1 auto;
min-width: 0;
}
.add-account-controls button {
@@ -1419,6 +1427,11 @@ html[data-theme="dark"] .codebox {
min-width: 285px;
}
.account-card-actions > label:not(.checks) input,
.account-card-actions > label:not(.checks) select {
width: 100%;
}
.account-buttons {
display: flex;
gap: 8px;
+16 -14
View File
@@ -2,8 +2,8 @@ import { describe, expect, it } from "vitest";
import { ApiError, toErrorMessage } from "./api";
import {
decodeAction,
decodeAccount,
decodeDashboard,
decodeDeviceLogin,
decodeGeneral,
decodeTelegram,
} from "./types";
@@ -77,19 +77,21 @@ describe("API decoders", () => {
).toThrow("theme格式无效");
});
it("只接受 HTTPS 设备授权地址", () => {
expect(() =>
decodeDeviceLogin({
verificationUrl: "http://example.com",
userCode: "ABC",
}),
).toThrow("HTTPS");
expect(
decodeDeviceLogin({
verificationUrl: "https://example.com",
userCode: "ABC",
}).userCode,
).toBe("ABC");
it("解码账号的 CPA authIndex", () => {
const account = decodeAccount({
id: 1,
displayName: "主账号",
authIndex: "index-1",
email: null,
planType: "plus",
expectedKind: "personal",
publicVisible: false,
actualKind: "personal",
validationStatus: "matched",
possibleDuplicate: false,
connected: true,
});
expect(account.authIndex).toBe("index-1");
});
it("保留 Telegram 操作警告", () => {
+4 -21
View File
@@ -17,12 +17,13 @@ export type AsyncState<T> =
export interface Status {
initialized: boolean;
appServer: boolean;
cpa: boolean;
version: string;
}
export interface Account {
id: number;
displayName: string;
authIndex: string;
email: string | null;
planType: string | null;
expectedKind: ExpectedKind;
@@ -32,11 +33,6 @@ export interface Account {
possibleDuplicate: boolean;
connected: boolean;
}
export interface DeviceLogin {
accountId: number;
verificationUrl: string;
userCode: string;
}
export interface Limit {
limitId: string;
limitName: string | null;
@@ -134,7 +130,7 @@ export const decodeStatus: Decoder<Status> = (value) => {
const x = record(value);
return {
initialized: boolean(x.initialized, "initialized"),
appServer: boolean(x.appServer, "appServer"),
cpa: boolean(x.cpa, "cpa"),
version: string(x.version, "version"),
};
};
@@ -143,6 +139,7 @@ export const decodeAccount: Decoder<Account> = (value) => {
return {
id: number(x.id, "id"),
displayName: string(x.displayName, "displayName"),
authIndex: string(x.authIndex ?? "", "authIndex"),
email: nullableString(x.email, "email"),
planType: nullableString(x.planType, "planType"),
expectedKind: enumValue(
@@ -296,20 +293,6 @@ export const decodeSMTP: Decoder<SMTPSettingsResponse> = (value) => {
configured: boolean(x.configured, "configured"),
};
};
export const decodeDeviceLogin: Decoder<Omit<DeviceLogin, "accountId">> = (
value,
) => {
const x = record(value),
verificationUrl = string(x.verificationUrl, "verificationUrl");
let url: URL;
try {
url = new URL(verificationUrl);
} catch {
throw new Error("设备授权地址无效");
}
if (url.protocol !== "https:") throw new Error("设备授权地址必须使用 HTTPS");
return { verificationUrl, userCode: string(x.userCode, "userCode") };
};
export const decodeOK: Decoder<{ ok: boolean }> = (value) => {
const x = record(value);
return { ok: boolean(x.ok, "ok") };
+61 -38
View File
@@ -3,7 +3,7 @@ import { expect, test, type Page } from "@playwright/test";
const responses: Record<string, unknown> = {
"system/status": {
initialized: true,
appServer: true,
cpa: true,
version: "0.3.0-beta.1",
},
"auth/me": { username: "admin" },
@@ -20,6 +20,7 @@ const responses: Record<string, unknown> = {
{
id: 1,
displayName: "默认账号",
authIndex: "index-default",
email: "test@example.com",
planType: "plus",
expectedKind: "personal",
@@ -56,7 +57,7 @@ async function openSettings(page: Page, version = "0.3.0-beta.1") {
await new Promise((resolve) => setTimeout(resolve, 40));
if (key === "system/status") {
await route.fulfill({
json: { initialized: true, appServer: true, version },
json: { initialized: true, cpa: true, version },
});
return;
}
@@ -159,7 +160,7 @@ test("shows every account as a collapsible summary on the overview", async ({
const key = new URL(request.url()).pathname.replace("/api/v1/", "");
if (key === "system/status")
return route.fulfill({
json: { initialized: true, appServer: true, version: "test" },
json: { initialized: true, cpa: true, version: "test" },
});
if (key === "auth/me")
return route.fulfill({ json: { username: "admin" } });
@@ -247,7 +248,7 @@ test("shows a read-only overview before login", async ({ page }) => {
const key = new URL(route.request().url()).pathname.replace("/api/v1/", "");
if (key === "system/status")
return route.fulfill({
json: { initialized: true, appServer: true, version: "test" },
json: { initialized: true, cpa: true, version: "test" },
});
if (key === "auth/me")
return route.fulfill({ status: 401, json: { error: "未登录" } });
@@ -354,7 +355,7 @@ test("keeps the authenticated brand on one line with a release version", async (
test("shows the build version before setup", async ({ page }) => {
await page.route("**/api/v1/system/status", (route) =>
route.fulfill({
json: { initialized: false, appServer: false, version: "test" },
json: { initialized: false, cpa: false, version: "test" },
}),
);
await page.goto("/");
@@ -370,7 +371,7 @@ test("shows the build version before setup", async ({ page }) => {
test("shows the build version on login", async ({ page }) => {
await page.route("**/api/v1/system/status", (route) =>
route.fulfill({
json: { initialized: true, appServer: false, version: "test" },
json: { initialized: true, cpa: false, version: "test" },
}),
);
await page.route("**/api/v1/auth/me", (route) =>
@@ -633,56 +634,50 @@ test("programmatic tab changes do not clamp an existing scroll position", async
.toEqual(before.content);
});
test("deleting a newly added account clears its device authorization", async ({
test("deleting a newly added account keeps the CPA credential external", async ({
page,
}) => {
let accounts: Array<(typeof responses.accounts)[number]> = [];
let createdAuthIndex = "";
await page.route("**/api/v1/**", async (route) => {
const request = route.request();
const key = new URL(request.url()).pathname.replace("/api/v1/", "");
if (key === "accounts" && request.method() === "GET")
return route.fulfill({ json: accounts });
if (key === "accounts" && request.method() === "POST") {
const body = request.postDataJSON() as { authIndex: string };
createdAuthIndex = body.authIndex;
const account = {
...responses.accounts[0],
id: 2,
displayName: "账号 1",
email: "",
connected: false,
validationStatus: "pending" as const,
displayName: "CPA 账号",
authIndex: body.authIndex,
};
accounts = [account];
return route.fulfill({ json: account });
}
if (key === "accounts/2/login/device")
return route.fulfill({
json: {
verificationUrl: "https://auth.openai.com/codex/device",
userCode: "PLTJ-7M6I6",
},
});
if (key === "accounts/2" && request.method() === "DELETE") {
accounts = [];
return route.fulfill({ json: { ok: true } });
}
return route.fulfill({ json: responses[key] ?? {} });
});
page.on("dialog", (dialog) => dialog.accept());
page.on("dialog", (dialog) => {
expect(dialog.message()).toContain("不会删除 CLIProxyAPI 中的凭据");
void dialog.accept();
});
await page.goto("/settings");
await page.getByRole("tab", { name: "Codex" }).click();
await page.getByLabel("CPA authIndex", { exact: true }).fill("index-new");
await page.getByRole("button", { name: "添加账号" }).click();
await expect(page.getByText("PLTJ-7M6I6")).toBeVisible();
await expect(
page.getByRole("link", { name: "https://auth.openai.com/codex/device" }),
).toBeVisible();
await expect.poll(() => createdAuthIndex).toBe("index-new");
await expect(page.getByLabel("“CPA 账号”的 CPA authIndex")).toHaveValue(
"index-new",
);
await page.getByRole("button", { name: "删除“账号 1”" }).click();
await expect(page.getByText("PLTJ-7M6I6")).toHaveCount(0);
await page.getByRole("button", { name: "删除“CPA 账号”" }).click();
await expect(
page.getByRole("link", { name: "https://auth.openai.com/codex/device" }),
).toHaveCount(0);
await expect(
page.getByText("尚未添加 Codex 账号", { exact: false }),
page.getByText("尚未绑定 CPA Codex 账号", { exact: false }),
).toBeVisible();
});
@@ -690,7 +685,7 @@ test("sets public visibility when adding and editing an account", async ({
page,
}) => {
let accounts: Array<(typeof responses.accounts)[number]> = [];
let createdBody: { publicVisible?: boolean } | undefined;
let createdBody: { authIndex?: string; publicVisible?: boolean } | undefined;
let updatedBody: { publicVisible?: boolean } | undefined;
await page.route("**/api/v1/**", async (route) => {
const request = route.request();
@@ -698,11 +693,15 @@ test("sets public visibility when adding and editing an account", async ({
if (key === "accounts" && request.method() === "GET")
return route.fulfill({ json: accounts });
if (key === "accounts" && request.method() === "POST") {
createdBody = request.postDataJSON() as { publicVisible?: boolean };
createdBody = request.postDataJSON() as {
authIndex?: string;
publicVisible?: boolean;
};
const account = {
...responses.accounts[0],
id: 2,
displayName: "账号 1",
authIndex: createdBody.authIndex ?? "",
email: "",
connected: false,
validationStatus: "pending" as const,
@@ -711,13 +710,6 @@ test("sets public visibility when adding and editing an account", async ({
accounts = [account];
return route.fulfill({ json: account });
}
if (key === "accounts/2/login/device")
return route.fulfill({
json: {
verificationUrl: "https://auth.openai.com/codex/device",
userCode: "PLTJ-7M6I6",
},
});
if (key === "accounts/2" && request.method() === "PUT") {
updatedBody = request.postDataJSON() as { publicVisible?: boolean };
accounts = [
@@ -738,8 +730,10 @@ test("sets public visibility when adding and editing an account", async ({
);
await expect(addVisibility).not.toBeChecked();
await addVisibility.check();
await page.getByLabel("CPA authIndex", { exact: true }).fill("index-public");
await page.getByRole("button", { name: "添加账号" }).click();
await expect.poll(() => createdBody?.publicVisible).toBe(true);
expect(createdBody?.authIndex).toBe("index-public");
const accountCard = page.locator(".account-card");
const accountVisibility = accountCard.locator('input[type="checkbox"]');
@@ -751,6 +745,35 @@ test("sets public visibility when adding and editing an account", async ({
await expect(accountVisibility).not.toBeChecked();
});
test("changing authIndex confirms local history deletion", async ({ page }) => {
let account = { ...responses.accounts[0] };
let updatedBody: { authIndex?: string } | undefined;
await page.route("**/api/v1/**", async (route) => {
const request = route.request();
const key = new URL(request.url()).pathname.replace("/api/v1/", "");
if (key === "accounts" && request.method() === "GET")
return route.fulfill({ json: [account] });
if (key === "accounts/1" && request.method() === "PUT") {
updatedBody = request.postDataJSON() as { authIndex?: string };
account = { ...account, authIndex: updatedBody.authIndex ?? "" };
return route.fulfill({ json: { ok: true } });
}
return route.fulfill({ json: responses[key] ?? {} });
});
page.on("dialog", (dialog) => {
expect(dialog.message()).toContain("会清除该本地账号已有的 Token 历史");
void dialog.accept();
});
await page.goto("/settings");
await page.getByRole("tab", { name: "Codex" }).click();
const input = page.getByLabel("“默认账号”的 CPA authIndex");
await input.fill("index-rebound");
await input.blur();
await expect.poll(() => updatedBody?.authIndex).toBe("index-rebound");
await expect(input).toHaveValue("index-rebound");
});
test("Codex account cards fit within the viewport", async ({ page }) => {
await openSettings(page);
await page.getByRole("tab", { name: "Codex" }).click();