refactor: 收口平台弹窗状态模型
This commit is contained in:
@@ -16,6 +16,14 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-06-03 平台入口弹窗状态规则收口
|
||||||
|
|
||||||
|
- 背景:`PlatformEntryFlowShellImpl.tsx` 曾同时持有平台级错误 / 完成弹窗的文案归一、来源格式、候选择一、dismiss key、后台生成 still-running 识别和任务完成文案,导致壳层 Interface 偏浅,测试面不稳定。
|
||||||
|
- 决策:新增 `src/components/platform-entry/platformDialogStateModel.ts` 作为 Platform Dialog State Module,统一导出 `normalizePlatformDialogMessage`、`formatPlatformDialogSource`、`resolvePlatformErrorDialog`、dismiss key builder、`resolveActivePlatformDialog`、`isBackgroundGenerationStillRunningMessage` 和 `PLATFORM_TASK_COMPLETION_MESSAGE`。平台壳只汇总候选、持有 React state,并在关闭弹窗时作为 Adapter 清理对应副作用 setter。
|
||||||
|
- 影响范围:平台入口错误弹窗、任务完成弹窗、后台生成仍在处理识别、草稿生成完成 / 失败通知。
|
||||||
|
- 验证方式:`npm run test -- src/components/platform-entry/platformDialogStateModel.test.ts`、`npm run test -- src/components/platform-entry/PlatformErrorDialog.test.tsx`、相关壳层交互测试、`npm run typecheck`、`npm run check:encoding`。
|
||||||
|
- 关联文档:`docs/technical/【前端架构】PlatformDialogStateModel收口计划-2026-06-03.md`。
|
||||||
|
|
||||||
## 2026-06-03 前端 SSE 客户端传输层统一收口
|
## 2026-06-03 前端 SSE 客户端传输层统一收口
|
||||||
|
|
||||||
- 背景:创作 Agent、创意互动 Agent、视觉小说运行态和微信充值订单状态等多个前端 client 曾各自手写 SSE 边界扫描、`TextDecoder` 解码、JSON 解析和流结束 flush,导致 CRLF / LF、UTF-8 尾部、多行 `data:` 和提前停止释放 reader 的处理容易漂移。
|
- 背景:创作 Agent、创意互动 Agent、视觉小说运行态和微信充值订单状态等多个前端 client 曾各自手写 SSE 边界扫描、`TextDecoder` 解码、JSON 解析和流结束 flush,导致 CRLF / LF、UTF-8 尾部、多行 `data:` 和提前停止释放 reader 的处理容易漂移。
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ AI 文字游戏模板接入以 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_
|
|||||||
|
|
||||||
平台入口创作恢复 URL 私有 query、拼图 runtime query 与拼图稳定身份互推收口到 `src/components/platform-entry/platformCreationUrlStateModel.ts` 和 `src/components/platform-entry/platformPuzzleIdentityModel.ts`,规则见 [【前端架构】CreationUrlStateModel收口计划-2026-06-03.md](./technical/【前端架构】CreationUrlStateModel收口计划-2026-06-03.md)。
|
平台入口创作恢复 URL 私有 query、拼图 runtime query 与拼图稳定身份互推收口到 `src/components/platform-entry/platformCreationUrlStateModel.ts` 和 `src/components/platform-entry/platformPuzzleIdentityModel.ts`,规则见 [【前端架构】CreationUrlStateModel收口计划-2026-06-03.md](./technical/【前端架构】CreationUrlStateModel收口计划-2026-06-03.md)。
|
||||||
|
|
||||||
|
平台入口错误 / 完成弹窗的文案归一、来源格式、候选择一、dismiss key 与任务完成文案收口到 `src/components/platform-entry/platformDialogStateModel.ts`,规则见 [【前端架构】PlatformDialogStateModel收口计划-2026-06-03.md](./technical/【前端架构】PlatformDialogStateModel收口计划-2026-06-03.md)。
|
||||||
|
|
||||||
小游戏 runtime client 的路径编码、JSON 请求、runtime guest auth 与 retry 选项收口到 `src/services/runtimeRequest.ts`,Match3D、SquareHole、Big Fish、Bark Battle、Puzzle 公开 / 推荐运行态请求、Jump Hop / Wooden Fish 正式 run 请求和 Visual Novel 局部 JSON runtime 请求已先迁移,规则见 [【前端架构】RuntimeClientFamily收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91RuntimeClientFamily%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
|
小游戏 runtime client 的路径编码、JSON 请求、runtime guest auth 与 retry 选项收口到 `src/services/runtimeRequest.ts`,Match3D、SquareHole、Big Fish、Bark Battle、Puzzle 公开 / 推荐运行态请求、Jump Hop / Wooden Fish 正式 run 请求和 Visual Novel 局部 JSON runtime 请求已先迁移,规则见 [【前端架构】RuntimeClientFamily收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91RuntimeClientFamily%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
|
||||||
|
|
||||||
抓大鹅 runtime profile 的公开详情转 work、session draft 转 profile、生成背景资产提升和 run/profile/public detail 素材优先级收口到 `src/components/platform-entry/platformMatch3DRuntimeProfile.ts`,规则见 [【前端架构】Match3DRuntimeProfile收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91Match3DRuntimeProfile%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
|
抓大鹅 runtime profile 的公开详情转 work、session draft 转 profile、生成背景资产提升和 run/profile/public detail 素材优先级收口到 `src/components/platform-entry/platformMatch3DRuntimeProfile.ts`,规则见 [【前端架构】Match3DRuntimeProfile收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91Match3DRuntimeProfile%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# PlatformDialogStateModel 收口计划
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
`PlatformEntryFlowShellImpl.tsx` 曾直接承载平台级错误 / 完成弹窗的纯状态规则:错误文案 trim、来源 label 与 id 拼接、后台生成仍在处理的识别、错误候选优先级、dismiss key 与生成完成文案都散在壳层 Implementation 内。壳层因此既要管理 React state 与副作用清理,又要记住弹窗判定细则;新增玩法错误或调整弹窗展示时缺少稳定测试面。
|
||||||
|
|
||||||
|
## 决策
|
||||||
|
|
||||||
|
- 新增 `src/components/platform-entry/platformDialogStateModel.ts` 作为 Platform Dialog State Module。
|
||||||
|
- Module Interface 收口:
|
||||||
|
- `normalizePlatformDialogMessage`
|
||||||
|
- `formatPlatformDialogSource`
|
||||||
|
- `isBackgroundGenerationStillRunningMessage`
|
||||||
|
- `resolvePlatformErrorDialog`
|
||||||
|
- `buildPlatformErrorDialogDismissKey`
|
||||||
|
- `buildPlatformTaskCompletionDialogDismissKey`
|
||||||
|
- `resolveActivePlatformDialog`
|
||||||
|
- `PLATFORM_TASK_COMPLETION_MESSAGE`
|
||||||
|
- `PlatformErrorDialogState`、`PlatformTaskFailureDialogState` 与 `PlatformTaskCompletionDialogState`
|
||||||
|
- `PlatformEntryFlowShellImpl.tsx` 继续作为 Adapter:汇总各玩法候选、持有 React state、关闭弹窗时清理对应 setter。副作用清理不下沉到 Module,避免把大量壳层 setter 变成浅 Interface。
|
||||||
|
|
||||||
|
## Interface 约束
|
||||||
|
|
||||||
|
- 错误与完成弹窗文案先 trim;空字符串或全空白字符串统一视为 `null`。
|
||||||
|
- 来源格式固定为 `label + 空格 + trimmed id`;缺 id 时只返回 label。
|
||||||
|
- 平台错误候选按数组顺序取第一个有效文案;候选本身只描述 `key/source/message`。
|
||||||
|
- 错误 dismiss key 固定为 `key:source:message`;完成 dismiss key 固定为 `key:source:message:completedAtMs`,缺完成时间时补 `0`。
|
||||||
|
- `resolveActivePlatformDialog` 只根据当前弹窗 dismiss key 与已记录 dismiss key 决定是否隐藏,不修改底层错误或完成状态。
|
||||||
|
- 任务完成弹窗文案统一使用 `PLATFORM_TASK_COMPLETION_MESSAGE`,不得在壳层重复写同一中文 literal。
|
||||||
|
- `closePlatformErrorDialog` 保持在壳层 Adapter;它负责按错误来源清理 `creationEntryConfigError`、玩法 error、作品详情 error 等副作用状态,不属于纯状态 Module。
|
||||||
|
|
||||||
|
## Depth / Leverage / Locality
|
||||||
|
|
||||||
|
- **Depth**:壳层传入候选和 dismiss 记录,即可得到当前平台弹窗状态;文案归一、来源格式和 dismiss 规则藏在 Module Implementation 内。
|
||||||
|
- **Leverage**:新增玩法错误来源时只需补候选;调整弹窗纯规则时优先改 Module 与单测。
|
||||||
|
- **Locality**:平台错误弹窗、任务完成弹窗和后台生成 still-running 识别集中在一个小 Module,避免继续散落在大型平台壳 Implementation 内。
|
||||||
|
|
||||||
|
## 验收
|
||||||
|
|
||||||
|
- `npm run test -- src/components/platform-entry/platformDialogStateModel.test.ts`
|
||||||
|
- `npm run test -- src/components/platform-entry/PlatformErrorDialog.test.tsx`
|
||||||
|
- `npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "background match3d draft failure notifies and reopens failed retry page|completed match3d draft notice first opens trial then reopens result|puzzle compile timeout shows failure dialog when reread session is still generating"`
|
||||||
|
- `npx eslint src/components/platform-entry/platformDialogStateModel.ts src/components/platform-entry/platformDialogStateModel.test.ts --max-warnings 0`
|
||||||
|
- `npx eslint src/components/platform-entry/PlatformEntryFlowShellImpl.tsx --quiet`
|
||||||
|
- `npm run typecheck`
|
||||||
|
- `npm run check:encoding`
|
||||||
@@ -421,6 +421,19 @@ import {
|
|||||||
hasPuzzleRuntimeUrlStateValue,
|
hasPuzzleRuntimeUrlStateValue,
|
||||||
normalizeCreationUrlValue,
|
normalizeCreationUrlValue,
|
||||||
} from './platformCreationUrlStateModel';
|
} from './platformCreationUrlStateModel';
|
||||||
|
import {
|
||||||
|
buildPlatformErrorDialogDismissKey,
|
||||||
|
buildPlatformTaskCompletionDialogDismissKey,
|
||||||
|
formatPlatformDialogSource,
|
||||||
|
isBackgroundGenerationStillRunningMessage,
|
||||||
|
PLATFORM_TASK_COMPLETION_MESSAGE,
|
||||||
|
type PlatformDialogCandidate,
|
||||||
|
type PlatformErrorDialogState,
|
||||||
|
type PlatformTaskCompletionDialogState,
|
||||||
|
type PlatformTaskFailureDialogState,
|
||||||
|
resolveActivePlatformDialog,
|
||||||
|
resolvePlatformErrorDialog,
|
||||||
|
} from './platformDialogStateModel';
|
||||||
import {
|
import {
|
||||||
buildCreationWorkShelfRuntimeState,
|
buildCreationWorkShelfRuntimeState,
|
||||||
buildDraftCompletionDialogSource,
|
buildDraftCompletionDialogSource,
|
||||||
@@ -480,10 +493,7 @@ import type {
|
|||||||
SelectionStage,
|
SelectionStage,
|
||||||
} from './platformEntryTypes';
|
} from './platformEntryTypes';
|
||||||
import { PlatformEntryWorldDetailView } from './PlatformEntryWorldDetailView';
|
import { PlatformEntryWorldDetailView } from './PlatformEntryWorldDetailView';
|
||||||
import {
|
import { PlatformErrorDialog } from './PlatformErrorDialog';
|
||||||
PlatformErrorDialog,
|
|
||||||
type PlatformErrorDialogPayload,
|
|
||||||
} from './PlatformErrorDialog';
|
|
||||||
import { PlatformFeedbackView } from './PlatformFeedbackView';
|
import { PlatformFeedbackView } from './PlatformFeedbackView';
|
||||||
import {
|
import {
|
||||||
buildMatch3DProfileFromSession,
|
buildMatch3DProfileFromSession,
|
||||||
@@ -509,10 +519,7 @@ import {
|
|||||||
buildPuzzleResultProfileId,
|
buildPuzzleResultProfileId,
|
||||||
buildPuzzleResultWorkId,
|
buildPuzzleResultWorkId,
|
||||||
} from './platformPuzzleIdentityModel';
|
} from './platformPuzzleIdentityModel';
|
||||||
import {
|
import { PlatformTaskCompletionDialog } from './PlatformTaskCompletionDialog';
|
||||||
PlatformTaskCompletionDialog,
|
|
||||||
type PlatformTaskCompletionDialogPayload,
|
|
||||||
} from './PlatformTaskCompletionDialog';
|
|
||||||
import { PlatformWorkDetailView } from './PlatformWorkDetailView';
|
import { PlatformWorkDetailView } from './PlatformWorkDetailView';
|
||||||
import { usePlatformCreationAgentFlowController } from './usePlatformCreationAgentFlowController';
|
import { usePlatformCreationAgentFlowController } from './usePlatformCreationAgentFlowController';
|
||||||
import { usePlatformEntryBootstrap } from './usePlatformEntryBootstrap';
|
import { usePlatformEntryBootstrap } from './usePlatformEntryBootstrap';
|
||||||
@@ -1507,39 +1514,6 @@ function buildWoodenFishPendingSession(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizePlatformErrorMessage(message: string | null | undefined) {
|
|
||||||
const normalized = message?.trim();
|
|
||||||
return normalized ? normalized : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatPlatformErrorSource(label: string, id?: string | null) {
|
|
||||||
const normalizedId = id?.trim();
|
|
||||||
return normalizedId ? `${label} ${normalizedId}` : label;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isBackgroundGenerationStillRunningMessage(message: string) {
|
|
||||||
return /仍在后台处理|后台仍在处理|仍在生成|后台生成/u.test(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildPlatformErrorDialogDismissKey(
|
|
||||||
error: (PlatformErrorDialogPayload & { key: string }) | null,
|
|
||||||
) {
|
|
||||||
return error ? `${error.key}:${error.source}:${error.message}` : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildPlatformTaskCompletionDialogDismissKey(
|
|
||||||
completion:
|
|
||||||
| (PlatformTaskCompletionDialogPayload & {
|
|
||||||
key: string;
|
|
||||||
completedAtMs: number | null;
|
|
||||||
})
|
|
||||||
| null,
|
|
||||||
) {
|
|
||||||
return completion
|
|
||||||
? `${completion.key}:${completion.source}:${completion.message}:${completion.completedAtMs ?? 0}`
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 为恢复的小游戏草稿重建生成态,保留后端开始时间作为进度事实源。 */
|
/** 为恢复的小游戏草稿重建生成态,保留后端开始时间作为进度事实源。 */
|
||||||
function createMiniGameDraftGenerationStateForRestoredDraft(
|
function createMiniGameDraftGenerationStateForRestoredDraft(
|
||||||
kind: MiniGameDraftGenerationKind,
|
kind: MiniGameDraftGenerationKind,
|
||||||
@@ -2733,23 +2707,11 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
const [
|
const [
|
||||||
pendingPlatformTaskCompletionDialog,
|
pendingPlatformTaskCompletionDialog,
|
||||||
setPendingPlatformTaskCompletionDialog,
|
setPendingPlatformTaskCompletionDialog,
|
||||||
] = useState<
|
] = useState<PlatformTaskCompletionDialogState | null>(null);
|
||||||
| (PlatformTaskCompletionDialogPayload & {
|
|
||||||
key: string;
|
|
||||||
completedAtMs: number | null;
|
|
||||||
})
|
|
||||||
| null
|
|
||||||
>(null);
|
|
||||||
const [
|
const [
|
||||||
pendingPlatformTaskFailureDialog,
|
pendingPlatformTaskFailureDialog,
|
||||||
setPendingPlatformTaskFailureDialog,
|
setPendingPlatformTaskFailureDialog,
|
||||||
] = useState<
|
] = useState<PlatformTaskFailureDialogState | null>(null);
|
||||||
| (PlatformErrorDialogPayload & {
|
|
||||||
key: string;
|
|
||||||
failedAtMs: number;
|
|
||||||
})
|
|
||||||
| null
|
|
||||||
>(null);
|
|
||||||
const [profileTaskRefreshKey, setProfileTaskRefreshKey] = useState(0);
|
const [profileTaskRefreshKey, setProfileTaskRefreshKey] = useState(0);
|
||||||
const [initialCreationUrlState] = useState(() => readCreationUrlState());
|
const [initialCreationUrlState] = useState(() => readCreationUrlState());
|
||||||
const handledInitialCreationUrlStateRef = useRef(false);
|
const handledInitialCreationUrlStateRef = useRef(false);
|
||||||
@@ -2916,7 +2878,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
setPendingPlatformTaskCompletionDialog({
|
setPendingPlatformTaskCompletionDialog({
|
||||||
key: `${kind}:${collectDraftNoticeKeys(kind, ids).join('|')}:${completedAtMs}`,
|
key: `${kind}:${collectDraftNoticeKeys(kind, ids).join('|')}:${completedAtMs}`,
|
||||||
source: buildDraftCompletionDialogSource(kind, ids),
|
source: buildDraftCompletionDialogSource(kind, ids),
|
||||||
message: '生成任务已完成,可以继续查看草稿。',
|
message: PLATFORM_TASK_COMPLETION_MESSAGE,
|
||||||
completedAtMs,
|
completedAtMs,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -5462,263 +5424,237 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
dismissedPlatformTaskCompletionDialogKey,
|
dismissedPlatformTaskCompletionDialogKey,
|
||||||
setDismissedPlatformTaskCompletionDialogKey,
|
setDismissedPlatformTaskCompletionDialogKey,
|
||||||
] = useState<string | null>(null);
|
] = useState<string | null>(null);
|
||||||
const currentPlatformErrorDialog = useMemo<
|
const currentPlatformErrorDialog =
|
||||||
(PlatformErrorDialogPayload & { key: string }) | null
|
useMemo<PlatformErrorDialogState | null>(() => {
|
||||||
>(() => {
|
const candidates: PlatformDialogCandidate[] = [
|
||||||
const candidates: Array<{
|
{
|
||||||
key: string;
|
key: pendingPlatformTaskFailureDialog?.key ?? 'draft-failure',
|
||||||
source: string;
|
source: pendingPlatformTaskFailureDialog?.source ?? '创作草稿',
|
||||||
message: string | null | undefined;
|
message: pendingPlatformTaskFailureDialog?.message,
|
||||||
}> = [
|
},
|
||||||
{
|
{
|
||||||
key: pendingPlatformTaskFailureDialog?.key ?? 'draft-failure',
|
key: 'creation-entry-config',
|
||||||
source: pendingPlatformTaskFailureDialog?.source ?? '创作草稿',
|
source: '创作入口配置',
|
||||||
message: pendingPlatformTaskFailureDialog?.message,
|
message: creationEntryConfigError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'creation-entry-config',
|
key: 'platform-bootstrap',
|
||||||
source: '创作入口配置',
|
source: '平台首页',
|
||||||
message: creationEntryConfigError,
|
message: platformBootstrap.platformError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'platform-bootstrap',
|
key: 'rpg-creation-type',
|
||||||
source: '平台首页',
|
source: '创作入口',
|
||||||
message: platformBootstrap.platformError,
|
message: sessionController.creationTypeError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'rpg-creation-type',
|
key: 'rpg-restore',
|
||||||
source: '创作入口',
|
source: '创作作品架',
|
||||||
message: sessionController.creationTypeError,
|
message: sessionController.agentWorkspaceRestoreError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'rpg-restore',
|
key: 'rpg-result',
|
||||||
source: '创作作品架',
|
source: formatPlatformDialogSource(
|
||||||
message: sessionController.agentWorkspaceRestoreError,
|
'RPG 草稿',
|
||||||
},
|
sessionController.agentSession?.sessionId ??
|
||||||
{
|
sessionController.generatedCustomWorldProfile?.id,
|
||||||
key: 'rpg-result',
|
),
|
||||||
source: formatPlatformErrorSource(
|
message: resultViewError,
|
||||||
'RPG 草稿',
|
},
|
||||||
sessionController.agentSession?.sessionId ??
|
{
|
||||||
sessionController.generatedCustomWorldProfile?.id,
|
key: 'public-work-detail',
|
||||||
),
|
source: formatPlatformDialogSource(
|
||||||
message: resultViewError,
|
'作品详情',
|
||||||
},
|
selectedPublicWorkDetail
|
||||||
{
|
? resolvePlatformPublicWorkCode(selectedPublicWorkDetail)
|
||||||
key: 'public-work-detail',
|
: selectedDetailEntry?.profileId,
|
||||||
source: formatPlatformErrorSource(
|
),
|
||||||
'作品详情',
|
message: publicWorkDetailError ?? detailNavigation.detailError,
|
||||||
selectedPublicWorkDetail
|
},
|
||||||
? resolvePlatformPublicWorkCode(selectedPublicWorkDetail)
|
{
|
||||||
: selectedDetailEntry?.profileId,
|
key: 'big-fish',
|
||||||
),
|
source: formatPlatformDialogSource(
|
||||||
message: publicWorkDetailError ?? detailNavigation.detailError,
|
selectionStage === 'big-fish-runtime'
|
||||||
},
|
? '大鱼吃小鱼游玩'
|
||||||
{
|
: '大鱼草稿',
|
||||||
key: 'big-fish',
|
bigFishRun?.runId ?? bigFishSession?.sessionId,
|
||||||
source: formatPlatformErrorSource(
|
),
|
||||||
selectionStage === 'big-fish-runtime' ? '大鱼吃小鱼游玩' : '大鱼草稿',
|
message: bigFishError,
|
||||||
bigFishRun?.runId ?? bigFishSession?.sessionId,
|
},
|
||||||
),
|
{
|
||||||
message: bigFishError,
|
key: 'match3d',
|
||||||
},
|
source: formatPlatformDialogSource(
|
||||||
{
|
selectionStage === 'match3d-runtime' ? '抓大鹅游玩' : '抓大鹅草稿',
|
||||||
key: 'match3d',
|
match3dRun?.runId ??
|
||||||
source: formatPlatformErrorSource(
|
match3dGenerationViewSession?.sessionId ??
|
||||||
selectionStage === 'match3d-runtime' ? '抓大鹅游玩' : '抓大鹅草稿',
|
match3dSession?.sessionId,
|
||||||
match3dRun?.runId ??
|
),
|
||||||
match3dGenerationViewSession?.sessionId ??
|
message: match3dGenerationViewError ?? match3dError,
|
||||||
match3dSession?.sessionId,
|
},
|
||||||
),
|
{
|
||||||
message: match3dGenerationViewError ?? match3dError,
|
key: 'square-hole',
|
||||||
},
|
source: formatPlatformDialogSource(
|
||||||
{
|
selectionStage === 'square-hole-runtime'
|
||||||
key: 'square-hole',
|
? '方洞挑战游玩'
|
||||||
source: formatPlatformErrorSource(
|
: '方洞挑战草稿',
|
||||||
selectionStage === 'square-hole-runtime'
|
squareHoleRun?.runId ?? squareHoleSession?.sessionId,
|
||||||
? '方洞挑战游玩'
|
),
|
||||||
: '方洞挑战草稿',
|
message: squareHoleError,
|
||||||
squareHoleRun?.runId ?? squareHoleSession?.sessionId,
|
},
|
||||||
),
|
{
|
||||||
message: squareHoleError,
|
key: 'jump-hop',
|
||||||
},
|
source: formatPlatformDialogSource(
|
||||||
{
|
selectionStage === 'jump-hop-runtime' ? '跳一跳游玩' : '跳一跳草稿',
|
||||||
key: 'jump-hop',
|
jumpHopRun?.runId ?? jumpHopSession?.sessionId,
|
||||||
source: formatPlatformErrorSource(
|
),
|
||||||
selectionStage === 'jump-hop-runtime' ? '跳一跳游玩' : '跳一跳草稿',
|
message: jumpHopError,
|
||||||
jumpHopRun?.runId ?? jumpHopSession?.sessionId,
|
},
|
||||||
),
|
{
|
||||||
message: jumpHopError,
|
key: 'wooden-fish',
|
||||||
},
|
source: formatPlatformDialogSource(
|
||||||
{
|
selectionStage === 'wooden-fish-runtime'
|
||||||
key: 'wooden-fish',
|
? '敲木鱼游玩'
|
||||||
source: formatPlatformErrorSource(
|
: '敲木鱼草稿',
|
||||||
selectionStage === 'wooden-fish-runtime'
|
woodenFishRun?.runId ?? woodenFishSession?.sessionId,
|
||||||
? '敲木鱼游玩'
|
),
|
||||||
: '敲木鱼草稿',
|
message: woodenFishError,
|
||||||
woodenFishRun?.runId ?? woodenFishSession?.sessionId,
|
},
|
||||||
),
|
{
|
||||||
message: woodenFishError,
|
key: 'puzzle',
|
||||||
},
|
source: formatPlatformDialogSource(
|
||||||
{
|
selectionStage === 'puzzle-runtime' ? '拼图游玩' : '拼图草稿',
|
||||||
key: 'puzzle',
|
puzzleRun?.runId ??
|
||||||
source: formatPlatformErrorSource(
|
puzzleGenerationViewSession?.sessionId ??
|
||||||
selectionStage === 'puzzle-runtime' ? '拼图游玩' : '拼图草稿',
|
puzzleSession?.sessionId,
|
||||||
puzzleRun?.runId ??
|
),
|
||||||
puzzleGenerationViewSession?.sessionId ??
|
message:
|
||||||
puzzleSession?.sessionId,
|
puzzleGenerationViewError ?? puzzleCreationError ?? puzzleError,
|
||||||
),
|
},
|
||||||
message:
|
{
|
||||||
puzzleGenerationViewError ?? puzzleCreationError ?? puzzleError,
|
key: 'puzzle-onboarding',
|
||||||
},
|
source: '拼图首次创作',
|
||||||
{
|
message: puzzleOnboardingError,
|
||||||
key: 'puzzle-onboarding',
|
},
|
||||||
source: '拼图首次创作',
|
{
|
||||||
message: puzzleOnboardingError,
|
key: 'puzzle-shelf',
|
||||||
},
|
source: '拼图作品架',
|
||||||
{
|
message: puzzleShelfError,
|
||||||
key: 'puzzle-shelf',
|
},
|
||||||
source: '拼图作品架',
|
{
|
||||||
message: puzzleShelfError,
|
key: 'visual-novel',
|
||||||
},
|
source: formatPlatformDialogSource(
|
||||||
{
|
selectionStage === 'visual-novel-runtime'
|
||||||
key: 'visual-novel',
|
? '视觉小说游玩'
|
||||||
source: formatPlatformErrorSource(
|
: '视觉小说草稿',
|
||||||
selectionStage === 'visual-novel-runtime'
|
visualNovelRun?.runId ?? visualNovelSession?.sessionId,
|
||||||
? '视觉小说游玩'
|
),
|
||||||
: '视觉小说草稿',
|
message: visualNovelError,
|
||||||
visualNovelRun?.runId ?? visualNovelSession?.sessionId,
|
},
|
||||||
),
|
{
|
||||||
message: visualNovelError,
|
key: 'baby-object-match',
|
||||||
},
|
source: formatPlatformDialogSource(
|
||||||
{
|
selectionStage === 'baby-object-match-runtime'
|
||||||
key: 'baby-object-match',
|
? '宝贝识物游玩'
|
||||||
source: formatPlatformErrorSource(
|
: '宝贝识物草稿',
|
||||||
selectionStage === 'baby-object-match-runtime'
|
babyObjectMatchDraft?.profileId,
|
||||||
? '宝贝识物游玩'
|
),
|
||||||
: '宝贝识物草稿',
|
message: babyObjectMatchError,
|
||||||
babyObjectMatchDraft?.profileId,
|
},
|
||||||
),
|
{
|
||||||
message: babyObjectMatchError,
|
key: 'bark-battle',
|
||||||
},
|
source: formatPlatformDialogSource(
|
||||||
{
|
selectionStage === 'bark-battle-runtime'
|
||||||
key: 'bark-battle',
|
? '汪汪声浪游玩'
|
||||||
source: formatPlatformErrorSource(
|
: '汪汪声浪草稿',
|
||||||
selectionStage === 'bark-battle-runtime'
|
barkBattlePublishedConfig?.workId ?? barkBattleDraftConfig?.workId,
|
||||||
? '汪汪声浪游玩'
|
),
|
||||||
: '汪汪声浪草稿',
|
message: barkBattleError,
|
||||||
barkBattlePublishedConfig?.workId ?? barkBattleDraftConfig?.workId,
|
},
|
||||||
),
|
{
|
||||||
message: barkBattleError,
|
key: 'creative-agent',
|
||||||
},
|
source: formatPlatformDialogSource(
|
||||||
{
|
'智能创作 Agent',
|
||||||
key: 'creative-agent',
|
creativeAgentSession?.sessionId,
|
||||||
source: formatPlatformErrorSource(
|
),
|
||||||
'智能创作 Agent',
|
message: creativeAgentError,
|
||||||
creativeAgentSession?.sessionId,
|
},
|
||||||
),
|
{
|
||||||
message: creativeAgentError,
|
key: 'rpg-generation',
|
||||||
},
|
source: formatPlatformDialogSource(
|
||||||
{
|
'RPG 草稿生成',
|
||||||
key: 'rpg-generation',
|
sessionController.agentSession?.sessionId,
|
||||||
source: formatPlatformErrorSource(
|
),
|
||||||
'RPG 草稿生成',
|
message: sessionController.activeGenerationError,
|
||||||
sessionController.agentSession?.sessionId,
|
},
|
||||||
),
|
];
|
||||||
message: sessionController.activeGenerationError,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const candidate of candidates) {
|
return resolvePlatformErrorDialog(candidates);
|
||||||
const message = normalizePlatformErrorMessage(candidate.message);
|
}, [
|
||||||
if (message) {
|
babyObjectMatchDraft?.profileId,
|
||||||
return {
|
babyObjectMatchError,
|
||||||
key: candidate.key,
|
barkBattleDraftConfig?.workId,
|
||||||
source: candidate.source,
|
barkBattleError,
|
||||||
message,
|
barkBattlePublishedConfig?.workId,
|
||||||
};
|
bigFishError,
|
||||||
}
|
bigFishRun?.runId,
|
||||||
}
|
bigFishSession?.sessionId,
|
||||||
|
creationEntryConfigError,
|
||||||
return null;
|
creativeAgentError,
|
||||||
}, [
|
creativeAgentSession?.sessionId,
|
||||||
babyObjectMatchDraft?.profileId,
|
detailNavigation.detailError,
|
||||||
babyObjectMatchError,
|
jumpHopError,
|
||||||
barkBattleDraftConfig?.workId,
|
jumpHopRun?.runId,
|
||||||
barkBattleError,
|
jumpHopSession?.sessionId,
|
||||||
barkBattlePublishedConfig?.workId,
|
match3dError,
|
||||||
bigFishError,
|
match3dGenerationViewError,
|
||||||
bigFishRun?.runId,
|
match3dGenerationViewSession?.sessionId,
|
||||||
bigFishSession?.sessionId,
|
match3dRun?.runId,
|
||||||
creationEntryConfigError,
|
match3dSession?.sessionId,
|
||||||
creativeAgentError,
|
pendingPlatformTaskFailureDialog,
|
||||||
creativeAgentSession?.sessionId,
|
platformBootstrap.platformError,
|
||||||
detailNavigation.detailError,
|
publicWorkDetailError,
|
||||||
jumpHopError,
|
puzzleCreationError,
|
||||||
jumpHopRun?.runId,
|
puzzleError,
|
||||||
jumpHopSession?.sessionId,
|
puzzleGenerationViewError,
|
||||||
match3dError,
|
puzzleGenerationViewSession?.sessionId,
|
||||||
match3dGenerationViewError,
|
puzzleOnboardingError,
|
||||||
match3dGenerationViewSession?.sessionId,
|
puzzleRun?.runId,
|
||||||
match3dRun?.runId,
|
puzzleSession?.sessionId,
|
||||||
match3dSession?.sessionId,
|
puzzleShelfError,
|
||||||
pendingPlatformTaskFailureDialog,
|
resultViewError,
|
||||||
platformBootstrap.platformError,
|
selectedDetailEntry?.profileId,
|
||||||
publicWorkDetailError,
|
selectedPublicWorkDetail,
|
||||||
puzzleCreationError,
|
selectionStage,
|
||||||
puzzleError,
|
sessionController.activeGenerationError,
|
||||||
puzzleGenerationViewError,
|
sessionController.agentSession?.sessionId,
|
||||||
puzzleGenerationViewSession?.sessionId,
|
sessionController.agentWorkspaceRestoreError,
|
||||||
puzzleOnboardingError,
|
sessionController.creationTypeError,
|
||||||
puzzleRun?.runId,
|
sessionController.generatedCustomWorldProfile?.id,
|
||||||
puzzleSession?.sessionId,
|
squareHoleError,
|
||||||
puzzleShelfError,
|
squareHoleRun?.runId,
|
||||||
resultViewError,
|
squareHoleSession?.sessionId,
|
||||||
selectedDetailEntry?.profileId,
|
visualNovelError,
|
||||||
selectedPublicWorkDetail,
|
visualNovelRun?.runId,
|
||||||
selectionStage,
|
visualNovelSession?.sessionId,
|
||||||
sessionController.activeGenerationError,
|
woodenFishError,
|
||||||
sessionController.agentSession?.sessionId,
|
woodenFishRun?.runId,
|
||||||
sessionController.agentWorkspaceRestoreError,
|
woodenFishSession?.sessionId,
|
||||||
sessionController.creationTypeError,
|
]);
|
||||||
sessionController.generatedCustomWorldProfile?.id,
|
const currentPlatformTaskCompletionDialog =
|
||||||
squareHoleError,
|
useMemo<PlatformTaskCompletionDialogState | null>(
|
||||||
squareHoleRun?.runId,
|
() => pendingPlatformTaskCompletionDialog,
|
||||||
squareHoleSession?.sessionId,
|
[pendingPlatformTaskCompletionDialog],
|
||||||
visualNovelError,
|
|
||||||
visualNovelRun?.runId,
|
|
||||||
visualNovelSession?.sessionId,
|
|
||||||
woodenFishError,
|
|
||||||
woodenFishRun?.runId,
|
|
||||||
woodenFishSession?.sessionId,
|
|
||||||
]);
|
|
||||||
const currentPlatformTaskCompletionDialog = useMemo<
|
|
||||||
| (PlatformTaskCompletionDialogPayload & {
|
|
||||||
key: string;
|
|
||||||
completedAtMs: number | null;
|
|
||||||
})
|
|
||||||
| null
|
|
||||||
>(
|
|
||||||
() => pendingPlatformTaskCompletionDialog,
|
|
||||||
[pendingPlatformTaskCompletionDialog],
|
|
||||||
);
|
|
||||||
const activePlatformTaskCompletionDialogDismissKey =
|
|
||||||
buildPlatformTaskCompletionDialogDismissKey(
|
|
||||||
currentPlatformTaskCompletionDialog,
|
|
||||||
);
|
);
|
||||||
const activePlatformTaskCompletionDialog =
|
const activePlatformTaskCompletionDialog = resolveActivePlatformDialog(
|
||||||
activePlatformTaskCompletionDialogDismissKey &&
|
currentPlatformTaskCompletionDialog,
|
||||||
activePlatformTaskCompletionDialogDismissKey ===
|
dismissedPlatformTaskCompletionDialogKey,
|
||||||
dismissedPlatformTaskCompletionDialogKey
|
buildPlatformTaskCompletionDialogDismissKey,
|
||||||
? null
|
);
|
||||||
: currentPlatformTaskCompletionDialog;
|
const activePlatformErrorDialog = resolveActivePlatformDialog(
|
||||||
const activePlatformErrorDialogDismissKey =
|
currentPlatformErrorDialog,
|
||||||
buildPlatformErrorDialogDismissKey(currentPlatformErrorDialog);
|
dismissedPlatformErrorDialogKey,
|
||||||
const activePlatformErrorDialog =
|
buildPlatformErrorDialogDismissKey,
|
||||||
activePlatformErrorDialogDismissKey &&
|
);
|
||||||
activePlatformErrorDialogDismissKey === dismissedPlatformErrorDialogKey
|
|
||||||
? null
|
|
||||||
: currentPlatformErrorDialog;
|
|
||||||
const closePlatformErrorDialog = useCallback(() => {
|
const closePlatformErrorDialog = useCallback(() => {
|
||||||
if (!currentPlatformErrorDialog) {
|
if (!currentPlatformErrorDialog) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
113
src/components/platform-entry/platformDialogStateModel.test.ts
Normal file
113
src/components/platform-entry/platformDialogStateModel.test.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildPlatformErrorDialogDismissKey,
|
||||||
|
buildPlatformTaskCompletionDialogDismissKey,
|
||||||
|
formatPlatformDialogSource,
|
||||||
|
isBackgroundGenerationStillRunningMessage,
|
||||||
|
normalizePlatformDialogMessage,
|
||||||
|
PLATFORM_TASK_COMPLETION_MESSAGE,
|
||||||
|
resolveActivePlatformDialog,
|
||||||
|
resolvePlatformErrorDialog,
|
||||||
|
} from './platformDialogStateModel';
|
||||||
|
|
||||||
|
describe('platformDialogStateModel', () => {
|
||||||
|
test('normalizes platform dialog messages', () => {
|
||||||
|
expect(normalizePlatformDialogMessage(' 图片失败 ')).toBe('图片失败');
|
||||||
|
expect(normalizePlatformDialogMessage(' ')).toBeNull();
|
||||||
|
expect(normalizePlatformDialogMessage(null)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('formats dialog source with optional identity', () => {
|
||||||
|
expect(formatPlatformDialogSource('拼图草稿', ' puzzle-session-1 ')).toBe(
|
||||||
|
'拼图草稿 puzzle-session-1',
|
||||||
|
);
|
||||||
|
expect(formatPlatformDialogSource('拼图草稿', ' ')).toBe('拼图草稿');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('detects background generation still running messages', () => {
|
||||||
|
expect(
|
||||||
|
isBackgroundGenerationStillRunningMessage('后台仍在处理,请稍后查看。'),
|
||||||
|
).toBe(true);
|
||||||
|
expect(isBackgroundGenerationStillRunningMessage('素材生成失败。')).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolves the first non-empty error candidate', () => {
|
||||||
|
expect(
|
||||||
|
resolvePlatformErrorDialog([
|
||||||
|
{
|
||||||
|
key: 'empty',
|
||||||
|
source: '空来源',
|
||||||
|
message: ' ',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'puzzle',
|
||||||
|
source: '拼图草稿 puzzle-session-1',
|
||||||
|
message: ' 素材生成失败。 ',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
).toEqual({
|
||||||
|
key: 'puzzle',
|
||||||
|
source: '拼图草稿 puzzle-session-1',
|
||||||
|
message: '素材生成失败。',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
resolvePlatformErrorDialog([
|
||||||
|
{
|
||||||
|
key: 'empty',
|
||||||
|
source: '空来源',
|
||||||
|
message: null,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('builds stable dismiss keys for error and completion dialogs', () => {
|
||||||
|
expect(
|
||||||
|
buildPlatformErrorDialogDismissKey({
|
||||||
|
key: 'puzzle',
|
||||||
|
source: '拼图草稿 puzzle-session-1',
|
||||||
|
message: '素材生成失败。',
|
||||||
|
}),
|
||||||
|
).toBe('puzzle:拼图草稿 puzzle-session-1:素材生成失败。');
|
||||||
|
expect(buildPlatformErrorDialogDismissKey(null)).toBeNull();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
buildPlatformTaskCompletionDialogDismissKey({
|
||||||
|
key: 'match3d',
|
||||||
|
source: '抓大鹅草稿 match3d-session-1',
|
||||||
|
message: PLATFORM_TASK_COMPLETION_MESSAGE,
|
||||||
|
completedAtMs: null,
|
||||||
|
}),
|
||||||
|
).toBe(
|
||||||
|
`match3d:抓大鹅草稿 match3d-session-1:${PLATFORM_TASK_COMPLETION_MESSAGE}:0`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hides active dialog when the dismiss key has already been recorded', () => {
|
||||||
|
const dialog = {
|
||||||
|
key: 'puzzle',
|
||||||
|
source: '拼图草稿 puzzle-session-1',
|
||||||
|
message: '素材生成失败。',
|
||||||
|
};
|
||||||
|
const dismissKey = buildPlatformErrorDialogDismissKey(dialog);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
resolveActivePlatformDialog(
|
||||||
|
dialog,
|
||||||
|
dismissKey,
|
||||||
|
buildPlatformErrorDialogDismissKey,
|
||||||
|
),
|
||||||
|
).toBeNull();
|
||||||
|
expect(
|
||||||
|
resolveActivePlatformDialog(
|
||||||
|
dialog,
|
||||||
|
'other-dismiss-key',
|
||||||
|
buildPlatformErrorDialogDismissKey,
|
||||||
|
),
|
||||||
|
).toBe(dialog);
|
||||||
|
});
|
||||||
|
});
|
||||||
85
src/components/platform-entry/platformDialogStateModel.ts
Normal file
85
src/components/platform-entry/platformDialogStateModel.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import type { PlatformErrorDialogPayload } from './PlatformErrorDialog';
|
||||||
|
import type { PlatformTaskCompletionDialogPayload } from './PlatformTaskCompletionDialog';
|
||||||
|
|
||||||
|
export type PlatformErrorDialogState = PlatformErrorDialogPayload & {
|
||||||
|
key: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PlatformTaskFailureDialogState = PlatformErrorDialogState & {
|
||||||
|
failedAtMs: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PlatformTaskCompletionDialogState =
|
||||||
|
PlatformTaskCompletionDialogPayload & {
|
||||||
|
key: string;
|
||||||
|
completedAtMs: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PlatformDialogCandidate = {
|
||||||
|
key: string;
|
||||||
|
source: string;
|
||||||
|
message: string | null | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PLATFORM_TASK_COMPLETION_MESSAGE =
|
||||||
|
'生成任务已完成,可以继续查看草稿。';
|
||||||
|
|
||||||
|
/** 收口平台弹窗候选的纯状态规则,壳层只负责副作用清理。 */
|
||||||
|
export function normalizePlatformDialogMessage(
|
||||||
|
message: string | null | undefined,
|
||||||
|
) {
|
||||||
|
const normalized = message?.trim();
|
||||||
|
return normalized ? normalized : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatPlatformDialogSource(label: string, id?: string | null) {
|
||||||
|
const normalizedId = id?.trim();
|
||||||
|
return normalizedId ? `${label} ${normalizedId}` : label;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBackgroundGenerationStillRunningMessage(message: string) {
|
||||||
|
return /仍在后台处理|后台仍在处理|仍在生成|后台生成/u.test(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolvePlatformErrorDialog(
|
||||||
|
candidates: readonly PlatformDialogCandidate[],
|
||||||
|
): PlatformErrorDialogState | null {
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const message = normalizePlatformDialogMessage(candidate.message);
|
||||||
|
if (message) {
|
||||||
|
return {
|
||||||
|
key: candidate.key,
|
||||||
|
source: candidate.source,
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPlatformErrorDialogDismissKey(
|
||||||
|
error: PlatformErrorDialogState | null,
|
||||||
|
) {
|
||||||
|
return error ? `${error.key}:${error.source}:${error.message}` : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPlatformTaskCompletionDialogDismissKey(
|
||||||
|
completion: PlatformTaskCompletionDialogState | null,
|
||||||
|
) {
|
||||||
|
return completion
|
||||||
|
? `${completion.key}:${completion.source}:${completion.message}:${completion.completedAtMs ?? 0}`
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveActivePlatformDialog<TDialog>(
|
||||||
|
currentDialog: TDialog | null,
|
||||||
|
dismissedDialogKey: string | null,
|
||||||
|
buildDismissKey: (dialog: TDialog | null) => string | null,
|
||||||
|
): TDialog | null {
|
||||||
|
const currentDialogDismissKey = buildDismissKey(currentDialog);
|
||||||
|
return currentDialogDismissKey &&
|
||||||
|
currentDialogDismissKey === dismissedDialogKey
|
||||||
|
? null
|
||||||
|
: currentDialog;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user