[codex] improve proxy stability, routing, and site subscription visibility (#194)

* wip: preserve local changes before upstream merge

* Improve proxy routing stability and add stable-first strategy
This commit is contained in:
Bainily
2026-03-20 16:18:28 +08:00
committed by GitHub
parent e2595bcab3
commit e9c1bd898e
37 changed files with 3424 additions and 236 deletions
+143 -14
View File
@@ -23,6 +23,16 @@ import {
type SiteForm,
} from './helpers/sitesEditor.js';
type SiteSubscriptionSummary = {
activeCount: number;
totalUsedUsd: number;
totalMonthlyLimitUsd?: number | null;
totalRemainingUsd?: number | null;
nextExpiresAt?: string | null;
planNames?: string[];
updatedAt?: number | null;
};
type SiteRow = {
id: number;
name: string;
@@ -36,6 +46,7 @@ type SiteRow = {
isPinned?: boolean;
sortOrder?: number;
totalBalance?: number;
subscriptionSummary?: SiteSubscriptionSummary | null;
createdAt?: string;
};
@@ -43,6 +54,105 @@ function hasConfiguredCustomHeaders(customHeaders?: string | null): boolean {
return typeof customHeaders === 'string' && customHeaders.trim().length > 0;
}
function formatUsd(value?: number | null): string {
return `$${(value || 0).toFixed(2)}`;
}
function formatSubscriptionDate(value?: string | null): string {
if (!value) return '';
const parsed = Date.parse(value);
if (!Number.isFinite(parsed)) return value;
return new Date(parsed).toISOString().slice(0, 10);
}
function formatRemainingDuration(value?: string | null): string | null {
if (!value) return null;
const targetMs = Date.parse(value);
if (!Number.isFinite(targetMs)) return null;
const deltaMs = targetMs - Date.now();
if (deltaMs <= 0) return '已到期';
const minuteMs = 60 * 1000;
const hourMs = 60 * minuteMs;
const dayMs = 24 * hourMs;
if (deltaMs >= dayMs) return `剩余${Math.ceil(deltaMs / dayMs)}天`;
if (deltaMs >= hourMs) return `剩余${Math.ceil(deltaMs / hourMs)}小时`;
if (deltaMs >= minuteMs) return `剩余${Math.ceil(deltaMs / minuteMs)}分钟`;
return `剩余${Math.max(1, Math.ceil(deltaMs / 1000))}秒`;
}
function buildSubscriptionInlineValue(summary?: SiteSubscriptionSummary | null): string | null {
if (!summary) return null;
const remainingValue = typeof summary.totalRemainingUsd === 'number' && Number.isFinite(summary.totalRemainingUsd)
? formatUsd(summary.totalRemainingUsd)
: '--';
const usedValue = formatUsd(summary.totalUsedUsd);
const remainingDuration = formatRemainingDuration(summary.nextExpiresAt);
const remainingSuffix = remainingDuration ? `(${remainingDuration})` : '';
if (usedValue === '$0.00' && remainingValue === '--' && !remainingSuffix) return null;
return `${remainingValue}${remainingSuffix}`;
}
function buildSubscriptionTooltip(summary?: SiteSubscriptionSummary | null): string | null {
if (!summary) return null;
const parts: string[] = [];
if (summary.activeCount > 0) parts.push(`生效订阅 ${summary.activeCount} 个`);
const planNames = Array.isArray(summary.planNames)
? summary.planNames.filter((item) => typeof item === 'string' && item.trim())
: [];
if (planNames.length > 0) parts.push(`套餐 ${planNames.join(' / ')}`);
if (typeof summary.totalRemainingUsd === 'number' && Number.isFinite(summary.totalRemainingUsd)) {
parts.push(`订阅余额 ${formatUsd(summary.totalRemainingUsd)}`);
}
parts.push(`已用 ${formatUsd(summary.totalUsedUsd)}`);
if (typeof summary.totalMonthlyLimitUsd === 'number' && Number.isFinite(summary.totalMonthlyLimitUsd)) {
parts.push(`总额度 ${formatUsd(summary.totalMonthlyLimitUsd)}`);
}
const remainingDuration = formatRemainingDuration(summary.nextExpiresAt);
if (remainingDuration) parts.push(remainingDuration);
if (summary.nextExpiresAt) parts.push(`到期 ${formatSubscriptionDate(summary.nextExpiresAt)}`);
return parts.join(' | ');
}
function SiteBalanceDisplay(props: {
balance?: number | null;
summary?: SiteSubscriptionSummary | null;
align?: 'start' | 'end';
}) {
const { balance, summary, align = 'start' } = props;
const walletBalanceText = formatUsd(balance);
const subscriptionValue = buildSubscriptionInlineValue(summary);
const tooltip = buildSubscriptionTooltip(summary);
return (
<div
className={`site-balance-inline ${align === 'end' ? 'align-end' : ''}`.trim()}
>
<span className="site-balance-primary">{walletBalanceText}</span>
{subscriptionValue ? (
<>
<span className="site-balance-divider">/</span>
<span
className="site-balance-subscription"
data-tooltip={tooltip || undefined}
data-tooltip-align={align === 'end' ? 'end' : 'start'}
data-tooltip-side="top"
tabIndex={tooltip ? 0 : undefined}
>
{subscriptionValue}
</span>
</>
) : null}
</div>
);
}
const platformColors: Record<string, string> = {
'new-api': 'badge-info',
'one-api': 'badge-success',
@@ -873,7 +983,16 @@ export default function Sites() {
</span>
)}
/>
<MobileField label="余额" value={`$${(site.totalBalance || 0).toFixed(2)}`} />
<MobileField
label="余额"
value={(
<SiteBalanceDisplay
balance={site.totalBalance}
summary={site.subscriptionSummary}
align="end"
/>
)}
/>
<MobileField label="权重" value={(site.globalWeight || 1).toFixed(2)} />
{isExpanded ? (
<div className="mobile-card-extra">
@@ -1017,17 +1136,24 @@ export default function Sites() {
/>
</td>
<td style={{ fontWeight: 600 }}>
<a
href={site.url}
target="_blank"
rel="noopener noreferrer"
style={{
color: 'var(--color-text-primary)',
textDecoration: 'underline',
}}
>
{site.name}
</a>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, alignItems: 'flex-start' }}>
<a
href={site.url}
target="_blank"
rel="noopener noreferrer"
style={{
color: 'var(--color-text-primary)',
textDecoration: 'underline',
}}
>
{site.name}
</a>
{hasConfiguredCustomHeaders(site.customHeaders) ? (
<span className="badge badge-info" style={{ fontSize: 11 }}>
自定义头
</span>
) : null}
</div>
</td>
<td className="sites-url-cell" style={{ maxWidth: 300 }}>
{site.externalCheckinUrl ? (
@@ -1048,8 +1174,11 @@ export default function Sites() {
</a>
) : null}
</td>
<td style={{ fontVariantNumeric: 'tabular-nums', fontWeight: 600 }}>
${(site.totalBalance || 0).toFixed(2)}
<td className="site-balance-cell">
<SiteBalanceDisplay
balance={site.totalBalance}
summary={site.subscriptionSummary}
/>
</td>
<td>
<span className={`badge ${site.status === 'disabled' ? 'badge-muted' : 'badge-success'}`} style={{ fontSize: 11 }}>
+21 -3
View File
@@ -88,6 +88,24 @@ const EMPTY_ROUTE_FORM: RouteEditorForm = {
advancedOpen: false,
};
function normalizeRouteRoutingStrategyValue(value?: RouteRoutingStrategy | null): RouteRoutingStrategy {
if (value === 'round_robin' || value === 'stable_first') return value;
return 'weighted';
}
function getRouteRoutingStrategyLabel(value?: RouteRoutingStrategy | null): string {
const strategy = normalizeRouteRoutingStrategyValue(value);
if (strategy === 'round_robin') return tr('轮询');
if (strategy === 'stable_first') return tr('稳定优先');
return tr('权重随机');
}
function getRouteRoutingStrategySuccessMessage(value: RouteRoutingStrategy): string {
if (value === 'round_robin') return '已切换为轮询策略';
if (value === 'stable_first') return '已切换为稳定优先策略';
return '已切换为权重随机策略';
}
export default function TokenRoutes() {
const navigate = useNavigate();
const [routeSummaries, setRouteSummaries] = useState<RouteSummaryRow[]>([]);
@@ -421,7 +439,7 @@ export default function TokenRoutes() {
};
const handleRoutingStrategyChange = async (route: RouteSummaryRow, routingStrategy: RouteRoutingStrategy) => {
const currentStrategy = route.routingStrategy === 'round_robin' ? 'round_robin' : 'weighted';
const currentStrategy = normalizeRouteRoutingStrategyValue(route.routingStrategy);
if (routingStrategy === currentStrategy) return;
setUpdatingRoutingStrategyByRoute((prev) => ({ ...prev, [route.id]: true }));
@@ -432,7 +450,7 @@ export default function TokenRoutes() {
)));
try {
await api.updateRoute(route.id, { routingStrategy });
toast.success(routingStrategy === 'round_robin' ? '已切换为轮询策略' : '已切换为权重随机策略');
toast.success(getRouteRoutingStrategySuccessMessage(routingStrategy));
} catch (e: any) {
setRouteSummaries((prev) => prev.map((item) => (
item.id === route.id
@@ -1186,7 +1204,7 @@ export default function TokenRoutes() {
>
<MobileField label="模型" value={route.modelPattern} />
<MobileField label="通道" value={route.channelCount} />
<MobileField label="策略" value={isReadOnlyRoute ? tr('未生成') : (route.routingStrategy === 'round_robin' ? tr('轮询') : tr('权重随机'))} />
<MobileField label="策略" value={isReadOnlyRoute ? tr('未生成') : getRouteRoutingStrategyLabel(route.routingStrategy)} />
<MobileField label="状态" value={isReadOnlyRoute ? tr('未生成') : (route.enabled ? tr('启用') : tr('禁用'))} />
<div className="mobile-card-actions">
<button
+19 -2
View File
@@ -72,6 +72,18 @@ type RouteCardProps = {
onToggleSourceGroup: (groupKey: string) => void;
};
function normalizeRoutingStrategy(value?: RouteRoutingStrategy | null): RouteRoutingStrategy {
if (value === 'round_robin' || value === 'stable_first') return value;
return 'weighted';
}
function getRoutingStrategyLabel(value?: RouteRoutingStrategy | null): string {
const strategy = normalizeRoutingStrategy(value);
if (strategy === 'round_robin') return tr('轮询');
if (strategy === 'stable_first') return tr('稳定优先');
return tr('权重随机');
}
function AnimatedCollapseSection({ open, children }: { open: boolean; children: ReactNode }) {
const presence = useAnimatedVisibility(open, 220);
if (!presence.shouldRender) return null;
@@ -120,7 +132,7 @@ function RouteCardInner({
const readOnlyRoute = route.kind === 'zero_channel' || route.readOnly === true || route.isVirtual === true;
const channelManagementDisabled = explicitGroupRoute;
const title = resolveRouteTitle(route);
const routingStrategy = route.routingStrategy === 'round_robin' ? 'round_robin' : 'weighted';
const routingStrategy = normalizeRoutingStrategy(route.routingStrategy);
const routingStrategyOptions = [
{
value: 'weighted',
@@ -132,6 +144,11 @@ function RouteCardInner({
label: tr('轮询'),
description: tr('按全局顺序轮流调用,忽略优先级,连续失败 3 次后进入分级冷却'),
},
{
value: 'stable_first',
label: tr('稳定优先'),
description: tr('按优先级优先选择当前最稳、最快、成功率更高的通道,不做随机分流'),
},
] as const;
const sensors = useSensors(
@@ -228,7 +245,7 @@ function RouteCardInner({
</span>
) : (
<span className="badge badge-muted" style={{ fontSize: 10, flexShrink: 0 }}>
{routingStrategy === 'round_robin' ? tr('轮询') : tr('权重随机')}
{getRoutingStrategyLabel(routingStrategy)}
</span>
)}
+1 -1
View File
@@ -4,7 +4,7 @@ import type { BrandInfo } from '../../components/BrandIcon.js';
export type RouteSortBy = 'modelPattern' | 'channelCount';
export type RouteSortDir = 'asc' | 'desc';
export type GroupFilter = null | '__all__' | number;
export type RouteRoutingStrategy = 'weighted' | 'round_robin';
export type RouteRoutingStrategy = 'weighted' | 'round_robin' | 'stable_first';
export type RouteRowKind = 'persisted' | 'zero_channel';
export type RouteMode = 'pattern' | 'explicit_group';
@@ -127,4 +127,53 @@ describe('TokenRoutes routing strategy updates', () => {
root?.unmount();
}
});
it('supports switching to stable_first and keeps the optimistic label when refresh fails', async () => {
let root: ReturnType<typeof create> | null = null;
try {
await act(async () => {
root = create(
<MemoryRouter initialEntries={['/routes']}>
<ToastProvider>
<TokenRoutes />
</ToastProvider>
</MemoryRouter>,
);
});
await flushMicrotasks();
const expandButton = root.root.find((node) => (
node.type === 'div'
&& String(node.props.className || '').includes('route-card-collapsed')
));
await act(async () => {
expandButton.props.onClick();
});
await flushMicrotasks();
const stableFirstOption = root.root.find((node) => (
node.type === 'button'
&& typeof node.props.className === 'string'
&& node.props.className.includes('modern-select-option')
&& collectText(node).includes('稳定优先')
));
await act(async () => {
stableFirstOption.props.onClick();
});
await flushMicrotasks();
expect(apiMock.updateRoute).toHaveBeenCalledWith(1, { routingStrategy: 'stable_first' });
const strategyTrigger = root.root.find((node) => (
node.type === 'button'
&& typeof node.props.className === 'string'
&& node.props.className.includes('modern-select-trigger')
&& collectText(node).includes('稳定优先')
));
expect(collectText(strategyTrigger)).toContain('稳定优先');
} finally {
root?.unmount();
}
});
});