From 921b9684868be702c4e96f4284cea5278b454376 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 4 Jun 2024 22:05:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(openai):=20=E6=96=B0=E5=A2=9E=20OpenAI=20v?= =?UTF-8?q?2=20=E7=89=88=E6=8E=A5=E5=8F=A3=E5=AE=A2=E6=88=B7=E7=AB=AF?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E5=AE=9E=E7=8E=B0=E5=8A=A0=E8=A7=A3=E5=AF=86?= =?UTF-8?q?=E5=8F=8A=E7=AD=BE=E5=90=8D=E4=B8=AD=E9=97=B4=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/WechatOpenAI/README.md | 5 +- .../WechatApiSecurityApiInterceptor.cs | 7 +- ...echatOpenAIClientExecuteTokenExtensions.cs | 33 ++++ ...echatOpenAIRequestEncryptionInterceptor.cs | 158 ++++++++++++++++++ .../WechatOpenAIRequestSigningInterceptor.cs | 56 +++++++ .../Models/OpenAI/Token/TokenV2Request.cs | 15 ++ .../Models/OpenAI/Token/TokenV2Response.cs | 21 +++ .../README.md | 3 +- .../Settings/Credentials.cs | 15 +- .../Utilities/MD5Utility.cs | 44 +++++ .../WechatOpenAIClient.cs | 116 +++++++++++++ .../WechatOpenAIClientBuilder.cs | 94 +++++++++++ .../WechatOpenAIClientOptions.cs | 42 +++++ .../WechatOpenAIEndpoints.cs | 13 ++ .../WechatOpenAIException.cs | 27 +++ .../WechatOpenAIRequest.cs | 22 +++ .../WechatOpenAIResponse.cs | 55 ++++++ .../IMPORTANT_CodeAnalyzeTests.cs | 50 ++++++ .../OpenAI/Token/TokenV2Request.json | 3 + .../OpenAI/Token/TokenV2Response.json | 8 + .../TestCase_ApiExecuteDialogTests.cs | 2 +- .../TestCase_ApiExecuteTokenTests.cs | 17 ++ .../TestCase_ToolsAESUtilityTests.cs | 4 +- .../TestClients.cs | 11 +- 24 files changed, 807 insertions(+), 14 deletions(-) create mode 100644 src/SKIT.FlurlHttpClient.Wechat.OpenAI/Extensions/WechatOpenAIClientExecuteTokenExtensions.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.OpenAI/Interceptors/WechatOpenAIRequestEncryptionInterceptor.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.OpenAI/Interceptors/WechatOpenAIRequestSigningInterceptor.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.OpenAI/Models/OpenAI/Token/TokenV2Request.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.OpenAI/Models/OpenAI/Token/TokenV2Response.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.OpenAI/Utilities/MD5Utility.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIClient.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIClientBuilder.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIClientOptions.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIEndpoints.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIException.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIRequest.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIResponse.cs create mode 100644 test/SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests/ModelSamples/OpenAI/Token/TokenV2Request.json create mode 100644 test/SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests/ModelSamples/OpenAI/Token/TokenV2Response.json create mode 100644 test/SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests/TestCase_ApiExecuteTokenTests.cs diff --git a/docs/WechatOpenAI/README.md b/docs/WechatOpenAI/README.md index cd521ba2..f1be919c 100644 --- a/docs/WechatOpenAI/README.md +++ b/docs/WechatOpenAI/README.md @@ -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 等算法工具类。 - 提供了解析回调通知事件等扩展方法。 --- diff --git a/src/SKIT.FlurlHttpClient.Wechat.Api/Interceptors/WechatApiSecurityApiInterceptor.cs b/src/SKIT.FlurlHttpClient.Wechat.Api/Interceptors/WechatApiSecurityApiInterceptor.cs index e073bc96..0c7cd002 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.Api/Interceptors/WechatApiSecurityApiInterceptor.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.Api/Interceptors/WechatApiSecurityApiInterceptor.cs @@ -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(); diff --git a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Extensions/WechatOpenAIClientExecuteTokenExtensions.cs b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Extensions/WechatOpenAIClientExecuteTokenExtensions.cs new file mode 100644 index 00000000..54a97473 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Extensions/WechatOpenAIClientExecuteTokenExtensions.cs @@ -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 + { + /// + /// 异步调用 [POST] /v2/token 接口。 + /// + /// REF:
+ /// + ///
+ ///
+ /// + /// + /// + /// + public static async Task 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(flurlReq, data: request, cancellationToken: cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Interceptors/WechatOpenAIRequestEncryptionInterceptor.cs b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Interceptors/WechatOpenAIRequestEncryptionInterceptor.cs new file mode 100644 index 00000000..8e54195b --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Interceptors/WechatOpenAIRequestEncryptionInterceptor.cs @@ -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 ENCRYPT_REQUIRED_URLS = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "/v2/bot/query" + }; + + private readonly string _baseUrl; + private readonly string _encodingAESKey; + private readonly Func? _customEncryptedRequestPathMatcher; + + public WechatOpenAIRequestEncryptionInterceptor(string baseUrl, string encodingAESKey, Func? 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(); + 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(); + 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(); + 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; + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Interceptors/WechatOpenAIRequestSigningInterceptor.cs b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Interceptors/WechatOpenAIRequestSigningInterceptor.cs new file mode 100644 index 00000000..052b1493 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Interceptors/WechatOpenAIRequestSigningInterceptor.cs @@ -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); + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Models/OpenAI/Token/TokenV2Request.cs b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Models/OpenAI/Token/TokenV2Request.cs new file mode 100644 index 00000000..9ba1758c --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Models/OpenAI/Token/TokenV2Request.cs @@ -0,0 +1,15 @@ +namespace SKIT.FlurlHttpClient.Wechat.OpenAI.Models +{ + /// + /// 表示 [POST] /v2/token 接口的请求。 + /// + public class TokenV2Request : WechatOpenAIRequest + { + /// + /// 获取或设置操作数据的管理员 ID。 + /// + [Newtonsoft.Json.JsonProperty("account")] + [System.Text.Json.Serialization.JsonPropertyName("account")] + public string Account { get; set; } = string.Empty; + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Models/OpenAI/Token/TokenV2Response.cs b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Models/OpenAI/Token/TokenV2Response.cs new file mode 100644 index 00000000..be072356 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Models/OpenAI/Token/TokenV2Response.cs @@ -0,0 +1,21 @@ +namespace SKIT.FlurlHttpClient.Wechat.OpenAI.Models +{ + /// + /// 表示 [POST] /v2/token 接口的响应。 + /// + public class TokenV2Response : WechatOpenAIResponse + { + public static class Types + { + public class Data + { + /// + /// 获取或设置接口访问令牌。 + /// + [Newtonsoft.Json.JsonProperty("access_token")] + [System.Text.Json.Serialization.JsonPropertyName("access_token")] + public string AccessToken { get; set; } = default!; + } + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/README.md b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/README.md index 0622a3e4..24cffb39 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/README.md +++ b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/README.md @@ -9,7 +9,8 @@ ### 【功能特性】 - 基于微信对话开放平台 API 封装。 -- 提供了微信对话开放平台所需的 AES、SHA-1 等算法工具类。 +- 针对 v2 版接口,请求时自动生成签名,无需开发者手动干预。 +- 提供了微信对话开放平台所需的 AES、MD5、SHA-1 等算法工具类。 - 提供了解析回调通知事件等扩展方法。 --- diff --git a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Settings/Credentials.cs b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Settings/Credentials.cs index 90368e8f..4dcfc999 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Settings/Credentials.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Settings/Credentials.cs @@ -5,20 +5,29 @@ namespace SKIT.FlurlHttpClient.Wechat.OpenAI.Settings public sealed class Credentials { /// - /// 初始化客户端时 的副本。 + /// 初始化客户端时 / 的副本。 /// public string AppId { get; } /// - /// 初始化客户端时 的副本。 + /// 初始化客户端时 / 的副本。 /// public string Token { get; } /// - /// 初始化客户端时 的副本。 + /// 初始化客户端时 / 的副本。 /// 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)); diff --git a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Utilities/MD5Utility.cs b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Utilities/MD5Utility.cs new file mode 100644 index 00000000..4d1e0dfb --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Utilities/MD5Utility.cs @@ -0,0 +1,44 @@ +using System; +using System.Security.Cryptography; + +namespace SKIT.FlurlHttpClient.Wechat.OpenAI.Utilities +{ + using SKIT.FlurlHttpClient.Primitives; + + /// + /// MD5 算法工具类。 + /// + public static class MD5Utility + { + /// + /// 计算 MD5 哈希值。 + /// + /// 要计算哈希值的信息字节数组。 + /// 哈希值字节数组。 + 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 + } + + /// + /// 计算 MD5 哈希值。 + /// + /// 要计算哈希值的信息。 + /// 经过十六进制编码的哈希值。 + 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); + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIClient.cs b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIClient.cs new file mode 100644 index 00000000..97447cda --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIClient.cs @@ -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 +{ + /// + /// 一个微信智能对话 API HTTP 客户端。 + /// + public class WechatOpenAIClient : CommonClientBase, ICommonClient + { + /// + /// 获取当前客户端使用的微信智能对话平台凭证。 + /// + public Settings.Credentials Credentials { get; } + + /// + /// 用指定的配置项初始化 类的新实例。 + /// + /// 配置项。 + public WechatOpenAIClient(WechatOpenAIClientOptions options) + : this(options, null) + { + } + + /// + /// + /// + /// + /// + /// + 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 + )); + } + + /// + /// 使用当前客户端生成一个新的 对象。 + /// + /// + /// + /// + /// + 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); + } + + /// + /// 异步发起请求。 + /// + /// + /// + /// + /// + /// + public async Task SendFlurlRequestAsync(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(flurlResponse, cancellationToken).ConfigureAwait(false); + } + + /// + /// 异步发起请求。 + /// + /// + /// + /// + /// + /// + public async Task SendFlurlRequestAsJsonAsync(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(flurlResponse, cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIClientBuilder.cs b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIClientBuilder.cs new file mode 100644 index 00000000..130b6cb8 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIClientBuilder.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; + +namespace SKIT.FlurlHttpClient.Wechat.OpenAI +{ + /// + /// 用于构造 实例的构造器。 + /// + public partial class WechatOpenAIClientBuilder : ICommonClientBuilder + { + private readonly WechatOpenAIClientOptions _options; + private readonly IList> _configures; + private readonly IList _interceptors; + private HttpClient? _httpClient; + private bool? _disposeClient; + + private WechatOpenAIClientBuilder(WechatOpenAIClientOptions options) + { + _options = options; + _configures = new List>(); + _interceptors = new List(); + } + + ICommonClientBuilder ICommonClientBuilder.ConfigureSettings(Action configure) + { + return ConfigureSettings(configure); + } + + ICommonClientBuilder ICommonClientBuilder.UseInterceptor(HttpInterceptor interceptor) + { + return UseInterceptor(interceptor); + } + + ICommonClientBuilder ICommonClientBuilder.UseHttpClient(HttpClient httpClient, bool disposeClient) + { + return UseHttpClient(httpClient, disposeClient); + } + + public WechatOpenAIClientBuilder ConfigureSettings(Action 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 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); + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIClientOptions.cs b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIClientOptions.cs new file mode 100644 index 00000000..21e2f597 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIClientOptions.cs @@ -0,0 +1,42 @@ +using System; + +namespace SKIT.FlurlHttpClient.Wechat.OpenAI +{ + /// + /// 一个用于构造 时使用的配置项。 + /// + public class WechatOpenAIClientOptions + { + /// + /// 获取或设置请求超时时间(单位:毫秒)。 + /// 默认值:30000 + /// + public int Timeout { get; set; } = 30 * 1000; + + /// + /// 获取或设置微信智能对话 API 入口点。 + /// 默认值: + /// + public string Endpoint { get; set; } = WechatOpenAIEndpoints.DEFAULT; + + /// + /// 获取或设置微信智能对话 AppId。 + /// + public string AppId { get; set; } = string.Empty; + + /// + /// 获取或设置微信智能对话 Token。 + /// + public string Token { get; set; } = string.Empty; + + /// + /// 获取或设置微信智能对话 EncodingAESKey。 + /// + public string EncodingAESKey { get; set; } = string.Empty; + + /// + /// 获取或设置自定义需加密请求路径匹配器。 + /// + public Func? CustomEncryptedRequestPathMatcher { get; set; } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIEndpoints.cs b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIEndpoints.cs new file mode 100644 index 00000000..39c9aa81 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIEndpoints.cs @@ -0,0 +1,13 @@ +namespace SKIT.FlurlHttpClient.Wechat.OpenAI +{ + /// + /// 微信智能对话 API 接口域名。 + /// + public static class WechatOpenAIEndpoints + { + /// + /// 主域名(默认)。 + /// + public const string DEFAULT = "https://openaiapi.weixin.qq.com"; + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIException.cs b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIException.cs new file mode 100644 index 00000000..58ce6382 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIException.cs @@ -0,0 +1,27 @@ +using System; + +namespace SKIT.FlurlHttpClient.Wechat.OpenAI +{ + /// + /// 当调用微信智能对话 API 出错时引发的异常。 + /// + public class WechatOpenAIException : CommonException + { + /// + public WechatOpenAIException() + { + } + + /// + public WechatOpenAIException(string message) + : base(message) + { + } + + /// + public WechatOpenAIException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIRequest.cs b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIRequest.cs new file mode 100644 index 00000000..83917dad --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIRequest.cs @@ -0,0 +1,22 @@ +namespace SKIT.FlurlHttpClient.Wechat.OpenAI +{ + /// + /// 表示微信智能对话 API 请求的基类。 + /// + public abstract class WechatOpenAIRequest : CommonRequestBase, ICommonRequest + { + /// + /// 获取或设置微信智能对话 API 接口访问令牌。 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public virtual string? AccessToken { get; set; } + + /// + /// 获取或设置微信智能对话 API 请求唯一标识。如果不指定将由系统自动生成。 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public virtual string? RequestId { get; set; } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIResponse.cs b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIResponse.cs new file mode 100644 index 00000000..3c9e80c8 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIResponse.cs @@ -0,0 +1,55 @@ +namespace SKIT.FlurlHttpClient.Wechat.OpenAI +{ + /// + /// 表示微信智能对话 API 响应的基类。 + /// + public abstract class WechatOpenAIResponse : CommonResponseBase, ICommonResponse + { + /// + /// 获取微信智能对话 API 返回的返回码。 + /// + [Newtonsoft.Json.JsonProperty("code")] + [System.Text.Json.Serialization.JsonPropertyName("code")] + public virtual int? Code { get; set; } + + /// + /// 获取微信智能对话 API 返回的返回信息。 + /// + [Newtonsoft.Json.JsonProperty("msg")] + [System.Text.Json.Serialization.JsonPropertyName("msg")] + public virtual string? Message { get; set; } + + /// + /// 获取微信智能对话 API 返回的请求唯一标识。 + /// + [Newtonsoft.Json.JsonProperty("request_id")] + [System.Text.Json.Serialization.JsonPropertyName("request_id")] + public virtual string? RequestId { get; set; } + + /// + /// 获取一个值,该值指示调用微信 API 是否成功。 + /// + /// (即 HTTP 状态码为 200,且 值都为 0) + /// + /// + /// + public override bool IsSuccessful() + { + return GetRawStatus() == 200 && Code.GetValueOrDefault() == 0; + } + } + + /// + /// 表示微信智能对话 API 响应的泛型基类。 + /// + public abstract class WechatOpenAIResponse : WechatOpenAIResponse + where TData : class + { + /// + /// 获取微信智能对话 API 返回的数据。 + /// + [Newtonsoft.Json.JsonProperty("data")] + [System.Text.Json.Serialization.JsonPropertyName("data")] + public virtual TData? Data { get; set; } + } +} diff --git a/test/SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests/IMPORTANT_CodeAnalyzeTests.cs b/test/SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests/IMPORTANT_CodeAnalyzeTests.cs index 3e79d5f0..3d09c49d 100644 --- a/test/SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests/IMPORTANT_CodeAnalyzeTests.cs +++ b/test/SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests/IMPORTANT_CodeAnalyzeTests.cs @@ -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, diff --git a/test/SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests/ModelSamples/OpenAI/Token/TokenV2Request.json b/test/SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests/ModelSamples/OpenAI/Token/TokenV2Request.json new file mode 100644 index 00000000..9a4c78e3 --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests/ModelSamples/OpenAI/Token/TokenV2Request.json @@ -0,0 +1,3 @@ +{ + "account": "fb2ab07ce06" +} diff --git a/test/SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests/ModelSamples/OpenAI/Token/TokenV2Response.json b/test/SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests/ModelSamples/OpenAI/Token/TokenV2Response.json new file mode 100644 index 00000000..0847201c --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests/ModelSamples/OpenAI/Token/TokenV2Response.json @@ -0,0 +1,8 @@ +{ + "code": 0, + "data": { + "access_token": "MX6ddM5mN07ucVKy+Y-to7tKRufZ1YF05eb542d5170000001c" + }, + "msg": "success", + "request_id": "54ae04cf-5e95-44fd-ad3f-62e7b163836b" +} diff --git a/test/SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests/TestCase_ApiExecuteDialogTests.cs b/test/SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests/TestCase_ApiExecuteDialogTests.cs index 6c46b7a5..b0d5f4ac 100644 --- a/test/SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests/TestCase_ApiExecuteDialogTests.cs +++ b/test/SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests/TestCase_ApiExecuteDialogTests.cs @@ -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); } diff --git a/test/SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests/TestCase_ApiExecuteTokenTests.cs b/test/SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests/TestCase_ApiExecuteTokenTests.cs new file mode 100644 index 00000000..6ec6a772 --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests/TestCase_ApiExecuteTokenTests.cs @@ -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); + } + } +} diff --git a/test/SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests/TestCase_ToolsAESUtilityTests.cs b/test/SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests/TestCase_ToolsAESUtilityTests.cs index e708ba8e..7d4bdd29 100644 --- a/test/SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests/TestCase_ToolsAESUtilityTests.cs +++ b/test/SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests/TestCase_ToolsAESUtilityTests.cs @@ -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\"}"; diff --git a/test/SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests/TestClients.cs b/test/SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests/TestClients.cs index d559dafa..022b3185 100644 --- a/test/SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests/TestClients.cs +++ b/test/SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests/TestClients.cs @@ -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; } }