Compare commits

...

1 Commits

Author SHA1 Message Date
kdletters 8c899b23f3 完成 DirectProject Phaser 迁移闭环
Project CI / Repository checks (pull_request) Waiting to run
Project CI / Frontend tests (pull_request) Waiting to run
Project CI / Backend tests (pull_request) Waiting to run
Project CI / Native shell tests (pull_request) Waiting to run
新增受控 project.bootstrap 依赖安装与审计

支持 game 子目录 project.verify 构建和 dist 入口门禁

同步 Phaser 迁移提示、权限契约、Runtime 分发与项目文档

补充 npm 网络沙箱、缺失依赖诊断和共享契约测试
2026-09-14 13:30:46 +08:00
27 changed files with 526 additions and 37 deletions
@@ -1,6 +1,6 @@
处理代码任务时先用 project.search 定位,再用带行号的 file.read 获取足够上下文;单文件小改优先使用 file.patch;涉及多个文件时优先使用 project.patchset,它会自动创建 checkpoint,无需额外调用 project.checkpoint,并在成功后用返回的 checkpointId 调用 project.diff(includeContent=true) 审查整体变更;只有确认文件已废弃时才删除。
每次成功执行 file.write、file.patch、file.delete、project.patchset 或 project.restore,以及每次真正启动 command.execcommand.start,都会产生新的项目 revision最后一次修改后必须成功执行 project.verify、可验证 command.exec,或成功执行 command.run_limited 的 game.static_smoke,才能调用 respond_to_user 收束。文件回读不能替代可执行验证,验证后再次修改必须重新验证。需要执行 package.json 中的验证脚本时,先读取 package.json,再把真实脚本名和读到的完整命令原样提交给 project.verifyscript 可以是 check、typecheck、test、lint、build,或使用 check:<name>、test:<name>(例如 test:unit)、lint:<name>、typecheck:<name>、build:<name>、verify:<name>、validate:<name> 形式的命名脚本,其中冒号后的每个非空段必须以字母或数字开头且只能包含字母、数字、连字符、下划线或点;不得猜测或改写 expectedCommand。
每次成功执行 file.write、file.patch、file.delete、project.patchset 或 project.restore,以及每次真正启动 command.execcommand.start 或 project.bootstrap,都会产生新的项目 revisionDirectProject 的 npm/Phaser 工程先用 project.bootstrap {cwd:"game"} 执行受控无参数 npm install,再用 project.verify {cwd:"game",script:"build",expectedCommand:从 game/package.json 原样读取} 构建,并确认 game/dist/index.html 存在。最后一次修改后必须成功执行 project.verify、可验证 command.exec,或成功执行 command.run_limited 的 game.static_smoke,才能调用 respond_to_user 收束。文件回读不能替代可执行验证,验证后再次修改必须重新验证。需要执行 package.json 中的验证脚本时,先读取 package.json,再把真实脚本名和读到的完整命令原样提交给 project.verifyscript 可以是 check、typecheck、test、lint、build,或使用 check:<name>、test:<name>(例如 test:unit)、lint:<name>、typecheck:<name>、build:<name>、verify:<name>、validate:<name> 形式的命名脚本,其中冒号后的每个非空段必须以字母或数字开头且只能包含字母、数字、连字符、下划线或点;不得猜测或改写 expectedCommand。
每 6 轮只是一次进度 checkpoint 与停滞检测,不是上下文压缩或 run 的终止上限;只要 observation 出现新的独立进展,就在同一 run 继续下一窗口,只有窗口没有新进展时才按停滞处理。真正的上下文压缩仅由 token 阈值或显式 compact 触发。Agent 私有记忆只能由本人写入,跨 Agent 共享稳定结论用 blackboard.write,给单个 Agent 留上下文用 agent.message。
File diff suppressed because one or more lines are too long
@@ -16,7 +16,7 @@ JSON schema:
{ "group": "balance", "role": "Difficulty", "summary": "数值交接摘要", "outputs": ["game/balance.json"], "next": "交给程序组读取" },
{ "group": "art", "role": "Asset", "summary": "美术交接摘要", "outputs": ["assets/manifest.art.json"], "next": "进入画板或本地资产登记" },
{ "group": "audio", "role": "SFX", "summary": "音乐音效交接摘要", "outputs": ["assets/manifest.audio.json"], "next": "进入画板音频链路" },
{ "group": "code", "role": "Code", "summary": "程序交接摘要", "outputs": ["game/index.html"], "next": "交给 Playtest" },
{ "group": "code", "role": "Code", "summary": "程序交接摘要", "outputs": ["game/index.html", "game/game.js", "game/package.json", "game/vite.config.js"], "next": "先 project.bootstrap,再 project.verify(build)交给 Playtest" },
{ "group": "publishing", "role": "Publish", "summary": "运营交接摘要", "outputs": ["exports/README.md"], "next": "等待预览验收" }
],
"handoffSummary": "六组 agent 的交接摘要,每组一行",
@@ -24,6 +24,7 @@ JSON schema:
}
gameHtml 规则:
- 本次若目标是 Phaser/npmworkspaceMode 必须为 DirectProject;先写入完整 game/ 工程文件,再由受控项目工具安装与构建。
- 此 JSON 协议仅用于已有的单文件 HTML 项目;npm / Phaser 项目必须使用 DirectProject,不能通过 gameHtml 交付 package.json 或模块源码。
- 必须是单文件 HTML,不能加载远程脚本、远程图片、远程 CSS 或 CDN。
- 必须包含 canvas、canvas getContext、实际绘制调用、键盘或鼠标输入、requestAnimationFrame 主循环、目标、失败或胜利状态、R 或按钮重开。
@@ -1192,6 +1192,7 @@ pub(in crate::agent) fn agent_runtime_public_action_input_summary(
| "project.patchset"
| "project.search"
| "project.verify"
| "project.bootstrap"
| "file.list"
| "file.read"
| "file.write"
@@ -1259,6 +1260,7 @@ pub(crate) fn agent_runtime_tool_action_fingerprint(
| "command.stdin"
| "command.terminate"
| "project.verify"
| "project.bootstrap"
)
.then(|| {
let metadata = command_sandbox_platform_metadata();
@@ -1468,11 +1470,23 @@ pub(crate) fn agent_runtime_tool_action_input_summary(
.and_then(|value| value.as_bool())
.unwrap_or(false)
),
"project.bootstrap" => {
format!(
"cwd={} · timeoutSeconds={}",
relative_path(&["cwd"]),
input
.get("timeoutSeconds")
.or_else(|| input.get("timeout_seconds"))
.and_then(|value| value.as_u64())
.unwrap_or(300)
)
}
"project.verify" => {
let expected_command = text(&["expectedCommand", "expected_command"]);
let command_chars = expected_command.chars().count();
format!(
"script={} · expectedCommandSha256={:x} · expectedCommandChars={} · timeoutSeconds={}",
"cwd={} · script={} · expectedCommandSha256={:x} · expectedCommandChars={} · timeoutSeconds={}",
relative_path(&["cwd"]),
text(&["script"]),
Sha256::digest(expected_command.as_bytes()),
command_chars,
@@ -184,6 +184,17 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
)
.await
}
"project.bootstrap" => {
observe_agent_runtime_project_bootstrap(
root,
agent_id,
run_id,
action_id,
&action_fingerprint,
&action.input,
)
.await
}
"project.checkpoint" => observe_agent_runtime_project_checkpoint(root),
"project.restore" => {
observe_agent_runtime_project_restore(root, agent_id, run_id, &action.input)
@@ -8,10 +8,15 @@ pub(crate) struct AgentRuntimeAutonomousSourcePayloadStats {
}
pub(in crate::agent) fn agent_runtime_autonomous_project_verify_available(root: &Path) -> bool {
let package_path = root.join("package.json");
fs::symlink_metadata(package_path)
.map(|metadata| metadata.file_type().is_file() && !metadata.file_type().is_symlink())
.unwrap_or(false)
[root.join("package.json"), root.join("game/package.json")]
.into_iter()
.any(|package_path| {
fs::symlink_metadata(package_path)
.map(|metadata| {
metadata.file_type().is_file() && !metadata.file_type().is_symlink()
})
.unwrap_or(false)
})
}
pub(in crate::agent) fn add_agent_runtime_autonomous_source_field(
@@ -345,7 +345,7 @@ fn build_game_creator_agent_background_tool_plan_request_at(
let project_tools_contract = if runtime_owner_artifact_validation_available {
"project.search 使用 {\"query\":\"要查找的字面文本\",\"path\":\"\",\"maxResults\":20,\"caseSensitive\":false},path 为空字符串时搜索整个项目,返回 path:line 和匹配行;当前固定 owner 的函数目录不广告 project.verifyproject.checkpoint 使用空对象,只用于多个 file.* 写动作前或需要独立回退点时创建本地 checkpointproject.patchset 会自动创建 checkpoint,不要为同一批变更额外调用 project.checkpointproject.restore 使用 {\"checkpointId\":\"checkpoint id\"}project.diff 使用 {\"checkpointId\":\"checkpoint id\",\"includeContent\":true,\"maxFiles\":20,\"maxChars\":24000}git.inspect 使用 {\"includeDiff\":true,\"maxFiles\":20,\"maxChars\":24000},只读项目根 Git 状态和有界 diff,不得用它提交、暂存、切分支、合并、重置、stash、worktree 或访问 remote。".to_string()
} else {
"project.search 使用 {\"query\":\"要查找的字面文本\",\"path\":\"\",\"maxResults\":20,\"caseSensitive\":false},path 为空字符串时搜索整个项目,返回 path:line 和匹配行;project.verify 使用 {\"script\":\"check|typecheck|test|lint|build\",\"expectedCommand\":\"从 package.json 读取的完整原始脚本\",\"timeoutSeconds\":120},只执行项目根 package.json 中同名 npm 脚本,expectedCommand 不一致时拒绝执行,确认策略以当前工具策略中 project.verify 的独立权限为准;project.checkpoint 使用空对象,只用于多个 file.* 写动作前或需要独立回退点时创建本地 checkpointproject.patchset 会自动创建 checkpoint,不要为同一批变更额外调用 project.checkpointproject.restore 使用 {\"checkpointId\":\"checkpoint id\"}project.diff 使用 {\"checkpointId\":\"checkpoint id\",\"includeContent\":true,\"maxFiles\":20,\"maxChars\":24000}git.inspect 使用 {\"includeDiff\":true,\"maxFiles\":20,\"maxChars\":24000},只读项目根 Git 状态和有界 diff,不得用它提交、暂存、切分支、合并、重置、stash、worktree 或访问 remote。".to_string()
"project.search 使用 {\"query\":\"要查找的字面文本\",\"path\":\"\",\"maxResults\":20,\"caseSensitive\":false},path 为空字符串时搜索整个项目,返回 path:line 和匹配行;project.bootstrap 使用 {\"cwd\":\"game\",\"timeoutSeconds\":300},只执行 game 目录无参数 npm install 并记录 package/lock 指纹;project.verify 使用 {\"script\":\"check|typecheck|test|lint|build\",\"expectedCommand\":\"对应 cwd 的 package.json 读取的完整原始脚本\",\"timeoutSeconds\":120,\"cwd\":\"game\"},只执行对应 cwd package.json 中同名 npm 脚本,build 必须确认 game/dist/index.htmlexpectedCommand 不一致时拒绝执行,确认策略以当前工具策略中 project.verify 的独立权限为准;project.checkpoint 使用空对象,只用于多个 file.* 写动作前或需要独立回退点时创建本地 checkpointproject.patchset 会自动创建 checkpoint,不要为同一批变更额外调用 project.checkpointproject.restore 使用 {\"checkpointId\":\"checkpoint id\"}project.diff 使用 {\"checkpointId\":\"checkpoint id\",\"includeContent\":true,\"maxFiles\":20,\"maxChars\":24000}git.inspect 使用 {\"includeDiff\":true,\"maxFiles\":20,\"maxChars\":24000},只读项目根 Git 状态和有界 diff,不得用它提交、暂存、切分支、合并、重置、stash、worktree 或访问 remote。".to_string()
};
let prompt = format!(
concat!(
@@ -28,6 +28,7 @@ pub(crate) fn agent_runtime_executable_tools() -> Vec<&'static str> {
"project.index",
"project.search",
"project.verify",
"project.bootstrap",
"project.checkpoint",
"project.restore",
"project.diff",
@@ -83,6 +84,7 @@ pub(crate) fn agent_runtime_acceptance_evidence_tools() -> BTreeSet<&'static str
"project.index",
"project.search",
"project.verify",
"project.bootstrap",
"project.diff",
"git.inspect",
"project.patchset",
@@ -156,6 +156,7 @@ pub(super) const AGENT_RUNTIME_AUTONOMOUS_GAME_BUILD_AUTO_COMMAND_IDS: &[&str] =
"file.delete",
"project.patchset",
"project.verify",
"project.bootstrap",
"command.run_limited",
"preview.validate",
"canvas.asset_generate",
@@ -109,6 +109,7 @@ pub(crate) fn agent_runtime_tool_requires_repository_context_fingerprint_gate(to
| "project.index"
| "project.search"
| "project.verify"
| "project.bootstrap"
| "project.checkpoint"
| "project.patchset"
| "project.restore"
@@ -857,6 +857,12 @@ pub(crate) async fn observe_agent_runtime_project_verify(
.and_then(serde_json::Value::as_str)
.unwrap_or_default()
.to_string();
let cwd = input
.get("cwd")
.and_then(serde_json::Value::as_str)
.filter(|value| !value.trim().is_empty())
.unwrap_or(".")
.to_string();
let timeout_seconds =
agent_runtime_tool_input_usize(input, &["timeoutSeconds", "timeout_seconds"])
.unwrap_or(AGENT_RUNTIME_PROJECT_VERIFY_DEFAULT_TIMEOUT_SECONDS);
@@ -896,6 +902,7 @@ pub(crate) async fn observe_agent_runtime_project_verify(
script.as_str(),
expected_command.as_str(),
timeout_seconds,
cwd.as_str(),
|| {
verification_state = Some(begin_agent_runtime_project_verification_locked(
root,
@@ -931,6 +938,7 @@ pub(crate) async fn observe_agent_runtime_project_verify(
"script": verification.script,
"expectedCommand": audit_expected_command,
"packageManager": verification.package_manager,
"cwd": verification.cwd_relative,
"status": verification.status,
"exitCode": verification.exit_code,
"timedOut": verification.timed_out,
@@ -1039,3 +1047,81 @@ pub(crate) async fn observe_agent_runtime_project_verify(
}
}
}
pub(crate) async fn observe_agent_runtime_project_bootstrap(
root: &Path,
agent_id: &str,
run_id: &str,
action_id: Option<&str>,
action_fingerprint: &str,
input: &serde_json::Value,
) -> AgentRuntimeToolObservation {
let cwd = input
.get("cwd")
.and_then(serde_json::Value::as_str)
.unwrap_or_default();
let timeout = agent_runtime_tool_input_usize(input, &["timeoutSeconds", "timeout_seconds"])
.unwrap_or(300);
if cwd != "game" {
return AgentRuntimeToolObservation {
tool: "project.bootstrap".to_string(),
status: "failed".to_string(),
summary: "project.bootstrap 只允许 cwd=game".to_string(),
detail: None,
};
}
let _lock = match acquire_project_write_lock(root, "project.bootstrap") {
Ok(lock) => lock,
Err(error) => {
return AgentRuntimeToolObservation {
tool: "project.bootstrap".to_string(),
status: "failed".to_string(),
summary: "project.bootstrap 无法取得项目执行锁".to_string(),
detail: Some(redact_agent_runtime_project_paths(root, &error, 240)),
}
}
};
let result = crate::project::run_project_bootstrap_at(root, timeout as u64).await;
match result {
Ok(value) if value.status == "completed" => {
if let Err(error) = append_agent_db_record(
root,
serde_json::json!({
"recordType":"agent.runtime.project.bootstrap", "agentId":agent_id, "runId":run_id,
"actionId":action_id, "actionFingerprint":action_fingerprint, "cwd":"game",
"packageSha256":value.package_sha256, "lockSha256":value.lock_sha256,
"status":value.status, "logPath":value.log_path, "output":value.output
}),
) {
return AgentRuntimeToolObservation {
tool: "project.bootstrap".to_string(),
status: AGENT_RUNTIME_TOOL_OBSERVATION_STATUS_NEEDS_RECONCILIATION.to_string(),
summary: "project.bootstrap 已安装,但审计记录无法落盘".to_string(),
detail: Some(redact_agent_runtime_project_paths(root, &error, 500)),
};
}
AgentRuntimeToolObservation {
tool: "project.bootstrap".to_string(),
status: "ok".to_string(),
summary: "game 依赖安装已完成".to_string(),
detail: Some(format!(
"cwd=game · packageSha256={} · lockSha256={}",
value.package_sha256,
value.lock_sha256.as_deref().unwrap_or("none")
)),
}
}
Ok(value) => AgentRuntimeToolObservation {
tool: "project.bootstrap".to_string(),
status: "failed".to_string(),
summary: "game 依赖安装失败".to_string(),
detail: Some(value.output),
},
Err(error) => AgentRuntimeToolObservation {
tool: "project.bootstrap".to_string(),
status: "failed".to_string(),
summary: redact_agent_runtime_project_paths(root, &error, 240),
detail: None,
},
}
}
@@ -1009,7 +1009,8 @@ fn runtime_tool_description(tool: &str) -> &'static str {
}
"project.index" => "刷新并读取有界仓库启动上下文。",
"project.search" => "在项目文本文件中做有界字面量搜索。",
"project.verify" => "运行 package.json 原样声明的验证脚本。",
"project.verify" => "在项目或指定相对 cwd 中运行 package.json 原样声明的验证脚本,并检查构建产物",
"project.bootstrap" => "仅在项目 game 目录受控执行无参数 npm install,并记录依赖文件指纹。",
"project.checkpoint" => "创建项目本地 checkpoint。",
"project.restore" => "从 checkpoint 恢复当前项目。",
"project.diff" => "读取 checkpoint 与当前项目之间的有界差异。",
@@ -1154,10 +1155,18 @@ fn runtime_tool_input_schema(tool: &str) -> Value {
}
}),
"project.verify" => json!({
"type": "object", "required": ["script", "expectedCommand", "timeoutSeconds"], "additionalProperties": false,
"type": "object", "required": ["script", "expectedCommand", "timeoutSeconds", "cwd"], "additionalProperties": false,
"properties": {
"script": { "type": "string", "minLength": 1 },
"expectedCommand": { "type": "string", "minLength": 1 },
"timeoutSeconds": { "type": "integer", "minimum": 1, "maximum": 600 },
"cwd": { "type": ["string", "null"], "pattern": "^[A-Za-z0-9._/-]+$" }
}
}),
"project.bootstrap" => json!({
"type": "object", "required": ["cwd", "timeoutSeconds"], "additionalProperties": false,
"properties": {
"cwd": { "type": "string", "const": "game" },
"timeoutSeconds": { "type": "integer", "minimum": 1, "maximum": 600 }
}
}),
@@ -321,6 +321,44 @@ pub(crate) fn resolve_project_command_spec_at(
.map_err(|error| ProjectCommandError::new(ProjectCommandErrorStage::Validation, error))
}
/// Resolves the one privileged npm operation used to hydrate a DirectProject.
/// It intentionally bypasses the general command.exec npm allow-list: callers
/// must use the dedicated `project.bootstrap` action, which only accepts the
/// literal `npm install` in the project's `game` directory.
pub(crate) fn resolve_project_bootstrap_spec_at(
root: &Path,
timeout_seconds: u64,
) -> Result<ProjectCommandSpec, ProjectCommandError> {
validate_project_root(root)
.map_err(|error| ProjectCommandError::new(ProjectCommandErrorStage::Validation, error))?;
let cwd_relative = "game".to_string();
let cwd = resolve_local_project_path(root, &cwd_relative)
.map_err(|error| ProjectCommandError::new(ProjectCommandErrorStage::Validation, error))?;
validate_project_command_cwd_components(root, &cwd_relative)
.map_err(|error| ProjectCommandError::new(ProjectCommandErrorStage::Validation, error))?;
if !(PROJECT_COMMAND_MIN_TIMEOUT_SECONDS..=PROJECT_COMMAND_MAX_TIMEOUT_SECONDS)
.contains(&timeout_seconds)
{
return Err(ProjectCommandError::new(
ProjectCommandErrorStage::Validation,
format!("project.bootstrap timeoutSeconds 必须在 {PROJECT_COMMAND_MIN_TIMEOUT_SECONDS}-{PROJECT_COMMAND_MAX_TIMEOUT_SECONDS} 之间"),
));
}
let program = "npm".to_string();
let (executable, safe_path) = resolve_project_command_executable(root, &program)
.map_err(|error| ProjectCommandError::new(ProjectCommandErrorStage::Validation, error))?;
Ok(ProjectCommandSpec {
program,
executable,
safe_path,
arguments: vec!["install".to_string()],
cwd_relative,
cwd,
timeout_seconds,
verification_eligible: false,
})
}
fn resolve_project_command_spec_inner(
root: &Path,
program: &str,
@@ -1202,6 +1240,19 @@ pub(crate) fn prepare_project_command_launch_spec(
(OsString::from("NO_PROXY"), OsString::new()),
(OsString::from("PATH"), spec.safe_path.clone()),
];
if spec
.arguments
.first()
.is_some_and(|argument| argument == "install")
{
for (name, value) in &mut environment {
match name.to_string_lossy().as_ref() {
"npm_config_offline" => *value = OsString::from("false"),
"HTTP_PROXY" | "HTTPS_PROXY" | "ALL_PROXY" => *value = OsString::new(),
_ => {}
}
}
}
for name in ["SystemRoot", "PATHEXT", "RUSTUP_HOME"] {
if let Some(value) = std::env::var_os(name) {
environment.push((OsString::from(name), value));
@@ -1998,6 +2049,16 @@ pub(crate) async fn run_project_command_with_output_at(
.await
}
pub(crate) async fn run_project_bootstrap_command_at(
root: &Path,
timeout_seconds: u64,
) -> Result<ProjectCommandResult, ProjectCommandError> {
let spec = resolve_project_bootstrap_spec_at(root, timeout_seconds)?;
let launch = prepare_project_command_launch_spec(root, &spec)?;
let staged = stage_project_command_launch_spec(&spec, launch)?;
run_prepared_project_command_with_output_at(root, &spec, staged, None, || Ok(())).await
}
pub(crate) async fn run_prepared_project_command_with_output_at<F>(
root: &Path,
spec: &ProjectCommandSpec,
@@ -302,7 +302,19 @@ mod linux {
cwd: &Path,
environment: &[(OsString, OsString)],
) -> Result<CommandSandboxLaunch, CommandSandboxError> {
let metadata = CommandSandboxMetadata::enforced_linux();
let mut metadata = CommandSandboxMetadata::enforced_linux();
let network_enabled = executable
.file_name()
.and_then(OsStr::to_str)
.is_some_and(|name| {
name.eq_ignore_ascii_case("npm") || name.eq_ignore_ascii_case("npm.cmd")
})
&& arguments
.first()
.is_some_and(|argument| argument == "install");
if network_enabled {
metadata.network = "enabled";
}
let bwrap = find_trusted_bwrap().map_err(|error| {
CommandSandboxError::new(
format!("command sandbox unavailable: {error}"),
@@ -363,7 +375,7 @@ mod linux {
)?;
let fixed_system_read_only = collect_fixed_system_mounts();
let launch = build_linux_bwrap_launch(LinuxSandboxPlan {
let mut launch = build_linux_bwrap_launch(LinuxSandboxPlan {
bwrap,
root,
cwd,
@@ -375,6 +387,7 @@ mod linux {
external_read_only,
fixed_system_read_only,
});
launch.metadata = metadata.clone();
run_project_mount_preflight(&launch).map_err(|error| {
CommandSandboxError::new(
format!("command sandbox project mount preflight 失败:{error}"),
@@ -602,7 +615,19 @@ mod linux {
fn build_linux_bwrap_launch(plan: LinuxSandboxPlan) -> CommandSandboxLaunch {
let mut args = Vec::<OsString>::new();
push_namespace_arguments(&mut args);
push_namespace_arguments(
&mut args,
plan.executable
.file_name()
.and_then(OsStr::to_str)
.is_some_and(|name| {
name.eq_ignore_ascii_case("npm") || name.eq_ignore_ascii_case("npm.cmd")
})
&& plan
.arguments
.first()
.is_some_and(|argument| argument == "install"),
);
push_ro_bind(&mut args, Path::new("/usr"), Path::new("/usr"));
for (target, destination) in &plan.merged_usr_links {
push_option(
@@ -673,7 +698,7 @@ mod linux {
}
}
fn push_namespace_arguments(args: &mut Vec<OsString>) {
fn push_namespace_arguments(args: &mut Vec<OsString>, share_network: bool) {
for argument in [
"--die-with-parent",
"--unshare-all",
@@ -686,6 +711,9 @@ mod linux {
] {
args.push(OsString::from(argument));
}
if share_network {
args.push(OsString::from("--share-net"));
}
}
fn push_option<'a, I>(args: &mut Vec<OsString>, option: &str, values: I)
@@ -829,7 +857,7 @@ mod linux {
merged_usr_links: &[(OsString, PathBuf)],
) -> Result<(), String> {
let mut args = Vec::<OsString>::new();
push_namespace_arguments(&mut args);
push_namespace_arguments(&mut args, false);
push_ro_bind(&mut args, Path::new("/usr"), Path::new("/usr"));
for (target, destination) in merged_usr_links {
push_option(
@@ -39,6 +39,7 @@ pub(crate) const ISOLATED_AGENT_PRIVATE_MEMORY_SCHEMA_VERSION: &str =
pub(crate) const ISOLATED_AGENT_UNSCOPED_DENIED_COMMAND_IDS: &[&str] = &[
"project.verify",
"project.bootstrap",
"project.git_commit",
"command.exec",
"command.start",
@@ -59,6 +60,7 @@ pub(crate) const ISOLATED_AGENT_UNSCOPED_DENIED_COMMAND_IDS: &[&str] = &[
pub(crate) const ISOLATED_AGENT_UNSCOPED_DENIED_TOOLS: &[&str] = &[
"project.verify",
"project.bootstrap",
"project.git_commit",
"command.exec",
"command.start",
@@ -6,6 +6,7 @@ use std::io::{Seek, SeekFrom};
mod agent_db;
mod asset_export;
mod asset_rename;
mod bootstrap;
mod checkpoint;
mod conversation;
mod export;
@@ -23,6 +24,7 @@ mod write_lock;
pub(crate) use agent_db::*;
pub(crate) use asset_export::*;
pub(crate) use asset_rename::*;
pub(crate) use bootstrap::*;
pub(crate) use checkpoint::*;
pub(crate) use conversation::*;
pub(crate) use export::*;
@@ -0,0 +1,113 @@
use super::*;
use sha2::{Digest, Sha256};
const BOOTSTRAP_PACKAGE_MAX_BYTES: u64 = 512 * 1024;
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct ProjectBootstrapResult {
pub(crate) status: String,
pub(crate) output: String,
pub(crate) package_sha256: String,
pub(crate) lock_sha256: Option<String>,
pub(crate) log_path: String,
pub(crate) updated_at: u64,
}
fn read_bootstrap_file(path: &Path, label: &str) -> Result<Vec<u8>, String> {
let metadata = fs::symlink_metadata(path)
.map_err(|error| format!("读取 {label} 失败:{}: {error}", path.display()))?;
if metadata.file_type().is_symlink() || !metadata.is_file() {
return Err(format!("project.bootstrap 要求 {label} 是普通文件"));
}
if metadata.len() > BOOTSTRAP_PACKAGE_MAX_BYTES {
return Err(format!(
"project.bootstrap {label} 超过 {} 字节上限",
BOOTSTRAP_PACKAGE_MAX_BYTES
));
}
prepare_game_creator_private_path_for_read(path, false, label)?;
fs::read(path).map_err(|error| format!("读取 {label} 失败:{error}"))
}
pub(crate) async fn run_project_bootstrap_at(
root: &Path,
timeout_seconds: u64,
) -> Result<ProjectBootstrapResult, String> {
validate_project_root(root)?;
let game = resolve_local_project_path(root, "game")?;
if !game.is_dir() {
return Err("project.bootstrap 只允许项目内 game 目录".to_string());
}
let package = read_bootstrap_file(&game.join("package.json"), "game/package.json")?;
let package_json: serde_json::Value = serde_json::from_slice(&package)
.map_err(|error| format!("解析 game/package.json 失败:{error}"))?;
if let Some(manager) = package_json
.get("packageManager")
.and_then(serde_json::Value::as_str)
{
if !manager.trim().starts_with("npm@") && manager.trim() != "npm" {
return Err("project.bootstrap 当前只支持 npm packageManager".to_string());
}
}
for lock_name in ["pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"] {
if game.join(lock_name).exists() {
return Err(format!("project.bootstrap 检测到非 npm 锁文件 {lock_name}"));
}
}
if !package_json
.get("scripts")
.is_some_and(serde_json::Value::is_object)
{
return Err("project.bootstrap 要求 game/package.json 包含 scripts 对象".to_string());
}
let lock = match fs::symlink_metadata(game.join("package-lock.json")) {
Ok(_) => Some(read_bootstrap_file(
&game.join("package-lock.json"),
"game/package-lock.json",
)?),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
Err(error) => return Err(format!("读取 game/package-lock.json 失败:{error}")),
};
let command = crate::command_exec::run_project_bootstrap_command_at(root, timeout_seconds)
.await
.map_err(|error| error.to_string())?;
let completed = command.exit_code == Some(0) && !command.timed_out;
let status = if completed { "completed" } else { "failed" };
let updated_at = unix_timestamp();
let log_path = resolve_local_project_path(root, ".agent/logs/command.log")?;
let package_sha256 = format!("{:x}", Sha256::digest(&package));
let lock_sha256 = lock
.as_ref()
.map(|bytes| format!("{:x}", Sha256::digest(bytes)));
let output = sanitize_project_verification_output(&command.output);
let line = format!(
"{updated_at} project.bootstrap status={} packageSha256={} lockSha256={} cwd=game\n{}\n",
status,
package_sha256,
lock_sha256.as_deref().unwrap_or("none"),
output
);
append_game_creator_private_file(&log_path, line.as_bytes(), "命令日志")?;
record_command_run(
root,
GameCreationAppCommandRunState {
command_id: "project.bootstrap".to_string(),
status: if completed {
GameCreationAppCommandRunStatus::Completed
} else {
GameCreationAppCommandRunStatus::Failed
},
output: output.clone(),
log_path: ".agent/logs/command.log".to_string(),
updated_at,
},
)?;
Ok(ProjectBootstrapResult {
status: status.to_string(),
output,
package_sha256,
lock_sha256,
log_path: ".agent/logs/command.log".to_string(),
updated_at,
})
}
@@ -121,6 +121,7 @@ pub(crate) struct ProjectVerificationSpec {
pub(crate) program: String,
pub(crate) arguments: Vec<String>,
pub(crate) timeout_seconds: u64,
pub(crate) cwd_relative: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -129,6 +130,7 @@ pub(crate) struct ProjectVerificationResult {
pub(crate) script: String,
pub(crate) expected_command: String,
pub(crate) package_manager: String,
pub(crate) cwd_relative: String,
pub(crate) status: String,
pub(crate) exit_code: Option<i32>,
pub(crate) timed_out: bool,
@@ -290,9 +292,38 @@ pub(crate) fn resolve_project_verification_spec_at(
script: &str,
expected_command: &str,
timeout_seconds: u64,
) -> Result<ProjectVerificationSpec, String> {
resolve_project_verification_spec_with_cwd_at(
root,
script,
expected_command,
timeout_seconds,
".",
)
}
pub(crate) fn resolve_project_verification_spec_with_cwd_at(
root: &Path,
script: &str,
expected_command: &str,
timeout_seconds: u64,
cwd_relative: &str,
) -> Result<ProjectVerificationSpec, String> {
validate_project_root(root)?;
ensure_project_verification_has_no_project_npmrc(root)?;
let cwd_relative = if cwd_relative.trim().is_empty() || cwd_relative.trim() == "." {
".".to_string()
} else {
normalize_relative_path(cwd_relative)?
};
let package_root = if cwd_relative == "." {
root.to_path_buf()
} else {
resolve_local_project_path(root, &cwd_relative)?
};
if !package_root.is_dir() {
return Err("project.verify cwd 必须是项目内普通目录".to_string());
}
ensure_project_verification_has_no_project_npmrc(&package_root)?;
let script = script.trim();
if script.chars().count() > PROJECT_VERIFICATION_SCRIPT_MAX_CHARS {
return Err(format!(
@@ -323,7 +354,7 @@ pub(crate) fn resolve_project_verification_spec_at(
));
}
let package_path = root.join("package.json");
let package_path = package_root.join("package.json");
let metadata = fs::symlink_metadata(&package_path).map_err(|error| {
format!(
"读取 package.json 失败:{}: {error}",
@@ -348,7 +379,7 @@ pub(crate) fn resolve_project_verification_spec_at(
})?;
let package: serde_json::Value = serde_json::from_str(&package_content)
.map_err(|error| format!("解析 package.json 失败:{error}"))?;
let package_manager = project_verification_package_manager_at(root, &package)?;
let package_manager = project_verification_package_manager_at(&package_root, &package)?;
let actual_command = package
.get("scripts")
.and_then(serde_json::Value::as_object)
@@ -360,6 +391,20 @@ pub(crate) fn resolve_project_verification_spec_at(
"package.json 中的 {script} 脚本已变化,请重新读取后再确认执行"
));
}
if script == "build" && cwd_relative == "game" {
let modules_path = package_root.join("node_modules");
let modules_metadata = fs::symlink_metadata(&modules_path).map_err(|error| {
if error.kind() == std::io::ErrorKind::NotFound {
"project.verify build 前缺少 game/node_modules;请先执行 project.bootstrap"
.to_string()
} else {
format!("project.verify 检查 game/node_modules 失败:{error}")
}
})?;
if modules_metadata.file_type().is_symlink() || !modules_metadata.is_dir() {
return Err("project.verify build 前的 game/node_modules 不是普通目录;请重新执行 project.bootstrap".to_string());
}
}
Ok(ProjectVerificationSpec {
script: script.to_string(),
@@ -373,6 +418,7 @@ pub(crate) fn resolve_project_verification_spec_at(
script.to_string(),
],
timeout_seconds,
cwd_relative,
})
}
@@ -485,10 +531,21 @@ async fn run_project_verification_process<F>(
where
F: FnOnce() -> Result<(), String>,
{
ensure_project_verification_has_no_project_npmrc(root)
let package_root = if spec.cwd_relative == "." {
root.to_path_buf()
} else {
resolve_local_project_path(root, &spec.cwd_relative)
.map_err(|error| ProjectCommandError::new(ProjectCommandErrorStage::Preflight, error))?
};
ensure_project_verification_has_no_project_npmrc(&package_root)
.map_err(|error| ProjectCommandError::new(ProjectCommandErrorStage::Preflight, error))?;
let command_spec =
resolve_project_command_spec_at(root, "npm", &spec.arguments, ".", spec.timeout_seconds)?;
let command_spec = resolve_project_command_spec_at(
root,
"npm",
&spec.arguments,
&spec.cwd_relative,
spec.timeout_seconds,
)?;
let launch = prepare_project_command_launch_spec(root, &command_spec)?;
let launch_metadata = launch.clone();
let staged = stage_project_command_launch_spec(&command_spec, launch)?;
@@ -620,9 +677,14 @@ pub(crate) async fn run_project_verification_at(
expected_command: &str,
timeout_seconds: u64,
) -> Result<ProjectVerificationResult, String> {
run_project_verification_with_commit_at(root, script, expected_command, timeout_seconds, || {
Ok(())
})
run_project_verification_with_commit_at(
root,
script,
expected_command,
timeout_seconds,
".",
|| Ok(()),
)
.await
}
@@ -631,15 +693,21 @@ pub(crate) async fn run_project_verification_with_commit_at<F>(
script: &str,
expected_command: &str,
timeout_seconds: u64,
cwd_relative: &str,
durable_commit: F,
) -> Result<ProjectVerificationResult, String>
where
F: FnOnce() -> Result<(), String>,
{
let spec =
resolve_project_verification_spec_at(root, script, expected_command, timeout_seconds)?;
let spec = resolve_project_verification_spec_with_cwd_at(
root,
script,
expected_command,
timeout_seconds,
cwd_relative,
)?;
let started_at = std::time::Instant::now();
let process = match run_project_verification_process(root, &spec, durable_commit).await {
let mut process = match run_project_verification_process(root, &spec, durable_commit).await {
Ok(process) => process,
Err(error) if error.needs_reconciliation() => {
return Err(format!("project.verify 执行状态需要人工核对:{error}"));
@@ -658,7 +726,18 @@ where
},
};
let duration_ms = u64::try_from(started_at.elapsed().as_millis()).unwrap_or(u64::MAX);
let completed = !process.timed_out && process.exit_code == Some(0);
let mut completed = !process.timed_out && process.exit_code == Some(0);
if completed && spec.script == "build" && spec.cwd_relative == "game" {
let dist_entry = root.join("game").join("dist").join("index.html");
completed = fs::symlink_metadata(&dist_entry)
.is_ok_and(|metadata| metadata.is_file() && !metadata.file_type().is_symlink());
if !completed {
process.output = format!(
"{}\nproject.verify build 成功但缺少 game/dist/index.html",
process.output
);
}
}
let status = if completed { "completed" } else { "failed" };
let command_id = format!("project.verify.{}", spec.script);
let updated_at = unix_timestamp();
@@ -703,6 +782,7 @@ where
script: spec.script,
expected_command: spec.expected_command,
package_manager: spec.package_manager,
cwd_relative: spec.cwd_relative,
status: status.to_string(),
exit_code: process.exit_code,
timed_out: process.timed_out,
@@ -389,7 +389,23 @@ fn collect_native_tool_absolute_path_findings(
}
}
}
"project.bootstrap" => {
collect_native_string_field(
root,
input,
"cwd",
&format!("{input_pointer}/cwd"),
findings,
);
}
"project.verify" => {
collect_native_string_field(
root,
input,
"cwd",
&format!("{input_pointer}/cwd"),
findings,
);
collect_native_string_field(
root,
input,
+2 -2
View File
@@ -5063,7 +5063,7 @@ export function App({
...current,
{
role: 'assistant',
text: '当前仅支持确认 project.index、project.status、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.create、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.spawn_isolated、agent.schedule_ready、agent.audit、agent.trace_read、preview.status、preview.start、preview.validate、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。',
text: '当前仅支持确认 project.index、project.status、project.bootstrap、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.create、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.spawn_isolated、agent.schedule_ready、agent.audit、agent.trace_read、preview.status、preview.start、preview.validate、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。',
},
]);
return;
@@ -5126,7 +5126,7 @@ export function App({
...current,
{
role: 'assistant',
text: '当前仅支持确认 project.index、project.status、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.create、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.spawn_isolated、agent.schedule_ready、agent.audit、agent.trace_read、preview.status、preview.start、preview.validate、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。',
text: '当前仅支持确认 project.index、project.status、project.bootstrap、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.create、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.spawn_isolated、agent.schedule_ready、agent.audit、agent.trace_read、preview.status、preview.start、preview.validate、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。',
},
]);
return;
@@ -0,0 +1,26 @@
Version: 1.0
Status: implemented; real-project runtime smoke pending
Date: 2026-09-14
Parent Milestone: docs/project-memory/plans/【里程碑】DirectProject Phaser迁移闭环-2026-09-14.md
## 修改顺序
1. 扩展 shared command contract 与原生工具 schema。
2. 增加 `project.bootstrap` 的安全校验、npm install 执行、审计与分发。
3. 扩展 `project.verify` 的 cwd/package/dist 门禁和提示词。
4. 更新 DirectProject 迁移工程指导与前端命令显示。
5. 补充定向测试并运行 typecheck、Rust 检查、编码与 diff 检查。
## 验证命令
- `npm run ai-game-creator-shell:typecheck`
- `cargo test -p ai-game-creator-shell project_bootstrap`
- `cargo test -p ai-game-creator-shell project_verify`
- `npm run check:encoding`
- `git diff --check`
## 风险与回滚
-`project.verify` 输入不带 cwd 时保持项目根行为。
- bootstrap 不修改 command.exec 的 npm 白名单;失败关闭并返回可诊断错误。
- 若 sandbox 网络能力无法在当前平台验证,仅保留实现与单测证据,不宣称真实安装通过。
@@ -0,0 +1,28 @@
Version: 1.0
Status: implemented; real-project runtime smoke pending
Date: 2026-09-14
Parent Spec: docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md
## 目标
为 DirectProject 建立单 HTML 到 Phaser 4 + Vite 的可运行迁移闭环:项目文件真实落盘、受控依赖初始化、`game` 子目录构建验证、`game/dist` 预览与桌面/移动验收。
## 边界
- DirectProject 迁移请求固定使用 `workspaceMode=DirectProject`,不再通过 JSON Generator 的 `gameHtml` 协议生成 npm 工程。
- `project.bootstrap` 只允许在项目内 `game` 目录执行无参数 `npm install`,读取并记录 package/lock 指纹。
- `project.verify` 支持相对 `cwd`,构建验证必须读取该目录 package.json 的原始脚本并确认 `dist/index.html`
- 预览继续只读取构建产物;不实现自动迁移未知游戏玩法语义或真实 Provider 生成。
## 验收标准
1. 命令契约、原生工具 schema、运行时分发、权限面板和提示词包含 `project.bootstrap`
2. bootstrap 拒绝越界 cwd、非 `game` 目录、参数注入和缺失/非法 package 文件,并在成功后返回依赖指纹。
3. `project.verify` 能在 `cwd=game` 执行原样 `npm run build`,成功条件包含 `game/dist/index.html`
4. DirectProject 提示明确要求 Phaser 4.2.1、`import Phaser from 'phaser'`、Vite 输出 `game/dist` 及完整玩法迁移。
5. 定向 Rust/TypeScript 测试、类型检查、编码检查和 `git diff --check` 通过;无法运行真实 Provider/浏览器时明确记录。
## 依赖
- 现有 project command sandbox、verification gate、preview resolver 和 command run/audit 持久化。
- 用户选择的真实项目目录必须包含 `game/index.html` 才能执行实际迁移。
@@ -51,7 +51,7 @@ SpacetimeDB crate、SDK、CLI / standalone 与生成 bindings 按 `2.8.3` 对齐
## AGC DirectProject 与 UI workflow
- 新 Web 游戏为 `game/` 下的 npm + Vite + Phaser 4.2.1 工程,使用包导入且允许其它依赖;npm 预览与导出只读取 dist,运行素材需纳入构建,已有单 HTML/Godot 不自动迁移
- 新 Web 游戏为 `game/` 下的 npm + Vite + Phaser 4.2.1 工程,使用包导入且允许其它依赖;npm 预览与导出只读取 dist,运行素材需纳入构建。单 HTML → Phaser 迁移固定走 DirectProject:文件落盘后先用受控 `project.bootstrap``game` 执行无参数 `npm install`,再用支持相对 cwd 的 `project.verify` 构建并确认 `game/dist/index.html`,已有单 HTML/Godot 不通过 JSON Generator 伪装成 npm 工程
- 通用 Agent Rust 分层为 `agent-runtime-core`catalog、执行生命周期、ToolHost/spawn/all-join/Provider 契约)、`agent-runtime-orchestration`(动态无环任务图、ready、依赖波次、返工下游闭包和受限自主扩图提案)与 `platform-agent` 游戏适配器;循环返工通过新 pass / epoch 表达,不在单张依赖图中建立回边。LLM 可经宿主结构化 function call 提出新增节点/边,编排层只生成经校验的新候选图,epoch 与持久化仍由宿主掌控。
- DirectProject 始终连接客户端内置的 `agc_tools` STDIO MCP,并在启动时额外读取客户端扩展仓库中已启用的第三方 MCP 独立项。第三方 STDIO/HTTP 配置只写入本次隔离 `CODEX_HOME`,单项非 required,启停、重命名和内容指纹进入 app-server pool identity;完整 Plugin Runtime、hooks/apps 和单文件脚本手动指定入口仍关闭。Skill 正文与 references 由 Codex 原生按需读取;`agc_tools` 负责标准美术准备、已登记资源有界查询、视频 / 角色动画 / 音效 / BGM 的 create-or-derive、已登记图片去背景、desktop/mobile 浏览器试玩和受控 `agc_web_search`;付费资源调用仍由客户端绑定回合、幂等账本、请求上限和投影权威。
@@ -40,9 +40,9 @@ DirectProject 自身的 `read_direct_project_conversation` 也必须在 blocking
新建 Web 游戏使用 npm 工程:默认 `game/package.json` 声明 Phaser 4.2.1 和 Vite 构建工具,源码使用 `import Phaser from 'phaser'``package-lock.json` 由 npm 维护。默认脚手架文件位于 `game/`,包含 `index.html``game.js``style.css``vite.config.js` 和 npm 配置/锁文件;已有根目录 npm 工程沿用原根,可按需求拆分模块并添加任意其它 npm 依赖,不设置包名白名单。客户端不手工分发 Phaser bundle,不用 import map 模拟 package 导入。
Agent 在包含 `package.json` 的目录执行 `npm ci`(依赖变更使用 `npm install`)和 `npm run build`;默认可从工作区根执行 `npm --prefix game ci``npm --prefix game run build`。DirectProject 保持 workspace-write 目录边界并开放网络,以支持 npm 依赖解析和安装;凭据仍由客户端代理持有,不进入项目或原生命令环境。其它执行模式维持现有权限。新游戏完成后执行 `npm run build` 并用真实浏览器试玩;依赖安装与构建失败必须反馈真实错误,不能回退成未解析裸模块导入的静态页面。
Agent 在包含 `package.json` 的目录执行 `npm ci`(依赖变更使用 `npm install`)和 `npm run build`;默认可从工作区根执行 `npm --prefix game ci``npm --prefix game run build`。DirectProject 保持 workspace-write 目录边界并开放网络,以支持 npm 依赖解析和安装;凭据仍由客户端代理持有,不进入项目或原生命令环境。依赖初始化必须通过受控 `project.bootstrap`(固定 `cwd=game`、无参数 `npm install`、只读取 package/lock 并记录 SHA-256 指纹),不得放开通用 `command.exec npm install`。构建使用 `project.verify` 的相对 cwd(例如 `cwd=game`);`build` 通过后还必须存在 `game/dist/index.html` 才能签发验证凭证。其它执行模式维持现有权限。新游戏完成后执行 `npm run build` 并用真实浏览器试玩;依赖安装与构建失败必须反馈真实错误,不能回退成未解析裸模块导入的静态页面。
npm 游戏的可预览产物固定为对应 package 目录下的 `dist/index.html`,静态 smoke 校验构建入口及本地文件引用,实际可玩性由浏览器验证。预览服务与导出读取该构建目录,所有运行素材必须由构建纳入 dist;npm 预览不回退读取源码或项目素材目录,保证试玩与导出一致。源码投影包含 package、锁文件、配置和真实游戏源码,排除 `node_modules/``dist/`、控制文件与凭据;npm 试玩包将 dist 文件映射到 `game/` 并附带发布说明,不包含源码依赖安装目录。现有单 HTML 项目不自动迁移,Godot 项目保持原合同。旧 JSON Generator 仅接受已初始化的单 HTML 项目,npm 项目或新建请求在调用 LLM 前明确拒绝并引导使用 DirectProject,避免静态草案伪装为 npm 构建产物。本节覆盖下文仅适用于旧单 HTML 产物的 Canvas API、手写动画循环和禁止外部本地脚本要求。
npm 游戏的可预览产物固定为对应 package 目录下的 `dist/index.html`,静态 smoke 校验构建入口及本地文件引用,实际可玩性由浏览器验证。预览服务与导出读取该构建目录,所有运行素材必须由构建纳入 dist;npm 预览不回退读取源码或项目素材目录,保证试玩与导出一致。源码投影包含 package、锁文件、配置和真实游戏源码,排除 `node_modules/``dist/`、控制文件与凭据;npm 试玩包将 dist 文件映射到 `game/` 并附带发布说明,不包含源码依赖安装目录。单 HTML → Phaser 迁移必须在 DirectProject 中完成:先识别 `game/index.html`,再将状态、输入、敌人/守卫、波次、胜负、重开和画布绘制迁移到 Phaser Scene 与 update 循环,并落盘 `game/package.json``game/game.js``game/style.css``game/vite.config.js` 和锁文件;不得把迁移目标伪装成 `gameHtml`现有单 HTML 项目不自动迁移,Godot 项目保持原合同。旧 JSON Generator 仅接受已初始化的单 HTML 项目,npm 项目或新建请求在调用 LLM 前明确拒绝并引导使用 DirectProject,避免静态草案伪装为 npm 构建产物。本节覆盖下文仅适用于旧单 HTML 产物的 Canvas API、手写动画循环和禁止外部本地脚本要求。
## 2026-09-02 项目名称显示与自动提炼
@@ -34,7 +34,8 @@ describe('AI 游戏创作 App 共享契约', () => {
it('keeps command permissions explicit', () => {
const commandIds = GAME_CREATION_APP_COMMANDS.map((command) => command.id);
expect(GAME_CREATION_APP_COMMANDS).toHaveLength(65);
expect(GAME_CREATION_APP_COMMANDS).toHaveLength(66);
expect(commandIds).toContain('project.bootstrap');
expect(commandIds).toContain('project.git_inspect');
expect(commandIds).toContain('project.git_commit');
expect(commandIds).toContain('project.patchset');

Some files were not shown because too many files have changed in this diff Show More