429 lines
14 KiB
TypeScript
429 lines
14 KiB
TypeScript
import { z } from "zod";
|
|
import { requireAdmin } from "@/lib/middleware/admin";
|
|
import {
|
|
getAllSystemSettings,
|
|
getSystemSetting,
|
|
upsertSystemSetting,
|
|
} from "@/lib/db/queries";
|
|
import { ChatSDKError } from "@/lib/errors";
|
|
import { probeBflModelSupport } from "@/lib/realtime/bfl-capability";
|
|
import { probeFalModelSupport } from "@/lib/realtime/fal-capability";
|
|
import { probeReplicateModelSupport } from "@/lib/realtime/replicate-capability";
|
|
import {
|
|
normalizeRealtimeCanvasConfigForStorage,
|
|
resolveRealtimeProviders,
|
|
} from "@/lib/realtime/config";
|
|
import {
|
|
DETAIL_AGENT_LOCAL_MOONSHOT_BASE_URL,
|
|
DETAIL_AGENT_LOCAL_MOONSHOT_DISPLAY_NAME,
|
|
DETAIL_AGENT_LOCAL_MOONSHOT_MODEL_ID,
|
|
DETAIL_AGENT_LOCAL_MOONSHOT_SETTING_KEY,
|
|
} from "@/lib/ai/detail-agent-model-config";
|
|
import {
|
|
normalizeProductPlanningApiConfig,
|
|
PRODUCT_PLANNING_API_CONFIG_KEY,
|
|
} from "@/lib/product-planning/config";
|
|
import {
|
|
normalizeProductPlanningAiModelConfig,
|
|
PRODUCT_PLANNING_AI_MODEL_CONFIG_KEY,
|
|
} from "@/lib/product-planning/model-config";
|
|
import {
|
|
normalizeThirdPartySqlServerConfig,
|
|
THIRD_PARTY_SQLSERVER_CONFIG_KEY,
|
|
} from "@/lib/sqlserver/config";
|
|
import {
|
|
ENHANCE_PRO_KREA_SETTING_KEY,
|
|
normalizeEnhanceProKreaConfig,
|
|
} from "@/lib/enhance-pro/config";
|
|
import {
|
|
normalizeWorldLabsKeyPoolConfig,
|
|
WORLDLABS_KEY_POOL_CONFIG_KEY,
|
|
} from "@/lib/worldlabs/key-pool-config-shared";
|
|
import {
|
|
normalizeThreeDModelAliasConfig,
|
|
THREED_MODEL_ALIAS_CONFIG_SETTING_KEY,
|
|
} from "@/lib/model3d/model-alias-config";
|
|
import {
|
|
normalizeVideoCanvasFunctionConfig,
|
|
VIDEO_CANVAS_FUNCTION_CONFIG_KEY,
|
|
} from "@/lib/video-canvas/config";
|
|
import {
|
|
CANVAS_IMAGE_TASK_QUEUE_CONFIG_KEY,
|
|
normalizeCanvasImageTaskQueueConfig,
|
|
} from "@/lib/canvas/image-task-config";
|
|
import {
|
|
DINGTALK_ROBOT_NOTIFICATION_CONFIG_KEY,
|
|
normalizeDingTalkRobotNotificationConfig,
|
|
} from "@/lib/notifications/dingtalk-robot-config";
|
|
import {
|
|
DINGTALK_NOTIFICATION_DRAFT_CONFIG_KEY,
|
|
normalizeDingTalkNotificationDraftConfig,
|
|
} from "@/lib/notifications/dingtalk-notification-draft-config";
|
|
import {
|
|
STYLE_PRO_CONFIG_KEY,
|
|
normalizeStyleProConfig,
|
|
} from "@/lib/style-pro/config";
|
|
import {
|
|
EXTERNAL_IMAGE_API_CONFIG_KEY,
|
|
normalizeExternalImageApiConfig,
|
|
} from "@/lib/external-image-api/config";
|
|
import {
|
|
AGENT_WORKFLOW_MATERIAL_API_CONFIG_KEY,
|
|
normalizeAgentWorkflowMaterialApiConfig,
|
|
} from "@/lib/agents/workflow-material-config";
|
|
import { clearScopedSiteSettingsCache } from "@/lib/site-settings/public-cache";
|
|
|
|
const settingSchema = z.object({
|
|
key: z.string(),
|
|
value: z.any(),
|
|
});
|
|
|
|
const moonshotDetailAgentConfigSchema = z.object({
|
|
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 = DETAIL_AGENT_LOCAL_MOONSHOT_BASE_URL;
|
|
|
|
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 });
|
|
if (key === CANVAS_IMAGE_TASK_QUEUE_CONFIG_KEY) {
|
|
const value = normalizeCanvasImageTaskQueueConfig(setting?.value, {
|
|
envConcurrency: process.env.CANVAS_IMAGE_TASK_CONCURRENCY,
|
|
});
|
|
return Response.json({
|
|
setting: setting ? { ...setting, value } : { key, value },
|
|
});
|
|
}
|
|
if (key === DINGTALK_ROBOT_NOTIFICATION_CONFIG_KEY) {
|
|
const value = normalizeDingTalkRobotNotificationConfig(setting?.value);
|
|
return Response.json({
|
|
setting: setting ? { ...setting, value } : { key, value },
|
|
});
|
|
}
|
|
if (key === DINGTALK_NOTIFICATION_DRAFT_CONFIG_KEY) {
|
|
const value = normalizeDingTalkNotificationDraftConfig(setting?.value);
|
|
return Response.json({
|
|
setting: setting ? { ...setting, value } : { key, value },
|
|
});
|
|
}
|
|
if (key === STYLE_PRO_CONFIG_KEY) {
|
|
const value = normalizeStyleProConfig(setting?.value);
|
|
return Response.json({
|
|
setting: setting ? { ...setting, value } : { key, value },
|
|
});
|
|
}
|
|
if (key === EXTERNAL_IMAGE_API_CONFIG_KEY) {
|
|
const value = normalizeExternalImageApiConfig(setting?.value);
|
|
return Response.json({
|
|
setting: setting ? { ...setting, value } : { key, value },
|
|
});
|
|
}
|
|
if (key === AGENT_WORKFLOW_MATERIAL_API_CONFIG_KEY) {
|
|
const value = normalizeAgentWorkflowMaterialApiConfig(setting?.value);
|
|
return Response.json({
|
|
setting: setting ? { ...setting, value } : { key, value },
|
|
});
|
|
}
|
|
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 === DETAIL_AGENT_LOCAL_MOONSHOT_SETTING_KEY) {
|
|
const existingSetting = await getSystemSetting({
|
|
key: DETAIL_AGENT_LOCAL_MOONSHOT_SETTING_KEY,
|
|
}).catch(() => null);
|
|
const existingValue =
|
|
existingSetting?.value && typeof existingSetting.value === "object"
|
|
? (existingSetting.value as Record<string, unknown>)
|
|
: {};
|
|
const config = moonshotDetailAgentConfigSchema.parse(value || {});
|
|
const normalizedBaseUrl = String(config.baseUrl || "").trim() || MOONSHOT_OFFICIAL_BASE_URL;
|
|
const rawModelId = String(config.modelId || "").trim();
|
|
const rawDisplayName = String(config.displayName || "").trim();
|
|
const normalizedModelId =
|
|
!rawModelId || rawModelId === "kimi-k2.5"
|
|
? DETAIL_AGENT_LOCAL_MOONSHOT_MODEL_ID
|
|
: rawModelId;
|
|
const normalizedDisplayName =
|
|
!rawDisplayName || rawDisplayName === "官方 Kimi K2.5"
|
|
? DETAIL_AGENT_LOCAL_MOONSHOT_DISPLAY_NAME
|
|
: rawDisplayName;
|
|
const normalizedProviderName =
|
|
String(config.providerName || "").trim() || "Moonshot AI (Official)";
|
|
const normalizedApiKey =
|
|
String(config.apiKey || "").trim() ||
|
|
String(existingValue.apiKey || "").trim();
|
|
const enabled = config.isEnabled !== false;
|
|
|
|
if (!isMoonshotOfficialBaseUrl(normalizedBaseUrl)) {
|
|
return new ChatSDKError(
|
|
"bad_request:api",
|
|
`Moonshot 官方地址必须为 ${MOONSHOT_OFFICIAL_BASE_URL}`,
|
|
).toResponse();
|
|
}
|
|
|
|
if (!normalizedApiKey) {
|
|
return new ChatSDKError("bad_request:api", "缺少 Moonshot 官方 API Key").toResponse();
|
|
}
|
|
|
|
const saved = await upsertSystemSetting({
|
|
key: DETAIL_AGENT_LOCAL_MOONSHOT_SETTING_KEY,
|
|
value: {
|
|
enabled,
|
|
providerName: normalizedProviderName,
|
|
baseUrl: normalizedBaseUrl,
|
|
apiKey: normalizedApiKey,
|
|
modelId: normalizedModelId,
|
|
displayName: normalizedDisplayName,
|
|
},
|
|
updatedBy: session.user.id,
|
|
});
|
|
|
|
return Response.json({
|
|
setting: saved[0],
|
|
});
|
|
}
|
|
|
|
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,
|
|
})
|
|
: provider.type === "fal"
|
|
? await probeFalModelSupport({
|
|
apiKey: provider.apiKey,
|
|
modelId: provider.model,
|
|
})
|
|
: 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;
|
|
}
|
|
|
|
if (key === EXTERNAL_IMAGE_API_CONFIG_KEY) {
|
|
const saved = await upsertSystemSetting({
|
|
key: EXTERNAL_IMAGE_API_CONFIG_KEY,
|
|
value: normalizeExternalImageApiConfig(value),
|
|
updatedBy: session.user.id,
|
|
});
|
|
clearScopedSiteSettingsCache();
|
|
|
|
return Response.json({
|
|
setting: saved[0],
|
|
});
|
|
}
|
|
|
|
if (key === AGENT_WORKFLOW_MATERIAL_API_CONFIG_KEY) {
|
|
const saved = await upsertSystemSetting({
|
|
key: AGENT_WORKFLOW_MATERIAL_API_CONFIG_KEY,
|
|
value: normalizeAgentWorkflowMaterialApiConfig(value),
|
|
updatedBy: session.user.id,
|
|
});
|
|
clearScopedSiteSettingsCache();
|
|
|
|
return Response.json({
|
|
setting: saved[0],
|
|
});
|
|
}
|
|
|
|
if (key === VIDEO_CANVAS_FUNCTION_CONFIG_KEY) {
|
|
nextValue = normalizeVideoCanvasFunctionConfig(value);
|
|
}
|
|
|
|
if (key === CANVAS_IMAGE_TASK_QUEUE_CONFIG_KEY) {
|
|
nextValue = normalizeCanvasImageTaskQueueConfig(value);
|
|
}
|
|
|
|
if (key === DINGTALK_ROBOT_NOTIFICATION_CONFIG_KEY) {
|
|
nextValue = normalizeDingTalkRobotNotificationConfig(value);
|
|
}
|
|
|
|
if (key === DINGTALK_NOTIFICATION_DRAFT_CONFIG_KEY) {
|
|
nextValue = normalizeDingTalkNotificationDraftConfig(value);
|
|
}
|
|
|
|
if (key === STYLE_PRO_CONFIG_KEY) {
|
|
nextValue = normalizeStyleProConfig(value);
|
|
}
|
|
|
|
if (key === THIRD_PARTY_SQLSERVER_CONFIG_KEY) {
|
|
const existingSetting = await getSystemSetting({
|
|
key: THIRD_PARTY_SQLSERVER_CONFIG_KEY,
|
|
}).catch(() => null);
|
|
const existingValue =
|
|
existingSetting?.value && typeof existingSetting.value === "object"
|
|
? (existingSetting.value as Record<string, unknown>)
|
|
: {};
|
|
const normalizedValue = normalizeThirdPartySqlServerConfig(
|
|
value,
|
|
String(existingValue.password || "").trim(),
|
|
);
|
|
|
|
if (normalizedValue.enabled) {
|
|
if (!normalizedValue.host) {
|
|
return new ChatSDKError("bad_request:api", "请填写 SQL Server 主机地址").toResponse();
|
|
}
|
|
if (!normalizedValue.database) {
|
|
return new ChatSDKError("bad_request:api", "请填写数据库名称").toResponse();
|
|
}
|
|
if (!normalizedValue.username) {
|
|
return new ChatSDKError("bad_request:api", "请填写数据库账号").toResponse();
|
|
}
|
|
if (!normalizedValue.password) {
|
|
return new ChatSDKError("bad_request:api", "请填写数据库密码").toResponse();
|
|
}
|
|
}
|
|
|
|
nextValue = normalizedValue;
|
|
}
|
|
|
|
if (key === PRODUCT_PLANNING_API_CONFIG_KEY) {
|
|
const existingSetting = await getSystemSetting({
|
|
key: PRODUCT_PLANNING_API_CONFIG_KEY,
|
|
}).catch(() => null);
|
|
const existingValue =
|
|
existingSetting?.value && typeof existingSetting.value === "object"
|
|
? (existingSetting.value as Record<string, unknown>)
|
|
: {};
|
|
|
|
nextValue = normalizeProductPlanningApiConfig(value, {
|
|
taobaoApiKey: String(existingValue.taobaoApiKey || "").trim(),
|
|
taobaoApiSecret: String(existingValue.taobaoApiSecret || "").trim(),
|
|
detailPlusApiKey: String(existingValue.detailPlusApiKey || "").trim(),
|
|
qaToken: String(existingValue.qaToken || "").trim(),
|
|
saleToken: String(existingValue.saleToken || "").trim(),
|
|
planningDbPassword: String(existingValue.planningDbPassword || "").trim(),
|
|
});
|
|
}
|
|
|
|
if (key === PRODUCT_PLANNING_AI_MODEL_CONFIG_KEY) {
|
|
nextValue = normalizeProductPlanningAiModelConfig(value);
|
|
}
|
|
|
|
if (key === ENHANCE_PRO_KREA_SETTING_KEY) {
|
|
const existingSetting = await getSystemSetting({
|
|
key: ENHANCE_PRO_KREA_SETTING_KEY,
|
|
}).catch(() => null);
|
|
const existingValue =
|
|
existingSetting?.value && typeof existingSetting.value === "object"
|
|
? (existingSetting.value as Record<string, unknown>)
|
|
: {};
|
|
const normalizedValue = normalizeEnhanceProKreaConfig(
|
|
value,
|
|
String(existingValue.apiKey || "").trim(),
|
|
);
|
|
|
|
if (normalizedValue.enabled) {
|
|
if (!normalizedValue.baseUrl) {
|
|
return new ChatSDKError("bad_request:api", "请填写 Krea API 地址").toResponse();
|
|
}
|
|
if (!normalizedValue.apiKey && normalizedValue.apiKeys.length === 0) {
|
|
return new ChatSDKError("bad_request:api", "请填写 Krea API Key 或 Key 池").toResponse();
|
|
}
|
|
}
|
|
|
|
nextValue = normalizedValue;
|
|
}
|
|
|
|
if (key === WORLDLABS_KEY_POOL_CONFIG_KEY) {
|
|
nextValue = normalizeWorldLabsKeyPoolConfig(value);
|
|
}
|
|
|
|
if (key === THREED_MODEL_ALIAS_CONFIG_SETTING_KEY) {
|
|
nextValue = normalizeThreeDModelAliasConfig(value);
|
|
}
|
|
|
|
const setting = await upsertSystemSetting({
|
|
key,
|
|
value: nextValue,
|
|
updatedBy: session.user.id,
|
|
});
|
|
clearScopedSiteSettingsCache();
|
|
|
|
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();
|
|
}
|
|
}
|