diff --git a/mateclaw-server/src/main/java/vip/mate/system/model/SystemSettingsDTO.java b/mateclaw-server/src/main/java/vip/mate/system/model/SystemSettingsDTO.java index 9d0471a8..75852890 100644 --- a/mateclaw-server/src/main/java/vip/mate/system/model/SystemSettingsDTO.java +++ b/mateclaw-server/src/main/java/vip/mate/system/model/SystemSettingsDTO.java @@ -70,6 +70,16 @@ public class SystemSettingsDTO { @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) private String minimaxApiKey; private String minimaxApiKeyMasked; + /** + * MiniMax API region — selects which host to call. Shared by image + video + * providers because the API key is the same across both: + * + */ + private String minimaxRegion; // ===== 图片生成配置 ===== /** 是否启用图片生成能力 */ diff --git a/mateclaw-server/src/main/java/vip/mate/tool/image/provider/MiniMaxImageProvider.java b/mateclaw-server/src/main/java/vip/mate/tool/image/provider/MiniMaxImageProvider.java index e3f61c96..371dc195 100644 --- a/mateclaw-server/src/main/java/vip/mate/tool/image/provider/MiniMaxImageProvider.java +++ b/mateclaw-server/src/main/java/vip/mate/tool/image/provider/MiniMaxImageProvider.java @@ -20,9 +20,9 @@ import java.util.Set; * MiniMax 图片生成 Provider — image-01 模型 *

* 同步模式:返回 Base64 图片。 - * 复用视频生成中的 MiniMax API Key。 + * 复用视频生成中的 MiniMax API Key + region 设置({@code minimaxRegion})。 *

- * API: POST https://api.minimax.io/v1/image_generation + * API: POST {@code /v1/image_generation} —— host 由 region 决定。 * * @author MateClaw Team */ @@ -33,9 +33,30 @@ public class MiniMaxImageProvider implements ImageGenerationProvider { private final ObjectMapper objectMapper; - private static final String BASE_URL = "https://api.minimax.io"; + /** Global endpoint. */ + static final String BASE_URL_GLOBAL = "https://api.minimax.io"; + + /** China endpoint (mainland-CN low-latency host; same JSON shape). */ + static final String BASE_URL_CN = "https://api.minimaxi.com"; + + /** Region value selecting the CN endpoint. Anything else → global. */ + static final String REGION_CN = "cn"; + private static final String DEFAULT_MODEL = "image-01"; + /** + * Resolve MiniMax base URL from system settings region. Shared semantics + * with {@code MiniMaxVideoProvider.resolveBaseUrl} (single field controls + * both image + video routing because the API key is the same). + * Package-private for unit tests. + */ + static String resolveBaseUrl(SystemSettingsDTO config) { + if (config != null && REGION_CN.equalsIgnoreCase(config.getMinimaxRegion())) { + return BASE_URL_CN; + } + return BASE_URL_GLOBAL; + } + @Override public String id() { return "minimax"; @@ -96,7 +117,8 @@ public class MiniMaxImageProvider implements ImageGenerationProvider { body.put("aspect_ratio", request.getAspectRatio()); } - HttpResponse response = HttpRequest.post(BASE_URL + "/v1/image_generation") + String baseUrl = resolveBaseUrl(config); + HttpResponse response = HttpRequest.post(baseUrl + "/v1/image_generation") .header("Authorization", "Bearer " + apiKey) .header("Content-Type", "application/json") .body(body.toString()) diff --git a/mateclaw-server/src/main/java/vip/mate/tool/video/provider/MiniMaxVideoProvider.java b/mateclaw-server/src/main/java/vip/mate/tool/video/provider/MiniMaxVideoProvider.java index e05195f7..2d57e7ff 100644 --- a/mateclaw-server/src/main/java/vip/mate/tool/video/provider/MiniMaxVideoProvider.java +++ b/mateclaw-server/src/main/java/vip/mate/tool/video/provider/MiniMaxVideoProvider.java @@ -21,6 +21,13 @@ import java.util.Set; *

* API 文档: https://platform.minimaxi.com/document/video-generation * 鉴权: Bearer Token + *

+ * Region 切换:根据 {@link SystemSettingsDTO#getMinimaxRegion()} 选 host: + *

* * @author MateClaw Team */ @@ -31,9 +38,44 @@ public class MiniMaxVideoProvider implements VideoGenerationProvider { private final ObjectMapper objectMapper; - private static final String BASE_URL = "https://api.minimax.io"; + /** Global endpoint (used by accounts on api.minimax.io). */ + static final String BASE_URL_GLOBAL = "https://api.minimax.io"; + + /** China endpoint (api.minimaxi.com — same JSON shape, different host). */ + static final String BASE_URL_CN = "https://api.minimaxi.com"; + + /** Region value selecting the CN endpoint. Anything else → global. */ + static final String REGION_CN = "cn"; + private static final String DEFAULT_MODEL = "MiniMax-Hailuo-2.3"; + /** + * Full MiniMax video model catalog (matches openclaw + * {@code extensions/minimax/provider-models.ts}). Includes both T2V + * (Hailuo family) and I2V (I2V-01-* family) entries. + */ + private static final List MODEL_CATALOG = List.of( + // T2V (text-to-video) + "MiniMax-Hailuo-2.3", + "MiniMax-Hailuo-2.3-Fast", + "MiniMax-Hailuo-02", + // I2V (image-to-video) + "I2V-01-Director", + "I2V-01-live", + "I2V-01" + ); + + /** + * Resolve the MiniMax base URL from the system settings region. Package-private + * for unit tests — the only branching point that needs verification. + */ + static String resolveBaseUrl(SystemSettingsDTO config) { + if (config != null && REGION_CN.equalsIgnoreCase(config.getMinimaxRegion())) { + return BASE_URL_CN; + } + return BASE_URL_GLOBAL; + } + @Override public String id() { return "minimax"; @@ -67,7 +109,7 @@ public class MiniMaxVideoProvider implements VideoGenerationProvider { .supportedDurations(List.of(6, 10)) .maxDurationSeconds(10) .defaultModel(DEFAULT_MODEL) - .models(List.of("MiniMax-Hailuo-2.3", "MiniMax-Hailuo-2.3-Fast", "I2V-01-live")) + .models(MODEL_CATALOG) .build(); } @@ -80,6 +122,7 @@ public class MiniMaxVideoProvider implements VideoGenerationProvider { public VideoSubmitResult submit(VideoGenerationRequest request, SystemSettingsDTO config) { try { String apiKey = config.getMinimaxApiKey(); + String baseUrl = resolveBaseUrl(config); String model = request.getModel() != null ? request.getModel() : DEFAULT_MODEL; ObjectNode body = objectMapper.createObjectNode(); @@ -93,7 +136,7 @@ public class MiniMaxVideoProvider implements VideoGenerationProvider { body.put("duration", request.getDurationSeconds()); } - HttpResponse response = HttpRequest.post(BASE_URL + "/v1/video_generation") + HttpResponse response = HttpRequest.post(baseUrl + "/v1/video_generation") .header("Authorization", "Bearer " + apiKey) .header("Content-Type", "application/json") .body(body.toString()) @@ -106,11 +149,11 @@ public class MiniMaxVideoProvider implements VideoGenerationProvider { int statusCode = result.path("base_resp").path("status_code").asInt(-1); if (statusCode == 0 && result.has("task_id")) { String taskId = result.get("task_id").asText(); - log.info("[MiniMax] Submitted task: {} (model={})", taskId, model); + log.info("[MiniMax] Submitted task: {} (model={}, host={})", taskId, model, baseUrl); return VideoSubmitResult.success(taskId, id()); } else { String errMsg = result.path("base_resp").path("status_msg").asText("未知错误"); - log.warn("[MiniMax] Submit failed: {}", errMsg); + log.warn("[MiniMax] Submit failed (host={}): {}", baseUrl, errMsg); return VideoSubmitResult.failure(id(), errMsg); } } catch (Exception e) { @@ -123,9 +166,10 @@ public class MiniMaxVideoProvider implements VideoGenerationProvider { public TaskPollResult checkStatus(String providerTaskId, SystemSettingsDTO config) { try { String apiKey = config.getMinimaxApiKey(); + String baseUrl = resolveBaseUrl(config); HttpResponse response = HttpRequest.get( - BASE_URL + "/v1/query/video_generation?task_id=" + providerTaskId) + baseUrl + "/v1/query/video_generation?task_id=" + providerTaskId) .header("Authorization", "Bearer " + apiKey) .timeout(15_000) .execute(); @@ -138,7 +182,7 @@ public class MiniMaxVideoProvider implements VideoGenerationProvider { // 优先取 video_url,备选 file_id String videoUrl = result.has("video_url") ? result.get("video_url").asText(null) : null; if (videoUrl == null && result.has("file_id")) { - videoUrl = resolveFileUrl(result.get("file_id").asText(), apiKey); + videoUrl = resolveFileUrl(result.get("file_id").asText(), apiKey, baseUrl); } yield TaskPollResult.succeeded(videoUrl, null, result.toString()); } @@ -156,12 +200,13 @@ public class MiniMaxVideoProvider implements VideoGenerationProvider { } /** - * 通过 file_id 获取视频下载 URL + * 通过 file_id 获取视频下载 URL。Region must match the host that produced + * the file_id — otherwise the cross-host lookup 404s. */ - private String resolveFileUrl(String fileId, String apiKey) { + private String resolveFileUrl(String fileId, String apiKey, String baseUrl) { try { HttpResponse response = HttpRequest.get( - BASE_URL + "/v1/files/retrieve?file_id=" + fileId) + baseUrl + "/v1/files/retrieve?file_id=" + fileId) .header("Authorization", "Bearer " + apiKey) .timeout(10_000) .execute(); diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts index 9ba3e05d..c1731cc3 100644 --- a/mateclaw-ui/src/i18n/locales/en-US.ts +++ b/mateclaw-ui/src/i18n/locales/en-US.ts @@ -465,6 +465,7 @@ export default { klingSecretKey: 'Kling Secret Key', runwayApiKey: 'Runway API Key', minimaxApiKey: 'MiniMax API Key', + minimaxRegion: 'MiniMax API Region', }, hints: { provider: 'Current implementation applies DashScope model options at runtime.', @@ -518,6 +519,7 @@ export default { klingSecretKey: 'Paired with Access Key for JWT authentication.', runwayApiKey: 'Get from runwayml.com. Supports gen4.5, gen4_turbo flagship models.', minimaxApiKey: 'Get from minimaxi.com. Hailuo video with free quota, excellent for Chinese scenes.', + minimaxRegion: 'Choose the API host. Mainland China accounts must use the CN endpoint; others use Global.', }, searchTitle: 'Search Service', searchDesc: 'Configure the built-in search tool provider and API credentials', @@ -564,6 +566,8 @@ export default { freeQuota: 'Free Quota Available', configuredInModels: 'See Model Settings', }, + minimaxRegionGlobal: 'Global (api.minimax.io)', + minimaxRegionCn: 'China (api.minimaxi.com)', actions: { setDefault: 'Set Default', saveSystem: 'Save System Settings', diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts index 09e2f213..72553ab2 100644 --- a/mateclaw-ui/src/i18n/locales/zh-CN.ts +++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts @@ -460,6 +460,7 @@ export default { klingSecretKey: '可灵 Secret Key', runwayApiKey: 'Runway API Key', minimaxApiKey: 'MiniMax API Key', + minimaxRegion: 'MiniMax API 区域', }, hints: { provider: '当前版本会把 DashScope 模型参数真实应用到 Agent 调用链路。', @@ -518,6 +519,7 @@ export default { klingSecretKey: '与 Access Key 配对使用,用于 JWT 签名鉴权。', runwayApiKey: '从 runwayml.com 获取。支持 gen4.5、gen4_turbo 等旗舰模型。', minimaxApiKey: '从 minimaxi.com 获取。海螺 Hailuo 视频,有免费额度,中文场景优秀。', + minimaxRegion: '选择 API 入口域名。中国账号用 CN,海外账号用 Global。', }, searchTitle: '搜索服务', searchDesc: '配置内置搜索工具的提供商与 API 凭证', @@ -564,6 +566,8 @@ export default { freeQuota: '有免费额度', configuredInModels: '前往模型设置查看', }, + minimaxRegionGlobal: '海外(api.minimax.io)', + minimaxRegionCn: '中国大陆(api.minimaxi.com)', actions: { setDefault: '设为默认', saveSystem: '保存系统设置', diff --git a/mateclaw-ui/src/views/Settings/Video/index.vue b/mateclaw-ui/src/views/Settings/Video/index.vue index ea2dbe28..a3591b53 100644 --- a/mateclaw-ui/src/views/Settings/Video/index.vue +++ b/mateclaw-ui/src/views/Settings/Video/index.vue @@ -225,6 +225,18 @@ /> +
+
+
{{ t('settings.fields.minimaxRegion') }}
+
{{ t('settings.hints.minimaxRegion') }}
+
+
+ +
+
@@ -265,6 +277,8 @@ const settings = reactive({ klingSecretKeyMasked: '', runwayApiKeyMasked: '', minimaxApiKeyMasked: '', + // RFC: MiniMax region 控制 image+video 共用的 API host. "global" 默认 / "cn" → api.minimaxi.com + minimaxRegion: 'global', }) onMounted(async () => { @@ -284,6 +298,7 @@ async function loadSettings() { settings.klingSecretKeyMasked = data.klingSecretKeyMasked ?? '' settings.runwayApiKeyMasked = data.runwayApiKeyMasked ?? '' settings.minimaxApiKeyMasked = data.minimaxApiKeyMasked ?? '' + settings.minimaxRegion = data.minimaxRegion ?? 'global' // 清空密钥输入 zhipuApiKeyInput.value = '' falApiKeyInput.value = '' @@ -306,6 +321,7 @@ async function onSaveSettings() { if (klingSecretKeyInput.value) payload.klingSecretKey = klingSecretKeyInput.value if (runwayApiKeyInput.value) payload.runwayApiKey = runwayApiKeyInput.value if (minimaxApiKeyInput.value) payload.minimaxApiKey = minimaxApiKeyInput.value + payload.minimaxRegion = settings.minimaxRegion await settingsApi.update(payload) await loadSettings()