package api import ( "encoding/json" "os" "path/filepath" "sync" "time" "unicode/utf8" "github.com/muyue/muyue/internal/config" ) const shellMaxTokens = 100000 const shellCharsPerToken = 4 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 { total += utf8.RuneCountInString(m.Content) / 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) }