Merge branch 'master' into codex/sse-stream-architecture

# Conflicts:
#	.hermes/shared-memory/decision-log.md
#	docs/【玩法创作】平台入口与玩法链路-2026-05-15.md
#	src/components/platform-entry/PlatformEntryFlowShellImpl.tsx
This commit is contained in:
2026-06-05 17:21:24 +08:00
51 changed files with 2222 additions and 755 deletions

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>
);
}

View File

@@ -414,6 +414,7 @@ import {
buildPlatformTaskCompletionDialogDismissKey,
formatPlatformDialogSource,
isBackgroundGenerationStillRunningMessage,
normalizePlatformDialogMessage,
PLATFORM_TASK_COMPLETION_MESSAGE,
type PlatformDialogCandidate,
type PlatformErrorDialogState,
@@ -561,6 +562,7 @@ import {
buildPlatformPublicGalleryFeeds,
getPlatformPublicGalleryEntryKey,
getPlatformRecommendRuntimeKind,
isPlatformRecommendRuntimeReadyForEntry,
isSamePlatformPublicGalleryEntry,
type RecommendRuntimeKind,
resolvePlatformRecommendRuntimeAutoStartDecision,
@@ -744,6 +746,19 @@ const PUZZLE_DRAFT_GENERATION_POINT_COST = 2;
const MATCH3D_DRAFT_GENERATION_POINT_COST = 10;
const BARK_BATTLE_DRAFT_GENERATION_POINT_COST = 3;
/** 中文注释:首页公开数据降级时,入口关闭错误不弹窗;真实创作动作仍由对应工作台提示。 */
function isCreationEntryDisabledErrorMessage(
message: string | null | undefined,
) {
const normalized = normalizePlatformDialogMessage(message);
return Boolean(
normalized &&
(normalized.includes('creation_entry_disabled') ||
normalized.includes('该玩法入口暂不可用') ||
normalized.includes('创作入口已关闭')),
);
}
const PUZZLE_ONBOARDING_FIRST_VISIT_STORAGE_KEY =
'genarrative.puzzle-onboarding.first-visit.v1';
const PUZZLE_ONBOARDING_COPY = '待定待定待定';
@@ -4185,6 +4200,11 @@ export function PlatformEntryFlowShellImpl({
isMiniGameDraftGenerating(
activePuzzleBackgroundCompileTask?.generationState ?? null,
);
const platformBootstrapErrorForDisplay = isCreationEntryDisabledErrorMessage(
platformBootstrap.platformError,
)
? null
: platformBootstrap.platformError;
const [dismissedPlatformErrorDialogKey, setDismissedPlatformErrorDialogKey] =
useState<string | null>(null);
const [
@@ -4207,7 +4227,7 @@ export function PlatformEntryFlowShellImpl({
{
key: 'platform-bootstrap',
source: '平台首页',
message: platformBootstrap.platformError,
message: platformBootstrapErrorForDisplay,
},
{
key: 'rpg-creation-type',
@@ -4378,7 +4398,7 @@ export function PlatformEntryFlowShellImpl({
match3dRun?.runId,
match3dSession?.sessionId,
pendingPlatformTaskFailureDialog,
platformBootstrap.platformError,
platformBootstrapErrorForDisplay,
publicWorkDetailError,
puzzleCreationError,
puzzleError,
@@ -12011,6 +12031,29 @@ export function PlatformEntryFlowShellImpl({
isDesktopLayout,
]);
const activeRecommendEntry =
activeRecommendEntryKey && !isDesktopLayout
? (recommendRuntimeEntries.find(
(entry) =>
getPlatformPublicGalleryEntryKey(entry) ===
activeRecommendEntryKey,
) ?? null)
: null;
const isActiveRecommendRuntimeReady =
activeRecommendEntry !== null &&
isPlatformRecommendRuntimeReadyForEntry(activeRecommendEntry, {
activeKind: activeRecommendRuntimeKind,
hasBabyObjectMatchDraft: Boolean(babyObjectMatchDraft),
hasBigFishRun: Boolean(bigFishRun),
hasJumpHopRun: Boolean(jumpHopRun),
hasMatch3DRun: Boolean(match3dRun),
hasSquareHoleRun: Boolean(squareHoleRun),
hasVisualNovelRun: Boolean(visualNovelRun),
hasWoodenFishRun: Boolean(woodenFishRun),
puzzleRunEntryProfileId: puzzleRun?.entryProfileId ?? null,
puzzleRunCurrentLevelProfileId: puzzleRun?.currentLevel?.profileId ?? null,
});
useEffect(() => {
const decision = resolvePlatformRecommendRuntimeAutoStartDecision({
isDesktopLayout,
@@ -12972,7 +13015,7 @@ export function PlatformEntryFlowShellImpl({
(isVisualNovelCreationOpen && isVisualNovelLoadingLibrary) ||
isBabyObjectMatchBusy
? null
: (platformBootstrap.platformError ??
: (platformBootstrapErrorForDisplay ??
sessionController.agentWorkspaceRestoreError ??
bigFishError ??
match3dError ??
@@ -13093,7 +13136,7 @@ export function PlatformEntryFlowShellImpl({
platformError={
platformBootstrap.isLoadingPlatform
? null
: (platformBootstrap.platformError ??
: (platformBootstrapErrorForDisplay ??
sessionController.agentWorkspaceRestoreError)
}
dashboardError={
@@ -13122,6 +13165,7 @@ export function PlatformEntryFlowShellImpl({
onOpenRecommendGalleryDetail={openRecommendGalleryDetail}
recommendRuntimeContent={recommendRuntimeContent}
activeRecommendEntryKey={activeRecommendEntryKey}
isRecommendRuntimeReady={isActiveRecommendRuntimeReady}
isStartingRecommendEntry={
isStartingRecommendEntry ||
isBigFishBusy ||

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>
</>