diff --git a/packages/core/src/opendirector/integrations/generation_errors.py b/packages/core/src/opendirector/integrations/generation_errors.py index 18ee7bda..9f84bdb9 100644 --- a/packages/core/src/opendirector/integrations/generation_errors.py +++ b/packages/core/src/opendirector/integrations/generation_errors.py @@ -22,6 +22,14 @@ 'provider': ('生成服务未能完成当前操作。', '请核对原任务;错误码和请求标识可用于向供应商排查。'), } +# MiniMax content moderation: 1026 rejects the input, 1027 the generated output. +MINIMAX_SENSITIVE = { + '1026': ('供应商判定输入内容涉及敏感信息,已拒绝生成。', + '请修改提示词或更换参考素材后再生成;相同输入重试仍会被拒绝。'), + '1027': ('供应商判定生成内容涉及敏感信息,已拒绝交付。', + '请调整提示词或参考素材,避免可能触发审核的角色或内容后再生成;相同输入重试仍会被拒绝。'), +} + def _identifier(value, limit, secrets): if isinstance(value, int) and not isinstance(value, bool): @@ -80,6 +88,10 @@ def provider_failure(value, *, stage, headers=None, http_status=None, secrets=() suggestion = '请核对参考类型和生成模式,或选择其他已适配模型;当前输入和已有结果保留。' if name == 'inputimagesensitivecontentdetected.privacyinformation': message = '供应商判定参考图片可能包含真人,已拒绝生成。' + elif code in MINIMAX_SENSITIVE: + # A moderation verdict is deterministic: the same input fails again, so + # the suggestion must lead to changing it rather than retrying. + category, (message, suggestion) = 'input', MINIMAX_SENSITIVE[code] elif category == 'provider' and value.get('status') in {'cancelled', 'expired'}: message = '供应商任务已取消。' if value['status'] == 'cancelled' else '供应商任务已过期。' elif http_status == 404: diff --git a/src/canvas/generationFailureText.ts b/src/canvas/generationFailureText.ts index 5c246035..d9fd845d 100644 --- a/src/canvas/generationFailureText.ts +++ b/src/canvas/generationFailureText.ts @@ -24,6 +24,10 @@ const BY_MESSAGE: Record = { "errors.generationFailure.referenceUnsupported", "供应商判定参考图片可能包含真人,已拒绝生成。": "errors.generationFailure.privacy", + "供应商判定输入内容涉及敏感信息,已拒绝生成。": + "errors.generationFailure.sensitiveInput", + "供应商判定生成内容涉及敏感信息,已拒绝交付。": + "errors.generationFailure.sensitiveOutput", "供应商任务已取消。": "errors.generationFailure.cancelled", "供应商任务已过期。": "errors.generationFailure.expired", "供应商未找到该任务。": "errors.generationFailure.notFoundTask", diff --git a/src/i18n/messages/en-US/errors.ts b/src/i18n/messages/en-US/errors.ts index f38b2c77..56e73ebf 100644 --- a/src/i18n/messages/en-US/errors.ts +++ b/src/i18n/messages/en-US/errors.ts @@ -678,6 +678,10 @@ export const errors = { "The provider rejected the reference type or input combination.", "errors.generationFailure.privacy": "The provider rejected the reference image because it may contain a real person.", + "errors.generationFailure.sensitiveInput": + "The provider flagged the input as sensitive content and declined to generate. Change the prompt or reference media before generating again.", + "errors.generationFailure.sensitiveOutput": + "The provider flagged the generated content as sensitive and withheld it. Adjust the prompt or reference media before generating again.", "errors.history.failed": "Generation failed. Check the service log.", "errors.connection.endpointMissing": "The node no longer exists", diff --git a/src/i18n/messages/zh-CN/errors.ts b/src/i18n/messages/zh-CN/errors.ts index aabd245b..b6510c4b 100644 --- a/src/i18n/messages/zh-CN/errors.ts +++ b/src/i18n/messages/zh-CN/errors.ts @@ -470,6 +470,8 @@ export const errors = { "errors.generationFailure.referenceAudio": "音频参考不能单独使用,请搭配参考图片。", "errors.generationFailure.referenceUnsupported": "供应商拒绝了参考类型或输入组合。", "errors.generationFailure.privacy": "供应商判定参考图片可能包含真人,已拒绝生成。", + "errors.generationFailure.sensitiveInput": "供应商判定输入内容涉及敏感信息,已拒绝生成。请修改提示词或参考素材后再生成。", + "errors.generationFailure.sensitiveOutput": "供应商判定生成内容涉及敏感信息,已拒绝交付。请调整提示词或参考素材后再生成。", "errors.history.failed": "生成失败,请检查服务日志。", "errors.connection.endpointMissing": "节点已不存在", "errors.connection.sourceEmpty": "当前节点内容为空,请先上传或生成节点内容", diff --git a/tests/generationFailureText.test.ts b/tests/generationFailureText.test.ts index a073ac81..fbaed8e6 100644 --- a/tests/generationFailureText.test.ts +++ b/tests/generationFailureText.test.ts @@ -82,3 +82,22 @@ test("submission unknown failures remain explicit", () => { ); setActiveLocale("zh-CN"); }); + +test("provider moderation verdicts are explained in both languages", () => { + const moderated = (message: string) => + failure({ stage: "poll", httpStatus: 200, providerStatus: "failed", providerCode: "1027", message }); + setActiveLocale("en-US"); + assert.match( + generationFailureMessage(moderated("供应商判定输入内容涉及敏感信息,已拒绝生成。")), + /flagged the input as sensitive/, + ); + assert.match( + generationFailureMessage(moderated("供应商判定生成内容涉及敏感信息,已拒绝交付。")), + /flagged the generated content as sensitive/, + ); + setActiveLocale("zh-CN"); + assert.match( + generationFailureMessage(moderated("供应商判定生成内容涉及敏感信息,已拒绝交付。")), + /生成内容涉及敏感信息/, + ); +}); diff --git a/tests/test_minimax_video.py b/tests/test_minimax_video.py index 4c9e7ca9..890b459e 100644 --- a/tests/test_minimax_video.py +++ b/tests/test_minimax_video.py @@ -265,3 +265,27 @@ def test_official_variants_enforce_native_outputs(model, resolutions, minimum): adapter.payload(configured, {'parameters': {'resolution': invalid}}, prepared) with pytest.raises(ProjectError, match='输出规格无效'): adapter.payload(configured, {'parameters': {'duration': minimum - 1}}, prepared) + + +@pytest.mark.anyio +@pytest.mark.parametrize(('code', 'message'), [(1026, '输入内容涉及敏感信息'), (1027, '生成内容涉及敏感信息')]) +async def test_moderation_failures_say_the_content_was_rejected_instead_of_a_generic_provider_error(code, message): + def handle(request): + if request.method == 'POST': + return httpx.Response(200, json={'task_id': 'task-1'}) + return httpx.Response(200, json={'task': {'id': 'task-1', 'status': 'failed', + 'error': {'code': code, 'message': 'private moderation detail'}}}) + accepted = [] + async def submitted(identity): + accepted.append(identity) + adapter = MinimaxVideoGeneration(None, httpx.MockTransport(handle), poll_interval=0) + adapter.prepare_inputs = lambda *args: {'content': [{'type': 'text', 'text': 'test'}]} + with pytest.raises(GenerationError) as failure: + await adapter.generate(config(), task('project'), submitted) + details = failure.value.details + assert accepted == ['task-1'] + assert details.category == 'input' and details.stage == 'poll' and details.providerStatus == 'failed' + assert details.providerCode == str(code) and message in details.message + # Moderation is deterministic: the suggestion must not invite an unchanged retry. + assert '相同输入重试仍会被拒绝' in details.suggestion + assert 'private moderation detail' not in str(failure.value)