mirror of
https://github.com/Megghy/MegghysAPI.git
synced 2025-12-06 14:16:56 +08:00
401 lines
16 KiB
C#
401 lines
16 KiB
C#
|
||
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);
|
||
}
|
||
}
|
||
}
|