feat: restore Wolai workspace navigation and assets
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user