refactor: 简化模型管理,移除启用/禁用,优化测试和布局

- 移除供应商/模型启用禁用能力,清理DB schema/migration/API/前端
- 供应商测试改为Base URL连通性+/models探测
- 新增POST /api/models/test模型连接测试
- 新增GET /api/providers/options专用供应商选项接口
- 统一工具栏为ModelsToolbar,参考项目管理布局
- 模型弹窗优化:默认能力、响应式3列标签、并排数值
- 前后端正整数校验、供应商下拉loading/error/empty状态
- 表格列宽统一,操作列/名称列固定宽度
This commit is contained in:
2026-05-29 18:03:33 +08:00
parent 9241c782e6
commit 34e915ccf4
39 changed files with 895 additions and 961 deletions

View File

@@ -8,10 +8,10 @@ import { createProviderRegistry, generateText } from "ai";
import type { AIProviderConfig } from "./types";
export function buildProviderRegistry(db: Database) {
const enabledProviders = getEnabledProviders(db);
const providers = getProviders(db);
const providerEntries: Record<string, ReturnType<typeof createProvider>> = {};
for (const p of enabledProviders) {
for (const p of providers) {
providerEntries[p.id] = createProvider({
apiKey: p.api_key,
baseUrl: p.base_url,
@@ -23,24 +23,105 @@ export function buildProviderRegistry(db: Database) {
return createProviderRegistry(providerEntries);
}
export async function testProviderConnection(config: AIProviderConfig): Promise<{ message: string; ok: boolean }> {
export async function testModelConnection(
config: AIProviderConfig & { modelId: string },
): Promise<{ message: string; ok: boolean }> {
try {
const provider = createProvider(config);
const model = provider.languageModel("test");
await generateText({
maxOutputTokens: 1,
model,
maxOutputTokens: 10,
model: provider.languageModel(config.modelId),
prompt: "Hi",
});
return { message: "连接成功", ok: true };
return { message: "模型连接成功", ok: true };
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return { message: `连接失败: ${msg}`, ok: false };
return { message: `模型连接失败${msg}`, ok: false };
}
}
export async function testProviderConnection(config: AIProviderConfig): Promise<{ message: string; ok: boolean }> {
const baseUrlResult = await probeBaseUrl(config.baseUrl);
if (!baseUrlResult.ok) return baseUrlResult;
const modelsUrl = buildModelsUrl(config.baseUrl);
try {
const response = await fetch(modelsUrl, {
headers: buildModelsHeaders(config),
signal: AbortSignal.timeout(5000),
});
if (response.status === 401 || response.status === 403) {
return { message: "Base URL 可连接,但 API Key 无效或权限不足。", ok: false };
}
if ([404, 405, 501].includes(response.status)) {
return {
message: "Base URL 可连接,但可能不支持 /models 接口;可检查 URL 或忽略此提示。",
ok: true,
};
}
if (!response.ok) {
return {
message: `Base URL 可连接,但 /models 请求失败HTTP ${response.status});可检查 URL 或忽略此提示。`,
ok: true,
};
}
const body = (await response.json().catch(() => null)) as unknown;
const modelCount = countModels(body);
if (modelCount !== null) {
return { message: `连接成功,/models 返回 ${modelCount} 个模型。`, ok: true };
}
return {
message: "Base URL 可连接,但 /models 返回格式不兼容,可能不支持 /models可检查 URL 或忽略此提示。",
ok: true,
};
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return { message: `Base URL 可连接,但 /models 请求异常:${msg};可检查 URL 或忽略此提示。`, ok: true };
}
}
function buildModelsHeaders(config: AIProviderConfig): HeadersInit {
if (config.type === "anthropic") {
return {
accept: "application/json",
"anthropic-version": "2023-06-01",
"x-api-key": config.apiKey,
};
}
return {
accept: "application/json",
authorization: `Bearer ${config.apiKey}`,
};
}
function buildModelsUrl(baseUrl: string): string {
const url = new URL(baseUrl);
url.pathname = `${url.pathname.replace(/\/$/, "")}/models`;
url.search = "";
url.hash = "";
return url.toString();
}
function countModels(body: unknown): null | number {
if (Array.isArray(body)) return body.length;
if (!body || typeof body !== "object") return null;
const data = (body as { data?: unknown }).data;
if (Array.isArray(data)) return data.length;
const models = (body as { models?: unknown }).models;
if (Array.isArray(models)) return models.length;
return null;
}
function createProvider(config: AIProviderConfig) {
switch (config.type) {
case "anthropic":
@@ -56,14 +137,14 @@ function createProvider(config: AIProviderConfig) {
}
}
function getEnabledProviders(db: Database): Array<{
function getProviders(db: Database): Array<{
api_key: string;
base_url: string;
id: string;
name: string;
type: "anthropic" | "openai" | "openai-compatible";
}> {
const stmt = db.prepare("SELECT id, name, type, base_url, api_key FROM providers WHERE enabled = 1");
const stmt = db.prepare("SELECT id, name, type, base_url, api_key FROM providers");
return stmt.all() as Array<{
api_key: string;
base_url: string;
@@ -72,3 +153,16 @@ function getEnabledProviders(db: Database): Array<{
type: "anthropic" | "openai" | "openai-compatible";
}>;
}
async function probeBaseUrl(baseUrl: string): Promise<{ message: string; ok: boolean }> {
try {
await fetch(baseUrl, {
method: "HEAD",
signal: AbortSignal.timeout(5000),
});
return { message: "Base URL 可连接", ok: true };
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return { message: `Base URL 不可达:${msg}`, ok: false };
}
}

View File

@@ -36,7 +36,6 @@ export function createModel(
capabilities: JSON.stringify(capabilities),
contextLength: request.contextLength ?? null,
createdAt: now,
enabled: true,
id,
maxOutputTokens: request.maxOutputTokens ?? null,
modelId,
@@ -66,32 +65,6 @@ export function deleteModel(raw: Database, id: string): { error: string; status:
return { success: true };
}
export function disableModel(raw: Database, id: string): { error: string; status: number } | { model: Model } {
const db = wrap(raw);
const existing = db.select().from(models).where(eq(models.id, id)).get();
if (!existing) return { error: "模型不存在", status: 404 };
if (!existing.enabled) return { error: "模型已禁用", status: 409 };
const now = new Date().toISOString();
db.update(models).set({ enabled: false, updatedAt: now }).where(eq(models.id, id)).run();
const updated = db.select().from(models).where(eq(models.id, id)).get();
return { model: toModel(updated!) };
}
export function enableModel(raw: Database, id: string): { error: string; status: number } | { model: Model } {
const db = wrap(raw);
const existing = db.select().from(models).where(eq(models.id, id)).get();
if (!existing) return { error: "模型不存在", status: 404 };
if (existing.enabled) return { error: "模型已启用", status: 409 };
const now = new Date().toISOString();
db.update(models).set({ enabled: true, updatedAt: now }).where(eq(models.id, id)).run();
const updated = db.select().from(models).where(eq(models.id, id)).get();
return { model: toModel(updated!) };
}
export function getModel(raw: Database, id: string): { error: string; status: number } | { model: Model } {
const db = wrap(raw);
const row = db.select().from(models).where(eq(models.id, id)).get();
@@ -222,7 +195,6 @@ function toModel(row: typeof models.$inferSelect): Model {
capabilities: JSON.parse(row.capabilities) as ModelCapability[],
contextLength: row.contextLength,
createdAt: row.createdAt,
enabled: row.enabled,
id: row.id,
maxOutputTokens: row.maxOutputTokens,
modelId: row.modelId,

View File

@@ -3,7 +3,7 @@ import type Database from "bun:sqlite";
import { and, desc, eq, like, sql } from "drizzle-orm";
import { drizzle } from "drizzle-orm/bun-sqlite";
import type { CreateProviderRequest, Provider, UpdateProviderRequest } from "../../shared/api";
import type { CreateProviderRequest, Provider, ProviderOption, UpdateProviderRequest } from "../../shared/api";
import { providers } from "./schema";
@@ -30,7 +30,6 @@ export function createProvider(
apiKey,
baseUrl,
createdAt: now,
enabled: true,
id,
name,
type: request.type,
@@ -58,32 +57,6 @@ export function deleteProvider(raw: Database, id: string): { error: string; stat
return { success: true };
}
export function disableProvider(raw: Database, id: string): { error: string; status: number } | { provider: Provider } {
const db = wrap(raw);
const existing = db.select().from(providers).where(eq(providers.id, id)).get();
if (!existing) return { error: "供应商不存在", status: 404 };
if (!existing.enabled) return { error: "供应商已禁用", status: 409 };
const now = new Date().toISOString();
db.update(providers).set({ enabled: false, updatedAt: now }).where(eq(providers.id, id)).run();
const updated = db.select().from(providers).where(eq(providers.id, id)).get();
return { provider: toProvider(updated!) };
}
export function enableProvider(raw: Database, id: string): { error: string; status: number } | { provider: Provider } {
const db = wrap(raw);
const existing = db.select().from(providers).where(eq(providers.id, id)).get();
if (!existing) return { error: "供应商不存在", status: 404 };
if (existing.enabled) return { error: "供应商已启用", status: 409 };
const now = new Date().toISOString();
db.update(providers).set({ enabled: true, updatedAt: now }).where(eq(providers.id, id)).run();
const updated = db.select().from(providers).where(eq(providers.id, id)).get();
return { provider: toProvider(updated!) };
}
export function getProvider(raw: Database, id: string): { error: string; status: number } | { provider: Provider } {
const db = wrap(raw);
const row = db.select().from(providers).where(eq(providers.id, id)).get();
@@ -92,6 +65,17 @@ export function getProvider(raw: Database, id: string): { error: string; status:
return { provider: toProvider(row) };
}
export function listProviderOptions(raw: Database): ProviderOption[] {
const db = wrap(raw);
const rows = db
.select({ id: providers.id, name: providers.name, type: providers.type })
.from(providers)
.orderBy(desc(providers.createdAt))
.all();
return rows;
}
export function listProviders(
raw: Database,
options: { keyword?: string; page: number; pageSize: number },
@@ -189,7 +173,6 @@ function toProvider(row: typeof providers.$inferSelect): Provider {
apiKey: row.apiKey,
baseUrl: row.baseUrl,
createdAt: row.createdAt,
enabled: row.enabled,
id: row.id,
name: row.name,
type: row.type,

View File

@@ -16,7 +16,6 @@ export const providers = sqliteTable("providers", {
apiKey: text("api_key").notNull(),
baseUrl: text("base_url").notNull(),
createdAt: text("created_at").notNull(),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
id: text("id").primaryKey(),
name: text("name").notNull().unique(),
type: text("type", { enum: ["anthropic", "openai", "openai-compatible"] })
@@ -31,7 +30,6 @@ export const models = sqliteTable(
capabilities: text("capabilities").notNull(),
contextLength: integer("context_length"),
createdAt: text("created_at").notNull(),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
id: text("id").primaryKey(),
maxOutputTokens: integer("max_output_tokens"),
modelId: text("model_id").notNull(),

View File

@@ -38,6 +38,12 @@ export async function handleCreateModel(req: Request, db: Database, mode: Runtim
return jsonResponse(createApiError(`Invalid capabilities: ${invalidCaps.join(", ")}`, 400), { mode, status: 400 });
}
const numberError = validateOptionalPositiveInteger("contextLength", body.contextLength);
if (numberError) return jsonResponse(createApiError(numberError, 400), { mode, status: 400 });
const tokenError = validateOptionalPositiveInteger("maxOutputTokens", body.maxOutputTokens);
if (tokenError) return jsonResponse(createApiError(tokenError, 400), { mode, status: 400 });
const result = createModel(db, body);
if ("error" in result) {
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
@@ -45,3 +51,9 @@ export async function handleCreateModel(req: Request, db: Database, mode: Runtim
return jsonResponse(result, { mode, status: 201 });
}
function validateOptionalPositiveInteger(field: string, value: null | number | undefined): null | string {
if (value === undefined || value === null) return null;
if (!Number.isInteger(value) || value <= 0) return `${field} must be a positive integer`;
return null;
}

View File

@@ -1,22 +0,0 @@
import type Database from "bun:sqlite";
import type { RuntimeMode } from "../../../shared/api";
import { disableModel } from "../../db/models";
import { createApiError, jsonResponse } from "../../helpers";
import { validateIdParam } from "../../middleware";
export function handleDisableModel(req: Request, db: Database, mode: RuntimeMode): Response {
const url = new URL(req.url);
const idStr = url.pathname.split("/")[3];
const validated = validateIdParam(idStr ?? "", mode);
if (validated instanceof Response) return validated;
const result = disableModel(db, validated.id);
if ("error" in result) {
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
}
return jsonResponse(result, { mode });
}

View File

@@ -1,22 +0,0 @@
import type Database from "bun:sqlite";
import type { RuntimeMode } from "../../../shared/api";
import { enableModel } from "../../db/models";
import { createApiError, jsonResponse } from "../../helpers";
import { validateIdParam } from "../../middleware";
export function handleEnableModel(req: Request, db: Database, mode: RuntimeMode): Response {
const url = new URL(req.url);
const idStr = url.pathname.split("/")[3];
const validated = validateIdParam(idStr ?? "", mode);
if (validated instanceof Response) return validated;
const result = enableModel(db, validated.id);
if ("error" in result) {
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
}
return jsonResponse(result, { mode });
}

View File

@@ -0,0 +1,42 @@
import type Database from "bun:sqlite";
import type { RuntimeMode, TestModelRequest } from "../../../shared/api";
import { testModelConnection } from "../../ai/registry";
import { getProvider } from "../../db/providers";
import { createApiError, jsonResponse } from "../../helpers";
export async function handleTestModelConfig(req: Request, db: Database, mode: RuntimeMode): Promise<Response> {
let body: TestModelRequest;
try {
body = (await req.json()) as TestModelRequest;
} catch {
return jsonResponse(createApiError("Invalid JSON body", 400), { mode, status: 400 });
}
if (!body.providerId || typeof body.providerId !== "string") {
return jsonResponse(createApiError("providerId is required", 400), { mode, status: 400 });
}
if (!body.modelId || typeof body.modelId !== "string") {
return jsonResponse(createApiError("modelId is required", 400), { mode, status: 400 });
}
const providerResult = getProvider(db, body.providerId);
if ("error" in providerResult) {
return jsonResponse(createApiError(providerResult.error, providerResult.status), {
mode,
status: providerResult.status,
});
}
const testResult = await testModelConnection({
apiKey: providerResult.provider.apiKey,
baseUrl: providerResult.provider.baseUrl,
modelId: body.modelId,
name: providerResult.provider.name,
type: providerResult.provider.type,
});
return jsonResponse({ modelTestResponse: testResult }, { mode });
}

View File

@@ -34,6 +34,12 @@ export async function handleUpdateModel(req: Request, db: Database, mode: Runtim
}
}
const numberError = validateOptionalPositiveInteger("contextLength", body.contextLength);
if (numberError) return jsonResponse(createApiError(numberError, 400), { mode, status: 400 });
const tokenError = validateOptionalPositiveInteger("maxOutputTokens", body.maxOutputTokens);
if (tokenError) return jsonResponse(createApiError(tokenError, 400), { mode, status: 400 });
const result = updateModel(db, validated.id, body);
if ("error" in result) {
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
@@ -41,3 +47,9 @@ export async function handleUpdateModel(req: Request, db: Database, mode: Runtim
return jsonResponse(result, { mode });
}
function validateOptionalPositiveInteger(field: string, value: null | number | undefined): null | string {
if (value === undefined || value === null) return null;
if (!Number.isInteger(value) || value <= 0) return `${field} must be a positive integer`;
return null;
}

View File

@@ -1,22 +0,0 @@
import type Database from "bun:sqlite";
import type { RuntimeMode } from "../../../shared/api";
import { disableProvider } from "../../db/providers";
import { createApiError, jsonResponse } from "../../helpers";
import { validateIdParam } from "../../middleware";
export function handleDisableProvider(req: Request, db: Database, mode: RuntimeMode): Response {
const url = new URL(req.url);
const idStr = url.pathname.split("/")[3];
const validated = validateIdParam(idStr ?? "", mode);
if (validated instanceof Response) return validated;
const result = disableProvider(db, validated.id);
if ("error" in result) {
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
}
return jsonResponse(result, { mode });
}

View File

@@ -1,22 +0,0 @@
import type Database from "bun:sqlite";
import type { RuntimeMode } from "../../../shared/api";
import { enableProvider } from "../../db/providers";
import { createApiError, jsonResponse } from "../../helpers";
import { validateIdParam } from "../../middleware";
export function handleEnableProvider(req: Request, db: Database, mode: RuntimeMode): Response {
const url = new URL(req.url);
const idStr = url.pathname.split("/")[3];
const validated = validateIdParam(idStr ?? "", mode);
if (validated instanceof Response) return validated;
const result = enableProvider(db, validated.id);
if ("error" in result) {
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
}
return jsonResponse(result, { mode });
}

View File

@@ -0,0 +1,10 @@
import type Database from "bun:sqlite";
import type { RuntimeMode } from "../../../shared/api";
import { listProviderOptions } from "../../db/providers";
import { jsonResponse } from "../../helpers";
export function handleListProviderOptions(db: Database, mode: RuntimeMode): Response {
return jsonResponse({ items: listProviderOptions(db) }, { mode });
}

View File

@@ -3,35 +3,7 @@ import type Database from "bun:sqlite";
import type { CreateProviderRequest, RuntimeMode } from "../../../shared/api";
import { testProviderConnection } from "../../ai/registry";
import { getProvider } from "../../db/providers";
import { createApiError, jsonResponse } from "../../helpers";
import { validateIdParam } from "../../middleware";
export async function handleTestProvider(req: Request, db: Database, mode: RuntimeMode): Promise<Response> {
const url = new URL(req.url);
const idStr = url.pathname.split("/")[3];
const validated = validateIdParam(idStr ?? "", mode);
if (validated instanceof Response) return validated;
const providerResult = getProvider(db, validated.id);
if ("error" in providerResult) {
return jsonResponse(createApiError(providerResult.error, providerResult.status), {
mode,
status: providerResult.status,
});
}
const provider = providerResult.provider;
const testResult = await testProviderConnection({
apiKey: provider.apiKey,
baseUrl: provider.baseUrl,
name: provider.name,
type: provider.type,
});
return jsonResponse({ providerTestResponse: testResult }, { mode });
}
export async function handleTestProviderConfig(req: Request, _db: Database, mode: RuntimeMode): Promise<Response> {
const validated = await readProviderConfig(req, mode);

View File

@@ -67,16 +67,10 @@ export function startServer(options: StartServerOptions) {
return handleUpdateModel(req, db, mode);
},
},
"/api/models/:id/disable": {
"/api/models/test": {
POST: async (req) => {
const { handleDisableModel } = await import("./routes/models/disable");
return handleDisableModel(req, db, mode);
},
},
"/api/models/:id/enable": {
POST: async (req) => {
const { handleEnableModel } = await import("./routes/models/enable");
return handleEnableModel(req, db, mode);
const { handleTestModelConfig } = await import("./routes/models/test");
return handleTestModelConfig(req, db, mode);
},
},
"/api/projects": {
@@ -139,22 +133,10 @@ export function startServer(options: StartServerOptions) {
return handleUpdateProvider(req, db, mode);
},
},
"/api/providers/:id/disable": {
POST: async (req) => {
const { handleDisableProvider } = await import("./routes/providers/disable");
return handleDisableProvider(req, db, mode);
},
},
"/api/providers/:id/enable": {
POST: async (req) => {
const { handleEnableProvider } = await import("./routes/providers/enable");
return handleEnableProvider(req, db, mode);
},
},
"/api/providers/:id/test": {
POST: async (req) => {
const { handleTestProvider } = await import("./routes/providers/test");
return handleTestProvider(req, db, mode);
"/api/providers/options": {
GET: async () => {
const { handleListProviderOptions } = await import("./routes/providers/options");
return handleListProviderOptions(db, mode);
},
},
"/api/providers/test": {

View File

@@ -40,7 +40,6 @@ export interface Model {
capabilities: ModelCapability[];
contextLength: null | number;
createdAt: string;
enabled: boolean;
id: string;
maxOutputTokens: null | number;
modelId: string;
@@ -81,6 +80,15 @@ export interface ModelResponse {
model: Model;
}
export interface ModelTestResponse {
message: string;
ok: boolean;
}
export interface ModelTestResultResponse {
modelTestResponse: ModelTestResponse;
}
export interface Project {
archivedAt: null | string;
createdAt: string;
@@ -108,7 +116,6 @@ export interface Provider {
apiKey: string;
baseUrl: string;
createdAt: string;
enabled: boolean;
id: string;
name: string;
type: ProviderType;
@@ -122,6 +129,16 @@ export interface ProviderListResponse {
total: number;
}
export interface ProviderOption {
id: string;
name: string;
type: ProviderType;
}
export interface ProviderOptionsResponse {
items: ProviderOption[];
}
export interface ProviderResponse {
provider: Provider;
}
@@ -139,6 +156,11 @@ export type ProviderType = "anthropic" | "openai" | "openai-compatible";
export type RuntimeMode = "development" | "production" | "test";
export interface TestModelRequest {
modelId: string;
providerId: string;
}
export interface UpdateModelRequest {
capabilities?: ModelCapability[];
contextLength?: null | number;

View File

@@ -1,6 +1,15 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { CreateModelRequest, Model, ModelListResponse, ModelResponse, UpdateModelRequest } from "../../shared/api";
import type {
CreateModelRequest,
Model,
ModelListResponse,
ModelResponse,
ModelTestResponse,
ModelTestResultResponse,
TestModelRequest,
UpdateModelRequest,
} from "../../shared/api";
const MODELS_KEY = ["models"] as const;
@@ -21,16 +30,6 @@ export async function deleteModel(id: string): Promise<void> {
}
}
export async function disableModel(id: string): Promise<Model> {
const response = await fetch(`/api/models/${id}/disable`, { method: "POST" });
return handleResponse(response);
}
export async function enableModel(id: string): Promise<Model> {
const response = await fetch(`/api/models/${id}/enable`, { method: "POST" });
return handleResponse(response);
}
export async function fetchModel(id: string): Promise<Model> {
const response = await fetch(`/api/models/${id}`);
return handleResponse(response);
@@ -57,6 +56,20 @@ export async function fetchModelList(params: {
return response.json() as Promise<ModelListResponse>;
}
export async function testModelConnection(data: TestModelRequest): Promise<ModelTestResponse> {
const response = await fetch("/api/models/test", {
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
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 result = (await response.json()) as ModelTestResultResponse;
return result.modelTestResponse;
}
export async function updateModel(id: string, data: UpdateModelRequest): Promise<Model> {
const response = await fetch(`/api/models/${id}`, {
body: JSON.stringify(data),
@@ -86,26 +99,6 @@ export function useDeleteModel() {
});
}
export function useDisableModel() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: disableModel,
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: MODELS_KEY });
},
});
}
export function useEnableModel() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: enableModel,
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: MODELS_KEY });
},
});
}
export function useModel(id: string) {
return useQuery({
enabled: !!id,
@@ -121,6 +114,12 @@ export function useModelList(params: { keyword?: string; page?: number; pageSize
});
}
export function useTestModelConnection() {
return useMutation({
mutationFn: testModelConnection,
});
}
export function useUpdateModel() {
const queryClient = useQueryClient();
return useMutation({

View File

@@ -4,6 +4,7 @@ import type {
CreateProviderRequest,
Provider,
ProviderListResponse,
ProviderOptionsResponse,
ProviderResponse,
ProviderTestResponse,
ProviderTestResultResponse,
@@ -30,16 +31,6 @@ export async function deleteProvider(id: string): Promise<void> {
}
}
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);
@@ -64,6 +55,15 @@ export async function fetchProviderList(params: {
return response.json() as Promise<ProviderListResponse>;
}
export async function fetchProviderOptions(): Promise<ProviderOptionsResponse> {
const response = await fetch("/api/providers/options");
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<ProviderOptionsResponse>;
}
export async function testProviderConfig(data: CreateProviderRequest): Promise<ProviderTestResponse> {
const response = await fetch("/api/providers/test", {
body: JSON.stringify(data),
@@ -78,16 +78,6 @@ export async function testProviderConfig(data: CreateProviderRequest): Promise<P
return result.providerTestResponse;
}
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 ProviderTestResultResponse;
return data.providerTestResponse;
}
export async function updateProvider(id: string, data: UpdateProviderRequest): Promise<Provider> {
const response = await fetch(`/api/providers/${id}`, {
body: JSON.stringify(data),
@@ -118,26 +108,6 @@ export function useDeleteProvider() {
});
}
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,
@@ -153,15 +123,16 @@ export function useProviderList(params: { keyword?: string; page?: number; pageS
});
}
export function useTestProviderConfig() {
return useMutation({
mutationFn: testProviderConfig,
export function useProviderOptions() {
return useQuery({
queryFn: fetchProviderOptions,
queryKey: [...PROVIDERS_KEY, "options"],
});
}
export function useTestProviderConnection() {
export function useTestProviderConfig() {
return useMutation({
mutationFn: testProviderConnection,
mutationFn: testProviderConfig,
});
}

View File

@@ -5,8 +5,9 @@ import type {
CreateModelRequest,
Model,
ModelCapability,
Provider,
ProviderTestResponse,
ModelTestResponse,
ProviderOption,
TestModelRequest,
UpdateModelRequest,
} from "../../../../shared/api";
@@ -26,11 +27,15 @@ interface ModelFormModalProps {
onOpenChange: (open: boolean) => void;
onUpdate: (args: { data: UpdateModelRequest; id: string }) => Promise<unknown>;
open: boolean;
providers: Provider[];
providers: ProviderOption[];
providersError: Error | null;
providersLoading: boolean;
submitting: boolean;
testConnection?: (providerId: string) => Promise<ProviderTestResponse>;
testModelConnection?: (data: TestModelRequest) => Promise<ModelTestResponse>;
}
const DEFAULT_CAPABILITIES: ModelCapability[] = ["text", "reasoning"];
const CAPABILITY_OPTIONS: Array<{ label: string; value: ModelCapability }> = [
{ label: "文本", value: "text" },
{ label: "推理", value: "reasoning" },
@@ -50,8 +55,10 @@ export function ModelFormModal({
onUpdate,
open,
providers,
providersError,
providersLoading,
submitting,
testConnection,
testModelConnection,
}: ModelFormModalProps) {
const { message } = AntApp.useApp();
const [form] = Form.useForm<FormValues>();
@@ -70,6 +77,7 @@ export function ModelFormModal({
});
} else {
form.resetFields();
form.setFieldsValue({ capabilities: DEFAULT_CAPABILITIES });
}
}, [editingModel, form, open]);
@@ -109,15 +117,20 @@ export function ModelFormModal({
};
const handleTest = async () => {
if (!testConnection) return;
if (!testModelConnection) return;
const providerId: unknown = form.getFieldValue("providerId");
const modelId: unknown = form.getFieldValue("modelId");
if (typeof providerId !== "string" || !providerId) {
message.warning("请先选择供应商");
return;
}
if (typeof modelId !== "string" || !modelId) {
message.warning("请先输入模型 ID");
return;
}
setTesting(true);
try {
const result = await testConnection(providerId);
const result = await testModelConnection({ modelId, providerId });
if (result.ok) {
message.success(result.message);
} else {
@@ -130,7 +143,7 @@ export function ModelFormModal({
}
};
const providerOptions = providers.filter((p) => p.enabled).map((p) => ({ label: p.name, value: p.id }));
const providerOptions = providers.map((p) => ({ label: p.name, value: p.id }));
return (
<Modal
@@ -152,7 +165,15 @@ export function ModelFormModal({
<Input placeholder="请输入模型名称" />
</Form.Item>
<Form.Item label="所属供应商" name="providerId" rules={[{ message: "请选择供应商", required: true }]}>
<Select options={providerOptions} placeholder="请选择供应商" />
<Select
loading={providersLoading}
notFoundContent={getProviderNotFoundContent(providersLoading, providersError)}
optionFilterProp="label"
options={providerOptions}
placeholder="请选择供应商"
showSearch
status={providersError ? "error" : undefined}
/>
</Form.Item>
<Form.Item
label="模型 ID"
@@ -163,22 +184,28 @@ export function ModelFormModal({
</Form.Item>
<Form.Item label="能力标签" name="capabilities" rules={[{ message: "请至少选择一个能力标签", required: true }]}>
<Checkbox.Group>
<Row>
<Row gutter={[8, 8]}>
{CAPABILITY_OPTIONS.map((opt) => (
<Col key={opt.value} span={12}>
<Col key={opt.value} md={8} sm={12} xs={24}>
<Checkbox value={opt.value}>{opt.label}</Checkbox>
</Col>
))}
</Row>
</Checkbox.Group>
</Form.Item>
<Form.Item label="上下文长度" name="contextLength">
<InputNumber placeholder="可选" />
</Form.Item>
<Form.Item label="最大输出 Token" name="maxOutputTokens">
<InputNumber placeholder="可选" />
</Form.Item>
{testConnection && (
<Row gutter={16}>
<Col sm={12} xs={24}>
<Form.Item label="上下文长度" name="contextLength" rules={[positiveIntegerRule("上下文长度")]}>
<InputNumber min={1} placeholder="可选" precision={0} styles={{ root: { width: "100%" } }} />
</Form.Item>
</Col>
<Col sm={12} xs={24}>
<Form.Item label="最大输出 Token" name="maxOutputTokens" rules={[positiveIntegerRule("最大输出 Token")]}>
<InputNumber min={1} placeholder="可选" precision={0} styles={{ root: { width: "100%" } }} />
</Form.Item>
</Col>
</Row>
{testModelConnection && (
<Form.Item>
<Space>
<Button loading={testing} onClick={() => void handleTest()}>
@@ -191,3 +218,19 @@ export function ModelFormModal({
</Modal>
);
}
function getProviderNotFoundContent(loading: boolean, error: Error | null): string {
if (loading) return "正在加载供应商";
if (error) return `供应商加载失败:${error.message}`;
return "暂无供应商,请先新建供应商";
}
function positiveIntegerRule(label: string) {
return {
validator(_: unknown, value: null | number | undefined) {
if (value === undefined || value === null) return Promise.resolve();
if (Number.isInteger(value) && value > 0) return Promise.resolve();
return Promise.reject(new Error(`${label}必须为正整数`));
},
};
}

View File

@@ -1,21 +1,19 @@
import type { ColumnsType } from "antd/es/table";
import { CheckCircleOutlined, DeleteOutlined, EditOutlined, StopOutlined } from "@ant-design/icons";
import { DeleteOutlined, EditOutlined } from "@ant-design/icons";
import { App as AntApp, Button, Popconfirm, Space, Table, Tag } from "antd";
import type { Model, ModelListResponse, Provider } from "../../../../shared/api";
import type { Model, ModelListResponse, ProviderOption } from "../../../../shared/api";
interface ModelTableProps {
data: ModelListResponse | undefined;
loading: boolean;
onDelete: (id: string) => Promise<unknown>;
onDisable: (id: string) => Promise<unknown>;
onEdit: (model: Model) => void;
onEnable: (id: string) => Promise<unknown>;
onPageChange: (page: number, pageSize: number) => void;
page: number;
pageSize: number;
providers: Provider[];
providers: ProviderOption[];
}
const CAPABILITY_LABELS: Record<string, string> = {
@@ -29,13 +27,12 @@ const CAPABILITY_LABELS: Record<string, string> = {
"video-recognition": "视频识别",
};
function getProviderName(providerId: string, providers: Provider[]): string {
function getProviderName(providerId: string, providers: ProviderOption[]): string {
return providers.find((p) => p.id === providerId)?.name ?? providerId;
}
const COLUMNS: ColumnsType<Model> = [
{ dataIndex: "name", ellipsis: true, title: "模型名称", width: 160 },
{ dataIndex: "modelId", ellipsis: true, title: "模型 ID", width: 180 },
{ dataIndex: "name", ellipsis: true, title: "名称", width: 180 },
{
dataIndex: "providerId",
ellipsis: true,
@@ -46,28 +43,13 @@ const COLUMNS: ColumnsType<Model> = [
dataIndex: "capabilities",
render: (value: string[]) =>
value.length > 0 ? (
<Space size={[0, 4]} wrap>
<Space size={[4, 4]} wrap>
{value.map((c) => (
<Tag key={c}>{CAPABILITY_LABELS[c] ?? c}</Tag>
))}
</Space>
) : null,
title: "能力",
width: 200,
},
{
align: "center",
dataIndex: "enabled",
render: (value: boolean) => (value ? <Tag color="blue"></Tag> : <Tag></Tag>),
title: "状态",
width: 100,
},
{
align: "center",
dataIndex: "createdAt",
render: (_value: unknown, record: Model) => formatDatetime(record.createdAt),
title: "创建时间",
width: 185,
},
];
@@ -75,9 +57,7 @@ export function ModelTable({
data,
loading,
onDelete,
onDisable,
onEdit,
onEnable,
onPageChange,
page,
pageSize,
@@ -85,24 +65,6 @@ export function ModelTable({
}: ModelTableProps) {
const { message } = AntApp.useApp();
const handleEnable = async (id: string) => {
try {
await onEnable(id);
message.success("模型已启用");
} catch (err) {
message.error((err as Error).message);
}
};
const handleDisable = async (id: string) => {
try {
await onDisable(id);
message.success("模型已禁用");
} catch (err) {
message.error((err as Error).message);
}
};
const handleDelete = async (id: string) => {
try {
await onDelete(id);
@@ -123,23 +85,11 @@ export function ModelTable({
const operationColumn: ColumnsType<Model>[number] = {
dataIndex: "op",
fixed: "right",
render: (_value: unknown, record: Model) => (
<Space size="small">
<Button icon={<EditOutlined />} onClick={() => onEdit(record)} size="small" type="link">
</Button>
{record.enabled ? (
<Popconfirm onConfirm={() => void handleDisable(record.id)} title="确认禁用此模型?">
<Button color="orange" icon={<StopOutlined />} size="small" variant="link">
</Button>
</Popconfirm>
) : (
<Button icon={<CheckCircleOutlined />} onClick={() => void handleEnable(record.id)} size="small" type="link">
</Button>
)}
<Popconfirm
description="此操作不可恢复。"
onConfirm={() => void handleDelete(record.id)}
@@ -152,7 +102,7 @@ export function ModelTable({
</Space>
),
title: "操作",
width: 220,
width: 180,
};
return (
@@ -169,13 +119,6 @@ export function ModelTable({
total: data?.total ?? 0,
}}
rowKey="id"
scroll={{ x: 1100 }}
/>
);
}
function formatDatetime(dateStr: string): string {
const d = new Date(dateStr);
const pad = (n: number) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}

View File

@@ -1,34 +0,0 @@
import { PlusOutlined } from "@ant-design/icons";
import { Button, Flex, Input } from "antd";
import { useState } from "react";
interface ModelToolbarProps {
keyword: string;
onSearch: (value: string) => void;
onSearchClear: () => void;
openCreateDialog: () => void;
}
export function ModelToolbar({ keyword, onSearch, onSearchClear, openCreateDialog }: ModelToolbarProps) {
const [draftKeyword, setDraftKeyword] = useState(keyword);
return (
<Flex align="center" gap="small" justify="space-between" wrap="wrap">
<Input.Search
allowClear
enterButton="搜索"
onChange={(event) => setDraftKeyword(event.target.value)}
onClear={() => {
setDraftKeyword("");
onSearchClear();
}}
onSearch={(value) => onSearch(value)}
placeholder="搜索模型名称或 ID"
value={draftKeyword}
/>
<Button icon={<PlusOutlined />} onClick={openCreateDialog} type="primary">
</Button>
</Flex>
);
}

View File

@@ -0,0 +1,53 @@
import { PlusOutlined } from "@ant-design/icons";
import { Button, Flex, Input, Tabs } from "antd";
import { useState } from "react";
interface ModelsToolbarProps {
activeTab: string;
keyword: string;
onSearch: (value: string) => void;
onSearchClear: () => void;
onTabChange: (key: string) => void;
openCreateDialog: () => void;
}
const TAB_ITEMS = [
{ key: "models", label: "模型" },
{ key: "providers", label: "供应商" },
];
export function ModelsToolbar({
activeTab,
keyword,
onSearch,
onSearchClear,
onTabChange,
openCreateDialog,
}: ModelsToolbarProps) {
const [draftKeyword, setDraftKeyword] = useState(keyword);
const placeholder = activeTab === "providers" ? "搜索供应商名称" : "搜索模型名称或 ID";
const createLabel = activeTab === "providers" ? "新建供应商" : "新建模型";
return (
<Flex align="center" gap="var(--ant-margin-lg)" justify="space-between" wrap="wrap">
<Tabs activeKey={activeTab} items={TAB_ITEMS} onChange={onTabChange} />
<Flex align="center" gap="small">
<Input.Search
allowClear
enterButton="搜索"
onChange={(event) => setDraftKeyword(event.target.value)}
onClear={() => {
setDraftKeyword("");
onSearchClear();
}}
onSearch={(value) => onSearch(value)}
placeholder={placeholder}
value={draftKeyword}
/>
<Button icon={<PlusOutlined />} onClick={openCreateDialog} type="primary">
{createLabel}
</Button>
</Flex>
</Flex>
);
}

View File

@@ -1,25 +1,16 @@
import type { ColumnsType } from "antd/es/table";
import {
CheckCircleOutlined,
DeleteOutlined,
EditOutlined,
StopOutlined,
ThunderboltOutlined,
} from "@ant-design/icons";
import { App as AntApp, Button, Popconfirm, Space, Table, Tag, Tooltip } from "antd";
import { DeleteOutlined, EditOutlined } from "@ant-design/icons";
import { App as AntApp, Button, Popconfirm, Space, Table } from "antd";
import type { Provider, ProviderListResponse, ProviderTestResponse } from "../../../../shared/api";
import type { Provider, ProviderListResponse } from "../../../../shared/api";
interface ProviderTableProps {
data: ProviderListResponse | undefined;
loading: boolean;
onDelete: (id: string) => Promise<unknown>;
onDisable: (id: string) => Promise<unknown>;
onEdit: (provider: Provider) => void;
onEnable: (id: string) => Promise<unknown>;
onPageChange: (page: number, pageSize: number) => void;
onTest: (id: string) => Promise<ProviderTestResponse>;
page: number;
pageSize: number;
}
@@ -31,63 +22,19 @@ const TYPE_LABELS: Record<Provider["type"], string> = {
};
const COLUMNS: ColumnsType<Provider> = [
{ dataIndex: "name", ellipsis: true, title: "供应商名称", width: 160 },
{ dataIndex: "name", ellipsis: true, title: "名称", width: 180 },
{
align: "center",
dataIndex: "type",
render: (value: Provider["type"]) => TYPE_LABELS[value] ?? value,
title: "类型",
width: 130,
width: 140,
},
{ dataIndex: "baseUrl", ellipsis: true, title: "Base URL" },
{
align: "center",
dataIndex: "enabled",
render: (value: boolean) => (value ? <Tag color="blue"></Tag> : <Tag></Tag>),
title: "状态",
width: 100,
},
{
align: "center",
dataIndex: "createdAt",
render: (_value: unknown, record: Provider) => formatDatetime(record.createdAt),
title: "创建时间",
width: 185,
},
];
export function ProviderTable({
data,
loading,
onDelete,
onDisable,
onEdit,
onEnable,
onPageChange,
onTest,
page,
pageSize,
}: ProviderTableProps) {
export function ProviderTable({ data, loading, onDelete, onEdit, onPageChange, page, pageSize }: ProviderTableProps) {
const { message } = AntApp.useApp();
const handleEnable = async (id: string) => {
try {
await onEnable(id);
message.success("供应商已启用");
} catch (err) {
message.error((err as Error).message);
}
};
const handleDisable = async (id: string) => {
try {
await onDisable(id);
message.success("供应商已禁用");
} catch (err) {
message.error((err as Error).message);
}
};
const handleDelete = async (id: string) => {
try {
await onDelete(id);
@@ -97,49 +44,15 @@ export function ProviderTable({
}
};
const handleTest = async (id: string) => {
try {
const result = await onTest(id);
if (result.ok) {
message.success(result.message);
} else {
message.error(result.message);
}
} catch (err) {
message.error((err as Error).message);
}
};
const operationColumn: ColumnsType<Provider>[number] = {
dataIndex: "op",
fixed: "right",
render: (_value: unknown, record: Provider) => (
<Space size="small">
<Tooltip title="测试连接">
<Button
aria-label="测试连接"
icon={<ThunderboltOutlined />}
onClick={() => void handleTest(record.id)}
size="small"
type="link"
/>
</Tooltip>
<Button icon={<EditOutlined />} onClick={() => onEdit(record)} size="small" type="link">
</Button>
{record.enabled ? (
<Popconfirm onConfirm={() => void handleDisable(record.id)} title="确认禁用此供应商?">
<Button color="orange" icon={<StopOutlined />} size="small" variant="link">
</Button>
</Popconfirm>
) : (
<Button icon={<CheckCircleOutlined />} onClick={() => void handleEnable(record.id)} size="small" type="link">
</Button>
)}
<Popconfirm
description="该供应商下存在模型时无法删除。"
description="该供应商下存在模型时无法删除,请先删除或迁移相关模型。"
onConfirm={() => void handleDelete(record.id)}
title="确认删除此供应商?"
>
@@ -150,7 +63,7 @@ export function ProviderTable({
</Space>
),
title: "操作",
width: 280,
width: 180,
};
return (
@@ -167,13 +80,6 @@ export function ProviderTable({
total: data?.total ?? 0,
}}
rowKey="id"
scroll={{ x: 900 }}
/>
);
}
function formatDatetime(dateStr: string): string {
const d = new Date(dateStr);
const pad = (n: number) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}

View File

@@ -1,34 +0,0 @@
import { PlusOutlined } from "@ant-design/icons";
import { Button, Flex, Input } from "antd";
import { useState } from "react";
interface ProviderToolbarProps {
keyword: string;
onSearch: (value: string) => void;
onSearchClear: () => void;
openCreateDialog: () => void;
}
export function ProviderToolbar({ keyword, onSearch, onSearchClear, openCreateDialog }: ProviderToolbarProps) {
const [draftKeyword, setDraftKeyword] = useState(keyword);
return (
<Flex align="center" gap="small" justify="space-between" wrap="wrap">
<Input.Search
allowClear
enterButton="搜索"
onChange={(event) => setDraftKeyword(event.target.value)}
onClear={() => {
setDraftKeyword("");
onSearchClear();
}}
onSearch={(value) => onSearch(value)}
placeholder="搜索供应商名称"
value={draftKeyword}
/>
<Button icon={<PlusOutlined />} onClick={openCreateDialog} type="primary">
</Button>
</Flex>
);
}

View File

@@ -1,35 +1,31 @@
import { Flex, Tabs } from "antd";
import { Flex } from "antd";
import { useState } from "react";
import type { Model, Provider } from "../../../shared/api";
import type { Model, Provider, TestModelRequest } from "../../../shared/api";
import {
useCreateModel,
useDeleteModel,
useDisableModel,
useEnableModel,
useModelList,
useTestModelConnection,
useUpdateModel,
} from "../../hooks/use-models";
import {
useCreateProvider,
useDeleteProvider,
useDisableProvider,
useEnableProvider,
useProviderList,
useProviderOptions,
useTestProviderConfig,
useTestProviderConnection,
useUpdateProvider,
} from "../../hooks/use-providers";
import { ModelFormModal } from "./components/ModelFormModal";
import { ModelsToolbar } from "./components/ModelsToolbar";
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 [activeTab, setActiveTab] = useState<string>("models");
const [providerPage, setProviderPage] = useState(1);
const [providerPageSize, setProviderPageSize] = useState(20);
@@ -49,10 +45,12 @@ export function ModelsPage() {
pageSize: providerPageSize,
});
const { data: modelProviderData, isLoading: modelProviderLoading } = useProviderList({
page: 1,
pageSize: 1000,
});
const {
data: providerOptionsData,
error: providerOptionsError,
isError: providerOptionsIsError,
isLoading: providerOptionsLoading,
} = useProviderOptions();
const { data: modelData, isLoading: modelLoading } = useModelList({
keyword: modelKeyword || undefined,
@@ -63,69 +61,81 @@ export function ModelsPage() {
const createProviderMutation = useCreateProvider();
const updateProviderMutation = useUpdateProvider();
const deleteProviderMutation = useDeleteProvider();
const enableProviderMutation = useEnableProvider();
const disableProviderMutation = useDisableProvider();
const testProviderMutation = useTestProviderConnection();
const testProviderConfigMutation = useTestProviderConfig();
const createModelMutation = useCreateModel();
const updateModelMutation = useUpdateModel();
const deleteModelMutation = useDeleteModel();
const enableModelMutation = useEnableModel();
const disableModelMutation = useDisableModel();
const testModelMutation = useTestModelConnection();
const isProviderSubmitting = createProviderMutation.isPending || updateProviderMutation.isPending;
const isProviderActionPending =
deleteProviderMutation.isPending || enableProviderMutation.isPending || disableProviderMutation.isPending;
const isProviderActionPending = deleteProviderMutation.isPending;
const isModelSubmitting = createModelMutation.isPending || updateModelMutation.isPending;
const isModelActionPending =
deleteModelMutation.isPending || enableModelMutation.isPending || disableModelMutation.isPending;
const modelProviders = modelProviderData?.items ?? [];
const isModelActionPending = deleteModelMutation.isPending;
const modelProviders = providerOptionsData?.items ?? [];
const currentKeyword = activeTab === "providers" ? providerKeyword : modelKeyword;
const handleSearch =
activeTab === "providers"
? (value: string) => {
setProviderKeyword(value);
setProviderPage(1);
}
: (value: string) => {
setModelKeyword(value);
setModelPage(1);
};
const handleSearchClear =
activeTab === "providers"
? () => {
setProviderKeyword("");
setProviderPage(1);
}
: () => {
setModelKeyword("");
setModelPage(1);
};
const handleOpenCreate =
activeTab === "providers"
? () => {
setEditingProvider(null);
setProviderDialogOpen(true);
}
: () => {
setEditingModel(null);
setModelDialogOpen(true);
};
return (
<Flex flex={1} gap="var(--ant-margin-lg)" vertical>
<Tabs
activeKey={activeTab}
items={[
{ key: "providers", label: "供应商" },
{ key: "models", label: "模型" },
]}
onChange={(key) => setActiveTab(key)}
<ModelsToolbar
activeTab={activeTab}
key={activeTab}
keyword={currentKeyword}
onSearch={handleSearch}
onSearchClear={handleSearchClear}
onTabChange={(key) => setActiveTab(key)}
openCreateDialog={handleOpenCreate}
/>
{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}
/>
@@ -137,38 +147,21 @@ export function ModelsPage() {
onTest={(data) => testProviderConfigMutation.mutateAsync(data)}
onUpdate={(args) => updateProviderMutation.mutateAsync(args)}
open={providerDialogOpen}
submitting={isProviderSubmitting || testProviderConfigMutation.isPending}
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 || modelProviderLoading || isModelActionPending}
loading={modelLoading || providerOptionsLoading || 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);
@@ -185,8 +178,10 @@ export function ModelsPage() {
onUpdate={(args) => updateModelMutation.mutateAsync(args)}
open={modelDialogOpen}
providers={modelProviders}
providersError={providerOptionsIsError ? providerOptionsError : null}
providersLoading={providerOptionsLoading}
submitting={isModelSubmitting}
testConnection={(id: string) => testProviderMutation.mutateAsync(id)}
testModelConnection={(data: TestModelRequest) => testModelMutation.mutateAsync(data)}
/>
</>
)}