+
+
Codex 账户与工作区
+
个人订阅和 Team 工作区请分别添加为独立连接;同一邮箱可以添加多次。
+
+
+
添加新连接
+
选择要连接的订阅类型,然后使用设备码完成授权。
+
+
+
+
+
+
+ {xs.length === 0 && (
+
+ 尚未添加 Codex 账号,请使用上方的“添加账号”创建连接。
+
+ )}
{xs.map((x) => (
-
-
-
-
{
- const name = e.target.value.trim();
- if (name && name !== x.displayName) {
+
+
+
+
+
{
+ const name = e.target.value.trim();
+ if (name && name !== x.displayName) {
+ await put(`accounts/${x.id}`, {
+ displayName: name,
+ expectedKind: x.expectedKind,
+ });
+ load();
+ }
+ }}
+ />
+
+ {x.email || "尚未登录"}
+ {planLabel(x.planType)}
+
+ {validationLabel(x)}
+
+
+ {x.possibleDuplicate && (
+
+ 同一邮箱已有相同类型连接,请确认没有重复授权同一工作区
+
+ )}
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+ {deviceLogin?.accountId === x.id && (
+
+ )}
+
))}
- {result && (
-
- )}
{err &&
{err}
}
);
diff --git a/frontend/src/styles.css b/frontend/src/styles.css
index f3a9e21..2a6f813 100644
--- a/frontend/src/styles.css
+++ b/frontend/src/styles.css
@@ -443,18 +443,10 @@ header p {
.checks input {
width: auto;
}
-.actions,
-.settings-title {
+.actions {
display: flex;
gap: 10px;
}
-.settings-title {
- justify-content: space-between;
- align-items: start;
-}
-.settings-title p {
- margin-bottom: 0;
-}
.danger {
background: transparent;
border: 1px solid var(--danger);
@@ -464,43 +456,126 @@ header p {
padding: 10px;
}
.codebox {
- padding: 16px;
+ grid-column: 1 / -1;
+ padding: 16px 18px;
border: 1px dashed var(--accent);
border-radius: 12px;
display: flex;
justify-content: space-between;
align-items: center;
+ gap: 18px;
+ background: color-mix(in srgb, var(--accent) 6%, var(--panel));
+ overflow-wrap: anywhere;
}
.codebox strong {
font-family: monospace;
- font-size: 20px;
+ font-size: 24px;
+ letter-spacing: 0.08em;
color: var(--accent);
+ white-space: nowrap;
+}
+.codex-settings {
+ display: flex;
+ flex-direction: column;
+ gap: 18px;
+}
+.codex-heading h2,
+.add-account-card h3 {
+ margin: 0;
+}
+.codex-heading h2 {
+ font-size: 26px;
+}
+.codex-heading p,
+.add-account-card p {
+ color: var(--muted);
+ margin: 7px 0 0;
+}
+.add-account-card {
+ padding: 22px 24px;
+ display: flex;
+ align-items: end;
+ justify-content: space-between;
+ gap: 24px;
+}
+.add-account-controls {
+ display: flex;
+ align-items: end;
+ gap: 10px;
+}
+.add-account-controls label {
+ min-width: 250px;
+}
+.add-account-controls button {
+ min-height: 43px;
+ white-space: nowrap;
}
.account-list {
display: flex;
flex-direction: column;
- gap: 10px;
+ gap: 14px;
}
-.account-row {
+.account-card {
display: grid;
- grid-template-columns: auto minmax(180px, 1fr) auto auto auto auto;
- gap: 10px;
- align-items: center;
- padding: 14px;
- border: 1px solid var(--border);
- border-radius: 12px;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 20px 28px;
+ padding: 22px 24px;
}
-.account-row > div {
+.account-card-main {
+ display: flex;
+ align-items: flex-start;
+ gap: 14px;
+ min-width: 0;
+}
+.account-card-main .dot {
+ margin-top: 15px;
+}
+.account-details {
display: flex;
flex-direction: column;
- gap: 5px;
+ gap: 8px;
+ min-width: 0;
+ flex: 1;
}
-.account-row input {
- padding: 7px;
+.account-details input {
+ width: 100%;
+ max-width: 360px;
+ padding: 9px 11px;
font-weight: 700;
+ font-size: 16px;
}
-.account-row small {
- padding-left: 8px;
+.account-meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px 16px;
+ color: var(--muted);
+ font-size: 13px;
+}
+.account-meta span + span {
+ position: relative;
+}
+.account-meta span + span::before {
+ content: "·";
+ position: absolute;
+ left: -10px;
+ color: var(--border);
+}
+.account-card-actions {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ gap: 10px;
+ min-width: 285px;
+}
+.account-buttons {
+ display: flex;
+ gap: 8px;
+}
+.account-buttons .secondary:first-child {
+ flex: 1;
+}
+.account-empty {
+ padding: 44px 24px;
}
a {
color: var(--accent);
@@ -575,11 +650,30 @@ a {
.fresh {
display: none;
}
- .account-row {
- grid-template-columns: auto 1fr;
+ .add-account-card,
+ .add-account-controls {
+ align-items: stretch;
+ flex-direction: column;
}
- .account-row button {
- grid-column: auto;
+ .add-account-controls label {
+ min-width: 0;
+ }
+ .account-card {
+ grid-template-columns: 1fr;
+ padding: 18px;
+ }
+ .account-card-actions {
+ min-width: 0;
+ }
+ .account-buttons button {
+ flex: 1;
+ }
+ .account-buttons .icon {
+ flex: none;
+ }
+ .codebox {
+ flex-direction: column;
+ align-items: flex-start;
}
.limit {
grid-template-columns: 1fr;
@@ -592,9 +686,6 @@ a {
.limit footer {
grid-column: 1;
}
- .settings-title {
- flex-direction: column;
- }
}
@media (prefers-reduced-motion: reduce) {
* {
diff --git a/frontend/tests/settings-layout.spec.ts b/frontend/tests/settings-layout.spec.ts
index 68a7980..0d9ab8b 100644
--- a/frontend/tests/settings-layout.spec.ts
+++ b/frontend/tests/settings-layout.spec.ts
@@ -111,3 +111,61 @@ test("programmatic tab changes do not clamp an existing scroll position", async
await expect.poll(async () => (await layout(page)).scrollY).toBe(before.scrollY);
await expect.poll(async () => (await layout(page)).content).toEqual(before.content);
});
+
+test("deleting a newly added account clears its device authorization", async ({ page }) => {
+ let accounts: Array<(typeof responses.accounts)[number]> = [];
+ 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 account = {
+ ...responses.accounts[0],
+ id: 2,
+ displayName: "账号 1",
+ email: "",
+ connected: false,
+ validationStatus: "pending" as const,
+ };
+ 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());
+ await page.goto("/settings");
+ await page.getByRole("tab", { name: "Codex" }).click();
+ 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 page.getByRole("button", { name: "删除“账号 1”" }).click();
+ await expect(page.getByText("PLTJ-7M6I6")).toHaveCount(0);
+ await expect(page.getByRole("link", { name: "https://auth.openai.com/codex/device" })).toHaveCount(0);
+ await expect(page.getByText("尚未添加 Codex 账号", { exact: false })).toBeVisible();
+});
+
+test("Codex account cards fit within the viewport", async ({ page }) => {
+ await openSettings(page);
+ await page.getByRole("tab", { name: "Codex" }).click();
+ const overflow = await page.evaluate(() => ({
+ documentWidth: document.documentElement.scrollWidth,
+ viewportWidth: document.documentElement.clientWidth,
+ cardWidth: document.querySelector(".account-card")!.getBoundingClientRect().width,
+ contentWidth: document.querySelector(".settings-content")!.getBoundingClientRect().width,
+ }));
+ expect(overflow.documentWidth).toBeLessThanOrEqual(overflow.viewportWidth);
+ expect(overflow.cardWidth).toBeLessThanOrEqual(overflow.contentWidth);
+});