收口 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
+86 -35
View File
@@ -90,6 +90,7 @@ pub struct CliOutputContext {
pub actor_id: String,
pub actor_type: String,
pub session_id: Option<String>,
pub reason: Option<String>,
pub workspace_id: Option<String>,
pub idempotency_key: Option<String>,
pub validate_only: bool,
@@ -566,6 +567,7 @@ pub fn plan_page_get(
"page",
"get",
&bridge,
ctx,
query.name,
json!({
"pageId": page_id,
@@ -614,6 +616,7 @@ pub fn plan_page_title(
"page",
"title",
&bridge,
ctx,
command.name,
command.command_id,
json!({
@@ -671,6 +674,7 @@ pub fn plan_page_save(
"page",
"save",
&bridge,
ctx,
command.name,
command.command_id,
json!({
@@ -724,6 +728,7 @@ pub fn plan_page_create(ctx: &CliContext, args: &PageCreateArgs<'_>) -> CliResul
"page",
"create",
&bridge,
ctx,
command.name,
command.command_id,
json!({
@@ -778,6 +783,7 @@ pub fn plan_page_move(ctx: &CliContext, args: &PageMoveArgs<'_>) -> CliResult<Cl
"page",
"move",
&bridge,
ctx,
command.name,
command.command_id,
json!({
@@ -825,6 +831,7 @@ pub fn plan_page_delete(ctx: &CliContext, args: &PageDeleteArgs<'_>) -> CliResul
"page",
"delete",
&bridge,
ctx,
command.name,
command.command_id,
json!({
@@ -868,6 +875,7 @@ pub fn plan_page_restore(ctx: &CliContext, args: &PageRestoreArgs<'_>) -> CliRes
"page",
"restore",
&bridge,
ctx,
command.name,
command.command_id,
json!({
@@ -923,6 +931,7 @@ pub fn plan_block_insert(
"block",
"insert",
&bridge,
ctx,
command.name,
command.command_id,
json!({
@@ -988,6 +997,7 @@ pub fn plan_block_patch(
"block",
"patch",
&bridge,
ctx,
command.name,
command.command_id,
json!({
@@ -1042,6 +1052,7 @@ pub fn plan_block_move(ctx: &CliContext, args: &BlockMoveArgs<'_>) -> CliResult<
"block",
"move",
&bridge,
ctx,
command.name,
command.command_id,
json!({
@@ -1091,6 +1102,7 @@ pub fn plan_block_embed(ctx: &CliContext, args: &BlockEmbedArgs<'_>) -> CliResul
"block",
"embed",
&bridge,
ctx,
command.name,
command.command_id,
json!({
@@ -1147,6 +1159,7 @@ pub fn plan_search_documents(
"search",
"documents",
&bridge,
ctx,
envelope.name,
json!({
"query": query,
@@ -1209,6 +1222,7 @@ pub fn plan_search_blocks(
"search",
"blocks",
&bridge,
ctx,
envelope.name,
json!({
"query": query,
@@ -1245,6 +1259,7 @@ pub fn plan_sidebar_dataset(ctx: &CliContext, workspace_id: &str) -> CliResult<C
"sidebar",
"dataset",
&bridge,
ctx,
envelope.name,
json!({
"workspaceId": workspace_id,
@@ -1286,6 +1301,7 @@ pub fn plan_mindmap_get(
"mindmap",
"get",
&bridge,
ctx,
envelope.name,
json!({
"workspaceId": workspace_id,
@@ -1351,6 +1367,7 @@ pub fn plan_mindmap_put(
"mindmap",
"put",
&bridge,
ctx,
command.name,
command.command_id,
json!({
@@ -2148,7 +2165,7 @@ fn execute_convex_query_raw(
args_json: Value,
cli_ctx: &CliContext,
) -> CliResult<Value> {
let env = ConvexCliEnv::load()?;
let env = ConvexCliEnv::load(cli_ctx)?;
let payload = json!({
"path": function_name,
"format": "convex_encoded_json",
@@ -2171,7 +2188,7 @@ fn execute_convex_mutation_raw(
args_json: Value,
cli_ctx: &CliContext,
) -> CliResult<Value> {
let env = ConvexCliEnv::load()?;
let env = ConvexCliEnv::load(cli_ctx)?;
let payload = json!({
"path": function_name,
"format": "convex_encoded_json",
@@ -2225,7 +2242,7 @@ struct ConvexCliEnv {
}
impl ConvexCliEnv {
fn load() -> CliResult<Self> {
fn load(cli_ctx: &CliContext) -> CliResult<Self> {
let url = read_env_or_dotenv("CONVEX_SELF_HOSTED_URL")?
.or_else(|| env::var("NEXT_PUBLIC_CONVEX_URL").ok())
.ok_or_else(|| {
@@ -2233,18 +2250,14 @@ impl ConvexCliEnv {
})?;
let admin_key = read_env_or_dotenv("CONVEX_SELF_HOSTED_ADMIN_KEY")?
.ok_or_else(|| CliError::validation("缺少 CONVEX_SELF_HOSTED_ADMIN_KEY"))?;
let dev_user_id = read_env_or_dotenv("DEV_USER_ID")?.unwrap_or_else(|| "dev-user".into());
let dev_user_id = normalize_actor_for_convex_identity(&cli_ctx.actor_id)
.or(read_env_or_dotenv("DEV_USER_ID")?)
.unwrap_or_else(|| "dev-user".into());
let dev_user_name =
read_env_or_dotenv("DEV_USER_NAME")?.unwrap_or_else(|| "开发用户".into());
let dev_user_email =
read_env_or_dotenv("DEV_USER_EMAIL")?.unwrap_or_else(|| "dev@mnote.local".into());
let identity = json!({
"subject": dev_user_id,
"issuer": "https://mnote.local/dev-auth",
"tokenIdentifier": format!("dev-user|{}", dev_user_id),
"name": dev_user_name,
"email": dev_user_email,
});
let identity = build_convex_dev_identity(&dev_user_id, &dev_user_name, &dev_user_email);
let encoded = base64::engine::general_purpose::STANDARD
.encode(serde_json::to_string(&identity).map_err(|error| {
CliError::transport(format!("开发用户身份序列化失败: {error}"))
@@ -2262,6 +2275,25 @@ impl ConvexCliEnv {
}
}
fn normalize_actor_for_convex_identity(actor_id: &str) -> Option<String> {
let trimmed = actor_id.trim();
if trimmed.is_empty() || trimmed == "anonymous" || trimmed == "cli_user" {
None
} else {
Some(trimmed.to_string())
}
}
fn build_convex_dev_identity(user_id: &str, name: &str, email: &str) -> Value {
json!({
"subject": user_id,
"issuer": "https://mnote.local/dev-auth",
"tokenIdentifier": format!("dev-user|{}", user_id),
"name": name,
"email": email,
})
}
fn read_env_or_dotenv(key: &str) -> CliResult<Option<String>> {
if let Ok(value) = env::var(key) {
let trimmed = value.trim().to_string();
@@ -2457,6 +2489,7 @@ fn build_query_output(
domain: &str,
action: &str,
bridge: &BridgeContext,
ctx: &CliContext,
name: String,
normalized_input: Value,
transport: CliTransportPlan,
@@ -2466,18 +2499,7 @@ fn build_query_output(
entrypoint: "mnote-cli",
domain: domain.into(),
action: action.into(),
context: build_output_context(
bridge,
&CliContext {
actor_id: bridge.actor_id.clone(),
actor_type: bridge.actor_type.clone(),
session_id: bridge.session_id.clone(),
reason: None,
idempotency_key: bridge.idempotency_key.clone(),
validate_only: bridge.validate_only,
dry_run: bridge.dry_run,
},
),
context: build_output_context(bridge, ctx),
operation: CliOperationOutput::Query {
name,
normalized_input,
@@ -2491,6 +2513,7 @@ fn build_command_output(
domain: &str,
action: &str,
bridge: &BridgeContext,
ctx: &CliContext,
name: String,
command_id: String,
normalized_input: Value,
@@ -2501,18 +2524,7 @@ fn build_command_output(
entrypoint: "mnote-cli",
domain: domain.into(),
action: action.into(),
context: build_output_context(
bridge,
&CliContext {
actor_id: bridge.actor_id.clone(),
actor_type: bridge.actor_type.clone(),
session_id: bridge.session_id.clone(),
reason: None,
idempotency_key: bridge.idempotency_key.clone(),
validate_only: bridge.validate_only,
dry_run: bridge.dry_run,
},
),
context: build_output_context(bridge, ctx),
operation: CliOperationOutput::Command {
name,
command_id,
@@ -2530,6 +2542,7 @@ fn build_output_context(bridge: &BridgeContext, ctx: &CliContext) -> CliOutputCo
actor_id: bridge.actor_id.clone(),
actor_type: bridge.actor_type.clone(),
session_id: bridge.session_id.clone(),
reason: ctx.reason.clone(),
workspace_id: bridge.workspace_id.clone(),
idempotency_key: ctx.idempotency_key.clone(),
validate_only: bridge.validate_only,
@@ -2786,6 +2799,18 @@ mod tests {
}
}
#[test]
fn output_context_keeps_reason_for_audit_trace() {
let ctx = CliContext {
reason: Some("phase7-cli-first".into()),
..CliContext::default()
};
let output = plan_page_save(&ctx, "page_1", Some("ws_1"), None, "[]", None)
.expect("page save plan should build");
assert_eq!(output.context.reason.as_deref(), Some("phase7-cli-first"));
}
#[test]
fn page_create_json_contract_uses_documents_create() {
let output = plan_page_create(
@@ -2817,6 +2842,32 @@ mod tests {
}
}
#[test]
fn convex_dev_identity_prefers_explicit_cli_actor() {
let ctx = CliContext {
actor_id: "nn7bhmt782sykdrecah0rbe2nx867ks1".into(),
..CliContext::default()
};
let user_id = normalize_actor_for_convex_identity(&ctx.actor_id)
.expect("显式 CLI actor 应成为 Convex 写入身份");
let identity = build_convex_dev_identity(&user_id, "开发用户", "dev@mnote.local");
assert_eq!(
identity["subject"],
json!("nn7bhmt782sykdrecah0rbe2nx867ks1")
);
assert_eq!(
identity["tokenIdentifier"],
json!("dev-user|nn7bhmt782sykdrecah0rbe2nx867ks1")
);
}
#[test]
fn convex_dev_identity_ignores_legacy_default_cli_actor() {
assert_eq!(normalize_actor_for_convex_identity("cli_user"), None);
assert_eq!(normalize_actor_for_convex_identity(" anonymous "), None);
}
#[test]
fn page_move_json_contract_uses_documents_move() {
let output = plan_page_move(