1
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { clearStoredAccessToken, setStoredAccessToken } from './apiClient';
|
||||
import {
|
||||
clearStoredAccessToken,
|
||||
getStoredAccessToken,
|
||||
setStoredAccessToken,
|
||||
} from './apiClient';
|
||||
import {
|
||||
clearSignedAssetReadUrlCache,
|
||||
getSignedAssetReadUrl,
|
||||
@@ -259,4 +263,42 @@ describe('assetReadUrlService', () => {
|
||||
|
||||
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('getSignedAssetReadUrl 401 不会清空全局登录态', async () => {
|
||||
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
ok: false,
|
||||
data: null,
|
||||
error: {
|
||||
code: 'UNAUTHORIZED',
|
||||
message: '登录状态已失效',
|
||||
},
|
||||
meta: {
|
||||
apiVersion: '2026-04-08',
|
||||
routeVersion: '2026-04-08',
|
||||
latencyMs: 1,
|
||||
timestamp: '2099-01-01T00:00:00Z',
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: 401,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await expect(
|
||||
getSignedAssetReadUrl({
|
||||
legacyPublicPath:
|
||||
'/generated-puzzle-assets/puzzle-session-1/candidate-1/asset-1/image.png',
|
||||
}),
|
||||
).rejects.toThrow('登录状态已失效');
|
||||
|
||||
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
||||
expect(getStoredAccessToken()).toBe('test-access-token');
|
||||
expect(window.dispatchEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { ApiClientError, requestJson } from './apiClient';
|
||||
import {
|
||||
ApiClientError,
|
||||
type ApiRequestOptions,
|
||||
requestJson,
|
||||
} from './apiClient';
|
||||
|
||||
export type AssetReadUrlRequest = {
|
||||
objectKey?: string;
|
||||
@@ -41,6 +45,11 @@ type CachedReadUrlFailureEntry = {
|
||||
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 ASSET_READ_URL_BACKGROUND_OPTIONS = {
|
||||
skipRefresh: true,
|
||||
notifyAuthStateChange: false,
|
||||
clearAuthOnUnauthorized: false,
|
||||
} satisfies ApiRequestOptions;
|
||||
const signedReadUrlCache = new Map<string, CachedReadUrlEntry>();
|
||||
const signedReadUrlFailureCache = new Map<string, CachedReadUrlFailureEntry>();
|
||||
const pendingSignedReadUrlRequests = new Map<string, Promise<string>>();
|
||||
@@ -165,6 +174,10 @@ export async function getSignedAssetReadUrl(
|
||||
signal,
|
||||
},
|
||||
'获取资源访问地址失败',
|
||||
{
|
||||
// 中文注释:图片换签属于展示层后台请求,失败只影响当前图片,不应刷新或清空全局登录态。
|
||||
...ASSET_READ_URL_BACKGROUND_OPTIONS,
|
||||
},
|
||||
);
|
||||
const payload = resolveSignedReadPayload(response);
|
||||
const expiresAtMs = parseExpiresAtMs(payload.expiresAt);
|
||||
|
||||
72
src/services/hyper3dModelGenerationService.ts
Normal file
72
src/services/hyper3dModelGenerationService.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import type {
|
||||
Hyper3dDownloadRequest,
|
||||
Hyper3dDownloadResponse,
|
||||
Hyper3dImageToModelRequest,
|
||||
Hyper3dTaskStatusRequest,
|
||||
Hyper3dTaskStatusResponse,
|
||||
Hyper3dTaskSubmitResponse,
|
||||
Hyper3dTextToModelRequest,
|
||||
} from '../../packages/shared/src/contracts/hyper3d';
|
||||
|
||||
import { requestJson } from './apiClient';
|
||||
|
||||
const HYPER3D_API_BASE = '/api/assets/hyper3d';
|
||||
const GENERATION_REQUEST_TIMEOUT_MS = 180_000;
|
||||
|
||||
function postHyper3dJson<TResponse>(
|
||||
path: string,
|
||||
payload: unknown,
|
||||
fallbackMessage: string,
|
||||
timeoutMs = GENERATION_REQUEST_TIMEOUT_MS,
|
||||
) {
|
||||
return requestJson<TResponse>(
|
||||
`${HYPER3D_API_BASE}${path}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
fallbackMessage,
|
||||
{
|
||||
timeoutMs,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function submitHyper3dTextToModel(
|
||||
payload: Hyper3dTextToModelRequest,
|
||||
) {
|
||||
return postHyper3dJson<Hyper3dTaskSubmitResponse>(
|
||||
'/text-to-model',
|
||||
payload,
|
||||
'提交文生 3D 模型任务失败',
|
||||
);
|
||||
}
|
||||
|
||||
export function submitHyper3dImageToModel(
|
||||
payload: Hyper3dImageToModelRequest,
|
||||
) {
|
||||
return postHyper3dJson<Hyper3dTaskSubmitResponse>(
|
||||
'/image-to-model',
|
||||
payload,
|
||||
'提交图生 3D 模型任务失败',
|
||||
);
|
||||
}
|
||||
|
||||
export function getHyper3dTaskStatus(payload: Hyper3dTaskStatusRequest) {
|
||||
return postHyper3dJson<Hyper3dTaskStatusResponse>(
|
||||
'/status',
|
||||
payload,
|
||||
'查询 3D 模型任务状态失败',
|
||||
60_000,
|
||||
);
|
||||
}
|
||||
|
||||
export function getHyper3dDownloads(payload: Hyper3dDownloadRequest) {
|
||||
return postHyper3dJson<Hyper3dDownloadResponse>(
|
||||
'/download',
|
||||
payload,
|
||||
'获取 3D 模型下载列表失败',
|
||||
60_000,
|
||||
);
|
||||
}
|
||||
@@ -119,6 +119,7 @@ export async function dragPuzzlePieceOrGroup(
|
||||
export async function advancePuzzleNextLevel(
|
||||
runId: string,
|
||||
payload: AdvancePuzzleNextLevelRequest = {},
|
||||
options: PuzzleRuntimeRequestOptions = {},
|
||||
) {
|
||||
const targetProfileId = payload.targetProfileId?.trim() ?? '';
|
||||
return requestJson<PuzzleRunResponse>(
|
||||
@@ -135,6 +136,9 @@ export async function advancePuzzleNextLevel(
|
||||
'进入下一关失败',
|
||||
{
|
||||
retry: PUZZLE_RUNTIME_WRITE_RETRY,
|
||||
skipRefresh: options.skipRefresh,
|
||||
notifyAuthStateChange: options.notifyAuthStateChange,
|
||||
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -145,6 +149,7 @@ export async function advancePuzzleNextLevel(
|
||||
export async function submitPuzzleLeaderboard(
|
||||
runId: string,
|
||||
payload: SubmitPuzzleLeaderboardRequest,
|
||||
options: PuzzleRuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestJson<PuzzleRunResponse>(
|
||||
`${PUZZLE_RUNTIME_API_BASE}/${encodeURIComponent(runId)}/leaderboard`,
|
||||
@@ -156,6 +161,9 @@ export async function submitPuzzleLeaderboard(
|
||||
'提交拼图排行榜失败',
|
||||
{
|
||||
retry: PUZZLE_RUNTIME_WRITE_RETRY,
|
||||
skipRefresh: options.skipRefresh,
|
||||
notifyAuthStateChange: options.notifyAuthStateChange,
|
||||
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -166,6 +174,7 @@ export async function submitPuzzleLeaderboard(
|
||||
export async function updatePuzzleRunPause(
|
||||
runId: string,
|
||||
payload: UpdatePuzzleRuntimePauseRequest,
|
||||
options: PuzzleRuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestJson<PuzzleRunResponse>(
|
||||
`${PUZZLE_RUNTIME_API_BASE}/${encodeURIComponent(runId)}/pause`,
|
||||
@@ -177,6 +186,9 @@ export async function updatePuzzleRunPause(
|
||||
'更新拼图计时状态失败',
|
||||
{
|
||||
retry: PUZZLE_RUNTIME_WRITE_RETRY,
|
||||
skipRefresh: options.skipRefresh,
|
||||
notifyAuthStateChange: options.notifyAuthStateChange,
|
||||
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -187,6 +199,7 @@ export async function updatePuzzleRunPause(
|
||||
export async function usePuzzleRuntimeProp(
|
||||
runId: string,
|
||||
payload: UsePuzzleRuntimePropRequest,
|
||||
options: PuzzleRuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestJson<PuzzleRunResponse>(
|
||||
`${PUZZLE_RUNTIME_API_BASE}/${encodeURIComponent(runId)}/props`,
|
||||
@@ -198,6 +211,9 @@ export async function usePuzzleRuntimeProp(
|
||||
'使用拼图道具失败',
|
||||
{
|
||||
retry: PUZZLE_RUNTIME_WRITE_RETRY,
|
||||
skipRefresh: options.skipRefresh,
|
||||
notifyAuthStateChange: options.notifyAuthStateChange,
|
||||
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user