// 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 => 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(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, "服务器内部错误"); } });