import { VM } from "vm2"; import sharp from "sharp"; import axios from "axios"; import { createOpenAI } from "@ai-sdk/openai"; import { createDeepSeek } from "@ai-sdk/deepseek"; import { createZhipu } from "zhipu-ai-provider"; import { createQwen } from "qwen-ai-provider-v5"; import { createGoogleGenerativeAI } from "@ai-sdk/google"; import { createAnthropic } from "@ai-sdk/anthropic"; import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; import { createXai } from "@ai-sdk/xai"; import FormData from "form-data"; export default function runCode(code: string) { // 创建一个沙盒 const exports = {}; const vm = new VM({ timeout: 0, sandbox: { createOpenAI, createDeepSeek, createZhipu, createQwen, createAnthropic, createOpenAICompatible, createXai, createGoogleGenerativeAI, zipImage, zipImageResolution, urlToBase64, mergeImages, pollTask, Response, exports, fetch, axios, FormData, }, compiler: "javascript", eval: false, wasm: false, }); vm.run(code); return exports as Record; } /** * 压缩图片,目标字节数不高于 size */ export async function zipImage(completeBase64: string, size: number): Promise { let quality = 80; let buffer = Buffer.from(completeBase64.split(",")[1], "base64"); let output = await sharp(buffer).jpeg({ quality }).toBuffer(); while (output.length > size && quality > 10) { quality -= 10; output = await sharp(buffer).jpeg({ quality }).toBuffer(); } return "data:image/jpeg;base64," + output.toString("base64"); } export async function zipImageResolution(completeBase64: string, width: number, height: number): Promise { const buffer = Buffer.from(completeBase64.split(",")[1], "base64"); const out = await sharp(buffer).resize(width, height).toBuffer(); return `data:image/jpeg;base64,${out.toString("base64")}`; } //url转Base64 export async function urlToBase64(url: string): Promise { const res = await axios.get(url, { responseType: "arraybuffer" }); const mime = res.headers["content-type"] || "image/jpeg"; const b64 = Buffer.from(res.data).toString("base64"); return `data:${mime};base64,${b64}`; } export async function pollTask( fn: () => Promise<{ completed: boolean; data?: string; error?: string }>, interval = 3000, timeout = 3000000, ): Promise<{ completed: boolean; data?: string; error?: string }> { const start = Date.now(); while (Date.now() - start < timeout) { try { const result = await fn(); if (result.completed) return result; } catch (e: any) { return { completed: false, error: e?.message || "poll error" }; } await new Promise((res) => setTimeout(res, interval)); } return { completed: false, error: "timeout" }; } /** * 将多张图片横向拼接为一张,并确保输出大小不超过指定限制 * @param imageBase64List - base64编码的图片数组 * @param maxSize - 最大输出大小,支持格式如 "10mb", "5MB", "1024kb" 等 * @returns 拼接后的图片base64字符串 */ export async function mergeImages(imageBase64List: string[], maxSize = "10mb"): Promise { if (imageBase64List.length === 0) { throw new Error("图片列表不能为空"); } const maxBytes = parseSize(maxSize); const imageBuffers = imageBase64List.map(base64ToBuffer); const imageMetadatas = await Promise.all(imageBuffers.map((buffer) => sharp(buffer).metadata())); const maxHeight = Math.max(...imageMetadatas.map((m) => m.height || 0)); // 计算各图片调整后的宽度 const imageWidths = imageMetadatas.map((metadata) => { const aspectRatio = (metadata.width || 1) / (metadata.height || 1); return Math.round(maxHeight * aspectRatio); }); const totalWidth = imageWidths.reduce((sum, w) => sum + w, 0); // 拼接图片 const resizedImages = await Promise.all( imageBuffers.map(async (buffer, index) => { return sharp(buffer).resize(imageWidths[index], maxHeight, { fit: "cover" }).toBuffer(); }), ); let currentX = 0; const compositeInputs = resizedImages.map((buffer, index) => { const input = { input: buffer, left: currentX, top: 0 }; currentX += imageWidths[index]; return input; }); const mergedBuffer = await sharp({ create: { width: totalWidth, height: maxHeight, channels: 4, background: { r: 255, g: 255, b: 255, alpha: 1 }, }, }) .composite(compositeInputs) .jpeg({ quality: 90 }) .toBuffer(); // 复用压缩逻辑 const resultBuffer = await compressToSize(mergedBuffer, maxBytes, totalWidth, maxHeight); return resultBuffer.toString("base64"); } /** * 解析大小字符串为字节数 */ function parseSize(size: string): number { const match = size.toLowerCase().match(/^(\d+(?:\.\d+)?)\s*(kb|mb|gb|b)?$/); if (!match) { throw new Error(`无效的大小格式: ${size}`); } const value = parseFloat(match[1]); const unit = match[2] || "b"; const multipliers: Record = { b: 1, kb: 1024, mb: 1024 * 1024, gb: 1024 * 1024 * 1024, }; return Math.floor(value * multipliers[unit]); } /** * 将base64字符串转换为Buffer */ function base64ToBuffer(base64: string): Buffer { const base64Data = base64.replace(/^data:image\/\w+;base64,/, ""); return Buffer.from(base64Data, "base64"); } /** * 压缩Buffer到指定大小以内 */ async function compressToSize(imageBuffer: Buffer, maxBytes: number, originalWidth: number, originalHeight: number): Promise { let quality = 90; let scale = 1; while (true) { const targetWidth = Math.round(originalWidth * scale); const targetHeight = Math.round(originalHeight * scale); const resultBuffer = await sharp(imageBuffer).resize(targetWidth, targetHeight, { fit: "fill" }).jpeg({ quality }).toBuffer(); if (resultBuffer.length <= maxBytes) { return resultBuffer; } if (quality > 10) { quality -= 10; } else { quality = 90; scale *= 0.8; } } }