Enrich external API failure audit metadata

This commit is contained in:
kdletters
2026-05-28 15:42:46 +08:00
parent 2cd2b9704b
commit f1fb92aa29
40 changed files with 315 additions and 152 deletions

View File

@@ -3,6 +3,7 @@ import {
postApiJson,
} from '../../editor/shared/editorApiClient';
import {
appendApiErrorRequestId,
fetchJson,
parseApiErrorMessage,
} from '../../editor/shared/jsonClient';
@@ -265,7 +266,10 @@ export async function putCharacterRoleAssetWorkflow(
if (!response.ok) {
throw new Error(
parseApiErrorMessage(responseText, '保存角色资产工坊缓存失败'),
appendApiErrorRequestId(
parseApiErrorMessage(responseText, '保存角色资产工坊缓存失败'),
response.headers.get('x-request-id'),
),
);
}

View File

@@ -1,4 +1,9 @@
import { fetchJson, parseApiErrorMessage, saveJsonObject } from './jsonClient';
import {
appendApiErrorRequestId,
fetchJson,
parseApiErrorMessage,
saveJsonObject,
} from './jsonClient';
export const EDITOR_API_BASE_PATH = '/api/editor';
export const ASSETS_API_BASE_PATH = '/api/assets';
@@ -69,7 +74,7 @@ export async function postApiJson<T>(
const responseText = await response.text();
if (!response.ok) {
throw new Error(parseApiErrorMessage(responseText, fallbackMessage));
throw new Error(appendApiErrorRequestId(parseApiErrorMessage(responseText, fallbackMessage), response.headers.get('x-request-id')));
}
return responseText ? (JSON.parse(responseText) as T) : ({} as T);

View File

@@ -1,6 +1,7 @@
import {
API_RESPONSE_ENVELOPE_HEADER,
API_RESPONSE_ENVELOPE_VERSION,
appendApiErrorRequestId,
parseApiErrorMessage,
unwrapApiResponse,
} from '../../../packages/shared/src/http';
@@ -17,7 +18,15 @@ export async function fetchJson<T>(
const responseText = await response.text();
if (!response.ok) {
throw new Error(parseApiErrorMessage(responseText, `${fallbackMessage}: ${response.status}`));
throw new Error(
appendApiErrorRequestId(
parseApiErrorMessage(
responseText,
`${fallbackMessage}: ${response.status}`,
),
response.headers.get('x-request-id'),
),
);
}
return responseText
@@ -41,8 +50,13 @@ export async function saveJsonObject(
const responseText = await response.text();
if (!response.ok) {
throw new Error(parseApiErrorMessage(responseText, fallbackMessage));
throw new Error(
appendApiErrorRequestId(
parseApiErrorMessage(responseText, fallbackMessage),
response.headers.get('x-request-id'),
),
);
}
}
export { parseApiErrorMessage };
export { appendApiErrorRequestId, parseApiErrorMessage };

View File

@@ -14,7 +14,10 @@ import type {
GenerateCustomWorldProfileInput,
GenerateCustomWorldProfileOptions,
} from '../../packages/shared/src/contracts/runtime';
import { parseApiErrorMessage } from '../../packages/shared/src/http';
import {
appendApiErrorRequestId,
parseApiErrorMessage,
} from '../../packages/shared/src/http';
import type {
AIResponse,
Character,
@@ -93,7 +96,12 @@ async function requestPlainTextStream(
if (!response.ok) {
const responseText = await response.text();
throw new Error(parseApiErrorMessage(responseText, '流式请求失败'));
throw new Error(
appendApiErrorRequestId(
parseApiErrorMessage(responseText, '流式请求失败'),
response.headers.get('x-request-id'),
),
);
}
if (!response.body) {
@@ -488,7 +496,12 @@ export async function streamNpcChatTurn(
if (!response.ok) {
const responseText = await response.text();
throw new Error(parseApiErrorMessage(responseText, 'NPC 聊天续写失败'));
throw new Error(
appendApiErrorRequestId(
parseApiErrorMessage(responseText, 'NPC 聊天续写失败'),
response.headers.get('x-request-id'),
),
);
}
if (!response.body) {

View File

@@ -547,6 +547,7 @@ describe('apiClient', () => {
routeVersion: 'runtime.v2',
},
});
expect((capturedError as Error).message).toContain('requestId: req-body');
});
it('uses api error details.message as ApiClientError message', async () => {

View File

@@ -671,9 +671,14 @@ async function buildApiClientError(
) {
const responseText = await response.text();
const parsedError = parseApiErrorShape(responseText);
const requestId =
parsedError?.meta.requestId ??
response.headers.get(REQUEST_ID_HEADER) ??
undefined;
const baseMessage = parseApiErrorMessage(responseText, fallbackMessage);
return new ApiClientError({
message: parseApiErrorMessage(responseText, fallbackMessage),
message: requestId ? `${baseMessage}requestId: ${requestId}` : baseMessage,
status: response.status,
code: parsedError?.code ?? `HTTP_${response.status || 0}`,
details: parsedError?.details ?? null,
@@ -682,10 +687,7 @@ async function buildApiClientError(
parsedError?.meta.apiVersion ??
response.headers.get(API_VERSION_HEADER) ??
API_VERSION,
requestId:
parsedError?.meta.requestId ??
response.headers.get(REQUEST_ID_HEADER) ??
undefined,
requestId,
routeVersion:
parsedError?.meta.routeVersion ??
response.headers.get(ROUTE_VERSION_HEADER) ??

View File

@@ -1,4 +1,7 @@
import { parseApiErrorMessage } from '../../packages/shared/src/http';
import {
appendApiErrorRequestId,
parseApiErrorMessage,
} from '../../packages/shared/src/http';
import {
ApiClientError,
BACKGROUND_AUTH_REQUEST_OPTIONS,
@@ -338,7 +341,12 @@ export async function readAssetBytes(
if (!response.ok) {
const message = await response
.text()
.then((text) => parseApiErrorMessage(text, '读取资源内容失败'))
.then((text) =>
appendApiErrorRequestId(
parseApiErrorMessage(text, '读取资源内容失败'),
response.headers.get('x-request-id'),
),
)
.catch(() => '');
throw new Error(message || '读取资源内容失败');
}

View File

@@ -1,4 +1,4 @@
import { parseApiErrorMessage } from '../../../packages/shared/src/http';
import { appendApiErrorRequestId, parseApiErrorMessage } from '../../../packages/shared/src/http';
import type { TextStreamOptions } from '../aiTypes';
import {
type ApiRetryOptions,
@@ -64,7 +64,7 @@ async function openCreationAgentSsePost(
if (!response.ok) {
const responseText = await response.text();
throw new Error(parseApiErrorMessage(responseText, fallbackMessage));
throw new Error(appendApiErrorRequestId(parseApiErrorMessage(responseText, fallbackMessage), response.headers.get('x-request-id')));
}
if (!response.body) {

View File

@@ -7,7 +7,7 @@ import type {
CreativeDraftEditStreamRequest,
StreamCreativeAgentMessageRequest,
} from '../../../packages/shared/src/contracts/creativeAgent';
import { parseApiErrorMessage } from '../../../packages/shared/src/http';
import { appendApiErrorRequestId, parseApiErrorMessage } from '../../../packages/shared/src/http';
import type { TextStreamOptions } from '../aiTypes';
import { fetchWithApiAuth, requestJson } from '../apiClient';
import {
@@ -42,7 +42,7 @@ async function openCreativeAgentSsePost(
if (!response.ok) {
const responseText = await response.text();
throw new Error(parseApiErrorMessage(responseText, fallbackMessage));
throw new Error(appendApiErrorRequestId(parseApiErrorMessage(responseText, fallbackMessage), response.headers.get('x-request-id')));
}
if (!response.body) {

View File

@@ -1,4 +1,4 @@
import { parseApiErrorMessage } from '../../../packages/shared/src/http';
import { appendApiErrorRequestId, parseApiErrorMessage } from '../../../packages/shared/src/http';
import { fetchWithApiAuth, requestJson } from '../apiClient';
export async function requestRpgCreationPostJson<T>(
@@ -32,7 +32,7 @@ export async function openRpgCreationSsePost(
if (!response.ok) {
const responseText = await response.text();
throw new Error(parseApiErrorMessage(responseText, fallbackMessage));
throw new Error(appendApiErrorRequestId(parseApiErrorMessage(responseText, fallbackMessage), response.headers.get('x-request-id')));
}
if (!response.body) {

View File

@@ -16,7 +16,7 @@ import type {
VisualNovelStartRunRequest,
VisualNovelWorksResponse,
} from '../../../packages/shared/src/contracts/visualNovel';
import { parseApiErrorMessage } from '../../../packages/shared/src/http';
import { appendApiErrorRequestId, parseApiErrorMessage } from '../../../packages/shared/src/http';
import type { TextStreamOptions } from '../aiTypes';
import {
type ApiRetryOptions,
@@ -100,7 +100,7 @@ async function openVisualNovelRuntimeSsePost(
if (!response.ok) {
const responseText = await response.text();
throw new Error(parseApiErrorMessage(responseText, fallbackMessage));
throw new Error(appendApiErrorRequestId(parseApiErrorMessage(responseText, fallbackMessage), response.headers.get('x-request-id')));
}
if (!response.body) {