feat: restore Wolai workspace navigation and assets
This commit is contained in:
@@ -6288,6 +6288,47 @@ fn legacy_block_projection_attrs(block: &Value, block_type: &str) -> Value {
|
||||
attrs.insert("tiptapImage".into(), tiptap_image);
|
||||
}
|
||||
}
|
||||
if block_type == "media" {
|
||||
for (raw_key, canonical_key) in [
|
||||
("name", "name"),
|
||||
("fileName", "name"),
|
||||
("file_name", "name"),
|
||||
("sourcePath", "sourcePath"),
|
||||
("source_path", "sourcePath"),
|
||||
("url", "sourcePath"),
|
||||
("src", "sourcePath"),
|
||||
] {
|
||||
if let Some(value) = props
|
||||
.and_then(|map| map.get(raw_key))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
attrs.insert(canonical_key.into(), json!(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
if matches!(block_type, "page_reference" | "pagereference") {
|
||||
for (raw_key, canonical_key) in [
|
||||
("title", "title"),
|
||||
("name", "title"),
|
||||
("pageId", "pageId"),
|
||||
("page_id", "pageId"),
|
||||
("sourcePath", "sourcePath"),
|
||||
("source_path", "sourcePath"),
|
||||
("href", "href"),
|
||||
("url", "href"),
|
||||
] {
|
||||
if let Some(value) = props
|
||||
.and_then(|map| map.get(raw_key))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
attrs.insert(canonical_key.into(), json!(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
if block_type == "table" {
|
||||
if let Some(tiptap_table) = props
|
||||
.and_then(|map| map.get("tiptapTable"))
|
||||
@@ -13359,9 +13400,31 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_markdown_table_legacy_projection_exposes_tiptap_table_attrs() {
|
||||
fn local_markdown_media_legacy_projection_exposes_attachment_attrs() {
|
||||
let legacy = json!([{
|
||||
"id": "local-block-3",
|
||||
"type": "media",
|
||||
"props": {
|
||||
"name": "Spec.pdf",
|
||||
"sourcePath": ".assets/file/Spec.pdf"
|
||||
},
|
||||
"content": [],
|
||||
"children": []
|
||||
}]);
|
||||
|
||||
let projection =
|
||||
project_legacy_content_to_block_document("local-md:docs~2FPage.md", &legacy, &json!(0))
|
||||
.expect("media block should project");
|
||||
let block = &projection["blocks"][0];
|
||||
assert_eq!(block["type"], json!("media"));
|
||||
assert_eq!(block["attrs"]["name"], json!("Spec.pdf"));
|
||||
assert_eq!(block["attrs"]["sourcePath"], json!(".assets/file/Spec.pdf"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_markdown_table_legacy_projection_exposes_tiptap_table_attrs() {
|
||||
let legacy = json!([{
|
||||
"id": "local-block-4",
|
||||
"type": "table",
|
||||
"props": {
|
||||
"tiptapTable": {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
-- 010-knowledge-bases.sql
|
||||
-- User/workspace-scoped knowledge base metadata.
|
||||
--
|
||||
-- Source indexing details remain in the local workspace source registry because
|
||||
-- they map local files to provider document/chunk identifiers. The knowledge
|
||||
-- base entity itself lives in control-plane so ownership, membership and share
|
||||
-- grants can resolve it consistently across users.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS knowledge_bases (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
workspace_id TEXT NOT NULL DEFAULT '',
|
||||
root_uri TEXT NOT NULL DEFAULT '',
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
provider TEXT NOT NULL,
|
||||
provider_kb_id TEXT NOT NULL,
|
||||
default_tool_enabled INTEGER NOT NULL DEFAULT 1,
|
||||
can_write INTEGER NOT NULL DEFAULT 1,
|
||||
source_count INTEGER NOT NULL DEFAULT 0,
|
||||
chunk_count INTEGER NOT NULL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
metadata_json TEXT NOT NULL DEFAULT '{}',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
revision INTEGER NOT NULL DEFAULT 1,
|
||||
UNIQUE(user_id, workspace_id, provider, provider_kb_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledge_bases_user_workspace
|
||||
ON knowledge_bases(user_id, workspace_id, status, updated_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledge_bases_root
|
||||
ON knowledge_bases(root_uri, status);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledge_bases_provider
|
||||
ON knowledge_bases(provider, provider_kb_id);
|
||||
@@ -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,
|
||||
|
||||
@@ -470,18 +470,33 @@ import {
|
||||
const topbarCurrent = document.querySelector('.wolai-breadcrumb-current [data-page-title-current]');
|
||||
if (topbarCurrent instanceof HTMLElement) topbarCurrent.textContent = title;
|
||||
window.dispatchEvent(new CustomEvent('mnote:primary-document-activated', {
|
||||
detail: { documentId, workspaceId, title }
|
||||
detail: {
|
||||
documentId,
|
||||
workspaceId,
|
||||
title,
|
||||
sourceKind: bootstrap.sourceKind || '',
|
||||
rootUri: bootstrap.rootUri || '',
|
||||
pageBlockNavigation: runtimeDescriptor.pageBlockNavigation === true,
|
||||
}
|
||||
}));
|
||||
document.documentElement.removeAttribute('data-mnote-side-target-unsupported');
|
||||
document.documentElement.removeAttribute('data-mnote-side-target-asset-id');
|
||||
document.querySelectorAll('.tree-row[data-active="true"], .tree-row[data-selected="true"]').forEach((row) => {
|
||||
const activeTreeTab = document.querySelector('[data-mnote-sidebar-tree-tab][aria-selected="true"]');
|
||||
const activeTreeMode = activeTreeTab instanceof HTMLElement
|
||||
&& activeTreeTab.getAttribute('data-mnote-sidebar-tree-tab') === 'filetree'
|
||||
? 'filetree'
|
||||
: 'page';
|
||||
const activeTreeRows = activeTreeMode === 'filetree'
|
||||
? '#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]'
|
||||
: '#sidebar-tree-root .tree-row[data-shell-mode="page"]';
|
||||
document.querySelectorAll(`${activeTreeRows}[data-active="true"], ${activeTreeRows}[data-selected="true"]`).forEach((row) => {
|
||||
if (row instanceof HTMLElement) {
|
||||
row.setAttribute('data-active', 'false');
|
||||
row.setAttribute('data-selected', 'false');
|
||||
}
|
||||
});
|
||||
const escapedId = window.CSS && typeof window.CSS.escape === 'function' ? window.CSS.escape(documentId) : String(documentId).replace(/["\\]/g, '\\$&');
|
||||
document.querySelectorAll(`.tree-row[data-node-id="${escapedId}"], .tree-row[data-document-id="${escapedId}"], .tree-row[data-doc-id="${escapedId}"]`).forEach((row) => {
|
||||
document.querySelectorAll(`${activeTreeRows}[data-node-id="${escapedId}"], ${activeTreeRows}[data-document-id="${escapedId}"], ${activeTreeRows}[data-doc-id="${escapedId}"]`).forEach((row) => {
|
||||
if (!(row instanceof HTMLElement)) return;
|
||||
if (row.getAttribute('data-shell-mode') === 'filetree') {
|
||||
row.setAttribute('data-selected', String(row.getAttribute('data-row-id') === `doc:${documentId}`));
|
||||
@@ -610,7 +625,7 @@ import {
|
||||
}
|
||||
const bootstrap = buildBootstrapFromAggregate(aggregate, descriptor, paneRole);
|
||||
syncPageAggregateScript({ pageAggregateScriptId: bootstrap.pageAggregateScriptId }, aggregate);
|
||||
const runtimeDescriptor = { paneRole, root, observability, aggregate, bootstrap };
|
||||
const runtimeDescriptor = { paneRole, root, observability, aggregate, bootstrap, pageBlockNavigation: descriptor.pageBlockNavigation === true };
|
||||
updatePaneChrome(runtimeDescriptor);
|
||||
if (paneRole === 'secondary' && workspace instanceof HTMLElement) {
|
||||
workspace.setAttribute('data-has-secondary-pane', 'true');
|
||||
@@ -677,6 +692,70 @@ import {
|
||||
pushUrlState(url);
|
||||
};
|
||||
|
||||
const openPrimaryDocumentFromPageBlockHref = (href) => {
|
||||
const raw = typeof href === 'string' ? href.trim() : '';
|
||||
if (!raw) return false;
|
||||
let target = null;
|
||||
try {
|
||||
target = new URL(raw, window.location.href);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
if (target.origin !== window.location.origin || !target.pathname.startsWith('/documents/')) return false;
|
||||
const encodedId = target.pathname.slice('/documents/'.length).split('/')[0] || '';
|
||||
const documentId = decodeURIComponent(encodedId).trim();
|
||||
if (!documentId || typeof window.__mnoteDocumentPaneRuntime?.openPrimaryDocument !== 'function') return false;
|
||||
const current = currentUrl();
|
||||
const workspaceId = target.searchParams.get('workspaceId') || current.searchParams.get('workspaceId') || '';
|
||||
const sourceKind = target.searchParams.get('sourceKind') || current.searchParams.get('sourceKind') || '';
|
||||
const rootUri = target.searchParams.get('rootUri') || current.searchParams.get('rootUri') || '';
|
||||
void window.__mnoteDocumentPaneRuntime.openPrimaryDocument({
|
||||
documentId,
|
||||
workspaceId,
|
||||
sourceKind,
|
||||
rootUri,
|
||||
url: target,
|
||||
pageBlockNavigation: true,
|
||||
}).catch((error) => {
|
||||
console.warn('mnote page block pane navigation failed, fallback to full navigation', error);
|
||||
window.location.assign(target.pathname + target.search + target.hash);
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
const installPageBlockOpenCapture = () => {
|
||||
if (window.__mnotePageBlockOpenCaptureInstalled === true) return;
|
||||
window.__mnotePageBlockOpenCaptureInstalled = true;
|
||||
document.addEventListener('mousedown', (event) => {
|
||||
if (event.button !== 0) return;
|
||||
const target = event.target;
|
||||
const anchor = target instanceof Element ? target.closest('a.mnote-page-block-link') : null;
|
||||
if (!(anchor instanceof HTMLAnchorElement)) return;
|
||||
const href = anchor.getAttribute('href') || '';
|
||||
if (!href) return;
|
||||
// Tiptap's Link extension handles mouse down before the click bubble. Keep
|
||||
// the target aside so that it cannot trigger a native document navigation.
|
||||
anchor.setAttribute('data-mnote-page-block-href', href);
|
||||
anchor.removeAttribute('href');
|
||||
}, true);
|
||||
document.addEventListener('click', (event) => {
|
||||
const target = event.target;
|
||||
const anchor = target instanceof Element ? target.closest('a.mnote-page-block-link') : null;
|
||||
if (!(anchor instanceof HTMLAnchorElement)) return;
|
||||
const href = anchor.getAttribute('data-mnote-page-block-href') || anchor.getAttribute('href') || '';
|
||||
if (!href) return;
|
||||
anchor.removeAttribute('data-mnote-page-block-href');
|
||||
anchor.removeAttribute('href');
|
||||
if (!openPrimaryDocumentFromPageBlockHref(href)) {
|
||||
anchor.setAttribute('href', href);
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation();
|
||||
}, true);
|
||||
};
|
||||
|
||||
const setSecondaryEditorHostVisible = (visible) => {
|
||||
const host = document.querySelector('[data-testid="mnote-secondary-editor-tab-host"]');
|
||||
if (host instanceof HTMLElement) host.hidden = !visible;
|
||||
@@ -973,10 +1052,11 @@ import {
|
||||
};
|
||||
|
||||
window.__mnoteDocumentPaneRuntime = {
|
||||
openPrimaryDocument: async ({ documentId, workspaceId, sourceKind, rootUri, url } = {}) => {
|
||||
openPrimaryDocument: async ({ documentId, workspaceId, sourceKind, rootUri, url, pageBlockNavigation } = {}) => {
|
||||
const id = typeof documentId === 'string' ? documentId.trim() : '';
|
||||
if (!id) return false;
|
||||
const descriptor = descriptorFromCurrentUrl('primary', id, { workspaceId, sourceKind, rootUri });
|
||||
descriptor.pageBlockNavigation = pageBlockNavigation === true;
|
||||
await replacePaneDocument('primary', descriptor);
|
||||
activateMainEditorTab('', 'primary');
|
||||
updatePrimaryUrl(descriptor, url instanceof URL ? url : null);
|
||||
@@ -1037,6 +1117,7 @@ import {
|
||||
return 1;
|
||||
},
|
||||
};
|
||||
installPageBlockOpenCapture();
|
||||
|
||||
bindMainEditorPageTab('primary');
|
||||
bindMainEditorPageTab('secondary');
|
||||
|
||||
@@ -138,6 +138,39 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
|
||||
|| localMarkdownRelativePathFromDocumentId(session?.documentId)
|
||||
);
|
||||
|
||||
const localFolderNavigationFallbackUrl = (session, reason) => {
|
||||
const url = new URL('/', window.location.origin);
|
||||
url.searchParams.set('sourceKind', 'local_folder');
|
||||
if (session?.rootUri) url.searchParams.set('rootUri', session.rootUri);
|
||||
url.searchParams.set('treeView', 'filetree');
|
||||
const relativePath = sessionRelativePath(session);
|
||||
const currentScope = String(new URL(window.location.href).searchParams.get('fileTreeScope') || '').trim();
|
||||
const parentScope = relativePath.includes('/') ? relativePath.split('/').slice(0, -1).join('/') : '';
|
||||
const scope = currentScope || parentScope;
|
||||
if (scope) url.searchParams.set('fileTreeScope', scope);
|
||||
if (session?.documentId) url.searchParams.set('missingPage', session.documentId);
|
||||
if (reason) url.searchParams.set('routeGuard', reason);
|
||||
return url;
|
||||
};
|
||||
|
||||
const navigateLocalMarkdownDeletedFallback = (session, reason = 'local_markdown_deleted') => {
|
||||
if (!session || session.sessionKind === 'resource') return false;
|
||||
if (session.saveTimer) {
|
||||
window.clearTimeout(session.saveTimer);
|
||||
session.saveTimer = 0;
|
||||
}
|
||||
if (session.externalRefreshTimer) {
|
||||
window.clearTimeout(session.externalRefreshTimer);
|
||||
session.externalRefreshTimer = 0;
|
||||
}
|
||||
session.externalChangePending = false;
|
||||
session.hasExternalConflict = false;
|
||||
session.bufferDirtyState = 'Deleted';
|
||||
clearSessionConflictSurface(session);
|
||||
window.location.assign(localFolderNavigationFallbackUrl(session, reason).toString());
|
||||
return true;
|
||||
};
|
||||
|
||||
const localFolderEventItems = (payload) => {
|
||||
if (!payload || typeof payload !== 'object') return [];
|
||||
const changedPaths = Array.isArray(payload.changedPaths)
|
||||
@@ -151,6 +184,7 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
|
||||
relativePath: String(item.relativePath || item.relative_path || item.path || '').trim(),
|
||||
documentId: String(item.documentId || item.document_id || '').trim(),
|
||||
eventKind: String(item.eventKind || item.event_kind || item.changeType || item.change_type || '').trim(),
|
||||
changeType: String(item.changeType || item.change_type || '').trim(),
|
||||
selfWriteEcho: Boolean(item.selfWriteEcho || item.self_write_echo),
|
||||
fileVersion: String(item.fileVersion || item.file_version || item.observedFileVersion || item.observed_file_version || '').trim(),
|
||||
observedFileVersion: String(item.observedFileVersion || item.observed_file_version || '').trim(),
|
||||
@@ -1167,19 +1201,7 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
|
||||
const bufferState = await fetchSessionBufferState(session);
|
||||
const dirtyState = String(bufferState?.dirtyState || session.bufferDirtyState || '').trim();
|
||||
if (dirtyState === 'Deleted') {
|
||||
session.lastExternalConflictEnvelope = {
|
||||
code: 'local_markdown_deleted',
|
||||
documentId: session.documentId,
|
||||
rootUri: session.rootUri || null,
|
||||
currentDiskVersion: bufferState?.fileVersion || session.conflictDetectionKey || session.fileVersion || null,
|
||||
editorBaseVersion: session.conflictDetectionKey || session.fileVersion || null,
|
||||
externalActor: bufferState?.externalActor || null,
|
||||
dirtyState,
|
||||
bufferFileVersion: bufferState?.fileVersion || session.fileVersion || null,
|
||||
message: '当前本地 Markdown 文件已被删除,请恢复文件或关闭当前标签',
|
||||
suggestedActions: ['keep_editor', 'open_diff'],
|
||||
};
|
||||
markSessionExternalConflict(session, session.lastExternalConflictEnvelope.message);
|
||||
navigateLocalMarkdownDeletedFallback(session);
|
||||
return;
|
||||
}
|
||||
if (session.hasExternalConflict) {
|
||||
@@ -1209,13 +1231,24 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
|
||||
cache: 'no-store',
|
||||
headers: { accept: 'application/json' },
|
||||
});
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (!response.ok) {
|
||||
const errorCode = String(payload?.error?.code || payload?.code || '').trim();
|
||||
if (session.sourceKind === 'local_folder' && (response.status === 404 || errorCode === 'local_markdown_not_found')) {
|
||||
navigateLocalMarkdownDeletedFallback(session, 'local_markdown_not_found');
|
||||
return;
|
||||
}
|
||||
const conflictEnvelope = conflictEnvelopeFromResponse(payload);
|
||||
if (response.status === 409 && conflictEnvelope) {
|
||||
session.lastExternalConflictEnvelope = conflictEnvelope;
|
||||
markSessionExternalConflict(session, conflictMessageFromEnvelope(session, conflictEnvelope));
|
||||
return;
|
||||
}
|
||||
if (session.sourceKind === 'local_folder') {
|
||||
markSessionExternalConflict(session, externalConflictMessage);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const payload = await response.json();
|
||||
const conflictEnvelope = conflictEnvelopeFromResponse(payload);
|
||||
if (response.status === 409 && conflictEnvelope) {
|
||||
session.lastExternalConflictEnvelope = conflictEnvelope;
|
||||
@@ -1404,6 +1437,11 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
|
||||
|| (!documentId && relativePath && relativeTarget && relativePath === relativeTarget)
|
||||
);
|
||||
if (!targetsCurrentDocument) return;
|
||||
const changeType = String(item.changeType || '').trim();
|
||||
if (changeType === 'deleted') {
|
||||
navigateLocalMarkdownDeletedFallback(targetSession);
|
||||
return;
|
||||
}
|
||||
if (item.selfWriteEcho === true) {
|
||||
targetSession.externalChangePending = false;
|
||||
targetSession.lastSelfSaveSignalAt = Date.now();
|
||||
|
||||
@@ -197,9 +197,67 @@ export const legacyBlockToTiptap = (block, index = 0) => {
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (type === 'page_reference' || type === 'pageReference' || type === 'pagereference') {
|
||||
const sourcePath = firstNonEmptyText(
|
||||
block?.props?.sourcePath,
|
||||
block?.props?.source_path,
|
||||
block?.props?.href,
|
||||
block?.props?.url,
|
||||
blockAttrs?.sourcePath,
|
||||
blockAttrs?.source_path,
|
||||
blockAttrs?.href,
|
||||
blockAttrs?.url,
|
||||
blockAttrs?.pageId,
|
||||
blockAttrs?.page_id
|
||||
);
|
||||
const title = firstNonEmptyText(
|
||||
block?.props?.title,
|
||||
block?.props?.name,
|
||||
blockAttrs?.title,
|
||||
blockAttrs?.name,
|
||||
content,
|
||||
sourcePath
|
||||
) || '页面';
|
||||
return {
|
||||
type: 'paragraph',
|
||||
attrs: withTextAlign({ blockId }),
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: title,
|
||||
marks: [{
|
||||
type: 'link',
|
||||
attrs: {
|
||||
href: sourcePath || '#',
|
||||
target: '_self',
|
||||
rel: 'noopener noreferrer nofollow',
|
||||
class: 'mnote-page-block-link',
|
||||
},
|
||||
}],
|
||||
}],
|
||||
};
|
||||
}
|
||||
if (type === 'media') {
|
||||
const sourcePath = firstNonEmptyText(block?.props?.sourcePath, block?.props?.url, block?.props?.src);
|
||||
const name = firstNonEmptyText(block?.props?.name, block?.props?.fileName, block?.props?.title, sourcePath);
|
||||
const sourcePath = firstNonEmptyText(
|
||||
block?.props?.sourcePath,
|
||||
block?.props?.source_path,
|
||||
block?.props?.url,
|
||||
block?.props?.src,
|
||||
blockAttrs?.sourcePath,
|
||||
blockAttrs?.source_path,
|
||||
blockAttrs?.url,
|
||||
blockAttrs?.src
|
||||
);
|
||||
const name = firstNonEmptyText(
|
||||
block?.props?.name,
|
||||
block?.props?.fileName,
|
||||
block?.props?.file_name,
|
||||
block?.props?.title,
|
||||
blockAttrs?.name,
|
||||
blockAttrs?.fileName,
|
||||
blockAttrs?.file_name,
|
||||
blockAttrs?.title,
|
||||
sourcePath
|
||||
);
|
||||
const mediaContent = name
|
||||
? [{
|
||||
type: 'text',
|
||||
@@ -336,7 +394,7 @@ export const localMarkdownDocumentIdFromRelativePath = (relativePath) => {
|
||||
if (!normalized) return '';
|
||||
return 'local-md:' + normalized
|
||||
.split('/')
|
||||
.map((part) => part.replace(/ /g, '~20'))
|
||||
.map((part) => encodeURIComponent(part).replace(/%/g, '~'))
|
||||
.join('~2F');
|
||||
};
|
||||
|
||||
@@ -377,6 +435,48 @@ export const normalizeLocalAssetRelativePath = (value, context) => {
|
||||
.join('/');
|
||||
};
|
||||
|
||||
export const normalizeLocalPageRelativePath = (value, context) => {
|
||||
const text = unwrapMarkdownLinkTarget(value);
|
||||
if (!text || text.startsWith('#')) return '';
|
||||
if (text.startsWith('/documents/')) return text;
|
||||
if (text.startsWith('/api/') || text.startsWith('http://') || text.startsWith('https://') || text.startsWith('mailto:')) return '';
|
||||
const baseDir = text.startsWith('/') ? '' : localMarkdownDirectoryFromDocumentId(context?.documentId);
|
||||
const parts = (baseDir ? `${baseDir}/${text}` : text)
|
||||
.replace(/\\/g, '/')
|
||||
.split('/');
|
||||
const normalized = [];
|
||||
for (const part of parts) {
|
||||
if (!part || part === '.') continue;
|
||||
if (part === '..') {
|
||||
if (!normalized.length) return '';
|
||||
normalized.pop();
|
||||
continue;
|
||||
}
|
||||
normalized.push(part);
|
||||
}
|
||||
return normalized.join('/');
|
||||
};
|
||||
|
||||
export const localPageOpenUrlForTiptap = (value, context) => {
|
||||
const text = unwrapMarkdownLinkTarget(value);
|
||||
if (!context || context.sourceKind !== 'local_folder' || !context.rootUri) return text || value;
|
||||
if (text.startsWith('/documents/')) {
|
||||
const url = new URL(text, window.location.origin);
|
||||
if (!url.searchParams.get('sourceKind')) url.searchParams.set('sourceKind', 'local_folder');
|
||||
if (!url.searchParams.get('rootUri')) url.searchParams.set('rootUri', context.rootUri);
|
||||
return `${url.pathname}${url.search}`;
|
||||
}
|
||||
const relativePath = normalizeLocalPageRelativePath(text, context);
|
||||
if (!relativePath) return text || value;
|
||||
const documentId = localMarkdownDocumentIdFromRelativePath(relativePath);
|
||||
if (!documentId) return text || value;
|
||||
const url = new URL(`/documents/${documentId}`, window.location.origin);
|
||||
url.searchParams.set('sourceKind', 'local_folder');
|
||||
url.searchParams.set('rootUri', context.rootUri);
|
||||
url.searchParams.set('treeView', 'filetree');
|
||||
return `${url.pathname}${url.search}`;
|
||||
};
|
||||
|
||||
export const localFileOpenUrlForTiptap = (value, context) => {
|
||||
if (!context || context.sourceKind !== 'local_folder' || !context.rootUri) return value;
|
||||
const relativePath = normalizeLocalAssetRelativePath(value, context);
|
||||
@@ -463,6 +563,8 @@ const attachmentRefForTiptapHref = (href, context) => {
|
||||
const withAttachmentProjectionAttrs = (attrs, attachmentRef) => {
|
||||
if (!attachmentRef || typeof attachmentRef !== 'object') return attrs;
|
||||
const next = { ...attrs };
|
||||
const fileSize = attachmentFileSizeText(attachmentRef);
|
||||
if (fileSize) next['data-file-size'] = fileSize;
|
||||
if (attachmentRef.exists === false) {
|
||||
next.class = mergeClassNames(next.class, 'mnote-uploaded-attachment-missing');
|
||||
next['data-mnote-attachment-missing'] = 'true';
|
||||
@@ -476,6 +578,14 @@ const withAttachmentProjectionAttrs = (attrs, attachmentRef) => {
|
||||
return next;
|
||||
};
|
||||
|
||||
export const attachmentFileSizeText = (attachmentRef) => {
|
||||
const size = Number(attachmentRef && (attachmentRef.fileSize || attachmentRef.file_size) || 0);
|
||||
if (!Number.isFinite(size) || size <= 0) return '';
|
||||
if (size >= 1024 * 1024) return `${(size / 1024 / 1024).toFixed(size >= 10 * 1024 * 1024 ? 1 : 2)} MB`;
|
||||
if (size >= 1024) return `${(size / 1024).toFixed(size >= 100 * 1024 ? 0 : 2)} KB`;
|
||||
return `${Math.round(size)} B`;
|
||||
};
|
||||
|
||||
export const localizeTiptapAssetUrls = (node, context) => {
|
||||
if (!node || typeof node !== 'object') return node;
|
||||
if (node.type === 'image' && node.attrs && typeof node.attrs.src === 'string') {
|
||||
@@ -490,12 +600,26 @@ export const localizeTiptapAssetUrls = (node, context) => {
|
||||
node.marks = node.marks.map((mark) => {
|
||||
if (!mark || mark.type !== 'link' || !mark.attrs || typeof mark.attrs.href !== 'string') return mark;
|
||||
const href = String(mark.attrs.href || '').trim();
|
||||
const markClass = String(mark.attrs.class || '');
|
||||
if (markClass.split(/\s+/).includes('mnote-page-block-link')) {
|
||||
return {
|
||||
...mark,
|
||||
attrs: {
|
||||
...mark.attrs,
|
||||
href: localPageOpenUrlForTiptap(href, context),
|
||||
target: mark.attrs.target || '_self',
|
||||
rel: mark.attrs.rel || 'noopener noreferrer nofollow',
|
||||
class: mergeClassNames(mark.attrs.class, 'mnote-page-block-link'),
|
||||
},
|
||||
};
|
||||
}
|
||||
const attachmentClass = localAttachmentClassForTiptapHref(href);
|
||||
const attachmentRef = attachmentRefForTiptapHref(href, context);
|
||||
const localizedHref = attachmentClass ? localFileOpenUrlForTiptap(href, context) : href;
|
||||
const attrs = attachmentClass
|
||||
? withAttachmentProjectionAttrs({
|
||||
...mark.attrs,
|
||||
href,
|
||||
href: localizedHref,
|
||||
class: mergeClassNames(mark.attrs.class, attachmentClass),
|
||||
target: mark.attrs.target || '_blank',
|
||||
rel: mark.attrs.rel || 'noopener noreferrer nofollow',
|
||||
|
||||
@@ -92,6 +92,7 @@ export const createSidebarAttachmentOpenRuntime = (dependencies = {}) => {
|
||||
function localMarkdownAttachmentPathFromHref(href, documentId) {
|
||||
var raw = String(href || '').trim();
|
||||
if (!raw || raw.indexOf('#') === 0 || /^[a-z][a-z0-9+.-]*:/i.test(raw)) return '';
|
||||
if (raw.indexOf('/documents/') === 0 || raw.indexOf('/api/') === 0) return '';
|
||||
var documentPath = localMarkdownPathFromDocumentId(documentId);
|
||||
if (!documentPath) return '';
|
||||
var slashIndex = documentPath.lastIndexOf('/');
|
||||
@@ -271,6 +272,7 @@ export const createSidebarAttachmentOpenRuntime = (dependencies = {}) => {
|
||||
|
||||
async function refreshLocalAttachmentExistence(link) {
|
||||
if (!(link instanceof HTMLAnchorElement)) return;
|
||||
if (link.classList.contains('mnote-page-block-link')) return;
|
||||
var href = link.getAttribute('href') || link.href || '';
|
||||
var paneContext = editorAttachmentPaneContext(link);
|
||||
var attachmentRef = attachmentRefForHref(href, paneContext, link ? link.textContent : '');
|
||||
@@ -317,6 +319,7 @@ export const createSidebarAttachmentOpenRuntime = (dependencies = {}) => {
|
||||
|
||||
function refreshEditorLocalAttachmentExistence() {
|
||||
document.querySelectorAll(editorAttachmentEnhanceSelector + ', .editor-surface .ProseMirror a[href*="/api/local-folder/files/open"]').forEach(function(link) {
|
||||
if (link instanceof HTMLAnchorElement && link.classList.contains('mnote-page-block-link')) return;
|
||||
if (link instanceof HTMLAnchorElement) void refreshLocalAttachmentExistence(link);
|
||||
});
|
||||
}
|
||||
@@ -418,16 +421,33 @@ export const createSidebarAttachmentOpenRuntime = (dependencies = {}) => {
|
||||
try {
|
||||
absoluteHref = new URL(href, window.location.href).href;
|
||||
} catch (_) {}
|
||||
var localPath = normalizeLocalRelativePath(
|
||||
localFileOpenPathFromHref(href) || localMarkdownAttachmentPathFromHref(href, paneContext && paneContext.documentId)
|
||||
);
|
||||
return refs.find(function(ref) {
|
||||
if (!ref || typeof ref !== 'object') return false;
|
||||
var refRelativePath = normalizeLocalRelativePath(ref.relativePath || '');
|
||||
var refNormalizedHref = normalizeLocalRelativePath(ref.normalizedHref || ref.rawHref || '').replace(/^\.\//, '');
|
||||
return String(ref.rawHref || '') === href
|
||||
|| String(ref.normalizedHref || '') === href
|
||||
|| String(ref.resolvedUri || '') === href
|
||||
|| (absoluteHref && String(ref.resolvedUri || '') === absoluteHref)
|
||||
|| (localPath && refRelativePath && refRelativePath === localPath)
|
||||
|| (localPath && refNormalizedHref && localPath.endsWith('/' + refNormalizedHref))
|
||||
|| (!href && textLabel && String(ref.label || '').trim() === textLabel);
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function attachmentFileSizeLabel(attachmentRef) {
|
||||
if (!attachmentRef || typeof attachmentRef !== 'object') return '';
|
||||
if (typeof uploadedFileSize === 'function') return uploadedFileSize(attachmentRef);
|
||||
var size = Number(attachmentRef.file_size || attachmentRef.fileSize || 0);
|
||||
if (!Number.isFinite(size) || size <= 0) return '';
|
||||
if (size >= 1024 * 1024) return (size / 1024 / 1024).toFixed(size >= 10 * 1024 * 1024 ? 1 : 2) + ' MB';
|
||||
if (size >= 1024) return (size / 1024).toFixed(size >= 100 * 1024 ? 0 : 2) + ' KB';
|
||||
return String(Math.round(size)) + ' B';
|
||||
}
|
||||
|
||||
function detailFromEditorAttachmentLink(link) {
|
||||
var rawHref = link instanceof HTMLAnchorElement ? (link.getAttribute('href') || '') : '';
|
||||
var params = attachmentQueryParams(rawHref);
|
||||
@@ -483,6 +503,7 @@ export const createSidebarAttachmentOpenRuntime = (dependencies = {}) => {
|
||||
function enhanceEditorAttachmentLink(link) {
|
||||
if (!(link instanceof HTMLAnchorElement)) return;
|
||||
var href = link.getAttribute('href') || '';
|
||||
if (link.classList.contains('mnote-page-block-link')) return;
|
||||
var params = attachmentQueryParams(href);
|
||||
var paneContext = editorAttachmentPaneContext(link);
|
||||
var attachmentRef = attachmentRefForHref(href, paneContext, link ? link.textContent : '');
|
||||
@@ -509,6 +530,8 @@ export const createSidebarAttachmentOpenRuntime = (dependencies = {}) => {
|
||||
if (typeof attachmentRef.authorized === 'boolean') {
|
||||
setEditorAttachmentUnauthorizedState(link, attachmentRef.authorized === false);
|
||||
}
|
||||
var fileSizeLabel = attachmentFileSizeLabel(attachmentRef);
|
||||
if (fileSizeLabel) setAttributeIfChanged(link, 'data-file-size', fileSizeLabel);
|
||||
}
|
||||
setAttributeIfChanged(link, 'target', '_blank');
|
||||
setAttributeIfChanged(link, 'rel', 'noopener noreferrer nofollow');
|
||||
|
||||
@@ -24,6 +24,7 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
|
||||
openEditorAttachmentEditTab,
|
||||
openEditorAttachmentNewWindow,
|
||||
openLocalResourceInActiveTab,
|
||||
knowledgeHostUrl,
|
||||
refreshLocalFolderSidebarSnapshot,
|
||||
removeFileTreeAssetRow,
|
||||
revealFileTreeResource,
|
||||
@@ -467,7 +468,13 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
|
||||
return;
|
||||
}
|
||||
if (action === 'open-knowledge-page') {
|
||||
var knowledgeUrl = '/knowledge';
|
||||
var knowledgeUrl = typeof knowledgeHostUrl === 'function'
|
||||
? knowledgeHostUrl({
|
||||
workspaceId: String(detail && detail.workspaceId || resolveWorkspaceId(trigger || document.body) || '').trim(),
|
||||
sourceKind: currentSourceKind() || 'local_folder',
|
||||
rootUri: knowledgeRagRootUri(detail, trigger)
|
||||
})
|
||||
: '/knowledge';
|
||||
if (typeof window.open === 'function') {
|
||||
window.open(knowledgeUrl, '_blank', 'noopener,noreferrer');
|
||||
} else {
|
||||
|
||||
@@ -705,7 +705,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
'<div class="mnote-weknora-kb-page-title">' +
|
||||
'<span class="mnote-weknora-kb-page-eyebrow">LightRAG</span>' +
|
||||
'<strong>知识库</strong>' +
|
||||
'<span>由 MNote source registry 与 LightRAG 索引驱动</span>' +
|
||||
'<span>资料、索引状态和检索</span>' +
|
||||
'</div>' +
|
||||
'<div class="mnote-weknora-kb-page-actions">' +
|
||||
'<button type="button" data-knowledge-rag-action="refresh" title="刷新"><span class="material-symbols-outlined" data-icon="refresh" aria-hidden="true"></span></button>' +
|
||||
@@ -714,44 +714,54 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="mnote-weknora-kb-page-status" data-testid="mnote-knowledge-rag-status"></div>' +
|
||||
'<div class="mnote-weknora-kb-page-body">' +
|
||||
'<aside class="mnote-weknora-kb-list-pane" data-testid="mnote-weknora-kb-list-pane">' +
|
||||
'<div class="mnote-weknora-kb-page-body" data-kb-list-hidden="true">' +
|
||||
'<aside class="mnote-weknora-kb-list-pane" data-testid="mnote-weknora-kb-list-pane" hidden>' +
|
||||
'<div class="mnote-weknora-kb-list-head">' +
|
||||
'<div><strong>资料库</strong><span data-testid="mnote-weknora-kb-list-count">0</span></div>' +
|
||||
'<button type="button" data-knowledge-rag-action="focus-create-kb" title="LightRAG 当前使用单资料库" hidden><span class="material-symbols-outlined" data-icon="create_new_folder" aria-hidden="true"></span></button>' +
|
||||
'<button type="button" data-knowledge-rag-action="toggle-create-kb" title="新建资料库"><span class="material-symbols-outlined" data-icon="create_new_folder" aria-hidden="true"></span></button>' +
|
||||
'</div>' +
|
||||
'<div class="mnote-weknora-kb-card-wrap" data-testid="mnote-weknora-kb-card-list"></div>' +
|
||||
'<section class="mnote-weknora-kb-create-card" data-testid="mnote-weknora-kb-controls" hidden>' +
|
||||
'<div class="mnote-weknora-kb-create-title"><span class="material-symbols-outlined" data-icon="folder_add" aria-hidden="true"></span><strong>新建逻辑资料库</strong></div>' +
|
||||
'<input type="text" class="wolai-page-settings-index-path" data-testid="mnote-weknora-create-kb-name" data-knowledge-rag-create-kb-name="true" placeholder="知识库名称" spellcheck="false" />' +
|
||||
'<input type="text" class="wolai-page-settings-index-path" data-testid="mnote-weknora-create-kb-description" data-knowledge-rag-create-kb-description="true" placeholder="描述,可留空" spellcheck="false" />' +
|
||||
'<button type="button" class="wolai-page-settings-index-add" data-knowledge-rag-action="create-kb">创建知识库</button>' +
|
||||
'<span class="mnote-knowledge-rag-input-status" data-testid="mnote-weknora-create-kb-status" data-knowledge-rag-create-kb-status hidden></span>' +
|
||||
'</section>' +
|
||||
'</aside>' +
|
||||
'<section class="mnote-weknora-kb-detail-pane" data-testid="mnote-weknora-kb-detail-pane">' +
|
||||
'<div class="mnote-weknora-kb-detail-hero" data-testid="mnote-weknora-kb-detail-hero">' +
|
||||
'<div>' +
|
||||
'<nav class="mnote-weknora-kb-breadcrumb" data-testid="mnote-weknora-kb-breadcrumb" aria-label="知识库面包屑"><span>知识库</span><span>/</span><strong data-testid="mnote-weknora-kb-breadcrumb-current">LightRAG 资料库</strong></nav>' +
|
||||
'<span class="mnote-weknora-kb-type-pill"><span class="material-symbols-outlined" data-icon="folder" aria-hidden="true"></span>Source Registry</span>' +
|
||||
'<span class="mnote-weknora-kb-type-pill"><span class="material-symbols-outlined" data-icon="folder" aria-hidden="true"></span>资料来源</span>' +
|
||||
'<h2 data-testid="mnote-weknora-kb-detail-title">LightRAG 资料库</h2>' +
|
||||
'<p data-testid="mnote-weknora-kb-detail-description">MNote 管理本地来源、引用定位和权限边界,LightRAG 负责索引与召回。</p>' +
|
||||
'</div>' +
|
||||
'<select class="mnote-kb-params-select" data-testid="mnote-weknora-kb-select" data-knowledge-rag-kb-select="true"><option value="">加载中...</option></select>' +
|
||||
'</div>' +
|
||||
'<div class="mnote-knowledge-rag-meta" data-testid="mnote-knowledge-rag-meta"></div>' +
|
||||
'<div class="mnote-knowledge-rag-detail-tabs" role="tablist" aria-label="LightRAG 知识库详情">' +
|
||||
'<button type="button" class="mnote-knowledge-rag-detail-tab is-active" role="tab" aria-selected="true" data-kb-rag-tab="sources" data-testid="mnote-weknora-tab-documents">来源</button>' +
|
||||
'<button type="button" class="mnote-knowledge-rag-detail-tab" role="tab" aria-selected="false" data-kb-rag-tab="wiki" data-testid="mnote-weknora-tab-wiki">Wiki</button>' +
|
||||
'<button type="button" class="mnote-knowledge-rag-detail-tab" role="tab" aria-selected="false" data-kb-rag-tab="graph" data-testid="mnote-weknora-tab-graph">Graph</button>' +
|
||||
'<section class="mnote-knowledge-rag-kb-manager" data-testid="mnote-knowledge-rag-kb-manager">' +
|
||||
'<div class="mnote-knowledge-rag-kb-manager-row">' +
|
||||
'<label class="mnote-knowledge-rag-kb-select-field"><span>当前资料库</span><select class="mnote-kb-params-select" data-testid="mnote-weknora-kb-select" data-knowledge-rag-kb-select="true"><option value="">加载中...</option></select></label>' +
|
||||
'<button type="button" data-knowledge-rag-action="toggle-create-kb"><span class="material-symbols-outlined" data-icon="folder_add" aria-hidden="true"></span>新建</button>' +
|
||||
'<button type="button" data-knowledge-rag-action="delete-kb" data-knowledge-rag-delete-selected-kb="true"><span class="material-symbols-outlined" data-icon="delete" aria-hidden="true"></span>删除</button>' +
|
||||
'</div>' +
|
||||
'<section class="mnote-weknora-kb-create-card" data-testid="mnote-weknora-kb-controls" hidden>' +
|
||||
'<div class="mnote-weknora-kb-create-title"><span class="material-symbols-outlined" data-icon="folder_add" aria-hidden="true"></span><strong>新建资料库</strong></div>' +
|
||||
'<input type="text" class="wolai-page-settings-index-path" data-testid="mnote-weknora-create-kb-name" data-knowledge-rag-create-kb-name="true" placeholder="知识库名称" spellcheck="false" />' +
|
||||
'<input type="text" class="wolai-page-settings-index-path" data-testid="mnote-weknora-create-kb-description" data-knowledge-rag-create-kb-description="true" placeholder="描述,可留空" spellcheck="false" />' +
|
||||
'<button type="button" class="wolai-page-settings-index-add" data-knowledge-rag-action="create-kb">创建知识库</button>' +
|
||||
'<span class="mnote-knowledge-rag-input-status" data-testid="mnote-weknora-create-kb-status" data-knowledge-rag-create-kb-status hidden></span>' +
|
||||
'</section>' +
|
||||
'<div class="mnote-knowledge-rag-kb-manager-note" data-testid="mnote-knowledge-rag-kb-manager-note"></div>' +
|
||||
'</section>' +
|
||||
'<details class="mnote-knowledge-rag-diagnostics" data-testid="mnote-knowledge-rag-diagnostics">' +
|
||||
'<summary>诊断信息</summary>' +
|
||||
'<div class="mnote-knowledge-rag-meta" data-testid="mnote-knowledge-rag-meta"></div>' +
|
||||
'</details>' +
|
||||
'<div class="mnote-knowledge-rag-detail-tabs" role="tablist" aria-label="当前资料库内容">' +
|
||||
'<button type="button" class="mnote-knowledge-rag-detail-tab is-active" role="tab" aria-selected="true" data-kb-rag-tab="sources" data-testid="mnote-weknora-tab-documents">资料</button>' +
|
||||
'<button type="button" class="mnote-knowledge-rag-detail-tab" role="tab" aria-selected="false" data-kb-rag-tab="wiki" data-testid="mnote-weknora-tab-wiki" hidden>Wiki</button>' +
|
||||
'<button type="button" class="mnote-knowledge-rag-detail-tab" role="tab" aria-selected="false" data-kb-rag-tab="graph" data-testid="mnote-weknora-tab-graph" hidden>Graph</button>' +
|
||||
'<button type="button" class="mnote-knowledge-rag-detail-tab" role="tab" aria-selected="false" data-kb-rag-tab="search">检索</button>' +
|
||||
'</div>' +
|
||||
'<div class="mnote-knowledge-rag-detail-panel" data-kb-rag-panel="sources">' +
|
||||
'<div class="mnote-weknora-doc-layout" data-testid="mnote-weknora-document-management">' +
|
||||
'<aside class="mnote-weknora-doc-sidebar" data-testid="mnote-weknora-doc-source-sidebar">' +
|
||||
'<aside class="mnote-weknora-doc-sidebar" data-testid="mnote-weknora-doc-source-sidebar" hidden>' +
|
||||
'<div class="mnote-weknora-doc-sidebar-head"><strong>来源</strong><span>MNote registry</span></div>' +
|
||||
'<div class="mnote-weknora-doc-source-tags" data-testid="mnote-weknora-doc-source-tags">' +
|
||||
'<button type="button" data-knowledge-rag-action="filter-sources" data-knowledge-rag-filter="all" data-active="true"><span class="material-symbols-outlined" data-icon="folder" aria-hidden="true"></span>全部来源</button>' +
|
||||
'<button type="button" data-knowledge-rag-action="filter-sources" data-knowledge-rag-filter="all" data-active="true"><span class="material-symbols-outlined" data-icon="folder" aria-hidden="true"></span>全部资料</button>' +
|
||||
'<button type="button" data-knowledge-rag-action="filter-sources" data-knowledge-rag-filter="indexed"><span class="material-symbols-outlined" data-icon="check_circle" aria-hidden="true"></span>已索引</button>' +
|
||||
'<button type="button" data-knowledge-rag-action="filter-sources" data-knowledge-rag-filter="indexing"><span class="material-symbols-outlined" data-icon="sync" aria-hidden="true"></span>处理中</button>' +
|
||||
'</div>' +
|
||||
@@ -760,21 +770,23 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
'</aside>' +
|
||||
'<section class="mnote-weknora-doc-main">' +
|
||||
'<div class="mnote-weknora-doc-toolbar">' +
|
||||
'<div class="mnote-weknora-doc-search"><span class="material-symbols-outlined" data-icon="search" aria-hidden="true"></span><input type="text" data-testid="mnote-weknora-doc-filter-input" data-knowledge-rag-doc-filter-input="true" placeholder="搜索或筛选当前知识库文档" spellcheck="false" /></div>' +
|
||||
'<div class="mnote-weknora-doc-search"><span class="material-symbols-outlined" data-icon="search" aria-hidden="true"></span><input type="text" data-testid="mnote-weknora-doc-filter-input" data-knowledge-rag-doc-filter-input="true" placeholder="搜索或筛选当前资料库内容" spellcheck="false" /></div>' +
|
||||
'<div class="mnote-weknora-view-switch" role="group" aria-label="文档视图切换" data-testid="mnote-weknora-view-switch">' +
|
||||
'<button type="button" class="is-active" data-knowledge-rag-view="list" aria-pressed="true" title="列表视图"><span class="material-symbols-outlined" data-icon="view_list" aria-hidden="true"></span></button>' +
|
||||
'<button type="button" data-knowledge-rag-view="grid" aria-pressed="false" title="网格视图"><span class="material-symbols-outlined" data-icon="grid_view" aria-hidden="true"></span></button>' +
|
||||
'</div>' +
|
||||
'<button type="button" data-knowledge-rag-action="use-filetree-selection"><span class="material-symbols-outlined" data-icon="account_tree" aria-hidden="true"></span>添加文件夹</button>' +
|
||||
'<button type="button" data-knowledge-rag-action="add-source"><span class="material-symbols-outlined" data-icon="upload_file" aria-hidden="true"></span>添加文档</button>' +
|
||||
'<button type="button" data-knowledge-rag-action="use-filetree-selection"><span class="material-symbols-outlined" data-icon="account_tree" aria-hidden="true"></span>从文件树添加</button>' +
|
||||
'<button type="button" class="mnote-knowledge-rag-primary-action" data-knowledge-rag-action="add-source"><span class="material-symbols-outlined" data-icon="upload_file" aria-hidden="true"></span>添加资料</button>' +
|
||||
'<button type="button" data-knowledge-rag-action="ingest-active"><span class="material-symbols-outlined" data-icon="note_add" aria-hidden="true"></span>加入当前页</button>' +
|
||||
'</div>' +
|
||||
'<div class="mnote-knowledge-rag-source-inputs" data-testid="mnote-knowledge-rag-source-inputs">' +
|
||||
knowledgeRagSourceInputRow('') +
|
||||
'</div>' +
|
||||
'<div class="mnote-weknora-doc-actions">' +
|
||||
'<button type="button" data-knowledge-rag-action="ingest">索引到 LightRAG</button>' +
|
||||
'<button type="button" data-knowledge-rag-action="prune">清除失效记录</button>' +
|
||||
'<div class="mnote-knowledge-rag-source-composer" data-knowledge-rag-source-composer="true" hidden>' +
|
||||
'<div class="mnote-knowledge-rag-source-inputs" data-testid="mnote-knowledge-rag-source-inputs">' +
|
||||
knowledgeRagSourceInputRow('') +
|
||||
'</div>' +
|
||||
'<div class="mnote-weknora-doc-actions">' +
|
||||
'<button type="button" class="mnote-knowledge-rag-primary-action" data-knowledge-rag-action="ingest">开始索引</button>' +
|
||||
'<button type="button" data-knowledge-rag-action="prune">清除失效记录</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="mnote-knowledge-rag-source-filters" data-testid="mnote-knowledge-rag-source-filters"></div>' +
|
||||
'<div class="mnote-weknora-doc-table" data-testid="mnote-knowledge-rag-sources" data-weknora-document-list-view="true"></div>' +
|
||||
@@ -795,11 +807,11 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
'</div>' +
|
||||
'<div class="mnote-knowledge-rag-detail-panel" data-kb-rag-panel="search" hidden>' +
|
||||
'<div class="mnote-kb-search-form">' +
|
||||
'<input type="text" class="mnote-kb-search-input" data-kb-rag-search-input="true" placeholder="输入检索关键词...">' +
|
||||
'<button type="button" class="wolai-page-settings-index-add" data-kb-rag-action="search">搜索</button>' +
|
||||
'<div class="mnote-kb-search-box"><span class="material-symbols-outlined" data-icon="search" aria-hidden="true"></span><input type="text" class="mnote-kb-search-input" data-kb-rag-search-input="true" placeholder="在当前资料库中检索关键词" spellcheck="false"></div>' +
|
||||
'<button type="button" class="wolai-page-settings-index-add" data-kb-rag-action="search"><span class="material-symbols-outlined" data-icon="search" aria-hidden="true"></span>检索</button>' +
|
||||
'</div>' +
|
||||
'<div class="mnote-kb-search-results" data-kb-rag-search-results="true">' +
|
||||
'<span class="mnote-extensions-loading">输入关键词后用 LightRAG hybrid 检索</span>' +
|
||||
'<div class="mnote-kb-search-empty">输入关键词后检索当前资料库</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="mnote-knowledge-rag-detail-panel" data-kb-rag-panel="params" hidden>' +
|
||||
@@ -819,7 +831,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
'</div>' +
|
||||
'<div class="mnote-kb-params-row">' +
|
||||
'<label class="mnote-kb-params-label">排序说明</label>' +
|
||||
'<span class="mnote-kb-params-help">结果按 provider 排序分返回;兼容阈值字段保留在请求层,不作为默认主控。</span>' +
|
||||
'<span class="mnote-kb-params-help">结果按相关度排序;高级阈值保留在请求层。</span>' +
|
||||
'</div>' +
|
||||
'<button type="button" class="wolai-page-settings-index-add" data-kb-rag-action="save-params">保存参数</button>' +
|
||||
'</div>' +
|
||||
@@ -1194,6 +1206,12 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function setKnowledgeRagSourceComposerVisible(visible) {
|
||||
var popover = ensureKnowledgeRagSettingsPopover();
|
||||
var composer = popover.querySelector('[data-knowledge-rag-source-composer]');
|
||||
if (composer instanceof HTMLElement) composer.hidden = !visible;
|
||||
}
|
||||
|
||||
function knowledgeRagSourceInputValues() {
|
||||
var popover = ensureKnowledgeRagSettingsPopover();
|
||||
return Array.from(popover.querySelectorAll('[data-knowledge-rag-source-input]')).map(function(input) {
|
||||
@@ -1342,6 +1360,25 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
return knowledgeRagStatusValue(status);
|
||||
}
|
||||
|
||||
function knowledgeRagUserStatusLabel(status) {
|
||||
var text = String(status || '').trim().toLowerCase();
|
||||
if (!text) return '';
|
||||
if (text === 'provider_missing') return '索引记录缺失';
|
||||
if (text === 'failed' || text.includes('fail')) return '索引失败';
|
||||
if (text === 'delete_submitted') return '正在移除索引';
|
||||
if (text === 'delete_completed') return '已移除索引';
|
||||
if (text === 'provider_mapped' || text === 'completed' || text === 'processed' || text === 'indexed') return '已索引';
|
||||
if (['pending', 'submitted', 'scanning', 'parsing', 'parse', 'parsed', 'analyzing', 'analyze', 'processing', 'process'].includes(text)) return '处理中';
|
||||
return text.replace(/_/g, ' ');
|
||||
}
|
||||
|
||||
function knowledgeRagUserDescription(description) {
|
||||
var text = String(description || '').trim();
|
||||
if (!text) return '';
|
||||
if (/source registry|provider|sidecar|chunk|kb id|knowledge base id/i.test(text)) return '';
|
||||
return text;
|
||||
}
|
||||
|
||||
function knowledgeRagProviderKnowledgeBaseIds(status) {
|
||||
var config = status && status.providerConfig && typeof status.providerConfig === 'object' ? status.providerConfig : {};
|
||||
var weknora = config.weknora && typeof config.weknora === 'object' ? config.weknora : {};
|
||||
@@ -1415,11 +1452,12 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
status: 'configured'
|
||||
});
|
||||
});
|
||||
if (!output.length && ['lightrag', 'lightrag_legacy'].includes(knowledgeRagProviderName(status || {}))) {
|
||||
output.push({
|
||||
if (!seen['lightrag-default'] && ['lightrag', 'lightrag_legacy'].includes(knowledgeRagProviderName(status || {}))) {
|
||||
seen['lightrag-default'] = true;
|
||||
output.unshift({
|
||||
providerKbId: 'lightrag-default',
|
||||
name: '默认资料库',
|
||||
description: 'LightRAG 单工作区索引,由 MNote source registry 划分范围',
|
||||
description: '包含当前工作区的全部已索引资料',
|
||||
sourceCount: ((status && status.registry && Array.isArray(status.registry.entries)) ? status.registry.entries.length : 0),
|
||||
chunkCount: 0,
|
||||
updatedAt: '',
|
||||
@@ -1440,6 +1478,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
|
||||
function setKnowledgeRagSelectedProviderKnowledgeBaseId(providerKnowledgeBaseId) {
|
||||
pageUiState.knowledgeRagSelectedProviderKnowledgeBaseId = String(providerKnowledgeBaseId || '').trim();
|
||||
pageUiState.knowledgeRagSourceFilter = 'all';
|
||||
renderKnowledgeRagSettings();
|
||||
}
|
||||
|
||||
@@ -1491,22 +1530,47 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
};
|
||||
}
|
||||
|
||||
function knowledgeRagBaseStatusLabel(status) {
|
||||
var text = String(status || 'active').trim().toLowerCase();
|
||||
if (!text || text === 'active' || text === 'configured') return '可用';
|
||||
if (text === 'processing' || text === 'indexing') return '处理中';
|
||||
if (text === 'test' || text === 'preview' || text === 'local') return '测试';
|
||||
if (text === 'failed' || text === 'error') return '异常';
|
||||
return String(status || '可用').replace(/_/g, ' ');
|
||||
}
|
||||
|
||||
function knowledgeRagOptimisticBases() {
|
||||
return Array.isArray(pageUiState.knowledgeRagOptimisticBases) ? pageUiState.knowledgeRagOptimisticBases : [];
|
||||
}
|
||||
|
||||
function knowledgeRagBaseIsOptimistic(providerKnowledgeBaseId) {
|
||||
var selected = String(providerKnowledgeBaseId || '').trim();
|
||||
return Boolean(selected && knowledgeRagOptimisticBases().some(function(base) {
|
||||
return knowledgeRagBaseProviderKbId(base) === selected;
|
||||
}));
|
||||
}
|
||||
|
||||
function renderKnowledgeRagKnowledgeBaseCards(popover, status, entries, providerDocs) {
|
||||
var listNode = popover.querySelector('[data-testid="mnote-weknora-kb-card-list"]');
|
||||
var countNode = popover.querySelector('[data-testid="mnote-weknora-kb-list-count"]');
|
||||
var listPane = popover.querySelector('[data-testid="mnote-weknora-kb-list-pane"]');
|
||||
var pageBody = popover.querySelector('.mnote-weknora-kb-page-body');
|
||||
if (!(listNode instanceof HTMLElement)) return;
|
||||
var bases = knowledgeRagKnowledgeBases(status || {});
|
||||
var selected = selectedKnowledgeRagProviderKnowledgeBaseId(status || {});
|
||||
var providerName = knowledgeRagProviderName(status || {});
|
||||
if (countNode instanceof HTMLElement) countNode.textContent = String(bases.length);
|
||||
if (listPane instanceof HTMLElement) listPane.hidden = bases.length <= 1;
|
||||
if (pageBody instanceof HTMLElement) pageBody.setAttribute('data-kb-list-hidden', bases.length <= 1 ? 'true' : 'false');
|
||||
if (!bases.length) {
|
||||
listNode.innerHTML = '<div class="wolai-page-settings-index-empty">还没有知识库来源</div>';
|
||||
listNode.innerHTML = '<div class="wolai-page-settings-index-empty">还没有资料库</div>';
|
||||
return;
|
||||
}
|
||||
listNode.innerHTML = bases.map(function(base) {
|
||||
var stats = knowledgeRagBaseStats(base, entries, providerDocs);
|
||||
var isActive = base.providerKbId === selected;
|
||||
var baseStatus = String(base.status || 'active');
|
||||
var baseStatusLabel = knowledgeRagBaseStatusLabel(baseStatus);
|
||||
return '' +
|
||||
'<button type="button" class="mnote-weknora-kb-card' + (isActive ? ' is-active' : '') + '" data-knowledge-rag-action="select-kb" data-knowledge-rag-kb-id="' + escapeHtml(base.providerKbId) + '" data-knowledge-rag-kb-provider="' + escapeHtml(providerName) + '" data-knowledge-rag-processing-count="' + escapeHtml(String(stats.processingCount)) + '">' +
|
||||
'<span class="mnote-weknora-kb-card-star"><span class="material-symbols-outlined" data-icon="folder" aria-hidden="true"></span></span>' +
|
||||
@@ -1519,12 +1583,13 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
(stats.processingCount ? '<i class="is-processing"><span class="material-symbols-outlined" data-icon="sync" aria-hidden="true"></span>' + escapeHtml(String(stats.processingCount)) + '</i>' : '') +
|
||||
'</span>' +
|
||||
'</span>' +
|
||||
'<span class="mnote-weknora-kb-card-status" data-status="' + escapeHtml(baseStatus) + '">' + escapeHtml(baseStatus) + '</span>' +
|
||||
'<span class="mnote-weknora-kb-card-status" data-status="' + escapeHtml(baseStatus) + '">' + escapeHtml(baseStatusLabel) + '</span>' +
|
||||
'</button>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderKnowledgeRagKnowledgeBaseHero(popover, status, entries, providerDocs) {
|
||||
var heroNode = popover.querySelector('[data-testid="mnote-weknora-kb-detail-hero"]');
|
||||
var titleNode = popover.querySelector('[data-testid="mnote-weknora-kb-detail-title"]');
|
||||
var descNode = popover.querySelector('[data-testid="mnote-weknora-kb-detail-description"]');
|
||||
var breadcrumbNode = popover.querySelector('[data-testid="mnote-weknora-kb-breadcrumb-current"]');
|
||||
@@ -1532,6 +1597,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
var bases = knowledgeRagKnowledgeBases(status || {});
|
||||
var selected = selectedKnowledgeRagProviderKnowledgeBaseId(status || {});
|
||||
var base = bases.find(function(item) { return item.providerKbId === selected; }) || bases[0] || null;
|
||||
if (heroNode instanceof HTMLElement) heroNode.hidden = true;
|
||||
if (!base) {
|
||||
titleNode.textContent = '未配置知识库';
|
||||
descNode.textContent = '加入资料来源后,可在这里管理 MNote 授权的本地文件和引用。';
|
||||
@@ -1541,27 +1607,66 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
var stats = knowledgeRagBaseStats(base, entries, providerDocs);
|
||||
titleNode.textContent = base.name;
|
||||
if (breadcrumbNode instanceof HTMLElement) breadcrumbNode.textContent = base.name;
|
||||
descNode.textContent = (base.description || '暂无描述') + ' · 文档 ' + stats.sourceCount + ' · chunk ' + stats.chunkCount + ' · processing ' + stats.processingCount;
|
||||
var userDescription = knowledgeRagUserDescription(base.description);
|
||||
descNode.textContent = stats.sourceCount + ' 个资料 · ' + stats.processingCount + ' 个处理中 · ' + stats.chunkCount + ' 个片段'
|
||||
+ (userDescription ? ' · ' + userDescription : '');
|
||||
}
|
||||
|
||||
function renderKnowledgeRagKnowledgeBaseControls(popover, status) {
|
||||
var select = popover.querySelector('[data-knowledge-rag-kb-select]');
|
||||
if (!(select instanceof HTMLSelectElement)) return;
|
||||
function renderKnowledgeRagKnowledgeBaseControls(popover, status, entries, providerDocs) {
|
||||
var selects = Array.from(popover.querySelectorAll('[data-knowledge-rag-kb-select]'))
|
||||
.filter(function(select) { return select instanceof HTMLSelectElement; });
|
||||
var deleteButton = popover.querySelector('[data-knowledge-rag-delete-selected-kb]');
|
||||
var noteNode = popover.querySelector('[data-testid="mnote-knowledge-rag-kb-manager-note"]');
|
||||
var bases = knowledgeRagKnowledgeBases(status || {});
|
||||
var selected = selectedKnowledgeRagProviderKnowledgeBaseId(status || {});
|
||||
if (!bases.length) {
|
||||
select.innerHTML = '<option value="">未配置 KB</option>';
|
||||
select.value = '';
|
||||
select.disabled = true;
|
||||
selects.forEach(function(select) {
|
||||
select.innerHTML = '<option value="">未配置资料库</option>';
|
||||
select.value = '';
|
||||
select.disabled = true;
|
||||
select.hidden = false;
|
||||
});
|
||||
if (deleteButton instanceof HTMLButtonElement) {
|
||||
deleteButton.disabled = true;
|
||||
deleteButton.title = '没有可删除的资料库';
|
||||
}
|
||||
if (noteNode instanceof HTMLElement) noteNode.textContent = '新建资料库后,可在这里切换当前工作区的资料范围。';
|
||||
return;
|
||||
}
|
||||
select.disabled = false;
|
||||
select.innerHTML = bases.map(function(base) {
|
||||
var label = base.name + ' · ' + base.sourceCount + ' sources · ' + base.providerKbId;
|
||||
var activeBase = bases.find(function(base) { return base.providerKbId === selected; }) || bases[0];
|
||||
var selectedValue = activeBase.providerKbId;
|
||||
var activeStats = knowledgeRagBaseStats(activeBase, entries || [], providerDocs || []);
|
||||
var optionsHtml = bases.map(function(base) {
|
||||
var stats = knowledgeRagBaseStats(base, entries || [], providerDocs || []);
|
||||
var label = base.name + ' · ' + stats.sourceCount + ' 个资料';
|
||||
return '<option value="' + escapeHtml(base.providerKbId) + '">' + escapeHtml(label) + '</option>';
|
||||
}).join('');
|
||||
select.value = selected || bases[0].providerKbId;
|
||||
pageUiState.knowledgeRagSelectedProviderKnowledgeBaseId = select.value;
|
||||
selects.forEach(function(select) {
|
||||
select.disabled = false;
|
||||
select.hidden = false;
|
||||
select.innerHTML = optionsHtml;
|
||||
select.value = selectedValue;
|
||||
});
|
||||
pageUiState.knowledgeRagSelectedProviderKnowledgeBaseId = selectedValue;
|
||||
if (deleteButton instanceof HTMLButtonElement) {
|
||||
var canDelete = selectedValue !== 'lightrag-default' && activeStats.sourceCount === 0 && activeStats.processingCount === 0;
|
||||
deleteButton.disabled = !canDelete;
|
||||
deleteButton.setAttribute('data-knowledge-rag-kb-id', selectedValue);
|
||||
deleteButton.title = canDelete ? '删除当前空资料库' : (selectedValue === 'lightrag-default' ? '默认资料库不能删除' : '请先移除资料后再删除资料库');
|
||||
}
|
||||
if (noteNode instanceof HTMLElement) {
|
||||
var providerName = knowledgeRagProviderName(status || {});
|
||||
var activeName = activeBase.name || '当前资料库';
|
||||
if (selectedValue === 'lightrag-default') {
|
||||
noteNode.textContent = '默认资料库显示当前工作区全部已索引资料;新建资料库可管理独立资料范围。';
|
||||
} else if (activeStats.sourceCount === 0 && activeStats.processingCount === 0) {
|
||||
noteNode.textContent = activeName + ':空资料库,可添加资料或直接删除。';
|
||||
} else if (providerName === 'weknora') {
|
||||
noteNode.textContent = bases.length > 1 ? '已连接多个资料库,可用下拉框切换当前范围。' : '当前只有一个资料库,可新建后切换。';
|
||||
} else {
|
||||
noteNode.textContent = bases.length > 1 ? '当前可切换资料库;删除前需要先移除其中资料。' : '当前使用默认资料库,也可以新建一个资料库管理独立范围。';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function knowledgeRagKbSummaryHtml(status, entries, providerDocs) {
|
||||
@@ -1581,8 +1686,8 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
'<div class="mnote-weknora-kb-summary-card" data-testid="mnote-weknora-kb-summary" data-knowledge-rag-kb-id="' + escapeHtml(kbIds[0] || '') + '" data-knowledge-rag-kb-ids="' + escapeHtml(kbIds.join(',')) + '" data-knowledge-rag-kb-provider="' + escapeHtml(knowledgeRagProviderName(status || {})) + '" data-knowledge-rag-kb-status="configured">' +
|
||||
'<div><span>知识库</span><strong>' + escapeHtml(activeCount ? String(activeCount) + ' 个' : '未配置') + '</strong></div>' +
|
||||
'<div><span>文档</span><strong>' + escapeHtml(String((entries || []).length)) + '</strong></div>' +
|
||||
'<div><span>Provider docs</span><strong>' + escapeHtml(String((providerDocs || []).length)) + '</strong></div>' +
|
||||
'<div><span>Processing</span><strong data-knowledge-rag-processing-count="' + escapeHtml(String(processingCount)) + '">' + escapeHtml(String(processingCount)) + '</strong></div>' +
|
||||
'<div><span>索引文档</span><strong>' + escapeHtml(String((providerDocs || []).length)) + '</strong></div>' +
|
||||
'<div><span>处理中</span><strong data-knowledge-rag-processing-count="' + escapeHtml(String(processingCount)) + '">' + escapeHtml(String(processingCount)) + '</strong></div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
@@ -1797,8 +1902,8 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
startKnowledgeRagPipelineWatcher(reason);
|
||||
}
|
||||
|
||||
function knowledgeRagUnmappedProviderRows(status, entries) {
|
||||
var docs = knowledgeRagProviderDocuments(status);
|
||||
function knowledgeRagUnmappedProviderRows(status, entries, providerDocs) {
|
||||
var docs = Array.isArray(providerDocs) ? providerDocs : knowledgeRagProviderDocuments(status);
|
||||
if (!docs.length) return [];
|
||||
var mappedDocIds = new Set();
|
||||
var mappedFilePaths = new Set();
|
||||
@@ -1846,14 +1951,14 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
}, { all: 0, attention: 0, indexing: 0, failed: 0, indexed: 0, removed: 0, unmapped: 0 });
|
||||
var current = knowledgeRagCurrentFilter();
|
||||
var options = [
|
||||
['all', '全部'],
|
||||
['attention', '需处理'],
|
||||
['indexing', '索引中'],
|
||||
['indexing', '处理中'],
|
||||
['failed', '失败/过期'],
|
||||
['indexed', '已索引'],
|
||||
['removed', '已移除'],
|
||||
['unmapped', 'Provider 未映射'],
|
||||
['all', '全部']
|
||||
['indexed', '已索引']
|
||||
];
|
||||
if (counts.removed) options.push(['removed', '已移除']);
|
||||
if (counts.unmapped) options.push(['unmapped', '未关联资料']);
|
||||
filtersNode.innerHTML = options.map(function(item) {
|
||||
var key = item[0];
|
||||
var label = item[1];
|
||||
@@ -1868,32 +1973,33 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
}
|
||||
}
|
||||
|
||||
function renderKnowledgeRagSources(popover, entries, status) {
|
||||
function renderKnowledgeRagSources(popover, entries, status, providerDocs, activeBase) {
|
||||
var sourcesNode = popover.querySelector('[data-testid="mnote-knowledge-rag-sources"]');
|
||||
if (!(sourcesNode instanceof HTMLElement)) return;
|
||||
var scopedProviderDocs = Array.isArray(providerDocs) ? providerDocs : knowledgeRagProviderDocuments(status || {});
|
||||
var registryRows = (Array.isArray(entries) ? entries : []).map(function(entry) {
|
||||
return { kind: 'registry', category: knowledgeRagRegistryEntryCategory(entry), entry: entry };
|
||||
});
|
||||
var rows = registryRows.concat(knowledgeRagUnmappedProviderRows(status || {}, entries));
|
||||
var rows = registryRows.concat(knowledgeRagUnmappedProviderRows(status || {}, entries, scopedProviderDocs));
|
||||
renderKnowledgeRagSourceFilters(popover, rows);
|
||||
var filter = knowledgeRagCurrentFilter();
|
||||
var items = rows.filter(function(row) {
|
||||
return knowledgeRagVisibleForFilter(row, filter);
|
||||
});
|
||||
if (!rows.length) {
|
||||
sourcesNode.innerHTML = '<div class="wolai-page-settings-index-empty">还没有登记到知识库 provider 的资料来源</div>';
|
||||
var emptyName = activeBase && activeBase.name ? activeBase.name : '当前资料库';
|
||||
sourcesNode.innerHTML = '<div class="wolai-page-settings-index-empty">' + escapeHtml(emptyName) + ' 还没有资料</div>';
|
||||
return;
|
||||
}
|
||||
if (!items.length) {
|
||||
sourcesNode.innerHTML = '<div class="wolai-page-settings-index-empty">当前分类没有资料来源</div>';
|
||||
sourcesNode.innerHTML = '<div class="wolai-page-settings-index-empty">当前分类没有资料</div>';
|
||||
return;
|
||||
}
|
||||
var pipelineProgress = knowledgeRagPipelineProgress(status || {});
|
||||
var providerName = knowledgeRagProviderName(status || {});
|
||||
var providerDocs = knowledgeRagProviderDocuments(status || {});
|
||||
var providerDocsById = new Map();
|
||||
var providerDocsByPath = new Map();
|
||||
providerDocs.forEach(function(doc) {
|
||||
scopedProviderDocs.forEach(function(doc) {
|
||||
var docId = knowledgeRagProviderDocId(doc);
|
||||
var filePath = String(doc && doc.filePath || '').trim();
|
||||
if (docId) providerDocsById.set(docId, doc);
|
||||
@@ -1907,28 +2013,28 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
var providerId = knowledgeRagProviderDocId(doc);
|
||||
var providerPath = String(doc.filePath || '');
|
||||
var docStatus = knowledgeRagProviderStatusText(doc.status || doc.statusGroup || doc.processingStatus || doc.processing_status);
|
||||
var docStatusLabel = knowledgeRagUserStatusLabel(docStatus) || '已记录';
|
||||
var failedDoc = row.category === 'unmapped_failed';
|
||||
var providerProgress = pipelineProgress && pipelineProgress.docId && pipelineProgress.docId === providerId ? pipelineProgress : null;
|
||||
var knowledgeId = knowledgeRagProviderKnowledgeId(doc);
|
||||
var providerTitle = providerPath || providerId || 'Provider 文档';
|
||||
var providerTitle = providerPath || providerId || '索引文档';
|
||||
var chunkCount = knowledgeRagProviderChunkCount(doc);
|
||||
var providerMeta = 'provider=' + providerName + ' · 未映射到 MNote source registry · ' + docStatus
|
||||
+ (providerId ? ' · provider id ' + providerId : '')
|
||||
+ (chunkCount ? ' · chunk ' + chunkCount : '')
|
||||
+ (providerPath ? '' : ' · 定位降级');
|
||||
var providerMeta = '未关联本地资料 · ' + docStatusLabel
|
||||
+ (chunkCount ? ' · ' + chunkCount + ' 个片段' : '')
|
||||
+ (providerPath ? '' : ' · 无法直接定位原文件');
|
||||
if (!providerProgress && !failedDoc && knowledgeRagDocumentIsInFlight(doc)) {
|
||||
providerProgress = knowledgeRagIndeterminateProgress(status || {}, doc, docStatus);
|
||||
}
|
||||
return '' +
|
||||
'<div class="mnote-knowledge-rag-source-row" data-knowledge-rag-source-row="true" data-knowledge-rag-source-kind="provider" data-knowledge-rag-provider="' + escapeHtml(providerName) + '" data-knowledge-rag-provider-id="' + escapeHtml(providerId) + '" data-knowledge-rag-knowledge-id="' + escapeHtml(knowledgeId) + '" data-knowledge-rag-chunk-count="' + escapeHtml(String(chunkCount || 0)) + '" data-knowledge-rag-processing-status="' + escapeHtml(docStatus) + '" data-knowledge-rag-open-reference-mode="provider-reference-fallback" data-knowledge-rag-source-path="' + escapeHtml(providerPath || providerId) + '">' +
|
||||
'<span class="wolai-page-settings-index-status-dot" data-index-status="' + (failedDoc ? 'fault' : 'indexed') + '" title="' + escapeHtml(docStatus || 'Provider 文档') + '"></span>' +
|
||||
'<span class="wolai-page-settings-index-status-dot" data-index-status="' + (failedDoc ? 'fault' : 'indexed') + '" title="' + escapeHtml(docStatusLabel || '索引文档') + '"></span>' +
|
||||
'<div class="mnote-knowledge-rag-source-main">' +
|
||||
'<strong title="' + escapeHtml(providerTitle) + '">' + escapeHtml(providerTitle) + '</strong>' +
|
||||
'<span>' + escapeHtml(providerMeta) + '</span>' +
|
||||
'<span class="mnote-weknora-source-aux"><i>' + escapeHtml(knowledgeId || 'provider-only') + '</i><i>' + escapeHtml(providerPath ? '引用可回跳' : '仅 provider 定位') + '</i></span>' +
|
||||
'<span class="mnote-weknora-source-aux"><i>' + escapeHtml(knowledgeId || '索引记录') + '</i><i>' + escapeHtml(providerPath ? '引用可回跳' : '无法直接定位') + '</i></span>' +
|
||||
knowledgeRagSourceProgressHtml(providerProgress) +
|
||||
'</div>' +
|
||||
'<div class="mnote-knowledge-rag-source-actions"><button type="button" disabled>只读</button></div>' +
|
||||
'<div class="mnote-knowledge-rag-source-actions"></div>' +
|
||||
'</div>';
|
||||
}
|
||||
var entry = row.entry || {};
|
||||
@@ -1944,11 +2050,12 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
var indexed = entry && (entry.indexedAtMs || providerStatus === 'provider_mapped' || providerStatus === 'completed' || providerStatus === 'processed') && docId && !deleted && !stale && !failed && !deleting && !deletedDone;
|
||||
var kind = deletedDone ? '' : (deleting ? 'indexing' : (failed || stale || deleted ? 'fault' : (indexed ? 'indexed' : 'indexing')));
|
||||
var title = String(entry && entry.sourceRootRelativePath || entry && entry.lightRagFilePath || '');
|
||||
var statusLabel = deletedDone ? 'Provider 已移除'
|
||||
var rawProviderStatusLabel = knowledgeRagUserStatusLabel(providerStatus);
|
||||
var statusLabel = deletedDone ? '已移除索引'
|
||||
: deleting ? '删除已提交'
|
||||
: deleted ? '已删除'
|
||||
: stale ? '已过期'
|
||||
: providerMissing ? 'Provider 索引缺失'
|
||||
: providerMissing ? '索引记录缺失'
|
||||
: failed ? '索引失败'
|
||||
: indexed ? '已索引'
|
||||
: (docId || providerStatus ? '正在索引' : '待提交');
|
||||
@@ -1958,12 +2065,10 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
var kbId = knowledgeRagEntryProviderKnowledgeBaseId(entry);
|
||||
var metaParts = [
|
||||
statusLabel,
|
||||
'provider=' + providerName,
|
||||
'MNote source registry',
|
||||
kbId ? 'kb ' + kbId : '',
|
||||
providerIds.length ? 'provider ids ' + providerIds.join(', ') : '',
|
||||
chunkCount ? 'chunk ' + chunkCount : '',
|
||||
providerStatus ? 'processing ' + providerStatus : ''
|
||||
chunkCount ? chunkCount + ' 个片段' : '',
|
||||
rawProviderStatusLabel && !indexed && rawProviderStatusLabel !== statusLabel ? rawProviderStatusLabel : '',
|
||||
kbId ? '已绑定资料库' : '',
|
||||
providerIds.length ? providerIds.length + ' 个索引记录' : ''
|
||||
].filter(Boolean);
|
||||
var meta = metaParts.join(' · ');
|
||||
var sourcePath = String(entry && entry.sourceRootRelativePath || '');
|
||||
@@ -1977,15 +2082,15 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
'<div class="mnote-knowledge-rag-source-row" data-knowledge-rag-source-row="true" data-knowledge-rag-provider="' + escapeHtml(providerName) + '" data-knowledge-rag-provider-ids="' + escapeHtml(providerIds.join(',')) + '" data-knowledge-rag-kb-id="' + escapeHtml(kbId) + '" data-knowledge-rag-knowledge-id="' + escapeHtml(knowledgeId) + '" data-knowledge-rag-chunk-count="' + escapeHtml(String(chunkCount || 0)) + '" data-knowledge-rag-processing-status="' + escapeHtml(providerStatus || statusLabel) + '" data-knowledge-rag-open-reference-mode="source-registry-reference" data-knowledge-rag-source-path="' + escapeHtml(sourcePath) + '">' +
|
||||
'<span class="wolai-page-settings-index-status-dot" data-index-status="' + kind + '" title="' + escapeHtml(statusLabel) + '"></span>' +
|
||||
'<div class="mnote-knowledge-rag-source-main">' +
|
||||
'<strong title="' + escapeHtml(title) + '">' + escapeHtml(title || '未命名来源') + '</strong>' +
|
||||
'<strong title="' + escapeHtml(title) + '">' + escapeHtml(title || '未命名资料') + '</strong>' +
|
||||
'<span>' + escapeHtml(meta) + '</span>' +
|
||||
'<span class="mnote-weknora-source-aux"><i>' + escapeHtml(kbId || '未绑定 KB') + '</i><i>' + escapeHtml(knowledgeId || '待生成 citation') + '</i></span>' +
|
||||
knowledgeRagSourceProgressHtml(entryProgress) +
|
||||
'</div>' +
|
||||
'<div class="mnote-knowledge-rag-source-actions">' +
|
||||
'<button type="button" data-knowledge-rag-action="open-source-reference" data-knowledge-rag-source-path="' + escapeHtml(sourcePath) + '"' + (sourcePath ? '' : ' disabled') + '>打开引用</button>' +
|
||||
'<button type="button" data-knowledge-rag-action="reindex-source" data-knowledge-rag-source-path="' + escapeHtml(sourcePath) + '"' + (canReindex ? '' : ' disabled') + '>重索引</button>' +
|
||||
'<button type="button" data-knowledge-rag-action="delete-source" data-knowledge-rag-delete-local-file-policy="preserve-local-file" data-knowledge-rag-source-path="' + escapeHtml(sourcePath) + '"' + (canDelete ? '' : ' disabled') + ' title="删除 provider index 与 registry 映射,不删除本地文件">删除索引</button>' +
|
||||
'<button type="button" data-knowledge-rag-action="open-source-reference" data-knowledge-rag-source-path="' + escapeHtml(sourcePath) + '"' + (sourcePath ? '' : ' disabled') + '>打开</button>' +
|
||||
'<button type="button" data-knowledge-rag-action="reindex-source" data-knowledge-rag-source-path="' + escapeHtml(sourcePath) + '"' + (canReindex ? '' : ' disabled') + '>重试</button>' +
|
||||
'<button type="button" data-knowledge-rag-action="delete-source" data-knowledge-rag-delete-local-file-policy="preserve-local-file" data-knowledge-rag-source-path="' + escapeHtml(sourcePath) + '"' + (canDelete ? '' : ' disabled') + ' title="只移除索引记录,不删除本地文件">移除索引</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
@@ -2002,7 +2107,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
statusNode.textContent = '知识库仅在本地工作区可用';
|
||||
if (metaNode instanceof HTMLElement) metaNode.innerHTML = '';
|
||||
renderKnowledgeRagSourceFilters(popover, []);
|
||||
renderKnowledgeRagSources(popover, [], {});
|
||||
renderKnowledgeRagSources(popover, [], {}, [], null);
|
||||
if (dashboardButton instanceof HTMLButtonElement) dashboardButton.disabled = true;
|
||||
return;
|
||||
}
|
||||
@@ -2012,7 +2117,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
statusNode.textContent = '正在读取知识库 provider 状态...';
|
||||
if (metaNode instanceof HTMLElement) metaNode.innerHTML = '';
|
||||
renderKnowledgeRagSourceFilters(popover, []);
|
||||
renderKnowledgeRagSources(popover, [], {});
|
||||
renderKnowledgeRagSources(popover, [], {}, [], null);
|
||||
if (dashboardButton instanceof HTMLButtonElement) dashboardButton.disabled = true;
|
||||
return;
|
||||
}
|
||||
@@ -2020,7 +2125,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
statusNode.textContent = '知识库 provider 状态读取失败:' + summary.error;
|
||||
if (metaNode instanceof HTMLElement) metaNode.innerHTML = '';
|
||||
renderKnowledgeRagSourceFilters(popover, []);
|
||||
renderKnowledgeRagSources(popover, [], {});
|
||||
renderKnowledgeRagSources(popover, [], {}, [], null);
|
||||
if (dashboardButton instanceof HTMLButtonElement) dashboardButton.disabled = true;
|
||||
return;
|
||||
}
|
||||
@@ -2046,12 +2151,21 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
button.classList.toggle('is-active', active);
|
||||
button.setAttribute('aria-pressed', active ? 'true' : 'false');
|
||||
});
|
||||
renderKnowledgeRagKnowledgeBaseControls(popover, status);
|
||||
renderKnowledgeRagKnowledgeBaseControls(popover, status, entries, providerDocs);
|
||||
renderKnowledgeRagKnowledgeBaseCards(popover, status, entries, providerDocs);
|
||||
renderKnowledgeRagKnowledgeBaseHero(popover, status, entries, providerDocs);
|
||||
var processingCount = knowledgeRagInFlightCount(entries, providerDocs);
|
||||
var bases = knowledgeRagKnowledgeBases(status || {});
|
||||
var selectedBaseId = selectedKnowledgeRagProviderKnowledgeBaseId(status || {});
|
||||
var activeBase = bases.find(function(base) { return base.providerKbId === selectedBaseId; }) || bases[0] || null;
|
||||
var activeStats = activeBase ? knowledgeRagBaseStats(activeBase, entries, providerDocs) : null;
|
||||
var activeEntries = activeBase ? knowledgeRagEntriesForKb(entries, activeBase.providerKbId) : entries;
|
||||
var activeProviderDocs = activeBase ? knowledgeRagProviderDocsForKb(providerDocs, activeBase.providerKbId) : providerDocs;
|
||||
statusNode.textContent = healthy
|
||||
? providerLabel + ' 就绪,MNote 登记 ' + entries.length + ' 个来源,provider 文档 ' + providerDocs.length + ' 个'
|
||||
: providerLabel + ' 未就绪';
|
||||
? (activeBase
|
||||
? activeBase.name + ' · 服务可用 · ' + activeStats.sourceCount + ' 个资料' + (activeStats.processingCount ? ' · ' + activeStats.processingCount + ' 个处理中' : '')
|
||||
: '知识库服务可用 · ' + entries.length + ' 个资料' + (processingCount ? ' · ' + processingCount + ' 个处理中' : ''))
|
||||
: '知识库服务不可用,页面仍可编辑';
|
||||
if (metaNode instanceof HTMLElement) {
|
||||
var dashboardUrl = publicKnowledgeRagDashboardUrl(status);
|
||||
var inputDir = String((status.legacyFallback && status.legacyFallback.inputDir) || status.inputDir || '');
|
||||
@@ -2072,15 +2186,15 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
: 'unknown';
|
||||
metaNode.innerHTML = '' +
|
||||
knowledgeRagKbSummaryHtml(status, entries, providerDocs) +
|
||||
'<div><span>Provider</span><code>' + escapeHtml(providerLabel) + '</code></div>' +
|
||||
'<div><span>Boundary</span><code>MNote source registry / LightRAG sidecar locator</code></div>' +
|
||||
'<div><span>Dashboard</span><code>' + escapeHtml(dashboardUrl || '未配置') + '</code></div>' +
|
||||
'<div><span>Model</span><code>' + escapeHtml(llmModel || '未上报') + (vlmModel ? ' / VLM ' + escapeHtml(vlmModel) : '') + '</code></div>' +
|
||||
'<div><span>服务</span><code>' + escapeHtml(providerLabel) + '</code></div>' +
|
||||
'<div><span>边界</span><code>MNote source registry / LightRAG sidecar locator</code></div>' +
|
||||
'<div><span>控制台</span><code>' + escapeHtml(dashboardUrl || '未配置') + '</code></div>' +
|
||||
'<div><span>模型</span><code>' + escapeHtml(llmModel || '未上报') + (vlmModel ? ' / VLM ' + escapeHtml(vlmModel) : '') + '</code></div>' +
|
||||
'<div><span>Rerank</span><code>' + escapeHtml(rerankLabel + (rerankBinding ? ' · ' + rerankBinding : '') + (rerankModel ? ' · ' + rerankModel : '')) + '</code></div>' +
|
||||
'<div><span>Input</span><code>' + escapeHtml(inputDir || '未配置') + '</code></div>' +
|
||||
'<div><span>Storage</span><code>' + escapeHtml(workingDir || '未上报') + '</code></div>';
|
||||
'<div><span>输入目录</span><code>' + escapeHtml(inputDir || '未配置') + '</code></div>' +
|
||||
'<div><span>存储目录</span><code>' + escapeHtml(workingDir || '未上报') + '</code></div>';
|
||||
}
|
||||
renderKnowledgeRagSources(popover, entries, status);
|
||||
renderKnowledgeRagSources(popover, activeEntries, status, activeProviderDocs, activeBase);
|
||||
if (dashboardButton instanceof HTMLButtonElement) dashboardButton.disabled = !publicKnowledgeRagDashboardUrl(status);
|
||||
}
|
||||
|
||||
@@ -2165,8 +2279,11 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
function addKnowledgeRagSourceInput() {
|
||||
var popover = ensureKnowledgeRagSettingsPopover();
|
||||
var node = popover.querySelector('[data-testid="mnote-knowledge-rag-source-inputs"]');
|
||||
var composer = popover.querySelector('[data-knowledge-rag-source-composer]');
|
||||
if (!(node instanceof HTMLElement)) return;
|
||||
node.insertAdjacentHTML('beforeend', knowledgeRagSourceInputRow(''));
|
||||
var shouldAppend = !(composer instanceof HTMLElement && composer.hidden);
|
||||
setKnowledgeRagSourceComposerVisible(true);
|
||||
if (shouldAppend) node.insertAdjacentHTML('beforeend', knowledgeRagSourceInputRow(''));
|
||||
var inputs = Array.from(node.querySelectorAll('[data-knowledge-rag-source-input]'));
|
||||
var last = inputs[inputs.length - 1];
|
||||
if (last instanceof HTMLInputElement) last.focus();
|
||||
@@ -2202,6 +2319,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
var popover = ensureKnowledgeRagSettingsPopover();
|
||||
var node = popover.querySelector('[data-testid="mnote-knowledge-rag-source-inputs"]');
|
||||
if (!(node instanceof HTMLElement)) return;
|
||||
setKnowledgeRagSourceComposerVisible(true);
|
||||
var existing = knowledgeRagSourceInputValues();
|
||||
var next = existing.concat(selectedPaths).filter(function(value, index, list) {
|
||||
return list.indexOf(value) === index;
|
||||
@@ -2249,6 +2367,78 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
if (node instanceof HTMLElement) node.innerHTML = knowledgeRagSourceInputRow('');
|
||||
}
|
||||
|
||||
function setKnowledgeRagCreateCardVisible(visible) {
|
||||
var popover = ensureKnowledgeRagSettingsPopover();
|
||||
var card = popover.querySelector('[data-testid="mnote-weknora-kb-controls"]');
|
||||
var statusNode = popover.querySelector('[data-knowledge-rag-create-kb-status]');
|
||||
if (card instanceof HTMLElement) card.hidden = !visible;
|
||||
if (statusNode instanceof HTMLElement && visible) statusNode.hidden = true;
|
||||
if (visible) {
|
||||
var input = popover.querySelector('[data-knowledge-rag-create-kb-name]');
|
||||
if (input instanceof HTMLInputElement) input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleKnowledgeRagCreateCard() {
|
||||
var popover = ensureKnowledgeRagSettingsPopover();
|
||||
var card = popover.querySelector('[data-testid="mnote-weknora-kb-controls"]');
|
||||
setKnowledgeRagCreateCardVisible(!(card instanceof HTMLElement) || card.hidden);
|
||||
}
|
||||
|
||||
function setKnowledgeRagCreateStatus(text, status) {
|
||||
var popover = ensureKnowledgeRagSettingsPopover();
|
||||
var statusNode = popover.querySelector('[data-knowledge-rag-create-kb-status]');
|
||||
if (statusNode instanceof HTMLElement) {
|
||||
statusNode.textContent = String(text || '');
|
||||
statusNode.setAttribute('data-status', status || 'done');
|
||||
statusNode.hidden = !text;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteKnowledgeRagKnowledgeBase() {
|
||||
var status = currentKnowledgeRagSummary().status || {};
|
||||
var bases = knowledgeRagKnowledgeBases(status);
|
||||
var selected = selectedKnowledgeRagProviderKnowledgeBaseId(status);
|
||||
if (!selected || selected === 'lightrag-default') {
|
||||
setKnowledgeRagCreateStatus('默认资料库不能删除', 'failed');
|
||||
return;
|
||||
}
|
||||
var activeBase = bases.find(function(base) { return base.providerKbId === selected; }) || null;
|
||||
var registry = status.registry && typeof status.registry === 'object' ? status.registry : {};
|
||||
var entries = Array.isArray(registry.entries) ? registry.entries : [];
|
||||
var activeStats = activeBase ? knowledgeRagBaseStats(activeBase, entries, knowledgeRagProviderDocuments(status)) : null;
|
||||
if (activeStats && (activeStats.sourceCount > 0 || activeStats.processingCount > 0)) {
|
||||
setKnowledgeRagCreateStatus('请先移除资料后再删除资料库', 'failed');
|
||||
return;
|
||||
}
|
||||
setKnowledgeRagCreateStatus('删除中', 'running');
|
||||
try {
|
||||
var response = await fetch('/api/knowledge-rag/knowledge-bases', {
|
||||
method: 'DELETE',
|
||||
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
||||
body: JSON.stringify({
|
||||
workspaceId: resolveWorkspaceId(document.body),
|
||||
rootUri: currentRootUri(),
|
||||
providerKnowledgeBaseId: selected
|
||||
})
|
||||
});
|
||||
var payload = await response.json().catch(function(){ return null; });
|
||||
if (!response.ok || !payload || payload.ok !== true) {
|
||||
throw new Error(payload && payload.message ? payload.message : 'knowledge_rag_delete_kb_' + response.status);
|
||||
}
|
||||
pageUiState.knowledgeRagOptimisticBases = knowledgeRagOptimisticBases().filter(function(base) {
|
||||
return knowledgeRagBaseProviderKbId(base) !== selected;
|
||||
});
|
||||
pageUiState.knowledgeRagSelectedProviderKnowledgeBaseId = 'lightrag-default';
|
||||
setKnowledgeRagCreateStatus('已删除资料库', 'done');
|
||||
await loadKnowledgeRagStatus(true, { reason: 'delete-kb', notifyUpdated: true });
|
||||
renderKnowledgeRagSettings();
|
||||
notifyKnowledgeRagSourceUpdated({ action: 'delete-kb', result: payload, providerKnowledgeBaseId: selected });
|
||||
} catch (error) {
|
||||
setKnowledgeRagCreateStatus(error instanceof Error ? error.message : String(error), 'failed');
|
||||
}
|
||||
}
|
||||
|
||||
async function createKnowledgeRagKnowledgeBase() {
|
||||
if (!knowledgeRagIsAvailable()) return;
|
||||
var popover = ensureKnowledgeRagSettingsPopover();
|
||||
@@ -2259,7 +2449,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
var description = descriptionInput instanceof HTMLInputElement ? descriptionInput.value.trim() : '';
|
||||
if (!name) {
|
||||
if (statusNode instanceof HTMLElement) {
|
||||
statusNode.textContent = '请填写 KB 名称';
|
||||
statusNode.textContent = '请填写资料库名称';
|
||||
statusNode.setAttribute('data-status', 'failed');
|
||||
statusNode.hidden = false;
|
||||
}
|
||||
@@ -2308,6 +2498,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
statusNode.setAttribute('data-status', 'done');
|
||||
statusNode.hidden = false;
|
||||
}
|
||||
setKnowledgeRagCreateCardVisible(false);
|
||||
await loadKnowledgeRagStatus(true, { reason: 'create-kb', notifyUpdated: true });
|
||||
if (providerKnowledgeBaseId) {
|
||||
pageUiState.knowledgeRagSelectedProviderKnowledgeBaseId = providerKnowledgeBaseId;
|
||||
@@ -2385,6 +2576,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
setKnowledgeRagInputStatuses(done, 'done');
|
||||
await loadKnowledgeRagStatus(true);
|
||||
resetKnowledgeRagSourceInputs();
|
||||
setKnowledgeRagSourceComposerVisible(false);
|
||||
notifyKnowledgeRagSourceUpdated({ action: 'ingest', sources: sources, result: payload });
|
||||
scheduleKnowledgeRagStatusBridge('ingest');
|
||||
} catch (error) {
|
||||
@@ -2590,10 +2782,10 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
if (!input || !resultsEl) return;
|
||||
var query = (input.value || '').trim();
|
||||
if (!query) {
|
||||
resultsEl.innerHTML = '<span class="mnote-extensions-error">请输入检索关键词</span>';
|
||||
resultsEl.innerHTML = '<div class="mnote-kb-search-empty">请输入检索关键词</div>';
|
||||
return;
|
||||
}
|
||||
resultsEl.innerHTML = '<span class="mnote-extensions-loading">检索中...</span>';
|
||||
resultsEl.innerHTML = '<div class="mnote-kb-search-empty">检索中...</div>';
|
||||
try {
|
||||
var params = new URLSearchParams();
|
||||
params.set('query', query);
|
||||
@@ -2623,7 +2815,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
? data.results
|
||||
: (Array.isArray(data.references) ? data.references : (Array.isArray(data) ? data : []));
|
||||
if (!results.length) {
|
||||
resultsEl.innerHTML = '<div class="mnote-extensions-item"><span>未找到匹配结果</span></div>';
|
||||
resultsEl.innerHTML = '<div class="mnote-kb-search-empty">未找到匹配结果</div>';
|
||||
return;
|
||||
}
|
||||
var html = '';
|
||||
@@ -2640,7 +2832,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
html += '<div class="mnote-kb-search-result-item" data-kb-rag-search-result="true" data-knowledge-rag-provider="' + escapeHtml(knowledgeRagProviderName(currentKnowledgeRagSummary().status || {})) + '" data-knowledge-rag-kb-id="' + escapeHtml(providerKnowledgeBaseId) + '" data-knowledge-rag-knowledge-id="' + escapeHtml(providerKnowledgeId) + '" data-knowledge-rag-chunk-id="' + escapeHtml(providerChunkId) + '" data-knowledge-rag-source-path="' + escapeHtml(String(source)) + '">' +
|
||||
'<div class="mnote-kb-search-result-header">' +
|
||||
'<span class="mnote-kb-search-result-index">#' + (i + 1) + '</span>' +
|
||||
'<span class="mnote-kb-search-result-score" data-kb-rag-score-label="provider-ranking-score" data-kb-rag-score-is-similarity-percent="false">provider 排序分: ' + score + '</span>' +
|
||||
'<span class="mnote-kb-search-result-score" data-kb-rag-score-label="relevance" data-kb-rag-score-is-similarity-percent="false">相关度 ' + score + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="mnote-kb-search-result-source">' + escapeHtml(String(source)) + '</div>' +
|
||||
'<div class="mnote-kb-search-result-preview">' + escapeHtml(preview) + (content.length > 200 ? '...' : '') + '</div>' +
|
||||
@@ -2650,7 +2842,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
});
|
||||
resultsEl.innerHTML = html;
|
||||
} catch (err) {
|
||||
resultsEl.innerHTML = '<div class="mnote-extensions-item"><span class="mnote-extensions-error">检索失败: ' + escapeHtml(String(err.message || err)) + '</span></div>';
|
||||
resultsEl.innerHTML = '<div class="mnote-kb-search-empty is-error">检索失败:' + escapeHtml(String(err.message || err)) + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3312,6 +3504,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
loadKnowledgeRagStatus,
|
||||
openKnowledgeRagDashboard,
|
||||
createKnowledgeRagKnowledgeBase,
|
||||
deleteKnowledgeRagKnowledgeBase,
|
||||
loadKnowledgeRagSearchParamsToForm,
|
||||
performKnowledgeRagSearch,
|
||||
saveKnowledgeRagSearchParams,
|
||||
@@ -3323,6 +3516,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
setKnowledgeRagSelectedProviderKnowledgeBaseId,
|
||||
setKnowledgeRagSourceFilter,
|
||||
useKnowledgeRagFileTreeSelection,
|
||||
toggleKnowledgeRagCreateCard,
|
||||
togglePageSettingsPopover,
|
||||
updatePageSettingsTriggerState,
|
||||
writeGlobalShowHeadingNumbers,
|
||||
|
||||
@@ -41,6 +41,51 @@ export const createSidebarPageTreeRuntime = (dependencies = {}) => {
|
||||
};
|
||||
}
|
||||
|
||||
function selectSidebarPageTreeDocument(nodeId, options) {
|
||||
var normalizedNodeId = String(nodeId || '').trim();
|
||||
if (!normalizedNodeId) return false;
|
||||
var targetRow = document.querySelector(
|
||||
'#sidebar-tree-root .tree-row[data-shell-mode="page"][data-node-id="' + cssEscape(normalizedNodeId) + '"]'
|
||||
);
|
||||
if (!(targetRow instanceof HTMLElement)) return false;
|
||||
|
||||
var parentId = String(targetRow.getAttribute('data-parent-id') || '').trim();
|
||||
var guard = 0;
|
||||
while (parentId && guard < 512) {
|
||||
var parentRow = document.querySelector(
|
||||
'#sidebar-tree-root .tree-row[data-shell-mode="page"][data-node-id="' + cssEscape(parentId) + '"]'
|
||||
);
|
||||
if (!(parentRow instanceof HTMLElement)) break;
|
||||
if (parentRow.getAttribute('aria-expanded') !== 'true') {
|
||||
toggleChildren(parentRow, parentRow.querySelector('[data-rust-action="toggle"]'));
|
||||
}
|
||||
parentId = String(parentRow.getAttribute('data-parent-id') || '').trim();
|
||||
guard += 1;
|
||||
}
|
||||
|
||||
document.querySelectorAll('#sidebar-tree-root .tree-row[data-shell-mode="page"][data-active="true"]').forEach(function(row) {
|
||||
if (!(row instanceof HTMLElement)) return;
|
||||
row.setAttribute('data-active', 'false');
|
||||
row.setAttribute('aria-selected', 'false');
|
||||
row.removeAttribute('aria-current');
|
||||
});
|
||||
targetRow.setAttribute('data-active', 'true');
|
||||
targetRow.setAttribute('aria-selected', 'true');
|
||||
targetRow.setAttribute('aria-current', 'page');
|
||||
if (!options || options.scrollIntoView !== false) {
|
||||
try { targetRow.scrollIntoView({ block: 'nearest' }); } catch (_) {}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function selectActiveTreeDocument(nodeId, treeView, options) {
|
||||
if (treeView === 'filetree') {
|
||||
selectSidebarFileTreeDocument(nodeId, options);
|
||||
return;
|
||||
}
|
||||
selectSidebarPageTreeDocument(nodeId, options);
|
||||
}
|
||||
|
||||
function navigateToDocument(nodeId, workspaceId, options) {
|
||||
if (!nodeId) return;
|
||||
var treeView = normalizeSidebarTreeMode(options && options.treeView ? options.treeView : activeSidebarTreeMode());
|
||||
@@ -49,22 +94,7 @@ export const createSidebarPageTreeRuntime = (dependencies = {}) => {
|
||||
if (typeof window.__mnoteDocumentPaneRuntime?.activatePageTab === 'function') {
|
||||
window.__mnoteDocumentPaneRuntime.activatePageTab();
|
||||
}
|
||||
document.querySelectorAll('.tree-row[data-active="true"], .tree-row[data-selected="true"]').forEach(function(row) {
|
||||
if (row instanceof HTMLElement) {
|
||||
row.setAttribute('data-active', 'false');
|
||||
row.setAttribute('data-selected', 'false');
|
||||
}
|
||||
});
|
||||
document.querySelectorAll('.tree-row[data-node-id="' + cssEscape(nodeId) + '"], .tree-row[data-document-id="' + cssEscape(nodeId) + '"], .tree-row[data-doc-id="' + cssEscape(nodeId) + '"]').forEach(function(row) {
|
||||
if (row instanceof HTMLElement) {
|
||||
if (row.getAttribute('data-shell-mode') === 'filetree') {
|
||||
row.setAttribute('data-selected', String(row.getAttribute('data-row-id') === 'doc:' + nodeId));
|
||||
} else {
|
||||
row.setAttribute('data-active', 'true');
|
||||
}
|
||||
}
|
||||
});
|
||||
selectSidebarFileTreeDocument(nodeId, { scrollIntoView: true });
|
||||
selectActiveTreeDocument(nodeId, treeView, { scrollIntoView: true });
|
||||
return;
|
||||
}
|
||||
var targetUrl = new URL('/documents/' + encodeURIComponent(nodeId), window.location.origin);
|
||||
@@ -88,7 +118,9 @@ export const createSidebarPageTreeRuntime = (dependencies = {}) => {
|
||||
}).then(function(){
|
||||
if (mnoteNavigationInFlight === url) mnoteNavigationInFlight = '';
|
||||
document.documentElement.removeAttribute('data-mnote-navigation-pending');
|
||||
selectSidebarFileTreeDocument(nodeId, { scrollIntoView: true });
|
||||
if (treeView === 'filetree') {
|
||||
selectSidebarFileTreeDocument(nodeId, { scrollIntoView: true });
|
||||
}
|
||||
}).catch(function(error){
|
||||
console.warn('mnote pane 内导航失败,将回退整页导航', error);
|
||||
if (window.__mnoteTreeLiveEventSource && typeof window.__mnoteTreeLiveEventSource.close === 'function') {
|
||||
@@ -110,6 +142,7 @@ export const createSidebarPageTreeRuntime = (dependencies = {}) => {
|
||||
async function createPage(trigger, parentId) {
|
||||
var workspaceId = resolveWorkspaceId(trigger);
|
||||
var effectiveParentId = (parentId || trigger.getAttribute('data-parent-id') || '').trim();
|
||||
var treeView = activeSidebarTreeMode();
|
||||
if (!workspaceId) return;
|
||||
var result = await dispatchTreeCommand(trigger, {
|
||||
action: 'create',
|
||||
@@ -126,10 +159,12 @@ export const createSidebarPageTreeRuntime = (dependencies = {}) => {
|
||||
if (typeof refreshLocalFolderAfterCommand === 'function') {
|
||||
await refreshLocalFolderAfterCommand('create', result, { parentId: effectiveParentId || null });
|
||||
}
|
||||
selectSidebarFileTreeDocument(nextDocumentId, localCreateSelectionOptions(result));
|
||||
if (treeView === 'filetree') {
|
||||
selectSidebarFileTreeDocument(nextDocumentId, localCreateSelectionOptions(result));
|
||||
}
|
||||
document.documentElement.setAttribute('data-mnote-create-page-selected-document-id', nextDocumentId);
|
||||
}
|
||||
navigateToDocument(nextDocumentId, nextWorkspaceId, { treeView: activeSidebarTreeMode() });
|
||||
navigateToDocument(nextDocumentId, nextWorkspaceId, { treeView: treeView });
|
||||
}
|
||||
|
||||
function startPageDrag(pageRow, event) {
|
||||
@@ -259,6 +294,7 @@ export const createSidebarPageTreeRuntime = (dependencies = {}) => {
|
||||
return {
|
||||
navigateToDocument,
|
||||
createPage,
|
||||
selectSidebarPageTreeDocument,
|
||||
startPageDrag,
|
||||
readPageDragNodeId,
|
||||
clearPageDropFeedback,
|
||||
|
||||
@@ -46,6 +46,7 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
var fileTreeVisibleHydrateTimer = 0;
|
||||
var fileTreeVisibleHydrateQueued = false;
|
||||
var fileTreeCommandBatchRefreshParents = new Map();
|
||||
var fileTreeRevealRequests = new Map();
|
||||
var FILETREE_VISIBLE_EXPANDED_HYDRATE_MAX = 20;
|
||||
var FILETREE_VISIBLE_EXPANDED_HYDRATE_BATCH = 2;
|
||||
var FILETREE_EXPANSION_STORAGE_KEY = 'mnote.localFileTree.expandedRelativePaths.v1';
|
||||
@@ -1795,32 +1796,46 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
ensureFileTreeLazyCacheScope();
|
||||
var relativePath = normalizeFileTreeRelativePath(input.relativePath || input.relative_path || '');
|
||||
var rowId = String(input.rowId || input.row_id || '').trim();
|
||||
var parentChain = fileTreeParentChainForRelativePath(relativePath);
|
||||
for (var index = 0; index < parentChain.length; index += 1) {
|
||||
var parentRelativePath = parentChain[index];
|
||||
var parentRow = fileTreeRowByRelativePath(parentRelativePath);
|
||||
if (!(parentRow instanceof HTMLElement)) continue;
|
||||
var button = parentRow.querySelector('[data-rust-action="toggle"]');
|
||||
await getFileTreeChildren(parentRelativePath);
|
||||
renderCachedFileTreeChildren(parentRow, button, parentRelativePath);
|
||||
setTreeRowExpanded(parentRow, button, true);
|
||||
}
|
||||
var targetRow = rowId
|
||||
? document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(rowId) + '"]')
|
||||
: null;
|
||||
if (!(targetRow instanceof HTMLElement) && relativePath) {
|
||||
targetRow = fileTreeRowByRelativePath(relativePath);
|
||||
}
|
||||
if (!(targetRow instanceof HTMLElement)) return false;
|
||||
var targetRowId = String(targetRow.getAttribute('data-row-id') || rowId || '').trim();
|
||||
if (targetRowId) {
|
||||
fileTreeViewState.selectedRowIds = new Set([targetRowId]);
|
||||
fileTreeViewState.focusedRowId = targetRowId;
|
||||
fileTreeViewState.activeRowId = targetRowId;
|
||||
}
|
||||
reprojectFileTreeSelectionState();
|
||||
try { targetRow.scrollIntoView({ block: 'nearest' }); } catch (_) {}
|
||||
return true;
|
||||
var requestKey = [rootUri, currentFileTreeScope(), relativePath, rowId].join('\n');
|
||||
if (fileTreeRevealRequests.has(requestKey)) return fileTreeRevealRequests.get(requestKey);
|
||||
var request = (async function() {
|
||||
var parentChain = fileTreeParentChainForRelativePath(relativePath);
|
||||
for (var index = 0; index < parentChain.length; index += 1) {
|
||||
var parentRelativePath = parentChain[index];
|
||||
var parentRow = fileTreeRowByRelativePath(parentRelativePath);
|
||||
if (!(parentRow instanceof HTMLElement)) continue;
|
||||
var button = parentRow.querySelector('[data-rust-action="toggle"]');
|
||||
var childrenLoaded = parentRow.getAttribute('data-filetree-children-loaded') === 'true';
|
||||
var expanded = parentRow.getAttribute('aria-expanded') === 'true';
|
||||
if (!childrenLoaded) {
|
||||
await getFileTreeChildren(parentRelativePath);
|
||||
renderCachedFileTreeChildren(parentRow, button, parentRelativePath);
|
||||
}
|
||||
if (!expanded) {
|
||||
setTreeRowExpanded(parentRow, button, true);
|
||||
}
|
||||
}
|
||||
var targetRow = rowId
|
||||
? document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(rowId) + '"]')
|
||||
: null;
|
||||
if (!(targetRow instanceof HTMLElement) && relativePath) {
|
||||
targetRow = fileTreeRowByRelativePath(relativePath);
|
||||
}
|
||||
if (!(targetRow instanceof HTMLElement)) return false;
|
||||
var targetRowId = String(targetRow.getAttribute('data-row-id') || rowId || '').trim();
|
||||
if (targetRowId) {
|
||||
fileTreeViewState.selectedRowIds = new Set([targetRowId]);
|
||||
fileTreeViewState.focusedRowId = targetRowId;
|
||||
fileTreeViewState.activeRowId = targetRowId;
|
||||
}
|
||||
reprojectFileTreeSelectionState();
|
||||
try { targetRow.scrollIntoView({ block: 'nearest' }); } catch (_) {}
|
||||
return true;
|
||||
})().finally(function() {
|
||||
fileTreeRevealRequests.delete(requestKey);
|
||||
});
|
||||
fileTreeRevealRequests.set(requestKey, request);
|
||||
return request;
|
||||
}
|
||||
|
||||
function toggleChildren(row, button) {
|
||||
|
||||
@@ -221,6 +221,8 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
const loadKnowledgeRagStatus = (...args) => sidebarPageSettings.loadKnowledgeRagStatus(...args);
|
||||
const openKnowledgeRagDashboard = (...args) => sidebarPageSettings.openKnowledgeRagDashboard(...args);
|
||||
const createKnowledgeRagKnowledgeBase = (...args) => sidebarPageSettings.createKnowledgeRagKnowledgeBase(...args);
|
||||
const deleteKnowledgeRagKnowledgeBase = (...args) => sidebarPageSettings.deleteKnowledgeRagKnowledgeBase(...args);
|
||||
const toggleKnowledgeRagCreateCard = (...args) => sidebarPageSettings.toggleKnowledgeRagCreateCard(...args);
|
||||
const setActiveKnowledgeRagDetailTab = (...args) => sidebarPageSettings.setActiveKnowledgeRagDetailTab(...args);
|
||||
const performKnowledgeRagSearch = (...args) => sidebarPageSettings.performKnowledgeRagSearch(...args);
|
||||
const saveKnowledgeRagSearchParams = (...args) => sidebarPageSettings.saveKnowledgeRagSearchParams(...args);
|
||||
@@ -367,6 +369,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
openPageTreeContextMenu: (...args) => openPageTreeContextMenu(...args),
|
||||
updateTitleEverywhere: (...args) => updateTitleEverywhere(...args),
|
||||
});
|
||||
const selectSidebarPageTreeDocument = (...args) => sidebarPageTree.selectSidebarPageTreeDocument(...args);
|
||||
const fileTreePageTitle = (...args) => sidebarTreeLiveApply.fileTreePageTitle(...args);
|
||||
const isFileTreePageRow = (...args) => sidebarTreeLiveApply.isFileTreePageRow(...args);
|
||||
const normalizeFileTreePageRenameTitle = (...args) => sidebarTreeLiveApply.normalizeFileTreePageRenameTitle(...args);
|
||||
@@ -476,6 +479,22 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
return true;
|
||||
}
|
||||
|
||||
function knowledgeHostUrl(options) {
|
||||
options = options || {};
|
||||
var knowledgeUrl = new URL('/knowledge', window.location.origin);
|
||||
var workspaceId = String(options.workspaceId || currentWorkspaceId() || resolveWorkspaceId(document.body) || '').trim();
|
||||
var sourceKind = String(options.sourceKind || currentSourceKind() || '').trim();
|
||||
var rootUri = String(options.rootUri || currentRootUri() || '').trim();
|
||||
var treeView = String(options.treeView || activeSidebarTreeMode() || '').trim();
|
||||
var fileTreeScope = String(options.fileTreeScope || currentFileTreeScope() || '').trim();
|
||||
if (workspaceId) knowledgeUrl.searchParams.set('workspaceId', workspaceId);
|
||||
if (sourceKind) knowledgeUrl.searchParams.set('sourceKind', sourceKind);
|
||||
if (rootUri) knowledgeUrl.searchParams.set('rootUri', rootUri);
|
||||
if (treeView) knowledgeUrl.searchParams.set('treeView', treeView);
|
||||
if (fileTreeScope) knowledgeUrl.searchParams.set('fileTreeScope', fileTreeScope);
|
||||
return knowledgeUrl.pathname + knowledgeUrl.search;
|
||||
}
|
||||
|
||||
function currentTopbarTitle() {
|
||||
var title = document.querySelector('[data-page-title-current="true"]');
|
||||
return title && title.textContent ? title.textContent.trim() : '无标题';
|
||||
@@ -2058,6 +2077,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
openEditorAttachmentEditTab: (...args) => openEditorAttachmentEditTab(...args),
|
||||
openEditorAttachmentNewWindow: (...args) => openEditorAttachmentNewWindow(...args),
|
||||
openLocalResourceInActiveTab: (...args) => openLocalResourceInActiveTab(...args),
|
||||
knowledgeHostUrl,
|
||||
refreshLocalFolderSidebarSnapshot,
|
||||
removeFileTreeAssetRow,
|
||||
revealFileTreeResource,
|
||||
@@ -2162,7 +2182,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
'<span class="wolai-search-sort-control"><span>按创建时间</span><button type="button" class="wolai-search-sort-value" data-search-sort="created" aria-label="按创建时间范围">所有</button></span>' +
|
||||
'</div>' +
|
||||
'<div class="wolai-search-options-right">' +
|
||||
'<span class="wolai-search-switch-control"><span>全盘知识库</span><button type="button" class="wolai-search-switch is-on" data-search-switch="knowledge" role="switch" aria-checked="true" aria-label="全盘知识库检索"></button></span>' +
|
||||
'<span class="wolai-search-switch-control"><span>全盘知识库</span><button type="button" class="wolai-search-switch" data-search-switch="knowledge" role="switch" aria-checked="false" aria-label="全盘知识库检索"></button></span>' +
|
||||
'<span class="wolai-search-switch-control"><span>页面内搜索</span><button type="button" class="wolai-search-switch" data-search-switch="page" role="switch" aria-checked="false" aria-label="页面内搜索"></button></span>' +
|
||||
'<span class="wolai-search-switch-control"><span>折叠同来源</span><button type="button" class="wolai-search-switch is-on" data-search-switch="collapseSource" role="switch" aria-checked="true" aria-label="默认折叠相同来源结果"></button></span>' +
|
||||
'</div>' +
|
||||
@@ -2174,7 +2194,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
document.body.appendChild(overlay);
|
||||
var input = overlay.querySelector('[data-testid="wolai-search-input"]');
|
||||
var closeButton = overlay.querySelector('[data-testid="wolai-search-close"]');
|
||||
applySearchSwitchState(overlay, { knowledge: true, collapseSource: readSearchCollapseSourcesDefault() });
|
||||
applySearchSwitchState(overlay, { knowledge: false, collapseSource: readSearchCollapseSourcesDefault() });
|
||||
var initialKnowledgeModeSelect = overlay.querySelector('[data-search-knowledge-mode]');
|
||||
if (initialKnowledgeModeSelect && 'value' in initialKnowledgeModeSelect) initialKnowledgeModeSelect.value = 'exact';
|
||||
if (input) input.addEventListener('input', function() {
|
||||
@@ -2986,10 +3006,20 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
return;
|
||||
}
|
||||
|
||||
var knowledgeHostTrigger = closestAction(e.target, '[data-mnote-action="open-knowledge-host"]');
|
||||
if (knowledgeHostTrigger) {
|
||||
var hostUrl = knowledgeHostUrl();
|
||||
if (knowledgeHostTrigger instanceof HTMLAnchorElement) knowledgeHostTrigger.setAttribute('href', hostUrl);
|
||||
if (knowledgeHostTrigger instanceof HTMLAnchorElement && (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1)) return;
|
||||
e.preventDefault();
|
||||
window.location.href = hostUrl;
|
||||
return;
|
||||
}
|
||||
|
||||
var knowledgeRagSettingsTrigger = closestAction(e.target, '[data-mnote-action="open-knowledge-rag-settings"]');
|
||||
if (knowledgeRagSettingsTrigger) {
|
||||
e.preventDefault();
|
||||
window.location.href = '/knowledge';
|
||||
window.location.href = knowledgeHostUrl();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3069,7 +3099,16 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
void createKnowledgeRagKnowledgeBase();
|
||||
return;
|
||||
}
|
||||
if (knowledgeRagActionName === 'delete-kb') {
|
||||
void deleteKnowledgeRagKnowledgeBase();
|
||||
return;
|
||||
}
|
||||
if (knowledgeRagActionName === 'toggle-create-kb') {
|
||||
toggleKnowledgeRagCreateCard();
|
||||
return;
|
||||
}
|
||||
if (knowledgeRagActionName === 'focus-create-kb') {
|
||||
toggleKnowledgeRagCreateCard();
|
||||
var createKbInput = document.querySelector('[data-knowledge-rag-create-kb-name]');
|
||||
if (createKbInput instanceof HTMLInputElement) createKbInput.focus();
|
||||
return;
|
||||
@@ -3870,21 +3909,44 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
});
|
||||
syncSidebarFileTreeSelection();
|
||||
schedulePendingLocalFolderRestoreFocus();
|
||||
window.addEventListener('mnote:primary-document-activated', function(event) {
|
||||
var detail = event && event.detail ? event.detail : {};
|
||||
var recentPageOptionsApply = { documentId: '', at: 0 };
|
||||
function applyPrimaryPageOptionsOnce(documentId) {
|
||||
var normalizedDocumentId = searchText(documentId || currentDocumentId() || '');
|
||||
pageUiState.pageOptions = null;
|
||||
applyPageOptionsToShell();
|
||||
if (isPageSettingsOpen()) renderPageSettingsPopover();
|
||||
selectSidebarFileTreeDocument(detail.documentId, { scrollIntoView: false });
|
||||
recentPageOptionsApply = { documentId: normalizedDocumentId, at: Date.now() };
|
||||
}
|
||||
function hasRecentPageOptionsApply(documentId) {
|
||||
var normalizedDocumentId = searchText(documentId || currentDocumentId() || '');
|
||||
return normalizedDocumentId
|
||||
&& recentPageOptionsApply.documentId === normalizedDocumentId
|
||||
&& Date.now() - recentPageOptionsApply.at < 1000;
|
||||
}
|
||||
window.addEventListener('mnote:primary-document-activated', function(event) {
|
||||
var detail = event && event.detail ? event.detail : {};
|
||||
if (!hasRecentPageOptionsApply(detail.documentId)) {
|
||||
applyPrimaryPageOptionsOnce(detail.documentId);
|
||||
}
|
||||
if (detail.pageBlockNavigation === true) {
|
||||
detail.sidebarSelectionHandled = false;
|
||||
return;
|
||||
}
|
||||
if (activeSidebarTreeMode() === 'filetree') {
|
||||
detail.sidebarSelectionHandled = selectSidebarFileTreeDocument(detail.documentId, {
|
||||
rootUri: detail.rootUri || currentRootUri(),
|
||||
scrollIntoView: false
|
||||
});
|
||||
} else {
|
||||
detail.sidebarSelectionHandled = selectSidebarPageTreeDocument(detail.documentId, { scrollIntoView: false });
|
||||
}
|
||||
});
|
||||
window.addEventListener('mnote:page-aggregate-synced', function(event) {
|
||||
var detail = event && event.detail ? event.detail : {};
|
||||
if (detail.scriptId && detail.scriptId !== '__MNOTE_PAGE_AGGREGATE__') return;
|
||||
var documentId = searchText(detail.documentId || '');
|
||||
if (documentId && currentDocumentId() && documentId !== currentDocumentId()) return;
|
||||
pageUiState.pageOptions = null;
|
||||
applyPageOptionsToShell();
|
||||
if (isPageSettingsOpen()) renderPageSettingsPopover();
|
||||
applyPrimaryPageOptionsOnce(documentId);
|
||||
});
|
||||
restoreSidebarTreeTab();
|
||||
if (window.location.pathname === '/debug/knowledge-rag' || window.location.pathname === '/knowledge') {
|
||||
|
||||
@@ -1017,8 +1017,19 @@ fn render_recent_navigation_items(records: &[NavigationRecentRecord]) -> String
|
||||
}
|
||||
|
||||
fn render_navigation_link(title: &str, href: &str, kind: &str, relative_path: &str) -> String {
|
||||
let is_page = kind == "page";
|
||||
let class_name = if is_page {
|
||||
"mnote-navigation-page__item mnote-page-block-link"
|
||||
} else {
|
||||
"mnote-navigation-page__item"
|
||||
};
|
||||
let page_attributes = if is_page {
|
||||
r#" data-area="page-block" data-block-type="page""#
|
||||
} else {
|
||||
""
|
||||
};
|
||||
format!(
|
||||
r#"<a class="mnote-navigation-page__item" data-navigation-item-kind="{}" data-local-relative-path="{}" href="{}">{}</a>"#,
|
||||
r#"<a class="{class_name}" data-navigation-item-kind="{}" data-local-relative-path="{}"{page_attributes} href="{}">{}</a>"#,
|
||||
escape_html(kind),
|
||||
escape_html(relative_path),
|
||||
escape_html(href),
|
||||
@@ -1079,7 +1090,7 @@ fn navigation_folder_href(root_uri: &str, relative_path: &str) -> String {
|
||||
|
||||
fn navigation_document_href(root_uri: &str, file_tree_scope: &str, document_id: &str) -> String {
|
||||
let mut href = format!(
|
||||
"/documents/{}?sourceKind=local_folder&rootUri={}&treeView=filetree",
|
||||
"/documents/{}?sourceKind=local_folder&rootUri={}",
|
||||
query_escape(document_id),
|
||||
query_escape(root_uri)
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ use axum::body::Body;
|
||||
use axum::extract::{Extension, Json, Query, State};
|
||||
use axum::http::{header, StatusCode};
|
||||
use axum::response::Response;
|
||||
use control_plane::{ControlPlaneError, KnowledgeBaseRecord, UpsertKnowledgeBaseInput};
|
||||
use core_protocol::evidence::{
|
||||
EvidenceBBox, EvidenceLocator, EvidenceOpenAction, EvidenceResourceKind,
|
||||
};
|
||||
@@ -131,6 +132,15 @@ pub struct KnowledgeRagCreateKnowledgeBaseRequest {
|
||||
pub default_tool_enabled: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct KnowledgeRagDeleteKnowledgeBaseRequest {
|
||||
pub workspace_id: Option<String>,
|
||||
pub root_uri: String,
|
||||
pub knowledge_base_id: Option<String>,
|
||||
pub provider_knowledge_base_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct KnowledgeRagOpenReferenceRequest {
|
||||
@@ -334,6 +344,170 @@ fn filter_knowledge_bases_for_actor(bases: &mut KnowledgeRagKnowledgeBaseRegistr
|
||||
.retain(|base| knowledge_base_visible_to_actor(base, actor_id));
|
||||
}
|
||||
|
||||
fn control_plane_knowledge_base_to_registry_base(
|
||||
record: &KnowledgeBaseRecord,
|
||||
) -> KnowledgeRagKnowledgeBase {
|
||||
KnowledgeRagKnowledgeBase {
|
||||
base_id: record.id.clone(),
|
||||
user_id: record.user_id.clone(),
|
||||
workspace_id: record.workspace_id.clone().unwrap_or_default(),
|
||||
root_uri: record.root_uri.clone().unwrap_or_default(),
|
||||
name: record.name.clone(),
|
||||
description: record.description.clone(),
|
||||
provider: record.provider.clone(),
|
||||
provider_kb_id: record.provider_kb_id.clone(),
|
||||
default_tool_enabled: record.default_tool_enabled,
|
||||
can_write: record.can_write,
|
||||
source_count: record.source_count.max(0) as usize,
|
||||
chunk_count: record.chunk_count.max(0) as usize,
|
||||
status: record.status.clone(),
|
||||
created_at_ms: 0,
|
||||
updated_at_ms: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn control_plane_knowledge_base_error(
|
||||
error: ControlPlaneError,
|
||||
context: &RequestContext,
|
||||
) -> WebError {
|
||||
match error {
|
||||
ControlPlaneError::NotFound(message) => WebError::new(
|
||||
StatusCode::NOT_FOUND,
|
||||
"knowledge_rag_control_plane_kb_not_found",
|
||||
message,
|
||||
),
|
||||
ControlPlaneError::Conflict(message) => WebError::new(
|
||||
StatusCode::CONFLICT,
|
||||
"knowledge_rag_control_plane_kb_conflict",
|
||||
message,
|
||||
),
|
||||
ControlPlaneError::InvalidInput(message) => {
|
||||
WebError::bad_request_code("knowledge_rag_control_plane_kb_invalid", message)
|
||||
}
|
||||
ControlPlaneError::Unauthorized(message) | ControlPlaneError::SessionExpired(message) => {
|
||||
WebError::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
"knowledge_rag_control_plane_kb_forbidden",
|
||||
message,
|
||||
)
|
||||
}
|
||||
ControlPlaneError::Timeout(message) => {
|
||||
WebError::gateway_timeout_code("knowledge_rag_control_plane_kb_timeout", message)
|
||||
}
|
||||
ControlPlaneError::RateLimit(message) => WebError::new(
|
||||
StatusCode::TOO_MANY_REQUESTS,
|
||||
"knowledge_rag_control_plane_kb_rate_limited",
|
||||
message,
|
||||
),
|
||||
ControlPlaneError::Storage(message) => WebError::service_unavailable_code(
|
||||
"knowledge_rag_control_plane_kb_storage_failed",
|
||||
message,
|
||||
),
|
||||
}
|
||||
.with_context(context)
|
||||
}
|
||||
|
||||
fn control_plane_knowledge_bases_registry(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
workspace_id: &str,
|
||||
root_uri: &str,
|
||||
) -> Result<KnowledgeRagKnowledgeBaseRegistry, WebError> {
|
||||
let records = state
|
||||
.control_plane()
|
||||
.list_knowledge_bases(
|
||||
context.auth.actor_id.as_str(),
|
||||
Some(workspace_id),
|
||||
Some(root_uri),
|
||||
)
|
||||
.map_err(|error| control_plane_knowledge_base_error(error, context))?;
|
||||
Ok(KnowledgeRagKnowledgeBaseRegistry {
|
||||
schema: KNOWLEDGE_BASES_SCHEMA.to_string(),
|
||||
workspace_id: workspace_id.to_string(),
|
||||
root_uri: root_uri.to_string(),
|
||||
updated_at_ms: now_ms(),
|
||||
bases: records
|
||||
.iter()
|
||||
.map(control_plane_knowledge_base_to_registry_base)
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
fn knowledge_base_source_counts(
|
||||
sources: &KnowledgeRagSourceRegistry,
|
||||
provider_kb_id: &str,
|
||||
) -> (i64, i64) {
|
||||
let source_count = sources
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|entry| {
|
||||
!entry.stale
|
||||
&& entry.deleted_at_ms.is_none()
|
||||
&& entry.provider_knowledge_base_id.as_deref() == Some(provider_kb_id)
|
||||
})
|
||||
.count() as i64;
|
||||
(source_count, 0)
|
||||
}
|
||||
|
||||
fn control_plane_knowledge_base_upsert_input(
|
||||
record: &KnowledgeBaseRecord,
|
||||
source_count: i64,
|
||||
chunk_count: i64,
|
||||
) -> UpsertKnowledgeBaseInput {
|
||||
UpsertKnowledgeBaseInput {
|
||||
id: Some(record.id.clone()),
|
||||
user_id: record.user_id.clone(),
|
||||
workspace_id: record.workspace_id.clone(),
|
||||
root_uri: record.root_uri.clone(),
|
||||
name: record.name.clone(),
|
||||
description: record.description.clone(),
|
||||
provider: record.provider.clone(),
|
||||
provider_kb_id: record.provider_kb_id.clone(),
|
||||
default_tool_enabled: record.default_tool_enabled,
|
||||
can_write: record.can_write,
|
||||
source_count,
|
||||
chunk_count,
|
||||
status: record.status.clone(),
|
||||
metadata_json: record.metadata_json.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_lightrag_control_plane_knowledge_base_counts(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
workspace_id: &str,
|
||||
root_uri: &str,
|
||||
sources: &KnowledgeRagSourceRegistry,
|
||||
) -> Result<(), WebError> {
|
||||
let records = state
|
||||
.control_plane()
|
||||
.list_knowledge_bases(
|
||||
context.auth.actor_id.as_str(),
|
||||
Some(workspace_id),
|
||||
Some(root_uri),
|
||||
)
|
||||
.map_err(|error| control_plane_knowledge_base_error(error, context))?;
|
||||
for record in records
|
||||
.iter()
|
||||
.filter(|record| is_legacy_lightrag_provider(&record.provider))
|
||||
{
|
||||
let (source_count, chunk_count) =
|
||||
knowledge_base_source_counts(sources, &record.provider_kb_id);
|
||||
if record.source_count == source_count && record.chunk_count == chunk_count {
|
||||
continue;
|
||||
}
|
||||
state
|
||||
.control_plane()
|
||||
.upsert_knowledge_base(control_plane_knowledge_base_upsert_input(
|
||||
record,
|
||||
source_count,
|
||||
chunk_count,
|
||||
))
|
||||
.map_err(|error| control_plane_knowledge_base_error(error, context))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn registry_owner_for_provider_kb(
|
||||
sources: &KnowledgeRagSourceRegistry,
|
||||
provider_kb_id: &str,
|
||||
@@ -473,25 +647,26 @@ pub async fn status(
|
||||
} else {
|
||||
(None, Value::Array(Vec::new()))
|
||||
};
|
||||
let knowledge_bases = if active_knowledge_provider_kind() == KnowledgeProvider::Weknora {
|
||||
if let Some(root_uri) = query.root_uri.as_deref().filter(|value| !value.is_empty()) {
|
||||
let root_path = local_folder_source::ensure_local_workspace_read_access_with_state(
|
||||
&state, &context, root_uri,
|
||||
)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
let workspace_id = effective_workspace_id(query.workspace_id.as_deref(), root_uri);
|
||||
let registry =
|
||||
registry
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| KnowledgeRagSourceRegistry {
|
||||
schema: REGISTRY_SCHEMA.to_string(),
|
||||
workspace_id: workspace_id.clone(),
|
||||
root_uri: root_uri.to_string(),
|
||||
updated_at_ms: now_ms(),
|
||||
indexed_roots: Vec::new(),
|
||||
entries: Vec::new(),
|
||||
});
|
||||
let knowledge_bases = if let Some(root_uri) =
|
||||
query.root_uri.as_deref().filter(|value| !value.is_empty())
|
||||
{
|
||||
let root_path = local_folder_source::ensure_local_workspace_read_access_with_state(
|
||||
&state, &context, root_uri,
|
||||
)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
let workspace_id = effective_workspace_id(query.workspace_id.as_deref(), root_uri);
|
||||
let registry = registry
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| KnowledgeRagSourceRegistry {
|
||||
schema: REGISTRY_SCHEMA.to_string(),
|
||||
workspace_id: workspace_id.clone(),
|
||||
root_uri: root_uri.to_string(),
|
||||
updated_at_ms: now_ms(),
|
||||
indexed_roots: Vec::new(),
|
||||
entries: Vec::new(),
|
||||
});
|
||||
if active_knowledge_provider_kind() == KnowledgeProvider::Weknora {
|
||||
let mut bases = read_knowledge_bases_registry(&root_path, &workspace_id, root_uri)?;
|
||||
ensure_default_weknora_knowledge_bases(
|
||||
&mut bases,
|
||||
@@ -510,7 +685,19 @@ pub async fn status(
|
||||
filter_knowledge_bases_for_actor(&mut visible_bases, &context.auth.actor_id);
|
||||
Some(visible_bases)
|
||||
} else {
|
||||
None
|
||||
sync_lightrag_control_plane_knowledge_base_counts(
|
||||
&state,
|
||||
&context,
|
||||
&workspace_id,
|
||||
root_uri,
|
||||
®istry,
|
||||
)?;
|
||||
Some(control_plane_knowledge_bases_registry(
|
||||
&state,
|
||||
&context,
|
||||
&workspace_id,
|
||||
root_uri,
|
||||
)?)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
@@ -658,13 +845,6 @@ pub async fn create_knowledge_base(
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Json(body): Json<KnowledgeRagCreateKnowledgeBaseRequest>,
|
||||
) -> Result<Json<Value>, WebError> {
|
||||
if active_knowledge_provider() != WEKNORA_PROVIDER {
|
||||
return Err(WebError::bad_request_code(
|
||||
"knowledge_rag_create_kb_provider_unsupported",
|
||||
"当前只有 WeKnora provider 支持从 MNote 创建知识库",
|
||||
)
|
||||
.with_context(&context));
|
||||
}
|
||||
let name = body.name.trim();
|
||||
if name.is_empty() {
|
||||
return Err(WebError::bad_request_code(
|
||||
@@ -681,6 +861,71 @@ pub async fn create_knowledge_base(
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
let workspace_id = effective_workspace_id(body.workspace_id.as_deref(), &body.root_uri);
|
||||
let mut source_registry = read_registry(&root_path, &workspace_id, &body.root_uri)?;
|
||||
|
||||
if active_knowledge_provider_kind() != KnowledgeProvider::Weknora {
|
||||
let now = now_ms();
|
||||
let provider_kb_id = format!(
|
||||
"lightrag-kb-{}",
|
||||
short_hash(&format!(
|
||||
"{}|{}|{}|{}",
|
||||
context.auth.actor_id, workspace_id, body.root_uri, now
|
||||
))
|
||||
);
|
||||
let (source_count, chunk_count) =
|
||||
knowledge_base_source_counts(&source_registry, &provider_kb_id);
|
||||
let record = state
|
||||
.control_plane()
|
||||
.upsert_knowledge_base(UpsertKnowledgeBaseInput {
|
||||
id: None,
|
||||
user_id: context.auth.actor_id.clone(),
|
||||
workspace_id: Some(workspace_id.clone()),
|
||||
root_uri: Some(body.root_uri.clone()),
|
||||
name: name.to_string(),
|
||||
description: body
|
||||
.description
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned),
|
||||
provider: LEGACY_KNOWLEDGE_PROVIDER.to_string(),
|
||||
provider_kb_id: provider_kb_id.clone(),
|
||||
default_tool_enabled: body.default_tool_enabled.unwrap_or(true),
|
||||
can_write: true,
|
||||
source_count,
|
||||
chunk_count,
|
||||
status: "active".into(),
|
||||
metadata_json: json!({
|
||||
"source": "mnote.knowledge_rag",
|
||||
"workspaceId": workspace_id.clone(),
|
||||
"rootUri": body.root_uri.clone(),
|
||||
})
|
||||
.to_string(),
|
||||
})
|
||||
.map_err(|error| control_plane_knowledge_base_error(error, &context))?;
|
||||
let bases = control_plane_knowledge_bases_registry(
|
||||
&state,
|
||||
&context,
|
||||
&record.workspace_id.clone().unwrap_or_default(),
|
||||
record.root_uri.as_deref().unwrap_or(body.root_uri.as_str()),
|
||||
)?;
|
||||
let base = Some(control_plane_knowledge_base_to_registry_base(&record));
|
||||
return Ok(Json(json!({
|
||||
"ok": true,
|
||||
"schema": "mnote.knowledge_rag.create_knowledge_base_result.v1",
|
||||
"provider": LEGACY_KNOWLEDGE_PROVIDER,
|
||||
"providerConfig": knowledge_provider_config(),
|
||||
"knowledgeBase": base,
|
||||
"providerKnowledgeBaseId": provider_kb_id,
|
||||
"knowledgeBases": bases,
|
||||
"registry": {
|
||||
"schema": source_registry.schema,
|
||||
"workspaceId": source_registry.workspace_id,
|
||||
"rootUri": source_registry.root_uri,
|
||||
"sourceCount": source_registry.entries.len(),
|
||||
},
|
||||
})));
|
||||
}
|
||||
|
||||
let mut bases = read_knowledge_bases_registry(&root_path, &workspace_id, &body.root_uri)?;
|
||||
ensure_default_weknora_knowledge_bases(
|
||||
&mut bases,
|
||||
@@ -750,6 +995,95 @@ pub async fn create_knowledge_base(
|
||||
})))
|
||||
}
|
||||
|
||||
pub async fn delete_knowledge_base(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Json(body): Json<KnowledgeRagDeleteKnowledgeBaseRequest>,
|
||||
) -> Result<Json<Value>, WebError> {
|
||||
let requested_kb_id = body
|
||||
.provider_knowledge_base_id
|
||||
.as_deref()
|
||||
.or(body.knowledge_base_id.as_deref())
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| {
|
||||
WebError::bad_request_code(
|
||||
"knowledge_rag_delete_kb_id_required",
|
||||
"删除资料库缺少 knowledgeBaseId",
|
||||
)
|
||||
.with_context(&context)
|
||||
})?;
|
||||
if requested_kb_id == "lightrag-default" {
|
||||
return Err(WebError::bad_request_code(
|
||||
"knowledge_rag_delete_default_kb_forbidden",
|
||||
"默认资料库不能删除",
|
||||
)
|
||||
.with_context(&context));
|
||||
}
|
||||
let root_path = local_folder_source::ensure_local_workspace_read_access_with_state(
|
||||
&state,
|
||||
&context,
|
||||
&body.root_uri,
|
||||
)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
let workspace_id = effective_workspace_id(body.workspace_id.as_deref(), &body.root_uri);
|
||||
let mut registry = read_registry(&root_path, &workspace_id, &body.root_uri)?;
|
||||
filter_source_registry_for_actor(&mut registry, &context.auth.actor_id);
|
||||
let active_source_count = registry
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|entry| {
|
||||
!entry.stale
|
||||
&& entry.deleted_at_ms.is_none()
|
||||
&& entry.provider_knowledge_base_id.as_deref() == Some(requested_kb_id)
|
||||
})
|
||||
.count();
|
||||
if active_source_count > 0 {
|
||||
return Err(WebError::new(
|
||||
StatusCode::CONFLICT,
|
||||
"knowledge_rag_delete_kb_not_empty",
|
||||
"请先移除该资料库中的资料,再删除资料库",
|
||||
)
|
||||
.with_context(&context));
|
||||
}
|
||||
let bases =
|
||||
control_plane_knowledge_bases_registry(&state, &context, &workspace_id, &body.root_uri)?;
|
||||
let base = bases
|
||||
.bases
|
||||
.iter()
|
||||
.find(|base| base.base_id == requested_kb_id || base.provider_kb_id == requested_kb_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| {
|
||||
WebError::new(
|
||||
StatusCode::NOT_FOUND,
|
||||
"knowledge_rag_delete_kb_not_found",
|
||||
"资料库不存在,或当前用户无权删除",
|
||||
)
|
||||
.with_context(&context)
|
||||
})?;
|
||||
if !base.can_write {
|
||||
return Err(WebError::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
"knowledge_rag_delete_kb_readonly",
|
||||
"当前资料库不可删除",
|
||||
)
|
||||
.with_context(&context));
|
||||
}
|
||||
state
|
||||
.control_plane()
|
||||
.delete_knowledge_base(context.auth.actor_id.as_str(), &base.base_id)
|
||||
.map_err(|error| control_plane_knowledge_base_error(error, &context))?;
|
||||
let knowledge_bases =
|
||||
control_plane_knowledge_bases_registry(&state, &context, &workspace_id, &body.root_uri)?;
|
||||
Ok(Json(json!({
|
||||
"ok": true,
|
||||
"schema": "mnote.knowledge_rag.delete_knowledge_base_result.v1",
|
||||
"providerKnowledgeBaseId": base.provider_kb_id,
|
||||
"knowledgeBaseId": base.base_id,
|
||||
"knowledgeBases": knowledge_bases,
|
||||
})))
|
||||
}
|
||||
|
||||
pub(crate) async fn sync_registry_for_root(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
@@ -808,6 +1142,17 @@ pub async fn ingest(
|
||||
&context,
|
||||
));
|
||||
}
|
||||
let lightrag_requested_provider_kb_id = requested_lightrag_knowledge_base_id(
|
||||
body.knowledge_base_id.as_deref(),
|
||||
body.provider_knowledge_base_id.as_deref(),
|
||||
);
|
||||
let lightrag_bases =
|
||||
control_plane_knowledge_bases_registry(&state, &context, &workspace_id, &body.root_uri)?;
|
||||
let provider_kb_id = scoped_lightrag_provider_kb_id_for_registry(
|
||||
&lightrag_bases,
|
||||
lightrag_requested_provider_kb_id.as_deref(),
|
||||
&context,
|
||||
)?;
|
||||
let input_dir = lightrag_input_dir();
|
||||
fs::create_dir_all(&input_dir).map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
@@ -903,6 +1248,8 @@ pub async fn ingest(
|
||||
.iter()
|
||||
.filter(|entry| {
|
||||
entry.source_path == canonical_key
|
||||
&& entry.provider_knowledge_base_id.as_deref()
|
||||
== provider_kb_id.as_deref()
|
||||
&& entry.deleted_at_ms.is_none()
|
||||
&& !entry.stale
|
||||
})
|
||||
@@ -919,6 +1266,7 @@ pub async fn ingest(
|
||||
if !force {
|
||||
if let Some(existing) = registry.entries.iter().find(|entry| {
|
||||
entry.source_path == canonical_key
|
||||
&& entry.provider_knowledge_base_id.as_deref() == provider_kb_id.as_deref()
|
||||
&& entry.source_hash == source_hash
|
||||
&& entry.deleted_at_ms.is_none()
|
||||
&& !entry.stale
|
||||
@@ -937,6 +1285,7 @@ pub async fn ingest(
|
||||
"lightRagFilePath": existing.light_rag_file_path,
|
||||
"lightRagDocId": existing.light_rag_doc_id,
|
||||
"lightRagStatus": existing.light_rag_status,
|
||||
"providerKnowledgeBaseId": provider_kb_id,
|
||||
"skipped": true,
|
||||
"skipReason": "already_registered",
|
||||
}));
|
||||
@@ -955,7 +1304,11 @@ pub async fn ingest(
|
||||
let now = now_ms();
|
||||
let source_id = format!(
|
||||
"lightrag-source-{}",
|
||||
short_hash(&canonical.display().to_string())
|
||||
short_hash(&format!(
|
||||
"{}:{}",
|
||||
canonical.display(),
|
||||
provider_kb_id.as_deref().unwrap_or_default()
|
||||
))
|
||||
);
|
||||
upsert_registry_entry(
|
||||
&mut registry,
|
||||
@@ -975,7 +1328,7 @@ pub async fn ingest(
|
||||
provider_status: Some("submitted".into()),
|
||||
provider_source_id: None,
|
||||
provider_knowledge_id: None,
|
||||
provider_knowledge_base_id: None,
|
||||
provider_knowledge_base_id: provider_kb_id.clone(),
|
||||
light_rag_doc_id: None,
|
||||
light_rag_status: Some("submitted".into()),
|
||||
light_rag_file_path: light_rag_file_path.clone(),
|
||||
@@ -989,7 +1342,7 @@ pub async fn ingest(
|
||||
&relative,
|
||||
resolved.source_kind,
|
||||
LEGACY_KNOWLEDGE_PROVIDER,
|
||||
None,
|
||||
provider_kb_id.as_deref(),
|
||||
None,
|
||||
)),
|
||||
deleted_at_ms: None,
|
||||
@@ -1009,11 +1362,19 @@ pub async fn ingest(
|
||||
"parserHint": parser_hint,
|
||||
"providerDelete": provider_delete,
|
||||
"scanMode": if direct_scan_source { "direct" } else { "markdown_wrapper" },
|
||||
"providerKnowledgeBaseId": provider_kb_id,
|
||||
"lightRagStatus": "submitted",
|
||||
}));
|
||||
}
|
||||
}
|
||||
write_registry(&root_path, &mut registry)?;
|
||||
sync_lightrag_control_plane_knowledge_base_counts(
|
||||
&state,
|
||||
&context,
|
||||
&workspace_id,
|
||||
&body.root_uri,
|
||||
®istry,
|
||||
)?;
|
||||
|
||||
let scan = lightrag_json(
|
||||
reqwest::Method::POST,
|
||||
@@ -1500,6 +1861,17 @@ pub async fn query_rag(
|
||||
&context,
|
||||
));
|
||||
}
|
||||
let requested_provider_kb_id = requested_lightrag_knowledge_base_id(
|
||||
body.knowledge_base_id.as_deref(),
|
||||
body.provider_knowledge_base_id.as_deref(),
|
||||
);
|
||||
let lightrag_bases =
|
||||
control_plane_knowledge_bases_registry(&state, &context, &workspace_id, &body.root_uri)?;
|
||||
let scoped_provider_kb_id = scoped_lightrag_provider_kb_id_for_registry(
|
||||
&lightrag_bases,
|
||||
requested_provider_kb_id.as_deref(),
|
||||
&context,
|
||||
)?;
|
||||
let requested_mode = normalize_lightrag_query_mode(body.mode.as_deref());
|
||||
let mode_decision =
|
||||
resolve_lightrag_query_mode_for_scope(®istry, &source_scope, &requested_mode);
|
||||
@@ -1523,6 +1895,7 @@ pub async fn query_rag(
|
||||
let mut references =
|
||||
mapped_references(&raw, ®istry, &body.root_uri, &root_path, Some(&query));
|
||||
filter_mapped_references_by_source_scope(&mut references, &source_scope);
|
||||
filter_mapped_references_by_provider_kb_id(&mut references, scoped_provider_kb_id.as_deref());
|
||||
let include_document_structure_index = body
|
||||
.include_document_structure_index
|
||||
.unwrap_or_else(|| mode_decision.reason == "source_scope_skip_kg_document");
|
||||
@@ -1620,6 +1993,17 @@ pub async fn search(
|
||||
&context,
|
||||
));
|
||||
}
|
||||
let requested_provider_kb_id = requested_lightrag_knowledge_base_id(
|
||||
body.knowledge_base_id.as_deref(),
|
||||
body.provider_knowledge_base_id.as_deref(),
|
||||
);
|
||||
let lightrag_bases =
|
||||
control_plane_knowledge_bases_registry(&state, &context, &workspace_id, &body.root_uri)?;
|
||||
let scoped_provider_kb_id = scoped_lightrag_provider_kb_id_for_registry(
|
||||
&lightrag_bases,
|
||||
requested_provider_kb_id.as_deref(),
|
||||
&context,
|
||||
)?;
|
||||
let requested_search_mode = normalize_knowledge_rag_search_mode(body.mode.as_deref());
|
||||
let mode_decision = resolve_knowledge_rag_search_mode_for_scope(
|
||||
®istry,
|
||||
@@ -1663,6 +2047,7 @@ pub async fn search(
|
||||
let mut references =
|
||||
mapped_references(&raw, ®istry, &body.root_uri, &root_path, Some(&query));
|
||||
filter_mapped_references_by_source_scope(&mut references, &source_scope);
|
||||
filter_mapped_references_by_provider_kb_id(&mut references, scoped_provider_kb_id.as_deref());
|
||||
if search_mode == "exact" {
|
||||
filter_mapped_references_by_search_query(&mut references, &query);
|
||||
rank_mapped_references_for_query(&mut references, &query);
|
||||
@@ -1988,6 +2373,43 @@ fn filter_mapped_references_by_source_scope(references: &mut Vec<Value>, source_
|
||||
});
|
||||
}
|
||||
|
||||
fn filter_mapped_references_by_provider_kb_id(
|
||||
references: &mut Vec<Value>,
|
||||
provider_kb_id: Option<&str>,
|
||||
) {
|
||||
let Some(provider_kb_id) = provider_kb_id
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
references.retain(|reference| {
|
||||
mapped_reference_provider_kb_id(reference).as_deref() == Some(provider_kb_id)
|
||||
});
|
||||
}
|
||||
|
||||
fn mapped_reference_provider_kb_id(reference: &Value) -> Option<String> {
|
||||
reference
|
||||
.get("providerKnowledgeBaseId")
|
||||
.or_else(|| reference.get("provider_knowledge_base_id"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| {
|
||||
reference
|
||||
.get("providerIds")
|
||||
.and_then(|ids| {
|
||||
ids.get("knowledgeBaseId")
|
||||
.or_else(|| ids.get("knowledge_base_id"))
|
||||
.and_then(Value::as_str)
|
||||
})
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
})
|
||||
}
|
||||
|
||||
fn filter_mapped_references_by_search_query(references: &mut Vec<Value>, query: &str) {
|
||||
let query_normalized = normalize_text_for_match(query).to_ascii_lowercase();
|
||||
if query_normalized.is_empty() {
|
||||
@@ -2695,6 +3117,55 @@ fn requested_weknora_knowledge_base_id(
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
fn requested_lightrag_knowledge_base_id(
|
||||
knowledge_base_id: Option<&str>,
|
||||
provider_knowledge_base_id: Option<&str>,
|
||||
) -> Option<String> {
|
||||
provider_knowledge_base_id
|
||||
.or(knowledge_base_id)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty() && *value != "lightrag-default")
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
fn resolve_lightrag_provider_kb_id_for_registry(
|
||||
bases: &KnowledgeRagKnowledgeBaseRegistry,
|
||||
requested_kb_id: &str,
|
||||
) -> Result<String, ()> {
|
||||
let requested_kb_id = requested_kb_id.trim();
|
||||
if requested_kb_id.is_empty() || requested_kb_id == "lightrag-default" {
|
||||
return Err(());
|
||||
}
|
||||
bases
|
||||
.bases
|
||||
.iter()
|
||||
.find(|base| {
|
||||
is_legacy_lightrag_provider(&base.provider)
|
||||
&& base.can_write
|
||||
&& (base.provider_kb_id == requested_kb_id || base.base_id == requested_kb_id)
|
||||
})
|
||||
.map(|base| base.provider_kb_id.clone())
|
||||
.ok_or(())
|
||||
}
|
||||
|
||||
fn scoped_lightrag_provider_kb_id_for_registry(
|
||||
bases: &KnowledgeRagKnowledgeBaseRegistry,
|
||||
requested_provider_kb_id: Option<&str>,
|
||||
context: &RequestContext,
|
||||
) -> Result<Option<String>, WebError> {
|
||||
requested_provider_kb_id
|
||||
.map(|requested| resolve_lightrag_provider_kb_id_for_registry(bases, requested))
|
||||
.transpose()
|
||||
.map_err(|()| {
|
||||
WebError::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
"knowledge_rag_lightrag_kb_not_writable",
|
||||
"当前资料库不存在,或无权写入",
|
||||
)
|
||||
.with_context(context)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn scoped_weknora_knowledge_base_ids(requested_provider_kb_id: Option<&str>) -> Vec<String> {
|
||||
if let Some(provider_kb_id) = requested_provider_kb_id
|
||||
@@ -4187,6 +4658,9 @@ fn map_reference_plan(
|
||||
let locator_degraded = entry.is_none_or(|entry| entry.stale || entry.deleted_at_ms.is_some());
|
||||
let source_path = entry.map(|entry| entry.source_path.clone());
|
||||
let source_root_relative_path = entry.map(|entry| entry.source_root_relative_path.clone());
|
||||
let provider_kb_id = entry
|
||||
.and_then(|entry| entry.provider_knowledge_base_id.as_deref())
|
||||
.unwrap_or_default();
|
||||
let primary_chunk = reference
|
||||
.get("chunks")
|
||||
.and_then(Value::as_array)
|
||||
@@ -4298,6 +4772,14 @@ fn map_reference_plan(
|
||||
"citationLabel": format!("[{}]", citation_id),
|
||||
"matchSource": if occurrence_index.is_null() { "lightrag_reference" } else { "lightrag_search" },
|
||||
"reference": reference,
|
||||
"providerIds": {
|
||||
"knowledgeBaseId": provider_kb_id,
|
||||
"knowledgeId": doc_id.unwrap_or_default(),
|
||||
"chunkId": chunk_id,
|
||||
},
|
||||
"providerKnowledgeBaseId": provider_kb_id,
|
||||
"providerKnowledgeId": doc_id.unwrap_or_default(),
|
||||
"providerChunkId": chunk_id,
|
||||
"filePath": file_path,
|
||||
"chunkId": chunk_id,
|
||||
"sourceChunkId": source_chunk_id,
|
||||
@@ -6907,13 +7389,18 @@ fn registry_entry_upsert_key_matches(
|
||||
if existing.source_path != next.source_path {
|
||||
return false;
|
||||
}
|
||||
let existing_provider = existing.provider.as_deref();
|
||||
let next_provider = next.provider.as_deref();
|
||||
if next_provider == Some(WEKNORA_PROVIDER)
|
||||
&& next.provider_knowledge_base_id.as_deref().is_some()
|
||||
if existing_provider.is_some() || next_provider.is_some() {
|
||||
if existing_provider != next_provider {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if existing.provider_knowledge_base_id.as_deref().is_some()
|
||||
|| next.provider_knowledge_base_id.as_deref().is_some()
|
||||
{
|
||||
return existing.provider.as_deref() == next_provider
|
||||
&& existing.provider_knowledge_base_id.as_deref()
|
||||
== next.provider_knowledge_base_id.as_deref();
|
||||
return existing.provider_knowledge_base_id.as_deref()
|
||||
== next.provider_knowledge_base_id.as_deref();
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@@ -3128,6 +3128,8 @@ pub fn resolve_local_markdown_page_aggregate(
|
||||
&parsed.body,
|
||||
&attachment_paths,
|
||||
);
|
||||
let content =
|
||||
infer_sibling_page_references_for_local_markdown(&canonical_root, &markdown_file.path, content);
|
||||
let attachment_refs = parse_markdown_attachment_refs(
|
||||
&parsed.body,
|
||||
&markdown_file.path.display().to_string(),
|
||||
@@ -3223,6 +3225,125 @@ pub fn resolve_local_markdown_page_aggregate(
|
||||
})
|
||||
}
|
||||
|
||||
fn infer_sibling_page_references_for_local_markdown(
|
||||
root: &Path,
|
||||
markdown_path: &Path,
|
||||
content: Value,
|
||||
) -> Value {
|
||||
let Some(blocks) = content.as_array() else {
|
||||
return content;
|
||||
};
|
||||
let sibling_pages = sibling_page_reference_targets(root, markdown_path);
|
||||
if sibling_pages.is_empty() {
|
||||
return content;
|
||||
}
|
||||
Value::Array(
|
||||
blocks
|
||||
.iter()
|
||||
.map(|block| {
|
||||
let Some(title) = plain_paragraph_page_title(block) else {
|
||||
return block.clone();
|
||||
};
|
||||
let Some(source_path) = sibling_pages.get(&title) else {
|
||||
return block.clone();
|
||||
};
|
||||
let block_id = block
|
||||
.get("id")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("local-page-ref")
|
||||
.to_string();
|
||||
json!({
|
||||
"id": block_id,
|
||||
"type": "page_reference",
|
||||
"props": {
|
||||
"title": title,
|
||||
"sourcePath": source_path,
|
||||
},
|
||||
"content": [{ "type": "text", "text": title, "styles": {} }],
|
||||
"children": [],
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn sibling_page_reference_targets(_root: &Path, markdown_path: &Path) -> BTreeMap<String, String> {
|
||||
let mut targets = BTreeMap::new();
|
||||
let Some(parent_dir) = markdown_path.parent() else {
|
||||
return targets;
|
||||
};
|
||||
let Ok(entries) = fs::read_dir(parent_dir) else {
|
||||
return targets;
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path == markdown_path {
|
||||
continue;
|
||||
}
|
||||
if path.is_dir() {
|
||||
let Some(dir_name) = path.file_name().and_then(|value| value.to_str()) else {
|
||||
continue;
|
||||
};
|
||||
let candidate = path.join(format!("{dir_name}.md"));
|
||||
if candidate.is_file() {
|
||||
if let Some(relative_path) = relative_path_from_base(parent_dir, &candidate) {
|
||||
targets.insert(file_stem_title(dir_name), relative_path);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let Some(file_name) = path.file_name().and_then(|value| value.to_str()) else {
|
||||
continue;
|
||||
};
|
||||
if !is_markdown_file(file_name) {
|
||||
continue;
|
||||
}
|
||||
if let Some(relative_path) = relative_path_from_base(parent_dir, &path) {
|
||||
targets.insert(file_stem_title(file_name), relative_path);
|
||||
}
|
||||
}
|
||||
targets
|
||||
}
|
||||
|
||||
fn relative_path_from_base(base: &Path, path: &Path) -> Option<String> {
|
||||
let relative = path.strip_prefix(base).ok()?;
|
||||
Some(
|
||||
relative
|
||||
.components()
|
||||
.map(|component| component.as_os_str().to_string_lossy().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("/"),
|
||||
)
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
fn plain_paragraph_page_title(block: &Value) -> Option<String> {
|
||||
if block.get("type").and_then(Value::as_str) != Some("paragraph") {
|
||||
return None;
|
||||
}
|
||||
let content = block.get("content").and_then(Value::as_array)?;
|
||||
if content.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
let node = content.first()?;
|
||||
if node.get("type").and_then(Value::as_str) != Some("text") {
|
||||
return None;
|
||||
}
|
||||
if node
|
||||
.get("styles")
|
||||
.and_then(Value::as_object)
|
||||
.map(|styles| !styles.is_empty())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
node.get("text")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
pub fn save_local_markdown_page(
|
||||
root_uri: &str,
|
||||
document_id: &str,
|
||||
@@ -4094,7 +4215,8 @@ pub(crate) fn write_local_markdown_asset(
|
||||
));
|
||||
}
|
||||
|
||||
let asset_dir = markdown_dir.to_path_buf();
|
||||
let asset_type = local_upload_asset_type(kind, &file.content_type);
|
||||
let asset_dir = local_markdown_asset_dir(&canonical_root, &markdown_file.path, asset_type)?;
|
||||
if !asset_dir.starts_with(&canonical_root) {
|
||||
return Err(WebError::bad_request_code(
|
||||
"local_folder_root_escape",
|
||||
@@ -4143,7 +4265,6 @@ pub(crate) fn write_local_markdown_asset(
|
||||
if let Some(ref mut value) = attachment_ref {
|
||||
value.authorized = Some(true);
|
||||
}
|
||||
let asset_type = local_upload_asset_type(kind, &file.content_type);
|
||||
let mut uploaded_assets = metadata.uploaded_assets;
|
||||
uploaded_assets.insert(
|
||||
root_relative_path.clone(),
|
||||
@@ -7583,6 +7704,7 @@ fn should_ignore_entry(relative_path: &str, file_name: &str) -> bool {
|
||||
if matches!(
|
||||
lower_name.as_str(),
|
||||
".git"
|
||||
| ".assets"
|
||||
| ".mnote"
|
||||
| ".codegraph"
|
||||
| ".codex"
|
||||
@@ -7613,7 +7735,8 @@ fn should_ignore_entry(relative_path: &str, file_name: &str) -> bool {
|
||||
{
|
||||
return true;
|
||||
}
|
||||
relative_path == ".mnote/trash"
|
||||
lower_name.ends_with(".assets")
|
||||
|| relative_path == ".mnote/trash"
|
||||
|| relative_path.starts_with(".mnote/trash/")
|
||||
|| is_local_index_artifact_entry(relative_path)
|
||||
|| is_local_ocr_intermediate_entry(relative_path, file_name)
|
||||
@@ -8817,10 +8940,62 @@ fn markdown_href_for_relative_path(relative_path: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn local_markdown_asset_dir(
|
||||
root: &Path,
|
||||
markdown_path: &Path,
|
||||
asset_type: &str,
|
||||
) -> Result<PathBuf, WebError> {
|
||||
let markdown_dir = markdown_path.parent().ok_or_else(|| {
|
||||
WebError::bad_request_code(
|
||||
"local_asset_upload_bad_markdown_path",
|
||||
"本地 Markdown 文件路径无父目录",
|
||||
)
|
||||
})?;
|
||||
if !markdown_dir.starts_with(root) {
|
||||
return Err(WebError::bad_request_code(
|
||||
"local_folder_root_escape",
|
||||
"本地资源目录不能越过 root",
|
||||
));
|
||||
}
|
||||
let stem = markdown_path
|
||||
.file_stem()
|
||||
.and_then(|value| value.to_str())
|
||||
.unwrap_or("Page")
|
||||
.trim();
|
||||
let parent_name = markdown_dir
|
||||
.file_name()
|
||||
.and_then(|value| value.to_str())
|
||||
.unwrap_or("")
|
||||
.trim();
|
||||
let asset_root = if !stem.is_empty() && stem == parent_name {
|
||||
markdown_dir.join(".assets")
|
||||
} else {
|
||||
markdown_dir.join(format!(
|
||||
"{}.assets",
|
||||
if stem.is_empty() { "Page" } else { stem }
|
||||
))
|
||||
};
|
||||
Ok(asset_root.join(local_markdown_asset_bucket(asset_type)))
|
||||
}
|
||||
|
||||
fn local_markdown_asset_bucket(asset_type: &str) -> &'static str {
|
||||
match asset_type.trim().to_ascii_lowercase().as_str() {
|
||||
"image" => "image",
|
||||
"audio" => "audio",
|
||||
"video" => "video",
|
||||
_ => "file",
|
||||
}
|
||||
}
|
||||
|
||||
fn local_upload_asset_type(kind: &str, mime_type: &str) -> &'static str {
|
||||
let normalized_kind = kind.trim().to_ascii_lowercase();
|
||||
if normalized_kind == "image" || mime_type.trim().to_ascii_lowercase().starts_with("image/") {
|
||||
let normalized_mime = mime_type.trim().to_ascii_lowercase();
|
||||
if normalized_kind == "image" || normalized_mime.starts_with("image/") {
|
||||
"image"
|
||||
} else if normalized_kind == "audio" || normalized_mime.starts_with("audio/") {
|
||||
"audio"
|
||||
} else if normalized_kind == "video" || normalized_mime.starts_with("video/") {
|
||||
"video"
|
||||
} else {
|
||||
"file"
|
||||
}
|
||||
@@ -10560,6 +10735,53 @@ fn main() {}
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_markdown_page_aggregate_infers_sibling_page_blocks_from_plain_lines() {
|
||||
let root = temp_root("mnote-local-sibling-page-lines");
|
||||
init_workspace(&root);
|
||||
std::fs::create_dir_all(root.join("完结项目").join("爱斯特完结项目"))
|
||||
.expect("create child page dir");
|
||||
std::fs::write(
|
||||
root.join("完结项目").join("完结项目.md"),
|
||||
"# 完结项目\n\n爱斯特完结项目\n\n普通文本\n",
|
||||
)
|
||||
.expect("write parent md");
|
||||
std::fs::write(
|
||||
root.join("完结项目")
|
||||
.join("爱斯特完结项目")
|
||||
.join("爱斯特完结项目.md"),
|
||||
"# 爱斯特完结项目\n",
|
||||
)
|
||||
.expect("write child md");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
|
||||
let document_id = format!(
|
||||
"local-md:{}",
|
||||
encode_local_id_segment("完结项目/完结项目.md")
|
||||
);
|
||||
let aggregate = resolve_local_markdown_page_aggregate(&root_uri, &document_id)
|
||||
.expect("aggregate");
|
||||
let blocks = aggregate.body.content.as_array().expect("blocks");
|
||||
|
||||
assert_eq!(blocks[1]["type"], "page_reference");
|
||||
assert_eq!(blocks[1]["props"]["title"], "爱斯特完结项目");
|
||||
assert_eq!(
|
||||
blocks[1]["props"]["sourcePath"],
|
||||
"爱斯特完结项目/爱斯特完结项目.md"
|
||||
);
|
||||
assert_eq!(blocks[2]["type"], "paragraph");
|
||||
assert_eq!(
|
||||
aggregate.body.block_document["blocks"][1]["type"],
|
||||
"page_reference"
|
||||
);
|
||||
assert_eq!(
|
||||
aggregate.body.block_document["blocks"][1]["attrs"]["sourcePath"],
|
||||
"爱斯特完结项目/爱斯特完结项目.md"
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_markdown_save_does_not_migrate_runtime_open_url_inline_link() {
|
||||
let root = temp_root("mnote-local-uploaded-md-inline-link");
|
||||
@@ -13994,9 +14216,22 @@ fn main() {}
|
||||
"# Assets\n",
|
||||
)
|
||||
.expect("write md");
|
||||
std::fs::create_dir_all(root.join("docs").join("README")).expect("create page dir");
|
||||
std::fs::write(root.join("docs").join("README").join("photo.png"), b"old")
|
||||
.expect("write existing asset");
|
||||
std::fs::create_dir_all(
|
||||
root.join("docs")
|
||||
.join("README")
|
||||
.join(".assets")
|
||||
.join("image"),
|
||||
)
|
||||
.expect("create image asset dir");
|
||||
std::fs::write(
|
||||
root.join("docs")
|
||||
.join("README")
|
||||
.join(".assets")
|
||||
.join("image")
|
||||
.join("photo.png"),
|
||||
b"old",
|
||||
)
|
||||
.expect("write existing asset");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
|
||||
let asset = write_local_markdown_asset(
|
||||
@@ -14011,15 +14246,21 @@ fn main() {}
|
||||
)
|
||||
.expect("upload asset");
|
||||
|
||||
assert_eq!(asset["sourcePath"], "photo-1.png");
|
||||
assert_eq!(asset["file_url"], "photo-1.png");
|
||||
assert_eq!(asset["sourcePath"], ".assets/image/photo-1.png");
|
||||
assert_eq!(asset["file_url"], ".assets/image/photo-1.png");
|
||||
assert_eq!(asset["asset_type"], "image");
|
||||
assert_eq!(asset["sourceKind"], "local_folder");
|
||||
assert_eq!(asset["uploadIntent"], "editor.markdown.attach");
|
||||
assert_eq!(asset["rootRelativePath"], "docs/README/photo-1.png");
|
||||
assert_eq!(asset["markdownRelativePath"], "photo-1.png");
|
||||
assert_eq!(asset["markdownHref"], "./photo-1.png");
|
||||
assert_eq!(asset["attachmentRef"]["rawHref"], "./photo-1.png");
|
||||
assert_eq!(
|
||||
asset["rootRelativePath"],
|
||||
"docs/README/.assets/image/photo-1.png"
|
||||
);
|
||||
assert_eq!(asset["markdownRelativePath"], ".assets/image/photo-1.png");
|
||||
assert_eq!(asset["markdownHref"], "./.assets/image/photo-1.png");
|
||||
assert_eq!(
|
||||
asset["attachmentRef"]["rawHref"],
|
||||
"./.assets/image/photo-1.png"
|
||||
);
|
||||
assert_eq!(asset["attachmentRef"]["kind"], "pageLocal");
|
||||
assert_eq!(asset["attachmentRef"]["openKind"], "image");
|
||||
assert_eq!(asset["attachmentRef"]["authorized"], true);
|
||||
@@ -14028,8 +14269,14 @@ fn main() {}
|
||||
"local-md:docs~2FREADME~2FREADME.md"
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read(root.join("docs").join("README").join("photo-1.png"))
|
||||
.expect("read copied asset"),
|
||||
std::fs::read(
|
||||
root.join("docs")
|
||||
.join("README")
|
||||
.join(".assets")
|
||||
.join("image")
|
||||
.join("photo-1.png"),
|
||||
)
|
||||
.expect("read copied asset"),
|
||||
b"new-image"
|
||||
);
|
||||
let markdown_asset = write_local_markdown_asset(
|
||||
@@ -14043,11 +14290,17 @@ fn main() {}
|
||||
},
|
||||
)
|
||||
.expect("upload markdown asset");
|
||||
assert_eq!(markdown_asset["sourcePath"], "notes.md");
|
||||
assert_eq!(markdown_asset["sourcePath"], ".assets/file/notes.md");
|
||||
assert_eq!(markdown_asset["uploadIntent"], "editor.markdown.attach");
|
||||
assert_eq!(markdown_asset["rootRelativePath"], "docs/README/notes.md");
|
||||
assert_eq!(markdown_asset["markdownRelativePath"], "notes.md");
|
||||
assert_eq!(markdown_asset["markdownHref"], "./notes.md");
|
||||
assert_eq!(
|
||||
markdown_asset["rootRelativePath"],
|
||||
"docs/README/.assets/file/notes.md"
|
||||
);
|
||||
assert_eq!(
|
||||
markdown_asset["markdownRelativePath"],
|
||||
".assets/file/notes.md"
|
||||
);
|
||||
assert_eq!(markdown_asset["markdownHref"], "./.assets/file/notes.md");
|
||||
assert_eq!(markdown_asset["attachmentRef"]["openKind"], "text");
|
||||
assert_eq!(
|
||||
markdown_asset["ownerDocumentId"],
|
||||
@@ -14071,16 +14324,16 @@ fn main() {}
|
||||
);
|
||||
assert_eq!(
|
||||
windows_path_asset["sourcePath"],
|
||||
"1768096586803-672be7a7-5cb3-454d-9b1b-6230f86ba392.pptx"
|
||||
".assets/file/1768096586803-672be7a7-5cb3-454d-9b1b-6230f86ba392.pptx"
|
||||
);
|
||||
assert_eq!(
|
||||
windows_path_asset["rootRelativePath"],
|
||||
"docs/README/1768096586803-672be7a7-5cb3-454d-9b1b-6230f86ba392.pptx"
|
||||
"docs/README/.assets/file/1768096586803-672be7a7-5cb3-454d-9b1b-6230f86ba392.pptx"
|
||||
);
|
||||
let uploaded_asset_index =
|
||||
std::fs::read_to_string(root.join(".mnote").join("uploaded-assets.json"))
|
||||
.expect("uploaded asset index");
|
||||
assert!(uploaded_asset_index.contains("docs/README/notes.md"));
|
||||
assert!(uploaded_asset_index.contains("docs/README/.assets/file/notes.md"));
|
||||
|
||||
std::fs::write(root.join("docs").join("Loose.md"), "# Loose\n").expect("write loose md");
|
||||
let loose_asset = write_local_markdown_asset(
|
||||
@@ -14094,45 +14347,47 @@ fn main() {}
|
||||
},
|
||||
)
|
||||
.expect("upload loose markdown asset");
|
||||
assert_eq!(loose_asset["sourcePath"], "loose.pdf");
|
||||
assert_eq!(loose_asset["rootRelativePath"], "docs/loose.pdf");
|
||||
assert_eq!(loose_asset["markdownRelativePath"], "loose.pdf");
|
||||
assert_eq!(loose_asset["markdownHref"], "./loose.pdf");
|
||||
assert_eq!(loose_asset["sourcePath"], "Loose.assets/file/loose.pdf");
|
||||
assert_eq!(
|
||||
loose_asset["rootRelativePath"],
|
||||
"docs/Loose.assets/file/loose.pdf"
|
||||
);
|
||||
assert_eq!(
|
||||
loose_asset["markdownRelativePath"],
|
||||
"Loose.assets/file/loose.pdf"
|
||||
);
|
||||
assert_eq!(loose_asset["markdownHref"], "./Loose.assets/file/loose.pdf");
|
||||
assert!(
|
||||
root.join("docs").join("loose.pdf").is_file(),
|
||||
"非 bundle Markdown 上传应写入 md 同目录"
|
||||
root.join("docs")
|
||||
.join("Loose.assets")
|
||||
.join("file")
|
||||
.join("loose.pdf")
|
||||
.is_file(),
|
||||
"非 bundle Markdown 上传应写入页面专属 assets 目录"
|
||||
);
|
||||
assert!(
|
||||
!root.join("docs").join("Loose").join("loose.pdf").exists(),
|
||||
"非 bundle Markdown 上传不应写入同名子目录"
|
||||
!root.join("docs").join("loose.pdf").exists(),
|
||||
"非 bundle Markdown 上传不应写入 md 同目录"
|
||||
);
|
||||
|
||||
let snapshot = load_local_folder_file_tree_children_snapshot(&root_uri, "docs/README")
|
||||
.expect("file tree");
|
||||
let items = snapshot.projection["items"].as_array().expect("items");
|
||||
let notes_row = items
|
||||
.iter()
|
||||
.find(|item| item["title"].as_str() == Some("notes.md"))
|
||||
.expect("uploaded markdown asset row");
|
||||
assert_eq!(notes_row["rowKind"].as_str(), Some("asset"));
|
||||
assert_eq!(notes_row["iconHint"].as_str(), Some("markdown"));
|
||||
assert_eq!(
|
||||
notes_row["resourceMeta"]["assetId"].as_str(),
|
||||
Some("local-file:docs/README/notes.md")
|
||||
assert!(
|
||||
!items
|
||||
.iter()
|
||||
.any(|item| item["title"].as_str() == Some(".assets")),
|
||||
"页面隐藏资源目录不应出现在 FileTree"
|
||||
);
|
||||
let page_tree = load_local_folder_page_tree_snapshot(&root_uri).expect("page tree");
|
||||
let page_items = page_tree.projection["items"]
|
||||
.as_array()
|
||||
.expect("page items");
|
||||
let page_notes_row = page_items
|
||||
.iter()
|
||||
.find(|item| item["title"].as_str() == Some("notes.md"))
|
||||
.expect("uploaded markdown asset page row");
|
||||
assert_eq!(page_notes_row["rowKind"].as_str(), Some("asset"));
|
||||
assert_eq!(page_notes_row["iconHint"].as_str(), Some("markdown"));
|
||||
assert_eq!(
|
||||
page_notes_row["resourceMeta"]["assetId"].as_str(),
|
||||
Some("local-file:docs/README/notes.md")
|
||||
assert!(
|
||||
!page_items
|
||||
.iter()
|
||||
.any(|item| item["title"].as_str() == Some("notes.md")),
|
||||
"页面隐藏资源目录内的 Markdown 附件不应进入 PageTree"
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
|
||||
@@ -52,6 +52,10 @@ enum MarkdownBlock {
|
||||
name: String,
|
||||
source_path: String,
|
||||
},
|
||||
PageReference {
|
||||
title: String,
|
||||
source_path: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -103,6 +107,7 @@ pub struct AttachmentRef {
|
||||
pub relative_path: Option<String>,
|
||||
pub ext: Option<String>,
|
||||
pub content_type: Option<String>,
|
||||
pub file_size: Option<u64>,
|
||||
pub exists: Option<bool>,
|
||||
pub authorized: Option<bool>,
|
||||
pub open_kind: String,
|
||||
@@ -346,6 +351,10 @@ fn build_attachment_ref(
|
||||
.as_deref()
|
||||
.and_then(attachment_content_type)
|
||||
.map(str::to_string);
|
||||
let file_size = resolved_absolute_path
|
||||
.as_ref()
|
||||
.and_then(|path| std::fs::metadata(path).ok())
|
||||
.map(|metadata| metadata.len());
|
||||
let exists = resolved_absolute_path
|
||||
.as_ref()
|
||||
.map(|path| Path::new(path).exists());
|
||||
@@ -378,6 +387,7 @@ fn build_attachment_ref(
|
||||
relative_path,
|
||||
ext,
|
||||
content_type,
|
||||
file_size,
|
||||
exists,
|
||||
authorized: None,
|
||||
open_kind,
|
||||
@@ -672,6 +682,10 @@ fn append_ast_paragraph<'a>(
|
||||
blocks.push(MarkdownBlock::Media { name, source_path });
|
||||
return;
|
||||
}
|
||||
if let Some((title, source_path)) = paragraph_page_reference(node) {
|
||||
blocks.push(MarkdownBlock::PageReference { title, source_path });
|
||||
return;
|
||||
}
|
||||
if let Some((name, source_path, remaining)) =
|
||||
paragraph_leading_attachment_media(node, attachment_paths)
|
||||
{
|
||||
@@ -696,6 +710,19 @@ fn append_ast_list_item<'a>(
|
||||
NodeValue::Item(_) => (false, false),
|
||||
_ => return append_ast_block(node, blocks, attachment_paths),
|
||||
};
|
||||
if !is_task {
|
||||
let mut children = node.children();
|
||||
if let Some(paragraph) = children.next() {
|
||||
if children.next().is_none()
|
||||
&& matches!(paragraph.data.borrow().value, NodeValue::Paragraph)
|
||||
{
|
||||
if let Some((title, source_path)) = paragraph_page_reference(paragraph) {
|
||||
blocks.push(MarkdownBlock::PageReference { title, source_path });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut content = Vec::new();
|
||||
for child in node.children() {
|
||||
match child.data.borrow().value.clone() {
|
||||
@@ -849,6 +876,23 @@ fn paragraph_attachment_media<'a>(
|
||||
link_attachment_media(first, attachment_paths)
|
||||
}
|
||||
|
||||
fn paragraph_page_reference<'a>(node: &'a AstNode<'a>) -> Option<(String, String)> {
|
||||
let mut children = node.children();
|
||||
let first = children.next()?;
|
||||
if children.next().is_some() {
|
||||
return None;
|
||||
}
|
||||
link_page_reference(first).or_else(|| {
|
||||
let inline = collect_inline_children(node);
|
||||
if inline.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
let item = inline.first()?;
|
||||
let href = item.styles.link.as_deref()?;
|
||||
page_reference_from_target(href, &item.text)
|
||||
})
|
||||
}
|
||||
|
||||
fn paragraph_mindmap<'a>(node: &'a AstNode<'a>) -> Option<(String, String)> {
|
||||
let mut children = node.children();
|
||||
let first = children.next()?;
|
||||
@@ -929,6 +973,67 @@ fn link_mindmap<'a>(node: &'a AstNode<'a>) -> Option<(String, String)> {
|
||||
))
|
||||
}
|
||||
|
||||
fn link_page_reference<'a>(node: &'a AstNode<'a>) -> Option<(String, String)> {
|
||||
let NodeValue::Link(link) = &node.data.borrow().value else {
|
||||
return None;
|
||||
};
|
||||
page_reference_from_target(link.url.trim(), &collect_plain_text(node))
|
||||
}
|
||||
|
||||
fn page_reference_from_target(target: &str, label: &str) -> Option<(String, String)> {
|
||||
let target = normalized_markdown_link_target(target)?;
|
||||
if target.is_empty()
|
||||
|| target.starts_with("http://")
|
||||
|| target.starts_with("https://")
|
||||
|| target.starts_with('#')
|
||||
|| target.starts_with("mailto:")
|
||||
|| target.starts_with("file:")
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let path_part = target
|
||||
.split_once('#')
|
||||
.map(|(path, _)| path)
|
||||
.unwrap_or(target)
|
||||
.split_once('?')
|
||||
.map(|(path, _)| path)
|
||||
.unwrap_or(target);
|
||||
let extension = std::path::Path::new(path_part)
|
||||
.extension()
|
||||
.and_then(|value| value.to_str())?;
|
||||
if !matches!(extension.to_ascii_lowercase().as_str(), "md" | "markdown") {
|
||||
return None;
|
||||
}
|
||||
let fallback_title = std::path::Path::new(path_part)
|
||||
.file_stem()
|
||||
.and_then(|value| value.to_str())
|
||||
.unwrap_or("页面")
|
||||
.trim();
|
||||
let title = label.trim();
|
||||
Some((
|
||||
if title.is_empty() {
|
||||
fallback_title.to_string()
|
||||
} else {
|
||||
title.to_string()
|
||||
},
|
||||
path_part.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn normalized_markdown_link_target(target: &str) -> Option<&str> {
|
||||
let trimmed = target.trim();
|
||||
if trimmed.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if let Some(value) = trimmed
|
||||
.strip_prefix('<')
|
||||
.and_then(|value| value.strip_suffix('>'))
|
||||
{
|
||||
return Some(value.trim());
|
||||
}
|
||||
Some(trimmed)
|
||||
}
|
||||
|
||||
fn is_local_mindmap_file_name(file_name: &str) -> bool {
|
||||
let trimmed = file_name.trim();
|
||||
let lower = trimmed.to_ascii_lowercase();
|
||||
@@ -1035,6 +1140,16 @@ fn markdown_block_to_json(block: &MarkdownBlock, block_number: usize) -> Value {
|
||||
"content": [],
|
||||
"children": [],
|
||||
}),
|
||||
MarkdownBlock::PageReference { title, source_path } => json!({
|
||||
"id": format!("local-block-{block_number}"),
|
||||
"type": "page_reference",
|
||||
"props": {
|
||||
"title": title,
|
||||
"sourcePath": source_path,
|
||||
},
|
||||
"content": [{ "type": "text", "text": title, "styles": {} }],
|
||||
"children": [],
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1267,6 +1382,35 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn markdown_local_page_link_parses_as_page_reference_block() {
|
||||
let blocks = markdown_to_blocks("[完结项目](<完结项目/完结项目.md> \"完结项目\")\n");
|
||||
let first = blocks
|
||||
.as_array()
|
||||
.and_then(|items| items.first())
|
||||
.expect("first block");
|
||||
|
||||
assert_eq!(first["type"].as_str(), Some("page_reference"));
|
||||
assert_eq!(first["props"]["title"].as_str(), Some("完结项目"));
|
||||
assert_eq!(
|
||||
first["props"]["sourcePath"].as_str(),
|
||||
Some("完结项目/完结项目.md")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn markdown_list_item_with_only_local_page_link_parses_as_page_reference_block() {
|
||||
let blocks = markdown_to_blocks("* [知识](<知识/知识.md>)\n");
|
||||
let first = blocks
|
||||
.as_array()
|
||||
.and_then(|items| items.first())
|
||||
.expect("first block");
|
||||
|
||||
assert_eq!(first["type"].as_str(), Some("page_reference"));
|
||||
assert_eq!(first["props"]["title"].as_str(), Some("知识"));
|
||||
assert_eq!(first["props"]["sourcePath"].as_str(), Some("知识/知识.md"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn markdown_attachment_refs_parse_standard_href_variants() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
@@ -1305,6 +1449,7 @@ mod tests {
|
||||
assert_eq!(refs[0].kind, "pageLocal");
|
||||
assert_eq!(refs[0].open_kind, "pdf");
|
||||
assert_eq!(refs[0].exists, Some(true));
|
||||
assert_eq!(refs[0].file_size, Some(3));
|
||||
assert_eq!(
|
||||
refs[0].relative_path,
|
||||
Some("docs/同目录 文件.pdf".to_string())
|
||||
|
||||
@@ -103,7 +103,7 @@ pub fn build_router(state: AppState) -> Router {
|
||||
)
|
||||
.route(
|
||||
"/api/knowledge-rag/knowledge-bases",
|
||||
post(knowledge_rag::create_knowledge_base),
|
||||
post(knowledge_rag::create_knowledge_base).delete(knowledge_rag::delete_knowledge_base),
|
||||
)
|
||||
.route("/api/knowledge-rag/ingest", post(knowledge_rag::ingest))
|
||||
.route("/api/knowledge-rag/search", post(knowledge_rag::search))
|
||||
|
||||
@@ -4524,6 +4524,34 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dev_hot_runtime_serves_page_block_pane_navigation_bridge() {
|
||||
let _guard = crate::test_support::hermes_env_lock()
|
||||
.lock()
|
||||
.expect("env lock");
|
||||
std::env::set_var("MNOTE_WEB_DEV_HOT_RELOAD", "1");
|
||||
let response = app()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island.js")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
std::env::remove_var("MNOTE_WEB_DEV_HOT_RELOAD");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let js = String::from_utf8(body.to_vec()).expect("utf8");
|
||||
assert!(
|
||||
js.contains("inline0.js?devHot="),
|
||||
"dev:hot 下 island 必须加载带 cache buster 的页面块导航 bridge"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn leptos_tiptap_lazy_mindmap_runtime_asset_is_served() {
|
||||
let response = app()
|
||||
@@ -5495,6 +5523,9 @@ mod tests {
|
||||
assert!(DOCUMENT_SESSION_RUNTIME_JS.contains("createDocumentSessionRuntime"));
|
||||
assert!(DOCUMENT_SESSION_RUNTIME_JS.contains("getOrCreateDocumentSession"));
|
||||
assert!(DOCUMENT_SESSION_RUNTIME_JS.contains("persistSession"));
|
||||
assert!(DOCUMENT_SESSION_RUNTIME_JS.contains("navigateLocalMarkdownDeletedFallback"));
|
||||
assert!(DOCUMENT_SESSION_RUNTIME_JS.contains("localFolderNavigationFallbackUrl"));
|
||||
assert!(DOCUMENT_SESSION_RUNTIME_JS.contains("window.location.assign"));
|
||||
assert!(DOCUMENT_SESSION_RUNTIME_JS.contains("__mnoteLocalFolderEventBus"));
|
||||
assert!(DOCUMENT_SESSION_RUNTIME_JS.contains("mnote:local-folder:document-changed"));
|
||||
assert!(DOCUMENT_RESOURCE_TAB_RUNTIME_JS.contains("__mnoteLocalFolderEventBus"));
|
||||
|
||||
@@ -288,6 +288,14 @@ mod tests {
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("/api/ui/preferences"));
|
||||
assert!(!SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("localStorage.setItem"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("restoreSidebarTreeTab"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS
|
||||
.contains("applySearchSwitchState(overlay, { knowledge: false"));
|
||||
assert!(!SIDEBAR_TREE_RUNTIME_JS
|
||||
.contains("applySearchSwitchState(overlay, { knowledge: true"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS
|
||||
.contains("data-search-switch=\"knowledge\" role=\"switch\" aria-checked=\"false\""));
|
||||
assert!(!SIDEBAR_TREE_RUNTIME_JS
|
||||
.contains("data-search-switch=\"knowledge\" role=\"switch\" aria-checked=\"true\""));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("treeView"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("tree:local-command"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("applyCreatedDocumentLocally"));
|
||||
@@ -566,7 +574,7 @@ mod tests {
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("data-knowledge-rag-action=\"prune\""));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("mnote-knowledge-rag-source-filters"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("filter-sources"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("Provider 未映射"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("未关联资料"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("Rerank"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("scheduleKnowledgeRagStatusBridge"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("knowledgeRagStatusHasInFlight"));
|
||||
@@ -593,7 +601,12 @@ mod tests {
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("data-knowledge-rag-input-status-label"));
|
||||
assert!(!SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("localOcrAutoEnabled"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("data-mnote-local-ocr-auto-retired"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("window.location.href = '/knowledge'"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function knowledgeHostUrl"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("knowledgeUrl.searchParams.set('workspaceId'"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("knowledgeUrl.searchParams.set('sourceKind'"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("knowledgeUrl.searchParams.set('rootUri'"));
|
||||
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("window.location.href = '/knowledge'"));
|
||||
assert!(!SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("var knowledgeUrl = '/knowledge'"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("open-knowledge-rag-debug-settings"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/debug/knowledge-rag"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("[data-knowledge-rag-action]"));
|
||||
@@ -607,7 +620,7 @@ mod tests {
|
||||
SIDEBAR_TREE_RUNTIME_JS.contains(r#"<option value="exact" selected>关键词</option>"#)
|
||||
);
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS
|
||||
.contains(r#"data-search-switch="knowledge" role="switch" aria-checked="true""#));
|
||||
.contains(r#"data-search-switch="knowledge" role="switch" aria-checked="false""#));
|
||||
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("knowledgeRagSourceRelativePath"));
|
||||
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("supportsKnowledgeRagSource"));
|
||||
assert!(!SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("runLocalOcr"));
|
||||
|
||||
@@ -154,7 +154,11 @@ a:hover {
|
||||
.material-symbols-outlined[data-icon="star"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 3 2.7 5.4 6 .9-4.4 4.3 1 6-5.3-2.8-5.3 2.8 1-6-4.4-4.3 6-.9z' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'/%3E%3C/svg%3E"); }
|
||||
.material-symbols-outlined[data-icon="history"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4 12a8 8 0 1 0 2.3-5.7L4 8.5' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M4 4v4.5h4.5M12 8v5l3.5 2' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
|
||||
.material-symbols-outlined[data-icon="auto_awesome"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 3 1.6 5.2L19 10l-5.4 1.8L12 17l-1.6-5.2L5 10l5.4-1.8zM5.5 15.5l.8 2.2 2.2.8-2.2.8-.8 2.2-.8-2.2-2.2-.8 2.2-.8zM18.5 2.5l.7 1.8 1.8.7-1.8.7-.7 1.8-.7-1.8-1.8-.7 1.8-.7z' fill='black'/%3E%3C/svg%3E"); }
|
||||
.material-symbols-outlined[data-icon="refresh"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M20 6v5h-5M4 18v-5h5M7.5 8.5A6.5 6.5 0 0 1 18.5 6M16.5 15.5A6.5 6.5 0 0 1 5.5 18' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
|
||||
.material-symbols-outlined[data-icon="folder"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3.5 7.5h6l2 2H20.5v8.5a2 2 0 0 1-2 2H5.5a2 2 0 0 1-2-2z' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'/%3E%3C/svg%3E"); }
|
||||
.material-symbols-outlined[data-icon="folder_open"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3.5 7.5h6l2 2H21v8.5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-9a1.5 1.5 0 0 1 1.5-1.5Z' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'/%3E%3Cpath d='M4 18 7 11h14l-3 7' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'/%3E%3C/svg%3E"); }
|
||||
.material-symbols-outlined[data-icon="folder_add"],
|
||||
.material-symbols-outlined[data-icon="create_new_folder"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3.5 7.5h6l2 2H20.5v8.5a2 2 0 0 1-2 2H5.5a2 2 0 0 1-2-2z' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'/%3E%3Cpath d='M14.5 12.5v5M12 15h5' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E"); }
|
||||
.material-symbols-outlined[data-icon="drive_file_move"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4 6h6l2 2h8v10H4z' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'/%3E%3Cpath d='m13 12 3 3-3 3M8 15h8' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
|
||||
.material-symbols-outlined[data-icon="delete"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5 6h14M9 6V4h6v2M8 6l1 14h6l1-14M10.5 10v6M13.5 10v6' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
|
||||
.material-symbols-outlined[data-icon="right_panel_open"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4 5h16v14H4zM14 5v14M8 12h6M11 9l3 3-3 3' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
|
||||
@@ -168,6 +172,12 @@ a:hover {
|
||||
.material-symbols-outlined[data-icon="arrow_outward"],
|
||||
.material-symbols-outlined[data-icon="open_in_new"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8 6H5v13h13v-3M12 5h7v7M10 14 19 5' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
|
||||
.material-symbols-outlined[data-icon="share"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='18' cy='5' r='3' fill='none' stroke='black' stroke-width='2'/%3E%3Ccircle cx='6' cy='12' r='3' fill='none' stroke='black' stroke-width='2'/%3E%3Ccircle cx='18' cy='19' r='3' fill='none' stroke='black' stroke-width='2'/%3E%3Cpath d='m8.7 10.7 6.6-4.4M8.7 13.3l6.6 4.4' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E"); }
|
||||
.material-symbols-outlined[data-icon="view_list"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8 6h12M8 12h12M8 18h12' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3Ccircle cx='4.5' cy='6' r='1.5' fill='black'/%3E%3Ccircle cx='4.5' cy='12' r='1.5' fill='black'/%3E%3Ccircle cx='4.5' cy='18' r='1.5' fill='black'/%3E%3C/svg%3E"); }
|
||||
.material-symbols-outlined[data-icon="grid_view"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4zM14 14h6v6h-6z' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'/%3E%3C/svg%3E"); }
|
||||
.material-symbols-outlined[data-icon="upload_file"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6 20h12a2 2 0 0 0 2-2V9l-5-5H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2Z' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'/%3E%3Cpath d='M14 4v6h6M12 16V9M9 12l3-3 3 3' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
|
||||
.material-symbols-outlined[data-icon="note_add"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6 4h12v16H6zM9 9h6M9 13h3' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M16 13v5M13.5 15.5h5' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E"); }
|
||||
.material-symbols-outlined[data-icon="check_circle"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='12' cy='12' r='9' fill='none' stroke='black' stroke-width='2'/%3E%3Cpath d='m8 12 2.6 2.6L16.5 9' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
|
||||
.material-symbols-outlined[data-icon="hub"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='6' cy='12' r='3' fill='none' stroke='black' stroke-width='2'/%3E%3Ccircle cx='18' cy='6' r='3' fill='none' stroke='black' stroke-width='2'/%3E%3Ccircle cx='18' cy='18' r='3' fill='none' stroke='black' stroke-width='2'/%3E%3Cpath d='M8.7 10.7 15.3 7.3M8.7 13.3l6.6 3.4' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E"); }
|
||||
|
||||
.material-symbols-outlined[data-icon="comment"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5 5h14v10H9l-4 4V5Z' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'/%3E%3C/svg%3E"); }
|
||||
.material-symbols-outlined[data-icon="chat"],
|
||||
@@ -219,7 +229,6 @@ a:hover {
|
||||
.material-symbols-outlined[data-icon="text_fields"]::before,
|
||||
.material-symbols-outlined[data-icon="toc"]::before,
|
||||
.material-symbols-outlined[data-icon="translate"]::before,
|
||||
.material-symbols-outlined[data-icon="upload_file"]::before,
|
||||
.material-symbols-outlined[data-icon="view_column"]::before {
|
||||
width: auto;
|
||||
height: auto;
|
||||
@@ -255,7 +264,6 @@ a:hover {
|
||||
.material-symbols-outlined[data-icon="text_fields"]::before { content: "T"; }
|
||||
.material-symbols-outlined[data-icon="toc"]::before { content: "☰"; }
|
||||
.material-symbols-outlined[data-icon="translate"]::before { content: "文"; }
|
||||
.material-symbols-outlined[data-icon="upload_file"]::before { content: "⇧"; }
|
||||
.material-symbols-outlined[data-icon="view_column"]::before { content: "▥"; }
|
||||
.material-symbols-outlined[data-icon="radio_button_unchecked"]::before {
|
||||
width: .78em;
|
||||
|
||||
@@ -1232,6 +1232,36 @@
|
||||
background: var(--wolai-bg-sidebar);
|
||||
}
|
||||
|
||||
.mnote-navigation-page__item.mnote-page-block-link {
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
gap: 7px;
|
||||
min-height: 28px;
|
||||
padding: 1px 3px;
|
||||
border-radius: 4px;
|
||||
color: #37352f;
|
||||
font-weight: 600;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: rgba(55, 53, 47, 0.42);
|
||||
text-decoration-thickness: 1px;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
.mnote-navigation-page__item.mnote-page-block-link::before {
|
||||
content: "";
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex: 0 0 auto;
|
||||
background: center / 16px 16px no-repeat url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4.25 2.25h5.25L12.75 5.5v8.25h-8.5V2.25Z' stroke='%2337352f' stroke-width='1.25' stroke-linejoin='round'/%3E%3Cpath d='M9.5 2.25V5.5h3.25' stroke='%2337352f' stroke-width='1.25' stroke-linejoin='round'/%3E%3Cpath d='M6.25 8h4M6.25 10.25h3' stroke='%2337352f' stroke-width='1.1' stroke-linecap='round'/%3E%3C/svg%3E");
|
||||
opacity: 0.86;
|
||||
}
|
||||
|
||||
.mnote-navigation-page__item.mnote-page-block-link:hover {
|
||||
background: rgba(55, 53, 47, 0.08);
|
||||
text-decoration-color: rgba(55, 53, 47, 0.68);
|
||||
}
|
||||
|
||||
.document-main-editor-group {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
@@ -2541,8 +2571,8 @@
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-settings-panel {
|
||||
width: min(1120px, calc(100vw - 24px));
|
||||
max-height: calc(100vh - 72px);
|
||||
width: min(1180px, calc(100vw - 24px));
|
||||
max-height: calc(100vh - 32px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -2558,7 +2588,7 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 16px 18px;
|
||||
padding: 10px 16px;
|
||||
border-bottom: 1px solid #E5E7EB;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
@@ -2566,11 +2596,13 @@
|
||||
.mnote-weknora-kb-page-title {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-page-eyebrow {
|
||||
display: none;
|
||||
color: #2F7D4A;
|
||||
font: 11px/16px "JetBrains Mono", "SFMono-Regular", Consolas, monospace;
|
||||
letter-spacing: 0;
|
||||
@@ -2579,8 +2611,8 @@
|
||||
|
||||
.mnote-weknora-kb-page-title strong {
|
||||
color: #111827;
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
font-size: 17px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-page-title span:last-child {
|
||||
@@ -2614,19 +2646,23 @@
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-page-status {
|
||||
padding: 10px 18px;
|
||||
padding: 6px 16px;
|
||||
border-bottom: 1px solid #E5E7EB;
|
||||
background: #F9FAFB;
|
||||
color: #4B5563;
|
||||
background: #F7FBF8;
|
||||
color: #166534;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-page-body {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(260px, 320px) minmax(0, 1fr);
|
||||
min-height: min(620px, calc(100vh - 190px));
|
||||
max-height: calc(100vh - 150px);
|
||||
grid-template-columns: minmax(220px, 260px) minmax(0, 1fr);
|
||||
min-height: min(710px, calc(100vh - 122px));
|
||||
max-height: calc(100vh - 96px);
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-page-body[data-kb-list-hidden="true"] {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-list-pane,
|
||||
@@ -2644,6 +2680,10 @@
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-list-pane[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-list-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -2675,6 +2715,7 @@
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-list-head button,
|
||||
.mnote-knowledge-rag-kb-manager button,
|
||||
.mnote-weknora-doc-toolbar button,
|
||||
.mnote-weknora-doc-actions button {
|
||||
display: inline-flex;
|
||||
@@ -2698,12 +2739,33 @@
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-list-head button:hover,
|
||||
.mnote-knowledge-rag-kb-manager button:hover:not(:disabled),
|
||||
.mnote-weknora-doc-toolbar button:hover,
|
||||
.mnote-weknora-doc-actions button:hover {
|
||||
background: #F3F4F6;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-kb-manager button:disabled,
|
||||
.mnote-weknora-doc-toolbar button:disabled,
|
||||
.mnote-weknora-doc-actions button:disabled {
|
||||
opacity: .48;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.mnote-weknora-doc-toolbar button.mnote-knowledge-rag-primary-action,
|
||||
.mnote-weknora-doc-actions button.mnote-knowledge-rag-primary-action {
|
||||
border-color: #2F7D4A;
|
||||
background: #2F7D4A;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.mnote-weknora-doc-toolbar button.mnote-knowledge-rag-primary-action:hover,
|
||||
.mnote-weknora-doc-actions button.mnote-knowledge-rag-primary-action:hover {
|
||||
background: #24683C;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-card-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -2713,7 +2775,7 @@
|
||||
.mnote-weknora-kb-card {
|
||||
display: grid;
|
||||
grid-template-columns: 28px minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
align-items: start;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
@@ -2820,6 +2882,10 @@
|
||||
background: #F9FAFB;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-create-card[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-create-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -2832,30 +2898,40 @@
|
||||
.mnote-weknora-kb-detail-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
gap: 10px;
|
||||
overflow: hidden;
|
||||
padding: 10px 14px 14px;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-detail-hero {
|
||||
order: 1;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(210px, 280px);
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
padding: 16px;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-detail-hero[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-detail-hero select[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-detail-hero h2 {
|
||||
margin: 8px 0 4px;
|
||||
margin: 0 0 2px;
|
||||
color: #111827;
|
||||
font-size: 22px;
|
||||
line-height: 30px;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-breadcrumb {
|
||||
display: flex;
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
min-width: 0;
|
||||
@@ -2880,7 +2956,59 @@
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-type-pill {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-kb-manager {
|
||||
order: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-kb-manager-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-kb-select-field {
|
||||
display: flex;
|
||||
flex: 1 1 300px;
|
||||
min-width: 220px;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
color: #6B7280;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-kb-select-field select {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
background: #FFFFFF;
|
||||
color: #111827;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-kb-manager-note {
|
||||
color: #6B7280;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-detail-tabs {
|
||||
order: 2;
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
@@ -2909,10 +3037,169 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-detail-tab[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-detail-panel[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-detail-panel:not([hidden]) {
|
||||
order: 3;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mnote-kb-search-form {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.mnote-kb-search-box {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 34px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
background: #F9FAFB;
|
||||
}
|
||||
|
||||
.mnote-kb-search-box .material-symbols-outlined {
|
||||
flex: 0 0 auto;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.mnote-kb-search-input {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
color: #111827;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.mnote-kb-search-results {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.mnote-kb-search-empty {
|
||||
display: grid;
|
||||
min-height: 160px;
|
||||
place-items: center;
|
||||
border: 1px dashed #D1D5DB;
|
||||
border-radius: 8px;
|
||||
background: #F9FAFB;
|
||||
color: #6B7280;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.mnote-kb-search-empty.is-error {
|
||||
border-color: #FCA5A5;
|
||||
background: #FEF2F2;
|
||||
color: #B91C1C;
|
||||
}
|
||||
|
||||
.mnote-kb-search-result-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 7px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.mnote-kb-search-result-header,
|
||||
.mnote-kb-search-result-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mnote-kb-search-result-index,
|
||||
.mnote-kb-search-result-score {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 20px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
padding: 0 7px;
|
||||
}
|
||||
|
||||
.mnote-kb-search-result-index {
|
||||
background: #EEF2FF;
|
||||
color: #3730A3;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mnote-kb-search-result-score {
|
||||
background: #ECFDF5;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.mnote-kb-search-result-source {
|
||||
overflow: hidden;
|
||||
color: #374151;
|
||||
font: 11px/16px "JetBrains Mono", "SFMono-Regular", Consolas, monospace;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mnote-kb-search-result-preview,
|
||||
.mnote-kb-search-result-citation {
|
||||
color: #4B5563;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.mnote-kb-search-result-citation {
|
||||
padding-top: 6px;
|
||||
border-top: 1px solid #F3F4F6;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.mnote-kb-search-result-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.mnote-kb-search-result-actions button {
|
||||
min-height: 26px;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 6px;
|
||||
background: #FFFFFF;
|
||||
color: #374151;
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.mnote-kb-search-result-actions button:hover:not(:disabled) {
|
||||
background: #F3F4F6;
|
||||
}
|
||||
|
||||
.mnote-weknora-doc-toolbar,
|
||||
.mnote-weknora-doc-actions {
|
||||
display: flex;
|
||||
@@ -2922,10 +3209,11 @@
|
||||
}
|
||||
|
||||
.mnote-weknora-doc-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 184px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
align-items: start;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.mnote-weknora-doc-sidebar {
|
||||
@@ -2939,6 +3227,10 @@
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.mnote-weknora-doc-sidebar[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mnote-weknora-doc-sidebar-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
@@ -2997,11 +3289,21 @@
|
||||
|
||||
.mnote-weknora-doc-main {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.mnote-weknora-doc-toolbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
padding-bottom: 0;
|
||||
background: #F6F7F9;
|
||||
}
|
||||
|
||||
.mnote-weknora-view-switch {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -3050,7 +3352,9 @@
|
||||
}
|
||||
|
||||
.mnote-weknora-doc-table {
|
||||
overflow: hidden;
|
||||
flex: 1 1 auto;
|
||||
min-height: 440px;
|
||||
overflow: auto;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
background: #FFFFFF;
|
||||
@@ -3065,13 +3369,13 @@
|
||||
|
||||
.mnote-weknora-doc-table::before {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 104px 104px 124px;
|
||||
grid-template-columns: minmax(0, 1fr) 132px;
|
||||
gap: 8px;
|
||||
padding: 9px 12px;
|
||||
border-bottom: 1px solid #E5E7EB;
|
||||
background: #F9FAFB;
|
||||
color: #6B7280;
|
||||
content: "文档 状态 Chunk 操作";
|
||||
content: "资料 操作";
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
white-space: pre;
|
||||
@@ -3112,6 +3416,44 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-diagnostics {
|
||||
order: 4;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-diagnostics summary {
|
||||
cursor: pointer;
|
||||
color: #4B5563;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
list-style: none;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-diagnostics summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-diagnostics summary::after {
|
||||
content: "展开";
|
||||
float: right;
|
||||
color: #9CA3AF;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-diagnostics[open] summary {
|
||||
border-bottom: 1px solid #E5E7EB;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-diagnostics[open] summary::after {
|
||||
content: "收起";
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-diagnostics .mnote-knowledge-rag-meta {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-meta div {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
@@ -3169,6 +3511,20 @@
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-source-composer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-source-composer[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-source-tools {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -3182,17 +3538,17 @@
|
||||
.mnote-knowledge-rag-source-filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-source-filters button {
|
||||
border: 1px solid #E3E1DE;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 999px;
|
||||
background: #FFFFFF;
|
||||
color: #5F5A54;
|
||||
color: #4B5563;
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
padding: 2px 7px;
|
||||
padding: 3px 9px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -3225,7 +3581,7 @@
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-source-input-row {
|
||||
grid-template-columns: minmax(0, 1fr) 56px 28px;
|
||||
grid-template-columns: minmax(0, 1fr) 56px 30px;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-input-status {
|
||||
@@ -3275,7 +3631,7 @@
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-source-main .mnote-weknora-source-aux {
|
||||
display: flex;
|
||||
display: none;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
font-family: inherit;
|
||||
|
||||
Reference in New Issue
Block a user