第一次提交

This commit is contained in:
rjb
2026-01-19 00:09:36 +08:00
parent de4b5059e9
commit 6674060f2f
191 changed files with 40940 additions and 0 deletions

View 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
}
}

View 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
}
}