From 152041cd1971d8bcbc906b14ca463c9bd6caa007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Tue, 15 Sep 2026 16:01:45 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=A0=E9=99=A4=E6=97=A7=20DirectProject=20?= =?UTF-8?q?=E5=BC=95=E7=94=A8=E5=8D=8F=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除 DirectCodexTurnReference 平行 DTO 与渲染路径 将引用数量上限收回 canonical user item 校验模块 --- .../src-tauri/src/agent.rs | 2 - .../src/agent/direct_codex_references.rs | 382 ------------------ .../direct_codex_user_item/validation.rs | 3 +- 3 files changed, 2 insertions(+), 385 deletions(-) delete mode 100644 apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_references.rs diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent.rs b/apps/ai-game-creator-shell/src-tauri/src/agent.rs index 3940aeeb7..d084e6174 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent.rs @@ -16,7 +16,6 @@ mod design_runtime; mod design_tools; mod direct_codex_attachments; mod direct_codex_audit; -mod direct_codex_references; mod direct_codex_user_item; mod direct_project_history; mod direct_project_turn_history; @@ -46,7 +45,6 @@ pub(crate) use codex_provider_proxy::*; pub(crate) use design_runtime::*; pub(crate) use direct_codex_attachments::*; pub(crate) use direct_codex_audit::*; -pub(crate) use direct_codex_references::*; pub(crate) use direct_codex_user_item::*; pub(crate) use direct_project_history::*; pub(crate) use direct_project_turn_history::*; diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_references.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_references.rs deleted file mode 100644 index f1b78b1ca..000000000 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_references.rs +++ /dev/null @@ -1,382 +0,0 @@ -use super::*; - -pub(crate) const MAX_DIRECT_CODEX_REFERENCES: usize = 32; -const MAX_DIRECT_CODEX_REFERENCE_ID_CHARS: usize = 200; -const MAX_DIRECT_CODEX_REFERENCE_LABEL_CHARS: usize = 160; -const MAX_DIRECT_CODEX_REFERENCE_SOURCE_CHARS: usize = 32; -const MAX_DIRECT_CODEX_REFERENCE_ELEMENT_CHARS: usize = 80; -const MAX_DIRECT_CODEX_REFERENCE_TEXT_CHARS: usize = 240; - -const DIRECT_CODEX_REFERENCE_HEADER: &str = - "[本轮用户引用素材:以下均为当前项目已确认的安全引用。请使用稳定资源 ID 和项目相对路径读取,不要读取或输出其它路径。]"; - -#[derive(Clone, Debug, serde::Deserialize)] -#[serde(rename_all = "camelCase", tag = "type")] -pub(crate) enum DirectCodexTurnReference { - #[serde(rename = "resource")] - Resource(DirectCodexResourceReference), - #[serde(rename = "runtime-region")] - RuntimeRegion(DirectCodexRuntimeRegionReference), -} - -#[derive(Clone, Debug, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct DirectCodexResourceReference { - pub(crate) resource_id: String, - #[serde(default)] - pub(crate) label: Option, - #[serde(default)] - pub(crate) source: Option, -} - -#[derive(Clone, Debug, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct DirectCodexRuntimeRegionReference { - #[serde(default)] - pub(crate) label: Option, - #[serde(default)] - pub(crate) run_id: Option, - #[serde(default)] - pub(crate) version_id: Option, - #[serde(default)] - pub(crate) element_tag: Option, - #[serde(default)] - pub(crate) element_role: Option, - #[serde(default)] - pub(crate) text: Option, - #[serde(default)] - pub(crate) width: Option, - #[serde(default)] - pub(crate) height: Option, - #[serde(default)] - pub(crate) resource_ids: Vec, -} - -fn sanitize_reference_text(value: &str, max_chars: usize) -> Option { - let sanitized = value - .trim() - .chars() - .filter(|character| !character.is_control()) - .take(max_chars) - .collect::(); - if sanitized.is_empty() { - None - } else { - Some(sanitized) - } -} - -fn sanitize_reference_label(value: Option<&str>) -> Option { - value.and_then(|value| sanitize_reference_text(value, MAX_DIRECT_CODEX_REFERENCE_LABEL_CHARS)) -} - -fn sanitize_reference_source(value: Option<&str>) -> Option { - let value = sanitize_reference_text(value?, MAX_DIRECT_CODEX_REFERENCE_SOURCE_CHARS)?; - if value - .chars() - .all(|character| character.is_ascii_alphanumeric() || matches!(character, '-' | '_' | '.')) - { - Some(value) - } else { - None - } -} - -fn sanitize_reference_element(value: Option<&str>) -> Option { - let value = sanitize_reference_text(value?, MAX_DIRECT_CODEX_REFERENCE_ELEMENT_CHARS)?; - if value - .chars() - .all(|character| character.is_ascii_alphanumeric() || matches!(character, '-' | '_' | '.')) - { - Some(value) - } else { - None - } -} - -fn sanitize_reference_dimension(value: Option) -> Option { - value - .filter(|value| value.is_finite() && *value > 0.0) - .map(|value| value.round().clamp(1.0, 100_000.0) as u32) -} - -fn asset_display_label(asset: &GameCreationAppAssetManifestEntry) -> String { - let basename = asset - .local_path - .rsplit(['/', '\\']) - .next() - .unwrap_or(&asset.id); - let without_extension = basename - .rsplit_once('.') - .map(|(name, _)| name) - .unwrap_or(basename) - .trim(); - if without_extension.is_empty() { - asset.id.clone() - } else { - without_extension - .chars() - .filter(|character| !character.is_control()) - .take(MAX_DIRECT_CODEX_REFERENCE_LABEL_CHARS) - .collect() - } -} - -fn validate_resource_reference_id(value: &str) -> Result { - let resource_id = value.trim(); - if resource_id.is_empty() - || resource_id.chars().count() > MAX_DIRECT_CODEX_REFERENCE_ID_CHARS - || resource_id.chars().any(char::is_control) - { - return Err("引用的素材 ID 无效,请移除后重新选择".to_string()); - } - Ok(resource_id.to_string()) -} - -fn render_resource_reference_line( - manifest: &GameCreationAppManifest, - reference: &DirectCodexResourceReference, -) -> Result { - let resource_id = validate_resource_reference_id(&reference.resource_id)?; - let asset = manifest - .assets - .iter() - .find(|asset| asset.id == resource_id) - .ok_or_else(|| "引用的素材已不存在,请移除后重新选择".to_string())?; - let local_path = sanitize_attachment_local_path(&asset.local_path) - .ok_or_else(|| "引用的素材路径无效,请移除后重新选择".to_string())?; - let label = sanitize_reference_label(reference.label.as_deref()) - .unwrap_or_else(|| asset_display_label(asset)); - let source = sanitize_reference_source(reference.source.as_deref()) - .unwrap_or_else(|| "unknown".to_string()); - Ok(format!( - "- 素材 ID:{resource_id};名称:{label};类型:{};媒体类型:{};项目路径:{local_path};来源:{source}", - asset.kind, asset.media_type - )) -} - -fn render_runtime_region_reference_line( - manifest: &GameCreationAppManifest, - reference: &DirectCodexRuntimeRegionReference, -) -> Result { - let label = sanitize_reference_label(reference.label.as_deref()) - .unwrap_or_else(|| "运行画面区域".to_string()); - let run_id = sanitize_reference_source(reference.run_id.as_deref()); - let version_id = sanitize_reference_source(reference.version_id.as_deref()); - let element_tag = sanitize_reference_element(reference.element_tag.as_deref()); - let element_role = sanitize_reference_element(reference.element_role.as_deref()); - let text = reference - .text - .as_deref() - .and_then(|value| sanitize_reference_text(value, MAX_DIRECT_CODEX_REFERENCE_TEXT_CHARS)); - let width = sanitize_reference_dimension(reference.width); - let height = sanitize_reference_dimension(reference.height); - - // `resourceIds` 是本模块唯一由客户端直接给出、且自身还是一条列表的字段:条数不设界时, - // 每个 id 都要扫一遍 manifest(O(assets)),注入提示词的 `关联素材 ID:…` 行也会跟着无界 - // 变长(最终只被 32 MiB 写入护栏拦下,变成一条和原因无关的连接级错误)。这里按模块的 - // 失败关闭口径直接拒绝超限,而不是静默丢掉用户选中的关联。 - if reference.resource_ids.len() > MAX_DIRECT_CODEX_REFERENCES { - return Err(format!( - "运行画面区域一次最多关联 {MAX_DIRECT_CODEX_REFERENCES} 个素材,请重新点选" - )); - } - let mut related_resource_ids = Vec::new(); - for resource_id in &reference.resource_ids { - let resource_id = validate_resource_reference_id(resource_id)?; - if !manifest.assets.iter().any(|asset| asset.id == resource_id) { - return Err("运行画面引用的素材已变化,请重新点选".to_string()); - } - // 去重:同一个 id 在注入提示词里重复出现没有信息量,只是把行撑长。 - // 条数已按上限收口,所以这里的逐项比较不会退化成大面积二次扫描。 - if !related_resource_ids.contains(&resource_id) { - related_resource_ids.push(resource_id); - } - } - - let mut parts = vec![format!("名称:{label}")]; - if let Some(run_id) = run_id { - parts.push(format!("运行标识:{run_id}")); - } - if let Some(version_id) = version_id { - parts.push(format!("版本标识:{version_id}")); - } - if let Some(element_tag) = element_tag { - parts.push(format!("元素:{element_tag}")); - } - if let Some(element_role) = element_role { - parts.push(format!("角色:{element_role}")); - } - if let Some(text) = text { - parts.push(format!("文本摘要:{text}")); - } - if let (Some(width), Some(height)) = (width, height) { - parts.push(format!("尺寸:{width}x{height}")); - } - if !related_resource_ids.is_empty() { - parts.push(format!("关联素材 ID:{}", related_resource_ids.join(","))); - } - Ok(format!("- 运行画面区域:{}", parts.join(";"))) -} - -pub(crate) fn render_direct_codex_references_section( - root: &Path, - references: &[DirectCodexTurnReference], -) -> Result, String> { - if references.is_empty() { - return Ok(None); - } - if references.len() > MAX_DIRECT_CODEX_REFERENCES { - return Err(format!("一次最多引用 {MAX_DIRECT_CODEX_REFERENCES} 个素材")); - } - let manifest = read_manifest_for_project(root)?; - let mut lines = Vec::with_capacity(references.len()); - for reference in references { - lines.push(match reference { - DirectCodexTurnReference::Resource(reference) => { - render_resource_reference_line(&manifest, reference)? - } - DirectCodexTurnReference::RuntimeRegion(reference) => { - render_runtime_region_reference_line(&manifest, reference)? - } - }); - } - Ok(Some( - std::iter::once(DIRECT_CODEX_REFERENCE_HEADER.to_string()) - .chain(lines) - .collect::>() - .join("\n"), - )) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn fixture_project() -> tempfile::TempDir { - let directory = tempfile::tempdir().expect("temp project"); - let root = directory.path(); - std::fs::create_dir_all(root.join(".agent")).expect("create agent dir"); - let mut manifest = new_game_creation_app_manifest("project-1", "测试项目"); - manifest.assets.push(GameCreationAppAssetManifestEntry { - id: "asset-hero".to_string(), - kind: "character".to_string(), - media_type: "image/png".to_string(), - local_path: "assets/hero.png".to_string(), - source: GameCreationAppAssetSource { - kind: GameCreationAppAssetSourceKind::Uploaded, - canvas_project_id: None, - resource_id: None, - asset_object_id: None, - task_id: None, - prompt: None, - model: None, - generation_route: None, - generation_kind: None, - reference_resource_ids: Vec::new(), - }, - image_sequence_frames: None, - image_sequence_duration_ms: None, - category: game_creation_app_asset_category_for_kind("character"), - tags: Vec::new(), - }); - write_manifest(&root.join(".agent/manifest.json"), &manifest).expect("write manifest"); - directory - } - - #[test] - fn resource_reference_uses_manifest_identity_and_never_accepts_client_paths() { - let project = fixture_project(); - let reference: DirectCodexTurnReference = serde_json::from_str( - r#"{"type":"resource","resourceId":"asset-hero","label":"主角","source":"asset-picker","localPath":"C:\\secret.png"}"#, - ) - .expect("reference json"); - let section = render_direct_codex_references_section( - project.path(), - std::slice::from_ref(&reference), - ) - .expect("render") - .expect("section"); - assert!(section.contains("素材 ID:asset-hero")); - assert!(section.contains("名称:主角")); - assert!(section.contains("项目路径:assets/hero.png")); - assert!(!section.contains("C:\\secret.png")); - } - - #[test] - fn deleted_resource_fails_closed() { - let project = fixture_project(); - let reference: DirectCodexTurnReference = serde_json::from_str( - r#"{"type":"resource","resourceId":"asset-missing","label":"不存在"}"#, - ) - .expect("reference json"); - let error = render_direct_codex_references_section( - project.path(), - std::slice::from_ref(&reference), - ) - .expect_err("missing resource"); - assert_eq!(error, "引用的素材已不存在,请移除后重新选择"); - } - - #[test] - fn runtime_region_keeps_only_safe_summary_and_existing_resource_ids() { - let project = fixture_project(); - let reference: DirectCodexTurnReference = serde_json::from_str( - r#"{"type":"runtime-region","label":"开始按钮","runId":"run-1","elementTag":"button","elementRole":"button","text":"开始游戏","width":120.4,"height":40.2,"resourceIds":["asset-hero"],"html":"