fix(frontend): 修复合并审阅发现的构建与契约问题
恢复 Vue TypeScript 生产构建,补齐可运行页面壳子,并修复文件树与 SSE 状态问题。 按 FastAPI Wire Contract 统一 Service DTO 映射,同时补充前端开发说明和问题修复复盘。
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { resolveApiUrl } from './apiClient'
|
||||
|
||||
export type SseEventHandler = (event: string, data: Record<string, unknown>) => void
|
||||
|
||||
export interface SseClientOptions {
|
||||
@@ -37,7 +39,7 @@ export class SseClient {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
const resp = await fetch(url, {
|
||||
const resp = await fetch(resolveApiUrl(url), {
|
||||
method,
|
||||
headers,
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
@@ -53,6 +55,39 @@ export class SseClient {
|
||||
onOpen?.()
|
||||
|
||||
const decoder = new TextDecoder('utf-8')
|
||||
let eventName = 'message'
|
||||
let dataLines: string[] = []
|
||||
let doneNotified = false
|
||||
|
||||
const dispatchEvent = () => {
|
||||
if (!dataLines.length) {
|
||||
eventName = 'message'
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(dataLines.join('\n')) as Record<string, unknown>
|
||||
onEvent?.(eventName, data)
|
||||
if (!doneNotified && ['Done', 'RunCompleted', 'RunFailed', 'RunCancelled'].includes(eventName)) {
|
||||
doneNotified = true
|
||||
onDone?.()
|
||||
}
|
||||
} catch (error) {
|
||||
onError?.(error instanceof Error ? error : new Error('Malformed SSE data'))
|
||||
}
|
||||
eventName = 'message'
|
||||
dataLines = []
|
||||
}
|
||||
|
||||
const consumeLine = (line: string) => {
|
||||
if (line === '') return dispatchEvent()
|
||||
if (line.startsWith(':')) return
|
||||
const separator = line.indexOf(':')
|
||||
const field = separator === -1 ? line : line.slice(0, separator)
|
||||
let fieldValue = separator === -1 ? '' : line.slice(separator + 1)
|
||||
if (fieldValue.startsWith(' ')) fieldValue = fieldValue.slice(1)
|
||||
if (field === 'event') eventName = fieldValue
|
||||
if (field === 'data') dataLines.push(fieldValue)
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const { value, done } = await this.reader.read()
|
||||
@@ -60,39 +95,15 @@ export class SseClient {
|
||||
|
||||
this.buffer += decoder.decode(value, { stream: true })
|
||||
|
||||
const lines = this.buffer.split('\n')
|
||||
const lines = this.buffer.split(/\r?\n/)
|
||||
this.buffer = lines.pop() || ''
|
||||
|
||||
let eventName = 'message'
|
||||
let dataStr = ''
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed) {
|
||||
if (dataStr) {
|
||||
try {
|
||||
const data = JSON.parse(dataStr)
|
||||
onEvent?.(eventName, data)
|
||||
if (eventName === 'Done' || eventName === 'RunCompleted' || eventName === 'RunFailed' || eventName === 'RunCancelled') {
|
||||
onDone?.()
|
||||
}
|
||||
} catch {
|
||||
/* ignore malformed json */
|
||||
}
|
||||
eventName = 'message'
|
||||
dataStr = ''
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (trimmed.startsWith('event:')) {
|
||||
eventName = trimmed.slice(6).trim()
|
||||
} else if (trimmed.startsWith('data:')) {
|
||||
const d = trimmed.slice(5).trim()
|
||||
dataStr += dataStr ? '\n' + d : d
|
||||
}
|
||||
}
|
||||
lines.forEach(consumeLine)
|
||||
}
|
||||
|
||||
this.buffer += decoder.decode()
|
||||
if (this.buffer) consumeLine(this.buffer.replace(/\r$/, ''))
|
||||
dispatchEvent()
|
||||
if (!doneNotified) onDone?.()
|
||||
} catch (e) {
|
||||
if ((e as Error).name === 'AbortError') return
|
||||
onError?.(e as Error)
|
||||
|
||||
Reference in New Issue
Block a user