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:
Pnant
2026-06-11 15:48:33 +08:00
parent 447dc4acc4
commit 762824c590
23 changed files with 988 additions and 177 deletions
+22 -7
View File
@@ -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")