use std::path::Path;
use std::sync::Arc;
use axum::{
Json,
extract::{Query, State},
http::StatusCode,
};
use log::error;
use serde_json;
use super::handlers_sync::workspace_dir_create_sync;
#[cfg(unix)]
use super::handlers_sync::{
workspace_delete_file_sync_unix, workspace_file_write_sync_unix, workspace_list_entries_sync,
workspace_read_file_sync_unix,
};
#[cfg(not(unix))]
use crate::text_encoding::decode_bytes_strict;
use crate::text_encoding::parse_text_encoding_name;
use crate::web::app_state::AppStateHttpCore;
use crate::web::http_types::validation::{
clamp_workspace_search_max_results, validate_workspace_file_write_request,
validate_workspace_query_encoding_optional, workspace_search_pattern_or_error,
};
#[cfg(not(unix))]
use crate::web::http_types::workspace::WorkspaceEntry;
use crate::web::http_types::workspace::{
WorkspaceDirCreateBody, WorkspaceDirCreateResponse, WorkspaceDirDeleteQuery,
WorkspaceDirDeleteResponse, WorkspaceFileDeleteResponse, WorkspaceFileQuery,
WorkspaceFileReadResponse, WorkspaceFileWriteBody, WorkspaceFileWriteResponse,
WorkspacePickResponse, WorkspaceProfileResponse, WorkspaceQuery, WorkspaceResponse,
WorkspaceSearchBody, WorkspaceSearchResponse, WorkspaceSetBody,
};
use crate::workspace::path::{
WorkspacePathError, resolve_web_workspace_read_path, resolve_web_workspace_write_path,
validate_effective_workspace_base, validate_workspace_set_path,
};
const WORKSPACE_FILE_READ_MAX_BYTES: u64 = 1_048_576;
async fn workspace_file_read_resolve(
http: &AppStateHttpCore,
query: &WorkspaceFileQuery,
) -> Result<
(
std::path::PathBuf,
std::path::PathBuf,
crate::text_encoding::TextEncodingName,
),
Json<WorkspaceFileReadResponse>,
> {
if let Err(e) = validate_workspace_query_encoding_optional(query.encoding.as_deref()) {
return Err(Json(WorkspaceFileReadResponse {
content: String::new(),
error: Some(e),
}));
}
let base_canonical = match effective_workspace_base_canonical(http).await {
Ok(p) => p,
Err(e) => {
return Err(Json(WorkspaceFileReadResponse {
content: String::new(),
error: Some(e.user_message()),
}));
}
};
let path = query.path.trim();
if path.is_empty() {
return Err(Json(WorkspaceFileReadResponse {
content: String::new(),
error: Some("path 不能为空".to_string()),
}));
}
let canonical =
match resolve_web_workspace_read_path(&base_canonical, Some(query.path.as_str())) {
Ok(p) => p,
Err(e) => {
return Err(Json(WorkspaceFileReadResponse {
content: String::new(),
error: Some(e.user_message()),
}));
}
};
let enc_name = match parse_text_encoding_name(query.encoding.as_deref()) {
Ok(n) => n,
Err(msg) => {
return Err(Json(WorkspaceFileReadResponse {
content: String::new(),
error: Some(msg),
}));
}
};
Ok((base_canonical, canonical, enc_name))
}
async fn effective_workspace_base_canonical(
http: &AppStateHttpCore,
) -> Result<std::path::PathBuf, WorkspacePathError> {
let base_str = http.effective_workspace_path().await;
if base_str.trim().is_empty() {
return Err(WorkspacePathError::WebEffectiveWorkspaceUnset);
}
let base = Path::new(&base_str);
let base_canonical = base
.canonicalize()
.map_err(WorkspacePathError::WorkspaceResolveFailed)?;
let cfg = http.cfg.read().await;
validate_effective_workspace_base(&cfg, &base_canonical)?;
Ok(base_canonical)
}
pub async fn workspace_pick_handler() -> Json<WorkspacePickResponse> {
Json(WorkspacePickResponse { path: None })
}
pub async fn workspace_set_handler(
State(http): State<AppStateHttpCore>,
Json(body): Json<WorkspaceSetBody>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
let raw = if let Some(project) = body
.project
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
{
let cfg = http.cfg.read().await;
match super::projects::resolve_workspace_set_project_path(&cfg, project) {
Ok(p) => p.display().to_string(),
Err(e) => {
let status = if e.is_policy_denied() {
StatusCode::FORBIDDEN
} else {
StatusCode::BAD_REQUEST
};
return Err((
status,
Json(serde_json::json!({ "ok": false, "error": e.user_message() })),
));
}
}
} else {
body.path
.as_deref()
.map(|s| s.trim())
.unwrap_or("")
.to_string()
};
set_workspace_from_raw_path(&http, raw.as_str()).await
}
pub(crate) async fn apply_workspace_override(http: &AppStateHttpCore, path_str: &str) {
let mut guard = http.workspace_override.write().await;
*guard = Some(path_str.to_string());
}
async fn set_workspace_from_raw_path(
http: &AppStateHttpCore,
raw: &str,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
let mut guard = http.workspace_override.write().await;
if raw.is_empty() {
*guard = Some(String::new());
return Ok(Json(serde_json::json!({ "ok": true, "path": "" })));
}
let cfg = http.cfg.read().await;
let canon = match validate_workspace_set_path(&cfg, raw) {
Ok(p) => p,
Err(e) => {
let status = if e.is_policy_denied() {
StatusCode::FORBIDDEN
} else {
StatusCode::BAD_REQUEST
};
return Err((
status,
Json(serde_json::json!({ "ok": false, "error": e.user_message() })),
));
}
};
let path_str = canon.display().to_string();
*guard = Some(path_str.clone());
Ok(Json(serde_json::json!({ "ok": true, "path": path_str })))
}
fn workspace_list_empty(path: String, error: Option<String>) -> Json<WorkspaceResponse> {
Json(WorkspaceResponse {
path,
entries: Vec::new(),
error,
})
}
async fn workspace_list_resolve_dirs(
http: &AppStateHttpCore,
query_path: Option<&str>,
) -> Result<(std::path::PathBuf, std::path::PathBuf), Json<WorkspaceResponse>> {
let base_canonical = match effective_workspace_base_canonical(http).await {
Ok(p) => p,
Err(WorkspacePathError::WebEffectiveWorkspaceUnset) => {
return Err(workspace_list_empty(String::new(), None));
}
Err(e) => {
log::warn!(
target: "crabmate",
"workspace list base error kind={} msg={}",
e.kind(),
e
);
return Err(workspace_list_empty(String::new(), Some(e.user_message())));
}
};
match resolve_web_workspace_read_path(&base_canonical, query_path) {
Ok(canonical) => Ok((base_canonical, canonical)),
Err(e) => Err(workspace_list_empty(
base_canonical.display().to_string(),
Some(e.user_message()),
)),
}
}
#[cfg(unix)]
async fn workspace_list_unix(
base_canonical: std::path::PathBuf,
canonical: std::path::PathBuf,
path_str: String,
) -> Json<WorkspaceResponse> {
let base = base_canonical;
let can = canonical;
let path_for_resp = path_str.clone();
match tokio::task::spawn_blocking(move || {
workspace_list_entries_sync(base, can).map(|entries| (path_for_resp, entries))
})
.await
{
Ok(Ok((p, entries))) => Json(WorkspaceResponse {
path: p,
entries,
error: None,
}),
Ok(Err(msg)) => {
log::warn!("{}", msg);
workspace_list_empty(path_str, Some(msg))
}
Err(e) => {
log::warn!("workspace list join error: {}", e);
workspace_list_empty(path_str, Some("列出工作区失败".to_string()))
}
}
}
#[cfg(not(unix))]
async fn workspace_list_non_unix(
canonical: std::path::PathBuf,
path_str: String,
) -> Json<WorkspaceResponse> {
let mut entries = Vec::new();
let mut read_dir = match tokio::fs::read_dir(&canonical).await {
Ok(d) => d,
Err(e) => {
let msg = format!("无法读取工作目录: {}", e);
log::warn!("{}", msg);
return workspace_list_empty(path_str, Some(msg));
}
};
loop {
let entry = match read_dir.next_entry().await {
Ok(Some(e)) => e,
Ok(None) => break,
Err(e) => {
let msg = format!("读取目录项失败: {}", e);
log::warn!("{}", msg);
break;
}
};
let name = entry.file_name().to_string_lossy().to_string();
let is_dir = entry.metadata().await.map(|m| m.is_dir()).unwrap_or(false);
entries.push(WorkspaceEntry { name, is_dir });
}
entries.sort_by_cached_key(|e| (!e.is_dir, e.name.to_lowercase()));
Json(WorkspaceResponse {
path: path_str,
entries,
error: None,
})
}
pub async fn workspace_handler(
State(http): State<AppStateHttpCore>,
Query(query): Query<WorkspaceQuery>,
) -> Json<WorkspaceResponse> {
let (base_canonical, canonical) =
match workspace_list_resolve_dirs(&http, query.path.as_deref()).await {
Ok(v) => v,
Err(resp) => return resp,
};
let path_str = canonical.display().to_string();
#[cfg(unix)]
{
workspace_list_unix(base_canonical, canonical, path_str).await
}
#[cfg(not(unix))]
{
let _ = base_canonical;
workspace_list_non_unix(canonical, path_str).await
}
}
fn workspace_search_rel_path(
base_canonical: &std::path::Path,
path: Option<&str>,
) -> Result<Option<String>, String> {
let Some(raw) = path else {
return Ok(None);
};
if raw.trim().is_empty() {
return Ok(None);
}
let canonical =
resolve_web_workspace_read_path(base_canonical, Some(raw)).map_err(|e| e.user_message())?;
Ok(canonical
.strip_prefix(base_canonical)
.ok()
.map(|r| r.to_string_lossy().to_string()))
}
fn build_workspace_search_args(
pattern: &str,
rel_path: Option<String>,
body: &WorkspaceSearchBody,
) -> String {
let mut args = serde_json::json!({ "pattern": pattern });
if let Some(p) = rel_path {
args["path"] = serde_json::Value::String(p);
}
if let Some(m) = clamp_workspace_search_max_results(body.max_results) {
args["max_results"] = serde_json::json!(m);
}
if let Some(ci) = body.case_insensitive {
args["case_insensitive"] = serde_json::json!(ci);
}
if let Some(ih) = body.ignore_hidden {
args["ignore_hidden"] = serde_json::json!(ih);
}
args.to_string()
}
pub async fn workspace_search_handler(
State(http): State<AppStateHttpCore>,
Json(body): Json<WorkspaceSearchBody>,
) -> Json<WorkspaceSearchResponse> {
let pattern = match workspace_search_pattern_or_error(&body) {
Ok(p) => p,
Err(e) => {
return Json(WorkspaceSearchResponse {
output: String::new(),
error: Some(e),
});
}
};
let base_canonical = match effective_workspace_base_canonical(&http).await {
Ok(p) => p,
Err(e) => {
return Json(WorkspaceSearchResponse {
output: String::new(),
error: Some(e.user_message()),
});
}
};
let rel_path = match workspace_search_rel_path(&base_canonical, body.path.as_deref()) {
Ok(p) => p,
Err(e) => {
return Json(WorkspaceSearchResponse {
output: String::new(),
error: Some(e),
});
}
};
let args_json = build_workspace_search_args(pattern, rel_path, &body);
let cfg_snap = {
let g = http.cfg.read().await;
g.clone()
};
let cfg_arc = Arc::new(cfg_snap);
let work_dir = base_canonical.clone();
let output = match tokio::task::spawn_blocking(move || {
let ctx = crate::tools::tool_context_for(
cfg_arc.as_ref(),
cfg_arc.command_exec.allowed_commands.as_ref(),
&work_dir,
);
crate::tools::run_tool("search_in_files", &args_json, &ctx)
})
.await
{
Ok(output) => output,
Err(e) => {
error!(
target: "crabmate",
"workspace_search 阻塞任务异常 error={}",
e
);
return Json(WorkspaceSearchResponse {
output: String::new(),
error: Some("搜索执行失败,请稍后重试".to_string()),
});
}
};
Json(WorkspaceSearchResponse {
output,
error: None,
})
}
fn workspace_file_read_err(msg: String) -> Json<WorkspaceFileReadResponse> {
Json(WorkspaceFileReadResponse {
content: String::new(),
error: Some(msg),
})
}
#[cfg(unix)]
async fn workspace_file_read_unix(
base_canonical: std::path::PathBuf,
canonical: std::path::PathBuf,
enc_name: crate::text_encoding::TextEncodingName,
) -> Json<WorkspaceFileReadResponse> {
let base = base_canonical;
let can = canonical;
let max_b = WORKSPACE_FILE_READ_MAX_BYTES;
match tokio::task::spawn_blocking(move || {
workspace_read_file_sync_unix(base, can, enc_name, max_b)
})
.await
{
Ok(Ok(content)) => Json(WorkspaceFileReadResponse {
content,
error: None,
}),
Ok(Err(msg)) => workspace_file_read_err(msg),
Err(e) => workspace_file_read_err(format!("读取文件任务失败: {}", e)),
}
}
#[cfg(not(unix))]
async fn workspace_file_read_non_unix(
canonical: std::path::PathBuf,
enc_name: crate::text_encoding::TextEncodingName,
) -> Json<WorkspaceFileReadResponse> {
let meta = match tokio::fs::metadata(&canonical).await {
Ok(m) => m,
Err(e) => {
return workspace_file_read_err(format!("无法读取文件信息: {}", e));
}
};
if meta.is_dir() {
return workspace_file_read_err("路径是目录,无法读取为文件".to_string());
}
if meta.len() > WORKSPACE_FILE_READ_MAX_BYTES {
return workspace_file_read_err(format!(
"文件过大({} 字节),当前最多读取 {} 字节",
meta.len(),
WORKSPACE_FILE_READ_MAX_BYTES
));
}
let raw = match tokio::fs::read(&canonical).await {
Ok(b) => b,
Err(e) => {
return workspace_file_read_err(format!("读取文件失败: {}", e));
}
};
match decode_bytes_strict(&raw, enc_name) {
Ok((content, _)) => Json(WorkspaceFileReadResponse {
content,
error: None,
}),
Err(msg) => workspace_file_read_err(msg),
}
}
pub async fn workspace_file_read_handler(
State(http): State<AppStateHttpCore>,
Query(query): Query<WorkspaceFileQuery>,
) -> Json<WorkspaceFileReadResponse> {
let (base_canonical, canonical, enc_name) =
match workspace_file_read_resolve(&http, &query).await {
Ok(x) => x,
Err(e) => return e,
};
#[cfg(unix)]
{
workspace_file_read_unix(base_canonical, canonical, enc_name).await
}
#[cfg(not(unix))]
{
let _ = base_canonical;
workspace_file_read_non_unix(canonical, enc_name).await
}
}
fn workspace_file_delete_err(msg: String) -> Json<WorkspaceFileDeleteResponse> {
Json(WorkspaceFileDeleteResponse { error: Some(msg) })
}
async fn workspace_file_delete_resolve(
http: &AppStateHttpCore,
query: &WorkspaceFileQuery,
) -> Result<(std::path::PathBuf, std::path::PathBuf), Json<WorkspaceFileDeleteResponse>> {
let base_canonical = match effective_workspace_base_canonical(http).await {
Ok(p) => p,
Err(e) => return Err(workspace_file_delete_err(e.user_message())),
};
if let Err(e) = validate_workspace_query_encoding_optional(query.encoding.as_deref()) {
return Err(workspace_file_delete_err(e));
}
if query.path.trim().is_empty() {
return Err(workspace_file_delete_err("path 不能为空".to_string()));
}
match resolve_web_workspace_read_path(&base_canonical, Some(query.path.as_str())) {
Ok(canonical) => Ok((base_canonical, canonical)),
Err(e) => Err(workspace_file_delete_err(e.user_message())),
}
}
#[cfg(unix)]
async fn workspace_file_delete_unix(
base_canonical: std::path::PathBuf,
canonical: std::path::PathBuf,
) -> Json<WorkspaceFileDeleteResponse> {
match tokio::task::spawn_blocking(move || {
workspace_delete_file_sync_unix(base_canonical, canonical)
})
.await
{
Ok(Ok(())) => Json(WorkspaceFileDeleteResponse { error: None }),
Ok(Err(msg)) => workspace_file_delete_err(msg),
Err(e) => workspace_file_delete_err(format!("删除文件任务失败: {}", e)),
}
}
#[cfg(not(unix))]
async fn workspace_file_delete_non_unix(
canonical: std::path::PathBuf,
) -> Json<WorkspaceFileDeleteResponse> {
let meta = match tokio::fs::metadata(&canonical).await {
Ok(m) => m,
Err(e) => {
return workspace_file_delete_err(format!("无法读取文件信息: {}", e));
}
};
if meta.is_dir() {
return workspace_file_delete_err("不支持删除目录".to_string());
}
match tokio::fs::remove_file(&canonical).await {
Ok(()) => Json(WorkspaceFileDeleteResponse { error: None }),
Err(e) => workspace_file_delete_err(format!("删除文件失败: {}", e)),
}
}
pub async fn workspace_file_delete_handler(
State(http): State<AppStateHttpCore>,
Query(query): Query<WorkspaceFileQuery>,
) -> Json<WorkspaceFileDeleteResponse> {
let (base_canonical, canonical) = match workspace_file_delete_resolve(&http, &query).await {
Ok(v) => v,
Err(e) => return e,
};
#[cfg(unix)]
{
workspace_file_delete_unix(base_canonical, canonical).await
}
#[cfg(not(unix))]
{
let _ = base_canonical;
workspace_file_delete_non_unix(canonical).await
}
}
async fn workspace_dir_create_response(
base_canonical: std::path::PathBuf,
canonical: std::path::PathBuf,
parents: bool,
) -> Json<WorkspaceFileWriteResponse> {
match tokio::task::spawn_blocking(move || {
workspace_dir_create_sync(base_canonical, canonical, parents)
})
.await
{
Ok(Ok(())) => Json(WorkspaceFileWriteResponse { error: None }),
Ok(Err(msg)) => Json(WorkspaceFileWriteResponse { error: Some(msg) }),
Err(e) => Json(WorkspaceFileWriteResponse {
error: Some(format!("创建目录任务失败: {}", e)),
}),
}
}
fn workspace_file_write_err(msg: String) -> Json<WorkspaceFileWriteResponse> {
Json(WorkspaceFileWriteResponse { error: Some(msg) })
}
#[cfg(not(unix))]
async fn workspace_file_write_non_unix(
canonical: std::path::PathBuf,
body: WorkspaceFileWriteBody,
) -> Json<WorkspaceFileWriteResponse> {
let exists = tokio::fs::try_exists(&canonical).await.unwrap_or(false);
if body.create_only && exists {
return workspace_file_write_err("文件已存在,无法仅创建".to_string());
}
if body.update_only && !exists {
return workspace_file_write_err("文件不存在,无法仅修改".to_string());
}
if let Some(parent) = canonical.parent()
&& !parent.as_os_str().is_empty()
&& let Err(e) = tokio::fs::create_dir_all(parent).await
{
return workspace_file_write_err(format!("创建目录失败: {}", e));
}
match tokio::fs::write(&canonical, body.content.as_bytes()).await {
Ok(()) => Json(WorkspaceFileWriteResponse { error: None }),
Err(e) => workspace_file_write_err(format!("写入文件失败: {}", e)),
}
}
async fn workspace_file_write_resolved(
base_canonical: std::path::PathBuf,
canonical: std::path::PathBuf,
body: WorkspaceFileWriteBody,
) -> Json<WorkspaceFileWriteResponse> {
if body.create_directory {
return workspace_dir_create_response(base_canonical, canonical, body.parents).await;
}
#[cfg(unix)]
{
let base = base_canonical;
let normalized = canonical;
let content = body.content;
let create_only = body.create_only;
let update_only = body.update_only;
match tokio::task::spawn_blocking(move || {
workspace_file_write_sync_unix(base, normalized, content, create_only, update_only)
})
.await
{
Ok(Ok(())) => Json(WorkspaceFileWriteResponse { error: None }),
Ok(Err(msg)) => workspace_file_write_err(msg),
Err(e) => workspace_file_write_err(format!("写入文件任务失败: {}", e)),
}
}
#[cfg(not(unix))]
{
let _ = base_canonical;
workspace_file_write_non_unix(canonical, body).await
}
}
pub async fn workspace_file_write_handler(
State(http): State<AppStateHttpCore>,
Json(body): Json<WorkspaceFileWriteBody>,
) -> Json<WorkspaceFileWriteResponse> {
let base_canonical = match effective_workspace_base_canonical(&http).await {
Ok(p) => p,
Err(e) => {
return Json(WorkspaceFileWriteResponse {
error: Some(e.user_message()),
});
}
};
if let Err(e) = validate_workspace_file_write_request(&body) {
return Json(WorkspaceFileWriteResponse { error: Some(e) });
}
let path = body.path.trim();
if path.is_empty() {
return Json(WorkspaceFileWriteResponse {
error: Some("path 不能为空".to_string()),
});
}
let canonical = match resolve_web_workspace_write_path(&base_canonical, path) {
Ok(p) => p,
Err(e) => {
return Json(WorkspaceFileWriteResponse {
error: Some(e.user_message()),
});
}
};
workspace_file_write_resolved(base_canonical, canonical, body).await
}
pub async fn workspace_dir_create_handler(
State(http): State<AppStateHttpCore>,
Json(body): Json<WorkspaceDirCreateBody>,
) -> Json<WorkspaceDirCreateResponse> {
if body.delete {
let WorkspaceDirDeleteResponse { error } =
workspace_dir_delete_resolved(&http, body.path.as_str(), body.confirm, body.recursive)
.await;
return Json(WorkspaceDirCreateResponse { error });
}
let base_canonical = match effective_workspace_base_canonical(&http).await {
Ok(p) => p,
Err(e) => {
return Json(WorkspaceDirCreateResponse {
error: Some(e.user_message()),
});
}
};
let path = body.path.trim();
if path.is_empty() {
return Json(WorkspaceDirCreateResponse {
error: Some("path 不能为空".to_string()),
});
}
let canonical = match resolve_web_workspace_write_path(&base_canonical, path) {
Ok(p) => p,
Err(e) => {
return Json(WorkspaceDirCreateResponse {
error: Some(e.user_message()),
});
}
};
let parents = body.parents;
match tokio::task::spawn_blocking(move || {
workspace_dir_create_sync(base_canonical, canonical, parents)
})
.await
{
Ok(Ok(())) => Json(WorkspaceDirCreateResponse { error: None }),
Ok(Err(msg)) => Json(WorkspaceDirCreateResponse { error: Some(msg) }),
Err(e) => Json(WorkspaceDirCreateResponse {
error: Some(format!("创建目录任务失败: {}", e)),
}),
}
}
pub async fn workspace_dir_delete_handler(
State(http): State<AppStateHttpCore>,
Query(query): Query<WorkspaceDirDeleteQuery>,
) -> Json<WorkspaceDirDeleteResponse> {
Json(
workspace_dir_delete_resolved(&http, query.path.as_str(), query.confirm, query.recursive)
.await,
)
}
async fn workspace_dir_delete_resolved(
http: &AppStateHttpCore,
path: &str,
confirm: bool,
recursive: bool,
) -> WorkspaceDirDeleteResponse {
if !confirm {
return WorkspaceDirDeleteResponse {
error: Some("拒绝执行:需要 confirm=true".to_string()),
};
}
let base_canonical = match effective_workspace_base_canonical(http).await {
Ok(p) => p,
Err(e) => {
return WorkspaceDirDeleteResponse {
error: Some(e.user_message()),
};
}
};
let path = path.trim();
if path.is_empty() {
return WorkspaceDirDeleteResponse {
error: Some("path 不能为空".to_string()),
};
}
let canonical = match resolve_web_workspace_read_path(&base_canonical, Some(path)) {
Ok(p) => p,
Err(e) => {
return WorkspaceDirDeleteResponse {
error: Some(e.user_message()),
};
}
};
if canonical == base_canonical {
return WorkspaceDirDeleteResponse {
error: Some("不能删除工作区根目录".to_string()),
};
}
match tokio::task::spawn_blocking(move || workspace_dir_delete_sync(canonical, recursive)).await
{
Ok(Ok(())) => WorkspaceDirDeleteResponse { error: None },
Ok(Err(msg)) => WorkspaceDirDeleteResponse { error: Some(msg) },
Err(e) => WorkspaceDirDeleteResponse {
error: Some(format!("删除目录任务失败: {}", e)),
},
}
}
fn workspace_dir_delete_sync(canonical: std::path::PathBuf, recursive: bool) -> Result<(), String> {
let meta = std::fs::metadata(&canonical).map_err(|e| format!("无法读取目录信息: {e}"))?;
if !meta.is_dir() {
return Err("路径不是目录".to_string());
}
let result = if recursive {
std::fs::remove_dir_all(&canonical)
} else {
std::fs::remove_dir(&canonical)
};
match result {
Ok(()) => Ok(()),
Err(e) if !recursive && e.kind() == std::io::ErrorKind::DirectoryNotEmpty => {
Err("目录非空,需要 recursive=true".to_string())
}
Err(e) => Err(format!("删除目录失败: {e}")),
}
}
pub async fn workspace_profile_handler(
State(http): State<AppStateHttpCore>,
) -> Json<WorkspaceProfileResponse> {
let base_canonical = match effective_workspace_base_canonical(&http).await {
Ok(p) => p,
Err(e) => {
return Json(WorkspaceProfileResponse {
markdown: String::new(),
error: Some(e.user_message()),
});
}
};
let max_chars = http
.cfg
.read()
.await
.context_bootstrap_inject
.project_profile_inject_max_chars;
let md_result = tokio::task::spawn_blocking(move || {
crate::context_bootstrap::project_profile::build_project_profile_markdown(
&base_canonical,
max_chars,
)
})
.await;
match md_result {
Ok(markdown) => Json(WorkspaceProfileResponse {
markdown,
error: None,
}),
Err(e) => Json(WorkspaceProfileResponse {
markdown: String::new(),
error: Some(format!("生成项目画像任务失败: {}", e)),
}),
}
}