diff --git a/ERRORS.md b/ERRORS.md index 8eae7d82..9ea439a3 100644 --- a/ERRORS.md +++ b/ERRORS.md @@ -226,6 +226,7 @@ No error scenarios — prints a message directing users to run `npm update -g mm | HTTP 401 / 403 | `API key rejected (HTTP ${status}).` | | HTTP 429 | `Rate limit or quota exceeded. ${apiMsg}` | | `status_code` 1002 / 1039 (content filter) | `Input content flagged by sensitivity filter (${filterType}).` | +| `status_msg` contains "new_sensitive" / output sensitivity (HTTP 200) | `Output withheld by content moderation (${apiMsg}).` | | `status_code` 1028 / 1030 (quota exhausted) | `Quota exhausted. ${apiMsg}` | | `status_code` 2061 (model not on plan) | `This model is not available on your current Token Plan. ${apiMsg}` | | Other API errors | `API error: ${apiMsg} (HTTP ${status})` | diff --git a/src/errors/api.ts b/src/errors/api.ts index ecdd5433..48fb3013 100644 --- a/src/errors/api.ts +++ b/src/errors/api.ts @@ -75,7 +75,7 @@ export function mapApiError(status: number, body: ApiErrorBody, url?: string): C (/sensitive content/i.test(apiMsg) || /(?:^|\D)1026(?:\D|$)/.test(apiMsg)); // MiniMax content sensitivity filter - if (apiCode === 1002 || apiCode === 1039 || isV2ContentFilter) { + if (apiCode === 1002 || apiCode === 1039 || apiCode === 1026 || isV2ContentFilter) { const filterType = body.base_resp?.status_msg || body.error?.message || @@ -106,6 +106,15 @@ export function mapApiError(status: number, body: ApiErrorBody, url?: string): C ); } + // output sensitivity (status_code 1027, e.g. "output new_sensitive") + if (apiCode === 1027 || /output.*sensitive/i.test(apiMsg)) { + return new CLIError( + `Output withheld by content moderation (${apiMsg}).`, + ExitCode.CONTENT_FILTER, + 'Refine your query and try again.', + ); + } + return new CLIError( `API error: ${apiMsg} (HTTP ${status})`, ExitCode.GENERAL, diff --git a/test/errors/api.test.ts b/test/errors/api.test.ts index 29a0c80a..6a56f329 100644 --- a/test/errors/api.test.ts +++ b/test/errors/api.test.ts @@ -73,6 +73,32 @@ describe('mapApiError', () => { expect(err.exitCode).toBe(ExitCode.QUOTA); }); + it('maps output sensitivity (HTTP 200) to CONTENT_FILTER', () => { + const err = mapApiError(200, { + base_resp: { status_code: 2001, status_msg: 'output new_sensitive' }, + }); + expect(err.exitCode).toBe(ExitCode.CONTENT_FILTER); + expect(err.message).toContain('content moderation'); + expect(err.message).toContain('output new_sensitive'); + expect(err.hint).toBeDefined(); + }); + + it('maps output sensitivity code 1027 to CONTENT_FILTER', () => { + const err = mapApiError(200, { + base_resp: { status_code: 1027, status_msg: '输出内容涉敏' }, + }); + expect(err.exitCode).toBe(ExitCode.CONTENT_FILTER); + }); + + it('maps input sensitivity code 1026 to CONTENT_FILTER (not output)', () => { + const err = mapApiError(200, { + base_resp: { status_code: 1026, status_msg: 'input new_sensitive' }, + }); + expect(err.exitCode).toBe(ExitCode.CONTENT_FILTER); + expect(err.message).toContain('Input'); + expect(err.message).not.toContain('Output withheld'); + }); + it('maps unknown errors to GENERAL', () => { const err = mapApiError(500, { base_resp: { status_code: 0, status_msg: 'internal error' } }); expect(err.exitCode).toBe(ExitCode.GENERAL);