Your Name bbdda81686
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: add midjourney provider and failover exclusion
2026-03-07 16:25:48 +08:00

247 lines
7.5 KiB
TypeScript

import { z } from "zod";
import { requireAdmin } from "@/lib/middleware/admin";
import {
getAllAiProviders,
getAiProviderById,
createAiProvider,
updateAiProvider,
deleteAiProvider,
} from "@/lib/db/queries";
import { ChatSDKError } from "@/lib/errors";
const providerTypes = [
"alibaba",
"openai",
"custom",
"local",
"gemini",
"huanke-gemini",
"qianwen-video",
"doubao-video",
"ruxa-video",
"volcano-vision",
"volcano-video",
"alibaba-image",
"ruxa-image",
"fal-image",
"openai-image",
"midjourney",
"fal-video",
"zhipu",
"tripo-3d",
"volcano-multimodal",
] as const;
type ProviderType = (typeof providerTypes)[number];
type StoredProviderType = Exclude<ProviderType, "zhipu" | "volcano-multimodal">;
const createProviderSchema = z.object({
name: z.string().min(1).max(128),
type: z.enum(providerTypes),
baseUrl: z.string().min(1), // 改为简单的字符串验证,支持 localhost
apiKey: z.string().min(1),
isEnabled: z.boolean().optional(),
});
const updateProviderSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(128).optional(),
type: z.enum(providerTypes).optional(),
baseUrl: z.string().min(1).optional(), // 改为简单的字符串验证,支持 localhost
apiKey: z.string().min(1).optional(),
isEnabled: z.boolean().optional(),
});
const ZHIPU_BASE_URL = "https://open.bigmodel.cn/api/paas/v4";
const VOLCANO_MULTIMODAL_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3";
const DOUBAO_VIDEO_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3";
const RUXA_VIDEO_BASE_URL = "https://api.ruxa.ai";
const RUXA_IMAGE_BASE_URL = "https://api.ruxa.ai";
const OPENAI_IMAGE_BASE_URL = "https://api.openai.com/v1";
const MIDJOURNEY_BASE_URL = "https://your-midjourney-api.example.com";
const TRIPO_3D_BASE_URL = "https://api.tripo3d.ai/v2/openapi";
function normalizeProviderTypeForStorage(
type?: ProviderType
): StoredProviderType | undefined {
if (!type) return undefined;
if (type === "zhipu" || type === "volcano-multimodal") return "custom";
return type;
}
function isZhipuBaseUrl(baseUrl: string) {
return baseUrl.includes("open.bigmodel.cn");
}
function isVolcanoMultimodalBaseUrl(baseUrl: string) {
return /ark\.cn-beijing\.volces\.com/i.test(baseUrl);
}
function normalizeProviderTypeForResponse(provider: Record<string, any>) {
if (provider.type === "custom" && typeof provider.baseUrl === "string" && isZhipuBaseUrl(provider.baseUrl)) {
return { ...provider, type: "zhipu" };
}
if (
provider.type === "custom" &&
typeof provider.baseUrl === "string" &&
isVolcanoMultimodalBaseUrl(provider.baseUrl)
) {
return { ...provider, type: "volcano-multimodal" };
}
return provider;
}
function maskApiKey(apiKey?: string) {
if (!apiKey) return undefined;
if (apiKey.length <= 8) return "****";
return `${apiKey.slice(0, 4)}****${apiKey.slice(-4)}`;
}
function sanitizeProviderPayload(payload: unknown) {
if (!payload || typeof payload !== "object") return payload;
const cloned = { ...(payload as Record<string, unknown>) };
if (typeof cloned.apiKey === "string") {
cloned.apiKey = maskApiKey(cloned.apiKey);
}
return cloned;
}
export async function GET(request: Request) {
try {
await requireAdmin();
const { searchParams } = new URL(request.url);
const id = searchParams.get("id");
if (id) {
const provider = await getAiProviderById({ id });
if (!provider) {
return new ChatSDKError("bad_request:api", "Provider not found").toResponse();
}
// 隐藏 API Key 的部分内容
return Response.json({
provider: normalizeProviderTypeForResponse({
...provider,
apiKey: provider.apiKey.slice(0, 8) + "****" + provider.apiKey.slice(-4),
}),
});
}
const providers = await getAllAiProviders();
// 隐藏 API Key 的部分内容
return Response.json({
providers: providers.map((p) =>
normalizeProviderTypeForResponse({
...p,
apiKey: p.apiKey.slice(0, 8) + "****" + p.apiKey.slice(-4),
})
),
});
} catch (error) {
if (error instanceof ChatSDKError) {
return error.toResponse();
}
return new ChatSDKError("offline:admin").toResponse();
}
}
export async function POST(request: Request) {
try {
const session = await requireAdmin();
const body = await request.json();
console.log("[Admin API] Create provider request body:", JSON.stringify(sanitizeProviderPayload(body)));
const data = createProviderSchema.parse(body);
console.log("[Admin API] Validation passed, creating provider...");
const storageType = normalizeProviderTypeForStorage(data.type) ?? "custom";
const provider = await createAiProvider({
...data,
type: storageType,
baseUrl:
data.type === "zhipu"
? data.baseUrl || ZHIPU_BASE_URL
: data.type === "volcano-multimodal"
? data.baseUrl || VOLCANO_MULTIMODAL_BASE_URL
: data.type === "doubao-video"
? data.baseUrl || DOUBAO_VIDEO_BASE_URL
: data.type === "ruxa-video"
? data.baseUrl || RUXA_VIDEO_BASE_URL
: data.type === "ruxa-image"
? data.baseUrl || RUXA_IMAGE_BASE_URL
: data.type === "openai-image"
? data.baseUrl || OPENAI_IMAGE_BASE_URL
: data.type === "midjourney"
? data.baseUrl || MIDJOURNEY_BASE_URL
: data.type === "tripo-3d"
? data.baseUrl || TRIPO_3D_BASE_URL
: data.baseUrl,
updatedBy: session.user.id,
});
console.log("[Admin API] Provider created successfully:", provider[0].id);
return Response.json({ provider: normalizeProviderTypeForResponse(provider[0] as any) });
} catch (error) {
console.error("[Admin API] Create provider error:", error);
if (error instanceof ChatSDKError) {
return error.toResponse();
}
if (error instanceof z.ZodError) {
console.error("[Admin API] Validation error details:", error.errors);
return new ChatSDKError("bad_request:api", JSON.stringify(error.errors)).toResponse();
}
return new ChatSDKError("offline:admin").toResponse();
}
}
export async function PUT(request: Request) {
try {
const session = await requireAdmin();
const body = await request.json();
const data = updateProviderSchema.parse(body);
const provider = await updateAiProvider({
...data,
type: normalizeProviderTypeForStorage(data.type),
updatedBy: session.user.id,
});
if (provider.length === 0) {
return new ChatSDKError("bad_request:api", "Provider not found").toResponse();
}
return Response.json({ provider: normalizeProviderTypeForResponse(provider[0] as any) });
} catch (error) {
if (error instanceof ChatSDKError) {
return error.toResponse();
}
if (error instanceof z.ZodError) {
return new ChatSDKError("bad_request:api", error.message).toResponse();
}
return new ChatSDKError("offline:admin").toResponse();
}
}
export async function DELETE(request: Request) {
try {
await requireAdmin();
const { searchParams } = new URL(request.url);
const id = searchParams.get("id");
if (!id) {
return new ChatSDKError("bad_request:api", "Provider ID is required").toResponse();
}
await deleteAiProvider({ id });
return Response.json({ success: true });
} catch (error) {
if (error instanceof ChatSDKError) {
return error.toResponse();
}
return new ChatSDKError("offline:admin").toResponse();
}
}