mirror of
https://github.com/7836246/cursor2api.git
synced 2026-09-03 07:20:02 +08:00
feat: config.yaml 热重载支持(修改后无需重启服务)
- 重构 config.ts:提取 parseYamlConfig / applyEnvOverrides 为可复用函数 - 新增 initConfigWatcher:fs.watch + 500ms 防抖监听 config.yaml - 端口变更仅警告(需重启),其余字段下一次请求即生效 - 中间件改用 getConfig() 实时读取,确保 auth_tokens 等热更新生效 - 新增 onConfigReload 回调机制、detectChanges 变更检测 - 优雅关闭:SIGTERM/SIGINT 时停止文件监听 - 环境变量覆盖始终保持最高优先级
This commit is contained in:
408
src/config.ts
408
src/config.ts
@@ -1,14 +1,158 @@
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { readFileSync, existsSync, watch, type FSWatcher } from 'fs';
|
||||
import { parse as parseYaml } from 'yaml';
|
||||
import type { AppConfig } from './types.js';
|
||||
|
||||
let config: AppConfig;
|
||||
let watcher: FSWatcher | null = null;
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
export function getConfig(): AppConfig {
|
||||
if (config) return config;
|
||||
// 配置变更回调
|
||||
type ConfigReloadCallback = (newConfig: AppConfig, changes: string[]) => void;
|
||||
const reloadCallbacks: ConfigReloadCallback[] = [];
|
||||
|
||||
// 默认配置
|
||||
config = {
|
||||
/**
|
||||
* 注册配置热重载回调
|
||||
*/
|
||||
export function onConfigReload(cb: ConfigReloadCallback): void {
|
||||
reloadCallbacks.push(cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 config.yaml 解析配置(纯解析,不含环境变量覆盖)
|
||||
*/
|
||||
function parseYamlConfig(defaults: AppConfig): { config: AppConfig; raw: Record<string, unknown> | null } {
|
||||
const result = { ...defaults, fingerprint: { ...defaults.fingerprint } };
|
||||
let raw: Record<string, unknown> | null = null;
|
||||
|
||||
if (!existsSync('config.yaml')) return { config: result, raw };
|
||||
|
||||
try {
|
||||
const content = readFileSync('config.yaml', 'utf-8');
|
||||
const yaml = parseYaml(content);
|
||||
raw = yaml;
|
||||
|
||||
if (yaml.port) result.port = yaml.port;
|
||||
if (yaml.timeout) result.timeout = yaml.timeout;
|
||||
if (yaml.proxy) result.proxy = yaml.proxy;
|
||||
if (yaml.cursor_model) result.cursorModel = yaml.cursor_model;
|
||||
if (typeof yaml.max_auto_continue === 'number') result.maxAutoContinue = yaml.max_auto_continue;
|
||||
if (typeof yaml.max_history_messages === 'number') result.maxHistoryMessages = yaml.max_history_messages;
|
||||
if (yaml.fingerprint) {
|
||||
if (yaml.fingerprint.user_agent) result.fingerprint.userAgent = yaml.fingerprint.user_agent;
|
||||
}
|
||||
if (yaml.vision) {
|
||||
result.vision = {
|
||||
enabled: yaml.vision.enabled !== false,
|
||||
mode: yaml.vision.mode || 'ocr',
|
||||
baseUrl: yaml.vision.base_url || 'https://api.openai.com/v1/chat/completions',
|
||||
apiKey: yaml.vision.api_key || '',
|
||||
model: yaml.vision.model || 'gpt-4o-mini',
|
||||
proxy: yaml.vision.proxy || undefined,
|
||||
};
|
||||
}
|
||||
// ★ API 鉴权 token
|
||||
if (yaml.auth_tokens) {
|
||||
result.authTokens = Array.isArray(yaml.auth_tokens)
|
||||
? yaml.auth_tokens.map(String)
|
||||
: String(yaml.auth_tokens).split(',').map((s: string) => s.trim()).filter(Boolean);
|
||||
}
|
||||
// ★ 历史压缩配置
|
||||
if (yaml.compression !== undefined) {
|
||||
const c = yaml.compression;
|
||||
result.compression = {
|
||||
enabled: c.enabled !== false, // 默认启用
|
||||
level: [1, 2, 3].includes(c.level) ? c.level : 1,
|
||||
keepRecent: typeof c.keep_recent === 'number' ? c.keep_recent : 10,
|
||||
earlyMsgMaxChars: typeof c.early_msg_max_chars === 'number' ? c.early_msg_max_chars : 4000,
|
||||
};
|
||||
}
|
||||
// ★ Thinking 开关(最高优先级)
|
||||
if (yaml.thinking !== undefined) {
|
||||
result.thinking = {
|
||||
enabled: yaml.thinking.enabled !== false, // 默认启用
|
||||
};
|
||||
}
|
||||
// ★ 日志文件持久化
|
||||
if (yaml.logging !== undefined) {
|
||||
result.logging = {
|
||||
file_enabled: yaml.logging.file_enabled === true, // 默认关闭
|
||||
dir: yaml.logging.dir || './logs',
|
||||
max_days: typeof yaml.logging.max_days === 'number' ? yaml.logging.max_days : 7,
|
||||
};
|
||||
}
|
||||
// ★ 工具处理配置
|
||||
if (yaml.tools !== undefined) {
|
||||
const t = yaml.tools;
|
||||
const validModes = ['compact', 'full', 'names_only'];
|
||||
result.tools = {
|
||||
schemaMode: validModes.includes(t.schema_mode) ? t.schema_mode : 'full',
|
||||
descriptionMaxLength: typeof t.description_max_length === 'number' ? t.description_max_length : 0,
|
||||
includeOnly: Array.isArray(t.include_only) ? t.include_only.map(String) : undefined,
|
||||
exclude: Array.isArray(t.exclude) ? t.exclude.map(String) : undefined,
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Config] 读取 config.yaml 失败:', e);
|
||||
}
|
||||
|
||||
return { config: result, raw };
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用环境变量覆盖(环境变量优先级最高,不受热重载影响)
|
||||
*/
|
||||
function applyEnvOverrides(cfg: AppConfig): void {
|
||||
if (process.env.PORT) cfg.port = parseInt(process.env.PORT);
|
||||
if (process.env.TIMEOUT) cfg.timeout = parseInt(process.env.TIMEOUT);
|
||||
if (process.env.PROXY) cfg.proxy = process.env.PROXY;
|
||||
if (process.env.CURSOR_MODEL) cfg.cursorModel = process.env.CURSOR_MODEL;
|
||||
if (process.env.MAX_AUTO_CONTINUE !== undefined) cfg.maxAutoContinue = parseInt(process.env.MAX_AUTO_CONTINUE);
|
||||
if (process.env.MAX_HISTORY_MESSAGES !== undefined) cfg.maxHistoryMessages = parseInt(process.env.MAX_HISTORY_MESSAGES);
|
||||
if (process.env.AUTH_TOKEN) {
|
||||
cfg.authTokens = process.env.AUTH_TOKEN.split(',').map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
// 压缩环境变量覆盖
|
||||
if (process.env.COMPRESSION_ENABLED !== undefined) {
|
||||
if (!cfg.compression) cfg.compression = { enabled: false, level: 1, keepRecent: 10, earlyMsgMaxChars: 4000 };
|
||||
cfg.compression.enabled = process.env.COMPRESSION_ENABLED !== 'false' && process.env.COMPRESSION_ENABLED !== '0';
|
||||
}
|
||||
if (process.env.COMPRESSION_LEVEL) {
|
||||
if (!cfg.compression) cfg.compression = { enabled: false, level: 1, keepRecent: 10, earlyMsgMaxChars: 4000 };
|
||||
const lvl = parseInt(process.env.COMPRESSION_LEVEL);
|
||||
if (lvl >= 1 && lvl <= 3) cfg.compression.level = lvl as 1 | 2 | 3;
|
||||
}
|
||||
// Thinking 环境变量覆盖(最高优先级)
|
||||
if (process.env.THINKING_ENABLED !== undefined) {
|
||||
cfg.thinking = {
|
||||
enabled: process.env.THINKING_ENABLED !== 'false' && process.env.THINKING_ENABLED !== '0',
|
||||
};
|
||||
}
|
||||
// Logging 环境变量覆盖
|
||||
if (process.env.LOG_FILE_ENABLED !== undefined) {
|
||||
if (!cfg.logging) cfg.logging = { file_enabled: false, dir: './logs', max_days: 7 };
|
||||
cfg.logging.file_enabled = process.env.LOG_FILE_ENABLED === 'true' || process.env.LOG_FILE_ENABLED === '1';
|
||||
}
|
||||
if (process.env.LOG_DIR) {
|
||||
if (!cfg.logging) cfg.logging = { file_enabled: false, dir: './logs', max_days: 7 };
|
||||
cfg.logging.dir = process.env.LOG_DIR;
|
||||
}
|
||||
|
||||
// 从 base64 FP 环境变量解析指纹
|
||||
if (process.env.FP) {
|
||||
try {
|
||||
const fp = JSON.parse(Buffer.from(process.env.FP, 'base64').toString());
|
||||
if (fp.userAgent) cfg.fingerprint.userAgent = fp.userAgent;
|
||||
} catch (e) {
|
||||
console.warn('[Config] 解析 FP 环境变量失败:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建默认配置
|
||||
*/
|
||||
function defaultConfig(): AppConfig {
|
||||
return {
|
||||
port: 3010,
|
||||
timeout: 120,
|
||||
cursorModel: 'anthropic/claude-sonnet-4.6',
|
||||
@@ -18,122 +162,150 @@ export function getConfig(): AppConfig {
|
||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 从 config.yaml 加载
|
||||
if (existsSync('config.yaml')) {
|
||||
try {
|
||||
const raw = readFileSync('config.yaml', 'utf-8');
|
||||
const yaml = parseYaml(raw);
|
||||
if (yaml.port) config.port = yaml.port;
|
||||
if (yaml.timeout) config.timeout = yaml.timeout;
|
||||
if (yaml.proxy) config.proxy = yaml.proxy;
|
||||
if (yaml.cursor_model) config.cursorModel = yaml.cursor_model;
|
||||
if (typeof yaml.max_auto_continue === 'number') config.maxAutoContinue = yaml.max_auto_continue;
|
||||
if (typeof yaml.max_history_messages === 'number') config.maxHistoryMessages = yaml.max_history_messages;
|
||||
if (yaml.fingerprint) {
|
||||
if (yaml.fingerprint.user_agent) config.fingerprint.userAgent = yaml.fingerprint.user_agent;
|
||||
}
|
||||
if (yaml.vision) {
|
||||
config.vision = {
|
||||
enabled: yaml.vision.enabled !== false,
|
||||
mode: yaml.vision.mode || 'ocr',
|
||||
baseUrl: yaml.vision.base_url || 'https://api.openai.com/v1/chat/completions',
|
||||
apiKey: yaml.vision.api_key || '',
|
||||
model: yaml.vision.model || 'gpt-4o-mini',
|
||||
proxy: yaml.vision.proxy || undefined,
|
||||
};
|
||||
}
|
||||
// ★ API 鉴权 token
|
||||
if (yaml.auth_tokens) {
|
||||
config.authTokens = Array.isArray(yaml.auth_tokens)
|
||||
? yaml.auth_tokens.map(String)
|
||||
: String(yaml.auth_tokens).split(',').map((s: string) => s.trim()).filter(Boolean);
|
||||
}
|
||||
// ★ 历史压缩配置
|
||||
if (yaml.compression !== undefined) {
|
||||
const c = yaml.compression;
|
||||
config.compression = {
|
||||
enabled: c.enabled !== false, // 默认启用
|
||||
level: [1, 2, 3].includes(c.level) ? c.level : 1,
|
||||
keepRecent: typeof c.keep_recent === 'number' ? c.keep_recent : 10,
|
||||
earlyMsgMaxChars: typeof c.early_msg_max_chars === 'number' ? c.early_msg_max_chars : 4000,
|
||||
};
|
||||
}
|
||||
// ★ Thinking 开关(最高优先级)
|
||||
if (yaml.thinking !== undefined) {
|
||||
config.thinking = {
|
||||
enabled: yaml.thinking.enabled !== false, // 默认启用
|
||||
};
|
||||
}
|
||||
// ★ 日志文件持久化
|
||||
if (yaml.logging !== undefined) {
|
||||
config.logging = {
|
||||
file_enabled: yaml.logging.file_enabled === true, // 默认关闭
|
||||
dir: yaml.logging.dir || './logs',
|
||||
max_days: typeof yaml.logging.max_days === 'number' ? yaml.logging.max_days : 7,
|
||||
};
|
||||
}
|
||||
// ★ 工具处理配置
|
||||
if (yaml.tools !== undefined) {
|
||||
const t = yaml.tools;
|
||||
const validModes = ['compact', 'full', 'names_only'];
|
||||
config.tools = {
|
||||
schemaMode: validModes.includes(t.schema_mode) ? t.schema_mode : 'full',
|
||||
descriptionMaxLength: typeof t.description_max_length === 'number' ? t.description_max_length : 0,
|
||||
includeOnly: Array.isArray(t.include_only) ? t.include_only.map(String) : undefined,
|
||||
exclude: Array.isArray(t.exclude) ? t.exclude.map(String) : undefined,
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Config] 读取 config.yaml 失败:', e);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 检测配置变更并返回变更描述列表
|
||||
*/
|
||||
function detectChanges(oldCfg: AppConfig, newCfg: AppConfig): string[] {
|
||||
const changes: string[] = [];
|
||||
|
||||
// 环境变量覆盖
|
||||
if (process.env.PORT) config.port = parseInt(process.env.PORT);
|
||||
if (process.env.TIMEOUT) config.timeout = parseInt(process.env.TIMEOUT);
|
||||
if (process.env.PROXY) config.proxy = process.env.PROXY;
|
||||
if (process.env.CURSOR_MODEL) config.cursorModel = process.env.CURSOR_MODEL;
|
||||
if (process.env.MAX_AUTO_CONTINUE !== undefined) config.maxAutoContinue = parseInt(process.env.MAX_AUTO_CONTINUE);
|
||||
if (process.env.MAX_HISTORY_MESSAGES !== undefined) config.maxHistoryMessages = parseInt(process.env.MAX_HISTORY_MESSAGES);
|
||||
if (process.env.AUTH_TOKEN) {
|
||||
config.authTokens = process.env.AUTH_TOKEN.split(',').map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
// 压缩环境变量覆盖
|
||||
if (process.env.COMPRESSION_ENABLED !== undefined) {
|
||||
if (!config.compression) config.compression = { enabled: false, level: 1, keepRecent: 10, earlyMsgMaxChars: 4000 };
|
||||
config.compression.enabled = process.env.COMPRESSION_ENABLED !== 'false' && process.env.COMPRESSION_ENABLED !== '0';
|
||||
}
|
||||
if (process.env.COMPRESSION_LEVEL) {
|
||||
if (!config.compression) config.compression = { enabled: false, level: 1, keepRecent: 10, earlyMsgMaxChars: 4000 };
|
||||
const lvl = parseInt(process.env.COMPRESSION_LEVEL);
|
||||
if (lvl >= 1 && lvl <= 3) config.compression.level = lvl as 1 | 2 | 3;
|
||||
}
|
||||
// Thinking 环境变量覆盖(最高优先级)
|
||||
if (process.env.THINKING_ENABLED !== undefined) {
|
||||
config.thinking = {
|
||||
enabled: process.env.THINKING_ENABLED !== 'false' && process.env.THINKING_ENABLED !== '0',
|
||||
};
|
||||
}
|
||||
// Logging 环境变量覆盖
|
||||
if (process.env.LOG_FILE_ENABLED !== undefined) {
|
||||
if (!config.logging) config.logging = { file_enabled: false, dir: './logs', max_days: 7 };
|
||||
config.logging.file_enabled = process.env.LOG_FILE_ENABLED === 'true' || process.env.LOG_FILE_ENABLED === '1';
|
||||
}
|
||||
if (process.env.LOG_DIR) {
|
||||
if (!config.logging) config.logging = { file_enabled: false, dir: './logs', max_days: 7 };
|
||||
config.logging.dir = process.env.LOG_DIR;
|
||||
}
|
||||
if (oldCfg.port !== newCfg.port) changes.push(`port: ${oldCfg.port} → ${newCfg.port}`);
|
||||
if (oldCfg.timeout !== newCfg.timeout) changes.push(`timeout: ${oldCfg.timeout} → ${newCfg.timeout}`);
|
||||
if (oldCfg.proxy !== newCfg.proxy) changes.push(`proxy: ${oldCfg.proxy || '(none)'} → ${newCfg.proxy || '(none)'}`);
|
||||
if (oldCfg.cursorModel !== newCfg.cursorModel) changes.push(`cursor_model: ${oldCfg.cursorModel} → ${newCfg.cursorModel}`);
|
||||
if (oldCfg.maxAutoContinue !== newCfg.maxAutoContinue) changes.push(`max_auto_continue: ${oldCfg.maxAutoContinue} → ${newCfg.maxAutoContinue}`);
|
||||
if (oldCfg.maxHistoryMessages !== newCfg.maxHistoryMessages) changes.push(`max_history_messages: ${oldCfg.maxHistoryMessages} → ${newCfg.maxHistoryMessages}`);
|
||||
|
||||
// 从 base64 FP 环境变量解析指纹
|
||||
if (process.env.FP) {
|
||||
try {
|
||||
const fp = JSON.parse(Buffer.from(process.env.FP, 'base64').toString());
|
||||
if (fp.userAgent) config.fingerprint.userAgent = fp.userAgent;
|
||||
} catch (e) {
|
||||
console.warn('[Config] 解析 FP 环境变量失败:', e);
|
||||
}
|
||||
}
|
||||
// auth_tokens
|
||||
const oldTokens = (oldCfg.authTokens || []).join(',');
|
||||
const newTokens = (newCfg.authTokens || []).join(',');
|
||||
if (oldTokens !== newTokens) changes.push(`auth_tokens: ${oldCfg.authTokens?.length || 0} → ${newCfg.authTokens?.length || 0} token(s)`);
|
||||
|
||||
// thinking
|
||||
if (JSON.stringify(oldCfg.thinking) !== JSON.stringify(newCfg.thinking)) changes.push(`thinking: ${JSON.stringify(oldCfg.thinking)} → ${JSON.stringify(newCfg.thinking)}`);
|
||||
|
||||
// vision
|
||||
if (JSON.stringify(oldCfg.vision) !== JSON.stringify(newCfg.vision)) changes.push('vision: (changed)');
|
||||
|
||||
// compression
|
||||
if (JSON.stringify(oldCfg.compression) !== JSON.stringify(newCfg.compression)) changes.push('compression: (changed)');
|
||||
|
||||
// logging
|
||||
if (JSON.stringify(oldCfg.logging) !== JSON.stringify(newCfg.logging)) changes.push('logging: (changed)');
|
||||
|
||||
// tools
|
||||
if (JSON.stringify(oldCfg.tools) !== JSON.stringify(newCfg.tools)) changes.push('tools: (changed)');
|
||||
|
||||
// fingerprint
|
||||
if (oldCfg.fingerprint.userAgent !== newCfg.fingerprint.userAgent) changes.push('fingerprint: (changed)');
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前配置(所有模块统一通过此函数获取最新配置)
|
||||
*/
|
||||
export function getConfig(): AppConfig {
|
||||
if (config) return config;
|
||||
|
||||
// 首次加载
|
||||
const defaults = defaultConfig();
|
||||
const { config: parsed } = parseYamlConfig(defaults);
|
||||
applyEnvOverrides(parsed);
|
||||
config = parsed;
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 config.yaml 文件监听,实现热重载
|
||||
*
|
||||
* 端口变更仅记录警告(需重启生效),其他字段下一次请求即生效。
|
||||
* 环境变量覆盖始终保持最高优先级,不受热重载影响。
|
||||
*/
|
||||
export function initConfigWatcher(): void {
|
||||
if (watcher) return; // 避免重复初始化
|
||||
if (!existsSync('config.yaml')) {
|
||||
console.log('[Config] config.yaml 不存在,跳过热重载监听');
|
||||
return;
|
||||
}
|
||||
|
||||
const DEBOUNCE_MS = 500;
|
||||
|
||||
watcher = watch('config.yaml', (eventType) => {
|
||||
if (eventType !== 'change') return;
|
||||
|
||||
// 防抖:多次快速写入只触发一次重载
|
||||
if (debounceTimer) clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => {
|
||||
try {
|
||||
if (!existsSync('config.yaml')) {
|
||||
console.warn('[Config] ⚠️ config.yaml 已被删除,保持当前配置');
|
||||
return;
|
||||
}
|
||||
|
||||
const oldConfig = config;
|
||||
const oldPort = oldConfig.port;
|
||||
|
||||
// 重新解析 YAML + 环境变量覆盖
|
||||
const defaults = defaultConfig();
|
||||
const { config: newConfig } = parseYamlConfig(defaults);
|
||||
applyEnvOverrides(newConfig);
|
||||
|
||||
// 检测变更
|
||||
const changes = detectChanges(oldConfig, newConfig);
|
||||
if (changes.length === 0) return; // 无实质变更
|
||||
|
||||
// ★ 端口变更特殊处理:仅警告,不生效
|
||||
if (newConfig.port !== oldPort) {
|
||||
console.warn(`[Config] ⚠️ 检测到 port 变更 (${oldPort} → ${newConfig.port}),端口变更需要重启服务才能生效`);
|
||||
newConfig.port = oldPort; // 保持原端口
|
||||
}
|
||||
|
||||
// 替换全局配置对象(下一次 getConfig() 调用即返回新配置)
|
||||
config = newConfig;
|
||||
|
||||
console.log(`[Config] 🔄 config.yaml 已热重载,${changes.length} 项变更:`);
|
||||
changes.forEach(c => console.log(` └─ ${c}`));
|
||||
|
||||
// 触发回调
|
||||
for (const cb of reloadCallbacks) {
|
||||
try {
|
||||
cb(newConfig, changes);
|
||||
} catch (e) {
|
||||
console.warn('[Config] 热重载回调执行失败:', e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Config] ❌ 热重载失败,保持当前配置:', e);
|
||||
}
|
||||
}, DEBOUNCE_MS);
|
||||
});
|
||||
|
||||
// 异常处理:watcher 挂掉后尝试重建
|
||||
watcher.on('error', (err) => {
|
||||
console.error('[Config] ❌ 文件监听异常:', err);
|
||||
watcher = null;
|
||||
// 2 秒后尝试重新建立监听
|
||||
setTimeout(() => {
|
||||
console.log('[Config] 🔄 尝试重新建立 config.yaml 监听...');
|
||||
initConfigWatcher();
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
console.log('[Config] 👁️ 正在监听 config.yaml 变更(热重载已启用)');
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止文件监听(用于优雅关闭)
|
||||
*/
|
||||
export function stopConfigWatcher(): void {
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = null;
|
||||
}
|
||||
if (watcher) {
|
||||
watcher.close();
|
||||
watcher = null;
|
||||
}
|
||||
}
|
||||
|
||||
19
src/index.ts
19
src/index.ts
@@ -8,7 +8,7 @@
|
||||
import 'dotenv/config';
|
||||
import { createRequire } from 'module';
|
||||
import express from 'express';
|
||||
import { getConfig } from './config.js';
|
||||
import { getConfig, initConfigWatcher, stopConfigWatcher } from './config.js';
|
||||
import { handleMessages, listModels, countTokens } from './handler.js';
|
||||
import { handleOpenAIChatCompletions, handleOpenAIResponses } from './openai-handler.js';
|
||||
import { serveLogViewer, apiGetLogs, apiGetRequests, apiGetStats, apiGetPayload, apiLogsStream, serveLogViewerLogin, apiClearLogs } from './log-viewer.js';
|
||||
@@ -42,7 +42,7 @@ app.use('/public', express.static('public'));
|
||||
|
||||
// ★ 日志查看器鉴权中间件:配置了 authTokens 时需要验证
|
||||
const logViewerAuth = (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const tokens = config.authTokens;
|
||||
const tokens = getConfig().authTokens;
|
||||
if (!tokens || tokens.length === 0) return next(); // 未配置 token 则放行
|
||||
|
||||
// 支持多种传入方式: query ?token=xxx, Authorization header, x-api-key header
|
||||
@@ -77,7 +77,7 @@ app.use((req, res, next) => {
|
||||
if (req.method === 'GET' || req.path === '/health') {
|
||||
return next();
|
||||
}
|
||||
const tokens = config.authTokens;
|
||||
const tokens = getConfig().authTokens;
|
||||
if (!tokens || tokens.length === 0) {
|
||||
return next(); // 未配置 token 则全部放行
|
||||
}
|
||||
@@ -173,4 +173,17 @@ app.listen(config.port, () => {
|
||||
console.log(` ├─ Logging: ${logPersist}`);
|
||||
console.log(` └─ Logs: \x1b[35mhttp://localhost:${config.port}/logs\x1b[0m`);
|
||||
console.log('');
|
||||
|
||||
// ★ 启动 config.yaml 热重载监听
|
||||
initConfigWatcher();
|
||||
});
|
||||
|
||||
// ★ 优雅关闭:停止文件监听
|
||||
process.on('SIGTERM', () => {
|
||||
stopConfigWatcher();
|
||||
process.exit(0);
|
||||
});
|
||||
process.on('SIGINT', () => {
|
||||
stopConfigWatcher();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user