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:
@@ -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
@@ -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));
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user