Files
OCS_service/server/api/user/settings.put.ts
T
2026-05-23 22:23:29 +08:00

139 lines
4.4 KiB
TypeScript

// server/api/user/settings.put.ts - 更新当前用户的自定义 API 配置
//
// 流程:鉴权 → 白名单校验 → 合并并写入 user_config 单行 JSON value → 返回
// 安全边界:
// - customApiKey 为空字符串时跳过,不清空已有 key
// - 只接受声明字段,拒绝未知字段
// - 不读取或返回系统配置任何内容
// 注意:保存时使用单条 SQL upsert,避免多次写入造成连接占用
import { randomUUID } from "node:crypto";
import { readBody, setResponseStatus } from "h3";
import { Prisma } from "~~/prisma/generated/client";
import { prisma } from "~~/server/utils/db";
import { createApiLogger, toSafeLogError } from "~~/server/utils/logging";
import { apiErr, apiOk } from "~~/server/utils/response";
import {
hasUserConfigMutation,
mergeUserConfigUpdate,
normalizeUserConfigValue,
toUserConfigJson
} from "~~/server/utils/userConfig";
import type { IUserSettingsUpdateRequest } from "~~/shared/types/settings";
/** 是否是纯 JSON 对象 */
const isRecord = (v: unknown): v is Record<string, unknown> =>
typeof v === "object" && v !== null && !Array.isArray(v);
const ALLOWED_KEYS = new Set([
"useCustomConfig",
"customApiBase",
"customApiKey",
"customModel",
"customMaxTokens",
"customTemperature"
]);
/** 写入用户配置单行 JSON value */
const writeUserConfigValue = async (
userId: string,
value: Prisma.InputJsonObject
) => {
const updatedAt = new Date();
const jsonValue = JSON.stringify(value);
await prisma.$executeRaw(Prisma.sql`
INSERT INTO user_config (id, userId, value, updatedAt)
VALUES (${randomUUID()}, ${userId}, ${jsonValue}, ${updatedAt})
ON DUPLICATE KEY UPDATE value = VALUES(value), updatedAt = VALUES(updatedAt)
`);
};
export default defineEventHandler(async (event) => {
const logger = createApiLogger(event, "user.settings.put");
const userId = event.context.auth!.user.id;
const body = await readBody<unknown>(event).catch(() => null);
if (!isRecord(body)) {
setResponseStatus(event, 400);
return apiErr(400, "请求体必须是 JSON 对象");
}
const unknownKeys = Object.keys(body).filter((key) => !ALLOWED_KEYS.has(key));
if (unknownKeys.length > 0) {
setResponseStatus(event, 400);
return apiErr(400, "请求体包含不支持的字段");
}
// 白名单字段提取
const req: IUserSettingsUpdateRequest = {};
if (typeof body.useCustomConfig === "boolean") {
req.useCustomConfig = body.useCustomConfig;
}
if (typeof body.customApiBase === "string") {
req.customApiBase = body.customApiBase.trim();
}
if (typeof body.customApiKey === "string") {
req.customApiKey = body.customApiKey; // 空字符串 = 不更新,见下方逻辑
}
if (typeof body.customModel === "string") {
req.customModel = body.customModel.trim();
}
if (
typeof body.customMaxTokens === "number" &&
Number.isFinite(body.customMaxTokens)
) {
req.customMaxTokens = body.customMaxTokens;
} else if (body.customMaxTokens === null) {
req.customMaxTokens = null;
}
if (
typeof body.customTemperature === "number" &&
Number.isFinite(body.customTemperature)
) {
req.customTemperature = body.customTemperature;
} else if (body.customTemperature === null) {
req.customTemperature = null;
}
// 没有任何变更时直接返回
if (!hasUserConfigMutation(req)) {
return apiOk(null);
}
try {
const currentRow = await prisma.userConfig.findUnique({
where: { userId },
select: { value: true }
});
const current = normalizeUserConfigValue(currentRow?.value);
const next = mergeUserConfigUpdate(current, req);
const jsonValue = toUserConfigJson(next);
logger.info("user_settings_write_start", {
hasExistingRow: Boolean(currentRow),
useCustomConfig: next.useCustomConfig
});
await writeUserConfigValue(userId, jsonValue);
logger.info("user_settings_updated", {
useCustomConfig: next.useCustomConfig,
hasCustomApiBase: Boolean(next.customApiBase),
hasCustomApiKey: Boolean(next.customApiKey),
hasCustomModel: Boolean(next.customModel),
hasCustomMaxTokens: next.customMaxTokens !== null,
hasCustomTemperature: next.customTemperature !== null
});
return apiOk(null);
} catch (error) {
logger.error("user_settings_update_failed", {
error: toSafeLogError(error)
});
setResponseStatus(event, 500);
return apiErr(500, "服务器内部错误");
}
});