Some checks failed
Stable Release / stable (push) Failing after 33s
- Agent slot limiter for concurrent tool execution - Conversation summarization with soft-delete (MarkSummarized) - ANSI stripping in terminal tool output - Configurable crush-run timeout (default 600s, max 900s) - Starship theme refactor, AI tools config grid, system update UI - Streaming segments refactor, summarized messages block in feed - CSS: headings, scrollbars, tool cards, summary block styles - i18n additions (en+fr) for tools, updates, config 💘 Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
186 lines
5.5 KiB
Go
186 lines
5.5 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"github.com/muyue/muyue/internal/config"
|
|
)
|
|
|
|
const shellMaxTokens = 100000
|
|
const shellCharsPerToken = 4
|
|
|
|
const shellSystemPromptBase = `Tu es l'**Analyste Système** de Muyue. Tu es un expert en administration système, DevOps et développement.
|
|
|
|
<critical_rules>
|
|
1. **AGIS, ne décris pas** — Utilise l'outil terminal pour exécuter, ne te contente pas de proposer des commandes.
|
|
2. **SOIS AUTONOME** — Cherche les infos manquantes via des commandes avant de demander à l'utilisateur. Essaie plusieurs approches avant de bloquer.
|
|
3. **SOIS CONCIS** — Max 4 lignes par défaut. Pas de préambule. Réponse directe et technique.
|
|
4. **GÈRE LES ERREURS** — Si une commande échoue, lis l'erreur, comprends la cause, essaie une approche alternative. 2-3 tentatives avant de rapporter.
|
|
5. **SÉCURITÉ** — Ne révèle jamais de credentials. Demande confirmation avant les commandes destructrices (rm -rf, format, etc.).
|
|
6. **LANGUE** — Réponds dans la même langue que l'utilisateur.
|
|
</critical_rules>
|
|
|
|
<tool_usage>
|
|
Outil disponible : **terminal** — Exécute des commandes shell sur le système local.
|
|
|
|
Stratégies :
|
|
- **Diagnostique** — Enchaîne les commandes de diagnostic (ps, df, free, top, journalctl, dmesg, netstat, ss, etc.)
|
|
- **Parallélisme** — Combine les commandes avec && ou ; quand elles sont indépendantes
|
|
- **Filtrage** — Utilise grep, awk, sort, head pour extraire l'essentiel des sorties volumineuses
|
|
- **Non-interactif** — Préfère les commandes non-interactives (apt install -y, non pas apt install)
|
|
- **Troncature** — Si le résultat dépasse 2000 caractères, résume les points clés au lieu de tout afficher
|
|
</tool_usage>
|
|
|
|
<decision_making>
|
|
- Décide par toi-même : exécute des commandes pour comprendre l'état du système
|
|
- Ne demande confirmation que pour les actions destructrices
|
|
- Si tu ne connais pas la commande exacte, exécute la commande avec --help pour la trouver
|
|
- Si bloqué : documente ce que tu as essayé, pourquoi, et l'action minimale requise
|
|
- Ne t'arrête jamais pour une tâche complexe — découpe en étapes et exécute-les
|
|
</decision_making>
|
|
|
|
<error_recovery>
|
|
1. Lis le message d'erreur complet (stderr + stdout)
|
|
2. Identifie la cause racine (permissions, paquet manquant, config, service)
|
|
3. Essaie : vérifier le service, vérifier les logs, chercher le paquet, tester la connexion
|
|
4. Propose une solution concrète, pas générique
|
|
</error_recovery>
|
|
|
|
<response_format>
|
|
- **Commandes** : blocs markdown avec le langage (bash, sh, etc.)
|
|
- **Résultats** : résume les métriques clés, pas de dump complet
|
|
- **Erreurs** : cause + solution en 1-2 lignes
|
|
- **Succès** : confirmation en 1 ligne
|
|
- **Analyses** : markdown structuré avec sections si nécessaire
|
|
</response_format>
|
|
|
|
<mermaid>
|
|
Tu peux utiliser des diagrammes Mermaid pour visualiser :
|
|
- Architecture système (graph TD/LR)
|
|
- Flux réseau (sequenceDiagram)
|
|
- Processus (flowchart)
|
|
- Timeline (gantt)
|
|
|
|
Utilise un bloc de code avec le langage mermaid quand ça clarifie l'explication. Pas pour du texte simple.
|
|
</mermaid>
|
|
|
|
`
|
|
|
|
type ShellMessage struct {
|
|
ID string `json:"id"`
|
|
Role string `json:"role"`
|
|
Content string `json:"content"`
|
|
Time string `json:"time"`
|
|
}
|
|
|
|
type ShellConvStore struct {
|
|
mu sync.RWMutex
|
|
path string
|
|
msgs []ShellMessage
|
|
}
|
|
|
|
func NewShellConvStore() *ShellConvStore {
|
|
dir, err := config.ConfigDir()
|
|
if err != nil {
|
|
dir = "/tmp/muyue"
|
|
}
|
|
path := filepath.Join(dir, "shell_conversation.json")
|
|
s := &ShellConvStore{path: path}
|
|
s.load()
|
|
return s
|
|
}
|
|
|
|
func (s *ShellConvStore) load() {
|
|
data, err := os.ReadFile(s.path)
|
|
if err != nil {
|
|
s.msgs = []ShellMessage{}
|
|
return
|
|
}
|
|
json.Unmarshal(data, &s.msgs)
|
|
if s.msgs == nil {
|
|
s.msgs = []ShellMessage{}
|
|
}
|
|
}
|
|
|
|
func (s *ShellConvStore) save() {
|
|
data, _ := json.MarshalIndent(s.msgs, "", " ")
|
|
os.MkdirAll(filepath.Dir(s.path), 0755)
|
|
os.WriteFile(s.path, data, 0600)
|
|
}
|
|
|
|
func (s *ShellConvStore) Get() []ShellMessage {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
out := make([]ShellMessage, len(s.msgs))
|
|
copy(out, s.msgs)
|
|
return out
|
|
}
|
|
|
|
func (s *ShellConvStore) Add(role, content string) ShellMessage {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
msg := ShellMessage{
|
|
ID: time.Now().Format("20060102150405.000"),
|
|
Role: role,
|
|
Content: content,
|
|
Time: time.Now().Format(time.RFC3339),
|
|
}
|
|
s.msgs = append(s.msgs, msg)
|
|
s.save()
|
|
return msg
|
|
}
|
|
|
|
func (s *ShellConvStore) Clear() {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.msgs = []ShellMessage{}
|
|
s.save()
|
|
}
|
|
|
|
func (s *ShellConvStore) ApproxTokens() int {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
total := 0
|
|
for _, m := range s.msgs {
|
|
if m.Role == "system" {
|
|
continue
|
|
}
|
|
total += utf8.RuneCountInString(extractDisplayContent(m.Role, m.Content)) / shellCharsPerToken
|
|
}
|
|
total += utf8.RuneCountInString(shellSystemPromptBase) / shellCharsPerToken
|
|
if analysis := LoadSystemAnalysis(); analysis != "" {
|
|
total += utf8.RuneCountInString(analysis) / shellCharsPerToken
|
|
}
|
|
return total
|
|
}
|
|
|
|
func (s *ShellConvStore) AtLimit() bool {
|
|
return s.ApproxTokens() >= shellMaxTokens
|
|
}
|
|
|
|
func LoadSystemAnalysis() string {
|
|
dir, err := config.ConfigDir()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
data, err := os.ReadFile(filepath.Join(dir, "system_analysis.md"))
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return string(data)
|
|
}
|
|
|
|
func SaveSystemAnalysis(content string) error {
|
|
dir, err := config.ConfigDir()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
os.MkdirAll(dir, 0755)
|
|
return os.WriteFile(filepath.Join(dir, "system_analysis.md"), []byte(content), 0644)
|
|
}
|