Merge branch 'master' into codex/puzzle-clear-template-runtime-fixes

# Conflicts:
#	.hermes/shared-memory/decision-log.md
#	.hermes/shared-memory/project-overview.md
#	docs/【开发运维】本地开发验证与生产运维-2026-05-15.md
#	scripts/dev.test.ts
#	server-rs/crates/api-server/src/creation_entry_config.rs
#	server-rs/crates/api-server/src/wooden_fish.rs
#	server-rs/crates/module-auth/src/lib.rs
#	server-rs/crates/spacetime-client/src/wooden_fish.rs
#	server-rs/crates/spacetime-module/src/auth/procedures.rs
#	src/components/custom-world-home/creationWorkShelf.ts
#	src/components/platform-entry/PlatformEntryFlowShellImpl.tsx
#	src/components/rpg-entry/rpgEntryWorldPresentation.ts
#	src/services/miniGameDraftGenerationProgress.test.ts
#	src/services/miniGameDraftGenerationProgress.ts
This commit is contained in:
2026-06-04 11:24:14 +08:00
451 changed files with 18452 additions and 5266 deletions

View File

@@ -36,9 +36,9 @@ const entryConfig = {
visible: true,
open: true,
sortOrder: 10,
categoryId: 'recent',
categoryLabel: '最近创作',
categorySortOrder: 10,
categoryId: 'recommended',
categoryLabel: '热门推荐',
categorySortOrder: 20,
updatedAtMicros: 1,
},
],

View File

@@ -1,4 +1,4 @@
import { ArrowRight } from 'lucide-react';
import { ArrowRight, LockKeyhole } from 'lucide-react';
import type { CreationEntryConfig } from '../../services/creationEntryConfigService';
import { UnifiedModal } from '../common/UnifiedModal';
@@ -33,6 +33,7 @@ function CreationTypeCard(props: {
}) {
const { item, busy, onSelect } = props;
const disabled = item.locked || busy;
const lockedBadge = item.badge.trim() || '暂未开放';
return (
<button
@@ -60,12 +61,15 @@ function CreationTypeCard(props: {
/>
<div className="relative z-10 flex min-h-6 items-start justify-end gap-3 px-4 pt-4">
{item.locked ? (
<span className="platform-pill platform-pill--neutral px-3 text-[var(--platform-text-soft)]">
{item.badge}
<span className="platform-pill platform-pill--neutral gap-1 px-3 text-[var(--platform-text-soft)]">
<LockKeyhole className="h-3.5 w-3.5" />
{lockedBadge}
</span>
) : null}
{item.locked ? (
<span className="text-lg leading-none text-white/62">·</span>
<span className="inline-flex h-7 w-7 items-center justify-center rounded-full bg-white/18 text-white/72">
<LockKeyhole className="h-3.5 w-3.5" />
</span>
) : (
<ArrowRight className="h-4 w-4 text-white/80" />
)}
@@ -169,7 +173,6 @@ export function PlatformEntryCreationTypeModal({
/>
))}
</div>
</UnifiedModal>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -58,6 +58,22 @@ describe('PlatformErrorDialog', () => {
expect(screen.queryByRole('dialog', { name: '发生错误' })).toBeNull();
});
test('does not render creation entry disabled errors', () => {
render(
<PlatformErrorDialog
error={{
source: '大鱼草稿',
message:
'creation_entry_disabledrequestId: req-big-fish-gallery',
}}
onClose={() => {}}
/>,
);
expect(screen.queryByRole('dialog', { name: '发生错误' })).toBeNull();
expect(screen.queryByText(/creation_entry_disabled/u)).toBeNull();
});
});
describe('PlatformTaskCompletionDialog', () => {

View File

@@ -20,6 +20,11 @@ function buildPlatformErrorReport(error: PlatformErrorDialogPayload) {
return [`来源:${error.source}`, `错误:${error.message}`].join('\n');
}
function isBlacklistedPlatformError(error: PlatformErrorDialogPayload | null) {
// 中文注释:入口关闭是平台开关状态,不作为全局错误弹窗打扰用户。
return Boolean(error?.message.includes('creation_entry_disabled'));
}
export function PlatformErrorDialog({
error,
onClose,
@@ -30,9 +35,10 @@ export function PlatformErrorDialog({
'idle',
);
const resetTimerRef = useRef<number | null>(null);
const dialogError = isBlacklistedPlatformError(error) ? null : error;
const reportText = useMemo(
() => (error ? buildPlatformErrorReport(error) : ''),
[error],
() => (dialogError ? buildPlatformErrorReport(dialogError) : ''),
[dialogError],
);
useEffect(
@@ -46,7 +52,7 @@ export function PlatformErrorDialog({
useEffect(() => {
setCopyState('idle');
}, [error?.source, error?.message]);
}, [dialogError?.source, dialogError?.message]);
const copyError = () => {
if (!reportText) {
@@ -67,7 +73,7 @@ export function PlatformErrorDialog({
return (
<UnifiedModal
open={Boolean(error)}
open={Boolean(dialogError)}
title="发生错误"
onClose={onClose}
size="sm"
@@ -95,14 +101,14 @@ export function PlatformErrorDialog({
</button>
}
>
{error ? (
{dialogError ? (
<>
<div className="rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/72 px-3 py-2">
<div className="text-xs font-bold text-[var(--platform-text-soft)]">
</div>
<div className="mt-1 break-words text-sm font-semibold leading-5 text-[var(--platform-text-strong)]">
{error.source}
{dialogError.source}
</div>
</div>
<div className="rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/72 px-3 py-2">
@@ -110,7 +116,7 @@ export function PlatformErrorDialog({
</div>
<div className="mt-1 whitespace-pre-wrap break-words text-sm leading-6 text-[var(--platform-text-strong)]">
{error.message}
{dialogError.message}
</div>
</div>
</>

View File

@@ -163,7 +163,7 @@ 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', () => {
test('PlatformWorkDetailView prefers display name without appending public user code', () => {
render(
<PlatformWorkDetailView
entry={createWoodenFishEntry()}
@@ -183,10 +183,11 @@ test('PlatformWorkDetailView prefers display name then public user code for wood
/>,
);
expect(screen.getByText('公开昵称 · SY-00000004')).toBeTruthy();
expect(screen.getByText('公开昵称')).toBeTruthy();
expect(screen.queryByText('公开昵称 · SY-00000004')).toBeNull();
expect(screen.queryByText('SY-00000004')).toBeNull();
expect(screen.queryByText('phone_00000004')).toBeNull();
expect(screen.queryByText('敲木鱼玩家')).toBeNull();
expect(screen.queryByText('公开昵称')).toBeNull();
});
test('PlatformWorkDetailView calls like handler', () => {

View File

@@ -306,7 +306,7 @@ test('groups visible platform creation types by backend category metadata', () =
const groups = groupVisiblePlatformCreationTypes(cards);
expect(groups.map((group) => group.label)).toEqual([
'最近创作',
'热门推荐',
'节日主题',
]);
expect(groups[0]?.items.map((item) => item.id)).toEqual([
@@ -337,14 +337,14 @@ test('falls back when backend creation type category metadata is missing', () =>
expect(cards[0]).toEqual(
expect.objectContaining({
id: 'legacy-entry',
categoryId: 'recent',
categoryLabel: '最近创作',
categoryId: 'recommended',
categoryLabel: '热门推荐',
}),
);
expect(groupVisiblePlatformCreationTypes(cards)).toEqual([
expect.objectContaining({
id: 'recent',
label: '最近创作',
id: 'recommended',
label: '热门推荐',
}),
]);
});

View File

@@ -24,8 +24,9 @@ export type PlatformCreationTypeGroup = {
items: PlatformCreationTypeCard[];
};
const FALLBACK_CREATION_CATEGORY_ID = 'recent';
const FALLBACK_CREATION_CATEGORY_LABEL = '最近创作';
const RECENT_CREATION_CATEGORY_ID = 'recent';
const FALLBACK_CREATION_CATEGORY_ID = 'recommended';
const FALLBACK_CREATION_CATEGORY_LABEL = '热门推荐';
export function getVisiblePlatformCreationTypes(
creationTypes: readonly PlatformCreationTypeCard[],
@@ -55,16 +56,25 @@ export function isPlatformCreationTypeOpen(
);
}
/** 归一化模板分类 ID历史 recent 分类会并入推荐分类。 */
function normalizeCategoryId(value: string | null | undefined) {
const normalized = typeof value === 'string' ? value.trim() : '';
if (normalized === RECENT_CREATION_CATEGORY_ID) {
return FALLBACK_CREATION_CATEGORY_ID;
}
return normalized || FALLBACK_CREATION_CATEGORY_ID;
}
/** 归一化模板分类名,避免最近创作被误当作模板页签。 */
function normalizeCategoryLabel(value: string | null | undefined) {
const normalized = typeof value === 'string' ? value.trim() : '';
if (normalized === '最近创作') {
return FALLBACK_CREATION_CATEGORY_LABEL;
}
return normalized || FALLBACK_CREATION_CATEGORY_LABEL;
}
/** 按玩法模板分类分组,旧 recent 分类不再作为模板页签展示。 */
export function groupVisiblePlatformCreationTypes(
creationTypes: readonly PlatformCreationTypeCard[],
): PlatformCreationTypeGroup[] {