feat(routing): ordered backend candidates + real-probing doctor
- backends is now an ordered candidate list (first = preferred); channels report the backend actually serving via active_backend, surfaced in the doctor text report and --json - new agent_reach/probe.py really executes upstream commands and tells apart missing / broken (stale venv shebang after a system Python upgrade) / timeout, with a reinstall prescription for broken installs - all 13 channels migrated off which()-only checks: fixes bilibili false-positive "bili-cli 可用" on broken shims, misleading xiaohongshu "连接失败", rdt OSError crashing doctor, mcporter breakage masquerading as "未配置" - twitter: 15s probe + 1 retry (flaky 10s timeout), broken twitter-cli now falls back to bird instead of aborting the check - doctor survives per-channel exceptions; config supports per-channel backend override (<channel>_backend / <CHANNEL>_BACKEND env) - fix skill install/uninstall crash on symlinked skill dirs (the "[Errno None] None" warning from shutil.rmtree on a symlink) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+22
-7
@@ -10,20 +10,37 @@ from agent_reach.channels import get_all_channels
|
||||
|
||||
|
||||
def check_all(config: Config) -> Dict[str, dict]:
|
||||
"""Check all channels and return status dict."""
|
||||
"""Check all channels and return status dict.
|
||||
|
||||
A single misbehaving channel must never take the whole report down,
|
||||
so per-channel exceptions degrade to status="error".
|
||||
"""
|
||||
results = {}
|
||||
for ch in get_all_channels():
|
||||
status, message = ch.check(config)
|
||||
try:
|
||||
status, message = ch.check(config)
|
||||
except Exception as e: # noqa: BLE001 — doctor must survive any channel
|
||||
status, message = "error", f"体检异常:{e}"
|
||||
results[ch.name] = {
|
||||
"status": status,
|
||||
"name": ch.description,
|
||||
"message": message,
|
||||
"tier": ch.tier,
|
||||
"backends": ch.backends,
|
||||
"active_backend": getattr(ch, "active_backend", None),
|
||||
}
|
||||
return results
|
||||
|
||||
|
||||
def _name_msg(r: dict, escape) -> str:
|
||||
"""Render one channel line; show the active backend when there is a choice."""
|
||||
text = f"[bold]{escape(r['name'])}[/bold] — {escape(r['message'])}"
|
||||
active = r.get("active_backend")
|
||||
if active and len(r.get("backends", [])) > 1:
|
||||
text += f" [dim](当前后端:{escape(active)})[/dim]"
|
||||
return text
|
||||
|
||||
|
||||
def format_report(results: Dict[str, dict]) -> str:
|
||||
"""Format results as a readable text report (with Rich markup)."""
|
||||
try:
|
||||
@@ -44,7 +61,7 @@ def format_report(results: Dict[str, dict]) -> str:
|
||||
lines.append("[bold]✅ 装好即用:[/bold]")
|
||||
for key, r in results.items():
|
||||
if r["tier"] == 0:
|
||||
name_msg = f"[bold]{escape(r['name'])}[/bold] — {escape(r['message'])}"
|
||||
name_msg = _name_msg(r, escape)
|
||||
if r["status"] == "ok":
|
||||
lines.append(f" [green]✅[/green] {name_msg}")
|
||||
elif r["status"] == "warn":
|
||||
@@ -60,8 +77,7 @@ def format_report(results: Dict[str, dict]) -> str:
|
||||
lines.append("")
|
||||
lines.append("[bold]可选渠道(已安装):[/bold]")
|
||||
for key, r in tier1_active.items():
|
||||
name_msg = f"[bold]{escape(r['name'])}[/bold] — {escape(r['message'])}"
|
||||
lines.append(f" [green]✅[/green] {name_msg}")
|
||||
lines.append(f" [green]✅[/green] {_name_msg(r, escape)}")
|
||||
|
||||
# Tier 2 — optional complex setup
|
||||
tier2 = {k: r for k, r in results.items() if r["tier"] == 2}
|
||||
@@ -72,8 +88,7 @@ def format_report(results: Dict[str, dict]) -> str:
|
||||
lines.append("")
|
||||
lines.append("[bold]可选渠道(已安装):[/bold]")
|
||||
for key, r in tier2_active.items():
|
||||
name_msg = f"[bold]{escape(r['name'])}[/bold] — {escape(r['message'])}"
|
||||
lines.append(f" [green]✅[/green] {name_msg}")
|
||||
lines.append(f" [green]✅[/green] {_name_msg(r, escape)}")
|
||||
|
||||
lines.append("")
|
||||
status_color = "green" if ok_count == total else ("yellow" if ok_count > 0 else "red")
|
||||
|
||||
Reference in New Issue
Block a user