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,29 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
/**
|
|
3
|
+
* AVIF 压缩策略
|
|
4
|
+
*
|
|
5
|
+
* 自动模式:lossless=true (无损)
|
|
6
|
+
* 手动模式:使用指定 quality(默认 50)
|
|
7
|
+
*/
|
|
8
|
+
export async function compressAvif(buffer, options) {
|
|
9
|
+
if (options.autoCompress) {
|
|
10
|
+
// 自动模式:无损压缩
|
|
11
|
+
return sharp(buffer)
|
|
12
|
+
.avif({
|
|
13
|
+
lossless: true,
|
|
14
|
+
effort: 6,
|
|
15
|
+
})
|
|
16
|
+
.toBuffer();
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
// 手动模式
|
|
20
|
+
const quality = options.quality ?? 50;
|
|
21
|
+
return sharp(buffer)
|
|
22
|
+
.avif({
|
|
23
|
+
quality,
|
|
24
|
+
effort: 4,
|
|
25
|
+
})
|
|
26
|
+
.toBuffer();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=avif.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"avif.js","sourceRoot":"","sources":["../../src/strategies/avif.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAC9B,MAAc,EACd,OAA2B;IAE3B,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;QACvB,YAAY;QACZ,OAAO,KAAK,CAAC,MAAM,CAAC;aACf,IAAI,CAAC;YACF,QAAQ,EAAE,IAAI;YACd,MAAM,EAAE,CAAC;SACZ,CAAC;aACD,QAAQ,EAAE,CAAC;IACpB,CAAC;SAAM,CAAC;QACJ,OAAO;QACP,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC;QACtC,OAAO,KAAK,CAAC,MAAM,CAAC;aACf,IAAI,CAAC;YACF,OAAO;YACP,MAAM,EAAE,CAAC;SACZ,CAAC;aACD,QAAQ,EAAE,CAAC;IACpB,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CompressionOptions } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* JPEG 压缩策略
|
|
4
|
+
*
|
|
5
|
+
* 自动模式:
|
|
6
|
+
* - < 500KB → quality=78 + mozjpeg 优化
|
|
7
|
+
* - > 2MB → quality=65 + mozjpeg 优化
|
|
8
|
+
* - 常规 → quality=72 + mozjpeg 优化
|
|
9
|
+
*
|
|
10
|
+
* 手动模式:使用指定 quality(默认 85)
|
|
11
|
+
*/
|
|
12
|
+
export declare function compressJpeg(buffer: Buffer, originalSize: number, options: CompressionOptions): Promise<Buffer>;
|
|
13
|
+
//# sourceMappingURL=jpeg.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jpeg.d.ts","sourceRoot":"","sources":["../../src/strategies/jpeg.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAE5D;;;;;;;;;GASG;AACH,wBAAsB,YAAY,CAC9B,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,kBAAkB,GAC5B,OAAO,CAAC,MAAM,CAAC,CA0BjB"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
/**
|
|
3
|
+
* JPEG 压缩策略
|
|
4
|
+
*
|
|
5
|
+
* 自动模式:
|
|
6
|
+
* - < 500KB → quality=78 + mozjpeg 优化
|
|
7
|
+
* - > 2MB → quality=65 + mozjpeg 优化
|
|
8
|
+
* - 常规 → quality=72 + mozjpeg 优化
|
|
9
|
+
*
|
|
10
|
+
* 手动模式:使用指定 quality(默认 85)
|
|
11
|
+
*/
|
|
12
|
+
export async function compressJpeg(buffer, originalSize, options) {
|
|
13
|
+
let quality;
|
|
14
|
+
if (options.autoCompress) {
|
|
15
|
+
// 自动模式:根据文件大小选择质量
|
|
16
|
+
if (originalSize < 500 * 1024) {
|
|
17
|
+
// < 500KB:较高质量
|
|
18
|
+
quality = 78;
|
|
19
|
+
}
|
|
20
|
+
else if (originalSize > 2 * 1024 * 1024) {
|
|
21
|
+
// > 2MB:较低质量以获得更好压缩
|
|
22
|
+
quality = 65;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
// 常规区间
|
|
26
|
+
quality = 72;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
// 手动模式
|
|
31
|
+
quality = options.quality ?? 85;
|
|
32
|
+
}
|
|
33
|
+
return sharp(buffer)
|
|
34
|
+
.jpeg({
|
|
35
|
+
quality,
|
|
36
|
+
mozjpeg: true, // 使用 mozjpeg 高级优化
|
|
37
|
+
})
|
|
38
|
+
.toBuffer();
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=jpeg.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jpeg.js","sourceRoot":"","sources":["../../src/strategies/jpeg.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAC9B,MAAc,EACd,YAAoB,EACpB,OAA2B;IAE3B,IAAI,OAAe,CAAC;IAEpB,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;QACvB,kBAAkB;QAClB,IAAI,YAAY,GAAG,GAAG,GAAG,IAAI,EAAE,CAAC;YAC5B,eAAe;YACf,OAAO,GAAG,EAAE,CAAC;QACjB,CAAC;aAAM,IAAI,YAAY,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,EAAE,CAAC;YACxC,oBAAoB;YACpB,OAAO,GAAG,EAAE,CAAC;QACjB,CAAC;aAAM,CAAC;YACJ,OAAO;YACP,OAAO,GAAG,EAAE,CAAC;QACjB,CAAC;IACL,CAAC;SAAM,CAAC;QACJ,OAAO;QACP,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC;IACpC,CAAC;IAED,OAAO,KAAK,CAAC,MAAM,CAAC;SACf,IAAI,CAAC;QACF,OAAO;QACP,OAAO,EAAE,IAAI,EAAE,kBAAkB;KACpC,CAAC;SACD,QAAQ,EAAE,CAAC;AACpB,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CompressionOptions } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* PNG 压缩策略
|
|
4
|
+
*
|
|
5
|
+
* 自动模式:
|
|
6
|
+
* - 有损量化: palette=true, quality 65-95
|
|
7
|
+
* - 压缩等级根据文件大小动态调整
|
|
8
|
+
*
|
|
9
|
+
* 手动模式:无损压缩 + 高压缩等级
|
|
10
|
+
*/
|
|
11
|
+
export declare function compressPng(buffer: Buffer, originalSize: number, options: CompressionOptions): Promise<Buffer>;
|
|
12
|
+
//# sourceMappingURL=png.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"png.d.ts","sourceRoot":"","sources":["../../src/strategies/png.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAE5D;;;;;;;;GAQG;AACH,wBAAsB,WAAW,CAC7B,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,kBAAkB,GAC5B,OAAO,CAAC,MAAM,CAAC,CAsCjB"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
/**
|
|
3
|
+
* PNG 压缩策略
|
|
4
|
+
*
|
|
5
|
+
* 自动模式:
|
|
6
|
+
* - 有损量化: palette=true, quality 65-95
|
|
7
|
+
* - 压缩等级根据文件大小动态调整
|
|
8
|
+
*
|
|
9
|
+
* 手动模式:无损压缩 + 高压缩等级
|
|
10
|
+
*/
|
|
11
|
+
export async function compressPng(buffer, originalSize, options) {
|
|
12
|
+
if (options.autoCompress) {
|
|
13
|
+
// 自动模式:使用有损量化
|
|
14
|
+
// 根据文件大小选择压缩等级
|
|
15
|
+
let compressionLevel;
|
|
16
|
+
let quality;
|
|
17
|
+
if (originalSize > 2 * 1024 * 1024) {
|
|
18
|
+
// > 2MB:更激进的压缩
|
|
19
|
+
compressionLevel = 9;
|
|
20
|
+
quality = 65;
|
|
21
|
+
}
|
|
22
|
+
else if (originalSize > 500 * 1024) {
|
|
23
|
+
// 500KB - 2MB
|
|
24
|
+
compressionLevel = 8;
|
|
25
|
+
quality = 75;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
// < 500KB
|
|
29
|
+
compressionLevel = 6;
|
|
30
|
+
quality = 85;
|
|
31
|
+
}
|
|
32
|
+
return sharp(buffer)
|
|
33
|
+
.png({
|
|
34
|
+
palette: true, // 启用量化 (24位 → 8位)
|
|
35
|
+
quality, // 量化质量
|
|
36
|
+
compressionLevel,
|
|
37
|
+
effort: 7, // 优化努力程度
|
|
38
|
+
})
|
|
39
|
+
.toBuffer();
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
// 手动模式:无损压缩
|
|
43
|
+
return sharp(buffer)
|
|
44
|
+
.png({
|
|
45
|
+
compressionLevel: 9,
|
|
46
|
+
effort: 10,
|
|
47
|
+
})
|
|
48
|
+
.toBuffer();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=png.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"png.js","sourceRoot":"","sources":["../../src/strategies/png.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC7B,MAAc,EACd,YAAoB,EACpB,OAA2B;IAE3B,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;QACvB,cAAc;QACd,eAAe;QACf,IAAI,gBAAwB,CAAC;QAC7B,IAAI,OAAe,CAAC;QAEpB,IAAI,YAAY,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,EAAE,CAAC;YACjC,eAAe;YACf,gBAAgB,GAAG,CAAC,CAAC;YACrB,OAAO,GAAG,EAAE,CAAC;QACjB,CAAC;aAAM,IAAI,YAAY,GAAG,GAAG,GAAG,IAAI,EAAE,CAAC;YACnC,cAAc;YACd,gBAAgB,GAAG,CAAC,CAAC;YACrB,OAAO,GAAG,EAAE,CAAC;QACjB,CAAC;aAAM,CAAC;YACJ,UAAU;YACV,gBAAgB,GAAG,CAAC,CAAC;YACrB,OAAO,GAAG,EAAE,CAAC;QACjB,CAAC;QAED,OAAO,KAAK,CAAC,MAAM,CAAC;aACf,GAAG,CAAC;YACD,OAAO,EAAE,IAAI,EAAE,kBAAkB;YACjC,OAAO,EAAQ,OAAO;YACtB,gBAAgB;YAChB,MAAM,EAAE,CAAC,EAAM,SAAS;SAC3B,CAAC;aACD,QAAQ,EAAE,CAAC;IACpB,CAAC;SAAM,CAAC;QACJ,YAAY;QACZ,OAAO,KAAK,CAAC,MAAM,CAAC;aACf,GAAG,CAAC;YACD,gBAAgB,EAAE,CAAC;YACnB,MAAM,EAAE,EAAE;SACb,CAAC;aACD,QAAQ,EAAE,CAAC;IACpB,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CompressionOptions } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* WebP 压缩策略
|
|
4
|
+
*
|
|
5
|
+
* 自动模式:quality=75, alphaQuality=100, effort=4
|
|
6
|
+
* 手动模式:使用指定 quality(默认 75)
|
|
7
|
+
*/
|
|
8
|
+
export declare function compressWebp(buffer: Buffer, options: CompressionOptions): Promise<Buffer>;
|
|
9
|
+
//# sourceMappingURL=webp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webp.d.ts","sourceRoot":"","sources":["../../src/strategies/webp.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAE5D;;;;;GAKG;AACH,wBAAsB,YAAY,CAC9B,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,kBAAkB,GAC5B,OAAO,CAAC,MAAM,CAAC,CAWjB"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
/**
|
|
3
|
+
* WebP 压缩策略
|
|
4
|
+
*
|
|
5
|
+
* 自动模式:quality=75, alphaQuality=100, effort=4
|
|
6
|
+
* 手动模式:使用指定 quality(默认 75)
|
|
7
|
+
*/
|
|
8
|
+
export async function compressWebp(buffer, options) {
|
|
9
|
+
const quality = options.autoCompress ? 75 : (options.quality ?? 75);
|
|
10
|
+
return sharp(buffer)
|
|
11
|
+
.webp({
|
|
12
|
+
quality,
|
|
13
|
+
alphaQuality: 100, // 保持 alpha 通道质量
|
|
14
|
+
effort: options.autoCompress ? 4 : 6,
|
|
15
|
+
smartSubsample: true, // 智能子采样
|
|
16
|
+
})
|
|
17
|
+
.toBuffer();
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=webp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webp.js","sourceRoot":"","sources":["../../src/strategies/webp.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAC9B,MAAc,EACd,OAA2B;IAE3B,MAAM,OAAO,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC;IAEpE,OAAO,KAAK,CAAC,MAAM,CAAC;SACf,IAAI,CAAC;QACF,OAAO;QACP,YAAY,EAAE,GAAG,EAAE,gBAAgB;QACnC,MAAM,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACpC,cAAc,EAAE,IAAI,EAAE,QAAQ;KACjC,CAAC;SACD,QAAQ,EAAE,CAAC;AACpB,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { UploadResult } from '../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* 上传图片工具的参数 Schema
|
|
5
|
+
*/
|
|
6
|
+
export declare const uploadImageSchema: z.ZodObject<{
|
|
7
|
+
source: z.ZodString;
|
|
8
|
+
skipCompress: z.ZodDefault<z.ZodBoolean>;
|
|
9
|
+
autoCompress: z.ZodDefault<z.ZodBoolean>;
|
|
10
|
+
quality: z.ZodOptional<z.ZodNumber>;
|
|
11
|
+
format: z.ZodOptional<z.ZodEnum<["jpeg", "png", "webp", "avif"]>>;
|
|
12
|
+
}, "strip", z.ZodTypeAny, {
|
|
13
|
+
source: string;
|
|
14
|
+
skipCompress: boolean;
|
|
15
|
+
autoCompress: boolean;
|
|
16
|
+
quality?: number | undefined;
|
|
17
|
+
format?: "jpeg" | "png" | "webp" | "avif" | undefined;
|
|
18
|
+
}, {
|
|
19
|
+
source: string;
|
|
20
|
+
quality?: number | undefined;
|
|
21
|
+
format?: "jpeg" | "png" | "webp" | "avif" | undefined;
|
|
22
|
+
skipCompress?: boolean | undefined;
|
|
23
|
+
autoCompress?: boolean | undefined;
|
|
24
|
+
}>;
|
|
25
|
+
export type UploadImageParams = z.infer<typeof uploadImageSchema>;
|
|
26
|
+
/**
|
|
27
|
+
* 执行图片上传
|
|
28
|
+
*/
|
|
29
|
+
export declare function executeUploadImage(params: UploadImageParams): Promise<UploadResult>;
|
|
30
|
+
//# sourceMappingURL=upload-image.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload-image.d.ts","sourceRoot":"","sources":["../../src/tools/upload-image.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,OAAO,KAAK,EAAe,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEnE;;GAEG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;EAsB5B,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE;;GAEG;AACH,wBAAsB,kBAAkB,CACpC,MAAM,EAAE,iBAAiB,GAC1B,OAAO,CAAC,YAAY,CAAC,CAyDvB"}
|
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
/**
|
|
7
|
+
* 上传图片工具的参数 Schema
|
|
8
|
+
*/
|
|
9
|
+
export const uploadImageSchema = z.object({
|
|
10
|
+
source: z
|
|
11
|
+
.string()
|
|
12
|
+
.describe('图片来源:本地文件路径或远程 URL'),
|
|
13
|
+
skipCompress: z
|
|
14
|
+
.boolean()
|
|
15
|
+
.default(false)
|
|
16
|
+
.describe('是否跳过压缩,直接上传原图(默认关闭)'),
|
|
17
|
+
autoCompress: z
|
|
18
|
+
.boolean()
|
|
19
|
+
.default(true)
|
|
20
|
+
.describe('是否启用智能自动压缩(默认开启),仅在 skipCompress=false 时生效'),
|
|
21
|
+
quality: z
|
|
22
|
+
.number()
|
|
23
|
+
.min(1)
|
|
24
|
+
.max(100)
|
|
25
|
+
.optional()
|
|
26
|
+
.describe('手动压缩质量 (1-100),仅在 skipCompress=false 且 autoCompress=false 时生效'),
|
|
27
|
+
format: z
|
|
28
|
+
.enum(['jpeg', 'png', 'webp', 'avif'])
|
|
29
|
+
.optional()
|
|
30
|
+
.describe('输出格式(可选,默认保持原格式)'),
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* 执行图片上传
|
|
34
|
+
*/
|
|
35
|
+
export async function executeUploadImage(params) {
|
|
36
|
+
const { source, skipCompress, autoCompress, quality, format } = params;
|
|
37
|
+
// 1. 加载图片
|
|
38
|
+
console.error(`[MCP] 正在加载图片: ${source}`);
|
|
39
|
+
const imageBuffer = await loadImage(source);
|
|
40
|
+
console.error(`[MCP] 图片已加载: ${formatBytes(imageBuffer.length)}`);
|
|
41
|
+
let finalBuffer;
|
|
42
|
+
let originalSize;
|
|
43
|
+
let compressedSize;
|
|
44
|
+
let imageFormat;
|
|
45
|
+
if (skipCompress) {
|
|
46
|
+
// 跳过压缩,直接使用原图
|
|
47
|
+
console.error(`[MCP] 跳过压缩,使用原图上传`);
|
|
48
|
+
const metadata = await getImageMetadata(imageBuffer);
|
|
49
|
+
finalBuffer = imageBuffer;
|
|
50
|
+
originalSize = imageBuffer.length;
|
|
51
|
+
compressedSize = imageBuffer.length;
|
|
52
|
+
imageFormat = format ?? metadata.format;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
// 2. 压缩图片
|
|
56
|
+
console.error(`[MCP] 正在压缩图片 (autoCompress=${autoCompress})`);
|
|
57
|
+
const compressionResult = await compressImage(imageBuffer, {
|
|
58
|
+
autoCompress,
|
|
59
|
+
quality,
|
|
60
|
+
format: format,
|
|
61
|
+
});
|
|
62
|
+
const ratio = calculateCompressionRatio(compressionResult.originalSize, compressionResult.compressedSize);
|
|
63
|
+
console.error(`[MCP] 压缩完成: ${formatBytes(compressionResult.originalSize)} → ${formatBytes(compressionResult.compressedSize)} (${ratio}%)`);
|
|
64
|
+
finalBuffer = compressionResult.buffer;
|
|
65
|
+
originalSize = compressionResult.originalSize;
|
|
66
|
+
compressedSize = compressionResult.compressedSize;
|
|
67
|
+
imageFormat = compressionResult.format;
|
|
68
|
+
}
|
|
69
|
+
// 3. 上传图片
|
|
70
|
+
console.error(`[MCP] 正在上传图片...`);
|
|
71
|
+
const url = await uploadImage(finalBuffer, imageFormat);
|
|
72
|
+
console.error(`[MCP] 上传成功: ${url}`);
|
|
73
|
+
const ratio = calculateCompressionRatio(originalSize, compressedSize);
|
|
74
|
+
return {
|
|
75
|
+
url,
|
|
76
|
+
originalSize,
|
|
77
|
+
compressedSize,
|
|
78
|
+
compressionRatio: ratio,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=upload-image.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload-image.js","sourceRoot":"","sources":["../../src/tools/upload-image.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 支持的图片格式
|
|
3
|
+
*/
|
|
4
|
+
export type ImageFormat = 'jpeg' | 'png' | 'webp' | 'avif' | 'gif' | 'tiff';
|
|
5
|
+
/**
|
|
6
|
+
* 压缩选项
|
|
7
|
+
*/
|
|
8
|
+
export interface CompressionOptions {
|
|
9
|
+
/** 是否启用自动压缩 */
|
|
10
|
+
autoCompress: boolean;
|
|
11
|
+
/** 手动压缩质量 (1-100) */
|
|
12
|
+
quality?: number;
|
|
13
|
+
/** 输出格式 */
|
|
14
|
+
format?: ImageFormat;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 图片元数据
|
|
18
|
+
*/
|
|
19
|
+
export interface ImageMetadata {
|
|
20
|
+
format: ImageFormat;
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
size: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 压缩结果
|
|
27
|
+
*/
|
|
28
|
+
export interface CompressionResult {
|
|
29
|
+
buffer: Buffer;
|
|
30
|
+
originalSize: number;
|
|
31
|
+
compressedSize: number;
|
|
32
|
+
format: ImageFormat;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 上传结果
|
|
36
|
+
*/
|
|
37
|
+
export interface UploadResult {
|
|
38
|
+
url: string;
|
|
39
|
+
originalSize: number;
|
|
40
|
+
compressedSize: number;
|
|
41
|
+
compressionRatio: number;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,CAAC;AAE5E;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAC/B,eAAe;IACf,YAAY,EAAE,OAAO,CAAC;IACtB,qBAAqB;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW;IACX,MAAM,CAAC,EAAE,WAAW,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC1B,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,WAAW,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;CAC5B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 判断是否为 URL
|
|
3
|
+
*/
|
|
4
|
+
export declare function isUrl(input: string): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* 格式化文件大小
|
|
7
|
+
*/
|
|
8
|
+
export declare function formatBytes(bytes: number): string;
|
|
9
|
+
/**
|
|
10
|
+
* 计算压缩比
|
|
11
|
+
*/
|
|
12
|
+
export declare function calculateCompressionRatio(original: number, compressed: number): number;
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAO5C;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMjD;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAGtF"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 判断是否为 URL
|
|
3
|
+
*/
|
|
4
|
+
export function isUrl(input) {
|
|
5
|
+
try {
|
|
6
|
+
const url = new URL(input);
|
|
7
|
+
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 格式化文件大小
|
|
15
|
+
*/
|
|
16
|
+
export function formatBytes(bytes) {
|
|
17
|
+
if (bytes === 0)
|
|
18
|
+
return '0 B';
|
|
19
|
+
const k = 1024;
|
|
20
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
21
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
22
|
+
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 计算压缩比
|
|
26
|
+
*/
|
|
27
|
+
export function calculateCompressionRatio(original, compressed) {
|
|
28
|
+
if (original === 0)
|
|
29
|
+
return 0;
|
|
30
|
+
return Math.round((1 - compressed / original) * 100);
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,UAAU,KAAK,CAAC,KAAa;IAC/B,IAAI,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;QAC3B,OAAO,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC;IACjE,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,KAAK,CAAC;IACjB,CAAC;AACL,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,WAAW,CAAC,KAAa;IACrC,IAAI,KAAK,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC9B,MAAM,CAAC,GAAG,IAAI,CAAC;IACf,MAAM,KAAK,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IACtC,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACpD,OAAO,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AAChE,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,yBAAyB,CAAC,QAAgB,EAAE,UAAkB;IAC1E,IAAI,QAAQ,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IAC7B,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,UAAU,GAAG,QAAQ,CAAC,GAAG,GAAG,CAAC,CAAC;AACzD,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-image-uploader",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "MCP server for image compression and upload",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mcp-image-uploader": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "tsx src/index.ts",
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"start:bun": "bun src/index.ts"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"mcp",
|
|
18
|
+
"image",
|
|
19
|
+
"compression",
|
|
20
|
+
"upload"
|
|
21
|
+
],
|
|
22
|
+
"author": "",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
26
|
+
"sharp": "^0.33.0",
|
|
27
|
+
"zod": "^3.23.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^22.0.0",
|
|
31
|
+
"tsx": "^4.19.0",
|
|
32
|
+
"typescript": "^5.6.0"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18.0.0"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Image Uploader 配置模块
|
|
3
|
+
*
|
|
4
|
+
* 支持通过命令行参数配置上传接口地址,实现业务解耦
|
|
5
|
+
*
|
|
6
|
+
* 使用方式(命令行参数):
|
|
7
|
+
* node dist/index.js UPLOAD_ENDPOINT=https://your-api.com/upload
|
|
8
|
+
*
|
|
9
|
+
* 使用方式(环境变量):
|
|
10
|
+
* UPLOAD_ENDPOINT=https://your-api.com/upload node dist/index.js
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface Config {
|
|
14
|
+
/** 上传接口地址 */
|
|
15
|
+
uploadUrl: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 默认配置
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_CONFIG: Config = {
|
|
22
|
+
uploadUrl: '',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 解析命令行参数获取配置
|
|
27
|
+
*/
|
|
28
|
+
function parseArgs(): Partial<Config> {
|
|
29
|
+
const args = process.argv.slice(2);
|
|
30
|
+
const config: Partial<Config> = {};
|
|
31
|
+
|
|
32
|
+
for (const arg of args) {
|
|
33
|
+
// 支持 UPLOAD_ENDPOINT=xxx 格式
|
|
34
|
+
if (arg.startsWith('UPLOAD_ENDPOINT=')) {
|
|
35
|
+
config.uploadUrl = arg.split('=').slice(1).join('=');
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 同时支持环境变量
|
|
41
|
+
if (!config.uploadUrl && process.env.UPLOAD_ENDPOINT) {
|
|
42
|
+
config.uploadUrl = process.env.UPLOAD_ENDPOINT;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return config;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 验证配置
|
|
50
|
+
*/
|
|
51
|
+
function validateConfig(config: Config): void {
|
|
52
|
+
if (!config.uploadUrl) {
|
|
53
|
+
console.error('[MCP] 错误: 必须配置上传接口地址 UPLOAD_ENDPOINT');
|
|
54
|
+
console.error('[MCP] 使用方式:');
|
|
55
|
+
console.error(' node dist/index.js UPLOAD_ENDPOINT=https://your-api.com/upload');
|
|
56
|
+
console.error('');
|
|
57
|
+
console.error('[MCP] MCP 配置示例:');
|
|
58
|
+
console.error(' {');
|
|
59
|
+
console.error(' "mcpServers": {');
|
|
60
|
+
console.error(' "image-uploader": {');
|
|
61
|
+
console.error(' "command": "node",');
|
|
62
|
+
console.error(' "args": ["/path/to/dist/index.js", "UPLOAD_ENDPOINT=https://your-api.com/upload"]');
|
|
63
|
+
console.error(' }');
|
|
64
|
+
console.error(' }');
|
|
65
|
+
console.error(' }');
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 验证 URL 格式
|
|
70
|
+
try {
|
|
71
|
+
new URL(config.uploadUrl);
|
|
72
|
+
} catch {
|
|
73
|
+
console.error(`[MCP] 错误: 无效的上传接口地址: ${config.uploadUrl}`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 获取配置(单例)
|
|
80
|
+
*/
|
|
81
|
+
let cachedConfig: Config | null = null;
|
|
82
|
+
|
|
83
|
+
export function getConfig(): Config {
|
|
84
|
+
if (cachedConfig) {
|
|
85
|
+
return cachedConfig;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const parsedArgs = parseArgs();
|
|
89
|
+
cachedConfig = {
|
|
90
|
+
...DEFAULT_CONFIG,
|
|
91
|
+
...parsedArgs,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
validateConfig(cachedConfig);
|
|
95
|
+
|
|
96
|
+
console.error(`[MCP] 配置已加载: UPLOAD_ENDPOINT=${cachedConfig.uploadUrl}`);
|
|
97
|
+
|
|
98
|
+
return cachedConfig;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 重置配置(仅用于测试)
|
|
103
|
+
*/
|
|
104
|
+
export function resetConfig(): void {
|
|
105
|
+
cachedConfig = null;
|
|
106
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
+
import { uploadImageSchema, executeUploadImage } from './tools/upload-image.js';
|
|
6
|
+
import { formatBytes } from './utils/index.js';
|
|
7
|
+
|
|
8
|
+
// 创建 MCP 服务器
|
|
9
|
+
const server = new McpServer({
|
|
10
|
+
name: 'mcp-image-uploader',
|
|
11
|
+
version: '1.0.0',
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// 注册 upload_image 工具
|
|
15
|
+
server.tool(
|
|
16
|
+
'upload_image',
|
|
17
|
+
'压缩并上传图片到图床。支持本地文件路径或远程 URL 作为输入,智能压缩后返回图床 URL。',
|
|
18
|
+
uploadImageSchema.shape,
|
|
19
|
+
async (params) => {
|
|
20
|
+
try {
|
|
21
|
+
const result = await executeUploadImage(params);
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
content: [
|
|
25
|
+
{
|
|
26
|
+
type: 'text' as const,
|
|
27
|
+
text: JSON.stringify({
|
|
28
|
+
success: true,
|
|
29
|
+
url: result.url,
|
|
30
|
+
stats: {
|
|
31
|
+
originalSize: formatBytes(result.originalSize),
|
|
32
|
+
compressedSize: formatBytes(result.compressedSize),
|
|
33
|
+
compressionRatio: `${result.compressionRatio}%`,
|
|
34
|
+
},
|
|
35
|
+
}, null, 2),
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
} catch (error) {
|
|
40
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
41
|
+
console.error(`[MCP] 错误: ${message}`);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
content: [
|
|
45
|
+
{
|
|
46
|
+
type: 'text' as const,
|
|
47
|
+
text: JSON.stringify({
|
|
48
|
+
success: false,
|
|
49
|
+
error: message,
|
|
50
|
+
}, null, 2),
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
isError: true,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// 启动服务器
|
|
60
|
+
async function main() {
|
|
61
|
+
const transport = new StdioServerTransport();
|
|
62
|
+
await server.connect(transport);
|
|
63
|
+
console.error('[MCP] Image Uploader Server 已启动');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
main().catch((error) => {
|
|
67
|
+
console.error('[MCP] 服务器启动失败:', error);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
});
|