Prune stale docs and update .hermes content

Delete a large set of outdated documentation (many files under docs/ and .hermes/plans/, including audits, design, prd, technical, planning, assets, and todos). Update and consolidate .hermes content: refresh shared-memory pages (decision-log, development-workflow, document-map, pitfalls, project-overview, team-conventions) and several skills/references under .hermes/skills. Also modify AGENTS.md, README.md, UI_CODING_STANDARD.md, docs/README.md and .encoding-check-ignore. Purpose: clean up stale planning/audit material and keep current hermes documentation and related top-level docs in sync.
This commit is contained in:
2026-05-15 06:24:07 +08:00
parent 2eded08bc7
commit 3cb3efb4d0
708 changed files with 4033 additions and 142328 deletions

View File

@@ -81,6 +81,9 @@ pub fn resolve_creation_entry_route_id(path: &str) -> Option<&'static str> {
if normalized.starts_with("/api/runtime/bark-battle") {
return Some("bark-battle");
}
if normalized.starts_with("/api/creation/bark-battle") {
return Some("bark-battle");
}
if normalized.starts_with("/api/runtime/square-hole") {
return Some("square-hole");
}
@@ -156,6 +159,10 @@ mod tests {
resolve_creation_entry_route_id("/api/runtime/bark-battle/works/work-1/config"),
Some("bark-battle"),
);
assert_eq!(
resolve_creation_entry_route_id("/api/creation/bark-battle/drafts"),
Some("bark-battle"),
);
assert_eq!(
resolve_creation_entry_route_id("/api/creation/edutainment/baby-object-match/assets"),
Some("baby-object-match"),
@@ -168,7 +175,7 @@ mod tests {
}
#[test]
fn test_creation_entry_config_response_keeps_baby_object_match_visible() {
fn test_creation_entry_config_response_keeps_baby_object_match_coming_soon() {
let config = test_creation_entry_config_response();
let baby_object_match = config
.creation_types
@@ -178,7 +185,8 @@ mod tests {
assert_eq!(baby_object_match.title, "宝贝识物");
assert!(baby_object_match.visible);
assert!(baby_object_match.open);
assert!(!baby_object_match.open);
assert_eq!(baby_object_match.badge, "敬请期待");
assert_eq!(baby_object_match.sort_order, 90);
}
}

View File

@@ -108,6 +108,9 @@ const MATCH3D_OSS_PUT_TIMEOUT_MS: u64 = 3 * 60_000;
const MATCH3D_LEGACY_MODEL_MAX_BYTES: usize = 120 * 1024 * 1024;
const MATCH3D_ITEM_IMAGE_MAX_BYTES: usize = 20 * 1024 * 1024;
const MATCH3D_WORK_METADATA_LLM_MODEL: &str = "gpt-4o";
const MATCH3D_ITEM_SIZE_LARGE: &str = "";
const MATCH3D_ITEM_SIZE_MEDIUM: &str = "";
const MATCH3D_ITEM_SIZE_SMALL: &str = "";
const MATCH3D_CONTAINER_REFERENCE_IMAGE_PATH: &str =
"public/match3d-background-references/pot-fused-reference.png";
const MATCH3D_QUESTION_THEME: &str = "你想创作什么题材";
@@ -136,6 +139,7 @@ struct Match3DConfigJson {
struct Match3DGeneratedItemAsset {
item_id: String,
item_name: String,
item_size: Option<String>,
image_src: Option<String>,
image_object_key: Option<String>,
image_views: Vec<Match3DGeneratedItemImageView>,
@@ -195,6 +199,7 @@ struct Match3DGeneratedWorkMetadata {
#[derive(Clone, Debug)]
struct Match3DGeneratedItemPlan {
name: String,
item_size: String,
sound_prompt: String,
}
@@ -211,6 +216,8 @@ struct Match3DGeneratedItemAssetJson {
item_id: String,
item_name: String,
#[serde(default)]
item_size: Option<String>,
#[serde(default)]
image_src: Option<String>,
#[serde(default)]
image_object_key: Option<String>,
@@ -920,6 +927,10 @@ pub async fn persist_match3d_generated_model(
let next_asset = Match3DGeneratedItemAsset {
item_id: payload.item_id,
item_name,
item_size: current_asset
.as_ref()
.and_then(|asset| asset.item_size.clone())
.or_else(|| Some(MATCH3D_ITEM_SIZE_LARGE.to_string())),
image_src: current_asset
.as_ref()
.and_then(|asset| asset.image_src.clone()),
@@ -1343,7 +1354,7 @@ pub async fn generate_match3d_item_assets_for_work(
item: map_match3d_work_profile_response(profile),
generated_item_assets: sort_match3d_generated_assets(assets)
.into_iter()
.map(Match3DGeneratedItemAssetJson::from)
.map(Match3DGeneratedItemAssetJson::from)
.map(map_match3d_generated_item_asset_for_work)
.collect(),
},
@@ -2456,6 +2467,7 @@ fn map_match3d_generated_item_asset_for_agent(
Match3DAgentGeneratedItemAssetResponse {
item_id: asset.item_id,
item_name: asset.item_name,
item_size: asset.item_size,
image_src: asset.image_src,
image_object_key: asset.image_object_key,
image_views: asset
@@ -2488,6 +2500,7 @@ fn map_match3d_generated_item_asset_for_work(
shared_contracts::match3d_works::Match3DGeneratedItemAssetResponse {
item_id: asset.item_id,
item_name: asset.item_name,
item_size: asset.item_size.or_else(|| Some(MATCH3D_ITEM_SIZE_LARGE.to_string())),
image_src: asset.image_src,
image_object_key: asset.image_object_key,
image_views: asset
@@ -3222,6 +3235,7 @@ impl From<Match3DGeneratedItemAsset> for Match3DGeneratedItemAssetJson {
Self {
item_id: asset.item_id,
item_name: asset.item_name,
item_size: asset.item_size,
image_src: asset.image_src,
image_object_key: asset.image_object_key,
image_views: asset.image_views,
@@ -3248,6 +3262,7 @@ impl From<Match3DGeneratedItemAssetJson> for Match3DGeneratedItemAsset {
Self {
item_id: asset.item_id,
item_name: asset.item_name,
item_size: asset.item_size.or_else(|| Some(MATCH3D_ITEM_SIZE_LARGE.to_string())),
image_src: asset.image_src,
image_object_key: asset.image_object_key,
image_views: asset.image_views,
@@ -3276,6 +3291,7 @@ impl From<shared_contracts::match3d_works::Match3DGeneratedItemAssetResponse>
Self {
item_id: asset.item_id,
item_name: asset.item_name,
item_size: asset.item_size,
image_src: asset.image_src,
image_object_key: asset.image_object_key,
image_views: asset
@@ -3401,6 +3417,7 @@ async fn ensure_match3d_item_image_assets(
Some(Match3DItemImageGenerationSeed {
item_id,
item_name: item.name.clone(),
item_size: item.item_size.clone(),
sound_prompt: item.sound_prompt.clone(),
persist_asset: true,
background_music_title: None,
@@ -3454,6 +3471,7 @@ async fn ensure_match3d_item_image_assets(
struct Match3DItemImageGenerationSeed {
item_id: String,
item_name: String,
item_size: String,
sound_prompt: String,
persist_asset: bool,
background_music_title: Option<String>,
@@ -3580,6 +3598,9 @@ async fn generate_match3d_item_image_assets_in_batches(
asset: Match3DGeneratedItemAsset {
item_id: seed.item_id,
item_name: seed.item_name,
item_size: Some(normalize_match3d_item_size(seed.item_size.as_str()))
.filter(|value| !value.is_empty())
.or_else(|| Some(MATCH3D_ITEM_SIZE_LARGE.to_string())),
image_src: primary_view
.as_ref()
.and_then(|view| view.image_src.clone()),
@@ -3685,6 +3706,7 @@ async fn append_match3d_new_item_assets(
let item_id = allocate_match3d_generated_item_id(&assets, &mut next_item_index);
Match3DItemImageGenerationSeed {
item_id,
item_size: infer_match3d_item_size(item_name.as_str()),
sound_prompt: build_fallback_match3d_item_sound_prompt(config, item_name.as_str()),
item_name,
persist_asset: index < requested_item_count,
@@ -3766,6 +3788,12 @@ async fn replace_match3d_item_assets(
});
Match3DItemImageGenerationSeed {
item_id,
item_size: matched_asset
.as_ref()
.and_then(|asset| asset.item_size.clone())
.map(|value| normalize_match3d_item_size(value.as_str()))
.filter(|value| !value.is_empty())
.unwrap_or_else(|| infer_match3d_item_size(item_name.as_str())),
sound_prompt: matched_asset
.as_ref()
.and_then(|asset| asset.sound_prompt.clone())
@@ -3857,7 +3885,7 @@ async fn generate_match3d_draft_plan(
let gameplay_item_count = resolve_match3d_gameplay_item_count(config);
let generated_item_count = resolve_match3d_generated_item_count(config);
let user_prompt = format!(
"题材设定:{}\n请生成抓大鹅游戏草稿生成计划。要求:只返回 JSON 对象,字段为 gameName、summary、tags、backgroundPrompt、items。gameName 为 4 到 12 个中文字符不要包含“作品”“游戏”summary 为 18 到 48 个中文字符的作品描述说明题材氛围和核心体验不要写规则说明tags 为 3 到 6 个中文短标签,每个 2 到 6 个汉字后续会用同一作品信息再次生成作品标签backgroundPrompt 是用于生成局内纯背景图的中文提示词只描述竖屏移动端抓大鹅题材氛围、色彩和环境不得描述锅、圆盘、托盘、拼图槽、物品槽、HUD、UI、文字、按钮、倒计时、分数或物品当前玩法需要 {} 种物品,但素材图固定每 5 个物品一批,因此 items 必须向上补齐并正好返回 {} 项,每项包含 name 和 soundPromptname 为 2 到 6 个汉字soundPrompt 只作为历史字段保留,可返回空字符串。",
"题材设定:{}\n请生成抓大鹅游戏草稿生成计划。要求:只返回 JSON 对象,字段为 gameName、summary、tags、backgroundPrompt、items。gameName 为 4 到 12 个中文字符不要包含“作品”“游戏”summary 为 18 到 48 个中文字符的作品描述说明题材氛围和核心体验不要写规则说明tags 为 3 到 6 个中文短标签,每个 2 到 6 个汉字后续会用同一作品信息再次生成作品标签backgroundPrompt 是用于生成局内纯背景图的中文提示词只描述竖屏移动端抓大鹅题材氛围、色彩和环境不得描述锅、圆盘、托盘、拼图槽、物品槽、HUD、UI、文字、按钮、倒计时、分数或物品当前玩法需要 {} 种物品,但素材图固定每 5 个物品一批,因此 items 必须向上补齐并正好返回 {} 项,每项包含 name、itemSize 和 soundPromptname 为 2 到 6 个汉字itemSize 只能是“大”“中”“小”之一,按物品真实相对尺寸判断,例如西瓜/大箱子偏大,苹果/杯子偏中,糖果/钥匙偏小;soundPrompt 只作为历史字段保留,可返回空字符串。",
config.theme_text, gameplay_item_count, generated_item_count
);
let response = llm_client
@@ -3931,6 +3959,14 @@ fn parse_match3d_draft_plan(
if name.is_empty() {
return None;
}
let item_size = item
.get("itemSize")
.or_else(|| item.get("item_size"))
.or_else(|| item.get("size"))
.and_then(Value::as_str)
.map(normalize_match3d_item_size)
.filter(|value| !value.is_empty())
.unwrap_or_else(|| infer_match3d_item_size(&name));
let sound_prompt = item
.get("soundPrompt")
.or_else(|| item.get("sound_prompt"))
@@ -3938,7 +3974,11 @@ fn parse_match3d_draft_plan(
.map(normalize_match3d_audio_prompt)
.filter(|value| !value.is_empty())
.unwrap_or_else(|| build_fallback_match3d_item_sound_prompt(config, &name));
Some(Match3DGeneratedItemPlan { name, sound_prompt })
Some(Match3DGeneratedItemPlan {
name,
item_size,
sound_prompt,
})
})
.collect::<Vec<_>>()
})
@@ -4020,6 +4060,7 @@ fn fallback_match3d_draft_plan(config: &Match3DConfigJson) -> Match3DGeneratedDr
.into_iter()
.take(resolve_match3d_generated_item_count(config))
.map(|name| Match3DGeneratedItemPlan {
item_size: infer_match3d_item_size(&name),
sound_prompt: build_fallback_match3d_item_sound_prompt(config, &name),
name,
})
@@ -4042,6 +4083,36 @@ fn normalize_match3d_item_name(raw: &str) -> String {
.to_string()
}
fn normalize_match3d_item_size(raw: &str) -> String {
let normalized = raw.trim().trim_matches(['"', '\'', '“', '”', '。', '', ',', '、']);
match normalized {
"" | "大型" | "偏大" | "large" | "Large" | "L" | "l" => MATCH3D_ITEM_SIZE_LARGE.to_string(),
"" | "中型" | "中等" | "medium" | "Medium" | "M" | "m" => MATCH3D_ITEM_SIZE_MEDIUM.to_string(),
"" | "小型" | "偏小" | "small" | "Small" | "S" | "s" => MATCH3D_ITEM_SIZE_SMALL.to_string(),
_ => String::new(),
}
}
fn infer_match3d_item_size(item_name: &str) -> String {
let name = item_name.trim();
let large_keywords = [
"西瓜", "南瓜", "椰子", "", "", "", "", "", "", "瓶子", "大瓶", "",
"书包", "", "抱枕", "玩偶", "", "圆球", "足球", "篮球", "",
];
if large_keywords.iter().any(|keyword| name.contains(keyword)) {
return MATCH3D_ITEM_SIZE_LARGE.to_string();
}
let small_keywords = [
"草莓", "蓝莓", "葡萄", "樱桃", "", "", "糖果", "钥匙", "硬币", "纽扣", "徽章",
"戒指", "耳环", "铃铛", "星星", "宝石", "叶片", "花瓣", "蘑菇", "贝壳", "印章",
"彩蛋", "棋子", "骰子", "挂件",
];
if small_keywords.iter().any(|keyword| name.contains(keyword)) {
return MATCH3D_ITEM_SIZE_SMALL.to_string();
}
MATCH3D_ITEM_SIZE_MEDIUM.to_string()
}
fn fallback_match3d_item_names(theme_text: &str) -> Vec<String> {
let theme = theme_text.trim();
let normalized_theme = if theme.is_empty() { "主题" } else { theme };
@@ -4094,7 +4165,13 @@ fn normalize_match3d_item_plan(
continue;
}
let sound_prompt = normalize_match3d_audio_prompt(item.sound_prompt.as_str());
let item_size = normalize_match3d_item_size(item.item_size.as_str());
normalized.push(Match3DGeneratedItemPlan {
item_size: if item_size.is_empty() {
infer_match3d_item_size(&name)
} else {
item_size
},
sound_prompt: if sound_prompt.is_empty() {
build_fallback_match3d_item_sound_prompt(config, &name)
} else {
@@ -4113,6 +4190,7 @@ fn normalize_match3d_item_plan(
continue;
}
normalized.push(Match3DGeneratedItemPlan {
item_size: infer_match3d_item_size(&name),
sound_prompt: build_fallback_match3d_item_sound_prompt(config, &name),
name,
});
@@ -4149,6 +4227,7 @@ fn fill_match3d_item_plan_to_count(
.any(|candidate: &Match3DGeneratedItemPlan| candidate.name == name)
{
normalized.push(Match3DGeneratedItemPlan {
item_size: infer_match3d_item_size(&name),
sound_prompt: build_fallback_match3d_item_sound_prompt(config, &name),
name,
});
@@ -4488,6 +4567,10 @@ fn merge_regenerated_match3d_item_asset(
Match3DGeneratedItemAsset {
item_id: current_asset.item_id,
item_name: current_asset.item_name,
item_size: current_asset
.item_size
.or(generated_asset.item_size)
.or_else(|| Some(MATCH3D_ITEM_SIZE_LARGE.to_string())),
image_src: generated_asset.image_src,
image_object_key: generated_asset.image_object_key,
image_views: generated_asset.image_views,
@@ -7092,6 +7175,7 @@ mod tests {
Match3DGeneratedItemAsset {
item_id: format!("match3d-item-{index}"),
item_name: name.to_string(),
item_size: Some(infer_match3d_item_size(name)),
image_src: Some(format!(
"/generated-match3d-assets/s/p/items/i{index}/views/view-01.png"
)),
@@ -7592,9 +7676,33 @@ mod tests {
);
assert!(plan.background_prompt.contains("纯背景"));
assert_eq!(plan.items[0].name, "草莓");
assert_eq!(plan.items[0].item_size, MATCH3D_ITEM_SIZE_SMALL);
assert!(plan.items[0].sound_prompt.contains("草莓"));
}
#[test]
fn match3d_draft_plan_parses_relative_item_sizes() {
let plan = parse_match3d_draft_plan(
r#"{"gameName":"果园大鹅宴","summary":"果园小物堆满浅盘,轻快明亮适合随手消除。","tags":["水果","抓大鹅"],"backgroundPrompt":"果园主题竖屏纯背景","items":[{"name":"西瓜","itemSize":"大","soundPrompt":""},{"name":"苹果","itemSize":"中","soundPrompt":""},{"name":"糖果","itemSize":"小","soundPrompt":""}]}"#,
&config("水果", 3, 3),
)
.expect("draft plan should parse");
assert_eq!(plan.items[0].item_size, MATCH3D_ITEM_SIZE_LARGE);
assert_eq!(plan.items[1].item_size, MATCH3D_ITEM_SIZE_MEDIUM);
assert_eq!(plan.items[2].item_size, MATCH3D_ITEM_SIZE_SMALL);
}
#[test]
fn match3d_legacy_item_asset_without_size_defaults_to_large() {
let assets = parse_match3d_generated_item_assets(Some(
r#"[{"itemId":"match3d-item-1","itemName":"草莓","status":"image_ready"}]"#,
));
let asset = Match3DGeneratedItemAsset::from(assets[0].clone());
assert_eq!(asset.item_size.as_deref(), Some(MATCH3D_ITEM_SIZE_LARGE));
}
#[test]
fn match3d_draft_item_plan_rounds_up_to_full_five_item_sheets() {
let plan = parse_match3d_draft_plan(
@@ -7642,6 +7750,7 @@ mod tests {
let existing_assets = vec![Match3DGeneratedItemAsset {
item_id: "match3d-item-1".to_string(),
item_name: "草莓".to_string(),
item_size: Some(MATCH3D_ITEM_SIZE_SMALL.to_string()),
image_src: None,
image_object_key: None,
image_views: Vec::new(),
@@ -7686,6 +7795,7 @@ mod tests {
.map(|index| Match3DGeneratedItemAsset {
item_id: format!("match3d-item-{index}"),
item_name: format!("已有物品{index}"),
item_size: Some(MATCH3D_ITEM_SIZE_LARGE.to_string()),
image_src: None,
image_object_key: None,
image_views: Vec::new(),
@@ -7977,6 +8087,7 @@ mod tests {
let assets = vec![Match3DGeneratedItemAsset {
item_id: "match3d-item-1".to_string(),
item_name: "草莓".to_string(),
item_size: Some(MATCH3D_ITEM_SIZE_SMALL.to_string()),
image_src: None,
image_object_key: None,
image_views: Vec::new(),
@@ -8078,6 +8189,7 @@ mod tests {
let assets = vec![Match3DGeneratedItemAsset {
item_id: "match3d-item-1".to_string(),
item_name: "草莓".to_string(),
item_size: Some(MATCH3D_ITEM_SIZE_SMALL.to_string()),
image_src: None,
image_object_key: None,
image_views: Vec::new(),
@@ -8223,6 +8335,7 @@ mod tests {
Match3DGeneratedItemAsset {
item_id: "match3d-item-2".to_string(),
item_name: "苹果".to_string(),
item_size: Some(MATCH3D_ITEM_SIZE_MEDIUM.to_string()),
image_src: Some("/generated-match3d-assets/s/p/items/i2/image.png".to_string()),
image_object_key: Some(
"generated-match3d-assets/s/p/items/i2/image.png".to_string(),
@@ -8250,6 +8363,7 @@ mod tests {
Match3DGeneratedItemAsset {
item_id: "match3d-item-1".to_string(),
item_name: "草莓".to_string(),
item_size: Some(MATCH3D_ITEM_SIZE_SMALL.to_string()),
image_src: Some("/generated-match3d-assets/s/p/items/i1/image.png".to_string()),
image_object_key: Some(
"generated-match3d-assets/s/p/items/i1/image.png".to_string(),
@@ -8282,6 +8396,7 @@ mod tests {
Match3DGeneratedItemAsset {
item_id: "match3d-item-1".to_string(),
item_name: "草莓".to_string(),
item_size: Some(MATCH3D_ITEM_SIZE_SMALL.to_string()),
image_src: Some("/generated-match3d-assets/s/p/items/i1/image.png".to_string()),
image_object_key: Some(
"generated-match3d-assets/s/p/items/i1/image.png".to_string(),
@@ -8305,6 +8420,7 @@ mod tests {
Match3DGeneratedItemAsset {
item_id: "match3d-item-2".to_string(),
item_name: "苹果".to_string(),
item_size: Some(MATCH3D_ITEM_SIZE_MEDIUM.to_string()),
image_src: Some("/generated-match3d-assets/s/p/items/i2/image.png".to_string()),
image_object_key: Some(
"generated-match3d-assets/s/p/items/i2/image.png".to_string(),
@@ -8328,6 +8444,7 @@ mod tests {
Match3DGeneratedItemAsset {
item_id: "match3d-item-3".to_string(),
item_name: "香蕉".to_string(),
item_size: Some(MATCH3D_ITEM_SIZE_MEDIUM.to_string()),
image_src: Some("/generated-match3d-assets/s/p/items/i3/image.png".to_string()),
image_object_key: None,
image_views: Vec::new(),
@@ -8354,6 +8471,7 @@ mod tests {
.map(|index| Match3DGeneratedItemAsset {
item_id: format!("match3d-item-{index}"),
item_name: format!("物品{index}"),
item_size: Some(MATCH3D_ITEM_SIZE_LARGE.to_string()),
image_src: Some(format!(
"/generated-match3d-assets/s/p/items/i{index}/views/view-01.png"
)),