This commit is contained in:
759
server-rs/crates/spacetime-module/src/big_fish/session.rs
Normal file
759
server-rs/crates/spacetime-module/src/big_fish/session.rs
Normal file
@@ -0,0 +1,759 @@
|
||||
use crate::big_fish::tables::{big_fish_agent_message, big_fish_creation_session};
|
||||
use crate::*;
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn create_big_fish_session(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: BigFishSessionCreateInput,
|
||||
) -> BigFishSessionProcedureResult {
|
||||
match ctx.try_with_tx(|tx| create_big_fish_session_tx(tx, input.clone())) {
|
||||
Ok(session) => BigFishSessionProcedureResult {
|
||||
ok: true,
|
||||
session: Some(session),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => BigFishSessionProcedureResult {
|
||||
ok: false,
|
||||
session: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn get_big_fish_session(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: BigFishSessionGetInput,
|
||||
) -> BigFishSessionProcedureResult {
|
||||
match ctx.try_with_tx(|tx| get_big_fish_session_tx(tx, input.clone())) {
|
||||
Ok(session) => BigFishSessionProcedureResult {
|
||||
ok: true,
|
||||
session: Some(session),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => BigFishSessionProcedureResult {
|
||||
ok: false,
|
||||
session: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn list_big_fish_works(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: BigFishWorksListInput,
|
||||
) -> BigFishWorksProcedureResult {
|
||||
match ctx.try_with_tx(|tx| list_big_fish_works_tx(tx, input.clone())) {
|
||||
Ok(items) => match serde_json::to_string(&items) {
|
||||
Ok(items_json) => BigFishWorksProcedureResult {
|
||||
ok: true,
|
||||
items_json: Some(items_json),
|
||||
error_message: None,
|
||||
},
|
||||
Err(error) => BigFishWorksProcedureResult {
|
||||
ok: false,
|
||||
items_json: None,
|
||||
error_message: Some(error.to_string()),
|
||||
},
|
||||
},
|
||||
Err(message) => BigFishWorksProcedureResult {
|
||||
ok: false,
|
||||
items_json: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn delete_big_fish_work(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: BigFishWorkDeleteInput,
|
||||
) -> BigFishWorksProcedureResult {
|
||||
match ctx.try_with_tx(|tx| delete_big_fish_work_tx(tx, input.clone())) {
|
||||
Ok(items) => match serde_json::to_string(&items) {
|
||||
Ok(items_json) => BigFishWorksProcedureResult {
|
||||
ok: true,
|
||||
items_json: Some(items_json),
|
||||
error_message: None,
|
||||
},
|
||||
Err(error) => BigFishWorksProcedureResult {
|
||||
ok: false,
|
||||
items_json: None,
|
||||
error_message: Some(error.to_string()),
|
||||
},
|
||||
},
|
||||
Err(message) => BigFishWorksProcedureResult {
|
||||
ok: false,
|
||||
items_json: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn submit_big_fish_message(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: BigFishMessageSubmitInput,
|
||||
) -> BigFishSessionProcedureResult {
|
||||
match ctx.try_with_tx(|tx| submit_big_fish_message_tx(tx, input.clone())) {
|
||||
Ok(session) => BigFishSessionProcedureResult {
|
||||
ok: true,
|
||||
session: Some(session),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => BigFishSessionProcedureResult {
|
||||
ok: false,
|
||||
session: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn finalize_big_fish_agent_message_turn(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: BigFishMessageFinalizeInput,
|
||||
) -> BigFishSessionProcedureResult {
|
||||
match ctx.try_with_tx(|tx| finalize_big_fish_agent_message_turn_tx(tx, input.clone())) {
|
||||
Ok(session) => BigFishSessionProcedureResult {
|
||||
ok: true,
|
||||
session: Some(session),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => BigFishSessionProcedureResult {
|
||||
ok: false,
|
||||
session: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn compile_big_fish_draft(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: BigFishDraftCompileInput,
|
||||
) -> BigFishSessionProcedureResult {
|
||||
match ctx.try_with_tx(|tx| compile_big_fish_draft_tx(tx, input.clone())) {
|
||||
Ok(session) => BigFishSessionProcedureResult {
|
||||
ok: true,
|
||||
session: Some(session),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => BigFishSessionProcedureResult {
|
||||
ok: false,
|
||||
session: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn create_big_fish_session_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: BigFishSessionCreateInput,
|
||||
) -> Result<BigFishSessionSnapshot, String> {
|
||||
validate_session_create_input(&input).map_err(|error| error.to_string())?;
|
||||
if ctx
|
||||
.db
|
||||
.big_fish_creation_session()
|
||||
.session_id()
|
||||
.find(&input.session_id)
|
||||
.is_some()
|
||||
{
|
||||
return Err("big_fish_creation_session.session_id 已存在".to_string());
|
||||
}
|
||||
if ctx
|
||||
.db
|
||||
.big_fish_agent_message()
|
||||
.message_id()
|
||||
.find(&input.welcome_message_id)
|
||||
.is_some()
|
||||
{
|
||||
return Err("big_fish_agent_message.message_id 已存在".to_string());
|
||||
}
|
||||
|
||||
let created_at = Timestamp::from_micros_since_unix_epoch(input.created_at_micros);
|
||||
let anchor_pack = infer_anchor_pack(&input.seed_text, None);
|
||||
let asset_coverage = build_asset_coverage(None, &[]);
|
||||
ctx.db
|
||||
.big_fish_creation_session()
|
||||
.insert(BigFishCreationSession {
|
||||
session_id: input.session_id.clone(),
|
||||
owner_user_id: input.owner_user_id.clone(),
|
||||
seed_text: input.seed_text.trim().to_string(),
|
||||
current_turn: 0,
|
||||
progress_percent: 20,
|
||||
stage: BigFishCreationStage::CollectingAnchors,
|
||||
anchor_pack_json: serialize_anchor_pack(&anchor_pack)
|
||||
.map_err(|error| error.to_string())?,
|
||||
draft_json: None,
|
||||
asset_coverage_json: serialize_asset_coverage(&asset_coverage)
|
||||
.map_err(|error| error.to_string())?,
|
||||
last_assistant_reply: Some(input.welcome_message_text.clone()),
|
||||
publish_ready: false,
|
||||
created_at,
|
||||
updated_at: created_at,
|
||||
});
|
||||
ctx.db.big_fish_agent_message().insert(BigFishAgentMessage {
|
||||
message_id: input.welcome_message_id,
|
||||
session_id: input.session_id.clone(),
|
||||
role: BigFishAgentMessageRole::Assistant,
|
||||
kind: BigFishAgentMessageKind::Chat,
|
||||
text: input.welcome_message_text,
|
||||
created_at,
|
||||
});
|
||||
|
||||
get_big_fish_session_tx(
|
||||
ctx,
|
||||
BigFishSessionGetInput {
|
||||
session_id: input.session_id,
|
||||
owner_user_id: input.owner_user_id,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn get_big_fish_session_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: BigFishSessionGetInput,
|
||||
) -> Result<BigFishSessionSnapshot, String> {
|
||||
validate_session_get_input(&input).map_err(|error| error.to_string())?;
|
||||
let session = ctx
|
||||
.db
|
||||
.big_fish_creation_session()
|
||||
.session_id()
|
||||
.find(&input.session_id)
|
||||
.filter(|row| row.owner_user_id == input.owner_user_id)
|
||||
.ok_or_else(|| "big_fish_creation_session 不存在".to_string())?;
|
||||
|
||||
build_big_fish_session_snapshot(ctx, &session)
|
||||
}
|
||||
|
||||
pub(crate) fn list_big_fish_works_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: BigFishWorksListInput,
|
||||
) -> Result<Vec<BigFishWorkSummarySnapshot>, String> {
|
||||
validate_works_list_input(&input).map_err(|error| error.to_string())?;
|
||||
|
||||
let mut items = ctx
|
||||
.db
|
||||
.big_fish_creation_session()
|
||||
.iter()
|
||||
.filter(|row| {
|
||||
row.owner_user_id == input.owner_user_id && should_include_big_fish_work(ctx, row)
|
||||
})
|
||||
.map(|row| build_big_fish_work_summary(ctx, &row))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
items.sort_by(|left, right| {
|
||||
right
|
||||
.updated_at_micros
|
||||
.cmp(&left.updated_at_micros)
|
||||
.then_with(|| left.work_id.cmp(&right.work_id))
|
||||
});
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
fn should_include_big_fish_work(ctx: &ReducerContext, row: &BigFishCreationSession) -> bool {
|
||||
if big_fish_session_has_direct_work_content(row) {
|
||||
return true;
|
||||
}
|
||||
|
||||
ctx.db.big_fish_agent_message().iter().any(|message| {
|
||||
message.session_id == row.session_id
|
||||
&& matches!(message.role, BigFishAgentMessageRole::User)
|
||||
})
|
||||
}
|
||||
|
||||
fn big_fish_session_has_direct_work_content(row: &BigFishCreationSession) -> bool {
|
||||
// 助手欢迎语和默认 anchorPack 只是工作台初始状态,不应被当成草稿作品。
|
||||
!row.seed_text.trim().is_empty()
|
||||
|| row.draft_json.is_some()
|
||||
|| row.stage == BigFishCreationStage::Published
|
||||
}
|
||||
|
||||
pub(crate) fn delete_big_fish_work_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: BigFishWorkDeleteInput,
|
||||
) -> Result<Vec<BigFishWorkSummarySnapshot>, String> {
|
||||
validate_session_get_input(&BigFishSessionGetInput {
|
||||
session_id: input.session_id.clone(),
|
||||
owner_user_id: input.owner_user_id.clone(),
|
||||
})
|
||||
.map_err(|error| error.to_string())?;
|
||||
let session = ctx
|
||||
.db
|
||||
.big_fish_creation_session()
|
||||
.session_id()
|
||||
.find(&input.session_id)
|
||||
.filter(|row| row.owner_user_id == input.owner_user_id)
|
||||
.ok_or_else(|| "big_fish_creation_session 不存在".to_string())?;
|
||||
|
||||
// 删除作品时同步清理 Agent 消息、素材槽与运行快照,避免创作页消失后残留孤儿数据。
|
||||
ctx.db
|
||||
.big_fish_creation_session()
|
||||
.session_id()
|
||||
.delete(&session.session_id);
|
||||
for message in ctx
|
||||
.db
|
||||
.big_fish_agent_message()
|
||||
.iter()
|
||||
.filter(|row| row.session_id == input.session_id)
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
ctx.db
|
||||
.big_fish_agent_message()
|
||||
.message_id()
|
||||
.delete(&message.message_id);
|
||||
}
|
||||
for slot in ctx
|
||||
.db
|
||||
.big_fish_asset_slot()
|
||||
.iter()
|
||||
.filter(|row| row.session_id == input.session_id)
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
ctx.db.big_fish_asset_slot().slot_id().delete(&slot.slot_id);
|
||||
}
|
||||
for run in ctx
|
||||
.db
|
||||
.big_fish_runtime_run()
|
||||
.iter()
|
||||
.filter(|row| {
|
||||
row.session_id == input.session_id && row.owner_user_id == input.owner_user_id
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
ctx.db.big_fish_runtime_run().run_id().delete(&run.run_id);
|
||||
}
|
||||
|
||||
list_big_fish_works_tx(
|
||||
ctx,
|
||||
BigFishWorksListInput {
|
||||
owner_user_id: input.owner_user_id,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn submit_big_fish_message_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: BigFishMessageSubmitInput,
|
||||
) -> Result<BigFishSessionSnapshot, String> {
|
||||
validate_message_submit_input(&input).map_err(|error| error.to_string())?;
|
||||
let session = ctx
|
||||
.db
|
||||
.big_fish_creation_session()
|
||||
.session_id()
|
||||
.find(&input.session_id)
|
||||
.filter(|row| row.owner_user_id == input.owner_user_id)
|
||||
.ok_or_else(|| "big_fish_creation_session 不存在".to_string())?;
|
||||
if ctx
|
||||
.db
|
||||
.big_fish_agent_message()
|
||||
.message_id()
|
||||
.find(&input.user_message_id)
|
||||
.is_some()
|
||||
{
|
||||
return Err("big_fish_agent_message.user_message_id 已存在".to_string());
|
||||
}
|
||||
if ctx
|
||||
.db
|
||||
.big_fish_agent_message()
|
||||
.message_id()
|
||||
.find(&input.assistant_message_id)
|
||||
.is_some()
|
||||
{
|
||||
return Err("big_fish_agent_message.assistant_message_id 已存在".to_string());
|
||||
}
|
||||
|
||||
let submitted_at = Timestamp::from_micros_since_unix_epoch(input.submitted_at_micros);
|
||||
ctx.db.big_fish_agent_message().insert(BigFishAgentMessage {
|
||||
message_id: input.user_message_id,
|
||||
session_id: input.session_id.clone(),
|
||||
role: BigFishAgentMessageRole::User,
|
||||
kind: BigFishAgentMessageKind::Chat,
|
||||
text: input.user_message_text.trim().to_string(),
|
||||
created_at: submitted_at,
|
||||
});
|
||||
|
||||
let next_session = BigFishCreationSession {
|
||||
session_id: session.session_id.clone(),
|
||||
owner_user_id: session.owner_user_id.clone(),
|
||||
seed_text: session.seed_text.clone(),
|
||||
current_turn: session.current_turn,
|
||||
progress_percent: session.progress_percent,
|
||||
stage: BigFishCreationStage::CollectingAnchors,
|
||||
anchor_pack_json: session.anchor_pack_json.clone(),
|
||||
draft_json: session.draft_json.clone(),
|
||||
asset_coverage_json: session.asset_coverage_json.clone(),
|
||||
last_assistant_reply: session.last_assistant_reply.clone(),
|
||||
publish_ready: session.publish_ready,
|
||||
created_at: session.created_at,
|
||||
updated_at: submitted_at,
|
||||
};
|
||||
replace_big_fish_session(ctx, &session, next_session);
|
||||
|
||||
get_big_fish_session_tx(
|
||||
ctx,
|
||||
BigFishSessionGetInput {
|
||||
session_id: input.session_id,
|
||||
owner_user_id: input.owner_user_id,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn finalize_big_fish_agent_message_turn_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: BigFishMessageFinalizeInput,
|
||||
) -> Result<BigFishSessionSnapshot, String> {
|
||||
validate_message_finalize_input(&input).map_err(|error| error.to_string())?;
|
||||
let session = ctx
|
||||
.db
|
||||
.big_fish_creation_session()
|
||||
.session_id()
|
||||
.find(&input.session_id)
|
||||
.filter(|row| row.owner_user_id == input.owner_user_id)
|
||||
.ok_or_else(|| "big_fish_creation_session 不存在".to_string())?;
|
||||
let updated_at = Timestamp::from_micros_since_unix_epoch(input.updated_at_micros);
|
||||
|
||||
if let Some(error_message) = input
|
||||
.error_message
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
let next_session = BigFishCreationSession {
|
||||
session_id: session.session_id.clone(),
|
||||
owner_user_id: session.owner_user_id.clone(),
|
||||
seed_text: session.seed_text.clone(),
|
||||
current_turn: session.current_turn,
|
||||
progress_percent: session.progress_percent,
|
||||
stage: session.stage,
|
||||
anchor_pack_json: session.anchor_pack_json.clone(),
|
||||
draft_json: session.draft_json.clone(),
|
||||
asset_coverage_json: session.asset_coverage_json.clone(),
|
||||
last_assistant_reply: session.last_assistant_reply.clone(),
|
||||
publish_ready: session.publish_ready,
|
||||
created_at: session.created_at,
|
||||
updated_at,
|
||||
};
|
||||
replace_big_fish_session(ctx, &session, next_session);
|
||||
return Err(error_message.to_string());
|
||||
}
|
||||
|
||||
let assistant_message_id = input
|
||||
.assistant_message_id
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| "big_fish assistant_message_id 不能为空".to_string())?
|
||||
.to_string();
|
||||
let assistant_reply_text = input
|
||||
.assistant_reply_text
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| "big_fish assistant_reply_text 不能为空".to_string())?
|
||||
.to_string();
|
||||
if ctx
|
||||
.db
|
||||
.big_fish_agent_message()
|
||||
.message_id()
|
||||
.find(&assistant_message_id)
|
||||
.is_some()
|
||||
{
|
||||
return Err("big_fish_agent_message.assistant_message_id 已存在".to_string());
|
||||
}
|
||||
let next_anchor_pack =
|
||||
deserialize_anchor_pack(&input.anchor_pack_json).map_err(|error| error.to_string())?;
|
||||
ctx.db.big_fish_agent_message().insert(BigFishAgentMessage {
|
||||
message_id: assistant_message_id,
|
||||
session_id: input.session_id.clone(),
|
||||
role: BigFishAgentMessageRole::Assistant,
|
||||
kind: BigFishAgentMessageKind::Chat,
|
||||
text: assistant_reply_text.clone(),
|
||||
created_at: updated_at,
|
||||
});
|
||||
|
||||
let next_session = BigFishCreationSession {
|
||||
session_id: session.session_id.clone(),
|
||||
owner_user_id: session.owner_user_id.clone(),
|
||||
seed_text: session.seed_text.clone(),
|
||||
current_turn: session.current_turn.saturating_add(1),
|
||||
progress_percent: input.progress_percent.min(100),
|
||||
stage: input.stage,
|
||||
anchor_pack_json: serialize_anchor_pack(&next_anchor_pack)
|
||||
.map_err(|error| error.to_string())?,
|
||||
draft_json: session.draft_json.clone(),
|
||||
asset_coverage_json: session.asset_coverage_json.clone(),
|
||||
last_assistant_reply: Some(assistant_reply_text),
|
||||
publish_ready: session.publish_ready,
|
||||
created_at: session.created_at,
|
||||
updated_at,
|
||||
};
|
||||
replace_big_fish_session(ctx, &session, next_session);
|
||||
|
||||
get_big_fish_session_tx(
|
||||
ctx,
|
||||
BigFishSessionGetInput {
|
||||
session_id: input.session_id,
|
||||
owner_user_id: input.owner_user_id,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn compile_big_fish_draft_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: BigFishDraftCompileInput,
|
||||
) -> Result<BigFishSessionSnapshot, String> {
|
||||
validate_draft_compile_input(&input).map_err(|error| error.to_string())?;
|
||||
let session = ctx
|
||||
.db
|
||||
.big_fish_creation_session()
|
||||
.session_id()
|
||||
.find(&input.session_id)
|
||||
.filter(|row| row.owner_user_id == input.owner_user_id)
|
||||
.ok_or_else(|| "big_fish_creation_session 不存在".to_string())?;
|
||||
let anchor_pack =
|
||||
deserialize_anchor_pack(&session.anchor_pack_json).map_err(|error| error.to_string())?;
|
||||
let draft = compile_default_draft(&anchor_pack);
|
||||
let asset_slots = list_big_fish_asset_slots(ctx, &session.session_id);
|
||||
let coverage = build_asset_coverage(Some(&draft), &asset_slots);
|
||||
let compiled_at = Timestamp::from_micros_since_unix_epoch(input.compiled_at_micros);
|
||||
let reply = "第一版玩法草稿已编译完成,可以在结果页逐级生成主图、动作和场地背景。".to_string();
|
||||
|
||||
let next_session = BigFishCreationSession {
|
||||
session_id: session.session_id.clone(),
|
||||
owner_user_id: session.owner_user_id.clone(),
|
||||
seed_text: session.seed_text.clone(),
|
||||
current_turn: session.current_turn,
|
||||
progress_percent: 80,
|
||||
stage: BigFishCreationStage::DraftReady,
|
||||
anchor_pack_json: session.anchor_pack_json.clone(),
|
||||
draft_json: Some(serialize_draft(&draft).map_err(|error| error.to_string())?),
|
||||
asset_coverage_json: serialize_asset_coverage(&coverage)
|
||||
.map_err(|error| error.to_string())?,
|
||||
last_assistant_reply: Some(reply.clone()),
|
||||
publish_ready: coverage.publish_ready,
|
||||
created_at: session.created_at,
|
||||
updated_at: compiled_at,
|
||||
};
|
||||
replace_big_fish_session(ctx, &session, next_session);
|
||||
append_big_fish_system_message(
|
||||
ctx,
|
||||
&input.session_id,
|
||||
format!("big-fish-message-compile-{}", input.compiled_at_micros),
|
||||
reply,
|
||||
input.compiled_at_micros,
|
||||
);
|
||||
|
||||
get_big_fish_session_tx(
|
||||
ctx,
|
||||
BigFishSessionGetInput {
|
||||
session_id: input.session_id,
|
||||
owner_user_id: input.owner_user_id,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn build_big_fish_session_snapshot(
|
||||
ctx: &ReducerContext,
|
||||
row: &BigFishCreationSession,
|
||||
) -> Result<BigFishSessionSnapshot, String> {
|
||||
let anchor_pack =
|
||||
deserialize_anchor_pack(&row.anchor_pack_json).unwrap_or_else(|_| empty_anchor_pack());
|
||||
let draft = row
|
||||
.draft_json
|
||||
.as_deref()
|
||||
.map(deserialize_draft)
|
||||
.transpose()
|
||||
.map_err(|error| format!("big_fish.draft_json 非法: {error}"))?;
|
||||
let asset_slots = list_big_fish_asset_slots(ctx, &row.session_id);
|
||||
let asset_coverage = build_asset_coverage(draft.as_ref(), &asset_slots);
|
||||
let mut messages = ctx
|
||||
.db
|
||||
.big_fish_agent_message()
|
||||
.iter()
|
||||
.filter(|message| message.session_id == row.session_id)
|
||||
.map(|message| BigFishAgentMessageSnapshot {
|
||||
message_id: message.message_id,
|
||||
session_id: message.session_id,
|
||||
role: message.role,
|
||||
kind: message.kind,
|
||||
text: message.text,
|
||||
created_at_micros: message.created_at.to_micros_since_unix_epoch(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
messages.sort_by_key(|message| (message.created_at_micros, message.message_id.clone()));
|
||||
|
||||
Ok(BigFishSessionSnapshot {
|
||||
session_id: row.session_id.clone(),
|
||||
owner_user_id: row.owner_user_id.clone(),
|
||||
seed_text: row.seed_text.clone(),
|
||||
current_turn: row.current_turn,
|
||||
progress_percent: row.progress_percent,
|
||||
stage: row.stage,
|
||||
anchor_pack,
|
||||
draft,
|
||||
asset_slots,
|
||||
asset_coverage,
|
||||
messages,
|
||||
last_assistant_reply: row.last_assistant_reply.clone(),
|
||||
publish_ready: row.publish_ready,
|
||||
created_at_micros: row.created_at.to_micros_since_unix_epoch(),
|
||||
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn build_big_fish_work_summary(
|
||||
ctx: &ReducerContext,
|
||||
row: &BigFishCreationSession,
|
||||
) -> Result<BigFishWorkSummarySnapshot, String> {
|
||||
let draft = row
|
||||
.draft_json
|
||||
.as_deref()
|
||||
.map(deserialize_draft)
|
||||
.transpose()
|
||||
.map_err(|error| format!("big_fish.draft_json 非法: {error}"))?;
|
||||
let asset_slots = list_big_fish_asset_slots(ctx, &row.session_id);
|
||||
let coverage = build_asset_coverage(draft.as_ref(), &asset_slots);
|
||||
let cover_image_src = asset_slots
|
||||
.iter()
|
||||
.find(|slot| slot.asset_kind == BigFishAssetKind::StageBackground)
|
||||
.and_then(|slot| slot.asset_url.clone())
|
||||
.or_else(|| {
|
||||
asset_slots
|
||||
.iter()
|
||||
.find(|slot| slot.asset_kind == BigFishAssetKind::LevelMainImage)
|
||||
.and_then(|slot| slot.asset_url.clone())
|
||||
});
|
||||
let title = draft
|
||||
.as_ref()
|
||||
.map(|value| value.title.clone())
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or_else(|| "未命名大鱼草稿".to_string());
|
||||
let subtitle = draft
|
||||
.as_ref()
|
||||
.map(|value| value.subtitle.clone())
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or_else(|| "等待整理玩法草稿".to_string());
|
||||
let summary = draft
|
||||
.as_ref()
|
||||
.map(|value| value.core_fun.clone())
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or_else(|| {
|
||||
row.last_assistant_reply
|
||||
.clone()
|
||||
.unwrap_or_else(|| "继续补齐锚点后即可生成玩法草稿。".to_string())
|
||||
});
|
||||
|
||||
Ok(BigFishWorkSummarySnapshot {
|
||||
work_id: format!("big-fish-work-{}", row.session_id),
|
||||
source_session_id: row.session_id.clone(),
|
||||
title,
|
||||
subtitle,
|
||||
summary,
|
||||
cover_image_src,
|
||||
status: if row.stage == BigFishCreationStage::Published {
|
||||
"published".to_string()
|
||||
} else {
|
||||
"draft".to_string()
|
||||
},
|
||||
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
||||
publish_ready: coverage.publish_ready,
|
||||
level_count: draft
|
||||
.as_ref()
|
||||
.map(|value| value.runtime_params.level_count)
|
||||
.unwrap_or(BIG_FISH_DEFAULT_LEVEL_COUNT),
|
||||
level_main_image_ready_count: coverage.level_main_image_ready_count,
|
||||
level_motion_ready_count: coverage.level_motion_ready_count,
|
||||
background_ready: coverage.background_ready,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn replace_big_fish_session(
|
||||
ctx: &ReducerContext,
|
||||
current: &BigFishCreationSession,
|
||||
next: BigFishCreationSession,
|
||||
) {
|
||||
ctx.db
|
||||
.big_fish_creation_session()
|
||||
.session_id()
|
||||
.delete(¤t.session_id);
|
||||
ctx.db.big_fish_creation_session().insert(next);
|
||||
}
|
||||
|
||||
pub(crate) fn append_big_fish_system_message(
|
||||
ctx: &ReducerContext,
|
||||
session_id: &str,
|
||||
message_id: String,
|
||||
text: String,
|
||||
created_at_micros: i64,
|
||||
) {
|
||||
if ctx
|
||||
.db
|
||||
.big_fish_agent_message()
|
||||
.message_id()
|
||||
.find(&message_id)
|
||||
.is_some()
|
||||
{
|
||||
return;
|
||||
}
|
||||
ctx.db.big_fish_agent_message().insert(BigFishAgentMessage {
|
||||
message_id,
|
||||
session_id: session_id.to_string(),
|
||||
role: BigFishAgentMessageRole::Assistant,
|
||||
kind: BigFishAgentMessageKind::ActionResult,
|
||||
text,
|
||||
created_at: Timestamp::from_micros_since_unix_epoch(created_at_micros),
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn build_test_big_fish_session(
|
||||
seed_text: &str,
|
||||
draft_json: Option<&str>,
|
||||
stage: BigFishCreationStage,
|
||||
) -> BigFishCreationSession {
|
||||
BigFishCreationSession {
|
||||
session_id: "big-fish-session-1".to_string(),
|
||||
owner_user_id: "user-1".to_string(),
|
||||
seed_text: seed_text.to_string(),
|
||||
current_turn: 0,
|
||||
progress_percent: 20,
|
||||
stage,
|
||||
anchor_pack_json: "{}".to_string(),
|
||||
draft_json: draft_json.map(str::to_string),
|
||||
asset_coverage_json: "{}".to_string(),
|
||||
last_assistant_reply: Some("欢迎来到大鱼吃小鱼共创。".to_string()),
|
||||
publish_ready: false,
|
||||
created_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn big_fish_direct_work_content_ignores_empty_created_session() {
|
||||
let empty_session =
|
||||
build_test_big_fish_session("", None, BigFishCreationStage::CollectingAnchors);
|
||||
let seeded_session = build_test_big_fish_session(
|
||||
"想做深海吞噬成长",
|
||||
None,
|
||||
BigFishCreationStage::CollectingAnchors,
|
||||
);
|
||||
let drafted_session = build_test_big_fish_session(
|
||||
"",
|
||||
Some(r#"{"title":"深海吞噬"}"#),
|
||||
BigFishCreationStage::DraftReady,
|
||||
);
|
||||
let published_session =
|
||||
build_test_big_fish_session("", None, BigFishCreationStage::Published);
|
||||
|
||||
assert!(!big_fish_session_has_direct_work_content(&empty_session));
|
||||
assert!(big_fish_session_has_direct_work_content(&seeded_session));
|
||||
assert!(big_fish_session_has_direct_work_content(&drafted_session));
|
||||
assert!(big_fish_session_has_direct_work_content(&published_session));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user