diff --git a/mateclaw-ui/src/components/channels/ChannelEditModal.vue b/mateclaw-ui/src/components/channels/ChannelEditModal.vue index 26d17cbc..03b24d54 100644 --- a/mateclaw-ui/src/components/channels/ChannelEditModal.vue +++ b/mateclaw-ui/src/components/channels/ChannelEditModal.vue @@ -424,6 +424,7 @@ import { ref, computed, onMounted } from 'vue' import { useI18n } from 'vue-i18n' import { ElMessage } from 'element-plus' import { CHANNEL_FIELD_DEFS } from '@/types' +import { copyToClipboard } from '@/utils/clipboard' import type { Agent, Channel, ChannelFieldDef } from '@/types' import { buildConfigJson, @@ -629,7 +630,7 @@ const copyLabel = ref(t('channels.webhook.copy')) async function copyWebhookUrl() { try { - await navigator.clipboard.writeText(webhookUrl.value) + await copyToClipboard(webhookUrl.value) copyLabel.value = t('channels.webhook.copied') setTimeout(() => { copyLabel.value = t('channels.webhook.copy') }, 2000) } catch { @@ -639,7 +640,7 @@ async function copyWebhookUrl() { async function copyText(text: string) { try { - await navigator.clipboard.writeText(text) + await copyToClipboard(text) ElMessage.success(t('common.copied')) } catch { ElMessage.warning(t('channels.webhook.copyFailed')) diff --git a/mateclaw-ui/src/components/chat/MessageBubble.vue b/mateclaw-ui/src/components/chat/MessageBubble.vue index 2586c036..3647a396 100644 --- a/mateclaw-ui/src/components/chat/MessageBubble.vue +++ b/mateclaw-ui/src/components/chat/MessageBubble.vue @@ -366,6 +366,7 @@ import { useMarkdownRenderer } from '@/composables/useMarkdownRenderer' import { useAuthenticatedAttachment } from '@/composables/useAuthenticatedAttachment' import { useToolLabel } from '@/composables/useToolLabel' import { http } from '@/api' +import { copyToClipboard } from '@/utils/clipboard' import TypingCursor from './TypingCursor.vue' import BrowserTimeline from './BrowserTimeline.vue' import ToolCallSegment from './ToolCallSegment.vue' @@ -563,7 +564,7 @@ let copyTimer: ReturnType | null = null function copyMessage() { const text = displayContent.value || props.message.content || '' if (!text) return - navigator.clipboard.writeText(text).then(() => { + copyToClipboard(text).then(() => { copyState.value = 'copied' if (copyTimer) clearTimeout(copyTimer) copyTimer = setTimeout(() => { copyState.value = 'idle' }, 2000) diff --git a/mateclaw-ui/src/utils/clipboard.ts b/mateclaw-ui/src/utils/clipboard.ts new file mode 100644 index 00000000..c1c881cf --- /dev/null +++ b/mateclaw-ui/src/utils/clipboard.ts @@ -0,0 +1,18 @@ +/** + * Copy text to clipboard with fallback for non-HTTPS contexts + * where navigator.clipboard is undefined. + */ +export async function copyToClipboard(text: string): Promise { + if (navigator.clipboard) { + return navigator.clipboard.writeText(text) + } + // Fallback: textarea + execCommand('copy') + const ta = document.createElement('textarea') + ta.value = text + ta.style.position = 'fixed' + ta.style.left = '-9999px' + document.body.appendChild(ta) + ta.select() + document.execCommand('copy') + document.body.removeChild(ta) +} diff --git a/mateclaw-ui/src/views/AgentContext.vue b/mateclaw-ui/src/views/AgentContext.vue index 830cfed6..d69dac7c 100644 --- a/mateclaw-ui/src/views/AgentContext.vue +++ b/mateclaw-ui/src/views/AgentContext.vue @@ -208,6 +208,7 @@ import { useI18n } from 'vue-i18n' import { ElMessage } from 'element-plus' import { mcConfirm } from '@/components/common/useConfirm' import { agentApi, agentContextApi } from '@/api/index' +import { copyToClipboard } from '@/utils/clipboard' import type { Agent, WorkspaceFile } from '@/types/index' import { useMarkdownRenderer } from '@/composables/useMarkdownRenderer' import { plainTextIcon } from '@/composables/usePixelarticons' @@ -330,7 +331,7 @@ function handlePreviewClick(e: MouseEvent) { const encoded = btn.getAttribute('data-code') if (!encoded) return const code = decodeURIComponent(encoded) - navigator.clipboard.writeText(code).then(() => { + copyToClipboard(code).then(() => { btn.classList.add('copied') const textEl = btn.querySelector('.code-block__copy-text') if (textEl) textEl.textContent = t('chat.copied') || 'Copied' diff --git a/mateclaw-ui/src/views/ChatConsole.vue b/mateclaw-ui/src/views/ChatConsole.vue index ef5dc920..65ae2387 100644 --- a/mateclaw-ui/src/views/ChatConsole.vue +++ b/mateclaw-ui/src/views/ChatConsole.vue @@ -317,6 +317,7 @@ import { mcConfirm } from '@/components/common/useConfirm' import { ChatDotRound, Delete, Plus, Setting, UploadFilled } from '@element-plus/icons-vue' import { conversationApi, agentApi, modelApi, chatApi, cronJobApi } from '@/api/index' import { channelIconUrl } from '@/utils/channelSource' +import { copyToClipboard } from '@/utils/clipboard' import { useChat } from '@/composables/chat/useChat' import { reconstructErrorInfo } from '@/types/chatError' import { reconcileMessages, extractMessages } from '@/utils/messageReconcile' @@ -1741,7 +1742,7 @@ function handleCodeCopy(e: MouseEvent) { const encoded = btn.getAttribute('data-code') if (!encoded) return const code = decodeURIComponent(encoded) - navigator.clipboard.writeText(code).then(() => { + copyToClipboard(code).then(() => { btn.classList.add('copied') const textEl = btn.querySelector('.code-block__copy-text') if (textEl) textEl.textContent = t('chat.copied') diff --git a/mateclaw-ui/src/views/Settings/Models/modals/DeviceCodeDialog.vue b/mateclaw-ui/src/views/Settings/Models/modals/DeviceCodeDialog.vue index 0b5e8319..6e38057f 100644 --- a/mateclaw-ui/src/views/Settings/Models/modals/DeviceCodeDialog.vue +++ b/mateclaw-ui/src/views/Settings/Models/modals/DeviceCodeDialog.vue @@ -41,6 +41,7 @@ import { computed, onBeforeUnmount, ref, watch } from 'vue' import { useI18n } from 'vue-i18n' import { ElMessage } from 'element-plus' +import { copyToClipboard } from '@/utils/clipboard' const props = defineProps<{ visible: boolean @@ -83,7 +84,7 @@ onBeforeUnmount(() => { async function copyCode() { try { - await navigator.clipboard.writeText(props.userCode) + await copyToClipboard(props.userCode) copied.value = true setTimeout(() => { copied.value = false }, 2000) } catch {