134 lines
4.2 KiB
TypeScript
134 lines
4.2 KiB
TypeScript
// server/api/admin/system-config.put.ts - 更新系统配置(仅 superadmin 可访问)
|
|
//
|
|
// 流程:鉴权 → 二次校验 superadmin → 白名单校验 → upsert system_config → 失效缓存 → 返回
|
|
// 安全边界:
|
|
// - handler 内独立校验 role === 'superadmin'
|
|
// - openAiApiKey 为空字符串时跳过,不清空已有 key
|
|
// - 更新成功后立即失效系统配置缓存,下次请求即生效
|
|
|
|
import { readBody, setResponseStatus } from "h3";
|
|
|
|
import { prisma } from "~~/server/utils/db";
|
|
import { createApiLogger, toSafeLogError } from "~~/server/utils/logging";
|
|
import { apiErr, apiOk } from "~~/server/utils/response";
|
|
import { invalidateSysConfigCache } from "~~/server/utils/sysConfig";
|
|
import type { ISystemConfigUpdateRequest } from "~~/shared/types/settings";
|
|
|
|
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
typeof v === "object" && v !== null && !Array.isArray(v);
|
|
|
|
/** system_config 表 key 的常量映射 */
|
|
const SYS_KEY = {
|
|
openAiApiKey: "openai_api_key",
|
|
openAiApiBase: "openai_api_base",
|
|
openAiModel: "openai_model",
|
|
maxTokens: "max_tokens",
|
|
temperature: "temperature",
|
|
maxSseLineBytes: "openai_stream_max_sse_line_bytes",
|
|
allowRegistration: "allow_registration"
|
|
} as const;
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
const logger = createApiLogger(event, "admin.system-config.put");
|
|
|
|
const role = (event.context.auth?.user as { role?: string } | undefined)
|
|
?.role;
|
|
if (role !== "superadmin") {
|
|
setResponseStatus(event, 403);
|
|
return apiErr(403, "权限不足");
|
|
}
|
|
|
|
const body = await readBody<unknown>(event).catch(() => null);
|
|
if (!isRecord(body)) {
|
|
setResponseStatus(event, 400);
|
|
return apiErr(400, "请求体必须是 JSON 对象");
|
|
}
|
|
|
|
const req: ISystemConfigUpdateRequest = {};
|
|
if (typeof body.openAiApiBase === "string") {
|
|
req.openAiApiBase = body.openAiApiBase.trim();
|
|
}
|
|
if (typeof body.openAiApiKey === "string") {
|
|
req.openAiApiKey = body.openAiApiKey; // 空字符串 = 跳过,见下方逻辑
|
|
}
|
|
if (typeof body.openAiModel === "string") {
|
|
req.openAiModel = body.openAiModel.trim();
|
|
}
|
|
if (typeof body.maxTokens === "number") {
|
|
req.maxTokens = body.maxTokens;
|
|
}
|
|
if (typeof body.temperature === "number") {
|
|
req.temperature = body.temperature;
|
|
}
|
|
if (typeof body.maxSseLineBytes === "number") {
|
|
req.maxSseLineBytes = body.maxSseLineBytes;
|
|
}
|
|
if (typeof body.allowRegistration === "boolean") {
|
|
req.allowRegistration = body.allowRegistration;
|
|
}
|
|
|
|
// 构建需要写入的 KV 对(key 为空时跳过)
|
|
type SysKv = { key: string; value: string };
|
|
const upserts: SysKv[] = [];
|
|
|
|
if (req.openAiApiBase) {
|
|
upserts.push({ key: SYS_KEY.openAiApiBase, value: req.openAiApiBase });
|
|
}
|
|
// key 为空字符串时跳过,不清空已有 key
|
|
if (req.openAiApiKey) {
|
|
upserts.push({ key: SYS_KEY.openAiApiKey, value: req.openAiApiKey });
|
|
}
|
|
if (req.openAiModel) {
|
|
upserts.push({ key: SYS_KEY.openAiModel, value: req.openAiModel });
|
|
}
|
|
if (req.maxTokens !== undefined) {
|
|
upserts.push({ key: SYS_KEY.maxTokens, value: String(req.maxTokens) });
|
|
}
|
|
if (req.temperature !== undefined) {
|
|
upserts.push({ key: SYS_KEY.temperature, value: String(req.temperature) });
|
|
}
|
|
if (req.maxSseLineBytes !== undefined) {
|
|
upserts.push({
|
|
key: SYS_KEY.maxSseLineBytes,
|
|
value: String(req.maxSseLineBytes)
|
|
});
|
|
}
|
|
if (req.allowRegistration !== undefined) {
|
|
upserts.push({
|
|
key: SYS_KEY.allowRegistration,
|
|
value: String(req.allowRegistration)
|
|
});
|
|
}
|
|
|
|
if (upserts.length === 0) {
|
|
return apiOk(null);
|
|
}
|
|
|
|
try {
|
|
await Promise.all(
|
|
upserts.map((item) =>
|
|
prisma.systemConfig.upsert({
|
|
where: { key: item.key },
|
|
create: { key: item.key, value: item.value },
|
|
update: { value: item.value }
|
|
})
|
|
)
|
|
);
|
|
|
|
// 立即失效缓存,确保下次请求读到最新配置
|
|
invalidateSysConfigCache();
|
|
|
|
logger.info("system_config_updated", {
|
|
updatedKeys: upserts.map((u) => u.key)
|
|
});
|
|
|
|
return apiOk(null);
|
|
} catch (error) {
|
|
logger.error("system_config_update_failed", {
|
|
error: toSafeLogError(error)
|
|
});
|
|
setResponseStatus(event, 500);
|
|
return apiErr(500, "服务器内部错误");
|
|
}
|
|
});
|