1
This commit is contained in:
@@ -9,8 +9,7 @@ use axum::{
|
||||
response::Response,
|
||||
};
|
||||
use platform_auth::{
|
||||
AccessTokenClaims, AuthProvider, BindingStatus, read_refresh_session_token,
|
||||
verify_access_token,
|
||||
AccessTokenClaims, AuthProvider, BindingStatus, read_refresh_session_token, verify_access_token,
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
use time::OffsetDateTime;
|
||||
@@ -237,11 +236,11 @@ mod tests {
|
||||
INTERNAL_API_SECRET_HEADER, INTERNAL_AUTH_USER_ID_HEADER, RefreshSessionToken,
|
||||
extract_bearer_token, try_build_internal_forwarded_claims,
|
||||
};
|
||||
use crate::{config::AppConfig, state::AppState};
|
||||
use axum::{
|
||||
http::{HeaderMap, HeaderValue, StatusCode, header::AUTHORIZATION},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use crate::{config::AppConfig, state::AppState};
|
||||
|
||||
#[test]
|
||||
fn extract_bearer_token_accepts_standard_header() {
|
||||
|
||||
@@ -9,8 +9,8 @@ use shared_contracts::big_fish::{
|
||||
BigFishActionResponse, BigFishAgentMessageResponse, BigFishAnchorItemResponse,
|
||||
BigFishAnchorPackResponse, BigFishAssetCoverageResponse, BigFishAssetSlotResponse,
|
||||
BigFishBackgroundBlueprintResponse, BigFishGameDraftResponse, BigFishLevelBlueprintResponse,
|
||||
BigFishRuntimeEntityResponse, BigFishRuntimeParamsResponse, BigFishRuntimeSnapshotResponse,
|
||||
BigFishRunResponse, BigFishSessionResponse, BigFishSessionSnapshotResponse,
|
||||
BigFishRunResponse, BigFishRuntimeEntityResponse, BigFishRuntimeParamsResponse,
|
||||
BigFishRuntimeSnapshotResponse, BigFishSessionResponse, BigFishSessionSnapshotResponse,
|
||||
BigFishVector2Response, CreateBigFishSessionRequest, ExecuteBigFishActionRequest,
|
||||
SendBigFishMessageRequest, SubmitBigFishInputRequest,
|
||||
};
|
||||
@@ -58,7 +58,9 @@ pub async fn create_big_fish_session(
|
||||
created_at_micros: current_utc_micros(),
|
||||
})
|
||||
.await
|
||||
.map_err(|error| big_fish_error_response(&request_context, map_big_fish_client_error(error)))?;
|
||||
.map_err(|error| {
|
||||
big_fish_error_response(&request_context, map_big_fish_client_error(error))
|
||||
})?;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
@@ -80,7 +82,9 @@ pub async fn get_big_fish_session(
|
||||
.spacetime_client()
|
||||
.get_big_fish_session(session_id, authenticated.claims().user_id().to_string())
|
||||
.await
|
||||
.map_err(|error| big_fish_error_response(&request_context, map_big_fish_client_error(error)))?;
|
||||
.map_err(|error| {
|
||||
big_fish_error_response(&request_context, map_big_fish_client_error(error))
|
||||
})?;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
@@ -128,7 +132,9 @@ pub async fn submit_big_fish_message(
|
||||
submitted_at_micros: current_utc_micros(),
|
||||
})
|
||||
.await
|
||||
.map_err(|error| big_fish_error_response(&request_context, map_big_fish_client_error(error)))?;
|
||||
.map_err(|error| {
|
||||
big_fish_error_response(&request_context, map_big_fish_client_error(error))
|
||||
})?;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
@@ -168,7 +174,9 @@ pub async fn stream_big_fish_message(
|
||||
submitted_at_micros: current_utc_micros(),
|
||||
})
|
||||
.await
|
||||
.map_err(|error| big_fish_error_response(&request_context, map_big_fish_client_error(error)))?;
|
||||
.map_err(|error| {
|
||||
big_fish_error_response(&request_context, map_big_fish_client_error(error))
|
||||
})?;
|
||||
|
||||
let session_response = map_big_fish_session_response(session);
|
||||
let reply_text = session_response
|
||||
@@ -176,9 +184,24 @@ pub async fn stream_big_fish_message(
|
||||
.clone()
|
||||
.unwrap_or_else(|| "锚点已更新。".to_string());
|
||||
let mut sse_body = String::new();
|
||||
append_sse_event(&request_context, &mut sse_body, "reply_delta", &json!({ "text": reply_text }))?;
|
||||
append_sse_event(&request_context, &mut sse_body, "session", &json!({ "session": session_response }))?;
|
||||
append_sse_event(&request_context, &mut sse_body, "done", &json!({ "ok": true }))?;
|
||||
append_sse_event(
|
||||
&request_context,
|
||||
&mut sse_body,
|
||||
"reply_delta",
|
||||
&json!({ "text": reply_text }),
|
||||
)?;
|
||||
append_sse_event(
|
||||
&request_context,
|
||||
&mut sse_body,
|
||||
"session",
|
||||
&json!({ "session": session_response }),
|
||||
)?;
|
||||
append_sse_event(
|
||||
&request_context,
|
||||
&mut sse_body,
|
||||
"done",
|
||||
&json!({ "ok": true }),
|
||||
)?;
|
||||
Ok(build_event_stream_response(sse_body))
|
||||
}
|
||||
|
||||
@@ -288,7 +311,9 @@ pub async fn start_big_fish_run(
|
||||
started_at_micros: current_utc_micros(),
|
||||
})
|
||||
.await
|
||||
.map_err(|error| big_fish_error_response(&request_context, map_big_fish_client_error(error)))?;
|
||||
.map_err(|error| {
|
||||
big_fish_error_response(&request_context, map_big_fish_client_error(error))
|
||||
})?;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
@@ -310,7 +335,9 @@ pub async fn get_big_fish_run(
|
||||
.spacetime_client()
|
||||
.get_big_fish_run(run_id, authenticated.claims().user_id().to_string())
|
||||
.await
|
||||
.map_err(|error| big_fish_error_response(&request_context, map_big_fish_client_error(error)))?;
|
||||
.map_err(|error| {
|
||||
big_fish_error_response(&request_context, map_big_fish_client_error(error))
|
||||
})?;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
@@ -348,7 +375,9 @@ pub async fn submit_big_fish_input(
|
||||
submitted_at_micros: current_utc_micros(),
|
||||
})
|
||||
.await
|
||||
.map_err(|error| big_fish_error_response(&request_context, map_big_fish_client_error(error)))?;
|
||||
.map_err(|error| {
|
||||
big_fish_error_response(&request_context, map_big_fish_client_error(error))
|
||||
})?;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
@@ -383,7 +412,9 @@ fn map_big_fish_session_response(session: BigFishSessionRecord) -> BigFishSessio
|
||||
}
|
||||
}
|
||||
|
||||
fn map_big_fish_anchor_pack_response(anchor_pack: BigFishAnchorPackRecord) -> BigFishAnchorPackResponse {
|
||||
fn map_big_fish_anchor_pack_response(
|
||||
anchor_pack: BigFishAnchorPackRecord,
|
||||
) -> BigFishAnchorPackResponse {
|
||||
BigFishAnchorPackResponse {
|
||||
gameplay_promise: map_big_fish_anchor_item_response(anchor_pack.gameplay_promise),
|
||||
ecology_visual_theme: map_big_fish_anchor_item_response(anchor_pack.ecology_visual_theme),
|
||||
@@ -417,7 +448,9 @@ fn map_big_fish_draft_response(draft: BigFishGameDraftRecord) -> BigFishGameDraf
|
||||
}
|
||||
}
|
||||
|
||||
fn map_big_fish_level_response(level: BigFishLevelBlueprintRecord) -> BigFishLevelBlueprintResponse {
|
||||
fn map_big_fish_level_response(
|
||||
level: BigFishLevelBlueprintRecord,
|
||||
) -> BigFishLevelBlueprintResponse {
|
||||
BigFishLevelBlueprintResponse {
|
||||
level: level.level,
|
||||
name: level.name,
|
||||
@@ -528,7 +561,9 @@ fn map_big_fish_runtime_response(run: BigFishRuntimeRecord) -> BigFishRuntimeSna
|
||||
}
|
||||
}
|
||||
|
||||
fn map_big_fish_entity_response(entity: BigFishRuntimeEntityRecord) -> BigFishRuntimeEntityResponse {
|
||||
fn map_big_fish_entity_response(
|
||||
entity: BigFishRuntimeEntityRecord,
|
||||
) -> BigFishRuntimeEntityResponse {
|
||||
BigFishRuntimeEntityResponse {
|
||||
entity_id: entity.entity_id,
|
||||
level: entity.level,
|
||||
@@ -547,7 +582,8 @@ fn map_big_fish_vector_response(vector: BigFishVector2Record) -> BigFishVector2R
|
||||
|
||||
fn build_big_fish_welcome_text(seed_text: &str) -> String {
|
||||
if seed_text.trim().is_empty() {
|
||||
return "我会先帮你确定大鱼吃小鱼的核心锚点。可以从主题生态、成长阶梯或风险节奏开始。".to_string();
|
||||
return "我会先帮你确定大鱼吃小鱼的核心锚点。可以从主题生态、成长阶梯或风险节奏开始。"
|
||||
.to_string();
|
||||
}
|
||||
"我已经收到你的玩法起点,会先把它整理成锚点并准备结果页草稿。".to_string()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{env, net::SocketAddr};
|
||||
use std::{env, fs, net::SocketAddr, path::PathBuf};
|
||||
|
||||
use platform_llm::{
|
||||
DEFAULT_ARK_BASE_URL, DEFAULT_MAX_RETRIES, DEFAULT_REQUEST_TIMEOUT_MS,
|
||||
@@ -7,6 +7,7 @@ use platform_llm::{
|
||||
|
||||
const DEFAULT_LLM_MODEL: &str = "doubao-1-5-pro-32k-character-250715";
|
||||
const DEFAULT_INTERNAL_API_SECRET: &str = "genarrative-dev-internal-bridge";
|
||||
const SPACETIME_LOCAL_CONFIG_FILE: &str = "spacetime.local.json";
|
||||
|
||||
// 集中管理 api-server 的启动配置,避免入口层直接散落环境变量解析逻辑。
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -123,6 +124,10 @@ impl AppConfig {
|
||||
pub fn from_env() -> Self {
|
||||
let mut config = Self::default();
|
||||
|
||||
if let Some(local_spacetime_database) = read_local_spacetime_database() {
|
||||
config.spacetime_database = local_spacetime_database;
|
||||
}
|
||||
|
||||
if let Ok(bind_host) = env::var("GENARRATIVE_API_HOST")
|
||||
&& !bind_host.trim().is_empty()
|
||||
{
|
||||
@@ -141,8 +146,7 @@ impl AppConfig {
|
||||
config.log_filter = log_filter;
|
||||
}
|
||||
|
||||
config.internal_api_secret =
|
||||
read_first_non_empty_env(&["GENARRATIVE_INTERNAL_API_SECRET"]);
|
||||
config.internal_api_secret = read_first_non_empty_env(&["GENARRATIVE_INTERNAL_API_SECRET"]);
|
||||
|
||||
if let Some(jwt_issuer) =
|
||||
read_first_non_empty_env(&["GENARRATIVE_JWT_ISSUER", "JWT_ISSUER"])
|
||||
@@ -361,6 +365,33 @@ fn read_first_non_empty_env(keys: &[&str]) -> Option<String> {
|
||||
})
|
||||
}
|
||||
|
||||
fn read_local_spacetime_database() -> Option<String> {
|
||||
let config_path = find_upward_file(SPACETIME_LOCAL_CONFIG_FILE)?;
|
||||
let raw_text = fs::read_to_string(config_path).ok()?;
|
||||
let parsed = serde_json::from_str::<serde_json::Value>(&raw_text).ok()?;
|
||||
parsed
|
||||
.get("database")
|
||||
.and_then(|value| value.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
fn find_upward_file(file_name: &str) -> Option<PathBuf> {
|
||||
let mut current_dir = env::current_dir().ok()?;
|
||||
|
||||
loop {
|
||||
let candidate = current_dir.join(file_name);
|
||||
if candidate.is_file() {
|
||||
return Some(candidate);
|
||||
}
|
||||
|
||||
if !current_dir.pop() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_first_duration_seconds_env(keys: &[&str]) -> Option<u64> {
|
||||
keys.iter().find_map(|key| {
|
||||
env::var(key)
|
||||
|
||||
@@ -30,15 +30,16 @@ use shared_contracts::{
|
||||
SwapPuzzlePiecesRequest,
|
||||
},
|
||||
puzzle_works::{
|
||||
PuzzleWorkDetailResponse, PuzzleWorkMutationResponse, PuzzleWorkProfileResponse,
|
||||
PuzzleWorkSummaryResponse, PuzzleWorksResponse, PutPuzzleWorkRequest,
|
||||
PutPuzzleWorkRequest, PuzzleWorkDetailResponse, PuzzleWorkMutationResponse,
|
||||
PuzzleWorkProfileResponse, PuzzleWorkSummaryResponse, PuzzleWorksResponse,
|
||||
},
|
||||
};
|
||||
use shared_kernel::build_prefixed_uuid_id;
|
||||
use spacetime_client::{
|
||||
PuzzleAgentMessageRecord, PuzzleAgentMessageSubmitRecordInput, PuzzleAgentSessionCreateRecordInput,
|
||||
PuzzleAgentSessionRecord, PuzzleAgentSuggestedActionRecord, PuzzleAnchorItemRecord,
|
||||
PuzzleAnchorPackRecord, PuzzleCreatorIntentRecord, PuzzleGeneratedImageCandidateRecord,
|
||||
PuzzleAgentMessageRecord, PuzzleAgentMessageSubmitRecordInput,
|
||||
PuzzleAgentSessionCreateRecordInput, PuzzleAgentSessionRecord,
|
||||
PuzzleAgentSuggestedActionRecord, PuzzleAnchorItemRecord, PuzzleAnchorPackRecord,
|
||||
PuzzleCreatorIntentRecord, PuzzleGeneratedImageCandidateRecord,
|
||||
PuzzleGeneratedImagesSaveRecordInput, PuzzlePublishRecordInput, PuzzleResultDraftRecord,
|
||||
PuzzleResultPreviewBlockerRecord, PuzzleResultPreviewFindingRecord, PuzzleResultPreviewRecord,
|
||||
PuzzleRunDragRecordInput, PuzzleRunRecord, PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput,
|
||||
@@ -47,11 +48,8 @@ use spacetime_client::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
api_response::json_success_body,
|
||||
auth::AuthenticatedAccessToken,
|
||||
http_error::AppError,
|
||||
request_context::RequestContext,
|
||||
state::AppState,
|
||||
api_response::json_success_body, auth::AuthenticatedAccessToken, http_error::AppError,
|
||||
request_context::RequestContext, state::AppState,
|
||||
};
|
||||
|
||||
const PUZZLE_AGENT_API_BASE_PROVIDER: &str = "puzzle-agent";
|
||||
@@ -110,14 +108,16 @@ pub async fn get_puzzle_agent_session(
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
ensure_non_empty(&request_context, PUZZLE_AGENT_API_BASE_PROVIDER, &session_id, "sessionId")?;
|
||||
ensure_non_empty(
|
||||
&request_context,
|
||||
PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||
&session_id,
|
||||
"sessionId",
|
||||
)?;
|
||||
|
||||
let session = state
|
||||
.spacetime_client()
|
||||
.get_puzzle_agent_session(
|
||||
session_id,
|
||||
authenticated.claims().user_id().to_string(),
|
||||
)
|
||||
.get_puzzle_agent_session(session_id, authenticated.claims().user_id().to_string())
|
||||
.await
|
||||
.map_err(|error| {
|
||||
puzzle_error_response(
|
||||
@@ -152,7 +152,12 @@ pub async fn submit_puzzle_agent_message(
|
||||
})),
|
||||
)
|
||||
})?;
|
||||
ensure_non_empty(&request_context, PUZZLE_AGENT_API_BASE_PROVIDER, &session_id, "sessionId")?;
|
||||
ensure_non_empty(
|
||||
&request_context,
|
||||
PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||
&session_id,
|
||||
"sessionId",
|
||||
)?;
|
||||
|
||||
let client_message_id = payload.client_message_id.trim().to_string();
|
||||
let message_text = payload.text.trim().to_string();
|
||||
@@ -207,7 +212,12 @@ pub async fn stream_puzzle_agent_message(
|
||||
})),
|
||||
)
|
||||
})?;
|
||||
ensure_non_empty(&request_context, PUZZLE_AGENT_API_BASE_PROVIDER, &session_id, "sessionId")?;
|
||||
ensure_non_empty(
|
||||
&request_context,
|
||||
PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||
&session_id,
|
||||
"sessionId",
|
||||
)?;
|
||||
|
||||
let session = state
|
||||
.spacetime_client()
|
||||
@@ -245,7 +255,12 @@ pub async fn stream_puzzle_agent_message(
|
||||
"session",
|
||||
&json!({ "session": session_response }),
|
||||
)?;
|
||||
append_sse_event(&request_context, &mut sse_body, "done", &json!({ "ok": true }))?;
|
||||
append_sse_event(
|
||||
&request_context,
|
||||
&mut sse_body,
|
||||
"done",
|
||||
&json!({ "ok": true }),
|
||||
)?;
|
||||
Ok(build_event_stream_response(sse_body))
|
||||
}
|
||||
|
||||
@@ -266,7 +281,12 @@ pub async fn execute_puzzle_agent_action(
|
||||
})),
|
||||
)
|
||||
})?;
|
||||
ensure_non_empty(&request_context, PUZZLE_AGENT_API_BASE_PROVIDER, &session_id, "sessionId")?;
|
||||
ensure_non_empty(
|
||||
&request_context,
|
||||
PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||
&session_id,
|
||||
"sessionId",
|
||||
)?;
|
||||
|
||||
let owner_user_id = authenticated.claims().user_id().to_string();
|
||||
let now = current_utc_micros();
|
||||
@@ -327,11 +347,11 @@ pub async fn execute_puzzle_agent_action(
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.map_err(|error| {
|
||||
SpacetimeClientError::Runtime(format!(
|
||||
"拼图候选图序列化失败:{error}"
|
||||
))
|
||||
});
|
||||
.map_err(|error| {
|
||||
SpacetimeClientError::Runtime(format!(
|
||||
"拼图候选图序列化失败:{error}"
|
||||
))
|
||||
});
|
||||
match candidates_json {
|
||||
Ok(candidates_json) => {
|
||||
state
|
||||
@@ -497,7 +517,12 @@ pub async fn get_puzzle_work_detail(
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(_authenticated): Extension<AuthenticatedAccessToken>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
ensure_non_empty(&request_context, PUZZLE_WORKS_PROVIDER, &profile_id, "profileId")?;
|
||||
ensure_non_empty(
|
||||
&request_context,
|
||||
PUZZLE_WORKS_PROVIDER,
|
||||
&profile_id,
|
||||
"profileId",
|
||||
)?;
|
||||
|
||||
let item = state
|
||||
.spacetime_client()
|
||||
@@ -536,7 +561,12 @@ pub async fn put_puzzle_work(
|
||||
})),
|
||||
)
|
||||
})?;
|
||||
ensure_non_empty(&request_context, PUZZLE_WORKS_PROVIDER, &profile_id, "profileId")?;
|
||||
ensure_non_empty(
|
||||
&request_context,
|
||||
PUZZLE_WORKS_PROVIDER,
|
||||
&profile_id,
|
||||
"profileId",
|
||||
)?;
|
||||
|
||||
let item = state
|
||||
.spacetime_client()
|
||||
@@ -599,7 +629,12 @@ pub async fn get_puzzle_gallery_detail(
|
||||
AxumPath(profile_id): AxumPath<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
ensure_non_empty(&request_context, PUZZLE_GALLERY_PROVIDER, &profile_id, "profileId")?;
|
||||
ensure_non_empty(
|
||||
&request_context,
|
||||
PUZZLE_GALLERY_PROVIDER,
|
||||
&profile_id,
|
||||
"profileId",
|
||||
)?;
|
||||
|
||||
let item = state
|
||||
.spacetime_client()
|
||||
@@ -637,7 +672,12 @@ pub async fn start_puzzle_run(
|
||||
})),
|
||||
)
|
||||
})?;
|
||||
ensure_non_empty(&request_context, PUZZLE_RUNTIME_PROVIDER, &payload.profile_id, "profileId")?;
|
||||
ensure_non_empty(
|
||||
&request_context,
|
||||
PUZZLE_RUNTIME_PROVIDER,
|
||||
&payload.profile_id,
|
||||
"profileId",
|
||||
)?;
|
||||
|
||||
let run = state
|
||||
.spacetime_client()
|
||||
@@ -767,7 +807,12 @@ pub async fn drag_puzzle_piece_or_group(
|
||||
)
|
||||
})?;
|
||||
ensure_non_empty(&request_context, PUZZLE_RUNTIME_PROVIDER, &run_id, "runId")?;
|
||||
ensure_non_empty(&request_context, PUZZLE_RUNTIME_PROVIDER, &payload.piece_id, "pieceId")?;
|
||||
ensure_non_empty(
|
||||
&request_context,
|
||||
PUZZLE_RUNTIME_PROVIDER,
|
||||
&payload.piece_id,
|
||||
"pieceId",
|
||||
)?;
|
||||
|
||||
let run = state
|
||||
.spacetime_client()
|
||||
@@ -850,12 +895,16 @@ fn map_puzzle_agent_session_response(
|
||||
.into_iter()
|
||||
.map(map_puzzle_suggested_action_response)
|
||||
.collect(),
|
||||
result_preview: session.result_preview.map(map_puzzle_result_preview_response),
|
||||
result_preview: session
|
||||
.result_preview
|
||||
.map(map_puzzle_result_preview_response),
|
||||
updated_at: session.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_puzzle_anchor_pack_response(anchor_pack: PuzzleAnchorPackRecord) -> PuzzleAnchorPackResponse {
|
||||
fn map_puzzle_anchor_pack_response(
|
||||
anchor_pack: PuzzleAnchorPackRecord,
|
||||
) -> PuzzleAnchorPackResponse {
|
||||
PuzzleAnchorPackResponse {
|
||||
theme_promise: map_puzzle_anchor_item_response(anchor_pack.theme_promise),
|
||||
visual_subject: map_puzzle_anchor_item_response(anchor_pack.visual_subject),
|
||||
@@ -1098,8 +1147,7 @@ fn resolve_author_display_name(
|
||||
|
||||
fn build_puzzle_welcome_text(seed_text: &str) -> String {
|
||||
if seed_text.trim().is_empty() {
|
||||
return "先告诉我你想做一张什么样的拼图图像,我会帮你收束成可发布的题材锚点。"
|
||||
.to_string();
|
||||
return "先告诉我你想做一张什么样的拼图图像,我会帮你收束成可发布的题材锚点。".to_string();
|
||||
}
|
||||
|
||||
"我先接住你的画面灵感,再一起把它收束成正式拼图关卡。".to_string()
|
||||
@@ -1124,11 +1172,7 @@ fn ensure_non_empty(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn puzzle_bad_request(
|
||||
request_context: &RequestContext,
|
||||
provider: &str,
|
||||
message: &str,
|
||||
) -> Response {
|
||||
fn puzzle_bad_request(request_context: &RequestContext, provider: &str, message: &str) -> Response {
|
||||
puzzle_error_response(
|
||||
request_context,
|
||||
provider,
|
||||
@@ -1279,7 +1323,11 @@ fn save_placeholder_puzzle_asset(
|
||||
fs::write(output_dir.join(&file_name), svg).map_err(io_error)?;
|
||||
|
||||
Ok(GeneratedPuzzleAssetResponse {
|
||||
image_src: format!("/{}/{}", relative_dir.to_string_lossy().replace('\\', "/"), file_name),
|
||||
image_src: format!(
|
||||
"/{}/{}",
|
||||
relative_dir.to_string_lossy().replace('\\', "/"),
|
||||
file_name
|
||||
),
|
||||
asset_id,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,35 +11,27 @@ use module_npc::{
|
||||
use module_runtime::RuntimeSnapshotRecord;
|
||||
use module_runtime_story_compat::{
|
||||
CONTINUE_ADVENTURE_FUNCTION_ID, CurrentEncounterNpcQuestContext, GeneratedStoryPayload,
|
||||
PendingQuestOfferContext, RuntimeStoryActionResponseParts,
|
||||
StoryResolution, add_player_currency, add_player_inventory_items,
|
||||
append_story_history, apply_equipment_loadout_to_state,
|
||||
battle_mode_text, build_battle_runtime_story_options, build_current_build_toast,
|
||||
build_status_patch,
|
||||
build_npc_gift_result_text,
|
||||
build_runtime_story_view_model,
|
||||
PendingQuestOfferContext, RuntimeStoryActionResponseParts, StoryResolution,
|
||||
add_player_currency, add_player_inventory_items, append_story_history,
|
||||
apply_equipment_loadout_to_state, battle_mode_text, build_battle_runtime_story_options,
|
||||
build_current_build_toast, build_disabled_runtime_story_option, build_npc_gift_result_text,
|
||||
build_runtime_story_option_from_story_option, build_runtime_story_view_model,
|
||||
build_static_runtime_story_option, build_status_patch, build_story_option_from_runtime_option,
|
||||
clear_encounter_only, clear_encounter_state, clone_inventory_item_with_quantity,
|
||||
current_encounter_id, current_encounter_name, current_world_type,
|
||||
ensure_inventory_action_available, ensure_json_object, equipment_slot_label,
|
||||
find_player_inventory_entry,
|
||||
format_now_rfc3339, grant_player_progression_experience, has_giftable_player_inventory,
|
||||
format_currency_text,
|
||||
increment_runtime_stat, normalize_equipped_item,
|
||||
normalize_equipment_slot_id, normalize_required_string, npc_buyback_price,
|
||||
npc_purchase_price, read_array_field, read_bool_field, read_field, read_i32_field,
|
||||
read_inventory_item_name, read_object_field, read_optional_string_field,
|
||||
find_player_inventory_entry, format_currency_text, format_now_rfc3339,
|
||||
grant_player_progression_experience, has_giftable_player_inventory, increment_runtime_stat,
|
||||
normalize_equipment_slot_id, normalize_equipped_item, normalize_required_string,
|
||||
npc_buyback_price, npc_purchase_price, read_array_field, read_bool_field, read_field,
|
||||
read_i32_field, read_inventory_item_name, read_object_field, read_optional_string_field,
|
||||
read_player_equipment_item, read_required_string_field, read_runtime_session_id,
|
||||
read_u32_field, recruit_companion_to_party, remove_player_inventory_item,
|
||||
restore_player_resource,
|
||||
resolve_action_text, resolve_battle_action, resolve_equipment_slot_for_item,
|
||||
resolve_forge_craft_action,
|
||||
resolve_forge_dismantle_action, resolve_forge_reforge_action,
|
||||
resolve_npc_gift_affinity_gain, simple_story_resolution, trade_quantity_suffix,
|
||||
resolve_current_encounter_npc_state,
|
||||
build_disabled_runtime_story_option, build_runtime_story_option_from_story_option,
|
||||
build_static_runtime_story_option, build_story_option_from_runtime_option,
|
||||
write_bool_field, write_i32_field, write_null_field, write_player_equipment_item,
|
||||
write_string_field, write_u32_field,
|
||||
read_u32_field, recruit_companion_to_party, remove_player_inventory_item, resolve_action_text,
|
||||
resolve_battle_action, resolve_current_encounter_npc_state, resolve_equipment_slot_for_item,
|
||||
resolve_forge_craft_action, resolve_forge_dismantle_action, resolve_forge_reforge_action,
|
||||
resolve_npc_gift_affinity_gain, restore_player_resource, simple_story_resolution,
|
||||
trade_quantity_suffix, write_bool_field, write_i32_field, write_null_field,
|
||||
write_player_equipment_item, write_string_field, write_u32_field,
|
||||
};
|
||||
use platform_llm::{LlmClient, LlmMessage, LlmTextRequest};
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
@@ -19,9 +19,9 @@ use module_big_fish::{
|
||||
BigFishAgentMessageKind, BigFishAgentMessageRole, BigFishAgentMessageSnapshot,
|
||||
BigFishAssetGenerateInput, BigFishAssetKind, BigFishAssetSlotSnapshot, BigFishAssetStatus,
|
||||
BigFishCreationStage, BigFishDraftCompileInput, BigFishMessageSubmitInput, BigFishPublishInput,
|
||||
BigFishRunGetInput, BigFishRunInputSubmitInput, BigFishRunProcedureResult, BigFishRunStartInput,
|
||||
BigFishRunStatus, BigFishRuntimeSnapshot, BigFishSessionCreateInput, BigFishSessionGetInput,
|
||||
BigFishSessionProcedureResult, BigFishSessionSnapshot,
|
||||
BigFishRunGetInput, BigFishRunInputSubmitInput, BigFishRunProcedureResult,
|
||||
BigFishRunStartInput, BigFishRunStatus, BigFishRuntimeSnapshot, BigFishSessionCreateInput,
|
||||
BigFishSessionGetInput, BigFishSessionProcedureResult, BigFishSessionSnapshot,
|
||||
advance_runtime_snapshot, build_asset_coverage, build_generated_asset_slot,
|
||||
build_initial_runtime_snapshot, compile_default_draft, deserialize_anchor_pack,
|
||||
deserialize_draft, deserialize_runtime_snapshot, empty_anchor_pack, infer_anchor_pack,
|
||||
@@ -41,29 +41,29 @@ use module_combat::{
|
||||
use module_custom_world::{
|
||||
CustomWorldAgentActionExecuteInput, CustomWorldAgentActionExecuteResult,
|
||||
CustomWorldAgentCardDetailGetInput, CustomWorldAgentMessageSnapshot,
|
||||
CustomWorldAgentMessageSubmitInput,
|
||||
CustomWorldAgentOperationGetInput, CustomWorldAgentOperationProcedureResult,
|
||||
CustomWorldAgentOperationSnapshot, CustomWorldAgentSessionCreateInput,
|
||||
CustomWorldAgentSessionGetInput, CustomWorldAgentSessionProcedureResult,
|
||||
CustomWorldAgentSessionSnapshot, CustomWorldDraftCardDetailResult,
|
||||
CustomWorldDraftCardDetailSectionSnapshot, CustomWorldDraftCardDetailSnapshot,
|
||||
CustomWorldDraftCardSnapshot, CustomWorldPublishBlockerSnapshot,
|
||||
CustomWorldPublishGateSnapshot, CustomWorldWorkSummarySnapshot, CustomWorldWorksListInput,
|
||||
CustomWorldWorksListResult,
|
||||
CustomWorldGalleryDetailInput, CustomWorldGalleryEntrySnapshot,
|
||||
CustomWorldGalleryListResult, CustomWorldGenerationMode, CustomWorldLibraryDetailInput,
|
||||
CustomWorldLibraryMutationResult, CustomWorldProfileListInput,
|
||||
CustomWorldProfileListResult, CustomWorldProfilePublishInput, CustomWorldProfileSnapshot,
|
||||
CustomWorldProfileUnpublishInput, CustomWorldProfileUpsertInput,
|
||||
CustomWorldPublicationStatus, CustomWorldPublishWorldInput, CustomWorldPublishWorldResult,
|
||||
CustomWorldAgentMessageSubmitInput, CustomWorldAgentOperationGetInput,
|
||||
CustomWorldAgentOperationProcedureResult, CustomWorldAgentOperationSnapshot,
|
||||
CustomWorldAgentSessionCreateInput, CustomWorldAgentSessionGetInput,
|
||||
CustomWorldAgentSessionProcedureResult, CustomWorldAgentSessionSnapshot,
|
||||
CustomWorldDraftCardDetailResult, CustomWorldDraftCardDetailSectionSnapshot,
|
||||
CustomWorldDraftCardDetailSnapshot, CustomWorldDraftCardSnapshot,
|
||||
CustomWorldGalleryDetailInput, CustomWorldGalleryEntrySnapshot, CustomWorldGalleryListResult,
|
||||
CustomWorldGenerationMode, CustomWorldLibraryDetailInput, CustomWorldLibraryMutationResult,
|
||||
CustomWorldProfileListInput, CustomWorldProfileListResult, CustomWorldProfilePublishInput,
|
||||
CustomWorldProfileSnapshot, CustomWorldProfileUnpublishInput, CustomWorldProfileUpsertInput,
|
||||
CustomWorldPublicationStatus, CustomWorldPublishBlockerSnapshot,
|
||||
CustomWorldPublishGateSnapshot, CustomWorldPublishWorldInput, CustomWorldPublishWorldResult,
|
||||
CustomWorldPublishedProfileCompileInput, CustomWorldPublishedProfileCompileResult,
|
||||
CustomWorldRoleAssetStatus, CustomWorldSessionStatus, CustomWorldThemeMode,
|
||||
CustomWorldWorkSummarySnapshot, CustomWorldWorksListInput, CustomWorldWorksListResult,
|
||||
RpgAgentDraftCardKind, RpgAgentDraftCardStatus, RpgAgentMessageKind, RpgAgentMessageRole,
|
||||
RpgAgentOperationStatus, RpgAgentOperationType, RpgAgentStage,
|
||||
build_custom_world_published_profile_compile_snapshot, validate_custom_world_agent_message_submit_input,
|
||||
build_custom_world_published_profile_compile_snapshot,
|
||||
validate_custom_world_agent_action_execute_input,
|
||||
validate_custom_world_agent_card_detail_get_input,
|
||||
validate_custom_world_agent_operation_get_input, validate_custom_world_agent_session_create_input,
|
||||
validate_custom_world_agent_message_submit_input,
|
||||
validate_custom_world_agent_operation_get_input,
|
||||
validate_custom_world_agent_session_create_input,
|
||||
validate_custom_world_agent_session_get_input, validate_custom_world_gallery_detail_input,
|
||||
validate_custom_world_library_detail_input, validate_custom_world_profile_list_input,
|
||||
validate_custom_world_profile_publish_input, validate_custom_world_profile_unpublish_input,
|
||||
@@ -105,26 +105,24 @@ use module_quest::{
|
||||
};
|
||||
use module_runtime::{
|
||||
DEFAULT_MUSIC_VOLUME, DEFAULT_PLATFORM_THEME, DEFAULT_SAVE_ARCHIVE_SUMMARY_TEXT,
|
||||
PROFILE_WALLET_LEDGER_LIST_LIMIT, SAVE_SNAPSHOT_VERSION,
|
||||
RuntimeBrowseHistoryClearInput, RuntimeBrowseHistoryListInput,
|
||||
RuntimeBrowseHistoryProcedureResult, RuntimeBrowseHistorySnapshot,
|
||||
RuntimeBrowseHistorySyncInput, RuntimeBrowseHistoryThemeMode, RuntimePlatformTheme,
|
||||
RuntimeProfileSaveArchiveListInput, RuntimeProfileSaveArchiveProcedureResult,
|
||||
RuntimeProfileSaveArchiveResumeInput, RuntimeProfileSaveArchiveSnapshot,
|
||||
RuntimeProfileDashboardGetInput, RuntimeProfileDashboardProcedureResult,
|
||||
PROFILE_WALLET_LEDGER_LIST_LIMIT, RuntimeBrowseHistoryClearInput,
|
||||
RuntimeBrowseHistoryListInput, RuntimeBrowseHistoryProcedureResult,
|
||||
RuntimeBrowseHistorySnapshot, RuntimeBrowseHistorySyncInput, RuntimeBrowseHistoryThemeMode,
|
||||
RuntimePlatformTheme, RuntimeProfileDashboardGetInput, RuntimeProfileDashboardProcedureResult,
|
||||
RuntimeProfileDashboardSnapshot, RuntimeProfilePlayStatsGetInput,
|
||||
RuntimeProfilePlayStatsProcedureResult, RuntimeProfilePlayStatsSnapshot,
|
||||
RuntimeProfilePlayedWorldSnapshot, RuntimeProfileWalletLedgerEntrySnapshot,
|
||||
RuntimeProfilePlayedWorldSnapshot, RuntimeProfileSaveArchiveListInput,
|
||||
RuntimeProfileSaveArchiveProcedureResult, RuntimeProfileSaveArchiveResumeInput,
|
||||
RuntimeProfileSaveArchiveSnapshot, RuntimeProfileWalletLedgerEntrySnapshot,
|
||||
RuntimeProfileWalletLedgerListInput, RuntimeProfileWalletLedgerProcedureResult,
|
||||
RuntimeProfileWalletLedgerSourceType, RuntimeSettingGetInput,
|
||||
RuntimeSettingProcedureResult, RuntimeSettingSnapshot, RuntimeSettingUpsertInput,
|
||||
RuntimeSnapshot, RuntimeSnapshotDeleteInput, RuntimeSnapshotGetInput,
|
||||
RuntimeSnapshotProcedureResult, RuntimeSnapshotUpsertInput,
|
||||
build_runtime_browse_history_clear_input,
|
||||
RuntimeProfileWalletLedgerSourceType, RuntimeSettingGetInput, RuntimeSettingProcedureResult,
|
||||
RuntimeSettingSnapshot, RuntimeSettingUpsertInput, RuntimeSnapshot, RuntimeSnapshotDeleteInput,
|
||||
RuntimeSnapshotGetInput, RuntimeSnapshotProcedureResult, RuntimeSnapshotUpsertInput,
|
||||
SAVE_SNAPSHOT_VERSION, build_runtime_browse_history_clear_input,
|
||||
build_runtime_browse_history_list_input, build_runtime_profile_dashboard_get_input,
|
||||
build_runtime_profile_play_stats_get_input, build_runtime_profile_wallet_ledger_list_input,
|
||||
build_runtime_profile_save_archive_list_input,
|
||||
build_runtime_profile_save_archive_resume_input, build_runtime_setting_get_input,
|
||||
build_runtime_profile_play_stats_get_input, build_runtime_profile_save_archive_list_input,
|
||||
build_runtime_profile_save_archive_resume_input,
|
||||
build_runtime_profile_wallet_ledger_list_input, build_runtime_setting_get_input,
|
||||
build_runtime_setting_upsert_input, build_runtime_snapshot_delete_input,
|
||||
build_runtime_snapshot_get_input, build_runtime_snapshot_upsert_input,
|
||||
prepare_runtime_browse_history_entries,
|
||||
@@ -142,13 +140,29 @@ use module_story::{
|
||||
build_story_session_snapshot, build_story_started_event, validate_story_continue_input,
|
||||
validate_story_session_input, validate_story_session_state_input,
|
||||
};
|
||||
use shared_kernel::format_timestamp_micros;
|
||||
use serde_json::{Map as JsonMap, Value as JsonValue, json};
|
||||
use shared_kernel::format_timestamp_micros;
|
||||
use spacetimedb::{ProcedureContext, ReducerContext, SpacetimeType, Table, Timestamp};
|
||||
|
||||
mod puzzle;
|
||||
|
||||
// 这层输入只服务 NPC 开战编排;普通聊天、援手、招募继续走已有 resolve_npc_interaction 接口。
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct ResolveNpcBattleInteractionInput {
|
||||
pub npc_interaction: ResolveNpcInteractionInput,
|
||||
pub story_session_id: String,
|
||||
pub actor_user_id: String,
|
||||
pub battle_state_id: Option<String>,
|
||||
pub player_hp: i32,
|
||||
pub player_max_hp: i32,
|
||||
pub player_mana: i32,
|
||||
pub player_max_mana: i32,
|
||||
pub target_hp: i32,
|
||||
pub target_max_hp: i32,
|
||||
pub experience_reward: u32,
|
||||
pub reward_items: Vec<RuntimeItemRewardItemSnapshot>,
|
||||
}
|
||||
|
||||
// 输出同时返回 NPC 交互结果与 battle_state 快照,避免 Axum 再回头读取 private table。
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct NpcBattleInteractionResult {
|
||||
@@ -2023,8 +2037,9 @@ fn submit_big_fish_message_tx(
|
||||
});
|
||||
|
||||
let anchor_pack = infer_anchor_pack(&session.seed_text, Some(&input.user_message_text));
|
||||
let assistant_text = "我已经把这版方向收束成 4 个高杠杆锚点,可以继续细化,也可以直接编译第一版玩法草稿。"
|
||||
.to_string();
|
||||
let assistant_text =
|
||||
"我已经把这版方向收束成 4 个高杠杆锚点,可以继续细化,也可以直接编译第一版玩法草稿。"
|
||||
.to_string();
|
||||
ctx.db.big_fish_agent_message().insert(BigFishAgentMessage {
|
||||
message_id: input.assistant_message_id,
|
||||
session_id: input.session_id.clone(),
|
||||
@@ -2208,9 +2223,15 @@ fn publish_big_fish_game_tx(
|
||||
.as_deref()
|
||||
.ok_or_else(|| "big_fish.draft 尚未编译".to_string())
|
||||
.and_then(|value| deserialize_draft(value).map_err(|error| error.to_string()))?;
|
||||
let coverage = build_asset_coverage(Some(&draft), &list_big_fish_asset_slots(ctx, &session.session_id));
|
||||
let coverage = build_asset_coverage(
|
||||
Some(&draft),
|
||||
&list_big_fish_asset_slots(ctx, &session.session_id),
|
||||
);
|
||||
if !coverage.publish_ready {
|
||||
return Err(format!("big_fish 发布校验未通过:{}", coverage.blockers.join(";")));
|
||||
return Err(format!(
|
||||
"big_fish 发布校验未通过:{}",
|
||||
coverage.blockers.join(";")
|
||||
));
|
||||
}
|
||||
|
||||
let published_at = Timestamp::from_micros_since_unix_epoch(input.published_at_micros);
|
||||
@@ -4217,18 +4238,16 @@ fn list_custom_world_work_snapshots(
|
||||
|
||||
let mut items = Vec::new();
|
||||
|
||||
for session in ctx
|
||||
.db
|
||||
.custom_world_agent_session()
|
||||
.iter()
|
||||
.filter(|row| row.owner_user_id == input.owner_user_id && row.stage != RpgAgentStage::Published)
|
||||
{
|
||||
for session in ctx.db.custom_world_agent_session().iter().filter(|row| {
|
||||
row.owner_user_id == input.owner_user_id && row.stage != RpgAgentStage::Published
|
||||
}) {
|
||||
let gate = build_custom_world_publish_gate_from_session(&session);
|
||||
let draft_profile = parse_optional_session_object(session.draft_profile_json.as_deref());
|
||||
let title = resolve_session_work_title(&session, draft_profile.as_ref());
|
||||
let summary = resolve_session_work_summary(&session, draft_profile.as_ref());
|
||||
let stage_label = Some(resolve_rpg_agent_stage_label(session.stage).to_string());
|
||||
let subtitle = resolve_session_work_subtitle(draft_profile.as_ref(), stage_label.as_deref());
|
||||
let subtitle =
|
||||
resolve_session_work_subtitle(draft_profile.as_ref(), stage_label.as_deref());
|
||||
let (playable_npc_count, landmark_count) =
|
||||
resolve_session_work_counts(ctx, &session, draft_profile.as_ref());
|
||||
|
||||
@@ -4301,8 +4320,16 @@ fn list_custom_world_work_snapshots(
|
||||
.updated_at_micros
|
||||
.cmp(&left.updated_at_micros)
|
||||
.then_with(|| {
|
||||
let left_rank = if left.source_type == "agent_session" { 0 } else { 1 };
|
||||
let right_rank = if right.source_type == "agent_session" { 0 } else { 1 };
|
||||
let left_rank = if left.source_type == "agent_session" {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
};
|
||||
let right_rank = if right.source_type == "agent_session" {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
};
|
||||
left_rank.cmp(&right_rank)
|
||||
})
|
||||
.then(left.work_id.cmp(&right.work_id))
|
||||
@@ -4363,7 +4390,9 @@ fn execute_custom_world_agent_action_tx(
|
||||
match input.action.trim() {
|
||||
"draft_foundation" => execute_draft_foundation_action(ctx, &session, &input, &payload),
|
||||
"update_draft_card" => execute_update_draft_card_action(ctx, &session, &input, &payload),
|
||||
"sync_result_profile" => execute_sync_result_profile_action(ctx, &session, &input, &payload),
|
||||
"sync_result_profile" => {
|
||||
execute_sync_result_profile_action(ctx, &session, &input, &payload)
|
||||
}
|
||||
"publish_world" => execute_publish_world_action(ctx, &session, &input, &payload),
|
||||
"revert_checkpoint" => execute_revert_checkpoint_action(ctx, &session, &input, &payload),
|
||||
"generate_characters"
|
||||
@@ -4388,18 +4417,19 @@ fn execute_draft_foundation_action(
|
||||
}
|
||||
|
||||
let updated_at = input.submitted_at_micros;
|
||||
let draft_profile = if let Some(profile) = payload.get("draftProfile").and_then(JsonValue::as_object) {
|
||||
profile.clone()
|
||||
} else if let Some(existing) = parse_optional_session_object(session.draft_profile_json.as_deref()) {
|
||||
ensure_minimal_draft_profile(existing, &session.seed_text)
|
||||
} else {
|
||||
build_minimal_draft_profile_from_seed(&session.seed_text)
|
||||
};
|
||||
let draft_profile =
|
||||
if let Some(profile) = payload.get("draftProfile").and_then(JsonValue::as_object) {
|
||||
profile.clone()
|
||||
} else if let Some(existing) =
|
||||
parse_optional_session_object(session.draft_profile_json.as_deref())
|
||||
{
|
||||
ensure_minimal_draft_profile(existing, &session.seed_text)
|
||||
} else {
|
||||
build_minimal_draft_profile_from_seed(&session.seed_text)
|
||||
};
|
||||
|
||||
let draft_profile_json =
|
||||
serde_json::to_string(&JsonValue::Object(draft_profile.clone())).map_err(|error| {
|
||||
format!("draft_foundation 无法序列化 draft_profile_json: {error}")
|
||||
})?;
|
||||
let draft_profile_json = serde_json::to_string(&JsonValue::Object(draft_profile.clone()))
|
||||
.map_err(|error| format!("draft_foundation 无法序列化 draft_profile_json: {error}"))?;
|
||||
let gate = summarize_publish_gate_from_json(
|
||||
&input.session_id,
|
||||
RpgAgentStage::ObjectRefining,
|
||||
@@ -4412,8 +4442,12 @@ fn execute_draft_foundation_action(
|
||||
progress_percent: Some(100),
|
||||
stage: Some(RpgAgentStage::ObjectRefining),
|
||||
draft_profile_json: Some(Some(draft_profile_json.clone())),
|
||||
last_assistant_reply: Some(Some("世界底稿已整理完成,接下来可以继续细化卡片和发布预览。".to_string())),
|
||||
publish_gate_json: Some(Some(serialize_json_value(&publish_gate_to_json_value(&gate))?)),
|
||||
last_assistant_reply: Some(Some(
|
||||
"世界底稿已整理完成,接下来可以继续细化卡片和发布预览。".to_string(),
|
||||
)),
|
||||
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,
|
||||
@@ -4460,7 +4494,8 @@ fn execute_update_draft_card_action(
|
||||
) -> Result<CustomWorldAgentOperationSnapshot, String> {
|
||||
ensure_refining_stage(session.stage, "update_draft_card")?;
|
||||
|
||||
let card_id = read_required_payload_text(payload, "cardId", "update_draft_card requires cardId")?;
|
||||
let card_id =
|
||||
read_required_payload_text(payload, "cardId", "update_draft_card requires cardId")?;
|
||||
let card = ctx
|
||||
.db
|
||||
.custom_world_draft_card()
|
||||
@@ -4476,7 +4511,8 @@ fn execute_update_draft_card_action(
|
||||
return Err("update_draft_card requires sections".to_string());
|
||||
}
|
||||
|
||||
let mut detail_object = parse_optional_session_object(card.detail_payload_json.as_deref()).unwrap_or_default();
|
||||
let mut detail_object =
|
||||
parse_optional_session_object(card.detail_payload_json.as_deref()).unwrap_or_default();
|
||||
let mut detail_sections = detail_object
|
||||
.get("sections")
|
||||
.and_then(JsonValue::as_array)
|
||||
@@ -4520,27 +4556,36 @@ fn execute_update_draft_card_action(
|
||||
}
|
||||
|
||||
detail_object.insert("id".to_string(), JsonValue::String(card.card_id.clone()));
|
||||
detail_object.insert("kind".to_string(), JsonValue::String(card.kind.as_str().to_string()));
|
||||
detail_object.insert(
|
||||
"kind".to_string(),
|
||||
JsonValue::String(card.kind.as_str().to_string()),
|
||||
);
|
||||
detail_object.insert("title".to_string(), JsonValue::String(card.title.clone()));
|
||||
detail_object.insert("sections".to_string(), JsonValue::Array(detail_sections.clone()));
|
||||
detail_object.insert(
|
||||
"sections".to_string(),
|
||||
JsonValue::Array(detail_sections.clone()),
|
||||
);
|
||||
detail_object.insert(
|
||||
"linkedIds".to_string(),
|
||||
serde_json::from_str::<JsonValue>(&card.linked_ids_json).unwrap_or_else(|_| JsonValue::Array(Vec::new())),
|
||||
serde_json::from_str::<JsonValue>(&card.linked_ids_json)
|
||||
.unwrap_or_else(|_| JsonValue::Array(Vec::new())),
|
||||
);
|
||||
detail_object.insert("locked".to_string(), JsonValue::Bool(false));
|
||||
detail_object.insert("editable".to_string(), JsonValue::Bool(false));
|
||||
detail_object.insert("editableSectionIds".to_string(), JsonValue::Array(Vec::new()));
|
||||
detail_object.insert(
|
||||
"editableSectionIds".to_string(),
|
||||
JsonValue::Array(Vec::new()),
|
||||
);
|
||||
detail_object.insert("warningMessages".to_string(), JsonValue::Array(Vec::new()));
|
||||
|
||||
let updated_title = extract_detail_section_value(&detail_sections, "title").unwrap_or_else(|| card.title.clone());
|
||||
let updated_subtitle =
|
||||
extract_detail_section_value(&detail_sections, "subtitle").unwrap_or_else(|| card.subtitle.clone());
|
||||
let updated_summary =
|
||||
extract_detail_section_value(&detail_sections, "summary").unwrap_or_else(|| card.summary.clone());
|
||||
let detail_payload_json =
|
||||
serde_json::to_string(&JsonValue::Object(detail_object)).map_err(|error| {
|
||||
format!("update_draft_card 无法序列化 detail_payload_json: {error}")
|
||||
})?;
|
||||
let updated_title = extract_detail_section_value(&detail_sections, "title")
|
||||
.unwrap_or_else(|| card.title.clone());
|
||||
let updated_subtitle = extract_detail_section_value(&detail_sections, "subtitle")
|
||||
.unwrap_or_else(|| card.subtitle.clone());
|
||||
let updated_summary = extract_detail_section_value(&detail_sections, "summary")
|
||||
.unwrap_or_else(|| card.summary.clone());
|
||||
let detail_payload_json = serde_json::to_string(&JsonValue::Object(detail_object))
|
||||
.map_err(|error| format!("update_draft_card 无法序列化 detail_payload_json: {error}"))?;
|
||||
|
||||
replace_custom_world_draft_card(
|
||||
ctx,
|
||||
@@ -4563,7 +4608,14 @@ fn execute_update_draft_card_action(
|
||||
},
|
||||
);
|
||||
|
||||
let next_session = sync_session_draft_profile_from_card_update(session, &card, &updated_title, &updated_subtitle, &updated_summary, input.submitted_at_micros)?;
|
||||
let next_session = sync_session_draft_profile_from_card_update(
|
||||
session,
|
||||
&card,
|
||||
&updated_title,
|
||||
&updated_subtitle,
|
||||
&updated_summary,
|
||||
input.submitted_at_micros,
|
||||
)?;
|
||||
replace_custom_world_agent_session(ctx, session, next_session);
|
||||
|
||||
append_custom_world_action_result_message(
|
||||
@@ -4610,9 +4662,13 @@ fn execute_sync_result_profile_action(
|
||||
let next_session = rebuild_custom_world_agent_session_row(
|
||||
session,
|
||||
CustomWorldAgentSessionPatch {
|
||||
draft_profile_json: Some(Some(serialize_json_value(&JsonValue::Object(draft_profile.clone()))?)),
|
||||
draft_profile_json: Some(Some(serialize_json_value(&JsonValue::Object(
|
||||
draft_profile.clone(),
|
||||
))?)),
|
||||
last_assistant_reply: Some(Some("结果页草稿已同步回当前会话。".to_string())),
|
||||
publish_gate_json: Some(Some(serialize_json_value(&publish_gate_to_json_value(&gate))?)),
|
||||
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,
|
||||
@@ -4658,12 +4714,13 @@ fn execute_publish_world_action(
|
||||
) -> Result<CustomWorldAgentOperationSnapshot, String> {
|
||||
ensure_publishable_stage(session.stage, "publish_world")?;
|
||||
|
||||
let draft_profile = if let Some(explicit) = payload.get("draftProfile").and_then(JsonValue::as_object) {
|
||||
explicit.clone()
|
||||
} else {
|
||||
parse_optional_session_object(session.draft_profile_json.as_deref())
|
||||
.ok_or_else(|| "publish_world requires draft_profile_json".to_string())?
|
||||
};
|
||||
let draft_profile =
|
||||
if let Some(explicit) = payload.get("draftProfile").and_then(JsonValue::as_object) {
|
||||
explicit.clone()
|
||||
} else {
|
||||
parse_optional_session_object(session.draft_profile_json.as_deref())
|
||||
.ok_or_else(|| "publish_world requires draft_profile_json".to_string())?
|
||||
};
|
||||
let gate = summarize_publish_gate_from_json(
|
||||
&session.session_id,
|
||||
session.stage,
|
||||
@@ -4723,7 +4780,10 @@ fn execute_publish_world_action(
|
||||
&session.session_id,
|
||||
RpgAgentOperationType::PublishWorld,
|
||||
"世界已发布",
|
||||
&format!("正式世界档案已写入作品库:{}。", publish_result.1.profile_id),
|
||||
&format!(
|
||||
"正式世界档案已写入作品库:{}。",
|
||||
publish_result.1.profile_id
|
||||
),
|
||||
input.submitted_at_micros,
|
||||
);
|
||||
|
||||
@@ -4797,9 +4857,15 @@ fn execute_revert_checkpoint_action(
|
||||
.map(|value| serialize_json_value(&JsonValue::Object(value.clone())))
|
||||
.transpose()?,
|
||||
),
|
||||
last_assistant_reply: Some(Some("已恢复到所选 checkpoint 的世界草稿状态。".to_string())),
|
||||
quality_findings_json: Some(serialize_json_value(&JsonValue::Array(restored_quality_findings))?),
|
||||
publish_gate_json: Some(Some(serialize_json_value(&publish_gate_to_json_value(&gate))?)),
|
||||
last_assistant_reply: Some(Some(
|
||||
"已恢复到所选 checkpoint 的世界草稿状态。".to_string(),
|
||||
)),
|
||||
quality_findings_json: Some(serialize_json_value(&JsonValue::Array(
|
||||
restored_quality_findings,
|
||||
))?),
|
||||
publish_gate_json: Some(Some(serialize_json_value(&publish_gate_to_json_value(
|
||||
&gate,
|
||||
))?)),
|
||||
result_preview_json: Some(build_result_preview_json(
|
||||
restored_draft_profile.as_ref(),
|
||||
&gate,
|
||||
@@ -4850,7 +4916,10 @@ fn execute_placeholder_custom_world_action(
|
||||
ctx,
|
||||
&session.session_id,
|
||||
&input.operation_id,
|
||||
&format!("动作 {} 已接入最小兼容占位,后续会继续补真实编排。", input.action),
|
||||
&format!(
|
||||
"动作 {} 已接入最小兼容占位,后续会继续补真实编排。",
|
||||
input.action
|
||||
),
|
||||
input.submitted_at_micros,
|
||||
);
|
||||
let operation = build_and_insert_custom_world_operation(
|
||||
@@ -4931,7 +5000,8 @@ fn summarize_publish_gate_from_json(
|
||||
blockers.push(CustomWorldPublishBlockerSnapshot {
|
||||
blocker_id: "publish_missing_player_premise".to_string(),
|
||||
code: "publish_missing_player_premise".to_string(),
|
||||
message: "当前世界缺少玩家身份与切入前提,发布前需要先补齐玩家 premise。".to_string(),
|
||||
message: "当前世界缺少玩家身份与切入前提,发布前需要先补齐玩家 premise。"
|
||||
.to_string(),
|
||||
});
|
||||
}
|
||||
if !json_array_has_non_empty_text(profile.get("coreConflicts")) {
|
||||
@@ -5061,8 +5131,10 @@ fn build_supported_actions_json(
|
||||
let has_checkpoint = checkpoints
|
||||
.iter()
|
||||
.any(|entry| entry.get("snapshot").is_some());
|
||||
let draft_refining_enabled =
|
||||
matches!(stage, RpgAgentStage::ObjectRefining | RpgAgentStage::VisualRefining);
|
||||
let draft_refining_enabled = matches!(
|
||||
stage,
|
||||
RpgAgentStage::ObjectRefining | RpgAgentStage::VisualRefining
|
||||
);
|
||||
let long_tail_enabled = matches!(
|
||||
stage,
|
||||
RpgAgentStage::ObjectRefining
|
||||
@@ -5181,8 +5253,10 @@ fn build_custom_world_draft_card_detail_snapshot(
|
||||
card: &CustomWorldDraftCard,
|
||||
) -> Result<CustomWorldDraftCardDetailSnapshot, String> {
|
||||
if let Some(detail_payload_json) = card.detail_payload_json.as_deref() {
|
||||
let detail_value = serde_json::from_str::<JsonValue>(detail_payload_json)
|
||||
.map_err(|error| format!("custom_world_draft_card.detail_payload_json 非法: {error}"))?;
|
||||
let detail_value =
|
||||
serde_json::from_str::<JsonValue>(detail_payload_json).map_err(|error| {
|
||||
format!("custom_world_draft_card.detail_payload_json 非法: {error}")
|
||||
})?;
|
||||
if let Some(object) = detail_value.as_object() {
|
||||
let sections = object
|
||||
.get("sections")
|
||||
@@ -5220,8 +5294,14 @@ fn build_custom_world_draft_card_detail_snapshot(
|
||||
.to_string(),
|
||||
sections,
|
||||
linked_ids_json: card.linked_ids_json.clone(),
|
||||
locked: object.get("locked").and_then(JsonValue::as_bool).unwrap_or(false),
|
||||
editable: object.get("editable").and_then(JsonValue::as_bool).unwrap_or(false),
|
||||
locked: object
|
||||
.get("locked")
|
||||
.and_then(JsonValue::as_bool)
|
||||
.unwrap_or(false),
|
||||
editable: object
|
||||
.get("editable")
|
||||
.and_then(JsonValue::as_bool)
|
||||
.unwrap_or(false),
|
||||
editable_section_ids_json: serialize_json_value(
|
||||
object
|
||||
.get("editableSectionIds")
|
||||
@@ -5253,7 +5333,9 @@ fn build_custom_world_draft_card_detail_snapshot(
|
||||
})
|
||||
}
|
||||
|
||||
fn build_fallback_card_sections(card: &CustomWorldDraftCard) -> Vec<CustomWorldDraftCardDetailSectionSnapshot> {
|
||||
fn build_fallback_card_sections(
|
||||
card: &CustomWorldDraftCard,
|
||||
) -> Vec<CustomWorldDraftCardDetailSectionSnapshot> {
|
||||
vec![
|
||||
CustomWorldDraftCardDetailSectionSnapshot {
|
||||
section_id: "title".to_string(),
|
||||
@@ -5297,7 +5379,9 @@ fn rebuild_custom_world_agent_session_row(
|
||||
current_turn: current.current_turn,
|
||||
progress_percent: patch.progress_percent.unwrap_or(current.progress_percent),
|
||||
stage: patch.stage.unwrap_or(current.stage),
|
||||
focus_card_id: patch.focus_card_id.unwrap_or_else(|| current.focus_card_id.clone()),
|
||||
focus_card_id: patch
|
||||
.focus_card_id
|
||||
.unwrap_or_else(|| current.focus_card_id.clone()),
|
||||
anchor_content_json: patch
|
||||
.anchor_content_json
|
||||
.unwrap_or_else(|| current.anchor_content_json.clone()),
|
||||
@@ -5307,8 +5391,12 @@ fn rebuild_custom_world_agent_session_row(
|
||||
creator_intent_readiness_json: patch
|
||||
.creator_intent_readiness_json
|
||||
.unwrap_or_else(|| current.creator_intent_readiness_json.clone()),
|
||||
anchor_pack_json: patch.anchor_pack_json.unwrap_or_else(|| current.anchor_pack_json.clone()),
|
||||
lock_state_json: patch.lock_state_json.unwrap_or_else(|| current.lock_state_json.clone()),
|
||||
anchor_pack_json: patch
|
||||
.anchor_pack_json
|
||||
.unwrap_or_else(|| current.anchor_pack_json.clone()),
|
||||
lock_state_json: patch
|
||||
.lock_state_json
|
||||
.unwrap_or_else(|| current.lock_state_json.clone()),
|
||||
draft_profile_json: patch
|
||||
.draft_profile_json
|
||||
.unwrap_or_else(|| current.draft_profile_json.clone()),
|
||||
@@ -5460,7 +5548,8 @@ fn upsert_world_foundation_card(
|
||||
status: RpgAgentDraftCardStatus::Confirmed,
|
||||
title: read_optional_text_field(draft_profile, &["name", "title"])
|
||||
.unwrap_or_else(|| "世界底稿".to_string()),
|
||||
subtitle: read_optional_text_field(draft_profile, &["subtitle"]).unwrap_or_default(),
|
||||
subtitle: read_optional_text_field(draft_profile, &["subtitle"])
|
||||
.unwrap_or_default(),
|
||||
summary: read_optional_text_field(draft_profile, &["summary"])
|
||||
.unwrap_or_else(|| "第一版世界底稿已生成。".to_string()),
|
||||
linked_ids_json: "[]".to_string(),
|
||||
@@ -5473,24 +5562,27 @@ fn upsert_world_foundation_card(
|
||||
},
|
||||
);
|
||||
} else {
|
||||
ctx.db.custom_world_draft_card().insert(CustomWorldDraftCard {
|
||||
card_id,
|
||||
session_id: session_id.to_string(),
|
||||
kind: RpgAgentDraftCardKind::World,
|
||||
status: RpgAgentDraftCardStatus::Confirmed,
|
||||
title: read_optional_text_field(draft_profile, &["name", "title"])
|
||||
.unwrap_or_else(|| "世界底稿".to_string()),
|
||||
subtitle: read_optional_text_field(draft_profile, &["subtitle"]).unwrap_or_default(),
|
||||
summary: read_optional_text_field(draft_profile, &["summary"])
|
||||
.unwrap_or_else(|| "第一版世界底稿已生成。".to_string()),
|
||||
linked_ids_json: "[]".to_string(),
|
||||
warning_count: 0,
|
||||
asset_status: None,
|
||||
asset_status_label: None,
|
||||
detail_payload_json: Some(detail_payload_json),
|
||||
created_at: Timestamp::from_micros_since_unix_epoch(updated_at_micros),
|
||||
updated_at: Timestamp::from_micros_since_unix_epoch(updated_at_micros),
|
||||
});
|
||||
ctx.db
|
||||
.custom_world_draft_card()
|
||||
.insert(CustomWorldDraftCard {
|
||||
card_id,
|
||||
session_id: session_id.to_string(),
|
||||
kind: RpgAgentDraftCardKind::World,
|
||||
status: RpgAgentDraftCardStatus::Confirmed,
|
||||
title: read_optional_text_field(draft_profile, &["name", "title"])
|
||||
.unwrap_or_else(|| "世界底稿".to_string()),
|
||||
subtitle: read_optional_text_field(draft_profile, &["subtitle"])
|
||||
.unwrap_or_default(),
|
||||
summary: read_optional_text_field(draft_profile, &["summary"])
|
||||
.unwrap_or_else(|| "第一版世界底稿已生成。".to_string()),
|
||||
linked_ids_json: "[]".to_string(),
|
||||
warning_count: 0,
|
||||
asset_status: None,
|
||||
asset_status_label: None,
|
||||
detail_payload_json: Some(detail_payload_json),
|
||||
created_at: Timestamp::from_micros_since_unix_epoch(updated_at_micros),
|
||||
updated_at: Timestamp::from_micros_since_unix_epoch(updated_at_micros),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -5507,7 +5599,10 @@ fn sync_session_draft_profile_from_card_update(
|
||||
let mut draft_profile = parse_optional_session_object(session.draft_profile_json.as_deref())
|
||||
.unwrap_or_else(|| build_minimal_draft_profile_from_seed(&session.seed_text));
|
||||
if card.kind == RpgAgentDraftCardKind::World {
|
||||
draft_profile.insert("name".to_string(), JsonValue::String(updated_title.to_string()));
|
||||
draft_profile.insert(
|
||||
"name".to_string(),
|
||||
JsonValue::String(updated_title.to_string()),
|
||||
);
|
||||
draft_profile.insert(
|
||||
"subtitle".to_string(),
|
||||
JsonValue::String(updated_subtitle.to_string()),
|
||||
@@ -5527,8 +5622,12 @@ fn sync_session_draft_profile_from_card_update(
|
||||
rebuild_custom_world_agent_session_row(
|
||||
session,
|
||||
CustomWorldAgentSessionPatch {
|
||||
draft_profile_json: Some(Some(serialize_json_value(&JsonValue::Object(draft_profile.clone()))?)),
|
||||
publish_gate_json: Some(Some(serialize_json_value(&publish_gate_to_json_value(&gate))?)),
|
||||
draft_profile_json: Some(Some(serialize_json_value(&JsonValue::Object(
|
||||
draft_profile.clone(),
|
||||
))?)),
|
||||
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,
|
||||
@@ -5543,7 +5642,10 @@ fn sync_session_draft_profile_from_card_update(
|
||||
}
|
||||
|
||||
fn ensure_refining_stage(stage: RpgAgentStage, action: &str) -> Result<(), String> {
|
||||
if matches!(stage, RpgAgentStage::ObjectRefining | RpgAgentStage::VisualRefining) {
|
||||
if matches!(
|
||||
stage,
|
||||
RpgAgentStage::ObjectRefining | RpgAgentStage::VisualRefining
|
||||
) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!(
|
||||
@@ -5652,10 +5754,7 @@ fn read_required_payload_text(
|
||||
.ok_or_else(|| error_message.to_string())
|
||||
}
|
||||
|
||||
fn read_optional_text_field(
|
||||
object: &JsonMap<String, JsonValue>,
|
||||
keys: &[&str],
|
||||
) -> Option<String> {
|
||||
fn read_optional_text_field(object: &JsonMap<String, JsonValue>, keys: &[&str]) -> Option<String> {
|
||||
for key in keys {
|
||||
let mut current = JsonValue::Object(object.clone());
|
||||
let mut found = true;
|
||||
@@ -5668,7 +5767,11 @@ fn read_optional_text_field(
|
||||
}
|
||||
}
|
||||
if found {
|
||||
if let Some(value) = current.as_str().map(str::trim).filter(|value| !value.is_empty()) {
|
||||
if let Some(value) = current
|
||||
.as_str()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
return Some(value.to_string());
|
||||
}
|
||||
}
|
||||
@@ -5860,21 +5963,28 @@ fn append_checkpoint_json(current: &str, checkpoint: &JsonValue) -> Result<Strin
|
||||
fn extract_detail_section_value(sections: &[JsonValue], target_id: &str) -> Option<String> {
|
||||
sections.iter().find_map(|entry| {
|
||||
let object = entry.as_object()?;
|
||||
(object.get("id").and_then(JsonValue::as_str) == Some(target_id))
|
||||
.then(|| {
|
||||
object
|
||||
.get("value")
|
||||
.and_then(JsonValue::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_string()
|
||||
})
|
||||
(object.get("id").and_then(JsonValue::as_str) == Some(target_id)).then(|| {
|
||||
object
|
||||
.get("value")
|
||||
.and_then(JsonValue::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_string()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn json_array_has_non_empty_text(value: Option<&JsonValue>) -> bool {
|
||||
value
|
||||
.and_then(JsonValue::as_array)
|
||||
.map(|entries| entries.iter().any(|entry| entry.as_str().map(str::trim).filter(|text| !text.is_empty()).is_some()))
|
||||
.map(|entries| {
|
||||
entries.iter().any(|entry| {
|
||||
entry
|
||||
.as_str()
|
||||
.map(str::trim)
|
||||
.filter(|text| !text.is_empty())
|
||||
.is_some()
|
||||
})
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
@@ -6054,12 +6164,14 @@ fn build_custom_world_agent_session_snapshot(
|
||||
recommended_replies_json: row.recommended_replies_json.clone(),
|
||||
asset_coverage_json: row.asset_coverage_json.clone(),
|
||||
checkpoints_json: row.checkpoints_json.clone(),
|
||||
supported_actions_json: serialize_json_value(&JsonValue::Array(build_supported_actions_json(
|
||||
row.stage,
|
||||
row.progress_percent,
|
||||
&build_custom_world_publish_gate_from_session(row),
|
||||
&parse_json_array_or_empty(&row.checkpoints_json),
|
||||
)))
|
||||
supported_actions_json: serialize_json_value(&JsonValue::Array(
|
||||
build_supported_actions_json(
|
||||
row.stage,
|
||||
row.progress_percent,
|
||||
&build_custom_world_publish_gate_from_session(row),
|
||||
&parse_json_array_or_empty(&row.checkpoints_json),
|
||||
),
|
||||
))
|
||||
.unwrap_or_else(|_| "[]".to_string()),
|
||||
messages,
|
||||
draft_cards,
|
||||
@@ -7524,7 +7636,10 @@ fn upsert_runtime_snapshot_record(
|
||||
|
||||
let snapshot = match ctx.db.runtime_snapshot().user_id().find(&prepared.user_id) {
|
||||
Some(existing) => {
|
||||
ctx.db.runtime_snapshot().user_id().delete(&existing.user_id);
|
||||
ctx.db
|
||||
.runtime_snapshot()
|
||||
.user_id()
|
||||
.delete(&existing.user_id);
|
||||
ctx.db.runtime_snapshot().insert(RuntimeSnapshotRow {
|
||||
user_id: existing.user_id.clone(),
|
||||
version: SAVE_SNAPSHOT_VERSION,
|
||||
@@ -7591,7 +7706,10 @@ fn delete_runtime_snapshot_record(
|
||||
.find(&validated_input.user_id);
|
||||
if let Some(existing) = existing {
|
||||
let snapshot = build_runtime_snapshot_from_row(&existing);
|
||||
ctx.db.runtime_snapshot().user_id().delete(&existing.user_id);
|
||||
ctx.db
|
||||
.runtime_snapshot()
|
||||
.user_id()
|
||||
.delete(&existing.user_id);
|
||||
return Ok(Some(snapshot));
|
||||
}
|
||||
|
||||
@@ -7602,8 +7720,8 @@ fn list_profile_save_archive_rows(
|
||||
ctx: &ReducerContext,
|
||||
input: RuntimeProfileSaveArchiveListInput,
|
||||
) -> Result<Vec<RuntimeProfileSaveArchiveSnapshot>, String> {
|
||||
let validated_input =
|
||||
build_runtime_profile_save_archive_list_input(input.user_id).map_err(|error| error.to_string())?;
|
||||
let validated_input = build_runtime_profile_save_archive_list_input(input.user_id)
|
||||
.map_err(|error| error.to_string())?;
|
||||
|
||||
let mut entries = ctx
|
||||
.db
|
||||
@@ -7627,13 +7745,16 @@ fn resume_profile_save_archive_record(
|
||||
ctx: &ReducerContext,
|
||||
input: RuntimeProfileSaveArchiveResumeInput,
|
||||
) -> Result<(RuntimeProfileSaveArchiveSnapshot, RuntimeSnapshot), String> {
|
||||
let validated_input = build_runtime_profile_save_archive_resume_input(input.user_id, input.world_key)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let validated_input =
|
||||
build_runtime_profile_save_archive_resume_input(input.user_id, input.world_key)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let archive = ctx
|
||||
.db
|
||||
.profile_save_archive()
|
||||
.iter()
|
||||
.find(|row| row.user_id == validated_input.user_id && row.world_key == validated_input.world_key)
|
||||
.find(|row| {
|
||||
row.user_id == validated_input.user_id && row.world_key == validated_input.world_key
|
||||
})
|
||||
.ok_or_else(|| "profile_save_archive 对应 world_key 不存在".to_string())?;
|
||||
|
||||
let existing_snapshot = ctx
|
||||
@@ -7647,7 +7768,10 @@ fn resume_profile_save_archive_record(
|
||||
.unwrap_or(archive.saved_at);
|
||||
|
||||
if let Some(existing) = existing_snapshot {
|
||||
ctx.db.runtime_snapshot().user_id().delete(&existing.user_id);
|
||||
ctx.db
|
||||
.runtime_snapshot()
|
||||
.user_id()
|
||||
.delete(&existing.user_id);
|
||||
}
|
||||
|
||||
ctx.db.runtime_snapshot().insert(RuntimeSnapshotRow {
|
||||
@@ -7701,21 +7825,23 @@ fn sync_profile_dashboard_from_snapshot(
|
||||
.profile_dashboard_state()
|
||||
.user_id()
|
||||
.find(&snapshot.user_id);
|
||||
let previous_wallet_balance = current_state.as_ref().map(|row| row.wallet_balance).unwrap_or(0);
|
||||
let previous_wallet_balance = current_state
|
||||
.as_ref()
|
||||
.map(|row| row.wallet_balance)
|
||||
.unwrap_or(0);
|
||||
let previous_total_play_time_ms = current_state
|
||||
.as_ref()
|
||||
.map(|row| row.total_play_time_ms)
|
||||
.unwrap_or(0);
|
||||
let next_wallet_balance = read_non_negative_u64(game_state.and_then(|state| state.get("playerCurrency")));
|
||||
let next_wallet_balance =
|
||||
read_non_negative_u64(game_state.and_then(|state| state.get("playerCurrency")));
|
||||
let mut next_total_play_time_ms = previous_total_play_time_ms;
|
||||
|
||||
if next_wallet_balance != previous_wallet_balance {
|
||||
ctx.db.profile_wallet_ledger().insert(ProfileWalletLedger {
|
||||
wallet_ledger_id: format!(
|
||||
"{}:{}:{}",
|
||||
snapshot.user_id,
|
||||
snapshot.saved_at_micros,
|
||||
next_wallet_balance
|
||||
snapshot.user_id, snapshot.saved_at_micros, next_wallet_balance
|
||||
),
|
||||
user_id: snapshot.user_id.clone(),
|
||||
amount_delta: next_wallet_balance as i64 - previous_wallet_balance as i64,
|
||||
@@ -7733,12 +7859,17 @@ fn sync_profile_dashboard_from_snapshot(
|
||||
.and_then(|stats| stats.get("playTimeMs")),
|
||||
);
|
||||
let played_world_id = format!("{}:{}", snapshot.user_id, world_meta.world_key);
|
||||
let existing = ctx.db.profile_played_world().played_world_id().find(&played_world_id);
|
||||
let existing = ctx
|
||||
.db
|
||||
.profile_played_world()
|
||||
.played_world_id()
|
||||
.find(&played_world_id);
|
||||
let previous_observed_play_time_ms = existing
|
||||
.as_ref()
|
||||
.map(|row| row.last_observed_play_time_ms)
|
||||
.unwrap_or(0);
|
||||
let incremental_play_time_ms = current_play_time_ms.saturating_sub(previous_observed_play_time_ms);
|
||||
let incremental_play_time_ms =
|
||||
current_play_time_ms.saturating_sub(previous_observed_play_time_ms);
|
||||
next_total_play_time_ms = next_total_play_time_ms.saturating_add(incremental_play_time_ms);
|
||||
|
||||
if let Some(existing) = existing {
|
||||
@@ -7757,7 +7888,8 @@ fn sync_profile_dashboard_from_snapshot(
|
||||
world_subtitle: world_meta.world_subtitle,
|
||||
first_played_at: existing.first_played_at,
|
||||
last_played_at: saved_at,
|
||||
last_observed_play_time_ms: current_play_time_ms.max(existing.last_observed_play_time_ms),
|
||||
last_observed_play_time_ms: current_play_time_ms
|
||||
.max(existing.last_observed_play_time_ms),
|
||||
});
|
||||
} else {
|
||||
ctx.db.profile_played_world().insert(ProfilePlayedWorld {
|
||||
@@ -7781,21 +7913,25 @@ fn sync_profile_dashboard_from_snapshot(
|
||||
.profile_dashboard_state()
|
||||
.user_id()
|
||||
.delete(&existing.user_id);
|
||||
ctx.db.profile_dashboard_state().insert(ProfileDashboardState {
|
||||
user_id: snapshot.user_id.clone(),
|
||||
wallet_balance: next_wallet_balance,
|
||||
total_play_time_ms: next_total_play_time_ms,
|
||||
created_at: existing.created_at,
|
||||
updated_at: saved_at,
|
||||
});
|
||||
ctx.db
|
||||
.profile_dashboard_state()
|
||||
.insert(ProfileDashboardState {
|
||||
user_id: snapshot.user_id.clone(),
|
||||
wallet_balance: next_wallet_balance,
|
||||
total_play_time_ms: next_total_play_time_ms,
|
||||
created_at: existing.created_at,
|
||||
updated_at: saved_at,
|
||||
});
|
||||
} else {
|
||||
ctx.db.profile_dashboard_state().insert(ProfileDashboardState {
|
||||
user_id: snapshot.user_id.clone(),
|
||||
wallet_balance: next_wallet_balance,
|
||||
total_play_time_ms: next_total_play_time_ms,
|
||||
created_at: saved_at,
|
||||
updated_at: saved_at,
|
||||
});
|
||||
ctx.db
|
||||
.profile_dashboard_state()
|
||||
.insert(ProfileDashboardState {
|
||||
user_id: snapshot.user_id.clone(),
|
||||
wallet_balance: next_wallet_balance,
|
||||
total_play_time_ms: next_total_play_time_ms,
|
||||
created_at: saved_at,
|
||||
updated_at: saved_at,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7805,17 +7941,18 @@ fn sync_profile_save_archive_from_snapshot(
|
||||
game_state: &JsonValue,
|
||||
saved_at: Timestamp,
|
||||
) -> Result<(), String> {
|
||||
let Some(archive_meta) = resolve_profile_save_archive_meta(game_state, snapshot.current_story_json.as_deref()) else {
|
||||
let Some(archive_meta) =
|
||||
resolve_profile_save_archive_meta(game_state, snapshot.current_story_json.as_deref())
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let archive_id = format!("{}:{}", snapshot.user_id, archive_meta.world_key);
|
||||
let existing = ctx
|
||||
.db
|
||||
.profile_save_archive()
|
||||
.archive_id()
|
||||
.find(&archive_id);
|
||||
let created_at = existing.as_ref().map(|row| row.created_at).unwrap_or(saved_at);
|
||||
let existing = ctx.db.profile_save_archive().archive_id().find(&archive_id);
|
||||
let created_at = existing
|
||||
.as_ref()
|
||||
.map(|row| row.created_at)
|
||||
.unwrap_or(saved_at);
|
||||
|
||||
if let Some(existing) = existing {
|
||||
ctx.db
|
||||
@@ -7905,7 +8042,8 @@ fn build_profile_save_archive_snapshot_from_row(
|
||||
}
|
||||
|
||||
fn parse_json_str(raw: &str) -> Result<JsonValue, String> {
|
||||
serde_json::from_str::<JsonValue>(raw).map_err(|error| format!("game_state_json 解析失败: {error}"))
|
||||
serde_json::from_str::<JsonValue>(raw)
|
||||
.map_err(|error| format!("game_state_json 解析失败: {error}"))
|
||||
}
|
||||
|
||||
fn parse_optional_json_str(raw: Option<&str>) -> Result<Option<JsonValue>, String> {
|
||||
@@ -7951,7 +8089,9 @@ fn resolve_profile_world_snapshot_meta(
|
||||
game_state: Option<&serde_json::Map<String, JsonValue>>,
|
||||
) -> Option<ProfileWorldSnapshotMeta> {
|
||||
let game_state = game_state?;
|
||||
let custom_world_profile = game_state.get("customWorldProfile").and_then(JsonValue::as_object);
|
||||
let custom_world_profile = game_state
|
||||
.get("customWorldProfile")
|
||||
.and_then(JsonValue::as_object);
|
||||
|
||||
if let Some(custom_world_profile) = custom_world_profile {
|
||||
let profile_id = read_string_from_json(custom_world_profile.get("id"));
|
||||
@@ -7976,7 +8116,9 @@ fn resolve_profile_world_snapshot_meta(
|
||||
}
|
||||
|
||||
let world_type = read_string_from_json(game_state.get("worldType"))?;
|
||||
let current_scene_preset = game_state.get("currentScenePreset").and_then(JsonValue::as_object);
|
||||
let current_scene_preset = game_state
|
||||
.get("currentScenePreset")
|
||||
.and_then(JsonValue::as_object);
|
||||
|
||||
Some(ProfileWorldSnapshotMeta {
|
||||
world_key: format!("builtin:{world_type}"),
|
||||
@@ -8004,8 +8146,8 @@ fn resolve_profile_save_archive_meta(
|
||||
let story_engine_memory = game_state_object
|
||||
.and_then(|state| state.get("storyEngineMemory"))
|
||||
.and_then(JsonValue::as_object);
|
||||
let continue_game_digest =
|
||||
story_engine_memory.and_then(|memory| read_string_from_json(memory.get("continueGameDigest")));
|
||||
let continue_game_digest = story_engine_memory
|
||||
.and_then(|memory| read_string_from_json(memory.get("continueGameDigest")));
|
||||
let current_story_text = parse_optional_json_str(current_story_json)
|
||||
.ok()
|
||||
.flatten()
|
||||
@@ -8704,7 +8846,11 @@ fn replace_big_fish_session(
|
||||
ctx.db.big_fish_creation_session().insert(next);
|
||||
}
|
||||
|
||||
fn replace_big_fish_run(ctx: &ReducerContext, current: &BigFishRuntimeRun, next: BigFishRuntimeRun) {
|
||||
fn replace_big_fish_run(
|
||||
ctx: &ReducerContext,
|
||||
current: &BigFishRuntimeRun,
|
||||
next: BigFishRuntimeRun,
|
||||
) {
|
||||
ctx.db
|
||||
.big_fish_runtime_run()
|
||||
.run_id()
|
||||
@@ -8713,12 +8859,7 @@ fn replace_big_fish_run(ctx: &ReducerContext, current: &BigFishRuntimeRun, next:
|
||||
}
|
||||
|
||||
fn upsert_big_fish_asset_slot(ctx: &ReducerContext, slot: BigFishAssetSlotSnapshot) {
|
||||
if let Some(existing) = ctx
|
||||
.db
|
||||
.big_fish_asset_slot()
|
||||
.slot_id()
|
||||
.find(&slot.slot_id)
|
||||
{
|
||||
if let Some(existing) = ctx.db.big_fish_asset_slot().slot_id().find(&slot.slot_id) {
|
||||
ctx.db
|
||||
.big_fish_asset_slot()
|
||||
.slot_id()
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
use module_puzzle::{
|
||||
use module_puzzle::{
|
||||
PUZZLE_MAX_TAG_COUNT, PuzzleAgentMessageKind, PuzzleAgentMessageRole,
|
||||
PuzzleAgentMessageSnapshot, PuzzleAgentSessionCreateInput, PuzzleAgentSessionGetInput,
|
||||
PuzzleAgentSessionProcedureResult, PuzzleAgentSessionSnapshot, PuzzleAgentStage,
|
||||
PuzzleAnchorPack, PuzzleDraftCompileInput, PuzzleGeneratedImageCandidate,
|
||||
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,
|
||||
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,
|
||||
};
|
||||
use serde_json::from_str as json_from_str;
|
||||
use serde_json::to_string as json_to_string;
|
||||
@@ -488,7 +486,10 @@ fn submit_puzzle_agent_message_tx(
|
||||
text: input.user_message_text.clone(),
|
||||
created_at: submitted_at,
|
||||
});
|
||||
let assistant_message_id = format!("{}assistant-{}", input.session_id, input.submitted_at_micros);
|
||||
let assistant_message_id = format!(
|
||||
"{}assistant-{}",
|
||||
input.session_id, input.submitted_at_micros
|
||||
);
|
||||
ctx.db.puzzle_agent_message().insert(PuzzleAgentMessageRow {
|
||||
message_id: assistant_message_id,
|
||||
session_id: input.session_id.clone(),
|
||||
@@ -547,7 +548,9 @@ fn compile_puzzle_agent_draft_tx(
|
||||
stage: PuzzleAgentStage::DraftReady,
|
||||
anchor_pack_json: serialize_json(&anchor_pack),
|
||||
draft_json: Some(serialize_json(&draft)),
|
||||
last_assistant_reply: Some("拼图结果页草稿已经生成,可以开始出图并确认标签。".to_string()),
|
||||
last_assistant_reply: Some(
|
||||
"拼图结果页草稿已经生成,可以开始出图并确认标签。".to_string(),
|
||||
),
|
||||
published_profile_id: row.published_profile_id.clone(),
|
||||
created_at: row.created_at,
|
||||
updated_at: compiled_at,
|
||||
@@ -574,14 +577,19 @@ fn save_puzzle_generated_images_tx(
|
||||
) -> Result<PuzzleAgentSessionSnapshot, String> {
|
||||
let row = get_owned_session_row(ctx, &input.session_id, &input.owner_user_id)?;
|
||||
let mut draft = deserialize_draft_required(&row.draft_json)?;
|
||||
let candidates: Vec<PuzzleGeneratedImageCandidate> =
|
||||
json_from_str(&input.candidates_json).map_err(|error| format!("拼图候选图 JSON 非法: {error}"))?;
|
||||
let candidates: Vec<PuzzleGeneratedImageCandidate> = json_from_str(&input.candidates_json)
|
||||
.map_err(|error| format!("拼图候选图 JSON 非法: {error}"))?;
|
||||
if candidates.is_empty() {
|
||||
return Err("拼图候选图不能为空".to_string());
|
||||
}
|
||||
draft.candidates = candidates;
|
||||
draft.generation_status = "ready".to_string();
|
||||
if let Some(selected) = draft.candidates.iter().find(|entry| entry.selected).cloned() {
|
||||
if let Some(selected) = draft
|
||||
.candidates
|
||||
.iter()
|
||||
.find(|entry| entry.selected)
|
||||
.cloned()
|
||||
{
|
||||
draft.selected_candidate_id = Some(selected.candidate_id);
|
||||
draft.cover_image_src = Some(selected.image_src);
|
||||
draft.cover_asset_id = Some(selected.asset_id);
|
||||
@@ -626,7 +634,8 @@ fn select_puzzle_cover_image_tx(
|
||||
) -> Result<PuzzleAgentSessionSnapshot, String> {
|
||||
let row = get_owned_session_row(ctx, &input.session_id, &input.owner_user_id)?;
|
||||
let draft = deserialize_draft_required(&row.draft_json)?;
|
||||
let draft = apply_selected_candidate(draft, &input.candidate_id).map_err(|error| error.to_string())?;
|
||||
let draft =
|
||||
apply_selected_candidate(draft, &input.candidate_id).map_err(|error| error.to_string())?;
|
||||
let selected_at = Timestamp::from_micros_since_unix_epoch(input.selected_at_micros);
|
||||
let next_stage = if build_result_preview(&draft, Some("创作者")).publish_ready {
|
||||
PuzzleAgentStage::ReadyToPublish
|
||||
@@ -814,7 +823,13 @@ fn start_puzzle_run_tx(
|
||||
ctx: &TxContext,
|
||||
input: PuzzleRunStartInput,
|
||||
) -> Result<PuzzleRunSnapshot, String> {
|
||||
if ctx.db.puzzle_runtime_run().run_id().find(&input.run_id).is_some() {
|
||||
if ctx
|
||||
.db
|
||||
.puzzle_runtime_run()
|
||||
.run_id()
|
||||
.find(&input.run_id)
|
||||
.is_some()
|
||||
{
|
||||
return Err("拼图 run 已存在".to_string());
|
||||
}
|
||||
let entry_profile_row = ctx
|
||||
@@ -827,7 +842,8 @@ fn start_puzzle_run_tx(
|
||||
return Err("入口拼图作品未发布".to_string());
|
||||
}
|
||||
let entry_profile = build_puzzle_work_profile_from_row(&entry_profile_row)?;
|
||||
let mut run = start_run(input.run_id.clone(), &entry_profile, 0).map_err(|error| error.to_string())?;
|
||||
let mut run =
|
||||
start_run(input.run_id.clone(), &entry_profile, 0).map_err(|error| error.to_string())?;
|
||||
run.recommended_next_profile_id = select_next_profile(
|
||||
&entry_profile,
|
||||
&run.played_profile_ids,
|
||||
@@ -854,8 +870,8 @@ fn swap_puzzle_pieces_tx(
|
||||
) -> Result<PuzzleRunSnapshot, String> {
|
||||
let row = get_owned_run_row(ctx, &input.run_id, &input.owner_user_id)?;
|
||||
let current_run = deserialize_run(&row.snapshot_json)?;
|
||||
let mut next_run =
|
||||
swap_pieces(¤t_run, &input.first_piece_id, &input.second_piece_id).map_err(|error| error.to_string())?;
|
||||
let mut next_run = swap_pieces(¤t_run, &input.first_piece_id, &input.second_piece_id)
|
||||
.map_err(|error| error.to_string())?;
|
||||
refresh_next_profile_recommendation(ctx, &mut next_run)?;
|
||||
replace_puzzle_runtime_run(ctx, &row, &next_run, input.swapped_at_micros);
|
||||
Ok(next_run)
|
||||
@@ -900,17 +916,18 @@ fn advance_puzzle_next_level_tx(
|
||||
.ok_or_else(|| "当前拼图作品不存在".to_string())?,
|
||||
)?;
|
||||
let candidates = list_published_puzzle_profiles(ctx)?;
|
||||
let next_profile = select_next_profile(¤t_profile, ¤t_run.played_profile_ids, &candidates)
|
||||
.ok_or_else(|| "没有可用的下一关候选".to_string())?
|
||||
.clone();
|
||||
let mut next_run =
|
||||
module_puzzle::advance_next_level(¤t_run, &next_profile).map_err(|error| error.to_string())?;
|
||||
next_run.recommended_next_profile_id = select_next_profile(
|
||||
&next_profile,
|
||||
&next_run.played_profile_ids,
|
||||
let next_profile = select_next_profile(
|
||||
¤t_profile,
|
||||
¤t_run.played_profile_ids,
|
||||
&candidates,
|
||||
)
|
||||
.map(|value| value.profile_id.clone());
|
||||
.ok_or_else(|| "没有可用的下一关候选".to_string())?
|
||||
.clone();
|
||||
let mut next_run = module_puzzle::advance_next_level(¤t_run, &next_profile)
|
||||
.map_err(|error| error.to_string())?;
|
||||
next_run.recommended_next_profile_id =
|
||||
select_next_profile(&next_profile, &next_run.played_profile_ids, &candidates)
|
||||
.map(|value| value.profile_id.clone());
|
||||
|
||||
if let Some(next_profile_row) = ctx
|
||||
.db
|
||||
@@ -954,7 +971,9 @@ fn build_puzzle_agent_session_snapshot(
|
||||
})
|
||||
}
|
||||
|
||||
fn build_puzzle_work_profile_from_row(row: &PuzzleWorkProfileRow) -> Result<PuzzleWorkProfile, String> {
|
||||
fn build_puzzle_work_profile_from_row(
|
||||
row: &PuzzleWorkProfileRow,
|
||||
) -> Result<PuzzleWorkProfile, String> {
|
||||
Ok(PuzzleWorkProfile {
|
||||
work_id: row.work_id.clone(),
|
||||
profile_id: row.profile_id.clone(),
|
||||
@@ -968,7 +987,9 @@ fn build_puzzle_work_profile_from_row(row: &PuzzleWorkProfileRow) -> Result<Puzz
|
||||
cover_asset_id: row.cover_asset_id.clone(),
|
||||
publication_status: row.publication_status,
|
||||
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()),
|
||||
published_at_micros: row
|
||||
.published_at
|
||||
.map(|value| value.to_micros_since_unix_epoch()),
|
||||
play_count: row.play_count,
|
||||
publish_ready: row.publish_ready,
|
||||
anchor_pack: deserialize_anchor_pack(&row.anchor_pack_json)?,
|
||||
@@ -994,7 +1015,9 @@ fn list_session_messages(ctx: &TxContext, session_id: &str) -> Vec<PuzzleAgentMe
|
||||
items
|
||||
}
|
||||
|
||||
fn build_puzzle_suggested_actions(stage: PuzzleAgentStage) -> Vec<module_puzzle::PuzzleAgentSuggestedAction> {
|
||||
fn build_puzzle_suggested_actions(
|
||||
stage: PuzzleAgentStage,
|
||||
) -> Vec<module_puzzle::PuzzleAgentSuggestedAction> {
|
||||
match stage {
|
||||
PuzzleAgentStage::CollectingAnchors => vec![module_puzzle::PuzzleAgentSuggestedAction {
|
||||
id: "compile-draft".to_string(),
|
||||
@@ -1051,14 +1074,26 @@ fn append_system_message(
|
||||
}
|
||||
|
||||
fn ensure_session_missing(ctx: &TxContext, session_id: &str) -> Result<(), String> {
|
||||
if ctx.db.puzzle_agent_session().session_id().find(&session_id.to_string()).is_some() {
|
||||
if ctx
|
||||
.db
|
||||
.puzzle_agent_session()
|
||||
.session_id()
|
||||
.find(&session_id.to_string())
|
||||
.is_some()
|
||||
{
|
||||
return Err("拼图 session 已存在".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_message_missing(ctx: &TxContext, message_id: &str) -> Result<(), String> {
|
||||
if ctx.db.puzzle_agent_message().message_id().find(&message_id.to_string()).is_some() {
|
||||
if ctx
|
||||
.db
|
||||
.puzzle_agent_message()
|
||||
.message_id()
|
||||
.find(&message_id.to_string())
|
||||
.is_some()
|
||||
{
|
||||
return Err("拼图消息已存在".to_string());
|
||||
}
|
||||
Ok(())
|
||||
@@ -1122,10 +1157,7 @@ fn replace_puzzle_work_profile(
|
||||
ctx.db.puzzle_work_profile().insert(next);
|
||||
}
|
||||
|
||||
fn upsert_puzzle_work_profile(
|
||||
ctx: &TxContext,
|
||||
profile: PuzzleWorkProfile,
|
||||
) -> Result<(), String> {
|
||||
fn upsert_puzzle_work_profile(ctx: &TxContext, profile: PuzzleWorkProfile) -> Result<(), String> {
|
||||
if let Some(existing) = ctx
|
||||
.db
|
||||
.puzzle_work_profile()
|
||||
@@ -1219,10 +1251,7 @@ fn replace_puzzle_runtime_run(
|
||||
run: &PuzzleRunSnapshot,
|
||||
updated_at_micros: i64,
|
||||
) {
|
||||
ctx.db
|
||||
.puzzle_runtime_run()
|
||||
.run_id()
|
||||
.delete(¤t.run_id);
|
||||
ctx.db.puzzle_runtime_run().run_id().delete(¤t.run_id);
|
||||
ctx.db.puzzle_runtime_run().insert(PuzzleRuntimeRunRow {
|
||||
run_id: run.run_id.clone(),
|
||||
owner_user_id: current.owner_user_id.clone(),
|
||||
@@ -1414,6 +1443,9 @@ mod tests {
|
||||
author_display_name: "作者".to_string(),
|
||||
summary: String::new(),
|
||||
};
|
||||
assert!(recommendation_score(&left, &right) > tag_similarity_score(&left.theme_tags, &right.theme_tags));
|
||||
assert!(
|
||||
recommendation_score(&left, &right)
|
||||
> tag_similarity_score(&left.theme_tags, &right.theme_tags)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user