From 7942f632e51d54e31e6b3d6aeeb2872b244de73f Mon Sep 17 00:00:00 2001 From: Peter Xue <203685922+Citrus086@users.noreply.github.com> Date: Sun, 8 Mar 2026 22:25:50 +0800 Subject: [PATCH] fix: make doctor checks resilient to slow mcporter calls (#103) --- agent_reach/channels/bosszhipin.py | 38 ++++++++--- agent_reach/channels/xiaohongshu.py | 28 +++++--- tests/test_channels.py | 45 ++++++++++++- tests/test_doctor.py | 101 ++++++++++++++++++++++------ 4 files changed, 173 insertions(+), 39 deletions(-) diff --git a/agent_reach/channels/bosszhipin.py b/agent_reach/channels/bosszhipin.py index 0e7a2de..20d7df4 100644 --- a/agent_reach/channels/bosszhipin.py +++ b/agent_reach/channels/bosszhipin.py @@ -29,15 +29,35 @@ class BossZhipinChannel(Channel): ) try: r = subprocess.run( - [mcporter, "list"], capture_output=True, - encoding="utf-8", errors="replace", timeout=10 + [mcporter, "config", "get", "bosszhipin", "--json"], + capture_output=True, + encoding="utf-8", + errors="replace", + timeout=5, + ) + if r.returncode != 0 or "bosszhipin" not in r.stdout.lower(): + return "off", ( + "mcporter 已装但 Boss直聘 MCP 未配置。\n" + " 详见 https://github.com/mucsbr/mcp-bosszp" + ) + except Exception: + return "off", "mcporter 连接异常" + + try: + r = subprocess.run( + [mcporter, "call", "bosszhipin.get_login_info_tool", "--output", "json"], + capture_output=True, + encoding="utf-8", + errors="replace", + timeout=10, ) out = r.stdout.lower() - if "boss" in out or "zhipin" in out: - return "ok", "可搜索职位、向 HR 打招呼" + if r.returncode == 0 and "\"is_logged_in\": true" in out: + return "ok", "完整可用(职位搜索、登录态检查、向 HR 打招呼)" + if r.returncode == 0: + return "ok", "MCP 已连接,可搜索职位;打招呼前可能需要先登录" + return "warn", "MCP 已配置,但连接异常;请检查 mcp-bosszp 服务状态" + except subprocess.TimeoutExpired: + return "warn", "MCP 已配置,但健康检查超时;请检查 mcp-bosszp 服务状态" except Exception: - pass - return "off", ( - "mcporter 已装但 Boss直聘 MCP 未配置。\n" - " 详见 https://github.com/mucsbr/mcp-bosszp" - ) + return "warn", "MCP 已配置,但连接异常;请检查 mcp-bosszp 服务状态" diff --git a/agent_reach/channels/xiaohongshu.py b/agent_reach/channels/xiaohongshu.py index 57393a1..1527146 100644 --- a/agent_reach/channels/xiaohongshu.py +++ b/agent_reach/channels/xiaohongshu.py @@ -51,10 +51,13 @@ class XiaoHongShuChannel(Channel): ) try: r = subprocess.run( - [mcporter, "config", "list"], capture_output=True, - encoding="utf-8", errors="replace", timeout=5 + [mcporter, "config", "get", "xiaohongshu", "--json"], + capture_output=True, + encoding="utf-8", + errors="replace", + timeout=5, ) - if "xiaohongshu" not in r.stdout: + if r.returncode != 0 or "xiaohongshu" not in r.stdout.lower(): return "off", ( "mcporter 已装但小红书 MCP 未配置。运行:\n" + _docker_run_hint() + "\n" @@ -62,13 +65,20 @@ class XiaoHongShuChannel(Channel): ) except Exception: return "off", "mcporter 连接异常" + try: r = subprocess.run( - [mcporter, "call", "xiaohongshu.check_login_status()"], - capture_output=True, encoding="utf-8", errors="replace", timeout=10 + [mcporter, "list", "xiaohongshu", "--json"], + capture_output=True, + encoding="utf-8", + errors="replace", + timeout=10, ) - if "已登录" in r.stdout or "logged" in r.stdout.lower(): - return "ok", "完整可用(阅读、搜索、发帖、评论、点赞)" - return "warn", "MCP 已连接但未登录,需扫码登录" + out = r.stdout.lower() + if r.returncode == 0 and '"status": "ok"' in out: + return "ok", "MCP 已连接(阅读、搜索、发帖、评论、点赞)" + return "warn", "MCP 已配置,但连接异常;请检查 xiaohongshu-mcp 服务状态" + except subprocess.TimeoutExpired: + return "warn", "MCP 已配置,但健康检查超时;请检查 xiaohongshu-mcp 服务状态" except Exception: - return "warn", "MCP 连接异常,检查 xiaohongshu-mcp 服务是否在运行" + return "warn", "MCP 已配置,但连接异常;请检查 xiaohongshu-mcp 服务状态" diff --git a/tests/test_channels.py b/tests/test_channels.py index 92e0059..c9c8dd5 100644 --- a/tests/test_channels.py +++ b/tests/test_channels.py @@ -1,7 +1,12 @@ # -*- coding: utf-8 -*- -"""Tests for channel registry basics.""" +"""Tests for channel registry basics and health checks.""" + +import shutil +import subprocess from agent_reach.channels import get_all_channels, get_channel +from agent_reach.channels.bosszhipin import BossZhipinChannel +from agent_reach.channels.xiaohongshu import XiaoHongShuChannel class TestChannelRegistry: @@ -19,3 +24,41 @@ class TestChannelRegistry: assert "web" in names assert "github" in names assert "twitter" in names + + +class TestBossZhipinChannel: + def test_reports_ok_when_configured_and_logged_in(self, monkeypatch): + monkeypatch.setattr(shutil, "which", lambda _: "/opt/homebrew/bin/mcporter") + + def fake_run(cmd, **kwargs): + if cmd[:4] == ["/opt/homebrew/bin/mcporter", "config", "get", "bosszhipin"]: + return subprocess.CompletedProcess(cmd, 0, '{"name":"bosszhipin"}', "") + if cmd[:3] == ["/opt/homebrew/bin/mcporter", "call", "bosszhipin.get_login_info_tool"]: + return subprocess.CompletedProcess(cmd, 0, '{"is_logged_in": true}', "") + raise AssertionError(f"unexpected command: {cmd}") + + monkeypatch.setattr(subprocess, "run", fake_run) + + assert BossZhipinChannel().check() == ( + "ok", + "完整可用(职位搜索、登录态检查、向 HR 打招呼)", + ) + + +class TestXiaoHongShuChannel: + def test_reports_ok_when_server_health_is_ok(self, monkeypatch): + monkeypatch.setattr(shutil, "which", lambda _: "/opt/homebrew/bin/mcporter") + + def fake_run(cmd, **kwargs): + if cmd[:4] == ["/opt/homebrew/bin/mcporter", "config", "get", "xiaohongshu"]: + return subprocess.CompletedProcess(cmd, 0, '{"name":"xiaohongshu"}', "") + if cmd[:4] == ["/opt/homebrew/bin/mcporter", "list", "xiaohongshu", "--json"]: + return subprocess.CompletedProcess(cmd, 0, '{"status": "ok"}', "") + raise AssertionError(f"unexpected command: {cmd}") + + monkeypatch.setattr(subprocess, "run", fake_run) + + assert XiaoHongShuChannel().check() == ( + "ok", + "MCP 已连接(阅读、搜索、发帖、评论、点赞)", + ) diff --git a/tests/test_doctor.py b/tests/test_doctor.py index 8f85c30..14e8339 100644 --- a/tests/test_doctor.py +++ b/tests/test_doctor.py @@ -2,8 +2,22 @@ """Tests for doctor module.""" import pytest + +import agent_reach.doctor as doctor from agent_reach.config import Config -from agent_reach.doctor import check_all, format_report + + +class _StubChannel: + def __init__(self, name, description, tier, status, message, backends=None): + self.name = name + self.description = description + self.tier = tier + self._status = status + self._message = message + self.backends = backends or [] + + def check(self, config=None): + return self._status, self._message @pytest.fixture @@ -12,26 +26,73 @@ def tmp_config(tmp_path): class TestDoctor: - def test_zero_config_channels_ok(self, tmp_config): - results = check_all(tmp_config) - assert results["web"]["status"] == "ok" - assert results["github"]["status"] in ("ok", "warn") # warn if gh CLI not installed - assert results["bilibili"]["status"] in ("ok", "warn") # warn on servers - assert results["rss"]["status"] == "ok" + def test_check_all_collects_channel_results(self, tmp_config, monkeypatch): + monkeypatch.setattr( + doctor, + "get_all_channels", + lambda: [ + _StubChannel("web", "网页", 0, "ok", "可抓取网页", ["requests"]), + _StubChannel("github", "GitHub", 0, "warn", "gh 未安装", ["gh"]), + _StubChannel("exa_search", "全网语义搜索", 1, "off", "mcporter 未配置", ["Exa"]), + ], + ) - def test_exa_off_without_key(self, tmp_config): - results = check_all(tmp_config) - assert results["exa_search"]["status"] == "off" + results = doctor.check_all(tmp_config) - def test_exa_key_does_not_force_enabled(self, tmp_config): - # Exa availability is determined by mcporter runtime/config state. - tmp_config.set("exa_api_key", "test-key") - results = check_all(tmp_config) - assert results["exa_search"]["status"] in ("off", "ok") + assert results == { + "web": { + "status": "ok", + "name": "网页", + "message": "可抓取网页", + "tier": 0, + "backends": ["requests"], + }, + "github": { + "status": "warn", + "name": "GitHub", + "message": "gh 未安装", + "tier": 0, + "backends": ["gh"], + }, + "exa_search": { + "status": "off", + "name": "全网语义搜索", + "message": "mcporter 未配置", + "tier": 1, + "backends": ["Exa"], + }, + } + + def test_format_report(self): + report = doctor.format_report( + { + "web": { + "status": "ok", + "name": "网页", + "message": "可抓取网页", + "tier": 0, + "backends": ["requests"], + }, + "exa_search": { + "status": "off", + "name": "全网语义搜索", + "message": "mcporter 未配置", + "tier": 1, + "backends": ["Exa"], + }, + "xiaohongshu": { + "status": "warn", + "name": "小红书", + "message": "MCP 已配置,但健康检查超时", + "tier": 2, + "backends": ["mcporter"], + }, + } + ) - def test_format_report(self, tmp_config): - results = check_all(tmp_config) - report = format_report(results) assert "Agent Reach" in report - assert "✅" in report - assert "渠道可用" in report + assert "✅ 装好即用:" in report + assert "🔍 搜索(mcporter 即可解锁):" in report + assert "🔧 配置后可用:" in report + assert "状态:1/3 个渠道可用" in report + assert "运行 `agent-reach setup` 解锁更多渠道" in report