接入资产生成发布叙世币消耗
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-27 23:32:34 +08:00
parent 1348b2e940
commit abe44948ee
11 changed files with 548 additions and 130 deletions

View File

@@ -37,10 +37,10 @@ use shared_contracts::{
puzzle_gallery::{PuzzleGalleryDetailResponse, PuzzleGalleryResponse},
puzzle_runtime::{
AdvanceLocalPuzzleNextLevelRequest, DragPuzzlePieceRequest, PuzzleBoardSnapshotResponse,
PuzzleCellPositionResponse, PuzzleLeaderboardEntryResponse,
PuzzleMergedGroupStateResponse, PuzzlePieceStateResponse, PuzzleRunResponse,
PuzzleRunSnapshotResponse, PuzzleRuntimeLevelSnapshotResponse, StartPuzzleRunRequest,
SubmitPuzzleLeaderboardRequest, SwapPuzzlePiecesRequest,
PuzzleCellPositionResponse, PuzzleLeaderboardEntryResponse, PuzzleMergedGroupStateResponse,
PuzzlePieceStateResponse, PuzzleRunResponse, PuzzleRunSnapshotResponse,
PuzzleRuntimeLevelSnapshotResponse, StartPuzzleRunRequest, SubmitPuzzleLeaderboardRequest,
SwapPuzzlePiecesRequest,
},
puzzle_works::{
PutPuzzleWorkRequest, PuzzleWorkDetailResponse, PuzzleWorkMutationResponse,
@@ -55,12 +55,11 @@ use spacetime_client::{
PuzzleBoardRecord, PuzzleCellPositionRecord, PuzzleCreatorIntentRecord,
PuzzleGeneratedImageCandidateRecord, PuzzleGeneratedImagesSaveRecordInput,
PuzzleLeaderboardEntryRecord, PuzzleLeaderboardSubmitRecordInput, PuzzleMergedGroupRecord,
PuzzlePieceStateRecord, PuzzlePublishRecordInput,
PuzzleResultDraftRecord, PuzzleResultPreviewBlockerRecord, PuzzleResultPreviewFindingRecord,
PuzzleResultPreviewRecord, PuzzleRunDragRecordInput, PuzzleRunRecord,
PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput, PuzzleRuntimeLevelRecord,
PuzzleSelectCoverImageRecordInput, PuzzleWorkProfileRecord, PuzzleWorkUpsertRecordInput,
SpacetimeClientError,
PuzzlePieceStateRecord, PuzzlePublishRecordInput, PuzzleResultDraftRecord,
PuzzleResultPreviewBlockerRecord, PuzzleResultPreviewFindingRecord, PuzzleResultPreviewRecord,
PuzzleRunDragRecordInput, PuzzleRunRecord, PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput,
PuzzleRuntimeLevelRecord, PuzzleSelectCoverImageRecordInput, PuzzleWorkProfileRecord,
PuzzleWorkUpsertRecordInput, SpacetimeClientError,
};
use std::convert::Infallible;
use tokio::time::sleep;
@@ -68,6 +67,7 @@ use tokio::time::sleep;
use crate::{
ai_generation_drafts::{AiGenerationDraftContext, AiGenerationDraftWriter},
api_response::json_success_body,
asset_billing::{consume_asset_operation_points, refund_asset_operation_points},
auth::AuthenticatedAccessToken,
http_error::AppError,
prompt::puzzle_image::{PUZZLE_DEFAULT_NEGATIVE_PROMPT, build_puzzle_image_prompt},
@@ -441,8 +441,22 @@ pub async fn execute_puzzle_agent_action(
let owner_user_id = authenticated.claims().user_id().to_string();
let now = current_utc_micros();
let action = payload.action.trim().to_string();
let billed_asset_kind = match action.as_str() {
"compile_puzzle_draft" => Some("puzzle_initial_image"),
"generate_puzzle_images" => Some("puzzle_generated_image"),
_ => None,
};
let billing_asset_id = format!("{session_id}:{now}");
if let Some(asset_kind) = billed_asset_kind {
consume_asset_operation_points(&state, &owner_user_id, asset_kind, &billing_asset_id)
.await
.map_err(|error| {
puzzle_error_response(&request_context, PUZZLE_AGENT_API_BASE_PROVIDER, error)
})?;
}
let (operation_type, phase_label, phase_detail, session) = match payload.action.trim() {
let (operation_type, phase_label, phase_detail, session) = match action.as_str() {
"compile_puzzle_draft" => {
let session = compile_puzzle_draft_with_initial_cover(
&state,
@@ -510,7 +524,7 @@ pub async fn execute_puzzle_agent_action(
.save_puzzle_generated_images(
PuzzleGeneratedImagesSaveRecordInput {
session_id: session.session_id,
owner_user_id,
owner_user_id: owner_user_id.clone(),
candidates_json,
saved_at_micros: now,
},
@@ -565,13 +579,18 @@ pub async fn execute_puzzle_agent_action(
}
"publish_puzzle_work" => {
let (work_id, profile_id) = build_stable_puzzle_work_ids(&session_id);
let profile = state
consume_asset_operation_points(&state, &owner_user_id, "puzzle_publish_work", &work_id)
.await
.map_err(|error| {
puzzle_error_response(&request_context, PUZZLE_AGENT_API_BASE_PROVIDER, error)
})?;
let profile_result = state
.spacetime_client()
.publish_puzzle_work(PuzzlePublishRecordInput {
session_id: session_id.clone(),
owner_user_id: owner_user_id.clone(),
// 发布沿用 session 派生的稳定作品 ID避免草稿卡与已发布卡重复。
work_id,
work_id: work_id.clone(),
profile_id,
author_display_name: resolve_author_display_name(&state, &authenticated),
level_name: payload.level_name.clone(),
@@ -579,14 +598,24 @@ pub async fn execute_puzzle_agent_action(
theme_tags: payload.theme_tags.clone(),
published_at_micros: now,
})
.await
.map_err(|error| {
puzzle_error_response(
.await;
let profile = match profile_result {
Ok(profile) => profile,
Err(error) => {
refund_asset_operation_points(
&state,
&owner_user_id,
"puzzle_publish_work",
&work_id,
)
.await;
return Err(puzzle_error_response(
&request_context,
PUZZLE_AGENT_API_BASE_PROVIDER,
map_puzzle_client_error(error),
)
})?;
));
}
};
let session = state
.spacetime_client()
@@ -626,6 +655,22 @@ pub async fn execute_puzzle_agent_action(
};
let session = session.map_err(|error| {
if let Some(asset_kind) = billed_asset_kind {
tokio::spawn({
let state = state.clone();
let owner_user_id = owner_user_id.clone();
let billing_asset_id = billing_asset_id.clone();
async move {
refund_asset_operation_points(
&state,
&owner_user_id,
asset_kind,
&billing_asset_id,
)
.await;
}
});
}
puzzle_error_response(
&request_context,
PUZZLE_AGENT_API_BASE_PROVIDER,