PMFlow/backend/app/services/integration.py

541 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
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=datetime.utcnow(),
),
IntegrationType.LARK: IntegrationHealth(
type=IntegrationType.LARK,
status=IntegrationStatus.PENDING,
last_check=datetime.utcnow(),
),
}
# 统计信息
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 = datetime.utcnow()
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 = (datetime.utcnow() - 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 = datetime.utcnow()
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 = (datetime.utcnow() - 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 = datetime.utcnow()
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 = datetime.utcnow()
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 = datetime.utcnow()
health.latency_ms = (datetime.utcnow() - 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 = datetime.utcnow()
return health
async def check_lark_health(self) -> IntegrationHealth:
"""检查飞书集成健康"""
integration_type = IntegrationType.LARK
health = self._health_status[integration_type]
start_time = datetime.utcnow()
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 = datetime.utcnow()
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 = datetime.utcnow()
health.latency_ms = (datetime.utcnow() - 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 = datetime.utcnow()
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 = datetime.utcnow()
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 = datetime.utcnow()
# 达到降级阈值
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