Files
aiagent/frontend/src/composables/useCollaboration.ts
2026-01-19 00:09:36 +08:00

285 lines
7.5 KiB
TypeScript
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.
/**
* 工作流协作 Composable
* 支持多人实时协作编辑工作流
*/
import { ref, onUnmounted } from 'vue'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/user'
import api from '@/api'
export interface CollaborationUser {
user_id: string
username: string
joined_at: string
color: string
}
export interface CollaborationOperation {
type: string
user_id: string
username: string
data?: any
timestamp?: string
}
export interface CollaborationMessage {
type: string
workflow_id?: string
current_user?: CollaborationUser
online_users?: CollaborationUser[]
user?: CollaborationUser
user_id?: string
operation?: CollaborationOperation
timestamp?: string
}
export function useCollaboration(workflowId: string) {
const connected = ref<boolean>(false)
const onlineUsers = ref<CollaborationUser[]>([])
const currentUser = ref<CollaborationUser | null>(null)
const ws = ref<WebSocket | null>(null)
let heartbeatInterval: number | null = null
let reconnectTimeout: number | null = null
const reconnectAttempts = ref<number>(0)
const maxReconnectAttempts = 5
// 操作监听器
type OperationHandler = (operation: CollaborationOperation) => void
const operationHandlers = new Set<OperationHandler>()
// 获取WebSocket URL
const getWebSocketUrl = () => {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
const hostname = window.location.hostname
const port = hostname === 'localhost' || hostname === '127.0.0.1' ? '8037' : '8037'
const userStore = useUserStore()
const token = userStore.token
if (!token) {
throw new Error('未登录,无法建立协作连接')
}
return `${protocol}//${hostname}:${port}/api/v1/collaboration/ws/workflows/${workflowId}?token=${token}`
}
// 连接WebSocket
const connect = () => {
if (ws.value && ws.value.readyState === WebSocket.OPEN) {
return // 已经连接
}
const wsUrl = getWebSocketUrl()
console.log('[协作] 连接中:', wsUrl)
try {
ws.value = new WebSocket(wsUrl)
ws.value.onopen = () => {
console.log('[协作] 连接已建立')
connected.value = true
reconnectAttempts.value = 0
// 启动心跳
startHeartbeat()
}
ws.value.onmessage = (event) => {
try {
const message: CollaborationMessage = JSON.parse(event.data)
handleMessage(message)
} catch (e) {
console.error('[协作] 解析消息失败:', e)
}
}
ws.value.onerror = (err) => {
console.error('[协作] 错误:', err)
connected.value = false
ElMessage.error('协作连接错误')
}
ws.value.onclose = (event) => {
console.log('[协作] 连接已关闭', event.code, event.reason)
connected.value = false
stopHeartbeat()
// 如果不是正常关闭,尝试重连
if (event.code !== 1000 && reconnectAttempts.value < maxReconnectAttempts) {
reconnectAttempts.value++
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts.value), 30000)
console.log(`[协作] ${delay}ms后尝试重连 (${reconnectAttempts.value}/${maxReconnectAttempts})`)
reconnectTimeout = window.setTimeout(() => {
connect()
}, delay)
}
}
} catch (err) {
console.error('[协作] 连接失败:', err)
ElMessage.error('协作连接失败')
}
}
// 处理消息
const handleMessage = (message: CollaborationMessage) => {
console.log('[协作] 收到消息:', message)
switch (message.type) {
case 'collaboration_init':
// 初始化消息
if (message.current_user) {
currentUser.value = message.current_user
}
if (message.online_users) {
onlineUsers.value = message.online_users
}
break
case 'user_joined':
// 用户加入
if (message.user) {
onlineUsers.value.push(message.user)
ElMessage.info(`${message.user.username} 加入了协作编辑`)
}
break
case 'user_left':
// 用户离开
if (message.user_id) {
onlineUsers.value = onlineUsers.value.filter(u => u.user_id !== message.user_id)
const user = onlineUsers.value.find(u => u.user_id === message.user_id)
if (user) {
ElMessage.info(`${user.username} 离开了协作编辑`)
}
}
break
case 'operation':
// 工作流操作
if (message.operation) {
// 触发所有注册的操作处理器
operationHandlers.forEach(handler => {
try {
handler(message.operation!)
} catch (e) {
console.error('[协作] 操作处理器执行失败:', e)
}
})
console.log('[协作] 收到操作:', message.operation)
}
break
case 'pong':
// 心跳响应
break
case 'error':
ElMessage.error(message.message || '协作错误')
break
}
}
// 发送操作
const sendOperation = (operation: Omit<CollaborationOperation, 'user_id' | 'username' | 'timestamp'>) => {
if (!ws.value || ws.value.readyState !== WebSocket.OPEN) {
console.warn('[协作] WebSocket未连接无法发送操作')
return
}
const fullOperation: CollaborationOperation = {
...operation,
user_id: currentUser.value?.user_id || '',
username: currentUser.value?.username || '',
timestamp: new Date().toISOString()
}
const message = {
type: 'operation',
operation: fullOperation
}
try {
ws.value.send(JSON.stringify(message))
} catch (e) {
console.error('[协作] 发送操作失败:', e)
}
}
// 启动心跳
const startHeartbeat = () => {
heartbeatInterval = window.setInterval(() => {
if (ws.value && ws.value.readyState === WebSocket.OPEN) {
ws.value.send(JSON.stringify({ type: 'ping' }))
}
}, 30000) // 每30秒发送一次心跳
}
// 停止心跳
const stopHeartbeat = () => {
if (heartbeatInterval) {
clearInterval(heartbeatInterval)
heartbeatInterval = null
}
if (reconnectTimeout) {
clearTimeout(reconnectTimeout)
reconnectTimeout = null
}
}
// 断开连接
const disconnect = () => {
stopHeartbeat()
if (ws.value) {
ws.value.close(1000, '正常关闭')
ws.value = null
}
connected.value = false
onlineUsers.value = []
currentUser.value = null
}
// 获取在线用户列表
const fetchOnlineUsers = async () => {
try {
const response = await api.get(`/api/v1/collaboration/workflows/${workflowId}/users`)
if (response.data.online_users) {
onlineUsers.value = response.data.online_users
}
} catch (e) {
console.error('[协作] 获取在线用户失败:', e)
}
}
// 注册操作监听器
const onOperation = (handler: OperationHandler) => {
operationHandlers.add(handler)
// 返回取消注册函数
return () => {
operationHandlers.delete(handler)
}
}
// 取消注册操作监听器
const offOperation = (handler: OperationHandler) => {
operationHandlers.delete(handler)
}
// 清理
onUnmounted(() => {
disconnect()
operationHandlers.clear()
})
return {
connected,
onlineUsers,
currentUser,
connect,
disconnect,
sendOperation,
fetchOnlineUsers,
onOperation,
offOperation
}
}