1
0
forked from boos/BoosAPI

[codex] add oauth route pools and proxy save flow (#440)

* feat: add oauth route pools and proxy save flow

* fix: align oauth route pool schema and rate limits

* fix: harden oauth route pool review fixes

* fix: address oauth ci regressions

* fix: close oauth codeql alerts

* fix: harden oauth route unit recovery

* fix: split oauth route limiters for codeql

* fix: address remaining oauth review comments
This commit is contained in:
Cita
2026-04-06 11:55:20 +08:00
committed by GitHub
parent a789ddf7f8
commit 2f0e63a1e0
29 changed files with 8902 additions and 328 deletions
@@ -78,6 +78,73 @@ function buildChannel(overrides: Partial<RouteChannel> = {}): RouteChannel {
}
describe('RouteCard', () => {
it('renders oauth route unit summary and member labels on expanded channels', () => {
const root = create(
<RouteCard
route={buildRoute({
modelPattern: 'gpt-4.1',
displayName: 'gpt-4.1',
channelCount: 1,
enabledChannelCount: 1,
})}
brand={null}
expanded
onToggleExpand={vi.fn()}
onEdit={vi.fn()}
onDelete={vi.fn()}
onToggleEnabled={vi.fn()}
onClearCooldown={vi.fn()}
clearingCooldown={false}
onRoutingStrategyChange={vi.fn()}
updatingRoutingStrategy={false}
channels={[
buildChannel({
account: { username: 'route-unit-anchor' },
routeUnit: {
id: 'pool-1',
name: 'Codex Pool A',
strategy: 'round_robin',
memberCount: 3,
members: [
{ accountId: 101, username: 'route-unit-anchor', siteName: 'site-a' },
{ accountId: 102, username: 'route-unit-backup', siteName: 'site-b' },
{ accountId: 103, username: 'route-unit-third', siteName: 'site-c' },
],
},
}),
]}
loadingChannels={false}
routeDecision={null}
loadingDecision={false}
candidateView={{ routeCandidates: [], accountOptions: [], tokenOptionsByAccountId: {} }}
channelTokenDraft={{}}
updatingChannel={{}}
savingPriority={false}
onTokenDraftChange={vi.fn()}
onSaveToken={vi.fn()}
onDeleteChannel={vi.fn()}
onToggleChannelEnabled={vi.fn()}
onChannelDragEnd={vi.fn()}
missingTokenSiteItems={[]}
missingTokenGroupItems={[]}
onCreateTokenForMissing={vi.fn()}
onAddChannel={vi.fn()}
onSiteBlockModel={vi.fn()}
expandedSourceGroupMap={{}}
onToggleSourceGroup={vi.fn()}
/>,
);
const text = collectText(root.root);
expect(text).toContain('Codex Pool A');
expect(text).toContain('3 个成员');
expect(text).toContain('轮询');
expect(text).toContain('成员摘要');
expect(text).toContain('route-unit-anchor');
expect(text).toContain('route-unit-backup');
expect(text).toContain('route-unit-third');
});
it('truncates the collapsed regex badge while keeping the group name primary', () => {
const root = create(
<RouteCard
@@ -371,6 +438,70 @@ describe('RouteCard', () => {
expect(p0RailNode.props.style.color).not.toBe(p1RailNode.props.style.color);
});
it('renders oauth route unit summary badges on expanded cards', () => {
const root = create(
<RouteCard
route={buildRoute()}
brand={null}
expanded
onToggleExpand={vi.fn()}
onEdit={vi.fn()}
onDelete={vi.fn()}
onToggleEnabled={vi.fn()}
onClearCooldown={vi.fn()}
clearingCooldown={false}
onRoutingStrategyChange={vi.fn()}
updatingRoutingStrategy={false}
channels={[
buildChannel({
id: 11,
account: { username: 'pool-representative' },
site: { id: 1, name: 'site-a', platform: 'openai' },
...( {
routeUnit: {
id: 7,
name: 'Codex 池',
memberCount: 3,
strategy: 'round_robin',
members: [
{ accountId: 101, username: 'user_a', siteName: 'site-a' },
{ accountId: 102, username: 'user_b', siteName: 'site-b' },
{ accountId: 103, username: 'user_c', siteName: 'site-c' },
],
},
} as any ),
}),
]}
loadingChannels={false}
routeDecision={null}
loadingDecision={false}
candidateView={{ routeCandidates: [], accountOptions: [], tokenOptionsByAccountId: {} }}
channelTokenDraft={{}}
updatingChannel={{}}
savingPriority={false}
onTokenDraftChange={vi.fn()}
onSaveToken={vi.fn()}
onDeleteChannel={vi.fn()}
onToggleChannelEnabled={vi.fn()}
onChannelDragEnd={vi.fn()}
missingTokenSiteItems={[]}
missingTokenGroupItems={[]}
onCreateTokenForMissing={vi.fn()}
onAddChannel={vi.fn()}
onSiteBlockModel={vi.fn()}
expandedSourceGroupMap={{}}
onToggleSourceGroup={vi.fn()}
/>,
);
const text = collectText(root.root);
expect(text).toContain('OAuth 路由池');
expect(text).toContain('Codex 池');
expect(text).toContain('3 个成员');
expect(text).toContain('轮询');
expect(text).toContain('成员摘要');
});
it('uses translate-only rect sorting for flat channel shell rows', () => {
const root = create(
<RouteCard
+46
View File
@@ -25,6 +25,7 @@ import { formatDateTimeMinuteLocal } from '../helpers/checkinLogTime.js';
import type {
RouteSummaryRow,
RouteChannel,
RouteChannelRouteUnit,
RouteDecision,
RouteDecisionCandidate,
MissingTokenRouteSiteActionItem,
@@ -100,6 +101,24 @@ type RouteCardProps = {
onToggleSourceGroup: (groupKey: string) => void;
};
function getRouteUnitStrategyLabel(strategy: string | null | undefined): string {
return strategy === 'stick_until_unavailable' ? '单个用到不可用再切' : '轮询';
}
function collectRouteUnits(channels: RouteChannel[] | undefined): RouteChannelRouteUnit[] {
if (!Array.isArray(channels) || channels.length === 0) return [];
const unitsById = new Map<string, RouteChannelRouteUnit>();
for (const channel of channels) {
const routeUnit = channel.routeUnit;
if (!routeUnit) continue;
const key = String(routeUnit.id);
if (!unitsById.has(key)) {
unitsById.set(key, routeUnit);
}
}
return Array.from(unitsById.values());
}
function PriorityRailNewLayerRow({
id,
highlighted,
@@ -566,6 +585,7 @@ function RouteCardInner({
: undefined;
const showAddChannelButton = !readOnlyRoute && !channelManagementDisabled;
const showMissingTokenHints = !channelManagementDisabled && (missingTokenSiteItems.length > 0 || missingTokenGroupItems.length > 0);
const routeUnits = collectRouteUnits(channels);
const routingStrategyOptions = [
{
value: 'weighted',
@@ -975,6 +995,32 @@ function RouteCardInner({
</div>
) : null}
{routeUnits.length > 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 8 }}>
<div style={{ fontSize: 11.5, color: 'var(--color-text-secondary)' }}>
OAuth 路由池
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
{routeUnits.map((routeUnit) => (
<span
key={`route-unit-${String(routeUnit.id)}`}
className="badge badge-info"
style={{
fontSize: 10.5,
maxWidth: '100%',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
title={`${routeUnit.name?.trim() || 'OAuth 路由池'} · ${routeUnit.memberCount} 个成员 · ${getRouteUnitStrategyLabel(routeUnit.strategy)}`}
>
{(routeUnit.name?.trim() || 'OAuth 路由池')} · {routeUnit.memberCount} 个成员 · {getRouteUnitStrategyLabel(routeUnit.strategy)}
</span>
))}
</div>
</div>
) : null}
{!readOnlyRoute && (
<div
data-testid={compact ? 'compact-route-action-row' : undefined}
@@ -9,6 +9,16 @@ import {
} from './tokenBindingPresentation.js';
import { getChannelDecisionState, getPriorityTagStyle, getProbabilityColor } from './utils.js';
function getRouteUnitStrategyLabel(strategy: string | null | undefined): string {
return strategy === 'stick_until_unavailable' ? '单个用到不可用再切' : '轮询';
}
function formatRouteUnitMemberLabel(member: { accountId: number; username: string | null; siteName: string | null }): string {
const accountLabel = member.username?.trim() || `account-${member.accountId}`;
const siteLabel = member.siteName?.trim();
return siteLabel ? `${accountLabel} @ ${siteLabel}` : accountLabel;
}
export function SortableChannelRow({
channel,
displayPriority,
@@ -87,6 +97,13 @@ export function SortableChannelRow({
accountName: channel.account?.username || `account-${channel.accountId}`,
},
);
const routeUnit = channel.routeUnit ?? null;
const routeUnitName = routeUnit?.name?.trim() || 'OAuth 路由池';
const routeUnitStrategyLabel = routeUnit ? getRouteUnitStrategyLabel(routeUnit.strategy) : '';
const routeUnitMemberSummary = routeUnit?.members?.length
? routeUnit.members.map((member) => formatRouteUnitMemberLabel(member)).join('、')
: null;
const routeUnitMemberSummaryText = routeUnitMemberSummary ? `成员:${routeUnitMemberSummary}` : null;
const [mobileDetailsOpen, setMobileDetailsOpen] = useState(false);
@@ -193,8 +210,36 @@ export function SortableChannelRow({
手动配置
</span>
) : null}
{routeUnit ? (
<>
<span className="badge badge-muted" style={{ fontSize: 10 }}>
OAuth 路由池
</span>
<span className="badge badge-info" style={{ fontSize: 10 }}>
{routeUnitName}
</span>
<span className="badge badge-muted" style={{ fontSize: 10 }}>
{routeUnit.memberCount} 成员
</span>
<span className="badge badge-muted" style={{ fontSize: 10 }}>
{routeUnitStrategyLabel}
</span>
</>
) : null}
</div>
{routeUnitMemberSummaryText ? (
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 6, flexWrap: 'wrap' }}>
<span style={{ fontSize: 11, color: 'var(--color-text-muted)', whiteSpace: 'nowrap' }}>
成员摘要({routeUnit?.memberCount || 0} 个成员 · {routeUnitStrategyLabel})
</span>
<span style={{ fontSize: 11, color: 'var(--color-text-secondary)', lineHeight: 1.45 }}>
{routeUnitMemberSummaryText}
</span>
</div>
) : null}
<div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
<span style={{ fontSize: 11, color: 'var(--color-text-muted)', whiteSpace: 'nowrap' }}>选中概率</span>
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 5, minWidth: 96 }}>
@@ -404,6 +449,34 @@ export function SortableChannelRow({
<span className="badge badge-muted" style={{ fontSize: 10 }}>已禁用</span>
) : null}
{routeUnit ? (
<>
<span className="badge badge-muted" style={{ fontSize: 10 }}>
OAuth 路由池
</span>
<span className="badge badge-info" style={{ fontSize: 10 }}>
{routeUnitName}
</span>
<span className="badge badge-muted" style={{ fontSize: 10 }}>
{routeUnit.memberCount} 成员
</span>
<span className="badge badge-muted" style={{ fontSize: 10 }}>
{routeUnitStrategyLabel}
</span>
</>
) : null}
{routeUnitMemberSummaryText ? (
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 6, width: '100%', flexWrap: 'wrap' }}>
<span style={{ fontSize: 11, color: 'var(--color-text-muted)', whiteSpace: 'nowrap' }}>
成员摘要({routeUnit?.memberCount || 0} 个成员 · {routeUnitStrategyLabel})
</span>
<span style={{ fontSize: 11, color: 'var(--color-text-secondary)', lineHeight: 1.45 }}>
{routeUnitMemberSummaryText}
</span>
</div>
) : null}
<div style={{ display: 'flex', alignItems: 'center', gap: 6, width: '100%', marginTop: mobile ? 0 : 1, flexWrap: 'wrap' }}>
<span style={{ fontSize: 11, color: 'var(--color-text-muted)', whiteSpace: 'nowrap' }}>选中概率</span>
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 5, minWidth: 96 }}>
+17
View File
@@ -7,6 +7,7 @@ export type RouteSortBy = 'modelPattern' | 'channelCount';
export type RouteSortDir = 'asc' | 'desc';
export type GroupFilter = null | '__all__' | number;
export type RouteRoutingStrategy = 'weighted' | 'round_robin' | 'stable_first';
export type OAuthRouteUnitStrategy = 'round_robin' | 'stick_until_unavailable';
export type RouteRowKind = 'persisted' | 'zero_channel';
export type RouteChannelDraft = {
accountId: number;
@@ -14,6 +15,20 @@ export type RouteChannelDraft = {
sourceModel: string;
};
export type RouteChannelRouteUnitMember = {
accountId: number;
username: string | null;
siteName: string | null;
};
export type RouteChannelRouteUnit = {
id: number | string;
name: string | null;
strategy: OAuthRouteUnitStrategy;
memberCount: number;
members?: RouteChannelRouteUnitMember[];
};
export type RouteChannel = {
id: number;
routeId?: number;
@@ -45,6 +60,8 @@ export type RouteChannel = {
enabled: boolean;
isDefault: boolean;
} | null;
oauthRouteUnitId?: number | null;
routeUnit?: RouteChannelRouteUnit | null;
};
export type RouteRow = {