type ClipboardImageSource = Pick; const imageExtensions: Record = { "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp", "image/gif": "gif", "image/bmp": "bmp", "image/avif": "avif", "image/tiff": "tiff", "image/svg+xml": "svg" }; /** Extract actual clipboard image files without interpreting text, HTML, or URLs. */ export function clipboardImageFiles(clipboardData: ClipboardImageSource | null | undefined): File[] { if (!clipboardData) return []; const fromItems: File[] = []; for (const item of Array.from(clipboardData.items || [])) { if (item.kind !== "file") continue; const file = item.getAsFile(); if (!file) continue; const mime = file.type || item.type; if (mime.startsWith("image/")) fromItems.push(withUsableImageName(file, mime)); } if (fromItems.length > 0) return fromItems; return Array.from(clipboardData.files || []) .filter((file) => file.type.startsWith("image/")) .map((file) => withUsableImageName(file, file.type)); } /** Recognize a single pasted HTTP(S) URL; the server must verify it is a safe image. */ export function clipboardImageURL(clipboardData: Pick | null | undefined): string | null { const value = clipboardData?.getData("text/plain").trim() || ""; if (!value || value.length > 8192 || /\s|[\u0000-\u001f\u007f]/.test(value)) return null; if (!/^https?:\/\//i.test(value)) return null; try { const parsed = new URL(value); if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null; const authority = value.match(/^https?:\/\/([^/?#]*)/i)?.[1] || ""; if (!parsed.hostname || authority.includes("@") || parsed.username || parsed.password) return null; return value; } catch { return null; } } function withUsableImageName(file: File, mime: string): File { const extension = imageExtensions[mime.toLowerCase()]; const safeName = file.name.replace(/[\\/\u0000-\u001f\u007f]/g, "_").trim(); const genericName = !safeName || /^(?:image|blob|unknown)(?:\.image)?$/i.test(safeName); const hasExtension = /\.[a-z\d]{1,10}$/i.test(safeName) && !/\.image$/i.test(safeName); if (safeName === file.name && hasExtension && file.type) return file; const stem = genericName ? "pasted-image" : safeName.replace(/\.image$/i, ""); const name = extension && !hasExtension ? `${stem}.${extension}` : stem || "pasted-image"; if (name === file.name && file.type) return file; return new File([file], name, { type: file.type || mime, lastModified: file.lastModified }); } /** Insert newly uploaded image references without replacing edits made during upload. */ export function insertClipboardImageReferences( currentPrompt: string, labels: string[], selection?: { text: string; start: number; end: number } ): string { const replaceSelection = selection?.text === currentPrompt && Number.isInteger(selection.start) && Number.isInteger(selection.end) && selection.start >= 0 && selection.end >= selection.start && selection.end <= currentPrompt.length; const before = replaceSelection ? currentPrompt.slice(0, selection.start) : currentPrompt; const after = replaceSelection ? currentPrompt.slice(selection.end) : ""; const retainedPrompt = before + after; const references: string[] = []; for (const label of labels) { const normalized = label.startsWith("@") ? label : `@${label}`; if (!/^@图片\d+$/.test(normalized) || references.includes(normalized)) continue; const escaped = normalized.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); if (!new RegExp(`${escaped}(?!\\d)`).test(retainedPrompt)) references.push(normalized); } if (references.length === 0) return currentPrompt; const inserted = references.join(" "); const leadingSpace = before && !/\s$/.test(before) ? " " : ""; const trailingSpace = after && !/^\s/.test(after) ? " " : ""; return before + leadingSpace + inserted + trailingSpace + after; }