Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,28 @@ jobs:
run: |
pytest -q

windows-test:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install package and test deps
run: |
python -m pip install --upgrade pip
python -m pip install -c constraints.txt -e ".[dev]"

- name: Run tests
shell: bash
run: |
export PATH="/c/Program Files/Git/bin:/c/Program Files/Git/usr/bin:$PATH"
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.
Expand Down
39 changes: 28 additions & 11 deletions agent_reach/channels/v2ex.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import subprocess
import urllib.request
from typing import Any
from urllib.parse import urlsplit
from urllib.parse import quote, urlencode, urlsplit

from agent_reach.utils.process import utf8_subprocess_env
from agent_reach.utils.text import scrub_url_credentials
Expand All @@ -17,6 +17,12 @@
_UA = "agent-reach/1.0"
_TIMEOUT = 10
_MAX_RESPONSE_BYTES = 1024 * 1024
_API_BASE = "https://www.v2ex.com"


def _v2ex_url(path: str, **params: Any) -> str:
"""Build a V2EX URL without letting caller values alter its query."""
return f"{_API_BASE}{path}?{urlencode(params)}"


def _validate_api_url(url: str) -> None:
Expand Down Expand Up @@ -203,9 +209,10 @@ def get_node_topics(self, node_name: str, limit: int = 20) -> list:
Returns a list of dicts with keys:
title, url, replies, node_name, node_title, content
"""
url = (
f"https://www.v2ex.com/api/topics/show.json"
f"?node_name={node_name}&page=1"
url = _v2ex_url(
"/api/topics/show.json",
node_name=node_name,
page=1,
)
data = _get_json(url)
results = []
Expand Down Expand Up @@ -237,7 +244,7 @@ def get_topic(self, topic_id: int) -> dict:
author, created, replies (list of dicts with: author, content, created)
"""
topic_data = _get_json(
f"https://www.v2ex.com/api/topics/show.json?id={topic_id}"
_v2ex_url("/api/topics/show.json", id=topic_id)
)
# API returns a list even for single-ID queries
if isinstance(topic_data, list):
Expand All @@ -251,8 +258,11 @@ def get_topic(self, topic_id: int) -> dict:
# Fetch replies (first page)
try:
replies_raw = _get_json(
f"https://www.v2ex.com/api/replies/show.json"
f"?topic_id={topic_id}&page=1"
_v2ex_url(
"/api/replies/show.json",
topic_id=topic_id,
page=1,
)
)
except Exception:
replies_raw = []
Expand All @@ -269,7 +279,10 @@ def get_topic(self, topic_id: int) -> dict:
return {
"id": topic.get("id", topic_id),
"title": topic.get("title", ""),
"url": topic.get("url", f"https://www.v2ex.com/t/{topic_id}"),
"url": topic.get(
"url",
f"{_API_BASE}/t/{quote(str(topic_id), safe='')}",
),
"content": topic.get("content", ""),
"replies_count": topic.get("replies", 0),
"node_name": node.get("name", ""),
Expand All @@ -290,12 +303,15 @@ def get_user(self, username: str) -> dict:
location, bio, avatar, created
"""
data = _get_json(
f"https://www.v2ex.com/api/members/show.json?username={username}"
_v2ex_url("/api/members/show.json", username=username)
)
return {
"id": data.get("id", 0),
"username": data.get("username", username),
"url": data.get("url", f"https://www.v2ex.com/member/{username}"),
"url": data.get(
"url",
f"{_API_BASE}/member/{quote(str(username), safe='')}",
),
"website": data.get("website", ""),
"twitter": data.get("twitter", ""),
"psn": data.get("psn", ""),
Expand All @@ -320,11 +336,12 @@ def search(self, query: str, limit: int = 10) -> list:
list of dicts with keys: title, url, snippet
如果搜索不可用,返回包含单条 {"error": str} 的列表。
"""
search_url = _v2ex_url("/", q=query)
return [
{
"error": (
"V2EX 公开 API 不提供搜索端点。"
f"建议改用:https://www.v2ex.com/?q={query} "
f"建议改用:{search_url} "
"或通过 Exa channel 使用 site:v2ex.com 搜索。"
)
}
Expand Down
7 changes: 6 additions & 1 deletion agent_reach/channels/xueqiu.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,14 @@ def get_hot_posts(self, limit: int = 20) -> list:
Returns a list of dicts with keys:
id, title, text, author, likes, url
"""
if limit < 0:
raise ValueError("limit must be non-negative")
limit = min(limit, 50)
if limit == 0:
return []
data = _get_json(
"https://xueqiu.com/v4/statuses/public_timeline_by_category.json"
"?since_id=-1&max_id=-1&count=20&category=-1"
f"?since_id=-1&max_id=-1&count={limit}&category=-1"
)
items = data.get("list") or []
results = []
Expand Down
11 changes: 10 additions & 1 deletion agent_reach/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1772,14 +1772,23 @@ def _configure_xhs_cookies(value) -> bool:
# Restart container so it reloads cookies from disk
print(" Restarting container to reload cookies...", end=" ", flush=True)
try:
subprocess.run(
restart = subprocess.run(
[docker, "restart", container_name],
capture_output=True, encoding="utf-8", timeout=30,
)
if restart.returncode != 0:
detail = (
(restart.stderr or "").strip()[:200]
or f"exit {restart.returncode}"
)
print(f"\n [!] Could not restart container: {detail}")
print(f" Restart manually: docker restart {container_name}")
return False
print("done")
except Exception as e:
print(f"\n [!] Could not restart container: {e}")
print(f" Restart manually: docker restart {container_name}")
return False
except Exception as e:
print(f"[X] Failed to write cookies: {e}")
return False
Expand Down
2 changes: 1 addition & 1 deletion agent_reach/guides/setup-exa.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ mcporter config add exa https://mcp.exa.ai/mcp --scope home
### 3. 验证
```bash
agent-reach doctor | grep "Search"
mcporter call 'exa.web_search_exa(query: "test", numResults: 1)'
mcporter call exa.web_search_exa query="test" numResults=1
```

## 需要用户手动做的步骤
Expand Down
2 changes: 1 addition & 1 deletion agent_reach/guides/setup-reddit.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ rdt read POST_ID
如果你已经配置了 Exa(通过 mcporter),也可以通过 Exa 搜索 Reddit 内容:

```bash
mcporter call 'exa.web_search_exa(query: "python best practices", numResults: 5, includeDomains: ["reddit.com"])'
mcporter call exa.web_search_exa query="site:reddit.com python best practices" numResults=5
```

rdt-cli 是当前推荐方案,无需额外配置即可使用。
2 changes: 1 addition & 1 deletion agent_reach/skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ metadata:

```bash
# Exa 网页搜索
mcporter call 'exa.web_search_exa(query: "query", numResults: 5)'
mcporter call exa.web_search_exa query="query" numResults=5

# 通用网页阅读
curl -s "https://r.jina.ai/URL"
Expand Down
2 changes: 1 addition & 1 deletion agent_reach/skill/SKILL_en.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ these platforms — do not invent your own approach.**

```bash
# Exa web search
mcporter call 'exa.web_search_exa(query: "query", numResults: 5)'
mcporter call exa.web_search_exa query="query" numResults=5

# Read any web page
curl -s "https://r.jina.ai/URL"
Expand Down
8 changes: 4 additions & 4 deletions agent_reach/skill/references/career.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@ LinkedIn。

```bash
# 获取个人资料
mcporter call 'linkedin.get_person_profile(linkedin_username: "username", sections: "experience,education")'
mcporter call linkedin.get_person_profile linkedin_username="username" sections="experience,education"

# 搜索人才
mcporter call 'linkedin.search_people(keywords: "AI engineer", location: "Shanghai")'
mcporter call linkedin.search_people keywords="AI engineer" location="Shanghai"

# 获取公司资料
mcporter call 'linkedin.get_company_profile(company_name: "openai", sections: "posts,jobs")'
mcporter call linkedin.get_company_profile company_name="openai" sections="posts,jobs"

# 搜索职位
mcporter call 'linkedin.search_jobs(keywords: "software engineer", location: "Remote", max_pages: 2)'
mcporter call linkedin.search_jobs keywords="software engineer" location="Remote" max_pages=2
```

> **需要登录**: 首次使用前运行 `uvx mcp-server-linkedin@latest --login`,保存有效登录态。
Expand Down
4 changes: 2 additions & 2 deletions agent_reach/skill/references/search.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ Exa AI 搜索引擎。
高质量 AI 搜索引擎,适合查找技术文档、官方示例和相关网页。

```bash
mcporter call 'exa.web_search_exa(query: "query", numResults: 5)'
mcporter call 'exa.web_search_exa(query: "library API code example", numResults: 5)'
mcporter call exa.web_search_exa query="query" numResults=5
mcporter call exa.web_search_exa query="library API code example" numResults=5
```

### 使用场景
Expand Down
6 changes: 3 additions & 3 deletions agent_reach/skill/references/social.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@ opencli xiaohongshu user USER_ID -f yaml
agent-reach configure xhs-cookies

# 只读检查当前状态
mcporter call 'xiaohongshu.check_login_status()' --timeout 120000
mcporter call xiaohongshu.check_login_status --timeout 120000

# 搜索
mcporter call 'xiaohongshu.search_feeds(keyword: "query")' --timeout 120000
mcporter call xiaohongshu.search_feeds keyword="query" --timeout 120000

# 笔记详情+评论(feed_id 和 xsec_token 从搜索结果取)
mcporter call 'xiaohongshu.get_feed_detail(feed_id: "...", xsec_token: "...")' --timeout 120000
mcporter call xiaohongshu.get_feed_detail feed_id="..." xsec_token="..." --timeout 120000
```

> 首次调用会自动下载约 150MB 无头浏览器,务必带 `--timeout 120000`。
Expand Down
6 changes: 3 additions & 3 deletions agent_reach/skill/references/web.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ curl -s "https://r.jina.ai/https://example.com/article"

```bash
# 读取网页内容 (Markdown 格式)
mcporter call 'web-reader.webReader(url: "https://example.com")'
mcporter call web-reader.webReader url="https://example.com"

# 保留图片
mcporter call 'web-reader.webReader(url: "https://example.com", retain_images: true)'
mcporter call web-reader.webReader url="https://example.com" retain_images=true

# 纯文本格式
mcporter call 'web-reader.webReader(url: "https://example.com", return_format: "text")'
mcporter call web-reader.webReader url="https://example.com" return_format="text"
```

**适用场景**: 需要更精确控制输出格式时使用。
Expand Down
11 changes: 9 additions & 2 deletions agent_reach/transcribe.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ def _probe_audio_duration(path: Path) -> float:
proc = subprocess.run(
cmd,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=FFPROBE_TIMEOUT_SECONDS,
)
except subprocess.TimeoutExpired:
Expand Down Expand Up @@ -158,7 +159,13 @@ def _run(cmd: List[str], timeout: int = 600) -> None:
network read or a hung probe must not block the CLI forever.
"""
try:
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
proc = subprocess.run(
cmd,
capture_output=True,
encoding="utf-8",
errors="replace",
timeout=timeout,
)
except subprocess.TimeoutExpired:
raise TranscribeError(f"{cmd[0]} timed out after {timeout}s")
if proc.returncode != 0:
Expand Down
2 changes: 1 addition & 1 deletion docs/dependency-locking.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Agent Reach uses `constraints.txt` as a reproducible dependency baseline.
## Install with constraints

```bash
pip install -c constraints.txt -e .[dev]
pip install -c constraints.txt -e '.[dev]'
```

## Update workflow
Expand Down
4 changes: 2 additions & 2 deletions docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -366,10 +366,10 @@ After installation, use upstream tools directly. See SKILL.md for the full comma
| Instagram | `opencli` | `opencli instagram user nasa -f yaml` |
| GitHub | `gh` | `gh search repos "query"` |
| Web | `curl` + Jina | `curl -s "https://r.jina.ai/URL"` |
| Exa Search | `mcporter` | `mcporter call 'exa.web_search_exa(...)'` |
| Exa Search | `mcporter` | `mcporter call exa.web_search_exa query="..." numResults=5` |
| 小红书 | `opencli`(服务器 `mcporter`) | `opencli xiaohongshu search "query" -f yaml` |
| 小宇宙播客 | `transcribe.sh` | `bash ~/.agent-reach/tools/xiaoyuzhou/transcribe.sh <URL>` |
| LinkedIn | `mcporter` | `mcporter call 'linkedin.get_person_profile(...)'` |
| LinkedIn | `mcporter` | `mcporter call linkedin.get_person_profile linkedin_username="..."` |
| RSS | `feedparser` | `python3 -c "import feedparser; ..."` |

> 多后端平台以 `agent-reach doctor --json` 的 `active_backend` 为准。
2 changes: 1 addition & 1 deletion docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ proxychains twitter search "test" -n 1
twitter-cli 不可用时,可以直接用 Exa 搜索 Twitter 内容:

```bash
mcporter call 'exa.web_search_exa(query: "site:x.com 搜索词", numResults: 5)'
mcporter call exa.web_search_exa query="site:x.com 搜索词" numResults=5
```

### 方案 4:检查认证
Expand Down
Loading
Loading