Files
Genarrative/src/services/image-editor/editorRetryOptions.ts
T
kdletters 6f181b36a1
Project CI / Frontend tests (push) Failing after 22s
Project CI / Repository checks (push) Successful in 58s
Project CI / Backend tests (push) Successful in 3m30s
Project CI / Native shell tests (push) Successful in 12m34s
修复编辑器生成幂等与参考图契约
生成队列按稳定请求标识去重并保持外部幂等哈希兼容
统一拒绝参考图超限并同步前端、后端、Provider 与 OpenAPI 契约
按真实归属重建生成引用并阻止直接持久化伪造来源
锁定参考图在途上传上下文并保留批量部分成功结果
关闭内部生成 POST 自动重试并补齐回归测试与项目文档
修正最新主线开发者密钥弹窗的导入排序门禁
2026-08-05 23:31:16 +08:00

88 lines
2.5 KiB
TypeScript

import type { ApiRetryOptions } from '../apiClient';
const EDITOR_RETRYABLE_STATUS_CODES = [408, 425, 429, 502, 503, 504];
const EDITOR_RETRY_MAX_RETRIES = 2;
const EDITOR_RETRY_BASE_DELAY_MS = 800;
const EDITOR_RETRY_MAX_DELAY_MS = 3000;
export const EDITOR_REQUEST_RETRY_OPTIONS = {
maxRetries: EDITOR_RETRY_MAX_RETRIES,
baseDelayMs: EDITOR_RETRY_BASE_DELAY_MS,
maxDelayMs: EDITOR_RETRY_MAX_DELAY_MS,
retryUnsafeMethods: true,
retryableStatusCodes: EDITOR_RETRYABLE_STATUS_CODES,
} satisfies ApiRetryOptions;
// 生成接口在 inline 模式下会直接调用 provider;服务端尚无结果级幂等前,
// 浏览器不能自动重试 POST,否则响应丢失会重复生成与持久化。
export const EDITOR_GENERATION_REQUEST_RETRY_OPTIONS = {
maxRetries: 0,
retryUnsafeMethods: false,
} satisfies ApiRetryOptions;
type EditorDirectUploadTarget = {
host: string;
formFields: Record<string, string | null | undefined>;
};
function buildRetryDelayMs(attempt: number) {
return Math.min(
EDITOR_RETRY_MAX_DELAY_MS,
EDITOR_RETRY_BASE_DELAY_MS * Math.max(1, attempt),
);
}
function waitForRetryDelay(attempt: number) {
return new Promise((resolve) => {
setTimeout(resolve, buildRetryDelayMs(attempt));
});
}
function isRetryableEditorUploadStatus(status: number) {
return EDITOR_RETRYABLE_STATUS_CODES.includes(status);
}
function buildDirectUploadFormData(
upload: EditorDirectUploadTarget,
file: File,
) {
const formData = new FormData();
Object.entries(upload.formFields).forEach(([key, value]) => {
if (value !== null && value !== undefined) {
formData.append(key, value);
}
});
formData.append('file', file, file.name);
return formData;
}
export async function postEditorDirectUploadFile(
upload: EditorDirectUploadTarget,
file: File,
errorMessage: string,
// 中文注释:调用方超时后仅停止 await 是不够的——这一步是唯一往 OSS 写实体的动作,
// 不取消的话被放弃的上传会继续跑完并注册对象,用户重试再产生一份,留下不可见的孤儿。
signal?: AbortSignal,
) {
for (let attempt = 0; ; attempt += 1) {
const response = await fetch(upload.host, {
method: 'POST',
body: buildDirectUploadFormData(upload, file),
signal,
});
if (response.ok) {
return;
}
if (
attempt >= EDITOR_RETRY_MAX_RETRIES ||
!isRetryableEditorUploadStatus(response.status)
) {
throw new Error(errorMessage);
}
await waitForRetryDelay(attempt + 1);
}
}