refactor: 清理 AI 客户端死代码并统一错误处理

- 删除未使用的 OpenAiCompatibleClient,DTO 抽离到 ai_contracts.cs
- 新增 AiHttpResponseGuard 统一 LLM/VLM 的 HTTP 错误处理:
  错误响应带响应体(截断 2000 字符)、空响应与无效 JSON 显式抛 InvalidDataException
- 空 catch 补全精确异常类型(IOException/UnauthorizedAccessException/JsonException)
- 修正 Program.cs 数据库初始化注释
- 新增 AI 客户端错误处理单元测试
This commit is contained in:
MingNian
2026-06-20 22:06:31 +08:00
parent 4d213b5a44
commit aa44d6f0f0
9 changed files with 182 additions and 256 deletions

View File

@@ -40,7 +40,7 @@ public sealed class DeepSeekClient(HttpClient http, IConfiguration config)
httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));
using var response = await _http.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, ct);
response.EnsureSuccessStatusCode();
await AiHttpResponseGuard.ThrowIfFailedAsync(response, "DeepSeek", ct);
using var stream = await response.Content.ReadAsStreamAsync(ct);
using var reader = new StreamReader(stream);
@@ -76,11 +76,9 @@ public sealed class DeepSeekClient(HttpClient http, IConfiguration config)
var json = JsonSerializer.Serialize(request, _jsonOptions);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _http.PostAsync("chat/completions", content, ct);
response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync(ct);
return JsonSerializer.Deserialize<ChatCompletionResponse>(body, _jsonOptions)!;
using var response = await _http.PostAsync("chat/completions", content, ct);
var body = await AiHttpResponseGuard.ReadSuccessBodyAsync(response, "DeepSeek", ct);
return AiHttpResponseGuard.DeserializeRequired<ChatCompletionResponse>(body, _jsonOptions, "DeepSeek");
}
}
@@ -125,13 +123,45 @@ public sealed class VisionClient(HttpClient http, IConfiguration config)
var json = JsonSerializer.Serialize(request, _jsonOptions);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _http.PostAsync("chat/completions", content, ct);
if (!response.IsSuccessStatusCode)
{
var errorBody = await response.Content.ReadAsStringAsync(ct);
throw new HttpRequestException($"VLM API {response.StatusCode}: {errorBody}");
}
var body = await response.Content.ReadAsStringAsync(ct);
return JsonSerializer.Deserialize<ChatCompletionResponse>(body, _jsonOptions)!;
using var response = await _http.PostAsync("chat/completions", content, ct);
var body = await AiHttpResponseGuard.ReadSuccessBodyAsync(response, "VLM", ct);
return AiHttpResponseGuard.DeserializeRequired<ChatCompletionResponse>(body, _jsonOptions, "VLM");
}
}
internal static class AiHttpResponseGuard
{
private const int MaxErrorBodyLength = 2000;
public static async Task ThrowIfFailedAsync(HttpResponseMessage response, string provider, CancellationToken ct)
{
if (response.IsSuccessStatusCode) return;
var body = await response.Content.ReadAsStringAsync(ct);
throw new HttpRequestException($"{provider} API {(int)response.StatusCode} ({response.StatusCode}): {Truncate(body)}");
}
public static async Task<string> ReadSuccessBodyAsync(HttpResponseMessage response, string provider, CancellationToken ct)
{
await ThrowIfFailedAsync(response, provider, ct);
var body = await response.Content.ReadAsStringAsync(ct);
if (string.IsNullOrWhiteSpace(body))
throw new InvalidDataException($"{provider} API返回了空响应");
return body;
}
public static T DeserializeRequired<T>(string body, JsonSerializerOptions options, string provider)
{
try
{
return JsonSerializer.Deserialize<T>(body, options)
?? throw new InvalidDataException($"{provider} API响应内容为空");
}
catch (JsonException ex)
{
throw new InvalidDataException($"{provider} API响应格式无效", ex);
}
}
private static string Truncate(string value) =>
value.Length <= MaxErrorBodyLength ? value : value[..MaxErrorBodyLength] + "...";
}