feat(dashboard): 大屏绘制智能体独立页面(按 taskId 存取,不分权)
- 新增带导航+侧边栏的独立页面 DashboardAgentPage:按 taskId 取/存会话, 有有效 report-json 直显大屏(DashboardApp),否则进生成过程(不自动跳)+输入框可改 - 后端 dashboard_sessions 存储(model/sql/__init__)+路由/by-task(GET/PUT 不分权) + deps/app/models 接线;新表靠 create_all 建,无迁移 - 扩展 useStep3Dashboard:buildInitialPrompt(输入任务JSON) + hydrateTranscript(恢复聊天记录) - 抽出共享 DashboardChatPanel;report-template 增 buildDashboardPromptFromJson - 输入 seam core/auth/dashboard-input:JSON 接口未就绪先用占位/演示数据兜底 - 路由 StrategyRoutes dashboard-agent + 侧边栏「问答管理→大屏绘制」 - 圆桌业务链卡片「关系图」改横向布局 - 顺带提交:AIWritingContext 登录切换处理、thread-access-error helper Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
500a562bb6
commit
32772b7448
20 files changed
+1396
-271
No files matched your search
@@ -0,0 +1,98 @@
|
||||
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")
|
||||
);
|
||||
}
|
||||
|
||||
/** 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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user