- 新增 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
165 lines
5.1 KiB
TypeScript
165 lines
5.1 KiB
TypeScript
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
|
|
import type {
|
|
CreateProviderRequest,
|
|
Provider,
|
|
ProviderListResponse,
|
|
ProviderResponse,
|
|
ProviderTestResponse,
|
|
UpdateProviderRequest,
|
|
} from "../../shared/api";
|
|
|
|
const PROVIDERS_KEY = ["providers"] as const;
|
|
const MODELS_KEY = ["models"] as const;
|
|
|
|
export async function createProvider(data: CreateProviderRequest): Promise<Provider> {
|
|
const response = await fetch("/api/providers", {
|
|
body: JSON.stringify(data),
|
|
headers: { "Content-Type": "application/json" },
|
|
method: "POST",
|
|
});
|
|
return handleResponse(response);
|
|
}
|
|
|
|
export async function deleteProvider(id: string): Promise<void> {
|
|
const response = await fetch(`/api/providers/${id}`, { method: "DELETE" });
|
|
if (!response.ok) {
|
|
const body = (await response.json().catch(() => null)) as null | { error?: string };
|
|
throw new Error(body?.error ?? `HTTP ${response.status}`);
|
|
}
|
|
}
|
|
|
|
export async function disableProvider(id: string): Promise<Provider> {
|
|
const response = await fetch(`/api/providers/${id}/disable`, { method: "POST" });
|
|
return handleResponse(response);
|
|
}
|
|
|
|
export async function enableProvider(id: string): Promise<Provider> {
|
|
const response = await fetch(`/api/providers/${id}/enable`, { method: "POST" });
|
|
return handleResponse(response);
|
|
}
|
|
|
|
export async function fetchProvider(id: string): Promise<Provider> {
|
|
const response = await fetch(`/api/providers/${id}`);
|
|
return handleResponse(response);
|
|
}
|
|
|
|
export async function fetchProviderList(params: {
|
|
keyword?: string;
|
|
page?: number;
|
|
pageSize?: number;
|
|
}): Promise<ProviderListResponse> {
|
|
const searchParams = new URLSearchParams();
|
|
if (params.page) searchParams.set("page", String(params.page));
|
|
if (params.pageSize) searchParams.set("pageSize", String(params.pageSize));
|
|
if (params.keyword) searchParams.set("keyword", params.keyword);
|
|
const qs = searchParams.toString();
|
|
const url = `/api/providers${qs ? `?${qs}` : ""}`;
|
|
const response = await fetch(url);
|
|
if (!response.ok) {
|
|
const body = (await response.json().catch(() => null)) as null | { error?: string };
|
|
throw new Error(body?.error ?? `HTTP ${response.status}`);
|
|
}
|
|
return response.json() as Promise<ProviderListResponse>;
|
|
}
|
|
|
|
export async function testProviderConnection(id: string): Promise<ProviderTestResponse> {
|
|
const response = await fetch(`/api/providers/${id}/test`, { method: "POST" });
|
|
if (!response.ok) {
|
|
const body = (await response.json().catch(() => null)) as null | { error?: string };
|
|
throw new Error(body?.error ?? `HTTP ${response.status}`);
|
|
}
|
|
const data = (await response.json()) as { providerTestResponse: ProviderTestResponse };
|
|
return data.providerTestResponse;
|
|
}
|
|
|
|
export async function updateProvider(id: string, data: UpdateProviderRequest): Promise<Provider> {
|
|
const response = await fetch(`/api/providers/${id}`, {
|
|
body: JSON.stringify(data),
|
|
headers: { "Content-Type": "application/json" },
|
|
method: "PATCH",
|
|
});
|
|
return handleResponse(response);
|
|
}
|
|
|
|
export function useCreateProvider() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: createProvider,
|
|
onSuccess: () => {
|
|
void queryClient.invalidateQueries({ queryKey: PROVIDERS_KEY });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useDeleteProvider() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: deleteProvider,
|
|
onSuccess: () => {
|
|
void queryClient.invalidateQueries({ queryKey: PROVIDERS_KEY });
|
|
void queryClient.invalidateQueries({ queryKey: MODELS_KEY });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useDisableProvider() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: disableProvider,
|
|
onSuccess: () => {
|
|
void queryClient.invalidateQueries({ queryKey: PROVIDERS_KEY });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useEnableProvider() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: enableProvider,
|
|
onSuccess: () => {
|
|
void queryClient.invalidateQueries({ queryKey: PROVIDERS_KEY });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useProvider(id: string) {
|
|
return useQuery({
|
|
enabled: !!id,
|
|
queryFn: () => fetchProvider(id),
|
|
queryKey: [...PROVIDERS_KEY, "detail", id],
|
|
});
|
|
}
|
|
|
|
export function useProviderList(params: { keyword?: string; page?: number; pageSize?: number }) {
|
|
return useQuery({
|
|
queryFn: () => fetchProviderList(params),
|
|
queryKey: [...PROVIDERS_KEY, "list", params],
|
|
});
|
|
}
|
|
|
|
export function useTestProviderConnection() {
|
|
return useMutation({
|
|
mutationFn: testProviderConnection,
|
|
});
|
|
}
|
|
|
|
export function useUpdateProvider() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (args: { data: UpdateProviderRequest; id: string }) => updateProvider(args.id, args.data),
|
|
onSuccess: () => {
|
|
void queryClient.invalidateQueries({ queryKey: PROVIDERS_KEY });
|
|
},
|
|
});
|
|
}
|
|
|
|
async function handleResponse(response: Response): Promise<Provider> {
|
|
if (!response.ok) {
|
|
const body = (await response.json().catch(() => null)) as null | { error?: string };
|
|
throw new Error(body?.error ?? `HTTP ${response.status}`);
|
|
}
|
|
const data = (await response.json()) as ProviderResponse;
|
|
return data.provider;
|
|
}
|