diff --git a/apps/ai-game-creator-shell/src-tauri/src/commands.rs b/apps/ai-game-creator-shell/src-tauri/src/commands.rs index b3cc58f28..d8dd83cae 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/commands.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/commands.rs @@ -811,6 +811,41 @@ pub(crate) async fn pick_local_project_directory( Ok(Some(path.to_string_lossy().into_owned())) } +/// Create a user-requested child directory using the current user's normal +/// filesystem rights. This deliberately does not inspect or rewrite ACLs and +/// never attempts elevation. +#[tauri::command] +pub(crate) fn create_local_project_directory( + parent_path: String, + directory_name: String, +) -> Result { + let parent = Path::new(parent_path.trim()); + if parent.as_os_str().is_empty() || !parent.is_absolute() { + return Err("父目录必须是绝对路径".to_string()); + } + if project_path_has_control_chars(parent) { + return Err("父目录不能包含控制字符".to_string()); + } + if !parent.is_dir() { + return Err("父目录不存在或不是文件夹".to_string()); + } + let name = directory_name.trim(); + if name.is_empty() || name == "." || name == ".." || name.chars().any(|c| c == '/' || c == '\\') + { + return Err("目录名称无效".to_string()); + } + if name.chars().any(|c| c.is_control()) { + return Err("目录名称不能包含控制字符".to_string()); + } + let target = parent.join(name); + fs::create_dir(&target).map_err(|error| match error.kind() { + std::io::ErrorKind::AlreadyExists => "目录已存在".to_string(), + std::io::ErrorKind::PermissionDenied => "没有权限在此位置创建目录".to_string(), + _ => format!("创建目录失败:{error}"), + })?; + Ok(target.to_string_lossy().into_owned()) +} + #[tauri::command] pub(crate) async fn pick_local_file(app: tauri::AppHandle) -> Result, String> { let (sender, receiver) = tokio::sync::oneshot::channel(); diff --git a/apps/ai-game-creator-shell/src-tauri/src/main.rs b/apps/ai-game-creator-shell/src-tauri/src/main.rs index 2f2f5b7e2..517e60ad0 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -2636,6 +2636,7 @@ fn main() { is_local_project_directory_non_empty, inspect_local_project_directory, pick_local_project_directory, + create_local_project_directory, rename_local_game_project, suggest_automatic_project_name, polish_local_project_prompt, diff --git a/apps/ai-game-creator-shell/src/features/app-shell/useHomeProjectCreation.ts b/apps/ai-game-creator-shell/src/features/app-shell/useHomeProjectCreation.ts index 61fb8681a..387bdf127 100644 --- a/apps/ai-game-creator-shell/src/features/app-shell/useHomeProjectCreation.ts +++ b/apps/ai-game-creator-shell/src/features/app-shell/useHomeProjectCreation.ts @@ -895,14 +895,26 @@ export function useHomeProjectCreation({ setProjectAction('creating'); setStatus('正在选择新项目文件夹'); try { - const selectedPath = await invoke( + const parentPath = await invoke( 'pick_local_project_directory', projectPath.trim() ? { initialPath: projectPath.trim() } : undefined, ); - if (!selectedPath) { + if (!parentPath) { setStatus('已取消'); return; } + const directoryName = window.prompt('请输入新目录名称'); + if (!directoryName?.trim()) { + setStatus('已取消'); + return; + } + const selectedPath = await invoke( + 'create_local_project_directory', + { + parentPath, + directoryName: directoryName.trim(), + }, + ); setProjectPath(selectedPath); projectActionRef.current = null; setProjectAction(null); diff --git a/docs/project-memory/plans/【实施计划】AGC首页自定义工作目录-2026-09-15.md b/docs/project-memory/plans/【实施计划】AGC首页自定义工作目录-2026-09-15.md new file mode 100644 index 000000000..2b3a86c47 --- /dev/null +++ b/docs/project-memory/plans/【实施计划】AGC首页自定义工作目录-2026-09-15.md @@ -0,0 +1,26 @@ +# 【实施计划】AGC 首页自定义工作目录 + +Version: 1 +Status: active +Date: 2026-09-15 +Parent Spec: 【里程碑】AGC首页自定义工作目录-2026-09-15 + +## 顺序 + +1. 定位现有首页创建与 Tauri 命令注册。 +2. 增加目录选择/创建/检查的最小 native command 与前端适配。 +3. 接入首页创建状态,保留现有 HomeCreationOperation。 +4. 补 Rust/前端定向测试,执行编码和差异检查。 + +## 验证 + +- AGC web typecheck +- 相关 Tauri cargo test +- AGC appSurface 定向测试 +- npm run check:encoding +- git diff --check + +## 风险与回滚 + +- 风险:Windows 权限检查误触发 ACL/UAC;只使用普通文件 API,不调用安全描述符或提升权限。 +- 回滚:移除新增 command/UI 适配,保留既有自动建项路径。 diff --git a/docs/project-memory/plans/【里程碑】AGC首页自定义工作目录-2026-09-15.md b/docs/project-memory/plans/【里程碑】AGC首页自定义工作目录-2026-09-15.md new file mode 100644 index 000000000..38c446f08 --- /dev/null +++ b/docs/project-memory/plans/【里程碑】AGC首页自定义工作目录-2026-09-15.md @@ -0,0 +1,33 @@ +# 【里程碑】AGC 首页自定义工作目录 + +Version: 1 +Status: active +Date: 2026-09-15 +Parent Spec: AGC 客户端 AI 游戏创作 App 实施计划 + +## 范围 + +- 首页创建入口支持选择已有目录或创建子目录。 +- 目录校验包含存在性、目录类型、可读写性和空目录判断。 +- 校验通过后复用现有首页建项流程。 +- 外部进程在校验后写入目录导致非空,不纳入本里程碑处理。 + +## 权限边界 + +- 不修改 ACL,不请求管理员权限,不通过提升权限探测目录。 +- 可写性以当前用户在目标目录执行无害临时文件创建/删除为准;失败返回普通错误。 +- 选择目录使用系统目录选择器,创建目录使用当前用户可写的父目录。 + +## 验收 + +- 选择空目录可继续建项。 +- 选择非空目录被阻止并可重新选择。 +- 创建目录成功后自动选中并可继续建项。 +- 不存在、非目录、不可读写均有稳定错误状态。 +- 不触发 UAC,不出现 ACL 权限栈错误泄漏。 + +## 不做 + +- 不合并非空目录内容。 +- 不监控校验后目录变化。 +- 不新增项目格式或公开 API。