1
This commit is contained in:
@@ -65,6 +65,32 @@ pub fn list_big_fish_works(
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
@@ -225,6 +251,69 @@ pub(crate) fn list_big_fish_works_tx(
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -453,14 +453,22 @@ fn submit_custom_world_agent_message_tx(
|
||||
{
|
||||
return Err("custom_world_agent_message.message_id 已存在".to_string());
|
||||
}
|
||||
if ctx
|
||||
if let Some(existing_operation) = ctx
|
||||
.db
|
||||
.custom_world_agent_operation()
|
||||
.operation_id()
|
||||
.find(&input.operation_id)
|
||||
.is_some()
|
||||
{
|
||||
return Err("custom_world_agent_operation.operation_id 已存在".to_string());
|
||||
let can_reuse_running_draft_operation = input.action.trim() == "draft_foundation"
|
||||
&& existing_operation.session_id == input.session_id
|
||||
&& existing_operation.operation_type == RpgAgentOperationType::DraftFoundation
|
||||
&& matches!(
|
||||
existing_operation.status,
|
||||
RpgAgentOperationStatus::Queued | RpgAgentOperationStatus::Running
|
||||
);
|
||||
if !can_reuse_running_draft_operation {
|
||||
return Err("custom_world_agent_operation.operation_id 已存在".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let submitted_at = Timestamp::from_micros_since_unix_epoch(input.submitted_at_micros);
|
||||
@@ -829,6 +837,25 @@ pub fn list_custom_world_works(
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn delete_custom_world_agent_session(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: CustomWorldAgentSessionGetInput,
|
||||
) -> CustomWorldWorksListResult {
|
||||
match ctx.try_with_tx(|tx| delete_custom_world_agent_session_tx(tx, input.clone())) {
|
||||
Ok(items) => CustomWorldWorksListResult {
|
||||
ok: true,
|
||||
items,
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => CustomWorldWorksListResult {
|
||||
ok: false,
|
||||
items: Vec::new(),
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn get_custom_world_agent_card_detail(
|
||||
ctx: &mut ProcedureContext,
|
||||
@@ -1531,6 +1558,73 @@ fn list_custom_world_work_snapshots(
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
fn delete_custom_world_agent_session_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: CustomWorldAgentSessionGetInput,
|
||||
) -> Result<Vec<CustomWorldWorkSummarySnapshot>, String> {
|
||||
validate_custom_world_agent_session_get_input(&input).map_err(|error| error.to_string())?;
|
||||
|
||||
let session = ctx
|
||||
.db
|
||||
.custom_world_agent_session()
|
||||
.session_id()
|
||||
.find(&input.session_id)
|
||||
.filter(|row| row.owner_user_id == input.owner_user_id)
|
||||
.ok_or_else(|| "custom_world_agent_session 不存在".to_string())?;
|
||||
if session.stage == RpgAgentStage::Published {
|
||||
return Err("已发布 RPG 作品请通过 profile 删除".to_string());
|
||||
}
|
||||
|
||||
// 删除纯 Agent 草稿时同步清理消息、操作与草稿卡,避免作品列表消失后残留孤儿数据。
|
||||
ctx.db
|
||||
.custom_world_agent_session()
|
||||
.session_id()
|
||||
.delete(&session.session_id);
|
||||
for message in ctx
|
||||
.db
|
||||
.custom_world_agent_message()
|
||||
.iter()
|
||||
.filter(|row| row.session_id == input.session_id)
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
ctx.db
|
||||
.custom_world_agent_message()
|
||||
.message_id()
|
||||
.delete(&message.message_id);
|
||||
}
|
||||
for operation in ctx
|
||||
.db
|
||||
.custom_world_agent_operation()
|
||||
.iter()
|
||||
.filter(|row| row.session_id == input.session_id)
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
ctx.db
|
||||
.custom_world_agent_operation()
|
||||
.operation_id()
|
||||
.delete(&operation.operation_id);
|
||||
}
|
||||
for card in ctx
|
||||
.db
|
||||
.custom_world_draft_card()
|
||||
.iter()
|
||||
.filter(|row| row.session_id == input.session_id)
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
ctx.db
|
||||
.custom_world_draft_card()
|
||||
.card_id()
|
||||
.delete(&card.card_id);
|
||||
}
|
||||
|
||||
list_custom_world_work_snapshots(
|
||||
ctx,
|
||||
CustomWorldWorksListInput {
|
||||
owner_user_id: input.owner_user_id,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn get_custom_world_agent_card_detail_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: CustomWorldAgentCardDetailGetInput,
|
||||
@@ -1601,6 +1695,156 @@ fn execute_custom_world_agent_action_tx(
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_generate_entities_action(
|
||||
ctx: &ReducerContext,
|
||||
session: &CustomWorldAgentSession,
|
||||
input: &CustomWorldAgentActionExecuteInput,
|
||||
payload: &JsonMap<String, JsonValue>,
|
||||
) -> Result<CustomWorldAgentOperationSnapshot, String> {
|
||||
ensure_refining_stage(session.stage, input.action.as_str())?;
|
||||
|
||||
let mut draft_profile = parse_optional_session_object(session.draft_profile_json.as_deref())
|
||||
.ok_or_else(|| format!("{} requires an existing draft foundation", input.action))?;
|
||||
// 结果页只消费服务端 resultPreview,这里必须先写回草稿真相再刷新预览。
|
||||
let (payload_key, profile_key, card_kind, operation_type, entity_label) =
|
||||
resolve_generate_entities_target(input.action.as_str(), payload)?;
|
||||
let generated_entities = payload
|
||||
.get(payload_key)
|
||||
.and_then(JsonValue::as_array)
|
||||
.cloned()
|
||||
.ok_or_else(|| format!("{} requires payload.{payload_key}", input.action))?;
|
||||
if generated_entities.is_empty() {
|
||||
return Err(format!("{} requires at least one generated entity", input.action));
|
||||
}
|
||||
|
||||
let mut appended_entities = Vec::new();
|
||||
for (index, entity) in generated_entities.into_iter().enumerate() {
|
||||
let normalized_entity = ensure_generated_entity_id(entity, card_kind, index);
|
||||
if normalized_entity.as_object().is_none() {
|
||||
return Err(format!("{payload_key} entries must be objects"));
|
||||
}
|
||||
upsert_generated_entity_card(
|
||||
ctx,
|
||||
&session.session_id,
|
||||
card_kind,
|
||||
&normalized_entity,
|
||||
input.submitted_at_micros,
|
||||
)?;
|
||||
appended_entities.push(normalized_entity);
|
||||
}
|
||||
|
||||
let entries = draft_profile
|
||||
.entry(profile_key.to_string())
|
||||
.or_insert_with(|| JsonValue::Array(Vec::new()))
|
||||
.as_array_mut()
|
||||
.ok_or_else(|| format!("draftProfile.{profile_key} must be array"))?;
|
||||
entries.extend(appended_entities.iter().cloned());
|
||||
|
||||
let gate = summarize_publish_gate_from_json(
|
||||
&session.session_id,
|
||||
session.stage,
|
||||
Some(&draft_profile),
|
||||
&parse_json_array_or_empty(&session.quality_findings_json),
|
||||
);
|
||||
let quality_findings = parse_json_array_or_empty(&session.quality_findings_json);
|
||||
let next_session = rebuild_custom_world_agent_session_row(
|
||||
session,
|
||||
CustomWorldAgentSessionPatch {
|
||||
draft_profile_json: Some(Some(serialize_json_value(&JsonValue::Object(
|
||||
draft_profile.clone(),
|
||||
))?)),
|
||||
last_assistant_reply: Some(Some(format!(
|
||||
"已新增 {} 个{},并刷新结果预览。",
|
||||
appended_entities.len(),
|
||||
entity_label,
|
||||
))),
|
||||
publish_gate_json: Some(Some(serialize_json_value(&publish_gate_to_json_value(
|
||||
&gate,
|
||||
))?)),
|
||||
result_preview_json: Some(build_result_preview_json(
|
||||
Some(&draft_profile),
|
||||
&gate,
|
||||
&quality_findings,
|
||||
input.submitted_at_micros,
|
||||
)?),
|
||||
checkpoints_json: Some(append_checkpoint_json(
|
||||
&session.checkpoints_json,
|
||||
&build_session_checkpoint_value(
|
||||
input.action.as_str(),
|
||||
&format!("新增{}", entity_label),
|
||||
session,
|
||||
),
|
||||
)?),
|
||||
updated_at_micros: Some(input.submitted_at_micros),
|
||||
..CustomWorldAgentSessionPatch::default()
|
||||
},
|
||||
)?;
|
||||
replace_custom_world_agent_session(ctx, session, next_session);
|
||||
|
||||
append_custom_world_action_result_message(
|
||||
ctx,
|
||||
&session.session_id,
|
||||
&input.operation_id,
|
||||
&format!("已新增 {} 个{}。", appended_entities.len(), entity_label),
|
||||
input.submitted_at_micros,
|
||||
);
|
||||
|
||||
let operation = build_and_insert_custom_world_operation(
|
||||
ctx,
|
||||
&input.operation_id,
|
||||
&session.session_id,
|
||||
operation_type,
|
||||
"新增内容已写入",
|
||||
&format!(
|
||||
"{} 已追加到 draftProfile.{},resultPreview 已刷新。",
|
||||
entity_label, profile_key,
|
||||
),
|
||||
input.submitted_at_micros,
|
||||
);
|
||||
|
||||
Ok(build_custom_world_agent_operation_snapshot(&operation))
|
||||
}
|
||||
|
||||
fn resolve_generate_entities_target(
|
||||
action: &str,
|
||||
payload: &JsonMap<String, JsonValue>,
|
||||
) -> Result<(
|
||||
&'static str,
|
||||
&'static str,
|
||||
RpgAgentDraftCardKind,
|
||||
RpgAgentOperationType,
|
||||
&'static str,
|
||||
), String> {
|
||||
match action {
|
||||
"generate_characters" => {
|
||||
let profile_key = match payload.get("roleType").and_then(JsonValue::as_str).map(str::trim) {
|
||||
Some("playable") => "playableNpcs",
|
||||
_ => "storyNpcs",
|
||||
};
|
||||
let entity_label = if profile_key == "playableNpcs" {
|
||||
"可扮演角色"
|
||||
} else {
|
||||
"场景角色"
|
||||
};
|
||||
Ok((
|
||||
"generatedCharacters",
|
||||
profile_key,
|
||||
RpgAgentDraftCardKind::Character,
|
||||
RpgAgentOperationType::GenerateCharacters,
|
||||
entity_label,
|
||||
))
|
||||
}
|
||||
"generate_landmarks" => Ok((
|
||||
"generatedLandmarks",
|
||||
"landmarks",
|
||||
RpgAgentDraftCardKind::Landmark,
|
||||
RpgAgentOperationType::GenerateLandmarks,
|
||||
"场景",
|
||||
)),
|
||||
other => Err(format!("custom world action `{other}` 当前尚未支持生成实体")),
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_draft_foundation_action(
|
||||
ctx: &ReducerContext,
|
||||
session: &CustomWorldAgentSession,
|
||||
@@ -1665,6 +1909,23 @@ fn execute_draft_foundation_action(
|
||||
updated_at,
|
||||
);
|
||||
|
||||
if let Some(existing_operation) = ctx
|
||||
.db
|
||||
.custom_world_agent_operation()
|
||||
.operation_id()
|
||||
.find(&input.operation_id)
|
||||
{
|
||||
if existing_operation.session_id != session.session_id
|
||||
|| existing_operation.operation_type != RpgAgentOperationType::DraftFoundation
|
||||
{
|
||||
return Err("custom_world_agent_operation 与 draft_foundation 写回不匹配".to_string());
|
||||
}
|
||||
ctx.db
|
||||
.custom_world_agent_operation()
|
||||
.operation_id()
|
||||
.delete(&input.operation_id);
|
||||
}
|
||||
|
||||
let operation = build_and_insert_custom_world_operation(
|
||||
ctx,
|
||||
&input.operation_id,
|
||||
|
||||
@@ -1497,7 +1497,8 @@ fn upsert_custom_world_agent_operation_progress_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: CustomWorldAgentOperationProgressInput,
|
||||
) -> Result<CustomWorldAgentOperationSnapshot, String> {
|
||||
validate_custom_world_agent_operation_progress_input(&input).map_err(|error| error.to_string())?;
|
||||
validate_custom_world_agent_operation_progress_input(&input)
|
||||
.map_err(|error| error.to_string())?;
|
||||
ctx.db
|
||||
.custom_world_agent_session()
|
||||
.session_id()
|
||||
@@ -1529,18 +1530,20 @@ fn upsert_custom_world_agent_operation_progress_tx(
|
||||
replace_custom_world_agent_operation(ctx, ¤t, next.clone());
|
||||
next
|
||||
} else {
|
||||
ctx.db.custom_world_agent_operation().insert(CustomWorldAgentOperation {
|
||||
operation_id: input.operation_id.clone(),
|
||||
session_id: input.session_id.clone(),
|
||||
operation_type: input.operation_type,
|
||||
status: input.operation_status,
|
||||
phase_label: input.phase_label.clone(),
|
||||
phase_detail: input.phase_detail.clone(),
|
||||
progress: input.operation_progress,
|
||||
error_message: input.error_message.clone(),
|
||||
created_at: timestamp,
|
||||
updated_at: timestamp,
|
||||
})
|
||||
ctx.db
|
||||
.custom_world_agent_operation()
|
||||
.insert(CustomWorldAgentOperation {
|
||||
operation_id: input.operation_id.clone(),
|
||||
session_id: input.session_id.clone(),
|
||||
operation_type: input.operation_type,
|
||||
status: input.operation_status,
|
||||
phase_label: input.phase_label.clone(),
|
||||
phase_detail: input.phase_detail.clone(),
|
||||
progress: input.operation_progress,
|
||||
error_message: input.error_message.clone(),
|
||||
created_at: timestamp,
|
||||
updated_at: timestamp,
|
||||
})
|
||||
};
|
||||
|
||||
Ok(build_custom_world_agent_operation_snapshot(&operation))
|
||||
|
||||
@@ -6,11 +6,12 @@ use module_puzzle::{
|
||||
PuzzleGeneratedImagesSaveInput, PuzzlePublicationStatus, PuzzlePublishInput, PuzzleResultDraft,
|
||||
PuzzleRunDragInput, PuzzleRunGetInput, PuzzleRunNextLevelInput, PuzzleRunProcedureResult,
|
||||
PuzzleRunSnapshot, PuzzleRunStartInput, PuzzleRunSwapInput, PuzzleRuntimeLevelStatus,
|
||||
PuzzleSelectCoverImageInput, PuzzleWorkGetInput, PuzzleWorkProcedureResult, PuzzleWorkProfile,
|
||||
PuzzleWorkUpsertInput, PuzzleWorksListInput, PuzzleWorksProcedureResult,
|
||||
apply_publish_overrides_to_draft, apply_selected_candidate, build_result_preview,
|
||||
compile_result_draft, create_work_profile, infer_anchor_pack, normalize_theme_tags,
|
||||
publish_work_profile, resolve_puzzle_grid_size, select_next_profile, start_run, swap_pieces,
|
||||
PuzzleSelectCoverImageInput, PuzzleWorkDeleteInput, PuzzleWorkGetInput,
|
||||
PuzzleWorkProcedureResult, PuzzleWorkProfile, PuzzleWorkUpsertInput, PuzzleWorksListInput,
|
||||
PuzzleWorksProcedureResult, apply_publish_overrides_to_draft, apply_selected_candidate,
|
||||
build_result_preview, compile_result_draft, create_work_profile, infer_anchor_pack,
|
||||
normalize_theme_tags, publish_work_profile, resolve_puzzle_grid_size, select_next_profile,
|
||||
start_run, swap_pieces,
|
||||
};
|
||||
use serde_json::from_str as json_from_str;
|
||||
use serde_json::to_string as json_to_string;
|
||||
@@ -310,6 +311,25 @@ pub fn update_puzzle_work(
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn delete_puzzle_work(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: PuzzleWorkDeleteInput,
|
||||
) -> PuzzleWorksProcedureResult {
|
||||
match ctx.try_with_tx(|tx| delete_puzzle_work_tx(tx, input.clone())) {
|
||||
Ok(items) => PuzzleWorksProcedureResult {
|
||||
ok: true,
|
||||
items_json: Some(serialize_json(&items)),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleWorksProcedureResult {
|
||||
ok: false,
|
||||
items_json: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn list_puzzle_gallery(ctx: &mut ProcedureContext) -> PuzzleWorksProcedureResult {
|
||||
match ctx.try_with_tx(|tx| list_puzzle_gallery_tx(tx)) {
|
||||
@@ -890,6 +910,65 @@ fn update_puzzle_work_tx(
|
||||
)
|
||||
}
|
||||
|
||||
fn delete_puzzle_work_tx(
|
||||
ctx: &TxContext,
|
||||
input: PuzzleWorkDeleteInput,
|
||||
) -> Result<Vec<PuzzleWorkProfile>, String> {
|
||||
let row = ctx
|
||||
.db
|
||||
.puzzle_work_profile()
|
||||
.profile_id()
|
||||
.find(&input.profile_id)
|
||||
.ok_or_else(|| "拼图作品不存在".to_string())?;
|
||||
if row.owner_user_id != input.owner_user_id {
|
||||
return Err("无权删除该拼图作品".to_string());
|
||||
}
|
||||
|
||||
// 删除作品时同步清理来源 Agent 会话和运行快照,保持创作页列表与运行态数据一致。
|
||||
ctx.db
|
||||
.puzzle_work_profile()
|
||||
.profile_id()
|
||||
.delete(&row.profile_id);
|
||||
if let Some(session_id) = row.source_session_id.as_ref() {
|
||||
if let Some(session) = ctx.db.puzzle_agent_session().session_id().find(session_id) {
|
||||
ctx.db
|
||||
.puzzle_agent_session()
|
||||
.session_id()
|
||||
.delete(&session.session_id);
|
||||
}
|
||||
for message in ctx
|
||||
.db
|
||||
.puzzle_agent_message()
|
||||
.iter()
|
||||
.filter(|message| message.session_id == *session_id)
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
ctx.db
|
||||
.puzzle_agent_message()
|
||||
.message_id()
|
||||
.delete(&message.message_id);
|
||||
}
|
||||
}
|
||||
for run in ctx
|
||||
.db
|
||||
.puzzle_runtime_run()
|
||||
.iter()
|
||||
.filter(|run| {
|
||||
run.owner_user_id == input.owner_user_id && run.entry_profile_id == input.profile_id
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
ctx.db.puzzle_runtime_run().run_id().delete(&run.run_id);
|
||||
}
|
||||
|
||||
list_puzzle_works_tx(
|
||||
ctx,
|
||||
PuzzleWorksListInput {
|
||||
owner_user_id: input.owner_user_id,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn list_puzzle_gallery_tx(ctx: &TxContext) -> Result<Vec<PuzzleWorkProfile>, String> {
|
||||
let mut items = ctx
|
||||
.db
|
||||
|
||||
Reference in New Issue
Block a user