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:
董宏凯andClaude Opus 4.8 committed 2026-06-18 14:26:27 +08:00
1 parent 500a562bb6
commit 32772b7448
20 files changed
+1396 -271

No files matched your search

@@ -15,6 +15,11 @@ import {
import { useStream } from '@langchain/langgraph-sdk/react';
import { getAPIClient } from '@/core/api';
import {
getStreamErrorMessage,
isThreadNotAccessibleError,
notifyThreadAccessChangedAndReload,
} from '@/core/threads/thread-access-error';
import { useCanvasGraphContext } from '../contexts/CanvasGraphContext';
import { saveAIWritingTranscript } from '../api/ai-writing-sessions';
@@ -186,10 +191,18 @@ const AIWritingStreamHost = memo(function AIWritingStreamHost({
},
onError(err: unknown) {
eventBatcherRef.current?.cancel();
dispatchRef.current?.({
type: 'set_error',
message: err instanceof Error ? err.message : String(err),
});
const message = getStreamErrorMessage(err);
// A "thread not accessible/not found" 404 almost always means the writing
// thread belongs to a previous login session — the user switched accounts
// (or this tab outlived a re-login), so the current account legitimately
// can't access the old thread. Instead of dumping the raw HTTP 404 into
// the panel, re-frame it around the current identity and reload — same UX
// the chat stream gives (see core/threads/hooks.ts).
if (isThreadNotAccessibleError(message)) {
notifyThreadAccessChangedAndReload();
return;
}
dispatchRef.current?.({ type: 'set_error', message });
},
onFinish(finishState) {
eventBatcherRef.current?.cancel();
@@ -291,7 +304,12 @@ export function AIWritingProvider({ children }: { children: ReactNode }) {
},
);
} catch (err) {
dispatch({ type: 'set_error', message: err instanceof Error ? err.message : String(err) });
const message = getStreamErrorMessage(err);
if (isThreadNotAccessibleError(message)) {
notifyThreadAccessChangedAndReload();
return;
}
dispatch({ type: 'set_error', message });
}
}, [state.status]);
@@ -372,7 +390,12 @@ export function AIWritingProvider({ children }: { children: ReactNode }) {
streamMode: ['values', 'updates', 'custom', 'messages-tuple'],
});
} catch (err) {
dispatch({ type: 'set_error', message: err instanceof Error ? err.message : String(err) });
const message = getStreamErrorMessage(err);
if (isThreadNotAccessibleError(message)) {
notifyThreadAccessChangedAndReload();
return;
}
dispatch({ type: 'set_error', message });
}
}, [state.sessionId, state.currentPause, state.materials, state.outline,
state.currentDraftTitle, state.currentDraftMarkdown, state.reviewResult,