1260 lines
42 KiB
Rust
1260 lines
42 KiB
Rust
pub(crate) mod tables;
|
|
mod types;
|
|
|
|
pub use tables::*;
|
|
pub use types::*;
|
|
|
|
use crate::{ProcedureContext, ReducerContext, SpacetimeType, Table, Timestamp};
|
|
use module_wooden_fish::{
|
|
WoodenFishRunSnapshot, WoodenFishRunStatus, WoodenFishWordCounter, apply_run_checkpoint,
|
|
default_floating_words, finish_run, normalize_floating_words, normalize_word_counters,
|
|
};
|
|
use serde::Serialize;
|
|
use serde::de::DeserializeOwned;
|
|
use spacetimedb::AnonymousViewContext;
|
|
|
|
#[spacetimedb::view(accessor = wooden_fish_gallery_view, public)]
|
|
pub fn wooden_fish_gallery_view(ctx: &AnonymousViewContext) -> Vec<WoodenFishGalleryViewRow> {
|
|
let mut items = ctx
|
|
.db
|
|
.wooden_fish_work_profile()
|
|
.by_wooden_fish_work_publication_status()
|
|
.filter(WOODEN_FISH_PUBLICATION_PUBLISHED)
|
|
.filter_map(|row| match build_gallery_view_row(&row) {
|
|
Ok(item) => Some(item),
|
|
Err(error) => {
|
|
log::warn!(
|
|
"敲木鱼公开广场 view 跳过损坏的作品投影 profile_id={}: {}",
|
|
row.profile_id,
|
|
error
|
|
);
|
|
None
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
items.sort_by(|left, right| {
|
|
right
|
|
.updated_at_micros
|
|
.cmp(&left.updated_at_micros)
|
|
.then_with(|| left.profile_id.cmp(&right.profile_id))
|
|
});
|
|
items
|
|
}
|
|
|
|
#[spacetimedb::view(accessor = wooden_fish_gallery_card_view, public)]
|
|
pub fn wooden_fish_gallery_card_view(
|
|
ctx: &AnonymousViewContext,
|
|
) -> Vec<WoodenFishGalleryCardViewRow> {
|
|
wooden_fish_gallery_view(ctx)
|
|
.into_iter()
|
|
.map(|row| WoodenFishGalleryCardViewRow {
|
|
public_work_code: row.public_work_code,
|
|
work_id: row.work_id,
|
|
profile_id: row.profile_id,
|
|
owner_user_id: row.owner_user_id,
|
|
author_display_name: row.author_display_name,
|
|
work_title: row.work_title,
|
|
work_description: row.work_description,
|
|
cover_image_src: row.cover_image_src,
|
|
theme_tags: row.theme_tags,
|
|
publication_status: row.publication_status,
|
|
play_count: row.play_count,
|
|
updated_at_micros: row.updated_at_micros,
|
|
published_at_micros: row.published_at_micros,
|
|
generation_status: row.generation_status,
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
|
pub struct WoodenFishGalleryViewRow {
|
|
pub public_work_code: String,
|
|
pub work_id: String,
|
|
pub profile_id: String,
|
|
pub owner_user_id: String,
|
|
pub source_session_id: String,
|
|
pub author_display_name: String,
|
|
pub work_title: String,
|
|
pub work_description: String,
|
|
pub theme_tags: Vec<String>,
|
|
pub hit_object_prompt: String,
|
|
pub hit_object_reference_image_src: Option<String>,
|
|
pub hit_sound_prompt: Option<String>,
|
|
pub hit_object_asset: Option<WoodenFishImageAssetSnapshot>,
|
|
pub background_asset: Option<WoodenFishImageAssetSnapshot>,
|
|
pub hit_sound_asset: Option<WoodenFishAudioAssetSnapshot>,
|
|
pub floating_words: Vec<String>,
|
|
pub cover_image_src: String,
|
|
pub publication_status: String,
|
|
pub publish_ready: bool,
|
|
pub play_count: u32,
|
|
pub generation_status: String,
|
|
pub updated_at_micros: i64,
|
|
pub published_at_micros: Option<i64>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
|
pub struct WoodenFishGalleryCardViewRow {
|
|
pub public_work_code: String,
|
|
pub work_id: String,
|
|
pub profile_id: String,
|
|
pub owner_user_id: String,
|
|
pub author_display_name: String,
|
|
pub work_title: String,
|
|
pub work_description: String,
|
|
pub cover_image_src: String,
|
|
pub theme_tags: Vec<String>,
|
|
pub publication_status: String,
|
|
pub play_count: u32,
|
|
pub updated_at_micros: i64,
|
|
pub published_at_micros: Option<i64>,
|
|
pub generation_status: String,
|
|
}
|
|
|
|
#[spacetimedb::procedure]
|
|
pub fn create_wooden_fish_agent_session(
|
|
ctx: &mut ProcedureContext,
|
|
input: WoodenFishAgentSessionCreateInput,
|
|
) -> WoodenFishAgentSessionProcedureResult {
|
|
match ctx.try_with_tx(|tx| create_wooden_fish_agent_session_tx(tx, input.clone())) {
|
|
Ok(session) => session_result(session),
|
|
Err(message) => session_error(message),
|
|
}
|
|
}
|
|
|
|
#[spacetimedb::procedure]
|
|
pub fn get_wooden_fish_agent_session(
|
|
ctx: &mut ProcedureContext,
|
|
input: WoodenFishAgentSessionGetInput,
|
|
) -> WoodenFishAgentSessionProcedureResult {
|
|
match ctx.try_with_tx(|tx| get_wooden_fish_agent_session_tx(tx, input.clone())) {
|
|
Ok(session) => session_result(session),
|
|
Err(message) => session_error(message),
|
|
}
|
|
}
|
|
|
|
#[spacetimedb::procedure]
|
|
pub fn compile_wooden_fish_draft(
|
|
ctx: &mut ProcedureContext,
|
|
input: WoodenFishDraftCompileInput,
|
|
) -> WoodenFishAgentSessionProcedureResult {
|
|
match ctx.try_with_tx(|tx| compile_wooden_fish_draft_tx(tx, input.clone())) {
|
|
Ok(session) => session_result(session),
|
|
Err(message) => session_error(message),
|
|
}
|
|
}
|
|
|
|
#[spacetimedb::procedure]
|
|
pub fn get_wooden_fish_work_profile(
|
|
ctx: &mut ProcedureContext,
|
|
input: WoodenFishWorkGetInput,
|
|
) -> WoodenFishWorkProcedureResult {
|
|
match ctx.try_with_tx(|tx| get_wooden_fish_work_profile_tx(tx, input.clone())) {
|
|
Ok(work) => work_result(work),
|
|
Err(message) => work_error(message),
|
|
}
|
|
}
|
|
|
|
#[spacetimedb::procedure]
|
|
pub fn update_wooden_fish_work(
|
|
ctx: &mut ProcedureContext,
|
|
input: WoodenFishWorkUpdateInput,
|
|
) -> WoodenFishWorkProcedureResult {
|
|
match ctx.try_with_tx(|tx| update_wooden_fish_work_tx(tx, input.clone())) {
|
|
Ok(work) => work_result(work),
|
|
Err(message) => work_error(message),
|
|
}
|
|
}
|
|
|
|
#[spacetimedb::procedure]
|
|
pub fn publish_wooden_fish_work(
|
|
ctx: &mut ProcedureContext,
|
|
input: WoodenFishWorkPublishInput,
|
|
) -> WoodenFishWorkProcedureResult {
|
|
match ctx.try_with_tx(|tx| publish_wooden_fish_work_tx(tx, input.clone())) {
|
|
Ok(work) => work_result(work),
|
|
Err(message) => work_error(message),
|
|
}
|
|
}
|
|
|
|
#[spacetimedb::procedure]
|
|
pub fn list_wooden_fish_works(
|
|
ctx: &mut ProcedureContext,
|
|
input: WoodenFishWorksListInput,
|
|
) -> WoodenFishWorksProcedureResult {
|
|
match ctx.try_with_tx(|tx| list_wooden_fish_works_tx(tx, input.clone())) {
|
|
Ok(items) => WoodenFishWorksProcedureResult {
|
|
ok: true,
|
|
items,
|
|
error_message: None,
|
|
},
|
|
Err(message) => WoodenFishWorksProcedureResult {
|
|
ok: false,
|
|
items: Vec::new(),
|
|
error_message: Some(message),
|
|
},
|
|
}
|
|
}
|
|
|
|
#[spacetimedb::procedure]
|
|
pub fn start_wooden_fish_run(
|
|
ctx: &mut ProcedureContext,
|
|
input: WoodenFishRunStartInput,
|
|
) -> WoodenFishRunProcedureResult {
|
|
match ctx.try_with_tx(|tx| start_wooden_fish_run_tx(tx, input.clone())) {
|
|
Ok(run) => run_result(run),
|
|
Err(message) => run_error(message),
|
|
}
|
|
}
|
|
|
|
#[spacetimedb::procedure]
|
|
pub fn get_wooden_fish_run(
|
|
ctx: &mut ProcedureContext,
|
|
input: WoodenFishRunGetInput,
|
|
) -> WoodenFishRunProcedureResult {
|
|
match ctx.try_with_tx(|tx| get_wooden_fish_run_tx(tx, input.clone())) {
|
|
Ok(run) => run_result(run),
|
|
Err(message) => run_error(message),
|
|
}
|
|
}
|
|
|
|
#[spacetimedb::procedure]
|
|
pub fn checkpoint_wooden_fish_run(
|
|
ctx: &mut ProcedureContext,
|
|
input: WoodenFishRunCheckpointInput,
|
|
) -> WoodenFishRunProcedureResult {
|
|
match ctx.try_with_tx(|tx| checkpoint_wooden_fish_run_tx(tx, input.clone())) {
|
|
Ok(run) => run_result(run),
|
|
Err(message) => run_error(message),
|
|
}
|
|
}
|
|
|
|
#[spacetimedb::procedure]
|
|
pub fn finish_wooden_fish_run(
|
|
ctx: &mut ProcedureContext,
|
|
input: WoodenFishRunFinishInput,
|
|
) -> WoodenFishRunProcedureResult {
|
|
match ctx.try_with_tx(|tx| finish_wooden_fish_run_tx(tx, input.clone())) {
|
|
Ok(run) => run_result(run),
|
|
Err(message) => run_error(message),
|
|
}
|
|
}
|
|
|
|
fn create_wooden_fish_agent_session_tx(
|
|
ctx: &ReducerContext,
|
|
input: WoodenFishAgentSessionCreateInput,
|
|
) -> Result<WoodenFishAgentSessionSnapshot, String> {
|
|
require_non_empty(&input.session_id, "wooden_fish session_id")?;
|
|
require_non_empty(&input.owner_user_id, "wooden_fish owner_user_id")?;
|
|
if ctx
|
|
.db
|
|
.wooden_fish_agent_session()
|
|
.session_id()
|
|
.find(&input.session_id)
|
|
.is_some()
|
|
{
|
|
return Err("wooden_fish_agent_session.session_id 已存在".to_string());
|
|
}
|
|
|
|
let created_at = Timestamp::from_micros_since_unix_epoch(input.created_at_micros);
|
|
let config = input
|
|
.config_json
|
|
.as_deref()
|
|
.map(parse_config)
|
|
.transpose()?
|
|
.unwrap_or_else(|| default_config_from_input(&input));
|
|
let draft = input
|
|
.draft_json
|
|
.as_deref()
|
|
.map(parse_json)
|
|
.transpose()?
|
|
.unwrap_or_else(|| draft_from_config(&config, None, WOODEN_FISH_GENERATION_DRAFT));
|
|
|
|
ctx.db
|
|
.wooden_fish_agent_session()
|
|
.insert(WoodenFishAgentSessionRow {
|
|
session_id: input.session_id.clone(),
|
|
owner_user_id: input.owner_user_id.clone(),
|
|
current_turn: 0,
|
|
progress_percent: 0,
|
|
stage: WOODEN_FISH_STAGE_COLLECTING.to_string(),
|
|
config_json: to_json_string(&config),
|
|
draft_json: to_json_string(&draft),
|
|
published_profile_id: String::new(),
|
|
created_at,
|
|
updated_at: created_at,
|
|
});
|
|
|
|
get_wooden_fish_agent_session_tx(
|
|
ctx,
|
|
WoodenFishAgentSessionGetInput {
|
|
session_id: input.session_id,
|
|
owner_user_id: input.owner_user_id,
|
|
},
|
|
)
|
|
}
|
|
|
|
fn get_wooden_fish_agent_session_tx(
|
|
ctx: &ReducerContext,
|
|
input: WoodenFishAgentSessionGetInput,
|
|
) -> Result<WoodenFishAgentSessionSnapshot, String> {
|
|
let row = find_owned_session(ctx, &input.session_id, &input.owner_user_id)?;
|
|
build_session_snapshot(&row)
|
|
}
|
|
|
|
fn compile_wooden_fish_draft_tx(
|
|
ctx: &ReducerContext,
|
|
input: WoodenFishDraftCompileInput,
|
|
) -> Result<WoodenFishAgentSessionSnapshot, String> {
|
|
require_non_empty(&input.profile_id, "wooden_fish profile_id")?;
|
|
let session = find_owned_session(ctx, &input.session_id, &input.owner_user_id)?;
|
|
let tags = parse_tags(input.theme_tags_json.as_deref().unwrap_or("[]"))?;
|
|
let floating_words = match input
|
|
.floating_words_json
|
|
.as_deref()
|
|
.map(parse_json::<Vec<String>>)
|
|
.transpose()?
|
|
{
|
|
Some(words) => normalize_floating_words(&words),
|
|
None => default_floating_words(),
|
|
};
|
|
let hit_object_asset = input
|
|
.hit_object_asset_json
|
|
.as_deref()
|
|
.map(parse_json)
|
|
.transpose()?;
|
|
let hit_sound_asset = input
|
|
.hit_sound_asset_json
|
|
.as_deref()
|
|
.map(parse_json)
|
|
.transpose()?;
|
|
let background_asset = input
|
|
.background_asset_json
|
|
.as_deref()
|
|
.map(parse_json)
|
|
.transpose()?;
|
|
let cover_image_src = input
|
|
.cover_image_src
|
|
.as_deref()
|
|
.and_then(clean_optional)
|
|
.or_else(|| {
|
|
hit_object_asset
|
|
.as_ref()
|
|
.map(|asset: &WoodenFishImageAssetSnapshot| asset.image_src.clone())
|
|
});
|
|
let draft = WoodenFishDraftSnapshot {
|
|
template_id: WOODEN_FISH_TEMPLATE_ID.to_string(),
|
|
template_name: WOODEN_FISH_TEMPLATE_NAME.to_string(),
|
|
profile_id: Some(input.profile_id.clone()),
|
|
work_title: clean_string(&input.work_title, WOODEN_FISH_TEMPLATE_NAME),
|
|
work_description: input.work_description.trim().to_string(),
|
|
theme_tags: tags.clone(),
|
|
hit_object_prompt: clean_string(
|
|
&input.hit_object_prompt,
|
|
"默认敲击物图案,圆润木质质感,透明背景",
|
|
),
|
|
hit_object_reference_image_src: input
|
|
.hit_object_reference_image_src
|
|
.as_deref()
|
|
.and_then(clean_optional),
|
|
hit_sound_prompt: input.hit_sound_prompt.as_deref().and_then(clean_optional),
|
|
floating_words: floating_words.clone(),
|
|
hit_object_asset: hit_object_asset.clone(),
|
|
background_asset: background_asset.clone(),
|
|
hit_sound_asset: hit_sound_asset.clone(),
|
|
cover_image_src: cover_image_src.clone(),
|
|
generation_status: input
|
|
.generation_status
|
|
.clone()
|
|
.unwrap_or_else(|| WOODEN_FISH_GENERATION_READY.to_string()),
|
|
};
|
|
let compiled_at = Timestamp::from_micros_since_unix_epoch(input.compiled_at_micros);
|
|
let row = WoodenFishWorkProfileRow {
|
|
profile_id: input.profile_id.clone(),
|
|
work_id: input.profile_id.clone(),
|
|
owner_user_id: input.owner_user_id.clone(),
|
|
source_session_id: input.session_id.clone(),
|
|
author_display_name: clean_string(&input.author_display_name, "敲木鱼玩家"),
|
|
work_title: draft.work_title.clone(),
|
|
work_description: draft.work_description.clone(),
|
|
theme_tags_json: to_json_string(&tags),
|
|
hit_object_prompt: draft.hit_object_prompt.clone(),
|
|
hit_object_reference_image_src: draft
|
|
.hit_object_reference_image_src
|
|
.clone()
|
|
.unwrap_or_default(),
|
|
hit_sound_prompt: draft.hit_sound_prompt.clone().unwrap_or_default(),
|
|
hit_object_asset_json: hit_object_asset
|
|
.as_ref()
|
|
.map(to_json_string)
|
|
.unwrap_or_default(),
|
|
hit_sound_asset_json: hit_sound_asset
|
|
.as_ref()
|
|
.map(to_json_string)
|
|
.unwrap_or_default(),
|
|
floating_words_json: to_json_string(&floating_words),
|
|
cover_image_src: cover_image_src.unwrap_or_default(),
|
|
generation_status: draft.generation_status.clone(),
|
|
publication_status: WOODEN_FISH_PUBLICATION_DRAFT.to_string(),
|
|
play_count: 0,
|
|
updated_at: compiled_at,
|
|
published_at: None,
|
|
background_asset_json: background_asset.as_ref().map(to_json_string),
|
|
};
|
|
upsert_work(ctx, row);
|
|
let config = config_from_draft(&draft);
|
|
replace_session(
|
|
ctx,
|
|
&session,
|
|
WoodenFishAgentSessionRow {
|
|
progress_percent: 100,
|
|
stage: WOODEN_FISH_STAGE_DRAFT_COMPILED.to_string(),
|
|
config_json: to_json_string(&config),
|
|
draft_json: to_json_string(&draft),
|
|
published_profile_id: input.profile_id,
|
|
updated_at: compiled_at,
|
|
..clone_session(&session)
|
|
},
|
|
);
|
|
|
|
get_wooden_fish_agent_session_tx(
|
|
ctx,
|
|
WoodenFishAgentSessionGetInput {
|
|
session_id: input.session_id,
|
|
owner_user_id: input.owner_user_id,
|
|
},
|
|
)
|
|
}
|
|
|
|
fn get_wooden_fish_work_profile_tx(
|
|
ctx: &ReducerContext,
|
|
input: WoodenFishWorkGetInput,
|
|
) -> Result<WoodenFishWorkSnapshot, String> {
|
|
let row = find_work(ctx, &input.profile_id)?;
|
|
if !input.owner_user_id.trim().is_empty() && row.owner_user_id != input.owner_user_id {
|
|
return Err("无权访问该 wooden_fish work".to_string());
|
|
}
|
|
build_work_snapshot(&row)
|
|
}
|
|
|
|
fn update_wooden_fish_work_tx(
|
|
ctx: &ReducerContext,
|
|
input: WoodenFishWorkUpdateInput,
|
|
) -> Result<WoodenFishWorkSnapshot, String> {
|
|
let row = find_owned_work(ctx, &input.profile_id, &input.owner_user_id)?;
|
|
let updated_at = Timestamp::from_micros_since_unix_epoch(input.updated_at_micros);
|
|
let mut next = clone_work(&row);
|
|
next.work_title = clean_string(&input.work_title, &row.work_title);
|
|
next.work_description = input.work_description.trim().to_string();
|
|
next.theme_tags_json = input.theme_tags_json.clone();
|
|
if let Some(value) = input.hit_object_prompt.as_deref().and_then(clean_optional) {
|
|
next.hit_object_prompt = value;
|
|
}
|
|
if let Some(value) = input
|
|
.hit_object_reference_image_src
|
|
.as_deref()
|
|
.and_then(clean_optional)
|
|
{
|
|
next.hit_object_reference_image_src = value;
|
|
}
|
|
if let Some(value) = input.hit_sound_prompt.as_deref().and_then(clean_optional) {
|
|
next.hit_sound_prompt = value;
|
|
}
|
|
if let Some(value) = input
|
|
.hit_object_asset_json
|
|
.as_deref()
|
|
.and_then(clean_optional)
|
|
{
|
|
let asset = parse_json::<WoodenFishImageAssetSnapshot>(&value)?;
|
|
next.hit_object_asset_json = to_json_string(&asset);
|
|
next.cover_image_src = asset.image_src;
|
|
}
|
|
if let Some(value) = input
|
|
.hit_sound_asset_json
|
|
.as_deref()
|
|
.and_then(clean_optional)
|
|
{
|
|
let asset = parse_json::<WoodenFishAudioAssetSnapshot>(&value)?;
|
|
next.hit_sound_asset_json = to_json_string(&asset);
|
|
}
|
|
if let Some(value) = input
|
|
.background_asset_json
|
|
.as_deref()
|
|
.and_then(clean_optional)
|
|
{
|
|
let asset = parse_json::<WoodenFishImageAssetSnapshot>(&value)?;
|
|
next.background_asset_json = Some(to_json_string(&asset));
|
|
}
|
|
if let Some(value) = input
|
|
.floating_words_json
|
|
.as_deref()
|
|
.and_then(clean_optional)
|
|
{
|
|
let words = parse_json::<Vec<String>>(&value)?;
|
|
next.floating_words_json = to_json_string(&normalize_floating_words(&words));
|
|
}
|
|
if let Some(value) = input.cover_image_src.as_deref().and_then(clean_optional) {
|
|
next.cover_image_src = value;
|
|
}
|
|
if let Some(value) = input.generation_status.as_deref().and_then(clean_optional) {
|
|
next.generation_status = value;
|
|
}
|
|
next.updated_at = updated_at;
|
|
replace_work(ctx, &row, next);
|
|
let updated = find_work(ctx, &row.profile_id)?;
|
|
sync_session_from_work_update(ctx, &updated, updated_at)?;
|
|
build_work_snapshot(&updated)
|
|
}
|
|
|
|
fn publish_wooden_fish_work_tx(
|
|
ctx: &ReducerContext,
|
|
input: WoodenFishWorkPublishInput,
|
|
) -> Result<WoodenFishWorkSnapshot, String> {
|
|
let row = find_owned_work(ctx, &input.profile_id, &input.owner_user_id)?;
|
|
if !is_publish_ready(&row) {
|
|
return Err("发布需要完整的敲击物图案、敲击音效和飘字配置".to_string());
|
|
}
|
|
let published_at = Timestamp::from_micros_since_unix_epoch(input.published_at_micros);
|
|
replace_work(
|
|
ctx,
|
|
&row,
|
|
WoodenFishWorkProfileRow {
|
|
publication_status: WOODEN_FISH_PUBLICATION_PUBLISHED.to_string(),
|
|
updated_at: published_at,
|
|
published_at: Some(published_at),
|
|
..clone_work(&row)
|
|
},
|
|
);
|
|
if let Some(session) = ctx
|
|
.db
|
|
.wooden_fish_agent_session()
|
|
.session_id()
|
|
.find(&row.source_session_id)
|
|
{
|
|
replace_session(
|
|
ctx,
|
|
&session,
|
|
WoodenFishAgentSessionRow {
|
|
stage: WOODEN_FISH_STAGE_PUBLISHED.to_string(),
|
|
updated_at: published_at,
|
|
..clone_session(&session)
|
|
},
|
|
);
|
|
}
|
|
let updated = find_work(ctx, &row.profile_id)?;
|
|
build_work_snapshot(&updated)
|
|
}
|
|
|
|
fn list_wooden_fish_works_tx(
|
|
ctx: &ReducerContext,
|
|
input: WoodenFishWorksListInput,
|
|
) -> Result<Vec<WoodenFishWorkSnapshot>, String> {
|
|
let mut rows = if input.owner_user_id.trim().is_empty() {
|
|
ctx.db.wooden_fish_work_profile().iter().collect::<Vec<_>>()
|
|
} else {
|
|
ctx.db
|
|
.wooden_fish_work_profile()
|
|
.by_wooden_fish_work_owner_user_id()
|
|
.filter(input.owner_user_id.as_str())
|
|
.collect::<Vec<_>>()
|
|
};
|
|
if input.published_only {
|
|
rows.retain(|row| row.publication_status == WOODEN_FISH_PUBLICATION_PUBLISHED);
|
|
}
|
|
rows.sort_by(|left, right| {
|
|
right
|
|
.updated_at
|
|
.cmp(&left.updated_at)
|
|
.then_with(|| left.profile_id.cmp(&right.profile_id))
|
|
});
|
|
rows.into_iter()
|
|
.map(|row| build_work_snapshot(&row))
|
|
.collect()
|
|
}
|
|
|
|
fn start_wooden_fish_run_tx(
|
|
ctx: &ReducerContext,
|
|
input: WoodenFishRunStartInput,
|
|
) -> Result<WoodenFishRunSnapshot, String> {
|
|
require_non_empty(&input.run_id, "wooden_fish run_id")?;
|
|
let work = find_work(ctx, &input.profile_id)?;
|
|
if !is_publish_ready(&work) {
|
|
return Err("敲木鱼运行态需要完整作品配置".to_string());
|
|
}
|
|
let snapshot = WoodenFishRunSnapshot {
|
|
run_id: input.run_id.clone(),
|
|
profile_id: input.profile_id.clone(),
|
|
owner_user_id: input.owner_user_id.clone(),
|
|
status: WoodenFishRunStatus::Playing,
|
|
total_tap_count: 0,
|
|
word_counters: Vec::new(),
|
|
started_at_ms: input.started_at_ms.max(0) as u64,
|
|
updated_at_ms: input.started_at_ms.max(0) as u64,
|
|
finished_at_ms: None,
|
|
};
|
|
upsert_run(ctx, &snapshot, input.started_at_ms);
|
|
increment_work_play_count(ctx, &work, input.started_at_ms);
|
|
insert_event(
|
|
ctx,
|
|
input.client_event_id,
|
|
input.owner_user_id,
|
|
input.profile_id,
|
|
input.run_id,
|
|
WOODEN_FISH_EVENT_RUN_STARTED,
|
|
None,
|
|
input.started_at_ms,
|
|
);
|
|
Ok(snapshot)
|
|
}
|
|
|
|
fn get_wooden_fish_run_tx(
|
|
ctx: &ReducerContext,
|
|
input: WoodenFishRunGetInput,
|
|
) -> Result<WoodenFishRunSnapshot, String> {
|
|
let row = find_owned_run(ctx, &input.run_id, &input.owner_user_id)?;
|
|
parse_json(&row.snapshot_json)
|
|
}
|
|
|
|
fn checkpoint_wooden_fish_run_tx(
|
|
ctx: &ReducerContext,
|
|
input: WoodenFishRunCheckpointInput,
|
|
) -> Result<WoodenFishRunSnapshot, String> {
|
|
let row = find_owned_run(ctx, &input.run_id, &input.owner_user_id)?;
|
|
let current = parse_json::<WoodenFishRunSnapshot>(&row.snapshot_json)?;
|
|
if current.status == WoodenFishRunStatus::Finished {
|
|
return Err("wooden_fish run 已结束,不能 checkpoint".to_string());
|
|
}
|
|
let counters = parse_json::<Vec<WoodenFishWordCounter>>(&input.word_counters_json)?;
|
|
let next = apply_run_checkpoint(
|
|
¤t,
|
|
input.total_tap_count,
|
|
normalize_word_counters(counters),
|
|
input.checkpoint_at_ms.max(0) as u64,
|
|
);
|
|
replace_run(ctx, &row, &next, input.checkpoint_at_ms);
|
|
insert_event(
|
|
ctx,
|
|
input.client_event_id,
|
|
input.owner_user_id,
|
|
next.profile_id.clone(),
|
|
input.run_id,
|
|
WOODEN_FISH_EVENT_RUN_CHECKPOINT,
|
|
Some(next.total_tap_count.to_string()),
|
|
input.checkpoint_at_ms,
|
|
);
|
|
Ok(next)
|
|
}
|
|
|
|
fn finish_wooden_fish_run_tx(
|
|
ctx: &ReducerContext,
|
|
input: WoodenFishRunFinishInput,
|
|
) -> Result<WoodenFishRunSnapshot, String> {
|
|
let row = find_owned_run(ctx, &input.run_id, &input.owner_user_id)?;
|
|
let current = parse_json::<WoodenFishRunSnapshot>(&row.snapshot_json)?;
|
|
let counters = parse_json::<Vec<WoodenFishWordCounter>>(&input.word_counters_json)?;
|
|
let next = finish_run(
|
|
¤t,
|
|
input.total_tap_count,
|
|
normalize_word_counters(counters),
|
|
input.finished_at_ms.max(0) as u64,
|
|
);
|
|
replace_run(ctx, &row, &next, input.finished_at_ms);
|
|
insert_event(
|
|
ctx,
|
|
input.client_event_id,
|
|
input.owner_user_id,
|
|
next.profile_id.clone(),
|
|
input.run_id,
|
|
WOODEN_FISH_EVENT_RUN_FINISHED,
|
|
Some(next.total_tap_count.to_string()),
|
|
input.finished_at_ms,
|
|
);
|
|
Ok(next)
|
|
}
|
|
|
|
fn build_gallery_view_row(
|
|
row: &WoodenFishWorkProfileRow,
|
|
) -> Result<WoodenFishGalleryViewRow, String> {
|
|
let work = build_work_snapshot(row)?;
|
|
Ok(WoodenFishGalleryViewRow {
|
|
public_work_code: build_wooden_fish_public_work_code(&work.profile_id),
|
|
work_id: work.work_id,
|
|
profile_id: work.profile_id,
|
|
owner_user_id: work.owner_user_id,
|
|
source_session_id: work.source_session_id,
|
|
author_display_name: work.author_display_name,
|
|
work_title: work.work_title,
|
|
work_description: work.work_description,
|
|
theme_tags: work.theme_tags,
|
|
hit_object_prompt: work.hit_object_prompt,
|
|
hit_object_reference_image_src: work.hit_object_reference_image_src,
|
|
hit_sound_prompt: work.hit_sound_prompt,
|
|
hit_object_asset: work.hit_object_asset,
|
|
background_asset: work.background_asset,
|
|
hit_sound_asset: work.hit_sound_asset,
|
|
floating_words: work.floating_words,
|
|
cover_image_src: work.cover_image_src,
|
|
publication_status: work.publication_status,
|
|
publish_ready: work.publish_ready,
|
|
play_count: work.play_count,
|
|
generation_status: work.generation_status,
|
|
updated_at_micros: work.updated_at_micros,
|
|
published_at_micros: work.published_at_micros,
|
|
})
|
|
}
|
|
|
|
fn build_session_snapshot(
|
|
row: &WoodenFishAgentSessionRow,
|
|
) -> Result<WoodenFishAgentSessionSnapshot, String> {
|
|
Ok(WoodenFishAgentSessionSnapshot {
|
|
session_id: row.session_id.clone(),
|
|
owner_user_id: row.owner_user_id.clone(),
|
|
current_turn: row.current_turn,
|
|
progress_percent: row.progress_percent,
|
|
stage: row.stage.clone(),
|
|
config: parse_config(&row.config_json)?,
|
|
draft: clean_optional(&row.draft_json)
|
|
.map(|value| parse_json(&value))
|
|
.transpose()?,
|
|
published_profile_id: clean_optional(&row.published_profile_id),
|
|
created_at_micros: row.created_at.to_micros_since_unix_epoch(),
|
|
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
|
})
|
|
}
|
|
|
|
fn build_work_snapshot(row: &WoodenFishWorkProfileRow) -> Result<WoodenFishWorkSnapshot, String> {
|
|
Ok(WoodenFishWorkSnapshot {
|
|
work_id: row.work_id.clone(),
|
|
profile_id: row.profile_id.clone(),
|
|
owner_user_id: row.owner_user_id.clone(),
|
|
source_session_id: row.source_session_id.clone(),
|
|
author_display_name: row.author_display_name.clone(),
|
|
work_title: row.work_title.clone(),
|
|
work_description: row.work_description.clone(),
|
|
theme_tags: parse_tags(&row.theme_tags_json)?,
|
|
hit_object_prompt: row.hit_object_prompt.clone(),
|
|
hit_object_reference_image_src: clean_optional(&row.hit_object_reference_image_src),
|
|
hit_sound_prompt: clean_optional(&row.hit_sound_prompt),
|
|
hit_object_asset: clean_optional(&row.hit_object_asset_json)
|
|
.map(|value| parse_json(&value))
|
|
.transpose()?,
|
|
background_asset: row
|
|
.background_asset_json
|
|
.as_deref()
|
|
.and_then(clean_optional)
|
|
.map(|value| parse_json(&value))
|
|
.transpose()?,
|
|
hit_sound_asset: clean_optional(&row.hit_sound_asset_json)
|
|
.map(|value| parse_json(&value))
|
|
.transpose()?,
|
|
floating_words: normalize_floating_words(&parse_json_or_default::<Vec<String>>(
|
|
&row.floating_words_json,
|
|
)),
|
|
cover_image_src: row.cover_image_src.clone(),
|
|
publication_status: row.publication_status.clone(),
|
|
publish_ready: is_publish_ready(row),
|
|
play_count: row.play_count,
|
|
generation_status: row.generation_status.clone(),
|
|
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
|
published_at_micros: row
|
|
.published_at
|
|
.map(|value| value.to_micros_since_unix_epoch()),
|
|
})
|
|
}
|
|
|
|
fn sync_session_from_work_update(
|
|
ctx: &ReducerContext,
|
|
work: &WoodenFishWorkProfileRow,
|
|
updated_at: Timestamp,
|
|
) -> Result<(), String> {
|
|
let Some(session) = ctx
|
|
.db
|
|
.wooden_fish_agent_session()
|
|
.session_id()
|
|
.find(&work.source_session_id)
|
|
else {
|
|
return Ok(());
|
|
};
|
|
|
|
let snapshot = build_work_snapshot(work)?;
|
|
let draft = draft_from_work_snapshot(&snapshot);
|
|
let config = config_from_draft(&draft);
|
|
replace_session(
|
|
ctx,
|
|
&session,
|
|
WoodenFishAgentSessionRow {
|
|
config_json: to_json_string(&config),
|
|
draft_json: to_json_string(&draft),
|
|
updated_at,
|
|
..clone_session(&session)
|
|
},
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
fn find_owned_session(
|
|
ctx: &ReducerContext,
|
|
session_id: &str,
|
|
owner_user_id: &str,
|
|
) -> Result<WoodenFishAgentSessionRow, String> {
|
|
let row = ctx
|
|
.db
|
|
.wooden_fish_agent_session()
|
|
.session_id()
|
|
.find(&session_id.to_string())
|
|
.ok_or_else(|| "wooden_fish_agent_session 不存在".to_string())?;
|
|
if row.owner_user_id != owner_user_id {
|
|
return Err("无权访问该 wooden_fish session".to_string());
|
|
}
|
|
Ok(row)
|
|
}
|
|
|
|
fn find_work(ctx: &ReducerContext, profile_id: &str) -> Result<WoodenFishWorkProfileRow, String> {
|
|
ctx.db
|
|
.wooden_fish_work_profile()
|
|
.profile_id()
|
|
.find(&profile_id.to_string())
|
|
.ok_or_else(|| "wooden_fish_work_profile 不存在".to_string())
|
|
}
|
|
|
|
fn find_owned_work(
|
|
ctx: &ReducerContext,
|
|
profile_id: &str,
|
|
owner_user_id: &str,
|
|
) -> Result<WoodenFishWorkProfileRow, String> {
|
|
let row = find_work(ctx, profile_id)?;
|
|
if row.owner_user_id != owner_user_id {
|
|
return Err("无权访问该 wooden_fish work".to_string());
|
|
}
|
|
Ok(row)
|
|
}
|
|
|
|
fn find_owned_run(
|
|
ctx: &ReducerContext,
|
|
run_id: &str,
|
|
owner_user_id: &str,
|
|
) -> Result<WoodenFishRuntimeRunRow, String> {
|
|
let row = ctx
|
|
.db
|
|
.wooden_fish_runtime_run()
|
|
.run_id()
|
|
.find(&run_id.to_string())
|
|
.ok_or_else(|| "wooden_fish_runtime_run 不存在".to_string())?;
|
|
if row.owner_user_id != owner_user_id {
|
|
return Err("无权访问该 wooden_fish run".to_string());
|
|
}
|
|
Ok(row)
|
|
}
|
|
|
|
fn upsert_work(ctx: &ReducerContext, row: WoodenFishWorkProfileRow) {
|
|
if let Some(old) = ctx
|
|
.db
|
|
.wooden_fish_work_profile()
|
|
.profile_id()
|
|
.find(&row.profile_id)
|
|
{
|
|
ctx.db.wooden_fish_work_profile().delete(old);
|
|
}
|
|
ctx.db.wooden_fish_work_profile().insert(row);
|
|
}
|
|
|
|
fn replace_work(
|
|
ctx: &ReducerContext,
|
|
old: &WoodenFishWorkProfileRow,
|
|
next: WoodenFishWorkProfileRow,
|
|
) {
|
|
ctx.db.wooden_fish_work_profile().delete(clone_work(old));
|
|
ctx.db.wooden_fish_work_profile().insert(next);
|
|
}
|
|
|
|
fn replace_session(
|
|
ctx: &ReducerContext,
|
|
old: &WoodenFishAgentSessionRow,
|
|
next: WoodenFishAgentSessionRow,
|
|
) {
|
|
ctx.db
|
|
.wooden_fish_agent_session()
|
|
.delete(clone_session(old));
|
|
ctx.db.wooden_fish_agent_session().insert(next);
|
|
}
|
|
|
|
fn upsert_run(ctx: &ReducerContext, snapshot: &WoodenFishRunSnapshot, updated_at_ms: i64) {
|
|
if let Some(old) = ctx
|
|
.db
|
|
.wooden_fish_runtime_run()
|
|
.run_id()
|
|
.find(&snapshot.run_id)
|
|
{
|
|
ctx.db.wooden_fish_runtime_run().delete(old);
|
|
}
|
|
let created_at = Timestamp::from_micros_since_unix_epoch(updated_at_ms.saturating_mul(1000));
|
|
ctx.db
|
|
.wooden_fish_runtime_run()
|
|
.insert(run_row_from_snapshot(snapshot, created_at, created_at));
|
|
}
|
|
|
|
fn replace_run(
|
|
ctx: &ReducerContext,
|
|
old: &WoodenFishRuntimeRunRow,
|
|
snapshot: &WoodenFishRunSnapshot,
|
|
updated_at_ms: i64,
|
|
) {
|
|
ctx.db.wooden_fish_runtime_run().delete(clone_run(old));
|
|
ctx.db
|
|
.wooden_fish_runtime_run()
|
|
.insert(run_row_from_snapshot(
|
|
snapshot,
|
|
old.created_at,
|
|
Timestamp::from_micros_since_unix_epoch(updated_at_ms.saturating_mul(1000)),
|
|
));
|
|
}
|
|
|
|
fn run_row_from_snapshot(
|
|
snapshot: &WoodenFishRunSnapshot,
|
|
created_at: Timestamp,
|
|
updated_at: Timestamp,
|
|
) -> WoodenFishRuntimeRunRow {
|
|
WoodenFishRuntimeRunRow {
|
|
run_id: snapshot.run_id.clone(),
|
|
owner_user_id: snapshot.owner_user_id.clone(),
|
|
profile_id: snapshot.profile_id.clone(),
|
|
status: run_status_as_str(snapshot.status).to_string(),
|
|
total_tap_count: snapshot.total_tap_count,
|
|
word_counters_json: to_json_string(&snapshot.word_counters),
|
|
started_at_ms: snapshot.started_at_ms as i64,
|
|
updated_at_ms: snapshot.updated_at_ms as i64,
|
|
finished_at_ms: snapshot
|
|
.finished_at_ms
|
|
.map(|value| value as i64)
|
|
.unwrap_or(0),
|
|
snapshot_json: to_json_string(snapshot),
|
|
created_at,
|
|
updated_at,
|
|
}
|
|
}
|
|
|
|
fn increment_work_play_count(
|
|
ctx: &ReducerContext,
|
|
row: &WoodenFishWorkProfileRow,
|
|
played_at_ms: i64,
|
|
) {
|
|
replace_work(
|
|
ctx,
|
|
row,
|
|
WoodenFishWorkProfileRow {
|
|
play_count: row.play_count.saturating_add(1),
|
|
updated_at: Timestamp::from_micros_since_unix_epoch(played_at_ms.saturating_mul(1000)),
|
|
..clone_work(row)
|
|
},
|
|
);
|
|
}
|
|
|
|
fn insert_event(
|
|
ctx: &ReducerContext,
|
|
event_id: String,
|
|
owner_user_id: String,
|
|
profile_id: String,
|
|
run_id: String,
|
|
event_type: &str,
|
|
result: Option<String>,
|
|
occurred_at_ms: i64,
|
|
) {
|
|
let event_id = clean_optional(&event_id).unwrap_or_else(|| {
|
|
format!(
|
|
"wooden-fish-event-{}-{}-{}",
|
|
run_id, event_type, occurred_at_ms
|
|
)
|
|
});
|
|
if ctx
|
|
.db
|
|
.wooden_fish_event()
|
|
.event_id()
|
|
.find(&event_id)
|
|
.is_some()
|
|
{
|
|
return;
|
|
}
|
|
ctx.db.wooden_fish_event().insert(WoodenFishEventRow {
|
|
event_id,
|
|
owner_user_id,
|
|
profile_id,
|
|
run_id,
|
|
event_type: event_type.to_string(),
|
|
result: result.unwrap_or_default(),
|
|
occurred_at: Timestamp::from_micros_since_unix_epoch(occurred_at_ms.saturating_mul(1000)),
|
|
});
|
|
}
|
|
|
|
fn is_publish_ready(row: &WoodenFishWorkProfileRow) -> bool {
|
|
!row.work_title.trim().is_empty()
|
|
&& !row.hit_object_asset_json.trim().is_empty()
|
|
&& row
|
|
.background_asset_json
|
|
.as_deref()
|
|
.and_then(clean_optional)
|
|
.is_some()
|
|
&& !row.hit_sound_asset_json.trim().is_empty()
|
|
&& !row.floating_words_json.trim().is_empty()
|
|
&& row.generation_status == WOODEN_FISH_GENERATION_READY
|
|
}
|
|
|
|
fn default_config_from_input(
|
|
input: &WoodenFishAgentSessionCreateInput,
|
|
) -> WoodenFishCreatorConfigSnapshot {
|
|
WoodenFishCreatorConfigSnapshot {
|
|
work_title: clean_string(&input.work_title, WOODEN_FISH_TEMPLATE_NAME),
|
|
work_description: input.work_description.trim().to_string(),
|
|
theme_tags: parse_tags(input.theme_tags_json.as_deref().unwrap_or("[]"))
|
|
.unwrap_or_default(),
|
|
hit_object_prompt: "默认敲击物图案,圆润木质质感,透明背景".to_string(),
|
|
hit_object_reference_image_src: None,
|
|
hit_sound_prompt: Some("清脆短促的木鱼敲击声".to_string()),
|
|
floating_words: default_floating_words(),
|
|
}
|
|
}
|
|
|
|
fn draft_from_config(
|
|
config: &WoodenFishCreatorConfigSnapshot,
|
|
profile_id: Option<String>,
|
|
generation_status: &str,
|
|
) -> WoodenFishDraftSnapshot {
|
|
WoodenFishDraftSnapshot {
|
|
template_id: WOODEN_FISH_TEMPLATE_ID.to_string(),
|
|
template_name: WOODEN_FISH_TEMPLATE_NAME.to_string(),
|
|
profile_id,
|
|
work_title: config.work_title.clone(),
|
|
work_description: config.work_description.clone(),
|
|
theme_tags: config.theme_tags.clone(),
|
|
hit_object_prompt: config.hit_object_prompt.clone(),
|
|
hit_object_reference_image_src: config.hit_object_reference_image_src.clone(),
|
|
hit_sound_prompt: config.hit_sound_prompt.clone(),
|
|
floating_words: normalize_floating_words(&config.floating_words),
|
|
hit_object_asset: None,
|
|
background_asset: None,
|
|
hit_sound_asset: None,
|
|
cover_image_src: None,
|
|
generation_status: generation_status.to_string(),
|
|
}
|
|
}
|
|
|
|
fn draft_from_work_snapshot(work: &WoodenFishWorkSnapshot) -> WoodenFishDraftSnapshot {
|
|
WoodenFishDraftSnapshot {
|
|
template_id: WOODEN_FISH_TEMPLATE_ID.to_string(),
|
|
template_name: WOODEN_FISH_TEMPLATE_NAME.to_string(),
|
|
profile_id: Some(work.profile_id.clone()),
|
|
work_title: work.work_title.clone(),
|
|
work_description: work.work_description.clone(),
|
|
theme_tags: work.theme_tags.clone(),
|
|
hit_object_prompt: work.hit_object_prompt.clone(),
|
|
hit_object_reference_image_src: work.hit_object_reference_image_src.clone(),
|
|
hit_sound_prompt: work.hit_sound_prompt.clone(),
|
|
floating_words: work.floating_words.clone(),
|
|
hit_object_asset: work.hit_object_asset.clone(),
|
|
background_asset: work.background_asset.clone(),
|
|
hit_sound_asset: work.hit_sound_asset.clone(),
|
|
cover_image_src: clean_optional(&work.cover_image_src),
|
|
generation_status: work.generation_status.clone(),
|
|
}
|
|
}
|
|
|
|
fn config_from_draft(draft: &WoodenFishDraftSnapshot) -> WoodenFishCreatorConfigSnapshot {
|
|
WoodenFishCreatorConfigSnapshot {
|
|
work_title: draft.work_title.clone(),
|
|
work_description: draft.work_description.clone(),
|
|
theme_tags: draft.theme_tags.clone(),
|
|
hit_object_prompt: draft.hit_object_prompt.clone(),
|
|
hit_object_reference_image_src: draft.hit_object_reference_image_src.clone(),
|
|
hit_sound_prompt: draft.hit_sound_prompt.clone(),
|
|
floating_words: normalize_floating_words(&draft.floating_words),
|
|
}
|
|
}
|
|
|
|
fn build_wooden_fish_public_work_code(profile_id: &str) -> String {
|
|
let normalized = profile_id
|
|
.chars()
|
|
.filter(|character| character.is_ascii_alphanumeric())
|
|
.flat_map(|character| character.to_uppercase())
|
|
.collect::<String>();
|
|
let fallback = if normalized.is_empty() {
|
|
"00000000".to_string()
|
|
} else {
|
|
normalized
|
|
};
|
|
let suffix = if fallback.len() > 8 {
|
|
fallback[fallback.len() - 8..].to_string()
|
|
} else {
|
|
format!("{fallback:0>8}")
|
|
};
|
|
format!("WF-{suffix}")
|
|
}
|
|
|
|
fn run_status_as_str(status: WoodenFishRunStatus) -> &'static str {
|
|
match status {
|
|
WoodenFishRunStatus::Playing => "playing",
|
|
WoodenFishRunStatus::Finished => "finished",
|
|
}
|
|
}
|
|
|
|
fn require_non_empty(value: &str, label: &str) -> Result<(), String> {
|
|
if value.trim().is_empty() {
|
|
Err(format!("{label} 不能为空"))
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn clean_optional(value: &str) -> Option<String> {
|
|
let value = value.trim();
|
|
if value.is_empty() {
|
|
None
|
|
} else {
|
|
Some(value.to_string())
|
|
}
|
|
}
|
|
|
|
fn clean_string(value: &str, fallback: &str) -> String {
|
|
clean_optional(value).unwrap_or_else(|| fallback.to_string())
|
|
}
|
|
|
|
fn parse_config(value: &str) -> Result<WoodenFishCreatorConfigSnapshot, String> {
|
|
parse_json(value)
|
|
}
|
|
|
|
fn parse_tags(value: &str) -> Result<Vec<String>, String> {
|
|
Ok(parse_json_or_default::<Vec<String>>(value)
|
|
.into_iter()
|
|
.map(|tag| tag.trim().to_string())
|
|
.filter(|tag| !tag.is_empty())
|
|
.take(8)
|
|
.collect())
|
|
}
|
|
|
|
fn parse_json<T>(value: &str) -> Result<T, String>
|
|
where
|
|
T: DeserializeOwned,
|
|
{
|
|
serde_json::from_str(value).map_err(|error| error.to_string())
|
|
}
|
|
|
|
fn parse_json_or_default<T>(value: &str) -> T
|
|
where
|
|
T: DeserializeOwned + Default,
|
|
{
|
|
serde_json::from_str(value).unwrap_or_default()
|
|
}
|
|
|
|
fn to_json_string<T>(value: &T) -> String
|
|
where
|
|
T: Serialize,
|
|
{
|
|
serde_json::to_string(value).unwrap_or_else(|_| "{}".to_string())
|
|
}
|
|
|
|
fn session_result(
|
|
session: WoodenFishAgentSessionSnapshot,
|
|
) -> WoodenFishAgentSessionProcedureResult {
|
|
WoodenFishAgentSessionProcedureResult {
|
|
ok: true,
|
|
session: Some(session),
|
|
error_message: None,
|
|
}
|
|
}
|
|
|
|
fn session_error(message: String) -> WoodenFishAgentSessionProcedureResult {
|
|
WoodenFishAgentSessionProcedureResult {
|
|
ok: false,
|
|
session: None,
|
|
error_message: Some(message),
|
|
}
|
|
}
|
|
|
|
fn work_result(work: WoodenFishWorkSnapshot) -> WoodenFishWorkProcedureResult {
|
|
WoodenFishWorkProcedureResult {
|
|
ok: true,
|
|
work: Some(work),
|
|
error_message: None,
|
|
}
|
|
}
|
|
|
|
fn work_error(message: String) -> WoodenFishWorkProcedureResult {
|
|
WoodenFishWorkProcedureResult {
|
|
ok: false,
|
|
work: None,
|
|
error_message: Some(message),
|
|
}
|
|
}
|
|
|
|
fn run_result(run: WoodenFishRunSnapshot) -> WoodenFishRunProcedureResult {
|
|
WoodenFishRunProcedureResult {
|
|
ok: true,
|
|
run: Some(run),
|
|
error_message: None,
|
|
}
|
|
}
|
|
|
|
fn run_error(message: String) -> WoodenFishRunProcedureResult {
|
|
WoodenFishRunProcedureResult {
|
|
ok: false,
|
|
run: None,
|
|
error_message: Some(message),
|
|
}
|
|
}
|
|
|
|
fn clone_session(row: &WoodenFishAgentSessionRow) -> WoodenFishAgentSessionRow {
|
|
WoodenFishAgentSessionRow {
|
|
session_id: row.session_id.clone(),
|
|
owner_user_id: row.owner_user_id.clone(),
|
|
current_turn: row.current_turn,
|
|
progress_percent: row.progress_percent,
|
|
stage: row.stage.clone(),
|
|
config_json: row.config_json.clone(),
|
|
draft_json: row.draft_json.clone(),
|
|
published_profile_id: row.published_profile_id.clone(),
|
|
created_at: row.created_at,
|
|
updated_at: row.updated_at,
|
|
}
|
|
}
|
|
|
|
fn clone_work(row: &WoodenFishWorkProfileRow) -> WoodenFishWorkProfileRow {
|
|
WoodenFishWorkProfileRow {
|
|
profile_id: row.profile_id.clone(),
|
|
work_id: row.work_id.clone(),
|
|
owner_user_id: row.owner_user_id.clone(),
|
|
source_session_id: row.source_session_id.clone(),
|
|
author_display_name: row.author_display_name.clone(),
|
|
work_title: row.work_title.clone(),
|
|
work_description: row.work_description.clone(),
|
|
theme_tags_json: row.theme_tags_json.clone(),
|
|
hit_object_prompt: row.hit_object_prompt.clone(),
|
|
hit_object_reference_image_src: row.hit_object_reference_image_src.clone(),
|
|
hit_sound_prompt: row.hit_sound_prompt.clone(),
|
|
hit_object_asset_json: row.hit_object_asset_json.clone(),
|
|
background_asset_json: row.background_asset_json.clone(),
|
|
hit_sound_asset_json: row.hit_sound_asset_json.clone(),
|
|
floating_words_json: row.floating_words_json.clone(),
|
|
cover_image_src: row.cover_image_src.clone(),
|
|
generation_status: row.generation_status.clone(),
|
|
publication_status: row.publication_status.clone(),
|
|
play_count: row.play_count,
|
|
updated_at: row.updated_at,
|
|
published_at: row.published_at,
|
|
}
|
|
}
|
|
|
|
fn clone_run(row: &WoodenFishRuntimeRunRow) -> WoodenFishRuntimeRunRow {
|
|
WoodenFishRuntimeRunRow {
|
|
run_id: row.run_id.clone(),
|
|
owner_user_id: row.owner_user_id.clone(),
|
|
profile_id: row.profile_id.clone(),
|
|
status: row.status.clone(),
|
|
total_tap_count: row.total_tap_count,
|
|
word_counters_json: row.word_counters_json.clone(),
|
|
started_at_ms: row.started_at_ms,
|
|
updated_at_ms: row.updated_at_ms,
|
|
finished_at_ms: row.finished_at_ms,
|
|
snapshot_json: row.snapshot_json.clone(),
|
|
created_at: row.created_at,
|
|
updated_at: row.updated_at,
|
|
}
|
|
}
|