收口 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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user