chore: checkpoint turso and ai runtime work
This commit is contained in:
@@ -8,7 +8,9 @@ use axum::extract::Request;
|
||||
use axum::middleware::Next;
|
||||
use axum::response::Response;
|
||||
use axum::Router;
|
||||
use control_plane::{ControlPlaneStore, SqliteControlPlaneStore};
|
||||
use control_plane::ControlPlaneStore;
|
||||
#[cfg(test)]
|
||||
use control_plane::SqliteControlPlaneStore;
|
||||
#[cfg(not(test))]
|
||||
use control_plane::{TursoControlPlaneConfig, TursoControlPlaneMode, TursoControlPlaneStore};
|
||||
use std::env;
|
||||
@@ -201,7 +203,7 @@ impl AppState {
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn open_control_plane_store() -> Arc<dyn ControlPlaneStore> {
|
||||
Arc::new(SqliteControlPlaneStore::in_memory().expect("初始化测试 SQLite 控制面"))
|
||||
Arc::new(SqliteControlPlaneStore::in_memory().expect("初始化测试 control-plane"))
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
@@ -210,9 +212,8 @@ pub(crate) fn open_control_plane_store() -> Arc<dyn ControlPlaneStore> {
|
||||
.ok()
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or_else(|| "sqlite".to_string());
|
||||
.unwrap_or_else(|| "libsql-local".to_string());
|
||||
match backend.as_str() {
|
||||
"sqlite" => open_sqlite_control_plane_store(),
|
||||
"libsql-local" | "turso-local" | "turso" => open_turso_local_control_plane_store(),
|
||||
"turso-remote" => open_turso_remote_control_plane_store(),
|
||||
"turso-local-replica" | "turso-remote-replica" => {
|
||||
@@ -220,33 +221,16 @@ pub(crate) fn open_control_plane_store() -> Arc<dyn ControlPlaneStore> {
|
||||
}
|
||||
"turso-synced" => open_turso_synced_control_plane_store(),
|
||||
other => panic!(
|
||||
"不支持的控制面后端 {other},支持 sqlite/libsql-local/turso-remote/turso-local-replica/turso-synced"
|
||||
"不支持的控制面后端 {other},mnote-web 运行时只支持 libsql-local/turso-remote/turso-local-replica/turso-synced;SQLite 仅保留给 control-plane-admin 迁移/导出和测试"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
fn open_sqlite_control_plane_store() -> Arc<dyn ControlPlaneStore> {
|
||||
let db_path = env::var("MNOTE_CONTROL_PLANE_DB_PATH")
|
||||
.ok()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or_else(|| "/mnt/Data1T/Mnote_data/control-plane/control-plane.db".to_string());
|
||||
if let Some(parent) = std::path::Path::new(&db_path).parent() {
|
||||
fs::create_dir_all(parent).expect("创建 SQLite 控制面目录");
|
||||
}
|
||||
Arc::new(SqliteControlPlaneStore::open(&db_path).expect("初始化 SQLite 控制面"))
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
fn open_turso_local_control_plane_store() -> Arc<dyn ControlPlaneStore> {
|
||||
let db_path = env::var("MNOTE_TURSO_LOCAL_PATH")
|
||||
.ok()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.or_else(|| {
|
||||
env::var("MNOTE_CONTROL_PLANE_DB_PATH")
|
||||
.ok()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
"/mnt/Data1T/Mnote_data/control-plane/control-plane-libsql.db".to_string()
|
||||
});
|
||||
@@ -412,7 +396,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_state_initializes_sqlite_control_plane_store() {
|
||||
fn app_state_initializes_control_plane_store_for_tests() {
|
||||
let state = AppState::new(test_config());
|
||||
|
||||
state
|
||||
|
||||
@@ -70,7 +70,7 @@ pub async fn query(
|
||||
let body = serde_json::from_value::<KnowledgeRagQueryRequest>(args).map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
"mnote_knowledge_rag_query_payload_invalid",
|
||||
format!("资料库问答参数无效: {error}"),
|
||||
format!("知识库问答参数无效: {error}"),
|
||||
)
|
||||
.with_context(context)
|
||||
})?;
|
||||
@@ -95,7 +95,7 @@ pub async fn open_reference(
|
||||
serde_json::from_value::<KnowledgeRagOpenReferenceRequest>(args).map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
"mnote_knowledge_rag_open_reference_payload_invalid",
|
||||
format!("资料库引用打开参数无效: {error}"),
|
||||
format!("知识库引用打开参数无效: {error}"),
|
||||
)
|
||||
.with_context(context)
|
||||
})?;
|
||||
@@ -119,7 +119,7 @@ pub async fn section_context(
|
||||
serde_json::from_value::<KnowledgeRagSectionContextRequest>(args).map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
"mnote_knowledge_rag_section_context_payload_invalid",
|
||||
format!("资料库章节上下文参数无效: {error}"),
|
||||
format!("知识库章节上下文参数无效: {error}"),
|
||||
)
|
||||
.with_context(context)
|
||||
})?;
|
||||
|
||||
@@ -57,10 +57,6 @@ fn doc_tools() -> Vec<Value> {
|
||||
fn knowledge_rag_tools() -> Vec<Value> {
|
||||
vec![
|
||||
knowledge_rag_status_tool(),
|
||||
weknora_search_tool(),
|
||||
weknora_list_sources_tool(),
|
||||
weknora_get_source_status_tool(),
|
||||
weknora_open_reference_tool(),
|
||||
knowledge_rag_query_tool(),
|
||||
knowledge_rag_section_context_tool(),
|
||||
knowledge_rag_open_reference_tool(),
|
||||
@@ -359,7 +355,7 @@ fn knowledge_rag_status_tool() -> Value {
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.knowledge_rag.status",
|
||||
"description": "查看当前资料库 provider 状态、dashboard 地址、source registry 和同步状态。默认 provider 是 WeKnora;LightRAG 仅作为 legacy fallback。",
|
||||
"description": "查看当前知识库 provider 状态、dashboard 地址、source registry 和同步状态。默认 provider 是 RAGFlow;WeKnora/LightRAG 仅作为 legacy fallback。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["knowledge_rag.read", "evidence.read"],
|
||||
"status": "available",
|
||||
@@ -371,114 +367,6 @@ fn knowledge_rag_status_tool() -> Value {
|
||||
})
|
||||
}
|
||||
|
||||
fn weknora_scope_properties() -> Value {
|
||||
let mut properties = base_identity_properties();
|
||||
if let Value::Object(map) = &mut properties {
|
||||
map.insert("rootUri".into(), json!({ "type": "string" }));
|
||||
map.insert("scope".into(), json!({ "type": "object" }));
|
||||
map.insert(
|
||||
"allowlist".into(),
|
||||
json!({ "type": "array", "items": { "type": "string" } }),
|
||||
);
|
||||
map.insert("allowedRoots".into(), json!({ "type": "array" }));
|
||||
map.insert("aiAccessScope".into(), json!({ "type": "object" }));
|
||||
map.insert(
|
||||
"sourcePaths".into(),
|
||||
json!({ "type": "array", "items": { "type": "string" } }),
|
||||
);
|
||||
}
|
||||
properties
|
||||
}
|
||||
|
||||
fn weknora_search_tool() -> Value {
|
||||
let mut properties = weknora_scope_properties();
|
||||
if let Value::Object(map) = &mut properties {
|
||||
map.insert("query".into(), json!({ "type": "string" }));
|
||||
map.insert("topK".into(), json!({ "type": "integer", "default": 10 }));
|
||||
map.insert(
|
||||
"includeChunkContent".into(),
|
||||
json!({ "type": "boolean", "default": true }),
|
||||
);
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.weknora.search",
|
||||
"description": "只读调用 WeKnora 检索。必须带 rootUri 和 scope/allowlist,返回 provider ids、MNote source registry 映射以及 locatorDegraded;不要把 WeKnora filename/chunk id 当成本地 path。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["knowledge_rag.read", "evidence.read"],
|
||||
"status": "available",
|
||||
"annotations": tool_annotations(true, false, true, false),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["workspaceId", "rootUri", "query"],
|
||||
"properties": properties
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn weknora_list_sources_tool() -> Value {
|
||||
json!({
|
||||
"name": "mnote.weknora.list_sources",
|
||||
"description": "只读列出 MNote source registry 与 WeKnora provider 状态。必须带 rootUri 和 scope/allowlist。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["knowledge_rag.read", "evidence.read"],
|
||||
"status": "available",
|
||||
"annotations": tool_annotations(true, false, true, false),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["workspaceId", "rootUri"],
|
||||
"properties": weknora_scope_properties()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn weknora_get_source_status_tool() -> Value {
|
||||
let mut properties = weknora_scope_properties();
|
||||
if let Value::Object(map) = &mut properties {
|
||||
map.insert("sourcePath".into(), json!({ "type": "string" }));
|
||||
map.insert("providerKnowledgeId".into(), json!({ "type": "string" }));
|
||||
map.insert("providerSourceId".into(), json!({ "type": "string" }));
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.weknora.get_source_status",
|
||||
"description": "只读查询某个 MNote source 或 WeKnora provider knowledge id 的映射状态。必须带 rootUri 和 scope/allowlist。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["knowledge_rag.read", "evidence.read"],
|
||||
"status": "available",
|
||||
"annotations": tool_annotations(true, false, true, false),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["workspaceId", "rootUri"],
|
||||
"properties": properties
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn weknora_open_reference_tool() -> Value {
|
||||
let mut properties = weknora_scope_properties();
|
||||
if let Value::Object(map) = &mut properties {
|
||||
map.insert("reference".into(), json!({ "type": "object" }));
|
||||
map.insert(
|
||||
"providerKnowledgeBaseId".into(),
|
||||
json!({ "type": "string" }),
|
||||
);
|
||||
map.insert("providerKnowledgeId".into(), json!({ "type": "string" }));
|
||||
map.insert("providerChunkId".into(), json!({ "type": "string" }));
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.weknora.open_reference",
|
||||
"description": "只读把 WeKnora reference 映射为 MNote open action。provider ids 独立返回;未命中 source registry 时必须视为 locatorDegraded。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["knowledge_rag.read", "evidence.read"],
|
||||
"status": "available",
|
||||
"annotations": tool_annotations(true, false, true, false),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["workspaceId", "rootUri", "reference"],
|
||||
"properties": properties
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn knowledge_rag_query_tool() -> Value {
|
||||
let mut properties = base_identity_properties();
|
||||
if let Value::Object(map) = &mut properties {
|
||||
@@ -517,7 +405,7 @@ fn knowledge_rag_query_tool() -> Value {
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.knowledge_rag.query",
|
||||
"description": "向当前资料库 provider 提问,返回 provider 原始结果和经 MNote registry 映射、sourcePaths 后过滤的 references/citations。默认 provider 是 WeKnora,LightRAG 仅 legacy fallback。用户要求链接、来源、引用或证据时优先调用;返回的 citationMarkdown 由 MNote 前端自动追加成可点击来源定位,agent 不应在最终回答中手写 /documents、mnote:// 或搜索引擎包装链接。",
|
||||
"description": "向当前知识库 provider 提问,返回 provider 原始结果和经 MNote registry 映射、sourcePaths 后过滤的 references/citations。默认 provider 是 RAGFlow,WeKnora/LightRAG 仅 legacy fallback。用户要求链接、来源、引用或证据时优先调用;返回的 citationMarkdown 由 MNote 前端自动追加成可点击来源定位,agent 不应在最终回答中手写 /documents、mnote:// 或搜索引擎包装链接。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["knowledge_rag.read", "evidence.read"],
|
||||
"status": "available",
|
||||
|
||||
@@ -37,8 +37,8 @@ const CAPABILITY_PACKS: &[MnoteCapabilityPack] = &[
|
||||
},
|
||||
MnoteCapabilityPack {
|
||||
id: "mnote-knowledge-rag",
|
||||
title: "资料库问答",
|
||||
description: "通过当前知识库 provider 检索多本书、论文、PDF 和附件资料库,并返回可回跳来源;默认 provider 是 WeKnora。",
|
||||
title: "知识库问答",
|
||||
description: "通过当前知识库 provider 检索多本书、论文、PDF 和附件,并返回可回跳来源;默认 provider 是 RAGFlow。",
|
||||
category: "knowledge",
|
||||
agent_ids: &["hermes", "reasonix"],
|
||||
read_only: true,
|
||||
@@ -46,10 +46,6 @@ const CAPABILITY_PACKS: &[MnoteCapabilityPack] = &[
|
||||
tool_names: &[
|
||||
"mnote.context.snapshot",
|
||||
"mnote.context.resolve_target",
|
||||
"mnote.weknora.search",
|
||||
"mnote.weknora.list_sources",
|
||||
"mnote.weknora.get_source_status",
|
||||
"mnote.weknora.open_reference",
|
||||
"mnote.knowledge_rag.status",
|
||||
"mnote.knowledge_rag.query",
|
||||
"mnote.knowledge_rag.section_context",
|
||||
|
||||
@@ -134,10 +134,10 @@ pub async fn auth_api(
|
||||
.with_header(HEADER_MNOTE_WEB_OWNER, "mnote-web"));
|
||||
}
|
||||
if action == "auth:signOut" {
|
||||
return Ok(build_sqlite_sign_out_response(&state, &context));
|
||||
return Ok(build_control_plane_sign_out_response(&state, &context));
|
||||
}
|
||||
|
||||
handle_sqlite_auth_action(&state, &context, &payload).await
|
||||
handle_control_plane_auth_action(&state, &context, &payload).await
|
||||
}
|
||||
|
||||
pub async fn auth_entry(
|
||||
@@ -2070,7 +2070,7 @@ fn has_real_auth_context(state: &AppState, context: &RequestContext) -> bool {
|
||||
|| extract_cookie_value(context, COOKIE_MNOTE_WEB_CONVEX_TOKEN).is_some()
|
||||
}
|
||||
|
||||
async fn handle_sqlite_auth_action(
|
||||
async fn handle_control_plane_auth_action(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
payload: &serde_json::Value,
|
||||
@@ -2131,7 +2131,7 @@ async fn handle_sqlite_auth_action(
|
||||
role: None,
|
||||
password_hash: None,
|
||||
})
|
||||
.map_err(|error| sqlite_auth_error(context, error))?;
|
||||
.map_err(|error| control_plane_auth_error(context, error))?;
|
||||
state
|
||||
.control_plane()
|
||||
.create_password_identity(CreatePasswordIdentityInput {
|
||||
@@ -2140,11 +2140,11 @@ async fn handle_sqlite_auth_action(
|
||||
username: user.username.clone(),
|
||||
password: password.clone(),
|
||||
})
|
||||
.map_err(|error| sqlite_auth_error(context, error))?;
|
||||
.map_err(|error| control_plane_auth_error(context, error))?;
|
||||
state
|
||||
.control_plane()
|
||||
.ensure_default_workspace(&user.id)
|
||||
.map_err(|error| sqlite_auth_error(context, error))?;
|
||||
.map_err(|error| control_plane_auth_error(context, error))?;
|
||||
state
|
||||
.control_plane()
|
||||
.authenticate_password(AuthenticatePasswordInput {
|
||||
@@ -2156,7 +2156,7 @@ async fn handle_sqlite_auth_action(
|
||||
ip_hash: None,
|
||||
expires_at: None,
|
||||
})
|
||||
.map_err(|error| sqlite_auth_error(context, error))?
|
||||
.map_err(|error| control_plane_auth_error(context, error))?
|
||||
} else {
|
||||
let account = params
|
||||
.get("account")
|
||||
@@ -2181,7 +2181,7 @@ async fn handle_sqlite_auth_action(
|
||||
ip_hash: None,
|
||||
expires_at: None,
|
||||
})
|
||||
.map_err(|error| sqlite_auth_error(context, error))?
|
||||
.map_err(|error| control_plane_auth_error(context, error))?
|
||||
};
|
||||
|
||||
let audit_action = if flow == "signUp" {
|
||||
@@ -2202,7 +2202,7 @@ async fn handle_sqlite_auth_action(
|
||||
.unwrap_or_else(|_| "{}".to_string()),
|
||||
});
|
||||
|
||||
let provider_sync_results = if flow == "signUp" {
|
||||
let provider_sync_results = if matches!(flow, "signUp" | "signIn") {
|
||||
let directory_grants = state
|
||||
.control_plane()
|
||||
.list_directory_grants_for_actor(&resolved.user.id)
|
||||
@@ -2233,13 +2233,13 @@ async fn handle_sqlite_auth_action(
|
||||
None
|
||||
};
|
||||
|
||||
let mut response = build_sqlite_auth_response(
|
||||
let mut response = build_control_plane_auth_response(
|
||||
context,
|
||||
&session_token,
|
||||
&resolved.user.id,
|
||||
resolved.user.email.as_deref().unwrap_or_default(),
|
||||
&resolved.user.display_name,
|
||||
&effective_sqlite_auth_actor_type(&resolved.user.id, &resolved.user.role),
|
||||
&effective_control_plane_auth_actor_type(&resolved.user.id, &resolved.user.role),
|
||||
);
|
||||
if let Some(results) = provider_sync_results {
|
||||
let all_ok = results.iter().all(|item| item.ok);
|
||||
@@ -2253,7 +2253,7 @@ async fn handle_sqlite_auth_action(
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn sqlite_auth_error(
|
||||
fn control_plane_auth_error(
|
||||
context: &RequestContext,
|
||||
error: control_plane::ControlPlaneError,
|
||||
) -> WebError {
|
||||
@@ -2273,7 +2273,7 @@ fn sqlite_auth_error(
|
||||
.with_header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
}
|
||||
|
||||
fn build_sqlite_auth_response(
|
||||
fn build_control_plane_auth_response(
|
||||
context: &RequestContext,
|
||||
session_token: &str,
|
||||
user_id: &str,
|
||||
@@ -2286,7 +2286,7 @@ fn build_sqlite_auth_response(
|
||||
"userId": user_id,
|
||||
"email": email,
|
||||
"name": name,
|
||||
"authMode": "sqliteSession",
|
||||
"authMode": "controlPlaneSession",
|
||||
"actorType": actor_type
|
||||
}))
|
||||
.into_response();
|
||||
@@ -2307,7 +2307,7 @@ fn build_sqlite_auth_response(
|
||||
response
|
||||
}
|
||||
|
||||
fn effective_sqlite_auth_actor_type(user_id: &str, stored_role: &str) -> String {
|
||||
fn effective_control_plane_auth_actor_type(user_id: &str, stored_role: &str) -> String {
|
||||
let role = stored_role.trim();
|
||||
let fallback_role = if role.is_empty() { "user" } else { role };
|
||||
if crate::routes::local_folder_source::is_local_access_policy_admin_actor(
|
||||
@@ -2320,7 +2320,7 @@ fn effective_sqlite_auth_actor_type(user_id: &str, stored_role: &str) -> String
|
||||
}
|
||||
}
|
||||
|
||||
fn build_sqlite_sign_out_response(state: &AppState, context: &RequestContext) -> Response {
|
||||
fn build_control_plane_sign_out_response(state: &AppState, context: &RequestContext) -> Response {
|
||||
if let Some(raw_token) = extract_cookie_value(context, COOKIE_MNOTE_SESSION) {
|
||||
let token_hash = session_token_hash(&raw_token);
|
||||
if let Ok(Some(resolved)) = state.control_plane().get_session_by_token_hash(&token_hash) {
|
||||
@@ -3062,7 +3062,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn root_entry_uses_sqlite_session_display_name_for_workspace_label() {
|
||||
async fn root_entry_uses_control_plane_session_display_name_for_workspace_label() {
|
||||
use control_plane::{session_token_hash, CreateSessionInput, UpsertUserInput};
|
||||
|
||||
let app_state = AppState::new(AppConfig {
|
||||
@@ -3095,7 +3095,7 @@ mod tests {
|
||||
role: None,
|
||||
password_hash: None,
|
||||
})
|
||||
.expect("upsert sqlite user");
|
||||
.expect("upsert control-plane user");
|
||||
app_state
|
||||
.control_plane()
|
||||
.create_session(CreateSessionInput {
|
||||
@@ -3106,7 +3106,7 @@ mod tests {
|
||||
ip_hash: None,
|
||||
expires_at: None,
|
||||
})
|
||||
.expect("create sqlite session");
|
||||
.expect("create control-plane session");
|
||||
|
||||
let response = build_app(app_state)
|
||||
.oneshot(
|
||||
@@ -3745,7 +3745,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn auth_api_signup_sets_sqlite_session_cookie_when_compat_disabled() {
|
||||
async fn auth_api_signup_sets_control_plane_session_cookie_when_compat_disabled() {
|
||||
let response = app_with_config("http://127.0.0.1:3100".into(), false)
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
@@ -3786,11 +3786,11 @@ mod tests {
|
||||
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["userId"], "new-user");
|
||||
assert_eq!(payload["email"], "new-user@example.com");
|
||||
assert_eq!(payload["authMode"], "sqliteSession");
|
||||
assert_eq!(payload["authMode"], "controlPlaneSession");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn auth_api_signin_accepts_sqlite_username_after_signup() {
|
||||
async fn auth_api_signin_accepts_control_plane_username_after_signup() {
|
||||
let app = app_with_config("http://127.0.0.1:3100".into(), false);
|
||||
let signup = app
|
||||
.clone()
|
||||
@@ -3829,7 +3829,7 @@ mod tests {
|
||||
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["userId"], "mnote-e2e");
|
||||
assert_eq!(payload["email"], "mnote.e2e@example.com");
|
||||
assert_eq!(payload["authMode"], "sqliteSession");
|
||||
assert_eq!(payload["authMode"], "controlPlaneSession");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -3865,7 +3865,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn auth_api_signout_clears_sqlite_session_cookie() {
|
||||
async fn auth_api_signout_clears_control_plane_session_cookie() {
|
||||
let response = app_with_config("http://127.0.0.1:3100".into(), false)
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
|
||||
@@ -34,7 +34,7 @@ use tracing::{info, warn};
|
||||
|
||||
const HEADER_MNOTE_WEB_OWNER: &str = "x-mnote-web-owner";
|
||||
const HEADER_HERMES_CLIENT_OWNER: &str = "x-mnote-hermes-client-owner";
|
||||
const ACP_RUNTIME_SQLITE_STORE: &str = "sqlite_acp_runtime_store";
|
||||
const ACP_RUNTIME_CONTROL_PLANE_STORE: &str = "control_plane_acp_runtime_store";
|
||||
const ACP_ABORT_NOTIFICATION_TIMEOUT_MS: u64 = 2_500;
|
||||
const LOCAL_SHARE_GRANTS_JSON: &str = "/mnt/Data1T/Mnote_data/control-plane/share-grants.json";
|
||||
const ENV_LOCAL_SHARE_GRANTS_FILE: &str = "MNOTE_SHARE_GRANTS_FILE";
|
||||
@@ -493,7 +493,7 @@ pub async fn search_sessions(
|
||||
Json(json!({
|
||||
"ok": true,
|
||||
"traceId": context.trace.trace_id,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
"results": results
|
||||
})),
|
||||
));
|
||||
@@ -590,24 +590,24 @@ async fn list_acp_sessions(
|
||||
.map(String::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty());
|
||||
let sqlite_runs = state
|
||||
let control_plane_runs = state
|
||||
.control_plane()
|
||||
.list_ai_runtime_runs(&user_id, workspace_id, document_id, session_id, limit)
|
||||
.map_err(|error| {
|
||||
WebError::internal(format!("SQLite ACP session 列表读取失败: {error}"))
|
||||
.with_context(context)
|
||||
})?;
|
||||
if !sqlite_runs.is_empty() {
|
||||
if !control_plane_runs.is_empty() {
|
||||
return Ok((
|
||||
StatusCode::OK,
|
||||
stamp_client_headers(),
|
||||
Json(json!({
|
||||
"ok": true,
|
||||
"traceId": context.trace.trace_id,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"sessionStorage": "sqlite_control_plane",
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
"sessionStorage": "control_plane",
|
||||
"legacyPersistence": "local_ai_session_jsonl",
|
||||
"sessions": sqlite_runs.iter().map(ai_runtime_run_to_json).collect::<Vec<_>>()
|
||||
"sessions": control_plane_runs.iter().map(ai_runtime_run_to_json).collect::<Vec<_>>()
|
||||
})),
|
||||
));
|
||||
}
|
||||
@@ -669,7 +669,7 @@ async fn list_acp_sessions(
|
||||
Json(json!({
|
||||
"ok": true,
|
||||
"traceId": context.trace.trace_id,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
"sessions": runs.iter().map(ai_runtime_run_to_json).collect::<Vec<_>>()
|
||||
})),
|
||||
));
|
||||
@@ -897,8 +897,8 @@ pub async fn create_session(
|
||||
"profile": profile,
|
||||
"title": payload.title.unwrap_or_else(|| "当前页问答".into()),
|
||||
"traceId": trace_id,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"sessionStorage": "sqlite_control_plane",
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
"sessionStorage": "control_plane",
|
||||
"legacyPersistence": "local_ai_session_jsonl",
|
||||
"legacySessionStorage": session_storage,
|
||||
"permissionLevel": permission_level,
|
||||
@@ -973,8 +973,8 @@ pub async fn create_session(
|
||||
"providerKind": "api-chat",
|
||||
"title": payload.title.unwrap_or_else(|| "当前页问答".into()),
|
||||
"traceId": trace_id,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"sessionStorage": "sqlite_control_plane"
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
"sessionStorage": "control_plane"
|
||||
})),
|
||||
));
|
||||
}
|
||||
@@ -1004,7 +1004,7 @@ pub async fn create_session(
|
||||
&runtime_payload,
|
||||
)
|
||||
.await?;
|
||||
persistence = ACP_RUNTIME_SQLITE_STORE;
|
||||
persistence = ACP_RUNTIME_CONTROL_PLANE_STORE;
|
||||
}
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
@@ -1450,7 +1450,7 @@ pub async fn toggle_skill(
|
||||
return Ok((
|
||||
StatusCode::OK,
|
||||
stamp_client_headers(),
|
||||
Json(json!({"ok": true, "skillKind": "mnote_builtin", "configScope": "user_sqlite"})),
|
||||
Json(json!({"ok": true, "skillKind": "mnote_builtin", "configScope": "user_control_plane"})),
|
||||
));
|
||||
}
|
||||
let access =
|
||||
@@ -2220,7 +2220,7 @@ pub async fn toggle_capability(
|
||||
"id": capability_id,
|
||||
"enabled": enabled,
|
||||
"profile": profile,
|
||||
"configScope": "user_sqlite+profile_tool_policy"
|
||||
"configScope": "user_control_plane+profile_tool_policy"
|
||||
})),
|
||||
))
|
||||
}
|
||||
@@ -2324,7 +2324,7 @@ pub async fn resume_session(
|
||||
payload["resumeSource"] = payload
|
||||
.get("persistence")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Value::String(ACP_RUNTIME_SQLITE_STORE.into()));
|
||||
.unwrap_or_else(|| Value::String(ACP_RUNTIME_CONTROL_PLANE_STORE.into()));
|
||||
return Ok((result.0, result.1, Json(payload)));
|
||||
}
|
||||
get_session(
|
||||
@@ -2422,7 +2422,7 @@ pub async fn delete_session(
|
||||
stamp_client_headers(),
|
||||
Json(json!({
|
||||
"ok": true,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
"result": {
|
||||
"ok": true,
|
||||
"sessionId": session_id,
|
||||
@@ -2494,7 +2494,7 @@ pub async fn rename_session(
|
||||
stamp_client_headers(),
|
||||
Json(json!({
|
||||
"ok": true,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
"result": {
|
||||
"ok": true,
|
||||
"sessionId": session_id,
|
||||
@@ -2579,7 +2579,7 @@ pub async fn auto_title_session(
|
||||
stamp_client_headers(),
|
||||
Json(json!({
|
||||
"ok": true,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
"result": {
|
||||
"ok": true,
|
||||
"sessionId": session_id,
|
||||
@@ -2720,8 +2720,8 @@ async fn get_acp_session(
|
||||
Json(json!({
|
||||
"ok": true,
|
||||
"traceId": context.trace.trace_id,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"sessionStorage": "sqlite_control_plane",
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
"sessionStorage": "control_plane",
|
||||
"legacyPersistence": "local_ai_session_jsonl",
|
||||
"sessionId": session_id,
|
||||
"session": {
|
||||
@@ -2804,7 +2804,7 @@ async fn get_acp_session(
|
||||
Json(json!({
|
||||
"ok": true,
|
||||
"traceId": context.trace.trace_id,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
"sessionId": session_id,
|
||||
"session": {
|
||||
"sessionId": session_id,
|
||||
@@ -2983,7 +2983,7 @@ pub async fn create_run(
|
||||
"persistence": persistence_result
|
||||
.get("persistence")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or(ACP_RUNTIME_SQLITE_STORE),
|
||||
.unwrap_or(ACP_RUNTIME_CONTROL_PLANE_STORE),
|
||||
"sessionStorage": persistence_result
|
||||
.get("sessionStorage")
|
||||
.cloned()
|
||||
@@ -3050,7 +3050,7 @@ pub async fn create_run(
|
||||
"persistence": persistence_result
|
||||
.get("persistence")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or(ACP_RUNTIME_SQLITE_STORE),
|
||||
.unwrap_or(ACP_RUNTIME_CONTROL_PLANE_STORE),
|
||||
"sessionStorage": persistence_result
|
||||
.get("sessionStorage")
|
||||
.cloned()
|
||||
@@ -4997,7 +4997,7 @@ fn stamp_mnote_builtin_skill_payload_policy(
|
||||
Value::Bool(ai_preference_bool(state, &actor_id, &key, true)?);
|
||||
skill["builtin"] = Value::Bool(true);
|
||||
skill["configurable"] = Value::Bool(true);
|
||||
skill["configScope"] = Value::String("user_sqlite".to_string());
|
||||
skill["configScope"] = Value::String("user_control_plane".to_string());
|
||||
skill["skillKind"] = Value::String("mnote_builtin".to_string());
|
||||
}
|
||||
}
|
||||
@@ -5411,7 +5411,7 @@ fn mnote_capabilities_payload(
|
||||
"toggleable": skill.get("toggleable").cloned().unwrap_or_else(|| json!(true)),
|
||||
"builtin": true,
|
||||
"configurable": true,
|
||||
"configScope": "user_sqlite+profile_tool_policy",
|
||||
"configScope": "user_control_plane+profile_tool_policy",
|
||||
"skillKind": "mnote_capability",
|
||||
"source": "mnote",
|
||||
"origin": "builtin",
|
||||
@@ -6319,7 +6319,7 @@ fn mnote_builtin_skills_payload(agent_id: Option<&str>) -> Value {
|
||||
"toggleable": true,
|
||||
"builtin": true,
|
||||
"configurable": true,
|
||||
"configScope": "user_sqlite",
|
||||
"configScope": "user_control_plane",
|
||||
"skillKind": "mnote_builtin",
|
||||
"source": "mnote",
|
||||
"origin": "builtin",
|
||||
@@ -6853,7 +6853,7 @@ fn enforce_local_ai_run_access(
|
||||
payload["allowedRoots"] = json!([{
|
||||
"rootUri": root_uri,
|
||||
"permission": if chat_only { "read" } else { "write" },
|
||||
"source": "sqlite_directory_grant",
|
||||
"source": "control_plane_directory_grant",
|
||||
"grantIds": access.grant_ids
|
||||
}]);
|
||||
Ok(())
|
||||
@@ -9903,7 +9903,7 @@ fn ai_runtime_run_to_json(record: &control_plane::AiRuntimeRunRecord) -> Value {
|
||||
"payload": payload,
|
||||
"createdAt": record.created_at,
|
||||
"updatedAt": record.updated_at,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10078,7 +10078,7 @@ fn ai_runtime_event_to_json(record: &control_plane::AiRuntimeEventRecord) -> Val
|
||||
"eventType": record.event_type,
|
||||
"payload": payload,
|
||||
"createdAt": record.created_at,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10210,7 +10210,7 @@ fn page_ai_run_to_journal_json(record: &control_plane::AiRuntimeRunRecord) -> Va
|
||||
"payload": payload,
|
||||
"createdAt": record.created_at,
|
||||
"updatedAt": record.updated_at,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10587,7 +10587,7 @@ fn page_ai_journal_event_to_json(record: &control_plane::AiRuntimeJournalEventRe
|
||||
"acpRuntime": event.acp_runtime,
|
||||
"payload": payload,
|
||||
"createdAt": event.created_at,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10615,7 +10615,7 @@ fn page_ai_synthetic_terminal_event_to_json(
|
||||
"reason": "terminal_status_without_terminal_event"
|
||||
},
|
||||
"createdAt": run.updated_at.clone(),
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
"synthetic": true,
|
||||
})
|
||||
}
|
||||
@@ -11645,8 +11645,8 @@ async fn persist_acp_runtime_run(
|
||||
})?;
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"sessionStorage": "sqlite_control_plane",
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
"sessionStorage": "control_plane",
|
||||
"legacyPersistence": legacy_persistence,
|
||||
"legacySessionStorage": legacy_session_storage,
|
||||
"sessionId": record.session_id,
|
||||
@@ -11824,8 +11824,8 @@ async fn persist_acp_runtime_event(
|
||||
}
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"sessionStorage": "sqlite_control_plane",
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
"sessionStorage": "control_plane",
|
||||
"legacyPersistence": legacy_persistence,
|
||||
"legacySessionStorage": legacy_session_storage,
|
||||
"sessionId": registration.session_id,
|
||||
@@ -12636,7 +12636,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn page_ai_agent_profiles_are_sqlite_user_scoped() {
|
||||
async fn page_ai_agent_profiles_are_control_plane_user_scoped() {
|
||||
let response = app()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
@@ -12743,7 +12743,7 @@ mod tests {
|
||||
.find(|skill| skill["id"] == "mnote-current-page")
|
||||
.expect("current page skill");
|
||||
assert_eq!(current_page["enabled"], false);
|
||||
assert_eq!(current_page["configScope"], "user_sqlite");
|
||||
assert_eq!(current_page["configScope"], "user_control_plane");
|
||||
let mindmap_skill = payload["categories"]
|
||||
.as_array()
|
||||
.expect("categories")
|
||||
@@ -14830,7 +14830,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn api_chat_create_session_persists_sqlite_without_acp_session() {
|
||||
async fn api_chat_create_session_persists_control_plane_without_acp_session() {
|
||||
let _runtime_guard = runtime_lock().lock().expect("runtime lock");
|
||||
clear_runtime_registry();
|
||||
clear_acp_run_payloads();
|
||||
@@ -14861,8 +14861,8 @@ mod tests {
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_SQLITE_STORE);
|
||||
assert_eq!(payload["sessionStorage"], "sqlite_control_plane");
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_CONTROL_PLANE_STORE);
|
||||
assert_eq!(payload["sessionStorage"], "control_plane");
|
||||
assert_eq!(payload["providerKind"], "api-chat");
|
||||
assert_eq!(payload["profile"], "api-gpt-chat");
|
||||
}
|
||||
@@ -14998,7 +14998,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn api_chat_session_detail_restores_messages_from_sqlite_events() {
|
||||
async fn api_chat_session_detail_restores_messages_from_control_plane_events() {
|
||||
let _env_guard = env_lock().lock().expect("env lock");
|
||||
let _runtime_guard = runtime_lock().lock().expect("runtime lock");
|
||||
clear_runtime_registry();
|
||||
@@ -15855,12 +15855,12 @@ mod tests {
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["ok"], true);
|
||||
assert_eq!(payload["sessionId"], "mnote_doc_1_trace_1");
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_SQLITE_STORE);
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_CONTROL_PLANE_STORE);
|
||||
assert!(payload.get("messages").is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_client_local_acp_session_create_writes_sqlite_and_private_jsonl() {
|
||||
async fn hermes_client_local_acp_session_create_writes_control_plane_and_private_jsonl() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-local-ai-session-private-{}",
|
||||
std::process::id()
|
||||
@@ -15931,8 +15931,8 @@ mod tests {
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_SQLITE_STORE);
|
||||
assert_eq!(payload["sessionStorage"], "sqlite_control_plane");
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_CONTROL_PLANE_STORE);
|
||||
assert_eq!(payload["sessionStorage"], "control_plane");
|
||||
assert_eq!(payload["legacyPersistence"], "local_ai_session_jsonl");
|
||||
assert_eq!(payload["legacySessionStorage"], "local_private");
|
||||
let session_id = payload["sessionId"].as_str().expect("session id");
|
||||
@@ -15965,8 +15965,8 @@ mod tests {
|
||||
.await
|
||||
.expect("list body");
|
||||
let list_payload: Value = serde_json::from_slice(&list_body).expect("list json");
|
||||
assert_eq!(list_payload["persistence"], ACP_RUNTIME_SQLITE_STORE);
|
||||
assert_eq!(list_payload["sessionStorage"], "sqlite_control_plane");
|
||||
assert_eq!(list_payload["persistence"], ACP_RUNTIME_CONTROL_PLANE_STORE);
|
||||
assert_eq!(list_payload["sessionStorage"], "control_plane");
|
||||
assert_eq!(list_payload["sessions"][0]["sessionId"], session_id);
|
||||
|
||||
let detail_uri = format!(
|
||||
@@ -15991,7 +15991,7 @@ mod tests {
|
||||
.await
|
||||
.expect("detail body");
|
||||
let detail_payload: Value = serde_json::from_slice(&detail_body).expect("detail json");
|
||||
assert_eq!(detail_payload["persistence"], ACP_RUNTIME_SQLITE_STORE);
|
||||
assert_eq!(detail_payload["persistence"], ACP_RUNTIME_CONTROL_PLANE_STORE);
|
||||
assert_eq!(
|
||||
detail_payload["session"]["runs"][0]["sessionId"],
|
||||
session_id
|
||||
@@ -16001,7 +16001,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_client_local_acp_run_writes_sqlite_and_private_jsonl_without_convex() {
|
||||
async fn hermes_client_local_acp_run_writes_control_plane_and_private_jsonl_without_convex() {
|
||||
let root =
|
||||
std::env::temp_dir().join(format!("mnote-local-ai-run-private-{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
@@ -16069,8 +16069,8 @@ mod tests {
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_SQLITE_STORE);
|
||||
assert_eq!(payload["sessionStorage"], "sqlite_control_plane");
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_CONTROL_PLANE_STORE);
|
||||
assert_eq!(payload["sessionStorage"], "control_plane");
|
||||
assert_eq!(payload["legacyPersistence"], "local_ai_session_jsonl");
|
||||
assert_eq!(payload["legacySessionStorage"], "local_private");
|
||||
let jsonl_path = root
|
||||
@@ -16085,9 +16085,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_client_local_acp_session_create_accepts_sqlite_directory_grant() {
|
||||
async fn hermes_client_local_acp_session_create_accepts_control_plane_directory_grant() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-local-ai-session-sqlite-grant-{}",
|
||||
"mnote-local-ai-session-control-plane-grant-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
@@ -16136,7 +16136,7 @@ mod tests {
|
||||
"documentId": "local-md:README.md",
|
||||
"sourceKind": "local_folder",
|
||||
"rootUri": root_uri,
|
||||
"traceId": "trace_sqlite_grant_session_1",
|
||||
"traceId": "trace_control_plane_grant_session_1",
|
||||
"profile": "reasonix",
|
||||
"title": "授权本地会话"
|
||||
})
|
||||
@@ -16152,8 +16152,8 @@ mod tests {
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_SQLITE_STORE);
|
||||
assert_eq!(payload["sessionStorage"], "sqlite_control_plane");
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_CONTROL_PLANE_STORE);
|
||||
assert_eq!(payload["sessionStorage"], "control_plane");
|
||||
assert_eq!(payload["legacyPersistence"], "local_ai_session_jsonl");
|
||||
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
@@ -16234,7 +16234,7 @@ mod tests {
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["sessionStorage"], "sqlite_control_plane");
|
||||
assert_eq!(payload["sessionStorage"], "control_plane");
|
||||
assert_eq!(payload["legacySessionStorage"], "local_shared");
|
||||
let session_id = payload["sessionId"].as_str().expect("session id");
|
||||
let jsonl_path = root
|
||||
@@ -16435,7 +16435,7 @@ mod tests {
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_SQLITE_STORE);
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_CONTROL_PLANE_STORE);
|
||||
|
||||
assert_eq!(
|
||||
captured_body.lock().expect("captured convex body").clone(),
|
||||
@@ -16444,7 +16444,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_client_acp_run_registers_scoped_runtime_record_in_sqlite() {
|
||||
async fn hermes_client_acp_run_registers_scoped_runtime_record_in_control_plane() {
|
||||
let _guard = runtime_lock().lock().expect("runtime lock");
|
||||
clear_runtime_registry();
|
||||
clear_run_queue();
|
||||
@@ -16482,7 +16482,7 @@ mod tests {
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_SQLITE_STORE);
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_CONTROL_PLANE_STORE);
|
||||
|
||||
let response = router
|
||||
.oneshot(
|
||||
@@ -16500,7 +16500,7 @@ mod tests {
|
||||
.await
|
||||
.expect("list body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_SQLITE_STORE);
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_CONTROL_PLANE_STORE);
|
||||
let run = &payload["sessions"][0];
|
||||
assert_eq!(run["sessionId"], "sess_1");
|
||||
assert_eq!(run["runId"], "run_trace_1");
|
||||
@@ -16753,7 +16753,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_client_acp_session_rename_uses_sqlite_store() {
|
||||
async fn hermes_client_acp_session_rename_uses_control_plane_store() {
|
||||
let response = build_app(seeded_acp_state())
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
@@ -16772,13 +16772,13 @@ mod tests {
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["ok"], true);
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_SQLITE_STORE);
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_CONTROL_PLANE_STORE);
|
||||
assert_eq!(payload["result"]["title"], "新标题");
|
||||
assert_eq!(payload["result"]["runs"][0]["sessionId"], "sess_1");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_client_acp_session_export_uses_sqlite_store() {
|
||||
async fn hermes_client_acp_session_export_uses_control_plane_store() {
|
||||
let response = build_app(seeded_acp_state())
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
@@ -16796,7 +16796,7 @@ mod tests {
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["ok"], true);
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_SQLITE_STORE);
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_CONTROL_PLANE_STORE);
|
||||
assert_eq!(
|
||||
payload["export"]["schema"],
|
||||
"mnote.page_ai_session_export.v1"
|
||||
@@ -16818,7 +16818,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_client_acp_session_delete_and_auto_title_use_sqlite_store() {
|
||||
async fn hermes_client_acp_session_delete_and_auto_title_use_control_plane_store() {
|
||||
let router = build_app(seeded_acp_state());
|
||||
|
||||
let response = router
|
||||
@@ -16840,7 +16840,7 @@ mod tests {
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_SQLITE_STORE);
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_CONTROL_PLANE_STORE);
|
||||
assert_eq!(payload["result"]["title"], "自动标题");
|
||||
|
||||
let response = router
|
||||
@@ -16859,7 +16859,7 @@ mod tests {
|
||||
.await
|
||||
.expect("delete body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_SQLITE_STORE);
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_CONTROL_PLANE_STORE);
|
||||
assert_eq!(payload["result"]["deleted"], 1);
|
||||
}
|
||||
|
||||
@@ -17687,7 +17687,7 @@ mod tests {
|
||||
"rootUri": "file:///mnt/Data1T/Mnote_data/users/user_1/我的空间",
|
||||
"permission": "write",
|
||||
"recursive": true,
|
||||
"source": "sqlite_directory_grant",
|
||||
"source": "control_plane_directory_grant",
|
||||
"absolutePath": "/mnt/Data1T/Mnote_data/users/user_1/我的空间"
|
||||
}],
|
||||
"editorTarget": {
|
||||
@@ -18069,7 +18069,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_ai_run_access_rewrites_allowed_roots_from_sqlite_grant() {
|
||||
fn local_ai_run_access_rewrites_allowed_roots_from_control_plane_grant() {
|
||||
let state = test_state();
|
||||
state
|
||||
.control_plane()
|
||||
@@ -18107,7 +18107,7 @@ mod tests {
|
||||
let mut payload = json!({
|
||||
"workspaceId": "local-ws:alice:test",
|
||||
"documentId": "local-md:README.md",
|
||||
"sessionId": "sess_sqlite_roots",
|
||||
"sessionId": "sess_control_plane_roots",
|
||||
"sourceKind": "local_folder",
|
||||
"rootUri": "file:///tmp/mnote-ai-allowed",
|
||||
"allowedRoots": [{"rootUri": "file:///tmp/evil", "permission": "write", "source": "client"}]
|
||||
@@ -18124,7 +18124,7 @@ mod tests {
|
||||
assert_eq!(payload["allowedRoots"][0]["permission"], "write");
|
||||
assert_eq!(
|
||||
payload["allowedRoots"][0]["source"],
|
||||
"sqlite_directory_grant"
|
||||
"control_plane_directory_grant"
|
||||
);
|
||||
assert!(payload["allowedRoots"][0]["grantIds"]
|
||||
.as_array()
|
||||
@@ -18134,7 +18134,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_ai_run_access_rejects_other_user_without_sqlite_grant() {
|
||||
fn local_ai_run_access_rejects_other_user_without_control_plane_grant() {
|
||||
let state = test_state();
|
||||
state
|
||||
.control_plane()
|
||||
|
||||
@@ -368,21 +368,20 @@ pub(crate) async fn execute_mnote_tool_call(
|
||||
Err(WebError::new(
|
||||
StatusCode::GONE,
|
||||
"mnote_evidence_tools_retired",
|
||||
"旧 docs/evidence/LiteParse tools 已退役;请使用 mnote.weknora.search/open_reference 或兼容 mnote.knowledge_rag.query/open_reference",
|
||||
"旧 docs/evidence/LiteParse tools 已退役;请使用兼容 mnote.knowledge_rag.query/open_reference",
|
||||
)
|
||||
.with_context(&context))
|
||||
}
|
||||
"mnote.knowledge_rag.status" => knowledge_rag::status(&state, &context, &input).await,
|
||||
"mnote.weknora.search" => knowledge_rag::search(&state, &context, &input).await,
|
||||
"mnote.weknora.list_sources" => {
|
||||
knowledge_rag::list_sources(&state, &context, &input).await
|
||||
}
|
||||
"mnote.weknora.get_source_status" => {
|
||||
knowledge_rag::get_source_status(&state, &context, &input).await
|
||||
}
|
||||
"mnote.weknora.open_reference" => {
|
||||
knowledge_rag::open_reference(&state, &context, &input).await
|
||||
}
|
||||
"mnote.weknora.search"
|
||||
| "mnote.weknora.list_sources"
|
||||
| "mnote.weknora.get_source_status"
|
||||
| "mnote.weknora.open_reference" => Err(WebError::new(
|
||||
StatusCode::GONE,
|
||||
"mnote_weknora_tools_retired",
|
||||
"WeKnora 专用工具已从默认 agent manifest 移除;请改用 provider-neutral mnote.knowledge_rag.*,或显式启动 legacy WeKnora provider 调试。",
|
||||
)
|
||||
.with_context(&context)),
|
||||
"mnote.knowledge_rag.query" => knowledge_rag::query(&state, &context, &input).await,
|
||||
"mnote.knowledge_rag.section_context" => {
|
||||
knowledge_rag::section_context(&state, &context, &input).await
|
||||
@@ -738,10 +737,6 @@ fn is_read_tool(tool_name: &str) -> bool {
|
||||
| "mnote.evidence.read"
|
||||
| "mnote.evidence.open"
|
||||
| "mnote.knowledge_rag.status"
|
||||
| "mnote.weknora.search"
|
||||
| "mnote.weknora.list_sources"
|
||||
| "mnote.weknora.get_source_status"
|
||||
| "mnote.weknora.open_reference"
|
||||
| "mnote.knowledge_rag.query"
|
||||
| "mnote.knowledge_rag.section_context"
|
||||
| "mnote.knowledge_rag.open_reference"
|
||||
@@ -775,10 +770,6 @@ fn is_evidence_receipt_tool(tool_name: &str) -> bool {
|
||||
| "mnote.evidence.read"
|
||||
| "mnote.evidence.open"
|
||||
| "mnote.knowledge_rag.status"
|
||||
| "mnote.weknora.search"
|
||||
| "mnote.weknora.list_sources"
|
||||
| "mnote.weknora.get_source_status"
|
||||
| "mnote.weknora.open_reference"
|
||||
| "mnote.knowledge_rag.query"
|
||||
| "mnote.knowledge_rag.section_context"
|
||||
| "mnote.knowledge_rag.open_reference"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -264,11 +264,14 @@ fn build_local_folder_watch_batch_payload(
|
||||
root_uri.hash(&mut hasher);
|
||||
workspace_id.hash(&mut hasher);
|
||||
for payload in watcher_payloads {
|
||||
let relative_path = payload
|
||||
let Some(relative_path) = payload
|
||||
.get("relativePath")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())?;
|
||||
.filter(|value| !value.is_empty())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let event_kind = payload
|
||||
.get("eventKind")
|
||||
.and_then(Value::as_str)
|
||||
@@ -286,7 +289,36 @@ fn build_local_folder_watch_batch_payload(
|
||||
changed_paths.push(json!({
|
||||
"relativePath": relative_path,
|
||||
"kind": event_kind,
|
||||
"eventKind": event_kind,
|
||||
"changeType": event_kind,
|
||||
"revision": event_revision,
|
||||
"documentId": payload.get("documentId").cloned().unwrap_or(Value::Null),
|
||||
"sourceKind": payload.get("sourceKind").cloned().unwrap_or(Value::Null),
|
||||
"observedFileVersion": payload
|
||||
.get("observedFileVersion")
|
||||
.cloned()
|
||||
.unwrap_or(Value::Null),
|
||||
"bufferFileVersion": payload
|
||||
.get("bufferFileVersion")
|
||||
.cloned()
|
||||
.unwrap_or(Value::Null),
|
||||
"fileVersion": payload
|
||||
.get("observedFileVersion")
|
||||
.cloned()
|
||||
.or_else(|| payload.get("bufferFileVersion").cloned())
|
||||
.unwrap_or(Value::Null),
|
||||
"lastWriteIntentId": payload
|
||||
.get("lastWriteIntentId")
|
||||
.cloned()
|
||||
.unwrap_or(Value::Null),
|
||||
"lastSaveOperationId": payload
|
||||
.get("lastSaveOperationId")
|
||||
.cloned()
|
||||
.unwrap_or(Value::Null),
|
||||
"selfWriteEcho": payload
|
||||
.get("selfWriteEcho")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false),
|
||||
}));
|
||||
}
|
||||
if seen_kinds.insert(event_kind.to_string()) {
|
||||
@@ -655,7 +687,13 @@ mod tests {
|
||||
vec![
|
||||
json!({
|
||||
"relativePath": "docs/README.md",
|
||||
"documentId": "local-md:docs~2FREADME.md",
|
||||
"eventKind": "Modify(Data)",
|
||||
"observedFileVersion": "sha256:observed",
|
||||
"bufferFileVersion": "sha256:buffer",
|
||||
"lastWriteIntentId": "write-1",
|
||||
"lastSaveOperationId": "save-1",
|
||||
"selfWriteEcho": true,
|
||||
}),
|
||||
json!({
|
||||
"relativePath": "docs/New.md",
|
||||
@@ -671,6 +709,15 @@ mod tests {
|
||||
assert_eq!(payload["watchRevision"]["scope"], "changed_paths");
|
||||
assert_eq!(payload["watchRevision"]["entryCount"], 2);
|
||||
assert_eq!(payload["changedPaths"].as_array().map(Vec::len), Some(2));
|
||||
let first_path = &payload["changedPaths"][0];
|
||||
assert_eq!(first_path["documentId"], "local-md:docs~2FREADME.md");
|
||||
assert_eq!(first_path["eventKind"], "Modify(Data)");
|
||||
assert_eq!(first_path["observedFileVersion"], "sha256:observed");
|
||||
assert_eq!(first_path["bufferFileVersion"], "sha256:buffer");
|
||||
assert_eq!(first_path["fileVersion"], "sha256:observed");
|
||||
assert_eq!(first_path["lastWriteIntentId"], "write-1");
|
||||
assert_eq!(first_path["lastSaveOperationId"], "save-1");
|
||||
assert_eq!(first_path["selfWriteEcho"], true);
|
||||
assert!(
|
||||
payload["affectedParents"]
|
||||
.as_array()
|
||||
|
||||
@@ -658,7 +658,7 @@ pub(crate) fn ensure_local_workspace_access_with_state(
|
||||
}
|
||||
let actor_id = context.auth.actor_id.trim();
|
||||
let canonical_root_uri = file_uri_for_path(&canonical_root);
|
||||
let sqlite_access = state
|
||||
let control_plane_access = state
|
||||
.control_plane()
|
||||
.resolve_access(actor_id, &canonical_root_uri)
|
||||
.unwrap_or_else(|error| {
|
||||
@@ -675,7 +675,7 @@ pub(crate) fn ensure_local_workspace_access_with_state(
|
||||
grant_ids: Vec::new(),
|
||||
}
|
||||
});
|
||||
if local_access_permission_allows(&sqlite_access.permission, mode) {
|
||||
if local_access_permission_allows(&control_plane_access.permission, mode) {
|
||||
return Ok(canonical_root);
|
||||
}
|
||||
ensure_local_workspace_access_for_actor_with_mode(
|
||||
@@ -785,22 +785,8 @@ pub(crate) fn control_plane_status_display() -> String {
|
||||
.ok()
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or_else(|| "sqlite".to_string());
|
||||
.unwrap_or_else(|| "libsql-local".to_string());
|
||||
match backend.as_str() {
|
||||
"sqlite" => {
|
||||
let db_path = std::env::var("MNOTE_CONTROL_PLANE_DB_PATH")
|
||||
.ok()
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or_else(|| {
|
||||
default_local_workspace_base_dir()
|
||||
.join("control-plane")
|
||||
.join("control-plane.db")
|
||||
.display()
|
||||
.to_string()
|
||||
});
|
||||
format!("backend=sqlite; db={db_path}")
|
||||
}
|
||||
"libsql-local" | "turso-local" | "turso" => {
|
||||
let db_path = std::env::var("MNOTE_TURSO_LOCAL_PATH")
|
||||
.ok()
|
||||
@@ -850,6 +836,7 @@ pub(crate) fn control_plane_status_display() -> String {
|
||||
});
|
||||
format!("backend=turso-synced; local={db_path}; remote=env:MNOTE_TURSO_DATABASE_URL")
|
||||
}
|
||||
"sqlite" => "backend=sqlite; unsupported-runtime; use control-plane-admin only".to_string(),
|
||||
other => format!("backend={other}; unsupported"),
|
||||
}
|
||||
}
|
||||
@@ -1211,7 +1198,7 @@ fn append_control_plane_outbox_event(state: &AppState, event_type: &str, payload
|
||||
let _ = state.stream_delta_tx.send(delta);
|
||||
}
|
||||
|
||||
fn list_sqlite_share_links_for_context(
|
||||
fn list_control_plane_share_links_for_context(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
query: ShareLinkListQuery,
|
||||
@@ -1227,15 +1214,15 @@ fn list_sqlite_share_links_for_context(
|
||||
let links = state
|
||||
.control_plane()
|
||||
.list_share_links(workspace_id)
|
||||
.map_err(|error| WebError::internal(format!("SQLite 分享链接读取失败: {error}")))?;
|
||||
.map_err(|error| WebError::internal(format!("control-plane 分享链接读取失败: {error}")))?;
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"controlPlane": "sqlite",
|
||||
"controlPlane": "control-plane",
|
||||
"links": links.iter().map(share_link_payload).collect::<Vec<_>>(),
|
||||
}))
|
||||
}
|
||||
|
||||
fn create_sqlite_share_link_for_context(
|
||||
fn create_control_plane_share_link_for_context(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
request: LocalShareLinkRequest,
|
||||
@@ -1263,7 +1250,7 @@ fn create_sqlite_share_link_for_context(
|
||||
created_by: context.auth.actor_id.trim().to_string(),
|
||||
expires_at: request.expires_at,
|
||||
})
|
||||
.map_err(|error| WebError::internal(format!("SQLite 分享链接写入失败: {error}")))?;
|
||||
.map_err(|error| WebError::internal(format!("control-plane 分享链接写入失败: {error}")))?;
|
||||
let _ = state.control_plane().append_audit(AppendAuditInput {
|
||||
actor_user_id: Some(context.auth.actor_id.trim().to_string()),
|
||||
action: "control.share.created".to_string(),
|
||||
@@ -1286,13 +1273,13 @@ fn create_sqlite_share_link_for_context(
|
||||
);
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"controlPlane": "sqlite",
|
||||
"controlPlane": "control-plane",
|
||||
"token": created.token,
|
||||
"link": share_link_payload(&created.link),
|
||||
}))
|
||||
}
|
||||
|
||||
fn revoke_sqlite_share_link_for_context(
|
||||
fn revoke_control_plane_share_link_for_context(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
link_id: &str,
|
||||
@@ -1312,7 +1299,7 @@ fn revoke_sqlite_share_link_for_context(
|
||||
control_plane::ControlPlaneError::NotFound(message) => {
|
||||
WebError::new(StatusCode::NOT_FOUND, "share_link_not_found", message)
|
||||
}
|
||||
other => WebError::internal(format!("SQLite 分享链接撤销失败: {other}")),
|
||||
other => WebError::internal(format!("control-plane 分享链接撤销失败: {other}")),
|
||||
})?;
|
||||
let _ = state.control_plane().append_audit(AppendAuditInput {
|
||||
actor_user_id: Some(context.auth.actor_id.trim().to_string()),
|
||||
@@ -1330,7 +1317,7 @@ fn revoke_sqlite_share_link_for_context(
|
||||
);
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"controlPlane": "sqlite",
|
||||
"controlPlane": "control-plane",
|
||||
"revokedLinkId": link_id,
|
||||
}))
|
||||
}
|
||||
@@ -1879,7 +1866,7 @@ fn is_default_workspace_auto_grant(grant: &DirectoryGrantRecord) -> bool {
|
||||
&& grant.root_uri.ends_with("/workspaces/my-space")
|
||||
}
|
||||
|
||||
fn sqlite_access_policy_payload(
|
||||
fn control_plane_access_policy_payload(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
) -> Result<Value, WebError> {
|
||||
@@ -1887,14 +1874,14 @@ fn sqlite_access_policy_payload(
|
||||
let grants = state
|
||||
.control_plane()
|
||||
.list_directory_grants()
|
||||
.map_err(|error| WebError::internal(format!("SQLite 控制面授权列表读取失败: {error}")))?;
|
||||
.map_err(|error| WebError::internal(format!("control-plane授权列表读取失败: {error}")))?;
|
||||
let grant_values = grants
|
||||
.iter()
|
||||
.map(control_plane_grant_payload)
|
||||
.collect::<Vec<_>>();
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"controlPlane": "sqlite",
|
||||
"controlPlane": "control-plane",
|
||||
"policyPath": local_access_policy_path().display().to_string(),
|
||||
"policy": {
|
||||
"admins": policy.admins,
|
||||
@@ -1907,7 +1894,7 @@ fn sqlite_access_policy_payload(
|
||||
}))
|
||||
}
|
||||
|
||||
fn sqlite_user_access_policy_payload(
|
||||
fn control_plane_user_access_policy_payload(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
) -> Result<Value, WebError> {
|
||||
@@ -1922,7 +1909,7 @@ fn sqlite_user_access_policy_payload(
|
||||
let grants = state
|
||||
.control_plane()
|
||||
.list_directory_grants()
|
||||
.map_err(|error| WebError::internal(format!("SQLite 控制面授权列表读取失败: {error}")))?;
|
||||
.map_err(|error| WebError::internal(format!("control-plane授权列表读取失败: {error}")))?;
|
||||
let grant_values = grants
|
||||
.iter()
|
||||
.filter(|grant| {
|
||||
@@ -1933,7 +1920,7 @@ fn sqlite_user_access_policy_payload(
|
||||
.collect::<Vec<_>>();
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"controlPlane": "sqlite",
|
||||
"controlPlane": "control-plane",
|
||||
"policy": {
|
||||
"grants": grant_values,
|
||||
},
|
||||
@@ -1956,7 +1943,7 @@ fn validate_local_access_root_for_context(
|
||||
}))
|
||||
}
|
||||
|
||||
fn add_sqlite_user_access_grant_for_context(
|
||||
fn add_control_plane_user_access_grant_for_context(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
mut request: LocalAccessGrantRequest,
|
||||
@@ -1973,19 +1960,19 @@ fn add_sqlite_user_access_grant_for_context(
|
||||
ensure_path_inside_owned_local_workspace(actor_id, &canonical)?;
|
||||
request.root_uri = file_uri_for_path(&canonical);
|
||||
request.root_path = canonical.display().to_string();
|
||||
add_sqlite_local_access_grant_for_context_inner(state, context, request, "user")
|
||||
add_control_plane_local_access_grant_for_context_inner(state, context, request, "user")
|
||||
}
|
||||
|
||||
fn add_sqlite_local_access_grant_for_context(
|
||||
fn add_control_plane_local_access_grant_for_context(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
request: LocalAccessGrantRequest,
|
||||
) -> Result<Value, WebError> {
|
||||
let _policy = require_local_access_policy_admin(context)?;
|
||||
add_sqlite_local_access_grant_for_context_inner(state, context, request, "admin")
|
||||
add_control_plane_local_access_grant_for_context_inner(state, context, request, "admin")
|
||||
}
|
||||
|
||||
fn add_sqlite_local_access_grant_for_context_inner(
|
||||
fn add_control_plane_local_access_grant_for_context_inner(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
request: LocalAccessGrantRequest,
|
||||
@@ -2010,7 +1997,7 @@ fn add_sqlite_local_access_grant_for_context_inner(
|
||||
root_uri: Some(root_uri.clone()),
|
||||
include_revoked: false,
|
||||
})
|
||||
.map_err(|error| WebError::internal(format!("SQLite 控制面授权查重失败: {error}")))?;
|
||||
.map_err(|error| WebError::internal(format!("control-plane授权查重失败: {error}")))?;
|
||||
if duplicates.iter().any(|grant| {
|
||||
grant.permission.trim() == permission
|
||||
&& grant.recursive == request.recursive
|
||||
@@ -2035,7 +2022,7 @@ fn add_sqlite_local_access_grant_for_context_inner(
|
||||
source: source.to_string(),
|
||||
created_by: Some(context.auth.actor_id.trim().to_string()),
|
||||
})
|
||||
.map_err(|error| WebError::internal(format!("SQLite 控制面授权写入失败: {error}")))?;
|
||||
.map_err(|error| WebError::internal(format!("control-plane授权写入失败: {error}")))?;
|
||||
let _ = state.control_plane().append_audit(AppendAuditInput {
|
||||
actor_user_id: Some(context.auth.actor_id.trim().to_string()),
|
||||
action: "control.grant.created".to_string(),
|
||||
@@ -2058,14 +2045,14 @@ fn add_sqlite_local_access_grant_for_context_inner(
|
||||
let grants = state
|
||||
.control_plane()
|
||||
.list_directory_grants()
|
||||
.map_err(|error| WebError::internal(format!("SQLite 控制面授权列表读取失败: {error}")))?;
|
||||
.map_err(|error| WebError::internal(format!("control-plane授权列表读取失败: {error}")))?;
|
||||
let grant_values = grants
|
||||
.iter()
|
||||
.map(control_plane_grant_payload)
|
||||
.collect::<Vec<_>>();
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"controlPlane": "sqlite",
|
||||
"controlPlane": "control-plane",
|
||||
"policyPath": local_access_policy_path().display().to_string(),
|
||||
"grant": control_plane_grant_payload(&grant),
|
||||
"policy": {
|
||||
@@ -2074,7 +2061,7 @@ fn add_sqlite_local_access_grant_for_context_inner(
|
||||
}))
|
||||
}
|
||||
|
||||
fn delete_sqlite_user_access_grant_for_context(
|
||||
fn delete_control_plane_user_access_grant_for_context(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
grant_id: &str,
|
||||
@@ -2102,7 +2089,7 @@ fn delete_sqlite_user_access_grant_for_context(
|
||||
root_uri: None,
|
||||
include_revoked: false,
|
||||
})
|
||||
.map_err(|error| WebError::internal(format!("SQLite 控制面授权查找失败: {error}")))?;
|
||||
.map_err(|error| WebError::internal(format!("control-plane授权查找失败: {error}")))?;
|
||||
let grant = grants.first().ok_or_else(|| {
|
||||
WebError::new(
|
||||
StatusCode::NOT_FOUND,
|
||||
@@ -2134,7 +2121,7 @@ fn delete_sqlite_user_access_grant_for_context(
|
||||
"local_access_policy_grant_not_found",
|
||||
message,
|
||||
),
|
||||
other => WebError::internal(format!("SQLite 控制面授权撤销失败: {other}")),
|
||||
other => WebError::internal(format!("control-plane授权撤销失败: {other}")),
|
||||
})?;
|
||||
let _ = state.control_plane().append_audit(AppendAuditInput {
|
||||
actor_user_id: Some(actor_id.to_string()),
|
||||
@@ -2152,14 +2139,14 @@ fn delete_sqlite_user_access_grant_for_context(
|
||||
);
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"controlPlane": "sqlite",
|
||||
"controlPlane": "control-plane",
|
||||
"deletedGrantId": grant_id,
|
||||
"revokedUserId": revoked_user_id,
|
||||
"policy": {
|
||||
"grants": state
|
||||
.control_plane()
|
||||
.list_directory_grants()
|
||||
.map_err(|error| WebError::internal(format!("SQLite 控制面授权列表读取失败: {error}")))?
|
||||
.map_err(|error| WebError::internal(format!("control-plane授权列表读取失败: {error}")))?
|
||||
.iter()
|
||||
.filter(|grant| grant.created_by.as_deref().map(str::trim) == Some(actor_id))
|
||||
.map(control_plane_grant_payload)
|
||||
@@ -2168,7 +2155,7 @@ fn delete_sqlite_user_access_grant_for_context(
|
||||
}))
|
||||
}
|
||||
|
||||
fn delete_sqlite_local_access_grant_for_context(
|
||||
fn delete_control_plane_local_access_grant_for_context(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
grant_id: &str,
|
||||
@@ -2189,7 +2176,7 @@ fn delete_sqlite_local_access_grant_for_context(
|
||||
root_uri: None,
|
||||
include_revoked: false,
|
||||
})
|
||||
.map_err(|error| WebError::internal(format!("SQLite 控制面授权查找失败: {error}")))?;
|
||||
.map_err(|error| WebError::internal(format!("control-plane授权查找失败: {error}")))?;
|
||||
if grants
|
||||
.first()
|
||||
.map(is_default_workspace_auto_grant)
|
||||
@@ -2211,7 +2198,7 @@ fn delete_sqlite_local_access_grant_for_context(
|
||||
"local_access_policy_grant_not_found",
|
||||
message,
|
||||
),
|
||||
other => WebError::internal(format!("SQLite 控制面授权撤销失败: {other}")),
|
||||
other => WebError::internal(format!("control-plane授权撤销失败: {other}")),
|
||||
})?;
|
||||
let _ = state.control_plane().append_audit(AppendAuditInput {
|
||||
actor_user_id: Some(context.auth.actor_id.trim().to_string()),
|
||||
@@ -2229,7 +2216,7 @@ fn delete_sqlite_local_access_grant_for_context(
|
||||
);
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"controlPlane": "sqlite",
|
||||
"controlPlane": "control-plane",
|
||||
"policyPath": local_access_policy_path().display().to_string(),
|
||||
"deletedGrantId": grant_id,
|
||||
"revokedUserId": revoked_user_id,
|
||||
@@ -2237,7 +2224,7 @@ fn delete_sqlite_local_access_grant_for_context(
|
||||
"grants": state
|
||||
.control_plane()
|
||||
.list_directory_grants()
|
||||
.map_err(|error| WebError::internal(format!("SQLite 控制面授权列表读取失败: {error}")))?
|
||||
.map_err(|error| WebError::internal(format!("control-plane授权列表读取失败: {error}")))?
|
||||
.iter()
|
||||
.map(control_plane_grant_payload)
|
||||
.collect::<Vec<_>>(),
|
||||
@@ -2420,7 +2407,7 @@ pub async fn get_local_access_policy(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
) -> Result<(StatusCode, Json<Value>), WebError> {
|
||||
let payload = sqlite_access_policy_payload(&state, &context)
|
||||
let payload = control_plane_access_policy_payload(&state, &context)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
Ok((StatusCode::OK, Json(payload)))
|
||||
}
|
||||
@@ -2429,7 +2416,7 @@ pub async fn get_user_access_policy(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
) -> Result<(StatusCode, Json<Value>), WebError> {
|
||||
let payload = sqlite_user_access_policy_payload(&state, &context)
|
||||
let payload = control_plane_user_access_policy_payload(&state, &context)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
Ok((StatusCode::OK, Json(payload)))
|
||||
}
|
||||
@@ -2448,7 +2435,7 @@ pub async fn create_local_access_grant(
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Json(request): Json<LocalAccessGrantRequest>,
|
||||
) -> Result<(StatusCode, Json<Value>), WebError> {
|
||||
let payload = add_sqlite_local_access_grant_for_context(&state, &context, request)
|
||||
let payload = add_control_plane_local_access_grant_for_context(&state, &context, request)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
if let Some(user_id) = payload
|
||||
.pointer("/grant/userId")
|
||||
@@ -2466,7 +2453,7 @@ pub async fn create_user_access_grant(
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Json(request): Json<LocalAccessGrantRequest>,
|
||||
) -> Result<(StatusCode, Json<Value>), WebError> {
|
||||
let payload = add_sqlite_user_access_grant_for_context(&state, &context, request)
|
||||
let payload = add_control_plane_user_access_grant_for_context(&state, &context, request)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
if let Some(user_id) = payload
|
||||
.pointer("/grant/userId")
|
||||
@@ -2484,7 +2471,7 @@ pub async fn delete_local_access_grant(
|
||||
Extension(context): Extension<RequestContext>,
|
||||
AxumPath(grant_id): AxumPath<String>,
|
||||
) -> Result<(StatusCode, Json<Value>), WebError> {
|
||||
let payload = delete_sqlite_local_access_grant_for_context(&state, &context, &grant_id)
|
||||
let payload = delete_control_plane_local_access_grant_for_context(&state, &context, &grant_id)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
if let Some(user_id) = payload
|
||||
.get("revokedUserId")
|
||||
@@ -2502,7 +2489,7 @@ pub async fn delete_user_access_grant(
|
||||
Extension(context): Extension<RequestContext>,
|
||||
AxumPath(grant_id): AxumPath<String>,
|
||||
) -> Result<(StatusCode, Json<Value>), WebError> {
|
||||
let payload = delete_sqlite_user_access_grant_for_context(&state, &context, &grant_id)
|
||||
let payload = delete_control_plane_user_access_grant_for_context(&state, &context, &grant_id)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
if let Some(user_id) = payload
|
||||
.get("revokedUserId")
|
||||
@@ -2536,7 +2523,7 @@ pub async fn get_share_links(
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Query(query): Query<ShareLinkListQuery>,
|
||||
) -> Result<(StatusCode, Json<Value>), WebError> {
|
||||
let payload = list_sqlite_share_links_for_context(&state, &context, query)
|
||||
let payload = list_control_plane_share_links_for_context(&state, &context, query)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
Ok((StatusCode::OK, Json(payload)))
|
||||
}
|
||||
@@ -2546,7 +2533,7 @@ pub async fn create_share_link(
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Json(request): Json<LocalShareLinkRequest>,
|
||||
) -> Result<(StatusCode, Json<Value>), WebError> {
|
||||
let payload = create_sqlite_share_link_for_context(&state, &context, request)
|
||||
let payload = create_control_plane_share_link_for_context(&state, &context, request)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
Ok((StatusCode::OK, Json(payload)))
|
||||
}
|
||||
@@ -2556,7 +2543,7 @@ pub async fn delete_share_link(
|
||||
Extension(context): Extension<RequestContext>,
|
||||
AxumPath(link_id): AxumPath<String>,
|
||||
) -> Result<(StatusCode, Json<Value>), WebError> {
|
||||
let payload = revoke_sqlite_share_link_for_context(&state, &context, &link_id)
|
||||
let payload = revoke_control_plane_share_link_for_context(&state, &context, &link_id)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
Ok((StatusCode::OK, Json(payload)))
|
||||
}
|
||||
@@ -9578,7 +9565,7 @@ fn markdown_page_subtree(document_id: &str, title: &str, content: &Value) -> Val
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
add_sqlite_local_access_grant_for_context,
|
||||
add_control_plane_local_access_grant_for_context,
|
||||
create_default_local_workspace_for_actor_at_base, create_local_access_grant,
|
||||
create_share_grant, create_share_link, create_user_access_grant, create_user_share_grant,
|
||||
delete_share_link, delete_user_access_grant, editor_blocks_to_markdown_for_file,
|
||||
@@ -11054,7 +11041,7 @@ fn main() {}
|
||||
.expect("created grant id")
|
||||
.to_string();
|
||||
assert_eq!(created["grant"]["permission"], "read");
|
||||
assert_eq!(created["controlPlane"], "sqlite");
|
||||
assert_eq!(created["controlPlane"], "control-plane");
|
||||
let created_events = state
|
||||
.control_plane()
|
||||
.drain_outbox(10)
|
||||
@@ -11080,7 +11067,7 @@ fn main() {}
|
||||
.expect_err("created read grant cannot write");
|
||||
assert_eq!(write_error.status(), StatusCode::FORBIDDEN);
|
||||
|
||||
let duplicate_error = add_sqlite_local_access_grant_for_context(
|
||||
let duplicate_error = add_control_plane_local_access_grant_for_context(
|
||||
&state,
|
||||
&request_context("admin_1", "user"),
|
||||
LocalAccessGrantRequest {
|
||||
@@ -11262,7 +11249,7 @@ fn main() {}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn user_access_policy_grant_uses_sqlite_directory_grants_for_owned_folder() {
|
||||
async fn user_access_policy_grant_uses_control_plane_directory_grants_for_owned_folder() {
|
||||
let _guard = env_lock().lock().expect("env lock");
|
||||
let owned_root = temp_root("mnote-user-access-policy-owned-root");
|
||||
let outside_root = temp_root("mnote-user-access-policy-outside-root");
|
||||
@@ -11304,7 +11291,7 @@ fn main() {}
|
||||
.as_str()
|
||||
.expect("grant id")
|
||||
.to_string();
|
||||
assert_eq!(created["controlPlane"], "sqlite");
|
||||
assert_eq!(created["controlPlane"], "control-plane");
|
||||
assert_eq!(created["grant"]["userId"], "user_target");
|
||||
assert_eq!(created["grant"]["createdBy"], "user_owner");
|
||||
assert_eq!(created["grant"]["permission"], "write");
|
||||
@@ -11324,7 +11311,7 @@ fn main() {}
|
||||
)
|
||||
.await
|
||||
.expect("owner can list self-created directory grants");
|
||||
assert_eq!(listed["controlPlane"], "sqlite");
|
||||
assert_eq!(listed["controlPlane"], "control-plane");
|
||||
assert_eq!(listed["grants"][0]["id"], grant_id);
|
||||
assert_eq!(listed["grants"][0]["permission"], "write");
|
||||
|
||||
@@ -11334,7 +11321,7 @@ fn main() {}
|
||||
)
|
||||
.await
|
||||
.expect("target can list incoming directory grants");
|
||||
assert_eq!(target_listed["controlPlane"], "sqlite");
|
||||
assert_eq!(target_listed["controlPlane"], "control-plane");
|
||||
assert_eq!(target_listed["grants"][0]["id"], grant_id);
|
||||
assert_eq!(target_listed["grants"][0]["userId"], "user_target");
|
||||
|
||||
@@ -11452,7 +11439,7 @@ fn main() {}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn share_link_api_creates_lists_and_revokes_sqlite_record() {
|
||||
async fn share_link_api_creates_lists_and_revokes_control_plane_record() {
|
||||
let _guard = env_lock().lock().expect("env lock");
|
||||
let state = test_state();
|
||||
std::env::set_var("MNOTE_ADMIN_USER_IDS", "admin_1");
|
||||
@@ -11488,7 +11475,7 @@ fn main() {}
|
||||
.await
|
||||
.expect("admin can create share link");
|
||||
let link_id = created["link"]["id"].as_str().expect("link id").to_string();
|
||||
assert_eq!(created["controlPlane"], "sqlite");
|
||||
assert_eq!(created["controlPlane"], "control-plane");
|
||||
assert_eq!(created["token"], "visible-token");
|
||||
assert!(created["link"].get("tokenHash").is_none());
|
||||
assert_eq!(created["link"]["permission"], "read");
|
||||
@@ -11507,7 +11494,7 @@ fn main() {}
|
||||
let stored = state
|
||||
.control_plane()
|
||||
.list_share_links(&workspace.id)
|
||||
.expect("list sqlite share links");
|
||||
.expect("list control-plane share links");
|
||||
assert_eq!(stored.len(), 1);
|
||||
assert_eq!(stored[0].id, link_id);
|
||||
assert_ne!(stored[0].token_hash, "visible-token");
|
||||
@@ -12691,30 +12678,30 @@ fn main() {}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_file_open_allows_sqlite_directory_read_grant_without_json_policy() {
|
||||
async fn local_file_open_allows_control_plane_directory_read_grant_without_json_policy() {
|
||||
let _guard = env_lock().lock().expect("env lock");
|
||||
let root = temp_root("mnote-local-file-open-sqlite-read-grant-root");
|
||||
let policy_root = temp_root("mnote-local-file-open-sqlite-read-grant-config");
|
||||
let root = temp_root("mnote-local-file-open-control-plane-read-grant-root");
|
||||
let policy_root = temp_root("mnote-local-file-open-control-plane-read-grant-config");
|
||||
let policy_file = policy_root.join("missing-access-policy.json");
|
||||
let state = test_state();
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
std::fs::write(root.join("README.txt"), "hello sqlite").expect("write file");
|
||||
std::fs::write(root.join("README.txt"), "hello control plane").expect("write file");
|
||||
std::env::set_var("MNOTE_LOCAL_ACCESS_POLICY_FILE", &policy_file);
|
||||
state
|
||||
.control_plane()
|
||||
.upsert_user(UpsertUserInput {
|
||||
id: Some("sqlite_reader".into()),
|
||||
email: Some("sqlite-reader@example.com".into()),
|
||||
username: "sqlite_reader".into(),
|
||||
display_name: "sqlite_reader".into(),
|
||||
id: Some("control_plane_reader".into()),
|
||||
email: Some("control-plane-reader@example.com".into()),
|
||||
username: "control_plane_reader".into(),
|
||||
display_name: "control_plane_reader".into(),
|
||||
role: None,
|
||||
password_hash: None,
|
||||
})
|
||||
.expect("upsert sqlite reader");
|
||||
.expect("upsert control-plane reader");
|
||||
state
|
||||
.control_plane()
|
||||
.grant_directory_access(DirectoryGrantInput {
|
||||
user_id: "sqlite_reader".into(),
|
||||
user_id: "control_plane_reader".into(),
|
||||
workspace_id: None,
|
||||
root_uri: root_uri.clone(),
|
||||
root_path: root
|
||||
@@ -12728,9 +12715,9 @@ fn main() {}
|
||||
source: "test".into(),
|
||||
created_by: None,
|
||||
})
|
||||
.expect("grant sqlite read");
|
||||
.expect("grant control-plane read");
|
||||
|
||||
let context = request_context("sqlite_reader", "user");
|
||||
let context = request_context("control_plane_reader", "user");
|
||||
let (_, _, bytes) = open_local_file(
|
||||
State(state),
|
||||
Extension(context),
|
||||
@@ -12741,8 +12728,8 @@ fn main() {}
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("sqlite read grant can open local file");
|
||||
assert_eq!(bytes, b"hello sqlite");
|
||||
.expect("control-plane read grant can open local file");
|
||||
assert_eq!(bytes, b"hello control plane");
|
||||
assert!(!policy_file.exists(), "SQLite grant 不应写旧 JSON policy");
|
||||
|
||||
std::env::remove_var("MNOTE_LOCAL_ACCESS_POLICY_FILE");
|
||||
@@ -12751,32 +12738,32 @@ fn main() {}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_file_open_allows_legacy_sqlite_directory_grant_with_path_root_uri() {
|
||||
async fn local_file_open_allows_legacy_control_plane_directory_grant_with_path_root_uri() {
|
||||
let _guard = env_lock().lock().expect("env lock");
|
||||
let root = temp_root("mnote-local-file-open-legacy-sqlite-grant-root");
|
||||
let policy_root = temp_root("mnote-local-file-open-legacy-sqlite-grant-config");
|
||||
let root = temp_root("mnote-local-file-open-legacy-control-plane-grant-root");
|
||||
let policy_root = temp_root("mnote-local-file-open-legacy-control-plane-grant-config");
|
||||
let policy_file = policy_root.join("missing-access-policy.json");
|
||||
let state = test_state();
|
||||
let canonical_root = root.canonicalize().expect("canonical root");
|
||||
let canonical_root_path = canonical_root.display().to_string();
|
||||
let root_uri = format!("file://{canonical_root_path}");
|
||||
std::fs::write(root.join("README.txt"), "hello legacy sqlite").expect("write file");
|
||||
std::fs::write(root.join("README.txt"), "hello legacy control plane").expect("write file");
|
||||
std::env::set_var("MNOTE_LOCAL_ACCESS_POLICY_FILE", &policy_file);
|
||||
state
|
||||
.control_plane()
|
||||
.upsert_user(UpsertUserInput {
|
||||
id: Some("legacy_sqlite_reader".into()),
|
||||
email: Some("legacy-sqlite-reader@example.com".into()),
|
||||
username: "legacy_sqlite_reader".into(),
|
||||
display_name: "legacy_sqlite_reader".into(),
|
||||
id: Some("legacy_control_plane_reader".into()),
|
||||
email: Some("legacy-control-plane-reader@example.com".into()),
|
||||
username: "legacy_control_plane_reader".into(),
|
||||
display_name: "legacy_control_plane_reader".into(),
|
||||
role: None,
|
||||
password_hash: None,
|
||||
})
|
||||
.expect("upsert sqlite reader");
|
||||
.expect("upsert control-plane reader");
|
||||
state
|
||||
.control_plane()
|
||||
.grant_directory_access(DirectoryGrantInput {
|
||||
user_id: "legacy_sqlite_reader".into(),
|
||||
user_id: "legacy_control_plane_reader".into(),
|
||||
workspace_id: None,
|
||||
root_uri: canonical_root_path.clone(),
|
||||
root_path: canonical_root_path,
|
||||
@@ -12786,9 +12773,9 @@ fn main() {}
|
||||
source: "legacy-test".into(),
|
||||
created_by: None,
|
||||
})
|
||||
.expect("grant legacy sqlite read");
|
||||
.expect("grant legacy control-plane read");
|
||||
|
||||
let context = request_context("legacy_sqlite_reader", "user");
|
||||
let context = request_context("legacy_control_plane_reader", "user");
|
||||
let (_, _, bytes) = open_local_file(
|
||||
State(state),
|
||||
Extension(context),
|
||||
@@ -12799,8 +12786,8 @@ fn main() {}
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("legacy sqlite read grant can open local file");
|
||||
assert_eq!(bytes, b"hello legacy sqlite");
|
||||
.expect("legacy control-plane read grant can open local file");
|
||||
assert_eq!(bytes, b"hello legacy control plane");
|
||||
assert!(!policy_file.exists(), "SQLite grant 不应写旧 JSON policy");
|
||||
|
||||
std::env::remove_var("MNOTE_LOCAL_ACCESS_POLICY_FILE");
|
||||
@@ -12809,10 +12796,10 @@ fn main() {}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_resource_write_allows_sqlite_directory_write_grant_without_json_policy() {
|
||||
async fn local_resource_write_allows_control_plane_directory_write_grant_without_json_policy() {
|
||||
let _guard = env_lock().lock().expect("env lock");
|
||||
let root = temp_root("mnote-local-resource-write-sqlite-grant-root");
|
||||
let policy_root = temp_root("mnote-local-resource-write-sqlite-grant-config");
|
||||
let root = temp_root("mnote-local-resource-write-control-plane-grant-root");
|
||||
let policy_root = temp_root("mnote-local-resource-write-control-plane-grant-config");
|
||||
let policy_file = policy_root.join("missing-access-policy.json");
|
||||
let state = test_state();
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
@@ -12822,18 +12809,18 @@ fn main() {}
|
||||
state
|
||||
.control_plane()
|
||||
.upsert_user(UpsertUserInput {
|
||||
id: Some("sqlite_writer".into()),
|
||||
email: Some("sqlite-writer@example.com".into()),
|
||||
username: "sqlite_writer".into(),
|
||||
display_name: "sqlite_writer".into(),
|
||||
id: Some("control_plane_writer".into()),
|
||||
email: Some("control-plane-writer@example.com".into()),
|
||||
username: "control_plane_writer".into(),
|
||||
display_name: "control_plane_writer".into(),
|
||||
role: None,
|
||||
password_hash: None,
|
||||
})
|
||||
.expect("upsert sqlite writer");
|
||||
.expect("upsert control-plane writer");
|
||||
state
|
||||
.control_plane()
|
||||
.grant_directory_access(DirectoryGrantInput {
|
||||
user_id: "sqlite_writer".into(),
|
||||
user_id: "control_plane_writer".into(),
|
||||
workspace_id: None,
|
||||
root_uri: root_uri.clone(),
|
||||
root_path: root
|
||||
@@ -12847,9 +12834,9 @@ fn main() {}
|
||||
source: "test".into(),
|
||||
created_by: None,
|
||||
})
|
||||
.expect("grant sqlite write");
|
||||
.expect("grant control-plane write");
|
||||
|
||||
let context = request_context("sqlite_writer", "user");
|
||||
let context = request_context("control_plane_writer", "user");
|
||||
let (_, _, payload) = write_local_resource(
|
||||
State(state),
|
||||
Extension(context),
|
||||
@@ -12868,7 +12855,7 @@ fn main() {}
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("sqlite write grant can write local resource");
|
||||
.expect("control-plane write grant can write local resource");
|
||||
assert_eq!(payload["ok"], true);
|
||||
let written = std::fs::read_to_string(root.join("README.md")).expect("read file");
|
||||
assert!(written.contains("new content"));
|
||||
|
||||
@@ -9,12 +9,15 @@ use control_plane::{ControlPlaneStore, UpsertUserInput, UpsertUserUiPreferenceIn
|
||||
use core_protocol::EvidenceSearchMatchInfo;
|
||||
#[cfg(test)]
|
||||
use core_protocol::{EvidenceLocator, EvidenceSearchResult};
|
||||
#[cfg(test)]
|
||||
use core_protocol::{
|
||||
ParsedResourceArtifact, ResourceSourceMap, SourceMapBlock, PARSED_RESOURCE_ARTIFACT_SCHEMA,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use rusqlite::{params, Connection, OptionalExtension};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
#[cfg(test)]
|
||||
use std::collections::BTreeSet;
|
||||
use std::fs;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
@@ -28,6 +31,7 @@ const LOCAL_INDEX_SETTINGS_SCOPE_KIND: &str = "localIndex";
|
||||
const LOCAL_INDEX_SOURCE_KIND: &str = "local_folder";
|
||||
const DEFAULT_INDEX_SCHEDULE_MODE: &str = "daily";
|
||||
const DEFAULT_INDEX_SCHEDULE_TIME: &str = "02:00";
|
||||
#[cfg(test)]
|
||||
const EVIDENCE_SQLITE_SCHEMA_VERSION: u32 = 1;
|
||||
|
||||
fn local_index_scope_id(root_path: &Path) -> String {
|
||||
@@ -460,7 +464,6 @@ fn local_index_status_for_settings(
|
||||
.join(".mnote")
|
||||
.join("index")
|
||||
.join("search-index.json");
|
||||
let evidence_path = evidence_sqlite_path(root_path);
|
||||
let mut document_count = 0usize;
|
||||
let mut resource_count = 0usize;
|
||||
let mut built_at = Value::Null;
|
||||
@@ -478,21 +481,15 @@ fn local_index_status_for_settings(
|
||||
cache_matches_settings = settings.include_paths.is_empty();
|
||||
local_index_schedule_is_due(&settings, 0)
|
||||
};
|
||||
let evidence_block_count = if evidence_path.exists() {
|
||||
count_evidence_blocks(&evidence_path).unwrap_or(0)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
Ok(json!({
|
||||
"schema": "mnote.local_index.status.v1",
|
||||
"rootUri": root_uri,
|
||||
"workspaceId": workspace_id,
|
||||
"settings": settings,
|
||||
"indexPath": ".mnote/index/search-index.json",
|
||||
"evidenceIndexPath": ".mnote/index/evidence.sqlite",
|
||||
"settingsPath": ".mnote/index/local-index-settings.json",
|
||||
"indexExists": index_path.exists(),
|
||||
"evidenceIndexExists": evidence_path.exists(),
|
||||
"evidenceIndexRetired": true,
|
||||
"cacheMatchesSettings": cache_matches_settings,
|
||||
"scheduledDue": scheduled_due,
|
||||
"builtAt": built_at,
|
||||
@@ -502,7 +499,6 @@ fn local_index_status_for_settings(
|
||||
.unwrap_or_default(),
|
||||
"documentCount": document_count,
|
||||
"resourceCount": resource_count,
|
||||
"evidenceBlockCount": evidence_block_count,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1233,6 +1229,7 @@ pub(crate) fn refresh_local_search_index_for_path_with_settings(
|
||||
index.built_at = now_ms();
|
||||
write_local_search_index_json(root_path, &index)?;
|
||||
let _ = included;
|
||||
#[cfg(test)]
|
||||
remove_evidence_sqlite_index_for_path(root_path, &index, &relative_path)?;
|
||||
return Ok(json!({
|
||||
"version": index.version,
|
||||
@@ -1283,6 +1280,7 @@ pub(crate) fn refresh_local_search_index_for_path_with_settings(
|
||||
.sort_by(|left, right| left.path.cmp(&right.path));
|
||||
index.built_at = now_ms();
|
||||
write_local_search_index_json(root_path, &index)?;
|
||||
#[cfg(test)]
|
||||
if included {
|
||||
refresh_evidence_sqlite_index_for_path(root_path, &index, &relative_path)?;
|
||||
} else {
|
||||
@@ -1518,6 +1516,7 @@ fn load_or_rebuild_local_search_index_with_settings(
|
||||
settings,
|
||||
);
|
||||
}
|
||||
#[cfg(test)]
|
||||
ensure_evidence_sqlite_index(root_path, &index)?;
|
||||
Ok(index)
|
||||
}
|
||||
@@ -2108,7 +2107,11 @@ fn index_resource_file(root_path: &Path, path: &Path) -> Result<LocalSearchResou
|
||||
|
||||
fn write_local_search_index(root_path: &Path, index: &LocalSearchIndex) -> Result<(), WebError> {
|
||||
write_local_search_index_json(root_path, index)?;
|
||||
write_evidence_sqlite_index(root_path, index)
|
||||
#[cfg(test)]
|
||||
{
|
||||
write_evidence_sqlite_index(root_path, index)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_local_search_index_json(
|
||||
@@ -2138,15 +2141,32 @@ fn clear_local_search_index(root_path: &Path) -> Result<(), WebError> {
|
||||
.join(".mnote")
|
||||
.join("index")
|
||||
.join("search-index.json");
|
||||
let evidence_path = evidence_sqlite_path(root_path);
|
||||
for path in [index_path, evidence_path] {
|
||||
match fs::remove_file(&path) {
|
||||
match fs::remove_file(&index_path) {
|
||||
Ok(()) => {}
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(error) => {
|
||||
return Err(WebError::bad_request_code(
|
||||
"local_search_index_delete_failed",
|
||||
format!(
|
||||
"无法删除本地搜索索引文件 {}: {error}",
|
||||
index_path.display()
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
{
|
||||
let evidence_path = evidence_sqlite_path(root_path);
|
||||
match fs::remove_file(&evidence_path) {
|
||||
Ok(()) => {}
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(error) => {
|
||||
return Err(WebError::bad_request_code(
|
||||
"local_search_index_delete_failed",
|
||||
format!("无法删除本地搜索索引文件 {}: {error}", path.display()),
|
||||
format!(
|
||||
"无法删除本地 evidence 测试索引文件 {}: {error}",
|
||||
evidence_path.display()
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -2154,6 +2174,7 @@ fn clear_local_search_index(root_path: &Path) -> Result<(), WebError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn ensure_evidence_sqlite_index(
|
||||
root_path: &Path,
|
||||
index: &LocalSearchIndex,
|
||||
@@ -2165,6 +2186,7 @@ fn ensure_evidence_sqlite_index(
|
||||
write_evidence_sqlite_index(root_path, index)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn write_evidence_sqlite_index(root_path: &Path, index: &LocalSearchIndex) -> Result<(), WebError> {
|
||||
let Some(parent) = evidence_sqlite_path(root_path)
|
||||
.parent()
|
||||
@@ -2220,6 +2242,7 @@ fn write_evidence_sqlite_index(root_path: &Path, index: &LocalSearchIndex) -> Re
|
||||
tx.commit().map_err(sqlite_error)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn create_evidence_schema(connection: &Connection) -> Result<(), WebError> {
|
||||
connection
|
||||
.execute_batch(
|
||||
@@ -2279,6 +2302,7 @@ CREATE TABLE IF NOT EXISTS evidence_edge(
|
||||
.map_err(sqlite_error)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn refresh_evidence_sqlite_index_for_path(
|
||||
root_path: &Path,
|
||||
index: &LocalSearchIndex,
|
||||
@@ -2338,6 +2362,7 @@ fn refresh_evidence_sqlite_index_for_path(
|
||||
tx.commit().map_err(sqlite_error)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn remove_evidence_sqlite_index_for_path(
|
||||
root_path: &Path,
|
||||
index: &LocalSearchIndex,
|
||||
@@ -2365,6 +2390,7 @@ fn remove_evidence_sqlite_index_for_path(
|
||||
tx.commit().map_err(sqlite_error)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn affected_evidence_resource_ids(index: &LocalSearchIndex, relative_path: &str) -> Vec<String> {
|
||||
let mut ids = Vec::new();
|
||||
let document_id = format!("local-md:{}", encode_local_id_segment(relative_path));
|
||||
@@ -2393,6 +2419,7 @@ fn affected_evidence_resource_ids(index: &LocalSearchIndex, relative_path: &str)
|
||||
ids
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn existing_evidence_resource_ids_for_path(
|
||||
connection: &Connection,
|
||||
relative_path: &str,
|
||||
@@ -2413,6 +2440,7 @@ fn existing_evidence_resource_ids_for_path(
|
||||
Ok(resource_ids)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn delete_evidence_resource_projection(
|
||||
connection: &Connection,
|
||||
resource_id: &str,
|
||||
@@ -2466,6 +2494,7 @@ fn delete_evidence_resource_projection(
|
||||
.map_err(sqlite_error)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn insert_document_evidence(
|
||||
connection: &Connection,
|
||||
index: &LocalSearchIndex,
|
||||
@@ -2521,6 +2550,7 @@ fn insert_document_evidence(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn markdown_parsed_artifact(document: &LocalSearchDocument) -> ParsedResourceArtifact {
|
||||
ParsedResourceArtifact {
|
||||
schema: PARSED_RESOURCE_ARTIFACT_SCHEMA.into(),
|
||||
@@ -2536,6 +2566,7 @@ fn markdown_parsed_artifact(document: &LocalSearchDocument) -> ParsedResourceArt
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn insert_evidence_resource_from_artifact(
|
||||
connection: &Connection,
|
||||
resource_id: &str,
|
||||
@@ -2555,6 +2586,7 @@ fn insert_evidence_resource_from_artifact(
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn insert_resource_evidence(
|
||||
connection: &Connection,
|
||||
root_path: &Path,
|
||||
@@ -2606,6 +2638,7 @@ fn insert_resource_evidence(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn parse_resource_evidence_artifact(
|
||||
_root_path: &Path,
|
||||
_index: &LocalSearchIndex,
|
||||
@@ -2614,10 +2647,12 @@ fn parse_resource_evidence_artifact(
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn parsed_resource_id(resource: &LocalSearchResource) -> String {
|
||||
format!("{}#parse", resource.resource_id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn insert_source_map_artifact_evidence(
|
||||
connection: &Connection,
|
||||
index: &LocalSearchIndex,
|
||||
@@ -2682,6 +2717,7 @@ fn insert_source_map_artifact_evidence(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn insert_source_map_evidence_section(
|
||||
connection: &Connection,
|
||||
resource_id: &str,
|
||||
@@ -2709,6 +2745,7 @@ fn insert_source_map_evidence_section(
|
||||
.map_err(sqlite_error)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn source_map_section_path_for_block(
|
||||
source_map: &ResourceSourceMap,
|
||||
block_id: &str,
|
||||
@@ -2721,6 +2758,7 @@ fn source_map_section_path_for_block(
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn source_map_block_locator(
|
||||
index: &LocalSearchIndex,
|
||||
artifact: &ParsedResourceArtifact,
|
||||
@@ -2766,6 +2804,7 @@ fn source_map_block_locator(
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn empty_source_map_block(text: &str) -> SourceMapBlock {
|
||||
SourceMapBlock {
|
||||
id: "artifact".into(),
|
||||
@@ -2776,6 +2815,7 @@ fn empty_source_map_block(text: &str) -> SourceMapBlock {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn insert_evidence_resource(
|
||||
connection: &Connection,
|
||||
resource_id: &str,
|
||||
@@ -2807,6 +2847,7 @@ fn insert_evidence_resource(
|
||||
.map_err(sqlite_error)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn insert_evidence_block(
|
||||
connection: &Connection,
|
||||
block_id: &str,
|
||||
@@ -2825,6 +2866,7 @@ fn insert_evidence_block(
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn insert_evidence_block_with_metadata(
|
||||
connection: &Connection,
|
||||
block_id: &str,
|
||||
@@ -2871,6 +2913,7 @@ fn insert_evidence_block_with_metadata(
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg(test)]
|
||||
struct MarkdownEvidenceBlock {
|
||||
block_id: String,
|
||||
text: String,
|
||||
@@ -2879,6 +2922,7 @@ struct MarkdownEvidenceBlock {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg(test)]
|
||||
struct MarkdownEvidenceSection {
|
||||
section_id: String,
|
||||
title: String,
|
||||
@@ -2887,6 +2931,7 @@ struct MarkdownEvidenceSection {
|
||||
parent_section_id: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn markdown_evidence_blocks(document: &LocalSearchDocument) -> Vec<MarkdownEvidenceBlock> {
|
||||
let mut headings: Vec<String> = Vec::new();
|
||||
let mut blocks = Vec::new();
|
||||
@@ -2910,6 +2955,7 @@ fn markdown_evidence_blocks(document: &LocalSearchDocument) -> Vec<MarkdownEvide
|
||||
blocks
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn markdown_evidence_sections(document: &LocalSearchDocument) -> Vec<MarkdownEvidenceSection> {
|
||||
let mut headings: Vec<String> = Vec::new();
|
||||
let mut sections = Vec::new();
|
||||
@@ -2945,6 +2991,7 @@ fn markdown_evidence_sections(document: &LocalSearchDocument) -> Vec<MarkdownEvi
|
||||
sections
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn markdown_heading(line: &str) -> Option<(usize, String)> {
|
||||
let trimmed = line.trim_start();
|
||||
let marker_count = trimmed.chars().take_while(|value| *value == '#').count();
|
||||
@@ -2958,6 +3005,7 @@ fn markdown_heading(line: &str) -> Option<(usize, String)> {
|
||||
Some((marker_count, rest.trim_matches('#').trim().to_string()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn insert_markdown_evidence_section(
|
||||
connection: &Connection,
|
||||
resource_id: &str,
|
||||
@@ -2985,6 +3033,7 @@ fn insert_markdown_evidence_section(
|
||||
.map_err(sqlite_error)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn insert_document_graph_edges(
|
||||
connection: &Connection,
|
||||
document: &LocalSearchDocument,
|
||||
@@ -3029,6 +3078,7 @@ fn insert_document_graph_edges(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn first_markdown_evidence_block_id(document: &LocalSearchDocument) -> String {
|
||||
document
|
||||
.raw_text
|
||||
@@ -3039,6 +3089,7 @@ fn first_markdown_evidence_block_id(document: &LocalSearchDocument) -> String {
|
||||
.unwrap_or_else(|| format!("{}#line1", document.document_id))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn insert_evidence_edge(
|
||||
connection: &Connection,
|
||||
from_id: &str,
|
||||
@@ -3073,6 +3124,7 @@ fn insert_evidence_edge(
|
||||
.map_err(sqlite_error)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn markdown_reference_target_id(owner_path: &str, target: &str) -> String {
|
||||
let target = target.trim();
|
||||
if target.starts_with("local-md:") {
|
||||
@@ -3087,6 +3139,7 @@ fn markdown_reference_target_id(owner_path: &str, target: &str) -> String {
|
||||
format!("markdown-title:{}", encode_local_id_segment(target))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn resource_reference_target_id(owner_path: &str, target: &str) -> String {
|
||||
format!(
|
||||
"local-resource:{}",
|
||||
@@ -3095,11 +3148,13 @@ fn resource_reference_target_id(owner_path: &str, target: &str) -> String {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg(test)]
|
||||
struct MarkdownMentionEdge {
|
||||
target_id: String,
|
||||
source_block_id: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn markdown_mention_edges(document: &LocalSearchDocument) -> Vec<MarkdownMentionEdge> {
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
let mut mentions = Vec::new();
|
||||
@@ -3118,6 +3173,7 @@ fn markdown_mention_edges(document: &LocalSearchDocument) -> Vec<MarkdownMention
|
||||
mentions
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn extract_line_mentions(line: &str) -> Vec<String> {
|
||||
let chars = line.chars().collect::<Vec<_>>();
|
||||
let mut mentions = Vec::new();
|
||||
@@ -3151,6 +3207,7 @@ fn extract_line_mentions(line: &str) -> Vec<String> {
|
||||
mentions
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn markdown_locator(
|
||||
index: &LocalSearchIndex,
|
||||
document_id: &str,
|
||||
@@ -3177,6 +3234,7 @@ fn markdown_locator(
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn evidence_sqlite_path(root_path: &Path) -> PathBuf {
|
||||
root_path
|
||||
.join(".mnote")
|
||||
@@ -3184,6 +3242,8 @@ fn evidence_sqlite_path(root_path: &Path) -> PathBuf {
|
||||
.join("evidence.sqlite")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(dead_code)]
|
||||
fn count_evidence_blocks(path: &Path) -> Result<u64, WebError> {
|
||||
let connection = Connection::open(path).map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
@@ -3198,6 +3258,7 @@ fn count_evidence_blocks(path: &Path) -> Result<u64, WebError> {
|
||||
.map_err(sqlite_error)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub(crate) struct LocalEvidenceSourceStatuses {
|
||||
@@ -3205,6 +3266,7 @@ pub(crate) struct LocalEvidenceSourceStatuses {
|
||||
pub(crate) failed_paths: BTreeSet<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn local_evidence_source_statuses(
|
||||
root_path: &Path,
|
||||
@@ -3251,6 +3313,7 @@ pub(crate) fn local_evidence_source_statuses(
|
||||
Ok(statuses)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn evidence_resource_kind_for_type(resource_type: &str) -> &'static str {
|
||||
match resource_type {
|
||||
"mindmap" => "mindmap",
|
||||
@@ -3262,6 +3325,7 @@ fn evidence_resource_kind_for_type(resource_type: &str) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn evidence_resource_kind_for_path(path: &str) -> &'static str {
|
||||
match Path::new(path)
|
||||
.extension()
|
||||
@@ -3315,6 +3379,7 @@ fn is_local_evidence_sidecar_relative_path(relative_path: &str) -> bool {
|
||||
&& (file_name.ends_with(".parse.md") || file_name.ends_with(".source-map.json"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn normalize_local_reference_path(owner_path: &str, target: &str) -> String {
|
||||
let target = target
|
||||
.split('#')
|
||||
@@ -3333,6 +3398,7 @@ fn normalize_local_reference_path(owner_path: &str, target: &str) -> String {
|
||||
normalize_path_components(owner_parent.join(target))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn normalize_path_components(path: PathBuf) -> String {
|
||||
let mut parts = Vec::new();
|
||||
for component in path.components() {
|
||||
@@ -3348,6 +3414,7 @@ fn normalize_path_components(path: PathBuf) -> String {
|
||||
parts.join("/")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn sqlite_error(error: rusqlite::Error) -> WebError {
|
||||
WebError::bad_request_code(
|
||||
"evidence_index_sqlite_error",
|
||||
|
||||
@@ -86,6 +86,9 @@ pub fn build_router(state: AppState) -> Router {
|
||||
)
|
||||
.route("/auth", get(gateway::auth_entry).post(gateway::auth_entry))
|
||||
.route("/search", get(search::shell))
|
||||
.route("/knowledge", get(gateway::root_entry))
|
||||
.route("/knowledge/{*path}", get(gateway::root_entry))
|
||||
.route("/debug/knowledge-rag", get(gateway::root_entry))
|
||||
.route("/api/evidence/search", post(evidence::search))
|
||||
.route("/api/evidence/read", post(evidence::read))
|
||||
.route("/api/evidence/open", post(evidence::open))
|
||||
|
||||
@@ -9,6 +9,7 @@ use axum::response::Response;
|
||||
use axum::{Extension, Json};
|
||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
|
||||
use control_plane::session_token_hash;
|
||||
use futures_util::TryStreamExt;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
@@ -364,6 +365,29 @@ fn mnote_user_id(state: &AppState, context: &RequestContext) -> String {
|
||||
"anonymous".to_string()
|
||||
}
|
||||
|
||||
fn build_allowed_roots(state: &AppState, user_id: &str) -> Vec<Value> {
|
||||
state
|
||||
.control_plane()
|
||||
.list_directory_grants_for_actor(user_id)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter(|grant| grant.status.trim() == "active")
|
||||
.filter(|grant| !grant.root_uri.trim().is_empty())
|
||||
.map(|grant| {
|
||||
json!({
|
||||
"grantId": grant.id,
|
||||
"workspaceId": grant.workspace_id,
|
||||
"rootUri": grant.root_uri,
|
||||
"rootPath": grant.root_path,
|
||||
"permission": grant.permission,
|
||||
"recursive": grant.recursive,
|
||||
"capabilities": serde_json::from_str::<Vec<String>>(&grant.capabilities_json).unwrap_or_default(),
|
||||
"source": grant.source,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn text_or_default(value: Option<&str>, fallback: &str) -> String {
|
||||
value
|
||||
.map(str::trim)
|
||||
@@ -378,6 +402,56 @@ fn file_path_from_root_uri(root_uri: &str) -> Option<PathBuf> {
|
||||
(!file_path.trim().is_empty()).then(|| PathBuf::from(file_path))
|
||||
}
|
||||
|
||||
fn default_openhub_workspace_root_uri(user_id: &str) -> String {
|
||||
format!("file:///mnt/Data1T/Mnote_data/users/{user_id}/workspaces/my-space")
|
||||
}
|
||||
|
||||
fn preferred_openhub_root_uri(root: &Value) -> Option<String> {
|
||||
root.get("rootUri")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| value.starts_with("file://"))
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| {
|
||||
root.get("rootPath")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(|value| format!("file://{value}"))
|
||||
})
|
||||
.or_else(|| {
|
||||
root.get("rootUri")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
})
|
||||
}
|
||||
|
||||
fn default_allowed_roots_for_scope(default_allowed_roots: &[Value], root_uri: &str) -> Value {
|
||||
let root_uri = root_uri.trim();
|
||||
let matched_root = default_allowed_roots
|
||||
.iter()
|
||||
.find(|root| {
|
||||
preferred_openhub_root_uri(root)
|
||||
.as_deref()
|
||||
.is_some_and(|candidate| candidate == root_uri)
|
||||
})
|
||||
.cloned();
|
||||
|
||||
if let Some(root) = matched_root {
|
||||
return json!([root]);
|
||||
}
|
||||
|
||||
json!([{
|
||||
"rootUri": root_uri,
|
||||
"permission": "write",
|
||||
"recursive": true,
|
||||
"capabilities": ["ai", "read", "write"],
|
||||
"source": "mnote_openhub_default_root",
|
||||
}])
|
||||
}
|
||||
|
||||
fn artifact_index_store_path(root_uri: Option<&str>) -> PathBuf {
|
||||
if let Ok(path) = std::env::var("MNOTE_OPENHUB_ARTIFACT_INDEX_PATH") {
|
||||
let path = path.trim();
|
||||
@@ -554,6 +628,12 @@ fn build_scope(
|
||||
request: &OpenHubBootstrapRequest,
|
||||
) -> Value {
|
||||
let mnote_user_id = mnote_user_id(state, context);
|
||||
let mnote_display_name = crate::routes::gateway::current_actor_display_name(state, context);
|
||||
let default_allowed_roots = build_allowed_roots(state, &mnote_user_id);
|
||||
let default_root_uri = default_allowed_roots
|
||||
.iter()
|
||||
.find_map(preferred_openhub_root_uri)
|
||||
.unwrap_or_else(|| default_openhub_workspace_root_uri(&mnote_user_id));
|
||||
let workspace_id = text_or_default(
|
||||
request
|
||||
.workspace_id
|
||||
@@ -561,7 +641,7 @@ fn build_scope(
|
||||
.or(context.workspace.workspace_id.as_deref()),
|
||||
"default-workspace",
|
||||
);
|
||||
let root_uri = text_or_default(request.root_uri.as_deref(), "");
|
||||
let root_uri = text_or_default(request.root_uri.as_deref(), &default_root_uri);
|
||||
let page_resource_id = text_or_default(
|
||||
request
|
||||
.page_id
|
||||
@@ -583,13 +663,14 @@ fn build_scope(
|
||||
"pageAbsolutePath": request.page_absolute_path.as_deref(),
|
||||
"pageTitle": request.page_title.as_deref(),
|
||||
});
|
||||
let allowed_roots = request
|
||||
.allowed_roots
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| default_allowed_roots_for_scope(&default_allowed_roots, &root_uri));
|
||||
let weknora_seed = format!(
|
||||
"{openhub_user_key}\n{openhub_workspace_key}\n{}",
|
||||
request
|
||||
.allowed_roots
|
||||
.as_ref()
|
||||
.map(Value::to_string)
|
||||
.unwrap_or_else(|| "[]".to_string())
|
||||
allowed_roots.to_string()
|
||||
);
|
||||
let weknora_tool_scope = stable_hash("weknora_tool", &weknora_seed);
|
||||
json!({
|
||||
@@ -597,6 +678,8 @@ fn build_scope(
|
||||
"authTruth": "mnote_session",
|
||||
"rejectsFrontendAuthTruth": ["openhub_jwt", "openhub_localStorage", "openhub_login_page"],
|
||||
"mnoteUserId": mnote_user_id,
|
||||
"mnoteActorId": context.auth.actor_id,
|
||||
"mnoteDisplayName": mnote_display_name,
|
||||
"openhubUserKey": openhub_user_key,
|
||||
"openhub_user_key": openhub_user_key,
|
||||
"openhubWorkspaceKey": openhub_workspace_key,
|
||||
@@ -612,7 +695,7 @@ fn build_scope(
|
||||
"weknoraToolScope": weknora_tool_scope,
|
||||
"weknora_tool_scope": weknora_tool_scope,
|
||||
"workspaceScope": root_scope,
|
||||
"allowedRoots": request.allowed_roots.as_ref().cloned().unwrap_or_else(|| json!([])),
|
||||
"allowedRoots": allowed_roots,
|
||||
"selectionPresent": request.selection.as_deref().map(str::trim).is_some_and(|value| !value.is_empty()),
|
||||
})
|
||||
}
|
||||
@@ -683,6 +766,12 @@ fn add_mnote_scope_headers(
|
||||
if let Some(value) = header_string(scope.get("openhub_user_key")) {
|
||||
request = request.header(HeaderName::from_static("x-mnote-user-key"), value);
|
||||
}
|
||||
if let Some(value) = header_string(scope.get("mnoteUserId")) {
|
||||
request = request.header(HeaderName::from_static("x-mnote-user-id"), value);
|
||||
}
|
||||
if let Some(value) = header_string(scope.get("mnoteDisplayName")) {
|
||||
request = request.header(HeaderName::from_static("x-mnote-display-name"), value);
|
||||
}
|
||||
if let Some(value) = header_string(scope.get("workspace_key")) {
|
||||
request = request.header(HeaderName::from_static("x-mnote-workspace-key"), value);
|
||||
}
|
||||
@@ -754,7 +843,7 @@ pub async fn status(
|
||||
let openhub_fastapi = probe_service(
|
||||
"openhub_fastapi",
|
||||
openhub_base.clone(),
|
||||
"/global/health",
|
||||
"/api/health",
|
||||
openhub_configured,
|
||||
)
|
||||
.await;
|
||||
@@ -1095,6 +1184,22 @@ fn response_with_status(status: StatusCode, body: String) -> Response {
|
||||
.unwrap_or_else(|_| Response::new(Body::from("openhub proxy response build failed")))
|
||||
}
|
||||
|
||||
fn is_streaming_response(headers: &HeaderMap) -> bool {
|
||||
let content_type = headers
|
||||
.get(header::CONTENT_TYPE)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or("")
|
||||
.to_ascii_lowercase();
|
||||
content_type.contains("text/event-stream")
|
||||
|| content_type.contains("application/x-ndjson")
|
||||
|| headers
|
||||
.get(header::CACHE_CONTROL)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or("")
|
||||
.to_ascii_lowercase()
|
||||
.contains("no-cache")
|
||||
}
|
||||
|
||||
pub async fn ai_proxy(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
@@ -1116,7 +1221,7 @@ pub async fn ai_proxy(
|
||||
let upstream_method =
|
||||
reqwest::Method::from_bytes(method.as_str().as_bytes()).unwrap_or(reqwest::Method::GET);
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(30))
|
||||
.connect_timeout(Duration::from_secs(10))
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.build()
|
||||
.map_err(|error| {
|
||||
@@ -1162,6 +1267,28 @@ pub async fn ai_proxy(
|
||||
})?;
|
||||
let status = upstream.status();
|
||||
let headers = upstream.headers().clone();
|
||||
if is_streaming_response(&headers) {
|
||||
let mut builder = Response::builder().status(status);
|
||||
let response_headers = strip_hop_headers(&headers);
|
||||
for (name, value) in response_headers.iter() {
|
||||
builder = builder.header(name, value);
|
||||
}
|
||||
builder = builder
|
||||
.header(header::CACHE_CONTROL, "no-cache, no-transform")
|
||||
.header("x-accel-buffering", "no");
|
||||
let stream = upstream.bytes_stream().map_err(|error| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("OpenHub AI proxy 流式响应读取失败:{error}"),
|
||||
)
|
||||
});
|
||||
return Ok(builder.body(Body::from_stream(stream)).unwrap_or_else(|_| {
|
||||
response_with_status(
|
||||
StatusCode::BAD_GATEWAY,
|
||||
"OpenHub AI proxy 流式响应构造失败".to_string(),
|
||||
)
|
||||
}));
|
||||
}
|
||||
let content_type = headers
|
||||
.get(header::CONTENT_TYPE)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
|
||||
@@ -82,7 +82,7 @@ fn build_session_response(state: &AppState, context: RequestContext) -> SessionR
|
||||
email: resolved.user.email.unwrap_or_default(),
|
||||
name: resolved.user.display_name,
|
||||
actor_type: effective_actor_type_for_user(&resolved.user.id, &resolved.user.role),
|
||||
auth_mode: "sqliteSession",
|
||||
auth_mode: "controlPlaneSession",
|
||||
request_id: context.trace.request_id,
|
||||
trace_id: context.trace.trace_id,
|
||||
};
|
||||
@@ -321,7 +321,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_prefers_sqlite_cookie_identity_over_dev_fallback() {
|
||||
async fn session_prefers_control_plane_cookie_identity_over_dev_fallback() {
|
||||
let state = AppState::new(AppConfig {
|
||||
service_name: "mnote-web".into(),
|
||||
service_version: "0.1.0".into(),
|
||||
@@ -386,11 +386,11 @@ mod tests {
|
||||
assert_eq!(payload["email"], "shujuan@163.com");
|
||||
assert_eq!(payload["name"], "shujuan");
|
||||
assert_eq!(payload["actorType"], "user");
|
||||
assert_eq!(payload["authMode"], "sqliteSession");
|
||||
assert_eq!(payload["authMode"], "controlPlaneSession");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_returns_admin_for_sqlite_admin_user() {
|
||||
async fn session_returns_admin_for_control_plane_admin_user() {
|
||||
let state = AppState::new(AppConfig {
|
||||
service_name: "mnote-web".into(),
|
||||
service_version: "0.1.0".into(),
|
||||
@@ -452,7 +452,7 @@ mod tests {
|
||||
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["userId"], "liaibo");
|
||||
assert_eq!(payload["actorType"], "admin");
|
||||
assert_eq!(payload["authMode"], "sqliteSession");
|
||||
assert_eq!(payload["authMode"], "controlPlaneSession");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -535,7 +535,7 @@ mod tests {
|
||||
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["userId"], "liaibo");
|
||||
assert_eq!(payload["actorType"], "admin");
|
||||
assert_eq!(payload["authMode"], "sqliteSession");
|
||||
assert_eq!(payload["authMode"], "controlPlaneSession");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -79,7 +79,7 @@ pub(crate) async fn get_tree_view_state(
|
||||
Some(&scope.source_kind),
|
||||
)
|
||||
.map_err(|error| {
|
||||
WebError::internal(format!("SQLite tree view state 读取失败: {error}"))
|
||||
WebError::internal(format!("control-plane tree view state 读取失败: {error}"))
|
||||
.with_context(&context)
|
||||
})?;
|
||||
let Some(record) = find_tree_view_state_record(&preferences, &scope) else {
|
||||
@@ -92,14 +92,14 @@ pub(crate) async fn get_tree_view_state(
|
||||
)));
|
||||
};
|
||||
let parsed = serde_json::from_str::<Value>(&record.value_json).map_err(|error| {
|
||||
WebError::internal(format!("SQLite tree view state JSON 无效: {error}"))
|
||||
WebError::internal(format!("control-plane tree view state JSON 无效: {error}"))
|
||||
.with_context(&context)
|
||||
})?;
|
||||
let normalized = normalize_state(&context, &scope, parsed)?;
|
||||
Ok(Json(response_payload(
|
||||
&actor_id,
|
||||
&scope,
|
||||
"sqlite",
|
||||
"control-plane",
|
||||
Some(record.revision),
|
||||
normalized,
|
||||
)))
|
||||
@@ -143,7 +143,7 @@ pub(crate) async fn put_tree_view_state(
|
||||
Ok(Json(response_payload(
|
||||
&actor_id,
|
||||
&scope,
|
||||
"sqlite",
|
||||
"control-plane",
|
||||
Some(record.revision),
|
||||
normalized,
|
||||
)))
|
||||
|
||||
@@ -3287,7 +3287,7 @@ fn annotate_local_attachment_refs_authorization(
|
||||
.control_plane()
|
||||
.resolve_access(&context.auth.actor_id, resolved_uri)
|
||||
.map_err(|error| {
|
||||
WebError::internal(format!("SQLite 控制面附件授权解析失败: {error}"))
|
||||
WebError::internal(format!("control-plane 附件授权解析失败: {error}"))
|
||||
})?;
|
||||
object.insert(
|
||||
"authorized".to_string(),
|
||||
@@ -3816,7 +3816,7 @@ mod tests {
|
||||
role: None,
|
||||
password_hash: None,
|
||||
})
|
||||
.expect("upsert sqlite reader");
|
||||
.expect("upsert control-plane reader");
|
||||
state
|
||||
.control_plane()
|
||||
.grant_directory_access(DirectoryGrantInput {
|
||||
@@ -3834,7 +3834,7 @@ mod tests {
|
||||
source: "unit-test".into(),
|
||||
created_by: None,
|
||||
})
|
||||
.expect("grant sqlite read");
|
||||
.expect("grant control-plane read");
|
||||
}
|
||||
|
||||
fn app_with_unreachable_convex_without_fixture() -> axum::Router {
|
||||
@@ -4706,7 +4706,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn page_aggregate_endpoint_allows_sqlite_granted_local_folder_read_access() {
|
||||
async fn page_aggregate_endpoint_allows_control_plane_granted_local_folder_read_access() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-local-page-aggregate-grant-{}",
|
||||
std::process::id()
|
||||
@@ -4753,7 +4753,7 @@ mod tests {
|
||||
Some(&root_uri),
|
||||
)
|
||||
.await
|
||||
.expect("sqlite read grant can open local page aggregate");
|
||||
.expect("control-plane read grant can open local page aggregate");
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
|
||||
@@ -4766,7 +4766,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_page_aggregate_marks_attachment_refs_authorization_from_sqlite_grants() {
|
||||
async fn local_page_aggregate_marks_attachment_refs_authorization_from_control_plane_grants() {
|
||||
let root = temp_root("mnote-local-page-aggregate-attachment-auth-root");
|
||||
let allowed_external_root = temp_root("mnote-local-page-aggregate-attachment-auth-allowed");
|
||||
let denied_external_root = temp_root("mnote-local-page-aggregate-attachment-auth-denied");
|
||||
@@ -4935,7 +4935,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_folder_page_aggregate_prefers_sqlite_ui_preference_over_default_title_header() {
|
||||
async fn local_folder_page_aggregate_prefers_control_plane_ui_preference_over_default_title_header() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-local-page-aggregate-ui-pref-{}",
|
||||
std::process::id()
|
||||
@@ -5118,7 +5118,7 @@ mod tests {
|
||||
.await
|
||||
.expect("body");
|
||||
let get_payload: Value = serde_json::from_slice(&get_body).expect("json");
|
||||
assert_eq!(get_payload["result"]["source"], "sqlite");
|
||||
assert_eq!(get_payload["result"]["source"], "control-plane");
|
||||
assert_eq!(
|
||||
get_payload["result"]["state"]["expandedRelativePaths"][0],
|
||||
"design/05-editor-mainline"
|
||||
|
||||
@@ -20,7 +20,7 @@ pub fn AuthPage() -> impl IntoView {
|
||||
<h1 id="mnote-auth-title">"账号登录"</h1>
|
||||
<p>"登录后进入你的工作区"</p>
|
||||
</div>
|
||||
<form class="mnote-auth-form" method="post" action="/api/auth" data-auth-mode="sqlite-session">
|
||||
<form class="mnote-auth-form" method="post" action="/api/auth" data-auth-mode="control-plane-session">
|
||||
<input type="hidden" name="action" value="auth:signIn" />
|
||||
<input type="hidden" name="provider" value="password" />
|
||||
<input type="hidden" name="flow" value="signIn" data-auth-flow />
|
||||
@@ -95,7 +95,7 @@ const AUTH_SCRIPT: &str = r#"
|
||||
(function () {
|
||||
var root = document.querySelector('[data-testid="mnote-auth-page"]');
|
||||
if (!root) return;
|
||||
var form = root.querySelector('form[data-auth-mode="sqlite-session"]');
|
||||
var form = root.querySelector('form[data-auth-mode="control-plane-session"]');
|
||||
var flowInput = root.querySelector('[data-auth-flow]');
|
||||
var accountInput = root.querySelector('#account');
|
||||
var emailInput = root.querySelector('#email');
|
||||
|
||||
@@ -192,7 +192,6 @@ pub fn PageLayout(
|
||||
<span class="wolai-public-pill" data-testid="wolai-public-state" hidden></span>
|
||||
<button type="button" class="wolai-icon-button" title="星标置顶" aria-label="星标置顶" data-state="closed" aria-haspopup="true" aria-expanded="false" data-mnote-action="toggle-sidebar-shortcut" data-mnote-shortcut-kind="page"><span class="material-symbols-outlined" data-icon="star" aria-hidden="true"></span></button>
|
||||
<button type="button" class="wolai-icon-button" title="演示" aria-label="演示"><span class="material-symbols-outlined" data-icon="slideshow" aria-hidden="true"></span></button>
|
||||
<button type="button" class="wolai-icon-button" title="资料库问答" aria-label="资料库问答" data-testid="mnote-knowledge-rag-settings-toggle" data-mnote-action="open-knowledge-rag-settings"><span class="material-symbols-outlined" data-icon="travel_explore" aria-hidden="true"></span></button>
|
||||
<button type="button" class="wolai-icon-button" title="评论" aria-label="评论"><span class="material-symbols-outlined" data-icon="comment" aria-hidden="true"></span></button>
|
||||
<button type="button" class="wolai-icon-button" title="分享" aria-label="分享"><span class="material-symbols-outlined" data-icon="share" aria-hidden="true"></span></button>
|
||||
<button type="button" class="wolai-icon-button" title="成员" aria-label="成员"><span class="material-symbols-outlined" data-icon="person_add" aria-hidden="true"></span></button>
|
||||
@@ -204,6 +203,7 @@ pub fn PageLayout(
|
||||
{children()}
|
||||
</article>
|
||||
<div class="wolai-floating-actions" aria-label="浮动操作">
|
||||
<a href="/knowledge" data-testid="mnote-floating-knowledge" class="wolai-floating-button wolai-floating-button--knowledge" aria-label="知识库" title="打开知识库" data-mnote-action="open-knowledge-host"><span class="material-symbols-outlined material-symbols-filled" data-icon="travel_explore" aria-hidden="true"></span></a>
|
||||
<button type="button" data-testid="wolai-floating-ai" class="wolai-floating-button wolai-floating-button--ai" aria-label="AI 助手" data-state="closed" aria-haspopup="dialog" aria-expanded="false" title="点击 进入空间智能问答" data-mnote-action="open-page-ai"><span class="material-symbols-outlined material-symbols-filled" data-icon="auto_awesome" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -547,8 +547,15 @@ mod tests {
|
||||
|
||||
assert!(!html.contains(r#"data-testid="mnote-local-index-settings-toggle""#));
|
||||
assert!(!html.contains(r#"data-mnote-action="open-index-settings""#));
|
||||
assert!(html.contains(r#"data-testid="mnote-knowledge-rag-settings-toggle""#));
|
||||
assert!(html.contains(r#"data-mnote-action="open-knowledge-rag-settings""#));
|
||||
assert!(!html.contains(r#"data-testid="mnote-knowledge-rag-settings-toggle""#));
|
||||
assert!(html.contains(r#"data-testid="mnote-floating-knowledge""#));
|
||||
assert!(html.contains(r#"href="/knowledge""#));
|
||||
assert!(html.contains(r#"data-mnote-action="open-knowledge-host""#));
|
||||
assert!(
|
||||
html.find(r#"data-testid="mnote-floating-knowledge""#)
|
||||
< html.find(r#"data-testid="wolai-floating-ai""#)
|
||||
);
|
||||
assert!(!html.contains(r#"data-mnote-action="open-knowledge-rag-settings""#));
|
||||
assert!(html.contains(r#"data-icon="travel_explore""#));
|
||||
assert!(crate::ssr::styles::MNOTE_CSS.contains(r#"data-icon="travel_explore""#));
|
||||
assert!(!html.contains(r#"data-testid="mnote-local-ocr-task-toggle""#));
|
||||
@@ -595,9 +602,9 @@ 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("[data-mnote-action=\"open-knowledge-rag-settings\"]")
|
||||
);
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("window.location.href = '/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]"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("useKnowledgeRagFileTreeSelection"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("reindex-source"));
|
||||
|
||||
@@ -2340,9 +2340,11 @@
|
||||
}
|
||||
|
||||
.wolai-floating-actions {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
right: 24px;
|
||||
bottom: 44px;
|
||||
flex-direction: column-reverse;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
z-index: var(--wolai-z-dock);
|
||||
}
|
||||
@@ -2366,7 +2368,8 @@
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.wolai-floating-button--ai {
|
||||
.wolai-floating-button--ai,
|
||||
.wolai-floating-button--knowledge {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 1px solid rgba(27, 28, 28, 0.1);
|
||||
@@ -2376,6 +2379,10 @@
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.wolai-floating-button--knowledge {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.document-shell[data-page-wide-layout="true"] {
|
||||
width: min(100%, 980px);
|
||||
}
|
||||
@@ -2614,7 +2621,7 @@
|
||||
.mnote-weknora-kb-page-body {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(260px, 320px) minmax(0, 1fr);
|
||||
min-height: 620px;
|
||||
min-height: min(620px, calc(100vh - 190px));
|
||||
max-height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
@@ -2869,6 +2876,39 @@
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-detail-tabs {
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
gap: 3px;
|
||||
padding: 3px;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-detail-tab {
|
||||
min-height: 28px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #4B5563;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-detail-tab.is-active {
|
||||
background: #EAF7EA;
|
||||
color: #166534;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-detail-panel[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mnote-weknora-doc-toolbar,
|
||||
.mnote-weknora-doc-actions {
|
||||
display: flex;
|
||||
@@ -3063,21 +3103,52 @@
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-meta div {
|
||||
display: grid;
|
||||
grid-template-columns: 72px minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
align-items: baseline;
|
||||
color: #8B8782;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
background: #FFFFFF;
|
||||
color: #6B7280;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-meta .mnote-weknora-kb-summary-card {
|
||||
grid-column: 1 / -1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-meta .mnote-weknora-kb-summary-card div {
|
||||
border: 0;
|
||||
border-right: 1px solid #E5E7EB;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-meta .mnote-weknora-kb-summary-card div:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-meta strong {
|
||||
color: #111827;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-meta code {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
@@ -3140,6 +3211,15 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-source-row {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid #EEF0F2;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-source-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-source-input-row {
|
||||
grid-template-columns: minmax(0, 1fr) 56px 28px;
|
||||
}
|
||||
@@ -3249,6 +3329,7 @@
|
||||
|
||||
.mnote-knowledge-rag-source-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
@@ -3274,6 +3355,23 @@
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.mnote-weknora-kb-page-body,
|
||||
.mnote-weknora-kb-detail-hero,
|
||||
.mnote-weknora-doc-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-list-pane {
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid #E5E7EB;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-meta .mnote-weknora-kb-summary-card {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.wolai-page-settings-tabs {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
|
||||
Reference in New Issue
Block a user