From 21049607f46bb79177aa31821c15f54ec6dabd64 Mon Sep 17 00:00:00 2001 From: weichen Date: Fri, 18 Sep 2026 03:44:08 +0000 Subject: [PATCH] =?UTF-8?q?fix(web):=20SSE=20=E9=87=8D=E8=BF=9E=20401/404?= =?UTF-8?q?=20fail-fast=EF=BC=8C=E5=8D=95=E5=B8=A7=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E5=AE=B9=E9=94=99=EF=BC=88R-7=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TaskRunHttpError 携带 status;非法 JSON 帧跳过并保留 seq。 Co-authored-by: Cursor --- web/src/api/chatStream.ts | 7 ++++++- web/src/api/taskRuns.ts | 18 +++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) 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) {