From cf49b5fe8344ffe16dc0bda9d691a7921e5f70ac Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Sat, 9 Oct 2021 12:53:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(openai):=20=E6=96=B0=E5=A2=9E=E5=8F=8D?= =?UTF-8?q?=E5=BA=8F=E5=88=97=E5=8C=96=E5=9B=9E=E8=B0=83=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6=E5=BE=97=E6=89=A9=E5=B1=95=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...WechatOpenAIEventSerializationException.cs | 24 ++ .../WechatOpenAIClientEventExtensions.cs | 143 ++++++++ .../Settings/Credentials.cs | 22 +- .../Utilities/WxBizMsgCryptor.cs | 332 ++++++++++++++++++ .../WechatOpenAIClientOptions.cs | 23 +- .../WechatOpenAIEvent.cs | 38 ++ 6 files changed, 576 insertions(+), 6 deletions(-) create mode 100644 src/SKIT.FlurlHttpClient.Wechat.OpenAI/Exceptions/WechatOpenAIEventSerializationException.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.OpenAI/Extensions/WechatOpenAIClientEventExtensions.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.OpenAI/Utilities/WxBizMsgCryptor.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIEvent.cs diff --git a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Exceptions/WechatOpenAIEventSerializationException.cs b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Exceptions/WechatOpenAIEventSerializationException.cs new file mode 100644 index 00000000..d75e5902 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Exceptions/WechatOpenAIEventSerializationException.cs @@ -0,0 +1,24 @@ +using System; + +namespace SKIT.FlurlHttpClient.Wechat.OpenAI.Exceptions +{ + public class WechatOpenAIEventSerializationException : WechatOpenAIException + { + /// + internal WechatOpenAIEventSerializationException() + { + } + + /// + internal WechatOpenAIEventSerializationException(string message) + : base(message) + { + } + + /// + internal WechatOpenAIEventSerializationException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Extensions/WechatOpenAIClientEventExtensions.cs b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Extensions/WechatOpenAIClientEventExtensions.cs new file mode 100644 index 00000000..82694c5b --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Extensions/WechatOpenAIClientEventExtensions.cs @@ -0,0 +1,143 @@ +using System; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; +using System.Xml.Linq; +using System.Xml.Serialization; + +namespace SKIT.FlurlHttpClient.Wechat.OpenAI +{ + /// + /// 为 提供回调通知事件的扩展方法。 + /// + public static class WechatOpenAIClientEventExtensions + { + private class EncryptedWechatOpenAIEvent + { + [Newtonsoft.Json.JsonProperty("Encrypt")] + [System.Text.Json.Serialization.JsonPropertyName("Encrypt")] + public string EncryptedData { get; set; } = default!; + + [Newtonsoft.Json.JsonProperty("TimeStamp")] + [System.Text.Json.Serialization.JsonPropertyName("TimeStamp")] + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Converters.NumericalStringConverter))] + public string Timestamp { get; set; } = default!; + + [Newtonsoft.Json.JsonProperty("Nonce")] + [System.Text.Json.Serialization.JsonPropertyName("Nonce")] + public string Nonce { get; set; } = default!; + + [Newtonsoft.Json.JsonProperty("MsgSignature")] + [System.Text.Json.Serialization.JsonPropertyName("MsgSignature")] + public string Signature { get; set; } = default!; + } + + private static TEvent InnerDeserializeEventFromXml(this WechatOpenAIClient client, string callbackXml) + where TEvent : WechatOpenAIEvent + { + if (client == null) throw new ArgumentNullException(nameof(client)); + if (string.IsNullOrEmpty(callbackXml)) throw new ArgumentNullException(callbackXml); + + try + { + if (!Utilities.WxBizMsgCryptor.TryParseXml(callbackXml, out string? encryptedXml)) + throw new Exceptions.WechatOpenAIEventSerializationException("Encrypt event failed, because of empty encrypted data."); + + callbackXml = Utilities.WxBizMsgCryptor.AESDecrypt(cipherText: encryptedXml!, encodingAESKey: client.Credentials.PushEncodingAESKey!, out _); + + using var reader = new StringReader(callbackXml); + + XmlSerializer xmlSerializer = new XmlSerializer(typeof(TEvent), new XmlRootAttribute("xml")); + return (TEvent)xmlSerializer.Deserialize(reader)!; + } + catch (WechatOpenAIException) + { + throw; + } + catch (Exception ex) + { + throw new Exceptions.WechatOpenAIEventSerializationException("Deserialize event failed. Please see the `InnerException` for more details.", ex); + } + } + + /// + /// 从 XML 反序列化得到 对象。 + /// + /// + /// + /// + /// + public static TEvent DeserializeEventFromXml(this WechatOpenAIClient client, string callbackXml) + where TEvent : WechatOpenAIEvent, WechatOpenAIEvent.Serialization.IXmlSerializable, new() + { + return InnerDeserializeEventFromXml(client, callbackXml); + } + + /// + /// 从 XML 反序列化得到 对象。 + /// + /// + /// + /// + public static WechatOpenAIEvent DeserializeEventFromXml(this WechatOpenAIClient client, string callbackXml) + { + return InnerDeserializeEventFromXml(client, callbackXml); + } + + /// + /// 将 对象序列化成 XML。 + /// + /// + /// + /// + /// + public static string SerializeEventToXml(this WechatOpenAIClient client, TEvent callbackModel) + where TEvent : WechatOpenAIEvent, WechatOpenAIEvent.Serialization.IXmlSerializable, new() + { + string xml; + + try + { + using var stream = new MemoryStream(); + using var writer = new System.Xml.XmlTextWriter(stream, Encoding.UTF8); + writer.Formatting = System.Xml.Formatting.None; + + XmlSerializer xmlSerializer = new XmlSerializer(typeof(TEvent), new XmlRootAttribute("xml")); + XmlSerializerNamespaces xmlNamespace = new XmlSerializerNamespaces(); + xmlNamespace.Add(string.Empty, string.Empty); + xmlSerializer.Serialize(writer, callbackModel, xmlNamespace); + writer.Flush(); + xml = Encoding.UTF8.GetString(stream.ToArray()); + xml = Regex.Replace(xml, "\\s+<\\w+ (xsi|d2p1):nil=\"true\"[^>]*/>", string.Empty, RegexOptions.IgnoreCase); + xml = Regex.Replace(xml, "<\\?xml[^>]*\\?>", string.Empty, RegexOptions.IgnoreCase); + } + catch (Exception ex) + { + throw new Exceptions.WechatOpenAIEventSerializationException("Serialize event failed. Please see the `InnerException` for more details.", ex); + } + + + if (string.IsNullOrEmpty(client.Credentials.PushEncodingAESKey)) + throw new Exceptions.WechatOpenAIEventSerializationException("Encrypt event failed, because there is no encoding AES key."); + if (string.IsNullOrEmpty(client.Credentials.PushToken)) + throw new Exceptions.WechatOpenAIEventSerializationException("Encrypt event failed, because there is no token."); + + try + { + string cipher = Utilities.WxBizMsgCryptor.AESEncrypt( + plainText: xml, + encodingAESKey: client.Credentials.PushEncodingAESKey!, + appId: client.Credentials.AppId! + ); + + xml = Utilities.WxBizMsgCryptor.WrapXml(sToken: client.Credentials.PushToken!, sMsgEncrypt: cipher); + } + catch (Exception ex) + { + throw new Exceptions.WechatOpenAIEventSerializationException("Encrypt event failed. Please see the `InnerException` for more details.", ex); + } + + return xml; + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Settings/Credentials.cs b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Settings/Credentials.cs index 1dca01a4..800dea03 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Settings/Credentials.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Settings/Credentials.cs @@ -11,12 +11,27 @@ namespace SKIT.FlurlHttpClient.Wechat.OpenAI.Settings /// /// 初始化客户端时 的副本。 /// - public string ClientId { get; } + public string? ClientId { get; } /// /// 初始化客户端时 的副本。 /// - public string ClientKey { get; } + public string? ClientKey { get; } + + /// + /// 初始化客户端时 的副本。 + /// + public string? AppId { get; } + + /// + /// 初始化客户端时 的副本。 + /// + public string? PushToken { get; } + + /// + /// 初始化客户端时 的副本。 + /// + public string? PushEncodingAESKey { get; } internal Credentials(WechatOpenAIClientOptions options) { @@ -24,6 +39,9 @@ namespace SKIT.FlurlHttpClient.Wechat.OpenAI.Settings ClientId = options.ClientId; ClientKey = options.ClientKey; + AppId = options.AppId; + PushToken = options.PushToken; + PushEncodingAESKey = options.PushEncodingAESKey; } } } diff --git a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Utilities/WxBizMsgCryptor.cs b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Utilities/WxBizMsgCryptor.cs new file mode 100644 index 00000000..7d324384 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Utilities/WxBizMsgCryptor.cs @@ -0,0 +1,332 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Security; +using System.Security.Cryptography; +using System.Text; +using System.Xml; + +namespace SKIT.FlurlHttpClient.Wechat.OpenAI.Utilities +{ + public static class WxBizMsgCryptor + { + private const int AES_KEY_SIZE = 256; + private const int AES_BLOCK_SIZE = 128; + + private static char ConvertToChar(int i) + { + byte ret = (byte)(i & 0xFF); + return (char)ret; + } + + private static byte[] KCS7Encoder(int len) + { + if (len <= 0) throw new ArgumentOutOfRangeException(nameof(len)); + + const int BLOCK_SIZE = 32; + + int paddingLength = BLOCK_SIZE - (len % BLOCK_SIZE); + if (paddingLength == 0) + { + paddingLength = BLOCK_SIZE; + } + + char paddingChar = ConvertToChar(paddingLength); + string tmp = string.Empty; + for (int index = 0; index < paddingLength; index++) + { + tmp += paddingChar; + } + + return Encoding.UTF8.GetBytes(tmp); + } + + private static string CreateRandCode(int len) + { + if (len <= 0) throw new ArgumentOutOfRangeException(nameof(len)); + + Random random = new Random(unchecked((int)DateTime.Now.Ticks)); + string[] source = "2,3,4,5,6,7,a,c,d,e,f,h,i,j,k,m,n,p,r,s,t,A,C,D,E,F,G,H,J,K,M,N,P,Q,R,S,U,V,W,X,Y,Z".Split(','); + string result = string.Empty; + for (int i = 0; i < len; i++) + { + int randval = random.Next(0, source.Length - 1); + result += source[randval]; + } + + return result; + } + + private static byte[] Decode2(byte[] decryptedBytes) + { + if (decryptedBytes == null) throw new ArgumentNullException(nameof(decryptedBytes)); + + int pad = (int)decryptedBytes[decryptedBytes.Length - 1]; + if (pad < 1 || pad > 32) + { + pad = 0; + } + + byte[] res = new byte[decryptedBytes.Length - pad]; + Array.Copy(decryptedBytes, 0, res, 0, decryptedBytes.Length - pad); + return res; + } + + private static byte[] AESDecrypt(byte[] keyBytes, byte[] ivBytes, byte[] ciperBytes) + { + if (keyBytes == null) throw new ArgumentNullException(nameof(keyBytes)); + if (ivBytes == null) throw new ArgumentNullException(nameof(ivBytes)); + if (ciperBytes == null) throw new ArgumentNullException(nameof(ciperBytes)); + + using RijndaelManaged aes = new RijndaelManaged(); + aes.KeySize = 256; + aes.BlockSize = 128; + aes.Mode = CipherMode.CBC; + //aes.Padding = PaddingMode.PKCS7; + aes.Padding = PaddingMode.None; + aes.Key = keyBytes; + aes.IV = ivBytes; + + using (var decryptor = aes.CreateDecryptor(aes.Key, aes.IV)) + using (var ms = new MemoryStream()) + using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Write)) + { + byte[] bMsg = new byte[ciperBytes.Length + 32 - ciperBytes.Length % 32]; + Array.Copy(ciperBytes, bMsg, ciperBytes.Length); + cs.Write(ciperBytes, 0, ciperBytes.Length); + + byte[] plainBytes = Decode2(ms.ToArray()); + return plainBytes; + } + } + + private static string AESEncrypt(byte[] keyBytes, byte[] ivBytes, byte[] plainBytes) + { + if (keyBytes == null) throw new ArgumentNullException(nameof(keyBytes)); + if (ivBytes == null) throw new ArgumentNullException(nameof(ivBytes)); + if (plainBytes == null) throw new ArgumentNullException(nameof(plainBytes)); + + using var aes = new RijndaelManaged(); + aes.KeySize = AES_KEY_SIZE; + aes.BlockSize = AES_BLOCK_SIZE; + //aes.Padding = PaddingMode.PKCS7; + aes.Padding = PaddingMode.None; + aes.Mode = CipherMode.CBC; + aes.Key = keyBytes; + aes.IV = ivBytes; + + byte[] msgBytes = new byte[plainBytes.Length + 32 - plainBytes.Length % 32]; + Array.Copy(plainBytes, msgBytes, plainBytes.Length); + byte[] padBytes = KCS7Encoder(plainBytes.Length); + Array.Copy(padBytes, 0, msgBytes, plainBytes.Length, padBytes.Length); + + using (var encryptor = aes.CreateEncryptor(aes.Key, aes.IV)) + using (var ms = new MemoryStream()) + using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write)) + { + cs.Write(msgBytes, 0, msgBytes.Length); + byte[] cipherBytes = ms.ToArray(); + return Convert.ToBase64String(cipherBytes); + } + } + + /// + /// AES 解密微信回调加密数据。 + /// + /// 微信推送来的加密文本内容(即 `Encrypt` 字段的值)。 + /// 微信后台设置的 EncodingAESKey。 + /// 微信 AppId。 + /// 解密后的文本内容。 + public static string AESDecrypt(string cipherText, string encodingAESKey, out string appId) + { + if (cipherText == null) throw new ArgumentNullException(nameof(cipherText)); + if (encodingAESKey == null) throw new ArgumentNullException(nameof(encodingAESKey)); + + byte[] cipherBytes = Convert.FromBase64String(cipherText); + byte[] keyBytes = Convert.FromBase64String(encodingAESKey + "="); + byte[] ivBytes = new byte[16]; + Array.Copy(keyBytes, ivBytes, 16); + byte[] btmpMsg = AESDecrypt(ciperBytes: cipherBytes, ivBytes: ivBytes, keyBytes: keyBytes); + + int len = BitConverter.ToInt32(btmpMsg, 16); + len = IPAddress.NetworkToHostOrder(len); + + byte[] bMsg = new byte[len]; + byte[] bCorpId = new byte[btmpMsg.Length - 20 - len]; + Array.Copy(btmpMsg, 20, bMsg, 0, len); + Array.Copy(btmpMsg, 20 + len, bCorpId, 0, btmpMsg.Length - 20 - len); + + appId = Encoding.UTF8.GetString(bCorpId); + return Encoding.UTF8.GetString(bMsg); + } + + /// + /// AES 加密微信回调敏感数据。 + /// + /// 返回给微信的原始文本内容。 + /// 微信后台设置的 EncodingAESKey。 + /// 微信 AppId。 + /// 加密后的文本内容。 + public static string AESEncrypt(string plainText, string encodingAESKey, string appId) + { + if (plainText == null) throw new ArgumentNullException(nameof(plainText)); + if (encodingAESKey == null) throw new ArgumentNullException(nameof(encodingAESKey)); + if (appId == null) throw new ArgumentNullException(nameof(appId)); + + byte[] keyBytes = Convert.FromBase64String(encodingAESKey + "="); + byte[] ivBytes = new byte[16]; + Array.Copy(keyBytes, ivBytes, 16); + + string randCode = CreateRandCode(16); + byte[] bRand = Encoding.UTF8.GetBytes(randCode); + byte[] bCorpId = Encoding.UTF8.GetBytes(appId); + byte[] bMsgTmp = Encoding.UTF8.GetBytes(plainText); + byte[] bMsgLen = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(bMsgTmp.Length)); + byte[] bMsg = new byte[bRand.Length + bMsgLen.Length + bCorpId.Length + bMsgTmp.Length]; + + Array.Copy(bRand, bMsg, bRand.Length); + Array.Copy(bMsgLen, 0, bMsg, bRand.Length, bMsgLen.Length); + Array.Copy(bMsgTmp, 0, bMsg, bRand.Length + bMsgLen.Length, bMsgTmp.Length); + Array.Copy(bCorpId, 0, bMsg, bRand.Length + bMsgLen.Length + bMsgTmp.Length, bCorpId.Length); + + return AESEncrypt(keyBytes: keyBytes, ivBytes: ivBytes, plainBytes: bMsg); + } + + /// + /// 验证微信回调签名。 + /// + /// 微信后台设置的 Token。 + /// 微信推送来的时间戳字符串。 + /// 微信推送来的随机字符串。 + /// 微信推送来的加密文本内容(即 `Encrypt` 字段的值)。 + /// 待验证的签名。 + /// 验证结果。 + public static bool VerifySignature(string sToken, string sTimestamp, string sNonce, string sMsgEncrypt, string sMsgSign) + { + if (sToken == null) throw new ArgumentNullException(nameof(sToken)); + if (sTimestamp == null) throw new ArgumentNullException(nameof(sTimestamp)); + if (sNonce == null) throw new ArgumentNullException(nameof(sNonce)); + if (sMsgEncrypt == null) throw new ArgumentNullException(nameof(sMsgEncrypt)); + if (sMsgSign == null) throw new ArgumentNullException(nameof(sMsgSign)); + + string expectedSign = GenerateSignature(sToken, sTimestamp, sNonce, sMsgEncrypt); + return string.Equals(expectedSign, sMsgSign, StringComparison.OrdinalIgnoreCase); + } + + /// + /// 生成微信回调签名。 + /// + /// 微信后台设置的 Token。 + /// 返回给微信的时间戳字符串。 + /// 返回给微信的随机字符串。 + /// 返回给微信的加密文本内容。 + /// 签名。 + public static string GenerateSignature(string sToken, string sTimestamp, string sNonce, string sMsgEncrypt) + { + if (sToken == null) throw new ArgumentNullException(nameof(sToken)); + if (sTimestamp == null) throw new ArgumentNullException(nameof(sTimestamp)); + if (sNonce == null) throw new ArgumentNullException(nameof(sNonce)); + if (sMsgEncrypt == null) throw new ArgumentNullException(nameof(sMsgEncrypt)); + + ISet set = new SortedSet(StringComparer.Ordinal); + set.Add(sToken); + set.Add(sTimestamp); + set.Add(sNonce); + set.Add(sMsgEncrypt); + + string rawText = string.Join(string.Empty, set.ToArray()); + string signText = Security.SHA1Utility.Hash(rawText); + return signText.ToLower(); + } + + /// + /// 尝试解析微信推送过来的 XML 数据(仅适用于兼容模式或安全模式)。 + /// + /// 微信推送来的 XML 数据。 + /// 如果解析成功,将返回解析后的 `Encrypt` 字段的值。 + /// 指示是否是有效的 XML 内容。 + public static bool TryParseXml(string xml, out string? encryptedMsg) + { + return TryParseXml(xml, out encryptedMsg, out _); + } + + /// + /// 尝试解析微信推送过来的 XML 数据(仅适用于兼容模式或安全模式)。 + /// + /// 微信推送来的 XML 数据。 + /// 如果解析成功,将返回解析后的 `ToUserName` 字段的值。 + /// 如果解析成功,将返回解析后的 `Encrypt` 字段的值。 + /// 指示是否是有效的 XML 内容。 + public static bool TryParseXml(string xml, out string? encryptedMsg, out string? toUserName) + { + if (xml == null) throw new ArgumentNullException(nameof(xml)); + + encryptedMsg = null; + toUserName = null; + + try + { + XmlDocument xmlDoc = new XmlDocument(); + xmlDoc.XmlResolver = null; + xmlDoc.LoadXml(xml); + + XmlNode? xmlRoot = xmlDoc.FirstChild; + if (xmlRoot == null) + return false; + + encryptedMsg = xmlRoot["Encrypt"]?.InnerText?.ToString(); + toUserName = xmlRoot["ToUserName"]?.InnerText?.ToString(); + + return !string.IsNullOrEmpty(encryptedMsg); + } + catch (XmlException) + { + return false; + } + } + + /// + /// 将返回给微信的加密文本内容包装成 XML 格式(仅适用于安全模式)。 + /// + /// 微信后台设置的 Token。 + /// 返回给微信的加密文本内容。 + /// + public static string WrapXml(string sToken, string sMsgEncrypt) + { + if (sToken == null) throw new ArgumentNullException(nameof(sToken)); + if (sMsgEncrypt == null) throw new ArgumentNullException(nameof(sMsgEncrypt)); + + string sTimestamp = DateTimeOffset.Now.ToLocalTime().ToUnixTimeSeconds().ToString(); + string sNonce = DateTimeOffset.Now.Ticks.ToString("x"); + string sMsgSign = GenerateSignature(sToken: sToken, sTimestamp: sTimestamp, sNonce: sNonce, sMsgEncrypt: sMsgEncrypt); + return WrapXml(sTimestamp: sTimestamp, sNonce: sNonce, sMsgEncrypt: sMsgEncrypt, sMsgSign: sMsgSign); + } + + /// + /// 将返回给微信的加密文本内容包装成 XML 格式(仅适用于安全模式)。 + /// + /// 返回给微信的时间戳字符串。 + /// 返回给微信的随机字符串。 + /// 返回给微信的加密文本内容。 + /// 返回给微信的签名。 + /// + public static string WrapXml(string sTimestamp, string sNonce, string sMsgEncrypt, string sMsgSign) + { + if (sTimestamp == null) throw new ArgumentNullException(nameof(sTimestamp)); + if (sNonce == null) throw new ArgumentNullException(nameof(sNonce)); + if (sMsgEncrypt == null) throw new ArgumentNullException(nameof(sMsgEncrypt)); + if (sMsgSign == null) throw new ArgumentNullException(nameof(sMsgSign)); + + StringBuilder builder = new StringBuilder(); + builder.AppendFormat(""); + builder.AppendFormat("", SecurityElement.Escape(sTimestamp)); + builder.AppendFormat("", SecurityElement.Escape(sNonce)); + builder.AppendFormat("", SecurityElement.Escape(sMsgEncrypt)); + builder.AppendFormat("", SecurityElement.Escape(sMsgSign)); + builder.AppendFormat(""); + return builder.ToString(); + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIClientOptions.cs b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIClientOptions.cs index 12a67690..58f2f8ad 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIClientOptions.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIClientOptions.cs @@ -18,13 +18,28 @@ public string Endpoints { get; set; } = WechatOpenAIEndpoints.DEFAULT; /// - /// 获取或设置微信智能对话 ClientId。 + /// 获取或设置微信智能对话 ClientId。仅限第三方平台接入时使用。 /// - public string ClientId { get; set; } = default!; + public string? ClientId { get; set; } /// - /// 获取或设置微信智能对话 ClientKey。 + /// 获取或设置微信智能对话 ClientKey。仅限第三方平台接入时使用。 /// - public string ClientKey { get; set; } = default!; + public string? ClientKey { get; set; } + + /// + /// 获取或设置微信智能对话 AppId。仅限平台接入时使用。 + /// + public string? AppId { get; set; } + + /// + /// 获取或设置微信智能对话服务器推送的 EncodingAESKey。仅限平台接入时使用。 + /// + public string? PushEncodingAESKey { get; set; } + + /// + /// 获取或设置微信智能对话服务器推送的 Token。仅限平台接入时使用。 + /// + public string? PushToken { get; set; } } } diff --git a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIEvent.cs b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIEvent.cs new file mode 100644 index 00000000..37fee6f0 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/WechatOpenAIEvent.cs @@ -0,0 +1,38 @@ +using System; +using System.Xml.Serialization; + +namespace SKIT.FlurlHttpClient.Wechat.OpenAI +{ + /// + /// 表示微信智能对话 API 回调通知事件的基类。 + /// + [Serializable] + public class WechatOpenAIEvent + { + public static class Serialization + { + [XmlRoot("xml")] + public interface IXmlSerializable + { + } + } + + /// + /// 获取或设置 AppId。 + /// + [XmlElement("appid", IsNullable = true)] + public string? AppId { get; set; } + + /// + /// 获取或设置事件类型。 + /// + [XmlElement("event", IsNullable = true)] + public string? Event { get; set; } + + /// + /// 获取或设置消息创建时间戳。 + /// + [XmlElement("createtime", IsNullable = true)] + public long? CreateTimestamp { get; set; } + } +}