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:
@@ -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]:
|
def check_sandbox(config_path: Path) -> list[CheckResult]:
|
||||||
if not config_path.exists():
|
if not config_path.exists():
|
||||||
return [CheckResult("sandbox configured", "skip")]
|
return [CheckResult("sandbox configured", "skip")]
|
||||||
@@ -682,6 +789,10 @@ def main() -> int:
|
|||||||
search_checks = [check_web_search(config_path), check_web_fetch(config_path)]
|
search_checks = [check_web_search(config_path), check_web_fetch(config_path)]
|
||||||
sections.append(("Web Capabilities", search_checks))
|
sections.append(("Web Capabilities", search_checks))
|
||||||
|
|
||||||
|
# ── Database ──────────────────────────────────────────────────────────────
|
||||||
|
db_checks = [check_database(config_path)]
|
||||||
|
sections.append(("Database", db_checks))
|
||||||
|
|
||||||
# ── Sandbox ──────────────────────────────────────────────────────────────
|
# ── Sandbox ──────────────────────────────────────────────────────────────
|
||||||
sandbox_checks = check_sandbox(config_path)
|
sandbox_checks = check_sandbox(config_path)
|
||||||
sections.append(("Sandbox", sandbox_checks))
|
sections.append(("Sandbox", sandbox_checks))
|
||||||
|
|||||||
Reference in New Issue
Block a user