feat: cut over rust web main shell
This commit is contained in:
@@ -1,15 +1,18 @@
|
||||
use crate::app::AppState;
|
||||
use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use axum::Json;
|
||||
use axum::extract::{Extension, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
|
||||
use axum::Json;
|
||||
use bridge_runtime::{
|
||||
RuntimeInput, build_failure_response, build_success_response, execute_runtime_input,
|
||||
execute_runtime_query, runtime_input_requests_result,
|
||||
build_failure_response, build_success_response, execute_runtime_input, execute_runtime_query,
|
||||
runtime_input_requests_result, RuntimeInput,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use serde_json::{Value, json};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
const HEADER_MNOTE_WEB_OWNER: &str = "x-mnote-web-owner";
|
||||
const HEADER_AI_BRIDGE_OWNER: &str = "x-mnote-ai-bridge-owner";
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -37,7 +40,7 @@ pub async fn health(
|
||||
pub async fn bridge_runtime(
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Json(runtime_input): Json<RuntimeInput>,
|
||||
) -> Result<(StatusCode, Json<Value>), WebError> {
|
||||
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
||||
let payload = if runtime_input_requests_result(&runtime_input) {
|
||||
match execute_runtime_query(runtime_input) {
|
||||
Ok(result) => json!({
|
||||
@@ -45,12 +48,14 @@ pub async fn bridge_runtime(
|
||||
"bridge": "hermes_runtime_result",
|
||||
"requestId": context.trace.request_id,
|
||||
"traceId": context.trace.trace_id,
|
||||
"contract": ai_bridge_contract(),
|
||||
"result": result,
|
||||
}),
|
||||
Err(error) => {
|
||||
let failure = build_failure_response(error);
|
||||
return Ok((
|
||||
StatusCode::BAD_REQUEST,
|
||||
stamp_ai_bridge_headers(),
|
||||
Json(serde_json::to_value(failure).expect("runtime failure 应可序列化")),
|
||||
));
|
||||
}
|
||||
@@ -64,6 +69,7 @@ pub async fn bridge_runtime(
|
||||
"bridge": "hermes_runtime_plan",
|
||||
"requestId": context.trace.request_id,
|
||||
"traceId": context.trace.trace_id,
|
||||
"contract": ai_bridge_contract(),
|
||||
"plan": success.plan,
|
||||
})
|
||||
}
|
||||
@@ -71,11 +77,140 @@ pub async fn bridge_runtime(
|
||||
let failure = build_failure_response(error);
|
||||
return Ok((
|
||||
StatusCode::BAD_REQUEST,
|
||||
stamp_ai_bridge_headers(),
|
||||
Json(serde_json::to_value(failure).expect("runtime failure 应可序列化")),
|
||||
));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok((StatusCode::OK, Json(payload)))
|
||||
Ok((StatusCode::OK, stamp_ai_bridge_headers(), Json(payload)))
|
||||
}
|
||||
|
||||
fn ai_bridge_contract() -> Value {
|
||||
json!({
|
||||
"schema": "mnote.ai_bridge.v1",
|
||||
"owner": "mnote-web",
|
||||
"bridge": "hermes",
|
||||
"sessionOwner": "rust-web-hermes",
|
||||
"toolEventOwner": "rust-web-hermes",
|
||||
"clientActionOwner": "rust-web-hermes"
|
||||
})
|
||||
}
|
||||
|
||||
fn stamp_ai_bridge_headers() -> HeaderMap {
|
||||
let mut headers = HeaderMap::new();
|
||||
if let Ok(name) = HeaderName::from_lowercase(HEADER_MNOTE_WEB_OWNER.as_bytes()) {
|
||||
headers.insert(name, HeaderValue::from_static("mnote-web"));
|
||||
}
|
||||
if let Ok(name) = HeaderName::from_lowercase(HEADER_AI_BRIDGE_OWNER.as_bytes()) {
|
||||
headers.insert(name, HeaderValue::from_static("rust-web-hermes"));
|
||||
}
|
||||
headers
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app::{build_app, AppConfig, AppState};
|
||||
use axum::body::{to_bytes, Body};
|
||||
use axum::http::{Request, StatusCode};
|
||||
use serde_json::{json, Value};
|
||||
use tower::util::ServiceExt;
|
||||
|
||||
fn app() -> axum::Router {
|
||||
build_app(AppState::new(AppConfig {
|
||||
service_name: "mnote-web".into(),
|
||||
service_version: "0.1.0".into(),
|
||||
bind_addr: "127.0.0.1:0".into(),
|
||||
public_bind_addr: "127.0.0.1:3000".into(),
|
||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
||||
enable_legacy_next_compat: true,
|
||||
enable_debug_shell_routes: false,
|
||||
hermes_base_path: "/api/hermes".into(),
|
||||
compat_next_base_path: "/api/compat/next".into(),
|
||||
convex_url: None,
|
||||
convex_admin_key: None,
|
||||
allow_dev_fixtures: true,
|
||||
query_fixtures_json: None,
|
||||
mutation_fixtures_json: None,
|
||||
dev_user_id: "dev-user".into(),
|
||||
dev_user_name: "开发用户".into(),
|
||||
dev_user_email: "dev@mnote.local".into(),
|
||||
}))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ai_bridge_route_returns_hermes_owner_contract() {
|
||||
let response = app()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/hermes/bridge")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"kind": "tool",
|
||||
"context": {
|
||||
"deploymentId": null,
|
||||
"projectId": null,
|
||||
"workspaceId": "ws_demo",
|
||||
"requestId": "req_1",
|
||||
"traceId": "trace_1",
|
||||
"actor": {
|
||||
"actorType": "user",
|
||||
"actorId": "user_1",
|
||||
"sessionId": null
|
||||
},
|
||||
"source": {
|
||||
"channel": "rust-web",
|
||||
"client": "mnote-web"
|
||||
},
|
||||
"tenantId": null,
|
||||
"authToken": null,
|
||||
"idempotencyKey": null,
|
||||
"validateOnly": false,
|
||||
"dryRun": false
|
||||
},
|
||||
"tool": {
|
||||
"tool": "docs_search",
|
||||
"kind": "query",
|
||||
"mode": "plan",
|
||||
"argsJson": {"query": "Rust Web"},
|
||||
"target": null,
|
||||
"reason": "owner gate",
|
||||
"refs": []
|
||||
},
|
||||
"data": null
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get("x-mnote-web-owner")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("mnote-web")
|
||||
);
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get("x-mnote-ai-bridge-owner")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("rust-web-hermes")
|
||||
);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["contract"]["schema"], "mnote.ai_bridge.v1");
|
||||
assert_eq!(payload["contract"]["sessionOwner"], "rust-web-hermes");
|
||||
assert_eq!(payload["contract"]["toolEventOwner"], "rust-web-hermes");
|
||||
assert_eq!(payload["contract"]["clientActionOwner"], "rust-web-hermes");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user