From 02d6cf4e947a7b6c26af5054c5e398992872b664 Mon Sep 17 00:00:00 2001 From: blankll Date: Sat, 20 Jun 2026 21:41:44 +0800 Subject: [PATCH 1/9] fix: agent self-healing, tool timeout, JDBC bridge hang, UI polish Backend (Rust): - lib.rs: manage AppState directly, not Arc (Tauri TypeId mismatch) - capabilities/sql.rs: 35 adapter arms for JdbcBridge/ClickHouse/HttpSql/Rqlite/Turso - capabilities/sql.rs: get_schema capped at 30 tables to prevent N+1 timeout on Oracle - capabilities/sql.rs: tool descriptions improved (list_tables for existence, get_schema for DDL) - jdbc_bridge/adapter.rs: spawn_blocking for pipe reads so tokio::time::timeout works - deps: data-studio-agent upgraded to v0.1.2 (self-healing, tool timeout, message ordering) Frontend (Vue/TS): - agent-message-bubble.vue: tool errors auto-expand, retry text shown inline - chat-panel.vue: removed floating status bar, cleaned up props - agentRuntime.ts: tool retry event handler with inline status - agentRuntime.ts: context-usage tracking wired - useChatAgent.ts: system prompt hardened with tool usage rules - agentApi.ts: onAgentLoopToolRetry event - dataStudioStore.ts: session tracking fields (startTime, tokenUsage, activeTool) --- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/capabilities/sql.rs | 230 +++++++++++++++++- src-tauri/src/database/jdbc_bridge/adapter.rs | 9 +- src-tauri/src/lib.rs | 3 +- src/components/agent-message-bubble.vue | 15 +- src/components/chat-panel.vue | 15 -- src/composables/agentRuntime.ts | 31 +++ src/composables/useChatAgent.ts | 19 ++ src/datasources/agentApi.ts | 13 + src/store/dataStudioStore.ts | 29 +++ 11 files changed, 335 insertions(+), 33 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 7cd2b23d..a8fcf68b 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1483,7 +1483,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "data-studio-agent" version = "0.1.1" -source = "git+https://github.com/geek-fun/data-studio-agent.git?tag=v0.1.1#4e82283a77d3627dbce6ecc8aeb7832ec0ad3d72" +source = "git+https://github.com/geek-fun/data-studio-agent.git?tag=v0.1.2#bd34c5a061ea3162b2b8d351234c05ebceae57fa" dependencies = [ "async-openai", "async-trait", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 0c5f060c..edfcfe86 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -86,7 +86,7 @@ http = "1" log = "0.4" futures = "0.3" rand = "0.8" -data-studio-agent = { git = "https://github.com/geek-fun/data-studio-agent.git", tag = "v0.1.1" } +data-studio-agent = { git = "https://github.com/geek-fun/data-studio-agent.git", tag = "v0.1.2" } # Archive extraction (JRE downloads) flate2 = "1.0" diff --git a/src-tauri/src/capabilities/sql.rs b/src-tauri/src/capabilities/sql.rs index 3b1d5f43..c159a686 100644 --- a/src-tauri/src/capabilities/sql.rs +++ b/src-tauri/src/capabilities/sql.rs @@ -96,7 +96,36 @@ async fn execute_on_adapter(adapter: &ActiveConnection, sql: &str) -> Result todo!(), + ActiveConnection::ClickHouse(a) => a + .lock() + .await + .execute_query(sql) + .await + .map_err(|e| e.to_string()), + ActiveConnection::JdbcBridge(a) => a + .lock() + .await + .execute_query(sql) + .await + .map_err(|e| e.to_string()), + ActiveConnection::HttpSql(a) => a + .lock() + .await + .execute_query(sql) + .await + .map_err(|e| e.to_string()), + ActiveConnection::Rqlite(a) => a + .lock() + .await + .execute_query(sql) + .await + .map_err(|e| e.to_string()), + ActiveConnection::Turso(a) => a + .lock() + .await + .execute_query(sql) + .await + .map_err(|e| e.to_string()), } } @@ -202,7 +231,36 @@ impl CapabilityHandler for ListDatabasesHandler { .list_databases() .await .map_err(|e| e.to_string())?, - _ => todo!(), + ActiveConnection::ClickHouse(a) => a + .lock() + .await + .list_databases() + .await + .map_err(|e| e.to_string())?, + ActiveConnection::JdbcBridge(a) => a + .lock() + .await + .list_databases() + .await + .map_err(|e| e.to_string())?, + ActiveConnection::HttpSql(a) => a + .lock() + .await + .list_databases() + .await + .map_err(|e| e.to_string())?, + ActiveConnection::Rqlite(a) => a + .lock() + .await + .list_databases() + .await + .map_err(|e| e.to_string())?, + ActiveConnection::Turso(a) => a + .lock() + .await + .list_databases() + .await + .map_err(|e| e.to_string())?, }; serde_json::to_string(&dbs).map_err(|e| e.to_string()) } @@ -238,7 +296,36 @@ impl CapabilityHandler for ListSchemasHandler { .list_schemas(database) .await .map_err(|e| e.to_string())?, - _ => todo!(), + ActiveConnection::ClickHouse(a) => a + .lock() + .await + .list_schemas(database) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::JdbcBridge(a) => a + .lock() + .await + .list_schemas(database) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::HttpSql(a) => a + .lock() + .await + .list_schemas(database) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::Rqlite(a) => a + .lock() + .await + .list_schemas(database) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::Turso(a) => a + .lock() + .await + .list_schemas(database) + .await + .map_err(|e| e.to_string())?, }; serde_json::to_string(&schemas).map_err(|e| e.to_string()) } @@ -280,7 +367,36 @@ impl CapabilityHandler for ListTablesHandler { .list_tables(database, schema) .await .map_err(|e| e.to_string())?, - _ => todo!(), + ActiveConnection::ClickHouse(a) => a + .lock() + .await + .list_tables(database, schema) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::JdbcBridge(a) => a + .lock() + .await + .list_tables(database, schema) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::HttpSql(a) => a + .lock() + .await + .list_tables(database, schema) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::Rqlite(a) => a + .lock() + .await + .list_tables(database, schema) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::Turso(a) => a + .lock() + .await + .list_tables(database, schema) + .await + .map_err(|e| e.to_string())?, }; serde_json::to_string(&tables).map_err(|e| e.to_string()) } @@ -323,10 +439,48 @@ impl CapabilityHandler for GetSchemaHandler { .list_tables(database, schema) .await .map_err(|e| e.to_string())?, - _ => todo!(), + ActiveConnection::ClickHouse(a) => a + .lock() + .await + .list_tables(database, schema) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::JdbcBridge(a) => a + .lock() + .await + .list_tables(database, schema) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::HttpSql(a) => a + .lock() + .await + .list_tables(database, schema) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::Rqlite(a) => a + .lock() + .await + .list_tables(database, schema) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::Turso(a) => a + .lock() + .await + .list_tables(database, schema) + .await + .map_err(|e| e.to_string())?, }; + const MAX_SCHEMA_TABLES: usize = 30; + let tables: Vec<_> = tables.into_iter().take(MAX_SCHEMA_TABLES).collect(); + let mut schema_lines: Vec = Vec::new(); + if tables.len() >= MAX_SCHEMA_TABLES { + schema_lines.push(format!( + "-- Showing first {} tables. Specify a schema filter for complete results.\n", + MAX_SCHEMA_TABLES + )); + } for table in &tables { let cols = match &adapter { ActiveConnection::Postgres(a) => a @@ -353,7 +507,36 @@ impl CapabilityHandler for GetSchemaHandler { .list_columns(database, schema, &table.name) .await .map_err(|e| e.to_string())?, - _ => todo!(), + ActiveConnection::ClickHouse(a) => a + .lock() + .await + .list_columns(database, schema, &table.name) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::JdbcBridge(a) => a + .lock() + .await + .list_columns(database, schema, &table.name) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::HttpSql(a) => a + .lock() + .await + .list_columns(database, schema, &table.name) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::Rqlite(a) => a + .lock() + .await + .list_columns(database, schema, &table.name) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::Turso(a) => a + .lock() + .await + .list_columns(database, schema, &table.name) + .await + .map_err(|e| e.to_string())?, }; let schema_name = table.schema.as_deref().unwrap_or("public"); @@ -425,7 +608,36 @@ impl CapabilityHandler for DescribeTableHandler { .list_columns(database, schema, table) .await .map_err(|e| e.to_string())?, - _ => todo!(), + ActiveConnection::ClickHouse(a) => a + .lock() + .await + .list_columns(database, schema, table) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::JdbcBridge(a) => a + .lock() + .await + .list_columns(database, schema, table) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::HttpSql(a) => a + .lock() + .await + .list_columns(database, schema, table) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::Rqlite(a) => a + .lock() + .await + .list_columns(database, schema, table) + .await + .map_err(|e| e.to_string())?, + ActiveConnection::Turso(a) => a + .lock() + .await + .list_columns(database, schema, table) + .await + .map_err(|e| e.to_string())?, }; serde_json::to_string(&cols).map_err(|e| e.to_string()) } @@ -504,7 +716,7 @@ pub fn register_sql_tools(reg: &mut CapabilityRegistry) { reg.register(Capability { name: "sqlkit__list_tables", - description: "List all tables in a database schema.", + description: "List all tables in a database schema. Returns table names, types, and row counts — fast and lightweight. Use this to check if tables exist or browse available objects. For full column details, use sqlkit__describe_table or sqlkit__get_schema.", handler: Arc::new(ListTablesHandler), input_schema: json!({"type": "object", "properties": { "connection_id": connection_id_schema(), @@ -518,7 +730,7 @@ pub fn register_sql_tools(reg: &mut CapabilityRegistry) { reg.register(Capability { name: "sqlkit__get_schema", - description: "Get the full database schema (all tables and columns) as DDL-like text. Use this before writing queries to understand the structure.", + description: "Get the full database schema (all tables and all columns) as DDL-like text. SLOW on databases with many objects. Prefer sqlkit__list_tables for browsing and sqlkit__describe_table for single-table details.", handler: Arc::new(GetSchemaHandler), input_schema: json!({"type": "object", "properties": { "connection_id": connection_id_schema(), diff --git a/src-tauri/src/database/jdbc_bridge/adapter.rs b/src-tauri/src/database/jdbc_bridge/adapter.rs index 0dc7dd39..2af93039 100644 --- a/src-tauri/src/database/jdbc_bridge/adapter.rs +++ b/src-tauri/src/database/jdbc_bridge/adapter.rs @@ -74,8 +74,13 @@ impl JdbcBridgeAdapter { launcher: &Arc>, req: JdbcRequest, ) -> DbResult { - let mut guard = launcher.lock().await; - let resp = guard.send_request(&req)?; + let launcher = launcher.clone(); + let resp = tokio::task::spawn_blocking(move || { + let mut guard = launcher.blocking_lock(); + guard.send_request(&req) + }) + .await + .map_err(|e| DbError::Connection(format!("JDBC bridge task panicked: {}", e)))??; Ok(resp.result.unwrap_or(serde_json::Value::Null)) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 77770ca2..0f67eeb7 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -80,7 +80,6 @@ pub fn run() { use crate::connection::guardian::ConnectionGuardian; use state::AppState; - let app_state = Arc::new(AppState::new()); let store = commands::store::Store::new(); tauri::Builder::default() @@ -91,7 +90,7 @@ pub fn run() { .plugin(tauri_plugin_deep_link::init()) .plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_window_state::Builder::default().build()) - .manage(app_state.clone()) + .manage(AppState::new()) .manage(store.clone()) .setup(move |app| { let handle = app.handle().clone(); diff --git a/src/components/agent-message-bubble.vue b/src/components/agent-message-bubble.vue index 70cd8e03..706b1590 100644 --- a/src/components/agent-message-bubble.vue +++ b/src/components/agent-message-bubble.vue @@ -87,11 +87,13 @@ const activeToolName = computed(() => { ) }) -function resultStatus(tc: AgentToolCall): 'success' | 'error' | 'denied' { +function resultStatus(tc: AgentToolCall): 'success' | 'error' | 'denied' | 'executing' { if (tc.status === 'denied') return 'denied' if (tc.status === 'error') return 'error' + if (tc.status === 'executing') + return 'executing' return 'success' } @@ -360,7 +362,10 @@ function toolVerb(toolName: string, tc: AgentToolCall): string {