"""应用配置(环境变量)。"""
from __future__ import annotations
import os
from functools import lru_cache
from pathlib import Path
from urllib.parse import urlparse
from app.core.paths import DB_FILE, PROJECT_ROOT
DEFAULT_SQLITE_URL = f"sqlite:///{DB_FILE}"
def is_production() -> bool:
env = (os.getenv("COMPILOT_ENV") or os.getenv("ENV") or "").strip().lower()
return env in ("production", "prod")
def is_development() -> bool:
return not is_production()
@lru_cache
def get_database_url() -> str:
return (os.getenv("DATABASE_URL") or os.getenv("COMPILOT_DATABASE_URL") or DEFAULT_SQLITE_URL).strip()
def database_dialect() -> str:
url = get_database_url()
if url.startswith("postgres://") or url.startswith("postgresql://"):
return "postgres"
return "sqlite"
def sqlite_path_from_url(url: str) -> Path:
parsed = urlparse(url)
if parsed.path:
return Path(parsed.path)
return DB_FILE
def get_jwt_secret() -> str:
"""JWT 签名密钥:生产环境必须通过环境变量提供。"""
secret = (os.getenv("JWT_SECRET") or os.getenv("COMPILOT_JWT_SECRET") or "").strip()
if secret:
return secret
if is_production():
raise RuntimeError(
"生产环境必须设置 JWT_SECRET 或 COMPILOT_JWT_SECRET 环境变量"
)
from app.core.security import load_dev_jwt_secret
return load_dev_jwt_secret()
def jwt_expire_hours() -> int:
raw = (os.getenv("JWT_EXPIRE_HOURS") or os.getenv("COMPILOT_JWT_EXPIRE_HOURS") or "").strip()
if raw:
try:
return max(1, int(raw))
except ValueError:
pass
return 24 * 7
def allow_default_admin_login() -> bool:
"""是否允许使用种子账号 admin/admin123 登录(生产默认关闭)。"""
raw = (os.getenv("COMPILOT_ALLOW_DEFAULT_ADMIN") or "").strip().lower()
if raw in ("1", "true", "yes", "on"):
return True
if raw in ("0", "false", "no", "off"):
return False
return is_development()
def rate_limit_login() -> tuple[int, int]:
"""(次数, 窗口秒) — 按客户端 IP。"""
return _rate_pair("COMPILOT_RATE_LOGIN", default=(10, 60))
def rate_limit_chat() -> tuple[int, int]:
return _rate_pair("COMPILOT_RATE_CHAT", default=(30, 60))
def terminal_tool_enabled() -> bool:
raw = (os.getenv("COMPILOT_TERMINAL_ENABLED") or "").strip().lower()
if raw in ("1", "true", "yes", "on"):
return True
if raw in ("0", "false", "no", "off"):
return False
return is_development()
def terminal_timeout_seconds() -> int:
raw = (os.getenv("COMPILOT_TERMINAL_TIMEOUT") or "").strip()
try:
return min(max(5, int(raw)), 300) if raw else 120
except ValueError:
return 120
def terminal_max_output_chars() -> int:
raw = (os.getenv("COMPILOT_TERMINAL_MAX_OUTPUT") or "").strip()
try:
return min(max(1000, int(raw)), 100_000) if raw else 40_000
except ValueError:
return 40_000
def bing_search_tool_enabled() -> bool:
raw = (os.getenv("COMPILOT_BING_SEARCH_ENABLED") or "").strip().lower()
if raw in ("1", "true", "yes", "on"):
return True
if raw in ("0", "false", "no", "off"):
return False
return is_development()
def bing_search_timeout_seconds() -> int:
raw = (os.getenv("COMPILOT_BING_SEARCH_TIMEOUT") or "").strip()
try:
return min(max(5, int(raw)), 120) if raw else 30
except ValueError:
return 30
def bing_search_max_results() -> int:
raw = (os.getenv("COMPILOT_BING_SEARCH_MAX_RESULTS") or "").strip()
try:
return min(max(1, int(raw)), 20) if raw else 8
except ValueError:
return 8
def bing_search_user_agent() -> str:
custom = (os.getenv("COMPILOT_BING_SEARCH_USER_AGENT") or "").strip()
if custom:
return custom
return (
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
)
def web_fetch_tool_enabled() -> bool:
raw = (os.getenv("COMPILOT_WEB_FETCH_ENABLED") or "").strip().lower()
if raw in ("1", "true", "yes", "on"):
return True
if raw in ("0", "false", "no", "off"):
return False
return is_development()
def web_fetch_js_enabled() -> bool:
raw = (os.getenv("COMPILOT_WEB_FETCH_JS_ENABLED") or "").strip().lower()
if raw in ("0", "false", "no", "off"):
return False
if raw in ("1", "true", "yes", "on"):
return True
return web_fetch_tool_enabled()
def web_fetch_timeout_seconds() -> int:
raw = (os.getenv("COMPILOT_WEB_FETCH_TIMEOUT") or "").strip()
try:
return min(max(5, int(raw)), 120) if raw else 45
except ValueError:
return 45
def web_fetch_max_content_chars() -> int:
raw = (os.getenv("COMPILOT_WEB_FETCH_MAX_CONTENT") or "").strip()
try:
return min(max(2000, int(raw)), 200_000) if raw else 50_000
except ValueError:
return 50_000
def web_fetch_user_agent() -> str:
custom = (os.getenv("COMPILOT_WEB_FETCH_USER_AGENT") or "").strip()
if custom:
return custom
return bing_search_user_agent()
def browser_tool_enabled() -> bool:
raw = (os.getenv("COMPILOT_BROWSER_ENABLED") or "").strip().lower()
if raw in ("1", "true", "yes", "on"):
return True
if raw in ("0", "false", "no", "off"):
return False
return is_development()
def browser_max_snapshot_chars() -> int:
raw = (os.getenv("COMPILOT_BROWSER_MAX_SNAPSHOT") or "").strip()
try:
return min(max(2000, int(raw)), 100_000) if raw else 30_000
except ValueError:
return 30_000
def browser_idle_seconds() -> int:
raw = (os.getenv("COMPILOT_BROWSER_IDLE_SECONDS") or "").strip()
try:
return min(max(60, int(raw)), 7200) if raw else 1800
except ValueError:
return 1800
def _rate_pair(env_key: str, *, default: tuple[int, int]) -> tuple[int, int]:
raw = (os.getenv(env_key) or "").strip()
if not raw:
return default
parts = raw.split(",")
if len(parts) != 2:
return default
try:
return max(1, int(parts[0])), max(1, int(parts[1]))
except ValueError:
return default
def load_dotenv_file() -> None:
"""加载项目根目录 .env(若存在)。"""
env_path = PROJECT_ROOT / ".env"
if not env_path.is_file():
return
for line in env_path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, val = line.partition("=")
key = key.strip()
val = val.strip().strip('"').strip("'")
if key and key not in os.environ:
os.environ[key] = val