Files
hamkadr/src/JobsMedical.Web/Models/AppSetting.cs
T
soroush.asadi 0c49b89891
CI/CD / CI · dotnet build (push) Successful in 1m46s
CI/CD / Deploy · hamkadr (push) Failing after 1m58s
[AI] Route AI calls through the Xray/V2Ray proxy (reach OpenAI from Iran)
Add AiUseProxy setting + a toggle in the AI settings section. ScrapeHttpClients.ForAi(settings) returns a proxied HttpClient (reusing IngestProxyUrl, 100s timeout) when AiUseProxy is on, otherwise direct; AI-cache keys are protected from the scrape-client cleanup. OpenAiCompatibleAuditor now uses it, so the AI auditor (e.g. api.openai.com) is reachable through the same Xray sidecar that serves Telegram. Migration adds the column.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 22:55:07 +03:30

129 lines
7.1 KiB
C#
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.ComponentModel.DataAnnotations;
namespace JobsMedical.Web.Models;
/// <summary>
/// Single-row (Id=1) platform settings the admin controls at runtime — chiefly the ingestion
/// automation policy and the optional AI audit layer. Kept in the DB (not appsettings) so it's
/// editable from the admin panel without a redeploy.
/// </summary>
public class AppSetting
{
public int Id { get; set; } = 1;
// --- Ingestion automation ---
public IngestionMode Mode { get; set; } = IngestionMode.Manual;
/// <summary>In Automatic mode WITHOUT AI, listings at/above this confidence auto-publish.</summary>
public int AutoPublishMinConfidence { get; set; } = 85;
// --- AI audit layer (optional) ---
public bool AiEnabled { get; set; } = false;
/// <summary>OpenAI-compatible chat-completions endpoint (self-hosted or Iranian provider).</summary>
[MaxLength(500)] public string? AiEndpoint { get; set; }
[MaxLength(200)] public string? AiApiKey { get; set; }
[MaxLength(120)] public string? AiModel { get; set; } = "gpt-4o-mini";
/// <summary>The prompt + "framework" the AI follows to approve / reject / structure a listing.</summary>
[MaxLength(4000)]
public string AiSystemPrompt { get; set; } = DefaultPrompt;
/// <summary>If AI approves AND Mode is Automatic, publish without human review.</summary>
public bool AiAutoApprove { get; set; } = false;
/// <summary>Route AI calls through the ingestion proxy (IngestProxyUrl) — needed when the AI
/// endpoint (e.g. api.openai.com) is blocked in Iran.</summary>
public bool AiUseProxy { get; set; } = false;
// --- Channel scraping sources (configured here, NOT in env) ---
/// <summary>Run the ingestion worker on a timer.</summary>
public bool AutoIngestEnabled { get; set; } = false;
public int IngestIntervalMinutes { get; set; } = 30;
public bool TelegramEnabled { get; set; } = false;
/// <summary>Public Telegram channel usernames, one per line or comma-separated.</summary>
[MaxLength(2000)] public string? TelegramChannels { get; set; }
public bool BaleEnabled { get; set; } = false;
[MaxLength(200)] public string? BaleBotToken { get; set; }
/// <summary>Demo mode — keep the sample Tehran board seeded/visible (for showcasing).</summary>
public bool DemoMode { get; set; } = false;
public bool WebsitesEnabled { get; set; } = false;
/// <summary>Generic web pages to scrape, one URL per line.</summary>
[MaxLength(4000)] public string? WebsiteUrls { get; set; }
/// <summary>Local proxy an Xray/V2Ray client sidecar exposes, e.g. socks5://xray:10808
/// (also accepts socks4:// or http://). The app cannot read vmess/vless/trojan directly;
/// the sidecar converts that config into this local proxy. Per-source toggles below decide
/// which channels actually route through it.</summary>
[MaxLength(200)] public string? IngestProxyUrl { get; set; }
/// <summary>Legacy global flag — kept for compatibility; per-source flags below now control routing.</summary>
public bool IngestProxyEnabled { get; set; } = false;
// Per-source: route this source's fetches through IngestProxyUrl (only when a URL is set).
public bool TelegramUseProxy { get; set; } = false;
public bool BaleUseProxy { get; set; } = false;
public bool DivarUseProxy { get; set; } = false;
public bool MedjobsUseProxy { get; set; } = false;
public bool WebsitesUseProxy { get; set; } = false;
public bool DivarEnabled { get; set; } = false;
[MaxLength(60)] public string? DivarCity { get; set; } = "tehran";
/// <summary>Divar search terms, one per line or comma-separated.</summary>
[MaxLength(2000)] public string? DivarQueries { get; set; }
/// <summary>Scrape medjobs.ir job ads (WordPress classifieds — crawled via its sitemaps).</summary>
public bool MedjobsEnabled { get; set; } = false;
/// <summary>Max ads to fetch per ingestion run (be polite; dedupe skips already-seen).</summary>
public int MedjobsMaxAds { get; set; } = 40;
// --- SMS OTP (Kavenegar). When off, the code is shown on screen (dev only). ---
public bool SmsEnabled { get; set; } = false;
[MaxLength(200)] public string? SmsApiKey { get; set; }
/// <summary>Kavenegar verify/lookup template name (preferred OTP method in Iran).</summary>
[MaxLength(100)] public string? SmsTemplate { get; set; }
/// <summary>Sender line for plain SMS fallback when no template is set.</summary>
[MaxLength(30)] public string? SmsSender { get; set; }
/// <summary>Neshan web map.js API key — enables the click-to-pick map on the facility form
/// (Google Maps is blocked in Iran). Empty → only the "my location" button is shown.</summary>
[MaxLength(200)] public string? NeshanMapKey { get; set; }
// --- Notification channels (master on/off, controlled from the admin panel) ---
/// <summary>Live in-app / web notifications (SSE bell + toast + local OS popup). Works in Iran
/// because it streams over our own origin — no external push service. On by default.</summary>
public bool WebNotificationsEnabled { get; set; } = true;
// --- Web Push (PWA notifications). VAPID keypair; generate once with the web-push tooling. ---
public bool PushEnabled { get; set; } = false;
[MaxLength(200)] public string? VapidPublicKey { get; set; }
[MaxLength(200)] public string? VapidPrivateKey { get; set; }
[MaxLength(120)] public string? VapidSubject { get; set; } = "mailto:admin@hamkadr.ir";
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
/// <summary>Split a textarea (newline/comma separated) into trimmed non-empty items.</summary>
public static List<string> SplitList(string? s) => string.IsNullOrWhiteSpace(s)
? new()
: s.Split(new[] { '\n', '\r', ',', '،' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.ToList();
public const string DefaultPrompt = """
تو دستیار بررسی آگهیهای کاری حوزه درمان برای پلتفرم «همکادر» هستی.
هر آگهی خام را بخوان و تصمیم بگیر:
- approve: آگهی واقعی و مرتبط با شیفت/استخدام کادر درمان است و اطلاعات کافی دارد.
- reject: تبلیغ، اسپم، نامرتبط، یا فاقد اطلاعات حداقلی است.
- review: مرتبط است اما ناقص/مبهم و نیاز به بررسی انسانی دارد.
نقش، شهر/محله، نوع شیفت، نوع همکاری، مبلغ یا درصد سهم، و عنوان را در صورت وجود استخراج کن.
فقط با یک شیء JSON پاسخ بده با کلیدهای:
decision (approve|reject|review)، confidence (0-100)، reason (فارسی کوتاه)،
kind (shift|job)، role، city، district، shiftType (day|evening|night|oncall)،
employmentType (fulltime|parttime|contract|plan)، payAmount (عدد تومان یا null)،
sharePercent (0-100 یا null)، title، facilityName.
""";
}