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 自请求回退逻辑
This commit is contained in:
Generated
+3
@@ -670,8 +670,11 @@ dependencies = [
|
||||
name = "mnote-cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bridge-runtime",
|
||||
"clap",
|
||||
"core-protocol",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"storage-convex-bridge",
|
||||
|
||||
@@ -153,6 +153,52 @@ struct MindmapOutlineToolPayload {
|
||||
outline: Vec<MindmapOutlineItemPayload>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DocsSearchDatasetPayload {
|
||||
workspace_id: String,
|
||||
documents: Vec<index_fts::SearchDocumentRecord>,
|
||||
mindmaps: Vec<index_fts::SearchMindmapRecord>,
|
||||
tables: Vec<index_fts::SearchTableRecord>,
|
||||
table_rows: Vec<index_fts::SearchTableRowRecord>,
|
||||
assets: Vec<index_fts::SearchAssetRecord>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DocsSearchToolPayload {
|
||||
query: String,
|
||||
workspace_id: Option<String>,
|
||||
limit: Option<u32>,
|
||||
include_deleted: Option<bool>,
|
||||
page_id: Option<String>,
|
||||
title_only: Option<bool>,
|
||||
exact: Option<bool>,
|
||||
include_ocr: Option<bool>,
|
||||
time_range: Option<String>,
|
||||
time_field: Option<String>,
|
||||
custom_range_from: Option<String>,
|
||||
custom_range_to: Option<String>,
|
||||
#[serde(default)]
|
||||
datasets: Vec<DocsSearchDatasetPayload>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DocsReadToolPayload {
|
||||
document_id: String,
|
||||
max_chars: Option<u32>,
|
||||
include_content: Option<bool>,
|
||||
title: Option<String>,
|
||||
workspace_id: Option<String>,
|
||||
parent_id: Option<String>,
|
||||
updated_at: Option<String>,
|
||||
raw_text_length: Option<u64>,
|
||||
raw_text: Option<String>,
|
||||
content: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RuntimeSuccess {
|
||||
@@ -388,6 +434,33 @@ struct MindmapPutCommandPayload {
|
||||
create_only: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct MindmapDeleteCommandPayload {
|
||||
document_id: String,
|
||||
mindmap_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct MindmapRestoreCommandPayload {
|
||||
document_id: String,
|
||||
mindmap_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct MindmapPurgeCommandPayload {
|
||||
document_id: String,
|
||||
mindmap_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct MindmapEmptyTrashCommandPayload {
|
||||
workspace_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DocumentCreateCommandPayload {
|
||||
@@ -427,6 +500,25 @@ struct DocumentDuplicateCommandPayload {
|
||||
title: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DocumentTemplateCommandPayload {
|
||||
document_id: String,
|
||||
is_template: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DocumentEmptyTrashCommandPayload {
|
||||
workspace_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DocumentPurgeCommandPayload {
|
||||
document_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DocumentCopyTreeItemPayload {
|
||||
@@ -1038,6 +1130,119 @@ fn execute_tool_result(
|
||||
"results": results,
|
||||
}))
|
||||
}
|
||||
"docs_search" => {
|
||||
let payload: DocsSearchToolPayload = parse_tool_input(&args, &data)?;
|
||||
let normalized_query = payload.query.trim().to_string();
|
||||
if normalized_query.is_empty() {
|
||||
return Ok(json!({
|
||||
"ok": true,
|
||||
"source": infer_tool_source(&data),
|
||||
"query": normalized_query,
|
||||
"results": [],
|
||||
"enqueueAssetIds": [],
|
||||
}));
|
||||
}
|
||||
let limit = payload.limit.unwrap_or(12).clamp(1, 30) as usize;
|
||||
let requested_workspace = payload
|
||||
.workspace_id
|
||||
.as_ref()
|
||||
.map(|value| value.trim())
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string);
|
||||
let mut enqueue_asset_ids = Vec::<String>::new();
|
||||
let mut results = Vec::<Value>::new();
|
||||
for dataset in payload.datasets {
|
||||
if let Some(workspace_id) = requested_workspace.as_ref() {
|
||||
if dataset.workspace_id != *workspace_id {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let evaluation = evaluate_search_documents(
|
||||
&SearchDocumentsRequest {
|
||||
query: normalized_query.clone(),
|
||||
workspace_id: dataset.workspace_id.clone(),
|
||||
page_id: payload.page_id.clone(),
|
||||
limit,
|
||||
title_only: payload.title_only.unwrap_or(false),
|
||||
exact: payload.exact.unwrap_or(false),
|
||||
include_ocr: payload.include_ocr.unwrap_or(false),
|
||||
time_range: payload.time_range.clone().unwrap_or_else(|| "any".into()),
|
||||
time_field: payload.time_field.clone().unwrap_or_else(|| "updated".into()),
|
||||
custom_range_from: payload.custom_range_from.clone(),
|
||||
custom_range_to: payload.custom_range_to.clone(),
|
||||
},
|
||||
&SearchDocumentsDataset {
|
||||
documents: dataset.documents,
|
||||
mindmaps: dataset.mindmaps,
|
||||
tables: dataset.tables,
|
||||
table_rows: dataset.table_rows,
|
||||
assets: dataset.assets,
|
||||
},
|
||||
);
|
||||
enqueue_asset_ids.extend(evaluation.enqueue_asset_ids);
|
||||
for item in evaluation.results {
|
||||
results.push(json!({
|
||||
"id": item.id,
|
||||
"title": item.title,
|
||||
"snippet": item.snippet,
|
||||
"updatedAt": item.updated_at,
|
||||
"createdAt": item.created_at,
|
||||
"matchField": item.match_field,
|
||||
"hasOcr": item.has_ocr,
|
||||
"publicPath": item.public_path,
|
||||
"score": item.score,
|
||||
"workspaceId": dataset.workspace_id,
|
||||
}));
|
||||
}
|
||||
}
|
||||
results.sort_by(|left, right| {
|
||||
let left_score = left.get("score").and_then(Value::as_f64).unwrap_or(0.0);
|
||||
let right_score = right.get("score").and_then(Value::as_f64).unwrap_or(0.0);
|
||||
right_score
|
||||
.partial_cmp(&left_score)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
.then_with(|| {
|
||||
let left_updated = left.get("updatedAt").and_then(Value::as_str).unwrap_or("");
|
||||
let right_updated = right.get("updatedAt").and_then(Value::as_str).unwrap_or("");
|
||||
right_updated.cmp(left_updated)
|
||||
})
|
||||
});
|
||||
results.truncate(limit);
|
||||
enqueue_asset_ids.sort();
|
||||
enqueue_asset_ids.dedup();
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"source": infer_tool_source(&data),
|
||||
"query": normalized_query,
|
||||
"results": results,
|
||||
"enqueueAssetIds": enqueue_asset_ids,
|
||||
"includeDeleted": payload.include_deleted.unwrap_or(false),
|
||||
}))
|
||||
}
|
||||
"docs_read" => {
|
||||
let payload: DocsReadToolPayload = parse_tool_input(&args, &data)?;
|
||||
let max_chars = payload.max_chars.unwrap_or(2500).clamp(200, 20_000) as usize;
|
||||
let raw_text = payload.raw_text.unwrap_or_default();
|
||||
let raw_text_length = payload.raw_text_length.unwrap_or_else(|| raw_text.chars().count() as u64);
|
||||
let trimmed = trim_text_for_docs_read(&raw_text, max_chars);
|
||||
let mut result = json!({
|
||||
"ok": true,
|
||||
"source": infer_tool_source(&data),
|
||||
"documentId": payload.document_id,
|
||||
"title": payload.title.unwrap_or_else(|| "".into()),
|
||||
"workspaceId": payload.workspace_id.unwrap_or_else(|| "".into()),
|
||||
"parentId": payload.parent_id,
|
||||
"updatedAt": payload.updated_at,
|
||||
"rawTextLength": raw_text_length,
|
||||
"rawText": trimmed,
|
||||
});
|
||||
if payload.include_content.unwrap_or(false) {
|
||||
if let Some(map) = result.as_object_mut() {
|
||||
map.insert("content".into(), payload.content.unwrap_or(Value::Null));
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
"doc_insert_blocks" => {
|
||||
let blocks = normalize_blocks_from_value(&data);
|
||||
let specs = parse_insert_specs(&args)?;
|
||||
@@ -1368,6 +1573,44 @@ fn build_tool_plan_steps(
|
||||
}]);
|
||||
}
|
||||
|
||||
if invocation.tool == "docs_search" {
|
||||
return Ok(vec![
|
||||
RuntimeToolPlanStep {
|
||||
kind: "transport".into(),
|
||||
name: "docs_search.dataset".into(),
|
||||
function_name: None,
|
||||
description: "通过 transport 拉取文档、导图、表格与 OCR 搜索数据集,再交由 Rust runtime 统一排序。".into(),
|
||||
args_json: tool_wire.args_json.clone(),
|
||||
},
|
||||
RuntimeToolPlanStep {
|
||||
kind: "transform".into(),
|
||||
name: "docs_search".into(),
|
||||
function_name: None,
|
||||
description: "在 Rust runtime 内复用统一搜索评估器,输出跨页文档搜索结果。".into(),
|
||||
args_json: tool_wire.args_json.clone(),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
if invocation.tool == "docs_read" {
|
||||
return Ok(vec![
|
||||
RuntimeToolPlanStep {
|
||||
kind: "transport".into(),
|
||||
name: "docs_read.document".into(),
|
||||
function_name: None,
|
||||
description: "通过 transport 读取目标文档 meta/content,再交由 Rust runtime 统一裁剪正文与返回结构。".into(),
|
||||
args_json: tool_wire.args_json.clone(),
|
||||
},
|
||||
RuntimeToolPlanStep {
|
||||
kind: "transform".into(),
|
||||
name: "docs_read".into(),
|
||||
function_name: None,
|
||||
description: "在 Rust runtime 内标准化跨页文档读取结果。".into(),
|
||||
args_json: tool_wire.args_json.clone(),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
if invocation.tool.starts_with("onlyoffice_") {
|
||||
let description = match invocation.tool.as_str() {
|
||||
"onlyoffice_session_resolve" => "解析 OnlyOffice 的附件、页面与用户上下文,生成稳定 session/asset 边界",
|
||||
@@ -2374,6 +2617,16 @@ fn strip_html_tags(value: &str) -> String {
|
||||
result.trim().to_string()
|
||||
}
|
||||
|
||||
|
||||
fn trim_text_for_docs_read(value: &str, max_chars: usize) -> String {
|
||||
let taken = value.chars().take(max_chars).collect::<String>();
|
||||
if value.chars().count() > max_chars {
|
||||
format!("{taken}…")
|
||||
} else {
|
||||
taken
|
||||
}
|
||||
}
|
||||
|
||||
fn walk_mindmap_summaries(root: &MindmapTreeNode, max_nodes: usize) -> Vec<RuntimeMindmapSummary> {
|
||||
let mut list = Vec::new();
|
||||
let mut queue = VecDeque::from([(root, None::<String>, 0usize)]);
|
||||
@@ -3110,6 +3363,137 @@ fn execute_command(
|
||||
}),
|
||||
}))
|
||||
}
|
||||
"mindmaps.delete" => {
|
||||
let payload: MindmapDeleteCommandPayload =
|
||||
parse_payload(command_wire.payload.clone())?;
|
||||
let command = CommandEnvelope {
|
||||
name: "mindmaps.delete".into(),
|
||||
command_id: command_wire.command_id.clone(),
|
||||
idempotency_key: command_wire.idempotency_key.clone(),
|
||||
actor: to_actor_payload(&command_wire.actor),
|
||||
source: to_source_payload(&command_wire.source),
|
||||
target: to_target_ref(command_wire.target.as_ref()),
|
||||
payload: payload.mindmap_id.clone(),
|
||||
reason: command_wire.reason,
|
||||
refs: command_wire.refs,
|
||||
dry_run: command_wire.dry_run,
|
||||
validate_only: command_wire.validate_only,
|
||||
};
|
||||
let request = build_write_request(&context, &command)?;
|
||||
Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan {
|
||||
command_name: command.name,
|
||||
command_id: command.command_id,
|
||||
function_name: request.function_name,
|
||||
workspace_id: request.workspace_id,
|
||||
request_id: request.request_id,
|
||||
trace_id: request.trace_id,
|
||||
actor_id: request.actor_id,
|
||||
idempotency_key: request.idempotency_key,
|
||||
payload_json: request.payload_json,
|
||||
args_json: json!({
|
||||
"docId": payload.document_id,
|
||||
"mindmapId": payload.mindmap_id,
|
||||
}),
|
||||
}))
|
||||
}
|
||||
"mindmaps.restore" => {
|
||||
let payload: MindmapRestoreCommandPayload =
|
||||
parse_payload(command_wire.payload.clone())?;
|
||||
let command = CommandEnvelope {
|
||||
name: "mindmaps.restore".into(),
|
||||
command_id: command_wire.command_id.clone(),
|
||||
idempotency_key: command_wire.idempotency_key.clone(),
|
||||
actor: to_actor_payload(&command_wire.actor),
|
||||
source: to_source_payload(&command_wire.source),
|
||||
target: to_target_ref(command_wire.target.as_ref()),
|
||||
payload: payload.mindmap_id.clone(),
|
||||
reason: command_wire.reason,
|
||||
refs: command_wire.refs,
|
||||
dry_run: command_wire.dry_run,
|
||||
validate_only: command_wire.validate_only,
|
||||
};
|
||||
let request = build_write_request(&context, &command)?;
|
||||
Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan {
|
||||
command_name: command.name,
|
||||
command_id: command.command_id,
|
||||
function_name: request.function_name,
|
||||
workspace_id: request.workspace_id,
|
||||
request_id: request.request_id,
|
||||
trace_id: request.trace_id,
|
||||
actor_id: request.actor_id,
|
||||
idempotency_key: request.idempotency_key,
|
||||
payload_json: request.payload_json,
|
||||
args_json: json!({
|
||||
"docId": payload.document_id,
|
||||
"mindmapId": payload.mindmap_id,
|
||||
}),
|
||||
}))
|
||||
}
|
||||
"mindmaps.purge" => {
|
||||
let payload: MindmapPurgeCommandPayload =
|
||||
parse_payload(command_wire.payload.clone())?;
|
||||
let command = CommandEnvelope {
|
||||
name: "mindmaps.purge".into(),
|
||||
command_id: command_wire.command_id.clone(),
|
||||
idempotency_key: command_wire.idempotency_key.clone(),
|
||||
actor: to_actor_payload(&command_wire.actor),
|
||||
source: to_source_payload(&command_wire.source),
|
||||
target: to_target_ref(command_wire.target.as_ref()),
|
||||
payload: payload.mindmap_id.clone(),
|
||||
reason: command_wire.reason,
|
||||
refs: command_wire.refs,
|
||||
dry_run: command_wire.dry_run,
|
||||
validate_only: command_wire.validate_only,
|
||||
};
|
||||
let request = build_write_request(&context, &command)?;
|
||||
Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan {
|
||||
command_name: command.name,
|
||||
command_id: command.command_id,
|
||||
function_name: request.function_name,
|
||||
workspace_id: request.workspace_id,
|
||||
request_id: request.request_id,
|
||||
trace_id: request.trace_id,
|
||||
actor_id: request.actor_id,
|
||||
idempotency_key: request.idempotency_key,
|
||||
payload_json: request.payload_json,
|
||||
args_json: json!({
|
||||
"docId": payload.document_id,
|
||||
"mindmapId": payload.mindmap_id,
|
||||
}),
|
||||
}))
|
||||
}
|
||||
"mindmaps.emptyTrashByWorkspace" => {
|
||||
let payload: MindmapEmptyTrashCommandPayload =
|
||||
parse_payload(command_wire.payload.clone())?;
|
||||
let command = CommandEnvelope {
|
||||
name: "mindmaps.emptyTrashByWorkspace".into(),
|
||||
command_id: command_wire.command_id.clone(),
|
||||
idempotency_key: command_wire.idempotency_key.clone(),
|
||||
actor: to_actor_payload(&command_wire.actor),
|
||||
source: to_source_payload(&command_wire.source),
|
||||
target: to_target_ref(command_wire.target.as_ref()),
|
||||
payload: payload.workspace_id.clone(),
|
||||
reason: command_wire.reason,
|
||||
refs: command_wire.refs,
|
||||
dry_run: command_wire.dry_run,
|
||||
validate_only: command_wire.validate_only,
|
||||
};
|
||||
let request = build_write_request(&context, &command)?;
|
||||
Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan {
|
||||
command_name: command.name,
|
||||
command_id: command.command_id,
|
||||
function_name: request.function_name,
|
||||
workspace_id: request.workspace_id,
|
||||
request_id: request.request_id,
|
||||
trace_id: request.trace_id,
|
||||
actor_id: request.actor_id,
|
||||
idempotency_key: request.idempotency_key,
|
||||
payload_json: request.payload_json,
|
||||
args_json: json!({
|
||||
"workspaceId": payload.workspace_id,
|
||||
}),
|
||||
}))
|
||||
}
|
||||
"documents.create" => {
|
||||
let payload: DocumentCreateCommandPayload =
|
||||
parse_payload(command_wire.payload.clone())?;
|
||||
@@ -3278,6 +3662,103 @@ fn execute_command(
|
||||
}),
|
||||
}))
|
||||
}
|
||||
"documents.template" => {
|
||||
let payload: DocumentTemplateCommandPayload =
|
||||
parse_payload(command_wire.payload.clone())?;
|
||||
let command = CommandEnvelope {
|
||||
name: "documents.template".into(),
|
||||
command_id: command_wire.command_id.clone(),
|
||||
idempotency_key: command_wire.idempotency_key.clone(),
|
||||
actor: to_actor_payload(&command_wire.actor),
|
||||
source: to_source_payload(&command_wire.source),
|
||||
target: to_target_ref(command_wire.target.as_ref()),
|
||||
payload: payload.is_template,
|
||||
reason: command_wire.reason,
|
||||
refs: command_wire.refs,
|
||||
dry_run: command_wire.dry_run,
|
||||
validate_only: command_wire.validate_only,
|
||||
};
|
||||
let request = build_write_request(&context, &command)?;
|
||||
Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan {
|
||||
command_name: command.name,
|
||||
command_id: command.command_id,
|
||||
function_name: request.function_name,
|
||||
workspace_id: request.workspace_id,
|
||||
request_id: request.request_id,
|
||||
trace_id: request.trace_id,
|
||||
actor_id: request.actor_id,
|
||||
idempotency_key: request.idempotency_key,
|
||||
payload_json: request.payload_json,
|
||||
args_json: json!({
|
||||
"id": payload.document_id,
|
||||
"isTemplate": payload.is_template,
|
||||
}),
|
||||
}))
|
||||
}
|
||||
"documents.emptyTrashByWorkspace" => {
|
||||
let payload: DocumentEmptyTrashCommandPayload =
|
||||
parse_payload(command_wire.payload.clone())?;
|
||||
let command = CommandEnvelope {
|
||||
name: "documents.emptyTrashByWorkspace".into(),
|
||||
command_id: command_wire.command_id.clone(),
|
||||
idempotency_key: command_wire.idempotency_key.clone(),
|
||||
actor: to_actor_payload(&command_wire.actor),
|
||||
source: to_source_payload(&command_wire.source),
|
||||
target: to_target_ref(command_wire.target.as_ref()),
|
||||
payload: payload.workspace_id.clone(),
|
||||
reason: command_wire.reason,
|
||||
refs: command_wire.refs,
|
||||
dry_run: command_wire.dry_run,
|
||||
validate_only: command_wire.validate_only,
|
||||
};
|
||||
let request = build_write_request(&context, &command)?;
|
||||
Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan {
|
||||
command_name: command.name,
|
||||
command_id: command.command_id,
|
||||
function_name: request.function_name,
|
||||
workspace_id: request.workspace_id,
|
||||
request_id: request.request_id,
|
||||
trace_id: request.trace_id,
|
||||
actor_id: request.actor_id,
|
||||
idempotency_key: request.idempotency_key,
|
||||
payload_json: request.payload_json,
|
||||
args_json: json!({
|
||||
"workspaceId": payload.workspace_id,
|
||||
}),
|
||||
}))
|
||||
}
|
||||
"documents.purge" => {
|
||||
let payload: DocumentPurgeCommandPayload =
|
||||
parse_payload(command_wire.payload.clone())?;
|
||||
let command = CommandEnvelope {
|
||||
name: "documents.purge".into(),
|
||||
command_id: command_wire.command_id.clone(),
|
||||
idempotency_key: command_wire.idempotency_key.clone(),
|
||||
actor: to_actor_payload(&command_wire.actor),
|
||||
source: to_source_payload(&command_wire.source),
|
||||
target: to_target_ref(command_wire.target.as_ref()),
|
||||
payload: payload.document_id.clone(),
|
||||
reason: command_wire.reason,
|
||||
refs: command_wire.refs,
|
||||
dry_run: command_wire.dry_run,
|
||||
validate_only: command_wire.validate_only,
|
||||
};
|
||||
let request = build_write_request(&context, &command)?;
|
||||
Ok(RuntimeExecutionPlan::Command(RuntimeCommandExecutionPlan {
|
||||
command_name: command.name,
|
||||
command_id: command.command_id,
|
||||
function_name: request.function_name,
|
||||
workspace_id: request.workspace_id,
|
||||
request_id: request.request_id,
|
||||
trace_id: request.trace_id,
|
||||
actor_id: request.actor_id,
|
||||
idempotency_key: request.idempotency_key,
|
||||
payload_json: request.payload_json,
|
||||
args_json: json!({
|
||||
"id": payload.document_id,
|
||||
}),
|
||||
}))
|
||||
}
|
||||
"documents.copy_tree" => {
|
||||
let payload: DocumentCopyTreeCommandPayload =
|
||||
parse_payload(command_wire.payload.clone())?;
|
||||
@@ -3762,6 +4243,122 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn docs_search_tool_plan_uses_transport_and_transform_steps() {
|
||||
let plan = execute_runtime_input(RuntimeInput::Tool {
|
||||
context: demo_context(),
|
||||
tool: RuntimeToolInvocationWire {
|
||||
tool: "docs_search".into(),
|
||||
kind: "query".into(),
|
||||
mode: Some("plan".into()),
|
||||
args_json: json!({
|
||||
"query": "rust",
|
||||
}),
|
||||
target: None,
|
||||
reason: Some("跨页搜索".into()),
|
||||
refs: vec!["task-050".into()],
|
||||
},
|
||||
data: None,
|
||||
})
|
||||
.expect("docs_search plan should build");
|
||||
|
||||
match plan {
|
||||
RuntimeExecutionPlan::Tool(plan) => {
|
||||
assert_eq!(plan.tool_name, "docs_search");
|
||||
assert_eq!(plan.toolset_id, "toolset.docs_read");
|
||||
assert_eq!(plan.steps.len(), 2);
|
||||
assert_eq!(plan.steps[0].name, "docs_search.dataset");
|
||||
assert_eq!(plan.steps[1].name, "docs_search");
|
||||
}
|
||||
_ => panic!("expected tool plan"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn docs_search_tool_result_uses_rust_search_evaluation() {
|
||||
let result = execute_runtime_query(RuntimeInput::Tool {
|
||||
context: demo_context(),
|
||||
tool: RuntimeToolInvocationWire {
|
||||
tool: "docs_search".into(),
|
||||
kind: "query".into(),
|
||||
mode: Some("result".into()),
|
||||
args_json: json!({
|
||||
"query": "rust",
|
||||
"limit": 5,
|
||||
}),
|
||||
target: None,
|
||||
reason: Some("跨页搜索".into()),
|
||||
refs: vec!["task-050".into()],
|
||||
},
|
||||
data: Some(json!({
|
||||
"source": "convex",
|
||||
"datasets": [
|
||||
{
|
||||
"workspaceId": "ws_1",
|
||||
"documents": [
|
||||
{
|
||||
"id": "page_1",
|
||||
"workspaceId": "ws_1",
|
||||
"title": "Rust 文档",
|
||||
"rawText": "这里记录 rust runtime 收口",
|
||||
"createdAt": "2026-04-15T00:00:00Z",
|
||||
"updatedAt": "2026-04-16T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"mindmaps": [],
|
||||
"tables": [],
|
||||
"tableRows": [],
|
||||
"assets": []
|
||||
}
|
||||
]
|
||||
})),
|
||||
})
|
||||
.expect("docs_search result should build");
|
||||
|
||||
assert_eq!(result.get("query").and_then(Value::as_str), Some("rust"));
|
||||
assert_eq!(result.get("results").and_then(Value::as_array).map(Vec::len), Some(1));
|
||||
assert_eq!(result["results"][0]["id"], json!("page_1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn docs_read_tool_result_trims_text_and_keeps_content_optional() {
|
||||
let long_text = "a".repeat(260);
|
||||
let result = execute_runtime_query(RuntimeInput::Tool {
|
||||
context: demo_context(),
|
||||
tool: RuntimeToolInvocationWire {
|
||||
tool: "docs_read".into(),
|
||||
kind: "query".into(),
|
||||
mode: Some("result".into()),
|
||||
args_json: json!({
|
||||
"documentId": "page_1",
|
||||
"maxChars": 200,
|
||||
"includeContent": true,
|
||||
}),
|
||||
target: None,
|
||||
reason: Some("读取文档".into()),
|
||||
refs: vec!["task-050".into()],
|
||||
},
|
||||
data: Some(json!({
|
||||
"source": "convex",
|
||||
"title": "示例页面",
|
||||
"workspaceId": "ws_1",
|
||||
"parentId": "parent_1",
|
||||
"updatedAt": "2026-04-16T00:00:00Z",
|
||||
"rawText": long_text,
|
||||
"rawTextLength": 260,
|
||||
"content": {"blocks": [{"id": "b1"}]}
|
||||
})),
|
||||
})
|
||||
.expect("docs_read result should build");
|
||||
|
||||
assert_eq!(result["documentId"], json!("page_1"));
|
||||
let trimmed = result["rawText"].as_str().expect("rawText should be string");
|
||||
assert_eq!(trimmed.chars().count(), 201);
|
||||
assert!(trimmed.ends_with('…'));
|
||||
assert_eq!(result["rawTextLength"], json!(260));
|
||||
assert_eq!(result["content"]["blocks"][0]["id"], json!("b1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doc_get_tool_plan_uses_documents_content_query() {
|
||||
let plan = execute_runtime_input(RuntimeInput::Tool {
|
||||
|
||||
@@ -28,8 +28,9 @@ pub use tool::{
|
||||
default_tool_registry, invocation_kind_label, tool_effect_label, tool_mode_label,
|
||||
InvocationKind, ToolEffect, ToolExecutionMode, ToolInvocation, ToolRegistry, ToolSetSpec,
|
||||
ToolSpec, BRIDGE_TOOL_COMMAND_GET, BRIDGE_TOOL_REQUEST_GET, BRIDGE_TOOL_TRACE_GET,
|
||||
DOC_TOOL_FIND, DOC_TOOL_GET, DOC_TOOL_INSERT_BLOCKS, DOC_TOOL_REPLACE_RANGE,
|
||||
DOC_TOOLSET_READ, DOC_TOOLSET_WRITE, INDEX_TOOL_REBUILD, MINDMAP_TOOL_APPLY_OPS,
|
||||
DOCS_TOOL_READ, DOCS_TOOL_SEARCH, DOCS_TOOLSET_READ, DOC_TOOL_FIND, DOC_TOOL_GET,
|
||||
DOC_TOOL_INSERT_BLOCKS, DOC_TOOL_REPLACE_RANGE, DOC_TOOLSET_READ, DOC_TOOLSET_WRITE,
|
||||
INDEX_TOOL_REBUILD, MINDMAP_TOOL_APPLY_OPS,
|
||||
MINDMAP_TOOL_EMPTY_TRASH, MINDMAP_TOOL_EXPAND_NODE, MINDMAP_TOOL_GET,
|
||||
MINDMAP_TOOL_GET_SUBTREE,
|
||||
MINDMAP_TOOL_OUTLINE_TO_MINDMAP, MINDMAP_TOOL_PUT, MINDMAP_TOOLSET_READ,
|
||||
@@ -81,6 +82,8 @@ mod tests {
|
||||
"search_web",
|
||||
"doc_get",
|
||||
"doc_find",
|
||||
"docs_search",
|
||||
"docs_read",
|
||||
"image_read",
|
||||
"doc_insert_blocks",
|
||||
"doc_replace_range",
|
||||
@@ -140,6 +143,11 @@ mod tests {
|
||||
.expect("slash toolset should exist");
|
||||
assert!(slash.write_toolset);
|
||||
assert_eq!(slash.tool_names, &["slash_run"]);
|
||||
let docs_read = registry
|
||||
.toolset("toolset.docs_read")
|
||||
.expect("docs_read toolset should exist");
|
||||
assert!(!docs_read.write_toolset);
|
||||
assert_eq!(docs_read.tool_names, &["docs_search", "docs_read"]);
|
||||
let doc_write = registry
|
||||
.toolset("toolset.doc_write")
|
||||
.expect("doc_write toolset should exist");
|
||||
|
||||
@@ -121,6 +121,31 @@ pub const DOC_TOOL_FIND: ToolSpec = ToolSpec {
|
||||
r#"{"type":"object","required":["query"],"properties":{"query":{"type":"string"},"maxResults":{"type":"integer","minimum":1,"maximum":30}}}"#,
|
||||
};
|
||||
|
||||
|
||||
pub const DOCS_TOOL_SEARCH: ToolSpec = ToolSpec {
|
||||
name: "docs_search",
|
||||
display_name: "跨页文档搜索",
|
||||
description: "在工作区内搜索文档标题、正文、导图、表格与 OCR 结果。",
|
||||
toolset_id: "toolset.docs_read",
|
||||
invocation_kind: InvocationKind::Query,
|
||||
effect: ToolEffect::Read,
|
||||
requires_confirmation: false,
|
||||
input_schema_json:
|
||||
r#"{"type":"object","required":["query"],"properties":{"query":{"type":"string"},"workspaceId":{"type":"string"},"limit":{"type":"integer","minimum":1,"maximum":30},"includeDeleted":{"type":"boolean"},"pageId":{"type":"string"},"titleOnly":{"type":"boolean"},"exact":{"type":"boolean"},"includeOcr":{"type":"boolean"},"timeRange":{"type":"string"},"timeField":{"type":"string"},"customRangeFrom":{"type":"string"},"customRangeTo":{"type":"string"}}}"#,
|
||||
};
|
||||
|
||||
pub const DOCS_TOOL_READ: ToolSpec = ToolSpec {
|
||||
name: "docs_read",
|
||||
display_name: "跨页文档读取",
|
||||
description: "读取指定文档的标题、正文摘要与可选 content 快照。",
|
||||
toolset_id: "toolset.docs_read",
|
||||
invocation_kind: InvocationKind::Query,
|
||||
effect: ToolEffect::Read,
|
||||
requires_confirmation: false,
|
||||
input_schema_json:
|
||||
r#"{"type":"object","required":["documentId"],"properties":{"documentId":{"type":"string"},"maxChars":{"type":"integer","minimum":200,"maximum":20000},"includeContent":{"type":"boolean"}}}"#,
|
||||
};
|
||||
|
||||
pub const IMAGE_READ_TOOL: ToolSpec = ToolSpec {
|
||||
name: "image_read",
|
||||
display_name: "读取图片",
|
||||
@@ -381,6 +406,15 @@ pub const DOC_TOOLSET_READ: ToolSetSpec = ToolSetSpec {
|
||||
tool_names: &["doc_get", "doc_find"],
|
||||
};
|
||||
|
||||
|
||||
pub const DOCS_TOOLSET_READ: ToolSetSpec = ToolSetSpec {
|
||||
id: "toolset.docs_read",
|
||||
display_name: "跨页文档读取",
|
||||
description: "跨页面搜索与读取文档的工具集合。",
|
||||
write_toolset: false,
|
||||
tool_names: &["docs_search", "docs_read"],
|
||||
};
|
||||
|
||||
pub const READONLY_TOOLSET: ToolSetSpec = ToolSetSpec {
|
||||
id: "toolset.readonly",
|
||||
display_name: "只读工具",
|
||||
@@ -470,6 +504,7 @@ pub const DOC_TOOL_REGISTRY: ToolRegistry = ToolRegistry::new(
|
||||
READONLY_TOOLSET,
|
||||
MEDIA_TOOLSET,
|
||||
SLASH_TOOLSET_WRITE,
|
||||
DOCS_TOOLSET_READ,
|
||||
DOC_TOOLSET_READ,
|
||||
DOC_TOOLSET_WRITE,
|
||||
MINDMAP_TOOLSET_READ,
|
||||
@@ -482,6 +517,8 @@ pub const DOC_TOOL_REGISTRY: ToolRegistry = ToolRegistry::new(
|
||||
SEARCH_WEB_TOOL,
|
||||
DOC_TOOL_GET,
|
||||
DOC_TOOL_FIND,
|
||||
DOCS_TOOL_SEARCH,
|
||||
DOCS_TOOL_READ,
|
||||
IMAGE_READ_TOOL,
|
||||
DOC_TOOL_INSERT_BLOCKS,
|
||||
DOC_TOOL_REPLACE_RANGE,
|
||||
|
||||
@@ -6,8 +6,11 @@ license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.22"
|
||||
bridge-runtime = { path = "../bridge-runtime" }
|
||||
clap = { version = "4.5.38", features = ["derive"] }
|
||||
core-protocol = { path = "../core-protocol" }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
storage-convex-bridge = { path = "../storage-convex-bridge" }
|
||||
|
||||
+1508
-17
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,17 @@
|
||||
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,
|
||||
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 2 最小 CLI 协议冻结器", long_about = None)]
|
||||
#[command(about = "Phase 8 最小真实执行 CLI", long_about = None)]
|
||||
struct Cli {
|
||||
#[command(flatten)]
|
||||
global: GlobalArgs,
|
||||
@@ -23,6 +25,9 @@ struct GlobalArgs {
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
|
||||
#[arg(long)]
|
||||
execute: bool,
|
||||
|
||||
#[arg(long, default_value = "cli_user")]
|
||||
actor_id: String,
|
||||
|
||||
@@ -66,6 +71,10 @@ enum PageAction {
|
||||
Get(PageGetArgs),
|
||||
Title(PageTitleArgs),
|
||||
Save(PageSaveArgs),
|
||||
Create(PageCreateCliArgs),
|
||||
Move(PageMoveCliArgs),
|
||||
Delete(PageDeleteCliArgs),
|
||||
Restore(PageRestoreCliArgs),
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
@@ -107,6 +116,60 @@ struct PageSaveArgs {
|
||||
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)]
|
||||
@@ -117,6 +180,8 @@ struct BlockCommand {
|
||||
enum BlockAction {
|
||||
Insert(BlockInsertArgs),
|
||||
Patch(BlockPatchArgs),
|
||||
Move(BlockMoveCliArgs),
|
||||
Embed(BlockEmbedCliArgs),
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
@@ -161,6 +226,33 @@ struct BlockPatchArgs {
|
||||
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)]
|
||||
@@ -349,6 +441,40 @@ fn main() {
|
||||
&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(
|
||||
@@ -369,6 +495,23 @@ fn main() {
|
||||
&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(
|
||||
@@ -421,7 +564,14 @@ fn main() {
|
||||
&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),
|
||||
|
||||
@@ -35,12 +35,19 @@ pub fn map_command_name_to_convex(command_name: &str) -> &'static str {
|
||||
"documents.restore" => "documents:restore",
|
||||
"documents.duplicate" => "documents:duplicateWithMindmaps",
|
||||
"documents.copy_tree" => "documents:copyTree",
|
||||
"documents.template" => "documents:setTemplate",
|
||||
"documents.emptyTrashByWorkspace" => "documents:emptyTrashByWorkspace",
|
||||
"documents.purge" => "documents:purge",
|
||||
"blocks.patch" => "documents:updateContent",
|
||||
"documents.save" => "documents:updateContent",
|
||||
"documents.title.update" => "documents:updateTitle",
|
||||
"documents.stats.update" => "documents:updateStats",
|
||||
"documents.options.update" => "documents:updateOptions",
|
||||
"mindmaps.put" => "mindmaps:put",
|
||||
"mindmaps.delete" => "mindmaps:softDelete",
|
||||
"mindmaps.restore" => "mindmaps:restore",
|
||||
"mindmaps.purge" => "mindmaps:purge",
|
||||
"mindmaps.emptyTrashByWorkspace" => "mindmaps:emptyTrashByWorkspace",
|
||||
"insert_block" => "blocks:insert",
|
||||
"update_block" => "blocks:update",
|
||||
"move_block" => "blocks:move",
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"workspaceId": "0e0e1304-9e8d-49ad-951b-3ae93a80fc85",
|
||||
"searchQuery": "cli",
|
||||
"pageTemplate": {
|
||||
"title": "CLI Smoke",
|
||||
"content": [
|
||||
{
|
||||
"id": "init_block",
|
||||
"type": "paragraph",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "hello cli"
|
||||
}
|
||||
],
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="/mnt/Data1T/mnote"
|
||||
export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-/tmp/mnote-rust-target-harness}"
|
||||
export WORKSPACE_ID="${1:-0e0e1304-9e8d-49ad-951b-3ae93a80fc85}"
|
||||
STAMP="$(date +%s)"
|
||||
export SOURCE_PAGE_ID="cli-smoke-src-${STAMP}"
|
||||
export TARGET_PAGE_ID="cli-smoke-dst-${STAMP}"
|
||||
export MINDMAP_PAGE_ID="cli-mindmap-${STAMP}"
|
||||
export MINDMAP_ID="mind_cli"
|
||||
export TMP_DIR="${TMPDIR:-/tmp}/task049-cli-smoke-${STAMP}"
|
||||
mkdir -p "$TMP_DIR"
|
||||
export ROOT
|
||||
|
||||
python3 - <<'PYTHON'
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
root = os.environ['ROOT']
|
||||
workspace_id = os.environ['WORKSPACE_ID']
|
||||
source_page_id = os.environ['SOURCE_PAGE_ID']
|
||||
target_page_id = os.environ['TARGET_PAGE_ID']
|
||||
mindmap_page_id = os.environ['MINDMAP_PAGE_ID']
|
||||
mindmap_id = os.environ['MINDMAP_ID']
|
||||
tmp_dir = pathlib.Path(os.environ['TMP_DIR'])
|
||||
|
||||
cargo = [
|
||||
'cargo',
|
||||
'run',
|
||||
'--manifest-path',
|
||||
f'{root}/rust/Cargo.toml',
|
||||
'-p',
|
||||
'mnote-cli',
|
||||
'--',
|
||||
]
|
||||
env = os.environ.copy()
|
||||
|
||||
|
||||
def run(name: str, *args: str) -> dict:
|
||||
cmd = cargo + ['--json', '--execute', *args]
|
||||
cp = subprocess.run(cmd, cwd=root, env=env, text=True, capture_output=True)
|
||||
(tmp_dir / f'{name}.stdout').write_text(cp.stdout, encoding='utf-8')
|
||||
(tmp_dir / f'{name}.stderr').write_text(cp.stderr, encoding='utf-8')
|
||||
if cp.returncode != 0:
|
||||
raise SystemExit(f'command failed {name}: {cp.stderr}')
|
||||
data = json.loads(cp.stdout)
|
||||
(tmp_dir / f'{name}.json').write_text(
|
||||
json.dumps(data, ensure_ascii=False, indent=2),
|
||||
encoding='utf-8',
|
||||
)
|
||||
if not data.get('ok', False):
|
||||
raise SystemExit(f'command returned ok=false {name}: {cp.stdout}')
|
||||
return data
|
||||
|
||||
sidebar_dataset = run('01-sidebar-dataset', 'sidebar', 'dataset', '--workspace-id', workspace_id)
|
||||
create_src = run(
|
||||
'02-page-create-src',
|
||||
'page',
|
||||
'create',
|
||||
'--page-id',
|
||||
source_page_id,
|
||||
'--workspace-id',
|
||||
workspace_id,
|
||||
'--title',
|
||||
f'CLI Verify SRC {source_page_id}',
|
||||
'--content-json',
|
||||
'[{"id":"init_block","type":"paragraph","content":[{"type":"text","text":"source init"}],"children":[]}]',
|
||||
)
|
||||
create_dst = run(
|
||||
'03-page-create-dst',
|
||||
'page',
|
||||
'create',
|
||||
'--page-id',
|
||||
target_page_id,
|
||||
'--workspace-id',
|
||||
workspace_id,
|
||||
'--title',
|
||||
f'CLI Verify DST {target_page_id}',
|
||||
'--content-json',
|
||||
'[]',
|
||||
)
|
||||
page_get = run('04-page-get', 'page', 'get', '--page-id', source_page_id, '--workspace-id', workspace_id)
|
||||
page_title = run(
|
||||
'05-page-title',
|
||||
'page',
|
||||
'title',
|
||||
'--page-id',
|
||||
source_page_id,
|
||||
'--workspace-id',
|
||||
workspace_id,
|
||||
'--title',
|
||||
f'CLI Verify {source_page_id} Renamed',
|
||||
)
|
||||
page_save = run(
|
||||
'06-page-save',
|
||||
'page',
|
||||
'save',
|
||||
'--page-id',
|
||||
source_page_id,
|
||||
'--workspace-id',
|
||||
workspace_id,
|
||||
'--revision',
|
||||
'0',
|
||||
'--conflict-detection-key',
|
||||
f'{source_page_id}:0',
|
||||
'--content-json',
|
||||
'[{"id":"init_block","type":"paragraph","content":[{"type":"text","text":"source saved"}],"children":[]}]',
|
||||
)
|
||||
block_insert = run(
|
||||
'07-block-insert',
|
||||
'block',
|
||||
'insert',
|
||||
'--workspace-id',
|
||||
workspace_id,
|
||||
'--page-id',
|
||||
source_page_id,
|
||||
'--content',
|
||||
'move me',
|
||||
'--block-type',
|
||||
'paragraph',
|
||||
)
|
||||
inserted_block_id = block_insert['operation']['execution']['inserted'][0]
|
||||
block_patch = run(
|
||||
'08-block-patch',
|
||||
'block',
|
||||
'patch',
|
||||
'--page-id',
|
||||
source_page_id,
|
||||
'--workspace-id',
|
||||
workspace_id,
|
||||
'--block-id',
|
||||
'init_block',
|
||||
'--revision',
|
||||
'2',
|
||||
'--conflict-detection-key',
|
||||
f'{source_page_id}:2',
|
||||
'--snapshot-json',
|
||||
'{"id":"init_block","type":"paragraph","content":[{"type":"text","text":"source patched"}],"children":[]}',
|
||||
)
|
||||
source_after_patch = run(
|
||||
'09-page-get-after-patch',
|
||||
'page',
|
||||
'get',
|
||||
'--page-id',
|
||||
source_page_id,
|
||||
'--workspace-id',
|
||||
workspace_id,
|
||||
)
|
||||
block_move = run(
|
||||
'10-block-move',
|
||||
'block',
|
||||
'move',
|
||||
'--source-document-id',
|
||||
source_page_id,
|
||||
'--block-id',
|
||||
inserted_block_id,
|
||||
'--target-document-id',
|
||||
target_page_id,
|
||||
)
|
||||
block_embed = run(
|
||||
'11-block-embed',
|
||||
'block',
|
||||
'embed',
|
||||
'--source-document-id',
|
||||
source_page_id,
|
||||
'--block-id',
|
||||
'init_block',
|
||||
'--target-document-id',
|
||||
target_page_id,
|
||||
)
|
||||
page_move = run(
|
||||
'12-page-move',
|
||||
'page',
|
||||
'move',
|
||||
'--page-id',
|
||||
target_page_id,
|
||||
'--workspace-id',
|
||||
workspace_id,
|
||||
'--parent-id',
|
||||
source_page_id,
|
||||
'--sort-order',
|
||||
'2',
|
||||
)
|
||||
search_documents = run(
|
||||
'13-search-documents',
|
||||
'search',
|
||||
'documents',
|
||||
'--workspace-id',
|
||||
workspace_id,
|
||||
'--query',
|
||||
source_page_id,
|
||||
)
|
||||
tool_docs_search = run(
|
||||
'14-tool-docs-search',
|
||||
'tool',
|
||||
'run',
|
||||
'--tool-name',
|
||||
'docs_search',
|
||||
'--kind',
|
||||
'query',
|
||||
'--mode',
|
||||
'result',
|
||||
'--args-json',
|
||||
json.dumps({'query': source_page_id, 'workspaceId': workspace_id, 'limit': 5}, ensure_ascii=False),
|
||||
)
|
||||
tool_docs_read = run(
|
||||
'15-tool-docs-read',
|
||||
'tool',
|
||||
'run',
|
||||
'--tool-name',
|
||||
'docs_read',
|
||||
'--kind',
|
||||
'query',
|
||||
'--mode',
|
||||
'result',
|
||||
'--args-json',
|
||||
json.dumps({'documentId': source_page_id, 'includeContent': True, 'maxChars': 1000}, ensure_ascii=False),
|
||||
)
|
||||
page_delete = run('16-page-delete-target', 'page', 'delete', '--page-id', target_page_id, '--workspace-id', workspace_id)
|
||||
page_restore = run('17-page-restore-target', 'page', 'restore', '--page-id', target_page_id, '--workspace-id', workspace_id)
|
||||
page_get_restored = run('18-page-get-restored-target', 'page', 'get', '--page-id', target_page_id, '--workspace-id', workspace_id)
|
||||
final_delete_target = run('19-page-delete-target-final', 'page', 'delete', '--page-id', target_page_id, '--workspace-id', workspace_id)
|
||||
create_mindmap_page = run(
|
||||
'20-page-create-mindmap',
|
||||
'page',
|
||||
'create',
|
||||
'--page-id',
|
||||
mindmap_page_id,
|
||||
'--workspace-id',
|
||||
workspace_id,
|
||||
'--title',
|
||||
f'CLI Mindmap {mindmap_page_id}',
|
||||
'--content-json',
|
||||
'[]',
|
||||
)
|
||||
mindmap_put = run(
|
||||
'21-mindmap-put',
|
||||
'mindmap',
|
||||
'put',
|
||||
'--document-id',
|
||||
mindmap_page_id,
|
||||
'--mindmap-id',
|
||||
mindmap_id,
|
||||
'--workspace-id',
|
||||
workspace_id,
|
||||
'--data-json',
|
||||
'{"data":{"uid":"root_1","text":"中心主题"},"children":[]}',
|
||||
'--create-only',
|
||||
)
|
||||
mindmap_get = run(
|
||||
'22-mindmap-get',
|
||||
'mindmap',
|
||||
'get',
|
||||
'--document-id',
|
||||
mindmap_page_id,
|
||||
'--mindmap-id',
|
||||
mindmap_id,
|
||||
'--workspace-id',
|
||||
workspace_id,
|
||||
)
|
||||
mindmap_op = run(
|
||||
'23-mindmap-op',
|
||||
'mindmap',
|
||||
'op',
|
||||
'--document-id',
|
||||
mindmap_page_id,
|
||||
'--mindmap-id',
|
||||
mindmap_id,
|
||||
'--workspace-id',
|
||||
workspace_id,
|
||||
'--ops-json',
|
||||
'[{"op":"addChild","parentUid":"root_1","node":{"text":"新分支"}}]',
|
||||
)
|
||||
final_delete_source = run('24-page-delete-source-final', 'page', 'delete', '--page-id', source_page_id, '--workspace-id', workspace_id)
|
||||
final_delete_mindmap_page = run('25-page-delete-mindmap-final', 'page', 'delete', '--page-id', mindmap_page_id, '--workspace-id', workspace_id)
|
||||
|
||||
summary = {
|
||||
'workspaceId': workspace_id,
|
||||
'sourcePageId': source_page_id,
|
||||
'targetPageId': target_page_id,
|
||||
'mindmapPageId': mindmap_page_id,
|
||||
'insertedBlockId': inserted_block_id,
|
||||
'sidebarDocuments': len(sidebar_dataset['operation']['execution'].get('documents', [])),
|
||||
'pageGetRevision': page_get['operation']['execution'].get('revision'),
|
||||
'pageTitleUpdatedAt': page_title['operation']['execution'].get('updated_at'),
|
||||
'pageSaveRevision': page_save['operation']['execution'].get('revision'),
|
||||
'sourceAfterPatchBlocks': len(source_after_patch['operation']['execution'].get('content', [])),
|
||||
'blockPatchRevision': block_patch['operation']['execution'].get('revision'),
|
||||
'blockMoveExecution': block_move['operation']['execution'],
|
||||
'blockEmbedExecution': block_embed['operation']['execution'],
|
||||
'pageMoveExecution': page_move['operation']['execution'],
|
||||
'searchResults': len(search_documents['operation']['execution'].get('results', [])),
|
||||
'toolDocsSearchResults': len(tool_docs_search['operation']['execution'].get('results', [])),
|
||||
'toolDocsReadRawTextLength': tool_docs_read['operation']['execution'].get('rawTextLength'),
|
||||
'pageRestoreExecution': page_restore['operation']['execution'],
|
||||
'restoredTargetBlocks': len(page_get_restored['operation']['execution'].get('content', [])),
|
||||
'mindmapPutExecution': mindmap_put['operation']['execution'],
|
||||
'mindmapGetExists': mindmap_get['operation']['execution'].get('meta', {}).get('exists'),
|
||||
'mindmapOpExecution': mindmap_op['operation']['execution'],
|
||||
'cleanup': {
|
||||
'targetDeleted': final_delete_target['operation']['execution'].get('ok'),
|
||||
'sourceDeleted': final_delete_source['operation']['execution'].get('ok'),
|
||||
'mindmapPageDeleted': final_delete_mindmap_page['operation']['execution'].get('ok'),
|
||||
},
|
||||
'tmpDir': str(tmp_dir),
|
||||
}
|
||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
||||
PYTHON
|
||||
|
||||
echo "task049-cli-smoke ok workspace_id=$WORKSPACE_ID source_page_id=$SOURCE_PAGE_ID target_page_id=$TARGET_PAGE_ID mindmap_page_id=$MINDMAP_PAGE_ID tmp_dir=$TMP_DIR"
|
||||
@@ -0,0 +1,6 @@
|
||||
# CLI 验收骨架
|
||||
|
||||
- `rust/scripts/task049-cli-smoke.sh`:最小 CLI 真实执行 smoke,现已覆盖 `sidebar dataset`、`page get/create/title/save/move/delete/restore`、`block insert/patch/move/embed`、`search documents`、`tool run docs_search/docs_read`、`mindmap put/get/op`。
|
||||
- `rust/fixtures/task049-cli-smoke.json`:默认 workspace 与页面模板示例。
|
||||
- smoke 会在 `/tmp/task049-cli-smoke-*` 落盘逐步 JSON 输出,便于失败后回查具体命令。
|
||||
- `mindmap op` 当前验收的是 Rust runtime 返回的应用结果;持久化仍由 `mindmap put/get` 单独验证。
|
||||
Reference in New Issue
Block a user