import os from pathlib import Path from typing import Final from urllib.parse import urlsplit, urlunsplit from dotenv import load_dotenv BACKEND_ROOT: Final[Path] = Path(__file__).resolve().parents[1] PROJECT_ROOT: Final[Path] = BACKEND_ROOT.parent # Load backend-local env first, then fallback to project root env. load_dotenv(BACKEND_ROOT / ".env", override=False) load_dotenv(PROJECT_ROOT / ".env", override=False) def _env_text(name: str, default: str) -> str: raw = os.getenv(name) if raw is None: return default return str(raw).replace("\\n", "\n") def _env_bool(name: str, default: bool) -> bool: raw = os.getenv(name) if raw is None: return default return str(raw).strip().lower() in {"1", "true", "yes", "on"} def _normalize_dir_path(path_value: str) -> str: raw = str(path_value or "").strip() if not raw: return raw p = Path(raw) if p.is_absolute(): return str(p) return str((BACKEND_ROOT / p).resolve()) DATA_ROOT: Final[str] = _normalize_dir_path(os.getenv("DATA_ROOT", str(PROJECT_ROOT / "data"))) BOTS_WORKSPACE_ROOT: Final[str] = _normalize_dir_path( os.getenv("BOTS_WORKSPACE_ROOT", str(PROJECT_ROOT / "workspace" / "bots")) ) def _normalize_database_url(url: str) -> str: raw = str(url or "").strip() prefix = "sqlite:///" if not raw.startswith(prefix): return raw path_part = raw[len(prefix) :] if not path_part or path_part.startswith("/"): return raw abs_path = (BACKEND_ROOT / path_part).resolve() return f"{prefix}{abs_path.as_posix()}" def _database_engine(url: str) -> str: raw = str(url or "").strip().lower() if raw.startswith("sqlite"): return "sqlite" if raw.startswith("postgresql"): return "postgresql" if raw.startswith("mysql"): return "mysql" if "+" in raw: return raw.split("+", 1)[0] if "://" in raw: return raw.split("://", 1)[0] return "unknown" def _mask_database_url(url: str) -> str: raw = str(url or "").strip() if not raw or raw.startswith("sqlite"): return raw try: parsed = urlsplit(raw) if parsed.password is None: return raw host = parsed.hostname or "" if parsed.port: host = f"{host}:{parsed.port}" auth = parsed.username or "" if auth: auth = f"{auth}:***@{host}" else: auth = host netloc = auth return urlunsplit((parsed.scheme, netloc, parsed.path, parsed.query, parsed.fragment)) except Exception: return raw _db_env = str(os.getenv("DATABASE_URL") or "").strip() DATABASE_URL: Final[str] = _normalize_database_url( _db_env if _db_env else f"sqlite:///{Path(DATA_ROOT) / 'nanobot_dashboard.db'}" ) DATABASE_ENGINE: Final[str] = _database_engine(DATABASE_URL) DATABASE_URL_DISPLAY: Final[str] = _mask_database_url(DATABASE_URL) DATABASE_ECHO: Final[bool] = _env_bool("DATABASE_ECHO", True) DEFAULT_AGENTS_MD: Final[str] = _env_text( "DEFAULT_AGENTS_MD", "# Agent Instructions\n\n- 优先完成任务目标\n- 操作前先说明意图\n- 输出必须可执行\n\n## 默认输出规范\n\n- 每次执行任务时,在 workspace 中创建新目录保存本次输出。\n- 输出内容默认采用 Markdown(.md)格式。", ).strip() DEFAULT_SOUL_MD: Final[str] = _env_text( "DEFAULT_SOUL_MD", "# Soul\n\n你是专业的企业数字员工,表达清晰、可执行。", ).strip() DEFAULT_USER_MD: Final[str] = _env_text( "DEFAULT_USER_MD", "# User\n\n- 语言: 中文\n- 风格: 专业\n- 偏好: 简明且有步骤", ).strip() DEFAULT_TOOLS_MD: Final[str] = _env_text( "DEFAULT_TOOLS_MD", "# Tools\n\n- 谨慎使用 shell\n- 修改文件后复核\n- 失败时说明原因并重试策略", ).strip() DEFAULT_IDENTITY_MD: Final[str] = _env_text( "DEFAULT_IDENTITY_MD", "# Identity\n\n- 角色: 企业数字员工\n- 领域: 运维与任务执行", ).strip()