fix: restore full cli file for xhs cookie import
ci / test (3.10) (push) Has been cancelled
ci / test (3.11) (push) Has been cancelled
ci / test (3.12) (push) Has been cancelled
ci / test (3.13) (push) Has been cancelled

This commit is contained in:
Pnant
2026-05-18 20:39:22 +08:00
parent 61d6536754
commit 1ee30ecf28
+704 -3
View File
@@ -1,5 +1,3 @@
Total output lines: 1762
# -*- coding: utf-8 -*-
"""
Agent Reach CLI — installer, doctor, and configuration tool.
@@ -518,7 +516,710 @@ def _install_system_deps():
with open(list_path, "w", encoding="utf-8") as f:
f.write(repo_line)
subprocess.run(["apt-get", "update", "-qq"], capture_output=True, timeout=60)
subproc6779 tokens truncated JSON array: \'[{"name":"x","value":"y","domain":".xiaohongshu.com",...}]\'')
subprocess.run(["apt-get", "install", "-y", "-qq", "gh"], capture_output=True, timeout=60)
if shutil.which("gh"):
print(" ✅ gh CLI installed")
else:
print(" [!] gh CLI install failed. You can try: snap install gh, or download from https://github.com/cli/cli/releases")
except Exception:
print(" [!] gh CLI install failed. You can try: snap install gh, or download from https://github.com/cli/cli/releases")
elif os_type == "darwin":
if shutil.which("brew"):
try:
subprocess.run(["brew", "install", "gh"], capture_output=True, timeout=120)
if shutil.which("gh"):
print(" ✅ gh CLI installed")
else:
print(" [!] gh CLI install failed. Try: brew install gh")
except Exception:
print(" [!] gh CLI install failed. Try: brew install gh")
else:
print(" [!] gh CLI not found. Install: https://cli.github.com")
else:
print(" [!] gh CLI not found. Install: https://cli.github.com")
# ── Node.js (needed for mcporter) ──
if shutil.which("node") and shutil.which("npm"):
print(" ✅ Node.js already installed")
else:
print(" Installing Node.js...")
try:
# Use NodeSource setup script without invoking a shell pipeline.
with tempfile.NamedTemporaryFile(delete=False, suffix=".sh") as tf:
script_path = tf.name
subprocess.run(
["curl", "-fsSL", "https://deb.nodesource.com/setup_22.x", "-o", script_path],
capture_output=True, timeout=60,
)
subprocess.run(
["bash", script_path],
capture_output=True, timeout=120,
)
try:
os.unlink(script_path)
except Exception:
pass
subprocess.run(
["apt-get", "install", "-y", "-qq", "nodejs"],
capture_output=True, timeout=120,
)
if shutil.which("node"):
print(" ✅ Node.js installed")
else:
print(" [!] Node.js install failed. Try: apt install nodejs npm, or nvm install 22, or download from https://nodejs.org")
except Exception:
print(" [!] Node.js install failed. Try: apt install nodejs npm, or nvm install 22, or download from https://nodejs.org")
# ── undici (proxy support for Node.js fetch) ──
npm_cmd = shutil.which("npm")
if npm_cmd:
npm_root = subprocess.run([npm_cmd, "root", "-g"], capture_output=True, encoding="utf-8", errors="replace", timeout=5).stdout.strip()
undici_path = os.path.join(npm_root, "undici", "index.js") if npm_root else ""
if os.path.exists(undici_path):
print(" ✅ undici already installed (Node.js proxy support)")
else:
try:
subprocess.run([npm_cmd, "install", "-g", "undici"], capture_output=True, encoding="utf-8", errors="replace", timeout=60)
print(" ✅ undici installed (Node.js proxy support)")
except Exception:
print(" -- undici install failed (optional — may not work behind proxies)")
# ── yt-dlp JS runtime config (YouTube requires external JS runtime) ──
if shutil.which("node"):
ytdlp_config_dir = os.path.expanduser("~/.config/yt-dlp")
ytdlp_config = os.path.join(ytdlp_config_dir, "config")
needs_config = True
if os.path.exists(ytdlp_config):
with open(ytdlp_config, "r") as f:
if "--js-runtimes" in f.read():
needs_config = False
print(" ✅ yt-dlp JS runtime already configured")
if needs_config:
try:
os.makedirs(ytdlp_config_dir, exist_ok=True)
with open(ytdlp_config, "a") as f:
f.write("--js-runtimes node\n")
print(" ✅ yt-dlp configured to use Node.js as JS runtime (YouTube)")
except Exception:
print(" -- Could not configure yt-dlp JS runtime (YouTube may not work)")
# 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():
"""Install Xiaoyuzhou podcast transcription script."""
import shutil
from agent_reach.config import Config
config = Config()
print("Setting up Xiaoyuzhou podcast transcription...")
tools_dir = os.path.expanduser("~/.agent-reach/tools/xiaoyuzhou")
script_dst = os.path.join(tools_dir, "transcribe.sh")
if os.path.isfile(script_dst):
print(" ✅ Xiaoyuzhou transcription script already installed")
else:
# Copy script from package
script_src = os.path.join(os.path.dirname(__file__), "scripts", "transcribe_xiaoyuzhou.sh")
if os.path.isfile(script_src):
try:
os.makedirs(tools_dir, exist_ok=True)
import shutil as _shutil
_shutil.copy2(script_src, script_dst)
os.chmod(script_dst, 0o755)
print(" ✅ Xiaoyuzhou transcription script installed")
except Exception as e:
print(f" [!] Failed to install script: {e}")
else:
print(" [!] Script source not found in package")
# Check ffmpeg
if shutil.which("ffmpeg"):
print(" ✅ ffmpeg available")
else:
print(" -- ffmpeg not found. Install: apt install -y ffmpeg (or brew install ffmpeg)")
# Check GROQ_API_KEY
has_key = bool(os.environ.get("GROQ_API_KEY")) or bool(config.get("groq_api_key"))
if has_key:
print(" ✅ Groq API key configured")
else:
print(" -- Groq API key not set. Get free key at https://console.groq.com")
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
import subprocess
print("Setting up Weibo MCP server...")
# Check if already installed and working
mcporter = shutil.which("mcporter")
if mcporter:
try:
r = subprocess.run(
[mcporter, "config", "list"], capture_output=True,
encoding="utf-8", errors="replace", timeout=5
)
if "weibo" in r.stdout:
print(" ✅ Weibo MCP already configured")
return
except Exception:
pass
# Install from our fork (has visitor passport auth fix)
try:
subprocess.run(
[sys.executable, "-m", "pip", "install", "-q",
"git+https://github.com/Panniantong/mcp-server-weibo.git"],
check=True, timeout=120
)
print(" ✅ mcp-server-weibo installed (Panniantong fork)")
except Exception as e:
print(f" [!] mcp-server-weibo install failed: {e}")
return
# Register with mcporter
if mcporter:
try:
subprocess.run(
[mcporter, "config", "add", "weibo", "--command", "mcp-server-weibo"],
check=True, capture_output=True, timeout=10
)
print(" ✅ Weibo MCP registered with mcporter")
except Exception:
print(" [!] mcporter config add failed. Run manually: mcporter config add weibo --command 'mcp-server-weibo'")
else:
print(" -- mcporter not found, skipping MCP registration. Install mcporter first, then run: mcporter config add weibo --command 'mcp-server-weibo'")
def _install_wechat_deps():
"""Install WeChat article reading and search dependencies."""
import subprocess
print("Setting up WeChat article tools...")
# Check if already installed
has_camoufox = False
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
# Install Python packages
if has_camoufox and has_miku:
print(" ✅ WeChat Python packages already installed")
else:
pkgs = []
if not has_camoufox:
pkgs.extend(["camoufox[geoip]", "markdownify", "beautifulsoup4", "httpx"])
if not has_miku:
pkgs.append("miku_ai")
try:
cmd = [sys.executable, "-m", "pip", "install", "--break-system-packages", "-q"] + pkgs
subprocess.run(cmd, capture_output=True, encoding="utf-8", errors="replace", timeout=120)
# Verify
ok = True
try:
import importlib
if not has_camoufox:
importlib.import_module("camoufox")
if not has_miku:
importlib.import_module("miku_ai")
except ImportError:
ok = False
if ok:
print(f" ✅ WeChat Python packages installed ({', '.join(pkgs)})")
else:
print(f" [!] Some WeChat packages failed to install. Try: pip install {' '.join(pkgs)}")
except Exception:
print(f" [!] WeChat packages install failed. Try: pip install {' '.join(pkgs)}")
# Clone wechat-article-for-ai tool
tools_dir = os.path.expanduser("~/.agent-reach/tools")
wechat_dir = os.path.join(tools_dir, "wechat-article-for-ai")
if os.path.isfile(os.path.join(wechat_dir, "main.py")):
print(" ✅ wechat-article-for-ai tool already installed")
else:
try:
os.makedirs(tools_dir, exist_ok=True)
subprocess.run(
["git", "clone", "--depth", "1",
"https://github.com/Panniantong/wechat-article-for-ai.git", wechat_dir],
capture_output=True, encoding="utf-8", errors="replace", timeout=60,
)
if os.path.isfile(os.path.join(wechat_dir, "main.py")):
print(" ✅ wechat-article-for-ai tool installed")
else:
print(" [!] wechat-article-for-ai clone failed. Try: git clone https://github.com/Panniantong/wechat-article-for-ai.git " + wechat_dir)
except Exception:
print(" [!] wechat-article-for-ai clone failed. Try: git clone https://github.com/Panniantong/wechat-article-for-ai.git " + wechat_dir)
def _install_system_deps_safe():
"""Safe mode: check what's installed, print instructions for what's missing."""
import shutil
print("Checking system dependencies (safe mode — no auto-install)...")
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"),
]
missing = []
for name, binaries, label, install_hint in deps:
found = any(shutil.which(b) for b in binaries)
if found:
print(f"{label} already installed")
else:
print(f" -- {label} not found")
missing.append((label, install_hint))
if missing:
print()
print(" To install missing dependencies manually:")
for label, hint in missing:
print(f" {label}: {hint}")
else:
print(" All system dependencies are installed!")
def _install_system_deps_dryrun():
"""Dry-run: just show what would be checked/installed."""
import shutil
print("[dry-run] System dependency check:")
checks = [
("gh CLI", ["gh"], "apt install gh / brew install gh"),
("Node.js", ["node"], "curl NodeSource setup | bash + apt install nodejs"),
]
for label, binaries, method in checks:
found = any(shutil.which(b) for b in binaries)
if found:
print(f"{label}: already installed, skip")
else:
print(f" {label}: would install via: {method}")
def _install_mcporter():
"""Install mcporter and configure Exa search."""
import shutil
import subprocess
print("Setting up mcporter (search backend)...")
if shutil.which("mcporter"):
print(" ✅ mcporter already installed")
else:
# Check for npm/npx
if not shutil.which("npm") and not shutil.which("npx"):
print(" [!] mcporter requires Node.js. Install Node.js first:")
print(" https://nodejs.org/ or: curl -fsSL https://fnm.vercel.app/install | bash")
return
try:
subprocess.run(
["npm", "install", "-g", "mcporter"],
capture_output=True, encoding="utf-8", errors="replace", timeout=120,
)
if shutil.which("mcporter"):
print(" ✅ mcporter installed")
else:
print(" [X] mcporter install failed. Retry: npm install -g mcporter (check network/timeout), or try: npx mcporter@latest list")
return
except Exception as e:
print(f" [X] mcporter install failed: {e}")
return
# Configure Exa MCP (free, no key needed)
try:
r = subprocess.run(
["mcporter", "config", "list"], capture_output=True, encoding="utf-8", errors="replace", timeout=5
)
if "exa" not in r.stdout:
subprocess.run(
["mcporter", "config", "add", "exa", "https://mcp.exa.ai/mcp"],
capture_output=True, encoding="utf-8", errors="replace", timeout=10,
)
print(" ✅ Exa search configured (free, no API key needed)")
else:
print(" ✅ Exa search already configured")
except Exception:
print(" [!] Could not configure Exa. Run manually: mcporter config add exa https://mcp.exa.ai/mcp")
# NOTE: xhs-cli is now optional, installed via --channels=xiaohongshu
def _install_mcporter_safe():
"""Safe mode: check mcporter status, print instructions."""
import shutil
print("Checking mcporter (safe mode)...")
if shutil.which("mcporter"):
print(" ✅ mcporter already installed")
print(" To configure Exa search: mcporter config add exa https://mcp.exa.ai/mcp")
else:
print(" -- mcporter not installed")
print(" To install: npm install -g mcporter")
print(" Then configure Exa: mcporter config add exa https://mcp.exa.ai/mcp")
def _detect_environment():
"""Auto-detect if running on local computer or server."""
import os
# Check common server indicators
indicators = 0
# SSH session
if os.environ.get("SSH_CONNECTION") or os.environ.get("SSH_CLIENT"):
indicators += 2
# Docker / container
if os.path.exists("/.dockerenv") or os.path.exists("/run/.containerenv"):
indicators += 2
# No display (headless)
if not os.environ.get("DISPLAY") and not os.environ.get("WAYLAND_DISPLAY"):
indicators += 1
# Cloud VM identifiers
for cloud_file in ["/sys/hypervisor/uuid", "/sys/class/dmi/id/product_name"]:
if os.path.exists(cloud_file):
try:
with open(cloud_file) as f:
content = f.read().lower()
if any(x in content for x in ["amazon", "google", "microsoft", "digitalocean", "linode", "vultr", "hetzner"]):
indicators += 2
except Exception:
pass
# systemd-detect-virt
try:
import subprocess
result = subprocess.run(["systemd-detect-virt"], capture_output=True, encoding="utf-8", errors="replace", timeout=3)
if result.returncode == 0 and result.stdout.strip() != "none":
indicators += 1
except Exception:
pass
return "server" if indicators >= 2 else "local"
def _cmd_configure(args):
"""Set a config value and test it, or auto-extract from browser."""
import shutil
from agent_reach.config import Config
config = Config()
# ── Auto-extract from browser ──
if args.from_browser:
from agent_reach.cookie_extract import configure_from_browser
browser = args.from_browser
print(f"Extracting cookies from {browser}...")
print()
results = configure_from_browser(browser, config)
found_any = False
for platform, success, message in results:
if success:
print(f"{platform}: {message}")
found_any = True
else:
print(f" -- {platform}: {message}")
print()
if found_any:
print("✅ Cookies configured! Run `agent-reach doctor` to see updated status.")
else:
print(f"No cookies found. Make sure you're logged into the platforms in {browser}.")
return
# ── Manual configure ──
if not args.key:
print("Usage: agent-reach configure <key> <value>")
print(" or: agent-reach configure --from-browser chrome")
return
value = " ".join(args.value) if args.value else ""
if not value:
print(f"Missing value for {args.key}")
return
if args.key == "proxy":
config.set("bilibili_proxy", value)
print(f"✅ Proxy configured for Bilibili!")
print(" Note: Reddit 已改为通过 rdt-cli 访问,无需代理。")
elif args.key == "twitter-cookies":
# Accept two formats:
# 1. auth_token ct0 (two separate values)
# 2. Full cookie header string: "auth_token=xxx; ct0=yyy; ..."
auth_token, ct0 = _parse_twitter_cookie_input(value)
if auth_token and ct0:
config.set("twitter_auth_token", auth_token)
config.set("twitter_ct0", ct0)
# Sync credentials to twitter-cli env
print("✅ Twitter cookies configured!")
print("Testing Twitter access...", end=" ")
try:
import subprocess
twitter_bin = shutil.which("twitter")
if not twitter_bin:
print("[!] twitter-cli not installed. Run: pipx install twitter-cli")
else:
import os
env = os.environ.copy()
env["TWITTER_AUTH_TOKEN"] = auth_token
env["TWITTER_CT0"] = ct0
result = subprocess.run(
[twitter_bin, "status"],
capture_output=True, encoding="utf-8", errors="replace", timeout=15,
env=env,
)
output = (result.stdout or "") + (result.stderr or "")
if "ok: true" in output:
print("✅ Twitter access works!")
else:
print("[!] Auth check failed (cookies might be wrong)")
except Exception as e:
print(f"[X] Failed: {e}")
else:
print("[X] Could not find auth_token and ct0 in your input.")
print(" Accepted formats:")
print(" 1. agent-reach configure twitter-cookies AUTH_TOKEN CT0")
print(' 2. agent-reach configure twitter-cookies "auth_token=xxx; ct0=yyy; ..."')
elif args.key == "youtube-cookies":
config.set("youtube_cookies_from", value)
print(f"✅ YouTube cookie source configured: {value}")
print(" yt-dlp will use cookies from this browser for age-restricted/member videos.")
elif args.key == "xhs-cookies":
_configure_xhs_cookies(value)
elif args.key == "github-token":
config.set("github_token", value)
print(f"✅ GitHub token configured!")
elif args.key == "groq-key":
config.set("groq_api_key", value)
print(f"✅ Groq key configured!")
def _parse_twitter_cookie_input(value: str):
"""Parse Twitter cookie input from either separate values or a cookie header."""
auth_token = None
ct0 = None
if "auth_token=" in value and "ct0=" in value:
# Full cookie string — parse it.
for part in value.replace(";", " ").split():
if part.startswith("auth_token="):
auth_token = part.split("=", 1)[1]
elif part.startswith("ct0="):
ct0 = part.split("=", 1)[1]
elif len(value.split()) == 2 and "=" not in value:
# Two separate values: AUTH_TOKEN CT0.
parts = value.split()
auth_token = parts[0]
ct0 = parts[1]
return auth_token, ct0
def _configure_xhs_cookies(value):
"""Import cookies for xhs-cli, with legacy xiaohongshu-mcp support.
Accepts two formats:
1. Cookie-Editor JSON export (array of cookie objects)
2. Header String: "name1=value1; name2=value2; ..."
xhs-cli stores cookies as a name/value dict at ~/.xiaohongshu-cli/cookies.json.
The legacy Docker MCP stores Cookie-Editor style arrays at $COOKIES_PATH.
"""
import json
import shutil
import subprocess
import time
value = value.strip()
if not value:
print("[X] Missing cookie value.")
print(" Usage: agent-reach configure xhs-cookies '<cookie JSON or header string>'")
return
# Detect format and parse
cookies_json = None
cookies = []
# Try JSON format first (Cookie-Editor JSON export)
if value.startswith("["):
try:
parsed = json.loads(value)
if isinstance(parsed, list) and parsed:
# Validate it looks like cookie objects
first = parsed[0]
if isinstance(first, dict) and "name" in first and "value" in first:
cookies = parsed
cookies_json = json.dumps(parsed)
print(f" Parsed {len(parsed)} cookies from JSON format")
else:
print("[X] JSON array doesn't contain cookie objects (need name/value fields)")
return
else:
print("[X] Empty or invalid JSON array")
return
except json.JSONDecodeError as e:
print(f"[X] Invalid JSON: {e}")
return
# Header String format: "key1=val1; key2=val2; ..."
if cookies_json is None and "=" in value:
for part in value.split(";"):
part = part.strip()
if "=" not in part:
continue
name, val = part.split("=", 1)
name = name.strip()
val = val.strip()
if name:
cookies.append({
"name": name,
"value": val,
"domain": ".xiaohongshu.com",
"path": "/",
"expires": -1,
"size": len(name) + len(val),
"httpOnly": False,
"secure": False,
"session": True,
"sameSite": "Lax",
})
if cookies:
cookies_json = json.dumps(cookies)
print(f" Parsed {len(cookies)} cookies from Header String format")
else:
print("[X] Could not parse any cookies from input")
return
if not cookies_json:
print("[X] Could not parse cookies. Accepted formats:")
print(' 1. JSON array: \'[{"name":"x","value":"y","domain":".xiaohongshu.com",...}]\'')
print(' 2. Header String: "key1=val1; key2=val2; ..."')
return