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 <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-13 03:20:38 +00:00
co-authored by Cursor
parent e0721a7944
commit 8032dce435
4 changed files with 51 additions and 9 deletions
+7 -1
View File
@@ -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 {
+32 -7
View File
@@ -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));
});
};
+3
View File
@@ -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,
+9 -1
View File
@@ -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),