fix(frontend): copy share links on the current origin (#8126)

* fix(frontend): copy share links on the current origin

The sidebar Share action rewrote the origin to the hosted demo
(https://deer-flow-v2.vercel.app) whenever the page ran on localhost or
127.0.0.1, which are the default make dev and Docker entry points. The
copied link never opened the thread, because the thread only exists on
the local deployment, and opening it sent the thread id (and a custom
agent name) to a host the operator does not control.

Always build the link from window.location.origin and pathOfThread().

* docs(changelog): reference #8126 for the share link origin fix
This commit is contained in:
Hyeonsang Cho 2026-11-11 14:06:19 +09:00 committed by GitHub
parent 4a27b133f8
commit c8d5c108f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 132 additions and 8 deletions

View File

@ -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

View File

@ -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

View File

@ -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) {

View File

@ -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<string, unknown> = {}) {
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(
<I18nProvider initialLocale={DEFAULT_LOCALE}>
<QueryClientProvider client={queryClient}>
<AuthProvider initialUser={user}>
<SidebarProvider>
<ThreadDeleteDialogProvider>
<ThreadSidebarItem thread={thread} isActive={false} />
</ThreadDeleteDialogProvider>
</SidebarProvider>
</AuthProvider>
</QueryClientProvider>
</I18nProvider>,
);
}
/** Opens the row's "more" menu and selects Share. Radix opens on pointerdown. */
async function share(): Promise<string> {
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}`,
);
});
});