merge master into codex/wechat-mini-program-virtual-payment
This commit is contained in:
@@ -3,6 +3,7 @@ import {
|
||||
postApiJson,
|
||||
} from '../../editor/shared/editorApiClient';
|
||||
import {
|
||||
appendApiErrorRequestId,
|
||||
fetchJson,
|
||||
parseApiErrorMessage,
|
||||
} from '../../editor/shared/jsonClient';
|
||||
@@ -265,7 +266,10 @@ export async function putCharacterRoleAssetWorkflow(
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
parseApiErrorMessage(responseText, '保存角色资产工坊缓存失败'),
|
||||
appendApiErrorRequestId(
|
||||
parseApiErrorMessage(responseText, '保存角色资产工坊缓存失败'),
|
||||
response.headers.get('x-request-id'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import { AccountModal } from './AccountModal';
|
||||
|
||||
const baseUser: AuthUser = {
|
||||
id: 'user-1',
|
||||
username: 'tester',
|
||||
displayName: '138****8000',
|
||||
avatarUrl: null,
|
||||
publicUserCode: 'user-tester',
|
||||
@@ -22,7 +21,6 @@ const baseUser: AuthUser = {
|
||||
loginMethod: 'phone',
|
||||
bindingStatus: 'active',
|
||||
wechatBound: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
function renderAccountModal(overrides?: {
|
||||
@@ -87,14 +85,7 @@ function buildSession(
|
||||
sessionId: 'usess_1',
|
||||
sessionIds: ['usess_1'],
|
||||
sessionCount: 1,
|
||||
clientType: 'web_browser',
|
||||
clientRuntime: 'chrome',
|
||||
clientPlatform: 'windows',
|
||||
clientLabel: 'Windows / Chrome',
|
||||
deviceDisplayName: 'Windows / Chrome',
|
||||
miniProgramAppId: null,
|
||||
miniProgramEnv: null,
|
||||
userAgent: 'Mozilla/5.0',
|
||||
ipMasked: '203.0.*.*',
|
||||
isCurrent: false,
|
||||
createdAt: '2026-05-01T10:00:00.000Z',
|
||||
@@ -271,14 +262,7 @@ test('account panel includes merged security devices and audit sections', async
|
||||
sessionId: 'session-1',
|
||||
sessionIds: ['session-1'],
|
||||
sessionCount: 1,
|
||||
clientType: 'mobile',
|
||||
clientRuntime: 'ios',
|
||||
clientPlatform: 'wechat',
|
||||
clientLabel: 'iPhone 15 Pro',
|
||||
deviceDisplayName: 'iPhone 15 Pro / 微信',
|
||||
miniProgramAppId: null,
|
||||
miniProgramEnv: null,
|
||||
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X)',
|
||||
isCurrent: true,
|
||||
createdAt: '2026-04-20T07:30:00.000Z',
|
||||
lastSeenAt: '2026-04-20T09:00:00.000Z',
|
||||
|
||||
@@ -94,7 +94,6 @@ vi.mock('./BindPhoneScreen', () => ({
|
||||
|
||||
const mockUser: AuthUser = {
|
||||
id: 'user-1',
|
||||
username: 'tester',
|
||||
displayName: '测试玩家',
|
||||
avatarUrl: null,
|
||||
publicUserCode: 'user-tester',
|
||||
@@ -102,7 +101,6 @@ const mockUser: AuthUser = {
|
||||
loginMethod: 'phone',
|
||||
bindingStatus: 'active',
|
||||
wechatBound: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -863,14 +861,7 @@ test('auth gate revokes merged session group and refreshes sessions', async () =
|
||||
sessionId: 'usess_remote',
|
||||
sessionIds: ['usess_remote', 'usess_remote_rotated'],
|
||||
sessionCount: 2,
|
||||
clientType: 'web_browser',
|
||||
clientRuntime: 'chrome',
|
||||
clientPlatform: 'windows',
|
||||
clientLabel: 'Windows / Chrome',
|
||||
deviceDisplayName: 'Windows / Chrome',
|
||||
miniProgramAppId: null,
|
||||
miniProgramEnv: null,
|
||||
userAgent: 'Mozilla/5.0',
|
||||
ipMasked: '203.0.*.*',
|
||||
isCurrent: false,
|
||||
createdAt: '2026-05-01T10:00:00.000Z',
|
||||
|
||||
@@ -1048,7 +1048,7 @@ test('buildCreationWorkShelfItems maps bark battle works with scene role cover a
|
||||
);
|
||||
});
|
||||
|
||||
test('bark battle draft generating state follows pending assets or missing three images', () => {
|
||||
test('bark battle draft generating state only follows pending assets', () => {
|
||||
const draft = {
|
||||
workId: 'bark-battle-work-draft',
|
||||
draftId: 'bark-battle-draft-1',
|
||||
@@ -1073,6 +1073,12 @@ test('bark battle draft generating state follows pending assets or missing three
|
||||
|
||||
expect(hasBarkBattleRequiredImages(draft)).toBe(false);
|
||||
expect(isPersistedBarkBattleDraftGenerating(draft)).toBe(true);
|
||||
expect(
|
||||
isPersistedBarkBattleDraftGenerating({
|
||||
...draft,
|
||||
generationStatus: 'partial_failed',
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isPersistedBarkBattleDraftGenerating({
|
||||
...draft,
|
||||
|
||||
@@ -1111,10 +1111,9 @@ export function isPersistedBarkBattleDraftGenerating(
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
item.generationStatus === 'pending_assets' ||
|
||||
!hasBarkBattleRequiredImages(item)
|
||||
);
|
||||
// 中文注释:汪汪声浪生成失败后会回写 partial_failed 并进入结果页承接错误槽位,
|
||||
// 不能因为三图未齐就继续把作品架整卡锁成“生成中”。
|
||||
return item.generationStatus === 'pending_assets';
|
||||
}
|
||||
|
||||
export function hasBarkBattleRequiredImages(item: BarkBattleWorkSummary) {
|
||||
|
||||
@@ -1966,13 +1966,65 @@ function buildWoodenFishCreationUrlState(params: {
|
||||
const profileId = normalizeCreationUrlValue(
|
||||
params.work?.summary.profileId ?? params.session?.draft?.profileId,
|
||||
);
|
||||
const draftId = profileId ?? sessionId;
|
||||
return {
|
||||
sessionId,
|
||||
profileId,
|
||||
draftId,
|
||||
workId: normalizeCreationUrlValue(params.work?.summary.workId ?? profileId),
|
||||
};
|
||||
}
|
||||
|
||||
function buildWoodenFishSessionFromWorkDetail(
|
||||
work: WoodenFishWorkProfileResponse,
|
||||
fallbackItem?: WoodenFishWorkSummaryResponse | null,
|
||||
): WoodenFishSessionSnapshotResponse {
|
||||
const sessionId =
|
||||
normalizeCreationUrlValue(work.summary.sourceSessionId) ??
|
||||
normalizeCreationUrlValue(fallbackItem?.sourceSessionId) ??
|
||||
work.summary.profileId;
|
||||
return {
|
||||
sessionId,
|
||||
ownerUserId: work.summary.ownerUserId,
|
||||
status: work.summary.generationStatus,
|
||||
draft: work.draft,
|
||||
createdAt: work.summary.updatedAt,
|
||||
updatedAt: work.summary.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
function buildWoodenFishPendingSession(
|
||||
item: WoodenFishWorkSummaryResponse,
|
||||
): WoodenFishSessionSnapshotResponse {
|
||||
const sessionId =
|
||||
normalizeCreationUrlValue(item.sourceSessionId) ?? item.profileId;
|
||||
return {
|
||||
sessionId,
|
||||
ownerUserId: item.ownerUserId,
|
||||
status: item.generationStatus,
|
||||
draft: {
|
||||
templateId: 'wooden-fish',
|
||||
templateName: '敲木鱼',
|
||||
profileId: item.profileId,
|
||||
workTitle: item.workTitle,
|
||||
workDescription: item.workDescription,
|
||||
themeTags: item.themeTags,
|
||||
hitObjectPrompt: '',
|
||||
hitObjectReferenceImageSrc: null,
|
||||
hitSoundPrompt: null,
|
||||
floatingWords: ['功德 +1'],
|
||||
hitObjectAsset: null,
|
||||
backgroundAsset: null,
|
||||
backButtonAsset: null,
|
||||
hitSoundAsset: null,
|
||||
coverImageSrc: item.coverImageSrc,
|
||||
generationStatus: item.generationStatus,
|
||||
},
|
||||
createdAt: item.updatedAt,
|
||||
updatedAt: item.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
function buildBarkBattleCreationUrlState(
|
||||
draft: BarkBattleDraftConfig | null,
|
||||
): CreationUrlState {
|
||||
@@ -2094,6 +2146,8 @@ function buildDraftCompletionDialogSource(
|
||||
return formatPlatformTaskCompletionSource('方洞挑战草稿', sourceId);
|
||||
case 'jump-hop':
|
||||
return formatPlatformTaskCompletionSource('跳一跳草稿', sourceId);
|
||||
case 'wooden-fish':
|
||||
return formatPlatformTaskCompletionSource('敲木鱼草稿', sourceId);
|
||||
case 'puzzle':
|
||||
return formatPlatformTaskCompletionSource('拼图草稿', sourceId);
|
||||
case 'visual-novel':
|
||||
@@ -2478,6 +2532,37 @@ function buildPendingJumpHopWorks(
|
||||
}));
|
||||
}
|
||||
|
||||
function buildPendingWoodenFishWorks(
|
||||
pending: Record<string, PendingDraftShelfState> | undefined,
|
||||
existingItems: readonly WoodenFishWorkSummaryResponse[],
|
||||
): WoodenFishWorkSummaryResponse[] {
|
||||
if (!pending) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.entries(pending)
|
||||
.filter(([sessionId]) =>
|
||||
existingItems.every((item) => item.sourceSessionId !== sessionId),
|
||||
)
|
||||
.map(([sessionId, state]) => ({
|
||||
runtimeKind: 'wooden-fish',
|
||||
workId: `wooden-fish-work-${sessionId}`,
|
||||
profileId: sessionId,
|
||||
ownerUserId: '',
|
||||
sourceSessionId: sessionId,
|
||||
workTitle: '敲木鱼草稿',
|
||||
workDescription: '正在生成敲木鱼草稿。',
|
||||
themeTags: ['敲木鱼'],
|
||||
coverImageSrc: null,
|
||||
publicationStatus: 'draft',
|
||||
playCount: 0,
|
||||
updatedAt: state.updatedAt,
|
||||
publishedAt: null,
|
||||
publishReady: false,
|
||||
generationStatus: state.status === 'generating' ? 'generating' : 'ready',
|
||||
}));
|
||||
}
|
||||
|
||||
function buildPendingMatch3DWorks(
|
||||
pending: Record<string, PendingDraftShelfState> | undefined,
|
||||
existingItems: readonly Match3DWorkSummary[],
|
||||
@@ -4510,8 +4595,14 @@ export function PlatformEntryFlowShellImpl({
|
||||
[jumpHopWorks, pendingDraftShelfItems],
|
||||
);
|
||||
const woodenFishShelfItems = useMemo(
|
||||
() => woodenFishWorks,
|
||||
[woodenFishWorks],
|
||||
() => [
|
||||
...buildPendingWoodenFishWorks(
|
||||
pendingDraftShelfItems['wooden-fish'],
|
||||
woodenFishWorks,
|
||||
),
|
||||
...woodenFishWorks,
|
||||
],
|
||||
[pendingDraftShelfItems, woodenFishWorks],
|
||||
);
|
||||
const match3dShelfItems = useMemo(
|
||||
() => [
|
||||
@@ -8887,6 +8978,35 @@ export function PlatformEntryFlowShellImpl({
|
||||
setWoodenFishGenerationState(generationState);
|
||||
setIsWoodenFishBusy(true);
|
||||
setSelectionStage('wooden-fish-generating');
|
||||
markDraftGenerating('wooden-fish', [created.session.sessionId]);
|
||||
markPendingDraftGenerating('wooden-fish', created.session.sessionId);
|
||||
const createdAt = created.session.updatedAt ?? created.session.createdAt;
|
||||
setWoodenFishWorks((current) => [
|
||||
{
|
||||
runtimeKind: 'wooden-fish',
|
||||
workId: created.session.sessionId,
|
||||
profileId: created.session.sessionId,
|
||||
ownerUserId: created.session.ownerUserId,
|
||||
sourceSessionId: created.session.sessionId,
|
||||
workTitle:
|
||||
payload?.workTitle ?? created.session.draft?.workTitle ?? '敲木鱼',
|
||||
workDescription:
|
||||
payload?.workDescription ??
|
||||
created.session.draft?.workDescription ??
|
||||
'',
|
||||
themeTags: payload?.themeTags ?? created.session.draft?.themeTags ?? ['敲木鱼'],
|
||||
coverImageSrc: created.session.draft?.coverImageSrc ?? null,
|
||||
publicationStatus: 'draft',
|
||||
playCount: 0,
|
||||
updatedAt: createdAt,
|
||||
publishedAt: null,
|
||||
publishReady: false,
|
||||
generationStatus: 'generating',
|
||||
},
|
||||
...current.filter(
|
||||
(item) => item.sourceSessionId !== created.session.sessionId,
|
||||
),
|
||||
]);
|
||||
|
||||
try {
|
||||
const response = await woodenFishClient.executeAction(
|
||||
@@ -8921,6 +9041,32 @@ export function PlatformEntryFlowShellImpl({
|
||||
setWoodenFishGenerationState(
|
||||
createReadyWoodenFishGenerationState(generationState),
|
||||
);
|
||||
if (response.work) {
|
||||
setWoodenFishWorks((current) => [
|
||||
response.work!.summary,
|
||||
...current.filter(
|
||||
(item) =>
|
||||
item.workId !== response.work!.summary.workId &&
|
||||
item.sourceSessionId !== response.work!.summary.sourceSessionId,
|
||||
),
|
||||
]);
|
||||
markPendingDraftReady(
|
||||
'wooden-fish',
|
||||
created.session.sessionId,
|
||||
false,
|
||||
);
|
||||
markDraftReady(
|
||||
'wooden-fish',
|
||||
[
|
||||
created.session.sessionId,
|
||||
response.work.summary.workId,
|
||||
response.work.summary.profileId,
|
||||
response.work.summary.sourceSessionId,
|
||||
],
|
||||
false,
|
||||
);
|
||||
void refreshWoodenFishShelf().catch(() => undefined);
|
||||
}
|
||||
setSelectionStage('wooden-fish-result');
|
||||
} catch (error) {
|
||||
const errorMessage = resolveRpgCreationErrorMessage(
|
||||
@@ -8955,7 +9101,15 @@ export function PlatformEntryFlowShellImpl({
|
||||
setIsWoodenFishBusy(false);
|
||||
}
|
||||
},
|
||||
[createReadyWoodenFishGenerationState, setSelectionStage],
|
||||
[
|
||||
createReadyWoodenFishGenerationState,
|
||||
markDraftGenerating,
|
||||
markDraftReady,
|
||||
markPendingDraftGenerating,
|
||||
markPendingDraftReady,
|
||||
refreshWoodenFishShelf,
|
||||
setSelectionStage,
|
||||
],
|
||||
);
|
||||
|
||||
const retryWoodenFishDraftGeneration = useCallback(() => {
|
||||
@@ -11776,18 +11930,43 @@ export function PlatformEntryFlowShellImpl({
|
||||
setWoodenFishError(null);
|
||||
setPublicWorkDetailError(null);
|
||||
setIsWoodenFishBusy(true);
|
||||
if (item.generationStatus === 'generating') {
|
||||
const pendingSession = buildWoodenFishPendingSession(item);
|
||||
setWoodenFishSession(pendingSession);
|
||||
setWoodenFishRun(null);
|
||||
setWoodenFishWork(null);
|
||||
writeCreationUrlState(
|
||||
buildWoodenFishCreationUrlState({ session: pendingSession }),
|
||||
);
|
||||
enterCreateTab();
|
||||
setSelectionStage('wooden-fish-generating');
|
||||
setIsWoodenFishBusy(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const detail = await woodenFishClient.getWorkDetail(item.profileId);
|
||||
setWoodenFishSession(null);
|
||||
const recoveredSession = buildWoodenFishSessionFromWorkDetail(
|
||||
detail.item,
|
||||
item,
|
||||
);
|
||||
setWoodenFishSession(recoveredSession);
|
||||
setWoodenFishRun(null);
|
||||
setWoodenFishWork(detail.item);
|
||||
setWoodenFishRuntimeReturnStage('wooden-fish-result');
|
||||
writeCreationUrlState(
|
||||
buildWoodenFishCreationUrlState({
|
||||
session: recoveredSession,
|
||||
work: detail.item,
|
||||
}),
|
||||
);
|
||||
enterCreateTab();
|
||||
setSelectionStage('wooden-fish-result');
|
||||
} catch (error) {
|
||||
setWoodenFishError(
|
||||
resolveRpgCreationErrorMessage(error, '读取敲木鱼草稿失败。'),
|
||||
);
|
||||
enterCreateTab();
|
||||
setSelectionStage('wooden-fish-generating');
|
||||
} finally {
|
||||
setIsWoodenFishBusy(false);
|
||||
}
|
||||
@@ -11796,6 +11975,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
enterCreateTab,
|
||||
markDraftNoticeSeen,
|
||||
openWoodenFishPublicWorkDetail,
|
||||
writeCreationUrlState,
|
||||
setSelectionStage,
|
||||
],
|
||||
);
|
||||
@@ -15114,8 +15294,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
>
|
||||
<PlatformWorkDetailView
|
||||
entry={selectedPublicWorkDetail}
|
||||
authorAvatarUrl={selectedPublicWorkAuthor?.avatarUrl ?? null}
|
||||
authorDisplayName={selectedPublicWorkAuthor?.displayName ?? null}
|
||||
authorSummary={selectedPublicWorkAuthor}
|
||||
isBusy={
|
||||
isPublicWorkDetailBusy ||
|
||||
isPuzzleBusy ||
|
||||
@@ -15161,10 +15340,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
) : selectedDetailEntry.visibility !== 'draft' ? (
|
||||
<PlatformWorkDetailView
|
||||
entry={mapRpgGalleryCardToPublicWorkDetail(selectedDetailEntry)}
|
||||
authorAvatarUrl={selectedPublicWorkAuthor?.avatarUrl ?? null}
|
||||
authorDisplayName={
|
||||
selectedPublicWorkAuthor?.displayName ?? null
|
||||
}
|
||||
authorSummary={selectedPublicWorkAuthor}
|
||||
isBusy={detailNavigation.isMutatingDetail}
|
||||
error={detailNavigation.detailError}
|
||||
actionMode={
|
||||
@@ -16473,11 +16649,11 @@ export function PlatformEntryFlowShellImpl({
|
||||
onInterrupt={undefined}
|
||||
backLabel="返回创作中心"
|
||||
settingActionLabel={null}
|
||||
retryLabel="重新生成草稿"
|
||||
retryLabel="重新生成图片"
|
||||
settingTitle="当前拼图信息"
|
||||
settingDescription={null}
|
||||
progressTitle="拼图草稿生成进度"
|
||||
activeBadgeLabel="草稿生成中"
|
||||
progressTitle="拼图图片生成进度"
|
||||
activeBadgeLabel="图片生成中"
|
||||
idleBadgeLabel="等待返回工作区"
|
||||
hideBatchModule
|
||||
/>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_NAME,
|
||||
type PlatformEdutainmentGalleryCard,
|
||||
type PlatformPuzzleGalleryCard,
|
||||
type PlatformWoodenFishGalleryCard,
|
||||
} from '../rpg-entry/rpgEntryWorldPresentation';
|
||||
import { PlatformWorkDetailView } from './PlatformWorkDetailView';
|
||||
|
||||
@@ -82,6 +83,29 @@ function createBabyObjectMatchEntry(): PlatformEdutainmentGalleryCard {
|
||||
};
|
||||
}
|
||||
|
||||
function createWoodenFishEntry(): PlatformWoodenFishGalleryCard {
|
||||
return {
|
||||
sourceType: 'wooden-fish',
|
||||
workId: 'wooden-fish-work-1',
|
||||
profileId: 'wooden-fish-profile-1',
|
||||
publicWorkCode: 'WF-001',
|
||||
ownerUserId: 'user-wooden-fish',
|
||||
authorDisplayName: '敲木鱼玩家',
|
||||
worldName: '莲花木鱼',
|
||||
subtitle: '敲木鱼',
|
||||
summaryText: '莲花主题敲木鱼。',
|
||||
coverImageSrc: null,
|
||||
themeTags: ['敲木鱼'],
|
||||
playCount: 12,
|
||||
remixCount: 0,
|
||||
likeCount: 4,
|
||||
recentPlayCount7d: 0,
|
||||
visibility: 'published',
|
||||
publishedAt: '2026-05-20T10:00:00.000Z',
|
||||
updatedAt: '2026-05-20T12:00:00.000Z',
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
@@ -119,7 +143,13 @@ test('PlatformWorkDetailView prefers resolved public user display name', () => {
|
||||
render(
|
||||
<PlatformWorkDetailView
|
||||
entry={createPuzzleEntry()}
|
||||
authorDisplayName="新的作者昵称"
|
||||
authorSummary={{
|
||||
id: 'user-2',
|
||||
publicUserCode: '',
|
||||
username: 'phone_00000002',
|
||||
displayName: '新的作者昵称',
|
||||
avatarUrl: null,
|
||||
}}
|
||||
isBusy={false}
|
||||
error={null}
|
||||
onBack={vi.fn()}
|
||||
@@ -133,6 +163,32 @@ test('PlatformWorkDetailView prefers resolved public user display name', () => {
|
||||
expect(screen.queryByText('137****6613')).toBeNull();
|
||||
});
|
||||
|
||||
test('PlatformWorkDetailView prefers display name then public user code for wooden fish works', () => {
|
||||
render(
|
||||
<PlatformWorkDetailView
|
||||
entry={createWoodenFishEntry()}
|
||||
authorSummary={{
|
||||
id: 'user-4',
|
||||
publicUserCode: 'SY-00000004',
|
||||
username: 'phone_00000004',
|
||||
displayName: '公开昵称',
|
||||
avatarUrl: null,
|
||||
}}
|
||||
isBusy={false}
|
||||
error={null}
|
||||
onBack={vi.fn()}
|
||||
onLike={vi.fn()}
|
||||
onStart={vi.fn()}
|
||||
onRemix={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('公开昵称 · SY-00000004')).toBeTruthy();
|
||||
expect(screen.queryByText('phone_00000004')).toBeNull();
|
||||
expect(screen.queryByText('敲木鱼玩家')).toBeNull();
|
||||
expect(screen.queryByText('公开昵称')).toBeNull();
|
||||
});
|
||||
|
||||
test('PlatformWorkDetailView calls like handler', () => {
|
||||
const onLike = vi.fn();
|
||||
render(
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import type { PublicUserSummary } from '../../../packages/shared/src/contracts/auth';
|
||||
import { buildPublicWorkDetailUrl } from '../../routing/appPageRoutes';
|
||||
import { copyTextToClipboard } from '../../services/clipboard';
|
||||
import { ResolvedAssetImage } from '../ResolvedAssetImage';
|
||||
@@ -25,6 +26,7 @@ import {
|
||||
isBarkBattleGalleryEntry,
|
||||
isEdutainmentGalleryEntry,
|
||||
type PlatformPublicGalleryCard,
|
||||
resolvePlatformWorkAuthorDisplayName,
|
||||
resolvePlatformPublicWorkCode,
|
||||
resolvePlatformWorldCoverSlides,
|
||||
resolvePlatformWorldStats,
|
||||
@@ -32,8 +34,7 @@ import {
|
||||
|
||||
export interface PlatformWorkDetailViewProps {
|
||||
entry: PlatformPublicGalleryCard;
|
||||
authorAvatarUrl?: string | null;
|
||||
authorDisplayName?: string | null;
|
||||
authorSummary?: PublicUserSummary | null;
|
||||
isBusy: boolean;
|
||||
error?: string | null;
|
||||
visibleCoverCount?: number;
|
||||
@@ -85,8 +86,7 @@ const PLATFORM_WORK_COVER_CAROUSEL_INTERVAL_MS = 4200;
|
||||
|
||||
export function PlatformWorkDetailView({
|
||||
entry,
|
||||
authorAvatarUrl,
|
||||
authorDisplayName,
|
||||
authorSummary,
|
||||
isBusy,
|
||||
visibleCoverCount = 1,
|
||||
onBack,
|
||||
@@ -108,9 +108,11 @@ export function PlatformWorkDetailView({
|
||||
const appIconImage = coverSlides[0]?.imageSrc ?? '';
|
||||
const hasCoverCarousel = coverSlides.length > 1;
|
||||
const publicWorkCode = resolvePlatformPublicWorkCode(entry);
|
||||
const normalizedAuthorAvatarUrl = authorAvatarUrl?.trim() ?? '';
|
||||
const resolvedAuthorDisplayName =
|
||||
authorDisplayName?.trim() || entry.authorDisplayName;
|
||||
const normalizedAuthorAvatarUrl = authorSummary?.avatarUrl?.trim() ?? '';
|
||||
const resolvedAuthorDisplayName = resolvePlatformWorkAuthorDisplayName(
|
||||
entry,
|
||||
authorSummary,
|
||||
);
|
||||
const [copyState, setCopyState] = useState<'idle' | 'copied' | 'failed'>(
|
||||
'idle',
|
||||
);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { PublicUserSummary } from '../../../packages/shared/src/contracts/auth';
|
||||
import type {
|
||||
BarkBattleDraftConfig,
|
||||
BarkBattleGenerationStatus as SharedBarkBattleGenerationStatus,
|
||||
@@ -58,7 +57,14 @@ export function shouldPreserveLocalBarkBattleWorkOnRefresh(
|
||||
|
||||
export function buildBarkBattleWorkSummaryFromDraft(
|
||||
draft: BarkBattleDraftConfig,
|
||||
user: PublicUserSummary | null | undefined,
|
||||
user:
|
||||
| {
|
||||
id: string;
|
||||
displayName: string;
|
||||
publicUserCode: string;
|
||||
}
|
||||
| null
|
||||
| undefined,
|
||||
generationStatus: BarkBattleGenerationStatus = 'pending_assets',
|
||||
): BarkBattleWorkSummary {
|
||||
const workId = draft.workId?.trim() || draft.draftId;
|
||||
|
||||
@@ -187,6 +187,7 @@ const authServiceMocks = vi.hoisted(() => ({
|
||||
async (publicUserCode: string): Promise<PublicUserSummary> => ({
|
||||
id: `public-user-${publicUserCode}`,
|
||||
publicUserCode,
|
||||
username: 'author_user',
|
||||
displayName: '公开作者',
|
||||
avatarUrl: null,
|
||||
}),
|
||||
@@ -195,6 +196,7 @@ const authServiceMocks = vi.hoisted(() => ({
|
||||
async (userId: string): Promise<PublicUserSummary> => ({
|
||||
id: userId,
|
||||
publicUserCode: `code-${userId}`,
|
||||
username: 'author_user',
|
||||
displayName: '公开作者',
|
||||
avatarUrl: null,
|
||||
}),
|
||||
@@ -1379,7 +1381,6 @@ const mockSession: CustomWorldAgentSessionSnapshot = {
|
||||
|
||||
const mockAuthUser: AuthUser = {
|
||||
id: 'user-1',
|
||||
username: 'tester',
|
||||
displayName: '测试玩家',
|
||||
avatarUrl: null,
|
||||
publicUserCode: 'user-tester',
|
||||
@@ -1387,7 +1388,6 @@ const mockAuthUser: AuthUser = {
|
||||
loginMethod: 'password',
|
||||
bindingStatus: 'active',
|
||||
wechatBound: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
function buildMockCreativeAgentSession(
|
||||
@@ -4271,7 +4271,7 @@ test('running puzzle form generation creates a new puzzle draft on same template
|
||||
);
|
||||
expect(
|
||||
await screen.findByRole('progressbar', {
|
||||
name: '拼图草稿生成进度',
|
||||
name: '拼图图片生成进度',
|
||||
}),
|
||||
).toBeTruthy();
|
||||
|
||||
@@ -4307,7 +4307,7 @@ test('running puzzle form generation creates a new puzzle draft on same template
|
||||
|
||||
expect(
|
||||
await screen.findByRole('progressbar', {
|
||||
name: '拼图草稿生成进度',
|
||||
name: '拼图图片生成进度',
|
||||
}),
|
||||
).toBeTruthy();
|
||||
await user.click(screen.getByRole('button', { name: '返回创作中心' }));
|
||||
@@ -4387,7 +4387,7 @@ test('running puzzle draft opens generation progress from draft tab', async () =
|
||||
);
|
||||
expect(
|
||||
await screen.findByRole('progressbar', {
|
||||
name: '拼图草稿生成进度',
|
||||
name: '拼图图片生成进度',
|
||||
}),
|
||||
).toBeTruthy();
|
||||
|
||||
@@ -4401,7 +4401,7 @@ test('running puzzle draft opens generation progress from draft tab', async () =
|
||||
|
||||
expect(
|
||||
await screen.findByRole('progressbar', {
|
||||
name: '拼图草稿生成进度',
|
||||
name: '拼图图片生成进度',
|
||||
}),
|
||||
).toBeTruthy();
|
||||
expect(screen.queryByText('拼图结果页')).toBeNull();
|
||||
@@ -5388,7 +5388,7 @@ test('embedded puzzle form recovers when compile request times out after backend
|
||||
});
|
||||
expect(screen.queryByText('执行拼图操作失败。')).toBeNull();
|
||||
expect(screen.queryByText('请求超时:90000ms')).toBeNull();
|
||||
expect(screen.queryByText('拼图草稿生成进度')).toBeNull();
|
||||
expect(screen.queryByText('拼图图片生成进度')).toBeNull();
|
||||
expect(startLocalPuzzleRun).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -7414,13 +7414,13 @@ test('persisted generating puzzle draft opens generation progress after refresh'
|
||||
});
|
||||
expect(
|
||||
await screen.findByRole('progressbar', {
|
||||
name: '拼图草稿生成进度',
|
||||
name: '拼图图片生成进度',
|
||||
}),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
Number(
|
||||
screen
|
||||
.getByRole('progressbar', { name: '拼图草稿生成进度' })
|
||||
.getByRole('progressbar', { name: '拼图图片生成进度' })
|
||||
.getAttribute('aria-valuenow'),
|
||||
),
|
||||
).toBe(0);
|
||||
@@ -7479,7 +7479,7 @@ test('persisted generating puzzle draft keeps session polling on the same sessio
|
||||
|
||||
expect(
|
||||
await screen.findByRole('progressbar', {
|
||||
name: '拼图草稿生成进度',
|
||||
name: '拼图图片生成进度',
|
||||
}),
|
||||
).toBeTruthy();
|
||||
|
||||
@@ -7527,7 +7527,7 @@ test('puzzle compile timeout shows failure dialog when reread session is still g
|
||||
expect(within(dialog).getByRole('button', { name: '复制报错' })).toBeTruthy();
|
||||
expect(
|
||||
await screen.findByRole('progressbar', {
|
||||
name: '拼图草稿生成进度',
|
||||
name: '拼图图片生成进度',
|
||||
}),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -141,7 +141,6 @@ const {
|
||||
amountDelta: 10,
|
||||
balanceAfter: 10,
|
||||
sourceType: 'daily_task_reward',
|
||||
createdAt: '2026-05-03T08:01:00Z',
|
||||
},
|
||||
center: buildClaimedTaskCenter(),
|
||||
})),
|
||||
@@ -204,9 +203,9 @@ const {
|
||||
amountCents: 600,
|
||||
status: 'paid',
|
||||
paymentChannel: 'mock',
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
paidAt: '2026-04-25T10:00:00Z',
|
||||
providerTransactionId: null,
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
pointsDelta: 120,
|
||||
membershipExpiresAt: null,
|
||||
},
|
||||
@@ -237,9 +236,9 @@ const {
|
||||
amountCents: 600,
|
||||
status: 'paid',
|
||||
paymentChannel: 'wechat_mp_virtual',
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
paidAt: '2026-04-25T10:01:00Z',
|
||||
providerTransactionId: 'wx-transaction-1',
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
pointsDelta: 120,
|
||||
membershipExpiresAt: null,
|
||||
},
|
||||
@@ -276,8 +275,8 @@ const {
|
||||
amountCents: 600,
|
||||
status: 'paid',
|
||||
paymentChannel: 'wechat_mp_virtual',
|
||||
providerTransactionId: 'wx-transaction-1',
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
providerTransactionId: 'wx-transaction-1',
|
||||
paidAt: '2026-04-25T10:01:00Z',
|
||||
pointsDelta: 120,
|
||||
membershipExpiresAt: null,
|
||||
@@ -305,14 +304,12 @@ const {
|
||||
amountDelta: -1,
|
||||
balanceAfter: 29,
|
||||
sourceType: 'asset_operation_consume',
|
||||
createdAt: '2026-04-28T10:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'ledger-2',
|
||||
amountDelta: 30,
|
||||
balanceAfter: 30,
|
||||
sourceType: 'invite_invitee_reward',
|
||||
createdAt: '2026-04-28T09:00:00Z',
|
||||
},
|
||||
],
|
||||
})),
|
||||
@@ -328,6 +325,7 @@ const {
|
||||
async (code: string): Promise<PublicUserSummary> => ({
|
||||
id: `id-${code}`,
|
||||
publicUserCode: code,
|
||||
username: 'author_user',
|
||||
displayName: '公开作者',
|
||||
avatarUrl: null,
|
||||
}),
|
||||
@@ -336,6 +334,7 @@ const {
|
||||
async (userId: string): Promise<PublicUserSummary> => ({
|
||||
id: userId,
|
||||
publicUserCode: `code-${userId}`,
|
||||
username: 'author_user',
|
||||
displayName: '公开作者',
|
||||
avatarUrl: null,
|
||||
}),
|
||||
@@ -362,14 +361,12 @@ vi.mock('../../services/payment/paymentRedirect', () => ({
|
||||
mockUpdateAuthProfile.mockResolvedValue({
|
||||
id: 'user-1',
|
||||
publicUserCode: '100001',
|
||||
username: 'tester',
|
||||
displayName: '测试玩家',
|
||||
avatarUrl: null,
|
||||
phoneNumberMasked: null,
|
||||
loginMethod: 'password',
|
||||
bindingStatus: 'active',
|
||||
wechatBound: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
vi.mock('../../services/rpg-entry/rpgProfileClient', () => ({
|
||||
@@ -723,14 +720,12 @@ function ProfileHomeViewHarness({
|
||||
user: {
|
||||
id: 'user-1',
|
||||
publicUserCode: '100001',
|
||||
username: 'tester',
|
||||
displayName: '测试玩家',
|
||||
avatarUrl: null,
|
||||
phoneNumberMasked: null,
|
||||
loginMethod: 'password',
|
||||
bindingStatus: 'active',
|
||||
wechatBound: false,
|
||||
createdAt: DEFAULT_PROFILE_CREATED_AT,
|
||||
...userOverrides,
|
||||
},
|
||||
canAccessProtectedData: true,
|
||||
@@ -911,14 +906,12 @@ function renderLoggedInHomeView(
|
||||
user: {
|
||||
id: 'user-1',
|
||||
publicUserCode: '100001',
|
||||
username: 'tester',
|
||||
displayName: '测试玩家',
|
||||
avatarUrl: null,
|
||||
phoneNumberMasked: null,
|
||||
loginMethod: 'password',
|
||||
bindingStatus: 'active',
|
||||
wechatBound: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
canAccessProtectedData: true,
|
||||
openLoginModal: vi.fn(),
|
||||
@@ -1095,7 +1088,6 @@ afterEach(() => {
|
||||
amountDelta: 10,
|
||||
balanceAfter: 10,
|
||||
sourceType: 'daily_task_reward',
|
||||
createdAt: '2026-05-03T08:01:00Z',
|
||||
},
|
||||
center: mockBuildTaskCenter({
|
||||
walletBalance: 10,
|
||||
@@ -1121,14 +1113,12 @@ afterEach(() => {
|
||||
mockUpdateAuthProfile.mockResolvedValue({
|
||||
id: 'user-1',
|
||||
publicUserCode: '100001',
|
||||
username: 'tester',
|
||||
displayName: '测试玩家',
|
||||
avatarUrl: null,
|
||||
phoneNumberMasked: null,
|
||||
loginMethod: 'password',
|
||||
bindingStatus: 'active',
|
||||
wechatBound: false,
|
||||
createdAt: DEFAULT_PROFILE_CREATED_AT,
|
||||
});
|
||||
mockQrCodeToDataUrl.mockResolvedValue('data:image/png;base64,QR');
|
||||
mockRedirectToPaymentUrl.mockReset();
|
||||
@@ -1190,9 +1180,9 @@ test('profile recharge modal shows native qr code on desktop web by default', as
|
||||
amountCents: 600,
|
||||
status: 'pending' as const,
|
||||
paymentChannel: 'wechat_native',
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
paidAt: null,
|
||||
providerTransactionId: null,
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
pointsDelta: 0,
|
||||
membershipExpiresAt: null,
|
||||
},
|
||||
@@ -1255,9 +1245,9 @@ test('profile recharge modal jumps to h5 payment on mobile web by default', asyn
|
||||
amountCents: 600,
|
||||
status: 'pending' as const,
|
||||
paymentChannel: 'wechat_h5',
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
paidAt: null,
|
||||
providerTransactionId: null,
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
pointsDelta: 0,
|
||||
membershipExpiresAt: null,
|
||||
},
|
||||
@@ -1367,9 +1357,9 @@ test('profile recharge modal posts virtual payment params in mini program web-vi
|
||||
amountCents: 600,
|
||||
status: 'pending' as const,
|
||||
paymentChannel: 'wechat_mp_virtual',
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
paidAt: null as string | null,
|
||||
providerTransactionId: null,
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
pointsDelta: 0,
|
||||
membershipExpiresAt: null,
|
||||
},
|
||||
@@ -1820,9 +1810,9 @@ test('profile recharge modal waits for paid confirmation before refreshing dashb
|
||||
amountCents: 600,
|
||||
status: 'pending' as const,
|
||||
paymentChannel: 'wechat_mp_virtual',
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
paidAt: null as string | null,
|
||||
providerTransactionId: null,
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
pointsDelta: 0,
|
||||
membershipExpiresAt: null,
|
||||
},
|
||||
@@ -1859,9 +1849,9 @@ test('profile recharge modal waits for paid confirmation before refreshing dashb
|
||||
amountCents: 600,
|
||||
status: 'pending' as const,
|
||||
paymentChannel: 'wechat_mp_virtual',
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
paidAt: null,
|
||||
providerTransactionId: null,
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
pointsDelta: 0,
|
||||
membershipExpiresAt: null,
|
||||
},
|
||||
@@ -1890,9 +1880,9 @@ test('profile recharge modal waits for paid confirmation before refreshing dashb
|
||||
amountCents: 600,
|
||||
status: 'paid' as const,
|
||||
paymentChannel: 'wechat_mp_virtual',
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
paidAt: '2026-04-25T10:01:00Z',
|
||||
providerTransactionId: 'wx-transaction-2',
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
pointsDelta: 120,
|
||||
membershipExpiresAt: null,
|
||||
},
|
||||
@@ -1954,9 +1944,9 @@ test('profile recharge modal loads wechat js sdk before mini program payment bri
|
||||
amountCents: 600,
|
||||
status: 'pending' as const,
|
||||
paymentChannel: 'wechat_mp_virtual',
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
paidAt: null as string | null,
|
||||
providerTransactionId: null,
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
pointsDelta: 0,
|
||||
membershipExpiresAt: null,
|
||||
},
|
||||
@@ -2040,9 +2030,9 @@ test('profile recharge modal releases submitting state after cancelled wechat pa
|
||||
amountCents: 600,
|
||||
status: 'pending' as const,
|
||||
paymentChannel: 'wechat_mp_virtual',
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
paidAt: null as string | null,
|
||||
providerTransactionId: null,
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
pointsDelta: 0,
|
||||
membershipExpiresAt: null,
|
||||
},
|
||||
@@ -2123,9 +2113,9 @@ test('profile native qr confirmation refreshes only after server reports paid',
|
||||
amountCents: 600,
|
||||
status: 'pending' as const,
|
||||
paymentChannel: 'wechat_native',
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
paidAt: null,
|
||||
providerTransactionId: null,
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
pointsDelta: 0,
|
||||
membershipExpiresAt: null,
|
||||
},
|
||||
@@ -2157,9 +2147,9 @@ test('profile native qr confirmation refreshes only after server reports paid',
|
||||
amountCents: 600,
|
||||
status: 'paid' as const,
|
||||
paymentChannel: 'wechat_native',
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
paidAt: '2026-04-25T10:01:00Z',
|
||||
providerTransactionId: 'wx-native-1',
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
pointsDelta: 120,
|
||||
membershipExpiresAt: null,
|
||||
},
|
||||
@@ -2564,7 +2554,7 @@ test('wallet ledger modal shows empty and error states', async () => {
|
||||
test('profile community shortcut shows reward subtitle and invited users', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderProfileView(vi.fn(), {}, { createdAt: buildFreshProfileCreatedAt() });
|
||||
renderProfileView(vi.fn(), {});
|
||||
|
||||
expect(screen.queryByRole('button', { name: /邀请好友/u })).toBeNull();
|
||||
|
||||
@@ -2586,7 +2576,6 @@ test('profile page hides legacy redeem invite secondary shortcut for fresh accou
|
||||
renderProfileView(
|
||||
vi.fn(),
|
||||
{},
|
||||
{ createdAt: buildFreshProfileCreatedAt() },
|
||||
);
|
||||
|
||||
const communityButton = screen.getByRole('button', { name: /玩家社区/u });
|
||||
@@ -2618,7 +2607,7 @@ test('profile redeem invite shortcut hides after redeemed or one day old', async
|
||||
});
|
||||
unmount();
|
||||
|
||||
renderProfileView(vi.fn(), {}, { createdAt: '2026-04-01T00:00:00.000Z' });
|
||||
renderProfileView(vi.fn(), {});
|
||||
const expiredShortcutRegion = screen.getByRole('region', {
|
||||
name: '常用功能',
|
||||
});
|
||||
@@ -2678,7 +2667,6 @@ test('profile redeem invite query modal submits code after login', async () => {
|
||||
renderProfileView(
|
||||
onRechargeSuccess,
|
||||
{},
|
||||
{ createdAt: buildFreshProfileCreatedAt() },
|
||||
);
|
||||
|
||||
expect(await screen.findByLabelText('邀请码')).toBeTruthy();
|
||||
@@ -2837,14 +2825,12 @@ test('logged in create tab shows real wallet balance beside the brand', () => {
|
||||
user: {
|
||||
id: 'user-1',
|
||||
publicUserCode: '100001',
|
||||
username: 'tester',
|
||||
displayName: '测试玩家',
|
||||
avatarUrl: null,
|
||||
phoneNumberMasked: null,
|
||||
loginMethod: 'password',
|
||||
bindingStatus: 'active',
|
||||
wechatBound: false,
|
||||
createdAt: DEFAULT_PROFILE_CREATED_AT,
|
||||
},
|
||||
canAccessProtectedData: true,
|
||||
openLoginModal: vi.fn(),
|
||||
@@ -3335,14 +3321,12 @@ test('logged in recommend page uses gated recommend detail callback', async () =
|
||||
user: {
|
||||
id: 'user-1',
|
||||
publicUserCode: '100001',
|
||||
username: 'tester',
|
||||
displayName: '测试玩家',
|
||||
avatarUrl: null,
|
||||
phoneNumberMasked: null,
|
||||
loginMethod: 'password',
|
||||
bindingStatus: 'active',
|
||||
wechatBound: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
canAccessProtectedData: true,
|
||||
openLoginModal: vi.fn(),
|
||||
@@ -3471,14 +3455,12 @@ test('logged in recommend runtime preloads adjacent work previews and drag switc
|
||||
user: {
|
||||
id: 'user-1',
|
||||
publicUserCode: '100001',
|
||||
username: 'tester',
|
||||
displayName: '测试玩家',
|
||||
avatarUrl: null,
|
||||
phoneNumberMasked: null,
|
||||
loginMethod: 'password',
|
||||
bindingStatus: 'active',
|
||||
wechatBound: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
canAccessProtectedData: true,
|
||||
openLoginModal: vi.fn(),
|
||||
@@ -3648,6 +3630,7 @@ test('mobile recommend meta loads real author avatar from public user summary',
|
||||
mockGetPublicAuthUserById.mockResolvedValueOnce({
|
||||
id: 'user-2',
|
||||
publicUserCode: 'SY-00000002',
|
||||
username: 'puzzle_user',
|
||||
displayName: '拼图玩家',
|
||||
avatarUrl: 'data:image/png;base64,AUTHOR',
|
||||
});
|
||||
@@ -3907,14 +3890,12 @@ test('desktop logged in home syncs mobile home modules without square or latest
|
||||
user: {
|
||||
id: 'user-1',
|
||||
publicUserCode: '100001',
|
||||
username: 'tester',
|
||||
displayName: '测试玩家',
|
||||
avatarUrl: null,
|
||||
phoneNumberMasked: null,
|
||||
loginMethod: 'password',
|
||||
bindingStatus: 'active',
|
||||
wechatBound: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
canAccessProtectedData: true,
|
||||
openLoginModal: vi.fn(),
|
||||
|
||||
@@ -150,6 +150,7 @@ import {
|
||||
isWoodenFishGalleryEntry,
|
||||
type PlatformPublicGalleryCard,
|
||||
type PlatformWorldCardLike,
|
||||
resolvePlatformWorkAuthorDisplayName,
|
||||
resolvePlatformPublicWorkCode,
|
||||
resolvePlatformWorldCoverImage,
|
||||
resolvePlatformWorldCoverSlides,
|
||||
@@ -247,7 +248,6 @@ const AVATAR_MAX_FILE_SIZE = 5 * 1024 * 1024;
|
||||
const AVATAR_OUTPUT_SIZE = 256;
|
||||
const AVATAR_ALLOWED_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp']);
|
||||
const PLATFORM_WORK_COVER_CAROUSEL_INTERVAL_MS = 4200;
|
||||
const PROFILE_INVITE_REDEEM_ENTRY_VISIBLE_MS = 24 * 60 * 60 * 1000;
|
||||
const PROFILE_INVITE_QUERY_KEYS = ['inviteCode', 'invite_code'] as const;
|
||||
const RECOMMEND_ENTRY_SWIPE_THRESHOLD_PX = 36;
|
||||
const RECOMMEND_ENTRY_COMMIT_ANIMATION_MS = 180;
|
||||
@@ -615,6 +615,7 @@ function WorldCard({
|
||||
onClick,
|
||||
className,
|
||||
authorAvatarUrl,
|
||||
authorSummary,
|
||||
feedCardKey,
|
||||
enableCoverCarousel = false,
|
||||
isCoverCarouselActive = false,
|
||||
@@ -624,6 +625,7 @@ function WorldCard({
|
||||
onClick: () => void;
|
||||
className?: string;
|
||||
authorAvatarUrl?: string | null;
|
||||
authorSummary?: PublicUserSummary | null;
|
||||
feedCardKey?: string;
|
||||
enableCoverCarousel?: boolean;
|
||||
isCoverCarouselActive?: boolean;
|
||||
@@ -657,7 +659,10 @@ function WorldCard({
|
||||
const remixCount = getPlatformWorldRemixCount(entry);
|
||||
const likeCount = getPlatformWorldLikeCount(entry);
|
||||
const typeLabel = describePublicGalleryCardKind(entry);
|
||||
const authorName = entry.authorDisplayName.trim() || '玩家';
|
||||
const authorName = resolvePlatformWorkAuthorDisplayName(
|
||||
entry,
|
||||
authorSummary,
|
||||
);
|
||||
const authorAvatarLabel = getPublicAuthorAvatarLabel(authorName);
|
||||
const normalizedAuthorAvatarUrl = authorAvatarUrl?.trim() ?? '';
|
||||
const cardLabel = `${entry.worldName},${typeLabel},${formatCompactCount(playCount)}游玩,${formatCompactCount(remixCount)}改造,${formatCompactCount(likeCount)}点赞`;
|
||||
@@ -939,6 +944,7 @@ function RecommendRuntimePreviewCard({
|
||||
function RecommendSwipeCard({
|
||||
entry,
|
||||
authorAvatarUrl,
|
||||
authorSummary,
|
||||
isActive,
|
||||
visual,
|
||||
shareState,
|
||||
@@ -952,6 +958,7 @@ function RecommendSwipeCard({
|
||||
}: {
|
||||
entry: PlatformPublicGalleryCard;
|
||||
authorAvatarUrl?: string | null;
|
||||
authorSummary?: PublicUserSummary | null;
|
||||
isActive: boolean;
|
||||
visual: ReactNode;
|
||||
shareState?: 'idle' | 'copied' | 'failed';
|
||||
@@ -976,6 +983,7 @@ function RecommendSwipeCard({
|
||||
<RecommendRuntimeMeta
|
||||
entry={entry}
|
||||
authorAvatarUrl={authorAvatarUrl}
|
||||
authorSummary={authorSummary}
|
||||
isActive={isActive}
|
||||
shareState={shareState}
|
||||
onDragPointerDown={onDragPointerDown}
|
||||
@@ -994,6 +1002,7 @@ function RecommendSwipeCard({
|
||||
function RecommendRuntimeMeta({
|
||||
entry,
|
||||
authorAvatarUrl,
|
||||
authorSummary,
|
||||
onDragPointerDown,
|
||||
onDragPointerMove,
|
||||
onDragPointerUp,
|
||||
@@ -1006,6 +1015,7 @@ function RecommendRuntimeMeta({
|
||||
}: {
|
||||
entry: PlatformPublicGalleryCard;
|
||||
authorAvatarUrl?: string | null;
|
||||
authorSummary?: PublicUserSummary | null;
|
||||
onDragPointerDown?: (event: PointerEvent<HTMLElement>) => void;
|
||||
onDragPointerMove?: (event: PointerEvent<HTMLElement>) => void;
|
||||
onDragPointerUp?: (event: PointerEvent<HTMLElement>) => void;
|
||||
@@ -1018,7 +1028,10 @@ function RecommendRuntimeMeta({
|
||||
}) {
|
||||
const likeCount = getPlatformWorldLikeCount(entry);
|
||||
const remixCount = getPlatformWorldRemixCount(entry);
|
||||
const authorName = entry.authorDisplayName.trim() || '玩家';
|
||||
const authorName = resolvePlatformWorkAuthorDisplayName(
|
||||
entry,
|
||||
authorSummary,
|
||||
);
|
||||
const authorAvatarLabel = getPublicAuthorAvatarLabel(authorName);
|
||||
const normalizedAuthorAvatarUrl = authorAvatarUrl?.trim() ?? '';
|
||||
const displayName = formatPlatformWorkDisplayName(entry.worldName);
|
||||
@@ -1894,28 +1907,28 @@ async function getPublicWorkAuthorSummary(
|
||||
|
||||
function describePublicGalleryCardKind(entry: PlatformPublicGalleryCard) {
|
||||
if (isBigFishGalleryEntry(entry)) {
|
||||
return formatPlatformWorkDisplayTag('??');
|
||||
return formatPlatformWorkDisplayTag('大鱼吃小鱼');
|
||||
}
|
||||
if (isPuzzleGalleryEntry(entry)) {
|
||||
return formatPlatformWorkDisplayTag('??');
|
||||
return formatPlatformWorkDisplayTag('拼图');
|
||||
}
|
||||
if (isMatch3DGalleryEntry(entry)) {
|
||||
return formatPlatformWorkDisplayTag('??');
|
||||
return formatPlatformWorkDisplayTag('抓大鹅');
|
||||
}
|
||||
if (isSquareHoleGalleryEntry(entry)) {
|
||||
return formatPlatformWorkDisplayTag('??');
|
||||
return formatPlatformWorkDisplayTag('方洞挑战');
|
||||
}
|
||||
if (isJumpHopGalleryEntry(entry)) {
|
||||
return formatPlatformWorkDisplayTag('???');
|
||||
return formatPlatformWorkDisplayTag('跳一跳');
|
||||
}
|
||||
if (isWoodenFishGalleryEntry(entry)) {
|
||||
return formatPlatformWorkDisplayTag('???');
|
||||
return formatPlatformWorkDisplayTag('敲木鱼');
|
||||
}
|
||||
if (isVisualNovelGalleryEntry(entry)) {
|
||||
return formatPlatformWorkDisplayTag('??');
|
||||
return formatPlatformWorkDisplayTag('视觉小说');
|
||||
}
|
||||
if (isBarkBattleGalleryEntry(entry)) {
|
||||
return formatPlatformWorkDisplayTag('??');
|
||||
return formatPlatformWorkDisplayTag('汪汪声浪');
|
||||
}
|
||||
if (isEdutainmentGalleryEntry(entry)) {
|
||||
return formatPlatformWorkDisplayTag(entry.templateName);
|
||||
@@ -2214,21 +2227,6 @@ function formatDashboardCount(value: number) {
|
||||
return normalizedValue.toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
function isWithinProfileInviteRedeemWindow(
|
||||
createdAt: string | null | undefined,
|
||||
) {
|
||||
if (!createdAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const createdTime = new Date(createdAt).getTime();
|
||||
if (Number.isNaN(createdTime)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Date.now() - createdTime <= PROFILE_INVITE_REDEEM_ENTRY_VISIBLE_MS;
|
||||
}
|
||||
|
||||
function normalizeProfileInviteQueryCode(value: string | null | undefined) {
|
||||
return (value ?? '')
|
||||
.trim()
|
||||
@@ -2273,16 +2271,13 @@ function buildPublicUserCode(user: AuthUser | null | undefined) {
|
||||
return user.publicUserCode.trim();
|
||||
}
|
||||
|
||||
const raw =
|
||||
user?.id.replace(/[^a-zA-Z0-9]/gu, '').toUpperCase() ||
|
||||
user?.username.replace(/[^a-zA-Z0-9]/gu, '').toUpperCase() ||
|
||||
'00000000';
|
||||
const raw = user?.id.replace(/[^a-zA-Z0-9]/gu, '').toUpperCase() || '00000000';
|
||||
|
||||
return `SY-${raw.slice(-8).padStart(8, '0')}`;
|
||||
}
|
||||
|
||||
function getUserAvatarLabel(user: AuthUser | null | undefined) {
|
||||
return (user?.displayName || user?.username || '叙')
|
||||
return (user?.displayName || '叙')
|
||||
.slice(0, 1)
|
||||
.toUpperCase();
|
||||
}
|
||||
@@ -4198,6 +4193,17 @@ export function RpgEntryHomeView({
|
||||
},
|
||||
[publicAuthorSummariesByKey],
|
||||
);
|
||||
const getPublicEntryAuthorSummary = useCallback(
|
||||
(entry: PlatformPublicGalleryCard) => {
|
||||
const authorLookupKey = buildPublicWorkAuthorLookupKey(entry);
|
||||
if (!authorLookupKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return publicAuthorSummariesByKey[authorLookupKey] ?? null;
|
||||
},
|
||||
[publicAuthorSummariesByKey],
|
||||
);
|
||||
const activeCategoryGroup =
|
||||
categoryGroups.find((group) => group.tag === selectedCategoryTag) ??
|
||||
categoryGroups[0] ??
|
||||
@@ -4930,18 +4936,14 @@ export function RpgEntryHomeView({
|
||||
});
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (
|
||||
activeTab !== 'profile' ||
|
||||
!isAuthenticated ||
|
||||
!isWithinProfileInviteRedeemWindow(authUi?.user?.createdAt)
|
||||
) {
|
||||
if (activeTab !== 'profile' || !isAuthenticated) {
|
||||
setIsReferralCenterInitialized(false);
|
||||
setReferralCenter(null);
|
||||
return;
|
||||
}
|
||||
|
||||
loadReferralCenter();
|
||||
}, [activeTab, authUi?.user?.createdAt, isAuthenticated, loadReferralCenter]);
|
||||
}, [activeTab, isAuthenticated, loadReferralCenter]);
|
||||
const openProfilePopupPanel = (panel: ProfileReferralPanel) => {
|
||||
setProfilePopupPanel(panel);
|
||||
setReferralError(null);
|
||||
@@ -5589,6 +5591,9 @@ export function RpgEntryHomeView({
|
||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(
|
||||
previousRecommendEntry,
|
||||
)}
|
||||
authorSummary={getPublicEntryAuthorSummary(
|
||||
previousRecommendEntry,
|
||||
)}
|
||||
isActive={false}
|
||||
visual={
|
||||
<RecommendRuntimePreviewCard
|
||||
@@ -5606,6 +5611,9 @@ export function RpgEntryHomeView({
|
||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(
|
||||
activeRecommendEntry,
|
||||
)}
|
||||
authorSummary={getPublicEntryAuthorSummary(
|
||||
activeRecommendEntry,
|
||||
)}
|
||||
isActive
|
||||
visual={
|
||||
<div className="platform-recommend-runtime-viewport">
|
||||
@@ -5630,6 +5638,9 @@ export function RpgEntryHomeView({
|
||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(
|
||||
nextRecommendEntry,
|
||||
)}
|
||||
authorSummary={getPublicEntryAuthorSummary(
|
||||
nextRecommendEntry,
|
||||
)}
|
||||
isActive={false}
|
||||
visual={
|
||||
<RecommendRuntimePreviewCard
|
||||
@@ -5783,6 +5794,7 @@ export function RpgEntryHomeView({
|
||||
onClick={() => onOpenGalleryDetail(entry)}
|
||||
className="w-full"
|
||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
|
||||
authorSummary={getPublicEntryAuthorSummary(entry)}
|
||||
feedCardKey={cardKey}
|
||||
/>
|
||||
);
|
||||
@@ -5844,13 +5856,14 @@ export function RpgEntryHomeView({
|
||||
return (
|
||||
<WorldCard
|
||||
key={`${cardKey}:mobile-feed:${discoverChannel}`}
|
||||
entry={entry}
|
||||
onClick={() => onOpenGalleryDetail(entry)}
|
||||
className="w-full"
|
||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
|
||||
feedCardKey={cardKey}
|
||||
enableCoverCarousel={mobileFeedCarouselEnabled}
|
||||
isCoverCarouselActive={
|
||||
entry={entry}
|
||||
onClick={() => onOpenGalleryDetail(entry)}
|
||||
className="w-full"
|
||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
|
||||
authorSummary={getPublicEntryAuthorSummary(entry)}
|
||||
feedCardKey={cardKey}
|
||||
enableCoverCarousel={mobileFeedCarouselEnabled}
|
||||
isCoverCarouselActive={
|
||||
mobileCenteredCardKey === cardKey
|
||||
}
|
||||
/>
|
||||
@@ -5954,6 +5967,7 @@ export function RpgEntryHomeView({
|
||||
onClick={() => openRecommendGalleryDetail(entry)}
|
||||
className="w-full min-w-0"
|
||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
|
||||
authorSummary={getPublicEntryAuthorSummary(entry)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -5981,6 +5995,7 @@ export function RpgEntryHomeView({
|
||||
onClick={() => openRecommendGalleryDetail(entry)}
|
||||
className="w-full min-w-0"
|
||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
|
||||
authorSummary={getPublicEntryAuthorSummary(entry)}
|
||||
/>
|
||||
))}
|
||||
{onOpenChildMotionDemo ? (
|
||||
@@ -6041,6 +6056,7 @@ export function RpgEntryHomeView({
|
||||
onClick={() => openRecommendGalleryDetail(entry)}
|
||||
className="w-full min-w-0"
|
||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
|
||||
authorSummary={getPublicEntryAuthorSummary(entry)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -6556,6 +6572,7 @@ export function RpgEntryHomeView({
|
||||
onClick={() => openRecommendGalleryDetail(entry)}
|
||||
className="w-full min-w-0"
|
||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
|
||||
authorSummary={getPublicEntryAuthorSummary(entry)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -6726,6 +6743,7 @@ export function RpgEntryHomeView({
|
||||
onClick={() => openRecommendGalleryDetail(entry)}
|
||||
className="w-full min-w-0"
|
||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
|
||||
authorSummary={getPublicEntryAuthorSummary(entry)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
mapWoodenFishWorkToPlatformGalleryCard,
|
||||
type PlatformEdutainmentGalleryCard,
|
||||
type PlatformPuzzleGalleryCard,
|
||||
resolvePlatformWorkAuthorDisplayName,
|
||||
resolvePlatformPublicWorkCode,
|
||||
resolvePlatformWorldFallbackCoverImage,
|
||||
} from './rpgEntryWorldPresentation';
|
||||
@@ -197,6 +198,45 @@ test('maps wooden fish work to platform gallery card with WF public code', () =>
|
||||
expect(buildPlatformWorldDisplayTags(card, 2)).toEqual(['敲木鱼']);
|
||||
});
|
||||
|
||||
test('resolves public work author from display name and public user code before stored author name', () => {
|
||||
const card = mapWoodenFishWorkToPlatformGalleryCard({
|
||||
publicWorkCode: 'WF-AUTHOR1',
|
||||
workId: 'wooden-fish-work-author',
|
||||
profileId: 'wooden-fish-profile-author',
|
||||
ownerUserId: 'user-author',
|
||||
authorDisplayName: '敲木鱼玩家',
|
||||
workTitle: '莲花木鱼',
|
||||
workDescription: '莲花主题敲木鱼。',
|
||||
coverImageSrc: null,
|
||||
themeTags: ['敲木鱼'],
|
||||
publicationStatus: 'published',
|
||||
playCount: 0,
|
||||
updatedAt: '2026-05-20T00:00:00.000Z',
|
||||
publishedAt: '2026-05-20T00:00:00.000Z',
|
||||
generationStatus: 'ready',
|
||||
});
|
||||
|
||||
expect(
|
||||
resolvePlatformWorkAuthorDisplayName(card, {
|
||||
id: 'user_00000004',
|
||||
publicUserCode: 'SY-00000004',
|
||||
username: 'phone_00000004',
|
||||
displayName: '公开昵称',
|
||||
avatarUrl: null,
|
||||
}),
|
||||
).toBe('公开昵称 · SY-00000004');
|
||||
expect(
|
||||
resolvePlatformWorkAuthorDisplayName(card, {
|
||||
id: 'user_00000004',
|
||||
publicUserCode: '',
|
||||
username: 'phone_00000004',
|
||||
displayName: '公开昵称',
|
||||
avatarUrl: null,
|
||||
}),
|
||||
).toBe('公开昵称');
|
||||
expect(resolvePlatformWorkAuthorDisplayName(card, null)).toBe('敲木鱼玩家');
|
||||
});
|
||||
|
||||
test('keeps baby object match public card code and template label intact', () => {
|
||||
const card: PlatformEdutainmentGalleryCard = {
|
||||
sourceType: 'edutainment',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle';
|
||||
import type { PublicUserSummary } from '../../../packages/shared/src/contracts/auth';
|
||||
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
|
||||
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
|
||||
import { BABY_OBJECT_MATCH_EDUTAINMENT_TAG } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
|
||||
@@ -219,6 +220,7 @@ export type PlatformWoodenFishGalleryCard = {
|
||||
sourceSessionId?: string | null;
|
||||
publicWorkCode: string;
|
||||
ownerUserId: string;
|
||||
authorUsername?: string | null;
|
||||
authorDisplayName: string;
|
||||
worldName: string;
|
||||
subtitle: string;
|
||||
@@ -562,6 +564,10 @@ export function mapWoodenFishWorkToPlatformGalleryCard(
|
||||
? summary.publicWorkCode
|
||||
: buildWoodenFishPublicWorkCode(summary.profileId),
|
||||
ownerUserId: summary.ownerUserId,
|
||||
authorUsername:
|
||||
'authorUsername' in summary && typeof summary.authorUsername === 'string'
|
||||
? summary.authorUsername
|
||||
: null,
|
||||
authorDisplayName:
|
||||
'authorDisplayName' in summary ? summary.authorDisplayName : '玩家',
|
||||
worldName: summary.workTitle,
|
||||
@@ -857,6 +863,16 @@ export function formatPlatformWorkDisplayTags(
|
||||
].slice(0, limit);
|
||||
}
|
||||
|
||||
export function resolvePlatformWorkAuthorDisplayName(
|
||||
entry: PlatformPublicGalleryCard,
|
||||
authorSummary?: PublicUserSummary | null,
|
||||
) {
|
||||
const displayName = authorSummary?.displayName?.trim();
|
||||
const publicUserCode = authorSummary?.publicUserCode?.trim();
|
||||
|
||||
return displayName || publicUserCode || entry.authorDisplayName.trim() || '玩家';
|
||||
}
|
||||
|
||||
export function buildPlatformWorldDisplayTags(
|
||||
entry: PlatformWorldCardLike,
|
||||
limit = 3,
|
||||
@@ -1063,4 +1079,4 @@ function buildBarkBattleThemeTags(work: BarkBattleWorkSummary) {
|
||||
.map((tag) => tag.trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, 3);
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,6 @@ type WoodenFishWorkspaceFormState = {
|
||||
floatingWords: string[];
|
||||
};
|
||||
|
||||
const DEFAULT_WORK_TITLE = '今日敲木鱼';
|
||||
const DEFAULT_THEME_TAGS = ['敲木鱼', '解压'];
|
||||
const DEFAULT_FLOATING_WORDS = ['幸运'];
|
||||
const MAX_FLOATING_WORD_COUNT = 8;
|
||||
@@ -309,8 +308,9 @@ export function WoodenFishWorkspace({
|
||||
try {
|
||||
const payload: WoodenFishWorkspaceCreateRequest = {
|
||||
templateId: 'wooden-fish',
|
||||
workTitle: DEFAULT_WORK_TITLE,
|
||||
workDescription: '',
|
||||
workTitle: '',
|
||||
workDescription:
|
||||
formState.hitObjectPrompt.trim() || WOODEN_FISH_DEFAULT_HIT_OBJECT_PROMPT,
|
||||
themeTags: DEFAULT_THEME_TAGS,
|
||||
hitObjectPrompt:
|
||||
formState.hitObjectPrompt.trim() || WOODEN_FISH_DEFAULT_HIT_OBJECT_PROMPT,
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { fetchJson, parseApiErrorMessage, saveJsonObject } from './jsonClient';
|
||||
import {
|
||||
appendApiErrorRequestId,
|
||||
fetchJson,
|
||||
parseApiErrorMessage,
|
||||
saveJsonObject,
|
||||
} from './jsonClient';
|
||||
|
||||
export const EDITOR_API_BASE_PATH = '/api/editor';
|
||||
export const ASSETS_API_BASE_PATH = '/api/assets';
|
||||
@@ -69,7 +74,7 @@ export async function postApiJson<T>(
|
||||
const responseText = await response.text();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(parseApiErrorMessage(responseText, fallbackMessage));
|
||||
throw new Error(appendApiErrorRequestId(parseApiErrorMessage(responseText, fallbackMessage), response.headers.get('x-request-id')));
|
||||
}
|
||||
|
||||
return responseText ? (JSON.parse(responseText) as T) : ({} as T);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
API_RESPONSE_ENVELOPE_HEADER,
|
||||
API_RESPONSE_ENVELOPE_VERSION,
|
||||
appendApiErrorRequestId,
|
||||
parseApiErrorMessage,
|
||||
unwrapApiResponse,
|
||||
} from '../../../packages/shared/src/http';
|
||||
@@ -17,7 +18,15 @@ export async function fetchJson<T>(
|
||||
const responseText = await response.text();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(parseApiErrorMessage(responseText, `${fallbackMessage}: ${response.status}`));
|
||||
throw new Error(
|
||||
appendApiErrorRequestId(
|
||||
parseApiErrorMessage(
|
||||
responseText,
|
||||
`${fallbackMessage}: ${response.status}`,
|
||||
),
|
||||
response.headers.get('x-request-id'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return responseText
|
||||
@@ -41,8 +50,13 @@ export async function saveJsonObject(
|
||||
const responseText = await response.text();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(parseApiErrorMessage(responseText, fallbackMessage));
|
||||
throw new Error(
|
||||
appendApiErrorRequestId(
|
||||
parseApiErrorMessage(responseText, fallbackMessage),
|
||||
response.headers.get('x-request-id'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { parseApiErrorMessage };
|
||||
export { appendApiErrorRequestId, parseApiErrorMessage };
|
||||
|
||||
@@ -14,7 +14,10 @@ import type {
|
||||
GenerateCustomWorldProfileInput,
|
||||
GenerateCustomWorldProfileOptions,
|
||||
} from '../../packages/shared/src/contracts/runtime';
|
||||
import { parseApiErrorMessage } from '../../packages/shared/src/http';
|
||||
import {
|
||||
appendApiErrorRequestId,
|
||||
parseApiErrorMessage,
|
||||
} from '../../packages/shared/src/http';
|
||||
import type {
|
||||
AIResponse,
|
||||
Character,
|
||||
@@ -93,7 +96,12 @@ async function requestPlainTextStream(
|
||||
|
||||
if (!response.ok) {
|
||||
const responseText = await response.text();
|
||||
throw new Error(parseApiErrorMessage(responseText, '流式请求失败'));
|
||||
throw new Error(
|
||||
appendApiErrorRequestId(
|
||||
parseApiErrorMessage(responseText, '流式请求失败'),
|
||||
response.headers.get('x-request-id'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
@@ -488,7 +496,12 @@ export async function streamNpcChatTurn(
|
||||
|
||||
if (!response.ok) {
|
||||
const responseText = await response.text();
|
||||
throw new Error(parseApiErrorMessage(responseText, 'NPC 聊天续写失败'));
|
||||
throw new Error(
|
||||
appendApiErrorRequestId(
|
||||
parseApiErrorMessage(responseText, 'NPC 聊天续写失败'),
|
||||
response.headers.get('x-request-id'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
|
||||
@@ -547,6 +547,7 @@ describe('apiClient', () => {
|
||||
routeVersion: 'runtime.v2',
|
||||
},
|
||||
});
|
||||
expect((capturedError as Error).message).toContain('requestId: req-body');
|
||||
});
|
||||
|
||||
it('uses api error details.message as ApiClientError message', async () => {
|
||||
|
||||
@@ -671,9 +671,14 @@ async function buildApiClientError(
|
||||
) {
|
||||
const responseText = await response.text();
|
||||
const parsedError = parseApiErrorShape(responseText);
|
||||
const requestId =
|
||||
parsedError?.meta.requestId ??
|
||||
response.headers.get(REQUEST_ID_HEADER) ??
|
||||
undefined;
|
||||
const baseMessage = parseApiErrorMessage(responseText, fallbackMessage);
|
||||
|
||||
return new ApiClientError({
|
||||
message: parseApiErrorMessage(responseText, fallbackMessage),
|
||||
message: requestId ? `${baseMessage}(requestId: ${requestId})` : baseMessage,
|
||||
status: response.status,
|
||||
code: parsedError?.code ?? `HTTP_${response.status || 0}`,
|
||||
details: parsedError?.details ?? null,
|
||||
@@ -682,10 +687,7 @@ async function buildApiClientError(
|
||||
parsedError?.meta.apiVersion ??
|
||||
response.headers.get(API_VERSION_HEADER) ??
|
||||
API_VERSION,
|
||||
requestId:
|
||||
parsedError?.meta.requestId ??
|
||||
response.headers.get(REQUEST_ID_HEADER) ??
|
||||
undefined,
|
||||
requestId,
|
||||
routeVersion:
|
||||
parsedError?.meta.routeVersion ??
|
||||
response.headers.get(ROUTE_VERSION_HEADER) ??
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { parseApiErrorMessage } from '../../packages/shared/src/http';
|
||||
import {
|
||||
appendApiErrorRequestId,
|
||||
parseApiErrorMessage,
|
||||
} from '../../packages/shared/src/http';
|
||||
import {
|
||||
ApiClientError,
|
||||
BACKGROUND_AUTH_REQUEST_OPTIONS,
|
||||
@@ -338,7 +341,12 @@ export async function readAssetBytes(
|
||||
if (!response.ok) {
|
||||
const message = await response
|
||||
.text()
|
||||
.then((text) => parseApiErrorMessage(text, '读取资源内容失败'))
|
||||
.then((text) =>
|
||||
appendApiErrorRequestId(
|
||||
parseApiErrorMessage(text, '读取资源内容失败'),
|
||||
response.headers.get('x-request-id'),
|
||||
),
|
||||
)
|
||||
.catch(() => '');
|
||||
throw new Error(message || '读取资源内容失败');
|
||||
}
|
||||
|
||||
@@ -91,14 +91,12 @@ describe('authService', () => {
|
||||
user: {
|
||||
id: 'user_1',
|
||||
publicUserCode: 'SY-00000001',
|
||||
username: 'phone_00000001',
|
||||
displayName: '138****8000',
|
||||
avatarUrl: null,
|
||||
phoneNumberMasked: '138****8000',
|
||||
loginMethod: 'password',
|
||||
bindingStatus: 'active',
|
||||
wechatBound: false,
|
||||
createdAt: '2026-05-01T00:00:00.000Z',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -128,14 +126,12 @@ describe('authService', () => {
|
||||
user: {
|
||||
id: 'user_1',
|
||||
publicUserCode: 'SY-00000001',
|
||||
username: 'phone_00000001',
|
||||
displayName: '旅人甲',
|
||||
avatarUrl: 'data:image/png;base64,AAAA',
|
||||
phoneNumberMasked: '138****8000',
|
||||
loginMethod: 'password',
|
||||
bindingStatus: 'active',
|
||||
wechatBound: false,
|
||||
createdAt: '2026-05-01T00:00:00.000Z',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -167,14 +163,12 @@ describe('authService', () => {
|
||||
user: {
|
||||
id: 'user_1',
|
||||
publicUserCode: 'SY-00000001',
|
||||
username: 'phone_00000001',
|
||||
displayName: '旅人甲',
|
||||
avatarUrl: null,
|
||||
phoneNumberMasked: '138****8000',
|
||||
loginMethod: 'password',
|
||||
bindingStatus: 'active',
|
||||
wechatBound: false,
|
||||
createdAt: '2026-05-01T00:00:00.000Z',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -254,14 +248,12 @@ describe('authService', () => {
|
||||
user: {
|
||||
id: 'user_phone',
|
||||
publicUserCode: 'SY-00000004',
|
||||
username: '138****8000',
|
||||
displayName: '138****8000',
|
||||
avatarUrl: null,
|
||||
phoneNumberMasked: '138****8000',
|
||||
loginMethod: 'phone',
|
||||
bindingStatus: 'active',
|
||||
wechatBound: false,
|
||||
createdAt: '2026-05-01T00:00:00.000Z',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -271,7 +263,7 @@ describe('authService', () => {
|
||||
'spring-2026',
|
||||
);
|
||||
|
||||
expect(response.user.username).toBe('138****8000');
|
||||
expect(response.user.displayName).toBe('138****8000');
|
||||
expect(apiClientMocks.requestJson).toHaveBeenCalledWith(
|
||||
'/api/auth/phone/login',
|
||||
expect.objectContaining({
|
||||
@@ -333,14 +325,12 @@ describe('authService', () => {
|
||||
user: {
|
||||
id: 'user_wechat',
|
||||
publicUserCode: 'SY-00000005',
|
||||
username: '138****8000',
|
||||
displayName: '138****8000',
|
||||
avatarUrl: null,
|
||||
phoneNumberMasked: '138****8000',
|
||||
loginMethod: 'wechat',
|
||||
bindingStatus: 'active',
|
||||
wechatBound: true,
|
||||
createdAt: '2026-05-01T00:00:00.000Z',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -356,14 +346,12 @@ describe('authService', () => {
|
||||
user: {
|
||||
id: 'user_phone',
|
||||
publicUserCode: 'SY-00000006',
|
||||
username: '139****9000',
|
||||
displayName: '139****9000',
|
||||
avatarUrl: null,
|
||||
phoneNumberMasked: '139****9000',
|
||||
loginMethod: 'phone',
|
||||
bindingStatus: 'active',
|
||||
wechatBound: false,
|
||||
createdAt: '2026-05-01T00:00:00.000Z',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { parseApiErrorMessage } from '../../../packages/shared/src/http';
|
||||
import { appendApiErrorRequestId, parseApiErrorMessage } from '../../../packages/shared/src/http';
|
||||
import type { TextStreamOptions } from '../aiTypes';
|
||||
import {
|
||||
type ApiRetryOptions,
|
||||
@@ -64,7 +64,7 @@ async function openCreationAgentSsePost(
|
||||
|
||||
if (!response.ok) {
|
||||
const responseText = await response.text();
|
||||
throw new Error(parseApiErrorMessage(responseText, fallbackMessage));
|
||||
throw new Error(appendApiErrorRequestId(parseApiErrorMessage(responseText, fallbackMessage), response.headers.get('x-request-id')));
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
CreativeDraftEditStreamRequest,
|
||||
StreamCreativeAgentMessageRequest,
|
||||
} from '../../../packages/shared/src/contracts/creativeAgent';
|
||||
import { parseApiErrorMessage } from '../../../packages/shared/src/http';
|
||||
import { appendApiErrorRequestId, parseApiErrorMessage } from '../../../packages/shared/src/http';
|
||||
import type { TextStreamOptions } from '../aiTypes';
|
||||
import { fetchWithApiAuth, requestJson } from '../apiClient';
|
||||
import {
|
||||
@@ -42,7 +42,7 @@ async function openCreativeAgentSsePost(
|
||||
|
||||
if (!response.ok) {
|
||||
const responseText = await response.text();
|
||||
throw new Error(parseApiErrorMessage(responseText, fallbackMessage));
|
||||
throw new Error(appendApiErrorRequestId(parseApiErrorMessage(responseText, fallbackMessage), response.headers.get('x-request-id')));
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { parseApiErrorMessage } from '../../../packages/shared/src/http';
|
||||
import { appendApiErrorRequestId, parseApiErrorMessage } from '../../../packages/shared/src/http';
|
||||
import { fetchWithApiAuth, requestJson } from '../apiClient';
|
||||
|
||||
export async function requestRpgCreationPostJson<T>(
|
||||
@@ -32,7 +32,7 @@ export async function openRpgCreationSsePost(
|
||||
|
||||
if (!response.ok) {
|
||||
const responseText = await response.text();
|
||||
throw new Error(parseApiErrorMessage(responseText, fallbackMessage));
|
||||
throw new Error(appendApiErrorRequestId(parseApiErrorMessage(responseText, fallbackMessage), response.headers.get('x-request-id')));
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user