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

97 lines
2.9 KiB
TypeScript

export function downloadImage(src: string, name: string): void {
const link = document.createElement("a");
link.href = src;
link.download = name;
link.rel = "noopener";
document.body.append(link);
link.click();
link.remove();
}
export function resolveImageDownloadName(
name: string | undefined,
src: string | null,
alt: string | undefined
): string {
const semanticName =
resolveImageNameBase(name) ??
resolveImageNameBase(alt) ??
resolveImageNameBase(src) ??
"image";
const extension =
resolveImageNameExtension(name) ??
resolveImageNameExtension(src) ??
resolveDataImageExtension(src) ??
"png";
return `${semanticName}-${formatImageDownloadTimestamp(new Date())}-${createDownloadRandomSuffix()}.${extension}`;
}
function resolveImageNameBase(value: string | null | undefined): string | null {
const segment = imageNameSegment(value);
if (!segment) {
return null;
}
const base = segment.replace(/\.[A-Za-z0-9]{2,8}$/u, "");
const sanitized = stripControlCharacters(base)
.replace(/[\\/:*?"<>|#%&{}$!'@+`=]+/gu, "-")
.replace(/\s+/gu, "-")
.replace(/-+/gu, "-")
.replace(/^-|-$/gu, "")
.slice(0, 80);
return sanitized || null;
}
function stripControlCharacters(value: string): string {
return Array.from(value)
.filter((char) => char.charCodeAt(0) >= 32)
.join("");
}
function resolveImageNameExtension(
value: string | null | undefined
): string | null {
const segment = imageNameSegment(value);
const match = segment?.match(/\.([A-Za-z0-9]{2,8})$/u);
return match?.[1] ? normalizeImageExtension(match[1]) : null;
}
function imageNameSegment(value: string | null | undefined): string | null {
const trimmed = value?.trim();
if (!trimmed) {
return null;
}
const withoutQuery = decodeURIComponentSafe(
trimmed.split(/[?#]/, 1)[0] ?? ""
);
return withoutQuery.split(/[\\/]/).pop()?.trim() || null;
}
function resolveDataImageExtension(src: string | null): string | null {
const match = src?.match(/^data:image\/([A-Za-z0-9.+-]+)[;,]/u);
return match?.[1] ? normalizeImageExtension(match[1]) : null;
}
function normalizeImageExtension(extension: string): string {
const normalized = extension.toLowerCase();
if (normalized === "jpeg") return "jpg";
if (normalized === "svg+xml") return "svg";
return normalized.replace(/[^a-z0-9]/gu, "") || "png";
}
function formatImageDownloadTimestamp(date: Date): string {
const pad = (value: number): string => String(value).padStart(2, "0");
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
}
function createDownloadRandomSuffix(): string {
return Math.random().toString(36).slice(2, 6).padEnd(4, "0");
}
function decodeURIComponentSafe(value: string): string {
try {
return decodeURIComponent(value);
} catch {
return value;
}
}