fix: merge 模式下按 content 匹配已有项,无需 agent 记住 UUID
- merge=true 时,无 id 的项先按 content 匹配已有项 - 匹配到→更新,匹配不到→新建(仍需 pending/in_progress) - 新增 list_todos 命令处理器,支持前端自动拉取 - prompt 增加规则6:更新已有任务必须传 id(作为最佳实践提示) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
750eed7326
commit
c4d10c6413
@ -272,7 +272,31 @@ impl Tool for TodoWriteTool {
|
|||||||
updated_at: now,
|
updated_at: now,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 传入 id 但旧状态中没有 → 按新 item 处理
|
// 传入 id 但旧状态中没有 → merge 模式按 content 匹配,否则按新 item
|
||||||
|
if merge_mode {
|
||||||
|
if let Some(old_item) = old_items.iter().find(|oi| oi.content == content) {
|
||||||
|
let old_status = match TodoStatus::from_str(&old_item.status) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => {
|
||||||
|
validation_errors.push(format!("Item '{}': corrupted old status", content));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(err) = validate_transition(&old_status, &new_status) {
|
||||||
|
validation_errors.push(format!("Item '{}': {}", content, err));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
processed_items.push(TodoItem {
|
||||||
|
id: old_item.id.clone(),
|
||||||
|
content,
|
||||||
|
status: new_status.as_str().to_string(),
|
||||||
|
priority: priority.as_str().to_string(),
|
||||||
|
created_at: old_item.created_at,
|
||||||
|
updated_at: now,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
if !is_valid_new_status(&new_status) {
|
if !is_valid_new_status(&new_status) {
|
||||||
validation_errors.push(format!(
|
validation_errors.push(format!(
|
||||||
"Item '{}': new items must start as 'pending' or 'in_progress', got '{}'",
|
"Item '{}': new items must start as 'pending' or 'in_progress', got '{}'",
|
||||||
@ -280,7 +304,6 @@ impl Tool for TodoWriteTool {
|
|||||||
));
|
));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
processed_items.push(TodoItem {
|
processed_items.push(TodoItem {
|
||||||
id: id.to_string(),
|
id: id.to_string(),
|
||||||
content,
|
content,
|
||||||
@ -291,7 +314,31 @@ impl Tool for TodoWriteTool {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 新 item(无 id)— 允许 pending 或 in_progress
|
// 新 item(无 id)— merge 模式按 content 匹配,否则按新 item
|
||||||
|
if merge_mode {
|
||||||
|
if let Some(old_item) = old_items.iter().find(|oi| oi.content == content) {
|
||||||
|
let old_status = match TodoStatus::from_str(&old_item.status) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => {
|
||||||
|
validation_errors.push(format!("Item '{}': corrupted old status", content));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(err) = validate_transition(&old_status, &new_status) {
|
||||||
|
validation_errors.push(format!("Item '{}': {}", content, err));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
processed_items.push(TodoItem {
|
||||||
|
id: old_item.id.clone(),
|
||||||
|
content,
|
||||||
|
status: new_status.as_str().to_string(),
|
||||||
|
priority: priority.as_str().to_string(),
|
||||||
|
created_at: old_item.created_at,
|
||||||
|
updated_at: now,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
if !is_valid_new_status(&new_status) {
|
if !is_valid_new_status(&new_status) {
|
||||||
validation_errors.push(format!(
|
validation_errors.push(format!(
|
||||||
"Item '{}': new items must start as 'pending' or 'in_progress', got '{}'",
|
"Item '{}': new items must start as 'pending' or 'in_progress', got '{}'",
|
||||||
@ -299,7 +346,6 @@ impl Tool for TodoWriteTool {
|
|||||||
));
|
));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
processed_items.push(TodoItem {
|
processed_items.push(TodoItem {
|
||||||
id: uuid::Uuid::new_v4().to_string(),
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
content,
|
content,
|
||||||
@ -1197,4 +1243,52 @@ mod tests {
|
|||||||
assert_eq!(todos.len(), 1);
|
assert_eq!(todos.len(), 1);
|
||||||
assert_eq!(output["changes"]["removed"].as_array().unwrap().len(), 1);
|
assert_eq!(output["changes"]["removed"].as_array().unwrap().len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_merge_match_by_content_without_id() {
|
||||||
|
let state = test_state();
|
||||||
|
let tool = TodoWriteTool::new(state.clone());
|
||||||
|
let context = test_context();
|
||||||
|
|
||||||
|
// 先创建 3 个 pending 任务
|
||||||
|
let _ = tool
|
||||||
|
.execute_with_context(
|
||||||
|
&context,
|
||||||
|
json!({
|
||||||
|
"todos": [
|
||||||
|
{"content": "任务1", "status": "pending"},
|
||||||
|
{"content": "任务2", "status": "pending"},
|
||||||
|
{"content": "任务3", "status": "pending"}
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// merge: true — 直接传 content + completed,不传 id,按 content 匹配
|
||||||
|
let result = tool
|
||||||
|
.execute_with_context(
|
||||||
|
&context,
|
||||||
|
json!({
|
||||||
|
"merge": true,
|
||||||
|
"todos": [
|
||||||
|
{"content": "任务1", "status": "completed"},
|
||||||
|
{"content": "任务2", "status": "completed"},
|
||||||
|
{"content": "任务3", "status": "cancelled"}
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(result.success);
|
||||||
|
let output: serde_json::Value = serde_json::from_str(&result.output).unwrap();
|
||||||
|
let todos = output["current_todos"].as_array().unwrap();
|
||||||
|
assert_eq!(todos.len(), 3);
|
||||||
|
assert_eq!(todos.iter().find(|t| t["content"] == "任务1").unwrap()["status"], "completed");
|
||||||
|
assert_eq!(todos.iter().find(|t| t["content"] == "任务2").unwrap()["status"], "completed");
|
||||||
|
assert_eq!(todos.iter().find(|t| t["content"] == "任务3").unwrap()["status"], "cancelled");
|
||||||
|
assert_eq!(output["changes"]["updated"].as_array().unwrap().len(), 3);
|
||||||
|
assert_eq!(output["changes"]["added"].as_array().unwrap().len(), 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user