mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-09 00:05:48 +08:00
fix: accept common tool parameter aliases
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,6 +12,18 @@ pub fn tool_query(id: u32, call: &ToolCall) -> Result<pb::AgentServerMessage> {
|
|||||||
.map(str::to_string)
|
.map(str::to_string)
|
||||||
.ok_or_else(|| Error::Protocol(format!("{} is missing {name}", call.name)))
|
.ok_or_else(|| Error::Protocol(format!("{} is missing {name}", call.name)))
|
||||||
};
|
};
|
||||||
|
// Claude 系模型常按 Claude Code 习惯输出别名参数(如 query),逐个回退兼容。
|
||||||
|
let string_aliased = |names: &[&str]| -> Result<String> {
|
||||||
|
for name in names {
|
||||||
|
if let Some(value) = call.arguments.get(name).and_then(Value::as_str) {
|
||||||
|
return Ok(value.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(Error::Protocol(format!(
|
||||||
|
"{} is missing {}",
|
||||||
|
call.name, names[0]
|
||||||
|
)))
|
||||||
|
};
|
||||||
let optional_string = |name: &str| {
|
let optional_string = |name: &str| {
|
||||||
call.arguments
|
call.arguments
|
||||||
.get(name)
|
.get(name)
|
||||||
@@ -80,7 +92,7 @@ pub fn tool_query(id: u32, call: &ToolCall) -> Result<pb::AgentServerMessage> {
|
|||||||
}
|
}
|
||||||
"websearch" => Query::WebSearchRequestQuery(pb::WebSearchRequestQuery {
|
"websearch" => Query::WebSearchRequestQuery(pb::WebSearchRequestQuery {
|
||||||
args: Some(pb::WebSearchArgs {
|
args: Some(pb::WebSearchArgs {
|
||||||
search_term: string("search_term")?,
|
search_term: string_aliased(&["search_term", "query"])?,
|
||||||
tool_call_id: call.call_id.clone(),
|
tool_call_id: call.call_id.clone(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
@@ -214,3 +226,37 @@ fn normalized(value: &str) -> String {
|
|||||||
.flat_map(char::to_lowercase)
|
.flat_map(char::to_lowercase)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use super::tool_query;
|
||||||
|
use crate::cursor::protocol::proto::agent::v1 as pb;
|
||||||
|
use crate::model::ToolCall;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn web_search_accepts_query_as_search_term_alias() {
|
||||||
|
// Claude 系模型常按 Claude Code 习惯发送 query 而非 search_term,
|
||||||
|
// 交互查询编码必须接受别名,而不是报 `WebSearch is missing search_term`。
|
||||||
|
let call = ToolCall {
|
||||||
|
index: 0,
|
||||||
|
call_id: "call-1".into(),
|
||||||
|
model_call_id: "model-1".into(),
|
||||||
|
name: "WebSearch".into(),
|
||||||
|
arguments_text: String::new(),
|
||||||
|
arguments: json!({ "query": "lmarena leaderboard" }),
|
||||||
|
argument_error: None,
|
||||||
|
};
|
||||||
|
let message = tool_query(1, &call).unwrap();
|
||||||
|
let Some(pb::agent_server_message::Message::InteractionQuery(query)) = message.message
|
||||||
|
else {
|
||||||
|
panic!("expected an InteractionQuery");
|
||||||
|
};
|
||||||
|
let Some(pb::interaction_query::Query::WebSearchRequestQuery(request)) = query.query
|
||||||
|
else {
|
||||||
|
panic!("expected a WebSearchRequestQuery");
|
||||||
|
};
|
||||||
|
assert_eq!(request.args.unwrap().search_term, "lmarena leaderboard");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -286,6 +286,20 @@ pub fn render_tool_call(call: &ToolCall, completed: bool) -> Result<pb::ToolCall
|
|||||||
.and_then(Value::as_str)
|
.and_then(Value::as_str)
|
||||||
.map(str::to_string)
|
.map(str::to_string)
|
||||||
};
|
};
|
||||||
|
// 与执行侧一致的参数别名兼容(如 Claude Code 习惯的 file_path),仅影响展示。
|
||||||
|
let aliased = |names: &[&str]| -> String {
|
||||||
|
names
|
||||||
|
.iter()
|
||||||
|
.find_map(|name| call.arguments.get(name).and_then(Value::as_str))
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string()
|
||||||
|
};
|
||||||
|
let optional_aliased = |names: &[&str]| -> Option<String> {
|
||||||
|
names
|
||||||
|
.iter()
|
||||||
|
.find_map(|name| call.arguments.get(name).and_then(Value::as_str))
|
||||||
|
.map(str::to_string)
|
||||||
|
};
|
||||||
match output.tool.as_mut() {
|
match output.tool.as_mut() {
|
||||||
Some(pb::tool_call::Tool::ShellToolCall(tool)) => {
|
Some(pb::tool_call::Tool::ShellToolCall(tool)) => {
|
||||||
tool.description = optional("description");
|
tool.description = optional("description");
|
||||||
@@ -299,7 +313,7 @@ pub fn render_tool_call(call: &ToolCall, completed: bool) -> Result<pb::ToolCall
|
|||||||
}
|
}
|
||||||
Some(pb::tool_call::Tool::DeleteToolCall(tool)) => {
|
Some(pb::tool_call::Tool::DeleteToolCall(tool)) => {
|
||||||
tool.args = Some(pb::DeleteArgs {
|
tool.args = Some(pb::DeleteArgs {
|
||||||
path: string("path"),
|
path: aliased(&["path", "file_path", "filePath"]),
|
||||||
tool_call_id: call.call_id.clone(),
|
tool_call_id: call.call_id.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -321,7 +335,7 @@ pub fn render_tool_call(call: &ToolCall, completed: bool) -> Result<pb::ToolCall
|
|||||||
}
|
}
|
||||||
Some(pb::tool_call::Tool::ReadToolCall(tool)) => {
|
Some(pb::tool_call::Tool::ReadToolCall(tool)) => {
|
||||||
tool.args = Some(pb::ReadToolArgs {
|
tool.args = Some(pb::ReadToolArgs {
|
||||||
path: string("path"),
|
path: aliased(&["path", "file_path", "filePath"]),
|
||||||
offset: call
|
offset: call
|
||||||
.arguments
|
.arguments
|
||||||
.get("offset")
|
.get("offset")
|
||||||
@@ -350,7 +364,7 @@ pub fn render_tool_call(call: &ToolCall, completed: bool) -> Result<pb::ToolCall
|
|||||||
}
|
}
|
||||||
Some(pb::tool_call::Tool::EditToolCall(tool)) => {
|
Some(pb::tool_call::Tool::EditToolCall(tool)) => {
|
||||||
let stream_content = if normalized(&call.name) == "write" {
|
let stream_content = if normalized(&call.name) == "write" {
|
||||||
optional("contents").unwrap_or_default()
|
optional_aliased(&["contents", "content"]).unwrap_or_default()
|
||||||
} else {
|
} else {
|
||||||
optional("new_string").unwrap_or_default()
|
optional("new_string").unwrap_or_default()
|
||||||
};
|
};
|
||||||
@@ -358,7 +372,7 @@ pub fn render_tool_call(call: &ToolCall, completed: bool) -> Result<pb::ToolCall
|
|||||||
path: if normalized(&call.name) == "editnotebook" {
|
path: if normalized(&call.name) == "editnotebook" {
|
||||||
string("target_notebook")
|
string("target_notebook")
|
||||||
} else {
|
} else {
|
||||||
string("path")
|
aliased(&["path", "file_path", "filePath"])
|
||||||
},
|
},
|
||||||
stream_content: Some(edit::normalize_newlines(&stream_content)),
|
stream_content: Some(edit::normalize_newlines(&stream_content)),
|
||||||
})
|
})
|
||||||
@@ -418,7 +432,7 @@ pub fn render_tool_call(call: &ToolCall, completed: bool) -> Result<pb::ToolCall
|
|||||||
}
|
}
|
||||||
Some(pb::tool_call::Tool::WebSearchToolCall(tool)) => {
|
Some(pb::tool_call::Tool::WebSearchToolCall(tool)) => {
|
||||||
tool.args = Some(pb::WebSearchArgs {
|
tool.args = Some(pb::WebSearchArgs {
|
||||||
search_term: string("search_term"),
|
search_term: aliased(&["search_term", "query"]),
|
||||||
tool_call_id: call.call_id.clone(),
|
tool_call_id: call.call_id.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,18 @@ pub fn request(id: u32, call: &ToolCall, context: &ExecContext) -> Result<pb::Ag
|
|||||||
.map(str::to_string)
|
.map(str::to_string)
|
||||||
.ok_or_else(|| Error::Protocol(format!("{} is missing {name}", call.name)))
|
.ok_or_else(|| Error::Protocol(format!("{} is missing {name}", call.name)))
|
||||||
};
|
};
|
||||||
|
// Claude 系模型常按 Claude Code 习惯输出别名参数(如 file_path),逐个回退兼容。
|
||||||
|
let string_aliased = |names: &[&str]| -> Result<String> {
|
||||||
|
for name in names {
|
||||||
|
if let Some(value) = call.arguments.get(name).and_then(Value::as_str) {
|
||||||
|
return Ok(value.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(Error::Protocol(format!(
|
||||||
|
"{} is missing {}",
|
||||||
|
call.name, names[0]
|
||||||
|
)))
|
||||||
|
};
|
||||||
let optional_string = |name: &str| {
|
let optional_string = |name: &str| {
|
||||||
call.arguments
|
call.arguments
|
||||||
.get(name)
|
.get(name)
|
||||||
@@ -63,7 +75,7 @@ pub fn request(id: u32, call: &ToolCall, context: &ExecContext) -> Result<pb::Ag
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
"read" => Message::ReadArgs(pb::ReadArgs {
|
"read" => Message::ReadArgs(pb::ReadArgs {
|
||||||
path: string("path")?,
|
path: string_aliased(&["path", "file_path", "filePath"])?,
|
||||||
tool_call_id: call.call_id.clone(),
|
tool_call_id: call.call_id.clone(),
|
||||||
offset: int("offset"),
|
offset: int("offset"),
|
||||||
limit: call
|
limit: call
|
||||||
@@ -74,7 +86,7 @@ pub fn request(id: u32, call: &ToolCall, context: &ExecContext) -> Result<pb::Ag
|
|||||||
encoding_hint: optional_string("encoding_hint"),
|
encoding_hint: optional_string("encoding_hint"),
|
||||||
}),
|
}),
|
||||||
"delete" => Message::DeleteArgs(pb::DeleteArgs {
|
"delete" => Message::DeleteArgs(pb::DeleteArgs {
|
||||||
path: string("path")?,
|
path: string_aliased(&["path", "file_path", "filePath"])?,
|
||||||
tool_call_id: call.call_id.clone(),
|
tool_call_id: call.call_id.clone(),
|
||||||
}),
|
}),
|
||||||
"grep" => Message::GrepArgs(pb::GrepArgs {
|
"grep" => Message::GrepArgs(pb::GrepArgs {
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ pub(crate) struct EditWrite {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn path(call: &ToolCall) -> Result<String> {
|
pub(crate) fn path(call: &ToolCall) -> Result<String> {
|
||||||
let field = if normalized(&call.name) == "editnotebook" {
|
if normalized(&call.name) == "editnotebook" {
|
||||||
"target_notebook"
|
string(call, "target_notebook")
|
||||||
} else {
|
} else {
|
||||||
"path"
|
// Claude 系模型常按 Claude Code 习惯输出 file_path/filePath,做别名兼容。
|
||||||
};
|
string_any(call, &["path", "file_path", "filePath"])
|
||||||
string(call, field)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn execution_path(call: &ToolCall) -> Result<Option<String>> {
|
pub(crate) fn execution_path(call: &ToolCall) -> Result<Option<String>> {
|
||||||
@@ -63,7 +63,10 @@ pub(crate) fn after_read(
|
|||||||
};
|
};
|
||||||
let after = match normalized(&call.name).as_str() {
|
let after = match normalized(&call.name).as_str() {
|
||||||
"write" => {
|
"write" => {
|
||||||
normalize_newlines(&string(call, "contents").map_err(|error| error.to_string())?)
|
// Claude Code 习惯的 content 作为 contents 的别名兼容。
|
||||||
|
normalize_newlines(
|
||||||
|
&string_any(call, &["contents", "content"]).map_err(|error| error.to_string())?,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
"strreplace" => replace_string(call, &before)?,
|
"strreplace" => replace_string(call, &before)?,
|
||||||
"editnotebook" => edit_notebook(call, &before)?,
|
"editnotebook" => edit_notebook(call, &before)?,
|
||||||
@@ -230,11 +233,19 @@ fn source_lines(value: &str) -> Vec<Value> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn string(call: &ToolCall, field: &str) -> Result<String> {
|
fn string(call: &ToolCall, field: &str) -> Result<String> {
|
||||||
call.arguments
|
string_any(call, &[field])
|
||||||
.get(field)
|
}
|
||||||
.and_then(Value::as_str)
|
|
||||||
.map(str::to_owned)
|
fn string_any(call: &ToolCall, fields: &[&str]) -> Result<String> {
|
||||||
.ok_or_else(|| Error::Protocol(format!("{} is missing {field}", call.name)))
|
for field in fields {
|
||||||
|
if let Some(value) = call.arguments.get(field).and_then(Value::as_str) {
|
||||||
|
return Ok(value.to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(Error::Protocol(format!(
|
||||||
|
"{} is missing {}",
|
||||||
|
call.name, fields[0]
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalized(value: &str) -> String {
|
fn normalized(value: &str) -> String {
|
||||||
|
|||||||
@@ -88,12 +88,17 @@ fn start_web_search(
|
|||||||
search: WebSearch,
|
search: WebSearch,
|
||||||
pending: PendingInteraction,
|
pending: PendingInteraction,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let query = pending
|
// Claude Code 习惯的 query 作为 search_term 的别名兼容。
|
||||||
|
let query = ["search_term", "query"]
|
||||||
|
.iter()
|
||||||
|
.find_map(|name| {
|
||||||
|
pending
|
||||||
.call
|
.call
|
||||||
.arguments
|
.arguments
|
||||||
.get("search_term")
|
.get(name)
|
||||||
.and_then(serde_json::Value::as_str)
|
.and_then(serde_json::Value::as_str)
|
||||||
.filter(|query| !query.trim().is_empty())
|
.filter(|value| !value.trim().is_empty())
|
||||||
|
})
|
||||||
.ok_or_else(|| Error::Protocol("WebSearch is missing search_term".into()))?
|
.ok_or_else(|| Error::Protocol("WebSearch is missing search_term".into()))?
|
||||||
.to_string();
|
.to_string();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
|
|||||||
Reference in New Issue
Block a user