/* METADATA { "name": "minimax_draw", "display_name": { "zh": "MiniMax 绘图", "en": "MiniMax Draw" }, "description": { "zh": "使用 MiniMax 官方图像生成接口 (/v1/image_generation) 生成图片,支持文生图和带参考图的生图;结果保存到本地 /sdcard/Download/Operit/plugins/draw/minimax_draw/draws/ 目录,并返回 Markdown 图片提示。", "en": "Generate images with the official MiniMax image generation API (/v1/image_generation). Supports text-to-image and reference-image generation. Saves results to /sdcard/Download/Operit/plugins/draw/minimax_draw/draws/ and returns Markdown image hints." }, "category": "Draw", "env": [ { "name": "MINIMAX_API_KEY", "description": { "zh": "MiniMax API Key(必填)", "en": "MiniMax API key (required)" }, "required": true }, { "name": "MINIMAX_API_BASE_URL", "description": { "zh": "MiniMax API Base URL(可选;默认 https://api.minimaxi.com,国际站可改为 https://api.minimax.io)", "en": "MiniMax API base URL (optional; defaults to https://api.minimaxi.com, international users can use https://api.minimax.io)" }, "required": false }, { "name": "MINIMAX_IMAGE_MODEL", "description": { "zh": "默认图片模型(可选;未传 model 时使用,默认 image-01)", "en": "Default image model (optional; used when model is omitted, default image-01)" }, "required": false }, { "name": "BEEIMG_API_KEY", "description": { "zh": "BeeIMG API Key(可选;仅当 image_paths 传本地参考图路径时需要,用于先上传到公网图床)", "en": "BeeIMG API key (optional; only needed when image_paths is used so local reference images can be uploaded first)" }, "required": false } ], "tools": [ { "name": "draw_image", "description": { "zh": "根据提示词调用 MiniMax 官方绘图接口生成图片,支持文生图和参考图生图,保存到本地并返回 Markdown 图片提示。", "en": "Generate images with the official MiniMax image API using a prompt. Supports text-to-image and reference-image generation, saves locally, and returns Markdown image hints." }, "parameters": [ { "name": "prompt", "description": { "zh": "绘图提示词(中文或英文)", "en": "Image prompt (Chinese or English)" }, "type": "string", "required": true }, { "name": "model", "description": { "zh": "模型名称(可选;不传则使用环境变量 MINIMAX_IMAGE_MODEL,否则默认 image-01)", "en": "Model name (optional; falls back to MINIMAX_IMAGE_MODEL, then image-01)" }, "type": "string", "required": false }, { "name": "aspect_ratio", "description": { "zh": "输出比例,可选 1:1、16:9、4:3、3:2、2:3、3:4、9:16、21:9;21:9 仅 image-01 支持", "en": "Aspect ratio. Supported: 1:1, 16:9, 4:3, 3:2, 2:3, 3:4, 9:16, 21:9. 21:9 is only supported by image-01." }, "type": "string", "required": false }, { "name": "width", "description": { "zh": "输出宽度(可选;仅 image-01 支持;需与 height 一起传,范围 512-2048 且为 8 的倍数)", "en": "Output width (optional; image-01 only; must be provided together with height, range 512-2048 and multiple of 8)" }, "type": "number", "required": false }, { "name": "height", "description": { "zh": "输出高度(可选;仅 image-01 支持;需与 width 一起传,范围 512-2048 且为 8 的倍数)", "en": "Output height (optional; image-01 only; must be provided together with width, range 512-2048 and multiple of 8)" }, "type": "number", "required": false }, { "name": "n", "description": { "zh": "生成图片数量,范围 1-9,默认 1", "en": "Number of images to generate, range 1-9, default 1" }, "type": "number", "required": false }, { "name": "response_format", "description": { "zh": "返回格式,可选 url 或 base64;默认 base64(更适合直接保存到本地)", "en": "Response format: url or base64. Defaults to base64 for direct local saving." }, "type": "string", "required": false }, { "name": "prompt_optimizer", "description": { "zh": "是否启用提示词优化(可选)", "en": "Enable prompt optimizer (optional)" }, "type": "boolean", "required": false }, { "name": "aigc_watermark", "description": { "zh": "是否添加 AIGC 水印(可选)", "en": "Add AIGC watermark (optional)" }, "type": "boolean", "required": false }, { "name": "seed", "description": { "zh": "随机种子(可选)", "en": "Seed (optional)" }, "type": "number", "required": false }, { "name": "image_urls", "description": { "zh": "参考图公网 URL 数组(可选;图生图用)。支持字符串数组、JSON 字符串或逗号分隔字符串", "en": "Reference image public URL list (optional; for reference-image generation). Accepts string array, JSON string, or comma-separated string." }, "type": "array", "required": false }, { "name": "image_paths", "description": { "zh": "参考图本地路径数组(可选;会先上传到图床再提交给 MiniMax)。支持字符串数组、JSON 字符串或逗号分隔字符串", "en": "Reference local image path list (optional; local images are uploaded first before calling MiniMax). Accepts string array, JSON string, or comma-separated string." }, "type": "array", "required": false }, { "name": "file_name", "description": { "zh": "自定义本地文件名(不含扩展名)", "en": "Custom local output file name without extension" }, "type": "string", "required": false }, { "name": "api_base_url", "description": { "zh": "自定义 MiniMax API Base URL(可选)", "en": "Custom MiniMax API base URL (optional)" }, "type": "string", "required": false } ] } ] }*/ /// const minimaxDraw = (function () { const HTTP_TIMEOUT_MS = 600000; const client = OkHttp.newBuilder() .connectTimeout(HTTP_TIMEOUT_MS) .readTimeout(HTTP_TIMEOUT_MS) .writeTimeout(HTTP_TIMEOUT_MS) .build(); const DEFAULT_API_BASE_URL = "https://api.minimaxi.com"; const DEFAULT_MODEL = "image-01"; const DEFAULT_RESPONSE_FORMAT = "base64"; const DEFAULT_IMAGE_COUNT = 1; const DEFAULT_BASE64_EXTENSION = "jpeg"; const SUPPORTED_ASPECT_RATIOS = ["1:1", "16:9", "4:3", "3:2", "2:3", "3:4", "9:16", "21:9"]; const BEEIMG_UPLOAD_ENDPOINT = "https://beeimg.com/api/upload/file/json/"; const DRAW_ROOT_DIR = getPluginConfigDir("draw"); const STORAGE_DIR = `${DRAW_ROOT_DIR}/minimax_draw`; const DRAWS_DIR = `${STORAGE_DIR}/draws`; function isRecord(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } function getErrorMessage(error) { if (error instanceof Error) return error.message; return String(error); } function getApiKey() { const apiKey = String(getEnv("MINIMAX_API_KEY") || "").trim(); if (!apiKey) { throw new Error("MINIMAX_API_KEY 未配置,请先在环境变量中设置 MiniMax API Key。"); } return apiKey; } function joinUrl(baseUrl, path) { const normalizedBase = String(baseUrl || "").endsWith("/") ? String(baseUrl || "") : `${baseUrl}/`; const normalizedPath = String(path || "").startsWith("/") ? String(path).slice(1) : String(path || ""); return `${normalizedBase}${normalizedPath}`; } function getApiBaseUrl(customBaseUrl) { const fromParam = String(customBaseUrl || "").trim(); if (fromParam) return fromParam; const fromEnv = String(getEnv("MINIMAX_API_BASE_URL") || "").trim(); if (fromEnv) return fromEnv; return DEFAULT_API_BASE_URL; } function getImageEndpoint(baseUrl) { const trimmed = String(baseUrl || "").trim(); if (!trimmed) return joinUrl(DEFAULT_API_BASE_URL, "v1/image_generation"); if (trimmed.includes("/v1/image_generation")) return trimmed; if (trimmed.endsWith("/v1")) return joinUrl(trimmed, "image_generation"); if (trimmed.endsWith("/v1/")) return joinUrl(trimmed, "image_generation"); return joinUrl(trimmed, "v1/image_generation"); } function resolveModel(model) { const fromParam = String(model || "").trim(); if (fromParam) return fromParam; const fromEnv = String(getEnv("MINIMAX_IMAGE_MODEL") || "").trim(); if (fromEnv) return fromEnv; return DEFAULT_MODEL; } function sanitizeFileName(name, fallbackPrefix) { const safe = String(name || "").replace(/[\\/:*?"<>|]/g, "_").trim(); if (!safe) return `${fallbackPrefix}_${Date.now()}`; return safe.substring(0, 80); } function buildFileBaseName(prompt, customName, fallbackPrefix) { const fromCustom = String(customName || "").trim(); if (fromCustom) { return sanitizeFileName(fromCustom, fallbackPrefix); } const rawPrompt = String(prompt || "").trim(); const shortPrompt = rawPrompt.length > 40 ? `${rawPrompt.substring(0, 40)}...` : rawPrompt; return `${sanitizeFileName(shortPrompt || fallbackPrefix, fallbackPrefix)}_${Date.now()}`; } async function ensureDirectories() { const dirs = [DRAW_ROOT_DIR, STORAGE_DIR, DRAWS_DIR]; for (const dir of dirs) { try { const result = await Tools.Files.mkdir(dir); if (!result.successful) { console.warn(`创建目录失败(可能已存在): ${dir} -> ${result.details}`); } } catch (error) { console.warn(`创建目录异常: ${dir} -> ${getErrorMessage(error)}`); } } } function parsePositiveInteger(value, fieldName, options?) { const hasDefault = !!options && Object.prototype.hasOwnProperty.call(options, "defaultValue"); const defaultValue = hasDefault ? options.defaultValue : undefined; if (value === undefined || value === null || value === "") { return defaultValue; } const numberValue = typeof value === "number" ? value : Number(String(value)); if (!Number.isFinite(numberValue) || numberValue <= 0 || !Number.isInteger(numberValue)) { throw new Error(`${fieldName} 必须是正整数。`); } if (options && options.min !== undefined && numberValue < options.min) { throw new Error(`${fieldName} 不能小于 ${options.min}。`); } if (options && options.max !== undefined && numberValue > options.max) { throw new Error(`${fieldName} 不能大于 ${options.max}。`); } return numberValue; } function parseInteger(value, fieldName) { if (value === undefined || value === null || value === "") { return undefined; } const numberValue = typeof value === "number" ? value : Number(String(value)); if (!Number.isFinite(numberValue) || !Number.isInteger(numberValue)) { throw new Error(`${fieldName} 必须是整数。`); } return numberValue; } function parseBoolean(value, fieldName) { if (value === undefined || value === null || value === "") { return undefined; } if (typeof value === "boolean") { return value; } const normalized = String(value).trim().toLowerCase(); if (normalized === "true" || normalized === "1" || normalized === "yes") { return true; } if (normalized === "false" || normalized === "0" || normalized === "no") { return false; } throw new Error(`${fieldName} 必须是布尔值。`); } function normalizeResponseFormat(value) { const normalized = String(value || DEFAULT_RESPONSE_FORMAT).trim().toLowerCase(); if (normalized !== "url" && normalized !== "base64") { throw new Error("response_format 仅支持 url 或 base64。"); } return normalized; } function normalizeAspectRatio(model, value) { const normalized = String(value || "").trim(); if (!normalized) return undefined; if (!SUPPORTED_ASPECT_RATIOS.includes(normalized)) { throw new Error(`aspect_ratio 仅支持 ${SUPPORTED_ASPECT_RATIOS.join("、")}。`); } if (normalized === "21:9" && String(model || "").trim() !== "image-01") { throw new Error("aspect_ratio=21:9 仅支持 model=image-01。"); } return normalized; } function normalizeDimensions(model, widthValue, heightValue, aspectRatio) { if (aspectRatio) { return { width: undefined, height: undefined }; } const width = parsePositiveInteger(widthValue, "width"); const height = parsePositiveInteger(heightValue, "height"); if (width === undefined && height === undefined) { return { width: undefined, height: undefined }; } if (width === undefined || height === undefined) { throw new Error("width 和 height 需要同时传入。"); } if (String(model || "").trim() !== "image-01") { throw new Error("width 和 height 仅支持 model=image-01。"); } if (width < 512 || width > 2048 || width % 8 !== 0) { throw new Error("width 需要在 512-2048 之间,且必须为 8 的倍数。"); } if (height < 512 || height > 2048 || height % 8 !== 0) { throw new Error("height 需要在 512-2048 之间,且必须为 8 的倍数。"); } return { width, height }; } function parseStringList(value, fieldName) { if (value === undefined || value === null || value === "") { return []; } if (Array.isArray(value)) { return value .map(item => String(item || "").trim()) .filter(item => item.length > 0); } if (typeof value === "string") { const trimmed = value.trim(); if (!trimmed) return []; try { const parsed = JSON.parse(trimmed); if (Array.isArray(parsed)) { return parsed .map(item => String(item || "").trim()) .filter(item => item.length > 0); } } catch { // ignore and try comma-separated parsing } return trimmed .split(",") .map(item => item.trim()) .filter(item => item.length > 0); } throw new Error(`${fieldName} 必须是字符串数组、JSON 字符串或逗号分隔字符串。`); } function isProbablyUrl(value) { return /^https?:\/\//i.test(String(value || "").trim()); } function getBeeimgApiKey() { return String(getEnv("BEEIMG_API_KEY") || "").trim(); } function guessMimeTypeFromPath(filePath) { const lower = String(filePath || "").toLowerCase(); if (lower.endsWith(".png")) return "image/png"; if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg"; if (lower.endsWith(".webp")) return "image/webp"; if (lower.endsWith(".gif")) return "image/gif"; return "application/octet-stream"; } async function uploadImageToBeeimg(filePath) { const exists = await Tools.Files.exists(filePath); if (!exists.exists) { throw new Error(`参考图文件不存在: ${filePath}`); } const apiKey = getBeeimgApiKey(); if (!apiKey) { throw new Error("使用 image_paths 需要配置 BEEIMG_API_KEY,用于先把本地参考图上传到公网图床。"); } const response = await Tools.Net.uploadFile({ url: BEEIMG_UPLOAD_ENDPOINT, method: "POST", form_data: { apikey: apiKey }, files: [ { field_name: "file", file_path: filePath, content_type: guessMimeTypeFromPath(filePath) } ] }); if (response.statusCode < 200 || response.statusCode >= 300) { throw new Error(`BeeIMG 上传失败: HTTP ${response.statusCode} - ${response.content}`); } let parsed; try { parsed = JSON.parse(response.content); } catch (error) { throw new Error(`解析 BeeIMG 响应失败: ${getErrorMessage(error)}`); } const files = isRecord(parsed) && isRecord(parsed.files) ? parsed.files : null; const fileUrl = files && files.url ? String(files.url).trim() : ""; const code = files && files.code !== undefined ? String(files.code) : ""; const status = files && files.status ? String(files.status) : ""; if (!fileUrl || (code !== "200" && code !== "" && status !== "Success")) { throw new Error(`BeeIMG 上传失败: ${response.content}`); } return fileUrl; } async function resolveSubjectReference(imageUrls, imagePaths) { const resolvedUrls = parseStringList(imageUrls, "image_urls"); const resolvedPaths = parseStringList(imagePaths, "image_paths"); for (const url of resolvedUrls) { if (!isProbablyUrl(url)) { throw new Error(`image_urls 中包含无效链接: ${url}`); } } for (const filePath of resolvedPaths) { const uploadedUrl = await uploadImageToBeeimg(filePath); resolvedUrls.push(uploadedUrl); } if (resolvedUrls.length === 0) { return undefined; } return resolvedUrls.map(url => ({ type: "character", image_file: url })); } function normalizeBase64(base64) { const raw = String(base64 || "").trim(); if (!raw) return raw; const prefixIndex = raw.indexOf("base64,"); if (raw.startsWith("data:") && prefixIndex >= 0) { return raw.substring(prefixIndex + "base64,".length).trim(); } return raw; } function guessExtensionFromUrl(url, fallbackExtension) { const match = String(url || "").match(/\.([a-z0-9]{2,5})(?:[?#].*)?$/i); if (match && match[1]) return match[1].toLowerCase(); return fallbackExtension; } function parseMiniMaxError(parsed, fallbackMessage) { const baseResp = isRecord(parsed) && isRecord(parsed.base_resp) ? parsed.base_resp : null; if (!baseResp) return; const statusCode = baseResp.status_code !== undefined ? Number(baseResp.status_code) : 0; if (Number.isFinite(statusCode) && statusCode !== 0) { const statusMsg = baseResp.status_msg ? String(baseResp.status_msg) : fallbackMessage; throw new Error(`MiniMax 图片接口返回错误: ${statusMsg} (status_code=${statusCode})`); } } async function callMiniMaxImageApi(params) { const apiBaseUrl = getApiBaseUrl(params.api_base_url); const endpoint = getImageEndpoint(apiBaseUrl); const effectiveModel = resolveModel(params.model); const responseFormat = normalizeResponseFormat(params.response_format); const aspectRatio = normalizeAspectRatio(effectiveModel, params.aspect_ratio); const dimensions = normalizeDimensions(effectiveModel, params.width, params.height, aspectRatio); const imageCount = parsePositiveInteger(params.n, "n", { defaultValue: DEFAULT_IMAGE_COUNT, min: 1, max: 9 }); const promptOptimizer = parseBoolean(params.prompt_optimizer, "prompt_optimizer"); const aigcWatermark = parseBoolean(params.aigc_watermark, "aigc_watermark"); const seed = parseInteger(params.seed, "seed"); const subjectReference = await resolveSubjectReference(params.image_urls, params.image_paths); const body: Record = { model: effectiveModel, prompt: String(params.prompt || "").trim(), response_format: responseFormat, n: imageCount }; if (aspectRatio) { body.aspect_ratio = aspectRatio; } if (dimensions.width !== undefined && dimensions.height !== undefined) { body.width = dimensions.width; body.height = dimensions.height; } if (promptOptimizer !== undefined) { body.prompt_optimizer = promptOptimizer; } if (aigcWatermark !== undefined) { body.aigc_watermark = aigcWatermark; } if (seed !== undefined) { body.seed = seed; } if (subjectReference) { body.subject_reference = subjectReference; } const request = client .newRequest() .url(endpoint) .method("POST") .headers({ "accept": "application/json", "content-type": "application/json", "Authorization": `Bearer ${getApiKey()}` }) .body(JSON.stringify(body), "json"); const response = await request.build().execute(); if (!response.isSuccessful()) { throw new Error(`MiniMax 图片 API 调用失败: ${response.statusCode} - ${response.content}`); } let parsed; try { parsed = JSON.parse(response.content); } catch (error) { throw new Error(`解析 MiniMax 响应失败: ${getErrorMessage(error)}`); } parseMiniMaxError(parsed, "MiniMax 接口返回错误"); const data = isRecord(parsed) && isRecord(parsed.data) ? parsed.data : null; if (!data) { throw new Error("MiniMax 响应中未找到 data 对象。"); } const metadata = isRecord(data.metadata) ? data.metadata : null; const successCount = metadata && metadata.success_count !== undefined ? Number(metadata.success_count) : undefined; const failedCount = metadata && metadata.failed_count !== undefined ? Number(metadata.failed_count) : undefined; const requestId = isRecord(parsed) && parsed.id ? String(parsed.id) : ""; if (responseFormat === "base64") { const imageBase64 = Array.isArray(data.image_base64) ? data.image_base64.map(item => String(item || "").trim()).filter(item => item.length > 0) : []; if (imageBase64.length === 0) { throw new Error("MiniMax 响应中未找到 data.image_base64。"); } return { request_id: requestId, effective_model: effectiveModel, response_format: responseFormat, image_base64: imageBase64, image_urls: [], success_count: Number.isFinite(successCount) ? successCount : imageBase64.length, failed_count: Number.isFinite(failedCount) ? failedCount : 0, reference_count: subjectReference ? subjectReference.length : 0 }; } const imageUrls = Array.isArray(data.image_urls) ? data.image_urls.map(item => String(item || "").trim()).filter(item => item.length > 0) : []; if (imageUrls.length === 0) { throw new Error("MiniMax 响应中未找到 data.image_urls。"); } return { request_id: requestId, effective_model: effectiveModel, response_format: responseFormat, image_base64: [], image_urls: imageUrls, success_count: Number.isFinite(successCount) ? successCount : imageUrls.length, failed_count: Number.isFinite(failedCount) ? failedCount : 0, reference_count: subjectReference ? subjectReference.length : 0 }; } async function draw_image(params) { const prompt = String(params && params.prompt ? params.prompt : "").trim(); if (!prompt) { throw new Error("参数 prompt 不能为空。"); } await ensureDirectories(); const apiResult = await callMiniMaxImageApi({ prompt, model: params.model, aspect_ratio: params.aspect_ratio, width: params.width, height: params.height, n: params.n, response_format: params.response_format, prompt_optimizer: params.prompt_optimizer, aigc_watermark: params.aigc_watermark, seed: params.seed, image_urls: params.image_urls, image_paths: params.image_paths, api_base_url: params.api_base_url }); const baseName = buildFileBaseName(prompt, params.file_name, "minimax_draw"); const files: Array<{ file_path: string; file_uri: string; markdown: string; remote_image_url: string | null; }> = []; if (apiResult.response_format === "base64") { for (let index = 0; index < apiResult.image_base64.length; index += 1) { const suffix = apiResult.image_base64.length > 1 ? `_${index + 1}` : ""; const filePath = `${DRAWS_DIR}/${baseName}${suffix}.${DEFAULT_BASE64_EXTENSION}`; const writeResult = await Tools.Files.writeBinary(filePath, normalizeBase64(apiResult.image_base64[index])); if (!writeResult.successful) { throw new Error(`保存图片失败: ${writeResult.details}`); } const fileUri = `file://${filePath}`; files.push({ file_path: filePath, file_uri: fileUri, markdown: `![MiniMax 生成的图片${apiResult.image_base64.length > 1 ? ` ${index + 1}` : ""}](${fileUri})`, remote_image_url: null }); } } else { for (let index = 0; index < apiResult.image_urls.length; index += 1) { const imageUrl = apiResult.image_urls[index]; const extension = guessExtensionFromUrl(imageUrl, "png"); const suffix = apiResult.image_urls.length > 1 ? `_${index + 1}` : ""; const filePath = `${DRAWS_DIR}/${baseName}${suffix}.${extension}`; const downloadResult = await Tools.Files.download(imageUrl, filePath); if (!downloadResult.successful) { throw new Error(`下载图片失败: ${downloadResult.details}`); } const fileUri = `file://${filePath}`; files.push({ file_path: filePath, file_uri: fileUri, markdown: `![MiniMax 生成的图片${apiResult.image_urls.length > 1 ? ` ${index + 1}` : ""}](${fileUri})`, remote_image_url: imageUrl }); } } const hintLines: string[] = []; hintLines.push(`图片已生成并保存在本地 ${DRAWS_DIR}。`); hintLines.push(`共生成 ${files.length} 张。`); if (apiResult.reference_count > 0) { hintLines.push(`本次使用了 ${apiResult.reference_count} 张参考图。`); } hintLines.push(""); hintLines.push("后续回答如果需要展示图片,请直接输出下面这些 Markdown:"); hintLines.push(""); for (const fileItem of files) { hintLines.push(fileItem.markdown); } return { prompt, model: apiResult.effective_model, request_id: apiResult.request_id || null, response_format: apiResult.response_format, success_count: apiResult.success_count, failed_count: apiResult.failed_count, reference_count: apiResult.reference_count, file_path: files[0].file_path, file_uri: files[0].file_uri, markdown: files[0].markdown, files, hint: hintLines.join("\n") }; } async function draw_image_wrapper(params) { try { const result = await draw_image(params || {}); complete({ success: true, message: "MiniMax 图片生成成功,已保存到本地。", data: result }); } catch (error) { console.error("draw_image 执行失败:", error); complete({ success: false, message: `MiniMax 图片生成失败: ${getErrorMessage(error)}`, error_stack: error && error.stack ? error.stack : undefined }); } } return { draw_image: draw_image_wrapper }; })(); exports.draw_image = minimaxDraw.draw_image;