You've already forked agentic-coding-workflow
fix(web): code review pass — scroll watch, SSE same-origin, store concurrency, modal reset, admin validation
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import { ref } from 'vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
export type ChatStreamEventType =
|
||||
@@ -26,65 +25,62 @@ export interface ChatStreamHandle {
|
||||
}
|
||||
|
||||
/**
|
||||
* useChatStream 通过 fetch + ReadableStream 自实现 SSE 解析,
|
||||
* startChatStream 通过 fetch + ReadableStream 自实现 SSE 解析,
|
||||
* 以支持 Authorization Bearer 头(EventSource 不支持自定义 header)。
|
||||
* start() 立即返回 stop 句柄,读流在后台 fire-and-forget。
|
||||
* 同步返回 stop 句柄,读流在后台 fire-and-forget。
|
||||
*
|
||||
* 仅接受同源 streamUrl —— 防止上游配置错误把 Bearer token 发到外域。
|
||||
*/
|
||||
export function useChatStream() {
|
||||
export function startChatStream(
|
||||
streamUrl: string,
|
||||
since: number,
|
||||
h: ChatStreamHandlers,
|
||||
): ChatStreamHandle {
|
||||
const auth = useAuthStore()
|
||||
const reading = ref(false)
|
||||
|
||||
function start(
|
||||
streamUrl: string,
|
||||
since: number,
|
||||
h: ChatStreamHandlers,
|
||||
): ChatStreamHandle {
|
||||
const url = new URL(streamUrl, window.location.origin)
|
||||
url.searchParams.set('since', String(since))
|
||||
const ctrl = new AbortController()
|
||||
reading.value = true
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const headers: Record<string, string> = { Accept: 'text/event-stream' }
|
||||
if (auth.token) headers['Authorization'] = `Bearer ${auth.token}`
|
||||
const resp = await fetch(url.toString(), {
|
||||
headers,
|
||||
signal: ctrl.signal,
|
||||
})
|
||||
if (!resp.ok || !resp.body) {
|
||||
throw new Error(`SSE HTTP ${resp.status}`)
|
||||
}
|
||||
const reader = resp.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buf = ''
|
||||
for (;;) {
|
||||
const { value, done } = await reader.read()
|
||||
if (done) break
|
||||
buf += decoder.decode(value, { stream: true })
|
||||
let idx = buf.indexOf('\n\n')
|
||||
while (idx >= 0) {
|
||||
const raw = buf.slice(0, idx)
|
||||
buf = buf.slice(idx + 2)
|
||||
const ev = parseSSE(raw)
|
||||
if (ev) h.onEvent(ev)
|
||||
idx = buf.indexOf('\n\n')
|
||||
}
|
||||
}
|
||||
h.onClose?.()
|
||||
} catch (err) {
|
||||
const e = err as { name?: string }
|
||||
if (e?.name === 'AbortError') return
|
||||
h.onError?.(err instanceof Error ? err : new Error(String(err)))
|
||||
} finally {
|
||||
reading.value = false
|
||||
}
|
||||
})()
|
||||
|
||||
return { stop: () => ctrl.abort() }
|
||||
const url = new URL(streamUrl, window.location.origin)
|
||||
if (url.origin !== window.location.origin) {
|
||||
throw new Error('refusing cross-origin SSE')
|
||||
}
|
||||
url.searchParams.set('since', String(since))
|
||||
|
||||
return { start, reading }
|
||||
const ctrl = new AbortController()
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const headers: Record<string, string> = { Accept: 'text/event-stream' }
|
||||
if (auth.token) headers['Authorization'] = `Bearer ${auth.token}`
|
||||
const resp = await fetch(url.toString(), {
|
||||
headers,
|
||||
signal: ctrl.signal,
|
||||
})
|
||||
if (!resp.ok || !resp.body) {
|
||||
throw new Error(`SSE HTTP ${resp.status}`)
|
||||
}
|
||||
const reader = resp.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buf = ''
|
||||
for (;;) {
|
||||
const { value, done } = await reader.read()
|
||||
if (done) break
|
||||
buf += decoder.decode(value, { stream: true })
|
||||
let idx = buf.indexOf('\n\n')
|
||||
while (idx >= 0) {
|
||||
const raw = buf.slice(0, idx)
|
||||
buf = buf.slice(idx + 2)
|
||||
const ev = parseSSE(raw)
|
||||
if (ev) h.onEvent(ev)
|
||||
idx = buf.indexOf('\n\n')
|
||||
}
|
||||
}
|
||||
h.onClose?.()
|
||||
} catch (err) {
|
||||
const e = err as { name?: string }
|
||||
if (e?.name === 'AbortError') return
|
||||
h.onError?.(err instanceof Error ? err : new Error(String(err)))
|
||||
}
|
||||
})()
|
||||
|
||||
return { stop: () => ctrl.abort() }
|
||||
}
|
||||
|
||||
function parseSSE(raw: string): ChatStreamEvent | null {
|
||||
|
||||
Reference in New Issue
Block a user