76a67e8237
将分散的 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>
153 lines
4.5 KiB
Python
153 lines
4.5 KiB
Python
"""KiloStar 统一配置管理:多 YAML 文件加载、组合校验、缓存、热重载。
|
|
|
|
配置目录:``config/``(项目根),含 config.yml、workflow.yaml、sandbox.yaml。
|
|
各文件独立维护,启动时统一加载到 ``AppConfig`` 并做 schema 校验。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
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):
|
|
max_attempts: int = Field(default=5, ge=1, le=100)
|
|
|
|
|
|
class WorkflowConfig(BaseModel):
|
|
retry: RetryConfig = Field(default_factory=RetryConfig)
|
|
|
|
|
|
# ─── Sandbox Models (镜像 sandbox.py 中的定义,供统一加载使用) ───
|
|
|
|
|
|
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:
|
|
return get_app_config().workflow
|
|
|
|
|
|
def reload_workflow_config() -> WorkflowConfig:
|
|
reload_app_config(section="workflow")
|
|
return get_app_config().workflow
|
|
|
|
|
|
def save_workflow_config(config: WorkflowConfig) -> None:
|
|
_WORKFLOW_YAML.parent.mkdir(parents=True, exist_ok=True)
|
|
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_app_config(section="workflow")
|