Files
AstrBot/dashboard/src/utils/clipboard.ts
alonguser 8d9ae55c8f fix: extract shared clipboard utility and fix copy actions in dialogs and insecure contexts (#7747)
* fix: 在非安全上下文中为 copyMessage 添加 execCommand 备用方案

在非安全上下文中(例如通过 HTTP 局域网 IP 访问),navigator.clipboard 不可用。为此,我们添加了使用 document.execCommand(‘copy’) 的备用方案,这与 ReadmeDialog.vue 和 Settings.vue 中的现有实现保持一致。

* fix: extract shared clipboard utility and fix copy actions in dialogs and insecure contexts

---------

Co-authored-by: RC-CHN <1051989940@qq.com>
2026-04-24 10:44:20 +08:00

93 lines
2.8 KiB
TypeScript

interface CopyToClipboardOptions {
container?: HTMLElement | null;
}
export async function copyToClipboard(
text: string,
options: CopyToClipboardOptions = {},
): Promise<boolean> {
const container = options.container;
const debugInfo = {
length: text?.length ?? 0,
trimmedLength: text?.trim().length ?? 0,
isSecureContext: typeof window !== "undefined" ? window.isSecureContext : false,
hasClipboardApi:
typeof navigator !== "undefined" && !!navigator.clipboard?.writeText,
containerTag: container?.tagName ?? null,
containerInBody:
typeof document !== "undefined" && !!container && document.body.contains(container),
};
if (!text) {
console.debug("[clipboard] empty text payload", debugInfo);
return false;
}
console.debug("[clipboard] copy request", debugInfo);
if (typeof navigator !== "undefined" && navigator.clipboard && window.isSecureContext) {
try {
await navigator.clipboard.writeText(text);
console.info("[clipboard] copied via Clipboard API", debugInfo);
return true;
} catch (err) {
console.warn("[clipboard] Clipboard API failed, falling back:", err, debugInfo);
}
}
const fallbackOk = fallbackCopy(text, container);
if (fallbackOk) {
console.info("[clipboard] fallback succeeded via document.execCommand('copy')", debugInfo);
} else {
console.warn("[clipboard] fallback failed via document.execCommand('copy')", debugInfo);
}
return fallbackOk;
}
function fallbackCopy(text: string, container?: HTMLElement | null): boolean {
if (typeof document === "undefined" || !document.body) return false;
const mountTarget =
container && document.body.contains(container) ? container : document.body;
const textArea = document.createElement("textarea");
const activeElement =
document.activeElement instanceof HTMLElement ? document.activeElement : null;
const selection = document.getSelection();
const selectedRanges = selection
? Array.from({ length: selection.rangeCount }, (_, index) =>
selection.getRangeAt(index).cloneRange(),
)
: [];
textArea.value = text;
textArea.readOnly = true;
Object.assign(textArea.style, {
position: "fixed",
left: "-9999px",
top: "0",
opacity: "0",
pointerEvents: "none",
});
mountTarget.appendChild(textArea);
textArea.focus();
textArea.select();
textArea.setSelectionRange(0, text.length);
try {
return document.execCommand("copy");
} catch (err) {
console.error("Fallback copy failed:", err);
return false;
} finally {
if (textArea.parentNode) {
textArea.parentNode.removeChild(textArea);
}
if (selection) {
selection.removeAllRanges();
selectedRanges.forEach((range) => selection.addRange(range));
}
activeElement?.focus?.();
}
}