diff --git a/frontend/src/app/workspace/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/chats/[thread_id]/page.tsx index e7bc36cf..c3d2697c 100644 --- a/frontend/src/app/workspace/chats/[thread_id]/page.tsx +++ b/frontend/src/app/workspace/chats/[thread_id]/page.tsx @@ -549,7 +549,7 @@ export default function ChatPage() { {/* MARK: 开发测试:iframe 通信功能测试面板 */} - {/* */} + ); diff --git a/frontend/src/components/ai-elements/code-block.tsx b/frontend/src/components/ai-elements/code-block.tsx index c0460238..0f227cc2 100644 --- a/frontend/src/components/ai-elements/code-block.tsx +++ b/frontend/src/components/ai-elements/code-block.tsx @@ -1,7 +1,7 @@ "use client"; import { Button } from "@/components/ui/button"; -import { cn } from "@/lib/utils"; +import { cn, copyToClipboard } from "@/lib/utils"; import { CheckIcon, CopyIcon } from "lucide-react"; import { type ComponentProps, @@ -146,14 +146,9 @@ export const CodeBlockCopyButton = ({ const [isCopied, setIsCopied] = useState(false); const { code } = useContext(CodeBlockContext); - const copyToClipboard = async () => { - if (typeof window === "undefined" || !navigator?.clipboard?.writeText) { - onError?.(new Error("Clipboard API not available")); - return; - } - + const handleCopy = async () => { try { - await navigator.clipboard.writeText(code); + await copyToClipboard(code); setIsCopied(true); onCopy?.(); setTimeout(() => setIsCopied(false), timeout); @@ -167,7 +162,7 @@ export const CodeBlockCopyButton = ({ return ( +
+ {isInIframe + ? "将通过 postMessage 请求父页面复制" + : "将直接调用 navigator.clipboard"} +
+ + + {/* 日志 */} {log.length > 0 && (
diff --git a/frontend/src/components/workspace/recent-chat-list.tsx b/frontend/src/components/workspace/recent-chat-list.tsx index 220aee22..546b1f46 100644 --- a/frontend/src/components/workspace/recent-chat-list.tsx +++ b/frontend/src/components/workspace/recent-chat-list.tsx @@ -39,6 +39,7 @@ import { } from "@/core/threads/hooks"; import { pathOfThread, titleOfThread } from "@/core/threads/utils"; import { env } from "@/env"; +import { copyToClipboard } from "@/lib/utils"; export function RecentChatList() { const { t } = useI18n(); @@ -102,7 +103,7 @@ export function RecentChatList() { const baseUrl = isLocalhost ? VERCEL_URL : window.location.origin; const shareUrl = `${baseUrl}/workspace/chats/${threadId}`; try { - await navigator.clipboard.writeText(shareUrl); + await copyToClipboard(shareUrl); toast.success(t.clipboard.linkCopied); } catch { toast.error(t.clipboard.failedToCopyToClipboard); diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index f8ff63a2..991b94bf 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -10,3 +10,26 @@ export const externalLinkClass = "text-primary underline underline-offset-2 hover:no-underline"; /** Link style without underline by default (e.g. for streaming/loading). */ export const externalLinkClassNoUnderline = "text-primary hover:underline"; + +/** + * Copy text to clipboard, using postMessage when in iframe. + * In iframe context, sends message to parent window to handle clipboard operation. + */ +export async function copyToClipboard(text: string): Promise { + const isInIframe = window.self !== window.top; + const message = { + type: "copyToClipboard", + data: text, + }; + + if (isInIframe && window.parent) { + // Request parent window to copy + window.parent.postMessage(message, "*"); + console.log("[copyToClipboard] iframe mode → postMessage to parent", message); + return; + } + + // Direct clipboard access when not in iframe + console.log("[copyToClipboard] direct mode", message); + await navigator.clipboard.writeText(text); +}