feat(upload): 支持 Agent 本地图片安全上传

This commit is contained in:
yuzhe
2026-07-23 13:26:10 +08:00
parent 69cfd0b51d
commit c067881415
12 changed files with 380 additions and 25 deletions

View File

@@ -4,7 +4,7 @@ import { notesRepository } from '../repositories/notesRepository.js';
import { imagesRepository } from '../repositories/imagesRepository.js';
import { annotationsRepository } from '../repositories/annotationsRepository.js';
import { database, databaseDialect, withTransaction, type QueryContext } from '../database.js';
import { storeExternalImageUrl, storeUploadedFile } from '../storage.js';
import { StorageImportError, storeExternalImageUrl, storeUploadedFile } from '../storage.js';
import { recalculateCollectionStatus } from './collectionsService.js';
import { ensureProjectCompatibilityCollection, recalculateProjectReviewStatus } from './projectsService.js';
@@ -15,13 +15,34 @@ export interface UrlRound { title: string; description: string; tags: string[];
type StoredImage = { url: string; width: number; height: number; storageProvider: 'local' | 'tencent_cos' | 'external'; storageKey: string };
type PreparedRound = { title: string; description: string; tags: string[]; images: StoredImage[] };
async function readImageSize(filePath: string): Promise<{ width: number; height: number }> {
try { const meta = await sharp(filePath).metadata(); return { width: meta.width ?? 0, height: meta.height ?? 0 }; }
catch { return { width: 0, height: 0 }; }
}
async function prepareFiles(files: UploadedFile[]): Promise<StoredImage[]> {
return Promise.all(files.map(async (file) => ({ ...(await readImageSize(file.path)), ...(await storeUploadedFile(file)) })));
const supported = new Map([
['jpeg', { extension: '.jpg', contentType: 'image/jpeg' }],
['png', { extension: '.png', contentType: 'image/png' }],
['gif', { extension: '.gif', contentType: 'image/gif' }],
['webp', { extension: '.webp', contentType: 'image/webp' }],
['avif', { extension: '.avif', contentType: 'image/avif' }],
]);
const inspected = await Promise.all(files.map(async (file) => {
try {
const metadata = await sharp(file.path).metadata();
const detected = metadata.format ? supported.get(metadata.format) : undefined;
if (!detected || !metadata.width || !metadata.height) throw new Error('invalid image');
return { file, width: metadata.width, height: metadata.height, ...detected };
} catch {
throw new StorageImportError(422, `上传文件不是有效或受支持的图片: ${file.originalname || file.filename}`);
}
}));
const prepared: StoredImage[] = [];
for (const item of inspected) {
const stored = await storeUploadedFile({
...item.file,
originalname: `upload${item.extension}`,
mimetype: item.contentType,
});
prepared.push({ width: item.width, height: item.height, ...stored });
}
return prepared;
}
async function prepareExternalImages(images: string[]): Promise<StoredImage[]> {