优化: AI Chat全栈多批审查修复与架构清理(risk_level清理/路由解耦/工具渲染/测试补测/死代码)
This commit is contained in:
@@ -1070,7 +1070,11 @@ fn ai_provider_from_row(row: &Row<'_>) -> std::result::Result<AiProviderRecord,
|
||||
// F-260614-04: enabled/weight 列老库经 v19 迁移补建,DEFAULT 1 / DEFAULT 50。
|
||||
// from_row 按 i32 取列值兼容(SQLite 无真 BOOLEAN),0→false/非0→true。
|
||||
enabled: row.get::<_, i32>("enabled").unwrap_or(1) != 0,
|
||||
weight: row.get::<_, i32>("weight").unwrap_or(50).max(0) as u32,
|
||||
// weight 读侧 clamp [0,100]:与 insert/update_full 落库的 `.min(100)` 对齐,
|
||||
// 防老库(clamp 落地前写入的)或外部直改 DB 产生的越界值污染路由权重语义。
|
||||
weight: row.get::<_, i32>("weight")
|
||||
.unwrap_or(50)
|
||||
.clamp(0, 100) as u32,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1657,6 +1661,40 @@ impl KnowledgeEventsRepo {
|
||||
.await
|
||||
.map_err(storage_err)?
|
||||
}
|
||||
|
||||
/// 跨知识列最近 N 条事件(全表 timestamp DESC,top-N)。
|
||||
///
|
||||
/// **专用兜底方法**:本表时间列名是 `timestamp` 而非 `created_at`,但
|
||||
/// `impl_repo!` 宏生成的 `query()` / `list_all()` 硬编码 `ORDER BY created_at`
|
||||
/// (见宏内 `ORDER BY created_at DESC` 字面量),误调 `state.knowledge_events.query(...)`
|
||||
/// 会触发 SQLite "no such column: created_at"。本方法走专用 SELECT 绕过宏硬编码,
|
||||
/// 供需要跨知识按时间倒序浏览事件的调用方使用(对标 AiToolExecutionRepo::list_recent
|
||||
/// 对 ai_tool_executions 表的同款兜底处理——那张表同样无 created_at 列)。
|
||||
/// limit 上限钳制 200,防前端恶意/失误传超大值。
|
||||
pub async fn list_recent(&self, limit: u32) -> Result<Vec<KnowledgeEventRecord>> {
|
||||
let conn = self.conn.clone();
|
||||
// 钳制 limit 防滥用(最大 200)
|
||||
let safe_limit = limit.min(200) as i64;
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
let mut stmt = guard
|
||||
.prepare(
|
||||
"SELECT id,knowledge_id,event_type,source_ref,context_json,timestamp \
|
||||
FROM knowledge_events ORDER BY timestamp DESC LIMIT ?1",
|
||||
)
|
||||
.map_err(storage_err)?;
|
||||
let rows = stmt
|
||||
.query_map(params![safe_limit], |row| knowledge_event_from_row(row))
|
||||
.map_err(storage_err)?;
|
||||
let mut results = Vec::new();
|
||||
for r in rows {
|
||||
results.push(r.map_err(storage_err)?);
|
||||
}
|
||||
Ok(results)
|
||||
})
|
||||
.await
|
||||
.map_err(storage_err)?
|
||||
}
|
||||
}
|
||||
|
||||
// AiConversationRepo 的整体更新已由 impl_repo! 宏统一生成的 update_full 提供。
|
||||
|
||||
@@ -595,3 +595,65 @@ CREATE TABLE IF NOT EXISTS app_settings (
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
";
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// column_exists 是幂等迁移的通用探测辅助(V4+ 全部迁移靠它判断列存在性),
|
||||
// 本组用 in-memory SQLite(:memory:) 建临时表覆盖四种语义:
|
||||
// 列存在→true / 列不存在→false / 表不存在→false / 多列中正确匹配目标列名。
|
||||
|
||||
/// 列存在返回 true:建表后探测真实列名。
|
||||
#[test]
|
||||
fn column_exists_returns_true_for_existing_column() {
|
||||
let conn = Connection::open_in_memory().expect("open in-memory db");
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE probe_t (id TEXT PRIMARY KEY, name TEXT NOT NULL, score INTEGER);",
|
||||
)
|
||||
.expect("create probe table");
|
||||
assert!(column_exists(&conn, "probe_t", "id"));
|
||||
assert!(column_exists(&conn, "probe_t", "name"));
|
||||
assert!(column_exists(&conn, "probe_t", "score"));
|
||||
}
|
||||
|
||||
/// 列不存在返回 false:表存在但目标列名未建。
|
||||
#[test]
|
||||
fn column_exists_returns_false_for_missing_column() {
|
||||
let conn = Connection::open_in_memory().expect("open in-memory db");
|
||||
conn.execute_batch("CREATE TABLE probe_t (id TEXT PRIMARY KEY, name TEXT);")
|
||||
.expect("create probe table");
|
||||
assert!(!column_exists(&conn, "probe_t", "archived"));
|
||||
assert!(!column_exists(&conn, "probe_t", "deleted_at"));
|
||||
}
|
||||
|
||||
/// 表不存在返回 false:PRAGMA table_info 对未知表返回空结果集,函数应安全降级为 false。
|
||||
#[test]
|
||||
fn column_exists_returns_false_when_table_absent() {
|
||||
let conn = Connection::open_in_memory().expect("open in-memory db");
|
||||
// 不建任何表,直接探测
|
||||
assert!(!column_exists(&conn, "ghost_table", "any_col"));
|
||||
}
|
||||
|
||||
/// 多列表中正确匹配目标列名(不误命中前缀/子串):验证精确比较非 contains。
|
||||
#[test]
|
||||
fn column_exists_matches_exact_name_among_many() {
|
||||
let conn = Connection::open_in_memory().expect("open in-memory db");
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE probe_t (
|
||||
id TEXT PRIMARY KEY,
|
||||
prompt_tokens INTEGER,
|
||||
completion_tokens INTEGER,
|
||||
model TEXT
|
||||
);",
|
||||
)
|
||||
.expect("create probe table");
|
||||
// 命中真实列
|
||||
assert!(column_exists(&conn, "probe_t", "prompt_tokens"));
|
||||
assert!(column_exists(&conn, "probe_t", "completion_tokens"));
|
||||
// 不误命中子串(prefix/suffix 近似名)
|
||||
assert!(!column_exists(&conn, "probe_t", "tokens"));
|
||||
assert!(!column_exists(&conn, "probe_t", "prompt"));
|
||||
assert!(!column_exists(&conn, "probe_t", "model_configs"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user