收口 Rust Web 入口与 AI 写入链

- 将 3000 主入口继续收口到 mnote-web,补齐 /favicon.ico、/api/auth、session alias、AI run 等 Rust Web 路由边界。

- 更新登录页与 Convex Auth 代理,支持测试账号快速登录写入真实 Convex Auth cookie。

- 推进页面设置、Wolai 对齐、Phase 7 AI kernel/CLI-first 设计文档与相关 smoke 脚本。

- 更新 leptos-tiptap 生成资产、mnote-cli/bridge-runtime、前端依赖和 dev/prod 启动脚本。
This commit is contained in:
lix-2026
2026-05-06 21:44:20 +08:00
parent 98b6360595
commit e8ba12e461
86 changed files with 8872 additions and 1716 deletions
+138 -20
View File
@@ -28,6 +28,7 @@ pub struct SessionResponse {
pub email: String,
pub name: String,
pub actor_type: String,
pub auth_mode: &'static str,
pub request_id: String,
pub trace_id: String,
}
@@ -47,36 +48,57 @@ pub async fn session(
State(state): State<AppState>,
Extension(context): Extension<RequestContext>,
) -> Response {
owner_json(Json(SessionResponse {
ok: true,
owner: "mnote-web",
user_id: state.config().dev_user_id.clone(),
email: state.config().dev_user_email.clone(),
name: state.config().dev_user_name.clone(),
actor_type: context.auth.actor_type,
request_id: context.trace.request_id,
trace_id: context.trace.trace_id,
}))
owner_json(Json(build_session_response(&state, context)))
}
pub async fn refresh_session(
State(state): State<AppState>,
Extension(context): Extension<RequestContext>,
) -> Response {
let mut response = owner_json(Json(SessionResponse {
ok: true,
owner: "mnote-web",
user_id: state.config().dev_user_id.clone(),
email: state.config().dev_user_email.clone(),
name: state.config().dev_user_name.clone(),
actor_type: context.auth.actor_type,
request_id: context.trace.request_id,
trace_id: context.trace.trace_id,
}));
let mut response = owner_json(Json(build_session_response(&state, context)));
*response.status_mut() = StatusCode::OK;
response
}
fn build_session_response(state: &AppState, context: RequestContext) -> SessionResponse {
let actor_id = context.auth.actor_id.trim();
let has_forwarded_actor = !actor_id.is_empty() && actor_id != "anonymous";
let user_id = if has_forwarded_actor {
actor_id.to_string()
} else {
state.config().dev_user_id.clone()
};
let actor_type = if has_forwarded_actor {
context.auth.actor_type
} else {
"devFallback".to_string()
};
SessionResponse {
ok: true,
owner: "mnote-web",
user_id,
email: if has_forwarded_actor {
String::new()
} else {
state.config().dev_user_email.clone()
},
name: if has_forwarded_actor {
actor_id.to_string()
} else {
state.config().dev_user_name.clone()
},
actor_type,
auth_mode: if has_forwarded_actor {
"forwardedActor"
} else {
"devFallback"
},
request_id: context.trace.request_id,
trace_id: context.trace.trace_id,
}
}
fn owner_json<T>(payload: Json<T>) -> Response
where
T: Serialize,
@@ -172,4 +194,100 @@ mod tests {
assert_eq!(payload["userId"], "dev-user");
assert!(payload.get("convexAdminKey").is_none());
}
#[tokio::test]
async fn session_prefers_forwarded_actor_identity_over_dev_fallback() {
let response = app()
.oneshot(
Request::builder()
.uri("/api/auth/session")
.header("x-mnote-actor-id", "user_real")
.header("x-mnote-actor-type", "user")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::OK);
let body = to_bytes(response.into_body(), usize::MAX)
.await
.expect("body");
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json");
assert_eq!(payload["owner"], "mnote-web");
assert_eq!(payload["userId"], "user_real");
assert_eq!(payload["actorType"], "user");
assert_eq!(payload["authMode"], "forwardedActor");
assert!(payload.get("convexAdminKey").is_none());
}
#[tokio::test]
async fn legacy_whoami_alias_prefers_forwarded_actor_identity() {
let response = app()
.oneshot(
Request::builder()
.uri("/api/auth/whoami")
.header("x-mnote-actor-id", "user_real")
.header("x-mnote-actor-type", "user")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::OK);
let body = to_bytes(response.into_body(), usize::MAX)
.await
.expect("body");
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json");
assert_eq!(payload["owner"], "mnote-web");
assert_eq!(payload["userId"], "user_real");
assert_eq!(payload["actorType"], "user");
assert_eq!(payload["authMode"], "forwardedActor");
assert_ne!(payload["code"], "legacy_next_compat_disabled");
}
#[tokio::test]
async fn legacy_whoami_alias_returns_dev_identity() {
let response = app()
.oneshot(
Request::builder()
.uri("/api/auth/whoami")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::OK);
let body = to_bytes(response.into_body(), usize::MAX)
.await
.expect("body");
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json");
assert_eq!(payload["owner"], "mnote-web");
assert_eq!(payload["userId"], "dev-user");
assert_ne!(payload["code"], "legacy_next_compat_disabled");
}
#[tokio::test]
async fn legacy_mnote_web_token_alias_returns_dev_identity() {
let response = app()
.oneshot(
Request::builder()
.uri("/api/auth/mnote-web-token")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::OK);
let body = to_bytes(response.into_body(), usize::MAX)
.await
.expect("body");
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json");
assert_eq!(payload["owner"], "mnote-web");
assert_eq!(payload["userId"], "dev-user");
assert_ne!(payload["code"], "legacy_next_compat_disabled");
}
}