feat: 完成 rust cutover phase 8 收口

This commit is contained in:
lix-2026
2026-04-15 20:01:12 +08:00
parent 822c730cd6
commit 98db79b301
104 changed files with 18777 additions and 3025 deletions
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "mnote-cli"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
[dependencies]
clap = { version = "4.5.38", features = ["derive"] }
core-protocol = { path = "../core-protocol" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
storage-convex-bridge = { path = "../storage-convex-bridge" }
+46
View File
@@ -0,0 +1,46 @@
# mnote-cli
当前 crate 是 Phase 2 的最小 CLI 协议入口。
当前目标不是直接替代全部执行链,而是先冻结这五类命令面的命名和 `--json` 输出协议:
- `page`
- `block`
- `search`
- `sidebar`
- `tool`
当前最小子命令面:
- `page get`
- `page title`
- `page save`
- `block insert`
- `block patch`
- `search documents`
- `search blocks`
- `sidebar dataset`
- `tool run`
最小示例:
```bash
cargo run --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml -p mnote-cli -- --json page get --page-id page_demo --workspace-id ws_demo
cargo run --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml -p mnote-cli -- --json page title --page-id page_demo --workspace-id ws_demo --title "新标题"
cargo run --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml -p mnote-cli -- --json page save --page-id page_demo --workspace-id ws_demo --content-json '[{\"id\":\"block_1\"}]'
cargo run --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml -p mnote-cli -- --json sidebar dataset --workspace-id ws_demo
cargo run --manifest-path /mnt/Data1T/mnote/rust/Cargo.toml -p mnote-cli -- --json tool run --tool-name doc_get --kind query --mode explain-plan --args-json '{"pageId":"page_demo"}'
```
当前输出的重点字段:
- `domain` / `action`
- `context.requestId` / `context.traceId`
- `operation.kind`
- `operation.name``operation.toolName`
- `operation.executionMode` / `operation.toolsetId` / `operation.effect`
- `operation.transport.functionName`
- `operation.transport.payloadJson`
- `operation.transport.argsJson`
`tool run` 当前会先校验 Rust 内置 tool registry,并统一暴露 `validateOnly` / `dryRun` / `explain-plan` 等安全模式字段。后续阶段会继续把这些 CLI 面接到真实执行器,但命令命名和 JSON 契约应尽量保持稳定。
File diff suppressed because it is too large Load Diff
+477
View File
@@ -0,0 +1,477 @@
use clap::{Args, Parser, Subcommand, ValueEnum};
use core_protocol::{InvocationKind, ToolExecutionMode};
use mnote_cli::{
plan_block_insert, plan_block_patch, plan_mindmap_get, plan_mindmap_op, plan_mindmap_put,
plan_page_get, plan_page_save, plan_page_title, plan_search_blocks,
plan_search_documents, plan_sidebar_dataset, plan_tool_run, render_plain_output, CliContext,
CliError, CliJsonOutput,
};
#[derive(Parser, Debug)]
#[command(name = "mnote-cli")]
#[command(about = "Phase 2 最小 CLI 协议冻结器", long_about = None)]
struct Cli {
#[command(flatten)]
global: GlobalArgs,
#[command(subcommand)]
command: Commands,
}
#[derive(Args, Debug, Clone)]
struct GlobalArgs {
#[arg(long)]
json: bool,
#[arg(long, default_value = "cli_user")]
actor_id: String,
#[arg(long, default_value = "human")]
actor_type: String,
#[arg(long)]
session_id: Option<String>,
#[arg(long)]
reason: Option<String>,
#[arg(long)]
idempotency_key: Option<String>,
#[arg(long)]
validate_only: bool,
#[arg(long)]
dry_run: bool,
}
#[derive(Subcommand, Debug)]
enum Commands {
Page(PageCommand),
Block(BlockCommand),
Mindmap(MindmapCommand),
Search(SearchCommand),
Sidebar(SidebarCommand),
Tool(ToolCommand),
}
#[derive(Args, Debug)]
struct PageCommand {
#[command(subcommand)]
action: PageAction,
}
#[derive(Subcommand, Debug)]
enum PageAction {
Get(PageGetArgs),
Title(PageTitleArgs),
Save(PageSaveArgs),
}
#[derive(Args, Debug)]
struct PageGetArgs {
#[arg(long)]
page_id: String,
#[arg(long)]
workspace_id: Option<String>,
}
#[derive(Args, Debug)]
struct PageTitleArgs {
#[arg(long)]
page_id: String,
#[arg(long)]
workspace_id: Option<String>,
#[arg(long)]
title: String,
}
#[derive(Args, Debug)]
struct PageSaveArgs {
#[arg(long)]
page_id: String,
#[arg(long)]
workspace_id: Option<String>,
#[arg(long)]
revision: Option<u64>,
#[arg(long)]
content_json: String,
#[arg(long)]
conflict_detection_key: Option<String>,
}
#[derive(Args, Debug)]
struct BlockCommand {
#[command(subcommand)]
action: BlockAction,
}
#[derive(Subcommand, Debug)]
enum BlockAction {
Insert(BlockInsertArgs),
Patch(BlockPatchArgs),
}
#[derive(Args, Debug)]
struct BlockInsertArgs {
#[arg(long)]
workspace_id: String,
#[arg(long)]
page_id: String,
#[arg(long)]
content: String,
#[arg(long, default_value = "paragraph")]
block_type: String,
#[arg(long)]
parent_block_id: Option<String>,
#[arg(long)]
prev_block_id: Option<String>,
}
#[derive(Args, Debug)]
struct BlockPatchArgs {
#[arg(long)]
page_id: String,
#[arg(long)]
block_id: String,
#[arg(long)]
workspace_id: Option<String>,
#[arg(long)]
revision: Option<u64>,
#[arg(long)]
snapshot_json: String,
#[arg(long)]
conflict_detection_key: Option<String>,
}
#[derive(Args, Debug)]
struct MindmapCommand {
#[command(subcommand)]
action: MindmapAction,
}
#[derive(Subcommand, Debug)]
enum MindmapAction {
Get(MindmapGetArgs),
Put(MindmapPutArgs),
Op(MindmapOpArgs),
}
#[derive(Args, Debug)]
struct MindmapGetArgs {
#[arg(long)]
document_id: String,
#[arg(long)]
mindmap_id: String,
#[arg(long)]
workspace_id: Option<String>,
}
#[derive(Args, Debug)]
struct MindmapPutArgs {
#[arg(long)]
document_id: String,
#[arg(long)]
mindmap_id: String,
#[arg(long)]
workspace_id: Option<String>,
#[arg(long)]
data_json: String,
#[arg(long, default_value_t = false)]
create_only: bool,
}
#[derive(Args, Debug)]
struct MindmapOpArgs {
#[arg(long)]
document_id: String,
#[arg(long)]
mindmap_id: String,
#[arg(long)]
workspace_id: Option<String>,
#[arg(long)]
ops_json: String,
}
#[derive(Args, Debug)]
struct SearchCommand {
#[command(subcommand)]
action: SearchAction,
}
#[derive(Subcommand, Debug)]
enum SearchAction {
Documents(SearchDocumentsArgs),
Blocks(SearchBlocksArgs),
}
#[derive(Args, Debug)]
struct SearchDocumentsArgs {
#[arg(long)]
query: String,
#[arg(long)]
workspace_id: Option<String>,
#[arg(long, default_value_t = 20)]
limit: u32,
#[arg(long)]
cursor: Option<String>,
}
#[derive(Args, Debug)]
struct SearchBlocksArgs {
#[arg(long)]
query: String,
#[arg(long)]
page_id: Option<String>,
#[arg(long, default_value_t = 20)]
limit: u32,
#[arg(long)]
cursor: Option<String>,
}
#[derive(Args, Debug)]
struct SidebarCommand {
#[command(subcommand)]
action: SidebarAction,
}
#[derive(Subcommand, Debug)]
enum SidebarAction {
Dataset(SidebarDatasetArgs),
}
#[derive(Args, Debug)]
struct SidebarDatasetArgs {
#[arg(long)]
workspace_id: String,
}
#[derive(Args, Debug)]
struct ToolCommand {
#[command(subcommand)]
action: ToolAction,
}
#[derive(Subcommand, Debug)]
enum ToolAction {
Run(ToolRunArgs),
}
#[derive(Args, Debug)]
struct ToolRunArgs {
#[arg(long)]
tool_name: String,
#[arg(long, value_enum, default_value_t = ToolKindArg::Command)]
kind: ToolKindArg,
#[arg(long, value_enum, default_value_t = ToolModeArg::Plan)]
mode: ToolModeArg,
#[arg(long)]
args_json: String,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
enum ToolKindArg {
Command,
Query,
Job,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
enum ToolModeArg {
Plan,
Result,
ExplainPlan,
}
fn main() {
let cli = Cli::parse();
let context = CliContext {
actor_id: cli.global.actor_id,
actor_type: cli.global.actor_type,
session_id: cli.global.session_id,
reason: cli.global.reason,
idempotency_key: cli.global.idempotency_key,
validate_only: cli.global.validate_only,
dry_run: cli.global.dry_run,
};
let result = match cli.command {
Commands::Page(command) => match command.action {
PageAction::Get(args) => {
plan_page_get(&context, &args.page_id, args.workspace_id.as_deref())
}
PageAction::Title(args) => plan_page_title(
&context,
&args.page_id,
args.workspace_id.as_deref(),
&args.title,
),
PageAction::Save(args) => plan_page_save(
&context,
&args.page_id,
args.workspace_id.as_deref(),
args.revision,
&args.content_json,
args.conflict_detection_key.as_deref(),
),
},
Commands::Block(command) => match command.action {
BlockAction::Insert(args) => plan_block_insert(
&context,
&args.workspace_id,
&args.page_id,
&args.content,
&args.block_type,
args.parent_block_id.as_deref(),
args.prev_block_id.as_deref(),
),
BlockAction::Patch(args) => plan_block_patch(
&context,
args.workspace_id.as_deref(),
&args.page_id,
&args.block_id,
args.revision,
&args.snapshot_json,
args.conflict_detection_key.as_deref(),
),
},
Commands::Mindmap(command) => match command.action {
MindmapAction::Get(args) => plan_mindmap_get(
&context,
args.workspace_id.as_deref(),
&args.document_id,
&args.mindmap_id,
),
MindmapAction::Put(args) => plan_mindmap_put(
&context,
args.workspace_id.as_deref(),
&args.document_id,
&args.mindmap_id,
&args.data_json,
args.create_only,
),
MindmapAction::Op(args) => plan_mindmap_op(
&context,
args.workspace_id.as_deref(),
&args.document_id,
&args.mindmap_id,
&args.ops_json,
),
},
Commands::Search(command) => match command.action {
SearchAction::Documents(args) => plan_search_documents(
&context,
&args.query,
args.workspace_id.as_deref(),
args.limit,
args.cursor.as_deref(),
),
SearchAction::Blocks(args) => plan_search_blocks(
&context,
&args.query,
args.page_id.as_deref(),
args.limit,
args.cursor.as_deref(),
),
},
Commands::Sidebar(command) => match command.action {
SidebarAction::Dataset(args) => plan_sidebar_dataset(&context, &args.workspace_id),
},
Commands::Tool(command) => match command.action {
ToolAction::Run(args) => plan_tool_run(
&context,
&args.tool_name,
to_invocation_kind(args.kind),
to_execution_mode(args.mode),
&args.args_json,
),
},
};
match result {
Ok(output) => emit_success(&output, cli.global.json),
Err(error) => emit_error(&error, cli.global.json),
}
}
fn emit_success(output: &CliJsonOutput, json_mode: bool) {
if json_mode {
println!(
"{}",
serde_json::to_string_pretty(output).expect("CLI JSON 输出必须可序列化")
);
} else {
println!("{}", render_plain_output(output));
}
}
fn emit_error(error: &CliError, json_mode: bool) -> ! {
if json_mode {
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"ok": false,
"entrypoint": "mnote-cli",
"error": {
"code": error.code,
"message": error.message,
}
}))
.expect("CLI 错误 JSON 输出必须可序列化")
);
} else {
eprintln!("{}: {}", error.code, error.message);
}
std::process::exit(1);
}
fn to_invocation_kind(kind: ToolKindArg) -> InvocationKind {
match kind {
ToolKindArg::Command => InvocationKind::Command,
ToolKindArg::Query => InvocationKind::Query,
ToolKindArg::Job => InvocationKind::Job,
}
}
fn to_execution_mode(mode: ToolModeArg) -> ToolExecutionMode {
match mode {
ToolModeArg::Plan => ToolExecutionMode::Plan,
ToolModeArg::Result => ToolExecutionMode::Result,
ToolModeArg::ExplainPlan => ToolExecutionMode::ExplainPlan,
}
}