use crate::app::AppState; use crate::context::RequestContext; use crate::error::WebError; use crate::hermes_tools::ToolCallInput; use crate::routes::command_support::execute_runtime_command_via_convex_with_artifacts; use bridge_runtime::{ RuntimeActorWire, RuntimeCommandEnvelopeWire, RuntimeSourceWire, RuntimeTargetWire, }; use serde_json::{json, Value}; pub async fn create_summary( state: &AppState, context: &RequestContext, input: &ToolCallInput, ) -> Result { create_artifact_node( state, context, input, "summary", "mnote.artifact.create_summary", ) .await } pub async fn create_ai_note( state: &AppState, context: &RequestContext, input: &ToolCallInput, ) -> Result { create_artifact_node( state, context, input, "ai_note", "mnote.artifact.create_ai_note", ) .await } async fn create_artifact_node( state: &AppState, context: &RequestContext, input: &ToolCallInput, node_type: &str, tool_name: &str, ) -> Result { ensure_write_contract(context, input)?; let document_id = input.effective_document_id().ok_or_else(|| { WebError::bad_request_code("mnote_tool_bad_request", "artifact 工具缺少 documentId") .with_context(context) })?; let workspace_id = input.effective_workspace_id(); let content = input .arg_string("summary") .or_else(|| input.arg_string("content")) .ok_or_else(|| { WebError::bad_request_code("mnote_tool_bad_request", "artifact 工具缺少内容") .with_context(context) })?; let idempotency_key = input.idempotency_key_or_default(&format!("{tool_name}_{}", context.trace.request_id)); let command_id = format!( "{}_{}", tool_name.replace('.', "_"), context.trace.request_id ); let artifact_document_id = if node_type == "summary" { format!("summary_{}", document_id) } else { format!("ai_note_{}_{}", document_id, context.trace.request_id) }; if input.dry_run.unwrap_or(false) { return Ok(json!({ "dryRun": true, "commandName": "tree.node.create", "commandId": command_id, "artifactType": node_type, "artifactDocumentId": artifact_document_id, "documentId": document_id, "workspaceId": workspace_id, "diff": [{"op": "create_artifact", "artifactType": node_type}] })); } let command = RuntimeCommandEnvelopeWire { name: "tree.node.create".into(), command_id: command_id.clone(), idempotency_key: Some(idempotency_key), actor: RuntimeActorWire { actor_type: context.auth.actor_type.clone(), actor_id: context.auth.actor_id.clone(), session_id: input .session_id .clone() .or_else(|| context.auth.session_id.clone()), }, source: RuntimeSourceWire { channel: "hermes".into(), client: "mnote-hermes-plugin".into(), source_kind: None, root_uri: None, workspace_id: workspace_id.clone(), capabilities: input.capability_scope.clone().unwrap_or_default(), }, target: Some(RuntimeTargetWire { workspace_id: workspace_id.clone(), page_id: Some(document_id.clone()), block_id: None, }), payload: json!({ "workspaceId": workspace_id, "parentId": document_id, "documentId": artifact_document_id, "accessScope": "private", "nodeType": node_type, "title": if node_type == "summary" { "AI Summary" } else { "AI Note" }, "content": [ { "id": format!("{}_body", node_type), "type": "paragraph", "content": [{"type": "text", "text": content}] } ], "artifact": { "kind": node_type, "sourceDocumentId": document_id, "source": "hermes", "sessionId": input.session_id, "runId": input.run_id, "toolCallId": input.tool_call_id, "traceId": input.effective_trace_id(&context.trace.trace_id) }, "referenceEdge": { "from": document_id, "kind": "ai_artifact_reference" } }), preflight_data: None, reason: Some(tool_name.into()), refs: vec![tool_name.into(), "hermes-tool-call".into()], dry_run: false, validate_only: false, }; let execution = execute_runtime_command_via_convex_with_artifacts( &state, context, workspace_id.as_deref(), command, ) .await?; Ok(json!({ "commandName": "tree.node.create", "commandId": command_id, "artifactType": node_type, "artifactDocumentId": artifact_document_id, "referenceEdge": { "from": document_id, "to": artifact_document_id, "kind": "ai_artifact_reference" }, "result": execution.result, "artifacts": execution.artifacts, "artifactError": execution.artifact_error })) } fn ensure_write_contract(context: &RequestContext, input: &ToolCallInput) -> Result<(), WebError> { if !input.has_idempotency_key() { return Err(WebError::bad_request_code( "mnote_tool_idempotency_required", "写入型 mnote Hermes tool 必须携带 idempotencyKey", ) .with_context(context)); } if input.dry_run.is_none() { return Err(WebError::bad_request_code( "mnote_tool_dry_run_required", "写入型 mnote Hermes tool 必须显式携带 dryRun", ) .with_context(context)); } Ok(()) }