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:
136
backend/src/Health.Infrastructure/AI/ai_clients.cs
Normal file
136
backend/src/Health.Infrastructure/AI/ai_clients.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace Health.Infrastructure.AI;
|
||||
|
||||
/// <summary>
|
||||
/// DeepSeek LLM 客户端(对话 + Tool Calling)
|
||||
/// </summary>
|
||||
public sealed class DeepSeekClient(HttpClient http, IConfiguration config)
|
||||
{
|
||||
private readonly HttpClient _http = http;
|
||||
private readonly string _model = config["DEEPSEEK_MODEL"] ?? "deepseek-chat";
|
||||
private readonly JsonSerializerOptions _jsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 流式 Chat Completions
|
||||
/// </summary>
|
||||
public async IAsyncEnumerable<string> ChatStreamAsync(
|
||||
List<ChatMessage> messages,
|
||||
List<ToolDefinition>? tools = null,
|
||||
int maxTokens = 2048,
|
||||
float temperature = 0.7f,
|
||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
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"));
|
||||
|
||||
using var response = await _http.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
using var stream = await response.Content.ReadAsStreamAsync(ct);
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
string? line;
|
||||
while ((line = await reader.ReadLineAsync(ct)) != 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(用于 Tool Calling)
|
||||
/// </summary>
|
||||
public async Task<ChatCompletionResponse> ChatAsync(
|
||||
List<ChatMessage> messages,
|
||||
List<ToolDefinition>? tools = null,
|
||||
int maxTokens = 2048,
|
||||
float temperature = 0.7f,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
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, ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var body = await response.Content.ReadAsStringAsync(ct);
|
||||
return JsonSerializer.Deserialize<ChatCompletionResponse>(body, _jsonOptions)!;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 千问 VL 视觉客户端(食物识别 + 报告解读)
|
||||
/// </summary>
|
||||
public sealed class QwenVisionClient(HttpClient http, IConfiguration config)
|
||||
{
|
||||
private readonly HttpClient _http = http;
|
||||
private readonly string _model = config["QWEN_VISION_MODEL"] ?? "qwen-vl-max";
|
||||
private readonly JsonSerializerOptions _jsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
public async Task<ChatCompletionResponse> VisionAsync(
|
||||
string systemPrompt,
|
||||
List<string> imageUrls,
|
||||
string? userText = null,
|
||||
int maxTokens = 2048,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
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, ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var body = await response.Content.ReadAsStringAsync(ct);
|
||||
return JsonSerializer.Deserialize<ChatCompletionResponse>(body, _jsonOptions)!;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user