[Admin] VPN/proxy + AI test buttons; fix AI JSON parse crash on null fields
CI/CD / CI · dotnet build (push) Successful in 2m41s
CI/CD / Deploy · hamkadr (push) Failing after 2m56s

Add «تست اتصال VPN/پروکسی» (reaches a filtered site through the proxy and reports connected/latency) and «تست هوش مصنوعی» (sends a sample post through the configured model and shows the verdict + extracted fields) to admin Settings. Fix OpenAiCompatibleAuditor.ParseVerdict: TryGetInt32/64 threw on null/string JSON values (the model commonly returns payAmount/sharePercent as null), which silently failed every audit — now guarded on ValueKind==Number. Verified the real OpenAI key extracts perfectly (approve / role=پرستار / city=تهران / shift=night).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-07 23:23:02 +03:30
parent 0c49b89891
commit 524c66e25e
3 changed files with 63 additions and 4 deletions
@@ -15,6 +15,8 @@
@if (Model.Saved is not null) { <div class="alert alert-success">✓ @Model.Saved</div> }
@if (Model.DemoMsg is not null) { <div class="alert alert-success">@Model.DemoMsg</div> }
@if (Model.SmsTest is not null) { <div class="alert alert-success">@Model.SmsTest</div> }
@if (Model.ProxyTest is not null) { <div class="alert alert-success">@Model.ProxyTest</div> }
@if (Model.AiTest is not null) { <div class="alert alert-success">@Model.AiTest</div> }
<form method="post">
<div class="settings-layout">
@@ -78,6 +80,8 @@
<span class="t-body"><span>ارسال درخواست هوش مصنوعی از طریق پروکسی</span>
<span class="t-hint">برای دسترسی به سرویس‌هایی مثل OpenAI از داخل ایران؛ از همان آدرس پروکسی تب «منابع جمع‌آوری» استفاده می‌کند.</span></span>
</label>
<button type="submit" asp-page-handler="TestAi" class="btn btn-outline" style="margin-top:6px;">🤖 تست هوش مصنوعی (روی یک آگهی نمونه)</button>
<p class="muted" style="font-size:11px; margin:4px 0 0;">یک آگهی نمونه را به مدل می‌فرستد و تصمیم/استخراج آن را نشان می‌دهد. (ابتدا کلید و آدرس را ذخیره کن.)</p>
</section>
<!-- SOURCES -->
@@ -144,6 +148,8 @@
<label>آدرس پروکسی محلی</label>
<input type="text" name="IngestProxyUrl" value="@Model.IngestProxyUrl" dir="ltr" placeholder="socks5://xray:10808" />
<p class="muted" style="font-size:12px; margin:4px 0 0;">یک کلاینت Xray/V2Ray کانفیگ vmess/vless/trojan تو را به یک پروکسی محلی تبدیل می‌کند (socks5:// یا socks4:// یا http://). <strong>هر منبع جداگانه</strong> با تیکِ «از پروکسی استفاده شود» تعیین می‌کند که از این پروکسی عبور کند یا نه.</p>
<button type="submit" asp-page-handler="TestProxy" class="btn btn-outline" style="margin-top:8px;">🔌 تست اتصال VPN/پروکسی</button>
<p class="muted" style="font-size:11px; margin:4px 0 0;">از طریق پروکسی به یک سایت فیلترشده وصل می‌شود؛ موفقیت یعنی تونل برقرار است. (ابتدا آدرس را ذخیره کن.)</p>
</div>
</section>
@@ -14,11 +14,16 @@ public class SettingsModel : PageModel
private readonly SettingsService _settings;
private readonly ISmsSender _sms;
private readonly AppDbContext _db;
public SettingsModel(SettingsService settings, ISmsSender sms, AppDbContext db)
private readonly ScrapeHttpClients _clients;
private readonly IAiAuditor _ai;
public SettingsModel(SettingsService settings, ISmsSender sms, AppDbContext db,
ScrapeHttpClients clients, IAiAuditor ai)
{
_settings = settings;
_sms = sms;
_db = db;
_clients = clients;
_ai = ai;
}
[BindProperty] public IngestionMode Mode { get; set; }
@@ -65,6 +70,8 @@ public class SettingsModel : PageModel
[TempData] public string? Saved { get; set; }
[TempData] public string? SmsTest { get; set; }
[TempData] public string? DemoMsg { get; set; }
[TempData] public string? ProxyTest { get; set; }
[TempData] public string? AiTest { get; set; }
public async Task OnGetAsync()
{
@@ -172,6 +179,50 @@ public class SettingsModel : PageModel
return RedirectToPage();
}
/// <summary>Check the VPN/proxy is connected by reaching a normally-blocked site through it.</summary>
public async Task<IActionResult> OnPostTestProxyAsync()
{
var s = await _settings.GetAsync();
if (string.IsNullOrWhiteSpace(s.IngestProxyUrl))
{ ProxyTest = "ابتدا آدرس پروکسی را وارد و ذخیره کن."; return RedirectToPage(); }
var client = _clients.For(s, useProxy: true);
var sw = System.Diagnostics.Stopwatch.StartNew();
try
{
// api.telegram.org is filtered in Iran — a reply means the tunnel reaches the open internet.
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
using var resp = await client.GetAsync("https://api.telegram.org",
HttpCompletionOption.ResponseHeadersRead, cts.Token);
sw.Stop();
ProxyTest = $"✅ پروکسی وصل است — به اینترنت آزاد دسترسی دارد (HTTP {(int)resp.StatusCode}، {sw.ElapsedMilliseconds} میلی‌ثانیه).";
}
catch (Exception ex)
{
ProxyTest = "❌ اتصال از طریق پروکسی ناموفق بود. مطمئن شو سرویس Xray اجراست و کانفیگ معتبر است. خطا: " + ex.Message;
}
return RedirectToPage();
}
/// <summary>Send a sample post to the AI endpoint and show the verdict (validates key/endpoint/proxy).</summary>
public async Task<IActionResult> OnPostTestAiAsync()
{
var s = await _settings.GetAsync();
if (!s.AiEnabled || string.IsNullOrWhiteSpace(s.AiEndpoint))
{ AiTest = "ابتدا «فعال‌سازی هوش مصنوعی» را بزن و آدرس/کلید را ذخیره کن."; return RedirectToPage(); }
const string sample = "استخدام پرستار خانم برای بخش اورژانس بیمارستان میلاد تهران، شیفت شب، حقوق توافقی، تماس ۰۹۱۲۱۲۳۴۵۶۷";
try
{
var r = await _ai.AuditAsync(sample, s);
AiTest = r is null
? "❌ پاسخی از هوش مصنوعی دریافت نشد. کلید/آدرس و (در صورت نیاز) تیک «از طریق پروکسی» را بررسی کن."
: $"✅ هوش مصنوعی پاسخ داد — تصمیم: {r.Decision} | اطمینان: {r.Confidence}٪ | نقش: {r.Data?.Role} | شهر: {r.Data?.City} | شیفت: {r.Data?.ShiftType}";
}
catch (Exception ex) { AiTest = "❌ خطا در تماس با هوش مصنوعی: " + ex.Message; }
return RedirectToPage();
}
public async Task<IActionResult> OnPostTestSmsAsync()
{
var s = await _settings.GetAsync();
@@ -94,10 +94,12 @@ public class OpenAiCompatibleAuditor : IAiAuditor
using var doc = JsonDocument.Parse(json);
var r = doc.RootElement;
// Guard on ValueKind == Number first — TryGetInt32/64 THROW on null/string values
// (the model often returns payAmount/sharePercent as null), which would fail the whole parse.
string? S(string k) => r.TryGetProperty(k, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() : null;
int I(string k, int d) => r.TryGetProperty(k, out var v) && v.TryGetInt32(out var n) ? n : d;
long? L(string k) => r.TryGetProperty(k, out var v) && v.TryGetInt64(out var n) ? n : null;
int? NI(string k) => r.TryGetProperty(k, out var v) && v.TryGetInt32(out var n) ? n : null;
int I(string k, int d) => r.TryGetProperty(k, out var v) && v.ValueKind == JsonValueKind.Number && v.TryGetInt32(out var n) ? n : d;
long? L(string k) => r.TryGetProperty(k, out var v) && v.ValueKind == JsonValueKind.Number && v.TryGetInt64(out var n) ? n : null;
int? NI(string k) => r.TryGetProperty(k, out var v) && v.ValueKind == JsonValueKind.Number && v.TryGetInt32(out var n) ? n : null;
var decision = (S("decision") ?? "review").ToLowerInvariant();
var data = new AiStructured(S("kind"), S("role"), S("city"), S("district"), S("shiftType"),