mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-13 16:14:57 +00:00
fix(ui): restore provider-owned Codex pool inherit state (#585)
* fix(ui): restore provider-owned codex pool inherit state * docs(pr): add before-after UI evidence * refactor: remove dead hasProviderDefaults param and harden pool rendering - Remove unused _hasProviderDefaults parameter from buildDraftRouting and routingDraftSignature, clean up all call sites and useMemo deps - Filter deleted providers from selectedPoolProviderNames to prevent ghost entries when a saved pool member no longer exists - Add symmetric backend test for inherit + non-nil provider defaults --------- Co-authored-by: viettranx <viettranx@gmail.com>
This commit is contained in:
1 parent
3fe0633d35
commit
287946bb9a
15 files changed
+545
-53
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 243 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 204 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 241 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 194 KiB |
@@ -0,0 +1,467 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>GoClaw Codex Pool UI Before / After</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #120905;
|
||||
--panel: #1c100b;
|
||||
--panel-2: #24150f;
|
||||
--text: #f6eee8;
|
||||
--muted: #c5afa0;
|
||||
--line: rgba(255, 220, 196, 0.16);
|
||||
--accent: #ff7b3d;
|
||||
--bad: #ff5d5d;
|
||||
--good: #49d18c;
|
||||
--shadow: 0 24px 80px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family:
|
||||
Inter,
|
||||
ui-sans-serif,
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
sans-serif;
|
||||
color: var(--text);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(255, 123, 61, 0.16), transparent 28%),
|
||||
radial-gradient(circle at top right, rgba(255, 183, 120, 0.08), transparent 22%),
|
||||
linear-gradient(180deg, #100704 0%, var(--bg) 100%);
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
width: min(1500px, calc(100vw - 48px));
|
||||
margin: 0 auto;
|
||||
padding: 32px 0 56px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 28px 28px 24px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 24px;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.01));
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid rgba(255, 123, 61, 0.28);
|
||||
border-radius: 999px;
|
||||
color: #ffd2ba;
|
||||
background: rgba(255, 123, 61, 0.1);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 14px 0 12px;
|
||||
font-size: clamp(30px, 5vw, 52px);
|
||||
line-height: 1.02;
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
max-width: 980px;
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 17px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.meta-card {
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
color: #d9c1b2;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
margin-top: 8px;
|
||||
font-size: 15px;
|
||||
line-height: 1.45;
|
||||
color: var(--text);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-top: 28px;
|
||||
padding: 24px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 24px;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.022), rgba(255, 255, 255, 0.015));
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
margin: 0;
|
||||
font-size: 26px;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.section-intro {
|
||||
margin: 10px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.findings {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.finding {
|
||||
padding: 16px;
|
||||
border-radius: 18px;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--panel-2);
|
||||
}
|
||||
|
||||
.finding strong {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.finding p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.comparison {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 18px;
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.shot {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 20px;
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.shot-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.shot-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.tag.before {
|
||||
color: #ffd3d3;
|
||||
background: rgba(255, 93, 93, 0.14);
|
||||
border: 1px solid rgba(255, 93, 93, 0.22);
|
||||
}
|
||||
|
||||
.tag.after {
|
||||
color: #d9ffea;
|
||||
background: rgba(73, 209, 140, 0.14);
|
||||
border: 1px solid rgba(73, 209, 140, 0.22);
|
||||
}
|
||||
|
||||
.shot img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
background: #0b0502;
|
||||
}
|
||||
|
||||
.caption {
|
||||
padding: 14px 16px 18px;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.caption ul {
|
||||
margin: 10px 0 0 18px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.caption li + li {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.foot {
|
||||
margin-top: 28px;
|
||||
padding: 18px 20px;
|
||||
border-radius: 18px;
|
||||
border: 1px solid var(--line);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 2px 6px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: #ffe8d8;
|
||||
font-family:
|
||||
ui-monospace,
|
||||
SFMono-Regular,
|
||||
Menlo,
|
||||
Consolas,
|
||||
monospace;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.meta-grid,
|
||||
.comparison,
|
||||
.findings {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.wrap {
|
||||
width: min(100vw - 24px, 100%);
|
||||
padding-top: 18px;
|
||||
}
|
||||
|
||||
.hero,
|
||||
.section {
|
||||
padding: 18px;
|
||||
border-radius: 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="wrap">
|
||||
<section class="hero">
|
||||
<div class="eyebrow">UI Test Visual Report</div>
|
||||
<h1>Provider-owned Codex pool UI before / after</h1>
|
||||
<p>
|
||||
This comparison uses two real local GoClaw dashboards seeded with the same fixture data.
|
||||
The only difference is the code revision:
|
||||
<code>main@b38a078e</code> versus <code>fix@faf98c73</code>.
|
||||
The screenshots below isolate the two regressions fixed by the PR: hidden saved pool
|
||||
membership on provider pages, and sticky custom routing state on agent pages.
|
||||
</p>
|
||||
|
||||
<div class="meta-grid">
|
||||
<div class="meta-card">
|
||||
<div class="meta-label">Before Revision</div>
|
||||
<div class="meta-value"><code>b38a078e</code><br />origin/main</div>
|
||||
</div>
|
||||
<div class="meta-card">
|
||||
<div class="meta-label">After Revision</div>
|
||||
<div class="meta-value"><code>faf98c73</code><br />PR branch</div>
|
||||
</div>
|
||||
<div class="meta-card">
|
||||
<div class="meta-label">Fixture</div>
|
||||
<div class="meta-value">
|
||||
<code>codex-owner</code> owns pool membership<br />
|
||||
<code>codex-disabled</code> is saved as a disabled member<br />
|
||||
<code>solo-agent</code> uses <code>codex-solo</code> with no saved pool defaults
|
||||
</div>
|
||||
</div>
|
||||
<div class="meta-card">
|
||||
<div class="meta-label">Capture Method</div>
|
||||
<div class="meta-value">
|
||||
Real local UI via Playwright<br />
|
||||
SQLite-backed gateways on separate ports
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Scenario 1: provider page hides saved non-ready members</h2>
|
||||
<p class="section-intro">
|
||||
The provider page should show saved pool membership even when an extra account is disabled
|
||||
or not ready. Before the fix, the page derived membership only from quota-fetched ready
|
||||
accounts, so the saved member disappeared entirely.
|
||||
</p>
|
||||
|
||||
<div class="findings">
|
||||
<div class="finding">
|
||||
<strong>What was broken on main</strong>
|
||||
<p>
|
||||
The pool owner <code>codex-owner</code> had a saved extra member
|
||||
<code>codex-disabled</code>, but the UI rendered <code>0 in pool</code>, an empty
|
||||
account list, and no selectable extra account.
|
||||
</p>
|
||||
</div>
|
||||
<div class="finding">
|
||||
<strong>What the patch changes</strong>
|
||||
<p>
|
||||
The page now renders membership from the saved provider settings, while still limiting
|
||||
quota lookups to ready accounts only. The disabled member remains visible and clearly
|
||||
labeled as blocked.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comparison">
|
||||
<article class="shot">
|
||||
<div class="shot-head">
|
||||
<div class="shot-title">Provider Detail</div>
|
||||
<span class="tag before">Before</span>
|
||||
</div>
|
||||
<img src="./before-provider-overview.png" alt="Before fix: provider detail hides the saved disabled pool member." />
|
||||
<div class="caption">
|
||||
Main branch behavior:
|
||||
<ul>
|
||||
<li><code>Available Extra Accounts</code> is effectively empty.</li>
|
||||
<li><code>Accounts in Pool</code> shows <code>0 in pool</code>.</li>
|
||||
<li>The saved disabled member is invisible, so the provider-owned pool looks unsaved.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="shot">
|
||||
<div class="shot-head">
|
||||
<div class="shot-title">Provider Detail</div>
|
||||
<span class="tag after">After</span>
|
||||
</div>
|
||||
<img src="./after-provider-overview.png" alt="After fix: provider detail shows the saved disabled pool member and pool count." />
|
||||
<div class="caption">
|
||||
Fixed branch behavior:
|
||||
<ul>
|
||||
<li><code>Codex Disabled</code> is visible in <code>Available Extra Accounts</code>.</li>
|
||||
<li><code>Accounts in Pool</code> correctly shows <code>2 in pool</code>.</li>
|
||||
<li>The disabled member is still shown as saved, with blocked state clearly surfaced.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Scenario 2: agent page manufactured a sticky custom override</h2>
|
||||
<p class="section-intro">
|
||||
When an agent had no explicit routing override and the provider had no saved pool yet,
|
||||
the UI should have represented that state as <em>inherit / use provider defaults</em>.
|
||||
Before the fix, the draft model incorrectly materialized it as a saved custom
|
||||
<code>primary_first + []</code> state.
|
||||
</p>
|
||||
|
||||
<div class="findings">
|
||||
<div class="finding">
|
||||
<strong>What was broken on main</strong>
|
||||
<p>
|
||||
The agent page defaulted to <code>Custom For This Agent</code> and framed the agent as
|
||||
if it had its own routing policy, even though there was no provider-owned pool and no
|
||||
explicit agent override saved.
|
||||
</p>
|
||||
</div>
|
||||
<div class="finding">
|
||||
<strong>What the patch changes</strong>
|
||||
<p>
|
||||
The draft state now preserves a true inherit mode. The UI shows
|
||||
<code>Use Provider Defaults</code> and clarifies that the agent stays on its primary
|
||||
account until provider-owned pool members are later added.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comparison">
|
||||
<article class="shot">
|
||||
<div class="shot-head">
|
||||
<div class="shot-title">Agent Codex Pool</div>
|
||||
<span class="tag before">Before</span>
|
||||
</div>
|
||||
<img src="./before-agent-codex-pool.png" alt="Before fix: agent page defaults to Custom For This Agent even with no provider-owned pool." />
|
||||
<div class="caption">
|
||||
Main branch behavior:
|
||||
<ul>
|
||||
<li><code>Custom For This Agent</code> is selected by default.</li>
|
||||
<li>The copy tells the user to configure provider members first before enabling pooled routing.</li>
|
||||
<li>This makes the implicit default state look like an explicit agent override.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="shot">
|
||||
<div class="shot-head">
|
||||
<div class="shot-title">Agent Codex Pool</div>
|
||||
<span class="tag after">After</span>
|
||||
</div>
|
||||
<img src="./after-agent-codex-pool.png" alt="After fix: agent page defaults to Use Provider Defaults and explains the temporary primary-only behavior." />
|
||||
<div class="caption">
|
||||
Fixed branch behavior:
|
||||
<ul>
|
||||
<li><code>Use Provider Defaults</code> is selected.</li>
|
||||
<li>The helper copy explains the temporary primary-only behavior without inventing a custom override.</li>
|
||||
<li>The agent can stay in inherit mode now and automatically pick up provider pool members later.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="foot">
|
||||
Generated from a real local browser session using two SQLite-backed GoClaw instances.
|
||||
The frontends were served from separate Vite dev servers, authenticated with
|
||||
<code>system</code> + a local owner token, and seeded through the HTTP API with the same
|
||||
provider / agent records before capture.
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -113,6 +113,38 @@ func TestResolveEffectiveChatGPTOAuthRoutingUsesProviderDefaultsWhenAgentUnset(t
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveEffectiveChatGPTOAuthRoutingAllowsInheritWithoutSavedProviderPool(t *testing.T) {
|
||||
override := &ChatGPTOAuthRoutingConfig{
|
||||
OverrideMode: ChatGPTOAuthOverrideInherit,
|
||||
}
|
||||
|
||||
got := ResolveEffectiveChatGPTOAuthRouting(nil, override)
|
||||
if got != nil {
|
||||
t.Fatalf("ResolveEffectiveChatGPTOAuthRouting() = %#v, want nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveEffectiveChatGPTOAuthRoutingInheritForwardsProviderDefaults(t *testing.T) {
|
||||
defaults := &ChatGPTOAuthRoutingConfig{
|
||||
Strategy: ChatGPTOAuthStrategyRoundRobin,
|
||||
ExtraProviderNames: []string{"codex-work"},
|
||||
}
|
||||
override := &ChatGPTOAuthRoutingConfig{
|
||||
OverrideMode: ChatGPTOAuthOverrideInherit,
|
||||
}
|
||||
|
||||
got := ResolveEffectiveChatGPTOAuthRouting(defaults, override)
|
||||
if got == nil {
|
||||
t.Fatal("ResolveEffectiveChatGPTOAuthRouting() = nil, want config forwarding provider defaults")
|
||||
}
|
||||
if got.Strategy != ChatGPTOAuthStrategyRoundRobin {
|
||||
t.Fatalf("Strategy = %q, want %q", got.Strategy, ChatGPTOAuthStrategyRoundRobin)
|
||||
}
|
||||
if !reflect.DeepEqual(got.ExtraProviderNames, []string{"codex-work"}) {
|
||||
t.Fatalf("ExtraProviderNames = %#v, want provider defaults", got.ExtraProviderNames)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveEffectiveChatGPTOAuthRoutingAllowsCustomSingleAccountToDisableDefaults(t *testing.T) {
|
||||
defaults := &ChatGPTOAuthRoutingConfig{
|
||||
Strategy: ChatGPTOAuthStrategyRoundRobin,
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
"manageAction": "Open Pool",
|
||||
"singleAccountHint": "Configure another named OpenAI Codex OAuth provider on the Providers page to create a provider-owned pool.",
|
||||
"membershipManagedAtProvider": "Pool members are managed on provider {{provider}}. Change membership there, then come back here to tune routing behavior.",
|
||||
"membershipConfigureProviderFirst": "No provider-owned pool is saved on {{provider}} yet. Configure pool members there before enabling pooled routing for this agent.",
|
||||
"membershipConfigureProviderFirst": "No provider-owned pool is saved on {{provider}} yet. This agent will stay on the primary account until pool members are added there.",
|
||||
"readySummary": "{{ready}} of {{total}} extra accounts are ready.",
|
||||
"preferredNeedsAttention": "The primary account needs attention before this agent can use it reliably.",
|
||||
"loadingAccounts": "Checking OpenAI Codex OAuth account status...",
|
||||
@@ -215,7 +215,7 @@
|
||||
"label": "Agent Routing Mode",
|
||||
"inherit": "Use Provider Defaults",
|
||||
"custom": "Custom For This Agent",
|
||||
"noProviderDefault": "No provider-owned pool is saved yet. Configure reusable pool members on the Providers page first, or keep this agent on its primary account.",
|
||||
"noProviderDefault": "No provider-owned pool is saved yet. Following provider defaults will keep this agent on its primary account for now and automatically pick up pool members added later.",
|
||||
"providerDefaultSummary": "{{count}} accounts in provider default",
|
||||
"summaryInherited": "This agent currently follows the provider default pool.",
|
||||
"summaryCustom": "This agent currently saves its own routing override for the provider-owned pool."
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
"manageAction": "Mở Pool",
|
||||
"singleAccountHint": "Hãy cấu hình thêm một provider OpenAI Codex OAuth khác trên trang Providers để tạo pool do provider quản lý.",
|
||||
"membershipManagedAtProvider": "Thành viên pool được quản lý tại provider {{provider}}. Hãy đổi membership ở đó rồi quay lại đây để chỉnh hành vi routing.",
|
||||
"membershipConfigureProviderFirst": "Provider {{provider}} chưa có provider-owned pool. Hãy cấu hình pool members ở đó trước khi bật pooled routing cho agent này.",
|
||||
"membershipConfigureProviderFirst": "Provider {{provider}} chưa có provider-owned pool. Agent này sẽ chỉ dùng tài khoản chính cho tới khi pool members được thêm ở đó.",
|
||||
"readySummary": "{{ready}} / {{total}} tài khoản phụ đã sẵn sàng.",
|
||||
"preferredNeedsAttention": "Tài khoản chính cần được xử lý trước khi agent dùng ổn định.",
|
||||
"loadingAccounts": "Đang kiểm tra trạng thái tài khoản OpenAI Codex OAuth...",
|
||||
@@ -215,7 +215,7 @@
|
||||
"label": "Chế độ routing của agent",
|
||||
"inherit": "Dùng mặc định từ provider",
|
||||
"custom": "Tùy chỉnh riêng cho agent",
|
||||
"noProviderDefault": "Chưa có provider-owned pool nào được lưu. Hãy cấu hình reusable pool members ở trang Providers trước, hoặc giữ agent này chỉ dùng tài khoản chính.",
|
||||
"noProviderDefault": "Chưa có provider-owned pool nào được lưu. Chế độ dùng mặc định của provider sẽ giữ agent này ở tài khoản chính lúc này và tự nhận pool members khi provider thêm sau.",
|
||||
"providerDefaultSummary": "Mặc định từ provider có {{count}} tài khoản",
|
||||
"summaryInherited": "Agent này hiện đang dùng pool mặc định của provider.",
|
||||
"summaryCustom": "Agent này hiện đang lưu routing override riêng cho provider-owned pool."
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
"manageAction": "打开池管理",
|
||||
"singleAccountHint": "请在 Providers 页面配置另一个具名 OpenAI Codex OAuth provider,以创建由 provider 管理的账户池。",
|
||||
"membershipManagedAtProvider": "池成员由 provider {{provider}} 管理。请先在那里修改成员关系,再回到这里调整路由行为。",
|
||||
"membershipConfigureProviderFirst": "provider {{provider}} 还没有保存 provider-owned pool。请先在那里配置池成员,再为这个 agent 启用池化路由。",
|
||||
"membershipConfigureProviderFirst": "provider {{provider}} 还没有保存 provider-owned pool。在那里添加池成员之前,这个 agent 会先只使用主账户。",
|
||||
"readySummary": "{{ready}} / {{total}} 个额外账户已就绪。",
|
||||
"preferredNeedsAttention": "主账户需要先处理,agent 才能稳定使用它。",
|
||||
"loadingAccounts": "正在检查 OpenAI Codex OAuth 账户状态...",
|
||||
@@ -215,7 +215,7 @@
|
||||
"label": "Agent 路由模式",
|
||||
"inherit": "使用 Provider 默认值",
|
||||
"custom": "仅此 Agent 自定义",
|
||||
"noProviderDefault": "还没有保存 provider-owned pool。请先去 Providers 页面配置可复用的池成员,或者继续让这个 agent 只使用主账户。",
|
||||
"noProviderDefault": "还没有保存 provider-owned pool。跟随 provider 默认状态时,这个 agent 现在会先停留在主账户,并在以后自动接收新增的池成员。",
|
||||
"providerDefaultSummary": "Provider 默认池包含 {{count}} 个账户",
|
||||
"summaryInherited": "这个 agent 当前正在使用 provider 默认账户池。",
|
||||
"summaryCustom": "这个 agent 当前保存了针对 provider-owned pool 的路由覆盖配置。"
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
buildAgentOtherConfigWithChatGPTOAuthRouting,
|
||||
normalizeChatGPTOAuthRouting,
|
||||
} from "./agent-display-utils";
|
||||
import { buildDraftRouting } from "./codex-pool-routing-draft-utils";
|
||||
|
||||
interface AgentAdvancedDialogProps {
|
||||
open: boolean;
|
||||
@@ -39,17 +40,10 @@ export function AgentAdvancedDialog({ open, onOpenChange, agent, onUpdate }: Age
|
||||
const deriveState = (a: AgentData) => {
|
||||
const otherObj = (a.other_config ?? {}) as Record<string, unknown>;
|
||||
const routing = normalizeChatGPTOAuthRouting(a.other_config);
|
||||
const draftRouting = buildDraftRouting(routing);
|
||||
return {
|
||||
thinkingLevel: typeof otherObj.thinking_level === "string" ? otherObj.thinking_level : "off",
|
||||
chatgptRouting: {
|
||||
override_mode: routing.isExplicit
|
||||
? routing.overrideMode
|
||||
: providerDefaults
|
||||
? "inherit"
|
||||
: "custom",
|
||||
strategy: routing.strategy,
|
||||
extra_provider_names: routing.extraProviderNames,
|
||||
} as ChatGPTOAuthRoutingConfig,
|
||||
chatgptRouting: draftRouting,
|
||||
wsSharing: (otherObj.workspace_sharing ?? {}) as WorkspaceSharingConfig,
|
||||
comp: a.compaction_config ?? {},
|
||||
pruneEnabled: a.context_pruning?.mode !== "off",
|
||||
|
||||
@@ -87,12 +87,12 @@ export function AgentCodexPoolPage() {
|
||||
[agent?.provider, currentProvider?.settings, savedRouting],
|
||||
);
|
||||
const savedDraftRouting = useMemo(
|
||||
() => buildDraftRouting(savedRouting, Boolean(providerDefaults)),
|
||||
[providerDefaults, savedRouting],
|
||||
() => buildDraftRouting(savedRouting),
|
||||
[savedRouting],
|
||||
);
|
||||
const savedDraftSignature = useMemo(
|
||||
() => routingDraftSignature(savedDraftRouting, Boolean(providerDefaults)),
|
||||
[providerDefaults, savedDraftRouting],
|
||||
() => routingDraftSignature(savedDraftRouting),
|
||||
[savedDraftRouting],
|
||||
);
|
||||
|
||||
const [routing, setRouting] = useState<ChatGPTOAuthRoutingConfig>(savedDraftRouting);
|
||||
@@ -101,8 +101,8 @@ export function AgentCodexPoolPage() {
|
||||
const savedDraftSignatureRef = useRef(savedDraftSignature);
|
||||
|
||||
const draftSignature = useMemo(
|
||||
() => routingDraftSignature(routing, Boolean(providerDefaults)),
|
||||
[providerDefaults, routing],
|
||||
() => routingDraftSignature(routing),
|
||||
[routing],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
|
||||
export function buildDraftRouting(
|
||||
savedRouting: NormalizedChatGPTOAuthRouting,
|
||||
hasProviderDefaults: boolean,
|
||||
): ChatGPTOAuthRoutingConfig {
|
||||
if (savedRouting.isExplicit) {
|
||||
return {
|
||||
@@ -16,16 +15,8 @@ export function buildDraftRouting(
|
||||
};
|
||||
}
|
||||
|
||||
if (hasProviderDefaults) {
|
||||
return {
|
||||
override_mode: "inherit",
|
||||
strategy: "primary_first",
|
||||
extra_provider_names: [],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
override_mode: "custom",
|
||||
override_mode: "inherit",
|
||||
strategy: "primary_first",
|
||||
extra_provider_names: [],
|
||||
};
|
||||
@@ -33,10 +24,9 @@ export function buildDraftRouting(
|
||||
|
||||
export function routingDraftSignature(
|
||||
routing: ChatGPTOAuthRoutingConfig,
|
||||
hasProviderDefaults: boolean,
|
||||
): string {
|
||||
const normalized = normalizeChatGPTOAuthRoutingInput(routing);
|
||||
if (normalized.overrideMode === "inherit" && hasProviderDefaults) {
|
||||
if (normalized.overrideMode === "inherit") {
|
||||
return JSON.stringify({ override_mode: "inherit" });
|
||||
}
|
||||
return JSON.stringify({
|
||||
|
||||
+4
-4
@@ -43,7 +43,7 @@ interface MembershipSectionProps {
|
||||
membershipManagedByLabel?: string;
|
||||
currentProvider: string;
|
||||
selectedEntries: SelectedEntry[];
|
||||
readyExtraProviders: ProviderData[];
|
||||
selectableExtraProviders: ProviderData[];
|
||||
selectedExtras: Set<string>;
|
||||
quotaByName?: Map<string, ChatGPTOAuthProviderQuota>;
|
||||
canEditMembership: boolean;
|
||||
@@ -57,7 +57,7 @@ export function MembershipSection({
|
||||
membershipManagedByLabel,
|
||||
currentProvider,
|
||||
selectedEntries,
|
||||
readyExtraProviders,
|
||||
selectableExtraProviders,
|
||||
selectedExtras,
|
||||
quotaByName,
|
||||
canEditMembership,
|
||||
@@ -91,9 +91,9 @@ export function MembershipSection({
|
||||
<div className="rounded-lg border border-dashed px-3 py-3 text-sm text-muted-foreground">
|
||||
{t("chatgptOAuthRouting.loadingAccounts")}
|
||||
</div>
|
||||
) : readyExtraProviders.length > 0 ? (
|
||||
) : selectableExtraProviders.length > 0 ? (
|
||||
<div className="grid gap-2 xl:grid-cols-2">
|
||||
{readyExtraProviders.map((provider) => {
|
||||
{selectableExtraProviders.map((provider) => {
|
||||
const selected = selectedExtras.has(provider.name);
|
||||
const failureKind = getQuotaFailureKind(quotaByName?.get(provider.name));
|
||||
return (
|
||||
|
||||
+7
-5
@@ -103,13 +103,15 @@ export function ChatGPTOAuthRoutingSection({
|
||||
if (!currentOAuthProvider) return null;
|
||||
|
||||
const allExtraProviders = oauthProviders.filter((p) => p.name !== currentProvider);
|
||||
const readyExtraProviders = allExtraProviders.filter(
|
||||
(p) => getAvailabilityFromMap(p, statusByName) === "ready",
|
||||
const selectedExtras = new Set(value.extra_provider_names ?? []);
|
||||
const selectableExtraProviders = allExtraProviders.filter(
|
||||
(p) =>
|
||||
selectedExtras.has(p.name) ||
|
||||
getAvailabilityFromMap(p, statusByName) === "ready",
|
||||
);
|
||||
const mode = value.override_mode === "inherit" ? "inherit" : "custom";
|
||||
const providerDefaultsAvailable =
|
||||
defaultRouting != null && defaultRouting.extraProviderNames.length > 0;
|
||||
const selectedExtras = new Set(value.extra_provider_names ?? []);
|
||||
|
||||
const selectedEntries = entries.map((entry) => ({
|
||||
...entry,
|
||||
@@ -214,7 +216,7 @@ export function ChatGPTOAuthRoutingSection({
|
||||
type="button"
|
||||
variant={mode === "inherit" ? "default" : "outline"}
|
||||
onClick={() => setMode("inherit")}
|
||||
disabled={!canManageProviders || !providerDefaultsAvailable}
|
||||
disabled={!canManageProviders}
|
||||
className="h-9 [@media(max-height:760px)]:h-8"
|
||||
>
|
||||
{t("chatgptOAuthRouting.mode.inherit")}
|
||||
@@ -277,7 +279,7 @@ export function ChatGPTOAuthRoutingSection({
|
||||
membershipManagedByLabel={membershipManagedByLabel}
|
||||
currentProvider={currentProvider}
|
||||
selectedEntries={selectedEntries}
|
||||
readyExtraProviders={readyExtraProviders}
|
||||
selectableExtraProviders={selectableExtraProviders}
|
||||
selectedExtras={selectedExtras}
|
||||
quotaByName={quotaByName}
|
||||
canEditMembership={canEditMembership}
|
||||
|
||||
@@ -171,6 +171,19 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
|
||||
strategy: initialRouting?.strategy ?? "primary_first",
|
||||
extra_provider_names: initialRouting?.extraProviderNames ?? [],
|
||||
});
|
||||
const selectedPoolProviderNames = useMemo(
|
||||
() =>
|
||||
canEditPoolRouting
|
||||
? Array.from(
|
||||
new Set(
|
||||
[provider.name, ...(poolRouting.extra_provider_names ?? [])]
|
||||
.filter(Boolean)
|
||||
.filter((name) => name === provider.name || providerByName.has(name)),
|
||||
),
|
||||
)
|
||||
: [provider.name],
|
||||
[canEditPoolRouting, poolRouting.extra_provider_names, provider.name, providerByName],
|
||||
);
|
||||
|
||||
const initEmb = getEmbeddingSettings(provider.settings);
|
||||
const [embEnabled, setEmbEnabled] = useState(initEmb?.enabled ?? false);
|
||||
@@ -253,13 +266,9 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
|
||||
const quotaProviderNames = useMemo(
|
||||
() => {
|
||||
if (!isOAuth) return [];
|
||||
const candidateNames = [
|
||||
provider.name,
|
||||
...(canEditPoolRouting ? poolRouting.extra_provider_names ?? [] : []),
|
||||
];
|
||||
return Array.from(
|
||||
new Set(
|
||||
candidateNames.filter((providerName) => {
|
||||
selectedPoolProviderNames.filter((providerName) => {
|
||||
if (!providerName) return false;
|
||||
const item = providerByName.get(providerName);
|
||||
return (
|
||||
@@ -270,11 +279,9 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
|
||||
);
|
||||
},
|
||||
[
|
||||
canEditPoolRouting,
|
||||
isOAuth,
|
||||
poolRouting.extra_provider_names,
|
||||
provider.name,
|
||||
providerByName,
|
||||
selectedPoolProviderNames,
|
||||
statusByName,
|
||||
],
|
||||
);
|
||||
@@ -285,7 +292,7 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
|
||||
} = useChatGPTOAuthProviderQuotas(quotaProviderNames, isOAuth);
|
||||
const poolEntries = useMemo<CodexPoolEntry[]>(() => {
|
||||
if (!canEditPoolRouting) return [];
|
||||
return quotaProviderNames.map((providerName) => {
|
||||
return selectedPoolProviderNames.map((providerName) => {
|
||||
const item = providerByName.get(providerName);
|
||||
return {
|
||||
name: providerName,
|
||||
@@ -305,10 +312,10 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
|
||||
quota: quotaByName.get(providerName),
|
||||
};
|
||||
});
|
||||
}, [canEditPoolRouting, provider.name, providerByName, quotaByName, quotaProviderNames, statusByName]);
|
||||
}, [canEditPoolRouting, provider.name, providerByName, quotaByName, selectedPoolProviderNames, statusByName]);
|
||||
|
||||
// Provider-scoped pool activity (only for pool owners)
|
||||
const isPoolOwner = canEditPoolRouting && poolEntries.length > 0;
|
||||
const isPoolOwner = canEditPoolRouting && selectedPoolProviderNames.length > 1;
|
||||
const {
|
||||
data: poolActivity,
|
||||
isFetching: poolActivityFetching,
|
||||
|
||||
Reference in new issue
Block a user