[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
+452 -49
View File
@@ -23,6 +23,8 @@ import {
api,
type OAuthConnectionInfo,
type OAuthProviderInfo,
type OAuthRouteParticipation,
type OAuthRouteUnitStrategy,
type OAuthQuotaInfo,
type OAuthQuotaWindowInfo,
type OAuthStartInstructions,
@@ -94,6 +96,12 @@ type OAuthModelsModalState = {
siteName: string;
};
type OAuthRouteUnitModalState = {
open: boolean;
name: string;
strategy: OAuthRouteUnitStrategy;
};
const COLUMN_OPTIONS: Array<{ key: ColumnKey; label: string }> = [
{ key: 'identity', label: '账号 / Provider' },
{ key: 'site', label: '站点' },
@@ -450,6 +458,47 @@ function hasOauthProxySelection(connection: OAuthConnectionInfo): boolean {
return !!connection.useSystemProxy || !!asTrimmedString(connection.proxyUrl);
}
function resolveRouteUnitStrategyLabel(strategy?: OAuthRouteUnitStrategy | null): string {
return strategy === 'stick_until_unavailable' ? '单个用到不可用再切' : '轮询';
}
function resolveConnectionRouteParticipation(
connection: OAuthConnectionInfo,
): OAuthRouteParticipation {
if (connection.routeParticipation?.kind === 'route_unit') {
return {
...connection.routeParticipation,
id: connection.routeParticipation.id ?? connection.routeUnit?.id,
routeUnitId: connection.routeParticipation.routeUnitId
?? connection.routeParticipation.id
?? connection.routeUnit?.routeUnitId
?? connection.routeUnit?.id,
};
}
if (connection.routeParticipation?.kind === 'single') {
return connection.routeParticipation;
}
if (connection.routeUnit) {
return {
kind: 'route_unit',
routeUnitId: connection.routeUnit.routeUnitId ?? connection.routeUnit.id,
id: connection.routeUnit.id,
name: connection.routeUnit.name,
strategy: connection.routeUnit.strategy,
memberCount: connection.routeUnit.memberCount,
};
}
return { kind: 'single' };
}
function resolveRouteParticipationSummary(connection: OAuthConnectionInfo): string {
const participation = resolveConnectionRouteParticipation(connection);
if (participation.kind !== 'route_unit') {
return '单体';
}
return `路由池:${participation.name} · ${participation.memberCount} 个成员 · ${resolveRouteUnitStrategyLabel(participation.strategy)}`;
}
function renderCodeBlock(value: string) {
return (
<code className="oauth-code-block">{value}</code>
@@ -588,12 +637,16 @@ export default function OAuthManagement() {
const [showColumnMenu, setShowColumnMenu] = useState(false);
const [autoRefreshSeconds, setAutoRefreshSeconds] = useState<number>(0);
const [autoRefreshCountdown, setAutoRefreshCountdown] = useState<number>(0);
const [runtimeSystemProxyConfigured, setRuntimeSystemProxyConfigured] = useState(false);
const [importOpen, setImportOpen] = useState(false);
const importFileInputRef = useRef<HTMLInputElement | null>(null);
const [importJsonText, setImportJsonText] = useState('');
const [importDrafts, setImportDrafts] = useState<OAuthImportDraft[]>([]);
const [importDragOver, setImportDragOver] = useState(false);
const [importing, setImporting] = useState(false);
const [importCustomProxyEnabled, setImportCustomProxyEnabled] = useState(false);
const [importSystemProxyEnabled, setImportSystemProxyEnabled] = useState(false);
const [importProxyUrl, setImportProxyUrl] = useState('');
const [drawerOpen, setDrawerOpen] = useState(false);
const [drawerIntent, setDrawerIntent] = useState<DrawerIntent>({ mode: 'create' });
const [selectedProviderKey, setSelectedProviderKey] = useState('');
@@ -615,6 +668,11 @@ export default function OAuthManagement() {
disabledCount: 0,
siteName: '',
});
const [routeUnitModal, setRouteUnitModal] = useState<OAuthRouteUnitModalState>({
open: false,
name: '',
strategy: 'round_robin',
});
const resetOauthProxySettings = useCallback(() => {
setOauthCustomProxyEnabled(false);
@@ -622,6 +680,12 @@ export default function OAuthManagement() {
setOauthProxyUrl('');
}, []);
const resetImportProxySettings = useCallback((defaultToSystem = false) => {
setImportCustomProxyEnabled(false);
setImportSystemProxyEnabled(defaultToSystem);
setImportProxyUrl('');
}, []);
const resetImportState = useCallback(() => {
setImportJsonText('');
setImportDrafts([]);
@@ -631,12 +695,14 @@ export default function OAuthManagement() {
const closeImportModal = useCallback(() => {
setImportOpen(false);
resetImportState();
}, [resetImportState]);
resetImportProxySettings(false);
}, [resetImportProxySettings, resetImportState]);
const openImportModal = useCallback(() => {
resetImportState();
resetImportProxySettings(runtimeSystemProxyConfigured);
setImportOpen(true);
}, [resetImportState]);
}, [resetImportProxySettings, resetImportState, runtimeSystemProxyConfigured]);
const loadConnections = useCallback(async () => {
const response = await api.getOAuthConnections({
@@ -656,6 +722,7 @@ export default function OAuthManagement() {
loadConnections(),
]);
const nextProviders = Array.isArray(providersResponse?.providers) ? providersResponse.providers : [];
setRuntimeSystemProxyConfigured(providersResponse?.defaults?.systemProxyConfigured === true);
setProviders(nextProviders);
setSelectedProviderKey((current) => current || nextProviders[0]?.provider || '');
} catch (error: any) {
@@ -815,6 +882,53 @@ export default function OAuthManagement() {
const allVisibleSelected = filteredConnections.length > 0
&& filteredConnections.every((connection) => selectedConnectionIds.includes(connection.accountId));
const selectedConnections = useMemo(
() => connections.filter((connection) => selectedConnectionIds.includes(connection.accountId)),
[connections, selectedConnectionIds],
);
const selectedRouteUnitParticipation = useMemo(() => {
if (selectedConnections.length <= 0) return null;
const participations = selectedConnections.map(resolveConnectionRouteParticipation);
if (participations.some((item) => item.kind !== 'route_unit')) return null;
const first = participations[0];
if (!first || first.kind !== 'route_unit') return null;
const routeUnitId = first.routeUnitId ?? first.id;
if (!routeUnitId) return null;
const allSameRouteUnit = participations.every((item) => (
item.kind === 'route_unit'
&& (item.routeUnitId ?? item.id) === routeUnitId
));
if (!allSameRouteUnit) return null;
const totalRouteUnitMembers = connections.filter((connection) => {
const participation = resolveConnectionRouteParticipation(connection);
return participation.kind === 'route_unit'
&& (participation.routeUnitId ?? participation.id) === routeUnitId;
}).length;
if (totalRouteUnitMembers !== selectedConnections.length) return null;
return {
...first,
memberCount: Math.max(first.memberCount, totalRouteUnitMembers),
};
}, [connections, selectedConnections]);
const canMergeSelectedIntoRouteUnit = useMemo(() => {
if (selectedConnections.length < 2) return false;
const first = selectedConnections[0];
if (!first) return false;
const firstSiteId = first.siteId;
const firstProvider = first.provider;
return selectedConnections.every((connection) => (
connection.siteId === firstSiteId
&& connection.provider === firstProvider
&& resolveConnectionRouteParticipation(connection).kind === 'single'
));
}, [selectedConnections]);
const canSplitSelectedRouteUnit = selectedRouteUnitParticipation != null;
const openCreateDrawer = (provider?: string) => {
setDrawerIntent({ mode: 'create', provider });
setSelectedProviderKey(provider || providers[0]?.provider || '');
@@ -842,7 +956,100 @@ export default function OAuthManagement() {
setOauthProxyUrl(connection.useSystemProxy ? '' : asTrimmedString(connection.proxyUrl));
setDrawerOpen(true);
setShowColumnMenu(false);
setSessionMessage('已打开 OAuth 代理设置,修改后点击“保存代理并重新授权”。');
setSessionMessage('已打开 OAuth 代理设置,修改后可直接保存代理,或保存后重新授权。');
};
const openRouteUnitModal = () => {
setRouteUnitModal({
open: true,
name: '',
strategy: 'round_robin',
});
};
const closeRouteUnitModal = () => {
setRouteUnitModal((current) => ({
...current,
open: false,
}));
};
const resolveProxySettingsPayload = ({
customEnabled,
systemEnabled,
proxyValue,
fallbackAccount,
clearToSiteFallback = false,
}: {
customEnabled: boolean;
systemEnabled: boolean;
proxyValue: string;
fallbackAccount?: OAuthConnectionInfo | null;
clearToSiteFallback?: boolean;
}): { proxyUrl?: string | null; useSystemProxy?: boolean } => {
const customProxyUrl = asTrimmedString(proxyValue);
if (customEnabled) {
return {
proxyUrl: customProxyUrl,
useSystemProxy: false,
};
}
if (systemEnabled) {
return {
proxyUrl: null,
useSystemProxy: true,
};
}
if (clearToSiteFallback) {
return {
proxyUrl: null,
useSystemProxy: false,
};
}
if (fallbackAccount?.useSystemProxy) {
return {
proxyUrl: null,
useSystemProxy: true,
};
}
if (fallbackAccount?.proxyUrl !== undefined) {
return {
proxyUrl: asTrimmedString(fallbackAccount.proxyUrl) || null,
useSystemProxy: false,
};
}
return {};
};
const handleSaveProxy = async () => {
if (drawerIntent.mode !== 'proxy') return;
const customProxyUrl = asTrimmedString(oauthProxyUrl);
if (oauthCustomProxyEnabled && !customProxyUrl) {
setSessionMessage('已开启代理,请先输入完整代理地址');
return;
}
const actionKey = `save-proxy:${drawerIntent.account.accountId}`;
setActionLoadingKey(actionKey);
try {
await api.updateOAuthConnectionProxy(
drawerIntent.account.accountId,
resolveProxySettingsPayload({
customEnabled: oauthCustomProxyEnabled,
systemEnabled: oauthSystemProxyEnabled,
proxyValue: oauthProxyUrl,
clearToSiteFallback: true,
}),
);
await loadConnections();
setDrawerOpen(false);
resetOauthProxySettings();
setSessionMessage('代理已保存');
} catch (error: any) {
setSessionMessage(error?.message || '保存代理失败');
} finally {
setActionLoadingKey('');
}
};
const handleStart = async () => {
@@ -869,24 +1076,13 @@ export default function OAuthManagement() {
return;
}
let resolvedProxyUrl: string | null | undefined;
let resolvedUseSystemProxy: boolean | undefined;
if (oauthCustomProxyEnabled) {
resolvedProxyUrl = customProxyUrl;
resolvedUseSystemProxy = false;
} else if (oauthSystemProxyEnabled) {
resolvedProxyUrl = null;
resolvedUseSystemProxy = true;
} else if (drawerIntent.mode === 'proxy') {
resolvedProxyUrl = null;
resolvedUseSystemProxy = false;
} else if (rebindAccount?.useSystemProxy) {
resolvedProxyUrl = null;
resolvedUseSystemProxy = true;
} else if (rebindAccount?.proxyUrl !== undefined) {
resolvedProxyUrl = asTrimmedString(rebindAccount.proxyUrl) || null;
resolvedUseSystemProxy = false;
}
const proxySettings = resolveProxySettingsPayload({
customEnabled: oauthCustomProxyEnabled,
systemEnabled: oauthSystemProxyEnabled,
proxyValue: oauthProxyUrl,
fallbackAccount: rebindAccount,
clearToSiteFallback: drawerIntent.mode === 'proxy',
});
const actionKey = `start:${provider.provider}:${accountId || 0}`;
setActionLoadingKey(actionKey);
@@ -898,14 +1094,12 @@ export default function OAuthManagement() {
? await api.rebindOAuthConnection(
accountId,
{
...(resolvedProxyUrl !== undefined ? { proxyUrl: resolvedProxyUrl } : {}),
...(resolvedUseSystemProxy !== undefined ? { useSystemProxy: resolvedUseSystemProxy } : {}),
...proxySettings,
},
)
: await api.startOAuthProvider(provider.provider, {
projectId,
...(resolvedProxyUrl !== undefined ? { proxyUrl: resolvedProxyUrl } : {}),
...(resolvedUseSystemProxy !== undefined ? { useSystemProxy: resolvedUseSystemProxy } : {}),
...proxySettings,
});
setSessionMessage('等待授权完成');
@@ -1155,21 +1349,42 @@ export default function OAuthManagement() {
setSessionMessage('请先修正无效的 OAuth JSON');
return;
}
if (importCustomProxyEnabled && !asTrimmedString(importProxyUrl)) {
setSessionMessage('已开启代理,请先输入完整代理地址');
return;
}
setImporting(true);
try {
let importedCount = 0;
for (const item of importPreviewSummary.items) {
if (!item.valid || !item.parsedData) continue;
const result = await api.importOAuthConnections(item.parsedData);
importedCount += result.imported;
}
const parsedItems = importPreviewSummary.items
.filter((item) => item.valid && item.parsedData)
.map((item) => item.parsedData as Record<string, unknown>);
const importProxySettings = importSystemProxyEnabled && !importCustomProxyEnabled
? { useSystemProxy: true as const }
: resolveProxySettingsPayload({
customEnabled: importCustomProxyEnabled,
systemEnabled: importSystemProxyEnabled,
proxyValue: importProxyUrl,
});
const result = parsedItems.length === 1
&& !('proxyUrl' in importProxySettings)
&& !('useSystemProxy' in importProxySettings)
? await api.importOAuthConnections(parsedItems[0]!)
: await api.importOAuthConnections({
items: parsedItems,
...importProxySettings,
});
await loadConnections();
const successMessage = `已添加 ${importedCount} 个 OAuth 连接`;
toast.success(successMessage);
setSessionMessage(successMessage);
const importMessage = result.failed > 0
? `批量导入完成,成功 ${result.imported} 个,失败 ${result.failed} 个`
: `已添加 ${result.imported} 个 OAuth 连接`;
if (result.failed > 0) {
toast.info(importMessage);
} else {
toast.success(importMessage);
}
setSessionMessage(importMessage);
closeImportModal();
} catch (error: any) {
const message = error?.message || '导入 OAuth JSON 失败';
@@ -1180,6 +1395,50 @@ export default function OAuthManagement() {
}
};
const handleCreateRouteUnit = async () => {
const name = asTrimmedString(routeUnitModal.name);
if (!canMergeSelectedIntoRouteUnit || selectedConnections.length < 2) return;
if (!name) {
setSessionMessage('请先输入路由池名称');
return;
}
setActionLoadingKey('route-unit:create');
try {
await api.createOAuthRouteUnit({
accountIds: selectedConnections.map((connection) => connection.accountId),
name,
strategy: routeUnitModal.strategy,
});
await loadConnections();
setSelectedConnectionIds([]);
closeRouteUnitModal();
setSessionMessage('已合并为路由池');
} catch (error: any) {
setSessionMessage(error?.message || '创建路由池失败');
} finally {
setActionLoadingKey('');
}
};
const handleDeleteSelectedRouteUnit = async () => {
if (!selectedRouteUnitParticipation) return;
const routeUnitId = selectedRouteUnitParticipation.routeUnitId ?? selectedRouteUnitParticipation.id;
if (!routeUnitId) return;
setActionLoadingKey('route-unit:delete');
try {
await api.deleteOAuthRouteUnit(routeUnitId);
await loadConnections();
setSelectedConnectionIds([]);
setSessionMessage('已拆回单体');
} catch (error: any) {
setSessionMessage(error?.message || '拆回单体失败');
} finally {
setActionLoadingKey('');
}
};
const toggleColumn = (key: ColumnKey) => {
setVisibleColumns((current) => ({
...current,
@@ -1489,6 +1748,7 @@ export default function OAuthManagement() {
<td className="oauth-col-proxy">
<div className="oauth-cell-stack">
<div className="oauth-cell-secondary">{resolveProxyProjectSummary(connection)}</div>
<div className="oauth-cell-secondary">{resolveRouteParticipationSummary(connection)}</div>
<div className="oauth-cell-tertiary">{resolveProxyDisplayText(connection)}</div>
<button
type="button"
@@ -1566,6 +1826,7 @@ export default function OAuthManagement() {
<MobileField label="站点" value={connection.site?.name || '--'} />
<MobileField label="邮箱" value={resolveConnectionEmailLabel(connection) || '--'} />
<MobileField label="计划 / 项目" value={connection.projectId ? `${connection.planType || '--'} · ${connection.projectId}` : (connection.planType || '--')} />
<MobileField label="路由参与" value={resolveRouteParticipationSummary(connection)} />
<MobileField
label="账号代理"
value={(
@@ -1719,6 +1980,25 @@ export default function OAuthManagement() {
>
{actionLoadingKey === 'quota:selected' ? '刷新中...' : '批量刷新额度'}
</button>
{canMergeSelectedIntoRouteUnit ? (
<button
type="button"
className="btn btn-ghost oauth-outline-button"
onClick={openRouteUnitModal}
>
合并参与路由
</button>
) : null}
{canSplitSelectedRouteUnit ? (
<button
type="button"
className="btn btn-ghost oauth-outline-button"
onClick={handleDeleteSelectedRouteUnit}
disabled={actionLoadingKey === 'route-unit:delete'}
>
{actionLoadingKey === 'route-unit:delete' ? '拆分中...' : '拆回单体'}
</button>
) : null}
<button
type="button"
className="btn btn-link btn-link-danger"
@@ -1806,7 +2086,7 @@ export default function OAuthManagement() {
<div className="oauth-form-note">
{drawerIntent.mode === 'proxy'
? '这里修改的是账号级 OAuth 代理。保存后会立刻按当前设置重新走一次授权,后续生成的账号也会沿用这份代理配置。若两项都不勾选,则回退到站点代理配置。'
? '这里修改的是账号级 OAuth 代理。点击“保存代理”会立即落库并刷新列表;只有“保存并重新授权”才会重新走授权流程。若两项都不勾选,则回退到站点代理配置。'
: '这里的设置会作用于下一次“连接”或“重新授权”。填写代理地址后,本次 OAuth 换 token 和后续生成的账号都会直接带上这份账号级代理配置;若不勾选,则回退到站点代理配置。'}
</div>
@@ -1855,20 +2135,47 @@ export default function OAuthManagement() {
/>
</div>
<button
type="button"
className="btn btn-primary"
onClick={handleStart}
disabled={!selectedProvider || !selectedProvider.enabled || actionLoadingKey.startsWith('start:')}
>
{actionLoadingKey.startsWith('start:')
? '启动中...'
: drawerIntent.mode === 'proxy'
? '保存代理并重新授权'
{drawerIntent.mode === 'proxy' ? (
<div className="oauth-inline-actions">
<button
type="button"
className="btn btn-primary"
onClick={handleSaveProxy}
disabled={
actionLoadingKey === `save-proxy:${drawerIntent.account.accountId}`
|| actionLoadingKey.startsWith('start:')
}
>
{actionLoadingKey === `save-proxy:${drawerIntent.account.accountId}` ? '保存中...' : '保存代理'}
</button>
<button
type="button"
className="btn btn-ghost"
onClick={handleStart}
disabled={
!selectedProvider
|| !selectedProvider.enabled
|| actionLoadingKey.startsWith('start:')
|| actionLoadingKey === `save-proxy:${drawerIntent.account.accountId}`
}
>
{actionLoadingKey.startsWith('start:') ? '启动中...' : '保存并重新授权'}
</button>
</div>
) : (
<button
type="button"
className="btn btn-primary"
onClick={handleStart}
disabled={!selectedProvider || !selectedProvider.enabled || actionLoadingKey.startsWith('start:')}
>
{actionLoadingKey.startsWith('start:')
? '启动中...'
: drawerIntent.mode === 'rebind'
? `重新授权 ${selectedProvider?.label || ''}`.trim()
: `连接 ${selectedProvider?.label || ''}`.trim()}
</button>
? `重新授权 ${selectedProvider?.label || ''}`.trim()
: `连接 ${selectedProvider?.label || ''}`.trim()}
</button>
)}
</div>
</div>
@@ -2025,6 +2332,52 @@ export default function OAuthManagement() {
</>
)}
</div>
<div className="oauth-form-note">
导入后的账号代理可在这里一次性指定;如果当前运行时已配置系统代理,会默认预选“使用系统级代理”。
</div>
<div className="oauth-toggle-group">
<label className="oauth-toggle">
<input
type="checkbox"
checked={importSystemProxyEnabled}
data-oauth-import-setting="use-system-proxy"
onChange={(event) => {
const checked = !!event.target.checked;
setImportSystemProxyEnabled(checked);
if (checked) {
setImportCustomProxyEnabled(false);
setImportProxyUrl('');
}
}}
/>
<span>使用系统级代理</span>
</label>
<label className="oauth-toggle">
<input
type="checkbox"
checked={importCustomProxyEnabled}
data-oauth-import-setting="use-custom-proxy"
onChange={(event) => {
const checked = !!event.target.checked;
setImportCustomProxyEnabled(checked);
if (checked) setImportSystemProxyEnabled(false);
}}
/>
<span>使用自定义代理</span>
</label>
</div>
<div className="oauth-form-field">
<div className="oauth-field-label">代理地址</div>
<input
type="text"
className="oauth-input"
value={importProxyUrl}
data-oauth-import-setting="proxy-url"
onChange={(event) => setImportProxyUrl(event.target.value)}
placeholder="如 http://127.0.0.1:7890 或 socks5://127.0.0.1:1080"
disabled={!importCustomProxyEnabled}
/>
</div>
<div className="oauth-import-copy">或者手动粘贴单个 JSON 内容:</div>
<textarea
className="oauth-textarea oauth-mono"
@@ -2070,6 +2423,56 @@ export default function OAuthManagement() {
</div>
) : null}
</CenteredModal>
<CenteredModal
open={routeUnitModal.open}
onClose={closeRouteUnitModal}
title="创建路由池"
maxWidth={520}
bodyStyle={{ display: 'grid', gap: 16 }}
footer={(
<>
<button type="button" className="btn btn-ghost" onClick={closeRouteUnitModal}>
取消
</button>
<button
type="button"
className="btn btn-primary"
onClick={handleCreateRouteUnit}
disabled={actionLoadingKey === 'route-unit:create' || !asTrimmedString(routeUnitModal.name)}
>
{actionLoadingKey === 'route-unit:create' ? '创建中...' : '创建路由池'}
</button>
</>
)}
>
<div className="oauth-form-field">
<div className="oauth-field-label">路由池名称</div>
<input
type="text"
className="oauth-input"
data-testid="oauth-route-unit-name"
value={routeUnitModal.name}
onChange={(event) => setRouteUnitModal((current) => ({ ...current, name: event.target.value }))}
placeholder="例如 Codex Pool"
/>
</div>
<div className="oauth-form-field">
<div className="oauth-field-label">策略</div>
<ModernSelect
value={routeUnitModal.strategy}
onChange={(value) => setRouteUnitModal((current) => ({
...current,
strategy: String(value || 'round_robin') as OAuthRouteUnitStrategy,
}))}
options={[
{ value: 'round_robin', label: '轮询' },
{ value: 'stick_until_unavailable', label: '单个用到不可用再切' },
]}
placeholder="选择路由池策略"
/>
</div>
</CenteredModal>
</div>
);
}