forked from boos/BoosAPI
[codex] improve oauth route unit feedback (#445)
* feat: improve oauth route unit feedback * fix: harden oauth route unit feedback flow
This commit is contained in:
@@ -2950,6 +2950,44 @@ textarea {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.oauth-page-message-info {
|
||||
border: 1px solid color-mix(in srgb, var(--color-primary) 18%, var(--color-border-light));
|
||||
background: color-mix(in srgb, var(--color-primary) 6%, var(--color-bg-card));
|
||||
}
|
||||
|
||||
.oauth-page-message-success {
|
||||
border: 1px solid color-mix(in srgb, var(--color-success) 22%, var(--color-border-light));
|
||||
background: color-mix(in srgb, var(--color-success) 8%, var(--color-bg-card));
|
||||
}
|
||||
|
||||
.oauth-page-message-error {
|
||||
border: 1px solid color-mix(in srgb, var(--color-danger) 22%, var(--color-border-light));
|
||||
background: color-mix(in srgb, var(--color-danger) 7%, var(--color-bg-card));
|
||||
}
|
||||
|
||||
.oauth-page-message-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.oauth-page-message-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.oauth-page-message-detail {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.oauth-workbench-card {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
@@ -571,6 +571,11 @@ describe('OAuthManagement page', () => {
|
||||
strategy: 'round_robin',
|
||||
});
|
||||
expect(apiMock.getOAuthConnections).toHaveBeenCalledTimes(2);
|
||||
expect(collectText(root.root)).toContain('已创建路由池');
|
||||
expect(collectText(root.root)).toContain('Codex Pool');
|
||||
expect(collectText(root.root)).toContain('2 个成员');
|
||||
expect(collectText(root.root)).toContain('轮询');
|
||||
expect(collectText(root.root)).toContain('已将选中的 OAuth 账号合并为一个路由池,后续会以单个路由单元参与路由。');
|
||||
expect(collectText(root.root)).toContain('路由池:Codex Pool · 2 个成员 · 轮询');
|
||||
} finally {
|
||||
root?.unmount();
|
||||
@@ -727,6 +732,227 @@ describe('OAuthManagement page', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps created route unit feedback when the connection list refresh fails', async () => {
|
||||
apiMock.getOAuthProviders.mockResolvedValue({
|
||||
providers: [
|
||||
{
|
||||
provider: 'codex',
|
||||
label: 'Codex',
|
||||
platform: 'codex',
|
||||
enabled: true,
|
||||
loginType: 'oauth',
|
||||
requiresProjectId: false,
|
||||
supportsDirectAccountRouting: true,
|
||||
supportsCloudValidation: true,
|
||||
supportsNativeProxy: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
apiMock.getOAuthConnections
|
||||
.mockResolvedValueOnce({
|
||||
items: [
|
||||
{
|
||||
accountId: 51,
|
||||
siteId: 8,
|
||||
provider: 'codex',
|
||||
email: 'pool-a@example.com',
|
||||
planType: 'team',
|
||||
modelCount: 2,
|
||||
modelsPreview: ['gpt-5.4'],
|
||||
status: 'healthy',
|
||||
routeParticipation: { kind: 'single' },
|
||||
site: {
|
||||
id: 8,
|
||||
name: 'ChatGPT Codex OAuth',
|
||||
url: 'https://chatgpt.com/backend-api/codex',
|
||||
platform: 'codex',
|
||||
},
|
||||
},
|
||||
{
|
||||
accountId: 52,
|
||||
siteId: 8,
|
||||
provider: 'codex',
|
||||
email: 'pool-b@example.com',
|
||||
planType: 'team',
|
||||
modelCount: 2,
|
||||
modelsPreview: ['gpt-5.4'],
|
||||
status: 'healthy',
|
||||
routeParticipation: { kind: 'single' },
|
||||
site: {
|
||||
id: 8,
|
||||
name: 'ChatGPT Codex OAuth',
|
||||
url: 'https://chatgpt.com/backend-api/codex',
|
||||
platform: 'codex',
|
||||
},
|
||||
},
|
||||
],
|
||||
total: 2,
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
})
|
||||
.mockRejectedValueOnce(new Error('列表刷新失败'));
|
||||
apiMock.createOAuthRouteUnit.mockResolvedValue({ success: true });
|
||||
|
||||
let root!: WebTestRenderer;
|
||||
try {
|
||||
await act(async () => {
|
||||
root = create(
|
||||
<ToastProvider>
|
||||
<MemoryRouter>
|
||||
<OAuthManagement />
|
||||
</MemoryRouter>
|
||||
</ToastProvider>,
|
||||
);
|
||||
});
|
||||
await flushMicrotasks();
|
||||
|
||||
const selectAll = root.root.find((node) => (
|
||||
node.type === 'input'
|
||||
&& node.props.type === 'checkbox'
|
||||
&& node.props['data-testid'] === 'oauth-select-all'
|
||||
));
|
||||
|
||||
await act(async () => {
|
||||
selectAll.props.onChange({ target: { checked: true } });
|
||||
});
|
||||
|
||||
await clickButton(root, '合并参与路由');
|
||||
|
||||
const nameInput = root.root.find((node) => (
|
||||
node.type === 'input'
|
||||
&& node.props['data-testid'] === 'oauth-route-unit-name'
|
||||
));
|
||||
await act(async () => {
|
||||
nameInput.props.onChange({ target: { value: 'Fallback Pool' } });
|
||||
});
|
||||
|
||||
await clickButton(root, '创建路由池');
|
||||
|
||||
expect(apiMock.createOAuthRouteUnit).toHaveBeenCalledWith({
|
||||
accountIds: [51, 52],
|
||||
name: 'Fallback Pool',
|
||||
strategy: 'round_robin',
|
||||
});
|
||||
expect(apiMock.getOAuthConnections).toHaveBeenCalledTimes(2);
|
||||
expect(collectText(root.root)).toContain('已创建路由池,但连接列表刷新失败');
|
||||
expect(collectText(root.root)).toContain('Fallback Pool');
|
||||
expect(collectText(root.root)).toContain('2 个成员');
|
||||
expect(collectText(root.root)).toContain('轮询');
|
||||
expect(collectText(root.root)).not.toContain('已选 2 项');
|
||||
} finally {
|
||||
root?.unmount();
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps split feedback when the connection list refresh fails', async () => {
|
||||
apiMock.getOAuthProviders.mockResolvedValue({
|
||||
providers: [
|
||||
{
|
||||
provider: 'codex',
|
||||
label: 'Codex',
|
||||
platform: 'codex',
|
||||
enabled: true,
|
||||
loginType: 'oauth',
|
||||
requiresProjectId: false,
|
||||
supportsDirectAccountRouting: true,
|
||||
supportsCloudValidation: true,
|
||||
supportsNativeProxy: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
apiMock.getOAuthConnections
|
||||
.mockResolvedValueOnce({
|
||||
items: [
|
||||
{
|
||||
accountId: 61,
|
||||
siteId: 8,
|
||||
provider: 'codex',
|
||||
email: 'split-a@example.com',
|
||||
planType: 'team',
|
||||
modelCount: 2,
|
||||
modelsPreview: ['gpt-5.4'],
|
||||
status: 'healthy',
|
||||
routeParticipation: {
|
||||
kind: 'route_unit',
|
||||
routeUnitId: 96,
|
||||
name: 'Sticky Pool',
|
||||
strategy: 'stick_until_unavailable',
|
||||
memberCount: 2,
|
||||
},
|
||||
site: {
|
||||
id: 8,
|
||||
name: 'ChatGPT Codex OAuth',
|
||||
url: 'https://chatgpt.com/backend-api/codex',
|
||||
platform: 'codex',
|
||||
},
|
||||
},
|
||||
{
|
||||
accountId: 62,
|
||||
siteId: 8,
|
||||
provider: 'codex',
|
||||
email: 'split-b@example.com',
|
||||
planType: 'team',
|
||||
modelCount: 2,
|
||||
modelsPreview: ['gpt-5.4'],
|
||||
status: 'healthy',
|
||||
routeParticipation: {
|
||||
kind: 'route_unit',
|
||||
routeUnitId: 96,
|
||||
name: 'Sticky Pool',
|
||||
strategy: 'stick_until_unavailable',
|
||||
memberCount: 2,
|
||||
},
|
||||
site: {
|
||||
id: 8,
|
||||
name: 'ChatGPT Codex OAuth',
|
||||
url: 'https://chatgpt.com/backend-api/codex',
|
||||
platform: 'codex',
|
||||
},
|
||||
},
|
||||
],
|
||||
total: 2,
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
})
|
||||
.mockRejectedValueOnce(new Error('列表刷新失败'));
|
||||
apiMock.deleteOAuthRouteUnit.mockResolvedValue({ success: true });
|
||||
|
||||
let root!: WebTestRenderer;
|
||||
try {
|
||||
await act(async () => {
|
||||
root = create(
|
||||
<ToastProvider>
|
||||
<MemoryRouter>
|
||||
<OAuthManagement />
|
||||
</MemoryRouter>
|
||||
</ToastProvider>,
|
||||
);
|
||||
});
|
||||
await flushMicrotasks();
|
||||
|
||||
const selectAll = root.root.find((node) => (
|
||||
node.type === 'input'
|
||||
&& node.props.type === 'checkbox'
|
||||
&& node.props['data-testid'] === 'oauth-select-all'
|
||||
));
|
||||
|
||||
await act(async () => {
|
||||
selectAll.props.onChange({ target: { checked: true } });
|
||||
});
|
||||
|
||||
await clickButton(root, '拆回单体');
|
||||
|
||||
expect(apiMock.deleteOAuthRouteUnit).toHaveBeenCalledWith(96);
|
||||
expect(apiMock.getOAuthConnections).toHaveBeenCalledTimes(2);
|
||||
expect(collectText(root.root)).toContain('已拆回单体,但连接列表刷新失败');
|
||||
expect(collectText(root.root)).toContain('Sticky Pool');
|
||||
expect(collectText(root.root)).toContain('2 个成员');
|
||||
expect(collectText(root.root)).toContain('单个用到不可用再切');
|
||||
} finally {
|
||||
root?.unmount();
|
||||
}
|
||||
});
|
||||
|
||||
it('only enables splitting when the full route pool is selected and falls back to routeUnit ids', async () => {
|
||||
apiMock.getOAuthProviders.mockResolvedValue({
|
||||
providers: [
|
||||
|
||||
@@ -102,6 +102,19 @@ type OAuthRouteUnitModalState = {
|
||||
strategy: OAuthRouteUnitStrategy;
|
||||
};
|
||||
|
||||
type SessionRouteUnitFeedback = {
|
||||
action: 'created' | 'deleted';
|
||||
name: string;
|
||||
memberCount: number;
|
||||
strategy: OAuthRouteUnitStrategy;
|
||||
};
|
||||
|
||||
type SessionFeedback = {
|
||||
message: string;
|
||||
tone: 'info' | 'success' | 'error';
|
||||
routeUnit?: SessionRouteUnitFeedback | null;
|
||||
};
|
||||
|
||||
const COLUMN_OPTIONS: Array<{ key: ColumnKey; label: string }> = [
|
||||
{ key: 'identity', label: '账号 / Provider' },
|
||||
{ key: 'site', label: '站点' },
|
||||
@@ -619,7 +632,7 @@ export default function OAuthManagement() {
|
||||
const [providers, setProviders] = useState<OAuthProviderInfo[]>([]);
|
||||
const [connections, setConnections] = useState<OAuthConnectionInfo[]>([]);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [sessionMessage, setSessionMessage] = useState('');
|
||||
const [sessionFeedback, setSessionFeedback] = useState<SessionFeedback | null>(null);
|
||||
const [actionLoadingKey, setActionLoadingKey] = useState('');
|
||||
const [selectedConnectionIds, setSelectedConnectionIds] = useState<number[]>([]);
|
||||
const [showMobileFilters, setShowMobileFilters] = useState(false);
|
||||
@@ -674,6 +687,47 @@ export default function OAuthManagement() {
|
||||
strategy: 'round_robin',
|
||||
});
|
||||
|
||||
const setSessionMessage = useCallback((
|
||||
message: string,
|
||||
tone: SessionFeedback['tone'],
|
||||
options?: {
|
||||
routeUnit?: SessionRouteUnitFeedback | null;
|
||||
},
|
||||
) => {
|
||||
setSessionFeedback({
|
||||
message,
|
||||
tone,
|
||||
routeUnit: options?.routeUnit ?? null,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setSessionInfo = useCallback((
|
||||
message: string,
|
||||
options?: {
|
||||
routeUnit?: SessionRouteUnitFeedback | null;
|
||||
},
|
||||
) => {
|
||||
setSessionMessage(message, 'info', options);
|
||||
}, [setSessionMessage]);
|
||||
|
||||
const setSessionSuccess = useCallback((
|
||||
message: string,
|
||||
options?: {
|
||||
routeUnit?: SessionRouteUnitFeedback | null;
|
||||
},
|
||||
) => {
|
||||
setSessionMessage(message, 'success', options);
|
||||
}, [setSessionMessage]);
|
||||
|
||||
const setSessionError = useCallback((
|
||||
message: string,
|
||||
options?: {
|
||||
routeUnit?: SessionRouteUnitFeedback | null;
|
||||
},
|
||||
) => {
|
||||
setSessionMessage(message, 'error', options);
|
||||
}, [setSessionMessage]);
|
||||
|
||||
const resetOauthProxySettings = useCallback(() => {
|
||||
setOauthCustomProxyEnabled(false);
|
||||
setOauthSystemProxyEnabled(false);
|
||||
@@ -727,7 +781,7 @@ export default function OAuthManagement() {
|
||||
setSelectedProviderKey((current) => current || nextProviders[0]?.provider || '');
|
||||
} catch (error: any) {
|
||||
console.error('failed to load oauth management data', error);
|
||||
setSessionMessage(error?.message || 'OAuth 管理数据加载失败');
|
||||
setSessionError(error?.message || 'OAuth 管理数据加载失败');
|
||||
} finally {
|
||||
setLoaded(true);
|
||||
}
|
||||
@@ -748,7 +802,7 @@ export default function OAuthManagement() {
|
||||
setAutoRefreshCountdown((current) => {
|
||||
if (current <= 1) {
|
||||
void loadConnections().catch((error: any) => {
|
||||
setSessionMessage(error?.message || 'OAuth 连接列表刷新失败');
|
||||
setSessionError(error?.message || 'OAuth 连接列表刷新失败');
|
||||
});
|
||||
return autoRefreshSeconds;
|
||||
}
|
||||
@@ -770,7 +824,7 @@ export default function OAuthManagement() {
|
||||
setSelectedProviderKey(provider);
|
||||
setDrawerProjectId('');
|
||||
setDrawerOpen(true);
|
||||
setSessionMessage('从建站流程跳转到 OAuth 管理,请在这里完成授权。');
|
||||
setSessionInfo('从建站流程跳转到 OAuth 管理,请在这里完成授权。');
|
||||
}, [loaded, location.search, providers]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -785,23 +839,23 @@ export default function OAuthManagement() {
|
||||
if (cancelled) return;
|
||||
|
||||
if (session.status === 'pending') {
|
||||
setSessionMessage('等待授权完成');
|
||||
setSessionInfo('等待授权完成');
|
||||
timer = setTimeout(poll, POLL_INTERVAL_MS);
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.status === 'success') {
|
||||
setSessionMessage('授权成功');
|
||||
setSessionSuccess('授权成功');
|
||||
await loadConnections();
|
||||
setActiveSession(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setSessionMessage(`授权失败:${session.error || '未知错误'}`);
|
||||
setSessionError(`授权失败:${session.error || '未知错误'}`);
|
||||
setActiveSession(null);
|
||||
} catch (error: any) {
|
||||
if (cancelled) return;
|
||||
setSessionMessage(error?.message || 'OAuth 会话状态查询失败');
|
||||
setSessionError(error?.message || 'OAuth 会话状态查询失败');
|
||||
setActiveSession(null);
|
||||
}
|
||||
};
|
||||
@@ -956,7 +1010,7 @@ export default function OAuthManagement() {
|
||||
setOauthProxyUrl(connection.useSystemProxy ? '' : asTrimmedString(connection.proxyUrl));
|
||||
setDrawerOpen(true);
|
||||
setShowColumnMenu(false);
|
||||
setSessionMessage('已打开 OAuth 代理设置,修改后可直接保存代理,或保存后重新授权。');
|
||||
setSessionInfo('已打开 OAuth 代理设置,修改后可直接保存代理,或保存后重新授权。');
|
||||
};
|
||||
|
||||
const openRouteUnitModal = () => {
|
||||
@@ -1025,7 +1079,7 @@ export default function OAuthManagement() {
|
||||
if (drawerIntent.mode !== 'proxy') return;
|
||||
const customProxyUrl = asTrimmedString(oauthProxyUrl);
|
||||
if (oauthCustomProxyEnabled && !customProxyUrl) {
|
||||
setSessionMessage('已开启代理,请先输入完整代理地址');
|
||||
setSessionError('已开启代理,请先输入完整代理地址');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1044,9 +1098,9 @@ export default function OAuthManagement() {
|
||||
await loadConnections();
|
||||
setDrawerOpen(false);
|
||||
resetOauthProxySettings();
|
||||
setSessionMessage('代理已保存');
|
||||
setSessionSuccess('代理已保存');
|
||||
} catch (error: any) {
|
||||
setSessionMessage(error?.message || '保存代理失败');
|
||||
setSessionError(error?.message || '保存代理失败');
|
||||
} finally {
|
||||
setActionLoadingKey('');
|
||||
}
|
||||
@@ -1060,11 +1114,11 @@ export default function OAuthManagement() {
|
||||
|| providers[0]
|
||||
|| null;
|
||||
if (!provider) {
|
||||
setSessionMessage('当前没有可用的 OAuth Provider。');
|
||||
setSessionError('当前没有可用的 OAuth Provider。');
|
||||
return;
|
||||
}
|
||||
if (!provider.enabled) {
|
||||
setSessionMessage(`${provider.label} 当前环境未启用`);
|
||||
setSessionError(`${provider.label} 当前环境未启用`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1072,7 +1126,7 @@ export default function OAuthManagement() {
|
||||
const accountId = rebindAccount?.accountId;
|
||||
const customProxyUrl = asTrimmedString(oauthProxyUrl);
|
||||
if (oauthCustomProxyEnabled && !customProxyUrl) {
|
||||
setSessionMessage('已开启代理,请先输入完整代理地址');
|
||||
setSessionError('已开启代理,请先输入完整代理地址');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1102,7 +1156,7 @@ export default function OAuthManagement() {
|
||||
...proxySettings,
|
||||
});
|
||||
|
||||
setSessionMessage('等待授权完成');
|
||||
setSessionInfo('等待授权完成');
|
||||
setActiveSession({
|
||||
provider: started.provider,
|
||||
state: started.state,
|
||||
@@ -1112,7 +1166,7 @@ export default function OAuthManagement() {
|
||||
resetOauthProxySettings();
|
||||
openOAuthPopup(provider.provider, started.authorizationUrl);
|
||||
} catch (error: any) {
|
||||
setSessionMessage(error?.message || '无法启动 OAuth 授权');
|
||||
setSessionError(error?.message || '无法启动 OAuth 授权');
|
||||
} finally {
|
||||
setActionLoadingKey('');
|
||||
}
|
||||
@@ -1122,15 +1176,15 @@ export default function OAuthManagement() {
|
||||
if (!activeSession) return;
|
||||
const callbackUrl = manualCallbackUrl.trim();
|
||||
if (!callbackUrl) {
|
||||
setSessionMessage('请输入完整的回调 URL');
|
||||
setSessionError('请输入完整的回调 URL');
|
||||
return;
|
||||
}
|
||||
setManualCallbackSubmitting(true);
|
||||
try {
|
||||
await api.submitOAuthManualCallback(activeSession.state, callbackUrl);
|
||||
setSessionMessage('回调已提交,等待授权完成');
|
||||
setSessionInfo('回调已提交,等待授权完成');
|
||||
} catch (error: any) {
|
||||
setSessionMessage(error?.message || '提交回调 URL 失败');
|
||||
setSessionError(error?.message || '提交回调 URL 失败');
|
||||
} finally {
|
||||
setManualCallbackSubmitting(false);
|
||||
}
|
||||
@@ -1145,11 +1199,11 @@ export default function OAuthManagement() {
|
||||
setActionLoadingKey(actionKey);
|
||||
try {
|
||||
await api.deleteOAuthConnection(accountId);
|
||||
setSessionMessage('连接已删除');
|
||||
setSessionSuccess('连接已删除');
|
||||
await loadConnections();
|
||||
setSelectedConnectionIds((current) => current.filter((id) => id !== accountId));
|
||||
} catch (error: any) {
|
||||
setSessionMessage(error?.message || '删除 OAuth 连接失败');
|
||||
setSessionError(error?.message || '删除 OAuth 连接失败');
|
||||
} finally {
|
||||
setActionLoadingKey('');
|
||||
}
|
||||
@@ -1167,9 +1221,11 @@ export default function OAuthManagement() {
|
||||
const failed = results.filter((item) => item.status === 'rejected').length;
|
||||
await loadConnections();
|
||||
setSelectedConnectionIds([]);
|
||||
setSessionMessage(failed > 0
|
||||
? `批量删除完成,${failed} 个连接删除失败`
|
||||
: `已删除 ${results.length} 个 OAuth 连接`);
|
||||
if (failed > 0) {
|
||||
setSessionInfo(`批量删除完成,${failed} 个连接删除失败`);
|
||||
} else {
|
||||
setSessionSuccess(`已删除 ${results.length} 个 OAuth 连接`);
|
||||
}
|
||||
} finally {
|
||||
setActionLoadingKey('');
|
||||
}
|
||||
@@ -1180,10 +1236,10 @@ export default function OAuthManagement() {
|
||||
setActionLoadingKey(actionKey);
|
||||
try {
|
||||
await api.refreshOAuthConnectionQuota(accountId);
|
||||
setSessionMessage('额度信息已刷新');
|
||||
setSessionSuccess('额度信息已刷新');
|
||||
await loadConnections();
|
||||
} catch (error: any) {
|
||||
setSessionMessage(error?.message || '刷新额度失败');
|
||||
setSessionError(error?.message || '刷新额度失败');
|
||||
} finally {
|
||||
setActionLoadingKey('');
|
||||
}
|
||||
@@ -1195,11 +1251,13 @@ export default function OAuthManagement() {
|
||||
try {
|
||||
const result = await api.refreshOAuthConnectionQuotaBatch(selectedConnectionIds);
|
||||
await loadConnections();
|
||||
setSessionMessage(result.failed > 0
|
||||
? `批量刷新完成,成功 ${result.refreshed} 个,失败 ${result.failed} 个`
|
||||
: `已批量刷新 ${result.refreshed} 个 OAuth 连接`);
|
||||
if (result.failed > 0) {
|
||||
setSessionInfo(`批量刷新完成,成功 ${result.refreshed} 个,失败 ${result.failed} 个`);
|
||||
} else {
|
||||
setSessionSuccess(`已批量刷新 ${result.refreshed} 个 OAuth 连接`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setSessionMessage(error?.message || '批量刷新额度失败');
|
||||
setSessionError(error?.message || '批量刷新额度失败');
|
||||
} finally {
|
||||
setActionLoadingKey('');
|
||||
}
|
||||
@@ -1251,11 +1309,11 @@ export default function OAuthManagement() {
|
||||
applyLoadedModelsModal(connection, result);
|
||||
if (options.refreshUpstream) {
|
||||
await loadConnections();
|
||||
setSessionMessage('模型列表已刷新');
|
||||
setSessionSuccess('模型列表已刷新');
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (modelsModalRequestSeqRef.current !== requestId) return;
|
||||
setSessionMessage(error?.message || '加载模型列表失败');
|
||||
setSessionError(error?.message || '加载模型列表失败');
|
||||
setModelsModal((current) => (
|
||||
options.closeOnError
|
||||
? {
|
||||
@@ -1342,15 +1400,15 @@ export default function OAuthManagement() {
|
||||
|
||||
const handleImport = async () => {
|
||||
if (importSources.length <= 0) {
|
||||
setSessionMessage('请先选择 JSON 文件或粘贴 OAuth 连接 JSON 内容');
|
||||
setSessionError('请先选择 JSON 文件或粘贴 OAuth 连接 JSON 内容');
|
||||
return;
|
||||
}
|
||||
if (!importPreviewSummary?.canImport) {
|
||||
setSessionMessage('请先修正无效的 OAuth JSON');
|
||||
setSessionError('请先修正无效的 OAuth JSON');
|
||||
return;
|
||||
}
|
||||
if (importCustomProxyEnabled && !asTrimmedString(importProxyUrl)) {
|
||||
setSessionMessage('已开启代理,请先输入完整代理地址');
|
||||
setSessionError('已开启代理,请先输入完整代理地址');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1384,12 +1442,16 @@ export default function OAuthManagement() {
|
||||
} else {
|
||||
toast.success(importMessage);
|
||||
}
|
||||
setSessionMessage(importMessage);
|
||||
if (result.failed > 0) {
|
||||
setSessionInfo(importMessage);
|
||||
} else {
|
||||
setSessionSuccess(importMessage);
|
||||
}
|
||||
closeImportModal();
|
||||
} catch (error: any) {
|
||||
const message = error?.message || '导入 OAuth JSON 失败';
|
||||
toast.error(message);
|
||||
setSessionMessage(message);
|
||||
setSessionError(message);
|
||||
} finally {
|
||||
setImporting(false);
|
||||
}
|
||||
@@ -1399,23 +1461,49 @@ export default function OAuthManagement() {
|
||||
const name = asTrimmedString(routeUnitModal.name);
|
||||
if (!canMergeSelectedIntoRouteUnit || selectedConnections.length < 2) return;
|
||||
if (!name) {
|
||||
setSessionMessage('请先输入路由池名称');
|
||||
setSessionError('请先输入路由池名称');
|
||||
return;
|
||||
}
|
||||
|
||||
setActionLoadingKey('route-unit:create');
|
||||
try {
|
||||
await api.createOAuthRouteUnit({
|
||||
const result = await api.createOAuthRouteUnit({
|
||||
accountIds: selectedConnections.map((connection) => connection.accountId),
|
||||
name,
|
||||
strategy: routeUnitModal.strategy,
|
||||
});
|
||||
await loadConnections();
|
||||
const routeUnitDefaults: SessionRouteUnitFeedback = {
|
||||
action: 'created' as const,
|
||||
name: asTrimmedString(name) || name,
|
||||
memberCount: selectedConnections.length,
|
||||
strategy: routeUnitModal.strategy,
|
||||
};
|
||||
const routeUnit: SessionRouteUnitFeedback = result.routeUnit
|
||||
? {
|
||||
...routeUnitDefaults,
|
||||
name: asTrimmedString(result.routeUnit.name) || routeUnitDefaults.name,
|
||||
memberCount: result.routeUnit.memberCount || routeUnitDefaults.memberCount,
|
||||
strategy: result.routeUnit.strategy || routeUnitDefaults.strategy,
|
||||
}
|
||||
: routeUnitDefaults;
|
||||
toast.success(`已创建路由池:${routeUnit.name}`);
|
||||
setSessionSuccess('已创建路由池', {
|
||||
routeUnit,
|
||||
});
|
||||
setSelectedConnectionIds([]);
|
||||
closeRouteUnitModal();
|
||||
setSessionMessage('已合并为路由池');
|
||||
try {
|
||||
await loadConnections();
|
||||
} catch {
|
||||
toast.error('OAuth 连接列表刷新失败');
|
||||
setSessionError('已创建路由池,但连接列表刷新失败', {
|
||||
routeUnit,
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
setSessionMessage(error?.message || '创建路由池失败');
|
||||
const message = error?.message || '创建路由池失败';
|
||||
toast.error(message);
|
||||
setSessionError(message);
|
||||
} finally {
|
||||
setActionLoadingKey('');
|
||||
}
|
||||
@@ -1428,12 +1516,30 @@ export default function OAuthManagement() {
|
||||
|
||||
setActionLoadingKey('route-unit:delete');
|
||||
try {
|
||||
const routeUnitFeedback = {
|
||||
action: 'deleted' as const,
|
||||
name: selectedRouteUnitParticipation.name,
|
||||
memberCount: selectedRouteUnitParticipation.memberCount,
|
||||
strategy: selectedRouteUnitParticipation.strategy,
|
||||
};
|
||||
await api.deleteOAuthRouteUnit(routeUnitId);
|
||||
await loadConnections();
|
||||
toast.success(`已拆回单体:${routeUnitFeedback.name}`);
|
||||
setSessionSuccess('已拆回单体', {
|
||||
routeUnit: routeUnitFeedback,
|
||||
});
|
||||
setSelectedConnectionIds([]);
|
||||
setSessionMessage('已拆回单体');
|
||||
try {
|
||||
await loadConnections();
|
||||
} catch {
|
||||
toast.error('OAuth 连接列表刷新失败');
|
||||
setSessionError('已拆回单体,但连接列表刷新失败', {
|
||||
routeUnit: routeUnitFeedback,
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
setSessionMessage(error?.message || '拆回单体失败');
|
||||
const message = error?.message || '拆回单体失败';
|
||||
toast.error(message);
|
||||
setSessionError(message);
|
||||
} finally {
|
||||
setActionLoadingKey('');
|
||||
}
|
||||
@@ -1930,9 +2036,26 @@ export default function OAuthManagement() {
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{sessionMessage ? (
|
||||
<div className="card oauth-page-message">
|
||||
<div className="oauth-page-message-text">{sessionMessage}</div>
|
||||
{sessionFeedback ? (
|
||||
<div className={`card oauth-page-message oauth-page-message-${sessionFeedback.tone}`.trim()}>
|
||||
<div className="oauth-page-message-head">
|
||||
<div className="oauth-page-message-text">{sessionFeedback.message}</div>
|
||||
<span className={`badge ${sessionFeedback.tone === 'success' ? 'badge-success' : sessionFeedback.tone === 'error' ? 'badge-danger' : 'badge-info'}`}>
|
||||
{sessionFeedback.tone === 'success' ? '成功' : sessionFeedback.tone === 'error' ? '失败' : '提示'}
|
||||
</span>
|
||||
</div>
|
||||
{sessionFeedback.routeUnit ? (
|
||||
<div className="oauth-page-message-meta">
|
||||
<span className="badge badge-info">{sessionFeedback.routeUnit.name}</span>
|
||||
<span className="badge badge-muted">{sessionFeedback.routeUnit.memberCount} 个成员</span>
|
||||
<span className="badge badge-muted">{resolveRouteUnitStrategyLabel(sessionFeedback.routeUnit.strategy)}</span>
|
||||
<div className="oauth-page-message-detail">
|
||||
{sessionFeedback.routeUnit.action === 'created'
|
||||
? '已将选中的 OAuth 账号合并为一个路由池,后续会以单个路由单元参与路由。'
|
||||
: '该路由池已拆分回单体账号,后续会分别参与路由。'}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user