From 35edc631a9721939a6c18b53e1e49feb9bc2df69 Mon Sep 17 00:00:00 2001 From: nathaninline Date: Fri, 7 Aug 2026 18:07:33 +0200 Subject: [PATCH] Correctif du worker detache, outil de migration 0.7 dans le depot, CI staticcheck MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - uiUserSvcCtl lancait encore « ajean link serve », sous-commande supprimee : l'acces distant etait casse sur macOS et Windows (hors systemd) - tools/migrate-0.7 : reprise d'une installation 0.7.x, hors du binaire pour ne pas reintroduire de compat dans le produit ; procedure complete au README - CI : staticcheck ajoute (le projet est a zero avertissement, il doit le rester) - .gitignore purge de ses entrees mortes (jean, config.env, .api_key...) --- .github/workflows/ci.yml | 4 + .gitignore | 11 +-- README.md | 38 +++++++- internal/ajean/relay_link.go | 4 +- tools/migrate-0.7/main.go | 183 +++++++++++++++++++++++++++++++++++ 5 files changed, 228 insertions(+), 12 deletions(-) create mode 100644 tools/migrate-0.7/main.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee379a4..cdd8895 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,10 @@ jobs: - run: go vet ./... - run: go build ./... - run: go test ./... + # staticcheck : le projet est à zéro avertissement, et doit le rester. + # Les règles écartées et leur raison vivent dans staticcheck.conf. + - name: staticcheck + run: go run honnef.co/go/tools/cmd/staticcheck@latest ./... - name: cross-compile (cibles release Linux + Windows) run: | # darwin n'est pas ici : depuis v0.5.4 l'icône de barre de menus passe diff --git a/.gitignore b/.gitignore index aa7cc33..3710cdb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # Binaires compilés -/jean -/jean.exe +/ajean +/ajean.exe /dist/ # Artefacts de build Go @@ -8,13 +8,6 @@ *.test *.out -# Config locale / secrets runtime (générés à l'install, jamais versionnés) -config.env -.api_key -.web_key -.link_token -.link_machine - # OS / éditeurs .DS_Store Thumbs.db diff --git a/README.md b/README.md index ba2a079..af61ab9 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ sudo mv ajean /usr/local/bin/ajean ### 2. Installation et compilation du moteur ```bash -sudo ajean install # unité systemd, sudoers, dossiers +sudo ajean install # deux unités systemd, sudoers, dossiers ajean llamacpp install # compile llama.cpp pour le GPU présent ``` @@ -305,6 +305,42 @@ CGO_ENABLED=0 go build -o ajean ./cmd/ajean - `cmd/ajean/` : point d'entrée + ressources Windows (icône, versioninfo). - `internal/ajean/` : tout le code, fichiers préfixés par domaine (`web_*`, `chat_*`, `llm_*`, `backend_*`, `relay_*`, `sys_*`, `mcp_*`) ; carte dans `doc.go`. - `internal/ajean/ui/` : interface web embarquée. **`index.html` est généré** : les sources vivent dans `ui/src/`. Pour modifier l'interface, éditer `ui/src/` puis lancer `go generate ./internal/ajean`. +- `tools/` : outils hors binaire — `assemble-ui` (génère `index.html`), `gen-icon` (icônes Windows), `migrate-0.7` (reprise d'une installation 0.7.x, voir ci-dessous). + +## Migrer depuis la 0.7.x + +La 0.8 ne contient **aucun code de compatibilité** : elle ignore `config.env`, `configs/`, `MEMORY/` et les fichiers d'état de la 0.7. Sur une machine déjà installée, la reprise se fait une fois, à la main. Rien n'est supprimé : les anciens fichiers restent en place, et un retour arrière consiste à réactiver les anciennes unités. + +```bash +# 0. Sauvegarde (petits fichiers ; les .gguf ne risquent rien, ils ne bougent pas de volume) +sudo tar czf /root/ajean-avant-0.8.tar.gz -C /etc/ajean --exclude=backends --exclude='*.gguf' --exclude='*.log' . + +sudo systemctl stop jean-link jean # anciens noms de services + +# 1. Nouvelle arborescence — on COPIE presets et mémoire, on DÉPLACE les modèles +sudo mkdir -p /etc/ajean/{presets,memory,models,bin} +sudo cp -a /etc/ajean/configs/. /etc/ajean/presets/ +sudo cp -a /etc/ajean/MEMORY/. /etc/ajean/memory/ +sudo mv /etc/ajean/*.gguf /etc/ajean/models/ + +# 2. Le binaire, puis les réglages dans la base +sudo install -m755 ajean /usr/local/bin/ajean +go run ./tools/migrate-0.7 /etc/ajean # ou le binaire pré-compilé de l'outil +sudo chown -R : /etc/ajean + +# 3. Les deux services (remplacent jean.service et jean-link.service) +sudo systemctl disable --now jean jean-link +sudo ajean install # écrit ajean-engine + ajean-ui, sudoers, /etc/default +sudo systemctl enable --now ajean-engine ajean-ui +``` + +`migrate-0.7` reprend la configuration, les préférences de l'interface, la conversation, les clés (API, pilotage, Crawl4AI), le jeton de liaison et l'identifiant machine, les interrupteurs agent/internet/OpenAI, les serveurs MCP et les benchmarks. Il ne touche ni aux presets, ni à la mémoire, ni aux modèles — ce sont des fichiers, ils ont juste changé de dossier. + +**`.e2e_key` ne doit pas être supprimé** : l'empreinte de chiffrement de bout en bout confirmée dans le portail en dépend. Il reste à sa place, la migration ne le touche pas. + +Vérification : `ajean where`, `ajean switch` (le preset actif doit être détecté), `ajean test`, puis `journalctl -u ajean-ui -n 20` doit afficher « lien établi ✓ » et l'empreinte E2E inchangée. + +Si `ajean install` écrase une unité que vous aviez réglée à la main (priorité CPU, `CUDA_VISIBLE_DEVICES`, utilisateur dédié), rétablissez vos directives dans `/etc/systemd/system/ajean-engine.service` puis `systemctl daemon-reload`. ## Licence diff --git a/internal/ajean/relay_link.go b/internal/ajean/relay_link.go index 022e905..65e1bbe 100644 --- a/internal/ajean/relay_link.go +++ b/internal/ajean/relay_link.go @@ -416,10 +416,10 @@ func uiUserSvcCtl(action string) error { } defer logf.Close() - cmd := spawnDetached(self, "link", "serve") + cmd := spawnDetached(self, "web") cmd.Stdout, cmd.Stderr = logf, logf if err := cmd.Start(); err != nil { - return fmt.Errorf("démarrage de « ajean link serve »: %w", err) + return fmt.Errorf("démarrage de « ajean web »: %w", err) } pid := cmd.Process.Pid // PID + binaire d'origine : la 2e ligne dit QUELLE copie de l'app a lancé ce diff --git a/tools/migrate-0.7/main.go b/tools/migrate-0.7/main.go new file mode 100644 index 0000000..d69cbb5 --- /dev/null +++ b/tools/migrate-0.7/main.go @@ -0,0 +1,183 @@ +// migrate-0.7 — reprend une installation AJEAN 0.7.x dans la base de la 0.8. +// +// USAGE UNIQUE, et volontairement HORS du binaire ajean. La 0.8 ne contient +// aucun code de compatibilité : c'est tout l'intérêt de la version. Mais une +// machine déjà installée ne doit pas pour autant perdre ses réglages, sa +// conversation et ses clés. D'où cet outil séparé, qu'on lance une fois puis +// qu'on oublie — le produit reste propre, la migration reste possible. +// +// go run ./tools/migrate-0.7 [$AJEAN_HOME] (défaut : /etc/ajean) +// +// Il lit l'ancienne disposition (config.env, webprefs.json, conversation.json, +// .api_key, .link_token…) et la recopie dans ajean.db, aux mêmes buckets et aux +// mêmes clés que internal/ajean/store.go. +// +// Il ne SUPPRIME rien : les anciens fichiers restent en place, intacts. Si la +// migration s'avère fausse, il suffit d'effacer ajean.db et de la relancer. +// +// Ce qu'il ne fait PAS, parce que ça se fait mieux au shell (voir le README) : +// déplacer configs/ vers presets/, MEMORY/ vers memory/, les .gguf vers +// models/, et remplacer les unités systemd. +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + bolt "go.etcd.io/bbolt" +) + +const ( + bkConfig = "config" + bkPrefs = "prefs" + bkState = "state" + bkChat = "chat" +) + +var home = "/etc/ajean" + +func main() { + if len(os.Args) > 1 { + home = os.Args[1] + } + db, err := bolt.Open(filepath.Join(home, "ajean.db"), 0o600, &bolt.Options{Timeout: 5 * time.Second}) + must(err) + defer db.Close() + must(db.Update(func(tx *bolt.Tx) error { + for _, b := range []string{bkConfig, bkPrefs, bkState, bkChat} { + if _, err := tx.CreateBucketIfNotExists([]byte(b)); err != nil { + return err + } + } + return nil + })) + + // Configuration du moteur. + if cfg := parseEnv(read("config.env")); len(cfg) > 0 { + must(putAll(db, bkConfig, cfg)) + fmt.Printf("config : %d clés\n", len(cfg)) + } else { + fmt.Println("config : ABSENTE — le service ne démarrera pas") + } + + // Préférences de l'interface web. + var prefs map[string]string + if b := read("webprefs.json"); b != "" && json.Unmarshal([]byte(b), &prefs) == nil { + must(putAll(db, bkPrefs, prefs)) + fmt.Printf("prefs : %d clés\n", len(prefs)) + } + + // Clés, jetons et interrupteurs. + for key, file := 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(file)); v != "" { + must(put(db, bkState, key, []byte(v))) + fmt.Printf("%-12s: repris\n", key) + } + } + // Les drapeaux étaient des fichiers dont seule l'EXISTENCE comptait. + for key, file := range map[string]string{ + "agent": ".agent_enabled", + "internet": ".internet_enabled", + "oai_public": ".oai_public", + } { + if _, err := os.Stat(filepath.Join(home, file)); err == nil { + must(put(db, bkState, key, []byte("1"))) + fmt.Printf("%-12s: actif\n", key) + } + } + + // Serveurs MCP : on ne garde que la map interne, la clé « mcpServers » du + // fichier n'a plus de raison d'être. + if b := read("mcp.json"); b != "" { + var f struct { + MCPServers json.RawMessage `json:"mcpServers"` + } + if json.Unmarshal([]byte(b), &f) == nil && len(f.MCPServers) > 0 { + must(put(db, bkState, "mcp", f.MCPServers)) + fmt.Println("mcp : repris") + } + } + + // Benchmarks et dossiers de modèles : recopiés tels quels (même forme JSON). + for key, file := range map[string]string{ + "last_bench": ".last_bench.json", + "bench_presets": ".bench_presets.json", + "model_dirs": "model_dirs.json", + } { + if b := read(file); b != "" { + must(put(db, bkState, key, []byte(b))) + fmt.Printf("%-12s: repris\n", key) + } + } + + // Conversation partagée. + if b := read("conversation.json"); b != "" { + must(put(db, bkChat, "conversation", []byte(b))) + fmt.Printf("conversation: %d octets\n", len(b)) + } + + fmt.Println("\nTerminé — aucun ancien fichier n'a été supprimé.") +} + +func read(name string) string { + b, err := os.ReadFile(filepath.Join(home, name)) + if err != nil { + return "" + } + return string(b) +} + +func put(db *bolt.DB, bucket, key string, val []byte) error { + return db.Update(func(tx *bolt.Tx) error { + return tx.Bucket([]byte(bucket)).Put([]byte(key), val) + }) +} + +func putAll(db *bolt.DB, bucket string, m map[string]string) error { + return db.Update(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(bucket)) + for k, v := range m { + if v == "" { + continue + } + if err := b.Put([]byte(k), []byte(v)); err != nil { + return err + } + } + return nil + }) +} + +// parseEnv reprend à l'identique la lecture de backend_config.go. +func parseEnv(text string) map[string]string { + m := map[string]string{} + for _, line := range strings.Split(text, "\n") { + s := strings.TrimSpace(line) + if s == "" || strings.HasPrefix(s, "#") { + continue + } + k, v, ok := strings.Cut(strings.TrimPrefix(s, "export "), "=") + if !ok { + continue + } + m[strings.TrimSpace(k)] = strings.Trim(strings.TrimSpace(v), "\"") + } + return m +} + +func must(err error) { + if err != nil { + fmt.Fprintln(os.Stderr, "migration:", err) + os.Exit(1) + } +}