diff --git a/src-tauri/src/commands/workspace_cmd.rs b/src-tauri/src/commands/workspace_cmd.rs index 9746ade07..5d36d577d 100644 --- a/src-tauri/src/commands/workspace_cmd.rs +++ b/src-tauri/src/commands/workspace_cmd.rs @@ -13,7 +13,9 @@ use crate::database::DbConnection; use crate::models::project_model::ProjectContext; -use crate::services::workspace_health_service::ensure_workspace_root_ready; +use crate::services::workspace_health_service::{ + ensure_workspace_ready_with_auto_relocate, ensure_workspace_root_ready, +}; use crate::workspace::{ Workspace, WorkspaceManager, WorkspaceSettings, WorkspaceType, WorkspaceUpdate, }; @@ -85,6 +87,9 @@ pub struct WorkspaceEnsureResult { pub existed: bool, pub created: bool, pub repaired: bool, + pub relocated: bool, + pub previous_root_path: Option, + pub warning: Option, } impl From for WorkspaceListItem { @@ -200,12 +205,12 @@ pub async fn workspace_update( let new_root_path = if let Some(ref path_str) = request.root_path { let path = PathBuf::from(path_str); - // 预校验新路径:路径必须存在且为目录 - if !path.exists() { - return Err(format!("指定的路径不存在: {path_str}")); - } - if !path.is_dir() { - return Err(format!("指定的路径不是目录: {path_str}")); + let created = ensure_workspace_root_ready(&path)?; + if created { + tracing::warn!( + "[Workspace] 更新路径时检测到目录缺失,已自动创建: {}", + path.to_string_lossy() + ); } Some(path) } else { @@ -277,23 +282,31 @@ pub async fn workspace_ensure_ready( let workspace = manager .get(&id)? .ok_or_else(|| format!("Workspace 不存在: {id}"))?; - let root_path = workspace.root_path.to_string_lossy().to_string(); + let ensured = ensure_workspace_ready_with_auto_relocate(&manager, &workspace)?; + let root_path = ensured.root_path.to_string_lossy().to_string(); + let previous_root_path = ensured + .previous_root_path + .as_ref() + .map(|path| path.to_string_lossy().to_string()); - let created = ensure_workspace_root_ready(&workspace.root_path)?; - if created { + if ensured.repaired { tracing::warn!( - "[Workspace] 检测到目录缺失,已自动创建: id={}, root={}", + "[Workspace] 检测到目录异常并已修复: id={}, root={}, relocated={}", workspace.id, - root_path + root_path, + ensured.relocated ); } Ok(WorkspaceEnsureResult { workspace_id: workspace.id, root_path, - existed: !created, - created, - repaired: created, + existed: ensured.existed, + created: ensured.created, + repaired: ensured.repaired, + relocated: ensured.relocated, + previous_root_path, + warning: ensured.warning, }) } @@ -306,21 +319,29 @@ pub async fn workspace_ensure_default_ready( let Some(workspace) = manager.get_default()? else { return Ok(None); }; - let root_path = workspace.root_path.to_string_lossy().to_string(); - let created = ensure_workspace_root_ready(&workspace.root_path)?; - if created { + let ensured = ensure_workspace_ready_with_auto_relocate(&manager, &workspace)?; + let root_path = ensured.root_path.to_string_lossy().to_string(); + let previous_root_path = ensured + .previous_root_path + .as_ref() + .map(|path| path.to_string_lossy().to_string()); + if ensured.repaired { tracing::warn!( - "[Workspace] 启动检查发现默认 workspace 目录缺失,已自动创建: id={}, root={}", + "[Workspace] 启动检查发现默认 workspace 目录异常并已修复: id={}, root={}, relocated={}", workspace.id, - root_path + root_path, + ensured.relocated ); } Ok(Some(WorkspaceEnsureResult { workspace_id: workspace.id, root_path, - existed: !created, - created, - repaired: created, + existed: ensured.existed, + created: ensured.created, + repaired: ensured.repaired, + relocated: ensured.relocated, + previous_root_path, + warning: ensured.warning, })) } @@ -375,9 +396,8 @@ pub async fn get_or_create_default_project( // 不存在则创建默认项目 let default_project_path = get_workspace_projects_root_dir()?.join("default"); - std::fs::create_dir_all(&default_project_path).map_err(|e| { - format!("创建默认项目目录失败: {e}") - })?; + std::fs::create_dir_all(&default_project_path) + .map_err(|e| format!("创建默认项目目录失败: {e}"))?; let workspace = manager.create_with_type( "默认项目".to_string(), diff --git a/src-tauri/src/services/workspace_health_service.rs b/src-tauri/src/services/workspace_health_service.rs index bf5b33e2a..f159c170d 100644 --- a/src-tauri/src/services/workspace_health_service.rs +++ b/src-tauri/src/services/workspace_health_service.rs @@ -1,4 +1,16 @@ -use std::path::Path; +use crate::workspace::{Workspace, WorkspaceManager, WorkspaceUpdate}; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone)] +pub struct WorkspaceReadyResult { + pub root_path: PathBuf, + pub existed: bool, + pub created: bool, + pub repaired: bool, + pub relocated: bool, + pub previous_root_path: Option, + pub warning: Option, +} /// 确保 workspace 根目录可用。 /// @@ -41,3 +53,102 @@ pub fn ensure_workspace_root_ready(workspace_root: &Path) -> Result Result { + let original_root = workspace.root_path.clone(); + + match ensure_workspace_root_ready(&original_root) { + Ok(created) => { + return Ok(WorkspaceReadyResult { + root_path: original_root, + existed: !created, + created, + repaired: created, + relocated: false, + previous_root_path: None, + warning: None, + }); + } + Err(primary_error) => { + let fallback_root = build_workspace_fallback_root(workspace)?; + if fallback_root == original_root { + return Err(primary_error); + } + + let fallback_created = + ensure_workspace_root_ready(&fallback_root).map_err(|fallback_error| { + format!("{primary_error};自动迁移到托管目录失败: {fallback_error}") + })?; + + manager + .update( + &workspace.id, + WorkspaceUpdate { + root_path: Some(fallback_root.clone()), + ..WorkspaceUpdate::default() + }, + ) + .map_err(|error| format!("Workspace 自动迁移后更新配置失败: {error}"))?; + + let warning = format!( + "Workspace 原路径不可用,已自动迁移到托管目录: {} -> {}", + original_root.display(), + fallback_root.display() + ); + + tracing::warn!( + "[WorkspaceHealth] {} (workspace_id={})", + warning, + workspace.id + ); + + Ok(WorkspaceReadyResult { + root_path: fallback_root, + existed: !fallback_created, + created: fallback_created, + repaired: true, + relocated: true, + previous_root_path: Some(original_root), + warning: Some(warning), + }) + } + } +} + +fn build_workspace_fallback_root(workspace: &Workspace) -> Result { + let home_dir = dirs::home_dir().ok_or_else(|| "无法获取主目录".to_string())?; + let recovered_root = home_dir + .join(".proxycast") + .join("projects") + .join("recovered"); + let workspace_name = sanitize_path_segment(&workspace.name); + let short_id: String = workspace.id.chars().take(8).collect(); + let dir_name = if short_id.is_empty() { + workspace_name + } else { + format!("{}-{}", workspace_name, short_id) + }; + Ok(recovered_root.join(dir_name)) +} + +fn sanitize_path_segment(raw: &str) -> String { + let sanitized: String = raw + .trim() + .chars() + .map(|ch| match ch { + '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_', + _ if ch.is_control() => '_', + _ => ch, + }) + .collect(); + + let trimmed = sanitized.trim().trim_matches('.').to_string(); + if trimmed.is_empty() { + "workspace".to_string() + } else { + trimmed + } +} diff --git a/src/components/agent/chat/index.tsx b/src/components/agent/chat/index.tsx index c4e99c3b6..bd70938a2 100644 --- a/src/components/agent/chat/index.tsx +++ b/src/components/agent/chat/index.tsx @@ -469,11 +469,11 @@ export function AgentChatPage({ const normalizedId = normalizeProjectId(projectId); if (!normalizedId) return; - invoke<{ created: boolean; rootPath: string }>("workspace_ensure_ready", { + invoke<{ created: boolean; repaired: boolean; rootPath: string }>("workspace_ensure_ready", { id: normalizedId, }) - .then(({ created, rootPath }) => { - if (created) { + .then(({ repaired, rootPath }) => { + if (repaired) { recordWorkspaceRepair({ workspaceId: normalizedId, rootPath, diff --git a/src/hooks/useProjects.ts b/src/hooks/useProjects.ts index 7780666f2..b417e1da4 100644 --- a/src/hooks/useProjects.ts +++ b/src/hooks/useProjects.ts @@ -21,6 +21,9 @@ interface WorkspaceEnsureResult { existed: boolean; created: boolean; repaired: boolean; + relocated?: boolean; + previousRootPath?: string | null; + warning?: string | null; } // WorkspaceType 用于类型定义,暂未使用 // import type { WorkspaceType } from '@/types/workspace'; @@ -79,7 +82,7 @@ export function useProjects(): UseProjectsReturn { "workspace_ensure_ready", { id: defaultProj.id }, ); - if (ensureResult.created) { + if (ensureResult.repaired) { recordWorkspaceRepair({ workspaceId: ensureResult.workspaceId, rootPath: ensureResult.rootPath, diff --git a/src/hooks/useWorkspace.ts b/src/hooks/useWorkspace.ts index c4717c797..47671fe50 100644 --- a/src/hooks/useWorkspace.ts +++ b/src/hooks/useWorkspace.ts @@ -71,6 +71,9 @@ interface WorkspaceEnsureResult { existed: boolean; created: boolean; repaired: boolean; + relocated?: boolean; + previousRootPath?: string | null; + warning?: string | null; } /** @@ -103,7 +106,7 @@ export function useWorkspace(): UseWorkspaceReturn { "workspace_ensure_ready", { id: defaultWs.id }, ); - if (ensureResult.created) { + if (ensureResult.repaired) { recordWorkspaceRepair({ workspaceId: ensureResult.workspaceId, rootPath: ensureResult.rootPath, @@ -165,7 +168,7 @@ export function useWorkspace(): UseWorkspaceReturn { "workspace_ensure_ready", { id }, ); - if (ensureResult.created) { + if (ensureResult.repaired) { recordWorkspaceRepair({ workspaceId: ensureResult.workspaceId, rootPath: ensureResult.rootPath,