diff --git a/mateclaw-ui/src/utils/chatRouteHydration.ts b/mateclaw-ui/src/utils/chatRouteHydration.ts new file mode 100644 index 00000000..78c17eee --- /dev/null +++ b/mateclaw-ui/src/utils/chatRouteHydration.ts @@ -0,0 +1,25 @@ +type IdLike = string | number + +interface RouteHydrationAgent { + id: IdLike +} + +interface RouteHydrationConversation { + conversationId: string +} + +export function resolveRouteHydrationQuery(options: { + routeAgentId?: string + routeConversationId?: string + agents: RouteHydrationAgent[] + conversations: RouteHydrationConversation[] +}): { agentId: string; conversationId: string } { + let agentId = options.routeAgentId || '' + const conversationId = options.routeConversationId || '' + + if (agentId && options.agents.length > 0 && !options.agents.some(a => String(a.id) === agentId)) { + agentId = '' + } + + return { agentId, conversationId } +} diff --git a/mateclaw-ui/src/views/ChatConsole.vue b/mateclaw-ui/src/views/ChatConsole.vue index 5166d99d..5c2ad122 100644 --- a/mateclaw-ui/src/views/ChatConsole.vue +++ b/mateclaw-ui/src/views/ChatConsole.vue @@ -291,6 +291,7 @@ import { useChat } from '@/composables/chat/useChat' import RunOverviewPanel from '@/components/chat/RunOverviewPanel.vue' import { reconstructErrorInfo } from '@/types/chatError' import { reconcileMessages, extractMessages } from '@/utils/messageReconcile' +import { resolveRouteHydrationQuery } from '@/utils/chatRouteHydration' import type { Conversation, Agent, ModelConfig, ProviderInfo, ActiveModelsInfo, ChatAttachment, MessageContentPart, Message, ToolCallMeta } from '@/types' // 导入组件化组件 @@ -1617,24 +1618,12 @@ async function refreshCurrentConversationMessages(conversationId: string) { } async function hydrateStateFromRoute() { - let agentId = route.query.agentId ? String(route.query.agentId) : '' - let conversationId = String(route.query.conversationId || '') - - // The URL can outlive its workspace: switching workspaces remounts this view - // (via the router-view key) but keeps the query string, so agentId / - // conversationId may still point at entities of the previous workspace. An - // agentId missing from the workspace-scoped agent list is such a leftover — - // drop it so the default-select below picks a real employee instead of the - // picker rendering the unresolvable raw id. - if (agentId && agents.value.length > 0 && !agents.value.some(a => String(a.id) === agentId)) { - agentId = '' - // Only follow the paired conversationId when it resolves locally (e.g. a - // Sessions-page jump within this workspace); otherwise it is equally stale - // and would attach the fallback agent to a foreign conversation. - if (!conversations.value.some(conv => conv.conversationId === conversationId)) { - conversationId = '' - } - } + const { agentId, conversationId } = resolveRouteHydrationQuery({ + routeAgentId: route.query.agentId ? String(route.query.agentId) : '', + routeConversationId: String(route.query.conversationId || ''), + agents: agents.value, + conversations: conversations.value, + }) if (agentId && agentId !== String(selectedAgentId.value)) { selectedAgentId.value = agentId diff --git a/mateclaw-ui/src/views/__tests__/chatConsoleRouteHydration.test.ts b/mateclaw-ui/src/views/__tests__/chatConsoleRouteHydration.test.ts new file mode 100644 index 00000000..2ef202bb --- /dev/null +++ b/mateclaw-ui/src/views/__tests__/chatConsoleRouteHydration.test.ts @@ -0,0 +1,36 @@ +// @vitest-environment happy-dom +import { describe, expect, it } from 'vitest' +import { resolveRouteHydrationQuery } from '@/utils/chatRouteHydration' + +const agents = [{ id: 'agent-visible' }] +const conversations = [{ conversationId: 'conv-listed' }] + +describe('resolveRouteHydrationQuery', () => { + it('keeps a deep-linked child conversation even when it is not in the sidebar list', () => { + const result = resolveRouteHydrationQuery({ + routeAgentId: 'agent-deleted-or-hidden', + routeConversationId: 'team-task-finished', + agents, + conversations, + }) + + expect(result).toEqual({ + agentId: '', + conversationId: 'team-task-finished', + }) + }) + + it('keeps valid route agent and conversation ids unchanged', () => { + const result = resolveRouteHydrationQuery({ + routeAgentId: 'agent-visible', + routeConversationId: 'conv-listed', + agents, + conversations, + }) + + expect(result).toEqual({ + agentId: 'agent-visible', + conversationId: 'conv-listed', + }) + }) +})