Files
mnote/rust/crates/mnote-web/src/transport/convex.rs
T
lix-2026 1882db7681 收口 MNote P0 P1 P2 审查尾项
- 归档 OnlyOffice live bridge、Page AI、mindmap、design governance 与相关 bug 条目
- 补齐 MinerU OCR 后端 runtime 合同与 smoke/test 基线
- 收口 ChatOnly/Doubao、ObjectIdentity、Page Aggregate compat 与 runtime owner 文档口径

验证:
- cargo test --manifest-path rust/Cargo.toml -p mnote-web local_ocr -- --test-threads=1
- cargo test --manifest-path rust/Cargo.toml -p mnote-web onlyoffice_bridge -- --test-threads=1
- git diff --check
- git diff --cached --check
- codegraph index . --force && codegraph status .
- codegraph sync . && codegraph status .
2026-06-01 09:29:12 +08:00

224 lines
6.9 KiB
Rust

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<RuntimeCommandArtifactPlan>,
pub artifact_error: Option<String>,
}
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<Option<Value>, 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<Option<Value>, 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<Option<Value>, 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<Value, WebError> {
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<Value, WebError> {
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<Value, WebError> {
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<Value, WebError> {
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<Value, WebError> {
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<RetiredCloudCommandExecution, WebError> {
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,
})
}