收口创作入口契约后台表单

将统一创作契约泥点消耗改为数字字段并由前端格式化展示
将后台契约编辑从 JSON 文本改为结构化卡片与弹窗表单
隐藏玩法阶段等内部标识并按玩法默认映射自动带出
更新创作入口文档、团队记忆和回归测试
This commit is contained in:
2026-06-07 23:53:26 +08:00
parent 2a6da01307
commit 17662916cd
13 changed files with 822 additions and 108 deletions

View File

@@ -83,6 +83,12 @@ pub struct CreationEntryTypeResponse {
pub struct UnifiedCreationSpecResponse {
pub play_id: String,
pub title: String,
#[serde(
default = "default_unified_creation_mud_point_cost",
alias = "mudPointCostText",
deserialize_with = "deserialize_unified_creation_mud_point_cost"
)]
pub mud_point_cost: u32,
pub workspace_stage: String,
pub generation_stage: String,
pub result_stage: String,
@@ -99,6 +105,11 @@ pub struct UnifiedCreationFieldResponse {
}
pub const UNIFIED_CREATION_FIELD_KINDS: [&str; 4] = ["text", "select", "image", "audio"];
pub const DEFAULT_UNIFIED_CREATION_MUD_POINT_COST: u32 = 10;
pub fn default_unified_creation_mud_point_cost() -> u32 {
DEFAULT_UNIFIED_CREATION_MUD_POINT_COST
}
pub fn build_phase1_unified_creation_spec(play_id: &str) -> Option<UnifiedCreationSpecResponse> {
let (workspace_stage, generation_stage, result_stage, fields) = match play_id {
@@ -202,6 +213,7 @@ pub fn build_phase1_unified_creation_spec(play_id: &str) -> Option<UnifiedCreati
Some(UnifiedCreationSpecResponse {
play_id: play_id.to_string(),
title: default_unified_creation_title(play_id)?.to_string(),
mud_point_cost: default_unified_creation_mud_point_cost(),
workspace_stage: workspace_stage.to_string(),
generation_stage: generation_stage.to_string(),
result_stage: result_stage.to_string(),
@@ -235,6 +247,9 @@ pub fn validate_unified_creation_spec_response(
if spec.title.trim().is_empty() {
return Err("统一创作契约标题不能为空".to_string());
}
if spec.mud_point_cost == 0 {
return Err("统一创作契约泥点消耗数量必须大于 0".to_string());
}
let workspace_stage = spec.workspace_stage.trim();
let generation_stage = spec.generation_stage.trim();
@@ -305,6 +320,31 @@ pub fn decode_unified_creation_spec_response(
Ok(spec)
}
fn deserialize_unified_creation_mud_point_cost<'de, D>(deserializer: D) -> Result<u32, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
match value {
serde_json::Value::Number(number) => number
.as_u64()
.and_then(|raw| u32::try_from(raw).ok())
.ok_or_else(|| serde::de::Error::custom("泥点消耗数量必须是非负整数")),
serde_json::Value::String(text) => parse_unified_creation_mud_point_cost_text(&text)
.ok_or_else(|| serde::de::Error::custom("泥点消耗数量必须是数字")),
_ => Err(serde::de::Error::custom("泥点消耗数量必须是数字")),
}
}
fn parse_unified_creation_mud_point_cost_text(value: &str) -> Option<u32> {
let digits: String = value
.chars()
.skip_while(|character| !character.is_ascii_digit())
.take_while(|character| character.is_ascii_digit())
.collect();
digits.parse::<u32>().ok()
}
pub fn resolve_unified_creation_spec_response(
play_id: &str,
value: Option<&str>,
@@ -337,6 +377,7 @@ mod tests {
fn phase1_unified_creation_specs_cover_existing_templates() {
let puzzle = build_phase1_unified_creation_spec("puzzle").expect("puzzle spec");
assert_eq!(puzzle.title, "拼图");
assert_eq!(puzzle.mud_point_cost, 10);
assert_eq!(puzzle.fields[0].id, "pictureDescription");
assert_eq!(puzzle.fields[1].kind, "image");
@@ -385,6 +426,7 @@ mod tests {
let raw = r#"{
"playId": "puzzle",
"title": "想做个什么玩法?",
"mudPointCost": 12,
"workspaceStage": "puzzle-agent-workspace",
"generationStage": "puzzle-generating",
"resultStage": "puzzle-result",
@@ -402,6 +444,56 @@ mod tests {
resolve_unified_creation_spec_response("puzzle", Some(raw)).expect("puzzle spec");
assert_eq!(spec.title, "想做个什么玩法?");
assert_eq!(spec.mud_point_cost, 12);
}
#[test]
fn unified_creation_spec_reads_legacy_mud_point_cost_text() {
let raw = r#"{
"playId": "puzzle",
"title": "想做个什么玩法?",
"mudPointCostText": "12泥点数",
"workspaceStage": "puzzle-agent-workspace",
"generationStage": "puzzle-generating",
"resultStage": "puzzle-result",
"fields": [
{
"id": "pictureDescription",
"kind": "text",
"label": "画面描述",
"required": true
}
]
}"#;
let spec =
resolve_unified_creation_spec_response("puzzle", Some(raw)).expect("puzzle spec");
assert_eq!(spec.mud_point_cost, 12);
}
#[test]
fn unified_creation_spec_uses_default_mud_point_cost_for_legacy_json() {
let raw = r#"{
"playId": "puzzle",
"title": "拼图",
"workspaceStage": "puzzle-agent-workspace",
"generationStage": "puzzle-generating",
"resultStage": "puzzle-result",
"fields": [
{
"id": "pictureDescription",
"kind": "text",
"label": "画面描述",
"required": true
}
]
}"#;
let spec =
resolve_unified_creation_spec_response("puzzle", Some(raw)).expect("puzzle spec");
assert_eq!(spec.mud_point_cost, 10);
}
#[test]