Merge branch 'main' of github.com:leookun/cursor-byok

This commit is contained in:
leokun
2026-09-09 17:45:15 +08:00
5 changed files with 113 additions and 25 deletions
+47 -1
View File
@@ -12,6 +12,18 @@ pub fn tool_query(id: u32, call: &ToolCall) -> Result<pb::AgentServerMessage> {
.map(str::to_string)
.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| {
call.arguments
.get(name)
@@ -80,7 +92,7 @@ pub fn tool_query(id: u32, call: &ToolCall) -> Result<pb::AgentServerMessage> {
}
"websearch" => Query::WebSearchRequestQuery(pb::WebSearchRequestQuery {
args: Some(pb::WebSearchArgs {
search_term: string("search_term")?,
search_term: string_aliased(&["search_term", "query"])?,
tool_call_id: call.call_id.clone(),
}),
}),
@@ -214,3 +226,37 @@ fn normalized(value: &str) -> String {
.flat_map(char::to_lowercase)
.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");
}
}
+19 -5
View File
@@ -286,6 +286,20 @@ pub fn render_tool_call(call: &ToolCall, completed: bool) -> Result<pb::ToolCall
.and_then(Value::as_str)
.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() {
Some(pb::tool_call::Tool::ShellToolCall(tool)) => {
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)) => {
tool.args = Some(pb::DeleteArgs {
path: string("path"),
path: aliased(&["path", "file_path", "filePath"]),
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)) => {
tool.args = Some(pb::ReadToolArgs {
path: string("path"),
path: aliased(&["path", "file_path", "filePath"]),
offset: call
.arguments
.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)) => {
let stream_content = if normalized(&call.name) == "write" {
optional("contents").unwrap_or_default()
optional_aliased(&["contents", "content"]).unwrap_or_default()
} else {
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" {
string("target_notebook")
} else {
string("path")
aliased(&["path", "file_path", "filePath"])
},
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)) => {
tool.args = Some(pb::WebSearchArgs {
search_term: string("search_term"),
search_term: aliased(&["search_term", "query"]),
tool_call_id: call.call_id.clone(),
})
}
+14 -2
View File
@@ -22,6 +22,18 @@ pub fn request(id: u32, call: &ToolCall, context: &ExecContext) -> Result<pb::Ag
.map(str::to_string)
.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| {
call.arguments
.get(name)
@@ -63,7 +75,7 @@ pub fn request(id: u32, call: &ToolCall, context: &ExecContext) -> Result<pb::Ag
})
}
"read" => Message::ReadArgs(pb::ReadArgs {
path: string("path")?,
path: string_aliased(&["path", "file_path", "filePath"])?,
tool_call_id: call.call_id.clone(),
offset: int("offset"),
limit: call
@@ -74,7 +86,7 @@ pub fn request(id: u32, call: &ToolCall, context: &ExecContext) -> Result<pb::Ag
encoding_hint: optional_string("encoding_hint"),
}),
"delete" => Message::DeleteArgs(pb::DeleteArgs {
path: string("path")?,
path: string_aliased(&["path", "file_path", "filePath"])?,
tool_call_id: call.call_id.clone(),
}),
"grep" => Message::GrepArgs(pb::GrepArgs {
+22 -11
View File
@@ -13,12 +13,12 @@ pub(crate) struct EditWrite {
}
pub(crate) fn path(call: &ToolCall) -> Result<String> {
let field = if normalized(&call.name) == "editnotebook" {
"target_notebook"
if normalized(&call.name) == "editnotebook" {
string(call, "target_notebook")
} else {
"path"
};
string(call, field)
// Claude 系模型常按 Claude Code 习惯输出 file_path/filePath,做别名兼容。
string_any(call, &["path", "file_path", "filePath"])
}
}
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() {
"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)?,
"editnotebook" => edit_notebook(call, &before)?,
@@ -230,11 +233,19 @@ fn source_lines(value: &str) -> Vec<Value> {
}
fn string(call: &ToolCall, field: &str) -> Result<String> {
call.arguments
.get(field)
.and_then(Value::as_str)
.map(str::to_owned)
.ok_or_else(|| Error::Protocol(format!("{} is missing {field}", call.name)))
string_any(call, &[field])
}
fn string_any(call: &ToolCall, fields: &[&str]) -> Result<String> {
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 {
@@ -88,12 +88,17 @@ fn start_web_search(
search: WebSearch,
pending: PendingInteraction,
) -> Result<()> {
let query = pending
// Claude Code 习惯的 query 作为 search_term 的别名兼容。
let query = ["search_term", "query"]
.iter()
.find_map(|name| {
pending
.call
.arguments
.get("search_term")
.get(name)
.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()))?
.to_string();
tokio::spawn(async move {