"""
Witty Service 统一配置管理
所有环境变量集中在此模块管理,支持的配置项详见下方各配置类。
环境变量列表:
# 基础配置
AUTH_TOKEN 认证令牌 (默认: dev-token)
# 工作空间配置
WITTY_WORKSPACE_ROOT 工作空间根目录 (默认: ~/.witty)
WITTY_AGENT_SERVER_APP_DIR Agent服务器应用目录 (可选)
# 数据库配置
WITTY_DATABASE_URL 数据库连接URL (默认: sqlite:///~/.witty/db/witty_service.sqlite3)
WITTY_DATABASE_AUTO_CREATE 是否允许启动时自动建表 (默认: false)
# 日志配置
WITTY_LOG_LEVEL 日志级别: DEBUG, INFO, WARNING, ERROR (默认: INFO)
WITTY_LOG_FILE 日志文件路径 (默认: WITTY_WORKSPACE_ROOT/logs/witty-service.log)
WITTY_LOG_MAX_BYTES 单个日志文件最大字节数 (默认: 10485760)
WITTY_LOG_BACKUP_COUNT 备份日志文件数量 (默认: 5)
# Docker沙箱配置
WITTY_DOCKER_HOST Docker主机地址 (默认: 127.0.0.1)
WITTY_DOCKER_CONTAINER_PORT Docker容器端口 (默认: 8080)
WITTY_DOCKER_CONTAINER_WORKSPACE_PATH Docker容器内工作空间路径 (默认: /witty-workspace)
WITTY_DOCKER_STOP_TIMEOUT Docker容器停止超时时间(秒) (默认: 10)
WITTY_DOCKER_IMAGE Docker镜像名称 (默认: ghcr.io/openwitty/witty-agent-server)
WITTY_DOCKER_IMAGE_TAG Docker镜像标签 (默认: latest)
# OpenClaw Gateway配置
OPENCLAW_GATEWAY_IDLE_TIMEOUT 网关空闲超时时间(秒) (默认: 1200)
OPENCLAW_GATEWAY_LIFECYCLE_END_DRAIN_TIMEOUT 生命周期结束排水超时时间(秒) (默认: 60)
OPENCLAW_STATE_DIR 状态目录 (可选)
# OpenClaw配置
OPENCLAW_CONFIG_PATH OpenClaw配置文件路径 (默认: ~/.openclaw/openclaw.json)
# Witty Insight 配置
WITTY_INSIGHT_ENABLED 是否启用 insight 集成 (默认: true)
WITTY_INSIGHT_BASE_URL insight API 地址 (默认: http://127.0.0.1:7396)
WITTY_INSIGHT_TIMEOUT_SECONDS insight 请求超时(秒) (默认: 10)
WITTY_INSIGHT_BEARER_TOKEN insight Bearer Token (可选)
使用示例:
from witty_service.config import get_settings
settings = get_settings()
print(settings.database.url)
print(settings.docker.host)
print(settings.logging.level)
"""
from dataclasses import dataclass, field
from pathlib import Path
import os
@dataclass(frozen=True)
class CorsSettings:
"""CORS 跨域资源共享配置"""
origins: list[str] = field(default_factory=lambda: ["*"])
credentials: bool = True
methods: list[str] = field(default_factory=lambda: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"])
headers: list[str] = field(default_factory=lambda: ["*"])
@dataclass(frozen=True)
class DatabaseSettings:
"""数据库连接配置
环境变量:
WITTY_DATABASE_URL: 数据库连接URL
- SQLite: sqlite:///path/to/db.sqlite3
- PostgreSQL: postgresql://user:pass@localhost/dbname
- 默认: sqlite:///~/.witty/db/witty_service.sqlite3
WITTY_DATABASE_AUTO_CREATE: 是否允许使用 ORM 自动建表
- true/false
- 默认: false
"""
url: str
auto_create: bool = False
@classmethod
def from_env(cls) -> "DatabaseSettings":
default_db_path = Path("~/.witty/db/witty_service.sqlite3").expanduser()
url = os.getenv("WITTY_DATABASE_URL", f"sqlite:///{default_db_path}")
auto_create = os.getenv("WITTY_DATABASE_AUTO_CREATE", "false").lower() in (
"1",
"true",
"yes",
)
return cls(url=url, auto_create=auto_create)
@dataclass(frozen=True)
class LoggingSettings:
"""日志系统配置
环境变量:
WITTY_LOG_LEVEL: 日志级别 (DEBUG, INFO, WARNING, ERROR), 默认 INFO
WITTY_LOG_FILE: 日志文件路径, 默认 WITTY_WORKSPACE_ROOT/logs/witty-service.log
WITTY_LOG_MAX_BYTES: 单个日志文件最大字节数, 默认 10MB
WITTY_LOG_BACKUP_COUNT: 备份日志文件数量, 默认 5
"""
level: str = "INFO"
file: str = ""
max_bytes: int = 10 * 1024 * 1024
backup_count: int = 5
@classmethod
def from_env(cls) -> "LoggingSettings":
workspace_root = Path(os.getenv("WITTY_WORKSPACE_ROOT", "~/.witty")).expanduser()
default_log_file = workspace_root / "logs" / "witty-service.log"
return cls(
level=os.getenv("WITTY_LOG_LEVEL", "INFO").upper(),
file=os.getenv("WITTY_LOG_FILE", str(default_log_file)),
max_bytes=int(os.getenv("WITTY_LOG_MAX_BYTES", 10 * 1024 * 1024)),
backup_count=int(os.getenv("WITTY_LOG_BACKUP_COUNT", 5)),
)
@dataclass(frozen=True)
class DockerSettings:
"""Docker 沙箱运行时配置
环境变量:
WITTY_DOCKER_HOST: Docker 守护进程主机地址, 默认 127.0.0.1
WITTY_DOCKER_CONTAINER_PORT: 容器内部服务端口, 默认 8080
WITTY_DOCKER_CONTAINER_WORKSPACE_PATH: 容器内工作空间挂载路径, 默认 /witty-workspace
WITTY_DOCKER_STOP_TIMEOUT: 容器停止超时时间(秒), 默认 10
WITTY_DOCKER_IMAGE: Docker 镜像名称, 默认 ghcr.io/openwitty/witty-agent-server
WITTY_DOCKER_IMAGE_TAG: Docker 镜像标签, 默认 latest
# 资源管控
WITTY_DOCKER_MEMORY_LIMIT: 容器内存硬限制, 默认 512m
WITTY_DOCKER_PIDS_LIMIT: 容器内最大进程数, 默认 100
WITTY_DOCKER_CPU_SHARES: CPU 相对权重, 默认 512
WITTY_DOCKER_NOFILE_SOFT_LIMIT: 文件描述符软限制, 默认 1024
WITTY_DOCKER_NOFILE_HARD_LIMIT: 文件描述符硬限制, 默认 4096
WITTY_DOCKER_TMPFS_SIZE: /tmp tmpfs 大小, 默认 256M
WITTY_DOCKER_READ_ONLY: 根文件系统只读, 默认 true
"""
host: str = "127.0.0.1"
container_port: int = 8080
container_workspace_path: str = "/witty-workspace"
stop_timeout: int = 10
image: str = "ghcr.io/openwitty/witty-agent-server"
image_tag: str = "latest"
memory_limit: str = "512m"
pids_limit: int = 100
cpu_shares: int = 512
nofile_soft_limit: int = 1024
nofile_hard_limit: int = 4096
tmpfs_size: str = "256M"
read_only: bool = True
@classmethod
def from_env(cls) -> "DockerSettings":
return cls(
host=os.getenv("WITTY_DOCKER_HOST", "127.0.0.1"),
container_port=int(os.getenv("WITTY_DOCKER_CONTAINER_PORT", 8080)),
container_workspace_path=os.getenv("WITTY_DOCKER_CONTAINER_WORKSPACE_PATH", "/witty-workspace"),
stop_timeout=int(os.getenv("WITTY_DOCKER_STOP_TIMEOUT", 10)),
image=os.getenv("WITTY_DOCKER_IMAGE", "ghcr.io/openwitty/witty-agent-server"),
image_tag=os.getenv("WITTY_DOCKER_IMAGE_TAG", "latest"),
memory_limit=os.getenv("WITTY_DOCKER_MEMORY_LIMIT", "512m"),
pids_limit=int(os.getenv("WITTY_DOCKER_PIDS_LIMIT", "100")),
cpu_shares=int(os.getenv("WITTY_DOCKER_CPU_SHARES", "512")),
nofile_soft_limit=int(os.getenv("WITTY_DOCKER_NOFILE_SOFT_LIMIT", "1024")),
nofile_hard_limit=int(os.getenv("WITTY_DOCKER_NOFILE_HARD_LIMIT", "4096")),
tmpfs_size=os.getenv("WITTY_DOCKER_TMPFS_SIZE", "256M"),
read_only=os.getenv("WITTY_DOCKER_READ_ONLY", "true").lower() not in ("0", "false", "no"),
)
def get_full_image_name(self) -> str:
"""获取完整的镜像名称,包含标签"""
image = self.image
if "@" in image or self._has_explicit_tag(image):
return image
return f"{image}:{self.image_tag}"
@staticmethod
def _has_explicit_tag(image: str) -> bool:
"""检查镜像名是否包含显式标签"""
last_segment = image.rsplit("/", 1)[-1]
return ":" in last_segment
@dataclass(frozen=True)
class OpenClawGatewaySettings:
"""OpenClaw Gateway 网关配置
环境变量:
OPENCLAW_GATEWAY_IDLE_TIMEOUT: 网关空闲超时时间(秒), 默认 1200 (20分钟)
OPENCLAW_GATEWAY_LIFECYCLE_END_DRAIN_TIMEOUT: 生命周期结束排水超时时间(秒), 默认 60
OPENCLAW_STATE_DIR: 网关状态存储目录, 默认 ~/.openclaw
"""
idle_timeout: float = 1200.0
lifecycle_end_drain_timeout: float = 60.0
state_dir: str | None = None
@classmethod
def from_env(cls) -> "OpenClawGatewaySettings":
return cls(
idle_timeout=float(os.getenv("OPENCLAW_GATEWAY_IDLE_TIMEOUT", "1200")),
lifecycle_end_drain_timeout=float(os.getenv("OPENCLAW_GATEWAY_LIFECYCLE_END_DRAIN_TIMEOUT", "60")),
state_dir=os.getenv("OPENCLAW_STATE_DIR"),
)
@dataclass(frozen=True)
class WorkspaceSettings:
"""工作空间目录配置
环境变量:
WITTY_WORKSPACE_ROOT: 工作空间根目录, 默认 ~/.witty
WITTY_AGENT_SERVER_APP_DIR: Agent 服务器应用目录, 可选
WITTY_RECOVERY_MAX_CONCURRENT: 启动时恢复 agent 的最大并发数, 默认 5
"""
root: str = "~/.witty"
agent_server_app_dir: str | None = None
recovery_max_concurrent: int = 5
@classmethod
def from_env(cls) -> "WorkspaceSettings":
return cls(
root=os.getenv("WITTY_WORKSPACE_ROOT", "~/.witty"),
agent_server_app_dir=os.getenv("WITTY_AGENT_SERVER_APP_DIR"),
recovery_max_concurrent=int(os.getenv("WITTY_RECOVERY_MAX_CONCURRENT", "5")),
)
def root_path(self) -> Path:
"""获取工作空间根目录 (Path 对象)"""
return Path(self.root).expanduser()
@dataclass(frozen=True)
class OpenClawSettings:
"""OpenClaw 通用配置
环境变量:
OPENCLAW_CONFIG_PATH: OpenClaw 配置文件路径, 默认 ~/.openclaw/openclaw.json
"""
config_path: str = "~/.openclaw/openclaw.json"
@classmethod
def from_env(cls) -> "OpenClawSettings":
return cls(
config_path=os.getenv("OPENCLAW_CONFIG_PATH", "~/.openclaw/openclaw.json"),
)
def config_path_resolved(self) -> Path:
"""获取配置文件路径 (Path 对象)"""
return Path(self.config_path).expanduser().resolve(strict=False)
@dataclass(frozen=True)
class InsightSettings:
"""Witty Insight 上游服务配置
环境变量:
WITTY_INSIGHT_ENABLED: 是否启用 insight 集成, 默认 true
WITTY_INSIGHT_BASE_URL: insight API 地址, 默认 http://127.0.0.1:7396
WITTY_INSIGHT_TIMEOUT_SECONDS: insight 请求超时(秒), 默认 10
WITTY_INSIGHT_BEARER_TOKEN: 调用 insight 时附带的 Bearer Token, 可选
"""
enabled: bool = True
base_url: str = "http://127.0.0.1:7396"
timeout_seconds: float = 10.0
bearer_token: str | None = None
@classmethod
def from_env(cls) -> "InsightSettings":
raw_token = os.getenv("WITTY_INSIGHT_BEARER_TOKEN")
bearer_token = raw_token.strip() or None if raw_token is not None else None
return cls(
enabled=os.getenv("WITTY_INSIGHT_ENABLED", "true").lower() in ("1", "true", "yes"),
base_url=os.getenv("WITTY_INSIGHT_BASE_URL", "http://127.0.0.1:7396"),
timeout_seconds=float(os.getenv("WITTY_INSIGHT_TIMEOUT_SECONDS", "10")),
bearer_token=bearer_token,
)
@dataclass(frozen=True)
class OpenCodeSettings:
"""OpenCode runtime 配置。
环境变量:
OPENCODE_STATE_DIR: opencode 状态存储目录, 默认 ~/.opencode
"""
state_dir: str = "~/.opencode"
@classmethod
def from_env(cls) -> "OpenCodeSettings":
return cls(
state_dir=os.getenv("OPENCODE_STATE_DIR", "~/.opencode"),
)
def state_dir_resolved(self) -> Path:
return Path(self.state_dir).expanduser().resolve(strict=False)
@dataclass(frozen=True)
class RuntimeSettings:
"""Runtime 调度配置。
每个 agent-server 实例运行 ``default_type`` 指定的单个 runtime
``supported`` 声明该镜像/部署能支持哪些 runtime 类型,供外界调度参考。
环境变量:
WITTY_RUNTIME_DEFAULT: 当前实例运行的 runtime 类型, 默认 openclaw
WITTY_RUNTIME_SUPPORTED: 镜像支持的所有 runtime 类型, 默认 openclaw,opencode
"""
default_type: str = "openclaw"
supported: tuple[str, ...] = ("openclaw", "opencode")
@classmethod
def from_env(cls) -> "RuntimeSettings":
default_type = os.getenv("WITTY_RUNTIME_DEFAULT", "openclaw")
raw_supported = os.getenv("WITTY_RUNTIME_SUPPORTED", "openclaw,opencode")
supported = tuple(
item.strip() for item in raw_supported.split(",") if item.strip()
)
if not supported:
supported = (default_type,)
return cls(default_type=default_type, supported=supported)
@dataclass(frozen=True)
class Settings:
"""Witty Service 统一配置容器
整合所有子配置模块,通过 from_env() 类方法从环境变量加载配置。
使用 get_settings() 函数获取单例配置实例。
"""
auth_token: str
cors: CorsSettings
database: DatabaseSettings
logging: LoggingSettings
docker: DockerSettings
openclaw_gateway: OpenClawGatewaySettings
workspace: WorkspaceSettings
openclaw: OpenClawSettings
insight: InsightSettings
opencode: OpenCodeSettings
runtime: RuntimeSettings
@classmethod
def from_env(cls) -> "Settings":
"""从环境变量加载所有配置"""
return cls(
auth_token=os.getenv("AUTH_TOKEN", "dev-token"),
cors=CorsSettings(),
database=DatabaseSettings.from_env(),
logging=LoggingSettings.from_env(),
docker=DockerSettings.from_env(),
openclaw_gateway=OpenClawGatewaySettings.from_env(),
workspace=WorkspaceSettings.from_env(),
openclaw=OpenClawSettings.from_env(),
insight=InsightSettings.from_env(),
opencode=OpenCodeSettings.from_env(),
runtime=RuntimeSettings.from_env(),
)
_settings: Settings | None = None
def get_settings() -> Settings:
"""获取配置单例实例
使用全局单例模式,首次调用时从环境变量加载配置,
后续调用直接返回缓存的配置实例。
Returns:
Settings: 配置实例
"""
global _settings
if _settings is None:
_settings = Settings.from_env()
return _settings
def get_docker_image() -> str:
"""获取 Docker 镜像完整名称
Returns:
str: 完整的镜像名称,包含标签
"""
return get_settings().docker.get_full_image_name()