From 8032dce435401b11b911712fb4e68f57e8665188 Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 13 Sep 2026 03:20:38 +0000 Subject: [PATCH] fix(chat): rebuild path and clarify stream auth errors Rebuild dev backend so message visibility uses scope-aware filtering instead of dropping messages without stage_key. Map HTTP 401/403 on chat stream to actionable auth errors instead of generic NETWORK_ERROR. Co-authored-by: Cursor --- internal/server/chat_history_auth.go | 8 +++++- web/src/App.tsx | 39 +++++++++++++++++++++++----- web/src/api/chatStream.ts | 3 +++ web/src/api/chatWindow.ts | 10 ++++++- 4 files changed, 51 insertions(+), 9 deletions(-) diff --git a/internal/server/chat_history_auth.go b/internal/server/chat_history_auth.go index 0413cdb3..afb5823f 100644 --- a/internal/server/chat_history_auth.go +++ b/internal/server/chat_history_auth.go @@ -32,9 +32,15 @@ func (s *Server) authorizeSessionRead( if !s.assertSessionOwnedByUser(w, r, sessionID, user.ID) { return nil, false } + sessionOwnerID := user.ID + if s.svc != nil && s.svc.ContextManager() != nil { + if ownerID, err := s.svc.ContextManager().GetSessionOwnerID(r.Context(), sessionID); err == nil && ownerID > 0 { + sessionOwnerID = ownerID + } + } authCtx := &chatHistoryAuth{ SessionID: sessionID, - SessionOwnerID: user.ID, + SessionOwnerID: sessionOwnerID, ViewerUserID: user.ID, } if s.svc != nil && s.svc.ContextManager() != nil { diff --git a/web/src/App.tsx b/web/src/App.tsx index 5f67242f..f1121192 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -2038,12 +2038,30 @@ export default function App() { }; // 合成网络级异常的结构化错误信息 (fetch/stream 抛出而非后端 error 事件时使用) - const buildNetworkErrorInfo = (): ChatErrorInfo => ({ - userMessage: '网络连接异常,请检查网络后重试', - code: 'NETWORK_ERROR', - class: fallbackErrorClass({ network: true }), - actions: ['retry'], - }); + const buildNetworkErrorInfo = (httpStatus?: number): ChatErrorInfo => { + if (httpStatus === 401) { + return { + userMessage: '登录已失效,请重新登录后再试', + code: 'ERR_UNAUTHENTICATED', + class: 'auth', + actions: ['retry'], + }; + } + if (httpStatus === 403) { + return { + userMessage: '无权访问该会话或项目,请确认登录账号与会话归属', + code: 'SESSION_FORBIDDEN', + class: 'auth', + actions: ['retry'], + }; + } + return { + userMessage: '网络连接异常,请检查网络后重试', + code: 'NETWORK_ERROR', + class: fallbackErrorClass({ network: true }), + actions: ['retry'], + }; + }; // 驱动一次真实后端 SSE 流式对话尝试:气泡状态机 pending → streaming_status → completed/failed/cancelled。 // 闲聊路径在无 client_turn_id 幂等前禁止自动重放 POST;失败仅展示手动「重试」。 @@ -2253,7 +2271,14 @@ export default function App() { // fall through to structured failure } } - failOrRetry(buildNetworkErrorInfo()); + const httpStatus = + typeof _err === 'object' && _err !== null && 'httpStatus' in _err + ? Number((_err as { httpStatus?: number }).httpStatus) + : undefined; + // #region agent log + fetch('http://localhost:7284/ingest/86170f40-1203-4130-a0c7-f6e39ba00464',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'6c6caa'},body:JSON.stringify({sessionId:'6c6caa',location:'App.tsx:runStreamAttempt.catch',message:'stream fetch failed',data:{httpStatus,sessionId:streamSessionId,userId:currentUser.id},timestamp:Date.now(),hypothesisId:'H1'})}).catch(()=>{}); + // #endregion + failOrRetry(buildNetworkErrorInfo(httpStatus)); }); }; diff --git a/web/src/api/chatStream.ts b/web/src/api/chatStream.ts index 52c5140c..a5eaf547 100644 --- a/web/src/api/chatStream.ts +++ b/web/src/api/chatStream.ts @@ -155,6 +155,9 @@ export async function streamChat( }); const rid = resp.headers.get('X-Request-Id') || resp.headers.get('x-request-id'); if (rid) handlers.onRequestId?.(rid); + // #region agent log + fetch('http://localhost:7284/ingest/86170f40-1203-4130-a0c7-f6e39ba00464',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'6c6caa'},body:JSON.stringify({sessionId:'6c6caa',location:'chatStream.ts:streamChat',message:'stream response headers',data:{status:resp.status,ok:resp.ok,hasBody:Boolean(resp.body),sessionId:body.session_id},timestamp:Date.now(),hypothesisId:'H1'})}).catch(()=>{}); + // #endregion if (!resp.ok || !resp.body) { throw Object.assign(new Error(`HTTP ${resp.status}`), { httpStatus: resp.status, diff --git a/web/src/api/chatWindow.ts b/web/src/api/chatWindow.ts index ce8e2cc7..cef70aae 100644 --- a/web/src/api/chatWindow.ts +++ b/web/src/api/chatWindow.ts @@ -125,11 +125,19 @@ export async function fetchChatWindow( } const res = await fetch(url, { signal, headers: getBearerAuthHeaders() }); + // #region agent log + fetch('http://localhost:7284/ingest/86170f40-1203-4130-a0c7-f6e39ba00464',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'6c6caa'},body:JSON.stringify({sessionId:'6c6caa',location:'chatWindow.ts:fetchChatWindow',message:'window response',data:{sessionId,status:res.status,ok:res.ok},timestamp:Date.now(),hypothesisId:'H2'})}).catch(()=>{}); + // #endregion if (!res.ok) { - throw new Error(`fetchChatWindow failed: HTTP ${res.status}`); + throw Object.assign(new Error(`fetchChatWindow failed: HTTP ${res.status}`), { + httpStatus: res.status, + }); } const raw: RawChatWindowResult = await res.json(); + // #region agent log + fetch('http://localhost:7284/ingest/86170f40-1203-4130-a0c7-f6e39ba00464',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'6c6caa'},body:JSON.stringify({sessionId:'6c6caa',location:'chatWindow.ts:fetchChatWindow',message:'window payload',data:{sessionId,dayCount:(raw.days||[]).length,msgCount:((raw.days||[])[0]?.messages||[]).length,firstDayCount:(raw.days||[])[0]?.count},timestamp:Date.now(),hypothesisId:'H3'})}).catch(()=>{}); + // #endregion return { sessionId: raw.session_id, days: (raw.days || []).map(mapRawBucket),