feat: add Google OAuth, remote worker system, and file browser

- Google OAuth login with JWT session cookies, per-user project isolation
- Remote worker registration via WebSocket, execute_on_worker/list_workers agent tools
- File browser UI in workflow view, file upload/download API
- Deploy script switched to local build, added tori.euphon.cloud ingress
This commit is contained in:
2026-03-17 01:57:57 +00:00
parent 186d882f35
commit 63f0582f54
26 changed files with 2338 additions and 106 deletions

View File

@@ -10,6 +10,7 @@ use crate::llm::{LlmClient, ChatMessage, Tool, ToolFunction};
use crate::exec::LocalExecutor;
use crate::template::{self, LoadedTemplate};
use crate::tools::ExternalToolManager;
use crate::worker::WorkerManager;
use crate::LlmConfig;
use crate::state::{AgentState, AgentPhase, Artifact, Step, StepStatus, StepResult, StepResultStatus, check_scratchpad_size};
@@ -80,6 +81,7 @@ pub struct AgentManager {
template_repo: Option<crate::TemplateRepoConfig>,
kb: Option<Arc<crate::kb::KbManager>>,
jwt_private_key_path: Option<String>,
pub worker_mgr: Arc<WorkerManager>,
}
impl AgentManager {
@@ -89,6 +91,7 @@ impl AgentManager {
template_repo: Option<crate::TemplateRepoConfig>,
kb: Option<Arc<crate::kb::KbManager>>,
jwt_private_key_path: Option<String>,
worker_mgr: Arc<WorkerManager>,
) -> Arc<Self> {
Arc::new(Self {
agents: RwLock::new(HashMap::new()),
@@ -100,6 +103,7 @@ impl AgentManager {
template_repo,
kb,
jwt_private_key_path,
worker_mgr,
})
}
@@ -755,6 +759,19 @@ fn build_step_tools() -> Vec<Tool> {
})),
tool_kb_search(),
tool_kb_read(),
make_tool("list_workers", "列出所有已注册的远程 worker 节点及其硬件/软件信息CPU、内存、GPU、OS、内核", serde_json::json!({
"type": "object",
"properties": {}
})),
make_tool("execute_on_worker", "在指定的远程 worker 上执行脚本。脚本以 bash 执行。可以通过 HTTP 访问项目文件GET/POST /api/obj/{project_id}/files/{path}", serde_json::json!({
"type": "object",
"properties": {
"worker": { "type": "string", "description": "Worker 名称(从 list_workers 获取)" },
"script": { "type": "string", "description": "要执行的 bash 脚本内容" },
"timeout": { "type": "integer", "description": "超时秒数(默认 300", "default": 300 }
},
"required": ["worker", "script"]
})),
]
}
@@ -1500,6 +1517,52 @@ async fn run_step_loop(
step_chat_history.push(ChatMessage::tool_result(&tc.id, &result));
}
"list_workers" => {
let _ = broadcast_tx.send(WsMessage::ActivityUpdate {
workflow_id: workflow_id.to_string(),
activity: format!("步骤 {} — 列出 Workers", step_order),
});
let workers = mgr.worker_mgr.list().await;
let result = if workers.is_empty() {
"没有已注册的 worker。".to_string()
} else {
let items: Vec<String> = workers.iter().map(|(name, info)| {
format!("- {} (cpu={}, mem={}, gpu={}, os={}, kernel={})",
name, info.cpu, info.memory, info.gpu, info.os, info.kernel)
}).collect();
format!("已注册的 workers:\n{}", items.join("\n"))
};
log_execution(pool, broadcast_tx, workflow_id, step_order, "list_workers", "", &result, "done").await;
step_chat_history.push(ChatMessage::tool_result(&tc.id, &result));
}
"execute_on_worker" => {
let worker_name = args.get("worker").and_then(|v| v.as_str()).unwrap_or("");
let script = args.get("script").and_then(|v| v.as_str()).unwrap_or("");
let timeout = args.get("timeout").and_then(|v| v.as_u64()).unwrap_or(300);
let _ = broadcast_tx.send(WsMessage::ActivityUpdate {
workflow_id: workflow_id.to_string(),
activity: format!("步骤 {} — 在 {} 上执行脚本", step_order, worker_name),
});
let result = match mgr.worker_mgr.execute(worker_name, script, timeout).await {
Ok(wr) => {
let mut out = String::new();
out.push_str(&format!("exit_code: {}\n", wr.exit_code));
if !wr.stdout.is_empty() {
out.push_str(&format!("stdout:\n{}\n", truncate_str(&wr.stdout, 8192)));
}
if !wr.stderr.is_empty() {
out.push_str(&format!("stderr:\n{}\n", truncate_str(&wr.stderr, 4096)));
}
out
}
Err(e) => format!("Error: {}", e),
};
let status = if result.starts_with("Error:") { "failed" } else { "done" };
log_execution(pool, broadcast_tx, workflow_id, step_order, "execute_on_worker", &tc.function.arguments, &result, status).await;
step_chat_history.push(ChatMessage::tool_result(&tc.id, &result));
}
// External tools
name if external_tools.as_ref().is_some_and(|e| e.has_tool(name)) => {
let _ = broadcast_tx.send(WsMessage::ActivityUpdate {