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
This commit is contained in:
digua committed 2026-05-11 19:55:33 +08:00
1 parent 6d1408ec56
commit e49f2fbbe8
6 files changed
+214 -1

No files matched your search

+12
View File
@@ -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"
}
}
+9
View File
@@ -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'
+126
View File
@@ -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>): 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<string, unknown> {
if (fs.existsSync(CONFIG_TOML)) {
try {
const content = fs.readFileSync(CONFIG_TOML, 'utf-8')
return parseToml(content) as Record<string, unknown>
} 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<string, unknown>
} 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<string, unknown> {
const result: Record<string, Record<string, unknown>> = {}
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<string, unknown>, override: Record<string, unknown>): Record<string, unknown> {
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<string, unknown>) ?? {},
value as Record<string, unknown>
)
} else if (value !== undefined) {
result[key] = value
}
}
return result
}
+40
View File
@@ -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<typeof configSchema>
export type LlmConfig = z.infer<typeof llmConfigSchema>
export type DataConfig = z.infer<typeof dataConfigSchema>
export type ApiConfig = z.infer<typeof apiConfigSchema>
export type LocaleConfig = z.infer<typeof localeConfigSchema>
+25
View File
@@ -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
+2 -1
View File
@@ -9,7 +9,8 @@
"src/types/**/*",
"packages/shared-types/**/*",
"packages/core/**/*",
"packages/node-runtime/**/*"
"packages/node-runtime/**/*",
"packages/config/**/*"
],
"compilerOptions": {
"composite": true,