This commit is contained in:
2026-05-01 00:33:39 +08:00
parent 61969c5116
commit fe02603ba1
68 changed files with 4586 additions and 748 deletions

View File

@@ -0,0 +1,86 @@
/// 拼图作品草稿生成动作的提示词主源。
///
/// 拼图结果页草稿本体仍由 SpacetimeDB reducer 按表单/锚点确定性编译;
/// 这里收口 api-server 在生成草稿前后需要写入 reducer 的表单 seed 文本,
/// 以及草稿首图生成时的 prompt 来源选择,避免业务路由直接拼提示词文本。
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct PuzzleFormSeedPromptParts<'a> {
pub(crate) title: Option<&'a str>,
pub(crate) work_description: Option<&'a str>,
pub(crate) picture_description: Option<&'a str>,
}
/// 将填表式拼图输入编译成 SpacetimeDB 可恢复的表单 seed prompt。
pub(crate) fn build_puzzle_form_seed_prompt(parts: PuzzleFormSeedPromptParts<'_>) -> String {
[
("作品名称", normalize_prompt_part(parts.title)),
("作品描述", normalize_prompt_part(parts.work_description)),
("画面描述", normalize_prompt_part(parts.picture_description)),
]
.into_iter()
.filter_map(|(label, value)| value.map(|value| format!("{label}{value}")))
.collect::<Vec<_>>()
.join("\n")
}
/// 生成作品草稿时,首图 prompt 优先使用玩家当前表单里的画面描述。
pub(crate) fn resolve_puzzle_draft_cover_prompt(
explicit_prompt: Option<&str>,
level_picture_description: &str,
draft_summary: &str,
) -> String {
normalize_prompt_part(explicit_prompt)
.or_else(|| normalize_prompt_part(Some(level_picture_description)))
.or_else(|| normalize_prompt_part(Some(draft_summary)))
.unwrap_or_default()
.to_string()
}
/// 结果页单关重新生成时,优先使用面板当前编辑态 prompt再回退关卡画面描述。
pub(crate) fn resolve_puzzle_level_image_prompt(
explicit_prompt: Option<&str>,
level_picture_description: &str,
) -> String {
normalize_prompt_part(explicit_prompt)
.or_else(|| normalize_prompt_part(Some(level_picture_description)))
.unwrap_or_default()
.to_string()
}
fn normalize_prompt_part(value: Option<&str>) -> Option<&str> {
value.map(str::trim).filter(|value| !value.is_empty())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn form_seed_prompt_keeps_only_user_visible_fields() {
let prompt = build_puzzle_form_seed_prompt(PuzzleFormSeedPromptParts {
title: Some(" 暖灯猫街 "),
work_description: Some("雨夜礼物拼图"),
picture_description: Some("猫咪在灯牌下回头"),
});
assert_eq!(
prompt,
"作品名称:暖灯猫街\n作品描述:雨夜礼物拼图\n画面描述:猫咪在灯牌下回头"
);
}
#[test]
fn draft_cover_prompt_prefers_current_picture_description() {
let prompt =
resolve_puzzle_draft_cover_prompt(Some(" 当前表单画面 "), "旧关卡画面", "作品简介");
assert_eq!(prompt, "当前表单画面");
}
#[test]
fn level_image_prompt_falls_back_to_level_description() {
let prompt = resolve_puzzle_level_image_prompt(Some(" "), "关卡画面描述");
assert_eq!(prompt, "关卡画面描述");
}
}

View File

@@ -38,17 +38,15 @@ pub(crate) fn build_puzzle_image_prompt(level_name: &str, prompt: &str) -> Strin
image_prompt
}
fn build_puzzle_image_prompt_text(level_name: &str, prompt: &str) -> String {
fn build_puzzle_image_prompt_text(_level_name: &str, prompt: &str) -> String {
format!(
concat!(
"请生成一张高清插画。",
"关卡名:{level_name}。",
"画面主体:{prompt}。",
"画面要求1:1 正方形拼图关卡,适配 3x3 或 4x4 拼图切块,",
"画面要求1:1",
"主体要清晰集中,前中后景层次明确,局部细节丰富但不要杂乱,",
"避免文字、水印、边框和 UI 元素。"
),
level_name = level_name,
prompt = prompt,
)
}
@@ -78,10 +76,9 @@ mod tests {
fn build_puzzle_image_prompt_keeps_puzzle_asset_constraints() {
let prompt = build_puzzle_image_prompt("雨夜神庙", "猫咪在发光遗迹前寻找线索");
assert!(prompt.contains("雨夜神庙"));
assert!(prompt.contains("猫咪在发光遗迹前寻找线索"));
assert!(prompt.contains("1:1 正方形拼图关卡"));
assert!(prompt.contains("3x3 或 4x4"));
assert!(prompt.contains("1:1"));
assert!(prompt.contains("主体要清晰集中"));
assert!(prompt.contains("避免文字、水印、边框和 UI 元素"));
}
@@ -93,8 +90,8 @@ mod tests {
let prompt = build_puzzle_image_prompt(long_level_name.as_str(), long_description.as_str());
assert!(prompt.chars().count() <= PUZZLE_TEXT_TO_IMAGE_PROMPT_MAX_CHARS);
assert!(prompt.contains("1:1 正方形拼图关卡"));
assert!(prompt.contains("3x3 或 4x4"));
assert!(prompt.contains("1:1"));
assert!(prompt.contains("主体要清晰集中"));
assert!(prompt.contains("避免文字、水印、边框和 UI 元素"));
}

View File

@@ -1,2 +1,3 @@
pub(crate) mod agent_chat;
pub(crate) mod draft;
pub(crate) mod image;