From aeb9a554977991e175a83bce9788ab4b3f878b01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Tue, 1 Sep 2026 13:47:31 +0800 Subject: [PATCH] =?UTF-8?q?feat(agent-chat):=20=E4=BF=AE=E5=A4=8D=E6=99=BA?= =?UTF-8?q?=E8=83=BD=E4=BD=93=E4=BC=9A=E8=AF=9D=E5=BD=92=E5=B1=9E=E4=B8=8E?= =?UTF-8?q?=E5=8E=86=E5=8F=B2=E8=B7=AF=E7=94=B1=E6=81=A2=E5=A4=8D=EF=BC=9A?= =?UTF-8?q?=E4=BE=A7=E8=BE=B9=E6=A0=8F=E6=8C=89=E5=BD=93=E5=89=8Dagent?= =?UTF-8?q?=E8=BF=87=E6=BB=A4=E4=BC=9A=E8=AF=9D=E5=B9=B6=E6=90=BA=E5=B8=A6?= =?UTF-8?q?agent=E4=B8=8A=E4=B8=8B=E6=96=87=E8=B7=B3=E8=BD=AC=EF=BC=8C?= =?UTF-8?q?=E8=A1=A5=E5=85=85=E6=97=A7=E7=BA=BF=E7=A8=8B=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E5=9B=9E=E6=BA=AF=E5=88=B0=E6=99=BA=E8=83=BD=E4=BD=93=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E5=8C=BA=EF=BC=8C=E9=98=B2=E6=AD=A2=E8=AF=AF=E5=85=A5?= =?UTF-8?q?=E9=80=9A=E7=94=A8=E9=97=AE=E7=AD=94=EF=BC=8C=E5=B9=B6=E5=AE=8C?= =?UTF-8?q?=E5=96=84=E8=BE=93=E5=85=A5=E6=A1=86=E8=AE=B0=E5=BF=86=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E6=98=BE=E9=9A=90=E4=B8=8E=E4=BE=A7=E8=BE=B9=E6=A0=8F?= =?UTF-8?q?=E5=B1=95=E5=BC=80=E5=85=A5=E5=8F=A3=EF=BC=8C=E6=8F=90=E5=8D=87?= =?UTF-8?q?=E4=BD=93=E9=AA=8C=E4=B8=80=E8=87=B4=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workspace/agents/agent-chat-sidebar.tsx | 23 ++++-- .../src/components/workspace/input-box.tsx | 4 +- frontend-web/src/core/threads/utils.ts | 14 +++- frontend-web/src/pages/AgentChatPage.tsx | 55 +++++++++----- frontend-web/src/pages/WorkspaceRoutes.tsx | 74 ++++++++++++++++++- 5 files changed, 140 insertions(+), 30 deletions(-) diff --git a/frontend-web/src/components/workspace/agents/agent-chat-sidebar.tsx b/frontend-web/src/components/workspace/agents/agent-chat-sidebar.tsx index 45f39694..4ac99677 100644 --- a/frontend-web/src/components/workspace/agents/agent-chat-sidebar.tsx +++ b/frontend-web/src/components/workspace/agents/agent-chat-sidebar.tsx @@ -64,7 +64,7 @@ import { import { useDeleteThread, useRenameThread, - useThreads, + useThreadsPage, useToggleThreadFavorite, useToggleThreadPin, } from "@/core/threads/hooks"; @@ -351,8 +351,8 @@ export function AgentChatSidebar({ activeAgentId }: { activeAgentId?: string }) } /** - * 「最近的对话」面板(参考首页 GeneralQaSidebar 的「最近的对话」区块)。 - * 跨智能体的近期会话,点击打开;支持重命名/置顶/收藏/分享/导出/删除。 + * 当前智能体的「最近对话」面板(参考首页 GeneralQaSidebar 的「最近的对话」区块)。 + * 按 agent_id 在服务端过滤;支持重命名/置顶/收藏/分享/导出/删除。 */ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) { const { t } = useI18n(); @@ -361,7 +361,12 @@ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) { const pathOf = usePathOfThread(); const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>(); - const { data: threads = [] } = useThreads(); + const { data: threadPage } = useThreadsPage({ + page: 1, + pageSize: 50, + metadata: { agent_id: activeAgentId ?? "" }, + }); + const threads = threadPage?.threads ?? []; const { mutate: deleteThread } = useDeleteThread(); const { mutate: renameThread } = useRenameThread(); const { toggle: togglePin } = useToggleThreadPin(); @@ -373,8 +378,8 @@ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) { const [renameValue, setRenameValue] = useState(""); const [shareThreadId, setShareThreadId] = useState(null); - // 排除调度/会商/报告结构&写作配置向导等系统会话,与首页一致; - // 智能体创建(bootstrap)会话保留展示(带机器人图标、点击回创建页);置顶优先。 + // 服务端先按当前智能体过滤;客户端再排除调度/会商/报告结构&写作配置向导等 + // 系统会话。智能体创建(bootstrap)会话保留展示(带机器人图标、点击回创建页);置顶优先。 const visibleThreads = useMemo( () => sortThreadsPinnedFirst(threads.filter((thread) => !isHiddenFromRecentChats(thread))), [threads], @@ -479,7 +484,11 @@ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) { ) : ( filteredThreads.map((thread) => { const isBoot = isBootstrapThread(thread); - const href = isBoot ? bootstrapResumePath(thread.thread_id) : pathOf(thread); + // 正常记录依据自身保存的 agent_id 跳转;早期记录可能尚未写入 + // agent_id,此时保留当前智能体上下文,避免从智能体页误落到通用问答。 + const href = isBoot + ? bootstrapResumePath(thread.thread_id) + : pathOf(thread, { agent_id: activeAgentId }); const isActive = href === pathname; const pinned = isThreadPinned(thread); const favorited = isThreadFavorite(thread); diff --git a/frontend-web/src/components/workspace/input-box.tsx b/frontend-web/src/components/workspace/input-box.tsx index 4d0a0be8..3deab8dc 100644 --- a/frontend-web/src/components/workspace/input-box.tsx +++ b/frontend-web/src/components/workspace/input-box.tsx @@ -758,7 +758,7 @@ export function InputBox({ )} /> )} - {!frontendRagActive && !hideContextTools && !hideMemoryTool && 文件 } - {!frontendRagActive && !hideContextTools && | null; +}): string | undefined { + return pickAgentRef(thread.context, thread.metadata); +} + export function pathOfThread( thread: ThreadRouteTarget, context?: ThreadAgentRef | null, @@ -36,7 +48,7 @@ export function pathOfThread( const agentRef = typeof thread === "string" ? pickAgentRef(context, null) - : pickAgentRef(thread.context, thread.metadata); + : agentRefOfThread(thread) ?? pickAgentRef(context, null); return agentRef ? `${base}/agents/${encodeURIComponent(agentRef)}/chats/${threadId}` diff --git a/frontend-web/src/pages/AgentChatPage.tsx b/frontend-web/src/pages/AgentChatPage.tsx index 54f67752..57e0d7cb 100644 --- a/frontend-web/src/pages/AgentChatPage.tsx +++ b/frontend-web/src/pages/AgentChatPage.tsx @@ -3,6 +3,7 @@ import { BotIcon, Compass, + PanelLeftOpenIcon, PanelRightOpenIcon, PlusSquare, } from "lucide-react"; @@ -13,6 +14,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } fro import type { PromptInputMessage } from "@/components/ai-elements/prompt-input"; import { Button } from "@/components/ui/button"; +import { useSidebarSafe } from "@/components/ui/sidebar"; import { AgentChatSidebar } from "@/components/workspace/agents/agent-chat-sidebar"; import { AgentDetailPanel } from "@/components/workspace/agents/agent-detail-panel"; import { AgentQuestionChips } from "@/components/workspace/agents/agent-question-chips"; @@ -225,6 +227,7 @@ export default function AgentChatPage({ const { t } = useI18n(); const [showFollowups, setShowFollowups] = useState(false); const router = useRouter(); + const workspaceSidebar = useSidebarSafe(); const { agent_id: agentIdFromPath } = useParams<{ agent_id: string; @@ -806,6 +809,23 @@ export default function AgentChatPage({
{/* Header in normal flow, fixed height. */}
+ {/* 工作区左栏收起后,在智能体聊天区左上角提供展开入口。 */} + {showAgentList && + !isEmbedded && + workspaceSidebar && + !workspaceSidebar.open && ( + + )}
)} - - - {/* 右侧智能体详情面板收起时的「展开」入口:仅收起态显示 (展开态的「收起」按钮在面板右上角)。桌面端专用(lg+), 窄屏面板走抽屉交互;嵌入态面板固定渲染,不显示该开关。 */} @@ -919,6 +922,23 @@ export default function AgentChatPage({ enabled={tokenUsageEnabled} messages={displayThread.messages} /> + + +
@@ -1101,7 +1121,6 @@ export default function AgentChatPage({ onAskQuestion={(text) => handleSubmit({ text, files: [] })} collapsed={detailCollapsed} onCollapsedChange={setDetailCollapsed} - showRecentChats={!taskCtxOn} compactAppearanceActions={compactAppearanceActions} /> diff --git a/frontend-web/src/pages/WorkspaceRoutes.tsx b/frontend-web/src/pages/WorkspaceRoutes.tsx index d6edf46a..cf1cb3dc 100644 --- a/frontend-web/src/pages/WorkspaceRoutes.tsx +++ b/frontend-web/src/pages/WorkspaceRoutes.tsx @@ -1,7 +1,8 @@ -import type { CSSProperties, ReactNode } from 'react'; +import { useEffect, useState, type CSSProperties, type ReactNode } from 'react'; import { Navigate, Route, Routes, useLocation, useParams } from 'react-router-dom'; import { getStoredAuth } from '@/core/auth'; +import { getAPIClient } from '@/core/api'; import { isSentimentAgentVisible } from '@/core/sentiment-agent/config'; import { resolveChatsIframeEntryRedirect, @@ -25,6 +26,7 @@ import { ArtifactsProvider } from '@/components/workspace/artifacts'; import { DocumentRewritePresentationProvider } from '@/core/document-rewrite/context'; import { RouteBaseProvider } from '@/core/route-base'; +import { agentRefOfThread } from '@/core/threads/utils'; import ChatsPage from './ChatsPage'; import ChatPage from './ChatPage'; @@ -341,12 +343,80 @@ function ChatThreadEntryRoute({ return ( - + ); } +/** + * Older bookmarks and a few historic list entries use the generic + * ``/chats/:thread_id`` path even for an agent-owned conversation. Recover the + * owning agent from the persisted thread record before rendering so those + * links keep the agent workspace (agent list and detail panel) instead of + * falling back to the general Q&A page. Task workspace threads intentionally + * remain on their explicit task routes because their surrounding layout is + * part of the task context. + */ +function AgentAwareChatThreadContent() { + const { thread_id: threadId } = useParams<{ thread_id: string }>(); + const location = useLocation(); + const [agentId, setAgentId] = useState(null); + const [resolved, setResolved] = useState(false); + + useEffect(() => { + let cancelled = false; + setAgentId(null); + setResolved(false); + + if (!threadId) { + setResolved(true); + return () => { + cancelled = true; + }; + } + + void getAPIClient() + .threads.get(threadId) + .then((thread) => { + if (cancelled) return; + const record = thread as { + context?: { agent_id?: string; agent_name?: string } | null; + metadata?: Record | null; + }; + // Task-bound threads need their task host to preserve the purpose gate, + // action partitioning, and other task-specific controls. + const isTaskThread = typeof record.metadata?.taskId === 'string'; + setAgentId(isTaskThread ? null : agentRefOfThread(record) ?? null); + }) + .catch(() => { + if (!cancelled) setAgentId(null); + }) + .finally(() => { + if (!cancelled) setResolved(true); + }); + + return () => { + cancelled = true; + }; + }, [threadId]); + + if (!resolved) { + return
; + } + + if (threadId && agentId) { + return ( + + ); + } + + return ; +} + /** * `chats_iframe` is reserved for the Brainy postMessage protocol. `from=iframe` * and `chatMode=iframe` opt into this page; ordinary iframe/Wujie embedding