From e49f2fbbe8203ad474fa07950b37fbdaa38a54d4 Mon Sep 17 00:00:00 2001 From: digua Date: Mon, 11 May 2026 19:55:33 +0800 Subject: [PATCH] feat(config): create @openchatlab/config package Phase 1.6: configuration management with TOML/JSON file support, CHATLAB_* environment variable overrides, and Zod schema validation. Config file location: ~/.chatlab/config.toml --- packages/config/package.json | 12 ++++ packages/config/src/index.ts | 9 +++ packages/config/src/loader.ts | 126 ++++++++++++++++++++++++++++++++++ packages/config/src/schema.ts | 40 +++++++++++ pnpm-lock.yaml | 25 +++++++ tsconfig.node.json | 3 +- 6 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 packages/config/package.json create mode 100644 packages/config/src/index.ts create mode 100644 packages/config/src/loader.ts create mode 100644 packages/config/src/schema.ts diff --git a/packages/config/package.json b/packages/config/package.json new file mode 100644 index 00000000..e7f9b5ba --- /dev/null +++ b/packages/config/package.json @@ -0,0 +1,12 @@ +{ + "name": "@openchatlab/config", + "version": "0.0.0", + "private": true, + "description": "ChatLab 配置读取层:TOML/JSON 文件 + 环境变量 + Zod 校验", + "main": "./src/index.ts", + "types": "./src/index.ts", + "dependencies": { + "smol-toml": "^1.3.1", + "zod": "^3.24.4" + } +} diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts new file mode 100644 index 00000000..2f6467db --- /dev/null +++ b/packages/config/src/index.ts @@ -0,0 +1,9 @@ +/** + * @openchatlab/config + * + * ChatLab 配置管理:TOML/JSON 文件读取 + CHATLAB_* 环境变量覆盖 + Zod 校验。 + */ + +export { loadConfig, getConfigPath, getConfigDir } from './loader' +export { configSchema } from './schema' +export type { ChatLabConfig, LlmConfig, DataConfig, ApiConfig, LocaleConfig } from './schema' diff --git a/packages/config/src/loader.ts b/packages/config/src/loader.ts new file mode 100644 index 00000000..b8f87d3e --- /dev/null +++ b/packages/config/src/loader.ts @@ -0,0 +1,126 @@ +/** + * 配置加载器 + * + * 优先级(从高到低): + * 1. CHATLAB_* 环境变量 + * 2. ~/.chatlab/config.toml(或 config.json) + * 3. Zod schema 默认值 + */ + +import * as fs from 'fs' +import * as os from 'os' +import * as path from 'path' +import { parse as parseToml } from 'smol-toml' +import { configSchema, type ChatLabConfig } from './schema' + +const CONFIG_DIR = path.join(os.homedir(), '.chatlab') +const CONFIG_TOML = path.join(CONFIG_DIR, 'config.toml') +const CONFIG_JSON = path.join(CONFIG_DIR, 'config.json') + +/** + * 加载完整配置 + */ +export function loadConfig(overrides?: Partial): ChatLabConfig { + const fileConfig = loadConfigFile() + const envConfig = loadEnvConfig() + + const merged = deepMerge(deepMerge(fileConfig, envConfig), overrides ?? {}) + return configSchema.parse(merged) +} + +/** + * 获取配置文件路径(返回实际存在的路径或默认 TOML 路径) + */ +export function getConfigPath(): string { + if (fs.existsSync(CONFIG_TOML)) return CONFIG_TOML + if (fs.existsSync(CONFIG_JSON)) return CONFIG_JSON + return CONFIG_TOML +} + +/** + * 获取配置目录路径 + */ +export function getConfigDir(): string { + return CONFIG_DIR +} + +/** + * 从文件加载配置 + */ +function loadConfigFile(): Record { + if (fs.existsSync(CONFIG_TOML)) { + try { + const content = fs.readFileSync(CONFIG_TOML, 'utf-8') + return parseToml(content) as Record + } catch (err) { + console.warn(`[Config] Failed to parse ${CONFIG_TOML}:`, err) + } + } + + if (fs.existsSync(CONFIG_JSON)) { + try { + const content = fs.readFileSync(CONFIG_JSON, 'utf-8') + return JSON.parse(content) as Record + } catch (err) { + console.warn(`[Config] Failed to parse ${CONFIG_JSON}:`, err) + } + } + + return {} +} + +/** + * 从 CHATLAB_* 环境变量加载配置 + * + * 映射规则: + * - CHATLAB_DATA_DIR -> data.dir + * - CHATLAB_API_PORT -> api.port + * - CHATLAB_API_HOST -> api.host + * - CHATLAB_LLM_PROVIDER -> llm.provider + * - CHATLAB_LLM_API_KEY -> llm.api_key + * - CHATLAB_LLM_MODEL -> llm.model + * - CHATLAB_LLM_BASE_URL -> llm.base_url + * - CHATLAB_LOCALE_LANG -> locale.lang + */ +function loadEnvConfig(): Record { + const result: Record> = {} + + const envMap: Array<{ env: string; section: string; key: string; transform?: (v: string) => unknown }> = [ + { env: 'CHATLAB_DATA_DIR', section: 'data', key: 'dir' }, + { env: 'CHATLAB_API_PORT', section: 'api', key: 'port', transform: (v) => parseInt(v, 10) }, + { env: 'CHATLAB_API_HOST', section: 'api', key: 'host' }, + { env: 'CHATLAB_LLM_PROVIDER', section: 'llm', key: 'provider' }, + { env: 'CHATLAB_LLM_API_KEY', section: 'llm', key: 'api_key' }, + { env: 'CHATLAB_LLM_MODEL', section: 'llm', key: 'model' }, + { env: 'CHATLAB_LLM_BASE_URL', section: 'llm', key: 'base_url' }, + { env: 'CHATLAB_LOCALE_LANG', section: 'locale', key: 'lang' }, + ] + + for (const { env, section, key, transform } of envMap) { + const value = process.env[env] + if (value !== undefined && value !== '') { + if (!result[section]) result[section] = {} + result[section][key] = transform ? transform(value) : value + } + } + + return result +} + +/** + * 简单的深度合并(仅处理纯对象,不处理数组) + */ +function deepMerge(base: Record, override: Record): Record { + const result = { ...base } + for (const [key, value] of Object.entries(override)) { + if (value !== undefined && value !== null && typeof value === 'object' && !Array.isArray(value)) { + result[key] = deepMerge( + (result[key] as Record) ?? {}, + value as Record + ) + } else if (value !== undefined) { + result[key] = value + } + } + return result +} diff --git a/packages/config/src/schema.ts b/packages/config/src/schema.ts new file mode 100644 index 00000000..c917049d --- /dev/null +++ b/packages/config/src/schema.ts @@ -0,0 +1,40 @@ +/** + * 配置 Zod Schema 定义 + * + * 同时服务于 TOML 文件校验和环境变量解析。 + */ + +import { z } from 'zod' + +export const llmConfigSchema = z.object({ + provider: z.string().default(''), + api_key: z.string().default(''), + model: z.string().default(''), + base_url: z.string().default(''), +}) + +export const dataConfigSchema = z.object({ + dir: z.string().default(''), +}) + +export const apiConfigSchema = z.object({ + port: z.number().int().min(1).max(65535).default(3210), + host: z.string().default('127.0.0.1'), +}) + +export const localeConfigSchema = z.object({ + lang: z.string().default('zh-CN'), +}) + +export const configSchema = z.object({ + llm: llmConfigSchema.default({}), + data: dataConfigSchema.default({}), + api: apiConfigSchema.default({}), + locale: localeConfigSchema.default({}), +}) + +export type ChatLabConfig = z.infer +export type LlmConfig = z.infer +export type DataConfig = z.infer +export type ApiConfig = z.infer +export type LocaleConfig = z.infer diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d2627ff1..fbd52942 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -142,6 +142,25 @@ importers: specifier: ^3.1.1 version: 3.2.6(typescript@5.9.3) + packages/config: + dependencies: + smol-toml: + specifier: ^1.3.1 + version: 1.6.1 + zod: + specifier: ^3.24.4 + version: 3.25.76 + + packages/core: {} + + packages/node-runtime: + dependencies: + '@openchatlab/core': + specifier: workspace:* + version: link:../core + + packages/shared-types: {} + packages: 7zip-bin@5.2.0: @@ -4311,6 +4330,10 @@ packages: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + smol-toml@1.6.1: + resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} + engines: {node: '>= 18'} + socks-proxy-agent@8.0.5: resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} engines: {node: '>= 14'} @@ -10016,6 +10039,8 @@ snapshots: smart-buffer@4.2.0: {} + smol-toml@1.6.1: {} + socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.4 diff --git a/tsconfig.node.json b/tsconfig.node.json index 943bd6f9..a5f98e78 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -9,7 +9,8 @@ "src/types/**/*", "packages/shared-types/**/*", "packages/core/**/*", - "packages/node-runtime/**/*" + "packages/node-runtime/**/*", + "packages/config/**/*" ], "compilerOptions": { "composite": true,