- Enable ProTable search on Accounts (username/email), Models (model_id/alias), Providers (display_name/name) with hideInSearch for non-searchable columns - Add scenarios (Select tags) and quick_commands (Form.List) to AgentTemplates create form, plus service type updates - Remove unused quota_reset_interval from ProviderKey model, key_pool SQL, handlers, and frontend types; add migration + bump schema to v11 - Add Vitest config, test setup, request interceptor tests (7 cases), authStore tests (8 cases) — all 15 passing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
192 lines
6.9 KiB
TypeScript
192 lines
6.9 KiB
TypeScript
// ============================================================
|
|
// 模型管理
|
|
// ============================================================
|
|
|
|
import { useState } from 'react'
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
import { Button, message, Tag, Modal, Form, Input, InputNumber, Switch, Select, Space, Popconfirm } from 'antd'
|
|
import { PlusOutlined } from '@ant-design/icons'
|
|
import type { ProColumns } from '@ant-design/pro-components'
|
|
import { ProTable } from '@ant-design/pro-components'
|
|
import { modelService } from '@/services/models'
|
|
import { providerService } from '@/services/providers'
|
|
import type { Model } from '@/types'
|
|
|
|
export default function Models() {
|
|
const queryClient = useQueryClient()
|
|
const [form] = Form.useForm()
|
|
const [modalOpen, setModalOpen] = useState(false)
|
|
const [editingId, setEditingId] = useState<string | null>(null)
|
|
|
|
const { data, isLoading } = useQuery({
|
|
queryKey: ['models'],
|
|
queryFn: ({ signal }) => modelService.list(signal),
|
|
})
|
|
|
|
const { data: providersData } = useQuery({
|
|
queryKey: ['providers-for-select'],
|
|
queryFn: ({ signal }) => providerService.list(signal),
|
|
})
|
|
|
|
const createMutation = useMutation({
|
|
mutationFn: (data: Partial<Omit<Model, 'id'>>) => modelService.create(data),
|
|
onSuccess: () => {
|
|
message.success('创建成功')
|
|
queryClient.invalidateQueries({ queryKey: ['models'] })
|
|
setModalOpen(false)
|
|
form.resetFields()
|
|
},
|
|
onError: (err: Error) => message.error(err.message || '创建失败'),
|
|
})
|
|
|
|
const updateMutation = useMutation({
|
|
mutationFn: ({ id, data }: { id: string; data: Partial<Omit<Model, 'id'>> }) =>
|
|
modelService.update(id, data),
|
|
onSuccess: () => {
|
|
message.success('更新成功')
|
|
queryClient.invalidateQueries({ queryKey: ['models'] })
|
|
setModalOpen(false)
|
|
},
|
|
onError: (err: Error) => message.error(err.message || '更新失败'),
|
|
})
|
|
|
|
const deleteMutation = useMutation({
|
|
mutationFn: (id: string) => modelService.delete(id),
|
|
onSuccess: () => {
|
|
message.success('删除成功')
|
|
queryClient.invalidateQueries({ queryKey: ['models'] })
|
|
},
|
|
onError: (err: Error) => message.error(err.message || '删除失败'),
|
|
})
|
|
|
|
const columns: ProColumns<Model>[] = [
|
|
{ title: '模型 ID', dataIndex: 'model_id', width: 180, render: (_, r) => <code>{r.model_id}</code> },
|
|
{ title: '别名', dataIndex: 'alias', width: 140 },
|
|
{
|
|
title: '服务商',
|
|
dataIndex: 'provider_id',
|
|
width: 140,
|
|
hideInSearch: true,
|
|
render: (_, r) => {
|
|
const provider = providersData?.items?.find((p) => p.id === r.provider_id)
|
|
return provider?.display_name || r.provider_id.substring(0, 8)
|
|
},
|
|
},
|
|
{ title: '上下文窗口', dataIndex: 'context_window', width: 110, hideInSearch: true, render: (_, r) => r.context_window?.toLocaleString() },
|
|
{ title: '最大输出', dataIndex: 'max_output_tokens', width: 100, hideInSearch: true, render: (_, r) => r.max_output_tokens?.toLocaleString() },
|
|
{
|
|
title: '流式',
|
|
dataIndex: 'supports_streaming',
|
|
width: 70,
|
|
hideInSearch: true,
|
|
render: (_, r) => r.supports_streaming ? <Tag color="green">是</Tag> : <Tag>否</Tag>,
|
|
},
|
|
{
|
|
title: '视觉',
|
|
dataIndex: 'supports_vision',
|
|
width: 70,
|
|
hideInSearch: true,
|
|
render: (_, r) => r.supports_vision ? <Tag color="blue">是</Tag> : <Tag>否</Tag>,
|
|
},
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'enabled',
|
|
width: 70,
|
|
hideInSearch: true,
|
|
render: (_, r) => r.enabled ? <Tag color="green">启用</Tag> : <Tag>禁用</Tag>,
|
|
},
|
|
{
|
|
title: '操作',
|
|
width: 160,
|
|
hideInSearch: true,
|
|
render: (_, record) => (
|
|
<Space>
|
|
<Button size="small" onClick={() => { setEditingId(record.id); form.setFieldsValue(record); setModalOpen(true) }}>
|
|
编辑
|
|
</Button>
|
|
<Popconfirm title="确定删除此模型?" onConfirm={() => deleteMutation.mutate(record.id)}>
|
|
<Button size="small" danger>删除</Button>
|
|
</Popconfirm>
|
|
</Space>
|
|
),
|
|
},
|
|
]
|
|
|
|
const handleSave = async () => {
|
|
const values = await form.validateFields()
|
|
if (editingId) {
|
|
updateMutation.mutate({ id: editingId, data: values })
|
|
} else {
|
|
createMutation.mutate(values)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<ProTable<Model>
|
|
columns={columns}
|
|
dataSource={data?.items ?? []}
|
|
loading={isLoading}
|
|
rowKey="id"
|
|
search={{}}
|
|
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}
|
|
width={600}
|
|
>
|
|
<Form form={form} layout="vertical">
|
|
<Form.Item name="provider_id" label="服务商" rules={[{ required: true }]}>
|
|
<Select
|
|
options={(providersData?.items ?? []).map((p) => ({ value: p.id, label: p.display_name }))}
|
|
placeholder="选择服务商"
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item name="model_id" label="模型 ID" rules={[{ required: true }]}>
|
|
<Input placeholder="如 gpt-4o" />
|
|
</Form.Item>
|
|
<Form.Item name="alias" label="别名">
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item name="context_window" label="上下文窗口">
|
|
<InputNumber min={0} style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
<Form.Item name="max_output_tokens" label="最大输出 Token">
|
|
<InputNumber min={0} style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
<Form.Item name="enabled" label="启用" valuePropName="checked">
|
|
<Switch />
|
|
</Form.Item>
|
|
<Form.Item name="supports_streaming" label="支持流式" valuePropName="checked">
|
|
<Switch />
|
|
</Form.Item>
|
|
<Form.Item name="supports_vision" label="支持视觉" valuePropName="checked">
|
|
<Switch />
|
|
</Form.Item>
|
|
<Form.Item name="pricing_input" label="输入价格 (每百万 Token)">
|
|
<InputNumber min={0} step={0.01} style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
<Form.Item name="pricing_output" label="输出价格 (每百万 Token)">
|
|
<InputNumber min={0} step={0.01} style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
</div>
|
|
)
|
|
}
|