闽公网安备 35020302035485号
public interface IChatGPTKeyService
{
//初始话密钥
public Task InitAsync();
//堆代码 duidaima.com
//随机获取密钥KEY
public Task<string> GetRandomAsync();
//获取所有密钥
Task<string[]> GetAllAsync();
//移除密钥
Task RemoveAsync(string apiKey);
}
InitAsync方法用以初始化密钥,GetRandomAsync方法用于随机读取一个密钥,GetAllAsync方法用于读取所有密钥,RemoveAsync方法用于删除指定密钥。PM> Install-Package StackExchange.Redis右键点击Extensions文件夹,新建一个ChatGPTKeyService.cs文件,并在文件中写入以下代码:
using StackExchange.Redis;
public class ChatGPTKeyService : IChatGPTKeyService
{
private ConnectionMultiplexer? _connection;
private IDatabase? _cache;
private readonly string _configuration;
private const string _redisKey = "ChatGPTKey";
public ChatGPTKeyService(string configuration)
{
_configuration = configuration;
}
private async Task ConnectAsync()
{
if (_cache != null) return;
_connection = await ConnectionMultiplexer.ConnectAsync(_configuration);
_cache = _connection.GetDatabase();
}
public async Task InitAsync()
{
await ConnectAsync();
//使用Set对象存储密钥
await _cache!.SetAddAsync(_redisKey, new RedisValue[] {
"sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx1",
"sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx2",
});
}
public async Task<string> GetRandomAsync()
{
await ConnectAsync();
//使用Set随机返回一个密钥
var redisValue = await _cache!.SetRandomMemberAsync(_redisKey);
return redisValue.ToString();
}
public async Task<string[]> GetAllAsync()
{
await ConnectAsync();
//读取所有密钥
var redisValues = await _cache!.SetMembersAsync(_redisKey);
return redisValues.Select(m => m.ToString()).ToArray();
}
public async Task RemoveAsync(string apiKey)
{
await ConnectAsync();
await _cache!.SetRemoveAsync(_redisKey, apiKey);
}
}
为了保存KEY,我们选择使用Redis的Set数据结构,它可以存储不重复的元素,并且可以随机返回一个元素。这样,我们就可以实现密钥的随机轮换功能。ConnectAsync方法是用来建立和Redis数据库的连接。using ChatGPT.Demo4.Extensions;
//注册IChatGPTKeyService单例服务
builder.Services.AddSingleton<IChatGPTKeyService>(
new ChatGPTKeyService("localhost"));
var app = builder.Build();
//初始化redis数据库
var _chatGPTKeyService = app.Services.GetRequiredService<IChatGPTKeyService>();
_chatGPTKeyService.InitAsync().Wait();
Betalgo.OpenAI提供了两种使用方式,一种是依赖注入,一种是非依赖注入。之前我们采用的是依赖注入方式,大家会发现,依赖注入并不支持多KEY的设置,为此,我们先来看看如何使用非依赖注入的方式实现。//Betalgo.OpenAI地址 https://github.com/betalgo/openai

using ChatGPT.Demo4.Extensions;
//private readonly IOpenAIService _openAiService;
private readonly IChatGPTKeyService _chatGPTKeyService;
public ChatController(/*IOpenAIService openAiService,*/ IChatGPTKeyService chatGPTKeyService)
{
//_openAiService = openAiService;
_chatGPTKeyService = chatGPTKeyService;
}
string apiKey = await _chatGPTKeyService.GetRandomAsync();
IOpenAIService _openAiService = new OpenAIService(new OpenAiOptions
{
ApiKey = apiKey
});

public class ChatGPTHttpMessageHandler : DelegatingHandler
{
private readonly IChatGPTKeyService _chatGPTKeyService;
public ChatGPTHttpMessageHandler(IChatGPTKeyService chatGPTKeyService)
{
_chatGPTKeyService = chatGPTKeyService;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var apiKey = await _chatGPTKeyService.GetRandomAsync();
request.Headers.Remove("Authorization");
request.Headers.Add("Authorization", $"Bearer {apiKey}");
return await base.SendAsync(request, cancellationToken);
}
protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
{
var apiKey = _chatGPTKeyService.GetRandomAsync().Result;
request.Headers.Remove("Authorization");
request.Headers.Add("Authorization", $"Bearer {apiKey}");
return base.Send(request, cancellationToken);
}
}
在ChatGPTHttpMessageHandler中,我们通过依赖注入的方式获取IChatGPTKeyService密钥服务的实例,然后重写了Send方法,调用IChatGPTKeyService的GetRandomAsync方法随机获取一个KEY,接着使用HttpHeaders的Remove方法移除默认的KEY,再使用HttpHeaders的Add方法添加获取的KEY,最后我们调用base.SendAsync方法将请求传递给内部处理程序进行后续的处理。这样我们就完成了KEY的切换。builder.Services.AddTransient<ChatGPTHttpMessageHandler>(); builder.Services.AddHttpClient<IOpenAIService, OpenAIService>().AddHttpMessageHandler<ChatGPTHttpMessageHandler>();3、重新注册IOpenAIService服务


动态删除无效KEY
当ChatGPT账号使用达到额度上限时,KEY将会失效,为此,我们需要及时删除无效的KEY,避免影响请求的正常发送。但比较遗憾,OpenAI官方并没有提供直接的API来查询额度,那么,我们怎么知道KEY是否还有效呢?//账单查询API https://api.openai.com/v1/dashboard/billing/subscription另一个是账单明细查询,用来查询已使用的额度和具体的请求记录,它也是一个GET请求,在Header中同样需要携带授权Token(API KEY),另外还可以通过参数指定要查询的日期范围。
//账单明细: https://api.openai.com/v1/v1/dashboard/billing/usage?start_date=2023-07-01&end_date=2023-07-02
public interface IChatGPTBillService
{
/// <summary>
/// 查询账单
/// </summary>
/// <param name="apiKey">api密钥</param>
/// <returns></returns>
Task<ChatGPTBillModel?> QueryAsync(string apiKey);
/// <summary>
/// 账单明细
/// </summary>
/// <param name="apiKey">api密钥</param>
/// <param name="startTime">开始日期</param>
/// <param name="endTime">结束日期</param>
/// <returns></returns>
Task<ChatGPTBillDetailsModel?> QueryDetailsAsync(string apiKey, DateTimeOffset startTime, DateTimeOffset endTime);
}
ChatGPTBillService服务是IChatGPTBillService接口的实现,代码如下所示:public class ChatGPTBillService : IChatGPTBillService
{
private readonly IHttpClientFactory _httpClientFactory;
public ChatGPTBillService(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task<ChatGPTBillModel?> QueryAsync(string apiKey)
{
string url = "https://api.openai.com/v1/dashboard/billing/subscription";
var client = _httpClientFactory.CreateClient();
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
var response = await client.GetFromJsonAsync<ChatGPTBillModel>(url);
return response;
}
public async Task<ChatGPTBillDetailsModel?> QueryDetailsAsync(string apiKey, DateTimeOffset startTime, DateTimeOffset endTime)
{
string url = $"https://api.openai.com/dashboard/billing/usage?start_date={startTime:yyyy-MM-dd}&end_date={endTime:yyyy-MM-dd}";
var client = _httpClientFactory.CreateClient();
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
var response = await client.GetFromJsonAsync<ChatGPTBillDetailsModel>(url);
return response;
}
}
ChatGPTBillService通过使用IHttpClientFactory工厂创建HttpClient来发送请求,并在请求头中添加ChatGPT的授权Token,即API KEY,从而实现对ChatGPT的账单和明细的查询功能。考虑到篇幅长度,这里不再给出账单类ChatGPTBillModel和账单明细类ChatGPTBillDetailsModel的具体定义。public class ChatGPTBillBackgroundService : BackgroundService
{
private readonly IChatGPTKeyService _chatGPTKeyService;
private readonly IChatGPTBillService _chatGPTBillService;
public ChatGPTBillBackgroundService(IChatGPTKeyService chatGPTKeyService, IChatGPTBillService chatGPTBillService)
{
_chatGPTKeyService = chatGPTKeyService;
_chatGPTBillService = chatGPTBillService;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var apiKeys = await _chatGPTKeyService.GetAllAsync();
foreach (var apiKey in apiKeys)
{
var bill = await _chatGPTBillService.QueryAsync(apiKey);
if (bill == null) continue;
var dt = DateTimeOffset.Now;
//判断key是否到期或是否有额度
if (bill.AccessUntil < dt.ToUnixTimeSeconds() || bill.HardLimitUsd == 0)
{
await _chatGPTKeyService.RemoveAsync(apiKey);
continue;
}
//查询99天以内的账单明细
var billDetails = await _chatGPTBillService.QueryDetailsAsync(
apiKey, dt.AddDays(-99), dt.AddDays(1));
if (billDetails == null) continue;
//判断已使用额度大于等于总额度
if (billDetails.TotalUsage >= bill.HardLimitUsd)
{
await _chatGPTKeyService.RemoveAsync(apiKey);
continue;
}
}
// 堆代码 duidaima.com
// 创建一个异步的任务,该任务在指定1分钟间隔后完成
await Task.Delay(1 * 60 * 1000, stoppingToken);
}
}
}
ChatGPTBillBackgroundService类继承自BackgroundService,并通过构造函数注入了IChatGPTKeyService密钥服务和IChatGPTBillService账单服务,然后重写了ExecuteAsync方法,通过使用while循环和Task.Delay方法间接实现每分钟执行一次的定时任务,任务的逻辑是:从缓存中获取所有密钥,然后对每个密钥进行以下操作://注册账单服务 builder.Services.AddSingleton<IChatGPTBillService, ChatGPTBillService>(); //注册后台任务 builder.Services.AddHostedService<ChatGPTBillBackgroundService>();n
