PMFlow/backend/app/services/integration.py

542 lines
18 KiB
Python

"""
Neural Sync 2.0 - Integration Service
集成服务 - 管理 Plane 和飞书集成
- 配置管理
- 状态监控
- 健康检查
- 故障降级
"""
import asyncio
import logging
from typing import Optional, Dict, List, Any
from pydantic import BaseModel, Field
from datetime import datetime, timedelta
from enum import Enum
import httpx
from ..utils.timezone import now_cn_naive
logger = logging.getLogger(__name__)
class IntegrationType(str, Enum):
"""集成类型"""
PLANE = "plane"
LARK = "lark"
GITHUB = "github"
GITLAB = "gitlab"
class IntegrationStatus(str, Enum):
"""集成状态"""
ACTIVE = "active"
INACTIVE = "inactive"
ERROR = "error"
DEGRADED = "degraded"
PENDING = "pending"
class PlaneConfig(BaseModel):
"""Plane 配置"""
api_url: str = "https://api.plane.so/api/v1"
api_key: str = ""
workspace_slug: str = ""
project_id: str = ""
enabled: bool = True
class LarkConfig(BaseModel):
"""飞书配置"""
app_id: str = ""
app_secret: str = ""
verification_token: str = ""
encrypt_key: str = ""
default_chat_id: str = ""
enabled: bool = True
class IntegrationHealth(BaseModel):
"""集成健康状态"""
type: IntegrationType
status: IntegrationStatus
last_check: datetime
last_success: Optional[datetime] = None
last_error: Optional[str] = None
error_count: int = 0
consecutive_failures: int = 0
latency_ms: Optional[float] = None
message: str = ""
class IntegrationConfig(BaseModel):
"""集成配置"""
plane: PlaneConfig = Field(default_factory=PlaneConfig)
lark: LarkConfig = Field(default_factory=LarkConfig)
class TaskSyncResult(BaseModel):
"""任务同步结果"""
success: bool
integration_type: IntegrationType
tasks_created: int = 0
tasks_updated: int = 0
errors: List[str] = Field(default_factory=list)
details: Dict[str, Any] = Field(default_factory=dict)
class NotificationResult(BaseModel):
"""通知发送结果"""
success: bool
integration_type: IntegrationType
recipients: int = 0
error: Optional[str] = None
class IntegrationService:
"""
集成服务 - 管理外部集成
特性:
- 配置管理(动态加载/保存)
- 健康检查和监控
- 自动故障降级
- 重试机制
- 事件通知
"""
# 健康检查配置
HEALTH_CHECK_INTERVAL = 60 # 秒
MAX_CONSECUTIVE_FAILURES = 3
DEGRADE_THRESHOLD = 5 # 错误次数后降级
def __init__(self, config: Optional[IntegrationConfig] = None, event_bus=None):
self.config = config or IntegrationConfig()
self.event_bus = event_bus
# 健康状态
self._health_status: Dict[IntegrationType, IntegrationHealth] = {
IntegrationType.PLANE: IntegrationHealth(
type=IntegrationType.PLANE,
status=IntegrationStatus.PENDING,
last_check=now_cn_naive(),
),
IntegrationType.LARK: IntegrationHealth(
type=IntegrationType.LARK,
status=IntegrationStatus.PENDING,
last_check=now_cn_naive(),
),
}
# 统计信息
self._stats = {
IntegrationType.PLANE: {"synced": 0, "failed": 0},
IntegrationType.LARK: {"sent": 0, "failed": 0},
}
# 服务实例(延迟加载)
self._plane_service = None
self._lark_service = None
# 健康检查任务
self._health_check_task: Optional[asyncio.Task] = None
async def start(self):
"""启动集成服务"""
self._health_check_task = asyncio.create_task(self._health_check_loop())
logger.info("[Integration] Service started")
async def stop(self):
"""停止集成服务"""
if self._health_check_task:
self._health_check_task.cancel()
try:
await self._health_check_task
except asyncio.CancelledError:
pass
logger.info("[Integration] Service stopped")
async def sync_tasks_to_plane(
self,
tasks: List[Dict[str, Any]],
is_urgent: bool = False
) -> TaskSyncResult:
"""
同步任务到 Plane
增强功能:
- 批量创建
- 标签管理
- 优先级映射
- 错误重试
"""
integration_type = IntegrationType.PLANE
health = self._health_status[integration_type]
# 检查服务是否可用
if not self._is_integration_available(integration_type):
return TaskSyncResult(
success=False,
integration_type=integration_type,
errors=[f"Plane integration is {health.status.value}"],
)
start_time = now_cn_naive()
try:
# 获取 Plane 服务
plane = await self._get_plane_service()
# 批量同步
result = await plane.sync_tasks(tasks, is_urgent)
# 更新统计
if result.get("status") == "success":
self._stats[integration_type]["synced"] += result.get("created_count", 0)
await self._record_success(integration_type)
else:
self._stats[integration_type]["failed"] += len(result.get("errors", []))
await self._record_failure(integration_type, ", ".join(result.get("errors", [])))
# 计算延迟
latency = (now_cn_naive() - start_time).total_seconds() * 1000
health.latency_ms = latency
return TaskSyncResult(
success=result.get("status") == "success",
integration_type=integration_type,
tasks_created=result.get("created_count", 0),
errors=result.get("errors", []),
details=result,
)
except Exception as e:
logger.error(f"[Integration] Plane sync failed: {e}")
await self._record_failure(integration_type, str(e))
return TaskSyncResult(
success=False,
integration_type=integration_type,
errors=[str(e)],
)
async def send_strategic_alert(
self,
chat_id: str,
signal_content: str,
decision: Dict[str, Any],
tasks: List[Dict[str, Any]]
) -> NotificationResult:
"""
发送战略预警到飞书
增强功能:
- 富文本卡片
- 可操作按钮
- @提及支持
- 消息去重
"""
integration_type = IntegrationType.LARK
health = self._health_status[integration_type]
# 检查服务是否可用
if not self._is_integration_available(integration_type):
return NotificationResult(
success=False,
integration_type=integration_type,
error=f"Lark integration is {health.status.value}",
)
start_time = now_cn_naive()
try:
# 获取飞书服务
lark = await self._get_lark_service()
# 发送卡片消息
await lark.send_strategic_alert(chat_id, signal_content, decision, tasks)
# 更新统计
self._stats[integration_type]["sent"] += 1
await self._record_success(integration_type)
# 计算延迟
latency = (now_cn_naive() - start_time).total_seconds() * 1000
health.latency_ms = latency
# 发送事件
if self.event_bus:
from ..events import EventType, Event
await self.event_bus.publish(
Event(
type=EventType.LARK_NOTIFY_SUCCESS,
data={
"type": "strategic_alert",
"chat_id": chat_id,
},
)
)
return NotificationResult(
success=True,
integration_type=integration_type,
recipients=1,
)
except Exception as e:
logger.error(f"[Integration] Lark notification failed: {e}")
await self._record_failure(integration_type, str(e))
# 发送失败事件
if self.event_bus:
from ..events import EventType, Event
await self.event_bus.publish(
Event(
type=EventType.LARK_NOTIFY_FAILED,
data={
"type": "strategic_alert",
"error": str(e),
},
)
)
return NotificationResult(
success=False,
integration_type=integration_type,
error=str(e),
)
async def send_card_message(
self,
chat_id: str,
content: str,
tasks: List[Dict[str, Any]]
) -> NotificationResult:
"""
发送卡片消息到飞书
"""
integration_type = IntegrationType.LARK
health = self._health_status[integration_type]
if not self._is_integration_available(integration_type):
return NotificationResult(
success=False,
integration_type=integration_type,
error=f"Lark integration is {health.status.value}",
)
try:
lark = await self._get_lark_service()
await lark.send_card_message(chat_id, content, tasks)
self._stats[integration_type]["sent"] += 1
await self._record_success(integration_type)
return NotificationResult(
success=True,
integration_type=integration_type,
recipients=1,
)
except Exception as e:
logger.error(f"[Integration] Lark card message failed: {e}")
await self._record_failure(integration_type, str(e))
return NotificationResult(
success=False,
integration_type=integration_type,
error=str(e),
)
async def update_plane_issue_status(
self,
issue_id: str,
status: str = "Done"
) -> bool:
"""更新 Plane 工单状态"""
integration_type = IntegrationType.PLANE
if not self._is_integration_available(integration_type):
return False
try:
plane = await self._get_plane_service()
result = await plane.update_issue_status(issue_id, status)
if "error" not in result:
await self._record_success(integration_type)
return True
await self._record_failure(integration_type, result.get("error", "Unknown error"))
return False
except Exception as e:
logger.error(f"[Integration] Plane status update failed: {e}")
await self._record_failure(integration_type, str(e))
return False
def get_health_status(self) -> Dict[str, IntegrationHealth]:
"""获取所有集成健康状态"""
return {k.value: v for k, v in self._health_status.items()}
def get_stats(self) -> Dict[str, Dict[str, int]]:
"""获取集成统计信息"""
return {k.value: v for k, v in self._stats.items()}
async def check_plane_health(self) -> IntegrationHealth:
"""检查 Plane 集成健康"""
integration_type = IntegrationType.PLANE
health = self._health_status[integration_type]
start_time = now_cn_naive()
if not self.config.plane.enabled or not self.config.plane.api_key:
health.status = IntegrationStatus.INACTIVE
health.message = "Plane integration is disabled"
health.last_check = now_cn_naive()
return health
try:
# 简单的健康检查:尝试获取项目标签
plane = await self._get_plane_service()
label_id = await plane.get_label_id("__health_check__")
health.status = IntegrationStatus.ACTIVE
health.message = "Connected"
health.last_check = now_cn_naive()
health.latency_ms = (now_cn_naive() - start_time).total_seconds() * 1000
except Exception as e:
logger.warning(f"[Integration] Plane health check failed: {e}")
health.status = IntegrationStatus.ERROR
health.message = f"Health check failed: {str(e)}"
health.last_check = now_cn_naive()
return health
async def check_lark_health(self) -> IntegrationHealth:
"""检查飞书集成健康"""
integration_type = IntegrationType.LARK
health = self._health_status[integration_type]
start_time = now_cn_naive()
if not self.config.lark.enabled or not self.config.lark.app_id:
health.status = IntegrationStatus.INACTIVE
health.message = "Lark integration is disabled"
health.last_check = now_cn_naive()
return health
try:
# 简单的健康检查:尝试获取访问令牌
lark = await self._get_lark_service()
token = await lark.get_tenant_access_token()
if token and token != "mock_token":
health.status = IntegrationStatus.ACTIVE
health.message = "Connected"
else:
health.status = IntegrationStatus.DEGRADED
health.message = "Running in simulation mode"
health.last_check = now_cn_naive()
health.latency_ms = (now_cn_naive() - start_time).total_seconds() * 1000
except Exception as e:
logger.warning(f"[Integration] Lark health check failed: {e}")
health.status = IntegrationStatus.ERROR
health.message = f"Health check failed: {str(e)}"
health.last_check = now_cn_naive()
return health
async def update_config(self, config: IntegrationConfig) -> None:
"""更新集成配置"""
self.config = config
# 重新评估健康状态
await self.check_plane_health()
await self.check_lark_health()
logger.info("[Integration] Configuration updated")
async def _get_plane_service(self):
"""获取 Plane 服务实例"""
if self._plane_service is None:
from .plane import PlaneService
self._plane_service = PlaneService()
# 应用配置
if self.config.plane.api_url:
self._plane_service.api_url = self.config.plane.api_url
if self.config.plane.api_key:
self._plane_service.api_key = self.config.plane.api_key
if self.config.plane.workspace_slug:
self._plane_service.workspace_slug = self.config.plane.workspace_slug
if self.config.plane.project_id:
self._plane_service.project_id = self.config.plane.project_id
self._plane_service.is_simulation = not self.config.plane.api_key
return self._plane_service
async def _get_lark_service(self):
"""获取飞书服务实例"""
if self._lark_service is None:
from .lark import LarkService
self._lark_service = LarkService()
# 应用配置
if self.config.lark.app_id:
self._lark_service.app_id = self.config.lark.app_id
if self.config.lark.app_secret:
self._lark_service.app_secret = self.config.lark.app_secret
if self.config.lark.verification_token:
self._lark_service.verification_token = self.config.lark.verification_token
return self._lark_service
def _is_integration_available(self, integration_type: IntegrationType) -> bool:
"""检查集成是否可用"""
health = self._health_status[integration_type]
return health.status in [IntegrationStatus.ACTIVE, IntegrationStatus.DEGRADED]
async def _record_success(self, integration_type: IntegrationType) -> None:
"""记录成功操作"""
health = self._health_status[integration_type]
health.last_success = now_cn_naive()
health.consecutive_failures = 0
if health.status == IntegrationStatus.ERROR:
health.status = IntegrationStatus.ACTIVE
async def _record_failure(self, integration_type: IntegrationType, error: str) -> None:
"""记录失败操作"""
health = self._health_status[integration_type]
health.last_error = error
health.error_count += 1
health.consecutive_failures += 1
health.last_check = now_cn_naive()
# 达到降级阈值
if health.consecutive_failures >= self.MAX_CONSECUTIVE_FAILURES:
health.status = IntegrationStatus.ERROR
health.message = f"Service degraded after {health.consecutive_failures} consecutive failures"
logger.warning(f"[Integration] {integration_type.value} degraded after {health.consecutive_failures} failures")
async def _health_check_loop(self) -> None:
"""健康检查循环"""
while True:
try:
await asyncio.sleep(self.HEALTH_CHECK_INTERVAL)
await self.check_plane_health()
await self.check_lark_health()
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"[Integration] Health check loop error: {e}")
# 全局服务实例
_integration_service = None
def get_integration_service(config: Optional[IntegrationConfig] = None, event_bus=None) -> IntegrationService:
"""获取集成服务实例"""
global _integration_service
if _integration_service is None:
_integration_service = IntegrationService(config, event_bus)
return _integration_service