feat(bilibili,twitter): yt-dlp exits bilibili; OpenCLI joins both
bilibili: - live-verified 2026-06: bilibili 412-blocks yt-dlp in every configuration (latest version, direct, proxied, warmed cookies) while bili-cli works fine without login — so yt-dlp no longer serves this channel (it remains the YouTube backend) - backends = [bili-cli, OpenCLI, B站搜索 API]: bili-cli covers search/hot/rank/video-detail/audio, OpenCLI adds subtitles through the browser session, the search API is the zero-dependency fallback - when a broken candidate is bypassed by a working fallback, its reinstall prescription is appended to the winning message instead of being swallowed - skill docs (social.md + video.md): "do NOT use yt-dlp for bilibili" warning, bili-cli/OpenCLI command groups, curl fallback recipe, B站 audio transcription path via `bili audio` + agent-reach transcribe twitter: - OpenCLI joins as the middle candidate [twitter-cli, OpenCLI, bird] - social.md: explicit 4-step search retry chain (retry → upgrade → OpenCLI → stable-command detour) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,15 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Bilibili — video via yt-dlp, search/browse via bili-cli or API."""
|
"""Bilibili — multi-backend: bili-cli / OpenCLI / search API.
|
||||||
|
|
||||||
|
yt-dlp was REMOVED from this channel (live-verified 2026-06): bilibili's
|
||||||
|
risk control 412-blocks yt-dlp's requests in every configuration we
|
||||||
|
tried — latest version, direct, proxied, with warmed cookies — while
|
||||||
|
bili-cli keeps working (search/hot/video detail without login) and
|
||||||
|
OpenCLI covers subtitles through the browser session. yt-dlp remains the
|
||||||
|
YouTube backend; it just no longer serves bilibili.
|
||||||
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
from agent_reach.probe import probe_command
|
from agent_reach.probe import probe_command
|
||||||
@@ -28,7 +35,7 @@ def _search_api_ok() -> bool:
|
|||||||
class BilibiliChannel(Channel):
|
class BilibiliChannel(Channel):
|
||||||
name = "bilibili"
|
name = "bilibili"
|
||||||
description = "B站视频、字幕和搜索"
|
description = "B站视频、字幕和搜索"
|
||||||
backends = ["yt-dlp", "bili-cli (可选)", "B站搜索 API"]
|
backends = ["bili-cli", "OpenCLI", "B站搜索 API"]
|
||||||
tier = 1
|
tier = 1
|
||||||
|
|
||||||
def can_handle(self, url: str) -> bool:
|
def can_handle(self, url: str) -> bool:
|
||||||
@@ -37,49 +44,76 @@ class BilibiliChannel(Channel):
|
|||||||
return "bilibili.com" in d or "b23.tv" in d
|
return "bilibili.com" in d or "b23.tv" in d
|
||||||
|
|
||||||
def check(self, config=None):
|
def check(self, config=None):
|
||||||
|
"""Probe candidates in order; first fully-usable backend wins."""
|
||||||
self.active_backend = None
|
self.active_backend = None
|
||||||
|
findings = []
|
||||||
|
|
||||||
# 真跑 yt-dlp --version,区分 未装 / 断链 / 异常(which 命中不等于能用)
|
for backend in self.ordered_backends(config):
|
||||||
yt = probe_command("yt-dlp", ["--version"], timeout=10, package="yt-dlp")
|
if backend == "bili-cli":
|
||||||
if yt.status == "missing":
|
result = self._check_bili_cli()
|
||||||
return "off", "yt-dlp 未安装。安装:pip install yt-dlp"
|
elif backend == "OpenCLI":
|
||||||
if yt.status == "broken":
|
result = self._check_opencli()
|
||||||
return "error", "yt-dlp 已安装但无法执行\n" + yt.hint
|
|
||||||
if not yt.ok:
|
|
||||||
detail = yt.hint or yt.output or yt.status
|
|
||||||
return "error", f"yt-dlp 探测失败({yt.status}):{detail}"
|
|
||||||
|
|
||||||
self.active_backend = "yt-dlp"
|
|
||||||
|
|
||||||
proxy = (config.get("bilibili_proxy") if config else None) or os.environ.get("BILIBILI_PROXY")
|
|
||||||
# 真跑 bili --version——断链时旧的 which 检测会误报"bili-cli 可用"
|
|
||||||
bili = probe_command("bili", ["--version"], timeout=10, package="bilibili-cli")
|
|
||||||
|
|
||||||
parts = []
|
|
||||||
|
|
||||||
# 视频读取状态
|
|
||||||
if proxy:
|
|
||||||
parts.append("视频读取:yt-dlp(代理已配置)")
|
|
||||||
else:
|
|
||||||
parts.append("视频读取:yt-dlp")
|
|
||||||
|
|
||||||
# bili-cli 增强
|
|
||||||
if bili.ok:
|
|
||||||
parts.append("搜索/热门/排行:bili-cli 可用")
|
|
||||||
status = "ok"
|
|
||||||
else:
|
|
||||||
if bili.status == "broken":
|
|
||||||
parts.append("bili-cli 已安装但无法执行,不计为可用\n" + bili.hint)
|
|
||||||
elif bili.status in ("timeout", "error"):
|
|
||||||
parts.append(f"bili-cli 探测失败({bili.status}),不计为可用")
|
|
||||||
# 降级走搜索 API;只探测一次,message 和 status 共用结果
|
|
||||||
api_ok = _search_api_ok()
|
|
||||||
if api_ok:
|
|
||||||
parts.append("搜索:B站 API 可用")
|
|
||||||
else:
|
else:
|
||||||
parts.append("搜索:B站 API 不可达")
|
result = self._check_search_api()
|
||||||
if bili.status == "missing":
|
if result is None:
|
||||||
parts.append("提示:安装 bili-cli 可解锁热门/排行/动态:pipx install bilibili-cli")
|
continue
|
||||||
status = "ok" if api_ok else "warn"
|
findings.append((backend, *result))
|
||||||
|
|
||||||
return status, "。".join(parts)
|
# 有后端断链时,即使别的候选兜底成功也要把处方带出来
|
||||||
|
broken_notes = [m for _, s, m in findings if s == "error"]
|
||||||
|
|
||||||
|
for wanted in ("ok", "warn"):
|
||||||
|
for backend, status, message in findings:
|
||||||
|
if status == wanted:
|
||||||
|
self.active_backend = backend
|
||||||
|
if broken_notes:
|
||||||
|
message += "\n[备选后端异常] " + ";".join(broken_notes)
|
||||||
|
return status, message
|
||||||
|
|
||||||
|
if findings:
|
||||||
|
return "error", "\n".join(m for _, _, m in findings)
|
||||||
|
|
||||||
|
return "off", (
|
||||||
|
"没有可用的 B站后端(搜索 API 也不可达,可能是网络问题)。推荐:\n"
|
||||||
|
" pipx install bilibili-cli(搜索/热门/视频详情,无需登录)\n"
|
||||||
|
" 或桌面装 OpenCLI(额外解锁字幕):agent-reach install --channels opencli"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _check_bili_cli(self):
|
||||||
|
"""bili-cli candidate. None = not installed."""
|
||||||
|
probe = probe_command("bili", ["--version"], timeout=10, package="bilibili-cli")
|
||||||
|
if probe.status == "missing":
|
||||||
|
return None
|
||||||
|
if probe.status == "broken":
|
||||||
|
return "error", "bili 命令存在但无法执行\n" + probe.hint
|
||||||
|
if not probe.ok:
|
||||||
|
return "warn", f"bili-cli 探测失败({probe.status}),运行 `bili status` 查看详情"
|
||||||
|
return "ok", (
|
||||||
|
"bili-cli 可用(搜索/热门/排行/视频详情/音频,无需登录;"
|
||||||
|
"字幕需 OpenCLI。上游 2026-03 起停更)"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 bilibili search/video/subtitle/ranking -f yaml"
|
||||||
|
)
|
||||||
|
return "warn", st.hint
|
||||||
|
|
||||||
|
def _check_search_api(self):
|
||||||
|
"""Zero-dependency search API fallback. None = unreachable."""
|
||||||
|
if not _search_api_ok():
|
||||||
|
return None
|
||||||
|
return "ok", (
|
||||||
|
"B站搜索 API 可达(仅搜索,curl 直连)。"
|
||||||
|
"完整功能建议安装 bili-cli:pipx install bilibili-cli"
|
||||||
|
)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from agent_reach.probe import probe_command
|
|||||||
class TwitterChannel(Channel):
|
class TwitterChannel(Channel):
|
||||||
name = "twitter"
|
name = "twitter"
|
||||||
description = "Twitter/X 推文"
|
description = "Twitter/X 推文"
|
||||||
backends = ["twitter-cli", "bird CLI (legacy)"]
|
backends = ["twitter-cli", "OpenCLI", "bird CLI (legacy)"]
|
||||||
tier = 1
|
tier = 1
|
||||||
|
|
||||||
def can_handle(self, url: str) -> bool:
|
def can_handle(self, url: str) -> bool:
|
||||||
@@ -24,6 +24,8 @@ class TwitterChannel(Channel):
|
|||||||
for backend in self.ordered_backends(config):
|
for backend in self.ordered_backends(config):
|
||||||
if backend == "twitter-cli":
|
if backend == "twitter-cli":
|
||||||
result = self._check_twitter_cli()
|
result = self._check_twitter_cli()
|
||||||
|
elif backend == "OpenCLI":
|
||||||
|
result = self._check_opencli()
|
||||||
elif backend == "bird CLI (legacy)":
|
elif backend == "bird CLI (legacy)":
|
||||||
result = self._check_bird()
|
result = self._check_bird()
|
||||||
else:
|
else:
|
||||||
@@ -84,6 +86,22 @@ class TwitterChannel(Channel):
|
|||||||
" twitter -v status 查看详细信息"
|
" twitter -v status 查看详细信息"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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 twitter search/article/user-posts -f yaml"
|
||||||
|
)
|
||||||
|
return "warn", st.hint
|
||||||
|
|
||||||
def _check_bird(self):
|
def _check_bird(self):
|
||||||
"""探测 bird/birdx(legacy 回退)。返回 None 表示均未安装,否则返回 (status, message)。"""
|
"""探测 bird/birdx(legacy 回退)。返回 None 表示均未安装,否则返回 (status, message)。"""
|
||||||
last_failure = None
|
last_failure = None
|
||||||
|
|||||||
@@ -89,12 +89,18 @@ twitter user @username
|
|||||||
```bash
|
```bash
|
||||||
# 搜索推文(Twitter 频繁改 GraphQL 端点,可能 404)
|
# 搜索推文(Twitter 频繁改 GraphQL 端点,可能 404)
|
||||||
twitter search "query" -n 10
|
twitter search "query" -n 10
|
||||||
# 如果 search 返回 404,升级 twitter-cli:pipx upgrade twitter-cli
|
|
||||||
|
|
||||||
# likes(2024 年后只能看自己的,平台限制)
|
# likes(2024 年后只能看自己的,平台限制)
|
||||||
twitter likes
|
twitter likes
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### search 失败时的重试链(按序执行,成功即停)
|
||||||
|
|
||||||
|
1. 直接重试一次(偶发失败常见):`twitter search "query" -n 10`
|
||||||
|
2. 升级后再试:`pipx upgrade twitter-cli && twitter search "query" -n 10`
|
||||||
|
3. 换 OpenCLI 备选(桌面,复用浏览器登录态):`opencli twitter search "query" -f yaml`
|
||||||
|
4. 都不行就改用 `twitter feed` / `twitter user-posts @somebody` 等稳定命令绕路
|
||||||
|
|
||||||
### 重要注意事项
|
### 重要注意事项
|
||||||
|
|
||||||
> **安装**: `pipx install twitter-cli`(确保 v0.8.5+)
|
> **安装**: `pipx install twitter-cli`(确保 v0.8.5+)
|
||||||
@@ -103,21 +109,25 @@ twitter likes
|
|||||||
>
|
>
|
||||||
> **IP 风控**: 不要在 VPS/数据中心 IP 上频繁调用,尤其是 followers/following,有封号风险。使用住宅代理或本地环境。
|
> **IP 风控**: 不要在 VPS/数据中心 IP 上频繁调用,尤其是 followers/following,有封号风险。使用住宅代理或本地环境。
|
||||||
>
|
>
|
||||||
> **search 可能失效**: Twitter 频繁修改 GraphQL API,search 命令可能随时返回 404。如遇到,先 `pipx upgrade twitter-cli`。如果最新版仍不行,说明上游还没跟上 Twitter 的改动,用 `twitter feed` 替代。
|
> **OpenCLI 备选**: 桌面装了 OpenCLI 的话,`opencli twitter search/article/user-posts -f yaml` 全套可用(浏览器登录态,无需 cookie 环境变量)。
|
||||||
>
|
>
|
||||||
> **输出格式**: 建议用 `--yaml` 或 `--json` 获得结构化输出,对 AI agent 更友好。
|
> **输出格式**: 建议用 `--yaml` 或 `--json` 获得结构化输出,对 AI agent 更友好。
|
||||||
|
|
||||||
## B站 / Bilibili
|
## B站 / Bilibili
|
||||||
|
|
||||||
```bash
|
> ⚠️ **不要用 yt-dlp 读 B站**(风控已全面 412 拦截,实测无解)。用 bili-cli / OpenCLI。
|
||||||
# 获取视频元数据
|
|
||||||
yt-dlp --dump-json "https://www.bilibili.com/video/BVxxx"
|
|
||||||
|
|
||||||
# 下载字幕
|
```bash
|
||||||
yt-dlp --write-sub --write-auto-sub --sub-lang "zh-Hans,zh,en" --convert-subs vtt --skip-download -o "/tmp/%(id)s" "URL"
|
# 搜索 / 热门 / 视频详情(bili-cli,只读无需登录)
|
||||||
|
bili search "query" --type video -n 5
|
||||||
|
bili hot -n 10
|
||||||
|
bili video BVxxx
|
||||||
|
|
||||||
|
# 字幕(OpenCLI,需桌面 Chrome)
|
||||||
|
opencli bilibili subtitle BVxxx
|
||||||
```
|
```
|
||||||
|
|
||||||
> **注意**: 服务器 IP 可能遇到 412 错误。使用 `--cookies-from-browser chrome` 或配置代理。
|
> 详细命令(音频转写、API 直连兜底)见 [references/video.md](video.md)。
|
||||||
|
|
||||||
## V2EX (公开 API)
|
## V2EX (公开 API)
|
||||||
|
|
||||||
|
|||||||
@@ -50,35 +50,48 @@ agent-reach transcribe ./local_audio.mp3 -o /tmp/transcript.txt
|
|||||||
> 需要先配置 key:`agent-reach configure groq-key gsk_xxx`(免费,console.groq.com)
|
> 需要先配置 key:`agent-reach configure groq-key gsk_xxx`(免费,console.groq.com)
|
||||||
> 或 `agent-reach configure openai-key sk-xxx`。默认 auto 模式:groq 失败自动降级 openai。
|
> 或 `agent-reach configure openai-key sk-xxx`。默认 auto 模式:groq 失败自动降级 openai。
|
||||||
|
|
||||||
## B站 / Bilibili (yt-dlp + bili-cli)
|
## B站 / Bilibili(bili-cli 为主,OpenCLI 补字幕)
|
||||||
|
|
||||||
### 视频元数据 (yt-dlp)
|
> ⚠️ **不要用 yt-dlp 读 B站**:B站风控已全面 412 拦截 yt-dlp(实测最新版、直连/代理/带 Cookie 全部无效)。yt-dlp 只用于 YouTube。
|
||||||
|
|
||||||
|
### 视频详情/搜索/热门/排行 (bili-cli,只读无需登录)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yt-dlp --dump-json "https://www.bilibili.com/video/BVxxx"
|
# 视频详情(标题/UP主/时长/播放互动数据/字幕可用性)
|
||||||
```
|
bili video BVxxx
|
||||||
|
|
||||||
### 字幕 (yt-dlp)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yt-dlp --write-sub --write-auto-sub --sub-lang "zh-Hans,zh,en" --convert-subs vtt --skip-download -o "/tmp/%(id)s" "URL"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 搜索/热门/排行 (bili-cli)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 搜索视频
|
# 搜索视频
|
||||||
bili search "query" --type video -n 5
|
bili search "query" --type video -n 5
|
||||||
|
|
||||||
# 热门视频
|
# 热门视频 / 排行榜
|
||||||
bili hot -n 10
|
bili hot -n 10
|
||||||
|
|
||||||
# 排行榜
|
|
||||||
bili rank -n 10
|
bili rank -n 10
|
||||||
|
|
||||||
|
# 下载音频并切分为 ASR-ready WAV(无字幕时配合 agent-reach transcribe 转写)
|
||||||
|
bili audio BVxxx
|
||||||
```
|
```
|
||||||
|
|
||||||
> **412 风控**: 海外 IP 必须提供 Cookie(`--cookies-from-browser chrome` 或 `--cookies /path/to/cookies.txt`),国内 IP 一般不受影响。
|
### 字幕 (OpenCLI,需要桌面 Chrome)
|
||||||
> **安装 bili-cli**: `pipx install bilibili-cli`,然后 `bili login` 扫码登录。
|
|
||||||
|
```bash
|
||||||
|
# 字幕逐句带时间轴
|
||||||
|
opencli bilibili subtitle BVxxx
|
||||||
|
|
||||||
|
# OpenCLI 也能搜索/读视频元数据(备选)
|
||||||
|
opencli bilibili search "query" -f yaml
|
||||||
|
opencli bilibili video BVxxx -f yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### 零配置兜底:搜索 API 直连
|
||||||
|
|
||||||
|
```bash
|
||||||
|
UA="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
|
||||||
|
curl -s -c /tmp/bili_ck.txt -o /dev/null -A "$UA" "https://www.bilibili.com/"
|
||||||
|
curl -s -b /tmp/bili_ck.txt -A "$UA" -e "https://www.bilibili.com/" \
|
||||||
|
"https://api.bilibili.com/x/web-interface/search/all/v2?keyword=QUERY&page=1"
|
||||||
|
```
|
||||||
|
|
||||||
|
> **安装 bili-cli**: `pipx install bilibili-cli`(上游 2026-03 起停更但实测健康;只读场景无需登录,`bili login` 扫码可解锁动态/收藏等个人功能)。
|
||||||
|
|
||||||
## 小宇宙播客 / Xiaoyuzhou Podcast
|
## 小宇宙播客 / Xiaoyuzhou Podcast
|
||||||
|
|
||||||
@@ -111,6 +124,7 @@ agent-reach doctor
|
|||||||
| 场景 | 推荐工具 |
|
| 场景 | 推荐工具 |
|
||||||
|-----|---------|
|
|-----|---------|
|
||||||
| YouTube 字幕 | yt-dlp |
|
| YouTube 字幕 | yt-dlp |
|
||||||
| B站字幕 | yt-dlp |
|
| B站视频详情/搜索 | bili-cli |
|
||||||
|
| B站字幕 | opencli bilibili subtitle |
|
||||||
| 播客转录 | 小宇宙 transcribe.sh |
|
| 播客转录 | 小宇宙 transcribe.sh |
|
||||||
| 无字幕音视频 | agent-reach transcribe |
|
| 无字幕音视频 | agent-reach transcribe(B站音频先 `bili audio`) |
|
||||||
|
|||||||
+76
-37
@@ -946,9 +946,60 @@ class TestXiaoHongShuChannel:
|
|||||||
|
|
||||||
|
|
||||||
class TestBilibiliChannel:
|
class TestBilibiliChannel:
|
||||||
def test_reports_error_with_reinstall_hint_when_ytdlp_broken(self, monkeypatch):
|
"""多后端:bili-cli > OpenCLI > 搜索 API。yt-dlp 已退出 B站(412 实锤)。"""
|
||||||
"""yt-dlp which 命中但 exec 失败(venv 断链)→ error + 重装处方。"""
|
|
||||||
monkeypatch.setattr(shutil, "which", lambda _: "/usr/local/bin/yt-dlp")
|
@staticmethod
|
||||||
|
def _isolate(monkeypatch, opencli=None, api_ok=False):
|
||||||
|
import agent_reach.channels.bilibili as bilibili_mod
|
||||||
|
monkeypatch.setattr(
|
||||||
|
bilibili_mod.BilibiliChannel, "_check_opencli", lambda self: opencli
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(bilibili_mod, "_search_api_ok", lambda: api_ok)
|
||||||
|
|
||||||
|
def test_bili_cli_ok_is_active_backend(self, monkeypatch):
|
||||||
|
self._isolate(monkeypatch)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
shutil, "which",
|
||||||
|
lambda cmd: "/usr/local/bin/bili" if cmd == "bili" else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def fake_run(cmd, **kwargs):
|
||||||
|
return subprocess.CompletedProcess(cmd, 0, "bili, version 0.6.2", "")
|
||||||
|
|
||||||
|
monkeypatch.setattr(subprocess, "run", fake_run)
|
||||||
|
from agent_reach.channels.bilibili import BilibiliChannel
|
||||||
|
ch = BilibiliChannel()
|
||||||
|
status, msg = ch.check()
|
||||||
|
assert status == "ok"
|
||||||
|
assert "bili-cli 可用" in msg
|
||||||
|
assert ch.active_backend == "bili-cli"
|
||||||
|
|
||||||
|
def test_bili_broken_falls_back_to_api_with_hint_kept(self, monkeypatch):
|
||||||
|
"""bili 断链 → API 兜底获胜,但重装处方必须保留在消息里。"""
|
||||||
|
self._isolate(monkeypatch, api_ok=True)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
shutil, "which",
|
||||||
|
lambda cmd: "/usr/local/bin/bili" if cmd == "bili" else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def fake_run(cmd, **kwargs):
|
||||||
|
raise FileNotFoundError(cmd[0])
|
||||||
|
|
||||||
|
monkeypatch.setattr(subprocess, "run", fake_run)
|
||||||
|
from agent_reach.channels.bilibili import BilibiliChannel
|
||||||
|
ch = BilibiliChannel()
|
||||||
|
status, msg = ch.check()
|
||||||
|
assert status == "ok" # 搜索 API 兜底
|
||||||
|
assert ch.active_backend == "B站搜索 API"
|
||||||
|
assert "备选后端异常" in msg
|
||||||
|
assert "pipx reinstall bilibili-cli" in msg
|
||||||
|
|
||||||
|
def test_bili_broken_and_no_fallback_reports_error(self, monkeypatch):
|
||||||
|
self._isolate(monkeypatch, api_ok=False)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
shutil, "which",
|
||||||
|
lambda cmd: "/usr/local/bin/bili" if cmd == "bili" else None,
|
||||||
|
)
|
||||||
|
|
||||||
def fake_run(cmd, **kwargs):
|
def fake_run(cmd, **kwargs):
|
||||||
raise FileNotFoundError(cmd[0])
|
raise FileNotFoundError(cmd[0])
|
||||||
@@ -958,48 +1009,36 @@ class TestBilibiliChannel:
|
|||||||
ch = BilibiliChannel()
|
ch = BilibiliChannel()
|
||||||
status, msg = ch.check()
|
status, msg = ch.check()
|
||||||
assert status == "error"
|
assert status == "error"
|
||||||
assert "无法执行" in msg
|
assert "uv tool install --force bilibili-cli" in msg
|
||||||
assert "uv tool install --force yt-dlp" in msg
|
|
||||||
assert "pipx reinstall yt-dlp" in msg
|
|
||||||
assert ch.active_backend is None
|
assert ch.active_backend is None
|
||||||
|
|
||||||
def test_active_backend_set_when_ytdlp_and_bili_ok(self, monkeypatch):
|
def test_opencli_serves_when_bili_missing(self, monkeypatch):
|
||||||
monkeypatch.setattr(
|
self._isolate(monkeypatch, opencli=("ok", "OpenCLI 可用(字幕)"), api_ok=True)
|
||||||
shutil, "which",
|
monkeypatch.setattr(shutil, "which", lambda _: None)
|
||||||
lambda cmd: f"/usr/local/bin/{cmd}" if cmd in ("yt-dlp", "bili") else None,
|
|
||||||
)
|
|
||||||
|
|
||||||
def fake_run(cmd, **kwargs):
|
|
||||||
return subprocess.CompletedProcess(cmd, 0, "2026.06.09", "")
|
|
||||||
|
|
||||||
monkeypatch.setattr(subprocess, "run", fake_run)
|
|
||||||
from agent_reach.channels.bilibili import BilibiliChannel
|
from agent_reach.channels.bilibili import BilibiliChannel
|
||||||
ch = BilibiliChannel()
|
ch = BilibiliChannel()
|
||||||
status, msg = ch.check()
|
status, msg = ch.check()
|
||||||
assert status == "ok"
|
assert status == "ok"
|
||||||
assert "bili-cli 可用" in msg
|
assert ch.active_backend == "OpenCLI"
|
||||||
assert ch.active_backend == "yt-dlp"
|
|
||||||
|
|
||||||
def test_bili_broken_does_not_count_as_available(self, monkeypatch):
|
def test_api_only_still_ok_with_install_nudge(self, monkeypatch):
|
||||||
"""bili-cli 断链时不计为可用,降级走搜索 API;yt-dlp 仍是 active_backend。"""
|
self._isolate(monkeypatch, api_ok=True)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(shutil, "which", lambda _: None)
|
||||||
shutil, "which",
|
from agent_reach.channels.bilibili import BilibiliChannel
|
||||||
lambda cmd: f"/usr/local/bin/{cmd}" if cmd in ("yt-dlp", "bili") else None,
|
ch = BilibiliChannel()
|
||||||
)
|
|
||||||
|
|
||||||
def fake_run(cmd, **kwargs):
|
|
||||||
if "yt-dlp" in cmd[0]:
|
|
||||||
return subprocess.CompletedProcess(cmd, 0, "2026.06.09", "")
|
|
||||||
raise FileNotFoundError(cmd[0])
|
|
||||||
|
|
||||||
monkeypatch.setattr(subprocess, "run", fake_run)
|
|
||||||
import agent_reach.channels.bilibili as bilibili_mod
|
|
||||||
monkeypatch.setattr(bilibili_mod, "_search_api_ok", lambda: True)
|
|
||||||
ch = bilibili_mod.BilibiliChannel()
|
|
||||||
status, msg = ch.check()
|
status, msg = ch.check()
|
||||||
assert status == "ok" # 搜索 API 兜底
|
assert status == "ok"
|
||||||
assert "不计为可用" in msg
|
assert ch.active_backend == "B站搜索 API"
|
||||||
assert ch.active_backend == "yt-dlp"
|
assert "bilibili-cli" in msg
|
||||||
|
|
||||||
|
def test_off_when_everything_unreachable(self, monkeypatch):
|
||||||
|
self._isolate(monkeypatch, api_ok=False)
|
||||||
|
monkeypatch.setattr(shutil, "which", lambda _: None)
|
||||||
|
from agent_reach.channels.bilibili import BilibiliChannel
|
||||||
|
ch = BilibiliChannel()
|
||||||
|
status, msg = ch.check()
|
||||||
|
assert status == "off"
|
||||||
|
assert ch.active_backend is None
|
||||||
|
|
||||||
|
|
||||||
class TestYouTubeChannel:
|
class TestYouTubeChannel:
|
||||||
|
|||||||
Reference in New Issue
Block a user