完善folder页面的web接口实现

This commit is contained in:
xintaofei
2026-03-25 15:27:43 +08:00
parent ac09d3db9e
commit 218055ab01
18 changed files with 569 additions and 37 deletions

View File

@@ -1,3 +1,141 @@
// ACP (Agent Communication Protocol) web handlers.
// TODO: Implement ACP handlers for web mode.
// These require special handling for connection lifecycle and streaming events.
use axum::{extract::Extension, Json};
use serde::Deserialize;
use tauri::Manager;
use crate::acp::manager::ConnectionManager;
use crate::acp::registry;
use crate::acp::types::{AcpAgentInfo, AcpAgentStatus};
use crate::app_error::AppCommandError;
use crate::commands::acp as acp_commands;
use crate::db::service::agent_setting_service;
use crate::db::AppDatabase;
use crate::models::agent::AgentType;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentTypeParams {
pub agent_type: AgentType,
}
pub async fn acp_get_agent_status(
Extension(app): Extension<tauri::AppHandle>,
Json(params): Json<AgentTypeParams>,
) -> Result<Json<AcpAgentStatus>, AppCommandError> {
let db = app.state::<crate::db::AppDatabase>();
let result = acp_commands::acp_get_agent_status(params.agent_type, db)
.await
.map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?;
Ok(Json(result))
}
pub async fn acp_list_agents(
Extension(app): Extension<tauri::AppHandle>,
) -> Result<Json<Vec<AcpAgentInfo>>, AppCommandError> {
let db = app.state::<crate::db::AppDatabase>();
let result = acp_commands::acp_list_agents(db)
.await
.map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?;
Ok(Json(result))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AcpConnectParams {
pub agent_type: AgentType,
pub working_dir: Option<String>,
pub session_id: Option<String>,
}
pub async fn acp_connect(
Extension(app): Extension<tauri::AppHandle>,
Json(params): Json<AcpConnectParams>,
) -> Result<Json<String>, AppCommandError> {
let db = app.state::<AppDatabase>();
let manager = app.state::<ConnectionManager>();
let meta = registry::get_agent_meta(params.agent_type);
let setting = agent_setting_service::get_by_agent_type(&db.conn, params.agent_type)
.await
.map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?;
let disabled = setting
.as_ref()
.map(|model| !model.enabled)
.unwrap_or(false);
if disabled {
return Err(AppCommandError::task_execution_failed(format!(
"{} is disabled in settings",
params.agent_type
)));
}
let local_config_json = acp_commands::load_agent_local_config_json(params.agent_type);
let mut runtime_env = acp_commands::build_runtime_env_from_setting(
params.agent_type,
setting.as_ref(),
local_config_json.as_deref(),
);
if params.agent_type == AgentType::OpenClaw && params.session_id.is_none() {
runtime_env.insert("OPENCLAW_RESET_SESSION".into(), "1".into());
}
if let registry::AgentDistribution::Npx { cmd, .. } = meta.distribution {
if !acp_commands::is_cmd_available(cmd) {
return Err(AppCommandError::task_execution_failed(format!(
"{} SDK is not installed. Please install it in Agent Settings.",
meta.name
)));
}
}
let connection_id = manager
.spawn_agent(
params.agent_type,
params.working_dir,
params.session_id,
runtime_env,
"web".to_string(),
app.clone(),
)
.await
.map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?;
Ok(Json(connection_id))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AcpDisconnectParams {
pub connection_id: String,
}
pub async fn acp_disconnect(
Extension(app): Extension<tauri::AppHandle>,
Json(params): Json<AcpDisconnectParams>,
) -> Result<Json<()>, AppCommandError> {
let manager = app.state::<ConnectionManager>();
manager
.disconnect(&params.connection_id)
.await
.map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?;
Ok(Json(()))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AcpPromptParams {
pub connection_id: String,
pub blocks: Vec<crate::acp::types::PromptInputBlock>,
}
pub async fn acp_prompt(
Extension(app): Extension<tauri::AppHandle>,
Json(params): Json<AcpPromptParams>,
) -> Result<Json<()>, AppCommandError> {
let manager = app.state::<ConnectionManager>();
manager
.send_prompt(&params.connection_id, params.blocks)
.await
.map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?;
Ok(Json(()))
}

View File

@@ -262,3 +262,21 @@ pub async fn delete_conversation(
.map_err(AppCommandError::from)?;
Ok(Json(()))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateConversationExternalIdParams {
pub conversation_id: i32,
pub external_id: String,
}
pub async fn update_conversation_external_id(
Extension(app): Extension<tauri::AppHandle>,
Json(params): Json<UpdateConversationExternalIdParams>,
) -> Result<Json<()>, AppCommandError> {
let db = app.state::<AppDatabase>();
conversation_service::update_external_id(&db.conn, params.conversation_id, params.external_id)
.await
.map_err(AppCommandError::from)?;
Ok(Json(()))
}

View File

@@ -1,2 +1,25 @@
// Folder commands web handlers.
// TODO: Implement folder command CRUD handlers for web mode.
use axum::{extract::Extension, Json};
use serde::Deserialize;
use tauri::Manager;
use crate::app_error::AppCommandError;
use crate::db::service::folder_command_service;
use crate::db::AppDatabase;
use crate::models::*;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FolderIdParams {
pub folder_id: i32,
}
pub async fn list_folder_commands(
Extension(app): Extension<tauri::AppHandle>,
Json(params): Json<FolderIdParams>,
) -> Result<Json<Vec<FolderCommandInfo>>, AppCommandError> {
let db = app.state::<AppDatabase>();
let result = folder_command_service::list_by_folder(&db.conn, params.folder_id)
.await
.map_err(AppCommandError::from)?;
Ok(Json(result))
}

View File

@@ -1,8 +1,9 @@
use axum::{extract::Extension, Json};
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use tauri::Manager;
use crate::app_error::AppCommandError;
use crate::commands::folders as folder_commands;
use crate::db::service::folder_service;
use crate::db::AppDatabase;
use crate::models::*;
@@ -54,5 +55,224 @@ pub async fn open_folder_window(
Ok(Json(entry))
}
// TODO: Add remaining folder handlers (git operations, file operations, etc.)
// These will be added incrementally as needed.
// --- New handlers below ---
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SaveFolderOpenedConversationsParams {
pub folder_id: i32,
pub items: Vec<OpenedConversation>,
}
pub async fn save_folder_opened_conversations(
Extension(app): Extension<tauri::AppHandle>,
Json(params): Json<SaveFolderOpenedConversationsParams>,
) -> Result<Json<()>, AppCommandError> {
let db = app.state::<AppDatabase>();
folder_service::save_opened_conversations(&db.conn, params.folder_id, params.items)
.await
.map_err(AppCommandError::from)?;
Ok(Json(()))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PathParams {
pub path: String,
}
pub async fn get_git_branch(
Json(params): Json<PathParams>,
) -> Result<Json<Option<String>>, AppCommandError> {
let result = folder_commands::get_git_branch(params.path).await?;
Ok(Json(result))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetFileTreeParams {
pub path: String,
pub max_depth: Option<usize>,
}
pub async fn get_file_tree(
Json(params): Json<GetFileTreeParams>,
) -> Result<Json<Vec<folder_commands::FileTreeNode>>, AppCommandError> {
let result = folder_commands::get_file_tree(params.path, params.max_depth).await?;
Ok(Json(result))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RootPathParams {
pub root_path: String,
}
pub async fn start_file_tree_watch(
Extension(app): Extension<tauri::AppHandle>,
Json(params): Json<RootPathParams>,
) -> Result<Json<()>, AppCommandError> {
folder_commands::start_file_tree_watch(app, params.root_path).await?;
Ok(Json(()))
}
pub async fn stop_file_tree_watch(
Json(params): Json<RootPathParams>,
) -> Result<Json<()>, AppCommandError> {
folder_commands::stop_file_tree_watch(params.root_path).await?;
Ok(Json(()))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OpenSettingsWindowParams {
pub section: Option<String>,
pub agent_type: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SettingsNavigationResult {
pub path: String,
}
/// Web equivalent of `open_settings_window`: returns the target navigation path.
/// The web client handles the actual navigation.
pub async fn open_settings_window(
Json(params): Json<OpenSettingsWindowParams>,
) -> Result<Json<SettingsNavigationResult>, AppCommandError> {
let route = match params.section.as_deref() {
Some("appearance") => "settings/appearance",
Some("agents") => "settings/agents",
Some("mcp") => "settings/mcp",
Some("skills") => "settings/skills",
Some("shortcuts") => "settings/shortcuts",
Some("system") => "settings/system",
_ => "settings/system",
};
let path = if route == "settings/agents" {
if let Some(ref agent) = params.agent_type {
let trimmed = agent.trim();
if !trimmed.is_empty()
&& trimmed
.chars()
.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '_')
{
format!("/{route}?agent={trimmed}")
} else {
format!("/{route}")
}
} else {
format!("/{route}")
}
} else {
format!("/{route}")
};
Ok(Json(SettingsNavigationResult { path }))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GitStatusParams {
pub path: String,
pub show_all_untracked: Option<bool>,
}
pub async fn git_status(
Json(params): Json<GitStatusParams>,
) -> Result<Json<Vec<folder_commands::GitStatusEntry>>, AppCommandError> {
let result =
folder_commands::git_status(params.path, params.show_all_untracked).await?;
Ok(Json(result))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReadFilePreviewParams {
pub root_path: String,
pub path: String,
}
pub async fn read_file_preview(
Json(params): Json<ReadFilePreviewParams>,
) -> Result<Json<folder_commands::FilePreviewContent>, AppCommandError> {
let result =
folder_commands::read_file_preview(params.root_path, params.path).await?;
Ok(Json(result))
}
pub async fn git_list_all_branches(
Json(params): Json<PathParams>,
) -> Result<Json<folder_commands::GitBranchList>, AppCommandError> {
let result = folder_commands::git_list_all_branches(params.path).await?;
Ok(Json(result))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GitCommitBranchesParams {
pub path: String,
pub commit: String,
}
pub async fn git_commit_branches(
Json(params): Json<GitCommitBranchesParams>,
) -> Result<Json<Vec<String>>, AppCommandError> {
let result =
folder_commands::git_commit_branches(params.path, params.commit).await?;
Ok(Json(result))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GitShowFileParams {
pub path: String,
pub file: String,
pub ref_name: Option<String>,
}
pub async fn git_show_file(
Json(params): Json<GitShowFileParams>,
) -> Result<Json<String>, AppCommandError> {
let result =
folder_commands::git_show_file(params.path, params.file, params.ref_name).await?;
Ok(Json(result))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GitDiffParams {
pub path: String,
pub file: Option<String>,
}
pub async fn git_diff(
Json(params): Json<GitDiffParams>,
) -> Result<Json<String>, AppCommandError> {
let result = folder_commands::git_diff(params.path, params.file).await?;
Ok(Json(result))
}
pub async fn git_list_remotes(
Json(params): Json<PathParams>,
) -> Result<Json<Vec<folder_commands::GitRemote>>, AppCommandError> {
let result = folder_commands::git_list_remotes(params.path).await?;
Ok(Json(result))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OpenCommitWindowParams {
pub folder_id: i32,
}
/// Web equivalent of `open_commit_window`: returns the navigation path.
pub async fn open_commit_window(
Json(params): Json<OpenCommitWindowParams>,
) -> Result<Json<SettingsNavigationResult>, AppCommandError> {
Ok(Json(SettingsNavigationResult {
path: format!("/commit?folderId={}", params.folder_id),
}))
}

View File

@@ -1,3 +1,45 @@
// Terminal web handlers.
// TODO: Implement terminal handlers for web mode.
// Terminal I/O streams over WebSocket instead of Tauri events.
use axum::{extract::Extension, Json};
use serde::Deserialize;
use tauri::Manager;
use crate::app_error::AppCommandError;
use crate::commands::terminal::prepare_credential_env;
use crate::terminal::manager::{SpawnOptions, TerminalManager};
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TerminalSpawnParams {
pub working_dir: String,
pub initial_command: Option<String>,
}
pub async fn terminal_spawn(
Extension(app): Extension<tauri::AppHandle>,
Json(params): Json<TerminalSpawnParams>,
) -> Result<Json<String>, AppCommandError> {
let manager = app.state::<TerminalManager>();
let terminal_id = uuid::Uuid::new_v4().to_string();
let app_data_dir = app
.path()
.app_data_dir()
.map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?;
let extra_env = prepare_credential_env(&app_data_dir);
let id = manager
.spawn_with_id(
SpawnOptions {
terminal_id,
working_dir: params.working_dir,
owner_window_label: "web".to_string(),
initial_command: params.initial_command,
extra_env,
temp_files: vec![],
},
app.clone(),
)
.map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?;
Ok(Json(id))
}

View File

@@ -1,2 +1,22 @@
// Version control web handlers.
// TODO: Implement git settings and GitHub account handlers for web mode.
use axum::Json;
use serde::Deserialize;
use crate::app_error::AppCommandError;
use crate::commands::folders as folder_commands;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GitLogParams {
pub path: String,
pub limit: Option<u32>,
pub branch: Option<String>,
pub remote: Option<String>,
}
pub async fn git_log(
Json(params): Json<GitLogParams>,
) -> Result<Json<folder_commands::GitLogResult>, AppCommandError> {
let result =
folder_commands::git_log(params.path, params.limit, params.branch, params.remote).await?;
Ok(Json(result))
}

View File

@@ -35,6 +35,7 @@ pub fn build_router(app: tauri::AppHandle, token: String, static_dir: std::path:
.route("/update_conversation_status", post(handlers::conversations::update_conversation_status))
.route("/update_conversation_title", post(handlers::conversations::update_conversation_title))
.route("/delete_conversation", post(handlers::conversations::delete_conversation))
.route("/update_conversation_external_id", post(handlers::conversations::update_conversation_external_id))
// Folders
.route("/load_folder_history", post(handlers::folders::load_folder_history))
.route("/get_folder", post(handlers::folders::get_folder))
@@ -42,6 +43,35 @@ pub fn build_router(app: tauri::AppHandle, token: String, static_dir: std::path:
// System settings
.route("/get_system_proxy_settings", post(handlers::system_settings::get_system_proxy_settings))
.route("/get_system_language_settings", post(handlers::system_settings::get_system_language_settings))
// Folders (extended)
.route("/save_folder_opened_conversations", post(handlers::folders::save_folder_opened_conversations))
.route("/get_git_branch", post(handlers::folders::get_git_branch))
.route("/get_file_tree", post(handlers::folders::get_file_tree))
.route("/start_file_tree_watch", post(handlers::folders::start_file_tree_watch))
.route("/stop_file_tree_watch", post(handlers::folders::stop_file_tree_watch))
.route("/open_settings_window", post(handlers::folders::open_settings_window))
// Version control
.route("/git_log", post(handlers::version_control::git_log))
// Folder commands
.route("/list_folder_commands", post(handlers::folder_commands::list_folder_commands))
// Git operations
.route("/git_status", post(handlers::folders::git_status))
.route("/git_list_all_branches", post(handlers::folders::git_list_all_branches))
.route("/git_commit_branches", post(handlers::folders::git_commit_branches))
.route("/git_show_file", post(handlers::folders::git_show_file))
.route("/git_diff", post(handlers::folders::git_diff))
.route("/git_list_remotes", post(handlers::folders::git_list_remotes))
.route("/open_commit_window", post(handlers::folders::open_commit_window))
// File operations
.route("/read_file_preview", post(handlers::folders::read_file_preview))
// ACP
.route("/acp_get_agent_status", post(handlers::acp::acp_get_agent_status))
.route("/acp_list_agents", post(handlers::acp::acp_list_agents))
.route("/acp_connect", post(handlers::acp::acp_connect))
.route("/acp_disconnect", post(handlers::acp::acp_disconnect))
.route("/acp_prompt", post(handlers::acp::acp_prompt))
// Terminal
.route("/terminal_spawn", post(handlers::terminal::terminal_spawn))
// Catch-all: return proper JSON 404 for unimplemented API endpoints
.fallback(api_not_found)
// Auth middleware for API routes