自动命名仅使用用户需求
Project CI / Frontend tests (pull_request) Successful in 3m17s
Project CI / Backend tests (pull_request) Successful in 7m5s
Project CI / Native shell tests (pull_request) Successful in 17m47s
Project CI / Repository checks (pull_request) Successful in 2m25s

删除自动项目命名专用附件结构体和附件元数据拼接
前端取名请求只传用户需求
更新前后端回归测试以固定输入边界
This commit is contained in:
2026-09-03 11:37:49 +08:00
parent 86867e9903
commit 5ac29ee691
4 changed files with 16 additions and 79 deletions
@@ -14,20 +14,10 @@ const UI_EDITOR_IMAGE_MAX_COUNT: usize = 100;
const AGENT_EDITOR_ASSET_LIBRARY_MAX_ITEMS: usize = 500;
const AGENT_EDITOR_ASSET_ID_MAX_CHARS: usize = 512;
const AUTOMATIC_PROJECT_NAME_MAX_PROMPT_CHARS: usize = 8_000;
const AUTOMATIC_PROJECT_NAME_MAX_ATTACHMENTS: usize = 20;
const AUTOMATIC_PROJECT_NAME_MAX_OUTPUT_TOKENS: u32 = 64;
const AUTOMATIC_PROJECT_NAME_SYSTEM_PROMPT: &str =
include_str!("../prompts/automatic-project-name.md");
// 自动命名只需要附件名称和媒体类型提示。保持独立的窄 DTO 并拒绝未知字段,
// 避免把 Direct 回合附件中的本地路径、导入状态和文件大小引入无项目命名边界。
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub(crate) struct AutomaticProjectNameAttachment {
pub(crate) name: String,
pub(crate) media_type: Option<String>,
}
fn is_chinese_project_name_character(value: char) -> bool {
matches!(
value,
@@ -49,51 +39,19 @@ pub(crate) fn normalize_suggested_project_name(value: &str) -> Option<String> {
normalize_game_creation_project_name(value).ok()
}
pub(crate) fn build_automatic_project_name_prompt(
prompt: &str,
attachments: &[AutomaticProjectNameAttachment],
) -> Result<String, String> {
pub(crate) fn build_automatic_project_name_prompt(prompt: &str) -> Result<String, String> {
let prompt = prompt.trim();
let mut attachment_lines = Vec::new();
for attachment in attachments
.iter()
.take(AUTOMATIC_PROJECT_NAME_MAX_ATTACHMENTS)
{
let name = attachment.name.trim();
if name.is_empty() || name.chars().any(char::is_control) {
continue;
}
let name: String = name.chars().take(160).collect();
let media_type = attachment
.media_type
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty() && !value.chars().any(char::is_control))
.map(|value| value.chars().take(80).collect::<String>());
attachment_lines.push(match media_type {
Some(media_type) => format!("- 附件:{name}{media_type}"),
None => format!("- 附件:{name}"),
});
}
if prompt.is_empty() && attachment_lines.is_empty() {
if prompt.is_empty() {
return Err("首页创作需求为空,不能提炼项目名称".to_string());
}
let bounded_prompt: String = prompt
Ok(prompt
.chars()
.take(AUTOMATIC_PROJECT_NAME_MAX_PROMPT_CHARS)
.collect();
Ok(match attachment_lines.is_empty() {
true => bounded_prompt,
false if prompt.is_empty() => attachment_lines.join("\n"),
false => format!("{bounded_prompt}\n{}", attachment_lines.join("\n")),
})
.collect())
}
async fn request_automatic_project_name(
prompt: &str,
attachments: &[AutomaticProjectNameAttachment],
) -> Result<Option<String>, String> {
let user_prompt = build_automatic_project_name_prompt(prompt, attachments)?;
async fn request_automatic_project_name(prompt: &str) -> Result<Option<String>, String> {
let user_prompt = build_automatic_project_name_prompt(prompt)?;
let app_config = load_game_creator_app_config()?;
if app_config.agent_mode == GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER {
let reply = crate::agent::direct_game_creator_home_codex_chat(
@@ -1992,9 +1950,8 @@ pub(crate) fn check_game_creator_llm_config() -> GameCreatorLlmConfigStatus {
#[tauri::command]
pub(crate) async fn suggest_automatic_project_name(
prompt: String,
attachments: Option<Vec<AutomaticProjectNameAttachment>>,
) -> Result<Option<String>, String> {
request_automatic_project_name(prompt.trim(), attachments.as_deref().unwrap_or_default())
request_automatic_project_name(prompt.trim())
.await
.map_err(|error| redact_agent_runtime_error(Path::new("."), &error, 320))
}
@@ -1649,31 +1649,21 @@ fn automatic_project_name_suggestions_are_normalized_fail_closed() {
}
#[test]
fn automatic_project_name_prompt_is_bounded_and_uses_attachment_metadata_only() {
fn automatic_project_name_prompt_is_bounded_and_uses_user_requirement_only() {
let system_prompt = include_str!("../../prompts/automatic-project-name.md");
assert!(system_prompt.contains("长度为 2 到 16 个字符"));
assert!(system_prompt.contains("不要使用知名作品名、路径、URL、凭据、Markdown、引号或解释"));
let prompt = build_automatic_project_name_prompt(
"做一个在月球邮局送信的解谜游戏",
&[
AutomaticProjectNameAttachment {
name: " 月球邮局参考.png ".to_string(),
media_type: Some("image/png".to_string()),
},
AutomaticProjectNameAttachment {
name: "bad\nname".to_string(),
media_type: Some("image/png".to_string()),
},
],
)
.expect("build prompt");
let prompt = build_automatic_project_name_prompt("做一个在月球邮局送信的解谜游戏")
.expect("build prompt");
assert!(prompt.contains("做一个在月球邮局送信的解谜游戏"));
assert!(prompt.contains("附件:月球邮局参考.pngimage/png"));
assert!(!prompt.contains("bad\nname"));
assert_eq!(prompt, "做一个在月球邮局送信的解谜游戏");
assert_eq!(
build_automatic_project_name_prompt(&"".repeat(8_001)).expect("bound prompt"),
"".repeat(8_000)
);
assert!(build_automatic_project_name_prompt("", &[]).is_err());
assert!(build_automatic_project_name_prompt("").is_err());
}
#[cfg(unix)]
@@ -78,10 +78,6 @@ async function suggestAutomaticProjectName(
try {
return await invoke<string | null>('suggest_automatic_project_name', {
prompt: draft.prompt,
attachments: draft.attachments.map((attachment) => ({
name: attachment.file.name,
mediaType: attachment.file.type || null,
})),
});
} catch {
// 项目命名是增强能力:配置缺失、Provider 失败或非法输出都回退默认名,
@@ -1490,12 +1490,6 @@ export function registerHomeProjectCreationTests() {
if (command === 'suggest_automatic_project_name') {
expect(args).toEqual({
prompt: '按这个角色做游戏',
attachments: [
{
name: '角色参考.png',
mediaType: 'image/png',
},
],
});
return '角色参考游戏';
}