Spaces:
Running
Running
github-actions[bot]
sync: upstream b70f787 Merge pull request #84 from huangzt/feature/vue-logs-ui
c6dedd5 | import { readFileSync, writeFileSync, existsSync } from 'fs'; | |
| import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'; | |
| import type { Request, Response } from 'express'; | |
| import { getConfig } from './config.js'; | |
| /** | |
| * GET /api/config | |
| * 返回当前可热重载的配置字段(snake_case,过滤 port/proxy/auth_tokens/fingerprint/vision) | |
| */ | |
| export function apiGetConfig(_req: Request, res: Response): void { | |
| const cfg = getConfig(); | |
| res.json({ | |
| cursor_model: cfg.cursorModel, | |
| timeout: cfg.timeout, | |
| max_auto_continue: cfg.maxAutoContinue, | |
| max_history_messages: cfg.maxHistoryMessages, | |
| thinking: cfg.thinking !== undefined ? { enabled: cfg.thinking.enabled } : null, | |
| compression: { | |
| enabled: cfg.compression?.enabled ?? false, | |
| level: cfg.compression?.level ?? 1, | |
| keep_recent: cfg.compression?.keepRecent ?? 10, | |
| early_msg_max_chars: cfg.compression?.earlyMsgMaxChars ?? 4000, | |
| }, | |
| tools: { | |
| schema_mode: cfg.tools?.schemaMode ?? 'full', | |
| description_max_length: cfg.tools?.descriptionMaxLength ?? 0, | |
| passthrough: cfg.tools?.passthrough ?? false, | |
| disabled: cfg.tools?.disabled ?? false, | |
| }, | |
| sanitize_response: cfg.sanitizeEnabled, | |
| refusal_patterns: cfg.refusalPatterns ?? [], | |
| logging: cfg.logging ?? { file_enabled: false, dir: './logs', max_days: 7, persist_mode: 'summary' }, | |
| }); | |
| } | |
| /** | |
| * POST /api/config | |
| * 接收可热重载字段,合并写入 config.yaml,热重载由 fs.watch 自动触发 | |
| */ | |
| export function apiSaveConfig(req: Request, res: Response): void { | |
| const body = req.body as Record<string, unknown>; | |
| // 基本类型校验 | |
| if (body.cursor_model !== undefined && typeof body.cursor_model !== 'string') { | |
| res.status(400).json({ error: 'cursor_model must be a string' }); return; | |
| } | |
| if (body.timeout !== undefined && (typeof body.timeout !== 'number' || body.timeout <= 0)) { | |
| res.status(400).json({ error: 'timeout must be a positive number' }); return; | |
| } | |
| if (body.max_auto_continue !== undefined && typeof body.max_auto_continue !== 'number') { | |
| res.status(400).json({ error: 'max_auto_continue must be a number' }); return; | |
| } | |
| if (body.max_history_messages !== undefined && typeof body.max_history_messages !== 'number') { | |
| res.status(400).json({ error: 'max_history_messages must be a number' }); return; | |
| } | |
| try { | |
| // 读取现有 yaml(如不存在则从空对象开始) | |
| let raw: Record<string, unknown> = {}; | |
| if (existsSync('config.yaml')) { | |
| raw = (parseYaml(readFileSync('config.yaml', 'utf-8')) as Record<string, unknown>) ?? {}; | |
| } | |
| // 记录变更 | |
| const changes: string[] = []; | |
| // 合并可热重载字段 | |
| if (body.cursor_model !== undefined && body.cursor_model !== raw.cursor_model) { | |
| changes.push(`cursor_model: ${raw.cursor_model ?? '(unset)'} → ${body.cursor_model}`); | |
| raw.cursor_model = body.cursor_model; | |
| } | |
| if (body.timeout !== undefined && body.timeout !== raw.timeout) { | |
| changes.push(`timeout: ${raw.timeout ?? '(unset)'} → ${body.timeout}`); | |
| raw.timeout = body.timeout; | |
| } | |
| if (body.max_auto_continue !== undefined && body.max_auto_continue !== raw.max_auto_continue) { | |
| changes.push(`max_auto_continue: ${raw.max_auto_continue ?? '(unset)'} → ${body.max_auto_continue}`); | |
| raw.max_auto_continue = body.max_auto_continue; | |
| } | |
| if (body.max_history_messages !== undefined && body.max_history_messages !== raw.max_history_messages) { | |
| changes.push(`max_history_messages: ${raw.max_history_messages ?? '(unset)'} → ${body.max_history_messages}`); | |
| raw.max_history_messages = body.max_history_messages; | |
| } | |
| if (body.thinking !== undefined) { | |
| const t = body.thinking as { enabled: boolean | null } | null; | |
| const oldVal = JSON.stringify(raw.thinking); | |
| if (t === null || t?.enabled === null) { | |
| // null = 跟随客户端:从 yaml 中删除 thinking 节 | |
| if (raw.thinking !== undefined) { | |
| changes.push(`thinking: ${oldVal} → (跟随客户端)`); | |
| delete raw.thinking; | |
| } | |
| } else { | |
| const newVal = JSON.stringify(t); | |
| if (oldVal !== newVal) { | |
| changes.push(`thinking: ${oldVal ?? '(unset)'} → ${newVal}`); | |
| raw.thinking = t; | |
| } | |
| } | |
| } | |
| if (body.compression !== undefined) { | |
| const oldVal = JSON.stringify(raw.compression); | |
| const newVal = JSON.stringify(body.compression); | |
| if (oldVal !== newVal) { | |
| changes.push(`compression: (changed)`); | |
| raw.compression = body.compression; | |
| } | |
| } | |
| if (body.tools !== undefined) { | |
| const oldVal = JSON.stringify(raw.tools); | |
| const newVal = JSON.stringify(body.tools); | |
| if (oldVal !== newVal) { | |
| changes.push(`tools: (changed)`); | |
| raw.tools = body.tools; | |
| } | |
| } | |
| if (body.sanitize_response !== undefined && body.sanitize_response !== raw.sanitize_response) { | |
| changes.push(`sanitize_response: ${raw.sanitize_response ?? '(unset)'} → ${body.sanitize_response}`); | |
| raw.sanitize_response = body.sanitize_response; | |
| } | |
| if (body.refusal_patterns !== undefined) { | |
| const oldVal = JSON.stringify(raw.refusal_patterns); | |
| const newVal = JSON.stringify(body.refusal_patterns); | |
| if (oldVal !== newVal) { | |
| changes.push(`refusal_patterns: (changed)`); | |
| raw.refusal_patterns = body.refusal_patterns; | |
| } | |
| } | |
| if (body.logging !== undefined) { | |
| const oldVal = JSON.stringify(raw.logging); | |
| const newVal = JSON.stringify(body.logging); | |
| if (oldVal !== newVal) { | |
| changes.push(`logging: (changed)`); | |
| raw.logging = body.logging; | |
| } | |
| } | |
| if (changes.length === 0) { | |
| res.json({ ok: true, changes: [] }); | |
| return; | |
| } | |
| // 写入 config.yaml(热重载由 fs.watch 自动触发) | |
| writeFileSync('config.yaml', stringifyYaml(raw, { lineWidth: 0 }), 'utf-8'); | |
| console.log(`[Config API] ✏️ 通过 UI 更新配置,${changes.length} 项变更:`); | |
| changes.forEach(c => console.log(` └─ ${c}`)); | |
| res.json({ ok: true, changes }); | |
| } catch (e) { | |
| console.error('[Config API] 写入 config.yaml 失败:', e); | |
| res.status(500).json({ error: String(e) }); | |
| } | |
| } | |