2026-03-01 16:26:03 +00:00
|
|
|
|
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"}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-03 06:09:11 +00:00
|
|
|
|
def _env_int(name: str, default: int, min_value: int, max_value: int) -> int:
|
|
|
|
|
|
raw = os.getenv(name)
|
|
|
|
|
|
if raw is None:
|
|
|
|
|
|
return default
|
|
|
|
|
|
try:
|
|
|
|
|
|
value = int(str(raw).strip())
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
value = default
|
|
|
|
|
|
return max(min_value, min(max_value, value))
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-01 16:26:03 +00:00
|
|
|
|
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)
|
2026-03-03 06:09:11 +00:00
|
|
|
|
UPLOAD_MAX_MB: Final[int] = _env_int("UPLOAD_MAX_MB", 100, 1, 2048)
|
2026-03-01 16:26:03 +00:00
|
|
|
|
|
2026-03-09 04:53:15 +00:00
|
|
|
|
REDIS_ENABLED: Final[bool] = _env_bool("REDIS_ENABLED", False)
|
|
|
|
|
|
REDIS_URL: Final[str] = str(os.getenv("REDIS_URL") or "").strip()
|
|
|
|
|
|
REDIS_PREFIX: Final[str] = str(os.getenv("REDIS_PREFIX") or "dashboard_nanobot").strip() or "dashboard_nanobot"
|
|
|
|
|
|
REDIS_DEFAULT_TTL: Final[int] = _env_int("REDIS_DEFAULT_TTL", 60, 1, 86400)
|
|
|
|
|
|
AUTO_MIGRATE_SQLITE_TO_PRIMARY: Final[bool] = _env_bool("AUTO_MIGRATE_SQLITE_TO_PRIMARY", True)
|
|
|
|
|
|
SQLITE_MIGRATION_SOURCE: Final[str] = _normalize_dir_path(
|
|
|
|
|
|
os.getenv("SQLITE_MIGRATION_SOURCE", str(Path(DATA_ROOT) / "nanobot_dashboard.db"))
|
|
|
|
|
|
)
|
|
|
|
|
|
PANEL_ACCESS_PASSWORD: Final[str] = str(os.getenv("PANEL_ACCESS_PASSWORD") or "").strip()
|
|
|
|
|
|
|
2026-03-01 16:26:03 +00:00
|
|
|
|
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()
|