feat: 新增模型管理功能(供应商 + 模型 CRUD)
- 新增 providers/models 数据库表、迁移和数据访问层 - 新增 15 个后端 API 路由(供应商/模型 CRUD + 连通性测试) - 新增 AI 服务层(registry.ts: buildProviderRegistry + testProviderConnection) - 新增前端模型管理页面(Tabs: 供应商/模型,含表格、表单、工具栏) - 新增前端 hooks(use-providers, use-models) - 新增共享类型和 MODEL_CAPABILITIES 常量 - 新增 10 个测试文件(66 个测试用例,4 个因 bun test ESM 兼容问题待修复) - 更新开发文档(architecture, backend, frontend) - 附带 apply-review 修复:统一错误响应、提取共享常量、清理重复测试 注意:registry.test.ts 中 4 个测试因 bun test 无法解析 createProviderRegistry ESM 导出而失败,详情见 context.md
This commit is contained in:
186
src/web/pages/models/index.tsx
Normal file
186
src/web/pages/models/index.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import { Flex, Tabs } from "antd";
|
||||
import { useState } from "react";
|
||||
|
||||
import type { Model, Provider } from "../../../shared/api";
|
||||
|
||||
import {
|
||||
useCreateModel,
|
||||
useDeleteModel,
|
||||
useDisableModel,
|
||||
useEnableModel,
|
||||
useModelList,
|
||||
useUpdateModel,
|
||||
} from "../../hooks/use-models";
|
||||
import {
|
||||
useCreateProvider,
|
||||
useDeleteProvider,
|
||||
useDisableProvider,
|
||||
useEnableProvider,
|
||||
useProviderList,
|
||||
useTestProviderConnection,
|
||||
useUpdateProvider,
|
||||
} from "../../hooks/use-providers";
|
||||
import { ModelFormModal } from "./components/ModelFormModal";
|
||||
import { ModelTable } from "./components/ModelTable";
|
||||
import { ModelToolbar } from "./components/ModelToolbar";
|
||||
import { ProviderFormModal } from "./components/ProviderFormModal";
|
||||
import { ProviderTable } from "./components/ProviderTable";
|
||||
import { ProviderToolbar } from "./components/ProviderToolbar";
|
||||
|
||||
export function ModelsPage() {
|
||||
const [activeTab, setActiveTab] = useState<string>("providers");
|
||||
|
||||
const [providerPage, setProviderPage] = useState(1);
|
||||
const [providerPageSize, setProviderPageSize] = useState(20);
|
||||
const [providerKeyword, setProviderKeyword] = useState("");
|
||||
const [providerDialogOpen, setProviderDialogOpen] = useState(false);
|
||||
const [editingProvider, setEditingProvider] = useState<null | Provider>(null);
|
||||
|
||||
const [modelPage, setModelPage] = useState(1);
|
||||
const [modelPageSize, setModelPageSize] = useState(20);
|
||||
const [modelKeyword, setModelKeyword] = useState("");
|
||||
const [modelDialogOpen, setModelDialogOpen] = useState(false);
|
||||
const [editingModel, setEditingModel] = useState<Model | null>(null);
|
||||
|
||||
const { data: providerData, isLoading: providerLoading } = useProviderList({
|
||||
keyword: providerKeyword || undefined,
|
||||
page: providerPage,
|
||||
pageSize: providerPageSize,
|
||||
});
|
||||
|
||||
const { data: modelData, isLoading: modelLoading } = useModelList({
|
||||
keyword: modelKeyword || undefined,
|
||||
page: modelPage,
|
||||
pageSize: modelPageSize,
|
||||
});
|
||||
|
||||
const createProviderMutation = useCreateProvider();
|
||||
const updateProviderMutation = useUpdateProvider();
|
||||
const deleteProviderMutation = useDeleteProvider();
|
||||
const enableProviderMutation = useEnableProvider();
|
||||
const disableProviderMutation = useDisableProvider();
|
||||
const testProviderMutation = useTestProviderConnection();
|
||||
|
||||
const createModelMutation = useCreateModel();
|
||||
const updateModelMutation = useUpdateModel();
|
||||
const deleteModelMutation = useDeleteModel();
|
||||
const enableModelMutation = useEnableModel();
|
||||
const disableModelMutation = useDisableModel();
|
||||
|
||||
const isProviderSubmitting = createProviderMutation.isPending || updateProviderMutation.isPending;
|
||||
const isProviderActionPending =
|
||||
deleteProviderMutation.isPending || enableProviderMutation.isPending || disableProviderMutation.isPending;
|
||||
|
||||
const isModelSubmitting = createModelMutation.isPending || updateModelMutation.isPending;
|
||||
const isModelActionPending =
|
||||
deleteModelMutation.isPending || enableModelMutation.isPending || disableModelMutation.isPending;
|
||||
|
||||
return (
|
||||
<Flex flex={1} gap="var(--ant-margin-lg)" vertical>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
items={[
|
||||
{ key: "providers", label: "供应商" },
|
||||
{ key: "models", label: "模型" },
|
||||
]}
|
||||
onChange={(key) => setActiveTab(key)}
|
||||
/>
|
||||
|
||||
{activeTab === "providers" && (
|
||||
<>
|
||||
<ProviderToolbar
|
||||
keyword={providerKeyword}
|
||||
onSearch={(value) => {
|
||||
setProviderKeyword(value);
|
||||
setProviderPage(1);
|
||||
}}
|
||||
onSearchClear={() => {
|
||||
setProviderKeyword("");
|
||||
setProviderPage(1);
|
||||
}}
|
||||
openCreateDialog={() => {
|
||||
setEditingProvider(null);
|
||||
setProviderDialogOpen(true);
|
||||
}}
|
||||
/>
|
||||
<ProviderTable
|
||||
data={providerData}
|
||||
loading={providerLoading || isProviderActionPending}
|
||||
onDelete={(id) => deleteProviderMutation.mutateAsync(id)}
|
||||
onDisable={(id) => disableProviderMutation.mutateAsync(id)}
|
||||
onEdit={(provider) => {
|
||||
setEditingProvider(provider);
|
||||
setProviderDialogOpen(true);
|
||||
}}
|
||||
onEnable={(id) => enableProviderMutation.mutateAsync(id)}
|
||||
onPageChange={(p, ps) => {
|
||||
setProviderPage(p);
|
||||
setProviderPageSize(ps);
|
||||
}}
|
||||
onTest={(id) => testProviderMutation.mutateAsync(id)}
|
||||
page={providerPage}
|
||||
pageSize={providerPageSize}
|
||||
/>
|
||||
<ProviderFormModal
|
||||
editingProvider={editingProvider}
|
||||
onCancel={() => setProviderDialogOpen(false)}
|
||||
onCreate={(data) => createProviderMutation.mutateAsync(data)}
|
||||
onOpenChange={setProviderDialogOpen}
|
||||
onUpdate={(args) => updateProviderMutation.mutateAsync(args)}
|
||||
open={providerDialogOpen}
|
||||
submitting={isProviderSubmitting}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === "models" && (
|
||||
<>
|
||||
<ModelToolbar
|
||||
keyword={modelKeyword}
|
||||
onSearch={(value) => {
|
||||
setModelKeyword(value);
|
||||
setModelPage(1);
|
||||
}}
|
||||
onSearchClear={() => {
|
||||
setModelKeyword("");
|
||||
setModelPage(1);
|
||||
}}
|
||||
openCreateDialog={() => {
|
||||
setEditingModel(null);
|
||||
setModelDialogOpen(true);
|
||||
}}
|
||||
/>
|
||||
<ModelTable
|
||||
data={modelData}
|
||||
loading={modelLoading || isModelActionPending}
|
||||
onDelete={(id) => deleteModelMutation.mutateAsync(id)}
|
||||
onDisable={(id) => disableModelMutation.mutateAsync(id)}
|
||||
onEdit={(model) => {
|
||||
setEditingModel(model);
|
||||
setModelDialogOpen(true);
|
||||
}}
|
||||
onEnable={(id) => enableModelMutation.mutateAsync(id)}
|
||||
onPageChange={(p, ps) => {
|
||||
setModelPage(p);
|
||||
setModelPageSize(ps);
|
||||
}}
|
||||
page={modelPage}
|
||||
pageSize={modelPageSize}
|
||||
providers={providerData?.items ?? []}
|
||||
/>
|
||||
<ModelFormModal
|
||||
editingModel={editingModel}
|
||||
onCancel={() => setModelDialogOpen(false)}
|
||||
onCreate={(data) => createModelMutation.mutateAsync(data)}
|
||||
onOpenChange={setModelDialogOpen}
|
||||
onUpdate={(args) => updateModelMutation.mutateAsync(args)}
|
||||
open={modelDialogOpen}
|
||||
providers={providerData?.items ?? []}
|
||||
submitting={isModelSubmitting}
|
||||
testConnection={editingModel ? (id: string) => testProviderMutation.mutateAsync(id) : undefined}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user