feat: 项目初始化 + 3D方块世界原型 + AI助搭系统
CI / Go Backend (push) Canceled after 0s

初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器

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

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

AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
xyou
2026-08-08 14:07:56 +08:00
parent 9500c4c80a
commit f70b061d1a
1972 changed files with 159760 additions and 6 deletions
@@ -0,0 +1,40 @@
using System.Text.Json.Serialization;
using PCL.Core.Utils.Exts;
namespace PCL.Core.Minecraft.IdentityModel.OAuth;
public record AuthorizeResult
{
public bool IsError => !Error.IsNullOrEmpty();
/// <summary>
/// 错误类型 (e.g. invalid_request)
/// </summary>
[JsonPropertyName("error")] public string? Error { get; init; }
/// <summary>
/// 描述此错误的文本
/// </summary>
[JsonPropertyName("error_description")] public string? ErrorDescription { get; init; }
// 不用 SecureString,因为这东西依赖 DPAPI,不是最佳实践
/// <summary>
/// 访问令牌
/// </summary>
[JsonPropertyName("access_token")] public string? AccessToken { get; init; }
/// <summary>
/// 刷新令牌
/// </summary>
[JsonPropertyName("refresh_token")] public string? RefreshToken { get; init; }
/// <summary>
/// ID Token
/// </summary>
[JsonPropertyName("id_token")] public string? IdToken { get; init; }
/// <summary>
/// 令牌类型
/// </summary>
[JsonPropertyName("token_type")] public string? TokenType { get; init; }
/// <summary>
/// 过期时间
/// </summary>
[JsonPropertyName("expires_in")] public int? ExpiresIn { get; init; }
}
@@ -0,0 +1,155 @@
using System;
using System.Text;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Threading;
using PCL.Core.IO.Net.Http;
using PCL.Core.Minecraft.IdentityModel;
namespace PCL.Core.Minecraft.IdentityModel.OAuth;
/// <summary>
/// OAuth 客户端实现,配合 Polly 食用效果更佳
/// </summary>
/// <param name="options">OAuth 参数</param>
public sealed class SimpleOAuthClient(OAuthClientOptions options):IOAuthClient
{
/// <summary>
/// 获取授权 Url
/// </summary>
/// <param name="scopes">访问权限列表</param>
/// <param name="state"></param>
/// <param name="extData"></param>
/// <returns></returns>
public string GetAuthorizeUrl(string[] scopes,string state,Dictionary<string,string>? extData = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(options.Meta.AuthorizeEndpoint);
var sb = new StringBuilder();
sb.Append(options.Meta.AuthorizeEndpoint);
sb.Append($"?response_type=code&scope={Uri.EscapeDataString(string.Join(" ", scopes))}");
sb.Append($"&redirect_uri={Uri.EscapeDataString(options.RedirectUri)}");
sb.Append($"&client_id={options.ClientId}&state={state}");
if (extData is null) return sb.ToString();
foreach (var kvp in extData)
sb.Append($"&{kvp.Key}={Uri.EscapeDataString(kvp.Value)}");
return sb.ToString();
}
/// <summary>
/// 使用授权代码获取令牌
/// </summary>
/// <param name="code">授权代码</param>
/// <param name="extData">附加属性,不应该包含必须参数和预定义字段 (e.g. client_id、grant_type)</param>
/// <param name="token"></param>
/// <returns></returns>
public async Task<AuthorizeResult?> AuthorizeWithCodeAsync(
string code,CancellationToken token,Dictionary<string,string>? extData = null
)
{
extData ??= new Dictionary<string, string>();
extData["client_id"] = options.ClientId;
extData["grant_type"] = "authorization_code";
extData["code"] = code;
var client = options.GetClient.Invoke();
using var content = new FormUrlEncodedContent(extData);
using var response = await HttpRequest
.CreatePost(options.Meta.TokenEndpoint)
.WithContent(content)
.WithHeaders(options.Headers ?? [])
.SendAsync(client, cancellationToken: token)
.ConfigureAwait(false);
return await response
.AsJsonAsync<AuthorizeResult>(cancellationToken: token)
.ConfigureAwait(false);
}
/// <summary>
/// 获取设备代码对
/// </summary>
/// <param name="scopes"></param>
/// <param name="token"></param>
/// <param name="extData"></param>
/// <returns></returns>
public async Task<DeviceCodeData?> GetCodePairAsync
(string[] scopes,CancellationToken token, Dictionary<string, string>? extData = null)
{
ArgumentException.ThrowIfNullOrEmpty(options.Meta.DeviceEndpoint);
var client = options.GetClient.Invoke();
extData ??= new Dictionary<string, string>();
extData["scope"] = string.Join(" ", scopes);
extData["client_id"] = options.ClientId;
var content = new FormUrlEncodedContent(extData);
using var response = await HttpRequest
.CreatePost(options.Meta.DeviceEndpoint)
.WithContent(content)
.WithHeaders(options.Headers ?? [])
.SendAsync(client, cancellationToken: token)
.ConfigureAwait(false);
return await response
.AsJsonAsync<DeviceCodeData>(cancellationToken: token)
.ConfigureAwait(false);
}
/// <summary>
/// 验证用户授权状态 <br/>
/// 注:此方法不会检查是否过去了 Interval 秒,请自行处理
/// </summary>
/// <param name="data"></param>
/// <param name="token"></param>
/// <param name="extData"></param>
/// <returns></returns>
/// <exception cref="IdentityModelAuthenticationException">认证服务器返回设备授权错误</exception>
public async Task<AuthorizeResult?> AuthorizeWithDeviceAsync
(DeviceCodeData data,CancellationToken token,Dictionary<string,string>? extData = null)
{
if (data.IsError) throw new IdentityModelAuthenticationException(data.Error, data.ErrorDescription);
var client = options.GetClient.Invoke();
extData ??= new Dictionary<string, string>();
extData["client_id"] = options.ClientId;
extData["grant_type"] = "urn:ietf:params:oauth:grant-type:device_code";
extData["device_code"] = data.DeviceCode!;
using var content = new FormUrlEncodedContent(extData);
using var response = await HttpRequest
.CreatePost(options.Meta.TokenEndpoint)
.WithContent(content)
.WithHeaders(options.Headers ?? [])
.SendAsync(client, cancellationToken: token)
.ConfigureAwait(false);
return await response
.AsJsonAsync<AuthorizeResult>(cancellationToken: token)
.ConfigureAwait(false);
}
/// <summary>
/// 刷新登录
/// </summary>
/// <param name="data"></param>
/// <param name="token"></param>
/// <param name="extData"></param>
/// <returns></returns>
/// <exception cref="IdentityModelAuthenticationException">认证服务器返回刷新授权错误</exception>
public async Task<AuthorizeResult?> AuthorizeWithSilentAsync
(AuthorizeResult data,CancellationToken token,Dictionary<string,string>? extData = null)
{
var client = options.GetClient.Invoke();
if (data.IsError) throw new IdentityModelAuthenticationException(data.Error, data.ErrorDescription);
extData ??= [];
extData["refresh_token"] = data.RefreshToken!;
extData["grant_type"] = "refresh_token";
extData["client_id"] = options.ClientId;
using var content = new FormUrlEncodedContent(extData);
using var response = await HttpRequest
.CreatePost(options.Meta.TokenEndpoint)
.WithHeaders(options.Headers ?? [])
.WithContent(content)
.SendAsync(client, cancellationToken: token)
.ConfigureAwait(false);
return await response
.AsJsonAsync<AuthorizeResult>(cancellationToken: token)
.ConfigureAwait(false);
}
}
@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
namespace PCL.Core.Minecraft.IdentityModel.OAuth;
public record OAuthClientOptions
{
/// <summary>
/// 请求头
/// </summary>
public Dictionary<string,string>? Headers { get; set; }
/// <summary>
/// 端点数据
/// </summary>
public required EndpointMeta Meta { get; set; }
public required Func<HttpClient> GetClient { get; set; }
/// <summary>
/// 重定向 Uri
/// </summary>
public required string RedirectUri { get; set; }
/// <summary>
/// 客户端 ID
/// </summary>
public required string ClientId { get; set; }
}
@@ -0,0 +1,49 @@
using System.Text.Json.Serialization;
using PCL.Core.Utils.Exts;
namespace PCL.Core.Minecraft.IdentityModel.OAuth;
public record DeviceCodeData
{
public bool IsError => !Error.IsNullOrEmpty();
/// <summary>
/// 错误类型
/// </summary>
[JsonPropertyName("error")]
public string? Error { get; init; }
/// <summary>
/// 错误描述
/// </summary>
[JsonPropertyName("error_description")]
public string? ErrorDescription { get; init; }
/// <summary>
/// 用户授权码
/// </summary>
[JsonPropertyName("user_code")]
public string? UserCode { get; init; }
/// <summary>
/// 设备授权码
/// </summary>
[JsonPropertyName("device_code")]
public string? DeviceCode { get; init; }
/// <summary>
/// 验证 Uri
/// </summary>
[JsonPropertyName("verification_uri")]
public string? VerificationUri { get; init; }
/// <summary>
/// 验证 Uri (自动填充代码)
/// </summary>
[JsonPropertyName("verification_uri_complete")]
public string? VerificationUriComplete { get; init; }
/// <summary>
/// 轮询间隔
/// </summary>
[JsonPropertyName("interval")]
public int? Interval { get; init; }
/// <summary>
/// 过期时间
/// </summary>
[JsonPropertyName("expires_in")]
public int? ExpiresIn { get; init; }
}
@@ -0,0 +1,17 @@
namespace PCL.Core.Minecraft.IdentityModel.OAuth;
public record EndpointMeta
{
/// <summary>
/// 设备授权端点
/// </summary>
public string? DeviceEndpoint { get; set; }
/// <summary>
/// 授权端点
/// </summary>
public required string AuthorizeEndpoint { get; set; }
/// <summary>
/// 令牌端点
/// </summary>
public required string TokenEndpoint { get; set; }
}
@@ -0,0 +1,14 @@
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
namespace PCL.Core.Minecraft.IdentityModel.OAuth;
public interface IOAuthClient
{
public string GetAuthorizeUrl(string[] scopes,string state,Dictionary<string,string>? extData);
public Task<AuthorizeResult?> AuthorizeWithCodeAsync(string code,CancellationToken token,Dictionary<string,string>? extData = null);
public Task<DeviceCodeData?> GetCodePairAsync(string[] scopes,CancellationToken token, Dictionary<string, string>? extData = null);
public Task<AuthorizeResult?> AuthorizeWithDeviceAsync(DeviceCodeData data,CancellationToken token,Dictionary<string,string>? extData = null);
public Task<AuthorizeResult?> AuthorizeWithSilentAsync(AuthorizeResult data,CancellationToken token,Dictionary<string,string>? extData = null);
}