初始化web服务功能
This commit is contained in:
3
src-tauri/src/web/handlers/acp.rs
Normal file
3
src-tauri/src/web/handlers/acp.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
// ACP (Agent Communication Protocol) web handlers.
|
||||
// TODO: Implement ACP handlers for web mode.
|
||||
// These require special handling for connection lifecycle and streaming events.
|
||||
264
src-tauri/src/web/handlers/conversations.rs
Normal file
264
src-tauri/src/web/handlers/conversations.rs
Normal file
@@ -0,0 +1,264 @@
|
||||
use axum::{extract::Extension, Json};
|
||||
use serde::Deserialize;
|
||||
use tauri::Manager;
|
||||
|
||||
use crate::app_error::AppCommandError;
|
||||
use crate::db::service::{conversation_service, folder_service, import_service};
|
||||
use crate::db::AppDatabase;
|
||||
use crate::models::*;
|
||||
use crate::parsers::claude::ClaudeParser;
|
||||
use crate::parsers::codex::CodexParser;
|
||||
use crate::parsers::gemini::GeminiParser;
|
||||
use crate::parsers::openclaw::OpenClawParser;
|
||||
use crate::parsers::opencode::OpenCodeParser;
|
||||
use crate::parsers::AgentParser;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ListFolderConversationsParams {
|
||||
pub folder_id: i32,
|
||||
pub agent_type: Option<AgentType>,
|
||||
pub search: Option<String>,
|
||||
pub sort_by: Option<String>,
|
||||
pub status: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn list_folder_conversations(
|
||||
Extension(app): Extension<tauri::AppHandle>,
|
||||
Json(params): Json<ListFolderConversationsParams>,
|
||||
) -> Result<Json<Vec<DbConversationSummary>>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
let result = conversation_service::list_by_folder(
|
||||
&db.conn,
|
||||
params.folder_id,
|
||||
params.agent_type,
|
||||
params.search,
|
||||
params.sort_by,
|
||||
params.status,
|
||||
)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ListConversationsParams {
|
||||
pub agent_type: Option<AgentType>,
|
||||
pub search: Option<String>,
|
||||
pub sort_by: Option<String>,
|
||||
pub folder_path: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn list_conversations(
|
||||
Json(params): Json<ListConversationsParams>,
|
||||
) -> Result<Json<Vec<ConversationSummary>>, AppCommandError> {
|
||||
let result = crate::commands::conversations::list_conversations_for_web(
|
||||
params.agent_type,
|
||||
params.search,
|
||||
params.sort_by,
|
||||
params.folder_path,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetConversationParams {
|
||||
pub agent_type: AgentType,
|
||||
pub conversation_id: String,
|
||||
}
|
||||
|
||||
pub async fn get_conversation(
|
||||
Json(params): Json<GetConversationParams>,
|
||||
) -> Result<Json<ConversationDetail>, AppCommandError> {
|
||||
let at = params.agent_type;
|
||||
let cid = params.conversation_id;
|
||||
let result = tokio::task::spawn_blocking(move || -> Result<ConversationDetail, AppCommandError> {
|
||||
let parser: Box<dyn AgentParser> = match at {
|
||||
AgentType::ClaudeCode => Box::new(ClaudeParser::new()),
|
||||
AgentType::Codex => Box::new(CodexParser::new()),
|
||||
AgentType::OpenCode => Box::new(OpenCodeParser::new()),
|
||||
AgentType::Gemini => Box::new(GeminiParser::new()),
|
||||
AgentType::OpenClaw => Box::new(OpenClawParser::new()),
|
||||
};
|
||||
parser
|
||||
.get_conversation(&cid)
|
||||
.map_err(|e| AppCommandError::not_found("Conversation not found").with_detail(e.to_string()))
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
AppCommandError::task_execution_failed("Failed to load conversation")
|
||||
.with_detail(e.to_string())
|
||||
})??;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetFolderConversationParams {
|
||||
pub conversation_id: i32,
|
||||
}
|
||||
|
||||
pub async fn get_folder_conversation(
|
||||
Extension(app): Extension<tauri::AppHandle>,
|
||||
Json(params): Json<GetFolderConversationParams>,
|
||||
) -> Result<Json<DbConversationDetail>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
let summary = conversation_service::get_by_id(&db.conn, params.conversation_id)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
|
||||
let (turns, session_stats, _resolved_ext_id) = if let Some(ref ext_id) = summary.external_id {
|
||||
let at = summary.agent_type;
|
||||
let eid = ext_id.clone();
|
||||
tokio::task::spawn_blocking(move || -> Result<_, AppCommandError> {
|
||||
let parser: Box<dyn AgentParser> = match at {
|
||||
AgentType::ClaudeCode => Box::new(ClaudeParser::new()),
|
||||
AgentType::Codex => Box::new(CodexParser::new()),
|
||||
AgentType::OpenCode => Box::new(OpenCodeParser::new()),
|
||||
AgentType::Gemini => Box::new(GeminiParser::new()),
|
||||
AgentType::OpenClaw => Box::new(OpenClawParser::new()),
|
||||
};
|
||||
match parser.get_conversation(&eid) {
|
||||
Ok(d) => Ok((d.turns, d.session_stats, None::<String>)),
|
||||
Err(_) => Ok((vec![], None, None)),
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
AppCommandError::task_execution_failed("Failed to read conversation turns")
|
||||
.with_detail(e.to_string())
|
||||
})??
|
||||
} else {
|
||||
(vec![], None, None)
|
||||
};
|
||||
|
||||
let mut summary = summary;
|
||||
summary.message_count = turns.len() as u32;
|
||||
|
||||
Ok(Json(DbConversationDetail {
|
||||
summary,
|
||||
turns,
|
||||
session_stats,
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn list_folders() -> Result<Json<Vec<FolderInfo>>, AppCommandError> {
|
||||
let result = crate::commands::conversations::list_folders_for_web().await?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
pub async fn get_stats() -> Result<Json<AgentStats>, AppCommandError> {
|
||||
let result = crate::commands::conversations::get_stats_for_web().await?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
pub async fn get_sidebar_data() -> Result<Json<SidebarData>, AppCommandError> {
|
||||
let result = crate::commands::conversations::get_sidebar_data_for_web().await?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ImportLocalConversationsParams {
|
||||
pub folder_id: i32,
|
||||
}
|
||||
|
||||
pub async fn import_local_conversations(
|
||||
Extension(app): Extension<tauri::AppHandle>,
|
||||
Json(params): Json<ImportLocalConversationsParams>,
|
||||
) -> Result<Json<ImportResult>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
let folder = folder_service::get_folder_by_id(&db.conn, params.folder_id)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?
|
||||
.ok_or_else(|| AppCommandError::not_found("Folder not found"))?;
|
||||
let result = import_service::import_local_conversations(&db.conn, params.folder_id, &folder.path)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateConversationParams {
|
||||
pub folder_id: i32,
|
||||
pub agent_type: AgentType,
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn create_conversation(
|
||||
Extension(app): Extension<tauri::AppHandle>,
|
||||
Json(params): Json<CreateConversationParams>,
|
||||
) -> Result<Json<i32>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
let model = conversation_service::create(
|
||||
&db.conn,
|
||||
params.folder_id,
|
||||
params.agent_type,
|
||||
params.title,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
Ok(Json(model.id))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateConversationStatusParams {
|
||||
pub conversation_id: i32,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
pub async fn update_conversation_status(
|
||||
Extension(app): Extension<tauri::AppHandle>,
|
||||
Json(params): Json<UpdateConversationStatusParams>,
|
||||
) -> Result<Json<()>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
let status_enum: crate::db::entities::conversation::ConversationStatus =
|
||||
serde_json::from_value(serde_json::Value::String(params.status)).map_err(|e| {
|
||||
AppCommandError::invalid_input("Invalid conversation status").with_detail(e.to_string())
|
||||
})?;
|
||||
conversation_service::update_status(&db.conn, params.conversation_id, status_enum)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateConversationTitleParams {
|
||||
pub conversation_id: i32,
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
pub async fn update_conversation_title(
|
||||
Extension(app): Extension<tauri::AppHandle>,
|
||||
Json(params): Json<UpdateConversationTitleParams>,
|
||||
) -> Result<Json<()>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
conversation_service::update_title(&db.conn, params.conversation_id, params.title)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeleteConversationParams {
|
||||
pub conversation_id: i32,
|
||||
}
|
||||
|
||||
pub async fn delete_conversation(
|
||||
Extension(app): Extension<tauri::AppHandle>,
|
||||
Json(params): Json<DeleteConversationParams>,
|
||||
) -> Result<Json<()>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
conversation_service::soft_delete(&db.conn, params.conversation_id)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
Ok(Json(()))
|
||||
}
|
||||
29
src-tauri/src/web/handlers/error.rs
Normal file
29
src-tauri/src/web/handlers/error.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
|
||||
use crate::app_error::{AppCommandError, AppErrorCode};
|
||||
|
||||
impl IntoResponse for AppCommandError {
|
||||
fn into_response(self) -> Response {
|
||||
let status = match self.code {
|
||||
AppErrorCode::InvalidInput => StatusCode::BAD_REQUEST,
|
||||
AppErrorCode::NotFound => StatusCode::NOT_FOUND,
|
||||
AppErrorCode::AlreadyExists => StatusCode::CONFLICT,
|
||||
AppErrorCode::PermissionDenied => StatusCode::FORBIDDEN,
|
||||
AppErrorCode::AuthenticationFailed => StatusCode::UNAUTHORIZED,
|
||||
AppErrorCode::ConfigurationMissing
|
||||
| AppErrorCode::ConfigurationInvalid
|
||||
| AppErrorCode::DependencyMissing => StatusCode::UNPROCESSABLE_ENTITY,
|
||||
AppErrorCode::NetworkError
|
||||
| AppErrorCode::DatabaseError
|
||||
| AppErrorCode::IoError
|
||||
| AppErrorCode::ExternalCommandFailed
|
||||
| AppErrorCode::WindowOperationFailed
|
||||
| AppErrorCode::TaskExecutionFailed => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
(status, Json(self)).into_response()
|
||||
}
|
||||
}
|
||||
2
src-tauri/src/web/handlers/folder_commands.rs
Normal file
2
src-tauri/src/web/handlers/folder_commands.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
// Folder commands web handlers.
|
||||
// TODO: Implement folder command CRUD handlers for web mode.
|
||||
58
src-tauri/src/web/handlers/folders.rs
Normal file
58
src-tauri/src/web/handlers/folders.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use axum::{extract::Extension, Json};
|
||||
use serde::Deserialize;
|
||||
use tauri::Manager;
|
||||
|
||||
use crate::app_error::AppCommandError;
|
||||
use crate::db::service::folder_service;
|
||||
use crate::db::AppDatabase;
|
||||
use crate::models::*;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FolderIdParams {
|
||||
pub folder_id: i32,
|
||||
}
|
||||
|
||||
pub async fn load_folder_history(
|
||||
Extension(app): Extension<tauri::AppHandle>,
|
||||
) -> Result<Json<Vec<FolderHistoryEntry>>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
let result = folder_service::list_folders(&db.conn)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
pub async fn get_folder(
|
||||
Extension(app): Extension<tauri::AppHandle>,
|
||||
Json(params): Json<FolderIdParams>,
|
||||
) -> Result<Json<FolderDetail>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
let folder = folder_service::get_folder_by_id(&db.conn, params.folder_id)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?
|
||||
.ok_or_else(|| AppCommandError::not_found("Folder not found"))?;
|
||||
Ok(Json(folder))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddFolderParams {
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
/// Web equivalent of `open_folder_window`: adds the folder to DB and returns its ID.
|
||||
/// The web client then navigates to `/folder?id=N` itself.
|
||||
pub async fn open_folder_window(
|
||||
Extension(app): Extension<tauri::AppHandle>,
|
||||
Json(params): Json<AddFolderParams>,
|
||||
) -> Result<Json<FolderHistoryEntry>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
let entry = folder_service::add_folder(&db.conn, ¶ms.path)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
Ok(Json(entry))
|
||||
}
|
||||
|
||||
// TODO: Add remaining folder handlers (git operations, file operations, etc.)
|
||||
// These will be added incrementally as needed.
|
||||
2
src-tauri/src/web/handlers/mcp.rs
Normal file
2
src-tauri/src/web/handlers/mcp.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
// MCP (Model Context Protocol) web handlers.
|
||||
// TODO: Implement MCP marketplace and server management handlers for web mode.
|
||||
9
src-tauri/src/web/handlers/mod.rs
Normal file
9
src-tauri/src/web/handlers/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
mod error;
|
||||
pub mod conversations;
|
||||
pub mod folders;
|
||||
pub mod acp;
|
||||
pub mod terminal;
|
||||
pub mod system_settings;
|
||||
pub mod version_control;
|
||||
pub mod folder_commands;
|
||||
pub mod mcp;
|
||||
38
src-tauri/src/web/handlers/system_settings.rs
Normal file
38
src-tauri/src/web/handlers/system_settings.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use axum::{extract::Extension, Json};
|
||||
use tauri::Manager;
|
||||
|
||||
use crate::app_error::AppCommandError;
|
||||
use crate::db::service::app_metadata_service;
|
||||
use crate::db::AppDatabase;
|
||||
use crate::models::*;
|
||||
|
||||
const SYSTEM_PROXY_SETTINGS_KEY: &str = "system_proxy_settings";
|
||||
const SYSTEM_LANGUAGE_SETTINGS_KEY: &str = "system_language_settings";
|
||||
|
||||
pub async fn get_system_proxy_settings(
|
||||
Extension(app): Extension<tauri::AppHandle>,
|
||||
) -> Result<Json<SystemProxySettings>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
let raw = app_metadata_service::get_value(&db.conn, SYSTEM_PROXY_SETTINGS_KEY)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
|
||||
let settings = raw
|
||||
.and_then(|v| serde_json::from_str::<SystemProxySettings>(&v).ok())
|
||||
.unwrap_or_default();
|
||||
Ok(Json(settings))
|
||||
}
|
||||
|
||||
pub async fn get_system_language_settings(
|
||||
Extension(app): Extension<tauri::AppHandle>,
|
||||
) -> Result<Json<SystemLanguageSettings>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
let raw = app_metadata_service::get_value(&db.conn, SYSTEM_LANGUAGE_SETTINGS_KEY)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
|
||||
let settings = raw
|
||||
.and_then(|v| serde_json::from_str::<SystemLanguageSettings>(&v).ok())
|
||||
.unwrap_or_default();
|
||||
Ok(Json(settings))
|
||||
}
|
||||
3
src-tauri/src/web/handlers/terminal.rs
Normal file
3
src-tauri/src/web/handlers/terminal.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
// Terminal web handlers.
|
||||
// TODO: Implement terminal handlers for web mode.
|
||||
// Terminal I/O streams over WebSocket instead of Tauri events.
|
||||
2
src-tauri/src/web/handlers/version_control.rs
Normal file
2
src-tauri/src/web/handlers/version_control.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
// Version control web handlers.
|
||||
// TODO: Implement git settings and GitHub account handlers for web mode.
|
||||
Reference in New Issue
Block a user