Files
aiagent/backend/app/websocket/collaboration_manager.py
2026-01-19 00:09:36 +08:00

200 lines
7.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
工作流协作管理器
管理多人协作编辑工作流的WebSocket连接和消息同步
"""
from typing import Dict, Set, List, Optional
from fastapi import WebSocket
import json
import asyncio
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
class CollaborationManager:
"""工作流协作管理器"""
def __init__(self):
# workflow_id -> Set[WebSocket] 映射,存储每个工作流的连接
self.active_connections: Dict[str, Set[WebSocket]] = {}
# WebSocket -> user_info 映射,存储每个连接的用户信息
self.connection_users: Dict[WebSocket, Dict] = {}
# workflow_id -> Dict[user_id, user_info] 映射,存储每个工作流的在线用户
self.workflow_users: Dict[str, Dict[str, Dict]] = {}
# 操作锁,用于冲突解决
self.operation_locks: Dict[str, asyncio.Lock] = {}
async def connect(self, websocket: WebSocket, workflow_id: str, user_id: str, username: str):
"""建立协作连接"""
await websocket.accept()
if workflow_id not in self.active_connections:
self.active_connections[workflow_id] = set()
self.workflow_users[workflow_id] = {}
self.operation_locks[workflow_id] = asyncio.Lock()
self.active_connections[workflow_id].add(websocket)
user_info = {
"user_id": user_id,
"username": username,
"joined_at": datetime.now().isoformat(),
"color": self._get_user_color(user_id) # 为用户分配颜色
}
self.connection_users[websocket] = user_info
self.workflow_users[workflow_id][user_id] = user_info
# 通知其他用户有新用户加入
await self.broadcast_user_joined(workflow_id, user_info, exclude_websocket=websocket)
# 发送当前在线用户列表给新用户
await self.send_personal_message({
"type": "collaboration_init",
"workflow_id": workflow_id,
"current_user": user_info,
"online_users": list(self.workflow_users[workflow_id].values())
}, websocket)
logger.info(f"用户 {username} ({user_id}) 加入工作流 {workflow_id} 的协作编辑")
def disconnect(self, websocket: WebSocket, workflow_id: str):
"""断开协作连接"""
if workflow_id in self.active_connections:
self.active_connections[workflow_id].discard(websocket)
if websocket in self.connection_users:
user_info = self.connection_users[websocket]
user_id = user_info["user_id"]
# 从工作流用户列表中移除
if user_id in self.workflow_users[workflow_id]:
del self.workflow_users[workflow_id][user_id]
# 通知其他用户有用户离开
self.broadcast_user_left(workflow_id, user_id, exclude_websocket=websocket)
del self.connection_users[websocket]
logger.info(f"用户 {user_info.get('username')} ({user_id}) 离开工作流 {workflow_id} 的协作编辑")
# 如果没有连接了,清理资源
if not self.active_connections[workflow_id]:
del self.active_connections[workflow_id]
del self.workflow_users[workflow_id]
if workflow_id in self.operation_locks:
del self.operation_locks[workflow_id]
async def broadcast_operation(self, workflow_id: str, operation: Dict, exclude_websocket: Optional[WebSocket] = None):
"""广播操作到所有连接的客户端"""
if workflow_id not in self.active_connections:
return
message = {
"type": "operation",
"workflow_id": workflow_id,
"operation": operation,
"timestamp": datetime.now().isoformat()
}
disconnected = set()
for websocket in self.active_connections[workflow_id]:
if websocket == exclude_websocket:
continue
try:
await websocket.send_json(message)
except Exception as e:
logger.warning(f"发送协作消息失败: {e}")
disconnected.add(websocket)
# 清理断开的连接
for ws in disconnected:
self.disconnect(ws, workflow_id)
async def broadcast_user_joined(self, workflow_id: str, user_info: Dict, exclude_websocket: Optional[WebSocket] = None):
"""广播用户加入消息"""
if workflow_id not in self.active_connections:
return
message = {
"type": "user_joined",
"workflow_id": workflow_id,
"user": user_info
}
disconnected = set()
for websocket in self.active_connections[workflow_id]:
if websocket == exclude_websocket:
continue
try:
await websocket.send_json(message)
except Exception as e:
logger.warning(f"发送用户加入消息失败: {e}")
disconnected.add(websocket)
for ws in disconnected:
self.disconnect(ws, workflow_id)
async def broadcast_user_left(self, workflow_id: str, user_id: str, exclude_websocket: Optional[WebSocket] = None):
"""广播用户离开消息"""
if workflow_id not in self.active_connections:
return
message = {
"type": "user_left",
"workflow_id": workflow_id,
"user_id": user_id
}
disconnected = set()
for websocket in self.active_connections[workflow_id]:
if websocket == exclude_websocket:
continue
try:
await websocket.send_json(message)
except Exception as e:
logger.warning(f"发送用户离开消息失败: {e}")
disconnected.add(websocket)
for ws in disconnected:
self.disconnect(ws, workflow_id)
async def send_personal_message(self, message: Dict, websocket: WebSocket):
"""发送个人消息"""
try:
await websocket.send_json(message)
except Exception as e:
logger.warning(f"发送个人消息失败: {e}")
def get_online_users(self, workflow_id: str) -> List[Dict]:
"""获取在线用户列表"""
if workflow_id not in self.workflow_users:
return []
return list(self.workflow_users[workflow_id].values())
def _get_user_color(self, user_id: str) -> str:
"""为用户分配颜色基于用户ID的哈希"""
colors = [
"#FF6B6B", "#4ECDC4", "#45B7D1", "#FFA07A", "#98D8C8",
"#F7DC6F", "#BB8FCE", "#85C1E2", "#F8B739", "#52BE80"
]
hash_value = hash(user_id) % len(colors)
return colors[hash_value]
async def acquire_lock(self, workflow_id: str):
"""获取操作锁(用于冲突解决)"""
if workflow_id not in self.operation_locks:
self.operation_locks[workflow_id] = asyncio.Lock()
return await self.operation_locks[workflow_id].acquire()
def release_lock(self, workflow_id: str):
"""释放操作锁"""
if workflow_id in self.operation_locks:
self.operation_locks[workflow_id].release()
# 全局协作管理器实例
collaboration_manager = CollaborationManager()