use crate::app::AppConfig; use crate::context::RequestContext; use crate::error::WebError; use bridge_runtime::{ build_runtime_command_artifact_plan, RuntimeCommandArtifactPlan, RuntimeCommandExecutionPlan, RuntimeQueryExecutionPlan, }; use serde_json::Value; use time::{format_description::well_known::Rfc3339, OffsetDateTime}; #[derive(Debug)] pub struct RetiredCloudCommandExecution { pub result: Value, pub artifacts: Option, pub artifact_error: Option, } fn retired_error(context: &RequestContext, phase: &'static str) -> WebError { WebError::service_unavailable_code( "convex_retired", "Convex 运行时已退役;请使用 local-first Rust/SQLite control-plane 路径", ) .with_context(context) .with_header("x-error-phase", phase) .with_header("x-upstream-service", "convex-retired") } fn load_query_fixture( config: &AppConfig, context: &RequestContext, plan: &RuntimeQueryExecutionPlan, ) -> Result, WebError> { if !config.allow_dev_fixtures { return Ok(None); } let Some(raw) = config.query_fixtures_json.as_deref() else { return Ok(None); }; let trimmed = raw.trim(); if trimmed.is_empty() { return Ok(None); } let fixtures: Value = serde_json::from_str(trimmed).map_err(|error| { WebError::internal(format!("MNOTE_WEB_QUERY_FIXTURES_JSON 非法: {error}")) .with_context(context) .with_header("x-error-phase", "fixture_parse") })?; Ok(fixtures .as_object() .and_then(|map| map.get(plan.function_name.as_str())) .cloned()) } fn load_mutation_fixture_by_name( config: &AppConfig, context: &RequestContext, function_name: &str, ) -> Result, WebError> { if !config.allow_dev_fixtures { return Ok(None); } let Some(raw) = config.mutation_fixtures_json.as_deref() else { return Ok(None); }; let trimmed = raw.trim(); if trimmed.is_empty() { return Ok(None); } let fixtures: Value = serde_json::from_str(trimmed).map_err(|error| { WebError::internal(format!("MNOTE_WEB_MUTATION_FIXTURES_JSON 非法: {error}")) .with_context(context) .with_header("x-error-phase", "fixture_parse") })?; Ok(fixtures .as_object() .and_then(|map| map.get(function_name)) .cloned()) } fn load_mutation_fixture( config: &AppConfig, context: &RequestContext, plan: &RuntimeCommandExecutionPlan, ) -> Result, WebError> { load_mutation_fixture_by_name(config, context, plan.function_name.as_str()) } pub async fn execute_retired_query_plan( config: &AppConfig, context: &RequestContext, plan: &RuntimeQueryExecutionPlan, ) -> Result { if plan.function_name.trim().is_empty() || plan.function_name.ends_with(":unknown") { return Err(WebError::bad_request_code( "transport_query_unsupported", format!("mnote-web transport 暂不支持 query: {}", plan.function_name), ) .with_context(context) .with_header("x-error-phase", "plan_validation")); } if let Some(fixture) = load_query_fixture(config, context, plan)? { return Ok(fixture); } Err(retired_error(context, "convex_query_retired")) } pub async fn execute_sidebar_dataset_query( config: &AppConfig, context: &RequestContext, plan: &RuntimeQueryExecutionPlan, ) -> Result { execute_retired_query_plan(config, context, plan).await } pub async fn execute_retired_query_by_name( config: &AppConfig, context: &RequestContext, function_name: &str, args: Value, workspace_id: Option<&str>, error_phase: &'static str, ) -> Result { let plan = RuntimeQueryExecutionPlan { query_name: function_name.to_string(), function_name: function_name.to_string(), workspace_id: workspace_id.map(ToOwned::to_owned), request_id: context.trace.request_id.clone(), trace_id: context.trace.trace_id.clone(), actor_id: context.auth.actor_id.clone(), payload_json: args.to_string(), args_json: args, }; execute_retired_query_plan(config, context, &plan) .await .map_err(|error| error.with_header("x-error-phase", error_phase)) } pub async fn execute_retired_command_plan( config: &AppConfig, context: &RequestContext, plan: &RuntimeCommandExecutionPlan, ) -> Result { if plan.function_name.trim().is_empty() || plan.function_name.ends_with(":unknown") { return Err(WebError::bad_request_code( "transport_command_unsupported", format!( "mnote-web transport 暂不支持 command: {}", plan.function_name ), ) .with_context(context) .with_header("x-error-phase", "plan_validation")); } if let Some(fixture) = load_mutation_fixture(config, context, plan)? { return Ok(fixture); } Err(retired_error(context, "convex_mutation_retired")) } pub async fn execute_retired_mutation_by_name( config: &AppConfig, context: &RequestContext, function_name: &str, args: Value, _workspace_id: Option<&str>, _idempotency_key: Option<&str>, error_phase: &'static str, ) -> Result { if let Some(fixture) = load_mutation_fixture_by_name(config, context, function_name)? { return Ok(fixture); } let _ = args; Err(retired_error(context, error_phase)) } pub async fn persist_runtime_command_artifacts( _config: &AppConfig, _context: &RequestContext, _artifacts: &RuntimeCommandArtifactPlan, ) -> Result<(), WebError> { // Convex 已退役。Rust 侧仍会把 artifact plan 返回给调用方和 realtime // consumer;这里保持 no-op,避免兼容路径因为历史持久化层退役而失败。 Ok(()) } pub async fn execute_retired_command_plan_with_artifacts( config: &AppConfig, context: &RequestContext, runtime_context: &bridge_runtime::RuntimeBridgeContextWire, command: &bridge_runtime::RuntimeCommandEnvelopeWire, plan: &RuntimeCommandExecutionPlan, ) -> Result { let result = execute_retired_command_plan(config, context, plan).await?; let now = OffsetDateTime::now_utc() .format(&Rfc3339) .unwrap_or_else(|_| "1970-01-01T00:00:00Z".into()); let artifacts = build_runtime_command_artifact_plan(runtime_context, command, plan, &result, &now); let artifact_error = if let Some(artifacts) = artifacts.as_ref() { persist_runtime_command_artifacts(config, context, artifacts) .await .err() .map(|error| error.message().to_string()) } else { None }; Ok(RetiredCloudCommandExecution { result, artifacts, artifact_error, }) }