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