修复泥点消耗刷新不及时 (#138)
refactor: - 主站复用game agent的zustand store fix: - 修复旧请求覆盖问题 - 修复账号切换问题 - 主站原本已经每 4 秒轮询 external generation 任务状态, 在这里补充触发余额刷新的时机 --------- Co-authored-by: 段舒康 <kdletters@qq.com> Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/138 Co-authored-by: 王德宇 <kvtodev@outlook.com> Co-committed-by: 王德宇 <kvtodev@outlook.com>
This commit was merged in pull request #138.
This commit is contained in:
+105
-15
@@ -114,7 +114,9 @@ function normalizeHeaders(headers?: HeadersInit) {
|
||||
|
||||
function hasHeader(headers: Record<string, string>, name: string) {
|
||||
const normalizedName = name.toLowerCase();
|
||||
return Object.keys(headers).some((key) => key.toLowerCase() === normalizedName);
|
||||
return Object.keys(headers).some(
|
||||
(key) => key.toLowerCase() === normalizedName,
|
||||
);
|
||||
}
|
||||
|
||||
function setHeaderIfMissing(
|
||||
@@ -146,7 +148,11 @@ function attachHostRuntimeHeaders(headers: Record<string, string>) {
|
||||
runtime.clientRuntime || 'wechat_mini_program',
|
||||
);
|
||||
setHeaderIfMissing(headers, CLIENT_PLATFORM_HEADER, runtime.hostPlatform);
|
||||
setHeaderIfMissing(headers, MINI_PROGRAM_ENV_HEADER, runtime.miniProgramEnv);
|
||||
setHeaderIfMissing(
|
||||
headers,
|
||||
MINI_PROGRAM_ENV_HEADER,
|
||||
runtime.miniProgramEnv,
|
||||
);
|
||||
return headers;
|
||||
}
|
||||
|
||||
@@ -171,7 +177,10 @@ function buildClientRequestId() {
|
||||
return `web-${randomId}`;
|
||||
}
|
||||
|
||||
function resolveRequestIdHeader(headers: Record<string, string>, options: ApiRequestOptions) {
|
||||
function resolveRequestIdHeader(
|
||||
headers: Record<string, string>,
|
||||
options: ApiRequestOptions,
|
||||
) {
|
||||
const explicitRequestId = options.requestId?.trim();
|
||||
const existingRequestId = Object.entries(headers).find(
|
||||
([key, value]) => key.toLowerCase() === REQUEST_ID_HEADER && value.trim(),
|
||||
@@ -630,7 +639,9 @@ export function getStoredAccessToken() {
|
||||
return window.localStorage.getItem(ACCESS_TOKEN_KEY)?.trim() || '';
|
||||
}
|
||||
|
||||
export function setStoredAccessToken(
|
||||
let authStateGeneration = 0;
|
||||
|
||||
function writeStoredAccessToken(
|
||||
token: string,
|
||||
options: {
|
||||
emit?: boolean;
|
||||
@@ -654,6 +665,20 @@ export function setStoredAccessToken(
|
||||
}
|
||||
}
|
||||
|
||||
export function setStoredAccessToken(
|
||||
token: string,
|
||||
options: {
|
||||
emit?: boolean;
|
||||
} = {},
|
||||
) {
|
||||
if (!canUseLocalStorage()) {
|
||||
return;
|
||||
}
|
||||
|
||||
authStateGeneration += 1;
|
||||
writeStoredAccessToken(token, options);
|
||||
}
|
||||
|
||||
export function clearStoredAccessToken(
|
||||
options: {
|
||||
emit?: boolean;
|
||||
@@ -663,6 +688,7 @@ export function clearStoredAccessToken(
|
||||
return;
|
||||
}
|
||||
|
||||
authStateGeneration += 1;
|
||||
const previousToken = getStoredAccessToken();
|
||||
window.localStorage.removeItem(ACCESS_TOKEN_KEY);
|
||||
|
||||
@@ -687,7 +713,50 @@ function withAuthorizationHeaders(
|
||||
return nextHeaders;
|
||||
}
|
||||
|
||||
let refreshAccessTokenPromise: Promise<string> | null = null;
|
||||
type AuthStateSnapshot = {
|
||||
generation: number;
|
||||
accessToken: string;
|
||||
};
|
||||
|
||||
type RefreshAccessTokenAttempt = AuthStateSnapshot & {
|
||||
promise: Promise<string>;
|
||||
};
|
||||
|
||||
class AuthStateChangedDuringRefreshError extends Error {
|
||||
constructor() {
|
||||
super('刷新期间登录状态已变化');
|
||||
this.name = 'AuthStateChangedDuringRefreshError';
|
||||
}
|
||||
}
|
||||
|
||||
let refreshAccessTokenAttempt: RefreshAccessTokenAttempt | null = null;
|
||||
|
||||
function captureAuthStateSnapshot(): AuthStateSnapshot {
|
||||
return {
|
||||
generation: authStateGeneration,
|
||||
accessToken: getStoredAccessToken(),
|
||||
};
|
||||
}
|
||||
|
||||
function isCurrentAuthState(snapshot: AuthStateSnapshot) {
|
||||
return (
|
||||
authStateGeneration === snapshot.generation &&
|
||||
getStoredAccessToken() === snapshot.accessToken
|
||||
);
|
||||
}
|
||||
|
||||
function publishRefreshedAccessToken(
|
||||
nextToken: string,
|
||||
snapshot: AuthStateSnapshot,
|
||||
) {
|
||||
if (!isCurrentAuthState(snapshot)) {
|
||||
throw new AuthStateChangedDuringRefreshError();
|
||||
}
|
||||
|
||||
// refresh 只轮换同一账号的 access token,不推进账号代际。
|
||||
// 外部登录、切号或退出通过公开 setter 推进代际,使旧 refresh 发布失效。
|
||||
writeStoredAccessToken(nextToken, { emit: false });
|
||||
}
|
||||
|
||||
function shouldClearAuthAfterRefreshFailure(error: unknown) {
|
||||
return (
|
||||
@@ -697,11 +766,16 @@ function shouldClearAuthAfterRefreshFailure(error: unknown) {
|
||||
}
|
||||
|
||||
async function refreshAccessToken() {
|
||||
if (refreshAccessTokenPromise) {
|
||||
return refreshAccessTokenPromise;
|
||||
const authStateSnapshot = captureAuthStateSnapshot();
|
||||
if (
|
||||
refreshAccessTokenAttempt &&
|
||||
refreshAccessTokenAttempt.generation === authStateSnapshot.generation &&
|
||||
refreshAccessTokenAttempt.accessToken === authStateSnapshot.accessToken
|
||||
) {
|
||||
return refreshAccessTokenAttempt.promise;
|
||||
}
|
||||
|
||||
refreshAccessTokenPromise = (async () => {
|
||||
const promise = (async () => {
|
||||
const response = await fetch('/api/auth/refresh', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
@@ -726,14 +800,21 @@ async function refreshAccessToken() {
|
||||
throw new Error('刷新登录状态失败');
|
||||
}
|
||||
|
||||
setStoredAccessToken(nextToken, { emit: false });
|
||||
publishRefreshedAccessToken(nextToken, authStateSnapshot);
|
||||
return nextToken;
|
||||
})();
|
||||
const attempt: RefreshAccessTokenAttempt = {
|
||||
...authStateSnapshot,
|
||||
promise,
|
||||
};
|
||||
refreshAccessTokenAttempt = attempt;
|
||||
|
||||
try {
|
||||
return await refreshAccessTokenPromise;
|
||||
return await promise;
|
||||
} finally {
|
||||
refreshAccessTokenPromise = null;
|
||||
if (refreshAccessTokenAttempt === attempt) {
|
||||
refreshAccessTokenAttempt = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -752,12 +833,14 @@ export async function refreshStoredAccessToken(
|
||||
clearOnFailure?: boolean;
|
||||
} = {},
|
||||
) {
|
||||
const authStateSnapshot = captureAuthStateSnapshot();
|
||||
try {
|
||||
return await refreshAccessToken();
|
||||
} catch (error) {
|
||||
if (
|
||||
options.clearOnFailure !== false &&
|
||||
shouldClearAuthAfterRefreshFailure(error)
|
||||
shouldClearAuthAfterRefreshFailure(error) &&
|
||||
isCurrentAuthState(authStateSnapshot)
|
||||
) {
|
||||
clearStoredAccessToken({ emit: false });
|
||||
}
|
||||
@@ -774,7 +857,10 @@ export async function fetchWithApiAuth(
|
||||
const retry = resolveRetryOptions(method, options.retry);
|
||||
const authFailurePolicy = resolveAuthFailurePolicy(options);
|
||||
const requestSignal = init.signal ?? undefined;
|
||||
const requestId = resolveRequestIdHeader(normalizeHeaders(init.headers), options);
|
||||
const requestId = resolveRequestIdHeader(
|
||||
normalizeHeaders(init.headers),
|
||||
options,
|
||||
);
|
||||
let attempt = 0;
|
||||
let refreshAttempted = false;
|
||||
|
||||
@@ -830,6 +916,7 @@ export async function fetchWithApiAuth(
|
||||
!authFailurePolicy.skipRefresh &&
|
||||
!refreshAttempted
|
||||
) {
|
||||
const refreshAuthStateSnapshot = captureAuthStateSnapshot();
|
||||
try {
|
||||
await awaitWithAbortSignal(refreshAccessToken(), requestSignal);
|
||||
refreshAttempted = true;
|
||||
@@ -844,7 +931,8 @@ export async function fetchWithApiAuth(
|
||||
const shouldClearAuth =
|
||||
hasAuthHeader &&
|
||||
authFailurePolicy.clearAuthOnUnauthorized &&
|
||||
shouldClearAuthAfterRefreshFailure(refreshError);
|
||||
shouldClearAuthAfterRefreshFailure(refreshError) &&
|
||||
isCurrentAuthState(refreshAuthStateSnapshot);
|
||||
if (shouldClearAuth) {
|
||||
clearStoredAccessToken({ emit: false });
|
||||
}
|
||||
@@ -897,7 +985,9 @@ async function buildApiClientError(
|
||||
const baseMessage = parseApiErrorMessage(responseText, fallbackMessage);
|
||||
|
||||
return new ApiClientError({
|
||||
message: requestId ? `${baseMessage}(requestId: ${requestId})` : baseMessage,
|
||||
message: requestId
|
||||
? `${baseMessage}(requestId: ${requestId})`
|
||||
: baseMessage,
|
||||
status: response.status,
|
||||
code: parsedError?.code ?? `HTTP_${response.status || 0}`,
|
||||
details: parsedError?.details ?? null,
|
||||
|
||||
Reference in New Issue
Block a user