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; }
+ }
+}