给 UI 设计文档三个工具补上可读的回执明细与入参摘要
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
- 三个工具的持久回执补白名单分支:只放身份、相对路径、计数与回填说明,不再整块变成明细不可用 - 身份字段禁控制字符并限长,相对路径必须归一化后落在 ui/ 下,含宿主路径或 file URI 即失败关闭 - 回填说明按既有口径把绝对路径脱敏成占位符,超长或带控制字符的整条明细失败关闭 - 明细总长仍守 500 字符上限:清单能放多少放多少,放不下的部分用总数表达 - 入参摘要给出设计图逐张身份或目标文档 id,并把三个工具加进可读名单,不再只报哈希 - 补四条用例覆盖可读明细、脱敏、越界路径失败关闭与入参摘要
This commit is contained in:
@@ -677,6 +677,15 @@ fn agent_runtime_action_receipt_safe_detail_with_owner(
|
||||
"revisionAdvanceCount": value.get("revisionAdvanceCount").and_then(serde_json::Value::as_u64).unwrap_or(0),
|
||||
})).ok();
|
||||
}
|
||||
if matches!(
|
||||
observation.tool.as_str(),
|
||||
"ui-design-doc.from-images" | "ui-design-doc.run-workflow"
|
||||
) {
|
||||
return agent_runtime_ui_design_doc_safe_detail(root, observation.detail.as_deref());
|
||||
}
|
||||
if observation.tool == "ui-design-doc.into-js" {
|
||||
return agent_runtime_ui_design_code_safe_detail(root, observation.detail.as_deref());
|
||||
}
|
||||
if observation.tool != "project.patchset" {
|
||||
return None;
|
||||
}
|
||||
@@ -1020,6 +1029,184 @@ pub(in crate::agent) fn agent_runtime_action_receipt_safe_text(
|
||||
Some(sanitized)
|
||||
}
|
||||
|
||||
/// UI 设计文档回执里身份类字段的长度上限:素材 id / 文档 id / 生成文件名都远小于这个值。
|
||||
const AGENT_RUNTIME_UI_DESIGN_SAFE_ID_MAX_CHARS: usize = 96;
|
||||
/// 文档相对路径的长度上限;同时要求必须落在 `ui/` 下,避免把项目别处的路径带进回执。
|
||||
const AGENT_RUNTIME_UI_DESIGN_SAFE_RELATIVE_PATH_MAX_CHARS: usize = 160;
|
||||
/// 单条回填说明的长度上限与总量上限;超出总量的部分不逐条外传,改用总数表达。
|
||||
const AGENT_RUNTIME_UI_DESIGN_SAFE_ERROR_MAX_CHARS: usize = 96;
|
||||
const AGENT_RUNTIME_UI_DESIGN_SAFE_ERROR_CHAR_BUDGET: usize = 240;
|
||||
/// 一份文档最多四张设计图,逐张身份也按同一上限放行。
|
||||
const AGENT_RUNTIME_UI_DESIGN_SAFE_IMAGE_ID_LIMIT: usize = 4;
|
||||
|
||||
/// UI 设计文档链路(新建文档 / 跑工作流)的回执明细:只放身份、相对路径、计数与回填说明。
|
||||
/// 计数与身份必须完全合格;体积较大的清单(设计图身份、回填说明)在回执长度上限内能放多少放多少,
|
||||
/// 放不下的部分用总数补齐,绝不把原始明细整体外传。
|
||||
fn agent_runtime_ui_design_doc_safe_detail(root: &Path, detail: Option<&str>) -> Option<String> {
|
||||
let detail = serde_json::from_str::<serde_json::Value>(detail.unwrap_or_default()).ok()?;
|
||||
let mut safe = serde_json::Map::new();
|
||||
safe.insert(
|
||||
"assetId".to_string(),
|
||||
agent_runtime_action_receipt_identity_text(
|
||||
root,
|
||||
detail.get("assetId")?.as_str()?,
|
||||
AGENT_RUNTIME_UI_DESIGN_SAFE_ID_MAX_CHARS,
|
||||
"assetId",
|
||||
)
|
||||
.ok()?
|
||||
.into(),
|
||||
);
|
||||
safe.insert(
|
||||
"relativePath".to_string(),
|
||||
agent_runtime_ui_design_safe_relative_path(root, detail.get("relativePath")?.as_str()?)?
|
||||
.into(),
|
||||
);
|
||||
for key in [
|
||||
"revision",
|
||||
"revisionAdvanceCount",
|
||||
"recognizedTreeCount",
|
||||
"boundNodeCount",
|
||||
"problematicNodeCount",
|
||||
] {
|
||||
if let Some(value) = detail.get(key) {
|
||||
safe.insert(key.to_string(), value.as_u64()?.into());
|
||||
}
|
||||
}
|
||||
if let Some(value) = detail.get("recoveredFromCheckpoint") {
|
||||
safe.insert(
|
||||
"recoveredFromCheckpoint".to_string(),
|
||||
value.as_bool()?.into(),
|
||||
);
|
||||
}
|
||||
if !agent_runtime_action_receipt_detail_fits(&safe) {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(image_ids) = detail.get("imageIds") {
|
||||
let image_ids = image_ids.as_array()?;
|
||||
if image_ids.is_empty() || image_ids.len() > AGENT_RUNTIME_UI_DESIGN_SAFE_IMAGE_ID_LIMIT {
|
||||
return None;
|
||||
}
|
||||
let image_ids = image_ids
|
||||
.iter()
|
||||
.map(|value| {
|
||||
agent_runtime_action_receipt_identity_text(
|
||||
root,
|
||||
value.as_str()?,
|
||||
AGENT_RUNTIME_UI_DESIGN_SAFE_ID_MAX_CHARS,
|
||||
"imageIds",
|
||||
)
|
||||
.ok()
|
||||
})
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
agent_runtime_action_receipt_insert_while_fits(
|
||||
&mut safe,
|
||||
"imageIds",
|
||||
serde_json::json!(image_ids),
|
||||
);
|
||||
}
|
||||
if let Some(errors) = detail.get("backfillErrors") {
|
||||
let errors = errors.as_array()?;
|
||||
let mut safe_errors = Vec::new();
|
||||
for value in errors {
|
||||
// 回填说明是自由文本:绝对路径按既有口径脱敏成占位符,控制字符或超长则整条明细失败关闭。
|
||||
let raw = value.as_str()?.trim();
|
||||
let error = agent_runtime_action_receipt_safe_text(
|
||||
root,
|
||||
raw,
|
||||
AGENT_RUNTIME_UI_DESIGN_SAFE_ERROR_MAX_CHARS,
|
||||
None,
|
||||
)?;
|
||||
if sanitize_agent_runtime_text(&error, AGENT_RUNTIME_UI_DESIGN_SAFE_ERROR_MAX_CHARS)
|
||||
!= error
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let mut candidate = safe_errors.clone();
|
||||
candidate.push(serde_json::Value::String(error));
|
||||
let mut probe = safe.clone();
|
||||
probe.insert("backfillErrors".to_string(), serde_json::json!(candidate));
|
||||
probe.insert(
|
||||
"backfillErrorCount".to_string(),
|
||||
serde_json::json!(errors.len()),
|
||||
);
|
||||
if !agent_runtime_action_receipt_detail_fits(&probe) {
|
||||
break;
|
||||
}
|
||||
safe_errors = candidate;
|
||||
}
|
||||
if !safe_errors.is_empty()
|
||||
&& serde_json::to_string(&serde_json::json!(safe_errors))
|
||||
.map(|text| text.chars().count())
|
||||
.unwrap_or(0)
|
||||
<= AGENT_RUNTIME_UI_DESIGN_SAFE_ERROR_CHAR_BUDGET
|
||||
{
|
||||
safe.insert("backfillErrors".to_string(), serde_json::json!(safe_errors));
|
||||
}
|
||||
safe.insert(
|
||||
"backfillErrorCount".to_string(),
|
||||
serde_json::json!(errors.len()),
|
||||
);
|
||||
}
|
||||
serde_json::to_string(&serde_json::Value::Object(safe)).ok()
|
||||
}
|
||||
|
||||
/// 代码生成回执:相对路径与计数;导出名清单只用来核对数量,不逐项外传。
|
||||
fn agent_runtime_ui_design_code_safe_detail(root: &Path, detail: Option<&str>) -> Option<String> {
|
||||
let detail = serde_json::from_str::<serde_json::Value>(detail.unwrap_or_default()).ok()?;
|
||||
let relative_path =
|
||||
agent_runtime_ui_design_safe_relative_path(root, detail.get("relativePath")?.as_str()?)?;
|
||||
let tree_count = detail.get("treeCount")?.as_u64()?;
|
||||
let node_count = detail.get("nodeCount")?.as_u64()?;
|
||||
if usize::try_from(tree_count).ok()? != detail.get("treeExports")?.as_array()?.len() {
|
||||
return None;
|
||||
}
|
||||
let safe = serde_json::to_string(&serde_json::json!({
|
||||
"relativePath": relative_path,
|
||||
"treeCount": tree_count,
|
||||
"nodeCount": node_count,
|
||||
}))
|
||||
.ok()?;
|
||||
(sanitize_agent_runtime_text(&safe, AGENT_RUNTIME_ACTION_RECEIPT_SAFE_DETAIL_MAX_CHARS) == safe)
|
||||
.then_some(safe)
|
||||
}
|
||||
|
||||
/// `ui/` 下的项目相对路径:绝对路径、反斜杠、上跳与超长一律拒绝。
|
||||
fn agent_runtime_ui_design_safe_relative_path(root: &Path, value: &str) -> Option<String> {
|
||||
let path = normalize_relative_path(value).ok()?;
|
||||
if !path.starts_with("ui/") {
|
||||
return None;
|
||||
}
|
||||
agent_runtime_action_receipt_identity_text(
|
||||
root,
|
||||
&path,
|
||||
AGENT_RUNTIME_UI_DESIGN_SAFE_RELATIVE_PATH_MAX_CHARS,
|
||||
"relativePath",
|
||||
)
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn agent_runtime_action_receipt_detail_fits(
|
||||
detail: &serde_json::Map<String, serde_json::Value>,
|
||||
) -> bool {
|
||||
serde_json::to_string(&serde_json::Value::Object(detail.clone())).is_ok_and(|text| {
|
||||
sanitize_agent_runtime_text(&text, AGENT_RUNTIME_ACTION_RECEIPT_SAFE_DETAIL_MAX_CHARS)
|
||||
== text
|
||||
})
|
||||
}
|
||||
|
||||
fn agent_runtime_action_receipt_insert_while_fits(
|
||||
detail: &mut serde_json::Map<String, serde_json::Value>,
|
||||
key: &str,
|
||||
value: serde_json::Value,
|
||||
) {
|
||||
let mut candidate = detail.clone();
|
||||
candidate.insert(key.to_string(), value);
|
||||
if agent_runtime_action_receipt_detail_fits(&candidate) {
|
||||
*detail = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn agent_runtime_public_action_input_summary(
|
||||
root: &Path,
|
||||
tool: &str,
|
||||
@@ -1060,6 +1247,9 @@ pub(in crate::agent) fn agent_runtime_public_action_input_summary(
|
||||
| "agent.schedule_ready"
|
||||
| "agent.action_history"
|
||||
| "agent.run_status"
|
||||
| "ui-design-doc.from-images"
|
||||
| "ui-design-doc.run-workflow"
|
||||
| "ui-design-doc.into-js"
|
||||
);
|
||||
if public_shape_only {
|
||||
return agent_runtime_action_receipt_safe_text(root, input_summary, 320, None);
|
||||
@@ -1593,8 +1783,82 @@ pub(crate) fn agent_runtime_tool_action_input_summary(
|
||||
text(&["agentId", "agent_id", "targetAgentId", "target_agent_id"]),
|
||||
text(&["delegationId", "delegation_id"])
|
||||
),
|
||||
// UI 设计文档三兄弟:入参只有设计图引用或目标文档 id,直接给出可读摘要,
|
||||
// 不再退化成"只报哈希"——否则模型看不到自己这次到底传了什么。
|
||||
"ui-design-doc.from-images" => ui_design_doc_images_input_summary(input),
|
||||
"ui-design-doc.run-workflow" | "ui-design-doc.into-js" => format!(
|
||||
"designDocAssetId={}",
|
||||
text(&["designDocAssetId", "design_doc_asset_id", "assetId", "asset_id"])
|
||||
),
|
||||
_ => String::new(),
|
||||
};
|
||||
let summary = redact_agent_runtime_project_paths(root, &summary, 320);
|
||||
(!summary.trim().is_empty()).then_some(summary)
|
||||
}
|
||||
|
||||
/// 新建文档工具的设计图入参摘要:逐张给出身份(素材 id 或相对路径),绝对路径按既有口径降级成拒绝标记。
|
||||
fn ui_design_doc_images_input_summary(input: &serde_json::Value) -> String {
|
||||
let Some(images) = input.get("images").and_then(serde_json::Value::as_array) else {
|
||||
return String::new();
|
||||
};
|
||||
let described = images
|
||||
.iter()
|
||||
.map(|image| {
|
||||
if let Some(asset_id) = image.get("assetId").and_then(serde_json::Value::as_str) {
|
||||
return format!("assetId={asset_id}");
|
||||
}
|
||||
match image.get("path").and_then(serde_json::Value::as_str) {
|
||||
Some(path) if Path::new(path).is_absolute() => {
|
||||
"[absolute path rejected]".to_string()
|
||||
}
|
||||
Some(path) => format!("path={path}"),
|
||||
None => "input".to_string(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
format!("images={} · {described}", images.len())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod ui_design_doc_public_input_summary_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ui_design_doc_input_summaries_are_readable_not_hashed() {
|
||||
let root = Path::new("/nonexistent-ui-design-doc-input-summary-project");
|
||||
for (tool, summary) in [
|
||||
(
|
||||
"ui-design-doc.from-images",
|
||||
"images=2 · assetId=image-1, path=assets/two.png",
|
||||
),
|
||||
(
|
||||
"ui-design-doc.run-workflow",
|
||||
"designDocAssetId=generated-ui-design-1",
|
||||
),
|
||||
(
|
||||
"ui-design-doc.into-js",
|
||||
"designDocAssetId=generated-ui-design-1",
|
||||
),
|
||||
] {
|
||||
let public = agent_runtime_public_action_input_summary(root, tool, Some(summary))
|
||||
.expect("input summary");
|
||||
assert_eq!(public, summary, "{tool} 的入参不能被压成摘要哈希");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_images_input_summary_lists_each_image_reference() {
|
||||
let input = serde_json::json!({
|
||||
"images": [
|
||||
{"assetId": "image-1"},
|
||||
{"path": "assets/two.png"},
|
||||
{"path": "/tmp/host-only.png"},
|
||||
]
|
||||
});
|
||||
assert_eq!(
|
||||
ui_design_doc_images_input_summary(&input),
|
||||
"images=3 · assetId=image-1, path=assets/two.png, [absolute path rejected]"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2230,3 +2230,145 @@ fn seed_refresh_preserves_completed_visual_tasks_when_registered_file_is_missing
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_design_doc_receipts_keep_safe_detail_instead_of_dropping_it() {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(&root, "project-1", "UI 设计文档回执脱敏测试")
|
||||
.expect("project init");
|
||||
|
||||
let from_images = AgentRuntimeToolObservation {
|
||||
tool: "ui-design-doc.from-images".to_string(),
|
||||
status: "ok".to_string(),
|
||||
summary: "已新建 UI 设计文档".to_string(),
|
||||
detail: Some(
|
||||
serde_json::json!({
|
||||
"assetId": "generated-ui-design-1",
|
||||
"relativePath": "ui/UI 设计 1.json",
|
||||
"imageIds": ["image-1", "image-2"],
|
||||
"revisionAdvanceCount": 2,
|
||||
})
|
||||
.to_string(),
|
||||
),
|
||||
};
|
||||
let public = agent_runtime_action_receipt_public_safe_detail_for_test(&root, &from_images)
|
||||
.expect("from-images 必须留下可用明细");
|
||||
assert!(public.chars().count() <= 500, "{public}");
|
||||
let public = serde_json::from_str::<Value>(&public).expect("parse from-images detail");
|
||||
assert_eq!(public["assetId"], "generated-ui-design-1");
|
||||
assert_eq!(public["relativePath"], "ui/UI 设计 1.json");
|
||||
assert_eq!(public["imageIds"].as_array().map(Vec::len), Some(2));
|
||||
assert_eq!(public["revisionAdvanceCount"], 2);
|
||||
|
||||
let run_workflow = AgentRuntimeToolObservation {
|
||||
tool: "ui-design-doc.run-workflow".to_string(),
|
||||
status: "ok".to_string(),
|
||||
summary: "UI 设计文档工作流完成".to_string(),
|
||||
detail: Some(
|
||||
serde_json::json!({
|
||||
"assetId": "generated-ui-design-1",
|
||||
"relativePath": "ui/UI 设计 1.json",
|
||||
"revision": 3,
|
||||
"recoveredFromCheckpoint": true,
|
||||
"recognizedTreeCount": 2,
|
||||
"boundNodeCount": 9,
|
||||
"problematicNodeCount": 1,
|
||||
"backfillErrors": [
|
||||
"未能登记自动切分素材图片:assets/cut.png",
|
||||
format!("读取资源失败:{}", root.join("assets/cut.png").display()),
|
||||
],
|
||||
"revisionAdvanceCount": 3,
|
||||
})
|
||||
.to_string(),
|
||||
),
|
||||
};
|
||||
let public = agent_runtime_action_receipt_public_safe_detail_for_test(&root, &run_workflow)
|
||||
.expect("run-workflow 必须留下可用明细");
|
||||
assert!(public.chars().count() <= 500, "{public}");
|
||||
assert!(
|
||||
!public.contains(root.to_string_lossy().as_ref()),
|
||||
"回填说明里的宿主路径必须脱敏:{public}"
|
||||
);
|
||||
let public = serde_json::from_str::<Value>(&public).expect("parse run-workflow detail");
|
||||
assert_eq!(public["recoveredFromCheckpoint"], true);
|
||||
assert_eq!(public["problematicNodeCount"], 1);
|
||||
assert_eq!(public["backfillErrorCount"], 2);
|
||||
assert_eq!(
|
||||
public["backfillErrors"][0],
|
||||
"未能登记自动切分素材图片:assets/cut.png"
|
||||
);
|
||||
|
||||
let into_js = AgentRuntimeToolObservation {
|
||||
tool: "ui-design-doc.into-js".to_string(),
|
||||
status: "ok".to_string(),
|
||||
summary: "已生成 UI 设计代码".to_string(),
|
||||
detail: Some(
|
||||
serde_json::json!({
|
||||
"relativePath": "ui/generated-ui-design-1-0123456789abcdef.js",
|
||||
"treeExports": ["treeA", "treeB"],
|
||||
"treeCount": 2,
|
||||
"nodeCount": 12,
|
||||
})
|
||||
.to_string(),
|
||||
),
|
||||
};
|
||||
let public = agent_runtime_action_receipt_public_safe_detail_for_test(&root, &into_js)
|
||||
.expect("into-js 必须留下可用明细");
|
||||
let public = serde_json::from_str::<Value>(&public).expect("parse into-js detail");
|
||||
assert_eq!(public["treeCount"], 2);
|
||||
assert_eq!(public["nodeCount"], 12);
|
||||
assert!(
|
||||
public.get("treeExports").is_none(),
|
||||
"导出名清单只用于核对数量,不逐项外传:{public}"
|
||||
);
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_design_doc_receipts_reject_out_of_scope_paths_and_malformed_detail() {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(&root, "project-1", "UI 设计文档回执边界测试")
|
||||
.expect("project init");
|
||||
|
||||
let outside_ui_directory = AgentRuntimeToolObservation {
|
||||
tool: "ui-design-doc.from-images".to_string(),
|
||||
status: "ok".to_string(),
|
||||
summary: "已新建 UI 设计文档".to_string(),
|
||||
detail: Some(
|
||||
serde_json::json!({
|
||||
"assetId": "generated-ui-design-1",
|
||||
"relativePath": "game/UI 设计 1.json",
|
||||
"imageIds": ["image-1"],
|
||||
"revisionAdvanceCount": 1,
|
||||
})
|
||||
.to_string(),
|
||||
),
|
||||
};
|
||||
assert!(
|
||||
agent_runtime_action_receipt_public_safe_detail_for_test(&root, &outside_ui_directory)
|
||||
.is_none(),
|
||||
"不在 ui/ 下的相对路径必须失败关闭"
|
||||
);
|
||||
|
||||
let absolute_document_id = AgentRuntimeToolObservation {
|
||||
tool: "ui-design-doc.run-workflow".to_string(),
|
||||
status: "ok".to_string(),
|
||||
summary: "UI 设计文档工作流完成".to_string(),
|
||||
detail: Some(
|
||||
serde_json::json!({
|
||||
"assetId": root.join("ui/UI 设计 1.json").to_string_lossy(),
|
||||
"relativePath": "ui/UI 设计 1.json",
|
||||
"revisionAdvanceCount": 1,
|
||||
})
|
||||
.to_string(),
|
||||
),
|
||||
};
|
||||
assert!(
|
||||
agent_runtime_action_receipt_public_safe_detail_for_test(&root, &absolute_document_id)
|
||||
.is_none(),
|
||||
"身份字段不能是宿主路径"
|
||||
);
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
@@ -9275,6 +9275,15 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 影响面:`apps/ai-game-creator-shell/src-tauri/src/{main.rs,ui_editor/**}`、`src/features/ui-editor/**`、`src/view/ui-editor/**`、`src/view/project-development/index.tsx`、`tests/{uiEditorPage,uiEditorState,previewWorkspaceZoom}.test.*`、`docs/technical/【技术方案】UI编辑器代码地图与模块职责-2026-09-23.md`、`docs/【技术方案】UI工作流资源桥接与Runtime执行-2026-08-24.md`、`docs/technical/【设计】UI编辑器工作流完成通知弹窗-2026-09-04.md`、`docs/technical/【前端架构】UI编辑会话模块边界-2026-08-19.md`。
|
||||
- 验证:`cargo check` 与 `cargo test --bin genarrative-ai-game-creator-shell ui_editor`(160 passed)通过,ts-rs 重新导出 `types/UIDesignImage.ts` 并删除三个已退役类型;`npx vitest run` 定向 `uiEditorPage` / `uiEditorState` / `uiDesignStateStore` / `previewWorkspaceZoom` / `appSurface` 全绿;AGC `tsc --noEmit`、改动文件 eslint、`cargo fmt --check`、`check:encoding`、`git diff --check` 通过。整套 Rust 测试在本容器仍有 60 条环境性失败(`/sbin -> usr/bin` 让 `command.exec` 沙箱 merged-usr 预检失败),与本次改动无关。
|
||||
|
||||
## 2026-09-24 UI 设计文档三个工具的持久回执明细与入参摘要改为可读白名单
|
||||
|
||||
- 背景:动作回执的"安全明细"是一份白名单——只有认识的工具才把明细整理成安全字段。`ui.workflow.run` 退役时删掉了它那段分支,接管的 `ui-design-doc.*` 三个工具没补,于是统一落到兜底:明细整块变成 `detailUnavailable`,模型与审计都看不到;入参那一栏也只剩哈希。原始明细既不落盘也不给模型,所以**没有宿主路径泄露**,丢的是可用性(例如切分没登记上的素材清单)。
|
||||
- 决策:按既有白名单口径给三个工具各补一条分支。`from-images` 放 `assetId` / `relativePath` / `imageIds` / `revisionAdvanceCount`;`run-workflow` 再放文档 `revision`、恢复标记、识别与绑定计数、`backfillErrors` 与总数;`into-js` 只放相对路径与计数(导出名清单只用来核对数量,不逐项外传)。
|
||||
- 决策(校验口径):身份字段走 `agent_runtime_action_receipt_identity_text`(禁控制字符、限长、含 file URI 或绝对路径即失败关闭);相对路径必须归一化后落在 `ui/` 下;回填说明是自由文本,按既有口径把绝对路径脱敏成占位符,含控制字符或超长则整条明细失败关闭。
|
||||
- 决策(体积):回执明细总长仍受 `AGENT_RUNTIME_ACTION_RECEIPT_SAFE_DETAIL_MAX_CHARS`(500)约束。设计图身份与回填说明按长度上限能放多少放多少,放不下的部分用 `backfillErrorCount` 表达总数;必需字段本身就超限时整条明细不可用。
|
||||
- 决策(入参摘要):`agent_runtime_tool_action_input_summary` 为三个工具产出可读摘要(设计图逐张身份或目标文档 id),并把工具名加进 `agent_runtime_public_action_input_summary` 的可读名单,不再退化成只报哈希。
|
||||
- 验证方式:`tests::runtime_actions::action_execution::ui_design_doc_receipts_*` 两条用例(三个工具都能留下可用明细且不超长;宿主路径被脱敏;`ui/` 之外与宿主路径身份失败关闭)与 `action_audit::ui_design_doc_public_input_summary_tests` 两条用例;`cargo test --bin genarrative-ai-game-creator-shell receipt` 55 条全绿。
|
||||
|
||||
## 2026-09-24 UI 设计文档三个工具纳入项目变更门禁(成功返回即算改过项目)
|
||||
|
||||
- 背景:`ui.workflow.run` 退役时,项目变更门禁的两处工具名单(`agent/runtime_actions/project_gates.rs` 的 `is_agent_runtime_project_mutation_observation` 与 `agent_runtime_observation_advances_project_revision`)只删未补,新接管的 `ui-design-doc.from-images` / `ui-design-doc.run-workflow` / `ui-design-doc.into-js` 都没登记。后果:改完项目可能被判定"没改过",于是不要求验证就判完成、自动模式的 liveness 判据看不到进展;`agent_runtime_pending_expected_project_revision` 少算推进量又会误报 `pending_project_revision_drift`("并行项目变更使旧动作过期")。
|
||||
|
||||
Reference in New Issue
Block a user