Compare commits

...

2 Commits

Author SHA1 Message Date
lhk229 dfb7617cbb 补充AGC工作目录创建测试
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 5m14s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 5m33s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 5m49s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 5m52s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m39s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m23s
Project CI / Frontend tests (pull_request) Failing after 2m46s
Project CI / Repository checks (pull_request) Failing after 3m29s
Project CI / AI game creator shell web tests (pull_request) Failing after 3m24s
Project CI / Native shell tests (pull_request) Successful in 8m35s
Project CI / Backend tests (pull_request) Successful in 10m52s
覆盖子目录创建、重名和路径分隔符校验
2026-09-15 14:42:10 +08:00
lhk229 fa2a7caa95 新增AGC首页自定义工作目录
新增用户目录创建命令与首页接线

保持普通用户权限并避免ACL和UAC耦合

补充里程碑与实施计划
2026-09-15 14:38:04 +08:00
5 changed files with 153 additions and 2 deletions
@@ -811,6 +811,41 @@ pub(crate) async fn pick_local_project_directory(
Ok(Some(path.to_string_lossy().into_owned())) 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<String, String> {
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] #[tauri::command]
pub(crate) async fn pick_local_file(app: tauri::AppHandle) -> Result<Option<String>, String> { pub(crate) async fn pick_local_file(app: tauri::AppHandle) -> Result<Option<String>, String> {
let (sender, receiver) = tokio::sync::oneshot::channel(); let (sender, receiver) = tokio::sync::oneshot::channel();
@@ -5364,3 +5399,47 @@ pub(crate) fn write_project_permission_policy(
let _lock = acquire_project_write_lock(root, "project.policy_write")?; let _lock = acquire_project_write_lock(root, "project.policy_write")?;
write_project_permission_policy_at(root, policy) write_project_permission_policy_at(root, policy)
} }
#[cfg(test)]
mod custom_directory_tests {
use super::create_local_project_directory;
use std::fs;
#[test]
fn creates_child_directory_without_touching_parent_contents() {
let root = tempfile::tempdir().expect("temp parent");
let created = create_local_project_directory(
root.path().to_string_lossy().into_owned(),
"new-game".to_string(),
)
.expect("create directory");
let path = std::path::PathBuf::from(created);
assert!(path.is_dir());
assert!(fs::read_dir(root.path())
.expect("read parent")
.next()
.is_some());
}
#[test]
fn rejects_existing_child_and_path_separator() {
let root = tempfile::tempdir().expect("temp parent");
fs::create_dir(root.path().join("existing")).expect("existing");
assert_eq!(
create_local_project_directory(
root.path().to_string_lossy().into_owned(),
"existing".to_string(),
)
.expect_err("duplicate must fail"),
"目录已存在"
);
assert_eq!(
create_local_project_directory(
root.path().to_string_lossy().into_owned(),
"nested/name".to_string(),
)
.expect_err("separator must fail"),
"目录名称无效"
);
}
}
@@ -2636,6 +2636,7 @@ fn main() {
is_local_project_directory_non_empty, is_local_project_directory_non_empty,
inspect_local_project_directory, inspect_local_project_directory,
pick_local_project_directory, pick_local_project_directory,
create_local_project_directory,
rename_local_game_project, rename_local_game_project,
suggest_automatic_project_name, suggest_automatic_project_name,
polish_local_project_prompt, polish_local_project_prompt,
@@ -895,14 +895,26 @@ export function useHomeProjectCreation({
setProjectAction('creating'); setProjectAction('creating');
setStatus('正在选择新项目文件夹'); setStatus('正在选择新项目文件夹');
try { try {
const selectedPath = await invoke<string | null>( const parentPath = await invoke<string | null>(
'pick_local_project_directory', 'pick_local_project_directory',
projectPath.trim() ? { initialPath: projectPath.trim() } : undefined, projectPath.trim() ? { initialPath: projectPath.trim() } : undefined,
); );
if (!selectedPath) { if (!parentPath) {
setStatus('已取消'); setStatus('已取消');
return; return;
} }
const directoryName = window.prompt('请输入新目录名称');
if (!directoryName?.trim()) {
setStatus('已取消');
return;
}
const selectedPath = await invoke<string>(
'create_local_project_directory',
{
parentPath,
directoryName: directoryName.trim(),
},
);
setProjectPath(selectedPath); setProjectPath(selectedPath);
projectActionRef.current = null; projectActionRef.current = null;
setProjectAction(null); setProjectAction(null);
@@ -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 适配,保留既有自动建项路径。
@@ -0,0 +1,33 @@
# 【里程碑】AGC 首页自定义工作目录
Version: 1
Status: active
Date: 2026-09-15
Parent Spec: AGC 客户端 AI 游戏创作 App 实施计划
## 范围
- 首页创建入口支持选择已有目录或创建子目录。
- 目录校验包含存在性、目录类型、可读写性和空目录判断。
- 校验通过后复用现有首页建项流程。
- 外部进程在校验后写入目录导致非空,不纳入本里程碑处理。
## 权限边界
- 不修改 ACL,不请求管理员权限,不通过提升权限探测目录。
- 可写性以当前用户在目标目录执行无害临时文件创建/删除为准;失败返回普通错误。
- 选择目录使用系统目录选择器,创建目录使用当前用户可写的父目录。
## 验收
- 选择空目录可继续建项。
- 选择非空目录被阻止并可重新选择。
- 创建目录成功后自动选中并可继续建项。
- 不存在、非目录、不可读写均有稳定错误状态。
- 不触发 UAC,不出现 ACL 权限栈错误泄漏。
## 不做
- 不合并非空目录内容。
- 不监控校验后目录变化。
- 不新增项目格式或公开 API。