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:
+ *
+ * {@code "global"} (default) → {@code https://api.minimax.io}
+ * {@code "cn"} → {@code https://api.minimaxi.com} (lower latency from
+ * mainland China; required for accounts registered there).
+ *
+ */
+ 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:
+ *
+ * {@code "global"} (默认) → {@code https://api.minimax.io}
+ * {@code "cn"} → {@code https://api.minimaxi.com} (mainland-CN
+ * lower-latency endpoint; required for accounts registered in CN).
+ *
*
* @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') }}
+
+
+
+ {{ t('settings.minimaxRegionGlobal') }}
+ {{ t('settings.minimaxRegionCn') }}
+
+
+
@@ -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()