| 39 | * 接收可热重载字段,合并写入 config.yaml,热重载由 fs.watch 自动触发 |
| 40 | */ |
| 41 | export function apiSaveConfig(req: Request, res: Response): void { |
| 42 | const body = req.body as Record<string, unknown>; |
| 43 | |
| 44 | // 基本类型校验 |
| 45 | if (body.cursor_model !== undefined && typeof body.cursor_model !== 'string') { |
| 46 | res.status(400).json({ error: 'cursor_model must be a string' }); return; |
| 47 | } |
| 48 | if (body.timeout !== undefined && (typeof body.timeout !== 'number' || body.timeout <= 0)) { |
| 49 | res.status(400).json({ error: 'timeout must be a positive number' }); return; |
| 50 | } |
| 51 | if (body.max_auto_continue !== undefined && typeof body.max_auto_continue !== 'number') { |
| 52 | res.status(400).json({ error: 'max_auto_continue must be a number' }); return; |
| 53 | } |
| 54 | if (body.max_history_messages !== undefined && typeof body.max_history_messages !== 'number') { |
| 55 | res.status(400).json({ error: 'max_history_messages must be a number' }); return; |
| 56 | } |
| 57 | if (body.max_history_tokens !== undefined && typeof body.max_history_tokens !== 'number') { |
| 58 | res.status(400).json({ error: 'max_history_tokens must be a number' }); return; |
| 59 | } |
| 60 | |
| 61 | try { |
| 62 | // 读取现有 yaml(如不存在则从空对象开始) |
| 63 | let raw: Record<string, unknown> = {}; |
| 64 | if (existsSync('config.yaml')) { |
| 65 | raw = (parseYaml(readFileSync('config.yaml', 'utf-8')) as Record<string, unknown>) ?? {}; |
| 66 | } |
| 67 | |
| 68 | // 记录变更 |
| 69 | const changes: string[] = []; |
| 70 | |
| 71 | // 合并可热重载字段 |
| 72 | if (body.cursor_model !== undefined && body.cursor_model !== raw.cursor_model) { |
| 73 | changes.push(`cursor_model: ${raw.cursor_model ?? '(unset)'} → ${body.cursor_model}`); |
| 74 | raw.cursor_model = body.cursor_model; |
| 75 | } |
| 76 | if (body.timeout !== undefined && body.timeout !== raw.timeout) { |
| 77 | changes.push(`timeout: ${raw.timeout ?? '(unset)'} → ${body.timeout}`); |
| 78 | raw.timeout = body.timeout; |
| 79 | } |
| 80 | if (body.max_auto_continue !== undefined && body.max_auto_continue !== raw.max_auto_continue) { |
| 81 | changes.push(`max_auto_continue: ${raw.max_auto_continue ?? '(unset)'} → ${body.max_auto_continue}`); |
| 82 | raw.max_auto_continue = body.max_auto_continue; |
| 83 | } |
| 84 | if (body.max_history_messages !== undefined && body.max_history_messages !== raw.max_history_messages) { |
| 85 | changes.push(`max_history_messages: ${raw.max_history_messages ?? '(unset)'} → ${body.max_history_messages}`); |
| 86 | raw.max_history_messages = body.max_history_messages; |
| 87 | } |
| 88 | if (body.max_history_tokens !== undefined && body.max_history_tokens !== raw.max_history_tokens) { |
| 89 | changes.push(`max_history_tokens: ${raw.max_history_tokens ?? '(unset)'} → ${body.max_history_tokens}`); |
| 90 | raw.max_history_tokens = body.max_history_tokens; |
| 91 | } |
| 92 | if (body.thinking !== undefined) { |
| 93 | const t = body.thinking as { enabled: boolean | null } | null; |
| 94 | const oldVal = JSON.stringify(raw.thinking); |
| 95 | if (t === null || t?.enabled === null) { |
| 96 | // null = 跟随客户端:从 yaml 中删除 thinking 节 |
| 97 | if (raw.thinking !== undefined) { |
| 98 | changes.push(`thinking: ${oldVal} → (跟随客户端)`); |