- 收口 rust final closure checklist,推进页面/块系统/Mindmap/CLI/AI tools 到最终 cutover 状态 - 按 ai-frontend-simplification-plan-v1 接入 Hermes bridge,合并 AI 面板并清理旧前端编排残留 - 补充 harness 任务与进度记录,加入 CLI smoke 夹具/脚本,并修正文档页 bridge SSR 自请求回退逻辑
628 lines
14 KiB
Rust
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,
|
|
}
|
|
}
|