feat(doctor): add Database section probing configured backend

scripts/doctor.py 新增 check_database():

  - sqlite → OK + 显示 sqlite_dir
  - memory → WARN(数据非持久化)
  - postgres → 解析 database.postgres_url 中的 \$DATABASE_URL,asyncpg
    实际连接 + SELECT version() → OK with server version;连接失败时
    FAIL 给出可执行 fix(启 docker compose postgres / 检查 DATABASE_URL)
  - 未知 backend → WARN

Database section 插在 LLM Provider 与 Sandbox 之间。SQLite 部署不会触发
PG 探测(不破坏现有 dev 体验)。

Stage 0 PR1 T1.7.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1445043649
2026-05-10 22:52:26 +08:00
parent 8f480cd76f
commit e6f5ba53bd
+111
View File
@@ -543,6 +543,113 @@ def check_frontend_env(project_root: Path) -> CheckResult:
)
def check_database(config_path: Path) -> CheckResult:
"""Verify the configured DB backend is reachable.
- sqlite: returns OK with a "dev only" hint
- postgres: resolves DATABASE_URL env, attempts asyncpg.connect,
reports server version on success; FAIL with fix hint otherwise
- memory: WARN ("data not persistent")
- missing/unknown backend: WARN
"""
if not config_path.exists():
return CheckResult("database backend reachable", "skip")
try:
data = _load_yaml_file(config_path)
except Exception as exc:
return CheckResult(
"database backend reachable",
"fail",
str(exc),
fix="Fix config.yaml syntax, then re-run 'make doctor'",
)
db = data.get("database") or {}
backend = (db.get("backend") or "sqlite").lower()
if backend == "sqlite":
return CheckResult(
f"database backend = sqlite ({db.get('sqlite_dir', '.deer-flow/data')})",
"ok",
)
if backend == "memory":
return CheckResult(
"database backend = memory",
"warn",
fix="Use 'sqlite' or 'postgres' for persistent state; memory loses data on restart",
)
if backend != "postgres":
return CheckResult(
f"database backend = {backend!r}",
"warn",
fix="Set database.backend to 'sqlite', 'postgres', or 'memory' in config.yaml",
)
# ── postgres path: resolve URL and try a live connection ───────────────
raw_url = db.get("postgres_url") or db.get("url")
if isinstance(raw_url, str) and raw_url.startswith("$"):
env_name = raw_url[1:]
resolved = os.environ.get(env_name)
if not resolved:
return CheckResult(
f"database backend = postgres ({raw_url})",
"fail",
f"env var {env_name} is not set",
fix=f"Set {env_name} in .env (e.g. postgresql+asyncpg://user:pass@host:5432/db)",
)
url = resolved
elif isinstance(raw_url, str) and raw_url:
url = raw_url
else:
return CheckResult(
"database backend = postgres",
"fail",
"postgres_url not configured",
fix="Set database.postgres_url in config.yaml (or use $DATABASE_URL env ref)",
)
# Strip SQLAlchemy dialect prefix for raw asyncpg connect.
asyncpg_url = url.replace("postgresql+asyncpg://", "postgresql://")
try:
import asyncio
import asyncpg # type: ignore[import-not-found]
except ImportError:
return CheckResult(
"database backend = postgres",
"warn",
"asyncpg not installed",
fix="cd backend && uv sync --extra postgres",
)
async def _ping() -> str:
conn = await asyncpg.connect(asyncpg_url, timeout=5)
try:
return await conn.fetchval("SELECT version()")
finally:
await conn.close()
try:
version = asyncio.run(_ping())
# Show just "PostgreSQL 16.4 ..." prefix, not the full build banner
short = version.split(" on ")[0] if version else "unknown"
return CheckResult(f"postgres reachable ({short})", "ok")
except Exception as exc:
return CheckResult(
"postgres reachable",
"fail",
str(exc),
fix=(
"Verify DATABASE_URL host/port/credentials; "
"for local dev start postgres via 'docker compose -f docker/docker-compose-dev.yaml up -d postgres'"
),
)
def check_sandbox(config_path: Path) -> list[CheckResult]:
if not config_path.exists():
return [CheckResult("sandbox configured", "skip")]
@@ -682,6 +789,10 @@ def main() -> int:
search_checks = [check_web_search(config_path), check_web_fetch(config_path)]
sections.append(("Web Capabilities", search_checks))
# ── Database ──────────────────────────────────────────────────────────────
db_checks = [check_database(config_path)]
sections.append(("Database", db_checks))
# ── Sandbox ──────────────────────────────────────────────────────────────
sandbox_checks = check_sandbox(config_path)
sections.append(("Sandbox", sandbox_checks))