f7a720204a
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用 - 添加所有子项目的完整源代码 - 保留原始 .git 为 .git.bak 备份
216 lines
7.7 KiB
Rust
216 lines
7.7 KiB
Rust
//! Shared test fixtures for the knowledge crate: an in-memory
|
|
//! `IKnowledgeRepository`, a no-op event broadcaster, and a service factory.
|
|
|
|
use std::path::Path;
|
|
use std::sync::atomic::{AtomicI64, Ordering};
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use nomifun_common::TimestampMs;
|
|
use nomifun_db::models::{CreateKnowledgeTagParams, KnowledgeBaseRow, KnowledgeBindingRow, KnowledgeTagRow, UpdateKnowledgeTagParams};
|
|
use nomifun_db::{DbError, IKnowledgeRepository};
|
|
|
|
use crate::events::KnowledgeEventEmitter;
|
|
use crate::service::KnowledgeService;
|
|
|
|
#[derive(Default)]
|
|
pub(crate) struct MemRepo {
|
|
pub bases: Mutex<Vec<KnowledgeBaseRow>>,
|
|
/// Each binding row plus its ordered `kb_id` list (mirrors the
|
|
/// `knowledge_binding_bases` junction).
|
|
pub bindings: Mutex<Vec<(KnowledgeBindingRow, Vec<String>)>>,
|
|
next_binding_id: AtomicI64,
|
|
pub tags: Mutex<Vec<KnowledgeTagRow>>,
|
|
}
|
|
|
|
/// Build a binding row with the `target_id` written to the column selected by
|
|
/// `target_kind` (the in-memory analogue of the CHECK-constrained columns).
|
|
fn binding_row(
|
|
binding_id: i64,
|
|
kind: &str,
|
|
target_id: &str,
|
|
enabled: bool,
|
|
writeback: bool,
|
|
writeback_mode: &str,
|
|
writeback_eagerness: &str,
|
|
channel_write_enabled: bool,
|
|
updated_at: TimestampMs,
|
|
) -> KnowledgeBindingRow {
|
|
let mut row = KnowledgeBindingRow {
|
|
binding_id,
|
|
target_kind: kind.to_owned(),
|
|
target_workpath: None,
|
|
target_conv_id: None,
|
|
target_term_id: None,
|
|
target_companion_id: None,
|
|
enabled,
|
|
writeback,
|
|
writeback_mode: writeback_mode.to_owned(),
|
|
writeback_eagerness: writeback_eagerness.to_owned(),
|
|
channel_write_enabled,
|
|
updated_at,
|
|
};
|
|
// `target_conv_id` / `target_term_id` are integer FKs now; `target_workpath`
|
|
// / `target_companion_id` stay string keys. Route the test id to the right column
|
|
// with the right type.
|
|
match kind {
|
|
"workpath" => row.target_workpath = Some(target_id.to_owned()),
|
|
"conversation" => row.target_conv_id = target_id.parse::<i64>().ok(),
|
|
"terminal" => row.target_term_id = target_id.parse::<i64>().ok(),
|
|
"companion" => row.target_companion_id = Some(target_id.to_owned()),
|
|
_ => {}
|
|
}
|
|
row
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl IKnowledgeRepository for MemRepo {
|
|
async fn insert_base(&self, row: &KnowledgeBaseRow) -> Result<(), DbError> {
|
|
self.bases.lock().unwrap().push(row.clone());
|
|
Ok(())
|
|
}
|
|
async fn update_base(&self, row: &KnowledgeBaseRow) -> Result<(), DbError> {
|
|
let mut bases = self.bases.lock().unwrap();
|
|
match bases.iter_mut().find(|r| r.id == row.id) {
|
|
Some(r) => {
|
|
*r = row.clone();
|
|
Ok(())
|
|
}
|
|
None => Err(DbError::NotFound(row.id.clone())),
|
|
}
|
|
}
|
|
async fn delete_base(&self, id: &str) -> Result<(), DbError> {
|
|
let mut bases = self.bases.lock().unwrap();
|
|
let before = bases.len();
|
|
bases.retain(|r| r.id != id);
|
|
if bases.len() == before {
|
|
Err(DbError::NotFound(id.to_owned()))
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
async fn get_base(&self, id: &str) -> Result<Option<KnowledgeBaseRow>, DbError> {
|
|
Ok(self.bases.lock().unwrap().iter().find(|r| r.id == id).cloned())
|
|
}
|
|
async fn list_bases(&self) -> Result<Vec<KnowledgeBaseRow>, DbError> {
|
|
Ok(self.bases.lock().unwrap().clone())
|
|
}
|
|
async fn get_binding(
|
|
&self,
|
|
kind: &str,
|
|
id: &str,
|
|
) -> Result<Option<(KnowledgeBindingRow, Vec<String>)>, DbError> {
|
|
Ok(self
|
|
.bindings
|
|
.lock()
|
|
.unwrap()
|
|
.iter()
|
|
.find(|(b, _)| b.target_kind == kind && b.target_id().as_deref() == Some(id))
|
|
.cloned())
|
|
}
|
|
async fn set_binding(
|
|
&self,
|
|
kind: &str,
|
|
id: &str,
|
|
kb_ids: &[String],
|
|
enabled: bool,
|
|
writeback: bool,
|
|
writeback_mode: &str,
|
|
writeback_eagerness: &str,
|
|
channel_write_enabled: bool,
|
|
updated_at: TimestampMs,
|
|
) -> Result<i64, DbError> {
|
|
let mut bindings = self.bindings.lock().unwrap();
|
|
// Reuse the existing binding_id on upsert; allocate a fresh one
|
|
// otherwise (the surrogate-key analogue of the real table).
|
|
let binding_id = bindings
|
|
.iter()
|
|
.find(|(b, _)| b.target_kind == kind && b.target_id().as_deref() == Some(id))
|
|
.map(|(b, _)| b.binding_id)
|
|
.unwrap_or_else(|| self.next_binding_id.fetch_add(1, Ordering::SeqCst) + 1);
|
|
bindings.retain(|(b, _)| !(b.target_kind == kind && b.target_id().as_deref() == Some(id)));
|
|
let row = binding_row(binding_id, kind, id, enabled, writeback, writeback_mode, writeback_eagerness, channel_write_enabled, updated_at);
|
|
bindings.push((row, kb_ids.to_vec()));
|
|
Ok(binding_id)
|
|
}
|
|
async fn delete_binding(&self, kind: &str, id: &str) -> Result<(), DbError> {
|
|
self.bindings
|
|
.lock()
|
|
.unwrap()
|
|
.retain(|(b, _)| !(b.target_kind == kind && b.target_id().as_deref() == Some(id)));
|
|
Ok(())
|
|
}
|
|
async fn list_bindings_using_kb(&self, kb_id: &str) -> Result<Vec<KnowledgeBindingRow>, DbError> {
|
|
let mut rows: Vec<KnowledgeBindingRow> = self
|
|
.bindings
|
|
.lock()
|
|
.unwrap()
|
|
.iter()
|
|
.filter(|(_, kb_ids)| kb_ids.iter().any(|k| k == kb_id))
|
|
.map(|(b, _)| b.clone())
|
|
.collect();
|
|
rows.sort_by(|a, b| a.target_kind.cmp(&b.target_kind).then(a.binding_id.cmp(&b.binding_id)));
|
|
Ok(rows)
|
|
}
|
|
|
|
async fn list_knowledge_tags(&self) -> Result<Vec<KnowledgeTagRow>, DbError> {
|
|
let mut tags = self.tags.lock().unwrap().clone();
|
|
tags.sort_by(|a, b| a.sort_order.cmp(&b.sort_order).then(a.key.cmp(&b.key)));
|
|
Ok(tags)
|
|
}
|
|
|
|
async fn create_knowledge_tag(&self, params: CreateKnowledgeTagParams) -> Result<(), DbError> {
|
|
self.tags.lock().unwrap().push(KnowledgeTagRow {
|
|
key: params.key,
|
|
label: params.label,
|
|
color: params.color,
|
|
sort_order: params.sort_order,
|
|
created_at: params.created_at,
|
|
});
|
|
Ok(())
|
|
}
|
|
|
|
async fn update_knowledge_tag(&self, key: &str, params: UpdateKnowledgeTagParams) -> Result<(), DbError> {
|
|
let mut tags = self.tags.lock().unwrap();
|
|
match tags.iter_mut().find(|t| t.key == key) {
|
|
Some(t) => {
|
|
if let Some(label) = params.label {
|
|
t.label = label;
|
|
}
|
|
if let Some(color) = params.color {
|
|
t.color = color;
|
|
}
|
|
if let Some(sort_order) = params.sort_order {
|
|
t.sort_order = sort_order;
|
|
}
|
|
Ok(())
|
|
}
|
|
None => Err(DbError::NotFound(format!("knowledge tag {key}"))),
|
|
}
|
|
}
|
|
|
|
async fn delete_knowledge_tag(&self, key: &str) -> Result<(), DbError> {
|
|
let mut tags = self.tags.lock().unwrap();
|
|
let before = tags.len();
|
|
tags.retain(|t| t.key != key);
|
|
if tags.len() == before {
|
|
Err(DbError::NotFound(format!("knowledge tag {key}")))
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) struct NoopBroadcaster;
|
|
|
|
impl nomifun_realtime::EventBroadcaster for NoopBroadcaster {
|
|
fn broadcast(&self, _event: nomifun_api_types::WebSocketMessage<serde_json::Value>) {}
|
|
}
|
|
|
|
pub(crate) fn make_service(data_dir: &Path) -> KnowledgeService {
|
|
KnowledgeService::new(
|
|
Arc::new(MemRepo::default()),
|
|
data_dir,
|
|
KnowledgeEventEmitter::new(Arc::new(NoopBroadcaster)),
|
|
)
|
|
}
|