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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ AI Agent 已经能帮你写代码、改文档、管项目——但你让它去
| 💬 **微信公众号** | 搜索 + 阅读公众号文章(全文 Markdown) | — | 无需配置 |
| 📰 **微博** | 热搜、搜索内容/用户/话题、用户动态、评论 | — | 无需配置 |
| 💻 **V2EX** | 热门帖子、节点帖子、帖子详情+回复、用户信息 | — | 无需配置 |
| 📈 **雪球** | 股票行情、搜索股票、热门帖子、热门股票排行 | — | 无需配置 |
| 🎙️ **小宇宙播客** | — | 播客音频转文字(Whisper 转录,免费 Key) | 告诉 Agent「帮我配小宇宙播客」 |

> **不知道怎么配?不用查文档。** 直接告诉 Agent「帮我配 XXX」,它知道需要什么、会一步一步引导你。
Expand Down
2 changes: 2 additions & 0 deletions agent_reach/channels/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from .weibo import WeiboChannel
from .xiaoyuzhou import XiaoyuzhouChannel
from .v2ex import V2EXChannel
from .xueqiu import XueqiuChannel



Expand All @@ -38,6 +39,7 @@
WeiboChannel(),
XiaoyuzhouChannel(),
V2EXChannel(),
XueqiuChannel(),
RSSChannel(),
ExaSearchChannel(),
WebChannel(),
Expand Down
196 changes: 196 additions & 0 deletions agent_reach/channels/xueqiu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
# -*- coding: utf-8 -*-
"""Xueqiu (雪球) — stock quotes, search, trending posts & hot stocks."""

import http.cookiejar
import json
import re
import urllib.request
from typing import Any

from .base import Channel

_UA = "agent-reach/1.0"
_TIMEOUT = 10
_XUEQIU_HOME = "https://xueqiu.com"

# --------------- cookie-aware HTTP helpers --------------- #

_cookie_jar = http.cookiejar.CookieJar()
_opener = urllib.request.build_opener(
urllib.request.HTTPCookieProcessor(_cookie_jar),
)
_cookies_initialized = False


def _ensure_cookies() -> None:
"""Visit xueqiu.com homepage once to obtain session cookies."""
global _cookies_initialized
if _cookies_initialized:
return
req = urllib.request.Request(_XUEQIU_HOME, headers={"User-Agent": _UA})
_opener.open(req, timeout=_TIMEOUT)
_cookies_initialized = True


def _get_json(url: str) -> Any:
"""Fetch *url* with Xueqiu session cookies and return parsed JSON."""
_ensure_cookies()
req = urllib.request.Request(url, headers={"User-Agent": _UA})
with _opener.open(req, timeout=_TIMEOUT) as resp:
return json.loads(resp.read().decode("utf-8"))


def _strip_html(text: str) -> str:
"""Remove HTML tags and decode common entities."""
text = re.sub(r"<[^>]+>", "", text)
for entity, char in (("&nbsp;", " "), ("&amp;", "&"), ("&lt;", "<"), ("&gt;", ">")):
text = text.replace(entity, char)
return text.strip()


class XueqiuChannel(Channel):
name = "xueqiu"
description = "雪球股票行情与社区动态"
backends = ["Xueqiu API (public)"]
tier = 0

# ------------------------------------------------------------------ #
# URL routing
# ------------------------------------------------------------------ #

def can_handle(self, url: str) -> bool:
from urllib.parse import urlparse

d = urlparse(url).netloc.lower()
return "xueqiu.com" in d

# ------------------------------------------------------------------ #
# Health check
# ------------------------------------------------------------------ #

def check(self, config=None):
try:
data = _get_json("https://stock.xueqiu.com/v5/stock/batch/quote.json?symbol=SH000001")
items = (data.get("data") or {}).get("items") or []
if items:
return "ok", "公开 API 可用(行情、搜索、热帖、热股)"
return "warn", "API 响应异常(返回数据为空)"
except Exception as e:
return "warn", f"Xueqiu API 连接失败(可能需要代理):{e}"

# ------------------------------------------------------------------ #
# Data-fetching methods
# ------------------------------------------------------------------ #

def get_stock_quote(self, symbol: str) -> dict:
"""获取实时股票行情。

Args:
symbol: 股票代码,如 SH600519(沪)、SZ000858(深)、AAPL(美)、00700(港)

Returns a dict with keys:
symbol, name, current, percent, chg, high, low, open, last_close,
volume, amount, market_capital, turnover_rate, pe_ttm, timestamp
"""
data = _get_json(f"https://stock.xueqiu.com/v5/stock/batch/quote.json?symbol={symbol}")
items = (data.get("data") or {}).get("items") or []
q = (items[0].get("quote") or {}) if items else {}
return {
"symbol": q.get("symbol", symbol),
"name": q.get("name", ""),
"current": q.get("current"),
"percent": q.get("percent"),
"chg": q.get("chg"),
"high": q.get("high"),
"low": q.get("low"),
"open": q.get("open"),
"last_close": q.get("last_close"),
"volume": q.get("volume"),
"amount": q.get("amount"),
"market_capital": q.get("market_capital"),
"turnover_rate": q.get("turnover_rate"),
"pe_ttm": q.get("pe_ttm"),
"timestamp": q.get("timestamp"),
}

def search_stock(self, query: str, limit: int = 10) -> list:
"""搜索股票。

Args:
query: 股票代码或中文名称,如 "茅台"、"600519"
limit: 最多返回条数

Returns a list of dicts with keys:
symbol, name, exchange
"""
data = _get_json(
f"https://xueqiu.com/stock/search.json?code={urllib.request.quote(query)}&size={limit}"
)
stocks = data.get("stocks") or []
results = []
for s in stocks[:limit]:
results.append(
{
"symbol": s.get("code", ""),
"name": s.get("name", ""),
"exchange": s.get("exchange", ""),
}
)
return results

def get_hot_posts(self, limit: int = 20) -> list:
"""获取雪球热门帖子。

Args:
limit: 最多返回条数(上限 50)

Returns a list of dicts with keys:
id, title, text, author, likes, url
"""
data = _get_json("https://xueqiu.com/statuses/hot/listV3.json?source=hot&page=1")
items = (data.get("data") or {}).get("items") or []
results = []
for item in items[:limit]:
original = item.get("original_status") or item
text = _strip_html(original.get("text") or original.get("description") or "")
user = original.get("user") or {}
results.append(
{
"id": original.get("id", 0),
"title": original.get("title") or "",
"text": text[:200],
"author": user.get("screen_name", ""),
"likes": original.get("like_count", 0),
"url": f"https://xueqiu.com{original['target']}"
if original.get("target")
else "",
}
)
return results

def get_hot_stocks(self, limit: int = 10, stock_type: int = 10) -> list:
"""获取热门股票排行。

Args:
limit: 最多返回条数(上限 50)
stock_type: 10=人气榜(默认),12=关注榜

Returns a list of dicts with keys:
symbol, name, current, percent, rank
"""
data = _get_json(
f"https://stock.xueqiu.com/v5/stock/hot_stock/list.json?size={limit}&type={stock_type}"
)
items = (data.get("data") or {}).get("items") or []
results = []
for idx, item in enumerate(items[:limit], 1):
results.append(
{
"symbol": item.get("code") or item.get("symbol", ""),
"name": item.get("name", ""),
"current": item.get("current"),
"percent": item.get("percent"),
"rank": idx,
}
)
return results
41 changes: 38 additions & 3 deletions agent_reach/skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@
name: agent-reach
description: >
Give your AI agent eyes to see the entire internet.
Search and read 16 platforms: Twitter/X, Reddit, YouTube, GitHub, Bilibili,
Search and read 17 platforms: Twitter/X, Reddit, YouTube, GitHub, Bilibili,
XiaoHongShu, Douyin, Weibo, WeChat Articles, Xiaoyuzhou Podcast, LinkedIn,
Instagram, V2EX, RSS, Exa web search, and any web page.
V2EX, Xueqiu, RSS, Exa web search, and any web page.
Zero config for 8 channels. Use when user asks to search, read, or interact
on any supported platform, shares a URL, or asks to search the web.
Triggers: "搜推特", "搜小红书", "看视频", "搜一下", "上网搜", "帮我查",
"search twitter", "youtube transcript", "search reddit", "read this link",
"B站", "bilibili", "抖音视频", "微信文章", "公众号", "微博", "V2EX",
"小宇宙", "播客", "podcast", "web search", "research", "帮我安装".
"小宇宙", "播客", "podcast", "雪球", "股票", "stock quote",
"web search", "research", "帮我安装".
metadata:
openclaw:
homepage: https://github.com/Panniantong/Agent-Reach
Expand Down Expand Up @@ -247,6 +248,40 @@ print(result[0]["error"]) # 提示使用站内搜索或 Exa channel

> No auth required. Results are public JSON. V2EX 节点名见 https://www.v2ex.com/planes

## 雪球 / Xueqiu (public API)

```python
from agent_reach.channels.xueqiu import XueqiuChannel

ch = XueqiuChannel()

# 获取股票行情(符号格式:SH600519 沪市、SZ000858 深市、AAPL 美股、00700 港股)
# 返回字段:symbol, name, current, percent, chg, high, low, open, last_close,
# volume, amount, market_capital, turnover_rate, pe_ttm, timestamp
quote = ch.get_stock_quote("SH600519")
print(f"{quote['name']} ({quote['symbol']}): {quote['current']} ({quote['percent']}%)")

# 搜索股票
# 返回字段:symbol, name, exchange
stocks = ch.search_stock("茅台", limit=5)
for s in stocks:
print(f"{s['name']} ({s['symbol']}) - {s['exchange']}")

# 热门帖子
# 返回字段:id, title, text(前200字), author, likes, url
posts = ch.get_hot_posts(limit=10)
for p in posts:
print(f"{p['author']}: {p['text'][:50]}... ({p['likes']} 赞)")

# 热门股票(stock_type=10 人气榜,stock_type=12 关注榜)
# 返回字段: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']}%)")
```

> 无需登录。自动获取会话 Cookie,所有公开 API 均可直接使用。

## RSS (feedparser)

## RSS
Expand Down
1 change: 1 addition & 0 deletions docs/README_en.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ Update Agent Reach: https://raw.githubusercontent.com/Panniantong/agent-reach/ma
| 💬 **WeChat Articles** | Search + Read | Zero config | Search + read WeChat Official Account articles (full Markdown) ([wechat-article-for-ai](https://github.com/Panniantong/wechat-article-for-ai) + [miku_ai](https://github.com/GobinFan/Miku_Spider)) |
| 📰 **Weibo** | Trending · Search · Feeds · Comments | Zero config | Hot search, content/user/topic search, feeds, comments ([mcp-server-weibo](https://github.com/Panniantong/mcp-server-weibo)) |
| 💻 **V2EX** | Hot topics · Node topics · Topic detail + replies · User profile | Zero config | Public JSON API, no auth required. Great for tech community content |
| 📈 **Xueqiu (雪球)** | Stock quotes · Search · Hot posts · Hot stocks | Zero config | Public API with auto session cookies, no login required |
| 🎙️ **Xiaoyuzhou Podcast** | Transcription | Free API key | Podcast audio → full text transcript via Groq Whisper (free) |
| 🔍 **Web Search** | Search | Auto-configured | Auto-configured during install, free, no API key ([Exa](https://exa.ai) via [mcporter](https://github.com/nicepkg/mcporter)) |
| 📦 **GitHub** | Read · Search | Zero config | [gh CLI](https://cli.github.com) powered. Public repos work immediately. `gh auth login` unlocks Fork, Issue, PR |
Expand Down
1 change: 1 addition & 0 deletions tests/test_channel_contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ def test_channel_can_handle_contract():
"linkedin": "https://www.linkedin.com/in/test",
"weibo": "https://weibo.com/u/1749127163",
"rss": "https://example.com/feed.xml",
"xueqiu": "https://xueqiu.com/S/SH600519",
"exa_search": "https://example.com",
"web": "https://example.com",
}
Expand Down
Loading
Loading