212 lines
9.3 KiB
JavaScript
212 lines
9.3 KiB
JavaScript
import { z } from "zod";
|
|
import axios from "axios";
|
|
import sharp from "sharp";
|
|
import { withAuth, getSessionCredentials } from "../../auth/index.js";
|
|
import { handleToolError } from "../helpers/errorHandler.js";
|
|
import { saveFileBuilder } from "../helpers/fileBuilder.js";
|
|
import { withAuthParams } from "../helpers/authSchema.js";
|
|
|
|
/**
|
|
* Upload an image to the website assets folder
|
|
* Accepts base64, data URI, or URL
|
|
* Optionally resizes/compresses the image
|
|
*/
|
|
export function registerUploadImageToAssetsTool(server) {
|
|
server.tool(
|
|
"upload_image_to_assets",
|
|
"Upload an image to website assets (/images/). Accepts: base64, data URI, or URL. Optional resize (maxWidth/maxHeight) and compression (quality). Returns public URL.",
|
|
withAuthParams({
|
|
image: z.string().describe("Image data: base64 string, data URI, or URL to download from"),
|
|
fileName: z.string().optional().describe("Custom filename (without extension). If not provided, auto-generated name will be used"),
|
|
path: z.string().optional().default("/images/").describe("Path within assets folder (default: '/images/')"),
|
|
|
|
// Resize options
|
|
maxWidth: z.number().optional().describe("Maximum width in pixels. Image will be resized proportionally if larger"),
|
|
maxHeight: z.number().optional().describe("Maximum height in pixels. Image will be resized proportionally if larger"),
|
|
quality: z.number().min(1).max(100).optional().default(85).describe("JPEG/WebP quality (1-100, default: 85)"),
|
|
format: z.enum(["png", "jpg", "webp"]).optional().default("png").describe("Output format (default: png)"),
|
|
}),
|
|
{ readOnlyHint: false, destructiveHint: false },
|
|
withAuth(async ({
|
|
image,
|
|
fileName,
|
|
path: assetsPath = "/images/",
|
|
maxWidth,
|
|
maxHeight,
|
|
quality = 85,
|
|
format = "png"
|
|
}, extra) => {
|
|
try {
|
|
let imageBuffer;
|
|
|
|
// Step 1: Get image buffer from various sources
|
|
if (image.startsWith('data:')) {
|
|
// Data URI format: data:image/png;base64,xxxxx
|
|
const match = image.match(/data:image\/[^;]+;base64,(.+)/);
|
|
if (match) {
|
|
imageBuffer = Buffer.from(match[1], 'base64');
|
|
} else {
|
|
return {
|
|
content: [{
|
|
type: "text",
|
|
text: "Error: Invalid data URI format. Expected: data:image/xxx;base64,..."
|
|
}],
|
|
isError: true,
|
|
};
|
|
}
|
|
} else if (image.startsWith('http://') || image.startsWith('https://')) {
|
|
// URL - download the image
|
|
try {
|
|
const response = await axios.get(image, {
|
|
responseType: 'arraybuffer',
|
|
timeout: 30000,
|
|
maxContentLength: 50 * 1024 * 1024 // 50MB max
|
|
});
|
|
imageBuffer = Buffer.from(response.data, 'binary');
|
|
} catch (downloadError) {
|
|
return {
|
|
content: [{
|
|
type: "text",
|
|
text: `Error downloading image from URL: ${downloadError.message}`
|
|
}],
|
|
isError: true,
|
|
};
|
|
}
|
|
} else {
|
|
// Assume it's raw base64
|
|
try {
|
|
imageBuffer = Buffer.from(image, 'base64');
|
|
} catch (base64Error) {
|
|
return {
|
|
content: [{
|
|
type: "text",
|
|
text: "Error: Could not parse image data. Provide base64, data URI, or URL"
|
|
}],
|
|
isError: true,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Validate we have a buffer
|
|
if (!imageBuffer || imageBuffer.length === 0) {
|
|
return {
|
|
content: [{
|
|
type: "text",
|
|
text: "Error: No valid image data received"
|
|
}],
|
|
isError: true,
|
|
};
|
|
}
|
|
|
|
// Step 2: Process image with sharp (resize/compress)
|
|
let sharpInstance = sharp(imageBuffer);
|
|
|
|
// Get original metadata
|
|
const metadata = await sharpInstance.metadata();
|
|
const originalSize = imageBuffer.length;
|
|
|
|
// Resize if dimensions specified
|
|
if (maxWidth || maxHeight) {
|
|
sharpInstance = sharpInstance.resize({
|
|
width: maxWidth,
|
|
height: maxHeight,
|
|
fit: 'inside', // Maintain aspect ratio
|
|
withoutEnlargement: true // Don't upscale
|
|
});
|
|
}
|
|
|
|
// Convert to target format with quality
|
|
let outputBuffer;
|
|
let mimeType;
|
|
let extension;
|
|
|
|
switch (format) {
|
|
case 'jpg':
|
|
outputBuffer = await sharpInstance.jpeg({ quality }).toBuffer();
|
|
mimeType = 'image/jpeg';
|
|
extension = 'jpg';
|
|
break;
|
|
case 'webp':
|
|
outputBuffer = await sharpInstance.webp({ quality }).toBuffer();
|
|
mimeType = 'image/webp';
|
|
extension = 'webp';
|
|
break;
|
|
case 'png':
|
|
default:
|
|
outputBuffer = await sharpInstance.png({
|
|
compressionLevel: Math.floor((100 - quality) / 11) // 0-9 compression
|
|
}).toBuffer();
|
|
mimeType = 'image/png';
|
|
extension = 'png';
|
|
break;
|
|
}
|
|
|
|
// Step 3: Upload to assets
|
|
const base64Image = outputBuffer.toString('base64');
|
|
|
|
// Generate filename if not provided
|
|
const finalFileName = fileName
|
|
? fileName.replace(/\.(jpg|jpeg|png|webp|gif)$/i, '') + '.' + extension
|
|
: `uploaded-${Date.now()}.${extension}`;
|
|
|
|
// Get credentials
|
|
const credentials = await getSessionCredentials(extra.sessionId);
|
|
|
|
// Upload using saveFileBuilder
|
|
const uploadResult = await saveFileBuilder({
|
|
web_url: credentials.api_web_url || credentials.web_url,
|
|
token: credentials.token,
|
|
tokenHash: credentials.tokenHash,
|
|
path: assetsPath,
|
|
fileName: finalFileName,
|
|
content: base64Image,
|
|
rawDataSended: false
|
|
});
|
|
|
|
if (uploadResult && uploadResult.success) {
|
|
// Build the public URL for the uploaded image
|
|
const imageUrl = `${credentials.web_url}/template/estandar/images/${finalFileName}`;
|
|
|
|
// Get final metadata
|
|
const finalMetadata = await sharp(outputBuffer).metadata();
|
|
|
|
return {
|
|
content: [{
|
|
type: "text",
|
|
text: JSON.stringify({
|
|
success: true,
|
|
imageUrl: imageUrl,
|
|
fileName: finalFileName,
|
|
path: assetsPath,
|
|
format: format,
|
|
originalSize: originalSize,
|
|
finalSize: outputBuffer.length,
|
|
compressionRatio: ((1 - outputBuffer.length / originalSize) * 100).toFixed(1) + '%',
|
|
dimensions: {
|
|
original: { width: metadata.width, height: metadata.height },
|
|
final: { width: finalMetadata.width, height: finalMetadata.height }
|
|
},
|
|
message: `Image uploaded successfully. Use this URL: ${imageUrl}`
|
|
}, null, 2)
|
|
}],
|
|
};
|
|
} else {
|
|
return {
|
|
content: [{
|
|
type: "text",
|
|
text: JSON.stringify({
|
|
success: false,
|
|
error: uploadResult?.message || "Unknown error uploading to assets"
|
|
}, null, 2)
|
|
}],
|
|
isError: true,
|
|
};
|
|
}
|
|
|
|
} catch (error) {
|
|
return handleToolError(error, 'upload_image_to_assets', { fileName, path: assetsPath });
|
|
}
|
|
})
|
|
);
|
|
}
|