5e627a4677
新增仓库上下文指纹复核与 durable action 漂移保护 拆分 App CLI 与独立 Runner 的投递执行边界并补齐恢复诊断 加入桌面和移动端浏览器验证及跨源网络阻断 实现隔离子 Agent 私有记忆与终止状态传播 补齐 Provider 协议确认副作用重放与 join 真实端到端验证 同步共享契约实施计划决策记录与开发流程文档
1051 lines
38 KiB
Rust
1051 lines
38 KiB
Rust
use super::*;
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn init_local_game_project(
|
|
project_path: String,
|
|
project_id: String,
|
|
name: String,
|
|
) -> Result<InitLocalProjectResult, String> {
|
|
let root = Path::new(project_path.trim());
|
|
enforce_project_permission_policy(root, "project.create")?;
|
|
let _lock = acquire_project_write_lock(root, "project.create")?;
|
|
init_local_game_project_at(root, project_id.trim(), name.trim())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn is_local_project_directory_non_empty(project_path: String) -> Result<bool, String> {
|
|
let root = Path::new(project_path.trim());
|
|
if root.as_os_str().is_empty() {
|
|
return Err("项目目录不能为空".to_string());
|
|
}
|
|
if !root.is_absolute() {
|
|
return Err("项目目录必须是绝对路径".to_string());
|
|
}
|
|
if project_path_has_control_chars(root) {
|
|
return Err("项目目录不能包含控制字符".to_string());
|
|
}
|
|
if !root.exists() {
|
|
return Ok(false);
|
|
}
|
|
if !root.is_dir() {
|
|
return Err("项目目录已存在但不是文件夹".to_string());
|
|
}
|
|
fs::read_dir(root)
|
|
.map_err(|error| format!("读取项目目录失败:{}: {error}", root.display()))?
|
|
.next()
|
|
.transpose()
|
|
.map_err(|error| format!("读取项目目录失败:{}: {error}", root.display()))
|
|
.map(|entry| entry.is_some())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn inspect_local_project_directory(
|
|
project_path: String,
|
|
) -> Result<LocalProjectDirectoryStatus, String> {
|
|
let root = Path::new(project_path.trim());
|
|
if root.as_os_str().is_empty() {
|
|
return Err("项目目录不能为空".to_string());
|
|
}
|
|
if !root.is_absolute() {
|
|
return Err("项目目录必须是绝对路径".to_string());
|
|
}
|
|
if project_path_has_control_chars(root) {
|
|
return Err("项目目录不能包含控制字符".to_string());
|
|
}
|
|
let recent_run_trace = recent_game_creator_run_trace(root);
|
|
Ok(LocalProjectDirectoryStatus {
|
|
project_path: root.to_string_lossy().into_owned(),
|
|
exists: root.exists(),
|
|
is_directory: root.is_dir(),
|
|
is_game_creator_project: is_game_creator_project_directory(root),
|
|
project_name: game_creator_project_name(root),
|
|
manifest_error: game_creator_project_manifest_error(root),
|
|
recent_run_status: recent_run_trace.as_ref().map(|trace| trace.status.clone()),
|
|
recent_run_stop_reason: recent_run_trace.map(|trace| trace.stop_reason),
|
|
})
|
|
}
|
|
|
|
pub(crate) fn is_game_creator_project_directory(root: &Path) -> bool {
|
|
if !root.is_dir() {
|
|
return false;
|
|
}
|
|
let manifest_path = root.join(".agent/manifest.json");
|
|
read_manifest(&manifest_path).is_ok()
|
|
}
|
|
|
|
pub(crate) fn game_creator_project_name(root: &Path) -> Option<String> {
|
|
let manifest_path = root.join(".agent/manifest.json");
|
|
let manifest = read_manifest(&manifest_path).ok()?;
|
|
let name = manifest.name.trim();
|
|
if name.is_empty() {
|
|
None
|
|
} else {
|
|
Some(name.to_string())
|
|
}
|
|
}
|
|
|
|
pub(crate) fn game_creator_project_manifest_error(root: &Path) -> Option<String> {
|
|
if !root.is_dir() {
|
|
return None;
|
|
}
|
|
let manifest_path = root.join(".agent/manifest.json");
|
|
if !manifest_storage_exists(&manifest_path).unwrap_or(true) {
|
|
return None;
|
|
}
|
|
read_manifest(&manifest_path).err()
|
|
}
|
|
|
|
pub(crate) fn recent_game_creator_run_trace(root: &Path) -> Option<GameCreationAgentRunTrace> {
|
|
let trace_path = root.join(".agent/run.latest.json");
|
|
let content = fs::read_to_string(trace_path).ok()?;
|
|
serde_json::from_str::<GameCreationAgentRunTrace>(&content).ok()
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn pick_local_project_directory(
|
|
app: tauri::AppHandle,
|
|
) -> Result<Option<String>, String> {
|
|
let Some(path) = app.dialog().file().blocking_pick_folder() else {
|
|
return Ok(None);
|
|
};
|
|
path.into_path()
|
|
.map(|path| Some(path.to_string_lossy().into_owned()))
|
|
.map_err(|error| format!("读取项目目录失败:{error}"))
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn pick_local_file(app: tauri::AppHandle) -> Result<Option<String>, String> {
|
|
let Some(path) = app.dialog().file().blocking_pick_file() else {
|
|
return Ok(None);
|
|
};
|
|
path.into_path()
|
|
.map(|path| Some(path.to_string_lossy().into_owned()))
|
|
.map_err(|error| format!("读取本地文件失败:{error}"))
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn open_local_project_directory(
|
|
app: tauri::AppHandle,
|
|
project_path: String,
|
|
) -> Result<(), String> {
|
|
let path = validated_local_project_directory_path(project_path.trim())?;
|
|
app.opener()
|
|
.open_path(path.to_string_lossy().into_owned(), None::<&str>)
|
|
.map_err(|error| format!("打开项目目录失败:{error}"))
|
|
}
|
|
|
|
pub(crate) fn validated_local_project_directory_path(
|
|
project_path: &str,
|
|
) -> Result<PathBuf, String> {
|
|
let path = Path::new(project_path);
|
|
if path.as_os_str().is_empty() {
|
|
return Err("项目目录不能为空".to_string());
|
|
}
|
|
if !path.is_absolute() {
|
|
return Err("项目目录必须是绝对路径".to_string());
|
|
}
|
|
if !path.exists() {
|
|
return Err("项目目录不存在".to_string());
|
|
}
|
|
if !path.is_dir() {
|
|
return Err("项目路径不是文件夹".to_string());
|
|
}
|
|
Ok(path.to_path_buf())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn get_local_game_manifest(
|
|
project_path: String,
|
|
command_id: Option<String>,
|
|
) -> Result<GameCreationAppManifest, String> {
|
|
let root = Path::new(project_path.trim());
|
|
let command_id = command_id
|
|
.as_deref()
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.unwrap_or("project.status");
|
|
if !matches!(
|
|
command_id,
|
|
"project.status" | "asset.list" | "task.list" | "agent.audit"
|
|
) {
|
|
return Err(format!("不支持通过 manifest 执行命令:{command_id}"));
|
|
}
|
|
enforce_project_permission_policy(root, command_id)?;
|
|
read_manifest_for_project(root)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) async fn control_agent_run(
|
|
app: tauri::AppHandle,
|
|
project_path: String,
|
|
action: String,
|
|
detail: Option<String>,
|
|
) -> Result<AgentRunControlResult, String> {
|
|
let root = Path::new(project_path.trim());
|
|
let command_id = match action.trim() {
|
|
"status" => "agent.run_status",
|
|
"kill" => "agent.kill",
|
|
"retry" => "agent.retry",
|
|
"resume" => "agent.resume",
|
|
_ => "agent.run_status",
|
|
};
|
|
enforce_project_permission_policy(root, command_id)?;
|
|
if matches!(action.trim(), "retry" | "resume") {
|
|
enforce_project_permission_policy(root, "game.generate_draft")?;
|
|
}
|
|
let _lock = if command_id == "agent.run_status" {
|
|
None
|
|
} else {
|
|
Some(acquire_project_write_lock(root, command_id)?)
|
|
};
|
|
control_agent_run_at(
|
|
root,
|
|
action.trim(),
|
|
detail
|
|
.as_deref()
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty()),
|
|
Some(&AgentProgressEmitter::new(&app, project_path.trim())),
|
|
)
|
|
.await
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) async fn generate_local_game_draft(
|
|
app: tauri::AppHandle,
|
|
project_path: String,
|
|
prompt: String,
|
|
) -> Result<GenerateLocalGameDraftResult, String> {
|
|
let root = Path::new(project_path.trim());
|
|
enforce_project_permission_policy(root, "game.generate_draft")?;
|
|
let _lock = acquire_project_write_lock(root, "game.generate_draft")?;
|
|
advance_agent_runtime_project_revision_locked(root)?;
|
|
generate_local_game_draft_at(
|
|
root,
|
|
prompt.trim(),
|
|
Some(&AgentProgressEmitter::new(&app, project_path.trim())),
|
|
)
|
|
.await
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) async fn chat_with_game_creator_agent(
|
|
project_path: String,
|
|
prompt: String,
|
|
) -> Result<GameCreatorChatAgentReply, String> {
|
|
let root = Path::new(project_path.trim());
|
|
enforce_project_permission_policy(root, "conversation.read")?;
|
|
chat_with_game_creator_agent_at(root, prompt.trim()).await
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) async fn chat_with_game_creator_role_agent(
|
|
project_path: String,
|
|
agent_id: String,
|
|
session_id: Option<String>,
|
|
prompt: String,
|
|
) -> Result<GameCreatorChatAgentReply, String> {
|
|
let root = Path::new(project_path.trim());
|
|
let agent_id = normalize_game_creator_runtime_agent_id(agent_id.trim())?;
|
|
enforce_project_permission_policy(root, "conversation.read")?;
|
|
enforce_project_permission_policy(root, "conversation.write")?;
|
|
let runtime_lock = try_acquire_game_creator_agent_runtime_task_lock(root, &agent_id)?
|
|
.ok_or_else(|| format!("Agent 正在执行其他前台或后台任务:{agent_id}"))?;
|
|
let result = chat_with_game_creator_role_agent_runtime_for_session_at(
|
|
root,
|
|
&agent_id,
|
|
session_id.as_deref(),
|
|
prompt.trim(),
|
|
"",
|
|
)
|
|
.await
|
|
.map(|(reply, _runtime)| reply);
|
|
spawn_next_game_creator_agent_background_task_drain_with_lock(root, &agent_id, runtime_lock);
|
|
match result {
|
|
Ok(reply) => Ok(reply),
|
|
Err(error) => Err(error),
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) async fn chat_with_game_creator_role_agent_stream(
|
|
app: tauri::AppHandle,
|
|
project_path: String,
|
|
agent_id: String,
|
|
session_id: Option<String>,
|
|
prompt: String,
|
|
run_id: String,
|
|
) -> Result<GameCreatorChatAgentReply, String> {
|
|
let project_path = project_path.trim().to_string();
|
|
let agent_id = normalize_game_creator_runtime_agent_id(agent_id.trim())?;
|
|
let run_id = run_id.trim().to_string();
|
|
let root = Path::new(project_path.as_str());
|
|
let session_id =
|
|
resolve_agent_conversation_session_id_at(root, &agent_id, session_id.as_deref(), true)?;
|
|
enforce_project_permission_policy(root, "conversation.read")?;
|
|
enforce_project_permission_policy(root, "conversation.write")?;
|
|
let runtime_lock = try_acquire_game_creator_agent_runtime_task_lock(root, &agent_id)?
|
|
.ok_or_else(|| format!("Agent 正在执行其他前台或后台任务:{agent_id}"))?;
|
|
let emit_app = app.clone();
|
|
let event_project_path = project_path.clone();
|
|
let event_agent_id = agent_id.clone();
|
|
let event_run_id = run_id.clone();
|
|
let command_result = async {
|
|
let mut runtime_state = start_game_creator_agent_runtime_turn_for_session_at(
|
|
root,
|
|
agent_id.as_str(),
|
|
Some(&session_id),
|
|
prompt.trim(),
|
|
&run_id,
|
|
)?;
|
|
runtime_state = advance_game_creator_agent_runtime_turn_at(
|
|
root,
|
|
runtime_state,
|
|
"llm",
|
|
"请求 Agent LLM",
|
|
"已读取项目上下文,正在让 Agent 独立推理。",
|
|
)?;
|
|
let mut streaming_runtime_state = runtime_state.clone();
|
|
streaming_runtime_state.current_action = "正在接收 Agent 回复".to_string();
|
|
let _ = app.emit(
|
|
"game-creator-role-agent-chat-stream",
|
|
GameCreatorRoleAgentChatStreamEvent {
|
|
project_path: event_project_path.clone(),
|
|
agent_id: event_agent_id.clone(),
|
|
run_id: event_run_id.clone(),
|
|
status: "started".to_string(),
|
|
delta_text: String::new(),
|
|
accumulated_text: String::new(),
|
|
finish_reason: None,
|
|
session_id: Some(runtime_state.session_id.clone()),
|
|
runtime_status: Some(runtime_state.status.clone()),
|
|
runtime_phase: Some(runtime_state.phase.clone()),
|
|
runtime_summary: Some(runtime_state.current_action.clone()),
|
|
runtime_state: Some(runtime_state.clone()),
|
|
},
|
|
);
|
|
let result = chat_with_game_creator_role_agent_stream_for_session_at(
|
|
root,
|
|
agent_id.as_str(),
|
|
Some(&session_id),
|
|
prompt.trim(),
|
|
|delta| {
|
|
let _ = emit_app.emit(
|
|
"game-creator-role-agent-chat-stream",
|
|
GameCreatorRoleAgentChatStreamEvent {
|
|
project_path: event_project_path.clone(),
|
|
agent_id: event_agent_id.clone(),
|
|
run_id: event_run_id.clone(),
|
|
status: "delta".to_string(),
|
|
delta_text: delta.delta_text.clone(),
|
|
accumulated_text: delta.accumulated_text.clone(),
|
|
finish_reason: delta.finish_reason.clone(),
|
|
session_id: Some(streaming_runtime_state.session_id.clone()),
|
|
runtime_status: Some("running".to_string()),
|
|
runtime_phase: Some("llm".to_string()),
|
|
runtime_summary: Some("正在接收 Agent 回复".to_string()),
|
|
runtime_state: Some(streaming_runtime_state.clone()),
|
|
},
|
|
);
|
|
},
|
|
)
|
|
.await;
|
|
match result {
|
|
Ok(reply) => {
|
|
let completed_runtime = finish_game_creator_agent_runtime_turn_at(
|
|
root,
|
|
runtime_state,
|
|
&reply.reply_text,
|
|
)?;
|
|
let _ = app.emit(
|
|
"game-creator-role-agent-chat-stream",
|
|
GameCreatorRoleAgentChatStreamEvent {
|
|
project_path: project_path.clone(),
|
|
agent_id: agent_id.clone(),
|
|
run_id: run_id.clone(),
|
|
status: "completed".to_string(),
|
|
delta_text: String::new(),
|
|
accumulated_text: reply.reply_text.clone(),
|
|
finish_reason: None,
|
|
session_id: Some(completed_runtime.session_id.clone()),
|
|
runtime_status: Some(completed_runtime.status.clone()),
|
|
runtime_phase: Some(completed_runtime.phase.clone()),
|
|
runtime_summary: Some(completed_runtime.current_action.clone()),
|
|
runtime_state: Some(completed_runtime),
|
|
},
|
|
);
|
|
Ok(reply)
|
|
}
|
|
Err(error) => {
|
|
let failed_runtime =
|
|
fail_game_creator_agent_runtime_turn_at(root, runtime_state, &error).ok();
|
|
let _ = app.emit(
|
|
"game-creator-role-agent-chat-stream",
|
|
GameCreatorRoleAgentChatStreamEvent {
|
|
project_path: project_path.clone(),
|
|
agent_id: agent_id.clone(),
|
|
run_id: run_id.clone(),
|
|
status: "failed".to_string(),
|
|
delta_text: String::new(),
|
|
accumulated_text: String::new(),
|
|
finish_reason: None,
|
|
session_id: failed_runtime
|
|
.as_ref()
|
|
.map(|runtime| runtime.session_id.clone()),
|
|
runtime_status: failed_runtime
|
|
.as_ref()
|
|
.map(|runtime| runtime.status.clone()),
|
|
runtime_phase: failed_runtime.as_ref().map(|runtime| runtime.phase.clone()),
|
|
runtime_summary: failed_runtime
|
|
.as_ref()
|
|
.map(|runtime| runtime.current_action.clone()),
|
|
runtime_state: failed_runtime,
|
|
},
|
|
);
|
|
Err(error)
|
|
}
|
|
}
|
|
}
|
|
.await;
|
|
spawn_next_game_creator_agent_background_task_drain_with_lock(root, &agent_id, runtime_lock);
|
|
match command_result {
|
|
Ok(reply) => Ok(reply),
|
|
Err(error) => Err(error),
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn start_game_creator_agent_runtime_task(
|
|
project_path: String,
|
|
agent_id: String,
|
|
session_id: Option<String>,
|
|
task: String,
|
|
run_id: String,
|
|
) -> Result<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")?;
|
|
start_game_creator_agent_background_task_for_session_at(
|
|
root,
|
|
agent_id.trim(),
|
|
session_id.as_deref(),
|
|
task.trim(),
|
|
run_id.trim(),
|
|
)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn cancel_game_creator_agent_runtime_task(
|
|
project_path: String,
|
|
agent_id: String,
|
|
run_id: String,
|
|
) -> Result<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")?;
|
|
cancel_game_creator_agent_runtime_task_at(root, agent_id.trim(), run_id.trim())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn retry_game_creator_agent_runtime_task(
|
|
project_path: String,
|
|
agent_id: String,
|
|
run_id: String,
|
|
next_run_id: String,
|
|
) -> Result<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")?;
|
|
enforce_project_auto_permission_policy(root, "agent.resume")?;
|
|
retry_game_creator_agent_runtime_task_at(
|
|
root,
|
|
agent_id.trim(),
|
|
run_id.trim(),
|
|
next_run_id.trim(),
|
|
)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn confirm_game_creator_agent_runtime_task(
|
|
project_path: String,
|
|
agent_id: String,
|
|
run_id: String,
|
|
action_id: String,
|
|
note: String,
|
|
) -> Result<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")?;
|
|
// 该命令只由开发者显式点击“确认继续”触发。
|
|
enforce_project_permission_policy(root, "agent.resume")?;
|
|
confirm_game_creator_agent_runtime_task_at(
|
|
root,
|
|
agent_id.trim(),
|
|
run_id.trim(),
|
|
action_id.trim(),
|
|
note.trim(),
|
|
)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn reject_game_creator_agent_runtime_task(
|
|
project_path: String,
|
|
agent_id: String,
|
|
run_id: String,
|
|
action_id: String,
|
|
note: String,
|
|
) -> Result<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")?;
|
|
// 该命令只由开发者显式点击“拒绝并继续”触发。
|
|
enforce_project_permission_policy(root, "agent.resume")?;
|
|
reject_game_creator_agent_runtime_task_at(
|
|
root,
|
|
agent_id.trim(),
|
|
run_id.trim(),
|
|
action_id.trim(),
|
|
note.trim(),
|
|
)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn read_game_creator_agent_runtime(
|
|
project_path: String,
|
|
agent_id: String,
|
|
session_id: Option<String>,
|
|
) -> Result<AgentRuntimeResult, String> {
|
|
let root = Path::new(project_path.trim());
|
|
enforce_project_permission_policy(root, "conversation.read")?;
|
|
read_game_creator_agent_runtime_for_session_at(root, agent_id.trim(), session_id.as_deref())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn read_game_creator_agent_runtimes(
|
|
project_path: String,
|
|
) -> Result<Vec<AgentRuntimeResult>, String> {
|
|
let root = Path::new(project_path.trim());
|
|
enforce_project_permission_policy(root, "conversation.read")?;
|
|
read_game_creator_agent_runtimes_at(root)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn resume_game_creator_agent_runtime_tasks(
|
|
project_path: String,
|
|
) -> 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")?;
|
|
enforce_project_auto_permission_policy(root, "agent.resume")?;
|
|
resume_game_creator_agent_background_tasks_at(root)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn confirm_resume_game_creator_agent_runtime_tasks(
|
|
project_path: String,
|
|
) -> 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")?;
|
|
// 该命令只由开发者在 agent.resume 确认卡中明确批准后调用。
|
|
enforce_project_permission_policy(root, "agent.resume")?;
|
|
resume_game_creator_agent_background_tasks_at(root)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn schedule_game_creator_agent_ready_tasks(
|
|
project_path: String,
|
|
limit: usize,
|
|
) -> 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")?;
|
|
enforce_project_auto_permission_policy(root, "agent.schedule_ready")?;
|
|
schedule_game_creator_agent_ready_tasks_at(root, limit)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn check_game_creator_llm_config() -> GameCreatorLlmConfigStatus {
|
|
check_game_creator_llm_config_from_config()
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn read_game_creator_app_config() -> Result<GameCreatorAppConfigView, String> {
|
|
game_creator_app_config_view(load_game_creator_app_config()?)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn write_game_creator_app_config(
|
|
config: GameCreatorAppConfig,
|
|
) -> Result<GameCreatorAppConfigView, String> {
|
|
let config = normalize_game_creator_app_config(config)?;
|
|
let path = writable_game_creator_config_path()?;
|
|
let content = serde_json::to_string_pretty(&config)
|
|
.map_err(|error| format!("序列化客户端配置失败:{error}"))?;
|
|
write_game_creator_config_atomically(&path, &format!("{content}\n"))?;
|
|
game_creator_app_config_view(load_game_creator_app_config()?)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn upload_local_asset(
|
|
project_path: String,
|
|
file_name: String,
|
|
media_type: String,
|
|
bytes: Vec<u8>,
|
|
) -> Result<UploadLocalAssetResult, String> {
|
|
let root = Path::new(project_path.trim());
|
|
enforce_project_permission_policy(root, "asset.upload")?;
|
|
let _lock = acquire_project_write_lock(root, "asset.upload")?;
|
|
advance_agent_runtime_project_revision_locked(root)?;
|
|
upload_local_asset_at(root, file_name.trim(), media_type.trim(), &bytes)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn register_local_asset(
|
|
project_path: String,
|
|
local_path: String,
|
|
kind: String,
|
|
media_type: String,
|
|
source_kind: String,
|
|
canvas_project_id: Option<String>,
|
|
resource_id: Option<String>,
|
|
asset_object_id: Option<String>,
|
|
task_id: Option<String>,
|
|
prompt: Option<String>,
|
|
model: Option<String>,
|
|
) -> Result<UploadLocalAssetResult, String> {
|
|
let root = Path::new(project_path.trim());
|
|
enforce_project_permission_policy(root, "asset.register")?;
|
|
let _lock = acquire_project_write_lock(root, "asset.register")?;
|
|
advance_agent_runtime_project_revision_locked(root)?;
|
|
register_local_asset_at(
|
|
root,
|
|
local_path.trim(),
|
|
kind.trim(),
|
|
media_type.trim(),
|
|
source_kind.trim(),
|
|
GameCreationAppAssetSource {
|
|
kind: parse_asset_source_kind(source_kind.trim())?,
|
|
canvas_project_id: trim_optional_string(canvas_project_id),
|
|
resource_id: trim_optional_string(resource_id),
|
|
asset_object_id: trim_optional_string(asset_object_id),
|
|
task_id: trim_optional_string(task_id),
|
|
prompt: trim_optional_string(prompt),
|
|
model: trim_optional_string(model),
|
|
},
|
|
)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn import_canvas_asset(
|
|
project_path: String,
|
|
local_path: String,
|
|
kind: String,
|
|
media_type: String,
|
|
canvas_project_id: String,
|
|
resource_id: Option<String>,
|
|
asset_object_id: Option<String>,
|
|
task_id: Option<String>,
|
|
prompt: Option<String>,
|
|
model: Option<String>,
|
|
) -> Result<UploadLocalAssetResult, String> {
|
|
let root = Path::new(project_path.trim());
|
|
enforce_project_permission_policy(root, "canvas.asset_import")?;
|
|
let _lock = acquire_project_write_lock(root, "canvas.asset_import")?;
|
|
advance_agent_runtime_project_revision_locked(root)?;
|
|
import_canvas_asset_at(
|
|
root,
|
|
local_path.trim(),
|
|
kind.trim(),
|
|
media_type.trim(),
|
|
canvas_project_id.trim(),
|
|
trim_optional_string(resource_id),
|
|
trim_optional_string(asset_object_id),
|
|
trim_optional_string(task_id),
|
|
trim_optional_string(prompt),
|
|
trim_optional_string(model),
|
|
)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn import_canvas_export(
|
|
project_path: String,
|
|
export_path: String,
|
|
canvas_project_id: String,
|
|
) -> Result<ImportCanvasExportResult, String> {
|
|
let root = Path::new(project_path.trim());
|
|
enforce_project_permission_policy(root, "canvas.export_import")?;
|
|
let _lock = acquire_project_write_lock(root, "canvas.export_import")?;
|
|
advance_agent_runtime_project_revision_locked(root)?;
|
|
import_canvas_export_at(
|
|
root,
|
|
Path::new(export_path.trim()),
|
|
canvas_project_id.trim(),
|
|
)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) async fn sync_canvas_project_assets(
|
|
project_path: String,
|
|
canvas_project_id: String,
|
|
api_base_url: Option<String>,
|
|
api_key: Option<String>,
|
|
) -> Result<SyncCanvasProjectAssetsResult, String> {
|
|
let root = Path::new(project_path.trim());
|
|
enforce_project_permission_policy(root, "canvas.project_sync")?;
|
|
let _lock = acquire_project_write_lock(root, "canvas.project_sync")?;
|
|
advance_agent_runtime_project_revision_locked(root)?;
|
|
sync_canvas_project_assets_at(root, canvas_project_id.trim(), api_base_url, api_key).await
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) async fn generate_platform_art_asset(
|
|
project_path: String,
|
|
prompt: String,
|
|
) -> Result<UploadLocalAssetResult, String> {
|
|
let root = Path::new(project_path.trim());
|
|
enforce_project_permission_policy(root, "canvas.asset_generate")?;
|
|
let _lock = acquire_project_write_lock(root, "canvas.asset_generate")?;
|
|
advance_agent_runtime_project_revision_locked(root)?;
|
|
let generated = generate_platform_art_asset_at(root, prompt.trim(), &[]).await?;
|
|
Ok(generated.asset)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn open_canvas_project(
|
|
app: tauri::AppHandle,
|
|
canvas_project_id: Option<String>,
|
|
editor_base_url: Option<String>,
|
|
) -> Result<OpenCanvasProjectResult, String> {
|
|
let url = build_canvas_project_url(editor_base_url.as_deref(), canvas_project_id.as_deref())?;
|
|
app.opener()
|
|
.open_url(&url, None::<&str>)
|
|
.map_err(|error| format!("打开画板失败:{error}"))?;
|
|
Ok(OpenCanvasProjectResult { url })
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn get_game_creation_agent_capabilities() -> Vec<GameCreationAgentCapabilityDescriptor> {
|
|
GAME_CREATION_AGENT_CAPABILITIES.to_vec()
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn get_limited_local_commands() -> Vec<GameCreationAppLimitedRunCommandDescriptor> {
|
|
GAME_CREATION_APP_LIMITED_RUN_COMMANDS.to_vec()
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn run_limited_local_command(
|
|
project_path: String,
|
|
command_id: String,
|
|
) -> Result<LimitedLocalCommandResult, String> {
|
|
let root = Path::new(project_path.trim());
|
|
let command_id = command_id.trim();
|
|
enforce_project_permission_policy(root, "command.run_limited")?;
|
|
let _lock = acquire_project_write_lock(root, "command.run_limited")?;
|
|
let result = run_limited_local_command_at(root, command_id)?;
|
|
if command_id == "game.static_smoke" {
|
|
append_static_smoke_manual_trace_step(root, &result)?;
|
|
}
|
|
Ok(result)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn append_local_permission_log(
|
|
project_path: String,
|
|
event: String,
|
|
command_id: String,
|
|
) -> Result<(), String> {
|
|
append_local_permission_log_at(
|
|
Path::new(project_path.trim()),
|
|
event.trim(),
|
|
command_id.trim(),
|
|
)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn list_local_project_files(
|
|
project_path: String,
|
|
) -> Result<ListLocalProjectFilesResult, String> {
|
|
let root = Path::new(project_path.trim());
|
|
enforce_project_permission_policy(root, "file.list")?;
|
|
list_local_project_files_at(root)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn read_local_project_file(
|
|
project_path: String,
|
|
relative_path: String,
|
|
command_id: Option<String>,
|
|
) -> Result<LocalProjectFileResult, String> {
|
|
let root = Path::new(project_path.trim());
|
|
let normalized_path = normalize_relative_path(relative_path.trim())?;
|
|
let command_id = command_id
|
|
.as_deref()
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.unwrap_or("file.read");
|
|
if command_id == "agent.trace_read" {
|
|
if !is_agent_trace_read_path(&normalized_path) {
|
|
return Err("agent.trace_read 只能读取 Agent run trace".to_string());
|
|
}
|
|
} else if command_id != "file.read" {
|
|
return Err(format!("不支持通过文件读取执行命令:{command_id}"));
|
|
}
|
|
enforce_project_permission_policy(root, command_id)?;
|
|
read_local_project_file_at(root, &normalized_path)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn write_local_project_file(
|
|
project_path: String,
|
|
relative_path: String,
|
|
content: String,
|
|
) -> Result<LocalProjectFileMutationResult, String> {
|
|
let root = Path::new(project_path.trim());
|
|
enforce_project_permission_policy(root, "file.write")?;
|
|
let _lock = acquire_project_write_lock(root, "file.write")?;
|
|
advance_agent_runtime_project_revision_locked(root)?;
|
|
write_local_project_file_at(root, relative_path.trim(), &content)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn delete_local_project_file(
|
|
project_path: String,
|
|
relative_path: String,
|
|
) -> Result<LocalProjectFileMutationResult, String> {
|
|
let root = Path::new(project_path.trim());
|
|
enforce_project_permission_policy(root, "file.delete")?;
|
|
let _lock = acquire_project_write_lock(root, "file.delete")?;
|
|
advance_agent_runtime_project_revision_locked(root)?;
|
|
delete_local_project_file_at(root, relative_path.trim())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn read_local_game_memory(
|
|
project_path: String,
|
|
scope: String,
|
|
) -> Result<LocalGameMemoryResult, String> {
|
|
let root = Path::new(project_path.trim());
|
|
enforce_project_permission_policy(root, "memory.read")?;
|
|
read_local_game_memory_at(root, scope.trim())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn read_local_agent_memory(
|
|
project_path: String,
|
|
task_id: String,
|
|
) -> Result<LocalAgentMemoryResult, String> {
|
|
let root = Path::new(project_path.trim());
|
|
let task_id = normalize_game_creator_runtime_agent_id(task_id.trim())?;
|
|
enforce_project_permission_policy(root, "memory.read")?;
|
|
read_local_agent_memory_at(root, &task_id)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn write_local_agent_memory(
|
|
project_path: String,
|
|
task_id: String,
|
|
content: String,
|
|
) -> Result<LocalAgentMemoryResult, String> {
|
|
let root = Path::new(project_path.trim());
|
|
let task_id = normalize_game_creator_runtime_agent_id(task_id.trim())?;
|
|
enforce_project_permission_policy(root, "memory.write")?;
|
|
let _lock = acquire_project_write_lock(root, "memory.write")?;
|
|
advance_agent_runtime_project_revision_locked(root)?;
|
|
write_local_agent_memory_at(root, &task_id, &content)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn write_local_game_memory(
|
|
project_path: String,
|
|
scope: String,
|
|
content: String,
|
|
) -> Result<LocalGameMemoryResult, String> {
|
|
let root = Path::new(project_path.trim());
|
|
enforce_project_permission_policy(root, "memory.write")?;
|
|
let _lock = acquire_project_write_lock(root, "memory.write")?;
|
|
advance_agent_runtime_project_revision_locked(root)?;
|
|
write_local_game_memory_at(root, scope.trim(), &content)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn delete_local_game_memory(
|
|
project_path: String,
|
|
scope: String,
|
|
) -> Result<LocalGameMemoryResult, String> {
|
|
let root = Path::new(project_path.trim());
|
|
enforce_project_permission_policy(root, "memory.delete")?;
|
|
let _lock = acquire_project_write_lock(root, "memory.delete")?;
|
|
advance_agent_runtime_project_revision_locked(root)?;
|
|
delete_local_game_memory_at(root, scope.trim())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn list_game_creator_agent_sessions(
|
|
project_path: String,
|
|
agent_id: String,
|
|
) -> Result<AgentConversationSessionListResult, String> {
|
|
let root = Path::new(project_path.trim());
|
|
enforce_project_permission_policy(root, "conversation.read")?;
|
|
list_game_creator_agent_sessions_at(root, agent_id.trim())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn create_game_creator_agent_session(
|
|
project_path: String,
|
|
agent_id: String,
|
|
title: String,
|
|
) -> Result<AgentConversationSessionListResult, String> {
|
|
let root = Path::new(project_path.trim());
|
|
enforce_project_permission_policy(root, "conversation.read")?;
|
|
enforce_project_permission_policy(root, "conversation.write")?;
|
|
let _lock = acquire_project_write_lock(root, "conversation.write")?;
|
|
create_game_creator_agent_session_at(root, agent_id.trim(), title.trim())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn set_active_game_creator_agent_session(
|
|
project_path: String,
|
|
agent_id: String,
|
|
session_id: String,
|
|
) -> Result<AgentConversationSessionListResult, String> {
|
|
let root = Path::new(project_path.trim());
|
|
enforce_project_permission_policy(root, "conversation.read")?;
|
|
enforce_project_permission_policy(root, "conversation.write")?;
|
|
let _lock = acquire_project_write_lock(root, "conversation.write")?;
|
|
set_active_game_creator_agent_session_at(root, agent_id.trim(), session_id.trim())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn archive_game_creator_agent_session(
|
|
project_path: String,
|
|
agent_id: String,
|
|
session_id: String,
|
|
) -> Result<AgentConversationSessionListResult, String> {
|
|
let root = Path::new(project_path.trim());
|
|
enforce_project_permission_policy(root, "conversation.read")?;
|
|
enforce_project_permission_policy(root, "conversation.write")?;
|
|
let _lock = acquire_project_write_lock(root, "conversation.write")?;
|
|
archive_game_creator_agent_session_at(root, agent_id.trim(), session_id.trim())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn read_local_conversation(
|
|
project_path: String,
|
|
agent_id: Option<String>,
|
|
session_id: Option<String>,
|
|
) -> Result<LocalConversationResult, String> {
|
|
let root = Path::new(project_path.trim());
|
|
enforce_project_permission_policy(root, "conversation.read")?;
|
|
read_local_conversation_for_session_at(root, agent_id.as_deref(), session_id.as_deref())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn append_local_conversation_message(
|
|
project_path: String,
|
|
agent_id: Option<String>,
|
|
session_id: Option<String>,
|
|
message: LocalConversationMessage,
|
|
) -> 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,
|
|
)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn build_local_project_index(
|
|
project_path: String,
|
|
) -> Result<LocalProjectIndexResult, String> {
|
|
let root = Path::new(project_path.trim());
|
|
enforce_project_permission_policy(root, "project.index")?;
|
|
let _lock = acquire_project_write_lock(root, "project.index")?;
|
|
build_local_project_index_at(root)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn create_local_project_checkpoint(
|
|
project_path: String,
|
|
) -> Result<LocalProjectCheckpointResult, String> {
|
|
let root = Path::new(project_path.trim());
|
|
enforce_project_permission_policy(root, "project.checkpoint")?;
|
|
let _lock = acquire_project_write_lock(root, "project.checkpoint")?;
|
|
create_local_project_checkpoint_at(root)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn export_local_project_package(
|
|
project_path: String,
|
|
) -> Result<LocalProjectExportPackageResult, String> {
|
|
let root = Path::new(project_path.trim());
|
|
enforce_project_permission_policy(root, "project.export_package")?;
|
|
let _lock = acquire_project_write_lock(root, "project.export_package")?;
|
|
advance_agent_runtime_project_revision_locked(root)?;
|
|
export_local_project_package_at(root)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn list_local_project_export_packages(
|
|
project_path: String,
|
|
) -> Result<LocalProjectExportPackagesResult, String> {
|
|
let root = Path::new(project_path.trim());
|
|
enforce_project_permission_policy(root, "project.export_list")?;
|
|
list_local_project_export_packages_at(root)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn diff_local_project_checkpoint(
|
|
project_path: String,
|
|
checkpoint_id: String,
|
|
) -> Result<LocalProjectDiffResult, String> {
|
|
let root = Path::new(project_path.trim());
|
|
enforce_project_permission_policy(root, "project.diff")?;
|
|
diff_local_project_checkpoint_at(root, checkpoint_id.trim())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn restore_local_project_checkpoint(
|
|
project_path: String,
|
|
checkpoint_id: String,
|
|
) -> Result<LocalProjectRestoreResult, String> {
|
|
let root = Path::new(project_path.trim());
|
|
enforce_project_permission_policy(root, "project.restore")?;
|
|
let _lock = acquire_project_write_lock(root, "project.restore")?;
|
|
advance_agent_runtime_project_revision_locked(root)?;
|
|
restore_local_project_checkpoint_at(root, checkpoint_id.trim())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn read_project_permission_policy(
|
|
project_path: String,
|
|
) -> Result<ProjectPermissionPolicyView, String> {
|
|
let root = Path::new(project_path.trim());
|
|
enforce_project_permission_policy(root, "project.policy_read")?;
|
|
read_project_permission_policy_at(root)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) fn write_project_permission_policy(
|
|
project_path: String,
|
|
policy: ProjectPermissionPolicy,
|
|
) -> Result<ProjectPermissionPolicyView, String> {
|
|
let root = Path::new(project_path.trim());
|
|
enforce_project_permission_policy(root, "project.policy_write")?;
|
|
let _lock = acquire_project_write_lock(root, "project.policy_write")?;
|
|
write_project_permission_policy_at(root, policy)
|
|
}
|