Files
glz bdaa6a0dc8 refactor: 统一业务异常处理,标准化结果码和错误响应
1.  新增并完善ResultCode枚举,补充标准HTTP状态码对应的业务状态码
2.  重构BusinessException,新增基于ResultCode的构造函数和ThrowIf扩展方法
3.  替换所有硬编码的HTTP状态码为统一的ResultCode枚举
4.  优化全局异常中间件,根据业务状态码映射对应HTTP状态码并规范化JSON响应
5.  修复OssImageHelper和AutoDotCodeConsumer中的OSS文件处理逻辑
6.  新增用户答题快照实体类
7.  清理废弃的宠物模块迁移脚本
2026-06-29 16:34:26 +08:00

174 lines
7.6 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Dto;
using System.Net.Http.Headers;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace QYZH.InteractiveMagazine.Service;
/// <summary>
/// AI聊天服务实现OpenAI API规范流式返回
/// </summary>
public class AiChatService(
IHttpClientFactory httpClientFactory,
IConfiguration configuration,
ILogger<AiChatService> logger) : IAiChatService
{
/// <summary>
/// 预设系统提示词
/// </summary>
private const string SystemPrompt = """
# AI "题目评分/报告提示词"
**"题目评分/报告提示词"便 AI **
****
---
##
1. ****
2. **** ````````
3. ****"要写得好一点""必须包含三层结构:解释原因 + 表达情绪 + 提出方案"
4. ****"仅""必须""至少"
5. **** Markdown 使`#` `##` `###``-`/
---
##
1. ****
- /
-
- //
-
-
2. ****
- `## ` +
- `## `
- `## `
- `## `/
- `## `
3. ****
- "可能""大概""必须""至少"
- 1. 2. 3.
-
4. ****
---
##
- AI 使/ prompt
- 使 Markdown `#`
- "示例"
---
**** Markdown
""";
/// <summary>
/// 流式AI聊天逐块返回AI生成的内容
/// </summary>
public async IAsyncEnumerable<string> ChatAsync(
AiChatInput input,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(input.Message))
{
throw new BusinessException("消息内容不能为空", ResultCode.BAD_REQUEST);
}
var apiKey = configuration["AiChat:ApiKey"];
var baseUrl = configuration["AiChat:BaseUrl"];
var model = configuration["AiChat:Model"];
var timeoutSeconds = configuration.GetValue<int>("AiChat:TimeoutSeconds");
var maxTokens = configuration.GetValue<int>("AiChat:MaxTokens");
var temperature = configuration.GetValue<double>("AiChat:Temperature");
if (string.IsNullOrWhiteSpace(apiKey) || string.IsNullOrWhiteSpace(baseUrl) || string.IsNullOrWhiteSpace(model))
{
throw new BusinessException("AI聊天服务配置不完整请检查 AiChat 配置节", ResultCode.GLOBAL_ERROR);
}
logger.LogInformation("开始调用AI聊天服务流式消息长度{Length}", input.Message.Length);
// 构建 OpenAI Chat Completions 请求体(启用流式返回)
var requestBody = new
{
model = model,
messages = new[]
{
new { role = "system", content = SystemPrompt },
new { role = "user", content = input.Message }
},
max_tokens = maxTokens,
temperature = temperature,
stream = true
};
var jsonContent = JsonSerializer.Serialize(requestBody, new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
});
// 创建 HttpClient 并发起请求(使用 ResponseHeadersRead 提前获取响应头以流式读取)
var client = httpClientFactory.CreateClient();
client.Timeout = TimeSpan.FromSeconds(timeoutSeconds > 0 ? timeoutSeconds : 300);
var request = new HttpRequestMessage(HttpMethod.Post, $"{baseUrl.TrimEnd('/')}/chat/completions");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
request.Content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken);
logger.LogError("AI聊天服务调用失败状态码{StatusCode},响应:{Response}", response.StatusCode, errorContent);
throw new BusinessException($"AI服务调用失败{response.StatusCode}", ResultCode.GLOBAL_ERROR);
}
// 流式读取响应体
using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken);
using var reader = new StreamReader(responseStream);
while (!reader.EndOfStream)
{
cancellationToken.ThrowIfCancellationRequested();
var line = await reader.ReadLineAsync(cancellationToken);
if (line == null) break;
// 只处理 data: 开头的行
if (!line.StartsWith("data: ")) continue;
var data = line.Substring(6);
if (data == "[DONE]") break;
// 解析 SSE chunk提取 delta.content
using var chunkDoc = JsonDocument.Parse(data);
var choicesEl = chunkDoc.RootElement.GetProperty("choices");
if (choicesEl.GetArrayLength() == 0) continue;
var delta = choicesEl[0].GetProperty("delta");
if (delta.TryGetProperty("content", out var contentEl))
{
var content = contentEl.GetString();
if (!string.IsNullOrEmpty(content))
{
yield return content;
}
}
}
logger.LogInformation("AI聊天服务流式调用完成");
}
}