mirror of
https://github.com/Megghy/MegghysAPI.git
synced 2025-12-06 22:26:56 +08:00
fix: 修复文件列表为空时的返回值处理并添加图像处理相关依赖
This commit is contained in:
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user