fix: 修正入口熔断与跳一跳草稿判定

This commit is contained in:
2026-06-04 17:43:56 +08:00
parent bbb9269bab
commit b60382a752
5 changed files with 116 additions and 57 deletions

View File

@@ -34,6 +34,8 @@ RPG Agent 结果页发布门禁展示由 `platformRpgAgentResultPreviewModel.ts`
生成任务在用户离开生成页后异步完成时,平台壳层必须弹出 `PlatformTaskCompletionDialog`。完成弹窗同样要带来源,例如某个草稿或生成会话,并提供复制按钮复制“来源 + 状态”;如果用户仍停留在生成页并被自动带入结果页或试玩页,生成页 / 结果页本身即为完成反馈,不再额外叠加完成弹窗。
入口配置中的 `open=false` 表示关闭新建创作入口不表示下架已有草稿、私有作品或公开作品。api-server 的入口熔断只允许拦截新建创作、新建草稿、首次生成入口和 Remix 成草稿等会产生新创作的请求;公开广场列表、公开详情、点赞、已发布作品启动、运行态过程请求、存档 / 浏览记录和已有作品回读不能因为创作入口关闭而返回 `creation_entry_disabled`。平台首页如果遇到旧服务端返回的 `creation_entry_disabled`,只能降级为空列表或隐藏入口,不弹平台级错误弹窗。
`PlatformEntryFlowShellImpl.tsx` 仍是平台入口编排壳,后续维护时应优先把独立 UI 片段、公开作品映射、草稿生成 notice 和运行态状态 helper 拆到 `src/components/platform-entry/PlatformEntryFlowShellImpl/` 或同目录紧邻 helper 文件。拆分只允许改变文件组织不改变入口配置事实源、默认导出、props、页面阶段、UI 文案或现有交互;其中拼图首访 onboarding 已拆为 `PlatformEntryFlowShellImpl/PuzzleOnboardingView.tsx`
`platformEntryCreationTypes.ts` 只做前端展示派生,分组时必须把后端 `creationTypes` 里的 `categoryId` / `categoryLabel` 当作可缺失字段处理,空值统一回退到 `recommended` / `热门推荐`,并把历史 `recent` / `最近创作` 归一到推荐分类。`最近创作` 不属于模板分类页签,只能由 7 天内的真实草稿 / 作品架后端数据决定是否展示;展示内容仍然从后端入口配置的模板卡中筛选,不读取或渲染作品标题、作品摘要、草稿阶段文案。

View File

@@ -740,7 +740,8 @@ mod tests {
let response = app
.oneshot(
Request::builder()
.uri("/api/runtime/puzzle/works")
.method("POST")
.uri("/api/runtime/puzzle/agent/sessions")
.body(Body::empty())
.expect("request should build"),
)
@@ -756,6 +757,31 @@ mod tests {
assert_eq!(body["error"]["details"]["creationTypeId"], "puzzle");
}
#[tokio::test]
async fn disabled_creation_entry_does_not_block_published_runtime_routes() {
let state = AppState::new(AppConfig::default()).expect("state should build");
state.set_test_creation_entry_route_enabled("puzzle", false);
let app = build_router(state);
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/runtime/puzzle/runs")
.body(Body::empty())
.expect("request should build"),
)
.await
.expect("request should succeed");
assert_ne!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
let body = read_json_response(response).await;
assert_ne!(
body["error"]["details"]["reason"],
"creation_entry_disabled"
);
}
#[tokio::test]
async fn disabled_visual_novel_creation_route_returns_service_unavailable() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
@@ -789,7 +815,7 @@ mod tests {
}
#[tokio::test]
async fn disabled_rpg_route_returns_service_unavailable() {
async fn disabled_rpg_creation_route_returns_service_unavailable() {
let state = AppState::new(AppConfig::default()).expect("state should build");
state.set_test_creation_entry_route_enabled("rpg", false);
let app = build_router(state);

View File

@@ -34,7 +34,7 @@ pub async fn get_creation_entry_config_handler(
Ok(json_success_body(Some(&request_context), config))
}
/// 中文注释api-server 路由熔断只拦创作/运行态 API 请求,不改变前端入口展示规则
/// 中文注释api-server 路由熔断只拦新建创作入口,不限制已有作品读取、发布作品游玩或公开广场浏览
pub async fn require_creation_entry_route_enabled(
State(state): State<AppState>,
request: Request<Body>,
@@ -72,54 +72,56 @@ pub async fn require_creation_entry_route_enabled(
pub fn resolve_creation_entry_route_id(path: &str) -> Option<&'static str> {
let normalized = path.trim_end_matches('/');
if normalized.starts_with("/api/runtime/puzzle") {
if normalized == "/api/runtime/puzzle/agent/sessions"
|| normalized == "/api/runtime/puzzle/onboarding/generate"
{
return Some("puzzle");
}
if normalized.starts_with("/api/runtime/match3d") {
return Some("match3d");
if normalized.starts_with("/api/runtime/puzzle/gallery/")
&& normalized.ends_with("/remix")
{
return Some("puzzle");
}
if normalized.starts_with("/api/runtime/bark-battle") {
return Some("bark-battle");
}
if normalized.starts_with("/api/creation/bark-battle") {
return Some("bark-battle");
}
if normalized.starts_with("/api/runtime/wooden-fish") {
return Some("wooden-fish");
}
if normalized.starts_with("/api/creation/wooden-fish") {
return Some("wooden-fish");
}
if normalized.starts_with("/api/runtime/square-hole") {
return Some("square-hole");
}
if normalized.starts_with("/api/runtime/jump-hop") {
return Some("jump-hop");
}
if normalized.starts_with("/api/creation/jump-hop") {
return Some("jump-hop");
}
if normalized.starts_with("/api/runtime/big-fish") {
if normalized == "/api/runtime/big-fish/agent/sessions" {
return Some("big-fish");
}
if normalized.starts_with("/api/runtime/custom-world")
|| normalized.starts_with("/api/runtime/custom-world-library")
|| normalized.starts_with("/api/runtime/custom-world-gallery")
|| normalized.starts_with("/api/runtime/chat")
|| normalized.starts_with("/api/story")
if normalized.starts_with("/api/runtime/big-fish/gallery/")
&& normalized.ends_with("/remix")
{
return Some("big-fish");
}
if normalized == "/api/runtime/custom-world/agent/sessions"
|| normalized == "/api/runtime/custom-world/profile"
{
return Some("rpg");
}
if normalized.starts_with("/api/runtime/visual-novel") {
if normalized.starts_with("/api/runtime/custom-world-gallery/")
&& normalized.ends_with("/remix")
{
return Some("rpg");
}
if normalized == "/api/creation/match3d/sessions" {
return Some("match3d");
}
if normalized == "/api/creation/square-hole/sessions" {
return Some("square-hole");
}
if normalized == "/api/creation/bark-battle/drafts" {
return Some("bark-battle");
}
if normalized == "/api/creation/wooden-fish/sessions" {
return Some("wooden-fish");
}
if normalized == "/api/creation/jump-hop/sessions" {
return Some("jump-hop");
}
if normalized == "/api/creation/visual-novel/sessions" {
return Some("visual-novel");
}
if normalized.starts_with("/api/creation/visual-novel") {
return Some("visual-novel");
}
if normalized.starts_with("/api/creation/edutainment/baby-object-match") {
if normalized == "/api/creation/edutainment/baby-object-match/assets" {
return Some("baby-object-match");
}
if normalized.starts_with("/api/creation/edutainment/baby-love-drawing") {
if normalized == "/api/creation/edutainment/baby-love-drawing/magic" {
return Some("baby-love-drawing");
}
None
@@ -171,58 +173,68 @@ mod tests {
use super::*;
#[test]
fn resolves_runtime_paths_to_creation_type_ids() {
fn resolves_new_creation_paths_to_creation_type_ids() {
assert_eq!(
resolve_creation_entry_route_id("/api/runtime/puzzle/works"),
resolve_creation_entry_route_id("/api/runtime/puzzle/agent/sessions"),
Some("puzzle"),
);
assert_eq!(
resolve_creation_entry_route_id("/api/runtime/match3d/runs/run-1"),
resolve_creation_entry_route_id("/api/runtime/puzzle/gallery/profile-1/remix"),
Some("puzzle"),
);
assert_eq!(
resolve_creation_entry_route_id("/api/creation/match3d/sessions"),
Some("match3d"),
);
assert_eq!(
resolve_creation_entry_route_id("/api/runtime/square-hole/runs/run-1"),
resolve_creation_entry_route_id("/api/creation/square-hole/sessions"),
Some("square-hole"),
);
assert_eq!(
resolve_creation_entry_route_id("/api/runtime/visual-novel/works"),
Some("visual-novel"),
);
assert_eq!(
resolve_creation_entry_route_id("/api/creation/visual-novel/sessions"),
Some("visual-novel"),
);
assert_eq!(
resolve_creation_entry_route_id("/api/runtime/big-fish/agent/sessions"),
Some("big-fish"),
);
assert_eq!(
resolve_creation_entry_route_id("/api/runtime/custom-world/agent/sessions"),
Some("rpg"),
);
assert_eq!(
resolve_creation_entry_route_id("/api/runtime/custom-world-library/profile-1"),
resolve_creation_entry_route_id(
"/api/runtime/custom-world-gallery/user-1/profile-1/remix"
),
Some("rpg"),
);
assert_eq!(
resolve_creation_entry_route_id("/api/runtime/custom-world-library/profile-1"),
None,
);
assert_eq!(
resolve_creation_entry_route_id("/api/runtime/custom-world-gallery/user-1/profile-1"),
Some("rpg"),
None,
);
assert_eq!(
resolve_creation_entry_route_id("/api/story/sessions/runtime"),
Some("rpg"),
None,
);
assert_eq!(
resolve_creation_entry_route_id("/api/runtime/chat/npc/turn/stream"),
Some("rpg"),
);
assert_eq!(
resolve_creation_entry_route_id("/api/runtime/bark-battle/works/work-1/config"),
Some("bark-battle"),
None,
);
assert_eq!(
resolve_creation_entry_route_id("/api/creation/bark-battle/drafts"),
Some("bark-battle"),
);
assert_eq!(
resolve_creation_entry_route_id("/api/runtime/bark-battle/works/work-1/config"),
None,
);
assert_eq!(
resolve_creation_entry_route_id("/api/runtime/wooden-fish/runs/run-1"),
Some("wooden-fish"),
None,
);
assert_eq!(
resolve_creation_entry_route_id("/api/creation/wooden-fish/sessions"),

View File

@@ -337,6 +337,24 @@ describe('platformDraftGenerationShelfModel', () => {
).toMatchObject({
type: 'load-detail',
});
expect(
resolveJumpHopDraftOpenIntent({
item: buildJumpHopWork({ sourceSessionId: null }),
notices: {
'jump-hop:jump-hop-work-base': {
status: 'failed',
seen: false,
},
},
generation: emptyGenerationFacts({
activeSessionId: null,
hasActiveGenerationFailure: true,
}),
}),
).toMatchObject({
type: 'load-detail',
});
});
test('resolveWoodenFishDraftOpenIntent uses profile fallback and failure fallback stage', () => {

View File

@@ -897,8 +897,9 @@ export function resolveJumpHopDraftOpenIntent(params: {
noticeIds,
'failed',
);
const activeSessionId = normalizeDraftNoticeId(generation.activeSessionId);
const isCurrentSession =
sourceSessionId === normalizeDraftNoticeId(generation.activeSessionId);
sourceSessionId !== null && sourceSessionId === activeSessionId;
if (
hasFailedNotice &&