Files
EmailBill/Service/OpenAiService.cs
孙诚 ef4ed9fd57
Some checks failed
Docker Build & Deploy / Build Docker Image (push) Failing after 6s
Docker Build & Deploy / Deploy to Production (push) Has been skipped
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
feat: Implement scheduled tasks management and budget archiving functionality
- Added BudgetArchiveJob for monthly budget archiving.
- Created BudgetArchive entity and BudgetArchiveRepository for managing archived budgets.
- Introduced JobController for handling job execution, pausing, and resuming.
- Developed ScheduledTasksView for displaying and managing scheduled tasks in the frontend.
- Updated PeriodicBillJob to improve scope handling.
- Enhanced OpenAiService with increased HTTP timeout.
- Added archiveBudgets API endpoint for archiving budgets by year and month.
- Refactored BudgetController to utilize new repository patterns and improved error handling.
- Introduced rich-content styles for better rendering of HTML content in Vue components.
- Updated various Vue components to support rich HTML content display.
2026-01-09 14:03:01 +08:00

278 lines
9.5 KiB
C#
Raw 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 System.Net.Http.Headers;
namespace Service;
public interface IOpenAiService
{
Task<string?> ChatAsync(string systemPrompt, string userPrompt);
Task<string?> ChatAsync(string prompt);
IAsyncEnumerable<string> ChatStreamAsync(string systemPrompt, string userPrompt);
IAsyncEnumerable<string> ChatStreamAsync(string prompt);
}
public class OpenAiService(
IOptions<AISettings> aiSettings,
ILogger<OpenAiService> logger
) : IOpenAiService
{
public async Task<string?> ChatAsync(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");
return null;
}
using var http = new HttpClient();
http.Timeout = TimeSpan.FromSeconds(15);
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", cfg.Key);
var payload = new
{
model = cfg.Model,
temperature = 0,
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");
try
{
using var resp = await http.PostAsync(url, content);
if (!resp.IsSuccessStatusCode)
{
var err = await resp.Content.ReadAsStringAsync();
throw new InvalidOperationException($"AI接口调用失败: {(int)resp.StatusCode} {resp.ReasonPhrase}, {err}");
}
var respText = await resp.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(respText);
var root = doc.RootElement;
var contentText = root.GetProperty("choices")[0]
.GetProperty("message")
.GetProperty("content")
.GetString();
return contentText;
}
catch (Exception ex)
{
logger.LogError(ex, "AI 调用失败");
throw;
}
}
public async Task<string?> ChatAsync(string prompt)
{
var cfg = aiSettings.Value;
if (string.IsNullOrWhiteSpace(cfg.Endpoint) ||
string.IsNullOrWhiteSpace(cfg.Key) ||
string.IsNullOrWhiteSpace(cfg.Model))
{
logger.LogWarning("未配置 OpenAI/DeepSeek 接口,无法调用 AI");
return null;
}
using var http = new HttpClient();
http.Timeout = TimeSpan.FromSeconds(60 * 5);
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", cfg.Key);
var payload = new
{
model = cfg.Model,
temperature = 0,
messages = new object[]
{
new { role = "user", content = prompt }
}
};
var url = cfg.Endpoint.TrimEnd('/') + "/chat/completions";
var json = JsonSerializer.Serialize(payload);
using var content = new StringContent(json, Encoding.UTF8, "application/json");
try
{
using var resp = await http.PostAsync(url, content);
if (!resp.IsSuccessStatusCode)
{
var err = await resp.Content.ReadAsStringAsync();
throw new InvalidOperationException($"AI接口调用失败: {(int)resp.StatusCode} {resp.ReasonPhrase}, {err}");
}
var respText = await resp.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(respText);
var root = doc.RootElement;
var contentText = root.GetProperty("choices")[0]
.GetProperty("message")
.GetProperty("content")
.GetString();
return contentText;
}
catch (Exception ex)
{
logger.LogError(ex, "AI 调用失败");
throw;
}
}
public async IAsyncEnumerable<string> ChatStreamAsync(string prompt)
{
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 = "user", content = prompt }
}
};
var url = cfg.Endpoint.TrimEnd('/') + "/chat/completions";
var json = JsonSerializer.Serialize(payload);
using var content = new StringContent(json, Encoding.UTF8, "application/json");
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;
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;
}
}
}
}
}
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;
}
}
}
}
}
}