v2.0.0 — Pure glue architecture: zero copied code, pluggable channels

BREAKING: Complete architectural rewrite.

Before: Copied x-reader's fetcher code into readers/ (1205 lines of borrowed code)
After: Pluggable channel system where each channel is a thin wrapper (~50 lines)
       around the best external tool for that platform. Zero copied code.

Architecture:
- channels/base.py — Universal Channel interface (read, search, check)
- channels/web.py — Jina Reader API (swappable)
- channels/github.py — GitHub API (swappable)
- channels/twitter.py — birdx + Jina fallback (swappable)
- channels/youtube.py — yt-dlp (swappable)
- channels/reddit.py — Reddit JSON API + proxy (swappable)
- channels/rss.py — feedparser (swappable)
- channels/bilibili.py — Bilibili API (swappable)
- channels/exa_search.py — Exa semantic search (swappable)

Key design: every backend can be swapped by changing ONE file.
YouTube dies? Change youtube.py. Exa sucks? Swap exa_search.py for Tavily.
Nothing else changes.

Removed: reader.py, schema.py, readers/, search/, utils/ (all x-reader code)
Tests: 36/36 passing
This commit is contained in:
Panniantong
2026-02-24 05:38:21 +01:00
parent 7e4cd961ee
commit 74c3df5c3d
38 changed files with 1155 additions and 2725 deletions
+33 -190
View File
@@ -1,187 +1,26 @@
# -*- coding: utf-8 -*-
"""Environment health checker for Agent Eyes.
"""Environment health checker — powered by channels.
Checks all platforms, tools, and API keys.
Outputs a rich-formatted status report.
Each channel knows how to check itself. Doctor just collects the results.
"""
import shutil
import subprocess
from typing import Dict
from agent_eyes.config import Config
STATUS_OK = "ok"
STATUS_WARN = "warn"
STATUS_OFF = "off"
STATUS_ERROR = "error"
def _check_command(cmd: str) -> bool:
"""Check if a command exists on PATH."""
return shutil.which(cmd) is not None
def _check_python_import(module: str) -> bool:
"""Check if a Python module is importable."""
try:
__import__(module)
return True
except ImportError:
return False
from agent_eyes.channels import get_all_channels
def check_all(config: Config) -> Dict[str, dict]:
"""Check all features and return status dict."""
"""Check all channels and return status dict."""
results = {}
# === Zero-config (always available) ===
results["web"] = {
"status": STATUS_OK,
"name": "Web Pages",
"message": "Jina Reader (built-in)",
"tier": 0,
}
results["github_read"] = {
"status": STATUS_OK,
"name": "GitHub",
"message": "Public API (built-in)",
"tier": 0,
}
results["bilibili"] = {
"status": STATUS_OK,
"name": "Bilibili",
"message": "Public API (built-in)",
"tier": 0,
}
results["rss"] = {
"status": STATUS_OK,
"name": "RSS",
"message": "feedparser (built-in)",
"tier": 0,
}
results["tweet_read"] = {
"status": STATUS_OK,
"name": "Tweet (single)",
"message": "Jina Reader (built-in)",
"tier": 0,
}
# YouTube — needs yt-dlp
if _check_command("yt-dlp"):
results["youtube"] = {
"status": STATUS_OK,
"name": "YouTube",
"message": "yt-dlp found",
"tier": 0,
for ch in get_all_channels():
status, message = ch.check(config)
results[ch.name] = {
"status": status,
"name": ch.description,
"message": message,
"tier": ch.tier,
"backends": ch.backends,
}
else:
results["youtube"] = {
"status": STATUS_WARN,
"name": "YouTube",
"message": "Install yt-dlp: pip install yt-dlp",
"tier": 0,
}
# === Needs API key (Tier 1) ===
exa_key = config.get("exa_api_key")
if exa_key:
results["search_web"] = {
"status": STATUS_OK,
"name": "Web Search",
"message": "Exa API configured",
"tier": 1,
}
results["search_reddit"] = {
"status": STATUS_OK,
"name": "Reddit Search",
"message": "Via Exa (site:reddit.com)",
"tier": 1,
}
results["search_twitter"] = {
"status": STATUS_OK,
"name": "Twitter Search",
"message": "Via Exa (site:x.com)",
"tier": 1,
}
else:
for key in ("search_web", "search_reddit", "search_twitter"):
results[key] = {
"status": STATUS_OFF,
"name": {"search_web": "Web Search", "search_reddit": "Reddit Search",
"search_twitter": "Twitter Search"}[key],
"message": "Need Exa API key (free). Run: agent-eyes setup",
"tier": 1,
}
# GitHub search (always works, token optional for higher limits)
github_token = config.get("github_token")
results["search_github"] = {
"status": STATUS_OK,
"name": "GitHub Search",
"message": f"API ({'authenticated' if github_token else 'public, 60 req/hr'})",
"tier": 0,
}
# === Optional tools (Tier 2) ===
# Twitter advanced (birdx)
if _check_command("birdx"):
results["twitter_advanced"] = {
"status": STATUS_OK,
"name": "Twitter Advanced",
"message": "birdx found",
"tier": 2,
}
else:
results["twitter_advanced"] = {
"status": STATUS_OFF,
"name": "Twitter Advanced",
"message": "Install birdx for timeline/deep search",
"tier": 2,
}
# Reddit full reader
reddit_proxy = config.get("reddit_proxy")
if reddit_proxy:
results["reddit_full"] = {
"status": STATUS_OK,
"name": "Reddit Reader",
"message": "Proxy configured",
"tier": 2,
}
else:
results["reddit_full"] = {
"status": STATUS_OFF,
"name": "Reddit Reader",
"message": "Need proxy for full post reading",
"tier": 2,
}
# WeChat / XHS (playwright)
if _check_python_import("playwright"):
results["wechat"] = {
"status": STATUS_OK,
"name": "WeChat",
"message": "Playwright available",
"tier": 2,
}
results["xhs"] = {
"status": STATUS_OK,
"name": "XiaoHongShu",
"message": "Playwright available",
"tier": 2,
}
else:
for key in ("wechat", "xhs"):
results[key] = {
"status": STATUS_OFF,
"name": "WeChat" if key == "wechat" else "XiaoHongShu",
"message": "pip install agent-eyes[browser]",
"tier": 2,
}
return results
@@ -191,35 +30,39 @@ def format_report(results: Dict[str, dict]) -> str:
lines.append("👁️ Agent Eyes Status")
lines.append("=" * 40)
# Count stats
ok_count = sum(1 for r in results.values() if r["status"] == STATUS_OK)
ok_count = sum(1 for r in results.values() if r["status"] == "ok")
total = len(results)
# Group by tier
# Tier 0 — zero config
lines.append("")
lines.append("✅ Ready (no setup needed):")
for key, r in results.items():
if r["tier"] == 0 and r["status"] == STATUS_OK:
lines.append(f"{r['name']}")
elif r["tier"] == 0 and r["status"] == STATUS_WARN:
if r["tier"] == 0 and r["status"] == "ok":
backends = ", ".join(r["backends"]) if r["backends"] else "built-in"
lines.append(f"{r['name']} [{backends}]")
elif r["tier"] == 0 and r["status"] in ("warn", "off"):
lines.append(f" ⚠️ {r['name']}{r['message']}")
lines.append("")
lines.append("🔍 Search (need free Exa API key):")
for key, r in results.items():
if r["tier"] == 1:
icon = "" if r["status"] == STATUS_OK else ""
# Tier 1 — needs free key
tier1 = {k: r for k, r in results.items() if r["tier"] == 1}
if tier1:
lines.append("")
lines.append("🔍 Search (need free Exa API key):")
for key, r in tier1.items():
icon = "" if r["status"] == "ok" else ""
lines.append(f" {icon} {r['name']}")
lines.append("")
lines.append("🔧 Optional (advanced setup):")
for key, r in results.items():
if r["tier"] == 2:
icon = "" if r["status"] == STATUS_OK else ""
# Tier 2 — optional setup
tier2 = {k: r for k, r in results.items() if r["tier"] == 2}
if tier2:
lines.append("")
lines.append("🔧 Optional (advanced setup):")
for key, r in tier2.items():
icon = "" if r["status"] == "ok" else ""
lines.append(f" {icon} {r['name']}{r['message']}")
lines.append("")
lines.append(f"Status: {ok_count}/{total} platforms active")
lines.append(f"Status: {ok_count}/{total} channels active")
if ok_count < total:
lines.append("Run `agent-eyes setup` to unlock more!")