feat(rag): harden post-LightRAG runtime
Retire legacy OCR/media/evidence fallbacks, add local-folder event bus and Page Aggregate guards, and archive completed design checklists. Validation: cargo test -p mnote-web -- --test-threads=1; cargo test --workspace -- --test-threads=1; git diff --check; codegraph sync .; codegraph_status.
This commit is contained in:
@@ -143,9 +143,9 @@ pub fn build_query_request<T>(
|
||||
context: &BridgeContext,
|
||||
query: &QueryEnvelope<T>,
|
||||
) -> BridgeResult<RetiredQueryRequest> {
|
||||
let function_name = legacy_query_function_name(&query.name)?;
|
||||
legacy_query_function_name(&query.name)?;
|
||||
Ok(RetiredQueryRequest {
|
||||
function_name: function_name.to_string(),
|
||||
function_name: query.name.clone(),
|
||||
deployment_id: context.deployment_id.clone(),
|
||||
project_id: context.project_id.clone(),
|
||||
workspace_id: context.workspace_id.clone(),
|
||||
@@ -160,9 +160,9 @@ pub fn build_write_request<T>(
|
||||
context: &BridgeContext,
|
||||
command: &CommandEnvelope<T>,
|
||||
) -> BridgeResult<RetiredMutationRequest> {
|
||||
let function_name = legacy_command_function_name(&command.name)?;
|
||||
legacy_command_function_name(&command.name)?;
|
||||
Ok(RetiredMutationRequest {
|
||||
function_name: function_name.to_string(),
|
||||
function_name: command.name.clone(),
|
||||
deployment_id: context.deployment_id.clone(),
|
||||
project_id: context.project_id.clone(),
|
||||
workspace_id: context.workspace_id.clone(),
|
||||
@@ -182,26 +182,55 @@ fn legacy_query_function_name(name: &str) -> BridgeResult<&'static str> {
|
||||
match name {
|
||||
"documents.content.get" => Ok("documents:getContent"),
|
||||
"documents.meta.get" => Ok("documents:getMeta"),
|
||||
"blocks.get" => Ok("blocks:getById"),
|
||||
"mindmaps.get" => Ok("mindmaps:get"),
|
||||
"search.documents" => Ok("documents:listSearchDataByWorkspace"),
|
||||
"search.documents" => Ok("search:documents"),
|
||||
"search_blocks" => Ok("documents:searchBlocks"),
|
||||
"sidebar.dataset.list" => Ok("sidebar:datasetList"),
|
||||
"bridge.request.get" => Ok("bridgeLogs:listByRequest"),
|
||||
"bridge.trace.get" => Ok("bridgeLogs:listByTrace"),
|
||||
"bridge.command.get" => Ok("bridgeLogs:listByCommand"),
|
||||
"bridge.workspace.overview" => Ok("bridgeLogs:listWorkspaceOverview"),
|
||||
_ => Err(retired_bridge_error()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn retired_query_transport_function_name(name: &str) -> BridgeResult<&'static str> {
|
||||
legacy_query_function_name(name)
|
||||
}
|
||||
|
||||
fn legacy_command_function_name(name: &str) -> BridgeResult<&'static str> {
|
||||
match name {
|
||||
"documents.title.update" => Ok("documents:updateTitle"),
|
||||
"page.head.updateTitle" | "tree.node.rename" => Ok("documents:updateTitle"),
|
||||
"documents.save" => Ok("documents:updateContent"),
|
||||
"documents.create" => Ok("documents:createWithParentReference"),
|
||||
"documents.move" => Ok("documents:move"),
|
||||
"documents.delete" => Ok("documents:softDelete"),
|
||||
"documents.restore" => Ok("documents:restore"),
|
||||
"insert_block" | "blocks.patch" | "blocks.move" | "blocks.embed" => {
|
||||
Ok("documents:updateContent")
|
||||
"page.body.save" => Ok("documents:updateContent"),
|
||||
"documents.options.update" | "page.layout.updateOptions" => Ok("documents:updateOptions"),
|
||||
"documents.stats.update" => Ok("documents:updateStats"),
|
||||
"documents.create" | "tree.node.create" => Ok("documents:createWithParentReference"),
|
||||
"documents.move" | "tree.node.move" | "tree.subtree.move" => Ok("documents:move"),
|
||||
"documents.delete" | "tree.node.archive" => Ok("documents:softDelete"),
|
||||
"documents.restore" | "tree.node.restore" => Ok("documents:restore"),
|
||||
"documents.purge" | "tree.node.purge" => Ok("documents:purge"),
|
||||
"documents.copy_tree" | "tree.subtree.copy" => Ok("documents:copyTree"),
|
||||
"documents.duplicate" => Ok("documents:duplicateWithMindmaps"),
|
||||
"documents.embed" | "tree.node.embed" => Ok("documents:updateContent"),
|
||||
"documents.emptyTrashByWorkspace" | "tree.trash.emptyWorkspace" => {
|
||||
Ok("documents:emptyTrashByWorkspace")
|
||||
}
|
||||
"insert_block" | "blocks.patch" => Ok("documents:updateContent"),
|
||||
"blocks.move" => Ok("blocks:move"),
|
||||
"blocks.embed" => Ok("blocks:insert"),
|
||||
"mindmaps.put" => Ok("mindmaps:put"),
|
||||
"mindmap.command.apply" => Ok("mindmaps:applyCommand"),
|
||||
"media.assets.replace_storage" => Ok("mediaAssets:replaceStorageFromUpload"),
|
||||
"tree.filetree.drop.preflight" => Ok("tree:fileTreeDropPreflight"),
|
||||
"tree.filetree.delete.preflight" => Ok("tree:fileTreeDeletePreflight"),
|
||||
"tree.filetree.paste.preflight" => Ok("tree:fileTreePastePreflight"),
|
||||
"tree.filetree.upload-target.preflight" => Ok("tree:fileTreeUploadTargetPreflight"),
|
||||
"tree.resource.copy" => Ok("mediaAssets:batchCopy"),
|
||||
"tree.resource.move" => Ok("mediaAssets:batchMove"),
|
||||
"tree.resource.upload" => Ok("mediaAssets:createWithStorage"),
|
||||
"tree.resource.archive" => Ok("treeResource:archive"),
|
||||
"tree.resource.restore" => Ok("treeResource:restore"),
|
||||
"tree.resource.purge" => Ok("treeResource:purge"),
|
||||
@@ -210,6 +239,10 @@ fn legacy_command_function_name(name: &str) -> BridgeResult<&'static str> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn retired_command_transport_function_name(name: &str) -> BridgeResult<&'static str> {
|
||||
legacy_command_function_name(name)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", tag = "kind")]
|
||||
pub enum RuntimeInput {
|
||||
@@ -1857,7 +1890,7 @@ fn build_resource_lifecycle_plan(
|
||||
Ok(plan)
|
||||
}
|
||||
|
||||
fn resource_lifecycle_convex_function(
|
||||
fn retired_resource_lifecycle_transport_function(
|
||||
action: &str,
|
||||
resource_kind: &str,
|
||||
) -> Result<&'static str, BridgeError> {
|
||||
@@ -4639,7 +4672,7 @@ fn build_tool_plan_steps(
|
||||
steps.push(RuntimeToolPlanStep {
|
||||
kind: "write".into(),
|
||||
name: "media.assets.replace_storage".into(),
|
||||
function_name: Some("mediaAssets:replaceStorageFromUpload".into()),
|
||||
function_name: Some("media.assets.replace_storage".into()),
|
||||
description:
|
||||
"callback 下载并上传文件后,会继续通过统一 bridge 命令写回附件 storage 绑定"
|
||||
.into(),
|
||||
@@ -4754,7 +4787,7 @@ fn build_tool_plan_steps(
|
||||
steps.push(RuntimeToolPlanStep {
|
||||
kind: "write".into(),
|
||||
name: "mindmaps.put".into(),
|
||||
function_name: Some("mindmaps:put".into()),
|
||||
function_name: Some("mindmaps.put".into()),
|
||||
description: "用完整思维导图树覆盖当前导图".into(),
|
||||
args_json: tool_wire.args_json.clone(),
|
||||
});
|
||||
@@ -11384,8 +11417,7 @@ fn execute_command(
|
||||
validate_only: command_wire.validate_only,
|
||||
};
|
||||
let request = build_write_request(&context, &command)?;
|
||||
let function_name =
|
||||
resource_lifecycle_convex_function(action, &lifecycle_plan.resource_kind)?;
|
||||
retired_resource_lifecycle_transport_function(action, &lifecycle_plan.resource_kind)?;
|
||||
let stream_delta_hint = resource_lifecycle_stream_delta_hint(&lifecycle_plan);
|
||||
let event_type = resource_lifecycle_event_type(action);
|
||||
|
||||
@@ -11448,7 +11480,7 @@ fn execute_command(
|
||||
Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan {
|
||||
command_name: command.name,
|
||||
command_id: command.command_id,
|
||||
function_name: function_name.into(),
|
||||
function_name: request.function_name,
|
||||
workspace_id: request.workspace_id,
|
||||
request_id: request.request_id,
|
||||
trace_id: request.trace_id,
|
||||
@@ -12568,7 +12600,7 @@ mod tests {
|
||||
|
||||
match plan {
|
||||
RuntimeExecutionPlan::Query(plan) => {
|
||||
assert_eq!(plan.function_name, "blocks:getById");
|
||||
assert_eq!(plan.function_name, "blocks.get");
|
||||
assert_eq!(
|
||||
plan.args_json,
|
||||
json!({
|
||||
@@ -12628,7 +12660,7 @@ mod tests {
|
||||
|
||||
match plan {
|
||||
RuntimeExecutionPlan::Command(plan) => {
|
||||
assert_eq!(plan.function_name, "documents:updateContent");
|
||||
assert_eq!(plan.function_name, "blocks.patch");
|
||||
assert_eq!(plan.command_name, "blocks.patch");
|
||||
assert_eq!(
|
||||
plan.args_json,
|
||||
@@ -12965,7 +12997,7 @@ mod tests {
|
||||
|
||||
match plan {
|
||||
RuntimeExecutionPlan::Query(plan) => {
|
||||
assert_eq!(plan.function_name, "sidebar:datasetList");
|
||||
assert_eq!(plan.function_name, "sidebar.dataset.list");
|
||||
assert_eq!(plan.args_json, json!({ "workspaceId": "ws_1" }));
|
||||
}
|
||||
RuntimeExecutionPlan::Command(_) => panic!("expected query plan"),
|
||||
@@ -12999,7 +13031,7 @@ mod tests {
|
||||
|
||||
match plan {
|
||||
RuntimeExecutionPlan::Query(plan) => {
|
||||
assert_eq!(plan.function_name, "search:documents");
|
||||
assert_eq!(plan.function_name, "search.documents");
|
||||
assert_eq!(
|
||||
plan.args_json,
|
||||
json!({
|
||||
@@ -13969,7 +14001,7 @@ mod tests {
|
||||
match plan {
|
||||
RuntimeExecutionPlan::Query(plan) => {
|
||||
assert_eq!(plan.query_name, "mindmaps.get");
|
||||
assert_eq!(plan.function_name, "mindmaps:get");
|
||||
assert_eq!(plan.function_name, "mindmaps.get");
|
||||
assert_eq!(plan.args_json["docId"], json!("doc_1"));
|
||||
assert_eq!(plan.args_json["mindmapId"], json!("mind_1"));
|
||||
}
|
||||
@@ -14022,7 +14054,7 @@ mod tests {
|
||||
match plan {
|
||||
RuntimeExecutionPlan::Command(plan) => {
|
||||
assert_eq!(plan.command_name, "mindmap.command.apply");
|
||||
assert_eq!(plan.function_name, "mindmaps:applyCommand");
|
||||
assert_eq!(plan.function_name, "mindmap.command.apply");
|
||||
assert_eq!(
|
||||
plan.args_json["canonicalCommand"],
|
||||
json!("mindmap.command.apply")
|
||||
@@ -14301,7 +14333,7 @@ mod tests {
|
||||
|
||||
match plan {
|
||||
RuntimeExecutionPlan::Query(plan) => {
|
||||
assert_eq!(plan.function_name, "mindmaps:get");
|
||||
assert_eq!(plan.function_name, "mindmaps.get");
|
||||
assert_eq!(
|
||||
plan.args_json,
|
||||
json!({
|
||||
@@ -14360,7 +14392,7 @@ mod tests {
|
||||
|
||||
match plan {
|
||||
RuntimeExecutionPlan::Command(plan) => {
|
||||
assert_eq!(plan.function_name, "mindmaps:put");
|
||||
assert_eq!(plan.function_name, "mindmaps.put");
|
||||
assert_eq!(plan.args_json["docId"], json!("doc_1"));
|
||||
assert_eq!(plan.args_json["mindmapId"], json!("mind_1"));
|
||||
assert_eq!(
|
||||
@@ -14963,7 +14995,7 @@ mod tests {
|
||||
panic!("expected command plan");
|
||||
};
|
||||
|
||||
assert_eq!(plan.function_name, "documents:updateContent");
|
||||
assert_eq!(plan.function_name, "documents.save");
|
||||
assert_eq!(plan.args_json.get("id"), Some(&json!("doc_1")));
|
||||
assert_eq!(
|
||||
plan.args_json.pointer("/editorDocument/rootBlockIds/0"),
|
||||
@@ -15507,7 +15539,7 @@ mod tests {
|
||||
panic!("expected command plan");
|
||||
};
|
||||
assert_eq!(plan.command_name, "page.body.save");
|
||||
assert_eq!(plan.function_name, "documents:updateContent");
|
||||
assert_eq!(plan.function_name, "page.body.save");
|
||||
assert_eq!(plan.args_json["expectedRevision"], json!(1));
|
||||
assert_eq!(plan.args_json["conflictDetectionKey"], json!("doc_1:1"));
|
||||
}
|
||||
@@ -15551,7 +15583,7 @@ mod tests {
|
||||
panic!("expected command plan");
|
||||
};
|
||||
assert_eq!(plan.command_name, "page.head.updateTitle");
|
||||
assert_eq!(plan.function_name, "documents:updateTitle");
|
||||
assert_eq!(plan.function_name, "page.head.updateTitle");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -15594,7 +15626,7 @@ mod tests {
|
||||
panic!("expected command plan");
|
||||
};
|
||||
assert_eq!(plan.command_name, "page.layout.updateOptions");
|
||||
assert_eq!(plan.function_name, "documents:updateOptions");
|
||||
assert_eq!(plan.function_name, "page.layout.updateOptions");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -15892,7 +15924,7 @@ mod tests {
|
||||
panic!("expected command plan");
|
||||
};
|
||||
|
||||
assert_eq!(plan.function_name, "documents:updateContent");
|
||||
assert_eq!(plan.function_name, "documents.save");
|
||||
assert_eq!(
|
||||
plan.args_json.pointer("/content/0/content"),
|
||||
Some(&json!("来自 editorDocument"))
|
||||
@@ -15960,7 +15992,7 @@ mod tests {
|
||||
panic!("expected command plan");
|
||||
};
|
||||
|
||||
assert_eq!(plan.function_name, "documents:updateContent");
|
||||
assert_eq!(plan.function_name, "page.body.save");
|
||||
assert_eq!(
|
||||
plan.args_json.pointer("/content/0/id"),
|
||||
Some(&json!("legacy_content_1"))
|
||||
@@ -16276,7 +16308,7 @@ mod tests {
|
||||
|
||||
match plan {
|
||||
RuntimeExecutionPlan::Command(plan) => {
|
||||
assert_eq!(plan.function_name, "blocks:move");
|
||||
assert_eq!(plan.function_name, "blocks.move");
|
||||
assert_eq!(plan.command_name, "blocks.move");
|
||||
assert_eq!(
|
||||
plan.args_json,
|
||||
@@ -16372,7 +16404,7 @@ mod tests {
|
||||
|
||||
match plan {
|
||||
RuntimeExecutionPlan::Command(plan) => {
|
||||
assert_eq!(plan.function_name, "blocks:insert");
|
||||
assert_eq!(plan.function_name, "blocks.embed");
|
||||
assert_eq!(plan.command_name, "blocks.embed");
|
||||
assert_eq!(
|
||||
plan.args_json,
|
||||
@@ -16631,7 +16663,7 @@ mod tests {
|
||||
|
||||
match plan {
|
||||
RuntimeExecutionPlan::Command(plan) => {
|
||||
assert_eq!(plan.function_name, "documents:updateContent");
|
||||
assert_eq!(plan.function_name, "documents.embed");
|
||||
assert_eq!(plan.command_name, "documents.embed");
|
||||
assert_eq!(
|
||||
plan.args_json,
|
||||
@@ -16677,7 +16709,7 @@ mod tests {
|
||||
let cases = [
|
||||
(
|
||||
"tree.node.archive",
|
||||
"documents:softDelete",
|
||||
"tree.node.archive",
|
||||
json!({
|
||||
"documentId": "doc_1",
|
||||
"workspaceId": "ws_1",
|
||||
@@ -16719,7 +16751,7 @@ mod tests {
|
||||
),
|
||||
(
|
||||
"tree.node.restore",
|
||||
"documents:restore",
|
||||
"tree.node.restore",
|
||||
json!({
|
||||
"documentId": "doc_1",
|
||||
"workspaceId": "ws_1",
|
||||
@@ -16761,7 +16793,7 @@ mod tests {
|
||||
),
|
||||
(
|
||||
"tree.node.purge",
|
||||
"documents:purge",
|
||||
"tree.node.purge",
|
||||
json!({
|
||||
"documentId": "doc_1",
|
||||
}),
|
||||
@@ -17305,7 +17337,7 @@ mod tests {
|
||||
};
|
||||
|
||||
assert_eq!(plan.command_name, "tree.subtree.move");
|
||||
assert_eq!(plan.function_name, "documents:move");
|
||||
assert_eq!(plan.function_name, "tree.subtree.move");
|
||||
assert_eq!(plan.args_json["sortOrder"], json!(-2));
|
||||
assert_eq!(
|
||||
plan.args_json["commandProtocol"],
|
||||
@@ -17793,7 +17825,7 @@ mod tests {
|
||||
match create_plan {
|
||||
RuntimeExecutionPlan::Command(plan) => {
|
||||
assert_eq!(plan.command_name, "tree.node.create");
|
||||
assert_eq!(plan.function_name, "documents:createWithParentReference");
|
||||
assert_eq!(plan.function_name, "tree.node.create");
|
||||
assert_eq!(
|
||||
plan.args_json["streamDeltaHint"],
|
||||
json!({
|
||||
@@ -17872,7 +17904,7 @@ mod tests {
|
||||
match rename_plan {
|
||||
RuntimeExecutionPlan::Command(plan) => {
|
||||
assert_eq!(plan.command_name, "tree.node.rename");
|
||||
assert_eq!(plan.function_name, "documents:updateTitle");
|
||||
assert_eq!(plan.function_name, "tree.node.rename");
|
||||
assert_eq!(
|
||||
plan.args_json["streamDeltaHint"],
|
||||
json!({
|
||||
@@ -17978,7 +18010,7 @@ mod tests {
|
||||
|
||||
match embed_plan {
|
||||
RuntimeExecutionPlan::Command(plan) => {
|
||||
assert_eq!(plan.function_name, "documents:updateContent");
|
||||
assert_eq!(plan.function_name, "tree.node.embed");
|
||||
assert_eq!(plan.command_name, "tree.node.embed");
|
||||
assert_eq!(
|
||||
plan.args_json,
|
||||
@@ -18105,7 +18137,7 @@ mod tests {
|
||||
|
||||
match copy_plan {
|
||||
RuntimeExecutionPlan::Command(plan) => {
|
||||
assert_eq!(plan.function_name, "documents:copyTree");
|
||||
assert_eq!(plan.function_name, "tree.subtree.copy");
|
||||
assert_eq!(plan.command_name, "tree.subtree.copy");
|
||||
assert_eq!(
|
||||
plan.args_json,
|
||||
@@ -18199,7 +18231,7 @@ mod tests {
|
||||
match plan {
|
||||
RuntimeExecutionPlan::Command(plan) => {
|
||||
assert_eq!(plan.command_name, "documents.emptyTrashByWorkspace");
|
||||
assert_eq!(plan.function_name, "documents:emptyTrashByWorkspace");
|
||||
assert_eq!(plan.function_name, "documents.emptyTrashByWorkspace");
|
||||
assert_eq!(plan.args_json["workspaceId"], json!("ws_1"));
|
||||
assert_eq!(
|
||||
plan.args_json["streamDeltaHint"],
|
||||
@@ -18284,7 +18316,7 @@ mod tests {
|
||||
|
||||
match plan {
|
||||
RuntimeExecutionPlan::Command(plan) => {
|
||||
assert_eq!(plan.function_name, "documents:duplicateWithMindmaps");
|
||||
assert_eq!(plan.function_name, "documents.duplicate");
|
||||
assert_eq!(plan.command_name, "documents.duplicate");
|
||||
assert_eq!(
|
||||
plan.args_json["streamDeltaHint"],
|
||||
@@ -18361,7 +18393,7 @@ mod tests {
|
||||
match plan {
|
||||
RuntimeExecutionPlan::Command(plan) => {
|
||||
assert_eq!(plan.command_name, "documents.options.update");
|
||||
assert_eq!(plan.function_name, "documents:updateOptions");
|
||||
assert_eq!(plan.function_name, "documents.options.update");
|
||||
assert_eq!(plan.args_json["id"], json!("doc_1"));
|
||||
assert_eq!(
|
||||
plan.args_json["options"],
|
||||
@@ -18453,7 +18485,7 @@ mod tests {
|
||||
match plan {
|
||||
RuntimeExecutionPlan::Command(plan) => {
|
||||
assert_eq!(plan.command_name, "documents.stats.update");
|
||||
assert_eq!(plan.function_name, "documents:updateStats");
|
||||
assert_eq!(plan.function_name, "documents.stats.update");
|
||||
assert_eq!(
|
||||
plan.args_json,
|
||||
json!({
|
||||
@@ -18517,7 +18549,7 @@ mod tests {
|
||||
match plan {
|
||||
RuntimeExecutionPlan::Command(plan) => {
|
||||
assert_eq!(plan.command_name, "media.assets.replace_storage");
|
||||
assert_eq!(plan.function_name, "mediaAssets:replaceStorageFromUpload");
|
||||
assert_eq!(plan.function_name, "media.assets.replace_storage");
|
||||
assert_eq!(
|
||||
plan.args_json,
|
||||
json!({
|
||||
@@ -18539,13 +18571,13 @@ mod tests {
|
||||
let cases = [
|
||||
(
|
||||
"tree.resource.copy",
|
||||
"mediaAssets:batchCopy",
|
||||
"tree.resource.copy",
|
||||
"copy",
|
||||
"tree.resource.copied",
|
||||
),
|
||||
(
|
||||
"tree.resource.move",
|
||||
"mediaAssets:batchMove",
|
||||
"tree.resource.move",
|
||||
"move",
|
||||
"tree.resource.moved",
|
||||
),
|
||||
@@ -18642,7 +18674,7 @@ mod tests {
|
||||
let cases = [
|
||||
(
|
||||
"tree.resource.archive",
|
||||
"mediaAssets:patchById",
|
||||
"tree.resource.archive",
|
||||
"archive",
|
||||
"tree.resource.archived",
|
||||
json!({
|
||||
@@ -18690,7 +18722,7 @@ mod tests {
|
||||
),
|
||||
(
|
||||
"tree.resource.restore",
|
||||
"mediaAssets:patchById",
|
||||
"tree.resource.restore",
|
||||
"restore",
|
||||
"tree.resource.restored",
|
||||
json!({
|
||||
@@ -18738,7 +18770,7 @@ mod tests {
|
||||
),
|
||||
(
|
||||
"tree.resource.purge",
|
||||
"mediaAssets:purgeById",
|
||||
"tree.resource.purge",
|
||||
"purge",
|
||||
"tree.resource.purged",
|
||||
json!({
|
||||
@@ -18786,7 +18818,7 @@ mod tests {
|
||||
),
|
||||
(
|
||||
"tree.resource.rename",
|
||||
"mediaAssets:patchById",
|
||||
"tree.resource.rename",
|
||||
"rename",
|
||||
"tree.resource.renamed",
|
||||
json!({
|
||||
@@ -18891,7 +18923,7 @@ mod tests {
|
||||
(
|
||||
"tree.resource.archive",
|
||||
"mindmap",
|
||||
"mindmaps:softDelete",
|
||||
"tree.resource.archive",
|
||||
json!({
|
||||
"resourceKind": "mindmap",
|
||||
"documentId": "doc_1",
|
||||
@@ -18915,7 +18947,7 @@ mod tests {
|
||||
(
|
||||
"tree.resource.restore",
|
||||
"mindmap",
|
||||
"mindmaps:restore",
|
||||
"tree.resource.restore",
|
||||
json!({
|
||||
"resourceKind": "mindmap",
|
||||
"documentId": "doc_1",
|
||||
@@ -18939,7 +18971,7 @@ mod tests {
|
||||
(
|
||||
"tree.resource.purge",
|
||||
"mindmap",
|
||||
"mindmaps:purge",
|
||||
"tree.resource.purge",
|
||||
json!({
|
||||
"resourceKind": "mindmap",
|
||||
"documentId": "doc_1",
|
||||
@@ -18963,7 +18995,7 @@ mod tests {
|
||||
(
|
||||
"tree.resource.archive",
|
||||
"table",
|
||||
"tables:remove",
|
||||
"tree.resource.archive",
|
||||
json!({
|
||||
"resourceKind": "table",
|
||||
"tableId": "table_1",
|
||||
@@ -18986,7 +19018,7 @@ mod tests {
|
||||
(
|
||||
"tree.resource.restore",
|
||||
"table",
|
||||
"tables:restore",
|
||||
"tree.resource.restore",
|
||||
json!({
|
||||
"resourceKind": "table",
|
||||
"tableId": "table_1",
|
||||
@@ -19009,7 +19041,7 @@ mod tests {
|
||||
(
|
||||
"tree.resource.purge",
|
||||
"table",
|
||||
"tables:purge",
|
||||
"tree.resource.purge",
|
||||
json!({
|
||||
"resourceKind": "table",
|
||||
"tableId": "table_1",
|
||||
@@ -19263,7 +19295,7 @@ mod tests {
|
||||
};
|
||||
|
||||
assert_eq!(plan.command_name, "tree.filetree.drop.preflight");
|
||||
assert_eq!(plan.function_name, "tree:fileTreeDropPreflight");
|
||||
assert_eq!(plan.function_name, "tree.filetree.drop.preflight");
|
||||
assert_eq!(
|
||||
plan.args_json["fileTreeDropPlan"],
|
||||
json!({
|
||||
@@ -19677,7 +19709,7 @@ mod tests {
|
||||
};
|
||||
|
||||
assert_eq!(plan.command_name, "tree.filetree.delete.preflight");
|
||||
assert_eq!(plan.function_name, "tree:fileTreeDeletePreflight");
|
||||
assert_eq!(plan.function_name, "tree.filetree.delete.preflight");
|
||||
assert_eq!(
|
||||
plan.args_json["fileTreeDeletePlan"],
|
||||
json!({
|
||||
@@ -19773,7 +19805,7 @@ mod tests {
|
||||
};
|
||||
|
||||
assert_eq!(plan.command_name, "tree.filetree.paste.preflight");
|
||||
assert_eq!(plan.function_name, "tree:fileTreePastePreflight");
|
||||
assert_eq!(plan.function_name, "tree.filetree.paste.preflight");
|
||||
assert_eq!(
|
||||
plan.args_json["fileTreePastePlan"],
|
||||
json!({
|
||||
@@ -19864,7 +19896,7 @@ mod tests {
|
||||
};
|
||||
|
||||
assert_eq!(plan.command_name, "tree.filetree.upload-target.preflight");
|
||||
assert_eq!(plan.function_name, "tree:fileTreeUploadTargetPreflight");
|
||||
assert_eq!(plan.function_name, "tree.filetree.upload-target.preflight");
|
||||
assert_eq!(
|
||||
plan.args_json["fileTreeUploadTargetPlan"],
|
||||
json!({
|
||||
@@ -19926,7 +19958,7 @@ mod tests {
|
||||
};
|
||||
|
||||
assert_eq!(plan.command_name, "tree.resource.upload");
|
||||
assert_eq!(plan.function_name, "mediaAssets:createWithStorage");
|
||||
assert_eq!(plan.function_name, "tree.resource.upload");
|
||||
assert_eq!(
|
||||
plan.args_json,
|
||||
json!({
|
||||
@@ -20093,7 +20125,7 @@ mod tests {
|
||||
|
||||
match plan {
|
||||
RuntimeExecutionPlan::Query(plan) => {
|
||||
assert_eq!(plan.function_name, "bridgeLogs:listByRequest");
|
||||
assert_eq!(plan.function_name, "bridge.request.get");
|
||||
assert_eq!(plan.args_json["workspaceId"], json!("ws_1"));
|
||||
assert_eq!(plan.args_json["requestId"], json!("req_lookup_1"));
|
||||
assert_eq!(plan.args_json["commandId"], json!("cmd_lookup_1"));
|
||||
@@ -20129,7 +20161,7 @@ mod tests {
|
||||
assert_eq!(plan.steps[0].name, "bridge.trace.get");
|
||||
assert_eq!(
|
||||
plan.steps[0].function_name.as_deref(),
|
||||
Some("bridgeLogs:listByTrace")
|
||||
Some("bridge.trace.get")
|
||||
);
|
||||
}
|
||||
_ => panic!("expected tool plan"),
|
||||
|
||||
@@ -571,7 +571,7 @@ pub fn plan_page_get(
|
||||
"workspaceId": workspace_id,
|
||||
}),
|
||||
CliTransportPlan {
|
||||
kind: "convex_query".into(),
|
||||
kind: "runtime_query_plan".into(),
|
||||
function_name: request.function_name,
|
||||
payload_json: request.payload_json,
|
||||
args_json: json!({
|
||||
@@ -622,7 +622,7 @@ pub fn plan_page_title(
|
||||
"title": title,
|
||||
}),
|
||||
CliTransportPlan {
|
||||
kind: "convex_mutation".into(),
|
||||
kind: "runtime_command_plan".into(),
|
||||
function_name: request.function_name,
|
||||
payload_json: request.payload_json,
|
||||
args_json: json!({
|
||||
@@ -682,7 +682,7 @@ pub fn plan_page_save(
|
||||
"conflictDetectionKey": conflict_detection_key,
|
||||
}),
|
||||
CliTransportPlan {
|
||||
kind: "convex_mutation".into(),
|
||||
kind: "runtime_command_plan".into(),
|
||||
function_name: request.function_name,
|
||||
payload_json: request.payload_json,
|
||||
args_json: json!({
|
||||
@@ -737,7 +737,7 @@ pub fn plan_page_create(ctx: &CliContext, args: &PageCreateArgs<'_>) -> CliResul
|
||||
"content": content_value,
|
||||
}),
|
||||
CliTransportPlan {
|
||||
kind: "convex_mutation".into(),
|
||||
kind: "runtime_command_plan".into(),
|
||||
function_name: request.function_name,
|
||||
payload_json: request.payload_json,
|
||||
args_json: json!({
|
||||
@@ -790,7 +790,7 @@ pub fn plan_page_move(ctx: &CliContext, args: &PageMoveArgs<'_>) -> CliResult<Cl
|
||||
"sortOrder": args.sort_order,
|
||||
}),
|
||||
CliTransportPlan {
|
||||
kind: "convex_mutation".into(),
|
||||
kind: "runtime_command_plan".into(),
|
||||
function_name: request.function_name,
|
||||
payload_json: request.payload_json,
|
||||
args_json: json!({
|
||||
@@ -836,7 +836,7 @@ pub fn plan_page_delete(ctx: &CliContext, args: &PageDeleteArgs<'_>) -> CliResul
|
||||
"workspaceId": args.workspace_id,
|
||||
}),
|
||||
CliTransportPlan {
|
||||
kind: "convex_mutation".into(),
|
||||
kind: "runtime_command_plan".into(),
|
||||
function_name: request.function_name,
|
||||
payload_json: request.payload_json,
|
||||
args_json: json!({
|
||||
@@ -880,7 +880,7 @@ pub fn plan_page_restore(ctx: &CliContext, args: &PageRestoreArgs<'_>) -> CliRes
|
||||
"workspaceId": args.workspace_id,
|
||||
}),
|
||||
CliTransportPlan {
|
||||
kind: "convex_mutation".into(),
|
||||
kind: "runtime_command_plan".into(),
|
||||
function_name: request.function_name,
|
||||
payload_json: request.payload_json,
|
||||
args_json: json!({
|
||||
@@ -940,7 +940,7 @@ pub fn plan_block_insert(
|
||||
"prevBlockId": prev_block_id,
|
||||
}),
|
||||
CliTransportPlan {
|
||||
kind: "convex_mutation".into(),
|
||||
kind: "runtime_command_plan".into(),
|
||||
function_name: request.function_name,
|
||||
payload_json: request.payload_json,
|
||||
args_json: json!({
|
||||
@@ -1006,7 +1006,7 @@ pub fn plan_block_patch(
|
||||
"conflictDetectionKey": conflict_detection_key,
|
||||
}),
|
||||
CliTransportPlan {
|
||||
kind: "convex_mutation".into(),
|
||||
kind: "runtime_command_plan".into(),
|
||||
function_name: request.function_name,
|
||||
payload_json: request.payload_json,
|
||||
args_json: json!({
|
||||
@@ -1058,7 +1058,7 @@ pub fn plan_block_move(ctx: &CliContext, args: &BlockMoveArgs<'_>) -> CliResult<
|
||||
"targetDocumentId": args.target_document_id,
|
||||
}),
|
||||
CliTransportPlan {
|
||||
kind: "convex_mutation".into(),
|
||||
kind: "runtime_command_plan".into(),
|
||||
function_name: request.function_name,
|
||||
payload_json: request.payload_json,
|
||||
args_json: json!({
|
||||
@@ -1109,7 +1109,7 @@ pub fn plan_block_embed(ctx: &CliContext, args: &BlockEmbedArgs<'_>) -> CliResul
|
||||
"targetBlockId": args.target_block_id,
|
||||
}),
|
||||
CliTransportPlan {
|
||||
kind: "convex_mutation".into(),
|
||||
kind: "runtime_command_plan".into(),
|
||||
function_name: request.function_name,
|
||||
payload_json: request.payload_json,
|
||||
args_json: json!({
|
||||
@@ -1174,7 +1174,7 @@ pub fn plan_search_documents(
|
||||
},
|
||||
}),
|
||||
CliTransportPlan {
|
||||
kind: "convex_query".into(),
|
||||
kind: "runtime_query_plan".into(),
|
||||
function_name: request.function_name,
|
||||
payload_json: request.payload_json,
|
||||
args_json: json!({
|
||||
@@ -1230,7 +1230,7 @@ pub fn plan_search_blocks(
|
||||
},
|
||||
}),
|
||||
CliTransportPlan {
|
||||
kind: "convex_query".into(),
|
||||
kind: "runtime_query_plan".into(),
|
||||
function_name: request.function_name,
|
||||
payload_json: request.payload_json,
|
||||
args_json: json!({
|
||||
@@ -1262,7 +1262,7 @@ pub fn plan_sidebar_dataset(ctx: &CliContext, workspace_id: &str) -> CliResult<C
|
||||
"workspaceId": workspace_id,
|
||||
}),
|
||||
CliTransportPlan {
|
||||
kind: "convex_query".into(),
|
||||
kind: "runtime_query_plan".into(),
|
||||
function_name: request.function_name,
|
||||
payload_json: request.payload_json,
|
||||
args_json: json!({
|
||||
@@ -1306,7 +1306,7 @@ pub fn plan_mindmap_get(
|
||||
"mindmapId": mindmap_id,
|
||||
}),
|
||||
CliTransportPlan {
|
||||
kind: "convex_query".into(),
|
||||
kind: "runtime_query_plan".into(),
|
||||
function_name: request.function_name,
|
||||
payload_json: request.payload_json,
|
||||
args_json: json!({
|
||||
@@ -1375,7 +1375,7 @@ pub fn plan_mindmap_put(
|
||||
"data": data,
|
||||
}),
|
||||
CliTransportPlan {
|
||||
kind: "convex_mutation".into(),
|
||||
kind: "runtime_command_plan".into(),
|
||||
function_name: request.function_name,
|
||||
payload_json: request.payload_json,
|
||||
args_json: json!({
|
||||
@@ -2118,7 +2118,8 @@ mod tests {
|
||||
} => {
|
||||
assert_eq!(name, "documents.save");
|
||||
assert_eq!(command_id, "cmd_page_save_page_1");
|
||||
assert_eq!(transport.function_name, "documents:updateContent");
|
||||
assert_eq!(transport.kind, "runtime_command_plan");
|
||||
assert_eq!(transport.function_name, "documents.save");
|
||||
assert_eq!(
|
||||
transport.args_json,
|
||||
json!({
|
||||
@@ -2165,10 +2166,8 @@ mod tests {
|
||||
name, transport, ..
|
||||
} => {
|
||||
assert_eq!(name, "documents.create");
|
||||
assert_eq!(
|
||||
transport.function_name,
|
||||
"documents:createWithParentReference"
|
||||
);
|
||||
assert_eq!(transport.kind, "runtime_command_plan");
|
||||
assert_eq!(transport.function_name, "documents.create");
|
||||
assert_eq!(transport.args_json["workspaceId"], json!("ws_1"));
|
||||
assert_eq!(transport.args_json["parentId"], json!("parent_1"));
|
||||
}
|
||||
@@ -2194,7 +2193,8 @@ mod tests {
|
||||
name, transport, ..
|
||||
} => {
|
||||
assert_eq!(name, "documents.move");
|
||||
assert_eq!(transport.function_name, "documents:move");
|
||||
assert_eq!(transport.kind, "runtime_command_plan");
|
||||
assert_eq!(transport.function_name, "documents.move");
|
||||
assert_eq!(transport.args_json["sortOrder"], json!(3));
|
||||
}
|
||||
_ => panic!("expected command output"),
|
||||
@@ -2211,7 +2211,8 @@ mod tests {
|
||||
name, transport, ..
|
||||
} => {
|
||||
assert_eq!(name, "sidebar.dataset.list");
|
||||
assert_eq!(transport.function_name, "sidebar:datasetList");
|
||||
assert_eq!(transport.kind, "runtime_query_plan");
|
||||
assert_eq!(transport.function_name, "sidebar.dataset.list");
|
||||
assert_eq!(transport.args_json, json!({ "workspaceId": "ws_1" }));
|
||||
}
|
||||
_ => panic!("expected query output"),
|
||||
@@ -2292,7 +2293,7 @@ mod tests {
|
||||
name, transport, ..
|
||||
} => {
|
||||
assert_eq!(name, "mindmaps.get");
|
||||
assert_eq!(transport.function_name, "mindmaps:get");
|
||||
assert_eq!(transport.function_name, "mindmaps.get");
|
||||
assert_eq!(
|
||||
transport.args_json,
|
||||
json!({
|
||||
@@ -2322,7 +2323,7 @@ mod tests {
|
||||
name, transport, ..
|
||||
} => {
|
||||
assert_eq!(name, "mindmaps.put");
|
||||
assert_eq!(transport.function_name, "mindmaps:put");
|
||||
assert_eq!(transport.function_name, "mindmaps.put");
|
||||
assert_eq!(
|
||||
transport.args_json,
|
||||
json!({
|
||||
|
||||
@@ -842,6 +842,8 @@ import {
|
||||
setStatus(runtimeDescriptor, 'ready');
|
||||
}
|
||||
if (session.pageBodySource) runtimeDescriptor.root.setAttribute('data-mnote-page-body-source', session.pageBodySource);
|
||||
runtimeDescriptor.root.setAttribute('data-mnote-page-body-local-compat-fallback', session.pageBodyLocalCompatFallback ? 'true' : 'false');
|
||||
if (session.pageBodyHardGuard) runtimeDescriptor.root.setAttribute('data-mnote-page-body-hard-guard', session.pageBodyHardGuard);
|
||||
if (session.projectionSource) runtimeDescriptor.root.setAttribute('data-mnote-projection-source', session.projectionSource);
|
||||
if (session.blockProjectionVersion) runtimeDescriptor.root.setAttribute('data-mnote-block-projection-version', session.blockProjectionVersion);
|
||||
enhanceEditorAttachmentLinksSoon();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -125,6 +125,8 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
|
||||
return `${String(session?.rootUri || '').trim()}#${scope}`;
|
||||
};
|
||||
|
||||
const localFolderEventBusChannelKey = (session) => `${String(session?.rootUri || '').trim()}#event-bus`;
|
||||
|
||||
const localMarkdownRelativePathFromDocumentId = (documentId) => {
|
||||
const value = String(documentId || '').trim();
|
||||
if (!value.startsWith('local-md:')) return '';
|
||||
@@ -136,6 +138,25 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
|
||||
|| localMarkdownRelativePathFromDocumentId(session?.documentId)
|
||||
);
|
||||
|
||||
const localFolderEventItems = (payload) => {
|
||||
if (!payload || typeof payload !== 'object') return [];
|
||||
const changedPaths = Array.isArray(payload.changedPaths)
|
||||
? payload.changedPaths
|
||||
: (Array.isArray(payload.changed_paths) ? payload.changed_paths : []);
|
||||
if (changedPaths.length > 0) {
|
||||
return changedPaths.map((item) => {
|
||||
if (typeof item === 'string') return { relativePath: item };
|
||||
if (!item || typeof item !== 'object') return null;
|
||||
return {
|
||||
relativePath: String(item.relativePath || item.relative_path || item.path || '').trim(),
|
||||
documentId: String(item.documentId || item.document_id || '').trim(),
|
||||
eventKind: String(item.eventKind || item.event_kind || item.changeType || item.change_type || '').trim(),
|
||||
};
|
||||
}).filter(Boolean);
|
||||
}
|
||||
return [payload];
|
||||
};
|
||||
|
||||
const sessionBufferStateUrl = (session) => {
|
||||
if (!session || session.sourceKind !== 'local_folder' || !session.rootUri || !session.documentId) return null;
|
||||
const url = new URL('/api/documents/buffer-state', window.location.origin);
|
||||
@@ -206,10 +227,14 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
|
||||
if (!channel) return;
|
||||
channel.sessions.delete(session.key);
|
||||
if (channel.sessions.size === 0) {
|
||||
try {
|
||||
channel.eventSource.close();
|
||||
} catch (_) {
|
||||
// noop
|
||||
if (typeof channel.unsubscribe === 'function') {
|
||||
channel.unsubscribe();
|
||||
} else if (channel.eventSource && typeof channel.eventSource.close === 'function') {
|
||||
try {
|
||||
channel.eventSource.close();
|
||||
} catch (_) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
localFolderEventRegistry.delete(channel.key);
|
||||
}
|
||||
@@ -385,7 +410,53 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
|
||||
}, source || 'mnote-web-document-session-meta');
|
||||
};
|
||||
|
||||
const pageBodySourceMeta = (sourceKind, pageBody) => {
|
||||
const pageBodySource = pageBodyTiptapDocumentSource(pageBody || {}, '');
|
||||
const projectionSource = String(pageBody?.projectionSource || pageBody?.projection_source || '').trim();
|
||||
const blockProjectionVersion = String(pageBody?.blockProjectionVersion || pageBody?.block_projection_version || '').trim();
|
||||
const localCompatFallback = sourceKind === 'local_folder' && pageBodySource === 'compat.legacy_content';
|
||||
return {
|
||||
pageBodySource,
|
||||
projectionSource,
|
||||
blockProjectionVersion,
|
||||
localCompatFallback,
|
||||
hardGuard: localCompatFallback
|
||||
? 'local_compat_fallback'
|
||||
: (sourceKind === 'local_folder' ? 'local_ok' : 'compat_allowed'),
|
||||
};
|
||||
};
|
||||
|
||||
const applyPageBodySourceMetaToSession = (session, pageBody) => {
|
||||
const meta = pageBodySourceMeta(session.sourceKind, pageBody || {});
|
||||
session.pageBodySource = meta.pageBodySource;
|
||||
session.projectionSource = meta.projectionSource;
|
||||
session.blockProjectionVersion = meta.blockProjectionVersion;
|
||||
session.pageBodyLocalCompatFallback = meta.localCompatFallback;
|
||||
session.pageBodyHardGuard = meta.hardGuard;
|
||||
};
|
||||
|
||||
const syncPageBodySourceDiagnosticsToViews = (session) => {
|
||||
sessionViews(session).forEach((view) => {
|
||||
const root = view?.runtimeDescriptor?.root;
|
||||
if (!(root instanceof HTMLElement)) return;
|
||||
root.setAttribute('data-mnote-page-body-source', session.pageBodySource || '');
|
||||
root.setAttribute('data-mnote-page-body-local-compat-fallback', session.pageBodyLocalCompatFallback ? 'true' : 'false');
|
||||
root.setAttribute('data-mnote-page-body-hard-guard', session.pageBodyHardGuard || '');
|
||||
if (session.projectionSource) {
|
||||
root.setAttribute('data-mnote-projection-source', session.projectionSource);
|
||||
} else {
|
||||
root.removeAttribute('data-mnote-projection-source');
|
||||
}
|
||||
if (session.blockProjectionVersion) {
|
||||
root.setAttribute('data-mnote-block-projection-version', session.blockProjectionVersion);
|
||||
} else {
|
||||
root.removeAttribute('data-mnote-block-projection-version');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const syncSessionMetaToViews = (session) => {
|
||||
syncPageBodySourceDiagnosticsToViews(session);
|
||||
sessionViews(session).forEach((view) => {
|
||||
if (view.mountId != null) dispatchSessionMetaToView(session, view);
|
||||
});
|
||||
@@ -543,6 +614,7 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
|
||||
const nextRevision = Number.isInteger(nextBody.revision) ? nextBody.revision : revisionFromConflictKey(nextConflictKey);
|
||||
session.latestAggregate = nextAggregate;
|
||||
syncPageAggregateScript(session, nextAggregate);
|
||||
applyPageBodySourceMetaToSession(session, nextBody);
|
||||
session.title = nextAggregate?.head?.title || session.title;
|
||||
session.currentTiptapDocument = nextTiptapDocument;
|
||||
session.currentSerialized = nextSerialized;
|
||||
@@ -1168,15 +1240,22 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
|
||||
const nextPermissions = nextAggregate?.head?.permissions || {};
|
||||
const nextConflictKey = conflictDetectionKeyFromBody(nextBody);
|
||||
const nextTiptapDocument = pageBodyTiptapDocument(nextBody, '', session);
|
||||
const nextPageBodyMeta = pageBodySourceMeta(session.sourceKind, nextBody);
|
||||
const nextSerialized = JSON.stringify(nextTiptapDocument);
|
||||
const contentChanged = nextSerialized !== session.currentSerialized;
|
||||
session.externalChangePending = false;
|
||||
session.bufferDirtyState = 'Clean';
|
||||
if (!nextConflictKey || !session.lastExternalConflictDetectionKey) {
|
||||
session.lastExternalConflictDetectionKey = nextConflictKey || session.lastExternalConflictDetectionKey;
|
||||
if (!contentChanged) return;
|
||||
if (!contentChanged) {
|
||||
applyPageBodySourceMetaToSession(session, nextBody);
|
||||
syncPageBodySourceDiagnosticsToViews(session);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (nextConflictKey === session.lastExternalConflictDetectionKey) {
|
||||
applyPageBodySourceMetaToSession(session, nextBody);
|
||||
syncPageBodySourceDiagnosticsToViews(session);
|
||||
if (nextConflictKey && session.conflictDetectionKey !== nextConflictKey) {
|
||||
session.conflictDetectionKey = nextConflictKey;
|
||||
session.fileVersion = nextConflictKey;
|
||||
@@ -1187,6 +1266,11 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
|
||||
const nextRevision = Number.isInteger(nextBody.revision) ? nextBody.revision : revisionFromConflictKey(nextConflictKey);
|
||||
session.latestAggregate = nextAggregate;
|
||||
syncPageAggregateScript(session, nextAggregate);
|
||||
session.pageBodySource = nextPageBodyMeta.pageBodySource;
|
||||
session.projectionSource = nextPageBodyMeta.projectionSource;
|
||||
session.blockProjectionVersion = nextPageBodyMeta.blockProjectionVersion;
|
||||
session.pageBodyLocalCompatFallback = nextPageBodyMeta.localCompatFallback;
|
||||
session.pageBodyHardGuard = nextPageBodyMeta.hardGuard;
|
||||
session.title = nextAggregate?.head?.title || session.title;
|
||||
session.currentTiptapDocument = nextTiptapDocument;
|
||||
session.currentSerialized = nextSerialized;
|
||||
@@ -1284,10 +1368,94 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
|
||||
await refreshSessionFromExternalChange(session, 'mnote-web-local-folder-watch');
|
||||
};
|
||||
|
||||
const handleLocalFolderEventPayloadForSessions = (channel, payload) => {
|
||||
if (!payload) return;
|
||||
localFolderEventItems(payload).forEach((item) => {
|
||||
Array.from(channel.sessions.values()).forEach((targetSession) => {
|
||||
if (!targetSession || targetSession.views.size === 0) return;
|
||||
const documentId = typeof item.documentId === 'string' ? item.documentId.trim() : '';
|
||||
const relativePath = typeof item.relativePath === 'string' ? item.relativePath.trim() : '';
|
||||
if (targetSession.sessionKind === 'resource') {
|
||||
if (!relativePath || relativePath !== resourceWatchPath(targetSession)) return;
|
||||
if (targetSession.saving) {
|
||||
targetSession.externalChangePending = false;
|
||||
targetSession.lastSelfSaveSignalAt = Date.now();
|
||||
return;
|
||||
}
|
||||
targetSession.lastExternalChangeSignalAt = Date.now();
|
||||
targetSession.externalChangePending = true;
|
||||
scheduleSessionExternalRefresh(targetSession, 'mnote-web-local-resource-watch');
|
||||
return;
|
||||
}
|
||||
const eventKind = String(item.eventKind || '');
|
||||
const relativeTarget = sessionRelativePath(targetSession);
|
||||
const targetsCurrentDocument = Boolean(
|
||||
(documentId && documentId === targetSession.documentId)
|
||||
|| (!documentId && relativePath && relativeTarget && relativePath === relativeTarget)
|
||||
);
|
||||
if (!targetsCurrentDocument) return;
|
||||
if (shouldSuppressLocalFolderSelfChange(targetSession.documentId, eventKind)) {
|
||||
targetSession.externalChangePending = false;
|
||||
targetSession.lastSelfSaveSignalAt = Date.now();
|
||||
return;
|
||||
}
|
||||
if (targetSession.saving) {
|
||||
targetSession.externalChangePending = false;
|
||||
targetSession.lastSelfSaveSignalAt = Date.now();
|
||||
return;
|
||||
}
|
||||
targetSession.lastExternalChangeSignalAt = Date.now();
|
||||
targetSession.externalChangePending = true;
|
||||
scheduleSessionExternalRefresh(targetSession, 'mnote-web-local-folder-watch');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const ensureLocalFolderEventChannel = (session) => {
|
||||
if (session.sourceKind !== 'local_folder' || !session.rootUri || typeof window.EventSource !== 'function') {
|
||||
if (session.sourceKind !== 'local_folder' || !session.rootUri) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventBus = window.__mnoteLocalFolderEventBus;
|
||||
if (eventBus && typeof eventBus.startLocalFolderWatcher === 'function') {
|
||||
const channelKey = localFolderEventBusChannelKey(session);
|
||||
let channel = localFolderEventRegistry.get(channelKey);
|
||||
if (!channel) {
|
||||
const rootUri = String(session.rootUri || '').trim();
|
||||
const workspaceId = String(session.workspaceId || '').trim();
|
||||
const handler = (event) => {
|
||||
const detail = event?.detail || {};
|
||||
const eventRootUri = String(detail.rootUri || detail.payload?.rootUri || '').trim();
|
||||
if (eventRootUri && eventRootUri !== rootUri) return;
|
||||
handleLocalFolderEventPayloadForSessions(channel, detail.payload || detail);
|
||||
};
|
||||
const handle = eventBus.startLocalFolderWatcher({
|
||||
rootUri,
|
||||
workspaceId,
|
||||
bootstrap: {
|
||||
schema: 'mnote.document_session.local_folder_event_bus.v1',
|
||||
transport: 'local-folder-events',
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
channel = {
|
||||
key: channelKey,
|
||||
rootUri,
|
||||
documentId: '',
|
||||
resourcePath: '',
|
||||
eventSource: handle,
|
||||
sessions: new Map(),
|
||||
unsubscribe: () => window.removeEventListener('mnote:local-folder:document-changed', handler),
|
||||
};
|
||||
window.addEventListener('mnote:local-folder:document-changed', handler);
|
||||
localFolderEventRegistry.set(channelKey, channel);
|
||||
}
|
||||
channel.sessions.set(session.key, session);
|
||||
session.localFolderChannel = channel;
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof window.EventSource !== 'function') return;
|
||||
const channelKey = localFolderEventChannelKey(session);
|
||||
let channel = localFolderEventRegistry.get(channelKey);
|
||||
if (!channel) {
|
||||
@@ -1308,42 +1476,7 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
|
||||
sessions: new Map(),
|
||||
};
|
||||
eventSource.addEventListener('change', (event) => {
|
||||
const payload = parseLocalFolderEventPayload(event);
|
||||
if (!payload) return;
|
||||
Array.from(channel.sessions.values()).forEach((targetSession) => {
|
||||
if (!targetSession || targetSession.views.size === 0) return;
|
||||
const documentId = typeof payload.documentId === 'string' ? payload.documentId.trim() : '';
|
||||
const relativePath = typeof payload.relativePath === 'string' ? payload.relativePath.trim() : '';
|
||||
if (targetSession.sessionKind === 'resource') {
|
||||
if (!relativePath || relativePath !== resourceWatchPath(targetSession)) return;
|
||||
if (targetSession.saving) {
|
||||
targetSession.externalChangePending = false;
|
||||
targetSession.lastSelfSaveSignalAt = Date.now();
|
||||
return;
|
||||
}
|
||||
targetSession.lastExternalChangeSignalAt = Date.now();
|
||||
targetSession.externalChangePending = true;
|
||||
scheduleSessionExternalRefresh(targetSession, 'mnote-web-local-resource-watch');
|
||||
return;
|
||||
}
|
||||
if (!documentId) return;
|
||||
const eventKind = String(payload.eventKind || '');
|
||||
const targetsCurrentDocument = Boolean(documentId && documentId === targetSession.documentId);
|
||||
if (documentId && !targetsCurrentDocument) return;
|
||||
if (targetsCurrentDocument && shouldSuppressLocalFolderSelfChange(documentId, eventKind)) {
|
||||
targetSession.externalChangePending = false;
|
||||
targetSession.lastSelfSaveSignalAt = Date.now();
|
||||
return;
|
||||
}
|
||||
if (targetsCurrentDocument && targetSession.saving) {
|
||||
targetSession.externalChangePending = false;
|
||||
targetSession.lastSelfSaveSignalAt = Date.now();
|
||||
return;
|
||||
}
|
||||
targetSession.lastExternalChangeSignalAt = Date.now();
|
||||
targetSession.externalChangePending = true;
|
||||
scheduleSessionExternalRefresh(targetSession, 'mnote-web-local-folder-watch');
|
||||
});
|
||||
handleLocalFolderEventPayloadForSessions(channel, parseLocalFolderEventPayload(event));
|
||||
});
|
||||
eventSource.onerror = () => {
|
||||
console.warn('mnote local folder 外部更新事件流中断,将等待浏览器自动重连');
|
||||
@@ -1609,7 +1742,7 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
|
||||
const keyRevision = revisionFromConflictKey(conflictDetectionKey);
|
||||
const sourceKind = normalizeSessionSourceKind(runtimeDescriptor.bootstrap);
|
||||
const tiptapDocument = pageBodyTiptapDocument(pageBody, '', runtimeDescriptor.bootstrap);
|
||||
const pageBodySource = pageBodyTiptapDocumentSource(pageBody, '');
|
||||
const pageBodyMeta = pageBodySourceMeta(sourceKind, pageBody);
|
||||
const session = {
|
||||
key: buildDocumentSessionKey(runtimeDescriptor.bootstrap),
|
||||
documentId: runtimeDescriptor.bootstrap.documentId,
|
||||
@@ -1620,9 +1753,11 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
|
||||
pageAggregateScriptId: runtimeDescriptor.bootstrap.pageAggregateScriptId || '__MNOTE_PAGE_AGGREGATE__',
|
||||
latestAggregate: runtimeDescriptor.aggregate,
|
||||
title: runtimeDescriptor.aggregate.head?.title || '无标题',
|
||||
pageBodySource,
|
||||
projectionSource: String(pageBody.projectionSource || pageBody.projection_source || '').trim(),
|
||||
blockProjectionVersion: String(pageBody.blockProjectionVersion || pageBody.block_projection_version || '').trim(),
|
||||
pageBodySource: pageBodyMeta.pageBodySource,
|
||||
projectionSource: pageBodyMeta.projectionSource,
|
||||
blockProjectionVersion: pageBodyMeta.blockProjectionVersion,
|
||||
pageBodyLocalCompatFallback: pageBodyMeta.localCompatFallback,
|
||||
pageBodyHardGuard: pageBodyMeta.hardGuard,
|
||||
currentTiptapDocument: tiptapDocument,
|
||||
currentSerialized: JSON.stringify(tiptapDocument),
|
||||
lastPersistedSerialized: JSON.stringify(tiptapDocument),
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
(function(){
|
||||
'use strict';
|
||||
|
||||
if (window.__mnoteLocalFolderEventBus) {
|
||||
return;
|
||||
}
|
||||
|
||||
var connections = new Map();
|
||||
var sidebarRefreshQueues = new Map();
|
||||
var lastSource = '';
|
||||
var lastReason = '';
|
||||
|
||||
function root() {
|
||||
return document.documentElement;
|
||||
}
|
||||
|
||||
function setDiagnostics(source, reason) {
|
||||
lastSource = source || lastSource || '';
|
||||
lastReason = reason || lastReason || '';
|
||||
root().setAttribute('data-mnote-local-folder-event-bus', 'ready');
|
||||
root().setAttribute('data-mnote-local-folder-event-bus-connections', String(connections.size));
|
||||
root().setAttribute('data-mnote-local-folder-event-bus-last-source', lastSource);
|
||||
root().setAttribute('data-mnote-local-folder-event-bus-last-reason', lastReason);
|
||||
}
|
||||
|
||||
function normalizeRootUri(rootUri) {
|
||||
return String(rootUri || '').trim();
|
||||
}
|
||||
|
||||
function normalizeWorkspaceId(workspaceId) {
|
||||
return String(workspaceId || 'default').trim() || 'default';
|
||||
}
|
||||
|
||||
function emit(name, detail) {
|
||||
window.dispatchEvent(new CustomEvent(name, { detail: detail || {} }));
|
||||
}
|
||||
|
||||
function parseEventPayload(event) {
|
||||
try {
|
||||
return JSON.parse((event && event.data) || '{}') || {};
|
||||
} catch (_) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function revisionOf(payload, event) {
|
||||
return payload.revision || payload.cursor || (event && event.lastEventId) || null;
|
||||
}
|
||||
|
||||
function arrayOf(value) {
|
||||
if (!value) return [];
|
||||
if (Array.isArray(value)) return value.filter(Boolean).map(String);
|
||||
return [String(value)].filter(Boolean);
|
||||
}
|
||||
|
||||
function pathArrayOf(value) {
|
||||
if (!value) return [];
|
||||
var list = Array.isArray(value) ? value : [value];
|
||||
return list.map(function(item) {
|
||||
if (typeof item === 'string') return item.trim();
|
||||
if (!item || typeof item !== 'object') return '';
|
||||
return String(item.relativePath || item.relative_path || item.path || item.sourcePath || item.source_path || '').trim();
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
function collectChangedPaths(payload) {
|
||||
var paths = []
|
||||
.concat(pathArrayOf(payload.changedPaths))
|
||||
.concat(pathArrayOf(payload.changed_paths))
|
||||
.concat(pathArrayOf(payload.paths));
|
||||
var events = Array.isArray(payload.events) ? payload.events : [];
|
||||
events.forEach(function(item) {
|
||||
if (!item || typeof item !== 'object') return;
|
||||
paths = paths
|
||||
.concat(arrayOf(item.path))
|
||||
.concat(arrayOf(item.relativePath))
|
||||
.concat(arrayOf(item.relative_path))
|
||||
.concat(arrayOf(item.sourcePath))
|
||||
.concat(arrayOf(item.source_path));
|
||||
});
|
||||
return Array.from(new Set(paths));
|
||||
}
|
||||
|
||||
function collectChangedPathItems(payload, reason) {
|
||||
var raw = []
|
||||
.concat(Array.isArray(payload.changedPaths) ? payload.changedPaths : [])
|
||||
.concat(Array.isArray(payload.changed_paths) ? payload.changed_paths : [])
|
||||
.concat(Array.isArray(payload.paths) ? payload.paths : [])
|
||||
.concat(Array.isArray(payload.events) ? payload.events : []);
|
||||
var seen = new Set();
|
||||
var items = [];
|
||||
raw.forEach(function(item) {
|
||||
var relativePath = typeof item === 'string'
|
||||
? item.trim()
|
||||
: String(item && (item.relativePath || item.relative_path || item.path || item.sourcePath || item.source_path || '') || '').trim();
|
||||
if (!relativePath || seen.has(relativePath)) return;
|
||||
seen.add(relativePath);
|
||||
items.push({
|
||||
relativePath: relativePath,
|
||||
reason: reason || 'event-bus',
|
||||
kind: typeof item === 'object' && item ? String(item.kind || '') : '',
|
||||
eventKind: typeof item === 'object' && item ? String(item.eventKind || item.event_kind || item.changeType || item.change_type || '') : ''
|
||||
});
|
||||
});
|
||||
if (items.length) return items;
|
||||
return pathItems(collectChangedPaths(payload), reason);
|
||||
}
|
||||
|
||||
function collectAffectedParents(payload) {
|
||||
var parents = []
|
||||
.concat(pathArrayOf(payload.affectedParents))
|
||||
.concat(pathArrayOf(payload.affected_parents))
|
||||
.concat(pathArrayOf(payload.parentRelativePaths))
|
||||
.concat(pathArrayOf(payload.parent_relative_paths));
|
||||
return Array.from(new Set(parents));
|
||||
}
|
||||
|
||||
function pathItems(paths, reason) {
|
||||
return (paths || []).map(function(relativePath) {
|
||||
return {
|
||||
relativePath: String(relativePath || '').trim(),
|
||||
reason: reason || 'event-bus'
|
||||
};
|
||||
}).filter(function(item) { return item.relativePath || item.relativePath === ''; });
|
||||
}
|
||||
|
||||
function queueSidebarRefresh(detail) {
|
||||
var key = String(detail.rootUri || '').trim();
|
||||
if (!key) return;
|
||||
var queue = sidebarRefreshQueues.get(key);
|
||||
if (!queue) {
|
||||
queue = {
|
||||
rootUri: detail.rootUri,
|
||||
workspaceId: detail.workspaceId,
|
||||
revisions: new Set(),
|
||||
reasons: new Set(),
|
||||
changedPaths: new Map(),
|
||||
affectedParents: new Set(),
|
||||
resyncRequired: false,
|
||||
timer: 0
|
||||
};
|
||||
sidebarRefreshQueues.set(key, queue);
|
||||
}
|
||||
if (detail.revision) queue.revisions.add(String(detail.revision));
|
||||
if (detail.reason) queue.reasons.add(String(detail.reason));
|
||||
(detail.changedPaths || []).forEach(function(item) {
|
||||
var path = typeof item === 'string' ? item : String(item && (item.relativePath || item.relative_path || '') || '');
|
||||
if (!path && path !== '') return;
|
||||
var existing = queue.changedPaths.get(path) || { relativePath: path };
|
||||
queue.changedPaths.set(path, {
|
||||
relativePath: path,
|
||||
reason: String((item && item.reason) || existing.reason || detail.reason || 'event-bus-orchestrated'),
|
||||
kind: String((item && item.kind) || existing.kind || ''),
|
||||
eventKind: String((item && item.eventKind) || (item && item.event_kind) || existing.eventKind || '')
|
||||
});
|
||||
});
|
||||
(detail.affectedParents || []).forEach(function(item) {
|
||||
var parent = typeof item === 'string' ? item : String(item && (item.relativePath || item.relative_path || '') || '');
|
||||
if (parent || parent === '') queue.affectedParents.add(parent);
|
||||
});
|
||||
queue.resyncRequired = queue.resyncRequired || detail.resyncRequired === true;
|
||||
if (!queue.timer) {
|
||||
queue.timer = window.setTimeout(function() {
|
||||
flushSidebarRefresh(key);
|
||||
}, 0);
|
||||
}
|
||||
root().setAttribute('data-mnote-local-folder-event-bus-sidebar-refresh-pending', String(queue.affectedParents.size));
|
||||
}
|
||||
|
||||
function flushSidebarRefresh(key) {
|
||||
var queue = sidebarRefreshQueues.get(key);
|
||||
if (!queue) return;
|
||||
sidebarRefreshQueues.delete(key);
|
||||
var reasons = Array.from(queue.reasons);
|
||||
var detail = {
|
||||
schema: 'mnote.local_folder.event_bus.sidebar_refresh.v1',
|
||||
source: 'event_bus_orchestrator',
|
||||
reason: reasons.join(',') || 'watch_batch',
|
||||
rootUri: queue.rootUri,
|
||||
workspaceId: queue.workspaceId,
|
||||
revision: Array.from(queue.revisions).pop() || null,
|
||||
reasons: reasons,
|
||||
changedPaths: Array.from(queue.changedPaths.values()),
|
||||
affectedParents: pathItems(Array.from(queue.affectedParents), 'event-bus-orchestrated'),
|
||||
resyncRequired: queue.resyncRequired,
|
||||
viaEventBus: true
|
||||
};
|
||||
root().setAttribute('data-mnote-local-folder-event-bus-sidebar-refresh-applied', detail.reason);
|
||||
root().setAttribute('data-mnote-local-folder-event-bus-sidebar-refresh-parents', String(detail.affectedParents.length));
|
||||
emit('mnote:local-folder:sidebar-refresh-requested', detail);
|
||||
}
|
||||
|
||||
function dispatchWatchBatch(entry, event, payload, meta) {
|
||||
var revision = revisionOf(payload, event);
|
||||
var changedPaths = collectChangedPaths(payload);
|
||||
var affectedParents = collectAffectedParents(payload);
|
||||
var source = String((meta && meta.source) || payload.source || 'watcher_sse').trim() || 'watcher_sse';
|
||||
var reason = String((meta && meta.reason) || payload.reason || 'watch_batch').trim() || 'watch_batch';
|
||||
var resyncRequired = payload.fallbackResync === true
|
||||
|| payload.requiresResync === true
|
||||
|| payload.resyncRequired === true
|
||||
|| payload.resync_required === true;
|
||||
var detail = {
|
||||
schema: 'mnote.local_folder.event_bus.watch_batch.v1',
|
||||
source: source,
|
||||
reason: reason,
|
||||
rootUri: entry.rootUri,
|
||||
workspaceId: entry.workspaceId,
|
||||
revision: revision,
|
||||
payload: payload,
|
||||
bootstrap: entry.bootstrap || null,
|
||||
changedPaths: collectChangedPathItems(payload, reason),
|
||||
affectedParents: pathItems(affectedParents, reason),
|
||||
resyncRequired: resyncRequired,
|
||||
viaEventBus: true
|
||||
};
|
||||
|
||||
setDiagnostics(source, reason);
|
||||
root().setAttribute('data-mnote-tree-live-revision', String(revision || ''));
|
||||
emit('mnote:local-folder:watch-batch', detail);
|
||||
emit('tree:local-folder-watch-batch', detail);
|
||||
|
||||
if (affectedParents.length > 0) {
|
||||
emit('mnote:local-folder:filetree-parent-changed', detail);
|
||||
}
|
||||
if (changedPaths.length > 0) {
|
||||
emit('mnote:local-folder:document-changed', detail);
|
||||
emit('mnote:local-folder:resource-changed', detail);
|
||||
emit('mnote:local-folder:knowledge-rag-source-updated', detail);
|
||||
}
|
||||
queueSidebarRefresh(detail);
|
||||
if (resyncRequired) {
|
||||
emit('mnote:local-folder:resync-required', detail);
|
||||
}
|
||||
}
|
||||
|
||||
function closeEntry(entry) {
|
||||
if (!entry || !entry.source || typeof entry.source.close !== 'function') return;
|
||||
entry.source.close();
|
||||
}
|
||||
|
||||
function startLocalFolderWatcher(options) {
|
||||
var rootUri = normalizeRootUri(options && options.rootUri);
|
||||
if (!rootUri || typeof window.EventSource !== 'function') return null;
|
||||
var workspaceId = normalizeWorkspaceId(options && options.workspaceId);
|
||||
var key = rootUri;
|
||||
if (connections.has(key)) {
|
||||
var existing = connections.get(key);
|
||||
if ((!existing.workspaceId || existing.workspaceId === 'default') && workspaceId && workspaceId !== 'default') {
|
||||
existing.workspaceId = workspaceId;
|
||||
if (existing.handle) existing.handle.workspaceId = workspaceId;
|
||||
}
|
||||
setDiagnostics(existing.lastSource || 'watcher_sse', 'reuse_connection');
|
||||
return existing.handle;
|
||||
}
|
||||
|
||||
var url = new URL('/api/local-folder/events', window.location.origin);
|
||||
url.searchParams.set('rootUri', rootUri);
|
||||
url.searchParams.set('treeLive', 'true');
|
||||
|
||||
var eventSource = new EventSource(url.toString());
|
||||
var entry = {
|
||||
key: key,
|
||||
rootUri: rootUri,
|
||||
workspaceId: workspaceId,
|
||||
bootstrap: (options && options.bootstrap) || null,
|
||||
source: eventSource,
|
||||
lastSource: 'watcher_sse',
|
||||
close: function() {
|
||||
connections.delete(key);
|
||||
closeEntry(entry);
|
||||
setDiagnostics('watcher_sse', 'closed');
|
||||
}
|
||||
};
|
||||
entry.handle = {
|
||||
key: key,
|
||||
rootUri: rootUri,
|
||||
workspaceId: workspaceId,
|
||||
source: eventSource,
|
||||
close: entry.close
|
||||
};
|
||||
connections.set(key, entry);
|
||||
setDiagnostics('watcher_sse', 'connect');
|
||||
emit('mnote:local-folder:event-bus-ready', {
|
||||
schema: 'mnote.local_folder.event_bus.ready.v1',
|
||||
source: 'watcher_sse',
|
||||
reason: 'connect',
|
||||
rootUri: rootUri,
|
||||
workspaceId: workspaceId,
|
||||
connections: connections.size
|
||||
});
|
||||
|
||||
eventSource.addEventListener('open', function() {
|
||||
setDiagnostics('watcher_sse', 'open');
|
||||
});
|
||||
eventSource.addEventListener('watch_batch', function(event) {
|
||||
dispatchWatchBatch(entry, event, parseEventPayload(event), {
|
||||
source: 'watcher_sse',
|
||||
reason: 'watch_batch'
|
||||
});
|
||||
});
|
||||
eventSource.addEventListener('tree_error', function(event) {
|
||||
var payload = parseEventPayload(event);
|
||||
setDiagnostics('watcher_sse', 'tree_error');
|
||||
emit('tree:error', { payload: payload, bootstrap: entry.bootstrap || null, viaEventBus: true });
|
||||
});
|
||||
eventSource.onerror = function() {
|
||||
setDiagnostics('watcher_sse', 'error');
|
||||
};
|
||||
|
||||
return entry.handle;
|
||||
}
|
||||
|
||||
function emitSyntheticWatchBatch(detail) {
|
||||
var payload = detail && detail.payload ? detail.payload : (detail || {});
|
||||
var rootUri = normalizeRootUri(detail && detail.rootUri);
|
||||
var workspaceId = normalizeWorkspaceId(detail && detail.workspaceId);
|
||||
var entry = {
|
||||
rootUri: rootUri,
|
||||
workspaceId: workspaceId,
|
||||
bootstrap: (detail && detail.bootstrap) || null
|
||||
};
|
||||
dispatchWatchBatch(entry, { lastEventId: detail && detail.revision }, payload, {
|
||||
source: (detail && detail.source) || 'synthetic',
|
||||
reason: (detail && detail.reason) || payload.source || 'synthetic_watch_batch'
|
||||
});
|
||||
}
|
||||
|
||||
function closeAll() {
|
||||
Array.from(connections.values()).forEach(function(entry) {
|
||||
closeEntry(entry);
|
||||
});
|
||||
connections.clear();
|
||||
setDiagnostics('watcher_sse', 'closed');
|
||||
}
|
||||
|
||||
window.__mnoteLocalFolderEventBus = {
|
||||
startLocalFolderWatcher: startLocalFolderWatcher,
|
||||
emitSyntheticWatchBatch: emitSyntheticWatchBatch,
|
||||
closeAll: closeAll,
|
||||
flushSidebarRefresh: function(rootUri) {
|
||||
flushSidebarRefresh(normalizeRootUri(rootUri));
|
||||
},
|
||||
connectionCount: function() { return connections.size; },
|
||||
diagnostics: function() {
|
||||
return {
|
||||
connections: connections.size,
|
||||
lastSource: lastSource,
|
||||
lastReason: lastReason
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
setDiagnostics('', 'ready');
|
||||
})();
|
||||
@@ -413,23 +413,8 @@ async function uploadLocalFolderAsset(file, plan, context) {
|
||||
}
|
||||
|
||||
async function uploadMediaAsset(file, plan, context) {
|
||||
var form = new FormData();
|
||||
form.append('file', file);
|
||||
form.append('workspaceId', plan && plan.workspaceId || '');
|
||||
form.append('documentId', plan && plan.targetDocumentId || '');
|
||||
if (plan && plan.targetMindmapId) form.append('mindmapId', plan.targetMindmapId);
|
||||
var response = await fetchWithTimeout('/api/media/upload', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: form
|
||||
}, Number(context && context.timeoutMs) || 15000, '上传');
|
||||
var payload = await response.json().catch(function() { return null; });
|
||||
if (!response.ok || !payload || !payload.asset) {
|
||||
throw new Error(payload && payload.error ? payload.error : '上传失败');
|
||||
}
|
||||
return {
|
||||
asset: payload.asset
|
||||
};
|
||||
document.documentElement.setAttribute('data-mnote-media-upload-retired', 'true');
|
||||
throw new Error('旧 Convex Files 上传链已退役;local-first 附件请使用本地文件夹上传目标。');
|
||||
}
|
||||
|
||||
async function insertUploadedAssetIntoEditor(asset, targetRoot, deps) {
|
||||
|
||||
@@ -749,23 +749,7 @@ export const createSidebarAttachmentOpenRuntime = (dependencies = {}) => {
|
||||
};
|
||||
}
|
||||
}
|
||||
if (assetId) {
|
||||
var response = await fetch('/api/media/sign?assetId=' + encodeURIComponent(assetId), {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
cache: 'no-store'
|
||||
});
|
||||
var payload = await response.json().catch(function() { return null; });
|
||||
if (!response.ok || !payload) {
|
||||
throw new Error(payload && payload.error ? payload.error : '生成签名链接失败');
|
||||
}
|
||||
var signedUrl = String(payload && payload.signedUrl || '').trim();
|
||||
if (!signedUrl) throw new Error('附件链接不可用');
|
||||
return {
|
||||
url: signedUrl,
|
||||
asset: payload.asset && typeof payload.asset === 'object' ? payload.asset : {}
|
||||
};
|
||||
}
|
||||
if (assetId) document.documentElement.setAttribute('data-mnote-media-sign-retired', 'true');
|
||||
var url = String(detail && (detail.fileUrl || detail.href) || '').trim();
|
||||
if (!url) throw new Error('附件链接不可用');
|
||||
return { url: url, asset: {} };
|
||||
@@ -855,20 +839,7 @@ export const createSidebarAttachmentOpenRuntime = (dependencies = {}) => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (detail.assetId) {
|
||||
try {
|
||||
var response = await fetch('/api/media/sign?assetId=' + encodeURIComponent(detail.assetId), {
|
||||
method: 'GET',
|
||||
credentials: 'include'
|
||||
});
|
||||
var payload = await response.json().catch(function() { return null; });
|
||||
var signedUrl = String(payload && payload.signedUrl || '').trim();
|
||||
if (response.ok && signedUrl) {
|
||||
window.open(signedUrl, '_blank', 'noopener,noreferrer');
|
||||
return;
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
if (detail.assetId) document.documentElement.setAttribute('data-mnote-media-sign-retired', 'true');
|
||||
var localRelativePath = String(detail.localRelativePath || '').trim();
|
||||
if (localRelativePath) {
|
||||
var localPathDownloadUrl = buildLocalFileOpenUrl(localRelativePath, true);
|
||||
|
||||
@@ -380,11 +380,7 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
|
||||
callback();
|
||||
}
|
||||
|
||||
function isLocalOcrSourceFileName(fileName) {
|
||||
return /\.(png|jpe?g|webp|gif|bmp|tiff?|pdf)$/i.test(String(fileName || '').trim());
|
||||
}
|
||||
|
||||
function localOcrSourceRelativePath(detail) {
|
||||
function knowledgeRagSourceRelativePath(detail) {
|
||||
var workspacePath = detail && detail.workspacePath && typeof detail.workspacePath === 'object' ? detail.workspacePath : null;
|
||||
var relativePath = String(
|
||||
detail && detail.localRelativePath
|
||||
@@ -395,7 +391,7 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
|
||||
return relativePath.replace(/^\/+/, '');
|
||||
}
|
||||
|
||||
function localOcrRootUri(detail, trigger) {
|
||||
function knowledgeRagRootUri(detail, trigger) {
|
||||
var workspacePath = detail && detail.workspacePath && typeof detail.workspacePath === 'object' ? detail.workspacePath : null;
|
||||
var rootUri = String(
|
||||
detail && (detail.localRootUri || detail.rootUri)
|
||||
@@ -409,39 +405,15 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
|
||||
return rootUri || currentRootUri() || '';
|
||||
}
|
||||
|
||||
function supportsLocalOcr(detail) {
|
||||
var path = localOcrSourceRelativePath(detail);
|
||||
var title = String(detail && (detail.title || detail.fileName) || '').trim() || path.split('/').pop() || '';
|
||||
return Boolean(path && localOcrRootUri(detail, null) && isLocalOcrSourceFileName(title || path));
|
||||
}
|
||||
|
||||
function localOcrProvider() {
|
||||
var override = String(window.__MNOTE_LOCAL_OCR_PROVIDER || '').trim().toLowerCase();
|
||||
return override === 'mock' ? 'mock' : 'mineru';
|
||||
}
|
||||
|
||||
async function runLocalOcrForDetail(detail, trigger) {
|
||||
document.documentElement.setAttribute('data-mnote-local-ocr-menu-status', 'running');
|
||||
var payload = await ingestKnowledgeRagForDetail(detail, trigger).catch(function(error) {
|
||||
document.documentElement.setAttribute('data-mnote-local-ocr-menu-status', 'failed');
|
||||
throw error;
|
||||
});
|
||||
var sourceRootRelativePath = localOcrSourceRelativePath(detail)
|
||||
|| String(detail && (detail.localRelativePath || detail.path || detail.title) || '').trim();
|
||||
var rootUri = localOcrRootUri(detail, trigger);
|
||||
var status = payload && payload.retryRequired ? 'retry' : 'done';
|
||||
document.documentElement.setAttribute('data-mnote-local-ocr-menu-status', status);
|
||||
document.documentElement.setAttribute('data-mnote-local-ocr-menu-path', sourceRootRelativePath);
|
||||
window.dispatchEvent(new CustomEvent('mnote:local-ocr-job-updated', {
|
||||
detail: { status: status, job: payload, rootUri: rootUri, sourceRootRelativePath: sourceRootRelativePath }
|
||||
}));
|
||||
return payload;
|
||||
function supportsKnowledgeRagSource(detail) {
|
||||
var path = knowledgeRagSourceRelativePath(detail);
|
||||
return Boolean(path && knowledgeRagRootUri(detail, null));
|
||||
}
|
||||
|
||||
async function ingestKnowledgeRagForDetail(detail, trigger) {
|
||||
var sourceRootRelativePath = localOcrSourceRelativePath(detail)
|
||||
var sourceRootRelativePath = knowledgeRagSourceRelativePath(detail)
|
||||
|| String(detail && (detail.localRelativePath || detail.path || detail.title) || '').trim();
|
||||
var rootUri = localOcrRootUri(detail, trigger);
|
||||
var rootUri = knowledgeRagRootUri(detail, trigger);
|
||||
if (!sourceRootRelativePath || !rootUri) {
|
||||
throw new Error('缺少资料库来源或 rootUri');
|
||||
}
|
||||
@@ -473,17 +445,6 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
|
||||
function handleTreeContextMenuAction(action, detail, trigger) {
|
||||
closeTreeContextMenu();
|
||||
detail = detail || {};
|
||||
if (action === 'local-ocr') {
|
||||
recordFileTreeAction('knowledge-rag-index', detail);
|
||||
recordFileTreeActionStatus('pending', detail);
|
||||
void runLocalOcrForDetail(detail, trigger).then(function(job) {
|
||||
recordFileTreeActionStatus(job && job.retryRequired ? 'retry' : 'done', detail);
|
||||
}).catch(function(error) {
|
||||
recordFileTreeActionStatus('failed', Object.assign({}, detail, { fallback: 'alert' }));
|
||||
window.alert(error && error.message ? error.message : '资料库索引失败');
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (action === 'knowledge-rag-index') {
|
||||
recordFileTreeAction('knowledge-rag-index', detail);
|
||||
recordFileTreeActionStatus('pending', detail);
|
||||
@@ -954,7 +915,7 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
|
||||
menu.setAttribute('data-command-context-target-kind', String(ctx['tree.targetResourceKind'] || ''));
|
||||
menu.setAttribute('data-command-context-editor-dirty', String(ctx['editor.dirty'] === true));
|
||||
menu.setAttribute('data-command-context-ai-can-write', String(ctx['ai.canWrite'] === true));
|
||||
var localOcrSupported = supportsLocalOcr(detail);
|
||||
var knowledgeRagSupported = supportsKnowledgeRagSource(detail);
|
||||
var items = isAttachment ? [
|
||||
{ action: 'duplicate', icon: 'file_copy', label: '拷贝副本', shortcut: 'Ctrl + D' },
|
||||
{ action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true, destructive: true, requiresApproval: true, when: '!workspace.readonly' },
|
||||
@@ -1017,7 +978,7 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
|
||||
{ separator: true },
|
||||
{ action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true, destructive: true, requiresApproval: true, when: '!workspace.readonly && !editor.dirty' }
|
||||
];
|
||||
if (currentSourceKind() === 'local_folder' && detail.rowKind !== 'index') {
|
||||
if (currentSourceKind() === 'local_folder' && detail.rowKind !== 'index' && knowledgeRagSupported) {
|
||||
var ragItem = { action: 'knowledge-rag-index', icon: 'travel_explore', label: '加入资料库索引', when: '!workspace.readonly' };
|
||||
var insertAt = isAttachment ? 11 : isAsset ? 3 : 3;
|
||||
if (insertAt >= 0) items.splice(insertAt, 0, ragItem);
|
||||
|
||||
@@ -479,92 +479,8 @@ export const createSidebarFileTreeOpenRuntime = (dependencies = {}) => {
|
||||
navigateToMindmapObject(documentId, assetId, String(detail.workspaceId || '').trim());
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var response = await fetch('/api/media/sign?assetId=' + encodeURIComponent(assetId), {
|
||||
method: 'GET',
|
||||
credentials: 'include'
|
||||
});
|
||||
var payload = await response.json().catch(function() { return null; });
|
||||
if (!response.ok) {
|
||||
throw new Error(payload && payload.error ? payload.error : '生成签名链接失败');
|
||||
}
|
||||
var asset = payload && payload.asset && typeof payload.asset === 'object' ? payload.asset : {};
|
||||
var fileUrl = String(payload && payload.signedUrl || asset.file_url || asset.signed_url || '').trim();
|
||||
if (!fileUrl) throw new Error('附件链接不可用');
|
||||
var fileName = String(asset.file_name || detail.fileName || '未命名资源').trim() || '未命名资源';
|
||||
var fileType = inferOnlyOfficeFileType(fileName, asset.mime_type);
|
||||
if (fileType) {
|
||||
var userId = await fetchCurrentOnlyOfficeUserId();
|
||||
var officeUrl = buildOnlyOfficeOpenUrl({
|
||||
fileUrl: fileUrl,
|
||||
fileName: fileName,
|
||||
fileType: fileType,
|
||||
assetId: assetId,
|
||||
documentId: String(asset.document_id || detail.documentId || '').trim(),
|
||||
userId: userId,
|
||||
mode: forceEditMode ? 'edit' : 'view'
|
||||
});
|
||||
if (typeof window.__mnoteDocumentPaneRuntime?.openResourceInActiveTab === 'function') {
|
||||
var didOpen = await window.__mnoteDocumentPaneRuntime.openResourceInActiveTab({
|
||||
objectIdentity: resourceObjectIdentityFromWorkspacePath({
|
||||
workspacePath: detailWorkspacePath,
|
||||
objectKind: 'only_office',
|
||||
documentId: String(asset.document_id || detail.documentId || '').trim(),
|
||||
assetId: assetId
|
||||
}),
|
||||
assetId: assetId,
|
||||
title: fileName,
|
||||
fileName: fileName,
|
||||
kind: 'office',
|
||||
officeUrl: officeUrl,
|
||||
documentId: String(asset.document_id || detail.documentId || '').trim(),
|
||||
workspaceId: String(detail.workspaceId || '').trim(),
|
||||
workspacePath: detailWorkspacePath
|
||||
});
|
||||
if (didOpen) return;
|
||||
}
|
||||
window.open(officeUrl, '_blank', 'noopener,noreferrer');
|
||||
return;
|
||||
}
|
||||
if (isPdfAttachmentFileName(fileName)) {
|
||||
var pdfPreviewUrl = buildPdfPreviewOpenUrl(fileUrl, fileName);
|
||||
if (typeof window.__mnoteDocumentPaneRuntime?.openResourceInActiveTab === 'function') {
|
||||
var didOpenPdf = await window.__mnoteDocumentPaneRuntime.openResourceInActiveTab({
|
||||
objectIdentity: resourceObjectIdentityFromWorkspacePath({
|
||||
workspacePath: detailWorkspacePath,
|
||||
objectKind: 'pdf',
|
||||
documentId: String(asset.document_id || detail.documentId || '').trim(),
|
||||
assetId: assetId
|
||||
}),
|
||||
assetId: assetId,
|
||||
title: fileName,
|
||||
fileName: fileName,
|
||||
kind: 'pdf',
|
||||
href: pdfPreviewUrl,
|
||||
documentId: String(asset.document_id || detail.documentId || '').trim(),
|
||||
workspaceId: String(detail.workspaceId || '').trim(),
|
||||
workspacePath: detailWorkspacePath
|
||||
});
|
||||
if (didOpenPdf) return;
|
||||
}
|
||||
window.open(pdfPreviewUrl || fileUrl, '_blank', 'noopener,noreferrer');
|
||||
return;
|
||||
}
|
||||
if (isCodeAttachmentFileName(fileName) && String(asset.document_id || detail.documentId || '').trim() === currentDocumentId()) {
|
||||
await openCodeEditorAttachment({
|
||||
href: fileUrl,
|
||||
fileUrl: fileUrl,
|
||||
fileName: fileName,
|
||||
assetId: assetId,
|
||||
documentId: String(asset.document_id || detail.documentId || '').trim(),
|
||||
fileSize: uploadedFileSize(asset)
|
||||
});
|
||||
return;
|
||||
}
|
||||
window.open(fileUrl, '_blank', 'noopener,noreferrer');
|
||||
} catch (error) {
|
||||
window.alert(error && error.message ? error.message : '打开附件失败');
|
||||
}
|
||||
document.documentElement.setAttribute('data-mnote-media-sign-retired', 'true');
|
||||
window.alert('旧 Convex Files 附件签名链已退役;local-first 附件请通过本地文件夹资源打开。');
|
||||
}
|
||||
|
||||
window.addEventListener('tree.asset.open', function(event) {
|
||||
|
||||
@@ -469,7 +469,6 @@ export function createSidebarPageAiRuntime(context) {
|
||||
const pageAiWorkspacePathForTarget = (...args) => pageAiTargetRuntime.pageAiWorkspacePathForTarget(...args);
|
||||
const pageAiBuildAllowedRoots = (...args) => pageAiTargetRuntime.pageAiBuildAllowedRoots(...args);
|
||||
const pageAiBuildContextRefs = (...args) => pageAiTargetRuntime.pageAiBuildContextRefs(...args);
|
||||
const pageAiEnrichOcrContextRefs = (...args) => pageAiTargetRuntime.pageAiEnrichOcrContextRefs(...args);
|
||||
const pageAiBuildRunTargetSnapshot = (...args) => pageAiTargetRuntime.pageAiBuildRunTargetSnapshot(...args);
|
||||
const pageAiPageContextForRefs = (...args) => pageAiTargetRuntime.pageAiPageContextForRefs(...args);
|
||||
const pageAiSetRunTargetSnapshot = (...args) => pageAiTargetRuntime.pageAiSetRunTargetSnapshot(...args);
|
||||
@@ -1085,19 +1084,29 @@ export function createSidebarPageAiRuntime(context) {
|
||||
return index === list.findIndex(function(candidate) { return candidate.relativePath === item.relativePath; });
|
||||
});
|
||||
if (changedPaths.length) {
|
||||
var rootUri = String((receipt && receipt.rootUri) || (fallbackAudit && fallbackAudit.rootUri) || currentRootUri() || '');
|
||||
var syntheticPayload = {
|
||||
schema: 'mnote.local_folder.watch_batch.v1',
|
||||
source: 'agent_run_receipt',
|
||||
runId: runId,
|
||||
rootUri: rootUri,
|
||||
changedPaths: changedPaths,
|
||||
affectedParents: affectedParents
|
||||
};
|
||||
document.documentElement.setAttribute('data-mnote-page-ai-receipt-filetree-refresh', 'true');
|
||||
window.dispatchEvent(new CustomEvent('tree:local-folder-watch-batch', {
|
||||
detail: {
|
||||
payload: {
|
||||
schema: 'mnote.local_folder.watch_batch.v1',
|
||||
source: 'agent_run_receipt',
|
||||
runId: runId,
|
||||
rootUri: String((receipt && receipt.rootUri) || (fallbackAudit && fallbackAudit.rootUri) || currentRootUri() || ''),
|
||||
changedPaths: changedPaths,
|
||||
affectedParents: affectedParents
|
||||
}
|
||||
}
|
||||
}));
|
||||
if (window.__mnoteLocalFolderEventBus && typeof window.__mnoteLocalFolderEventBus.emitSyntheticWatchBatch === 'function') {
|
||||
window.__mnoteLocalFolderEventBus.emitSyntheticWatchBatch({
|
||||
source: 'synthetic_page_ai_receipt',
|
||||
reason: 'agent_run_receipt',
|
||||
rootUri: rootUri,
|
||||
workspaceId: resolveWorkspaceId(document.body),
|
||||
payload: syntheticPayload
|
||||
});
|
||||
} else {
|
||||
window.dispatchEvent(new CustomEvent('tree:local-folder-watch-batch', {
|
||||
detail: { payload: syntheticPayload }
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (refresh.touchesCurrentFile === true) {
|
||||
@@ -1975,11 +1984,6 @@ export function createSidebarPageAiRuntime(context) {
|
||||
var allowedRoots = pageAiBuildAllowedRoots();
|
||||
var agentTargetPackage = pageAiBuildAgentTargetPackage(scopedContext, runTargetSnapshot);
|
||||
assertPageAiLocalWritePermission(prompt, agentTargetPackage);
|
||||
if (typeof pageAiEnrichOcrContextRefs === 'function') {
|
||||
var ocrContext = await pageAiEnrichOcrContextRefs(contextRefs, agentTargetPackage, scopedContext.editorTarget);
|
||||
contextRefs = ocrContext.contextRefs || contextRefs;
|
||||
agentTargetPackage = ocrContext.agentTargetPackage || agentTargetPackage;
|
||||
}
|
||||
if (scopedContext.pageContext && scopedContext.pageContext.aiContext) {
|
||||
scopedContext.pageContext.aiContext.runTargetSnapshot = runTargetSnapshot;
|
||||
scopedContext.pageContext.aiContext.agentTargetPackage = agentTargetPackage;
|
||||
|
||||
@@ -427,48 +427,6 @@ export function createSidebarPageAiTargetRuntime(context) {
|
||||
});
|
||||
}
|
||||
|
||||
function pageAiOcrEligibleResourceKind(value) {
|
||||
var normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized === 'image' || normalized === 'pdf' || normalized === 'attachment' || normalized === 'resource';
|
||||
}
|
||||
|
||||
function pageAiOcrEligiblePath(value) {
|
||||
var path = String(value || '').trim().toLowerCase();
|
||||
return /\.(png|jpg|jpeg|webp|bmp|tif|tiff|pdf)$/.test(path);
|
||||
}
|
||||
|
||||
function pageAiOcrBodyPreview(markdown) {
|
||||
var body = String(markdown || '').replace(/^---\n[\s\S]*?\n---\n?/, '').trim();
|
||||
return body.slice(0, 1600);
|
||||
}
|
||||
|
||||
async function fetchPageAiOcrSidecarContext(editorTarget) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function pageAiEnrichOcrContextRefs(contextRefs, agentTargetPackage, editorTarget) {
|
||||
var ocrContext = await fetchPageAiOcrSidecarContext(editorTarget);
|
||||
if (!ocrContext) return { contextRefs, agentTargetPackage };
|
||||
var nextRefs = pageAiNormalizeArray(contextRefs).map(function(ref) {
|
||||
if (ref && ref.kind === 'active_editor') return Object.assign({}, ref, { ocrContext: ocrContext });
|
||||
return ref;
|
||||
});
|
||||
var nextPackage = agentTargetPackage && typeof agentTargetPackage === 'object'
|
||||
? Object.assign({}, agentTargetPackage, { ocrContext: ocrContext })
|
||||
: agentTargetPackage;
|
||||
if (nextPackage && nextPackage.currentFile && typeof nextPackage.currentFile === 'object') {
|
||||
nextPackage.currentFile = Object.assign({}, nextPackage.currentFile, { ocrRootRelativePath: ocrContext.ocrRootRelativePath });
|
||||
}
|
||||
if (nextPackage && Array.isArray(nextPackage.targets)) {
|
||||
nextPackage.targets = nextPackage.targets.map(function(target, index) {
|
||||
return index === 0 && target && typeof target === 'object'
|
||||
? Object.assign({}, target, { ocrContext: ocrContext })
|
||||
: target;
|
||||
});
|
||||
}
|
||||
return { contextRefs: nextRefs, agentTargetPackage: nextPackage };
|
||||
}
|
||||
|
||||
function pageAiBuildAllowedRoots() {
|
||||
return pageAiNormalizeArray(pageUiState.pageAiAllowedRoots).map(function(root) {
|
||||
return {
|
||||
@@ -790,7 +748,6 @@ export function createSidebarPageAiTargetRuntime(context) {
|
||||
pageAiBuildAgentTargetPackage,
|
||||
pageAiBuildAllowedRoots,
|
||||
pageAiBuildContextRefs,
|
||||
pageAiEnrichOcrContextRefs,
|
||||
pageAiBuildRunTargetSnapshot,
|
||||
pageAiCloneJson,
|
||||
pageAiContextKindsFromRefs,
|
||||
|
||||
@@ -67,10 +67,6 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
return Object.assign({}, DEFAULT_PAGE_WIDTH_PREFERENCES, pageUiState.pageWidthPreferences || {});
|
||||
}
|
||||
|
||||
function currentLocalOcrPreferences() {
|
||||
return Object.assign({ 'localOcr.autoEnabled': false }, pageUiState.localOcrPreferences || {});
|
||||
}
|
||||
|
||||
function currentKnowledgeRagSummary() {
|
||||
return pageUiState.knowledgeRagSummary || {};
|
||||
}
|
||||
@@ -340,12 +336,6 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
indexTrigger.setAttribute('data-state', indexOpen ? 'open' : 'closed');
|
||||
indexTrigger.setAttribute('aria-expanded', indexOpen ? 'true' : 'false');
|
||||
}
|
||||
var ocrTrigger = document.querySelector('[data-testid="mnote-local-ocr-task-toggle"]');
|
||||
var ocrOpen = isLocalOcrSettingsOpen();
|
||||
if (ocrTrigger instanceof HTMLElement) {
|
||||
ocrTrigger.setAttribute('data-state', ocrOpen ? 'open' : 'closed');
|
||||
ocrTrigger.setAttribute('aria-expanded', ocrOpen ? 'true' : 'false');
|
||||
}
|
||||
var ragTrigger = document.querySelector('[data-testid="mnote-knowledge-rag-settings-toggle"]');
|
||||
var ragOpen = isKnowledgeRagSettingsOpen();
|
||||
if (ragTrigger instanceof HTMLElement) {
|
||||
@@ -391,17 +381,6 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
'</label>';
|
||||
}
|
||||
|
||||
function createLocalOcrAutoRow() {
|
||||
return '' +
|
||||
'<label class="wolai-page-setting-row" data-page-setting-row="localOcrAutoEnabled">' +
|
||||
'<span class="wolai-page-setting-copy">' +
|
||||
'<span class="wolai-page-setting-label">资料库自动索引</span>' +
|
||||
'<span class="wolai-page-setting-hint">LiteParse/OCR sidecar 已退役;图片、PDF、Office 统一由 LightRAG 资料库处理</span>' +
|
||||
'</span>' +
|
||||
'<input type="checkbox" class="wolai-page-setting-checkbox" data-local-ocr-option-checkbox="autoEnabled" />' +
|
||||
'</label>';
|
||||
}
|
||||
|
||||
function createPageWidthSelectRow(type) {
|
||||
var options = type === 'default'
|
||||
? [
|
||||
@@ -456,11 +435,8 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
});
|
||||
}
|
||||
|
||||
function renderLocalOcrOptions(popover) {
|
||||
var localOcrPreferences = currentLocalOcrPreferences();
|
||||
popover.querySelectorAll('[data-local-ocr-option-checkbox="autoEnabled"]').forEach(function(input) {
|
||||
input.checked = localOcrPreferences['localOcr.autoEnabled'] === true;
|
||||
});
|
||||
function markRetiredLocalOcrPreferenceSurface() {
|
||||
document.documentElement.setAttribute('data-mnote-local-ocr-auto-retired', 'true');
|
||||
}
|
||||
|
||||
function createPageFontRow() {
|
||||
@@ -673,10 +649,6 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
return popover;
|
||||
}
|
||||
|
||||
function ensureLocalOcrSettingsPopover() {
|
||||
return ensureKnowledgeRagSettingsPopover();
|
||||
}
|
||||
|
||||
function createKnowledgeRagSettingsPanelHtml() {
|
||||
return '' +
|
||||
'<div class="wolai-page-settings-panel mnote-settings-panel mnote-knowledge-rag-settings-panel" role="dialog" aria-modal="false" aria-label="资料库问答设置">' +
|
||||
@@ -1809,51 +1781,12 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
var payload = await response.json().catch(function(){ return null; });
|
||||
if (!response.ok || !payload || payload.ok !== true) return;
|
||||
pageUiState.pageWidthPreferences = normalizePageWidthPreferences(payload.result && payload.result.pageWidthPreferences);
|
||||
pageUiState.localOcrPreferences = Object.assign({ 'localOcr.autoEnabled': false }, payload.result && payload.result.localOcrPreferences || {});
|
||||
window.__MNOTE_LOCAL_OCR_PREFERENCES = Object.assign({}, pageUiState.localOcrPreferences);
|
||||
document.documentElement.setAttribute('data-mnote-local-ocr-auto-enabled', String(pageUiState.localOcrPreferences['localOcr.autoEnabled'] === true));
|
||||
markRetiredLocalOcrPreferenceSurface();
|
||||
applyPageOptionsToShell();
|
||||
if (isPageSettingsOpen()) renderPageSettingsPopover();
|
||||
if (isLocalOcrSettingsOpen()) renderLocalOcrOptions(ensureLocalOcrSettingsPopover());
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
async function persistLocalOcrAutoPreference(enabled) {
|
||||
var previous = currentLocalOcrPreferences();
|
||||
var next = Object.assign({}, previous, { 'localOcr.autoEnabled': Boolean(enabled) });
|
||||
pageUiState.localOcrPreferences = next;
|
||||
window.__MNOTE_LOCAL_OCR_PREFERENCES = Object.assign({}, next);
|
||||
if (isPageSettingsOpen()) renderPageSettingsPopover();
|
||||
if (isLocalOcrSettingsOpen()) renderLocalOcrOptions(ensureLocalOcrSettingsPopover());
|
||||
try {
|
||||
var response = await fetch('/api/ui/preferences', {
|
||||
method: 'PUT',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
documentId: currentDocumentId(),
|
||||
workspaceId: resolveWorkspaceId(document.body),
|
||||
...currentWorkspaceSourcePayload(),
|
||||
updates: { 'localOcr.autoEnabled': Boolean(enabled) }
|
||||
})
|
||||
});
|
||||
var payload = await response.json().catch(function(){ return null; });
|
||||
if (!response.ok || !payload || payload.ok !== true) {
|
||||
throw new Error(payload && payload.error && payload.error.message ? payload.error.message : 'local_ocr_preference_save_failed_' + response.status);
|
||||
}
|
||||
pageUiState.localOcrPreferences = Object.assign({ 'localOcr.autoEnabled': false }, payload.result && payload.result.localOcrPreferences || {});
|
||||
window.__MNOTE_LOCAL_OCR_PREFERENCES = Object.assign({}, pageUiState.localOcrPreferences);
|
||||
document.documentElement.setAttribute('data-mnote-local-ocr-auto-enabled', String(pageUiState.localOcrPreferences['localOcr.autoEnabled'] === true));
|
||||
if (isPageSettingsOpen()) renderPageSettingsPopover();
|
||||
if (isLocalOcrSettingsOpen()) renderLocalOcrOptions(ensureLocalOcrSettingsPopover());
|
||||
} catch (error) {
|
||||
pageUiState.localOcrPreferences = previous;
|
||||
window.__MNOTE_LOCAL_OCR_PREFERENCES = Object.assign({}, previous);
|
||||
if (isPageSettingsOpen()) renderPageSettingsPopover();
|
||||
if (isLocalOcrSettingsOpen()) renderLocalOcrOptions(ensureLocalOcrSettingsPopover());
|
||||
document.documentElement.setAttribute('data-mnote-local-ocr-preference-error', error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
async function persistPageWidthPreference(type, mode) {
|
||||
if (PAGE_WIDTH_TYPES.indexOf(type) < 0) return;
|
||||
var previous = pageUiState.pageWidthPreferences;
|
||||
@@ -1908,24 +1841,18 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
return popover instanceof HTMLElement && !popover.hidden;
|
||||
}
|
||||
|
||||
function isLocalOcrSettingsOpen() {
|
||||
var popover = document.querySelector('[data-testid="mnote-local-ocr-settings-popover"]');
|
||||
return popover instanceof HTMLElement && !popover.hidden;
|
||||
}
|
||||
|
||||
function isKnowledgeRagSettingsOpen() {
|
||||
var popover = document.querySelector('[data-testid="mnote-knowledge-rag-settings-popover"]');
|
||||
return popover instanceof HTMLElement && !popover.hidden;
|
||||
}
|
||||
|
||||
function isAnySettingsOpen() {
|
||||
return isPageSettingsOpen() || isLocalIndexSettingsOpen() || isLocalOcrSettingsOpen() || isKnowledgeRagSettingsOpen();
|
||||
return isPageSettingsOpen() || isLocalIndexSettingsOpen() || isKnowledgeRagSettingsOpen();
|
||||
}
|
||||
|
||||
function openPageSettingsPopover(initialTab) {
|
||||
if (!currentDocumentId()) return;
|
||||
closeLocalIndexSettingsPopover();
|
||||
closeLocalOcrSettingsPopover();
|
||||
closeKnowledgeRagSettingsPopover();
|
||||
var popover = ensurePageSettingsPopover();
|
||||
renderPageSettingsPopover();
|
||||
@@ -1937,7 +1864,6 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
|
||||
function openPageIndexSettingsPopover() {
|
||||
closePageSettingsPopover();
|
||||
closeLocalOcrSettingsPopover();
|
||||
closeKnowledgeRagSettingsPopover();
|
||||
var popover = ensureLocalIndexSettingsPopover();
|
||||
renderPageSettingsLocalIndex(popover);
|
||||
@@ -1946,14 +1872,9 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
updateStandaloneSettingsTriggerState();
|
||||
}
|
||||
|
||||
function openLocalOcrSettingsPopover() {
|
||||
openKnowledgeRagSettingsPopover();
|
||||
}
|
||||
|
||||
function openKnowledgeRagSettingsPopover() {
|
||||
closePageSettingsPopover();
|
||||
closeLocalIndexSettingsPopover();
|
||||
closeLocalOcrSettingsPopover();
|
||||
var popover = ensureKnowledgeRagSettingsPopover();
|
||||
renderKnowledgeRagSettings(popover);
|
||||
popover.hidden = false;
|
||||
@@ -1978,12 +1899,6 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
updateStandaloneSettingsTriggerState();
|
||||
}
|
||||
|
||||
function closeLocalOcrSettingsPopover() {
|
||||
var popover = document.querySelector('[data-testid="mnote-local-ocr-settings-popover"]');
|
||||
if (popover instanceof HTMLElement) popover.hidden = true;
|
||||
updateStandaloneSettingsTriggerState();
|
||||
}
|
||||
|
||||
function closeKnowledgeRagSettingsPopover() {
|
||||
var popover = document.querySelector('[data-testid="mnote-knowledge-rag-settings-popover"]');
|
||||
if (popover instanceof HTMLElement) popover.hidden = true;
|
||||
@@ -1993,7 +1908,6 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
function closeAllSettingsPopovers() {
|
||||
closePageSettingsPopover();
|
||||
closeLocalIndexSettingsPopover();
|
||||
closeLocalOcrSettingsPopover();
|
||||
closeKnowledgeRagSettingsPopover();
|
||||
}
|
||||
|
||||
@@ -2006,10 +1920,6 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
applyPageOptionsToShell();
|
||||
});
|
||||
|
||||
window.addEventListener('mnote:open-local-ocr-settings', function() {
|
||||
openLocalOcrSettingsPopover();
|
||||
});
|
||||
|
||||
window.addEventListener('mnote:knowledge-rag-source-updated', function() {
|
||||
if (isKnowledgeRagSettingsOpen()) void loadKnowledgeRagStatus(true);
|
||||
});
|
||||
@@ -2021,32 +1931,26 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
closeKnowledgeRagSettingsPopover,
|
||||
closePageHistoryDrawer,
|
||||
closeLocalIndexSettingsPopover,
|
||||
closeLocalOcrSettingsPopover,
|
||||
closePageSettingsPopover,
|
||||
closePageShareDialog,
|
||||
currentLocalOcrPreferences,
|
||||
currentKnowledgeRagSummary,
|
||||
currentPageOptions,
|
||||
ensureHistorySnapshotsSeeded,
|
||||
ensureKnowledgeRagSettingsPopover,
|
||||
ensureLocalIndexSettingsPopover,
|
||||
ensureLocalOcrSettingsPopover,
|
||||
ensurePageHistoryDrawer,
|
||||
ensurePageSettingsPopover,
|
||||
ensurePageShareDialog,
|
||||
isAnySettingsOpen,
|
||||
isKnowledgeRagSettingsOpen,
|
||||
isLocalIndexSettingsOpen,
|
||||
isLocalOcrSettingsOpen,
|
||||
isPageSettingsOpen,
|
||||
openKnowledgeRagSettingsPopover,
|
||||
openPageHistoryDrawer,
|
||||
openLocalOcrSettingsPopover,
|
||||
openPageSettingsPopover,
|
||||
openPageIndexSettingsPopover,
|
||||
openPageShareDialog,
|
||||
pageOptionIsSupported,
|
||||
persistLocalOcrAutoPreference,
|
||||
persistLocalIndexSettings,
|
||||
persistPageOptionsPatch,
|
||||
persistPageWidthPreference,
|
||||
|
||||
@@ -2004,7 +2004,17 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
setTreeLiveApplyError('tree_resync_missing_projection_payload');
|
||||
});
|
||||
|
||||
window.addEventListener('mnote:local-folder:sidebar-refresh-requested', function(event) {
|
||||
var detail = event.detail || {};
|
||||
document.documentElement.setAttribute('data-mnote-local-folder-event-bus-sidebar-refresh-received', String(detail.reason || 'event-bus'));
|
||||
applyLocalFolderWatchBatch(detail);
|
||||
});
|
||||
|
||||
window.addEventListener('tree:local-folder-watch-batch', function(event) {
|
||||
if (event.detail && event.detail.viaEventBus === true) {
|
||||
document.documentElement.setAttribute('data-mnote-local-folder-watch-batch-skipped', 'event-bus-orchestrated');
|
||||
return;
|
||||
}
|
||||
var payload = event.detail && event.detail.payload ? event.detail.payload : event.detail;
|
||||
applyLocalFolderWatchBatch(payload);
|
||||
});
|
||||
|
||||
@@ -209,7 +209,6 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
const isPageSettingsOpen = (...args) => sidebarPageSettings.isPageSettingsOpen(...args);
|
||||
const openPageHistoryDrawer = (...args) => sidebarPageSettings.openPageHistoryDrawer(...args);
|
||||
const openPageIndexSettingsPopover = (...args) => sidebarPageSettings.openPageIndexSettingsPopover(...args);
|
||||
const openLocalOcrSettingsPopover = (...args) => sidebarPageSettings.openLocalOcrSettingsPopover(...args);
|
||||
const openKnowledgeRagSettingsPopover = (...args) => sidebarPageSettings.openKnowledgeRagSettingsPopover(...args);
|
||||
const openPageShareDialog = (...args) => sidebarPageSettings.openPageShareDialog(...args);
|
||||
const pageOptionIsSupported = (...args) => sidebarPageSettings.pageOptionIsSupported(...args);
|
||||
@@ -223,7 +222,6 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
const pruneKnowledgeRagRegistry = (...args) => sidebarPageSettings.pruneKnowledgeRagRegistry(...args);
|
||||
const setKnowledgeRagSourceFilter = (...args) => sidebarPageSettings.setKnowledgeRagSourceFilter(...args);
|
||||
const useKnowledgeRagFileTreeSelection = (...args) => sidebarPageSettings.useKnowledgeRagFileTreeSelection(...args);
|
||||
const persistLocalOcrAutoPreference = (...args) => sidebarPageSettings.persistLocalOcrAutoPreference(...args);
|
||||
const persistLocalIndexSettings = (...args) => sidebarPageSettings.persistLocalIndexSettings(...args);
|
||||
const persistPageOptionsPatch = (...args) => sidebarPageSettings.persistPageOptionsPatch(...args);
|
||||
const persistPageWidthPreference = (...args) => sidebarPageSettings.persistPageWidthPreference(...args);
|
||||
@@ -477,7 +475,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
return title && title.textContent ? title.textContent.trim() : (fallback || '文件夹');
|
||||
}
|
||||
|
||||
function isLocalOcrMarkdownPath(relativePath) {
|
||||
function isRetiredOcrSidecarMarkdownPath(relativePath) {
|
||||
var normalized = String(relativePath || '').trim().replace(/\\/g, '/').replace(/^\/+/, '');
|
||||
return /(^|\/)[^/]+\.ocr\/[^/]+\.ocr\.md$/i.test(normalized);
|
||||
}
|
||||
@@ -1298,24 +1296,8 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
applyEditorAttachmentMeta(link, attachmentMetaCache[assetId]);
|
||||
return;
|
||||
}
|
||||
attachmentMetaPending[assetId] = fetch('/api/media/sign?assetId=' + encodeURIComponent(assetId), {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
cache: 'no-store'
|
||||
}).then(function(response) {
|
||||
return response.json().catch(function() { return null; }).then(function(payload) {
|
||||
if (!response.ok || !payload) return null;
|
||||
var asset = payload.asset && typeof payload.asset === 'object' ? payload.asset : {};
|
||||
var meta = {
|
||||
assetId: assetId,
|
||||
fileSize: uploadedFileSize(asset)
|
||||
};
|
||||
attachmentMetaCache[assetId] = meta;
|
||||
return meta;
|
||||
});
|
||||
}).catch(function() {
|
||||
return null;
|
||||
}).finally(function() {
|
||||
document.documentElement.setAttribute('data-mnote-media-sign-retired', 'true');
|
||||
attachmentMetaPending[assetId] = Promise.resolve(null).finally(function() {
|
||||
delete attachmentMetaPending[assetId];
|
||||
});
|
||||
try {
|
||||
@@ -1899,20 +1881,8 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
throw new Error('上传失败');
|
||||
}
|
||||
} else {
|
||||
var form = new FormData();
|
||||
form.append('file', file);
|
||||
form.append('workspaceId', plan.workspaceId);
|
||||
form.append('documentId', plan.targetDocumentId);
|
||||
if (plan.targetMindmapId) form.append('mindmapId', plan.targetMindmapId);
|
||||
var response = await fetchWithTimeout('/api/media/upload', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: form
|
||||
}, 15000, '上传');
|
||||
payload = await response.json().catch(function() { return null; });
|
||||
if (!response.ok || !payload || !payload.asset) {
|
||||
throw new Error(payload && payload.error ? payload.error : '上传失败');
|
||||
}
|
||||
document.documentElement.setAttribute('data-mnote-media-upload-retired', 'true');
|
||||
throw new Error('旧 Convex Files 上传链已退役;local-first 附件请使用本地文件夹上传目标。');
|
||||
}
|
||||
appendUploadedAssetRow(payload.asset, plan.targetDocumentId);
|
||||
if (options && options.insertIntoEditor) {
|
||||
@@ -2695,20 +2665,6 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
return;
|
||||
}
|
||||
|
||||
var ocrTaskTrigger = closestAction(e.target, '[data-mnote-action="toggle-ocr-tasks"]');
|
||||
if (ocrTaskTrigger) {
|
||||
e.preventDefault();
|
||||
openKnowledgeRagSettingsPopover();
|
||||
return;
|
||||
}
|
||||
|
||||
var ocrSettingsTrigger = closestAction(e.target, '[data-mnote-action="open-ocr-settings"]');
|
||||
if (ocrSettingsTrigger) {
|
||||
e.preventDefault();
|
||||
openKnowledgeRagSettingsPopover();
|
||||
return;
|
||||
}
|
||||
|
||||
var settingsClose = closestAction(e.target, '[data-settings-action="close"]');
|
||||
if (settingsClose) {
|
||||
e.preventDefault();
|
||||
@@ -2750,17 +2706,6 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
}
|
||||
}
|
||||
|
||||
var localOcrSettingsAction = closestAction(e.target, '[data-local-ocr-settings-action]');
|
||||
if (localOcrSettingsAction) {
|
||||
e.preventDefault();
|
||||
var localOcrSettingsActionName = localOcrSettingsAction.getAttribute('data-local-ocr-settings-action') || '';
|
||||
closeAllSettingsPopovers();
|
||||
window.dispatchEvent(new CustomEvent('mnote:local-ocr-settings-action', {
|
||||
detail: { action: localOcrSettingsActionName }
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
var knowledgeRagAction = closestAction(e.target, '[data-knowledge-rag-action]');
|
||||
if (knowledgeRagAction) {
|
||||
e.preventDefault();
|
||||
@@ -3042,14 +2987,14 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
if (e.shiftKey || e.ctrlKey || e.metaKey) {
|
||||
return;
|
||||
}
|
||||
if ((rowKind === 'document' || rowKind === 'index' || rowKind === 'markdown') && isLocalOcrMarkdownPath(localRelativePath)) {
|
||||
document.documentElement.setAttribute('data-mnote-local-ocr-filetree-open', 'resource-tab');
|
||||
var ocrResourceInput = {
|
||||
if ((rowKind === 'document' || rowKind === 'index' || rowKind === 'markdown') && isRetiredOcrSidecarMarkdownPath(localRelativePath)) {
|
||||
document.documentElement.setAttribute('data-mnote-retired-ocr-sidecar-filetree-open', 'resource-tab');
|
||||
var retiredOcrSidecarResourceInput = {
|
||||
path: localRelativePath,
|
||||
title: fileTreeRowTitleForShortcut(fileRow, localRelativePath),
|
||||
kind: 'markdown',
|
||||
objectIdentity: 'local-ocr:' + localRelativePath,
|
||||
assetId: 'local-ocr:' + localRelativePath,
|
||||
objectIdentity: 'retired-ocr-sidecar:' + localRelativePath,
|
||||
assetId: 'retired-ocr-sidecar:' + localRelativePath,
|
||||
documentId: documentId || ownerDocumentId || null,
|
||||
workspaceId: resolveWorkspaceId(fileRow),
|
||||
sourceKind: 'local_folder',
|
||||
@@ -3059,9 +3004,9 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
paneRole: 'primary'
|
||||
};
|
||||
if (typeof window.__mnoteDocumentPaneRuntime?.openResourceInActiveTab === 'function') {
|
||||
void window.__mnoteDocumentPaneRuntime.openResourceInActiveTab(ocrResourceInput);
|
||||
void window.__mnoteDocumentPaneRuntime.openResourceInActiveTab(retiredOcrSidecarResourceInput);
|
||||
} else {
|
||||
void openLocalResourceInActiveTab(ocrResourceInput);
|
||||
void openLocalResourceInActiveTab(retiredOcrSidecarResourceInput);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -3219,11 +3164,6 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
renderPageSettingsPopover();
|
||||
return;
|
||||
}
|
||||
var localOcrCheckbox = closestAction(event.target, '[data-local-ocr-option-checkbox="autoEnabled"]');
|
||||
if (localOcrCheckbox instanceof HTMLInputElement) {
|
||||
void persistLocalOcrAutoPreference(localOcrCheckbox.checked);
|
||||
return;
|
||||
}
|
||||
var checkbox = closestAction(event.target, '[data-page-option-checkbox]');
|
||||
if (checkbox instanceof HTMLInputElement) {
|
||||
var key = checkbox.getAttribute('data-page-option-checkbox') || '';
|
||||
|
||||
@@ -194,6 +194,19 @@
|
||||
var localRootUri = (params.get('rootUri') || '').trim()
|
||||
|| (document.body instanceof HTMLElement ? (document.body.getAttribute('data-mnote-root-uri') || '').trim() : '');
|
||||
if (localRootUri && 'EventSource' in window) {
|
||||
var localBus = window.__mnoteLocalFolderEventBus;
|
||||
if (localBus && typeof localBus.startLocalFolderWatcher === 'function') {
|
||||
var localHandle = localBus.startLocalFolderWatcher({
|
||||
rootUri: localRootUri,
|
||||
workspaceId: bootstrap.workspaceId || resolveWorkspaceId(),
|
||||
bootstrap: bootstrap
|
||||
});
|
||||
if (localHandle) {
|
||||
window.__mnoteTreeLiveEventSource = localHandle;
|
||||
applyStatus('connected');
|
||||
return;
|
||||
}
|
||||
}
|
||||
var url = new URL('/api/local-folder/events', window.location.origin);
|
||||
url.searchParams.set('rootUri', localRootUri);
|
||||
url.searchParams.set('treeLive', 'true');
|
||||
|
||||
@@ -9,10 +9,9 @@ use axum::middleware::Next;
|
||||
use axum::response::Response;
|
||||
use axum::Router;
|
||||
use control_plane::{ControlPlaneStore, SqliteControlPlaneStore};
|
||||
use std::collections::BTreeMap;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::sync::Arc;
|
||||
use tower_http::trace::TraceLayer;
|
||||
use tracing::{error, warn};
|
||||
|
||||
@@ -140,8 +139,6 @@ pub struct AppState {
|
||||
pub editor_actor: EditorRuntimeActor,
|
||||
pub block_delta_tx: broadcast::Sender<serde_json::Value>,
|
||||
pub stream_delta_tx: broadcast::Sender<serde_json::Value>,
|
||||
pub local_ocr_job_tx: broadcast::Sender<serde_json::Value>,
|
||||
pub local_ocr_active_jobs: Arc<RwLock<BTreeMap<String, serde_json::Value>>>,
|
||||
pub acp_runtime: Arc<AcpRuntimeManager>,
|
||||
pub buffer_store: BufferStore,
|
||||
control_plane: Arc<SqliteControlPlaneStore>,
|
||||
@@ -151,7 +148,6 @@ impl AppState {
|
||||
pub fn new(config: AppConfig) -> Self {
|
||||
let (block_delta_tx, _) = broadcast::channel(256);
|
||||
let (stream_delta_tx, _) = broadcast::channel(256);
|
||||
let (local_ocr_job_tx, _) = broadcast::channel(256);
|
||||
let actor = EditorRuntimeActor::new();
|
||||
actor.set_block_delta_tx(block_delta_tx.clone());
|
||||
let buffer_store = BufferStore::new();
|
||||
@@ -165,8 +161,6 @@ impl AppState {
|
||||
editor_actor: actor,
|
||||
block_delta_tx,
|
||||
stream_delta_tx,
|
||||
local_ocr_job_tx,
|
||||
local_ocr_active_jobs: Arc::new(RwLock::new(BTreeMap::new())),
|
||||
acp_runtime: Arc::new(AcpRuntimeManager::from_env()),
|
||||
buffer_store,
|
||||
control_plane,
|
||||
|
||||
@@ -130,6 +130,8 @@ fn compact_query_result_for_agent(payload: Value) -> Value {
|
||||
"references": references,
|
||||
"citations": citations,
|
||||
"sourceScope": payload.get("sourceScope").cloned().unwrap_or(Value::Array(Vec::new())),
|
||||
"sourceScopeMode": payload.get("sourceScopeMode").cloned().unwrap_or_else(|| json!("post_filter_mapped_references")),
|
||||
"rawScopeFiltered": payload.get("rawScopeFiltered").cloned().unwrap_or(Value::Bool(false)),
|
||||
"rawStatus": payload.pointer("/raw/status").cloned().unwrap_or(Value::Null),
|
||||
"rawMessage": payload.pointer("/raw/message").cloned().unwrap_or(Value::Null),
|
||||
"rawMetadata": payload.pointer("/raw/metadata").cloned().unwrap_or(Value::Null),
|
||||
@@ -155,3 +157,43 @@ fn compact_reference_for_agent(reference: &Value) -> Value {
|
||||
"citationUrl": reference.get("citationUrl").cloned().unwrap_or(Value::Null),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn compact_query_result_marks_post_filter_scope_without_raw_chunks() {
|
||||
let payload = json!({
|
||||
"ok": true,
|
||||
"provider": "lightrag",
|
||||
"sourceScope": ["docs/a.md"],
|
||||
"sourceScopeMode": "post_filter_mapped_references",
|
||||
"rawScopeFiltered": false,
|
||||
"raw": {
|
||||
"status": "success",
|
||||
"chunks": [{"content": "raw chunk must not be exposed to agent"}],
|
||||
"metadata": {"count": 1}
|
||||
},
|
||||
"references": [{
|
||||
"sourceRootRelativePath": "docs/a.md",
|
||||
"quote": "scoped quote",
|
||||
"locatorDegraded": true,
|
||||
"citationMarkdown": "[来源定位降级:docs/a.md](/documents/local-md:docs~2Fa.md)"
|
||||
}]
|
||||
});
|
||||
|
||||
let compact = compact_query_result_for_agent(payload);
|
||||
assert_eq!(
|
||||
compact["sourceScopeMode"].as_str(),
|
||||
Some("post_filter_mapped_references")
|
||||
);
|
||||
assert_eq!(compact["rawScopeFiltered"].as_bool(), Some(false));
|
||||
assert!(compact.get("raw").is_none());
|
||||
assert!(compact.get("chunks").is_none());
|
||||
assert_eq!(
|
||||
compact["references"][0]["sourceRootRelativePath"].as_str(),
|
||||
Some("docs/a.md")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,6 +290,7 @@ fn doc_find_tool() -> Value {
|
||||
})
|
||||
}
|
||||
|
||||
// 旧 evidence 工具仅保留为历史对照;当前 manifest() 不注册这些工具,资料库问答走 mnote.knowledge_rag.*。
|
||||
#[allow(dead_code)]
|
||||
fn evidence_search_tool() -> Value {
|
||||
let mut properties = base_identity_properties();
|
||||
@@ -434,13 +435,13 @@ fn knowledge_rag_query_tool() -> Value {
|
||||
json!({
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "可选 MNote workspace 相对路径范围;可传文件或目录,返回 references 会限制在这些来源内。"
|
||||
"description": "可选 MNote workspace 相对路径范围;可传文件或目录。当前语义是 LightRAG provider 检索后,MNote 只过滤返回的 references;provider raw 仍可能是全局结果。"
|
||||
}),
|
||||
);
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.knowledge_rag.query",
|
||||
"description": "向 LightRAG 资料库提问,返回 provider 原始结果和经 MNote registry 映射后的引用。",
|
||||
"description": "向 LightRAG 资料库提问,返回 provider 原始结果和经 MNote registry 映射、sourcePaths 后过滤的引用。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["knowledge_rag.read", "evidence.read"],
|
||||
"status": "available",
|
||||
@@ -477,6 +478,7 @@ fn knowledge_rag_open_reference_tool() -> Value {
|
||||
})
|
||||
}
|
||||
|
||||
// 旧 local index agent 工具仅保留为历史对照;当前 manifest() 不注册这些工具。
|
||||
#[allow(dead_code)]
|
||||
fn index_status_tool() -> Value {
|
||||
let mut properties = base_identity_properties();
|
||||
|
||||
@@ -1087,7 +1087,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn evidence_search_route_prefers_sqlite_index() {
|
||||
async fn evidence_search_route_returns_retired_guard() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-evidence-route-sqlite-{}-{}",
|
||||
std::process::id(),
|
||||
@@ -1182,22 +1182,15 @@ mod tests {
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(response.status(), StatusCode::GONE);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
let first = payload["results"]
|
||||
.as_array()
|
||||
.and_then(|items| items.first())
|
||||
.expect("sqlite evidence result");
|
||||
assert_eq!(payload["ok"], false);
|
||||
assert_eq!(
|
||||
first["source"]["ownerDocumentPath"].as_str(),
|
||||
Some("README.md")
|
||||
);
|
||||
assert_eq!(
|
||||
first["source"]["schema"].as_str(),
|
||||
Some("mnote.evidence_locator.v1")
|
||||
payload["code"].as_str(),
|
||||
Some("mnote_evidence_search_retired")
|
||||
);
|
||||
fs::remove_dir_all(&root).ok();
|
||||
}
|
||||
|
||||
@@ -3646,7 +3646,7 @@ mod tests {
|
||||
assert!(!html.contains(r#"data-testid="mnote-workspace-empty-state""#));
|
||||
assert!(!html.contains("当前还没有可显示的本地工作区"));
|
||||
assert!(!html.contains(r#"data-testid="mnote-empty-create-page""#));
|
||||
assert!(html.contains(r#""transport":"disabled""#));
|
||||
assert!(html.contains(r#""transport":"tree-live-ws""#));
|
||||
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
}
|
||||
|
||||
@@ -10608,7 +10608,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn page_ai_capabilities_expose_local_index_and_toggle_tools() {
|
||||
async fn page_ai_capabilities_expose_knowledge_rag_and_toggle_tools() {
|
||||
let _env_guard = env_lock().lock().expect("env lock");
|
||||
let hermes_home = std::env::temp_dir().join(format!(
|
||||
"mnote-web-ai-capability-policy-{}",
|
||||
@@ -13223,20 +13223,16 @@ mod tests {
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["ok"], true);
|
||||
assert_eq!(payload["sessions"][0]["sessionId"], "sess_1");
|
||||
assert_eq!(payload["persistence"], "convex_acp_runtime_store");
|
||||
assert_eq!(payload["ok"], false);
|
||||
assert_eq!(payload["code"], "convex_retired");
|
||||
|
||||
let query_body = captured_body.lock().expect("captured convex body").clone();
|
||||
assert_eq!(query_body["path"], "aiSessions:listRuntimeRuns");
|
||||
assert_eq!(query_body["args"]["userId"], "user_1");
|
||||
assert_eq!(query_body["args"]["workspaceId"], "ws_1");
|
||||
assert_eq!(query_body["args"]["documentId"], "doc_1");
|
||||
assert_eq!(query_body, Value::Null);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -13317,28 +13313,16 @@ mod tests {
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["ok"], true);
|
||||
assert_eq!(payload["persistence"], "convex_acp_runtime_store");
|
||||
assert_eq!(payload["session"]["sessionId"], "sess_1");
|
||||
assert_eq!(payload["session"]["runs"][0]["runId"], "run_1");
|
||||
assert_eq!(payload["runtime"]["runId"], "run_1");
|
||||
assert_eq!(payload["events"][0]["eventType"], "message.delta");
|
||||
assert_eq!(
|
||||
payload["session"]["messages"].as_array().map(Vec::len),
|
||||
Some(0)
|
||||
);
|
||||
assert_eq!(payload["ok"], false);
|
||||
assert_eq!(payload["code"], "convex_retired");
|
||||
|
||||
let bodies = captured_bodies.lock().expect("captured convex bodies");
|
||||
assert_eq!(bodies[0]["path"], "aiSessions:listRuntimeRuns");
|
||||
assert_eq!(bodies[0]["args"]["userId"], "user_1");
|
||||
assert_eq!(bodies[0]["args"]["sessionId"], "sess_1");
|
||||
assert_eq!(bodies[1]["path"], "aiSessions:listRuntimeEvents");
|
||||
assert_eq!(bodies[1]["args"]["runId"], "run_1");
|
||||
assert!(bodies.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -13403,15 +13387,13 @@ mod tests {
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["ok"], true);
|
||||
assert_eq!(payload["resumed"], true);
|
||||
assert_eq!(payload["resumeSource"], "convex_acp_runtime_store");
|
||||
assert_eq!(payload["session"]["runs"][0]["runId"], "run_1");
|
||||
assert_eq!(payload["ok"], false);
|
||||
assert_eq!(payload["code"], "convex_retired");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -13486,7 +13468,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_client_acp_session_search_returns_convex_snippets() {
|
||||
async fn hermes_client_acp_session_search_legacy_convex_returns_retired_guard() {
|
||||
let captured_body = Arc::new(Mutex::new(Value::Null));
|
||||
let captured_for_route = Arc::clone(&captured_body);
|
||||
let mock = axum::Router::new().route(
|
||||
@@ -13548,21 +13530,16 @@ mod tests {
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["ok"], true);
|
||||
assert_eq!(payload["results"][0]["sessionId"], "sess_1");
|
||||
assert_eq!(payload["results"][0]["snippet"], "帮我总结化学页面");
|
||||
assert_eq!(payload["ok"], false);
|
||||
assert_eq!(payload["code"], "convex_retired");
|
||||
|
||||
let query_body = captured_body.lock().expect("captured convex body").clone();
|
||||
assert_eq!(query_body["path"], "aiSessions:searchRuntimeSessions");
|
||||
assert_eq!(query_body["args"]["userId"], "user_1");
|
||||
assert_eq!(query_body["args"]["workspaceId"], "ws_1");
|
||||
assert_eq!(query_body["args"]["q"], "化学");
|
||||
assert_eq!(query_body["args"]["limit"], 5);
|
||||
assert_eq!(query_body, Value::Null);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -14520,13 +14497,40 @@ mod tests {
|
||||
&"/api/hermes/client/runs".parse().expect("uri"),
|
||||
&headers,
|
||||
);
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-local-agent-run-receipt-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
std::fs::create_dir_all(&root).expect("create root");
|
||||
std::fs::write(root.join("README.md"), "# Readme\nold\n").expect("write readme");
|
||||
std::fs::write(root.join("Other.md"), "# Other\nold\n").expect("write other");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
let payload = json!({
|
||||
"workspaceId": "local-workspace-1",
|
||||
"documentId": "local-md:README.md",
|
||||
"sessionId": "sess_local_1",
|
||||
"rootUri": "file:///tmp/mnote-agent-run-receipt",
|
||||
"actorId": "user_1"
|
||||
"rootUri": root_uri,
|
||||
"actorId": "user_1",
|
||||
"contextRefs": [{
|
||||
"kind": "folder",
|
||||
"rootUri": root_uri,
|
||||
"relativePath": ""
|
||||
}],
|
||||
"targetPackage": {
|
||||
"schema": "mnote.agent_target_package.v1",
|
||||
"allowedFiles": ["README.md"],
|
||||
"currentFile": {
|
||||
"relativePath": "README.md"
|
||||
}
|
||||
}
|
||||
});
|
||||
let before = local_agent_audit_collect_snapshot_for_payload(&payload, None)
|
||||
.expect("before allowed-files snapshot");
|
||||
std::fs::write(root.join("README.md"), "# Readme\nnew\n").expect("modify readme");
|
||||
std::fs::write(root.join("Other.md"), "# Other\nnew\n").expect("modify other");
|
||||
let after = local_agent_audit_collect_snapshot_for_payload(&payload, Some(&before))
|
||||
.expect("after allowed-files snapshot");
|
||||
let changed_files = json!([
|
||||
{
|
||||
"path": "README.md",
|
||||
@@ -14541,8 +14545,8 @@ mod tests {
|
||||
"reasonix",
|
||||
"completed",
|
||||
changed_files,
|
||||
None,
|
||||
None,
|
||||
Some(&before),
|
||||
Some(&after),
|
||||
false,
|
||||
);
|
||||
let receipt = &event["agentRunReceipt"];
|
||||
@@ -14553,6 +14557,11 @@ mod tests {
|
||||
assert_eq!(receipt["status"], "completed");
|
||||
assert_eq!(receipt["changedFiles"][0]["path"], "README.md");
|
||||
assert_eq!(receipt["refresh"]["touchesCurrentFile"], true);
|
||||
assert_eq!(event["auditScope"]["scope"], "allowed_files");
|
||||
assert_eq!(event["auditScope"]["fileCount"], 1);
|
||||
assert_eq!(receipt["auditScope"]["scope"], "allowed_files");
|
||||
assert_eq!(receipt["auditScope"]["fileCount"], 1);
|
||||
let _ = std::fs::remove_dir_all(root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -15066,7 +15075,7 @@ mod tests {
|
||||
"/api/hermes/client/profile-memory?profile=chemist",
|
||||
None,
|
||||
),
|
||||
("GET", "/api/hermes/client/skills?profile=chemist", None),
|
||||
("GET", "/api/hermes/client/skills", None),
|
||||
(
|
||||
"PUT",
|
||||
"/api/hermes/client/profiles/active",
|
||||
@@ -15107,7 +15116,7 @@ mod tests {
|
||||
);
|
||||
let request = Request::builder()
|
||||
.method("GET")
|
||||
.uri("/api/hermes/client/skills?profile=chemist")
|
||||
.uri("/api/hermes/client/skills")
|
||||
.header("x-mnote-actor-id", "user_1")
|
||||
.body(Body::empty())
|
||||
.expect("request");
|
||||
|
||||
@@ -3099,7 +3099,7 @@ mod tests {
|
||||
assert!(markdown_edit["description"]
|
||||
.as_str()
|
||||
.expect("description")
|
||||
.contains("兼容"));
|
||||
.contains("compat"));
|
||||
assert!(markdown_edit["description"]
|
||||
.as_str()
|
||||
.expect("description")
|
||||
@@ -6432,7 +6432,7 @@ mod tests {
|
||||
"toolCallId": "call_1",
|
||||
"traceId": "trace_1",
|
||||
"idempotencyKey": "idem_markdown_normalized_1",
|
||||
"dryRun": false,
|
||||
"dryRun": true,
|
||||
"args": {
|
||||
"operations": [{"search": "第二 段", "replace": "测试123"}]
|
||||
}
|
||||
@@ -6444,25 +6444,23 @@ mod tests {
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let status = response.status();
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
assert_eq!(status, StatusCode::OK, "{}", String::from_utf8_lossy(&body));
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["result"]["operationsApplied"], 1);
|
||||
// 7-27: 新路径 changedBlocks 格式验证
|
||||
let changed = payload["result"]["applyResult"]["changedBlocks"]
|
||||
let changed = payload["result"]["applyResult"]["diff"]
|
||||
.as_array()
|
||||
.expect("changedBlocks");
|
||||
assert!(!changed.is_empty(), "changedBlocks should not be empty");
|
||||
.expect("diff");
|
||||
assert!(!changed.is_empty(), "diff should not be empty");
|
||||
assert_eq!(
|
||||
payload["result"]["applyResult"]["changedBlocks"][0]["blockId"],
|
||||
payload["result"]["applyResult"]["diff"][0]["blockId"],
|
||||
"p_2"
|
||||
);
|
||||
assert_eq!(
|
||||
payload["result"]["applyResult"]["changedBlocks"][0]["op"],
|
||||
"replace"
|
||||
);
|
||||
assert_eq!(payload["result"]["applyResult"]["diff"][0]["op"], "replace");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -24,6 +24,7 @@ const DEFAULT_LIGHTRAG_ENDPOINT: &str = "http://127.0.0.1:9621";
|
||||
const DEFAULT_LIGHTRAG_INPUT_DIR: &str = "/mnt/Data1T/Mnote_data/lightrag/inputs";
|
||||
const DEFAULT_LIGHTRAG_WORKING_DIR: &str = "/mnt/Data1T/Mnote_data/lightrag/rag_storage";
|
||||
const MAX_INGEST_SOURCES_PER_REQUEST: usize = 200;
|
||||
const SOURCE_SCOPE_MODE_POST_FILTER: &str = "post_filter_mapped_references";
|
||||
const KNOWLEDGE_RAG_SOURCE_EXTENSIONS: &[&str] = &[
|
||||
"md", "markdown", "txt", "pdf", "doc", "docx", "ppt", "pptx", "xls", "xlsx", "csv", "png",
|
||||
"jpg", "jpeg", "webp", "gif", "bmp", "tif", "tiff",
|
||||
@@ -154,6 +155,8 @@ pub(crate) fn knowledge_rag_source_statuses(
|
||||
}
|
||||
if provider_status == "failed" {
|
||||
statuses.failed_paths.insert(path.to_string());
|
||||
} else if provider_status == "delete_retry_required" {
|
||||
statuses.failed_paths.insert(path.to_string());
|
||||
} else if provider_status == "delete_submitted"
|
||||
|| (entry.deleted_at_ms.is_some() && entry.light_rag_doc_id.is_some())
|
||||
{
|
||||
@@ -473,6 +476,8 @@ pub async fn query_rag(
|
||||
"schema": "mnote.knowledge_rag.query_result.v1",
|
||||
"provider": "lightrag",
|
||||
"sourceScope": source_scope,
|
||||
"sourceScopeMode": SOURCE_SCOPE_MODE_POST_FILTER,
|
||||
"rawScopeFiltered": false,
|
||||
"raw": raw,
|
||||
"references": references,
|
||||
})))
|
||||
@@ -735,7 +740,13 @@ pub async fn prune_registry(
|
||||
}
|
||||
|
||||
fn knowledge_rag_registry_entry_prunable(entry: &KnowledgeRagSourceRegistryEntry) -> bool {
|
||||
if entry.light_rag_status.as_deref() == Some("delete_submitted") {
|
||||
if matches!(
|
||||
entry.light_rag_status.as_deref(),
|
||||
Some("delete_submitted" | "delete_retry_required")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if entry.light_rag_doc_id.is_some() && (entry.deleted_at_ms.is_some() || entry.stale) {
|
||||
return false;
|
||||
}
|
||||
entry.deleted_at_ms.is_some()
|
||||
@@ -746,6 +757,23 @@ fn knowledge_rag_registry_entry_prunable(entry: &KnowledgeRagSourceRegistryEntry
|
||||
)
|
||||
}
|
||||
|
||||
fn knowledge_rag_provider_delete_confirmed(entry: &KnowledgeRagSourceRegistryEntry) -> bool {
|
||||
entry.light_rag_doc_id.is_some()
|
||||
&& (entry.deleted_at_ms.is_some()
|
||||
|| entry.stale
|
||||
|| matches!(
|
||||
entry.light_rag_status.as_deref(),
|
||||
Some("delete_submitted" | "delete_retry_required")
|
||||
))
|
||||
}
|
||||
|
||||
fn mark_registry_entry_delete_completed(entry: &mut KnowledgeRagSourceRegistryEntry, now: u128) {
|
||||
entry.light_rag_doc_id = None;
|
||||
entry.indexed_at_ms = None;
|
||||
entry.light_rag_status = Some("delete_completed".into());
|
||||
entry.updated_at_ms = now;
|
||||
}
|
||||
|
||||
async fn sync_registry_with_documents(
|
||||
root_path: &Path,
|
||||
registry: &mut KnowledgeRagSourceRegistry,
|
||||
@@ -758,22 +786,28 @@ async fn sync_registry_with_documents(
|
||||
let by_file_path = lightrag_documents_by_file_path(&docs);
|
||||
let now = now_ms();
|
||||
let mut changed = false;
|
||||
let mut retry_doc_ids = Vec::new();
|
||||
for entry in &mut registry.entries {
|
||||
if let Some(doc) = document_for_registry_entry(&by_file_path, entry) {
|
||||
if let Some(id) = doc.get("id").and_then(Value::as_str) {
|
||||
entry.light_rag_doc_id = Some(id.to_string());
|
||||
}
|
||||
if let Some(status) = doc.get("status").and_then(Value::as_str) {
|
||||
entry.light_rag_status = Some(
|
||||
if entry.deleted_at_ms.is_some() {
|
||||
"delete_submitted"
|
||||
} else {
|
||||
status
|
||||
}
|
||||
.to_string(),
|
||||
);
|
||||
let delete_pending = matches!(
|
||||
entry.light_rag_status.as_deref(),
|
||||
Some("delete_submitted" | "delete_retry_required")
|
||||
) || entry.deleted_at_ms.is_some();
|
||||
if delete_pending {
|
||||
if let Some(doc_id) = entry.light_rag_doc_id.clone() {
|
||||
retry_doc_ids.push(doc_id);
|
||||
}
|
||||
if entry.light_rag_status.is_none() {
|
||||
entry.light_rag_status = Some("delete_submitted".into());
|
||||
}
|
||||
} else if let Some(status) = doc.get("status").and_then(Value::as_str) {
|
||||
entry.light_rag_status = Some(status.to_string());
|
||||
}
|
||||
if entry.deleted_at_ms.is_none()
|
||||
if !entry.stale
|
||||
&& entry.deleted_at_ms.is_none()
|
||||
&& doc.get("status").and_then(Value::as_str) == Some("processed")
|
||||
{
|
||||
entry.indexed_at_ms.get_or_insert(now);
|
||||
@@ -781,11 +815,8 @@ async fn sync_registry_with_documents(
|
||||
}
|
||||
entry.updated_at_ms = now;
|
||||
changed = true;
|
||||
} else if entry.deleted_at_ms.is_some() && entry.light_rag_doc_id.is_some() {
|
||||
entry.light_rag_doc_id = None;
|
||||
entry.indexed_at_ms = None;
|
||||
entry.light_rag_status = Some("delete_completed".into());
|
||||
entry.updated_at_ms = now;
|
||||
} else if knowledge_rag_provider_delete_confirmed(entry) {
|
||||
mark_registry_entry_delete_completed(entry, now);
|
||||
changed = true;
|
||||
} else if entry.deleted_at_ms.is_some()
|
||||
&& entry.light_rag_doc_id.is_none()
|
||||
@@ -801,9 +832,12 @@ async fn sync_registry_with_documents(
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
let stale_doc_ids = sync_registry_source_state(registry, now)?;
|
||||
let mut stale_doc_ids = sync_registry_source_state(registry, now)?;
|
||||
stale_doc_ids.extend(retry_doc_ids);
|
||||
stale_doc_ids.sort();
|
||||
stale_doc_ids.dedup();
|
||||
if !stale_doc_ids.is_empty() {
|
||||
let _ = lightrag_json(
|
||||
let delete_result = lightrag_json(
|
||||
reqwest::Method::DELETE,
|
||||
"/documents/delete_document",
|
||||
Some(json!({
|
||||
@@ -815,6 +849,23 @@ async fn sync_registry_with_documents(
|
||||
context,
|
||||
)
|
||||
.await;
|
||||
for entry in &mut registry.entries {
|
||||
if entry
|
||||
.light_rag_doc_id
|
||||
.as_deref()
|
||||
.is_some_and(|doc_id| stale_doc_ids.iter().any(|item| item == doc_id))
|
||||
{
|
||||
entry.light_rag_status = Some(
|
||||
if delete_result.is_err() {
|
||||
"delete_retry_required"
|
||||
} else {
|
||||
"delete_submitted"
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
entry.updated_at_ms = now;
|
||||
}
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
if changed {
|
||||
@@ -897,8 +948,8 @@ fn sync_registry_source_state(
|
||||
if !source_path.exists() {
|
||||
entry.stale = true;
|
||||
entry.deleted_at_ms = Some(now);
|
||||
entry.light_rag_doc_id = None;
|
||||
entry.indexed_at_ms = None;
|
||||
entry.light_rag_status = Some("delete_submitted".into());
|
||||
entry.updated_at_ms = now;
|
||||
stale_doc_ids.push(doc_id);
|
||||
continue;
|
||||
@@ -907,8 +958,8 @@ fn sync_registry_source_state(
|
||||
if current_hash != entry.source_hash {
|
||||
entry.stale = true;
|
||||
entry.source_hash = current_hash;
|
||||
entry.light_rag_doc_id = None;
|
||||
entry.indexed_at_ms = None;
|
||||
entry.light_rag_status = Some("delete_submitted".into());
|
||||
entry.updated_at_ms = now;
|
||||
stale_doc_ids.push(doc_id);
|
||||
}
|
||||
@@ -989,6 +1040,10 @@ fn mapped_references(
|
||||
.get("deleted")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false)
|
||||
&& !reference
|
||||
.get("unmapped")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -1127,6 +1182,7 @@ fn map_reference_plan(
|
||||
"sourceId": entry.map(|entry| entry.source_id.clone()),
|
||||
"sourcePath": source_path,
|
||||
"sourceRootRelativePath": source_root_relative_path,
|
||||
"unmapped": entry.is_none(),
|
||||
"stale": entry.is_some_and(|entry| entry.stale),
|
||||
"deleted": entry.is_some_and(|entry| entry.deleted_at_ms.is_some()),
|
||||
"locatorDegraded": locator_degraded,
|
||||
@@ -1346,15 +1402,11 @@ fn find_lightrag_sidecar_block(
|
||||
}
|
||||
let path = sidecar_blocks_path(entry)?;
|
||||
let content = fs::read_to_string(path).ok()?;
|
||||
let mut first_positioned_block = None;
|
||||
for line in content.lines() {
|
||||
let block = serde_json::from_str::<Value>(line).ok()?;
|
||||
if block.get("positions").and_then(Value::as_array).is_none() {
|
||||
continue;
|
||||
}
|
||||
if first_positioned_block.is_none() {
|
||||
first_positioned_block = Some(block.clone());
|
||||
}
|
||||
let block_text = block
|
||||
.get("content")
|
||||
.and_then(Value::as_str)
|
||||
@@ -1369,7 +1421,7 @@ fn find_lightrag_sidecar_block(
|
||||
return Some(block);
|
||||
}
|
||||
}
|
||||
first_positioned_block
|
||||
None
|
||||
}
|
||||
|
||||
fn normalize_text_for_match(value: &str) -> String {
|
||||
@@ -2234,17 +2286,85 @@ mod tests {
|
||||
let stale_doc_ids = sync_registry_source_state(&mut registry, 42).expect("sync");
|
||||
assert_eq!(stale_doc_ids, vec!["doc-changed", "doc-missing"]);
|
||||
assert_eq!(registry.entries[0].deleted_at_ms, Some(42));
|
||||
assert_eq!(registry.entries[0].light_rag_doc_id, None);
|
||||
assert_eq!(
|
||||
registry.entries[0].light_rag_doc_id.as_deref(),
|
||||
Some("doc-missing")
|
||||
);
|
||||
assert_eq!(
|
||||
registry.entries[0].light_rag_status.as_deref(),
|
||||
Some("delete_submitted")
|
||||
);
|
||||
assert_eq!(registry.entries[0].indexed_at_ms, None);
|
||||
assert!(registry.entries[0].stale);
|
||||
assert_eq!(registry.entries[1].deleted_at_ms, None);
|
||||
assert_eq!(registry.entries[1].light_rag_doc_id, None);
|
||||
assert_eq!(
|
||||
registry.entries[1].light_rag_doc_id.as_deref(),
|
||||
Some("doc-changed")
|
||||
);
|
||||
assert_eq!(
|
||||
registry.entries[1].light_rag_status.as_deref(),
|
||||
Some("delete_submitted")
|
||||
);
|
||||
assert_eq!(registry.entries[1].indexed_at_ms, None);
|
||||
assert!(registry.entries[1].stale);
|
||||
assert_ne!(registry.entries[1].source_hash, "mnote-fnv64:old");
|
||||
let _ = fs::remove_dir_all(root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_delete_confirmation_clears_doc_id_for_deleted_or_stale_entries() {
|
||||
let root = temp_root("mnote-knowledge-rag-delete-confirmed");
|
||||
let mut deleted = test_registry_entry(
|
||||
&root,
|
||||
"deleted.pdf",
|
||||
Some("doc-deleted"),
|
||||
Some("delete_submitted"),
|
||||
Some(2),
|
||||
Some(3),
|
||||
true,
|
||||
);
|
||||
let mut changed = test_registry_entry(
|
||||
&root,
|
||||
"changed.pdf",
|
||||
Some("doc-changed"),
|
||||
Some("delete_submitted"),
|
||||
Some(2),
|
||||
None,
|
||||
true,
|
||||
);
|
||||
let active = test_registry_entry(
|
||||
&root,
|
||||
"active.pdf",
|
||||
Some("doc-active"),
|
||||
Some("processed"),
|
||||
Some(2),
|
||||
None,
|
||||
false,
|
||||
);
|
||||
|
||||
assert!(knowledge_rag_provider_delete_confirmed(&deleted));
|
||||
mark_registry_entry_delete_completed(&mut deleted, 42);
|
||||
assert_eq!(deleted.light_rag_doc_id, None);
|
||||
assert_eq!(deleted.indexed_at_ms, None);
|
||||
assert_eq!(
|
||||
deleted.light_rag_status.as_deref(),
|
||||
Some("delete_completed")
|
||||
);
|
||||
|
||||
assert!(knowledge_rag_provider_delete_confirmed(&changed));
|
||||
mark_registry_entry_delete_completed(&mut changed, 43);
|
||||
assert_eq!(changed.light_rag_doc_id, None);
|
||||
assert_eq!(
|
||||
changed.light_rag_status.as_deref(),
|
||||
Some("delete_completed")
|
||||
);
|
||||
|
||||
assert!(!knowledge_rag_provider_delete_confirmed(&active));
|
||||
assert_eq!(active.light_rag_doc_id.as_deref(), Some("doc-active"));
|
||||
|
||||
let _ = fs::remove_dir_all(root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_statuses_distinguish_indexed_processing_failed_and_deleted() {
|
||||
let root = temp_root("mnote-knowledge-rag-source-statuses");
|
||||
@@ -2372,9 +2492,29 @@ mod tests {
|
||||
);
|
||||
let failed =
|
||||
test_registry_entry(&root, "failed.pdf", None, Some("failed"), None, None, false);
|
||||
let retry = test_registry_entry(
|
||||
&root,
|
||||
"retry.pdf",
|
||||
Some("doc-retry"),
|
||||
Some("delete_retry_required"),
|
||||
Some(2),
|
||||
Some(3),
|
||||
true,
|
||||
);
|
||||
let stale_with_doc = test_registry_entry(
|
||||
&root,
|
||||
"stale.pdf",
|
||||
Some("doc-stale"),
|
||||
Some("processed"),
|
||||
Some(2),
|
||||
None,
|
||||
true,
|
||||
);
|
||||
|
||||
assert!(!knowledge_rag_registry_entry_prunable(&active));
|
||||
assert!(!knowledge_rag_registry_entry_prunable(&deleting));
|
||||
assert!(!knowledge_rag_registry_entry_prunable(&retry));
|
||||
assert!(!knowledge_rag_registry_entry_prunable(&stale_with_doc));
|
||||
assert!(knowledge_rag_registry_entry_prunable(&removed));
|
||||
assert!(knowledge_rag_registry_entry_prunable(&failed));
|
||||
|
||||
@@ -2466,6 +2606,42 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapped_references_filters_unmapped_provider_references() {
|
||||
let registry = KnowledgeRagSourceRegistry {
|
||||
schema: REGISTRY_SCHEMA.to_string(),
|
||||
workspace_id: "ws".into(),
|
||||
root_uri: "file:///tmp/root".into(),
|
||||
updated_at_ms: 1,
|
||||
entries: vec![],
|
||||
};
|
||||
let raw = json!({
|
||||
"data": {
|
||||
"references": [{"reference_id":"1","file_path":"orphan.pdf"}],
|
||||
"chunks": [{
|
||||
"reference_id":"1",
|
||||
"chunk_id":"orphan-chunk",
|
||||
"file_path":"orphan.pdf",
|
||||
"content":"orphan provider chunk"
|
||||
}]
|
||||
}
|
||||
});
|
||||
let mapped = mapped_references(&raw, ®istry, "file:///tmp/root", Path::new("/tmp/root"));
|
||||
assert!(
|
||||
mapped.is_empty(),
|
||||
"unmapped provider references must not become MNote citations"
|
||||
);
|
||||
|
||||
let plan = map_reference_plan(
|
||||
&json!({"file_path":"orphan.pdf","chunk_id":"orphan-chunk"}),
|
||||
®istry,
|
||||
"file:///tmp/root",
|
||||
Path::new("/tmp/root"),
|
||||
);
|
||||
assert_eq!(plan["unmapped"], true);
|
||||
assert_eq!(plan["locatorDegraded"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_ranking_prefers_exact_source_and_quote_match() {
|
||||
let mut references = vec![
|
||||
@@ -2597,6 +2773,77 @@ mod tests {
|
||||
let _ = fs::remove_dir_all(root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn locator_degrades_when_sidecar_quote_does_not_match() {
|
||||
let _guard = env_lock().lock().expect("env lock");
|
||||
let root = temp_root("mnote-knowledge-rag-sidecar-no-match");
|
||||
fs::create_dir_all(root.join("docs")).expect("docs");
|
||||
fs::write(root.join("docs").join("Host.md"), "# Host\n").expect("host");
|
||||
fs::write(root.join("docs").join("scan.pdf"), b"pdf").expect("pdf");
|
||||
let input_dir = root.join("inputs");
|
||||
let parsed_dir = input_dir
|
||||
.join("__parsed__")
|
||||
.join("mnote-hash-scan.pdf.parsed");
|
||||
fs::create_dir_all(&parsed_dir).expect("parsed dir");
|
||||
fs::write(
|
||||
parsed_dir.join("mnote-hash-scan.blocks.jsonl"),
|
||||
[
|
||||
r#"{"type":"meta","blocks":1}"#,
|
||||
r##"{"type":"content","blockid":"block1","content":"This block is not the returned quote.","positions":[{"type":"bbox","anchor":"9","range":[1.0,2.0,3.0,4.0]}]}"##,
|
||||
]
|
||||
.join("\n"),
|
||||
)
|
||||
.expect("blocks");
|
||||
std::env::set_var("MNOTE_LIGHTRAG_INPUT_DIR", &input_dir);
|
||||
|
||||
let registry = KnowledgeRagSourceRegistry {
|
||||
schema: REGISTRY_SCHEMA.to_string(),
|
||||
workspace_id: "ws".into(),
|
||||
root_uri: "file:///tmp/root".into(),
|
||||
updated_at_ms: 1,
|
||||
entries: vec![KnowledgeRagSourceRegistryEntry {
|
||||
source_id: "src1".into(),
|
||||
workspace_id: "ws".into(),
|
||||
root_uri: "file:///tmp/root".into(),
|
||||
source_path: root.join("docs").join("scan.pdf").display().to_string(),
|
||||
source_root_relative_path: "docs/scan.pdf".into(),
|
||||
source_hash: "mnote-fnv64:1".into(),
|
||||
light_rag_doc_id: Some("doc1".into()),
|
||||
light_rag_status: Some("processed".into()),
|
||||
light_rag_file_path: "mnote-hash-scan.pdf".into(),
|
||||
symlink_path: input_dir.join("mnote-hash-scan.pdf").display().to_string(),
|
||||
parser_hint: None,
|
||||
indexed_at_ms: Some(2),
|
||||
deleted_at_ms: None,
|
||||
stale: false,
|
||||
updated_at_ms: 2,
|
||||
}],
|
||||
};
|
||||
|
||||
let mapped = map_reference_plan(
|
||||
&json!({
|
||||
"file_path": "mnote-hash-scan.pdf",
|
||||
"chunk_id": "doc1-chunk-000",
|
||||
"chunks": [{"chunk_id": "doc1-chunk-000", "content": "A different quote should not get page or bbox."}]
|
||||
}),
|
||||
®istry,
|
||||
"file:///tmp/root",
|
||||
&root,
|
||||
);
|
||||
assert!(mapped["locator"].is_null());
|
||||
assert_eq!(mapped["locatorDegraded"], true);
|
||||
assert!(mapped["citationUrl"]
|
||||
.as_str()
|
||||
.is_some_and(|url| url.contains("resourceTab=")));
|
||||
assert!(mapped["citationMarkdown"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains("来源定位降级"));
|
||||
|
||||
std::env::remove_var("MNOTE_LIGHTRAG_INPUT_DIR");
|
||||
let _ = fs::remove_dir_all(root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lightrag_paths_prefer_source_env_over_legacy_process_env() {
|
||||
let _guard = env_lock().lock().expect("env lock");
|
||||
|
||||
@@ -23,7 +23,7 @@ use std::hash::{Hash, Hasher};
|
||||
use std::path::PathBuf;
|
||||
use std::pin::Pin;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use tokio::sync::broadcast::{error::RecvError, Receiver};
|
||||
use tokio::sync::broadcast::error::RecvError;
|
||||
use tokio::time::timeout;
|
||||
|
||||
type BoxedEventStream =
|
||||
@@ -85,7 +85,6 @@ async fn build_document_events_stream(
|
||||
.local_folder_watcher_registry()
|
||||
.subscribe(&canonical_root)
|
||||
.map_err(|error| WebError::internal(error).with_context(&context))?;
|
||||
let local_ocr_job_rx = state.local_ocr_job_tx.subscribe();
|
||||
|
||||
let initial = json!({
|
||||
"sourceKind": "local_folder",
|
||||
@@ -95,79 +94,29 @@ async fn build_document_events_stream(
|
||||
"revision": system_time_ms(SystemTime::now()),
|
||||
});
|
||||
let stream = stream::unfold(
|
||||
(
|
||||
Some(initial),
|
||||
subscription,
|
||||
document_relative_path,
|
||||
query.root_uri.clone(),
|
||||
local_ocr_job_rx,
|
||||
),
|
||||
|(
|
||||
initial,
|
||||
mut subscription,
|
||||
document_relative_path,
|
||||
root_uri,
|
||||
mut local_ocr_job_rx,
|
||||
)| async move {
|
||||
(Some(initial), subscription, document_relative_path),
|
||||
|(initial, mut subscription, document_relative_path)| async move {
|
||||
if let Some(payload) = initial {
|
||||
return Some((
|
||||
Ok(stream_event("ready", &payload)),
|
||||
(
|
||||
None,
|
||||
subscription,
|
||||
document_relative_path,
|
||||
root_uri,
|
||||
local_ocr_job_rx,
|
||||
),
|
||||
(None, subscription, document_relative_path),
|
||||
));
|
||||
}
|
||||
loop {
|
||||
tokio::select! {
|
||||
watcher_result = subscription.receiver.recv() => {
|
||||
match watcher_result {
|
||||
Ok(payload) => {
|
||||
if let Some(expected) = document_relative_path.as_deref() {
|
||||
if !document_event_targets_relative_path(&payload, expected) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return Some((
|
||||
Ok(stream_event("change", &payload)),
|
||||
(
|
||||
None,
|
||||
subscription,
|
||||
document_relative_path,
|
||||
root_uri,
|
||||
local_ocr_job_rx,
|
||||
),
|
||||
));
|
||||
match subscription.receiver.recv().await {
|
||||
Ok(payload) => {
|
||||
if let Some(expected) = document_relative_path.as_deref() {
|
||||
if !document_event_targets_relative_path(&payload, expected) {
|
||||
continue;
|
||||
}
|
||||
Err(RecvError::Lagged(_)) => continue,
|
||||
Err(RecvError::Closed) => return None,
|
||||
}
|
||||
}
|
||||
ocr_result = recv_matching_ocr_event(&mut local_ocr_job_rx, &root_uri) => {
|
||||
match ocr_result {
|
||||
Some(payload) => {
|
||||
if let Some(expected) = document_relative_path.as_deref() {
|
||||
if !document_event_targets_relative_path(&payload, expected) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return Some((
|
||||
Ok(stream_event("local_ocr.job.updated", &payload)),
|
||||
(
|
||||
None,
|
||||
subscription,
|
||||
document_relative_path,
|
||||
root_uri,
|
||||
local_ocr_job_rx,
|
||||
),
|
||||
));
|
||||
}
|
||||
None => continue,
|
||||
}
|
||||
return Some((
|
||||
Ok(stream_event("change", &payload)),
|
||||
(None, subscription, document_relative_path),
|
||||
));
|
||||
}
|
||||
Err(RecvError::Lagged(_)) => continue,
|
||||
Err(RecvError::Closed) => return None,
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -177,20 +126,6 @@ async fn build_document_events_stream(
|
||||
Ok((HeaderMap::new(), stream))
|
||||
}
|
||||
|
||||
async fn recv_matching_ocr_event(rx: &mut Receiver<Value>, root_uri: &str) -> Option<Value> {
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Ok(payload) => {
|
||||
if payload.get("rootUri").and_then(Value::as_str) == Some(root_uri) {
|
||||
return Some(payload);
|
||||
}
|
||||
}
|
||||
Err(RecvError::Lagged(_)) => continue,
|
||||
Err(RecvError::Closed) => return None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the tree live stream: emits `snapshot` (initial) and `resync` (on watcher change)
|
||||
/// with full sidebar + file tree projections.
|
||||
///
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1217,11 +1217,8 @@ pub(crate) fn refresh_local_search_index_for_path_with_settings(
|
||||
if local_ocr::is_local_ocr_sidecar_relative_path(&relative_path) {
|
||||
index.built_at = now_ms();
|
||||
write_local_search_index_json(root_path, &index)?;
|
||||
if included {
|
||||
refresh_evidence_sqlite_index_for_path(root_path, &index, &relative_path)?;
|
||||
} else {
|
||||
remove_evidence_sqlite_index_for_path(root_path, &index, &relative_path)?;
|
||||
}
|
||||
let _ = included;
|
||||
remove_evidence_sqlite_index_for_path(root_path, &index, &relative_path)?;
|
||||
return Ok(json!({
|
||||
"version": index.version,
|
||||
"rootUri": index.root_uri,
|
||||
@@ -1948,13 +1945,6 @@ fn index_relative_path_is_included(relative_path: &str, include_paths: &[String]
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn local_index_relative_path_is_included(
|
||||
relative_path: &str,
|
||||
settings: &LocalIndexSettings,
|
||||
) -> bool {
|
||||
index_relative_path_is_included(relative_path, &settings.include_paths)
|
||||
}
|
||||
|
||||
fn collect_markdown_documents(
|
||||
root_path: &Path,
|
||||
current: &Path,
|
||||
@@ -2613,84 +2603,6 @@ fn parsed_resource_id(resource: &LocalSearchResource) -> String {
|
||||
format!("{}#parse", resource.resource_id)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn insert_ocr_evidence(
|
||||
connection: &Connection,
|
||||
root_path: &Path,
|
||||
index: &LocalSearchIndex,
|
||||
entry: &local_ocr::OcrIndexEntry,
|
||||
) -> Result<(), WebError> {
|
||||
if entry.status != "done" {
|
||||
return Ok(());
|
||||
}
|
||||
let ocr_path = root_path.join(&entry.ocr_root_relative_path);
|
||||
let Ok(markdown) = fs::read_to_string(&ocr_path) else {
|
||||
return Ok(());
|
||||
};
|
||||
let body = local_ocr::strip_ocr_frontmatter(&markdown);
|
||||
let source_map = path_source_map_path(&entry.ocr_root_relative_path).unwrap_or_default();
|
||||
let resource_id = format!(
|
||||
"{}#ocr:{}",
|
||||
entry.owner_document_id, entry.source_root_relative_path
|
||||
);
|
||||
let artifact = ocr_parsed_artifact(entry, &source_map);
|
||||
if !source_map.is_empty() {
|
||||
let source_map_path = root_path.join(&source_map);
|
||||
if let Ok(source_map_content) = fs::read_to_string(&source_map_path) {
|
||||
if let Ok(resource_source_map) =
|
||||
serde_json::from_str::<ResourceSourceMap>(&source_map_content)
|
||||
{
|
||||
return insert_source_map_artifact_evidence(
|
||||
connection,
|
||||
index,
|
||||
&resource_id,
|
||||
&artifact,
|
||||
&resource_source_map,
|
||||
body,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
insert_evidence_resource_from_artifact(connection, &resource_id, &artifact)?;
|
||||
let locator = json!({
|
||||
"schema": "mnote.evidence_locator.v1",
|
||||
"rootUri": index.root_uri,
|
||||
"ownerDocumentId": entry.owner_document_id,
|
||||
"ownerDocumentPath": entry.owner_document_path,
|
||||
"resourcePath": entry.source_root_relative_path,
|
||||
"resourceKind": evidence_resource_kind_for_path(&entry.source_root_relative_path),
|
||||
"sourceMapPath": source_map,
|
||||
"openAction": {
|
||||
"actionType": "mnote.open_resource_locator",
|
||||
"url": format!("/documents/{}?sourceKind=local_folder&rootUri={}", entry.owner_document_id, encode_query_component(&index.root_uri)),
|
||||
"params": {
|
||||
"resourcePath": entry.source_root_relative_path,
|
||||
"sourceMapPath": source_map
|
||||
}
|
||||
}
|
||||
});
|
||||
insert_evidence_block(connection, &resource_id, &resource_id, body, locator)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn ocr_parsed_artifact(
|
||||
entry: &local_ocr::OcrIndexEntry,
|
||||
source_map_root_relative_path: &str,
|
||||
) -> ParsedResourceArtifact {
|
||||
ParsedResourceArtifact {
|
||||
schema: PARSED_RESOURCE_ARTIFACT_SCHEMA.into(),
|
||||
provider: entry.provider.clone(),
|
||||
model_version: Some(entry.model_version.clone()),
|
||||
owner_document_id: entry.owner_document_id.clone(),
|
||||
owner_document_path: entry.owner_document_path.clone(),
|
||||
source_root_relative_path: entry.source_root_relative_path.clone(),
|
||||
source_hash: format!("size:{}:mtime:{}", entry.source_size, entry.source_mtime_ms),
|
||||
artifact_root_relative_path: entry.ocr_root_relative_path.clone(),
|
||||
source_map_root_relative_path: source_map_root_relative_path.to_string(),
|
||||
updated_at_ms: entry.updated_at_ms as u64,
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_source_map_artifact_evidence(
|
||||
connection: &Connection,
|
||||
index: &LocalSearchIndex,
|
||||
@@ -3479,35 +3391,6 @@ fn local_search_resource_matches(
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn local_search_ocr_matches(
|
||||
entry: &local_ocr::OcrIndexEntry,
|
||||
body: &str,
|
||||
query: &str,
|
||||
title_only: bool,
|
||||
exact: bool,
|
||||
) -> bool {
|
||||
if query.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let haystack = if title_only {
|
||||
normalize_search_text(&format!(
|
||||
"{}\n{}",
|
||||
entry.source_root_relative_path, entry.ocr_root_relative_path
|
||||
))
|
||||
} else {
|
||||
normalize_search_text(&format!(
|
||||
"{}\n{}\n{}",
|
||||
entry.source_root_relative_path, entry.ocr_root_relative_path, body
|
||||
))
|
||||
};
|
||||
if exact {
|
||||
haystack.contains(query)
|
||||
} else {
|
||||
token_search_match(&haystack, query)
|
||||
}
|
||||
}
|
||||
|
||||
fn local_search_document_projection(
|
||||
document: &LocalSearchDocument,
|
||||
root_uri: &str,
|
||||
@@ -3557,43 +3440,6 @@ fn local_search_resource_projection(resource: &LocalSearchResource, root_uri: &s
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn local_search_ocr_projection(
|
||||
entry: &local_ocr::OcrIndexEntry,
|
||||
body: &str,
|
||||
root_uri: &str,
|
||||
query: &str,
|
||||
) -> Value {
|
||||
let title = Path::new(&entry.owner_document_path)
|
||||
.file_stem()
|
||||
.and_then(|value| value.to_str())
|
||||
.unwrap_or("OCR")
|
||||
.to_string();
|
||||
json!({
|
||||
"id": format!("{}#ocr:{}", entry.owner_document_id, entry.source_root_relative_path),
|
||||
"documentId": entry.owner_document_id,
|
||||
"title": title,
|
||||
"path": entry.owner_document_path,
|
||||
"resourceType": "markdown",
|
||||
"sourceKind": "local_folder",
|
||||
"rootUri": root_uri,
|
||||
"hasOcr": true,
|
||||
"snippet": ocr_search_snippet(body, query),
|
||||
"ocrEvidence": {
|
||||
"sourceRootRelativePath": entry.source_root_relative_path,
|
||||
"ocrRootRelativePath": entry.ocr_root_relative_path,
|
||||
"provider": entry.provider,
|
||||
"status": entry.status,
|
||||
},
|
||||
"updatedAt": entry.updated_at_ms,
|
||||
"publicPath": format!(
|
||||
"/documents/{}?sourceKind=local_folder&rootUri={}",
|
||||
entry.owner_document_id,
|
||||
encode_query_component(root_uri),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn encode_query_component(value: &str) -> String {
|
||||
let mut encoded = String::with_capacity(value.len());
|
||||
for byte in value.as_bytes() {
|
||||
@@ -5143,6 +4989,10 @@ mod tests {
|
||||
.documents
|
||||
.iter()
|
||||
.any(|document| document.path == "docs/Page.ocr/photo.png-704905.ocr.md"));
|
||||
let refreshed_evidence = query_evidence_sqlite_results(&root, "OCR-hash-token", None, 10)
|
||||
.expect("query refreshed evidence")
|
||||
.unwrap_or_default();
|
||||
assert!(refreshed_evidence.is_empty());
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
@@ -5157,6 +5007,8 @@ mod tests {
|
||||
"---\ntitle: Child\n---\n# Child\nOriginalToken body.\n",
|
||||
)
|
||||
.expect("write child");
|
||||
write_local_index_settings(&root, &[String::from(".")], None, None, None, None)
|
||||
.expect("settings");
|
||||
|
||||
let first_projection = query_local_search_index(
|
||||
&root,
|
||||
@@ -5421,7 +5273,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn evidence_index_parses_resource_body_with_liteparse_sidecar() {
|
||||
fn evidence_index_does_not_parse_resource_body_with_retired_liteparse_sidecar() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let _guard = env_lock().lock().expect("env lock");
|
||||
@@ -5479,23 +5331,11 @@ JSON
|
||||
let results = query_evidence_sqlite_results(&root, "ResourceBodyToken", None, 10)
|
||||
.expect("sqlite query")
|
||||
.expect("sqlite exists");
|
||||
let hit = results
|
||||
.iter()
|
||||
.find(|result| result.quote.contains("ResourceBodyToken"))
|
||||
.expect("parsed resource body hit");
|
||||
assert_eq!(
|
||||
hit.source.owner_document_id, "local-md:docs~2FPage.md",
|
||||
"资源正文证据应归属引用它的 owner Markdown"
|
||||
);
|
||||
assert_eq!(
|
||||
hit.source.resource_path.as_deref(),
|
||||
Some("docs/Page.assets/spec.pdf")
|
||||
);
|
||||
assert_eq!(hit.source.page, Some(2));
|
||||
assert!(hit.source.bbox.is_some());
|
||||
assert_eq!(
|
||||
hit.source.source_map_path.as_deref(),
|
||||
Some("docs/Page.ocr/spec.pdf.source-map.json")
|
||||
assert!(
|
||||
!results
|
||||
.iter()
|
||||
.any(|result| result.quote.contains("ResourceBodyToken")),
|
||||
"LiteParse resource body fallback is retired from active evidence indexing"
|
||||
);
|
||||
let resource_scoped = query_evidence_sqlite_results_with_mode(
|
||||
&root,
|
||||
@@ -5506,18 +5346,16 @@ JSON
|
||||
)
|
||||
.expect("resource scoped sqlite query")
|
||||
.expect("sqlite exists");
|
||||
assert_eq!(resource_scoped.len(), 1);
|
||||
assert_eq!(
|
||||
resource_scoped[0].source.resource_path.as_deref(),
|
||||
Some("docs/Page.assets/spec.pdf"),
|
||||
"页面内搜索打开资源时应只限制到当前资源,而不是它的 owner Markdown"
|
||||
assert!(
|
||||
resource_scoped.is_empty(),
|
||||
"retired LiteParse sidecar must not create resource-scoped evidence hits"
|
||||
);
|
||||
assert!(root
|
||||
assert!(!root
|
||||
.join("docs")
|
||||
.join("Page.ocr")
|
||||
.join("spec.pdf.parse.md")
|
||||
.exists());
|
||||
assert!(root
|
||||
assert!(!root
|
||||
.join("docs")
|
||||
.join("Page.ocr")
|
||||
.join("spec.pdf.source-map.json")
|
||||
|
||||
@@ -1,28 +1,11 @@
|
||||
use crate::app::AppState;
|
||||
use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use crate::routes::command_support::{build_runtime_command_plan, runtime_context};
|
||||
use crate::transport::convex::{
|
||||
execute_retired_mutation_by_name, execute_retired_query_by_name,
|
||||
persist_runtime_command_artifacts,
|
||||
};
|
||||
use axum::extract::{Multipart, Query, State};
|
||||
use axum::http::{header, HeaderMap};
|
||||
use axum::extract::Query;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::{Extension, Json};
|
||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
|
||||
use bridge_runtime::{
|
||||
build_runtime_command_artifact_plan, RuntimeActorWire, RuntimeCommandEnvelopeWire,
|
||||
RuntimeSourceWire, RuntimeTargetWire,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
static UPLOAD_COUNTER: AtomicU64 = AtomicU64::new(1);
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -69,242 +52,6 @@ pub struct FileTreeUploadTargetPlan {
|
||||
target_sub_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct UploadFile {
|
||||
name: String,
|
||||
content_type: String,
|
||||
bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
async fn current_user_id(state: &AppState, context: &RequestContext) -> String {
|
||||
let actor_id = context.auth.actor_id.trim();
|
||||
if !actor_id.is_empty() && actor_id != "anonymous" {
|
||||
return actor_id.to_string();
|
||||
}
|
||||
if let Ok(user) = execute_retired_query_by_name(
|
||||
state.config(),
|
||||
context,
|
||||
"users:currentUser",
|
||||
json!({}),
|
||||
context.workspace.workspace_id.as_deref(),
|
||||
"media_current_user",
|
||||
)
|
||||
.await
|
||||
{
|
||||
for key in ["_id", "id"] {
|
||||
if let Some(user_id) = user
|
||||
.get(key)
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
return user_id.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
state.config().dev_user_id.clone()
|
||||
}
|
||||
|
||||
fn now_millis() -> u128 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis()
|
||||
}
|
||||
|
||||
fn new_asset_id() -> String {
|
||||
format!(
|
||||
"asset_{}_{}",
|
||||
now_millis(),
|
||||
UPLOAD_COUNTER.fetch_add(1, Ordering::Relaxed)
|
||||
)
|
||||
}
|
||||
|
||||
fn now_iso_like() -> String {
|
||||
OffsetDateTime::now_utc()
|
||||
.format(&Rfc3339)
|
||||
.unwrap_or_else(|_| "1970-01-01T00:00:00Z".into())
|
||||
}
|
||||
|
||||
fn asset_type(mime: &str) -> &'static str {
|
||||
if mime.starts_with("image/") {
|
||||
"image"
|
||||
} else if mime.starts_with("video/") {
|
||||
"video"
|
||||
} else if mime.starts_with("audio/") {
|
||||
"audio"
|
||||
} else {
|
||||
"file"
|
||||
}
|
||||
}
|
||||
|
||||
async fn record_upload_artifacts(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
user_id: &str,
|
||||
workspace_id: &str,
|
||||
document_id: &str,
|
||||
asset_id: &str,
|
||||
file: &UploadFile,
|
||||
asset_kind: &str,
|
||||
target_sub_path: Option<&str>,
|
||||
created: &Value,
|
||||
) -> Result<(), WebError> {
|
||||
let command = RuntimeCommandEnvelopeWire {
|
||||
name: "tree.resource.upload".into(),
|
||||
command_id: format!("resource_upload_{}", context.trace.request_id),
|
||||
idempotency_key: context.source.idempotency_key.clone(),
|
||||
actor: RuntimeActorWire {
|
||||
actor_type: context.auth.actor_type.clone(),
|
||||
actor_id: user_id.to_string(),
|
||||
session_id: context.auth.session_id.clone(),
|
||||
},
|
||||
source: RuntimeSourceWire {
|
||||
channel: context.source.channel.clone(),
|
||||
client: context.source.client.clone(),
|
||||
source_kind: None,
|
||||
root_uri: None,
|
||||
workspace_id: Some(workspace_id.to_string()),
|
||||
capabilities: Vec::new(),
|
||||
},
|
||||
target: Some(RuntimeTargetWire {
|
||||
workspace_id: Some(workspace_id.to_string()),
|
||||
page_id: Some(document_id.to_string()),
|
||||
block_id: Some(asset_id.to_string()),
|
||||
}),
|
||||
payload: json!({
|
||||
"assetId": asset_id,
|
||||
"workspaceId": workspace_id,
|
||||
"targetDocumentId": document_id,
|
||||
"targetSubPath": target_sub_path,
|
||||
"fileName": file.name,
|
||||
"fileSize": file.bytes.len(),
|
||||
"mimeType": file.content_type,
|
||||
"assetType": asset_kind,
|
||||
}),
|
||||
preflight_data: None,
|
||||
reason: Some("mnote-web media upload tree.resource.upload".into()),
|
||||
refs: vec!["file-tree-resource-upload".into()],
|
||||
dry_run: false,
|
||||
validate_only: false,
|
||||
};
|
||||
let runtime_context = runtime_context(context, Some(workspace_id));
|
||||
let plan = build_runtime_command_plan(context, Some(workspace_id), command.clone())?;
|
||||
let artifact_result = json!({
|
||||
"items": [created.clone()],
|
||||
});
|
||||
if let Some(artifacts) = build_runtime_command_artifact_plan(
|
||||
&runtime_context,
|
||||
&command,
|
||||
&plan,
|
||||
&artifact_result,
|
||||
&now_iso_like(),
|
||||
) {
|
||||
persist_runtime_command_artifacts(state.config(), context, &artifacts).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_upload_multipart(
|
||||
mut multipart: Multipart,
|
||||
) -> Result<(UploadFile, String, String, Option<String>), WebError> {
|
||||
let mut file: Option<UploadFile> = None;
|
||||
let mut workspace_id = String::new();
|
||||
let mut document_id = String::new();
|
||||
let mut mindmap_id: Option<String> = None;
|
||||
|
||||
while let Some(field) = multipart.next_field().await.map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
"media_upload_bad_multipart",
|
||||
format!("上传表单解析失败: {error}"),
|
||||
)
|
||||
})? {
|
||||
let name = field.name().unwrap_or_default().to_string();
|
||||
if name == "file" {
|
||||
let file_name = field
|
||||
.file_name()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or("附件")
|
||||
.to_string();
|
||||
let content_type = field
|
||||
.content_type()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or("application/octet-stream")
|
||||
.to_string();
|
||||
let bytes = field
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
"media_upload_file_read_failed",
|
||||
format!("读取上传文件失败: {error}"),
|
||||
)
|
||||
})?
|
||||
.to_vec();
|
||||
file = Some(UploadFile {
|
||||
name: file_name,
|
||||
content_type,
|
||||
bytes,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
let value = field.text().await.map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
"media_upload_field_read_failed",
|
||||
format!("读取上传字段失败: {error}"),
|
||||
)
|
||||
})?;
|
||||
match name.as_str() {
|
||||
"workspaceId" => workspace_id = value.trim().to_string(),
|
||||
"documentId" => document_id = value.trim().to_string(),
|
||||
"mindmapId" => {
|
||||
let trimmed = value.trim();
|
||||
if !trimmed.is_empty() {
|
||||
mindmap_id = Some(trimmed.to_string());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let file =
|
||||
file.ok_or_else(|| WebError::bad_request_code("media_upload_file_missing", "缺少 file"))?;
|
||||
if file.bytes.is_empty() || workspace_id.is_empty() || document_id.is_empty() {
|
||||
return Err(WebError::bad_request_code(
|
||||
"media_upload_required_missing",
|
||||
"缺少必要参数",
|
||||
));
|
||||
}
|
||||
Ok((file, workspace_id, document_id, mindmap_id))
|
||||
}
|
||||
|
||||
fn absolute_origin(headers: &HeaderMap) -> String {
|
||||
let proto = headers
|
||||
.get("x-forwarded-proto")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or("http");
|
||||
let host = headers
|
||||
.get(header::HOST)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or("127.0.0.1:3000");
|
||||
format!("{proto}://{host}")
|
||||
}
|
||||
|
||||
fn proxied_file_url(headers: &HeaderMap, raw: &str) -> String {
|
||||
let encoded = URL_SAFE_NO_PAD.encode(raw.as_bytes());
|
||||
format!(
|
||||
"{}/api/onlyoffice/proxy?u={encoded}",
|
||||
absolute_origin(headers)
|
||||
)
|
||||
}
|
||||
|
||||
fn trim_string(value: Option<&String>) -> Option<String> {
|
||||
value
|
||||
.map(String::as_str)
|
||||
@@ -353,128 +100,8 @@ fn document_for_target_row(
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn upload(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
multipart: Multipart,
|
||||
) -> Result<Response, WebError> {
|
||||
let (file, workspace_id, document_id, mindmap_id) = read_upload_multipart(multipart).await?;
|
||||
let user_id = current_user_id(&state, &context).await;
|
||||
let upload_url = execute_retired_mutation_by_name(
|
||||
state.config(),
|
||||
&context,
|
||||
"mediaAssets:generateUploadUrl",
|
||||
json!({ "userId": user_id }),
|
||||
Some(&workspace_id),
|
||||
None,
|
||||
"media_upload_generate_url",
|
||||
)
|
||||
.await?;
|
||||
let upload_url = upload_url.as_str().ok_or_else(|| {
|
||||
WebError::bad_gateway_code("media_upload_bad_upload_url", "Convex 未返回上传 URL")
|
||||
})?;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let upload_response = client
|
||||
.post(upload_url)
|
||||
.header(header::CONTENT_TYPE, file.content_type.as_str())
|
||||
.body(file.bytes.clone())
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| {
|
||||
WebError::bad_gateway_code(
|
||||
"media_upload_storage_failed",
|
||||
format!("上传到 Convex Files 失败: {error}"),
|
||||
)
|
||||
})?;
|
||||
let upload_status = upload_response.status();
|
||||
let upload_json: Value = upload_response.json().await.map_err(|error| {
|
||||
WebError::bad_gateway_code(
|
||||
"media_upload_storage_bad_response",
|
||||
format!("Convex Files 响应解析失败: {error}"),
|
||||
)
|
||||
.with_header("x-upstream-status", upload_status.as_u16().to_string())
|
||||
})?;
|
||||
if !upload_status.is_success() {
|
||||
return Err(WebError::bad_gateway_code(
|
||||
"media_upload_storage_status",
|
||||
format!("上传到 Convex Files 失败: {upload_json}"),
|
||||
)
|
||||
.with_header("x-upstream-status", upload_status.as_u16().to_string()));
|
||||
}
|
||||
let storage_id = upload_json
|
||||
.get("storageId")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| {
|
||||
WebError::bad_gateway_code(
|
||||
"media_upload_storage_id_missing",
|
||||
"Convex Files 缺少 storageId",
|
||||
)
|
||||
})?;
|
||||
|
||||
let target_sub_path = mindmap_id
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(|value| format!("mindmaps/{value}"));
|
||||
let id = new_asset_id();
|
||||
let kind = asset_type(&file.content_type);
|
||||
let created = execute_retired_mutation_by_name(
|
||||
state.config(),
|
||||
&context,
|
||||
"mediaAssets:createWithStorage",
|
||||
json!({
|
||||
"userId": user_id,
|
||||
"storageId": storage_id,
|
||||
"targetSubPath": target_sub_path,
|
||||
"asset": {
|
||||
"id": id,
|
||||
"workspace_id": workspace_id,
|
||||
"document_id": document_id,
|
||||
"asset_type": kind,
|
||||
"file_name": file.name,
|
||||
"file_size": file.bytes.len(),
|
||||
"mime_type": file.content_type,
|
||||
}
|
||||
}),
|
||||
Some(&workspace_id),
|
||||
None,
|
||||
"media_upload_create_asset",
|
||||
)
|
||||
.await?;
|
||||
|
||||
let asset_id = created
|
||||
.get("id")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
if let Err(error) = record_upload_artifacts(
|
||||
&state,
|
||||
&context,
|
||||
&user_id,
|
||||
&workspace_id,
|
||||
&document_id,
|
||||
&asset_id,
|
||||
&file,
|
||||
&kind,
|
||||
target_sub_path.as_deref(),
|
||||
&created,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(
|
||||
error = %error.message(),
|
||||
asset_id = %asset_id,
|
||||
"media upload tree.resource.upload artifacts 记录失败,主上传结果继续返回"
|
||||
);
|
||||
}
|
||||
Ok(Json(json!({
|
||||
"asset": created,
|
||||
"mindmapUrl": format!("asset:{asset_id}"),
|
||||
}))
|
||||
.into_response())
|
||||
pub async fn upload(Extension(context): Extension<RequestContext>) -> Response {
|
||||
retired_convex_media_response(&context, "upload", None)
|
||||
}
|
||||
|
||||
pub async fn filetree_upload_target_preflight(
|
||||
@@ -529,63 +156,39 @@ pub async fn filetree_upload_target_preflight(
|
||||
}
|
||||
|
||||
pub async fn sign(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Query(query): Query<MediaSignQuery>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Response, WebError> {
|
||||
) -> Response {
|
||||
let asset_id = query
|
||||
.asset_id
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| WebError::bad_request_code("media_sign_asset_missing", "缺少 assetId"))?;
|
||||
let user_id = current_user_id(&state, &context).await;
|
||||
let asset = execute_retired_query_by_name(
|
||||
state.config(),
|
||||
&context,
|
||||
"mediaAssets:getById",
|
||||
json!({ "userId": user_id, "id": asset_id }),
|
||||
None,
|
||||
"media_sign_get_asset",
|
||||
)
|
||||
.await?;
|
||||
if asset.is_null() {
|
||||
return Err(WebError::new(
|
||||
axum::http::StatusCode::NOT_FOUND,
|
||||
"media_asset_not_found",
|
||||
"资源不存在",
|
||||
));
|
||||
}
|
||||
let refreshed = execute_retired_mutation_by_name(
|
||||
state.config(),
|
||||
&context,
|
||||
"mediaAssets:refreshUrl",
|
||||
json!({ "userId": user_id, "id": asset_id }),
|
||||
None,
|
||||
None,
|
||||
"media_sign_refresh_url",
|
||||
.filter(|value| !value.is_empty());
|
||||
retired_convex_media_response(&context, "sign", asset_id)
|
||||
}
|
||||
|
||||
fn retired_convex_media_response(
|
||||
context: &RequestContext,
|
||||
operation: &str,
|
||||
asset_id: Option<&str>,
|
||||
) -> Response {
|
||||
(
|
||||
StatusCode::GONE,
|
||||
Json(json!({
|
||||
"ok": false,
|
||||
"code": "mnote_media_convex_retired",
|
||||
"error": "旧 Convex Files media route 已退役",
|
||||
"message": "旧 /api/media Convex Files 上传与签名链已退役;local-first 附件请使用 /api/local-folder/assets/upload 与 /api/local-folder/files/open。",
|
||||
"operation": operation,
|
||||
"assetId": asset_id,
|
||||
"replacement": {
|
||||
"upload": "/api/local-folder/assets/upload",
|
||||
"open": "/api/local-folder/files/open",
|
||||
"preflight": "/api/tree/filetree/upload-target-preflight"
|
||||
},
|
||||
"requestId": context.trace.request_id,
|
||||
"traceId": context.trace.trace_id,
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
let signed_url = refreshed
|
||||
.get("signedUrl")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| WebError::bad_gateway_code("media_sign_url_missing", "生成签名链接失败"))?;
|
||||
|
||||
Ok(Json(json!({
|
||||
"signedUrl": proxied_file_url(&headers, signed_url),
|
||||
"asset": {
|
||||
"id": asset.get("id").cloned().unwrap_or(Value::Null),
|
||||
"document_id": asset.get("document_id").cloned().unwrap_or(Value::Null),
|
||||
"workspace_id": asset.get("workspace_id").cloned().unwrap_or(Value::Null),
|
||||
"file_name": asset.get("file_name").cloned().unwrap_or(Value::Null),
|
||||
"mime_type": asset.get("mime_type").cloned().unwrap_or(Value::Null),
|
||||
"file_size": asset.get("file_size").cloned().unwrap_or(Value::Null),
|
||||
"storage_id": asset.get("storage_id").cloned().unwrap_or(Value::Null),
|
||||
"updated_at": asset.get("updated_at").cloned().unwrap_or(Value::Null),
|
||||
}
|
||||
}))
|
||||
.into_response())
|
||||
.into_response()
|
||||
}
|
||||
|
||||
@@ -252,6 +252,10 @@ pub fn build_router(state: AppState) -> Router {
|
||||
"/api/mnote-browser-runtime/sidebar-tree-runtime.js",
|
||||
get(web_shell::sidebar_tree_runtime_asset),
|
||||
)
|
||||
.route(
|
||||
"/api/mnote-browser-runtime/local-folder-event-bus-runtime.js",
|
||||
get(web_shell::local_folder_event_bus_runtime_asset),
|
||||
)
|
||||
.route(
|
||||
"/api/mnote-browser-runtime/tree-live-controller.js",
|
||||
get(web_shell::tree_live_controller_runtime_asset),
|
||||
@@ -723,6 +727,7 @@ mod tests {
|
||||
use crate::context::RequestContext;
|
||||
use axum::body::{to_bytes, Body};
|
||||
use axum::http::{Request, StatusCode};
|
||||
use axum::Extension;
|
||||
use axum::Router;
|
||||
use serde_json::{json, Value};
|
||||
use std::fs;
|
||||
@@ -848,6 +853,38 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn legacy_media_routes_return_retired_guard() {
|
||||
for (method, path, operation) in [
|
||||
("POST", "/api/media/upload", "upload"),
|
||||
("GET", "/api/media/sign?assetId=asset_1", "sign"),
|
||||
] {
|
||||
let request = Request::builder()
|
||||
.method(method)
|
||||
.uri(path)
|
||||
.body(Body::empty())
|
||||
.expect("request");
|
||||
let context =
|
||||
RequestContext::from_http_parts(request.method(), request.uri(), request.headers());
|
||||
let response = app(false)
|
||||
.layer(Extension(context))
|
||||
.oneshot(request)
|
||||
.await
|
||||
.expect("response");
|
||||
assert_eq!(response.status(), StatusCode::GONE);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body bytes");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json body");
|
||||
assert_eq!(payload["code"], "mnote_media_convex_retired");
|
||||
assert_eq!(payload["operation"], operation);
|
||||
assert_eq!(
|
||||
payload["replacement"]["upload"],
|
||||
"/api/local-folder/assets/upload"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn office_preview_page_serves_lightweight_viewer_shell() {
|
||||
let response = app(false)
|
||||
@@ -1144,8 +1181,12 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
alice_payload["result"]["localOcrPreferences"]["localOcr.autoEnabled"],
|
||||
true
|
||||
false
|
||||
);
|
||||
assert!(alice_payload["result"]["sources"]
|
||||
.as_object()
|
||||
.and_then(|sources| sources.get("localOcr.autoEnabled"))
|
||||
.is_none());
|
||||
|
||||
let mut bob_get = Request::builder()
|
||||
.uri(format!("/api/ui/preferences/effective?workspaceId=workspace-ai-a&sourceKind=local_folder&rootUri={root_uri}&documentId=local-md:ai.md"))
|
||||
@@ -1201,6 +1242,7 @@ mod tests {
|
||||
"/api/mnote-browser-runtime/sidebar-page-ai-target-runtime.js",
|
||||
"/api/mnote-browser-runtime/sidebar-page-settings-runtime.js",
|
||||
"/api/mnote-browser-runtime/sidebar-tree-runtime.js",
|
||||
"/api/mnote-browser-runtime/local-folder-event-bus-runtime.js",
|
||||
"/api/mnote-browser-runtime/tree-live-controller.js",
|
||||
"/api/mnote-browser-runtime/tree-shell-runtime.js",
|
||||
"/api/mnote-browser-runtime/tree-shell-render-runtime.js",
|
||||
|
||||
@@ -224,7 +224,7 @@ async fn record_media_empty_trash_artifacts(
|
||||
let plan = RuntimeCommandExecutionPlan {
|
||||
command_name: command.name.clone(),
|
||||
command_id: command.command_id.clone(),
|
||||
function_name: "mediaAssets:emptyTrashByWorkspace".into(),
|
||||
function_name: command.name.clone(),
|
||||
workspace_id: Some(workspace_id.to_string()),
|
||||
request_id: context.trace.request_id.clone(),
|
||||
trace_id: context.trace.trace_id.clone(),
|
||||
|
||||
@@ -7,16 +7,15 @@ use crate::routes::query_support::{
|
||||
resolve_effective_workspace_id,
|
||||
};
|
||||
use crate::routes::web_shell::load_sidebar_tree_html;
|
||||
use crate::routes::{evidence, local_folder_source, local_search_index};
|
||||
use crate::routes::{local_folder_source, local_search_index};
|
||||
use crate::ssr::pages::search::SearchPage;
|
||||
use axum::Json;
|
||||
use axum::extract::{Extension, Query, State};
|
||||
use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
|
||||
use axum::response::{Html, IntoResponse, Response};
|
||||
use axum::Json;
|
||||
use bridge_runtime::RuntimeQueryEnvelopeWire;
|
||||
use core_protocol::{EvidenceSearchMode, EvidenceSearchResult};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Value, json};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
const HEADER_MNOTE_WEB_OWNER: &str = "x-mnote-web-owner";
|
||||
const HEADER_QUERY_NAME: &str = "x-query-name";
|
||||
@@ -183,100 +182,60 @@ pub async fn documents(
|
||||
None
|
||||
};
|
||||
|
||||
let (result, evidence_results) =
|
||||
if body.source_kind.as_deref().map(str::trim) == Some("local_folder") {
|
||||
let root_uri = body
|
||||
.root_uri
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| {
|
||||
WebError::bad_request_code("local_search_root_required", "本地搜索缺少 rootUri")
|
||||
.with_context(&context)
|
||||
})?;
|
||||
let root_path = local_folder_source::ensure_local_workspace_read_access_with_state(
|
||||
&state, &context, root_uri,
|
||||
)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
let user_settings = resolve_local_index_user_settings(
|
||||
&state,
|
||||
&context,
|
||||
&effective_workspace_id,
|
||||
&root_path,
|
||||
)?;
|
||||
let effective_settings = local_search_index::effective_local_index_settings_for_root(
|
||||
state.control_plane(),
|
||||
&effective_workspace_id,
|
||||
&root_path,
|
||||
)?;
|
||||
let result = local_search_index::query_local_search_index_with_settings(
|
||||
&root_path,
|
||||
root_uri,
|
||||
&effective_workspace_id,
|
||||
&effective_settings,
|
||||
&user_settings,
|
||||
&normalized_query,
|
||||
page_id.as_deref(),
|
||||
body.limit.unwrap_or(30),
|
||||
filters.title_only.unwrap_or(false),
|
||||
filters.exact.unwrap_or(false),
|
||||
filters.include_ocr.unwrap_or(false),
|
||||
)?;
|
||||
let evidence_results = evidence::evidence_results_from_local_search(
|
||||
&result,
|
||||
&root_path,
|
||||
root_uri,
|
||||
EvidenceSearchMode::Hybrid,
|
||||
&normalized_query,
|
||||
);
|
||||
let limit = body.limit.unwrap_or(30).max(1) as usize;
|
||||
let direct_evidence_results = if !filters.title_only.unwrap_or(false) {
|
||||
local_search_index::query_evidence_sqlite_results_with_mode(
|
||||
&root_path,
|
||||
&normalized_query,
|
||||
page_id.as_deref(),
|
||||
body.limit.unwrap_or(30),
|
||||
filters.exact.unwrap_or(false),
|
||||
)?
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter(|evidence| {
|
||||
let path = evidence
|
||||
.source
|
||||
.resource_path
|
||||
.as_deref()
|
||||
.unwrap_or(evidence.source.owner_document_path.as_str());
|
||||
local_search_index::local_index_relative_path_is_included(path, &user_settings)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let (result, evidence_results) = merge_local_search_with_evidence_results(
|
||||
result,
|
||||
evidence_results,
|
||||
direct_evidence_results,
|
||||
limit,
|
||||
);
|
||||
(result, evidence_results)
|
||||
} else {
|
||||
let result = load_search_results_with_filters(
|
||||
state.config(),
|
||||
&context,
|
||||
&effective_workspace_id,
|
||||
&normalized_query,
|
||||
page_id,
|
||||
body.limit.unwrap_or(30),
|
||||
filters,
|
||||
)
|
||||
.await?;
|
||||
(result, Vec::new())
|
||||
};
|
||||
let result = if body.source_kind.as_deref().map(str::trim) == Some("local_folder") {
|
||||
let root_uri = body
|
||||
.root_uri
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| {
|
||||
WebError::bad_request_code("local_search_root_required", "本地搜索缺少 rootUri")
|
||||
.with_context(&context)
|
||||
})?;
|
||||
let root_path = local_folder_source::ensure_local_workspace_read_access_with_state(
|
||||
&state, &context, root_uri,
|
||||
)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
let user_settings = resolve_local_index_user_settings(
|
||||
&state,
|
||||
&context,
|
||||
&effective_workspace_id,
|
||||
&root_path,
|
||||
)?;
|
||||
let effective_settings = local_search_index::effective_local_index_settings_for_root(
|
||||
state.control_plane(),
|
||||
&effective_workspace_id,
|
||||
&root_path,
|
||||
)?;
|
||||
local_search_index::query_local_search_index_with_settings(
|
||||
&root_path,
|
||||
root_uri,
|
||||
&effective_workspace_id,
|
||||
&effective_settings,
|
||||
&user_settings,
|
||||
&normalized_query,
|
||||
page_id.as_deref(),
|
||||
body.limit.unwrap_or(30),
|
||||
filters.title_only.unwrap_or(false),
|
||||
filters.exact.unwrap_or(false),
|
||||
filters.include_ocr.unwrap_or(false),
|
||||
)?
|
||||
} else {
|
||||
load_search_results_with_filters(
|
||||
state.config(),
|
||||
&context,
|
||||
&effective_workspace_id,
|
||||
&normalized_query,
|
||||
page_id,
|
||||
body.limit.unwrap_or(30),
|
||||
filters,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
let results = result
|
||||
.get("results")
|
||||
.cloned()
|
||||
.unwrap_or(Value::Array(vec![]));
|
||||
let results = attach_evidence_to_search_results(results, &evidence_results);
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
stamp_search_headers(&mut headers);
|
||||
@@ -285,13 +244,20 @@ pub async fn documents(
|
||||
headers,
|
||||
Json(json!({
|
||||
"results": results,
|
||||
"evidence": evidence_results,
|
||||
"evidence": [],
|
||||
"projectionOwner": result.get("projectionOwner").cloned().unwrap_or_else(|| json!("rust-kernel")),
|
||||
"recent": result.get("recentChanges").cloned().unwrap_or_else(|| Value::Array(vec![])),
|
||||
"meta": {
|
||||
"owner": "mnote-web",
|
||||
"projectionOwner": result.get("projectionOwner").cloned().unwrap_or_else(|| json!("rust-kernel")),
|
||||
"queryName": "search.documents.query",
|
||||
"boundary": {
|
||||
"kind": "ordinary_local_search",
|
||||
"knowledgeRag": false,
|
||||
"evidenceSqliteFallback": false,
|
||||
"liteParseFallback": false,
|
||||
"ocrSidecarFallback": false
|
||||
},
|
||||
"degraded": result.get("degraded").cloned().unwrap_or_else(|| json!(false)),
|
||||
"degradedReason": result.get("degradedReason").cloned().unwrap_or(Value::Null),
|
||||
"requestId": context.trace.request_id,
|
||||
@@ -475,158 +441,6 @@ pub async fn update_local_index_settings(
|
||||
))
|
||||
}
|
||||
|
||||
fn merge_local_search_with_evidence_results(
|
||||
mut result: Value,
|
||||
mut evidence_results: Vec<EvidenceSearchResult>,
|
||||
direct_evidence_results: Vec<EvidenceSearchResult>,
|
||||
limit: usize,
|
||||
) -> (Value, Vec<EvidenceSearchResult>) {
|
||||
if direct_evidence_results.is_empty() {
|
||||
return (result, evidence_results);
|
||||
}
|
||||
let original_items = result
|
||||
.get("results")
|
||||
.and_then(Value::as_array)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let original_evidence_results = std::mem::take(&mut evidence_results);
|
||||
let mut result_items = Vec::new();
|
||||
let mut merged_evidence_results = Vec::new();
|
||||
let mut seen_result_ids = std::collections::HashSet::new();
|
||||
let mut seen_evidence_ids = std::collections::HashSet::new();
|
||||
let mut seen_paths = std::collections::HashSet::new();
|
||||
for evidence in direct_evidence_results {
|
||||
if result_items.len() >= limit || !seen_evidence_ids.insert(evidence.evidence_id.clone()) {
|
||||
continue;
|
||||
}
|
||||
let item = search_result_from_evidence(&evidence);
|
||||
if let Some(id) = item.get("id").and_then(Value::as_str) {
|
||||
seen_result_ids.insert(id.to_string());
|
||||
}
|
||||
if let Some(path) = search_result_dedupe_path(&item) {
|
||||
seen_paths.insert(path);
|
||||
}
|
||||
result_items.push(item);
|
||||
merged_evidence_results.push(evidence);
|
||||
}
|
||||
for (index, item) in original_items.into_iter().enumerate() {
|
||||
if result_items.len() >= limit {
|
||||
break;
|
||||
}
|
||||
let item_id = item
|
||||
.get("id")
|
||||
.and_then(Value::as_str)
|
||||
.map(ToOwned::to_owned)
|
||||
.unwrap_or_default();
|
||||
if !item_id.is_empty() && !seen_result_ids.insert(item_id) {
|
||||
continue;
|
||||
}
|
||||
if let Some(path) = search_result_dedupe_path(&item) {
|
||||
if !seen_paths.insert(path) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some(evidence) = original_evidence_results.get(index).cloned() {
|
||||
merged_evidence_results.push(evidence);
|
||||
}
|
||||
result_items.push(item);
|
||||
}
|
||||
if let Some(map) = result.as_object_mut() {
|
||||
map.insert("results".into(), Value::Array(result_items));
|
||||
}
|
||||
(result, merged_evidence_results)
|
||||
}
|
||||
|
||||
fn search_result_dedupe_path(item: &Value) -> Option<String> {
|
||||
let source_kind = item
|
||||
.get("sourceKind")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default();
|
||||
if source_kind != "local_folder" {
|
||||
return None;
|
||||
}
|
||||
item.get("path")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
fn search_result_from_evidence(evidence: &EvidenceSearchResult) -> Value {
|
||||
let source = &evidence.source;
|
||||
let evidence_value = serde_json::to_value(evidence).unwrap_or(Value::Null);
|
||||
let resource_path = source
|
||||
.resource_path
|
||||
.as_deref()
|
||||
.unwrap_or(source.owner_document_path.as_str());
|
||||
let title = std::path::Path::new(resource_path)
|
||||
.file_name()
|
||||
.and_then(|value| value.to_str())
|
||||
.unwrap_or(resource_path)
|
||||
.to_string();
|
||||
let resource_type = match source.resource_kind {
|
||||
core_protocol::EvidenceResourceKind::Markdown => "markdown",
|
||||
core_protocol::EvidenceResourceKind::Pdf => "pdf",
|
||||
core_protocol::EvidenceResourceKind::Image => "image",
|
||||
core_protocol::EvidenceResourceKind::Office => "office",
|
||||
core_protocol::EvidenceResourceKind::Mindmap => "mindmap",
|
||||
core_protocol::EvidenceResourceKind::RawFile => "resource",
|
||||
};
|
||||
json!({
|
||||
"id": format!("evidence:{}", evidence.evidence_id),
|
||||
"documentId": source.owner_document_id,
|
||||
"title": title,
|
||||
"path": source.owner_document_path,
|
||||
"resourceType": resource_type,
|
||||
"sourceKind": "local_folder",
|
||||
"rootUri": source.root_uri,
|
||||
"snippet": evidence.quote,
|
||||
"score": evidence.score,
|
||||
"matchInfo": evidence.match_info,
|
||||
"publicPath": source.open_action.url,
|
||||
"evidence": evidence_value,
|
||||
"source": {
|
||||
"locator": source
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn attach_evidence_to_search_results(
|
||||
results: Value,
|
||||
evidence_results: &[EvidenceSearchResult],
|
||||
) -> Value {
|
||||
let Value::Array(items) = results else {
|
||||
return results;
|
||||
};
|
||||
Value::Array(
|
||||
items
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, item)| {
|
||||
let Some(evidence) = evidence_results.get(index) else {
|
||||
return item;
|
||||
};
|
||||
let mut item = item;
|
||||
if let Some(map) = item.as_object_mut() {
|
||||
if map.get("evidence").is_some() {
|
||||
return item;
|
||||
}
|
||||
let evidence_value = serde_json::to_value(evidence).unwrap_or(Value::Null);
|
||||
map.insert("evidence".into(), evidence_value);
|
||||
let source = map.entry("source").or_insert_with(|| json!({}));
|
||||
if let Some(source_map) = source.as_object_mut() {
|
||||
source_map.insert(
|
||||
"locator".into(),
|
||||
serde_json::to_value(&evidence.source).unwrap_or(Value::Null),
|
||||
);
|
||||
}
|
||||
}
|
||||
item
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn resolve_local_index_user_settings(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
@@ -940,12 +754,12 @@ fn stamp_search_headers(headers: &mut HeaderMap) {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app::{AppConfig, AppState, build_app};
|
||||
use crate::app::{build_app, AppConfig, AppState};
|
||||
use crate::routes::local_search_index;
|
||||
use axum::body::{Body, to_bytes};
|
||||
use axum::body::{to_bytes, Body};
|
||||
use axum::http::{Request, StatusCode};
|
||||
use control_plane::{DirectoryGrantInput, UpsertUserInput};
|
||||
use serde_json::{Value, json};
|
||||
use serde_json::{json, Value};
|
||||
use std::fs;
|
||||
use tower::util::ServiceExt;
|
||||
|
||||
@@ -1194,41 +1008,38 @@ mod tests {
|
||||
assert_eq!(home["sourceKind"].as_str(), Some("local_folder"));
|
||||
assert_eq!(home["resourceType"].as_str(), Some("markdown"));
|
||||
assert_eq!(
|
||||
home["source"]["locator"]["schema"].as_str(),
|
||||
Some("mnote.evidence_locator.v1")
|
||||
payload["meta"]["boundary"]["kind"].as_str(),
|
||||
Some("ordinary_local_search")
|
||||
);
|
||||
assert_eq!(
|
||||
home["evidence"]["source"]["ownerDocumentPath"].as_str(),
|
||||
Some("README.md")
|
||||
payload["meta"]["boundary"]["knowledgeRag"].as_bool(),
|
||||
Some(false)
|
||||
);
|
||||
assert!(!payload["evidence"].as_array().expect("evidence").is_empty());
|
||||
assert!(
|
||||
home["tags"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|tag| tag.as_str() == Some("alpha"))
|
||||
);
|
||||
assert!(
|
||||
home["backlinks"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|link| link.as_str() == Some("Daily"))
|
||||
);
|
||||
assert!(
|
||||
home["resourceRefs"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|reference| reference.as_str() == Some("assets/spec.pdf"))
|
||||
);
|
||||
assert!(
|
||||
root.join(".mnote")
|
||||
.join("index")
|
||||
.join("search-index.json")
|
||||
.exists()
|
||||
assert_eq!(
|
||||
payload["meta"]["boundary"]["evidenceSqliteFallback"].as_bool(),
|
||||
Some(false)
|
||||
);
|
||||
assert!(payload["evidence"].as_array().expect("evidence").is_empty());
|
||||
assert!(home["tags"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|tag| tag.as_str() == Some("alpha")));
|
||||
assert!(home["backlinks"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|link| link.as_str() == Some("Daily")));
|
||||
assert!(home["resourceRefs"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|reference| reference.as_str() == Some("assets/spec.pdf")));
|
||||
assert!(root
|
||||
.join(".mnote")
|
||||
.join("index")
|
||||
.join("search-index.json")
|
||||
.exists());
|
||||
let evidence_db = root.join(".mnote").join("index").join("evidence.sqlite");
|
||||
assert!(evidence_db.exists(), "evidence sqlite should be built");
|
||||
let connection = rusqlite::Connection::open(&evidence_db).expect("open evidence sqlite");
|
||||
@@ -1258,19 +1069,17 @@ mod tests {
|
||||
locator["schema"].as_str(),
|
||||
Some("mnote.evidence_locator.v1")
|
||||
);
|
||||
assert!(
|
||||
payload["recent"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|item| item["documentId"].as_str() == Some("local-md:README.md"))
|
||||
);
|
||||
assert!(payload["recent"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|item| item["documentId"].as_str() == Some("local-md:README.md")));
|
||||
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn search_documents_local_folder_includes_evidence_sqlite_body_hits() {
|
||||
async fn search_documents_local_folder_does_not_promote_evidence_sqlite_body_hits() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-local-search-evidence-route-{}",
|
||||
std::process::id()
|
||||
@@ -1350,20 +1159,18 @@ mod tests {
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
let results = payload["results"].as_array().expect("results");
|
||||
let hit = results
|
||||
assert!(!results.iter().any(|item| item["id"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.starts_with("evidence:")));
|
||||
assert!(results
|
||||
.iter()
|
||||
.find(|item| {
|
||||
item["snippet"]
|
||||
.as_str()
|
||||
.unwrap_or("")
|
||||
.contains("BodyOnlyEvidenceToken")
|
||||
})
|
||||
.expect("evidence sqlite body hit should be promoted to search result");
|
||||
assert_eq!(hit["resourceType"].as_str(), Some("markdown"));
|
||||
.any(|item| item["resourceType"].as_str() == Some("markdown")));
|
||||
assert_eq!(
|
||||
hit["evidence"]["source"]["schema"].as_str(),
|
||||
Some("mnote.evidence_locator.v1")
|
||||
payload["meta"]["boundary"]["evidenceSqliteFallback"].as_bool(),
|
||||
Some(false)
|
||||
);
|
||||
assert!(payload["evidence"].as_array().expect("evidence").is_empty());
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
@@ -1382,6 +1189,15 @@ mod tests {
|
||||
.expect("manifest");
|
||||
fs::write(root.join("README.md"), "# Refresh\nrefresh-token\n").expect("readme");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
local_search_index::write_local_index_settings(
|
||||
&root,
|
||||
&[String::from(".")],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("settings");
|
||||
|
||||
let response = app()
|
||||
.oneshot(
|
||||
@@ -1582,6 +1398,15 @@ mod tests {
|
||||
)
|
||||
.expect("child");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
local_search_index::write_local_index_settings(
|
||||
&root,
|
||||
&[String::from(".")],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("settings");
|
||||
let encoded_root = query_escape(&root_uri);
|
||||
|
||||
let backlinks_response = app()
|
||||
@@ -1607,13 +1432,11 @@ mod tests {
|
||||
backlinks_payload["meta"]["queryName"].as_str(),
|
||||
Some("search.local_index.backlinks")
|
||||
);
|
||||
assert!(
|
||||
backlinks_payload["result"]["backlinks"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|item| item["documentId"].as_str() == Some("local-md:README.md"))
|
||||
);
|
||||
assert!(backlinks_payload["result"]["backlinks"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|item| item["documentId"].as_str() == Some("local-md:README.md")));
|
||||
|
||||
let tags_response = app()
|
||||
.oneshot(
|
||||
|
||||
@@ -4323,7 +4323,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tree_commands_keep_documents_alias_mapping_for_runtime_plan() {
|
||||
fn tree_commands_use_protocol_names_in_runtime_plan() {
|
||||
let context = RequestContext::from_http_parts(
|
||||
&Method::POST,
|
||||
&"/api/tree/commands".parse::<Uri>().expect("uri"),
|
||||
@@ -4354,14 +4354,8 @@ mod tests {
|
||||
let compat_create_plan =
|
||||
build_runtime_command_plan(&context, Some("ws_demo"), compat_create_wire)
|
||||
.expect("compat create plan");
|
||||
assert_eq!(
|
||||
tree_create_plan.function_name,
|
||||
"documents:createWithParentReference"
|
||||
);
|
||||
assert_eq!(
|
||||
tree_create_plan.function_name,
|
||||
compat_create_plan.function_name
|
||||
);
|
||||
assert_eq!(tree_create_plan.function_name, "tree.node.create");
|
||||
assert_eq!(compat_create_plan.function_name, "documents.create");
|
||||
|
||||
let tree_rename_wire = create_command_wire(
|
||||
&context,
|
||||
@@ -4384,11 +4378,8 @@ mod tests {
|
||||
let compat_rename_plan =
|
||||
build_runtime_command_plan(&context, Some("ws_demo"), compat_rename_wire)
|
||||
.expect("compat rename plan");
|
||||
assert_eq!(tree_rename_plan.function_name, "documents:updateTitle");
|
||||
assert_eq!(
|
||||
tree_rename_plan.function_name,
|
||||
compat_rename_plan.function_name
|
||||
);
|
||||
assert_eq!(tree_rename_plan.function_name, "tree.node.rename");
|
||||
assert_eq!(compat_rename_plan.function_name, "documents.title.update");
|
||||
|
||||
let tree_move_wire = create_command_wire(
|
||||
&context,
|
||||
@@ -4411,8 +4402,8 @@ mod tests {
|
||||
let compat_move_plan =
|
||||
build_runtime_command_plan(&context, Some("ws_demo"), compat_move_wire)
|
||||
.expect("compat move plan");
|
||||
assert_eq!(tree_move_plan.function_name, "documents:move");
|
||||
assert_eq!(tree_move_plan.function_name, compat_move_plan.function_name);
|
||||
assert_eq!(tree_move_plan.function_name, "tree.subtree.move");
|
||||
assert_eq!(compat_move_plan.function_name, "documents.move");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -282,7 +282,6 @@ fn apply_preference_records(
|
||||
"source_family".to_string(),
|
||||
"workspace".to_string(),
|
||||
"document".to_string(),
|
||||
"localOcr".to_string(),
|
||||
];
|
||||
for preference in preferences {
|
||||
if preference.scope_kind.starts_with("ai.") && !scope_kinds.contains(&preference.scope_kind)
|
||||
@@ -301,7 +300,6 @@ fn apply_preference_records(
|
||||
"workspace" => preference.scope_id.trim() == scope.workspace_id,
|
||||
"document" => preference.scope_id.trim() == scope.document_id,
|
||||
kind if kind.starts_with("ai.") => preference.scope_id.trim() == scope.workspace_id,
|
||||
"localOcr" => preference.scope_id.trim() == scope.workspace_id,
|
||||
_ => false,
|
||||
};
|
||||
if !scope_matches {
|
||||
@@ -319,8 +317,8 @@ fn apply_preference_records(
|
||||
continue;
|
||||
}
|
||||
if preference.key.starts_with("localOcr.") {
|
||||
local_ocr_preferences.insert(preference.key.clone(), value);
|
||||
sources.insert(preference.key.clone(), scope_kind.to_string());
|
||||
local_ocr_preferences.insert(preference.key.clone(), Value::Bool(false));
|
||||
sources.insert(preference.key.clone(), "retired".to_string());
|
||||
continue;
|
||||
}
|
||||
if let Some(content_type) = page_width_content_type_for_key(&preference.key) {
|
||||
@@ -381,7 +379,7 @@ fn preference_scope_for_key(key: &str, scope: &PagePreferenceScope) -> Option<(S
|
||||
}
|
||||
}
|
||||
if trimmed.starts_with("localOcr.") {
|
||||
return Some(("localOcr".to_string(), scope.workspace_id.clone()));
|
||||
return None;
|
||||
}
|
||||
if page_width_content_type_for_key(key).is_some() {
|
||||
return Some(("global".to_string(), "default".to_string()));
|
||||
@@ -416,11 +414,7 @@ fn preference_scope_for_key(key: &str, scope: &PagePreferenceScope) -> Option<(S
|
||||
}
|
||||
|
||||
fn preference_workspace_for_scope(workspace_id: &str, scope_kind: &str) -> Option<String> {
|
||||
if scope_kind == "workspace"
|
||||
|| scope_kind == "document"
|
||||
|| scope_kind.starts_with("ai.")
|
||||
|| scope_kind == "localOcr"
|
||||
{
|
||||
if scope_kind == "workspace" || scope_kind == "document" || scope_kind.starts_with("ai.") {
|
||||
Some(workspace_id.to_string())
|
||||
} else {
|
||||
None
|
||||
@@ -428,11 +422,7 @@ fn preference_workspace_for_scope(workspace_id: &str, scope_kind: &str) -> Optio
|
||||
}
|
||||
|
||||
fn preference_source_kind_for_scope(source_kind: &str, scope_kind: &str) -> Option<String> {
|
||||
if scope_kind == "workspace"
|
||||
|| scope_kind == "document"
|
||||
|| scope_kind.starts_with("ai.")
|
||||
|| scope_kind == "localOcr"
|
||||
{
|
||||
if scope_kind == "workspace" || scope_kind == "document" || scope_kind.starts_with("ai.") {
|
||||
Some(source_kind.to_string())
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -2328,6 +2328,20 @@ pub async fn filetree_selection_runtime_asset() -> Response {
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
}
|
||||
|
||||
pub async fn local_folder_event_bus_runtime_asset() -> Response {
|
||||
const JS: &str = include_str!("../../browser/local-folder-event-bus-runtime.js");
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(browser_runtime_js_body(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
}
|
||||
|
||||
pub async fn tree_live_controller_runtime_asset() -> Response {
|
||||
const JS: &str = include_str!("../../browser/tree-live-controller.js");
|
||||
Response::builder()
|
||||
@@ -3632,11 +3646,11 @@ mod tests {
|
||||
assert!(runtime.contains("document-resource-tab-runtime.js"));
|
||||
assert!(resource_runtime.contains("resourceTabBadgeKind(input, kind)"));
|
||||
assert!(resource_runtime.contains("后台任务"));
|
||||
assert!(resource_runtime.contains("data-mnote-local-ocr-task-tab"));
|
||||
assert!(resource_runtime.contains("data-mnote-local-ocr-task-clear-completed"));
|
||||
assert!(resource_runtime.contains("data-mnote-knowledge-rag-task-tab"));
|
||||
assert!(resource_runtime.contains("data-mnote-knowledge-rag-task-clear-completed"));
|
||||
assert!(resource_runtime.contains("role=\"progressbar\""));
|
||||
assert!(resource_runtime.contains("localOcrTaskCategory(job)"));
|
||||
assert!(resource_runtime.contains("localOcrTaskProgress(job)"));
|
||||
assert!(resource_runtime.contains("knowledgeRagTaskCategory(job)"));
|
||||
assert!(resource_runtime.contains("knowledgeRagTaskProgress(job)"));
|
||||
assert!(resource_runtime.contains("mnote.open_editors_snapshot.v1"));
|
||||
assert!(runtime.contains("getOpenEditorsSnapshot"));
|
||||
assert!(resource_runtime.contains("bindMainEditorTabStrip"));
|
||||
@@ -3734,6 +3748,11 @@ mod tests {
|
||||
assert!(session_runtime.contains("mnote.localFolder.selfChangeSuppressions.v1"));
|
||||
assert!(session_runtime.contains("ensureLocalFolderSelfChangeSuppressions()"));
|
||||
assert!(session_runtime.contains("markLocalFolderSelfChangeSuppression(session);"));
|
||||
assert!(session_runtime.contains("data-mnote-page-body-local-compat-fallback"));
|
||||
assert!(session_runtime.contains("data-mnote-page-body-hard-guard"));
|
||||
assert!(session_runtime.contains("local_compat_fallback"));
|
||||
assert!(runtime.contains("data-mnote-page-body-local-compat-fallback"));
|
||||
assert!(runtime.contains("data-mnote-page-body-hard-guard"));
|
||||
assert!(session_runtime.contains("const suppressibleSelfWrite = kind.includes('Create')"));
|
||||
assert!(session_runtime.contains("|| kind.includes('Modify(Data')"));
|
||||
assert!(session_runtime.contains("|| kind.includes('Modify(Any')"));
|
||||
@@ -3765,8 +3784,9 @@ mod tests {
|
||||
resource_runtime.contains("const refreshExistingOfficeResourceTab = (entry, input) =>")
|
||||
);
|
||||
assert!(resource_runtime.contains("if (!entry || entry.kind !== 'office') return false;"));
|
||||
assert!(resource_runtime
|
||||
.contains("if (currentHref !== nextHref) void openPassiveResourceTab(entry, input);"));
|
||||
assert!(resource_runtime.contains(
|
||||
"if (officePreviewBaseHref(currentHref) !== officePreviewBaseHref(nextHref)) void openPassiveResourceTab(entry, input);"
|
||||
));
|
||||
assert!(resource_runtime.contains("refreshExistingOfficeResourceTab(existing, input);"));
|
||||
assert!(resource_runtime.contains("syncActiveResourceWidthType(activeEntry, role);"));
|
||||
assert!(resource_runtime.contains("data-mnote-active-resource-width-type"));
|
||||
@@ -4076,14 +4096,14 @@ mod tests {
|
||||
.headers()
|
||||
.get("x-error-phase")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("query_send")
|
||||
Some("convex_query_retired")
|
||||
);
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get("x-upstream-service")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("convex")
|
||||
Some("convex-retired")
|
||||
);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
@@ -4733,7 +4753,9 @@ mod tests {
|
||||
assert!(session_runtime.contains("/api/local-folder/events"));
|
||||
assert!(session_runtime.contains("localFolderEventChannelKey"));
|
||||
assert!(session_runtime.contains("url.searchParams.set('documentId', session.documentId);"));
|
||||
assert!(session_runtime.contains("if (!documentId) return;"));
|
||||
assert!(session_runtime.contains(
|
||||
"if (!session || session.sourceKind !== 'local_folder' || !session.rootUri || !session.documentId) return null;"
|
||||
));
|
||||
assert!(session_runtime.contains("new EventSource(url.toString())"));
|
||||
assert!(session_runtime.contains("localFolderEventRegistry"));
|
||||
assert!(session_runtime
|
||||
@@ -4939,6 +4961,18 @@ mod tests {
|
||||
assert!(DOCUMENT_SESSION_RUNTIME_JS.contains("createDocumentSessionRuntime"));
|
||||
assert!(DOCUMENT_SESSION_RUNTIME_JS.contains("getOrCreateDocumentSession"));
|
||||
assert!(DOCUMENT_SESSION_RUNTIME_JS.contains("persistSession"));
|
||||
assert!(DOCUMENT_SESSION_RUNTIME_JS.contains("__mnoteLocalFolderEventBus"));
|
||||
assert!(DOCUMENT_SESSION_RUNTIME_JS.contains("mnote:local-folder:document-changed"));
|
||||
assert!(DOCUMENT_RESOURCE_TAB_RUNTIME_JS.contains("__mnoteLocalFolderEventBus"));
|
||||
assert!(DOCUMENT_RESOURCE_TAB_RUNTIME_JS.contains("mnote:local-folder:resource-changed"));
|
||||
assert!(
|
||||
DOCUMENT_RESOURCE_TAB_RUNTIME_JS.contains("data-mnote-local-ocr-event-stream-retired")
|
||||
);
|
||||
assert!(!DOCUMENT_RESOURCE_TAB_RUNTIME_JS.contains("local_ocr.job.updated"));
|
||||
assert!(DOCUMENT_RESOURCE_TAB_RUNTIME_JS.contains("mnote:knowledge-rag-job-updated"));
|
||||
assert!(!DOCUMENT_RESOURCE_TAB_RUNTIME_JS.contains("mnote:local-ocr-job-updated"));
|
||||
assert!(DOCUMENT_RESOURCE_TAB_RUNTIME_JS
|
||||
.contains("data-mnote-resource-watch-ready', 'event-bus'"));
|
||||
assert!(DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS.contains("document.createElement('script')"));
|
||||
assert!(DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS
|
||||
.contains("syncPageAggregateScript({ pageAggregateScriptId"));
|
||||
@@ -5120,8 +5154,7 @@ mod tests {
|
||||
assert!(runtime.contains("typeof payload.text === 'string'"));
|
||||
assert!(runtime.contains("payload.type === 'hard_break'"));
|
||||
assert!(runtime.contains("typeof body?.fileVersion === 'string'"));
|
||||
assert!(DOCUMENT_SESSION_RUNTIME_JS
|
||||
.contains("expectedFileVersion: session.conflictDetectionKey"));
|
||||
assert!(DOCUMENT_SESSION_RUNTIME_JS.contains("expectedFileVersion: expectedFileVersion"));
|
||||
assert!(runtime.contains("node?.attrs?.mnoteBlockType === 'mindmap'"));
|
||||
assert!(runtime.contains("blockType: 'mindmap'"));
|
||||
assert!(runtime.contains("...mindmapPropsFromAttrs(node?.attrs, blockId)"));
|
||||
|
||||
@@ -171,6 +171,7 @@ pub fn PageLayout(
|
||||
<script type="module" src={browser_runtime_src("sidebar-shell-runtime.js")}></script>
|
||||
<script type="module" src={browser_runtime_src("sidebar-tree-runtime.js")}></script>
|
||||
<script inner_html={SIDEBAR_TREE_JS.to_string()}></script>
|
||||
<script type="module" src={browser_runtime_src("local-folder-event-bus-runtime.js")}></script>
|
||||
<script type="module" src={browser_runtime_src("tree-live-controller.js")}></script>
|
||||
</aside>
|
||||
<div class="mnote-sidebar-resizer" data-mnote-sidebar-resizer="true" role="separator" aria-orientation="vertical" aria-label="调整侧栏宽度"></div>
|
||||
@@ -254,6 +255,8 @@ mod tests {
|
||||
const FILETREE_RUNTIME_JS: &str = include_str!("../../../browser/filetree-runtime.js");
|
||||
const FILETREE_SELECTION_RUNTIME_JS: &str =
|
||||
include_str!("../../../browser/filetree-selection-runtime.js");
|
||||
const LOCAL_FOLDER_EVENT_BUS_RUNTIME_JS: &str =
|
||||
include_str!("../../../browser/local-folder-event-bus-runtime.js");
|
||||
const TREE_LIVE_CONTROLLER_JS: &str = include_str!("../../../browser/tree-live-controller.js");
|
||||
|
||||
fn js_function_body(source: &str, name: &str) -> String {
|
||||
@@ -337,7 +340,12 @@ mod tests {
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("readFileTreeObjectIdentity"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("objectIdentity: objectIdentity"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("workspaceId: resolveWorkspaceId(fileRow)"));
|
||||
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("/api/media/sign?assetId="));
|
||||
assert!(!SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("/api/media/sign?assetId="));
|
||||
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("data-mnote-media-sign-retired"));
|
||||
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("/api/media/sign?assetId="));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-media-sign-retired"));
|
||||
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("/api/media/upload"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-media-upload-retired"));
|
||||
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("function localFilePathFromAssetId"));
|
||||
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("/api/local-folder/files/open"));
|
||||
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("local-file:"));
|
||||
@@ -347,7 +355,7 @@ mod tests {
|
||||
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("/api/auth/whoami"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("buildOnlyOfficeOpenUrl"));
|
||||
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("target.searchParams.set('userId'"));
|
||||
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("window.open(officeUrl,"));
|
||||
assert!(!SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("window.open(officeUrl,"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("tree.filetree.internal-drop"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("tree.filetree.external-drop"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("beginFileTreeInlineRename"));
|
||||
@@ -429,6 +437,29 @@ mod tests {
|
||||
html.contains("/api/mnote-browser-runtime/sidebar-tree-runtime.js?devHot="),
|
||||
"dev:hot 下 sidebar runtime URL 也必须带 cache buster"
|
||||
);
|
||||
assert!(
|
||||
html.contains("/api/mnote-browser-runtime/local-folder-event-bus-runtime.js?devHot="),
|
||||
"dev:hot 下 local-folder event bus URL 必须带 cache buster,避免复用旧连接编排逻辑"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_layout_loads_local_folder_event_bus_before_tree_live_controller() {
|
||||
let html = crate::ssr::render_view(leptos::view! {
|
||||
<super::PageLayout current_nav="documents" topbar_title={"个人".to_string()}>
|
||||
<main>"正文"</main>
|
||||
</super::PageLayout>
|
||||
});
|
||||
let bus_index = html
|
||||
.find("/api/mnote-browser-runtime/local-folder-event-bus-runtime.js")
|
||||
.expect("local-folder event bus runtime should be loaded");
|
||||
let controller_index = html
|
||||
.find("/api/mnote-browser-runtime/tree-live-controller.js")
|
||||
.expect("tree live controller runtime should be loaded");
|
||||
assert!(
|
||||
bus_index < controller_index,
|
||||
"local-folder event bus 必须先于 tree-live-controller 加载,确保 local-folder watcher 连接由 bus 接管"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -524,6 +555,8 @@ mod tests {
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("publicKnowledgeRagDashboardUrl"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("label.hidden = status === 'idle'"));
|
||||
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\"]")
|
||||
);
|
||||
@@ -534,6 +567,11 @@ mod tests {
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("pruneKnowledgeRagRegistry"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("setKnowledgeRagSourceFilter"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("mnote:knowledge-rag-source-updated"));
|
||||
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("knowledgeRagSourceRelativePath"));
|
||||
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("supportsKnowledgeRagSource"));
|
||||
assert!(!SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("runLocalOcr"));
|
||||
assert!(!SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("mnote:local-ocr-job-updated"));
|
||||
assert!(!SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("data-mnote-local-ocr-menu-status"));
|
||||
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("openPageIndexSettingsPopover();"));
|
||||
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("openLocalOcrSettingsPopover();"));
|
||||
assert!(!SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("data-page-settings-tab=\"index\""));
|
||||
@@ -594,6 +632,14 @@ mod tests {
|
||||
.contains("if (currentSourceKind() === 'local_folder') return false;"),
|
||||
"local source 不应进入 page-ai fast-path,后端会把 run 收敛为文件引用 scope"
|
||||
);
|
||||
assert!(
|
||||
!SIDEBAR_PAGE_AI_RUNTIME_JS.contains("pageAiEnrichOcrContextRefs"),
|
||||
"Page AI 目标包不应保留已退役 OCR sidecar context enrichment"
|
||||
);
|
||||
assert!(
|
||||
!SIDEBAR_PAGE_AI_TARGET_RUNTIME_JS.contains("ocrRootRelativePath"),
|
||||
"Page AI target runtime 不应再注入旧 OCR sidecar 路径"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -786,7 +832,7 @@ mod tests {
|
||||
.contains("var resolvedSidebarPayload = sidebarPayload ? (sidebarPayload.result || sidebarPayload) : null"));
|
||||
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("var renderedPage = false;"));
|
||||
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
||||
.contains("void renderPageProjection(resolvedSidebarPayload);"));
|
||||
.contains("renderedPage = await renderPageProjection(resolvedSidebarPayload);"));
|
||||
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("refreshFileTreeParent(fileTreeScope)"));
|
||||
assert!(
|
||||
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("data-mnote-local-folder-watch-applied")
|
||||
@@ -802,6 +848,25 @@ mod tests {
|
||||
TREE_LIVE_CONTROLLER_JS.contains("bootstrap.transport === 'local-folder-events'"),
|
||||
"本地文件夹 bootstrap transport 应直接选择 /api/local-folder/events"
|
||||
);
|
||||
assert!(LOCAL_FOLDER_EVENT_BUS_RUNTIME_JS.contains("__mnoteLocalFolderEventBus"));
|
||||
assert!(LOCAL_FOLDER_EVENT_BUS_RUNTIME_JS.contains("startLocalFolderWatcher"));
|
||||
assert!(LOCAL_FOLDER_EVENT_BUS_RUNTIME_JS.contains("connections.has(key)"));
|
||||
assert!(LOCAL_FOLDER_EVENT_BUS_RUNTIME_JS.contains("function pathArrayOf"));
|
||||
assert!(LOCAL_FOLDER_EVENT_BUS_RUNTIME_JS.contains("function queueSidebarRefresh"));
|
||||
assert!(LOCAL_FOLDER_EVENT_BUS_RUNTIME_JS
|
||||
.contains("mnote:local-folder:sidebar-refresh-requested"));
|
||||
assert!(LOCAL_FOLDER_EVENT_BUS_RUNTIME_JS
|
||||
.contains("data-mnote-local-folder-event-bus-connections"));
|
||||
assert!(LOCAL_FOLDER_EVENT_BUS_RUNTIME_JS.contains("tree:local-folder-watch-batch"));
|
||||
assert!(LOCAL_FOLDER_EVENT_BUS_RUNTIME_JS.contains("viaEventBus: true"));
|
||||
assert!(LOCAL_FOLDER_EVENT_BUS_RUNTIME_JS.contains("emitSyntheticWatchBatch"));
|
||||
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("__mnoteLocalFolderEventBus"));
|
||||
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("synthetic_page_ai_receipt"));
|
||||
assert!(TREE_LIVE_CONTROLLER_JS.contains("__mnoteLocalFolderEventBus"));
|
||||
assert!(TREE_LIVE_CONTROLLER_JS.contains("startLocalFolderWatcher"));
|
||||
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
||||
.contains("mnote:local-folder:sidebar-refresh-requested"));
|
||||
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("event.detail.viaEventBus === true"));
|
||||
assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("/api/tree/local-folder-watch"));
|
||||
assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
||||
.contains("fetch(window.location.href, { headers: { accept: 'text/html' } })"));
|
||||
@@ -896,6 +961,8 @@ mod tests {
|
||||
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function fetchWithTimeout"));
|
||||
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function uploadLocalFolderAsset"));
|
||||
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function uploadMediaAsset"));
|
||||
assert!(!LOCAL_UPLOAD_RUNTIME_JS.contains("/api/media/upload"));
|
||||
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("data-mnote-media-upload-retired"));
|
||||
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function insertUploadedAssetIntoEditor"));
|
||||
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function dispatchUploadedEditorChange"));
|
||||
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function persistUploadedEditorChange"));
|
||||
@@ -1361,7 +1428,7 @@ mod tests {
|
||||
.contains("return patchFileTreeParentChildren(parentRelativePath, rows);"));
|
||||
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("var renderedPage = false;"));
|
||||
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
||||
.contains("void renderPageProjection(resolvedSidebarPayload);"));
|
||||
.contains("renderedPage = await renderPageProjection(resolvedSidebarPayload);"));
|
||||
let render_start = SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
||||
.find("function renderFileProjection(projection)")
|
||||
.expect("renderFileProjection");
|
||||
@@ -1656,9 +1723,8 @@ mod tests {
|
||||
SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("recordFileTreeActionStatus('blocked'")
|
||||
);
|
||||
assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS.contains("/api/tree/filetree/drop-preflight"));
|
||||
assert!(
|
||||
SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("ensureFileTreeWritableTarget('paste'")
|
||||
);
|
||||
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
|
||||
.contains("moveSidebarFileTreeRows(runtimeState.sidebarFileTreeClipboard.rowIds"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("ensureFileTreeWritableTarget('drop'"));
|
||||
}
|
||||
|
||||
@@ -1851,10 +1917,13 @@ mod tests {
|
||||
);
|
||||
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("function openCodeEditorAttachment"));
|
||||
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("function resolveEditorAttachmentUrl"));
|
||||
assert!(!SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("/api/media/sign?assetId="));
|
||||
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("data-mnote-media-sign-retired"));
|
||||
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS
|
||||
.contains("var localFilePath = localFilePathFromAssetId(assetId)"));
|
||||
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS
|
||||
.contains("var localDownloadUrl = buildLocalFileOpenUrl(localFilePath, true)"));
|
||||
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains(
|
||||
"var localDownloadUrl = buildLocalFileOpenUrlForRoot(localFilePath, detail.localRootUri || currentRootUri(), true)"
|
||||
));
|
||||
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("type: 'codeBlock'"));
|
||||
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("attrs: { language: language }"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("inferCodeAttachmentLanguage"));
|
||||
@@ -1862,9 +1931,10 @@ mod tests {
|
||||
.contains("if (isPdfAttachmentFileName(detail.fileName))"));
|
||||
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS
|
||||
.contains("if (isCodeAttachmentFileName(detail.fileName))"));
|
||||
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS
|
||||
assert!(!SIDEBAR_FILETREE_OPEN_RUNTIME_JS
|
||||
.contains("if (isCodeAttachmentFileName(fileName) && String(asset.document_id"));
|
||||
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("await openCodeEditorAttachment({"));
|
||||
assert!(!SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("await openCodeEditorAttachment({"));
|
||||
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("data-mnote-media-sign-retired"));
|
||||
let attachment_class_index = SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS
|
||||
.find("attachmentClassForFileName(fileName).split")
|
||||
.expect("editor attachment links apply type-specific classes");
|
||||
@@ -1874,10 +1944,7 @@ mod tests {
|
||||
assert!(attachment_class_index < local_refresh_index);
|
||||
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("'mnote:leptos-tiptap-spike:ready'"));
|
||||
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("window.setTimeout(function()"));
|
||||
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("[120, 500, 1200, 2500]"));
|
||||
assert!(
|
||||
SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("attachmentInitialEnhanceAttempts >= 120")
|
||||
);
|
||||
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("enhanceEditorAttachmentLinks();"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1889,6 +1956,7 @@ mod tests {
|
||||
assert!(TREE_LIVE_CONTROLLER_JS.contains("data-mnote-tree-live-transport"));
|
||||
assert!(TREE_LIVE_CONTROLLER_JS.contains("pagehide"));
|
||||
assert!(TREE_LIVE_CONTROLLER_JS.contains("__mnoteTreeLiveEventSource"));
|
||||
assert!(TREE_LIVE_CONTROLLER_JS.contains("__mnoteLocalFolderEventBus"));
|
||||
assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:snapshot"));
|
||||
assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:delta"));
|
||||
assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:resync"));
|
||||
|
||||
@@ -2771,7 +2771,7 @@ body {
|
||||
background: #FFF;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-toolbar {
|
||||
.mnote-knowledge-rag-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
@@ -2784,7 +2784,7 @@ body {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-status {
|
||||
.mnote-knowledge-rag-status {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
@@ -2793,7 +2793,7 @@ body {
|
||||
color: #787774;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-toolbar button {
|
||||
.mnote-knowledge-rag-toolbar button {
|
||||
flex: 0 0 auto;
|
||||
height: 28px;
|
||||
border: 1px solid rgba(55, 53, 47, 0.16);
|
||||
@@ -2805,17 +2805,17 @@ body {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-toolbar button:hover:not(:disabled) {
|
||||
.mnote-knowledge-rag-toolbar button:hover:not(:disabled) {
|
||||
background: rgba(55, 53, 47, 0.06);
|
||||
}
|
||||
|
||||
.mnote-local-ocr-toolbar button:disabled {
|
||||
.mnote-knowledge-rag-toolbar button:disabled {
|
||||
cursor: default;
|
||||
color: #a8a29e;
|
||||
background: #f7f6f3;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-dock {
|
||||
.mnote-knowledge-rag-task-dock {
|
||||
position: fixed;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
@@ -2826,11 +2826,11 @@ body {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-toggle {
|
||||
.mnote-knowledge-rag-task-toggle {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-badge {
|
||||
.mnote-knowledge-rag-task-badge {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
@@ -2847,17 +2847,17 @@ body {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-drawer {
|
||||
.mnote-knowledge-rag-task-drawer {
|
||||
width: min(440px, calc(100vw - 24px));
|
||||
height: 100%;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-drawer[hidden] {
|
||||
.mnote-knowledge-rag-task-drawer[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-panel {
|
||||
.mnote-knowledge-rag-task-panel {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -2870,18 +2870,18 @@ body {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-head {
|
||||
.mnote-knowledge-rag-task-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-head > div {
|
||||
.mnote-knowledge-rag-task-head > div {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-head strong {
|
||||
.mnote-knowledge-rag-task-head strong {
|
||||
display: block;
|
||||
color: #1B1C1C;
|
||||
font-size: 16px;
|
||||
@@ -2889,7 +2889,7 @@ body {
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-head span {
|
||||
.mnote-knowledge-rag-task-head span {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
color: #8B8782;
|
||||
@@ -2897,7 +2897,7 @@ body {
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-close {
|
||||
.mnote-knowledge-rag-task-close {
|
||||
flex: 0 0 auto;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
@@ -2908,12 +2908,12 @@ body {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-close:hover {
|
||||
.mnote-knowledge-rag-task-close:hover {
|
||||
background: rgba(55, 53, 47, 0.08);
|
||||
color: #37352f;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-tabs {
|
||||
.mnote-knowledge-rag-task-tabs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 4px;
|
||||
@@ -2922,7 +2922,7 @@ body {
|
||||
background: #F4F3F2;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-tabs button {
|
||||
.mnote-knowledge-rag-task-tabs button {
|
||||
min-width: 0;
|
||||
height: 28px;
|
||||
border: 0;
|
||||
@@ -2936,26 +2936,26 @@ body {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-tabs button[aria-selected="true"] {
|
||||
.mnote-knowledge-rag-task-tabs button[aria-selected="true"] {
|
||||
background: #FFF;
|
||||
color: #1B1C1C;
|
||||
box-shadow: 0 1px 4px rgba(27, 28, 28, 0.08);
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-toolbar {
|
||||
.mnote-knowledge-rag-task-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-toolbar span {
|
||||
.mnote-knowledge-rag-task-toolbar span {
|
||||
color: #8B8782;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-toolbar button,
|
||||
.mnote-local-ocr-task-actions button {
|
||||
.mnote-knowledge-rag-task-toolbar button,
|
||||
.mnote-knowledge-rag-task-actions button {
|
||||
min-height: 28px;
|
||||
border: 1px solid rgba(27, 28, 28, 0.08);
|
||||
border-radius: 6px;
|
||||
@@ -2965,12 +2965,12 @@ body {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-toolbar button:disabled {
|
||||
.mnote-knowledge-rag-task-toolbar button:disabled {
|
||||
color: #AAA6A0;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-list {
|
||||
.mnote-knowledge-rag-task-list {
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
@@ -2978,7 +2978,7 @@ body {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-row {
|
||||
.mnote-knowledge-rag-task-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: start;
|
||||
@@ -2989,20 +2989,20 @@ body {
|
||||
background: #FFF;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-main {
|
||||
.mnote-knowledge-rag-task-main {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-title-line {
|
||||
.mnote-knowledge-rag-task-title-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-main strong {
|
||||
.mnote-knowledge-rag-task-main strong {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
@@ -3014,7 +3014,7 @@ body {
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-main em {
|
||||
.mnote-knowledge-rag-task-main em {
|
||||
flex: 0 0 auto;
|
||||
padding: 1px 6px;
|
||||
border-radius: 999px;
|
||||
@@ -3025,17 +3025,17 @@ body {
|
||||
line-height: 15px;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-row[data-mnote-local-ocr-task-category="attention"] .mnote-local-ocr-task-main em {
|
||||
.mnote-knowledge-rag-task-row[data-mnote-knowledge-rag-task-category="attention"] .mnote-knowledge-rag-task-main em {
|
||||
background: #FEE2E2;
|
||||
color: #B3261E;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-row[data-mnote-local-ocr-task-category="active"] .mnote-local-ocr-task-main em {
|
||||
.mnote-knowledge-rag-task-row[data-mnote-knowledge-rag-task-category="active"] .mnote-knowledge-rag-task-main em {
|
||||
background: #DBEAFE;
|
||||
color: #1D4ED8;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-main span {
|
||||
.mnote-knowledge-rag-task-main span {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
@@ -3046,7 +3046,7 @@ body {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-progress {
|
||||
.mnote-knowledge-rag-task-progress {
|
||||
position: relative;
|
||||
height: 4px;
|
||||
overflow: hidden;
|
||||
@@ -3054,14 +3054,14 @@ body {
|
||||
background: #ECE9E4;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-progress i {
|
||||
.mnote-knowledge-rag-task-progress i {
|
||||
display: block;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: #5B8DEF;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-progress[data-progress-mode="indeterminate"] i {
|
||||
.mnote-knowledge-rag-task-progress[data-progress-mode="indeterminate"] i {
|
||||
width: 40%;
|
||||
animation: mnote-task-progress-slide 1.15s ease-in-out infinite;
|
||||
}
|
||||
@@ -3075,7 +3075,7 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-empty {
|
||||
.mnote-knowledge-rag-task-empty {
|
||||
padding: 24px 12px;
|
||||
border: 1px dashed rgba(27, 28, 28, 0.12);
|
||||
border-radius: 8px;
|
||||
@@ -3083,34 +3083,34 @@ body {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-actions {
|
||||
.mnote-knowledge-rag-task-actions {
|
||||
display: grid;
|
||||
flex: 0 0 auto;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-actions button {
|
||||
.mnote-knowledge-rag-task-actions button {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-actions button[data-mnote-local-ocr-task-delete] {
|
||||
.mnote-knowledge-rag-task-actions button[data-mnote-knowledge-rag-task-delete] {
|
||||
color: #b3261e;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.mnote-local-ocr-task-dock {
|
||||
.mnote-knowledge-rag-task-dock {
|
||||
left: 12px;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-drawer {
|
||||
.mnote-knowledge-rag-task-drawer {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-row {
|
||||
.mnote-knowledge-rag-task-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.mnote-local-ocr-task-actions {
|
||||
.mnote-knowledge-rag-task-actions {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
@@ -5870,6 +5870,6 @@ mod tests {
|
||||
// 至少 2000 字符才能包含完整样式
|
||||
assert!(MNOTE_CSS.len() > 2000);
|
||||
// 当前整合了工作区壳、编辑器样式、树菜单、文件树图标、页面 AI、账号弹窗与授权管理控制面,仍保持在单文件可审阅范围内。
|
||||
assert!(MNOTE_CSS.len() < 114000);
|
||||
assert!(MNOTE_CSS.len() < 145000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ use crate::app::AppConfig;
|
||||
use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use bridge_runtime::{
|
||||
build_runtime_command_artifact_plan, RuntimeCommandArtifactPlan, RuntimeCommandExecutionPlan,
|
||||
build_runtime_command_artifact_plan, retired_command_transport_function_name,
|
||||
retired_query_transport_function_name, RuntimeCommandArtifactPlan, RuntimeCommandExecutionPlan,
|
||||
RuntimeQueryExecutionPlan,
|
||||
};
|
||||
use serde_json::Value;
|
||||
@@ -48,9 +49,15 @@ fn load_query_fixture(
|
||||
.with_header("x-error-phase", "fixture_parse")
|
||||
})?;
|
||||
|
||||
Ok(fixtures
|
||||
.as_object()
|
||||
.and_then(|map| map.get(plan.function_name.as_str()))
|
||||
let Some(map) = fixtures.as_object() else {
|
||||
return Ok(None);
|
||||
};
|
||||
if let Some(fixture) = map.get(plan.function_name.as_str()) {
|
||||
return Ok(Some(fixture.clone()));
|
||||
}
|
||||
Ok(retired_query_transport_function_name(&plan.query_name)
|
||||
.ok()
|
||||
.and_then(|legacy_name| map.get(legacy_name))
|
||||
.cloned())
|
||||
}
|
||||
|
||||
@@ -88,7 +95,51 @@ fn load_mutation_fixture(
|
||||
context: &RequestContext,
|
||||
plan: &RuntimeCommandExecutionPlan,
|
||||
) -> Result<Option<Value>, WebError> {
|
||||
load_mutation_fixture_by_name(config, context, plan.function_name.as_str())
|
||||
if let Some(fixture) =
|
||||
load_mutation_fixture_by_name(config, context, plan.function_name.as_str())?
|
||||
{
|
||||
return Ok(Some(fixture));
|
||||
}
|
||||
if let Some(legacy_name) = resource_lifecycle_transport_function_name(plan) {
|
||||
if let Some(fixture) = load_mutation_fixture_by_name(config, context, legacy_name)? {
|
||||
return Ok(Some(fixture));
|
||||
}
|
||||
}
|
||||
retired_command_transport_function_name(&plan.command_name)
|
||||
.ok()
|
||||
.map(|legacy_name| load_mutation_fixture_by_name(config, context, legacy_name))
|
||||
.transpose()
|
||||
.map(|fixture| fixture.flatten())
|
||||
}
|
||||
|
||||
fn resource_lifecycle_transport_function_name(
|
||||
plan: &RuntimeCommandExecutionPlan,
|
||||
) -> Option<&'static str> {
|
||||
let action = match plan.command_name.as_str() {
|
||||
"tree.resource.archive" => "archive",
|
||||
"tree.resource.restore" => "restore",
|
||||
"tree.resource.purge" => "purge",
|
||||
"tree.resource.rename" => "rename",
|
||||
_ => return None,
|
||||
};
|
||||
let resource_kind = plan
|
||||
.args_json
|
||||
.get("resourceLifecyclePlan")
|
||||
.and_then(|value| value.get("resourceKind"))
|
||||
.or_else(|| plan.args_json.get("resourceKind"))
|
||||
.and_then(Value::as_str)?;
|
||||
match (action, resource_kind) {
|
||||
("archive" | "restore" | "rename", "file" | "media") => Some("mediaAssets:patchById"),
|
||||
("purge", "file" | "media") => Some("mediaAssets:purgeById"),
|
||||
("archive", "mindmap") => Some("mindmaps:softDelete"),
|
||||
("restore", "mindmap") => Some("mindmaps:restore"),
|
||||
("purge", "mindmap") => Some("mindmaps:purge"),
|
||||
("archive", "table") => Some("tables:remove"),
|
||||
("restore", "table") => Some("tables:restore"),
|
||||
("purge", "table") => Some("tables:purge"),
|
||||
("rename", "table") => Some("tables:update"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn execute_retired_query_plan(
|
||||
|
||||
Reference in New Issue
Block a user