添加智能分类功能,支持获取未分类账单数量和列表;实现AI分类逻辑;更新相关API和前端视图
This commit is contained in:
@@ -197,8 +197,12 @@ public class EmailBackgroundService(
|
||||
message.TextBody ?? message.HtmlBody ?? string.Empty
|
||||
))
|
||||
{
|
||||
#if DEBUG
|
||||
logger.LogDebug("DEBUG 模式下,跳过标记已读步骤");
|
||||
#else
|
||||
// 标记邮件为已读
|
||||
await emailFetchService.MarkAsReadAsync(uid);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace Service;
|
||||
public interface IOpenAiService
|
||||
{
|
||||
Task<string?> ChatAsync(string systemPrompt, string userPrompt);
|
||||
IAsyncEnumerable<string> ChatStreamAsync(string systemPrompt, string userPrompt);
|
||||
}
|
||||
|
||||
public class OpenAiService(
|
||||
@@ -68,4 +69,79 @@ public class OpenAiService(
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<string> ChatStreamAsync(string systemPrompt, string userPrompt)
|
||||
{
|
||||
var cfg = aiSettings.Value;
|
||||
if (string.IsNullOrWhiteSpace(cfg.Endpoint) ||
|
||||
string.IsNullOrWhiteSpace(cfg.Key) ||
|
||||
string.IsNullOrWhiteSpace(cfg.Model))
|
||||
{
|
||||
logger.LogWarning("未配置 OpenAI/DeepSeek 接口,无法调用 AI");
|
||||
yield break;
|
||||
}
|
||||
|
||||
using var http = new HttpClient();
|
||||
http.Timeout = TimeSpan.FromMinutes(5);
|
||||
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", cfg.Key);
|
||||
|
||||
var payload = new
|
||||
{
|
||||
model = cfg.Model,
|
||||
temperature = 0,
|
||||
stream = true,
|
||||
messages = new object[]
|
||||
{
|
||||
new { role = "system", content = systemPrompt },
|
||||
new { role = "user", content = userPrompt }
|
||||
}
|
||||
};
|
||||
|
||||
var url = cfg.Endpoint.TrimEnd('/') + "/chat/completions";
|
||||
var json = JsonSerializer.Serialize(payload);
|
||||
using var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
|
||||
// 使用 SendAsync 来支持 HttpCompletionOption
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, url)
|
||||
{
|
||||
Content = content
|
||||
};
|
||||
using var resp = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
||||
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
var err = await resp.Content.ReadAsStringAsync();
|
||||
throw new InvalidOperationException($"AI接口调用失败: {(int)resp.StatusCode} {resp.ReasonPhrase}, {err}");
|
||||
}
|
||||
|
||||
using var stream = await resp.Content.ReadAsStreamAsync();
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
string? line;
|
||||
while ((line = await reader.ReadLineAsync()) != null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line) || !line.StartsWith("data: "))
|
||||
continue;
|
||||
|
||||
var data = line.Substring(6).Trim();
|
||||
if (data == "[DONE]")
|
||||
break;
|
||||
|
||||
// 解析JSON时不使用try-catch,因为在async iterator中不能使用
|
||||
using var doc = JsonDocument.Parse(data);
|
||||
var root = doc.RootElement;
|
||||
if (root.TryGetProperty("choices", out var choices) && choices.GetArrayLength() > 0)
|
||||
{
|
||||
var delta = choices[0].GetProperty("delta");
|
||||
if (delta.TryGetProperty("content", out var contentProp))
|
||||
{
|
||||
var contentText = contentProp.GetString();
|
||||
if (!string.IsNullOrEmpty(contentText))
|
||||
{
|
||||
yield return contentText;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user