Add English skill locale support (#223)
feat: add English skill locale support with SKILL_en.md
This commit is contained in:
+26
-4
@@ -326,8 +326,30 @@ def _install_skill():
|
|||||||
import shutil
|
import shutil
|
||||||
import importlib.resources
|
import importlib.resources
|
||||||
|
|
||||||
|
def _is_english_locale(value: str) -> bool:
|
||||||
|
normalized = value.strip().lower()
|
||||||
|
return normalized.startswith("en") or normalized.startswith("english")
|
||||||
|
|
||||||
|
def _skill_resource_name() -> str:
|
||||||
|
locale_candidates = (
|
||||||
|
os.environ.get("AGENT_REACH_LANG", ""),
|
||||||
|
os.environ.get("LC_ALL", ""),
|
||||||
|
os.environ.get("LC_MESSAGES", ""),
|
||||||
|
os.environ.get("LANG", ""),
|
||||||
|
)
|
||||||
|
if any(_is_english_locale(candidate) for candidate in locale_candidates):
|
||||||
|
return "SKILL_en.md"
|
||||||
|
return "SKILL.md"
|
||||||
|
|
||||||
|
def _read_skill_markdown(skill_pkg):
|
||||||
|
resource_name = _skill_resource_name()
|
||||||
|
try:
|
||||||
|
return skill_pkg.joinpath(resource_name).read_text(encoding="utf-8")
|
||||||
|
except FileNotFoundError:
|
||||||
|
return skill_pkg.joinpath("SKILL.md").read_text(encoding="utf-8")
|
||||||
|
|
||||||
def _copy_skill_dir(target: str) -> bool:
|
def _copy_skill_dir(target: str) -> bool:
|
||||||
"""Copy entire skill directory (SKILL.md + references/)."""
|
"""Copy entire skill directory (locale-specific SKILL.md + references/)."""
|
||||||
try:
|
try:
|
||||||
# Clear existing installation
|
# Clear existing installation
|
||||||
if os.path.exists(target):
|
if os.path.exists(target):
|
||||||
@@ -337,13 +359,13 @@ def _install_skill():
|
|||||||
# Get skill directory from package (with fallback for editable installs)
|
# Get skill directory from package (with fallback for editable installs)
|
||||||
try:
|
try:
|
||||||
skill_pkg = importlib.resources.files("agent_reach").joinpath("skill")
|
skill_pkg = importlib.resources.files("agent_reach").joinpath("skill")
|
||||||
skill_md = skill_pkg.joinpath("SKILL.md").read_text(encoding="utf-8")
|
skill_md = _read_skill_markdown(skill_pkg)
|
||||||
except Exception:
|
except Exception:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
skill_pkg = Path(__file__).resolve().parent / "skill"
|
skill_pkg = Path(__file__).resolve().parent / "skill"
|
||||||
skill_md = (skill_pkg / "SKILL.md").read_text(encoding="utf-8")
|
skill_md = _read_skill_markdown(skill_pkg)
|
||||||
|
|
||||||
# Copy SKILL.md
|
# Copy SKILL.md using the selected locale file
|
||||||
with open(os.path.join(target, "SKILL.md"), "w", encoding="utf-8") as f:
|
with open(os.path.join(target, "SKILL.md"), "w", encoding="utf-8") as f:
|
||||||
f.write(skill_md)
|
f.write(skill_md)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,312 @@
|
|||||||
|
---
|
||||||
|
name: agent-reach
|
||||||
|
description: >
|
||||||
|
Give your AI agent eyes to see the entire internet.
|
||||||
|
Search and read 17 platforms: Twitter/X, Reddit, YouTube, GitHub, Bilibili,
|
||||||
|
XiaoHongShu, Douyin, Weibo, WeChat Articles, Xiaoyuzhou Podcast, LinkedIn,
|
||||||
|
V2EX, Xueqiu, RSS, Exa web search, and any web page.
|
||||||
|
Zero config for 8 channels. Use when the user asks to search, read, or interact
|
||||||
|
on any supported platform, shares a URL, or asks to search the web.
|
||||||
|
Triggers: "search twitter", "search xiaohongshu", "watch this video",
|
||||||
|
"search the web", "look this up", "research", "youtube transcript",
|
||||||
|
"search reddit", "read this link", "bilibili", "douyin video",
|
||||||
|
"wechat article", "wechat official account", "weibo", "V2EX",
|
||||||
|
"xiaoyuzhou", "podcast", "xueqiu", "stock quote",
|
||||||
|
"install agent reach".
|
||||||
|
metadata:
|
||||||
|
openclaw:
|
||||||
|
homepage: https://github.com/Panniantong/Agent-Reach
|
||||||
|
---
|
||||||
|
|
||||||
|
# Agent Reach — Usage Guide
|
||||||
|
|
||||||
|
Upstream tools for 17 platforms. Call them directly.
|
||||||
|
|
||||||
|
Run `agent-reach doctor` to check which channels are available.
|
||||||
|
|
||||||
|
## ⚠️ Workspace Rules
|
||||||
|
|
||||||
|
**Never create files in the agent workspace.** Use `/tmp/` for temporary output and `~/.agent-reach/` for persistent data.
|
||||||
|
|
||||||
|
## Web — Any URL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s "https://r.jina.ai/URL"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Web Search (Exa)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mcporter call 'exa.web_search_exa(query: "query", numResults: 5)'
|
||||||
|
mcporter call 'exa.get_code_context_exa(query: "code question", tokensNum: 3000)'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Twitter/X (bird)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bird search "query" -n 10 # search
|
||||||
|
bird read URL_OR_ID # read tweet (supports /status/ and /article/ URLs)
|
||||||
|
bird user-tweets @username -n 20 # user timeline
|
||||||
|
bird thread URL_OR_ID # full thread
|
||||||
|
```
|
||||||
|
|
||||||
|
## YouTube (yt-dlp)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yt-dlp --dump-json "URL" # video metadata
|
||||||
|
yt-dlp --write-sub --write-auto-sub --sub-lang "zh-Hans,zh,en" --skip-download -o "/tmp/%(id)s" "URL"
|
||||||
|
# download subtitles, then read the .vtt file
|
||||||
|
yt-dlp --dump-json "ytsearch5:query" # search
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bilibili (yt-dlp)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yt-dlp --dump-json "https://www.bilibili.com/video/BVxxx"
|
||||||
|
yt-dlp --write-sub --write-auto-sub --sub-lang "zh-Hans,zh,en" --convert-subs vtt --skip-download -o "/tmp/%(id)s" "URL"
|
||||||
|
```
|
||||||
|
|
||||||
|
> Server IPs may get 412. Use `--cookies-from-browser chrome` or configure a proxy.
|
||||||
|
|
||||||
|
## Reddit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s "https://www.reddit.com/r/SUBREDDIT/hot.json?limit=10" -H "User-Agent: agent-reach/1.0"
|
||||||
|
curl -s "https://www.reddit.com/search.json?q=QUERY&limit=10" -H "User-Agent: agent-reach/1.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
> Server IPs may get 403. Search via Exa instead, or configure a proxy.
|
||||||
|
|
||||||
|
## GitHub (gh CLI)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh search repos "query" --sort stars --limit 10
|
||||||
|
gh repo view owner/repo
|
||||||
|
gh search code "query" --language python
|
||||||
|
gh issue list -R owner/repo --state open
|
||||||
|
gh issue view 123 -R owner/repo
|
||||||
|
```
|
||||||
|
|
||||||
|
## XiaoHongShu (mcporter)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mcporter call 'xiaohongshu.search_feeds(keyword: "query")'
|
||||||
|
mcporter call 'xiaohongshu.get_feed_detail(feed_id: "xxx", xsec_token: "yyy")'
|
||||||
|
mcporter call 'xiaohongshu.get_feed_detail(feed_id: "xxx", xsec_token: "yyy", load_all_comments: true)'
|
||||||
|
mcporter call 'xiaohongshu.publish_content(title: "Title", content: "Body text", images: ["/path/img.jpg"], tags: ["tag"])'
|
||||||
|
```
|
||||||
|
|
||||||
|
> Requires login. Use Cookie-Editor to import cookies.
|
||||||
|
|
||||||
|
> **Tip: Clean bloated output.** The XHS API returns large JSON with many unused fields.
|
||||||
|
> Pipe through the formatter to save context:
|
||||||
|
> ```bash
|
||||||
|
> mcporter call 'xiaohongshu.search_feeds(keyword: "query")' | agent-reach format xhs
|
||||||
|
> ```
|
||||||
|
> This keeps only: title, content, author, engagement counts, image URLs, and tags.
|
||||||
|
|
||||||
|
## Douyin (mcporter)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mcporter call 'douyin.parse_douyin_video_info(share_link: "https://v.douyin.com/xxx/")'
|
||||||
|
mcporter call 'douyin.get_douyin_download_link(share_link: "https://v.douyin.com/xxx/")'
|
||||||
|
```
|
||||||
|
|
||||||
|
> No login needed.
|
||||||
|
|
||||||
|
## WeChat Articles
|
||||||
|
|
||||||
|
**Search** (`miku_ai`):
|
||||||
|
```bash
|
||||||
|
# miku_ai is installed inside the agent-reach Python environment.
|
||||||
|
# Use the same interpreter that runs agent-reach (handles pipx / venv installs):
|
||||||
|
AGENT_REACH_PYTHON=$(python3 -c "import agent_reach, sys; print(sys.executable)" 2>/dev/null || echo python3)
|
||||||
|
$AGENT_REACH_PYTHON -c "
|
||||||
|
import asyncio
|
||||||
|
from miku_ai import get_wexin_article
|
||||||
|
async def s():
|
||||||
|
for a in await get_wexin_article('query', 5):
|
||||||
|
print(f'{a[\"title\"]} | {a[\"url\"]}')
|
||||||
|
asyncio.run(s())
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Read** (Camoufox — bypasses WeChat anti-bot):
|
||||||
|
```bash
|
||||||
|
cd ~/.agent-reach/tools/wechat-article-for-ai && python3 main.py "https://mp.weixin.qq.com/s/ARTICLE_ID"
|
||||||
|
```
|
||||||
|
|
||||||
|
> WeChat articles cannot be read with Jina Reader or curl. Use Camoufox.
|
||||||
|
|
||||||
|
## Weibo (mcporter)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Trending topics
|
||||||
|
mcporter call 'weibo.get_trendings(limit: 20)'
|
||||||
|
|
||||||
|
# Search users
|
||||||
|
mcporter call 'weibo.search_users(keyword: "Lei Jun", limit: 10)'
|
||||||
|
|
||||||
|
# Get a user profile
|
||||||
|
mcporter call 'weibo.get_profile(uid: "1195230310")'
|
||||||
|
|
||||||
|
# Get a user's feed
|
||||||
|
mcporter call 'weibo.get_feeds(uid: "1195230310", limit: 20)'
|
||||||
|
|
||||||
|
# Get a user's hot posts
|
||||||
|
mcporter call 'weibo.get_hot_feeds(uid: "1195230310", limit: 10)'
|
||||||
|
|
||||||
|
# Search post content
|
||||||
|
mcporter call 'weibo.search_content(keyword: "artificial intelligence", limit: 20)'
|
||||||
|
|
||||||
|
# Search topics
|
||||||
|
mcporter call 'weibo.search_topics(keyword: "AI", limit: 10)'
|
||||||
|
|
||||||
|
# Get post comments
|
||||||
|
mcporter call 'weibo.get_comments(mid: "5099916367123456", limit: 50)'
|
||||||
|
|
||||||
|
# Get fans
|
||||||
|
mcporter call 'weibo.get_fans(uid: "1195230310", limit: 20)'
|
||||||
|
|
||||||
|
# Get followings
|
||||||
|
mcporter call 'weibo.get_followers(uid: "1195230310", limit: 20)'
|
||||||
|
```
|
||||||
|
|
||||||
|
> Zero config. No login needed. Uses the mobile API with auto-generated visitor cookies.
|
||||||
|
|
||||||
|
## Xiaoyuzhou Podcast (groq-whisper + ffmpeg)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Transcribe a single podcast episode (outputs text to /tmp/)
|
||||||
|
~/.agent-reach/tools/xiaoyuzhou/transcribe.sh "https://www.xiaoyuzhoufm.com/episode/EPISODE_ID"
|
||||||
|
```
|
||||||
|
|
||||||
|
> Requires `ffmpeg` and a Groq API key (free).
|
||||||
|
> Configure the key with `agent-reach configure groq-key YOUR_KEY`.
|
||||||
|
> On first run, install the tools with `agent-reach install --env=auto`.
|
||||||
|
> Run `agent-reach doctor` to check status.
|
||||||
|
> Output Markdown files are saved to `/tmp/` by default.
|
||||||
|
|
||||||
|
## LinkedIn (mcporter)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mcporter call 'linkedin.get_person_profile(linkedin_url: "https://linkedin.com/in/username")'
|
||||||
|
mcporter call 'linkedin.search_people(keyword: "AI engineer", limit: 10)'
|
||||||
|
```
|
||||||
|
|
||||||
|
Fallback: `curl -s "https://r.jina.ai/https://linkedin.com/in/username"`
|
||||||
|
|
||||||
|
## V2EX (public API)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Hot topics
|
||||||
|
curl -s "https://www.v2ex.com/api/topics/hot.json" -H "User-Agent: agent-reach/1.0"
|
||||||
|
|
||||||
|
# Topics in a node (node_name examples: python, tech, jobs, qna)
|
||||||
|
curl -s "https://www.v2ex.com/api/topics/show.json?node_name=python&page=1" -H "User-Agent: agent-reach/1.0"
|
||||||
|
|
||||||
|
# Topic details (extract topic_id from URLs like https://www.v2ex.com/t/1234567)
|
||||||
|
curl -s "https://www.v2ex.com/api/topics/show.json?id=TOPIC_ID" -H "User-Agent: agent-reach/1.0"
|
||||||
|
|
||||||
|
# Topic replies
|
||||||
|
curl -s "https://www.v2ex.com/api/replies/show.json?topic_id=TOPIC_ID&page=1" -H "User-Agent: agent-reach/1.0"
|
||||||
|
|
||||||
|
# User profile
|
||||||
|
curl -s "https://www.v2ex.com/api/members/show.json?username=USERNAME" -H "User-Agent: agent-reach/1.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
Python example (`V2EXChannel`):
|
||||||
|
|
||||||
|
```python
|
||||||
|
from agent_reach.channels.v2ex import V2EXChannel
|
||||||
|
|
||||||
|
ch = V2EXChannel()
|
||||||
|
|
||||||
|
# Get hot topics (default 20 items)
|
||||||
|
# Returned fields: id, title, url, replies, node_name, node_title, content(first 200 chars), created
|
||||||
|
topics = ch.get_hot_topics(limit=10)
|
||||||
|
for t in topics:
|
||||||
|
print(f"[{t['node_title']}] {t['title']} ({t['replies']} replies) {t['url']}")
|
||||||
|
print(f" id={t['id']} created={t['created']}")
|
||||||
|
|
||||||
|
# Get latest topics for a specific node
|
||||||
|
# Returned fields: id, title, url, replies, node_name, node_title, content(first 200 chars), created
|
||||||
|
node_topics = ch.get_node_topics("python", limit=5)
|
||||||
|
for t in node_topics:
|
||||||
|
print(t["id"], t["title"], t["url"])
|
||||||
|
|
||||||
|
# Get one topic plus replies
|
||||||
|
# Returned fields: id, title, url, content, replies_count, node_name, node_title,
|
||||||
|
# author, created, replies (list of {author, content, created})
|
||||||
|
topic = ch.get_topic(1234567)
|
||||||
|
print(topic["title"], "—", topic["author"])
|
||||||
|
for r in topic["replies"]:
|
||||||
|
print(f" {r['author']}: {r['content'][:80]}")
|
||||||
|
|
||||||
|
# Get user info
|
||||||
|
# Returned fields: id, username, url, website, twitter, psn, github, btc, location, bio, avatar, created
|
||||||
|
user = ch.get_user("Livid")
|
||||||
|
print(user["username"], user["bio"], user["github"])
|
||||||
|
|
||||||
|
# Search (not supported by the public V2EX API; returns guidance instead)
|
||||||
|
result = ch.search("asyncio")
|
||||||
|
print(result[0]["error"]) # Use built-in site search or the Exa channel instead
|
||||||
|
```
|
||||||
|
|
||||||
|
> No auth required. Results are public JSON. V2EX node names are listed at https://www.v2ex.com/planes
|
||||||
|
|
||||||
|
## Xueqiu (public API)
|
||||||
|
|
||||||
|
```python
|
||||||
|
from agent_reach.channels.xueqiu import XueqiuChannel
|
||||||
|
|
||||||
|
ch = XueqiuChannel()
|
||||||
|
|
||||||
|
# Get stock quotes (symbol examples: SH600519 mainland China, SZ000858 Shenzhen, AAPL US, 00700 HK)
|
||||||
|
# Returned fields: symbol, name, current, percent, chg, high, low, open, last_close,
|
||||||
|
# volume, amount, market_capital, turnover_rate, pe_ttm, timestamp
|
||||||
|
quote = ch.get_stock_quote("AAPL")
|
||||||
|
print(f"{quote['name']} ({quote['symbol']}): {quote['current']} ({quote['percent']}%)")
|
||||||
|
|
||||||
|
# Search stocks
|
||||||
|
# Returned fields: symbol, name, exchange
|
||||||
|
stocks = ch.search_stock("Apple", limit=5)
|
||||||
|
for s in stocks:
|
||||||
|
print(f"{s['name']} ({s['symbol']}) - {s['exchange']}")
|
||||||
|
|
||||||
|
# Hot posts
|
||||||
|
# Returned fields: id, title, text(first 200 chars), author, likes, url
|
||||||
|
posts = ch.get_hot_posts(limit=10)
|
||||||
|
for p in posts:
|
||||||
|
print(f"{p['author']}: {p['text'][:50]}... ({p['likes']} likes)")
|
||||||
|
|
||||||
|
# Hot stocks (stock_type=10 popularity ranking, stock_type=12 watchlist ranking)
|
||||||
|
# Returned fields: symbol, name, current, percent, rank
|
||||||
|
hot = ch.get_hot_stocks(limit=10, stock_type=10)
|
||||||
|
for s in hot:
|
||||||
|
print(f"#{s['rank']} {s['name']} ({s['symbol']}): {s['current']} ({s['percent']}%)")
|
||||||
|
```
|
||||||
|
|
||||||
|
> No login required. Agent Reach auto-fetches session cookies, and all public APIs can be used directly.
|
||||||
|
|
||||||
|
## RSS (feedparser)
|
||||||
|
|
||||||
|
```python
|
||||||
|
python3 -c "
|
||||||
|
import feedparser
|
||||||
|
for e in feedparser.parse('FEED_URL').entries[:5]:
|
||||||
|
print(f'{e.title} — {e.link}')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **Channel not working?** Run `agent-reach doctor` — it shows status and fix instructions.
|
||||||
|
- **Twitter fetch failed?** Ensure `undici` is installed: `npm install -g undici`. Configure a proxy if needed: `agent-reach configure proxy URL`.
|
||||||
|
|
||||||
|
## Setting Up a Channel ("help me configure XXX")
|
||||||
|
|
||||||
|
If a channel needs setup (cookies, Docker, etc.), fetch the install guide:
|
||||||
|
https://raw.githubusercontent.com/Panniantong/agent-reach/main/docs/install.md
|
||||||
|
|
||||||
|
The user only provides cookies. Everything else is your job.
|
||||||
@@ -116,6 +116,11 @@ npx skills add Panniantong/Agent-Reach@agent-reach
|
|||||||
After the Skill is installed, the Agent will auto-detect whether `agent-reach` CLI is available and install it if needed.
|
After the Skill is installed, the Agent will auto-detect whether `agent-reach` CLI is available and install it if needed.
|
||||||
|
|
||||||
> If you install via `agent-reach install`, the skill is registered automatically — no extra steps needed.
|
> If you install via `agent-reach install`, the skill is registered automatically — no extra steps needed.
|
||||||
|
>
|
||||||
|
> Prefer an English-only skill file? Set an English locale or export `AGENT_REACH_LANG=en`
|
||||||
|
> before running `agent-reach install --env=auto` or `agent-reach skill --install`.
|
||||||
|
> The installed file is always written as `SKILL.md`, so switching languages means rerunning
|
||||||
|
> the install command with the new locale and replacing the previously installed skill file.
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ packages = ["agent_reach"]
|
|||||||
|
|
||||||
[tool.hatch.build.targets.wheel.force-include]
|
[tool.hatch.build.targets.wheel.force-include]
|
||||||
"agent_reach/guides" = "agent_reach/guides"
|
"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/skill" = "agent_reach/skill"
|
||||||
"agent_reach/scripts" = "agent_reach/scripts"
|
"agent_reach/scripts" = "agent_reach/scripts"
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Tests for 'agent-reach skill' command and _install_skill / _uninstall_skill."""
|
"""Tests for 'agent-reach skill' command and _install_skill / _uninstall_skill."""
|
||||||
|
|
||||||
|
import importlib.resources
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
@@ -12,6 +13,16 @@ from agent_reach.cli import _install_skill, _uninstall_skill
|
|||||||
class TestSkillCommand(unittest.TestCase):
|
class TestSkillCommand(unittest.TestCase):
|
||||||
"""Test skill install and uninstall via CLI helpers."""
|
"""Test skill install and uninstall via CLI helpers."""
|
||||||
|
|
||||||
|
def test_skill_resources_include_both_locales(self):
|
||||||
|
"""Package resources should expose both default and English skill markdown files."""
|
||||||
|
skill_dir = importlib.resources.files("agent_reach").joinpath("skill")
|
||||||
|
|
||||||
|
default_skill = skill_dir.joinpath("SKILL.md").read_text(encoding="utf-8")
|
||||||
|
english_skill = skill_dir.joinpath("SKILL_en.md").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
self.assertTrue(default_skill.strip())
|
||||||
|
self.assertTrue(english_skill.strip())
|
||||||
|
|
||||||
def test_install_skill_creates_skill_md(self):
|
def test_install_skill_creates_skill_md(self):
|
||||||
"""_install_skill should create SKILL.md in the first available skill dir."""
|
"""_install_skill should create SKILL.md in the first available skill dir."""
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
@@ -85,6 +96,33 @@ class TestSkillCommand(unittest.TestCase):
|
|||||||
content = f.read()
|
content = f.read()
|
||||||
self.assertIn("Agent Reach", content)
|
self.assertIn("Agent Reach", content)
|
||||||
|
|
||||||
|
def test_install_uses_english_skill_for_english_locale(self):
|
||||||
|
"""_install_skill should install the English skill file for English locales."""
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
skill_parent = os.path.join(tmpdir, ".openclaw", "skills")
|
||||||
|
os.makedirs(skill_parent)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"agent_reach.cli.os.path.expanduser",
|
||||||
|
side_effect=lambda p: p.replace("~", tmpdir),
|
||||||
|
):
|
||||||
|
env = os.environ.copy()
|
||||||
|
env.pop("OPENCLAW_HOME", None)
|
||||||
|
env["LANG"] = "en_US.UTF-8"
|
||||||
|
with patch.dict(os.environ, env, clear=True):
|
||||||
|
_install_skill()
|
||||||
|
|
||||||
|
target = os.path.join(skill_parent, "agent-reach", "SKILL.md")
|
||||||
|
self.assertTrue(os.path.exists(target))
|
||||||
|
with open(target) as f:
|
||||||
|
content = f.read()
|
||||||
|
self.assertTrue(content.strip())
|
||||||
|
self.assertIn("Give your AI agent eyes to see the entire internet.", content)
|
||||||
|
self.assertNotIn("搜推特", content)
|
||||||
|
self.assertTrue(
|
||||||
|
os.path.exists(os.path.join(skill_parent, "agent-reach", "references"))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user