From 1e3067989af10f16a870da1fa41af22b78f5014c Mon Sep 17 00:00:00 2001 From: oxpoe Date: Thu, 5 Nov 2026 17:49:54 +0700 Subject: [PATCH] feat(settings): tombol Test API di AI (tanpa simpan) - endpoint POST /admin/settings/test-ai (provider, key, base URL, model) - UI status valid/tidak + latensi - versi 0.5.14 --- CHANGELOG.md | 5 +++ README.md | 2 +- package.json | 2 +- routes/adminPortal.js | 70 ++++++++++++++++++++++++++++++++++++++++ version.txt | 2 +- views/admin/settings.ejs | 44 +++++++++++++++++++++++++ 6 files changed, 122 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c307e3b..7a3b9b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ Format mengikuti [Keep a Changelog](https://keepachangelog.com/) dan [Semantic Versioning](https://semver.org/). +## [0.5.14] — 2026-11-05 — "ai-test" + +### Ditambahkan +- Tombol **Test API** di Pengaturan → AI: memvalidasi provider, API key, base URL, dan model yang sedang diisi **tanpa harus menyimpan**. Menampilkan status valid/tidak, HTTP status, dan latensi. + ## [0.5.13] — 2026-11-05 — "lunas-notify" ### Diperbaiki diff --git a/README.md b/README.md index 7c82c86..dc71844 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@

Lisensi: ISC - Versi 0.5.13 (lunas-notify) + Versi 0.5.14 (ai-test) Node.js >= 20 Docker Demo diff --git a/package.json b/package.json index aed4612..9017fb5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-isp", - "version": "0.5.13", + "version": "0.5.14", "description": "OPEN-ISP \u2014 sistem billing & manajemen ISP open-source (PWA pelanggan, multi-OLT, RADIUS, WhatsApp, QRIS)", "main": "app-customer.js", "scripts": { diff --git a/routes/adminPortal.js b/routes/adminPortal.js index 6750f19..a44a9d5 100644 --- a/routes/adminPortal.js +++ b/routes/adminPortal.js @@ -4788,6 +4788,76 @@ router.post('/api/telegram/sync', requireAdminSession, async (req, res) => { } }); +router.post('/settings/test-ai', requireAdminSession, express.json(), async (req, res) => { + const presets = { + deepseek: { baseUrl: 'https://api.deepseek.com', model: 'deepseek-chat' }, + openai: { baseUrl: 'https://api.openai.com/v1', model: 'gpt-4o-mini' }, + groq: { baseUrl: 'https://api.groq.com/openai/v1', model: 'llama-3.3-70b-versatile' }, + openrouter: { baseUrl: 'https://openrouter.ai/api/v1', model: 'meta-llama/llama-3.3-70b-instruct' }, + custom: { baseUrl: '', model: '' } + }; + try { + const b = req.body || {}; + const provider = String(b.provider || 'deepseek').trim().toLowerCase(); + const p = presets[provider] || presets.deepseek; + const apiKey = String(b.apiKey || '').trim(); + const baseUrl = String(b.baseUrl || p.baseUrl || '').trim().replace(/\/+$/, ''); + const model = String(b.model || p.model || '').trim(); + + if (!baseUrl) return res.status(400).json({ ok: false, message: 'Base URL AI kosong.' }); + if (!model) return res.status(400).json({ ok: false, message: 'Model AI kosong.' }); + if (!apiKey && provider !== 'custom') return res.status(400).json({ ok: false, message: 'API key kosong.' }); + + const started = Date.now(); + const controller = new AbortController(); + const timer = setTimeout(function () { controller.abort(); }, 15000); + let r; + let text = ''; + try { + r = await fetch(baseUrl + '/chat/completions', { + method: 'POST', + headers: Object.assign( + { 'Content-Type': 'application/json' }, + apiKey ? { Authorization: 'Bearer ' + apiKey } : {} + ), + body: JSON.stringify({ + model: model, + messages: [{ role: 'user', content: 'ping' }], + max_tokens: 5, + stream: false + }), + signal: controller.signal + }); + text = await r.text(); + } finally { + clearTimeout(timer); + } + const latencyMs = Date.now() - started; + let json = null; + try { json = JSON.parse(text); } catch (e) {} + + if (!r.ok) { + const detail = (json && json.error && (json.error.message || json.error.code)) || text || ('HTTP ' + r.status); + const message = (r.status === 401 || r.status === 403) + ? 'API key tidak valid / tidak punya akses (HTTP ' + r.status + ').' + : 'Gagal (HTTP ' + r.status + '): ' + String(detail).replace(/\s+/g, ' ').slice(0, 200); + return res.json({ ok: false, provider, model, baseUrl, status: r.status, latencyMs, message }); + } + + const reply = (json && json.choices && json.choices[0] && + ((json.choices[0].message && json.choices[0].message.content) || json.choices[0].text)) || ''; + return res.json({ + ok: true, provider, model, baseUrl, status: r.status, latencyMs, + message: 'API valid — ' + (reply ? ('balasan: ' + String(reply).trim().slice(0, 80)) : 'respon OK') + ' (' + latencyMs + ' ms)' + }); + } catch (e) { + const message = e && e.name === 'AbortError' + ? 'Timeout 15 detik — server AI tidak merespons.' + : ('Tidak bisa menghubungi API: ' + (e && e.message ? e.message : String(e))); + return res.status(500).json({ ok: false, message }); + } +}); + router.post('/settings', requireAdminSession, express.urlencoded({ extended: true }), (req, res) => { try { const newSettings = { ...req.body }; diff --git a/version.txt b/version.txt index 7988f13..63df075 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.5.13 \ No newline at end of file +0.5.14 \ No newline at end of file diff --git a/views/admin/settings.ejs b/views/admin/settings.ejs index b8eecf8..ab563e5 100644 --- a/views/admin/settings.ejs +++ b/views/admin/settings.ejs @@ -238,6 +238,10 @@

+
+ + Menguji API key & model yang sedang diisi (tanpa simpan). +
Auto Repair
@@ -1252,6 +1256,46 @@ function setSearch(q) { })(); + +