第一次提交
This commit is contained in:
284
frontend/src/composables/useCollaboration.ts
Normal file
284
frontend/src/composables/useCollaboration.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
/**
|
||||
* 工作流协作 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
|
||||
}
|
||||
}
|
||||
166
frontend/src/composables/useWebSocket.ts
Normal file
166
frontend/src/composables/useWebSocket.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* WebSocket Composable
|
||||
*/
|
||||
import { ref, onUnmounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
export interface WebSocketMessage {
|
||||
type: string
|
||||
execution_id?: string
|
||||
status?: string
|
||||
progress?: number
|
||||
message?: string
|
||||
result?: any
|
||||
error?: string
|
||||
execution_time?: number
|
||||
}
|
||||
|
||||
export function useWebSocket(executionId: string) {
|
||||
const status = ref<string>('pending')
|
||||
const progress = ref<number>(0)
|
||||
const result = ref<any>(null)
|
||||
const error = ref<string | null>(null)
|
||||
const executionTime = ref<number | null>(null)
|
||||
const connected = ref<boolean>(false)
|
||||
const ws = ref<WebSocket | null>(null)
|
||||
let heartbeatInterval: number | null = null
|
||||
|
||||
// 获取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'
|
||||
return `${protocol}//${hostname}:${port}/api/v1/ws/executions/${executionId}`
|
||||
}
|
||||
|
||||
// 连接WebSocket
|
||||
const connect = () => {
|
||||
if (ws.value && ws.value.readyState === WebSocket.OPEN) {
|
||||
return // 已经连接
|
||||
}
|
||||
|
||||
const wsUrl = getWebSocketUrl()
|
||||
console.log('[WebSocket] 连接中:', wsUrl)
|
||||
|
||||
try {
|
||||
ws.value = new WebSocket(wsUrl)
|
||||
|
||||
ws.value.onopen = () => {
|
||||
console.log('[WebSocket] 连接已建立')
|
||||
connected.value = true
|
||||
|
||||
// 启动心跳
|
||||
startHeartbeat()
|
||||
}
|
||||
|
||||
ws.value.onmessage = (event) => {
|
||||
try {
|
||||
const message: WebSocketMessage = JSON.parse(event.data)
|
||||
handleMessage(message)
|
||||
} catch (e) {
|
||||
console.error('[WebSocket] 解析消息失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
ws.value.onerror = (err) => {
|
||||
console.error('[WebSocket] 错误:', err)
|
||||
connected.value = false
|
||||
ElMessage.error('WebSocket连接错误')
|
||||
}
|
||||
|
||||
ws.value.onclose = () => {
|
||||
console.log('[WebSocket] 连接已关闭')
|
||||
connected.value = false
|
||||
stopHeartbeat()
|
||||
|
||||
// 如果执行还在进行中,尝试重连
|
||||
if (status.value === 'running' || status.value === 'pending') {
|
||||
setTimeout(() => {
|
||||
console.log('[WebSocket] 尝试重连...')
|
||||
connect()
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[WebSocket] 连接失败:', err)
|
||||
ElMessage.error('WebSocket连接失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 处理消息
|
||||
const handleMessage = (message: WebSocketMessage) => {
|
||||
console.log('[WebSocket] 收到消息:', message)
|
||||
|
||||
switch (message.type) {
|
||||
case 'status':
|
||||
if (message.status) {
|
||||
status.value = message.status
|
||||
}
|
||||
if (message.progress !== undefined) {
|
||||
progress.value = message.progress
|
||||
}
|
||||
if (message.result) {
|
||||
result.value = message.result
|
||||
}
|
||||
if (message.error) {
|
||||
error.value = message.error
|
||||
}
|
||||
if (message.execution_time !== undefined) {
|
||||
executionTime.value = message.execution_time
|
||||
}
|
||||
break
|
||||
|
||||
case 'pong':
|
||||
// 心跳响应,无需处理
|
||||
break
|
||||
|
||||
case 'error':
|
||||
error.value = message.message || '未知错误'
|
||||
ElMessage.error(message.message || '执行出错')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 启动心跳
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 断开连接
|
||||
const disconnect = () => {
|
||||
stopHeartbeat()
|
||||
if (ws.value) {
|
||||
ws.value.close()
|
||||
ws.value = null
|
||||
}
|
||||
connected.value = false
|
||||
}
|
||||
|
||||
// 清理
|
||||
onUnmounted(() => {
|
||||
disconnect()
|
||||
})
|
||||
|
||||
return {
|
||||
status,
|
||||
progress,
|
||||
result,
|
||||
error,
|
||||
executionTime,
|
||||
connected,
|
||||
connect,
|
||||
disconnect
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user