mirror of
https://gitee.com/fudiwei/DotNetCore.SKIT.FlurlHttpClient.Wechat.git
synced 2025-09-20 02:29:40 +08:00
feat(openai): 新增 OpenAI v2 版接口客户端,并实现加解密及签名中间件
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# SKIT.FlurlHttpClient.Wechat.OpenAI
|
||||
# SKIT.FlurlHttpClient.Wechat.OpenAI
|
||||
|
||||
基于 `Flurl.Http` 的[微信对话开放平台](https://chatbot.weixin.qq.com/) HTTP API SDK。
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
## 功能
|
||||
|
||||
- 基于微信对话开放平台 API 封装。
|
||||
- 提供了微信对话开放平台所需的 AES、SHA-1 等算法工具类。
|
||||
- 针对 v2 版接口,请求时自动生成签名,无需开发者手动干预。
|
||||
- 提供了微信对话开放平台所需的 AES、MD5、SHA-1 等算法工具类。
|
||||
- 提供了解析回调通知事件等扩展方法。
|
||||
|
||||
---
|
||||
|
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
@@ -299,12 +300,12 @@ namespace SKIT.FlurlHttpClient.Wechat.Api.Interceptors
|
||||
|
||||
if (context.FlurlCall.HttpRequestMessage.Method != HttpMethod.Post)
|
||||
return;
|
||||
if (context.FlurlCall.HttpRequestMessage.RequestUri is null)
|
||||
return;
|
||||
if (!IsRequestUrlPathMatched(context.FlurlCall.HttpRequestMessage.RequestUri))
|
||||
if (context.FlurlCall.HttpRequestMessage.RequestUri is null || !IsRequestUrlPathMatched(context.FlurlCall.HttpRequestMessage.RequestUri))
|
||||
return;
|
||||
if (context.FlurlCall.HttpResponseMessage is null)
|
||||
return;
|
||||
if (context.FlurlCall.HttpResponseMessage.StatusCode != HttpStatusCode.OK)
|
||||
return;
|
||||
|
||||
string urlpath = GetRequestUrlPath(context.FlurlCall.HttpRequestMessage.RequestUri);
|
||||
byte[] respBytes = Array.Empty<byte>();
|
||||
|
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Flurl.Http;
|
||||
|
||||
namespace SKIT.FlurlHttpClient.Wechat.OpenAI
|
||||
{
|
||||
public static class WechatOpenAIClientExecuteTokenExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// <para>异步调用 [POST] /v2/token 接口。</para>
|
||||
/// <para>
|
||||
/// REF: <br/>
|
||||
/// <![CDATA[ https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/token.html ]]>
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="client"></param>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public static async Task<Models.TokenV2Response> ExecuteTokenV2Async(this WechatOpenAIClient client, Models.TokenV2Request request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (client is null) throw new ArgumentNullException(nameof(client));
|
||||
if (request is null) throw new ArgumentNullException(nameof(request));
|
||||
|
||||
IFlurlRequest flurlReq = client
|
||||
.CreateFlurlRequest(request, HttpMethod.Post, "v2", "token");
|
||||
|
||||
return await client.SendFlurlRequestAsJsonAsync<Models.TokenV2Response>(flurlReq, data: request, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,158 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Flurl.Http;
|
||||
|
||||
namespace SKIT.FlurlHttpClient.Wechat.OpenAI.Interceptors
|
||||
{
|
||||
using SKIT.FlurlHttpClient.Internal;
|
||||
|
||||
internal class WechatOpenAIRequestEncryptionInterceptor : HttpInterceptor
|
||||
{
|
||||
/**
|
||||
* REF:
|
||||
* https://developers.weixin.qq.com/doc/aispeech/confapi/dialog/token.html
|
||||
*/
|
||||
private static readonly ISet<string> ENCRYPT_REQUIRED_URLS = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"/v2/bot/query"
|
||||
};
|
||||
|
||||
private readonly string _baseUrl;
|
||||
private readonly string _encodingAESKey;
|
||||
private readonly Func<string, bool>? _customEncryptedRequestPathMatcher;
|
||||
|
||||
public WechatOpenAIRequestEncryptionInterceptor(string baseUrl, string encodingAESKey, Func<string, bool>? customEncryptedRequestPathMatcher)
|
||||
{
|
||||
_baseUrl = baseUrl;
|
||||
_encodingAESKey = encodingAESKey;
|
||||
_customEncryptedRequestPathMatcher = customEncryptedRequestPathMatcher;
|
||||
|
||||
// AES 密钥的长度不是 4 的倍数需要补齐,确保其始终为有效的 Base64 字符串
|
||||
const int MULTI = 4;
|
||||
int tLen = _encodingAESKey.Length;
|
||||
int tRem = tLen % MULTI;
|
||||
if (tRem > 0)
|
||||
{
|
||||
_encodingAESKey = _encodingAESKey.PadRight(tLen - tRem + MULTI, '=');
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task BeforeCallAsync(HttpInterceptorContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (context is null) throw new ArgumentNullException(nameof(context));
|
||||
if (context.FlurlCall.Completed) throw new WechatOpenAIException("Failed to encrypt request. This interceptor must be called before request completed.");
|
||||
|
||||
if (context.FlurlCall.HttpRequestMessage.RequestUri is null || !IsRequestUrlPathMatched(context.FlurlCall.HttpRequestMessage.RequestUri))
|
||||
return;
|
||||
|
||||
byte[] reqBytes = Array.Empty<byte>();
|
||||
if (context.FlurlCall.HttpRequestMessage?.Content is not null)
|
||||
{
|
||||
if (context.FlurlCall.HttpRequestMessage.Content is not MultipartFormDataContent)
|
||||
{
|
||||
reqBytes = await
|
||||
#if NET5_0_OR_GREATER
|
||||
context.FlurlCall.HttpRequestMessage.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
|
||||
#else
|
||||
_AsyncEx.RunTaskWithCancellationTokenAsync(context.FlurlCall.HttpRequestMessage.Content.ReadAsByteArrayAsync(), cancellationToken).ConfigureAwait(false);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
byte[] reqBytesEncrypted = Array.Empty<byte>();
|
||||
try
|
||||
{
|
||||
const int AES_BLOCK_SIZE = 16;
|
||||
byte[] keyBytes = Convert.FromBase64String(_encodingAESKey);
|
||||
byte[] ivBytes = new byte[AES_BLOCK_SIZE]; // iv 是 key 的前 16 个字节
|
||||
Buffer.BlockCopy(keyBytes, 0, ivBytes, 0, ivBytes.Length);
|
||||
|
||||
reqBytesEncrypted = Utilities.AESUtility.EncryptWithCBC(
|
||||
keyBytes: keyBytes,
|
||||
ivBytes: ivBytes,
|
||||
plainBytes: reqBytes
|
||||
)!;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new WechatOpenAIException("Failed to encrypt request. Please see the inner exception for more details.", ex);
|
||||
}
|
||||
|
||||
context.FlurlCall.HttpRequestMessage!.Content?.Dispose();
|
||||
context.FlurlCall.HttpRequestMessage!.Content = new ByteArrayContent(reqBytesEncrypted);
|
||||
context.FlurlCall.Request.WithHeader(HttpHeaders.ContentType, MimeTypes.Text);
|
||||
}
|
||||
|
||||
public override async Task AfterCallAsync(HttpInterceptorContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (context is null) throw new ArgumentNullException(nameof(context));
|
||||
if (!context.FlurlCall.Completed) throw new WechatOpenAIException("Failed to decrypt response. This interceptor must be called after request completed.");
|
||||
|
||||
if (context.FlurlCall.HttpRequestMessage.RequestUri is null || !IsRequestUrlPathMatched(context.FlurlCall.HttpRequestMessage.RequestUri))
|
||||
return;
|
||||
if (context.FlurlCall.HttpResponseMessage is null)
|
||||
return;
|
||||
if (context.FlurlCall.HttpResponseMessage.StatusCode != HttpStatusCode.OK)
|
||||
return;
|
||||
|
||||
byte[] respBytes = Array.Empty<byte>();
|
||||
if (context.FlurlCall.HttpResponseMessage.Content is not null)
|
||||
{
|
||||
HttpContent httpContent = context.FlurlCall.HttpResponseMessage.Content;
|
||||
respBytes = await
|
||||
#if NET5_0_OR_GREATER
|
||||
httpContent.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
|
||||
#else
|
||||
_AsyncEx.RunTaskWithCancellationTokenAsync(httpContent.ReadAsByteArrayAsync(), cancellationToken).ConfigureAwait(false);
|
||||
#endif
|
||||
}
|
||||
|
||||
byte[] respBytesDecrypted;
|
||||
try
|
||||
{
|
||||
const int AES_BLOCK_SIZE = 16;
|
||||
byte[] keyBytes = Convert.FromBase64String(_encodingAESKey);
|
||||
byte[] ivBytes = new byte[AES_BLOCK_SIZE]; // iv 是 key 的前 16 个字节
|
||||
Buffer.BlockCopy(keyBytes, 0, ivBytes, 0, ivBytes.Length);
|
||||
|
||||
respBytesDecrypted = Utilities.AESUtility.DecryptWithCBC(
|
||||
keyBytes: keyBytes,
|
||||
ivBytes: ivBytes,
|
||||
cipherBytes: respBytes
|
||||
)!;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new WechatOpenAIException("Failed to decrypt response. Please see the inner exception for more details.", ex);
|
||||
}
|
||||
|
||||
context.FlurlCall.HttpResponseMessage!.Content?.Dispose();
|
||||
context.FlurlCall.HttpResponseMessage!.Content = new ByteArrayContent(respBytesDecrypted);
|
||||
}
|
||||
|
||||
private string GetRequestUrlPath(Uri uri)
|
||||
{
|
||||
return uri.AbsoluteUri.Substring(0, uri.AbsoluteUri.Length - uri.Query.Length);
|
||||
}
|
||||
|
||||
private bool IsRequestUrlPathMatched(Uri uri)
|
||||
{
|
||||
string absoluteUrl = GetRequestUrlPath(uri);
|
||||
if (!absoluteUrl.StartsWith(_baseUrl))
|
||||
return false;
|
||||
|
||||
string relativeUrl = absoluteUrl.Substring(_baseUrl.TrimEnd('/').Length);
|
||||
if (!ENCRYPT_REQUIRED_URLS.Contains(relativeUrl))
|
||||
{
|
||||
if (_customEncryptedRequestPathMatcher is not null)
|
||||
return _customEncryptedRequestPathMatcher(relativeUrl);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Flurl.Http;
|
||||
|
||||
namespace SKIT.FlurlHttpClient.Wechat.OpenAI.Interceptors
|
||||
{
|
||||
using SKIT.FlurlHttpClient.Internal;
|
||||
|
||||
internal class WechatOpenAIRequestSigningInterceptor : HttpInterceptor
|
||||
{
|
||||
private readonly string _token;
|
||||
|
||||
public WechatOpenAIRequestSigningInterceptor(string token)
|
||||
{
|
||||
_token = token;
|
||||
}
|
||||
|
||||
public override async Task BeforeCallAsync(HttpInterceptorContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (context is null) throw new ArgumentNullException(nameof(context));
|
||||
if (context.FlurlCall.Completed) throw new WechatOpenAIException("Failed to sign request. This interceptor must be called before request completed.");
|
||||
|
||||
if (context.FlurlCall.HttpRequestMessage.RequestUri is null)
|
||||
return;
|
||||
|
||||
string timestamp = DateTimeOffset.Now.ToLocalTime().ToUnixTimeSeconds().ToString();
|
||||
string nonce = Guid.NewGuid().ToString("N");
|
||||
string body = string.Empty;
|
||||
if (context.FlurlCall.HttpRequestMessage?.Content is not null)
|
||||
{
|
||||
if (context.FlurlCall.HttpRequestMessage.Content is MultipartFormDataContent)
|
||||
{
|
||||
body = string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
body = await
|
||||
#if NET5_0_OR_GREATER
|
||||
context.FlurlCall.HttpRequestMessage.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
#else
|
||||
_AsyncEx.RunTaskWithCancellationTokenAsync(context.FlurlCall.HttpRequestMessage.Content.ReadAsStringAsync(), cancellationToken).ConfigureAwait(false);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
string signData = $"{_token}{timestamp}{nonce}{Utilities.MD5Utility.Hash(body).Value!.ToLower()}";
|
||||
string sign = Utilities.MD5Utility.Hash(signData).Value!.ToLower();
|
||||
|
||||
context.FlurlCall.Request.WithHeader("timestamp", timestamp);
|
||||
context.FlurlCall.Request.WithHeader("nonce", nonce);
|
||||
context.FlurlCall.Request.WithHeader("sign", sign);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
namespace SKIT.FlurlHttpClient.Wechat.OpenAI.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// <para>表示 [POST] /v2/token 接口的请求。</para>
|
||||
/// </summary>
|
||||
public class TokenV2Request : WechatOpenAIRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置操作数据的管理员 ID。
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonProperty("account")]
|
||||
[System.Text.Json.Serialization.JsonPropertyName("account")]
|
||||
public string Account { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
namespace SKIT.FlurlHttpClient.Wechat.OpenAI.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// <para>表示 [POST] /v2/token 接口的响应。</para>
|
||||
/// </summary>
|
||||
public class TokenV2Response : WechatOpenAIResponse<TokenV2Response.Types.Data>
|
||||
{
|
||||
public static class Types
|
||||
{
|
||||
public class Data
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置接口访问令牌。
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonProperty("access_token")]
|
||||
[System.Text.Json.Serialization.JsonPropertyName("access_token")]
|
||||
public string AccessToken { get; set; } = default!;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -9,7 +9,8 @@
|
||||
### 【功能特性】
|
||||
|
||||
- 基于微信对话开放平台 API 封装。
|
||||
- 提供了微信对话开放平台所需的 AES、SHA-1 等算法工具类。
|
||||
- 针对 v2 版接口,请求时自动生成签名,无需开发者手动干预。
|
||||
- 提供了微信对话开放平台所需的 AES、MD5、SHA-1 等算法工具类。
|
||||
- 提供了解析回调通知事件等扩展方法。
|
||||
|
||||
---
|
||||
|
@@ -5,20 +5,29 @@ namespace SKIT.FlurlHttpClient.Wechat.OpenAI.Settings
|
||||
public sealed class Credentials
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化客户端时 <see cref="WechatChatbotClientOptions.AppId"/> 的副本。
|
||||
/// 初始化客户端时 <see cref="WechatOpenAIClientOptions.AppId"/> / <see cref="WechatChatbotClientOptions.AppId"/> 的副本。
|
||||
/// </summary>
|
||||
public string AppId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 初始化客户端时 <see cref="WechatChatbotClientOptions.Token"/> 的副本。
|
||||
/// 初始化客户端时 <see cref="WechatOpenAIClientOptions.Token"/> / <see cref="WechatChatbotClientOptions.Token"/> 的副本。
|
||||
/// </summary>
|
||||
public string Token { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 初始化客户端时 <see cref="WechatChatbotClientOptions.EncodingAESKey"/> 的副本。
|
||||
/// 初始化客户端时 <see cref="WechatOpenAIClientOptions.EncodingAESKey"/> / <see cref="WechatChatbotClientOptions.EncodingAESKey"/> 的副本。
|
||||
/// </summary>
|
||||
public string EncodingAESKey { get; }
|
||||
|
||||
internal Credentials(WechatOpenAIClientOptions options)
|
||||
{
|
||||
if (options is null) throw new ArgumentNullException(nameof(options));
|
||||
|
||||
AppId = options.AppId;
|
||||
Token = options.Token;
|
||||
EncodingAESKey = options.EncodingAESKey;
|
||||
}
|
||||
|
||||
internal Credentials(WechatChatbotClientOptions options)
|
||||
{
|
||||
if (options is null) throw new ArgumentNullException(nameof(options));
|
||||
|
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace SKIT.FlurlHttpClient.Wechat.OpenAI.Utilities
|
||||
{
|
||||
using SKIT.FlurlHttpClient.Primitives;
|
||||
|
||||
/// <summary>
|
||||
/// MD5 算法工具类。
|
||||
/// </summary>
|
||||
public static class MD5Utility
|
||||
{
|
||||
/// <summary>
|
||||
/// 计算 MD5 哈希值。
|
||||
/// </summary>
|
||||
/// <param name="messageBytes">要计算哈希值的信息字节数组。</param>
|
||||
/// <returns>哈希值字节数组。</returns>
|
||||
public static byte[] Hash(byte[] messageBytes)
|
||||
{
|
||||
if (messageBytes is null) throw new ArgumentNullException(nameof(messageBytes));
|
||||
|
||||
#if NET5_0_OR_GREATER
|
||||
return MD5.HashData(messageBytes);
|
||||
#else
|
||||
using MD5 md5 = MD5.Create();
|
||||
return md5.ComputeHash(messageBytes);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算 MD5 哈希值。
|
||||
/// </summary>
|
||||
/// <param name="messageData">要计算哈希值的信息。</param>
|
||||
/// <returns>经过十六进制编码的哈希值。</returns>
|
||||
public static EncodedString Hash(string messageData)
|
||||
{
|
||||
if (messageData is null) throw new ArgumentNullException(nameof(messageData));
|
||||
|
||||
byte[] messageBytes = EncodedString.FromLiteralString(messageData);
|
||||
byte[] hashBytes = Hash(messageBytes);
|
||||
return EncodedString.ToHexString(hashBytes);
|
||||
}
|
||||
}
|
||||
}
|
116
src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIClient.cs
Normal file
116
src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIClient.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Flurl.Http;
|
||||
|
||||
namespace SKIT.FlurlHttpClient.Wechat.OpenAI
|
||||
{
|
||||
/// <summary>
|
||||
/// 一个微信智能对话 API HTTP 客户端。
|
||||
/// </summary>
|
||||
public class WechatOpenAIClient : CommonClientBase, ICommonClient
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前客户端使用的微信智能对话平台凭证。
|
||||
/// </summary>
|
||||
public Settings.Credentials Credentials { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 用指定的配置项初始化 <see cref="WechatOpenAIClient"/> 类的新实例。
|
||||
/// </summary>
|
||||
/// <param name="options">配置项。</param>
|
||||
public WechatOpenAIClient(WechatOpenAIClientOptions options)
|
||||
: this(options, null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="options"></param>
|
||||
/// <param name="httpClient"></param>
|
||||
/// <param name="disposeClient"></param>
|
||||
internal protected WechatOpenAIClient(WechatOpenAIClientOptions options, HttpClient? httpClient, bool disposeClient = true)
|
||||
: base(httpClient, disposeClient)
|
||||
{
|
||||
if (options is null) throw new ArgumentNullException(nameof(options));
|
||||
|
||||
Credentials = new Settings.Credentials(options);
|
||||
|
||||
FlurlClient.BaseUrl = options.Endpoint ?? WechatOpenAIEndpoints.DEFAULT;
|
||||
FlurlClient.WithTimeout(options.Timeout <= 0 ? Timeout.InfiniteTimeSpan : TimeSpan.FromMilliseconds(options.Timeout));
|
||||
|
||||
Interceptors.Add(new Interceptors.WechatOpenAIRequestEncryptionInterceptor(
|
||||
baseUrl: FlurlClient.BaseUrl,
|
||||
encodingAESKey: options.EncodingAESKey,
|
||||
customEncryptedRequestPathMatcher: options.CustomEncryptedRequestPathMatcher
|
||||
));
|
||||
Interceptors.Add(new Interceptors.WechatOpenAIRequestSigningInterceptor(
|
||||
token: options.Token
|
||||
));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用当前客户端生成一个新的 <see cref="IFlurlRequest"/> 对象。
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="httpMethod"></param>
|
||||
/// <param name="urlSegments"></param>
|
||||
/// <returns></returns>
|
||||
public IFlurlRequest CreateFlurlRequest(WechatOpenAIRequest request, HttpMethod httpMethod, params object[] urlSegments)
|
||||
{
|
||||
IFlurlRequest flurlRequest = base.CreateFlurlRequest(request, httpMethod, urlSegments);
|
||||
|
||||
if (request.RequestId is null)
|
||||
{
|
||||
request.RequestId = Guid.NewGuid().ToString("N");
|
||||
}
|
||||
|
||||
return flurlRequest
|
||||
.WithHeader("X-APPID", Credentials.AppId)
|
||||
.WithHeader("X-OPENAI-TOKEN", request.AccessToken)
|
||||
.WithHeader("request_id", request.RequestId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步发起请求。
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="flurlRequest"></param>
|
||||
/// <param name="httpContent"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<T> SendFlurlRequestAsync<T>(IFlurlRequest flurlRequest, HttpContent? httpContent = null, CancellationToken cancellationToken = default)
|
||||
where T : WechatOpenAIResponse, new()
|
||||
{
|
||||
if (flurlRequest is null) throw new ArgumentNullException(nameof(flurlRequest));
|
||||
|
||||
using IFlurlResponse flurlResponse = await base.SendFlurlRequestAsync(flurlRequest, httpContent, cancellationToken).ConfigureAwait(false);
|
||||
return await WrapFlurlResponseAsJsonAsync<T>(flurlResponse, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步发起请求。
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="flurlRequest"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<T> SendFlurlRequestAsJsonAsync<T>(IFlurlRequest flurlRequest, object? data = null, CancellationToken cancellationToken = default)
|
||||
where T : WechatOpenAIResponse, new()
|
||||
{
|
||||
if (flurlRequest is null) throw new ArgumentNullException(nameof(flurlRequest));
|
||||
|
||||
bool isSimpleRequest = data is null ||
|
||||
flurlRequest.Verb == HttpMethod.Get ||
|
||||
flurlRequest.Verb == HttpMethod.Head ||
|
||||
flurlRequest.Verb == HttpMethod.Options;
|
||||
using IFlurlResponse flurlResponse = isSimpleRequest ?
|
||||
await base.SendFlurlRequestAsync(flurlRequest, null, cancellationToken).ConfigureAwait(false) :
|
||||
await base.SendFlurlRequestAsJsonAsync(flurlRequest, data, cancellationToken).ConfigureAwait(false);
|
||||
return await WrapFlurlResponseAsJsonAsync<T>(flurlResponse, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace SKIT.FlurlHttpClient.Wechat.OpenAI
|
||||
{
|
||||
/// <summary>
|
||||
/// 用于构造 <see cref="WechatOpenAIClient"/> 实例的构造器。
|
||||
/// </summary>
|
||||
public partial class WechatOpenAIClientBuilder : ICommonClientBuilder<WechatOpenAIClient>
|
||||
{
|
||||
private readonly WechatOpenAIClientOptions _options;
|
||||
private readonly IList<Action<CommonClientSettings>> _configures;
|
||||
private readonly IList<HttpInterceptor> _interceptors;
|
||||
private HttpClient? _httpClient;
|
||||
private bool? _disposeClient;
|
||||
|
||||
private WechatOpenAIClientBuilder(WechatOpenAIClientOptions options)
|
||||
{
|
||||
_options = options;
|
||||
_configures = new List<Action<CommonClientSettings>>();
|
||||
_interceptors = new List<HttpInterceptor>();
|
||||
}
|
||||
|
||||
ICommonClientBuilder<WechatOpenAIClient> ICommonClientBuilder<WechatOpenAIClient>.ConfigureSettings(Action<CommonClientSettings> configure)
|
||||
{
|
||||
return ConfigureSettings(configure);
|
||||
}
|
||||
|
||||
ICommonClientBuilder<WechatOpenAIClient> ICommonClientBuilder<WechatOpenAIClient>.UseInterceptor(HttpInterceptor interceptor)
|
||||
{
|
||||
return UseInterceptor(interceptor);
|
||||
}
|
||||
|
||||
ICommonClientBuilder<WechatOpenAIClient> ICommonClientBuilder<WechatOpenAIClient>.UseHttpClient(HttpClient httpClient, bool disposeClient)
|
||||
{
|
||||
return UseHttpClient(httpClient, disposeClient);
|
||||
}
|
||||
|
||||
public WechatOpenAIClientBuilder ConfigureSettings(Action<CommonClientSettings> configure)
|
||||
{
|
||||
if (configure is null) throw new ArgumentNullException(nameof(configure));
|
||||
|
||||
_configures.Add(configure);
|
||||
return this;
|
||||
}
|
||||
|
||||
public WechatOpenAIClientBuilder UseInterceptor(HttpInterceptor interceptor)
|
||||
{
|
||||
if (interceptor is null) throw new ArgumentNullException(nameof(interceptor));
|
||||
|
||||
_interceptors.Add(interceptor);
|
||||
return this;
|
||||
}
|
||||
|
||||
public WechatOpenAIClientBuilder UseHttpClient(HttpClient httpClient, bool disposeClient = true)
|
||||
{
|
||||
if (httpClient is null) throw new ArgumentNullException(nameof(httpClient));
|
||||
|
||||
_httpClient = httpClient;
|
||||
_disposeClient = disposeClient;
|
||||
return this;
|
||||
}
|
||||
|
||||
public WechatOpenAIClient Build()
|
||||
{
|
||||
WechatOpenAIClient client = _disposeClient.HasValue
|
||||
? new WechatOpenAIClient(_options, _httpClient, _disposeClient.Value)
|
||||
: new WechatOpenAIClient(_options, _httpClient);
|
||||
|
||||
foreach (Action<CommonClientSettings> configure in _configures)
|
||||
{
|
||||
client.Configure(configure);
|
||||
}
|
||||
|
||||
foreach (HttpInterceptor interceptor in _interceptors)
|
||||
{
|
||||
client.Interceptors.Add(interceptor);
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
}
|
||||
|
||||
partial class WechatOpenAIClientBuilder
|
||||
{
|
||||
public static WechatOpenAIClientBuilder Create(WechatOpenAIClientOptions options)
|
||||
{
|
||||
if (options is null) throw new ArgumentNullException(nameof(options));
|
||||
|
||||
return new WechatOpenAIClientBuilder(options);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
|
||||
namespace SKIT.FlurlHttpClient.Wechat.OpenAI
|
||||
{
|
||||
/// <summary>
|
||||
/// 一个用于构造 <see cref="WechatOpenAIClient"/> 时使用的配置项。
|
||||
/// </summary>
|
||||
public class WechatOpenAIClientOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置请求超时时间(单位:毫秒)。
|
||||
/// <para>默认值:30000</para>
|
||||
/// </summary>
|
||||
public int Timeout { get; set; } = 30 * 1000;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置微信智能对话 API 入口点。
|
||||
/// <para>默认值:<see cref="WechatOpenAIEndpoints.DEFAULT"/></para>
|
||||
/// </summary>
|
||||
public string Endpoint { get; set; } = WechatOpenAIEndpoints.DEFAULT;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置微信智能对话 AppId。
|
||||
/// </summary>
|
||||
public string AppId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置微信智能对话 Token。
|
||||
/// </summary>
|
||||
public string Token { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置微信智能对话 EncodingAESKey。
|
||||
/// </summary>
|
||||
public string EncodingAESKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置自定义需加密请求路径匹配器。
|
||||
/// </summary>
|
||||
public Func<string, bool>? CustomEncryptedRequestPathMatcher { get; set; }
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
namespace SKIT.FlurlHttpClient.Wechat.OpenAI
|
||||
{
|
||||
/// <summary>
|
||||
/// <para>微信智能对话 API 接口域名。</para>
|
||||
/// </summary>
|
||||
public static class WechatOpenAIEndpoints
|
||||
{
|
||||
/// <summary>
|
||||
/// 主域名(默认)。
|
||||
/// </summary>
|
||||
public const string DEFAULT = "https://openaiapi.weixin.qq.com";
|
||||
}
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
|
||||
namespace SKIT.FlurlHttpClient.Wechat.OpenAI
|
||||
{
|
||||
/// <summary>
|
||||
/// 当调用微信智能对话 API 出错时引发的异常。
|
||||
/// </summary>
|
||||
public class WechatOpenAIException : CommonException
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public WechatOpenAIException()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public WechatOpenAIException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public WechatOpenAIException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
namespace SKIT.FlurlHttpClient.Wechat.OpenAI
|
||||
{
|
||||
/// <summary>
|
||||
/// 表示微信智能对话 API 请求的基类。
|
||||
/// </summary>
|
||||
public abstract class WechatOpenAIRequest : CommonRequestBase, ICommonRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置微信智能对话 API 接口访问令牌。
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonIgnore]
|
||||
[System.Text.Json.Serialization.JsonIgnore]
|
||||
public virtual string? AccessToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置微信智能对话 API 请求唯一标识。如果不指定将由系统自动生成。
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonIgnore]
|
||||
[System.Text.Json.Serialization.JsonIgnore]
|
||||
public virtual string? RequestId { get; set; }
|
||||
}
|
||||
}
|
@@ -0,0 +1,55 @@
|
||||
namespace SKIT.FlurlHttpClient.Wechat.OpenAI
|
||||
{
|
||||
/// <summary>
|
||||
/// 表示微信智能对话 API 响应的基类。
|
||||
/// </summary>
|
||||
public abstract class WechatOpenAIResponse : CommonResponseBase, ICommonResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取微信智能对话 API 返回的返回码。
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonProperty("code")]
|
||||
[System.Text.Json.Serialization.JsonPropertyName("code")]
|
||||
public virtual int? Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取微信智能对话 API 返回的返回信息。
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonProperty("msg")]
|
||||
[System.Text.Json.Serialization.JsonPropertyName("msg")]
|
||||
public virtual string? Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取微信智能对话 API 返回的请求唯一标识。
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonProperty("request_id")]
|
||||
[System.Text.Json.Serialization.JsonPropertyName("request_id")]
|
||||
public virtual string? RequestId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取一个值,该值指示调用微信 API 是否成功。
|
||||
/// <para>
|
||||
/// (即 HTTP 状态码为 200,且 <see cref="Code"/> 值都为 0)
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override bool IsSuccessful()
|
||||
{
|
||||
return GetRawStatus() == 200 && Code.GetValueOrDefault() == 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示微信智能对话 API 响应的泛型基类。
|
||||
/// </summary>
|
||||
public abstract class WechatOpenAIResponse<TData> : WechatOpenAIResponse
|
||||
where TData : class
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取微信智能对话 API 返回的数据。
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonProperty("data")]
|
||||
[System.Text.Json.Serialization.JsonPropertyName("data")]
|
||||
public virtual TData? Data { get; set; }
|
||||
}
|
||||
}
|
@@ -15,6 +15,26 @@ namespace SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests
|
||||
// 如果 Visual Studio 遇到 “缺少 SKIT.FlurlHttpClient.Tools.CodeAnalyzer 包” 的错误,
|
||||
// 请参考此 Issue:https://github.com/fudiwei/DotNetCore.SKIT.FlurlHttpClient/issues/8
|
||||
|
||||
Assert.Null(Record.Exception(() =>
|
||||
{
|
||||
var options = new TypeDeclarationAnalyzerOptions()
|
||||
{
|
||||
SdkAssembly = Assembly.GetAssembly(typeof(WechatOpenAIClient))!,
|
||||
SdkRequestModelDeclarationNamespace = "SKIT.FlurlHttpClient.Wechat.OpenAI.Models",
|
||||
SdkResponseModelDeclarationNamespace = "SKIT.FlurlHttpClient.Wechat.OpenAI.Models",
|
||||
SdkExecutingExtensionDeclarationNamespace = "SKIT.FlurlHttpClient.Wechat.OpenAI",
|
||||
SdkWebhookEventDeclarationNamespace = "SKIT.FlurlHttpClient.Wechat.OpenAI.Events",
|
||||
IgnoreRequestModelTypes = (type) => !typeof(WechatOpenAIRequest).IsAssignableFrom(type),
|
||||
IgnoreResponseModelTypes = (type) => !typeof(WechatOpenAIResponse).IsAssignableFrom(type),
|
||||
IgnoreExecutingExtensionTypes = (type) => !type.Name.StartsWith(nameof(WechatOpenAIClient)),
|
||||
IgnoreWebhookEventTypes = (_) => true,
|
||||
ThrowOnNotFoundRequestModelTypes = true,
|
||||
ThrowOnNotFoundResponseModelTypes = true,
|
||||
ThrowOnNotFoundExecutingExtensionTypes = true
|
||||
};
|
||||
new TypeDeclarationAnalyzer(options).AssertNoIssues();
|
||||
}));
|
||||
|
||||
Assert.Null(Record.Exception(() =>
|
||||
{
|
||||
var options = new TypeDeclarationAnalyzerOptions()
|
||||
@@ -36,6 +56,35 @@ namespace SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests
|
||||
new TypeDeclarationAnalyzer(options).AssertNoIssues();
|
||||
}));
|
||||
|
||||
Assert.Null(Record.Exception(() =>
|
||||
{
|
||||
string workdir = Environment.CurrentDirectory;
|
||||
string projdir = Path.Combine(workdir, "../../../../../");
|
||||
|
||||
var options = new SourceFileAnalyzerOptions()
|
||||
{
|
||||
SdkAssembly = Assembly.GetAssembly(typeof(WechatOpenAIClient))!,
|
||||
SdkRequestModelDeclarationNamespace = "SKIT.FlurlHttpClient.Wechat.OpenAI.Models",
|
||||
SdkResponseModelDeclarationNamespace = "SKIT.FlurlHttpClient.Wechat.OpenAI.Models",
|
||||
SdkWebhookEventDeclarationNamespace = "SKIT.FlurlHttpClient.Wechat.OpenAI.Events",
|
||||
ProjectSourceRootDirectory = Path.Combine(projdir, "./src/SKIT.FlurlHttpClient.Wechat.OpenAI/"),
|
||||
ProjectTestRootDirectory = Path.Combine(projdir, "./test/SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests/"),
|
||||
ProjectSourceRequestModelClassCodeSubDirectory = "Models/OpenAI/",
|
||||
ProjectSourceResponseModelClassCodeSubDirectory = "Models/OpenAI/",
|
||||
ProjectSourceWebhookEventClassCodeSubDirectory = "Events/OpenAI/",
|
||||
ProjectTestRequestModelSerializationSampleSubDirectory = "ModelSamples/OpenAI/",
|
||||
ProjectTestResponseModelSerializationSampleSubDirectory = "ModelSamples/OpenAI/",
|
||||
ProjectTestWebhookEventSerializationSampleSubDirectory = "EventSamples/OpenAI/",
|
||||
IgnoreExecutingExtensionClassCodeFiles = (file) => !file.Name.StartsWith(nameof(WechatOpenAIClient)),
|
||||
ThrowOnNotFoundRequestModelClassCodeFiles = true,
|
||||
ThrowOnNotFoundResponseModelClassCodeFiles = true,
|
||||
ThrowOnNotFoundExecutingExtensionClassCodeFiles = true,
|
||||
ThrowOnNotFoundRequestModelSerializationSampleFiles = true,
|
||||
ThrowOnNotFoundResponseModelSerializationSampleFiles = true,
|
||||
};
|
||||
new SourceFileAnalyzer(options).AssertNoIssues();
|
||||
}));
|
||||
|
||||
Assert.Null(Record.Exception(() =>
|
||||
{
|
||||
string workdir = Environment.CurrentDirectory;
|
||||
@@ -55,6 +104,7 @@ namespace SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests
|
||||
ProjectTestRequestModelSerializationSampleSubDirectory = "ModelSamples/Chatbot/",
|
||||
ProjectTestResponseModelSerializationSampleSubDirectory = "ModelSamples/Chatbot/",
|
||||
ProjectTestWebhookEventSerializationSampleSubDirectory = "EventSamples/Chatbot/",
|
||||
IgnoreExecutingExtensionClassCodeFiles = (file) => !file.Name.StartsWith(nameof(WechatChatbotClient)),
|
||||
ThrowOnNotFoundRequestModelClassCodeFiles = true,
|
||||
ThrowOnNotFoundResponseModelClassCodeFiles = true,
|
||||
ThrowOnNotFoundExecutingExtensionClassCodeFiles = true,
|
||||
|
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"account": "fb2ab07ce06"
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"access_token": "MX6ddM5mN07ucVKy+Y-to7tKRufZ1YF05eb542d5170000001c"
|
||||
},
|
||||
"msg": "success",
|
||||
"request_id": "54ae04cf-5e95-44fd-ad3f-62e7b163836b"
|
||||
}
|
@@ -9,7 +9,7 @@ namespace SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests
|
||||
public async Task TestExecuteSign()
|
||||
{
|
||||
var request = new Models.SignRequest() { UserId = "TEST_USERID" };
|
||||
var response = await TestClients.Instance.ExecuteSignAsync(request);
|
||||
var response = await TestClients.ChatbotInstance.ExecuteSignAsync(request);
|
||||
|
||||
Assert.NotNull(response.Signature);
|
||||
}
|
||||
|
@@ -0,0 +1,17 @@
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests
|
||||
{
|
||||
public class TestCase_ApiExecuteTokenTests
|
||||
{
|
||||
[Fact(DisplayName = "测试用例:调用 API [POST] /v2/token")]
|
||||
public async Task TestExecuteTokenV2()
|
||||
{
|
||||
var request = new Models.TokenV2Request() { Account = "TEST_ACCOUNTID" };
|
||||
var response = await TestClients.OpenAIInstance.ExecuteTokenV2Async(request);
|
||||
|
||||
Assert.NotNull(response.Data?.AccessToken);
|
||||
}
|
||||
}
|
||||
}
|
@@ -10,7 +10,7 @@ namespace SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests
|
||||
public void TestAESCBCEncryption()
|
||||
{
|
||||
string key = "q1Os1ZMe0nG28KUEx9lg3HjK7V5QyXvi212fzsgDqgz=";
|
||||
string iv = "q1Os1ZMe0nG28KUEx9lg3A=="; // iv 是 key 的前 16 个字节
|
||||
string iv = "q1Os1ZMe0nG28KUEx9lg3A==";
|
||||
string plainText = "{\"answer_type\":\"text\",\"text_info\":{\"short_answer\":\"answer\"}}";
|
||||
|
||||
string expectedCipherData = "aJhHfz6xc9iQiTLwusQe0HYKT6itYwq/YgQHltmLPf2UfpD+8ODJ8lrrxOMxy5NiALZqz1eYGtwD7cLQDP3ADg==";
|
||||
@@ -23,7 +23,7 @@ namespace SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests
|
||||
public void TestAESCBCDecryption()
|
||||
{
|
||||
string key = "q1Os1ZMe0nG28KUEx9lg3HjK7V5QyXvi212fzsgDqgz=";
|
||||
string iv = "q1Os1ZMe0nG28KUEx9lg3A=="; // iv 是 key 的前 16 个字节
|
||||
string iv = "q1Os1ZMe0nG28KUEx9lg3A==";
|
||||
string cipherText = "rUWkvTY9vRPOeVDSH/IdNXHmvgsUQtPkp7QtBQjSS1tcuTHGPWv8O3PlxbnsjCogsM7+EY+As4yF2kp4yxXpP2U7RmbDsU/luRO/EqkpFFsoxMZZArz2XH1YeSdnDyHYPWzjiicBYjNiqqpTMX8ekrqooN0cCEH7JBcbEe6btmiK8hZkysKTUJfG1DTpbONxON5+YuVPelVpzW5ry9sRYLDcqhImMb9FqI+BlIVAIXt5g+e70rheSqpeXz98pEROx7yPeRi3tXPAibuwg+vKDhoN6LuM0hzvyNzPjwK2gMmQB5yVuBZUalYIIZTVaMNGu4H6RK6MovLyM2cKfMUTphKaBBKpAvsV0o4/QRY0MvxeRYvZAQXEzOG3dJ7BRB2KEqBKttT7jMK8MO5HEXDE0CJxtNI4Rjww9XYmPhBM7lOZSF97YNEg1NhwcXvUc3YcrR334PhWJeu2dZCHaJzBqVXFxq/WprNHM0Gw06o6p5oWb4/nzXKYbpJWDyqTN/aztwo5sppHwlYrzNzF7gERP691qoabTHiCd0H+Ea3t65gTyNo2+ssvS1RVsKubApS4BkbZb/EaZCTKP20pcvDBoJk3QLi8ObyBq8sIcLwVjzelLMUgCDa059gBuao+S9qdHXebEZyS49BqAxngMWjHU5uCRO/x2b9w8nwfCCT8b0Q=";
|
||||
|
||||
string expectedPlainData = "{\"RequestId\":\"123123456456789789123456789\",\"SessionId\":\"12345678901234567_12345678909876543\",\"Query\":\"北京限行尾号是多少\",\"SkillName\":\"限行\",\"IntentName\":\"查限行尾号\",\"Slots\":[{\"SlotName\":\"from_loc\",\"SlotValue\":\"北京\",\"NormalizeValue\":\"{\\\"type\\\":\\\"LOC_CHINA_CITY\\\",\\\"city\\\":\\\"北京市\\\",\\\"city_simple\\\":\\\"北京\\\",\\\"loc_ori\\\":\\\"北京\\\"}\"}],\"Timestamp\":1704135845,\"Signature\":\"96f439043e1f7d2bb38162e35406f173\",\"ThirdApiId\":1234,\"ThirdApiName\":\"车辆限行\",\"UserId\":\"97f7e892\"}";
|
||||
|
@@ -4,7 +4,13 @@ namespace SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests
|
||||
{
|
||||
static TestClients()
|
||||
{
|
||||
Instance = new WechatChatbotClient(new WechatChatbotClientOptions()
|
||||
OpenAIInstance = new WechatOpenAIClient(new WechatOpenAIClientOptions()
|
||||
{
|
||||
AppId = TestConfigs.WechatAppId,
|
||||
Token = TestConfigs.WechatToken,
|
||||
EncodingAESKey = TestConfigs.WechatEncodingAESKey
|
||||
});
|
||||
ChatbotInstance = new WechatChatbotClient(new WechatChatbotClientOptions()
|
||||
{
|
||||
AppId = TestConfigs.WechatAppId,
|
||||
Token = TestConfigs.WechatToken,
|
||||
@@ -12,6 +18,7 @@ namespace SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests
|
||||
});
|
||||
}
|
||||
|
||||
public static readonly WechatChatbotClient Instance;
|
||||
public static readonly WechatOpenAIClient OpenAIInstance;
|
||||
public static readonly WechatChatbotClient ChatbotInstance;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user