542 lines
18 KiB
Python
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
|