diff --git a/desktop/src/components/chat/AssistantMessage.linkrouting.test.tsx b/desktop/src/components/chat/AssistantMessage.linkrouting.test.tsx new file mode 100644 index 00000000..3a7a00af --- /dev/null +++ b/desktop/src/components/chat/AssistantMessage.linkrouting.test.tsx @@ -0,0 +1,24 @@ +import '@testing-library/jest-dom' +import { fireEvent, render, screen } from '@testing-library/react' +import { afterEach, describe, expect, it, vi } from 'vitest' + +const { openBrowser } = vi.hoisted(() => ({ openBrowser: vi.fn() })) +vi.mock('../../stores/browserPanelStore', () => ({ + useBrowserPanelStore: { getState: () => ({ open: openBrowser }) }, +})) +vi.mock('../../lib/desktopRuntime', async (orig) => ({ + ...(await orig>()), + getServerBaseUrl: () => 'http://127.0.0.1:4321', +})) + +import { AssistantMessage } from './AssistantMessage' + +afterEach(() => openBrowser.mockReset()) + +describe('AssistantMessage link routing', () => { + it('opens a localhost link in the in-app browser', () => { + render() + fireEvent.click(screen.getByText('预览')) + expect(openBrowser).toHaveBeenCalledWith('s1', 'http://localhost:5173/') + }) +}) diff --git a/desktop/src/components/chat/AssistantMessage.tsx b/desktop/src/components/chat/AssistantMessage.tsx index 7f254788..3a8d057a 100644 --- a/desktop/src/components/chat/AssistantMessage.tsx +++ b/desktop/src/components/chat/AssistantMessage.tsx @@ -1,15 +1,43 @@ -import { memo } from 'react' +import { memo, useCallback } from 'react' +import type { MouseEvent as ReactMouseEvent } from 'react' import { MarkdownRenderer } from '../markdown/MarkdownRenderer' import { MessageActionBar, type MessageBranchAction } from './MessageActionBar' import { InlineImageGallery } from './InlineImageGallery' +import { handlePreviewLink } from '../../lib/handlePreviewLink' +import { getServerBaseUrl } from '../../lib/desktopRuntime' +import { useBrowserPanelStore } from '../../stores/browserPanelStore' +import { useWorkspacePanelStore } from '../../stores/workspacePanelStore' type Props = { content: string isStreaming?: boolean branchAction?: MessageBranchAction + sessionId?: string } -export const AssistantMessage = memo(function AssistantMessage({ content, isStreaming, branchAction }: Props) { +export const AssistantMessage = memo(function AssistantMessage({ content, isStreaming, branchAction, sessionId }: Props) { + const handleLinkClick = useCallback( + (href: string, event: ReactMouseEvent): boolean => { + if (!sessionId) return false + const handled = handlePreviewLink(href, { + sessionId, + serverBaseUrl: getServerBaseUrl(), + openBrowser: (id, url) => useBrowserPanelStore.getState().open(id, url), + openFilePreview: (id, path) => { + void useWorkspacePanelStore.getState().openPreview(id, path, 'file') + }, + openExternal: (url) => { + void import('@tauri-apps/plugin-shell') + .then((m) => m.open(url)) + .catch(() => window.open(url, '_blank')) + }, + }) + if (handled) event.preventDefault() + return handled + }, + [sessionId], + ) + if (!content.trim()) return null const documentLayout = shouldUseDocumentLayout(content) @@ -32,6 +60,7 @@ export const AssistantMessage = memo(function AssistantMessage({ content, isStre content={content} variant={documentLayout ? 'document' : 'default'} streaming={isStreaming} + onLinkClick={sessionId ? handleLinkClick : undefined} /> {!isStreaming && } {isStreaming && ( diff --git a/desktop/src/components/chat/MessageList.tsx b/desktop/src/components/chat/MessageList.tsx index 4c20bcc6..9d1efe71 100644 --- a/desktop/src/components/chat/MessageList.tsx +++ b/desktop/src/components/chat/MessageList.tsx @@ -1960,7 +1960,7 @@ export const MessageBlock = memo(function MessageBlock({ role="assistant" content={message.content} > - + ) case 'thinking': diff --git a/desktop/src/lib/desktopRuntime.ts b/desktop/src/lib/desktopRuntime.ts index f39ff09b..bafa0298 100644 --- a/desktop/src/lib/desktopRuntime.ts +++ b/desktop/src/lib/desktopRuntime.ts @@ -1,5 +1,6 @@ import { api, + getBaseUrl, getDefaultBaseUrl, hasExplicitDefaultBaseUrl, setAuthToken, @@ -41,6 +42,19 @@ export function isBrowserH5Runtime() { return typeof window !== 'undefined' && !isTauriRuntime() } +/** + * Synchronously return the running local server's base URL (e.g. + * `http://127.0.0.1:`). + * + * The api client caches the resolved base after startup: `initializeDesktopServerUrl` + * calls `invoke('get_server_url')` (desktop) or resolves a browser/H5 URL, then + * `setBaseUrl(...)`. Until that runs, `getBaseUrl()` returns the default + * (`http://127.0.0.1:3456` or `VITE_DESKTOP_SERVER_URL`). + */ +export function getServerBaseUrl(): string { + return getBaseUrl() +} + export function readStoredH5Connection(): StoredH5Connection { if (typeof window === 'undefined') { return { serverUrl: null, token: null }