推进资源工具与本地优先兼容链收口
This commit is contained in:
@@ -726,8 +726,8 @@ pub async fn empty_trash(
|
||||
);
|
||||
}
|
||||
let command = RuntimeCommandEnvelopeWire {
|
||||
name: "documents.emptyTrashByWorkspace".into(),
|
||||
command_id: format!("documents_empty_trash_{}", context.trace.request_id),
|
||||
name: "tree.trash.emptyWorkspace".into(),
|
||||
command_id: format!("tree_trash_empty_workspace_{}", context.trace.request_id),
|
||||
idempotency_key: context.source.idempotency_key.clone(),
|
||||
actor: RuntimeActorWire {
|
||||
actor_type: context.auth.actor_type.clone(),
|
||||
@@ -751,8 +751,11 @@ pub async fn empty_trash(
|
||||
"workspaceId": workspace_id,
|
||||
}),
|
||||
preflight_data: None,
|
||||
reason: Some("mnote-web documents empty trash".into()),
|
||||
refs: vec!["mnote-web-documents-trash".into()],
|
||||
reason: Some("mnote-web tree trash empty workspace".into()),
|
||||
refs: vec![
|
||||
"mnote-web-documents-trash-compat".into(),
|
||||
"tree.trash.emptyWorkspace".into(),
|
||||
],
|
||||
dry_run: false,
|
||||
validate_only: false,
|
||||
};
|
||||
@@ -769,7 +772,7 @@ pub async fn empty_trash(
|
||||
if let Value::Object(map) = &mut result {
|
||||
map.insert(
|
||||
"canonicalCommand".into(),
|
||||
json!("documents.emptyTrashByWorkspace"),
|
||||
json!("tree.trash.emptyWorkspace"),
|
||||
);
|
||||
map.insert("compatRoute".into(), json!("/api/documents/empty-trash"));
|
||||
}
|
||||
@@ -784,8 +787,9 @@ pub async fn empty_trash(
|
||||
"traceId": context.trace.trace_id,
|
||||
"owner": "mnote-web",
|
||||
"meta": {
|
||||
"commandName": "documents.emptyTrashByWorkspace",
|
||||
"canonicalCommand": "documents.emptyTrashByWorkspace",
|
||||
"commandName": "tree.trash.emptyWorkspace",
|
||||
"canonicalCommand": "tree.trash.emptyWorkspace",
|
||||
"compatCommandName": "documents.emptyTrashByWorkspace",
|
||||
"artifacts": artifacts,
|
||||
"artifactError": artifact_error,
|
||||
},
|
||||
@@ -1242,11 +1246,11 @@ mod tests {
|
||||
assert_eq!(payload["result"]["deletedCount"], 2);
|
||||
assert_eq!(
|
||||
payload["result"]["canonicalCommand"],
|
||||
"documents.emptyTrashByWorkspace"
|
||||
"tree.trash.emptyWorkspace"
|
||||
);
|
||||
assert_eq!(
|
||||
payload["meta"]["artifacts"]["commandLog"]["commandName"],
|
||||
"documents.emptyTrashByWorkspace"
|
||||
"tree.trash.emptyWorkspace"
|
||||
);
|
||||
assert_eq!(
|
||||
payload["meta"]["artifacts"]["domainEvent"]["eventType"],
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::hermes_client;
|
||||
use crate::app::AppState;
|
||||
use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use crate::hermes_tools::{artifact, block, doc, manifest, page, ToolCallInput};
|
||||
use crate::hermes_tools::{artifact, block, doc, manifest, page, resource, ToolCallInput};
|
||||
use axum::extract::{Extension, Query, State};
|
||||
use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
|
||||
use axum::Json;
|
||||
@@ -344,6 +344,10 @@ pub(crate) async fn execute_mnote_tool_call(
|
||||
"mnote.page.save" => page::page_save(&state, &context, &input).await,
|
||||
"mnote.page.update_title" => page::update_title(&state, &context, &input).await,
|
||||
"mnote.page.update_options" => page::update_options(&state, &context, &input).await,
|
||||
"mnote.mindmap.fetch" => resource::mindmap_fetch(&context, &input).await,
|
||||
"mnote.mindmap.apply_ops" => resource::mindmap_apply_ops(&context, &input).await,
|
||||
"mnote.office.fetch_summary" => resource::office_fetch_summary(&context, &input).await,
|
||||
"mnote.office.propose_changes" => resource::office_propose_changes(&context, &input).await,
|
||||
"mnote.artifact.create_summary" => artifact::create_summary(&state, &context, &input).await,
|
||||
"mnote.artifact.create_ai_note" => artifact::create_ai_note(&state, &context, &input).await,
|
||||
_ => Err(
|
||||
@@ -448,7 +452,13 @@ pub(crate) async fn execute_mnote_tool_call(
|
||||
fn is_read_tool(tool_name: &str) -> bool {
|
||||
matches!(
|
||||
tool_name,
|
||||
"mnote.page.get" | "mnote.doc.fetch" | "mnote.doc.find" | "mnote.block.fetch"
|
||||
"mnote.page.get"
|
||||
| "mnote.doc.fetch"
|
||||
| "mnote.doc.find"
|
||||
| "mnote.block.fetch"
|
||||
| "mnote.mindmap.fetch"
|
||||
| "mnote.office.fetch_summary"
|
||||
| "mnote.office.propose_changes"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -995,6 +1005,18 @@ mod tests {
|
||||
assert!(tools
|
||||
.iter()
|
||||
.any(|tool| tool["name"] == "mnote.doc.apply_block_ops"));
|
||||
assert!(tools
|
||||
.iter()
|
||||
.any(|tool| tool["name"] == "mnote.mindmap.fetch"));
|
||||
assert!(tools
|
||||
.iter()
|
||||
.any(|tool| tool["name"] == "mnote.mindmap.apply_ops"));
|
||||
assert!(tools
|
||||
.iter()
|
||||
.any(|tool| tool["name"] == "mnote.office.fetch_summary"));
|
||||
assert!(tools
|
||||
.iter()
|
||||
.any(|tool| tool["name"] == "mnote.office.propose_changes"));
|
||||
let page_save = tools
|
||||
.iter()
|
||||
.find(|tool| tool["name"] == "mnote.page.save")
|
||||
@@ -1148,6 +1170,324 @@ mod tests {
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_tools_resource_tools_require_allowed_resource_scope() {
|
||||
let root =
|
||||
std::env::temp_dir().join(format!("mnote-resource-scope-{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
fs::create_dir_all(root.join("maps")).expect("maps");
|
||||
fs::write(
|
||||
root.join("maps").join("idea.mindmap.json"),
|
||||
r#"{"data":{"text":"中心主题","uid":"root"},"children":[]}"#,
|
||||
)
|
||||
.expect("mindmap");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
crate::routes::local_folder_source::initialize_local_workspace_for_actor(
|
||||
"user_1", &root_uri,
|
||||
)
|
||||
.expect("workspace");
|
||||
|
||||
let response = app()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/hermes/tools/mnote/call")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-mnote-actor-id", "user_1")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"toolName": "mnote.mindmap.fetch",
|
||||
"workspaceId": "local-ws-resource",
|
||||
"documentId": "local-md:README.md",
|
||||
"sourceKind": "local_folder",
|
||||
"rootUri": root_uri,
|
||||
"actorId": "user_1",
|
||||
"sessionId": "sess_resource_scope",
|
||||
"runId": "run_resource_scope",
|
||||
"toolCallId": "call_resource_scope",
|
||||
"traceId": "trace_resource_scope",
|
||||
"args": {
|
||||
"mindmapId": "mind_allowed",
|
||||
"resourcePath": "maps/idea.mindmap.json",
|
||||
"aiAccessScope": {
|
||||
"permissionLevel": "shared_read",
|
||||
"allowedResourceIds": ["mind_other"]
|
||||
}
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get("x-error-code")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("mnote_resource_ai_scope_forbidden")
|
||||
);
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_tools_mindmap_fetch_reads_authorized_local_resource() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-resource-mindmap-fetch-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
fs::create_dir_all(root.join("maps")).expect("maps");
|
||||
fs::write(
|
||||
root.join("maps").join("idea.mindmap.json"),
|
||||
r#"{"data":{"text":"中心主题","uid":"root"},"children":[{"data":{"text":"分支一","uid":"child_1"},"children":[]}]}"#,
|
||||
)
|
||||
.expect("mindmap");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
crate::routes::local_folder_source::initialize_local_workspace_for_actor(
|
||||
"user_1", &root_uri,
|
||||
)
|
||||
.expect("workspace");
|
||||
|
||||
let response = app()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/hermes/tools/mnote/call")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-mnote-actor-id", "user_1")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"toolName": "mnote.mindmap.fetch",
|
||||
"workspaceId": "local-ws-resource",
|
||||
"documentId": "local-md:README.md",
|
||||
"sourceKind": "local_folder",
|
||||
"rootUri": root_uri,
|
||||
"actorId": "user_1",
|
||||
"sessionId": "sess_mindmap_fetch",
|
||||
"runId": "run_mindmap_fetch",
|
||||
"toolCallId": "call_mindmap_fetch",
|
||||
"traceId": "trace_mindmap_fetch",
|
||||
"args": {
|
||||
"mindmapId": "mind_allowed",
|
||||
"resourcePath": "maps/idea.mindmap.json",
|
||||
"aiAccessScope": {
|
||||
"permissionLevel": "shared_read",
|
||||
"allowedResourceIds": ["mind_allowed"]
|
||||
}
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
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["toolName"], "mnote.mindmap.fetch");
|
||||
assert_eq!(payload["audit"]["effect"], "read");
|
||||
assert_eq!(payload["result"]["resourceKind"], "mindmap");
|
||||
assert_eq!(
|
||||
payload["result"]["objectIdentity"],
|
||||
"resource:mindmap:local-md:README.md:mind_allowed"
|
||||
);
|
||||
assert!(payload["result"]["markdownSummary"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.contains("中心主题"));
|
||||
assert_eq!(payload["result"]["nodes"][1]["text"], "分支一");
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_tools_mindmap_apply_ops_shared_read_is_forbidden() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-resource-mindmap-write-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
fs::create_dir_all(root.join("maps")).expect("maps");
|
||||
fs::write(
|
||||
root.join("maps").join("idea.mindmap.json"),
|
||||
r#"{"data":{"text":"中心主题","uid":"root"},"children":[]}"#,
|
||||
)
|
||||
.expect("mindmap");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
crate::routes::local_folder_source::initialize_local_workspace_for_actor(
|
||||
"user_1", &root_uri,
|
||||
)
|
||||
.expect("workspace");
|
||||
|
||||
let response = app()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/hermes/tools/mnote/call")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-mnote-actor-id", "user_1")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"toolName": "mnote.mindmap.apply_ops",
|
||||
"workspaceId": "local-ws-resource",
|
||||
"documentId": "local-md:README.md",
|
||||
"sourceKind": "local_folder",
|
||||
"rootUri": root_uri,
|
||||
"actorId": "user_1",
|
||||
"sessionId": "sess_mindmap_write",
|
||||
"runId": "run_mindmap_write",
|
||||
"toolCallId": "call_mindmap_write",
|
||||
"traceId": "trace_mindmap_write",
|
||||
"idempotencyKey": "idem_mindmap_write",
|
||||
"dryRun": false,
|
||||
"args": {
|
||||
"mindmapId": "mind_allowed",
|
||||
"resourcePath": "maps/idea.mindmap.json",
|
||||
"ops": [{"op": "update_node", "nodeId": "root", "text": "改名"}],
|
||||
"aiAccessScope": {
|
||||
"permissionLevel": "shared_read",
|
||||
"allowedResourceIds": ["mind_allowed"]
|
||||
}
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get("x-error-code")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("mnote_tool_shared_read_write_forbidden")
|
||||
);
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_tools_office_fetch_and_propose_changes_do_not_write_file() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-resource-office-fetch-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
fs::create_dir_all(root.join("office")).expect("office");
|
||||
fs::write(root.join("office").join("report.docx"), "Office text").expect("office file");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
crate::routes::local_folder_source::initialize_local_workspace_for_actor(
|
||||
"user_1", &root_uri,
|
||||
)
|
||||
.expect("workspace");
|
||||
|
||||
let fetch = app()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/hermes/tools/mnote/call")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-mnote-actor-id", "user_1")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"toolName": "mnote.office.fetch_summary",
|
||||
"workspaceId": "local-ws-resource",
|
||||
"documentId": "local-md:README.md",
|
||||
"sourceKind": "local_folder",
|
||||
"rootUri": root_uri,
|
||||
"actorId": "user_1",
|
||||
"sessionId": "sess_office_fetch",
|
||||
"runId": "run_office_fetch",
|
||||
"toolCallId": "call_office_fetch",
|
||||
"traceId": "trace_office_fetch",
|
||||
"args": {
|
||||
"assetId": "asset_report",
|
||||
"resourcePath": "office/report.docx",
|
||||
"aiAccessScope": {
|
||||
"permissionLevel": "shared_read",
|
||||
"allowedResourceIds": ["resource:onlyoffice:local-md:README.md:asset_report"]
|
||||
}
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
assert_eq!(fetch.status(), StatusCode::OK);
|
||||
let fetch_body = to_bytes(fetch.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("fetch body");
|
||||
let fetch_payload: Value = serde_json::from_slice(&fetch_body).expect("fetch json");
|
||||
assert_eq!(fetch_payload["audit"]["effect"], "read");
|
||||
assert_eq!(fetch_payload["result"]["resourceKind"], "only_office");
|
||||
assert_eq!(fetch_payload["result"]["fileName"], "report.docx");
|
||||
|
||||
let before = fs::read(root.join("office").join("report.docx")).expect("before");
|
||||
let propose = app()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/hermes/tools/mnote/call")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-mnote-actor-id", "user_1")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"toolName": "mnote.office.propose_changes",
|
||||
"workspaceId": "local-ws-resource",
|
||||
"documentId": "local-md:README.md",
|
||||
"sourceKind": "local_folder",
|
||||
"rootUri": root_uri,
|
||||
"actorId": "user_1",
|
||||
"sessionId": "sess_office_propose",
|
||||
"runId": "run_office_propose",
|
||||
"toolCallId": "call_office_propose",
|
||||
"traceId": "trace_office_propose",
|
||||
"args": {
|
||||
"assetId": "asset_report",
|
||||
"resourcePath": "office/report.docx",
|
||||
"instructions": "补充结论",
|
||||
"aiAccessScope": {
|
||||
"permissionLevel": "shared_read",
|
||||
"allowedResourceIds": ["asset_report"]
|
||||
}
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
assert_eq!(propose.status(), StatusCode::OK);
|
||||
let propose_body = to_bytes(propose.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("propose body");
|
||||
let propose_payload: Value = serde_json::from_slice(&propose_body).expect("propose json");
|
||||
assert_eq!(propose_payload["audit"]["effect"], "read");
|
||||
assert_eq!(propose_payload["result"]["writesBinary"], false);
|
||||
assert_eq!(
|
||||
fs::read(root.join("office").join("report.docx")).expect("after"),
|
||||
before
|
||||
);
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_tools_page_get_accepts_delegated_actor_from_hermes_payload() {
|
||||
let response = app()
|
||||
|
||||
@@ -30,6 +30,7 @@ pub struct MediaBatchRequest {
|
||||
#[serde(default)]
|
||||
pub asset_ids: Vec<String>,
|
||||
pub new_name: Option<String>,
|
||||
pub target_document_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -510,10 +511,10 @@ pub async fn media_batch(
|
||||
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
||||
let user_id = current_user_id(&state, &context).await;
|
||||
let action = body.action.trim();
|
||||
if action != "restore" && action != "delete" && action != "rename" {
|
||||
if action != "restore" && action != "delete" && action != "rename" && action != "move" {
|
||||
return Err(WebError::bad_request_code(
|
||||
"media_batch_action_unsupported",
|
||||
"Rust resource trash 当前仅支持附件 delete / restore / rename",
|
||||
"Rust resource trash 当前仅支持附件 delete / restore / rename / move",
|
||||
)
|
||||
.with_context(&context)
|
||||
.with_header(HEADER_MNOTE_WEB_OWNER, "mnote-web"));
|
||||
@@ -553,6 +554,21 @@ pub async fn media_batch(
|
||||
"newName": new_name,
|
||||
}),
|
||||
)
|
||||
} else if action == "move" {
|
||||
let target_document_id = require_id(
|
||||
&context,
|
||||
body.target_document_id.as_deref().unwrap_or_default(),
|
||||
"targetDocumentId",
|
||||
)?;
|
||||
(
|
||||
"tree.resource.move",
|
||||
json!({
|
||||
"resourceKind": "file",
|
||||
"assetId": asset_id,
|
||||
"fromDocumentId": document_id,
|
||||
"targetDocumentId": target_document_id,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"tree.resource.archive",
|
||||
@@ -595,6 +611,29 @@ pub async fn media_batch(
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if action == "move" {
|
||||
let target_document_id = require_id(
|
||||
&context,
|
||||
body.target_document_id.as_deref().unwrap_or_default(),
|
||||
"targetDocumentId",
|
||||
)?;
|
||||
execute_convex_mutation_by_name(
|
||||
state.config(),
|
||||
&context,
|
||||
"mediaAssets:patchById",
|
||||
json!({
|
||||
"userId": user_id,
|
||||
"id": asset_id,
|
||||
"patch": {
|
||||
"document_id": target_document_id,
|
||||
},
|
||||
}),
|
||||
workspace_id.as_deref(),
|
||||
None,
|
||||
"media_batch_move_patch",
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
let command_result = execute_runtime_command_via_convex_with_artifacts(
|
||||
&state,
|
||||
&context,
|
||||
@@ -602,12 +641,13 @@ pub async fn media_batch(
|
||||
command,
|
||||
)
|
||||
.await;
|
||||
if action == "rename" {
|
||||
if action == "rename" || action == "move" {
|
||||
if let Err(error) = command_result {
|
||||
tracing::warn!(
|
||||
error = %error.message(),
|
||||
asset_id = %asset_id,
|
||||
"tree.resource.rename artifact command 失败,已保留兼容 patch 结果"
|
||||
action = %action,
|
||||
"tree.resource artifact command 失败,已保留兼容 patch 结果"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -624,10 +664,13 @@ pub async fn media_batch(
|
||||
"restored": if action == "restore" { updated } else { 0 },
|
||||
"deleted": if action == "delete" { updated } else { 0 },
|
||||
"renamed": if action == "rename" { updated } else { 0 },
|
||||
"moved": if action == "move" { updated } else { 0 },
|
||||
"canonicalCommand": if action == "restore" {
|
||||
"tree.resource.restore"
|
||||
} else if action == "rename" {
|
||||
"tree.resource.rename"
|
||||
} else if action == "move" {
|
||||
"tree.resource.move"
|
||||
} else {
|
||||
"tree.resource.archive"
|
||||
},
|
||||
@@ -1195,6 +1238,19 @@ mod tests {
|
||||
assert_ne!(renamed["result"]["canonicalCommand"], "tree.node.rename");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resource_move_uses_resource_command_not_document_command() {
|
||||
let moved = post_json(
|
||||
"/api/media/batch",
|
||||
json!({"action": "move", "assetIds": ["asset_1"], "targetDocumentId": "doc_2"}),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(moved["result"]["moved"], 1);
|
||||
assert_eq!(moved["result"]["canonicalCommand"], "tree.resource.move");
|
||||
assert_ne!(moved["result"]["canonicalCommand"], "documents.move");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mindmap_trash_routes_delete_restore_purge_and_empty() {
|
||||
let deleted = delete_json("/api/mindmap/doc_1/mind_1").await;
|
||||
|
||||
@@ -1023,6 +1023,12 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
return textToTiptapDocument(fallbackText);
|
||||
};
|
||||
|
||||
const pageBodyTiptapDocument = (body, fallbackText = '') => {
|
||||
const blockDocument = body?.blockDocument || body?.block_document || body?.editorDocument || body?.editor_document || null;
|
||||
if (blockDocument) return toTiptapDocument(blockDocument, fallbackText);
|
||||
return toTiptapDocument(body?.content, fallbackText);
|
||||
};
|
||||
|
||||
const inlineTextNodes = (node) => {
|
||||
if (!node || typeof node !== 'object') return [];
|
||||
if (Array.isArray(node.content)) {
|
||||
@@ -1568,7 +1574,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
|
||||
const aggregatePlainText = (aggregate) => {
|
||||
const body = aggregate?.body || {};
|
||||
return flattenText(toTiptapDocument(body.content)).replace(/\n{3,}/g, '\n\n').trim();
|
||||
return flattenText(pageBodyTiptapDocument(body)).replace(/\n{3,}/g, '\n\n').trim();
|
||||
};
|
||||
|
||||
const conflictEnvelopeFromResponse = (payload) => (
|
||||
@@ -1604,7 +1610,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
const nextBody = nextAggregate?.body || {};
|
||||
const nextPermissions = nextAggregate?.head?.permissions || {};
|
||||
const nextConflictKey = conflictDetectionKeyFromBody(nextBody);
|
||||
const nextTiptapDocument = toTiptapDocument(nextBody.content);
|
||||
const nextTiptapDocument = pageBodyTiptapDocument(nextBody);
|
||||
const nextSerialized = JSON.stringify(nextTiptapDocument);
|
||||
const nextRevision = Number.isInteger(nextBody.revision) ? nextBody.revision : revisionFromConflictKey(nextConflictKey);
|
||||
session.latestAggregate = nextAggregate;
|
||||
@@ -1971,7 +1977,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
const nextBody = nextAggregate?.body || {};
|
||||
const nextPermissions = nextAggregate?.head?.permissions || {};
|
||||
const nextConflictKey = conflictDetectionKeyFromBody(nextBody);
|
||||
const nextTiptapDocument = toTiptapDocument(nextBody.content);
|
||||
const nextTiptapDocument = pageBodyTiptapDocument(nextBody);
|
||||
const nextSerialized = JSON.stringify(nextTiptapDocument);
|
||||
const contentChanged = nextSerialized !== session.currentSerialized;
|
||||
session.externalChangePending = false;
|
||||
@@ -2250,7 +2256,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
const conflictDetectionKey = conflictDetectionKeyFromBody(pageBody);
|
||||
const pageBodyRevision = Number.isInteger(pageBody.revision) ? pageBody.revision : null;
|
||||
const keyRevision = revisionFromConflictKey(conflictDetectionKey);
|
||||
const tiptapDocument = toTiptapDocument(pageBody.content);
|
||||
const tiptapDocument = pageBodyTiptapDocument(pageBody);
|
||||
const sourceKind = normalizeSessionSourceKind(runtimeDescriptor.bootstrap);
|
||||
const session = {
|
||||
key: buildDocumentSessionKey(runtimeDescriptor.bootstrap),
|
||||
@@ -3492,6 +3498,10 @@ mod tests {
|
||||
assert!(html.contains("/api/tree/events"));
|
||||
assert!(html.contains("data-mnote-tree-live-transport"));
|
||||
assert!(html.contains("syncPageAggregateScript(session, nextAggregate);"));
|
||||
assert!(html.contains("const pageBodyTiptapDocument = (body, fallbackText = '') => {"));
|
||||
assert!(html.contains("body?.blockDocument || body?.block_document"));
|
||||
assert!(html.contains("const nextTiptapDocument = pageBodyTiptapDocument(nextBody);"));
|
||||
assert!(html.contains("const tiptapDocument = pageBodyTiptapDocument(pageBody);"));
|
||||
assert!(!html.contains("mnote-web-document-shell"));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user