feat(assets): 优化资产预览性能并添加资源管理器连接状态提示
- 使用缩略图替代原图展示,通过 sharp 库生成缓存缩略图 - 优化资产分组逻辑,避免不必要的重计算 - 添加 WebSocket 连接状态提示到输入框 - 使用 `useCallback` 和 `useRef` 优化组件渲染性能 - 添加 AbortController 支持请求取消,防止内存泄漏 - 添加 `disconnected` 事件处理,自动重置会话状态
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { RefreshCw, FolderOpen, Image as ImageIcon, Film } from 'lucide-react';
|
||||
import { useAssets } from '@/hooks/useAssets';
|
||||
import { useAccounts } from '@/hooks/useAccounts';
|
||||
@@ -16,6 +16,13 @@ interface ManifestGroup {
|
||||
latest_at: string;
|
||||
}
|
||||
|
||||
interface GroupEntry {
|
||||
manifestPath: string;
|
||||
assets: Asset[];
|
||||
title: string;
|
||||
subtitle: string;
|
||||
}
|
||||
|
||||
type TypeTab = 'all' | 'image' | 'video';
|
||||
|
||||
function AssetGroupSkeleton() {
|
||||
@@ -34,18 +41,28 @@ function AssetGroupSkeleton() {
|
||||
);
|
||||
}
|
||||
|
||||
function getGroupName(manifestPath: string) {
|
||||
const parts = manifestPath.replace(/\\/g, '/').split('/');
|
||||
return parts.length >= 2 ? parts[parts.length - 2] : manifestPath;
|
||||
}
|
||||
|
||||
export function AssetGallery() {
|
||||
const [accountFilter, setAccountFilter] = useState('');
|
||||
const [typeTab, setTypeTab] = useState<TypeTab>('all');
|
||||
const [manifests, setManifests] = useState<ManifestGroup[]>([]);
|
||||
const [previewAsset, setPreviewAsset] = useState<Asset | null>(null);
|
||||
const { accounts } = useAccounts();
|
||||
const { assets, loading, remove } = useAssets({
|
||||
const { assets, loading, refresh, remove } = useAssets({
|
||||
accountId: accountFilter || undefined,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/assets/manifests').then((r) => r.json()).then(setManifests).catch(() => {});
|
||||
const controller = new AbortController();
|
||||
fetch('/api/assets/manifests', { signal: controller.signal })
|
||||
.then((r) => r.json())
|
||||
.then(setManifests)
|
||||
.catch(() => {});
|
||||
return () => controller.abort();
|
||||
}, []);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
@@ -53,49 +70,55 @@ export function AssetGallery() {
|
||||
return assets.filter((a) => a.type === typeTab);
|
||||
}, [assets, typeTab]);
|
||||
|
||||
const grouped = useMemo(() => {
|
||||
const imageCount = useMemo(() => assets.filter((a) => a.type === 'image').length, [assets]);
|
||||
const videoCount = useMemo(() => assets.filter((a) => a.type === 'video').length, [assets]);
|
||||
|
||||
const groupEntries = useMemo(() => {
|
||||
const map = new Map<string, Asset[]>();
|
||||
for (const asset of filtered) {
|
||||
const key = asset.manifest_path || 'ungrouped';
|
||||
if (!map.has(key)) map.set(key, []);
|
||||
map.get(key)!.push(asset);
|
||||
}
|
||||
return map;
|
||||
}, [filtered]);
|
||||
|
||||
const imageCount = assets.filter((a) => a.type === 'image').length;
|
||||
const videoCount = assets.filter((a) => a.type === 'video').length;
|
||||
const entries: GroupEntry[] = [];
|
||||
for (const [manifestPath, groupAssets] of map) {
|
||||
// Filter by account
|
||||
if (accountFilter && manifestPath !== 'ungrouped') {
|
||||
const m = manifests.find((x) => x.manifest_path === manifestPath);
|
||||
if (m?.account_id !== accountFilter) continue;
|
||||
}
|
||||
|
||||
const handleScan = async () => {
|
||||
await fetch('/api/assets/scan', { method: 'POST' });
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const getGroupName = (manifestPath: string) => {
|
||||
const parts = manifestPath.replace(/\\/g, '/').split('/');
|
||||
const dir = parts.length >= 2 ? parts[parts.length - 2] : manifestPath;
|
||||
return dir;
|
||||
};
|
||||
|
||||
const getGroupSubtitle = (manifestPath: string) => {
|
||||
const m = manifests.find((m) => m.manifest_path === manifestPath);
|
||||
if (!m) return '';
|
||||
const title = getGroupName(manifestPath);
|
||||
const m = manifests.find((x) => x.manifest_path === manifestPath);
|
||||
const parts: string[] = [];
|
||||
if (m.image_count) parts.push(`${m.image_count} 图`);
|
||||
if (m.video_count) parts.push(`${m.video_count} 视频`);
|
||||
const acc = accounts.find((a) => a.id === m.account_id);
|
||||
if (m?.image_count) parts.push(`${m.image_count} 图`);
|
||||
if (m?.video_count) parts.push(`${m.video_count} 视频`);
|
||||
const acc = accounts.find((a) => a.id === m?.account_id);
|
||||
if (acc) parts.push(acc.name);
|
||||
return parts.join(' · ');
|
||||
};
|
||||
|
||||
const filteredGroups = useMemo(() => {
|
||||
if (!accountFilter) return [...grouped.entries()];
|
||||
return [...grouped.entries()].filter(([key]) => {
|
||||
if (key === 'ungrouped') return true;
|
||||
const m = manifests.find((m) => m.manifest_path === key);
|
||||
return m?.account_id === accountFilter;
|
||||
});
|
||||
}, [grouped, manifests, accountFilter]);
|
||||
entries.push({ manifestPath, assets: groupAssets, title, subtitle: parts.join(' · ') });
|
||||
}
|
||||
return entries;
|
||||
}, [filtered, manifests, accounts, accountFilter]);
|
||||
|
||||
const handleScan = useCallback(async () => {
|
||||
await fetch('/api/assets/scan', { method: 'POST' });
|
||||
const controller = new AbortController();
|
||||
fetch('/api/assets/manifests', { signal: controller.signal })
|
||||
.then((r) => r.json())
|
||||
.then(setManifests)
|
||||
.catch(() => {});
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const handlePreview = useCallback((asset: Asset) => setPreviewAsset(asset), []);
|
||||
const handleClosePreview = useCallback(() => setPreviewAsset(null), []);
|
||||
|
||||
const previewIndex = useMemo(() => {
|
||||
if (!previewAsset) return -1;
|
||||
return filtered.findIndex((a) => a.id === previewAsset.id);
|
||||
}, [previewAsset, filtered]);
|
||||
|
||||
const tabs: { key: TypeTab; label: string; icon: typeof ImageIcon; count: number }[] = [
|
||||
{ key: 'all', label: '全部', icon: FolderOpen, count: assets.length },
|
||||
@@ -151,7 +174,7 @@ export function AssetGallery() {
|
||||
<div>
|
||||
{Array.from({ length: 3 }).map((_, i) => <AssetGroupSkeleton key={i} />)}
|
||||
</div>
|
||||
) : filteredGroups.length === 0 ? (
|
||||
) : groupEntries.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center mt-16 text-zinc-300">
|
||||
<div className="w-16 h-16 rounded-2xl bg-zinc-50 flex items-center justify-center mb-4">
|
||||
<FolderOpen size={24} strokeWidth={1.5} className="text-zinc-300" />
|
||||
@@ -160,20 +183,28 @@ export function AssetGallery() {
|
||||
<p className="text-xs mt-1 text-zinc-300">点击「扫描」从 output 目录导入</p>
|
||||
</div>
|
||||
) : (
|
||||
filteredGroups.map(([manifestPath, groupAssets]) => (
|
||||
groupEntries.map((entry) => (
|
||||
<AssetProjectGroup
|
||||
key={manifestPath}
|
||||
title={getGroupName(manifestPath)}
|
||||
subtitle={getGroupSubtitle(manifestPath)}
|
||||
assets={groupAssets}
|
||||
onPreview={setPreviewAsset}
|
||||
key={entry.manifestPath}
|
||||
title={entry.title}
|
||||
subtitle={entry.subtitle}
|
||||
assets={entry.assets}
|
||||
onPreview={handlePreview}
|
||||
onDelete={remove}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{previewAsset && <AssetPreview asset={previewAsset} allAssets={filtered} onClose={() => setPreviewAsset(null)} onNavigate={setPreviewAsset} />}
|
||||
{previewAsset && (
|
||||
<AssetPreview
|
||||
asset={previewAsset}
|
||||
allAssets={filtered}
|
||||
currentIndex={previewIndex}
|
||||
onClose={handleClosePreview}
|
||||
onNavigate={handlePreview}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import { useEffect, useCallback, useRef } from 'react';
|
||||
import { X, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import type { Asset } from '@/types';
|
||||
|
||||
interface Props {
|
||||
asset: Asset;
|
||||
allAssets: Asset[];
|
||||
currentIndex: number;
|
||||
onClose: () => void;
|
||||
onNavigate: (asset: Asset) => void;
|
||||
}
|
||||
|
||||
export function AssetPreview({ asset, allAssets, onClose, onNavigate }: Props) {
|
||||
const currentIndex = allAssets.findIndex((a) => a.id === asset.id);
|
||||
export function AssetPreview({ asset, allAssets, currentIndex, onClose, onNavigate }: Props) {
|
||||
const lengthRef = useRef(allAssets.length);
|
||||
lengthRef.current = allAssets.length;
|
||||
|
||||
const goPrev = useCallback(() => {
|
||||
if (currentIndex > 0) onNavigate(allAssets[currentIndex - 1]);
|
||||
}, [currentIndex, allAssets, onNavigate]);
|
||||
|
||||
const goNext = useCallback(() => {
|
||||
if (currentIndex < allAssets.length - 1) onNavigate(allAssets[currentIndex + 1]);
|
||||
if (currentIndex < lengthRef.current - 1) onNavigate(allAssets[currentIndex + 1]);
|
||||
}, [currentIndex, allAssets, onNavigate]);
|
||||
|
||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { memo, useMemo } from 'react';
|
||||
import { Trash2, Film, Image as ImageIcon } from 'lucide-react';
|
||||
import type { Asset } from '@/types';
|
||||
|
||||
@@ -9,9 +10,9 @@ interface Props {
|
||||
onDelete: (id: string) => void;
|
||||
}
|
||||
|
||||
export function AssetProjectGroup({ title, subtitle, assets, onPreview, onDelete }: Props) {
|
||||
const images = assets.filter((a) => a.type === 'image');
|
||||
const videos = assets.filter((a) => a.type === 'video');
|
||||
export const AssetProjectGroup = memo(function AssetProjectGroup({ title, subtitle, assets, onPreview, onDelete }: Props) {
|
||||
const images = useMemo(() => assets.filter((a) => a.type === 'image'), [assets]);
|
||||
const videos = useMemo(() => assets.filter((a) => a.type === 'video'), [assets]);
|
||||
|
||||
return (
|
||||
<div className="mb-6">
|
||||
@@ -43,7 +44,7 @@ export function AssetProjectGroup({ title, subtitle, assets, onPreview, onDelete
|
||||
>
|
||||
{asset.type === 'image' ? (
|
||||
<img
|
||||
src={`/api/assets/file?path=${encodeURIComponent(asset.file_path)}`}
|
||||
src={`/api/assets/thumb?path=${encodeURIComponent(asset.file_path)}&size=200`}
|
||||
alt=""
|
||||
className="w-full h-full object-cover"
|
||||
loading="lazy"
|
||||
@@ -75,4 +76,4 @@ export function AssetProjectGroup({ title, subtitle, assets, onPreview, onDelete
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface ImageAttachment {
|
||||
preview: string;
|
||||
}
|
||||
|
||||
export function ChatInput({ onSend, disabled }: { onSend: (content: string, images?: Array<{ data: string; mimeType: string }>) => void; disabled?: boolean }) {
|
||||
export function ChatInput({ onSend, disabled, connecting }: { onSend: (content: string, images?: Array<{ data: string; mimeType: string }>) => void; disabled?: boolean; connecting?: boolean }) {
|
||||
const [input, setInput] = useState('');
|
||||
const [showCmds, setShowCmds] = useState(false);
|
||||
const [cmdIdx, setCmdIdx] = useState(0);
|
||||
@@ -186,7 +186,7 @@ export function ChatInput({ onSend, disabled }: { onSend: (content: string, imag
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
rows={1}
|
||||
placeholder={disabled ? '等待回复中...' : '输入消息...'}
|
||||
placeholder={disabled ? (connecting ? '连接中...' : '等待回复中...') : '输入消息...'}
|
||||
className="flex-1 bg-transparent text-[15px] resize-none outline-none placeholder:text-zinc-300 text-zinc-800 min-h-[40px] max-h-[140px] py-2 leading-relaxed"
|
||||
disabled={disabled}
|
||||
style={{ height: 'auto' }}
|
||||
|
||||
@@ -150,7 +150,7 @@ export function ChatView() {
|
||||
<h2 className="text-lg font-semibold text-zinc-800">开始新对话</h2>
|
||||
<p className="text-sm text-zinc-400 mt-1">输入消息开始创作,对话将自动创建</p>
|
||||
</div>
|
||||
<ChatInput onSend={handleSend} disabled={thinking || creatingRef.current} />
|
||||
<ChatInput onSend={handleSend} disabled={thinking || !connected || creatingRef.current} connecting={!connected} />
|
||||
{accounts.length > 0 && (
|
||||
<div className="flex items-center gap-2 mt-3 justify-center">
|
||||
<span className="text-xs text-zinc-400">当前账号:</span>
|
||||
@@ -275,7 +275,7 @@ export function ChatView() {
|
||||
)}
|
||||
|
||||
{/* Input */}
|
||||
<ChatInput onSend={handleSend} disabled={thinking} />
|
||||
<ChatInput onSend={handleSend} disabled={thinking || !connected} connecting={!connected} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { api } from '@/lib/api';
|
||||
import type { Asset } from '@/types';
|
||||
|
||||
export function useAssets(params?: { accountId?: string; type?: string }) {
|
||||
const [assets, setAssets] = useState<Asset[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
const accountId = params?.accountId;
|
||||
const type = params?.type;
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
abortRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
abortRef.current = controller;
|
||||
setLoading(true);
|
||||
api.listAssets(params).then(setAssets).finally(() => setLoading(false));
|
||||
}, [params?.accountId, params?.type]);
|
||||
api.listAssets({ accountId, type }, controller.signal)
|
||||
.then(setAssets)
|
||||
.catch((err) => { if (err.name !== 'AbortError') throw err; })
|
||||
.finally(() => { if (controller === abortRef.current) setLoading(false); });
|
||||
}, [accountId, type]);
|
||||
|
||||
useEffect(() => { refresh(); }, [refresh]);
|
||||
useEffect(() => { refresh(); return () => { abortRef.current?.abort(); }; }, [refresh]);
|
||||
|
||||
const remove = (id: string) => api.deleteAsset(id).then(refresh);
|
||||
const remove = useCallback((id: string) => api.deleteAsset(id).then(refresh), [refresh]);
|
||||
|
||||
return { assets, loading, refresh, remove };
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@ export function useChat(conversationId: string | null) {
|
||||
|
||||
useEffect(() => {
|
||||
const onConnected = () => setConnected(true);
|
||||
const onDisconnected = () => {
|
||||
setConnected(false);
|
||||
activeConvRef.current = null;
|
||||
};
|
||||
const onHistory = (data: Record<string, unknown>) => {
|
||||
setMessages((data.messages as Message[]) || []);
|
||||
};
|
||||
@@ -96,6 +100,7 @@ export function useChat(conversationId: string | null) {
|
||||
};
|
||||
|
||||
chatSocket.on('connected', onConnected);
|
||||
chatSocket.on('disconnected', onDisconnected);
|
||||
chatSocket.on('history', onHistory);
|
||||
chatSocket.on('message', onMessage);
|
||||
chatSocket.on('status', onStatus);
|
||||
@@ -112,6 +117,7 @@ export function useChat(conversationId: string | null) {
|
||||
|
||||
return () => {
|
||||
chatSocket.off('connected', onConnected);
|
||||
chatSocket.off('disconnected', onDisconnected);
|
||||
chatSocket.off('history', onHistory);
|
||||
chatSocket.off('message', onMessage);
|
||||
chatSocket.off('status', onStatus);
|
||||
|
||||
@@ -40,11 +40,11 @@ export const api = {
|
||||
request<void>(`/pipeline/messages/${id}`, { method: 'DELETE' }),
|
||||
|
||||
// Assets
|
||||
listAssets: (params?: { accountId?: string; type?: string }) => {
|
||||
listAssets: (params?: { accountId?: string; type?: string }, signal?: AbortSignal) => {
|
||||
const qs = new URLSearchParams();
|
||||
if (params?.accountId) qs.set('accountId', params.accountId);
|
||||
if (params?.type) qs.set('type', params.type);
|
||||
return request<Asset[]>(`/assets?${qs}`);
|
||||
return request<Asset[]>(`/assets?${qs}`, { signal });
|
||||
},
|
||||
deleteAsset: (id: string) => request<void>(`/assets/${id}`, { method: 'DELETE' }),
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ class ChatSocket {
|
||||
try { const { type, data } = JSON.parse(event.data); this.emit(type, data); } catch {}
|
||||
};
|
||||
this.ws.onclose = () => {
|
||||
this.emit('disconnected', {});
|
||||
if (!this.intentionallyClosed) {
|
||||
this.reconnectTimer = setTimeout(() => this.connect(), 3000);
|
||||
}
|
||||
|
||||
544
web/package-lock.json
generated
544
web/package-lock.json
generated
@@ -18,6 +18,7 @@
|
||||
"express": "^4.21.0",
|
||||
"multer": "^2.1.1",
|
||||
"openai": "^6.36.0",
|
||||
"sharp": "^0.34.5",
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -932,6 +933,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
|
||||
@@ -1398,6 +1409,471 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@img/colour": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
||||
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
|
||||
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
|
||||
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-riscv64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
|
||||
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
|
||||
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
|
||||
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-ppc64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
|
||||
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-ppc64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-riscv64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
|
||||
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-riscv64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-s390x": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
|
||||
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-s390x": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-wasm32": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
|
||||
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/runtime": "^1.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-ia32": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
|
||||
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@mariozechner/clipboard": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.5.tgz",
|
||||
@@ -4544,18 +5020,6 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/node-abi/node_modules/semver": {
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
@@ -5041,6 +5505,18 @@
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
|
||||
@@ -5101,6 +5577,50 @@
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
||||
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@img/colour": "^1.0.0",
|
||||
"detect-libc": "^2.1.2",
|
||||
"semver": "^7.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "0.34.5",
|
||||
"@img/sharp-darwin-x64": "0.34.5",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-darwin-x64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-arm": "1.2.4",
|
||||
"@img/sharp-libvips-linux-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-ppc64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-riscv64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-s390x": "1.2.4",
|
||||
"@img/sharp-libvips-linux-x64": "1.2.4",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
|
||||
"@img/sharp-linux-arm": "0.34.5",
|
||||
"@img/sharp-linux-arm64": "0.34.5",
|
||||
"@img/sharp-linux-ppc64": "0.34.5",
|
||||
"@img/sharp-linux-riscv64": "0.34.5",
|
||||
"@img/sharp-linux-s390x": "0.34.5",
|
||||
"@img/sharp-linux-x64": "0.34.5",
|
||||
"@img/sharp-linuxmusl-arm64": "0.34.5",
|
||||
"@img/sharp-linuxmusl-x64": "0.34.5",
|
||||
"@img/sharp-wasm32": "0.34.5",
|
||||
"@img/sharp-win32-arm64": "0.34.5",
|
||||
"@img/sharp-win32-ia32": "0.34.5",
|
||||
"@img/sharp-win32-x64": "0.34.5"
|
||||
}
|
||||
},
|
||||
"node_modules/shell-exec": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/shell-exec/-/shell-exec-1.0.2.tgz",
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"express": "^4.21.0",
|
||||
"multer": "^2.1.1",
|
||||
"openai": "^6.36.0",
|
||||
"sharp": "^0.34.5",
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -92,6 +92,8 @@ export async function runAgentChat(ws: WebSocket, convId: string, userContent: s
|
||||
type: 'message',
|
||||
data: { id: '', role: 'assistant', content: `抱歉,出错了:${errMsg}` },
|
||||
}));
|
||||
} finally {
|
||||
ws.send(JSON.stringify({ type: 'status', data: { status: 'done' } }));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,8 @@ assetsRouter.post('/scan', async (_req, res) => {
|
||||
manifest = JSON.parse(await fs.readFile(manifestPath, 'utf-8'));
|
||||
} catch { continue; }
|
||||
|
||||
const accountId = manifest.account?.id || '';
|
||||
const accountId = typeof manifest.account === 'string' ? manifest.account
|
||||
: manifest.account?.id || '';
|
||||
|
||||
// Scan images
|
||||
const imagesDir = path.join(outputDir, dir.name, 'images');
|
||||
@@ -131,5 +132,35 @@ assetsRouter.get('/file', (req, res) => {
|
||||
const fullPath = path.resolve(PROJECT_ROOT, filePath);
|
||||
if (!fullPath.startsWith(PROJECT_ROOT)) return res.status(403).send('Forbidden');
|
||||
if (!fss.existsSync(fullPath)) return res.status(404).send('Not found');
|
||||
// Cache static assets for 1 hour
|
||||
res.setHeader('Cache-Control', 'public, max-age=3600');
|
||||
res.sendFile(fullPath);
|
||||
});
|
||||
|
||||
// Serve resized thumbnail for images
|
||||
assetsRouter.get('/thumb', async (req, res) => {
|
||||
const filePath = req.query.path as string;
|
||||
const size = Math.min(parseInt(req.query.size as string) || 200, 400);
|
||||
if (!filePath) return res.status(400).send('Missing path');
|
||||
const fullPath = path.resolve(PROJECT_ROOT, filePath);
|
||||
if (!fullPath.startsWith(PROJECT_ROOT)) return res.status(403).send('Forbidden');
|
||||
if (!fss.existsSync(fullPath)) return res.status(404).send('Not found');
|
||||
|
||||
// For non-images, redirect to full file
|
||||
if (!/\.(jpe?g|png|webp)$/i.test(fullPath)) {
|
||||
return res.redirect(`/api/assets/file?path=${encodeURIComponent(filePath)}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const sharp = (await import('sharp')).default;
|
||||
const buffer = await sharp(fullPath)
|
||||
.resize(size, size, { fit: 'cover' })
|
||||
.jpeg({ quality: 70 })
|
||||
.toBuffer();
|
||||
res.setHeader('Cache-Control', 'public, max-age=86400');
|
||||
res.setHeader('Content-Type', 'image/jpeg');
|
||||
res.send(buffer);
|
||||
} catch {
|
||||
res.sendFile(fullPath);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user