diff --git a/CHANGELOG.md b/CHANGELOG.md index d1bab5dba3..cf1427ecf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,13 @@ This section accumulates work toward the **2.3.0** milestone ### Fixed +- **frontend:** **Share** in a sidebar chat's menu now copies a link to the + deployment you are using. On `localhost` and `127.0.0.1`, the default + `make dev` and Docker addresses, it copied a link to the hosted demo + (`deer-flow-v2.vercel.app`) instead. That link never opened the + conversation, and opening it sent the thread id to an outside host. + ([#8126]) + - **runtime:** In multi-worker mode (`run_ownership.heartbeat_enabled`), a worker that had lost its run to a peer could put the row back. Its assembly evidence write was an unconditional full-row upsert, so after a peer @@ -16291,3 +16298,4 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#8069]: https://github.com/bytedance/deer-flow/issues/8069 [#8071]: https://github.com/bytedance/deer-flow/issues/8071 [#8072]: https://github.com/bytedance/deer-flow/pull/8072 +[#8126]: https://github.com/bytedance/deer-flow/pull/8126 diff --git a/CHANGELOG_zh.md b/CHANGELOG_zh.md index a43cab4f29..eae7f8f393 100644 --- a/CHANGELOG_zh.md +++ b/CHANGELOG_zh.md @@ -26,6 +26,11 @@ ### 修复 +- **前端:** 侧边栏对话菜单中的 **分享** 现在复制指向当前部署的链接。此前在 `localhost` 和 + `127.0.0.1`(`make dev` 与 Docker 的默认地址)上,复制的是托管演示站 + (`deer-flow-v2.vercel.app`)的链接:该链接无法打开对话,打开时还会把线程 ID 发送到外部主机。 + ([#8126]) + - **运行时:** 多 Worker 模式(`run_ownership.heartbeat_enabled`)下,已被其他 Worker 接管的运行不再被原 Worker 恢复。此前装配证据通过无条件的整行 upsert 写入,在对端接管后会把 行改回 `running` 并清除接管错误;运行可能在对端已报告失败后以 `success` 结束,或被再次接管。 @@ -12862,3 +12867,4 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#8043]: https://github.com/bytedance/deer-flow/pull/8043 [#8046]: https://github.com/bytedance/deer-flow/pull/8046 [#8064]: https://github.com/bytedance/deer-flow/pull/8064 +[#8126]: https://github.com/bytedance/deer-flow/pull/8126 diff --git a/frontend/src/components/workspace/recent-chat-list.tsx b/frontend/src/components/workspace/recent-chat-list.tsx index 4e4192c587..99e98e348c 100644 --- a/frontend/src/components/workspace/recent-chat-list.tsx +++ b/frontend/src/components/workspace/recent-chat-list.tsx @@ -178,14 +178,10 @@ export function ThreadSidebarItem({ }, [t.chats.pinChatFailed, thread, updatePinnedThread]); const handleShare = useCallback(async () => { - // Always use Vercel URL for sharing so others can access - const VERCEL_URL = "https://deer-flow-v2.vercel.app"; - const isLocalhost = - window.location.hostname === "localhost" || - window.location.hostname === "127.0.0.1"; - // On localhost: use Vercel URL; On production: use current origin - const baseUrl = isLocalhost ? VERCEL_URL : window.location.origin; - const shareUrl = `${baseUrl}${pathOfThread(thread)}`; + // Link to this deployment, the only place the thread exists. Never + // rewrite the origin to another host: the thread id would leave the + // deployment and the link would not open the thread. + const shareUrl = `${window.location.origin}${pathOfThread(thread)}`; try { const didCopy = await writeTextToClipboard(shareUrl); if (!didCopy) { diff --git a/frontend/tests/unit/components/workspace/thread-sidebar-item-share.dom.test.tsx b/frontend/tests/unit/components/workspace/thread-sidebar-item-share.dom.test.tsx new file mode 100644 index 0000000000..938197e901 --- /dev/null +++ b/frontend/tests/unit/components/workspace/thread-sidebar-item-share.dom.test.tsx @@ -0,0 +1,114 @@ +import { afterEach, describe, expect, it, rs } from "@rstest/core"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { + cleanup, + fireEvent, + render, + screen, + waitFor, +} from "@testing-library/react"; + +rs.mock("next/navigation", () => ({ + useRouter: () => ({ push: rs.fn(), replace: rs.fn(), refresh: rs.fn() }), + usePathname: () => "/workspace", + useSearchParams: () => new URLSearchParams(), + useParams: () => ({}), +})); +rs.mock("@/core/clipboard", () => ({ writeTextToClipboard: rs.fn() })); + +import { SidebarProvider } from "@/components/ui/sidebar"; +import { ThreadSidebarItem } from "@/components/workspace/recent-chat-list"; +import { ThreadDeleteDialogProvider } from "@/components/workspace/thread-delete-dialog"; +import { AuthProvider } from "@/core/auth/AuthProvider"; +import type { User } from "@/core/auth/types"; +import { writeTextToClipboard } from "@/core/clipboard"; +import { DEFAULT_LOCALE } from "@/core/i18n"; +import { I18nProvider } from "@/core/i18n/context"; + +type HappyDOMWindow = Window & { + happyDOM: { setURL: (url: string) => void }; +}; + +const THREAD_ID = "83d5133d-f8aa-4095-9bba-2aca03f8f61c"; +const writeText = rs.mocked(writeTextToClipboard); +const initialURL = window.location.href; + +function renderItem(metadata: Record = {}) { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, + }); + const user = { + id: "user-1", + email: "user@example.test", + system_role: "user", + } as User; + const thread = { + thread_id: THREAD_ID, + updated_at: "2026-01-01T00:00:00Z", + metadata, + values: { title: "A thread" }, + } as never; + return render( + + + + + + + + + + + , + ); +} + +/** Opens the row's "more" menu and selects Share. Radix opens on pointerdown. */ +async function share(): Promise { + const trigger = await screen.findByRole("button", { name: /more/i }); + fireEvent.pointerDown(trigger, { button: 0, pointerType: "mouse" }); + fireEvent.click(trigger); + fireEvent.click(await screen.findByText("Share")); + await waitFor(() => expect(writeText).toHaveBeenCalledTimes(1)); + return writeText.mock.calls[0]![0]; +} + +afterEach(() => { + cleanup(); + writeText.mockReset(); + (window as unknown as HappyDOMWindow).happyDOM.setURL(initialURL); +}); + +describe("ThreadSidebarItem Share", () => { + // The default `make dev` and Docker entry points are localhost:2026 and + // 127.0.0.1:2026. The link must stay on the deployment that holds the + // thread rather than point at a hosted demo. + for (const origin of [ + "http://localhost:2026", + "http://127.0.0.1:2026", + "http://localhost:3000", + "http://[::1]:2026", + "http://192.168.1.20:2026", + "https://deerflow.example.com", + ]) { + it(`copies a link on the current origin (${origin})`, async () => { + (window as unknown as HappyDOMWindow).happyDOM.setURL( + `${origin}/workspace/chats`, + ); + writeText.mockResolvedValue(true); + renderItem(); + expect(await share()).toBe(`${origin}/workspace/chats/${THREAD_ID}`); + }); + } + + it("copies the custom agent route on the current origin", async () => { + (window as unknown as HappyDOMWindow).happyDOM.setURL( + "http://localhost:2026/workspace", + ); + writeText.mockResolvedValue(true); + renderItem({ agent_name: "research bot" }); + expect(await share()).toBe( + `http://localhost:2026/workspace/agents/research%20bot/chats/${THREAD_ID}`, + ); + }); +});