mcp-image-uploader 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +33 -0
- package/README.md +173 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +89 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/dist/services/compressor.d.ts +10 -0
- package/dist/services/compressor.d.ts.map +1 -0
- package/dist/services/compressor.js +64 -0
- package/dist/services/compressor.js.map +1 -0
- package/dist/services/image-loader.d.ts +6 -0
- package/dist/services/image-loader.d.ts.map +1 -0
- package/dist/services/image-loader.js +47 -0
- package/dist/services/image-loader.js.map +1 -0
- package/dist/services/uploader.d.ts +6 -0
- package/dist/services/uploader.d.ts.map +1 -0
- package/dist/services/uploader.js +65 -0
- package/dist/services/uploader.js.map +1 -0
- package/dist/strategies/avif.d.ts +9 -0
- package/dist/strategies/avif.d.ts.map +1 -0
- package/dist/strategies/avif.js +29 -0
- package/dist/strategies/avif.js.map +1 -0
- package/dist/strategies/jpeg.d.ts +13 -0
- package/dist/strategies/jpeg.d.ts.map +1 -0
- package/dist/strategies/jpeg.js +40 -0
- package/dist/strategies/jpeg.js.map +1 -0
- package/dist/strategies/png.d.ts +12 -0
- package/dist/strategies/png.d.ts.map +1 -0
- package/dist/strategies/png.js +51 -0
- package/dist/strategies/png.js.map +1 -0
- package/dist/strategies/webp.d.ts +9 -0
- package/dist/strategies/webp.d.ts.map +1 -0
- package/dist/strategies/webp.js +19 -0
- package/dist/strategies/webp.js.map +1 -0
- package/dist/tools/upload-image.d.ts +30 -0
- package/dist/tools/upload-image.d.ts.map +1 -0
- package/dist/tools/upload-image.js +81 -0
- package/dist/tools/upload-image.js.map +1 -0
- package/dist/types/index.d.ts +43 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/index.d.ts +13 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +32 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +37 -0
- package/src/config.ts +106 -0
- package/src/index.ts +69 -0
- package/src/services/compressor.ts +75 -0
- package/src/services/image-loader.ts +49 -0
- package/src/services/uploader.ts +85 -0
- package/src/strategies/avif.ts +32 -0
- package/src/strategies/jpeg.ts +44 -0
- package/src/strategies/png.ts +55 -0
- package/src/strategies/webp.ts +24 -0
- package/src/tools/upload-image.ts +100 -0
- package/src/types/index.ts +46 -0
- package/src/utils/index.ts +30 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
import type { CompressionResult, CompressionOptions, ImageFormat, ImageMetadata } from '../types/index.js';
|
|
3
|
+
import { compressJpeg } from '../strategies/jpeg.js';
|
|
4
|
+
import { compressPng } from '../strategies/png.js';
|
|
5
|
+
import { compressWebp } from '../strategies/webp.js';
|
|
6
|
+
import { compressAvif } from '../strategies/avif.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 获取图片元数据
|
|
10
|
+
*/
|
|
11
|
+
export async function getImageMetadata(buffer: Buffer): Promise<ImageMetadata> {
|
|
12
|
+
const metadata = await sharp(buffer).metadata();
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
format: normalizeFormat(metadata.format),
|
|
16
|
+
width: metadata.width ?? 0,
|
|
17
|
+
height: metadata.height ?? 0,
|
|
18
|
+
size: buffer.length,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 标准化格式名称
|
|
24
|
+
*/
|
|
25
|
+
function normalizeFormat(format: string | undefined): ImageFormat {
|
|
26
|
+
const formatMap: Record<string, ImageFormat> = {
|
|
27
|
+
'jpeg': 'jpeg',
|
|
28
|
+
'jpg': 'jpeg',
|
|
29
|
+
'png': 'png',
|
|
30
|
+
'webp': 'webp',
|
|
31
|
+
'avif': 'avif',
|
|
32
|
+
'gif': 'gif',
|
|
33
|
+
'tiff': 'tiff',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return formatMap[format?.toLowerCase() ?? ''] ?? 'jpeg';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 压缩图片
|
|
41
|
+
*/
|
|
42
|
+
export async function compressImage(
|
|
43
|
+
buffer: Buffer,
|
|
44
|
+
options: CompressionOptions
|
|
45
|
+
): Promise<CompressionResult> {
|
|
46
|
+
const metadata = await getImageMetadata(buffer);
|
|
47
|
+
const targetFormat = options.format ?? metadata.format;
|
|
48
|
+
|
|
49
|
+
let compressedBuffer: Buffer;
|
|
50
|
+
|
|
51
|
+
switch (targetFormat) {
|
|
52
|
+
case 'jpeg':
|
|
53
|
+
compressedBuffer = await compressJpeg(buffer, metadata.size, options);
|
|
54
|
+
break;
|
|
55
|
+
case 'png':
|
|
56
|
+
compressedBuffer = await compressPng(buffer, metadata.size, options);
|
|
57
|
+
break;
|
|
58
|
+
case 'webp':
|
|
59
|
+
compressedBuffer = await compressWebp(buffer, options);
|
|
60
|
+
break;
|
|
61
|
+
case 'avif':
|
|
62
|
+
compressedBuffer = await compressAvif(buffer, options);
|
|
63
|
+
break;
|
|
64
|
+
default:
|
|
65
|
+
// 对于不支持的格式,直接返回原始 buffer
|
|
66
|
+
compressedBuffer = buffer;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
buffer: compressedBuffer,
|
|
71
|
+
originalSize: metadata.size,
|
|
72
|
+
compressedSize: compressedBuffer.length,
|
|
73
|
+
format: targetFormat,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { isUrl } from '../utils/index.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 加载图片
|
|
7
|
+
* 支持本地文件路径和远程 URL
|
|
8
|
+
*/
|
|
9
|
+
export async function loadImage(source: string): Promise<Buffer> {
|
|
10
|
+
if (isUrl(source)) {
|
|
11
|
+
return loadFromUrl(source);
|
|
12
|
+
} else {
|
|
13
|
+
return loadFromFile(source);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 从本地文件加载
|
|
19
|
+
*/
|
|
20
|
+
async function loadFromFile(filePath: string): Promise<Buffer> {
|
|
21
|
+
const absolutePath = path.isAbsolute(filePath)
|
|
22
|
+
? filePath
|
|
23
|
+
: path.resolve(process.cwd(), filePath);
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const buffer = await fs.readFile(absolutePath);
|
|
27
|
+
return buffer;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
throw new Error(`无法读取文件: ${absolutePath} - ${error instanceof Error ? error.message : String(error)}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 从远程 URL 加载
|
|
35
|
+
*/
|
|
36
|
+
async function loadFromUrl(url: string): Promise<Buffer> {
|
|
37
|
+
try {
|
|
38
|
+
const response = await fetch(url);
|
|
39
|
+
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
45
|
+
return Buffer.from(arrayBuffer);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
throw new Error(`无法下载图片: ${url} - ${error instanceof Error ? error.message : String(error)}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ImageFormat } from '../types/index.js';
|
|
2
|
+
import { getConfig } from '../config.js';
|
|
3
|
+
|
|
4
|
+
interface UploadResponse {
|
|
5
|
+
url?: string;
|
|
6
|
+
dm_error?: number;
|
|
7
|
+
error_msg?: string;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 获取 MIME 类型
|
|
13
|
+
*/
|
|
14
|
+
function getMimeType(format: ImageFormat): string {
|
|
15
|
+
const mimeTypes: Record<ImageFormat, string> = {
|
|
16
|
+
'jpeg': 'image/jpeg',
|
|
17
|
+
'png': 'image/png',
|
|
18
|
+
'webp': 'image/webp',
|
|
19
|
+
'avif': 'image/avif',
|
|
20
|
+
'gif': 'image/gif',
|
|
21
|
+
'tiff': 'image/tiff',
|
|
22
|
+
};
|
|
23
|
+
return mimeTypes[format] ?? 'application/octet-stream';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 生成文件名
|
|
28
|
+
*/
|
|
29
|
+
function generateFilename(format: ImageFormat): string {
|
|
30
|
+
const timestamp = Date.now();
|
|
31
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
32
|
+
const extension = format === 'jpeg' ? 'jpg' : format;
|
|
33
|
+
return `image_${timestamp}_${random}.${extension}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 上传图片到图床
|
|
38
|
+
*/
|
|
39
|
+
export async function uploadImage(
|
|
40
|
+
buffer: Buffer,
|
|
41
|
+
format: ImageFormat
|
|
42
|
+
): Promise<string> {
|
|
43
|
+
const filename = generateFilename(format);
|
|
44
|
+
const mimeType = getMimeType(format);
|
|
45
|
+
|
|
46
|
+
console.error(`[MCP] 准备上传: ${filename}, MIME: ${mimeType}, 大小: ${buffer.length} bytes`);
|
|
47
|
+
|
|
48
|
+
// 创建 FormData - 使用 File 对象确保正确的 MIME 类型
|
|
49
|
+
|
|
50
|
+
// 在 Node.js/Bun 中使用 File 对象
|
|
51
|
+
const file = new File([buffer], filename, { type: mimeType });
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const { uploadUrl } = getConfig();
|
|
55
|
+
const response = await fetch(uploadUrl, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
body: file,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const responseText = await response.text();
|
|
61
|
+
console.error(`[MCP] 上传响应: ${responseText}`);
|
|
62
|
+
|
|
63
|
+
let result: UploadResponse;
|
|
64
|
+
try {
|
|
65
|
+
result = JSON.parse(responseText) as UploadResponse;
|
|
66
|
+
} catch {
|
|
67
|
+
throw new Error(`上传响应解析失败: ${responseText}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (result.dm_error) {
|
|
71
|
+
throw new Error(`上传失败 (${result.dm_error}): ${result.error_msg ?? '未知错误'}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!result.url) {
|
|
75
|
+
throw new Error(`上传响应缺少 url 字段: ${responseText}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return result.url;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (error instanceof Error && (error.message.startsWith('上传') || error.message.includes('dm_error'))) {
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
throw new Error(`上传请求失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
import type { CompressionOptions } from '../types/index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* AVIF 压缩策略
|
|
6
|
+
*
|
|
7
|
+
* 自动模式:lossless=true (无损)
|
|
8
|
+
* 手动模式:使用指定 quality(默认 50)
|
|
9
|
+
*/
|
|
10
|
+
export async function compressAvif(
|
|
11
|
+
buffer: Buffer,
|
|
12
|
+
options: CompressionOptions
|
|
13
|
+
): Promise<Buffer> {
|
|
14
|
+
if (options.autoCompress) {
|
|
15
|
+
// 自动模式:无损压缩
|
|
16
|
+
return sharp(buffer)
|
|
17
|
+
.avif({
|
|
18
|
+
lossless: true,
|
|
19
|
+
effort: 6,
|
|
20
|
+
})
|
|
21
|
+
.toBuffer();
|
|
22
|
+
} else {
|
|
23
|
+
// 手动模式
|
|
24
|
+
const quality = options.quality ?? 50;
|
|
25
|
+
return sharp(buffer)
|
|
26
|
+
.avif({
|
|
27
|
+
quality,
|
|
28
|
+
effort: 4,
|
|
29
|
+
})
|
|
30
|
+
.toBuffer();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
import type { CompressionOptions } from '../types/index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* JPEG 压缩策略
|
|
6
|
+
*
|
|
7
|
+
* 自动模式:
|
|
8
|
+
* - < 500KB → quality=78 + mozjpeg 优化
|
|
9
|
+
* - > 2MB → quality=65 + mozjpeg 优化
|
|
10
|
+
* - 常规 → quality=72 + mozjpeg 优化
|
|
11
|
+
*
|
|
12
|
+
* 手动模式:使用指定 quality(默认 85)
|
|
13
|
+
*/
|
|
14
|
+
export async function compressJpeg(
|
|
15
|
+
buffer: Buffer,
|
|
16
|
+
originalSize: number,
|
|
17
|
+
options: CompressionOptions
|
|
18
|
+
): Promise<Buffer> {
|
|
19
|
+
let quality: number;
|
|
20
|
+
|
|
21
|
+
if (options.autoCompress) {
|
|
22
|
+
// 自动模式:根据文件大小选择质量
|
|
23
|
+
if (originalSize < 500 * 1024) {
|
|
24
|
+
// < 500KB:较高质量
|
|
25
|
+
quality = 78;
|
|
26
|
+
} else if (originalSize > 2 * 1024 * 1024) {
|
|
27
|
+
// > 2MB:较低质量以获得更好压缩
|
|
28
|
+
quality = 65;
|
|
29
|
+
} else {
|
|
30
|
+
// 常规区间
|
|
31
|
+
quality = 72;
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
// 手动模式
|
|
35
|
+
quality = options.quality ?? 85;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return sharp(buffer)
|
|
39
|
+
.jpeg({
|
|
40
|
+
quality,
|
|
41
|
+
mozjpeg: true, // 使用 mozjpeg 高级优化
|
|
42
|
+
})
|
|
43
|
+
.toBuffer();
|
|
44
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
import type { CompressionOptions } from '../types/index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* PNG 压缩策略
|
|
6
|
+
*
|
|
7
|
+
* 自动模式:
|
|
8
|
+
* - 有损量化: palette=true, quality 65-95
|
|
9
|
+
* - 压缩等级根据文件大小动态调整
|
|
10
|
+
*
|
|
11
|
+
* 手动模式:无损压缩 + 高压缩等级
|
|
12
|
+
*/
|
|
13
|
+
export async function compressPng(
|
|
14
|
+
buffer: Buffer,
|
|
15
|
+
originalSize: number,
|
|
16
|
+
options: CompressionOptions
|
|
17
|
+
): Promise<Buffer> {
|
|
18
|
+
if (options.autoCompress) {
|
|
19
|
+
// 自动模式:使用有损量化
|
|
20
|
+
// 根据文件大小选择压缩等级
|
|
21
|
+
let compressionLevel: number;
|
|
22
|
+
let quality: number;
|
|
23
|
+
|
|
24
|
+
if (originalSize > 2 * 1024 * 1024) {
|
|
25
|
+
// > 2MB:更激进的压缩
|
|
26
|
+
compressionLevel = 9;
|
|
27
|
+
quality = 65;
|
|
28
|
+
} else if (originalSize > 500 * 1024) {
|
|
29
|
+
// 500KB - 2MB
|
|
30
|
+
compressionLevel = 8;
|
|
31
|
+
quality = 75;
|
|
32
|
+
} else {
|
|
33
|
+
// < 500KB
|
|
34
|
+
compressionLevel = 6;
|
|
35
|
+
quality = 85;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return sharp(buffer)
|
|
39
|
+
.png({
|
|
40
|
+
palette: true, // 启用量化 (24位 → 8位)
|
|
41
|
+
quality, // 量化质量
|
|
42
|
+
compressionLevel,
|
|
43
|
+
effort: 7, // 优化努力程度
|
|
44
|
+
})
|
|
45
|
+
.toBuffer();
|
|
46
|
+
} else {
|
|
47
|
+
// 手动模式:无损压缩
|
|
48
|
+
return sharp(buffer)
|
|
49
|
+
.png({
|
|
50
|
+
compressionLevel: 9,
|
|
51
|
+
effort: 10,
|
|
52
|
+
})
|
|
53
|
+
.toBuffer();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
import type { CompressionOptions } from '../types/index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* WebP 压缩策略
|
|
6
|
+
*
|
|
7
|
+
* 自动模式:quality=75, alphaQuality=100, effort=4
|
|
8
|
+
* 手动模式:使用指定 quality(默认 75)
|
|
9
|
+
*/
|
|
10
|
+
export async function compressWebp(
|
|
11
|
+
buffer: Buffer,
|
|
12
|
+
options: CompressionOptions
|
|
13
|
+
): Promise<Buffer> {
|
|
14
|
+
const quality = options.autoCompress ? 75 : (options.quality ?? 75);
|
|
15
|
+
|
|
16
|
+
return sharp(buffer)
|
|
17
|
+
.webp({
|
|
18
|
+
quality,
|
|
19
|
+
alphaQuality: 100, // 保持 alpha 通道质量
|
|
20
|
+
effort: options.autoCompress ? 4 : 6,
|
|
21
|
+
smartSubsample: true, // 智能子采样
|
|
22
|
+
})
|
|
23
|
+
.toBuffer();
|
|
24
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { loadImage } from '../services/image-loader.js';
|
|
3
|
+
import { compressImage, getImageMetadata } from '../services/compressor.js';
|
|
4
|
+
import { uploadImage } from '../services/uploader.js';
|
|
5
|
+
import { formatBytes, calculateCompressionRatio } from '../utils/index.js';
|
|
6
|
+
import type { ImageFormat, UploadResult } from '../types/index.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 上传图片工具的参数 Schema
|
|
10
|
+
*/
|
|
11
|
+
export const uploadImageSchema = z.object({
|
|
12
|
+
source: z
|
|
13
|
+
.string()
|
|
14
|
+
.describe('图片来源:本地文件路径或远程 URL'),
|
|
15
|
+
skipCompress: z
|
|
16
|
+
.boolean()
|
|
17
|
+
.default(false)
|
|
18
|
+
.describe('是否跳过压缩,直接上传原图(默认关闭)'),
|
|
19
|
+
autoCompress: z
|
|
20
|
+
.boolean()
|
|
21
|
+
.default(true)
|
|
22
|
+
.describe('是否启用智能自动压缩(默认开启),仅在 skipCompress=false 时生效'),
|
|
23
|
+
quality: z
|
|
24
|
+
.number()
|
|
25
|
+
.min(1)
|
|
26
|
+
.max(100)
|
|
27
|
+
.optional()
|
|
28
|
+
.describe('手动压缩质量 (1-100),仅在 skipCompress=false 且 autoCompress=false 时生效'),
|
|
29
|
+
format: z
|
|
30
|
+
.enum(['jpeg', 'png', 'webp', 'avif'])
|
|
31
|
+
.optional()
|
|
32
|
+
.describe('输出格式(可选,默认保持原格式)'),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export type UploadImageParams = z.infer<typeof uploadImageSchema>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 执行图片上传
|
|
39
|
+
*/
|
|
40
|
+
export async function executeUploadImage(
|
|
41
|
+
params: UploadImageParams
|
|
42
|
+
): Promise<UploadResult> {
|
|
43
|
+
const { source, skipCompress, autoCompress, quality, format } = params;
|
|
44
|
+
|
|
45
|
+
// 1. 加载图片
|
|
46
|
+
console.error(`[MCP] 正在加载图片: ${source}`);
|
|
47
|
+
const imageBuffer = await loadImage(source);
|
|
48
|
+
console.error(`[MCP] 图片已加载: ${formatBytes(imageBuffer.length)}`);
|
|
49
|
+
|
|
50
|
+
let finalBuffer: Buffer;
|
|
51
|
+
let originalSize: number;
|
|
52
|
+
let compressedSize: number;
|
|
53
|
+
let imageFormat: ImageFormat;
|
|
54
|
+
|
|
55
|
+
if (skipCompress) {
|
|
56
|
+
// 跳过压缩,直接使用原图
|
|
57
|
+
console.error(`[MCP] 跳过压缩,使用原图上传`);
|
|
58
|
+
const metadata = await getImageMetadata(imageBuffer);
|
|
59
|
+
finalBuffer = imageBuffer;
|
|
60
|
+
originalSize = imageBuffer.length;
|
|
61
|
+
compressedSize = imageBuffer.length;
|
|
62
|
+
imageFormat = format as ImageFormat ?? metadata.format;
|
|
63
|
+
} else {
|
|
64
|
+
// 2. 压缩图片
|
|
65
|
+
console.error(`[MCP] 正在压缩图片 (autoCompress=${autoCompress})`);
|
|
66
|
+
const compressionResult = await compressImage(imageBuffer, {
|
|
67
|
+
autoCompress,
|
|
68
|
+
quality,
|
|
69
|
+
format: format as ImageFormat | undefined,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const ratio = calculateCompressionRatio(
|
|
73
|
+
compressionResult.originalSize,
|
|
74
|
+
compressionResult.compressedSize
|
|
75
|
+
);
|
|
76
|
+
console.error(
|
|
77
|
+
`[MCP] 压缩完成: ${formatBytes(compressionResult.originalSize)} → ${formatBytes(compressionResult.compressedSize)} (${ratio}%)`
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
finalBuffer = compressionResult.buffer;
|
|
81
|
+
originalSize = compressionResult.originalSize;
|
|
82
|
+
compressedSize = compressionResult.compressedSize;
|
|
83
|
+
imageFormat = compressionResult.format;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 3. 上传图片
|
|
87
|
+
console.error(`[MCP] 正在上传图片...`);
|
|
88
|
+
const url = await uploadImage(finalBuffer, imageFormat);
|
|
89
|
+
console.error(`[MCP] 上传成功: ${url}`);
|
|
90
|
+
|
|
91
|
+
const ratio = calculateCompressionRatio(originalSize, compressedSize);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
url,
|
|
95
|
+
originalSize,
|
|
96
|
+
compressedSize,
|
|
97
|
+
compressionRatio: ratio,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 支持的图片格式
|
|
3
|
+
*/
|
|
4
|
+
export type ImageFormat = 'jpeg' | 'png' | 'webp' | 'avif' | 'gif' | 'tiff';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 压缩选项
|
|
8
|
+
*/
|
|
9
|
+
export interface CompressionOptions {
|
|
10
|
+
/** 是否启用自动压缩 */
|
|
11
|
+
autoCompress: boolean;
|
|
12
|
+
/** 手动压缩质量 (1-100) */
|
|
13
|
+
quality?: number;
|
|
14
|
+
/** 输出格式 */
|
|
15
|
+
format?: ImageFormat;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 图片元数据
|
|
20
|
+
*/
|
|
21
|
+
export interface ImageMetadata {
|
|
22
|
+
format: ImageFormat;
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
size: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 压缩结果
|
|
30
|
+
*/
|
|
31
|
+
export interface CompressionResult {
|
|
32
|
+
buffer: Buffer;
|
|
33
|
+
originalSize: number;
|
|
34
|
+
compressedSize: number;
|
|
35
|
+
format: ImageFormat;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 上传结果
|
|
40
|
+
*/
|
|
41
|
+
export interface UploadResult {
|
|
42
|
+
url: string;
|
|
43
|
+
originalSize: number;
|
|
44
|
+
compressedSize: number;
|
|
45
|
+
compressionRatio: number;
|
|
46
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 判断是否为 URL
|
|
3
|
+
*/
|
|
4
|
+
export function isUrl(input: string): boolean {
|
|
5
|
+
try {
|
|
6
|
+
const url = new URL(input);
|
|
7
|
+
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
8
|
+
} catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 格式化文件大小
|
|
15
|
+
*/
|
|
16
|
+
export function formatBytes(bytes: number): string {
|
|
17
|
+
if (bytes === 0) return '0 B';
|
|
18
|
+
const k = 1024;
|
|
19
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
20
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
21
|
+
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 计算压缩比
|
|
26
|
+
*/
|
|
27
|
+
export function calculateCompressionRatio(original: number, compressed: number): number {
|
|
28
|
+
if (original === 0) return 0;
|
|
29
|
+
return Math.round((1 - compressed / original) * 100);
|
|
30
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": [
|
|
7
|
+
"ES2022"
|
|
8
|
+
],
|
|
9
|
+
"outDir": "./dist",
|
|
10
|
+
"rootDir": "./src",
|
|
11
|
+
"strict": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"declaration": true,
|
|
17
|
+
"declarationMap": true,
|
|
18
|
+
"sourceMap": true
|
|
19
|
+
},
|
|
20
|
+
"include": [
|
|
21
|
+
"src/**/*"
|
|
22
|
+
],
|
|
23
|
+
"exclude": [
|
|
24
|
+
"node_modules",
|
|
25
|
+
"dist"
|
|
26
|
+
]
|
|
27
|
+
}
|