From 085e05634616cdb8db048575e1972c939e58a01d Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Thu, 13 Aug 2026 04:50:59 +0800 Subject: [PATCH] feat: add MiniMax image generation tool --- .../bisheng/database/data/t_gpts_tools.json | 80 ++++++++++++++++++- .../database/data/t_gpts_tools_type.json | 16 ++++ src/backend/bisheng/utils/mask_data.py | 2 + .../gpts/tools/api_tools/__init__.py | 5 ++ .../gpts/tools/api_tools/minimax_image.py | 79 ++++++++++++++++++ .../tools/api_tools/minimax_image_core.py | 54 +++++++++++++ src/backend/test/tool/test_minimax_image.py | 76 ++++++++++++++++++ .../platform/public/locales/en-US/tool.json | 4 +- .../platform/public/locales/ja/tool.json | 4 +- .../platform/public/locales/zh-Hans/tool.json | 4 +- .../src/pages/BuildPage/tools/ToolSet.tsx | 3 + .../tools/builtInTool/MiniMaxConfig.tsx | 42 ++++++++++ 12 files changed, 365 insertions(+), 4 deletions(-) create mode 100644 src/backend/bisheng_langchain/gpts/tools/api_tools/minimax_image.py create mode 100644 src/backend/bisheng_langchain/gpts/tools/api_tools/minimax_image_core.py create mode 100644 src/backend/test/tool/test_minimax_image.py create mode 100644 src/frontend/platform/src/pages/BuildPage/tools/builtInTool/MiniMaxConfig.tsx diff --git a/src/backend/bisheng/database/data/t_gpts_tools.json b/src/backend/bisheng/database/data/t_gpts_tools.json index 3d0934ea31..4cbdd0df25 100644 --- a/src/backend/bisheng/database/data/t_gpts_tools.json +++ b/src/backend/bisheng/database/data/t_gpts_tools.json @@ -1,4 +1,82 @@ [ + { + "name": "MiniMax Image Generation", + "logo": null, + "desc": "Generate images from a text prompt with MiniMax", + "api_params": [ + { + "in": "query", + "name": "prompt", + "schema": {"type": "string"}, + "required": true, + "description": "Text description of the image" + }, + { + "in": "query", + "name": "model", + "schema": {"type": "string", "enum": ["image-01", "image-01-live"], "default": "image-01"}, + "required": false, + "description": "MiniMax image model" + }, + { + "in": "query", + "name": "aspect_ratio", + "schema": {"type": "string"}, + "required": false, + "description": "Generated image aspect ratio" + }, + { + "in": "query", + "name": "width", + "schema": {"type": "integer", "minimum": 512, "maximum": 2048, "multipleOf": 8}, + "required": false, + "description": "Generated image width" + }, + { + "in": "query", + "name": "height", + "schema": {"type": "integer", "minimum": 512, "maximum": 2048, "multipleOf": 8}, + "required": false, + "description": "Generated image height" + }, + { + "in": "query", + "name": "response_format", + "schema": {"type": "string", "enum": ["url", "base64"], "default": "url"}, + "required": false, + "description": "Image response format" + }, + { + "in": "query", + "name": "seed", + "schema": {"type": "integer"}, + "required": false, + "description": "Seed for deterministic generation" + }, + { + "in": "query", + "name": "n", + "schema": {"type": "integer", "minimum": 1, "maximum": 9, "default": 1}, + "required": false, + "description": "Number of images to generate" + }, + { + "in": "query", + "name": "prompt_optimizer", + "schema": {"type": "boolean", "default": false}, + "required": false, + "description": "Enable prompt optimization" + } + ], + "tool_key": "minimax_image_generation", + "type": 17, + "is_preset": 1, + "is_delete": 0, + "user_id": 1, + "create_time": "2026-08-12 00:00:00", + "update_time": "2026-08-12 00:00:00", + "id": 39 + }, { "name": "时间", "logo": null, @@ -1115,4 +1193,4 @@ "user_id": 1, "id": 38 } -] \ No newline at end of file +] diff --git a/src/backend/bisheng/database/data/t_gpts_tools_type.json b/src/backend/bisheng/database/data/t_gpts_tools_type.json index 692c3c56f5..40af921c13 100644 --- a/src/backend/bisheng/database/data/t_gpts_tools_type.json +++ b/src/backend/bisheng/database/data/t_gpts_tools_type.json @@ -1,4 +1,20 @@ [ + { + "id": 17, + "name": "MiniMax", + "logo": "", + "description": "Generate images from text prompts with MiniMax image models", + "server_host": "", + "auth_method": 0, + "api_key": "", + "auth_type": "basic", + "is_preset": 1, + "user_id": null, + "is_delete": 0, + "create_time": "2026-08-12 00:00:00", + "update_time": "2026-08-12 00:00:00", + "openapi_schema": null + }, { "id": 1, "name": "时间", diff --git a/src/backend/bisheng/utils/mask_data.py b/src/backend/bisheng/utils/mask_data.py index 8a1dd4b705..174b9a06d2 100644 --- a/src/backend/bisheng/utils/mask_data.py +++ b/src/backend/bisheng/utils/mask_data.py @@ -23,6 +23,7 @@ def __init__(self): 'api_secret': self.mask_api_key, 'openai_api_key': self.mask_api_key, 'siliconflow_api_key': self.mask_api_key, + 'minimax_api_key': self.mask_api_key, 'email_password': self.mask_api_key, 'jina_api_key': self.mask_api_key, 'app_secret': self.mask_api_key, @@ -42,6 +43,7 @@ def __init__(self): 'api_secret': r'^\*+$', 'openai_api_key': r'^\*+$', 'siliconflow_api_key': r'^\*+$', + 'minimax_api_key': r'^\*+$', 'email_password': r'^\*+$', 'jina_api_key': r'^\*+$', 'app_secret': r'^\*+$', diff --git a/src/backend/bisheng_langchain/gpts/tools/api_tools/__init__.py b/src/backend/bisheng_langchain/gpts/tools/api_tools/__init__.py index ae7d12ee6b..af3ca37766 100644 --- a/src/backend/bisheng_langchain/gpts/tools/api_tools/__init__.py +++ b/src/backend/bisheng_langchain/gpts/tools/api_tools/__init__.py @@ -3,6 +3,7 @@ # from .eastmoney import from bisheng_langchain.gpts.tools.api_tools.firecrawl import FireCrawl from bisheng_langchain.gpts.tools.api_tools.jina import JinaTool +from bisheng_langchain.gpts.tools.api_tools.minimax_image import MiniMaxImage from bisheng_langchain.gpts.tools.api_tools.silicon_flow import SiliconFlow from bisheng_langchain.gpts.tools.message.dingding import DingdingMessageTool from bisheng_langchain.gpts.tools.message.email import EmailMessageTool @@ -74,6 +75,9 @@ 'silicon_stable_diffusion': (SiliconFlow.get_api_tool, ['siliconflow_api_key']), 'silicon_flux': (SiliconFlow.get_api_tool, ['siliconflow_api_key']) } +_MINIMAX_IMAGE_TOOLS: Dict[str, Tuple[Callable[[KwArg(Any)], BaseTool], List[str]]] = { + "minimax_image_generation": (MiniMaxImage.get_api_tool, ["minimax_api_key", "minimax_base_url"]) +} dingding_class_methods = [ method for method in DingdingMessageTool.__dict__ if isinstance(DingdingMessageTool.__dict__[method], classmethod) @@ -120,6 +124,7 @@ ALL_API_TOOLS.update(_FIRE_TOOLS) ALL_API_TOOLS.update(_JINA_TOOLS) ALL_API_TOOLS.update(_SILICON_TOOLS) +ALL_API_TOOLS.update(_MINIMAX_IMAGE_TOOLS) ALL_API_TOOLS.update(_DING_TOOLS) ALL_API_TOOLS.update(_EMAIL_TOOLS) ALL_API_TOOLS.update(_FEISHU_TOOLS) diff --git a/src/backend/bisheng_langchain/gpts/tools/api_tools/minimax_image.py b/src/backend/bisheng_langchain/gpts/tools/api_tools/minimax_image.py new file mode 100644 index 0000000000..bca71849c6 --- /dev/null +++ b/src/backend/bisheng_langchain/gpts/tools/api_tools/minimax_image.py @@ -0,0 +1,79 @@ +import json +from typing import Any, Literal + +import requests +from langchain_core.tools import BaseTool +from pydantic import BaseModel, Field, model_validator + +from bisheng_langchain.gpts.tools.api_tools.base import APIToolBase, MultArgsSchemaTool +from bisheng_langchain.gpts.tools.api_tools.minimax_image_core import ( + build_image_generation_payload, + parse_image_generation_response, +) + + +class MiniMaxImageInput(BaseModel): + prompt: str = Field(description="Text description of the image", min_length=1, max_length=1500) + model: Literal["image-01", "image-01-live"] = "image-01" + aspect_ratio: Literal["1:1", "16:9", "4:3", "3:2", "2:3", "3:4", "9:16", "21:9"] | None = None + width: int | None = Field(default=None, ge=512, le=2048, multiple_of=8) + height: int | None = Field(default=None, ge=512, le=2048, multiple_of=8) + response_format: Literal["url", "base64"] = "url" + seed: int | None = None + n: int = Field(default=1, ge=1, le=9) + prompt_optimizer: bool = False + + @model_validator(mode="after") + def validate_dimensions(self) -> "MiniMaxImageInput": + if (self.width is None) != (self.height is None): + raise ValueError("width and height must be provided together") + return self + + +class MiniMaxImage(APIToolBase): + minimax_api_key: str = Field(description="MiniMax API key") + minimax_base_url: str = Field(default="https://api.minimax.io", description="MiniMax API base URL") + + def generate( + self, + prompt: str, + model: str = "image-01", + aspect_ratio: str | None = None, + width: int | None = None, + height: int | None = None, + response_format: str = "url", + seed: int | None = None, + n: int = 1, + prompt_optimizer: bool = False, + ) -> str: + """Generate images from a text prompt with MiniMax.""" + payload = build_image_generation_payload( + prompt=prompt, + model=model, + aspect_ratio=aspect_ratio, + width=width, + height=height, + response_format=response_format, + seed=seed, + n=n, + prompt_optimizer=prompt_optimizer, + ) + response = requests.post( + f"{self.minimax_base_url.rstrip('/')}/v1/image_generation", + json=payload, + headers={"Authorization": f"Bearer {self.minimax_api_key}", "Content-Type": "application/json"}, + timeout=self.request_timeout, + ) + response.raise_for_status() + result = parse_image_generation_response(response.json(), response_format) + return json.dumps(result, ensure_ascii=False) + + @classmethod + def get_api_tool(cls, name: str, **kwargs: Any) -> BaseTool: + instance = cls(**kwargs) + return MultArgsSchemaTool( + name=name, + description=instance.generate.__doc__, + func=instance.generate, + args_schema=MiniMaxImageInput, + ) diff --git a/src/backend/bisheng_langchain/gpts/tools/api_tools/minimax_image_core.py b/src/backend/bisheng_langchain/gpts/tools/api_tools/minimax_image_core.py new file mode 100644 index 0000000000..78a6a97e50 --- /dev/null +++ b/src/backend/bisheng_langchain/gpts/tools/api_tools/minimax_image_core.py @@ -0,0 +1,54 @@ +from typing import Any + + +def build_image_generation_payload( + *, + prompt: str, + model: str, + aspect_ratio: str | None, + width: int | None, + height: int | None, + response_format: str, + seed: int | None, + n: int, + prompt_optimizer: bool, +) -> dict[str, Any]: + payload: dict[str, Any] = { + "model": model, + "prompt": prompt, + "response_format": response_format, + "n": n, + "prompt_optimizer": prompt_optimizer, + } + optional_fields = { + "aspect_ratio": aspect_ratio, + "width": width, + "height": height, + "seed": seed, + } + payload.update({key: value for key, value in optional_fields.items() if value is not None}) + return payload + + +def parse_image_generation_response(payload: dict[str, Any], response_format: str) -> dict[str, Any]: + base_response = payload.get("base_resp") + if not isinstance(base_response, dict) or base_response.get("status_code") != 0: + message = base_response.get("status_msg") if isinstance(base_response, dict) else None + raise ValueError(message or "MiniMax image generation failed") + + data = payload.get("data") + if not isinstance(data, dict): + raise ValueError("MiniMax image generation returned no image data") + response_key = "image_urls" if response_format == "url" else "image_base64" + images = data.get(response_key) + if not isinstance(images, list) or not images or not all(isinstance(image, str) for image in images): + raise ValueError("MiniMax image generation returned invalid image data") + + metadata = payload.get("metadata") + metadata = metadata if isinstance(metadata, dict) else {} + return { + "response_format": response_format, + "images": images, + "success_count": metadata.get("success_count", len(images)), + "failed_count": metadata.get("failed_count", 0), + } diff --git a/src/backend/test/tool/test_minimax_image.py b/src/backend/test/tool/test_minimax_image.py new file mode 100644 index 0000000000..405348377a --- /dev/null +++ b/src/backend/test/tool/test_minimax_image.py @@ -0,0 +1,76 @@ +import importlib.util +import unittest +from pathlib import Path + +MODULE_PATH = Path(__file__).parents[2] / "bisheng_langchain" / "gpts" / "tools" / "api_tools" / "minimax_image_core.py" +SPEC = importlib.util.spec_from_file_location("minimax_image_core", MODULE_PATH) +assert SPEC and SPEC.loader +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) +build_image_generation_payload = MODULE.build_image_generation_payload +parse_image_generation_response = MODULE.parse_image_generation_response + + +class TestMiniMaxImagePayload(unittest.TestCase): + def test_builds_supported_request_fields(self): + payload = build_image_generation_payload( + prompt="A small robot reading a book", + model="image-01-live", + aspect_ratio=None, + width=1024, + height=768, + response_format="base64", + seed=7, + n=2, + prompt_optimizer=True, + ) + + self.assertEqual( + payload, + { + "model": "image-01-live", + "prompt": "A small robot reading a book", + "width": 1024, + "height": 768, + "response_format": "base64", + "seed": 7, + "n": 2, + "prompt_optimizer": True, + }, + ) + + def test_parses_url_response_and_metadata(self): + result = parse_image_generation_response( + { + "data": {"image_urls": ["https://example.test/image.png"]}, + "metadata": {"success_count": 1, "failed_count": 0}, + "base_resp": {"status_code": 0, "status_msg": "success"}, + }, + "url", + ) + + self.assertEqual(result["images"], ["https://example.test/image.png"]) + self.assertEqual(result["success_count"], 1) + self.assertEqual(result["failed_count"], 0) + + def test_parses_base64_response(self): + result = parse_image_generation_response( + { + "data": {"image_base64": ["aW1hZ2U="]}, + "base_resp": {"status_code": 0, "status_msg": "success"}, + }, + "base64", + ) + + self.assertEqual(result["images"], ["aW1hZ2U="]) + + def test_rejects_provider_error(self): + with self.assertRaisesRegex(ValueError, "invalid request"): + parse_image_generation_response( + {"base_resp": {"status_code": 2013, "status_msg": "invalid request"}}, + "url", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/frontend/platform/public/locales/en-US/tool.json b/src/frontend/platform/public/locales/en-US/tool.json index 711023e01f..261b9b99a8 100644 --- a/src/frontend/platform/public/locales/en-US/tool.json +++ b/src/frontend/platform/public/locales/en-US/tool.json @@ -387,6 +387,8 @@ "fieldCannotBeEmpty": "{{label}} cannot be empty", "jinaApiKeyLabel": "Jina API Key", "siliconflowApiKeyLabel": "SiliconFlow API Key", + "minimaxApiKeyLabel": "MiniMax API Key", + "minimaxApiBaseUrlLabel": "MiniMax API Base URL", "uploadFile": "Upload File", "organizeKnowledgeBase": "Organize Knowledge Base", "个人知识库": "Personal Knowledge Base", @@ -406,4 +408,4 @@ "写入文件内容desc": "Append text to a file or create one if it does not exist.", "替换文件指定行范围内容": "Replace File Line Range", "替换文件指定行范围内容desc": "Replace a specified range of lines in a file." -} \ No newline at end of file +} diff --git a/src/frontend/platform/public/locales/ja/tool.json b/src/frontend/platform/public/locales/ja/tool.json index 43208a1cb7..2df07fb733 100644 --- a/src/frontend/platform/public/locales/ja/tool.json +++ b/src/frontend/platform/public/locales/ja/tool.json @@ -387,6 +387,8 @@ "fieldCannotBeEmpty": "{{label}}は空にできません", "jinaApiKeyLabel": "Jina API Key", "siliconflowApiKeyLabel": "SiliconFlow API Key", + "minimaxApiKeyLabel": "MiniMax API Key", + "minimaxApiBaseUrlLabel": "MiniMax API Base URL", "uploadFile": "ファイルをアップロード", "organizeKnowledgeBase": "ナレッジベース整理", "个人知识库": "個人ナレッジベース", @@ -406,4 +408,4 @@ "写入文件内容desc": "テキストを追記し、存在しない場合は新規作成します。", "替换文件指定行范围内容": "行範囲を置換", "替换文件指定行范围内容desc": "ファイル内の指定行範囲を置き換えます。" -} \ No newline at end of file +} diff --git a/src/frontend/platform/public/locales/zh-Hans/tool.json b/src/frontend/platform/public/locales/zh-Hans/tool.json index 4766671610..1dd04cc429 100644 --- a/src/frontend/platform/public/locales/zh-Hans/tool.json +++ b/src/frontend/platform/public/locales/zh-Hans/tool.json @@ -387,6 +387,8 @@ "fieldCannotBeEmpty": "{{label}} 不能为空", "jinaApiKeyLabel": "Jina API 密钥", "siliconflowApiKeyLabel": "SiliconFlow API 密钥", + "minimaxApiKeyLabel": "MiniMax API Key", + "minimaxApiBaseUrlLabel": "MiniMax API Base URL", "uploadFile": "上传文件", "organizeKnowledgeBase": "组织知识库", "个人知识库": "个人知识库", @@ -406,4 +408,4 @@ "写入文件内容desc": "将文本内容追加到文本文件,如果文件不存在,则创建文件", "替换文件指定行范围内容": "替换文件指定行范围内容", "替换文件指定行范围内容desc": "替换文件中的指定行范围。" -} \ No newline at end of file +} diff --git a/src/frontend/platform/src/pages/BuildPage/tools/ToolSet.tsx b/src/frontend/platform/src/pages/BuildPage/tools/ToolSet.tsx index 5a018ab0c1..7247035719 100644 --- a/src/frontend/platform/src/pages/BuildPage/tools/ToolSet.tsx +++ b/src/frontend/platform/src/pages/BuildPage/tools/ToolSet.tsx @@ -10,6 +10,7 @@ import Dalle3ToolForm from "./builtInTool/Dalle3"; import EmailConfigForm from "./builtInTool/EmailConfig"; import FeishuConfigForm from "./builtInTool/FeishuConfig"; import JinaApiKeyForm from "./builtInTool/JinaConfig"; +import { MiniMaxConfig } from "./builtInTool/MiniMaxConfig"; import SiliconFlowApiKeyForm from "./builtInTool/SiliconFlowApiKey"; import TianyanchaToolForm from "./builtInTool/Tianyancha"; import WebSearchForm from "./builtInTool/WebSearchFrom"; @@ -102,6 +103,8 @@ const ToolSet = forwardRef(function ToolSet({ onChange }, ref) { return ; case 'SiliconFlow': return ; + case 'MiniMax': + return ; case '发送邮件': return ; case '飞书消息': diff --git a/src/frontend/platform/src/pages/BuildPage/tools/builtInTool/MiniMaxConfig.tsx b/src/frontend/platform/src/pages/BuildPage/tools/builtInTool/MiniMaxConfig.tsx new file mode 100644 index 0000000000..af77025889 --- /dev/null +++ b/src/frontend/platform/src/pages/BuildPage/tools/builtInTool/MiniMaxConfig.tsx @@ -0,0 +1,42 @@ +import { Button } from '@/components/bs-ui/button'; +import { DialogClose, DialogFooter } from '@/components/bs-ui/dialog'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { InputField } from './InputField'; + +const defaultValues = { + minimax_api_key: '', + minimax_base_url: 'https://api.minimax.io', +}; + +export function MiniMaxConfig({ formData = {}, onSubmit }) { + const { t } = useTranslation('tool'); + const [localFormData, setLocalFormData] = useState(() => ({ ...defaultValues, ...formData })); + const [errors, setErrors] = useState>({}); + + const handleChange = (event) => { + const { name, value } = event.target; + setLocalFormData((previous) => ({ ...previous, [name]: value })); + }; + + const handleSubmit = (event) => { + event.preventDefault(); + const nextErrors = { + minimax_api_key: !localFormData.minimax_api_key, + minimax_base_url: !localFormData.minimax_base_url, + }; + setErrors(nextErrors); + if (!Object.values(nextErrors).some(Boolean)) onSubmit(localFormData); + }; + + return ( +
+ + + + + + + + ); +}