加入手动完美像素入口;像素艺术勾选时自动补提示词约束 (#124)
Co-authored-by: 段舒康 <kdletters@qq.com> Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/124 Reviewed-by: 段舒康 <kdletters@qq.com> Co-authored-by: Linghong <ink29535@proton.me> Co-committed-by: Linghong <ink29535@proton.me>
This commit was merged in pull request #124.
This commit is contained in:
+167
-14
@@ -49,6 +49,12 @@ export type ApiRequestOptions = {
|
||||
requestId?: string;
|
||||
};
|
||||
|
||||
export type ApiJsonRequestOptions = ApiRequestOptions & {
|
||||
// 从 requestJson 入口起覆盖鉴权、重试、业务请求和响应体读取的绝对截止时间。
|
||||
// 未传时保持既有 timeoutMs 仅约束单次业务 fetch 的语义。
|
||||
deadlineAt?: number;
|
||||
};
|
||||
|
||||
export const BACKGROUND_AUTH_REQUEST_OPTIONS = {
|
||||
authImpact: 'local',
|
||||
skipRefresh: true,
|
||||
@@ -317,6 +323,93 @@ function composeAbortSignal(
|
||||
};
|
||||
}
|
||||
|
||||
function composeAbsoluteDeadlineSignal(
|
||||
signal: AbortSignal | undefined,
|
||||
deadlineAt: number | undefined,
|
||||
) {
|
||||
const hasDeadline =
|
||||
typeof deadlineAt === 'number' && Number.isFinite(deadlineAt);
|
||||
if (!hasDeadline) {
|
||||
return {
|
||||
signal,
|
||||
hasDeadline: false,
|
||||
cleanup: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const remainingMs = Math.max(0, deadlineAt - Date.now());
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||
const cleanup = () => {
|
||||
if (timeoutId !== undefined) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
signal?.removeEventListener('abort', onAbort);
|
||||
};
|
||||
const onAbort = () => {
|
||||
controller.abort(signal?.reason ?? createAbortError());
|
||||
};
|
||||
|
||||
if (signal?.aborted) {
|
||||
controller.abort(signal.reason ?? createAbortError());
|
||||
} else {
|
||||
signal?.addEventListener('abort', onAbort, { once: true });
|
||||
if (remainingMs <= 0) {
|
||||
controller.abort(createTimeoutError(0));
|
||||
} else {
|
||||
timeoutId = setTimeout(() => {
|
||||
controller.abort(createTimeoutError(remainingMs));
|
||||
}, remainingMs);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
signal: controller.signal,
|
||||
hasDeadline: true,
|
||||
cleanup,
|
||||
};
|
||||
}
|
||||
|
||||
function awaitWithAbortSignal<T>(
|
||||
work: Promise<T>,
|
||||
signal?: AbortSignal,
|
||||
): Promise<T> {
|
||||
if (!signal) {
|
||||
return work;
|
||||
}
|
||||
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
let settled = false;
|
||||
const cleanup = () => {
|
||||
signal.removeEventListener('abort', onAbort);
|
||||
};
|
||||
const settle = (callback: () => void) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
cleanup();
|
||||
callback();
|
||||
};
|
||||
const onAbort = () => {
|
||||
settle(() => reject(signal.reason ?? createAbortError()));
|
||||
};
|
||||
|
||||
signal.addEventListener('abort', onAbort, { once: true });
|
||||
work.then(
|
||||
(value) => {
|
||||
settle(() => resolve(value));
|
||||
},
|
||||
(error: unknown) => {
|
||||
settle(() => reject(error));
|
||||
},
|
||||
);
|
||||
if (signal.aborted) {
|
||||
onAbort();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForRetry(ms: number, signal?: AbortSignal) {
|
||||
if (ms <= 0) {
|
||||
return;
|
||||
@@ -451,6 +544,31 @@ function resolveAuthFailurePolicy(
|
||||
};
|
||||
}
|
||||
|
||||
// 中文注释:这些错误体由 Pingora 网关自己合成,应用层从未产生过——它们只有 code / message,
|
||||
// 没有 details,所以任何「有 details 标记就是已知结果」的判定对它们都不成立。
|
||||
// 只收上游/代理这三类:请求可能已经到达 api-server 并被执行完,只是响应没回到客户端,
|
||||
// 属于未知结果。GATEWAY_RATE_LIMITED / GATEWAY_CONCURRENCY_LIMITED / PAYLOAD_TOO_LARGE
|
||||
// 是在网关就被拒、根本没到应用,属于确定失败,收进来会让普通节流也弹出「请核对素材库」,
|
||||
// 变成反向谎报。
|
||||
const GATEWAY_UNKNOWN_OUTCOME_ERROR_CODES = new Set([
|
||||
'GATEWAY_UPSTREAM_ERROR',
|
||||
'GATEWAY_UPSTREAM_TIMEOUT',
|
||||
'GATEWAY_PROXY_ERROR',
|
||||
]);
|
||||
|
||||
/**
|
||||
* 中文注释:该错误是否由网关合成、因而无法证明应用层没有执行过这次请求。
|
||||
*
|
||||
* 有副作用的写接口在判断「结果是否已知」时必须把它算作未知:拿到 HTTP 响应不等于服务端
|
||||
* 明确表态过,网关超时或上游断连时 api-server 可能已经完成了持久化。
|
||||
*/
|
||||
export function isGatewayUnknownOutcomeError(error: unknown): boolean {
|
||||
return (
|
||||
error instanceof ApiClientError &&
|
||||
GATEWAY_UNKNOWN_OUTCOME_ERROR_CODES.has(error.code)
|
||||
);
|
||||
}
|
||||
|
||||
export class ApiClientError extends Error {
|
||||
status: number;
|
||||
code: string;
|
||||
@@ -677,14 +795,17 @@ export async function fetchWithApiAuth(
|
||||
try {
|
||||
// 受保护请求在本地 access token 缺失时,先尝试用 refresh cookie 静默补票,
|
||||
// 避免把后端原始 “缺少 Bearer Token” 直接暴露给业务 UI。
|
||||
await ensureStoredAccessToken();
|
||||
await awaitWithAbortSignal(ensureStoredAccessToken(), requestSignal);
|
||||
requestHeaders = withAuthorizationHeaders(init.headers, options);
|
||||
requestHeaders[REQUEST_ID_HEADER] = requestId;
|
||||
hasAuthHeader = Boolean(
|
||||
requestHeaders.Authorization?.trim() ||
|
||||
requestHeaders.authorization?.trim(),
|
||||
);
|
||||
} catch {
|
||||
} catch (error) {
|
||||
if (requestSignal?.aborted) {
|
||||
throw requestSignal.reason ?? error;
|
||||
}
|
||||
// 补票失败时继续走原始请求,让调用方按真实 401 分支处理。
|
||||
}
|
||||
}
|
||||
@@ -710,13 +831,16 @@ export async function fetchWithApiAuth(
|
||||
!refreshAttempted
|
||||
) {
|
||||
try {
|
||||
await refreshAccessToken();
|
||||
await awaitWithAbortSignal(refreshAccessToken(), requestSignal);
|
||||
refreshAttempted = true;
|
||||
// refresh 成功只代表 access token 已补票成功,
|
||||
// 不能把当前业务请求的首次 401 直接放大成全局鉴权变更,
|
||||
// 否则像 Puzzle works 这类受保护列表会把单接口失败放大成整个平台重复 hydrate。
|
||||
continue;
|
||||
} catch (refreshError) {
|
||||
if (requestSignal?.aborted) {
|
||||
throw requestSignal.reason ?? refreshError;
|
||||
}
|
||||
const shouldClearAuth =
|
||||
hasAuthHeader &&
|
||||
authFailurePolicy.clearAuthOnUnauthorized &&
|
||||
@@ -746,6 +870,9 @@ export async function fetchWithApiAuth(
|
||||
return response;
|
||||
}
|
||||
} catch (error) {
|
||||
if (requestSignal?.aborted) {
|
||||
throw requestSignal.reason ?? error;
|
||||
}
|
||||
if (!shouldRetryError(error, attempt, retry)) {
|
||||
throw error;
|
||||
}
|
||||
@@ -759,8 +886,9 @@ export async function fetchWithApiAuth(
|
||||
async function buildApiClientError(
|
||||
response: Response,
|
||||
fallbackMessage: string,
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
const responseText = await response.text();
|
||||
const responseText = await awaitWithAbortSignal(response.text(), signal);
|
||||
const parsedError = parseApiErrorShape(responseText);
|
||||
const requestId =
|
||||
parsedError?.meta.requestId ??
|
||||
@@ -795,17 +923,42 @@ export async function requestJson<T>(
|
||||
url: string,
|
||||
init: RequestInit,
|
||||
fallbackMessage: string,
|
||||
options: ApiRequestOptions = {},
|
||||
options: ApiJsonRequestOptions = {},
|
||||
): Promise<T> {
|
||||
const response = await fetchWithApiAuth(url, init, options);
|
||||
const lifecycle = composeAbsoluteDeadlineSignal(
|
||||
init.signal ?? undefined,
|
||||
options.deadlineAt,
|
||||
);
|
||||
const requestOptions = lifecycle.hasDeadline
|
||||
? { ...options, timeoutMs: undefined }
|
||||
: options;
|
||||
const requestInit = lifecycle.signal
|
||||
? { ...init, signal: lifecycle.signal }
|
||||
: init;
|
||||
|
||||
if (!response.ok) {
|
||||
throw await buildApiClientError(response, fallbackMessage);
|
||||
try {
|
||||
const response = await fetchWithApiAuth(url, requestInit, requestOptions);
|
||||
|
||||
if (!response.ok) {
|
||||
throw await buildApiClientError(
|
||||
response,
|
||||
fallbackMessage,
|
||||
lifecycle.signal,
|
||||
);
|
||||
}
|
||||
|
||||
const responseText = await awaitWithAbortSignal(
|
||||
response.text(),
|
||||
lifecycle.signal,
|
||||
);
|
||||
if (lifecycle.signal?.aborted) {
|
||||
throw lifecycle.signal.reason ?? createAbortError();
|
||||
}
|
||||
|
||||
return responseText
|
||||
? unwrapApiResponse<T>(JSON.parse(responseText) as T)
|
||||
: (null as T);
|
||||
} finally {
|
||||
lifecycle.cleanup();
|
||||
}
|
||||
|
||||
const responseText = await response.text();
|
||||
|
||||
return responseText
|
||||
? unwrapApiResponse<T>(JSON.parse(responseText) as T)
|
||||
: (null as T);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user