mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
- module github.com/R0m1k3/Loki, cmd/loki, internal/loki (package loki) - LOKI_HOME, LOKI_MODEL_DIRS, LOKI_SERVICE, LOKI_DL_CONNS ; /etc/loki ; units loki-engine / loki-ui ; binaire et aide CLI - updateRepo pointe sur R0m1k3/Loki (l'auto-update ne tirera plus les binaires AJEAN amont) Conservé à l'identique : le domaine ajean.link (service de tunnel amont), les littéraux de migration 0.7.x (migrate_07.go), RELEASE_NOTES.md et LICENSE (historique et licence de l'amont). go build/vet/test : verts.
303 lines
11 KiB
Go
303 lines
11 KiB
Go
package loki
|
|
|
|
// migrate_07.go — CODE TEMPORAIRE, à supprimer d'ici une version ou deux.
|
|
//
|
|
// Reprend une installation 0.7.x au moment de `loki install`. Tout est ici, et
|
|
// rien d'autre n'en dépend : le jour où le parc aura basculé, supprimer ce
|
|
// fichier et les deux appels à migrateFrom07() (sys_install_unix.go et
|
|
// sys_install_windows.go) suffit. C'est la seule raison d'être de ce fichier —
|
|
// la 0.8 ne contient AUCUN autre code de compatibilité.
|
|
//
|
|
// POURQUOI ICI, ET PAS AU DÉMARRAGE. La version précédente migrait à chaque
|
|
// lancement. Sur un parc, ça veut dire une réécriture d'unités systemd à chaque
|
|
// boot, et un échec laisse une machine distante sans service et sans terminal
|
|
// pour réparer. `install` est le seul bon moment : root, délibéré, et il écrit
|
|
// déjà les unités.
|
|
//
|
|
// CE QU'ON NE DÉTRUIT PAS. Les presets, la mémoire et les modèles sont
|
|
// DÉPLACÉS, jamais copiés ni effacés. Les petits fichiers d'état sont lus puis
|
|
// rangés dans avant-0.8/ — ils ne servent plus, mais ils sont là si quelque
|
|
// chose a mal tourné.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// legacyUnits : les noms de services qu'a portés le produit avant la 0.8.
|
|
//
|
|
// Les désactiver n'est pas cosmétique. Une unité `jean.service` restée active
|
|
// relance un SECOND llama-server au prochain boot : deux moteurs sur le port
|
|
// 8080, la VRAM prise deux fois, et un démarrage qui échoue à moitié sans
|
|
// raison apparente.
|
|
var legacyUnits = []string{"jean", "jean-link", "ajean", "ajean-link"}
|
|
|
|
// hasEntry dit si home contient une entrée portant EXACTEMENT ce nom.
|
|
//
|
|
// os.Stat ne convient pas : sous Windows et macOS le système de fichiers est
|
|
// insensible à la casse, donc Stat("MEMORY") réussit sur un dossier nommé
|
|
// « memory ». Une installation 0.8 toute neuve passait ainsi pour une 0.7, et
|
|
// la migration se redéclenchait à chaque `install`. On compare donc les noms
|
|
// réels, tels que le système de fichiers les rapporte.
|
|
func hasEntry(home, name string) bool {
|
|
entries, err := os.ReadDir(home)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
for _, e := range entries {
|
|
if e.Name() == name {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// sameDir dit si deux chemins désignent le MÊME dossier — ce qui arrive quand
|
|
// ils ne diffèrent que par la casse sur un système de fichiers qui l'ignore.
|
|
func sameDir(a, b string) bool {
|
|
ai, err := os.Stat(a)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
bi, err := os.Stat(b)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return os.SameFile(ai, bi)
|
|
}
|
|
|
|
// needsMigration07 dit si le dossier de données est encore dans l'ancienne
|
|
// disposition. On se fie à ce qui ne peut PAS exister en 0.8.
|
|
func needsMigration07(home string) bool {
|
|
return hasEntry(home, "config.env") || hasEntry(home, "configs") || hasEntry(home, "MEMORY")
|
|
}
|
|
|
|
// migrateFrom07 reprend une installation 0.7.x. Sans effet si le dossier est
|
|
// déjà en 0.8. Doit être appelée AVANT provisionDataDir : elle déplace des
|
|
// dossiers entiers, ce qui suppose que la destination n'existe pas encore.
|
|
func migrateFrom07(home string) error {
|
|
if !needsMigration07(home) {
|
|
return nil
|
|
}
|
|
fmt.Printf("\n%s installation 0.7 détectée — reprise en cours\n", cyan("[migration]"))
|
|
|
|
// 1. Arrêter les anciens services : ils tiennent les fichiers qu'on déplace,
|
|
// et sous Windows un .gguf ouvert par llama-server refuse d'être renommé.
|
|
stopLegacyUnits()
|
|
|
|
// 2. Les dossiers. Un rename sur le même volume est instantané, même pour
|
|
// 130 Go de modèles, et atomique : jamais d'état à moitié déplacé.
|
|
for _, m := range []struct{ from, to string }{
|
|
{"configs", "presets"},
|
|
{"MEMORY", "memory"},
|
|
} {
|
|
if !hasEntry(home, m.from) { // nom exact : « MEMORY » n'est pas « memory »
|
|
continue
|
|
}
|
|
src, dst := filepath.Join(home, m.from), filepath.Join(home, m.to)
|
|
// « configs » et « presets » ne se confondent pas, mais « MEMORY » et
|
|
// « memory » si : sur un système insensible à la casse, ouvrir la
|
|
// destination ouvre en réalité la source. On ne considère donc la
|
|
// destination comme distincte que si c'est un AUTRE dossier.
|
|
if !sameDir(src, dst) {
|
|
// Destination déjà remplie : la machine a été migrée à la main et
|
|
// l'ancien dossier n'a pas été retiré. Écraser détruirait le travail
|
|
// fait, fusionner devinerait qui gagne. On laisse les deux et on le
|
|
// dit — c'est le seul cas où quelqu'un doit trancher.
|
|
if n, _ := os.ReadDir(dst); len(n) > 0 {
|
|
fmt.Printf(" %s %s/ ET %s/ existent tous les deux — %s/ laissé tel quel, à supprimer à la main\n",
|
|
yellow("[info]"), m.from, m.to, m.from)
|
|
continue
|
|
}
|
|
_ = os.Remove(dst) // destination vide créée par une install précédente
|
|
}
|
|
if err := os.Rename(src, dst); err != nil {
|
|
return fmt.Errorf("%s → %s : %w", m.from, m.to, err)
|
|
}
|
|
fmt.Printf(" %s %s/ → %s/\n", green("✓"), m.from, m.to)
|
|
}
|
|
if n, err := moveModels(home); err != nil {
|
|
return err
|
|
} else if n > 0 {
|
|
fmt.Printf(" %s %d modèle(s) → models/\n", green("✓"), n)
|
|
}
|
|
|
|
// 3. Les réglages, vers la base.
|
|
if err := importLegacyState(home); err != nil {
|
|
return err
|
|
}
|
|
// SKILLS/ : les skills ont été fondus dans la mémoire avant la 0.8, le
|
|
// dossier ne sert plus à rien mais peut contenir du travail de l'utilisateur.
|
|
// On l'archive au lieu de le laisser traîner ou de l'effacer.
|
|
if hasEntry(home, "SKILLS") {
|
|
_ = os.Rename(filepath.Join(home, "SKILLS"), filepath.Join(home, "avant-0.8", "SKILLS"))
|
|
}
|
|
fmt.Printf("%s reprise terminée — les anciens fichiers sont dans avant-0.8/\n\n", green("[ok]"))
|
|
return nil
|
|
}
|
|
|
|
// stopLegacyUnits est une variable pour que le test de migration n'aille pas
|
|
// invoquer systemctl sur la machine qui exécute les tests.
|
|
var stopLegacyUnits = stopLegacyUnitsReal
|
|
|
|
// stopLegacyUnitsReal arrête et désactive les services d'avant la 0.8,
|
|
// silencieux sur ceux qui n'existent pas (le cas courant).
|
|
func stopLegacyUnitsReal() {
|
|
for _, u := range legacyUnits {
|
|
switch runtime.GOOS {
|
|
case "linux":
|
|
_ = exec.Command("systemctl", "stop", u).Run()
|
|
_ = exec.Command("systemctl", "disable", u).Run()
|
|
case "darwin":
|
|
for _, label := range []string{"com.jean." + u, "com.ajean." + u} {
|
|
_ = exec.Command("launchctl", "unload", "-w", "/Library/LaunchDaemons/"+label+".plist").Run()
|
|
}
|
|
}
|
|
}
|
|
if runtime.GOOS == "linux" {
|
|
_ = exec.Command("systemctl", "daemon-reload").Run()
|
|
}
|
|
}
|
|
|
|
// moveModels range les .gguf de la racine dans models/. Renvoie le nombre
|
|
// déplacé. Un fichier qui résiste (encore ouvert) interrompt la migration : le
|
|
// laisser derrière donnerait une installation à laquelle il manque un modèle,
|
|
// sans le dire.
|
|
func moveModels(home string) (int, error) {
|
|
entries, err := os.ReadDir(home)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
dst := filepath.Join(home, "models")
|
|
n := 0
|
|
for _, e := range entries {
|
|
name := e.Name()
|
|
if e.IsDir() || !strings.HasSuffix(strings.ToLower(name), ".gguf") {
|
|
continue
|
|
}
|
|
if n == 0 {
|
|
if err := os.MkdirAll(dst, 0o755); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
if err := os.Rename(filepath.Join(home, name), filepath.Join(dst, name)); err != nil {
|
|
return n, fmt.Errorf("déplacement de %s : %w", name, err)
|
|
}
|
|
n++
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
// importLegacyState recopie les anciens fichiers d'état dans la base, puis les
|
|
// range dans avant-0.8/. Les clés et les buckets sont ceux de store.go.
|
|
func importLegacyState(home string) error {
|
|
old := filepath.Join(home, "avant-0.8")
|
|
if err := os.MkdirAll(old, 0o755); err != nil {
|
|
return err
|
|
}
|
|
consumed := []string{}
|
|
read := func(name string) string {
|
|
b, err := os.ReadFile(filepath.Join(home, name))
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
consumed = append(consumed, name)
|
|
return string(b)
|
|
}
|
|
|
|
// La configuration d'abord : sans elle le moteur ne démarre pas.
|
|
if cfg := parseEnv(read("config.env")); len(cfg) > 0 {
|
|
if err := WriteConfig(cfg); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf(" %s configuration (%d clés)\n", green("✓"), len(cfg))
|
|
}
|
|
if prefs := map[string]string{}; json.Unmarshal([]byte(read("webprefs.json")), &prefs) == nil && len(prefs) > 0 {
|
|
_ = replaceKV(bkPrefs, prefs)
|
|
}
|
|
if conv := read("conversation.json"); conv != "" {
|
|
_ = putBytes(bkChat, "conversation", []byte(conv))
|
|
fmt.Printf(" %s conversation\n", green("✓"))
|
|
}
|
|
|
|
// Clés et jetons : le contenu du fichier, débarrassé de sa fin de ligne.
|
|
for key, name := range map[string]string{
|
|
"api_key": ".api_key",
|
|
"web_key": ".web_key",
|
|
"crawl_key": ".crawl4ai_key",
|
|
"link_token": ".link_token",
|
|
"link_machine": ".link_machine",
|
|
} {
|
|
if v := strings.TrimSpace(read(name)); v != "" {
|
|
_ = putStr(bkState, key, v)
|
|
}
|
|
}
|
|
// Drapeaux : seule l'EXISTENCE du fichier comptait.
|
|
for key, name := range map[string]string{
|
|
"agent": ".agent_enabled",
|
|
"internet": ".internet_enabled",
|
|
"oai_public": ".oai_public",
|
|
} {
|
|
if _, err := os.Stat(filepath.Join(home, name)); err == nil {
|
|
_ = putBool(bkState, key, true)
|
|
consumed = append(consumed, name)
|
|
}
|
|
}
|
|
// Documents JSON repris tels quels.
|
|
for key, name := range map[string]string{
|
|
"model_dirs": "model_dirs.json",
|
|
"last_bench": ".last_bench.json",
|
|
"bench_presets": ".bench_presets.json",
|
|
"sysprompt": "sysprompt.txt",
|
|
} {
|
|
if v := read(name); v != "" {
|
|
if key == "sysprompt" {
|
|
_ = putStr(bkState, key, strings.TrimSpace(v))
|
|
} else {
|
|
_ = putBytes(bkState, key, []byte(v))
|
|
}
|
|
}
|
|
}
|
|
// .authorized_users : les identités appairées pour le chat chiffré de bout en
|
|
// bout, une clé publique par ligne. L'oublier coûte cher et de façon peu
|
|
// lisible : le tunnel s'établit, la machine s'affiche en ligne, mais le
|
|
// portail refuse toute conversation chiffrée — il faut ré-appairer sans
|
|
// comprendre pourquoi. Vécu sur le serveur de test.
|
|
if v := read(".authorized_users"); v != "" {
|
|
var list []string
|
|
for _, line := range strings.Split(v, "\n") {
|
|
if h := strings.ToLower(strings.TrimSpace(line)); h != "" {
|
|
list = append(list, h)
|
|
}
|
|
}
|
|
if len(list) > 0 {
|
|
sort.Strings(list)
|
|
_ = putJSON(bkState, "authorized_users", list)
|
|
fmt.Printf(" %s %d identité(s) appairée(s)\n", green("✓"), len(list))
|
|
}
|
|
}
|
|
// .pair_codes n'est PAS repris : ces codes expirent au bout de 10 minutes,
|
|
// les reprendre n'aurait aucun sens.
|
|
|
|
// mcp.json : on ne garde que la map interne, la clé « mcpServers » disparaît.
|
|
if v := read("mcp.json"); v != "" {
|
|
var f struct {
|
|
MCPServers json.RawMessage `json:"mcpServers"`
|
|
}
|
|
if json.Unmarshal([]byte(v), &f) == nil && len(f.MCPServers) > 0 {
|
|
_ = putBytes(bkState, "mcp", f.MCPServers)
|
|
}
|
|
}
|
|
|
|
for _, name := range consumed {
|
|
_ = os.Rename(filepath.Join(home, name), filepath.Join(old, name))
|
|
}
|
|
return nil
|
|
}
|