mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(minimax): expand video model catalog + add CN endpoint support
This commit is contained in:
parent
410c6c28cd
commit
4d7c6593c4
@ -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;
|
||||
|
||||
// ===== 图片生成配置 =====
|
||||
/** 是否启用图片生成能力 */
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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 获取视频下载 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();
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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: '保存系统设置',
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user