feat: 重构配置管理,使用数据库替代环境变量,优化系统配置读取

This commit is contained in:
2026-05-23 15:37:58 +08:00
parent ea95886cfc
commit 92b063c4c4
10 changed files with 249 additions and 129 deletions
+96
View File
@@ -0,0 +1,96 @@
// server/utils/sysConfig.ts - 从数据库读取系统配置,带 60 秒 TTL 内存缓存
// 替代原 env.ts 中的 OpenAI 配置项,配置以 DB 为准,管理员可通过后台修改
import { prisma } from "~~/server/utils/db";
/** 系统配置字段,对应 system_config 表中的各 key */
export interface SysConfig {
/** OpenAI 或兼容 API Key,仅服务端使用,不能返回给前端 */
openAiApiKey: string;
/** OpenAI 兼容 API base URL,不含结尾斜杠 */
openAiApiBase: string;
/** Chat Completions 使用的模型名 */
openAiModel: string;
/** 单次回答最大 token 数 */
maxTokens: number;
/** 模型采样温度,越低越稳定 */
temperature: number;
/** 单行 SSE data 最大字节数,防止异常流无限堆内存 */
maxSseLineBytes: number;
}
// system_config 表 key 字段的常量映射
const CONFIG_KEYS = {
openAiApiKey: "openai_api_key",
openAiApiBase: "openai_api_base",
openAiModel: "openai_model",
maxTokens: "max_tokens",
temperature: "temperature",
maxSseLineBytes: "openai_stream_max_sse_line_bytes"
} as const;
// 硬编码默认值,与原 env.ts 的 fallback 保持一致
const DEFAULTS: SysConfig = {
openAiApiKey: "",
openAiApiBase: "https://api.openai.com/v1",
openAiModel: "gpt-3.5-turbo",
maxTokens: 500,
temperature: 0.7,
maxSseLineBytes: 1_048_576
};
// 内存缓存:减少每请求查 DB 的开销
let _cachedConfig: SysConfig | null = null;
let _cacheExpiresAt = 0;
const CACHE_TTL_MS = 60_000;
/** baseURL 不能带结尾斜杠,后续拼 `/chat/completions` 时格式才正确 */
const normalizeBaseUrl = (url: string) => url.replace(/\/+$/u, "");
/**
* 读取系统配置
*
* 60 秒内命中内存缓存直接返回,过期后重新查 DB;
* 管理员更新配置后可调用 invalidateSysConfigCache() 主动失效
*/
export const getSysConfig = async (): Promise<SysConfig> => {
const now = Date.now();
if (_cachedConfig && now < _cacheExpiresAt) {
return _cachedConfig;
}
const rows = await prisma.systemConfig.findMany({
select: { key: true, value: true }
});
const map = new Map(rows.map((r) => [r.key, r.value]));
const str = (key: string, fallback: string) => map.get(key) ?? fallback;
const int = (key: string, fallback: number) => {
const v = Number.parseInt(map.get(key) ?? "", 10);
return Number.isNaN(v) ? fallback : v;
};
const float = (key: string, fallback: number) => {
const v = Number.parseFloat(map.get(key) ?? "");
return Number.isNaN(v) ? fallback : v;
};
const config: SysConfig = {
openAiApiKey: str(CONFIG_KEYS.openAiApiKey, DEFAULTS.openAiApiKey),
openAiApiBase: normalizeBaseUrl(
str(CONFIG_KEYS.openAiApiBase, DEFAULTS.openAiApiBase)
),
openAiModel: str(CONFIG_KEYS.openAiModel, DEFAULTS.openAiModel),
maxTokens: int(CONFIG_KEYS.maxTokens, DEFAULTS.maxTokens),
temperature: float(CONFIG_KEYS.temperature, DEFAULTS.temperature),
maxSseLineBytes: int(CONFIG_KEYS.maxSseLineBytes, DEFAULTS.maxSseLineBytes)
};
_cachedConfig = config;
_cacheExpiresAt = now + CACHE_TTL_MS;
return config;
};
/** 主动使缓存失效,管理员更新系统配置后调用,下次请求将重新查 DB */
export const invalidateSysConfigCache = () => {
_cachedConfig = null;
_cacheExpiresAt = 0;
};