接入 UI 文档引用代码上下文生成

Direct Codex 引用 ui-design-doc 时生成带文档的 JS 文件

生成成功或失败信息追加到当前 prompt

补充成功、失败与 ui-prototype 隔离测试
This commit is contained in:
2026-09-14 20:33:13 +08:00
parent 470e85c0ff
commit 9cd1a94369
@@ -1,4 +1,8 @@
use super::*;
use crate::ui_editor::persistence::{
generate_ui_design_code_at, GenerateUiDesignCodeInput, UI_DESIGN_DOC_ASSET_KIND,
UI_DESIGN_DOC_MEDIA_TYPE,
};
pub(crate) const MAX_DIRECT_CODEX_REFERENCES: usize = 32;
const MAX_DIRECT_CODEX_REFERENCE_ID_CHARS: usize = 200;
@@ -138,19 +142,30 @@ fn validate_resource_reference_id(value: &str) -> Result<String, String> {
/// Keep this separate from the generic resource renderer so UI-specific
/// instructions/metadata can evolve without changing other asset kinds.
fn render_ui_design_reference_line(
root: &Path,
manifest: &GameCreationAppManifest,
asset: &GameCreationAppAssetManifestEntry,
resource_id: &str,
label: &str,
local_path: &str,
source: &str,
) -> String {
let context = match generate_ui_design_code_at(GenerateUiDesignCodeInput {
project_path: root.to_string_lossy().into_owned(),
expected_project_id: manifest.project_id.clone(),
asset_id: resource_id.to_string(),
}) {
Ok(result) => format!("请先阅读生成的带有文档的代码片段: {}", result.relative_path),
Err(error) => format!("生成代码遇到错误{error}"),
};
format!(
"- 素材 ID{resource_id};名称:{label};类型:{};媒体类型:{};项目路径:{local_path};来源:{source}",
asset.kind, asset.media_type
)
) + "\n" + &context
}
fn render_resource_reference_line(
root: &Path,
manifest: &GameCreationAppManifest,
reference: &DirectCodexResourceReference,
) -> Result<String, String> {
@@ -166,12 +181,12 @@ fn render_resource_reference_line(
.unwrap_or_else(|| asset_display_label(asset));
let source = sanitize_reference_source(reference.source.as_deref())
.unwrap_or_else(|| "unknown".to_string());
let is_ui_design = matches!(
asset.kind.to_ascii_lowercase().as_str(),
"ui" | "ui-design" | "ui-prototype"
);
let is_ui_design =
asset.kind == UI_DESIGN_DOC_ASSET_KIND && asset.media_type == UI_DESIGN_DOC_MEDIA_TYPE;
if is_ui_design {
return Ok(render_ui_design_reference_line(
root,
manifest,
asset,
&resource_id,
&label,
@@ -264,7 +279,7 @@ pub(crate) fn render_direct_codex_references_section(
for reference in references {
lines.push(match reference {
DirectCodexTurnReference::Resource(reference) => {
render_resource_reference_line(&manifest, reference)?
render_resource_reference_line(root, &manifest, reference)?
}
DirectCodexTurnReference::RuntimeRegion(reference) => {
render_runtime_region_reference_line(&manifest, reference)?
@@ -282,6 +297,7 @@ pub(crate) fn render_direct_codex_references_section(
#[cfg(test)]
mod tests {
use super::*;
use crate::ui_editor::persistence::initialize_ui_design_state_at;
fn fixture_project() -> tempfile::TempDir {
let directory = tempfile::tempdir().expect("temp project");
@@ -314,6 +330,128 @@ mod tests {
directory
}
fn add_ui_design_doc(project: &tempfile::TempDir) -> String {
let root = project.path();
let relative_path = "ui/design.json";
let absolute_path = root.join(relative_path);
std::fs::create_dir_all(absolute_path.parent().expect("ui parent"))
.expect("create ui parent");
std::fs::write(&absolute_path, b"").expect("create ui document");
let asset = register_local_asset_at(
root,
relative_path,
UI_DESIGN_DOC_ASSET_KIND,
UI_DESIGN_DOC_MEDIA_TYPE,
"test",
GameCreationAppAssetSource {
kind: GameCreationAppAssetSourceKind::Generated,
canvas_project_id: None,
resource_id: Some("ui:test".to_string()),
asset_object_id: None,
task_id: None,
prompt: None,
model: None,
generation_route: None,
generation_kind: None,
reference_resource_ids: Vec::new(),
},
)
.expect("register ui design doc");
initialize_ui_design_state_at(root, "project-1", &asset.id)
.expect("initialize ui design doc");
asset.id
}
#[test]
fn ui_design_doc_reference_generates_documented_code_context() {
let project = fixture_project();
let asset_id = add_ui_design_doc(&project);
let reference = DirectCodexTurnReference::Resource(DirectCodexResourceReference {
resource_id: asset_id,
label: Some("界面设计".to_string()),
source: Some("asset-picker".to_string()),
});
let section = render_direct_codex_references_section(project.path(), &[reference])
.expect("render")
.expect("section");
assert!(section.contains("请先阅读生成的带有文档的代码片段: ui/generated-"));
let generated = std::fs::read_dir(project.path().join("ui"))
.expect("read ui dir")
.filter_map(Result::ok)
.find(|entry| {
entry
.file_name()
.to_string_lossy()
.starts_with("generated-")
})
.expect("generated code");
let code = std::fs::read_to_string(generated.path()).expect("read generated code");
assert!(code.contains("这是从用户手动调整过的UI设计文档生成的html片段"));
}
#[test]
fn ui_design_doc_generation_errors_stay_in_prompt_and_do_not_abort_section() {
let project = fixture_project();
let asset_id = add_ui_design_doc(&project);
let manifest = read_manifest_for_project(project.path()).expect("read manifest");
let asset = manifest
.assets
.iter()
.find(|asset| asset.id == asset_id)
.unwrap();
std::fs::write(project.path().join(&asset.local_path), b"not-json")
.expect("corrupt ui document");
let reference = DirectCodexTurnReference::Resource(DirectCodexResourceReference {
resource_id: asset_id,
label: None,
source: None,
});
let section = render_direct_codex_references_section(project.path(), &[reference])
.expect("render")
.expect("section");
assert!(section.contains("素材 ID"));
assert!(section.contains("生成代码遇到错误"));
}
#[test]
fn ui_prototype_image_does_not_trigger_code_generation() {
let project = fixture_project();
let root = project.path();
let relative_path = "assets/ui-prototype.png";
std::fs::create_dir_all(root.join("assets")).expect("assets dir");
std::fs::write(root.join(relative_path), b"png").expect("prototype image");
let asset = register_local_asset_at(
root,
relative_path,
"ui-prototype",
"image/png",
"test",
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(),
},
)
.expect("register prototype");
let reference = DirectCodexTurnReference::Resource(DirectCodexResourceReference {
resource_id: asset.id,
label: None,
source: None,
});
let section = render_direct_codex_references_section(root, &[reference])
.expect("render")
.expect("section");
assert!(!section.contains("请先阅读生成的带有文档的代码片段"));
assert!(!root.join("ui").exists());
}
#[test]
fn resource_reference_uses_manifest_identity_and_never_accepts_client_paths() {
let project = fixture_project();