mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-22 22:48:11 +02:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a6911af27 | ||
|
|
965a4d6247 | ||
|
|
99c96a6c33 |
No files matched your search
@@ -52,6 +52,21 @@ et le **chiffrement de la mémoire** (`mem_crypto.go`, `mem_vault.go`,
|
||||
**pas** reprise : elle vise ajean.link, le service de l'auteur amont. Loki
|
||||
expose à la place un export/import de paquet chiffré en fichier local.
|
||||
|
||||
Le **rendu des formules LaTeX** dans les réponses (`ui/src/js/00-math.js`,
|
||||
`web_katex.go`) est repris d'AJEAN (#123) : $…$, $$…$$, \\(…\\) et \\[…\\] rendus
|
||||
par KaTeX, chargé à la première formule seulement, sans toucher au code ni aux
|
||||
montants en $. Loki y ajoute deux garde-fous de détection (« 5 $, » et un $
|
||||
voisin d'un code en ligne restent du texte) et sert la route `/katex/` aussi
|
||||
depuis la façade Rust (`rust/loki-rs/src/ui.rs`).
|
||||
|
||||
## KaTeX
|
||||
|
||||
**[KaTeX](https://katex.org/)** 0.16.11 (licence MIT, © Khan Academy et
|
||||
contributeurs) est embarqué tel que publié — script, feuille de style et
|
||||
polices woff2 — sous `internal/loki/ui/katex/`, avec sa licence
|
||||
(`internal/loki/ui/katex/LICENSE`). Il est servi par Loki sous `/katex/`, sans
|
||||
appel à un CDN.
|
||||
|
||||
## Moteur Strata
|
||||
|
||||
Le second moteur de Loki (`internal/loki/backend_strata.go`) est
|
||||
|
||||
@@ -37,15 +37,11 @@ var facadeMCPRoutes = map[string]bool{
|
||||
"/api/mcp/toggle": true,
|
||||
"/api/mcp/tool": true,
|
||||
"/api/mcp/test": true,
|
||||
// Les délais globaux : la façade tient le pool, c'est elle qui doit
|
||||
// fermer ses sessions quand ils changent.
|
||||
"/api/mcp/timeouts": true,
|
||||
"/api/mcp/timeouts/save": true,
|
||||
}
|
||||
|
||||
// facadeMCPJSON appelle l'API interne MCP de la façade et décode sa réponse.
|
||||
// Délai large, simple garde-fou : la façade borne elle-même la connexion
|
||||
// (1 h au plus) et l'appel, nouvel essai compris (mcpCallTimeoutMax chacun).
|
||||
// Délai large : un appel d'outil MCP peut durer (mcpCallTimeout), une
|
||||
// première connexion npx/uvx aussi (mcpConnectTimeoutCold).
|
||||
func facadeMCPJSON(path string, body any, out any) error {
|
||||
b, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
@@ -57,7 +53,7 @@ func facadeMCPJSON(path string, body any, out any) error {
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set(internalTokenHeader, internalToken())
|
||||
resp, err := (&http.Client{Timeout: mcpTimeoutMaxSec*time.Second + 2*mcpCallTimeoutMax}).Do(req)
|
||||
resp, err := (&http.Client{Timeout: mcpConnectTimeoutCold + mcpCallTimeout}).Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -74,27 +74,22 @@ func TestMCPCallHonorsTurnCancel(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMCPCallTimeoutFor(t *testing.T) {
|
||||
def := MCPTimeouts{}.normalized()
|
||||
cases := []struct {
|
||||
cfg MCPServerConfig
|
||||
t MCPTimeouts
|
||||
args map[string]any
|
||||
want time.Duration
|
||||
}{
|
||||
{MCPServerConfig{}, def, nil, 120 * time.Second},
|
||||
{MCPServerConfig{CallTimeoutSec: 600}, def, nil, 10 * time.Minute},
|
||||
{MCPServerConfig{}, def, map[string]any{"timeout": 300.0}, 330 * time.Second}, // secondes
|
||||
{MCPServerConfig{}, def, map[string]any{"timeout": 200000.0}, 230 * time.Second}, // millisecondes
|
||||
{MCPServerConfig{}, def, map[string]any{"timeout": 5000.0}, 120 * time.Second}, // 5 s : le défaut reste
|
||||
{MCPServerConfig{}, def, map[string]any{"timeout": "400"}, 430 * time.Second}, // chaîne
|
||||
{MCPServerConfig{}, def, map[string]any{"timeout": 86400.0 * 1000}, mcpCallTimeoutMax}, // plafond
|
||||
{MCPServerConfig{CallTimeoutSec: 30}, def, nil, 30 * time.Second}, // plus court que le défaut
|
||||
{MCPServerConfig{}, MCPTimeouts{Connect: 20, Call: 45}, nil, 45 * time.Second}, // réglage global
|
||||
{MCPServerConfig{CallTimeoutSec: 90}, MCPTimeouts{Connect: 20, Call: 45}, nil, 90 * time.Second},
|
||||
{MCPServerConfig{}, MCPTimeouts{Connect: 20, Call: 3600}, nil, time.Hour}, // pas rogné par le plafond
|
||||
{MCPServerConfig{}, nil, mcpCallTimeout},
|
||||
{MCPServerConfig{CallTimeoutSec: 600}, nil, 10 * time.Minute},
|
||||
{MCPServerConfig{}, map[string]any{"timeout": 300.0}, 330 * time.Second}, // secondes
|
||||
{MCPServerConfig{}, map[string]any{"timeout": 200000.0}, 230 * time.Second}, // millisecondes
|
||||
{MCPServerConfig{}, map[string]any{"timeout": 5000.0}, mcpCallTimeout}, // 5 s : le défaut reste
|
||||
{MCPServerConfig{}, map[string]any{"timeout": "400"}, 430 * time.Second}, // chaîne
|
||||
{MCPServerConfig{}, map[string]any{"timeout": 86400.0 * 1000}, mcpCallTimeoutMax}, // plafond
|
||||
{MCPServerConfig{CallTimeoutSec: 30}, nil, 30 * time.Second}, // plus court que le défaut
|
||||
}
|
||||
for i, c := range cases {
|
||||
if got := mcpCallTimeoutFor(c.cfg, c.t, c.args); got != c.want {
|
||||
if got := mcpCallTimeoutFor(c.cfg, c.args); got != c.want {
|
||||
t.Errorf("cas %d : %s, attendu %s", i, got, c.want)
|
||||
}
|
||||
}
|
||||
|
||||
+26
-38
@@ -36,17 +36,19 @@ import (
|
||||
// pour router les appels sans avoir à re-parser des noms potentiellement
|
||||
// ambigus.
|
||||
|
||||
// Les délais de connexion et d'appel sont réglables (mcp_timeouts.go) :
|
||||
// 20 s et 120 s par défaut.
|
||||
const (
|
||||
// mcpConnectTimeout borne l'établissement d'une session (handshake initialize
|
||||
// + tools/list). Un serveur qui rame ne doit pas figer le tour de chat.
|
||||
mcpConnectTimeout = 20 * time.Second
|
||||
// mcpConnectTimeoutCold : npx et uvx TÉLÉCHARGENT le paquet du serveur au
|
||||
// premier lancement (le catalogue les utilise tous les deux). Sur un réseau
|
||||
// ou un disque lents, ça dépasse largement 20 s, et l'utilisateur voyait
|
||||
// « enregistré mais connexion échouée : context deadline exceeded » pour un
|
||||
// serveur parfaitement sain. Les lancements suivants profitent du cache et
|
||||
// retombent dans le régime normal.
|
||||
// C'est un minimum : un délai de connexion réglé plus long l'emporte.
|
||||
mcpConnectTimeoutCold = 3 * time.Minute
|
||||
// mcpCallTimeout borne un appel d'outil MCP.
|
||||
mcpCallTimeout = 120 * time.Second
|
||||
)
|
||||
|
||||
// mcpOutputCap borne la sortie d'un outil MCP renvoyée au modèle. L'amont
|
||||
@@ -192,12 +194,11 @@ func (m *mcpManager) ensure(name string, cfg MCPServerConfig) *mcpSession {
|
||||
m.mu.Unlock()
|
||||
|
||||
// Connexion hors-lock (peut être lente).
|
||||
timeouts := LoadMCPTimeouts()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), mcpTimeoutFor(cfg, timeouts))
|
||||
ctx, cancel := context.WithTimeout(context.Background(), mcpTimeoutFor(cfg))
|
||||
defer cancel()
|
||||
sess, err := mcpConnect(ctx, name, cfg)
|
||||
if err != nil {
|
||||
s.err = mcpFriendlyErr(err, cfg, timeouts)
|
||||
s.err = mcpFriendlyErr(err, cfg)
|
||||
} else {
|
||||
s.sess = sess
|
||||
lt, lerr := sess.ListTools(ctx, nil)
|
||||
@@ -390,7 +391,7 @@ func mcpCall(parent context.Context, name string, args map[string]any) string {
|
||||
}
|
||||
servers, _ := LoadMCPConfig()
|
||||
cfg, exists := servers[ref.server]
|
||||
timeout := mcpCallTimeoutFor(cfg, LoadMCPTimeouts(), args)
|
||||
timeout := mcpCallTimeoutFor(cfg, args)
|
||||
|
||||
result, err := mcpCallOnce(parent, ref, args, timeout)
|
||||
if err != nil {
|
||||
@@ -423,16 +424,14 @@ func mcpCall(parent context.Context, name string, args map[string]any) string {
|
||||
|
||||
// mcpCallTimeoutMax plafonne le délai d'un appel, quoi que demandent l'outil
|
||||
// ou la config : au-delà, c'est un job d'arrière-plan, pas un appel d'outil.
|
||||
// Il suit la borne des réglages (1 h) : un délai réglé à 3600 s n'est pas
|
||||
// rogné en silence.
|
||||
const mcpCallTimeoutMax = mcpTimeoutMaxSec * time.Second
|
||||
const mcpCallTimeoutMax = 30 * time.Minute
|
||||
|
||||
// mcpCallTimeoutFor : délai d'un appel. Le réglage global (t.Call), remplacé
|
||||
// par CallTimeoutSec du serveur ; un argument « timeout » de l'outil lui-même
|
||||
// mcpCallTimeoutFor : délai d'un appel. Défaut mcpCallTimeout, remplacé par
|
||||
// CallTimeoutSec du serveur ; un argument « timeout » de l'outil lui-même
|
||||
// (beaucoup en ont un : navigateur, build, requête) l'allonge de 30 s pour que
|
||||
// l'outil ait le temps de rendre SA propre erreur de délai.
|
||||
func mcpCallTimeoutFor(cfg MCPServerConfig, t MCPTimeouts, args map[string]any) time.Duration {
|
||||
d := t.call()
|
||||
func mcpCallTimeoutFor(cfg MCPServerConfig, args map[string]any) time.Duration {
|
||||
d := mcpCallTimeout
|
||||
if cfg.CallTimeoutSec > 0 {
|
||||
d = time.Duration(cfg.CallTimeoutSec) * time.Second
|
||||
}
|
||||
@@ -599,8 +598,6 @@ type MCPServerStatus struct {
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Headers map[string]string `json:"headers,omitempty"`
|
||||
// CallTimeoutSec : délai d'un appel propre au serveur (0 = réglage global).
|
||||
CallTimeoutSec int `json:"callTimeoutSec,omitempty"`
|
||||
}
|
||||
|
||||
// MCPStatus renvoie l'état de tous les serveurs configurés, en tentant de
|
||||
@@ -624,9 +621,7 @@ func MCPStatus() ([]MCPServerStatus, error) {
|
||||
URL: cfg.URL,
|
||||
Headers: cfg.Headers,
|
||||
Tools: []string{},
|
||||
|
||||
CallTimeoutSec: cfg.CallTimeoutSec,
|
||||
Disabled: cfg.DisabledTools,
|
||||
Disabled: cfg.DisabledTools,
|
||||
}
|
||||
if st.Disabled == nil {
|
||||
st.Disabled = []string{}
|
||||
@@ -648,36 +643,29 @@ func MCPStatus() ([]MCPServerStatus, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// mcpTimeoutFor choisit le délai de connexion d'un serveur : le réglage
|
||||
// global, mais au moins mcpConnectTimeoutCold pour un lanceur qui télécharge
|
||||
// son paquet à la volée (npx, uvx).
|
||||
func mcpTimeoutFor(cfg MCPServerConfig, t MCPTimeouts) time.Duration {
|
||||
if mcpColdLauncher(cfg) {
|
||||
return max(t.connect(), mcpConnectTimeoutCold)
|
||||
}
|
||||
return t.connect()
|
||||
}
|
||||
|
||||
// mcpColdLauncher : npx ou uvx, qui téléchargent le paquet au premier
|
||||
// lancement.
|
||||
func mcpColdLauncher(cfg MCPServerConfig) bool {
|
||||
// mcpTimeoutFor choisit le délai de connexion d'un serveur : long pour un
|
||||
// lanceur qui télécharge son paquet à la volée (npx, uvx), court sinon.
|
||||
func mcpTimeoutFor(cfg MCPServerConfig) time.Duration {
|
||||
base := strings.ToLower(filepath.Base(cfg.Command))
|
||||
base = strings.TrimSuffix(strings.TrimSuffix(base, ".exe"), ".cmd")
|
||||
return base == "npx" || base == "uvx"
|
||||
if base == "npx" || base == "uvx" {
|
||||
return mcpConnectTimeoutCold
|
||||
}
|
||||
return mcpConnectTimeout
|
||||
}
|
||||
|
||||
// mcpFriendlyErr habille l'erreur de connexion la plus vécue : le délai
|
||||
// dépassé d'un npx/uvx en plein téléchargement. L'erreur brute (« context
|
||||
// deadline exceeded ») envoyait l'utilisateur chercher un bug de config alors
|
||||
// qu'il suffit souvent de réessayer une fois le paquet en cache.
|
||||
func mcpFriendlyErr(err error, cfg MCPServerConfig, t MCPTimeouts) error {
|
||||
func mcpFriendlyErr(err error, cfg MCPServerConfig) error {
|
||||
if err == nil || !errors.Is(err, context.DeadlineExceeded) {
|
||||
return err
|
||||
}
|
||||
if mcpColdLauncher(cfg) {
|
||||
base := strings.ToLower(filepath.Base(cfg.Command))
|
||||
base = strings.TrimSuffix(strings.TrimSuffix(base, ".exe"), ".cmd")
|
||||
base := strings.ToLower(filepath.Base(cfg.Command))
|
||||
base = strings.TrimSuffix(strings.TrimSuffix(base, ".exe"), ".cmd")
|
||||
if base == "npx" || base == "uvx" {
|
||||
return fmt.Errorf("délai dépassé — %s télécharge le paquet du serveur au premier lancement, ce qui peut être long ; clique « tester » pour réessayer (le paquet reste en cache), et vérifie que le conteneur atteint le registre (npm/PyPI)", base)
|
||||
}
|
||||
return fmt.Errorf("délai dépassé — le serveur n'a pas répondu au handshake en %s (délai de connexion réglable dans les réglages MCP)", t.connect())
|
||||
return fmt.Errorf("délai dépassé — le serveur n'a pas répondu au handshake en %s", mcpConnectTimeout)
|
||||
}
|
||||
@@ -52,7 +52,7 @@ type MCPServerConfig struct {
|
||||
DisabledTools []string `json:"disabledTools,omitempty"`
|
||||
|
||||
// CallTimeoutSec : délai d'un appel d'outil de ce serveur, en secondes.
|
||||
// 0 = réglage global (MCPTimeouts.Call). Pour un serveur dont les outils sont longs
|
||||
// 0 = défaut (mcpCallTimeout). Pour un serveur dont les outils sont longs
|
||||
// par nature (build, scraping, requête lourde).
|
||||
CallTimeoutSec int `json:"callTimeoutSec,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Délais MCP réglables depuis l'UI (repris d'AJEAN c05a0a9f) : connexion d'une
|
||||
// session et appel d'outil, en secondes, communs à tous les serveurs. Un
|
||||
// serveur peut remplacer le délai d'appel par le sien (CallTimeoutSec), et un
|
||||
// lancement npx/uvx garde au moins mcpConnectTimeoutCold pour se connecter.
|
||||
|
||||
// MCPTimeouts : les délais globaux, en secondes. Stockés dans bkState sous
|
||||
// la clé mcpTimeoutsKey ; zéro (ou clé absente) = valeur par défaut.
|
||||
type MCPTimeouts struct {
|
||||
// Connect borne l'établissement d'une session (handshake initialize +
|
||||
// tools/list). Un serveur qui rame ne doit pas figer le tour de chat.
|
||||
Connect int `json:"connect"`
|
||||
// Call borne un appel d'outil MCP.
|
||||
Call int `json:"call"`
|
||||
}
|
||||
|
||||
const (
|
||||
// Valeurs par défaut, en secondes (les anciennes constantes).
|
||||
mcpConnectTimeoutDefault = 20
|
||||
mcpCallTimeoutDefault = 120
|
||||
// mcpTimeoutMaxSec : borne haute des réglages (1 h), globaux comme par
|
||||
// serveur.
|
||||
mcpTimeoutMaxSec = 3600
|
||||
mcpTimeoutsKey = "mcp_timeouts"
|
||||
)
|
||||
|
||||
// LoadMCPTimeouts lit les délais globaux. Une valeur absente, nulle ou
|
||||
// illisible retombe sur son défaut ; une valeur hors bornes (base modifiée à
|
||||
// la main) est ramenée à mcpTimeoutMaxSec.
|
||||
func LoadMCPTimeouts() MCPTimeouts {
|
||||
var t MCPTimeouts
|
||||
if !getJSON(bkState, mcpTimeoutsKey, &t) {
|
||||
t = MCPTimeouts{}
|
||||
}
|
||||
return t.normalized()
|
||||
}
|
||||
|
||||
func (t MCPTimeouts) normalized() MCPTimeouts {
|
||||
fix := func(v, def int) int {
|
||||
switch {
|
||||
case v <= 0:
|
||||
return def
|
||||
case v > mcpTimeoutMaxSec:
|
||||
return mcpTimeoutMaxSec
|
||||
}
|
||||
return v
|
||||
}
|
||||
return MCPTimeouts{
|
||||
Connect: fix(t.Connect, mcpConnectTimeoutDefault),
|
||||
Call: fix(t.Call, mcpCallTimeoutDefault),
|
||||
}
|
||||
}
|
||||
|
||||
// SaveMCPTimeouts enregistre les délais globaux (valeurs déjà validées par
|
||||
// l'appelant ; normalisées par sûreté).
|
||||
func SaveMCPTimeouts(t MCPTimeouts) error {
|
||||
mcpConfigMu.Lock()
|
||||
defer mcpConfigMu.Unlock()
|
||||
return putJSON(bkState, mcpTimeoutsKey, t.normalized())
|
||||
}
|
||||
|
||||
// connect et call : les délais en durées.
|
||||
func (t MCPTimeouts) connect() time.Duration { return time.Duration(t.Connect) * time.Second }
|
||||
func (t MCPTimeouts) call() time.Duration { return time.Duration(t.Call) * time.Second }
|
||||
|
||||
// mcpTimeoutsError : le refus d'un réglage hors bornes, nil sinon. Même texte
|
||||
// pour la façade (mcp/mod.rs).
|
||||
func mcpTimeoutsError(t MCPTimeouts) string {
|
||||
if t.Connect < 1 || t.Connect > mcpTimeoutMaxSec {
|
||||
return "connexion : 1 à 3600 s"
|
||||
}
|
||||
if t.Call < 1 || t.Call > mcpTimeoutMaxSec {
|
||||
return "appel d'outil : 1 à 3600 s"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// mcpCallTimeoutSecError : le refus d'un délai par serveur hors bornes.
|
||||
func mcpCallTimeoutSecError(sec int) string {
|
||||
if sec < 0 || sec > mcpTimeoutMaxSec {
|
||||
return "délai d'un appel : 0 à 3600 s (vide = réglage global)"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Une base vide donne les délais par défaut : 20 s de connexion, 120 s
|
||||
// d'appel.
|
||||
func TestMCPTimeoutsDefautsSansConfig(t *testing.T) {
|
||||
testHome(t)
|
||||
got := LoadMCPTimeouts()
|
||||
if got != (MCPTimeouts{Connect: 20, Call: 120}) {
|
||||
t.Fatalf("défauts = %+v", got)
|
||||
}
|
||||
if d := mcpTimeoutFor(MCPServerConfig{Command: "node"}, got); d != 20*time.Second {
|
||||
t.Errorf("connexion = %s", d)
|
||||
}
|
||||
if d := mcpCallTimeoutFor(MCPServerConfig{}, got, nil); d != 120*time.Second {
|
||||
t.Errorf("appel = %s", d)
|
||||
}
|
||||
}
|
||||
|
||||
// Enregistrés puis relus ; une valeur nulle retombe sur son défaut, une
|
||||
// valeur trop grande (base modifiée à la main) sur la borne, une valeur
|
||||
// illisible sur les défauts.
|
||||
func TestMCPTimeoutsAllerRetour(t *testing.T) {
|
||||
testHome(t)
|
||||
if err := SaveMCPTimeouts(MCPTimeouts{Connect: 5, Call: 300}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := LoadMCPTimeouts(); got != (MCPTimeouts{Connect: 5, Call: 300}) {
|
||||
t.Fatalf("relecture = %+v", got)
|
||||
}
|
||||
for raw, want := range map[string]MCPTimeouts{
|
||||
`{"connect":0,"call":0}`: {20, 120},
|
||||
`{"connect":-4,"call":99999}`: {20, 3600},
|
||||
`{"connect":"x","call":50}`: {20, 120},
|
||||
`{"CONNECT":7}`: {7, 120},
|
||||
`null`: {20, 120},
|
||||
`{"connect":30,"call":40} reste`: {20, 120},
|
||||
} {
|
||||
if err := putStr(bkState, mcpTimeoutsKey, raw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := LoadMCPTimeouts(); got != want {
|
||||
t.Errorf("%s : %+v, attendu %+v", raw, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Un lanceur npx/uvx garde au moins mcpConnectTimeoutCold ; un délai réglé
|
||||
// plus long l'emporte.
|
||||
func TestMCPTimeoutForLanceurFroid(t *testing.T) {
|
||||
npx := MCPServerConfig{Command: "npx"}
|
||||
if d := mcpTimeoutFor(npx, MCPTimeouts{Connect: 20, Call: 120}); d != mcpConnectTimeoutCold {
|
||||
t.Errorf("npx, 20 s : %s", d)
|
||||
}
|
||||
if d := mcpTimeoutFor(npx, MCPTimeouts{Connect: 600, Call: 120}); d != 10*time.Minute {
|
||||
t.Errorf("npx, 600 s : %s", d)
|
||||
}
|
||||
if d := mcpTimeoutFor(MCPServerConfig{URL: "http://x"}, MCPTimeouts{Connect: 45, Call: 120}); d != 45*time.Second {
|
||||
t.Errorf("http, 45 s : %s", d)
|
||||
}
|
||||
}
|
||||
|
||||
func mcpRoute(t *testing.T, h func(w *httptest.ResponseRecorder), want int) map[string]any {
|
||||
t.Helper()
|
||||
w := httptest.NewRecorder()
|
||||
h(w)
|
||||
var out map[string]any
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &out); err != nil {
|
||||
t.Fatalf("réponse %q : %v", w.Body.String(), err)
|
||||
}
|
||||
if w.Code != want {
|
||||
t.Fatalf("code %d, attendu %d : %s", w.Code, want, w.Body.String())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Les routes des délais globaux : lecture, refus hors bornes, enregistrement
|
||||
// qui ferme les sessions ouvertes.
|
||||
func TestMCPTimeoutsRoutes(t *testing.T) {
|
||||
testHome(t)
|
||||
t.Setenv(facadeMCPEnv, "")
|
||||
get := func() map[string]any {
|
||||
return mcpRoute(t, func(w *httptest.ResponseRecorder) {
|
||||
handleMCPTimeouts(w, httptest.NewRequest("GET", "/api/mcp/timeouts", nil))
|
||||
}, 200)
|
||||
}
|
||||
post := func(body string, want int) map[string]any {
|
||||
return mcpRoute(t, func(w *httptest.ResponseRecorder) {
|
||||
handleMCPTimeoutsSave(w, httptest.NewRequest("POST", "/api/mcp/timeouts/save", strings.NewReader(body)))
|
||||
}, want)
|
||||
}
|
||||
if v := get(); v["connect"] != 20.0 || v["call"] != 120.0 {
|
||||
t.Fatalf("GET : %v", v)
|
||||
}
|
||||
for body, msg := range map[string]string{
|
||||
`{"connect":0,"call":120}`: "connexion : 1 à 3600 s",
|
||||
`{"connect":20,"call":3601}`: "appel d'outil : 1 à 3600 s",
|
||||
`{"connect":20}`: "appel d'outil : 1 à 3600 s",
|
||||
`{"connect":"20","call":120}`: "json: cannot unmarshal string into Go struct field MCPTimeouts.connect of type int",
|
||||
} {
|
||||
if v := post(body, 400); v["error"] != msg {
|
||||
t.Errorf("%s : %v", body, v["error"])
|
||||
}
|
||||
}
|
||||
// Une session ouverte sur un serveur configuré : fermée à l'enregistrement.
|
||||
if err := SetMCPServer("lent", MCPServerConfig{URL: "http://127.0.0.1:1/mcp", Enabled: false}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fakeMCPSlow(t)
|
||||
if v := post(`{"connect":30,"call":600}`, 200); v["connect"] != 30.0 || v["call"] != 600.0 {
|
||||
t.Fatalf("POST : %v", v)
|
||||
}
|
||||
mcpMgr.mu.Lock()
|
||||
_, open := mcpMgr.sessions["lent"]
|
||||
mcpMgr.mu.Unlock()
|
||||
if open {
|
||||
t.Error("la session est restée ouverte")
|
||||
}
|
||||
if v := get(); v["connect"] != 30.0 || v["call"] != 600.0 {
|
||||
t.Fatalf("GET après enregistrement : %v", v)
|
||||
}
|
||||
if got := getStr(bkState, mcpTimeoutsKey); got != `{"connect":30,"call":600}` {
|
||||
t.Errorf("stocké : %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Le délai d'un serveur passe par /api/mcp/save, revient dans /api/mcp et
|
||||
// survit à un nouvel enregistrement sans lui (0 = réglage global).
|
||||
func TestMCPSaveCallTimeoutSec(t *testing.T) {
|
||||
testHome(t)
|
||||
t.Setenv(facadeMCPEnv, "")
|
||||
t.Cleanup(mcpCloseAll)
|
||||
save := func(body string, want int) map[string]any {
|
||||
return mcpRoute(t, func(w *httptest.ResponseRecorder) {
|
||||
handleMCPSave(w, httptest.NewRequest("POST", "/api/mcp/save", strings.NewReader(body)))
|
||||
}, want)
|
||||
}
|
||||
v := save(`{"name":"long","url":"http://127.0.0.1:1/mcp","enabled":false,"callTimeoutSec":900}`, 200)
|
||||
srv := v["servers"].([]any)[0].(map[string]any)
|
||||
if srv["callTimeoutSec"] != 900.0 {
|
||||
t.Fatalf("état : %v", srv)
|
||||
}
|
||||
servers, _ := LoadMCPConfig()
|
||||
if servers["long"].CallTimeoutSec != 900 {
|
||||
t.Fatalf("config : %+v", servers["long"])
|
||||
}
|
||||
st, _ := MCPStatus()
|
||||
if len(st) != 1 || st[0].CallTimeoutSec != 900 {
|
||||
t.Fatalf("MCPStatus : %+v", st)
|
||||
}
|
||||
// Champ vidé dans l'UI : retour au réglage global, clé absente.
|
||||
v = save(`{"name":"long","url":"http://127.0.0.1:1/mcp","enabled":false}`, 200)
|
||||
if _, ok := v["servers"].([]any)[0].(map[string]any)["callTimeoutSec"]; ok {
|
||||
t.Fatalf("callTimeoutSec resté : %v", v)
|
||||
}
|
||||
for _, body := range []string{
|
||||
`{"name":"x","url":"http://h","callTimeoutSec":-1}`,
|
||||
`{"name":"x","url":"http://h","callTimeoutSec":3601}`,
|
||||
} {
|
||||
if v := save(body, 400); v["error"] != "délai d'un appel : 0 à 3600 s (vide = réglage global)" {
|
||||
t.Errorf("%s : %v", body, v["error"])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,76 +91,13 @@ func TestPariteMCP(t *testing.T) {
|
||||
}
|
||||
fx["prompt_line"] = lines
|
||||
timeouts := map[string]any{}
|
||||
def := MCPTimeouts{}.normalized()
|
||||
for _, cmd := range []string{"npx", "/usr/local/bin/NPX", "uvx.exe", "npx.cmd", "node", "", "uvx-x", "C:/bin/uvx.EXE"} {
|
||||
cfg := MCPServerConfig{Command: cmd}
|
||||
timeouts[cmd] = map[string]any{"secs": int(mcpTimeoutFor(cfg, def).Seconds()),
|
||||
"err": mcpFriendlyErr(context.DeadlineExceeded, cfg, def).Error()}
|
||||
timeouts[cmd] = map[string]any{"secs": int(mcpTimeoutFor(cfg).Seconds()),
|
||||
"err": mcpFriendlyErr(context.DeadlineExceeded, cfg).Error()}
|
||||
}
|
||||
fx["timeouts"] = timeouts
|
||||
fx["friendly_other"] = mcpFriendlyErr(errors.New("autre"), MCPServerConfig{Command: "npx"}, def).Error()
|
||||
// Délais globaux (mcp_timeouts) : lecture de la base, puis leur effet sur
|
||||
// la connexion et sur l'appel.
|
||||
var stored []any
|
||||
for _, raw := range []string{"", `{"connect":5,"call":300}`, `{"connect":0,"call":0}`, `{"connect":-4,"call":99999}`,
|
||||
`{"connect":"x","call":50}`, `{"CONNECT":7,"Call":8}`, `null`, `[1]`, `{"connect":30,"call":40} reste`,
|
||||
`{"connect":1.5,"call":60}`, `{"connect":3600,"call":3600,"autre":1}`} {
|
||||
_ = putStr(bkState, mcpTimeoutsKey, raw)
|
||||
stored = append(stored, map[string]any{"in": raw, "out": LoadMCPTimeouts()})
|
||||
}
|
||||
fx["timeouts_stored"] = stored
|
||||
var effects []any
|
||||
for _, tm := range []MCPTimeouts{{20, 120}, {5, 45}, {600, 3600}, {180, 1}} {
|
||||
for _, c := range []struct {
|
||||
cfg MCPServerConfig
|
||||
args string
|
||||
}{
|
||||
{MCPServerConfig{Command: "node"}, `{}`},
|
||||
{MCPServerConfig{Command: "npx"}, `{}`},
|
||||
{MCPServerConfig{URL: "http://h", CallTimeoutSec: 90}, `{}`},
|
||||
{MCPServerConfig{Command: "uvx", CallTimeoutSec: 3600}, `{"timeout":7200}`},
|
||||
{MCPServerConfig{URL: "http://h"}, `{"timeout":600}`},
|
||||
{MCPServerConfig{URL: "http://h"}, `{"timeout":99999999}`},
|
||||
} {
|
||||
var args map[string]any
|
||||
_ = json.Unmarshal([]byte(c.args), &args)
|
||||
cfgJSON, _ := json.Marshal(c.cfg)
|
||||
effects = append(effects, map[string]any{
|
||||
"timeouts": tm, "cfg": string(cfgJSON), "args": c.args,
|
||||
"connect": mcpTimeoutFor(c.cfg, tm).Milliseconds(),
|
||||
"err": mcpFriendlyErr(context.DeadlineExceeded, c.cfg, tm).Error(),
|
||||
"call": mcpCallTimeoutFor(c.cfg, tm, args).Milliseconds(),
|
||||
})
|
||||
}
|
||||
}
|
||||
fx["timeouts_effects"] = effects
|
||||
_ = putStr(bkState, mcpTimeoutsKey, "")
|
||||
var routes []any
|
||||
for _, rq := range [][2]string{
|
||||
{"/api/mcp/timeouts", ""},
|
||||
{"/api/mcp/timeouts/save", `{"connect":0,"call":120}`},
|
||||
{"/api/mcp/timeouts/save", `{"connect":20,"call":3601}`},
|
||||
{"/api/mcp/timeouts/save", `{"connect":20}`},
|
||||
{"/api/mcp/timeouts/save", `{"connect":"20","call":120}`},
|
||||
{"/api/mcp/timeouts/save", `{"connect":2.5,"call":120}`},
|
||||
{"/api/mcp/timeouts/save", `[]`},
|
||||
{"/api/mcp/timeouts/save", `pas du json`},
|
||||
{"/api/mcp/timeouts/save", `{"Connect":45,"CALL":900,"x":1}`},
|
||||
{"/api/mcp/timeouts", ""},
|
||||
{"/api/mcp/timeouts/save", `{"connect":3600,"call":1}`},
|
||||
{"/api/mcp/timeouts", ""},
|
||||
} {
|
||||
w := httptest.NewRecorder()
|
||||
if rq[0] == "/api/mcp/timeouts" {
|
||||
handleMCPTimeouts(w, httptest.NewRequest("GET", rq[0], nil))
|
||||
} else {
|
||||
handleMCPTimeoutsSave(w, httptest.NewRequest("POST", rq[0], strings.NewReader(rq[1])))
|
||||
}
|
||||
routes = append(routes, map[string]any{"path": rq[0], "body": rq[1], "code": w.Code,
|
||||
"resp": strings.TrimSpace(w.Body.String()), "state": getStr(bkState, mcpTimeoutsKey)})
|
||||
}
|
||||
fx["timeouts_routes"] = routes
|
||||
_ = putStr(bkState, mcpTimeoutsKey, "")
|
||||
fx["friendly_other"] = mcpFriendlyErr(errors.New("autre"), MCPServerConfig{Command: "npx"}).Error()
|
||||
var flat []any
|
||||
for _, raw := range []string{
|
||||
`{"content":[{"type":"text","text":" un "},{"type":"text","text":"deux\n"}]}`,
|
||||
@@ -211,7 +148,7 @@ func TestPariteMCP(t *testing.T) {
|
||||
{Op: "set", Name: "a__b", Cfg: `{"command":"x"}`},
|
||||
{Op: "set", Name: "both", Cfg: `{"command":"x","url":"http://u"}`},
|
||||
{Op: "set", Name: "none", Cfg: `{"enabled":true}`},
|
||||
{Op: "set", Name: "web", Cfg: `{"url":"http://u/mcp","headers":{"X":"1"},"enabled":false,"callTimeoutSec":45}`},
|
||||
{Op: "set", Name: "web", Cfg: `{"url":"http://u/mcp","headers":{"X":"1"},"enabled":false}`},
|
||||
{Op: "enable", Name: "web", On: true},
|
||||
{Op: "enable", Name: "nope", On: true},
|
||||
{Op: "tool", Name: "fs", Tool: "write", On: false},
|
||||
@@ -348,7 +285,7 @@ func TestPariteMCP(t *testing.T) {
|
||||
}
|
||||
_ = SetMCPToolEnabled("alpha", "secret", false)
|
||||
_ = SetMCPServer("beta", MCPServerConfig{Command: "loki-mcp-absent-pour-test", Enabled: true})
|
||||
_ = SetMCPServer("gamma", MCPServerConfig{URL: ts.URL, Enabled: false, CallTimeoutSec: 300})
|
||||
_ = SetMCPServer("gamma", MCPServerConfig{URL: ts.URL, Enabled: false})
|
||||
raw := getStr(bkState, "mcp")
|
||||
raw = strings.Replace(raw, `}}`, `},"delta":{"enabled":true}}`, 1)
|
||||
_ = putStr(bkState, "mcp", raw)
|
||||
|
||||
+82
-67
@@ -1418,6 +1418,10 @@ button:hover{border-color:var(--dim);color:var(--text)}
|
||||
border:1px solid var(--border);border-radius:999px;background:var(--panel);
|
||||
color:var(--dim);font-size:12px;line-height:1.5}
|
||||
.stopnote::before{content:"";flex:none;width:6px;height:6px;border-radius:50%;background:var(--dim)}
|
||||
/* Formules LaTeX (KaTeX, 00-math.js) : une longue formule défile au lieu de déborder. */
|
||||
.katex{font-size:1.08em}
|
||||
.katex-display{margin:.6em 0;overflow-x:auto;overflow-y:hidden;padding:2px 0}
|
||||
.math-pending{font-family:var(--mono);font-size:.92em;color:var(--dim)}
|
||||
|
||||
/* ===== Fenêtres et sélecteurs =====================================
|
||||
Coins arrondis et ombre douce plutôt qu'un cadre marqué. */
|
||||
@@ -2773,17 +2777,6 @@ html[data-files="1"] #files-btn{color:var(--accent)}
|
||||
<div id="mcp-block" class="gated-group">
|
||||
<div class="subhead">Serveurs MCP<span id="mcp-badge" class="sbadge"></span><span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Connecte des <b>serveurs MCP</b> (Model Context Protocol) pour donner de nouveaux outils à l'IA : accès fichiers, bases de données, APIs métier… <b>stdio</b> = programme lancé localement (npx/uvx/binaire) · <b>http</b> = serveur distant. ⚠ un serveur stdio exécute du code sur cette machine, au même titre que le shell.</span></span></div>
|
||||
<div class="slist">
|
||||
<!-- Délais communs à tous les serveurs (AJEAN c05a0a9f, e44b6139),
|
||||
enregistrés à chaque modification. Un serveur peut remplacer le
|
||||
délai d'appel dans sa fenêtre d'édition. -->
|
||||
<div class="pe-row">
|
||||
<span class="pe-row-l">Délai de connexion<span class="pe-sub">pour joindre un serveur ; au moins 3 min au premier lancement npx/uvx</span></span>
|
||||
<span class="pe-row-c"><input id="mcp-timeout-connect" class="pe-val" type="number" min="1" max="3600" step="1" value="20" style="width:54px" onchange="saveMcpTimeouts()"><span class="pe-unit">s</span></span>
|
||||
</div>
|
||||
<div class="pe-row">
|
||||
<span class="pe-row-l">Délai d'un appel d'outil<span class="pe-sub">réglable aussi serveur par serveur</span></span>
|
||||
<span class="pe-row-c"><input id="mcp-timeout-call" class="pe-val" type="number" min="1" max="3600" step="1" value="120" style="width:54px" onchange="saveMcpTimeouts()"><span class="pe-unit">s</span></span>
|
||||
</div>
|
||||
<div class="rowbody">
|
||||
<!-- Même texte que celui rendu après chargement quand il n'y a aucun
|
||||
serveur : sinon la carte partait vide puis grandissait d'une ligne. -->
|
||||
@@ -3303,10 +3296,6 @@ html[data-files="1"] #files-btn{color:var(--accent)}
|
||||
<textarea id="mcp-headers" class="ctl ta" spellcheck="false" style="min-height:44px" placeholder="Authorization: Bearer sk-…"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fld">
|
||||
<label class="fld-l" for="mcp-call-timeout">Délai d'un appel d'outil <span class="opt">(secondes, 1 à 3600 — vide = réglage global)</span></label>
|
||||
<input id="mcp-call-timeout" class="ctl" type="number" min="1" max="3600" step="1" inputmode="numeric" placeholder="réglage global">
|
||||
</div>
|
||||
<div id="mcp-modal-status" class="muted" style="font-size:12px;min-height:16px"></div>
|
||||
<div id="mcp-tools-section" style="display:none">
|
||||
<div class="subhead" style="margin-top:6px">Outils <span id="mcp-tools-count" class="muted" style="text-transform:none;letter-spacing:0;font-weight:400;font-family:var(--font)"></span><span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Décoche un outil pour le masquer à l'IA sans déconnecter le serveur. Utile pour ne garder que ce dont tu as besoin.</span></span></div>
|
||||
@@ -3835,6 +3824,76 @@ html[data-files="1"] #files-btn{color:var(--accent)}
|
||||
</div>
|
||||
<script src="marked.min.js"></script>
|
||||
<script>
|
||||
// --- Formules LaTeX (KaTeX) ---------------------------------------------------
|
||||
// Repris d'AJEAN (#123). md() (00-md.js) met les formules de côté AVANT marked,
|
||||
// qui abîmerait \\, _ et * : un repère (caractères à usage privé U+E000/U+E001
|
||||
// autour du numéro) prend leur place, puis reçoit le rendu. Le code (blocs et
|
||||
// `en ligne`) n'est jamais touché. Formes reconnues : $$…$$ et \[…\] (bloc),
|
||||
// \(…\) et $…$ (en ligne). $…$ n'est une formule que s'il ne commence ni ne finit
|
||||
// par une espace, n'est pas collé à un mot ni échappé (\$) avant, et n'est pas
|
||||
// suivi d'un chiffre : « 5 $ et 10 $ » ou « $5 et $10 » restent du texte.
|
||||
// Ajouts de Loki : un $ ouvrant précédé d'un chiffre et d'une espace (« 5 $, »)
|
||||
// est un montant, et $…$ ne contient pas de ` — sans quoi « 5 $, code `$x$` »
|
||||
// s'apparierait par-dessus le code en ligne.
|
||||
// KaTeX (embarqué, servi sous /katex/ par web_katex.go et la façade) n'est chargé
|
||||
// qu'à la première formule ; d'ici là la source s'affiche, puis est rendue sur
|
||||
// place. (Pas de lookbehind dans l'expression : les iOS d'avant 16.4 refusent de
|
||||
// charger le script entier. Ces contrôles-là sont faits dans le remplacement.)
|
||||
//
|
||||
// MATH_RE et mathExtract sont purs (ni DOM ni réseau) : web_katex_math_test.go les
|
||||
// exécute sous node sur les cas limites (montants, code, échappements).
|
||||
const MATH_RE = /(```[\s\S]*?(?:```|$)|~~~[\s\S]*?(?:~~~|$)|`[^`\n]*`)|\$\$([\s\S]+?)\$\$|\\\[([\s\S]+?)\\\]|\\\(([\s\S]+?)\\\)|\$(?![\s$])((?:\\.|[^$\\\n`])+?)\$(?![\d$])/g;
|
||||
const MATH_MARK_RE = /\uE000(\d+)\uE001/g;
|
||||
function mathExtract(s){
|
||||
const list = [];
|
||||
const text = s.replace(MATH_RE, (all, code, dd, br, pa, d, at)=>{
|
||||
if(code !== undefined) return all;
|
||||
// $…$ : pas collé à un mot ni échappé (\$) avant, pas un montant (« 5 $ »),
|
||||
// pas d'espace avant le $ final.
|
||||
if(d !== undefined && (/[\\$\w]/.test(s[at-1] || '') || /\d\s$/.test(s.slice(Math.max(0, at-2), at)) || /\s$/.test(d))) return all;
|
||||
const display = dd !== undefined || br !== undefined;
|
||||
list.push({tex: (dd ?? br ?? pa ?? d).trim(), display});
|
||||
return '\uE000' + (list.length-1) + '\uE001';
|
||||
});
|
||||
return {text, list};
|
||||
}
|
||||
// mathRestore remplace chaque repère du HTML produit par marked par le rendu de
|
||||
// sa formule (render : formule → HTML).
|
||||
function mathRestore(html, list, render){
|
||||
return list.length ? html.replace(MATH_MARK_RE, (all, i)=>list[+i] ? render(list[+i]) : all) : html;
|
||||
}
|
||||
const mathEsc = (s)=>s.replace(/[&<>"]/g, c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
|
||||
const MATH_OPTS = {throwOnError: false, output: 'html'};
|
||||
function mathHtml(f){
|
||||
if(window.katex){
|
||||
try{ return katex.renderToString(f.tex, {...MATH_OPTS, displayMode: f.display}); }
|
||||
catch(_){ return '<code>'+mathEsc(f.tex)+'</code>'; }
|
||||
}
|
||||
mathLoad();
|
||||
return '<span class="math-pending" data-d="'+(f.display?1:0)+'">'+mathEsc(f.tex)+'</span>';
|
||||
}
|
||||
let mathLoading = null;
|
||||
function mathLoad(){
|
||||
if(mathLoading) return mathLoading;
|
||||
if(!document.querySelector('link[data-katex]')){
|
||||
const css = document.createElement('link');
|
||||
css.rel = 'stylesheet'; css.href = 'katex/katex.min.css'; css.dataset.katex = '1';
|
||||
document.head.appendChild(css);
|
||||
}
|
||||
mathLoading = new Promise((ok, ko)=>{
|
||||
const s = document.createElement('script');
|
||||
s.src = 'katex/katex.min.js'; s.onload = ok;
|
||||
// Échec (hors ligne, 404) : script retiré, nouvel essai à la prochaine formule.
|
||||
s.onerror = ()=>{ s.remove(); ko(); };
|
||||
document.head.appendChild(s);
|
||||
}).then(()=>{
|
||||
document.querySelectorAll('.math-pending').forEach(el=>{
|
||||
try{ katex.render(el.textContent, el, {...MATH_OPTS, displayMode: el.dataset.d === '1'}); }catch(_){}
|
||||
el.classList.remove('math-pending');
|
||||
});
|
||||
}).catch(()=>{ mathLoading = null; });
|
||||
return mathLoading;
|
||||
}
|
||||
// marked: GFM on, no auto-breaks (single newlines stay inline).
|
||||
marked.setOptions({ gfm: true, breaks: false });
|
||||
// Pre-process to fix common LLM markdown mistakes before handing to marked:
|
||||
@@ -3851,7 +3910,13 @@ function fixMd(s){
|
||||
s = s.replace(/\n{3,}/g, '\n\n');
|
||||
return s;
|
||||
}
|
||||
function md(src){ return src ? marked.parse(fixMd(src)) : ''; }
|
||||
// Les formules LaTeX sont mises de côté avant marked puis rendues par KaTeX
|
||||
// (00-math.js) ; le code et les montants en $ ne sont pas touchés.
|
||||
function md(src){
|
||||
if(!src) return '';
|
||||
const m = mathExtract(fixMd(src));
|
||||
return mathRestore(marked.parse(m.text), m.list, mathHtml);
|
||||
}
|
||||
// Les garde-fous du serveur ("[stop: trop d'appels d'outils]") sont concaténés
|
||||
// au texte de la réponse : ils arrivent donc en clair, au milieu du markdown.
|
||||
// markNotices les sort du fil pour qu'on ne les prenne pas pour une phrase du
|
||||
@@ -9517,47 +9582,7 @@ async function deleteExternal(){
|
||||
let mcpServers = [];
|
||||
let mcpEditing = null; // nom du serveur en cours d'édition ('' ou null = nouveau)
|
||||
|
||||
async function loadMCP(){
|
||||
renderMCP(await jget('/api/mcp'));
|
||||
loadMcpTimeouts();
|
||||
}
|
||||
|
||||
// ─── Délais globaux ──────────────────────────────────────────────────────────
|
||||
// Connexion et appel d'outil, communs à tous les serveurs (/api/mcp/timeouts).
|
||||
// Un seul enregistrement par modification : « change » part à la validation
|
||||
// ou à la perte du focus, pas à chaque frappe.
|
||||
let mcpTimeouts = {connect: 20, call: 120};
|
||||
|
||||
async function loadMcpTimeouts(){
|
||||
const r = await jget('/api/mcp/timeouts');
|
||||
if(!r || !r.ok) return;
|
||||
mcpTimeouts = {connect: r.connect, call: r.call};
|
||||
document.getElementById('mcp-timeout-connect').value = r.connect;
|
||||
document.getElementById('mcp-timeout-call').value = r.call;
|
||||
mcpCallTimeoutPlaceholder();
|
||||
}
|
||||
|
||||
async function saveMcpTimeouts(){
|
||||
const connect = Number(document.getElementById('mcp-timeout-connect').value);
|
||||
const call = Number(document.getElementById('mcp-timeout-call').value);
|
||||
const ok = v => Number.isInteger(v) && v >= 1 && v <= 3600;
|
||||
if(!ok(connect) || !ok(call)){
|
||||
toast('délai invalide : de 1 à 3600 s');
|
||||
loadMcpTimeouts(); // revient aux valeurs enregistrées
|
||||
return;
|
||||
}
|
||||
const r = await jpost('/api/mcp/timeouts/save', {connect, call});
|
||||
if(!r || !r.ok){ toast('erreur : '+((r && r.error) || 'délais non enregistrés')); loadMcpTimeouts(); return; }
|
||||
mcpTimeouts = {connect: r.connect, call: r.call};
|
||||
mcpCallTimeoutPlaceholder();
|
||||
toast('délais MCP enregistrés');
|
||||
}
|
||||
|
||||
// Le champ « délai d'un appel » de la fenêtre d'édition montre le réglage
|
||||
// global qu'il remplacerait.
|
||||
function mcpCallTimeoutPlaceholder(){
|
||||
document.getElementById('mcp-call-timeout').placeholder = 'réglage global ('+mcpTimeouts.call+' s)';
|
||||
}
|
||||
async function loadMCP(){ renderMCP(await jget('/api/mcp')); }
|
||||
|
||||
function renderMCP(r){
|
||||
mcpServers = (r && r.servers) || [];
|
||||
@@ -9730,7 +9755,6 @@ function openMcpFromCatalog(e){
|
||||
Object.entries(e.env||{}).map(([k,v])=>k+'='+v).join('\n');
|
||||
document.getElementById('mcp-url').value = '';
|
||||
document.getElementById('mcp-headers').value = '';
|
||||
document.getElementById('mcp-call-timeout').value = '';
|
||||
document.getElementById('mcp-modal-title').textContent = e.name+' — depuis le catalogue';
|
||||
}
|
||||
const st = document.getElementById('mcp-modal-status');
|
||||
@@ -9758,8 +9782,6 @@ function openMcp(name){
|
||||
document.getElementById('mcp-env').value = s && s.env ? Object.entries(s.env).map(([k,v])=>k+'='+v).join('\n') : '';
|
||||
document.getElementById('mcp-url').value = s ? (s.url||'') : '';
|
||||
document.getElementById('mcp-headers').value = s && s.headers ? Object.entries(s.headers).map(([k,v])=>k+': '+v).join('\n') : '';
|
||||
document.getElementById('mcp-call-timeout').value = s && s.callTimeoutSec ? s.callTimeoutSec : '';
|
||||
mcpCallTimeoutPlaceholder();
|
||||
const st = document.getElementById('mcp-modal-status');
|
||||
st.textContent = ''; st.style.color = '';
|
||||
if(s && s.enabled && s.error){ st.textContent = '⚠ '+s.error; st.style.color = 'var(--err)'; }
|
||||
@@ -9838,13 +9860,6 @@ async function saveMcp(){
|
||||
body.headers = mcpParseKV(document.getElementById('mcp-headers').value, ':');
|
||||
body.command = ''; body.args = undefined; body.env = undefined;
|
||||
}
|
||||
// Vide = réglage global (0 côté serveur).
|
||||
const ct = document.getElementById('mcp-call-timeout').value.trim();
|
||||
if(ct){
|
||||
const n = Number(ct);
|
||||
if(!Number.isInteger(n) || n < 1 || n > 3600){ toast("délai d'un appel d'outil : de 1 à 3600 s, ou vide"); return; }
|
||||
body.callTimeoutSec = n;
|
||||
}
|
||||
const st = document.getElementById('mcp-modal-status');
|
||||
st.textContent = 'connexion…'; st.style.color = '';
|
||||
const r = await jpost('/api/mcp/save', body);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2020 Khan Academy and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Vendored
+1
@@ -0,0 +1 @@
|
||||
@font-face{font-family:KaTeX_AMS;font-style:normal;font-weight:400;src:url(fonts/KaTeX_AMS-Regular.woff2) format("woff2"),url(fonts/KaTeX_AMS-Regular.woff) format("woff"),url(fonts/KaTeX_AMS-Regular.ttf) format("truetype")}@font-face{font-family:KaTeX_Caligraphic;font-style:normal;font-weight:700;src:url(fonts/KaTeX_Caligraphic-Bold.woff2) format("woff2"),url(fonts/KaTeX_Caligraphic-Bold.woff) format("woff"),url(fonts/KaTeX_Caligraphic-Bold.ttf) format("truetype")}@font-face{font-family:KaTeX_Caligraphic;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Caligraphic-Regular.woff2) format("woff2"),url(fonts/KaTeX_Caligraphic-Regular.woff) format("woff"),url(fonts/KaTeX_Caligraphic-Regular.ttf) format("truetype")}@font-face{font-family:KaTeX_Fraktur;font-style:normal;font-weight:700;src:url(fonts/KaTeX_Fraktur-Bold.woff2) format("woff2"),url(fonts/KaTeX_Fraktur-Bold.woff) format("woff"),url(fonts/KaTeX_Fraktur-Bold.ttf) format("truetype")}@font-face{font-family:KaTeX_Fraktur;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Fraktur-Regular.woff2) format("woff2"),url(fonts/KaTeX_Fraktur-Regular.woff) format("woff"),url(fonts/KaTeX_Fraktur-Regular.ttf) format("truetype")}@font-face{font-family:KaTeX_Main;font-style:normal;font-weight:700;src:url(fonts/KaTeX_Main-Bold.woff2) format("woff2"),url(fonts/KaTeX_Main-Bold.woff) format("woff"),url(fonts/KaTeX_Main-Bold.ttf) format("truetype")}@font-face{font-family:KaTeX_Main;font-style:italic;font-weight:700;src:url(fonts/KaTeX_Main-BoldItalic.woff2) format("woff2"),url(fonts/KaTeX_Main-BoldItalic.woff) format("woff"),url(fonts/KaTeX_Main-BoldItalic.ttf) format("truetype")}@font-face{font-family:KaTeX_Main;font-style:italic;font-weight:400;src:url(fonts/KaTeX_Main-Italic.woff2) format("woff2"),url(fonts/KaTeX_Main-Italic.woff) format("woff"),url(fonts/KaTeX_Main-Italic.ttf) format("truetype")}@font-face{font-family:KaTeX_Main;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Main-Regular.woff2) format("woff2"),url(fonts/KaTeX_Main-Regular.woff) format("woff"),url(fonts/KaTeX_Main-Regular.ttf) format("truetype")}@font-face{font-family:KaTeX_Math;font-style:italic;font-weight:700;src:url(fonts/KaTeX_Math-BoldItalic.woff2) format("woff2"),url(fonts/KaTeX_Math-BoldItalic.woff) format("woff"),url(fonts/KaTeX_Math-BoldItalic.ttf) format("truetype")}@font-face{font-family:KaTeX_Math;font-style:italic;font-weight:400;src:url(fonts/KaTeX_Math-Italic.woff2) format("woff2"),url(fonts/KaTeX_Math-Italic.woff) format("woff"),url(fonts/KaTeX_Math-Italic.ttf) format("truetype")}@font-face{font-family:"KaTeX_SansSerif";font-style:normal;font-weight:700;src:url(fonts/KaTeX_SansSerif-Bold.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Bold.woff) format("woff"),url(fonts/KaTeX_SansSerif-Bold.ttf) format("truetype")}@font-face{font-family:"KaTeX_SansSerif";font-style:italic;font-weight:400;src:url(fonts/KaTeX_SansSerif-Italic.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Italic.woff) format("woff"),url(fonts/KaTeX_SansSerif-Italic.ttf) format("truetype")}@font-face{font-family:"KaTeX_SansSerif";font-style:normal;font-weight:400;src:url(fonts/KaTeX_SansSerif-Regular.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Regular.woff) format("woff"),url(fonts/KaTeX_SansSerif-Regular.ttf) format("truetype")}@font-face{font-family:KaTeX_Script;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Script-Regular.woff2) format("woff2"),url(fonts/KaTeX_Script-Regular.woff) format("woff"),url(fonts/KaTeX_Script-Regular.ttf) format("truetype")}@font-face{font-family:KaTeX_Size1;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size1-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size1-Regular.woff) format("woff"),url(fonts/KaTeX_Size1-Regular.ttf) format("truetype")}@font-face{font-family:KaTeX_Size2;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size2-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size2-Regular.woff) format("woff"),url(fonts/KaTeX_Size2-Regular.ttf) format("truetype")}@font-face{font-family:KaTeX_Size3;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size3-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size3-Regular.woff) format("woff"),url(fonts/KaTeX_Size3-Regular.ttf) format("truetype")}@font-face{font-family:KaTeX_Size4;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size4-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size4-Regular.woff) format("woff"),url(fonts/KaTeX_Size4-Regular.ttf) format("truetype")}@font-face{font-family:KaTeX_Typewriter;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Typewriter-Regular.woff2) format("woff2"),url(fonts/KaTeX_Typewriter-Regular.woff) format("woff"),url(fonts/KaTeX_Typewriter-Regular.ttf) format("truetype")}.katex{font:normal 1.21em KaTeX_Main,Times New Roman,serif;line-height:1.2;text-indent:0;text-rendering:auto}.katex *{-ms-high-contrast-adjust:none!important;border-color:currentColor}.katex .katex-version:after{content:"0.16.11"}.katex .katex-mathml{clip:rect(1px,1px,1px,1px);border:0;height:1pLine truncated
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.katex=t():e.katex=t()}("undefined"!=typeof self?self:this,(function(){return function(){"use strict";var e={d:function(t,r){for(var n in r)e.o(r,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:r[n]})},o:function(e,t){return Object.prototype.hasOwnProperty.call(e,t)}},t={};e.d(t,{default:function(){return Yn}});class r{constructor(e,t){this.name=void 0,this.position=void 0,this.length=void 0,this.rawMessage=void 0;let n,o,s="KaTeX parse error: "+e;const i=t&&t.loc;if(i&&i.start<=i.end){const e=i.lexer.input;n=i.start,o=i.end,n===e.length?s+=" at end of input: ":s+=" at position "+(n+1)+": ";const t=e.slice(n,o).replace(/[^]/g,"$&\u0332");let r,a;r=n>15?"\u2026"+e.slice(n-15,n):e.slice(0,n),a=o+15<e.length?e.slice(o,o+15)+"\u2026":e.slice(o),s+=r+t+a}const a=new Error(s);return a.name="ParseError",a.__proto__=r.prototype,a.position=n,null!=n&&null!=o&&(a.length=o-n),a.rawMessage=e,a}}r.prototype.__proto__=Error.prototype;var n=r;const o=/([A-Z])/g,s={"&":"&",">":">","<":"<",'"':""","'":"'"},i=/[&><"']/g;const a=function(e){return"ordgroup"===e.type||"color"===e.type?1===e.body.length?a(e.body[0]):e:"font"===e.type?a(e.body):e};var l={contains:function(e,t){return-1!==e.indexOf(t)},deflt:function(e,t){return void 0===e?t:e},escape:function(e){return String(e).replace(i,(e=>s[e]))},hyphenate:function(e){return e.replace(o,"-$1").toLowerCase()},getBaseElem:a,isCharacterBox:function(e){const t=a(e);return"mathord"===t.type||"textord"===t.type||"atom"===t.type},protocolFromUrl:function(e){const t=/^[\x00-\x20]*([^\\/#?]*?)(:|�*58|�*3a|&colon)/i.exec(e);return t?":"!==t[2]?null:/^[a-zA-Z][a-zA-Z0-9+\-.]*$/.test(t[1])?t[1].toLowerCase():null:"_relative"}};const h={displayMode:{type:"boolean",description:"Render math in display mode, which puts the math in display style (so \\int and \\sum are large, for example), and centers the math on the page on its own line.",cli:"-d, --display-mode"},output:{type:{enum:["htmlAndMathml","html","mathml"]},description:"Determines the markup language of the output.",cli:"-F, --format <type>"},leqno:{type:"boolean",description:"Render display math in leqno style (left-justified tags)."},fleqn:{type:"boolean",description:"Render display math flush left."},throwOnError:{type:"boolean",default:!0,cli:"-t, --no-throw-on-error",cliDescription:"Render errors (in the color given by --error-color) instead of throwing a ParseError exception when encountering an error."},errorColor:{type:"string",default:"#cc0000",cli:"-c, --error-color <color>",cliDescription:"A color string given in the format 'rgb' or 'rrggbb' (no #). This option determines the color of errors rendered by the -t option.",cliProcessor:e=>"#"+e},macros:{type:"object",cli:"-m, --macro <def>",cliDescription:"Define custom macro of the form '\\foo:expansion' (use multiple -m arguments for multiple macros).",cliDefault:[],cliProcessor:(e,t)=>(t.push(e),t)},minRuleThickness:{type:"number",description:"Specifies a minimum thickness, in ems, for fraction lines, `\\sqrt` top lines, `{array}` vertical lines, `\\hline`, `\\hdashline`, `\\underline`, `\\overline`, and the borders of `\\fbox`, `\\boxed`, and `\\fcolorbox`.",processor:e=>Math.max(0,e),cli:"--min-rule-thickness <size>",cliProcessor:parseFloat},colorIsTextColor:{type:"boolean",description:"Makes \\color behave like LaTeX's 2-argument \\textcolor, instead of LaTeX's one-argument \\color mode change.",cli:"-b, --color-is-text-color"},strict:{type:[{enum:["warn","ignore","error"]},"boolean","function"],description:"Turn on strict / LaTeX faithfulness mode, which throws an error if the input uses features that are not supported by LaTeX.",cli:"-S, --strict",cliDefault:!1},trust:{type:["boolean","function"],description:"Trust the input, enabling all HTML features such as \\url.",cli:"-T, --trust"},maxSize:{type:"number",default:1/0,description:"If non-zero, all user-specified sizes, e.g. in \\rule{500em}{500em}, will be capped to maxSize ems. Otherwise, elements and spaces can be arbitrarily large",processor:e=>Math.max(0,e),cli:"-s, --max-size <n>",cliProcessor:parseInt},maxExpand:{type:"number",default:1e3,description:"Limit the number of macro expansions to the specified number, to prevent e.g. infinite macro loops. If set to Infinity, the macro expander will try to fully expand as in LaTeX.",processor:e=>Math.max(0,e),cli:"-e, --max-expand <n>",cliProcessor:e=>"Infinity"===e?1/0:parseInt(e)},globalGroup:{type:"boolean",cli:!1}};function c(e){if(e.default)return e.default;const t=e.type,r=Array.isArray(t)?t[0]:t;if("string"!=typeof r)return r.enum[0];switch(r){case"boolean":return!1;case"string":return"";case"number":return 0;case"object":return{}}}class m{constructor(e){this.displayMode=void 0,this.output=void 0,this.leqno=void 0,this.fleqn=void 0,this.throwOnError=void 0Line truncated
|
||||
@@ -488,17 +488,6 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
|
||||
<div id="mcp-block" class="gated-group">
|
||||
<div class="subhead">Serveurs MCP<span id="mcp-badge" class="sbadge"></span><span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Connecte des <b>serveurs MCP</b> (Model Context Protocol) pour donner de nouveaux outils à l'IA : accès fichiers, bases de données, APIs métier… <b>stdio</b> = programme lancé localement (npx/uvx/binaire) · <b>http</b> = serveur distant. ⚠ un serveur stdio exécute du code sur cette machine, au même titre que le shell.</span></span></div>
|
||||
<div class="slist">
|
||||
<!-- Délais communs à tous les serveurs (AJEAN c05a0a9f, e44b6139),
|
||||
enregistrés à chaque modification. Un serveur peut remplacer le
|
||||
délai d'appel dans sa fenêtre d'édition. -->
|
||||
<div class="pe-row">
|
||||
<span class="pe-row-l">Délai de connexion<span class="pe-sub">pour joindre un serveur ; au moins 3 min au premier lancement npx/uvx</span></span>
|
||||
<span class="pe-row-c"><input id="mcp-timeout-connect" class="pe-val" type="number" min="1" max="3600" step="1" value="20" style="width:54px" onchange="saveMcpTimeouts()"><span class="pe-unit">s</span></span>
|
||||
</div>
|
||||
<div class="pe-row">
|
||||
<span class="pe-row-l">Délai d'un appel d'outil<span class="pe-sub">réglable aussi serveur par serveur</span></span>
|
||||
<span class="pe-row-c"><input id="mcp-timeout-call" class="pe-val" type="number" min="1" max="3600" step="1" value="120" style="width:54px" onchange="saveMcpTimeouts()"><span class="pe-unit">s</span></span>
|
||||
</div>
|
||||
<div class="rowbody">
|
||||
<!-- Même texte que celui rendu après chargement quand il n'y a aucun
|
||||
serveur : sinon la carte partait vide puis grandissait d'une ligne. -->
|
||||
@@ -1018,10 +1007,6 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
|
||||
<textarea id="mcp-headers" class="ctl ta" spellcheck="false" style="min-height:44px" placeholder="Authorization: Bearer sk-…"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fld">
|
||||
<label class="fld-l" for="mcp-call-timeout">Délai d'un appel d'outil <span class="opt">(secondes, 1 à 3600 — vide = réglage global)</span></label>
|
||||
<input id="mcp-call-timeout" class="ctl" type="number" min="1" max="3600" step="1" inputmode="numeric" placeholder="réglage global">
|
||||
</div>
|
||||
<div id="mcp-modal-status" class="muted" style="font-size:12px;min-height:16px"></div>
|
||||
<div id="mcp-tools-section" style="display:none">
|
||||
<div class="subhead" style="margin-top:6px">Outils <span id="mcp-tools-count" class="muted" style="text-transform:none;letter-spacing:0;font-weight:400;font-family:var(--font)"></span><span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Décoche un outil pour le masquer à l'IA sans déconnecter le serveur. Utile pour ne garder que ce dont tu as besoin.</span></span></div>
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// --- Formules LaTeX (KaTeX) ---------------------------------------------------
|
||||
// Repris d'AJEAN (#123). md() (00-md.js) met les formules de côté AVANT marked,
|
||||
// qui abîmerait \\, _ et * : un repère (caractères à usage privé U+E000/U+E001
|
||||
// autour du numéro) prend leur place, puis reçoit le rendu. Le code (blocs et
|
||||
// `en ligne`) n'est jamais touché. Formes reconnues : $$…$$ et \[…\] (bloc),
|
||||
// \(…\) et $…$ (en ligne). $…$ n'est une formule que s'il ne commence ni ne finit
|
||||
// par une espace, n'est pas collé à un mot ni échappé (\$) avant, et n'est pas
|
||||
// suivi d'un chiffre : « 5 $ et 10 $ » ou « $5 et $10 » restent du texte.
|
||||
// Ajouts de Loki : un $ ouvrant précédé d'un chiffre et d'une espace (« 5 $, »)
|
||||
// est un montant, et $…$ ne contient pas de ` — sans quoi « 5 $, code `$x$` »
|
||||
// s'apparierait par-dessus le code en ligne.
|
||||
// KaTeX (embarqué, servi sous /katex/ par web_katex.go et la façade) n'est chargé
|
||||
// qu'à la première formule ; d'ici là la source s'affiche, puis est rendue sur
|
||||
// place. (Pas de lookbehind dans l'expression : les iOS d'avant 16.4 refusent de
|
||||
// charger le script entier. Ces contrôles-là sont faits dans le remplacement.)
|
||||
//
|
||||
// MATH_RE et mathExtract sont purs (ni DOM ni réseau) : web_katex_math_test.go les
|
||||
// exécute sous node sur les cas limites (montants, code, échappements).
|
||||
const MATH_RE = /(```[\s\S]*?(?:```|$)|~~~[\s\S]*?(?:~~~|$)|`[^`\n]*`)|\$\$([\s\S]+?)\$\$|\\\[([\s\S]+?)\\\]|\\\(([\s\S]+?)\\\)|\$(?![\s$])((?:\\.|[^$\\\n`])+?)\$(?![\d$])/g;
|
||||
const MATH_MARK_RE = /\uE000(\d+)\uE001/g;
|
||||
function mathExtract(s){
|
||||
const list = [];
|
||||
const text = s.replace(MATH_RE, (all, code, dd, br, pa, d, at)=>{
|
||||
if(code !== undefined) return all;
|
||||
// $…$ : pas collé à un mot ni échappé (\$) avant, pas un montant (« 5 $ »),
|
||||
// pas d'espace avant le $ final.
|
||||
if(d !== undefined && (/[\\$\w]/.test(s[at-1] || '') || /\d\s$/.test(s.slice(Math.max(0, at-2), at)) || /\s$/.test(d))) return all;
|
||||
const display = dd !== undefined || br !== undefined;
|
||||
list.push({tex: (dd ?? br ?? pa ?? d).trim(), display});
|
||||
return '\uE000' + (list.length-1) + '\uE001';
|
||||
});
|
||||
return {text, list};
|
||||
}
|
||||
// mathRestore remplace chaque repère du HTML produit par marked par le rendu de
|
||||
// sa formule (render : formule → HTML).
|
||||
function mathRestore(html, list, render){
|
||||
return list.length ? html.replace(MATH_MARK_RE, (all, i)=>list[+i] ? render(list[+i]) : all) : html;
|
||||
}
|
||||
const mathEsc = (s)=>s.replace(/[&<>"]/g, c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
|
||||
const MATH_OPTS = {throwOnError: false, output: 'html'};
|
||||
function mathHtml(f){
|
||||
if(window.katex){
|
||||
try{ return katex.renderToString(f.tex, {...MATH_OPTS, displayMode: f.display}); }
|
||||
catch(_){ return '<code>'+mathEsc(f.tex)+'</code>'; }
|
||||
}
|
||||
mathLoad();
|
||||
return '<span class="math-pending" data-d="'+(f.display?1:0)+'">'+mathEsc(f.tex)+'</span>';
|
||||
}
|
||||
let mathLoading = null;
|
||||
function mathLoad(){
|
||||
if(mathLoading) return mathLoading;
|
||||
if(!document.querySelector('link[data-katex]')){
|
||||
const css = document.createElement('link');
|
||||
css.rel = 'stylesheet'; css.href = 'katex/katex.min.css'; css.dataset.katex = '1';
|
||||
document.head.appendChild(css);
|
||||
}
|
||||
mathLoading = new Promise((ok, ko)=>{
|
||||
const s = document.createElement('script');
|
||||
s.src = 'katex/katex.min.js'; s.onload = ok;
|
||||
// Échec (hors ligne, 404) : script retiré, nouvel essai à la prochaine formule.
|
||||
s.onerror = ()=>{ s.remove(); ko(); };
|
||||
document.head.appendChild(s);
|
||||
}).then(()=>{
|
||||
document.querySelectorAll('.math-pending').forEach(el=>{
|
||||
try{ katex.render(el.textContent, el, {...MATH_OPTS, displayMode: el.dataset.d === '1'}); }catch(_){}
|
||||
el.classList.remove('math-pending');
|
||||
});
|
||||
}).catch(()=>{ mathLoading = null; });
|
||||
return mathLoading;
|
||||
}
|
||||
@@ -14,7 +14,13 @@ function fixMd(s){
|
||||
s = s.replace(/\n{3,}/g, '\n\n');
|
||||
return s;
|
||||
}
|
||||
function md(src){ return src ? marked.parse(fixMd(src)) : ''; }
|
||||
// Les formules LaTeX sont mises de côté avant marked puis rendues par KaTeX
|
||||
// (00-math.js) ; le code et les montants en $ ne sont pas touchés.
|
||||
function md(src){
|
||||
if(!src) return '';
|
||||
const m = mathExtract(fixMd(src));
|
||||
return mathRestore(marked.parse(m.text), m.list, mathHtml);
|
||||
}
|
||||
// Les garde-fous du serveur ("[stop: trop d'appels d'outils]") sont concaténés
|
||||
// au texte de la réponse : ils arrivent donc en clair, au milieu du markdown.
|
||||
// markNotices les sort du fil pour qu'on ne les prenne pas pour une phrase du
|
||||
|
||||
@@ -5,47 +5,7 @@
|
||||
let mcpServers = [];
|
||||
let mcpEditing = null; // nom du serveur en cours d'édition ('' ou null = nouveau)
|
||||
|
||||
async function loadMCP(){
|
||||
renderMCP(await jget('/api/mcp'));
|
||||
loadMcpTimeouts();
|
||||
}
|
||||
|
||||
// ─── Délais globaux ──────────────────────────────────────────────────────────
|
||||
// Connexion et appel d'outil, communs à tous les serveurs (/api/mcp/timeouts).
|
||||
// Un seul enregistrement par modification : « change » part à la validation
|
||||
// ou à la perte du focus, pas à chaque frappe.
|
||||
let mcpTimeouts = {connect: 20, call: 120};
|
||||
|
||||
async function loadMcpTimeouts(){
|
||||
const r = await jget('/api/mcp/timeouts');
|
||||
if(!r || !r.ok) return;
|
||||
mcpTimeouts = {connect: r.connect, call: r.call};
|
||||
document.getElementById('mcp-timeout-connect').value = r.connect;
|
||||
document.getElementById('mcp-timeout-call').value = r.call;
|
||||
mcpCallTimeoutPlaceholder();
|
||||
}
|
||||
|
||||
async function saveMcpTimeouts(){
|
||||
const connect = Number(document.getElementById('mcp-timeout-connect').value);
|
||||
const call = Number(document.getElementById('mcp-timeout-call').value);
|
||||
const ok = v => Number.isInteger(v) && v >= 1 && v <= 3600;
|
||||
if(!ok(connect) || !ok(call)){
|
||||
toast('délai invalide : de 1 à 3600 s');
|
||||
loadMcpTimeouts(); // revient aux valeurs enregistrées
|
||||
return;
|
||||
}
|
||||
const r = await jpost('/api/mcp/timeouts/save', {connect, call});
|
||||
if(!r || !r.ok){ toast('erreur : '+((r && r.error) || 'délais non enregistrés')); loadMcpTimeouts(); return; }
|
||||
mcpTimeouts = {connect: r.connect, call: r.call};
|
||||
mcpCallTimeoutPlaceholder();
|
||||
toast('délais MCP enregistrés');
|
||||
}
|
||||
|
||||
// Le champ « délai d'un appel » de la fenêtre d'édition montre le réglage
|
||||
// global qu'il remplacerait.
|
||||
function mcpCallTimeoutPlaceholder(){
|
||||
document.getElementById('mcp-call-timeout').placeholder = 'réglage global ('+mcpTimeouts.call+' s)';
|
||||
}
|
||||
async function loadMCP(){ renderMCP(await jget('/api/mcp')); }
|
||||
|
||||
function renderMCP(r){
|
||||
mcpServers = (r && r.servers) || [];
|
||||
@@ -218,7 +178,6 @@ function openMcpFromCatalog(e){
|
||||
Object.entries(e.env||{}).map(([k,v])=>k+'='+v).join('\n');
|
||||
document.getElementById('mcp-url').value = '';
|
||||
document.getElementById('mcp-headers').value = '';
|
||||
document.getElementById('mcp-call-timeout').value = '';
|
||||
document.getElementById('mcp-modal-title').textContent = e.name+' — depuis le catalogue';
|
||||
}
|
||||
const st = document.getElementById('mcp-modal-status');
|
||||
@@ -246,8 +205,6 @@ function openMcp(name){
|
||||
document.getElementById('mcp-env').value = s && s.env ? Object.entries(s.env).map(([k,v])=>k+'='+v).join('\n') : '';
|
||||
document.getElementById('mcp-url').value = s ? (s.url||'') : '';
|
||||
document.getElementById('mcp-headers').value = s && s.headers ? Object.entries(s.headers).map(([k,v])=>k+': '+v).join('\n') : '';
|
||||
document.getElementById('mcp-call-timeout').value = s && s.callTimeoutSec ? s.callTimeoutSec : '';
|
||||
mcpCallTimeoutPlaceholder();
|
||||
const st = document.getElementById('mcp-modal-status');
|
||||
st.textContent = ''; st.style.color = '';
|
||||
if(s && s.enabled && s.error){ st.textContent = '⚠ '+s.error; st.style.color = 'var(--err)'; }
|
||||
@@ -326,13 +283,6 @@ async function saveMcp(){
|
||||
body.headers = mcpParseKV(document.getElementById('mcp-headers').value, ':');
|
||||
body.command = ''; body.args = undefined; body.env = undefined;
|
||||
}
|
||||
// Vide = réglage global (0 côté serveur).
|
||||
const ct = document.getElementById('mcp-call-timeout').value.trim();
|
||||
if(ct){
|
||||
const n = Number(ct);
|
||||
if(!Number.isInteger(n) || n < 1 || n > 3600){ toast("délai d'un appel d'outil : de 1 à 3600 s, ou vide"); return; }
|
||||
body.callTimeoutSec = n;
|
||||
}
|
||||
const st = document.getElementById('mcp-modal-status');
|
||||
st.textContent = 'connexion…'; st.style.color = '';
|
||||
const r = await jpost('/api/mcp/save', body);
|
||||
|
||||
@@ -1383,6 +1383,10 @@ button:hover{border-color:var(--dim);color:var(--text)}
|
||||
border:1px solid var(--border);border-radius:999px;background:var(--panel);
|
||||
color:var(--dim);font-size:12px;line-height:1.5}
|
||||
.stopnote::before{content:"";flex:none;width:6px;height:6px;border-radius:50%;background:var(--dim)}
|
||||
/* Formules LaTeX (KaTeX, 00-math.js) : une longue formule défile au lieu de déborder. */
|
||||
.katex{font-size:1.08em}
|
||||
.katex-display{margin:.6em 0;overflow-x:auto;overflow-y:hidden;padding:2px 0}
|
||||
.math-pending{font-family:var(--mono);font-size:.92em;color:var(--dim)}
|
||||
|
||||
/* ===== Fenêtres et sélecteurs =====================================
|
||||
Coins arrondis et ombre douce plutôt qu'un cadre marqué. */
|
||||
|
||||
@@ -1166,9 +1166,6 @@ type mcpSaveReq struct {
|
||||
URL string `json:"url"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
// CallTimeoutSec : délai d'un appel propre au serveur, en secondes
|
||||
// (0 ou absent = réglage global).
|
||||
CallTimeoutSec int `json:"callTimeoutSec"`
|
||||
}
|
||||
|
||||
// handleMCP renvoie l'état de tous les serveurs MCP configurés (connexion tentée
|
||||
@@ -1244,10 +1241,6 @@ func handleMCPSave(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
if msg := mcpCallTimeoutSecError(req.CallTimeoutSec); msg != "" {
|
||||
sendJSON(w, 400, map[string]any{"ok": false, "error": msg})
|
||||
return
|
||||
}
|
||||
enabled := true
|
||||
if req.Enabled != nil {
|
||||
enabled = *req.Enabled
|
||||
@@ -1259,8 +1252,6 @@ func handleMCPSave(w http.ResponseWriter, r *http.Request) {
|
||||
URL: strings.TrimSpace(req.URL),
|
||||
Headers: req.Headers,
|
||||
Enabled: enabled,
|
||||
|
||||
CallTimeoutSec: req.CallTimeoutSec,
|
||||
}
|
||||
if err := SetMCPServer(req.Name, cfg); err != nil {
|
||||
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
|
||||
@@ -1343,36 +1334,6 @@ func handleMCPTest(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, 200, map[string]any{"ok": true, "servers": list})
|
||||
}
|
||||
|
||||
// handleMCPTimeouts renvoie les délais MCP globaux (secondes).
|
||||
func handleMCPTimeouts(w http.ResponseWriter, r *http.Request) {
|
||||
t := LoadMCPTimeouts()
|
||||
sendJSON(w, 200, map[string]any{"ok": true, "connect": t.Connect, "call": t.Call})
|
||||
}
|
||||
|
||||
// handleMCPTimeoutsSave enregistre les délais MCP globaux (1 à 3600 s) et
|
||||
// ferme les sessions ouvertes, pour que le délai de connexion s'applique dès
|
||||
// la suivante.
|
||||
func handleMCPTimeoutsSave(w http.ResponseWriter, r *http.Request) {
|
||||
var req MCPTimeouts
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
if msg := mcpTimeoutsError(req); msg != "" {
|
||||
sendJSON(w, 400, map[string]any{"ok": false, "error": msg})
|
||||
return
|
||||
}
|
||||
if err := SaveMCPTimeouts(req); err != nil {
|
||||
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
servers, _ := LoadMCPConfig()
|
||||
for name := range servers {
|
||||
mcpInvalidate(name)
|
||||
}
|
||||
sendJSON(w, 200, map[string]any{"ok": true, "connect": req.Connect, "call": req.Call})
|
||||
}
|
||||
|
||||
// handleMem / handleMemSave / handleMemDelete : éditeur web des pages mémoire
|
||||
// (memory/<nom>.md). Payload partagé saveReq (name/old/content) ; "name" = nom
|
||||
// de fichier de la page.
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// web_katex.go — KaTeX 0.16.11 (formules LaTeX des réponses), repris d'AJEAN.
|
||||
//
|
||||
// Embarqué sous ui/katex/ et servi sous /katex/ : l'UI ne le charge qu'à la
|
||||
// première formule d'une réponse (00-md.js), et il doit marcher hors ligne,
|
||||
// sans appeler un CDN. Seuls le script, la feuille de style et les polices
|
||||
// woff2 sont servis — pas d'index de dossier — avec un Content-Type fixe : le
|
||||
// registre MIME de l'OS (Windows notamment) ne doit pas le décider, et la
|
||||
// façade Rust (rust/loki-rs/src/ui.rs) rend exactement les mêmes en-têtes.
|
||||
|
||||
// katexTypes : extensions servies et leur Content-Type.
|
||||
var katexTypes = map[string]string{
|
||||
".js": "application/javascript",
|
||||
".css": "text/css; charset=utf-8",
|
||||
".woff2": "font/woff2",
|
||||
}
|
||||
|
||||
// katexFile : chemin relatif à ui/katex/ d'une ressource servable, ou "" si la
|
||||
// demande n'en est pas une (racine, dossier, autre extension, remontée).
|
||||
func katexFile(urlPath string) (rel, ctype string) {
|
||||
rel = strings.TrimPrefix(urlPath, "/katex/")
|
||||
if rel == urlPath || rel == "" || strings.Contains(rel, "..") || path.Clean(rel) != rel {
|
||||
return "", ""
|
||||
}
|
||||
ctype, ok := katexTypes[path.Ext(rel)]
|
||||
if !ok {
|
||||
return "", ""
|
||||
}
|
||||
return rel, ctype
|
||||
}
|
||||
|
||||
func handleKatex(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
rel, ctype := katexFile(r.URL.Path)
|
||||
if rel == "" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
b, err := uiFS.ReadFile("ui/katex/" + rel)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", ctype)
|
||||
w.Header().Set("Cache-Control", "public, max-age=86400")
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(b)))
|
||||
w.Write(b)
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Détection des formules (ui/src/js/00-math.js), exécutée sous node : le dépôt
|
||||
// n'a pas de banc de tests JS, et MATH_RE/mathExtract sont purs. Ignoré si node
|
||||
// n'est pas installé (il l'est sur les runners de la CI).
|
||||
|
||||
type formule struct {
|
||||
Tex string `json:"tex"`
|
||||
Display bool `json:"display"`
|
||||
}
|
||||
|
||||
// harnaisMath : charge 00-math.js et marked.min.js, puis pour chaque entrée
|
||||
// (JSON sur stdin) rend {text, list} de mathExtract et, pour le bout en bout, le
|
||||
// HTML de marked après mathRestore (chaque formule rendue « [tex] »).
|
||||
const harnaisMath = `
|
||||
const fs = require('fs');
|
||||
const [mathPath, markedPath] = process.argv.slice(2);
|
||||
const m = new Function(fs.readFileSync(mathPath, 'utf8') + '; return {mathExtract, mathRestore};')();
|
||||
const {marked} = require(markedPath);
|
||||
marked.setOptions({ gfm: true, breaks: false });
|
||||
const cases = JSON.parse(fs.readFileSync(0, 'utf8'));
|
||||
console.log(JSON.stringify(cases.map(s => {
|
||||
const r = m.mathExtract(s);
|
||||
const html = m.mathRestore(marked.parse(r.text), r.list, f => '[' + (f.display ? 'D:' : '') + f.tex + ']');
|
||||
return {text: r.text, list: r.list, html};
|
||||
})));
|
||||
`
|
||||
|
||||
type résultatMath struct {
|
||||
Text string `json:"text"`
|
||||
List []formule `json:"list"`
|
||||
HTML string `json:"html"`
|
||||
}
|
||||
|
||||
func lanceMath(t *testing.T, cas []string) []résultatMath {
|
||||
t.Helper()
|
||||
node, err := exec.LookPath("node")
|
||||
if err != nil {
|
||||
t.Skip("node absent : détection des formules non testée")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
h := filepath.Join(dir, "harnais.js")
|
||||
if err := os.WriteFile(h, []byte(harnaisMath), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
math, _ := filepath.Abs("ui/src/js/00-math.js")
|
||||
marked, _ := filepath.Abs("ui/marked.min.js")
|
||||
in, _ := json.Marshal(cas)
|
||||
cmd := exec.Command(node, h, math, marked)
|
||||
cmd.Stdin = strings.NewReader(string(in))
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("node : %v\n%s", err, out)
|
||||
}
|
||||
var res []résultatMath
|
||||
if err := json.Unmarshal(out, &res); err != nil {
|
||||
t.Fatalf("sortie illisible : %v\n%s", err, out)
|
||||
}
|
||||
if len(res) != len(cas) {
|
||||
t.Fatalf("%d résultats pour %d cas", len(res), len(cas))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func TestMathDetection(t *testing.T) {
|
||||
in := func(tex string) formule { return formule{Tex: tex} }
|
||||
bloc := func(tex string) formule { return formule{Tex: tex, Display: true} }
|
||||
cas := []struct {
|
||||
src string
|
||||
want []formule
|
||||
}{
|
||||
// Montants : jamais des formules.
|
||||
{"Ça coûte 5 $ et 10 $.", nil},
|
||||
{"Entre $5 et $10 par mois.", nil},
|
||||
{"Prix : 5$ ou 6$, voire 7 $.", nil},
|
||||
{`Échappés : \$5 et \$6.`, nil},
|
||||
{"Collé à un mot : x$y$z.", nil},
|
||||
{"Espace finale : $x $.", nil},
|
||||
{"De 5 $, à 10$.", nil},
|
||||
{"Montant : 5 $, code `$x_1$`.", nil},
|
||||
// Code : jamais touché.
|
||||
{"En ligne `$x$` et `$$y$$`.", nil},
|
||||
{"```\n$x$ et $$y$$\n```", nil},
|
||||
{"~~~tex\n\\[a\\]\n~~~", nil},
|
||||
{"```\nbloc non fermé $x$", nil},
|
||||
// Formules.
|
||||
{"Soit $x^2 + y^2 = z^2$ ici.", []formule{in("x^2 + y^2 = z^2")}},
|
||||
{"Deux : $a$, $b$.", []formule{in("a"), in("b")}},
|
||||
{"$$\n\\int_0^1 x\\,dx\n$$", []formule{bloc(`\int_0^1 x\,dx`)}},
|
||||
{`En ligne \(a+b\) et en bloc \[c\].`, []formule{in("a+b"), bloc("c")}},
|
||||
{"Code `$x$` puis $y$.", []formule{in("y")}},
|
||||
{`Échappement dedans : $a\$b$.`, []formule{in(`a\$b`)}},
|
||||
}
|
||||
srcs := make([]string, len(cas))
|
||||
for i, c := range cas {
|
||||
srcs[i] = c.src
|
||||
}
|
||||
res := lanceMath(t, srcs)
|
||||
for i, c := range cas {
|
||||
got := res[i].List
|
||||
if len(got) == 0 {
|
||||
got = nil
|
||||
}
|
||||
if !reflect.DeepEqual(got, c.want) {
|
||||
t.Errorf("%q : %+v, attendu %+v", c.src, got, c.want)
|
||||
}
|
||||
if c.want == nil && res[i].Text != c.src {
|
||||
t.Errorf("%q : texte modifié en %q", c.src, res[i].Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bout en bout avec marked : \\, _ et * d'une formule arrivent intacts au rendu,
|
||||
// le markdown autour reste du markdown, et aucun repère ne survit.
|
||||
func TestMathAvecMarked(t *testing.T) {
|
||||
res := lanceMath(t, []string{
|
||||
"*Gras* : $a_1 * b_2$ et $$\\begin{matrix}a & b \\\\ c & d\\end{matrix}$$",
|
||||
"Montant : 5 $, code `$x_1$`.",
|
||||
})
|
||||
for _, want := range []string{"<em>Gras</em>", "[a_1 * b_2]", `[D:\begin{matrix}a & b \\ c & d\end{matrix}]`} {
|
||||
if !strings.Contains(res[0].HTML, want) {
|
||||
t.Errorf("HTML sans %q :\n%s", want, res[0].HTML)
|
||||
}
|
||||
}
|
||||
for _, r := range res {
|
||||
if strings.ContainsAny(r.HTML, "\uE000\uE001") {
|
||||
t.Errorf("repère resté dans le HTML : %q", r.HTML)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(res[1].HTML, "5 $") || !strings.Contains(res[1].HTML, "<code>$x_1$</code>") {
|
||||
t.Errorf("montant ou code abîmé : %s", res[1].HTML)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func getKatex(t *testing.T, méthode, chemin string) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
rec := httptest.NewRecorder()
|
||||
handleKatex(rec, httptest.NewRequest(méthode, chemin, nil))
|
||||
return rec
|
||||
}
|
||||
|
||||
// Le script, embarqué, est servi sous /katex/ avec les en-têtes que rend aussi
|
||||
// la façade Rust (ui.rs).
|
||||
func TestKatexScriptServi(t *testing.T) {
|
||||
rec := getKatex(t, http.MethodGet, "/katex/katex.min.js")
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("code %d", rec.Code)
|
||||
}
|
||||
h := rec.Header()
|
||||
if got := h.Get("Content-Type"); got != "application/javascript" {
|
||||
t.Errorf("Content-Type = %q", got)
|
||||
}
|
||||
if got := h.Get("Cache-Control"); got != "public, max-age=86400" {
|
||||
t.Errorf("Cache-Control = %q", got)
|
||||
}
|
||||
if !strings.Contains(rec.Body.String(), `version:"0.16.11"`) {
|
||||
t.Error("katex.min.js n'est pas KaTeX 0.16.11")
|
||||
}
|
||||
if h.Get("Content-Length") != strconv.Itoa(rec.Body.Len()) {
|
||||
t.Errorf("Content-Length = %q pour %d octets", h.Get("Content-Length"), rec.Body.Len())
|
||||
}
|
||||
}
|
||||
|
||||
// Chaque police woff2 que la feuille de style demande est embarquée et servie.
|
||||
func TestKatexFeuilleEtPolices(t *testing.T) {
|
||||
rec := getKatex(t, http.MethodGet, "/katex/katex.min.css")
|
||||
if rec.Code != http.StatusOK || rec.Header().Get("Content-Type") != "text/css; charset=utf-8" {
|
||||
t.Fatalf("css : code %d, type %q", rec.Code, rec.Header().Get("Content-Type"))
|
||||
}
|
||||
polices := regexp.MustCompile(`url\((fonts/[^)]+\.woff2)\)`).FindAllStringSubmatch(rec.Body.String(), -1)
|
||||
if len(polices) < 10 {
|
||||
t.Fatalf("%d polices woff2 dans la feuille", len(polices))
|
||||
}
|
||||
for _, m := range polices {
|
||||
r := getKatex(t, http.MethodGet, "/katex/"+m[1])
|
||||
if r.Code != http.StatusOK || r.Header().Get("Content-Type") != "font/woff2" || r.Body.Len() == 0 {
|
||||
t.Errorf("%s : code %d, type %q", m[1], r.Code, r.Header().Get("Content-Type"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rien d'autre : ni index de dossier, ni licence, ni remontée, ni POST.
|
||||
func TestKatexRefus(t *testing.T) {
|
||||
for _, c := range []struct{ méthode, chemin string }{
|
||||
{http.MethodGet, "/katex/"},
|
||||
{http.MethodGet, "/katex/fonts/"},
|
||||
{http.MethodGet, "/katex/LICENSE"},
|
||||
{http.MethodGet, "/katex/fonts/absente.woff2"},
|
||||
{http.MethodGet, "/katex/../index.html"},
|
||||
{http.MethodGet, "/katex/fonts/../katex.min.js"},
|
||||
{http.MethodGet, "/katexkatex.min.js"},
|
||||
{http.MethodPost, "/katex/katex.min.js"},
|
||||
} {
|
||||
if rec := getKatex(t, c.méthode, c.chemin); rec.Code != http.StatusNotFound {
|
||||
t.Errorf("%s %s : code %d, attendu 404", c.méthode, c.chemin, rec.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Derrière gzipHandler (comme en production) : script et feuille compressés,
|
||||
// polices non — comme la façade.
|
||||
func TestKatexCompression(t *testing.T) {
|
||||
h := gzipHandler(http.HandlerFunc(handleKatex))
|
||||
for chemin, gz := range map[string]bool{
|
||||
"/katex/katex.min.js": true,
|
||||
"/katex/katex.min.css": true,
|
||||
"/katex/fonts/KaTeX_Main-Regular.woff2": false,
|
||||
} {
|
||||
req := httptest.NewRequest(http.MethodGet, chemin, nil)
|
||||
req.Header.Set("Accept-Encoding", "gzip")
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
if got := rec.Header().Get("Content-Encoding") == "gzip"; got != gz {
|
||||
t.Errorf("%s : gzip %v, attendu %v", chemin, got, gz)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
)
|
||||
|
||||
//go:generate go run ../../tools/assemble-ui ui
|
||||
//go:embed ui/index.html ui/marked.min.js ui/sw.js ui/manifest.webmanifest ui/fonts/*.woff2
|
||||
//go:embed ui/index.html ui/marked.min.js ui/sw.js ui/manifest.webmanifest ui/fonts/*.woff2 ui/katex
|
||||
var uiFS embed.FS
|
||||
|
||||
// cmdWeb starts the HTTP server on the given port (default 8090).
|
||||
@@ -217,6 +217,9 @@ func newWebMux() *http.ServeMux {
|
||||
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
||||
w.Write(b)
|
||||
})
|
||||
// KaTeX (formules LaTeX des réponses, web_katex.go) : chargé par l'UI
|
||||
// seulement quand une réponse contient des maths. Embarqué, comme les polices.
|
||||
mux.HandleFunc("/katex/", handleKatex)
|
||||
// Endpoint compatible OpenAI, servi par Loki SUR SON PORT et relayé vers
|
||||
// llama-server (voir mountOAI). C'est ce qui le rend joignable par le nom de
|
||||
// domaine ou l'IP de l'interface, sans publier de second port ni ouvrir le
|
||||
@@ -334,8 +337,6 @@ func newWebMux() *http.ServeMux {
|
||||
api("/api/mcp/toggle", handleMCPToggle)
|
||||
api("/api/mcp/tool", handleMCPTool)
|
||||
api("/api/mcp/test", handleMCPTest)
|
||||
api("/api/mcp/timeouts", handleMCPTimeouts)
|
||||
api("/api/mcp/timeouts/save", handleMCPTimeoutsSave)
|
||||
api("/api/memory", handleMemoryMode)
|
||||
// Tâches planifiées : l'IA exécute des consignes toute seule sur une fréquence
|
||||
// réglable (tasks.go). Le planificateur tourne dans CE process — celui qui
|
||||
|
||||
@@ -42,6 +42,32 @@ fn main() {
|
||||
}
|
||||
out.push_str("];\n");
|
||||
|
||||
// KaTeX (formules LaTeX, repris d'AJEAN) : script, feuille et polices
|
||||
// woff2 sous ui/katex/, les polices nommées par leur chemin relatif
|
||||
// (« fonts/x.woff2 »), comme les sert web_katex.go.
|
||||
let katex = ui.join("katex");
|
||||
println!("cargo:rerun-if-changed={}", katex.display());
|
||||
for (ident, name) in [("KATEX_JS", "katex.min.js"), ("KATEX_CSS", "katex.min.css")] {
|
||||
let p = katex.join(name);
|
||||
assert!(p.is_file(), "fichier KaTeX manquant : {}", p.display());
|
||||
println!("cargo:rerun-if-changed={}", p.display());
|
||||
writeln!(out, "pub static {ident}: &[u8] = include_bytes!({p:?});").unwrap();
|
||||
}
|
||||
let katex_fonts = katex.join("fonts");
|
||||
println!("cargo:rerun-if-changed={}", katex_fonts.display());
|
||||
let mut kfonts: Vec<PathBuf> = std::fs::read_dir(&katex_fonts)
|
||||
.unwrap_or_else(|e| panic!("{} : {e}", katex_fonts.display()))
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().is_some_and(|x| x == "woff2"))
|
||||
.collect();
|
||||
kfonts.sort();
|
||||
out.push_str("pub static KATEX_FONTS: &[(&str, &[u8])] = &[\n");
|
||||
for p in &kfonts {
|
||||
let name = format!("fonts/{}", p.file_name().unwrap().to_str().unwrap());
|
||||
writeln!(out, " ({name:?}, include_bytes!({p:?})),").unwrap();
|
||||
}
|
||||
out.push_str("];\n");
|
||||
|
||||
let dst = PathBuf::from(std::env::var("OUT_DIR").unwrap()).join("ui_assets.rs");
|
||||
std::fs::write(dst, out).unwrap();
|
||||
}
|
||||
+32
-158
@@ -26,7 +26,7 @@ pub struct ServerConfig {
|
||||
#[serde(rename = "disabledTools", skip_serializing_if = "Vec::is_empty")]
|
||||
pub disabled_tools: Vec<String>,
|
||||
/// `CallTimeoutSec` : délai d'un appel d'outil de ce serveur, en
|
||||
/// secondes ; 0 = réglage global ([`Timeouts::call`]).
|
||||
/// secondes ; 0 = défaut ([`CALL_TIMEOUT`]).
|
||||
#[serde(rename = "callTimeoutSec", skip_serializing_if = "is_zero")]
|
||||
pub call_timeout_sec: i64,
|
||||
}
|
||||
@@ -285,107 +285,21 @@ pub fn prompt_line(tools: &[(String, String)]) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
/// `mcpConnectTimeout`.
|
||||
pub const CONNECT_TIMEOUT: Duration = Duration::from_secs(20);
|
||||
/// `mcpConnectTimeoutCold` : npx et uvx téléchargent le paquet au premier
|
||||
/// lancement ; un minimum, un délai de connexion réglé plus long l'emporte.
|
||||
/// lancement.
|
||||
pub const CONNECT_TIMEOUT_COLD: Duration = Duration::from_secs(180);
|
||||
/// `mcpTimeoutMaxSec` : borne haute des réglages (1 h).
|
||||
pub const TIMEOUT_MAX_SECS: i64 = 3600;
|
||||
/// `mcpCallTimeout`.
|
||||
pub const CALL_TIMEOUT: Duration = Duration::from_secs(120);
|
||||
/// `mcpCallTimeoutMax` : plafond d'un appel, quoi que demandent l'outil ou la
|
||||
/// config ; il suit la borne des réglages.
|
||||
pub const CALL_TIMEOUT_MAX: Duration = Duration::from_secs(TIMEOUT_MAX_SECS as u64);
|
||||
/// `mcpTimeoutsKey` : clé des délais globaux (`state/mcp_timeouts`).
|
||||
pub const ST_TIMEOUTS: &str = "mcp_timeouts";
|
||||
/// config.
|
||||
pub const CALL_TIMEOUT_MAX: Duration = Duration::from_secs(30 * 60);
|
||||
|
||||
/// `MCPTimeouts` : les délais globaux, en secondes (`mcp_timeouts.go`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Timeouts {
|
||||
pub connect: i64,
|
||||
pub call: i64,
|
||||
}
|
||||
|
||||
impl Default for Timeouts {
|
||||
/// 20 s de connexion, 120 s d'appel.
|
||||
fn default() -> Self {
|
||||
Timeouts {
|
||||
connect: 0,
|
||||
call: 0,
|
||||
}
|
||||
.normalized()
|
||||
}
|
||||
}
|
||||
|
||||
impl Timeouts {
|
||||
/// `normalized` : nul ou négatif = défaut, au-delà de la borne = la borne.
|
||||
pub fn normalized(self) -> Self {
|
||||
let fix = |v: i64, def: i64| if v <= 0 { def } else { v.min(TIMEOUT_MAX_SECS) };
|
||||
Timeouts {
|
||||
connect: fix(self.connect, 20),
|
||||
call: fix(self.call, 120),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connect(&self) -> Duration {
|
||||
Duration::from_secs(self.connect.max(0) as u64)
|
||||
}
|
||||
|
||||
pub fn call(&self) -> Duration {
|
||||
Duration::from_secs(self.call.max(0) as u64)
|
||||
}
|
||||
|
||||
/// `json.Marshal(MCPTimeouts)`.
|
||||
pub fn marshal(&self) -> String {
|
||||
format!(r#"{{"connect":{},"call":{}}}"#, self.connect, self.call)
|
||||
}
|
||||
}
|
||||
|
||||
/// Le type Go décodé (`MCPTimeouts`) et ses champs, pour [`crate::gojson`].
|
||||
pub const TIMEOUTS_TYPE: &str = "MCPTimeouts";
|
||||
pub const TIMEOUTS_FIELDS: [(&str, crate::gojson::Kind); 2] = [
|
||||
("connect", crate::gojson::Kind::Int),
|
||||
("call", crate::gojson::Kind::Int),
|
||||
];
|
||||
|
||||
/// `LoadMCPTimeouts` : `getJSON` puis normalisation. Une valeur que
|
||||
/// `json.Unmarshal` refuse, même en partie, donne les défauts.
|
||||
pub fn decode_timeouts(raw: Option<&[u8]>) -> Timeouts {
|
||||
let raw = raw.unwrap_or_default();
|
||||
if raw.is_empty() || serde_json::from_slice::<Value>(raw).is_err() {
|
||||
return Timeouts::default();
|
||||
}
|
||||
let d = crate::gojson::decode_struct(raw, TIMEOUTS_TYPE, &TIMEOUTS_FIELDS);
|
||||
if d.err.is_some() {
|
||||
return Timeouts::default();
|
||||
}
|
||||
Timeouts {
|
||||
connect: d.int("connect"),
|
||||
call: d.int("call"),
|
||||
}
|
||||
.normalized()
|
||||
}
|
||||
|
||||
/// `mcpTimeoutsError` : le refus d'un réglage hors bornes.
|
||||
pub fn timeouts_error(t: &Timeouts) -> Option<&'static str> {
|
||||
if !(1..=TIMEOUT_MAX_SECS).contains(&t.connect) {
|
||||
return Some("connexion : 1 à 3600 s");
|
||||
}
|
||||
if !(1..=TIMEOUT_MAX_SECS).contains(&t.call) {
|
||||
return Some("appel d'outil : 1 à 3600 s");
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// `mcpCallTimeoutSecError` : le refus d'un délai par serveur hors bornes.
|
||||
pub fn call_timeout_sec_error(sec: i64) -> Option<&'static str> {
|
||||
if !(0..=TIMEOUT_MAX_SECS).contains(&sec) {
|
||||
return Some("délai d'un appel : 0 à 3600 s (vide = réglage global)");
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// `mcpCallTimeoutFor` : le réglage global, remplacé par `callTimeoutSec` du
|
||||
/// `mcpCallTimeoutFor` : [`CALL_TIMEOUT`], remplacé par `callTimeoutSec` du
|
||||
/// serveur ; un argument `timeout` de l'outil l'allonge de 30 s.
|
||||
pub fn call_timeout_for(cfg: &ServerConfig, tm: &Timeouts, args: &Map<String, Value>) -> Duration {
|
||||
let mut d = tm.call();
|
||||
pub fn call_timeout_for(cfg: &ServerConfig, args: &Map<String, Value>) -> Duration {
|
||||
let mut d = CALL_TIMEOUT;
|
||||
if cfg.call_timeout_sec > 0 {
|
||||
d = Duration::from_secs(cfg.call_timeout_sec as u64);
|
||||
}
|
||||
@@ -440,27 +354,23 @@ fn launcher(cfg: &ServerConfig) -> String {
|
||||
b.strip_suffix(".cmd").unwrap_or(b).to_string()
|
||||
}
|
||||
|
||||
/// `mcpTimeoutFor` : le réglage global, au moins [`CONNECT_TIMEOUT_COLD`]
|
||||
/// pour npx et uvx.
|
||||
pub fn timeout_for(cfg: &ServerConfig, tm: &Timeouts) -> Duration {
|
||||
/// `mcpTimeoutFor`.
|
||||
pub fn timeout_for(cfg: &ServerConfig) -> Duration {
|
||||
match launcher(cfg).as_str() {
|
||||
"npx" | "uvx" => tm.connect().max(CONNECT_TIMEOUT_COLD),
|
||||
_ => tm.connect(),
|
||||
"npx" | "uvx" => CONNECT_TIMEOUT_COLD,
|
||||
_ => CONNECT_TIMEOUT,
|
||||
}
|
||||
}
|
||||
|
||||
/// `mcpFriendlyErr` pour un délai dépassé.
|
||||
pub fn timeout_error(cfg: &ServerConfig, tm: &Timeouts) -> String {
|
||||
pub fn timeout_error(cfg: &ServerConfig) -> String {
|
||||
let l = launcher(cfg);
|
||||
if l == "npx" || l == "uvx" {
|
||||
return format!(
|
||||
"délai dépassé — {l} télécharge le paquet du serveur au premier lancement, ce qui peut être long ; clique « tester » pour réessayer (le paquet reste en cache), et vérifie que le conteneur atteint le registre (npm/PyPI)"
|
||||
);
|
||||
}
|
||||
format!(
|
||||
"délai dépassé — le serveur n'a pas répondu au handshake en {} (délai de connexion réglable dans les réglages MCP)",
|
||||
crate::tools::shell::go_duration(tm.connect())
|
||||
)
|
||||
"délai dépassé — le serveur n'a pas répondu au handshake en 20s".into()
|
||||
}
|
||||
|
||||
/// `mcpOutputCap` : environ un caractère par jeton de contexte, jamais sous
|
||||
@@ -624,24 +534,20 @@ mod tests {
|
||||
};
|
||||
let s = Duration::from_secs;
|
||||
let none = Map::new();
|
||||
let def = Timeouts::default();
|
||||
let ct = |c: &ServerConfig, a: &Map<String, Value>| call_timeout_for(c, &def, a);
|
||||
assert_eq!(ct(&cfg(0), &none), s(120));
|
||||
assert_eq!(ct(&cfg(600), &none), s(600));
|
||||
assert_eq!(ct(&cfg(0), &args(json!(300.0))), s(330));
|
||||
assert_eq!(ct(&cfg(0), &args(json!(200000.0))), s(230));
|
||||
assert_eq!(ct(&cfg(0), &args(json!(5000.0))), s(120));
|
||||
assert_eq!(ct(&cfg(0), &args(json!("400"))), s(430));
|
||||
assert_eq!(call_timeout_for(&cfg(0), &none), CALL_TIMEOUT);
|
||||
assert_eq!(call_timeout_for(&cfg(600), &none), s(600));
|
||||
assert_eq!(call_timeout_for(&cfg(0), &args(json!(300.0))), s(330));
|
||||
assert_eq!(call_timeout_for(&cfg(0), &args(json!(200000.0))), s(230));
|
||||
assert_eq!(
|
||||
ct(&cfg(0), &args(json!(86400.0 * 1000.0))),
|
||||
call_timeout_for(&cfg(0), &args(json!(5000.0))),
|
||||
CALL_TIMEOUT
|
||||
);
|
||||
assert_eq!(call_timeout_for(&cfg(0), &args(json!("400"))), s(430));
|
||||
assert_eq!(
|
||||
call_timeout_for(&cfg(0), &args(json!(86400.0 * 1000.0))),
|
||||
CALL_TIMEOUT_MAX
|
||||
);
|
||||
assert_eq!(ct(&cfg(30), &none), s(30));
|
||||
// Le réglage global, remplacé par celui du serveur, jamais rogné.
|
||||
let tm = |call| Timeouts { connect: 20, call };
|
||||
assert_eq!(call_timeout_for(&cfg(0), &tm(45), &none), s(45));
|
||||
assert_eq!(call_timeout_for(&cfg(90), &tm(45), &none), s(90));
|
||||
assert_eq!(call_timeout_for(&cfg(0), &tm(3600), &none), s(3600));
|
||||
assert_eq!(call_timeout_for(&cfg(30), &none), s(30));
|
||||
assert_eq!(
|
||||
call_deadline_error(s(330)),
|
||||
"[erreur MCP] pas de réponse après 5m30s (délai de l'appel ; un argument timeout plus grand l'allonge)"
|
||||
@@ -671,41 +577,9 @@ mod tests {
|
||||
command: cmd.into(),
|
||||
..Default::default()
|
||||
};
|
||||
let def = Timeouts::default();
|
||||
assert_eq!(timeout_for(&c("/usr/bin/npx"), &def), CONNECT_TIMEOUT_COLD);
|
||||
assert_eq!(timeout_for(&c("UVX.EXE"), &def), CONNECT_TIMEOUT_COLD);
|
||||
assert_eq!(timeout_for(&c("npx.cmd"), &def), CONNECT_TIMEOUT_COLD);
|
||||
assert_eq!(timeout_for(&c("node"), &def), Duration::from_secs(20));
|
||||
// Un délai réglé plus long que le minimum des lanceurs l'emporte.
|
||||
let long = Timeouts {
|
||||
connect: 600,
|
||||
call: 120,
|
||||
};
|
||||
assert_eq!(timeout_for(&c("npx"), &long), Duration::from_secs(600));
|
||||
assert_eq!(timeout_for(&c("node"), &long), Duration::from_secs(600));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delais_globaux() {
|
||||
let d = |raw: &str| decode_timeouts(Some(raw.as_bytes()));
|
||||
let t = |connect, call| Timeouts { connect, call };
|
||||
assert_eq!(decode_timeouts(None), t(20, 120));
|
||||
assert_eq!(d(r#"{"connect":5,"call":300}"#), t(5, 300));
|
||||
assert_eq!(d(r#"{"connect":0,"call":0}"#), t(20, 120));
|
||||
assert_eq!(d(r#"{"connect":-4,"call":99999}"#), t(20, 3600));
|
||||
assert_eq!(d(r#"{"connect":"x","call":50}"#), t(20, 120));
|
||||
assert_eq!(d(r#"{"CONNECT":7}"#), t(7, 120));
|
||||
assert_eq!(d("null"), t(20, 120));
|
||||
assert_eq!(d(r#"{"connect":30,"call":40} reste"#), t(20, 120));
|
||||
assert_eq!(t(30, 600).marshal(), r#"{"connect":30,"call":600}"#);
|
||||
assert_eq!(timeouts_error(&t(0, 120)), Some("connexion : 1 à 3600 s"));
|
||||
assert_eq!(
|
||||
timeouts_error(&t(20, 3601)),
|
||||
Some("appel d'outil : 1 à 3600 s")
|
||||
);
|
||||
assert_eq!(timeouts_error(&t(3600, 1)), None);
|
||||
assert!(call_timeout_sec_error(-1).is_some());
|
||||
assert!(call_timeout_sec_error(0).is_none());
|
||||
assert!(call_timeout_sec_error(3601).is_some());
|
||||
assert_eq!(timeout_for(&c("/usr/bin/npx")), CONNECT_TIMEOUT_COLD);
|
||||
assert_eq!(timeout_for(&c("UVX.EXE")), CONNECT_TIMEOUT_COLD);
|
||||
assert_eq!(timeout_for(&c("npx.cmd")), CONNECT_TIMEOUT_COLD);
|
||||
assert_eq!(timeout_for(&c("node")), CONNECT_TIMEOUT);
|
||||
}
|
||||
}
|
||||
+16
-96
@@ -29,7 +29,7 @@ use crate::gojson::{self, Kind};
|
||||
use crate::proxy::Body;
|
||||
use crate::store::{BK_STATE, Store};
|
||||
use client::Session;
|
||||
use config::{ServerConfig, Servers, Timeouts};
|
||||
use config::{ServerConfig, Servers};
|
||||
|
||||
/// Clé de la configuration (`state/mcp`).
|
||||
pub const ST_MCP: &str = "mcp";
|
||||
@@ -108,26 +108,6 @@ impl Mcp {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `SaveMCPTimeouts` puis la fermeture des sessions des serveurs
|
||||
/// configurés : le nouveau délai de connexion vaut dès la suivante.
|
||||
async fn save_timeouts(&self, store: &Store, t: Timeouts) -> Result<(), String> {
|
||||
let names: Vec<String> = {
|
||||
let _g = self.cfg_lock.lock().await;
|
||||
store
|
||||
.put(
|
||||
BK_STATE,
|
||||
config::ST_TIMEOUTS,
|
||||
t.normalized().marshal().into_bytes(),
|
||||
)
|
||||
.await?;
|
||||
Self::load(store).await.keys().cloned().collect()
|
||||
};
|
||||
for name in names {
|
||||
self.invalidate(&name).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `mcpInvalidate` : la session du serveur fermée et oubliée, ses outils
|
||||
/// retirés du registre.
|
||||
pub async fn invalidate(&self, name: &str) {
|
||||
@@ -156,27 +136,8 @@ impl Mcp {
|
||||
}
|
||||
}
|
||||
|
||||
/// `LoadMCPTimeouts` : les délais globaux (`state/mcp_timeouts`).
|
||||
pub async fn timeouts(store: &Store) -> Timeouts {
|
||||
config::decode_timeouts(
|
||||
store
|
||||
.get(BK_STATE, config::ST_TIMEOUTS)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.as_deref(),
|
||||
)
|
||||
}
|
||||
|
||||
/// `ensure` : la session du serveur, connectée ou en erreur. Le délai de
|
||||
/// connexion est lu au moment de se connecter.
|
||||
async fn ensure(
|
||||
&self,
|
||||
store: &Store,
|
||||
name: &str,
|
||||
cfg: &ServerConfig,
|
||||
version: &str,
|
||||
) -> Arc<Slot> {
|
||||
/// `ensure` : la session du serveur, connectée ou en erreur.
|
||||
async fn ensure(&self, name: &str, cfg: &ServerConfig, version: &str) -> Arc<Slot> {
|
||||
let slot = lock(&self.slots)
|
||||
.entry(name.to_string())
|
||||
.or_insert_with(|| {
|
||||
@@ -189,15 +150,14 @@ impl Mcp {
|
||||
slot.state
|
||||
.get_or_init(|| async {
|
||||
let cfg = &slot.cfg;
|
||||
let tm = Self::timeouts(store).await;
|
||||
let mut st = SlotState::default();
|
||||
let work = async {
|
||||
let sess = Session::connect(cfg, version).await?;
|
||||
let tools = sess.list_tools().await;
|
||||
Ok::<_, String>((sess, tools))
|
||||
};
|
||||
match tokio::time::timeout(config::timeout_for(cfg, &tm), work).await {
|
||||
Err(_) => st.err = Some(config::timeout_error(cfg, &tm)),
|
||||
match tokio::time::timeout(config::timeout_for(cfg), work).await {
|
||||
Err(_) => st.err = Some(config::timeout_error(cfg)),
|
||||
Ok(Err(e)) => st.err = Some(e),
|
||||
Ok(Ok((sess, tools))) => {
|
||||
match tools {
|
||||
@@ -231,12 +191,12 @@ impl Mcp {
|
||||
}
|
||||
|
||||
/// `mcpEnsureAll` : les serveurs activés connectés en parallèle.
|
||||
async fn ensure_all(&self, store: &Store, servers: &Servers, version: &str) {
|
||||
async fn ensure_all(&self, servers: &Servers, version: &str) {
|
||||
join_all(
|
||||
servers
|
||||
.iter()
|
||||
.filter(|(_, c)| c.enabled && !c.transport().is_empty())
|
||||
.map(|(n, c)| self.ensure(store, n, c, version)),
|
||||
.map(|(n, c)| self.ensure(n, c, version)),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -248,13 +208,13 @@ impl Mcp {
|
||||
if servers.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
self.ensure_all(store, &servers, version).await;
|
||||
self.ensure_all(&servers, version).await;
|
||||
let mut out = Vec::new();
|
||||
for (name, cfg) in &servers {
|
||||
if !cfg.enabled || cfg.transport().is_empty() {
|
||||
continue;
|
||||
}
|
||||
let slot = self.ensure(store, name, cfg, version).await;
|
||||
let slot = self.ensure(name, cfg, version).await;
|
||||
let Some(st) = slot.state.get() else { continue };
|
||||
if st.sess.is_none() {
|
||||
continue;
|
||||
@@ -295,11 +255,8 @@ impl Mcp {
|
||||
};
|
||||
let cap = config::output_cap(config::ctx_window(&store.config().await));
|
||||
let servers = Self::load(store).await;
|
||||
let timeout = config::call_timeout_for(
|
||||
&servers.get(&server).cloned().unwrap_or_default(),
|
||||
&Self::timeouts(store).await,
|
||||
args,
|
||||
);
|
||||
let timeout =
|
||||
config::call_timeout_for(&servers.get(&server).cloned().unwrap_or_default(), args);
|
||||
let mut err = match self.call_once(&server, &tool, args, cap, timeout).await {
|
||||
Ok(r) => return r,
|
||||
Err(e) => e,
|
||||
@@ -309,7 +266,7 @@ impl Mcp {
|
||||
}
|
||||
if let Some(cfg) = servers.get(&server).filter(|c| c.enabled) {
|
||||
self.invalidate(&server).await;
|
||||
self.ensure(store, &server, cfg, version).await;
|
||||
self.ensure(&server, cfg, version).await;
|
||||
match self.call_once(&server, &tool, args, cap, timeout).await {
|
||||
Ok(r) => return r,
|
||||
Err(e) => err = e,
|
||||
@@ -350,7 +307,7 @@ impl Mcp {
|
||||
/// activés.
|
||||
pub async fn status(&self, store: &Store, version: &str) -> Vec<Value> {
|
||||
let servers = Self::load(store).await;
|
||||
self.ensure_all(store, &servers, version).await;
|
||||
self.ensure_all(&servers, version).await;
|
||||
let mut out = Vec::new();
|
||||
for (name, cfg) in &servers {
|
||||
let mut st = json!({
|
||||
@@ -367,13 +324,11 @@ impl Mcp {
|
||||
("env", json!(cfg.env)),
|
||||
("url", json!(cfg.url)),
|
||||
("headers", json!(cfg.headers)),
|
||||
("callTimeoutSec", json!(cfg.call_timeout_sec)),
|
||||
] {
|
||||
let empty = match &v {
|
||||
Value::String(s) => s.is_empty(),
|
||||
Value::Array(a) => a.is_empty(),
|
||||
Value::Object(o) => o.is_empty(),
|
||||
Value::Number(n) => n.as_i64() == Some(0),
|
||||
_ => false,
|
||||
};
|
||||
if !empty {
|
||||
@@ -381,7 +336,7 @@ impl Mcp {
|
||||
}
|
||||
}
|
||||
if cfg.enabled && !cfg.transport().is_empty() {
|
||||
let slot = self.ensure(store, name, cfg, version).await;
|
||||
let slot = self.ensure(name, cfg, version).await;
|
||||
if let Some(s) = slot.state.get() {
|
||||
if s.sess.is_some() {
|
||||
st["connected"] = json!(true);
|
||||
@@ -412,7 +367,7 @@ impl Mcp {
|
||||
}
|
||||
let servers = Self::load(&store).await;
|
||||
if !servers.is_empty() {
|
||||
me.ensure_all(&store, &servers, &version).await;
|
||||
me.ensure_all(&servers, &version).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -539,8 +494,6 @@ pub fn handles(path: &str) -> bool {
|
||||
| "/api/mcp/toggle"
|
||||
| "/api/mcp/tool"
|
||||
| "/api/mcp/test"
|
||||
| "/api/mcp/timeouts"
|
||||
| "/api/mcp/timeouts/save"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -609,16 +562,11 @@ pub async fn serve(
|
||||
("url", Kind::Str),
|
||||
("headers", Kind::StrMap),
|
||||
("enabled", Kind::OptBool),
|
||||
("callTimeoutSec", Kind::Int),
|
||||
],
|
||||
);
|
||||
if let Some(e) = d.err.clone() {
|
||||
return fail(StatusCode::BAD_REQUEST, e);
|
||||
}
|
||||
let call_timeout_sec = d.int("callTimeoutSec");
|
||||
if let Some(e) = config::call_timeout_sec_error(call_timeout_sec) {
|
||||
return fail(StatusCode::BAD_REQUEST, e);
|
||||
}
|
||||
let cfg = ServerConfig {
|
||||
command: d.str("command").trim().to_string(),
|
||||
args: d.str_list("args").unwrap_or_default(),
|
||||
@@ -631,7 +579,7 @@ pub async fn serve(
|
||||
.collect(),
|
||||
enabled: d.opt_bool("enabled").unwrap_or(true),
|
||||
disabled_tools: Vec::new(),
|
||||
call_timeout_sec,
|
||||
call_timeout_sec: 0,
|
||||
};
|
||||
let name = d.str("name");
|
||||
let res = mcp
|
||||
@@ -706,34 +654,6 @@ pub async fn serve(
|
||||
mcp.invalidate(&d.str("name")).await;
|
||||
servers_ok(mcp.status(store, &version).await)
|
||||
}
|
||||
"/api/mcp/timeouts" => {
|
||||
let t = Mcp::timeouts(store).await;
|
||||
send_json(
|
||||
StatusCode::OK,
|
||||
&json!({ "ok": true, "connect": t.connect, "call": t.call }),
|
||||
)
|
||||
}
|
||||
"/api/mcp/timeouts/save" => {
|
||||
let raw = read_body(body).await;
|
||||
let d = gojson::decode_struct(&raw, config::TIMEOUTS_TYPE, &config::TIMEOUTS_FIELDS);
|
||||
if let Some(e) = d.err.clone() {
|
||||
return fail(StatusCode::BAD_REQUEST, e);
|
||||
}
|
||||
let t = Timeouts {
|
||||
connect: d.int("connect"),
|
||||
call: d.int("call"),
|
||||
};
|
||||
if let Some(e) = config::timeouts_error(&t) {
|
||||
return fail(StatusCode::BAD_REQUEST, e);
|
||||
}
|
||||
match mcp.save_timeouts(store, t).await {
|
||||
Ok(()) => send_json(
|
||||
StatusCode::OK,
|
||||
&json!({ "ok": true, "connect": t.connect, "call": t.call }),
|
||||
),
|
||||
Err(e) => fail(StatusCode::INTERNAL_SERVER_ERROR, e),
|
||||
}
|
||||
}
|
||||
_ => fail(StatusCode::NOT_FOUND, "route inconnue"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,10 @@ static MANIFEST: Asset = Asset::new(
|
||||
"application/manifest+json",
|
||||
"public, max-age=3600",
|
||||
);
|
||||
/// KaTeX (formules LaTeX des réponses) : mêmes en-têtes que `web_katex.go`.
|
||||
const KATEX_CACHE: &str = "public, max-age=86400";
|
||||
static KATEX_JS: Asset = Asset::new(assets::KATEX_JS, "application/javascript", KATEX_CACHE);
|
||||
static KATEX_CSS: Asset = Asset::new(assets::KATEX_CSS, "text/css; charset=utf-8", KATEX_CACHE);
|
||||
|
||||
/// Réponse statique pour cette requête, ou `None` si la route n'est pas une
|
||||
/// ressource de l'UI (elle part alors au Go, qui rendra son 404 habituel).
|
||||
@@ -76,6 +80,9 @@ pub fn serve(method: &Method, path: &str, headers: &HeaderMap) -> Option<Respons
|
||||
}
|
||||
"/manifest.webmanifest" => asset(&MANIFEST, accepts_gzip),
|
||||
p if p.starts_with("/fonts/") => font(p)?,
|
||||
"/katex/katex.min.js" => asset(&KATEX_JS, accepts_gzip),
|
||||
"/katex/katex.min.css" => asset(&KATEX_CSS, accepts_gzip),
|
||||
p if p.starts_with("/katex/") => katex_font(p)?,
|
||||
_ => return None,
|
||||
};
|
||||
if accepts_gzip {
|
||||
@@ -130,6 +137,20 @@ fn font(path: &str) -> Option<Response<Body>> {
|
||||
Some(r)
|
||||
}
|
||||
|
||||
/// `/katex/fonts/<nom>.woff2` — chemin exact (pas le dernier segment seul,
|
||||
/// contrairement à `/fonts/`), comme `katexFile` dans `web_katex.go`. Le
|
||||
/// reste de `/katex/` part au Go, qui rend son 404.
|
||||
fn katex_font(path: &str) -> Option<Response<Body>> {
|
||||
let rel = path.strip_prefix("/katex/")?;
|
||||
let (_, body) = assets::KATEX_FONTS.iter().find(|(n, _)| *n == rel)?;
|
||||
let mut r = Response::new(full(Bytes::from_static(body)));
|
||||
let h = r.headers_mut();
|
||||
h.insert(header::CONTENT_TYPE, HeaderValue::from_static("font/woff2"));
|
||||
h.insert(header::CACHE_CONTROL, HeaderValue::from_static(KATEX_CACHE));
|
||||
h.insert(header::CONTENT_LENGTH, HeaderValue::from(body.len()));
|
||||
Some(r)
|
||||
}
|
||||
|
||||
fn compress(b: &[u8]) -> Vec<u8> {
|
||||
let mut enc = GzEncoder::new(Vec::with_capacity(b.len() / 3), Compression::default());
|
||||
enc.write_all(b).expect("gzip en mémoire");
|
||||
@@ -193,6 +214,46 @@ mod tests {
|
||||
assert!(get("/fonts/inter.ttf", false).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn katex() {
|
||||
let r = get("/katex/katex.min.js", true).unwrap();
|
||||
assert_eq!(hv(&r, "content-type"), "application/javascript");
|
||||
assert_eq!(hv(&r, "cache-control"), "public, max-age=86400");
|
||||
assert_eq!(hv(&r, "content-encoding"), "gzip");
|
||||
assert!(
|
||||
String::from_utf8_lossy(assets::KATEX_JS).contains("version:\"0.16.11\""),
|
||||
"KaTeX 0.16.11 attendu"
|
||||
);
|
||||
let r = get("/katex/katex.min.css", false).unwrap();
|
||||
assert_eq!(hv(&r, "content-type"), "text/css; charset=utf-8");
|
||||
// Chaque police woff2 demandée par la feuille est servie, non compressée.
|
||||
let css = String::from_utf8_lossy(assets::KATEX_CSS);
|
||||
let polices: Vec<&str> = css
|
||||
.split("url(")
|
||||
.skip(1)
|
||||
.filter_map(|s| s.split(')').next())
|
||||
.filter(|u| u.ends_with(".woff2"))
|
||||
.collect();
|
||||
assert!(polices.len() >= 10, "{} polices", polices.len());
|
||||
for p in polices {
|
||||
let r = get(&format!("/katex/{p}"), true).unwrap_or_else(|| panic!("{p}"));
|
||||
assert_eq!(hv(&r, "content-type"), "font/woff2");
|
||||
assert_eq!(hv(&r, "content-encoding"), "");
|
||||
assert_eq!(hv(&r, "cache-control"), "public, max-age=86400");
|
||||
}
|
||||
// Le reste part au Go (404) : ni index, ni licence, ni dernier segment seul.
|
||||
for p in [
|
||||
"/katex/",
|
||||
"/katex/LICENSE",
|
||||
"/katex/fonts/",
|
||||
"/katex/fonts/absente.woff2",
|
||||
"/katex/KaTeX_Main-Regular.woff2",
|
||||
"/katex/x/fonts/KaTeX_Main-Regular.woff2",
|
||||
] {
|
||||
assert!(get(p, false).is_none(), "{p}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gzip_si_accepte() {
|
||||
let r = get("/", true).unwrap();
|
||||
|
||||
+2
-136
@@ -66,44 +66,17 @@ fn parite_des_fonctions_pures() {
|
||||
.unwrap_or_default();
|
||||
assert_eq!(cf::prompt_line(&tools), s(&c["out"]), "{c}");
|
||||
}
|
||||
let def = cf::Timeouts::default();
|
||||
for (cmd, v) in fx["timeouts"].as_object().unwrap() {
|
||||
let cfg = cf::ServerConfig {
|
||||
command: cmd.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
cf::timeout_for(&cfg, &def).as_secs() as i64,
|
||||
cf::timeout_for(&cfg).as_secs() as i64,
|
||||
v["secs"].as_i64().unwrap(),
|
||||
"{cmd}"
|
||||
);
|
||||
assert_eq!(cf::timeout_error(&cfg, &def), s(&v["err"]), "{cmd}");
|
||||
}
|
||||
// Délais globaux : lecture de la base, puis leur effet.
|
||||
let tm = |v: &Value| cf::Timeouts {
|
||||
connect: v["connect"].as_i64().unwrap(),
|
||||
call: v["call"].as_i64().unwrap(),
|
||||
};
|
||||
for c in fx["timeouts_stored"].as_array().unwrap() {
|
||||
let raw = s(&c["in"]);
|
||||
let got = cf::decode_timeouts((!raw.is_empty()).then_some(raw.as_bytes()));
|
||||
assert_eq!(got, tm(&c["out"]), "{raw}");
|
||||
}
|
||||
for c in fx["timeouts_effects"].as_array().unwrap() {
|
||||
let t = tm(&c["timeouts"]);
|
||||
let cfg: cf::ServerConfig = serde_json::from_str(s(&c["cfg"])).unwrap();
|
||||
let args: Map<String, Value> = serde_json::from_str(s(&c["args"])).unwrap();
|
||||
assert_eq!(
|
||||
cf::timeout_for(&cfg, &t).as_millis() as i64,
|
||||
c["connect"].as_i64().unwrap(),
|
||||
"{c}"
|
||||
);
|
||||
assert_eq!(cf::timeout_error(&cfg, &t), s(&c["err"]), "{c}");
|
||||
assert_eq!(
|
||||
cf::call_timeout_for(&cfg, &t, &args).as_millis() as i64,
|
||||
c["call"].as_i64().unwrap(),
|
||||
"{c}"
|
||||
);
|
||||
assert_eq!(cf::timeout_error(&cfg), s(&v["err"]), "{cmd}");
|
||||
}
|
||||
for c in fx["flatten"].as_array().unwrap() {
|
||||
let v: Value = serde_json::from_str(s(&c["in"])).unwrap();
|
||||
@@ -449,50 +422,6 @@ async fn routes_de_configuration() {
|
||||
);
|
||||
let stored: Value = serde_json::from_str(&go.get("state", "mcp").unwrap()).unwrap();
|
||||
assert_eq!(stored["web"]["url"], json!(url));
|
||||
assert!(stored["web"].get("callTimeoutSec").is_none());
|
||||
// Délai d'un appel propre au serveur : rangé, rendu, puis retiré quand le
|
||||
// champ revient vide.
|
||||
let body = |ct: Option<i64>| {
|
||||
let mut b = json!({ "name": "web", "url": url, "headers": { "X-Cle": "k" } });
|
||||
if let Some(n) = ct {
|
||||
b["callTimeoutSec"] = json!(n);
|
||||
}
|
||||
b.to_string()
|
||||
};
|
||||
let (c, v) = post("/api/mcp/save", body(Some(900))).await;
|
||||
assert_eq!(
|
||||
(c, v["servers"][0]["callTimeoutSec"].clone()),
|
||||
(200, json!(900))
|
||||
);
|
||||
let stored: Value = serde_json::from_str(&go.get("state", "mcp").unwrap()).unwrap();
|
||||
assert_eq!(stored["web"]["callTimeoutSec"], 900);
|
||||
let (c, v) = post("/api/mcp/save", body(None)).await;
|
||||
assert_eq!(c, 200);
|
||||
assert!(v["servers"][0].get("callTimeoutSec").is_none(), "{v}");
|
||||
for n in [-1, 3601] {
|
||||
let (c, v) = post("/api/mcp/save", body(Some(n))).await;
|
||||
assert_eq!(
|
||||
(c, v["error"].clone()),
|
||||
(
|
||||
400,
|
||||
json!("délai d'un appel : 0 à 3600 s (vide = réglage global)")
|
||||
)
|
||||
);
|
||||
}
|
||||
let (c, v) = post(
|
||||
"/api/mcp/save",
|
||||
r#"{"name":"x","url":"http://h","callTimeoutSec":"9"}"#.into(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
(c, v["error"].clone()),
|
||||
(
|
||||
400,
|
||||
json!(
|
||||
"json: cannot unmarshal string into Go struct field mcpSaveReq.callTimeoutSec of type int"
|
||||
)
|
||||
)
|
||||
);
|
||||
// Erreurs de la configuration et du JSON.
|
||||
let (c, v) = post("/api/mcp/save", r#"{"name":"a__b","command":"x"}"#.into()).await;
|
||||
assert_eq!(
|
||||
@@ -577,67 +506,6 @@ async fn routes_de_configuration() {
|
||||
pool.close_all().await;
|
||||
}
|
||||
|
||||
/// Les routes des délais globaux rejouées contre la façade : mêmes codes,
|
||||
/// mêmes réponses, même valeur rangée ; un enregistrement réussi ferme les
|
||||
/// sessions ouvertes.
|
||||
#[tokio::test]
|
||||
async fn routes_des_delais() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let seen = Arc::new(Mutex::new(Vec::new()));
|
||||
let fx = fixture();
|
||||
let url = fake_server(fx["wire"].clone(), seen.clone()).await;
|
||||
let pool = Arc::new(Mcp::new(tmp.path().to_path_buf()));
|
||||
let (facade, go, _stop, _) = fakego::start_with(
|
||||
tmp.path().to_path_buf(),
|
||||
fakego::Opts {
|
||||
mcp: Some(pool.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await;
|
||||
go.set(
|
||||
"state",
|
||||
"mcp",
|
||||
&json!({ "web": { "url": url, "enabled": true } }).to_string(),
|
||||
);
|
||||
let initialize = || {
|
||||
seen.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|l| l.starts_with("initialize"))
|
||||
.count()
|
||||
};
|
||||
let (_, b) = fakego::send(facade, "GET", "/api/mcp", &[], "").await;
|
||||
let v: Value = serde_json::from_slice(&b).unwrap();
|
||||
assert_eq!(v["servers"][0]["connected"], true, "{v}");
|
||||
let mut connections = initialize();
|
||||
for c in fx["timeouts_routes"].as_array().unwrap() {
|
||||
let path = s(&c["path"]);
|
||||
let method = if path.ends_with("/save") {
|
||||
"POST"
|
||||
} else {
|
||||
"GET"
|
||||
};
|
||||
let (code, b) = fakego::send(facade, method, path, &[], s(&c["body"])).await;
|
||||
assert_eq!(code.as_u16() as i64, c["code"].as_i64().unwrap(), "{c}");
|
||||
let got: Value = serde_json::from_slice(&b).unwrap();
|
||||
let want: Value = serde_json::from_str(s(&c["resp"])).unwrap();
|
||||
assert_eq!(got, want, "{c}");
|
||||
assert_eq!(
|
||||
go.get("state", cf::ST_TIMEOUTS).unwrap_or_default(),
|
||||
s(&c["state"]),
|
||||
"{c}"
|
||||
);
|
||||
// Session fermée : la liste suivante se reconnecte.
|
||||
fakego::send(facade, "GET", "/api/mcp", &[], "").await;
|
||||
let now = initialize();
|
||||
let saved = method == "POST" && code.as_u16() == 200;
|
||||
assert_eq!(now, connections + usize::from(saved), "{c}");
|
||||
connections = now;
|
||||
}
|
||||
pool.close_all().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn catalogue_embarque() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
@@ -676,8 +544,6 @@ async fn sans_la_variable_mcp_reste_au_go() {
|
||||
"/api/mcp/save",
|
||||
"/api/mcp/catalog",
|
||||
"/api/mcp/test",
|
||||
"/api/mcp/timeouts",
|
||||
"/api/mcp/timeouts/save",
|
||||
] {
|
||||
let (_, _, b) = fakego::get(facade, p, &[]).await;
|
||||
assert_eq!(&b[..], format!("go:{p}").as_bytes());
|
||||
|
||||
@@ -231,8 +231,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
},
|
||||
@@ -260,7 +260,7 @@
|
||||
},
|
||||
{
|
||||
"body": {
|
||||
"content": "<163664 octets b7452851885a>"
|
||||
"content": "<163781 octets 608b16075e1c>"
|
||||
},
|
||||
"method": "POST",
|
||||
"path": "/tokenize"
|
||||
@@ -304,7 +304,7 @@
|
||||
"max_tokens": 300,
|
||||
"messages": [
|
||||
{
|
||||
"content": "<8130 octets 081c43a7796f>",
|
||||
"content": "<8103 octets e6352865574a>",
|
||||
"role": "user"
|
||||
}
|
||||
],
|
||||
@@ -335,8 +335,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -358,8 +358,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -387,8 +387,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -410,8 +410,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -433,8 +433,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -542,8 +542,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
},
|
||||
@@ -571,7 +571,7 @@
|
||||
},
|
||||
{
|
||||
"body": {
|
||||
"content": "<163664 octets b7452851885a>"
|
||||
"content": "<163781 octets 608b16075e1c>"
|
||||
},
|
||||
"method": "POST",
|
||||
"path": "/tokenize"
|
||||
@@ -615,7 +615,7 @@
|
||||
"max_tokens": 300,
|
||||
"messages": [
|
||||
{
|
||||
"content": "<8130 octets 081c43a7796f>",
|
||||
"content": "<8103 octets e6352865574a>",
|
||||
"role": "user"
|
||||
}
|
||||
],
|
||||
@@ -855,8 +855,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -947,8 +947,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -993,8 +993,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
},
|
||||
@@ -1022,7 +1022,7 @@
|
||||
},
|
||||
{
|
||||
"body": {
|
||||
"content": "<163664 octets b7452851885a>"
|
||||
"content": "<163781 octets 608b16075e1c>"
|
||||
},
|
||||
"method": "POST",
|
||||
"path": "/tokenize"
|
||||
@@ -1066,7 +1066,7 @@
|
||||
"max_tokens": 300,
|
||||
"messages": [
|
||||
{
|
||||
"content": "<8130 octets 081c43a7796f>",
|
||||
"content": "<8103 octets e6352865574a>",
|
||||
"role": "user"
|
||||
}
|
||||
],
|
||||
@@ -1103,8 +1103,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -1132,8 +1132,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -1241,8 +1241,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
},
|
||||
@@ -1270,7 +1270,7 @@
|
||||
},
|
||||
{
|
||||
"body": {
|
||||
"content": "<163664 octets b7452851885a>"
|
||||
"content": "<163781 octets 608b16075e1c>"
|
||||
},
|
||||
"method": "POST",
|
||||
"path": "/tokenize"
|
||||
@@ -1314,7 +1314,7 @@
|
||||
"max_tokens": 300,
|
||||
"messages": [
|
||||
{
|
||||
"content": "<8130 octets 081c43a7796f>",
|
||||
"content": "<8103 octets e6352865574a>",
|
||||
"role": "user"
|
||||
}
|
||||
],
|
||||
@@ -1554,8 +1554,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -1646,8 +1646,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -1686,8 +1686,8 @@
|
||||
"predicted_ms": 81920,
|
||||
"predicted_n": 4096,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 16395,
|
||||
"prompt_n": 32790,
|
||||
"prompt_ms": 16400.5,
|
||||
"prompt_n": 32801,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
},
|
||||
@@ -1715,7 +1715,7 @@
|
||||
},
|
||||
{
|
||||
"body": {
|
||||
"content": "<163664 octets b7452851885a>"
|
||||
"content": "<163781 octets 608b16075e1c>"
|
||||
},
|
||||
"method": "POST",
|
||||
"path": "/tokenize"
|
||||
@@ -1759,7 +1759,7 @@
|
||||
"max_tokens": 4096,
|
||||
"messages": [
|
||||
{
|
||||
"content": "<131145 octets 8d4ca4a3239d>",
|
||||
"content": "<131191 octets 64f00ccc3d63>",
|
||||
"role": "user"
|
||||
}
|
||||
],
|
||||
@@ -1790,8 +1790,8 @@
|
||||
"predicted_ms": 81920,
|
||||
"predicted_n": 4096,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 16395,
|
||||
"prompt_n": 32790,
|
||||
"prompt_ms": 16400.5,
|
||||
"prompt_n": 32801,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -1813,8 +1813,8 @@
|
||||
"predicted_ms": 81920,
|
||||
"predicted_n": 4096,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 16395,
|
||||
"prompt_n": 32790,
|
||||
"prompt_ms": 16400.5,
|
||||
"prompt_n": 32801,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -1882,7 +1882,7 @@
|
||||
},
|
||||
{
|
||||
"body": {
|
||||
"content": "<163664 octets b7452851885a>"
|
||||
"content": "<163781 octets 608b16075e1c>"
|
||||
},
|
||||
"method": "POST",
|
||||
"path": "/tokenize"
|
||||
@@ -2049,7 +2049,7 @@
|
||||
},
|
||||
{
|
||||
"body": {
|
||||
"content": "<163664 octets b7452851885a>"
|
||||
"content": "<163781 octets 608b16075e1c>"
|
||||
},
|
||||
"method": "POST",
|
||||
"path": "/tokenize"
|
||||
@@ -2093,7 +2093,7 @@
|
||||
"max_tokens": 16,
|
||||
"messages": [
|
||||
{
|
||||
"content": "<3745 octets 4d9bad4c301c>",
|
||||
"content": "<3744 octets dff015dced82>",
|
||||
"role": "user"
|
||||
}
|
||||
],
|
||||
@@ -2187,8 +2187,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
},
|
||||
@@ -2216,7 +2216,7 @@
|
||||
},
|
||||
{
|
||||
"body": {
|
||||
"content": "<163664 octets b7452851885a>"
|
||||
"content": "<163781 octets 608b16075e1c>"
|
||||
},
|
||||
"method": "POST",
|
||||
"path": "/tokenize"
|
||||
@@ -2260,7 +2260,7 @@
|
||||
"max_tokens": 300,
|
||||
"messages": [
|
||||
{
|
||||
"content": "<8130 octets 081c43a7796f>",
|
||||
"content": "<8103 octets e6352865574a>",
|
||||
"role": "user"
|
||||
}
|
||||
],
|
||||
@@ -2291,8 +2291,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -2314,8 +2314,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -2368,7 +2368,7 @@
|
||||
},
|
||||
{
|
||||
"body": {
|
||||
"content": "<163664 octets b7452851885a>"
|
||||
"content": "<163781 octets 608b16075e1c>"
|
||||
},
|
||||
"method": "POST",
|
||||
"path": "/tokenize"
|
||||
@@ -2420,8 +2420,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -2443,8 +2443,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -2497,7 +2497,7 @@
|
||||
},
|
||||
{
|
||||
"body": {
|
||||
"content": "<163664 octets b7452851885a>"
|
||||
"content": "<163781 octets 608b16075e1c>"
|
||||
},
|
||||
"method": "POST",
|
||||
"path": "/tokenize"
|
||||
@@ -2541,7 +2541,7 @@
|
||||
"max_tokens": 300,
|
||||
"messages": [
|
||||
{
|
||||
"content": "<8130 octets 081c43a7796f>",
|
||||
"content": "<8103 octets e6352865574a>",
|
||||
"role": "user"
|
||||
}
|
||||
],
|
||||
@@ -2572,8 +2572,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -2595,8 +2595,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -2649,7 +2649,7 @@
|
||||
},
|
||||
{
|
||||
"body": {
|
||||
"content": "<163664 octets b7452851885a>"
|
||||
"content": "<163781 octets 608b16075e1c>"
|
||||
},
|
||||
"method": "POST",
|
||||
"path": "/tokenize"
|
||||
@@ -2693,7 +2693,7 @@
|
||||
"max_tokens": 300,
|
||||
"messages": [
|
||||
{
|
||||
"content": "<8130 octets 081c43a7796f>",
|
||||
"content": "<8103 octets e6352865574a>",
|
||||
"role": "user"
|
||||
}
|
||||
],
|
||||
@@ -2747,8 +2747,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -2770,8 +2770,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -2824,7 +2824,7 @@
|
||||
},
|
||||
{
|
||||
"body": {
|
||||
"content": "<163664 octets b7452851885a>"
|
||||
"content": "<163781 octets 608b16075e1c>"
|
||||
},
|
||||
"method": "POST",
|
||||
"path": "/tokenize"
|
||||
@@ -2868,7 +2868,7 @@
|
||||
"max_tokens": 300,
|
||||
"messages": [
|
||||
{
|
||||
"content": "<8130 octets 081c43a7796f>",
|
||||
"content": "<8103 octets e6352865574a>",
|
||||
"role": "user"
|
||||
}
|
||||
],
|
||||
@@ -2992,8 +2992,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -3015,8 +3015,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -3069,7 +3069,7 @@
|
||||
},
|
||||
{
|
||||
"body": {
|
||||
"content": "<163664 octets b7452851885a>"
|
||||
"content": "<163781 octets 608b16075e1c>"
|
||||
},
|
||||
"method": "POST",
|
||||
"path": "/tokenize"
|
||||
@@ -3121,8 +3121,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -3144,8 +3144,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -3189,8 +3189,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
},
|
||||
@@ -3218,7 +3218,7 @@
|
||||
},
|
||||
{
|
||||
"body": {
|
||||
"content": "<163664 octets b7452851885a>"
|
||||
"content": "<163781 octets 608b16075e1c>"
|
||||
},
|
||||
"method": "POST",
|
||||
"path": "/tokenize"
|
||||
@@ -3287,7 +3287,7 @@
|
||||
"max_tokens": 300,
|
||||
"messages": [
|
||||
{
|
||||
"content": "<8130 octets 081c43a7796f>",
|
||||
"content": "<8103 octets e6352865574a>",
|
||||
"role": "user"
|
||||
}
|
||||
],
|
||||
@@ -3319,8 +3319,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -3342,8 +3342,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -3383,8 +3383,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
},
|
||||
@@ -3412,7 +3412,7 @@
|
||||
},
|
||||
{
|
||||
"body": {
|
||||
"content": "<163664 octets b7452851885a>"
|
||||
"content": "<163781 octets 608b16075e1c>"
|
||||
},
|
||||
"method": "POST",
|
||||
"path": "/tokenize"
|
||||
@@ -3457,7 +3457,7 @@
|
||||
"max_tokens": 300,
|
||||
"messages": [
|
||||
{
|
||||
"content": "<8130 octets 081c43a7796f>",
|
||||
"content": "<8103 octets e6352865574a>",
|
||||
"role": "user"
|
||||
}
|
||||
],
|
||||
@@ -3489,8 +3489,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -3512,8 +3512,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -3579,8 +3579,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
},
|
||||
@@ -3608,7 +3608,7 @@
|
||||
},
|
||||
{
|
||||
"body": {
|
||||
"content": "<163664 octets b7452851885a>"
|
||||
"content": "<163781 octets 608b16075e1c>"
|
||||
},
|
||||
"method": "POST",
|
||||
"path": "/tokenize"
|
||||
@@ -3652,7 +3652,7 @@
|
||||
"max_tokens": 300,
|
||||
"messages": [
|
||||
{
|
||||
"content": "<8130 octets 081c43a7796f>",
|
||||
"content": "<8103 octets e6352865574a>",
|
||||
"role": "user"
|
||||
}
|
||||
],
|
||||
@@ -3737,8 +3737,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -3760,8 +3760,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -3823,7 +3823,7 @@
|
||||
},
|
||||
{
|
||||
"body": {
|
||||
"content": "<163664 octets b7452851885a>"
|
||||
"content": "<163781 octets 608b16075e1c>"
|
||||
},
|
||||
"method": "POST",
|
||||
"path": "/tokenize"
|
||||
@@ -3867,7 +3867,7 @@
|
||||
"max_tokens": 300,
|
||||
"messages": [
|
||||
{
|
||||
"content": "<8130 octets 081c43a7796f>",
|
||||
"content": "<8103 octets e6352865574a>",
|
||||
"role": "user"
|
||||
}
|
||||
],
|
||||
@@ -3952,8 +3952,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -3975,8 +3975,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -4034,8 +4034,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -4057,8 +4057,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -4089,8 +4089,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -4112,8 +4112,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
@@ -4135,8 +4135,8 @@
|
||||
"predicted_ms": 6000,
|
||||
"predicted_n": 300,
|
||||
"predicted_per_second": 50,
|
||||
"prompt_ms": 1018,
|
||||
"prompt_n": 2036,
|
||||
"prompt_ms": 1014.5,
|
||||
"prompt_n": 2029,
|
||||
"prompt_per_second": 2000,
|
||||
"protocol": 2
|
||||
}
|
||||
|
||||
@@ -163,37 +163,37 @@
|
||||
{
|
||||
"op": "set",
|
||||
"name": "web",
|
||||
"cfg": "{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":false,\"callTimeoutSec\":45}",
|
||||
"cfg": "{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":false}",
|
||||
"error": "",
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":false,\"callTimeoutSec\":45}}"
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":false}}"
|
||||
},
|
||||
{
|
||||
"op": "enable",
|
||||
"name": "web",
|
||||
"on": true,
|
||||
"error": "",
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":true,\"callTimeoutSec\":45}}"
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":true}}"
|
||||
},
|
||||
{
|
||||
"op": "enable",
|
||||
"name": "nope",
|
||||
"on": true,
|
||||
"error": "serveur MCP inconnu: nope",
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":true,\"callTimeoutSec\":45}}"
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":true}}"
|
||||
},
|
||||
{
|
||||
"op": "tool",
|
||||
"name": "fs",
|
||||
"tool": "write",
|
||||
"error": "",
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true,\"disabledTools\":[\"write\"]},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":true,\"callTimeoutSec\":45}}"
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true,\"disabledTools\":[\"write\"]},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":true}}"
|
||||
},
|
||||
{
|
||||
"op": "tool",
|
||||
"name": "fs",
|
||||
"tool": "read",
|
||||
"error": "",
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true,\"disabledTools\":[\"write\",\"read\"]},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":true,\"callTimeoutSec\":45}}"
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true,\"disabledTools\":[\"write\",\"read\"]},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":true}}"
|
||||
},
|
||||
{
|
||||
"op": "tool",
|
||||
@@ -201,21 +201,21 @@
|
||||
"tool": "write",
|
||||
"on": true,
|
||||
"error": "",
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true,\"disabledTools\":[\"read\"]},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":true,\"callTimeoutSec\":45}}"
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true,\"disabledTools\":[\"read\"]},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":true}}"
|
||||
},
|
||||
{
|
||||
"op": "tool",
|
||||
"name": "fs",
|
||||
"tool": "read",
|
||||
"error": "",
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true,\"disabledTools\":[\"read\"]},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":true,\"callTimeoutSec\":45}}"
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true,\"disabledTools\":[\"read\"]},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":true}}"
|
||||
},
|
||||
{
|
||||
"op": "tool",
|
||||
"name": "nope",
|
||||
"tool": "x",
|
||||
"error": "serveur MCP inconnu: nope",
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true,\"disabledTools\":[\"read\"]},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":true,\"callTimeoutSec\":45}}"
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true,\"disabledTools\":[\"read\"]},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":true}}"
|
||||
},
|
||||
{
|
||||
"op": "delete",
|
||||
@@ -382,14 +382,13 @@
|
||||
"connected": false,
|
||||
"tools": [],
|
||||
"disabled": [],
|
||||
"url": "{{URL}}",
|
||||
"callTimeoutSec": 300
|
||||
"url": "{{URL}}"
|
||||
}
|
||||
],
|
||||
"store": "{\"alpha\":{\"url\":\"{{URL}}\",\"enabled\":true,\"disabledTools\":[\"secret\"]},\"beta\":{\"command\":\"loki-mcp-absent-pour-test\",\"enabled\":true},\"gamma\":{\"url\":\"{{URL}}\",\"enabled\":false,\"callTimeoutSec\":300},\"delta\":{\"enabled\":true}}",
|
||||
"store": "{\"alpha\":{\"url\":\"{{URL}}\",\"enabled\":true,\"disabledTools\":[\"secret\"]},\"beta\":{\"command\":\"loki-mcp-absent-pour-test\",\"enabled\":true},\"gamma\":{\"url\":\"{{URL}}\",\"enabled\":false},\"delta\":{\"enabled\":true}}",
|
||||
"timeouts": {
|
||||
"": {
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 20s (délai de connexion réglable dans les réglages MCP)",
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 20s",
|
||||
"secs": 20
|
||||
},
|
||||
"/usr/local/bin/NPX": {
|
||||
@@ -401,7 +400,7 @@
|
||||
"secs": 180
|
||||
},
|
||||
"node": {
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 20s (délai de connexion réglable dans les réglages MCP)",
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 20s",
|
||||
"secs": 20
|
||||
},
|
||||
"npx": {
|
||||
@@ -413,7 +412,7 @@
|
||||
"secs": 180
|
||||
},
|
||||
"uvx-x": {
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 20s (délai de connexion réglable dans les réglages MCP)",
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 20s",
|
||||
"secs": 20
|
||||
},
|
||||
"uvx.exe": {
|
||||
@@ -421,437 +420,6 @@
|
||||
"secs": 180
|
||||
}
|
||||
},
|
||||
"timeouts_effects": [
|
||||
{
|
||||
"args": "{}",
|
||||
"call": 120000,
|
||||
"cfg": "{\"command\":\"node\",\"enabled\":false}",
|
||||
"connect": 20000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 20s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 20,
|
||||
"call": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{}",
|
||||
"call": 120000,
|
||||
"cfg": "{\"command\":\"npx\",\"enabled\":false}",
|
||||
"connect": 180000,
|
||||
"err": "délai dépassé — npx télécharge le paquet du serveur au premier lancement, ce qui peut être long ; clique « tester » pour réessayer (le paquet reste en cache), et vérifie que le conteneur atteint le registre (npm/PyPI)",
|
||||
"timeouts": {
|
||||
"connect": 20,
|
||||
"call": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{}",
|
||||
"call": 90000,
|
||||
"cfg": "{\"url\":\"http://h\",\"enabled\":false,\"callTimeoutSec\":90}",
|
||||
"connect": 20000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 20s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 20,
|
||||
"call": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{\"timeout\":7200}",
|
||||
"call": 3600000,
|
||||
"cfg": "{\"command\":\"uvx\",\"enabled\":false,\"callTimeoutSec\":3600}",
|
||||
"connect": 180000,
|
||||
"err": "délai dépassé — uvx télécharge le paquet du serveur au premier lancement, ce qui peut être long ; clique « tester » pour réessayer (le paquet reste en cache), et vérifie que le conteneur atteint le registre (npm/PyPI)",
|
||||
"timeouts": {
|
||||
"connect": 20,
|
||||
"call": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{\"timeout\":600}",
|
||||
"call": 630000,
|
||||
"cfg": "{\"url\":\"http://h\",\"enabled\":false}",
|
||||
"connect": 20000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 20s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 20,
|
||||
"call": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{\"timeout\":99999999}",
|
||||
"call": 3600000,
|
||||
"cfg": "{\"url\":\"http://h\",\"enabled\":false}",
|
||||
"connect": 20000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 20s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 20,
|
||||
"call": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{}",
|
||||
"call": 45000,
|
||||
"cfg": "{\"command\":\"node\",\"enabled\":false}",
|
||||
"connect": 5000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 5s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 5,
|
||||
"call": 45
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{}",
|
||||
"call": 45000,
|
||||
"cfg": "{\"command\":\"npx\",\"enabled\":false}",
|
||||
"connect": 180000,
|
||||
"err": "délai dépassé — npx télécharge le paquet du serveur au premier lancement, ce qui peut être long ; clique « tester » pour réessayer (le paquet reste en cache), et vérifie que le conteneur atteint le registre (npm/PyPI)",
|
||||
"timeouts": {
|
||||
"connect": 5,
|
||||
"call": 45
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{}",
|
||||
"call": 90000,
|
||||
"cfg": "{\"url\":\"http://h\",\"enabled\":false,\"callTimeoutSec\":90}",
|
||||
"connect": 5000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 5s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 5,
|
||||
"call": 45
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{\"timeout\":7200}",
|
||||
"call": 3600000,
|
||||
"cfg": "{\"command\":\"uvx\",\"enabled\":false,\"callTimeoutSec\":3600}",
|
||||
"connect": 180000,
|
||||
"err": "délai dépassé — uvx télécharge le paquet du serveur au premier lancement, ce qui peut être long ; clique « tester » pour réessayer (le paquet reste en cache), et vérifie que le conteneur atteint le registre (npm/PyPI)",
|
||||
"timeouts": {
|
||||
"connect": 5,
|
||||
"call": 45
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{\"timeout\":600}",
|
||||
"call": 630000,
|
||||
"cfg": "{\"url\":\"http://h\",\"enabled\":false}",
|
||||
"connect": 5000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 5s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 5,
|
||||
"call": 45
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{\"timeout\":99999999}",
|
||||
"call": 3600000,
|
||||
"cfg": "{\"url\":\"http://h\",\"enabled\":false}",
|
||||
"connect": 5000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 5s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 5,
|
||||
"call": 45
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{}",
|
||||
"call": 3600000,
|
||||
"cfg": "{\"command\":\"node\",\"enabled\":false}",
|
||||
"connect": 600000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 10m0s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 600,
|
||||
"call": 3600
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{}",
|
||||
"call": 3600000,
|
||||
"cfg": "{\"command\":\"npx\",\"enabled\":false}",
|
||||
"connect": 600000,
|
||||
"err": "délai dépassé — npx télécharge le paquet du serveur au premier lancement, ce qui peut être long ; clique « tester » pour réessayer (le paquet reste en cache), et vérifie que le conteneur atteint le registre (npm/PyPI)",
|
||||
"timeouts": {
|
||||
"connect": 600,
|
||||
"call": 3600
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{}",
|
||||
"call": 90000,
|
||||
"cfg": "{\"url\":\"http://h\",\"enabled\":false,\"callTimeoutSec\":90}",
|
||||
"connect": 600000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 10m0s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 600,
|
||||
"call": 3600
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{\"timeout\":7200}",
|
||||
"call": 3600000,
|
||||
"cfg": "{\"command\":\"uvx\",\"enabled\":false,\"callTimeoutSec\":3600}",
|
||||
"connect": 600000,
|
||||
"err": "délai dépassé — uvx télécharge le paquet du serveur au premier lancement, ce qui peut être long ; clique « tester » pour réessayer (le paquet reste en cache), et vérifie que le conteneur atteint le registre (npm/PyPI)",
|
||||
"timeouts": {
|
||||
"connect": 600,
|
||||
"call": 3600
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{\"timeout\":600}",
|
||||
"call": 3600000,
|
||||
"cfg": "{\"url\":\"http://h\",\"enabled\":false}",
|
||||
"connect": 600000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 10m0s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 600,
|
||||
"call": 3600
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{\"timeout\":99999999}",
|
||||
"call": 3600000,
|
||||
"cfg": "{\"url\":\"http://h\",\"enabled\":false}",
|
||||
"connect": 600000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 10m0s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 600,
|
||||
"call": 3600
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{}",
|
||||
"call": 1000,
|
||||
"cfg": "{\"command\":\"node\",\"enabled\":false}",
|
||||
"connect": 180000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 3m0s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 180,
|
||||
"call": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{}",
|
||||
"call": 1000,
|
||||
"cfg": "{\"command\":\"npx\",\"enabled\":false}",
|
||||
"connect": 180000,
|
||||
"err": "délai dépassé — npx télécharge le paquet du serveur au premier lancement, ce qui peut être long ; clique « tester » pour réessayer (le paquet reste en cache), et vérifie que le conteneur atteint le registre (npm/PyPI)",
|
||||
"timeouts": {
|
||||
"connect": 180,
|
||||
"call": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{}",
|
||||
"call": 90000,
|
||||
"cfg": "{\"url\":\"http://h\",\"enabled\":false,\"callTimeoutSec\":90}",
|
||||
"connect": 180000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 3m0s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 180,
|
||||
"call": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{\"timeout\":7200}",
|
||||
"call": 3600000,
|
||||
"cfg": "{\"command\":\"uvx\",\"enabled\":false,\"callTimeoutSec\":3600}",
|
||||
"connect": 180000,
|
||||
"err": "délai dépassé — uvx télécharge le paquet du serveur au premier lancement, ce qui peut être long ; clique « tester » pour réessayer (le paquet reste en cache), et vérifie que le conteneur atteint le registre (npm/PyPI)",
|
||||
"timeouts": {
|
||||
"connect": 180,
|
||||
"call": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{\"timeout\":600}",
|
||||
"call": 630000,
|
||||
"cfg": "{\"url\":\"http://h\",\"enabled\":false}",
|
||||
"connect": 180000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 3m0s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 180,
|
||||
"call": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{\"timeout\":99999999}",
|
||||
"call": 3600000,
|
||||
"cfg": "{\"url\":\"http://h\",\"enabled\":false}",
|
||||
"connect": 180000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 3m0s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 180,
|
||||
"call": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"timeouts_routes": [
|
||||
{
|
||||
"body": "",
|
||||
"code": 200,
|
||||
"path": "/api/mcp/timeouts",
|
||||
"resp": "{\"call\":120,\"connect\":20,\"ok\":true}",
|
||||
"state": ""
|
||||
},
|
||||
{
|
||||
"body": "{\"connect\":0,\"call\":120}",
|
||||
"code": 400,
|
||||
"path": "/api/mcp/timeouts/save",
|
||||
"resp": "{\"error\":\"connexion : 1 à 3600 s\",\"ok\":false}",
|
||||
"state": ""
|
||||
},
|
||||
{
|
||||
"body": "{\"connect\":20,\"call\":3601}",
|
||||
"code": 400,
|
||||
"path": "/api/mcp/timeouts/save",
|
||||
"resp": "{\"error\":\"appel d'outil : 1 à 3600 s\",\"ok\":false}",
|
||||
"state": ""
|
||||
},
|
||||
{
|
||||
"body": "{\"connect\":20}",
|
||||
"code": 400,
|
||||
"path": "/api/mcp/timeouts/save",
|
||||
"resp": "{\"error\":\"appel d'outil : 1 à 3600 s\",\"ok\":false}",
|
||||
"state": ""
|
||||
},
|
||||
{
|
||||
"body": "{\"connect\":\"20\",\"call\":120}",
|
||||
"code": 400,
|
||||
"path": "/api/mcp/timeouts/save",
|
||||
"resp": "{\"error\":\"json: cannot unmarshal string into Go struct field MCPTimeouts.connect of type int\",\"ok\":false}",
|
||||
"state": ""
|
||||
},
|
||||
{
|
||||
"body": "{\"connect\":2.5,\"call\":120}",
|
||||
"code": 400,
|
||||
"path": "/api/mcp/timeouts/save",
|
||||
"resp": "{\"error\":\"json: cannot unmarshal number 2.5 into Go struct field MCPTimeouts.connect of type int\",\"ok\":false}",
|
||||
"state": ""
|
||||
},
|
||||
{
|
||||
"body": "[]",
|
||||
"code": 400,
|
||||
"path": "/api/mcp/timeouts/save",
|
||||
"resp": "{\"error\":\"json: cannot unmarshal array into Go value of type loki.MCPTimeouts\",\"ok\":false}",
|
||||
"state": ""
|
||||
},
|
||||
{
|
||||
"body": "pas du json",
|
||||
"code": 400,
|
||||
"path": "/api/mcp/timeouts/save",
|
||||
"resp": "{\"error\":\"invalid character 'p' looking for beginning of value\",\"ok\":false}",
|
||||
"state": ""
|
||||
},
|
||||
{
|
||||
"body": "{\"Connect\":45,\"CALL\":900,\"x\":1}",
|
||||
"code": 200,
|
||||
"path": "/api/mcp/timeouts/save",
|
||||
"resp": "{\"call\":900,\"connect\":45,\"ok\":true}",
|
||||
"state": "{\"connect\":45,\"call\":900}"
|
||||
},
|
||||
{
|
||||
"body": "",
|
||||
"code": 200,
|
||||
"path": "/api/mcp/timeouts",
|
||||
"resp": "{\"call\":900,\"connect\":45,\"ok\":true}",
|
||||
"state": "{\"connect\":45,\"call\":900}"
|
||||
},
|
||||
{
|
||||
"body": "{\"connect\":3600,\"call\":1}",
|
||||
"code": 200,
|
||||
"path": "/api/mcp/timeouts/save",
|
||||
"resp": "{\"call\":1,\"connect\":3600,\"ok\":true}",
|
||||
"state": "{\"connect\":3600,\"call\":1}"
|
||||
},
|
||||
{
|
||||
"body": "",
|
||||
"code": 200,
|
||||
"path": "/api/mcp/timeouts",
|
||||
"resp": "{\"call\":1,\"connect\":3600,\"ok\":true}",
|
||||
"state": "{\"connect\":3600,\"call\":1}"
|
||||
}
|
||||
],
|
||||
"timeouts_stored": [
|
||||
{
|
||||
"in": "",
|
||||
"out": {
|
||||
"connect": 20,
|
||||
"call": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "{\"connect\":5,\"call\":300}",
|
||||
"out": {
|
||||
"connect": 5,
|
||||
"call": 300
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "{\"connect\":0,\"call\":0}",
|
||||
"out": {
|
||||
"connect": 20,
|
||||
"call": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "{\"connect\":-4,\"call\":99999}",
|
||||
"out": {
|
||||
"connect": 20,
|
||||
"call": 3600
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "{\"connect\":\"x\",\"call\":50}",
|
||||
"out": {
|
||||
"connect": 20,
|
||||
"call": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "{\"CONNECT\":7,\"Call\":8}",
|
||||
"out": {
|
||||
"connect": 7,
|
||||
"call": 8
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "null",
|
||||
"out": {
|
||||
"connect": 20,
|
||||
"call": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "[1]",
|
||||
"out": {
|
||||
"connect": 20,
|
||||
"call": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "{\"connect\":30,\"call\":40} reste",
|
||||
"out": {
|
||||
"connect": 20,
|
||||
"call": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "{\"connect\":1.5,\"call\":60}",
|
||||
"out": {
|
||||
"connect": 20,
|
||||
"call": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "{\"connect\":3600,\"call\":3600,\"autre\":1}",
|
||||
"out": {
|
||||
"connect": 3600,
|
||||
"call": 3600
|
||||
}
|
||||
}
|
||||
],
|
||||
"tool_server": [
|
||||
{
|
||||
"desc": "[MCP: alpha] desc",
|
||||
|
||||
Reference in new issue
Block a user