返回 ViMax
config-store.mjs
根目录 / web / config-store.mjs
1 import {readFile, rename, writeFile} from 'node:fs/promises';
2 import path from 'node:path';
3 import {parse, stringify} from 'yaml';
4
5 const SECTION_FIELDS = {
6 llm: ['model_provider', 'model', 'base_url'],
7 image: ['model', 'base_url'],
8 video: ['model', 'base_url'],
9 embedding: ['model_provider', 'model', 'base_url'],
10 reranker: ['model', 'base_url'],
11 };
12
13 export async function readAgentConfig(repoRoot) {
14 const {payload} = await loadConfig(repoRoot);
15 return publicConfig(payload);
16 }
17
18 export async function saveAgentConfig(repoRoot, input) {
19 if (!input || typeof input !== 'object' || !input.sections || typeof input.sections !== 'object') {
20 throw new Error('Configuration sections are required');
21 }
22 const {configPath, payload} = await loadConfig(repoRoot);
23 for (const [section, fields] of Object.entries(SECTION_FIELDS)) {
24 const update = input.sections[section];
25 if (!update || typeof update !== 'object') continue;
26 const current = payload[section] && typeof payload[section] === 'object' ? payload[section] : {};
27 for (const field of fields) {
28 if (!(field in update)) continue;
29 current[field] = validatedValue(update[field], `${section}.${field}`, 2_048);
30 }
31 if (typeof update.api_key === 'string' && update.api_key.trim()) {
32 current.api_key = validatedValue(update.api_key, `${section}.api_key`, 8_192);
33 }
34 payload[section] = current;
35 }
36 const temporaryPath = `${configPath}.${process.pid}.tmp`;
37 await writeFile(temporaryPath, stringify(payload, {lineWidth: 0}), {mode: 0o600});
38 await rename(temporaryPath, configPath);
39 return publicConfig(payload);
40 }
41
42 async function loadConfig(repoRoot) {
43 const configPath = path.join(repoRoot, 'configs', 'agent.local.yaml');
44 let text = '';
45 try {
46 text = await readFile(configPath, 'utf8');
47 } catch (error) {
48 if (error?.code !== 'ENOENT') throw error;
49 }
50 const payload = text ? parse(text) : {};
51 if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
52 throw new Error('configs/agent.local.yaml must be a YAML mapping');
53 }
54 return {configPath, payload};
55 }
56
57 function publicConfig(payload) {
58 const sections = {};
59 for (const [section, fields] of Object.entries(SECTION_FIELDS)) {
60 const source = payload[section] && typeof payload[section] === 'object' ? payload[section] : {};
61 const result = {};
62 for (const field of fields) result[field] = typeof source[field] === 'string' ? source[field] : '';
63 result.api_key = '';
64 result.has_api_key = Boolean(typeof source.api_key === 'string' && source.api_key.trim());
65 sections[section] = result;
66 }
67 return {sections};
68 }
69
70 function validatedValue(value, label, maxLength) {
71 if (typeof value !== 'string') throw new Error(`${label} must be a string`);
72 const normalized = value.trim();
73 if (normalized.length > maxLength) throw new Error(`${label} is too long`);
74 if (label.endsWith('.base_url') && normalized && !/^https?:\/\//i.test(normalized)) {
75 throw new Error(`${label} must use http:// or https://`);
76 }
77 return normalized;
78 }
79
79 lines Plain Text