mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-12 01:37:06 +02:00
v0.5.1 : accès distant ajean.link depuis l'UI + app Windows sans console
- Panneau « Accès distant » dans l'UI web : abonnement + liaison ajean.link en un clic (popup connect.html → clé remise à l'agent local), sans terminal. Endpoints locaux /api/link/status|connect|disconnect|paircode (web_link_api.go). - Accès OpenAI public : refuse d'être activé sans accès distant, avec message. - Windows : binaire compilé en sous-système GUI (-H=windowsgui) → plus aucune fenêtre de console au double-clic ; rattachement à la console parente en CLI (sys_console_windows.go). Suppression du hack relaunchDetachedApp. - Windows : masquage (CREATE_NO_WINDOW) de TOUS les sous-process — git/vswhere du panneau MOTEUR (flash à chaque refresh), llama-server, MCP stdio, winget… - Vouvoiement du nouveau parcours.
This commit is contained in:
1 parent
8e57685b19
commit
d8b08dbb89
22 files changed
+444
-103
No files matched your search
@@ -24,9 +24,12 @@ jobs:
|
||||
run: |
|
||||
mkdir -p dist
|
||||
for t in darwin/amd64 darwin/arm64 linux/amd64 linux/arm64 windows/amd64 windows/arm64; do
|
||||
os=${t%/*}; arch=${t#*/}; ext=""; [ "$os" = windows ] && ext=".exe"
|
||||
os=${t%/*}; arch=${t#*/}; ext=""; ldflags="-s -w"
|
||||
# Windows : sous-système GUI → aucune console noire au double-clic
|
||||
# (l'app se rattache à la console du terminal parent si lancée en CLI).
|
||||
[ "$os" = windows ] && { ext=".exe"; ldflags="$ldflags -H=windowsgui"; }
|
||||
GOOS=$os GOARCH=$arch CGO_ENABLED=0 \
|
||||
go build -trimpath -ldflags="-s -w" -o "dist/jean-$os-$arch$ext" ./cmd/jean
|
||||
go build -trimpath -ldflags="$ldflags" -o "dist/jean-$os-$arch$ext" ./cmd/jean
|
||||
done
|
||||
(cd dist && sha256sum jean-* > SHA256SUMS)
|
||||
ls -l dist && cat dist/SHA256SUMS
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -3,13 +3,13 @@
|
||||
"FileVersion": {
|
||||
"Major": 0,
|
||||
"Minor": 5,
|
||||
"Patch": 0,
|
||||
"Patch": 1,
|
||||
"Build": 0
|
||||
},
|
||||
"ProductVersion": {
|
||||
"Major": 0,
|
||||
"Minor": 5,
|
||||
"Patch": 0,
|
||||
"Patch": 1,
|
||||
"Build": 0
|
||||
},
|
||||
"FileFlagsMask": "3f",
|
||||
@@ -25,7 +25,7 @@
|
||||
"LegalCopyright": "Copyright (c) 2026 Jean contributors. MIT License.",
|
||||
"OriginalFilename": "jean.exe",
|
||||
"ProductName": "Jean",
|
||||
"ProductVersion": "0.5.0",
|
||||
"ProductVersion": "0.5.1",
|
||||
"Comments": "https://github.com/nathaninline/jean — projet open source (MIT)"
|
||||
},
|
||||
"VarFileInfo": {
|
||||
|
||||
@@ -441,7 +441,7 @@ func missingTools(tools []string) []string {
|
||||
|
||||
// gitOutput runs a git command in `dir` and returns trimmed stdout (or "").
|
||||
func gitOutput(dir string, args ...string) string {
|
||||
cmd := exec.Command("git", args...)
|
||||
cmd := hideCmd(exec.Command("git", args...)) // pas de flash console (appelé à chaque refresh de l'UI)
|
||||
cmd.Dir = dir
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
@@ -459,7 +459,7 @@ func runStep(name, dir, bin string, args ...string) error {
|
||||
// pairs in `extraEnv`, which override existing ones).
|
||||
func runStepEnv(name, dir, extraEnv, bin string, args ...string) error {
|
||||
fmt.Printf("\n%s %s %s\n", cyan("▶"), name, dim(strings.Join(args, " ")))
|
||||
cmd := exec.Command(bin, args...)
|
||||
cmd := hideCmd(exec.Command(bin, args...))
|
||||
cmd.Dir = dir
|
||||
// Tee vers le sink de build (jobs web) en plus du terminal.
|
||||
var out io.Writer = os.Stdout
|
||||
@@ -495,7 +495,7 @@ func runStepEnv(name, dir, extraEnv, bin string, args ...string) error {
|
||||
// failure the tail of the log is printed so the actual error is never lost.
|
||||
func runBuildStep(name, dir, extraEnv, bin, logPath string, args ...string) error {
|
||||
fmt.Printf("\n%s %s\n", cyan("▶"), name)
|
||||
cmd := exec.Command(bin, args...)
|
||||
cmd := hideCmd(exec.Command(bin, args...))
|
||||
cmd.Dir = dir
|
||||
if extraEnv != "" {
|
||||
env := os.Environ()
|
||||
|
||||
@@ -119,7 +119,7 @@ func mcpConnect(ctx context.Context, name string, cfg MCPServerConfig) (*mcpsdk.
|
||||
var transport mcpsdk.Transport
|
||||
switch cfg.Transport() {
|
||||
case "stdio":
|
||||
cmd := exec.Command(cfg.Command, cfg.Args...)
|
||||
cmd := hideCmd(exec.Command(cfg.Command, cfg.Args...)) // pas de flash console (mode app Windows)
|
||||
// Hérite de l'environnement du service + surcharges déclarées.
|
||||
cmd.Env = os.Environ()
|
||||
for k, v := range cfg.Env {
|
||||
|
||||
@@ -75,6 +75,14 @@ func saveLinkToken(tok string) error {
|
||||
return os.WriteFile(linkTokenPath(), []byte(tok+"\n"), 0o600)
|
||||
}
|
||||
|
||||
// removeLinkToken oublie la clé de liaison enregistrée (idempotent).
|
||||
func removeLinkToken() error {
|
||||
if err := os.Remove(linkTokenPath()); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// relayURL resolves the relay WebSocket endpoint (env override → default).
|
||||
func relayURL() string {
|
||||
if u := strings.TrimSpace(os.Getenv("JEAN_LINK_URL")); u != "" {
|
||||
|
||||
+13
-6
@@ -10,10 +10,16 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const Version = "0.5.0"
|
||||
const Version = "0.5.1"
|
||||
|
||||
// Main est le vrai main() du binaire (cmd/jean ne fait que l'appeler).
|
||||
func Main() {
|
||||
// Rattache la console du terminal parent si on est lancé depuis un shell
|
||||
// (Windows : binaire GUI). Retourne false au double-clic (aucune console) →
|
||||
// on bascule alors sur l'expérience « application ». Hors Windows : toujours
|
||||
// true. À faire AVANT toute écriture.
|
||||
haveConsole := setupConsole()
|
||||
|
||||
// Migration one-shot des anciens skills (SKILLS/<nom>/SKILL.md) vers la
|
||||
// nouvelle mémoire (MEMORY/<nom>.md). Idempotente, silencieuse si rien à faire.
|
||||
migrateSkillsToMemory()
|
||||
@@ -27,12 +33,13 @@ func Main() {
|
||||
cmd = args[0]
|
||||
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,
|
||||
// Double-clic sur le binaire (aucun argument, aucune console rattachée) → on
|
||||
// lance l'expérience « application » (UI web + navigateur + icône tray) plutôt
|
||||
// que d'afficher l'aide. Le binaire étant compilé en sous-système GUI, il n'y a
|
||||
// AUCUNE console à ce stade (donc plus de fenêtre noire). 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
|
||||
if noArgs && !haveConsole {
|
||||
mustExit(cmdApp(args))
|
||||
return
|
||||
}
|
||||
switch cmd {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
//go:build !windows
|
||||
|
||||
package jean
|
||||
|
||||
// setupConsole : hors Windows, le process a toujours une vraie console/tty
|
||||
// standard (stdout hérité). Rien à faire → on signale simplement « on a une
|
||||
// console » pour que le double-clic Windows soit le seul cas « mode application ».
|
||||
func setupConsole() bool { return true }
|
||||
@@ -0,0 +1,60 @@
|
||||
//go:build windows
|
||||
|
||||
package jean
|
||||
|
||||
// sys_console_windows.go — gestion de la console sous Windows.
|
||||
//
|
||||
// Le binaire est compilé en sous-système GUI (`-H=windowsgui`) : au double-clic,
|
||||
// Windows n'alloue AUCUNE console → plus jamais de « fenêtre noire de terminal »
|
||||
// qui s'ouvre et se ferme. En contrepartie, un binaire GUI lancé depuis un
|
||||
// terminal (cmd/PowerShell) n'écrit nulle part par défaut : on se rattache alors
|
||||
// explicitement à la console du parent et on réouvre stdout/stderr/stdin dessus,
|
||||
// pour que l'usage CLI (`jean web`, `jean status`, …) reste lisible.
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// setupConsole rattache le process à la console de son parent si elle existe.
|
||||
// Retourne true si on dispose d'une console (→ usage CLI), false sinon (double-
|
||||
// clic sans console → expérience « application »).
|
||||
func setupConsole() bool {
|
||||
const attachParentProcess = ^uintptr(0) // (DWORD)-1 = ATTACH_PARENT_PROCESS
|
||||
k := syscall.NewLazyDLL("kernel32.dll")
|
||||
r, _, _ := k.NewProc("AttachConsole").Call(attachParentProcess)
|
||||
if r == 0 {
|
||||
return false // aucune console parente = lancé par double-clic (mode app)
|
||||
}
|
||||
// Réouvre les flux standard sur la console fraîchement rattachée : sans ça,
|
||||
// os.Stdout/Stderr pointent sur des handles invalides (sous-système GUI).
|
||||
if con, err := os.OpenFile("CONOUT$", os.O_WRONLY, 0); err == nil {
|
||||
os.Stdout = con
|
||||
os.Stderr = con
|
||||
}
|
||||
if cin, err := os.OpenFile("CONIN$", os.O_RDONLY, 0); err == nil {
|
||||
os.Stdin = cin
|
||||
}
|
||||
configureConsole()
|
||||
return true
|
||||
}
|
||||
|
||||
// configureConsole passe la console en UTF-8 + traitement des séquences ANSI
|
||||
// (couleurs/curseur), comme le faisait l'init() au chargement — mais APRÈS le
|
||||
// rattachement, sinon ça ciblait une console encore inexistante.
|
||||
func configureConsole() {
|
||||
const (
|
||||
cpUTF8 = 65001
|
||||
enableVirtualTerminalProcessing = 0x0004
|
||||
stdOutputHandle = ^uintptr(10) // -11
|
||||
)
|
||||
k := syscall.NewLazyDLL("kernel32.dll")
|
||||
_, _, _ = k.NewProc("SetConsoleOutputCP").Call(uintptr(cpUTF8))
|
||||
_, _, _ = k.NewProc("SetConsoleCP").Call(uintptr(cpUTF8))
|
||||
h, _, _ := k.NewProc("GetStdHandle").Call(stdOutputHandle)
|
||||
var mode uint32
|
||||
if r, _, _ := k.NewProc("GetConsoleMode").Call(h, uintptr(unsafe.Pointer(&mode))); r != 0 {
|
||||
_, _, _ = k.NewProc("SetConsoleMode").Call(h, uintptr(mode|enableVirtualTerminalProcessing))
|
||||
}
|
||||
}
|
||||
@@ -150,7 +150,7 @@ if ($parts -contains $d) { Write-Output 'present'; exit 0 }
|
||||
$new=(@($parts) + $d) -join ';'
|
||||
[Environment]::SetEnvironmentVariable('Path',$new,'User')
|
||||
Write-Output 'added'`, psQuote(dir))
|
||||
cmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", ps)
|
||||
cmd := hideCmd(exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", ps))
|
||||
outBytes, err := cmd.CombinedOutput()
|
||||
out := strings.TrimSpace(string(outBytes))
|
||||
if err != nil {
|
||||
@@ -169,7 +169,7 @@ $parts=$p.Split(';') | Where-Object { $_ -ne '' -and $_ -ne $d }
|
||||
if (($p.Split(';') | Where-Object { $_ -eq $d }).Count -eq 0) { Write-Output 'absent'; exit 0 }
|
||||
[Environment]::SetEnvironmentVariable('Path',($parts -join ';'),'User')
|
||||
Write-Output 'removed'`, psQuote(dir))
|
||||
cmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", ps)
|
||||
cmd := hideCmd(exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", ps))
|
||||
outBytes, err := cmd.CombinedOutput()
|
||||
out := strings.TrimSpace(string(outBytes))
|
||||
if err != nil {
|
||||
|
||||
@@ -65,11 +65,6 @@ func totalRAMGB() float64 {
|
||||
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
|
||||
// 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
|
||||
|
||||
@@ -97,19 +97,6 @@ func totalRAMGB() float64 {
|
||||
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
|
||||
// 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
|
||||
@@ -139,7 +126,7 @@ func setLibraryPath(dir string) {
|
||||
// parent (this is the detached process the service supervisor tracks).
|
||||
// args[0] is the binary path; the rest are its arguments.
|
||||
func execServer(bin string, args []string) error {
|
||||
cmd := exec.Command(bin, args[1:]...)
|
||||
cmd := hideCmd(exec.Command(bin, args[1:]...)) // pas de console pour llama-server (mode app)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
@@ -165,9 +152,9 @@ func autoInstallTool(name string) error {
|
||||
if !ok {
|
||||
id = name
|
||||
}
|
||||
cmd := exec.Command("winget", "install", "--id", id, "-e",
|
||||
cmd := hideCmd(exec.Command("winget", "install", "--id", id, "-e",
|
||||
"--accept-source-agreements", "--accept-package-agreements",
|
||||
"--disable-interactivity", "--silent")
|
||||
"--disable-interactivity", "--silent"))
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
@@ -180,7 +167,7 @@ func refreshToolPath() {
|
||||
ps := `$m=[Environment]::GetEnvironmentVariable('Path','Machine')
|
||||
$u=[Environment]::GetEnvironmentVariable('Path','User')
|
||||
Write-Output ((@($m,$u) | Where-Object { $_ }) -join ';')`
|
||||
out, err := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", ps).Output()
|
||||
out, err := hideCmd(exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", ps)).Output()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -206,9 +193,9 @@ func msvcInstallVersion() string {
|
||||
if _, err := os.Stat(vs); err != nil {
|
||||
return ""
|
||||
}
|
||||
out, err := exec.Command(vs, "-latest", "-products", "*",
|
||||
out, err := hideCmd(exec.Command(vs, "-latest", "-products", "*",
|
||||
"-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
|
||||
"-property", "installationVersion").Output()
|
||||
"-property", "installationVersion")).Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
@@ -247,10 +234,10 @@ func ensureCompiler() error {
|
||||
return fmt.Errorf("compilateur C++ absent et winget introuvable — installe « Visual Studio Build Tools » (charge de travail C++) manuellement")
|
||||
}
|
||||
fmt.Printf("%s compilateur C++ absent — installation des Build Tools MSVC (gros téléchargement, une seule fois)…\n", yellow("[info]"))
|
||||
cmd := exec.Command("winget", "install", "--id", "Microsoft.VisualStudio.2022.BuildTools", "-e",
|
||||
cmd := hideCmd(exec.Command("winget", "install", "--id", "Microsoft.VisualStudio.2022.BuildTools", "-e",
|
||||
"--accept-source-agreements", "--accept-package-agreements",
|
||||
"--disable-interactivity",
|
||||
"--override", "--quiet --wait --norestart --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended")
|
||||
"--override", "--quiet --wait --norestart --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"))
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
@@ -280,9 +267,9 @@ func ensureAccelerator() {
|
||||
return
|
||||
}
|
||||
fmt.Printf("%s GPU NVIDIA détecté — installation du CUDA Toolkit pour l'accélération GPU (gros téléchargement, une seule fois)…\n", yellow("[info]"))
|
||||
cmd := exec.Command("winget", "install", "--id", "Nvidia.CUDA", "-e",
|
||||
cmd := hideCmd(exec.Command("winget", "install", "--id", "Nvidia.CUDA", "-e",
|
||||
"--accept-source-agreements", "--accept-package-agreements",
|
||||
"--disable-interactivity")
|
||||
"--disable-interactivity"))
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
@@ -312,9 +299,9 @@ func ensureCudaVSIntegration(toolkitDir string) error {
|
||||
if _, err := os.Stat(vs); err != nil {
|
||||
return nil
|
||||
}
|
||||
out, err := exec.Command(vs, "-latest", "-products", "*",
|
||||
out, err := hideCmd(exec.Command(vs, "-latest", "-products", "*",
|
||||
"-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
|
||||
"-property", "installationPath").Output()
|
||||
"-property", "installationPath")).Output()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
//go:build !windows
|
||||
|
||||
package jean
|
||||
|
||||
// sys_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) }
|
||||
@@ -1,39 +0,0 @@
|
||||
//go:build windows
|
||||
|
||||
package jean
|
||||
|
||||
// sys_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
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
const (
|
||||
createNewProcessGroup = 0x00000200 // CREATE_NEW_PROCESS_GROUP
|
||||
detachedProcess = 0x00000008 // DETACHED_PROCESS
|
||||
createNoWindow = 0x08000000 // CREATE_NO_WINDOW (aucune console pour l'enfant)
|
||||
)
|
||||
|
||||
func pidFilePath() string { return filepath.Join(JeanHome(), serviceName()+".pid") }
|
||||
@@ -70,7 +71,12 @@ func svcStart() error {
|
||||
cmd := exec.Command(self, "serve")
|
||||
cmd.Stdout = logf
|
||||
cmd.Stderr = logf
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: createNewProcessGroup | detachedProcess}
|
||||
// createNoWindow + HideWindow : le service enfant (`jean serve`) ne doit JAMAIS
|
||||
// faire clignoter de console noire quand Jean est lancé en mode app (double-clic).
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
HideWindow: true,
|
||||
CreationFlags: createNewProcessGroup | detachedProcess | createNoWindow,
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("démarrage de 'jean serve': %w", err)
|
||||
}
|
||||
|
||||
+115
-2
@@ -484,6 +484,30 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
<button onclick="apiKeyAction('clear')">retirer</button>
|
||||
</div>
|
||||
</details>
|
||||
<details id="remote-details"><summary>Accès distant <span id="remote-badge" style="margin-left:auto;margin-right:12px;font-size:10px"></span></summary>
|
||||
<div class="subhead">ajean.link<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Accédez à ce serveur Jean depuis <b>n'importe où</b>, sans ouvrir de port, à travers le relais <b>ajean.link</b>. Le trafic est chiffré de <b>bout en bout</b> : le relais ne voit rien. Abonnement 4,80 €/mois.</span></span></div>
|
||||
<!-- Non connecté : un seul bouton, tout se passe dans la popup (compte + paiement). -->
|
||||
<div id="remote-off">
|
||||
<p class="muted" style="font-size:12px;line-height:1.5;margin:6px 0 10px">Ouvrez l'accès distant à ce serveur en quelques clics — compte, abonnement et connexion, sans terminal.</p>
|
||||
<button onclick="remoteConnect()" style="border-color:var(--accent);color:var(--accent)">🔗 Connecter à ajean.link</button>
|
||||
</div>
|
||||
<!-- Connecté : URL publique + appairage + déconnexion. -->
|
||||
<div id="remote-on" style="display:none">
|
||||
<div class="muted" style="font-size:11px;margin-bottom:2px">🌍 votre adresse d'accès distant</div>
|
||||
<div class="row">
|
||||
<input id="remote-url" readonly style="flex:1;min-width:0;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px;font:inherit;font-size:12px">
|
||||
<button onclick="copyText(document.getElementById('remote-url').value,'adresse copiée')" title="copier">⧉</button>
|
||||
<button onclick="window.open(document.getElementById('remote-url').value,'_blank')" title="ouvrir">↗</button>
|
||||
</div>
|
||||
<div id="remote-status" class="muted" style="font-size:11px;margin-top:6px"></div>
|
||||
<div class="subhead" style="margin-top:12px">Première connexion<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">À votre première ouverture du portail distant, confirmez l'<b>empreinte</b> et saisissez le <b>code d'appairage</b> : cela verrouille la boîte noire sur CE serveur (anti-interception). Une seule fois par appareil.</span></span></div>
|
||||
<button onclick="remotePairCode()" style="margin-top:4px">🔑 Afficher l'empreinte + le code d'appairage</button>
|
||||
<div id="remote-pair" class="muted" style="font-size:12px;margin-top:8px;display:none;line-height:1.7"></div>
|
||||
<div class="row" style="margin-top:12px">
|
||||
<button onclick="remoteDisconnect()" style="border-color:var(--err);color:var(--err)">déconnecter l'accès distant</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<details id="lc-details"><summary>MOTEUR</summary>
|
||||
<div id="lc-status" class="lc-head muted">…</div>
|
||||
<div class="lc-modes">
|
||||
@@ -1155,7 +1179,19 @@ async function apiKeyAction(action){
|
||||
renderApiKey(await jpost('/api/apikey', {action}));
|
||||
}
|
||||
async function toggleOAIPublic(){
|
||||
const on = document.getElementById('oai-public-toggle').checked;
|
||||
const cb = document.getElementById('oai-public-toggle');
|
||||
const on = cb.checked;
|
||||
// L'accès OpenAI public passe par ajean.link (<machine>.oai.ajean.link) : il exige
|
||||
// que l'accès distant soit activé sur ce serveur. Sinon, on annule et on explique.
|
||||
if(on){
|
||||
let linked = false;
|
||||
try{ const s = await jget('/api/link/status'); linked = !!(s && s.linked); }catch(e){}
|
||||
if(!linked){
|
||||
cb.checked = false;
|
||||
await askAlert('Vous devez activer l\'accès distant (ajean.link) pour bénéficier de cette fonctionnalité. Ouvrez le panneau « Accès distant » pour connecter ce serveur.', {title:'Accès distant requis'});
|
||||
return;
|
||||
}
|
||||
}
|
||||
await jpost('/api/oai/public', {enabled:on});
|
||||
toast(on ? 'accès public activé' : 'accès public coupé');
|
||||
loadApiKey();
|
||||
@@ -1176,7 +1212,7 @@ async function saveCrawlUrl(){
|
||||
const url=document.getElementById('crawl-url').value.trim();
|
||||
renderInternet(await jpost('/api/internet',{url}));
|
||||
}
|
||||
async function loadAll(){ await Promise.all([loadStatus(),loadVram(),loadRam(),loadCfg(),loadPresets(),loadAgent(),loadInternet(),loadMCP(),loadApiKey(),loadPrefs(),loadLlamacpp()]); }
|
||||
async function loadAll(){ await Promise.all([loadStatus(),loadVram(),loadRam(),loadCfg(),loadPresets(),loadAgent(),loadInternet(),loadMCP(),loadApiKey(),loadPrefs(),loadLlamacpp(),loadRemote()]); }
|
||||
async function act(a){ toast(a+'…'); await jpost('/api/'+a); setTimeout(loadAll,1500); }
|
||||
function openBenchModal(){ document.getElementById('bench-modal').style.display = 'flex'; }
|
||||
function closeBenchModal(){ document.getElementById('bench-modal').style.display = 'none'; }
|
||||
@@ -2167,4 +2203,81 @@ async function deleteMcp(){
|
||||
loadMCP();
|
||||
toast('serveur supprimé');
|
||||
}
|
||||
// Accès distant (ajean.link) — piloté depuis l'UI, sans terminal.
|
||||
// « Connecter » ouvre une popup app.ajean.link/connect.html qui gère compte +
|
||||
// abonnement, puis renvoie une clé de liaison par postMessage. On la POSTe à
|
||||
// l'agent local (/api/link/connect) qui fait le `jean link` (token + service).
|
||||
|
||||
const AJEAN_APP_ORIGIN = 'https://app.ajean.link';
|
||||
|
||||
function renderRemote(d){
|
||||
const off = document.getElementById('remote-off'), on = document.getElementById('remote-on');
|
||||
const badge = document.getElementById('remote-badge');
|
||||
if(!d || !d.linked){
|
||||
off.style.display=''; on.style.display='none';
|
||||
if(badge){ badge.textContent=''; }
|
||||
return;
|
||||
}
|
||||
off.style.display='none'; on.style.display='';
|
||||
document.getElementById('remote-url').value = d.machineURL || '';
|
||||
const st = document.getElementById('remote-status');
|
||||
if(d.active){ st.textContent='● en ligne — accessible à distance'; st.style.color='var(--accent)'; }
|
||||
else { st.textContent='○ service arrêté (l\'accès distant ne répondra pas)'; st.style.color='var(--warn)'; }
|
||||
if(badge){ badge.textContent = d.active ? '● connecté' : '○ arrêté'; badge.style.color = d.active?'var(--accent)':'var(--warn)'; }
|
||||
}
|
||||
|
||||
async function loadRemote(){
|
||||
// Le panneau ne vaut qu'en LOCAL : il pilote /api/link/* que le tunnel bloque
|
||||
// (boîte noire). Sur le portail distant (app.ajean.link/server.html), on le cache.
|
||||
const det = document.getElementById('remote-details');
|
||||
if(location.hostname === 'app.ajean.link'){ if(det) det.style.display='none'; return; }
|
||||
try{ renderRemote(await jget('/api/link/status')); }catch(e){}
|
||||
}
|
||||
|
||||
// Ouvre la popup de connexion et attend la clé renvoyée par postMessage.
|
||||
function remoteConnect(){
|
||||
const params = new URLSearchParams({ origin: window.location.origin, host: window.location.hostname || 'ce serveur' });
|
||||
const url = AJEAN_APP_ORIGIN + '/connect.html?' + params.toString();
|
||||
const pop = window.open(url, 'ajean-connect', 'width=440,height=640');
|
||||
if(!pop){ toast('autorisez les popups pour connecter ajean.link'); return; }
|
||||
|
||||
async function onMsg(ev){
|
||||
// Anti-usurpation : n'accepte QUE des messages du portail ajean.link.
|
||||
if(ev.origin !== AJEAN_APP_ORIGIN) return;
|
||||
const d = ev.data || {};
|
||||
if(d.type !== 'ajean-link' || !d.token) return;
|
||||
window.removeEventListener('message', onMsg);
|
||||
try{
|
||||
const r = await jpost('/api/link/connect', { token: d.token });
|
||||
if(r && r.linked){
|
||||
toast('✓ accès distant connecté');
|
||||
if(r.serviceErr){ toast('token enregistré (service : '+r.serviceErr+')'); }
|
||||
loadRemote();
|
||||
} else {
|
||||
toast((r && r.error) || 'échec de la connexion');
|
||||
}
|
||||
}catch(e){ toast('erreur : '+e); }
|
||||
}
|
||||
window.addEventListener('message', onMsg);
|
||||
}
|
||||
|
||||
async function remoteDisconnect(){
|
||||
const ok = await askConfirm('Couper l\'accès distant et oublier la clé de liaison de ce serveur ?', {title:'Accès distant', okLabel:'Déconnecter'});
|
||||
if(!ok) return;
|
||||
try{ await jpost('/api/link/disconnect', {}); toast('accès distant coupé'); loadRemote(); }
|
||||
catch(e){ toast('erreur : '+e); }
|
||||
}
|
||||
|
||||
async function remotePairCode(){
|
||||
const box = document.getElementById('remote-pair');
|
||||
box.style.display=''; box.textContent='génération du code…';
|
||||
try{
|
||||
const r = await jpost('/api/link/paircode', {});
|
||||
if(r && r.code){
|
||||
box.innerHTML = 'Empreinte : <b>'+(r.fingerprint||'—')+'</b><br>Code d\'appairage (valable 10 min, usage unique) : <b>'+r.code+'</b>';
|
||||
} else {
|
||||
box.textContent = (r && r.error) || 'code indisponible';
|
||||
}
|
||||
}catch(e){ box.textContent='erreur : '+e; }
|
||||
}
|
||||
</script></body></html>
|
||||
@@ -124,6 +124,30 @@
|
||||
<button onclick="apiKeyAction('clear')">retirer</button>
|
||||
</div>
|
||||
</details>
|
||||
<details id="remote-details"><summary>Accès distant <span id="remote-badge" style="margin-left:auto;margin-right:12px;font-size:10px"></span></summary>
|
||||
<div class="subhead">ajean.link<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Accédez à ce serveur Jean depuis <b>n'importe où</b>, sans ouvrir de port, à travers le relais <b>ajean.link</b>. Le trafic est chiffré de <b>bout en bout</b> : le relais ne voit rien. Abonnement 4,80 €/mois.</span></span></div>
|
||||
<!-- Non connecté : un seul bouton, tout se passe dans la popup (compte + paiement). -->
|
||||
<div id="remote-off">
|
||||
<p class="muted" style="font-size:12px;line-height:1.5;margin:6px 0 10px">Ouvrez l'accès distant à ce serveur en quelques clics — compte, abonnement et connexion, sans terminal.</p>
|
||||
<button onclick="remoteConnect()" style="border-color:var(--accent);color:var(--accent)">🔗 Connecter à ajean.link</button>
|
||||
</div>
|
||||
<!-- Connecté : URL publique + appairage + déconnexion. -->
|
||||
<div id="remote-on" style="display:none">
|
||||
<div class="muted" style="font-size:11px;margin-bottom:2px">🌍 votre adresse d'accès distant</div>
|
||||
<div class="row">
|
||||
<input id="remote-url" readonly style="flex:1;min-width:0;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px;font:inherit;font-size:12px">
|
||||
<button onclick="copyText(document.getElementById('remote-url').value,'adresse copiée')" title="copier">⧉</button>
|
||||
<button onclick="window.open(document.getElementById('remote-url').value,'_blank')" title="ouvrir">↗</button>
|
||||
</div>
|
||||
<div id="remote-status" class="muted" style="font-size:11px;margin-top:6px"></div>
|
||||
<div class="subhead" style="margin-top:12px">Première connexion<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">À votre première ouverture du portail distant, confirmez l'<b>empreinte</b> et saisissez le <b>code d'appairage</b> : cela verrouille la boîte noire sur CE serveur (anti-interception). Une seule fois par appareil.</span></span></div>
|
||||
<button onclick="remotePairCode()" style="margin-top:4px">🔑 Afficher l'empreinte + le code d'appairage</button>
|
||||
<div id="remote-pair" class="muted" style="font-size:12px;margin-top:8px;display:none;line-height:1.7"></div>
|
||||
<div class="row" style="margin-top:12px">
|
||||
<button onclick="remoteDisconnect()" style="border-color:var(--err);color:var(--err)">déconnecter l'accès distant</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<details id="lc-details"><summary>MOTEUR</summary>
|
||||
<div id="lc-status" class="lc-head muted">…</div>
|
||||
<div class="lc-modes">
|
||||
|
||||
@@ -195,7 +195,19 @@ async function apiKeyAction(action){
|
||||
renderApiKey(await jpost('/api/apikey', {action}));
|
||||
}
|
||||
async function toggleOAIPublic(){
|
||||
const on = document.getElementById('oai-public-toggle').checked;
|
||||
const cb = document.getElementById('oai-public-toggle');
|
||||
const on = cb.checked;
|
||||
// L'accès OpenAI public passe par ajean.link (<machine>.oai.ajean.link) : il exige
|
||||
// que l'accès distant soit activé sur ce serveur. Sinon, on annule et on explique.
|
||||
if(on){
|
||||
let linked = false;
|
||||
try{ const s = await jget('/api/link/status'); linked = !!(s && s.linked); }catch(e){}
|
||||
if(!linked){
|
||||
cb.checked = false;
|
||||
await askAlert('Vous devez activer l\'accès distant (ajean.link) pour bénéficier de cette fonctionnalité. Ouvrez le panneau « Accès distant » pour connecter ce serveur.', {title:'Accès distant requis'});
|
||||
return;
|
||||
}
|
||||
}
|
||||
await jpost('/api/oai/public', {enabled:on});
|
||||
toast(on ? 'accès public activé' : 'accès public coupé');
|
||||
loadApiKey();
|
||||
@@ -216,5 +228,5 @@ async function saveCrawlUrl(){
|
||||
const url=document.getElementById('crawl-url').value.trim();
|
||||
renderInternet(await jpost('/api/internet',{url}));
|
||||
}
|
||||
async function loadAll(){ await Promise.all([loadStatus(),loadVram(),loadRam(),loadCfg(),loadPresets(),loadAgent(),loadInternet(),loadMCP(),loadApiKey(),loadPrefs(),loadLlamacpp()]); }
|
||||
async function loadAll(){ await Promise.all([loadStatus(),loadVram(),loadRam(),loadCfg(),loadPresets(),loadAgent(),loadInternet(),loadMCP(),loadApiKey(),loadPrefs(),loadLlamacpp(),loadRemote()]); }
|
||||
async function act(a){ toast(a+'…'); await jpost('/api/'+a); setTimeout(loadAll,1500); }
|
||||
@@ -0,0 +1,77 @@
|
||||
// Accès distant (ajean.link) — piloté depuis l'UI, sans terminal.
|
||||
// « Connecter » ouvre une popup app.ajean.link/connect.html qui gère compte +
|
||||
// abonnement, puis renvoie une clé de liaison par postMessage. On la POSTe à
|
||||
// l'agent local (/api/link/connect) qui fait le `jean link` (token + service).
|
||||
|
||||
const AJEAN_APP_ORIGIN = 'https://app.ajean.link';
|
||||
|
||||
function renderRemote(d){
|
||||
const off = document.getElementById('remote-off'), on = document.getElementById('remote-on');
|
||||
const badge = document.getElementById('remote-badge');
|
||||
if(!d || !d.linked){
|
||||
off.style.display=''; on.style.display='none';
|
||||
if(badge){ badge.textContent=''; }
|
||||
return;
|
||||
}
|
||||
off.style.display='none'; on.style.display='';
|
||||
document.getElementById('remote-url').value = d.machineURL || '';
|
||||
const st = document.getElementById('remote-status');
|
||||
if(d.active){ st.textContent='● en ligne — accessible à distance'; st.style.color='var(--accent)'; }
|
||||
else { st.textContent='○ service arrêté (l\'accès distant ne répondra pas)'; st.style.color='var(--warn)'; }
|
||||
if(badge){ badge.textContent = d.active ? '● connecté' : '○ arrêté'; badge.style.color = d.active?'var(--accent)':'var(--warn)'; }
|
||||
}
|
||||
|
||||
async function loadRemote(){
|
||||
// Le panneau ne vaut qu'en LOCAL : il pilote /api/link/* que le tunnel bloque
|
||||
// (boîte noire). Sur le portail distant (app.ajean.link/server.html), on le cache.
|
||||
const det = document.getElementById('remote-details');
|
||||
if(location.hostname === 'app.ajean.link'){ if(det) det.style.display='none'; return; }
|
||||
try{ renderRemote(await jget('/api/link/status')); }catch(e){}
|
||||
}
|
||||
|
||||
// Ouvre la popup de connexion et attend la clé renvoyée par postMessage.
|
||||
function remoteConnect(){
|
||||
const params = new URLSearchParams({ origin: window.location.origin, host: window.location.hostname || 'ce serveur' });
|
||||
const url = AJEAN_APP_ORIGIN + '/connect.html?' + params.toString();
|
||||
const pop = window.open(url, 'ajean-connect', 'width=440,height=640');
|
||||
if(!pop){ toast('autorisez les popups pour connecter ajean.link'); return; }
|
||||
|
||||
async function onMsg(ev){
|
||||
// Anti-usurpation : n'accepte QUE des messages du portail ajean.link.
|
||||
if(ev.origin !== AJEAN_APP_ORIGIN) return;
|
||||
const d = ev.data || {};
|
||||
if(d.type !== 'ajean-link' || !d.token) return;
|
||||
window.removeEventListener('message', onMsg);
|
||||
try{
|
||||
const r = await jpost('/api/link/connect', { token: d.token });
|
||||
if(r && r.linked){
|
||||
toast('✓ accès distant connecté');
|
||||
if(r.serviceErr){ toast('token enregistré (service : '+r.serviceErr+')'); }
|
||||
loadRemote();
|
||||
} else {
|
||||
toast((r && r.error) || 'échec de la connexion');
|
||||
}
|
||||
}catch(e){ toast('erreur : '+e); }
|
||||
}
|
||||
window.addEventListener('message', onMsg);
|
||||
}
|
||||
|
||||
async function remoteDisconnect(){
|
||||
const ok = await askConfirm('Couper l\'accès distant et oublier la clé de liaison de ce serveur ?', {title:'Accès distant', okLabel:'Déconnecter'});
|
||||
if(!ok) return;
|
||||
try{ await jpost('/api/link/disconnect', {}); toast('accès distant coupé'); loadRemote(); }
|
||||
catch(e){ toast('erreur : '+e); }
|
||||
}
|
||||
|
||||
async function remotePairCode(){
|
||||
const box = document.getElementById('remote-pair');
|
||||
box.style.display=''; box.textContent='génération du code…';
|
||||
try{
|
||||
const r = await jpost('/api/link/paircode', {});
|
||||
if(r && r.code){
|
||||
box.innerHTML = 'Empreinte : <b>'+(r.fingerprint||'—')+'</b><br>Code d\'appairage (valable 10 min, usage unique) : <b>'+r.code+'</b>';
|
||||
} else {
|
||||
box.textContent = (r && r.error) || 'code indisponible';
|
||||
}
|
||||
}catch(e){ box.textContent='erreur : '+e; }
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package jean
|
||||
|
||||
// web_link_api.go — API locale pilotant l'accès distant (ajean.link) depuis l'UI
|
||||
// web de Jean, pour éviter le terminal. Le panneau « Accès distant » ouvre une
|
||||
// popup app.ajean.link/connect.html qui gère compte + paiement puis renvoie une
|
||||
// clé de liaison (jl_…) ; l'UI la POSTe ici, et Jean fait le `jean link` tout
|
||||
// seul (écrit le token + (re)démarre le service).
|
||||
//
|
||||
// Ces routes ne sont accessibles qu'en LOCAL : à travers le tunnel du relais,
|
||||
// newLinkHandler refuse tout /api/* non chiffré (boîte noire). La configuration
|
||||
// de l'accès distant se fait donc depuis la machine elle-même, ce qui est correct.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// remoteServerURL est l'URL du portail distant pointant droit sur cette machine.
|
||||
func remoteServerURL() string {
|
||||
return "https://app.ajean.link/server.html?m=" + machineID()
|
||||
}
|
||||
|
||||
// handleLinkStatus (GET /api/link/status) : état de l'accès distant pour l'UI.
|
||||
func handleLinkStatus(w http.ResponseWriter, r *http.Request) {
|
||||
tok := readLinkToken()
|
||||
sendJSON(w, http.StatusOK, map[string]any{
|
||||
"linked": tok != "",
|
||||
"active": linkServiceActive(),
|
||||
"machineURL": remoteServerURL(),
|
||||
"fingerprint": e2eFingerprint(),
|
||||
})
|
||||
}
|
||||
|
||||
// handleLinkConnect (POST /api/link/connect {token}) : enregistre la clé de
|
||||
// liaison remise par la popup connect.html et (re)démarre le service de lien.
|
||||
func handleLinkConnect(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct{ Token string }
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
tok := strings.TrimSpace(req.Token)
|
||||
if !strings.HasPrefix(tok, "jl_") {
|
||||
sendJSON(w, http.StatusBadRequest, map[string]any{"error": "clé de liaison invalide"})
|
||||
return
|
||||
}
|
||||
if err := saveLinkToken(tok); err != nil {
|
||||
sendJSON(w, http.StatusInternalServerError, map[string]any{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
// (re)démarre le worker pour qu'il prenne la nouvelle clé. Sur une machine sans
|
||||
// systemd (dev/Windows), le token est quand même enregistré : on renvoie l'info
|
||||
// mais on signale que le service n'a pas pu démarrer ici.
|
||||
svcErr := ""
|
||||
if err := startLink(true); err != nil {
|
||||
svcErr = err.Error()
|
||||
}
|
||||
sendJSON(w, http.StatusOK, map[string]any{
|
||||
"ok": true,
|
||||
"linked": true,
|
||||
"active": linkServiceActive(),
|
||||
"machineURL": remoteServerURL(),
|
||||
"serviceErr": svcErr,
|
||||
})
|
||||
}
|
||||
|
||||
// handleLinkDisconnect (POST /api/link/disconnect) : arrête le service et oublie
|
||||
// la clé (équivalent `jean link stop` + `jean link logout`).
|
||||
func handleLinkDisconnect(w http.ResponseWriter, r *http.Request) {
|
||||
_ = linkServiceCtl("stop")
|
||||
_ = removeLinkToken()
|
||||
sendJSON(w, http.StatusOK, map[string]any{"ok": true, "linked": false, "active": false})
|
||||
}
|
||||
|
||||
// handleLinkPairCode (POST /api/link/paircode) : génère un code d'appairage frais
|
||||
// (usage unique, TTL 10 min) + l'empreinte E2E, à saisir UNE fois dans le portail
|
||||
// distant lors de la première connexion (confirmation de la boîte noire). Évite le
|
||||
// détour par `jean link code` en terminal.
|
||||
func handleLinkPairCode(w http.ResponseWriter, r *http.Request) {
|
||||
code, err := newPairCode()
|
||||
if err != nil {
|
||||
sendJSON(w, http.StatusInternalServerError, map[string]any{"error": fmt.Sprintf("génération du code (droits ?): %v", err)})
|
||||
return
|
||||
}
|
||||
sendJSON(w, http.StatusOK, map[string]any{"code": code, "fingerprint": e2eFingerprint()})
|
||||
}
|
||||
@@ -107,6 +107,10 @@ func newWebMux() *http.ServeMux {
|
||||
api("/api/agent/compact", handleCompactToggle)
|
||||
api("/api/apikey", handleAPIKey)
|
||||
api("/api/oai/public", handleOAIPublic)
|
||||
api("/api/link/status", handleLinkStatus) // état de l'accès distant (ajean.link)
|
||||
api("/api/link/connect", handleLinkConnect) // clé de liaison remise par connect.html → jean link
|
||||
api("/api/link/disconnect", handleLinkDisconnect) // arrête le lien + oublie la clé
|
||||
api("/api/link/paircode", handleLinkPairCode) // code d'appairage + empreinte pour la 1re connexion
|
||||
api("/api/internet", handleInternet)
|
||||
api("/api/mcp", handleMCP)
|
||||
api("/api/mcp/save", handleMCPSave)
|
||||
@@ -187,7 +191,7 @@ func killPid(pid int, force bool) {
|
||||
if force {
|
||||
args = append(args, "/F")
|
||||
}
|
||||
_ = exec.Command("taskkill", args...).Run()
|
||||
_ = hideCmd(exec.Command("taskkill", args...)).Run()
|
||||
return
|
||||
}
|
||||
sig := "-TERM"
|
||||
@@ -204,7 +208,7 @@ func killPid(pid int, force bool) {
|
||||
func pidOnPort(port int) (int, string) {
|
||||
if runtime.GOOS == "windows" {
|
||||
// netstat -ano : " TCP 0.0.0.0:8090 0.0.0.0:0 LISTENING 1234"
|
||||
out, err := exec.Command("netstat", "-ano", "-p", "tcp").Output()
|
||||
out, err := hideCmd(exec.Command("netstat", "-ano", "-p", "tcp")).Output()
|
||||
if err != nil {
|
||||
return 0, ""
|
||||
}
|
||||
@@ -240,7 +244,7 @@ func pidOnPort(port int) (int, string) {
|
||||
func processName(pid int) string {
|
||||
if runtime.GOOS == "windows" {
|
||||
// tasklist CSV : "jean.exe","1234","Console","1","12 345 K"
|
||||
out, err := exec.Command("tasklist", "/FI", "PID eq "+strconv.Itoa(pid), "/FO", "CSV", "/NH").Output()
|
||||
out, err := hideCmd(exec.Command("tasklist", "/FI", "PID eq "+strconv.Itoa(pid), "/FO", "CSV", "/NH")).Output()
|
||||
if err == nil {
|
||||
if f := strings.SplitN(strings.TrimSpace(string(out)), "\",\"", 2); len(f) == 2 {
|
||||
return strings.TrimPrefix(f[0], "\"")
|
||||
|
||||
Reference in new issue
Block a user