统一 Rust 与 TypeScript 格式化门禁

纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口

完成项目 TypeScript/Prettier 与 Rust 全量格式化

修复 Pingora expected executable 门禁的空白敏感误报

同步开发运维文档与 AGC skill pack 格式化忽略规则
This commit is contained in:
2026-09-01 16:26:20 +08:00
parent ae79d50b14
commit 071faa482c
1051 changed files with 28412 additions and 19834 deletions
@@ -8,13 +8,9 @@ import { RpgEntryBrandLogo } from './RpgEntryBrandLogo';
test('陶泥儿品牌标识展示统一产品形象', () => {
const { container } = render(<RpgEntryBrandLogo />);
expect(screen.getByRole('img', { name: '陶泥儿 GENARRATIVE' })).toBeTruthy();
expect(
screen.getByRole('img', { name: '陶泥儿 GENARRATIVE' }),
).toBeTruthy();
expect(
container
.querySelector('.platform-brand-logo__image')
?.getAttribute('src'),
container.querySelector('.platform-brand-logo__image')?.getAttribute('src'),
).toBe('/branding/taonier-product-ip.png');
});
@@ -4,9 +4,7 @@ import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, expect, test, vi } from 'vitest';
import {
buildCustomWorldPlayableCharacters,
} from '../../data/characterPresets';
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
import {
type Character,
type CustomWorldProfile,
@@ -129,9 +127,8 @@ test('custom world character selection stays stable when character ids are empty
this.dispatchEvent(new Event('scroll'));
};
vi
.spyOn(HTMLElement.prototype, 'getBoundingClientRect')
.mockImplementation(function mockGetBoundingClientRect(this: HTMLElement) {
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(
function mockGetBoundingClientRect(this: HTMLElement) {
if ((this as HTMLElement).dataset.carouselCard === 'true') {
return {
width: 240,
@@ -157,57 +154,60 @@ test('custom world character selection stays stable when character ids are empty
y: 0,
toJSON: () => ({}),
} as DOMRect;
});
},
);
render(
<RpgEntryCharacterSelectView
worldType={WorldType.CUSTOM}
customWorldProfile={{
attributeSchema: {
id: 'schema:custom:test',
worldId: 'custom:test',
schemaVersion: 1,
generatedFrom: {
worldType: WorldType.CUSTOM,
worldName: '潮城',
settingSummary: '潮水与迷雾交织的港城。',
tone: '潮湿、危险、带着试探。',
conflictCore: '在涨落之间抢先一步。',
customWorldProfile={
{
attributeSchema: {
id: 'schema:custom:test',
worldId: 'custom:test',
schemaVersion: 1,
generatedFrom: {
worldType: WorldType.CUSTOM,
worldName: '潮城',
settingSummary: '潮水与迷雾交织的港城。',
tone: '潮湿、危险、带着试探。',
conflictCore: '在涨落之间抢先一步。',
},
slots: [
{
slotId: 'axis_a',
name: '潮骨',
},
{
slotId: 'axis_b',
name: '浪步',
},
{
slotId: 'axis_c',
name: '舟识',
},
{
slotId: 'axis_d',
name: '潮魄',
},
{
slotId: 'axis_e',
name: '契汐',
},
{
slotId: 'axis_f',
name: '回澜',
},
],
},
slots: [
{
slotId: 'axis_a',
name: '潮骨',
},
{
slotId: 'axis_b',
name: '浪步',
},
{
slotId: 'axis_c',
name: '舟识',
},
{
slotId: 'axis_d',
name: '潮魄',
},
{
slotId: 'axis_e',
name: '契汐',
},
{
slotId: 'axis_f',
name: '回澜',
},
],
},
} as unknown as CustomWorldProfile}
} as unknown as CustomWorldProfile
}
onBack={handleBack}
onConfirm={handleConfirm}
/>,
);
const backButton = screen.getByRole('button', {name: '返回'});
const backButton = screen.getByRole('button', { name: '返回' });
expect(backButton.className).toContain('platform-action-button--editor-dark');
expect(backButton.className).toContain('rounded-full');
expect(backButton.className).toContain('px-3');
@@ -223,7 +223,9 @@ test('custom world character selection stays stable when character ids are empty
await user.click(screen.getByRole('button', { name: /闻潮/u }));
await waitFor(() => {
expect(screen.getByRole('button', { name: /查看闻潮的详情/u })).toBeTruthy();
expect(
screen.getByRole('button', { name: /查看闻潮的详情/u }),
).toBeTruthy();
});
await user.click(screen.getByRole('button', { name: /进入营地/u }));
@@ -238,8 +240,8 @@ test('custom world character selection stays stable when character ids are empty
const duplicateKeyCalls = consoleErrorSpy.mock.calls.filter((call) =>
call.some(
(arg) =>
typeof arg === 'string'
&& arg.includes('Encountered two children with the same key'),
typeof arg === 'string' &&
arg.includes('Encountered two children with the same key'),
),
);
@@ -257,30 +259,32 @@ test('custom world character selection falls back instead of rendering a blank s
render(
<RpgEntryCharacterSelectView
worldType={WorldType.CUSTOM}
customWorldProfile={{
id: 'broken-profile',
name: '坏数据',
attributeSchema: {
id: 'schema:custom:fallback',
worldId: 'broken-profile',
schemaVersion: 1,
generatedFrom: {
worldType: WorldType.CUSTOM,
worldName: '坏数据',
settingSummary: '坏数据',
tone: '测试',
conflictCore: '测试',
customWorldProfile={
{
id: 'broken-profile',
name: '坏数据',
attributeSchema: {
id: 'schema:custom:fallback',
worldId: 'broken-profile',
schemaVersion: 1,
generatedFrom: {
worldType: WorldType.CUSTOM,
worldName: '坏数据',
settingSummary: '坏数据',
tone: '测试',
conflictCore: '测试',
},
slots: [
{ slotId: 'axis_a', name: '骨势' },
{ slotId: 'axis_b', name: '身法' },
{ slotId: 'axis_c', name: '眼脉' },
{ slotId: 'axis_d', name: '心焰' },
{ slotId: 'axis_e', name: '尘缘' },
{ slotId: 'axis_f', name: '玄息' },
],
},
slots: [
{ slotId: 'axis_a', name: '骨势' },
{ slotId: 'axis_b', name: '身法' },
{ slotId: 'axis_c', name: '眼脉' },
{ slotId: 'axis_d', name: '心焰' },
{ slotId: 'axis_e', name: '尘缘' },
{ slotId: 'axis_f', name: '玄息' },
],
},
} as unknown as CustomWorldProfile}
} as unknown as CustomWorldProfile
}
onBack={handleBack}
onConfirm={() => {}}
/>,
@@ -290,6 +294,6 @@ test('custom world character selection falls back instead of rendering a blank s
expect(screen.getAllByText('兜底侠').length).toBeGreaterThan(0);
expect(screen.getByRole('button', { name: /进入营地/u })).toBeTruthy();
await user.click(screen.getByRole('button', {name: '返回'}));
await user.click(screen.getByRole('button', { name: '返回' }));
expect(handleBack).toHaveBeenCalledTimes(1);
});
@@ -1,8 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
buildCharacterAttributeProfile,
} from '../../data/attributeProfileGenerator';
import { buildCharacterAttributeProfile } from '../../data/attributeProfileGenerator';
import {
resolveAttributeSchema,
resolveCharacterAttributeProfile,
@@ -105,10 +103,10 @@ function buildSelectionCharacterKey(character: Character, index: number) {
}
const fallbackSeed =
character.name.trim()
|| character.title.trim()
|| character.description.trim()
|| 'character';
character.name.trim() ||
character.title.trim() ||
character.description.trim() ||
'character';
return `selection-character-${index}-${fallbackSeed}`;
}
@@ -141,14 +139,21 @@ function applyCharacterSelectionDraft(
function getPersonalityTags(personality: string) {
const tags = personality
.split(/[,.!?/\\\s]+/u)
.map(tag => tag.trim())
.map((tag) => tag.trim())
.filter(Boolean);
return tags.length > 0 ? [...new Set(tags)] : [personality.trim()].filter(Boolean);
return tags.length > 0
? [...new Set(tags)]
: [personality.trim()].filter(Boolean);
}
function readCarouselProgress(container: HTMLDivElement, orientation: CarouselOrientation) {
const firstCard = container.querySelector<HTMLElement>('[data-carousel-card="true"]');
function readCarouselProgress(
container: HTMLDivElement,
orientation: CarouselOrientation,
) {
const firstCard = container.querySelector<HTMLElement>(
'[data-carousel-card="true"]',
);
if (!firstCard) return 0;
const styles = window.getComputedStyle(container);
@@ -157,17 +162,26 @@ function readCarouselProgress(container: HTMLDivElement, orientation: CarouselOr
? styles.rowGap || styles.gap || '0'
: styles.columnGap || styles.gap || '0',
);
const stride = orientation === 'vertical'
? firstCard.getBoundingClientRect().height + gap
: firstCard.getBoundingClientRect().width + gap;
const stride =
orientation === 'vertical'
? firstCard.getBoundingClientRect().height + gap
: firstCard.getBoundingClientRect().width + gap;
if (stride <= 0) return 0;
return orientation === 'vertical' ? container.scrollTop / stride : container.scrollLeft / stride;
return orientation === 'vertical'
? container.scrollTop / stride
: container.scrollLeft / stride;
}
function scrollCarouselToIndex(container: HTMLDivElement | null, index: number, orientation: CarouselOrientation) {
function scrollCarouselToIndex(
container: HTMLDivElement | null,
index: number,
orientation: CarouselOrientation,
) {
if (!container) return;
const firstCard = container.querySelector<HTMLElement>('[data-carousel-card="true"]');
const firstCard = container.querySelector<HTMLElement>(
'[data-carousel-card="true"]',
);
if (!firstCard) return;
const styles = window.getComputedStyle(container);
@@ -176,15 +190,16 @@ function scrollCarouselToIndex(container: HTMLDivElement | null, index: number,
? styles.rowGap || styles.gap || '0'
: styles.columnGap || styles.gap || '0',
);
const stride = orientation === 'vertical'
? firstCard.getBoundingClientRect().height + gap
: firstCard.getBoundingClientRect().width + gap;
const stride =
orientation === 'vertical'
? firstCard.getBoundingClientRect().height + gap
: firstCard.getBoundingClientRect().width + gap;
const behavior: ScrollBehavior = 'smooth';
if (orientation === 'vertical') {
container.scrollTo({top: stride * index, behavior});
container.scrollTo({ top: stride * index, behavior });
} else {
container.scrollTo({left: stride * index, behavior});
container.scrollTo({ left: stride * index, behavior });
}
}
@@ -216,7 +231,7 @@ function getCharacterCardStyle(index: number, progress: number) {
};
}
function CharacterSelectBackButton({onBack}: {onBack: () => void}) {
function CharacterSelectBackButton({ onBack }: { onBack: () => void }) {
return (
<PlatformActionButton
surface="editorDark"
@@ -249,30 +264,46 @@ export function RpgEntryCharacterSelectView({
})),
[selectionCharacters],
);
const [selectedCharacterKey, setSelectedCharacterKey] = useState(selectionEntries[0]?.selectionKey ?? '');
const [detailCharacter, setDetailCharacter] = useState<Character | null>(null);
const [selectedCharacterKey, setSelectedCharacterKey] = useState(
selectionEntries[0]?.selectionKey ?? '',
);
const [detailCharacter, setDetailCharacter] = useState<Character | null>(
null,
);
const characterCarouselRef = useRef<HTMLDivElement | null>(null);
const [characterCarouselProgress, setCharacterCarouselProgress] = useState(0);
const [showCharacterDraftModal, setShowCharacterDraftModal] = useState(false);
const [characterDraftName, setCharacterDraftName] = useState('');
const [characterDraftBackstory, setCharacterDraftBackstory] = useState('');
const [characterDraftError, setCharacterDraftError] = useState<string | null>(null);
const [characterSelectionDrafts, setCharacterSelectionDrafts] = useState<Record<string, CharacterSelectionDraft>>({});
const [characterDraftError, setCharacterDraftError] = useState<string | null>(
null,
);
const [characterSelectionDrafts, setCharacterSelectionDrafts] = useState<
Record<string, CharacterSelectionDraft>
>({});
const selectedCharacterEntry = useMemo(
() => selectionEntries.find(entry => entry.selectionKey === selectedCharacterKey) ?? selectionEntries[0] ?? null,
() =>
selectionEntries.find(
(entry) => entry.selectionKey === selectedCharacterKey,
) ??
selectionEntries[0] ??
null,
[selectedCharacterKey, selectionEntries],
);
const selectedCharacter = selectedCharacterEntry?.character ?? null;
const selectedCharacterDraft = selectedCharacterEntry
? characterSelectionDrafts[selectedCharacterEntry.selectionKey] ?? null
? (characterSelectionDrafts[selectedCharacterEntry.selectionKey] ?? null)
: null;
const selectedCharacterPreview = useMemo(
() => applyCharacterSelectionDraft(selectedCharacter, selectedCharacterDraft),
() =>
applyCharacterSelectionDraft(selectedCharacter, selectedCharacterDraft),
[selectedCharacter, selectedCharacterDraft],
);
const selectedCharacterMeta = selectedCharacter
? getCharacterMeta(selectedCharacter, {name: selectedCharacterDraft?.name})
? getCharacterMeta(selectedCharacter, {
name: selectedCharacterDraft?.name,
})
: null;
const attributeSchema = useMemo(
() => resolveAttributeSchema(worldType, customWorldProfile),
@@ -281,24 +312,32 @@ export function RpgEntryCharacterSelectView({
const selectedAttributeProfile = useMemo(
() =>
selectedCharacter
? resolveCharacterAttributeProfile(
? (resolveCharacterAttributeProfile(
selectedCharacter,
worldType,
customWorldProfile,
)
?? buildCharacterAttributeProfile(selectedCharacter, attributeSchema)
) ??
buildCharacterAttributeProfile(selectedCharacter, attributeSchema))
: null,
[attributeSchema, customWorldProfile, selectedCharacter, worldType],
);
const selectedCharacterPersonalityTags = useMemo(
() => (selectedCharacterPreview ? getPersonalityTags(selectedCharacterPreview.personality) : []),
() =>
selectedCharacterPreview
? getPersonalityTags(selectedCharacterPreview.personality)
: [],
[selectedCharacterPreview],
);
const focusedCharacterIndex = clampIndex(Math.round(characterCarouselProgress), selectionCharacters.length);
const focusedCharacterIndex = clampIndex(
Math.round(characterCarouselProgress),
selectionCharacters.length,
);
const syncCharacterCarousel = useCallback(() => {
if (!characterCarouselRef.current) return;
setCharacterCarouselProgress(readCarouselProgress(characterCarouselRef.current, 'horizontal'));
setCharacterCarouselProgress(
readCarouselProgress(characterCarouselRef.current, 'horizontal'),
);
}, []);
useEffect(() => {
@@ -316,7 +355,11 @@ export function RpgEntryCharacterSelectView({
useEffect(() => {
if (selectionEntries.length === 0) return;
if (!selectionEntries.some(entry => entry.selectionKey === selectedCharacterKey)) {
if (
!selectionEntries.some(
(entry) => entry.selectionKey === selectedCharacterKey,
)
) {
const firstEntry = selectionEntries[0];
if (firstEntry) {
setSelectedCharacterKey(firstEntry.selectionKey);
@@ -346,7 +389,7 @@ export function RpgEntryCharacterSelectView({
return;
}
setCharacterSelectionDrafts(current => ({
setCharacterSelectionDrafts((current) => ({
...current,
[selectedCharacterEntry.selectionKey]: {
name: nextName,
@@ -373,8 +416,12 @@ export function RpgEntryCharacterSelectView({
<CharacterSelectBackButton onBack={onBack} />
</div>
<div className="mb-4 text-center">
<div className="text-2xl font-black text-white sm:text-[2rem]">选择你的角色</div>
<div className="mt-1 text-[11px] tracking-[0.14em] text-zinc-500">左右滑动浏览角色</div>
<div className="text-2xl font-black text-white sm:text-[2rem]">
选择你的角色
</div>
<div className="mt-1 text-[11px] tracking-[0.14em] text-zinc-500">
左右滑动浏览角色
</div>
</div>
<div
@@ -384,7 +431,9 @@ export function RpgEntryCharacterSelectView({
>
{selectionEntries.map(({ character, selectionKey }, index) => {
const characterDraft = characterSelectionDrafts[selectionKey];
const meta = getCharacterMeta(character, {name: characterDraft?.name});
const meta = getCharacterMeta(character, {
name: characterDraft?.name,
});
const selected = selectionKey === selectedCharacterKey;
return (
@@ -393,13 +442,19 @@ export function RpgEntryCharacterSelectView({
type="button"
onClick={() => {
setSelectedCharacterKey(selectionKey);
scrollCarouselToIndex(characterCarouselRef.current, index, 'horizontal');
scrollCarouselToIndex(
characterCarouselRef.current,
index,
'horizontal',
);
}}
data-carousel-card="true"
className={`character-carousel__card ${selected ? 'character-carousel__card--active' : ''}`}
style={getCharacterCardStyle(index, characterCarouselProgress)}
>
<span className={`character-carousel__surface ${selected ? 'character-carousel__surface--active' : ''}`}>
<span
className={`character-carousel__surface ${selected ? 'character-carousel__surface--active' : ''}`}
>
<span className="character-carousel__cover">
{selected ? (
<CharacterAnimator
@@ -412,21 +467,29 @@ export function RpgEntryCharacterSelectView({
src={character.portrait}
alt={meta.name}
className="character-carousel__portrait"
style={{imageRendering: 'pixelated'}}
style={{ imageRendering: 'pixelated' }}
/>
)}
</span>
{selected ? (
<>
<span className="character-carousel__selected-name">{meta.name}</span>
<span className="character-carousel__selected-name">
{meta.name}
</span>
<span className="character-carousel__meta character-carousel__meta--selected">
<span className="character-carousel__title character-carousel__title--selected">{meta.title}</span>
<span className="character-carousel__title character-carousel__title--selected">
{meta.title}
</span>
</span>
</>
) : (
<span className="character-carousel__meta">
<span className="character-carousel__name">{meta.name}</span>
<span className="character-carousel__title">{meta.title}</span>
<span className="character-carousel__name">
{meta.name}
</span>
<span className="character-carousel__title">
{meta.title}
</span>
</span>
)}
</span>
@@ -436,7 +499,13 @@ export function RpgEntryCharacterSelectView({
</div>
<div className="mt-3 grid gap-2 md:grid-cols-[0.85fr_1.15fr]">
<div className="pixel-nine-slice pixel-panel" style={getNineSliceStyle(UI_CHROME.statsPanel, {paddingX: 12, paddingY: 10})}>
<div
className="pixel-nine-slice pixel-panel"
style={getNineSliceStyle(UI_CHROME.statsPanel, {
paddingX: 12,
paddingY: 10,
})}
>
<div className="mb-2 flex items-center justify-between gap-3">
<div className="text-xs font-bold text-white">角色属性</div>
<div className="flex items-center gap-2 text-[10px] text-zinc-500">
@@ -448,8 +517,12 @@ export function RpgEntryCharacterSelectView({
</div>
<div className="grid grid-cols-2 gap-1 text-[11px] text-zinc-300 sm:grid-cols-3 sm:gap-1.5 sm:text-[13px]">
{attributeSchema.slots.map((slot) => (
<div key={slot.slotId} className="rounded-lg border border-white/6 bg-black/20 px-2 py-1.5 text-center sm:px-2.5">
{slot.name}: {selectedAttributeProfile?.values?.[slot.slotId] ?? 0}
<div
key={slot.slotId}
className="rounded-lg border border-white/6 bg-black/20 px-2 py-1.5 text-center sm:px-2.5"
>
{slot.name}:{' '}
{selectedAttributeProfile?.values?.[slot.slotId] ?? 0}
</div>
))}
</div>
@@ -457,10 +530,15 @@ export function RpgEntryCharacterSelectView({
<div
className="pixel-nine-slice pixel-panel character-backstory-panel flex flex-col"
style={getNineSliceStyle(UI_CHROME.panel, {paddingX: 12, paddingY: 10})}
style={getNineSliceStyle(UI_CHROME.panel, {
paddingX: 12,
paddingY: 10,
})}
>
<div className="mb-2 flex items-start justify-between gap-3">
<div className="character-backstory-title text-xs font-bold text-white">背景故事</div>
<div className="character-backstory-title text-xs font-bold text-white">
背景故事
</div>
<button
type="button"
onClick={openCharacterDraftEditor}
@@ -470,11 +548,17 @@ export function RpgEntryCharacterSelectView({
</button>
</div>
<div className="flex flex-1 flex-col text-[13px] leading-6 text-zinc-300">
<div>{selectedCharacterPreview?.backstory ?? selectedCharacter.backstory}</div>
<div>
{selectedCharacterPreview?.backstory ??
selectedCharacter.backstory}
</div>
<div className="mt-auto flex items-end justify-between gap-3 pt-3">
<div className="min-w-0 flex flex-wrap gap-1.5">
{selectedCharacterPersonalityTags.map(tag => (
<span key={tag} className="rounded-full border border-white/10 bg-black/20 px-2 py-0.5 text-[10px] text-zinc-300">
{selectedCharacterPersonalityTags.map((tag) => (
<span
key={tag}
className="rounded-full border border-white/10 bg-black/20 px-2 py-0.5 text-[10px] text-zinc-300"
>
{tag}
</span>
))}
@@ -495,9 +579,14 @@ export function RpgEntryCharacterSelectView({
<div className="mt-3">
<button
type="button"
onClick={() => onConfirm(selectedCharacterPreview ?? selectedCharacter)}
onClick={() =>
onConfirm(selectedCharacterPreview ?? selectedCharacter)
}
className="pixel-nine-slice pixel-pressable mx-auto block w-full max-w-[16rem] text-left"
style={getNineSliceStyle(UI_CHROME.choiceButton, {paddingX: 14, paddingY: 9})}
style={getNineSliceStyle(UI_CHROME.choiceButton, {
paddingX: 14,
paddingY: 9,
})}
>
<div className="flex items-center justify-between gap-4">
<span className="text-sm font-semibold text-white">进入营地</span>
@@ -516,14 +605,18 @@ export function RpgEntryCharacterSelectView({
/>
<CharacterDraftModal
isOpen={showCharacterDraftModal}
characterLabel={selectedCharacterMeta ? `${selectedCharacterMeta.name} / ${selectedCharacterMeta.title}` : '当前角色'}
characterLabel={
selectedCharacterMeta
? `${selectedCharacterMeta.name} / ${selectedCharacterMeta.title}`
: '当前角色'
}
draftName={characterDraftName}
draftBackstory={characterDraftBackstory}
onNameChange={value => {
onNameChange={(value) => {
setCharacterDraftName(value);
if (characterDraftError) setCharacterDraftError(null);
}}
onBackstoryChange={value => {
onBackstoryChange={(value) => {
setCharacterDraftBackstory(value);
if (characterDraftError) setCharacterDraftError(null);
}}
@@ -9451,9 +9451,7 @@ test('creation home desktop rail hides recommend and discover entries', async ()
test('profile feedback shortcut opens Feishu form in a new tab', async () => {
const user = userEvent.setup();
const openWindow = vi
.spyOn(window, 'open')
.mockImplementation(() => null);
const openWindow = vi.spyOn(window, 'open').mockImplementation(() => null);
mockDesktopPlatformLayout();
window.history.replaceState(null, '', '/creation');
const { container } = render(<TestWrapper withAuth />);
@@ -9531,17 +9529,25 @@ test('desktop project tab switch clears creation primary styling', async () => {
const nav = container.querySelector('.platform-desktop-rail');
expect(nav).toBeTruthy();
await user.click(within(nav as HTMLElement).getByRole('button', { name: '项目' }));
await user.click(
within(nav as HTMLElement).getByRole('button', { name: '项目' }),
);
expect(await screen.findByRole('main', { name: '项目' })).toBeTruthy();
const projectNav = container.querySelector('.platform-desktop-rail');
expect(projectNav).toBeTruthy();
const createNavButton = within(projectNav as HTMLElement).getByRole('button', {
name: '创作',
});
const projectNavButton = within(projectNav as HTMLElement).getByRole('button', {
name: '项目',
});
const createNavButton = within(projectNav as HTMLElement).getByRole(
'button',
{
name: '创作',
},
);
const projectNavButton = within(projectNav as HTMLElement).getByRole(
'button',
{
name: '项目',
},
);
expect(projectNavButton.className).toContain(
'platform-desktop-rail__button--active',
);
@@ -9565,9 +9571,7 @@ test('direct mobile creation home guides users to desktop without mobile nav ent
expect(
screen.getByText('图片画布、项目管理和素材库需要更大的操作空间。'),
).toBeTruthy();
expect(
screen.queryByRole('main', { name: '陶泥儿创作主页' }),
).toBeNull();
expect(screen.queryByRole('main', { name: '陶泥儿创作主页' })).toBeNull();
const bottomNav = container.querySelector('.platform-bottom-nav');
expect(bottomNav).toBeTruthy();
@@ -9582,9 +9586,7 @@ test('direct mobile creation home guides users to desktop without mobile nav ent
await waitFor(() => {
expect(window.location.pathname).toBe('/');
expect(
screen.queryByRole('main', { name: '桌面端创作提示' }),
).toBeNull();
expect(screen.queryByRole('main', { name: '桌面端创作提示' })).toBeNull();
});
});
@@ -10581,7 +10583,9 @@ test('direct missing public work detail shows unified dialog before returning ho
expect(await screen.findByText('正在读取作品详情...')).toBeTruthy();
const dialog = await screen.findByRole('dialog', { name: '作品不可用' });
expect(within(dialog).getByText('作品不存在或已下架,将返回首页。')).toBeTruthy();
expect(
within(dialog).getByText('作品不存在或已下架,将返回首页。'),
).toBeTruthy();
expect(alertSpy).not.toHaveBeenCalled();
await user.click(within(dialog).getByRole('button', { name: '知道了' }));
@@ -4468,12 +4468,8 @@ test('logged in mobile recommend page exposes the shared wallet details and rech
test('the latest recharge center read wins when wallet and recharge overlap', async () => {
const user = userEvent.setup();
mockNarrowMobileLayout();
let resolveWalletRead!: (
center: ProfileRechargeCenterResponse,
) => void;
let resolveRechargeRead!: (
center: ProfileRechargeCenterResponse,
) => void;
let resolveWalletRead!: (center: ProfileRechargeCenterResponse) => void;
let resolveRechargeRead!: (center: ProfileRechargeCenterResponse) => void;
const walletReadPromise = new Promise<ProfileRechargeCenterResponse>(
(resolve) => {
resolveWalletRead = resolve;
@@ -17,7 +17,9 @@ function formatMembershipPeriodPrefix(periodDays: number) {
return '每周期';
}
export function buildRechargeProductValueLabel(product: ProfileRechargeProduct) {
export function buildRechargeProductValueLabel(
product: ProfileRechargeProduct,
) {
if (product.kind === 'membership') {
const membershipPeriodPoints = product.membershipPeriodPoints;
const membershipPeriodDays =
@@ -64,7 +66,9 @@ export function buildMembershipLabel(
}
const tierLabel =
membership.tier === 'normal' ? '会员' : getMembershipTierLabel(membership.tier);
membership.tier === 'normal'
? '会员'
: getMembershipTierLabel(membership.tier);
const resetLabel = membership.cycleResetsAt
? ` · ${formatTime(membership.cycleResetsAt)}重置`
: '';
@@ -13,9 +13,7 @@ import {
selectProfileTaskCenterTasks,
} from './rpgEntryProfileTaskViewModel';
function buildTask(
overrides: Partial<ProfileTaskItem> = {},
): ProfileTaskItem {
function buildTask(overrides: Partial<ProfileTaskItem> = {}): ProfileTaskItem {
return {
taskId: 'task-1',
title: '游玩一次',
@@ -33,9 +31,7 @@ function buildTask(
};
}
function buildCenter(
tasks: ProfileTaskItem[],
): ProfileTaskCenterResponse {
function buildCenter(tasks: ProfileTaskItem[]): ProfileTaskCenterResponse {
return {
dayKey: 20260603,
walletBalance: 12,
@@ -65,9 +61,9 @@ test('profile task ViewModel selects one actionable task by status priority and
claimable,
]),
).toEqual([claimable]);
expect(selectProfileTaskCenterTasks([firstIncomplete, secondIncomplete])).toEqual(
[firstIncomplete],
);
expect(
selectProfileTaskCenterTasks([firstIncomplete, secondIncomplete]),
).toEqual([firstIncomplete]);
});
test('profile task ViewModel falls back from card task to claimed and enabled tasks', () => {
@@ -118,9 +114,9 @@ test('profile task ViewModel exposes task labels for the modal', () => {
expect(getProfileTaskStatusLabel('claimable')).toBe('可领取');
expect(buildProfileTaskProgressLabel(task)).toBe('0/1');
expect(getProfileTaskClaimButtonLabel(task, true)).toBe('领取中');
expect(getProfileTaskClaimButtonLabel(buildTask({ status: 'claimed' }), false)).toBe(
'已领取',
);
expect(
getProfileTaskClaimButtonLabel(buildTask({ status: 'claimed' }), false),
).toBe('已领取');
expect(
getProfileTaskClaimButtonLabel(buildTask({ status: 'claimable' }), false),
).toBe('领取');
@@ -32,8 +32,7 @@ export function selectProfileTaskCenterTasks(tasks: ProfileTaskItem[]) {
return tasks
.map((task, index) => ({ task, index }))
.filter(
({ task }) =>
task.status === 'claimable' || task.status === 'incomplete',
({ task }) => task.status === 'claimable' || task.status === 'incomplete',
)
.sort(
(left, right) =>
@@ -205,9 +205,9 @@ test('public gallery ViewModel keeps recommend feed in scored runtime order', ()
publishedAt: '2026-05-02T00:00:00.000Z',
});
expect(buildPlatformRecommendFeedEntries([], [quietPuzzle, hotPuzzle])).toEqual(
[hotPuzzle, quietPuzzle],
);
expect(
buildPlatformRecommendFeedEntries([], [quietPuzzle, hotPuzzle]),
).toEqual([hotPuzzle, quietPuzzle]);
});
test('public gallery ViewModel selects recommend feed window with wraparound neighbors', () => {
@@ -302,9 +302,9 @@ test('public gallery ViewModel searches compact work codes and ranks name prefix
worldName: '静心木鱼',
});
expect(filterPlatformWorkSearchResults([codeMatch, nameMatch], '星桥')).toEqual(
[nameMatch, codeMatch],
);
expect(
filterPlatformWorkSearchResults([codeMatch, nameMatch], '星桥'),
).toEqual([nameMatch, codeMatch]);
expect(filterPlatformWorkSearchResults([codeMatch], 'pz xing_qiao')).toEqual([
codeMatch,
]);
@@ -327,15 +327,15 @@ test('public gallery ViewModel keeps source kinds behind one category filter sea
expect(matchesPlatformCategoryKindFilter(jumpHopEntry, 'jump-hop')).toBe(
true,
);
expect(matchesPlatformCategoryKindFilter(woodenFishEntry, 'wooden-fish')).toBe(
true,
);
expect(
matchesPlatformCategoryKindFilter(woodenFishEntry, 'wooden-fish'),
).toBe(true);
expect(matchesPlatformCategoryKindFilter(jumpHopEntry, 'custom-world')).toBe(
false,
);
expect(matchesPlatformCategoryKindFilter(woodenFishEntry, 'custom-world')).toBe(
false,
);
expect(
matchesPlatformCategoryKindFilter(woodenFishEntry, 'custom-world'),
).toBe(false);
});
test('public gallery ViewModel exposes category filter and sort option interface', () => {
@@ -169,8 +169,7 @@ export function selectAdjacentPlatformRecommendEntry(
)
: -1;
const baseIndex = activeIndex >= 0 ? activeIndex : 0;
const nextIndex =
(baseIndex + direction + entries.length) % entries.length;
const nextIndex = (baseIndex + direction + entries.length) % entries.length;
const nextEntry = entries[nextIndex] ?? null;
if (
nextEntry &&
@@ -280,9 +279,7 @@ function normalizePlatformCompactSearchText(value: string | null | undefined) {
return normalizePlatformSearchText(value).replace(/[\s_-]+/gu, '');
}
export function getPlatformSearchableWorkIds(
entry: PlatformPublicGalleryCard,
) {
export function getPlatformSearchableWorkIds(entry: PlatformPublicGalleryCard) {
const ids = [entry.publicWorkCode, entry.profileId];
if ('workId' in entry) {
ids.push(entry.workId);
@@ -406,11 +403,17 @@ export function filterTodayPublishedEntries(
}
export function getPlatformWorldLikeCount(entry: PlatformWorldCardLike) {
return Math.max(0, Math.round(('likeCount' in entry && entry.likeCount) || 0));
return Math.max(
0,
Math.round(('likeCount' in entry && entry.likeCount) || 0),
);
}
export function getPlatformWorldPlayCount(entry: PlatformWorldCardLike) {
return Math.max(0, Math.round(('playCount' in entry && entry.playCount) || 0));
return Math.max(
0,
Math.round(('playCount' in entry && entry.playCount) || 0),
);
}
export function getPlatformWorldRemixCount(entry: PlatformWorldCardLike) {
@@ -17,10 +17,7 @@ export function hasRecommendDragStarted(deltaY: number) {
return Math.abs(deltaY) >= RECOMMEND_ENTRY_SWIPE_THRESHOLD_PX / 2;
}
export function clampRecommendDragOffset(
deltaY: number,
stageHeight: number,
) {
export function clampRecommendDragOffset(deltaY: number, stageHeight: number) {
const dragLimit =
stageHeight > 0 ? stageHeight : RECOMMEND_ENTRY_DRAG_LIMIT_PX;
return Math.max(-dragLimit, Math.min(dragLimit, deltaY));
@@ -274,7 +274,10 @@ test('maps puzzle clear work to platform gallery card with PC public code', () =
expect(resolvePlatformWorldFallbackCoverImage(card)).toBe(
'/creation-type-references/puzzle.webp',
);
expect(buildPlatformWorldDisplayTags(card, 2)).toEqual(['拼消消', '霓虹星港']);
expect(buildPlatformWorldDisplayTags(card, 2)).toEqual([
'拼消消',
'霓虹星港',
]);
});
test('resolves public work author from display name and public user code before stored author name', () => {
@@ -663,12 +663,11 @@ export function mapJumpHopWorkToPlatformGalleryCard(
};
}
function normalizePuzzleClearThemeTags(summary: PuzzleClearWorkSummaryResponse) {
function normalizePuzzleClearThemeTags(
summary: PuzzleClearWorkSummaryResponse,
) {
const themePrompt = summary.themePrompt.trim();
return [
'拼消消',
...(themePrompt ? [themePrompt] : []),
];
return ['拼消消', ...(themePrompt ? [themePrompt] : [])];
}
function getPuzzleClearRecentPlayCount(
@@ -1094,7 +1093,9 @@ export function describePlatformPublicWorkKind(
return formatPlatformWorkDisplayTag(entry.templateName);
}
return formatPlatformWorkDisplayTag(describePlatformThemeLabel(entry.themeMode));
return formatPlatformWorkDisplayTag(
describePlatformThemeLabel(entry.themeMode),
);
}
export function resolvePlatformWorkAuthorDisplayName(
@@ -1195,7 +1196,9 @@ export function describePublicGalleryCardKind(
if (isEdutainmentGalleryEntry(entry)) {
return formatPlatformWorkDisplayTag(entry.templateName);
}
return formatPlatformWorkDisplayTag(describePlatformThemeLabel(entry.themeMode));
return formatPlatformWorkDisplayTag(
describePlatformThemeLabel(entry.themeMode),
);
}
export function buildPlatformWorldTags(entry: PlatformWorldCardLike) {
@@ -4,9 +4,7 @@ import type {
CustomWorldAgentOperationRecord,
CustomWorldAgentSessionSnapshot,
} from '../../../packages/shared/src/contracts/customWorldAgent';
import {
getRpgCreationOperation,
} from '../../services/rpg-creation';
import { getRpgCreationOperation } from '../../services/rpg-creation';
import {
createFailedAgentOperation,
resolveRpgCreationErrorMessage,
@@ -91,7 +89,10 @@ export function useRpgCreationAgentOperationPolling(
createFailedAgentOperation({
type: 'process_message',
phaseLabel: '读取操作状态失败',
error: resolveRpgCreationErrorMessage(error, '读取共创操作状态失败。'),
error: resolveRpgCreationErrorMessage(
error,
'读取共创操作状态失败。',
),
}),
);
persistAgentUiState(activeAgentSessionId, null);
@@ -270,9 +270,9 @@ describe('useRpgCreationEnterWorld', () => {
expect(executePublishWorld).toHaveBeenCalledTimes(1);
expect(syncAgentCreationResultView).toHaveBeenCalledWith('session-1');
expect(setGeneratedCustomWorldProfile).toHaveBeenCalledWith(syncedProfile);
expect(
setGeneratedCustomWorldProfile.mock.calls.at(-1)?.[0]?.id,
).toBe('draft-profile');
expect(setGeneratedCustomWorldProfile.mock.calls.at(-1)?.[0]?.id).toBe(
'draft-profile',
);
expect(
setGeneratedCustomWorldProfile.mock.calls.at(-1)?.[0]?.playableNpcs[0]
?.imageSrc,
@@ -341,7 +341,6 @@ describe('useRpgCreationEnterWorld', () => {
);
});
it('正式进入世界回读结果页字段更少时不降级当前完整 profile', async () => {
const resultProfile = {
...buildProfile({
@@ -357,7 +356,8 @@ describe('useRpgCreationEnterWorld', () => {
openingCg: {
id: 'opening-cg-stardust',
status: 'ready' as const,
storyboardImageSrc: '/generated-custom-world-scenes/opening/storyboard.png',
storyboardImageSrc:
'/generated-custom-world-scenes/opening/storyboard.png',
videoSrc: '/generated-custom-world-scenes/opening/opening.mp4',
imageModel: 'gpt-image-2' as const,
videoModel: 'doubao-seedance-2-0-fast-260128',
@@ -468,8 +468,7 @@ describe('useRpgCreationEnterWorld', () => {
'/generated-characters/draft-role/portrait.png',
);
expect(
launchedProfile?.sceneChapterBlueprints?.[0]?.acts[0]
?.backgroundImageSrc,
launchedProfile?.sceneChapterBlueprints?.[0]?.acts[0]?.backgroundImageSrc,
).toBe('/assets/custom-world/act-stardust-opening.png');
});
});
@@ -16,9 +16,10 @@ type UseRpgCreationEnterWorldParams = {
customWorldProfile: CustomWorldProfile,
options?: CustomWorldRuntimeLaunchOptions,
) => void;
syncAgentDraftResultProfile: (
profile: CustomWorldProfile,
) => Promise<{ profile: CustomWorldProfile | null; view?: RpgCreationResultView | null }>;
syncAgentDraftResultProfile: (profile: CustomWorldProfile) => Promise<{
profile: CustomWorldProfile | null;
view?: RpgCreationResultView | null;
}>;
executePublishWorld: () => Promise<unknown>;
syncAgentCreationResultView: (
sessionId: string,
@@ -82,7 +83,8 @@ export function useRpgCreationEnterWorld(
}
if (currentAgentSessionStage === 'published') {
const latestView = await syncAgentCreationResultView(activeAgentSessionId);
const latestView =
await syncAgentCreationResultView(activeAgentSessionId);
const publishedProfile = chooseMoreCompleteCustomWorldProfile(
generatedCustomWorldProfile,
rpgCreationPreviewAdapter.buildPreviewFromResultView(latestView),
@@ -111,7 +113,8 @@ export function useRpgCreationEnterWorld(
syncedResult.view.canEnterWorld;
if (canEnterPublishedWorld) {
const latestView = await syncAgentCreationResultView(activeAgentSessionId);
const latestView =
await syncAgentCreationResultView(activeAgentSessionId);
return chooseMoreCompleteCustomWorldProfile(
latestProfile,
rpgCreationPreviewAdapter.buildPreviewFromResultView(latestView),
@@ -182,9 +182,8 @@ describe('RPG Agent 草稿恢复', () => {
const setGeneratedCustomWorldProfile = vi.fn();
const setCustomWorldResultViewSource = vi.fn();
const suppressAgentDraftResultAutoOpen = vi.fn();
let openWork:
| ((work: CustomWorldWorkSummary) => Promise<void>)
| null = null;
let openWork: ((work: CustomWorldWorkSummary) => Promise<void>) | null =
null;
function Harness() {
openWork = useRpgEntryLibraryDetail({
@@ -310,9 +309,7 @@ describe('RPG Agent 草稿恢复', () => {
schemaId: 'schema-星砂废都',
values: { axis_a: 8 },
topTraits: ['星砂共鸣'],
evidence: [
{ slotId: 'axis_a', reason: '能听见星砂潮汐。' },
],
evidence: [{ slotId: 'axis_a', reason: '能听见星砂潮汐。' }],
},
},
],
@@ -393,9 +390,10 @@ describe('RPG Agent 草稿恢复', () => {
const selectedEntries: CustomWorldLibraryEntry<CustomWorldProfile>[] = [];
function Harness() {
const [selectedDetailEntry, setSelectedDetailEntry] = useState<
CustomWorldLibraryEntry<CustomWorldProfile> | null
>(detailEntry);
const [selectedDetailEntry, setSelectedDetailEntry] =
useState<CustomWorldLibraryEntry<CustomWorldProfile> | null>(
detailEntry,
);
useEffect(() => {
if (selectedDetailEntry) {
selectedEntries.push(selectedDetailEntry);
@@ -442,10 +440,12 @@ describe('RPG Agent 草稿恢复', () => {
lastSelected?.profile.playableNpcs[0]?.skills[0]?.actionPreviewConfig
?.previewVideoPath,
).toBe('/assets/custom-world/shamian/skill.mp4');
expect(lastSelected?.profile.playableNpcs[0]?.initialItems[0]?.iconSrc).toBe(
'/assets/custom-world/items/sand-compass.png',
);
expect(lastSelected?.profile.playableNpcs[0]?.attributeProfile).toBeTruthy();
expect(
lastSelected?.profile.playableNpcs[0]?.initialItems[0]?.iconSrc,
).toBe('/assets/custom-world/items/sand-compass.png');
expect(
lastSelected?.profile.playableNpcs[0]?.attributeProfile,
).toBeTruthy();
expect(
lastSelected?.profile.sceneChapterBlueprints?.[0]?.acts[0]
?.backgroundImageSrc,
@@ -503,9 +503,7 @@ describe('RPG Agent 草稿恢复', () => {
schemaId: 'schema-星砂废都',
values: { axis_a: 8 },
topTraits: ['星砂共鸣'],
evidence: [
{ slotId: 'axis_a', reason: '能听见星砂潮汐。' },
],
evidence: [{ slotId: 'axis_a', reason: '能听见星砂潮汐。' }],
},
} satisfies CustomWorldProfile['playableNpcs'][number];
const fullProfile: CustomWorldProfile = {
@@ -547,9 +545,10 @@ describe('RPG Agent 草稿恢复', () => {
const selectedEntries: CustomWorldLibraryEntry<CustomWorldProfile>[] = [];
function Harness() {
const [selectedDetailEntry, setSelectedDetailEntry] = useState<
CustomWorldLibraryEntry<CustomWorldProfile> | null
>(detailEntry);
const [selectedDetailEntry, setSelectedDetailEntry] =
useState<CustomWorldLibraryEntry<CustomWorldProfile> | null>(
detailEntry,
);
useEffect(() => {
if (selectedDetailEntry) {
selectedEntries.push(selectedDetailEntry);
@@ -596,12 +595,12 @@ describe('RPG Agent 草稿恢复', () => {
lastSelected?.profile.playableNpcs[0]?.skills[0]?.actionPreviewConfig
?.previewVideoPath,
).toBe('/assets/custom-world/shamian/skill.mp4');
expect(lastSelected?.profile.playableNpcs[0]?.initialItems[0]?.iconSrc).toBe(
'/assets/custom-world/items/sand-compass.png',
);
expect(lastSelected?.profile.playableNpcs[0]?.attributeProfile?.values).toEqual(
{ axis_a: 8 },
);
expect(
lastSelected?.profile.playableNpcs[0]?.initialItems[0]?.iconSrc,
).toBe('/assets/custom-world/items/sand-compass.png');
expect(
lastSelected?.profile.playableNpcs[0]?.attributeProfile?.values,
).toEqual({ axis_a: 8 });
});
it('详情删除由上层确认弹窗触发,hook 不再调用原生 confirm', async () => {
@@ -1,8 +1,6 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import type {
CustomWorldWorkSummary,
} from '../../../packages/shared/src/contracts/customWorldAgent';
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldAgent';
import type {
CustomWorldGalleryCard,
CustomWorldLibraryEntry,
@@ -38,16 +36,12 @@ type UseRpgEntryBootstrapParams = {
getProfileDashboard: (
options?: RuntimeRequestOptions,
) => Promise<ProfileDashboardSummary | null>;
handleContinueGame: (
snapshot?: HydratedSavedGameSnapshot | null,
) => void;
handleContinueGame: (snapshot?: HydratedSavedGameSnapshot | null) => void;
hasInitialAgentSession: boolean;
initialPlatformTab?: PlatformHomeTab;
};
export function useRpgEntryBootstrap(
params: UseRpgEntryBootstrapParams,
) {
export function useRpgEntryBootstrap(params: UseRpgEntryBootstrapParams) {
const {
user,
canAccessProtectedData = Boolean(user),
@@ -76,7 +70,9 @@ export function useRpgEntryBootstrap(
const [historyEntries, setHistoryEntries] = useState<
PlatformBrowseHistoryEntry[]
>([]);
const [saveEntries, setSaveEntries] = useState<ProfileSaveArchiveSummary[]>([]);
const [saveEntries, setSaveEntries] = useState<ProfileSaveArchiveSummary[]>(
[],
);
const [platformTab, setPlatformTabState] =
useState<PlatformHomeTab>(initialPlatformTab);
const [platformError, setPlatformError] = useState<string | null>(null);
@@ -204,7 +200,9 @@ export function useRpgEntryBootstrap(
setSaveError(null);
try {
const resumedArchive = await resumeRpgProfileSaveArchive(entry.worldKey);
const resumedArchive = await resumeRpgProfileSaveArchive(
entry.worldKey,
);
setSaveEntries((currentEntries) =>
currentEntries.map((currentEntry) =>
currentEntry.worldKey === resumedArchive.entry.worldKey
@@ -242,9 +240,7 @@ export function useRpgEntryBootstrap(
setSaveError(null);
setIsLoadingPlatform(shouldShowBlockingLoading);
setPlatformError(null);
setIsLoadingDashboard(
canReadProtectedData && shouldShowBlockingLoading,
);
setIsLoadingDashboard(canReadProtectedData && shouldShowBlockingLoading);
setDashboardError(null);
if (!canReadProtectedData) {
setSavedCustomWorldEntries([]);
@@ -307,8 +303,7 @@ export function useRpgEntryBootstrap(
if (
(canReadProtectedData &&
libraryEntriesResult.status === 'rejected') ||
(canReadProtectedData &&
workEntriesResult.status === 'rejected')
(canReadProtectedData && workEntriesResult.status === 'rejected')
) {
const platformFailure =
libraryEntriesResult.status === 'rejected'
@@ -337,7 +332,10 @@ export function useRpgEntryBootstrap(
setHistoryEntries(historyResult.value);
} else if (canReadProtectedData) {
setHistoryError(
resolveRpgEntryErrorMessage(historyResult.reason, '读取浏览历史失败。'),
resolveRpgEntryErrorMessage(
historyResult.reason,
'读取浏览历史失败。',
),
);
}
@@ -6,7 +6,9 @@ type UseRpgEntryCharacterSelectParams = {
gameState: GameState;
handleBackToWorldSelect: () => void;
setSelectionStage: (stage: 'platform') => void;
handleCharacterSelect: (character: NonNullable<GameState['playerCharacter']>) => void;
handleCharacterSelect: (
character: NonNullable<GameState['playerCharacter']>,
) => void;
};
/**
@@ -14,9 +14,7 @@ type UseRpgEntryNavigationParams = {
/**
* 收口 RPG 入口壳层的阶段跳转,避免页面内散落大量匿名 setState 回调。
*/
export function useRpgEntryNavigation(
params: UseRpgEntryNavigationParams,
) {
export function useRpgEntryNavigation(params: UseRpgEntryNavigationParams) {
const { setSelectionStage, setSelectedDetailEntry } = params;
const backToPlatformHome = useCallback(() => {
@@ -10,9 +10,7 @@ type UseRpgEntrySaveResumeParams = {
* RPG 入口域里的“继续游戏”入口只负责转发恢复动作,
* 让壳层组件不直接知道具体的存档恢复实现细节。
*/
export function useRpgEntrySaveResume(
params: UseRpgEntrySaveResumeParams,
) {
export function useRpgEntrySaveResume(params: UseRpgEntrySaveResumeParams) {
const { handleResumeSaveEntry } = params;
const resumeSelectedSave = useCallback(