合并最新master并解决工作台冲突

合并game-chat五分钟首版与平台美术硬门更新
解决App响应流Hook的等价内容冲突
稳定新增消息追加回调并整理合并后的导入顺序
通过前端完整测试、Lint、类型检查和Rust tests编译
This commit is contained in:
2026-08-03 10:30:48 +08:00
40 changed files with 6298 additions and 328 deletions
@@ -283,7 +283,7 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
&action_fingerprint,
pending_action,
false,
|| observe_agent_runtime_task_list(root),
|| observe_agent_runtime_task_list(root, agent_id, run_id),
),
"task.create" => observe_agent_runtime_task_create(root, agent_id, &action.input),
"task.update" => observe_agent_runtime_task_update(root, agent_id, &action.input),
@@ -414,7 +414,9 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
action_id,
&action.input,
),
"agent.schedule_ready" => observe_agent_runtime_schedule_ready_tasks(root, &action.input),
"agent.schedule_ready" => {
observe_agent_runtime_schedule_ready_tasks(root, agent_id, run_id, &action.input)
}
"agent.action_history" => observe_agent_runtime_project_snapshot_with_lock(
root,
agent_id,
@@ -584,7 +584,9 @@ pub(in crate::agent) fn autonomous_manifest_dag_in_progress_at(
})
})
})
.unwrap_or_else(|| AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE.to_string());
.ok_or_else(|| {
"无法解析当前自主构建根 Run 的可信 source,拒绝按 GUI 完整 DAG 回退".to_string()
})?;
let seed_task_ids = autonomous_manifest_seed_tasks_for_source(&source)
.into_iter()
.map(|task| task.id)
@@ -115,7 +115,7 @@ pub(in crate::agent) fn execute_game_creator_agent_runtime_parallel_safe_read_at
"git.inspect" => observe_agent_runtime_git_inspect(root, &action.input),
"file.list" => observe_agent_runtime_file_list(root, &action.input),
"file.read" => observe_agent_runtime_file(root, &action.input),
"task.list" => observe_agent_runtime_task_list(root),
"task.list" => observe_agent_runtime_task_list(root, agent_id, run_id),
_ => AgentRuntimeToolObservation {
tool: tool.to_string(),
status: "rejected".to_string(),
@@ -655,6 +655,20 @@ pub(in crate::agent) fn supervisor_collaboration_policy_completion_blocker_at_lo
if agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
return None;
}
if read_game_creator_agent_runtime_run_profile_binding(root, agent_id, run_id)
.ok()
.flatten()
.is_some_and(|binding| {
binding.profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& binding.source == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
&& binding.root_agent_id == binding.agent_id
&& binding.root_run_id == binding.run_id
})
{
// game-chat 首版由 source-aware manifest scheduler 固定编排
// code -> static smoke -> playtest,不再要求 Provider 建立额外委派波。
return None;
}
let policy = match resolve_supervisor_collaboration_policy_for_run_at(root, agent_id, run_id) {
Ok(resolution) => resolution.policy,
Err(error) => {
@@ -2,6 +2,43 @@ use super::*;
const AGENT_RUNTIME_COMPLETION_BLOCKER_TOOL_PLAN_PROTOCOL: &str = "通用完成阻断规则:如果最新 observation 的 tool 为 runtime.autonomous_completion 且 status 为 blocked,本轮禁止直接调用 respond_to_user,也禁止在 legacy response 中填写最终回复;必须先读取该 observation.detail 的 nextRequiredAction,并据此调用合适的读取、修复和验证工具。只有完成要求的动作、取得后续可信 observation 且完成门禁不再阻断后,才能给最终回复;不得反复提交 final response,也不得按项目正文硬编码某一种 blocker 的处理方式。";
const GAME_CHAT_CODE_PROTOTYPE_FAST_PATH_PROMPT: &str = "game-chat 首版使用五分钟快车道。当前任务的第一目标是在一次 Provider planning 内产出首个完整可玩版本:如果最新 observation 尚未显示 game/index.html 已由本 run 写入,本响应必须直接调用一次 file.write,把完整、自包含、可运行的 game/index.html 一次写完;禁止先调用读取、搜索、任务查询、委派、只更新计划或提交半成品。HTML 必须满足下方固定试玩合同,包含真实 Canvas 游戏循环、键盘与触控输入、开始、主要操作、重开、胜负状态和移动端布局;可以采用保守的原创玩法默认值。已登记的平台视觉规范图 ../assets/art-spec.png 是首版必需资源,必须在主要游戏画面中显著可见使用:至少把规范图实际绘制为主要背景,并从规范图中绘制玩家角色和目标实体。禁止仅放置隐藏 img、透明或屏外元素、微小水印、不可见预加载或只在源码中引用;也禁止用纯几何图形冒充平台图片使用。若规范图无法加载,游戏必须明确失败关闭,不能退回纯 Canvas 几何兜底。一次写入后不要继续扩写功能;Runtime 会在下一步自动执行静态自检并在通过后立即试玩。";
fn game_chat_fast_path_prompt_for_root_source(
agent_id: &str,
root_source: &str,
) -> Option<&'static str> {
(agent_id.trim() == "code-prototype"
&& root_source.trim() == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE)
.then_some(GAME_CHAT_CODE_PROTOTYPE_FAST_PATH_PROMPT)
}
pub(in crate::agent) fn agent_runtime_root_source_at(
root: &Path,
agent_id: &str,
run_id: &str,
) -> Result<String, String> {
let binding = read_game_creator_agent_runtime_run_profile_binding(root, agent_id, run_id)?
.ok_or_else(|| "Agent Runtime 缺少 Run Profile 绑定,无法解析 root source".to_string())?;
if binding.root_agent_id == binding.agent_id && binding.root_run_id == binding.run_id {
return Ok(binding.source);
}
let root_binding = read_game_creator_agent_runtime_run_profile_binding(
root,
&binding.root_agent_id,
&binding.root_run_id,
)?
.ok_or_else(|| "Agent Runtime 缺少 root Run Profile 绑定".to_string())?;
if root_binding.agent_id != binding.root_agent_id
|| root_binding.run_id != binding.root_run_id
|| root_binding.root_agent_id != root_binding.agent_id
|| root_binding.root_run_id != root_binding.run_id
{
return Err("Agent Runtime root Run Profile 绑定身份不一致".to_string());
}
Ok(root_binding.source)
}
fn game_creator_agent_context_preload_notice(agent_id: &str) -> &'static str {
if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
"下方已预加载有界仓库启动上下文、Supervisor 当前 Session、legacy 项目对话、项目记忆、黑板和资产摘要;源码正文仍只能通过已获准工具读取"
@@ -142,6 +179,15 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request(
system_prompt.push_str(playtest_contract);
system_prompt.push_str(" 只有当前 revision 通过 game.static_smoke,并由 preview.validate 对上述固定状态面和控件完成真实浏览器动作后,Runtime 才允许最终回复;不要伪造已通过 observation。");
}
if autonomous_game_build {
let root_source = agent_runtime_root_source_at(root, agent_id, run_id)?;
if let Some(fast_path_prompt) =
game_chat_fast_path_prompt_for_root_source(agent_id, &root_source)
{
system_prompt.push_str("\n\n");
system_prompt.push_str(fast_path_prompt);
}
}
let mut request = LlmRunRequest::new(vec![
LlmMessage::system(system_prompt),
LlmMessage::user(prompt),
@@ -349,10 +395,13 @@ pub(in crate::agent) fn build_game_creator_background_agent_context(
#[cfg(test)]
mod tests {
use super::{
agent_runtime_root_source_at, bind_game_creator_agent_runtime_run_profile_at,
build_game_creator_agent_background_tool_plan_request,
game_creator_agent_context_preload_notice, init_local_game_project_at,
start_game_creator_agent_runtime_task_at, GameCreatorMcpCatalog,
AGENT_RUNTIME_COMPLETION_BLOCKER_TOOL_PLAN_PROTOCOL,
game_chat_fast_path_prompt_for_root_source, game_creator_agent_context_preload_notice,
init_local_game_project_at, start_game_creator_agent_runtime_task_at, AgentRuntimeTaskLink,
GameCreatorMcpCatalog, AGENT_RUNTIME_COMPLETION_BLOCKER_TOOL_PLAN_PROTOCOL,
AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD, AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE, AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
};
@@ -456,4 +505,87 @@ mod tests {
assert!(protocol.contains("不得反复提交 final response"));
assert!(protocol.contains("不得按项目正文硬编码"));
}
#[test]
fn game_chat_fast_path_prompt_forces_one_shot_playable_write_only_for_code_agent() {
let prompt = game_chat_fast_path_prompt_for_root_source(
"code-prototype",
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
)
.expect("game-chat code fast path prompt");
assert!(prompt.contains("五分钟快车道"));
assert!(prompt.contains("一次 Provider planning"));
assert!(prompt.contains("直接调用一次 file.write"));
assert!(prompt.contains("禁止先调用读取、搜索、任务查询、委派"));
assert!(prompt.contains("../assets/art-spec.png"));
assert!(prompt.contains("平台视觉规范图"));
assert!(prompt.contains("主要背景"));
assert!(prompt.contains("玩家角色和目标实体"));
assert!(prompt.contains("禁止仅放置隐藏 img"));
assert!(prompt.contains("不能退回纯 Canvas 几何兜底"));
assert!(!prompt.contains("art-spritesheet.png"));
assert!(game_chat_fast_path_prompt_for_root_source(
"quality-review",
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
)
.is_none());
assert!(game_chat_fast_path_prompt_for_root_source(
"code-prototype",
AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
)
.is_none());
assert!(game_chat_fast_path_prompt_for_root_source(
"code-prototype",
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
)
.is_none());
assert!(game_chat_fast_path_prompt_for_root_source(
"code-prototype",
"agent-background-task",
)
.is_none());
}
#[test]
fn root_source_resolver_uses_root_binding_for_game_chat_child() {
let temporary = tempfile::tempdir().expect("temporary project root");
let root = temporary.path().join("project");
init_local_game_project_at(&root, "root-source-project", "root source test")
.expect("project init");
let parent = bind_game_creator_agent_runtime_run_profile_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"root-source-game-chat-run",
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
None,
)
.expect("bind game-chat root profile");
let child_link = AgentRuntimeTaskLink {
parent_agent_id: Some(parent.agent_id.clone()),
parent_run_id: Some(parent.run_id.clone()),
delegation_id: Some("root-source-game-chat-child-delegation".to_string()),
};
let child = bind_game_creator_agent_runtime_run_profile_at(
&root,
"code-prototype",
"root-source-game-chat-child",
"agent-ready-task-scheduler",
None,
Some(&child_link),
)
.expect("bind game-chat child profile");
assert_eq!(
agent_runtime_root_source_at(&root, &parent.agent_id, &parent.run_id)
.expect("resolve root source"),
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
);
assert_eq!(
agent_runtime_root_source_at(&root, &child.agent_id, &child.run_id)
.expect("resolve child root source"),
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
);
}
}
@@ -206,10 +206,13 @@ pub(super) struct AgentRuntimeAutonomousPlaytestReceipt {
mod entrypoints;
mod finalization;
mod game_chat_fast_path;
mod interaction;
mod lifecycle_control;
mod main_loop;
#[cfg(test)]
mod main_loop_deadline_tests;
#[cfg(test)]
mod main_loop_tests;
mod pending_execution;
mod pending_recovery;
@@ -220,6 +223,7 @@ mod task_start;
pub(in crate::agent) use entrypoints::*;
pub(in crate::agent) use finalization::*;
pub(in crate::agent) use game_chat_fast_path::*;
pub(in crate::agent) use interaction::*;
pub(in crate::agent) use lifecycle_control::*;
pub(in crate::agent) use main_loop::*;
@@ -278,6 +282,7 @@ pub(crate) use provider_recovery::{
};
pub(crate) use recovery_scan::{
cleanup_game_creator_agent_runtime_completed_finalizations_at,
has_recoverable_game_creator_agent_background_tasks_at,
resume_game_creator_agent_background_tasks_at,
resume_game_creator_agent_pending_action_for_agent_at,
wake_pending_game_creator_agent_background_tasks_at,
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,162 @@
use super::main_loop::{
await_game_chat_absolute_deadline_at, finish_game_chat_absolute_deadline_timeout_at,
game_chat_absolute_deadline_from_bound_at,
};
use super::*;
#[test]
fn game_chat_absolute_deadline_is_root_bound_at_plus_hard_budget() {
let now_instant = tokio::time::Instant::now();
let bound_at = 10_000;
let now_unix = bound_at + 17;
let deadline = game_chat_absolute_deadline_from_bound_at(now_instant, now_unix, bound_at);
assert_eq!(
deadline.duration_since(now_instant),
std::time::Duration::from_secs(GAME_CHAT_FIRST_PLAYABLE_HARD_BUDGET_SECONDS - 17)
);
}
#[test]
fn game_chat_absolute_deadline_is_immediate_once_root_budget_is_exhausted() {
let now_instant = tokio::time::Instant::now();
let bound_at = 20_000;
let deadline = game_chat_absolute_deadline_from_bound_at(
now_instant,
bound_at + GAME_CHAT_FIRST_PLAYABLE_HARD_BUDGET_SECONDS + 1,
bound_at,
);
assert_eq!(deadline, now_instant);
}
#[tokio::test]
async fn game_chat_absolute_deadline_stops_a_never_resolving_in_flight_action() {
let deadline = tokio::time::Instant::now() + std::time::Duration::from_millis(20);
let started = std::time::Instant::now();
let result = await_game_chat_absolute_deadline_at(
deadline,
std::future::pending::<AgentBackgroundTaskOutcome>(),
)
.await;
assert!(
result.is_err(),
"never-resolving action must hit the hard deadline"
);
assert!(
started.elapsed() < std::time::Duration::from_secs(1),
"deadline test must not hang"
);
}
#[tokio::test]
async fn game_chat_absolute_deadline_returns_an_in_flight_result_before_expiry() {
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(1);
let result = await_game_chat_absolute_deadline_at(deadline, async { "completed" }).await;
assert_eq!(result.expect("in-flight action completes"), "completed");
}
#[test]
fn game_chat_absolute_deadline_forces_needs_reconciliation_to_failed_before_cleanup() {
let root = std::env::temp_dir().join(format!(
"genarrative-game-chat-deadline-reconciliation-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system clock")
.as_nanos()
));
init_local_game_project_at(&root, "deadline-reconciliation", "硬截止收尾测试")
.expect("project init");
let mut runtime = start_game_creator_agent_runtime_task_at(
&root,
"code-prototype",
"执行可能悬挂的首版写入",
"game-chat-deadline-reconciliation-run",
"agent-ready-task-scheduler",
"正在执行首版写入",
vec!["执行首版写入".to_string()],
)
.expect("start runtime");
runtime.loop_iteration = 1;
let action = AgentRuntimeToolAction {
tool: "file.write".to_string(),
reason: Some("模拟截止时仍在途的写入".to_string()),
input: serde_json::json!({
"path": "game/index.html",
"content": "<!doctype html><title>deadline</title>"
}),
};
let plan = AgentRuntimeToolPlan {
thinking_summary: "准备首版写入".to_string(),
plan_update: None,
plan: vec!["写入首版".to_string()],
actions: vec![action.clone()],
response: String::new(),
};
let project_revision =
read_game_creator_agent_runtime_project_revision(&root).expect("read project revision");
let repository_fingerprint = build_repository_startup_context_at(&root)
.expect("repository context")
.fingerprint;
let pending = build_game_creator_agent_runtime_pending_tool_action(
&root,
&runtime,
&runtime.current_task,
&plan,
&[],
&project_revision,
&repository_fingerprint,
&action,
0,
AGENT_RUNTIME_ACTION_EXECUTION_MODE_AUTO,
AGENT_RUNTIME_PENDING_ACTION_STATUS_APPROVED,
None,
)
.expect("build pending action");
write_game_creator_agent_runtime_pending_tool_action(&root, &pending)
.expect("write pending action");
runtime.pending_tool_action = Some(pending.summary());
runtime.status = "failed".to_string();
runtime.phase = "needs-reconciliation".to_string();
runtime.current_action = "等待人工核对在途动作".to_string();
runtime.waiting_on = "开发者核对副作用".to_string();
runtime.next_step = "核对后恢复".to_string();
runtime.error = Some("模拟 needs-reconciliation".to_string());
append_game_creator_agent_runtime_task(&root, &runtime).expect("append reconciliation task");
write_game_creator_agent_runtime_state(&root, &runtime).expect("write reconciliation state");
let outcome = finish_game_chat_absolute_deadline_timeout_at(
&root,
&runtime.agent_id,
&runtime.session_id,
runtime.clone(),
);
assert!(matches!(outcome, AgentBackgroundTaskOutcome::Finished));
let terminal = read_game_creator_agent_runtime_at(&root, &runtime.agent_id)
.expect("read terminal runtime")
.state;
assert_eq!(terminal.run_id, runtime.run_id);
assert_eq!(terminal.status, "failed");
assert_eq!(terminal.phase, "failed");
assert!(terminal.pending_tool_action.is_none());
assert!(!game_creator_agent_runtime_pending_tool_action_exists(
&root,
&runtime.agent_id,
&runtime.run_id
));
assert!(!game_creator_agent_runtime_provider_action_batch_exists(
&root,
&runtime.agent_id,
&runtime.run_id
));
fs::remove_dir_all(root).ok();
}
@@ -25,6 +25,86 @@ fn register_autonomous_recovery_visual_fixture(root: &Path, local_path: &str, ki
.expect("register autonomous recovery visual fixture");
}
fn register_game_chat_art_spec_fixture(root: &Path) {
image::RgbaImage::from_pixel(4, 4, image::Rgba([90, 140, 220, u8::MAX]))
.save(root.join(AGENT_RUNTIME_ART_SPEC_PATH))
.expect("write game-chat art spec PNG");
register_local_asset_at(
root,
AGENT_RUNTIME_ART_SPEC_PATH,
"icon-spec",
"image/png",
"canvas",
GameCreationAppAssetSource {
kind: GameCreationAppAssetSourceKind::Canvas,
canvas_project_id: Some("game-chat-canvas".to_string()),
resource_id: Some("game-chat-art-spec-resource".to_string()),
asset_object_id: Some("game-chat-art-spec-object".to_string()),
task_id: Some("art-director".to_string()),
prompt: None,
model: None,
generation_route: Some("/api/external/v1/editor/images/generations".to_string()),
generation_kind: Some("spec".to_string()),
reference_resource_ids: Vec::new(),
},
)
.expect("register game-chat art spec");
}
fn queue_game_chat_fast_path_child(
root: &Path,
root_run_id: &str,
root_task: &str,
child_id: &str,
) -> (AgentRuntimeState, AgentRuntimeState) {
let root_session = resolve_agent_conversation_session_id_at(
root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
None,
true,
)
.expect("resolve game-chat root session");
let root_record = append_unique_game_creator_agent_runtime_pending_task(
root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
&root_session,
root_task,
root_run_id,
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
None,
)
.expect("queue game-chat root");
let root_state = agent_runtime_state_from_task_record(&root_record);
let manifest = read_manifest_for_project(root).expect("read game-chat manifest");
let task = manifest
.tasks
.iter()
.find(|task| task.id == child_id)
.unwrap_or_else(|| panic!("missing game-chat child task {child_id}"));
let child_session = resolve_agent_conversation_session_id_at(root, child_id, None, true)
.expect("resolve game-chat child session");
let child_record = append_unique_game_creator_agent_runtime_pending_task(
root,
child_id,
&child_session,
&render_autonomous_manifest_ready_task_background_prompt(task),
&autonomous_manifest_ready_task_run_id(root_run_id, child_id),
"agent-ready-task-scheduler",
None,
Some(&AgentRuntimeTaskLink {
parent_agent_id: Some(root_state.agent_id.clone()),
parent_run_id: Some(root_state.run_id.clone()),
delegation_id: None,
}),
)
.expect("queue game-chat fast-path child");
(
root_state,
agent_runtime_state_from_task_record(&child_record),
)
}
#[test]
fn autonomous_parent_keeps_planning_before_scheduling_registered_legacy_derived_visuals() {
let root = std::env::temp_dir().join(format!(
@@ -65,7 +145,7 @@ fn prepare_autonomous_completion_evidence(root: &Path, state: &AgentRuntimeState
write_local_project_file_at(
root,
AGENT_RUNTIME_GAME_INDEX_PATH,
"<!doctype html><title>可玩塔防</title><canvas></canvas>",
&render_game_chat_fast_path_html("可玩塔防"),
)
.expect("write autonomous game index");
for (path, content) in [
@@ -84,6 +164,7 @@ fn prepare_autonomous_completion_evidence(root: &Path, state: &AgentRuntimeState
}
revision
};
register_game_chat_art_spec_fixture(root);
for task in new_game_creation_app_seed_tasks() {
update_manifest_task_status_at(root, &task.id, GameCreationAppTaskStatus::Completed)
.expect("complete autonomous manifest task");
@@ -263,6 +344,214 @@ fn autonomous_visual_ready_tasks_only_require_images_when_editor_api_key_is_conf
));
}
#[test]
fn game_chat_art_director_uses_deterministic_canvas_plan_without_provider_planning() {
let _config_guard = crate::tests::write_test_local_config(
r#"{"editorApi":{"apiKey":"game-chat-fast-art-key"}}"#.to_string(),
);
let temporary = tempfile::tempdir().expect("create game-chat art root");
let root = temporary.path().join("project");
init_local_game_project_at(&root, "game-chat-art-director", "星空飞船收集能量")
.expect("init game-chat art project");
let (root_state, child_state) = queue_game_chat_fast_path_child(
&root,
"game-chat-art-director-root",
"制作星空飞船收集能量小游戏",
"art-director",
);
let binding = read_game_creator_agent_runtime_run_profile_binding(
&root,
&root_state.agent_id,
&root_state.run_id,
)
.expect("read game-chat root binding")
.expect("game-chat root binding exists");
let plan = game_chat_fast_path_plan_at(
&root,
&child_state,
&child_state.current_task,
binding.bound_at,
)
.expect("build deterministic art-director plan")
.expect("art-director fast path must bypass Provider planning");
assert_eq!(plan.actions.len(), 1);
assert_eq!(plan.actions[0].tool, "canvas.asset_generate");
assert_eq!(
plan.actions[0].input["outputPath"],
AGENT_RUNTIME_ART_SPEC_PATH
);
assert_eq!(plan.actions[0].input["assetKind"], "icon-spec");
assert_eq!(plan.actions[0].input["aspectRatio"], "1:1");
assert_eq!(plan.actions[0].input["imageSize"], "1K");
assert_eq!(plan.actions[0].input["replaceExisting"], false);
let serialized = serde_json::to_string(&plan).expect("serialize deterministic art plan");
assert!(!serialized.contains("art-spritesheet"));
assert!(!serialized.contains("icon-spritesheets/generations"));
assert!(!root.join("assets/manifest.art.json").exists());
}
#[test]
fn game_chat_art_stage_fails_before_provider_when_editor_api_is_missing() {
let _config_guard = crate::tests::write_test_local_config("{}".to_string());
let temporary = tempfile::tempdir().expect("create unconfigured game-chat art root");
let root = temporary.path().join("project");
init_local_game_project_at(&root, "game-chat-art-unconfigured", "太空躲避")
.expect("init unconfigured game-chat art project");
let (root_state, child_state) = queue_game_chat_fast_path_child(
&root,
"game-chat-art-unconfigured-root",
"制作太空躲避小游戏",
"art-director",
);
let binding = read_game_creator_agent_runtime_run_profile_binding(
&root,
&root_state.agent_id,
&root_state.run_id,
)
.expect("read game-chat root binding")
.expect("game-chat root binding exists");
let error = game_chat_fast_path_plan_at(
&root,
&child_state,
&child_state.current_task,
binding.bound_at,
)
.expect_err("unconfigured game-chat art must fail closed before Provider planning");
assert!(error.contains("External Editor API Key"));
assert!(error.contains("平台美术资源"));
}
#[test]
fn game_chat_art_fast_path_idempotently_settles_registered_art_spec() {
let _config_guard = crate::tests::write_test_local_config(
r#"{"editorApi":{"apiKey":"game-chat-idempotent-art-key"}}"#.to_string(),
);
let art_director_temporary = tempfile::tempdir().expect("create idempotent art-spec root");
let art_director_root = art_director_temporary.path().join("project");
init_local_game_project_at(
&art_director_root,
"game-chat-idempotent-art-spec",
"海岛收集",
)
.expect("init idempotent art-spec project");
register_game_chat_art_spec_fixture(&art_director_root);
let (root_state, art_director_state) = queue_game_chat_fast_path_child(
&art_director_root,
"game-chat-idempotent-art-spec-root",
"制作海岛收集小游戏",
"art-director",
);
let bound_at = read_game_creator_agent_runtime_run_profile_binding(
&art_director_root,
&root_state.agent_id,
&root_state.run_id,
)
.expect("read idempotent art-spec root binding")
.expect("idempotent art-spec root binding exists")
.bound_at;
let art_director_plan = game_chat_fast_path_plan_at(
&art_director_root,
&art_director_state,
&art_director_state.current_task,
bound_at,
)
.expect("settle registered art spec")
.expect("registered art spec must use deterministic settlement");
assert!(art_director_plan.actions.is_empty());
assert!(art_director_plan.response.contains("已生成并登记"));
}
#[test]
fn game_chat_code_prototype_ignores_art_director_global_revision_before_its_own_mutation() {
let _config_guard = crate::tests::write_test_local_config(
r#"{"editorApi":{"apiKey":"game-chat-code-revision-key"}}"#.to_string(),
);
let temporary = tempfile::tempdir().expect("create game-chat code revision root");
let root = temporary.path().join("project");
init_local_game_project_at(&root, "game-chat-code-revision", "太空飞船收集能量")
.expect("init game-chat code revision project");
let (root_state, art_director_state) = queue_game_chat_fast_path_child(
&root,
"game-chat-code-revision-root",
"制作太空飞船收集能量小游戏",
"art-director",
);
{
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
&root,
"test.game-chat.art-director.mutate",
)
.expect("acquire art-director mutation lock");
let revision = prepare_agent_runtime_project_mutation_locked(
&root,
&art_director_state.agent_id,
&art_director_state.run_id,
"canvas.asset_generate",
)
.expect("advance global revision for art-director");
assert_eq!(revision, 1);
}
let manifest = read_manifest_for_project(&root).expect("read game-chat manifest");
let code_task = manifest
.tasks
.iter()
.find(|task| task.id == "code-prototype")
.expect("code-prototype manifest task");
let code_session =
resolve_agent_conversation_session_id_at(&root, "code-prototype", None, true)
.expect("resolve code-prototype session");
let code_record = append_unique_game_creator_agent_runtime_pending_task(
&root,
"code-prototype",
&code_session,
&render_autonomous_manifest_ready_task_background_prompt(code_task),
&autonomous_manifest_ready_task_run_id(&root_state.run_id, "code-prototype"),
"agent-ready-task-scheduler",
None,
Some(&AgentRuntimeTaskLink {
parent_agent_id: Some(root_state.agent_id.clone()),
parent_run_id: Some(root_state.run_id.clone()),
delegation_id: None,
}),
)
.expect("queue code-prototype child after art mutation");
let code_state = agent_runtime_state_from_task_record(&code_record);
let code_gate = read_game_creator_agent_runtime_verification_gate(
&root,
&code_state.agent_id,
&code_state.run_id,
)
.expect("read code-prototype gate");
assert_eq!(code_gate.mutation_revision, None);
assert_eq!(
read_game_creator_agent_runtime_project_revision(&root)
.expect("read global revision")
.revision,
1
);
let bound_at = read_game_creator_agent_runtime_run_profile_binding(
&root,
&root_state.agent_id,
&root_state.run_id,
)
.expect("read game-chat root binding")
.expect("game-chat root binding exists")
.bound_at;
let plan = game_chat_fast_path_plan_at(&root, &code_state, &code_state.current_task, bound_at)
.expect("evaluate code-prototype fast path");
assert!(
plan.is_none(),
"art-director's global revision must not skip the code Provider"
);
}
#[test]
fn autonomous_supervisor_empty_plan_uses_deterministic_final_reply_fallback() {
assert_eq!(
@@ -277,6 +566,157 @@ fn autonomous_supervisor_empty_plan_uses_deterministic_final_reply_fallback() {
);
}
#[test]
fn game_chat_single_round_converges_without_another_provider_plan_after_playtest() {
const RUN_ID: &str = "game-chat-single-round-convergence";
const TASK: &str = "生成一个可玩的原创塔防小游戏,完成一轮后停止并打开预览";
let _config_guard = crate::tests::write_test_local_config("{}".to_string());
let temporary = tempfile::tempdir().expect("create game-chat convergence root");
let root = temporary.path().join("project");
init_local_game_project_at(&root, "game-chat-convergence", TASK)
.expect("init game-chat convergence project");
let session_id = resolve_agent_conversation_session_id_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
None,
true,
)
.expect("resolve game-chat Supervisor session");
let task_record = append_unique_game_creator_agent_runtime_pending_task(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
&session_id,
TASK,
RUN_ID,
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
None,
)
.expect("queue game-chat Supervisor task");
let mut runtime = agent_runtime_state_from_task_record(&task_record);
apply_agent_runtime_plan_update(
&mut runtime,
&AgentRuntimePlanUpdate {
explanation: "模型原本规划了继续迭代".to_string(),
steps: vec![
AgentRuntimePlanUpdateStep {
step: "实现游戏".to_string(),
status: AGENT_RUNTIME_PLAN_STATUS_COMPLETED.to_string(),
},
AgentRuntimePlanUpdateStep {
step: "完成试玩".to_string(),
status: AGENT_RUNTIME_PLAN_STATUS_IN_PROGRESS.to_string(),
},
AgentRuntimePlanUpdateStep {
step: "继续下一轮".to_string(),
status: AGENT_RUNTIME_PLAN_STATUS_PENDING.to_string(),
},
],
},
)
.expect("apply structured game-chat plan");
assert!(runtime.plan_revision > 0);
let revision = prepare_autonomous_completion_evidence(&root, &runtime);
for task in new_game_creation_app_seed_tasks() {
if !matches!(
task.id.as_str(),
"art-director" | "code-prototype" | "preview-readiness" | "preview-playtest"
) {
update_manifest_task_status_at(&root, &task.id, GameCreationAppTaskStatus::Pending)
.unwrap_or_else(|error| panic!("leave non-fast-path task pending: {error}"));
}
}
let mut provider_plan = AgentRuntimeToolPlan {
actions: vec![AgentRuntimeToolAction {
tool: "agent.run_status".to_string(),
reason: Some("模型原本还想继续轮询".to_string()),
input: serde_json::json!({}),
}],
..AgentRuntimeToolPlan::default()
};
let convergence = prepare_game_chat_single_round_convergence_at(
&root,
&mut runtime,
&mut provider_plan,
unix_timestamp(),
)
.expect("prepare deterministic game-chat convergence")
.expect("playtest-complete game-chat run must converge");
assert_eq!(convergence.1, revision);
assert!(convergence.0.contains(&format!("revision {revision}")));
assert!(provider_plan.actions.is_empty());
assert!(runtime
.plan_steps
.iter()
.all(|step| step.status == "completed"));
assert_eq!(runtime.current_action, "首个可试玩版本已完成,正在结束本轮");
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &runtime).is_none());
}
#[test]
fn game_chat_single_round_cannot_converge_after_the_hard_budget() {
const RUN_ID: &str = "game-chat-single-round-hard-budget";
const TASK: &str = "生成一个可玩的原创塔防小游戏,并在五分钟内停止";
let _config_guard = crate::tests::write_test_local_config("{}".to_string());
let temporary = tempfile::tempdir().expect("create game-chat hard budget root");
let root = temporary.path().join("project");
init_local_game_project_at(&root, "game-chat-hard-budget", TASK)
.expect("init game-chat hard budget project");
let session_id = resolve_agent_conversation_session_id_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
None,
true,
)
.expect("resolve game-chat Supervisor session");
let task_record = append_unique_game_creator_agent_runtime_pending_task(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
&session_id,
TASK,
RUN_ID,
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
None,
)
.expect("queue game-chat Supervisor task");
let mut runtime = agent_runtime_state_from_task_record(&task_record);
prepare_autonomous_completion_evidence(&root, &runtime);
for task in new_game_creation_app_seed_tasks() {
if !matches!(
task.id.as_str(),
"art-director" | "code-prototype" | "preview-readiness" | "preview-playtest"
) {
update_manifest_task_status_at(&root, &task.id, GameCreationAppTaskStatus::Pending)
.unwrap_or_else(|error| panic!("leave non-fast-path task pending: {error}"));
}
}
let binding = read_game_creator_agent_runtime_run_profile_binding(
&root,
&runtime.agent_id,
&runtime.run_id,
)
.expect("read game-chat root binding")
.expect("game-chat root binding exists");
let mut provider_plan = AgentRuntimeToolPlan::default();
let error = prepare_game_chat_single_round_convergence_at(
&root,
&mut runtime,
&mut provider_plan,
binding.bound_at + GAME_CHAT_FIRST_PLAYABLE_HARD_BUDGET_SECONDS,
)
.expect_err("hard-budget-expired game-chat run must not converge");
assert!(error.starts_with(GAME_CHAT_FIRST_PLAYABLE_HARD_BUDGET_ERROR_PREFIX));
assert!(!runtime
.observations
.iter()
.any(|observation| observation.contains("单轮完成门已通过")));
}
#[test]
fn autonomous_specialist_empty_plan_uses_internal_completion_fallback() {
assert_eq!(
@@ -1,7 +1,17 @@
use super::*;
pub(crate) fn autonomous_manifest_parent_wake_error_is_transient(error: &str) -> bool {
let normalized = error.to_ascii_lowercase();
error.starts_with("项目正在被其他写操作占用:")
|| error.contains("另一个程序正在使用此文件")
|| normalized.contains("sharing violation")
|| normalized.contains("lock violation")
|| normalized.contains("os error 32")
|| normalized.contains("os error 33")
|| normalized.contains("resource temporarily unavailable")
|| normalized.contains("would block")
|| normalized.contains("timed out")
|| normalized.contains("timeout")
}
pub(in crate::agent) fn schedule_waiting_provider_retry_wake_after_lane_release(
@@ -109,7 +119,7 @@ async fn drive_waiting_autonomous_manifest_parent_wake_pass(
agent_id: &str,
run_id: &str,
) {
for _ in 0..40 {
for _ in 0..200 {
tokio::time::sleep(Duration::from_millis(10)).await;
let Ok(Some(task)) =
read_latest_game_creator_agent_runtime_task_by_run_id(root, agent_id, run_id)
@@ -124,6 +134,9 @@ async fn drive_waiting_autonomous_manifest_parent_wake_pass(
Ok(false) => match autonomous_manifest_dag_in_progress_at(root) {
Ok(true) => return,
Ok(false) => {}
Err(error) if autonomous_manifest_parent_wake_error_is_transient(&error) => {
continue;
}
Err(error) => {
let _ = mark_autonomous_manifest_parent_wake_needs_reconciliation_at(
root, agent_id, run_id, &error,
@@ -422,6 +422,108 @@ pub(crate) fn resume_game_creator_agent_background_tasks_at(
.map_err(|error| redact_agent_runtime_error(root, &error, 500))
}
pub(crate) fn has_recoverable_game_creator_agent_background_tasks_at(
root: &Path,
) -> Result<bool, String> {
validate_project_root(root)?;
for relative_directory in [
".agent/runtime/finalizations",
".agent/runtime/tool-plan-handoffs",
".agent/runtime/provider-retries",
".agent/runtime/pending-actions",
".agent/runtime/parallel-read-batches",
".agent/runtime/provider-action-batches",
".agent/runtime/cancel",
] {
if durable_agent_runtime_recovery_directory_has_entries(&root.join(relative_directory)) {
return Ok(true);
}
}
if durable_process_session_recovery_exists_at(root) {
return Ok(true);
}
for agent_id in collect_game_creator_agent_runtime_agent_ids(root)? {
match read_recoverable_game_creator_agent_runtime_task(root, &agent_id) {
Ok(Some(_)) | Err(_) => return Ok(true),
Ok(None) => {}
}
}
Ok(false)
}
fn durable_agent_runtime_recovery_directory_has_entries(directory: &Path) -> bool {
let mut pending_directories = vec![directory.to_path_buf()];
while let Some(directory) = pending_directories.pop() {
let entries = match fs::read_dir(&directory) {
Ok(entries) => entries,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
Err(_) => return true,
};
for entry in entries {
let entry = match entry {
Ok(entry) => entry,
Err(_) => return true,
};
let file_type = match entry.file_type() {
Ok(file_type) => file_type,
Err(_) => return true,
};
if file_type.is_symlink() || !file_type.is_dir() {
return true;
}
pending_directories.push(entry.path());
}
}
false
}
fn durable_process_session_recovery_exists_at(root: &Path) -> bool {
let directory = root.join(".agent/runtime/process-sessions");
let entries = match fs::read_dir(&directory) {
Ok(entries) => entries,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return false,
Err(_) => return true,
};
for entry in entries {
let entry = match entry {
Ok(entry) => entry,
Err(_) => return true,
};
let Some(file_name) = entry.file_name().to_str().map(str::to_string) else {
return true;
};
if !file_name.ends_with(".json") || file_name.ends_with(".output.json") {
continue;
}
match entry.file_type() {
Ok(file_type) if !file_type.is_symlink() && file_type.is_file() => {}
_ => return true,
}
let relative_path = format!(".agent/runtime/process-sessions/{file_name}");
let record = match read_agent_runtime_json_sidecar_with_max_bytes::<ProcessSessionRecord>(
root,
&relative_path,
"Agent Runtime process session preflight",
32 * 1024,
) {
Ok(Some(record)) => record,
Ok(None) | Err(_) => return true,
};
if record.schema_version != "3" || record.needs_reconciliation {
return true;
}
if !matches!(
record.status.as_str(),
"exited" | "terminated" | "timed-out" | "output-limit-exceeded" | "failed"
) {
return true;
}
}
false
}
pub(in crate::agent) fn resume_game_creator_agent_background_tasks_unredacted_at(
root: &Path,
) -> Result<Vec<AgentRuntimeResult>, String> {
@@ -659,14 +659,24 @@ pub(in crate::agent) fn autonomous_manifest_seed_tasks_for_source(
if source.trim() == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE {
seed_tasks
.into_iter()
.take_while(|task| task.id != "publish-strategy")
.filter_map(|mut task| {
let dependencies = match task.id.as_str() {
"art-director" => Some(Vec::new()),
"code-prototype" => Some(vec!["art-director".to_string()]),
"preview-readiness" => Some(vec!["code-prototype".to_string()]),
"preview-playtest" => Some(vec!["preview-readiness".to_string()]),
_ => None,
}?;
task.dependencies = dependencies;
Some(task)
})
.collect()
} else {
seed_tasks
}
}
fn autonomous_manifest_ready_task_ids(
pub(in crate::agent) fn autonomous_manifest_ready_task_ids(
tasks: &[GameCreationAppTaskState],
source: &str,
) -> Vec<String> {
@@ -680,7 +690,7 @@ fn autonomous_manifest_ready_task_ids(
| GameCreationAppTaskStatus::WaitingForConfirmation
);
(status_is_ready
&& task.dependencies.iter().all(|dependency| {
&& seed_task.dependencies.iter().all(|dependency| {
task_has_status(tasks, dependency, GameCreationAppTaskStatus::Completed)
}))
.then(|| task.id.clone())
@@ -1152,8 +1162,12 @@ pub(in crate::agent) fn project_autonomous_manifest_ready_task_terminal_at_locke
manifest_task,
&task_text,
)?;
let game_chat_requires_visual_asset = root_parent_binding.source
== AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
&& manifest_task.id == "art-director";
if status == GameCreationAppTaskStatus::Completed
&& autonomous_manifest_ready_task_requires_visual_asset(&manifest_task.id)
&& (autonomous_manifest_ready_task_requires_visual_asset(&manifest_task.id)
|| game_chat_requires_visual_asset)
&& !manifest_has_required_visual_asset(root, &manifest, &manifest_task.id)
{
status = GameCreationAppTaskStatus::Failed;
@@ -1185,6 +1199,50 @@ pub(in crate::agent) fn project_autonomous_manifest_ready_task_terminal_at_locke
));
}
}
if status == GameCreationAppTaskStatus::Completed
&& state.agent_id == "code-prototype"
&& root_parent_binding.source == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
{
let revision = read_game_creator_agent_runtime_project_revision(root)?;
let child_gate = read_game_creator_agent_runtime_verification_gate(
root,
&state.agent_id,
&state.run_id,
)?;
if child_gate.verified_revision != Some(revision.revision)
|| child_gate.last_verification_tool.as_deref() != Some("game.static_smoke")
|| child_gate.last_verification_status.as_deref()
!= Some(AGENT_RUNTIME_VERIFICATION_STATUS_PASSED)
{
return Err("game-chat code-prototype 缺少当前 revision 的静态验证凭证".to_string());
}
let mut root_gate = read_game_creator_agent_runtime_verification_gate(
root,
&parent_agent_id,
&parent_run_id,
)?;
root_gate.requires_verification = false;
root_gate.mutation_revision = None;
root_gate.verified_revision = Some(revision.revision);
root_gate.last_mutation_tool = None;
root_gate.last_verification_tool = Some("game.static_smoke".to_string());
root_gate.last_verification_status =
Some(AGENT_RUNTIME_VERIFICATION_STATUS_PASSED.to_string());
root_gate.failed_playtest_revision = None;
root_gate.updated_at = unix_timestamp();
write_game_creator_agent_runtime_verification_gate(root, &root_gate)?;
append_agent_db_record(
root,
serde_json::json!({
"recordType": "agent.runtime.game_chat.static_verification_projected",
"agentId": state.agent_id,
"runId": state.run_id,
"parentAgentId": parent_agent_id,
"parentRunId": parent_run_id,
"revision": revision.revision,
}),
)?;
}
update_manifest_task_status_at(root, &state.agent_id, status.clone())?;
append_agent_db_record(
root,
@@ -1266,7 +1324,7 @@ pub(in crate::agent) fn render_autonomous_manifest_ready_task_background_prompt(
let code_visual_asset_requirement = if task.id == "code-prototype"
&& editor_api_key_is_configured()
{
" External Editor API 已配置时必须先调用 asset.list 确认 assets/art-spritesheet.png 已登记且 source.kind=canvas、资源有效game/index.html 必须实际通过 HTML、CSS background 或 Canvas drawImage 引用 assets/art-spritesheet.png 作为游戏 UI 素材,不得只用 emoji、色块、CSS 绘图或占位文本冒充。"
" External Editor API 已配置时必须先调用 asset.list 核对 Canvas 登记与资源有效性:game-chat Run 使用 assets/art-spec.pngicon-spec、images/generations/spec),GUI/CLI Run 使用 assets/art-spritesheet.pnggame/index.html 必须通过 HTML、CSS background 或 Canvas drawImage 可见使用对应资源,不得只用 emoji、色块、CSS 绘图或占位文本冒充。"
} else {
""
};
@@ -71,6 +71,78 @@ fn autonomous_supervisor_source_allowlist_includes_game_chat_only() {
));
}
#[test]
fn game_chat_manifest_seed_projection_is_a_serial_four_task_lane() {
let game_chat_tasks =
autonomous_manifest_seed_tasks_for_source(AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE);
assert_eq!(
game_chat_tasks
.iter()
.map(|task| task.id.as_str())
.collect::<Vec<_>>(),
[
"art-director",
"code-prototype",
"preview-readiness",
"preview-playtest",
]
);
assert_eq!(game_chat_tasks[0].dependencies, Vec::<String>::new());
assert_eq!(
game_chat_tasks[1].dependencies,
vec!["art-director".to_string()]
);
assert_eq!(
game_chat_tasks[2].dependencies,
vec!["code-prototype".to_string()]
);
assert_eq!(
game_chat_tasks[3].dependencies,
vec!["preview-readiness".to_string()]
);
let full_seed_tasks = new_game_creation_app_seed_tasks();
for source in [
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
] {
assert_eq!(
autonomous_manifest_seed_tasks_for_source(source),
full_seed_tasks
);
}
let mut manifest_tasks = full_seed_tasks;
for task in &mut manifest_tasks {
if matches!(
task.id.as_str(),
"art-director" | "code-prototype" | "preview-readiness" | "preview-playtest"
) {
task.status = GameCreationAppTaskStatus::Pending;
}
}
assert_eq!(
autonomous_manifest_ready_task_ids(
&manifest_tasks,
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
),
vec!["art-director".to_string()]
);
manifest_tasks
.iter_mut()
.find(|task| task.id == "art-director")
.expect("art director task exists")
.status = GameCreationAppTaskStatus::Completed;
assert_eq!(
autonomous_manifest_ready_task_ids(
&manifest_tasks,
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
),
vec!["code-prototype".to_string()]
);
}
fn autonomous_fixture_with_setup(
task: &str,
run_id: &str,
@@ -116,16 +188,13 @@ fn autonomous_fixture_with_setup(
}
fn register_autonomous_visual_fixture(root: &Path, local_path: &str, kind: &str) {
let bytes = if kind == "art-spritesheet" {
use image::ImageEncoder;
let mut bytes = Vec::new();
image::codecs::png::PngEncoder::new(&mut bytes)
.write_image(&[12, 34, 56, 0], 1, 1, image::ColorType::Rgba8.into())
.expect("encode transparent autonomous visual fixture");
bytes
} else {
b"\x89PNG\r\n\x1a\nfixture".to_vec()
};
use image::ImageEncoder;
let alpha = if kind == "art-spritesheet" { 0 } else { 255 };
let pixels = [12, 34, 56, alpha].repeat(64 * 64);
let mut bytes = Vec::new();
image::codecs::png::PngEncoder::new(&mut bytes)
.write_image(&pixels, 64, 64, image::ColorType::Rgba8.into())
.expect("encode autonomous visual fixture");
fs::write(root.join(local_path), bytes).expect("write autonomous visual fixture");
let (generation_route, generation_kind, reference_resource_ids) = match kind {
"icon-spec" => (
@@ -1008,10 +1077,19 @@ fn game_chat_parent_completion_stops_after_preview_playtest_without_publish_task
.expect("leave publish strategy pending");
update_manifest_task_status_at(&root, "publish-package", GameCreationAppTaskStatus::Pending)
.expect("leave publish package pending");
for task in new_game_creation_app_seed_tasks() {
if !matches!(
task.id.as_str(),
"art-director" | "code-prototype" | "preview-readiness" | "preview-playtest"
) {
update_manifest_task_status_at(&root, &task.id, GameCreationAppTaskStatus::Pending)
.unwrap_or_else(|error| panic!("leave non-fast-path task pending: {error}"));
}
}
let revision = advance_game_index_revision(
&root,
&state,
"<!doctype html><html><body><img src=\"assets/art-spritesheet.png\"><canvas></canvas></body></html>",
"<!doctype html><html><body><img id=\"art\" hidden src=\"../assets/art-spec.png\"><canvas></canvas><script>const image=document.getElementById('art');context.drawImage(image,0,0,canvas.width,canvas.height);context.drawImage(image,0,0,96,96);context.drawImage(image,120,80,112,112);</script></body></html>",
);
mark_verification_passed(&root, &state, "game.static_smoke");
let result = browser_result_fixture(&root, &state, revision, contract.playtest_scenario);
@@ -1035,12 +1113,50 @@ fn game_chat_parent_completion_stops_after_preview_playtest_without_publish_task
}
#[test]
fn code_prototype_requires_registered_canvas_spritesheet_reference_when_editor_is_configured() {
fn game_chat_schedule_ready_tool_cannot_bypass_the_single_round_publish_boundary() {
let (_temporary, root, state, _contract) = autonomous_fixture_with_source(
"创建一轮植物塔防游戏",
"game-chat-schedule-ready-boundary",
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
);
for task_id in ["publish-strategy", "publish-package"] {
update_manifest_task_status_at(&root, task_id, GameCreationAppTaskStatus::Pending)
.expect("leave game-chat publish task pending");
}
let observation = observe_agent_runtime_schedule_ready_tasks(
&root,
&state.agent_id,
&state.run_id,
&serde_json::json!({ "limit": 16 }),
);
assert_eq!(observation.status, "ok");
assert_eq!(observation.summary, "已调度 0 个 Ready 任务");
let manifest = read_manifest_for_project(&root).expect("read game-chat manifest");
for task_id in ["publish-strategy", "publish-package"] {
assert_eq!(
manifest
.tasks
.iter()
.find(|task| task.id == task_id)
.map(|task| &task.status),
Some(&GameCreationAppTaskStatus::Pending),
"game-chat must not schedule {task_id} after preview-playtest"
);
}
}
#[test]
fn game_chat_code_prototype_requires_registered_canvas_art_spec_visible_use() {
let _config_guard = crate::tests::write_test_local_config(
r#"{"editorApi":{"apiKey":"game-chat-art-gate-key"}}"#.to_string(),
);
let (_temporary, root, parent_state, _contract) =
autonomous_fixture("创建一轮植物塔防游戏", "game-chat-code-art-gate-parent");
let (_temporary, root, parent_state, _contract) = autonomous_fixture_with_source(
"创建一轮植物塔防游戏",
"game-chat-code-art-gate-parent",
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
);
update_manifest_task_status_at(&root, "code-prototype", GameCreationAppTaskStatus::Running)
.expect("mark code prototype running");
let code_record =
@@ -1052,7 +1168,140 @@ fn code_prototype_requires_registered_canvas_spritesheet_reference_when_editor_i
"<!doctype html><html><body><canvas></canvas></body></html>",
);
let blocker = autonomous_game_build_completion_blocker_at_locked(&root, &code_state)
.expect("missing spritesheet reference must block code prototype");
.expect("missing art-spec reference must block code prototype");
assert!(blocker
.detail
.as_deref()
.is_some_and(|detail| detail.contains("assets/art-spec.png")));
advance_game_index_revision(
&root,
&code_state,
"<!doctype html><html><body><img src=\"assets/art-spec.png\"><canvas></canvas></body></html>",
);
assert!(
autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_some(),
"URL relative to game/index.html must resolve to the registered asset"
);
advance_game_index_revision(
&root,
&code_state,
"<!doctype html><html><body><canvas></canvas><script>const unused = '../assets/art-spec.png';</script></body></html>",
);
let blocker = autonomous_game_build_completion_blocker_at_locked(&root, &code_state)
.expect("an unused art-spec string must not satisfy visible-use validation");
assert!(blocker
.detail
.as_deref()
.is_some_and(|detail| detail.contains("missing-visible-art-spec-use")));
advance_game_index_revision(
&root,
&code_state,
"<!doctype html><html><body><canvas></canvas><script>const fake = '<img src=\"../assets/art-spec.png\">';</script></body></html>",
);
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_some());
advance_game_index_revision(
&root,
&code_state,
"<!doctype html><style>.hidden-canvas{display:none}</style><img id=\"preload\" hidden src=\"../assets/art-spec.png\"><canvas class=\"hidden-canvas\"></canvas><script>const art=document.getElementById('preload');context.drawImage(art,0,0,canvas.width,canvas.height);context.drawImage(art,0,0,96,96);context.drawImage(art,120,80,112,112);</script>",
);
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_some());
advance_game_index_revision(
&root,
&code_state,
"<!doctype html><img id=\"preload\" hidden src=\"../assets/art-spec.png\"><canvas></canvas><script>const art=document.getElementById('preload');function never(){context.drawImage(art,0,0,canvas.width,canvas.height);context.drawImage(art,0,0,96,96);context.drawImage(art,120,80,112,112);}</script>",
);
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_some());
advance_game_index_revision(
&root,
&code_state,
"<!doctype html><img id=\"preload\" hidden src=\"../assets/art-spec.png\"><canvas></canvas><script>const art=document.getElementById('preload');function draw(){context.drawImage(art,0,0,canvas.width,canvas.height);context.drawImage(art,0,0,96,96);context.drawImage(art,120,80,112,112);}requestAnimationFrame(draw);</script>",
);
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_none());
advance_game_index_revision(
&root,
&code_state,
"<!doctype html><html><body><img hidden src=\"../assets/art-spec.png\"><canvas></canvas></body></html>",
);
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_some());
for hidden_html in [
"<!doctype html><img width=\"1\" height=\"1\" src=\"../assets/art-spec.png\"><canvas></canvas>",
"<!doctype html><img style=\"opacity:0.05\" src=\"../assets/art-spec.png\"><canvas></canvas>",
"<!doctype html><img style=\"position:absolute;left:-9999px\" src=\"../assets/art-spec.png\"><canvas></canvas>",
] {
advance_game_index_revision(&root, &code_state, hidden_html);
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_some());
}
advance_game_index_revision(
&root,
&code_state,
"<!doctype html><html><head><style>.hero{width:96px;height:96px;background-image:url('../assets/art-spec.png')}</style></head><body><div class=\"hero\"></div><canvas></canvas></body></html>",
);
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_some());
advance_game_index_revision(
&root,
&code_state,
"<!doctype html><html><body><canvas></canvas><script>const art=new Image(); art\n .src =\n '../assets/art-spec.png'; context.drawImage(art,0,0);</script></body></html>",
);
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_some());
advance_game_index_revision(
&root,
&code_state,
"<!doctype html><html><body><canvas></canvas><script>const art=new Image(); art.src='../assets/art-spec.png'; context.drawImage(art,0,0,1,1);</script></body></html>",
);
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_some());
advance_game_index_revision(
&root,
&code_state,
"<!doctype html><html><body><img id=\"preload\" hidden src=\"../assets/art-spec.png\"><canvas></canvas><script>const art=document.getElementById('preload'); context.drawImage(art,0,0,96,96);</script></body></html>",
);
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_some());
advance_game_index_revision(
&root,
&code_state,
"<!doctype html><html><body><img src=\"../assets/art-spec.png\"><canvas></canvas></body></html>",
);
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_some());
advance_game_index_revision(
&root,
&code_state,
"<!doctype html><html><body><img id=\"preload\" hidden src=\"../assets/art-spec.png\"><canvas></canvas><script>const art=document.getElementById('preload'); context.drawImage(art,0,0,canvas.width,canvas.height); context.drawImage(art,0,0,96,96); context.drawImage(art,120,80,112,112);</script></body></html>",
);
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_none());
}
#[test]
fn cli_code_prototype_keeps_registered_canvas_spritesheet_gate_when_editor_is_configured() {
let _config_guard = crate::tests::write_test_local_config(
r#"{"editorApi":{"apiKey":"cli-art-gate-key"}}"#.to_string(),
);
let (_temporary, root, parent_state, _contract) =
autonomous_fixture("创建完整小游戏", "cli-code-art-gate-parent");
update_manifest_task_status_at(&root, "code-prototype", GameCreationAppTaskStatus::Running)
.expect("mark CLI code prototype running");
let code_record =
queue_autonomous_manifest_child_fixture(&root, &parent_state, "code-prototype");
let code_state = agent_runtime_state_from_task_record(&code_record);
advance_game_index_revision(
&root,
&code_state,
"<!doctype html><html><body><img src=\"../assets/art-spec.png\"><canvas></canvas></body></html>",
);
let blocker = autonomous_game_build_completion_blocker_at_locked(&root, &code_state)
.expect("CLI must still require the art spritesheet");
assert!(blocker
.detail
.as_deref()
@@ -1061,11 +1310,177 @@ fn code_prototype_requires_registered_canvas_spritesheet_reference_when_editor_i
advance_game_index_revision(
&root,
&code_state,
"<!doctype html><html><body><img src=\"assets/art-spritesheet.png\"><canvas></canvas></body></html>",
"<!doctype html><html><body><img src=\"../assets/art-spritesheet.png\"><canvas></canvas></body></html>",
);
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_none());
}
#[test]
fn game_chat_requires_canvas_art_spec_even_without_editor_configuration() {
let _config_guard = crate::tests::write_test_local_config("{}".to_string());
let (_temporary, root, parent_state, _contract) = autonomous_fixture_with_source(
"创建一轮必须使用平台美术的小游戏",
"game-chat-unconfigured-art-gate-parent",
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
);
update_manifest_task_status_at(&root, "code-prototype", GameCreationAppTaskStatus::Running)
.expect("mark code prototype running");
let code_record =
queue_autonomous_manifest_child_fixture(&root, &parent_state, "code-prototype");
let code_state = agent_runtime_state_from_task_record(&code_record);
fs::remove_file(root.join("assets/art-spec.png")).expect("remove registered art-spec file");
advance_game_index_revision(
&root,
&code_state,
"<!doctype html><html><body><img src=\"../assets/art-spec.png\"><canvas></canvas></body></html>",
);
let blocker = autonomous_game_build_completion_blocker_at_locked(&root, &code_state)
.expect("game-chat must fail closed without a valid Canvas art spec");
assert!(blocker
.detail
.as_deref()
.is_some_and(|detail| detail.contains("canvas-registration-invalid")));
}
#[test]
fn game_chat_art_stage_fails_closed_without_editor_configuration_or_canvas_asset() {
let _config_guard = crate::tests::write_test_local_config("{}".to_string());
let (_temporary, root, parent_state, _contract) = autonomous_fixture_with_source(
"创建一轮必须先完成美术阶段的小游戏",
"game-chat-unconfigured-art-stage-parent",
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
);
update_manifest_task_status_at(&root, "art-director", GameCreationAppTaskStatus::Running)
.expect("mark art director running");
let art_record = queue_autonomous_manifest_child_fixture(&root, &parent_state, "art-director");
let art_state = agent_runtime_state_from_task_record(&art_record);
fs::remove_file(root.join("assets/art-spec.png")).expect("remove Canvas art spec file");
let blocker = autonomous_game_build_completion_blocker_at_locked(&root, &art_state)
.expect("game-chat art stage must fail closed without a valid Canvas asset");
assert!(blocker.summary.contains("Canvas"));
assert!(blocker
.detail
.as_deref()
.is_some_and(|detail| detail.contains("task=art-director")));
}
#[test]
fn game_chat_ready_child_can_converge_after_hydration_restores_manifest_to_pending() {
let _config_guard = crate::tests::write_test_local_config("{}".to_string());
let (_temporary, root, parent_state, _contract) = autonomous_fixture_with_source(
"创建一轮星空收集游戏",
"game-chat-ready-child-hydration-parent",
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
);
update_manifest_task_status_at(&root, "code-prototype", GameCreationAppTaskStatus::Pending)
.expect("restore code prototype to pending as late hydration can do");
let code_record =
queue_autonomous_manifest_child_fixture(&root, &parent_state, "code-prototype");
let mut code_state = agent_runtime_state_from_task_record(&code_record);
advance_game_index_revision(
&root,
&code_state,
"<!doctype html><html><body><img id=\"art\" hidden src=\"../assets/art-spec.png\"><canvas></canvas><script>const image=document.getElementById('art');context.drawImage(image,0,0,canvas.width,canvas.height);context.drawImage(image,0,0,96,96);context.drawImage(image,120,80,112,112);requestAnimationFrame(()=>{});</script></body></html>",
);
assert!(
autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_none(),
"the verified game-chat child owns the artifact and terminal projection will persist Completed"
);
mark_verification_passed(&root, &code_state, "game.static_smoke");
code_state.status = "completed".to_string();
code_state.phase = "completed".to_string();
assert!(
project_autonomous_manifest_ready_task_terminal_at(&root, &code_state)
.expect("project completed game-chat code child")
);
let manifest = read_manifest_for_project(&root).expect("read projected game-chat manifest");
assert_eq!(
manifest
.tasks
.iter()
.find(|task| task.id == "code-prototype")
.map(|task| &task.status),
Some(&GameCreationAppTaskStatus::Completed)
);
let root_gate = read_game_creator_agent_runtime_verification_gate(
&root,
&parent_state.agent_id,
&parent_state.run_id,
)
.expect("read projected root verification gate");
assert_eq!(root_gate.verified_revision, Some(1));
assert_eq!(root_gate.agent_id, parent_state.agent_id);
assert_eq!(root_gate.run_id, parent_state.run_id);
assert!(!root_gate.requires_verification);
assert_eq!(root_gate.mutation_revision, None);
assert_eq!(
root_gate.last_verification_status.as_deref(),
Some(AGENT_RUNTIME_VERIFICATION_STATUS_PASSED)
);
assert_eq!(
root_gate.last_verification_tool.as_deref(),
Some("game.static_smoke")
);
for task_id in ["preview-readiness", "preview-playtest"] {
update_manifest_task_status_at(&root, task_id, GameCreationAppTaskStatus::Completed)
.unwrap_or_else(|error| panic!("complete {task_id}: {error}"));
}
let blocker = autonomous_game_build_completion_blocker_at_locked(&root, &parent_state)
.expect("missing root playtest receipt must still block completion");
assert!(blocker.summary.contains("交互试玩回执"));
}
#[test]
fn game_chat_preview_child_still_rejects_pending_manifest_status() {
let (_temporary, root, parent_state, _contract) = autonomous_fixture_with_source(
"创建一轮星空收集游戏",
"game-chat-preview-pending-parent",
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
);
update_manifest_task_status_at(
&root,
"preview-readiness",
GameCreationAppTaskStatus::Pending,
)
.expect("leave preview readiness pending");
let preview_record =
queue_autonomous_manifest_child_fixture(&root, &parent_state, "preview-readiness");
let preview_state = agent_runtime_state_from_task_record(&preview_record);
let blocker = autonomous_game_build_completion_blocker_at_locked(&root, &preview_state)
.expect("preview child must keep the strict manifest status gate");
assert!(blocker
.detail
.as_deref()
.is_some_and(|detail| detail.contains("status=pending")));
}
#[test]
fn gui_ready_child_still_rejects_pending_manifest_status() {
let (_temporary, root, parent_state, _contract) =
autonomous_fixture("创建完整小游戏", "gui-ready-child-pending-manifest-parent");
update_manifest_task_status_at(&root, "code-prototype", GameCreationAppTaskStatus::Pending)
.expect("mark GUI code prototype pending");
let code_record =
queue_autonomous_manifest_child_fixture(&root, &parent_state, "code-prototype");
let code_state = agent_runtime_state_from_task_record(&code_record);
advance_game_index_revision(
&root,
&code_state,
"<!doctype html><html><body><canvas></canvas></body></html>",
);
let blocker = autonomous_game_build_completion_blocker_at_locked(&root, &code_state)
.expect("GUI child must keep the strict manifest status gate");
assert!(blocker
.detail
.as_deref()
.is_some_and(|detail| detail.contains("status=pending")));
}
#[test]
fn autonomous_ready_child_missing_or_invalid_owner_artifact_is_blocked() {
let (_temporary, root, parent_state, _contract) =
@@ -1,5 +1,118 @@
use super::*;
static AGENT_RUNTIME_EVENT_ID_SEQUENCE: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(1);
fn new_game_creator_agent_runtime_event_id(
state: &AgentRuntimeState,
event_type: &str,
phase: &str,
action_id: Option<&str>,
) -> String {
if let Some(action_id) = action_id {
return format!(
"runtime-event-action-{}-{}-{}-{}",
state.run_id, event_type, phase, action_id
);
}
let sequence =
AGENT_RUNTIME_EVENT_ID_SEQUENCE.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
format!(
"runtime-event-{}-{}-{}-{}",
std::process::id(),
unix_millis(),
sequence,
event_type
)
}
fn game_creator_agent_runtime_event_type_is_public(event_type: &str) -> bool {
matches!(
event_type,
"thinking_summary"
| "plan"
| "plan_update"
| "action"
| "observation"
| "turn.started"
| "turn.progress"
| "turn.completed"
| "turn.failed"
| "turn.budget_exhausted"
| "turn.cancelled"
| "response"
| "response.stale"
| "goal.paused"
| "goal.resumed"
| "agent.delegate.result"
| "agent.delegate.result_failed"
) || event_type.starts_with("tool_confirmation.")
|| event_type.starts_with("user_input.")
}
fn game_creator_agent_runtime_public_event_text(
root: &Path,
event_type: &str,
summary: &str,
) -> Option<String> {
let event_type = event_type.trim();
if !game_creator_agent_runtime_event_type_is_public(event_type) {
return None;
}
let summary = redact_agent_runtime_error(root, summary.trim(), 240);
if summary.is_empty() {
return None;
}
let lower = summary.to_ascii_lowercase();
if [
"runtime.",
"agent.runtime.",
"provider.",
"provider_request.",
"parallel_read_batch.",
"provider_action_batch.",
"finalization.",
"context.",
"process_session.",
"steer.",
"autonomous_manifest.parent_wake",
"agent.delegate.parent_wake",
"command.exec",
"command.exec:",
"command.output_read",
"command.output_read:",
"agent.action_history",
"agent.action_history:",
]
.iter()
.any(|prefix| lower.starts_with(prefix))
{
return None;
}
if [
"sha256",
"fingerprint",
"authorization",
"bearer",
"api key",
"api_key",
"password",
"secret",
"cookie",
"token=",
"private process output",
"<absolute-path",
"<redacted-url",
"[redacted",
]
.iter()
.any(|marker| lower.contains(marker))
{
return None;
}
Some(summary)
}
pub(crate) fn start_game_creator_agent_runtime_turn_at(
root: &Path,
agent_id: &str,
@@ -2037,6 +2150,8 @@ pub(super) fn try_open_game_creator_agent_runtime_task_lock_file(
const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x0200_0000;
const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
const FILE_SHARE_READ_WRITE: u32 = 0x0000_0003;
const ERROR_SHARING_VIOLATION: i32 = 32;
const ERROR_LOCK_VIOLATION: i32 = 33;
validate_project_root(root)?;
let relative_path = normalize_relative_path(relative_path)?;
let path = root.join(&relative_path);
@@ -2135,6 +2250,9 @@ pub(super) fn try_open_game_creator_agent_runtime_task_lock_file(
if matches!(
error.kind(),
std::io::ErrorKind::PermissionDenied | std::io::ErrorKind::WouldBlock
) || matches!(
error.raw_os_error(),
Some(ERROR_SHARING_VIOLATION | ERROR_LOCK_VIOLATION)
) =>
{
Ok(None)
@@ -2318,10 +2436,12 @@ pub(super) fn append_game_creator_agent_runtime_event_with_action(
run_id: state.run_id.clone(),
source: state.source.clone(),
event_type: event_type.to_string(),
event_id: new_game_creator_agent_runtime_event_id(state, event_type, phase, action_id),
action_id: action_id.map(ToString::to_string),
status: status.to_string(),
phase: phase.to_string(),
summary: summary.to_string(),
public_text: game_creator_agent_runtime_public_event_text(root, event_type, summary),
detail: detail
.filter(|_| {
!(event_type == "observation"
@@ -228,6 +228,34 @@ pub(in crate::agent) fn render_static_delegate_task_contract(
Ok(rendered)
}
fn validate_publish_delegate_run_profile_at(
root: &Path,
agent_id: &str,
parent_run_id: &str,
target_agent_id: &str,
) -> Result<(), String> {
if !matches!(target_agent_id, "publish-strategy" | "publish-package") {
return Ok(());
}
let current_binding =
read_game_creator_agent_runtime_run_profile_binding(root, agent_id, parent_run_id)?
.ok_or_else(|| {
"agent.delegate 缺少当前 Run Profile binding,已拒绝发布委派".to_string()
})?;
let root_binding = read_game_creator_agent_runtime_run_profile_binding(
root,
&current_binding.root_agent_id,
&current_binding.root_run_id,
)?
.ok_or_else(|| "agent.delegate 缺少 root Run Profile binding,已拒绝发布委派".to_string())?;
if root_binding.source.trim() == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE {
return Err(format!(
"game-chat Run Profile 禁止委派 {target_agent_id},未创建 child runtime"
));
}
Ok(())
}
pub(crate) fn observe_agent_runtime_agent_delegate(
root: &Path,
agent_id: &str,
@@ -359,6 +387,16 @@ pub(crate) fn observe_agent_runtime_agent_delegate(
detail: None,
};
}
if let Err(error) =
validate_publish_delegate_run_profile_at(root, agent_id, parent_run_id, &target_agent_id)
{
return AgentRuntimeToolObservation {
tool: "agent.delegate".to_string(),
status: "failed".to_string(),
summary: redact_agent_runtime_project_paths(root, &error, 240),
detail: None,
};
}
let action_identity = action_id
.filter(|value| !value.trim().is_empty())
.map(str::to_string)
@@ -1347,12 +1347,31 @@ pub(in crate::agent) fn record_game_creator_agent_runtime_receipt_start_warning(
pub(in crate::agent) fn observe_agent_runtime_schedule_ready_tasks(
root: &Path,
agent_id: &str,
run_id: &str,
input: &serde_json::Value,
) -> AgentRuntimeToolObservation {
let limit = agent_runtime_tool_input_usize(input, &["limit", "maxTasks"])
.map(|value| value.clamp(1, 16))
.unwrap_or(16);
match schedule_game_creator_agent_ready_tasks_at(root, limit) {
let scheduled =
match read_game_creator_agent_runtime_run_profile_binding(root, agent_id, run_id) {
Ok(Some(binding))
if binding.profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD =>
{
schedule_autonomous_game_build_ready_tasks_at(
root,
&binding.root_agent_id,
&binding.root_run_id,
limit.min(3),
)
}
Ok(_) => schedule_game_creator_agent_ready_tasks_at(root, limit),
Err(error) => Err(format!(
"agent.schedule_ready 无法核对当前 Run Profile 绑定:{error}"
)),
};
match scheduled {
Ok(results) => {
let detail = results
.iter()
@@ -203,8 +203,10 @@ pub(in crate::agent) async fn observe_agent_runtime_preview_validate(
.as_ref()
.map(|contract| contract.playtest_scenario.clone())
.or(input.playtest_scenario);
if completion_contract.is_some() {
if let Err(error) = remove_autonomous_playtest_receipt(root, agent_id, run_id) {
if let Some(contract) = completion_contract.as_ref() {
if let Err(error) =
remove_autonomous_playtest_receipt(root, &contract.agent_id, &contract.run_id)
{
return AgentRuntimeToolObservation {
tool: "preview.validate".to_string(),
status: "failed".to_string(),
@@ -246,10 +248,14 @@ pub(in crate::agent) async fn observe_agent_runtime_preview_validate(
}
},
};
let (evidence_agent_id, evidence_run_id) = completion_contract
.as_ref()
.map(|contract| (contract.agent_id.as_str(), contract.run_id.as_str()))
.unwrap_or((agent_id, run_id));
let evidence_relative_root = format!(
".agent/runtime/browser-validations/{}/{}/{}",
agent_runtime_confirmation_path_component(agent_id, "agent"),
agent_runtime_confirmation_path_component(run_id, "run"),
agent_runtime_confirmation_path_component(evidence_agent_id, "agent"),
agent_runtime_confirmation_path_component(evidence_run_id, "run"),
revision_before.revision,
);
let evidence_root = match resolve_local_project_path(root, &evidence_relative_root) {
@@ -339,7 +345,10 @@ pub(in crate::agent) async fn observe_agent_runtime_preview_validate(
}
}
if completion_contract.is_some() && !result.passed {
let contract_belongs_to_runtime = completion_contract.as_ref().is_some_and(|contract| {
contract.agent_id == runtime.agent_id && contract.run_id == runtime.run_id
});
if contract_belongs_to_runtime && !result.passed {
if let Err(error) = invalidate_agent_runtime_project_verification_after_preview_failure_at(
root,
agent_id,
@@ -385,18 +394,20 @@ pub(in crate::agent) async fn observe_agent_runtime_preview_validate(
};
}
};
if let Err(error) = clear_agent_runtime_failed_playtest_at(
root,
agent_id,
run_id,
revision_after.revision,
) {
return AgentRuntimeToolObservation {
tool: "preview.validate".to_string(),
status: "failed".to_string(),
summary: "浏览器验证已通过,但失败试玩凭证无法安全清除".to_string(),
detail: Some(redact_agent_runtime_project_paths(root, &error, 500)),
};
if contract_belongs_to_runtime {
if let Err(error) = clear_agent_runtime_failed_playtest_at(
root,
agent_id,
run_id,
revision_after.revision,
) {
return AgentRuntimeToolObservation {
tool: "preview.validate".to_string(),
status: "failed".to_string(),
summary: "浏览器验证已通过,但失败试玩凭证无法安全清除".to_string(),
detail: Some(redact_agent_runtime_project_paths(root, &error, 500)),
};
}
}
receipt
}
@@ -2,15 +2,37 @@ use super::*;
pub(in crate::agent) fn observe_agent_runtime_task_list(
root: &Path,
agent_id: &str,
run_id: &str,
) -> AgentRuntimeToolObservation {
let result = read_manifest_for_project(root).map(|manifest| {
let ready_task_ids = ready_task_ids_for_tasks(&manifest.tasks);
let seed_task_ids = new_game_creation_app_seed_tasks()
.into_iter()
.map(|task| task.id)
.collect::<std::collections::BTreeSet<_>>();
let seed_tasks = manifest
.tasks
let result = (|| -> Result<String, String> {
let game_chat_single_round = root_run_source_is_game_chat(root, agent_id, run_id)?;
let manifest = read_manifest_for_project(root)?;
let visible_tasks = if game_chat_single_round {
autonomous_manifest_seed_tasks_for_source(AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE)
.into_iter()
.map(|mut projected| {
if let Some(persisted) =
manifest.tasks.iter().find(|task| task.id == projected.id)
{
projected.status = persisted.status.clone();
}
projected
})
.collect::<Vec<_>>()
} else {
manifest.tasks.clone()
};
let ready_task_ids = ready_task_ids_for_tasks(&visible_tasks);
let seed_task_ids = autonomous_manifest_seed_tasks_for_source(if game_chat_single_round {
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
} else {
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE
})
.into_iter()
.map(|task| task.id)
.collect::<std::collections::BTreeSet<_>>();
let seed_tasks = visible_tasks
.iter()
.filter(|task| seed_task_ids.contains(&task.id))
.collect::<Vec<_>>();
@@ -19,28 +41,23 @@ pub(in crate::agent) fn observe_agent_runtime_task_list(
} else {
ready_task_ids.join(", ")
};
let completed = manifest
.tasks
let completed = visible_tasks
.iter()
.filter(|task| task.status == GameCreationAppTaskStatus::Completed)
.count();
let running = manifest
.tasks
let running = visible_tasks
.iter()
.filter(|task| task.status == GameCreationAppTaskStatus::Running)
.count();
let pending = manifest
.tasks
let pending = visible_tasks
.iter()
.filter(|task| task.status == GameCreationAppTaskStatus::Pending)
.count();
let waiting = manifest
.tasks
let waiting = visible_tasks
.iter()
.filter(|task| task.status == GameCreationAppTaskStatus::WaitingForConfirmation)
.count();
let failed = manifest
.tasks
let failed = visible_tasks
.iter()
.filter(|task| task.status == GameCreationAppTaskStatus::Failed)
.count();
@@ -72,10 +89,10 @@ pub(in crate::agent) fn observe_agent_runtime_task_list(
),
format!(
"taskCounts: completed={completed} running={running} pending={pending} waiting={waiting} failed={failed} total={}",
manifest.tasks.len()
visible_tasks.len()
),
];
lines.extend(manifest.tasks.iter().map(|task| {
lines.extend(visible_tasks.iter().map(|task| {
let dependencies = if task.dependencies.is_empty() {
"-".to_string()
} else {
@@ -97,11 +114,144 @@ pub(in crate::agent) fn observe_agent_runtime_task_list(
artifacts
)
}));
lines.join("\n")
});
Ok(lines.join("\n"))
})();
observation_from_text_result("task.list", result, "已读取 manifest 任务图")
}
fn root_run_source_is_game_chat(root: &Path, agent_id: &str, run_id: &str) -> Result<bool, String> {
let binding = read_game_creator_agent_runtime_run_profile_binding(root, agent_id, run_id)?
.ok_or_else(|| "task.list 缺少当前 Run Profile binding,无法确认运行来源".to_string())?;
let root_binding = if binding.root_agent_id == binding.agent_id
&& binding.root_run_id == binding.run_id
{
binding
} else {
read_game_creator_agent_runtime_run_profile_binding(
root,
&binding.root_agent_id,
&binding.root_run_id,
)?
.ok_or_else(|| "task.list 缺少 root Run Profile binding,无法确认运行来源".to_string())?
};
Ok(root_binding.source.trim() == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn game_chat_task_list_hides_publish_tasks_and_counts() {
let temporary = tempfile::tempdir().expect("create task list project");
let root = temporary.path();
init_local_game_project_at(root, "game-chat-task-list", "game-chat task list")
.expect("initialize project");
bind_game_creator_agent_runtime_run_profile_at(
root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"game-chat-task-list-run",
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
None,
)
.expect("bind game-chat run");
for task_id in [
"art-director",
"code-prototype",
"preview-readiness",
"preview-playtest",
] {
update_manifest_task_status_at(root, task_id, GameCreationAppTaskStatus::Completed)
.expect("complete game-chat seed task");
}
let observation = observe_agent_runtime_task_list(
root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"game-chat-task-list-run",
);
assert_eq!(observation.status, "ok");
let detail = observation.detail.expect("task list detail");
assert!(detail.contains("readyTaskIds: (none)"), "{detail}");
assert!(!detail.contains("publish-strategy"), "{detail}");
assert!(!detail.contains("publish-package"), "{detail}");
assert!(
detail.contains(
"seedTaskCounts: completed=4 running=0 pending=0 waiting=0 failed=0 total=4"
),
"{detail}"
);
assert!(
detail
.contains("taskCounts: completed=4 running=0 pending=0 waiting=0 failed=0 total=4"),
"{detail}"
);
for task in new_game_creation_app_seed_tasks().into_iter().take(14) {
update_manifest_task_status_at(root, &task.id, GameCreationAppTaskStatus::Completed)
.expect("complete full pre-publish DAG for GUI comparison");
}
bind_game_creator_agent_runtime_run_profile_at(
root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"gui-task-list-run",
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
None,
)
.expect("bind GUI run");
let gui_observation = observe_agent_runtime_task_list(
root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"gui-task-list-run",
);
assert_eq!(gui_observation.status, "ok");
let gui_detail = gui_observation.detail.expect("GUI task list detail");
assert!(
gui_detail.contains("readyTaskIds: publish-strategy"),
"{gui_detail}"
);
assert!(
gui_detail.contains(
"taskCounts: completed=14 running=0 pending=2 waiting=0 failed=0 total=16"
),
"{gui_detail}"
);
assert!(gui_detail.contains("publish-strategy"), "{gui_detail}");
}
#[test]
fn task_list_fails_closed_when_current_binding_parent_is_missing() {
let temporary = tempfile::tempdir().expect("create task list project");
let root = temporary.path();
init_local_game_project_at(root, "game-chat-task-list-missing-parent", "task list")
.expect("initialize project");
let parent_run_id = "missing-game-chat-parent";
let task_link = AgentRuntimeTaskLink {
parent_agent_id: Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string()),
parent_run_id: Some(parent_run_id.to_string()),
..Default::default()
};
bind_game_creator_agent_runtime_run_profile_at(
root,
"code-prototype",
"game-chat-child-run",
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
Some(AGENT_RUNTIME_RUN_PROFILE_STANDARD),
Some(&task_link),
)
.expect("bind child run");
let observation =
observe_agent_runtime_task_list(root, "code-prototype", "game-chat-child-run");
assert_eq!(observation.status, "failed");
assert!(observation.detail.is_none());
assert!(observation.summary.contains("binding"), "{observation:?}");
}
}
pub(in crate::agent) fn observe_agent_runtime_task_create(
root: &Path,
agent_id: &str,
@@ -879,8 +879,11 @@ pub(crate) fn resume_game_creator_agent_runtime_tasks(
) -> Result<Vec<AgentRuntimeResult>, String> {
let root = Path::new(project_path.trim());
enforce_project_permission_policy(root, "conversation.read")?;
enforce_project_permission_policy(root, "conversation.write")?;
enforce_project_permission_policy(root, "agent.run_status")?;
if !has_recoverable_game_creator_agent_background_tasks_at(root)? {
return Ok(Vec::new());
}
enforce_project_permission_policy(root, "conversation.write")?;
enforce_project_auto_permission_policy(root, "agent.resume")?;
resume_game_creator_agent_background_tasks_at(root)
}
@@ -1344,16 +1347,26 @@ pub(crate) fn append_local_conversation_message(
agent_id: Option<String>,
session_id: Option<String>,
message: LocalConversationMessage,
message_id: Option<String>,
) -> Result<LocalConversationResult, String> {
let root = Path::new(project_path.trim());
enforce_project_permission_policy(root, "conversation.write")?;
let _lock = acquire_project_write_lock(root, "conversation.write")?;
append_local_conversation_message_for_session_at(
root,
agent_id.as_deref(),
session_id.as_deref(),
message,
)
match message_id.as_deref() {
Some(message_id) => append_local_conversation_message_for_session_idempotent_at(
root,
agent_id.as_deref(),
session_id.as_deref(),
message,
message_id,
),
None => append_local_conversation_message_for_session_at(
root,
agent_id.as_deref(),
session_id.as_deref(),
message,
),
}
}
#[tauri::command]
@@ -675,6 +675,24 @@ pub(crate) fn initialize_windows_game_creator_file_owner_for_current_user(
secure_windows_game_creator_path_for_current_user_with_owner_policy(path, false, true, true)
}
#[cfg(windows)]
pub(crate) fn windows_private_dacl_security_information(
initialize_owner: bool,
owner_matches: bool,
) -> u32 {
const OWNER_SECURITY_INFORMATION: u32 = 0x0000_0001;
const DACL_SECURITY_INFORMATION: u32 = 0x0000_0004;
const PROTECTED_DACL_SECURITY_INFORMATION: u32 = 0x8000_0000;
DACL_SECURITY_INFORMATION
| PROTECTED_DACL_SECURITY_INFORMATION
| if initialize_owner && !owner_matches {
OWNER_SECURITY_INFORMATION
} else {
0
}
}
#[cfg(windows)]
fn secure_windows_game_creator_path_for_current_user_with_owner_policy(
path: &Path,
@@ -795,7 +813,6 @@ fn secure_windows_game_creator_path_for_current_user_with_owner_policy(
const SE_FILE_OBJECT: u32 = 1;
const OWNER_SECURITY_INFORMATION: u32 = 0x0000_0001;
const DACL_SECURITY_INFORMATION: u32 = 0x0000_0004;
const PROTECTED_DACL_SECURITY_INFORMATION: u32 = 0x8000_0000;
const SE_DACL_PROTECTED: u16 = 0x1000;
const TOKEN_QUERY: u32 = 0x0000_0008;
const TOKEN_USER_CLASS: u32 = 1;
@@ -925,18 +942,13 @@ fn secure_windows_game_creator_path_for_current_user_with_owner_policy(
));
}
// SAFETY: path is NUL terminated and private_dacl was allocated by SetEntriesInAclW.
let should_initialize_owner = initialize_owner && !owner_matches;
let set_status = unsafe {
SetNamedSecurityInfoW(
wide_path.as_mut_ptr(),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION
| PROTECTED_DACL_SECURITY_INFORMATION
| if initialize_owner {
OWNER_SECURITY_INFORMATION
} else {
0
},
if initialize_owner {
windows_private_dacl_security_information(initialize_owner, owner_matches),
if should_initialize_owner {
current_user_sid
} else {
std::ptr::null_mut()
@@ -468,6 +468,8 @@ struct AgentRuntimeEvent {
#[serde(default)]
event_type: String,
#[serde(default)]
event_id: String,
#[serde(default)]
action_id: Option<String>,
#[serde(default)]
status: String,
@@ -476,6 +478,8 @@ struct AgentRuntimeEvent {
#[serde(default)]
summary: String,
#[serde(default)]
public_text: Option<String>,
#[serde(default)]
detail: Option<String>,
#[serde(default)]
updated_at: u64,
@@ -84,6 +84,32 @@ impl PreviewRegistry {
static GAME_CREATOR_PREVIEW_REGISTRY: OnceLock<PreviewRegistry> = OnceLock::new();
const PREVIEW_REQUEST_READ_TIMEOUT: Duration = Duration::from_secs(2);
const PREVIEW_REQUEST_MAX_HEADER_BYTES: usize = 32 * 1024;
const PREVIEW_REQUEST_MAX_HEADER_LINES: usize = 100;
const PREVIEW_RESPONSE_DRAIN_TIMEOUT: Duration = Duration::from_millis(250);
const PREVIEW_RESPONSE_DRAIN_MAX_BYTES: usize = 32 * 1024;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum PreviewListenerAcceptDisposition {
Sleep,
Retry,
Stop,
}
pub(crate) fn classify_preview_listener_accept_error(
error: &std::io::Error,
) -> PreviewListenerAcceptDisposition {
match error.kind() {
std::io::ErrorKind::WouldBlock => PreviewListenerAcceptDisposition::Sleep,
std::io::ErrorKind::ConnectionAborted
| std::io::ErrorKind::ConnectionReset
| std::io::ErrorKind::Interrupted
| std::io::ErrorKind::TimedOut => PreviewListenerAcceptDisposition::Retry,
_ => PreviewListenerAcceptDisposition::Stop,
}
}
pub(crate) fn game_creator_preview_registry() -> PreviewRegistry {
GAME_CREATOR_PREVIEW_REGISTRY
.get_or_init(PreviewRegistry::default)
@@ -392,10 +418,18 @@ pub(crate) fn start_local_game_preview_for_project(
}
match listener.accept() {
Ok((stream, _)) => handle_preview_stream(stream, &served_root),
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
thread::sleep(Duration::from_millis(25));
}
Err(_) => break,
// Chromium can abandon a speculative loopback socket before accept() consumes
// it. Keep the listener alive for that connection; only an unrecoverable listener
// error should tear down the preview server.
Err(error) => match classify_preview_listener_accept_error(&error) {
PreviewListenerAcceptDisposition::Sleep => {
thread::sleep(Duration::from_millis(25));
}
PreviewListenerAcceptDisposition::Retry => {
thread::sleep(Duration::from_millis(5));
}
PreviewListenerAcceptDisposition::Stop => break,
},
}
});
@@ -410,19 +444,109 @@ pub(crate) fn start_local_game_preview_for_project(
}
fn handle_preview_stream(mut stream: TcpStream, root: &Path) {
let mut request_line = String::new();
{
let mut reader = BufReader::new(&mut stream);
if reader.read_line(&mut request_line).is_err() {
return;
}
// The listener is nonblocking so its accept loop can observe the stop channel. Windows may
// inherit that mode on accepted sockets; switch each connection back to blocking mode before
// waiting for Chromium's split request headers.
if stream.set_nonblocking(false).is_err() {
return;
}
let request_line = match read_preview_request_line(&mut stream) {
Ok(Some(request_line)) => request_line,
Ok(None) | Err(_) => return,
};
let mut parts = request_line.split_whitespace();
let method = parts.next().unwrap_or_default();
let url_path = parts.next().unwrap_or("/");
let response = build_preview_response(root, method, url_path);
let _ = stream.write_all(&response);
if stream.write_all(&response).is_ok() {
let _ = stream.flush();
// Explicitly half-close after the complete response, then consume the peer's remaining
// request bytes for a short bounded interval. This lets Windows complete a graceful
// FIN/ACK exchange instead of surfacing the close as WSAECONNABORTED to Chromium.
let _ = stream.shutdown(std::net::Shutdown::Write);
drain_preview_request_after_response(&mut stream);
}
}
fn drain_preview_request_after_response(stream: &mut TcpStream) {
let _ = stream.set_read_timeout(Some(PREVIEW_RESPONSE_DRAIN_TIMEOUT));
let mut buffer = [0u8; 4096];
let mut drained_bytes = 0usize;
while drained_bytes < PREVIEW_RESPONSE_DRAIN_MAX_BYTES {
match stream.read(&mut buffer) {
Ok(0) => break,
Ok(bytes_read) => {
drained_bytes = drained_bytes.saturating_add(bytes_read);
}
Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue,
Err(error)
if matches!(
error.kind(),
std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
) =>
{
break;
}
Err(_) => break,
}
}
}
/// Read the request line and all headers before closing the connection.
///
/// Chromium can deliver the request line and headers in separate packets. Dropping the
/// stream after only `read_line` leaves unread request bytes on Windows and may make the
/// close look like an abortive RST (`net::ERR_SOCKET_NOT_CONNECTED`). The bounded read keeps
/// slow or malformed clients from occupying a preview thread indefinitely.
fn read_preview_request_line(stream: &mut TcpStream) -> std::io::Result<Option<String>> {
stream.set_read_timeout(Some(PREVIEW_REQUEST_READ_TIMEOUT))?;
let mut reader = BufReader::new(stream);
let mut request_line = Vec::new();
let mut total_bytes = 0usize;
for line_index in 0..PREVIEW_REQUEST_MAX_HEADER_LINES {
let mut line = Vec::new();
loop {
let available = reader.fill_buf()?;
if available.is_empty() {
return Ok(None);
}
let newline_index = available.iter().position(|byte| *byte == b'\n');
let bytes_to_consume = newline_index
.map(|index| index + 1)
.unwrap_or(available.len());
if total_bytes.saturating_add(bytes_to_consume) > PREVIEW_REQUEST_MAX_HEADER_BYTES {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"preview request headers exceed the size limit",
));
}
line.extend_from_slice(&available[..bytes_to_consume]);
total_bytes += bytes_to_consume;
reader.consume(bytes_to_consume);
if newline_index.is_some() {
break;
}
}
let is_blank_line = line == b"\r\n" || line == b"\n";
if line_index == 0 {
request_line = line;
}
if is_blank_line {
return String::from_utf8(request_line).map(Some).map_err(|_| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
"preview request line is not valid UTF-8",
)
});
}
}
Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"preview request headers exceed the line limit",
))
}
pub(crate) fn build_preview_response(root: &Path, method: &str, url_path: &str) -> Vec<u8> {
@@ -258,11 +258,12 @@ pub(crate) fn validate_manifest_required_visual_asset(
.and_then(|path| fs::read(path).ok())
.filter(|bytes| bytes.starts_with(b"\x89PNG\r\n\x1a\n"))
.ok_or_else(|| format!("规范视觉资产不是有效登记的 PNG 文件:{expected_path}"))?;
if task_id == "art-asset-plan"
&& !image::load_from_memory(&bytes)
.ok()
.is_some_and(|image| image.to_rgba8().pixels().any(|pixel| pixel[3] < u8::MAX))
{
let decoded = image::load_from_memory(&bytes)
.map_err(|_| format!("规范视觉资产 PNG 无法完整解码:{expected_path}"))?;
if decoded.width() == 0 || decoded.height() == 0 {
return Err(format!("规范视觉资产 PNG 尺寸无效:{expected_path}"));
}
if task_id == "art-asset-plan" && !decoded.to_rgba8().pixels().any(|pixel| pixel[3] < u8::MAX) {
return Err("首版美术素材图没有真实透明像素".to_string());
}
let canvas_project_id = asset

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