mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-17 04:10:04 +02:00
web: panneau Acces OpenAI (endpoint /v1 + cle API masquee, gestion depuis le web)
Nouvel endpoint /api/apikey (GET etat + POST generate/set/clear -> restart). Section sidebar affichant l'URL compatible OpenAI et la cle API masquee par defaut (reveler/copier/generer/definir/retirer). Refactor config.go: helpers genAPIKey/writeAPIKey/maskAPIKey partages CLI + web.
This commit is contained in:
1 parent
d2e31ad6ee
commit
9a93702313
3 files changed
+135
-12
No files matched your search
@@ -31,6 +31,40 @@ func authHeader(req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// genAPIKey returns a fresh random OpenAI-style completion key.
|
||||
func genAPIKey() string {
|
||||
buf := make([]byte, 24)
|
||||
_, _ = rand.Read(buf)
|
||||
return "sk-jean-" + hex.EncodeToString(buf)
|
||||
}
|
||||
|
||||
// writeAPIKey persists (key != "") or clears (key == "") the completion API key
|
||||
// in $JEAN_HOME/.api_key. It also wipes any residual API_KEY in config.env to
|
||||
// avoid ambiguity. It does NOT restart the service — callers decide when to
|
||||
// apply (llama-server only reads --api-key at launch).
|
||||
func writeAPIKey(key string) error {
|
||||
_ = SetConfigKey("API_KEY", "")
|
||||
if key == "" {
|
||||
if err := os.Remove(apiKeyPath()); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return os.WriteFile(apiKeyPath(), []byte(key+"\n"), 0o600)
|
||||
}
|
||||
|
||||
// maskAPIKey renders a key for display: keep the "sk-jean-" prefix and last 4
|
||||
// chars, elide the middle. Empty in → empty out.
|
||||
func maskAPIKey(k string) string {
|
||||
if k == "" {
|
||||
return ""
|
||||
}
|
||||
if len(k) <= 12 {
|
||||
return "…" + k[len(k)-2:]
|
||||
}
|
||||
return k[:8] + "…" + k[len(k)-4:]
|
||||
}
|
||||
|
||||
// cmdSetAPIKey sets (or clears) the API key stored in $JEAN_HOME/.api_key. When
|
||||
// exposed on the internet, llama-server requires "Authorization: Bearer <clé>"
|
||||
// for every call.
|
||||
@@ -42,11 +76,7 @@ func cmdSetAPIKey(args []string) error {
|
||||
var key string
|
||||
switch {
|
||||
case len(args) == 0:
|
||||
buf := make([]byte, 24)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
return err
|
||||
}
|
||||
key = "sk-jean-" + hex.EncodeToString(buf)
|
||||
key = genAPIKey()
|
||||
fmt.Printf("%s clé générée : %s\n", green("[ok]"), bold(key))
|
||||
case args[0] == "" || args[0] == "off" || args[0] == "none":
|
||||
key = ""
|
||||
@@ -55,12 +85,7 @@ func cmdSetAPIKey(args []string) error {
|
||||
}
|
||||
// Stocke dans le fichier dédié (survit aux switches de preset). On nettoie
|
||||
// aussi un éventuel API_KEY résiduel dans config.env pour éviter la confusion.
|
||||
_ = SetConfigKey("API_KEY", "")
|
||||
if key == "" {
|
||||
if err := os.Remove(apiKeyPath()); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
} else if err := os.WriteFile(apiKeyPath(), []byte(key+"\n"), 0o600); err != nil {
|
||||
if err := writeAPIKey(key); err != nil {
|
||||
return err
|
||||
}
|
||||
if key == "" {
|
||||
|
||||
+54
-1
@@ -329,6 +329,25 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
<details><summary>System prompt</summary>
|
||||
<textarea id="sysprompt" placeholder="(optionnel) ex: tu es un assistant concis en français" oninput="saveSys()" style="width:100%;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:8px;font:inherit;min-height:70px;resize:vertical"></textarea>
|
||||
</details>
|
||||
<details><summary>Accès OpenAI</summary>
|
||||
<div class="subhead">Endpoint compatible OpenAI<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Branche n'importe quel client compatible OpenAI (OpenCode, Continue, Cursor…) sur cette URL. Modèle : <code>jean</code>. Si une clé est définie, le client doit envoyer <code>Authorization: Bearer <clé></code>.</span></span></div>
|
||||
<div class="row" style="margin-top:4px">
|
||||
<input id="oai-url" readonly style="flex:1;min-width:0;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px;font:inherit;font-size:12px">
|
||||
<button onclick="copyText(document.getElementById('oai-url').value,'URL copiée')" title="copier">⧉</button>
|
||||
</div>
|
||||
<div class="muted" style="font-size:11px;margin-top:3px">modèle : <code>jean</code></div>
|
||||
<div class="subhead" style="margin-top:12px">Clé API<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Protège l'endpoint des complétions (llama-server). Indispensable avant d'exposer le serveur sur internet. Modifier la clé <b>redémarre</b> le service.</span></span></div>
|
||||
<div class="row" style="margin-top:4px">
|
||||
<input id="oai-key" readonly style="flex:1;min-width:0;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px;font:inherit;font-size:12px">
|
||||
<button id="oai-key-eye" onclick="toggleKeyReveal()" title="afficher / masquer">👁</button>
|
||||
<button onclick="copyApiKey()" title="copier la clé">⧉</button>
|
||||
</div>
|
||||
<div class="row" style="margin-top:6px">
|
||||
<button onclick="apiKeyAction('generate')">générer</button>
|
||||
<button onclick="apiKeySet()">définir…</button>
|
||||
<button onclick="apiKeyAction('clear')">retirer</button>
|
||||
</div>
|
||||
</details>
|
||||
<details><summary>Service</summary>
|
||||
<div class="row">
|
||||
<button onclick="act('start')">start</button>
|
||||
@@ -726,6 +745,40 @@ function renderInternet(s){
|
||||
else { st.innerHTML='⚠️ serveur injoignable — les outils web ne seront pas proposés'; }
|
||||
}
|
||||
async function loadInternet(){ renderInternet(await jget('/api/internet')); }
|
||||
// --- Accès OpenAI (endpoint /v1 + clé API des complétions) -----------------
|
||||
let OAI_KEY='', OAI_REVEAL=false;
|
||||
async function copyText(txt, msg){
|
||||
if(!txt){ toast('rien à copier'); return; }
|
||||
try{ await navigator.clipboard.writeText(txt); }
|
||||
catch(_){ const ta=document.createElement('textarea'); ta.value=txt; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); }
|
||||
toast(msg||'copié');
|
||||
}
|
||||
function copyApiKey(){ copyText(OAI_KEY, OAI_KEY?'clé copiée':'aucune clé'); }
|
||||
function renderApiKey(d){
|
||||
OAI_KEY = d.key || '';
|
||||
// URL de l'endpoint : llama-server tourne sur d.port (≠ port de l'UI web).
|
||||
const proto = location.protocol==='https:' ? 'https:' : 'http:';
|
||||
document.getElementById('oai-url').value = proto+'//'+location.hostname+':'+d.port+'/v1';
|
||||
const inp=document.getElementById('oai-key');
|
||||
if(!d.set){ inp.value='(aucune clé — serveur ouvert)'; inp.style.opacity=.6; }
|
||||
else { inp.style.opacity=1; inp.value = OAI_REVEAL ? OAI_KEY : d.masked; }
|
||||
document.getElementById('oai-key-eye').style.display = d.set ? '' : 'none';
|
||||
}
|
||||
async function loadApiKey(){ renderApiKey(await jget('/api/apikey')); }
|
||||
function toggleKeyReveal(){ OAI_REVEAL=!OAI_REVEAL; const inp=document.getElementById('oai-key'); if(OAI_KEY) inp.value = OAI_REVEAL ? OAI_KEY : (OAI_KEY.slice(0,8)+'…'+OAI_KEY.slice(-4)); }
|
||||
async function apiKeyAction(action){
|
||||
if(action==='clear' && !await askConfirm('Retirer la clé rend l\'endpoint OpenAI accessible SANS authentification. Le service va redémarrer.', {title:'Retirer la clé API ?', okText:'Retirer'})) return;
|
||||
if(action==='generate' && OAI_KEY && !await askConfirm('Générer une nouvelle clé invalide l\'ancienne (les clients devront être mis à jour). Le service va redémarrer.', {title:'Régénérer la clé ?', okText:'Générer'})) return;
|
||||
OAI_REVEAL=(action==='generate'); // révèle la clé fraîche pour qu'on puisse la copier
|
||||
toast('application…');
|
||||
renderApiKey(await jpost('/api/apikey', {action}));
|
||||
}
|
||||
async function apiKeySet(){
|
||||
const k = await askPrompt('Colle ta clé API (ou laisse vide pour annuler) :', {title:'Définir la clé API', placeholder:'sk-…'});
|
||||
if(!k || !k.trim()) return;
|
||||
OAI_REVEAL=true; toast('application…');
|
||||
renderApiKey(await jpost('/api/apikey', {action:'set', key:k.trim()}));
|
||||
}
|
||||
async function toggleInternet(){
|
||||
const on=document.getElementById('internet-toggle').checked;
|
||||
const url=document.getElementById('crawl-url').value.trim();
|
||||
@@ -736,7 +789,7 @@ async function saveCrawlUrl(){
|
||||
const url=document.getElementById('crawl-url').value.trim();
|
||||
renderInternet(await jpost('/api/internet',{url}));
|
||||
}
|
||||
async function loadAll(){ await Promise.all([loadStatus(),loadVram(),loadCfg(),loadPresets(),loadAgent(),loadInternet()]); }
|
||||
async function loadAll(){ await Promise.all([loadStatus(),loadVram(),loadCfg(),loadPresets(),loadAgent(),loadInternet(),loadApiKey()]); }
|
||||
async function act(a){ toast(a+'…'); await jpost('/api/'+a); setTimeout(loadAll,1500); }
|
||||
function openBenchModal(){ document.getElementById('bench-modal').style.display = 'flex'; }
|
||||
function closeBenchModal(){ document.getElementById('bench-modal').style.display = 'none'; }
|
||||
|
||||
@@ -86,6 +86,7 @@ func newWebMux() *http.ServeMux {
|
||||
api("/api/agent", handleAgent)
|
||||
api("/api/agent/toggle", handleAgentToggle)
|
||||
api("/api/agent/tool-limit", handleToolLimitToggle)
|
||||
api("/api/apikey", handleAPIKey)
|
||||
api("/api/internet", handleInternet)
|
||||
api("/api/memory", handleMemoryMode)
|
||||
// Alias rétro-compat : l'ancien portail ajean.link (dépôt jean-relay) pilote
|
||||
@@ -505,6 +506,50 @@ func handleAgentToggle(w http.ResponseWriter, r *http.Request) {
|
||||
//
|
||||
// GET → {enabled, url, reachable}
|
||||
// POST {enabled, url} → enregistre CRAWL4AI_URL + le drapeau .internet_enabled
|
||||
// handleAPIKey expose et pilote la clé d'accès à l'endpoint compatible OpenAI
|
||||
// (llama-server /v1). GET renvoie l'état ; POST {action:"generate"|"set"|"clear",
|
||||
// key?} l'écrit puis redémarre le service (llama-server lit --api-key au lancement).
|
||||
func handleAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPost {
|
||||
var req struct {
|
||||
Action string `json:"action"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
var key string
|
||||
switch req.Action {
|
||||
case "generate":
|
||||
key = genAPIKey()
|
||||
case "set":
|
||||
key = strings.TrimSpace(req.Key)
|
||||
case "clear":
|
||||
key = ""
|
||||
default:
|
||||
sendJSON(w, 400, map[string]any{"ok": false, "error": "action inconnue"})
|
||||
return
|
||||
}
|
||||
if err := writeAPIKey(key); err != nil {
|
||||
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
// La clé n'est appliquée qu'au (re)démarrage de llama-server.
|
||||
if serviceIsActive() {
|
||||
_ = serviceAction("restart")
|
||||
}
|
||||
}
|
||||
k := readAPIKey()
|
||||
sendJSON(w, 200, map[string]any{
|
||||
"ok": true,
|
||||
"set": k != "",
|
||||
"key": k,
|
||||
"masked": maskAPIKey(k),
|
||||
"port": LLMPort(),
|
||||
})
|
||||
}
|
||||
|
||||
func handleInternet(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPost {
|
||||
var req struct {
|
||||
|
||||
Reference in new issue
Block a user