feat(reddit): honest tiering — OpenCLI / rdt-cli, no zero-config path

- live-verified 2026-06: anonymous .json endpoints are 403-blocked (all
  variants) and the official API closed self-service registration in
  2025-11 — so the channel now says plainly: every Reddit backend needs
  a logged-in session, mainland China needs a proxy
- backends = [OpenCLI, rdt-cli]: OpenCLI rides the browser session
  (desktop preferred); rdt-cli stays for servers and existing installs
  (upstream unmaintained since 2026-03, noted in the ok message)
- install: desktop routes to OpenCLI, server installs rdt-cli from the
  pinned git source (split out as _install_rdt_cli)
- skill/references/social.md: Reddit section rewritten as two backend
  command groups + a PRAW note scoped to users who already hold
  pre-2025-11 credentials (explicitly not recommended for new users)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Pnant
2026-06-11 16:10:04 +08:00
parent 3e5f9df5b8
commit d7551bf19d
5 changed files with 156 additions and 38 deletions
+63 -19
View File
@@ -1,9 +1,12 @@
# -*- coding: utf-8 -*-
"""Reddit — search and read via rdt-cli (public-clis/rdt-cli).
"""Reddit — multi-backend: OpenCLI / rdt-cli. Login is mandatory.
NOTE: Reddit requires authentication since 2024. All API requests
(including public subreddit reads) return HTTP 403 without a valid
session cookie. Run `rdt login` after installation to authenticate.
Honest tiering (live-verified 2026-06): there is NO zero-config path.
Anonymous .json endpoints are blocked (403 anti-bot, all variants), and
the official API closed self-service registration in 2025-11 (manual
approval, individual scripts rarely granted — PRAW is only an option for
users who already hold credentials). Every working backend rides a
logged-in session: OpenCLI reuses the browser's, rdt-cli imports cookies.
"""
import json
@@ -32,8 +35,8 @@ _RDT_BROKEN_HINT = (
class RedditChannel(Channel):
name = "reddit"
description = "Reddit 帖子和评论"
backends = ["rdt-cli"]
tier = 1 # Reddit requires login since 2024 (rdt login) — not zero-config
backends = ["OpenCLI", "rdt-cli"]
tier = 1 # no zero-config path exists — see module docstring
def can_handle(self, url: str) -> bool:
from urllib.parse import urlparse
@@ -42,19 +45,59 @@ class RedditChannel(Channel):
return "reddit.com" in d or "redd.it" in d
def check(self, config=None):
"""Probe candidates in order; first fully-usable backend wins."""
self.active_backend = None
findings = []
for backend in self.ordered_backends(config):
if backend == "OpenCLI":
result = self._check_opencli()
else:
result = self._check_rdt()
if result is None:
continue
findings.append((backend, *result))
for wanted in ("ok", "warn"):
for backend, status, message in findings:
if status == wanted:
self.active_backend = backend
return status, message
if findings:
return "error", "\n".join(m for _, _, m in findings)
return "off", (
"未安装任何 Reddit 后端。注意:Reddit 没有零配置路径"
"(匿名 .json 已被封,官方 API 需人工审批),必须用登录态。推荐:\n"
" 桌面:agent-reach install --channels opencli\n"
" (复用 Chrome 登录态,登录过 reddit.com 即可用)\n"
f" 服务器/存量:pipx install '{_RDT_GIT_SOURCE}'\n"
" 然后 `rdt login` 或手动写入 Cookie(见 doctor 提示)\n"
"中国大陆访问 Reddit 需要代理"
)
def _check_opencli(self):
"""OpenCLI candidate. None = not installed."""
from agent_reach.backends import opencli_status
st = opencli_status()
if not st.installed:
return None
if st.broken:
return "error", st.hint
if st.ready:
return "ok", (
"OpenCLI 可用(复用浏览器登录态)。用法:"
"opencli reddit search/read/subreddit/hot -f yaml"
)
return "warn", st.hint
def _check_rdt(self):
"""rdt-cli candidate. None = not installed."""
rdt = shutil.which("rdt")
if not rdt:
return "off", (
"需要安装 rdt-cli。PyPI 版本可能暂时落后,推荐直接从 GitHub 安装:\n"
f" pipx install '{_RDT_GIT_SOURCE}'\n"
"如已确认 PyPI 版本已更新,也可使用:\n"
" pipx install rdt-cli\n"
" uv tool install rdt-cli\n"
"最新源码:https://github.com/public-clis/rdt-cli\n"
"安装后运行 `rdt login` 登录(需先在浏览器登录 reddit.com)"
)
return None
# 不走 probe_command:实测 `rdt status --json` 成功时(rc=0)也会向 stderr
# 打网络重试日志,probe 把 stdout+stderr 合并后 JSON 解析必炸。
@@ -83,9 +126,7 @@ class RedditChannel(Channel):
tail = detail[-1] if detail else "无输出"
return "error", f"rdt 异常退出(exit {r.returncode}):{tail}。运行 `rdt status` 查看详情"
# 进程正常退出 → rdt 本身是活的(无论登录与否),后端即为可用
self.active_backend = "rdt-cli"
# 进程正常退出 → rdt 本身是活的(无论登录与否)
try:
data = json.loads(r.stdout or "")
except json.JSONDecodeError:
@@ -101,7 +142,10 @@ class RedditChannel(Channel):
if authenticated:
suffix = f"(已登录:{username}" if username else ""
return "ok", (f"rdt-cli 可用{suffix}(搜索帖子、阅读全文、查看评论)")
return "ok", (
f"rdt-cli 可用{suffix}(搜索帖子、阅读全文、查看评论;"
"上游 2026-03 起停更,桌面用户建议迁移到 OpenCLI)"
)
return "warn", (
"rdt-cli 已安装但未登录。Reddit 自 2024 年起要求认证,"
+18 -1
View File
@@ -774,7 +774,24 @@ def _install_opencli_deps():
def _install_reddit_deps():
"""Install rdt-cli for Reddit search + reading."""
"""Set up Reddit — desktop prefers OpenCLI, rdt-cli for servers/legacy.
No zero-config path exists (anonymous .json blocked, official API
approval-gated since 2025-11) — every backend needs a logged-in session.
"""
if _detect_environment() != "server":
_install_opencli_deps()
print(" Reddit 走 OpenCLI(浏览器里登录过 reddit.com 即可用)")
import shutil
if shutil.which("rdt"):
print(" ✅ 检测到存量 rdt-cli,将作为备选后端继续可用")
return
_install_rdt_cli()
def _install_rdt_cli():
"""Install rdt-cli (pinned git source — PyPI lags upstream)."""
import shutil
import subprocess
+30 -12
View File
@@ -180,25 +180,43 @@ user = ch.get_user("Livid")
> **节点列表**: https://www.v2ex.com/planes
## Reddit (rdt-cli)
## Reddit(多后端,必须登录态)
**Reddit 没有零配置路径**:匿名 `.json` 端点已被封(403),官方 API 自 2025-11 起人工审批基本不批。两个后端都靠登录态,先跑 `agent-reach doctor --json` 看 reddit 的 `active_backend`。中国大陆访问需代理。
### 后端 A:OpenCLI(桌面首选,复用浏览器登录态)
```bash
# 搜索帖子
rdt search "query" --limit 10
opencli reddit search "query" -f yaml
# 读帖子全文 + 评论
rdt read POST_ID
opencli reddit read POST_ID -f yaml
# 浏览 subreddit
rdt sub python --limit 20
# 浏览 subreddit / 热门 / Popular
opencli reddit subreddit LocalLLaMA -f yaml
opencli reddit hot -f yaml
opencli reddit popular -f yaml
# 浏览热门
rdt popular --limit 10
# 浏览 /r/all
rdt all --limit 10
# subreddit 元信息(订阅数、简介)
opencli reddit subreddit-info LocalLLaMA -f yaml
```
> **安装**: `pipx install 'git+https://github.com/public-clis/rdt-cli.git'`PyPI 版本暂时落后,需从 GitHub 装 v0.4.2+)。需要先登录(`rdt login`)才能搜索和阅读
> 需要登录的功能:`rdt feed --subs-only`(订阅列表)、`rdt saved`(收藏)。
> 要求 Chrome 打开且浏览器里登录过 reddit.com
### 后端 B:rdt-cli(存量/服务器备选,上游 2026-03 起停更)
```bash
rdt search "query" --limit 10 # 搜索帖子
rdt read POST_ID # 读帖子全文 + 评论
rdt sub python --limit 20 # 浏览 subreddit
rdt popular --limit 10 # 浏览热门
rdt all --limit 10 # 浏览 /r/all
```
> **安装**: `pipx install 'git+https://github.com/public-clis/rdt-cli.git'`PyPI 版本落后,需从 GitHub 装 v0.4.2+)。先 `rdt login` 才能搜索和阅读(服务器无浏览器时手动写 Cookie,见 doctor 提示)。
> 建议使用 `--yaml` 输出,对 AI agent 更友好。
### 高级选项:官方 API + PRAW(仅限已有凭证的用户)
2025-11 前注册过 Reddit script app(持有 client_id/client_secret)的用户可以用 PRAW 走官方 API(100 QPM 免费)。新申请需人工审批且个人项目基本不批,**不要推荐新用户走这条路**。
+27 -4
View File
@@ -647,17 +647,36 @@ class TestXueqiuChannel:
class TestRedditChannel:
def test_reports_off_when_not_installed(self, monkeypatch):
"""多后端:OpenCLI > rdt-cli,没有零配置路径。"""
@staticmethod
def _isolate(monkeypatch, opencli=None):
"""隔离 OpenCLI 候选(None = 未安装),聚焦 rdt-cli 路径。"""
from agent_reach.channels.reddit import RedditChannel
monkeypatch.setattr(RedditChannel, "_check_opencli", lambda self: opencli)
def test_reports_off_when_nothing_installed(self, monkeypatch):
self._isolate(monkeypatch)
monkeypatch.setattr(shutil, "which", lambda _: None)
from agent_reach.channels.reddit import RedditChannel
status, msg = RedditChannel().check()
assert status == "off"
assert "rdt-cli" in msg
assert "public-clis/rdt-cli" in msg
# 诚实口径:明说没有零配置路径,推荐 OpenCLI + rdt git 源
assert "零配置" in msg
assert "opencli" in msg
assert "git+https://github.com/public-clis/rdt-cli.git" in msg
assert "rdt-cli>=0.4.2" not in msg
def test_opencli_ready_wins(self, monkeypatch):
self._isolate(monkeypatch, opencli=("ok", "OpenCLI 可用(复用浏览器登录态)"))
monkeypatch.setattr(shutil, "which", lambda _: None)
from agent_reach.channels.reddit import RedditChannel
ch = RedditChannel()
status, msg = ch.check()
assert status == "ok"
assert ch.active_backend == "OpenCLI"
def test_reports_ok_when_authenticated(self, monkeypatch):
self._isolate(monkeypatch)
monkeypatch.setattr(shutil, "which", lambda _: "/usr/local/bin/rdt")
fake_output = json.dumps({
"ok": True,
@@ -677,6 +696,7 @@ class TestRedditChannel:
assert ch.active_backend == "rdt-cli"
def test_reports_warn_when_not_authenticated(self, monkeypatch):
self._isolate(monkeypatch)
monkeypatch.setattr(shutil, "which", lambda _: "/usr/local/bin/rdt")
fake_output = json.dumps({
"ok": True,
@@ -701,6 +721,7 @@ class TestRedditChannel:
def test_reports_error_when_status_check_fails(self, monkeypatch):
"""rdt 非零退出且输出不可解析 → 工具异常(error),不再算 warn。"""
self._isolate(monkeypatch)
monkeypatch.setattr(shutil, "which", lambda _: "/usr/local/bin/rdt")
def fake_run(cmd, **kwargs):
@@ -716,6 +737,7 @@ class TestRedditChannel:
def test_reports_error_with_reinstall_hint_when_broken(self, monkeypatch):
"""which 命中但 exec 抛 FileNotFoundErrorvenv 断链)→ error + 重装处方。"""
self._isolate(monkeypatch)
monkeypatch.setattr(shutil, "which", lambda _: "/usr/local/bin/rdt")
def fake_run(cmd, **kwargs):
@@ -733,6 +755,7 @@ class TestRedditChannel:
def test_reports_error_with_reinstall_hint_on_exit_127(self, monkeypatch):
"""退出码 127(找到但跑不动)同样按断链处理。"""
self._isolate(monkeypatch)
monkeypatch.setattr(shutil, "which", lambda _: "/usr/local/bin/rdt")
def fake_run(cmd, **kwargs):
+18 -2
View File
@@ -60,7 +60,7 @@ class TestCLI:
assert auth_token == "token123"
assert ct0 == "ct0abc"
def test_install_reddit_deps_prefers_github_source(self, monkeypatch, capsys):
def test_install_rdt_cli_prefers_github_source(self, monkeypatch, capsys):
state = {"rdt_installed": False}
commands = []
@@ -79,12 +79,28 @@ class TestCLI:
monkeypatch.setattr(shutil, "which", fake_which)
monkeypatch.setattr(subprocess, "run", fake_run)
cli._install_reddit_deps()
cli._install_rdt_cli()
out = capsys.readouterr().out
assert commands == [["pipx", "install", cli._RDT_GIT_SOURCE]]
assert "✅ rdt-cli installed" in out
def test_install_reddit_deps_routes_by_environment(self, monkeypatch):
"""桌面 → OpenCLI;服务器 → rdt-cli(钉 git 源)。"""
calls = []
monkeypatch.setattr(cli, "_install_opencli_deps", lambda: calls.append("opencli"))
monkeypatch.setattr(cli, "_install_rdt_cli", lambda: calls.append("rdt"))
monkeypatch.setattr(shutil, "which", lambda _: None)
monkeypatch.setattr(cli, "_detect_environment", lambda: "local")
cli._install_reddit_deps()
assert calls == ["opencli"]
calls.clear()
monkeypatch.setattr(cli, "_detect_environment", lambda: "server")
cli._install_reddit_deps()
assert calls == ["rdt"]
class TestCheckUpdateRetry:
def test_retry_timeout_classification(self):