[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>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace JobsMedical.Web.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AiUseProxy : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "AiUseProxy",
|
||||||
|
table: "AppSettings",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "AiUseProxy",
|
||||||
|
table: "AppSettings");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,6 +53,9 @@ namespace JobsMedical.Web.Migrations
|
|||||||
.HasMaxLength(4000)
|
.HasMaxLength(4000)
|
||||||
.HasColumnType("character varying(4000)");
|
.HasColumnType("character varying(4000)");
|
||||||
|
|
||||||
|
b.Property<bool>("AiUseProxy")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.Property<bool>("AutoIngestEnabled")
|
b.Property<bool>("AutoIngestEnabled")
|
||||||
.HasColumnType("boolean");
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ public class AppSetting
|
|||||||
/// <summary>If AI approves AND Mode is Automatic, publish without human review.</summary>
|
/// <summary>If AI approves AND Mode is Automatic, publish without human review.</summary>
|
||||||
public bool AiAutoApprove { get; set; } = false;
|
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) ---
|
// --- Channel scraping sources (configured here, NOT in env) ---
|
||||||
/// <summary>Run the ingestion worker on a timer.</summary>
|
/// <summary>Run the ingestion worker on a timer.</summary>
|
||||||
public bool AutoIngestEnabled { get; set; } = false;
|
public bool AutoIngestEnabled { get; set; } = false;
|
||||||
|
|||||||
@@ -73,6 +73,11 @@
|
|||||||
<input type="checkbox" name="AiAutoApprove" value="true" checked="@Model.AiAutoApprove" />
|
<input type="checkbox" name="AiAutoApprove" value="true" checked="@Model.AiAutoApprove" />
|
||||||
<span class="t-body"><span>در حالت خودکار، آگهیهایی که AI تأیید میکند مستقیم منتشر شوند</span></span>
|
<span class="t-body"><span>در حالت خودکار، آگهیهایی که AI تأیید میکند مستقیم منتشر شوند</span></span>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="toggle-row">
|
||||||
|
<input type="checkbox" name="AiUseProxy" value="true" checked="@Model.AiUseProxy" />
|
||||||
|
<span class="t-body"><span>ارسال درخواست هوش مصنوعی از طریق پروکسی</span>
|
||||||
|
<span class="t-hint">برای دسترسی به سرویسهایی مثل OpenAI از داخل ایران؛ از همان آدرس پروکسی تب «منابع جمعآوری» استفاده میکند.</span></span>
|
||||||
|
</label>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- SOURCES -->
|
<!-- SOURCES -->
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ public class SettingsModel : PageModel
|
|||||||
[BindProperty] public string? AiModel { get; set; }
|
[BindProperty] public string? AiModel { get; set; }
|
||||||
[BindProperty] public string AiSystemPrompt { get; set; } = "";
|
[BindProperty] public string AiSystemPrompt { get; set; } = "";
|
||||||
[BindProperty] public bool AiAutoApprove { get; set; }
|
[BindProperty] public bool AiAutoApprove { get; set; }
|
||||||
|
[BindProperty] public bool AiUseProxy { get; set; }
|
||||||
// Channel scraping sources
|
// Channel scraping sources
|
||||||
[BindProperty] public bool AutoIngestEnabled { get; set; }
|
[BindProperty] public bool AutoIngestEnabled { get; set; }
|
||||||
[BindProperty] public int IngestIntervalMinutes { get; set; } = 30;
|
[BindProperty] public int IngestIntervalMinutes { get; set; } = 30;
|
||||||
@@ -76,6 +77,7 @@ public class SettingsModel : PageModel
|
|||||||
AiModel = s.AiModel;
|
AiModel = s.AiModel;
|
||||||
AiSystemPrompt = s.AiSystemPrompt;
|
AiSystemPrompt = s.AiSystemPrompt;
|
||||||
AiAutoApprove = s.AiAutoApprove;
|
AiAutoApprove = s.AiAutoApprove;
|
||||||
|
AiUseProxy = s.AiUseProxy;
|
||||||
AutoIngestEnabled = s.AutoIngestEnabled;
|
AutoIngestEnabled = s.AutoIngestEnabled;
|
||||||
IngestIntervalMinutes = s.IngestIntervalMinutes;
|
IngestIntervalMinutes = s.IngestIntervalMinutes;
|
||||||
TelegramEnabled = s.TelegramEnabled;
|
TelegramEnabled = s.TelegramEnabled;
|
||||||
@@ -120,6 +122,7 @@ public class SettingsModel : PageModel
|
|||||||
AiModel = AiModel,
|
AiModel = AiModel,
|
||||||
AiSystemPrompt = AiSystemPrompt,
|
AiSystemPrompt = AiSystemPrompt,
|
||||||
AiAutoApprove = AiAutoApprove,
|
AiAutoApprove = AiAutoApprove,
|
||||||
|
AiUseProxy = AiUseProxy,
|
||||||
AutoIngestEnabled = AutoIngestEnabled,
|
AutoIngestEnabled = AutoIngestEnabled,
|
||||||
IngestIntervalMinutes = IngestIntervalMinutes,
|
IngestIntervalMinutes = IngestIntervalMinutes,
|
||||||
TelegramEnabled = TelegramEnabled,
|
TelegramEnabled = TelegramEnabled,
|
||||||
|
|||||||
@@ -30,12 +30,12 @@ public interface IAiAuditor
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class OpenAiCompatibleAuditor : IAiAuditor
|
public class OpenAiCompatibleAuditor : IAiAuditor
|
||||||
{
|
{
|
||||||
private readonly IHttpClientFactory _http;
|
private readonly ScrapeHttpClients _clients;
|
||||||
private readonly ILogger<OpenAiCompatibleAuditor> _log;
|
private readonly ILogger<OpenAiCompatibleAuditor> _log;
|
||||||
|
|
||||||
public OpenAiCompatibleAuditor(IHttpClientFactory http, ILogger<OpenAiCompatibleAuditor> log)
|
public OpenAiCompatibleAuditor(ScrapeHttpClients clients, ILogger<OpenAiCompatibleAuditor> log)
|
||||||
{
|
{
|
||||||
_http = http;
|
_clients = clients;
|
||||||
_log = log;
|
_log = log;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,8 +57,7 @@ public class OpenAiCompatibleAuditor : IAiAuditor
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
var client = _http.CreateClient("ai");
|
var client = _clients.ForAi(s); // proxy-aware when AiUseProxy is on (e.g. OpenAI from Iran)
|
||||||
client.Timeout = TimeSpan.FromSeconds(30);
|
|
||||||
using var req = new HttpRequestMessage(HttpMethod.Post, s.AiEndpoint)
|
using var req = new HttpRequestMessage(HttpMethod.Post, s.AiEndpoint)
|
||||||
{
|
{
|
||||||
Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"),
|
Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"),
|
||||||
|
|||||||
@@ -27,14 +27,29 @@ public sealed class ScrapeHttpClients : IDisposable
|
|||||||
? s.IngestProxyUrl.Trim()
|
? s.IngestProxyUrl.Trim()
|
||||||
: "direct";
|
: "direct";
|
||||||
|
|
||||||
// Drop stale clients if the proxy URL changed (keep only "direct" + the current proxy).
|
// Drop stale clients if the proxy URL changed (keep "direct", current proxy, and AI clients).
|
||||||
foreach (var k in _cache.Keys)
|
foreach (var k in _cache.Keys)
|
||||||
if (k != "direct" && k != key && _cache.TryRemove(k, out var stale))
|
if (k != "direct" && k != key && !k.StartsWith("ai:") && _cache.TryRemove(k, out var stale))
|
||||||
stale.Dispose();
|
stale.Dispose();
|
||||||
|
|
||||||
return _cache.GetOrAdd(key, Build);
|
return _cache.GetOrAdd(key, Build);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>HttpClient for AI calls — routed through the proxy when AiUseProxy is on (e.g. to
|
||||||
|
/// reach api.openai.com from Iran). Longer timeout; cached per proxy URL.</summary>
|
||||||
|
public HttpClient ForAi(AppSetting s)
|
||||||
|
{
|
||||||
|
var useProxy = s.AiUseProxy && !string.IsNullOrWhiteSpace(s.IngestProxyUrl);
|
||||||
|
var url = useProxy ? s.IngestProxyUrl!.Trim() : null;
|
||||||
|
var key = "ai:" + (url ?? "direct");
|
||||||
|
return _cache.GetOrAdd(key, _ =>
|
||||||
|
{
|
||||||
|
var handler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.All };
|
||||||
|
if (url is not null) { handler.Proxy = new WebProxy(url); handler.UseProxy = true; }
|
||||||
|
return new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(100) }; // LLMs can be slow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private static HttpClient Build(string key)
|
private static HttpClient Build(string key)
|
||||||
{
|
{
|
||||||
var handler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.All };
|
var handler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.All };
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ public class SettingsService
|
|||||||
s.AiSystemPrompt = string.IsNullOrWhiteSpace(incoming.AiSystemPrompt)
|
s.AiSystemPrompt = string.IsNullOrWhiteSpace(incoming.AiSystemPrompt)
|
||||||
? AppSetting.DefaultPrompt : incoming.AiSystemPrompt;
|
? AppSetting.DefaultPrompt : incoming.AiSystemPrompt;
|
||||||
s.AiAutoApprove = incoming.AiAutoApprove;
|
s.AiAutoApprove = incoming.AiAutoApprove;
|
||||||
|
s.AiUseProxy = incoming.AiUseProxy;
|
||||||
// Channel scraping sources
|
// Channel scraping sources
|
||||||
s.AutoIngestEnabled = incoming.AutoIngestEnabled;
|
s.AutoIngestEnabled = incoming.AutoIngestEnabled;
|
||||||
s.IngestIntervalMinutes = Math.Max(1, incoming.IngestIntervalMinutes);
|
s.IngestIntervalMinutes = Math.Max(1, incoming.IngestIntervalMinutes);
|
||||||
|
|||||||
Reference in New Issue
Block a user