合并 master 并保留外部生成 worker 模式
合入 master 的拼消消、微信能力、OpenSSL 3.2 和 SpacetimeDB 2.4.1 更新 保留外部内容生成 queue/inline、worker lease 与动态扩缩容口径 补齐拼图后台图片生成队列轮询和运行态返回恢复 同步容器、生产运维和 Hermes 共享记忆中的 worker 文档
This commit is contained in:
@@ -137,6 +137,10 @@ pub(crate) async fn create_seeded_puzzle_session_when_form_save_missing(
|
||||
Ok(replacement.session_id)
|
||||
}
|
||||
|
||||
fn default_puzzle_image_generation_points_cost() -> u64 {
|
||||
PUZZLE_IMAGE_GENERATION_POINTS_COST
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct PuzzleCompileDraftWorkerPayload {
|
||||
@@ -144,6 +148,8 @@ pub(crate) struct PuzzleCompileDraftWorkerPayload {
|
||||
pub owner_user_id: String,
|
||||
pub billing_asset_id: String,
|
||||
pub ai_redraw: bool,
|
||||
#[serde(default = "default_puzzle_image_generation_points_cost")]
|
||||
pub billing_points_cost: u64,
|
||||
#[serde(default)]
|
||||
pub prompt_text: Option<String>,
|
||||
#[serde(default)]
|
||||
@@ -166,7 +172,7 @@ pub(crate) async fn execute_puzzle_compile_draft_worker_job(
|
||||
&payload.owner_user_id,
|
||||
"puzzle_initial_image",
|
||||
&payload.billing_asset_id,
|
||||
PUZZLE_IMAGE_GENERATION_POINTS_COST,
|
||||
payload.billing_points_cost,
|
||||
async {
|
||||
compile_puzzle_draft_with_initial_cover(
|
||||
state,
|
||||
@@ -198,7 +204,32 @@ pub(crate) async fn execute_puzzle_compile_draft_worker_job(
|
||||
};
|
||||
|
||||
match session {
|
||||
Ok(session) => Ok(session),
|
||||
Ok(session) => {
|
||||
if session
|
||||
.draft
|
||||
.as_ref()
|
||||
.is_some_and(|draft| draft.generation_status == "ready")
|
||||
{
|
||||
send_generation_result_subscribe_message_after_completion(
|
||||
state.root_state(),
|
||||
GenerationResultSubscribeMessage {
|
||||
owner_user_id: payload.owner_user_id.clone(),
|
||||
task_name: Some("拼图".to_string()),
|
||||
work_name: session.draft.as_ref().map(|draft| draft.work_title.clone()),
|
||||
status: GenerationResultSubscribeMessageStatus::Succeeded,
|
||||
consumed_points: if payload.ai_redraw {
|
||||
payload.billing_points_cost
|
||||
} else {
|
||||
0
|
||||
},
|
||||
completed_at_micros: current_utc_micros(),
|
||||
page: Some("/pages/web-view/index".to_string()),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Ok(session)
|
||||
}
|
||||
Err(error) => {
|
||||
match mark_puzzle_compile_failure_for_worker(
|
||||
state,
|
||||
@@ -211,6 +242,19 @@ pub(crate) async fn execute_puzzle_compile_draft_worker_job(
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
send_generation_result_subscribe_message_after_completion(
|
||||
state.root_state(),
|
||||
GenerationResultSubscribeMessage {
|
||||
owner_user_id: payload.owner_user_id.clone(),
|
||||
task_name: Some("拼图".to_string()),
|
||||
work_name: None,
|
||||
status: GenerationResultSubscribeMessageStatus::Failed,
|
||||
consumed_points: 0,
|
||||
completed_at_micros: now,
|
||||
page: Some("/pages/web-view/index".to_string()),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
Err(PuzzleExternalGenerationWorkerError::with_failure_state_written(error))
|
||||
}
|
||||
Err(mark_error) => {
|
||||
@@ -425,13 +469,18 @@ pub(crate) fn build_puzzle_session_snapshot_from_action_payload(
|
||||
levels,
|
||||
form_draft: None,
|
||||
};
|
||||
let stage = if is_puzzle_session_snapshot_publish_ready(&draft) {
|
||||
"ready_to_publish"
|
||||
} else {
|
||||
"image_refining"
|
||||
};
|
||||
|
||||
Ok(PuzzleAgentSessionRecord {
|
||||
session_id: session_id.to_string(),
|
||||
seed_text: String::new(),
|
||||
current_turn: 0,
|
||||
progress_percent: 94,
|
||||
stage: "ready_to_publish".to_string(),
|
||||
stage: stage.to_string(),
|
||||
anchor_pack,
|
||||
draft: Some(draft),
|
||||
messages: Vec::new(),
|
||||
@@ -1095,6 +1144,7 @@ pub(crate) fn attach_selected_puzzle_candidate_to_levels(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn resolve_puzzle_initial_ui_background_prompt(
|
||||
draft: &PuzzleResultDraftRecord,
|
||||
target_level: &PuzzleDraftLevelRecord,
|
||||
@@ -1160,6 +1210,7 @@ pub(crate) fn build_puzzle_ui_background_generation_prompt(
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn attach_puzzle_level_ui_background(
|
||||
levels: &mut [PuzzleDraftLevelRecord],
|
||||
level_id: &str,
|
||||
@@ -1201,27 +1252,6 @@ pub(crate) fn attach_puzzle_level_asset_bundle(
|
||||
level.ui_background_image_object_key = Some(generated.level_background.object_key);
|
||||
}
|
||||
|
||||
pub(crate) async fn generate_puzzle_initial_ui_background_required(
|
||||
state: &PuzzleApiState,
|
||||
request_context: &RequestContext,
|
||||
owner_user_id: &str,
|
||||
session_id: &str,
|
||||
draft: &PuzzleResultDraftRecord,
|
||||
target_level: &PuzzleDraftLevelRecord,
|
||||
) -> Result<(String, GeneratedPuzzleUiBackgroundResponse), AppError> {
|
||||
let prompt = resolve_puzzle_initial_ui_background_prompt(draft, target_level);
|
||||
let generated = generate_puzzle_ui_background_image(
|
||||
state,
|
||||
request_context,
|
||||
owner_user_id,
|
||||
session_id,
|
||||
target_level.level_name.as_str(),
|
||||
prompt.as_str(),
|
||||
)
|
||||
.await?;
|
||||
Ok((prompt, generated))
|
||||
}
|
||||
|
||||
pub(crate) async fn generate_puzzle_level_asset_bundle_required(
|
||||
state: &PuzzleApiState,
|
||||
request_context: &RequestContext,
|
||||
@@ -1309,7 +1339,7 @@ pub(crate) async fn compile_puzzle_draft_with_initial_cover(
|
||||
let compiled_session = state
|
||||
.spacetime_client()
|
||||
.compile_puzzle_agent_draft_with_external_generation_guard(
|
||||
session_id.clone(),
|
||||
session_id,
|
||||
owner_user_id.clone(),
|
||||
now,
|
||||
external_generation_guard.job_id.clone(),
|
||||
@@ -1318,6 +1348,32 @@ pub(crate) async fn compile_puzzle_draft_with_initial_cover(
|
||||
)
|
||||
.await
|
||||
.map_err(map_puzzle_compile_error)?;
|
||||
|
||||
generate_puzzle_initial_cover_from_compiled_session(
|
||||
state,
|
||||
request_context,
|
||||
compiled_session,
|
||||
owner_user_id,
|
||||
prompt_text,
|
||||
reference_image_src,
|
||||
image_model,
|
||||
now,
|
||||
external_generation_guard,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn generate_puzzle_initial_cover_from_compiled_session(
|
||||
state: &PuzzleApiState,
|
||||
request_context: &RequestContext,
|
||||
compiled_session: PuzzleAgentSessionRecord,
|
||||
owner_user_id: String,
|
||||
prompt_text: Option<&str>,
|
||||
reference_image_src: Option<&str>,
|
||||
image_model: Option<&str>,
|
||||
now: i64,
|
||||
external_generation_guard: &ExternalGenerationWriteLeaseGuard,
|
||||
) -> Result<PuzzleAgentSessionRecord, AppError> {
|
||||
let draft = compiled_session.draft.clone().ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||
@@ -1458,7 +1514,7 @@ pub(crate) async fn compile_puzzle_draft_with_initial_cover(
|
||||
"message": format!("拼图候选图序列化失败:{error}"),
|
||||
}))
|
||||
})?;
|
||||
let saved_session = state
|
||||
let (saved_session, save_used_fallback) = state
|
||||
.spacetime_client()
|
||||
.save_puzzle_generated_images(PuzzleGeneratedImagesSaveRecordInput {
|
||||
session_id: compiled_session.session_id.clone(),
|
||||
@@ -1472,7 +1528,40 @@ pub(crate) async fn compile_puzzle_draft_with_initial_cover(
|
||||
external_generation_lease_token: external_generation_guard.lease_token.clone(),
|
||||
})
|
||||
.await
|
||||
.map_err(map_puzzle_client_error)?;
|
||||
.map_err(map_puzzle_client_error)
|
||||
.map(|session| (session, false))
|
||||
.or_else(|error| {
|
||||
if is_spacetimedb_connectivity_app_error(&error) {
|
||||
// 中文注释:首图已落 OSS 时,SpacetimeDB 短暂不可用先返回本地快照,避免整次 VectorEngine 生图被判失败。
|
||||
tracing::warn!(
|
||||
provider = PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||
session_id = %compiled_session.session_id,
|
||||
owner_user_id = %owner_user_id,
|
||||
message = %error.body_text(),
|
||||
"拼图首图已生成但 SpacetimeDB 草稿回写不可用,降级返回本次生成快照"
|
||||
);
|
||||
let session = apply_generated_puzzle_candidates_to_session_snapshot(
|
||||
apply_generated_puzzle_levels_to_session_snapshot(
|
||||
apply_generated_puzzle_first_level_name_to_session_snapshot(
|
||||
compiled_session.clone(),
|
||||
target_level.level_id.as_str(),
|
||||
generated_level_name.as_str(),
|
||||
fallback_level_name.as_str(),
|
||||
now,
|
||||
),
|
||||
updated_levels.clone(),
|
||||
now,
|
||||
),
|
||||
target_level.level_id.as_str(),
|
||||
candidates.into_records(),
|
||||
reference_image_src,
|
||||
now,
|
||||
);
|
||||
Ok((session, true))
|
||||
} else {
|
||||
Err(error)
|
||||
}
|
||||
})?;
|
||||
match state
|
||||
.spacetime_client()
|
||||
.update_puzzle_work(PuzzleWorkUpsertRecordInput {
|
||||
@@ -1509,10 +1598,13 @@ pub(crate) async fn compile_puzzle_draft_with_initial_cover(
|
||||
fallback_level_name.as_str(),
|
||||
now,
|
||||
);
|
||||
if save_used_fallback {
|
||||
return Ok(saved_session);
|
||||
}
|
||||
match state
|
||||
.spacetime_client()
|
||||
.select_puzzle_cover_image(PuzzleSelectCoverImageRecordInput {
|
||||
session_id,
|
||||
session_id: compiled_session.session_id.clone(),
|
||||
owner_user_id,
|
||||
level_id: Some(target_level.level_id),
|
||||
candidate_id: selected_candidate_id,
|
||||
@@ -1729,7 +1821,7 @@ pub(crate) async fn compile_puzzle_draft_with_uploaded_cover(
|
||||
"message": format!("拼图上传图候选序列化失败:{error}"),
|
||||
}))
|
||||
})?;
|
||||
let saved_session = state
|
||||
let (saved_session, save_used_fallback) = state
|
||||
.spacetime_client()
|
||||
.save_puzzle_generated_images(PuzzleGeneratedImagesSaveRecordInput {
|
||||
session_id: compiled_session.session_id.clone(),
|
||||
@@ -1743,7 +1835,39 @@ pub(crate) async fn compile_puzzle_draft_with_uploaded_cover(
|
||||
external_generation_lease_token: external_generation_guard.lease_token.clone(),
|
||||
})
|
||||
.await
|
||||
.map_err(map_puzzle_client_error)?;
|
||||
.map_err(map_puzzle_client_error)
|
||||
.map(|session| (session, false))
|
||||
.or_else(|error| {
|
||||
if is_spacetimedb_connectivity_app_error(&error) {
|
||||
tracing::warn!(
|
||||
provider = PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||
session_id = %compiled_session.session_id,
|
||||
owner_user_id = %owner_user_id,
|
||||
message = %error.body_text(),
|
||||
"拼图上传图草稿回写不可用,降级返回本地快照"
|
||||
);
|
||||
let session = apply_generated_puzzle_candidates_to_session_snapshot(
|
||||
apply_generated_puzzle_levels_to_session_snapshot(
|
||||
apply_generated_puzzle_first_level_name_to_session_snapshot(
|
||||
compiled_session.clone(),
|
||||
target_level.level_id.as_str(),
|
||||
generated_level_name.as_str(),
|
||||
fallback_level_name.as_str(),
|
||||
now,
|
||||
),
|
||||
updated_levels.clone(),
|
||||
now,
|
||||
),
|
||||
target_level.level_id.as_str(),
|
||||
vec![candidate.clone()],
|
||||
reference_image_src,
|
||||
now,
|
||||
);
|
||||
Ok((session, true))
|
||||
} else {
|
||||
Err(error)
|
||||
}
|
||||
})?;
|
||||
let (_, profile_id) = build_stable_puzzle_work_ids(&compiled_session.session_id);
|
||||
match state
|
||||
.spacetime_client()
|
||||
@@ -1781,6 +1905,9 @@ pub(crate) async fn compile_puzzle_draft_with_uploaded_cover(
|
||||
fallback_level_name.as_str(),
|
||||
now,
|
||||
);
|
||||
if save_used_fallback {
|
||||
return Ok(saved_session);
|
||||
}
|
||||
match state
|
||||
.spacetime_client()
|
||||
.select_puzzle_cover_image(PuzzleSelectCoverImageRecordInput {
|
||||
@@ -1811,7 +1938,6 @@ pub(crate) async fn compile_puzzle_draft_with_uploaded_cover(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn apply_generated_puzzle_candidates_to_session_snapshot(
|
||||
mut session: PuzzleAgentSessionRecord,
|
||||
target_level_id: &str,
|
||||
@@ -1858,13 +1984,33 @@ pub(crate) fn apply_generated_puzzle_candidates_to_session_snapshot(
|
||||
sync_puzzle_primary_draft_fields_from_level(draft);
|
||||
}
|
||||
session.progress_percent = session.progress_percent.max(94);
|
||||
session.stage = "ready_to_publish".to_string();
|
||||
session.stage = if is_puzzle_session_snapshot_publish_ready(draft) {
|
||||
"ready_to_publish".to_string()
|
||||
} else {
|
||||
"image_refining".to_string()
|
||||
};
|
||||
session.last_assistant_reply = Some("拼图图片已经生成,并已替换当前正式图。".to_string());
|
||||
session.updated_at = format_timestamp_micros(updated_at_micros);
|
||||
session
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn apply_generated_puzzle_levels_to_session_snapshot(
|
||||
mut session: PuzzleAgentSessionRecord,
|
||||
levels: Vec<PuzzleDraftLevelRecord>,
|
||||
updated_at_micros: i64,
|
||||
) -> PuzzleAgentSessionRecord {
|
||||
let Some(draft) = session.draft.as_mut() else {
|
||||
return session;
|
||||
};
|
||||
if levels.is_empty() {
|
||||
return session;
|
||||
}
|
||||
draft.levels = levels;
|
||||
sync_puzzle_primary_draft_fields_from_level(draft);
|
||||
session.updated_at = format_timestamp_micros(updated_at_micros);
|
||||
session
|
||||
}
|
||||
|
||||
pub(crate) fn apply_generated_puzzle_first_level_name_to_session_snapshot(
|
||||
mut session: PuzzleAgentSessionRecord,
|
||||
target_level_id: &str,
|
||||
@@ -1917,6 +2063,45 @@ pub(crate) fn apply_generated_puzzle_initial_metadata_to_session_snapshot(
|
||||
session
|
||||
}
|
||||
|
||||
pub(crate) fn apply_generated_puzzle_metadata_to_session_snapshot(
|
||||
mut session: PuzzleAgentSessionRecord,
|
||||
target_level_id: &str,
|
||||
metadata: &PuzzleLevelNaming,
|
||||
previous_level_name: &str,
|
||||
updated_at_micros: i64,
|
||||
) -> PuzzleAgentSessionRecord {
|
||||
let Some(draft) = session.draft.as_mut() else {
|
||||
return session;
|
||||
};
|
||||
let Some(target_index) = draft
|
||||
.levels
|
||||
.iter()
|
||||
.position(|level| level.level_id == target_level_id)
|
||||
.or_else(|| (!draft.levels.is_empty()).then_some(0))
|
||||
else {
|
||||
return session;
|
||||
};
|
||||
|
||||
draft.levels[target_index].level_name = metadata.level_name.clone();
|
||||
if metadata.ui_background_prompt.is_some() {
|
||||
draft.levels[target_index].ui_background_prompt = metadata.ui_background_prompt.clone();
|
||||
}
|
||||
|
||||
if target_index == 0 {
|
||||
apply_generated_puzzle_initial_metadata_to_draft(
|
||||
draft,
|
||||
metadata,
|
||||
previous_level_name,
|
||||
updated_at_micros,
|
||||
);
|
||||
} else {
|
||||
sync_puzzle_primary_draft_fields_from_level(draft);
|
||||
}
|
||||
|
||||
session.updated_at = format_timestamp_micros(updated_at_micros);
|
||||
session
|
||||
}
|
||||
|
||||
pub(crate) fn apply_generated_puzzle_initial_metadata_to_draft(
|
||||
draft: &mut PuzzleResultDraftRecord,
|
||||
metadata: &PuzzleLevelNaming,
|
||||
@@ -1966,3 +2151,45 @@ pub(crate) fn sync_puzzle_primary_draft_fields_from_level(draft: &mut PuzzleResu
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn replace_puzzle_session_draft_snapshot(
|
||||
mut session: PuzzleAgentSessionRecord,
|
||||
draft: PuzzleResultDraftRecord,
|
||||
updated_at_micros: i64,
|
||||
) -> PuzzleAgentSessionRecord {
|
||||
session.draft = Some(draft);
|
||||
session.updated_at = format_timestamp_micros(updated_at_micros);
|
||||
session
|
||||
}
|
||||
|
||||
pub(crate) fn apply_generated_puzzle_ui_background_to_session_snapshot(
|
||||
mut session: PuzzleAgentSessionRecord,
|
||||
target_level_id: &str,
|
||||
prompt: String,
|
||||
image_src: String,
|
||||
image_object_key: Option<String>,
|
||||
updated_at_micros: i64,
|
||||
) -> PuzzleAgentSessionRecord {
|
||||
let Some(draft) = session.draft.as_mut() else {
|
||||
return session;
|
||||
};
|
||||
let Some(target_index) = draft
|
||||
.levels
|
||||
.iter()
|
||||
.position(|level| level.level_id == target_level_id)
|
||||
.or_else(|| (!draft.levels.is_empty()).then_some(0))
|
||||
else {
|
||||
return session;
|
||||
};
|
||||
let level = &mut draft.levels[target_index];
|
||||
level.ui_background_prompt = Some(prompt);
|
||||
level.ui_background_image_src = Some(image_src);
|
||||
level.ui_background_image_object_key = image_object_key;
|
||||
if target_index == 0 {
|
||||
sync_puzzle_primary_draft_fields_from_level(draft);
|
||||
}
|
||||
session.progress_percent = session.progress_percent.max(96);
|
||||
session.last_assistant_reply = Some("拼图 UI 背景图已生成。".to_string());
|
||||
session.updated_at = format_timestamp_micros(updated_at_micros);
|
||||
session
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user