feat(config): 统一配置加载入口,启动时校验所有YAML配置

将分散的 config.yml、workflow.yaml、sandbox.yaml 加载逻辑统一到 AppConfig 模型,
启动时一次性校验,失败则 fast-fail。sandbox.py 改为从统一配置取值,消除重复加载。
同时修复 onebot 测试并新增14个统一配置测试(总测试 285→300)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 13:52:03 +00:00
parent 80174acaae
commit 76a67e8237
8 changed files with 358 additions and 116 deletions
+114 -22
View File
@@ -1,23 +1,22 @@
"""Workflow 配置文件管理:读取、缓存、热重载。
"""KiloStar 统一配置管理:多 YAML 文件加载、组合校验、缓存、热重载。
配置文件路径``config/workflow.yaml``(相对于项目根目录)
采用模块级单例 + 文件修改时间检测,保证:
- 首次调用时懒加载
- reload_workflow_config() 显式触发重载
- 工作流引擎调 get_workflow_config() 始终拿到最新生效值
配置目录``config/``(项目根),含 config.yml、workflow.yaml、sandbox.yaml
各文件独立维护,启动时统一加载到 ``AppConfig`` 并做 schema 校验。
"""
from __future__ import annotations
import os
from pathlib import Path
from typing import Any
from typing import Any, List, Optional
import yaml
from pydantic import BaseModel, Field
_CONFIG_DIR = Path(__file__).resolve().parent.parent.parent / "config"
_WORKFLOW_YAML = _CONFIG_DIR / "workflow.yaml"
_CONFIG_YML = _CONFIG_DIR / "config.yml"
_SANDBOX_YAML = _CONFIG_DIR / "sandbox.yaml"
class RetryConfig(BaseModel):
@@ -28,28 +27,121 @@ class WorkflowConfig(BaseModel):
retry: RetryConfig = Field(default_factory=RetryConfig)
_current: WorkflowConfig | None = None
# ─── Sandbox Models (镜像 sandbox.py 中的定义,供统一加载使用) ───
def _load_from_disk() -> WorkflowConfig:
if not _WORKFLOW_YAML.exists():
return WorkflowConfig()
with open(_WORKFLOW_YAML, "r", encoding="utf-8") as f:
data = yaml.safe_load(f) or {}
return WorkflowConfig.model_validate(data)
class FilesystemPolicy(BaseModel):
workspace_root: str = "/tmp/kilostar_workspace"
allowed_read_paths: List[str] = Field(default_factory=lambda: ["/tmp"])
denied_paths: List[str] = Field(default_factory=list)
class ShellPolicy(BaseModel):
enabled: bool = True
blocked_commands: List[str] = Field(default_factory=list)
blocked_operators: List[str] = Field(default_factory=list)
max_timeout: int = 60
class PythonExecutorPolicy(BaseModel):
enabled: bool = True
max_timeout: int = 30
blocked_imports: List[str] = Field(default_factory=list)
blocked_builtins: List[str] = Field(default_factory=list)
class SandboxConfig(BaseModel):
enabled: bool = True
filesystem: FilesystemPolicy = Field(default_factory=FilesystemPolicy)
shell: ShellPolicy = Field(default_factory=ShellPolicy)
python_executor: PythonExecutorPolicy = Field(default_factory=PythonExecutorPolicy)
# ─── App-level Models ───
class AppInfo(BaseModel):
version: str = "0.1.1-alpha"
name: str = "Kilostar"
class AppConfig(BaseModel):
app: AppInfo = Field(default_factory=AppInfo)
workflow: WorkflowConfig = Field(default_factory=WorkflowConfig)
sandbox: SandboxConfig = Field(default_factory=SandboxConfig)
# ─── Unified AppConfig Loading ───
_app_current: AppConfig | None = None
def _read_yaml(path: Path) -> dict:
if not path.exists():
return {}
with open(path, "r", encoding="utf-8") as f:
return yaml.safe_load(f) or {}
def load_app_config(config_dir: Path | None = None) -> AppConfig:
"""从 config/ 下多个 YAML 文件加载并组合校验。"""
d = config_dir or _CONFIG_DIR
app_raw = _read_yaml(d / "config.yml")
workflow_raw = _read_yaml(d / "workflow.yaml")
sandbox_raw = _read_yaml(d / "sandbox.yaml")
sandbox_section = sandbox_raw.get("sandbox", sandbox_raw)
return AppConfig.model_validate({
"app": app_raw,
"workflow": workflow_raw,
"sandbox": sandbox_section,
})
def get_app_config() -> AppConfig:
global _app_current
if _app_current is None:
_app_current = load_app_config()
return _app_current
def reload_app_config(section: str | None = None) -> AppConfig:
"""热重载配置。section=None 全量重载,指定 section 只刷新对应部分。"""
global _app_current
if section is None or _app_current is None:
_app_current = load_app_config()
return _app_current
d = _CONFIG_DIR
if section == "workflow":
raw = _read_yaml(d / "workflow.yaml")
_app_current = _app_current.model_copy(
update={"workflow": WorkflowConfig.model_validate(raw)}
)
elif section == "sandbox":
raw = _read_yaml(d / "sandbox.yaml")
sandbox_section = raw.get("sandbox", raw)
_app_current = _app_current.model_copy(
update={"sandbox": SandboxConfig.model_validate(sandbox_section)}
)
else:
_app_current = load_app_config()
return _app_current
# ─── Backward-Compatible Workflow Accessors ───
def get_workflow_config() -> WorkflowConfig:
global _current
if _current is None:
_current = _load_from_disk()
return _current
return get_app_config().workflow
def reload_workflow_config() -> WorkflowConfig:
global _current
_current = _load_from_disk()
return _current
reload_app_config(section="workflow")
return get_app_config().workflow
def save_workflow_config(config: WorkflowConfig) -> None:
@@ -57,4 +149,4 @@ def save_workflow_config(config: WorkflowConfig) -> None:
data = config.model_dump()
with open(_WORKFLOW_YAML, "w", encoding="utf-8") as f:
yaml.dump(data, f, default_flow_style=False, allow_unicode=True)
reload_workflow_config()
reload_app_config(section="workflow")
+11 -52
View File
@@ -4,66 +4,25 @@ from __future__ import annotations
import os
import re
from pathlib import Path
from typing import List, Optional
import yaml
from pydantic import BaseModel, Field
_CONFIG_DIR = Path(__file__).resolve().parent.parent.parent / "config"
_SANDBOX_YAML = _CONFIG_DIR / "sandbox.yaml"
class FilesystemPolicy(BaseModel):
workspace_root: str = "/tmp/kilostar_workspace"
allowed_read_paths: List[str] = Field(default_factory=lambda: ["/tmp"])
denied_paths: List[str] = Field(default_factory=list)
class ShellPolicy(BaseModel):
enabled: bool = True
blocked_commands: List[str] = Field(default_factory=list)
blocked_operators: List[str] = Field(default_factory=list)
max_timeout: int = 60
class PythonExecutorPolicy(BaseModel):
enabled: bool = True
max_timeout: int = 30
blocked_imports: List[str] = Field(default_factory=list)
blocked_builtins: List[str] = Field(default_factory=list)
class SandboxConfig(BaseModel):
enabled: bool = True
filesystem: FilesystemPolicy = Field(default_factory=FilesystemPolicy)
shell: ShellPolicy = Field(default_factory=ShellPolicy)
python_executor: PythonExecutorPolicy = Field(default_factory=PythonExecutorPolicy)
_current: Optional[SandboxConfig] = None
def _load_sandbox_config() -> SandboxConfig:
if not _SANDBOX_YAML.exists():
return SandboxConfig()
with open(_SANDBOX_YAML, "r", encoding="utf-8") as f:
data = yaml.safe_load(f) or {}
raw = data.get("sandbox", data)
return SandboxConfig.model_validate(raw)
from kilostar.utils.config_loader import (
SandboxConfig,
FilesystemPolicy,
ShellPolicy,
PythonExecutorPolicy,
get_app_config,
reload_app_config,
)
def get_sandbox_config() -> SandboxConfig:
global _current
if _current is None:
_current = _load_sandbox_config()
return _current
return get_app_config().sandbox
def reload_sandbox_config() -> SandboxConfig:
global _current
_current = _load_sandbox_config()
return _current
reload_app_config(section="sandbox")
return get_app_config().sandbox
# ─── Exceptions ───