mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-23 06:58:14 +02:00
v0.3.3 : mode application Windows (double-clic → UI + icône de notification + splash, icône sur l'exe), catalogue de modèles servi par ajean.link (/api/catalog + détection RAM), correctif des fenêtres de console qui clignotaient sous Windows, version affichée dans l'UI
This commit is contained in:
1 parent
0c4ff12a00
commit
cd58382d73
23 files changed
+810
-30
No files matched your search
@@ -0,0 +1,66 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// app.go — expérience « application » de Jean.
|
||||||
|
//
|
||||||
|
// Quand on double-clique sur le binaire (aucun argument, console fraîche), au
|
||||||
|
// lieu d'afficher l'aide dans une console qui se ferme, Jean démarre son UI web,
|
||||||
|
// l'ouvre dans le navigateur ET pose une icône dans la zone de notification
|
||||||
|
// Windows (voir tray_windows.go) : on voit que Jean tourne et on le pilote
|
||||||
|
// (« Ouvrir Jean » / « Quitter »).
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const appPort = 8090
|
||||||
|
|
||||||
|
// cmdApp lance l'UI web, l'ouvre dans le navigateur et fait tourner l'icône de
|
||||||
|
// la zone de notification (Windows). Sur une machine vierge, il crée d'abord le
|
||||||
|
// dossier de données + config.env pour que l'interface s'ouvre quand même
|
||||||
|
// (l'écran d'accueil prendra le relais pour le choix du modèle).
|
||||||
|
func cmdApp(args []string) error {
|
||||||
|
url := fmt.Sprintf("http://localhost:%d", appPort)
|
||||||
|
|
||||||
|
if _, err := os.Stat(confPath()); err != nil {
|
||||||
|
// Première fois : installe le minimum (dossiers + config de départ).
|
||||||
|
_ = cmdInstall(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := fmt.Sprintf("0.0.0.0:%d", appPort)
|
||||||
|
ln, err := net.Listen("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
// Jean tourne déjà sur ce port : on ouvre juste l'UI sur l'instance
|
||||||
|
// existante plutôt que d'échouer.
|
||||||
|
fmt.Printf("Jean est déjà lancé — ouverture de %s\n", url)
|
||||||
|
return openBrowser(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sert l'UI en tâche de fond ; l'icône tray tient le premier plan.
|
||||||
|
go func() { _ = http.Serve(ln, newWebMux()) }()
|
||||||
|
|
||||||
|
sp := showSplash("Lancement de Jean en cours…")
|
||||||
|
waitServerReady(url)
|
||||||
|
_ = openBrowser(url)
|
||||||
|
time.Sleep(900 * time.Millisecond) // laisse le navigateur s'afficher par-dessus le splash
|
||||||
|
sp.close()
|
||||||
|
|
||||||
|
runTray(url) // icône zone de notification ; bloque jusqu'à « Quitter »
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// waitServerReady attend que l'UI réponde (jusqu'à ~4 s) pour n'ouvrir le
|
||||||
|
// navigateur qu'une fois le serveur prêt.
|
||||||
|
func waitServerReady(url string) {
|
||||||
|
c := &http.Client{Timeout: 400 * time.Millisecond}
|
||||||
|
for i := 0; i < 20; i++ {
|
||||||
|
if resp, err := c.Get(url); err == nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(150 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// catalog.go — récupère la liste de modèles curatée servie par ajean.link et la
|
||||||
|
// combine avec les infos matérielles locales, pour que l'écran d'accueil (à
|
||||||
|
// venir) propose « en un clic » un modèle adapté à la machine.
|
||||||
|
//
|
||||||
|
// La liste vit sur https://ajean.link/models.json (éditée par l'opérateur, voir
|
||||||
|
// jean-relay/models_catalog.go). Si le réseau échoue, on retombe sur un
|
||||||
|
// catalogue minimal embarqué (fallbackCatalogJSON) pour ne jamais bloquer.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const catalogURL = "https://ajean.link/models.json"
|
||||||
|
|
||||||
|
type catalogModel struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Params string `json:"params"`
|
||||||
|
Quant string `json:"quant"`
|
||||||
|
SizeGB float64 `json:"size_gb"`
|
||||||
|
MinRAMGB float64 `json:"min_ram_gb"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Note string `json:"note"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type catalog struct {
|
||||||
|
Version int `json:"version"`
|
||||||
|
Models []catalogModel `json:"models"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type hardwareInfo struct {
|
||||||
|
OS string `json:"os"`
|
||||||
|
Arch string `json:"arch"`
|
||||||
|
RAMGB float64 `json:"ram_gb"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchCatalog récupère le catalogue distant, avec repli embarqué.
|
||||||
|
func fetchCatalog() catalog {
|
||||||
|
var c catalog
|
||||||
|
client := &http.Client{Timeout: 8 * time.Second}
|
||||||
|
if resp, err := client.Get(catalogURL); err == nil {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode == 200 && json.NewDecoder(resp.Body).Decode(&c) == nil && len(c.Models) > 0 {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = json.Unmarshal([]byte(fallbackCatalogJSON), &c)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectHardware() hardwareInfo {
|
||||||
|
return hardwareInfo{OS: runtime.GOOS, Arch: runtime.GOARCH, RAMGB: totalRAMGB()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCatalog : renvoie le catalogue + le matériel local. L'UI s'en sert pour
|
||||||
|
// marquer chaque modèle « tient / trop lourd » et proposer le bon par défaut.
|
||||||
|
func handleCatalog(w http.ResponseWriter, r *http.Request) {
|
||||||
|
resp := struct {
|
||||||
|
Hardware hardwareInfo `json:"hardware"`
|
||||||
|
Models []catalogModel `json:"models"`
|
||||||
|
}{Hardware: detectHardware(), Models: fetchCatalog().Models}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallbackCatalogJSON — repli minimal si ajean.link/models.json est injoignable.
|
||||||
|
const fallbackCatalogJSON = `{
|
||||||
|
"version": 1,
|
||||||
|
"models": [
|
||||||
|
{"id":"qwen2.5-3b-instruct-q4","name":"Qwen2.5 3B Instruct","params":"3B","quant":"Q4_K_M","size_gb":2.1,"min_ram_gb":6,"url":"https://huggingface.co/bartowski/Qwen2.5-3B-Instruct-GGUF/resolve/main/Qwen2.5-3B-Instruct-Q4_K_M.gguf","note":"Léger et rapide — idéal petites machines."},
|
||||||
|
{"id":"qwen2.5-7b-instruct-q4","name":"Qwen2.5 7B Instruct","params":"7B","quant":"Q4_K_M","size_gb":4.7,"min_ram_gb":10,"url":"https://huggingface.co/bartowski/Qwen2.5-7B-Instruct-GGUF/resolve/main/Qwen2.5-7B-Instruct-Q4_K_M.gguf","note":"Plus capable — recommandé avec 16 Go de RAM ou un GPU."}
|
||||||
|
]
|
||||||
|
}`
|
||||||
@@ -5,16 +5,25 @@ go 1.25.0
|
|||||||
require (
|
require (
|
||||||
github.com/caddyserver/certmagic v0.25.4
|
github.com/caddyserver/certmagic v0.25.4
|
||||||
github.com/coder/websocket v1.8.15
|
github.com/coder/websocket v1.8.15
|
||||||
|
github.com/getlantern/systray v1.2.2
|
||||||
github.com/hashicorp/yamux v0.1.2
|
github.com/hashicorp/yamux v0.1.2
|
||||||
golang.org/x/mod v0.35.0
|
golang.org/x/mod v0.35.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/caddyserver/zerossl v0.1.5 // indirect
|
github.com/caddyserver/zerossl v0.1.5 // indirect
|
||||||
|
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
|
||||||
|
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
|
||||||
|
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
|
||||||
|
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
|
||||||
|
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
|
||||||
|
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
|
||||||
|
github.com/go-stack/stack v1.8.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/libdns/libdns v1.1.1 // indirect
|
github.com/libdns/libdns v1.1.1 // indirect
|
||||||
github.com/mholt/acmez/v3 v3.1.6 // indirect
|
github.com/mholt/acmez/v3 v3.1.6 // indirect
|
||||||
github.com/miekg/dns v1.1.72 // indirect
|
github.com/miekg/dns v1.1.72 // indirect
|
||||||
|
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
|
||||||
github.com/zeebo/blake3 v0.2.4 // indirect
|
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.uber.org/zap v1.27.1 // indirect
|
go.uber.org/zap v1.27.1 // indirect
|
||||||
|
|||||||
@@ -6,10 +6,27 @@ github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7l
|
|||||||
github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
|
github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
|
||||||
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
|
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
|
||||||
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
|
||||||
|
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
|
||||||
|
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
|
||||||
|
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
|
||||||
|
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
|
||||||
|
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
|
||||||
|
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
|
||||||
|
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
|
||||||
|
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
|
||||||
|
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
|
||||||
|
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
|
||||||
|
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
||||||
|
github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE=
|
||||||
|
github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||||
|
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
|
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
|
||||||
@@ -22,12 +39,19 @@ github.com/letsencrypt/pebble/v2 v2.10.0 h1:Wq6gYXlsY6ubqI3hhxsTzdyotvfdjFBxuwYq
|
|||||||
github.com/letsencrypt/pebble/v2 v2.10.0/go.mod h1:Sk8cmUIPcIdv2nINo+9PB4L+ZBhzY+F9A1a/h/xmWiQ=
|
github.com/letsencrypt/pebble/v2 v2.10.0/go.mod h1:Sk8cmUIPcIdv2nINo+9PB4L+ZBhzY+F9A1a/h/xmWiQ=
|
||||||
github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
|
github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
|
||||||
github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
||||||
|
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||||
|
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||||
github.com/mholt/acmez/v3 v3.1.6 h1:eGVQNObP0pBN4sxqrXeg7MYqTOWyoiYpQqITVWlrevk=
|
github.com/mholt/acmez/v3 v3.1.6 h1:eGVQNObP0pBN4sxqrXeg7MYqTOWyoiYpQqITVWlrevk=
|
||||||
github.com/mholt/acmez/v3 v3.1.6/go.mod h1:5nTPosTGosLxF3+LU4ygbgMRFDhbAVpqMI4+a4aHLBY=
|
github.com/mholt/acmez/v3 v3.1.6/go.mod h1:5nTPosTGosLxF3+LU4ygbgMRFDhbAVpqMI4+a4aHLBY=
|
||||||
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||||
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||||
|
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
|
||||||
|
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
|
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
|
||||||
@@ -52,11 +76,14 @@ golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
|||||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||||
|
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -131,9 +131,9 @@ func detectGPUs() ([]gpuInfo, error) {
|
|||||||
if !hasTool("nvidia-smi") {
|
if !hasTool("nvidia-smi") {
|
||||||
return nil, fmt.Errorf("nvidia-smi introuvable — sélection GPU disponible uniquement sur NVIDIA")
|
return nil, fmt.Errorf("nvidia-smi introuvable — sélection GPU disponible uniquement sur NVIDIA")
|
||||||
}
|
}
|
||||||
out, err := exec.Command("nvidia-smi",
|
out, err := hideCmd(exec.Command("nvidia-smi",
|
||||||
"--query-gpu=index,name,memory.total,memory.used,compute_cap",
|
"--query-gpu=index,name,memory.total,memory.used,compute_cap",
|
||||||
"--format=csv,noheader,nounits").Output()
|
"--format=csv,noheader,nounits")).Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("nvidia-smi a échoué: %w", err)
|
return nil, fmt.Errorf("nvidia-smi a échoué: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -584,7 +584,7 @@ func hasNvidiaGPU() bool {
|
|||||||
if !hasTool("nvidia-smi") {
|
if !hasTool("nvidia-smi") {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
out, err := exec.Command("nvidia-smi", "-L").Output()
|
out, err := hideCmd(exec.Command("nvidia-smi", "-L")).Output()
|
||||||
return err == nil && strings.Contains(string(out), "GPU")
|
return err == nil && strings.Contains(string(out), "GPU")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -593,7 +593,7 @@ func hasNvidiaGPU() bool {
|
|||||||
// joined with ';'. Empty when the driver is too old to report it (CMake then
|
// joined with ';'. Empty when the driver is too old to report it (CMake then
|
||||||
// falls back to native detection).
|
// falls back to native detection).
|
||||||
func detectCudaArch() string {
|
func detectCudaArch() string {
|
||||||
out, err := exec.Command("nvidia-smi", "--query-gpu=compute_cap", "--format=csv,noheader").Output()
|
out, err := hideCmd(exec.Command("nvidia-smi", "--query-gpu=compute_cap", "--format=csv,noheader")).Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
// les .syso après avoir bumpé la version : `go generate ./...`
|
// les .syso après avoir bumpé la version : `go generate ./...`
|
||||||
// (nécessite : go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@latest)
|
// (nécessite : go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@latest)
|
||||||
//
|
//
|
||||||
//go:generate goversioninfo -64 -o resource_windows_amd64.syso versioninfo.json
|
//go:generate goversioninfo -64 -icon=icon.ico -o resource_windows_amd64.syso versioninfo.json
|
||||||
//go:generate goversioninfo -64 -arm -o resource_windows_arm64.syso versioninfo.json
|
//go:generate goversioninfo -64 -arm -icon=icon.ico -o resource_windows_arm64.syso versioninfo.json
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const Version = "0.3.2"
|
const Version = "0.3.3"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Migration one-shot des anciens skills (SKILLS/<nom>/SKILL.md) vers la
|
// Migration one-shot des anciens skills (SKILLS/<nom>/SKILL.md) vers la
|
||||||
@@ -26,12 +26,23 @@ func main() {
|
|||||||
cleanupOldBinary()
|
cleanupOldBinary()
|
||||||
|
|
||||||
args := os.Args[1:]
|
args := os.Args[1:]
|
||||||
|
noArgs := len(args) == 0
|
||||||
cmd := "help"
|
cmd := "help"
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
cmd = args[0]
|
cmd = args[0]
|
||||||
args = args[1:]
|
args = args[1:]
|
||||||
}
|
}
|
||||||
|
// Double-clic sur le binaire (aucun argument, console fraîche) → on lance
|
||||||
|
// l'expérience « application » (UI web + navigateur) plutôt que d'afficher
|
||||||
|
// l'aide dans une console qui se referme aussitôt. Lancé depuis un shell,
|
||||||
|
// `jean` sans argument garde son comportement d'aide.
|
||||||
|
if noArgs && launchedByDoubleClick() {
|
||||||
|
relaunchDetachedApp() // relance sans console (ne revient pas), puis quitte l'original
|
||||||
|
return
|
||||||
|
}
|
||||||
switch cmd {
|
switch cmd {
|
||||||
|
case "app":
|
||||||
|
mustExit(cmdApp(args))
|
||||||
case "start", "stop", "restart", "status", "enable", "disable":
|
case "start", "stop", "restart", "status", "enable", "disable":
|
||||||
mustExit(serviceAction(cmd))
|
mustExit(serviceAction(cmd))
|
||||||
case "logs":
|
case "logs":
|
||||||
@@ -96,6 +107,9 @@ func printHelp() {
|
|||||||
|
|
||||||
Usage: jean <commande> [args]
|
Usage: jean <commande> [args]
|
||||||
|
|
||||||
|
Application:
|
||||||
|
app lance l'UI web + ouvre le navigateur (auto au double-clic du binaire)
|
||||||
|
|
||||||
Service:
|
Service:
|
||||||
start | stop | restart gérer le service (systemd sous Linux, processus en arrière-plan sous Windows)
|
start | stop | restart gérer le service (systemd sous Linux, processus en arrière-plan sous Windows)
|
||||||
status | logs état / logs en direct
|
status | logs état / logs en direct
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
@@ -20,6 +22,54 @@ func defaultJeanHome() string { return "/etc/jean" }
|
|||||||
// defaultEditor is used by `jean edit` when $EDITOR is unset.
|
// defaultEditor is used by `jean edit` when $EDITOR is unset.
|
||||||
func defaultEditor() string { return "nano" }
|
func defaultEditor() string { return "nano" }
|
||||||
|
|
||||||
|
// hideCmd : no-op sur Unix (pas de fenêtre de console à masquer).
|
||||||
|
func hideCmd(cmd *exec.Cmd) *exec.Cmd { return cmd }
|
||||||
|
|
||||||
|
// openBrowser ouvre l'URL dans le navigateur par défaut (macOS: open, Linux:
|
||||||
|
// xdg-open). Best-effort.
|
||||||
|
func openBrowser(url string) error {
|
||||||
|
bin := "xdg-open"
|
||||||
|
if runtime.GOOS == "darwin" {
|
||||||
|
bin = "open"
|
||||||
|
}
|
||||||
|
return exec.Command(bin, url).Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// totalRAMGB renvoie la RAM physique totale en Go (Linux: /proc/meminfo,
|
||||||
|
// macOS: sysctl hw.memsize).
|
||||||
|
func totalRAMGB() float64 {
|
||||||
|
if runtime.GOOS == "darwin" {
|
||||||
|
out, err := exec.Command("sysctl", "-n", "hw.memsize").Output()
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if v, err := strconv.ParseFloat(strings.TrimSpace(string(out)), 64); err == nil {
|
||||||
|
return v / (1024 * 1024 * 1024)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
b, err := os.ReadFile("/proc/meminfo")
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
for _, line := range strings.Split(string(b), "\n") {
|
||||||
|
if strings.HasPrefix(line, "MemTotal:") {
|
||||||
|
f := strings.Fields(line)
|
||||||
|
if len(f) >= 2 {
|
||||||
|
if kb, err := strconv.ParseFloat(f[1], 64); err == nil {
|
||||||
|
return kb / (1024 * 1024) // kB → Go
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// launchedByDoubleClick : sur Unix, jean tourne en service/CLI, on ne déclenche
|
||||||
|
// jamais le mode app sur un simple no-arg (éviterait de surprendre un serveur).
|
||||||
|
// L'expérience app double-clic est propre à Windows/macOS (coque native, phase 3).
|
||||||
|
func launchedByDoubleClick() bool { return false }
|
||||||
|
|
||||||
// setLibraryPath ensures llama-server can load shared libs bundled next to the
|
// setLibraryPath ensures llama-server can load shared libs bundled next to the
|
||||||
// binary by prepending dir to LD_LIBRARY_PATH. It also appends the CUDA runtime
|
// binary by prepending dir to LD_LIBRARY_PATH. It also appends the CUDA runtime
|
||||||
// lib directories: a CUDA-enabled build links against libcudart/libcublas, which
|
// lib directories: a CUDA-enabled build links against libcudart/libcublas, which
|
||||||
|
|||||||
+58
-2
@@ -55,6 +55,61 @@ func defaultJeanHome() string {
|
|||||||
// defaultEditor is used by `jean edit` when $EDITOR is unset.
|
// defaultEditor is used by `jean edit` when $EDITOR is unset.
|
||||||
func defaultEditor() string { return "notepad" }
|
func defaultEditor() string { return "notepad" }
|
||||||
|
|
||||||
|
// openBrowser ouvre l'URL dans le navigateur par défaut. rundll32 évite les
|
||||||
|
// pièges de quoting de `cmd /c start`.
|
||||||
|
func openBrowser(url string) error {
|
||||||
|
return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// hideCmd empêche une commande externe d'ouvrir une fenêtre de console
|
||||||
|
// (CREATE_NO_WINDOW). Indispensable quand Jean tourne sans console (mode app) :
|
||||||
|
// sinon chaque `nvidia-smi`/`git`/… ferait clignoter une fenêtre noire. Fusionne
|
||||||
|
// avec les flags déjà présents pour ne pas écraser un éventuel détachement.
|
||||||
|
func hideCmd(cmd *exec.Cmd) *exec.Cmd {
|
||||||
|
const createNoWindow = 0x08000000
|
||||||
|
if cmd.SysProcAttr == nil {
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
||||||
|
}
|
||||||
|
cmd.SysProcAttr.HideWindow = true
|
||||||
|
cmd.SysProcAttr.CreationFlags |= createNoWindow
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// totalRAMGB renvoie la RAM physique totale en Go (GlobalMemoryStatusEx).
|
||||||
|
func totalRAMGB() float64 {
|
||||||
|
var m struct {
|
||||||
|
dwLength uint32
|
||||||
|
dwMemoryLoad uint32
|
||||||
|
ullTotalPhys uint64
|
||||||
|
ullAvailPhys uint64
|
||||||
|
ullTotalPageFile uint64
|
||||||
|
ullAvailPageFile uint64
|
||||||
|
ullTotalVirtual uint64
|
||||||
|
ullAvailVirtual uint64
|
||||||
|
ullAvailExtendedVirtual uint64
|
||||||
|
}
|
||||||
|
m.dwLength = uint32(unsafe.Sizeof(m))
|
||||||
|
kernel32 := syscall.NewLazyDLL("kernel32.dll")
|
||||||
|
r, _, _ := kernel32.NewProc("GlobalMemoryStatusEx").Call(uintptr(unsafe.Pointer(&m)))
|
||||||
|
if r == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return float64(m.ullTotalPhys) / (1024 * 1024 * 1024)
|
||||||
|
}
|
||||||
|
|
||||||
|
// launchedByDoubleClick indique qu'on a été lancé par un double-clic (Explorer)
|
||||||
|
// et non depuis un shell existant. GetConsoleProcessList renvoie le nombre de
|
||||||
|
// process attachés à la console : 1 = on possède une console fraîche (double-
|
||||||
|
// clic), >1 = on a hérité de la console d'un shell (cmd/PowerShell), auquel cas
|
||||||
|
// l'utilisateur veut la CLI, pas l'app.
|
||||||
|
func launchedByDoubleClick() bool {
|
||||||
|
kernel32 := syscall.NewLazyDLL("kernel32.dll")
|
||||||
|
proc := kernel32.NewProc("GetConsoleProcessList")
|
||||||
|
var pids [4]uint32
|
||||||
|
r, _, _ := proc.Call(uintptr(unsafe.Pointer(&pids[0])), uintptr(len(pids)))
|
||||||
|
return r == 1
|
||||||
|
}
|
||||||
|
|
||||||
// setLibraryPath ensures llama-server can load its dependent DLLs. Windows
|
// setLibraryPath ensures llama-server can load its dependent DLLs. Windows
|
||||||
// resolves them via PATH (and the binary's own directory), so we prepend dir.
|
// resolves them via PATH (and the binary's own directory), so we prepend dir.
|
||||||
// For a CUDA build we must also add the CUDA Toolkit's bin: ggml-cuda.dll links
|
// For a CUDA build we must also add the CUDA Toolkit's bin: ggml-cuda.dll links
|
||||||
@@ -256,7 +311,8 @@ func cudaPathEnv(toolkitDir string) []string {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// newShellCmd builds the command used by the run_shell tool.
|
// newShellCmd builds the command used by the run_shell tool. hideCmd évite un
|
||||||
|
// flash de console à chaque commande d'agent quand Jean tourne en mode app.
|
||||||
func newShellCmd(ctx context.Context, command string) *exec.Cmd {
|
func newShellCmd(ctx context.Context, command string) *exec.Cmd {
|
||||||
return exec.CommandContext(ctx, "cmd", "/C", command)
|
return hideCmd(exec.CommandContext(ctx, "cmd", "/C", command))
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
// proc_other.go — hors Windows, pas de détachement (le mode app double-clic est
|
||||||
|
// propre à Windows). Défini pour la compilation ; non appelé en pratique car
|
||||||
|
// launchedByDoubleClick renvoie false sur Unix.
|
||||||
|
|
||||||
|
func relaunchDetachedApp() { _ = cmdApp(nil) }
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
// proc_windows.go — au double-clic, on relance Jean en processus DÉTACHÉ, sans
|
||||||
|
// console. L'original (qui, lui, possède la console fraîche créée par Explorer)
|
||||||
|
// se termine aussitôt, fermant sa console. Le nouveau process n'a aucune fenêtre
|
||||||
|
// de console : il n'y a donc plus de « terminal noir » et plus rien à fermer qui
|
||||||
|
// couperait le serveur. Seuls restent le splash puis l'icône de la zone de
|
||||||
|
// notification.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func relaunchDetachedApp() {
|
||||||
|
const (
|
||||||
|
detachedProcess = 0x00000008
|
||||||
|
createNoWindow = 0x08000000
|
||||||
|
createNewProcGrp = 0x00000200
|
||||||
|
)
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
_ = cmdApp(nil) // repli : on lance quand même en place
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cmd := exec.Command(exe, "app")
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
HideWindow: true,
|
||||||
|
CreationFlags: detachedProcess | createNoWindow | createNewProcGrp,
|
||||||
|
}
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
_ = cmdApp(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
os.Exit(0) // ferme l'original → sa console disparaît ; le détaché continue
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
+2
-2
@@ -36,9 +36,9 @@ func editConfig() error {
|
|||||||
// nvidia-smi is available on both Linux and Windows when an NVIDIA driver is
|
// nvidia-smi is available on both Linux and Windows when an NVIDIA driver is
|
||||||
// installed, so this is platform-neutral.
|
// installed, so this is platform-neutral.
|
||||||
func showVram() error {
|
func showVram() error {
|
||||||
out, err := exec.Command("nvidia-smi",
|
out, err := hideCmd(exec.Command("nvidia-smi",
|
||||||
"--query-gpu=name,memory.used,memory.total,utilization.gpu,temperature.gpu",
|
"--query-gpu=name,memory.used,memory.total,utilization.gpu,temperature.gpu",
|
||||||
"--format=csv,noheader,nounits").Output()
|
"--format=csv,noheader,nounits")).Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("nvidia-smi indisponible: %w", err)
|
return fmt.Errorf("nvidia-smi indisponible: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-8
@@ -12,6 +12,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
// On Windows there is no systemd, so "the service" is a background copy of
|
// On Windows there is no systemd, so "the service" is a background copy of
|
||||||
@@ -108,7 +109,7 @@ func svcStop(verbose bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// taskkill /T tue aussi le processus enfant llama-server.
|
// taskkill /T tue aussi le processus enfant llama-server.
|
||||||
cmd := exec.Command("taskkill", "/PID", strconv.Itoa(pid), "/T", "/F")
|
cmd := hideCmd(exec.Command("taskkill", "/PID", strconv.Itoa(pid), "/T", "/F"))
|
||||||
out, err := cmd.CombinedOutput()
|
out, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("arrêt du PID %d: %w\n%s", pid, err, string(out))
|
return fmt.Errorf("arrêt du PID %d: %w\n%s", pid, err, string(out))
|
||||||
@@ -175,18 +176,31 @@ func readServicePID() int {
|
|||||||
return pid
|
return pid
|
||||||
}
|
}
|
||||||
|
|
||||||
// processAlive reports whether a PID is currently running, via tasklist (always
|
// processAlive reports whether a PID is currently running. On utilise l'API
|
||||||
// present on Windows, needs no special privileges).
|
// Win32 (OpenProcess + GetExitCodeProcess) plutôt que `tasklist` : l'ancien
|
||||||
|
// appel externe faisait CLIGNOTER une fenêtre de console à CHAQUE vérification,
|
||||||
|
// or l'UI web poll le statut en boucle → rafale de fenêtres noires. L'API ne
|
||||||
|
// lance aucun process.
|
||||||
func processAlive(pid int) bool {
|
func processAlive(pid int) bool {
|
||||||
out, err := exec.Command("tasklist", "/FI", "PID eq "+strconv.Itoa(pid), "/NH").Output()
|
if pid <= 0 {
|
||||||
if err != nil {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
s := string(out)
|
const (
|
||||||
if strings.Contains(s, "No tasks") {
|
queryLimitedInfo = 0x1000
|
||||||
|
stillActive = 259
|
||||||
|
)
|
||||||
|
k := syscall.NewLazyDLL("kernel32.dll")
|
||||||
|
h, _, _ := k.NewProc("OpenProcess").Call(queryLimitedInfo, 0, uintptr(pid))
|
||||||
|
if h == 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return strings.Contains(s, strconv.Itoa(pid))
|
defer k.NewProc("CloseHandle").Call(h)
|
||||||
|
var code uint32
|
||||||
|
r, _, _ := k.NewProc("GetExitCodeProcess").Call(h, uintptr(unsafe.Pointer(&code)))
|
||||||
|
if r == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return code == stillActive
|
||||||
}
|
}
|
||||||
|
|
||||||
// tailFile returns the last n lines of the file at path (best-effort).
|
// tailFile returns the last n lines of the file at path (best-effort).
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
// splash_other.go — pas de fenêtre de démarrage hors Windows (no-op).
|
||||||
|
|
||||||
|
type splash struct{}
|
||||||
|
|
||||||
|
func showSplash(text string) *splash { return &splash{} }
|
||||||
|
func (s *splash) close() {}
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
// splash_windows.go — écran de démarrage « Lancement de Jean… ».
|
||||||
|
//
|
||||||
|
// Petite fenêtre sans bordure, à coins arrondis, aux couleurs de la marque
|
||||||
|
// (fond bleu #1f6feb, logo « j » blanc, texte blanc), affichée le temps que le
|
||||||
|
// serveur monte et que le navigateur s'ouvre. Rendu GDI (pur syscall, aucun
|
||||||
|
// CGO). Elle tourne sur son propre thread avec sa boucle de messages ; close()
|
||||||
|
// lui envoie WM_CLOSE.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
splashW = 440
|
||||||
|
splashH = 150
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
u32s = syscall.NewLazyDLL("user32.dll")
|
||||||
|
g32s = syscall.NewLazyDLL("gdi32.dll")
|
||||||
|
k32s = syscall.NewLazyDLL("kernel32.dll")
|
||||||
|
|
||||||
|
pRegisterClassExW = u32s.NewProc("RegisterClassExW")
|
||||||
|
pCreateWindowExW = u32s.NewProc("CreateWindowExW")
|
||||||
|
pDefWindowProcW = u32s.NewProc("DefWindowProcW")
|
||||||
|
pShowWindow = u32s.NewProc("ShowWindow")
|
||||||
|
pUpdateWindow = u32s.NewProc("UpdateWindow")
|
||||||
|
pGetMessageW = u32s.NewProc("GetMessageW")
|
||||||
|
pTranslateMessage = u32s.NewProc("TranslateMessage")
|
||||||
|
pDispatchMessageW = u32s.NewProc("DispatchMessageW")
|
||||||
|
pPostQuitMessage = u32s.NewProc("PostQuitMessage")
|
||||||
|
pDestroyWindow = u32s.NewProc("DestroyWindow")
|
||||||
|
pPostMessageW = u32s.NewProc("PostMessageW")
|
||||||
|
pBeginPaint = u32s.NewProc("BeginPaint")
|
||||||
|
pEndPaint = u32s.NewProc("EndPaint")
|
||||||
|
pFillRect = u32s.NewProc("FillRect")
|
||||||
|
pDrawTextW = u32s.NewProc("DrawTextW")
|
||||||
|
pGetSystemMetrics = u32s.NewProc("GetSystemMetrics")
|
||||||
|
pLoadCursorW = u32s.NewProc("LoadCursorW")
|
||||||
|
pSetWindowRgn = u32s.NewProc("SetWindowRgn")
|
||||||
|
|
||||||
|
pCreateSolidBrush = g32s.NewProc("CreateSolidBrush")
|
||||||
|
pDeleteObject = g32s.NewProc("DeleteObject")
|
||||||
|
pCreateFontW = g32s.NewProc("CreateFontW")
|
||||||
|
pSelectObject = g32s.NewProc("SelectObject")
|
||||||
|
pSetTextColor = g32s.NewProc("SetTextColor")
|
||||||
|
pSetBkMode = g32s.NewProc("SetBkMode")
|
||||||
|
pCreateRoundRectRgn = g32s.NewProc("CreateRoundRectRgn")
|
||||||
|
|
||||||
|
pGetModuleHandleW = k32s.NewProc("GetModuleHandleW")
|
||||||
|
|
||||||
|
splashClassOnce sync.Once
|
||||||
|
splashProc = syscall.NewCallback(splashWndProc)
|
||||||
|
)
|
||||||
|
|
||||||
|
type rect struct{ left, top, right, bottom int32 }
|
||||||
|
type point struct{ x, y int32 }
|
||||||
|
type msg struct {
|
||||||
|
hwnd uintptr
|
||||||
|
message uint32
|
||||||
|
wParam uintptr
|
||||||
|
lParam uintptr
|
||||||
|
time uint32
|
||||||
|
pt point
|
||||||
|
}
|
||||||
|
type paintStruct struct {
|
||||||
|
hdc uintptr
|
||||||
|
fErase int32
|
||||||
|
rcPaint rect
|
||||||
|
fRestore int32
|
||||||
|
fIncUpdate int32
|
||||||
|
rgbReserved [32]byte
|
||||||
|
}
|
||||||
|
type wndClassExW struct {
|
||||||
|
cbSize uint32
|
||||||
|
style uint32
|
||||||
|
lpfnWndProc uintptr
|
||||||
|
cbClsExtra int32
|
||||||
|
cbWndExtra int32
|
||||||
|
hInstance uintptr
|
||||||
|
hIcon uintptr
|
||||||
|
hCursor uintptr
|
||||||
|
hbrBackground uintptr
|
||||||
|
lpszMenuName *uint16
|
||||||
|
lpszClassName *uint16
|
||||||
|
hIconSm uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
colBlue = 0x00EB6F1F // COLORREF = 0x00BBGGRR pour #1f6feb
|
||||||
|
colWhite = 0x00FFFFFF
|
||||||
|
)
|
||||||
|
|
||||||
|
type splash struct{ hwnd uintptr }
|
||||||
|
|
||||||
|
func showSplash(text string) *splash {
|
||||||
|
s := &splash{}
|
||||||
|
ready := make(chan uintptr, 1)
|
||||||
|
go func() {
|
||||||
|
runtime.LockOSThread()
|
||||||
|
defer runtime.UnlockOSThread()
|
||||||
|
hwnd := createSplashWindow()
|
||||||
|
ready <- hwnd
|
||||||
|
if hwnd != 0 {
|
||||||
|
pumpMessages()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
s.hwnd = <-ready
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *splash) close() {
|
||||||
|
if s == nil || s.hwnd == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const wmClose = 0x0010
|
||||||
|
pPostMessageW.Call(s.hwnd, wmClose, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSplashWindow() uintptr {
|
||||||
|
className, _ := syscall.UTF16PtrFromString("JeanSplash")
|
||||||
|
hInst, _, _ := pGetModuleHandleW.Call(0)
|
||||||
|
|
||||||
|
splashClassOnce.Do(func() {
|
||||||
|
idcArrow := uintptr(32512)
|
||||||
|
cursor, _, _ := pLoadCursorW.Call(0, idcArrow)
|
||||||
|
wc := wndClassExW{
|
||||||
|
lpfnWndProc: splashProc,
|
||||||
|
hInstance: hInst,
|
||||||
|
hCursor: cursor,
|
||||||
|
lpszClassName: className,
|
||||||
|
}
|
||||||
|
wc.cbSize = uint32(unsafe.Sizeof(wc))
|
||||||
|
pRegisterClassExW.Call(uintptr(unsafe.Pointer(&wc)))
|
||||||
|
})
|
||||||
|
|
||||||
|
const (
|
||||||
|
wsPopup = 0x80000000
|
||||||
|
exTopmost = 0x00000008
|
||||||
|
exToolwindow = 0x00000080
|
||||||
|
smCX = 0
|
||||||
|
smCY = 1
|
||||||
|
)
|
||||||
|
cx, _, _ := pGetSystemMetrics.Call(smCX)
|
||||||
|
cy, _, _ := pGetSystemMetrics.Call(smCY)
|
||||||
|
x := (int(cx) - splashW) / 2
|
||||||
|
y := (int(cy) - splashH) / 2
|
||||||
|
title, _ := syscall.UTF16PtrFromString("Jean")
|
||||||
|
|
||||||
|
hwnd, _, _ := pCreateWindowExW.Call(
|
||||||
|
uintptr(exTopmost|exToolwindow),
|
||||||
|
uintptr(unsafe.Pointer(className)),
|
||||||
|
uintptr(unsafe.Pointer(title)),
|
||||||
|
uintptr(uint32(wsPopup)),
|
||||||
|
uintptr(x), uintptr(y), uintptr(splashW), uintptr(splashH),
|
||||||
|
0, 0, hInst, 0,
|
||||||
|
)
|
||||||
|
if hwnd == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
// Coins arrondis.
|
||||||
|
rgn, _, _ := pCreateRoundRectRgn.Call(0, 0, splashW+1, splashH+1, 22, 22)
|
||||||
|
pSetWindowRgn.Call(hwnd, rgn, 1)
|
||||||
|
|
||||||
|
const swShowNoActivate = 4
|
||||||
|
pShowWindow.Call(hwnd, swShowNoActivate)
|
||||||
|
pUpdateWindow.Call(hwnd)
|
||||||
|
return hwnd
|
||||||
|
}
|
||||||
|
|
||||||
|
func pumpMessages() {
|
||||||
|
var m msg
|
||||||
|
for {
|
||||||
|
r, _, _ := pGetMessageW.Call(uintptr(unsafe.Pointer(&m)), 0, 0, 0)
|
||||||
|
if int32(r) <= 0 { // 0 = WM_QUIT, -1 = erreur
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pTranslateMessage.Call(uintptr(unsafe.Pointer(&m)))
|
||||||
|
pDispatchMessageW.Call(uintptr(unsafe.Pointer(&m)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func splashWndProc(hwnd, message, wParam, lParam uintptr) uintptr {
|
||||||
|
const (
|
||||||
|
wmDestroy = 0x0002
|
||||||
|
wmClose = 0x0010
|
||||||
|
wmPaint = 0x000F
|
||||||
|
)
|
||||||
|
switch message {
|
||||||
|
case wmPaint:
|
||||||
|
var ps paintStruct
|
||||||
|
hdc, _, _ := pBeginPaint.Call(hwnd, uintptr(unsafe.Pointer(&ps)))
|
||||||
|
paintSplash(hdc)
|
||||||
|
pEndPaint.Call(hwnd, uintptr(unsafe.Pointer(&ps)))
|
||||||
|
return 0
|
||||||
|
case wmClose:
|
||||||
|
pDestroyWindow.Call(hwnd)
|
||||||
|
return 0
|
||||||
|
case wmDestroy:
|
||||||
|
pPostQuitMessage.Call(0)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
r, _, _ := pDefWindowProcW.Call(hwnd, message, wParam, lParam)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func paintSplash(hdc uintptr) {
|
||||||
|
// Fond bleu.
|
||||||
|
blue, _, _ := pCreateSolidBrush.Call(colBlue)
|
||||||
|
full := rect{0, 0, splashW, splashH}
|
||||||
|
pFillRect.Call(hdc, uintptr(unsafe.Pointer(&full)), blue)
|
||||||
|
pDeleteObject.Call(blue)
|
||||||
|
|
||||||
|
// Logo « j » blanc (rects du favicon sur grille 12, mis à l'échelle).
|
||||||
|
white, _, _ := pCreateSolidBrush.Call(colWhite)
|
||||||
|
const logo = 58
|
||||||
|
ox, oy := int32(40), int32((splashH-logo)/2)
|
||||||
|
scale := float64(logo) / 12
|
||||||
|
for _, rc := range [][4]float64{{6, 3, 2, 2}, {6, 5, 2, 2}, {4, 7, 2, 2}} {
|
||||||
|
r := rect{
|
||||||
|
left: ox + int32(rc[0]*scale),
|
||||||
|
top: oy + int32(rc[1]*scale),
|
||||||
|
right: ox + int32((rc[0]+rc[2])*scale),
|
||||||
|
bottom: oy + int32((rc[1]+rc[3])*scale),
|
||||||
|
}
|
||||||
|
pFillRect.Call(hdc, uintptr(unsafe.Pointer(&r)), white)
|
||||||
|
}
|
||||||
|
pDeleteObject.Call(white)
|
||||||
|
|
||||||
|
// Textes (blanc, fond transparent).
|
||||||
|
const transparent = 1
|
||||||
|
pSetBkMode.Call(hdc, transparent)
|
||||||
|
pSetTextColor.Call(hdc, colWhite)
|
||||||
|
|
||||||
|
textX := ox + logo + 26
|
||||||
|
drawText(hdc, "Jean", -30, 700, rect{textX, 40, splashW - 20, 82})
|
||||||
|
drawText(hdc, "Lancement en cours…", -17, 400, rect{textX, 84, splashW - 20, 118})
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawText(hdc uintptr, s string, height, weight int32, r rect) {
|
||||||
|
face, _ := syscall.UTF16PtrFromString("Segoe UI")
|
||||||
|
const (
|
||||||
|
defaultCharset = 1
|
||||||
|
cleartypeQuality = 5
|
||||||
|
)
|
||||||
|
font, _, _ := pCreateFontW.Call(
|
||||||
|
uintptr(height), 0, 0, 0, uintptr(weight),
|
||||||
|
0, 0, 0, defaultCharset, 0, 0, cleartypeQuality, 0,
|
||||||
|
uintptr(unsafe.Pointer(face)),
|
||||||
|
)
|
||||||
|
old, _, _ := pSelectObject.Call(hdc, font)
|
||||||
|
txt, _ := syscall.UTF16PtrFromString(s)
|
||||||
|
const (
|
||||||
|
dtLeft = 0x0000
|
||||||
|
dtSingleline = 0x0020
|
||||||
|
dtVcenter = 0x0004
|
||||||
|
dtNoclip = 0x0100
|
||||||
|
)
|
||||||
|
rc := r
|
||||||
|
pDrawTextW.Call(hdc, uintptr(unsafe.Pointer(txt)), ^uintptr(0),
|
||||||
|
uintptr(unsafe.Pointer(&rc)), dtLeft|dtSingleline|dtVcenter|dtNoclip)
|
||||||
|
pSelectObject.Call(hdc, old)
|
||||||
|
pDeleteObject.Call(font)
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
// tray_other.go — pas d'icône de zone de notification hors Windows (la demande
|
||||||
|
// vise le tray Windows). On se contente d'ouvrir l'UI dans le navigateur et de
|
||||||
|
// garder le serveur en vie. Aucun import systray ici → les builds Linux/macOS
|
||||||
|
// restent purs (CGO_ENABLED=0).
|
||||||
|
|
||||||
|
func runTray(url string) {
|
||||||
|
select {} // garde le serveur en vie (Ctrl-C pour quitter) ; le navigateur est déjà ouvert par cmdApp
|
||||||
|
}
|
||||||
+121
@@ -0,0 +1,121 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
// tray_windows.go — icône de Jean dans la zone de notification Windows (system
|
||||||
|
// tray). Quand on lance l'app (double-clic ou `jean app`), l'UI web s'ouvre dans
|
||||||
|
// le navigateur ET une petite icône apparaît près de l'horloge : elle montre que
|
||||||
|
// Jean tourne et offre un menu « Ouvrir Jean » / « Quitter ».
|
||||||
|
//
|
||||||
|
// getlantern/systray est pur Go sur Windows (Win32 via x/sys, aucun CGO) — le
|
||||||
|
// binaire reste statique. L'import est confiné à ce fichier windows pour que les
|
||||||
|
// builds Linux/macOS (CGO_ENABLED=0) ne le touchent jamais.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/png"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/getlantern/systray"
|
||||||
|
)
|
||||||
|
|
||||||
|
// runTray ouvre l'UI dans le navigateur puis fait tourner l'icône de la zone de
|
||||||
|
// notification. Bloque jusqu'à ce que l'utilisateur choisisse « Quitter ».
|
||||||
|
func runTray(url string) {
|
||||||
|
systray.Run(func() {
|
||||||
|
systray.SetIcon(trayIcon())
|
||||||
|
systray.SetTitle("Jean")
|
||||||
|
systray.SetTooltip("Jean — votre IA locale")
|
||||||
|
mOpen := systray.AddMenuItem("Ouvrir Jean", "Ouvrir l'interface")
|
||||||
|
systray.AddSeparator()
|
||||||
|
mQuit := systray.AddMenuItem("Quitter", "Arrêter Jean")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-mOpen.ClickedCh:
|
||||||
|
_ = openBrowser(url)
|
||||||
|
case <-mQuit.ClickedCh:
|
||||||
|
systray.Quit()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}, func() {
|
||||||
|
os.Exit(0) // « Quitter » → arrête tout le process (serveur inclus)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// trayIcon génère l'icône (.ico) de Jean, RÉPLIQUE EXACTE du favicon de l'UI
|
||||||
|
// web : carré bleu arrondi #1f6feb + « j » blanc (rects (6,3) (6,5) (4,7) sur
|
||||||
|
// une grille 12x12). Générée à la volée — pas d'asset binaire à embarquer.
|
||||||
|
// Windows accepte un PNG (avec alpha pour les coins arrondis) dans le .ico.
|
||||||
|
func trayIcon() []byte {
|
||||||
|
const n = 32
|
||||||
|
blue := color.RGBA{0x1f, 0x6f, 0xeb, 0xff}
|
||||||
|
white := color.RGBA{0xff, 0xff, 0xff, 0xff}
|
||||||
|
clear := color.RGBA{0, 0, 0, 0}
|
||||||
|
const r = 2.0 // rayon des coins, en unités de la grille 12
|
||||||
|
// rects blancs du favicon (x, y, w, h en unités 12)
|
||||||
|
rects := [][4]float64{{6, 3, 2, 2}, {6, 5, 2, 2}, {4, 7, 2, 2}}
|
||||||
|
// hors du rectangle à coins arrondis (grille 12) → pixel transparent
|
||||||
|
outside := func(gx, gy float64) bool {
|
||||||
|
corner := func(cx, cy float64) bool {
|
||||||
|
dx, dy := gx-cx, gy-cy
|
||||||
|
return dx*dx+dy*dy > r*r
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case gx < r && gy < r:
|
||||||
|
return corner(r, r)
|
||||||
|
case gx > 12-r && gy < r:
|
||||||
|
return corner(12-r, r)
|
||||||
|
case gx < r && gy > 12-r:
|
||||||
|
return corner(r, 12-r)
|
||||||
|
case gx > 12-r && gy > 12-r:
|
||||||
|
return corner(12-r, 12-r)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
img := image.NewRGBA(image.Rect(0, 0, n, n))
|
||||||
|
scale := float64(n) / 12
|
||||||
|
for y := 0; y < n; y++ {
|
||||||
|
for x := 0; x < n; x++ {
|
||||||
|
gx := (float64(x) + 0.5) / scale
|
||||||
|
gy := (float64(y) + 0.5) / scale
|
||||||
|
if outside(gx, gy) {
|
||||||
|
img.Set(x, y, clear)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c := blue
|
||||||
|
for _, rc := range rects {
|
||||||
|
if gx >= rc[0] && gx < rc[0]+rc[2] && gy >= rc[1] && gy < rc[1]+rc[3] {
|
||||||
|
c = white
|
||||||
|
}
|
||||||
|
}
|
||||||
|
img.Set(x, y, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var pngBuf bytes.Buffer
|
||||||
|
_ = png.Encode(&pngBuf, img)
|
||||||
|
p := pngBuf.Bytes()
|
||||||
|
|
||||||
|
// Conteneur ICO : ICONDIR (6) + 1 ICONDIRENTRY (16) + PNG.
|
||||||
|
var ico bytes.Buffer
|
||||||
|
binary.Write(&ico, binary.LittleEndian, uint16(0)) // réservé
|
||||||
|
binary.Write(&ico, binary.LittleEndian, uint16(1)) // type = icône
|
||||||
|
binary.Write(&ico, binary.LittleEndian, uint16(1)) // nombre d'images
|
||||||
|
ico.WriteByte(n) // largeur
|
||||||
|
ico.WriteByte(n) // hauteur
|
||||||
|
ico.WriteByte(0) // couleurs
|
||||||
|
ico.WriteByte(0) // réservé
|
||||||
|
binary.Write(&ico, binary.LittleEndian, uint16(1)) // plans
|
||||||
|
binary.Write(&ico, binary.LittleEndian, uint16(32)) // bits/pixel
|
||||||
|
binary.Write(&ico, binary.LittleEndian, uint32(len(p))) // taille données
|
||||||
|
binary.Write(&ico, binary.LittleEndian, uint32(6+16)) // offset données
|
||||||
|
ico.Write(p)
|
||||||
|
return ico.Bytes()
|
||||||
|
}
|
||||||
+3
-1
@@ -59,6 +59,7 @@ body{padding:env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-i
|
|||||||
}
|
}
|
||||||
h1{margin:0;font-size:18px;color:var(--accent)}
|
h1{margin:0;font-size:18px;color:var(--accent)}
|
||||||
#brand{display:inline-flex;align-items:baseline}
|
#brand{display:inline-flex;align-items:baseline}
|
||||||
|
#ver{font-size:10px;font-weight:400;color:var(--dim);margin-left:5px;letter-spacing:0}
|
||||||
.sidehead{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:18px}
|
.sidehead{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:18px}
|
||||||
/* pastille d'état service, en haut à droite de la barre latérale */
|
/* pastille d'état service, en haut à droite de la barre latérale */
|
||||||
.statuspill{display:inline-flex;align-items:center;gap:6px;font-size:11px;padding:3px 10px;border-radius:999px;border:1px solid var(--border);background:var(--panel);color:var(--dim);font-weight:600;white-space:nowrap}
|
.statuspill{display:inline-flex;align-items:center;gap:6px;font-size:11px;padding:3px 10px;border-radius:999px;border:1px solid var(--border);background:var(--panel);color:var(--dim);font-weight:600;white-space:nowrap}
|
||||||
@@ -288,7 +289,7 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
|||||||
<div id="backdrop" onclick="toggleSide()"></div>
|
<div id="backdrop" onclick="toggleSide()"></div>
|
||||||
<div class="side" id="side">
|
<div class="side" id="side">
|
||||||
<div class="sidehead">
|
<div class="sidehead">
|
||||||
<h1 id="brand"><span id="brand-a">a</span>jean</h1>
|
<h1 id="brand"><span id="brand-a">a</span>jean<span id="ver"></span></h1>
|
||||||
<span id="status-svc" class="statuspill">…</span>
|
<span id="status-svc" class="statuspill">…</span>
|
||||||
</div>
|
</div>
|
||||||
<details open><summary>GPU</summary>
|
<details open><summary>GPU</summary>
|
||||||
@@ -639,6 +640,7 @@ async function loadStatus(){
|
|||||||
el.innerHTML='<span class="dot"></span>'+txt+' <span class="port">:'+s.port+'</span>';
|
el.innerHTML='<span class="dot"></span>'+txt+' <span class="port">:'+s.port+'</span>';
|
||||||
MODEL_READY = !!(s.active && s.health);
|
MODEL_READY = !!(s.active && s.health);
|
||||||
if(s.ctx){ CTX_MAX=s.ctx; updateCtxMeter(); }
|
if(s.ctx){ CTX_MAX=s.ctx; updateCtxMeter(); }
|
||||||
|
if(s.version){ document.getElementById('ver').textContent='v'+s.version; }
|
||||||
}
|
}
|
||||||
// Compteur de contexte : CTX_USED estimé via les stats serveur (prefill+decode
|
// Compteur de contexte : CTX_USED estimé via les stats serveur (prefill+decode
|
||||||
// du dernier tour ≈ taille du prochain prompt). À 90% on propose de compacter.
|
// du dernier tour ≈ taille du prochain prompt). À 90% on propose de compacter.
|
||||||
|
|||||||
+3
-3
@@ -3,13 +3,13 @@
|
|||||||
"FileVersion": {
|
"FileVersion": {
|
||||||
"Major": 0,
|
"Major": 0,
|
||||||
"Minor": 3,
|
"Minor": 3,
|
||||||
"Patch": 2,
|
"Patch": 3,
|
||||||
"Build": 0
|
"Build": 0
|
||||||
},
|
},
|
||||||
"ProductVersion": {
|
"ProductVersion": {
|
||||||
"Major": 0,
|
"Major": 0,
|
||||||
"Minor": 3,
|
"Minor": 3,
|
||||||
"Patch": 2,
|
"Patch": 3,
|
||||||
"Build": 0
|
"Build": 0
|
||||||
},
|
},
|
||||||
"FileFlagsMask": "3f",
|
"FileFlagsMask": "3f",
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"LegalCopyright": "Copyright (c) 2026 Jean contributors. MIT License.",
|
"LegalCopyright": "Copyright (c) 2026 Jean contributors. MIT License.",
|
||||||
"OriginalFilename": "jean.exe",
|
"OriginalFilename": "jean.exe",
|
||||||
"ProductName": "Jean",
|
"ProductName": "Jean",
|
||||||
"ProductVersion": "0.3.2",
|
"ProductVersion": "0.3.3",
|
||||||
"Comments": "https://github.com/nathaninline/jean — projet open source (MIT)"
|
"Comments": "https://github.com/nathaninline/jean — projet open source (MIT)"
|
||||||
},
|
},
|
||||||
"VarFileInfo": {
|
"VarFileInfo": {
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ func newWebMux() *http.ServeMux {
|
|||||||
api("/api/status", handleStatus)
|
api("/api/status", handleStatus)
|
||||||
api("/api/vram", handleVram)
|
api("/api/vram", handleVram)
|
||||||
api("/api/config", handleConfigEnv)
|
api("/api/config", handleConfigEnv)
|
||||||
|
api("/api/catalog", handleCatalog)
|
||||||
api("/api/models", handleModels)
|
api("/api/models", handleModels)
|
||||||
api("/api/models/delete", handleModelDelete)
|
api("/api/models/delete", handleModelDelete)
|
||||||
api("/api/models/download", handleModelDownload)
|
api("/api/models/download", handleModelDownload)
|
||||||
@@ -229,18 +230,19 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
sendJSON(w, 200, map[string]any{
|
sendJSON(w, 200, map[string]any{
|
||||||
"state": state,
|
"state": state,
|
||||||
"active": active,
|
"active": active,
|
||||||
"health": health,
|
"health": health,
|
||||||
"port": LLMPort(),
|
"port": LLMPort(),
|
||||||
"ctx": ctx,
|
"ctx": ctx,
|
||||||
|
"version": Version,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleVram(w http.ResponseWriter, r *http.Request) {
|
func handleVram(w http.ResponseWriter, r *http.Request) {
|
||||||
out, err := exec.Command("nvidia-smi",
|
out, err := hideCmd(exec.Command("nvidia-smi",
|
||||||
"--query-gpu=name,memory.used,memory.total,utilization.gpu,temperature.gpu",
|
"--query-gpu=name,memory.used,memory.total,utilization.gpu,temperature.gpu",
|
||||||
"--format=csv,noheader,nounits").Output()
|
"--format=csv,noheader,nounits")).Output()
|
||||||
gpus := []map[string]any{}
|
gpus := []map[string]any{}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
||||||
|
|||||||
Reference in new issue
Block a user