loader.ts•4.17 kB
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { parse } from 'yaml';
import { configSchema, type Config } from './schema.js';
import { PROJECT_ROOT } from '../utils/paths.js';
/**
* 加载配置文件
* 支持环境变量替换(如 ${OPENAI_API_KEY})
* 配置文件路径优先级:
* 1. 传入的 configPath 参数(如果是相对路径,相对于项目根目录)
* 2. 默认值:项目根目录的 'config.yaml'
*/
export function loadConfig(configPath?: string): Config {
let finalPath: string;
if (configPath) {
// 如果是绝对路径,直接使用;否则相对于项目根目录
finalPath = configPath.startsWith('/') ? configPath : resolve(PROJECT_ROOT, configPath);
} else {
// 默认使用项目根目录的 config.yaml
finalPath = resolve(PROJECT_ROOT, 'config.yaml');
}
const content = readFileSync(finalPath, 'utf-8');
const config = parse(content);
// 递归替换环境变量
const resolved = replaceEnvVars(config) as Record<string, unknown>;
// 使用环境变量填充缺失的配置
const env = process.env;
if (!(resolved.llm as Record<string, unknown>)?.apiKey && env.OPENAI_API_KEY) {
resolved.llm = (resolved.llm || {}) as Record<string, unknown>;
(resolved.llm as Record<string, unknown>).apiKey = env.OPENAI_API_KEY;
}
if (!(resolved.llm as Record<string, unknown>)?.baseURL && env.OPENAI_BASE_URL) {
resolved.llm = (resolved.llm || {}) as Record<string, unknown>;
(resolved.llm as Record<string, unknown>).baseURL = env.OPENAI_BASE_URL;
}
if (!(resolved.llm as Record<string, unknown>)?.model && env.OPENAI_MODEL) {
resolved.llm = (resolved.llm || {}) as Record<string, unknown>;
(resolved.llm as Record<string, unknown>).model = env.OPENAI_MODEL;
}
if (!(resolved.embedding as Record<string, unknown>)?.baseURL && env.EMBEDDING_BASE_URL) {
resolved.embedding = (resolved.embedding || {}) as Record<string, unknown>;
(resolved.embedding as Record<string, unknown>).baseURL = env.EMBEDDING_BASE_URL;
}
if (!(resolved.embedding as Record<string, unknown>)?.model && env.EMBEDDING_MODEL) {
resolved.embedding = (resolved.embedding || {}) as Record<string, unknown>;
(resolved.embedding as Record<string, unknown>).model = env.EMBEDDING_MODEL;
}
if (!(resolved.phabricator as Record<string, unknown>)?.host && env.PHABRICATOR_HOST) {
resolved.phabricator = (resolved.phabricator || {}) as Record<string, unknown>;
(resolved.phabricator as Record<string, unknown>).host = env.PHABRICATOR_HOST;
}
if (!(resolved.phabricator as Record<string, unknown>)?.token && env.PHABRICATOR_TOKEN) {
resolved.phabricator = (resolved.phabricator || {}) as Record<string, unknown>;
(resolved.phabricator as Record<string, unknown>).token = env.PHABRICATOR_TOKEN;
}
if (!resolved.projectRoot && env.PROJECT_ROOT) {
resolved.projectRoot = env.PROJECT_ROOT;
}
// 验证配置
return configSchema.parse(resolved);
}
/**
* 递归替换环境变量
*/
function replaceEnvVars(obj: unknown): unknown {
if (typeof obj === 'string') {
// 匹配 ${VAR_NAME} 格式
const match = obj.match(/^\$\{(.+)\}$/);
if (match) {
const envKey = match[1];
const envValue = process.env[envKey];
if (envValue === undefined) {
return obj; // 保持原样,后续会用默认值
}
// 转换布尔值字符串
if (envValue === 'true') return true;
if (envValue === 'false') return false;
// 转换数字
if (/^\d+$/.test(envValue)) {
return Number(envValue);
}
// 尝试转换浮点数
const floatValue = parseFloat(envValue);
if (!isNaN(floatValue) && isFinite(floatValue)) {
return floatValue;
}
return envValue;
}
return obj;
}
if (Array.isArray(obj)) {
return obj.map((item) => replaceEnvVars(item));
}
if (obj && typeof obj === 'object') {
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj)) {
result[key] = replaceEnvVars(value);
}
return result;
}
return obj;
}