diff --git a/web/src/api/chatStream.ts b/web/src/api/chatStream.ts index 70b921fa..255eade0 100644 --- a/web/src/api/chatStream.ts +++ b/web/src/api/chatStream.ts @@ -119,7 +119,12 @@ function parseFrame(frame: string): { event: StreamEvent | null; seq?: number } // SSE comment / heartbeat frames (": ...") have no data — ignore. if (!dataLine) return { event: null, seq }; - return { event: JSON.parse(dataLine) as StreamEvent, seq }; + try { + return { event: JSON.parse(dataLine) as StreamEvent, seq }; + } catch (err) { + console.warn('[sse] 忽略无法解析的帧:', dataLine.slice(0, 200), err); + return { event: null, seq }; + } } export function parseSSEChunk( diff --git a/web/src/api/taskRuns.ts b/web/src/api/taskRuns.ts index 68c37825..406c0717 100644 --- a/web/src/api/taskRuns.ts +++ b/web/src/api/taskRuns.ts @@ -37,6 +37,19 @@ export function sleepMs(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } +export class TaskRunHttpError extends Error { + status: number; + constructor(status: number) { + super(`subscribe task run failed: HTTP ${status}`); + this.name = 'TaskRunHttpError'; + this.status = status; + } +} + +function isNonRetryableStatus(status: number): boolean { + return status === 401 || status === 404; +} + /** 单次 SSE 读取;意外 EOF 时返回 { terminal:false, lastSeq } 供上层重连。 */ export async function readTaskRunSSEOnce( runId: string, @@ -51,7 +64,7 @@ export async function readTaskRunSSEOnce( signal, }); if (!resp.ok || !resp.body) { - throw new Error(`subscribe task run failed: HTTP ${resp.status}`); + throw new TaskRunHttpError(resp.status); } const reader = resp.body.getReader(); const decoder = new TextDecoder(); @@ -113,6 +126,9 @@ export async function subscribeTaskRunEvents( // 意外 EOF:未见到 done/error,按 after_seq 重连 } catch (err) { if (signal?.aborted) return; + if (err instanceof TaskRunHttpError && isNonRetryableStatus(err.status)) { + throw err; + } if (attempt >= maxReconnects) throw err; } if (attempt >= maxReconnects) {