优化: AI Chat全栈多批审查修复与架构清理(risk_level清理/路由解耦/工具渲染/测试补测/死代码)

This commit is contained in:
2026-06-18 22:57:19 +08:00
parent 0ca5d9805f
commit a2871a66e0
87 changed files with 5720 additions and 3012 deletions

View File

@@ -58,10 +58,12 @@ struct AnthropicToolDef {
/// Anthropic 同步响应
#[derive(Debug, Deserialize)]
struct AnthropicResponse {
// SW-260618-25: Anthropic 响应反序列化字段,保留以对齐响应结构(响应 id),标注意图消除 dead_code warning
#[allow(dead_code)]
id: String,
model: String,
content: Vec<AnthropicContentBlock>,
// SW-260618-25: Anthropic 响应反序列化字段,保留以备调试/未来消费(如日志记录调用终止原因),标注意图消除 dead_code warning
#[allow(dead_code)]
stop_reason: Option<String>,
usage: AnthropicUsage,
@@ -246,13 +248,8 @@ impl AnthropicCompatProvider {
/// - `api_key`: API 密钥Anthropic 用 x-api-key 头,非 Bearer
/// - `default_model`: 默认模型名称
pub fn new(base_url: impl Into<String>, api_key: impl Into<String>, default_model: impl Into<String>) -> Self {
let client = Client::builder()
.connect_timeout(std::time::Duration::from_secs(30))
.build()
.unwrap_or_else(|e| {
warn!("reqwest builder 失败,回退默认 client: {}", e);
Client::new()
});
// SW-260618-10: reqwest Client 构建抽 crate::build_provider_client与 OpenAI 共用 DRY
let client = crate::build_provider_client();
Self {
client,
api_key: api_key.into(),
@@ -354,8 +351,9 @@ impl AnthropicCompatProvider {
"text": text,
}),
crate::provider::ContentPart::Image { url, base64, media_type, alt: _ } => {
let mt = media_type.clone().unwrap_or_else(|| "image/png".into());
let data = base64.clone().unwrap_or_else(|| {
// p 已被 match 取得所有权,直接 move media_type/base64 避免大 base64 clone。
let mt = media_type.unwrap_or_else(|| "image/png".into());
let data = base64.unwrap_or_else(|| {
// 完整性兜底:当前 url 模式不可达(前端 Phase2b 只产 base64 图片片)。
// 若未来接入 url 图片输入而 commands 层未补 url→base64 预拉,
// 此处会发空 data 致 Anthropic 400warn 留痕但不阻塞(避免静默吞数据)。
@@ -391,8 +389,11 @@ impl AnthropicCompatProvider {
}
if let Some(calls) = &m.tool_calls {
for tc in calls {
let input: serde_json::Value =
serde_json::from_str(&tc.function.arguments).unwrap_or(serde_json::Value::Null);
// B-260618-25: arguments 非法 JSON(流式中断残留 / ToolCall::new 默认空串)
// → 空 object 兜底。Anthropic/GLM 要求 tool_use.input 必为 object,
// null 直触发 1214「messages 参数非法」。
let input: serde_json::Value = serde_json::from_str(&tc.function.arguments)
.unwrap_or_else(|_| serde_json::json!({}));
content.push(serde_json::json!({
"type": "tool_use",
"id": tc.id,
@@ -415,7 +416,13 @@ impl AnthropicCompatProvider {
.map(|d| AnthropicToolDef {
name: d.function.name,
description: Some(d.function.description).filter(|s| !s.is_empty()),
input_schema: d.function.parameters,
// B-260618-25: input_schema 非 object(未来误用)→ 兜底 {"type":"object"},
// 防 Anthropic 拒非法 tool schema(当前全走 object_schema 恒 object,纯防御)。
input_schema: if d.function.parameters.is_object() {
d.function.parameters
} else {
serde_json::json!({"type": "object"})
},
})
.collect()
});
@@ -478,10 +485,22 @@ impl LlmProvider for AnthropicCompatProvider {
.header("anthropic-version", ANTHROPIC_VERSION)
.header("Content-Type", "application/json")
.timeout(Duration::from_secs(60))
.version(reqwest::Version::HTTP_11)
.json(&body);
let resp = match rb.send().await {
Ok(r) => r,
Err(e) => {
// B-260618-26: 记 reqwest 错误源因链(is_*/source)。原仅 Display
// "error sending request for url" 无法定位 reset/TLS/超时/body 真因。
tracing::error!(
is_timeout = e.is_timeout(),
is_connect = e.is_connect(),
is_body = e.is_body(),
is_request = e.is_request(),
url = ?e.url(),
source = ?std::error::Error::source(&e),
"Anthropic 同步 send 失败"
);
if is_reqwest_error_retryable(&e) {
return AttemptOutcome::Retryable(format!("请求失败(可重试): {}", e));
}
@@ -558,11 +577,29 @@ impl LlmProvider for AnthropicCompatProvider {
debug!(model = %body.model, "Anthropic 流式调用");
let resp = self
let resp = match self
.auth_headers(self.client.post(self.messages_url()))
.json(&body)
.version(reqwest::Version::HTTP_11)
.send()
.await?;
.await
{
Ok(r) => r,
Err(e) => {
// B-260618-26: 记 reqwest 错误源因链。原 ? 转 anyhow 仅 Display
// "error sending request for url" 无法定位 reset/TLS/超时/body 真因。
tracing::error!(
is_timeout = e.is_timeout(),
is_connect = e.is_connect(),
is_body = e.is_body(),
is_request = e.is_request(),
url = %self.messages_url(),
source = ?std::error::Error::source(&e),
"Anthropic 流式 send 失败"
);
anyhow::bail!("Anthropic 流式 send 失败: {}", e);
}
};
if !resp.status().is_success() {
let status = resp.status();