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
|
||||
|
||||
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` :
|
||||
|
||||
|
||||
+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
|
||||
|
||||
`$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
|
||||
|
||||
|
||||
+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
|
||||
// (Go n'autorise pas de sous-dossiers dans un même package) :
|
||||
// AJEAN — cœur du binaire (package ajean, appelé par cmd/ajean). Go n'autorise
|
||||
// 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)
|
||||
// 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,
|
||||
// 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)
|
||||
// backend_* gestion llama.cpp : build (llamacpp), GPU, serve (ExecStart),
|
||||
// modèles/téléchargements, catalogue, presets, config.env
|
||||
// relay_* accès distant ajean.link : tunnel (link), chiffrement E2E, appairage
|
||||
// sys_* intégration OS : install, services (systemd/launchd/Windows),
|
||||
// plateforme, process, tray, splash, tty, auto-update
|
||||
// backend_* gestion llama.cpp : build, GPU, serve (ExecStart), modèles et
|
||||
// téléchargements, catalogue, presets, configuration
|
||||
// relay_* accès distant ajean.link : tunnel, chiffrement E2E, appairage
|
||||
// sys_* intégration OS : install, services, plateforme, process, tray,
|
||||
// splash, tty, auto-update
|
||||
//
|
||||
// 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
|
||||
@@ -25,8 +25,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -44,17 +43,12 @@ var (
|
||||
authSet = map[string]bool{}
|
||||
)
|
||||
|
||||
func authUsersPath() string { return filepath.Join(AjeanHome(), ".authorized_users") }
|
||||
|
||||
func loadAuthUsers() {
|
||||
authOnce.Do(func() {
|
||||
b, err := os.ReadFile(authUsersPath())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, line := range strings.Split(string(b), "\n") {
|
||||
h := strings.ToLower(strings.TrimSpace(line))
|
||||
if isHexPub(h) {
|
||||
var list []string
|
||||
getJSON(bkState, "authorized_users", &list)
|
||||
for _, h := range list {
|
||||
if h = strings.ToLower(strings.TrimSpace(h)); isHexPub(h) {
|
||||
authSet[h] = true
|
||||
}
|
||||
}
|
||||
@@ -88,23 +82,21 @@ func authorizeUser(uPubHex string) error {
|
||||
return nil
|
||||
}
|
||||
authSet[uPubHex] = true
|
||||
_ = os.MkdirAll(AjeanHome(), 0o755)
|
||||
f, err := os.OpenFile(authUsersPath(), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
list := make([]string, 0, len(authSet))
|
||||
for h := range authSet {
|
||||
list = append(list, h)
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = f.WriteString(uPubHex + "\n")
|
||||
return err
|
||||
sort.Strings(list)
|
||||
return putJSON(bkState, "authorized_users", list)
|
||||
}
|
||||
|
||||
// ---- 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
|
||||
// partagés avec le worker « ajean link --foreground » (systemd, autre process) via
|
||||
// un fichier AjeanHome/.pair_codes — sinon les deux process ne s'accorderaient pas.
|
||||
// Stockés HACHÉS (SHA-256) : le fichier ne révèle aucun code. Chaque code expire
|
||||
// au bout de 10 min et est consommé (retiré) au premier appairage réussi.
|
||||
// partagés avec le service d'interface (autre process) via la base — sinon les
|
||||
// deux ne s'accorderaient pas. Stockés HACHÉS (SHA-256) : la base ne révèle
|
||||
// aucun code. Chaque code expire au bout de 10 min et est consommé (retiré) au
|
||||
// premier appairage réussi.
|
||||
|
||||
const pairCodeTTL = 10 * time.Minute
|
||||
|
||||
@@ -113,8 +105,6 @@ type pairEntry struct {
|
||||
Exp int64 `json:"e"` // expiration (unix ms)
|
||||
}
|
||||
|
||||
func pairCodesPath() string { return filepath.Join(AjeanHome(), ".pair_codes") }
|
||||
|
||||
func hashPairCode(code string) string {
|
||||
h := sha256.Sum256([]byte(strings.ToUpper(strings.TrimSpace(code))))
|
||||
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.
|
||||
func loadPairEntries() []pairEntry {
|
||||
b, err := os.ReadFile(pairCodesPath())
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var es []pairEntry
|
||||
if json.Unmarshal(b, &es) != nil {
|
||||
if !getJSON(bkState, "pair_codes", &es) {
|
||||
return nil
|
||||
}
|
||||
now := time.Now().UnixMilli()
|
||||
@@ -140,17 +126,7 @@ func loadPairEntries() []pairEntry {
|
||||
return kept
|
||||
}
|
||||
|
||||
func savePairEntries(es []pairEntry) error {
|
||||
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
|
||||
}
|
||||
func savePairEntries(es []pairEntry) error { return putJSON(bkState, "pair_codes", es) }
|
||||
|
||||
// newPairCode génère un code frais (usage unique, 10 min), le persiste haché et
|
||||
// le retourne en clair.
|
||||
|
||||
@@ -19,8 +19,10 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func lcJobStatePath() string { return filepath.Join(AjeanHome(), ".lc_job.json") }
|
||||
func lcJobLogPath() string { return filepath.Join(AjeanHome(), ".lc_job.log") }
|
||||
// L'ENTÊTE du job vit en base ; ses LIGNES restent un fichier, écrit en append.
|
||||
// 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
|
||||
// vivent dans le .log à côté, en append).
|
||||
@@ -60,7 +62,7 @@ func lcSave(force bool) {
|
||||
OldCommit: lcCur.OldCommit, NewCommit: lcCur.NewCommit,
|
||||
})
|
||||
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
|
||||
// plutôt que de le faire passer pour vivant ou de l'effacer en silence.
|
||||
func lcRestore() {
|
||||
b, err := os.ReadFile(lcJobStatePath())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var p lcPersisted
|
||||
if json.Unmarshal(b, &p) != nil || p.Action == "" {
|
||||
if !getJSON(bkState, "build_job", &p) || p.Action == "" {
|
||||
return
|
||||
}
|
||||
j := &lcJob{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// web_sysprompt.go — prompt système personnalisé de l'utilisateur, persisté
|
||||
// CÔTÉ SERVEUR ($AJEAN_HOME/sysprompt.txt) et partagé entre appareils, comme la
|
||||
// conversation elle-même. Historique : avant la conversation serveur (v0.4.x),
|
||||
// CÔTÉ SERVEUR (en base) et partagé entre appareils, comme la conversation
|
||||
// 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,
|
||||
// /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
|
||||
@@ -10,32 +10,14 @@ package ajean
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func sysPromptPath() string { return filepath.Join(AjeanHome(), "sysprompt.txt") }
|
||||
|
||||
// readSysPrompt renvoie le prompt système personnalisé ("" si absent).
|
||||
func readSysPrompt() string {
|
||||
b, err := os.ReadFile(sysPromptPath())
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(b))
|
||||
}
|
||||
func readSysPrompt() string { return getStr(bkState, "sysprompt") }
|
||||
|
||||
func saveSysPrompt(text string) error {
|
||||
text = 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)
|
||||
return putStr(bkState, "sysprompt", strings.TrimSpace(text))
|
||||
}
|
||||
|
||||
// handleSysPrompt :
|
||||
|
||||
Reference in new issue
Block a user