Some checks failed
Deploy Admin Frontend / build-and-deploy (push) Failing after 10s
- 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>
176 lines
9.7 KiB
TypeScript
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>
|
|
)
|
|
}
|