You've already forked agentic-coding-workflow
133 lines
3.3 KiB
TypeScript
133 lines
3.3 KiB
TypeScript
// useRunStream 封装 run profile 的日志 WebSocket,结构照搬 useAcpStream:
|
|
// - connect + 自动重连(指数退避 1s/3s,最多 2 次)
|
|
// - ?since=lastLogID 续传
|
|
// - 把日志行累积进 ref 数组;state 帧更新 runStatus
|
|
import { ref, onScopeDispose, type Ref } from 'vue'
|
|
import { openRunLogsWS, type LogLine, type RunState } from '@/api/runs'
|
|
import { useAuthStore } from '@/stores/auth'
|
|
|
|
export type StreamStatus = 'idle' | 'connecting' | 'streaming' | 'closed' | 'error'
|
|
|
|
export interface UseRunStreamReturn {
|
|
status: Ref<StreamStatus>
|
|
runStatus: Ref<RunState>
|
|
exitCode: Ref<number | null>
|
|
logs: Ref<LogLine[]>
|
|
reconnect: () => void
|
|
close: () => void
|
|
clear: () => void
|
|
}
|
|
|
|
interface WireFrame {
|
|
id?: number
|
|
kind?: string
|
|
level?: 'info' | 'error'
|
|
text?: string
|
|
ts?: string
|
|
status?: RunState
|
|
exit_code?: number
|
|
}
|
|
|
|
export function useRunStream(profileId: string): UseRunStreamReturn {
|
|
const status = ref<StreamStatus>('idle')
|
|
const runStatus = ref<RunState>('stopped')
|
|
const exitCode = ref<number | null>(null)
|
|
const logs = ref<LogLine[]>([])
|
|
const lastLogID = ref(0)
|
|
|
|
let ws: WebSocket | null = null
|
|
let retried = 0
|
|
let manualClose = false
|
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
|
|
|
const auth = useAuthStore()
|
|
|
|
function connect() {
|
|
if (ws || manualClose) return
|
|
status.value = 'connecting'
|
|
const token = auth.token ?? ''
|
|
const sock = openRunLogsWS(profileId, lastLogID.value, token)
|
|
ws = sock
|
|
|
|
sock.onopen = () => {
|
|
status.value = 'streaming'
|
|
retried = 0
|
|
}
|
|
|
|
sock.onmessage = (ev: MessageEvent) => {
|
|
let parsed: WireFrame
|
|
try {
|
|
parsed = JSON.parse(ev.data as string) as WireFrame
|
|
} catch {
|
|
return
|
|
}
|
|
if (!parsed || typeof parsed !== 'object') return
|
|
|
|
if (parsed.kind === 'state') {
|
|
if (parsed.status) runStatus.value = parsed.status
|
|
exitCode.value = parsed.exit_code ?? null
|
|
return
|
|
}
|
|
if (parsed.kind === 'slow_consumer_disconnect') {
|
|
status.value = 'error'
|
|
return
|
|
}
|
|
// 普通日志行
|
|
if (parsed.kind === 'log' && typeof parsed.id === 'number') {
|
|
if (parsed.id > lastLogID.value) lastLogID.value = parsed.id
|
|
logs.value.push({
|
|
id: parsed.id,
|
|
level: parsed.level ?? 'info',
|
|
text: parsed.text ?? '',
|
|
ts: parsed.ts ?? '',
|
|
})
|
|
}
|
|
}
|
|
|
|
sock.onerror = () => {
|
|
status.value = 'error'
|
|
}
|
|
|
|
sock.onclose = () => {
|
|
ws = null
|
|
if (manualClose) {
|
|
status.value = 'closed'
|
|
return
|
|
}
|
|
if (retried < 2) {
|
|
const delay = retried === 0 ? 1000 : 3000
|
|
retried += 1
|
|
reconnectTimer = setTimeout(connect, delay)
|
|
} else {
|
|
status.value = 'closed'
|
|
}
|
|
}
|
|
}
|
|
|
|
function reconnect() {
|
|
if (ws) ws.close()
|
|
retried = 0
|
|
manualClose = false
|
|
connect()
|
|
}
|
|
|
|
function close() {
|
|
manualClose = true
|
|
if (reconnectTimer) {
|
|
clearTimeout(reconnectTimer)
|
|
reconnectTimer = null
|
|
}
|
|
if (ws) ws.close()
|
|
status.value = 'closed'
|
|
}
|
|
|
|
function clear() {
|
|
logs.value = []
|
|
}
|
|
|
|
connect()
|
|
onScopeDispose(close)
|
|
|
|
return { status, runStatus, exitCode, logs, reconnect, close, clear }
|
|
}
|