- 新增 operit_runtime.py: 跨平台 Python 运行时 - 实现 Tools.System/Files/Net/UI/Memory/FFmpeg/Workflow/Chat/SoftwareSettings/Tasker - 实现 OkHttp 链式 HTTP 客户端兼容层 - 实现 complete()/exports/console 全局对象 - 实现 OperitScriptLoader 脚本加载器 - 支持 Windows/macOS/Linux 三平台 - 新增 operit_py/ 目录: 47 个 Python 翻译脚本 - 搜索类: duckduckgo, google_search, tavily, zhipu_search, various_search, crossref - 绘图类: openai_draw, qwen_draw, xai_draw, minimax_draw, siliconflow_draw, zhipu_draw, nanobanana_draw - 媒体类: ffmpeg, file_converter, douyin_download, bilibili_tools, jmcomic - 文件类: extended_file_tools, hex_editor, reader, beeimg_image_uploader - 网络类: extended_http_tools, network_test - 自动化类: browser, automatic_ui_base, automatic_ui_subagent, automatic_bilibili_assistant, automatic_xiaohongshu_assistant, automatic_baidu_map_assistant - 系统类: system_tools, super_admin, daily_life, operit_editor - 对话类: ai_chat, extended_chat, qq_intelligent - 工作流类: workflow, vflow_trigger, tasker - 工具类: time, code_runner, quick_start, extended_memory_tools, baidu_map, 12306, pdf_vision_parser - 测试结果: 47/47 语法检查通过, 38 脚本加载成功, 280 个工具注册 - Android 专属功能已降级处理(保留接口,返回不支持提示)
598 lines
26 KiB
Python
598 lines
26 KiB
Python
# 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 }
|
||
# ]
|
||
# }
|
||
# ]
|
||
# }
|
||
|
||
import json
|
||
import math
|
||
import re
|
||
import time
|
||
import traceback
|
||
|
||
HTTP_TIMEOUT_MS = 600000
|
||
client = OkHttp.newBuilder().connectTimeout(HTTP_TIMEOUT_MS).readTimeout(HTTP_TIMEOUT_MS).writeTimeout(HTTP_TIMEOUT_MS).build()
|
||
|
||
DEFAULT_API_BASE_URL = "https://api.minimaxi.com"
|
||
DEFAULT_MODEL = "image-01"
|
||
DEFAULT_RESPONSE_FORMAT = "base64"
|
||
DEFAULT_IMAGE_COUNT = 1
|
||
DEFAULT_BASE64_EXTENSION = "jpeg"
|
||
SUPPORTED_ASPECT_RATIOS = ["1:1", "16:9", "4:3", "3:2", "2:3", "3:4", "9:16", "21:9"]
|
||
BEEIMG_UPLOAD_ENDPOINT = "https://beeimg.com/api/upload/file/json/"
|
||
|
||
DRAW_ROOT_DIR = getPluginConfigDir("draw")
|
||
STORAGE_DIR = f"{DRAW_ROOT_DIR}/minimax_draw"
|
||
DRAWS_DIR = f"{STORAGE_DIR}/draws"
|
||
|
||
|
||
def _is_finite(n):
|
||
return isinstance(n, (int, float)) and not isinstance(n, bool) and math.isfinite(n)
|
||
|
||
|
||
def _is_integer(n):
|
||
return _is_finite(n) and n == int(n)
|
||
|
||
|
||
def isRecord(value):
|
||
return isinstance(value, dict)
|
||
|
||
|
||
def getErrorMessage(error):
|
||
if isinstance(error, Exception):
|
||
return str(error)
|
||
return str(error)
|
||
|
||
|
||
def getApiKey():
|
||
apiKey = str(getEnv("MINIMAX_API_KEY") or "").strip()
|
||
if not apiKey:
|
||
raise Exception("MINIMAX_API_KEY 未配置,请先在环境变量中设置 MiniMax API Key。")
|
||
return apiKey
|
||
|
||
|
||
def joinUrl(baseUrl, path):
|
||
baseStr = str(baseUrl or "")
|
||
normalizedBase = baseStr if baseStr.endswith("/") else f"{baseStr}/"
|
||
pathStr = str(path or "")
|
||
normalizedPath = pathStr[1:] if pathStr.startswith("/") else pathStr
|
||
return f"{normalizedBase}{normalizedPath}"
|
||
|
||
|
||
def getApiBaseUrl(customBaseUrl=None):
|
||
fromParam = str(customBaseUrl or "").strip()
|
||
if fromParam:
|
||
return fromParam
|
||
|
||
fromEnv = str(getEnv("MINIMAX_API_BASE_URL") or "").strip()
|
||
if fromEnv:
|
||
return fromEnv
|
||
|
||
return DEFAULT_API_BASE_URL
|
||
|
||
|
||
def getImageEndpoint(baseUrl):
|
||
trimmed = str(baseUrl or "").strip()
|
||
if not trimmed:
|
||
return joinUrl(DEFAULT_API_BASE_URL, "v1/image_generation")
|
||
if "/v1/image_generation" in trimmed:
|
||
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")
|
||
|
||
|
||
def resolveModel(model):
|
||
fromParam = str(model or "").strip()
|
||
if fromParam:
|
||
return fromParam
|
||
|
||
fromEnv = str(getEnv("MINIMAX_IMAGE_MODEL") or "").strip()
|
||
if fromEnv:
|
||
return fromEnv
|
||
|
||
return DEFAULT_MODEL
|
||
|
||
|
||
def sanitizeFileName(name, fallbackPrefix):
|
||
safe = re.sub(r'[\\/:*?"<>|]', "_", str(name or "")).strip()
|
||
if not safe:
|
||
return f"{fallbackPrefix}_{int(time.time() * 1000)}"
|
||
return safe[:80]
|
||
|
||
|
||
def buildFileBaseName(prompt, customName, fallbackPrefix):
|
||
fromCustom = str(customName or "").strip()
|
||
if fromCustom:
|
||
return sanitizeFileName(fromCustom, fallbackPrefix)
|
||
rawPrompt = str(prompt or "").strip()
|
||
shortPrompt = f"{rawPrompt[:40]}..." if len(rawPrompt) > 40 else rawPrompt
|
||
return f"{sanitizeFileName(shortPrompt or fallbackPrefix, fallbackPrefix)}_{int(time.time() * 1000)}"
|
||
|
||
|
||
async def ensureDirectories():
|
||
dirs = [DRAW_ROOT_DIR, STORAGE_DIR, DRAWS_DIR]
|
||
for dir_ in dirs:
|
||
try:
|
||
result = await Tools.Files.mkdir(dir_)
|
||
if not result.get("successful"):
|
||
console.warn(f"创建目录失败(可能已存在): {dir_} -> {result.get('details')}")
|
||
except Exception as error:
|
||
console.warn(f"创建目录异常: {dir_} -> {getErrorMessage(error)}")
|
||
|
||
|
||
def parsePositiveInteger(value, fieldName, options=None):
|
||
hasDefault = options is not None and "defaultValue" in options
|
||
defaultValue = options["defaultValue"] if hasDefault else None
|
||
|
||
if value is None or value == "":
|
||
return defaultValue
|
||
|
||
numberValue = value if isinstance(value, (int, float)) and not isinstance(value, bool) else float(str(value))
|
||
if not _is_finite(numberValue) or numberValue <= 0 or not _is_integer(numberValue):
|
||
raise Exception(f"{fieldName} 必须是正整数。")
|
||
numberValue = int(numberValue)
|
||
|
||
if options and options.get("min") is not None and numberValue < options["min"]:
|
||
raise Exception(f"{fieldName} 不能小于 {options['min']}。")
|
||
|
||
if options and options.get("max") is not None and numberValue > options["max"]:
|
||
raise Exception(f"{fieldName} 不能大于 {options['max']}。")
|
||
|
||
return numberValue
|
||
|
||
|
||
def parseInteger(value, fieldName):
|
||
if value is None or value == "":
|
||
return None
|
||
|
||
numberValue = value if isinstance(value, (int, float)) and not isinstance(value, bool) else float(str(value))
|
||
if not _is_finite(numberValue) or not _is_integer(numberValue):
|
||
raise Exception(f"{fieldName} 必须是整数。")
|
||
|
||
return int(numberValue)
|
||
|
||
|
||
def parseBoolean(value, fieldName):
|
||
if value is None or value == "":
|
||
return None
|
||
|
||
if isinstance(value, bool):
|
||
return value
|
||
|
||
normalized = str(value).strip().lower()
|
||
if normalized in ("true", "1", "yes"):
|
||
return True
|
||
if normalized in ("false", "0", "no"):
|
||
return False
|
||
|
||
raise Exception(f"{fieldName} 必须是布尔值。")
|
||
|
||
|
||
def normalizeResponseFormat(value):
|
||
normalized = str(value or DEFAULT_RESPONSE_FORMAT).strip().lower()
|
||
if normalized != "url" and normalized != "base64":
|
||
raise Exception("response_format 仅支持 url 或 base64。")
|
||
return normalized
|
||
|
||
|
||
def normalizeAspectRatio(model, value):
|
||
normalized = str(value or "").strip()
|
||
if not normalized:
|
||
return None
|
||
|
||
if normalized not in SUPPORTED_ASPECT_RATIOS:
|
||
raise Exception(f"aspect_ratio 仅支持 {'、'.join(SUPPORTED_ASPECT_RATIOS)}。")
|
||
|
||
if normalized == "21:9" and str(model or "").strip() != "image-01":
|
||
raise Exception("aspect_ratio=21:9 仅支持 model=image-01。")
|
||
|
||
return normalized
|
||
|
||
|
||
def normalizeDimensions(model, widthValue, heightValue, aspectRatio):
|
||
if aspectRatio:
|
||
return {"width": None, "height": None}
|
||
|
||
width = parsePositiveInteger(widthValue, "width")
|
||
height = parsePositiveInteger(heightValue, "height")
|
||
|
||
if width is None and height is None:
|
||
return {"width": None, "height": None}
|
||
|
||
if width is None or height is None:
|
||
raise Exception("width 和 height 需要同时传入。")
|
||
|
||
if str(model or "").strip() != "image-01":
|
||
raise Exception("width 和 height 仅支持 model=image-01。")
|
||
|
||
if width < 512 or width > 2048 or width % 8 != 0:
|
||
raise Exception("width 需要在 512-2048 之间,且必须为 8 的倍数。")
|
||
|
||
if height < 512 or height > 2048 or height % 8 != 0:
|
||
raise Exception("height 需要在 512-2048 之间,且必须为 8 的倍数。")
|
||
|
||
return {"width": width, "height": height}
|
||
|
||
|
||
def parseStringList(value, fieldName):
|
||
if value is None or value == "":
|
||
return []
|
||
|
||
if isinstance(value, list):
|
||
return [str(item or "").strip() for item in value if str(item or "").strip()]
|
||
|
||
if isinstance(value, str):
|
||
trimmed = value.strip()
|
||
if not trimmed:
|
||
return []
|
||
|
||
try:
|
||
parsed = json.loads(trimmed)
|
||
if isinstance(parsed, list):
|
||
return [str(item or "").strip() for item in parsed if str(item or "").strip()]
|
||
except Exception:
|
||
# ignore and try comma-separated parsing
|
||
pass
|
||
|
||
return [item.strip() for item in trimmed.split(",") if item.strip()]
|
||
|
||
raise Exception(f"{fieldName} 必须是字符串数组、JSON 字符串或逗号分隔字符串。")
|
||
|
||
|
||
def isProbablyUrl(value):
|
||
return bool(re.match(r"^https?://", str(value or "").strip(), re.IGNORECASE))
|
||
|
||
|
||
def getBeeimgApiKey():
|
||
return str(getEnv("BEEIMG_API_KEY") or "").strip()
|
||
|
||
|
||
def guessMimeTypeFromPath(filePath):
|
||
lower = str(filePath or "").lower()
|
||
if lower.endswith(".png"):
|
||
return "image/png"
|
||
if lower.endswith(".jpg") or 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 def uploadImageToBeeimg(filePath):
|
||
exists = await Tools.Files.exists(filePath)
|
||
if not exists.get("exists"):
|
||
raise Exception(f"参考图文件不存在: {filePath}")
|
||
|
||
apiKey = getBeeimgApiKey()
|
||
if not apiKey:
|
||
raise Exception("使用 image_paths 需要配置 BEEIMG_API_KEY,用于先把本地参考图上传到公网图床。")
|
||
|
||
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 or response.statusCode >= 300:
|
||
raise Exception(f"BeeIMG 上传失败: HTTP {response.statusCode} - {response.content}")
|
||
|
||
try:
|
||
parsed = json.loads(response.content)
|
||
except Exception as error:
|
||
raise Exception(f"解析 BeeIMG 响应失败: {getErrorMessage(error)}")
|
||
|
||
files = parsed.get("files") if (isRecord(parsed) and isRecord(parsed.get("files"))) else None
|
||
fileUrl = str(files.get("url")).strip() if (files and files.get("url")) else ""
|
||
code = str(files.get("code")) if (files and files.get("code") is not None) else ""
|
||
status = str(files.get("status")) if (files and files.get("status")) else ""
|
||
|
||
if not fileUrl or (code != "200" and code != "" and status != "Success"):
|
||
raise Exception(f"BeeIMG 上传失败: {response.content}")
|
||
|
||
return fileUrl
|
||
|
||
|
||
async def resolveSubjectReference(imageUrls, imagePaths):
|
||
resolvedUrls = parseStringList(imageUrls, "image_urls")
|
||
resolvedPaths = parseStringList(imagePaths, "image_paths")
|
||
|
||
for url in resolvedUrls:
|
||
if not isProbablyUrl(url):
|
||
raise Exception(f"image_urls 中包含无效链接: {url}")
|
||
|
||
for filePath in resolvedPaths:
|
||
uploadedUrl = await uploadImageToBeeimg(filePath)
|
||
resolvedUrls.append(uploadedUrl)
|
||
|
||
if len(resolvedUrls) == 0:
|
||
return None
|
||
|
||
return [{"type": "character", "image_file": url} for url in resolvedUrls]
|
||
|
||
|
||
def normalizeBase64(base64):
|
||
raw = str(base64 or "").strip()
|
||
if not raw:
|
||
return raw
|
||
|
||
prefixIndex = raw.find("base64,")
|
||
if raw.startswith("data:") and prefixIndex >= 0:
|
||
return raw[prefixIndex + len("base64,"):].strip()
|
||
|
||
return raw
|
||
|
||
|
||
def guessExtensionFromUrl(url, fallbackExtension):
|
||
match = re.search(r"\.([a-z0-9]{2,5})(?:[?#].*)?$", str(url or ""), re.IGNORECASE)
|
||
if match and match.group(1):
|
||
return match.group(1).lower()
|
||
return fallbackExtension
|
||
|
||
|
||
def parseMiniMaxError(parsed, fallbackMessage):
|
||
baseResp = parsed.get("base_resp") if (isRecord(parsed) and isRecord(parsed.get("base_resp"))) else None
|
||
if not baseResp:
|
||
return
|
||
|
||
statusCode = float(baseResp.get("status_code")) if baseResp.get("status_code") is not None else 0
|
||
if _is_finite(statusCode) and statusCode != 0:
|
||
statusMsg = str(baseResp.get("status_msg")) if baseResp.get("status_msg") else fallbackMessage
|
||
raise Exception(f"MiniMax 图片接口返回错误: {statusMsg} (status_code={int(statusCode) if _is_integer(statusCode) else statusCode})")
|
||
|
||
|
||
async def callMiniMaxImageApi(params):
|
||
apiBaseUrl = getApiBaseUrl(params.get("api_base_url"))
|
||
endpoint = getImageEndpoint(apiBaseUrl)
|
||
effectiveModel = resolveModel(params.get("model"))
|
||
responseFormat = normalizeResponseFormat(params.get("response_format"))
|
||
aspectRatio = normalizeAspectRatio(effectiveModel, params.get("aspect_ratio"))
|
||
dimensions = normalizeDimensions(effectiveModel, params.get("width"), params.get("height"), aspectRatio)
|
||
imageCount = parsePositiveInteger(params.get("n"), "n", {"defaultValue": DEFAULT_IMAGE_COUNT, "min": 1, "max": 9})
|
||
promptOptimizer = parseBoolean(params.get("prompt_optimizer"), "prompt_optimizer")
|
||
aigcWatermark = parseBoolean(params.get("aigc_watermark"), "aigc_watermark")
|
||
seed = parseInteger(params.get("seed"), "seed")
|
||
subjectReference = await resolveSubjectReference(params.get("image_urls"), params.get("image_paths"))
|
||
|
||
body = {
|
||
"model": effectiveModel,
|
||
"prompt": str(params.get("prompt") or "").strip(),
|
||
"response_format": responseFormat,
|
||
"n": imageCount
|
||
}
|
||
|
||
if aspectRatio:
|
||
body["aspect_ratio"] = aspectRatio
|
||
|
||
if dimensions.get("width") is not None and dimensions.get("height") is not None:
|
||
body["width"] = dimensions["width"]
|
||
body["height"] = dimensions["height"]
|
||
|
||
if promptOptimizer is not None:
|
||
body["prompt_optimizer"] = promptOptimizer
|
||
|
||
if aigcWatermark is not None:
|
||
body["aigc_watermark"] = aigcWatermark
|
||
|
||
if seed is not None:
|
||
body["seed"] = seed
|
||
|
||
if subjectReference:
|
||
body["subject_reference"] = subjectReference
|
||
|
||
request = client.newRequest().url(endpoint).method("POST").headers({
|
||
"accept": "application/json",
|
||
"content-type": "application/json",
|
||
"Authorization": f"Bearer {getApiKey()}"
|
||
}).body(json.dumps(body), "json")
|
||
|
||
response = await request.build().execute()
|
||
if not response.isSuccessful():
|
||
raise Exception(f"MiniMax 图片 API 调用失败: {response.statusCode} - {response.content}")
|
||
|
||
try:
|
||
parsed = json.loads(response.content)
|
||
except Exception as error:
|
||
raise Exception(f"解析 MiniMax 响应失败: {getErrorMessage(error)}")
|
||
|
||
parseMiniMaxError(parsed, "MiniMax 接口返回错误")
|
||
|
||
data = parsed.get("data") if (isRecord(parsed) and isRecord(parsed.get("data"))) else None
|
||
if not data:
|
||
raise Exception("MiniMax 响应中未找到 data 对象。")
|
||
|
||
metadata = data.get("metadata") if isRecord(data.get("metadata")) else None
|
||
successCount = float(metadata.get("success_count")) if (metadata and metadata.get("success_count") is not None) else None
|
||
failedCount = float(metadata.get("failed_count")) if (metadata and metadata.get("failed_count") is not None) else None
|
||
requestId = str(parsed.get("id")) if (isRecord(parsed) and parsed.get("id")) else ""
|
||
|
||
if responseFormat == "base64":
|
||
imageBase64 = [str(item or "").strip() for item in data.get("image_base64")] if isinstance(data.get("image_base64"), list) else []
|
||
imageBase64 = [item for item in imageBase64 if len(item) > 0]
|
||
|
||
if len(imageBase64) == 0:
|
||
raise Exception("MiniMax 响应中未找到 data.image_base64。")
|
||
|
||
return {
|
||
"request_id": requestId,
|
||
"effective_model": effectiveModel,
|
||
"response_format": responseFormat,
|
||
"image_base64": imageBase64,
|
||
"image_urls": [],
|
||
"success_count": int(successCount) if _is_finite(successCount) else len(imageBase64),
|
||
"failed_count": int(failedCount) if _is_finite(failedCount) else 0,
|
||
"reference_count": len(subjectReference) if subjectReference else 0
|
||
}
|
||
|
||
imageUrls = [str(item or "").strip() for item in data.get("image_urls")] if isinstance(data.get("image_urls"), list) else []
|
||
imageUrls = [item for item in imageUrls if len(item) > 0]
|
||
|
||
if len(imageUrls) == 0:
|
||
raise Exception("MiniMax 响应中未找到 data.image_urls。")
|
||
|
||
return {
|
||
"request_id": requestId,
|
||
"effective_model": effectiveModel,
|
||
"response_format": responseFormat,
|
||
"image_base64": [],
|
||
"image_urls": imageUrls,
|
||
"success_count": int(successCount) if _is_finite(successCount) else len(imageUrls),
|
||
"failed_count": int(failedCount) if _is_finite(failedCount) else 0,
|
||
"reference_count": len(subjectReference) if subjectReference else 0
|
||
}
|
||
|
||
|
||
async def draw_image(params):
|
||
prompt = str(params.get("prompt") if params and params.get("prompt") else "").strip()
|
||
if not prompt:
|
||
raise Exception("参数 prompt 不能为空。")
|
||
|
||
await ensureDirectories()
|
||
|
||
apiResult = await callMiniMaxImageApi({
|
||
"prompt": prompt,
|
||
"model": params.get("model"),
|
||
"aspect_ratio": params.get("aspect_ratio"),
|
||
"width": params.get("width"),
|
||
"height": params.get("height"),
|
||
"n": params.get("n"),
|
||
"response_format": params.get("response_format"),
|
||
"prompt_optimizer": params.get("prompt_optimizer"),
|
||
"aigc_watermark": params.get("aigc_watermark"),
|
||
"seed": params.get("seed"),
|
||
"image_urls": params.get("image_urls"),
|
||
"image_paths": params.get("image_paths"),
|
||
"api_base_url": params.get("api_base_url")
|
||
})
|
||
|
||
baseName = buildFileBaseName(prompt, params.get("file_name"), "minimax_draw")
|
||
files = []
|
||
|
||
if apiResult["response_format"] == "base64":
|
||
for index in range(len(apiResult["image_base64"])):
|
||
suffix = f"_{index + 1}" if len(apiResult["image_base64"]) > 1 else ""
|
||
filePath = f"{DRAWS_DIR}/{baseName}{suffix}.{DEFAULT_BASE64_EXTENSION}"
|
||
writeResult = await Tools.Files.writeBinary(filePath, normalizeBase64(apiResult["image_base64"][index]))
|
||
if not writeResult.get("successful"):
|
||
raise Exception(f"保存图片失败: {writeResult.get('details')}")
|
||
fileUri = f"file://{filePath}"
|
||
files.append({
|
||
"file_path": filePath,
|
||
"file_uri": fileUri,
|
||
"markdown": f"![MiniMax 生成的图片{f' {index + 1}' if len(apiResult['image_base64']) > 1 else ''}]({fileUri})",
|
||
"remote_image_url": None
|
||
})
|
||
else:
|
||
for index in range(len(apiResult["image_urls"])):
|
||
imageUrl = apiResult["image_urls"][index]
|
||
extension = guessExtensionFromUrl(imageUrl, "png")
|
||
suffix = f"_{index + 1}" if len(apiResult["image_urls"]) > 1 else ""
|
||
filePath = f"{DRAWS_DIR}/{baseName}{suffix}.{extension}"
|
||
downloadResult = await Tools.Files.download(imageUrl, filePath)
|
||
if not downloadResult.get("successful"):
|
||
raise Exception(f"下载图片失败: {downloadResult.get('details')}")
|
||
fileUri = f"file://{filePath}"
|
||
files.append({
|
||
"file_path": filePath,
|
||
"file_uri": fileUri,
|
||
"markdown": f"![MiniMax 生成的图片{f' {index + 1}' if len(apiResult['image_urls']) > 1 else ''}]({fileUri})",
|
||
"remote_image_url": imageUrl
|
||
})
|
||
|
||
hintLines = []
|
||
hintLines.append(f"图片已生成并保存在本地 {DRAWS_DIR}。")
|
||
hintLines.append(f"共生成 {len(files)} 张。")
|
||
if apiResult["reference_count"] > 0:
|
||
hintLines.append(f"本次使用了 {apiResult['reference_count']} 张参考图。")
|
||
hintLines.append("")
|
||
hintLines.append("后续回答如果需要展示图片,请直接输出下面这些 Markdown:")
|
||
hintLines.append("")
|
||
for fileItem in files:
|
||
hintLines.append(fileItem["markdown"])
|
||
|
||
return {
|
||
"prompt": prompt,
|
||
"model": apiResult["effective_model"],
|
||
"request_id": apiResult["request_id"] or None,
|
||
"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": files,
|
||
"hint": "\n".join(hintLines)
|
||
}
|
||
|
||
|
||
async def draw_image_wrapper(params=None):
|
||
if params is None:
|
||
params = {}
|
||
try:
|
||
result = await draw_image(params)
|
||
complete({
|
||
"success": True,
|
||
"message": "MiniMax 图片生成成功,已保存到本地。",
|
||
"data": result
|
||
})
|
||
except Exception as error:
|
||
console.error("draw_image 执行失败:", error)
|
||
complete({
|
||
"success": False,
|
||
"message": f"MiniMax 图片生成失败: {getErrorMessage(error)}",
|
||
"error_stack": traceback.format_exc()
|
||
})
|
||
|
||
|
||
exports.draw_image = draw_image_wrapper
|