Files
agent-desktop/src/pineagents/cli/doctor_connectivity.py
T
Pine 96659759b0 feat: 智能体后端运行时(src/pineagents)
- Agent OS:agents 决策循环、runtime 编排、providers、governance/security/sandbox
- app 服务层(认证转发到 server-core)、channels 19 路频道、memory、plugins
2026-08-23 22:41:48 +08:00

349 lines
9.5 KiB
Python

# -*- coding: utf-8 -*-
"""Optional ``--deep`` network reachability checks for enabled channels.
Failures are reported as non-fatal notes (firewalls and offline use are
common). Built-in probes live here; custom channels can override
:class:`~pineagents.app.channels.base.BaseChannel`.doctor_connectivity_notes.
"""
from __future__ import annotations
import socket
from typing import Any, Callable
from urllib.parse import urlparse
import httpx
from ..app.channels.registry import get_channel_registry
from ..config.config import (
ChannelConfig,
Config,
DingTalkConfig,
FeishuConfig,
MatrixConfig,
MattermostConfig,
MQTTConfig,
OneBotConfig,
QQConfig,
TelegramConfig,
VoiceChannelConfig,
WecomConfig,
XiaoYiConfig,
WeChatConfig,
)
from .doctor_checks import _effective_channels_mcp, _read_workspace_agent_json
ChannelProbe = Callable[[str, Any, float], list[str]]
def _tcp_check(host: str, port: int, timeout: float) -> str | None:
try:
with socket.create_connection((host, port), timeout=timeout):
pass
except OSError as exc:
return str(exc)
return None
def _http_get_ok(url: str, timeout: float) -> str | None:
try:
resp = httpx.get(url, timeout=timeout, follow_redirects=True)
if resp.status_code >= 400:
return f"HTTP {resp.status_code}"
except httpx.RequestError as exc:
return str(exc)
return None
def _probe_mqtt(agent_id: str, cfg: MQTTConfig, timeout: float) -> list[str]:
host = (cfg.host or "").strip()
if not host:
return []
port = int(cfg.port or 1883)
err = _tcp_check(host, port, timeout)
if err:
return [f"{agent_id}: mqtt: TCP {host}:{port}{err}"]
return []
def _probe_mattermost(
agent_id: str,
cfg: MattermostConfig,
timeout: float,
) -> list[str]:
url = (cfg.url or "").strip().rstrip("/")
if not url:
return []
ping = f"{url}/api/v4/system/ping"
err = _http_get_ok(ping, timeout)
if err:
return [f"{agent_id}: mattermost: GET {ping!r}{err}"]
return []
def _probe_matrix(
agent_id: str,
cfg: MatrixConfig,
timeout: float,
) -> list[str]:
hs = (cfg.homeserver or "").strip().rstrip("/")
if not hs:
return []
ver = f"{hs}/_matrix/client/versions"
err = _http_get_ok(ver, timeout)
if err:
return [f"{agent_id}: matrix: GET {ver!r}{err}"]
return []
def _probe_telegram(
agent_id: str,
cfg: TelegramConfig,
timeout: float,
) -> list[str]:
url = (cfg.base_url or "").strip().rstrip(
"/",
) or "https://api.telegram.org"
err = _http_get_ok(url, timeout)
if err:
return [f"{agent_id}: telegram: reach {url}{err}"]
return []
def _probe_discord(agent_id: str, _cfg: Any, timeout: float) -> list[str]:
err = _http_get_ok("https://discord.com/api/v10/gateway", timeout)
if err:
return [f"{agent_id}: discord: reach discord API — {err}"]
return []
def _probe_onebot(
agent_id: str,
cfg: OneBotConfig,
timeout: float,
) -> list[str]:
host = (cfg.ws_host or "127.0.0.1").strip()
port = int(cfg.ws_port or 6199)
if host in ("0.0.0.0", ""):
host = "127.0.0.1"
err = _tcp_check(host, port, timeout)
if err:
return [
f"{agent_id}: onebot: TCP {host}:{port}{err} "
"(is the reverse WebSocket server running?)",
]
return []
def _probe_feishu(
agent_id: str,
cfg: FeishuConfig,
timeout: float,
) -> list[str]:
base = (
"https://open.feishu.cn"
if getattr(cfg, "domain", "feishu") == "feishu"
else "https://open.larksuite.com"
)
err = _http_get_ok(base + "/", timeout)
if err:
return [f"{agent_id}: feishu: reach {base}{err}"]
return []
def _probe_dingtalk(
agent_id: str,
_cfg: DingTalkConfig,
timeout: float,
) -> list[str]:
err = _http_get_ok("https://oapi.dingtalk.com/", timeout)
if err:
return [f"{agent_id}: dingtalk: reach oapi.dingtalk.com — {err}"]
return []
def _probe_qq(agent_id: str, _cfg: QQConfig, timeout: float) -> list[str]:
err = _tcp_check("qq.com", 443, timeout)
if err:
return [f"{agent_id}: qq: TCP qq.com:443 — {err}"]
return []
def _probe_wecom(
agent_id: str,
_cfg: WecomConfig,
timeout: float,
) -> list[str]:
err = _tcp_check("qyapi.weixin.qq.com", 443, timeout)
if err:
return [f"{agent_id}: wecom: TCP qyapi.weixin.qq.com:443 — {err}"]
return []
def _probe_voice(
agent_id: str,
_cfg: VoiceChannelConfig,
timeout: float,
) -> list[str]:
err = _http_get_ok("https://api.twilio.com/", timeout)
if err:
return [f"{agent_id}: voice (Twilio): reach api.twilio.com — {err}"]
return []
def _probe_xiaoyi(
agent_id: str,
_cfg: XiaoYiConfig,
timeout: float,
) -> list[str]:
from pineagents.app.channels.xiaoyi.constants import (
DEFAULT_WS_URL,
DEFAULT_WS_URL_BACKUP,
)
notes: list[str] = []
for label, raw in (
("primary", DEFAULT_WS_URL),
("backup", DEFAULT_WS_URL_BACKUP),
):
if not raw:
continue
parsed = urlparse(raw)
host = parsed.hostname
if not host:
continue
port = parsed.port or (
443 if parsed.scheme in ("wss", "https") else 80
)
err = _tcp_check(host, port, timeout)
if err:
notes.append(
f"{agent_id}: xiaoyi: "
f"TCP {host}:{port} ({label}) "
f"\u2014 {err}",
)
return notes
def _probe_wechat(
agent_id: str,
cfg: WeChatConfig,
timeout: float,
) -> list[str]:
base = (cfg.base_url or "").strip().rstrip("/")
if base:
err = _http_get_ok(base + "/", timeout)
if err:
return [f"{agent_id}: wechat: GET {base!r}{err}"]
return []
err = _http_get_ok("https://api.weixin.qq.com/", timeout)
if err:
return [f"{agent_id}: wechat: reach api.weixin.qq.com — {err}"]
return []
_BUILTIN_PROBES: dict[str, ChannelProbe] = {
"mqtt": _probe_mqtt,
"mattermost": _probe_mattermost,
"matrix": _probe_matrix,
"telegram": _probe_telegram,
"discord": _probe_discord,
"onebot": _probe_onebot,
"feishu": _probe_feishu,
"dingtalk": _probe_dingtalk,
"qq": _probe_qq,
"wecom": _probe_wecom,
"voice": _probe_voice,
"xiaoyi": _probe_xiaoyi,
"wechat": _probe_wechat,
}
def _channel_enabled(sub: Any) -> bool:
return bool(getattr(sub, "enabled", False))
# pylint: disable-next=too-many-branches
def collect_deep_channel_connectivity_notes(
cfg: Config,
timeout: float,
) -> list[str]:
"""Run reachability probes for each enabled channel (per agent profile)."""
notes: list[str] = []
reg = get_channel_registry()
for agent_id, ref in cfg.agents.profiles.items():
raw = _read_workspace_agent_json(ref)
ch, _ = _effective_channels_mcp(cfg, raw)
for name in ChannelConfig.model_fields:
sub = getattr(ch, name, None)
if sub is None or not _channel_enabled(sub):
continue
if name == "console":
continue
if name == "imessage":
continue
cls = reg.get(name)
custom_lines: list[str] = []
if cls is not None:
try:
custom_lines = cls.doctor_connectivity_notes(
agent_id,
sub,
timeout=timeout,
)
except (
Exception
) as exc: # pylint: disable=broad-exception-caught
custom_lines = [
f"{agent_id}: {name}: "
f"doctor_connectivity_notes error: {exc}",
]
if custom_lines:
notes.extend(custom_lines)
continue
probe = _BUILTIN_PROBES.get(name)
if probe is None:
continue
try:
notes.extend(probe(agent_id, sub, timeout))
except Exception as exc: # pylint: disable=broad-exception-caught
notes.append(
f"{agent_id}: {name}: connectivity probe error: {exc}",
)
extra = getattr(ch, "__pydantic_extra__", None) or {}
for key, val in extra.items():
en = False
if isinstance(val, dict):
en = bool(val.get("enabled"))
elif hasattr(val, "enabled"):
en = bool(getattr(val, "enabled"))
if not en:
continue
cls = reg.get(key)
if cls is None:
notes.append(
f"{agent_id}: plugin channel {key!r} enabled — "
"not in registry; add a BaseChannel subclass "
"or skip --deep",
)
continue
try:
notes.extend(
cls.doctor_connectivity_notes(
agent_id,
val,
timeout=timeout,
),
)
except Exception as exc: # pylint: disable=broad-exception-caught
notes.append(
f"{agent_id}: {key}: doctor_connectivity_notes error: "
f"{exc}",
)
return notes