Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
refactor(saas): 重构认证中间件与限流策略
- 登录限流调整为5次/分钟/IP
- 注册限流调整为3次/小时/IP
- GET请求不计入限流
fix(saas): 修复调度器时间戳处理
- 使用NOW()替代文本时间戳
- 兼容TEXT和TIMESTAMPTZ列类型
feat(saas): 实现环境变量插值
- 支持${ENV_VAR}语法解析
- 数据库密码支持环境变量注入
chore: 新增前端管理界面
- 基于React+Ant Design Pro
- 包含路由守卫/错误边界
- 对接58个API端点
docs: 更新安全加固文档
- 新增密钥管理规范
- 记录P0安全项审计结果
- 补充TLS终止说明
test: 完善配置解析单元测试
- 新增环境变量插值测试用例
189 lines
6.5 KiB
TypeScript
189 lines
6.5 KiB
TypeScript
// ============================================================
|
|
// 服务商管理
|
|
// ============================================================
|
|
|
|
import { useState } from 'react'
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
import { Button, message, Tag, Modal, Form, Input, InputNumber, Switch, Space, Popconfirm, Typography } from 'antd'
|
|
import { PlusOutlined } from '@ant-design/icons'
|
|
import type { ProColumns } from '@ant-design/pro-components'
|
|
import { ProTable } from '@ant-design/pro-components'
|
|
import { providerService } from '@/services/providers'
|
|
import type { Provider, ProviderKey } from '@/types'
|
|
|
|
const { Text } = Typography
|
|
|
|
export default function Providers() {
|
|
const queryClient = useQueryClient()
|
|
const [form] = Form.useForm()
|
|
const [modalOpen, setModalOpen] = useState(false)
|
|
const [editingId, setEditingId] = useState<string | null>(null)
|
|
const [keyModalProviderId, setKeyModalProviderId] = useState<string | null>(null)
|
|
|
|
const { data, isLoading } = useQuery({
|
|
queryKey: ['providers'],
|
|
queryFn: ({ signal }) => providerService.list(signal),
|
|
})
|
|
|
|
const { data: keysData, isLoading: keysLoading } = useQuery({
|
|
queryKey: ['provider-keys', keyModalProviderId],
|
|
queryFn: ({ signal }) => providerService.listKeys(keyModalProviderId!, signal),
|
|
enabled: !!keyModalProviderId,
|
|
})
|
|
|
|
const createMutation = useMutation({
|
|
mutationFn: (data: Partial<Omit<Provider, 'id' | 'created_at' | 'updated_at'>>) =>
|
|
providerService.create(data),
|
|
onSuccess: () => {
|
|
message.success('创建成功')
|
|
queryClient.invalidateQueries({ queryKey: ['providers'] })
|
|
setModalOpen(false)
|
|
form.resetFields()
|
|
},
|
|
onError: (err: Error) => message.error(err.message || '创建失败'),
|
|
})
|
|
|
|
const updateMutation = useMutation({
|
|
mutationFn: ({ id, data }: { id: string; data: Partial<Omit<Provider, 'id' | 'created_at' | 'updated_at'>> }) =>
|
|
providerService.update(id, data),
|
|
onSuccess: () => {
|
|
message.success('更新成功')
|
|
queryClient.invalidateQueries({ queryKey: ['providers'] })
|
|
setModalOpen(false)
|
|
},
|
|
onError: (err: Error) => message.error(err.message || '更新失败'),
|
|
})
|
|
|
|
const deleteMutation = useMutation({
|
|
mutationFn: (id: string) => providerService.delete(id),
|
|
onSuccess: () => {
|
|
message.success('删除成功')
|
|
queryClient.invalidateQueries({ queryKey: ['providers'] })
|
|
},
|
|
onError: (err: Error) => message.error(err.message || '删除失败'),
|
|
})
|
|
|
|
const columns: ProColumns<Provider>[] = [
|
|
{ title: '名称', dataIndex: 'display_name', width: 140 },
|
|
{ title: '标识', dataIndex: 'name', width: 120, render: (_, r) => <Text code>{r.name}</Text> },
|
|
{ title: '协议', dataIndex: 'api_protocol', width: 100 },
|
|
{ title: 'RPM 限制', dataIndex: 'rate_limit_rpm', width: 100, render: (_, r) => r.rate_limit_rpm ?? '-' },
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'enabled',
|
|
width: 80,
|
|
render: (_, r) => r.enabled ? <Tag color="green">启用</Tag> : <Tag>禁用</Tag>,
|
|
},
|
|
{
|
|
title: '操作',
|
|
width: 260,
|
|
render: (_, record) => (
|
|
<Space>
|
|
<Button size="small" onClick={() => { setEditingId(record.id); form.setFieldsValue(record); setModalOpen(true) }}>
|
|
编辑
|
|
</Button>
|
|
<Button size="small" onClick={() => setKeyModalProviderId(record.id)}>
|
|
Key Pool
|
|
</Button>
|
|
<Popconfirm title="确定删除此服务商?" onConfirm={() => deleteMutation.mutate(record.id)}>
|
|
<Button size="small" danger>删除</Button>
|
|
</Popconfirm>
|
|
</Space>
|
|
),
|
|
},
|
|
]
|
|
|
|
const keyColumns: ProColumns<ProviderKey>[] = [
|
|
{ title: '标签', dataIndex: 'key_label', width: 120 },
|
|
{ title: '优先级', dataIndex: 'priority', width: 80 },
|
|
{ title: '请求数', dataIndex: 'total_requests', width: 80 },
|
|
{ title: 'Token 数', dataIndex: 'total_tokens', width: 100 },
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'is_active',
|
|
width: 80,
|
|
render: (_, r) => r.is_active ? <Tag color="green">活跃</Tag> : <Tag>冷却</Tag>,
|
|
},
|
|
]
|
|
|
|
const handleSave = async () => {
|
|
const values = await form.validateFields()
|
|
if (editingId) {
|
|
updateMutation.mutate({ id: editingId, data: values })
|
|
} else {
|
|
createMutation.mutate(values)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<ProTable<Provider>
|
|
columns={columns}
|
|
dataSource={data?.items ?? []}
|
|
loading={isLoading}
|
|
rowKey="id"
|
|
search={false}
|
|
toolBarRender={() => [
|
|
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => { setEditingId(null); form.resetFields(); setModalOpen(true) }}>
|
|
新建服务商
|
|
</Button>,
|
|
]}
|
|
pagination={{
|
|
total: data?.total ?? 0,
|
|
pageSize: data?.page_size ?? 20,
|
|
current: data?.page ?? 1,
|
|
showSizeChanger: false,
|
|
}}
|
|
/>
|
|
|
|
<Modal
|
|
title={editingId ? '编辑服务商' : '新建服务商'}
|
|
open={modalOpen}
|
|
onOk={handleSave}
|
|
onCancel={() => { setModalOpen(false); setEditingId(null); form.resetFields() }}
|
|
confirmLoading={createMutation.isPending || updateMutation.isPending}
|
|
>
|
|
<Form form={form} layout="vertical">
|
|
<Form.Item name="name" label="标识" rules={[{ required: true }]}>
|
|
<Input disabled={!!editingId} />
|
|
</Form.Item>
|
|
<Form.Item name="display_name" label="显示名称" rules={[{ required: true }]}>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item name="base_url" label="Base URL" rules={[{ required: true }]}>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item name="api_protocol" label="API 协议">
|
|
<Input placeholder="openai" />
|
|
</Form.Item>
|
|
<Form.Item name="enabled" label="启用" valuePropName="checked">
|
|
<Switch />
|
|
</Form.Item>
|
|
<Form.Item name="rate_limit_rpm" label="RPM 限制">
|
|
<InputNumber min={0} style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
|
|
<Modal
|
|
title="Key Pool"
|
|
open={!!keyModalProviderId}
|
|
onCancel={() => setKeyModalProviderId(null)}
|
|
footer={null}
|
|
width={700}
|
|
>
|
|
<ProTable<ProviderKey>
|
|
columns={keyColumns}
|
|
dataSource={keysData ?? []}
|
|
loading={keysLoading}
|
|
rowKey="id"
|
|
search={false}
|
|
toolBarRender={false}
|
|
pagination={false}
|
|
size="small"
|
|
/>
|
|
</Modal>
|
|
</div>
|
|
)
|
|
}
|