Spaces:
Sleeping
Sleeping
File size: 6,947 Bytes
c6dedd5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | 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) });
}
}
|