[Admin] VPN/proxy + AI test buttons; fix AI JSON parse crash on null fields
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:
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user