Skip to main content
Glama

Open Search MCP

by flyanima
MIT License
2
  • Apple
  • Linux
collaborative-editing.ts15 kB
/** * Collaborative Editing - 协作编辑管理器 * * 核心功能: * - 多用户实时协作 * - 冲突检测和解决 * - 操作同步 * - 权限管理 * - 版本控制 */ import { InteractiveThoughtMap, ThoughtMapEvent } from './interactive-thought-map.js'; import { ThoughtNode, ThoughtNodeStatus } from './thought-node.js'; import { Logger } from '../utils/logger.js'; import { EventEmitter } from 'events'; /** * 用户角色枚举 */ export enum UserRole { OWNER = 'owner', EDITOR = 'editor', VIEWER = 'viewer', COMMENTER = 'commenter' } /** * 操作类型枚举 */ export enum OperationType { NODE_CREATE = 'node_create', NODE_UPDATE = 'node_update', NODE_DELETE = 'node_delete', NODE_MOVE = 'node_move', STATUS_CHANGE = 'status_change', CURSOR_MOVE = 'cursor_move', SELECTION_CHANGE = 'selection_change' } /** * 协作用户接口 */ export interface CollaborativeUser { id: string; name: string; email: string; role: UserRole; isOnline: boolean; lastSeen: Date; cursor?: { nodeId: string; position: { x: number; y: number }; }; selection?: string[]; // 选中的节点ID列表 color: string; // 用户标识颜色 } /** * 协作操作接口 */ export interface CollaborativeOperation { id: string; type: OperationType; userId: string; timestamp: Date; data: any; metadata: { nodeId?: string; previousValue?: any; newValue?: any; conflictResolution?: string; }; } /** * 冲突检测结果 */ export interface ConflictDetection { hasConflict: boolean; conflictType: 'concurrent_edit' | 'version_mismatch' | 'permission_denied' | 'node_deleted'; conflictingOperations: CollaborativeOperation[]; resolution: 'merge' | 'reject' | 'manual'; mergedResult?: any; } /** * 协作会话接口 */ export interface CollaborativeSession { id: string; thoughtMapId: string; owner: string; participants: CollaborativeUser[]; createdAt: Date; lastActivity: Date; isActive: boolean; settings: { allowAnonymous: boolean; maxParticipants: number; autoSave: boolean; conflictResolution: 'auto' | 'manual'; }; } /** * 协作编辑管理器类 */ export class CollaborativeEditingManager extends EventEmitter { private logger: Logger; private sessions: Map<string, CollaborativeSession> = new Map(); private operations: Map<string, CollaborativeOperation[]> = new Map(); private thoughtMaps: Map<string, InteractiveThoughtMap> = new Map(); private operationCounter: number = 0; constructor() { super(); this.logger = new Logger('CollaborativeEditingManager'); } /** * 创建协作会话 */ createSession( thoughtMapId: string, owner: CollaborativeUser, settings?: Partial<CollaborativeSession['settings']> ): string { const sessionId = this.generateSessionId(); const session: CollaborativeSession = { id: sessionId, thoughtMapId, owner: owner.id, participants: [owner], createdAt: new Date(), lastActivity: new Date(), isActive: true, settings: { allowAnonymous: false, maxParticipants: 10, autoSave: true, conflictResolution: 'auto', ...settings } }; this.sessions.set(sessionId, session); this.operations.set(sessionId, []); this.logger.info(`Created collaborative session: ${sessionId}`); this.emit('sessionCreated', session); return sessionId; } /** * 加入协作会话 */ joinSession(sessionId: string, user: CollaborativeUser): boolean { const session = this.sessions.get(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } // 检查权限和限制 if (!this.canJoinSession(session, user)) { return false; } // 添加用户到会话 const existingUserIndex = session.participants.findIndex(p => p.id === user.id); if (existingUserIndex >= 0) { // 更新现有用户信息 session.participants[existingUserIndex] = { ...user, isOnline: true }; } else { // 添加新用户 session.participants.push({ ...user, isOnline: true }); } session.lastActivity = new Date(); this.logger.info(`User ${user.id} joined session: ${sessionId}`); this.emit('userJoined', sessionId, user); return true; } /** * 离开协作会话 */ leaveSession(sessionId: string, userId: string): void { const session = this.sessions.get(sessionId); if (!session) { return; } const userIndex = session.participants.findIndex(p => p.id === userId); if (userIndex >= 0) { session.participants[userIndex].isOnline = false; session.participants[userIndex].lastSeen = new Date(); } session.lastActivity = new Date(); this.logger.info(`User ${userId} left session: ${sessionId}`); this.emit('userLeft', sessionId, userId); // 如果没有在线用户,标记会话为非活跃 const onlineUsers = session.participants.filter(p => p.isOnline); if (onlineUsers.length === 0) { session.isActive = false; } } /** * 执行协作操作 */ async executeOperation( sessionId: string, userId: string, type: OperationType, data: any ): Promise<CollaborativeOperation> { const session = this.sessions.get(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } const user = session.participants.find(p => p.id === userId); if (!user) { throw new Error(`User ${userId} not in session`); } // 检查权限 if (!this.hasPermission(user, type)) { throw new Error(`User ${userId} does not have permission for ${type}`); } const operation: CollaborativeOperation = { id: this.generateOperationId(), type, userId, timestamp: new Date(), data, metadata: {} }; // 冲突检测 const conflictResult = await this.detectConflicts(sessionId, operation); if (conflictResult.hasConflict) { operation.metadata.conflictResolution = await this.resolveConflict( sessionId, operation, conflictResult ); } // 执行操作 await this.applyOperation(sessionId, operation); // 记录操作 const sessionOperations = this.operations.get(sessionId) || []; sessionOperations.push(operation); this.operations.set(sessionId, sessionOperations); // 更新会话活动时间 session.lastActivity = new Date(); this.logger.debug(`Executed operation: ${type} by user ${userId}`); this.emit('operationExecuted', sessionId, operation); return operation; } /** * 同步操作到其他用户 */ syncOperation(sessionId: string, operation: CollaborativeOperation): void { const session = this.sessions.get(sessionId); if (!session) { return; } // 向所有在线用户(除了操作发起者)广播操作 const onlineUsers = session.participants.filter( p => p.isOnline && p.id !== operation.userId ); for (const user of onlineUsers) { this.emit('operationSync', sessionId, user.id, operation); } } /** * 更新用户光标位置 */ updateUserCursor( sessionId: string, userId: string, nodeId: string, position: { x: number; y: number } ): void { const session = this.sessions.get(sessionId); if (!session) { return; } const user = session.participants.find(p => p.id === userId); if (user) { user.cursor = { nodeId, position }; this.emit('cursorUpdated', sessionId, userId, user.cursor); } } /** * 更新用户选择 */ updateUserSelection(sessionId: string, userId: string, selectedNodeIds: string[]): void { const session = this.sessions.get(sessionId); if (!session) { return; } const user = session.participants.find(p => p.id === userId); if (user) { user.selection = selectedNodeIds; this.emit('selectionUpdated', sessionId, userId, selectedNodeIds); } } /** * 获取会话信息 */ getSession(sessionId: string): CollaborativeSession | undefined { return this.sessions.get(sessionId); } /** * 获取会话操作历史 */ getSessionOperations(sessionId: string): CollaborativeOperation[] { return this.operations.get(sessionId) || []; } /** * 获取在线用户 */ getOnlineUsers(sessionId: string): CollaborativeUser[] { const session = this.sessions.get(sessionId); if (!session) { return []; } return session.participants.filter(p => p.isOnline); } /** * 私有方法 */ private canJoinSession(session: CollaborativeSession, user: CollaborativeUser): boolean { // 检查最大参与者限制 const onlineCount = session.participants.filter(p => p.isOnline).length; if (onlineCount >= session.settings.maxParticipants) { return false; } // 检查匿名用户权限 if (!session.settings.allowAnonymous && !user.email) { return false; } return true; } private hasPermission(user: CollaborativeUser, operationType: OperationType): boolean { switch (user.role) { case UserRole.OWNER: return true; case UserRole.EDITOR: return ![OperationType.NODE_DELETE].includes(operationType); case UserRole.COMMENTER: return [OperationType.CURSOR_MOVE, OperationType.SELECTION_CHANGE].includes(operationType); case UserRole.VIEWER: return [OperationType.CURSOR_MOVE, OperationType.SELECTION_CHANGE].includes(operationType); default: return false; } } private async detectConflicts( sessionId: string, operation: CollaborativeOperation ): Promise<ConflictDetection> { const sessionOperations = this.operations.get(sessionId) || []; const recentOperations = sessionOperations.filter( op => Date.now() - op.timestamp.getTime() < 5000 // 5秒内的操作 ); const conflictingOps = recentOperations.filter(op => op.metadata.nodeId === operation.metadata.nodeId && op.userId !== operation.userId && op.type === operation.type ); if (conflictingOps.length > 0) { return { hasConflict: true, conflictType: 'concurrent_edit', conflictingOperations: conflictingOps, resolution: 'merge' }; } return { hasConflict: false, conflictType: 'concurrent_edit', conflictingOperations: [], resolution: 'merge' }; } private async resolveConflict( sessionId: string, operation: CollaborativeOperation, conflict: ConflictDetection ): Promise<string> { const session = this.sessions.get(sessionId); if (!session) { return 'reject'; } switch (session.settings.conflictResolution) { case 'auto': return this.autoResolveConflict(operation, conflict); case 'manual': return await this.requestManualResolution(sessionId, operation, conflict); default: return 'reject'; } } private autoResolveConflict( operation: CollaborativeOperation, conflict: ConflictDetection ): string { // 简单的自动解决策略:最后操作获胜 const latestConflictOp = conflict.conflictingOperations.reduce((latest, op) => op.timestamp > latest.timestamp ? op : latest ); if (operation.timestamp > latestConflictOp.timestamp) { return 'accept'; } else { return 'merge'; } } private async requestManualResolution( sessionId: string, operation: CollaborativeOperation, conflict: ConflictDetection ): Promise<string> { // 发出冲突解决请求事件 this.emit('conflictResolutionRequired', sessionId, operation, conflict); // 在实际实现中,这里会等待用户输入 // 现在返回默认解决方案 return 'merge'; } private async applyOperation(sessionId: string, operation: CollaborativeOperation): Promise<void> { const thoughtMap = this.thoughtMaps.get(sessionId); if (!thoughtMap) { return; } switch (operation.type) { case OperationType.NODE_CREATE: const { title, content, type, parentId } = operation.data; thoughtMap.addNode(title, content, type, undefined, parentId); break; case OperationType.NODE_UPDATE: const { nodeId, updates } = operation.data; if (updates.content) { thoughtMap.updateNodeContent(nodeId, updates.content, updates.summary); } break; case OperationType.NODE_DELETE: thoughtMap.removeNode(operation.data.nodeId); break; case OperationType.NODE_MOVE: thoughtMap.moveNode(operation.data.nodeId, operation.data.newParentId); break; case OperationType.STATUS_CHANGE: thoughtMap.updateNodeStatus(operation.data.nodeId, operation.data.status); break; } // 同步操作到其他用户 this.syncOperation(sessionId, operation); } private generateSessionId(): string { return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } private generateOperationId(): string { return `op_${++this.operationCounter}_${Date.now()}`; } /** * 导出会话数据 */ exportSession(sessionId: string): any { const session = this.sessions.get(sessionId); const operations = this.operations.get(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } return { session, operations, exportedAt: new Date() }; } /** * 清理非活跃会话 */ cleanupInactiveSessions(maxAgeMs: number = 86400000): void { // 24小时 const now = Date.now(); for (const [sessionId, session] of this.sessions.entries()) { if (!session.isActive || (now - session.lastActivity.getTime()) > maxAgeMs) { this.sessions.delete(sessionId); this.operations.delete(sessionId); this.thoughtMaps.delete(sessionId); this.logger.info(`Cleaned up inactive session: ${sessionId}`); } } } /** * 获取会话统计 */ getSessionStats(sessionId: string): any { const session = this.sessions.get(sessionId); const operations = this.operations.get(sessionId) || []; if (!session) { return null; } const onlineUsers = session.participants.filter(p => p.isOnline); const operationsByUser = operations.reduce((stats, op) => { stats[op.userId] = (stats[op.userId] || 0) + 1; return stats; }, {} as Record<string, number>); return { sessionId, duration: Date.now() - session.createdAt.getTime(), totalParticipants: session.participants.length, onlineParticipants: onlineUsers.length, totalOperations: operations.length, operationsByUser, lastActivity: session.lastActivity }; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/flyanima/open-search-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server