mirror of
https://github.com/Megghy/MegghysAPI.git
synced 2025-12-06 14:16:56 +08:00
fix: 修复文件列表为空时的返回值处理并添加图像处理相关依赖
This commit is contained in:
136
Controllers/BiliController.cs
Normal file
136
Controllers/BiliController.cs
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
using MegghysAPI.Modules;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
|
||||||
|
namespace MegghysAPI.Controllers
|
||||||
|
{
|
||||||
|
[Route("api/bili")]
|
||||||
|
[ApiController]
|
||||||
|
public class BiliController : MControllerBase
|
||||||
|
{
|
||||||
|
[HttpGet("test-cap")]
|
||||||
|
public async Task<IActionResult> TestCap([FromQuery] string gt, [FromQuery] string challenge)
|
||||||
|
{
|
||||||
|
var solution = await CapResolver.SolveGeetestAsync("https://www.bilibili.com", gt, challenge);
|
||||||
|
return Ok(ResponseOK(solution));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("resolve-grisk-id")]
|
||||||
|
public async Task<IActionResult> ResolveGriskId([FromQuery] string v_voucher, [FromQuery] string? csrf)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(v_voucher))
|
||||||
|
{
|
||||||
|
return BadRequest(ResponseBadRequest("v_voucher 不能为空"));
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 1. 调用B站注册接口获取 gt, challenge, token
|
||||||
|
var (gt, challenge, token) = await RegisterFromVVoucher(v_voucher, csrf);
|
||||||
|
|
||||||
|
// 2. 使用 CapResolver 解决验证码, 获取 validate
|
||||||
|
var solution = await CapResolver.SolveGeetestAsync("https://live.bilibili.com", gt, challenge);
|
||||||
|
if (solution == null)
|
||||||
|
{
|
||||||
|
throw new Exception("从 CapSolver 获取 validate 失败");
|
||||||
|
}
|
||||||
|
string validate = solution.Validate;
|
||||||
|
|
||||||
|
// 3. 调用B站验证接口获取 grisk_id
|
||||||
|
var griskId = await ValidateToGriskId(challenge, token, validate, csrf);
|
||||||
|
|
||||||
|
return Ok(ResponseOK(new { grisk_id = griskId }));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logs.Error($"ResolveGriskId 失败: {ex.Message}");
|
||||||
|
return StatusCode(500, ResponseInternalError(ex.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public const string FirstHost = "https://i.ukamnads.icu/api-bilibili/";
|
||||||
|
public const string SecondHost = "https://api.bilibili.com/";
|
||||||
|
private static readonly string[] PreferredHosts = { FirstHost, SecondHost };
|
||||||
|
|
||||||
|
private async Task<(string gt, string challenge, string token)> RegisterFromVVoucher(string vVoucher, string? csrf)
|
||||||
|
{
|
||||||
|
var path = "x/gaia-vgate/v1/register";
|
||||||
|
var payload = new Dictionary<string, string> { { "v_voucher", vVoucher } };
|
||||||
|
if (!string.IsNullOrEmpty(csrf))
|
||||||
|
{
|
||||||
|
payload["csrf"] = csrf;
|
||||||
|
}
|
||||||
|
|
||||||
|
var root = await PostFormWithFailoverAsync(path, payload, "注册接口");
|
||||||
|
var data = root["data"] ?? throw new Exception("注册接口返回缺少 data 字段");
|
||||||
|
|
||||||
|
var geetest = data["geetest"] ?? throw new Exception("注册返回缺少 geetest 字段");
|
||||||
|
var gt = (string?)geetest["gt"] ?? throw new Exception("注册返回缺少 gt 字段");
|
||||||
|
var challenge = (string?)geetest["challenge"] ?? throw new Exception("注册返回缺少 challenge 字段");
|
||||||
|
var apiToken = (string?)data["token"] ?? throw new Exception("注册返回缺少 token 字段");
|
||||||
|
|
||||||
|
return (gt, challenge, apiToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> ValidateToGriskId(string challenge, string token, string validate, string? csrf)
|
||||||
|
{
|
||||||
|
var path = "x/gaia-vgate/v1/validate";
|
||||||
|
var seccode = $"{validate}|jordan";
|
||||||
|
var payload = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "challenge", challenge },
|
||||||
|
{ "token", token },
|
||||||
|
{ "validate", validate },
|
||||||
|
{ "seccode", seccode },
|
||||||
|
};
|
||||||
|
if (!string.IsNullOrEmpty(csrf))
|
||||||
|
{
|
||||||
|
payload["csrf"] = csrf;
|
||||||
|
}
|
||||||
|
|
||||||
|
var root = await PostFormWithFailoverAsync(path, payload, "验证接口");
|
||||||
|
var data = root["data"] ?? throw new Exception("验证接口返回缺少 data 字段");
|
||||||
|
|
||||||
|
var isValid = (int?)data["is_valid"];
|
||||||
|
var griskId = (string?)data["grisk_id"];
|
||||||
|
|
||||||
|
if (isValid != 1 || string.IsNullOrEmpty(griskId))
|
||||||
|
{
|
||||||
|
throw new Exception($"验证失败或缺少 grisk_id: is_valid={isValid} grisk_id={griskId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return griskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<JsonNode> PostFormWithFailoverAsync(string path, Dictionary<string, string> payload, string operation)
|
||||||
|
{
|
||||||
|
Exception? lastException = null;
|
||||||
|
foreach (var host in PreferredHosts)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var content = new FormUrlEncodedContent(payload);
|
||||||
|
var response = await Utils._client.PostAsync($"{host}{path}", content);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var json = await response.Content.ReadAsStringAsync();
|
||||||
|
return JsonNode.Parse(json) ?? throw new Exception($"{operation} 返回内容解析失败");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
lastException = ex;
|
||||||
|
if (host == FirstHost)
|
||||||
|
{
|
||||||
|
Logs.Warn($"{operation} 使用 FirstHost 调用失败: {ex.Message}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logs.Error($"{operation} 使用 SecondHost 调用失败: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception($"{operation} 调用失败: {lastException?.Message}", lastException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
Controllers/MyController.cs
Normal file
12
Controllers/MyController.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using MegghysAPI.Modules;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace MegghysAPI.Controllers
|
||||||
|
{
|
||||||
|
[Route("api/my")]
|
||||||
|
[ApiController]
|
||||||
|
public class MyController : MControllerBase
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -109,6 +109,10 @@ namespace MegghysAPI.Core
|
|||||||
.WithIncludeUserMetadata(includeUserMetadata));
|
.WithIncludeUserMetadata(includeUserMetadata));
|
||||||
|
|
||||||
var fileList = new List<Minio.DataModel.Item>();
|
var fileList = new List<Minio.DataModel.Item>();
|
||||||
|
if(fileList.Count == 0)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
await foreach (var item in result)
|
await foreach (var item in result)
|
||||||
{
|
{
|
||||||
fileList.Add(item);
|
fileList.Add(item);
|
||||||
|
|||||||
BIN
Data/siamese.onnx
Normal file
BIN
Data/siamese.onnx
Normal file
Binary file not shown.
BIN
Data/yolov11n_captcha.onnx
Normal file
BIN
Data/yolov11n_captcha.onnx
Normal file
Binary file not shown.
@@ -10,8 +10,11 @@
|
|||||||
<PackageReference Include="FreeSql.Extensions.JsonMap" Version="3.5.213" />
|
<PackageReference Include="FreeSql.Extensions.JsonMap" Version="3.5.213" />
|
||||||
<PackageReference Include="FreeSql.Provider.PostgreSQL" Version="3.5.213" />
|
<PackageReference Include="FreeSql.Provider.PostgreSQL" Version="3.5.213" />
|
||||||
<PackageReference Include="Masuit.Tools.Core" Version="2025.5.1" />
|
<PackageReference Include="Masuit.Tools.Core" Version="2025.5.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.9" />
|
||||||
|
<PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.23.0" />
|
||||||
<PackageReference Include="Minio" Version="6.0.5" />
|
<PackageReference Include="Minio" Version="6.0.5" />
|
||||||
<PackageReference Include="AntDesign" Version="1.4.3" />
|
<PackageReference Include="AntDesign" Version="1.4.3" />
|
||||||
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
400
Modules/BiliCaptchaResolver.cs
Normal file
400
Modules/BiliCaptchaResolver.cs
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Linq;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
using CaptchaBreaker;
|
||||||
|
using MegghysAPI;
|
||||||
|
|
||||||
|
namespace MegghysAPI.Modules
|
||||||
|
{
|
||||||
|
public enum VerifyType
|
||||||
|
{
|
||||||
|
Click,
|
||||||
|
Slide
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Geetest 点选验证码解析器(C# 版)。
|
||||||
|
/// - 对应 Rust 版本的 Click 结构实现:注册、拉取参数、计算 key、生成 w、验证、刷新。
|
||||||
|
/// - 依赖 CaptchaBreaker.ChineseClick0(ONNX 模型)。
|
||||||
|
public sealed class BiliCaptchaResolver : IDisposable
|
||||||
|
{
|
||||||
|
private readonly VerifyType _verifyType;
|
||||||
|
private readonly ChineseClick0 _cb;
|
||||||
|
|
||||||
|
private BiliCaptchaResolver(string yoloModelPath, string siameseModelPath, VerifyType verifyType = VerifyType.Click)
|
||||||
|
{
|
||||||
|
_verifyType = verifyType;
|
||||||
|
_cb = new ChineseClick0(yoloModelPath, siameseModelPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<BiliCaptchaResolver> CreateAsync(string yoloModelName = "yolov11n_captcha.onnx", string siameseModelName = "siamese.onnx", VerifyType verifyType = VerifyType.Click)
|
||||||
|
{
|
||||||
|
var yoloPath = await EnsureModelExistsAsync(yoloModelName);
|
||||||
|
var siamesePath = await EnsureModelExistsAsync(siameseModelName);
|
||||||
|
return new BiliCaptchaResolver(yoloPath, siamesePath, verifyType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<string> EnsureModelExistsAsync(string modelName)
|
||||||
|
{
|
||||||
|
var modelPath = Path.Combine(Datas.DataPath, modelName);
|
||||||
|
if (File.Exists(modelPath))
|
||||||
|
{
|
||||||
|
return modelPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
var url = $"https://oss.suki.club/things/{modelName}";
|
||||||
|
var client = Utils._client;
|
||||||
|
var modelBytes = await client.GetByteArrayAsync(url);
|
||||||
|
await File.WriteAllBytesAsync(modelPath, modelBytes);
|
||||||
|
return modelPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_cb?.Dispose();
|
||||||
|
}
|
||||||
|
// ========== Public APIs (Async) ==========
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从业务 URL 注册,返回 (gt, challenge)。
|
||||||
|
/// </summary>
|
||||||
|
public async Task<(string gt, string challenge)> RegisterTestAsync(string url, string? proxy = null)
|
||||||
|
{
|
||||||
|
var http = await ProxyManager.GetHttpClientAsync(proxy);
|
||||||
|
var text = await http.GetStringAsync(url);
|
||||||
|
var root = JsonNode.Parse(text);
|
||||||
|
var geetest = root?["data"]?["geetest"] ?? throw new Exception("缺少 data.geetest");
|
||||||
|
var gt = (string?)geetest["gt"] ?? throw new Exception("缺少 gt");
|
||||||
|
var challenge = (string?)geetest["challenge"] ?? throw new Exception("缺少 challenge");
|
||||||
|
return (gt, challenge);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取初始 c/s 参数,对应 Rust 版本的 get_c_s 方法。
|
||||||
|
/// </summary>
|
||||||
|
public async Task<(byte[] c, string s)> GetCSAsync(string gt, string challenge, string? proxy = null)
|
||||||
|
{
|
||||||
|
var url = "http://api.geevisit.com/get.php";
|
||||||
|
var callback = "geetest_1717911889779";
|
||||||
|
var query = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["gt"] = gt,
|
||||||
|
["challenge"] = challenge,
|
||||||
|
["callback"] = callback,
|
||||||
|
};
|
||||||
|
|
||||||
|
var http = await ProxyManager.GetHttpClientAsync(proxy);
|
||||||
|
var full = BuildUrl(url, query);
|
||||||
|
var jsonp = await http.GetStringAsync(full);
|
||||||
|
var json = UnwrapJsonp(jsonp, callback);
|
||||||
|
|
||||||
|
var data = JsonNode.Parse(json)?["data"] ?? throw new Exception("缺少 data");
|
||||||
|
|
||||||
|
// c
|
||||||
|
var cList = data["c"]?.AsArray().Select(v => (byte)v!.GetValue<int>()).ToArray() ?? [];
|
||||||
|
var s = (string?)data["s"] ?? throw new Exception("缺少 s");
|
||||||
|
|
||||||
|
return (cList, s);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取验证类型,对应 Rust 版本的 get_type 方法。
|
||||||
|
/// </summary>
|
||||||
|
public async Task<string> GetTypeAsync(string gt, string challenge, string? proxy = null)
|
||||||
|
{
|
||||||
|
var url = "http://api.geevisit.com/gettype.php";
|
||||||
|
var callback = "geetest_1717934072177";
|
||||||
|
var query = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["gt"] = gt,
|
||||||
|
["challenge"] = challenge,
|
||||||
|
["callback"] = callback,
|
||||||
|
};
|
||||||
|
|
||||||
|
var http = await ProxyManager.GetHttpClientAsync(proxy);
|
||||||
|
var full = BuildUrl(url, query);
|
||||||
|
var jsonp = await http.GetStringAsync(full);
|
||||||
|
var json = UnwrapJsonp(jsonp, callback);
|
||||||
|
|
||||||
|
var data = JsonNode.Parse(json)?["data"] ?? throw new Exception("缺少 data");
|
||||||
|
return (string?)data["type"] ?? throw new Exception("缺少 type");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取 new c/s/args(图片地址)。
|
||||||
|
/// </summary>
|
||||||
|
public async Task<(byte[] c, string s, string picUrl)> GetNewCSArgsAsync(string gt, string challenge, string? proxy = null)
|
||||||
|
{
|
||||||
|
var url = "http://api.geevisit.com/get.php";
|
||||||
|
var callback = "geetest_1717911889779";
|
||||||
|
var query = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["gt"] = gt,
|
||||||
|
["challenge"] = challenge,
|
||||||
|
["is_next"] = "true",
|
||||||
|
["offline"] = "false",
|
||||||
|
["isPC"] = "true",
|
||||||
|
["callback"] = callback,
|
||||||
|
["type"] = _verifyType == VerifyType.Click ? "click" : "slide"
|
||||||
|
};
|
||||||
|
|
||||||
|
var http = await ProxyManager.GetHttpClientAsync(proxy);
|
||||||
|
var full = BuildUrl(url, query);
|
||||||
|
var jsonp = await http.GetStringAsync(full);
|
||||||
|
var json = UnwrapJsonp(jsonp, callback);
|
||||||
|
|
||||||
|
var data = JsonNode.Parse(json)?["data"] ?? throw new Exception("缺少 data");
|
||||||
|
|
||||||
|
// c
|
||||||
|
var cList = data["c"]?.AsArray().Select(v => (byte)v!.GetValue<int>()).ToArray() ?? [];
|
||||||
|
|
||||||
|
var s = (string?)data["s"] ?? throw new Exception("缺少 s");
|
||||||
|
var staticServers = data["static_servers"]?.AsArray() ?? throw new Exception("缺少 static_servers");
|
||||||
|
if (staticServers.Count == 0) throw new Exception("static_servers里面为空");
|
||||||
|
var staticServer = (string?)staticServers[0] ?? throw new Exception("static_servers[0] 不是字符串");
|
||||||
|
var pic = (string?)data["pic"] ?? throw new Exception("缺少 pic");
|
||||||
|
var picUrl = $"https://{staticServer}{pic.TrimStart('/')}";
|
||||||
|
|
||||||
|
return (cList, s, picUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 提交验证,可带 w。
|
||||||
|
/// </summary>
|
||||||
|
public async Task<(string result, string validate)> VerifyAsync(string gt, string challenge, string? w = null, string? proxy = null)
|
||||||
|
{
|
||||||
|
var url = "http://api.geevisit.com/ajax.php";
|
||||||
|
var callback = "geetest_1717918222610";
|
||||||
|
var query = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["gt"] = gt,
|
||||||
|
["challenge"] = challenge,
|
||||||
|
["callback"] = callback,
|
||||||
|
};
|
||||||
|
if (!string.IsNullOrEmpty(w)) query["w"] = w!;
|
||||||
|
|
||||||
|
var http = await ProxyManager.GetHttpClientAsync(proxy);
|
||||||
|
var full = BuildUrl(url, query);
|
||||||
|
var jsonp = await http.GetStringAsync(full);
|
||||||
|
var json = UnwrapJsonp(jsonp, callback);
|
||||||
|
|
||||||
|
var data = JsonNode.Parse(json)?["data"] ?? throw new Exception("缺少 data");
|
||||||
|
var result = (string?)data["result"] ?? throw new Exception("缺少 result");
|
||||||
|
var validate = (string?)data["validate"] ?? throw new Exception("缺少 validate");
|
||||||
|
return (result, validate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 刷新获取新的图片地址。
|
||||||
|
/// </summary>
|
||||||
|
public async Task<string> RefreshAsync(string gt, string challenge, string? proxy = null)
|
||||||
|
{
|
||||||
|
var url = "http://api.geevisit.com/refresh.php";
|
||||||
|
var callback = "geetest_1717918222610";
|
||||||
|
var query = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["gt"] = gt,
|
||||||
|
["challenge"] = challenge,
|
||||||
|
["callback"] = callback,
|
||||||
|
};
|
||||||
|
|
||||||
|
var http = await ProxyManager.GetHttpClientAsync(proxy);
|
||||||
|
var full = BuildUrl(url, query);
|
||||||
|
var jsonp = await http.GetStringAsync(full);
|
||||||
|
var json = UnwrapJsonp(jsonp, callback);
|
||||||
|
|
||||||
|
var data = JsonNode.Parse(json)?["data"] ?? throw new Exception("缺少 data");
|
||||||
|
var servers = data["image_servers"]?.AsArray() ?? throw new Exception("缺少 image_servers");
|
||||||
|
if (servers.Count == 0) throw new Exception("image_servers里面为空");
|
||||||
|
var imageServer = (string?)servers[0] ?? throw new Exception("image_servers[0] 不是字符串");
|
||||||
|
var pic = (string?)data["pic"] ?? throw new Exception("缺少 pic");
|
||||||
|
return $"https://{imageServer}{pic.TrimStart('/')}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 计算 key(坐标串),会下载图片并通过 ONNX 模型推理。
|
||||||
|
/// </summary>
|
||||||
|
public async Task<string> CalculateKeyAsync(string picUrl, string? proxy = null)
|
||||||
|
{
|
||||||
|
var http = await ProxyManager.GetHttpClientAsync(proxy);
|
||||||
|
var bytes = await http.GetByteArrayAsync(picUrl);
|
||||||
|
var raw = Image.Load<Rgba32>(bytes);
|
||||||
|
|
||||||
|
// 保障尺寸不超过 384
|
||||||
|
List<PointF> points;
|
||||||
|
if (raw.Width > 384 || raw.Height > 384)
|
||||||
|
{
|
||||||
|
var scale = Math.Min(384f / raw.Width, 384f / raw.Height);
|
||||||
|
var w = (int)MathF.Ceiling(raw.Width * scale);
|
||||||
|
var h = (int)MathF.Ceiling(raw.Height * scale);
|
||||||
|
var resized = raw.Clone(ctx => ctx.Resize(w, h));
|
||||||
|
points = _cb.Run(resized);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
points = _cb.Run(raw);
|
||||||
|
}
|
||||||
|
var list = new List<string>(points.Count);
|
||||||
|
foreach (var p in points)
|
||||||
|
{
|
||||||
|
// 与 Rust 版本保持一致的缩放与取整
|
||||||
|
var x = MathF.Round(p.X / 333.375f * 100f * 100f);
|
||||||
|
var y = MathF.Round(p.Y / 333.375f * 100f * 100f);
|
||||||
|
list.Add($"{x}_{y}");
|
||||||
|
}
|
||||||
|
return string.Join(",", list);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成 w。注意:目前使用占位实现,请按需替换为真实算法。
|
||||||
|
/// </summary>
|
||||||
|
public string GenerateW(string key, string gt, string challenge, ReadOnlySpan<byte> c, string s)
|
||||||
|
{
|
||||||
|
return ClickCalculate(key, gt, challenge, c, s);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 完整流程测试:注册 -> get_c_s -> get_type -> 拉取 c/s/args -> 计算 key -> 生成 w -> 提交 -> 返回 validate。
|
||||||
|
/// 按照原始 Rust 版本流程顺序,sleep 保证总耗时 >= 2s。
|
||||||
|
/// </summary>
|
||||||
|
public async Task<string> TestAsync(string registerUrl, string? proxy = null, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var (gt, challenge) = await RegisterTestAsync(registerUrl, proxy);
|
||||||
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
|
||||||
|
// 按照原始 Rust test 流程:先调用 get_c_s,然后 get_type
|
||||||
|
var (_c, _s) = await GetCSAsync(gt, challenge, proxy);
|
||||||
|
var _type = await GetTypeAsync(gt, challenge, proxy);
|
||||||
|
|
||||||
|
await Task.Delay(new Random().Next(500, 1000), ct);
|
||||||
|
var v = await VerifyAsync(gt, challenge);
|
||||||
|
|
||||||
|
|
||||||
|
var (c, s, args) = await GetNewCSArgsAsync(gt, challenge, proxy);
|
||||||
|
var key = await CalculateKeyAsync(args, proxy);
|
||||||
|
var w = GenerateW(key, gt, challenge, c, s);
|
||||||
|
|
||||||
|
// 至少 2 秒
|
||||||
|
var remain = 2000 - sw.ElapsedMilliseconds;
|
||||||
|
if (remain > 0)
|
||||||
|
{
|
||||||
|
await Task.Delay((int)remain, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
var (_, validate) = await VerifyAsync(gt, challenge, w, proxy);
|
||||||
|
return validate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 简单匹配(不注册):直接使用已知 gt, challenge,按照原始流程顺序。
|
||||||
|
/// </summary>
|
||||||
|
public async Task<string> SimpleMatchAsync(string gt, string challenge, string? proxy = null, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
|
||||||
|
// 按照原始 Rust simple_match 流程:先调用 get_c_s,然后 get_type
|
||||||
|
var (_, _) = await GetCSAsync(gt, challenge, proxy);
|
||||||
|
var __ = await GetTypeAsync(gt, challenge, proxy);
|
||||||
|
|
||||||
|
var (c, s, args) = await GetNewCSArgsAsync(gt, challenge, proxy);
|
||||||
|
var key = await CalculateKeyAsync(args, proxy);
|
||||||
|
var w = GenerateW(key, gt, challenge, c, s);
|
||||||
|
|
||||||
|
var remain = 2000 - sw.ElapsedMilliseconds;
|
||||||
|
if (remain > 0) await Task.Delay((int)remain, ct);
|
||||||
|
|
||||||
|
var (_, validate) = await VerifyAsync(gt, challenge, w, proxy);
|
||||||
|
return validate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 带自动重试的简单匹配,按照原始流程顺序。
|
||||||
|
/// </summary>
|
||||||
|
public async Task<string> SimpleMatchRetryAsync(string gt, string challenge, string? proxy = null, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
// 按照原始 Rust simple_match_retry 流程:先调用 get_c_s,然后 get_type
|
||||||
|
var (_, _) = await GetCSAsync(gt, challenge, proxy);
|
||||||
|
var __ = await GetTypeAsync(gt, challenge, proxy);
|
||||||
|
|
||||||
|
var (c, s, args) = await GetNewCSArgsAsync(gt, challenge, proxy);
|
||||||
|
var res = await TryOnceAsync(gt, challenge, c, s, args, proxy, ct);
|
||||||
|
if (res.success) return res.validate!;
|
||||||
|
|
||||||
|
while (!ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var nextArgs = await RefreshAsync(gt, challenge, proxy);
|
||||||
|
res = await TryOnceAsync(gt, challenge, c, s, nextArgs, proxy, ct);
|
||||||
|
if (res.success) return res.validate!;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new OperationCanceledException("已取消");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Private Helpers ==========
|
||||||
|
|
||||||
|
private async Task<(bool success, string? validate)> TryOnceAsync(
|
||||||
|
string gt, string challenge, byte[] c, string s, string args, string? proxy, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var start = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var key = await CalculateKeyAsync(args, proxy);
|
||||||
|
var w = GenerateW(key, gt, challenge, c, s);
|
||||||
|
var remain = 2000 - start.ElapsedMilliseconds;
|
||||||
|
if (remain > 0) await Task.Delay((int)remain, ct);
|
||||||
|
|
||||||
|
var (_, validate) = await VerifyAsync(gt, challenge, w, proxy);
|
||||||
|
return (true, validate);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return (false, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static string BuildUrl(string baseUrl, IReadOnlyDictionary<string, string> query)
|
||||||
|
{
|
||||||
|
var ub = new UriBuilder(baseUrl);
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
if (!string.IsNullOrEmpty(ub.Query))
|
||||||
|
{
|
||||||
|
sb.Append(ub.Query.TrimStart('?'));
|
||||||
|
if (sb.Length > 0) sb.Append('&');
|
||||||
|
}
|
||||||
|
bool first = true;
|
||||||
|
foreach (var kv in query)
|
||||||
|
{
|
||||||
|
if (!first) sb.Append('&');
|
||||||
|
first = false;
|
||||||
|
sb.Append(Uri.EscapeDataString(kv.Key));
|
||||||
|
sb.Append('=');
|
||||||
|
sb.Append(Uri.EscapeDataString(kv.Value));
|
||||||
|
}
|
||||||
|
ub.Query = sb.ToString();
|
||||||
|
return ub.Uri.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string UnwrapJsonp(string jsonp, string callback)
|
||||||
|
{
|
||||||
|
var prefix = callback + "(";
|
||||||
|
if (!jsonp.StartsWith(prefix)) throw new Exception("前缀错误");
|
||||||
|
if (!jsonp.EndsWith(")")) throw new Exception("后缀错误");
|
||||||
|
return jsonp.Substring(prefix.Length, jsonp.Length - prefix.Length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ClickCalculate(string key, string gt, string challenge, ReadOnlySpan<byte> c, string s)
|
||||||
|
{
|
||||||
|
_ = c;
|
||||||
|
_ = s;
|
||||||
|
return GeetestCrypto.ClickCalculate(key, gt, challenge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
161
Modules/CapResolver.cs
Normal file
161
Modules/CapResolver.cs
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Threading;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace MegghysAPI.Modules
|
||||||
|
{
|
||||||
|
public static class CapResolver
|
||||||
|
{
|
||||||
|
private const string ApiKey = "CAP-5537BB057BE85FC11F04BB5BB6E80E73AD44279056AC22D0E70664B24964C3D9";
|
||||||
|
private const string CreateTaskUrl = "https://api.capsolver.com/createTask";
|
||||||
|
private const string GetTaskResultUrl = "https://api.capsolver.com/getTaskResult";
|
||||||
|
private static readonly HttpClient HttpClient = new HttpClient();
|
||||||
|
|
||||||
|
private class CreateTaskRequest
|
||||||
|
{
|
||||||
|
[JsonPropertyName("clientKey")]
|
||||||
|
public string ClientKey { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("task")]
|
||||||
|
public object Task { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class GeetestTask
|
||||||
|
{
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string Type { get; set; } = "GeeTestTaskProxyLess";
|
||||||
|
|
||||||
|
[JsonPropertyName("websiteURL")]
|
||||||
|
public string WebsiteURL { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("gt")]
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public string Gt { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("challenge")]
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public string Challenge { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("captchaId")]
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public string CaptchaId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("geetestApiServerSubdomain")]
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public string GeetestApiServerSubdomain { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CreateTaskResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("errorId")]
|
||||||
|
public int ErrorId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("status")]
|
||||||
|
public string Status { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("taskId")]
|
||||||
|
public string TaskId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class GetTaskResultRequest
|
||||||
|
{
|
||||||
|
[JsonPropertyName("clientKey")]
|
||||||
|
public string ClientKey { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("taskId")]
|
||||||
|
public string TaskId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class GetTaskResultResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("errorId")]
|
||||||
|
public int ErrorId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("status")]
|
||||||
|
public string Status { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("solution")]
|
||||||
|
public GeetestSolution Solution { get; set; }
|
||||||
|
}
|
||||||
|
public class GeetestSolution
|
||||||
|
{
|
||||||
|
[JsonPropertyName("challenge")]
|
||||||
|
public string Challenge { get; set; }
|
||||||
|
[JsonPropertyName("validate")]
|
||||||
|
public string Validate { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<GeetestSolution?> SolveGeetestAsync(string websiteUrl, string gt = null, string challenge = null, string captchaId = null, string apiServerSubdomain = null)
|
||||||
|
{
|
||||||
|
var geetestTask = new GeetestTask
|
||||||
|
{
|
||||||
|
WebsiteURL = websiteUrl,
|
||||||
|
Gt = gt,
|
||||||
|
Challenge = challenge,
|
||||||
|
CaptchaId = captchaId,
|
||||||
|
GeetestApiServerSubdomain = apiServerSubdomain
|
||||||
|
};
|
||||||
|
|
||||||
|
var createTaskRequest = new CreateTaskRequest
|
||||||
|
{
|
||||||
|
ClientKey = ApiKey,
|
||||||
|
Task = geetestTask
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await HttpClient.PostAsJsonAsync(CreateTaskUrl, createTaskRequest);
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
// 可以添加日志记录
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var createTaskResponse = await response.Content.ReadFromJsonAsync<CreateTaskResponse>();
|
||||||
|
if (createTaskResponse == null || createTaskResponse.ErrorId != 0 || string.IsNullOrEmpty(createTaskResponse.TaskId))
|
||||||
|
{
|
||||||
|
// 可以添加日志记录
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var getTaskResultRequest = new GetTaskResultRequest
|
||||||
|
{
|
||||||
|
ClientKey = ApiKey,
|
||||||
|
TaskId = createTaskResponse.TaskId
|
||||||
|
};
|
||||||
|
|
||||||
|
await Task.Delay(3000);
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
await Task.Delay(1000);
|
||||||
|
|
||||||
|
var resultResponse = await HttpClient.PostAsJsonAsync(GetTaskResultUrl, getTaskResultRequest);
|
||||||
|
if (!resultResponse.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
// 可以添加日志记录
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var getTaskResultResponse = await resultResponse.Content.ReadFromJsonAsync<GetTaskResultResponse>();
|
||||||
|
if (getTaskResultResponse == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getTaskResultResponse.Status == "ready")
|
||||||
|
{
|
||||||
|
return getTaskResultResponse.Solution;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getTaskResultResponse.Status == "failed" || getTaskResultResponse.ErrorId != 0)
|
||||||
|
{
|
||||||
|
// 可以添加日志记录
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
453
Modules/CaptchaClick.cs
Normal file
453
Modules/CaptchaClick.cs
Normal file
@@ -0,0 +1,453 @@
|
|||||||
|
using Microsoft.ML.OnnxRuntime;
|
||||||
|
using Microsoft.ML.OnnxRuntime.Tensors;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Drawing;
|
||||||
|
using SessionOptions = Microsoft.ML.OnnxRuntime.SessionOptions;
|
||||||
|
using PointF = SixLabors.ImageSharp.PointF;
|
||||||
|
|
||||||
|
namespace CaptchaBreaker
|
||||||
|
{
|
||||||
|
public sealed class ChineseClick0 : IDisposable
|
||||||
|
{
|
||||||
|
private const int Canvas = 384;
|
||||||
|
private const int Patch = 96;
|
||||||
|
private const float ConfThreshold = 0.5f;
|
||||||
|
private const float SplitY = 344f;
|
||||||
|
|
||||||
|
private readonly InferenceSession _yolo;
|
||||||
|
private readonly InferenceSession _siamese;
|
||||||
|
|
||||||
|
public ChineseClick0(string yoloModelPath, string siameseModelPath, SessionOptions? options = null)
|
||||||
|
{
|
||||||
|
if (!File.Exists(yoloModelPath)) throw new FileNotFoundException("YOLO 模型未找到", yoloModelPath);
|
||||||
|
if (!File.Exists(siameseModelPath)) throw new FileNotFoundException("Siamese 模型未找到", siameseModelPath);
|
||||||
|
|
||||||
|
options ??= new SessionOptions(); // 默认 CPU EP
|
||||||
|
_yolo = new InferenceSession(yoloModelPath, options);
|
||||||
|
_siamese = new InferenceSession(siameseModelPath, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PointF> Run(string imagePath)
|
||||||
|
{
|
||||||
|
using var img = Image.Load<Rgba32>(imagePath);
|
||||||
|
return Run(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PointF> Run(Image<Rgba32> raw)
|
||||||
|
{
|
||||||
|
if (raw.Width > Canvas || raw.Height > Canvas)
|
||||||
|
throw new InvalidOperationException("不能输入大于384长宽的图片!");
|
||||||
|
|
||||||
|
using var image = PreprocessToCanvas(raw);
|
||||||
|
|
||||||
|
// 1. YOLO 检测
|
||||||
|
var boxes = Detect(image);
|
||||||
|
|
||||||
|
// 2. 分离答案框与问题框,并按 x 从左到右排序
|
||||||
|
var (ans, ques) = SplitBoxes(boxes);
|
||||||
|
|
||||||
|
if (ans.Count == 0 || ques.Count == 0)
|
||||||
|
return new List<PointF>();
|
||||||
|
|
||||||
|
// 3. 裁切并缩放为 96x96,按 [ans..., ques...] 组成批次
|
||||||
|
var (batch, dims) = CropAndResizeBatch(image, ans, ques);
|
||||||
|
|
||||||
|
// 4. Siamese 提取特征
|
||||||
|
var feats = ExtractFeatures(batch, dims);
|
||||||
|
|
||||||
|
// 5. 构建成本矩阵:question × answer
|
||||||
|
var cost = BuildCostMatrix(feats, ans.Count);
|
||||||
|
|
||||||
|
// 6. 匈牙利分配,返回 question -> answer 的映射
|
||||||
|
var assign = Hungarian.Solve(cost); // 长度 = questionCount,值域 [0..ansCount-1]
|
||||||
|
|
||||||
|
// 7. 生成结果:按题面顺序返回匹配到的答案框中心点
|
||||||
|
return GenerateResults(ans, assign);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Image<Rgba32> PreprocessToCanvas(Image<Rgba32> src)
|
||||||
|
{
|
||||||
|
var canvas = new Image<Rgba32>(Canvas, Canvas, new Rgba32(0, 0, 0, 255));
|
||||||
|
canvas.Mutate(ctx =>
|
||||||
|
{
|
||||||
|
ctx.DrawImage(src, new SixLabors.ImageSharp.Point(0, 0), 1f);
|
||||||
|
});
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<BBox> Detect(Image<Rgba32> img)
|
||||||
|
{
|
||||||
|
// 准备 CHW float32 [1,3,384,384], 0..1
|
||||||
|
var input = new DenseTensor<float>(new[] { 1, 3, Canvas, Canvas });
|
||||||
|
img.ProcessPixelRows(accessor =>
|
||||||
|
{
|
||||||
|
for (int y = 0; y < Canvas; y++)
|
||||||
|
{
|
||||||
|
var row = accessor.GetRowSpan(y);
|
||||||
|
for (int x = 0; x < Canvas; x++)
|
||||||
|
{
|
||||||
|
var p = row[x];
|
||||||
|
int idx = y * Canvas + x;
|
||||||
|
input[0, 0, y, x] = p.R / 255f;
|
||||||
|
input[0, 1, y, x] = p.G / 255f;
|
||||||
|
input[0, 2, y, x] = p.B / 255f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var inputs = new List<NamedOnnxValue>
|
||||||
|
{
|
||||||
|
NamedOnnxValue.CreateFromTensor("images", input)
|
||||||
|
};
|
||||||
|
|
||||||
|
using var results = _yolo.Run(inputs);
|
||||||
|
|
||||||
|
// 解析输出:假设为 [1, N, 6] -> (x_min, y_min, x_max, y_max, conf, cls)
|
||||||
|
var y = results.FirstOrDefault(r => r.Name == "output0") ?? results.First();
|
||||||
|
var t = y.AsTensor<float>();
|
||||||
|
var dims = t.Dimensions;
|
||||||
|
|
||||||
|
// 尝试兼容 [1,N,6] 或 [1,6,N]
|
||||||
|
var list = new List<BBox>();
|
||||||
|
if (dims.Length == 3 && dims[0] == 1 && dims[2] == 6)
|
||||||
|
{
|
||||||
|
int n = dims[1];
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
{
|
||||||
|
float conf = t[0, i, 4];
|
||||||
|
if (conf <= ConfThreshold) continue;
|
||||||
|
list.Add(new BBox(
|
||||||
|
t[0, i, 0], t[0, i, 1], t[0, i, 2], t[0, i, 3],
|
||||||
|
conf, t[0, i, 5]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (dims.Length == 3 && dims[0] == 1 && dims[1] == 6)
|
||||||
|
{
|
||||||
|
int n = dims[2];
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
{
|
||||||
|
float conf = t[0, 4, i];
|
||||||
|
if (conf <= ConfThreshold) continue;
|
||||||
|
list.Add(new BBox(
|
||||||
|
t[0, 0, i], t[0, 1, i], t[0, 2, i], t[0, 3, i],
|
||||||
|
conf, t[0, 5, i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new NotSupportedException($"不支持的 YOLO 输出维度:[{string.Join(",", dims.ToArray())}]");
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (List<BBox> ans, List<BBox> ques) SplitBoxes(List<BBox> boxes)
|
||||||
|
{
|
||||||
|
boxes.Sort((a, b) => a.XMin.CompareTo(b.XMin)); // 按 x 从左到右
|
||||||
|
var ans = new List<BBox>();
|
||||||
|
var ques = new List<BBox>();
|
||||||
|
foreach (var b in boxes)
|
||||||
|
{
|
||||||
|
if (b.YMin < SplitY) ans.Add(b);
|
||||||
|
else ques.Add(b);
|
||||||
|
}
|
||||||
|
return (ans, ques);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (DenseTensor<float> batch, int[] dims) CropAndResizeBatch(Image<Rgba32> img, List<BBox> ans, List<BBox> ques)
|
||||||
|
{
|
||||||
|
int a = ans.Count, q = ques.Count, total = a + q;
|
||||||
|
var tensor = new DenseTensor<float>(new[] { total, 3, Patch, Patch });
|
||||||
|
|
||||||
|
void ProcessOne(int index, BBox b)
|
||||||
|
{
|
||||||
|
var rect = ToSafeRect(b, img.Width, img.Height);
|
||||||
|
using var cropped = img.Clone(ctx => ctx.Crop(rect).Resize(Patch, Patch, KnownResamplers.Lanczos3));
|
||||||
|
|
||||||
|
cropped.ProcessPixelRows(rows =>
|
||||||
|
{
|
||||||
|
for (int y = 0; y < Patch; y++)
|
||||||
|
{
|
||||||
|
var row = rows.GetRowSpan(y);
|
||||||
|
for (int x = 0; x < Patch; x++)
|
||||||
|
{
|
||||||
|
var p = row[x];
|
||||||
|
tensor[index, 0, y, x] = p.R / 255f;
|
||||||
|
tensor[index, 1, y, x] = p.G / 255f;
|
||||||
|
tensor[index, 2, y, x] = p.B / 255f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < a; i++) ProcessOne(i, ans[i]);
|
||||||
|
for (int i = 0; i < q; i++) ProcessOne(a + i, ques[i]);
|
||||||
|
|
||||||
|
return (tensor, new[] { total, 3, Patch, Patch });
|
||||||
|
}
|
||||||
|
|
||||||
|
private float[,] ExtractFeatures(DenseTensor<float> batch, int[] dims)
|
||||||
|
{
|
||||||
|
var inputs = new List<NamedOnnxValue>
|
||||||
|
{
|
||||||
|
NamedOnnxValue.CreateFromTensor("input", batch)
|
||||||
|
};
|
||||||
|
|
||||||
|
using var results = _siamese.Run(inputs);
|
||||||
|
var y = results.FirstOrDefault(r => r.Name == "output") ?? results.First();
|
||||||
|
var t = y.AsTensor<float>();
|
||||||
|
|
||||||
|
// 期望 [batch, feat]
|
||||||
|
if (t.Dimensions.Length != 2 || t.Dimensions[0] != dims[0])
|
||||||
|
{
|
||||||
|
throw new NotSupportedException($"不支持的 Siamese 输出维度:[{string.Join(",", t.Dimensions.ToArray())}]");
|
||||||
|
}
|
||||||
|
|
||||||
|
int rows = t.Dimensions[0];
|
||||||
|
int cols = t.Dimensions[1];
|
||||||
|
var feats = new float[rows, cols];
|
||||||
|
for (int i = 0; i < rows; i++)
|
||||||
|
for (int j = 0; j < cols; j++)
|
||||||
|
feats[i, j] = t[i, j];
|
||||||
|
return feats;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float[,] BuildCostMatrix(float[,] feats, int ansCount)
|
||||||
|
{
|
||||||
|
int total = feats.GetLength(0);
|
||||||
|
int featDim = feats.GetLength(1);
|
||||||
|
int q = total - ansCount;
|
||||||
|
int a = ansCount;
|
||||||
|
|
||||||
|
var cost = new float[q, a];
|
||||||
|
for (int i = 0; i < q; i++)
|
||||||
|
{
|
||||||
|
for (int j = 0; j < a; j++)
|
||||||
|
{
|
||||||
|
float sum = 0f;
|
||||||
|
for (int k = 0; k < featDim; k++)
|
||||||
|
{
|
||||||
|
float d = feats[ansCount + i, k] - feats[j, k]; // question - answer
|
||||||
|
sum += d * d;
|
||||||
|
}
|
||||||
|
cost[i, j] = MathF.Sqrt(sum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cost;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<PointF> GenerateResults(List<BBox> ans, int[] assign)
|
||||||
|
{
|
||||||
|
var res = new List<PointF>(assign.Length);
|
||||||
|
foreach (var aIdx in assign)
|
||||||
|
{
|
||||||
|
var b = ans[aIdx];
|
||||||
|
res.Add(new PointF((b.XMin + b.XMax) / 2f, (b.YMin + b.YMax) / 2f));
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SixLabors.ImageSharp.Rectangle ToSafeRect(BBox b, int w, int h)
|
||||||
|
{
|
||||||
|
int x = Math.Clamp((int)MathF.Floor(b.XMin), 0, w - 1);
|
||||||
|
int y = Math.Clamp((int)MathF.Floor(b.YMin), 0, h - 1);
|
||||||
|
int rw = Math.Clamp((int)MathF.Ceiling(b.XMax - b.XMin), 1, w - x);
|
||||||
|
int rh = Math.Clamp((int)MathF.Ceiling(b.YMax - b.YMin), 1, h - y);
|
||||||
|
return new SixLabors.ImageSharp.Rectangle(x, y, rw, rh);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_yolo.Dispose();
|
||||||
|
_siamese.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct BBox(float XMin, float YMin, float XMax, float YMax, float Confidence, float Class);
|
||||||
|
}
|
||||||
|
// 最小化代价匹配(匈牙利算法),输入为 q×a 的代价矩阵,返回长度 q 的数组 rowsol:第 i 个问题匹配到的答案列索引
|
||||||
|
public static class Hungarian
|
||||||
|
{
|
||||||
|
public static int[] Solve(float[,] cost)
|
||||||
|
{
|
||||||
|
int nRows = cost.GetLength(0);
|
||||||
|
int nCols = cost.GetLength(1);
|
||||||
|
|
||||||
|
// 若列少于行,补齐为方阵
|
||||||
|
int n = Math.Max(nRows, nCols);
|
||||||
|
var a = new float[n, n];
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
for (int j = 0; j < n; j++)
|
||||||
|
a[i, j] = (i < nRows && j < nCols) ? cost[i, j] : 0f;
|
||||||
|
|
||||||
|
// 行最小值归一
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
{
|
||||||
|
float min = float.PositiveInfinity;
|
||||||
|
for (int j = 0; j < n; j++) min = Math.Min(min, a[i, j]);
|
||||||
|
for (int j = 0; j < n; j++) a[i, j] -= min;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列最小值归一
|
||||||
|
for (int j = 0; j < n; j++)
|
||||||
|
{
|
||||||
|
float min = float.PositiveInfinity;
|
||||||
|
for (int i = 0; i < n; i++) min = Math.Min(min, a[i, j]);
|
||||||
|
for (int i = 0; i < n; i++) a[i, j] -= min;
|
||||||
|
}
|
||||||
|
|
||||||
|
var starred = new bool[n, n];
|
||||||
|
var primed = new bool[n, n];
|
||||||
|
var rowCovered = new bool[n];
|
||||||
|
var colCovered = new bool[n];
|
||||||
|
|
||||||
|
// 初始:对每行选择第一个 0 且该列未被占用,打星
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
{
|
||||||
|
for (int j = 0; j < n; j++)
|
||||||
|
{
|
||||||
|
if (a[i, j] == 0 && !RowHasStar(starred, i, n) && !ColHasStar(starred, j, n))
|
||||||
|
{
|
||||||
|
starred[i, j] = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CoverStarredColumns(starred, colCovered, n);
|
||||||
|
while (CountTrue(colCovered) < n)
|
||||||
|
{
|
||||||
|
(int r, int c) = FindZero(a, rowCovered, colCovered, n);
|
||||||
|
while (r == -1)
|
||||||
|
{
|
||||||
|
AdjustMatrix(a, rowCovered, colCovered, n);
|
||||||
|
(r, c) = FindZero(a, rowCovered, colCovered, n);
|
||||||
|
}
|
||||||
|
primed[r, c] = true;
|
||||||
|
|
||||||
|
int starCol = FindStarInRow(starred, r, n);
|
||||||
|
if (starCol != -1)
|
||||||
|
{
|
||||||
|
rowCovered[r] = true;
|
||||||
|
colCovered[starCol] = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 交替路径:从这个打撇的 0 开始
|
||||||
|
var path = new List<(int r, int c)> { (r, c) };
|
||||||
|
int col = c;
|
||||||
|
int row;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
row = FindStarInCol(starred, col, n);
|
||||||
|
if (row == -1) break;
|
||||||
|
path.Add((row, col));
|
||||||
|
|
||||||
|
col = FindPrimeInRow(primed, row, n);
|
||||||
|
path.Add((row, col));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 交替:星改非星,撇改星
|
||||||
|
foreach (var (rr, cc) in path)
|
||||||
|
{
|
||||||
|
if (starred[rr, cc]) starred[rr, cc] = false;
|
||||||
|
else starred[rr, cc] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空撇与覆盖
|
||||||
|
Array.Clear(rowCovered, 0, n);
|
||||||
|
Array.Clear(colCovered, 0, n);
|
||||||
|
Array.Clear(primed, 0, primed.Length);
|
||||||
|
|
||||||
|
CoverStarredColumns(starred, colCovered, n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造结果
|
||||||
|
var rowsol = new int[nRows];
|
||||||
|
for (int i = 0; i < nRows; i++)
|
||||||
|
{
|
||||||
|
int j = FindStarInRow(starred, i, n);
|
||||||
|
rowsol[i] = (j < nCols) ? j : Math.Min(i, nCols - 1);
|
||||||
|
}
|
||||||
|
return rowsol;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CoverStarredColumns(bool[,] starred, bool[] colCovered, int n)
|
||||||
|
{
|
||||||
|
Array.Clear(colCovered, 0, colCovered.Length);
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
for (int j = 0; j < n; j++)
|
||||||
|
if (starred[i, j]) colCovered[j] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (int, int) FindZero(float[,] a, bool[] rowCovered, bool[] colCovered, int n)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
if (!rowCovered[i])
|
||||||
|
for (int j = 0; j < n; j++)
|
||||||
|
if (!colCovered[j] && a[i, j] == 0)
|
||||||
|
return (i, j);
|
||||||
|
return (-1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AdjustMatrix(float[,] a, bool[] rowCovered, bool[] colCovered, int n)
|
||||||
|
{
|
||||||
|
float min = float.PositiveInfinity;
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
if (!rowCovered[i])
|
||||||
|
for (int j = 0; j < n; j++)
|
||||||
|
if (!colCovered[j])
|
||||||
|
min = Math.Min(min, a[i, j]);
|
||||||
|
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
{
|
||||||
|
if (rowCovered[i])
|
||||||
|
for (int j = 0; j < n; j++)
|
||||||
|
a[i, j] += min;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int j = 0; j < n; j++)
|
||||||
|
{
|
||||||
|
if (!colCovered[j])
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
a[i, j] -= min;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool RowHasStar(bool[,] starred, int r, int n)
|
||||||
|
{
|
||||||
|
for (int j = 0; j < n; j++) if (starred[r, j]) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ColHasStar(bool[,] starred, int c, int n)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < n; i++) if (starred[i, c]) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int FindStarInRow(bool[,] starred, int r, int n)
|
||||||
|
{
|
||||||
|
for (int j = 0; j < n; j++) if (starred[r, j]) return j;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int FindStarInCol(bool[,] starred, int c, int n)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < n; i++) if (starred[i, c]) return i;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int FindPrimeInRow(bool[,] primed, int r, int n)
|
||||||
|
{
|
||||||
|
for (int j = 0; j < n; j++) if (primed[r, j]) return j;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CountTrue(bool[] a) => a.Count(x => x);
|
||||||
|
}
|
||||||
|
}
|
||||||
565
Modules/GeetestCrypto.cs
Normal file
565
Modules/GeetestCrypto.cs
Normal file
@@ -0,0 +1,565 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace MegghysAPI.Modules
|
||||||
|
{
|
||||||
|
internal static class GeetestCrypto
|
||||||
|
{
|
||||||
|
private const string RsaN = "00C1E3934D1614465B33053E7F48EE4EC87B14B95EF88947713D25EECBFF7E74C7977D02DC1D9451F79DD5D1C10C29ACB6A9B4D6FB7D0A0279B6719E1772565F09AF627715919221AEF91899CAE08C0D686D748B20A3603BE2318CA6BC2B59706592A9219D0BF05C9F65023A21D2330807252AE0066D59CEEFA5F2748EA80BAB81";
|
||||||
|
private const string RsaE = "010001";
|
||||||
|
private const string AesKey = "1234567890123456";
|
||||||
|
private static readonly byte[] AesIv =
|
||||||
|
{
|
||||||
|
48, 48, 48, 48, 48, 48, 48, 48,
|
||||||
|
48, 48, 48, 48, 48, 48, 48, 48
|
||||||
|
};
|
||||||
|
|
||||||
|
private const string Base64Table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789()";
|
||||||
|
private const int Mask1 = 7274496;
|
||||||
|
private const int Mask2 = 9483264;
|
||||||
|
private const int Mask3 = 19220;
|
||||||
|
private const int Mask4 = 235;
|
||||||
|
|
||||||
|
public static string ClickCalculate(string key, string gt, string challenge)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(challenge) || challenge.Length < 2)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("challenge 长度必须至少为 2", nameof(challenge));
|
||||||
|
}
|
||||||
|
|
||||||
|
var passTime = (int)(Random.Shared.NextDouble() * 700.0 + 1300.0);
|
||||||
|
var challengePrefix = challenge[..^2];
|
||||||
|
var md5Source = string.Concat(gt, challengePrefix, passTime.ToString());
|
||||||
|
var rpBytes = MD5.HashData(Encoding.UTF8.GetBytes(md5Source));
|
||||||
|
var rp = Convert.ToHexString(rpBytes).ToLowerInvariant();
|
||||||
|
|
||||||
|
var payload = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["lang"] = "zh-cn",
|
||||||
|
["passtime"] = passTime,
|
||||||
|
["a"] = key,
|
||||||
|
["tt"] = string.Empty,
|
||||||
|
["ep"] = BuildEpObject(),
|
||||||
|
["h9s9"] = "1816378497",
|
||||||
|
["rp"] = rp,
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(payload);
|
||||||
|
return Encrypt(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string SlideCalculate(int distance, string gt, string challenge, ReadOnlySpan<byte> c, string s)
|
||||||
|
{
|
||||||
|
if (distance < 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(distance), "distance 必须大于等于 0");
|
||||||
|
}
|
||||||
|
if (string.IsNullOrEmpty(challenge) || challenge.Length < 2)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("challenge 长度必须至少为 2", nameof(challenge));
|
||||||
|
}
|
||||||
|
|
||||||
|
var track = GetSlideTrack(distance);
|
||||||
|
var passTime = track[track.Count - 1][2];
|
||||||
|
var encryptedTrack = TrackEncrypt(track);
|
||||||
|
var aa = FinalEncrypt(encryptedTrack, c, s);
|
||||||
|
var userResponse = UserResponse(distance, challenge);
|
||||||
|
|
||||||
|
var challengePrefix = challenge[..^2];
|
||||||
|
var md5Source = string.Concat(gt, challengePrefix, passTime.ToString());
|
||||||
|
var rpBytes = MD5.HashData(Encoding.UTF8.GetBytes(md5Source));
|
||||||
|
var rp = Convert.ToHexString(rpBytes).ToLowerInvariant();
|
||||||
|
|
||||||
|
var payload = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["lang"] = "zh-cn",
|
||||||
|
["userresponse"] = userResponse,
|
||||||
|
["passtime"] = passTime,
|
||||||
|
["imgload"] = NextIntInclusive(100, 200),
|
||||||
|
["aa"] = aa,
|
||||||
|
["ep"] = BuildEpObject(),
|
||||||
|
["rp"] = rp,
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(payload);
|
||||||
|
return Encrypt(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, object?> BuildEpObject()
|
||||||
|
{
|
||||||
|
return new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["v"] = "9.1.8-bfget5",
|
||||||
|
["$_E_"] = false,
|
||||||
|
["me"] = true,
|
||||||
|
["ven"] = "Google Inc. (Intel)",
|
||||||
|
["ren"] = "ANGLE (Intel, Intel(R) HD Graphics 520 Direct3D11 vs_5_0 ps_5_0, D3D11)",
|
||||||
|
["fp"] = new object[] { "move", 483, 149, 1702019849214L, "pointermove" },
|
||||||
|
["lp"] = new object[] { "up", 657, 100, 1702019852230L, "pointerup" },
|
||||||
|
["em"] = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["ph"] = 0,
|
||||||
|
["cp"] = 0,
|
||||||
|
["ek"] = "11",
|
||||||
|
["wd"] = 1,
|
||||||
|
["nt"] = 0,
|
||||||
|
["si"] = 0,
|
||||||
|
["sc"] = 0,
|
||||||
|
},
|
||||||
|
["tm"] = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["a"] = 1702019845759L,
|
||||||
|
["b"] = 1702019845951L,
|
||||||
|
["c"] = 1702019845951L,
|
||||||
|
["d"] = 0,
|
||||||
|
["e"] = 0,
|
||||||
|
["f"] = 1702019845763L,
|
||||||
|
["g"] = 1702019845785L,
|
||||||
|
["h"] = 1702019845785L,
|
||||||
|
["i"] = 1702019845785L,
|
||||||
|
["j"] = 1702019845845L,
|
||||||
|
["k"] = 1702019845812L,
|
||||||
|
["l"] = 1702019845845L,
|
||||||
|
["m"] = 1702019845942L,
|
||||||
|
["n"] = 1702019845946L,
|
||||||
|
["o"] = 1702019845954L,
|
||||||
|
["p"] = 1702019846282L,
|
||||||
|
["q"] = 1702019846282L,
|
||||||
|
["r"] = 1702019846287L,
|
||||||
|
["s"] = 1702019846288L,
|
||||||
|
["t"] = 1702019846288L,
|
||||||
|
["u"] = 1702019846288L,
|
||||||
|
},
|
||||||
|
["dnf"] = "dnf",
|
||||||
|
["by"] = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Encrypt(string json)
|
||||||
|
{
|
||||||
|
var rsaEncryptedKey = RsaEncrypt(AesKey);
|
||||||
|
var aesEncryptedData = AesEncrypt(json);
|
||||||
|
var base64Part = CustomBase64(aesEncryptedData);
|
||||||
|
return base64Part + rsaEncryptedKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] AesEncrypt(string data)
|
||||||
|
{
|
||||||
|
using var aes = Aes.Create();
|
||||||
|
aes.Mode = CipherMode.CBC;
|
||||||
|
aes.Padding = PaddingMode.PKCS7;
|
||||||
|
aes.Key = Encoding.UTF8.GetBytes(AesKey);
|
||||||
|
aes.IV = AesIv;
|
||||||
|
|
||||||
|
using var encryptor = aes.CreateEncryptor();
|
||||||
|
var inputBytes = Encoding.UTF8.GetBytes(data);
|
||||||
|
return encryptor.TransformFinalBlock(inputBytes, 0, inputBytes.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RsaEncrypt(string data)
|
||||||
|
{
|
||||||
|
var modulus = TrimLeadingZero(HexToBytes(RsaN));
|
||||||
|
var exponent = TrimLeadingZero(HexToBytes(RsaE));
|
||||||
|
|
||||||
|
using var rsa = RSA.Create();
|
||||||
|
rsa.ImportParameters(new RSAParameters
|
||||||
|
{
|
||||||
|
Modulus = modulus,
|
||||||
|
Exponent = exponent
|
||||||
|
});
|
||||||
|
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(data);
|
||||||
|
var encrypted = rsa.Encrypt(bytes, RSAEncryptionPadding.Pkcs1);
|
||||||
|
return Convert.ToHexString(encrypted).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] TrimLeadingZero(byte[] value)
|
||||||
|
{
|
||||||
|
if (value.Length > 0 && value[0] == 0)
|
||||||
|
{
|
||||||
|
var trimmed = new byte[value.Length - 1];
|
||||||
|
Buffer.BlockCopy(value, 1, trimmed, 0, trimmed.Length);
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CustomBase64(ReadOnlySpan<byte> input)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
var padding = string.Empty;
|
||||||
|
var len = input.Length;
|
||||||
|
var ptr = 0;
|
||||||
|
|
||||||
|
while (ptr < len)
|
||||||
|
{
|
||||||
|
if (ptr + 2 < len)
|
||||||
|
{
|
||||||
|
var c = (input[ptr] << 16) + (input[ptr + 1] << 8) + input[ptr + 2];
|
||||||
|
sb.Append(Base64Table[GetIntByMask(c, Mask1)]);
|
||||||
|
sb.Append(Base64Table[GetIntByMask(c, Mask2)]);
|
||||||
|
sb.Append(Base64Table[GetIntByMask(c, Mask3)]);
|
||||||
|
sb.Append(Base64Table[GetIntByMask(c, Mask4)]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var remainder = len % 3;
|
||||||
|
if (remainder == 2)
|
||||||
|
{
|
||||||
|
var c = (input[ptr] << 16) + (input[ptr + 1] << 8);
|
||||||
|
sb.Append(Base64Table[GetIntByMask(c, Mask1)]);
|
||||||
|
sb.Append(Base64Table[GetIntByMask(c, Mask2)]);
|
||||||
|
sb.Append(Base64Table[GetIntByMask(c, Mask3)]);
|
||||||
|
padding = ".";
|
||||||
|
}
|
||||||
|
else if (remainder == 1)
|
||||||
|
{
|
||||||
|
var c = input[ptr] << 16;
|
||||||
|
sb.Append(Base64Table[GetIntByMask(c, Mask1)]);
|
||||||
|
sb.Append(Base64Table[GetIntByMask(c, Mask2)]);
|
||||||
|
padding = "..";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ptr += 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Append(padding);
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetIntByMask(int value, int mask)
|
||||||
|
{
|
||||||
|
var result = 0;
|
||||||
|
for (var bit = 23; bit >= 0; bit--)
|
||||||
|
{
|
||||||
|
if (ChooseBit(mask, bit) == 1)
|
||||||
|
{
|
||||||
|
result = (result << 1) | ChooseBit(value, bit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ChooseBit(int value, int bit)
|
||||||
|
{
|
||||||
|
return (value >> bit) & 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] HexToBytes(string hex)
|
||||||
|
{
|
||||||
|
var cleaned = hex.Replace("\n", string.Empty)
|
||||||
|
.Replace("\r", string.Empty)
|
||||||
|
.Replace(" ", string.Empty);
|
||||||
|
if (cleaned.Length % 2 != 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("十六进制字符串长度必须为偶数", nameof(hex));
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes = new byte[cleaned.Length / 2];
|
||||||
|
for (var i = 0; i < bytes.Length; i++)
|
||||||
|
{
|
||||||
|
bytes[i] = Convert.ToByte(cleaned.Substring(i * 2, 2), 16);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int NextIntInclusive(int minValue, int maxValue)
|
||||||
|
{
|
||||||
|
if (minValue > maxValue)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("minValue 必须小于等于 maxValue");
|
||||||
|
}
|
||||||
|
return Random.Shared.Next(minValue, maxValue + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<List<int>> GetSlideTrack(int distance)
|
||||||
|
{
|
||||||
|
if (distance < 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(distance));
|
||||||
|
}
|
||||||
|
|
||||||
|
var slideTrack = new List<List<int>>();
|
||||||
|
var x1 = NextIntInclusive(-50, -10);
|
||||||
|
var y1 = NextIntInclusive(-50, -10);
|
||||||
|
slideTrack.Add(new List<int> { x1, y1, 0 });
|
||||||
|
slideTrack.Add(new List<int> { 0, 0, 0 });
|
||||||
|
|
||||||
|
var count = 30 + distance / 2;
|
||||||
|
var t = NextIntInclusive(50, 100);
|
||||||
|
var currentX = 0;
|
||||||
|
var currentY = 0;
|
||||||
|
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var sep = i / (double)count;
|
||||||
|
double x;
|
||||||
|
if (Math.Abs(sep - 1.0) < double.Epsilon)
|
||||||
|
{
|
||||||
|
x = distance;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
x = (1.0 - Math.Pow(2.0, -10.0 * sep)) * distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
var xRounded = (int)Math.Round(x);
|
||||||
|
t += NextIntInclusive(10, 20);
|
||||||
|
|
||||||
|
if (xRounded == currentX)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
slideTrack.Add(new List<int> { xRounded, currentY, t });
|
||||||
|
currentX = xRounded;
|
||||||
|
}
|
||||||
|
|
||||||
|
var last = slideTrack[slideTrack.Count - 1];
|
||||||
|
slideTrack.Add(new List<int>(last));
|
||||||
|
|
||||||
|
return slideTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string TrackEncrypt(List<List<int>> track)
|
||||||
|
{
|
||||||
|
static List<List<int>> ProcessTrack(List<List<int>> trackData)
|
||||||
|
{
|
||||||
|
var result = new List<List<int>>();
|
||||||
|
var o = 0;
|
||||||
|
|
||||||
|
for (var s = 0; s < trackData.Count - 1; s++)
|
||||||
|
{
|
||||||
|
var e = trackData[s + 1][0] - trackData[s][0];
|
||||||
|
var n = trackData[s + 1][1] - trackData[s][1];
|
||||||
|
var r = trackData[s + 1][2] - trackData[s][2];
|
||||||
|
|
||||||
|
if (e == 0 && n == 0 && r == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e == 0 && n == 0)
|
||||||
|
{
|
||||||
|
o += r;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result.Add(new List<int> { e, n, r + o });
|
||||||
|
o = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (o != 0 && result.Count > 0)
|
||||||
|
{
|
||||||
|
var lastItem = result[result.Count - 1];
|
||||||
|
result.Add(new List<int> { lastItem[0], lastItem[1], o });
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static string EncodeValue(int t)
|
||||||
|
{
|
||||||
|
const string chars = "()*,-./0123456789:?@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqr";
|
||||||
|
var n = chars.Length;
|
||||||
|
var r = new StringBuilder();
|
||||||
|
|
||||||
|
var i = Math.Abs(t);
|
||||||
|
var oVal = i / n;
|
||||||
|
var o = oVal >= n ? n - 1 : oVal;
|
||||||
|
if (o >= chars.Length)
|
||||||
|
{
|
||||||
|
o = chars.Length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (o > 0)
|
||||||
|
{
|
||||||
|
r.Append(chars[o]);
|
||||||
|
}
|
||||||
|
|
||||||
|
var s = new StringBuilder();
|
||||||
|
if (t < 0)
|
||||||
|
{
|
||||||
|
s.Append('!');
|
||||||
|
}
|
||||||
|
if (r.Length > 0)
|
||||||
|
{
|
||||||
|
s.Append('$');
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Append(r);
|
||||||
|
s.Append(chars[i % n]);
|
||||||
|
return s.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static char? EncodePair(IReadOnlyList<int> values)
|
||||||
|
{
|
||||||
|
var pairs = new List<int[]>
|
||||||
|
{
|
||||||
|
new[] { 1, 0 },
|
||||||
|
new[] { 2, 0 },
|
||||||
|
new[] { 1, -1 },
|
||||||
|
new[] { 1, 1 },
|
||||||
|
new[] { 0, 1 },
|
||||||
|
new[] { 0, -1 },
|
||||||
|
new[] { 3, 0 },
|
||||||
|
new[] { 2, -1 },
|
||||||
|
new[] { 2, 1 },
|
||||||
|
};
|
||||||
|
const string chars = "stuvwxyz~";
|
||||||
|
|
||||||
|
for (var idx = 0; idx < pairs.Count; idx++)
|
||||||
|
{
|
||||||
|
var pair = pairs[idx];
|
||||||
|
if (values[0] == pair[0] && values[1] == pair[1])
|
||||||
|
{
|
||||||
|
return chars[idx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ProcessElement(IReadOnlyList<int> item, StringBuilder r, StringBuilder i, StringBuilder o)
|
||||||
|
{
|
||||||
|
var encodedPair = EncodePair(item);
|
||||||
|
if (encodedPair.HasValue)
|
||||||
|
{
|
||||||
|
i.Append(encodedPair.Value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
r.Append(EncodeValue(item[0]));
|
||||||
|
i.Append(EncodeValue(item[1]));
|
||||||
|
}
|
||||||
|
o.Append(EncodeValue(item[2]));
|
||||||
|
}
|
||||||
|
|
||||||
|
var processed = ProcessTrack(track);
|
||||||
|
var rBuilder = new StringBuilder();
|
||||||
|
var iBuilder = new StringBuilder();
|
||||||
|
var oBuilder = new StringBuilder();
|
||||||
|
|
||||||
|
foreach (var item in processed)
|
||||||
|
{
|
||||||
|
ProcessElement(item, rBuilder, iBuilder, oBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Concat(rBuilder, "!!", iBuilder, "!!", oBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FinalEncrypt(string t, ReadOnlySpan<byte> e, string n)
|
||||||
|
{
|
||||||
|
if (e.Length < 5 || string.IsNullOrEmpty(n))
|
||||||
|
{
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
var s = e[0];
|
||||||
|
var a = e[2];
|
||||||
|
var m = e[4];
|
||||||
|
var originalLength = t.Length;
|
||||||
|
var builder = new StringBuilder(t);
|
||||||
|
|
||||||
|
var index = 0;
|
||||||
|
while (index <= n.Length - 2)
|
||||||
|
{
|
||||||
|
var hexPair = n.Substring(index, 2);
|
||||||
|
index += 2;
|
||||||
|
var c = Convert.ToByte(hexPair, 16);
|
||||||
|
var u = (char)c;
|
||||||
|
|
||||||
|
var ll = ((ulong)s * c * c + (ulong)a * c + m) % (ulong)originalLength;
|
||||||
|
builder.Insert((int)ll, u);
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string UserResponse(int key, string challenge)
|
||||||
|
{
|
||||||
|
if (challenge.Length < 2)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("challenge 长度必须至少为 2", nameof(challenge));
|
||||||
|
}
|
||||||
|
|
||||||
|
var chars = challenge.ToCharArray();
|
||||||
|
var lastTwo = new[] { chars[chars.Length - 2], chars[chars.Length - 1] };
|
||||||
|
|
||||||
|
var r = new List<int>();
|
||||||
|
foreach (var c in lastTwo)
|
||||||
|
{
|
||||||
|
var code = (int)c;
|
||||||
|
r.Add(code > 57 ? code - 87 : code - 48);
|
||||||
|
}
|
||||||
|
|
||||||
|
var n = 36 * r[0] + r[1];
|
||||||
|
var a = key + n;
|
||||||
|
|
||||||
|
var underscores = new List<List<char>>
|
||||||
|
{
|
||||||
|
new List<char>(),
|
||||||
|
new List<char>(),
|
||||||
|
new List<char>(),
|
||||||
|
new List<char>(),
|
||||||
|
new List<char>()
|
||||||
|
};
|
||||||
|
var charSet = new HashSet<char>();
|
||||||
|
var idx = 0;
|
||||||
|
|
||||||
|
for (var i = 0; i < chars.Length - 2; i++)
|
||||||
|
{
|
||||||
|
var c = chars[i];
|
||||||
|
if (charSet.Add(c))
|
||||||
|
{
|
||||||
|
underscores[idx].Add(c);
|
||||||
|
idx = (idx + 1) % 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var f = a;
|
||||||
|
var d = 4;
|
||||||
|
var result = new StringBuilder();
|
||||||
|
var weights = new List<int> { 1, 2, 5, 10, 50 };
|
||||||
|
|
||||||
|
while (f > 0)
|
||||||
|
{
|
||||||
|
if (d >= weights.Count)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("权重数组越界,请检查输入参数有效性");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (f >= weights[d])
|
||||||
|
{
|
||||||
|
if (underscores[d].Count == 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"五元组数组 {d} 号位置无可用字符,请确保输入字符串包含足够多的唯一字符");
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Append(underscores[d][0]);
|
||||||
|
f -= weights[d];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
underscores.RemoveAt(d);
|
||||||
|
weights.RemoveAt(d);
|
||||||
|
if (d > 0)
|
||||||
|
{
|
||||||
|
d -= 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("权重数组耗尽,无法继续处理");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
177
Modules/ProxyManager.cs
Normal file
177
Modules/ProxyManager.cs
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
|
||||||
|
namespace MegghysAPI.Modules
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 代理IP管理器
|
||||||
|
/// </summary>
|
||||||
|
public static class ProxyManager
|
||||||
|
{
|
||||||
|
// 用于获取代理IP的客户端,不使用代理
|
||||||
|
private static readonly HttpClient ProxyApiHttpClient = new();
|
||||||
|
// 缓存代理IP的响应
|
||||||
|
private static readonly MemoryCache ProxyResponseCache = new(new MemoryCacheOptions());
|
||||||
|
// 缓存配置了代理的HttpClient
|
||||||
|
private static readonly MemoryCache HttpClientCache = new(new MemoryCacheOptions());
|
||||||
|
|
||||||
|
private const string BaseUrl = "https://share.proxy.qg.net/get";
|
||||||
|
private const string DefaultKey = "9IK2T3XL";
|
||||||
|
private const string DefaultPassword = "04BC86C88CDB";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取配置了代理的 HttpClient,带缓存和自动释放功能
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="proxyAddress">可选的代理地址。如果为空,则自动获取。</param>
|
||||||
|
/// <param name="key">代理用户名</param>
|
||||||
|
/// <param name="password">代理密码</param>
|
||||||
|
/// <returns>配置好代理的 HttpClient</returns>
|
||||||
|
public static async Task<HttpClient> GetHttpClientAsync(string? proxyAddress = null, string key = DefaultKey, string password = DefaultPassword)
|
||||||
|
{
|
||||||
|
// 如果未提供代理地址,则从API获取
|
||||||
|
if (string.IsNullOrWhiteSpace(proxyAddress))
|
||||||
|
{
|
||||||
|
var proxyResponse = await GetProxyAsync(key: key);
|
||||||
|
var proxyIp = proxyResponse?.Data?.FirstOrDefault();
|
||||||
|
|
||||||
|
if (proxyIp?.Server == null || proxyIp.Deadline == null)
|
||||||
|
{
|
||||||
|
// 如果无法获取有效代理,返回一个不带代理的普通HttpClient
|
||||||
|
return new HttpClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查缓存中是否已有此代理的HttpClient
|
||||||
|
if (HttpClientCache.TryGetValue(proxyIp.Server, out HttpClient? cachedClient))
|
||||||
|
{
|
||||||
|
return cachedClient!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新的HttpClient并配置缓存
|
||||||
|
var client = CreateConfiguredHttpClient(proxyIp.Server, key, password);
|
||||||
|
var deadline = DateTime.Parse(proxyIp.Deadline);
|
||||||
|
|
||||||
|
var cacheEntryOptions = new MemoryCacheEntryOptions()
|
||||||
|
.SetAbsoluteExpiration(deadline)
|
||||||
|
.RegisterPostEvictionCallback((cacheKey, value, reason, state) =>
|
||||||
|
{
|
||||||
|
(value as HttpClient)?.Dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
HttpClientCache.Set(proxyIp.Server, client, cacheEntryOptions);
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 如果用户手动提供了代理地址,我们无法知道其过期时间,因此不进行缓存
|
||||||
|
return CreateConfiguredHttpClient(proxyAddress, key, password);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HttpClient CreateConfiguredHttpClient(string proxyAddress, string key, string password)
|
||||||
|
{
|
||||||
|
var handler = new HttpClientHandler
|
||||||
|
{
|
||||||
|
AutomaticDecompression = DecompressionMethods.All,
|
||||||
|
Proxy = new WebProxy(proxyAddress, false)
|
||||||
|
{
|
||||||
|
Credentials = new NetworkCredential(key, password)
|
||||||
|
},
|
||||||
|
UseProxy = true
|
||||||
|
};
|
||||||
|
var client = new HttpClient(handler, disposeHandler: true);
|
||||||
|
client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36");
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 异步获取代理IP, 带缓存功能
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<ProxyResponse?> GetProxyAsync(string key = DefaultKey, string? area = null, string? areaEx = null, int? isp = null, int? num = null, bool? distinct = null)
|
||||||
|
{
|
||||||
|
var queryParams = new Dictionary<string, string?>
|
||||||
|
{
|
||||||
|
{ "key", key },
|
||||||
|
{ "area", area },
|
||||||
|
{ "area_ex", areaEx },
|
||||||
|
{ "isp", isp?.ToString() },
|
||||||
|
{ "num", num?.ToString() },
|
||||||
|
{ "distinct", distinct?.ToString().ToLower() }
|
||||||
|
};
|
||||||
|
|
||||||
|
var queryString = string.Join("&", queryParams
|
||||||
|
.Where(kvp => !string.IsNullOrEmpty(kvp.Value))
|
||||||
|
.Select(kvp => $"{kvp.Key}={Uri.EscapeDataString(kvp.Value!)}"));
|
||||||
|
|
||||||
|
var cacheKey = $"ProxyResponse_{queryString}";
|
||||||
|
|
||||||
|
if (ProxyResponseCache.TryGetValue(cacheKey, out ProxyResponse? cachedResponse))
|
||||||
|
{
|
||||||
|
return cachedResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestUrl = $"{BaseUrl}?{queryString}";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await ProxyApiHttpClient.GetAsync(requestUrl);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
var json = await response.Content.ReadAsStringAsync();
|
||||||
|
var proxyResponse = JsonSerializer.Deserialize<ProxyResponse>(json);
|
||||||
|
|
||||||
|
if (proxyResponse?.Code == "SUCCESS" && proxyResponse.Data?.Any() == true)
|
||||||
|
{
|
||||||
|
// 使用代理的过期时间作为缓存的过期时间
|
||||||
|
var deadline = DateTime.Parse(proxyResponse.Data.First().Deadline!);
|
||||||
|
ProxyResponseCache.Set(cacheKey, proxyResponse, deadline);
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxyResponse;
|
||||||
|
}
|
||||||
|
catch (HttpRequestException e)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"请求代理IP时发生错误: {e.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 代理IP接口响应
|
||||||
|
/// </summary>
|
||||||
|
public class ProxyResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("code")]
|
||||||
|
public string? Code { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public List<ProxyIp>? Data { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("request_id")]
|
||||||
|
public string? RequestId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 代理IP信息
|
||||||
|
/// </summary>
|
||||||
|
public class ProxyIp
|
||||||
|
{
|
||||||
|
[JsonPropertyName("proxy_ip")]
|
||||||
|
public string? ProxyIpAddress { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("server")]
|
||||||
|
public string? Server { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("area")]
|
||||||
|
public string? Area { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("isp")]
|
||||||
|
public string? Isp { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("deadline")]
|
||||||
|
public string? Deadline { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ using System.Diagnostics;
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using MegghysAPI.Attributes;
|
using MegghysAPI.Attributes;
|
||||||
using MegghysAPI.Components;
|
using MegghysAPI.Components;
|
||||||
|
using MegghysAPI.Modules;
|
||||||
using AntDesign;
|
using AntDesign;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
@@ -13,6 +14,7 @@ builder.Services.AddRazorComponents()
|
|||||||
builder.Services.AddAntDesign();
|
builder.Services.AddAntDesign();
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
|
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
|
|||||||
Reference in New Issue
Block a user