mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Portage des v0.9.9 → v0.10.2 de l'amont, la dernière vraie fonctionnalité qui nous manquait. Une consigne, une fréquence (« @every 2h », « tous les jours à 9h », ou une expression cron à 5 champs, dans le fuseau du navigateur), et l'IA l'exécute seule en arrière-plan. Preset épinglé par tâche (bascule de modèle avant l'exécution, attente du rechargement), accès mémoire et web réglables, interrupteur maître pour tout suspendre, bouton « tester maintenant ». Le planificateur est une goroutine, un tic par minute, dans le process qui détient la conversation et parle au moteur. Une seule tâche part par tic : de toute façon une seule inférence tourne à la fois, et étaler les départs évite qu'une rafale monopolise le modèle. Occupé ou modèle en cours de chargement n'est pas un échec — la tâche repasse au tic suivant. Une tâche ne partage QU'UN point avec le chat : le verrou de génération. Ni messages, ni journal d'affichage, ni epoch — elle construit son fil éphémère et le jette, ne gardant que son texte final comme compte-rendu (borné à 4000 caractères, réinjecté au passage suivant pour la continuité). Deux adaptations, parce que loki n'est pas l'amont : — Dossier de travail. Ici il appartient à la DISCUSSION ouverte : une tâche y aurait déposé ses fichiers, et en aurait changé en cours de route si l'utilisateur changeait de discussion — pour disparaître avec elle à la suppression. Chaque tâche a donc le sien (workspace/tasks/<id>/), stable d'un passage à l'autre. La bascule ne touche que les points d'entrée des OUTILS (agentCwd) : le panneau Fichiers, les dépôts et les liens des messages continuent de suivre la discussion de l'utilisateur. — Capacités. Mem est posé explicitement à MemOff quand l'agent est coupé : le zéro de MemMode est la chaîne vide, qu'EnabledTools ne reconnaît pas comme « coupée » — une tâche sans agent se serait vu offrir les outils mem_*. Le mode code reste off : rôles, critères et passe de vérification n'ont pas de sens sans personne en face. Le refus d'un message pendant qu'une tâche tourne dit maintenant LAQUELLE occupe le modèle : « génération en cours » sur un fil vide et immobile n'expliquait rien. Interface : section repliable dans les réglages (liste, état, prochain passage, pastille), modale d'édition bâtie sur le gabarit de l'éditeur de preset, panneau « dernier résultat » en markdown. Vérifié dans un vrai navigateur — création, rendu, réouverture en édition, bascule intervalle/cron, interrupteur maître, suppression — sans une seule erreur JS.
182 lines
5.7 KiB
Go
182 lines
5.7 KiB
Go
// web_tasks.go — endpoints de pilotage des tâches planifiées (voir tasks.go).
|
|
// Tous derrière la clé de pilotage (enregistrés via api(...) dans newWebMux).
|
|
package loki
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// handleTasks : GET → liste des tâches + état de l'interrupteur maître + si le
|
|
// mode agent est actif (sans lui, une tâche n'a aucun outil pour agir).
|
|
//
|
|
// Les horodatages (next_run, last_run) partent en epoch ms BRUTS : c'est le
|
|
// navigateur qui les met en forme, dans le fuseau de l'utilisateur. Les formater
|
|
// ici les figerait dans le fuseau du SERVEUR (souvent UTC), d'où un décalage
|
|
// horaire à l'écran.
|
|
func handleTasks(w http.ResponseWriter, r *http.Request) {
|
|
tasks := listTasks()
|
|
conv.mu.Lock()
|
|
runningID := conv.runningTaskID
|
|
conv.mu.Unlock()
|
|
// Presets (id + nom + actif) pour peupler le sélecteur du formulaire de tâche.
|
|
presets := []map[string]any{}
|
|
if list, err := ListPresets(); err == nil {
|
|
for _, p := range list {
|
|
presets = append(presets, map[string]any{"id": p.ID, "name": p.Name, "active": p.Active})
|
|
}
|
|
}
|
|
sendJSON(w, 200, map[string]any{
|
|
"ok": true,
|
|
"tasks": tasks,
|
|
"paused": tasksPaused(),
|
|
"agent": agentEnabled(),
|
|
"running_id": runningID,
|
|
"presets": presets,
|
|
// État global mémoire/web, pour proposer des défauts cohérents à la création.
|
|
"mem_on": memMode() != MemOff,
|
|
"web_on": internetEnabled() && crawlReachable(),
|
|
})
|
|
}
|
|
|
|
// handleTaskSave crée ou met à jour une tâche. Valide le schedule (400 sinon) et
|
|
// recalcule NextRun. Un ID vide = création.
|
|
func handleTaskSave(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Prompt string `json:"prompt"`
|
|
Schedule string `json:"schedule"`
|
|
TZ string `json:"tz"`
|
|
Preset string `json:"preset"`
|
|
NoMem bool `json:"no_mem"`
|
|
NoWeb bool `json:"no_web"`
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
req.Name = strings.TrimSpace(req.Name)
|
|
req.Prompt = strings.TrimSpace(req.Prompt)
|
|
req.Schedule = strings.TrimSpace(req.Schedule)
|
|
if req.Name == "" || req.Prompt == "" {
|
|
sendJSON(w, 400, map[string]any{"ok": false, "error": "nom et consigne obligatoires"})
|
|
return
|
|
}
|
|
if err := validateSchedule(req.Schedule, req.TZ); err != nil {
|
|
sendJSON(w, 400, map[string]any{"ok": false, "error": "fréquence invalide : " + err.Error()})
|
|
return
|
|
}
|
|
|
|
var t Task
|
|
if req.ID != "" {
|
|
if cur, ok := getTask(req.ID); ok {
|
|
t = cur // conserve l'historique d'exécution (LastRun/LastReport…)
|
|
} else {
|
|
t.ID = req.ID
|
|
}
|
|
}
|
|
if t.ID == "" {
|
|
t.ID = newTaskID()
|
|
}
|
|
t.Name, t.Prompt, t.Enabled = req.Name, req.Prompt, req.Enabled
|
|
t.TZ, t.Preset = req.TZ, req.Preset
|
|
t.NoMem, t.NoWeb = req.NoMem, req.NoWeb
|
|
// Recalcule NextRun si la fréquence a changé (ou à la création).
|
|
if t.Schedule != req.Schedule || t.NextRun == 0 {
|
|
t.NextRun = computeNextRun(req.Schedule, req.TZ, time.Now())
|
|
}
|
|
t.Schedule = req.Schedule
|
|
if err := saveTask(t); err != nil {
|
|
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
sendJSON(w, 200, map[string]any{"ok": true, "id": t.ID})
|
|
}
|
|
|
|
// handleTaskDelete supprime une tâche.
|
|
func handleTaskDelete(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
ID string `json:"id"`
|
|
}
|
|
_ = json.NewDecoder(r.Body).Decode(&req)
|
|
if req.ID == "" {
|
|
sendJSON(w, 400, map[string]any{"ok": false, "error": "id manquant"})
|
|
return
|
|
}
|
|
if err := deleteTask(req.ID); err != nil {
|
|
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
sendJSON(w, 200, map[string]any{"ok": true})
|
|
}
|
|
|
|
// handleTaskToggle active/désactive UNE tâche. En l'activant, on (re)pose son
|
|
// NextRun pour qu'elle reparte proprement.
|
|
func handleTaskToggle(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
ID string `json:"id"`
|
|
On bool `json:"on"`
|
|
}
|
|
_ = json.NewDecoder(r.Body).Decode(&req)
|
|
t, ok := getTask(req.ID)
|
|
if !ok {
|
|
sendJSON(w, 404, map[string]any{"ok": false, "error": "tâche introuvable"})
|
|
return
|
|
}
|
|
t.Enabled = req.On
|
|
if req.On {
|
|
t.NextRun = computeNextRun(t.Schedule, t.TZ, time.Now())
|
|
}
|
|
if err := saveTask(t); err != nil {
|
|
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
sendJSON(w, 200, map[string]any{"ok": true})
|
|
}
|
|
|
|
// handleTasksPause pilote l'interrupteur maître ({on:true} = suspendre tout).
|
|
func handleTasksPause(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
On bool `json:"on"`
|
|
}
|
|
_ = json.NewDecoder(r.Body).Decode(&req)
|
|
if err := setTasksPaused(req.On); err != nil {
|
|
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
sendJSON(w, 200, map[string]any{"ok": true, "paused": tasksPaused()})
|
|
}
|
|
|
|
// handleTaskRun lance une tâche MAINTENANT (bouton « tester »). Exécution
|
|
// détachée : on rend la main tout de suite et on rafraîchit l'état à la fin.
|
|
// 409 si une génération est déjà en cours.
|
|
func handleTaskRun(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
ID string `json:"id"`
|
|
}
|
|
_ = json.NewDecoder(r.Body).Decode(&req)
|
|
t, ok := getTask(req.ID)
|
|
if !ok {
|
|
sendJSON(w, 404, map[string]any{"ok": false, "error": "tâche introuvable"})
|
|
return
|
|
}
|
|
// Vérifie la disponibilité AVANT de détacher, pour pouvoir répondre 409/503.
|
|
conv.mu.Lock()
|
|
busy := conv.Generating
|
|
conv.mu.Unlock()
|
|
if busy {
|
|
sendJSON(w, 409, map[string]any{"ok": false, "error": ErrBusy.Error()})
|
|
return
|
|
}
|
|
if !healthCheck() {
|
|
sendJSON(w, 503, map[string]any{"ok": false, "error": errModelLoading.Error()})
|
|
return
|
|
}
|
|
go runTask(t)
|
|
sendJSON(w, 200, map[string]any{"ok": true})
|
|
}
|