Files
Alfred/src/web/pages/models/index.tsx
lanyuanxiaoyao 933c2133f0 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
2026-05-29 12:40:10 +08:00

187 lines
6.6 KiB
TypeScript

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>
);
}