chore: 全面规范化代码,遵循 CLAUDE.md 编码规范
- C# 文件命名改为 snake_case(28 个文件重命名) - C# 类转换为主构造函数(8 个类) - 空 catch 添加异常类型(2 处) - 新建 GlobalUsings.cs(Health.Infrastructure、Health.WebApi) - Flutter 移除 go_router,改用 Riverpod 路由栈 - Flutter 移除 flutter_secure_storage,改用 sqflite 持久化 - 修复 Flutter 构建路径(Flutter SDK 迁至 D 盘) - 后端端口改为 0.0.0.0:5000,支持局域网访问
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace Health.Infrastructure.AI;
|
||||
|
||||
/// <summary>
|
||||
/// OpenAI 兼容协议 HTTP 客户端,统一调用 DeepSeek / 千问 VL
|
||||
/// </summary>
|
||||
public sealed class OpenAiCompatibleClient(string baseUrl, string apiKey, string model)
|
||||
{
|
||||
private readonly HttpClient _http = CreateHttpClient(baseUrl, apiKey);
|
||||
private readonly string _model = model;
|
||||
private readonly JsonSerializerOptions _jsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
private static HttpClient CreateHttpClient(string baseUrl, string apiKey)
|
||||
{
|
||||
var client = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/"),
|
||||
Timeout = TimeSpan.FromSeconds(60)
|
||||
};
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
|
||||
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
return client;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 流式 Chat Completions(SSE)
|
||||
/// </summary>
|
||||
public async IAsyncEnumerable<string> ChatStreamAsync(
|
||||
List<ChatMessage> messages,
|
||||
List<ToolDefinition>? tools = null,
|
||||
int maxTokens = 2048,
|
||||
float temperature = 0.7f)
|
||||
{
|
||||
var request = new ChatCompletionRequest
|
||||
{
|
||||
Model = _model,
|
||||
Messages = messages,
|
||||
Stream = true,
|
||||
MaxTokens = maxTokens,
|
||||
Temperature = temperature,
|
||||
Tools = tools,
|
||||
};
|
||||
|
||||
if (tools?.Count > 0)
|
||||
request.ToolChoice = "auto";
|
||||
|
||||
var json = JsonSerializer.Serialize(request, _jsonOptions);
|
||||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
|
||||
var httpRequest = new HttpRequestMessage(HttpMethod.Post, "chat/completions")
|
||||
{
|
||||
Content = content
|
||||
};
|
||||
httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));
|
||||
|
||||
var response = await _http.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
using var stream = await response.Content.ReadAsStreamAsync();
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
string? line;
|
||||
while ((line = await reader.ReadLineAsync()) != null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line)) continue;
|
||||
if (!line.StartsWith("data: ")) continue;
|
||||
|
||||
var data = line["data: ".Length..];
|
||||
if (data == "[DONE]") break;
|
||||
|
||||
yield return data;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 非流式 Chat Completions
|
||||
/// </summary>
|
||||
public async Task<ChatCompletionResponse> ChatAsync(
|
||||
List<ChatMessage> messages,
|
||||
List<ToolDefinition>? tools = null,
|
||||
int maxTokens = 2048,
|
||||
float temperature = 0.7f)
|
||||
{
|
||||
var request = new ChatCompletionRequest
|
||||
{
|
||||
Model = _model,
|
||||
Messages = messages,
|
||||
Stream = false,
|
||||
MaxTokens = maxTokens,
|
||||
Temperature = temperature,
|
||||
Tools = tools,
|
||||
};
|
||||
|
||||
if (tools?.Count > 0)
|
||||
request.ToolChoice = "auto";
|
||||
|
||||
var json = JsonSerializer.Serialize(request, _jsonOptions);
|
||||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
|
||||
var response = await _http.PostAsync("chat/completions", content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
return JsonSerializer.Deserialize<ChatCompletionResponse>(body, _jsonOptions)!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Vision 图片理解(非流式)
|
||||
/// </summary>
|
||||
public async Task<ChatCompletionResponse> VisionAsync(
|
||||
string systemPrompt,
|
||||
List<string> imageUrls,
|
||||
string? userText = null,
|
||||
int maxTokens = 2048)
|
||||
{
|
||||
var messages = new List<ChatMessage>();
|
||||
|
||||
if (!string.IsNullOrEmpty(systemPrompt))
|
||||
messages.Add(new ChatMessage { Role = "system", Content = systemPrompt });
|
||||
|
||||
// 构建多模态消息内容
|
||||
var contentParts = new List<object>();
|
||||
foreach (var url in imageUrls)
|
||||
contentParts.Add(new { type = "image_url", image_url = new { url } });
|
||||
if (!string.IsNullOrEmpty(userText))
|
||||
contentParts.Add(new { type = "text", text = userText });
|
||||
|
||||
var userMessage = new ChatMessage
|
||||
{
|
||||
Role = "user",
|
||||
Content = JsonSerializer.Serialize(contentParts, _jsonOptions)
|
||||
};
|
||||
|
||||
messages.Add(userMessage);
|
||||
|
||||
var request = new ChatCompletionRequest
|
||||
{
|
||||
Model = _model,
|
||||
Messages = messages,
|
||||
MaxTokens = maxTokens,
|
||||
Stream = false,
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(request, _jsonOptions);
|
||||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
var response = await _http.PostAsync("chat/completions", content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
return JsonSerializer.Deserialize<ChatCompletionResponse>(body, _jsonOptions)!;
|
||||
}
|
||||
}
|
||||
|
||||
#region 请求/响应模型
|
||||
|
||||
public sealed class ChatCompletionRequest
|
||||
{
|
||||
public string Model { get; set; } = string.Empty;
|
||||
public List<ChatMessage> Messages { get; set; } = [];
|
||||
public bool Stream { get; set; }
|
||||
public int MaxTokens { get; set; } = 2048;
|
||||
public float Temperature { get; set; } = 0.7f;
|
||||
public List<ToolDefinition>? Tools { get; set; }
|
||||
public string? ToolChoice { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ChatMessage
|
||||
{
|
||||
public string Role { get; set; } = string.Empty;
|
||||
public string Content { get; set; } = string.Empty;
|
||||
public string? ToolCallId { get; set; }
|
||||
public List<ToolCall>? ToolCalls { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ToolDefinition
|
||||
{
|
||||
public string Type { get; set; } = "function";
|
||||
public ToolFunction Function { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class ToolFunction
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public object Parameters { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class ToolCall
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Type { get; set; } = "function";
|
||||
public ToolCallFunction Function { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class ToolCallFunction
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Arguments { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class ChatCompletionResponse
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public List<Choice> Choices { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class Choice
|
||||
{
|
||||
public int Index { get; set; }
|
||||
public ResponseMessage? Message { get; set; }
|
||||
public ResponseDelta? Delta { get; set; }
|
||||
public string? FinishReason { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ResponseMessage
|
||||
{
|
||||
public string Role { get; set; } = string.Empty;
|
||||
public string? Content { get; set; }
|
||||
public List<ToolCall>? ToolCalls { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ResponseDelta
|
||||
{
|
||||
public string? Content { get; set; }
|
||||
public string? Role { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
Reference in New Issue
Block a user