165 lines
5.5 KiB
Rust
165 lines
5.5 KiB
Rust
use crate::app::{AppConfig, AppState};
|
|
use crate::context::RequestContext;
|
|
use crate::error::WebError;
|
|
use crate::transport::legacy_cloud_guard::{
|
|
execute_retired_command_plan, execute_retired_command_plan_with_artifacts,
|
|
RetiredCloudCommandExecution,
|
|
};
|
|
use bridge_runtime::{
|
|
execute_runtime_input, RuntimeActorWire, RuntimeBridgeContextWire, RuntimeCommandEnvelopeWire,
|
|
RuntimeCommandExecutionPlan, RuntimeExecutionPlan, RuntimeInput, RuntimeSourceWire,
|
|
RuntimeTargetWire,
|
|
};
|
|
use serde_json::json;
|
|
use serde_json::Value;
|
|
|
|
pub fn runtime_context(
|
|
context: &RequestContext,
|
|
effective_workspace_id: Option<&str>,
|
|
) -> RuntimeBridgeContextWire {
|
|
RuntimeBridgeContextWire {
|
|
deployment_id: context.workspace.deployment_id.clone(),
|
|
project_id: context.workspace.project_id.clone(),
|
|
workspace_id: effective_workspace_id
|
|
.map(ToOwned::to_owned)
|
|
.or_else(|| context.workspace.workspace_id.clone()),
|
|
request_id: context.trace.request_id.clone(),
|
|
trace_id: context.trace.trace_id.clone(),
|
|
actor: RuntimeActorWire {
|
|
actor_type: context.auth.actor_type.clone(),
|
|
actor_id: context.auth.actor_id.clone(),
|
|
session_id: context.auth.session_id.clone(),
|
|
},
|
|
source: RuntimeSourceWire {
|
|
channel: context.source.channel.clone(),
|
|
client: context.source.client.clone(),
|
|
source_kind: None,
|
|
root_uri: None,
|
|
workspace_id: None,
|
|
capabilities: Vec::new(),
|
|
},
|
|
tenant_id: context.workspace.tenant_id.clone(),
|
|
auth_token: context.auth.authorization.clone(),
|
|
idempotency_key: context.source.idempotency_key.clone(),
|
|
validate_only: false,
|
|
dry_run: false,
|
|
}
|
|
}
|
|
|
|
pub fn build_runtime_command_plan(
|
|
context: &RequestContext,
|
|
effective_workspace_id: Option<&str>,
|
|
command: RuntimeCommandEnvelopeWire,
|
|
) -> Result<RuntimeCommandExecutionPlan, WebError> {
|
|
let runtime_input = RuntimeInput::Command {
|
|
context: runtime_context(context, effective_workspace_id),
|
|
command,
|
|
};
|
|
let RuntimeExecutionPlan::Command(plan) = execute_runtime_input(runtime_input)
|
|
.map_err(|error| WebError::bad_request(error.message).with_context(context))?
|
|
else {
|
|
return Err(WebError::internal("runtime command 未返回 command plan").with_context(context));
|
|
};
|
|
|
|
Ok(plan)
|
|
}
|
|
|
|
pub async fn execute_runtime_command_via_legacy_cloud(
|
|
config: &AppConfig,
|
|
context: &RequestContext,
|
|
effective_workspace_id: Option<&str>,
|
|
command: RuntimeCommandEnvelopeWire,
|
|
) -> Result<Value, WebError> {
|
|
let plan = build_runtime_command_plan(context, effective_workspace_id, command)?;
|
|
execute_retired_command_plan(config, context, &plan).await
|
|
}
|
|
|
|
pub async fn execute_runtime_command_via_legacy_cloud_with_artifacts(
|
|
state: &AppState,
|
|
context: &RequestContext,
|
|
effective_workspace_id: Option<&str>,
|
|
command: RuntimeCommandEnvelopeWire,
|
|
) -> Result<RetiredCloudCommandExecution, WebError> {
|
|
let runtime_context = runtime_context(context, effective_workspace_id);
|
|
let runtime_input = RuntimeInput::Command {
|
|
context: runtime_context.clone(),
|
|
command: command.clone(),
|
|
};
|
|
let RuntimeExecutionPlan::Command(plan) = execute_runtime_input(runtime_input)
|
|
.map_err(|error| WebError::bad_request(error.message).with_context(context))?
|
|
else {
|
|
return Err(WebError::internal("runtime command 未返回 command plan").with_context(context));
|
|
};
|
|
|
|
let execution = execute_retired_command_plan_with_artifacts(
|
|
state.config(),
|
|
context,
|
|
&runtime_context,
|
|
&command,
|
|
&plan,
|
|
)
|
|
.await?;
|
|
|
|
// Push stream delta notification via broadcast for WebSocket/SSE push consumers
|
|
let workspace_id = effective_workspace_id
|
|
.map(ToOwned::to_owned)
|
|
.or_else(|| context.workspace.workspace_id.clone());
|
|
let delta = json!({
|
|
"kind": "command_committed",
|
|
"commandName": command.name,
|
|
"commandId": command.command_id,
|
|
"workspaceId": workspace_id,
|
|
"requestId": context.trace.request_id,
|
|
"traceId": context.trace.trace_id,
|
|
});
|
|
let _ = state.stream_delta_tx.send(delta);
|
|
|
|
Ok(execution)
|
|
}
|
|
|
|
pub fn build_tree_target(
|
|
workspace_id: &str,
|
|
page_id: Option<&str>,
|
|
block_id: Option<&str>,
|
|
) -> RuntimeTargetWire {
|
|
RuntimeTargetWire {
|
|
workspace_id: Some(workspace_id.to_string()),
|
|
page_id: page_id.map(ToOwned::to_owned),
|
|
block_id: block_id.map(ToOwned::to_owned),
|
|
}
|
|
}
|
|
|
|
pub fn ensure_non_empty(
|
|
value: &str,
|
|
field: &'static str,
|
|
context: &RequestContext,
|
|
) -> Result<String, WebError> {
|
|
let trimmed = value.trim();
|
|
if trimmed.is_empty() {
|
|
return Err(WebError::bad_request_code(
|
|
"tree_command_validation",
|
|
format!("{field} 不能为空"),
|
|
)
|
|
.with_context(context)
|
|
.with_header("x-error-phase", "tree_command_validate"));
|
|
}
|
|
Ok(trimmed.to_string())
|
|
}
|
|
|
|
pub fn read_optional_non_empty(value: Option<String>) -> Option<String> {
|
|
value
|
|
.map(|item| item.trim().to_string())
|
|
.filter(|item| !item.is_empty())
|
|
}
|
|
|
|
pub fn ensure_sort_order(sort_order: i64, context: &RequestContext) -> Result<i64, WebError> {
|
|
if sort_order < 0 {
|
|
return Err(
|
|
WebError::bad_request_code("tree_command_validation", "sortOrder 不能小于 0")
|
|
.with_context(context)
|
|
.with_header("x-error-phase", "tree_command_validate"),
|
|
);
|
|
}
|
|
Ok(sort_order)
|
|
}
|