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:
@@ -2,8 +2,6 @@ import { describe, expect, mock, test } from "bun:test";
|
||||
|
||||
import { createMigratedTestDatabase } from "../../helpers";
|
||||
|
||||
let generateTextImpl: (_opts: unknown) => unknown = () => ({});
|
||||
|
||||
void mock.module("ai", () => ({
|
||||
createProviderRegistry: (providers: Record<string, { languageModel: (modelId: string) => unknown }>) => ({
|
||||
languageModel: (id: string) => {
|
||||
@@ -13,70 +11,116 @@ void mock.module("ai", () => ({
|
||||
return provider.languageModel(modelId);
|
||||
},
|
||||
}),
|
||||
generateText: mock((opts: unknown) => generateTextImpl(opts)),
|
||||
generateText: () => Promise.resolve({ text: "Hi" }),
|
||||
}));
|
||||
|
||||
describe("AI registry", () => {
|
||||
test("testProviderConnection rejects invalid config", async () => {
|
||||
generateTextImpl = () => {
|
||||
throw new Error("Connection failed");
|
||||
};
|
||||
async function withProviderServer(
|
||||
modelsResponse: Response,
|
||||
callback: (baseUrl: string) => Promise<void>,
|
||||
): Promise<void> {
|
||||
const server = Bun.serve({
|
||||
fetch(request) {
|
||||
if (request.method === "HEAD") return new Response(null, { status: 200 });
|
||||
return modelsResponse;
|
||||
},
|
||||
port: 0,
|
||||
});
|
||||
try {
|
||||
await callback(`http://127.0.0.1:${server.port}/v1`);
|
||||
} finally {
|
||||
await server.stop(true);
|
||||
}
|
||||
}
|
||||
|
||||
describe("AI registry", () => {
|
||||
test("testProviderConnection reports unreachable Base URL", async () => {
|
||||
const { testProviderConnection } = await import("../../../src/server/ai/registry");
|
||||
|
||||
const result = await testProviderConnection({
|
||||
apiKey: "bad-key",
|
||||
baseUrl: "https://0.0.0.0:1",
|
||||
baseUrl: "http://127.0.0.1:1",
|
||||
name: "Bad",
|
||||
type: "openai-compatible",
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.message).toContain("连接失败");
|
||||
expect(typeof result.message).toBe("string");
|
||||
expect(result.message).toContain("Base URL 不可达");
|
||||
});
|
||||
|
||||
test("testProviderConnection rejects invalid config", async () => {
|
||||
await withProviderServer(new Response(null, { status: 401 }), async (baseUrl) => {
|
||||
const { testProviderConnection } = await import("../../../src/server/ai/registry");
|
||||
|
||||
const result = await testProviderConnection({
|
||||
apiKey: "bad-key",
|
||||
baseUrl,
|
||||
name: "Bad",
|
||||
type: "openai-compatible",
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.message).toContain("API Key 无效");
|
||||
expect(typeof result.message).toBe("string");
|
||||
});
|
||||
});
|
||||
|
||||
test("testProviderConnection return shape is correct", async () => {
|
||||
generateTextImpl = () => ({});
|
||||
await withProviderServer(Response.json({ data: [{ id: "gpt-4o" }] }), async (baseUrl) => {
|
||||
const { testProviderConnection } = await import("../../../src/server/ai/registry");
|
||||
|
||||
const { testProviderConnection } = await import("../../../src/server/ai/registry");
|
||||
const result = await testProviderConnection({
|
||||
apiKey: "sk-test",
|
||||
baseUrl,
|
||||
name: "Test",
|
||||
type: "openai",
|
||||
});
|
||||
|
||||
const result = await testProviderConnection({
|
||||
apiKey: "sk-test",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
name: "Test",
|
||||
type: "openai",
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.message).toContain("/models 返回 1 个模型");
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.message).toBe("连接成功");
|
||||
});
|
||||
|
||||
test("buildProviderRegistry 从 DB 构建包含启用供应商的注册表", async () => {
|
||||
test("testProviderConnection treats unsupported /models as non-blocking", async () => {
|
||||
await withProviderServer(new Response(null, { status: 404 }), async (baseUrl) => {
|
||||
const { testProviderConnection } = await import("../../../src/server/ai/registry");
|
||||
|
||||
const result = await testProviderConnection({
|
||||
apiKey: "sk-test",
|
||||
baseUrl,
|
||||
name: "Test",
|
||||
type: "openai",
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.message).toContain("可能不支持 /models");
|
||||
});
|
||||
});
|
||||
|
||||
test("buildProviderRegistry 从 DB 构建包含所有供应商的注册表", async () => {
|
||||
const handle = createMigratedTestDatabase("registry-build-test");
|
||||
const now = new Date().toISOString();
|
||||
|
||||
handle.db
|
||||
.prepare(
|
||||
"INSERT INTO providers (id, name, type, base_url, api_key, enabled, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
"INSERT INTO providers (id, name, type, base_url, api_key, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
)
|
||||
.run("pv1", "OpenAI", "openai", "https://api.openai.com/v1", "sk-test", 1, now, now);
|
||||
.run("pv1", "OpenAI", "openai", "https://api.openai.com/v1", "sk-test", now, now);
|
||||
handle.db
|
||||
.prepare(
|
||||
"INSERT INTO providers (id, name, type, base_url, api_key, enabled, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
"INSERT INTO providers (id, name, type, base_url, api_key, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
)
|
||||
.run("pv2", "Disabled", "anthropic", "https://api.anthropic.com", "sk-off", 0, now, now);
|
||||
.run("pv2", "Anthropic", "anthropic", "https://api.anthropic.com", "sk-off", now, now);
|
||||
|
||||
const { buildProviderRegistry } = await import("../../../src/server/ai/registry");
|
||||
const registry = buildProviderRegistry(handle.db);
|
||||
|
||||
expect(() => registry.languageModel("pv1:gpt-4o")).not.toThrow();
|
||||
expect(() => registry.languageModel("pv2:claude-3")).toThrow();
|
||||
expect(() => registry.languageModel("pv2:claude-3")).not.toThrow();
|
||||
|
||||
handle.cleanup();
|
||||
});
|
||||
|
||||
test("buildProviderRegistry 无启用供应商时返回空注册表", async () => {
|
||||
test("buildProviderRegistry 无供应商时返回空注册表", async () => {
|
||||
const handle = createMigratedTestDatabase("registry-empty-test");
|
||||
|
||||
const { buildProviderRegistry } = await import("../../../src/server/ai/registry");
|
||||
@@ -86,4 +130,19 @@ describe("AI registry", () => {
|
||||
|
||||
handle.cleanup();
|
||||
});
|
||||
|
||||
test("testModelConnection 成功返回 ok:true", async () => {
|
||||
const { testModelConnection } = await import("../../../src/server/ai/registry");
|
||||
|
||||
const result = await testModelConnection({
|
||||
apiKey: "sk-test",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
modelId: "gpt-4o",
|
||||
name: "Test",
|
||||
type: "openai",
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.message).toContain("模型连接成功");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user