merge: codex/auth-spacetime-fail-closed into master

This commit is contained in:
kdletters
2026-05-28 00:54:21 +08:00
4 changed files with 48 additions and 7 deletions

View File

@@ -276,7 +276,7 @@ RPG / 拼图等运行态存档仍以 `/api/profile/save-archives` 的后端列
- 创作 Tab 表单:填写作品标题、简介、主题 / 竞技背景描述、玩家形象描述、对手形象描述、拟声词和难度。拟声词支持换行、逗号、顿号、斜杠或竖线分隔;未手动编辑时随主题 / 形象描述自动重算,手动编辑后保持创作者自定义。 - 创作 Tab 表单:填写作品标题、简介、主题 / 竞技背景描述、玩家形象描述、对手形象描述、拟声词和难度。拟声词支持换行、逗号、顿号、斜杠或竖线分隔;未手动编辑时随主题 / 形象描述自动重算,手动编辑后保持创作者自定义。
- 草稿编译:`POST /api/creation/bark-battle/drafts` 写入配置 JSON返回包含 `draftId`、稳定 `workId``configVersion``rulesetVersion` 的草稿结果。 - 草稿编译:`POST /api/creation/bark-battle/drafts` 写入配置 JSON返回包含 `draftId`、稳定 `workId``configVersion``rulesetVersion` 的草稿结果。
- 生成页:`bark-battle-generating` 自动并行产出玩家形象、对手形象和竞技背景三图;前端生成页 UI 和其它玩法保持同一圆环主视觉,`media/create_bg_video.mp4` 作为固定全屏页面背景层循环静音播放,主进度圆环居中展示总进度,只保留当前步骤名称和当前步骤进度,不再渲染三行槽位列表。视频层需要显式触发播放。三图都走 Bark Battle 专用后端生图接口 `POST /api/creation/bark-battle/images/generate`,由后端按 `player-character``opponent-character``ui-background` 分别拼装正式提示词、写入 `generated-bark-battle-assets` 私有资产前缀并返回实际 prompt。玩家 / 对手形象提示词必须保持用户形象描述,不强行注入狗相关主体,并要求正面、单个完整形象和透明背景。部分失败也继续进入结果页。 - 生成页:`bark-battle-generating` 自动并行产出玩家形象、对手形象和竞技背景三图;前端生成页 UI 和其它玩法保持同一圆环主视觉,`media/create_bg_video.mp4` 作为固定全屏页面背景层循环静音播放,主进度圆环居中展示总进度,只保留当前步骤名称和当前步骤进度,不再渲染三行槽位列表。视频层需要显式触发播放。三图都走 Bark Battle 专用后端生图接口 `POST /api/creation/bark-battle/images/generate`,由后端按 `player-character``opponent-character``ui-background` 分别拼装正式提示词、写入 `generated-bark-battle-assets` 私有资产前缀并返回实际 prompt。玩家 / 对手形象提示词必须保持用户形象描述,不强行注入狗相关主体,并要求正面、单个完整形象和透明背景。部分失败也继续进入结果页。
- 结果页:围绕三图槽位展示错误态与已生成结果,只保留单槽重试、重新生成和上传,不再提供一次生成按钮、音频配置入口或排名配置。 - 结果页:围绕三图槽位展示错误态与已生成结果,只保留单槽重试、重新生成和上传,不再提供一次生成按钮、音频配置入口或排名配置;生成回写 `partial_failed` 时作品架不再显示整卡“生成中”遮罩,由结果页槽位错误承接失败
- 手动上传:结果页通过平台资产直传 `/api/assets/direct-upload-tickets``/api/assets/objects/confirm` 写入私有资产,再把返回的历史 generated 路径写回草稿配置。 - 手动上传:结果页通过平台资产直传 `/api/assets/direct-upload-tickets``/api/assets/objects/confirm` 写入私有资产,再把返回的历史 generated 路径写回草稿配置。
- 发布:结果页确认后必须携带草稿返回的同一个 `workId` 和结果页最终 `publishedSnapshot` 调用 `POST /api/creation/bark-battle/works/publish`SpacetimeDB 发布态的 `config_json` 必须使用该最终快照works summary 若拿到 `publishedSnapshotJson` 也优先使用最终快照映射封面三图。发布成功后先进入统一作品详情页,再由详情页进入正式 runtime缺少 `workId` 的旧草稿状态需要重新生成草稿。 - 发布:结果页确认后必须携带草稿返回的同一个 `workId` 和结果页最终 `publishedSnapshot` 调用 `POST /api/creation/bark-battle/works/publish`SpacetimeDB 发布态的 `config_json` 必须使用该最终快照works summary 若拿到 `publishedSnapshotJson` 也优先使用最终快照映射封面三图。发布成功后先进入统一作品详情页,再由详情页进入正式 runtime缺少 `workId` 的旧草稿状态需要重新生成草稿。
- 作品架Bark Battle 草稿 / 已发布列表优先读取后端 `/works`,但创建、生成完成、保存或发布后的本地摘要必须在后端 read model 尚未回读到同 `workId` 前继续保留;创作中心作品架同时接入 pending shelf 兜底,避免 ready 且三图齐全的草稿在刷新窗口期从“我的草稿 / 已发布”中消失。 - 作品架Bark Battle 草稿 / 已发布列表优先读取后端 `/works`,但创建、生成完成、保存或发布后的本地摘要必须在后端 read model 尚未回读到同 `workId` 前继续保留;创作中心作品架同时接入 pending shelf 兜底,避免 ready 且三图齐全的草稿在刷新窗口期从“我的草稿 / 已发布”中消失。

View File

@@ -1048,7 +1048,7 @@ test('buildCreationWorkShelfItems maps bark battle works with scene role cover a
); );
}); });
test('bark battle draft generating state follows pending assets or missing three images', () => { test('bark battle draft generating state only follows pending assets', () => {
const draft = { const draft = {
workId: 'bark-battle-work-draft', workId: 'bark-battle-work-draft',
draftId: 'bark-battle-draft-1', draftId: 'bark-battle-draft-1',
@@ -1073,6 +1073,12 @@ test('bark battle draft generating state follows pending assets or missing three
expect(hasBarkBattleRequiredImages(draft)).toBe(false); expect(hasBarkBattleRequiredImages(draft)).toBe(false);
expect(isPersistedBarkBattleDraftGenerating(draft)).toBe(true); expect(isPersistedBarkBattleDraftGenerating(draft)).toBe(true);
expect(
isPersistedBarkBattleDraftGenerating({
...draft,
generationStatus: 'partial_failed',
}),
).toBe(false);
expect( expect(
isPersistedBarkBattleDraftGenerating({ isPersistedBarkBattleDraftGenerating({
...draft, ...draft,

View File

@@ -1111,10 +1111,9 @@ export function isPersistedBarkBattleDraftGenerating(
return false; return false;
} }
return ( // 中文注释:汪汪声浪生成失败后会回写 partial_failed 并进入结果页承接错误槽位,
item.generationStatus === 'pending_assets' || // 不能因为三图未齐就继续把作品架整卡锁成“生成中”。
!hasBarkBattleRequiredImages(item) return item.generationStatus === 'pending_assets';
);
} }
export function hasBarkBattleRequiredImages(item: BarkBattleWorkSummary) { export function hasBarkBattleRequiredImages(item: BarkBattleWorkSummary) {

View File

@@ -2094,6 +2094,8 @@ function buildDraftCompletionDialogSource(
return formatPlatformTaskCompletionSource('方洞挑战草稿', sourceId); return formatPlatformTaskCompletionSource('方洞挑战草稿', sourceId);
case 'jump-hop': case 'jump-hop':
return formatPlatformTaskCompletionSource('跳一跳草稿', sourceId); return formatPlatformTaskCompletionSource('跳一跳草稿', sourceId);
case 'wooden-fish':
return formatPlatformTaskCompletionSource('敲木鱼草稿', sourceId);
case 'puzzle': case 'puzzle':
return formatPlatformTaskCompletionSource('拼图草稿', sourceId); return formatPlatformTaskCompletionSource('拼图草稿', sourceId);
case 'visual-novel': case 'visual-novel':
@@ -8887,6 +8889,8 @@ export function PlatformEntryFlowShellImpl({
setWoodenFishGenerationState(generationState); setWoodenFishGenerationState(generationState);
setIsWoodenFishBusy(true); setIsWoodenFishBusy(true);
setSelectionStage('wooden-fish-generating'); setSelectionStage('wooden-fish-generating');
markDraftGenerating('wooden-fish', [created.session.sessionId]);
markPendingDraftGenerating('wooden-fish', created.session.sessionId);
try { try {
const response = await woodenFishClient.executeAction( const response = await woodenFishClient.executeAction(
@@ -8921,6 +8925,30 @@ export function PlatformEntryFlowShellImpl({
setWoodenFishGenerationState( setWoodenFishGenerationState(
createReadyWoodenFishGenerationState(generationState), createReadyWoodenFishGenerationState(generationState),
); );
if (response.work) {
setWoodenFishWorks((current) => [
response.work!.summary,
...current.filter(
(item) => item.workId !== response.work!.summary.workId,
),
]);
markPendingDraftReady(
'wooden-fish',
created.session.sessionId,
false,
);
markDraftReady(
'wooden-fish',
[
created.session.sessionId,
response.work.summary.workId,
response.work.summary.profileId,
response.work.summary.sourceSessionId,
],
false,
);
void refreshWoodenFishShelf().catch(() => undefined);
}
setSelectionStage('wooden-fish-result'); setSelectionStage('wooden-fish-result');
} catch (error) { } catch (error) {
const errorMessage = resolveRpgCreationErrorMessage( const errorMessage = resolveRpgCreationErrorMessage(
@@ -8955,7 +8983,15 @@ export function PlatformEntryFlowShellImpl({
setIsWoodenFishBusy(false); setIsWoodenFishBusy(false);
} }
}, },
[createReadyWoodenFishGenerationState, setSelectionStage], [
createReadyWoodenFishGenerationState,
markDraftGenerating,
markDraftReady,
markPendingDraftGenerating,
markPendingDraftReady,
refreshWoodenFishShelf,
setSelectionStage,
],
); );
const retryWoodenFishDraftGeneration = useCallback(() => { const retryWoodenFishDraftGeneration = useCallback(() => {