feat: restore Wolai workspace navigation and assets

This commit is contained in:
Agent Board
2026-07-15 23:12:15 +08:00
parent 31a160fcc5
commit 6f9c7d3b58
48 changed files with 6734 additions and 517 deletions
@@ -450,6 +450,43 @@ impl ControlPlaneStore for StoreHandle {
}
}
fn upsert_knowledge_base(
&self,
input: control_plane::UpsertKnowledgeBaseInput,
) -> Result<control_plane::KnowledgeBaseRecord, ControlPlaneError> {
match self {
StoreHandle::Sqlite(store) => store.upsert_knowledge_base(input),
StoreHandle::Turso(store) => store.upsert_knowledge_base(input),
}
}
fn list_knowledge_bases(
&self,
user_id: &str,
workspace_id: Option<&str>,
root_uri: Option<&str>,
) -> Result<Vec<control_plane::KnowledgeBaseRecord>, ControlPlaneError> {
match self {
StoreHandle::Sqlite(store) => {
store.list_knowledge_bases(user_id, workspace_id, root_uri)
}
StoreHandle::Turso(store) => {
store.list_knowledge_bases(user_id, workspace_id, root_uri)
}
}
}
fn delete_knowledge_base(
&self,
user_id: &str,
knowledge_base_id: &str,
) -> Result<(), ControlPlaneError> {
match self {
StoreHandle::Sqlite(store) => store.delete_knowledge_base(user_id, knowledge_base_id),
StoreHandle::Turso(store) => store.delete_knowledge_base(user_id, knowledge_base_id),
}
}
fn upsert_ai_policy(
&self,
input: control_plane::UpsertAiPolicyInput,
@@ -43,6 +43,10 @@ const MIGRATIONS: &[(&str, &str)] = &[
"v9-ai-tool-events-file-patches",
include_str!("../migrations/009-ai-tool-events-file-patches.sql"),
),
(
"v10-knowledge-bases",
include_str!("../migrations/010-knowledge-bases.sql"),
),
];
/// Create the `_migrations` meta-table if it does not exist.
+39
View File
@@ -323,6 +323,45 @@ pub struct UpsertNavigationRecentInput {
pub metadata_json: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct KnowledgeBaseRecord {
pub id: EntityId,
pub user_id: EntityId,
pub workspace_id: Option<EntityId>,
pub root_uri: Option<String>,
pub name: String,
pub description: Option<String>,
pub provider: String,
pub provider_kb_id: String,
pub default_tool_enabled: bool,
pub can_write: bool,
pub source_count: i64,
pub chunk_count: i64,
pub status: String,
pub metadata_json: String,
pub created_at: Timestamp,
pub updated_at: Timestamp,
pub revision: i64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UpsertKnowledgeBaseInput {
pub id: Option<EntityId>,
pub user_id: EntityId,
pub workspace_id: Option<EntityId>,
pub root_uri: Option<String>,
pub name: String,
pub description: Option<String>,
pub provider: String,
pub provider_kb_id: String,
pub default_tool_enabled: bool,
pub can_write: bool,
pub source_count: i64,
pub chunk_count: i64,
pub status: String,
pub metadata_json: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AiPolicyRecord {
pub id: EntityId,
+271 -8
View File
@@ -14,12 +14,13 @@ use crate::model::{
AppendAiFilePatchInput, AppendAiRuntimeEventInput, AppendAiToolEventInput, AppendAuditInput,
AuditLogRecord, AuthSessionRecord, AuthenticatePasswordInput, CreatePasswordIdentityInput,
CreateSessionInput, CreateShareLinkInput, CreatedShareLink, DirectoryGrantInput,
DirectoryGrantLookup, DirectoryGrantRecord, NavigationRecentRecord, OutboxEventInput,
OutboxEventRecord, ResolvedAccess, ResolvedAuthSession, ShareLinkRecord, SidebarShortcutRecord,
SyncStateRecord, UpsertAiExternalConversationBindingInput, UpsertAiPolicyInput,
UpsertAiRuntimeRunInput, UpsertNavigationRecentInput, UpsertSidebarShortcutInput,
UpsertSyncStateInput, UpsertUserInput, UpsertUserUiPreferenceInput, UpsertWorkspaceInput,
UserRecord, UserUiPreferenceRecord, WorkspaceRecord,
DirectoryGrantLookup, DirectoryGrantRecord, KnowledgeBaseRecord, NavigationRecentRecord,
OutboxEventInput, OutboxEventRecord, ResolvedAccess, ResolvedAuthSession, ShareLinkRecord,
SidebarShortcutRecord, SyncStateRecord, UpsertAiExternalConversationBindingInput,
UpsertAiPolicyInput, UpsertAiRuntimeRunInput, UpsertKnowledgeBaseInput,
UpsertNavigationRecentInput, UpsertSidebarShortcutInput, UpsertSyncStateInput, UpsertUserInput,
UpsertUserUiPreferenceInput, UpsertWorkspaceInput, UserRecord, UserUiPreferenceRecord,
WorkspaceRecord,
};
use crate::store::ControlPlaneStore;
@@ -316,6 +317,28 @@ fn row_to_navigation_recent(row: &rusqlite::Row<'_>) -> rusqlite::Result<Navigat
})
}
fn row_to_knowledge_base(row: &rusqlite::Row<'_>) -> rusqlite::Result<KnowledgeBaseRecord> {
Ok(KnowledgeBaseRecord {
id: row.get(0)?,
user_id: row.get(1)?,
workspace_id: empty_string_to_option(row.get(2)?),
root_uri: empty_string_to_option(row.get(3)?),
name: row.get(4)?,
description: row.get(5)?,
provider: row.get(6)?,
provider_kb_id: row.get(7)?,
default_tool_enabled: row.get::<_, i64>(8)? != 0,
can_write: row.get::<_, i64>(9)? != 0,
source_count: row.get(10)?,
chunk_count: row.get(11)?,
status: row.get(12)?,
metadata_json: row.get(13)?,
created_at: row.get(14)?,
updated_at: row.get(15)?,
revision: row.get(16)?,
})
}
fn navigation_recent_target_key(
kind: &str,
root_uri: &str,
@@ -1815,6 +1838,146 @@ impl ControlPlaneStore for SqliteControlPlaneStore {
Ok(rows)
}
fn upsert_knowledge_base(
&self,
input: UpsertKnowledgeBaseInput,
) -> Result<KnowledgeBaseRecord, ControlPlaneError> {
let user_id = input.user_id.trim().to_string();
let workspace_id = option_to_stored_text(input.workspace_id);
let root_uri = option_to_stored_text(input.root_uri);
let name = input.name.trim().to_string();
let description = input
.description
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty());
let provider = input.provider.trim().to_string();
let provider_kb_id = input.provider_kb_id.trim().to_string();
let status = input.status.trim().to_string();
if user_id.is_empty() || name.is_empty() || provider.is_empty() || provider_kb_id.is_empty()
{
return Err(ControlPlaneError::InvalidInput(
"knowledge base user/name/provider/provider_kb_id 不能为空".to_string(),
));
}
serde_json::from_str::<serde_json::Value>(&input.metadata_json)?;
let conn = self.lock_conn()?;
let now = now_text();
conn.execute(
"INSERT INTO knowledge_bases (
id, user_id, workspace_id, root_uri, name, description, provider,
provider_kb_id, default_tool_enabled, can_write, source_count,
chunk_count, status, metadata_json, created_at, updated_at, revision
)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, 1)
ON CONFLICT(user_id, workspace_id, provider, provider_kb_id)
DO UPDATE SET
root_uri = excluded.root_uri,
name = excluded.name,
description = excluded.description,
default_tool_enabled = excluded.default_tool_enabled,
can_write = excluded.can_write,
source_count = excluded.source_count,
chunk_count = excluded.chunk_count,
status = excluded.status,
metadata_json = excluded.metadata_json,
updated_at = excluded.updated_at,
revision = knowledge_bases.revision + 1",
params![
input.id.unwrap_or_else(|| new_id("kb")),
user_id,
workspace_id,
root_uri,
name,
description,
provider,
provider_kb_id,
if input.default_tool_enabled { 1 } else { 0 },
if input.can_write { 1 } else { 0 },
input.source_count,
input.chunk_count,
if status.is_empty() { "active" } else { &status },
input.metadata_json,
now,
now,
],
)?;
let record = conn.query_row(
"SELECT id, user_id, workspace_id, root_uri, name, description, provider,
provider_kb_id, default_tool_enabled, can_write, source_count,
chunk_count, status, metadata_json, created_at, updated_at, revision
FROM knowledge_bases
WHERE user_id = ?1
AND workspace_id = ?2
AND provider = ?3
AND provider_kb_id = ?4
LIMIT 1",
params![user_id, workspace_id, provider, provider_kb_id],
row_to_knowledge_base,
)?;
Ok(record)
}
fn list_knowledge_bases(
&self,
user_id: &str,
workspace_id: Option<&str>,
root_uri: Option<&str>,
) -> Result<Vec<KnowledgeBaseRecord>, ControlPlaneError> {
let user_id = user_id.trim();
if user_id.is_empty() {
return Ok(Vec::new());
}
let workspace_id = workspace_id.map(str::trim).unwrap_or_default();
let root_uri = root_uri.map(str::trim).unwrap_or_default();
let conn = self.lock_conn()?;
let mut stmt = conn.prepare(
"SELECT id, user_id, workspace_id, root_uri, name, description, provider,
provider_kb_id, default_tool_enabled, can_write, source_count,
chunk_count, status, metadata_json, created_at, updated_at, revision
FROM knowledge_bases
WHERE user_id = ?1
AND status = 'active'
AND (?2 = '' OR workspace_id = ?2)
AND (?3 = '' OR root_uri = ?3)
ORDER BY created_at ASC, name ASC",
)?;
let rows = stmt
.query_map(
params![user_id, workspace_id, root_uri],
row_to_knowledge_base,
)?
.collect::<Result<Vec<_>, _>>()
.map_err(ControlPlaneError::from)?;
Ok(rows)
}
fn delete_knowledge_base(
&self,
user_id: &str,
knowledge_base_id: &str,
) -> Result<(), ControlPlaneError> {
let user_id = user_id.trim();
let knowledge_base_id = knowledge_base_id.trim();
if user_id.is_empty() || knowledge_base_id.is_empty() {
return Err(ControlPlaneError::InvalidInput(
"knowledge base user_id/id 不能为空".to_string(),
));
}
let conn = self.lock_conn()?;
let changed = conn.execute(
"UPDATE knowledge_bases
SET status = 'deleted', updated_at = ?1, revision = revision + 1
WHERE user_id = ?2 AND id = ?3 AND status = 'active'",
params![now_text(), user_id, knowledge_base_id],
)?;
if changed == 0 {
return Err(ControlPlaneError::NotFound(format!(
"knowledge base not found: {knowledge_base_id}"
)));
}
Ok(())
}
fn ensure_ai_agent_profile_policy(
&self,
user_id: &str,
@@ -3218,8 +3381,8 @@ mod tests {
use crate::model::{
password_hash_v1, session_token_hash, AppendAiRuntimeEventInput, AuthenticatePasswordInput,
CreatePasswordIdentityInput, UpsertAiExternalConversationBindingInput, UpsertAiPolicyInput,
UpsertAiRuntimeRunInput, UpsertNavigationRecentInput, UpsertSidebarShortcutInput,
UpsertSyncStateInput, UpsertUserUiPreferenceInput,
UpsertAiRuntimeRunInput, UpsertKnowledgeBaseInput, UpsertNavigationRecentInput,
UpsertSidebarShortcutInput, UpsertSyncStateInput, UpsertUserUiPreferenceInput,
};
fn store() -> SqliteControlPlaneStore {
@@ -4035,6 +4198,106 @@ mod tests {
assert!(bob_recent.is_empty());
}
#[test]
fn knowledge_bases_are_user_scoped_upserted_and_soft_deleted() {
let store = store();
create_user(&store, "alice");
create_user(&store, "bob");
let first = store
.upsert_knowledge_base(UpsertKnowledgeBaseInput {
id: None,
user_id: "alice".to_string(),
workspace_id: Some("local-ws:alice:main".to_string()),
root_uri: Some("file:///tmp/mnote-alice".to_string()),
name: "研究资料".to_string(),
description: Some("paper set".to_string()),
provider: "lightrag_legacy".to_string(),
provider_kb_id: "lightrag-kb-a".to_string(),
default_tool_enabled: true,
can_write: true,
source_count: 0,
chunk_count: 0,
status: "active".to_string(),
metadata_json: "{}".to_string(),
})
.expect("insert knowledge base");
let updated = store
.upsert_knowledge_base(UpsertKnowledgeBaseInput {
id: None,
user_id: "alice".to_string(),
workspace_id: Some("local-ws:alice:main".to_string()),
root_uri: Some("file:///tmp/mnote-alice".to_string()),
name: "研究资料 v2".to_string(),
description: None,
provider: "lightrag_legacy".to_string(),
provider_kb_id: "lightrag-kb-a".to_string(),
default_tool_enabled: false,
can_write: true,
source_count: 3,
chunk_count: 12,
status: "active".to_string(),
metadata_json: "{\"from\":\"test\"}".to_string(),
})
.expect("update knowledge base");
assert_eq!(updated.id, first.id);
assert_eq!(updated.revision, first.revision + 1);
assert_eq!(updated.name, "研究资料 v2");
assert_eq!(updated.source_count, 3);
assert!(!updated.default_tool_enabled);
store
.upsert_knowledge_base(UpsertKnowledgeBaseInput {
id: None,
user_id: "bob".to_string(),
workspace_id: Some("local-ws:alice:main".to_string()),
root_uri: Some("file:///tmp/mnote-alice".to_string()),
name: "Bob KB".to_string(),
description: None,
provider: "lightrag_legacy".to_string(),
provider_kb_id: "lightrag-kb-a".to_string(),
default_tool_enabled: true,
can_write: true,
source_count: 1,
chunk_count: 0,
status: "active".to_string(),
metadata_json: "{}".to_string(),
})
.expect("insert bob knowledge base");
let alice_bases = store
.list_knowledge_bases(
"alice",
Some("local-ws:alice:main"),
Some("file:///tmp/mnote-alice"),
)
.expect("list alice knowledge bases");
assert_eq!(alice_bases.len(), 1);
assert_eq!(alice_bases[0].id, first.id);
store
.delete_knowledge_base("alice", &first.id)
.expect("soft delete alice knowledge base");
let alice_after_delete = store
.list_knowledge_bases(
"alice",
Some("local-ws:alice:main"),
Some("file:///tmp/mnote-alice"),
)
.expect("list alice after delete");
assert!(alice_after_delete.is_empty());
let bob_bases = store
.list_knowledge_bases(
"bob",
Some("local-ws:alice:main"),
Some("file:///tmp/mnote-alice"),
)
.expect("list bob knowledge bases");
assert_eq!(bob_bases.len(), 1);
assert_eq!(bob_bases[0].name, "Bob KB");
}
#[test]
fn ai_policy_upsert_returns_workspace_policy_before_user_policy() {
let store = store();
+25 -6
View File
@@ -7,12 +7,13 @@ use crate::model::{
AiToolEventRecord, AppendAiFilePatchInput, AppendAiRuntimeEventInput, AppendAiToolEventInput,
AppendAuditInput, AuditLogRecord, AuthSessionRecord, AuthenticatePasswordInput,
CreatePasswordIdentityInput, CreateSessionInput, CreateShareLinkInput, CreatedShareLink,
DirectoryGrantInput, DirectoryGrantLookup, DirectoryGrantRecord, NavigationRecentRecord,
OutboxEventInput, OutboxEventRecord, ResolvedAccess, ResolvedAuthSession, ShareLinkRecord,
SidebarShortcutRecord, SyncStateRecord, UpsertAiExternalConversationBindingInput,
UpsertAiPolicyInput, UpsertAiRuntimeRunInput, UpsertNavigationRecentInput,
UpsertSidebarShortcutInput, UpsertSyncStateInput, UpsertUserInput, UpsertUserUiPreferenceInput,
UpsertWorkspaceInput, UserRecord, UserUiPreferenceRecord, WorkspaceRecord,
DirectoryGrantInput, DirectoryGrantLookup, DirectoryGrantRecord, KnowledgeBaseRecord,
NavigationRecentRecord, OutboxEventInput, OutboxEventRecord, ResolvedAccess,
ResolvedAuthSession, ShareLinkRecord, SidebarShortcutRecord, SyncStateRecord,
UpsertAiExternalConversationBindingInput, UpsertAiPolicyInput, UpsertAiRuntimeRunInput,
UpsertKnowledgeBaseInput, UpsertNavigationRecentInput, UpsertSidebarShortcutInput,
UpsertSyncStateInput, UpsertUserInput, UpsertUserUiPreferenceInput, UpsertWorkspaceInput,
UserRecord, UserUiPreferenceRecord, WorkspaceRecord,
};
pub trait ControlPlaneStore: Send + Sync {
@@ -187,6 +188,24 @@ pub trait ControlPlaneStore: Send + Sync {
limit: usize,
) -> Result<Vec<NavigationRecentRecord>, ControlPlaneError>;
fn upsert_knowledge_base(
&self,
input: UpsertKnowledgeBaseInput,
) -> Result<KnowledgeBaseRecord, ControlPlaneError>;
fn list_knowledge_bases(
&self,
user_id: &str,
workspace_id: Option<&str>,
root_uri: Option<&str>,
) -> Result<Vec<KnowledgeBaseRecord>, ControlPlaneError>;
fn delete_knowledge_base(
&self,
user_id: &str,
knowledge_base_id: &str,
) -> Result<(), ControlPlaneError>;
fn upsert_ai_policy(
&self,
input: UpsertAiPolicyInput,
+173 -6
View File
@@ -20,12 +20,13 @@ use crate::model::{
AppendAiFilePatchInput, AppendAiRuntimeEventInput, AppendAiToolEventInput, AppendAuditInput,
AuditLogRecord, AuthSessionRecord, AuthenticatePasswordInput, CreatePasswordIdentityInput,
CreateSessionInput, CreateShareLinkInput, CreatedShareLink, DirectoryGrantInput,
DirectoryGrantLookup, DirectoryGrantRecord, NavigationRecentRecord, OutboxEventInput,
OutboxEventRecord, ResolvedAccess, ResolvedAuthSession, ShareLinkRecord, SidebarShortcutRecord,
SyncStateRecord, UpsertAiExternalConversationBindingInput, UpsertAiPolicyInput,
UpsertAiRuntimeRunInput, UpsertNavigationRecentInput, UpsertSidebarShortcutInput,
UpsertSyncStateInput, UpsertUserInput, UpsertUserUiPreferenceInput, UpsertWorkspaceInput,
UserRecord, UserUiPreferenceRecord, WorkspaceRecord,
DirectoryGrantLookup, DirectoryGrantRecord, KnowledgeBaseRecord, NavigationRecentRecord,
OutboxEventInput, OutboxEventRecord, ResolvedAccess, ResolvedAuthSession, ShareLinkRecord,
SidebarShortcutRecord, SyncStateRecord, UpsertAiExternalConversationBindingInput,
UpsertAiPolicyInput, UpsertAiRuntimeRunInput, UpsertKnowledgeBaseInput,
UpsertNavigationRecentInput, UpsertSidebarShortcutInput, UpsertSyncStateInput, UpsertUserInput,
UpsertUserUiPreferenceInput, UpsertWorkspaceInput, UserRecord, UserUiPreferenceRecord,
WorkspaceRecord,
};
use crate::store::ControlPlaneStore;
@@ -850,6 +851,28 @@ fn row_to_navigation_recent(row: &libsql::Row) -> libsql::Result<NavigationRecen
})
}
fn row_to_knowledge_base(row: &libsql::Row) -> libsql::Result<KnowledgeBaseRecord> {
Ok(KnowledgeBaseRecord {
id: row.get(0)?,
user_id: row.get(1)?,
workspace_id: empty_string_to_option(row.get(2)?),
root_uri: empty_string_to_option(row.get(3)?),
name: row.get(4)?,
description: row.get(5)?,
provider: row.get(6)?,
provider_kb_id: row.get(7)?,
default_tool_enabled: row.get::<i64>(8)? != 0,
can_write: row.get::<i64>(9)? != 0,
source_count: row.get(10)?,
chunk_count: row.get(11)?,
status: row.get(12)?,
metadata_json: row.get(13)?,
created_at: row.get(14)?,
updated_at: row.get(15)?,
revision: row.get(16)?,
})
}
fn navigation_recent_target_key(
kind: &str,
root_uri: &str,
@@ -2339,6 +2362,150 @@ impl ControlPlaneStore for TursoControlPlaneStore {
Ok(rows)
}
fn upsert_knowledge_base(
&self,
input: UpsertKnowledgeBaseInput,
) -> Result<KnowledgeBaseRecord, ControlPlaneError> {
let user_id = input.user_id.trim().to_string();
let workspace_id = option_to_stored_text(input.workspace_id);
let root_uri = option_to_stored_text(input.root_uri);
let name = input.name.trim().to_string();
let description = input
.description
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty());
let provider = input.provider.trim().to_string();
let provider_kb_id = input.provider_kb_id.trim().to_string();
let status = input.status.trim().to_string();
if user_id.is_empty() || name.is_empty() || provider.is_empty() || provider_kb_id.is_empty()
{
return Err(ControlPlaneError::InvalidInput(
"knowledge base user/name/provider/provider_kb_id 不能为空".to_string(),
));
}
serde_json::from_str::<serde_json::Value>(&input.metadata_json)?;
let conn = self.lock_conn()?;
let now = now_text();
conn.execute(
"INSERT INTO knowledge_bases (
id, user_id, workspace_id, root_uri, name, description, provider,
provider_kb_id, default_tool_enabled, can_write, source_count,
chunk_count, status, metadata_json, created_at, updated_at, revision
)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, 1)
ON CONFLICT(user_id, workspace_id, provider, provider_kb_id)
DO UPDATE SET
root_uri = excluded.root_uri,
name = excluded.name,
description = excluded.description,
default_tool_enabled = excluded.default_tool_enabled,
can_write = excluded.can_write,
source_count = excluded.source_count,
chunk_count = excluded.chunk_count,
status = excluded.status,
metadata_json = excluded.metadata_json,
updated_at = excluded.updated_at,
revision = knowledge_bases.revision + 1",
params![
input.id.unwrap_or_else(|| new_id("kb")),
user_id,
workspace_id,
root_uri,
name,
description,
provider,
provider_kb_id,
if input.default_tool_enabled {
1i64
} else {
0i64
},
if input.can_write { 1i64 } else { 0i64 },
input.source_count,
input.chunk_count,
if status.is_empty() { "active" } else { &status },
input.metadata_json,
now,
now,
],
)?;
let record = conn.query_row(
"SELECT id, user_id, workspace_id, root_uri, name, description, provider,
provider_kb_id, default_tool_enabled, can_write, source_count,
chunk_count, status, metadata_json, created_at, updated_at, revision
FROM knowledge_bases
WHERE user_id = ?1
AND workspace_id = ?2
AND provider = ?3
AND provider_kb_id = ?4
LIMIT 1",
params![user_id, workspace_id, provider, provider_kb_id],
row_to_knowledge_base,
)?;
Ok(record)
}
fn list_knowledge_bases(
&self,
user_id: &str,
workspace_id: Option<&str>,
root_uri: Option<&str>,
) -> Result<Vec<KnowledgeBaseRecord>, ControlPlaneError> {
let user_id = user_id.trim();
if user_id.is_empty() {
return Ok(Vec::new());
}
let workspace_id = workspace_id.map(str::trim).unwrap_or_default();
let root_uri = root_uri.map(str::trim).unwrap_or_default();
let conn = self.lock_conn()?;
let mut stmt = conn.prepare(
"SELECT id, user_id, workspace_id, root_uri, name, description, provider,
provider_kb_id, default_tool_enabled, can_write, source_count,
chunk_count, status, metadata_json, created_at, updated_at, revision
FROM knowledge_bases
WHERE user_id = ?1
AND status = 'active'
AND (?2 = '' OR workspace_id = ?2)
AND (?3 = '' OR root_uri = ?3)
ORDER BY created_at ASC, name ASC",
)?;
let rows = stmt
.query_map(
params![user_id, workspace_id, root_uri],
row_to_knowledge_base,
)?
.collect::<Result<Vec<_>, _>>()
.map_err(ControlPlaneError::from)?;
Ok(rows)
}
fn delete_knowledge_base(
&self,
user_id: &str,
knowledge_base_id: &str,
) -> Result<(), ControlPlaneError> {
let user_id = user_id.trim();
let knowledge_base_id = knowledge_base_id.trim();
if user_id.is_empty() || knowledge_base_id.is_empty() {
return Err(ControlPlaneError::InvalidInput(
"knowledge base user_id/id 不能为空".to_string(),
));
}
let conn = self.lock_conn()?;
let changed = conn.execute(
"UPDATE knowledge_bases
SET status = 'deleted', updated_at = ?1, revision = revision + 1
WHERE user_id = ?2 AND id = ?3 AND status = 'active'",
params![now_text(), user_id, knowledge_base_id],
)?;
if changed == 0 {
return Err(ControlPlaneError::NotFound(format!(
"knowledge base not found: {knowledge_base_id}"
)));
}
Ok(())
}
fn ensure_ai_agent_profile_policy(
&self,
user_id: &str,