From 75bc98a7ebcb6b9e2e16528bfc8d894408ed4322 Mon Sep 17 00:00:00 2001 From: Pnant <73925474+Panniantong@users.noreply.github.com> Date: Tue, 31 Mar 2026 18:19:59 +0800 Subject: [PATCH] feat(install): split into core + optional channels with --channels flag (#237) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- agent_reach/cli.py | 268 ++++++++++++++++++++++++------------------ agent_reach/doctor.py | 46 ++++---- docs/install.md | 59 ++++++---- tests/test_doctor.py | 5 +- 4 files changed, 216 insertions(+), 162 deletions(-) diff --git a/agent_reach/cli.py b/agent_reach/cli.py index f41f19d..9f36fe8 100644 --- a/agent_reach/cli.py +++ b/agent_reach/cli.py @@ -68,6 +68,10 @@ def main(): help="Safe mode: skip automatic system changes, show what's needed instead") p_install.add_argument("--dry-run", action="store_true", 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 ── p_conf = sub.add_parser("configure", help="Set a config value or auto-extract from browser") @@ -173,11 +177,33 @@ def _cmd_install(args): print("SAFE MODE — skipping automatic system changes") 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 env = args.env if env == "auto": env = _detect_environment() - + if env == "server": print(f"Environment: Server/VPS (auto-detected)") else: @@ -191,7 +217,7 @@ def _cmd_install(args): config.set("bilibili_proxy", args.proxy) print(f"✅ Proxy configured for Bilibili") - # ── Install system dependencies ── + # ── Install core system dependencies (lightweight, always) ── print() if dry_run: _install_system_deps_dryrun() @@ -200,7 +226,7 @@ def _cmd_install(args): else: _install_system_deps() - # ── mcporter (for Exa search + XiaoHongShu) ── + # ── mcporter (for Exa search) ── print() if dry_run: print("[dry-run] Would install mcporter and configure Exa search") @@ -209,10 +235,26 @@ def _cmd_install(args): else: _install_mcporter() - # Auto-import cookies on local computers - if env == "local" and not safe_mode and not dry_run: + # ── Install optional channels (only if --channels specified) ── + if requested_channels and not dry_run and not safe_mode: 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: from agent_reach.cookie_extract import configure_from_browser results = configure_from_browser("chrome", config) @@ -222,7 +264,6 @@ def _cmd_install(args): print(f" ✅ {platform}: {message}") found = True if not found: - # Try firefox results = configure_from_browser("firefox", config) for platform, success, message in results: if success: @@ -231,12 +272,8 @@ def _cmd_install(args): if not found: print(" -- No cookies found (normal if you haven't logged into these sites)") except Exception: - print(" -- Could not read browser cookies (browser might be open)") - elif env == "local" and safe_mode: - 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(" -- Could not read browser cookies (browser might be open or password was denied)") + elif env == "local" and needs_cookies and dry_run: print() 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.") - if ok < total: + if not requested_channels: + # First install — hint about optional channels 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 print() @@ -502,37 +542,6 @@ def _install_system_deps(): except Exception: 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) ── npm_cmd = shutil.which("npm") if npm_cmd: @@ -566,14 +575,9 @@ def _install_system_deps(): except Exception: print(" -- Could not configure yt-dlp JS runtime (YouTube may not work)") - # ── Weibo (mcp-server-weibo fork with visitor passport fix) ── - _install_weibo_deps() - - # ── Xiaoyuzhou Podcast (transcribe.sh + ffmpeg) ── - _install_xiaoyuzhou_deps() - - # ── WeChat Articles (miku_ai + camoufox + wechat-article-for-ai) ── - _install_wechat_deps() + # NOTE: twitter-cli, weibo, xiaoyuzhou, wechat, xhs-cli etc. are optional. + # They are installed via --channels flag, not here. + # See CHANNEL_INSTALLERS in _cmd_install(). def _install_xiaoyuzhou_deps(): @@ -619,6 +623,98 @@ def _install_xiaoyuzhou_deps(): 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(): """Install Weibo MCP server (Panniantong fork with visitor passport auth).""" import shutil @@ -745,7 +841,6 @@ def _install_system_deps_safe(): deps = [ ("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"), - ("twitter", ["twitter"], "twitter-cli (Twitter)", "pipx install twitter-cli"), ] missing = [] @@ -765,29 +860,6 @@ def _install_system_deps_safe(): else: 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(): """Dry-run: just show what would be checked/installed.""" @@ -798,7 +870,6 @@ def _install_system_deps_dryrun(): checks = [ ("gh CLI", ["gh"], "apt install gh / brew install gh"), ("Node.js", ["node"], "curl NodeSource setup | bash + apt install nodejs"), - ("twitter-cli", ["twitter"], "pipx install twitter-cli"), ] for label, binaries, method in checks: @@ -808,30 +879,14 @@ def _install_system_deps_dryrun(): else: 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(): - """Install mcporter and configure Exa + XiaoHongShu MCP servers.""" + """Install mcporter and configure Exa search.""" import shutil import subprocess - print("Setting up mcporter (search + XiaoHongShu backend)...") + print("Setting up mcporter (search backend)...") if shutil.which("mcporter"): print(" ✅ mcporter already installed") @@ -871,24 +926,7 @@ def _install_mcporter(): except Exception: print(" [!] Could not configure Exa. Run manually: mcporter config add exa https://mcp.exa.ai/mcp") - # Check XiaoHongShu CLI - 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") + # NOTE: xhs-cli is now optional, installed via --channels=xiaohongshu def _install_mcporter_safe(): diff --git a/agent_reach/doctor.py b/agent_reach/doctor.py index 775ea41..09db30e 100644 --- a/agent_reach/doctor.py +++ b/agent_reach/doctor.py @@ -51,37 +51,41 @@ def format_report(results: Dict[str, dict]) -> str: elif r["status"] in ("off", "error"): 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} - 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("[bold]搜索(mcporter 即可解锁):[/bold]") - for key, r in tier1.items(): + lines.append("[bold]可选渠道(已安装):[/bold]") + for key, r in tier1_active.items(): name_msg = f"[bold]{escape(r['name'])}[/bold] — {escape(r['message'])}" - if r["status"] == "ok": - lines.append(f" [green]✅[/green] {name_msg}") - else: - lines.append(f" [dim]--[/dim] {name_msg}") + lines.append(f" [green]✅[/green] {name_msg}") - # Tier 2 — optional setup + # Tier 2 — optional complex setup tier2 = {k: r for k, r in results.items() if r["tier"] == 2} - if tier2: - lines.append("") - lines.append("[bold]配置后可用:[/bold]") - for key, r in tier2.items(): + 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("[bold]可选渠道(已安装):[/bold]") + for key, r in tier2_active.items(): name_msg = f"[bold]{escape(r['name'])}[/bold] — {escape(r['message'])}" - if r["status"] == "ok": - 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(f" [green]✅[/green] {name_msg}") lines.append("") 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}] 个渠道可用") - 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) import os diff --git a/docs/install.md b/docs/install.md index 61a55e2..296c03f 100644 --- a/docs/install.md +++ b/docs/install.md @@ -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. -### Step 1: Install - -**Default (fully automatic):** +### Step 1: Install the basics ```bash # 推荐:pipx(最省心) @@ -63,40 +61,53 @@ pip install https://github.com/Panniantong/agent-reach/archive/main.zip 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`?** > 这是 PEP 668 保护,不是 Agent Reach 本身的问题。优先用 `pipx install ...`,或先创建 `venv` 再安装。 - -**Safe mode (no automatic system changes):** - -If the user asked for safe mode, or you're on a production machine: +**Safe mode / Dry run:** ```bash -pip install https://github.com/Panniantong/agent-reach/archive/main.zip -agent-reach install --env=auto --safe +agent-reach install --env=auto --safe # Check only, no auto-install +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 -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 -agent-reach doctor -``` - -### Step 2: Fix what's broken - -`agent-reach doctor` shows you which channels are active and which aren't. +Run `agent-reach doctor` and check the output. 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 | |---------|-------------| -| `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 --dry-run` | Preview what would be done | | `agent-reach doctor` | Show channel status | diff --git a/tests/test_doctor.py b/tests/test_doctor.py index ddaa22b..f3aa77d 100644 --- a/tests/test_doctor.py +++ b/tests/test_doctor.py @@ -95,7 +95,6 @@ class TestDoctor: plain = re.sub(r"\[[^\]]*\]", "", report) assert "Agent Reach" in plain assert "装好即用:" in plain - assert "搜索(mcporter 即可解锁):" in plain - assert "配置后可用:" 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