Files
xyou f70b061d1a
CI / Go Backend (push) Canceled after 0s
feat: 项目初始化 + 3D方块世界原型 + AI助搭系统
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器

HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块

Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚

AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
2026-08-08 14:07:56 +08:00

346 lines
12 KiB
C#

using System.IO;
using System.Net;
using System.Net.Http;
using PCL.Core.App;
using PCL.Core.IO.Net.Http;
using PCL.Core.Link.Natayark;
namespace PCL;
public static class ModWebServer
{
private static readonly Dictionary<string, HttpServer> _webServers = new();
/// <summary>
/// 在新的 <see cref="Task" /> 中开始 HTTP 服务端响应。
/// </summary>
/// <param name="name">服务端名称</param>
/// <param name="server">服务端实例</param>
/// <returns>是否成功开始,若已存在同名实例则返回 <c>false</c></returns>
public static bool StartWebServer(string name, HttpServer server)
{
name = name.ToLowerInvariant();
lock (_webServers)
{
if (_webServers.ContainsKey(name))
return false;
_webServers[name] = server;
}
Task.Run(() =>
{
ModBase.Log($"[WebServer] 服务端 '{name}' 已启动");
try
{
server.Start();
// 保持服务器运行直到被停止(通过检查字典中是否还存在该服务器)
while (true)
{
lock (_webServers)
{
if (!_webServers.ContainsKey(name))
break;
}
Thread.Sleep(100);
}
}
catch (Exception ex)
{
ModBase.Log(ex, $"[WebServer] 服务端 '{name}' 运行出错");
}
finally
{
try
{
server.Dispose();
}
catch
{
// 忽略已释放的异常
}
ModBase.Log($"[WebServer] 服务端 '{name}' 已停止");
lock (_webServers)
{
_webServers.Remove(name);
}
}
});
return true;
}
/// <summary>
/// 检查指定名称的 HTTP 服务端是否正在运行
/// </summary>
/// <param name="name">服务端名称</param>
/// <returns>是否正在运行</returns>
public static bool IsWebServerRunning(string name)
{
name = name.ToLowerInvariant();
return _webServers.ContainsKey(name);
}
/// <summary>
/// 销毁 HTTP 服务端。若服务端正在运行,可能会引发异常。
/// </summary>
/// <param name="name">服务端名称</param>
/// <returns>是否成功销毁,若名称不存在或已经销毁则返回 <c>false</c></returns>
public static bool DisposeWebServer(string name)
{
name = name.ToLowerInvariant();
lock (_webServers)
{
if (!_webServers.ContainsKey(name))
return false;
try
{
_webServers[name].Dispose();
}
catch (ObjectDisposedException ex)
{
return false;
}
_webServers.Remove(name);
return true;
}
}
#region 网页登录回调
public class NaidCallbackServer : HttpServer
{
private readonly OAuthComplete _completeCallback;
private readonly string _picAddress;
private readonly string _serviceName;
private string _callbackContent;
private IDictionary<string, string> _callbackParameters;
private OAuthCompleteStatus _status;
public NaidCallbackServer(string serviceName, OAuthComplete completeCallback, string picAddress) : base(new[]
{ IPAddress.Parse("127.0.0.1") })
{
_serviceName = serviceName;
_completeCallback = completeCallback;
_picAddress = picAddress;
}
protected override void Init()
{
// 注册回调路由
Register(HttpMethod.Get, "/callback", HandleCallback);
Register(HttpMethod.Post, "/callback", HandleCallback);
// 注册状态路由
Register(HttpMethod.Get, "/status", HandleStatus);
// 注册资源路由
Register(HttpMethod.Get, "/assets/background", HandleBackground);
Register(HttpMethod.Get, "/assets/icon", HandleIcon);
Register(HttpMethod.Get, "/complete", HandleComplete);
}
private Task<HttpRouteResponse> HandleCallback(HttpListenerRequest request)
{
if (!request.IsLocal)
return HttpRouteResponse.Forbidden.AsTask();
var redirect = HttpRouteResponse.Redirect("/complete").AsTask();
// 解析回调 URL 参数
var parameterMap = new Dictionary<string, string>();
var query = request.Url.Query;
var queryIndex = query.IndexOf('?');
if (queryIndex != -1 && query.Length > queryIndex)
try
{
var sq = query.Substring(queryIndex + 1).Split('&');
var splitChar = new[] { '=' };
foreach (var iq in sq)
{
var q = iq.Split(splitChar, 2);
if (q.Length == 2) parameterMap[q[0]] = q[1];
}
}
catch (Exception ex)
{
_status = OAuthCompleteStatus.Failed("回调参数解析出错", ex);
return redirect;
}
_callbackParameters = parameterMap;
// 读取回调内容
if (request.HasEntityBody)
try
{
using (var reader = new StreamReader(request.InputStream, request.ContentEncoding))
{
_callbackContent = reader.ReadToEnd();
}
}
catch (Exception ex)
{
_status = OAuthCompleteStatus.Failed("读取回调内容出错", ex);
return redirect;
}
return redirect;
}
private Task<HttpRouteResponse> HandleStatus(HttpListenerRequest request)
{
if (_callbackParameters is null)
return HttpRouteResponse.NotFound.AsTask();
try
{
if (_status is null)
{
_callbackParameters["Port"] = Port.ToString();
_status = _completeCallback(true, _callbackParameters, _callbackContent);
}
else if (!_status.success)
{
ModBase.Log($"[OAuth] {_serviceName}: {_status.message}{"\r\n"}{_status.stacktrace}");
var pa = new Dictionary<string, string>();
pa["Port"] = Port.ToString();
_completeCallback(false, pa, _status.message);
}
}
catch (Exception ex)
{
_status = OAuthCompleteStatus.Failed("处理回调出错", ex);
}
return HttpRouteResponse.Json(_status).AsTask();
}
private Task<HttpRouteResponse> HandleBackground(HttpListenerRequest request)
{
if (string.IsNullOrWhiteSpace(_picAddress))
return Task.FromResult(HttpRouteResponse.NotFound);
return HttpRouteResponse
.Input(new FileStream(_picAddress, FileMode.Open, FileAccess.Read, FileShare.None, 16384, true))
.AsTask();
}
private Task<HttpRouteResponse> HandleIcon(HttpListenerRequest request)
{
return HttpRouteResponse.Input(ModBase.GetResourceStream("Images/icon.ico")).AsTask();
}
private Task<HttpRouteResponse> HandleComplete(HttpListenerRequest request)
{
return HttpRouteResponse.Input(ModBase.GetResourceStream("Resources/oauth-complete.html"), "text/html")
.AsTask();
}
}
private static readonly object changeLock = new();
private static string picAddress;
public static object BackgroundPicChangeCallback(string pic)
{
lock (changeLock)
{
picAddress = pic;
return true;
}
}
public class OAuthCompleteStatus
{
public bool success { get; set; }
public string username { get; set; }
public string message { get; set; }
public string stacktrace { get; set; }
public static OAuthCompleteStatus Complete(string username)
{
return new OAuthCompleteStatus { success = true, username = username };
}
public static OAuthCompleteStatus Failed(string message, Exception ex = null)
{
var init = new OAuthCompleteStatus();
return (init.success = false, init.message = message, init.stacktrace = ex?.ToString(), init).init;
}
}
public delegate OAuthCompleteStatus? OAuthComplete(bool success, IDictionary<string, string> parameters,
string content);
public static bool StartOAuthWaitingCallback(string serviceName, string url, OAuthComplete completeCallback)
{
if (IsWebServerRunning(serviceName))
return false;
ModBase.RunInNewThread(() =>
{
var serverPort = 0;
lock (_webServers)
{
NaidCallbackServer? server;
string currentPicAddress;
lock (changeLock)
{
currentPicAddress = picAddress;
}
server = new NaidCallbackServer(serviceName, completeCallback, currentPicAddress);
serverPort = server.Port;
ModBase.Log($"[OAuth] {serviceName}: 已开始监听 {server.Port} 端口,正在初始化路由");
// 开始响应请求
var webServiceName = $"oauth/{serviceName}";
if (DisposeWebServer(webServiceName)) ModBase.Log("[OAuth] 已关闭先前认证服务服务端");
StartWebServer(webServiceName, server);
ModBase.Log($"[OAuth] {serviceName}: 初始化完成,开始响应 HTTP 请求");
}
// 打开 OAuth URL
ModBase.OpenWebsite(url.Replace("%r", $"http://localhost:{serverPort}/callback"));
}, $"CallbackWebServerLoading/{serviceName}");
return true;
}
public static void StartNaidAuthorize(Action? completeCallback = null)
{
Exception? resultEx = null;
StartOAuthWaitingCallback("NatayarkID",
$"https://account.naids.com/oauth2/authorize?response_type=code&client_id={Secrets.NatayarkClientId}&redirect_uri=%r",
(success, parameters, content) =>
{
OAuthCompleteStatus? status;
if (!success)
{
ModMain.MyMsgBox(content, isWarn: true);
completeCallback?.Invoke();
return null;
}
var code = parameters["code"];
try
{
NatayarkProfileManager.GetNaidDataAsync(code, port: ushort.Parse(parameters["Port"])).Wait();
}
catch (AggregateException ex)
{
resultEx = ex.InnerExceptions[0];
}
if (resultEx is null)
status = OAuthCompleteStatus.Complete(NatayarkProfileManager.NaidProfile.Username);
else
status = OAuthCompleteStatus.Failed("获取用户信息失败,请尝试重新登录", resultEx);
completeCallback?.Invoke();
return status;
});
}
#endregion
}