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

194 lines
5.2 KiB
TypeScript

export interface AgentRichTextPromptImage {
name: string;
mimeType: "image/png" | "image/jpeg" | "image/webp";
data: string;
}
export function imageFilesFromDataTransfer(
dataTransfer: DataTransfer | null
): File[] {
if (!dataTransfer) {
return [];
}
const files: File[] = [];
const items = (dataTransfer as { items?: DataTransferItemList | null }).items;
if (!items) {
return Array.from(dataTransfer.files ?? []).filter((file) =>
supportedPromptImageMimeType(file.type)
);
}
for (const item of Array.from(items)) {
if (item.kind !== "file" || !supportedPromptImageMimeType(item.type)) {
continue;
}
const file = item.getAsFile();
if (file) {
files.push(file);
}
}
if (files.length === 0) {
return Array.from(dataTransfer.files ?? []).filter((file) =>
supportedPromptImageMimeType(file.type)
);
}
return files;
}
export function nonImageFilesFromDataTransfer(
dataTransfer: DataTransfer | null
): File[] {
if (!dataTransfer) {
return [];
}
const files: File[] = [];
const items = (dataTransfer as { items?: DataTransferItemList | null }).items;
if (!items) {
return Array.from(dataTransfer.files ?? []).filter(
(file) =>
!supportedPromptImageMimeType(file.type) &&
!supportedPromptImageFileName(file.name)
);
}
for (const item of Array.from(items)) {
if (item.kind !== "file" || supportedPromptImageMimeType(item.type)) {
continue;
}
const file = item.getAsFile();
if (file && supportedPromptImageFileName(file.name)) {
continue;
}
if (file) {
files.push(file);
}
}
if (files.length === 0) {
return Array.from(dataTransfer.files ?? []).filter(
(file) =>
!supportedPromptImageMimeType(file.type) &&
!supportedPromptImageFileName(file.name)
);
}
return files;
}
export function classifyAgentRichTextExternalFiles(
dataTransfer: DataTransfer | null
): { imageFiles: File[]; regularFiles: File[] } {
const imageFiles = imageFilesFromDataTransfer(dataTransfer);
const imageFileSet = new Set(imageFiles);
return {
imageFiles,
regularFiles: nonImageFilesFromDataTransfer(dataTransfer).filter(
(file) => !imageFileSet.has(file)
)
};
}
export function routeAgentRichTextExternalFiles(
dataTransfer: DataTransfer | null,
options: {
externalFilesSupported: boolean;
promptImagesSupported: boolean;
}
): {
externalFiles: File[];
imageFiles: File[];
imagesHandledAsFiles: boolean;
} {
const { imageFiles, regularFiles } =
classifyAgentRichTextExternalFiles(dataTransfer);
const imagesHandledAsFiles =
imageFiles.length > 0 &&
!options.promptImagesSupported &&
options.externalFilesSupported;
return {
externalFiles: options.externalFilesSupported
? [...regularFiles, ...(imagesHandledAsFiles ? imageFiles : [])]
: [],
imageFiles,
imagesHandledAsFiles
};
}
export function systemFileDragInfoFromDataTransfer(
dataTransfer: DataTransfer | null
): { hasImageFiles: boolean; hasRegularFiles: boolean } {
if (!dataTransfer) {
return { hasImageFiles: false, hasRegularFiles: false };
}
const items = (dataTransfer as { items?: DataTransferItemList | null }).items;
if (items) {
let hasImageFiles = false;
let hasRegularFiles = false;
for (const item of Array.from(items)) {
if (item.kind !== "file") {
continue;
}
if (supportedPromptImageMimeType(item.type)) {
hasImageFiles = true;
} else {
hasRegularFiles = true;
}
}
return { hasImageFiles, hasRegularFiles };
}
let hasImageFiles = false;
let hasRegularFiles = false;
for (const file of Array.from(dataTransfer.files ?? [])) {
if (
supportedPromptImageMimeType(file.type) ||
supportedPromptImageFileName(file.name)
) {
hasImageFiles = true;
} else {
hasRegularFiles = true;
}
}
return { hasImageFiles, hasRegularFiles };
}
export function supportedPromptImageMimeType(
value: string
): value is AgentRichTextPromptImage["mimeType"] {
return (
value === "image/png" || value === "image/jpeg" || value === "image/webp"
);
}
function supportedPromptImageFileName(value: string): boolean {
return /\.(png|jpe?g|webp)$/i.test(value.trim());
}
export async function readAgentRichTextPromptImages(
files: readonly File[]
): Promise<AgentRichTextPromptImage[]> {
const images = await Promise.all(
files.map(async (file) => {
if (!supportedPromptImageMimeType(file.type)) {
return null;
}
const data = await fileToBase64(file);
return {
name: file.name || "clipboard-image",
mimeType: file.type,
data
};
})
);
return images.filter((image): image is AgentRichTextPromptImage =>
Boolean(image)
);
}
function fileToBase64(file: File): 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(file);
});
}