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) });
    }
}