ForcePilot/backend/package/yuxi/channel/doctor/models.py
Kris 058513022a feat(channel/doctor): 新增渠道诊断修复工具模块
新增了完整的渠道诊断修复工具,包含数据模型定义与核心执行逻辑:
1. 定义了检查状态、严重性、修复模式等枚举类型
2. 实现了诊断选项、各类检查结果、修复步骤与结果等数据类
3. 提供了ChannelDoctorRunner核心类,支持单渠道/多渠道诊断、单步骤/全量修复
4. 实现了配置规范化、过期配置清理、权限/凭证/连通性检查等完整流程
5. 支持诊断结果序列化为字典格式,以及上下文管理器式的运行器管理
2026-05-21 10:25:13 +08:00

265 lines
7.4 KiB
Python

from __future__ import annotations
import hashlib
import time
from dataclasses import dataclass, field
from enum import StrEnum
class CheckStatus(StrEnum):
PASS = "pass"
WARN = "warn"
FAIL = "fail"
SKIP = "skip"
class Severity(StrEnum):
INFO = "info"
WARNING = "warning"
ERROR = "error"
class DoctorRepairMode(StrEnum):
NONE = "none"
YES = "yes"
REPAIR = "repair"
FORCE = "force"
NON_INTERACTIVE = "non_interactive"
DEEP = "deep"
@dataclass
class DoctorOptions:
repair_mode: DoctorRepairMode = DoctorRepairMode.NONE
deep: bool = False
generate_gateway_token: bool = False
workspace_suggestions: bool = True
@property
def should_repair(self) -> bool:
return self.repair_mode in (DoctorRepairMode.REPAIR, DoctorRepairMode.FORCE, DoctorRepairMode.YES)
@property
def should_force(self) -> bool:
return self.repair_mode == DoctorRepairMode.FORCE
@property
def is_non_interactive(self) -> bool:
return self.repair_mode in (
DoctorRepairMode.NON_INTERACTIVE,
DoctorRepairMode.REPAIR,
DoctorRepairMode.YES,
)
@dataclass
class CredentialCheckResult:
status: CheckStatus
token_obtained: bool
expires_in: int | None = None
error: str | None = None
detail: dict = field(default_factory=dict)
@dataclass
class PermissionCheckResult:
status: CheckStatus
can_send_message: bool = False
can_read_message: bool = False
can_manage_group: bool = False
missing_permissions: list[str] = field(default_factory=list)
error: str | None = None
@dataclass
class ConnectivityCheckResult:
status: CheckStatus
latency_ms: float | None = None
endpoint: str = ""
error: str | None = None
@dataclass
class DiagnosisWarning:
severity: Severity
code: str
message: str
suggestion: str
@dataclass
class LegacyConfigRule:
old_key: str
new_key: str
transform: str | None = None
@dataclass
class ConfigMutation:
description: str = ""
old_value: str = ""
new_value: str = ""
key: str = ""
version: int = 0
timestamp: float = field(default_factory=time.time)
operator: str = "system"
reason: str = ""
checksum: str = ""
def __post_init__(self):
if not self.checksum:
self.checksum = self._compute_checksum()
def _compute_checksum(self) -> str:
payload = f"{self.key}|{self.description}|{self.old_value}|{self.new_value}"
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
@dataclass
class ChannelStatusWarning:
channel_type: str
account_id: str
status: CheckStatus
message: str
detail: dict = field(default_factory=dict)
@dataclass
class RepairStep:
id: str
description: str
action: str
reversible: bool = True
params: dict = field(default_factory=dict)
requires_interactive: bool = False
is_aggressive: bool = False
@dataclass
class RepairResult:
step_id: str
success: bool
message: str
config_before: dict | None = None
config_after: dict | None = None
@dataclass
class DiagnosisResult:
channel_type: str
account_id: str
overall_status: CheckStatus
timestamp: float
credential: CredentialCheckResult | None = None
permission: PermissionCheckResult | None = None
connectivity: ConnectivityCheckResult | None = None
config_before: dict | None = None
config_after: dict | None = None
config_changes: list[ConfigMutation] = field(default_factory=list)
warnings: list[DiagnosisWarning] = field(default_factory=list)
security_warnings: list[str] = field(default_factory=list)
channel_status_warnings: list[ChannelStatusWarning] = field(default_factory=list)
repair_plan: list[RepairStep] = field(default_factory=list)
repair_results: list[RepairResult] = field(default_factory=list)
def to_dict(self) -> dict:
result: dict = {
"channel_type": self.channel_type,
"account_id": self.account_id,
"overall_status": self.overall_status.value,
"timestamp": self.timestamp,
"credential": self._credential_dict(),
"permission": self._permission_dict(),
"connectivity": self._connectivity_dict(),
"config_before": self.config_before,
"config_after": self.config_after,
"config_changes": [
{
"key": c.key,
"description": c.description,
"old_value": c.old_value,
"new_value": c.new_value,
"version": c.version,
"timestamp": c.timestamp,
"operator": c.operator,
"reason": c.reason,
"checksum": c.checksum,
}
for c in self.config_changes
],
"warnings": [
{"severity": w.severity.value, "code": w.code, "message": w.message, "suggestion": w.suggestion}
for w in self.warnings
],
"security_warnings": self.security_warnings,
"channel_status_warnings": [
{
"channel_type": c.channel_type,
"account_id": c.account_id,
"status": c.status.value,
"message": c.message,
"detail": c.detail,
}
for c in self.channel_status_warnings
],
"repair_plan": [
{
"id": s.id,
"description": s.description,
"action": s.action,
"reversible": s.reversible,
"params": s.params,
"requires_interactive": s.requires_interactive,
"is_aggressive": s.is_aggressive,
}
for s in self.repair_plan
],
"repair_results": [
{
"step_id": r.step_id,
"success": r.success,
"message": r.message,
"config_before": r.config_before,
"config_after": r.config_after,
}
for r in self.repair_results
],
}
return result
def _credential_dict(self) -> dict | None:
if not self.credential:
return None
return {
"status": self.credential.status.value,
"token_obtained": self.credential.token_obtained,
"expires_in": self.credential.expires_in,
"error": self.credential.error,
}
def _permission_dict(self) -> dict | None:
if not self.permission:
return None
return {
"status": self.permission.status.value,
"can_send_message": self.permission.can_send_message,
"can_read_message": self.permission.can_read_message,
"can_manage_group": self.permission.can_manage_group,
"missing_permissions": self.permission.missing_permissions,
"error": self.permission.error,
}
def _connectivity_dict(self) -> dict | None:
if not self.connectivity:
return None
return {
"status": self.connectivity.status.value,
"latency_ms": self.connectivity.latency_ms,
"endpoint": self.connectivity.endpoint,
"error": self.connectivity.error,
}