Files
waooplus/src/lib/query/hooks/run-stream/event-parser.ts
2026-03-08 17:10:06 +08:00

348 lines
10 KiB
TypeScript

import type { RunStreamEvent } from '@/lib/novel-promotion/run-stream/types'
import { TASK_EVENT_TYPE, TASK_SSE_EVENT_TYPE, type SSEEvent } from '@/lib/task/types'
import { resolveTaskErrorMessage as resolveUnifiedTaskErrorMessage } from '@/lib/task/error-message'
import type { RunResult } from './types'
export function parseSSEBlock(block: string): { event: string; data: string } | null {
const lines = block.split('\n')
let event = 'message'
const dataLines: string[] = []
for (const line of lines) {
if (!line) continue
if (line.startsWith('event:')) {
event = line.slice(6).trim()
continue
}
if (line.startsWith('data:')) {
dataLines.push(line.slice(5).trim())
}
}
if (dataLines.length === 0) return null
return {
event,
data: dataLines.join('\n'),
}
}
export function toObject(value: unknown): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) return {}
return value as Record<string, unknown>
}
export function readTextField(payload: Record<string, unknown>, key: string): string | undefined {
const value = payload[key]
return typeof value === 'string' ? value : undefined
}
export function readStepField(payload: Record<string, unknown>, key: string): number | undefined {
const value = payload[key]
return typeof value === 'number' && Number.isFinite(value) ? Math.max(1, Math.floor(value)) : undefined
}
function readStringArrayField(payload: Record<string, unknown>, key: string): string[] {
const value = payload[key]
if (!Array.isArray(value)) return []
const rows: string[] = []
for (const item of value) {
if (typeof item !== 'string') continue
const trimmed = item.trim()
if (!trimmed) continue
rows.push(trimmed)
}
return rows
}
function readBoolField(payload: Record<string, unknown>, key: string): boolean | undefined {
const value = payload[key]
if (value === true) return true
if (value === false) return false
return undefined
}
function normalizeLifecycleType(value: unknown): string | null {
if (typeof value !== 'string') return null
if (value === TASK_EVENT_TYPE.PROGRESS) return TASK_EVENT_TYPE.PROCESSING
if (
value === TASK_EVENT_TYPE.CREATED ||
value === TASK_EVENT_TYPE.PROCESSING ||
value === TASK_EVENT_TYPE.COMPLETED ||
value === TASK_EVENT_TYPE.FAILED
) {
return value
}
return null
}
function stageLooksCompleted(stage: string | undefined) {
if (!stage) return false
return (
stage === 'llm_completed' ||
stage === 'worker_llm_completed' ||
stage === 'worker_llm_complete' ||
stage === 'llm_proxy_persist' ||
stage === 'completed'
)
}
function stageLooksFailed(stage: string | undefined) {
if (!stage) return false
return stage === 'llm_error' || stage === 'worker_llm_error' || stage === 'error'
}
function resolveTaskErrorMessage(payload: Record<string, unknown>, fallback = 'task failed') {
return resolveUnifiedTaskErrorMessage(payload, fallback)
}
function extractTerminalPayload(payload: Record<string, unknown>) {
const result = toObject(payload.result)
if (Object.keys(result).length > 0) {
return result
}
return payload
}
export function mapTaskSSEEventToRunEvents(event: SSEEvent): RunStreamEvent[] {
const rawPayload = toObject(event.payload)
const payloadMeta = toObject(rawPayload.meta)
const runId = readTextField(rawPayload, 'runId')
|| readTextField(payloadMeta, 'runId')
|| (typeof event.taskId === 'string' ? event.taskId : '')
if (!runId) return []
const payload = rawPayload
const lifecycleType =
event.type === TASK_SSE_EVENT_TYPE.LIFECYCLE
? normalizeLifecycleType(payload.lifecycleType)
: null
const ts = typeof event.ts === 'string' ? event.ts : new Date().toISOString()
const flowStageTitle = readTextField(payload, 'flowStageTitle')
const flowStageIndex = readStepField(payload, 'flowStageIndex')
const flowStageTotal = readStepField(payload, 'flowStageTotal')
const rawStepId = readTextField(payload, 'stepId')
const rawStepTitle = readTextField(payload, 'stepTitle')
const stepId =
rawStepId ||
(
event.type === TASK_SSE_EVENT_TYPE.STREAM
? `step:${event.taskType || 'llm'}`
: undefined)
const stepAttempt = readStepField(payload, 'stepAttempt')
const stepTitle = rawStepTitle || flowStageTitle || undefined
const stepIndex = readStepField(payload, 'stepIndex') ?? flowStageIndex
const stepTotal = readStepField(payload, 'stepTotal') ?? flowStageTotal
const stage = readTextField(payload, 'stage')
const message = readTextField(payload, 'message')
const done = payload.done === true
const text = readTextField(payload, 'output') || readTextField(payload, 'text')
const reasoning = readTextField(payload, 'reasoning') || readTextField(payload, 'thinking')
const dependsOn = readStringArrayField(payload, 'dependsOn')
const blockedBy = readStringArrayField(payload, 'blockedBy')
const groupId = readTextField(payload, 'groupId')
const parallelKey = readTextField(payload, 'parallelKey')
const retryable = readBoolField(payload, 'retryable')
const stale = readBoolField(payload, 'stale')
if (event.type === TASK_SSE_EVENT_TYPE.STREAM) {
const stream = toObject(payload.stream)
const kind = stream.kind === 'reasoning' ? 'reasoning' : 'text'
const delta = typeof stream.delta === 'string' ? stream.delta : ''
if (!delta || !stepId) return []
const lane = stream.lane === 'reasoning' || kind === 'reasoning' ? 'reasoning' : 'text'
const seq =
typeof stream.seq === 'number' && Number.isFinite(stream.seq) ? Math.max(1, Math.floor(stream.seq)) : undefined
const streamStepAttempt =
typeof stream.attempt === 'number' && Number.isFinite(stream.attempt)
? Math.max(1, Math.floor(stream.attempt))
: undefined
return [{
runId,
event: 'step.chunk',
ts,
status: 'running',
stepId,
stepAttempt: stepAttempt ?? streamStepAttempt,
stepTitle,
stepIndex,
stepTotal,
dependsOn: dependsOn.length > 0 ? dependsOn : undefined,
blockedBy: blockedBy.length > 0 ? blockedBy : undefined,
groupId,
parallelKey,
retryable,
lane,
seq,
textDelta: lane === 'text' ? delta : undefined,
reasoningDelta: lane === 'reasoning' ? delta : undefined,
message,
}]
}
if (event.type !== TASK_SSE_EVENT_TYPE.LIFECYCLE) return []
const runEvents: RunStreamEvent[] = []
if (lifecycleType === TASK_EVENT_TYPE.CREATED) {
runEvents.push({
runId,
event: 'run.start',
ts,
status: 'running',
message,
payload,
})
return runEvents
}
if (lifecycleType === TASK_EVENT_TYPE.PROCESSING) {
if (stepId) {
runEvents.push({
runId,
event: 'step.start',
ts,
status: 'running',
stepId,
stepAttempt,
stepTitle,
stepIndex,
stepTotal,
dependsOn: dependsOn.length > 0 ? dependsOn : undefined,
blockedBy: blockedBy.length > 0 ? blockedBy : undefined,
groupId,
parallelKey,
retryable,
message,
})
if (done || stageLooksCompleted(stage)) {
runEvents.push({
runId,
event: 'step.complete',
ts,
status: stale ? 'stale' : 'completed',
stepId,
stepAttempt,
stepTitle,
stepIndex,
stepTotal,
dependsOn: dependsOn.length > 0 ? dependsOn : undefined,
blockedBy: blockedBy.length > 0 ? blockedBy : undefined,
groupId,
parallelKey,
retryable,
text,
reasoning,
message,
})
} else if (stageLooksFailed(stage)) {
runEvents.push({
runId,
event: 'step.error',
ts,
status: 'failed',
stepId,
stepAttempt,
stepTitle,
stepIndex,
stepTotal,
dependsOn: dependsOn.length > 0 ? dependsOn : undefined,
blockedBy: blockedBy.length > 0 ? blockedBy : undefined,
groupId,
parallelKey,
retryable,
message: resolveTaskErrorMessage(payload),
})
}
}
return runEvents
}
if (lifecycleType === TASK_EVENT_TYPE.COMPLETED) {
if (stepId) {
runEvents.push({
runId,
event: 'step.complete',
ts,
status: 'completed',
stepId,
stepAttempt,
stepTitle,
stepIndex,
stepTotal,
dependsOn: dependsOn.length > 0 ? dependsOn : undefined,
blockedBy: blockedBy.length > 0 ? blockedBy : undefined,
groupId,
parallelKey,
retryable,
text,
reasoning,
message,
})
}
runEvents.push({
runId,
event: 'run.complete',
ts,
status: 'completed',
message,
payload: extractTerminalPayload(payload),
})
return runEvents
}
if (lifecycleType === TASK_EVENT_TYPE.FAILED) {
const errorMessage = resolveTaskErrorMessage(payload)
if (stepId) {
runEvents.push({
runId,
event: 'step.error',
ts,
status: 'failed',
stepId,
stepAttempt,
stepTitle,
stepIndex,
stepTotal,
dependsOn: dependsOn.length > 0 ? dependsOn : undefined,
blockedBy: blockedBy.length > 0 ? blockedBy : undefined,
groupId,
parallelKey,
retryable,
message: errorMessage,
})
}
runEvents.push({
runId,
event: 'run.error',
ts,
status: 'failed',
message: errorMessage,
payload,
})
}
return runEvents
}
export function toTerminalRunResult(event: RunStreamEvent): RunResult | null {
if (event.event !== 'run.complete' && event.event !== 'run.error') return null
const summaryFromPayload =
event.payload &&
typeof event.payload.summary === 'object' &&
event.payload.summary
? (event.payload.summary as Record<string, unknown>)
: null
return {
runId: event.runId,
status: event.event === 'run.complete' ? 'completed' : 'failed',
summary:
event.event === 'run.complete'
? summaryFromPayload || event.payload || null
: summaryFromPayload,
payload: event.payload || null,
errorMessage: event.event === 'run.error' ? event.message || 'run failed' : '',
}
}