mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 20:08:18 +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)
|
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
|
||||||
private String minimaxApiKey;
|
private String minimaxApiKey;
|
||||||
private String minimaxApiKeyMasked;
|
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 模型
|
* MiniMax 图片生成 Provider — image-01 模型
|
||||||
* <p>
|
* <p>
|
||||||
* 同步模式:返回 Base64 图片。
|
* 同步模式:返回 Base64 图片。
|
||||||
* 复用视频生成中的 MiniMax API Key。
|
* 复用视频生成中的 MiniMax API Key + region 设置({@code minimaxRegion})。
|
||||||
* <p>
|
* <p>
|
||||||
* API: POST https://api.minimax.io/v1/image_generation
|
* API: POST {@code <baseUrl>/v1/image_generation} —— host 由 region 决定。
|
||||||
*
|
*
|
||||||
* @author MateClaw Team
|
* @author MateClaw Team
|
||||||
*/
|
*/
|
||||||
@ -33,9 +33,30 @@ public class MiniMaxImageProvider implements ImageGenerationProvider {
|
|||||||
|
|
||||||
private final ObjectMapper objectMapper;
|
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";
|
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
|
@Override
|
||||||
public String id() {
|
public String id() {
|
||||||
return "minimax";
|
return "minimax";
|
||||||
@ -96,7 +117,8 @@ public class MiniMaxImageProvider implements ImageGenerationProvider {
|
|||||||
body.put("aspect_ratio", request.getAspectRatio());
|
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("Authorization", "Bearer " + apiKey)
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.body(body.toString())
|
.body(body.toString())
|
||||||
|
|||||||
@ -21,6 +21,13 @@ import java.util.Set;
|
|||||||
* <p>
|
* <p>
|
||||||
* API 文档: https://platform.minimaxi.com/document/video-generation
|
* API 文档: https://platform.minimaxi.com/document/video-generation
|
||||||
* 鉴权: Bearer Token
|
* 鉴权: 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
|
* @author MateClaw Team
|
||||||
*/
|
*/
|
||||||
@ -31,9 +38,44 @@ public class MiniMaxVideoProvider implements VideoGenerationProvider {
|
|||||||
|
|
||||||
private final ObjectMapper objectMapper;
|
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";
|
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
|
@Override
|
||||||
public String id() {
|
public String id() {
|
||||||
return "minimax";
|
return "minimax";
|
||||||
@ -67,7 +109,7 @@ public class MiniMaxVideoProvider implements VideoGenerationProvider {
|
|||||||
.supportedDurations(List.of(6, 10))
|
.supportedDurations(List.of(6, 10))
|
||||||
.maxDurationSeconds(10)
|
.maxDurationSeconds(10)
|
||||||
.defaultModel(DEFAULT_MODEL)
|
.defaultModel(DEFAULT_MODEL)
|
||||||
.models(List.of("MiniMax-Hailuo-2.3", "MiniMax-Hailuo-2.3-Fast", "I2V-01-live"))
|
.models(MODEL_CATALOG)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,6 +122,7 @@ public class MiniMaxVideoProvider implements VideoGenerationProvider {
|
|||||||
public VideoSubmitResult submit(VideoGenerationRequest request, SystemSettingsDTO config) {
|
public VideoSubmitResult submit(VideoGenerationRequest request, SystemSettingsDTO config) {
|
||||||
try {
|
try {
|
||||||
String apiKey = config.getMinimaxApiKey();
|
String apiKey = config.getMinimaxApiKey();
|
||||||
|
String baseUrl = resolveBaseUrl(config);
|
||||||
String model = request.getModel() != null ? request.getModel() : DEFAULT_MODEL;
|
String model = request.getModel() != null ? request.getModel() : DEFAULT_MODEL;
|
||||||
|
|
||||||
ObjectNode body = objectMapper.createObjectNode();
|
ObjectNode body = objectMapper.createObjectNode();
|
||||||
@ -93,7 +136,7 @@ public class MiniMaxVideoProvider implements VideoGenerationProvider {
|
|||||||
body.put("duration", request.getDurationSeconds());
|
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("Authorization", "Bearer " + apiKey)
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.body(body.toString())
|
.body(body.toString())
|
||||||
@ -106,11 +149,11 @@ public class MiniMaxVideoProvider implements VideoGenerationProvider {
|
|||||||
int statusCode = result.path("base_resp").path("status_code").asInt(-1);
|
int statusCode = result.path("base_resp").path("status_code").asInt(-1);
|
||||||
if (statusCode == 0 && result.has("task_id")) {
|
if (statusCode == 0 && result.has("task_id")) {
|
||||||
String taskId = result.get("task_id").asText();
|
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());
|
return VideoSubmitResult.success(taskId, id());
|
||||||
} else {
|
} else {
|
||||||
String errMsg = result.path("base_resp").path("status_msg").asText("未知错误");
|
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);
|
return VideoSubmitResult.failure(id(), errMsg);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@ -123,9 +166,10 @@ public class MiniMaxVideoProvider implements VideoGenerationProvider {
|
|||||||
public TaskPollResult checkStatus(String providerTaskId, SystemSettingsDTO config) {
|
public TaskPollResult checkStatus(String providerTaskId, SystemSettingsDTO config) {
|
||||||
try {
|
try {
|
||||||
String apiKey = config.getMinimaxApiKey();
|
String apiKey = config.getMinimaxApiKey();
|
||||||
|
String baseUrl = resolveBaseUrl(config);
|
||||||
|
|
||||||
HttpResponse response = HttpRequest.get(
|
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)
|
.header("Authorization", "Bearer " + apiKey)
|
||||||
.timeout(15_000)
|
.timeout(15_000)
|
||||||
.execute();
|
.execute();
|
||||||
@ -138,7 +182,7 @@ public class MiniMaxVideoProvider implements VideoGenerationProvider {
|
|||||||
// 优先取 video_url,备选 file_id
|
// 优先取 video_url,备选 file_id
|
||||||
String videoUrl = result.has("video_url") ? result.get("video_url").asText(null) : null;
|
String videoUrl = result.has("video_url") ? result.get("video_url").asText(null) : null;
|
||||||
if (videoUrl == null && result.has("file_id")) {
|
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());
|
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 {
|
try {
|
||||||
HttpResponse response = HttpRequest.get(
|
HttpResponse response = HttpRequest.get(
|
||||||
BASE_URL + "/v1/files/retrieve?file_id=" + fileId)
|
baseUrl + "/v1/files/retrieve?file_id=" + fileId)
|
||||||
.header("Authorization", "Bearer " + apiKey)
|
.header("Authorization", "Bearer " + apiKey)
|
||||||
.timeout(10_000)
|
.timeout(10_000)
|
||||||
.execute();
|
.execute();
|
||||||
|
|||||||
@ -465,6 +465,7 @@ export default {
|
|||||||
klingSecretKey: 'Kling Secret Key',
|
klingSecretKey: 'Kling Secret Key',
|
||||||
runwayApiKey: 'Runway API Key',
|
runwayApiKey: 'Runway API Key',
|
||||||
minimaxApiKey: 'MiniMax API Key',
|
minimaxApiKey: 'MiniMax API Key',
|
||||||
|
minimaxRegion: 'MiniMax API Region',
|
||||||
},
|
},
|
||||||
hints: {
|
hints: {
|
||||||
provider: 'Current implementation applies DashScope model options at runtime.',
|
provider: 'Current implementation applies DashScope model options at runtime.',
|
||||||
@ -518,6 +519,7 @@ export default {
|
|||||||
klingSecretKey: 'Paired with Access Key for JWT authentication.',
|
klingSecretKey: 'Paired with Access Key for JWT authentication.',
|
||||||
runwayApiKey: 'Get from runwayml.com. Supports gen4.5, gen4_turbo flagship models.',
|
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.',
|
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',
|
searchTitle: 'Search Service',
|
||||||
searchDesc: 'Configure the built-in search tool provider and API credentials',
|
searchDesc: 'Configure the built-in search tool provider and API credentials',
|
||||||
@ -564,6 +566,8 @@ export default {
|
|||||||
freeQuota: 'Free Quota Available',
|
freeQuota: 'Free Quota Available',
|
||||||
configuredInModels: 'See Model Settings',
|
configuredInModels: 'See Model Settings',
|
||||||
},
|
},
|
||||||
|
minimaxRegionGlobal: 'Global (api.minimax.io)',
|
||||||
|
minimaxRegionCn: 'China (api.minimaxi.com)',
|
||||||
actions: {
|
actions: {
|
||||||
setDefault: 'Set Default',
|
setDefault: 'Set Default',
|
||||||
saveSystem: 'Save System Settings',
|
saveSystem: 'Save System Settings',
|
||||||
|
|||||||
@ -460,6 +460,7 @@ export default {
|
|||||||
klingSecretKey: '可灵 Secret Key',
|
klingSecretKey: '可灵 Secret Key',
|
||||||
runwayApiKey: 'Runway API Key',
|
runwayApiKey: 'Runway API Key',
|
||||||
minimaxApiKey: 'MiniMax API Key',
|
minimaxApiKey: 'MiniMax API Key',
|
||||||
|
minimaxRegion: 'MiniMax API 区域',
|
||||||
},
|
},
|
||||||
hints: {
|
hints: {
|
||||||
provider: '当前版本会把 DashScope 模型参数真实应用到 Agent 调用链路。',
|
provider: '当前版本会把 DashScope 模型参数真实应用到 Agent 调用链路。',
|
||||||
@ -518,6 +519,7 @@ export default {
|
|||||||
klingSecretKey: '与 Access Key 配对使用,用于 JWT 签名鉴权。',
|
klingSecretKey: '与 Access Key 配对使用,用于 JWT 签名鉴权。',
|
||||||
runwayApiKey: '从 runwayml.com 获取。支持 gen4.5、gen4_turbo 等旗舰模型。',
|
runwayApiKey: '从 runwayml.com 获取。支持 gen4.5、gen4_turbo 等旗舰模型。',
|
||||||
minimaxApiKey: '从 minimaxi.com 获取。海螺 Hailuo 视频,有免费额度,中文场景优秀。',
|
minimaxApiKey: '从 minimaxi.com 获取。海螺 Hailuo 视频,有免费额度,中文场景优秀。',
|
||||||
|
minimaxRegion: '选择 API 入口域名。中国账号用 CN,海外账号用 Global。',
|
||||||
},
|
},
|
||||||
searchTitle: '搜索服务',
|
searchTitle: '搜索服务',
|
||||||
searchDesc: '配置内置搜索工具的提供商与 API 凭证',
|
searchDesc: '配置内置搜索工具的提供商与 API 凭证',
|
||||||
@ -564,6 +566,8 @@ export default {
|
|||||||
freeQuota: '有免费额度',
|
freeQuota: '有免费额度',
|
||||||
configuredInModels: '前往模型设置查看',
|
configuredInModels: '前往模型设置查看',
|
||||||
},
|
},
|
||||||
|
minimaxRegionGlobal: '海外(api.minimax.io)',
|
||||||
|
minimaxRegionCn: '中国大陆(api.minimaxi.com)',
|
||||||
actions: {
|
actions: {
|
||||||
setDefault: '设为默认',
|
setDefault: '设为默认',
|
||||||
saveSystem: '保存系统设置',
|
saveSystem: '保存系统设置',
|
||||||
|
|||||||
@ -225,6 +225,18 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -265,6 +277,8 @@ const settings = reactive({
|
|||||||
klingSecretKeyMasked: '',
|
klingSecretKeyMasked: '',
|
||||||
runwayApiKeyMasked: '',
|
runwayApiKeyMasked: '',
|
||||||
minimaxApiKeyMasked: '',
|
minimaxApiKeyMasked: '',
|
||||||
|
// RFC: MiniMax region 控制 image+video 共用的 API host. "global" 默认 / "cn" → api.minimaxi.com
|
||||||
|
minimaxRegion: 'global',
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@ -284,6 +298,7 @@ async function loadSettings() {
|
|||||||
settings.klingSecretKeyMasked = data.klingSecretKeyMasked ?? ''
|
settings.klingSecretKeyMasked = data.klingSecretKeyMasked ?? ''
|
||||||
settings.runwayApiKeyMasked = data.runwayApiKeyMasked ?? ''
|
settings.runwayApiKeyMasked = data.runwayApiKeyMasked ?? ''
|
||||||
settings.minimaxApiKeyMasked = data.minimaxApiKeyMasked ?? ''
|
settings.minimaxApiKeyMasked = data.minimaxApiKeyMasked ?? ''
|
||||||
|
settings.minimaxRegion = data.minimaxRegion ?? 'global'
|
||||||
// 清空密钥输入
|
// 清空密钥输入
|
||||||
zhipuApiKeyInput.value = ''
|
zhipuApiKeyInput.value = ''
|
||||||
falApiKeyInput.value = ''
|
falApiKeyInput.value = ''
|
||||||
@ -306,6 +321,7 @@ async function onSaveSettings() {
|
|||||||
if (klingSecretKeyInput.value) payload.klingSecretKey = klingSecretKeyInput.value
|
if (klingSecretKeyInput.value) payload.klingSecretKey = klingSecretKeyInput.value
|
||||||
if (runwayApiKeyInput.value) payload.runwayApiKey = runwayApiKeyInput.value
|
if (runwayApiKeyInput.value) payload.runwayApiKey = runwayApiKeyInput.value
|
||||||
if (minimaxApiKeyInput.value) payload.minimaxApiKey = minimaxApiKeyInput.value
|
if (minimaxApiKeyInput.value) payload.minimaxApiKey = minimaxApiKeyInput.value
|
||||||
|
payload.minimaxRegion = settings.minimaxRegion
|
||||||
|
|
||||||
await settingsApi.update(payload)
|
await settingsApi.update(payload)
|
||||||
await loadSettings()
|
await loadSettings()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user