256 lines
7.8 KiB
TypeScript
256 lines
7.8 KiB
TypeScript
import { useEffect, useState, useCallback } from "react";
|
|
import {
|
|
Table, Button, Modal, Form, Input, Select, Switch,
|
|
Space, message, Popconfirm, Tag,
|
|
} from "antd";
|
|
import { PlusOutlined, EditOutlined, DeleteOutlined, ExperimentOutlined } from "@ant-design/icons";
|
|
import type { AIModel } from "../types";
|
|
import * as modelApi from "../api/models";
|
|
|
|
const PROVIDERS = [
|
|
{ label: "OpenAI", value: "openai" },
|
|
{ label: "Azure", value: "azure" },
|
|
{ label: "自定义", value: "custom" },
|
|
];
|
|
|
|
export default function ModelList() {
|
|
const [models, setModels] = useState<AIModel[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [modalOpen, setModalOpen] = useState(false);
|
|
const [editingModel, setEditingModel] = useState<AIModel | null>(null);
|
|
const [form] = Form.useForm();
|
|
|
|
const fetchModels = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const data = await modelApi.listModels();
|
|
setModels(data);
|
|
} catch {
|
|
message.error("获取模型列表失败");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
fetchModels();
|
|
}, [fetchModels]);
|
|
|
|
const handleCreate = () => {
|
|
setEditingModel(null);
|
|
setModalOpen(true);
|
|
};
|
|
|
|
const handleEdit = (model: AIModel) => {
|
|
setEditingModel(model);
|
|
setModalOpen(true);
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (!modalOpen) return;
|
|
// 延迟确保 Form DOM 已渲染
|
|
const timer = setTimeout(() => {
|
|
if (editingModel) {
|
|
const extra = editingModel.extra_params || {};
|
|
form.setFieldsValue({
|
|
name: editingModel.name,
|
|
provider: editingModel.provider,
|
|
model_name: extra.model || "",
|
|
endpoint: editingModel.endpoint,
|
|
api_key: "",
|
|
extra_params: JSON.stringify(extra, null, 2),
|
|
is_enabled: editingModel.is_enabled,
|
|
remark: editingModel.remark,
|
|
});
|
|
} else {
|
|
form.resetFields();
|
|
form.setFieldsValue({ provider: "openai", is_enabled: true, extra_params: "{}" });
|
|
}
|
|
}, 0);
|
|
return () => clearTimeout(timer);
|
|
}, [modalOpen, editingModel]);
|
|
|
|
const handleSubmit = async () => {
|
|
try {
|
|
const values = await form.validateFields();
|
|
let extraParams = {};
|
|
try {
|
|
extraParams = JSON.parse(values.extra_params || "{}");
|
|
} catch {
|
|
message.error("扩展参数 JSON 格式错误");
|
|
return;
|
|
}
|
|
|
|
const payload = {
|
|
...values,
|
|
extra_params: { ...extraParams, ...(values.model_name ? { model: values.model_name } : {}) },
|
|
};
|
|
delete payload.model_name;
|
|
|
|
if (editingModel) {
|
|
if (!payload.api_key) delete payload.api_key;
|
|
await modelApi.updateModel(editingModel.id, payload);
|
|
message.success("更新成功");
|
|
} else {
|
|
await modelApi.createModel(payload);
|
|
message.success("创建成功");
|
|
}
|
|
setModalOpen(false);
|
|
fetchModels();
|
|
} catch (err: unknown) {
|
|
if (err instanceof Error) message.error(err.message);
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (id: string) => {
|
|
await modelApi.deleteModel(id);
|
|
message.success("删除成功");
|
|
fetchModels();
|
|
};
|
|
|
|
const handleToggle = async (id: string, enabled: boolean) => {
|
|
await modelApi.toggleModel(id, enabled);
|
|
fetchModels();
|
|
};
|
|
|
|
const handleTest = async (id: string, name: string) => {
|
|
message.loading({ content: `正在测试 ${name}...`, key: "test" });
|
|
try {
|
|
const res = await modelApi.testModel(id);
|
|
if (res.success) {
|
|
message.success({ content: `${name} 测试通过`, key: "test" });
|
|
Modal.info({ title: `测试结果 - ${name}`, content: res.result, width: 600 });
|
|
} else {
|
|
message.error({ content: `测试失败: ${res.error}`, key: "test", duration: 5 });
|
|
}
|
|
} catch (err: unknown) {
|
|
message.error({ content: err instanceof Error ? err.message : "测试失败", key: "test", duration: 5 });
|
|
}
|
|
};
|
|
|
|
const columns = [
|
|
{ title: "名称", dataIndex: "name", key: "name", width: 150 },
|
|
{
|
|
title: "供应商",
|
|
dataIndex: "provider",
|
|
key: "provider",
|
|
width: 80,
|
|
render: (v: string) => {
|
|
const label = PROVIDERS.find((p) => p.value === v)?.label || v;
|
|
return <Tag>{label}</Tag>;
|
|
},
|
|
},
|
|
{
|
|
title: "模型名",
|
|
key: "model_name",
|
|
width: 140,
|
|
render: (_: unknown, r: AIModel) => <Tag>{(r.extra_params as Record<string,unknown>)?.model as string || "-"}</Tag>,
|
|
},
|
|
{ title: "接口地址", dataIndex: "endpoint", key: "endpoint", ellipsis: true },
|
|
{
|
|
title: "启用",
|
|
dataIndex: "is_enabled",
|
|
key: "is_enabled",
|
|
width: 80,
|
|
render: (v: boolean, record: AIModel) => (
|
|
<Switch
|
|
checked={v}
|
|
size="small"
|
|
onChange={(checked) => handleToggle(record.id, checked)}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
title: "备注",
|
|
dataIndex: "remark",
|
|
key: "remark",
|
|
width: 150,
|
|
ellipsis: true,
|
|
render: (v: string | null) => v || "-",
|
|
},
|
|
{
|
|
title: "操作",
|
|
key: "actions",
|
|
width: 160,
|
|
render: (_: unknown, record: AIModel) => (
|
|
<Space>
|
|
<Button
|
|
type="link"
|
|
size="small"
|
|
icon={<ExperimentOutlined />}
|
|
onClick={() => handleTest(record.id, record.name)}
|
|
/>
|
|
<Button
|
|
type="link"
|
|
size="small"
|
|
icon={<EditOutlined />}
|
|
onClick={() => handleEdit(record)}
|
|
/>
|
|
<Popconfirm
|
|
title="确定删除此模型?"
|
|
onConfirm={() => handleDelete(record.id)}
|
|
>
|
|
<Button type="link" size="small" danger icon={<DeleteOutlined />} />
|
|
</Popconfirm>
|
|
</Space>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div>
|
|
<div style={{ marginBottom: 16, display: "flex", justifyContent: "space-between" }}>
|
|
<h2>模型管理</h2>
|
|
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>
|
|
新建模型
|
|
</Button>
|
|
</div>
|
|
<Table
|
|
columns={columns}
|
|
dataSource={models}
|
|
rowKey="id"
|
|
loading={loading}
|
|
pagination={false}
|
|
/>
|
|
<Modal
|
|
title={editingModel ? "编辑模型" : "新建模型"}
|
|
open={modalOpen}
|
|
onCancel={() => { setModalOpen(false); form.resetFields(); }}
|
|
onOk={handleSubmit}
|
|
width={600}
|
|
>
|
|
<Form form={form} layout="vertical">
|
|
<Form.Item name="name" label="名称" rules={[{ required: true }]}>
|
|
<Input placeholder="模型名称" />
|
|
</Form.Item>
|
|
<Form.Item name="provider" label="供应商" rules={[{ required: true }]}>
|
|
<Select options={PROVIDERS} />
|
|
</Form.Item>
|
|
<Form.Item name="model_name" label="模型名" rules={[{ required: true }]}>
|
|
<Input placeholder="例如 deepseek-chat / gpt-4 / gpt-3.5-turbo" />
|
|
</Form.Item>
|
|
<Form.Item name="endpoint" label="接口地址" rules={[{ required: true }]}>
|
|
<Input placeholder="https://api.openai.com/v1/chat/completions" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
name="api_key"
|
|
label="API Key"
|
|
rules={editingModel ? [] : [{ required: true, message: "请输入 API Key" }]}
|
|
>
|
|
<Input.Password placeholder={editingModel ? "留空则不修改" : "sk-..."} />
|
|
</Form.Item>
|
|
<Form.Item name="extra_params" label="扩展参数 (JSON)">
|
|
<Input.TextArea rows={4} placeholder='{"temperature": 0.7, "max_tokens": 2000}' />
|
|
</Form.Item>
|
|
<Form.Item name="is_enabled" label="启用" valuePropName="checked">
|
|
<Switch />
|
|
</Form.Item>
|
|
<Form.Item name="remark" label="备注">
|
|
<Input.TextArea rows={2} />
|
|
</Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
</div>
|
|
);
|
|
}
|