diff --git a/Cargo.toml b/Cargo.toml index 7e5f9a3..1e09fa6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,18 +4,18 @@ version = "1.3.1" edition = "2024" [dependencies] -reqwest = { version = "0.13.3", default-features = false, features = ["json", "rustls", "multipart"] } +reqwest = { version = "0.13.4", default-features = false, features = ["json", "rustls", "multipart"] } serde = { version = "1.0", features = ["derive"] } -regex = "1.12" +regex = "1.13" serde_json = "1.0" async-trait = "0.1" -thiserror = "2.0.18" -tokio = { version = "1.52", features = ["full"] } +thiserror = "2.0.19" +tokio = { version = "1.53", features = ["full"] } tokio-util = { version = "0.7", features = ["rt", "io"] } -dashmap = "6.1" -uuid = { version = "1.23", features = ["v4"] } +dashmap = "6.2" +uuid = { version = "1.24", features = ["v4"] } axum = { version = "0.8", features = ["ws", "multipart"] } -tokio-tungstenite = { version = "0.29.0", features = ["rustls-tls-webpki-roots", "rustls"] } +tokio-tungstenite = { version = "0.30.0", features = ["rustls-tls-webpki-roots", "rustls"] } futures-util = "0.3" clap = { version = "4", features = ["derive"] } dirs = "6.0.0" @@ -26,21 +26,21 @@ tracing-appender = "0.2" time = { version = "0.3", features = ["formatting", "local-offset"] } anyhow = "1.0" mime_guess = "2.0" -base64 = "0.22" -sha2 = "0.10" +base64 = "0.23" +sha2 = "0.11" tempfile = "3" -cron = "0.16" +cron = "0.17" chrono-tz = "0.10" ratatui = "0.30" crossterm = { version = "0.29", features = ["event-stream"] } -termimad = "0.34" +termimad = "0.35" textwrap = "0.16" unicode-width = "0.2" chrono = "0.4" -sqlx = { version = "0.8", features = ["sqlite", "macros", "chrono", "runtime-tokio"] } -jieba-rs = "0.9" +sqlx = { version = "0.9", features = ["sqlite", "macros", "chrono", "runtime-tokio"] } +jieba-rs = "0.10" which = "8" -rmcp = { version = "1.7", default-features = false, features = [ +rmcp = { version = "2.2", default-features = false, features = [ "client", "transport-child-process", "transport-streamable-http-client-reqwest", diff --git a/src/mcp/mod.rs b/src/mcp/mod.rs index cfd5630..a699b16 100644 --- a/src/mcp/mod.rs +++ b/src/mcp/mod.rs @@ -5,7 +5,7 @@ use std::sync::{Arc, Mutex}; use anyhow::Context; use http::{HeaderName, HeaderValue}; -use rmcp::model::{CallToolRequestParams, RawContent}; +use rmcp::model::{CallToolRequestParams, ContentBlock}; use rmcp::transport::streamable_http_client::StreamableHttpClientTransportConfig; use rmcp::transport::{StreamableHttpClientTransport, TokioChildProcess}; use rmcp::{Peer, RoleClient, ServiceExt}; @@ -85,14 +85,14 @@ impl McpConnection { fn extract_text(result: &rmcp::model::CallToolResult) -> String { let mut parts = Vec::new(); for content in &result.content { - match &**content { - RawContent::Text(text) => { + match content { + ContentBlock::Text(text) => { parts.push(text.text.clone()); } - RawContent::Image(image) => { + ContentBlock::Image(image) => { parts.push(format!("[image: {}]", image.mime_type,)); } - RawContent::Resource(resource) => match &resource.resource { + ContentBlock::Resource(resource) => match &resource.resource { rmcp::model::ResourceContents::TextResourceContents { text, .. } => { parts.push(format!( "[resource text: {}]", @@ -102,6 +102,7 @@ fn extract_text(result: &rmcp::model::CallToolResult) -> String { rmcp::model::ResourceContents::BlobResourceContents { uri, .. } => { parts.push(format!("[resource blob: {}]", uri)); } + _ => parts.push("[unsupported resource]".to_string()), }, _ => { parts.push("[unsupported content]".to_string()); diff --git a/src/storage/memory.rs b/src/storage/memory.rs index 5e2db2d..50566f0 100644 --- a/src/storage/memory.rs +++ b/src/storage/memory.rs @@ -103,8 +103,9 @@ impl super::Storage { let fts_query = jieba() .cut(query, true) .into_iter() - .filter(|w| w.len() > 1 || w.bytes().any(|b| b > 127)) - .map(|w| format!("\"{}\"", w.replace('"', ""))) + .map(|token| token.word) + .filter(|word| word.len() > 1 || word.bytes().any(|b| b > 127)) + .map(|word| format!("\"{}\"", word.replace('"', ""))) .collect::>() .join(" OR "); @@ -138,8 +139,9 @@ impl super::Storage { let terms: Vec = jieba() .cut(query, true) .into_iter() - .filter(|w| w.len() > 1 || w.bytes().any(|b| b > 127)) - .map(|w| w.replace(['%', '_'], "")) + .map(|token| token.word) + .filter(|word| word.len() > 1 || word.bytes().any(|b| b > 127)) + .map(|word| word.replace(['%', '_'], "")) .collect(); if !terms.is_empty() { @@ -163,7 +165,8 @@ impl super::Storage { like_clauses ); - let mut query_builder = sqlx::query(&sql); + // The only interpolated fragment is a generated sequence of bind placeholders. + let mut query_builder = sqlx::query(sqlx::AssertSqlSafe(sql)); for term in &terms { let pattern = format!("%{}%", term); query_builder = query_builder.bind(pattern.clone()).bind(pattern); @@ -205,8 +208,9 @@ impl super::Storage { let terms: Vec = jieba() .cut(q, true) .into_iter() - .filter(|w| w.len() > 1 || w.bytes().any(|b| b > 127)) - .map(|w| w.replace(['%', '_'], "")) + .map(|token| token.word) + .filter(|word| word.len() > 1 || word.bytes().any(|b| b > 127)) + .map(|word| word.replace(['%', '_'], "")) .collect(); if terms.is_empty() { @@ -234,7 +238,8 @@ impl super::Storage { like_clauses ); - let mut query_builder = sqlx::query(&sql); + // The only interpolated fragment is a generated sequence of bind placeholders. + let mut query_builder = sqlx::query(sqlx::AssertSqlSafe(sql)); for term in &terms { let pattern = format!("%{}%", term); query_builder = query_builder.bind(pattern.clone()).bind(pattern); diff --git a/src/storage/mod.rs b/src/storage/mod.rs index c0331bb..6520855 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -29,7 +29,7 @@ const INSERT_MESSAGE_SQL: &str = r#" fn insert_message_query<'a>( session_id: &'a str, msg: &'a crate::storage::message::MessageMeta, -) -> sqlx::query::Query<'a, Sqlite, sqlx::sqlite::SqliteArguments<'a>> { +) -> sqlx::query::Query<'a, Sqlite, sqlx::sqlite::SqliteArguments> { sqlx::query(INSERT_MESSAGE_SQL) .bind(&msg.id) .bind(session_id) @@ -424,13 +424,19 @@ impl Storage { ("job_runs", "delivery_error", "delivery_error TEXT"), ] { let pragma = format!("PRAGMA table_info({table})"); - let columns = sqlx::query(&pragma).fetch_all(&mut *tx).await?; + // `table` comes exclusively from the fixed migration list above. + let columns = sqlx::query(sqlx::AssertSqlSafe(pragma)) + .fetch_all(&mut *tx) + .await?; if !columns .iter() .any(|row| row.get::("name") == column) { let alter = format!("ALTER TABLE {table} ADD COLUMN {definition}"); - sqlx::query(&alter).execute(&mut *tx).await?; + // All identifiers and definitions come from the fixed migration list above. + sqlx::query(sqlx::AssertSqlSafe(alter)) + .execute(&mut *tx) + .await?; } } @@ -461,9 +467,11 @@ impl Storage { ) .execute(&mut *tx) .await?; - sqlx::query(&format!("PRAGMA user_version = {SCHEMA_VERSION}")) - .execute(&mut *tx) - .await?; + sqlx::query(sqlx::AssertSqlSafe(format!( + "PRAGMA user_version = {SCHEMA_VERSION}" + ))) + .execute(&mut *tx) + .await?; tx.commit().await?; Ok(()) } @@ -1082,7 +1090,8 @@ impl Storage { where_extra ); - let mut count_query = sqlx::query(&count_sql).bind(session_id); + // `where_extra` contains only the fixed timestamp predicates constructed above. + let mut count_query = sqlx::query(sqlx::AssertSqlSafe(count_sql)).bind(session_id); if let Some(bt) = before_time { count_query = count_query.bind(bt); } @@ -1092,7 +1101,7 @@ impl Storage { let count_row = count_query.fetch_one(self.pool()).await?; let total: i64 = count_row.get("total"); - let mut select_query = sqlx::query(&select_sql).bind(session_id); + let mut select_query = sqlx::query(sqlx::AssertSqlSafe(select_sql)).bind(session_id); if let Some(bt) = before_time { select_query = select_query.bind(bt); } @@ -1602,7 +1611,7 @@ mod tests { vec!["locked_at", "lock_owner", "lease_until"], ), ] { - let columns = sqlx::query(&format!("PRAGMA table_info({table})")) + let columns = sqlx::query(sqlx::AssertSqlSafe(format!("PRAGMA table_info({table})"))) .fetch_all(storage.pool()) .await .unwrap();