Spaces:
Running
Running
Deploy bf62eed to Docker Space
Browse filesSource: MisonL/gpt-image-playground-customer@bf62eed15cedbc56fb8df227f2d76273004d494e
- public/hf-space-deploy-marker.json +3 -3
- src/app/api/agent/agent-routes.test.ts +59 -0
- src/app/api/deploy-marker/route.ts +1 -1
- src/lib/agent-image-service.ts +15 -0
- src/lib/api-error-response.test.ts +18 -0
- src/lib/api-error-response.ts +18 -1
- src/lib/channel-router.test.ts +30 -0
- src/lib/channel-router.ts +46 -1
- src/lib/image-route-support.ts +15 -0
public/hf-space-deploy-marker.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{
|
| 2 |
"schema_version": 1,
|
| 3 |
-
"local_sha": "
|
| 4 |
-
"created_at": "2026-06-
|
| 5 |
-
"deploy_id": "
|
| 6 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"schema_version": 1,
|
| 3 |
+
"local_sha": "bf62eed15cedbc56fb8df227f2d76273004d494e",
|
| 4 |
+
"created_at": "2026-06-26T09:26:30.691Z",
|
| 5 |
+
"deploy_id": "5b30e427-0d60-4ce4-b6ce-667ada74c8c0"
|
| 6 |
}
|
src/app/api/agent/agent-routes.test.ts
CHANGED
|
@@ -785,6 +785,42 @@ describe('Agent route integration', () => {
|
|
| 785 |
}
|
| 786 |
});
|
| 787 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 788 |
it('uses force-sse for Agent Responses image_generation SSE while keeping the final JSON contract', async () => {
|
| 789 |
const { generateImage } = await loadAgentRoutes();
|
| 790 |
let upstreamBody = '';
|
|
@@ -3462,6 +3498,29 @@ async function startStreamingResponsesImageUpstream(
|
|
| 3462 |
};
|
| 3463 |
}
|
| 3464 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3465 |
function readStoredResponseJson(idempotencyKey: string): string {
|
| 3466 |
const db = new Database(path.join(tempDir, 'agent.sqlite'), { readonly: true });
|
| 3467 |
try {
|
|
|
|
| 785 |
}
|
| 786 |
});
|
| 787 |
|
| 788 |
+
it('marks Responses image_generation-disabled 403s as unavailable request modes', async () => {
|
| 789 |
+
process.env.AGENT_STATE_BACKEND = 'memory';
|
| 790 |
+
delete process.env.AGENT_SQLITE_PATH;
|
| 791 |
+
const { generateImage } = await loadAgentRoutes();
|
| 792 |
+
const upstream = await startResponsesImageJsonUpstream(403, {
|
| 793 |
+
error: { message: 'Image generation is not enabled for this group' }
|
| 794 |
+
});
|
| 795 |
+
process.env.ENABLE_RESPONSES_IMAGE_BACKEND = 'true';
|
| 796 |
+
process.env.OPENAI_RESPONSES_API_MODEL = 'gpt-5.4';
|
| 797 |
+
process.env.OPENAI_API_KEY = 'test-key';
|
| 798 |
+
process.env.OPENAI_API_BASE_URL = upstream.baseUrl;
|
| 799 |
+
|
| 800 |
+
try {
|
| 801 |
+
const response = await generateImage(
|
| 802 |
+
agentJsonRequest('agent-responses-image-disabled-key', {
|
| 803 |
+
prompt: 'agent responses disabled',
|
| 804 |
+
image_backend: 'responses-image-generation',
|
| 805 |
+
streaming_strategy: 'responses-sse',
|
| 806 |
+
partial_images: 2
|
| 807 |
+
})
|
| 808 |
+
);
|
| 809 |
+
|
| 810 |
+
assert.equal(response.status, 502);
|
| 811 |
+
const body = await response.json();
|
| 812 |
+
assert.equal(body.error.code, 'upstream_unavailable');
|
| 813 |
+
assert.equal(body.error.upstream_status, 403);
|
| 814 |
+
assert.equal(body.error.diagnostics.channel_request_mode, 'responses-sse');
|
| 815 |
+
assert.equal(body.error.diagnostics.channel_cooldown_scope, 'channel');
|
| 816 |
+
assert.equal(body.error.diagnostics.cooldown_target.channel_id, 'default');
|
| 817 |
+
assert.equal(body.error.diagnostics.cooldown_target.request_mode, 'responses-sse');
|
| 818 |
+
assert.equal(JSON.stringify(body).includes('test-key'), false);
|
| 819 |
+
} finally {
|
| 820 |
+
await upstream.close();
|
| 821 |
+
}
|
| 822 |
+
});
|
| 823 |
+
|
| 824 |
it('uses force-sse for Agent Responses image_generation SSE while keeping the final JSON contract', async () => {
|
| 825 |
const { generateImage } = await loadAgentRoutes();
|
| 826 |
let upstreamBody = '';
|
|
|
|
| 3498 |
};
|
| 3499 |
}
|
| 3500 |
|
| 3501 |
+
async function startResponsesImageJsonUpstream(
|
| 3502 |
+
status: number,
|
| 3503 |
+
body: unknown
|
| 3504 |
+
): Promise<{ baseUrl: string; close: () => Promise<void> }> {
|
| 3505 |
+
const server = http.createServer((request, response) => {
|
| 3506 |
+
if (request.method !== 'POST' || !request.url?.endsWith('/responses')) {
|
| 3507 |
+
response.writeHead(404, { 'Content-Type': 'application/json' });
|
| 3508 |
+
response.end(JSON.stringify({ error: { message: 'not found' } }));
|
| 3509 |
+
return;
|
| 3510 |
+
}
|
| 3511 |
+
request.resume();
|
| 3512 |
+
response.writeHead(status, { 'Content-Type': 'application/json' });
|
| 3513 |
+
response.end(JSON.stringify(body));
|
| 3514 |
+
});
|
| 3515 |
+
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
| 3516 |
+
const address = server.address();
|
| 3517 |
+
assert.ok(address && typeof address === 'object');
|
| 3518 |
+
return {
|
| 3519 |
+
baseUrl: `http://127.0.0.1:${address.port}/v1`,
|
| 3520 |
+
close: () => new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())))
|
| 3521 |
+
};
|
| 3522 |
+
}
|
| 3523 |
+
|
| 3524 |
function readStoredResponseJson(idempotencyKey: string): string {
|
| 3525 |
const db = new Database(path.join(tempDir, 'agent.sqlite'), { readonly: true });
|
| 3526 |
try {
|
src/app/api/deploy-marker/route.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import { NextResponse } from 'next/server';
|
| 2 |
|
| 3 |
-
const deployMarker = {"schema_version":1,"local_sha":"
|
| 4 |
|
| 5 |
export const dynamic = 'force-dynamic';
|
| 6 |
|
|
|
|
| 1 |
import { NextResponse } from 'next/server';
|
| 2 |
|
| 3 |
+
const deployMarker = {"schema_version":1,"local_sha":"bf62eed15cedbc56fb8df227f2d76273004d494e","created_at":"2026-06-26T09:26:30.691Z","deploy_id":"5b30e427-0d60-4ce4-b6ce-667ada74c8c0"} as const;
|
| 4 |
|
| 5 |
export const dynamic = 'force-dynamic';
|
| 6 |
|
src/lib/agent-image-service.ts
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
| 45 |
type ChannelFailureReport,
|
| 46 |
describeChannelFailure,
|
| 47 |
isChannelFailure,
|
|
|
|
| 48 |
isCredentialFailure,
|
| 49 |
resolveEffectiveCredential
|
| 50 |
} from './channel-router';
|
|
@@ -1067,6 +1068,20 @@ function reportServerCredentialFailure(
|
|
| 1067 |
): ChannelFailureReport | undefined {
|
| 1068 |
const serverChannelRouter = getServerChannelState().router;
|
| 1069 |
if (!credential || !serverChannelRouter) return undefined;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1070 |
if (isChannelFailure(error)) {
|
| 1071 |
const reason = {
|
| 1072 |
...describeChannelFailure(error, 'channel'),
|
|
|
|
| 45 |
type ChannelFailureReport,
|
| 46 |
describeChannelFailure,
|
| 47 |
isChannelFailure,
|
| 48 |
+
isChannelRequestModeFailure,
|
| 49 |
isCredentialFailure,
|
| 50 |
resolveEffectiveCredential
|
| 51 |
} from './channel-router';
|
|
|
|
| 1068 |
): ChannelFailureReport | undefined {
|
| 1069 |
const serverChannelRouter = getServerChannelState().router;
|
| 1070 |
if (!credential || !serverChannelRouter) return undefined;
|
| 1071 |
+
if (isChannelRequestModeFailure(error, requestMode)) {
|
| 1072 |
+
const reason = {
|
| 1073 |
+
...describeChannelFailure(error, 'channel'),
|
| 1074 |
+
...(requestMode ? { requestMode } : {})
|
| 1075 |
+
};
|
| 1076 |
+
const report = serverChannelRouter.reportFailure(credential, { scope: 'channel', requestMode, reason });
|
| 1077 |
+
appLogger.warn(
|
| 1078 |
+
report.cooldownApplied
|
| 1079 |
+
? `Temporarily cooling down API channel request mode: ${credential.channelId}/${requestMode}`
|
| 1080 |
+
: `Recording API channel request mode failure without cooldown: ${credential.channelId}/${requestMode}`,
|
| 1081 |
+
reason
|
| 1082 |
+
);
|
| 1083 |
+
return report;
|
| 1084 |
+
}
|
| 1085 |
if (isChannelFailure(error)) {
|
| 1086 |
const reason = {
|
| 1087 |
...describeChannelFailure(error, 'channel'),
|
src/lib/api-error-response.test.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
| 7 |
toTerminalAgentErrorBody
|
| 8 |
} from './api-error-response';
|
| 9 |
import { RequestValidationError } from './image-request-utils';
|
|
|
|
| 10 |
import assert from 'node:assert/strict';
|
| 11 |
import { describe, it } from 'node:test';
|
| 12 |
|
|
@@ -76,6 +77,23 @@ describe('normalizeAgentError', () => {
|
|
| 76 |
assert.equal(error.diagnostics?.channel_cooldown_scope, 'channel');
|
| 77 |
});
|
| 78 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
it('classifies transport failures into machine-readable kinds', () => {
|
| 80 |
assert.equal(
|
| 81 |
normalizeAgentError(Object.assign(new Error('fetch failed'), { code: 'ENOTFOUND' })).diagnostics
|
|
|
|
| 7 |
toTerminalAgentErrorBody
|
| 8 |
} from './api-error-response';
|
| 9 |
import { RequestValidationError } from './image-request-utils';
|
| 10 |
+
import { type ChannelRequestMode } from './channel-request-mode';
|
| 11 |
import assert from 'node:assert/strict';
|
| 12 |
import { describe, it } from 'node:test';
|
| 13 |
|
|
|
|
| 77 |
assert.equal(error.diagnostics?.channel_cooldown_scope, 'channel');
|
| 78 |
});
|
| 79 |
|
| 80 |
+
it('maps Responses image generation disabled 403s to unavailable channel failures', () => {
|
| 81 |
+
const error = normalizeAgentError(
|
| 82 |
+
Object.assign(new Error('status_code=403, Image generation is not enabled for this group'), {
|
| 83 |
+
status: 403
|
| 84 |
+
}),
|
| 85 |
+
{
|
| 86 |
+
channel_request_mode: 'responses-sse' as ChannelRequestMode
|
| 87 |
+
}
|
| 88 |
+
);
|
| 89 |
+
|
| 90 |
+
assert.equal(error.code, 'upstream_unavailable');
|
| 91 |
+
assert.equal(error.status, 502);
|
| 92 |
+
assert.equal(error.retryable, true);
|
| 93 |
+
assert.equal(error.upstreamStatus, 403);
|
| 94 |
+
assert.equal(error.diagnostics?.channel_cooldown_scope, 'channel');
|
| 95 |
+
});
|
| 96 |
+
|
| 97 |
it('classifies transport failures into machine-readable kinds', () => {
|
| 98 |
assert.equal(
|
| 99 |
normalizeAgentError(Object.assign(new Error('fetch failed'), { code: 'ENOTFOUND' })).diagnostics
|
src/lib/api-error-response.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import { RequestValidationError } from './image-request-utils';
|
| 2 |
-
import { isChannelFailure } from './channel-router';
|
| 3 |
import { ChannelCapacityQueueError } from './channel-capacity-queue';
|
| 4 |
import {
|
| 5 |
CHANNEL_REQUEST_MODES,
|
|
@@ -469,6 +469,23 @@ export function normalizeAgentError(error: unknown, diagnostics: AgentErrorDiagn
|
|
| 469 |
|
| 470 |
const status = readNumberField(error, 'status') ?? readNumberField(error, 'statusCode');
|
| 471 |
const message = error instanceof Error ? error.message : (readStringField(error, 'message') ?? '发生未知错误。');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 472 |
if (error instanceof ChannelCapacityQueueError) {
|
| 473 |
const retryAfterSeconds = Math.max(1, Math.ceil(Number(error.details.max_wait_ms ?? 30_000) / 1000));
|
| 474 |
return new AgentApiError({
|
|
|
|
| 1 |
import { RequestValidationError } from './image-request-utils';
|
| 2 |
+
import { isChannelFailure, isChannelRequestModeFailure } from './channel-router';
|
| 3 |
import { ChannelCapacityQueueError } from './channel-capacity-queue';
|
| 4 |
import {
|
| 5 |
CHANNEL_REQUEST_MODES,
|
|
|
|
| 469 |
|
| 470 |
const status = readNumberField(error, 'status') ?? readNumberField(error, 'statusCode');
|
| 471 |
const message = error instanceof Error ? error.message : (readStringField(error, 'message') ?? '发生未知错误。');
|
| 472 |
+
if (isChannelRequestModeFailure(error, diagnostics.channel_request_mode)) {
|
| 473 |
+
const retryAfterSeconds = readRetryAfterSeconds(error) ?? 15;
|
| 474 |
+
return new AgentApiError({
|
| 475 |
+
code: 'upstream_unavailable',
|
| 476 |
+
message,
|
| 477 |
+
status: 502,
|
| 478 |
+
retryable: true,
|
| 479 |
+
upstreamStatus: 403,
|
| 480 |
+
retryAfterSeconds,
|
| 481 |
+
diagnostics: buildDiagnostics(error, {
|
| 482 |
+
...diagnostics,
|
| 483 |
+
upstreamStatus: 403,
|
| 484 |
+
retryAfterSeconds,
|
| 485 |
+
channel_cooldown_scope: 'channel'
|
| 486 |
+
})
|
| 487 |
+
});
|
| 488 |
+
}
|
| 489 |
if (error instanceof ChannelCapacityQueueError) {
|
| 490 |
const retryAfterSeconds = Math.max(1, Math.ceil(Number(error.details.max_wait_ms ?? 30_000) / 1000));
|
| 491 |
return new AgentApiError({
|
src/lib/channel-router.test.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
| 3 |
describeChannelFailure,
|
| 4 |
getChannelPoolSummary,
|
| 5 |
isChannelFailure,
|
|
|
|
| 6 |
isCredentialFailure,
|
| 7 |
parseChannelPoolConfig,
|
| 8 |
resolveEffectiveCredential,
|
|
@@ -1559,6 +1560,35 @@ describe('isCredentialFailure', () => {
|
|
| 1559 |
});
|
| 1560 |
});
|
| 1561 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1562 |
describe('isChannelFailure', () => {
|
| 1563 |
it('treats upstream 5xx and CDN timeout statuses as channel failures', () => {
|
| 1564 |
[500, 502, 503, 504, 520, 522, 523, 524].forEach((status) => {
|
|
|
|
| 3 |
describeChannelFailure,
|
| 4 |
getChannelPoolSummary,
|
| 5 |
isChannelFailure,
|
| 6 |
+
isChannelRequestModeFailure,
|
| 7 |
isCredentialFailure,
|
| 8 |
parseChannelPoolConfig,
|
| 9 |
resolveEffectiveCredential,
|
|
|
|
| 1560 |
});
|
| 1561 |
});
|
| 1562 |
|
| 1563 |
+
describe('isChannelRequestModeFailure', () => {
|
| 1564 |
+
it('treats Responses image_generation-disabled 403 as a request-mode failure', () => {
|
| 1565 |
+
assert.equal(
|
| 1566 |
+
isChannelRequestModeFailure(
|
| 1567 |
+
new Error('status_code=403, Image generation is not enabled for this group'),
|
| 1568 |
+
'responses-sse'
|
| 1569 |
+
),
|
| 1570 |
+
true
|
| 1571 |
+
);
|
| 1572 |
+
assert.equal(
|
| 1573 |
+
isChannelRequestModeFailure(
|
| 1574 |
+
{ status: 403, error: { message: 'Image generation is not enabled for this group' } },
|
| 1575 |
+
'responses-non-stream'
|
| 1576 |
+
),
|
| 1577 |
+
true
|
| 1578 |
+
);
|
| 1579 |
+
});
|
| 1580 |
+
|
| 1581 |
+
it('does not apply the Responses image_generation-disabled rule to Images API modes', () => {
|
| 1582 |
+
assert.equal(
|
| 1583 |
+
isChannelRequestModeFailure(
|
| 1584 |
+
{ status: 403, message: 'Image generation is not enabled for this group' },
|
| 1585 |
+
'images-sse'
|
| 1586 |
+
),
|
| 1587 |
+
false
|
| 1588 |
+
);
|
| 1589 |
+
});
|
| 1590 |
+
});
|
| 1591 |
+
|
| 1592 |
describe('isChannelFailure', () => {
|
| 1593 |
it('treats upstream 5xx and CDN timeout statuses as channel failures', () => {
|
| 1594 |
[500, 502, 503, 504, 520, 522, 523, 524].forEach((status) => {
|
src/lib/channel-router.ts
CHANGED
|
@@ -755,6 +755,18 @@ export function isCredentialFailure(error: unknown): boolean {
|
|
| 755 |
);
|
| 756 |
}
|
| 757 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 758 |
export function isChannelFailure(error: unknown): boolean {
|
| 759 |
if (error instanceof ChannelCapacityQueueError) {
|
| 760 |
return false;
|
|
@@ -1044,7 +1056,7 @@ function readErrorNumber(error: unknown, fieldName: string): number | undefined
|
|
| 1044 |
}
|
| 1045 |
|
| 1046 |
function readStatusField(error: unknown): { status?: number } {
|
| 1047 |
-
const value =
|
| 1048 |
return value === undefined ? {} : { status: value };
|
| 1049 |
}
|
| 1050 |
|
|
@@ -1090,6 +1102,39 @@ function readNestedErrorNumber(error: unknown, fieldName: string): number | unde
|
|
| 1090 |
return readErrorNumber((error as { error?: unknown }).error, fieldName);
|
| 1091 |
}
|
| 1092 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1093 |
function isConnectionFailure(error: unknown): boolean {
|
| 1094 |
const name = readErrorString(error, 'name') || readConstructorName(error);
|
| 1095 |
if (name === 'APIConnectionError' || name === 'APIConnectionTimeoutError') {
|
|
|
|
| 755 |
);
|
| 756 |
}
|
| 757 |
|
| 758 |
+
export function isChannelRequestModeFailure(error: unknown, requestMode?: ChannelRequestMode): boolean {
|
| 759 |
+
if (!requestMode || !requestMode.startsWith('responses-')) {
|
| 760 |
+
return false;
|
| 761 |
+
}
|
| 762 |
+
const status = readErrorStatusCode(error);
|
| 763 |
+
if (status !== 403) {
|
| 764 |
+
return false;
|
| 765 |
+
}
|
| 766 |
+
const message = readErrorMessage(error);
|
| 767 |
+
return /image generation is not enabled for this group/i.test(message);
|
| 768 |
+
}
|
| 769 |
+
|
| 770 |
export function isChannelFailure(error: unknown): boolean {
|
| 771 |
if (error instanceof ChannelCapacityQueueError) {
|
| 772 |
return false;
|
|
|
|
| 1056 |
}
|
| 1057 |
|
| 1058 |
function readStatusField(error: unknown): { status?: number } {
|
| 1059 |
+
const value = readErrorStatusCode(error);
|
| 1060 |
return value === undefined ? {} : { status: value };
|
| 1061 |
}
|
| 1062 |
|
|
|
|
| 1102 |
return readErrorNumber((error as { error?: unknown }).error, fieldName);
|
| 1103 |
}
|
| 1104 |
|
| 1105 |
+
function readErrorStatusCode(error: unknown): number | undefined {
|
| 1106 |
+
return (
|
| 1107 |
+
readErrorNumber(error, 'status') ??
|
| 1108 |
+
readErrorNumber(error, 'statusCode') ??
|
| 1109 |
+
readErrorNumber(error, 'status_code') ??
|
| 1110 |
+
readNestedErrorNumber(error, 'status') ??
|
| 1111 |
+
readNestedErrorNumber(error, 'statusCode') ??
|
| 1112 |
+
readNestedErrorNumber(error, 'status_code') ??
|
| 1113 |
+
readStatusCodeFromMessage(readErrorMessage(error))
|
| 1114 |
+
);
|
| 1115 |
+
}
|
| 1116 |
+
|
| 1117 |
+
function readErrorMessage(error: unknown): string {
|
| 1118 |
+
if (typeof error === 'string') return error;
|
| 1119 |
+
return [
|
| 1120 |
+
readErrorString(error, 'message'),
|
| 1121 |
+
readNestedErrorString(error, 'message'),
|
| 1122 |
+
readErrorString(error, 'detail'),
|
| 1123 |
+
readNestedErrorString(error, 'detail'),
|
| 1124 |
+
readErrorString(error, 'raw'),
|
| 1125 |
+
readNestedErrorString(error, 'raw')
|
| 1126 |
+
]
|
| 1127 |
+
.filter((value): value is string => Boolean(value))
|
| 1128 |
+
.join('\n');
|
| 1129 |
+
}
|
| 1130 |
+
|
| 1131 |
+
function readStatusCodeFromMessage(message: string): number | undefined {
|
| 1132 |
+
const match = /\bstatus(?:_code)?\s*=\s*(\d{3})\b/i.exec(message);
|
| 1133 |
+
if (!match) return undefined;
|
| 1134 |
+
const status = Number(match[1]);
|
| 1135 |
+
return Number.isInteger(status) ? status : undefined;
|
| 1136 |
+
}
|
| 1137 |
+
|
| 1138 |
function isConnectionFailure(error: unknown): boolean {
|
| 1139 |
const name = readErrorString(error, 'name') || readConstructorName(error);
|
| 1140 |
if (name === 'APIConnectionError' || name === 'APIConnectionTimeoutError') {
|
src/lib/image-route-support.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
| 4 |
type ChannelCredential,
|
| 5 |
describeChannelFailure,
|
| 6 |
isChannelFailure,
|
|
|
|
| 7 |
isCredentialFailure
|
| 8 |
} from './channel-router';
|
| 9 |
import type { ChannelRequestMode } from './channel-request-mode';
|
|
@@ -52,6 +53,20 @@ export function reportServerCredentialFailure(
|
|
| 52 |
) {
|
| 53 |
const serverChannelRouter = getServerChannelState().router;
|
| 54 |
if (!credential || !serverChannelRouter) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
if (isChannelFailure(error)) {
|
| 56 |
const reason = {
|
| 57 |
...describeChannelFailure(error, 'channel'),
|
|
|
|
| 4 |
type ChannelCredential,
|
| 5 |
describeChannelFailure,
|
| 6 |
isChannelFailure,
|
| 7 |
+
isChannelRequestModeFailure,
|
| 8 |
isCredentialFailure
|
| 9 |
} from './channel-router';
|
| 10 |
import type { ChannelRequestMode } from './channel-request-mode';
|
|
|
|
| 53 |
) {
|
| 54 |
const serverChannelRouter = getServerChannelState().router;
|
| 55 |
if (!credential || !serverChannelRouter) return;
|
| 56 |
+
if (isChannelRequestModeFailure(error, requestMode)) {
|
| 57 |
+
const reason = {
|
| 58 |
+
...describeChannelFailure(error, 'channel'),
|
| 59 |
+
...(requestMode ? { requestMode } : {})
|
| 60 |
+
};
|
| 61 |
+
const report = serverChannelRouter.reportFailure(credential, { scope: 'channel', requestMode, reason });
|
| 62 |
+
appLogger.warn(
|
| 63 |
+
report.cooldownApplied
|
| 64 |
+
? `暂时冷却 API 渠道请求方式:${credential.channelId}/${requestMode}`
|
| 65 |
+
: `记录 API 渠道请求方式失败,未启用冷却:${credential.channelId}/${requestMode}`,
|
| 66 |
+
reason
|
| 67 |
+
);
|
| 68 |
+
return;
|
| 69 |
+
}
|
| 70 |
if (isChannelFailure(error)) {
|
| 71 |
const reason = {
|
| 72 |
...describeChannelFailure(error, 'channel'),
|