Real channel fetch (Telegram/Bale/Divar) + AI-audited automation engine + CI/CD

- Fetch: Telegram via t.me/s, Bale via Bot API, Divar via web-search (HttpClient, config-gated, graceful)
- AI layer: DB-backed AppSetting (mode auto/manual, thresholds, AI endpoint/model/key/prompt/framework, auto-approve); OpenAI-compatible IAiAuditor (self-host/Iranian endpoints; fails safe to manual)
- Pipeline: fetch → dedupe(hash) → parse → validate → AI audit → Discard/Flag/Queue/auto-publish (resolve-or-create facility)
- Admin: /Admin/Settings automation+AI panel; queue shows confidence + AI verdict; flagged section
- CI/CD: Dockerfile, docker-compose.prod.yml, .gitea/workflows/ci-cd.yml, nginx vhost, DEPLOY.md; forwarded headers + /healthz + prod reference-only seed; ports 22/80/443 only

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-03 17:41:02 +03:30
parent 931b7b6ffb
commit 36bb165438
18 changed files with 1614 additions and 68 deletions
@@ -12,6 +12,7 @@
(@JalaliDate.ToPersianDigits(Model.Queue.Count.ToString()) در صف،
@JalaliDate.ToPersianDigits(Model.Flagged.Count.ToString()) پرچم‌خورده)
· <a asp-page="/Admin/Facilities">تأیید مراکز درمانی</a>
· <a asp-page="/Admin/Settings">تنظیمات جمع‌آوری و AI</a>
</p>
</div>
</div>
@@ -0,0 +1,67 @@
@page
@model JobsMedical.Web.Pages.Admin.SettingsModel
@{
ViewData["Title"] = "تنظیمات جمع‌آوری و هوش مصنوعی";
}
<div class="page-head">
<div class="container">
<h1>تنظیمات جمع‌آوری و هوش مصنوعی</h1>
<p class="muted"><a asp-page="/Admin/Index">← بازگشت به صف</a></p>
</div>
</div>
<div class="container section" style="max-width:680px;">
@if (Model.Saved is not null)
{
<div class="alert alert-success">✓ @Model.Saved</div>
}
<form method="post" class="card card-pad">
<h3 style="margin-top:0;">حالت انتشار</h3>
<div class="filter-group">
<label>نحوه افزودن آگهی‌ها به سایت</label>
<select name="Mode">
<option value="0" selected="@(Model.Mode == JobsMedical.Web.Models.IngestionMode.Manual)">دستی — همه به صف بررسی می‌روند</option>
<option value="1" selected="@(Model.Mode == JobsMedical.Web.Models.IngestionMode.Automatic)">خودکار — موارد تأییدشده مستقیم منتشر می‌شوند</option>
</select>
</div>
<div class="filter-group">
<label>حداقل درصد اطمینان برای انتشار خودکار (بدون هوش مصنوعی)</label>
<input type="number" name="AutoPublishMinConfidence" min="0" max="100" value="@Model.AutoPublishMinConfidence" dir="ltr" />
<p class="muted" style="font-size:12px; margin:4px 0 0;">در حالت خودکار و بدون AI، آگهی‌هایی با اطمینان بالاتر از این مقدار خودکار منتشر می‌شوند.</p>
</div>
<hr style="border:none; border-top:1px solid var(--line); margin:18px 0;" />
<h3 style="margin-top:0;">لایه هوش مصنوعی (اختیاری)</h3>
<div class="filter-group">
<label style="display:flex; align-items:center; gap:8px; font-weight:700;">
<input type="checkbox" name="AiEnabled" value="true" style="width:auto;" checked="@Model.AiEnabled" />
فعال‌سازی بررسی با هوش مصنوعی قبل از انتشار
</label>
<p class="muted" style="font-size:12px; margin:4px 0 0;">در صورت فعال بودن، هر آگهی پیش از انتشار توسط مدل بررسی و تأیید/رد/ساختارمند می‌شود.</p>
</div>
<div class="filter-group">
<label>آدرس سرویس (سازگار با OpenAI)</label>
<input type="text" name="AiEndpoint" value="@Model.AiEndpoint" placeholder="https://host/v1/chat/completions" dir="ltr" />
<p class="muted" style="font-size:12px; margin:4px 0 0;">می‌تواند یک مدل self-hosted یا سرویس داخلی باشد (OpenAI/Anthropic در ایران مسدودند).</p>
</div>
<div class="filter-group" style="display:flex; gap:8px;">
<div style="flex:1;"><label>کلید API</label><input type="password" name="AiApiKey" value="@Model.AiApiKey" dir="ltr" /></div>
<div style="flex:1;"><label>نام مدل</label><input type="text" name="AiModel" value="@Model.AiModel" dir="ltr" /></div>
</div>
<div class="filter-group">
<label>دستور و چارچوب هوش مصنوعی (Prompt / Framework)</label>
<textarea name="AiSystemPrompt" rows="10" dir="rtl">@Model.AiSystemPrompt</textarea>
<p class="muted" style="font-size:12px; margin:4px 0 0;">به مدل بگو چطور تأیید/رد کند و چه فیلدهایی را استخراج کند. خروجی باید JSON باشد.</p>
</div>
<div class="filter-group">
<label style="display:flex; align-items:center; gap:8px; font-weight:700;">
<input type="checkbox" name="AiAutoApprove" value="true" style="width:auto;" checked="@Model.AiAutoApprove" />
در حالت خودکار، آگهی‌هایی که AI تأیید می‌کند مستقیم منتشر شوند
</label>
</div>
<button type="submit" class="btn btn-accent btn-block btn-lg">ذخیره تنظیمات</button>
</form>
</div>
@@ -0,0 +1,54 @@
using JobsMedical.Web.Models;
using JobsMedical.Web.Services.Scraping;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace JobsMedical.Web.Pages.Admin;
[Authorize(Roles = "Admin")]
public class SettingsModel : PageModel
{
private readonly SettingsService _settings;
public SettingsModel(SettingsService settings) => _settings = settings;
[BindProperty] public IngestionMode Mode { get; set; }
[BindProperty] public int AutoPublishMinConfidence { get; set; }
[BindProperty] public bool AiEnabled { get; set; }
[BindProperty] public string? AiEndpoint { get; set; }
[BindProperty] public string? AiApiKey { get; set; }
[BindProperty] public string? AiModel { get; set; }
[BindProperty] public string AiSystemPrompt { get; set; } = "";
[BindProperty] public bool AiAutoApprove { get; set; }
[TempData] public string? Saved { get; set; }
public async Task OnGetAsync()
{
var s = await _settings.GetAsync();
Mode = s.Mode;
AutoPublishMinConfidence = s.AutoPublishMinConfidence;
AiEnabled = s.AiEnabled;
AiEndpoint = s.AiEndpoint;
AiApiKey = s.AiApiKey;
AiModel = s.AiModel;
AiSystemPrompt = s.AiSystemPrompt;
AiAutoApprove = s.AiAutoApprove;
}
public async Task<IActionResult> OnPostAsync()
{
await _settings.SaveAsync(new AppSetting
{
Mode = Mode,
AutoPublishMinConfidence = AutoPublishMinConfidence,
AiEnabled = AiEnabled,
AiEndpoint = AiEndpoint,
AiApiKey = AiApiKey,
AiModel = AiModel,
AiSystemPrompt = AiSystemPrompt,
AiAutoApprove = AiAutoApprove,
});
Saved = "تنظیمات ذخیره شد.";
return RedirectToPage();
}
}