275 lines
8.8 KiB
TypeScript
275 lines
8.8 KiB
TypeScript
import { z } from "zod";
|
|
import { requireAdmin } from "@/lib/middleware/admin";
|
|
import {
|
|
createAiModel,
|
|
createAiProvider,
|
|
getAiModelById,
|
|
getAiProviderById,
|
|
getAllAiModels,
|
|
getAllSystemSettings,
|
|
getSystemSetting,
|
|
upsertSystemSetting,
|
|
updateAiModel,
|
|
updateAiProvider,
|
|
} from "@/lib/db/queries";
|
|
import { ChatSDKError } from "@/lib/errors";
|
|
import { probeBflModelSupport } from "@/lib/realtime/bfl-capability";
|
|
import { probeReplicateModelSupport } from "@/lib/realtime/replicate-capability";
|
|
import {
|
|
normalizeRealtimeCanvasConfigForStorage,
|
|
resolveRealtimeProviders,
|
|
} from "@/lib/realtime/config";
|
|
import { db } from "@/lib/db";
|
|
import { siteSettings } from "@/lib/db/schema";
|
|
import { eq } from "drizzle-orm";
|
|
|
|
const settingSchema = z.object({
|
|
key: z.string(),
|
|
value: z.any(),
|
|
});
|
|
|
|
const moonshotDetailAgentConfigSchema = z.object({
|
|
providerId: z.string().uuid().optional(),
|
|
modelEntryId: z.string().uuid().optional(),
|
|
providerName: z.string().trim().min(1).max(128).optional(),
|
|
baseUrl: z.string().trim().min(1),
|
|
apiKey: z.string().optional(),
|
|
modelId: z.string().trim().min(1).max(128).optional(),
|
|
displayName: z.string().trim().min(1).max(128).optional(),
|
|
isEnabled: z.boolean().optional(),
|
|
});
|
|
|
|
const MOONSHOT_OFFICIAL_BASE_URL = "https://api.moonshot.cn/v1";
|
|
const MOONSHOT_KIMI_K25_MODEL_ID = "kimi-k2.5";
|
|
|
|
function isMoonshotOfficialBaseUrl(value: string) {
|
|
return String(value || "").trim().replace(/\/+$/, "") === MOONSHOT_OFFICIAL_BASE_URL;
|
|
}
|
|
|
|
function isObjectLike(value: unknown): value is Record<string, unknown> {
|
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
}
|
|
|
|
function mapProbeStatus(code: string) {
|
|
if (code === "UPSTREAM_ERROR") return 502;
|
|
return 400;
|
|
}
|
|
|
|
export async function GET(request: Request) {
|
|
try {
|
|
await requireAdmin();
|
|
|
|
const { searchParams } = new URL(request.url);
|
|
const key = searchParams.get("key");
|
|
|
|
if (key) {
|
|
const setting = await getSystemSetting({ key });
|
|
return Response.json({ setting });
|
|
}
|
|
|
|
const settings = await getAllSystemSettings();
|
|
return Response.json({ settings });
|
|
} 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();
|
|
const { key, value } = settingSchema.parse(body);
|
|
|
|
let nextValue = value;
|
|
|
|
if (key === "moonshot_detail_agent_config") {
|
|
const config = moonshotDetailAgentConfigSchema.parse(value || {});
|
|
const normalizedBaseUrl = String(config.baseUrl || "").trim() || MOONSHOT_OFFICIAL_BASE_URL;
|
|
const normalizedModelId = String(config.modelId || "").trim() || MOONSHOT_KIMI_K25_MODEL_ID;
|
|
const normalizedDisplayName = String(config.displayName || "").trim() || "官方 Kimi K2.5";
|
|
const normalizedProviderName =
|
|
String(config.providerName || "").trim() || "Moonshot AI (Official)";
|
|
const normalizedApiKey = String(config.apiKey || "").trim();
|
|
const enabled = config.isEnabled !== false;
|
|
|
|
if (!isMoonshotOfficialBaseUrl(normalizedBaseUrl)) {
|
|
return new ChatSDKError(
|
|
"bad_request:api",
|
|
`Moonshot 官方地址必须为 ${MOONSHOT_OFFICIAL_BASE_URL}`,
|
|
).toResponse();
|
|
}
|
|
|
|
if (!config.providerId && !normalizedApiKey) {
|
|
return new ChatSDKError("bad_request:api", "缺少 Moonshot 官方 API Key").toResponse();
|
|
}
|
|
|
|
const [existingSiteSettings] = await db.select().from(siteSettings).limit(1);
|
|
|
|
const providerRows = config.providerId
|
|
? await updateAiProvider({
|
|
id: config.providerId,
|
|
name: normalizedProviderName,
|
|
type: "openai",
|
|
baseUrl: normalizedBaseUrl,
|
|
apiKey: normalizedApiKey || undefined,
|
|
isEnabled: enabled,
|
|
updatedBy: session.user.id,
|
|
})
|
|
: await createAiProvider({
|
|
name: normalizedProviderName,
|
|
type: "openai",
|
|
baseUrl: normalizedBaseUrl,
|
|
apiKey: normalizedApiKey,
|
|
isEnabled: enabled,
|
|
updatedBy: session.user.id,
|
|
});
|
|
|
|
const provider = providerRows[0];
|
|
if (!provider) {
|
|
return new ChatSDKError("bad_request:database", "保存 Moonshot 提供商失败").toResponse();
|
|
}
|
|
|
|
let existingModel =
|
|
config.modelEntryId ? await getAiModelById({ id: config.modelEntryId }) : undefined;
|
|
|
|
if (!existingModel) {
|
|
const allModels = await getAllAiModels();
|
|
existingModel =
|
|
allModels.find(
|
|
(model) => model.providerId === provider.id && model.modelId === normalizedModelId,
|
|
) || undefined;
|
|
}
|
|
|
|
const modelRows = existingModel
|
|
? await updateAiModel({
|
|
id: existingModel.id,
|
|
providerId: provider.id,
|
|
modelId: normalizedModelId,
|
|
displayName: normalizedDisplayName,
|
|
description: "Moonshot 官方 Kimi K2.5,专用于 AI详情生成 的规划与排版复审",
|
|
supportsVision: true,
|
|
supportsImageGen: false,
|
|
isEnabled: enabled,
|
|
isHidden: false,
|
|
isDefault: false,
|
|
isAgentModel: false,
|
|
isMultimodal: true,
|
|
})
|
|
: await createAiModel({
|
|
providerId: provider.id,
|
|
modelId: normalizedModelId,
|
|
displayName: normalizedDisplayName,
|
|
description: "Moonshot 官方 Kimi K2.5,专用于 AI详情生成 的规划与排版复审",
|
|
supportsVision: true,
|
|
supportsImageGen: false,
|
|
isEnabled: enabled,
|
|
isHidden: false,
|
|
isDefault: false,
|
|
isAgentModel: false,
|
|
isMultimodal: true,
|
|
});
|
|
|
|
const model = modelRows[0];
|
|
if (!model) {
|
|
return new ChatSDKError("bad_request:database", "保存 Kimi K2.5 模型失败").toResponse();
|
|
}
|
|
|
|
if (existingSiteSettings) {
|
|
await db
|
|
.update(siteSettings)
|
|
.set({
|
|
canvasDetailAgentModelId: model.id,
|
|
updatedAt: new Date(),
|
|
updatedBy: session.user.id,
|
|
})
|
|
.where(eq(siteSettings.id, existingSiteSettings.id));
|
|
} else {
|
|
await db.insert(siteSettings).values({
|
|
siteName: "AI Chatbot",
|
|
canvasDetailAgentModelId: model.id,
|
|
updatedBy: session.user.id,
|
|
});
|
|
}
|
|
|
|
return Response.json({
|
|
setting: {
|
|
key: "moonshot_detail_agent_config",
|
|
value: {
|
|
providerId: provider.id,
|
|
modelEntryId: model.id,
|
|
providerName: provider.name,
|
|
baseUrl: provider.baseUrl,
|
|
modelId: model.modelId,
|
|
displayName: model.displayName,
|
|
isEnabled: model.isEnabled,
|
|
canvasDetailAgentModelId: model.id,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
if (key === "realtime_canvas_config") {
|
|
if (!isObjectLike(value)) {
|
|
return new ChatSDKError("bad_request:api", "realtime_canvas_config value 必须为对象").toResponse();
|
|
}
|
|
|
|
const enabled = value.enabled !== false;
|
|
const normalizedValue = normalizeRealtimeCanvasConfigForStorage(value);
|
|
const providers = resolveRealtimeProviders(normalizedValue);
|
|
|
|
if (enabled) {
|
|
if (providers.length === 0) {
|
|
return new ChatSDKError("bad_request:api", "启用实时画布必须至少配置一个可用服务").toResponse();
|
|
}
|
|
|
|
for (const provider of providers) {
|
|
const probeResult =
|
|
provider.type === "replicate"
|
|
? await probeReplicateModelSupport({
|
|
apiKey: provider.apiKey,
|
|
modelId: provider.model,
|
|
baseUrl: provider.baseUrl,
|
|
})
|
|
: await probeBflModelSupport({
|
|
apiKey: provider.apiKey,
|
|
modelId: provider.model,
|
|
baseUrl: provider.baseUrl,
|
|
});
|
|
|
|
if (!probeResult.supported) {
|
|
return Response.json(
|
|
{
|
|
code: probeResult.code,
|
|
message: `${provider.label}校验失败:${probeResult.message}`,
|
|
},
|
|
{ status: mapProbeStatus(probeResult.code) }
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
nextValue = normalizedValue;
|
|
}
|
|
|
|
const setting = await upsertSystemSetting({
|
|
key,
|
|
value: nextValue,
|
|
updatedBy: session.user.id,
|
|
});
|
|
|
|
return Response.json({ setting: setting[0] });
|
|
} 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();
|
|
}
|
|
}
|