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 } /// /// 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 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 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) ========== /// /// 从业务 URL 注册,返回 (gt, challenge)。 /// 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); } /// /// 获取初始 c/s 参数,对应 Rust 版本的 get_c_s 方法。 /// 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 { ["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()).ToArray() ?? []; var s = (string?)data["s"] ?? throw new Exception("缺少 s"); return (cList, s); } /// /// 获取验证类型,对应 Rust 版本的 get_type 方法。 /// public async Task GetTypeAsync(string gt, string challenge, string? proxy = null) { var url = "http://api.geevisit.com/gettype.php"; var callback = "geetest_1717934072177"; var query = new Dictionary { ["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"); } /// /// 获取 new c/s/args(图片地址)。 /// 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 { ["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()).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); } /// /// 提交验证,可带 w。 /// 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 { ["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); } /// /// 刷新获取新的图片地址。 /// public async Task RefreshAsync(string gt, string challenge, string? proxy = null) { var url = "http://api.geevisit.com/refresh.php"; var callback = "geetest_1717918222610"; var query = new Dictionary { ["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('/')}"; } /// /// 计算 key(坐标串),会下载图片并通过 ONNX 模型推理。 /// public async Task CalculateKeyAsync(string picUrl, string? proxy = null) { var http = await ProxyManager.GetHttpClientAsync(proxy); var bytes = await http.GetByteArrayAsync(picUrl); var raw = Image.Load(bytes); // 保障尺寸不超过 384 List 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(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); } /// /// 生成 w。注意:目前使用占位实现,请按需替换为真实算法。 /// public string GenerateW(string key, string gt, string challenge, ReadOnlySpan c, string s) { return ClickCalculate(key, gt, challenge, c, s); } /// /// 完整流程测试:注册 -> get_c_s -> get_type -> 拉取 c/s/args -> 计算 key -> 生成 w -> 提交 -> 返回 validate。 /// 按照原始 Rust 版本流程顺序,sleep 保证总耗时 >= 2s。 /// public async Task 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; } /// /// 简单匹配(不注册):直接使用已知 gt, challenge,按照原始流程顺序。 /// public async Task 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; } /// /// 带自动重试的简单匹配,按照原始流程顺序。 /// public async Task 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 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 c, string s) { _ = c; _ = s; return GeetestCrypto.ClickCalculate(key, gt, challenge); } } }