llm mail integration
This commit is contained in:
133
internal/agents/task/agent.go
Normal file
133
internal/agents/task/agent.go
Normal file
@@ -0,0 +1,133 @@
|
||||
// task/agent.go – Task-Agent: add/list/done/delete
|
||||
package task
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"my-brain-importer/internal/agents"
|
||||
)
|
||||
|
||||
// Agent verwaltet Aufgaben über tasks.json.
|
||||
type Agent struct {
|
||||
store *Store
|
||||
}
|
||||
|
||||
// New erstellt einen neuen Task-Agenten.
|
||||
func New() *Agent {
|
||||
return &Agent{store: NewStore()}
|
||||
}
|
||||
|
||||
// Handle unterstützt: add, list, done, delete
|
||||
func (a *Agent) Handle(req agents.Request) agents.Response {
|
||||
switch req.Action {
|
||||
case agents.ActionAdd:
|
||||
return a.add(req)
|
||||
case agents.ActionList:
|
||||
return a.list()
|
||||
case agents.ActionDone:
|
||||
return a.done(req)
|
||||
case agents.ActionDelete:
|
||||
return a.del(req)
|
||||
default:
|
||||
return agents.Response{Text: "❌ Unbekannte Task-Aktion. Verfügbar: add, list, done, delete"}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Agent) add(req agents.Request) agents.Response {
|
||||
if len(req.Args) == 0 {
|
||||
return agents.Response{Text: "❌ Kein Task-Text angegeben."}
|
||||
}
|
||||
text := strings.Join(req.Args, " ")
|
||||
t, err := a.store.Add(text)
|
||||
if err != nil {
|
||||
return agents.Response{Error: err, Text: fmt.Sprintf("❌ Fehler: %v", err)}
|
||||
}
|
||||
shortID := t.ID
|
||||
if len(shortID) > 6 {
|
||||
shortID = shortID[len(shortID)-6:]
|
||||
}
|
||||
return agents.Response{Text: fmt.Sprintf("✅ Task hinzugefügt: `%s` (ID: `%s`)", t.Text, shortID)}
|
||||
}
|
||||
|
||||
func (a *Agent) list() agents.Response {
|
||||
tasks, err := a.store.Load()
|
||||
if err != nil {
|
||||
return agents.Response{Error: err, Text: fmt.Sprintf("❌ Fehler: %v", err)}
|
||||
}
|
||||
if len(tasks) == 0 {
|
||||
return agents.Response{Text: "📋 Keine Tasks vorhanden."}
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("📋 **Task-Liste:**\n\n")
|
||||
openCount := 0
|
||||
for _, t := range tasks {
|
||||
status := "⬜"
|
||||
if t.Done {
|
||||
status = "✅"
|
||||
} else {
|
||||
openCount++
|
||||
}
|
||||
shortID := t.ID
|
||||
if len(shortID) > 6 {
|
||||
shortID = shortID[len(shortID)-6:]
|
||||
}
|
||||
fmt.Fprintf(&sb, "%s `%s` – %s\n", status, shortID, t.Text)
|
||||
}
|
||||
fmt.Fprintf(&sb, "\n*%d offen, %d gesamt*", openCount, len(tasks))
|
||||
return agents.Response{Text: sb.String()}
|
||||
}
|
||||
|
||||
func (a *Agent) done(req agents.Request) agents.Response {
|
||||
if len(req.Args) == 0 {
|
||||
return agents.Response{Text: "❌ Keine Task-ID angegeben."}
|
||||
}
|
||||
id := req.Args[0]
|
||||
tasks, err := a.store.Load()
|
||||
if err != nil {
|
||||
return agents.Response{Error: err, Text: fmt.Sprintf("❌ Fehler: %v", err)}
|
||||
}
|
||||
fullID := resolveID(tasks, id)
|
||||
if fullID == "" {
|
||||
return agents.Response{Text: fmt.Sprintf("❌ Task `%s` nicht gefunden.", id)}
|
||||
}
|
||||
if err := a.store.MarkDone(fullID); err != nil {
|
||||
return agents.Response{Error: err, Text: fmt.Sprintf("❌ Fehler: %v", err)}
|
||||
}
|
||||
return agents.Response{Text: fmt.Sprintf("✅ Task `%s` als erledigt markiert.", id)}
|
||||
}
|
||||
|
||||
func (a *Agent) del(req agents.Request) agents.Response {
|
||||
if len(req.Args) == 0 {
|
||||
return agents.Response{Text: "❌ Keine Task-ID angegeben."}
|
||||
}
|
||||
id := req.Args[0]
|
||||
tasks, err := a.store.Load()
|
||||
if err != nil {
|
||||
return agents.Response{Error: err, Text: fmt.Sprintf("❌ Fehler: %v", err)}
|
||||
}
|
||||
fullID := resolveID(tasks, id)
|
||||
if fullID == "" {
|
||||
return agents.Response{Text: fmt.Sprintf("❌ Task `%s` nicht gefunden.", id)}
|
||||
}
|
||||
if err := a.store.Delete(fullID); err != nil {
|
||||
return agents.Response{Error: err, Text: fmt.Sprintf("❌ Fehler: %v", err)}
|
||||
}
|
||||
return agents.Response{Text: fmt.Sprintf("🗑️ Task `%s` gelöscht.", id)}
|
||||
}
|
||||
|
||||
// resolveID findet eine vollständige ID aus einer vollständigen oder kurzen (letzte 6 Zeichen).
|
||||
func resolveID(tasks []Task, id string) string {
|
||||
for _, t := range tasks {
|
||||
if t.ID == id {
|
||||
return t.ID
|
||||
}
|
||||
}
|
||||
for _, t := range tasks {
|
||||
if len(t.ID) >= 6 && t.ID[len(t.ID)-6:] == id {
|
||||
return t.ID
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
172
internal/agents/task/store.go
Normal file
172
internal/agents/task/store.go
Normal file
@@ -0,0 +1,172 @@
|
||||
// task/store.go – JSON-Persistenz für Tasks (atomisches Schreiben)
|
||||
package task
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"my-brain-importer/internal/config"
|
||||
)
|
||||
|
||||
// Task repräsentiert eine Aufgabe.
|
||||
type Task struct {
|
||||
ID string `json:"id"`
|
||||
Text string `json:"text"`
|
||||
Done bool `json:"done"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
DoneAt *time.Time `json:"done_at,omitempty"`
|
||||
}
|
||||
|
||||
// Store verwaltet tasks.json mit atomischen Schreiboperationen.
|
||||
type Store struct {
|
||||
mu sync.Mutex
|
||||
path string
|
||||
}
|
||||
|
||||
// NewStore erstellt einen Store mit dem Pfad aus der Config.
|
||||
func NewStore() *Store {
|
||||
path := config.Cfg.Tasks.StorePath
|
||||
if path == "" {
|
||||
path = "./tasks.json"
|
||||
}
|
||||
return &Store{path: path}
|
||||
}
|
||||
|
||||
// Load liest alle Tasks aus der JSON-Datei.
|
||||
func (s *Store) Load() ([]Task, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
data, err := os.ReadFile(s.path)
|
||||
if os.IsNotExist(err) {
|
||||
return []Task{}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tasks lesen: %w", err)
|
||||
}
|
||||
|
||||
var tasks []Task
|
||||
if err := json.Unmarshal(data, &tasks); err != nil {
|
||||
return nil, fmt.Errorf("tasks parsen: %w", err)
|
||||
}
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
// save schreibt alle Tasks atomisch. Muss unter mu aufgerufen werden.
|
||||
func (s *Store) save(tasks []Task) error {
|
||||
data, err := json.MarshalIndent(tasks, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("tasks serialisieren: %w", err)
|
||||
}
|
||||
tmp := s.path + ".tmp"
|
||||
if err := os.WriteFile(tmp, data, 0644); err != nil {
|
||||
return fmt.Errorf("temp-datei schreiben: %w", err)
|
||||
}
|
||||
return os.Rename(tmp, s.path)
|
||||
}
|
||||
|
||||
// Add fügt einen neuen Task hinzu.
|
||||
func (s *Store) Add(text string) (Task, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
tasks, err := s.loadLocked()
|
||||
if err != nil {
|
||||
return Task{}, err
|
||||
}
|
||||
|
||||
t := Task{
|
||||
ID: fmt.Sprintf("%d", time.Now().UnixNano()),
|
||||
Text: text,
|
||||
Done: false,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
tasks = append(tasks, t)
|
||||
|
||||
if err := s.save(tasks); err != nil {
|
||||
return Task{}, err
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// MarkDone markiert einen Task als erledigt.
|
||||
func (s *Store) MarkDone(id string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
tasks, err := s.loadLocked()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
found := false
|
||||
now := time.Now()
|
||||
for i, t := range tasks {
|
||||
if t.ID == id {
|
||||
tasks[i].Done = true
|
||||
tasks[i].DoneAt = &now
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("task %q nicht gefunden", id)
|
||||
}
|
||||
return s.save(tasks)
|
||||
}
|
||||
|
||||
// Delete löscht einen Task.
|
||||
func (s *Store) Delete(id string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
tasks, err := s.loadLocked()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newTasks := tasks[:0]
|
||||
for _, t := range tasks {
|
||||
if t.ID != id {
|
||||
newTasks = append(newTasks, t)
|
||||
}
|
||||
}
|
||||
if len(newTasks) == len(tasks) {
|
||||
return fmt.Errorf("task %q nicht gefunden", id)
|
||||
}
|
||||
return s.save(newTasks)
|
||||
}
|
||||
|
||||
// OpenTasks gibt alle offenen Tasks zurück.
|
||||
func (s *Store) OpenTasks() ([]Task, error) {
|
||||
tasks, err := s.Load()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var open []Task
|
||||
for _, t := range tasks {
|
||||
if !t.Done {
|
||||
open = append(open, t)
|
||||
}
|
||||
}
|
||||
return open, nil
|
||||
}
|
||||
|
||||
// loadLocked liest ohne eigenes Lock (muss unter s.mu aufgerufen werden).
|
||||
func (s *Store) loadLocked() ([]Task, error) {
|
||||
data, err := os.ReadFile(s.path)
|
||||
if os.IsNotExist(err) {
|
||||
return []Task{}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tasks lesen: %w", err)
|
||||
}
|
||||
var tasks []Task
|
||||
if err := json.Unmarshal(data, &tasks); err != nil {
|
||||
return nil, fmt.Errorf("tasks parsen: %w", err)
|
||||
}
|
||||
return tasks, nil
|
||||
}
|
||||
Reference in New Issue
Block a user