From 5ea2dee663fc995f40aad57657bb65283dd9735b Mon Sep 17 00:00:00 2001 From: kdletters Date: Tue, 4 Aug 2026 13:00:31 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=BE=AA=E7=8E=AF=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E6=8A=95=E5=BD=B1=E8=BA=AB=E4=BB=BD=E4=B8=8E=E5=86=85?= =?UTF-8?q?=E8=81=94=E4=BD=93=E7=A7=AF=E9=97=A8=E7=A6=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按来源模块和原始根绑定传播投影声明身份 回流去重前把引用改接到既有 canonical 并拒绝未收敛固定点 将 inline module 纳入累计二 MiB JavaScript 预算 新增双端命名捕获循环与内联超限回归 同步技术方案与共享决策记录 --- .../runtime_protocol/autonomous_completion.rs | 129 ++++++++++++++++-- .../autonomous_completion_contract_tests.rs | 26 +++- .../shared-memory/decision-log.md | 1 + ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 2 + 4 files changed, 143 insertions(+), 15 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs index 5900681f4..7040cd474 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs @@ -6423,26 +6423,74 @@ pub(in crate::agent) fn javascript_module_binding_projection( projection } +type JavascriptProjectionDeclarationIdentity = (String, String); + +fn javascript_projection_declaration_identities( + module: &str, + analysis: &JavascriptModuleAnalysis, +) -> BTreeMap { + analysis + .root_bindings + .iter() + .chain(analysis.synthetic_declarations.keys()) + .map(|binding| (binding.clone(), (module.to_string(), binding.clone()))) + .collect() +} + fn javascript_remove_duplicate_projection_declarations( projection: &mut String, - importer: &str, + projection_identities: &mut BTreeMap, + included_names: &BTreeMap, ) -> bool { let projection_ranges = javascript_top_level_declaration_ranges(projection); - let importer_ranges = javascript_top_level_declaration_ranges(importer); - let mut duplicate_ranges = BTreeSet::new(); + let mut declaration_bindings = BTreeMap::<(usize, usize), Vec>::new(); for (binding, projection_range) in &projection_ranges { - let Some(importer_range) = importer_ranges.get(binding) else { - continue; - }; - if projection.get(projection_range.clone()) == importer.get(importer_range.clone()) { - duplicate_ranges.insert((projection_range.start, projection_range.end)); - } + declaration_bindings + .entry((projection_range.start, projection_range.end)) + .or_default() + .push(binding.clone()); } + let duplicate_bindings = declaration_bindings + .values() + .filter(|bindings| { + bindings.iter().all(|binding| { + projection_identities + .get(binding) + .is_some_and(|identity| included_names.contains_key(identity)) + }) + }) + .flatten() + .cloned() + .collect::>(); + let mut renamed_duplicates = BTreeSet::new(); + for binding in &duplicate_bindings { + let Some(identity) = projection_identities.get(binding) else { + return false; + }; + let Some(existing) = included_names.get(identity) else { + return false; + }; + if binding != existing && !rename_javascript_root_binding(projection, binding, existing) { + return false; + } + renamed_duplicates.insert(existing.clone()); + } + let duplicate_ranges = javascript_top_level_declaration_ranges(projection) + .into_iter() + .filter(|(binding, _)| renamed_duplicates.contains(binding)) + .map(|(_, range)| (range.start, range.end)) + .collect::>(); let mut replacements = duplicate_ranges .into_iter() .map(|(start, end)| (start..end, String::new())) .collect::>(); - apply_javascript_span_replacements(projection, &mut replacements) + if !apply_javascript_span_replacements(projection, &mut replacements) { + return false; + } + for binding in duplicate_bindings { + projection_identities.remove(&binding); + } + true } pub(in crate::agent) fn apply_javascript_span_replacements( @@ -6827,6 +6875,11 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at( .enumerate() { let inline_id = format!("inline-module:{index}"); + let inline_bytes = u64::try_from(inline_module.len()).unwrap_or(u64::MAX); + total_bytes = total_bytes + .checked_add(inline_bytes) + .filter(|bytes| *bytes <= MAX_EXTERNAL_GAMEPLAY_JAVASCRIPT_BYTES) + .ok_or_else(|| "自主构建外部脚本累计超过 2 MiB".to_string())?; let analysis = javascript_module_analysis(&inline_module, true) .ok_or_else(|| format!("自主构建内联模块不是有效 JavaScript:{inline_id}"))?; let analysis = normalize_javascript_module_analysis_sources("game/index.html", analysis)?; @@ -7156,10 +7209,24 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at( .module_units .extend(module_contents.values().cloned()); let mut projection_contents = module_contents.clone(); + let original_projection_identities = module_contents + .iter() + .map(|(module, _)| { + let identities = module_analyses + .get(module) + .map(|analysis| javascript_projection_declaration_identities(module, analysis)) + .unwrap_or_default(); + (module.clone(), identities) + }) + .collect::>(); + let mut projection_identities = original_projection_identities.clone(); let mut projected_units = BTreeMap::::new(); let mut projection_work_bytes = 0usize; + let mut projection_converged = false; for _ in 0..module_bindings.len().max(1) { let mut round_updates = BTreeMap::::new(); + let mut round_identity_updates = + BTreeMap::>::new(); for (importer, dependencies) in &module_bindings { let Some(importer_content) = module_contents.get(importer) else { continue; @@ -7284,10 +7351,23 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at( } let mut unit_root_bindings = importer_analysis.root_bindings.clone(); let mut unit_binding_names = importer_analysis.binding_names.clone(); + let mut unit_projection_identities = original_projection_identities + .get(importer) + .cloned() + .unwrap_or_default(); + let mut included_projection_names = unit_projection_identities + .iter() + .map(|(binding, identity)| (identity.clone(), binding.clone())) + .collect::>(); for (origin, bindings) in origins { let Some(origin_content) = projection_contents.get(&origin) else { continue; }; + let Some(origin_projection_identities) = projection_identities.get(&origin) else { + return Err(format!( + "自主构建模块投影缺少声明身份:{importer} <- {origin}" + )); + }; let used_names = bindings .iter() .map(|(exported, _, _, _)| exported.clone()) @@ -7297,9 +7377,20 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at( if projection.is_empty() { continue; } + let projection_ranges = javascript_top_level_declaration_ranges(&projection); + let mut selected_projection_identities = BTreeMap::new(); + for binding in projection_ranges.keys() { + let Some(identity) = origin_projection_identities.get(binding) else { + return Err(format!( + "自主构建模块投影声明身份无法解析:{origin}::{binding}" + )); + }; + selected_projection_identities.insert(binding.clone(), identity.clone()); + } if !javascript_remove_duplicate_projection_declarations( &mut projection, - importer_content, + &mut selected_projection_identities, + &included_projection_names, ) { return Err(format!( "自主构建循环模块投影声明范围无效:{importer} <- {origin}" @@ -7381,6 +7472,15 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at( )); } } + for (original, canonical) in &projected_binding_names { + let Some(identity) = selected_projection_identities.get(original) else { + return Err(format!( + "自主构建模块投影重命名后缺少声明身份:{origin}::{original}" + )); + }; + included_projection_names.insert(identity.clone(), canonical.clone()); + unit_projection_identities.insert(canonical.clone(), identity.clone()); + } if javascript_module_analysis(&projection, true).is_none() { return Err(format!("自主构建模块投影重命名后语义无效:{origin}")); } @@ -7507,9 +7607,11 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at( "自主构建模块投影累计处理超过 32 MiB,已拒绝继续展开".to_string() })?; round_updates.insert(importer.clone(), unit); + round_identity_updates.insert(importer.clone(), unit_projection_identities); } } if round_updates.is_empty() { + projection_converged = true; break; } let mut changed = false; @@ -7520,10 +7622,15 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at( } projected_units.insert(importer, unit); } + projection_identities.extend(round_identity_updates); if !changed { + projection_converged = true; break; } } + if !projection_converged { + return Err("自主构建模块投影未在有界轮次内收敛".to_string()); + } output.module_units.extend(projected_units.into_values()); Ok(output) } diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion_contract_tests.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion_contract_tests.rs index aceb8a6f8..6b6471e1a 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion_contract_tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion_contract_tests.rs @@ -3772,12 +3772,12 @@ fn javascript_projection_preserves_nested_names_shorthand_keys_and_cycles() { fs::write( root.join("game/a.mjs"), - "import { b } from './b.mjs'; export function a() { return b(); }", + "import { b } from './b.mjs'; function decoyA() { const b = 0; return b; } export function a() { return b(); }", ) .expect("write first cyclic module"); fs::write( root.join("game/b.mjs"), - "import { a } from './a.mjs'; export function b() { return a(); }", + "import { a } from './a.mjs'; function decoyB() { const a = 0; return a; } export function b() { return a(); }", ) .expect("write second cyclic module"); fs::write( @@ -3791,7 +3791,9 @@ fn javascript_projection_preserves_nested_names_shorthand_keys_and_cycles() { .module_units() .iter() .find(|unit| { - unit.contains("a();") && unit.contains("function a()") && unit.contains("function b()") + unit.contains("a();") + && unit.contains("function a()") + && unit.contains("function b__agc_import_1()") }) .unwrap_or_else(|| { panic!( @@ -3800,10 +3802,26 @@ fn javascript_projection_preserves_nested_names_shorthand_keys_and_cycles() { ) }); assert_eq!(projected.matches("function a()").count(), 1); - assert_eq!(projected.matches("function b()").count(), 1); + assert_eq!(projected.matches("function b__agc_import_1()").count(), 1); + assert!(!projected.contains("__agc_import_2")); + assert!(projected.contains("return a();")); assert!(javascript_is_syntactically_valid(projected, true)); } +#[test] +fn javascript_inline_modules_share_the_external_script_byte_limit() { + let temporary = tempfile::tempdir().expect("create inline module limit project"); + let oversized = " ".repeat(2 * 1024 * 1024 + 1); + let html = format!(""); + + let error = read_external_gameplay_javascript_at(temporary.path(), &html) + .expect_err("inline modules must count toward the 2 MiB JavaScript limit"); + assert!( + error.contains("累计超过 2 MiB"), + "unexpected error: {error}" + ); +} + #[test] fn javascript_alias_events_follow_function_invocation_time() { let static_source = "import * as real from './real.mjs'; import * as decoy from './decoy.mjs'; let facade = real; function start() { return facade.run(); } start(); facade = decoy;"; diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 10ba87906..df825bb23 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -5986,3 +5986,4 @@ - JavaScript / ESM 深层可达性补充:constructor、`.call/.apply` 与受控 inline callback 建立真实 invocation;具名 function expression 不再生成遮蔽外层 binding 的重叠节点,普通 inline function / arrow 未被执行时保持不可达。条件/循环赋值合并执行与跳过状态,conditional expression 合并各 owner,未知确定赋值显式 invalidation;已调用函数对外层 alias 的副作用按调用位置传播,`super` owner 固定在 class 定义点。恒假扫描先屏蔽 parser 识别的注释和 literal。投影 canonical 根名避让两侧全部非 import binding,dynamic shorthand 保留原键,循环模块按相同原始声明去重,同名 dynamic export 不得拉入无引用本地声明。 - 浏览器因果:状态证据仍只冻结 trusted input listener 及其点击派生微任务内的变化;完整手势身份改由宿主在成功完成 Chromium 元素鼠标输入后调用隔离世界 finish。更早注册的 `window` capture listener 即使调用 `stopImmediatePropagation()` 也不能阻断探针自身的完成身份,页面脚本不能伪造 host finish,RAF / timer 继续不计入动作结果。 - 验证边界:Linux 定向回归覆盖目录相对读写与清理、祖先 symlink、CAS 安装后错误、九文件混合快照、Tetris AST 反例和七项真实 Chrome generic 试玩。Windows cfg 代码必须继续在真实 Windows CI / 发布构建验证;本地缺少 MinGW C compiler 时,安装了 Rust target 也不能把交叉 `cargo check` 失败误报为源码失败。 +- JavaScript / ESM 循环与体积补充:投影声明按 `(origin module, original root binding)` 保存身份,canonical 重命名不能改写原始身份;删除回流声明前先把引用改接到 importer 已有 canonical,有界轮次未收敛时失败关闭。inline module 与外部脚本共同占用累计 `2 MiB` 源码预算。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 2b0e4e699..f708fa5de 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -838,6 +838,8 @@ game-project/ - 泥点不足是确定性业务中断,不是瞬态 Provider 故障或未知副作用。钱包的 `泥点余额不足` 与 `可消费泥点不足:...` 两种领域文案统一映射为稳定原因 `mud-points-insufficient`,不得自动重试;即使 External Generation durable ledger 已存在,也必须落为 `failed`,不能误入 `needs-reconciliation`。game-chat 顶部状态、持久失败对话与 `【Supervisor 阶段记录】` 统一显示“泥点余额不足,本轮游戏生成已中断。请充值后发送“继续”,系统会从当前项目进度接着完成。”,并禁止透传 operationId、URL、路径、密钥或任意上游正文。 - tool-plan 成功响应落账前,对内置 Runtime 原生函数与 legacy wrapper 的合法、无重复 key JSON arguments 按工具 schema 的精确位置做项目路径 canonicalization:`file.*.path`、`project.patchset.changes[*].path`、`project.git_commit.paths[*]`、`command.*.cwd`、`image.inspect.paths[*]` 与 `canvas.asset_generate.outputPath` 若是当前项目根目录内的完整绝对路径,转换为 `/` 分隔的项目相对路径后再校验、持久化并执行;源码/叙述字段、任务产物描述、动态 MCP arguments 和项目外绝对路径不得改写,后两者继续由绝对路径门禁失败关闭。项目根只允许搜索/列举范围与命令 cwd 规范化为 `.`,不能成为文件目标。当前进程与重启恢复都必须从同一份规范化 handoff 重放,禁止分别执行原响应和持久响应。 +- 循环 ESM 的投影身份固定为 `(origin module, original root binding)`;canonical 重命名只改变组合单元中的展示名,身份随投影闭包传播。删除已存在的回流声明前,必须先把其引用改接到 importer 中同一身份的既有 canonical;固定点未在模块数限定轮次内收敛时失败关闭,不能返回最后一轮仍变化的部分结果。inline module 与外部脚本共同计入累计源码 `2 MiB` 上限,不能绕过文件读取预算。 + ## 2026-07-31 长耗时与恢复收口 - `autonomous-game-build` 的固定 manifest DAG 是唯一缺省首轮专业执行链。缺省 collaboration policy 不再额外强制 `code-prototype / quality-review / art-*` 静态首波,Runtime 也不再按 Editor Key 或已有图片偷偷追加 Agent。2026-08-03 起,显式项目 policy 或旧 batch 恢复若进入首批 `agent.delegate` 兜底,同样只能激活 `design-director / art-director / code-director`,不得提前激活底层 Agent;这条兜底不能在默认 manifest 前复制同职责委派。