Merge branch 'dhk-1' into 'master'
feat(ai-writing): 进入写作后自动收起研究模板侧栏 See merge request knowledge-base-bj/deerflow-server!129
This commit is contained in:
commit
e621ce88e6
6 files changed
+179
-67
No files matched your search
@@ -84,7 +84,7 @@ The frontend (`frontend-web/`) is a React 19 + Vite SPA:
|
||||
- **LLM streaming**: `@langchain/langgraph-sdk` streams messages from the backend via SSE
|
||||
- **UI**: Radix UI primitives + TailwindCSS v4 + shadcn-style components in `src/components/ui/`
|
||||
- **Shims**: `src/shims/` provides `next/link` and `next/navigation` polyfills (original code was Next.js)
|
||||
- **Login URL params** (`LoginPage.tsx`, frontend-only — the backend never sees these): besides the auth params (`ticket`/`userId`/`env`/`theme`/`yUserId`/`token`/`access_token`/`authToken`/`password`), the page supports a **task deep-link** via `?taskId=&goPath=`. After login it calls the consumer API `GET {VITE_AIPI_CONSUMER_URL}/taskAnalysSearch/cop-task-detail?taskId=` (header `Authorization: Bearer admin`, auto-injected by `utils/request.ts`), stashes the full `data` (`id/overview/taskDirection/taskName/taskContent`) into `sessionStorage["login.copTaskDetail"]`, then routes by `goPath`: base codes `3qfx`/`rwfx`/`xdfx` → that business's configured **single agent** Q&A (resolved via `/api/business-mapping`, opening message in `sessionStorage.pendingAgentMessage`); the `-A` suffix (`3qfx-A`/…) → **multi-agent roundtable** with `roundtablePrefill.businessCode` so the page auto-selects the configured 业务链条. goPath→businessCode: `3qfx`→`3Q`, `rwfx`→`6BF`, `xdfx`→`7BF`. Any failure (no consumer URL / fetch error / unmapped goPath / no configured agent) silently falls back to the default home. Helpers: `core/auth/task-deeplink.ts`.
|
||||
- **Login URL params** (`LoginPage.tsx`, frontend-only — the backend never sees these): besides the auth params (`ticket`/`userId`/`env`/`theme`/`yUserId`/`token`/`access_token`/`authToken`/`password`), the page supports a **task deep-link** via `?taskId=&goPath=`. After login it calls the consumer API `GET {VITE_AIPI_CONSUMER_URL}/taskAnalysSearch/cop-task-detail?taskId=` (header `Authorization: Bearer admin`, auto-injected by `utils/request.ts`), stashes the full `data` (`id/overview/taskDirection/taskName/taskContent`) into `sessionStorage["login.copTaskDetail"]`, then routes by `goPath`: base codes `3qfx`/`rwfx`/`xdfx` → that business's configured **single agent** Q&A (resolved via `/api/business-mapping`, opening message in `sessionStorage.pendingAgentMessage`); the `-A` suffix (`3qfx-A`/…) → **multi-agent roundtable** with `roundtablePrefill.businessCode` so the page auto-selects the configured 业务链条. goPath→businessCode: `3qfx`→`3Q`, `rwfx`→`6BF`, `xdfx`→`7BF`. **Both targets render embedded under the 任务管理 layout** — nested routes under `task-management/<business-path>/{roundtable,agent/:agent_id/chats/:thread_id}` in `StrategyRoutes.tsx` (`<business-path>` = `plan-task-sentiment` / `plan-task/plan-task-fx` / `plan-task/plan-task-action`, matching the COH中任务 menu paths in `sidebar-menu.ts`) so the global sidebar keeps 任务管理 → COH中任务 → the matching business highlighted. The agent chat is rendered via `TaskEmbeddedAgentChat`, which passes `AgentChatPage`'s `chatBasePath` prop so the post-send URL rewrite + new-chat button stay in the task-management context (instead of the standalone `/agents/...` route). Any failure (no consumer URL / fetch error / unmapped goPath / no configured agent) silently falls back to the default home. Helpers: `core/auth/task-deeplink.ts`.
|
||||
|
||||
### Configuration
|
||||
|
||||
|
||||
@@ -86,12 +86,22 @@ function AppProviders({ children }: { children: React.ReactNode }) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根路由 `/` → `/login` 重定向,**保留 query string**。
|
||||
* 外部系统的任务深链落在 `#/?taskId=...&goPath=...`,裸 `<Navigate to="/login">` 会丢掉
|
||||
* query,导致 LoginPage 读不到参数;这里把 search 一并带过去。
|
||||
*/
|
||||
function RootRedirect() {
|
||||
const { search } = useLocation();
|
||||
return <Navigate to={`/login${search}`} replace />;
|
||||
}
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<HashRouter>
|
||||
<AppProviders>
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="/login" replace />} />
|
||||
<Route path="/" element={<RootRedirect />} />
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/login/:username" element={<LoginPage />} />
|
||||
<Route path="/landing" element={<LandingPage />} />
|
||||
|
||||
@@ -37,6 +37,16 @@ const GOPATH_BUSINESS_CODE: Record<string, string> = {
|
||||
xdfx: "7BF", // 行动分析(XDFX)
|
||||
};
|
||||
|
||||
/**
|
||||
* 业务码 → 「任务管理」布局下的路由前缀(与 sidebar-menu.ts 的 COH 中任务菜单路径一致)。
|
||||
* 会商 / 智能体问答都挂在此前缀下,保证侧边栏停留在 任务管理 → COH中任务 → 对应业务 高亮。
|
||||
*/
|
||||
const BUSINESS_TASK_BASE: Record<string, string> = {
|
||||
"3Q": "/page/strategy/task-management/plan-task-sentiment",
|
||||
"6BF": "/page/strategy/task-management/plan-task/plan-task-fx",
|
||||
"7BF": "/page/strategy/task-management/plan-task/plan-task-action",
|
||||
};
|
||||
|
||||
export interface ParsedGoPath {
|
||||
businessCode: string;
|
||||
/** true = 多智能体会商(``-A`` 后缀);false = 单智能体问答。 */
|
||||
@@ -125,10 +135,13 @@ export async function resolveTaskDeeplinkTarget(
|
||||
): Promise<TaskDeeplinkTarget | null> {
|
||||
const parsed = parseGoPath(goPath);
|
||||
if (!parsed) return null;
|
||||
const taskBase = BUSINESS_TASK_BASE[parsed.businessCode];
|
||||
if (!taskBase) return null;
|
||||
|
||||
if (parsed.roundtable) {
|
||||
// 多智能体会商:嵌进对应业务的「任务管理」布局;businessCode 让会商页自动选用配置的业务链条。
|
||||
return {
|
||||
path: "/page/strategy/qa/multi-agent-roundtable",
|
||||
path: `${taskBase}/roundtable`,
|
||||
state: {
|
||||
roundtablePrefill: {
|
||||
taskTitle: detail.taskName,
|
||||
@@ -157,5 +170,6 @@ export async function resolveTaskDeeplinkTarget(
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
return { path: `/page/strategy/agents/${agentId}/chats/new` };
|
||||
// 单智能体问答:同样嵌进对应业务的「任务管理」布局。
|
||||
return { path: `${taskBase}/agent/${agentId}/chats/new` };
|
||||
}
|
||||
@@ -217,6 +217,81 @@ function AIWritingLayout({ hideAdvancedFields, initialNotebookId, initialDocumen
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写作页主体(侧栏 + 内容区)。放在 Provider 内部,以便读 useAIWriting 状态:
|
||||
* 进入写作(status !== idle 或查看历史)后自动收起左侧研究模板栏,让写作区更宽。
|
||||
*/
|
||||
function AIWritingBody({ hideAdvancedFields, initialNotebookId, initialDocumentIds, studioToken }: { hideAdvancedFields: boolean } & NotebookProps) {
|
||||
const { state } = useAIWriting();
|
||||
const { isHistoryMode } = useAIWritingHistory();
|
||||
|
||||
// 需求 0616 #5:左栏点「笔记本 / 所有空间」→ 在右侧内嵌打开对应页,替换右侧内容
|
||||
// (写作布局不卸载,仅 display:none 隐藏,避免 WritingSetupChat 重挂丢线程)。
|
||||
const [rightView, setRightView] = useState<RightView>(null);
|
||||
const overlayActive = rightView != null;
|
||||
const viewingNotebookId = rightView?.kind === 'notebook' ? rightView.id : null;
|
||||
|
||||
// 进入写作(开始生成 / 查看历史)后收起研究模板侧栏;嵌入态(studioToken)始终不展示。
|
||||
const showSidebar = !studioToken && state.status === 'idle' && !isHistoryMode;
|
||||
|
||||
return (
|
||||
<div className="flex-1 min-h-0 overflow-hidden flex">
|
||||
{/* 需求 0616 #5:课题研究左栏(研究模板 + 笔记本)。进入写作后收起。 */}
|
||||
{showSidebar && (
|
||||
<ResearchSidebar
|
||||
viewingNotebookId={viewingNotebookId}
|
||||
onOpenNotebook={(id) => setRightView({ kind: 'notebook', id })}
|
||||
onOpenAllSpaces={() => setRightView({ kind: 'allSpaces' })}
|
||||
/>
|
||||
)}
|
||||
{/* 右侧:写作布局常驻(仅 display:none 隐藏,避免 WritingSetupChat 重挂丢线程),
|
||||
查看笔记本 / 所有空间时在其上叠加内嵌页。 */}
|
||||
<div className="relative flex-1 min-h-0 overflow-hidden">
|
||||
<div className={cn('absolute inset-0', overlayActive && 'hidden')}>
|
||||
<AIWritingLayout hideAdvancedFields={hideAdvancedFields} initialNotebookId={initialNotebookId} initialDocumentIds={initialDocumentIds} studioToken={studioToken} />
|
||||
</div>
|
||||
{overlayActive && (
|
||||
<div className="absolute inset-0 flex flex-col bg-background">
|
||||
<div className="flex shrink-0 items-center gap-1 border-b border-border px-4 py-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRightView(null)}
|
||||
className="flex items-center gap-1.5 rounded-md px-2 py-1 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
>
|
||||
<ArrowLeft className="size-4" />
|
||||
返回写作
|
||||
</button>
|
||||
{rightView.kind === 'notebook' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRightView({ kind: 'allSpaces' })}
|
||||
className="ml-1 rounded-md px-2 py-1 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
>
|
||||
所有空间
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-hidden">
|
||||
{rightView.kind === 'notebook' ? (
|
||||
// 笔记本详情页内部用 ChatBox(useArtifacts) → 需 ChatRuntime 提供 Provider。
|
||||
<ChatRuntime>
|
||||
<StudioNotebookDetailPage key={rightView.id} notebookId={rightView.id} />
|
||||
</ChatRuntime>
|
||||
) : (
|
||||
<div className="h-full overflow-y-auto">
|
||||
<StudioNotebooksPage
|
||||
onOpenNotebook={(id) => setRightView({ kind: 'notebook', id })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AIWritingPage({
|
||||
hideAdvancedFields = false,
|
||||
initialNotebookId,
|
||||
@@ -228,12 +303,6 @@ export default function AIWritingPage({
|
||||
initialDocumentIds?: number[];
|
||||
studioToken?: string;
|
||||
} = {}) {
|
||||
// 需求 0616 #5:左栏点「笔记本 / 所有空间」→ 在右侧内嵌打开对应页,替换右侧内容
|
||||
// (写作布局不卸载,仅 display:none 隐藏,避免 WritingSetupChat 重挂丢线程)。
|
||||
const [rightView, setRightView] = useState<RightView>(null);
|
||||
const overlayActive = rightView != null;
|
||||
const viewingNotebookId = rightView?.kind === 'notebook' ? rightView.id : null;
|
||||
|
||||
return (
|
||||
<CanvasThreadProvider>
|
||||
<CanvasGraphProvider>
|
||||
@@ -247,60 +316,12 @@ export default function AIWritingPage({
|
||||
>
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<div className="h-full flex flex-col overflow-hidden bg-background canvas-writing-bg">
|
||||
<div className="flex-1 min-h-0 overflow-hidden flex">
|
||||
{/* 需求 0616 #5:课题研究左栏(研究模板 + 笔记本)。studio 嵌入态(studioToken)隐藏。 */}
|
||||
{!studioToken && (
|
||||
<ResearchSidebar
|
||||
viewingNotebookId={viewingNotebookId}
|
||||
onOpenNotebook={(id) => setRightView({ kind: 'notebook', id })}
|
||||
onOpenAllSpaces={() => setRightView({ kind: 'allSpaces' })}
|
||||
/>
|
||||
)}
|
||||
{/* 右侧:写作布局常驻(仅 display:none 隐藏,避免 WritingSetupChat 重挂丢线程),
|
||||
查看笔记本 / 所有空间时在其上叠加内嵌页。 */}
|
||||
<div className="relative flex-1 min-h-0 overflow-hidden">
|
||||
<div className={cn('absolute inset-0', overlayActive && 'hidden')}>
|
||||
<AIWritingLayout hideAdvancedFields={hideAdvancedFields} initialNotebookId={initialNotebookId} initialDocumentIds={initialDocumentIds} studioToken={studioToken} />
|
||||
</div>
|
||||
{overlayActive && (
|
||||
<div className="absolute inset-0 flex flex-col bg-background">
|
||||
<div className="flex shrink-0 items-center gap-1 border-b border-border px-4 py-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRightView(null)}
|
||||
className="flex items-center gap-1.5 rounded-md px-2 py-1 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
>
|
||||
<ArrowLeft className="size-4" />
|
||||
返回写作
|
||||
</button>
|
||||
{rightView.kind === 'notebook' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRightView({ kind: 'allSpaces' })}
|
||||
className="ml-1 rounded-md px-2 py-1 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
>
|
||||
所有空间
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-hidden">
|
||||
{rightView.kind === 'notebook' ? (
|
||||
// 笔记本详情页内部用 ChatBox(useArtifacts) → 需 ChatRuntime 提供 Provider。
|
||||
<ChatRuntime>
|
||||
<StudioNotebookDetailPage key={rightView.id} notebookId={rightView.id} />
|
||||
</ChatRuntime>
|
||||
) : (
|
||||
<div className="h-full overflow-y-auto">
|
||||
<StudioNotebooksPage
|
||||
onOpenNotebook={(id) => setRightView({ kind: 'notebook', id })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<AIWritingBody
|
||||
hideAdvancedFields={hideAdvancedFields}
|
||||
initialNotebookId={initialNotebookId}
|
||||
initialDocumentIds={initialDocumentIds}
|
||||
studioToken={studioToken}
|
||||
/>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</WritingFormProvider>
|
||||
|
||||
@@ -33,10 +33,16 @@ import { desensitize } from "@/lib/desensitize";
|
||||
|
||||
export default function AgentChatPage({
|
||||
embeddedAgentId,
|
||||
chatBasePath,
|
||||
}: {
|
||||
/** 当作为「通用问答右侧嵌入」渲染时由父组件传入的 agent id(替代路由参数)。
|
||||
* 传入即视为嵌入态:路由仍停留在 /chats/new,仅右侧问答区换成该智能体的对话。 */
|
||||
embeddedAgentId?: string;
|
||||
/** 智能体对话所在的路由前缀(到 `/chats` 为止,不含 thread_id)。
|
||||
* 默认 `${routeBase}/agents/${agent_id}/chats`;任务深链把对话嵌进「任务管理」布局时,
|
||||
* 传入如 `/page/strategy/task-management/plan-task-sentiment/agent/${id}/chats`,
|
||||
* 使开新会话 / 首条消息后的 URL 改写都停留在任务管理上下文(侧边栏保持对应业务高亮)。 */
|
||||
chatBasePath?: string;
|
||||
} = {}) {
|
||||
const routeBase = useRouteBase();
|
||||
const { t } = useI18n();
|
||||
@@ -48,6 +54,8 @@ export default function AgentChatPage({
|
||||
}>();
|
||||
const agent_id = embeddedAgentId ?? agentIdFromPath;
|
||||
const isEmbedded = !!embeddedAgentId;
|
||||
// 对话路由前缀:调用方可覆盖(任务深链嵌入态),否则用当前 routeBase 下的独立智能体对话路径。
|
||||
const agentChatsBase = chatBasePath ?? `${routeBase}/agents/${agent_id}/chats`;
|
||||
|
||||
// If ChatPage stashed an opening message for this agent, skip the landing
|
||||
// screen on first render so the user doesn't see a flash of the welcome UI
|
||||
@@ -100,7 +108,7 @@ export default function AgentChatPage({
|
||||
history.replaceState(
|
||||
null,
|
||||
"",
|
||||
`${window.location.pathname}${window.location.search}#${routeBase}/agents/${agent_id}/chats/${createdThreadId}`,
|
||||
`${window.location.pathname}${window.location.search}#${agentChatsBase}/${createdThreadId}`,
|
||||
);
|
||||
},
|
||||
onFinish: (state) => {
|
||||
@@ -221,7 +229,7 @@ export default function AgentChatPage({
|
||||
router.push(
|
||||
isEmbedded
|
||||
? `/page/workspace/chats/new?agent=${agent_id}`
|
||||
: `${routeBase}/agents/${agent_id}/chats/new`,
|
||||
: `${agentChatsBase}/new`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Navigate, Route, Routes } from "react-router-dom";
|
||||
import { Navigate, Route, Routes, useParams } from "react-router-dom";
|
||||
import { Toaster } from "sonner";
|
||||
|
||||
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||
@@ -50,6 +50,20 @@ import AdminScheduledTasksPage from "./AdminScheduledTasksPage";
|
||||
import AdminUserActivityPage from "./AdminUserActivityPage";
|
||||
import { ChatRuntime } from "./WorkspaceRoutes";
|
||||
|
||||
/**
|
||||
* 任务深链:把智能体问答嵌进「任务管理」布局。读路由参数 agent_id,给 AgentChatPage
|
||||
* 传入以 taskBase 为前缀的对话路由(开新会话 / URL 改写都停留在任务管理上下文,
|
||||
* 侧边栏保持对应业务高亮)。
|
||||
*/
|
||||
function TaskEmbeddedAgentChat({ taskBase }: { taskBase: string }) {
|
||||
const { agent_id } = useParams<{ agent_id: string }>();
|
||||
return (
|
||||
<ChatRuntime>
|
||||
<AgentChatPage chatBasePath={`${taskBase}/agent/${agent_id}/chats`} />
|
||||
</ChatRuntime>
|
||||
);
|
||||
}
|
||||
|
||||
export default function StrategyRoutes() {
|
||||
return (
|
||||
<RouteBaseProvider value="/page/strategy">
|
||||
@@ -113,6 +127,21 @@ export default function StrategyRoutes() {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{/* 任务深链(3qfx / 3qfx-A):把智能体问答 / 会商嵌进 3QFX 业务上下文。 */}
|
||||
<Route
|
||||
path="plan-task-sentiment/roundtable"
|
||||
element={
|
||||
<ChatRuntime>
|
||||
<RoundtablePlanningPage />
|
||||
</ChatRuntime>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="plan-task-sentiment/agent/:agent_id/chats/:thread_id"
|
||||
element={
|
||||
<TaskEmbeddedAgentChat taskBase="/page/strategy/task-management/plan-task-sentiment" />
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="plan-task/plan-task-fx"
|
||||
element={
|
||||
@@ -123,6 +152,21 @@ export default function StrategyRoutes() {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{/* 任务深链(rwfx / rwfx-A):嵌进 任务FX 业务上下文。 */}
|
||||
<Route
|
||||
path="plan-task/plan-task-fx/roundtable"
|
||||
element={
|
||||
<ChatRuntime>
|
||||
<RoundtablePlanningPage />
|
||||
</ChatRuntime>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="plan-task/plan-task-fx/agent/:agent_id/chats/:thread_id"
|
||||
element={
|
||||
<TaskEmbeddedAgentChat taskBase="/page/strategy/task-management/plan-task/plan-task-fx" />
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="plan-task/plan-task-action"
|
||||
element={
|
||||
@@ -133,6 +177,21 @@ export default function StrategyRoutes() {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{/* 任务深链(xdfx / xdfx-A):嵌进 XDFX 业务上下文。 */}
|
||||
<Route
|
||||
path="plan-task/plan-task-action/roundtable"
|
||||
element={
|
||||
<ChatRuntime>
|
||||
<RoundtablePlanningPage />
|
||||
</ChatRuntime>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="plan-task/plan-task-action/agent/:agent_id/chats/:thread_id"
|
||||
element={
|
||||
<TaskEmbeddedAgentChat taskBase="/page/strategy/task-management/plan-task/plan-task-action" />
|
||||
}
|
||||
/>
|
||||
<Route path="mnty-task-mechanism" element={<TaskTyFrom defaultPage="iframeXdty" title="XDTY任务" />} />
|
||||
<Route path="mnty-task-plan" element={<TaskTyFrom defaultPage="iframeFaty" title="方案TY任务" />} />
|
||||
<Route path="mnty-task-zsty" element={<TaskTyFrom defaultPage="iframeSysystem" title="XXTY任务" />} />
|
||||
|
||||
Reference in new issue
Block a user