Merge branch 'master' into dev-jenken
This commit is contained in:
@@ -71,6 +71,8 @@ spacetime sql <database> "SELECT * FROM puzzle_gallery_card_view LIMIT 1" --serv
|
|||||||
|
|
||||||
VectorEngine 图片生成 / 编辑在 `request_send` 阶段出现 `timeout` 或 `connect` 错误时,`platform-image` 会对同一请求最多发送 3 次;multipart 图片编辑每次重试都会重新构造 form,避免复用已消费的 body。日志中 `VectorEngine 图片请求发送失败,准备重试` 表示本次失败已进入下一次尝试;最终仍失败时才会写入 `external_api_call_failure` 并返回 504。排查生产失败时应同时统计 retry 前的尝试日志和最终 audit,避免把一次用户请求内的多次发送误判成多个用户请求。
|
VectorEngine 图片生成 / 编辑在 `request_send` 阶段出现 `timeout` 或 `connect` 错误时,`platform-image` 会对同一请求最多发送 3 次;multipart 图片编辑每次重试都会重新构造 form,避免复用已消费的 body。日志中 `VectorEngine 图片请求发送失败,准备重试` 表示本次失败已进入下一次尝试;最终仍失败时才会写入 `external_api_call_failure` 并返回 504。排查生产失败时应同时统计 retry 前的尝试日志和最终 audit,避免把一次用户请求内的多次发送误判成多个用户请求。
|
||||||
|
|
||||||
|
拼图入口直创的 `compile_puzzle_draft` 是长耗时链路:后端会先快速编译草稿并返回 `image_refining` / `generating` 快照,然后在 api-server 后台任务中完成首图、UI 资产、OSS 持久化、作品投影、计费退款和失败态回写。生产排查小程序 `Failed to fetch` 时,若 Nginx access log 里 action POST 是 `499`、`upstream_status=-`,说明客户端或 WebView 先断开;此时不应再把长 POST 是否返回作为生成成败依据,而应继续按实际 `session_id` 查后台任务日志、VectorEngine provider 日志、`external_api_call_failure` 和后续 GET 轮询结果。同一用户可能先轮询旧的 `puzzle-session-*`,随后 POST 新建实际生成 session;必须用 action POST 的 `request_id` 和 `/api/runtime/puzzle/agent/sessions/<session_id>/actions` 路径对齐真实失败请求,避免被前端显示的“来源草稿”误导。
|
||||||
|
|
||||||
查看本地 Rust / SpacetimeDB 日志:
|
查看本地 Rust / SpacetimeDB 日志:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::BTreeMap,
|
collections::{BTreeMap, HashSet},
|
||||||
|
sync::{Mutex, OnceLock},
|
||||||
time::{Instant, SystemTime, UNIX_EPOCH},
|
time::{Instant, SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -130,6 +131,73 @@ const PUZZLE_UI_BACKGROUND_PROMPT_FALLBACK_MARKER: &str =
|
|||||||
const PUZZLE_VECTOR_ENGINE_SQUARE_IMAGE_SIZE: &str = "1024x1024";
|
const PUZZLE_VECTOR_ENGINE_SQUARE_IMAGE_SIZE: &str = "1024x1024";
|
||||||
const PUZZLE_VECTOR_ENGINE_PORTRAIT_IMAGE_SIZE: &str = "1024x1536";
|
const PUZZLE_VECTOR_ENGINE_PORTRAIT_IMAGE_SIZE: &str = "1024x1536";
|
||||||
|
|
||||||
|
static PUZZLE_BACKGROUND_COMPILE_TASKS: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
|
||||||
|
|
||||||
|
fn puzzle_background_compile_tasks() -> &'static Mutex<HashSet<String>> {
|
||||||
|
PUZZLE_BACKGROUND_COMPILE_TASKS.get_or_init(|| Mutex::new(HashSet::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_register_puzzle_background_compile_task(session_id: &str) -> bool {
|
||||||
|
match puzzle_background_compile_tasks().lock() {
|
||||||
|
Ok(mut tasks) => tasks.insert(session_id.to_string()),
|
||||||
|
Err(error) => {
|
||||||
|
tracing::warn!(
|
||||||
|
provider = PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||||
|
session_id,
|
||||||
|
error = %error,
|
||||||
|
"拼图后台生成任务注册表锁已损坏,允许本次任务继续"
|
||||||
|
);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unregister_puzzle_background_compile_task(session_id: &str) {
|
||||||
|
match puzzle_background_compile_tasks().lock() {
|
||||||
|
Ok(mut tasks) => {
|
||||||
|
tasks.remove(session_id);
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
tracing::warn!(
|
||||||
|
provider = PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||||
|
session_id,
|
||||||
|
error = %error,
|
||||||
|
"拼图后台生成任务注册表解锁失败,忽略清理"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_puzzle_cover_image_src(value: &Option<String>) -> bool {
|
||||||
|
value
|
||||||
|
.as_deref()
|
||||||
|
.map(str::trim)
|
||||||
|
.is_some_and(|value| !value.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mark_puzzle_initial_generation_started_snapshot(
|
||||||
|
mut session: PuzzleAgentSessionRecord,
|
||||||
|
) -> PuzzleAgentSessionRecord {
|
||||||
|
session.stage = "image_refining".to_string();
|
||||||
|
session.progress_percent = session.progress_percent.max(88);
|
||||||
|
if let Some(draft) = session.draft.as_mut() {
|
||||||
|
let draft_needs_cover = !has_puzzle_cover_image_src(&draft.cover_image_src);
|
||||||
|
if let Some(primary_level) = draft.levels.first_mut() {
|
||||||
|
if !has_puzzle_cover_image_src(&primary_level.cover_image_src) {
|
||||||
|
primary_level.generation_status = "generating".to_string();
|
||||||
|
}
|
||||||
|
draft.generation_status = primary_level.generation_status.clone();
|
||||||
|
draft.candidates = primary_level.candidates.clone();
|
||||||
|
draft.selected_candidate_id = primary_level.selected_candidate_id.clone();
|
||||||
|
draft.cover_image_src = primary_level.cover_image_src.clone();
|
||||||
|
draft.cover_asset_id = primary_level.cover_asset_id.clone();
|
||||||
|
} else if draft_needs_cover {
|
||||||
|
draft.generation_status = "generating".to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
session
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn format_puzzle_reference_image_upload_bytes(bytes: usize) -> String {
|
pub(crate) fn format_puzzle_reference_image_upload_bytes(bytes: usize) -> String {
|
||||||
format!("{:.1}MB", bytes as f64 / 1024.0 / 1024.0)
|
format!("{:.1}MB", bytes as f64 / 1024.0 / 1024.0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1177,21 +1177,16 @@ pub(crate) fn find_puzzle_level_for_initial_asset_check<'a>(
|
|||||||
.or_else(|| levels.first())
|
.or_else(|| levels.first())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn compile_puzzle_draft_with_initial_cover(
|
pub(crate) async fn generate_puzzle_initial_cover_from_compiled_session(
|
||||||
state: &PuzzleApiState,
|
state: &PuzzleApiState,
|
||||||
request_context: &RequestContext,
|
request_context: &RequestContext,
|
||||||
session_id: String,
|
compiled_session: PuzzleAgentSessionRecord,
|
||||||
owner_user_id: String,
|
owner_user_id: String,
|
||||||
prompt_text: Option<&str>,
|
prompt_text: Option<&str>,
|
||||||
reference_image_src: Option<&str>,
|
reference_image_src: Option<&str>,
|
||||||
image_model: Option<&str>,
|
image_model: Option<&str>,
|
||||||
now: i64,
|
now: i64,
|
||||||
) -> Result<PuzzleAgentSessionRecord, AppError> {
|
) -> Result<PuzzleAgentSessionRecord, AppError> {
|
||||||
let compiled_session = state
|
|
||||||
.spacetime_client()
|
|
||||||
.compile_puzzle_agent_draft(session_id.clone(), owner_user_id.clone(), now)
|
|
||||||
.await
|
|
||||||
.map_err(map_puzzle_compile_error)?;
|
|
||||||
let draft = compiled_session.draft.clone().ok_or_else(|| {
|
let draft = compiled_session.draft.clone().ok_or_else(|| {
|
||||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||||
"provider": PUZZLE_AGENT_API_BASE_PROVIDER,
|
"provider": PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||||
@@ -1419,7 +1414,7 @@ pub(crate) async fn compile_puzzle_draft_with_initial_cover(
|
|||||||
match state
|
match state
|
||||||
.spacetime_client()
|
.spacetime_client()
|
||||||
.select_puzzle_cover_image(PuzzleSelectCoverImageRecordInput {
|
.select_puzzle_cover_image(PuzzleSelectCoverImageRecordInput {
|
||||||
session_id,
|
session_id: compiled_session.session_id.clone(),
|
||||||
owner_user_id,
|
owner_user_id,
|
||||||
level_id: Some(target_level.level_id),
|
level_id: Some(target_level.level_id),
|
||||||
candidate_id: selected_candidate_id,
|
candidate_id: selected_candidate_id,
|
||||||
|
|||||||
@@ -623,7 +623,7 @@ pub async fn execute_puzzle_agent_action(
|
|||||||
session_id,
|
session_id,
|
||||||
owner_user_id,
|
owner_user_id,
|
||||||
error_message,
|
error_message,
|
||||||
failed_at_micros: now,
|
failed_at_micros: current_utc_micros(),
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
if let Err(error) = result {
|
if let Err(error) = result {
|
||||||
@@ -668,27 +668,128 @@ pub async fn execute_puzzle_agent_action(
|
|||||||
Err(response) => return Err(response),
|
Err(response) => return Err(response),
|
||||||
};
|
};
|
||||||
let session = if ai_redraw {
|
let session = if ai_redraw {
|
||||||
execute_billable_asset_operation_with_cost(
|
if !try_register_puzzle_background_compile_task(&compile_session_id) {
|
||||||
state.root_state(),
|
tracing::info!(
|
||||||
&owner_user_id,
|
provider = PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||||
"puzzle_initial_image",
|
session_id = %compile_session_id,
|
||||||
&billing_asset_id,
|
owner_user_id = %owner_user_id,
|
||||||
PUZZLE_IMAGE_GENERATION_POINTS_COST,
|
"拼图首图后台生成任务已存在,本次 action 直接返回生成中会话"
|
||||||
async {
|
);
|
||||||
compile_puzzle_draft_with_initial_cover(
|
state
|
||||||
&state,
|
.spacetime_client()
|
||||||
&request_context,
|
.get_puzzle_agent_session(
|
||||||
|
compile_session_id.clone(),
|
||||||
|
owner_user_id.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map(mark_puzzle_initial_generation_started_snapshot)
|
||||||
|
.map_err(map_puzzle_client_error)
|
||||||
|
} else {
|
||||||
|
let compiled_session = state
|
||||||
|
.spacetime_client()
|
||||||
|
.compile_puzzle_agent_draft(
|
||||||
compile_session_id.clone(),
|
compile_session_id.clone(),
|
||||||
owner_user_id.clone(),
|
owner_user_id.clone(),
|
||||||
prompt_text,
|
|
||||||
primary_reference_image_src,
|
|
||||||
payload.image_model.as_deref(),
|
|
||||||
now,
|
now,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
.map_err(map_puzzle_compile_error);
|
||||||
|
match compiled_session {
|
||||||
|
Ok(compiled_session) => {
|
||||||
|
let response_session =
|
||||||
|
mark_puzzle_initial_generation_started_snapshot(
|
||||||
|
compiled_session.clone(),
|
||||||
|
);
|
||||||
|
let background_state = state.clone();
|
||||||
|
let background_request_context = request_context.clone();
|
||||||
|
let background_session_id = compile_session_id.clone();
|
||||||
|
let background_owner_user_id = owner_user_id.clone();
|
||||||
|
let background_prompt_text = prompt_text.map(str::to_string);
|
||||||
|
let background_reference_image_src =
|
||||||
|
primary_reference_image_src.map(str::to_string);
|
||||||
|
let background_image_model = payload.image_model.clone();
|
||||||
|
let background_billing_asset_id =
|
||||||
|
format!("{background_session_id}:compile_puzzle_draft");
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let operation_owner_user_id =
|
||||||
|
background_owner_user_id.clone();
|
||||||
|
let background_root_state =
|
||||||
|
background_state.root_state().clone();
|
||||||
|
let operation_state = background_state.clone();
|
||||||
|
let result = execute_billable_asset_operation_with_cost(
|
||||||
|
&background_root_state,
|
||||||
|
&background_owner_user_id,
|
||||||
|
"puzzle_initial_image",
|
||||||
|
&background_billing_asset_id,
|
||||||
|
PUZZLE_IMAGE_GENERATION_POINTS_COST,
|
||||||
|
async move {
|
||||||
|
generate_puzzle_initial_cover_from_compiled_session(
|
||||||
|
&operation_state,
|
||||||
|
&background_request_context,
|
||||||
|
compiled_session,
|
||||||
|
operation_owner_user_id,
|
||||||
|
background_prompt_text.as_deref(),
|
||||||
|
background_reference_image_src.as_deref(),
|
||||||
|
background_image_model.as_deref(),
|
||||||
|
current_utc_micros(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await;
|
||||||
|
match result {
|
||||||
|
Ok(session) => {
|
||||||
|
tracing::info!(
|
||||||
|
provider = PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||||
|
session_id = %session.session_id,
|
||||||
|
owner_user_id = %background_owner_user_id,
|
||||||
|
"拼图首图后台生成任务完成"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
let error_message = error.body_text();
|
||||||
|
let failure_result = background_state
|
||||||
|
.spacetime_client()
|
||||||
|
.mark_puzzle_draft_generation_failed(
|
||||||
|
PuzzleDraftCompileFailureRecordInput {
|
||||||
|
session_id: background_session_id.clone(),
|
||||||
|
owner_user_id: background_owner_user_id
|
||||||
|
.clone(),
|
||||||
|
error_message: error_message.clone(),
|
||||||
|
failed_at_micros: current_utc_micros(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
if let Err(mark_error) = failure_result {
|
||||||
|
tracing::warn!(
|
||||||
|
provider = PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||||
|
session_id = %background_session_id,
|
||||||
|
owner_user_id = %background_owner_user_id,
|
||||||
|
message = %mark_error,
|
||||||
|
"拼图首图后台生成失败态回写失败"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
tracing::warn!(
|
||||||
|
provider = PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||||
|
session_id = %background_session_id,
|
||||||
|
owner_user_id = %background_owner_user_id,
|
||||||
|
message = %error_message,
|
||||||
|
"拼图首图后台生成任务失败"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unregister_puzzle_background_compile_task(
|
||||||
|
&background_session_id,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Ok(response_session)
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
unregister_puzzle_background_compile_task(&compile_session_id);
|
||||||
|
Err(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
compile_puzzle_draft_with_uploaded_cover(
|
compile_puzzle_draft_with_uploaded_cover(
|
||||||
&state,
|
&state,
|
||||||
@@ -716,7 +817,7 @@ pub async fn execute_puzzle_agent_action(
|
|||||||
"compile_puzzle_draft",
|
"compile_puzzle_draft",
|
||||||
"首关拼图草稿",
|
"首关拼图草稿",
|
||||||
if ai_redraw {
|
if ai_redraw {
|
||||||
"已编译首关草稿、并行生成首关画面和 UI 背景并写入正式草稿。"
|
"已编译首关草稿,并启动首关画面和 UI 资产后台生成。"
|
||||||
} else {
|
} else {
|
||||||
"已编译首关草稿,并直接应用上传图片、生成 UI 背景为第一关图片。"
|
"已编译首关草稿,并直接应用上传图片、生成 UI 背景为第一关图片。"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -980,6 +980,41 @@ fn puzzle_work_summary_response_keeps_levels_for_shelf_cover() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn puzzle_compile_started_snapshot_marks_primary_level_generating() {
|
||||||
|
let mut session = PuzzleAgentSessionRecord {
|
||||||
|
session_id: "puzzle-session-1".to_string(),
|
||||||
|
seed_text: "画面描述:一只猫在雨夜灯牌下回头。".to_string(),
|
||||||
|
current_turn: 1,
|
||||||
|
progress_percent: 88,
|
||||||
|
stage: "draft_ready".to_string(),
|
||||||
|
anchor_pack: test_puzzle_anchor_pack_record(),
|
||||||
|
draft: Some(test_puzzle_draft_record()),
|
||||||
|
messages: Vec::new(),
|
||||||
|
last_assistant_reply: None,
|
||||||
|
published_profile_id: None,
|
||||||
|
suggested_actions: Vec::new(),
|
||||||
|
result_preview: None,
|
||||||
|
updated_at: "2024-01-01T00:00:00Z".to_string(),
|
||||||
|
};
|
||||||
|
{
|
||||||
|
let draft = session.draft.as_mut().expect("draft");
|
||||||
|
draft.generation_status = "idle".to_string();
|
||||||
|
draft.levels[0].generation_status = "idle".to_string();
|
||||||
|
draft.levels[0].cover_image_src = None;
|
||||||
|
draft.levels[0].cover_asset_id = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let session = mark_puzzle_initial_generation_started_snapshot(session);
|
||||||
|
let draft = session.draft.expect("draft");
|
||||||
|
|
||||||
|
assert_eq!(session.stage, "image_refining");
|
||||||
|
assert_eq!(draft.generation_status, "generating");
|
||||||
|
assert_eq!(draft.levels[0].generation_status, "generating");
|
||||||
|
assert!(draft.cover_image_src.is_none());
|
||||||
|
assert!(draft.levels[0].cover_image_src.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn puzzle_ui_background_prompt_keeps_generated_slots_out_of_background() {
|
fn puzzle_ui_background_prompt_keeps_generated_slots_out_of_background() {
|
||||||
let prompt = build_puzzle_ui_background_request_prompt_for_test("雨夜猫街", "雨夜猫街主题背景");
|
let prompt = build_puzzle_ui_background_request_prompt_for_test("雨夜猫街", "雨夜猫街主题背景");
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ import type {
|
|||||||
PuzzleAgentSessionSnapshot,
|
PuzzleAgentSessionSnapshot,
|
||||||
SendPuzzleAgentMessageRequest,
|
SendPuzzleAgentMessageRequest,
|
||||||
} from '../../../packages/shared/src/contracts/puzzleAgentSession';
|
} from '../../../packages/shared/src/contracts/puzzleAgentSession';
|
||||||
|
import { isPuzzleCompileActionReady } from './puzzleDraftGenerationState';
|
||||||
import type { PuzzleCreativeTemplateSelection } from '../../../packages/shared/src/contracts/puzzleCreativeTemplate';
|
import type { PuzzleCreativeTemplateSelection } from '../../../packages/shared/src/contracts/puzzleCreativeTemplate';
|
||||||
import type {
|
import type {
|
||||||
PuzzleRunSnapshot,
|
PuzzleRunSnapshot,
|
||||||
@@ -6234,7 +6235,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
sessionController.setCreationTypeError(errorMessage);
|
sessionController.setCreationTypeError(errorMessage);
|
||||||
setPuzzleCreationError(errorMessage);
|
setPuzzleCreationError(errorMessage);
|
||||||
},
|
},
|
||||||
onActionComplete: async ({ payload, response, setSession }) => {
|
onActionComplete: async ({ payload, response, session, setSession }) => {
|
||||||
setPuzzleOperation(response.operation);
|
setPuzzleOperation(response.operation);
|
||||||
setSession(response.session);
|
setSession(response.session);
|
||||||
const formPayload = buildPuzzleFormPayloadFromAction(payload);
|
const formPayload = buildPuzzleFormPayloadFromAction(payload);
|
||||||
@@ -6258,6 +6259,47 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
|
|
||||||
if (payload.action === 'compile_puzzle_draft') {
|
if (payload.action === 'compile_puzzle_draft') {
|
||||||
const openResult = selectionStageRef.current === 'puzzle-generating';
|
const openResult = selectionStageRef.current === 'puzzle-generating';
|
||||||
|
if (!isPuzzleCompileActionReady(response.session)) {
|
||||||
|
const nextPayload =
|
||||||
|
formPayload ?? buildPuzzleFormPayloadFromSession(response.session);
|
||||||
|
const fallbackGenerationState = createPuzzleDraftGenerationStateFromPayload(
|
||||||
|
nextPayload,
|
||||||
|
response.session,
|
||||||
|
);
|
||||||
|
const nextGenerationState = mergePuzzleSessionProgressIntoGenerationState(
|
||||||
|
puzzleGenerationState ?? fallbackGenerationState,
|
||||||
|
response.session,
|
||||||
|
);
|
||||||
|
activePuzzleGenerationSessionIdRef.current = response.session.sessionId;
|
||||||
|
setSelectionStage('puzzle-generating');
|
||||||
|
markDraftGenerating('puzzle', [
|
||||||
|
response.session.sessionId,
|
||||||
|
buildPuzzleResultWorkId(response.session.sessionId),
|
||||||
|
response.session.publishedProfileId,
|
||||||
|
buildPuzzleResultProfileId(response.session.sessionId),
|
||||||
|
]);
|
||||||
|
markPendingDraftGenerating(
|
||||||
|
'puzzle',
|
||||||
|
response.session.sessionId,
|
||||||
|
buildPendingPuzzleDraftMetadata(nextPayload),
|
||||||
|
);
|
||||||
|
setPuzzleGenerationState(nextGenerationState);
|
||||||
|
setPuzzleBackgroundCompileTasks((current) => {
|
||||||
|
const next = { ...current };
|
||||||
|
if (session.sessionId !== response.session.sessionId) {
|
||||||
|
delete next[session.sessionId];
|
||||||
|
}
|
||||||
|
next[response.session.sessionId] = {
|
||||||
|
session: response.session,
|
||||||
|
payload: nextPayload,
|
||||||
|
generationState: nextGenerationState,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
void refreshPuzzleShelf();
|
||||||
|
return { openResult: false };
|
||||||
|
}
|
||||||
setPuzzleGenerationState((current) =>
|
setPuzzleGenerationState((current) =>
|
||||||
current
|
current
|
||||||
? resolveFinishedMiniGameDraftGenerationState(current, 'ready', {
|
? resolveFinishedMiniGameDraftGenerationState(current, 'ready', {
|
||||||
@@ -7152,6 +7194,22 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasRecoverableGeneratedPuzzleDraft(latestSession)) {
|
||||||
|
const payload =
|
||||||
|
puzzleGenerationViewPayload ??
|
||||||
|
buildPuzzleFormPayloadFromSession(latestSession);
|
||||||
|
const generationState =
|
||||||
|
puzzleGenerationViewState ??
|
||||||
|
createPuzzleDraftGenerationStateFromPayload(payload, latestSession);
|
||||||
|
await recoverCompletedPuzzleDraftGeneration({
|
||||||
|
sessionId: latestSession.sessionId,
|
||||||
|
payload,
|
||||||
|
generationState,
|
||||||
|
setSession: setPuzzleSession,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setPuzzleSession(latestSession);
|
setPuzzleSession(latestSession);
|
||||||
setPuzzleBackgroundCompileTasks((current) => {
|
setPuzzleBackgroundCompileTasks((current) => {
|
||||||
const task = current[activePuzzleGenerationSessionId];
|
const task = current[activePuzzleGenerationSessionId];
|
||||||
@@ -7195,6 +7253,9 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
activePuzzleGenerationSessionId,
|
activePuzzleGenerationSessionId,
|
||||||
|
puzzleGenerationViewPayload,
|
||||||
|
puzzleGenerationViewState,
|
||||||
|
recoverCompletedPuzzleDraftGeneration,
|
||||||
shouldPollPuzzleGenerationSession,
|
shouldPollPuzzleGenerationSession,
|
||||||
setPuzzleSession,
|
setPuzzleSession,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { isPuzzleCompileActionReady } from './puzzleDraftGenerationState';
|
||||||
|
import type { PuzzleAgentSessionSnapshot } from '../../../packages/shared/src/contracts/puzzleAgentSession';
|
||||||
|
|
||||||
|
describe('isPuzzleCompileActionReady', () => {
|
||||||
|
it('keeps compile action generating until the draft has a cover image', () => {
|
||||||
|
const session = {
|
||||||
|
sessionId: 'puzzle-session-1',
|
||||||
|
draft: {
|
||||||
|
coverImageSrc: null,
|
||||||
|
levels: [
|
||||||
|
{
|
||||||
|
generationStatus: 'generating',
|
||||||
|
coverImageSrc: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
} as PuzzleAgentSessionSnapshot;
|
||||||
|
|
||||||
|
expect(isPuzzleCompileActionReady(session)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats compile action as ready after the selected cover exists', () => {
|
||||||
|
const session = {
|
||||||
|
sessionId: 'puzzle-session-1',
|
||||||
|
draft: {
|
||||||
|
coverImageSrc: '/generated-puzzle-assets/session/cover.png',
|
||||||
|
levels: [
|
||||||
|
{
|
||||||
|
generationStatus: 'ready',
|
||||||
|
coverImageSrc: '/generated-puzzle-assets/session/cover.png',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
} as PuzzleAgentSessionSnapshot;
|
||||||
|
|
||||||
|
expect(isPuzzleCompileActionReady(session)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
20
src/components/platform-entry/puzzleDraftGenerationState.ts
Normal file
20
src/components/platform-entry/puzzleDraftGenerationState.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import type { PuzzleAgentSessionSnapshot } from '../../../packages/shared/src/contracts/puzzleAgentSession';
|
||||||
|
|
||||||
|
function hasText(value: string | null | undefined) {
|
||||||
|
return typeof value === 'string' && value.trim().length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPuzzleCompileActionReady(
|
||||||
|
session: PuzzleAgentSessionSnapshot,
|
||||||
|
) {
|
||||||
|
const draft = session.draft;
|
||||||
|
if (!draft) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (hasText(draft.coverImageSrc)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
draft.levels?.some((level) => hasText(level.coverImageSrc)) === true
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user