[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;
|
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 {
|
.oauth-workbench-card {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -571,6 +571,11 @@ describe('OAuthManagement page', () => {
|
|||||||
strategy: 'round_robin',
|
strategy: 'round_robin',
|
||||||
});
|
});
|
||||||
expect(apiMock.getOAuthConnections).toHaveBeenCalledTimes(2);
|
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 个成员 · 轮询');
|
expect(collectText(root.root)).toContain('路由池:Codex Pool · 2 个成员 · 轮询');
|
||||||
} finally {
|
} finally {
|
||||||
root?.unmount();
|
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 () => {
|
it('only enables splitting when the full route pool is selected and falls back to routeUnit ids', async () => {
|
||||||
apiMock.getOAuthProviders.mockResolvedValue({
|
apiMock.getOAuthProviders.mockResolvedValue({
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -102,6 +102,19 @@ type OAuthRouteUnitModalState = {
|
|||||||
strategy: OAuthRouteUnitStrategy;
|
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 }> = [
|
const COLUMN_OPTIONS: Array<{ key: ColumnKey; label: string }> = [
|
||||||
{ key: 'identity', label: '账号 / Provider' },
|
{ key: 'identity', label: '账号 / Provider' },
|
||||||
{ key: 'site', label: '站点' },
|
{ key: 'site', label: '站点' },
|
||||||
@@ -619,7 +632,7 @@ export default function OAuthManagement() {
|
|||||||
const [providers, setProviders] = useState<OAuthProviderInfo[]>([]);
|
const [providers, setProviders] = useState<OAuthProviderInfo[]>([]);
|
||||||
const [connections, setConnections] = useState<OAuthConnectionInfo[]>([]);
|
const [connections, setConnections] = useState<OAuthConnectionInfo[]>([]);
|
||||||
const [loaded, setLoaded] = useState(false);
|
const [loaded, setLoaded] = useState(false);
|
||||||
const [sessionMessage, setSessionMessage] = useState('');
|
const [sessionFeedback, setSessionFeedback] = useState<SessionFeedback | null>(null);
|
||||||
const [actionLoadingKey, setActionLoadingKey] = useState('');
|
const [actionLoadingKey, setActionLoadingKey] = useState('');
|
||||||
const [selectedConnectionIds, setSelectedConnectionIds] = useState<number[]>([]);
|
const [selectedConnectionIds, setSelectedConnectionIds] = useState<number[]>([]);
|
||||||
const [showMobileFilters, setShowMobileFilters] = useState(false);
|
const [showMobileFilters, setShowMobileFilters] = useState(false);
|
||||||
@@ -674,6 +687,47 @@ export default function OAuthManagement() {
|
|||||||
strategy: 'round_robin',
|
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(() => {
|
const resetOauthProxySettings = useCallback(() => {
|
||||||
setOauthCustomProxyEnabled(false);
|
setOauthCustomProxyEnabled(false);
|
||||||
setOauthSystemProxyEnabled(false);
|
setOauthSystemProxyEnabled(false);
|
||||||
@@ -727,7 +781,7 @@ export default function OAuthManagement() {
|
|||||||
setSelectedProviderKey((current) => current || nextProviders[0]?.provider || '');
|
setSelectedProviderKey((current) => current || nextProviders[0]?.provider || '');
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('failed to load oauth management data', error);
|
console.error('failed to load oauth management data', error);
|
||||||
setSessionMessage(error?.message || 'OAuth 管理数据加载失败');
|
setSessionError(error?.message || 'OAuth 管理数据加载失败');
|
||||||
} finally {
|
} finally {
|
||||||
setLoaded(true);
|
setLoaded(true);
|
||||||
}
|
}
|
||||||
@@ -748,7 +802,7 @@ export default function OAuthManagement() {
|
|||||||
setAutoRefreshCountdown((current) => {
|
setAutoRefreshCountdown((current) => {
|
||||||
if (current <= 1) {
|
if (current <= 1) {
|
||||||
void loadConnections().catch((error: any) => {
|
void loadConnections().catch((error: any) => {
|
||||||
setSessionMessage(error?.message || 'OAuth 连接列表刷新失败');
|
setSessionError(error?.message || 'OAuth 连接列表刷新失败');
|
||||||
});
|
});
|
||||||
return autoRefreshSeconds;
|
return autoRefreshSeconds;
|
||||||
}
|
}
|
||||||
@@ -770,7 +824,7 @@ export default function OAuthManagement() {
|
|||||||
setSelectedProviderKey(provider);
|
setSelectedProviderKey(provider);
|
||||||
setDrawerProjectId('');
|
setDrawerProjectId('');
|
||||||
setDrawerOpen(true);
|
setDrawerOpen(true);
|
||||||
setSessionMessage('从建站流程跳转到 OAuth 管理,请在这里完成授权。');
|
setSessionInfo('从建站流程跳转到 OAuth 管理,请在这里完成授权。');
|
||||||
}, [loaded, location.search, providers]);
|
}, [loaded, location.search, providers]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -785,23 +839,23 @@ export default function OAuthManagement() {
|
|||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
|
|
||||||
if (session.status === 'pending') {
|
if (session.status === 'pending') {
|
||||||
setSessionMessage('等待授权完成');
|
setSessionInfo('等待授权完成');
|
||||||
timer = setTimeout(poll, POLL_INTERVAL_MS);
|
timer = setTimeout(poll, POLL_INTERVAL_MS);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session.status === 'success') {
|
if (session.status === 'success') {
|
||||||
setSessionMessage('授权成功');
|
setSessionSuccess('授权成功');
|
||||||
await loadConnections();
|
await loadConnections();
|
||||||
setActiveSession(null);
|
setActiveSession(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSessionMessage(`授权失败:${session.error || '未知错误'}`);
|
setSessionError(`授权失败:${session.error || '未知错误'}`);
|
||||||
setActiveSession(null);
|
setActiveSession(null);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
setSessionMessage(error?.message || 'OAuth 会话状态查询失败');
|
setSessionError(error?.message || 'OAuth 会话状态查询失败');
|
||||||
setActiveSession(null);
|
setActiveSession(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -956,7 +1010,7 @@ export default function OAuthManagement() {
|
|||||||
setOauthProxyUrl(connection.useSystemProxy ? '' : asTrimmedString(connection.proxyUrl));
|
setOauthProxyUrl(connection.useSystemProxy ? '' : asTrimmedString(connection.proxyUrl));
|
||||||
setDrawerOpen(true);
|
setDrawerOpen(true);
|
||||||
setShowColumnMenu(false);
|
setShowColumnMenu(false);
|
||||||
setSessionMessage('已打开 OAuth 代理设置,修改后可直接保存代理,或保存后重新授权。');
|
setSessionInfo('已打开 OAuth 代理设置,修改后可直接保存代理,或保存后重新授权。');
|
||||||
};
|
};
|
||||||
|
|
||||||
const openRouteUnitModal = () => {
|
const openRouteUnitModal = () => {
|
||||||
@@ -1025,7 +1079,7 @@ export default function OAuthManagement() {
|
|||||||
if (drawerIntent.mode !== 'proxy') return;
|
if (drawerIntent.mode !== 'proxy') return;
|
||||||
const customProxyUrl = asTrimmedString(oauthProxyUrl);
|
const customProxyUrl = asTrimmedString(oauthProxyUrl);
|
||||||
if (oauthCustomProxyEnabled && !customProxyUrl) {
|
if (oauthCustomProxyEnabled && !customProxyUrl) {
|
||||||
setSessionMessage('已开启代理,请先输入完整代理地址');
|
setSessionError('已开启代理,请先输入完整代理地址');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1044,9 +1098,9 @@ export default function OAuthManagement() {
|
|||||||
await loadConnections();
|
await loadConnections();
|
||||||
setDrawerOpen(false);
|
setDrawerOpen(false);
|
||||||
resetOauthProxySettings();
|
resetOauthProxySettings();
|
||||||
setSessionMessage('代理已保存');
|
setSessionSuccess('代理已保存');
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setSessionMessage(error?.message || '保存代理失败');
|
setSessionError(error?.message || '保存代理失败');
|
||||||
} finally {
|
} finally {
|
||||||
setActionLoadingKey('');
|
setActionLoadingKey('');
|
||||||
}
|
}
|
||||||
@@ -1060,11 +1114,11 @@ export default function OAuthManagement() {
|
|||||||
|| providers[0]
|
|| providers[0]
|
||||||
|| null;
|
|| null;
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
setSessionMessage('当前没有可用的 OAuth Provider。');
|
setSessionError('当前没有可用的 OAuth Provider。');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!provider.enabled) {
|
if (!provider.enabled) {
|
||||||
setSessionMessage(`${provider.label} 当前环境未启用`);
|
setSessionError(`${provider.label} 当前环境未启用`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1072,7 +1126,7 @@ export default function OAuthManagement() {
|
|||||||
const accountId = rebindAccount?.accountId;
|
const accountId = rebindAccount?.accountId;
|
||||||
const customProxyUrl = asTrimmedString(oauthProxyUrl);
|
const customProxyUrl = asTrimmedString(oauthProxyUrl);
|
||||||
if (oauthCustomProxyEnabled && !customProxyUrl) {
|
if (oauthCustomProxyEnabled && !customProxyUrl) {
|
||||||
setSessionMessage('已开启代理,请先输入完整代理地址');
|
setSessionError('已开启代理,请先输入完整代理地址');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1102,7 +1156,7 @@ export default function OAuthManagement() {
|
|||||||
...proxySettings,
|
...proxySettings,
|
||||||
});
|
});
|
||||||
|
|
||||||
setSessionMessage('等待授权完成');
|
setSessionInfo('等待授权完成');
|
||||||
setActiveSession({
|
setActiveSession({
|
||||||
provider: started.provider,
|
provider: started.provider,
|
||||||
state: started.state,
|
state: started.state,
|
||||||
@@ -1112,7 +1166,7 @@ export default function OAuthManagement() {
|
|||||||
resetOauthProxySettings();
|
resetOauthProxySettings();
|
||||||
openOAuthPopup(provider.provider, started.authorizationUrl);
|
openOAuthPopup(provider.provider, started.authorizationUrl);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setSessionMessage(error?.message || '无法启动 OAuth 授权');
|
setSessionError(error?.message || '无法启动 OAuth 授权');
|
||||||
} finally {
|
} finally {
|
||||||
setActionLoadingKey('');
|
setActionLoadingKey('');
|
||||||
}
|
}
|
||||||
@@ -1122,15 +1176,15 @@ export default function OAuthManagement() {
|
|||||||
if (!activeSession) return;
|
if (!activeSession) return;
|
||||||
const callbackUrl = manualCallbackUrl.trim();
|
const callbackUrl = manualCallbackUrl.trim();
|
||||||
if (!callbackUrl) {
|
if (!callbackUrl) {
|
||||||
setSessionMessage('请输入完整的回调 URL');
|
setSessionError('请输入完整的回调 URL');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setManualCallbackSubmitting(true);
|
setManualCallbackSubmitting(true);
|
||||||
try {
|
try {
|
||||||
await api.submitOAuthManualCallback(activeSession.state, callbackUrl);
|
await api.submitOAuthManualCallback(activeSession.state, callbackUrl);
|
||||||
setSessionMessage('回调已提交,等待授权完成');
|
setSessionInfo('回调已提交,等待授权完成');
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setSessionMessage(error?.message || '提交回调 URL 失败');
|
setSessionError(error?.message || '提交回调 URL 失败');
|
||||||
} finally {
|
} finally {
|
||||||
setManualCallbackSubmitting(false);
|
setManualCallbackSubmitting(false);
|
||||||
}
|
}
|
||||||
@@ -1145,11 +1199,11 @@ export default function OAuthManagement() {
|
|||||||
setActionLoadingKey(actionKey);
|
setActionLoadingKey(actionKey);
|
||||||
try {
|
try {
|
||||||
await api.deleteOAuthConnection(accountId);
|
await api.deleteOAuthConnection(accountId);
|
||||||
setSessionMessage('连接已删除');
|
setSessionSuccess('连接已删除');
|
||||||
await loadConnections();
|
await loadConnections();
|
||||||
setSelectedConnectionIds((current) => current.filter((id) => id !== accountId));
|
setSelectedConnectionIds((current) => current.filter((id) => id !== accountId));
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setSessionMessage(error?.message || '删除 OAuth 连接失败');
|
setSessionError(error?.message || '删除 OAuth 连接失败');
|
||||||
} finally {
|
} finally {
|
||||||
setActionLoadingKey('');
|
setActionLoadingKey('');
|
||||||
}
|
}
|
||||||
@@ -1167,9 +1221,11 @@ export default function OAuthManagement() {
|
|||||||
const failed = results.filter((item) => item.status === 'rejected').length;
|
const failed = results.filter((item) => item.status === 'rejected').length;
|
||||||
await loadConnections();
|
await loadConnections();
|
||||||
setSelectedConnectionIds([]);
|
setSelectedConnectionIds([]);
|
||||||
setSessionMessage(failed > 0
|
if (failed > 0) {
|
||||||
? `批量删除完成,${failed} 个连接删除失败`
|
setSessionInfo(`批量删除完成,${failed} 个连接删除失败`);
|
||||||
: `已删除 ${results.length} 个 OAuth 连接`);
|
} else {
|
||||||
|
setSessionSuccess(`已删除 ${results.length} 个 OAuth 连接`);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setActionLoadingKey('');
|
setActionLoadingKey('');
|
||||||
}
|
}
|
||||||
@@ -1180,10 +1236,10 @@ export default function OAuthManagement() {
|
|||||||
setActionLoadingKey(actionKey);
|
setActionLoadingKey(actionKey);
|
||||||
try {
|
try {
|
||||||
await api.refreshOAuthConnectionQuota(accountId);
|
await api.refreshOAuthConnectionQuota(accountId);
|
||||||
setSessionMessage('额度信息已刷新');
|
setSessionSuccess('额度信息已刷新');
|
||||||
await loadConnections();
|
await loadConnections();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setSessionMessage(error?.message || '刷新额度失败');
|
setSessionError(error?.message || '刷新额度失败');
|
||||||
} finally {
|
} finally {
|
||||||
setActionLoadingKey('');
|
setActionLoadingKey('');
|
||||||
}
|
}
|
||||||
@@ -1195,11 +1251,13 @@ export default function OAuthManagement() {
|
|||||||
try {
|
try {
|
||||||
const result = await api.refreshOAuthConnectionQuotaBatch(selectedConnectionIds);
|
const result = await api.refreshOAuthConnectionQuotaBatch(selectedConnectionIds);
|
||||||
await loadConnections();
|
await loadConnections();
|
||||||
setSessionMessage(result.failed > 0
|
if (result.failed > 0) {
|
||||||
? `批量刷新完成,成功 ${result.refreshed} 个,失败 ${result.failed} 个`
|
setSessionInfo(`批量刷新完成,成功 ${result.refreshed} 个,失败 ${result.failed} 个`);
|
||||||
: `已批量刷新 ${result.refreshed} 个 OAuth 连接`);
|
} else {
|
||||||
|
setSessionSuccess(`已批量刷新 ${result.refreshed} 个 OAuth 连接`);
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setSessionMessage(error?.message || '批量刷新额度失败');
|
setSessionError(error?.message || '批量刷新额度失败');
|
||||||
} finally {
|
} finally {
|
||||||
setActionLoadingKey('');
|
setActionLoadingKey('');
|
||||||
}
|
}
|
||||||
@@ -1251,11 +1309,11 @@ export default function OAuthManagement() {
|
|||||||
applyLoadedModelsModal(connection, result);
|
applyLoadedModelsModal(connection, result);
|
||||||
if (options.refreshUpstream) {
|
if (options.refreshUpstream) {
|
||||||
await loadConnections();
|
await loadConnections();
|
||||||
setSessionMessage('模型列表已刷新');
|
setSessionSuccess('模型列表已刷新');
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (modelsModalRequestSeqRef.current !== requestId) return;
|
if (modelsModalRequestSeqRef.current !== requestId) return;
|
||||||
setSessionMessage(error?.message || '加载模型列表失败');
|
setSessionError(error?.message || '加载模型列表失败');
|
||||||
setModelsModal((current) => (
|
setModelsModal((current) => (
|
||||||
options.closeOnError
|
options.closeOnError
|
||||||
? {
|
? {
|
||||||
@@ -1342,15 +1400,15 @@ export default function OAuthManagement() {
|
|||||||
|
|
||||||
const handleImport = async () => {
|
const handleImport = async () => {
|
||||||
if (importSources.length <= 0) {
|
if (importSources.length <= 0) {
|
||||||
setSessionMessage('请先选择 JSON 文件或粘贴 OAuth 连接 JSON 内容');
|
setSessionError('请先选择 JSON 文件或粘贴 OAuth 连接 JSON 内容');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!importPreviewSummary?.canImport) {
|
if (!importPreviewSummary?.canImport) {
|
||||||
setSessionMessage('请先修正无效的 OAuth JSON');
|
setSessionError('请先修正无效的 OAuth JSON');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (importCustomProxyEnabled && !asTrimmedString(importProxyUrl)) {
|
if (importCustomProxyEnabled && !asTrimmedString(importProxyUrl)) {
|
||||||
setSessionMessage('已开启代理,请先输入完整代理地址');
|
setSessionError('已开启代理,请先输入完整代理地址');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1384,12 +1442,16 @@ export default function OAuthManagement() {
|
|||||||
} else {
|
} else {
|
||||||
toast.success(importMessage);
|
toast.success(importMessage);
|
||||||
}
|
}
|
||||||
setSessionMessage(importMessage);
|
if (result.failed > 0) {
|
||||||
|
setSessionInfo(importMessage);
|
||||||
|
} else {
|
||||||
|
setSessionSuccess(importMessage);
|
||||||
|
}
|
||||||
closeImportModal();
|
closeImportModal();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const message = error?.message || '导入 OAuth JSON 失败';
|
const message = error?.message || '导入 OAuth JSON 失败';
|
||||||
toast.error(message);
|
toast.error(message);
|
||||||
setSessionMessage(message);
|
setSessionError(message);
|
||||||
} finally {
|
} finally {
|
||||||
setImporting(false);
|
setImporting(false);
|
||||||
}
|
}
|
||||||
@@ -1399,23 +1461,49 @@ export default function OAuthManagement() {
|
|||||||
const name = asTrimmedString(routeUnitModal.name);
|
const name = asTrimmedString(routeUnitModal.name);
|
||||||
if (!canMergeSelectedIntoRouteUnit || selectedConnections.length < 2) return;
|
if (!canMergeSelectedIntoRouteUnit || selectedConnections.length < 2) return;
|
||||||
if (!name) {
|
if (!name) {
|
||||||
setSessionMessage('请先输入路由池名称');
|
setSessionError('请先输入路由池名称');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setActionLoadingKey('route-unit:create');
|
setActionLoadingKey('route-unit:create');
|
||||||
try {
|
try {
|
||||||
await api.createOAuthRouteUnit({
|
const result = await api.createOAuthRouteUnit({
|
||||||
accountIds: selectedConnections.map((connection) => connection.accountId),
|
accountIds: selectedConnections.map((connection) => connection.accountId),
|
||||||
name,
|
name,
|
||||||
strategy: routeUnitModal.strategy,
|
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([]);
|
setSelectedConnectionIds([]);
|
||||||
closeRouteUnitModal();
|
closeRouteUnitModal();
|
||||||
setSessionMessage('已合并为路由池');
|
try {
|
||||||
|
await loadConnections();
|
||||||
|
} catch {
|
||||||
|
toast.error('OAuth 连接列表刷新失败');
|
||||||
|
setSessionError('已创建路由池,但连接列表刷新失败', {
|
||||||
|
routeUnit,
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setSessionMessage(error?.message || '创建路由池失败');
|
const message = error?.message || '创建路由池失败';
|
||||||
|
toast.error(message);
|
||||||
|
setSessionError(message);
|
||||||
} finally {
|
} finally {
|
||||||
setActionLoadingKey('');
|
setActionLoadingKey('');
|
||||||
}
|
}
|
||||||
@@ -1428,12 +1516,30 @@ export default function OAuthManagement() {
|
|||||||
|
|
||||||
setActionLoadingKey('route-unit:delete');
|
setActionLoadingKey('route-unit:delete');
|
||||||
try {
|
try {
|
||||||
|
const routeUnitFeedback = {
|
||||||
|
action: 'deleted' as const,
|
||||||
|
name: selectedRouteUnitParticipation.name,
|
||||||
|
memberCount: selectedRouteUnitParticipation.memberCount,
|
||||||
|
strategy: selectedRouteUnitParticipation.strategy,
|
||||||
|
};
|
||||||
await api.deleteOAuthRouteUnit(routeUnitId);
|
await api.deleteOAuthRouteUnit(routeUnitId);
|
||||||
await loadConnections();
|
toast.success(`已拆回单体:${routeUnitFeedback.name}`);
|
||||||
|
setSessionSuccess('已拆回单体', {
|
||||||
|
routeUnit: routeUnitFeedback,
|
||||||
|
});
|
||||||
setSelectedConnectionIds([]);
|
setSelectedConnectionIds([]);
|
||||||
setSessionMessage('已拆回单体');
|
try {
|
||||||
|
await loadConnections();
|
||||||
|
} catch {
|
||||||
|
toast.error('OAuth 连接列表刷新失败');
|
||||||
|
setSessionError('已拆回单体,但连接列表刷新失败', {
|
||||||
|
routeUnit: routeUnitFeedback,
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setSessionMessage(error?.message || '拆回单体失败');
|
const message = error?.message || '拆回单体失败';
|
||||||
|
toast.error(message);
|
||||||
|
setSessionError(message);
|
||||||
} finally {
|
} finally {
|
||||||
setActionLoadingKey('');
|
setActionLoadingKey('');
|
||||||
}
|
}
|
||||||
@@ -1930,9 +2036,26 @@ export default function OAuthManagement() {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{sessionMessage ? (
|
{sessionFeedback ? (
|
||||||
<div className="card oauth-page-message">
|
<div className={`card oauth-page-message oauth-page-message-${sessionFeedback.tone}`.trim()}>
|
||||||
<div className="oauth-page-message-text">{sessionMessage}</div>
|
<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>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user