Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

100 lines
2.7 KiB
TypeScript

// ClipboardItem reliably supports only image/png, so non-png sources are
// rasterised to png via an offscreen canvas before writing.
export interface CopyImageClipboardHost {
writeImage?: (input: {
data: string;
mimeType: "image/png";
}) => Promise<void>;
}
async function imageSrcToPngBlob(src: string): Promise<Blob | null> {
const response = await fetch(src);
const blob = await response.blob();
if (blob.type === "image/png") {
return blob;
}
const bitmap = await createImageBitmap(blob);
const canvas = document.createElement("canvas");
canvas.width = bitmap.width;
canvas.height = bitmap.height;
const ctx = canvas.getContext("2d");
if (!ctx) {
return null;
}
ctx.drawImage(bitmap, 0, 0);
return await new Promise<Blob | null>((resolve) =>
canvas.toBlob((result) => resolve(result), "image/png")
);
}
// FileReader-based (jsdom-compatible) blob-to-base64; also reused by the
// conversation copy path to embed workspace image bytes as data URIs.
export async function blobToBase64(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = () => reject(reader.error ?? new Error("read failed"));
reader.onload = () => {
const result = typeof reader.result === "string" ? reader.result : "";
resolve(result.includes(",") ? (result.split(",").pop() ?? "") : result);
};
reader.readAsDataURL(blob);
});
}
async function pngBlobFromSrc(src: string): Promise<Blob> {
const blob = await imageSrcToPngBlob(src);
if (!blob) {
throw new Error("image conversion failed");
}
return blob;
}
async function copyImageWithWebClipboard(
data: Blob | Promise<Blob>
): Promise<boolean> {
if (
typeof navigator === "undefined" ||
typeof navigator.clipboard?.write !== "function" ||
typeof ClipboardItem === "undefined"
) {
return false;
}
try {
await navigator.clipboard.write([new ClipboardItem({ "image/png": data })]);
return true;
} catch {
return false;
}
}
export async function copyImageToClipboard(
src: string,
hostClipboard?: CopyImageClipboardHost | null
): Promise<boolean> {
try {
if (!hostClipboard?.writeImage) {
return await copyImageWithWebClipboard(pngBlobFromSrc(src));
}
const blob = await imageSrcToPngBlob(src);
if (!blob) {
return false;
}
try {
await hostClipboard.writeImage({
data: await blobToBase64(blob),
mimeType: "image/png"
});
return true;
} catch {
// Fall through to the browser clipboard path when a host exists but the
// native clipboard write is denied or unavailable.
}
return copyImageWithWebClipboard(blob);
} catch {
return false;
}
}