use crate::context::RequestContext; use crate::error::WebError; use crate::hermes_tools::{ensure_write_authorized, ToolCallInput}; use crate::routes::onlyoffice_bridge::{self, BridgeResultWire, BridgeRunError}; use axum::http::StatusCode; use serde_json::{json, Value}; use std::collections::HashSet; const DEFAULT_TIMEOUT_MS: u64 = 25_000; const MAX_SHEET_BATCH_CELLS: u64 = 5_000; const MAX_DOCUMENT_TABLE_CELLS: u64 = 1_000; const MAX_PRESENTATION_TABLE_CELLS: u64 = 200; pub async fn session_current( context: &RequestContext, input: &ToolCallInput, ) -> Result { let session = current_session(context, input)?; Ok(json!({ "schema": "mnote.onlyoffice.session.v1", "session": session })) } pub async fn capabilities( _context: &RequestContext, _input: &ToolCallInput, ) -> Result { Ok(onlyoffice_bridge::capability_payload()) } pub async fn selection_get( context: &RequestContext, input: &ToolCallInput, ) -> Result { run_read_action( context, input, "selection.get", json!({ "options": input.arg_value("options").unwrap_or_else(|| json!({})) }), ) .await } pub async fn sheet_get_range( context: &RequestContext, input: &ToolCallInput, ) -> Result { run_read_action( context, input, "sheet.get_range", json!({ "address": input.arg_string("address").unwrap_or_default() }), ) .await } pub async fn document_export( context: &RequestContext, input: &ToolCallInput, ) -> Result { let format = input .arg_string("format") .unwrap_or_else(|| "markdown".into()) .to_ascii_lowercase(); if !matches!(format.as_str(), "markdown" | "html") { return Err(WebError::bad_request_code( "mnote_onlyoffice_export_format_invalid", "format 仅支持 markdown 或 html", ) .with_context(context)); } run_read_action( context, input, "document.export", json!({ "format": format }), ) .await } pub async fn document_search_replace( context: &RequestContext, input: &ToolCallInput, ) -> Result { let search = required_string(context, input, "search")?; if search.is_empty() { return Err( WebError::bad_request_code("mnote_onlyoffice_search_empty", "search 不能为空") .with_context(context), ); } run_write_action( context, input, "document.search_replace", json!({ "search": search, "replace": input.arg_string("replace").unwrap_or_default(), "matchCase": arg_bool(input, "matchCase").unwrap_or(false) }), ) .await } pub async fn document_insert_table( context: &RequestContext, input: &ToolCallInput, ) -> Result { let data = input.arg_value("data").unwrap_or_else(|| json!([])); let rows_from_data = data.as_array().map(|rows| rows.len() as u64).unwrap_or(0); let cols_from_data = data .as_array() .and_then(|rows| { rows.iter() .map(|row| row.as_array().map(|cells| cells.len() as u64).unwrap_or(1)) .max() }) .unwrap_or(0); let rows = arg_u64(input, "rows").unwrap_or(rows_from_data).max(1); let cols = arg_u64(input, "cols") .or_else(|| arg_u64(input, "columns")) .unwrap_or(cols_from_data) .max(1); if rows.saturating_mul(cols) > MAX_DOCUMENT_TABLE_CELLS { return Err(WebError::bad_request_code( "mnote_onlyoffice_table_too_large", format!("单次 Word 表格最多支持 {MAX_DOCUMENT_TABLE_CELLS} 个单元格"), ) .with_context(context)); } run_write_action( context, input, "document.insert_table", json!({ "rows": rows, "cols": cols, "data": data }), ) .await } pub async fn document_get_comments( context: &RequestContext, input: &ToolCallInput, ) -> Result { run_read_action(context, input, "document.get_comments", json!({})).await } pub async fn document_add_comment( context: &RequestContext, input: &ToolCallInput, ) -> Result { let text = required_string(context, input, "text")?; run_write_action( context, input, "document.add_comment", json!({ "text": text, "author": input.arg_string("author").unwrap_or_else(|| "MNote AI".into()), "target": input.arg_string("target").unwrap_or_else(|| "selection".into()) }), ) .await } pub async fn sheet_get_sheets( context: &RequestContext, input: &ToolCallInput, ) -> Result { run_read_action(context, input, "sheet.get_sheets", json!({})).await } pub async fn sheet_add_sheet( context: &RequestContext, input: &ToolCallInput, ) -> Result { let name = required_string(context, input, "name")?; run_write_action(context, input, "sheet.add_sheet", json!({ "name": name })).await } pub async fn sheet_rename_sheet( context: &RequestContext, input: &ToolCallInput, ) -> Result { let name = required_string(context, input, "name")?; run_write_action( context, input, "sheet.rename_sheet", json!({ "name": name, "sheetIndex": arg_u64(input, "sheetIndex"), "sheetName": input.arg_string("sheetName").unwrap_or_default() }), ) .await } pub async fn sheet_get_range_values( context: &RequestContext, input: &ToolCallInput, ) -> Result { let address = input .arg_string("address") .or_else(|| input.arg_string("range")) .ok_or_else(|| { WebError::bad_request_code("mnote_onlyoffice_range_required", "缺少 address") .with_context(context) })?; let range = parse_a1_range(context, &address)?; ensure_sheet_cell_limit(context, range.row_count, range.col_count)?; run_read_action( context, input, "sheet.get_range_values", json!({ "address": address }), ) .await } pub async fn sheet_get_values( context: &RequestContext, input: &ToolCallInput, ) -> Result { let start_row = arg_u64(input, "startRow").unwrap_or(0); let start_col = arg_u64(input, "startCol").unwrap_or(0); let row_count = required_u64(context, input, "rowCount")?; let col_count = required_u64(context, input, "colCount")?; ensure_sheet_cell_limit(context, row_count, col_count)?; run_read_action( context, input, "sheet.get_values", json!({ "startRow": start_row, "startCol": start_col, "rowCount": row_count, "colCount": col_count }), ) .await } pub async fn presentation_get_slides( context: &RequestContext, input: &ToolCallInput, ) -> Result { run_read_action(context, input, "presentation.get_slides", json!({})).await } pub async fn presentation_get_slide_texts( context: &RequestContext, input: &ToolCallInput, ) -> Result { let mut payload = json!({}); if let Some(slide_index) = arg_u64(input, "slideIndex") { payload["slideIndex"] = json!(slide_index); } run_read_action(context, input, "presentation.get_slide_texts", payload).await } pub async fn presentation_get_shapes( context: &RequestContext, input: &ToolCallInput, ) -> Result { let mut payload = json!({}); if let Some(slide_index) = arg_u64(input, "slideIndex") { payload["slideIndex"] = json!(slide_index); } run_read_action(context, input, "presentation.get_shapes", payload).await } pub async fn document_insert_text( context: &RequestContext, input: &ToolCallInput, ) -> Result { let text = required_string(context, input, "text")?; run_write_action( context, input, "document.insert_text", json!({ "text": text }), ) .await } pub async fn document_replace_selection( context: &RequestContext, input: &ToolCallInput, ) -> Result { let text = required_string(context, input, "text")?; run_write_action( context, input, "document.replace_selection", json!({ "text": text }), ) .await } pub async fn document_insert_html( context: &RequestContext, input: &ToolCallInput, ) -> Result { let html = required_string(context, input, "html")?; run_write_action( context, input, "document.insert_html", json!({ "html": html }), ) .await } pub async fn sheet_set_value( context: &RequestContext, input: &ToolCallInput, ) -> Result { let value = input.arg_value("value").ok_or_else(|| { WebError::bad_request_code("mnote_onlyoffice_value_required", "缺少 value") .with_context(context) })?; run_write_action( context, input, "sheet.set_value", json!({ "address": input.arg_string("address").unwrap_or_default(), "value": value }), ) .await } pub async fn sheet_set_formula( context: &RequestContext, input: &ToolCallInput, ) -> Result { let formula = required_string(context, input, "formula")?; run_write_action( context, input, "sheet.set_formula", json!({ "address": input.arg_string("address").unwrap_or_default(), "formula": formula }), ) .await } pub async fn sheet_batch_set_values( context: &RequestContext, input: &ToolCallInput, ) -> Result { let values = input.arg_value("values").ok_or_else(|| { WebError::bad_request_code("mnote_onlyoffice_values_required", "缺少 values") .with_context(context) })?; let rows = values.as_array().ok_or_else(|| { WebError::bad_request_code("mnote_onlyoffice_values_invalid", "values 必须是二维数组") .with_context(context) })?; let row_count = rows.len() as u64; let col_count = rows .iter() .map(|row| row.as_array().map(|cells| cells.len()).unwrap_or(1)) .max() .unwrap_or(0) as u64; if row_count == 0 || col_count == 0 { return Err( WebError::bad_request_code("mnote_onlyoffice_values_empty", "values 不能为空") .with_context(context), ); } ensure_sheet_cell_limit(context, row_count, col_count)?; run_write_action( context, input, "sheet.batch_set_values", json!({ "startRow": arg_u64(input, "startRow").unwrap_or(0), "startCol": arg_u64(input, "startCol").unwrap_or(0), "values": values }), ) .await } pub async fn sheet_set_range_values( context: &RequestContext, input: &ToolCallInput, ) -> Result { let address = input .arg_string("address") .or_else(|| input.arg_string("range")) .ok_or_else(|| { WebError::bad_request_code("mnote_onlyoffice_range_required", "缺少 address") .with_context(context) })?; let range = parse_a1_range(context, &address)?; ensure_sheet_cell_limit(context, range.row_count, range.col_count)?; let values = input.arg_value("values").ok_or_else(|| { WebError::bad_request_code("mnote_onlyoffice_values_required", "缺少 values") .with_context(context) })?; let rows = values.as_array().ok_or_else(|| { WebError::bad_request_code("mnote_onlyoffice_values_invalid", "values 必须是二维数组") .with_context(context) })?; let row_count = rows.len() as u64; let col_count = rows .iter() .map(|row| row.as_array().map(|cells| cells.len()).unwrap_or(1)) .max() .unwrap_or(0) as u64; if row_count == 0 || col_count == 0 { return Err( WebError::bad_request_code("mnote_onlyoffice_values_empty", "values 不能为空") .with_context(context), ); } ensure_sheet_cell_limit(context, row_count, col_count)?; run_write_action( context, input, "sheet.set_range_values", json!({ "address": address, "values": values }), ) .await } pub async fn sheet_format_range( context: &RequestContext, input: &ToolCallInput, ) -> Result { let address = input .arg_string("address") .or_else(|| input.arg_string("range")) .ok_or_else(|| { WebError::bad_request_code("mnote_onlyoffice_range_required", "缺少 address") .with_context(context) })?; let range = parse_a1_range(context, &address)?; ensure_sheet_cell_limit(context, range.row_count, range.col_count)?; run_write_action( context, input, "sheet.format_range", json!({ "address": address, "bold": arg_bool(input, "bold"), "italic": arg_bool(input, "italic"), "underline": arg_bool(input, "underline"), "fillColor": input.arg_string("fillColor").or_else(|| input.arg_string("fill_color")).unwrap_or_default(), "fontColor": input.arg_string("fontColor").or_else(|| input.arg_string("font_color")).unwrap_or_default(), "fontSize": arg_f64(input, "fontSize"), "fontName": input.arg_string("fontName").or_else(|| input.arg_string("font_name")).unwrap_or_default(), "horizontalAlign": input.arg_string("horizontalAlign").or_else(|| input.arg_string("horizontal_align")).unwrap_or_default(), "verticalAlign": input.arg_string("verticalAlign").or_else(|| input.arg_string("vertical_align")).unwrap_or_default(), "numberFormat": input.arg_string("numberFormat").or_else(|| input.arg_string("number_format")).unwrap_or_default() }), ) .await } pub async fn sheet_set_dimensions( context: &RequestContext, input: &ToolCallInput, ) -> Result { let column_index = arg_u64(input, "columnIndex"); let column_width = arg_f64(input, "columnWidth"); let row_index = arg_u64(input, "rowIndex"); let row_height = arg_f64(input, "rowHeight"); let has_column = column_index.is_some() && column_width.is_some(); let has_row = row_index.is_some() && row_height.is_some(); if !has_column && !has_row { return Err(WebError::bad_request_code( "mnote_onlyoffice_dimensions_required", "缺少 columnIndex/columnWidth 或 rowIndex/rowHeight", ) .with_context(context)); } run_write_action( context, input, "sheet.set_dimensions", json!({ "columnIndex": column_index, "columnWidth": column_width, "rowIndex": row_index, "rowHeight": row_height }), ) .await } pub async fn sheet_sort_range( context: &RequestContext, input: &ToolCallInput, ) -> Result { let address = input .arg_string("address") .or_else(|| input.arg_string("range")) .ok_or_else(|| { WebError::bad_request_code("mnote_onlyoffice_range_required", "缺少 address") .with_context(context) })?; let range = parse_a1_range(context, &address)?; ensure_sheet_cell_limit(context, range.row_count, range.col_count)?; let key_column = arg_u64(input, "keyColumn").unwrap_or(1).max(1); if key_column > range.col_count { return Err(WebError::bad_request_code( "mnote_onlyoffice_sort_key_out_of_range", "keyColumn 必须落在排序 range 内,且从 1 开始", ) .with_context(context)); } let order = input .arg_string("order") .unwrap_or_else(|| "ascending".into()) .to_ascii_lowercase(); if !matches!( order.as_str(), "ascending" | "descending" | "asc" | "desc" | "xlascending" | "xldescending" ) { return Err(WebError::bad_request_code( "mnote_onlyoffice_sort_order_invalid", "order 仅支持 ascending/descending", ) .with_context(context)); } run_write_action( context, input, "sheet.sort_range", json!({ "address": address, "keyColumn": key_column, "order": order, "header": arg_bool(input, "header").unwrap_or(true) }), ) .await } pub async fn sheet_add_chart( context: &RequestContext, input: &ToolCallInput, ) -> Result { let address = input .arg_string("address") .or_else(|| input.arg_string("range")) .ok_or_else(|| { WebError::bad_request_code("mnote_onlyoffice_range_required", "缺少 address") .with_context(context) })?; let range = parse_a1_range(context, &address)?; ensure_sheet_cell_limit(context, range.row_count, range.col_count)?; let chart_type = input .arg_string("chartType") .or_else(|| input.arg_string("chart_type")) .unwrap_or_else(|| "bar".into()); run_write_action( context, input, "sheet.add_chart", json!({ "address": address, "chartType": chart_type, "inColumns": arg_bool(input, "inColumns").unwrap_or(true), "style": arg_u64(input, "style").unwrap_or(2), "widthMm": arg_f64(input, "widthMm").unwrap_or(120.0), "heightMm": arg_f64(input, "heightMm").unwrap_or(80.0), "fromCol": arg_u64(input, "fromCol").unwrap_or(4), "xOffsetMm": arg_f64(input, "xOffsetMm").unwrap_or(0.0), "fromRow": arg_u64(input, "fromRow").unwrap_or(1), "yOffsetMm": arg_f64(input, "yOffsetMm").unwrap_or(0.0) }), ) .await } pub async fn presentation_add_text_slide( context: &RequestContext, input: &ToolCallInput, ) -> Result { run_write_action( context, input, "presentation.add_text_slide", json!({ "title": input.arg_string("title").unwrap_or_default(), "body": input.arg_string("body").unwrap_or_default() }), ) .await } pub async fn presentation_add_table( context: &RequestContext, input: &ToolCallInput, ) -> Result { let data = input.arg_value("data").unwrap_or_else(|| json!([])); let rows_from_data = data.as_array().map(|rows| rows.len() as u64).unwrap_or(0); let cols_from_data = data .as_array() .and_then(|rows| { rows.iter() .map(|row| row.as_array().map(|cells| cells.len() as u64).unwrap_or(1)) .max() }) .unwrap_or(0); let rows = arg_u64(input, "rows").unwrap_or(rows_from_data).max(1); let cols = arg_u64(input, "cols") .or_else(|| arg_u64(input, "columns")) .unwrap_or(cols_from_data) .max(1); if rows.saturating_mul(cols) > MAX_PRESENTATION_TABLE_CELLS { return Err(WebError::bad_request_code( "mnote_onlyoffice_presentation_table_too_large", format!("单次 PPT 表格最多支持 {MAX_PRESENTATION_TABLE_CELLS} 个单元格"), ) .with_context(context)); } let mut payload = json!({ "rows": rows, "cols": cols, "data": data, "widthMm": arg_f64(input, "widthMm").unwrap_or(190.0), "heightMm": arg_f64(input, "heightMm").unwrap_or(90.0) }); if let Some(slide_index) = arg_u64(input, "slideIndex") { payload["slideIndex"] = json!(slide_index); } if let Some(x_mm) = arg_f64(input, "xMm") { payload["xMm"] = json!(x_mm); } if let Some(y_mm) = arg_f64(input, "yMm") { payload["yMm"] = json!(y_mm); } run_write_action(context, input, "presentation.add_table", payload).await } pub async fn presentation_clear_slide( context: &RequestContext, input: &ToolCallInput, ) -> Result { let slide_index = required_u64(context, input, "slideIndex")?; run_write_action( context, input, "presentation.clear_slide", json!({ "slideIndex": slide_index }), ) .await } pub async fn presentation_add_shape( context: &RequestContext, input: &ToolCallInput, ) -> Result { let shape_type = input .arg_string("shapeType") .or_else(|| input.arg_string("shape_type")) .unwrap_or_else(|| "rect".into()); let allowed_shapes = [ "rect", "roundRect", "ellipse", "triangle", "diamond", "cube", "cloud", "flowChartMagneticTape", ]; if !allowed_shapes.contains(&shape_type.as_str()) { return Err(WebError::bad_request_code( "mnote_onlyoffice_shape_type_invalid", "shapeType 不在允许列表中", ) .with_context(context)); } let mut payload = json!({ "shapeType": shape_type, "text": input.arg_string("text").unwrap_or_default(), "fillColor": input.arg_string("fillColor").or_else(|| input.arg_string("fill_color")).unwrap_or_else(|| "#4F81BD".into()), "strokeColor": input.arg_string("strokeColor").or_else(|| input.arg_string("stroke_color")).unwrap_or_default(), "strokeWidthMm": arg_f64(input, "strokeWidthMm").unwrap_or(0.0), "xMm": arg_f64(input, "xMm").unwrap_or(20.0), "yMm": arg_f64(input, "yMm").unwrap_or(35.0), "widthMm": arg_f64(input, "widthMm").unwrap_or(160.0), "heightMm": arg_f64(input, "heightMm").unwrap_or(70.0) }); if let Some(slide_index) = arg_u64(input, "slideIndex") { payload["slideIndex"] = json!(slide_index); } run_write_action(context, input, "presentation.add_shape", payload).await } pub async fn presentation_replace_text( context: &RequestContext, input: &ToolCallInput, ) -> Result { let search = required_string(context, input, "search")?; if search.is_empty() { return Err( WebError::bad_request_code("mnote_onlyoffice_search_empty", "search 不能为空") .with_context(context), ); } let mut payload = json!({ "search": search, "replace": input.arg_string("replace").unwrap_or_default(), "matchCase": arg_bool(input, "matchCase").unwrap_or(false), "replaceAll": arg_bool(input, "replaceAll").unwrap_or(true) }); if let Some(slide_index) = arg_u64(input, "slideIndex") { payload["slideIndex"] = json!(slide_index); } run_write_action(context, input, "presentation.replace_text", payload).await } pub async fn presentation_set_shape_text( context: &RequestContext, input: &ToolCallInput, ) -> Result { let slide_index = required_u64(context, input, "slideIndex")?; let text = required_string(context, input, "text")?; let shape_index = arg_u64(input, "shapeIndex"); let shape_id = input.arg_string("shapeId"); if shape_index.is_none() && shape_id.as_deref().unwrap_or_default().trim().is_empty() { return Err(WebError::bad_request_code( "mnote_onlyoffice_shape_locator_required", "缺少 shapeIndex 或 shapeId", ) .with_context(context)); } run_write_action( context, input, "presentation.set_shape_text", json!({ "slideIndex": slide_index, "shapeIndex": shape_index, "shapeId": shape_id.unwrap_or_default(), "text": text }), ) .await } pub async fn presentation_delete_slide( context: &RequestContext, input: &ToolCallInput, ) -> Result { let slide_index = required_u64(context, input, "slideIndex")?; run_write_action( context, input, "presentation.delete_slide", json!({ "slideIndex": slide_index }), ) .await } async fn run_read_action( context: &RequestContext, input: &ToolCallInput, action: &'static str, payload: Value, ) -> Result { let session_id = resolve_explicit_session_id(context, input)?; ensure_onlyoffice_resource_scope_allowed(context, input, &session_id)?; let result = run_bridge(context, input, &session_id, action, payload).await?; Ok(action_result(action, &session_id, result)) } async fn run_write_action( context: &RequestContext, input: &ToolCallInput, action: &'static str, payload: Value, ) -> Result { ensure_write_authorized(context, input)?; let session_id = resolve_explicit_session_id(context, input)?; ensure_onlyoffice_resource_scope_allowed(context, input, &session_id)?; if input.dry_run.unwrap_or(false) { return Ok(json!({ "schema": "mnote.onlyoffice.action_plan.v1", "sessionId": session_id, "action": action, "payload": payload, "dryRun": true, "wouldWrite": true })); } let result = run_bridge(context, input, &session_id, action, payload).await?; Ok(action_result(action, &session_id, result)) } async fn run_bridge( context: &RequestContext, input: &ToolCallInput, session_id: &str, action: &'static str, payload: Value, ) -> Result { let timeout_ms = input .arg_value("timeoutMs") .or_else(|| input.arg_value("timeout_ms")) .and_then(|value| value.as_u64()) .unwrap_or(DEFAULT_TIMEOUT_MS); let result = onlyoffice_bridge::run_bridge_command(session_id, action, payload, timeout_ms) .await .map_err(|error| bridge_run_error(context, error))?; if !result.ok { return Err(WebError::bad_request_code( "mnote_onlyoffice_bridge_failed", result .error .clone() .unwrap_or_else(|| "ONLYOFFICE bridge 命令执行失败".into()), ) .with_context(context)); } Ok(result) } fn action_result(action: &str, session_id: &str, result: BridgeResultWire) -> Value { json!({ "schema": "mnote.onlyoffice.action_result.v1", "sessionId": session_id, "action": action, "commandId": result.id, "result": result.result }) } fn resolve_explicit_session_id( context: &RequestContext, input: &ToolCallInput, ) -> Result { input .arg_string("onlyofficeSessionId") .or_else(|| input.arg_string("bridgeSessionId")) .ok_or_else(|| { WebError::bad_request_code( "mnote_onlyoffice_session_explicit_required", "ONLYOFFICE 工具必须显式携带 onlyofficeSessionId", ) .with_context(context) }) } fn ensure_onlyoffice_resource_scope_allowed( context: &RequestContext, input: &ToolCallInput, session_id: &str, ) -> Result<(), WebError> { let Some(scope) = input.arg_value("aiAccessScope") else { return Err(WebError::new( StatusCode::FORBIDDEN, "mnote_onlyoffice_resource_scope_required", "ONLYOFFICE 工具必须携带 aiAccessScope.allowedResourceIds", ) .with_context(context)); }; let allowed = scope .get("allowedResourceIds") .or_else(|| scope.get("allowed_resource_ids")) .and_then(Value::as_array) .map(|values| { values .iter() .filter_map(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned) .collect::>() }) .unwrap_or_default(); if allowed.is_empty() { return Err(WebError::new( StatusCode::FORBIDDEN, "mnote_onlyoffice_resource_scope_required", "ONLYOFFICE 工具必须携带非空 aiAccessScope.allowedResourceIds", ) .with_context(context)); } let Some(session) = onlyoffice_bridge::session_info(session_id) else { return Err(WebError::new( StatusCode::FORBIDDEN, "mnote_onlyoffice_resource_scope_forbidden", "ONLYOFFICE session 未注册,无法校验资源授权", ) .with_context(context)); }; let mut candidates = HashSet::new(); candidates.insert(session.session_id); let document_id = session.document_id; let asset_id = session.asset_id; if let Some(document_id) = document_id.as_deref() { candidates.insert(document_id.to_string()); } if let Some(asset_id) = asset_id.as_deref() { candidates.insert(asset_id.to_string()); } if let (Some(document_id), Some(asset_id)) = (document_id.as_deref(), asset_id.as_deref()) { candidates.insert(format!("resource:office:{document_id}:{asset_id}")); candidates.insert(format!("resource:onlyoffice:{document_id}:{asset_id}")); } if candidates .iter() .any(|candidate| allowed.contains(candidate)) { return Ok(()); } Err(WebError::new( StatusCode::FORBIDDEN, "mnote_onlyoffice_resource_scope_forbidden", "ONLYOFFICE session 不在当前 AI resource 授权范围内", ) .with_context(context)) } fn current_session(context: &RequestContext, input: &ToolCallInput) -> Result { let session_id = resolve_explicit_session_id(context, input)?; ensure_onlyoffice_resource_scope_allowed(context, input, &session_id)?; let session = onlyoffice_bridge::session_info(&session_id).ok_or_else(|| { WebError::new( StatusCode::NOT_FOUND, "mnote_onlyoffice_session_not_found", "指定的 ONLYOFFICE bridge session 不存在,请先打开 ONLYOFFICE 文档", ) .with_context(context) })?; Ok(json!({ "sessionId": session.session_id, "editorType": session.editor_type.unwrap_or_default(), "documentId": session.document_id.unwrap_or_default(), "assetId": session.asset_id.unwrap_or_default(), "fileType": session.file_type.unwrap_or_default(), "lastSeenMillis": session.last_seen_millis, "pendingCommands": session.pending_commands, "pendingResults": session.pending_results })) } fn required_string( context: &RequestContext, input: &ToolCallInput, key: &'static str, ) -> Result { input.arg_string(key).ok_or_else(|| { WebError::bad_request_code("mnote_onlyoffice_argument_required", format!("缺少 {key}")) .with_context(context) }) } fn required_u64( context: &RequestContext, input: &ToolCallInput, key: &'static str, ) -> Result { arg_u64(input, key).ok_or_else(|| { WebError::bad_request_code("mnote_onlyoffice_argument_required", format!("缺少 {key}")) .with_context(context) }) } fn arg_u64(input: &ToolCallInput, key: &'static str) -> Option { input .arg_value(key) .or_else(|| match key { "startRow" => input.arg_value("start_row"), "startCol" => input.arg_value("start_col"), "rowCount" => input.arg_value("row_count"), "colCount" => input.arg_value("col_count"), "slideIndex" => input.arg_value("slide_index"), "sheetIndex" => input.arg_value("sheet_index"), "shapeIndex" => input.arg_value("shape_index"), "columnIndex" => input.arg_value("column_index"), "rowIndex" => input.arg_value("row_index"), "keyColumn" => input.arg_value("key_column"), "fromCol" => input.arg_value("from_col"), "fromRow" => input.arg_value("from_row"), "cols" => input.arg_value("columns"), _ => None, }) .and_then(|value| value.as_u64()) } fn arg_f64(input: &ToolCallInput, key: &'static str) -> Option { input .arg_value(key) .or_else(|| match key { "columnWidth" => input.arg_value("column_width"), "rowHeight" => input.arg_value("row_height"), "fontSize" => input.arg_value("font_size"), "widthMm" => input.arg_value("width_mm"), "heightMm" => input.arg_value("height_mm"), "xOffsetMm" => input.arg_value("x_offset_mm"), "yOffsetMm" => input.arg_value("y_offset_mm"), "xMm" => input.arg_value("x_mm"), "yMm" => input.arg_value("y_mm"), "strokeWidthMm" => input.arg_value("stroke_width_mm"), _ => None, }) .and_then(|value| value.as_f64()) .filter(|value| value.is_finite() && *value >= 0.0) } fn arg_bool(input: &ToolCallInput, key: &'static str) -> Option { input .arg_value(key) .or_else(|| match key { "matchCase" => input.arg_value("match_case"), "replaceAll" => input.arg_value("replace_all"), "inColumns" => input.arg_value("in_columns"), _ => None, }) .and_then(|value| value.as_bool()) } fn ensure_sheet_cell_limit( context: &RequestContext, row_count: u64, col_count: u64, ) -> Result<(), WebError> { if row_count == 0 || col_count == 0 { return Err(WebError::bad_request_code( "mnote_onlyoffice_sheet_range_empty", "rowCount 和 colCount 必须大于 0", ) .with_context(context)); } if row_count.saturating_mul(col_count) > MAX_SHEET_BATCH_CELLS { return Err(WebError::bad_request_code( "mnote_onlyoffice_sheet_range_too_large", format!("单次表格批量操作最多支持 {MAX_SHEET_BATCH_CELLS} 个单元格"), ) .with_context(context)); } Ok(()) } #[derive(Debug)] struct A1Range { row_count: u64, col_count: u64, } fn parse_a1_range(context: &RequestContext, address: &str) -> Result { let value = address.trim(); let mut parts = value.split(':'); let start = parts.next().unwrap_or_default(); let end = parts.next().unwrap_or(start); if parts.next().is_some() { return Err(invalid_a1_range(context)); } let (start_row, start_col) = parse_a1_cell(start).ok_or_else(|| invalid_a1_range(context))?; let (end_row, end_col) = parse_a1_cell(end).ok_or_else(|| invalid_a1_range(context))?; Ok(A1Range { row_count: start_row.abs_diff(end_row) + 1, col_count: start_col.abs_diff(end_col) + 1, }) } fn parse_a1_cell(value: &str) -> Option<(u64, u64)> { let value = value.trim(); let split_at = value .char_indices() .find_map(|(index, ch)| ch.is_ascii_digit().then_some(index))?; let (letters, digits) = value.split_at(split_at); if letters.is_empty() || digits.is_empty() || !letters.chars().all(|ch| ch.is_ascii_alphabetic()) || !digits.chars().all(|ch| ch.is_ascii_digit()) { return None; } let mut col = 0_u64; for ch in letters.chars() { col = col .saturating_mul(26) .saturating_add((ch.to_ascii_uppercase() as u8 - b'A' + 1) as u64); } let row = digits.parse::().ok()?; if row == 0 || col == 0 { return None; } Some((row - 1, col - 1)) } fn invalid_a1_range(context: &RequestContext) -> WebError { WebError::bad_request_code( "mnote_onlyoffice_range_invalid", "address 必须是 A1 或 A1:B2", ) .with_context(context) } fn bridge_run_error(context: &RequestContext, error: BridgeRunError) -> WebError { match error { BridgeRunError::BadRequest(message) => { WebError::bad_request_code("mnote_onlyoffice_bridge_bad_request", message) } BridgeRunError::Timeout { session_id, command_id, } => WebError::new( StatusCode::GATEWAY_TIMEOUT, "mnote_onlyoffice_bridge_timeout", format!("ONLYOFFICE bridge 命令超时,sessionId={session_id}, commandId={command_id}"), ), } .with_context(context) }