席位流程图按已提交 Markdown 对齐;流式未命名产物不再误请求 artifacts API,并兼容临时文件名路径。同时补强会话流中断识别与 8BF/岗位会商展示。 Co-authored-by: Cursor <cursoragent@cursor.com>
130 lines
4.5 KiB
TypeScript
130 lines
4.5 KiB
TypeScript
import { toast } from "sonner";
|
|
|
|
import { getStoredAuth } from "@/core/auth";
|
|
|
|
/**
|
|
* Shared helpers for re-framing LangGraph "thread not accessible" 404s around
|
|
* the current login identity.
|
|
*
|
|
* The most common cause in practice is a login session change: a thread is
|
|
* owned by the previously logged-in account, so after switching accounts the
|
|
* backend (``app/gateway/authz.py``) correctly returns
|
|
* ``HTTP 404: {"detail":"Thread <uuid> not accessible"}``. Raw-toasting that
|
|
* 404 just leaves the user staring at a scary error. Both the chat stream
|
|
* (``core/threads/hooks.ts``) and the AI-writing stream
|
|
* (``open-canvas/contexts/AIWritingContext.tsx``) funnel their stream errors
|
|
* through here so the UX is identical: detect the login-change 404 and prompt
|
|
* the user to refresh against the current identity instead of erroring out.
|
|
*/
|
|
|
|
/** Normalize an unknown stream error into a human-readable message string. */
|
|
export function getStreamErrorMessage(error: unknown): string {
|
|
if (typeof error === "string" && error.trim()) {
|
|
return error;
|
|
}
|
|
if (error instanceof Error && error.message.trim()) {
|
|
return error.message;
|
|
}
|
|
if (typeof error === "object" && error !== null) {
|
|
const message = Reflect.get(error, "message");
|
|
if (typeof message === "string" && message.trim()) {
|
|
return message;
|
|
}
|
|
const nestedError = Reflect.get(error, "error");
|
|
if (nestedError instanceof Error && nestedError.message.trim()) {
|
|
return nestedError.message;
|
|
}
|
|
if (typeof nestedError === "string" && nestedError.trim()) {
|
|
return nestedError;
|
|
}
|
|
}
|
|
return "Request failed.";
|
|
}
|
|
|
|
/**
|
|
* Detect "thread not found" / forbidden errors from the LangGraph SDK.
|
|
*
|
|
* Backend returns 404 both when a thread truly doesn't exist and when the
|
|
* currently authenticated user doesn't own it ("not found" = no row + no
|
|
* checkpoint, "not accessible" = row owned by another user / stale login
|
|
* session). Both mean the current user can't use this thread.
|
|
*/
|
|
export function isThreadNotAccessibleError(message: string): boolean {
|
|
const lowered = message.toLowerCase();
|
|
if (/thread\s+[\w-]{8,}\s+not\s+(found|accessible)/i.test(message)) {
|
|
return true;
|
|
}
|
|
// Fallback: a bare 404/403 from the threads endpoint when the SDK has
|
|
// wrapped the detail differently.
|
|
return (
|
|
(lowered.includes("http 404") || lowered.includes("http 403")) &&
|
|
lowered.includes("thread")
|
|
);
|
|
}
|
|
|
|
export function isAbortLikeError(error: unknown): boolean {
|
|
if (error instanceof DOMException && error.name === "AbortError") {
|
|
return true;
|
|
}
|
|
if (error instanceof Error && error.name === "AbortError") {
|
|
return true;
|
|
}
|
|
const message = getStreamErrorMessage(error).toLowerCase();
|
|
return (
|
|
message.includes("aborterror") ||
|
|
message.includes("the operation was aborted") ||
|
|
message.includes("bodystreambuffer")
|
|
);
|
|
}
|
|
|
|
export function isTransientStreamTransportError(message: string): boolean {
|
|
const lowered = message.toLowerCase();
|
|
return (
|
|
/failed to fetch/i.test(message) ||
|
|
lowered.includes("networkerror") ||
|
|
lowered.includes("network error") ||
|
|
lowered.includes("load failed") ||
|
|
lowered.includes("err_connection") ||
|
|
lowered.includes("err_incomplete") ||
|
|
lowered.includes("err_network") ||
|
|
lowered.includes("econnreset") ||
|
|
lowered.includes("econnaborted") ||
|
|
/fetch failed/i.test(message)
|
|
);
|
|
}
|
|
|
|
/** Build a label for the *current* logged-in identity (email → user_id → 未知). */
|
|
export function buildCurrentUserLabel(): string {
|
|
const auth = getStoredAuth();
|
|
if (!auth) {
|
|
return "未登录";
|
|
}
|
|
const email = auth.email?.trim();
|
|
if (email) {
|
|
return email;
|
|
}
|
|
const userId = auth.user_id?.trim();
|
|
return userId ? userId : "未知用户";
|
|
}
|
|
|
|
/**
|
|
* Re-frame a login-change 404 around the current identity and reload the page.
|
|
*
|
|
* Shows a friendly toast naming the current login, then full-reloads after a
|
|
* short delay so the toast is visible and so TanStack Query cache / context
|
|
* providers / any state captured under the previous user get rebuilt against
|
|
* the current login. Used by streams whose session id lives in component state
|
|
* (not the URL) — a plain reload lands them back on a fresh, valid page.
|
|
*/
|
|
export function notifyThreadAccessChangedAndReload(): void {
|
|
const userLabel = buildCurrentUserLabel();
|
|
toast.info(
|
|
`当前登录用户为 ${userLabel},原会话不存在或无权访问,正在为你刷新页面…`,
|
|
);
|
|
if (typeof window !== "undefined") {
|
|
window.setTimeout(() => {
|
|
window.location.reload();
|
|
}, 900);
|
|
}
|
|
}
|