From cd80367fa1299bbd949f53d2a51dd37a00b5df04 Mon Sep 17 00:00:00 2001 From: ehgen0ng Date: Tue, 27 Jan 2026 20:09:46 +0800 Subject: [PATCH 01/10] =?UTF-8?q?=E2=9C=A8=20feat(amp=5Fprocessor):=20?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=20Claude=20Code=20=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E5=A4=84=E7=90=86=20-=20=E3=80=90=E7=B3=BB=E7=BB=9F=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E5=A4=84=E7=90=86=E3=80=91=E5=9C=A8=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E4=B8=AD=E6=B3=A8=E5=85=A5=20Claude=20Code?= =?UTF-8?q?=20=E8=BA=AB=E4=BB=BD=E5=A3=B0=E6=98=8E=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E6=B8=85=E6=B4=97=E5=93=81=E7=89=8C=E6=96=87=E6=9C=AC=EF=BC=8C?= =?UTF-8?q?=E4=BB=A5=E5=AF=B9=E9=BD=90=20JS=20=E6=8F=92=E4=BB=B6=E8=A1=8C?= =?UTF-8?q?=E4=B8=BA=20-=20=E3=80=90Anthropic=20Beta=20=E5=A4=B4=E3=80=91?= =?UTF-8?q?=E5=90=88=E5=B9=B6=E4=BC=A0=E5=85=A5=E7=9A=84=20beta=20?= =?UTF-8?q?=E5=A4=B4=E4=B8=8E=E5=BF=85=E9=9C=80=E7=9A=84=20beta=20?= =?UTF-8?q?=E5=A4=B4=EF=BC=88oauth,=20interleaved-thinking=EF=BC=89?= =?UTF-8?q?=EF=BC=8C=E7=A1=AE=E4=BF=9D=E5=85=BC=E5=AE=B9=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🐛 fix(amp_processor): 确保工具使用名称前缀一致 - 统一处理 `messages[].content[]` 中 `type=="tool_use"` 的工具名称前缀,保持幂等性 ♻️ refactor(proxy): 优化 AMP 处理器代码结构与工具函数 - 【AMP 处理器】提取 MCP 名称前缀和品牌清洗的正则表达式为惰性静态常量,提高可读性和效率 - 【AMP 处理器】将工具名称前缀移除逻辑和品牌文本清洗逻辑封装为独立函数,增强模块化 - 【代理实例】在 SSE 流和最终响应体处理中,统一使用新的工具名称前缀移除函数,消除重复代码 🔧 chore(amp_processor): 更新 Claude CLI 用户代理版本 - 将 `claude-cli` 的用户代理版本从 2.0.72 更新至 2.1.2 💄 style(proxy_instance): 明确流过滤器中的类型注解 - 为 `__stream_end_marker__` 过滤器回调函数添加显式类型注解,提升代码清晰度 --- .../services/proxy/headers/amp_processor.rs | 123 +++++++++++++++++- src-tauri/src/services/proxy/headers/mod.rs | 2 + .../src/services/proxy/proxy_instance.rs | 37 +++--- 3 files changed, 139 insertions(+), 23 deletions(-) diff --git a/src-tauri/src/services/proxy/headers/amp_processor.rs b/src-tauri/src/services/proxy/headers/amp_processor.rs index 07293bf..af8003b 100644 --- a/src-tauri/src/services/proxy/headers/amp_processor.rs +++ b/src-tauri/src/services/proxy/headers/amp_processor.rs @@ -36,6 +36,27 @@ static HTTP_CLIENT: Lazy = Lazy::new(|| { .expect("Failed to create HTTP client") }); +static MCP_NAME_PREFIX_RE: Lazy = Lazy::new(|| { + regex::Regex::new(r#""name"\s*:\s*"mcp_([^"]+)""#).expect("mcp name 前缀正则非法") +}); + +static BRAND_SANITIZE_RE: Lazy = Lazy::new(|| { + // 不区分大小写 + 单词边界替换,避免误伤子串(例如 "example" 中的 "amp")。 + regex::Regex::new(r"(?i)\b(?:opencode|amp(?:-?code)?)\b").expect("清洗正则非法") +}); + +const CLAUDE_CODE_PREAMBLE: &str = "You are Claude Code, Anthropic's official CLI for Claude."; + +pub(crate) fn strip_mcp_name_prefix_bytes(bytes: &Bytes) -> Bytes { + let text = String::from_utf8_lossy(bytes); + let cleaned = MCP_NAME_PREFIX_RE.replace_all(&text, r#""name": "$1""#); + Bytes::from(cleaned.into_owned()) +} + +fn sanitize_brand_text(s: &str) -> String { + BRAND_SANITIZE_RE.replace_all(s, "Claude Code").into_owned() +} + /// 最大响应体大小(5MB) const MAX_RESPONSE_SIZE: usize = 5 * 1024 * 1024; @@ -165,7 +186,7 @@ impl AmpHeadersProcessor { fn get_user_agent(api_type: ApiType, path: &str, body: &[u8]) -> String { match api_type { - ApiType::Claude => "claude-cli/2.0.72 (external, cli)".to_string(), + ApiType::Claude => "claude-cli/2.1.2 (external, cli)".to_string(), ApiType::Codex => { "codex_cli_rs/0.77.0 (Mac OS 15.7.2; arm64) Apple_Terminal/455.1".to_string() } @@ -194,6 +215,50 @@ impl AmpHeadersProcessor { } } + // 0) system 文本清洗 + 注入 Claude Code 身份声明(对齐 JS 插件行为) + // - 文本清洗:OpenCode/opencode/ampcode/amp-code/amp(不区分大小写) + // - 注入:将声明插到 system 最前面 + if let Some(system) = json.get_mut("system") { + match system { + serde_json::Value::Array(items) => { + for item in items.iter_mut() { + if item.get("type").and_then(|t| t.as_str()) != Some("text") { + continue; + } + if let Some(text) = item.get("text").and_then(|t| t.as_str()) { + item["text"] = serde_json::Value::String(sanitize_brand_text(text)); + } + } + + let already_prefixed = items + .first() + .and_then(|v| v.get("type").and_then(|t| t.as_str())) + == Some("text") + && items + .first() + .and_then(|v| v.get("text").and_then(|t| t.as_str())) + == Some(CLAUDE_CODE_PREAMBLE); + if !already_prefixed { + items.insert(0, json!({ "type": "text", "text": CLAUDE_CODE_PREAMBLE })); + } + } + serde_json::Value::String(s) => { + let cleaned = sanitize_brand_text(s); + if !cleaned.starts_with(CLAUDE_CODE_PREAMBLE) { + *s = format!("{}\n{}", CLAUDE_CODE_PREAMBLE, cleaned); + } else { + *s = cleaned; + } + } + _ => { + // 其他格式不处理 + } + } + } else { + json["system"] = json!([{ "type": "text", "text": CLAUDE_CODE_PREAMBLE }]); + } + + // 1) tools[].name 加前缀 if let Some(tools) = json.get_mut("tools").and_then(|t| t.as_array_mut()) { for tool in tools.iter_mut() { if let Some(name) = tool.get("name").and_then(|n| n.as_str()) { @@ -205,6 +270,32 @@ impl AmpHeadersProcessor { } } + // 2) messages[].content[] 里 type=="tool_use" 的 name 也要加前缀(保持幂等) + if let Some(messages) = json.get_mut("messages").and_then(|m| m.as_array_mut()) { + for msg in messages.iter_mut() { + let Some(content) = msg.get_mut("content") else { + continue; + }; + + let Some(arr) = content.as_array_mut() else { + continue; + }; + + for item in arr.iter_mut() { + if item.get("type").and_then(|t| t.as_str()) != Some("tool_use") { + continue; + } + + if let Some(name) = item.get("name").and_then(|n| n.as_str()) { + if !name.starts_with(TOOL_PREFIX) { + item["name"] = + serde_json::Value::String(format!("{}{}", TOOL_PREFIX, name)); + } + } + } + } + } + serde_json::to_vec(&json).unwrap_or_else(|_| body.to_vec()) } @@ -934,6 +1025,36 @@ impl RequestProcessor for AmpHeadersProcessor { ); result.headers.insert("x-app", "cli".parse().unwrap()); + // 保留调用方传入的 anthropic-beta,同时确保必需 beta 存在(对齐 JS 插件行为) + { + const REQUIRED_BETAS: [&str; 2] = + ["oauth-2025-04-20", "interleaved-thinking-2025-05-14"]; + let incoming = result + .headers + .get("anthropic-beta") + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + + let mut betas = std::collections::BTreeSet::new(); + for b in incoming + .split(',') + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + { + betas.insert(b.to_string()); + } + for b in REQUIRED_BETAS { + betas.insert(b.to_string()); + } + + if !betas.is_empty() { + let merged = betas.into_iter().collect::>().join(","); + result + .headers + .insert("anthropic-beta", merged.parse().unwrap()); + } + } + if !result.target_url.contains("beta=true") { if result.target_url.contains('?') { result.target_url.push_str("&beta=true"); diff --git a/src-tauri/src/services/proxy/headers/mod.rs b/src-tauri/src/services/proxy/headers/mod.rs index 4730e01..049738f 100644 --- a/src-tauri/src/services/proxy/headers/mod.rs +++ b/src-tauri/src/services/proxy/headers/mod.rs @@ -12,6 +12,8 @@ mod codex_processor; mod gemini_processor; pub use amp_processor::AmpHeadersProcessor; + +pub(crate) use amp_processor::strip_mcp_name_prefix_bytes; pub use claude_processor::ClaudeHeadersProcessor; pub use codex_processor::CodexHeadersProcessor; pub use gemini_processor::GeminiHeadersProcessor; diff --git a/src-tauri/src/services/proxy/proxy_instance.rs b/src-tauri/src/services/proxy/proxy_instance.rs index b3f3cad..69cc5ad 100644 --- a/src-tauri/src/services/proxy/proxy_instance.rs +++ b/src-tauri/src/services/proxy/proxy_instance.rs @@ -468,9 +468,10 @@ async fn handle_request_inner( // SSE 流式响应:收集响应体并调用 processor.record_request_log use futures_util::StreamExt; - use regex::Regex; use std::sync::{Arc, Mutex}; + use super::headers::strip_mcp_name_prefix_bytes; + let config_name = proxy_config .real_profile_name .clone() @@ -489,7 +490,6 @@ async fn handle_request_inner( // amp-code 需要移除工具名前缀 let is_amp_code = tool_id == "amp-code"; - let prefix_regex = Regex::new(r#""name"\s*:\s*"mcp_([^"]+)""#).ok(); // 拦截流数据并收集 let mapped_stream = stream @@ -508,13 +508,7 @@ async fn handle_request_inner( result .map(|bytes| { if is_amp_code { - if let Some(ref re) = prefix_regex { - let text = String::from_utf8_lossy(&bytes); - let cleaned = re.replace_all(&text, r#""name": "$1""#); - Frame::data(Bytes::from(cleaned.into_owned())) - } else { - Frame::data(bytes) - } + Frame::data(strip_mcp_name_prefix_bytes(&bytes)) } else { Frame::data(bytes) } @@ -531,15 +525,17 @@ async fn handle_request_inner( as Box) })) // 过滤掉结束标记 - .filter(|item| { - let is_end_marker = item - .as_ref() - .err() - .and_then(|e| e.downcast_ref::()) - .map(|e| e.to_string().contains("__stream_end_marker__")) - .unwrap_or(false); - futures_util::future::ready(!is_end_marker) - }); + .filter( + |item: &Result, Box>| { + let is_end_marker = item + .as_ref() + .err() + .and_then(|e| e.downcast_ref::()) + .map(|e| e.to_string().contains("__stream_end_marker__")) + .unwrap_or(false); + futures_util::future::ready(!is_end_marker) + }, + ); // 在流真正结束后异步记录日志 let processor_clone = Arc::clone(&processor); @@ -611,10 +607,7 @@ async fn handle_request_inner( // amp-code 需要清理响应体中的工具名前缀 let final_body = if tool_id == "amp-code" { - let text = String::from_utf8_lossy(&body_bytes); - let re = regex::Regex::new(r#""name"\s*:\s*"mcp_([^"]+)""#).unwrap(); - let cleaned = re.replace_all(&text, r#""name": "$1""#); - Bytes::from(cleaned.into_owned()) + super::headers::strip_mcp_name_prefix_bytes(&body_bytes) } else { body_bytes.clone() }; From 869529555acd43babda5958d4253c8776429ed23 Mon Sep 17 00:00:00 2001 From: ehgen0ng Date: Tue, 27 Jan 2026 20:46:43 +0800 Subject: [PATCH 02/10] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(proxy):=20?= =?UTF-8?q?=E6=A0=87=E5=87=86=E5=8C=96=E7=BC=93=E5=AD=98=E6=8E=A7=E5=88=B6?= =?UTF-8?q?TTL=20-=20=E5=BC=95=E5=85=A5=20`normalize=5Fcache=5Fcontrol`=20?= =?UTF-8?q?=E5=87=BD=E6=95=B0=E4=BB=A5=E7=BB=9F=E4=B8=80=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E6=8E=A7=E5=88=B6=E7=AD=96=E7=95=A5=20-=20=E5=B0=86=20system?= =?UTF-8?q?=20=E5=AD=97=E6=AE=B5=E4=B8=AD=E7=9A=84=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E6=8E=A7=E5=88=B6=E7=BB=9F=E4=B8=80=E4=B8=BA=205=20=E5=88=86?= =?UTF-8?q?=E9=92=9F=20TTL=20-=20=E5=B0=86=20tools=20=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E7=BC=93=E5=AD=98=E6=8E=A7=E5=88=B6=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E4=B8=BA=205=20=E5=88=86=E9=92=9F=20TTL=20-=20?= =?UTF-8?q?=E5=B0=86=20messages=20=E5=AD=97=E6=AE=B5=E4=B8=AD=20content=20?= =?UTF-8?q?=E9=A1=B9=E7=9A=84=E7=BC=93=E5=AD=98=E6=8E=A7=E5=88=B6=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E4=B8=BA=205=20=E5=88=86=E9=92=9F=20TTL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/proxy/headers/amp_processor.rs | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/services/proxy/headers/amp_processor.rs b/src-tauri/src/services/proxy/headers/amp_processor.rs index af8003b..5f9f18c 100644 --- a/src-tauri/src/services/proxy/headers/amp_processor.rs +++ b/src-tauri/src/services/proxy/headers/amp_processor.rs @@ -57,6 +57,18 @@ fn sanitize_brand_text(s: &str) -> String { BRAND_SANITIZE_RE.replace_all(s, "Claude Code").into_owned() } +/// 统一 cache_control 为标准 5m ttl +fn normalize_cache_control(item: &mut Value) { + if let Some(obj) = item.as_object_mut() { + if obj.contains_key("cache_control") { + obj.insert( + "cache_control".to_string(), + json!({ "type": "ephemeral", "ttl": "5m" }), + ); + } + } +} + /// 最大响应体大小(5MB) const MAX_RESPONSE_SIZE: usize = 5 * 1024 * 1024; @@ -218,10 +230,13 @@ impl AmpHeadersProcessor { // 0) system 文本清洗 + 注入 Claude Code 身份声明(对齐 JS 插件行为) // - 文本清洗:OpenCode/opencode/ampcode/amp-code/amp(不区分大小写) // - 注入:将声明插到 system 最前面 + // - 统一 cache_control 为 5m if let Some(system) = json.get_mut("system") { match system { serde_json::Value::Array(items) => { for item in items.iter_mut() { + normalize_cache_control(item); + if item.get("type").and_then(|t| t.as_str()) != Some("text") { continue; } @@ -258,9 +273,11 @@ impl AmpHeadersProcessor { json["system"] = json!([{ "type": "text", "text": CLAUDE_CODE_PREAMBLE }]); } - // 1) tools[].name 加前缀 + // 1) tools[].name 加前缀 + 统一 cache_control if let Some(tools) = json.get_mut("tools").and_then(|t| t.as_array_mut()) { for tool in tools.iter_mut() { + normalize_cache_control(tool); + if let Some(name) = tool.get("name").and_then(|n| n.as_str()) { if !name.starts_with(TOOL_PREFIX) { tool["name"] = @@ -270,7 +287,7 @@ impl AmpHeadersProcessor { } } - // 2) messages[].content[] 里 type=="tool_use" 的 name 也要加前缀(保持幂等) + // 2) messages[].content[] 里 type=="tool_use" 的 name 也要加前缀 + 统一所有 content item 的 cache_control if let Some(messages) = json.get_mut("messages").and_then(|m| m.as_array_mut()) { for msg in messages.iter_mut() { let Some(content) = msg.get_mut("content") else { @@ -282,6 +299,8 @@ impl AmpHeadersProcessor { }; for item in arr.iter_mut() { + normalize_cache_control(item); + if item.get("type").and_then(|t| t.as_str()) != Some("tool_use") { continue; } From 03154eed2a5237af9ca6f7bcb9cc46595404ee8a Mon Sep 17 00:00:00 2001 From: ehgen0ng Date: Tue, 3 Feb 2026 21:43:04 +0800 Subject: [PATCH 03/10] =?UTF-8?q?=E2=9C=A8=20feat(codex):=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=20codex=20=E8=AF=B7=E6=B1=82=E4=BD=93=E8=BD=AC?= =?UTF-8?q?=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除请求体中的 `max_output_tokens` 字段,以适应 Codex API 的要求。 - 从 `input` 数组中提取 `role=system` 的内容,并将其注入为 `instructions` 字段,以支持系统指令。 - 在请求体被修改后,移除 `content-length` 和 `transfer-encoding` 头,确保请求的准确性。 --- .../services/proxy/headers/amp_processor.rs | 75 ++++++++++++++++++- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/services/proxy/headers/amp_processor.rs b/src-tauri/src/services/proxy/headers/amp_processor.rs index 4da3700..8cb4caf 100644 --- a/src-tauri/src/services/proxy/headers/amp_processor.rs +++ b/src-tauri/src/services/proxy/headers/amp_processor.rs @@ -1084,7 +1084,73 @@ impl RequestProcessor for AmpHeadersProcessor { } ApiType::Codex => { let p = codex.ok_or_else(|| anyhow!("未配置 Codex Profile"))?; - tracing::info!("AMP Code → Codex: {}{}", p.base_url, llm_path); + let cleaned_body = if body.is_empty() { + None + } else { + match serde_json::from_slice::(body) { + Ok(mut json_body) => { + let mut modified = false; + + // 移除 max_output_tokens + if json_body + .as_object_mut() + .and_then(|obj| obj.remove("max_output_tokens")) + .is_some() + { + modified = true; + } + + // 注入 instructions:从 input 数组中提取 role=system 的 content + if let Some(obj) = json_body.as_object() { + if let Some(input) = obj.get("input").and_then(|v| v.as_array()) { + let system_content: String = input + .iter() + .filter(|item| { + item.get("role") + .and_then(|r| r.as_str()) + .map(|r| r == "system") + .unwrap_or(false) + }) + .filter_map(|item| { + item.get("content").and_then(|c| c.as_str()) + }) + .collect::>() + .join("\n\n"); + + if !system_content.is_empty() { + // 按顺序重建 JSON:model → instructions → 其他字段 + let mut ordered = Map::new(); + if let Some(model) = obj.get("model") { + ordered.insert("model".to_string(), model.clone()); + } + ordered.insert( + "instructions".to_string(), + Value::String(system_content), + ); + for (k, v) in obj.iter() { + if k != "model" && k != "instructions" { + ordered.insert(k.clone(), v.clone()); + } + } + json_body = Value::Object(ordered); + modified = true; + tracing::debug!( + "AMP Code Codex: 注入 instructions(来自 input system)" + ); + } + } + } + + if modified { + serde_json::to_vec(&json_body).ok() + } else { + None + } + } + Err(_) => None, + } + }; + let body_to_forward: &[u8] = cleaned_body.as_deref().unwrap_or(body); let mut result = CodexHeadersProcessor .process_outgoing_request( &p.base_url, @@ -1092,9 +1158,14 @@ impl RequestProcessor for AmpHeadersProcessor { &llm_path, query, original_headers, - body, + body_to_forward, ) .await?; + if cleaned_body.is_some() { + result.headers.remove("content-length"); + result.headers.remove("transfer-encoding"); + } + tracing::info!("AMP Code → Codex: {}", result.target_url); result.headers.insert( "user-agent", Self::get_user_agent(api_type, path, body).parse().unwrap(), From 66c44427f2eaa82e4ef2f23e29f8617e03658535 Mon Sep 17 00:00:00 2001 From: ehgen0ng Date: Wed, 4 Feb 2026 12:10:23 +0800 Subject: [PATCH 04/10] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(amp=5Fproce?= =?UTF-8?q?ssor):=20=E4=BC=98=E5=8C=96=E7=B3=BB=E7=BB=9F=E6=8C=87=E4=BB=A4?= =?UTF-8?q?=E6=B3=A8=E5=85=A5=E9=80=BB=E8=BE=91=20-=20=E7=A1=AE=E4=BF=9D?= =?UTF-8?q?=E5=BD=93=E5=AD=98=E5=9C=A8role=3Dsystem=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E6=97=B6=EF=BC=8C=E5=8D=B3=E4=BD=BF=E5=85=B6=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E4=B8=BA=E7=A9=BA=EF=BC=8C=E4=B9=9F=E8=83=BD=E6=AD=A3=E7=A1=AE?= =?UTF-8?q?=E6=B3=A8=E5=85=A5instructions=E5=AD=97=E6=AE=B5=20-=20?= =?UTF-8?q?=E8=B0=83=E6=95=B4=E4=B9=8B=E5=89=8D=E4=BB=85=E5=9C=A8system?= =?UTF-8?q?=E5=86=85=E5=AE=B9=E9=9D=9E=E7=A9=BA=E6=97=B6=E6=89=8D=E6=B3=A8?= =?UTF-8?q?=E5=85=A5=E7=9A=84=E9=99=90=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/proxy/headers/amp_processor.rs | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src-tauri/src/services/proxy/headers/amp_processor.rs b/src-tauri/src/services/proxy/headers/amp_processor.rs index 8cb4caf..1a4706d 100644 --- a/src-tauri/src/services/proxy/headers/amp_processor.rs +++ b/src-tauri/src/services/proxy/headers/amp_processor.rs @@ -1100,24 +1100,32 @@ impl RequestProcessor for AmpHeadersProcessor { modified = true; } - // 注入 instructions:从 input 数组中提取 role=system 的 content + // 注入 instructions:从 input 数组中提取 role=system 的 content(含空字符串) if let Some(obj) = json_body.as_object() { if let Some(input) = obj.get("input").and_then(|v| v.as_array()) { - let system_content: String = input - .iter() - .filter(|item| { - item.get("role") - .and_then(|r| r.as_str()) - .map(|r| r == "system") - .unwrap_or(false) - }) - .filter_map(|item| { - item.get("content").and_then(|c| c.as_str()) - }) - .collect::>() - .join("\n\n"); - - if !system_content.is_empty() { + // 检查是否存在 role=system 的消息 + let has_system = input.iter().any(|item| { + item.get("role") + .and_then(|r| r.as_str()) + .map(|r| r == "system") + .unwrap_or(false) + }); + + if has_system { + let system_content: String = input + .iter() + .filter(|item| { + item.get("role") + .and_then(|r| r.as_str()) + .map(|r| r == "system") + .unwrap_or(false) + }) + .filter_map(|item| { + item.get("content").and_then(|c| c.as_str()) + }) + .collect::>() + .join("\n\n"); + // 按顺序重建 JSON:model → instructions → 其他字段 let mut ordered = Map::new(); if let Some(model) = obj.get("model") { From 787b06eeaeaaf2cd0085d5fe1ca4b0cdd82e52ae Mon Sep 17 00:00:00 2001 From: ehgen0ng Date: Sat, 7 Feb 2026 22:47:34 +0800 Subject: [PATCH 05/10] =?UTF-8?q?=E2=9C=A8=20feat(amp):=20=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E8=BF=9C=E7=A8=8B=E5=B7=A5=E5=85=B7=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E5=92=8C=E5=B9=BF=E5=91=8A=E6=8B=A6=E6=88=AA=20-=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E9=80=9A=E8=BF=87=20ampcode.com=20=E8=BF=9C=E7=A8=8B?= =?UTF-8?q?=E6=89=A7=E8=A1=8C=E5=B7=A5=E5=85=B7=E7=9A=84=E8=83=BD=E5=8A=9B?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=85=88=E5=A4=84=E7=90=86=E5=85=8D=E8=B4=B9?= =?UTF-8?q?=E5=B1=82=E8=AF=B7=E6=B1=82=20-=20=E5=9C=A8=E8=BF=9C=E7=A8=8B?= =?UTF-8?q?=E6=89=A7=E8=A1=8C=E5=A4=B1=E8=B4=A5=E6=97=B6=EF=BC=8C=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E9=99=8D=E7=BA=A7=E5=88=B0=E6=9C=AC=E5=9C=B0=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E5=A4=84=E7=90=86=EF=BC=8C=E6=8F=90=E9=AB=98=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E9=B2=81=E6=A3=92=E6=80=A7=20-=20=E6=8B=A6=E6=88=AA?= =?UTF-8?q?=E5=B9=B6=E9=98=BB=E6=AD=A2=E5=86=85=E9=83=A8=E5=B9=BF=E5=91=8A?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=EF=BC=8C=E4=BC=98=E5=8C=96=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ♻️ refactor(proxy): 优化代理请求处理和日志输出 - 移除转发请求到 ampcode.com 时不必要的头部处理,确保 reqwest 自动解压 - 将多处 `tracing::info!` 日志级别降级为 `tracing::debug!`,减少冗余信息 --- .../services/proxy/headers/amp_processor.rs | 189 +++++++++++++++++- .../src/services/proxy/proxy_instance.rs | 10 +- 2 files changed, 184 insertions(+), 15 deletions(-) diff --git a/src-tauri/src/services/proxy/headers/amp_processor.rs b/src-tauri/src/services/proxy/headers/amp_processor.rs index 1a4706d..9f94856 100644 --- a/src-tauri/src/services/proxy/headers/amp_processor.rs +++ b/src-tauri/src/services/proxy/headers/amp_processor.rs @@ -1,7 +1,7 @@ // AMP Code 请求处理器 // // 路由逻辑: -// 0. 本地工具拦截:webSearch2 / extractWebPageContent → 本地处理 +// 0. 工具拦截:webSearch2 / extractWebPageContent → 远程免费层优先,失败降级本地处理 // 1. /api/provider/anthropic/* → Claude Profile(提取 /v1/messages) // 2. /api/provider/openai/* → Codex Profile(提取 /v1/responses 或 /v1/chat/completions) // 3. /api/provider/google/* → Gemini Profile(提取 /v1beta/...) @@ -36,6 +36,10 @@ static HTTP_CLIENT: Lazy = Lazy::new(|| { .expect("Failed to create HTTP client") }); +static IS_FREE_TIER_RE: Lazy = Lazy::new(|| { + regex::Regex::new(r#""isFreeTierRequest"\s*:\s*false"#).expect("isFreeTierRequest 正则非法") +}); + static MCP_NAME_PREFIX_RE: Lazy = Lazy::new(|| { regex::Regex::new(r#""name"\s*:\s*"mcp_([^"]+)""#).expect("mcp name 前缀正则非法") }); @@ -345,17 +349,17 @@ impl AmpHeadersProcessor { None => format!("{}{}", base_url, path), }; - tracing::info!("AMP Code → ampcode.com: {}", target_url); + tracing::debug!("AMP Code → ampcode.com: {}", target_url); let mut new_headers = headers.clone(); new_headers.remove(hyper::header::AUTHORIZATION); - let x_api_key = hyper::header::HeaderName::from_static("x-api-key"); - new_headers.remove(&x_api_key); + // 移除 accept-encoding,让 reqwest 自动解压响应体 + // 否则 strip_mcp_name_prefix_bytes 会对压缩数据执行 from_utf8_lossy 导致损坏 + new_headers.remove(hyper::header::ACCEPT_ENCODING); new_headers.insert( hyper::header::AUTHORIZATION, format!("Bearer {}", token).parse().unwrap(), ); - new_headers.insert(x_api_key, token.parse().unwrap()); Ok(ProcessedRequest { target_url, @@ -364,6 +368,142 @@ impl AmpHeadersProcessor { }) } + /// 尝试通过 ampcode.com 远程执行工具(篡改 isFreeTierRequest → true) + /// 成功返回 dc-local:// 响应,失败返回 Err 供调用方降级 + async fn try_remote_tool( + tool_name: &str, + path: &str, + query: Option<&str>, + headers: &HyperHeaderMap, + modified_body: &[u8], + ) -> Result { + let proxy_mgr = crate::services::proxy_config_manager::ProxyConfigManager::new() + .map_err(|e| anyhow!("ProxyConfigManager 初始化失败: {}", e))?; + let config = proxy_mgr + .get_config("amp-code") + .map_err(|e| anyhow!("读取配置失败: {}", e))? + .ok_or_else(|| anyhow!("AMP Code 代理未配置"))?; + let token = config + .real_api_key + .ok_or_else(|| anyhow!("AMP Code Access Token 未配置"))?; + let base_url = config + .real_base_url + .unwrap_or_else(|| "https://ampcode.com".to_string()); + + let target_url = match query { + Some(q) => format!("{}{}?{}", base_url, path, q), + None => format!("{}{}", base_url, path), + }; + + tracing::info!("AMP Code 远程工具 {} → {}", tool_name, target_url); + + // 构建请求 headers(不设 accept-encoding,让 reqwest 自动解压) + let mut req_headers = reqwest::header::HeaderMap::new(); + for (name, value) in headers.iter() { + let skip = name == hyper::header::AUTHORIZATION + || name == hyper::header::ACCEPT_ENCODING + || name == hyper::header::CONTENT_LENGTH + || name == hyper::header::HOST; + if skip { + continue; + } + if let (Ok(key), Ok(val)) = ( + reqwest::header::HeaderName::from_bytes(name.as_str().as_bytes()), + reqwest::header::HeaderValue::from_bytes(value.as_bytes()), + ) { + req_headers.insert(key, val); + } + } + req_headers.insert( + reqwest::header::AUTHORIZATION, + format!("Bearer {}", token).parse().unwrap(), + ); + + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .build() + .map_err(|e| anyhow!("构建 HTTP client 失败: {}", e))?; + + let response = client + .post(&target_url) + .headers(req_headers) + .body(modified_body.to_vec()) + .send() + .await + .map_err(|e| { + tracing::warn!( + "远程工具请求详细错误: {:?}, is_timeout={}, is_connect={}, is_request={}", + e, + e.is_timeout(), + e.is_connect(), + e.is_request() + ); + anyhow!("远程工具请求失败: {}", e) + })?; + + let status = response.status(); + if !status.is_success() { + let body_preview = response + .text() + .await + .unwrap_or_else(|_| "<读取失败>".to_string()); + return Err(anyhow!( + "ampcode.com 返回 {}: {}", + status, + &body_preview[..body_preview.len().min(200)] + )); + } + + let body_bytes = response + .bytes() + .await + .map_err(|e| anyhow!("读取响应体失败: {}", e))?; + tracing::debug!( + "AMP Code 远程工具 {} 响应 {} 字节: {}", + tool_name, + body_bytes.len(), + String::from_utf8_lossy(&body_bytes[..body_bytes.len().min(500)]) + ); + + // 检测业务层错误(HTTP 200 但 ok:false,如 insufficient-credits) + if let Ok(json) = serde_json::from_slice::(&body_bytes) { + if json.get("ok") == Some(&serde_json::Value::Bool(false)) { + let code = json + .pointer("/error/code") + .and_then(|v| v.as_str()) + .unwrap_or("unknown"); + return Err(anyhow!("ampcode.com 业务错误: {}", code)); + } + } + + tracing::debug!( + "AMP Code 远程工具 {} 成功, {} 字节", + tool_name, + body_bytes.len() + ); + + let mut resp_headers = reqwest::header::HeaderMap::new(); + resp_headers.insert("content-type", "application/json".parse().unwrap()); + + Ok(ProcessedRequest { + target_url: format!("dc-local://{}", tool_name), + headers: resp_headers, + body: body_bytes, + }) + } + + /// 检测是否为广告相关请求 + fn is_ad_request(query: Option<&str>) -> bool { + const AD_ENDPOINTS: &[&str] = &[ + "recordAdImpressionEnd", + "recordAdImpressionStart", + "getCurrentAd", + ]; + let Some(q) = query else { return false }; + q.split('&') + .any(|part| AD_ENDPOINTS.contains(&part.split('=').next().unwrap_or(part))) + } + /// 检测是否为本地工具请求(精确匹配,避免误判) fn detect_local_tool(query: Option<&str>) -> Option<&'static str> { let q = query?; @@ -679,7 +819,7 @@ impl AmpHeadersProcessor { // SSRF 防护:使用 URL 解析进行精确校验 Self::validate_url_security(target_url)?; - tracing::info!("本地网页提取: {}", target_url); + tracing::debug!("本地网页提取: {}", target_url); let resp = HTTP_CLIENT .get(target_url) @@ -706,7 +846,7 @@ impl AmpHeadersProcessor { } }); - tracing::info!("本地网页提取完成: {} bytes", html.len()); + tracing::debug!("本地网页提取完成: {} bytes", html.len()); Self::build_local_response("extractWebPageContent", response) } @@ -947,11 +1087,35 @@ impl RequestProcessor for AmpHeadersProcessor { original_headers: &HyperHeaderMap, body: &[u8], ) -> Result { - // 0. 本地工具拦截:webSearch2 / extractWebPageContent + // 0. 工具拦截:webSearch2 / extractWebPageContent + // 篡改 isFreeTierRequest: false → true,转发到 ampcode.com(对齐 PR #622) + // 无 isFreeTierRequest 字段时降级本地处理 if let Some(tool_name) = Self::detect_local_tool(query) { - tracing::info!("AMP Code 本地工具: {}", tool_name); + // 尝试篡改 isFreeTierRequest → true 并远程执行(对齐 PR #622) + let body_str = String::from_utf8_lossy(body); + if IS_FREE_TIER_RE.is_match(&body_str) { + let modified = IS_FREE_TIER_RE + .replace_all(&body_str, r#""isFreeTierRequest":true"#) + .into_owned(); + match Self::try_remote_tool( + tool_name, + path, + query, + original_headers, + modified.as_bytes(), + ) + .await + { + Ok(result) => return Ok(result), + Err(e) => { + tracing::warn!("AMP Code 远程工具 {} 失败: {}, 降级本地处理", tool_name, e); + // fall through 到本地处理 + } + } + } - // 获取 Tavily API Key(如果配置了) + // 无 isFreeTierRequest 字段或远程失败 → 降级本地处理 + tracing::info!("AMP Code 本地工具: {}", tool_name); let tavily_api_key = crate::services::proxy_config_manager::ProxyConfigManager::new() .ok() .and_then(|mgr| mgr.get_config("amp-code").ok().flatten()) @@ -964,6 +1128,11 @@ impl RequestProcessor for AmpHeadersProcessor { tracing::debug!("AMP Code 路由: path={}, type={:?}", path, api_type); if api_type == ApiType::AmpInternal { + // 拦截广告相关请求,返回空成功响应 + if Self::is_ad_request(query) { + tracing::debug!("AMP Code 拦截广告请求: path={}, query={:?}", path, query); + return Self::build_local_response("ad-blocked", json!({ "ok": true })); + } return Self::forward_to_amp(path, query, original_headers, body).await; } diff --git a/src-tauri/src/services/proxy/proxy_instance.rs b/src-tauri/src/services/proxy/proxy_instance.rs index ba4d0cc..9ff6510 100644 --- a/src-tauri/src/services/proxy/proxy_instance.rs +++ b/src-tauri/src/services/proxy/proxy_instance.rs @@ -362,10 +362,10 @@ async fn handle_request_inner( // 本地工具处理:dc-local:// 协议标记的请求直接返回 body if processed.target_url.starts_with("dc-local://") { - tracing::info!( + tracing::debug!( tool_id = %tool_id, - local_tool = %processed.target_url, - "本地工具响应" + source = %processed.target_url, + "直接返回预构建响应" ); let mut response = Response::builder() .status(StatusCode::OK) @@ -552,7 +552,7 @@ async fn handle_request_inner( // 等待流完全消费的信号(无超时,真正等待流结束) match stream_end_rx.await { Ok(_) => { - tracing::info!("✓ 收到 SSE 流完成信号,流已完全消费"); + tracing::debug!("✓ 收到 SSE 流完成信号,流已完全消费"); } Err(_) => { tracing::warn!("✗ 未收到 SSE 流完成信号(sender 被 drop),可能流被提前取消"); @@ -570,7 +570,7 @@ async fn handle_request_inner( } }; - tracing::info!( + tracing::debug!( chunks_count = chunks.len(), "开始处理 SSE chunks 进行 token 统计" ); From a322892ea73b5fd6798b80b9d5fb96c8149ca033 Mon Sep 17 00:00:00 2001 From: ehgen0ng Date: Sat, 7 Feb 2026 23:35:03 +0800 Subject: [PATCH 06/10] =?UTF-8?q?=F0=9F=90=9B=20fix(proxy):=20=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E6=9C=AA=E8=AF=86=E5=88=AB=E8=B7=AF=E5=BE=84=E7=9A=84?= =?UTF-8?q?API=E7=B1=BB=E5=9E=8B=20=20=20-=20=E6=9B=B4=E6=94=B9=E6=9C=AA?= =?UTF-8?q?=E7=9F=A5=E8=B7=AF=E5=BE=84=E7=9A=84=E9=BB=98=E8=AE=A4API?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E4=B8=BA=20AmpInternal=20=20=20-=20=E7=A1=AE?= =?UTF-8?q?=E4=BF=9D=E6=9C=AA=E8=AF=86=E5=88=AB=E7=9A=84=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E6=AD=A3=E7=A1=AE=E8=BD=AC=E5=8F=91=E5=88=B0=20ampcode.com?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ♻️ refactor(proxy): 优化错误日志记录 - 展开 reqwest 错误链,提供更详细的底层错误原因 --- src-tauri/src/services/proxy/headers/amp_processor.rs | 3 ++- src-tauri/src/services/proxy/proxy_instance.rs | 11 ++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/services/proxy/headers/amp_processor.rs b/src-tauri/src/services/proxy/headers/amp_processor.rs index 9f94856..537a6ee 100644 --- a/src-tauri/src/services/proxy/headers/amp_processor.rs +++ b/src-tauri/src/services/proxy/headers/amp_processor.rs @@ -134,7 +134,8 @@ impl AmpHeadersProcessor { return api_type; } - ApiType::Claude + // 无法识别的路径转发到 ampcode.com + ApiType::AmpInternal } fn detect_by_model(body: &[u8]) -> Option { diff --git a/src-tauri/src/services/proxy/proxy_instance.rs b/src-tauri/src/services/proxy/proxy_instance.rs index 9ff6510..9aa638a 100644 --- a/src-tauri/src/services/proxy/proxy_instance.rs +++ b/src-tauri/src/services/proxy/proxy_instance.rs @@ -419,7 +419,16 @@ async fn handle_request_inner( .unwrap_or_else(|| "default".to_string()); let proxy_pricing_template_id_clone = proxy_config.pricing_template_id.clone(); let request_body_clone = processed.body.clone(); - let error_msg = e.to_string(); + // 展开完整错误链(reqwest 的 source chain 包含底层原因如 DNS/TLS/超时等) + let error_msg = { + let mut msg = e.to_string(); + let mut source = std::error::Error::source(&e); + while let Some(cause) = source { + msg.push_str(&format!(" → {}", cause)); + source = std::error::Error::source(cause); + } + msg + }; // 从请求体中判断是否为流式请求 let is_sse = serde_json::from_slice::(&processed.body) From e241f5ed2b9fc01df7de66b27120313da9ad7a2f Mon Sep 17 00:00:00 2001 From: ehgen0ng Date: Sun, 8 Feb 2026 00:04:03 +0800 Subject: [PATCH 07/10] =?UTF-8?q?=F0=9F=90=9B=20fix(proxy):=20=E4=B8=BA=20?= =?UTF-8?q?tool=5Fchoice=20=E5=90=8D=E7=A7=B0=E6=B7=BB=E5=8A=A0=E5=89=8D?= =?UTF-8?q?=E7=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为 tool_choice.name 字段添加 TOOL_PREFIX 前缀 - 确保工具名称与 tools[].name 保持一致 --- .../src/services/proxy/headers/amp_processor.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/services/proxy/headers/amp_processor.rs b/src-tauri/src/services/proxy/headers/amp_processor.rs index 537a6ee..5ee36fc 100644 --- a/src-tauri/src/services/proxy/headers/amp_processor.rs +++ b/src-tauri/src/services/proxy/headers/amp_processor.rs @@ -292,7 +292,21 @@ impl AmpHeadersProcessor { } } - // 2) messages[].content[] 里 type=="tool_use" 的 name 也要加前缀 + 统一所有 content item 的 cache_control + // 2) tool_choice.name 加前缀(与 tools[].name 保持一致) + if let Some(tc) = json.get_mut("tool_choice").and_then(|v| v.as_object_mut()) { + if tc.get("type").and_then(|t| t.as_str()) == Some("tool") { + if let Some(name) = tc.get("name").and_then(|n| n.as_str()) { + if !name.starts_with(TOOL_PREFIX) { + tc.insert( + "name".to_string(), + serde_json::Value::String(format!("{}{}", TOOL_PREFIX, name)), + ); + } + } + } + } + + // 3) messages[].content[] 里 type=="tool_use" 的 name 也要加前缀 + 统一所有 content item 的 cache_control if let Some(messages) = json.get_mut("messages").and_then(|m| m.as_array_mut()) { for msg in messages.iter_mut() { let Some(content) = msg.get_mut("content") else { From f112d26c0140a1bccfde88023af3502688dc22f8 Mon Sep 17 00:00:00 2001 From: ehgen0ng Date: Sun, 8 Feb 2026 01:40:46 +0800 Subject: [PATCH 08/10] =?UTF-8?q?=F0=9F=90=9B=20fix(amp-processor):=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20Claude=20=E4=BB=A3=E7=A0=81=E5=89=8D?= =?UTF-8?q?=E8=A8=80=E5=A4=84=E7=90=86=20-=20=E4=BC=98=E5=8C=96=E4=BA=86?= =?UTF-8?q?=20Claude=20=E4=BB=A3=E7=A0=81=E5=89=8D=E8=A8=80=E7=9A=84?= =?UTF-8?q?=E6=8F=92=E5=85=A5=E9=80=BB=E8=BE=91=20-=20=E7=A1=AE=E4=BF=9D?= =?UTF-8?q?=E5=89=8D=E8=A8=80=E8=A2=AB=E6=B7=BB=E5=8A=A0=E5=88=B0=E7=AC=AC?= =?UTF-8?q?=E4=B8=80=E4=B8=AA=E7=8E=B0=E6=9C=89=E6=96=87=E6=9C=AC=E9=A1=B9?= =?UTF-8?q?=E7=9A=84=E5=BC=80=E5=A4=B4=20-=20=E5=A6=82=E6=9E=9C=E6=B2=A1?= =?UTF-8?q?=E6=9C=89=E6=96=87=E6=9C=AC=E9=A1=B9=EF=BC=8C=E5=88=99=E5=9C=A8?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E5=BC=80=E5=A4=B4=E6=8F=92=E5=85=A5=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=E6=96=B0=E7=9A=84=E6=96=87=E6=9C=AC=E9=A1=B9=20-=20?= =?UTF-8?q?=E9=81=BF=E5=85=8D=E4=BA=86=E5=89=8D=E8=A8=80=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E6=88=96=E9=81=97=E6=BC=8F=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/proxy/headers/amp_processor.rs | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src-tauri/src/services/proxy/headers/amp_processor.rs b/src-tauri/src/services/proxy/headers/amp_processor.rs index 5ee36fc..bfc82c7 100644 --- a/src-tauri/src/services/proxy/headers/amp_processor.rs +++ b/src-tauri/src/services/proxy/headers/amp_processor.rs @@ -250,15 +250,19 @@ impl AmpHeadersProcessor { } } - let already_prefixed = items - .first() - .and_then(|v| v.get("type").and_then(|t| t.as_str())) - == Some("text") - && items - .first() - .and_then(|v| v.get("text").and_then(|t| t.as_str())) - == Some(CLAUDE_CODE_PREAMBLE); - if !already_prefixed { + let first_text = items + .iter_mut() + .find(|item| item.get("type").and_then(|t| t.as_str()) == Some("text")); + if let Some(item) = first_text { + if let Some(text) = item.get("text").and_then(|t| t.as_str()) { + if !text.starts_with(CLAUDE_CODE_PREAMBLE) { + item["text"] = serde_json::Value::String(format!( + "{}{}", + CLAUDE_CODE_PREAMBLE, text + )); + } + } + } else { items.insert(0, json!({ "type": "text", "text": CLAUDE_CODE_PREAMBLE })); } } From 096d14a3eb27c2fc4fc82d4ea1d12576fabc06ee Mon Sep 17 00:00:00 2001 From: ehgen0ng Date: Sun, 8 Feb 2026 10:20:07 +0800 Subject: [PATCH 09/10] =?UTF-8?q?=E2=9C=A8=20feat(cache):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E7=BC=93=E5=AD=98=E6=8E=A7=E5=88=B6=E8=A7=84=E8=8C=83?= =?UTF-8?q?=E5=8C=96=E5=BC=80=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引入 NORMALIZE_CACHE_CONTROL 常量,用于启用或禁用缓存控制规范化 - 提供更灵活的缓存头处理方式 --- src-tauri/src/services/proxy/headers/amp_processor.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/services/proxy/headers/amp_processor.rs b/src-tauri/src/services/proxy/headers/amp_processor.rs index bfc82c7..6f664b9 100644 --- a/src-tauri/src/services/proxy/headers/amp_processor.rs +++ b/src-tauri/src/services/proxy/headers/amp_processor.rs @@ -61,8 +61,13 @@ fn sanitize_brand_text(s: &str) -> String { BRAND_SANITIZE_RE.replace_all(s, "Claude Code").into_owned() } -/// 统一 cache_control 为标准 5m ttl +/// 是否统一 cache_control 为标准 5m ttl +const NORMALIZE_CACHE_CONTROL: bool = false; + fn normalize_cache_control(item: &mut Value) { + if !NORMALIZE_CACHE_CONTROL { + return; + } if let Some(obj) = item.as_object_mut() { if obj.contains_key("cache_control") { obj.insert( From 851314a40fb90853ee7c87e00cd24cc3bd0a5c2e Mon Sep 17 00:00:00 2001 From: ehgen0ng Date: Sun, 8 Feb 2026 13:28:12 +0800 Subject: [PATCH 10/10] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(amp=5Fproce?= =?UTF-8?q?ssor):=20=E4=BC=98=E5=8C=96=E5=B7=A5=E5=85=B7=E5=90=8D=E7=A7=B0?= =?UTF-8?q?=E5=89=8D=E7=BC=80=E5=A4=84=E7=90=86=20-=20=E4=BC=98=E5=8C=96MC?= =?UTF-8?q?P=E5=90=8D=E7=A7=B0=E5=89=8D=E7=BC=80=E6=AD=A3=E5=88=99?= =?UTF-8?q?=E8=A1=A8=E8=BE=BE=E5=BC=8F=EF=BC=8C=E6=9B=B4=E7=B2=BE=E7=A1=AE?= =?UTF-8?q?=E5=9C=B0=E5=8C=B9=E9=85=8D=E5=B7=A5=E5=85=B7=E5=90=8D=E7=A7=B0?= =?UTF-8?q?=20-=20=E5=BC=95=E5=85=A5`=5F`=E4=BD=9C=E4=B8=BA=E6=96=B0?= =?UTF-8?q?=E7=9A=84=E5=B7=A5=E5=85=B7=E5=90=8D=E7=A7=B0=E5=89=8D=E7=BC=80?= =?UTF-8?q?=E6=A0=87=E8=AF=86=E7=AC=A6=20-=20=E6=9B=B4=E6=96=B0=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E5=90=8D=E7=A7=B0=E5=89=8D=E7=BC=80=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E9=81=BF=E5=85=8D=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E6=88=96=E9=94=99=E8=AF=AF=E7=9A=84=E5=89=8D=E7=BC=80=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=20-=20=E6=8F=90=E5=8D=87=E5=B7=A5=E5=85=B7=E5=90=8D?= =?UTF-8?q?=E7=A7=B0=E5=A4=84=E7=90=86=E7=9A=84=E7=81=B5=E6=B4=BB=E6=80=A7?= =?UTF-8?q?=E5=92=8C=E5=81=A5=E5=A3=AE=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/services/proxy/headers/amp_processor.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/services/proxy/headers/amp_processor.rs b/src-tauri/src/services/proxy/headers/amp_processor.rs index 6f664b9..2ba1315 100644 --- a/src-tauri/src/services/proxy/headers/amp_processor.rs +++ b/src-tauri/src/services/proxy/headers/amp_processor.rs @@ -41,7 +41,7 @@ static IS_FREE_TIER_RE: Lazy = Lazy::new(|| { }); static MCP_NAME_PREFIX_RE: Lazy = Lazy::new(|| { - regex::Regex::new(r#""name"\s*:\s*"mcp_([^"]+)""#).expect("mcp name 前缀正则非法") + regex::Regex::new(r#""name"\s*:\s*"mcp_([^_"][^"]*)""#).expect("mcp name 前缀正则非法") }); static BRAND_SANITIZE_RE: Lazy = Lazy::new(|| { @@ -222,6 +222,7 @@ impl AmpHeadersProcessor { fn add_tool_prefix(body: &[u8]) -> Vec { const TOOL_PREFIX: &str = "mcp_"; + const MCP_TOOL_PREFIX: &str = "_"; if body.is_empty() { return body.to_vec(); @@ -293,7 +294,7 @@ impl AmpHeadersProcessor { normalize_cache_control(tool); if let Some(name) = tool.get("name").and_then(|n| n.as_str()) { - if !name.starts_with(TOOL_PREFIX) { + if !name.starts_with(TOOL_PREFIX) && !name.starts_with(MCP_TOOL_PREFIX) { tool["name"] = serde_json::Value::String(format!("{}{}", TOOL_PREFIX, name)); } @@ -305,7 +306,7 @@ impl AmpHeadersProcessor { if let Some(tc) = json.get_mut("tool_choice").and_then(|v| v.as_object_mut()) { if tc.get("type").and_then(|t| t.as_str()) == Some("tool") { if let Some(name) = tc.get("name").and_then(|n| n.as_str()) { - if !name.starts_with(TOOL_PREFIX) { + if !name.starts_with(TOOL_PREFIX) && !name.starts_with(MCP_TOOL_PREFIX) { tc.insert( "name".to_string(), serde_json::Value::String(format!("{}{}", TOOL_PREFIX, name)), @@ -334,7 +335,7 @@ impl AmpHeadersProcessor { } if let Some(name) = item.get("name").and_then(|n| n.as_str()) { - if !name.starts_with(TOOL_PREFIX) { + if !name.starts_with(TOOL_PREFIX) && !name.starts_with(MCP_TOOL_PREFIX) { item["name"] = serde_json::Value::String(format!("{}{}", TOOL_PREFIX, name)); }