升级组件

This commit is contained in:
xiaoxixi 2026-07-27 22:40:28 +08:00
parent 1c1b96f5d4
commit 751de27e45
4 changed files with 51 additions and 36 deletions

View File

@ -4,18 +4,18 @@ version = "1.3.1"
edition = "2024" edition = "2024"
[dependencies] [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"] } serde = { version = "1.0", features = ["derive"] }
regex = "1.12" regex = "1.13"
serde_json = "1.0" serde_json = "1.0"
async-trait = "0.1" async-trait = "0.1"
thiserror = "2.0.18" thiserror = "2.0.19"
tokio = { version = "1.52", features = ["full"] } tokio = { version = "1.53", features = ["full"] }
tokio-util = { version = "0.7", features = ["rt", "io"] } tokio-util = { version = "0.7", features = ["rt", "io"] }
dashmap = "6.1" dashmap = "6.2"
uuid = { version = "1.23", features = ["v4"] } uuid = { version = "1.24", features = ["v4"] }
axum = { version = "0.8", features = ["ws", "multipart"] } 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" futures-util = "0.3"
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
dirs = "6.0.0" dirs = "6.0.0"
@ -26,21 +26,21 @@ tracing-appender = "0.2"
time = { version = "0.3", features = ["formatting", "local-offset"] } time = { version = "0.3", features = ["formatting", "local-offset"] }
anyhow = "1.0" anyhow = "1.0"
mime_guess = "2.0" mime_guess = "2.0"
base64 = "0.22" base64 = "0.23"
sha2 = "0.10" sha2 = "0.11"
tempfile = "3" tempfile = "3"
cron = "0.16" cron = "0.17"
chrono-tz = "0.10" chrono-tz = "0.10"
ratatui = "0.30" ratatui = "0.30"
crossterm = { version = "0.29", features = ["event-stream"] } crossterm = { version = "0.29", features = ["event-stream"] }
termimad = "0.34" termimad = "0.35"
textwrap = "0.16" textwrap = "0.16"
unicode-width = "0.2" unicode-width = "0.2"
chrono = "0.4" chrono = "0.4"
sqlx = { version = "0.8", features = ["sqlite", "macros", "chrono", "runtime-tokio"] } sqlx = { version = "0.9", features = ["sqlite", "macros", "chrono", "runtime-tokio"] }
jieba-rs = "0.9" jieba-rs = "0.10"
which = "8" which = "8"
rmcp = { version = "1.7", default-features = false, features = [ rmcp = { version = "2.2", default-features = false, features = [
"client", "client",
"transport-child-process", "transport-child-process",
"transport-streamable-http-client-reqwest", "transport-streamable-http-client-reqwest",

View File

@ -5,7 +5,7 @@ use std::sync::{Arc, Mutex};
use anyhow::Context; use anyhow::Context;
use http::{HeaderName, HeaderValue}; use http::{HeaderName, HeaderValue};
use rmcp::model::{CallToolRequestParams, RawContent}; use rmcp::model::{CallToolRequestParams, ContentBlock};
use rmcp::transport::streamable_http_client::StreamableHttpClientTransportConfig; use rmcp::transport::streamable_http_client::StreamableHttpClientTransportConfig;
use rmcp::transport::{StreamableHttpClientTransport, TokioChildProcess}; use rmcp::transport::{StreamableHttpClientTransport, TokioChildProcess};
use rmcp::{Peer, RoleClient, ServiceExt}; use rmcp::{Peer, RoleClient, ServiceExt};
@ -85,14 +85,14 @@ impl McpConnection {
fn extract_text(result: &rmcp::model::CallToolResult) -> String { fn extract_text(result: &rmcp::model::CallToolResult) -> String {
let mut parts = Vec::new(); let mut parts = Vec::new();
for content in &result.content { for content in &result.content {
match &**content { match content {
RawContent::Text(text) => { ContentBlock::Text(text) => {
parts.push(text.text.clone()); parts.push(text.text.clone());
} }
RawContent::Image(image) => { ContentBlock::Image(image) => {
parts.push(format!("[image: {}]", image.mime_type,)); parts.push(format!("[image: {}]", image.mime_type,));
} }
RawContent::Resource(resource) => match &resource.resource { ContentBlock::Resource(resource) => match &resource.resource {
rmcp::model::ResourceContents::TextResourceContents { text, .. } => { rmcp::model::ResourceContents::TextResourceContents { text, .. } => {
parts.push(format!( parts.push(format!(
"[resource text: {}]", "[resource text: {}]",
@ -102,6 +102,7 @@ fn extract_text(result: &rmcp::model::CallToolResult) -> String {
rmcp::model::ResourceContents::BlobResourceContents { uri, .. } => { rmcp::model::ResourceContents::BlobResourceContents { uri, .. } => {
parts.push(format!("[resource blob: {}]", uri)); parts.push(format!("[resource blob: {}]", uri));
} }
_ => parts.push("[unsupported resource]".to_string()),
}, },
_ => { _ => {
parts.push("[unsupported content]".to_string()); parts.push("[unsupported content]".to_string());

View File

@ -103,8 +103,9 @@ impl super::Storage {
let fts_query = jieba() let fts_query = jieba()
.cut(query, true) .cut(query, true)
.into_iter() .into_iter()
.filter(|w| w.len() > 1 || w.bytes().any(|b| b > 127)) .map(|token| token.word)
.map(|w| format!("\"{}\"", w.replace('"', ""))) .filter(|word| word.len() > 1 || word.bytes().any(|b| b > 127))
.map(|word| format!("\"{}\"", word.replace('"', "")))
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(" OR "); .join(" OR ");
@ -138,8 +139,9 @@ impl super::Storage {
let terms: Vec<String> = jieba() let terms: Vec<String> = jieba()
.cut(query, true) .cut(query, true)
.into_iter() .into_iter()
.filter(|w| w.len() > 1 || w.bytes().any(|b| b > 127)) .map(|token| token.word)
.map(|w| w.replace(['%', '_'], "")) .filter(|word| word.len() > 1 || word.bytes().any(|b| b > 127))
.map(|word| word.replace(['%', '_'], ""))
.collect(); .collect();
if !terms.is_empty() { if !terms.is_empty() {
@ -163,7 +165,8 @@ impl super::Storage {
like_clauses 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 { for term in &terms {
let pattern = format!("%{}%", term); let pattern = format!("%{}%", term);
query_builder = query_builder.bind(pattern.clone()).bind(pattern); query_builder = query_builder.bind(pattern.clone()).bind(pattern);
@ -205,8 +208,9 @@ impl super::Storage {
let terms: Vec<String> = jieba() let terms: Vec<String> = jieba()
.cut(q, true) .cut(q, true)
.into_iter() .into_iter()
.filter(|w| w.len() > 1 || w.bytes().any(|b| b > 127)) .map(|token| token.word)
.map(|w| w.replace(['%', '_'], "")) .filter(|word| word.len() > 1 || word.bytes().any(|b| b > 127))
.map(|word| word.replace(['%', '_'], ""))
.collect(); .collect();
if terms.is_empty() { if terms.is_empty() {
@ -234,7 +238,8 @@ impl super::Storage {
like_clauses 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 { for term in &terms {
let pattern = format!("%{}%", term); let pattern = format!("%{}%", term);
query_builder = query_builder.bind(pattern.clone()).bind(pattern); query_builder = query_builder.bind(pattern.clone()).bind(pattern);

View File

@ -29,7 +29,7 @@ const INSERT_MESSAGE_SQL: &str = r#"
fn insert_message_query<'a>( fn insert_message_query<'a>(
session_id: &'a str, session_id: &'a str,
msg: &'a crate::storage::message::MessageMeta, 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) sqlx::query(INSERT_MESSAGE_SQL)
.bind(&msg.id) .bind(&msg.id)
.bind(session_id) .bind(session_id)
@ -424,13 +424,19 @@ impl Storage {
("job_runs", "delivery_error", "delivery_error TEXT"), ("job_runs", "delivery_error", "delivery_error TEXT"),
] { ] {
let pragma = format!("PRAGMA table_info({table})"); 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 if !columns
.iter() .iter()
.any(|row| row.get::<String, _>("name") == column) .any(|row| row.get::<String, _>("name") == column)
{ {
let alter = format!("ALTER TABLE {table} ADD COLUMN {definition}"); 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,7 +467,9 @@ impl Storage {
) )
.execute(&mut *tx) .execute(&mut *tx)
.await?; .await?;
sqlx::query(&format!("PRAGMA user_version = {SCHEMA_VERSION}")) sqlx::query(sqlx::AssertSqlSafe(format!(
"PRAGMA user_version = {SCHEMA_VERSION}"
)))
.execute(&mut *tx) .execute(&mut *tx)
.await?; .await?;
tx.commit().await?; tx.commit().await?;
@ -1082,7 +1090,8 @@ impl Storage {
where_extra 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 { if let Some(bt) = before_time {
count_query = count_query.bind(bt); count_query = count_query.bind(bt);
} }
@ -1092,7 +1101,7 @@ impl Storage {
let count_row = count_query.fetch_one(self.pool()).await?; let count_row = count_query.fetch_one(self.pool()).await?;
let total: i64 = count_row.get("total"); 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 { if let Some(bt) = before_time {
select_query = select_query.bind(bt); select_query = select_query.bind(bt);
} }
@ -1602,7 +1611,7 @@ mod tests {
vec!["locked_at", "lock_owner", "lease_until"], 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()) .fetch_all(storage.pool())
.await .await
.unwrap(); .unwrap();