diff --git a/doc/todo.md b/doc/todo.md index 225800d..1b67972 100644 --- a/doc/todo.md +++ b/doc/todo.md @@ -2,8 +2,12 @@ context compaction rag / kb +内置一个向量数据库和kb管理能力吧,kb_search,管理界面简单点,cpu做embedding就够 + template +--- +时间观察app --- ## 代码啰嗦/可精简 diff --git a/src/agent.rs b/src/agent.rs index 7f6f843..a05d405 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -474,8 +474,13 @@ fn build_planning_prompt(project_id: &str) -> String { - 工作目录是独立的项目工作区,Python venv 已预先激活(.venv/)\n\ - 可用工具:bash、git、curl、uv\n\ - 静态文件访问:/api/projects/{0}/files/{{filename}}\n\ - - 后台服务访问:/api/projects/{0}/app/\n\ - - 如果要构建 Web 应用,推荐 FastAPI + 前端 HTML,API 请求用相对路径 /api/projects/{0}/app/...\n\ + - 后台服务访问:/api/projects/{0}/app/(反向代理,路径会被转发到应用的 /)\n\ + \n\ + 【重要】反向代理注意事项:\n\ + - 用户通过 /api/projects/{0}/app/ 访问应用,请求被代理到应用的 / 路径\n\ + - 因此前端 HTML 中的所有 API 请求必须使用【不带开头 / 的相对路径】\n\ + - 正确示例:fetch('todos') 或 fetch('./todos') 错误示例:fetch('/todos') 或 fetch('/api/todos')\n\ + - HTML 中的 标签不需要设置,只要不用绝对路径就行\n\ \n\ 请使用中文回复。", project_id, @@ -505,6 +510,7 @@ fn build_execution_prompt(project_id: &str) -> String { - 使用 `uv add <包名>` 或 `pip install <包名>` 安装依赖\n\ - 静态文件访问:/api/projects/{0}/files/{{filename}}\n\ - 后台服务访问:/api/projects/{0}/app/(启动命令需监听 0.0.0.0:$PORT)\n\ + - 【重要】应用通过反向代理访问,前端 HTML/JS 中的 fetch/XHR 请求必须使用相对路径(如 fetch('todos')),绝对不能用 / 开头的路径(如 fetch('/todos')),否则会 404\n\ \n\ 请使用中文回复。", project_id, diff --git a/src/api/projects.rs b/src/api/projects.rs index 1eaf9c4..69feb8b 100644 --- a/src/api/projects.rs +++ b/src/api/projects.rs @@ -40,7 +40,7 @@ pub fn router(state: Arc) -> Router { async fn list_projects( State(state): State>, ) -> ApiResult> { - sqlx::query_as::<_, Project>("SELECT * FROM projects ORDER BY updated_at DESC") + sqlx::query_as::<_, Project>("SELECT * FROM projects WHERE deleted = 0 ORDER BY updated_at DESC") .fetch_all(&state.db.pool) .await .map(Json) @@ -109,10 +109,30 @@ async fn delete_project( State(state): State>, Path(id): Path, ) -> ApiResult { - sqlx::query("DELETE FROM projects WHERE id = ?") + // Soft delete: mark as deleted in DB + let result = sqlx::query("UPDATE projects SET deleted = 1, updated_at = datetime('now') WHERE id = ? AND deleted = 0") .bind(&id) .execute(&state.db.pool) .await - .map(|r| Json(r.rows_affected() > 0)) - .map_err(db_err) + .map_err(db_err)?; + + if result.rows_affected() == 0 { + return Ok(Json(false)); + } + + // Move workspace to deleted folder + let src = std::path::PathBuf::from("/app/data/workspaces").join(&id); + let dst_dir = std::path::PathBuf::from("/app/data/deleted"); + if src.exists() { + if let Err(e) = tokio::fs::create_dir_all(&dst_dir).await { + tracing::error!("Failed to create deleted dir: {}", e); + } else { + let dst = dst_dir.join(&id); + if let Err(e) = tokio::fs::rename(&src, &dst).await { + tracing::error!("Failed to move workspace to deleted: {}", e); + } + } + } + + Ok(Json(true)) } diff --git a/src/db.rs b/src/db.rs index 31d0eed..94a7765 100644 --- a/src/db.rs +++ b/src/db.rs @@ -94,6 +94,13 @@ impl Database { .execute(&self.pool) .await; + // Migration: add deleted column to projects + let _ = sqlx::query( + "ALTER TABLE projects ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0" + ) + .execute(&self.pool) + .await; + sqlx::query( "CREATE TABLE IF NOT EXISTS timers ( id TEXT PRIMARY KEY, @@ -120,6 +127,8 @@ pub struct Project { pub description: String, pub created_at: String, pub updated_at: String, + #[serde(default)] + pub deleted: bool, } #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] diff --git a/web/src/components/AppLayout.vue b/web/src/components/AppLayout.vue index cdb084f..e1f9bd9 100644 --- a/web/src/components/AppLayout.vue +++ b/web/src/components/AppLayout.vue @@ -80,6 +80,23 @@ function onProjectUpdate(projectId: string, name: string) { const p = projects.value.find(p => p.id === projectId) if (p) p.name = name } + +async function onDeleteProject(id: string) { + try { + await api.deleteProject(id) + projects.value = projects.value.filter(p => p.id !== id) + if (selectedProjectId.value === id) { + selectedProjectId.value = projects.value[0]?.id ?? '' + if (selectedProjectId.value) { + history.replaceState(null, '', `/projects/${selectedProjectId.value}`) + } else { + history.replaceState(null, '', '/') + } + } + } catch (e: any) { + error.value = e.message + } +}