fix: 修复文件列表为空时的返回值处理并添加图像处理相关依赖

This commit is contained in:
Megghy
2025-09-29 13:41:32 +08:00
parent 7abe753401
commit 360cc79e18
12 changed files with 1913 additions and 0 deletions

View 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.ChineseClick0ONNX 模型)。
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);
}
}
}