ForcePilot/backend/package/yuxi/channels/adapters/matrix/doctor.py
Kris 2063145ce1 feat(matrix): 新增完整的Matrix适配器工具集
新增了20+个Matrix相关工具模块,包含线程处理、自动加入、消息去重、配置管理、安全策略、媒体处理、命令解析等功能,完善Matrix适配器基础能力
2026-05-12 00:45:54 +08:00

201 lines
7.1 KiB
Python

from __future__ import annotations
import os
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from nio import AsyncClient
from .config_schema import validate_config
from .encryption import check_crypto_store_ready
from .sync_store import SyncTokenStore, SSRFGuard
class MatrixDoctor:
def __init__(self, config: dict[str, Any], client: AsyncClient | None = None):
self._config = config
self._client = client
self._issues: list[dict[str, Any]] = []
async def diagnose(self) -> list[dict[str, Any]]:
self._issues = []
self._check_config_validity()
self._check_auth_method()
self._check_homeserver_reachability()
self._check_crypto_store()
self._check_sync_token()
self._check_private_network()
self._check_encryption_config()
return self._issues
def _check_config_validity(self) -> None:
errors = validate_config(self._config)
for err in errors:
self._issues.append(
{
"severity": "error",
"category": "config",
"field": err["field"],
"message": err["error"],
}
)
def _check_auth_method(self) -> None:
has_token = bool(self._config.get("access_token"))
has_password = bool(self._config.get("password"))
if not has_token and not has_password:
self._issues.append(
{
"severity": "error",
"category": "auth",
"message": "No authentication method configured (access_token or password required)",
}
)
def _check_homeserver_reachability(self) -> None:
homeserver = self._config.get("homeserver", "")
if not homeserver:
self._issues.append(
{
"severity": "warning",
"category": "connection",
"message": "Homeserver URL not configured",
}
)
def _check_crypto_store(self) -> None:
crypto_dir = self._config.get("crypto_store_dir", "")
if not crypto_dir and self._config.get("encryption"):
self._issues.append(
{
"severity": "error",
"category": "encryption",
"message": "Encryption enabled but crypto_store_dir not configured",
}
)
return
if crypto_dir:
result = check_crypto_store_ready(crypto_dir)
if result["status"] == "unavailable":
self._issues.append(
{
"severity": "error",
"category": "encryption",
"message": f"Crypto store unavailable: {', '.join(result.get('details', []))}",
}
)
elif result["status"] == "empty" and self._config.get("encryption"):
self._issues.append(
{
"severity": "info",
"category": "encryption",
"message": "Crypto store empty — keys will be created on first encrypted message",
}
)
def _check_sync_token(self) -> None:
crypto_dir = self._config.get("crypto_store_dir", "./matrix_crypto_store")
user_id = self._config.get("user_id", "unknown")
store = SyncTokenStore(crypto_dir, user_id)
token = store.load()
if token:
self._issues.append(
{
"severity": "info",
"category": "sync",
"message": "Sync token found — incremental sync will be used",
}
)
def _check_private_network(self) -> None:
homeserver = self._config.get("homeserver", "")
if homeserver:
from urllib.parse import urlparse
parsed = urlparse(homeserver)
hostname = parsed.hostname or ""
if hostname and SSRFGuard.is_private_host(hostname):
allowed = self._config.get("dangerouslyAllowPrivateNetwork", False)
if not allowed:
self._issues.append(
{
"severity": "warning",
"category": "security",
"message": f"Homeserver {homeserver} appears to be on a private network. Set dangerouslyAllowPrivateNetwork=true to allow.",
}
)
def _check_encryption_config(self) -> None:
encryption_enabled = self._config.get("encryption", False)
if encryption_enabled:
if not self._config.get("crypto_store_dir"):
self._issues.append(
{
"severity": "error",
"category": "encryption",
"message": "Encryption requires crypto_store_dir to be configured",
}
)
async def repair(self) -> list[dict[str, Any]]:
repairs: list[dict[str, Any]] = []
crypto_dir = self._config.get("crypto_store_dir", "")
if crypto_dir and not os.path.exists(crypto_dir):
try:
os.makedirs(crypto_dir, exist_ok=True)
repairs.append(
{
"status": "repaired",
"category": "encryption",
"message": f"Created crypto store directory: {crypto_dir}",
}
)
except OSError as e:
repairs.append(
{
"status": "failed",
"category": "encryption",
"message": f"Failed to create crypto store directory: {e}",
}
)
return repairs
def _check_homeserver_ssrf(self) -> None:
from urllib.parse import urlparse
homeserver = self._config.get("homeserver", "")
if not homeserver:
return
parsed = urlparse(homeserver)
hostname = parsed.hostname or ""
if hostname:
is_private = SSRFGuard.is_private_host(hostname)
allowed = self._config.get("dangerouslyAllowPrivateNetwork", False)
if is_private and not allowed:
self._issues.append(
{
"severity": "warning",
"category": "security",
"message": f"Homeserver {homeserver} is on a private network. Set dangerouslyAllowPrivateNetwork=true to connect.",
}
)
def get_summary(self) -> dict[str, int]:
summary = {"error": 0, "warning": 0, "info": 0}
for issue in self._issues:
severity = issue.get("severity", "info")
if severity in summary:
summary[severity] += 1
return summary
@property
def has_critical_issues(self) -> bool:
return any(issue.get("severity") == "error" for issue in self._issues)