添加询问李诫(AI 问答)功能
- 添加 AIChatComponent:用于主要的逻辑(构造请求,解析响应) - 添加 AIChatForm:用于问答的页面展示
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
// Feedback: mailto:ellan@gameframework.cn
|
||||
//------------------------------------------------------------
|
||||
|
||||
namespace StarForce
|
||||
namespace Network
|
||||
{
|
||||
public abstract class CSPacketBase : PacketBase
|
||||
{
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// Feedback: mailto:ellan@gameframework.cn
|
||||
//------------------------------------------------------------
|
||||
|
||||
namespace StarForce
|
||||
namespace Network
|
||||
{
|
||||
public sealed class CSPacketHeader : PacketHeaderBase
|
||||
{
|
||||
|
||||
@@ -16,7 +16,7 @@ using System.IO;
|
||||
using System.Reflection;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace StarForce
|
||||
namespace Network
|
||||
{
|
||||
public class NetworkChannelHelper : INetworkChannelHelper
|
||||
{
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Network
|
||||
{
|
||||
public sealed class OpenAIHttpNetworkService : IDisposable
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private bool _disposed;
|
||||
|
||||
public OpenAIHttpNetworkService()
|
||||
{
|
||||
HttpClientHandler handler = new HttpClientHandler
|
||||
{
|
||||
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
|
||||
};
|
||||
|
||||
_httpClient = new HttpClient(handler);
|
||||
}
|
||||
|
||||
public async Task<OpenAIHeartbeatResult> SendHeartbeatAsync(string url, string apiKey, int timeoutSeconds,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
return OpenAIHeartbeatResult.CreateFailure(false, false, 0, "Heartbeat failed. URL is empty.");
|
||||
}
|
||||
|
||||
using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url))
|
||||
{
|
||||
ApplyAuthorization(request, apiKey);
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
|
||||
using (CancellationTokenSource timeoutCts = new CancellationTokenSource(
|
||||
TimeSpan.FromSeconds(Math.Max(1, timeoutSeconds))))
|
||||
{
|
||||
using (CancellationTokenSource linkedCts =
|
||||
CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token))
|
||||
{
|
||||
try
|
||||
{
|
||||
using (HttpResponseMessage response = await _httpClient
|
||||
.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, linkedCts.Token)
|
||||
.ConfigureAwait(false))
|
||||
{
|
||||
long statusCode = (long)response.StatusCode;
|
||||
bool isHttpSuccess = response.IsSuccessStatusCode;
|
||||
if (isHttpSuccess)
|
||||
{
|
||||
return OpenAIHeartbeatResult.CreateSuccess(statusCode);
|
||||
}
|
||||
|
||||
if (statusCode == 401 || statusCode == 403)
|
||||
{
|
||||
return OpenAIHeartbeatResult.CreateFailure(true, false, statusCode,
|
||||
"Authentication failed. API key may be invalid.");
|
||||
}
|
||||
|
||||
string errorBody = await TryReadContentAsync(response.Content).ConfigureAwait(false);
|
||||
string errorMessage = string.IsNullOrEmpty(errorBody)
|
||||
? string.Format("Heartbeat failed with HTTP {0}.", statusCode.ToString())
|
||||
: string.Format("Heartbeat failed with HTTP {0}. body={1}", statusCode.ToString(),
|
||||
errorBody);
|
||||
|
||||
return OpenAIHeartbeatResult.CreateFailure(true, false, statusCode, errorMessage);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return OpenAIHeartbeatResult.CreateFailure(false, false, 0,
|
||||
"Heartbeat canceled by caller.");
|
||||
}
|
||||
|
||||
return OpenAIHeartbeatResult.CreateFailure(false, false, 0, "Heartbeat timeout.");
|
||||
}
|
||||
catch (HttpRequestException exception)
|
||||
{
|
||||
return OpenAIHeartbeatResult.CreateFailure(false, false, 0, exception.Message);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
return OpenAIHeartbeatResult.CreateFailure(false, false, 0, exception.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<OpenAIStreamResult> StreamChatAsync(
|
||||
string url,
|
||||
string apiKey,
|
||||
string requestBodyJson,
|
||||
int timeoutSeconds,
|
||||
Action<string> onSseDataPayload,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
return OpenAIStreamResult.CreateFailure(false, false, 0, "AI chat request failed. URL is empty.",
|
||||
string.Empty);
|
||||
}
|
||||
|
||||
using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, url))
|
||||
{
|
||||
ApplyAuthorization(request, apiKey);
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));
|
||||
request.Content = new StringContent(requestBodyJson ?? string.Empty, Encoding.UTF8, "application/json");
|
||||
|
||||
using (CancellationTokenSource timeoutCts = new CancellationTokenSource(
|
||||
TimeSpan.FromSeconds(Math.Max(1, timeoutSeconds))))
|
||||
{
|
||||
using (CancellationTokenSource linkedCts =
|
||||
CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token))
|
||||
{
|
||||
try
|
||||
{
|
||||
using (HttpResponseMessage response = await _httpClient
|
||||
.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, linkedCts.Token)
|
||||
.ConfigureAwait(false))
|
||||
{
|
||||
long statusCode = (long)response.StatusCode;
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
string errorBody = await TryReadContentAsync(response.Content).ConfigureAwait(false);
|
||||
string errorMessage = string.Format("HTTP {0} {1}", statusCode.ToString(),
|
||||
response.ReasonPhrase ?? string.Empty);
|
||||
return OpenAIStreamResult.CreateFailure(true, false, statusCode, errorMessage,
|
||||
errorBody);
|
||||
}
|
||||
|
||||
using (Stream stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
|
||||
using (StreamReader reader = new StreamReader(stream, Encoding.UTF8))
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
string line =
|
||||
await WaitReadLineAsync(reader, linkedCts.Token).ConfigureAwait(false);
|
||||
if (line == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!line.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string payload = line.Substring(5).Trim();
|
||||
if (string.IsNullOrEmpty(payload))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
onSseDataPayload?.Invoke(payload);
|
||||
}
|
||||
}
|
||||
|
||||
return OpenAIStreamResult.CreateSuccess(statusCode);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return OpenAIStreamResult.CreateCanceled();
|
||||
}
|
||||
catch (HttpRequestException exception)
|
||||
{
|
||||
return OpenAIStreamResult.CreateFailure(false, false, 0, exception.Message, string.Empty);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
return OpenAIStreamResult.CreateFailure(false, false, 0, exception.Message, string.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private static void ApplyAuthorization(HttpRequestMessage request, string apiKey)
|
||||
{
|
||||
if (request == null || string.IsNullOrWhiteSpace(apiKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey.Trim());
|
||||
}
|
||||
|
||||
private static async Task<string> WaitReadLineAsync(StreamReader reader, CancellationToken cancellationToken)
|
||||
{
|
||||
Task<string> readLineTask = reader.ReadLineAsync();
|
||||
if (readLineTask.IsCompleted || !cancellationToken.CanBeCanceled)
|
||||
{
|
||||
return await readLineTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
TaskCompletionSource<bool> cancellationTaskSource = new TaskCompletionSource<bool>();
|
||||
using (cancellationToken.Register(() => cancellationTaskSource.TrySetResult(true)))
|
||||
{
|
||||
Task completedTask = await Task.WhenAny(readLineTask, cancellationTaskSource.Task).ConfigureAwait(false);
|
||||
if (completedTask != readLineTask)
|
||||
{
|
||||
throw new OperationCanceledException(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
return await readLineTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async Task<string> TryReadContentAsync(HttpContent content)
|
||||
{
|
||||
if (content == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return await content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_httpClient.Dispose();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class OpenAIHeartbeatResult
|
||||
{
|
||||
public bool IsEndpointReachable { get; private set; }
|
||||
public bool IsConnectionValid { get; private set; }
|
||||
public long StatusCode { get; private set; }
|
||||
public string ErrorMessage { get; private set; }
|
||||
|
||||
public static OpenAIHeartbeatResult CreateSuccess(long statusCode)
|
||||
{
|
||||
return new OpenAIHeartbeatResult
|
||||
{
|
||||
IsEndpointReachable = true,
|
||||
IsConnectionValid = true,
|
||||
StatusCode = statusCode,
|
||||
ErrorMessage = string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
public static OpenAIHeartbeatResult CreateFailure(bool endpointReachable, bool connectionValid, long statusCode,
|
||||
string errorMessage)
|
||||
{
|
||||
return new OpenAIHeartbeatResult
|
||||
{
|
||||
IsEndpointReachable = endpointReachable,
|
||||
IsConnectionValid = connectionValid,
|
||||
StatusCode = statusCode,
|
||||
ErrorMessage = errorMessage ?? string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class OpenAIStreamResult
|
||||
{
|
||||
public bool IsSuccess { get; private set; }
|
||||
public bool IsCanceled { get; private set; }
|
||||
public bool IsEndpointReachable { get; private set; }
|
||||
public bool IsConnectionValid { get; private set; }
|
||||
public long StatusCode { get; private set; }
|
||||
public string ErrorMessage { get; private set; }
|
||||
public string ErrorBody { get; private set; }
|
||||
|
||||
public static OpenAIStreamResult CreateSuccess(long statusCode)
|
||||
{
|
||||
return new OpenAIStreamResult
|
||||
{
|
||||
IsSuccess = true,
|
||||
IsCanceled = false,
|
||||
IsEndpointReachable = true,
|
||||
IsConnectionValid = true,
|
||||
StatusCode = statusCode,
|
||||
ErrorMessage = string.Empty,
|
||||
ErrorBody = string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
public static OpenAIStreamResult CreateCanceled()
|
||||
{
|
||||
return new OpenAIStreamResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
IsCanceled = true,
|
||||
IsEndpointReachable = false,
|
||||
IsConnectionValid = false,
|
||||
StatusCode = 0,
|
||||
ErrorMessage = "Request canceled.",
|
||||
ErrorBody = string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
public static OpenAIStreamResult CreateFailure(bool endpointReachable, bool connectionValid, long statusCode,
|
||||
string errorMessage, string errorBody)
|
||||
{
|
||||
return new OpenAIStreamResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
IsCanceled = false,
|
||||
IsEndpointReachable = endpointReachable,
|
||||
IsConnectionValid = connectionValid,
|
||||
StatusCode = statusCode,
|
||||
ErrorMessage = errorMessage ?? string.Empty,
|
||||
ErrorBody = errorBody ?? string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8b1df451f58f9f04ea0357989526f6a7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -8,7 +8,7 @@
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
|
||||
namespace StarForce
|
||||
namespace Network
|
||||
{
|
||||
[Serializable, ProtoContract(Name = @"CSHeartBeat")]
|
||||
public class CSHeartBeat : CSPacketBase
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
|
||||
namespace StarForce
|
||||
namespace Network
|
||||
{
|
||||
[Serializable, ProtoContract(Name = @"SCHeartBeat")]
|
||||
public class SCHeartBeat : SCPacketBase
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
using GameFramework.Network;
|
||||
using ProtoBuf;
|
||||
|
||||
namespace StarForce
|
||||
namespace Network
|
||||
{
|
||||
public abstract class PacketBase : Packet, IExtensible
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
using GameFramework.Network;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace StarForce
|
||||
namespace Network
|
||||
{
|
||||
public class SCHeartBeatHandler : PacketHandlerBase
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
using GameFramework.Network;
|
||||
|
||||
namespace StarForce
|
||||
namespace Network
|
||||
{
|
||||
public abstract class PacketHandlerBase : IPacketHandler
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
using GameFramework;
|
||||
using GameFramework.Network;
|
||||
|
||||
namespace StarForce
|
||||
namespace Network
|
||||
{
|
||||
public abstract class PacketHeaderBase : IPacketHeader, IReference
|
||||
{
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// Feedback: mailto:ellan@gameframework.cn
|
||||
//------------------------------------------------------------
|
||||
|
||||
namespace StarForce
|
||||
namespace Network
|
||||
{
|
||||
public enum PacketType : byte
|
||||
{
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// Feedback: mailto:ellan@gameframework.cn
|
||||
//------------------------------------------------------------
|
||||
|
||||
namespace StarForce
|
||||
namespace Network
|
||||
{
|
||||
public abstract class SCPacketBase : PacketBase
|
||||
{
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// Feedback: mailto:ellan@gameframework.cn
|
||||
//------------------------------------------------------------
|
||||
|
||||
namespace StarForce
|
||||
namespace Network
|
||||
{
|
||||
public sealed class SCPacketHeader : PacketHeaderBase
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user