mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Etat residuel en base, carte du package et docs remises d'aplomb
- sysprompt, utilisateurs appairés, codes d'appairage et etat du job de build passent en base ; ne restent des fichiers que la cle E2E, les certificats et les journaux (le log de build reste un fichier : des milliers de lignes) - doc.go remis a jour (run.go, store.go, les deux services, tools/assemble-ui) - README et notes de release ne pretendent plus que la racine ne contient que six dossiers : c'etait faux
This commit is contained in:
1 parent
29bfe236cd
commit
7ecd869da0
6 files changed
+67
-84
No files matched your search
@@ -109,9 +109,21 @@ Installation :
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Tout vit sous **`$AJEAN_HOME`** (`/etc/ajean` sous Linux/macOS, `%ProgramData%\ajean` sous Windows), qui contient exactement six dossiers — `backends/`, `bin/`, `presets/`, `memory/`, `models/`, `workspace/` — et la base `ajean.db`.
|
Tout vit sous **`$AJEAN_HOME`** (`/etc/ajean` sous Linux/macOS, `%ProgramData%\ajean` sous Windows) :
|
||||||
|
|
||||||
Cette base, un unique fichier [bbolt](https://github.com/etcd-io/bbolt), remplace la dizaine de fichiers d'état d'autrefois : configuration, préférences de l'interface, conversation, clés et interrupteurs y vivent désormais ensemble. Restent des fichiers ce qui se lit et s'édite à la main : les presets (`presets/*.env`), les pages de mémoire (`memory/*.md`) et, bien sûr, les modèles.
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| `backends/` | llama.cpp, compilé ou téléchargé |
|
||||||
|
| `bin/` | le binaire installé (Windows) |
|
||||||
|
| `models/` | les `.gguf` |
|
||||||
|
| `presets/` | un `.env` par preset |
|
||||||
|
| `memory/` | les pages de mémoire de l'IA (`.md`) |
|
||||||
|
| `workspace/` | ce que l'IA écrit en mode agent |
|
||||||
|
| `ajean.db` | tout l'état : configuration, préférences, conversation, clés, interrupteurs |
|
||||||
|
|
||||||
|
S'y ajoutent à la racine les quelques fichiers qui ne peuvent pas aller ailleurs : `.e2e_key` (clé privée du chiffrement de bout en bout), `certs/` (certificats TLS gérés par certmagic), et les journaux et fichiers PID des services.
|
||||||
|
|
||||||
|
La base, un unique fichier [bbolt](https://github.com/etcd-io/bbolt), remplace la dizaine de fichiers d'état d'autrefois. Restent des fichiers ce qui se lit et s'édite à la main : les presets, les pages de mémoire et, bien sûr, les modèles.
|
||||||
|
|
||||||
La configuration du moteur s'édite avec `ajean edit`, qui la déroule au format `clé=valeur` dans `$EDITOR` :
|
La configuration du moteur s'édite avec `ajean edit`, qui la déroule au format `clé=valeur` dans `$EDITOR` :
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -12,7 +12,9 @@ Restent des fichiers ceux qui sont faits pour être lus, édités et sauvegardé
|
|||||||
|
|
||||||
## Six dossiers, et rien d'autre
|
## Six dossiers, et rien d'autre
|
||||||
|
|
||||||
`$AJEAN_HOME` contient exactement `backends/`, `bin/`, `presets/`, `memory/`, `models/`, `workspace/`, plus la base. `configs/` devient `presets/`, `MEMORY/` devient `memory/`, et les `.gguf` ont enfin leur `models/` au lieu d'être posés à la racine.
|
`$AJEAN_HOME` contient `backends/`, `bin/`, `presets/`, `memory/`, `models/`, `workspace/`, plus la base. `configs/` devient `presets/`, `MEMORY/` devient `memory/`, et les `.gguf` ont enfin leur `models/` au lieu d'être posés à la racine.
|
||||||
|
|
||||||
|
Ne restent à la racine que ce qui ne peut pas aller ailleurs : la clé privée du chiffrement de bout en bout, le dossier des certificats TLS, et les journaux et fichiers PID des services.
|
||||||
|
|
||||||
## Deux services qui portent enfin leur nom
|
## Deux services qui portent enfin leur nom
|
||||||
|
|
||||||
|
|||||||
+24
-11
@@ -1,18 +1,31 @@
|
|||||||
// AJEAN — cœur du binaire (package ajean, appelé par cmd/ajean). Les fichiers sont préfixés par domaine
|
// AJEAN — cœur du binaire (package ajean, appelé par cmd/ajean). Go n'autorise
|
||||||
// (Go n'autorise pas de sous-dossiers dans un même package) :
|
// pas de sous-dossiers dans un même package : les fichiers sont donc préfixés
|
||||||
|
// par domaine.
|
||||||
//
|
//
|
||||||
// main.go point d'entrée : dispatch des sous-commandes, chemins (AJEANHome…)
|
// run.go point d'entrée : dispatch des sous-commandes, AjeanHome() et
|
||||||
|
// l'arborescence de données
|
||||||
|
// store.go LA base (bbolt) : configuration, préférences, conversation,
|
||||||
|
// clés, jetons, interrupteurs — tout l'état non éditable à la main
|
||||||
// cli_* expérience « application » (double-clic : UI + tray + splash)
|
// cli_* expérience « application » (double-clic : UI + tray + splash)
|
||||||
// web_* serveur HTTP local :8090 (UI embarquée via go:embed ui/, auth, prefs)
|
// web_* serveur HTTP :8090 (UI embarquée via go:embed ui/, auth, prefs)
|
||||||
// chat_* chat CLI + conversation serveur partagée, compaction, mémoire,
|
// chat_* chat CLI + conversation serveur partagée, compaction, mémoire,
|
||||||
// outils de l'agent (dont accès internet via Crawl4AI)
|
// outils de l'agent (dont accès internet)
|
||||||
// llm_* client llama-server (complétions, endpoint OpenAI, bench, test)
|
// llm_* client llama-server (complétions, endpoint OpenAI, bench, test)
|
||||||
// backend_* gestion llama.cpp : build (llamacpp), GPU, serve (ExecStart),
|
// backend_* gestion llama.cpp : build, GPU, serve (ExecStart), modèles et
|
||||||
// modèles/téléchargements, catalogue, presets, config.env
|
// téléchargements, catalogue, presets, configuration
|
||||||
// relay_* accès distant ajean.link : tunnel (link), chiffrement E2E, appairage
|
// relay_* accès distant ajean.link : tunnel, chiffrement E2E, appairage
|
||||||
// sys_* intégration OS : install, services (systemd/launchd/Windows),
|
// sys_* intégration OS : install, services, plateforme, process, tray,
|
||||||
// plateforme, process, tray, splash, tty, auto-update
|
// splash, tty, auto-update
|
||||||
//
|
//
|
||||||
// Les suffixes _windows/_linux/_darwin/_unix/_other portent les contraintes de
|
// Les suffixes _windows/_linux/_darwin/_unix/_other portent les contraintes de
|
||||||
// compilation par OS. ui/index.html est GÉNÉRÉ depuis ui/src/ (voir ui/assemble.ps1).
|
// compilation par OS.
|
||||||
|
//
|
||||||
|
// DEUX SERVICES à l'exécution, un seul binaire :
|
||||||
|
// - ajean-engine (« ajean serve ») exec llama-server ;
|
||||||
|
// - ajean-ui (« ajean web ») sert l'UI locale, le tunnel du relais et
|
||||||
|
// l'endpoint OpenAI depuis un SEUL process — condition d'une conversation
|
||||||
|
// unique, celle-ci vivant en mémoire (voir chat_conversation.go).
|
||||||
|
//
|
||||||
|
// ui/index.html est GÉNÉRÉ depuis ui/src/ : éditer les sources puis
|
||||||
|
// `go generate ./internal/ajean` (voir tools/assemble-ui).
|
||||||
package ajean
|
package ajean
|
||||||
@@ -25,8 +25,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"sort"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -44,17 +43,12 @@ var (
|
|||||||
authSet = map[string]bool{}
|
authSet = map[string]bool{}
|
||||||
)
|
)
|
||||||
|
|
||||||
func authUsersPath() string { return filepath.Join(AjeanHome(), ".authorized_users") }
|
|
||||||
|
|
||||||
func loadAuthUsers() {
|
func loadAuthUsers() {
|
||||||
authOnce.Do(func() {
|
authOnce.Do(func() {
|
||||||
b, err := os.ReadFile(authUsersPath())
|
var list []string
|
||||||
if err != nil {
|
getJSON(bkState, "authorized_users", &list)
|
||||||
return
|
for _, h := range list {
|
||||||
}
|
if h = strings.ToLower(strings.TrimSpace(h)); isHexPub(h) {
|
||||||
for _, line := range strings.Split(string(b), "\n") {
|
|
||||||
h := strings.ToLower(strings.TrimSpace(line))
|
|
||||||
if isHexPub(h) {
|
|
||||||
authSet[h] = true
|
authSet[h] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,23 +82,21 @@ func authorizeUser(uPubHex string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
authSet[uPubHex] = true
|
authSet[uPubHex] = true
|
||||||
_ = os.MkdirAll(AjeanHome(), 0o755)
|
list := make([]string, 0, len(authSet))
|
||||||
f, err := os.OpenFile(authUsersPath(), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
|
for h := range authSet {
|
||||||
if err != nil {
|
list = append(list, h)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
defer f.Close()
|
sort.Strings(list)
|
||||||
_, err = f.WriteString(uPubHex + "\n")
|
return putJSON(bkState, "authorized_users", list)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Codes d'appairage : à la demande, usage unique, TTL 10 min --------------
|
// ---- Codes d'appairage : à la demande, usage unique, TTL 10 min --------------
|
||||||
//
|
//
|
||||||
// Générés par « ajean link code » (ou affichés par « ajean link »), ils sont
|
// Générés par « ajean link code » (ou affichés par « ajean link »), ils sont
|
||||||
// partagés avec le worker « ajean link --foreground » (systemd, autre process) via
|
// partagés avec le service d'interface (autre process) via la base — sinon les
|
||||||
// un fichier AjeanHome/.pair_codes — sinon les deux process ne s'accorderaient pas.
|
// deux ne s'accorderaient pas. Stockés HACHÉS (SHA-256) : la base ne révèle
|
||||||
// Stockés HACHÉS (SHA-256) : le fichier ne révèle aucun code. Chaque code expire
|
// aucun code. Chaque code expire au bout de 10 min et est consommé (retiré) au
|
||||||
// au bout de 10 min et est consommé (retiré) au premier appairage réussi.
|
// premier appairage réussi.
|
||||||
|
|
||||||
const pairCodeTTL = 10 * time.Minute
|
const pairCodeTTL = 10 * time.Minute
|
||||||
|
|
||||||
@@ -113,8 +105,6 @@ type pairEntry struct {
|
|||||||
Exp int64 `json:"e"` // expiration (unix ms)
|
Exp int64 `json:"e"` // expiration (unix ms)
|
||||||
}
|
}
|
||||||
|
|
||||||
func pairCodesPath() string { return filepath.Join(AjeanHome(), ".pair_codes") }
|
|
||||||
|
|
||||||
func hashPairCode(code string) string {
|
func hashPairCode(code string) string {
|
||||||
h := sha256.Sum256([]byte(strings.ToUpper(strings.TrimSpace(code))))
|
h := sha256.Sum256([]byte(strings.ToUpper(strings.TrimSpace(code))))
|
||||||
return hex.EncodeToString(h[:])
|
return hex.EncodeToString(h[:])
|
||||||
@@ -122,12 +112,8 @@ func hashPairCode(code string) string {
|
|||||||
|
|
||||||
// loadPairEntries lit le store et purge au passage les entrées expirées.
|
// loadPairEntries lit le store et purge au passage les entrées expirées.
|
||||||
func loadPairEntries() []pairEntry {
|
func loadPairEntries() []pairEntry {
|
||||||
b, err := os.ReadFile(pairCodesPath())
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var es []pairEntry
|
var es []pairEntry
|
||||||
if json.Unmarshal(b, &es) != nil {
|
if !getJSON(bkState, "pair_codes", &es) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
now := time.Now().UnixMilli()
|
now := time.Now().UnixMilli()
|
||||||
@@ -140,17 +126,7 @@ func loadPairEntries() []pairEntry {
|
|||||||
return kept
|
return kept
|
||||||
}
|
}
|
||||||
|
|
||||||
func savePairEntries(es []pairEntry) error {
|
func savePairEntries(es []pairEntry) error { return putJSON(bkState, "pair_codes", es) }
|
||||||
if err := os.MkdirAll(AjeanHome(), 0o755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
b, _ := json.Marshal(es)
|
|
||||||
tmp := pairCodesPath() + ".tmp"
|
|
||||||
if err := os.WriteFile(tmp, b, 0o600); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return os.Rename(tmp, pairCodesPath()) // remplacement atomique
|
|
||||||
}
|
|
||||||
|
|
||||||
// newPairCode génère un code frais (usage unique, 10 min), le persiste haché et
|
// newPairCode génère un code frais (usage unique, 10 min), le persiste haché et
|
||||||
// le retourne en clair.
|
// le retourne en clair.
|
||||||
|
|||||||
@@ -19,8 +19,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func lcJobStatePath() string { return filepath.Join(AjeanHome(), ".lc_job.json") }
|
// L'ENTÊTE du job vit en base ; ses LIGNES restent un fichier, écrit en append.
|
||||||
func lcJobLogPath() string { return filepath.Join(AjeanHome(), ".lc_job.log") }
|
// Une compilation en produit des milliers : les faire transiter par une
|
||||||
|
// transaction chacune coûterait un fsync par ligne.
|
||||||
|
func lcJobLogPath() string { return filepath.Join(AjeanHome(), "ajean-build.log") }
|
||||||
|
|
||||||
// lcPersisted est la forme sur disque : l'entête du job, sans les lignes (elles
|
// lcPersisted est la forme sur disque : l'entête du job, sans les lignes (elles
|
||||||
// vivent dans le .log à côté, en append).
|
// vivent dans le .log à côté, en append).
|
||||||
@@ -60,7 +62,7 @@ func lcSave(force bool) {
|
|||||||
OldCommit: lcCur.OldCommit, NewCommit: lcCur.NewCommit,
|
OldCommit: lcCur.OldCommit, NewCommit: lcCur.NewCommit,
|
||||||
})
|
})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
_ = os.WriteFile(lcJobStatePath(), b, 0o644)
|
_ = putBytes(bkState, "build_job", b)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,12 +84,8 @@ func lcResetLog() { _ = os.Remove(lcJobLogPath()) }
|
|||||||
// compilation est morte avec le process précédent, on le marque interrompu
|
// compilation est morte avec le process précédent, on le marque interrompu
|
||||||
// plutôt que de le faire passer pour vivant ou de l'effacer en silence.
|
// plutôt que de le faire passer pour vivant ou de l'effacer en silence.
|
||||||
func lcRestore() {
|
func lcRestore() {
|
||||||
b, err := os.ReadFile(lcJobStatePath())
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var p lcPersisted
|
var p lcPersisted
|
||||||
if json.Unmarshal(b, &p) != nil || p.Action == "" {
|
if !getJSON(bkState, "build_job", &p) || p.Action == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
j := &lcJob{
|
j := &lcJob{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// web_sysprompt.go — prompt système personnalisé de l'utilisateur, persisté
|
// web_sysprompt.go — prompt système personnalisé de l'utilisateur, persisté
|
||||||
// CÔTÉ SERVEUR ($AJEAN_HOME/sysprompt.txt) et partagé entre appareils, comme la
|
// CÔTÉ SERVEUR (en base) et partagé entre appareils, comme la conversation
|
||||||
// conversation elle-même. Historique : avant la conversation serveur (v0.4.x),
|
// elle-même. Historique : avant la conversation serveur (v0.4.x),
|
||||||
// l'UI envoyait son prompt système dans chaque requête /api/chat ; depuis,
|
// l'UI envoyait son prompt système dans chaque requête /api/chat ; depuis,
|
||||||
// /api/chat/send ne porte que le message → le champ de l'UI n'avait plus aucun
|
// /api/chat/send ne porte que le message → le champ de l'UI n'avait plus aucun
|
||||||
// effet. Il est maintenant lu ici par la génération (chat_conversation.go), et
|
// effet. Il est maintenant lu ici par la génération (chat_conversation.go), et
|
||||||
@@ -10,32 +10,14 @@ package ajean
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func sysPromptPath() string { return filepath.Join(AjeanHome(), "sysprompt.txt") }
|
|
||||||
|
|
||||||
// readSysPrompt renvoie le prompt système personnalisé ("" si absent).
|
// readSysPrompt renvoie le prompt système personnalisé ("" si absent).
|
||||||
func readSysPrompt() string {
|
func readSysPrompt() string { return getStr(bkState, "sysprompt") }
|
||||||
b, err := os.ReadFile(sysPromptPath())
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(string(b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func saveSysPrompt(text string) error {
|
func saveSysPrompt(text string) error {
|
||||||
text = strings.TrimSpace(text)
|
return putStr(bkState, "sysprompt", strings.TrimSpace(text))
|
||||||
if text == "" {
|
|
||||||
err := os.Remove(sysPromptPath())
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return os.WriteFile(sysPromptPath(), []byte(text+"\n"), 0o644)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleSysPrompt :
|
// handleSysPrompt :
|
||||||
|
|||||||
Reference in new issue
Block a user