6f181b36a1
生成队列按稳定请求标识去重并保持外部幂等哈希兼容 统一拒绝参考图超限并同步前端、后端、Provider 与 OpenAPI 契约 按真实归属重建生成引用并阻止直接持久化伪造来源 锁定参考图在途上传上下文并保留批量部分成功结果 关闭内部生成 POST 自动重试并补齐回归测试与项目文档 修正最新主线开发者密钥弹窗的导入排序门禁
88 lines
2.5 KiB
TypeScript
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);
|
|
}
|
|
}
|