feat(minimax): expand video model catalog + add CN endpoint support

This commit is contained in:
matevip 2026-04-26 08:34:35 +08:00
parent 410c6c28cd
commit 4d7c6593c4
6 changed files with 115 additions and 14 deletions

View File

@ -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:
* <ul>
* <li>{@code "global"} (default) {@code https://api.minimax.io}</li>
* <li>{@code "cn"} {@code https://api.minimaxi.com} (lower latency from
* mainland China; required for accounts registered there).</li>
* </ul>
*/
private String minimaxRegion;
// ===== 图片生成配置 =====
/** 是否启用图片生成能力 */

View File

@ -20,9 +20,9 @@ import java.util.Set;
* MiniMax 图片生成 Provider image-01 模型
* <p>
* 同步模式返回 Base64 图片
* 复用视频生成中的 MiniMax API Key
* 复用视频生成中的 MiniMax API Key + region 设置{@code minimaxRegion}
* <p>
* API: POST https://api.minimax.io/v1/image_generation
* API: POST {@code <baseUrl>/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())

View File

@ -21,6 +21,13 @@ import java.util.Set;
* <p>
* API 文档: https://platform.minimaxi.com/document/video-generation
* 鉴权: Bearer Token
* <p>
* Region 切换根据 {@link SystemSettingsDTO#getMinimaxRegion()} host
* <ul>
* <li>{@code "global"} (默认) {@code https://api.minimax.io}</li>
* <li>{@code "cn"} {@code https://api.minimaxi.com} (mainland-CN
* lower-latency endpoint; required for accounts registered in CN).</li>
* </ul>
*
* @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<String> 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 获取视频下载 URLRegion 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();

View File

@ -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',

View File

@ -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: '保存系统设置',

View File

@ -225,6 +225,18 @@
/>
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">{{ t('settings.fields.minimaxRegion') }}</div>
<div class="setting-hint">{{ t('settings.hints.minimaxRegion') }}</div>
</div>
<div class="setting-control">
<select v-model="settings.minimaxRegion" class="form-select">
<option value="global">{{ t('settings.minimaxRegionGlobal') }}</option>
<option value="cn">{{ t('settings.minimaxRegionCn') }}</option>
</select>
</div>
</div>
</div>
</div>
</template>
@ -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()