feat(install): split into core + optional channels with --channels flag (#237)
Base install now only sets up lightweight zero-config channels (Web, YouTube, GitHub, RSS, Exa, V2EX, Bilibili basic). Optional channels (Twitter, Weibo, WeChat, Xiaoyuzhou, XiaoHongShu, Reddit, Bilibili full, Douyin, LinkedIn) are installed on demand via --channels flag. - Add --channels param to install subcommand - Extract twitter/xhs/reddit/bili into independent install functions - Remove heavy deps (weibo/xiaoyuzhou/wechat/twitter-cli/xhs-cli) from default _install_system_deps() and _install_mcporter() - Cookie import only triggers when cookie-needing channels are selected - Doctor output: inactive optional channels summarized in one line - install.md: two-step flow (basics → ask user which channels) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+152
-114
@@ -68,6 +68,10 @@ def main():
|
|||||||
help="Safe mode: skip automatic system changes, show what's needed instead")
|
help="Safe mode: skip automatic system changes, show what's needed instead")
|
||||||
p_install.add_argument("--dry-run", action="store_true",
|
p_install.add_argument("--dry-run", action="store_true",
|
||||||
help="Show what would be done without making any changes")
|
help="Show what would be done without making any changes")
|
||||||
|
p_install.add_argument("--channels", default="",
|
||||||
|
help="Comma-separated optional channels to install "
|
||||||
|
"(twitter,weibo,wechat,xiaoyuzhou,xueqiu,xiaohongshu,"
|
||||||
|
"reddit,bilibili,douyin,linkedin,all)")
|
||||||
|
|
||||||
# ── configure ──
|
# ── configure ──
|
||||||
p_conf = sub.add_parser("configure", help="Set a config value or auto-extract from browser")
|
p_conf = sub.add_parser("configure", help="Set a config value or auto-extract from browser")
|
||||||
@@ -173,6 +177,28 @@ def _cmd_install(args):
|
|||||||
print("SAFE MODE — skipping automatic system changes")
|
print("SAFE MODE — skipping automatic system changes")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
# ── Parse --channels ──
|
||||||
|
CHANNEL_INSTALLERS = {
|
||||||
|
"twitter": _install_twitter_deps,
|
||||||
|
"weibo": _install_weibo_deps,
|
||||||
|
"wechat": _install_wechat_deps,
|
||||||
|
"xiaoyuzhou": _install_xiaoyuzhou_deps,
|
||||||
|
"xiaohongshu": _install_xhs_deps,
|
||||||
|
"reddit": _install_reddit_deps,
|
||||||
|
"bilibili": _install_bili_deps,
|
||||||
|
# xueqiu: cookie-only, no install step
|
||||||
|
# douyin/linkedin: manual setup, no auto-install
|
||||||
|
}
|
||||||
|
COOKIE_CHANNELS = {"twitter", "xueqiu", "bilibili"}
|
||||||
|
|
||||||
|
requested_channels = set()
|
||||||
|
if args.channels:
|
||||||
|
raw = [c.strip().lower() for c in args.channels.split(",") if c.strip()]
|
||||||
|
if "all" in raw:
|
||||||
|
requested_channels = set(CHANNEL_INSTALLERS.keys()) | {"xueqiu", "douyin", "linkedin"}
|
||||||
|
else:
|
||||||
|
requested_channels = set(raw)
|
||||||
|
|
||||||
# Auto-detect environment
|
# Auto-detect environment
|
||||||
env = args.env
|
env = args.env
|
||||||
if env == "auto":
|
if env == "auto":
|
||||||
@@ -191,7 +217,7 @@ def _cmd_install(args):
|
|||||||
config.set("bilibili_proxy", args.proxy)
|
config.set("bilibili_proxy", args.proxy)
|
||||||
print(f"✅ Proxy configured for Bilibili")
|
print(f"✅ Proxy configured for Bilibili")
|
||||||
|
|
||||||
# ── Install system dependencies ──
|
# ── Install core system dependencies (lightweight, always) ──
|
||||||
print()
|
print()
|
||||||
if dry_run:
|
if dry_run:
|
||||||
_install_system_deps_dryrun()
|
_install_system_deps_dryrun()
|
||||||
@@ -200,7 +226,7 @@ def _cmd_install(args):
|
|||||||
else:
|
else:
|
||||||
_install_system_deps()
|
_install_system_deps()
|
||||||
|
|
||||||
# ── mcporter (for Exa search + XiaoHongShu) ──
|
# ── mcporter (for Exa search) ──
|
||||||
print()
|
print()
|
||||||
if dry_run:
|
if dry_run:
|
||||||
print("[dry-run] Would install mcporter and configure Exa search")
|
print("[dry-run] Would install mcporter and configure Exa search")
|
||||||
@@ -209,10 +235,26 @@ def _cmd_install(args):
|
|||||||
else:
|
else:
|
||||||
_install_mcporter()
|
_install_mcporter()
|
||||||
|
|
||||||
# Auto-import cookies on local computers
|
# ── Install optional channels (only if --channels specified) ──
|
||||||
if env == "local" and not safe_mode and not dry_run:
|
if requested_channels and not dry_run and not safe_mode:
|
||||||
print()
|
print()
|
||||||
print("Trying to import cookies from browser...")
|
print("Installing optional channels...")
|
||||||
|
for ch_name in sorted(requested_channels):
|
||||||
|
installer = CHANNEL_INSTALLERS.get(ch_name)
|
||||||
|
if installer:
|
||||||
|
installer()
|
||||||
|
|
||||||
|
if requested_channels and dry_run:
|
||||||
|
print()
|
||||||
|
print(f"[dry-run] Would install optional channels: {', '.join(sorted(requested_channels))}")
|
||||||
|
|
||||||
|
# ── Auto-import cookies (only if cookie-needing channels are requested) ──
|
||||||
|
needs_cookies = bool(requested_channels & COOKIE_CHANNELS)
|
||||||
|
if env == "local" and needs_cookies and not safe_mode and not dry_run:
|
||||||
|
print()
|
||||||
|
print("Importing cookies from browser...")
|
||||||
|
print(" (macOS may ask for your login password to access the Keychain — this is normal,")
|
||||||
|
print(" it only happens once during install. Enter your password or click 'Allow'.)")
|
||||||
try:
|
try:
|
||||||
from agent_reach.cookie_extract import configure_from_browser
|
from agent_reach.cookie_extract import configure_from_browser
|
||||||
results = configure_from_browser("chrome", config)
|
results = configure_from_browser("chrome", config)
|
||||||
@@ -222,7 +264,6 @@ def _cmd_install(args):
|
|||||||
print(f" ✅ {platform}: {message}")
|
print(f" ✅ {platform}: {message}")
|
||||||
found = True
|
found = True
|
||||||
if not found:
|
if not found:
|
||||||
# Try firefox
|
|
||||||
results = configure_from_browser("firefox", config)
|
results = configure_from_browser("firefox", config)
|
||||||
for platform, success, message in results:
|
for platform, success, message in results:
|
||||||
if success:
|
if success:
|
||||||
@@ -231,12 +272,8 @@ def _cmd_install(args):
|
|||||||
if not found:
|
if not found:
|
||||||
print(" -- No cookies found (normal if you haven't logged into these sites)")
|
print(" -- No cookies found (normal if you haven't logged into these sites)")
|
||||||
except Exception:
|
except Exception:
|
||||||
print(" -- Could not read browser cookies (browser might be open)")
|
print(" -- Could not read browser cookies (browser might be open or password was denied)")
|
||||||
elif env == "local" and safe_mode:
|
elif env == "local" and needs_cookies and dry_run:
|
||||||
print()
|
|
||||||
print("Skipped auto cookie import (safe mode).")
|
|
||||||
print(" To import manually: agent-reach configure --from-browser chrome")
|
|
||||||
elif env == "local" and dry_run:
|
|
||||||
print()
|
print()
|
||||||
print("[dry-run] Would try to import cookies from Chrome/Firefox")
|
print("[dry-run] Would try to import cookies from Chrome/Firefox")
|
||||||
|
|
||||||
@@ -266,9 +303,12 @@ def _cmd_install(args):
|
|||||||
|
|
||||||
print(f"✅ Installation complete! {ok}/{total} channels active.")
|
print(f"✅ Installation complete! {ok}/{total} channels active.")
|
||||||
|
|
||||||
if ok < total:
|
if not requested_channels:
|
||||||
|
# First install — hint about optional channels
|
||||||
print()
|
print()
|
||||||
print("Some channels are not active yet. If you're an AI Agent, try to fix them yourself — then run `agent-reach doctor` again.")
|
print("More channels available! Use --channels to install:")
|
||||||
|
print(" agent-reach install --channels=twitter,weibo,xiaohongshu,...")
|
||||||
|
print(" agent-reach install --channels=all (install everything)")
|
||||||
|
|
||||||
# Star reminder
|
# Star reminder
|
||||||
print()
|
print()
|
||||||
@@ -502,37 +542,6 @@ def _install_system_deps():
|
|||||||
except Exception:
|
except Exception:
|
||||||
print(" [!] Node.js install failed. Try: apt install nodejs npm, or nvm install 22, or download from https://nodejs.org")
|
print(" [!] Node.js install failed. Try: apt install nodejs npm, or nvm install 22, or download from https://nodejs.org")
|
||||||
|
|
||||||
# ── twitter-cli (for Twitter search) ──
|
|
||||||
if shutil.which("twitter"):
|
|
||||||
print(" ✅ twitter-cli already installed")
|
|
||||||
else:
|
|
||||||
if shutil.which("pipx"):
|
|
||||||
try:
|
|
||||||
subprocess.run(
|
|
||||||
["pipx", "install", "twitter-cli"],
|
|
||||||
capture_output=True, encoding="utf-8", errors="replace", timeout=120,
|
|
||||||
)
|
|
||||||
if shutil.which("twitter"):
|
|
||||||
print(" ✅ twitter-cli installed (Twitter search + timeline + article)")
|
|
||||||
else:
|
|
||||||
print(" -- twitter-cli install failed (optional — Twitter reading still works via Jina)")
|
|
||||||
except Exception:
|
|
||||||
print(" -- twitter-cli install failed (optional — Twitter reading still works via Jina)")
|
|
||||||
elif shutil.which("uv"):
|
|
||||||
try:
|
|
||||||
subprocess.run(
|
|
||||||
["uv", "tool", "install", "twitter-cli"],
|
|
||||||
capture_output=True, encoding="utf-8", errors="replace", timeout=120,
|
|
||||||
)
|
|
||||||
if shutil.which("twitter"):
|
|
||||||
print(" ✅ twitter-cli installed (Twitter search + timeline + article)")
|
|
||||||
else:
|
|
||||||
print(" -- twitter-cli install failed (optional)")
|
|
||||||
except Exception:
|
|
||||||
print(" -- twitter-cli install failed (optional)")
|
|
||||||
else:
|
|
||||||
print(" -- twitter-cli requires pipx or uv (optional — Twitter reading still works via Jina)")
|
|
||||||
|
|
||||||
# ── undici (proxy support for Node.js fetch) ──
|
# ── undici (proxy support for Node.js fetch) ──
|
||||||
npm_cmd = shutil.which("npm")
|
npm_cmd = shutil.which("npm")
|
||||||
if npm_cmd:
|
if npm_cmd:
|
||||||
@@ -566,14 +575,9 @@ def _install_system_deps():
|
|||||||
except Exception:
|
except Exception:
|
||||||
print(" -- Could not configure yt-dlp JS runtime (YouTube may not work)")
|
print(" -- Could not configure yt-dlp JS runtime (YouTube may not work)")
|
||||||
|
|
||||||
# ── Weibo (mcp-server-weibo fork with visitor passport fix) ──
|
# NOTE: twitter-cli, weibo, xiaoyuzhou, wechat, xhs-cli etc. are optional.
|
||||||
_install_weibo_deps()
|
# They are installed via --channels flag, not here.
|
||||||
|
# See CHANNEL_INSTALLERS in _cmd_install().
|
||||||
# ── Xiaoyuzhou Podcast (transcribe.sh + ffmpeg) ──
|
|
||||||
_install_xiaoyuzhou_deps()
|
|
||||||
|
|
||||||
# ── WeChat Articles (miku_ai + camoufox + wechat-article-for-ai) ──
|
|
||||||
_install_wechat_deps()
|
|
||||||
|
|
||||||
|
|
||||||
def _install_xiaoyuzhou_deps():
|
def _install_xiaoyuzhou_deps():
|
||||||
@@ -619,6 +623,98 @@ def _install_xiaoyuzhou_deps():
|
|||||||
print(" Then run: agent-reach configure groq-key gsk_xxxxx")
|
print(" Then run: agent-reach configure groq-key gsk_xxxxx")
|
||||||
|
|
||||||
|
|
||||||
|
def _install_twitter_deps():
|
||||||
|
"""Install twitter-cli for Twitter search + timeline."""
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
print("Setting up Twitter (twitter-cli)...")
|
||||||
|
if shutil.which("twitter"):
|
||||||
|
print(" ✅ twitter-cli already installed")
|
||||||
|
return
|
||||||
|
for tool, cmd in [("pipx", ["pipx", "install", "twitter-cli"]),
|
||||||
|
("uv", ["uv", "tool", "install", "twitter-cli"])]:
|
||||||
|
if shutil.which(tool):
|
||||||
|
try:
|
||||||
|
subprocess.run(cmd, capture_output=True, encoding="utf-8",
|
||||||
|
errors="replace", timeout=120)
|
||||||
|
if shutil.which("twitter"):
|
||||||
|
print(" ✅ twitter-cli installed")
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
print(" [!] twitter-cli install failed. Run: pipx install twitter-cli")
|
||||||
|
|
||||||
|
|
||||||
|
def _install_xhs_deps():
|
||||||
|
"""Install xhs-cli (xiaohongshu-cli) for XiaoHongShu."""
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
print("Setting up XiaoHongShu (xhs-cli)...")
|
||||||
|
if shutil.which("xhs"):
|
||||||
|
print(" ✅ xhs-cli already installed")
|
||||||
|
return
|
||||||
|
for tool, cmd in [("pipx", ["pipx", "install", "xiaohongshu-cli"]),
|
||||||
|
("uv", ["uv", "tool", "install", "xiaohongshu-cli"])]:
|
||||||
|
if shutil.which(tool):
|
||||||
|
try:
|
||||||
|
subprocess.run(cmd, capture_output=True, encoding="utf-8",
|
||||||
|
errors="replace", timeout=120)
|
||||||
|
if shutil.which("xhs"):
|
||||||
|
print(" ✅ xhs-cli installed (run `xhs login` to authenticate)")
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
print(" [!] xhs-cli install failed. Run: pipx install xiaohongshu-cli")
|
||||||
|
|
||||||
|
|
||||||
|
def _install_reddit_deps():
|
||||||
|
"""Install rdt-cli for Reddit search + reading."""
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
print("Setting up Reddit (rdt-cli)...")
|
||||||
|
if shutil.which("rdt"):
|
||||||
|
print(" ✅ rdt-cli already installed")
|
||||||
|
return
|
||||||
|
for tool, cmd in [("pipx", ["pipx", "install", "rdt-cli"]),
|
||||||
|
("uv", ["uv", "tool", "install", "rdt-cli"])]:
|
||||||
|
if shutil.which(tool):
|
||||||
|
try:
|
||||||
|
subprocess.run(cmd, capture_output=True, encoding="utf-8",
|
||||||
|
errors="replace", timeout=120)
|
||||||
|
if shutil.which("rdt"):
|
||||||
|
print(" ✅ rdt-cli installed")
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
print(" [!] rdt-cli install failed. Run: pipx install rdt-cli")
|
||||||
|
|
||||||
|
|
||||||
|
def _install_bili_deps():
|
||||||
|
"""Install bili-cli for Bilibili hot/rank/search."""
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
print("Setting up Bilibili (bili-cli)...")
|
||||||
|
if shutil.which("bili"):
|
||||||
|
print(" ✅ bili-cli already installed")
|
||||||
|
return
|
||||||
|
for tool, cmd in [("pipx", ["pipx", "install", "bilibili-cli"]),
|
||||||
|
("uv", ["uv", "tool", "install", "bilibili-cli"])]:
|
||||||
|
if shutil.which(tool):
|
||||||
|
try:
|
||||||
|
subprocess.run(cmd, capture_output=True, encoding="utf-8",
|
||||||
|
errors="replace", timeout=120)
|
||||||
|
if shutil.which("bili"):
|
||||||
|
print(" ✅ bili-cli installed")
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
print(" [!] bili-cli install failed. Run: pipx install bilibili-cli")
|
||||||
|
|
||||||
|
|
||||||
def _install_weibo_deps():
|
def _install_weibo_deps():
|
||||||
"""Install Weibo MCP server (Panniantong fork with visitor passport auth)."""
|
"""Install Weibo MCP server (Panniantong fork with visitor passport auth)."""
|
||||||
import shutil
|
import shutil
|
||||||
@@ -745,7 +841,6 @@ def _install_system_deps_safe():
|
|||||||
deps = [
|
deps = [
|
||||||
("gh", ["gh"], "GitHub CLI", "https://cli.github.com — or: apt install gh / brew install gh"),
|
("gh", ["gh"], "GitHub CLI", "https://cli.github.com — or: apt install gh / brew install gh"),
|
||||||
("node", ["node", "npm"], "Node.js", "https://nodejs.org — or: apt install nodejs npm"),
|
("node", ["node", "npm"], "Node.js", "https://nodejs.org — or: apt install nodejs npm"),
|
||||||
("twitter", ["twitter"], "twitter-cli (Twitter)", "pipx install twitter-cli"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
missing = []
|
missing = []
|
||||||
@@ -765,29 +860,6 @@ def _install_system_deps_safe():
|
|||||||
else:
|
else:
|
||||||
print(" All system dependencies are installed!")
|
print(" All system dependencies are installed!")
|
||||||
|
|
||||||
# WeChat check (Python packages, not binaries)
|
|
||||||
has_camoufox = has_miku = False
|
|
||||||
try:
|
|
||||||
import camoufox # noqa: F401
|
|
||||||
has_camoufox = True
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
import miku_ai # noqa: F401
|
|
||||||
has_miku = True
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
if has_camoufox and has_miku:
|
|
||||||
print(" ✅ WeChat article tools already installed")
|
|
||||||
else:
|
|
||||||
pkgs = []
|
|
||||||
if not has_camoufox:
|
|
||||||
pkgs.extend(["camoufox[geoip]", "markdownify", "beautifulsoup4", "httpx"])
|
|
||||||
if not has_miku:
|
|
||||||
pkgs.append("miku_ai")
|
|
||||||
print(f" -- WeChat article tools not found")
|
|
||||||
print(f" Install: pip install {' '.join(pkgs)}")
|
|
||||||
|
|
||||||
|
|
||||||
def _install_system_deps_dryrun():
|
def _install_system_deps_dryrun():
|
||||||
"""Dry-run: just show what would be checked/installed."""
|
"""Dry-run: just show what would be checked/installed."""
|
||||||
@@ -798,7 +870,6 @@ def _install_system_deps_dryrun():
|
|||||||
checks = [
|
checks = [
|
||||||
("gh CLI", ["gh"], "apt install gh / brew install gh"),
|
("gh CLI", ["gh"], "apt install gh / brew install gh"),
|
||||||
("Node.js", ["node"], "curl NodeSource setup | bash + apt install nodejs"),
|
("Node.js", ["node"], "curl NodeSource setup | bash + apt install nodejs"),
|
||||||
("twitter-cli", ["twitter"], "pipx install twitter-cli"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
for label, binaries, method in checks:
|
for label, binaries, method in checks:
|
||||||
@@ -808,30 +879,14 @@ def _install_system_deps_dryrun():
|
|||||||
else:
|
else:
|
||||||
print(f" {label}: would install via: {method}")
|
print(f" {label}: would install via: {method}")
|
||||||
|
|
||||||
# WeChat
|
|
||||||
has_camoufox = has_miku = False
|
|
||||||
try:
|
|
||||||
import camoufox # noqa: F401
|
|
||||||
has_camoufox = True
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
import miku_ai # noqa: F401
|
|
||||||
has_miku = True
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
if has_camoufox and has_miku:
|
|
||||||
print(" ✅ WeChat article tools: already installed, skip")
|
|
||||||
else:
|
|
||||||
print(" WeChat article tools: would install via: pip install camoufox[geoip] markdownify beautifulsoup4 httpx miku_ai")
|
|
||||||
|
|
||||||
|
|
||||||
def _install_mcporter():
|
def _install_mcporter():
|
||||||
"""Install mcporter and configure Exa + XiaoHongShu MCP servers."""
|
"""Install mcporter and configure Exa search."""
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
print("Setting up mcporter (search + XiaoHongShu backend)...")
|
print("Setting up mcporter (search backend)...")
|
||||||
|
|
||||||
if shutil.which("mcporter"):
|
if shutil.which("mcporter"):
|
||||||
print(" ✅ mcporter already installed")
|
print(" ✅ mcporter already installed")
|
||||||
@@ -871,24 +926,7 @@ def _install_mcporter():
|
|||||||
except Exception:
|
except Exception:
|
||||||
print(" [!] Could not configure Exa. Run manually: mcporter config add exa https://mcp.exa.ai/mcp")
|
print(" [!] Could not configure Exa. Run manually: mcporter config add exa https://mcp.exa.ai/mcp")
|
||||||
|
|
||||||
# Check XiaoHongShu CLI
|
# NOTE: xhs-cli is now optional, installed via --channels=xiaohongshu
|
||||||
if shutil.which("xhs"):
|
|
||||||
print(" ✅ xhs-cli already installed (xiaohongshu-cli)")
|
|
||||||
else:
|
|
||||||
if shutil.which("pipx"):
|
|
||||||
try:
|
|
||||||
subprocess.run(
|
|
||||||
["pipx", "install", "xiaohongshu-cli"],
|
|
||||||
capture_output=True, encoding="utf-8", errors="replace", timeout=120,
|
|
||||||
)
|
|
||||||
if shutil.which("xhs"):
|
|
||||||
print(" ✅ xhs-cli installed (run `xhs login` to authenticate)")
|
|
||||||
else:
|
|
||||||
print(" -- xhs-cli install failed (optional). Run: pipx install xiaohongshu-cli")
|
|
||||||
except Exception:
|
|
||||||
print(" -- xhs-cli install failed (optional). Run: pipx install xiaohongshu-cli")
|
|
||||||
else:
|
|
||||||
print(" -- xhs-cli requires pipx (optional). Run: pipx install xiaohongshu-cli")
|
|
||||||
|
|
||||||
|
|
||||||
def _install_mcporter_safe():
|
def _install_mcporter_safe():
|
||||||
|
|||||||
+22
-18
@@ -51,37 +51,41 @@ def format_report(results: Dict[str, dict]) -> str:
|
|||||||
elif r["status"] in ("off", "error"):
|
elif r["status"] in ("off", "error"):
|
||||||
lines.append(f" [red][X][/red] {name_msg}")
|
lines.append(f" [red][X][/red] {name_msg}")
|
||||||
|
|
||||||
# Tier 1 — needs free key
|
# Tier 1 — needs free key / login
|
||||||
tier1 = {k: r for k, r in results.items() if r["tier"] == 1}
|
tier1 = {k: r for k, r in results.items() if r["tier"] == 1}
|
||||||
if tier1:
|
tier1_active = {k: r for k, r in tier1.items() if r["status"] == "ok"}
|
||||||
|
tier1_inactive = {k: r for k, r in tier1.items() if r["status"] != "ok"}
|
||||||
|
if tier1_active:
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("[bold]搜索(mcporter 即可解锁):[/bold]")
|
lines.append("[bold]可选渠道(已安装):[/bold]")
|
||||||
for key, r in tier1.items():
|
for key, r in tier1_active.items():
|
||||||
name_msg = f"[bold]{escape(r['name'])}[/bold] — {escape(r['message'])}"
|
name_msg = f"[bold]{escape(r['name'])}[/bold] — {escape(r['message'])}"
|
||||||
if r["status"] == "ok":
|
|
||||||
lines.append(f" [green]✅[/green] {name_msg}")
|
lines.append(f" [green]✅[/green] {name_msg}")
|
||||||
else:
|
|
||||||
lines.append(f" [dim]--[/dim] {name_msg}")
|
|
||||||
|
|
||||||
# Tier 2 — optional setup
|
# Tier 2 — optional complex setup
|
||||||
tier2 = {k: r for k, r in results.items() if r["tier"] == 2}
|
tier2 = {k: r for k, r in results.items() if r["tier"] == 2}
|
||||||
if tier2:
|
tier2_active = {k: r for k, r in tier2.items() if r["status"] == "ok"}
|
||||||
|
tier2_inactive = {k: r for k, r in tier2.items() if r["status"] != "ok"}
|
||||||
|
if tier2_active:
|
||||||
|
if not tier1_active:
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("[bold]配置后可用:[/bold]")
|
lines.append("[bold]可选渠道(已安装):[/bold]")
|
||||||
for key, r in tier2.items():
|
for key, r in tier2_active.items():
|
||||||
name_msg = f"[bold]{escape(r['name'])}[/bold] — {escape(r['message'])}"
|
name_msg = f"[bold]{escape(r['name'])}[/bold] — {escape(r['message'])}"
|
||||||
if r["status"] == "ok":
|
|
||||||
lines.append(f" [green]✅[/green] {name_msg}")
|
lines.append(f" [green]✅[/green] {name_msg}")
|
||||||
elif r["status"] == "warn":
|
|
||||||
lines.append(f" [yellow][!][/yellow] {name_msg}")
|
|
||||||
else:
|
|
||||||
lines.append(f" [dim]--[/dim] {name_msg}")
|
|
||||||
|
|
||||||
lines.append("")
|
lines.append("")
|
||||||
status_color = "green" if ok_count == total else ("yellow" if ok_count > 0 else "red")
|
status_color = "green" if ok_count == total else ("yellow" if ok_count > 0 else "red")
|
||||||
lines.append(f"状态:[{status_color}]{ok_count}/{total}[/{status_color}] 个渠道可用")
|
lines.append(f"状态:[{status_color}]{ok_count}/{total}[/{status_color}] 个渠道可用")
|
||||||
if ok_count < total:
|
|
||||||
lines.append("运行 [cyan]`agent-reach setup`[/cyan] 解锁更多渠道")
|
# Summarize inactive optional channels in one line instead of listing each
|
||||||
|
all_inactive = list(tier1_inactive.values()) + list(tier2_inactive.values())
|
||||||
|
if all_inactive:
|
||||||
|
names = [r["name"] for r in all_inactive]
|
||||||
|
lines.append(
|
||||||
|
f"还有 {len(names)} 个可选渠道可以解锁({'、'.join(names)}),"
|
||||||
|
"告诉你的 Agent「帮我装 XXX」即可"
|
||||||
|
)
|
||||||
|
|
||||||
# Security check: config file permissions (Unix only)
|
# Security check: config file permissions (Unix only)
|
||||||
import os
|
import os
|
||||||
|
|||||||
+36
-23
@@ -46,9 +46,7 @@ All Agent Reach files go in dedicated directories — **never in the agent works
|
|||||||
|
|
||||||
**Why?** If you clone repos or create files in the workspace, it pollutes the user's project directory and can break their agent over time. Keep the workspace clean.
|
**Why?** If you clone repos or create files in the workspace, it pollutes the user's project directory and can break their agent over time. Keep the workspace clean.
|
||||||
|
|
||||||
### Step 1: Install
|
### Step 1: Install the basics
|
||||||
|
|
||||||
**Default (fully automatic):**
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 推荐:pipx(最省心)
|
# 推荐:pipx(最省心)
|
||||||
@@ -63,40 +61,53 @@ pip install https://github.com/Panniantong/agent-reach/archive/main.zip
|
|||||||
agent-reach install --env=auto
|
agent-reach install --env=auto
|
||||||
```
|
```
|
||||||
|
|
||||||
This auto-installs system dependencies (gh CLI, Node.js, mcporter, bird), configures Exa search, detects environment, and tests all channels.
|
This installs core infrastructure (gh CLI, Node.js, mcporter, Exa search, yt-dlp config) and activates these zero-config channels:
|
||||||
|
|
||||||
|
- Web (Jina Reader), YouTube, GitHub, RSS, Exa Search, V2EX, Bilibili (basic)
|
||||||
|
|
||||||
> 💡 **macOS / Homebrew Python 提示 `externally-managed-environment`?**
|
> 💡 **macOS / Homebrew Python 提示 `externally-managed-environment`?**
|
||||||
> 这是 PEP 668 保护,不是 Agent Reach 本身的问题。优先用 `pipx install ...`,或先创建 `venv` 再安装。
|
> 这是 PEP 668 保护,不是 Agent Reach 本身的问题。优先用 `pipx install ...`,或先创建 `venv` 再安装。
|
||||||
|
|
||||||
|
**Safe mode / Dry run:**
|
||||||
**Safe mode (no automatic system changes):**
|
|
||||||
|
|
||||||
If the user asked for safe mode, or you're on a production machine:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install https://github.com/Panniantong/agent-reach/archive/main.zip
|
agent-reach install --env=auto --safe # Check only, no auto-install
|
||||||
agent-reach install --env=auto --safe
|
agent-reach install --env=auto --dry-run # Preview what would be done
|
||||||
```
|
```
|
||||||
|
|
||||||
Safe mode checks what's installed and tells you what's missing, but doesn't auto-install system packages. You can then install each dependency manually.
|
### Step 2: Ask the user which optional channels they want
|
||||||
|
|
||||||
**Dry run (preview only):**
|
After installing the basics, **ask the user** which additional channels they need. Present this list:
|
||||||
|
|
||||||
|
> 基础渠道装好了!你现在可以让我搜网页、看 YouTube、读 GitHub 等。
|
||||||
|
>
|
||||||
|
> 还有这些可选渠道,你需要哪些?
|
||||||
|
>
|
||||||
|
> - 🐦 **Twitter/X** — 搜推文、看时间线(需要登录 Cookie)
|
||||||
|
> - 📰 **微博** — 热搜、搜索、用户动态
|
||||||
|
> - 💬 **微信公众号** — 搜索和阅读公众号文章
|
||||||
|
> - 📈 **雪球** — 股票行情、热门帖子(需要登录 Cookie)
|
||||||
|
> - 🎙️ **小宇宙播客** — 音频转文字(需要免费 Groq Key)
|
||||||
|
> - 📕 **小红书** — 阅读、搜索、发帖(需要登录)
|
||||||
|
> - 📖 **Reddit** — 搜索和阅读帖子
|
||||||
|
> - 📺 **B站完整版** — 热门、排行、搜索
|
||||||
|
> - 🎵 **抖音** — 视频解析
|
||||||
|
> - 💼 **LinkedIn** — Profile、职位搜索
|
||||||
|
>
|
||||||
|
> 告诉我你要哪些,比如"帮我装微博和 Twitter"。或者说"全部装"。
|
||||||
|
|
||||||
|
Based on the user's choice, run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
agent-reach install --env=auto --dry-run
|
agent-reach install --env=auto --channels=twitter,weibo # Example: user chose Twitter + Weibo
|
||||||
|
agent-reach install --env=auto --channels=all # User wants everything
|
||||||
```
|
```
|
||||||
|
|
||||||
Shows what would be done without making any changes.
|
Supported channel names: `twitter`, `weibo`, `wechat`, `xiaoyuzhou`, `xueqiu`, `xiaohongshu`, `reddit`, `bilibili`, `douyin`, `linkedin`, `all`
|
||||||
|
|
||||||
**Read the output carefully**, then run:
|
### Step 3: Fix what's broken
|
||||||
|
|
||||||
```bash
|
Run `agent-reach doctor` and check the output.
|
||||||
agent-reach doctor
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: Fix what's broken
|
|
||||||
|
|
||||||
`agent-reach doctor` shows you which channels are active and which aren't.
|
|
||||||
|
|
||||||
Try to get as many channels to ✅ as possible. If something failed during install or shows as ❌/⚠️ in doctor, diagnose the problem and try to fix it — but stay within the boundaries above. If a fix requires elevated permissions or system changes, ask the user first.
|
Try to get as many channels to ✅ as possible. If something failed during install or shows as ❌/⚠️ in doctor, diagnose the problem and try to fix it — but stay within the boundaries above. If a fix requires elevated permissions or system changes, ask the user first.
|
||||||
|
|
||||||
@@ -358,7 +369,9 @@ If the user wants a different agent to handle it, let them choose.
|
|||||||
|
|
||||||
| Command | What it does |
|
| Command | What it does |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `agent-reach install --env=auto` | Full auto-setup (installs deps + configures) |
|
| `agent-reach install --env=auto` | Install core channels (lightweight, zero-config) |
|
||||||
|
| `agent-reach install --env=auto --channels=twitter,weibo` | Install core + optional channels |
|
||||||
|
| `agent-reach install --env=auto --channels=all` | Install everything |
|
||||||
| `agent-reach install --env=auto --safe` | Safe setup (no auto system changes) |
|
| `agent-reach install --env=auto --safe` | Safe setup (no auto system changes) |
|
||||||
| `agent-reach install --env=auto --dry-run` | Preview what would be done |
|
| `agent-reach install --env=auto --dry-run` | Preview what would be done |
|
||||||
| `agent-reach doctor` | Show channel status |
|
| `agent-reach doctor` | Show channel status |
|
||||||
|
|||||||
@@ -95,7 +95,6 @@ class TestDoctor:
|
|||||||
plain = re.sub(r"\[[^\]]*\]", "", report)
|
plain = re.sub(r"\[[^\]]*\]", "", report)
|
||||||
assert "Agent Reach" in plain
|
assert "Agent Reach" in plain
|
||||||
assert "装好即用:" in plain
|
assert "装好即用:" in plain
|
||||||
assert "搜索(mcporter 即可解锁):" in plain
|
|
||||||
assert "配置后可用:" in plain
|
|
||||||
assert "1/3 个渠道可用" in plain
|
assert "1/3 个渠道可用" in plain
|
||||||
assert "agent-reach setup" in plain
|
# Inactive optional channels should be summarized in one line
|
||||||
|
assert "可选渠道可以解锁" in plain
|
||||||
|
|||||||
Reference in New Issue
Block a user