feat(ai): provider OpenAI-compatible + DeepSeek, AI Telegram, self-healing dengan toggle (port upstream)

- telegramAiService/adminAiTools: dukung openai, deepseek, groq, openrouter, custom (base URL + key + model)
- selfHealingService: billing vs isolir, konsistensi secret PPPoE, sesi RADIUS stale, IP ACS kosong
- mikrotikService: getActivePppoeUsers, isolateCustomerPppoe, unisolateCustomerPppoe (upstream memanggil fungsi yang tidak ada)
- cron 30 menit hanya jalan bila ai_self_healing_enabled=true (default off) + scan brute force MikroTik tiap 2 menit
- Pengaturan: kartu AI & Self-Healing (provider, key/model, toggle, tombol Diagnosa/Perbaiki)
- bot Telegram: pesan non-command dari admin dijawab AI
This commit is contained in:
oxpoe
2026-11-05 01:48:50 +07:00
parent 1c40e6a0e8
commit 4003149046
9 changed files with 2022 additions and 4 deletions

View File

@@ -208,6 +208,55 @@
</div>
</div>
<div class="card mb16">
<div class="card-hd"><h6><i class="bi bi-robot"></i> AI &amp; Self-Healing</h6></div>
<div class="card-bd settings-grid">
<div>
<h6 style="font-size:13px;color:var(--muted);margin-bottom:12px;text-transform:uppercase;letter-spacing:1px"><i class="bi bi-cpu"></i> Provider AI</h6>
<div class="fg">
<label class="fl">Provider</label>
<select class="fs" name="ai_provider">
<% const _aiProv = String(settings.ai_provider || 'deepseek'); %>
<option value="deepseek" <%= _aiProv === 'deepseek' ? 'selected' : '' %>>DeepSeek</option>
<option value="openai" <%= _aiProv === 'openai' ? 'selected' : '' %>>OpenAI</option>
<option value="groq" <%= _aiProv === 'groq' ? 'selected' : '' %>>Groq</option>
<option value="openrouter" <%= _aiProv === 'openrouter' ? 'selected' : '' %>>OpenRouter</option>
<option value="custom" <%= _aiProv === 'custom' ? 'selected' : '' %>>Custom (OpenAI-compatible)</option>
</select>
<div style="font-size:10px;color:var(--muted);margin-top:4px">Semua provider di atas memakai format API OpenAI (chat/completions).</div>
</div>
<div class="fg"><label class="fl">DeepSeek API Key</label><input class="fc" name="deepseek_api_key" value="<%= settings.deepseek_api_key || '' %>" placeholder="sk-..."></div>
<div class="fg"><label class="fl">DeepSeek Model</label><input class="fc" name="deepseek_model" value="<%= settings.deepseek_model || 'deepseek-chat' %>"></div>
<div class="fg"><label class="fl">OpenAI API Key</label><input class="fc" name="openai_api_key" value="<%= settings.openai_api_key || '' %>" placeholder="sk-..."></div>
<div class="fg"><label class="fl">OpenAI Model</label><input class="fc" name="openai_model" value="<%= settings.openai_model || 'gpt-4o-mini' %>"></div>
<div class="fg"><label class="fl">Groq API Key</label><input class="fc" name="groq_api_key" value="<%= settings.groq_api_key || '' %>"></div>
<div class="fg"><label class="fl">Groq Model</label><input class="fc" name="groq_model" value="<%= settings.groq_model || 'llama-3.3-70b-versatile' %>"></div>
<div class="fg"><label class="fl">OpenRouter API Key</label><input class="fc" name="openrouter_api_key" value="<%= settings.openrouter_api_key || '' %>"></div>
<div class="fg"><label class="fl">OpenRouter Model</label><input class="fc" name="openrouter_model" value="<%= settings.openrouter_model || '' %>"></div>
<div class="fg"><label class="fl">Custom Base URL</label><input class="fc" name="custom_ai_base_url" value="<%= settings.custom_ai_base_url || '' %>" placeholder="http://localhost:11434/v1"></div>
<div class="fg"><label class="fl">Custom API Key</label><input class="fc" name="custom_ai_api_key" value="<%= settings.custom_ai_api_key || '' %>" placeholder="opsional untuk Ollama"></div>
<div class="fg"><label class="fl">Custom Model</label><input class="fc" name="custom_ai_model" value="<%= settings.custom_ai_model || 'llama3' %>"></div>
</div>
<div>
<h6 style="font-size:13px;color:var(--muted);margin-bottom:12px;text-transform:uppercase;letter-spacing:1px"><i class="bi bi-activity"></i> Auto Repair</h6>
<div class="fg">
<label class="fl">Jalankan Self-Healing otomatis (tiap 30 menit)</label>
<select class="fs" name="ai_self_healing_enabled">
<option value="false" <%= (settings.ai_self_healing_enabled === true || settings.ai_self_healing_enabled === 'true') ? '' : 'selected' %>>Nonaktif</option>
<option value="true" <%= (settings.ai_self_healing_enabled === true || settings.ai_self_healing_enabled === 'true') ? 'selected' : '' %>>Aktif</option>
</select>
<div style="font-size:10px;color:var(--muted);margin-top:4px">Saat aktif, sistem memeriksa &amp; memperbaiki: status isolir vs tagihan, secret PPPoE hilang/tertukar profil, sesi RADIUS menggantung, dan IP modem ACS kosong.</div>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button type="button" class="btn btn-g" onclick="selfHeal('diagnose')"><i class="bi bi-search"></i> Diagnosa</button>
<button type="button" class="btn btn-p" onclick="selfHeal('run')"><i class="bi bi-wrench-adjustable"></i> Perbaiki Sekarang</button>
</div>
<pre id="selfheal-out" class="mono" style="display:none;margin-top:12px;max-height:280px;overflow:auto;font-size:11px;white-space:pre-wrap"></pre>
</div>
</div>
</div>
<div class="card mb16">
<div class="card-hd"><h6><i class="bi bi-sliders"></i> <%= t('admin.settings.system_title', 'Pengaturan Sistem') %></h6></div>
<div class="card-bd settings-grid">
@@ -1187,5 +1236,20 @@ function setSearch(q) {
document.querySelectorAll('.set-tab').forEach(function (b) { b.addEventListener('click', function () { setSetTab(this.getAttribute('data-cat')); }); });
})();
</script>
<script>
async function selfHeal(mode) {
const out = document.getElementById('selfheal-out');
out.style.display = 'block';
out.textContent = mode === 'run' ? 'Menjalankan perbaikan...' : 'Mendiagnosa...';
try {
const res = await fetch('/admin/api/self-heal/' + (mode === 'run' ? 'run' : 'diagnose'), { method: mode === 'run' ? 'POST' : 'GET' });
const data = await res.json();
out.textContent = JSON.stringify(data, null, 2);
} catch (e) {
out.textContent = 'Gagal: ' + e.message;
}
}
</script>
</body>
</html>