Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ee30ecf28 | |||
| 61d6536754 | |||
| 27b2cd2d02 | |||
| 7eae32d0b7 |
@@ -28,43 +28,3 @@ jobs:
|
||||
- name: Run tests
|
||||
run: |
|
||||
pytest -q
|
||||
|
||||
# Editable installs (-e) never exercise wheel packaging, so a broken wheel
|
||||
# can pass tests and still fail every real `pip install` from source.
|
||||
# This job builds the actual wheel and installs it into a clean venv.
|
||||
wheel-gate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Build wheel
|
||||
run: |
|
||||
python -m pip install --upgrade pip build
|
||||
python -m build
|
||||
|
||||
- name: Verify wheel has no duplicate entries and ships data files
|
||||
run: |
|
||||
python - <<'PY'
|
||||
import glob, zipfile, collections
|
||||
whl = glob.glob("dist/*.whl")[0]
|
||||
names = zipfile.ZipFile(whl).namelist()
|
||||
dupes = [n for n, c in collections.Counter(names).items() if c > 1]
|
||||
assert not dupes, f"duplicate entries in wheel: {dupes}"
|
||||
assert "agent_reach/skill/SKILL.md" in names, "SKILL.md missing from wheel"
|
||||
for prefix in ("agent_reach/guides/", "agent_reach/scripts/", "agent_reach/skill/references/"):
|
||||
assert any(n.startswith(prefix) for n in names), f"{prefix} missing from wheel"
|
||||
print(f"wheel OK: {len(names)} entries, no duplicates, data files present")
|
||||
PY
|
||||
|
||||
- name: Smoke-install wheel into clean venv
|
||||
run: |
|
||||
python -m venv /tmp/smoke
|
||||
/tmp/smoke/bin/pip install --quiet dist/*.whl
|
||||
/tmp/smoke/bin/agent-reach version
|
||||
cd /tmp && /tmp/smoke/bin/python -c "import agent_reach; from importlib.resources import files; assert (files('agent_reach')/'skill'/'SKILL.md').is_file(); print('SKILL.md ships in site-packages OK')"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## Project
|
||||
Agent Reach — Python CLI + library that gives AI agents read/search access to 14+ internet platforms.
|
||||
Positioning: installer + doctor + config tool. NOT a wrapper — after install, agents call upstream tools directly.
|
||||
Repo: github.com/Panniantong/Agent-Reach | License: MIT | Version: 1.4.1
|
||||
Repo: github.com/Panniantong/Agent-Reach | License: MIT | Version: 1.3.0
|
||||
|
||||
## Commands
|
||||
- `pip install -e .` — Dev install
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Agent Reach — Give your AI Agent eyes to see the entire internet."""
|
||||
|
||||
__version__ = "1.4.1"
|
||||
__version__ = "1.4.0"
|
||||
__author__ = "Neo Reid"
|
||||
|
||||
from agent_reach.core import AgentReach
|
||||
|
||||
+35
-16
@@ -1143,19 +1143,19 @@ def _parse_twitter_cookie_input(value: str):
|
||||
|
||||
|
||||
def _configure_xhs_cookies(value):
|
||||
"""Import cookies into xiaohongshu-mcp Docker container.
|
||||
"""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; ..."
|
||||
|
||||
The xiaohongshu-mcp container stores cookies at $COOKIES_PATH
|
||||
(default: /app/data/cookies.json or cookies.json in workdir).
|
||||
Format: JSON array of {name, value, domain, path, expires, httpOnly, secure, sameSite}.
|
||||
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:
|
||||
@@ -1165,6 +1165,7 @@ def _configure_xhs_cookies(value):
|
||||
|
||||
# Detect format and parse
|
||||
cookies_json = None
|
||||
cookies = []
|
||||
|
||||
# Try JSON format first (Cookie-Editor JSON export)
|
||||
if value.startswith("["):
|
||||
@@ -1174,6 +1175,7 @@ def _configure_xhs_cookies(value):
|
||||
# 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:
|
||||
@@ -1188,7 +1190,6 @@ def _configure_xhs_cookies(value):
|
||||
|
||||
# Header String format: "key1=val1; key2=val2; ..."
|
||||
if cookies_json is None and "=" in value:
|
||||
cookies = []
|
||||
for part in value.split(";"):
|
||||
part = part.strip()
|
||||
if "=" not in part:
|
||||
@@ -1222,17 +1223,37 @@ def _configure_xhs_cookies(value):
|
||||
print(' 2. Header String: "key1=val1; key2=val2; ..."')
|
||||
return
|
||||
|
||||
# Primary path: configure xhs-cli directly.
|
||||
xhs_cookie_map = {
|
||||
str(c.get("name", "")).strip(): str(c.get("value", ""))
|
||||
for c in cookies
|
||||
if isinstance(c, dict) and str(c.get("name", "")).strip()
|
||||
}
|
||||
if xhs_cookie_map.get("a1"):
|
||||
xhs_config_dir = os.path.expanduser("~/.xiaohongshu-cli")
|
||||
os.makedirs(xhs_config_dir, exist_ok=True)
|
||||
xhs_cookie_path = os.path.join(xhs_config_dir, "cookies.json")
|
||||
with open(xhs_cookie_path, "w", encoding="utf-8") as f:
|
||||
json.dump({**xhs_cookie_map, "saved_at": time.time()}, f, indent=2)
|
||||
os.chmod(xhs_cookie_path, 0o600)
|
||||
print(f"✅ xhs-cli cookies saved to {xhs_cookie_path}")
|
||||
print(" Run `xhs status` or `agent-reach doctor` to verify.")
|
||||
else:
|
||||
print("[!] Cookie input does not include the required `a1` cookie.")
|
||||
print(" xhs-cli will not treat this as a logged-in session. Export all xiaohongshu.com cookies from Cookie-Editor.")
|
||||
|
||||
# Keep a legacy Cookie-Editor array for users still running xiaohongshu-mcp.
|
||||
legacy_cookie_path = os.path.expanduser("~/.agent-reach/xhs-cookies.json")
|
||||
os.makedirs(os.path.dirname(legacy_cookie_path), exist_ok=True)
|
||||
with open(legacy_cookie_path, "w") as f:
|
||||
f.write(cookies_json)
|
||||
os.chmod(legacy_cookie_path, 0o600)
|
||||
print(f" Legacy MCP cookie array saved to {legacy_cookie_path}")
|
||||
|
||||
# Find the container
|
||||
docker = shutil.which("docker")
|
||||
if not docker:
|
||||
# No Docker - write to a local file for manual import
|
||||
cookie_path = os.path.expanduser("~/.agent-reach/xhs-cookies.json")
|
||||
with open(cookie_path, "w") as f:
|
||||
f.write(cookies_json)
|
||||
os.chmod(cookie_path, 0o600)
|
||||
print(f" Cookies saved to {cookie_path}")
|
||||
print(" Docker not found. Copy manually:")
|
||||
print(f" docker cp {cookie_path} xiaohongshu-mcp:/app/data/cookies.json")
|
||||
print(" Docker not found; skipping xiaohongshu-mcp import.")
|
||||
return
|
||||
|
||||
# Check if xiaohongshu-mcp container is running
|
||||
@@ -1243,9 +1264,7 @@ def _configure_xhs_cookies(value):
|
||||
)
|
||||
container_name = result.stdout.strip()
|
||||
if not container_name:
|
||||
print("[X] xiaohongshu-mcp container is not running.")
|
||||
print(" Start it first:")
|
||||
print(" docker run -d --name xiaohongshu-mcp -p 18060:18060 xpzouying/xiaohongshu-mcp")
|
||||
print(" xiaohongshu-mcp container is not running; skipping legacy Docker import.")
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"[X] Could not check Docker: {e}")
|
||||
|
||||
@@ -39,7 +39,9 @@ agent-reach doctor
|
||||
> 3. 点击 Cookie-Editor 图标 → Export → Header String
|
||||
> 4. 把导出的字符串发给 Agent,运行:`agent-reach configure xhs-cookies "导出的cookie字符串"`
|
||||
>
|
||||
> **注意**:不要依赖 QR 扫码登录,Cookie-Editor 导出方式最简单可靠。
|
||||
> **注意**:不要依赖 QR 扫码登录,Cookie-Editor 导出方式最简单可靠。这个方式也适合 WSL、SSH、容器等无法直接读取桌面浏览器 Cookie 的环境。
|
||||
|
||||
`agent-reach configure xhs-cookies` 会把 Cookie 同步到 `~/.xiaohongshu-cli/cookies.json`,供 `xhs status/search/read` 直接使用;如果你还在用旧的 `xiaohongshu-mcp` Docker 方案,也会保留兼容导入文件。
|
||||
|
||||
## 使用示例
|
||||
|
||||
@@ -69,6 +71,9 @@ A: 推荐使用住宅代理:`export HTTP_PROXY="http://user:pass@ip:port"`。
|
||||
**Q: xhs-cli 不支持我的系统?**
|
||||
A: 确保 Python 3.10+ 和 pipx 已安装。运行 `pipx install xiaohongshu-cli` 即可。
|
||||
|
||||
**Q: WSL 里 `xhs login` 读不到 Windows 浏览器怎么办?**
|
||||
A: 这是常见限制。WSL 里没有直接可读的 Linux 浏览器 Cookie 时,`xhs login` 自动提取会失败。推荐在 Windows 浏览器用 Cookie-Editor 导出 `xiaohongshu.com` 的 Header String,然后在 WSL 里运行 `agent-reach configure xhs-cookies "..."`,再用 `xhs status` 验证。
|
||||
|
||||
## 备选方案:Docker MCP
|
||||
|
||||
如果你已经在使用 [xiaohongshu-mcp](https://github.com/xpzouying/xiaohongshu-mcp) Docker 方案,它也能正常工作:
|
||||
|
||||
+7
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "agent-reach"
|
||||
version = "1.4.1"
|
||||
version = "1.4.0"
|
||||
description = "Give your AI Agent eyes to see the entire internet. Search + Read 10+ platforms."
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
@@ -64,6 +64,12 @@ build-backend = "hatchling.build"
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["agent_reach"]
|
||||
|
||||
[tool.hatch.build.targets.wheel.force-include]
|
||||
"agent_reach/guides" = "agent_reach/guides"
|
||||
# Keep the whole skill directory so SKILL.md, SKILL_en.md, and references/ ship together.
|
||||
"agent_reach/skill" = "agent_reach/skill"
|
||||
"agent_reach/scripts" = "agent_reach/scripts"
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py310"
|
||||
line-length = 100
|
||||
|
||||
+22
-1
@@ -1,6 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests for Agent Reach CLI."""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
from unittest.mock import patch
|
||||
@@ -42,6 +45,24 @@ class TestCLI:
|
||||
assert auth_token == "token123"
|
||||
assert ct0 == "ct0abc"
|
||||
|
||||
def test_configure_xhs_cookies_writes_xhs_cli_cookie_file(self, tmp_path, monkeypatch, capsys):
|
||||
monkeypatch.setenv("HOME", str(tmp_path))
|
||||
monkeypatch.setattr("shutil.which", lambda _name: None)
|
||||
|
||||
cli._configure_xhs_cookies("a1=token123; web_session=session456; other=value")
|
||||
|
||||
cookie_path = tmp_path / ".xiaohongshu-cli" / "cookies.json"
|
||||
data = json.loads(cookie_path.read_text())
|
||||
assert data["a1"] == "token123"
|
||||
assert data["web_session"] == "session456"
|
||||
assert "saved_at" in data
|
||||
assert oct(os.stat(cookie_path).st_mode & 0o777) == "0o600"
|
||||
|
||||
legacy_path = tmp_path / ".agent-reach" / "xhs-cookies.json"
|
||||
assert legacy_path.exists()
|
||||
captured = capsys.readouterr()
|
||||
assert "xhs-cli cookies saved" in captured.out
|
||||
|
||||
|
||||
class TestCheckUpdateRetry:
|
||||
def test_retry_timeout_classification(self):
|
||||
@@ -89,7 +110,7 @@ class TestCheckUpdateRetry:
|
||||
|
||||
sequence = [
|
||||
R(429, headers={"Retry-After": "3"}),
|
||||
R(200, payload={"tag_name": "v1.4.1"}),
|
||||
R(200, payload={"tag_name": "v1.4.0"}),
|
||||
]
|
||||
|
||||
with patch("requests.get", side_effect=sequence):
|
||||
|
||||
Reference in New Issue
Block a user