Skip to content

Commit acec750

Browse files
committed
fix(app-server): 改造 agents 接口异步文件读写
1 parent 7954d02 commit acec750

1 file changed

Lines changed: 112 additions & 41 deletions

File tree

src/cortex-app-server/src/api/agents.rs

Lines changed: 112 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
//! Custom agents API endpoints.
22
3+
use std::io::ErrorKind;
4+
use std::path::Path as StdPath;
35
use std::sync::Arc;
46

57
use axum::{
68
Json,
79
extract::{Path, State},
810
};
11+
use tokio::fs;
912

1013
use crate::error::{AppError, AppResult};
1114
use crate::state::AppState;
@@ -16,12 +19,12 @@ use super::types::{
1619
};
1720

1821
/// Read agent file from disk.
19-
fn read_agent_file(path: &std::path::Path, scope: &str) -> Option<AgentDefinition> {
22+
async fn read_agent_file(path: &StdPath, scope: &str) -> Option<AgentDefinition> {
2023
if path.extension().and_then(|e| e.to_str()) != Some("md") {
2124
return None;
2225
}
2326

24-
let content = std::fs::read_to_string(path).ok()?;
27+
let content = fs::read_to_string(path).await.ok()?;
2528
let name = path.file_stem()?.to_str()?.to_string();
2629

2730
// Parse YAML frontmatter
@@ -70,34 +73,34 @@ fn read_agent_file(path: &std::path::Path, scope: &str) -> Option<AgentDefinitio
7073
})
7174
}
7275

76+
async fn list_agents_in_dir(dir: &StdPath, scope: &str) -> Vec<AgentDefinition> {
77+
let mut agents = Vec::new();
78+
let mut entries = match fs::read_dir(dir).await {
79+
Ok(entries) => entries,
80+
Err(_) => return agents,
81+
};
82+
83+
while let Ok(Some(entry)) = entries.next_entry().await {
84+
if let Some(agent) = read_agent_file(&entry.path(), scope).await {
85+
agents.push(agent);
86+
}
87+
}
88+
89+
agents
90+
}
91+
7392
/// List all agents.
7493
pub async fn list_agents() -> AppResult<Json<Vec<AgentDefinition>>> {
7594
let mut agents = Vec::new();
7695

7796
// Project agents (.factory/agents/)
78-
let project_dir = std::path::Path::new(".factory/agents");
79-
if project_dir.exists()
80-
&& let Ok(entries) = std::fs::read_dir(project_dir)
81-
{
82-
for entry in entries.flatten() {
83-
if let Some(agent) = read_agent_file(&entry.path(), "project") {
84-
agents.push(agent);
85-
}
86-
}
87-
}
97+
let project_dir = StdPath::new(".factory/agents");
98+
agents.extend(list_agents_in_dir(project_dir, "project").await);
8899

89100
// User agents (~/.factory/agents/)
90101
if let Some(home) = dirs::home_dir() {
91102
let user_dir = home.join(".factory/agents");
92-
if user_dir.exists()
93-
&& let Ok(entries) = std::fs::read_dir(&user_dir)
94-
{
95-
for entry in entries.flatten() {
96-
if let Some(agent) = read_agent_file(&entry.path(), "user") {
97-
agents.push(agent);
98-
}
99-
}
100-
}
103+
agents.extend(list_agents_in_dir(&user_dir, "user").await);
101104
}
102105

103106
Ok(Json(agents))
@@ -106,15 +109,15 @@ pub async fn list_agents() -> AppResult<Json<Vec<AgentDefinition>>> {
106109
/// Get a specific agent.
107110
pub async fn get_agent(Path(name): Path<String>) -> AppResult<Json<AgentDefinition>> {
108111
// Check project first
109-
let project_path = std::path::Path::new(".factory/agents").join(format!("{}.md", name));
110-
if let Some(agent) = read_agent_file(&project_path, "project") {
112+
let project_path = StdPath::new(".factory/agents").join(format!("{}.md", name));
113+
if let Some(agent) = read_agent_file(&project_path, "project").await {
111114
return Ok(Json(agent));
112115
}
113116

114117
// Check user
115118
if let Some(home) = dirs::home_dir() {
116119
let user_path = home.join(".factory/agents").join(format!("{}.md", name));
117-
if let Some(agent) = read_agent_file(&user_path, "user") {
120+
if let Some(agent) = read_agent_file(&user_path, "user").await {
118121
return Ok(Json(agent));
119122
}
120123
}
@@ -132,7 +135,8 @@ pub async fn create_agent(Json(req): Json<CreateAgentRequest>) -> AppResult<Json
132135
.join(".factory/agents")
133136
};
134137

135-
std::fs::create_dir_all(&dir)
138+
fs::create_dir_all(&dir)
139+
.await
136140
.map_err(|e| AppError::Internal(format!("Failed to create directory: {}", e)))?;
137141

138142
let path = dir.join(format!("{}.md", req.name));
@@ -153,7 +157,8 @@ pub async fn create_agent(Json(req): Json<CreateAgentRequest>) -> AppResult<Json
153157
req.prompt
154158
);
155159

156-
std::fs::write(&path, &content)
160+
fs::write(&path, &content)
161+
.await
157162
.map_err(|e| AppError::Internal(format!("Failed to write agent file: {}", e)))?;
158163

159164
Ok(Json(AgentDefinition {
@@ -170,20 +175,24 @@ pub async fn create_agent(Json(req): Json<CreateAgentRequest>) -> AppResult<Json
170175
/// Delete an agent.
171176
pub async fn delete_agent(Path(name): Path<String>) -> AppResult<Json<serde_json::Value>> {
172177
// Try project first
173-
let project_path = std::path::Path::new(".factory/agents").join(format!("{}.md", name));
174-
if project_path.exists() {
175-
std::fs::remove_file(&project_path)
176-
.map_err(|e| AppError::Internal(format!("Failed to delete: {}", e)))?;
177-
return Ok(Json(serde_json::json!({"deleted": true})));
178+
let project_path = StdPath::new(".factory/agents").join(format!("{}.md", name));
179+
match fs::remove_file(&project_path).await {
180+
Ok(()) => return Ok(Json(serde_json::json!({"deleted": true}))),
181+
Err(err) if err.kind() != ErrorKind::NotFound => {
182+
return Err(AppError::Internal(format!("Failed to delete: {}", err)));
183+
}
184+
Err(_) => {}
178185
}
179186

180187
// Try user
181188
if let Some(home) = dirs::home_dir() {
182189
let user_path = home.join(".factory/agents").join(format!("{}.md", name));
183-
if user_path.exists() {
184-
std::fs::remove_file(&user_path)
185-
.map_err(|e| AppError::Internal(format!("Failed to delete: {}", e)))?;
186-
return Ok(Json(serde_json::json!({"deleted": true})));
190+
match fs::remove_file(&user_path).await {
191+
Ok(()) => return Ok(Json(serde_json::json!({"deleted": true}))),
192+
Err(err) if err.kind() != ErrorKind::NotFound => {
193+
return Err(AppError::Internal(format!("Failed to delete: {}", err)));
194+
}
195+
Err(_) => {}
187196
}
188197
}
189198

@@ -229,14 +238,14 @@ pub async fn update_agent(
229238
Json(req): Json<UpdateAgentRequest>,
230239
) -> AppResult<Json<AgentDefinition>> {
231240
// Find existing agent
232-
let project_path = std::path::Path::new(".factory/agents").join(format!("{}.md", name));
241+
let project_path = StdPath::new(".factory/agents").join(format!("{}.md", name));
233242
let user_path =
234243
dirs::home_dir().map(|h| h.join(".factory/agents").join(format!("{}.md", name)));
235244

236-
let (existing, path) = if let Some(agent) = read_agent_file(&project_path, "project") {
245+
let (existing, path) = if let Some(agent) = read_agent_file(&project_path, "project").await {
237246
(agent, project_path)
238247
} else if let Some(ref user_path) = user_path {
239-
if let Some(agent) = read_agent_file(user_path, "user") {
248+
if let Some(agent) = read_agent_file(user_path, "user").await {
240249
(agent, user_path.clone())
241250
} else {
242251
return Err(AppError::NotFound(format!("Agent not found: {}", name)));
@@ -273,7 +282,8 @@ pub async fn update_agent(
273282
updated.prompt
274283
);
275284

276-
std::fs::write(&path, &content)
285+
fs::write(&path, &content)
286+
.await
277287
.map_err(|e| AppError::Internal(format!("Failed to update agent: {}", e)))?;
278288

279289
Ok(Json(updated))
@@ -360,13 +370,15 @@ pub async fn import_agent(Json(req): Json<ImportAgentRequest>) -> AppResult<Json
360370
.join(".factory/agents")
361371
};
362372

363-
std::fs::create_dir_all(&dir)
373+
fs::create_dir_all(&dir)
374+
.await
364375
.map_err(|e| AppError::Internal(format!("Failed to create directory: {}", e)))?;
365376

366377
let path = dir.join(format!("{}.md", name));
367378

368379
// Write the file
369-
std::fs::write(&path, &req.content)
380+
fs::write(&path, &req.content)
381+
.await
370382
.map_err(|e| AppError::Internal(format!("Failed to write agent file: {}", e)))?;
371383

372384
Ok(Json(AgentDefinition {
@@ -409,3 +421,62 @@ pub async fn generate_agent_prompt(
409421
permission_mode: "default".to_string(),
410422
}))
411423
}
424+
425+
#[cfg(test)]
426+
mod tests {
427+
use super::*;
428+
use std::path::PathBuf;
429+
use std::time::{SystemTime, UNIX_EPOCH};
430+
431+
fn temp_test_dir(name: &str) -> PathBuf {
432+
let unique = SystemTime::now()
433+
.duration_since(UNIX_EPOCH)
434+
.unwrap()
435+
.as_nanos();
436+
std::env::temp_dir().join(format!("cortex-app-server-{name}-{unique}"))
437+
}
438+
439+
#[tokio::test]
440+
async fn read_agent_file_parses_frontmatter_async() {
441+
let dir = temp_test_dir("frontmatter");
442+
fs::create_dir_all(&dir).await.unwrap();
443+
let path = dir.join("reviewer.md");
444+
let content = r#"---
445+
description: Reviews code
446+
tools: ["Read", "Grep"]
447+
model: claude
448+
permissionMode: default
449+
---
450+
451+
Review this patch.
452+
"#;
453+
454+
fs::write(&path, content).await.unwrap();
455+
456+
let agent = read_agent_file(&path, "project").await.unwrap();
457+
assert_eq!(agent.name, "reviewer");
458+
assert_eq!(agent.description, "Reviews code");
459+
assert_eq!(agent.tools, vec!["Read", "Grep"]);
460+
assert_eq!(agent.model, "claude");
461+
assert_eq!(agent.prompt, "Review this patch.");
462+
assert_eq!(agent.scope, "project");
463+
464+
let _ = fs::remove_dir_all(&dir).await;
465+
}
466+
467+
#[tokio::test]
468+
async fn list_agents_in_dir_ignores_non_markdown_files() {
469+
let dir = temp_test_dir("list");
470+
fs::create_dir_all(&dir).await.unwrap();
471+
fs::write(dir.join("first.md"), "Prompt one").await.unwrap();
472+
fs::write(dir.join("notes.txt"), "ignore me").await.unwrap();
473+
474+
let agents = list_agents_in_dir(&dir, "user").await;
475+
476+
assert_eq!(agents.len(), 1);
477+
assert_eq!(agents[0].name, "first");
478+
assert_eq!(agents[0].scope, "user");
479+
480+
let _ = fs::remove_dir_all(&dir).await;
481+
}
482+
}

0 commit comments

Comments
 (0)