admin-projects/src/pages/AiGateway.tsx
wangdl 8663ceba66
Some checks failed
Deploy Admin Frontend / build-and-deploy (push) Failing after 10s
feat: HTTP 层重构 + API 抽离 + 状态管理优化 + 路由修复
- HTTP 层:安装 axios,创建 lib/api-client.ts 统一拦截器
- API 层:创建 33 个 api/*.ts 文件,所有页面迁移完成
- DataPages.tsx 拆分为 10 个独立文件
- message 导入统一为 App.useApp()
- 新增 StaticAntdProvider 全局消息/通知
- 全局 HTTP 错误/成功提示(notification.error/success)
- learning 路由 /learning → /learning/dashboard 修复选中 bug
- 学习会话页面优化(移除 ID 列、加批量删除、后端排序)
- 文档导入页面重构(知识库筛选、批量重新解析)
- 删除旧 service 文件 10 个(admin-api、billing-api 等)
- antd App 包裹根组件

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-01 22:44:51 +08:00

176 lines
9.7 KiB
TypeScript

import { useState } from 'react'
import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query'
import { Card, Row, Col, Statistic, Table, Tag, Button, Typography, Tabs, Modal, Form, Input, InputNumber, Select, Switch, App } from 'antd'
import { ReloadOutlined, CloudOutlined, PlusOutlined, DeleteOutlined } from '@ant-design/icons'
import { aiGatewayAPI } from '@/api'
const { Title, Text } = Typography
export default function AiGatewayPage() {
const qc = useQueryClient()
const { message } = App.useApp()
const [activeTab, setActiveTab] = useState('overview')
const [routeModal, setRouteModal] = useState<{ open: boolean; editing?: any }>({ open: false })
const [form] = Form.useForm()
const { data: status } = useQuery({
queryKey: ['ai-gateway', 'status'],
queryFn: (): Promise<any> => aiGatewayAPI.status(),
staleTime: 30_000,
})
const { data: routes, refetch: refetchRoutes } = useQuery({
queryKey: ['ai-gateway', 'routes'],
queryFn: (): Promise<any> => aiGatewayAPI.routes(),
enabled: activeTab === 'routes',
})
const { data: providers, refetch: refetchProviders } = useQuery({
queryKey: ['ai-gateway', 'providers'],
queryFn: (): Promise<any> => aiGatewayAPI.providers(),
enabled: activeTab === 'providers',
})
const { data: fallbackEvents } = useQuery({
queryKey: ['ai-gateway', 'fallback-events'],
queryFn: (): Promise<any> => aiGatewayAPI.fallbackEvents(),
enabled: activeTab === 'fallback',
refetchInterval: 30_000,
})
const createRoute = useMutation({
mutationFn: (values: any) => aiGatewayAPI.createRoute(values),
onSuccess: () => { message.success('路由已创建'); refetchRoutes(); setRouteModal({ open: false }); form.resetFields() },
})
const deleteRoute = useMutation({
mutationFn: (id: string) => aiGatewayAPI.deleteRoute(id),
onSuccess: () => { message.success('路由已删除'); refetchRoutes() },
})
const toggleProvider = useMutation({
mutationFn: ({ name, enabled }: { name: string; enabled: boolean }) => aiGatewayAPI.toggleProvider(name, enabled),
onSuccess: () => { message.success('Provider 已更新'); refetchProviders() },
})
const routeColumns = [
{ title: '级别', dataIndex: 'tier', width: 80, render: (v: string) => <Tag color={v==='strong'?'red':v==='primary'?'blue':'default'}>{v}</Tag> },
{ title: '任务类型', dataIndex: 'taskType', width: 100 },
{ title: '首选 Provider', dataIndex: 'preferredProvider', width: 120, render: (v: string) => <Tag>{v}</Tag> },
{ title: '首选模型', dataIndex: 'preferredModel', width: 180, ellipsis: true },
{ title: '备用 Provider', dataIndex: 'fallbackProvider', width: 120, render: (v: string) => <Tag>{v}</Tag> },
{ title: '备用模型', dataIndex: 'fallbackModel', width: 180, ellipsis: true },
{ title: '重试', dataIndex: 'maxRetries', width: 60, align: 'center' as const },
{ title: '状态', dataIndex: 'isActive', width: 70, render: (v: boolean) => v ? <Tag color="green"></Tag> : <Tag color="red"></Tag> },
{ title: '操作', width: 80, render: (_: any, r: any) => (
<Button type="link" danger size="small" icon={<DeleteOutlined />} onClick={() => deleteRoute.mutate(r.id)} />
)},
]
const providerColumns = [
{ title: 'Provider', dataIndex: 'name', width: 150, render: (v: string) => <Tag color={v==='deepseek'?'blue':v==='minimax'?'purple':'green'}>{v}</Tag> },
{ title: '状态', dataIndex: 'enabled', width: 80, render: (v: boolean, r: any) => (
<Switch checked={v} onChange={(checked) => toggleProvider.mutate({ name: r.name, enabled: checked })} />
)},
{ title: 'Base URL', dataIndex: 'baseUrl', ellipsis: true },
{ title: '更新时间', dataIndex: 'updatedAt', width: 180 },
]
const fallbackColumns = [
{ title: '时间', dataIndex: 'createdAt', width: 170 },
{ title: '级别', dataIndex: 'tier', width: 80 },
{ title: '来源', dataIndex: 'fromProvider', width: 100, render: (v: string, r: any) => <Text>{v}/{r.fromModel?.slice(0,30)}</Text> },
{ title: '切换到', dataIndex: 'toProvider', width: 100, render: (v: string, r: any) => <Text type="warning">{v}/{r.toModel?.slice(0,30)}</Text> },
{ title: '错误', dataIndex: 'errorMessage', ellipsis: true },
]
const tabItems = [
{
key: 'overview', label: '概览',
children: (
<>
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
<Col span={6}><Card size="small"><Statistic title="Provider" value={status?.providers?.length || 0} suffix="个" /></Card></Col>
<Col span={6}><Card size="small"><Statistic title="活跃路由" value={status?.activeRoutes || 0} suffix="条" /></Card></Col>
<Col span={6}><Card size="small"><Statistic title="降级事件" value={status?.fallbackEvents || 0} suffix="次" /></Card></Col>
<Col span={6}><Card size="small"><Statistic title="路由级别" value={3} suffix="层" /></Card></Col>
</Row>
<Row gutter={[16, 16]}>
<Col span={12}>
<Card size="small" title="Provider 列表">
<Table dataSource={(status?.providers || []).map((p: any) => (typeof p === 'string' ? { name: p, enabled: true } : p))} columns={[
{ title: '名称', dataIndex: 'name', render: (v: string) => <Tag color={v==='deepseek'?'blue':v==='minimax'?'purple':'green'}>{v}</Tag> },
{ title: '状态', dataIndex: 'enabled', render: (v: boolean) => v ? <Tag color="green"></Tag> : <Tag color="red"></Tag> },
]} rowKey="name" pagination={false} size="small" />
</Card>
</Col>
<Col span={12}>
<Card size="small" title="模型路由">
<Table dataSource={(status?.routes || []).map((r: any) => (
{ tier: r.tier, taskType: r.taskType || '*', preferredProvider: r.preferred?.split('/')[0], preferredModel: r.preferred?.split('/').slice(1).join('/'), fallbackProvider: r.fallback?.split('/')[0], fallbackModel: r.fallback?.split('/').slice(1).join('/'), maxRetries: r.maxRetries }
))} columns={[
{ title: '级别', dataIndex: 'tier', width: 70, render: (v: string) => <Tag>{v}</Tag> },
{ title: '首选', dataIndex: 'preferred', width: 200, ellipsis: true, render: (_: any, r: any) => <Text>{r.preferredProvider}/{r.preferredModel?.slice(0,25)}</Text> },
{ title: '备用', dataIndex: 'fallback', width: 200, ellipsis: true, render: (_: any, r: any) => <Text>{r.fallbackProvider}/{r.fallbackModel?.slice(0,25)}</Text> },
]} rowKey="tier" pagination={false} size="small" />
</Card>
</Col>
</Row>
</>
),
},
{
key: 'routes', label: '路由规则',
children: (
<>
<Button type="primary" icon={<PlusOutlined />} style={{ marginBottom: 16 }} onClick={() => { form.resetFields(); setRouteModal({ open: true }) }}></Button>
<Table dataSource={routes || []} columns={routeColumns} rowKey="id" pagination={false} size="small" />
<Modal title={routeModal.editing ? '编辑路由' : '新增路由'} open={routeModal.open} onCancel={() => setRouteModal({ open: false })} onOk={() => form.submit()} confirmLoading={createRoute.isPending}>
<Form form={form} layout="vertical" onFinish={(v) => createRoute.mutate(v)}>
<Form.Item name="tier" label="级别" rules={[{ required: true }]}>
<Select options={[{ value: 'cheap', label: 'Cheap' }, { value: 'primary', label: 'Primary' }, { value: 'strong', label: 'Strong' }]} />
</Form.Item>
<Form.Item name="taskType" label="任务类型" initialValue="*">
<Input placeholder="* 表示所有" />
</Form.Item>
<Form.Item name="preferredProvider" label="首选 Provider" rules={[{ required: true }]}>
<Select options={[{ value: 'deepseek', label: 'DeepSeek' }, { value: 'minimax', label: 'MiniMax' }, { value: 'siliconflow', label: 'SiliconFlow' }]} />
</Form.Item>
<Form.Item name="preferredModel" label="首选模型" rules={[{ required: true }]}>
<Input placeholder="deepseek-v4-pro" />
</Form.Item>
<Form.Item name="fallbackProvider" label="备用 Provider" rules={[{ required: true }]}>
<Select options={[{ value: 'deepseek', label: 'DeepSeek' }, { value: 'minimax', label: 'MiniMax' }, { value: 'siliconflow', label: 'SiliconFlow' }]} />
</Form.Item>
<Form.Item name="fallbackModel" label="备用模型" rules={[{ required: true }]}>
<Input placeholder="deepseek-v4-flash" />
</Form.Item>
<Form.Item name="maxRetries" label="最大重试次数" initialValue={2}>
<InputNumber min={0} max={10} />
</Form.Item>
</Form>
</Modal>
</>
),
},
{
key: 'providers', label: 'Provider 管理',
children: <Table dataSource={providers || []} columns={providerColumns} rowKey="name" pagination={false} size="small" />,
},
{
key: 'fallback', label: '降级日志',
children: <Table dataSource={fallbackEvents || []} columns={fallbackColumns} rowKey="id" pagination={{ pageSize: 20 }} size="small" />,
},
]
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<Title level={5} style={{ margin: 0 }}><CloudOutlined /> AI Gateway</Title>
<Button icon={<ReloadOutlined />} onClick={() => qc.invalidateQueries({ queryKey: ['ai-gateway'] })}></Button>
</div>
<Tabs activeKey={activeTab} onChange={setActiveTab} items={tabItems} />
</div>
)
}