fix: recover Robot configuration loading

This commit is contained in:
2026-08-15 20:05:16 +08:00
parent a4050f0a65
commit 1bcd51964d
5 changed files with 238 additions and 19 deletions

View File

@@ -172,6 +172,42 @@ async function readBoundedResponse(response: Response): Promise<unknown> {
}
}
type TimedResponse = {
response: Response;
signal: AbortSignal;
finish: () => void;
};
async function readTimedResponse(call: TimedResponse): Promise<unknown> {
try {
return await readBoundedResponse(call.response);
} catch (error) {
if (call.signal.aborted) {
throw new SafeRouteError(504, 'AI_HARDWARE_TIMEOUT', 'AI hardware service timed out', true);
}
throw error;
} finally {
call.finish();
}
}
async function readSafeErrorPayload(call: TimedResponse): Promise<unknown> {
try {
return await readTimedResponse(call);
} catch (error) {
if (error instanceof SafeRouteError && error.code === 'AI_HARDWARE_TIMEOUT') throw error;
return null;
}
}
async function cancelTimedResponse(call: TimedResponse): Promise<void> {
try {
await call.response.body?.cancel().catch(() => undefined);
} finally {
call.finish();
}
}
function ensureExactKeys(body: Record<string, unknown>, allowed: Set<string>): void {
if (Object.keys(body).some((key) => !allowed.has(key))) {
throw new SafeRouteError(400, 'AI_HARDWARE_INVALID_REQUEST', 'Invalid AI hardware request');
@@ -431,11 +467,17 @@ export function createAiHardwareRouteHandler(dependencies: AiHardwareRouteDepend
return true;
}
const idempotencyKey = operationId ? `makelore-${operationId}` : undefined;
const call = async (accessToken: string): Promise<Response> => {
const call = async (accessToken: string): Promise<TimedResponse> => {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
let finished = false;
const finish = () => {
if (finished) return;
finished = true;
clearTimeout(timer);
};
try {
return await fetchImpl(`${apiBaseUrl}${upstreamPath}`, {
const response = await fetchImpl(`${apiBaseUrl}${upstreamPath}`, {
method,
headers: {
Accept: 'application/json',
@@ -448,24 +490,26 @@ export function createAiHardwareRouteHandler(dependencies: AiHardwareRouteDepend
signal: controller.signal,
redirect: 'manual',
});
return { response, signal: controller.signal, finish };
} catch (error) {
finish();
if (controller.signal.aborted) {
throw new SafeRouteError(504, 'AI_HARDWARE_TIMEOUT', 'AI hardware service timed out', true);
}
throw error;
} finally {
clearTimeout(timer);
}
};
let currentToken = token;
let response = await call(currentToken);
let activeCall = await call(currentToken);
let response = activeCall.response;
if (response.status === 401) {
await response.body?.cancel().catch(() => undefined);
await cancelTimedResponse(activeCall);
const refreshed = await getAccessToken({ fetchImpl, forceRefresh: true });
if (refreshed) {
currentToken = refreshed;
response = await call(currentToken);
activeCall = await call(currentToken);
response = activeCall.response;
}
}
@@ -473,11 +517,12 @@ export function createAiHardwareRouteHandler(dependencies: AiHardwareRouteDepend
if (response.status === 409 && operationId) {
const retryDelay = retryAfterSeconds(response, MAX_RETRY_AFTER_SECONDS);
if (retryDelay !== undefined) {
cachedErrorPayload = isJsonContentType(response) ? await readBoundedResponse(response).catch(() => null) : null;
cachedErrorPayload = isJsonContentType(response) ? await readSafeErrorPayload(activeCall) : null;
const safe = safeUpstreamError(cachedErrorPayload, response.status);
if (safe.code === 'ai_hardware_operation_in_progress') {
await new Promise<void>((resolve) => setTimeout(resolve, retryDelay * 1000));
response = await call(currentToken);
activeCall = await call(currentToken);
response = activeCall.response;
cachedErrorPayload = undefined;
}
}
@@ -486,14 +531,14 @@ export function createAiHardwareRouteHandler(dependencies: AiHardwareRouteDepend
if (!response.ok) {
const status = response.status;
if (status === 404 && upstreamPath === UPSTREAM_ROOT) {
await response.body?.cancel().catch(() => undefined);
await cancelTimedResponse(activeCall);
sendFailure(res, 200, 404, 'AI_HARDWARE_DISABLED', 'AI hardware module is not enabled', false);
return true;
}
const retryAfter = retryAfterSeconds(response);
const payload = cachedErrorPayload ?? (isJsonContentType(response)
? await readBoundedResponse(response).catch(() => null)
: (await response.body?.cancel().catch(() => undefined), null));
? await readSafeErrorPayload(activeCall)
: (await cancelTimedResponse(activeCall), null));
const safe = safeUpstreamError(payload, status);
sendFailure(
res,
@@ -509,11 +554,17 @@ export function createAiHardwareRouteHandler(dependencies: AiHardwareRouteDepend
}
if (response.status !== expectedStatus || !isJsonContentType(response)) {
await response.body?.cancel().catch(() => undefined);
await cancelTimedResponse(activeCall);
throw new SafeRouteError(502, 'AI_HARDWARE_INVALID_RESPONSE', 'AI hardware service returned an invalid response', true);
}
const revision = requireEtag ? strongRevisionFrom(response) : undefined;
const projected = project(await readBoundedResponse(response));
let revision: number | undefined;
try {
revision = requireEtag ? strongRevisionFrom(response) : undefined;
} catch (error) {
await cancelTimedResponse(activeCall);
throw error;
}
const projected = project(await readTimedResponse(activeCall));
if (!projected) throw new SafeRouteError(502, 'AI_HARDWARE_INVALID_RESPONSE', 'AI hardware service returned an invalid response', true);
if (revision !== undefined) {
const dtoRevision = 'config_revision' in projected