819 lines
27 KiB
TypeScript
819 lines
27 KiB
TypeScript
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||
import {
|
||
App,
|
||
AutoComplete,
|
||
Button,
|
||
Col,
|
||
Divider,
|
||
Drawer,
|
||
Form,
|
||
Input,
|
||
InputNumber,
|
||
Popconfirm,
|
||
Row,
|
||
Select,
|
||
Space,
|
||
Switch,
|
||
Table,
|
||
Tabs,
|
||
Tag,
|
||
Tooltip,
|
||
Typography,
|
||
} from "antd";
|
||
import {
|
||
DeleteOutlined,
|
||
EditOutlined,
|
||
PlusOutlined,
|
||
SafetyCertificateOutlined,
|
||
SaveOutlined,
|
||
SearchOutlined,
|
||
SyncOutlined,
|
||
WifiOutlined,
|
||
} from "@ant-design/icons";
|
||
import PageContainer from "@/components/shared/PageContainer";
|
||
import DataListPanel from "@/components/shared/DataListPanel";
|
||
import SectionCard from "@/components/shared/SectionCard";
|
||
import AppPagination from "../../components/shared/AppPagination";
|
||
import { useDict } from "../../hooks/useDict";
|
||
import {
|
||
AiLocalProfileVO,
|
||
AiModelDTO,
|
||
AiModelVO,
|
||
deleteAiModelByType,
|
||
getAiModelPage,
|
||
getRemoteModelList,
|
||
saveAiModel,
|
||
setTenantDefaultModel,
|
||
syncCurrentAsrSpeakers,
|
||
tenantDisableModel,
|
||
tenantEnableModel,
|
||
testLlmModelConnectivity,
|
||
testLocalModelConnectivity,
|
||
updateAiModel,
|
||
updatePlatformModelStatus,
|
||
} from "../../api/business/aimodel";
|
||
import {getMeetingCreateConfig, type MeetingCreateConfig} from "../../api/business/meeting";
|
||
import "./AiModels.css";
|
||
|
||
const { Option } = Select;
|
||
const { Title } = Typography;
|
||
|
||
type ModelType = "ASR" | "LLM";
|
||
|
||
const DEFAULT_CREATE_CONFIG: MeetingCreateConfig = {
|
||
offlineEnabled: true,
|
||
realtimeEnabled: false,
|
||
offlineAudioMaxSizeMb: 1024,
|
||
};
|
||
|
||
const PROVIDER_BASE_URL_MAP: Record<string, string> = {
|
||
openai: "https://api.openai.com",
|
||
deepseek: "https://api.deepseek.com",
|
||
aliyun: "https://dashscope.aliyuncs.com/compatible-mode",
|
||
qwen: "https://dashscope.aliyuncs.com/compatible-mode",
|
||
dashscope: "https://dashscope.aliyuncs.com/compatible-mode",
|
||
moonshot: "https://api.moonshot.cn",
|
||
kimi: "https://api.moonshot.cn",
|
||
groq: "https://api.groq.com/openai",
|
||
};
|
||
|
||
const DEFAULT_LLM_TEST_MESSAGE = "请只返回固定成功结果,用于 LLM 连通性测试。";
|
||
|
||
const AiModels: React.FC = () => {
|
||
const { message } = App.useApp();
|
||
const [form] = Form.useForm();
|
||
const { items: providers } = useDict("biz_ai_provider");
|
||
|
||
const [activeType, setActiveType] = useState<ModelType>("ASR");
|
||
const [loading, setLoading] = useState(false);
|
||
const [data, setData] = useState<AiModelVO[]>([]);
|
||
const [total, setTotal] = useState(0);
|
||
const [current, setCurrent] = useState(1);
|
||
const [size, setSize] = useState(10);
|
||
const [searchName, setSearchName] = useState("");
|
||
|
||
const [drawerVisible, setDrawerVisible] = useState(false);
|
||
const [editingId, setEditingId] = useState<number | null>(null);
|
||
const [submitLoading, setSubmitLoading] = useState(false);
|
||
const [fetchLoading, setFetchLoading] = useState(false);
|
||
const [connectivityLoading, setConnectivityLoading] = useState(false);
|
||
const [remoteModels, setRemoteModels] = useState<string[]>([]);
|
||
const [createConfig, setCreateConfig] = useState<MeetingCreateConfig>(DEFAULT_CREATE_CONFIG);
|
||
|
||
const modelNameAutoFilledRef = useRef(false);
|
||
const localProfileLoadedRef = useRef(false);
|
||
|
||
const provider = Form.useWatch("provider", form);
|
||
const isDefaultChecked = Form.useWatch("isDefaultChecked", form);
|
||
const isLocalProvider = String(provider || "").toLowerCase() === "local";
|
||
const isTencentProvider = String(provider || "").toLowerCase() === "tencent";
|
||
|
||
const isPlatformAdmin = useMemo(() => {
|
||
const profileStr = sessionStorage.getItem("userProfile");
|
||
if (!profileStr) {
|
||
return false;
|
||
}
|
||
try {
|
||
const profile = JSON.parse(profileStr);
|
||
return profile.isPlatformAdmin === true;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
void fetchData();
|
||
}, [current, size, searchName, activeType]);
|
||
|
||
useEffect(() => {
|
||
getMeetingCreateConfig()
|
||
.then((res) => {
|
||
const config = (res as any)?.data?.data ?? (res as any);
|
||
setCreateConfig({
|
||
...DEFAULT_CREATE_CONFIG,
|
||
...(config || {}),
|
||
});
|
||
})
|
||
.catch(() => {
|
||
setCreateConfig(DEFAULT_CREATE_CONFIG);
|
||
});
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (!drawerVisible || !provider) {
|
||
return;
|
||
}
|
||
|
||
const providerItem = providers.find((item) => item.itemValue === provider);
|
||
const providerLabel = providerItem?.itemLabel || provider;
|
||
const currentDisplayName = form.getFieldValue("modelName");
|
||
if (!editingId && (!currentDisplayName || modelNameAutoFilledRef.current)) {
|
||
form.setFieldValue("modelName", providerLabel);
|
||
modelNameAutoFilledRef.current = true;
|
||
}
|
||
|
||
const baseUrl = form.getFieldValue("baseUrl");
|
||
const providerKey = String(provider).toLowerCase();
|
||
const defaultBaseUrl = PROVIDER_BASE_URL_MAP[providerKey];
|
||
if (!baseUrl && defaultBaseUrl) {
|
||
form.setFieldValue("baseUrl", defaultBaseUrl);
|
||
}
|
||
}, [drawerVisible, editingId, form, provider, providers]);
|
||
|
||
useEffect(() => {
|
||
if (!drawerVisible || !isLocalProvider || !editingId || localProfileLoadedRef.current) {
|
||
return;
|
||
}
|
||
const values = form.getFieldsValue(["baseUrl", "apiKey"]);
|
||
if (!values.baseUrl || !values.apiKey) {
|
||
return;
|
||
}
|
||
localProfileLoadedRef.current = true;
|
||
void handleTestConnectivity();
|
||
}, [drawerVisible, editingId, form, isLocalProvider]);
|
||
|
||
const fetchData = async () => {
|
||
setLoading(true);
|
||
try {
|
||
const res = await getAiModelPage({
|
||
current,
|
||
size,
|
||
name: searchName || undefined,
|
||
type: activeType,
|
||
});
|
||
const pageData = (res as any)?.data?.data ?? (res as any);
|
||
setData(pageData?.records || []);
|
||
setTotal(pageData?.total || 0);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const openDrawer = (record?: AiModelVO) => {
|
||
setRemoteModels([]);
|
||
modelNameAutoFilledRef.current = false;
|
||
localProfileLoadedRef.current = false;
|
||
|
||
if (record) {
|
||
setEditingId(record.id);
|
||
form.setFieldsValue({
|
||
...record,
|
||
modelType: record.modelType,
|
||
speakerModel: record.mediaConfig?.speakerModel,
|
||
svThreshold: record.mediaConfig?.svThreshold,
|
||
tencentAppId: record.mediaConfig?.tencentAppId,
|
||
tencentSecretId: record.mediaConfig?.tencentSecretId,
|
||
tencentSecretKey: record.mediaConfig?.tencentSecretKey,
|
||
tencentOfflineModelCode: record.mediaConfig?.tencentOfflineModelCode || record.modelCode,
|
||
tencentRealtimeModelCode: record.mediaConfig?.tencentRealtimeModelCode || record.modelCode,
|
||
isDefaultChecked: record.isDefault === 1,
|
||
statusChecked: record.status === 1,
|
||
});
|
||
if (record.modelCode) {
|
||
setRemoteModels([record.modelCode]);
|
||
}
|
||
} else {
|
||
setEditingId(null);
|
||
form.resetFields();
|
||
form.setFieldsValue({
|
||
modelType: activeType,
|
||
isDefaultChecked: false,
|
||
statusChecked: true,
|
||
sortOrder: 0,
|
||
temperature: 0.2,
|
||
topP: 0.9,
|
||
apiPath: "/v1/chat/completions",
|
||
svThreshold: 0.45,
|
||
});
|
||
}
|
||
|
||
setDrawerVisible(true);
|
||
};
|
||
|
||
const handleFetchRemote = async () => {
|
||
if (isLocalProvider) {
|
||
await handleTestConnectivity();
|
||
return;
|
||
}
|
||
|
||
const values = form.getFieldsValue(["provider", "baseUrl", "apiKey"]);
|
||
if (!values.provider || !values.baseUrl) {
|
||
message.warning("请先填写提供商和 Base URL");
|
||
return;
|
||
}
|
||
|
||
setFetchLoading(true);
|
||
try {
|
||
const res = await getRemoteModelList(values);
|
||
const rawModels = (res as any)?.data?.data ?? (Array.isArray(res) ? res : []);
|
||
const models = Array.isArray(rawModels) ? rawModels : [];
|
||
setRemoteModels(models);
|
||
message.success(`获取到 ${models.length} 个模型`);
|
||
} finally {
|
||
setFetchLoading(false);
|
||
}
|
||
};
|
||
|
||
const resolveLocalWsUrl = (baseUrl: string, wsEndpoint?: string) => {
|
||
if (!wsEndpoint) {
|
||
return undefined;
|
||
}
|
||
try {
|
||
const base = new URL(baseUrl);
|
||
const endpoint = wsEndpoint.startsWith("/") ? wsEndpoint : `/${wsEndpoint}`;
|
||
const protocol = base.protocol === "https:" ? "wss:" : "ws:";
|
||
return `${protocol}//${base.host}${endpoint}`;
|
||
} catch {
|
||
return undefined;
|
||
}
|
||
};
|
||
|
||
const applyLocalProfile = (profile: AiLocalProfileVO, baseUrl: string) => {
|
||
const nextRemoteModels = Array.isArray(profile.asrModels) ? profile.asrModels : [];
|
||
setRemoteModels(nextRemoteModels);
|
||
|
||
const nextValues: Record<string, unknown> = {};
|
||
if (profile.activeAsrModel) {
|
||
nextValues.modelCode = profile.activeAsrModel;
|
||
}
|
||
if (profile.activeSpeakerModel) {
|
||
nextValues.speakerModel = profile.activeSpeakerModel;
|
||
}
|
||
if (profile.svThreshold !== undefined) {
|
||
nextValues.svThreshold = profile.svThreshold;
|
||
}
|
||
const wsUrl = resolveLocalWsUrl(baseUrl, profile.wsEndpoint);
|
||
if (wsUrl) {
|
||
nextValues.wsUrl = wsUrl;
|
||
}
|
||
form.setFieldsValue(nextValues);
|
||
};
|
||
|
||
const handleSubmit = async () => {
|
||
const values = await form.validateFields();
|
||
if (values.isDefaultChecked && !values.statusChecked) {
|
||
message.warning("默认模型必须保持启用状态");
|
||
return;
|
||
}
|
||
|
||
const payload: AiModelDTO = {
|
||
id: editingId ?? undefined,
|
||
modelType: values.modelType,
|
||
modelName: values.modelName,
|
||
provider: values.provider,
|
||
baseUrl: values.baseUrl,
|
||
apiPath: values.apiPath,
|
||
apiKey: values.apiKey,
|
||
modelCode: activeType === "ASR" && isTencentProvider ? values.tencentOfflineModelCode : values.modelCode,
|
||
wsUrl: activeType === "ASR" ? values.wsUrl : undefined,
|
||
mediaConfig:
|
||
activeType === "ASR" && isLocalProvider
|
||
? {
|
||
speakerModel: values.speakerModel,
|
||
svThreshold: values.svThreshold,
|
||
}
|
||
: activeType === "ASR" && isTencentProvider
|
||
? {
|
||
tencentAppId: values.tencentAppId,
|
||
tencentSecretId: values.tencentSecretId,
|
||
tencentSecretKey: values.tencentSecretKey,
|
||
tencentOfflineModelCode: values.tencentOfflineModelCode,
|
||
tencentRealtimeModelCode: values.tencentRealtimeModelCode,
|
||
}
|
||
: undefined,
|
||
temperature: values.temperature,
|
||
topP: values.topP,
|
||
isDefault: values.isDefaultChecked ? 1 : 0,
|
||
status: values.statusChecked ? 1 : 0,
|
||
sortOrder: values.sortOrder ?? 0,
|
||
remark: values.remark,
|
||
};
|
||
|
||
setSubmitLoading(true);
|
||
try {
|
||
if (editingId) {
|
||
await updateAiModel(payload);
|
||
message.success("更新成功");
|
||
} else {
|
||
await saveAiModel(payload);
|
||
message.success("新增成功");
|
||
}
|
||
setDrawerVisible(false);
|
||
void fetchData();
|
||
} finally {
|
||
setSubmitLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleTestConnectivity = async () => {
|
||
if (activeType === "LLM") {
|
||
const values = await form.validateFields(["provider", "baseUrl", "apiPath", "modelCode"]);
|
||
const extraValues = form.getFieldsValue(["apiKey", "temperature", "topP"]);
|
||
setConnectivityLoading(true);
|
||
try {
|
||
await testLlmModelConnectivity({
|
||
provider: values.provider,
|
||
baseUrl: values.baseUrl,
|
||
apiPath: values.apiPath,
|
||
apiKey: extraValues.apiKey,
|
||
modelCode: values.modelCode,
|
||
temperature: extraValues.temperature,
|
||
topP: extraValues.topP,
|
||
testMessage: DEFAULT_LLM_TEST_MESSAGE,
|
||
});
|
||
message.success("LLM 连通性测试成功");
|
||
} finally {
|
||
setConnectivityLoading(false);
|
||
}
|
||
return;
|
||
}
|
||
|
||
const values = await form.validateFields(["provider", "baseUrl"]);
|
||
if (String(values.provider || "").toLowerCase() !== "local") {
|
||
message.warning("只有本地 ASR 支持该连通性测试");
|
||
return;
|
||
}
|
||
|
||
const { apiKey } = form.getFieldsValue(["apiKey"]);
|
||
setConnectivityLoading(true);
|
||
try {
|
||
const res = await testLocalModelConnectivity({
|
||
baseUrl: values.baseUrl,
|
||
apiKey,
|
||
});
|
||
const profile = (res as any)?.data?.data ?? (res as any)?.data ?? (res as any);
|
||
applyLocalProfile(profile as AiLocalProfileVO, values.baseUrl);
|
||
message.success("本地模型连通性测试成功");
|
||
} finally {
|
||
setConnectivityLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleDelete = async (record: AiModelVO) => {
|
||
await deleteAiModelByType(record.id, record.modelType);
|
||
message.success("删除成功");
|
||
void fetchData();
|
||
};
|
||
|
||
const handleTenantToggle = async (record: AiModelVO, checked: boolean) => {
|
||
if (checked) {
|
||
await tenantEnableModel(record.id, activeType);
|
||
message.success(activeType === "ASR" ? "已切换当前 ASR" : "已启用当前 LLM");
|
||
} else {
|
||
await tenantDisableModel(record.id, activeType);
|
||
message.success(activeType === "ASR" ? "已关闭当前 ASR" : "已关闭当前 LLM");
|
||
}
|
||
await fetchData();
|
||
};
|
||
|
||
const handlePlatformStatusToggle = async (record: AiModelVO, checked: boolean) => {
|
||
await updatePlatformModelStatus(record.id, activeType, checked ? 1 : 0);
|
||
message.success(checked ? `平台级 ${activeType} 已启用` : `平台级 ${activeType} 已禁用`);
|
||
await fetchData();
|
||
};
|
||
|
||
const handleSyncCurrentAsr = async () => {
|
||
await syncCurrentAsrSpeakers();
|
||
message.success("已提交后台同步任务");
|
||
};
|
||
|
||
const handleSetTenantDefault = async (record: AiModelVO) => {
|
||
await setTenantDefaultModel(record.id, "LLM");
|
||
message.success("已设置为默认 LLM");
|
||
await fetchData();
|
||
};
|
||
|
||
const resolvedTableColumns = [
|
||
{
|
||
title: "模型名称",
|
||
dataIndex: "modelName",
|
||
key: "modelName",
|
||
render: (text: string, record: AiModelVO) => (
|
||
<Space>
|
||
{text}
|
||
{record.isDefault === 1 && <Tag color="gold">系统默认</Tag>}
|
||
{record.tenantDefault === 1 && <Tag color="blue">租户默认</Tag>}
|
||
{record.tenantId === 0 && (
|
||
<Tooltip title="平台透传模型">
|
||
<SafetyCertificateOutlined style={{ color: "#52c41a" }} />
|
||
</Tooltip>
|
||
)}
|
||
{record.scope && (
|
||
<Tag bordered={false} color={record.scope === "PLATFORM" ? "geekblue" : "default"}>
|
||
{record.scope === "PLATFORM" ? "平台级" : "租户级"}
|
||
</Tag>
|
||
)}
|
||
</Space>
|
||
),
|
||
},
|
||
{
|
||
title: "提供商",
|
||
dataIndex: "provider",
|
||
key: "provider",
|
||
render: (value: string) => {
|
||
const item = providers.find((providerItem) => providerItem.itemValue === value);
|
||
return item ? <Tag>{item.itemLabel}</Tag> : value;
|
||
},
|
||
},
|
||
{
|
||
title: "模型编码",
|
||
dataIndex: "modelCode",
|
||
key: "modelCode",
|
||
},
|
||
{
|
||
title: "排序",
|
||
dataIndex: "sortOrder",
|
||
key: "sortOrder",
|
||
render: (value: number | undefined) => value ?? 0,
|
||
},
|
||
{
|
||
title: "状态",
|
||
dataIndex: "status",
|
||
key: "status",
|
||
render: (status: number, record: AiModelVO) => {
|
||
if (isPlatformAdmin && record.scope === "PLATFORM") {
|
||
return (
|
||
<Switch
|
||
checked={status === 1}
|
||
checkedChildren="启用"
|
||
unCheckedChildren="禁用"
|
||
onChange={(checked) => void handlePlatformStatusToggle(record, checked)}
|
||
/>
|
||
);
|
||
}
|
||
return (
|
||
<Switch
|
||
checked={record.tenantEnabled === 1}
|
||
checkedChildren={activeType === "ASR" ? "当前生效" : "已启用"}
|
||
unCheckedChildren={activeType === "ASR" ? "未启用" : "已关闭"}
|
||
disabled={status !== 1}
|
||
onChange={(checked) => void handleTenantToggle(record, checked)}
|
||
/>
|
||
);
|
||
},
|
||
},
|
||
{
|
||
title: "操作",
|
||
key: "action",
|
||
render: (_: unknown, record: AiModelVO) => {
|
||
const canEdit = record.canEditConfig ?? (record.tenantId !== 0 || isPlatformAdmin);
|
||
const canSetDefault = activeType === "LLM" && record.tenantEnabled === 1;
|
||
return (
|
||
<Space>
|
||
{canSetDefault && (
|
||
<Button type="link" onClick={() => void handleSetTenantDefault(record)}>
|
||
{record.tenantDefault === 1 ? "默认 LLM" : "设为默认"}
|
||
</Button>
|
||
)}
|
||
{canEdit && (
|
||
<Button type="link" icon={<EditOutlined />} onClick={() => openDrawer(record)}>
|
||
编辑
|
||
</Button>
|
||
)}
|
||
{canEdit && (
|
||
<Popconfirm title="确定删除吗?" onConfirm={() => handleDelete(record)}>
|
||
<Button type="link" danger icon={<DeleteOutlined />}>
|
||
删除
|
||
</Button>
|
||
</Popconfirm>
|
||
)}
|
||
</Space>
|
||
);
|
||
},
|
||
},
|
||
];
|
||
|
||
const leftActions = (
|
||
<Space wrap>
|
||
<Button type="primary" icon={<PlusOutlined/>} onClick={() => openDrawer()}>
|
||
新增模型
|
||
</Button>
|
||
{activeType === "ASR" && (
|
||
<Button icon={<SyncOutlined/>} onClick={() => void handleSyncCurrentAsr()}>
|
||
同步当前 ASR 声纹
|
||
</Button>
|
||
)}
|
||
</Space>
|
||
);
|
||
|
||
return (
|
||
<PageContainer title={null} className="ai-models-page">
|
||
<SectionCard
|
||
title="AI 模型配置"
|
||
description="管理 ASR 语音识别和 LLM 大语言模型。"
|
||
tabs={
|
||
<Tabs
|
||
activeKey={activeType}
|
||
onChange={(key) => {
|
||
setActiveType(key as ModelType);
|
||
setCurrent(1);
|
||
}}
|
||
items={[
|
||
{ key: "ASR", label: "ASR 模型" },
|
||
{ key: "LLM", label: "LLM 模型" },
|
||
]}
|
||
size="middle"
|
||
type="card"
|
||
/>
|
||
}
|
||
>
|
||
<DataListPanel
|
||
className="ai-models-data-panel"
|
||
leftActions={leftActions}
|
||
rightActions={
|
||
<Input.Search
|
||
allowClear
|
||
placeholder="搜索模型名称"
|
||
prefix={<SearchOutlined />}
|
||
className="ai-models-search"
|
||
onSearch={(value) => {
|
||
setCurrent(1);
|
||
setSearchName(value.trim());
|
||
}}
|
||
/>
|
||
}
|
||
footer={
|
||
<AppPagination
|
||
current={current}
|
||
pageSize={size}
|
||
total={total}
|
||
onChange={(page, pageSize) => {
|
||
setCurrent(page);
|
||
setSize(pageSize);
|
||
}}
|
||
/>
|
||
}
|
||
>
|
||
<Table
|
||
rowKey="id"
|
||
columns={resolvedTableColumns}
|
||
dataSource={data}
|
||
loading={loading}
|
||
scroll={{ x: "max-content", y: "100%" }}
|
||
pagination={false}
|
||
/>
|
||
</DataListPanel>
|
||
</SectionCard>
|
||
|
||
<Drawer
|
||
width={600}
|
||
open={drawerVisible}
|
||
onClose={() => setDrawerVisible(false)}
|
||
title={<Title level={4} style={{ margin: 0 }}>{editingId ? "编辑模型" : "新增模型"}</Title>}
|
||
forceRender
|
||
extra={
|
||
<Space>
|
||
<Button onClick={() => setDrawerVisible(false)}>取消</Button>
|
||
<Button type="primary" icon={<SaveOutlined />} loading={submitLoading} onClick={handleSubmit}>
|
||
保存
|
||
</Button>
|
||
</Space>
|
||
}
|
||
>
|
||
<Form form={form} layout="vertical">
|
||
<Form.Item name="modelType" hidden>
|
||
<Input />
|
||
</Form.Item>
|
||
|
||
<Form.Item label="模型类型">
|
||
<Tag color={activeType === "ASR" ? "blue" : "purple"}>
|
||
{activeType === "ASR" ? "语音识别 (ASR)" : "大语言模型 (LLM)"}
|
||
</Tag>
|
||
</Form.Item>
|
||
|
||
<Row gutter={16}>
|
||
<Col span={12}>
|
||
<Form.Item
|
||
name="modelName"
|
||
label="显示名称"
|
||
rules={[{ required: true, message: "请输入显示名称" }]}
|
||
>
|
||
<Input onChange={() => {
|
||
modelNameAutoFilledRef.current = false;
|
||
}}/>
|
||
</Form.Item>
|
||
</Col>
|
||
<Col span={12}>
|
||
<Form.Item
|
||
name="provider"
|
||
label="提供商"
|
||
rules={[{ required: true, message: "请选择提供商" }]}
|
||
>
|
||
<Select allowClear placeholder="请选择">
|
||
{providers.map((item) => (
|
||
<Option key={item.itemValue} value={item.itemValue}>
|
||
{item.itemLabel}
|
||
</Option>
|
||
))}
|
||
</Select>
|
||
</Form.Item>
|
||
</Col>
|
||
</Row>
|
||
|
||
<Row gutter={16}>
|
||
<Col span={12}>
|
||
<Form.Item name="sortOrder" label="排序值">
|
||
<InputNumber min={0} style={{ width: "100%" }} />
|
||
</Form.Item>
|
||
</Col>
|
||
</Row>
|
||
|
||
{!isTencentProvider && (
|
||
<>
|
||
<Form.Item name="baseUrl" label="Base URL" rules={[{required: true, message: "请输入 Base URL"}]}>
|
||
<Input placeholder="https://api.example.com"/>
|
||
</Form.Item>
|
||
<Form.Item name="apiKey" label="API Key">
|
||
<Input.Password/>
|
||
</Form.Item>
|
||
</>
|
||
)}
|
||
|
||
{(activeType === "LLM" || isLocalProvider) && (
|
||
<Form.Item label="连通性测试">
|
||
<Button icon={<WifiOutlined />} loading={connectivityLoading} onClick={handleTestConnectivity}>
|
||
测试连接
|
||
</Button>
|
||
</Form.Item>
|
||
)}
|
||
|
||
<Divider orientation="left" style={{ fontSize: 14, color: "#999" }}>
|
||
模型参数
|
||
</Divider>
|
||
|
||
<Form.Item
|
||
label="模型编码"
|
||
required={activeType === "LLM"}
|
||
hidden={activeType === "ASR" && isTencentProvider}
|
||
tooltip="可从远程列表选择,也可手动输入;该值会作为模型编码传给后端"
|
||
>
|
||
<Space.Compact style={{ width: "100%" }}>
|
||
<Form.Item
|
||
name="modelCode"
|
||
noStyle
|
||
rules={activeType === "LLM" ? [{required: true, message: "请输入或选择模型编码"}] : []}
|
||
>
|
||
<AutoComplete
|
||
style={{ width: "calc(100% - 100px)" }}
|
||
onFocus={() => {
|
||
if (isLocalProvider && remoteModels.length === 0) {
|
||
void handleFetchRemote();
|
||
}
|
||
}}
|
||
options={remoteModels.map((model) => ({ value: model }))}
|
||
filterOption={(inputValue, option) =>
|
||
isLocalProvider || String(option?.value || "").toLowerCase().includes(inputValue.toLowerCase())
|
||
}
|
||
>
|
||
<Input allowClear placeholder="可选择或手动输入模型编码"/>
|
||
</AutoComplete>
|
||
</Form.Item>
|
||
{!isTencentProvider && (
|
||
<Button icon={<SyncOutlined spin={fetchLoading}/>} onClick={handleFetchRemote} style={{width: 100}}>
|
||
刷新
|
||
</Button>
|
||
)}
|
||
</Space.Compact>
|
||
</Form.Item>
|
||
|
||
<Form.Item name="wsUrl" label="WebSocket 地址"
|
||
hidden={!(activeType === "ASR" && createConfig.realtimeEnabled)}>
|
||
<Input placeholder="wss://api.example.com/v1/ws" />
|
||
</Form.Item>
|
||
|
||
{activeType === "ASR" && isLocalProvider && (
|
||
<Row gutter={16} hidden>
|
||
<Col span={12}>
|
||
<Form.Item name="svThreshold" label="声纹阈值">
|
||
<InputNumber min={0} max={1} step={0.01} style={{ width: "100%" }} />
|
||
</Form.Item>
|
||
</Col>
|
||
</Row>
|
||
)}
|
||
|
||
{activeType === "ASR" && isTencentProvider && (
|
||
<Row gutter={16}>
|
||
<Col span={12}>
|
||
<Form.Item name="tencentAppId" label="App ID" rules={[{required: true, message: "请输入 App ID"}]}>
|
||
<Input/>
|
||
</Form.Item>
|
||
</Col>
|
||
<Col span={12}>
|
||
<Form.Item name="tencentSecretId" label="Secret ID"
|
||
rules={[{required: true, message: "请输入 Secret ID"}]}>
|
||
<Input/>
|
||
</Form.Item>
|
||
</Col>
|
||
<Col span={24}>
|
||
<Form.Item name="tencentSecretKey" label="Secret Key"
|
||
rules={[{required: true, message: "请输入 Secret Key"}]}>
|
||
<Input.Password/>
|
||
</Form.Item>
|
||
</Col>
|
||
<Col span={12}>
|
||
<Form.Item name="tencentOfflineModelCode" label="离线识别模型"
|
||
rules={[{required: true, message: "请输入离线识别模型"}]}>
|
||
<Input placeholder="例如:16k_zh"/>
|
||
</Form.Item>
|
||
</Col>
|
||
<Col span={12}>
|
||
<Form.Item name="tencentRealtimeModelCode" label="实时识别模型"
|
||
rules={[{required: true, message: "请输入实时识别模型"}]}>
|
||
<Input placeholder="例如:16k_zh_realtime"/>
|
||
</Form.Item>
|
||
</Col>
|
||
</Row>
|
||
)}
|
||
|
||
{activeType === "LLM" && (
|
||
<>
|
||
<Form.Item name="apiPath" label="API 路径" initialValue="/v1/chat/completions">
|
||
<Input />
|
||
</Form.Item>
|
||
<Row gutter={16}>
|
||
<Col span={12}>
|
||
<Form.Item name="temperature" label="Temperature">
|
||
<InputNumber min={0} max={2} step={0.1} style={{ width: "100%" }} />
|
||
</Form.Item>
|
||
</Col>
|
||
<Col span={12}>
|
||
<Form.Item name="topP" label="Top P">
|
||
<InputNumber min={0} max={1} step={0.1} style={{ width: "100%" }} />
|
||
</Form.Item>
|
||
</Col>
|
||
</Row>
|
||
</>
|
||
)}
|
||
|
||
<Row gutter={16}>
|
||
<Col span={8}>
|
||
<Form.Item name="isDefaultChecked" label="设为默认" valuePropName="checked">
|
||
<Switch
|
||
checkedChildren="是"
|
||
unCheckedChildren="否"
|
||
onChange={(checked) => {
|
||
if (checked) {
|
||
form.setFieldValue("statusChecked", true);
|
||
}
|
||
}}
|
||
/>
|
||
</Form.Item>
|
||
</Col>
|
||
<Col span={8}>
|
||
<Form.Item name="statusChecked" label="状态" valuePropName="checked">
|
||
<Switch checkedChildren="启用" unCheckedChildren="禁用" disabled={Boolean(isDefaultChecked)} />
|
||
</Form.Item>
|
||
</Col>
|
||
</Row>
|
||
|
||
<Form.Item name="remark" label="备注">
|
||
<Input.TextArea rows={2} />
|
||
</Form.Item>
|
||
</Form>
|
||
</Drawer>
|
||
</PageContainer>
|
||
);
|
||
};
|
||
|
||
export default AiModels;
|