Files
mnote/rust/crates/mnote-cli/src/main.rs
T
lix-2026 2ff10fa86c feat: 提交 task-045 至 task-058 收口产物
- 收口 rust final closure checklist,推进页面/块系统/Mindmap/CLI/AI tools 到最终 cutover 状态

- 按 ai-frontend-simplification-plan-v1 接入 Hermes bridge,合并 AI 面板并清理旧前端编排残留

- 补充 harness 任务与进度记录,加入 CLI smoke 夹具/脚本,并修正文档页 bridge SSR 自请求回退逻辑
2026-04-16 15:24:37 +08:00

628 lines
14 KiB
Rust

use clap::{Args, Parser, Subcommand, ValueEnum};
use core_protocol::{InvocationKind, ToolExecutionMode};
use mnote_cli::{
execute_output, plan_block_embed, plan_block_insert, plan_block_move, plan_block_patch,
plan_mindmap_get, plan_mindmap_op, plan_mindmap_put, plan_page_create, plan_page_delete,
plan_page_get, plan_page_move, plan_page_restore, plan_page_save, plan_page_title,
plan_search_blocks, plan_search_documents, plan_sidebar_dataset, plan_tool_run,
render_plain_output, BlockEmbedArgs, BlockMoveArgs, CliContext, CliError, CliJsonOutput,
PageCreateArgs, PageDeleteArgs, PageMoveArgs, PageRestoreArgs,
};
#[derive(Parser, Debug)]
#[command(name = "mnote-cli")]
#[command(about = "Phase 8 最小真实执行 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)]
execute: 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),
Create(PageCreateCliArgs),
Move(PageMoveCliArgs),
Delete(PageDeleteCliArgs),
Restore(PageRestoreCliArgs),
}
#[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 PageCreateCliArgs {
#[arg(long)]
page_id: String,
#[arg(long)]
workspace_id: String,
#[arg(long)]
parent_id: Option<String>,
#[arg(long)]
title: String,
#[arg(long, default_value = "private")]
access_scope: String,
#[arg(long, default_value = "[]")]
content_json: String,
}
#[derive(Args, Debug)]
struct PageMoveCliArgs {
#[arg(long)]
page_id: String,
#[arg(long)]
workspace_id: Option<String>,
#[arg(long)]
parent_id: Option<String>,
#[arg(long)]
sort_order: i64,
}
#[derive(Args, Debug)]
struct PageDeleteCliArgs {
#[arg(long)]
page_id: String,
#[arg(long)]
workspace_id: Option<String>,
}
#[derive(Args, Debug)]
struct PageRestoreCliArgs {
#[arg(long)]
page_id: String,
#[arg(long)]
workspace_id: Option<String>,
}
#[derive(Args, Debug)]
struct BlockCommand {
#[command(subcommand)]
action: BlockAction,
}
#[derive(Subcommand, Debug)]
enum BlockAction {
Insert(BlockInsertArgs),
Patch(BlockPatchArgs),
Move(BlockMoveCliArgs),
Embed(BlockEmbedCliArgs),
}
#[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 BlockMoveCliArgs {
#[arg(long)]
source_document_id: String,
#[arg(long)]
block_id: String,
#[arg(long)]
target_document_id: String,
}
#[derive(Args, Debug)]
struct BlockEmbedCliArgs {
#[arg(long)]
source_document_id: String,
#[arg(long)]
block_id: String,
#[arg(long)]
target_document_id: String,
#[arg(long)]
target_block_id: 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(),
),
PageAction::Create(args) => plan_page_create(
&context,
&PageCreateArgs {
page_id: &args.page_id,
workspace_id: &args.workspace_id,
parent_id: args.parent_id.as_deref(),
title: &args.title,
access_scope: &args.access_scope,
content_json: &args.content_json,
},
),
PageAction::Move(args) => plan_page_move(
&context,
&PageMoveArgs {
page_id: &args.page_id,
workspace_id: args.workspace_id.as_deref(),
parent_id: args.parent_id.as_deref(),
sort_order: args.sort_order,
},
),
PageAction::Delete(args) => plan_page_delete(
&context,
&PageDeleteArgs {
page_id: &args.page_id,
workspace_id: args.workspace_id.as_deref(),
},
),
PageAction::Restore(args) => plan_page_restore(
&context,
&PageRestoreArgs {
page_id: &args.page_id,
workspace_id: args.workspace_id.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(),
),
BlockAction::Move(args) => plan_block_move(
&context,
&BlockMoveArgs {
source_document_id: &args.source_document_id,
block_id: &args.block_id,
target_document_id: &args.target_document_id,
},
),
BlockAction::Embed(args) => plan_block_embed(
&context,
&BlockEmbedArgs {
source_document_id: &args.source_document_id,
block_id: &args.block_id,
target_document_id: &args.target_document_id,
target_block_id: args.target_block_id.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,
),
},
}
.and_then(|output| {
if cli.global.execute {
execute_output(&output, &context)
} else {
Ok(output)
}
});
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,
}
}