ForcePilot/backend/package/yuxi/channels/adapters/nextcloudtalk/doctor.py
Kris b018ad25da feat(backend): add Nextcloud Talk channel adapter implementation
实现了完整的Nextcloud Talk聊天适配器,包含以下核心功能:
1. 基础客户端通信与认证
2. 会话路由与聊天类型解析
3. 消息分块与格式转换
4. 用户配对与权限校验
5. 轮询式消息监听
6. 配置验证与诊断工具
7. 安全策略与去重缓存
8. 指标统计与状态快照

新增配套的配对用户缓存文件与模块导出入口。
2026-05-12 00:47:06 +08:00

188 lines
6.7 KiB
Python

from __future__ import annotations
import logging
import os
from typing import Any
from .probe import probe_capabilities, resolve_api_version
from .security import collect_security_warnings
logger = logging.getLogger(__name__)
async def run_diagnostics(
client,
config: dict[str, Any],
adapter=None,
) -> list[dict[str, Any]]:
results: list[dict[str, Any]] = []
results.append(_check_credentials(config))
results.append(_check_server_url(config))
results.append(_check_server_url_ssl(config))
results.append(_check_environment_variables())
if client and client._session:
results.append(await _check_api_connectivity(client, config))
results.extend(_check_security_policies(config))
results.append(_check_room_configs(config))
results.append(_check_dm_configs(config))
results.append(_check_multi_account(config))
results.append(_check_webhook_config(config))
if adapter:
results.append(_check_runtime_status(adapter))
return results
def _check_credentials(config: dict[str, Any]) -> dict[str, Any]:
bot_user = config.get("bot_user", config.get("botUser", ""))
app_password = config.get("app_password", config.get("appPassword", ""))
if not bot_user or not app_password:
return {"check": "credentials", "status": "fail", "detail": "bot_user or app_password is missing"}
return {"check": "credentials", "status": "pass", "detail": f"Bot user: {bot_user}"}
def _check_server_url(config: dict[str, Any]) -> dict[str, Any]:
server_url = config.get("server_url", "").rstrip("/")
if not server_url:
return {"check": "server_url", "status": "fail", "detail": "server_url is missing"}
if not (server_url.startswith("https://") or server_url.startswith("http://")):
return {"check": "server_url", "status": "fail", "detail": f"Invalid URL format: {server_url}"}
return {"check": "server_url", "status": "pass", "detail": server_url}
async def _check_api_connectivity(client, config: dict[str, Any]) -> dict[str, Any]:
try:
server_url = client.server_url
caps = await probe_capabilities(client.session, server_url)
if caps:
version = caps.get("version", "unknown")
api_version = resolve_api_version(caps)
return {
"check": "api_connectivity",
"status": "pass",
"detail": f"Talk version: {version}, API: {api_version}",
}
return {"check": "api_connectivity", "status": "fail", "detail": "No Talk capabilities found"}
except Exception as e:
return {"check": "api_connectivity", "status": "fail", "detail": str(e)}
def _check_security_policies(config: dict[str, Any]) -> list[dict[str, Any]]:
warnings = collect_security_warnings(config)
if warnings:
return [
{
"check": "security_policies",
"status": "warn",
"detail": f"{len(warnings)} warning(s)",
"warnings": warnings,
}
]
return [{"check": "security_policies", "status": "pass", "detail": "No warnings"}]
def _check_room_configs(config: dict[str, Any]) -> dict[str, Any]:
rooms = config.get("rooms", {})
if not rooms:
return {"check": "room_configs", "status": "info", "detail": "No per-room configuration"}
disabled_rooms = [t for t, c in rooms.items() if not c.get("enabled", c.get("enabled_", True))]
return {
"check": "room_configs",
"status": "info",
"detail": f"{len(rooms)} room(s) configured, {len(disabled_rooms)} disabled",
}
def _check_dm_configs(config: dict[str, Any]) -> dict[str, Any]:
dms = config.get("dms", {})
if not dms:
return {"check": "dm_configs", "status": "info", "detail": "No per-DM configuration"}
disabled_dms = [u for u, c in dms.items() if not c.get("enabled", True)]
return {
"check": "dm_configs",
"status": "info",
"detail": f"{len(dms)} DM config(s), {len(disabled_dms)} disabled",
}
def _check_server_url_ssl(config: dict[str, Any]) -> dict[str, Any]:
server_url = config.get("server_url", "").rstrip("/")
if server_url.startswith("https://"):
return {"check": "server_url_ssl", "status": "pass", "detail": "HTTPS enabled"}
if server_url.startswith("http://"):
return {"check": "server_url_ssl", "status": "warn", "detail": "HTTP (non-SSL) in use"}
return {"check": "server_url_ssl", "status": "info", "detail": "No server URL configured"}
def _check_environment_variables() -> dict[str, Any]:
found: list[str] = []
missing: list[str] = []
for var in (
"NEXTCLOUD_TALK_BOT_USER",
"NEXTCLOUD_TALK_BOT_SECRET",
"NEXTCLOUD_TALK_APP_PASSWORD",
"NEXTCLOUD_TALK_API_PASSWORD",
"OPENCLAW_DEBUG_NEXTCLOUD_TALK_ACCOUNTS",
):
if os.getenv(var):
found.append(var)
else:
missing.append(var)
return {
"check": "environment_variables",
"status": "info",
"detail": f"{len(found)} set, {len(missing)} not set",
"set": found,
"not_set": missing,
}
def _check_multi_account(config: dict[str, Any]) -> dict[str, Any]:
accounts = config.get("accounts", [])
if isinstance(accounts, list) and accounts:
return {
"check": "multi_account",
"status": "info",
"detail": f"{len(accounts)} accounts configured",
"accounts": [
{"id": a.get("id", "?"), "label": a.get("label", "?")}
for a in accounts if isinstance(a, dict)
],
}
return {"check": "multi_account", "status": "info", "detail": "Single account mode (default)"}
def _check_webhook_config(config: dict[str, Any]) -> dict[str, Any]:
webhook = config.get("webhook", {})
if not webhook:
return {"check": "webhook", "status": "info", "detail": "Webhook server not configured"}
enabled = webhook.get("enabled", False)
if enabled:
return {
"check": "webhook",
"status": "info",
"detail": f"Enabled (port={webhook.get('port', '?')}, path={webhook.get('path', '?')})",
}
return {"check": "webhook", "status": "info", "detail": "Webhook server disabled"}
def _check_runtime_status(adapter) -> dict[str, Any]:
try:
stats = adapter.get_activity_stats()
metrics = adapter._metrics.snapshot() if hasattr(adapter, "_metrics") else {}
return {
"check": "runtime_status",
"status": "pass",
"detail": f"Connected: {adapter.status == 'connected'}",
"activity": stats,
"metrics": metrics,
}
except Exception as e:
return {"check": "runtime_status", "status": "warn", "detail": str(e)}