1
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
clearStoredAccessToken,
|
||||
fetchWithApiAuth,
|
||||
getStoredAccessToken,
|
||||
isTimeoutError,
|
||||
requestJson,
|
||||
setStoredAccessToken,
|
||||
} from './apiClient';
|
||||
@@ -150,6 +151,68 @@ describe('apiClient', () => {
|
||||
expect(getStoredAccessToken()).toBe('fresh-token');
|
||||
});
|
||||
|
||||
it('hydrates a missing local bearer token before the first protected request', async () => {
|
||||
fetchMock
|
||||
.mockResolvedValueOnce(
|
||||
createResponseMock({
|
||||
status: 200,
|
||||
body: JSON.stringify({
|
||||
ok: true,
|
||||
data: {
|
||||
ok: true,
|
||||
token: 'fresh-token',
|
||||
},
|
||||
error: null,
|
||||
meta: {
|
||||
apiVersion: '2026-04-08',
|
||||
},
|
||||
}),
|
||||
}),
|
||||
)
|
||||
.mockResolvedValueOnce(
|
||||
createResponseMock({
|
||||
status: 200,
|
||||
body: JSON.stringify({
|
||||
ok: true,
|
||||
data: {
|
||||
value: 9,
|
||||
},
|
||||
error: null,
|
||||
meta: {
|
||||
apiVersion: '2026-04-08',
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await requestJson<{ value: number }>(
|
||||
'/api/runtime/protected',
|
||||
{ method: 'GET' },
|
||||
'读取受保护数据失败',
|
||||
);
|
||||
|
||||
expect(result).toEqual({ value: 9 });
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'/api/auth/refresh',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
}),
|
||||
);
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'/api/runtime/protected',
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer fresh-token',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(getStoredAccessToken()).toBe('fresh-token');
|
||||
});
|
||||
|
||||
it('does not emit auth change events when 401 probe requests opt into silent mode', async () => {
|
||||
fetchMock.mockResolvedValueOnce(createResponseMock({ status: 401 }));
|
||||
|
||||
@@ -185,6 +248,38 @@ describe('apiClient', () => {
|
||||
expect(getStoredAccessToken()).toBe('');
|
||||
});
|
||||
|
||||
it('keeps the refreshed token when the retried protected request is still unauthorized', async () => {
|
||||
setStoredAccessToken('expired-token', { emit: false });
|
||||
fetchMock
|
||||
.mockResolvedValueOnce(createResponseMock({ status: 401 }))
|
||||
.mockResolvedValueOnce(
|
||||
createResponseMock({
|
||||
status: 200,
|
||||
body: JSON.stringify({
|
||||
ok: true,
|
||||
data: {
|
||||
ok: true,
|
||||
token: 'fresh-token',
|
||||
},
|
||||
error: null,
|
||||
meta: {
|
||||
apiVersion: '2026-04-08',
|
||||
},
|
||||
}),
|
||||
}),
|
||||
)
|
||||
.mockResolvedValueOnce(createResponseMock({ status: 401 }));
|
||||
|
||||
const response = await fetchWithApiAuth('/api/runtime/puzzle/works', {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
expect(getStoredAccessToken()).toBe('fresh-token');
|
||||
expect(dispatchEventMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('rejects refresh responses that do not return a renewed bearer token', async () => {
|
||||
setStoredAccessToken('expired-token', { emit: false });
|
||||
fetchMock
|
||||
@@ -280,7 +375,43 @@ describe('apiClient', () => {
|
||||
expect(result).toEqual({ value: 42 });
|
||||
});
|
||||
|
||||
it('aborts requests when timeoutMs is reached', async () => {
|
||||
setStoredAccessToken('timeout-token', { emit: false });
|
||||
fetchMock.mockImplementation(
|
||||
async (_input: string, init?: RequestInit) =>
|
||||
new Promise((_resolve, reject) => {
|
||||
init?.signal?.addEventListener(
|
||||
'abort',
|
||||
() => {
|
||||
reject(init.signal?.reason);
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
let capturedError: unknown;
|
||||
try {
|
||||
await requestJson(
|
||||
'/api/runtime/protected',
|
||||
{ method: 'POST' },
|
||||
'创建会话失败',
|
||||
{
|
||||
timeoutMs: 20,
|
||||
skipRefresh: true,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
capturedError = error;
|
||||
}
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(isTimeoutError(capturedError)).toBe(true);
|
||||
expect(capturedError).toBeInstanceOf(Error);
|
||||
});
|
||||
|
||||
it('surfaces response metadata through ApiClientError', async () => {
|
||||
setStoredAccessToken('metadata-token', { emit: false });
|
||||
fetchMock.mockResolvedValueOnce(
|
||||
createResponseMock({
|
||||
status: 503,
|
||||
|
||||
@@ -30,6 +30,7 @@ export type ApiRetryOptions = {
|
||||
|
||||
export type ApiRequestOptions = {
|
||||
retry?: ApiRetryOptions;
|
||||
timeoutMs?: number;
|
||||
skipAuth?: boolean;
|
||||
omitEnvelopeHeader?: boolean;
|
||||
skipRefresh?: boolean;
|
||||
@@ -172,6 +173,57 @@ function createAbortError() {
|
||||
return error;
|
||||
}
|
||||
|
||||
function createTimeoutError(timeoutMs: number) {
|
||||
const error = new Error(`请求超时:${timeoutMs}ms`);
|
||||
error.name = 'TimeoutError';
|
||||
return error;
|
||||
}
|
||||
|
||||
function composeAbortSignal(
|
||||
signal: AbortSignal | undefined,
|
||||
timeoutMs: number | undefined,
|
||||
) {
|
||||
const shouldUseTimeout =
|
||||
typeof timeoutMs === 'number' && Number.isFinite(timeoutMs) && timeoutMs > 0;
|
||||
|
||||
if (!shouldUseTimeout) {
|
||||
return {
|
||||
signal,
|
||||
cleanup: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => {
|
||||
controller.abort(createTimeoutError(timeoutMs));
|
||||
}, timeoutMs);
|
||||
|
||||
const cleanup = () => {
|
||||
clearTimeout(timeoutId);
|
||||
signal?.removeEventListener('abort', onAbort);
|
||||
};
|
||||
|
||||
const onAbort = () => {
|
||||
controller.abort(signal?.reason ?? createAbortError());
|
||||
};
|
||||
|
||||
if (signal?.aborted) {
|
||||
cleanup();
|
||||
controller.abort(signal.reason ?? createAbortError());
|
||||
return {
|
||||
signal: controller.signal,
|
||||
cleanup,
|
||||
};
|
||||
}
|
||||
|
||||
signal?.addEventListener('abort', onAbort, { once: true });
|
||||
|
||||
return {
|
||||
signal: controller.signal,
|
||||
cleanup,
|
||||
};
|
||||
}
|
||||
|
||||
async function waitForRetry(ms: number, signal?: AbortSignal) {
|
||||
if (ms <= 0) {
|
||||
return;
|
||||
@@ -268,6 +320,10 @@ export function isAbortError(error: unknown) {
|
||||
);
|
||||
}
|
||||
|
||||
export function isTimeoutError(error: unknown) {
|
||||
return error instanceof Error && error.name === 'TimeoutError';
|
||||
}
|
||||
|
||||
function shouldRetryError(error: unknown, attempt: number, retry: ResolvedRetryOptions) {
|
||||
if (attempt >= retry.maxRetries || isAbortError(error)) {
|
||||
return false;
|
||||
@@ -505,21 +561,48 @@ export async function fetchWithApiAuth(
|
||||
const method = (init.method ?? 'GET').toUpperCase();
|
||||
const retry = resolveRetryOptions(method, options.retry);
|
||||
const shouldNotifyAuthStateChange = options.notifyAuthStateChange !== false;
|
||||
const requestSignal = init.signal ?? undefined;
|
||||
let attempt = 0;
|
||||
let refreshAttempted = false;
|
||||
|
||||
for (;;) {
|
||||
try {
|
||||
const requestHeaders = withAuthorizationHeaders(init.headers, options);
|
||||
const hasAuthHeader = Boolean(
|
||||
let requestHeaders = withAuthorizationHeaders(init.headers, options);
|
||||
let hasAuthHeader = Boolean(
|
||||
requestHeaders.Authorization?.trim() ||
|
||||
requestHeaders.authorization?.trim(),
|
||||
);
|
||||
const response = await fetch(input, {
|
||||
credentials: 'same-origin',
|
||||
...init,
|
||||
headers: requestHeaders,
|
||||
});
|
||||
|
||||
if (!hasAuthHeader && !options.skipAuth && !options.skipRefresh) {
|
||||
try {
|
||||
// 受保护请求在本地 access token 缺失时,先尝试用 refresh cookie 静默补票,
|
||||
// 避免把后端原始 “缺少 Bearer Token” 直接暴露给业务 UI。
|
||||
await ensureStoredAccessToken();
|
||||
requestHeaders = withAuthorizationHeaders(init.headers, options);
|
||||
hasAuthHeader = Boolean(
|
||||
requestHeaders.Authorization?.trim() ||
|
||||
requestHeaders.authorization?.trim(),
|
||||
);
|
||||
} catch {
|
||||
// 补票失败时继续走原始请求,让调用方按真实 401 分支处理。
|
||||
}
|
||||
}
|
||||
|
||||
const timedRequest = composeAbortSignal(
|
||||
requestSignal,
|
||||
options.timeoutMs,
|
||||
);
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(input, {
|
||||
credentials: 'same-origin',
|
||||
...init,
|
||||
signal: timedRequest.signal,
|
||||
headers: requestHeaders,
|
||||
});
|
||||
} finally {
|
||||
timedRequest.cleanup();
|
||||
}
|
||||
|
||||
if (
|
||||
response.status === 401 &&
|
||||
@@ -543,7 +626,12 @@ export async function fetchWithApiAuth(
|
||||
emitAuthStateChange();
|
||||
}
|
||||
}
|
||||
} else if (response.status === 401 && hasAuthHeader && !options.skipAuth) {
|
||||
} else if (
|
||||
response.status === 401 &&
|
||||
hasAuthHeader &&
|
||||
!options.skipAuth &&
|
||||
!refreshAttempted
|
||||
) {
|
||||
clearStoredAccessToken({ emit: false });
|
||||
if (shouldNotifyAuthStateChange) {
|
||||
emitAuthStateChange();
|
||||
@@ -562,7 +650,7 @@ export async function fetchWithApiAuth(
|
||||
attempt += 1;
|
||||
await waitForRetry(
|
||||
buildRetryDelayMs(attempt, retry),
|
||||
init.signal ?? undefined,
|
||||
requestSignal,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,45 @@
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { clearStoredAccessToken, setStoredAccessToken } from './apiClient';
|
||||
import {
|
||||
clearSignedAssetReadUrlCache,
|
||||
getSignedAssetReadUrl,
|
||||
resolveAssetReadUrl,
|
||||
} from './assetReadUrlService';
|
||||
|
||||
function createLocalStorageMock() {
|
||||
const store = new Map<string, string>();
|
||||
|
||||
return {
|
||||
getItem(key: string) {
|
||||
return store.has(key) ? store.get(key)! : null;
|
||||
},
|
||||
setItem(key: string, value: string) {
|
||||
store.set(key, String(value));
|
||||
},
|
||||
removeItem(key: string) {
|
||||
store.delete(key);
|
||||
},
|
||||
clear() {
|
||||
store.clear();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('assetReadUrlService', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('window', {
|
||||
localStorage: createLocalStorageMock(),
|
||||
dispatchEvent: vi.fn(),
|
||||
});
|
||||
clearSignedAssetReadUrlCache();
|
||||
clearStoredAccessToken({ emit: false });
|
||||
setStoredAccessToken('test-access-token', { emit: false });
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clearStoredAccessToken({ emit: false });
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
@@ -110,4 +137,44 @@ describe('assetReadUrlService', () => {
|
||||
expect(second).toBe(first);
|
||||
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('getSignedAssetReadUrl caches not-found failures for the same legacy path', async () => {
|
||||
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
ok: false,
|
||||
data: null,
|
||||
error: {
|
||||
code: 'NOT_FOUND',
|
||||
message: '对象不存在',
|
||||
},
|
||||
meta: {
|
||||
apiVersion: '2026-04-08',
|
||||
routeVersion: '2026-04-08',
|
||||
latencyMs: 1,
|
||||
timestamp: '2099-01-01T00:00:00Z',
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: 404,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await expect(
|
||||
getSignedAssetReadUrl({
|
||||
legacyPublicPath: '/generated-characters/hero/missing/master.png',
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
await expect(
|
||||
getSignedAssetReadUrl({
|
||||
legacyPublicPath: '/generated-characters/hero/missing/master.png',
|
||||
}),
|
||||
).rejects.toThrow('资源不存在或暂时不可读取');
|
||||
|
||||
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { requestJson } from './apiClient';
|
||||
import { ApiClientError, requestJson } from './apiClient';
|
||||
|
||||
export type AssetReadUrlRequest = {
|
||||
objectKey?: string;
|
||||
@@ -22,9 +22,15 @@ type CachedReadUrlEntry = {
|
||||
expiresAtMs: number;
|
||||
};
|
||||
|
||||
type CachedReadUrlFailureEntry = {
|
||||
expiresAtMs: number;
|
||||
};
|
||||
|
||||
const ASSET_READ_URL_API_PATH = '/api/assets/read-url';
|
||||
const DEFAULT_CACHE_SAFETY_WINDOW_MS = 30 * 1000;
|
||||
const DEFAULT_FAILURE_CACHE_WINDOW_MS = 60 * 1000;
|
||||
const signedReadUrlCache = new Map<string, CachedReadUrlEntry>();
|
||||
const signedReadUrlFailureCache = new Map<string, CachedReadUrlFailureEntry>();
|
||||
const pendingSignedReadUrlRequests = new Map<string, Promise<string>>();
|
||||
|
||||
export function isGeneratedLegacyPath(value: string) {
|
||||
@@ -81,6 +87,16 @@ function shouldReuseCachedReadUrl(entry: CachedReadUrlEntry | undefined) {
|
||||
return entry.expiresAtMs - DEFAULT_CACHE_SAFETY_WINDOW_MS > Date.now();
|
||||
}
|
||||
|
||||
function shouldReuseCachedReadUrlFailure(
|
||||
entry: CachedReadUrlFailureEntry | undefined,
|
||||
) {
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return entry.expiresAtMs > Date.now();
|
||||
}
|
||||
|
||||
export async function getSignedAssetReadUrl(
|
||||
request: AssetReadUrlRequest,
|
||||
signal?: AbortSignal,
|
||||
@@ -91,6 +107,13 @@ export async function getSignedAssetReadUrl(
|
||||
return cached.signedUrl;
|
||||
}
|
||||
|
||||
const cachedFailure = cacheKey
|
||||
? signedReadUrlFailureCache.get(cacheKey)
|
||||
: undefined;
|
||||
if (cachedFailure && shouldReuseCachedReadUrlFailure(cachedFailure)) {
|
||||
throw new Error('资源不存在或暂时不可读取');
|
||||
}
|
||||
|
||||
if (cacheKey) {
|
||||
const pendingRequest = pendingSignedReadUrlRequests.get(cacheKey);
|
||||
if (pendingRequest) {
|
||||
@@ -117,25 +140,42 @@ export async function getSignedAssetReadUrl(
|
||||
searchParams.set('expireSeconds', String(Math.floor(request.expireSeconds)));
|
||||
}
|
||||
|
||||
const response = await requestJson<AssetReadUrlResponse>(
|
||||
`${ASSET_READ_URL_API_PATH}?${searchParams.toString()}`,
|
||||
{
|
||||
method: 'GET',
|
||||
signal,
|
||||
},
|
||||
'获取资源访问地址失败',
|
||||
);
|
||||
const payload = resolveSignedReadPayload(response);
|
||||
const expiresAtMs = parseExpiresAtMs(payload.expiresAt);
|
||||
try {
|
||||
const response = await requestJson<AssetReadUrlResponse>(
|
||||
`${ASSET_READ_URL_API_PATH}?${searchParams.toString()}`,
|
||||
{
|
||||
method: 'GET',
|
||||
signal,
|
||||
},
|
||||
'获取资源访问地址失败',
|
||||
);
|
||||
const payload = resolveSignedReadPayload(response);
|
||||
const expiresAtMs = parseExpiresAtMs(payload.expiresAt);
|
||||
|
||||
if (cacheKey && expiresAtMs > 0) {
|
||||
signedReadUrlCache.set(cacheKey, {
|
||||
signedUrl: payload.signedUrl,
|
||||
expiresAtMs,
|
||||
});
|
||||
if (cacheKey) {
|
||||
signedReadUrlFailureCache.delete(cacheKey);
|
||||
}
|
||||
|
||||
if (cacheKey && expiresAtMs > 0) {
|
||||
signedReadUrlCache.set(cacheKey, {
|
||||
signedUrl: payload.signedUrl,
|
||||
expiresAtMs,
|
||||
});
|
||||
}
|
||||
|
||||
return payload.signedUrl;
|
||||
} catch (error) {
|
||||
if (
|
||||
cacheKey &&
|
||||
error instanceof ApiClientError &&
|
||||
error.status === 404
|
||||
) {
|
||||
signedReadUrlFailureCache.set(cacheKey, {
|
||||
expiresAtMs: Date.now() + DEFAULT_FAILURE_CACHE_WINDOW_MS,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return payload.signedUrl;
|
||||
})();
|
||||
|
||||
if (cacheKey) {
|
||||
@@ -187,5 +227,6 @@ export async function resolveAssetReadUrl(
|
||||
|
||||
export function clearSignedAssetReadUrlCache() {
|
||||
signedReadUrlCache.clear();
|
||||
signedReadUrlFailureCache.clear();
|
||||
pendingSignedReadUrlRequests.clear();
|
||||
}
|
||||
|
||||
@@ -116,6 +116,10 @@ describe('authService', () => {
|
||||
}),
|
||||
}),
|
||||
'登录失败',
|
||||
{
|
||||
skipAuth: true,
|
||||
skipRefresh: true,
|
||||
},
|
||||
);
|
||||
expect(getStoredAccessToken()).toBe('jwt-entry-token');
|
||||
expect(window.dispatchEvent).not.toHaveBeenCalled();
|
||||
@@ -195,6 +199,10 @@ describe('authService', () => {
|
||||
}),
|
||||
}),
|
||||
'发送验证码失败',
|
||||
{
|
||||
skipAuth: true,
|
||||
skipRefresh: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -249,6 +257,10 @@ describe('authService', () => {
|
||||
}),
|
||||
}),
|
||||
'登录失败',
|
||||
{
|
||||
skipAuth: true,
|
||||
skipRefresh: true,
|
||||
},
|
||||
);
|
||||
expect(getStoredAccessToken()).toBe('jwt-phone-token');
|
||||
expect(window.dispatchEvent).not.toHaveBeenCalled();
|
||||
@@ -319,6 +331,10 @@ describe('authService', () => {
|
||||
method: 'GET',
|
||||
}),
|
||||
'微信登录暂不可用',
|
||||
{
|
||||
skipAuth: true,
|
||||
skipRefresh: true,
|
||||
},
|
||||
);
|
||||
expect(assignMock).toHaveBeenCalledWith(
|
||||
'/api/auth/wechat/callback?mock_code=wx-user&state=state123',
|
||||
@@ -339,6 +355,10 @@ describe('authService', () => {
|
||||
method: 'GET',
|
||||
}),
|
||||
'读取登录方式失败',
|
||||
{
|
||||
skipAuth: true,
|
||||
skipRefresh: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import type {
|
||||
LogoutResponse,
|
||||
} from '../../packages/shared/src/contracts/auth';
|
||||
import {
|
||||
type ApiRequestOptions,
|
||||
ApiClientError,
|
||||
clearStoredAccessToken,
|
||||
clearStoredAutoAuthCredentials,
|
||||
@@ -60,6 +61,13 @@ let pendingAutoAuthUser: Promise<{
|
||||
credentials: AutoAuthCredentials;
|
||||
}> | null = null;
|
||||
|
||||
// 登录前公开认证入口不能误带旧 token,也不能先触发 refresh 探测,
|
||||
// 否则无会话用户点击“获取验证码”时会先打出一条无意义的 /auth/refresh 401。
|
||||
const PUBLIC_AUTH_REQUEST_OPTIONS = {
|
||||
skipAuth: true,
|
||||
skipRefresh: true,
|
||||
} satisfies ApiRequestOptions;
|
||||
|
||||
export function normalizePhoneInput(phoneInput: string) {
|
||||
return phoneInput.replace(/[^\d+]/gu, '').trim();
|
||||
}
|
||||
@@ -147,6 +155,7 @@ export async function sendPhoneLoginCode(
|
||||
}),
|
||||
},
|
||||
'发送验证码失败',
|
||||
PUBLIC_AUTH_REQUEST_OPTIONS,
|
||||
);
|
||||
|
||||
return response;
|
||||
@@ -164,6 +173,7 @@ export async function loginWithPhoneCode(phone: string, code: string) {
|
||||
}),
|
||||
},
|
||||
'登录失败',
|
||||
PUBLIC_AUTH_REQUEST_OPTIONS,
|
||||
);
|
||||
|
||||
setStoredAccessToken(response.token, { emit: false });
|
||||
@@ -212,6 +222,7 @@ export async function startWechatLogin() {
|
||||
method: 'GET',
|
||||
},
|
||||
'微信登录暂不可用',
|
||||
PUBLIC_AUTH_REQUEST_OPTIONS,
|
||||
);
|
||||
|
||||
window.location.assign(response.authorizationUrl);
|
||||
@@ -224,6 +235,7 @@ export async function getAuthLoginOptions() {
|
||||
method: 'GET',
|
||||
},
|
||||
'读取登录方式失败',
|
||||
PUBLIC_AUTH_REQUEST_OPTIONS,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -237,6 +249,7 @@ export async function authEntry(username: string, password: string) {
|
||||
body: JSON.stringify(credentials),
|
||||
},
|
||||
'登录失败',
|
||||
PUBLIC_AUTH_REQUEST_OPTIONS,
|
||||
);
|
||||
|
||||
setStoredAccessToken(response.token, { emit: false });
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import { readCreationAgentSessionFromSse } from '../creation-agent';
|
||||
|
||||
const BIG_FISH_AGENT_API_BASE = '/api/runtime/big-fish/agent/sessions';
|
||||
const BIG_FISH_SESSION_START_TIMEOUT_MS = 15000;
|
||||
const BIG_FISH_READ_RETRY: ApiRetryOptions = {
|
||||
maxRetries: 1,
|
||||
baseDelayMs: 180,
|
||||
@@ -41,6 +42,7 @@ export async function createBigFishCreationSession(
|
||||
'创建大鱼吃小鱼共创会话失败',
|
||||
{
|
||||
retry: BIG_FISH_WRITE_RETRY,
|
||||
timeoutMs: BIG_FISH_SESSION_START_TIMEOUT_MS,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import { readCreationAgentSessionFromSse } from '../creation-agent';
|
||||
|
||||
const PUZZLE_AGENT_API_BASE = '/api/runtime/puzzle/agent/sessions';
|
||||
const PUZZLE_AGENT_SESSION_START_TIMEOUT_MS = 15000;
|
||||
const PUZZLE_AGENT_READ_RETRY: ApiRetryOptions = {
|
||||
maxRetries: 1,
|
||||
baseDelayMs: 180,
|
||||
@@ -47,6 +48,7 @@ export async function createPuzzleAgentSession(
|
||||
'创建拼图共创会话失败',
|
||||
{
|
||||
retry: PUZZLE_AGENT_WRITE_RETRY,
|
||||
timeoutMs: PUZZLE_AGENT_SESSION_START_TIMEOUT_MS,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import { requestRpgCreationRuntimeJson } from './rpgCreationRuntimeClient';
|
||||
|
||||
const RPG_AGENT_API_BASE = '/custom-world/agent/sessions';
|
||||
const CREATION_SESSION_START_TIMEOUT_MS = 15000;
|
||||
|
||||
export async function createRpgCreationSession(
|
||||
payload: CreateRpgAgentSessionRequest,
|
||||
@@ -29,6 +30,9 @@ export async function createRpgCreationSession(
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
'创建世界共创会话失败',
|
||||
{
|
||||
timeoutMs: CREATION_SESSION_START_TIMEOUT_MS,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user