mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
fix: add auto-relocate to workspace health and use repaired flag for telemetry
- workspace_health_service: add ensure_workspace_ready_with_auto_relocate that falls back to
a managed path (~/.proxycast/projects/{id}) when original root is inaccessible
- WorkspaceReadyResult: include relocated, previousRootPath, warning fields
- workspace_cmd: expose relocated/previousRootPath/warning in WorkspaceEnsureResult response;
add ensure_workspace_ready_with_auto_relocate to workspace_update path
- useProjects/useWorkspace/AgentChatPage: use repaired (not created) flag to trigger repair
telemetry, since repaired=true covers both create-new and auto-relocate scenarios
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
61270d6f3a
commit
0f1be80ba7
@@ -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<String>,
|
||||
pub warning: Option<String>,
|
||||
}
|
||||
|
||||
impl From<Workspace> 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(),
|
||||
|
||||
@@ -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<PathBuf>,
|
||||
pub warning: Option<String>,
|
||||
}
|
||||
|
||||
/// 确保 workspace 根目录可用。
|
||||
///
|
||||
@@ -41,3 +53,102 @@ pub fn ensure_workspace_root_ready(workspace_root: &Path) -> Result<bool, String
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn ensure_workspace_ready_with_auto_relocate(
|
||||
manager: &WorkspaceManager,
|
||||
workspace: &Workspace,
|
||||
) -> Result<WorkspaceReadyResult, String> {
|
||||
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<PathBuf, String> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user