package ai import ( "encoding/json" "fmt" "os" "path/filepath" "time" ) // Session 桌面本地会话(目标态落盘;HTTP 路由可后挂)。 type Session struct { ID string `json:"id"` Title string `json:"title,omitempty"` Status string `json:"status"` // idle|running|incomplete|completed|failed|cancelled CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` Messages []Message `json:"messages"` Runs []SessionRun `json:"runs,omitempty"` } // SessionRun 一次运行摘要。 type SessionRun struct { RunID string `json:"runId"` Outcome string `json:"outcome"` IncompleteReason string `json:"incompleteReason,omitempty"` Resumable bool `json:"resumable"` StepsUsed int `json:"stepsUsed"` StepsBudget int `json:"stepsBudget"` FinishedAt string `json:"finishedAt"` } func sessionsDir() (string, error) { home, err := os.UserHomeDir() if err != nil { return "", err } return filepath.Join(home, ".opsxdb", "ai-sessions"), nil } // SessionPath 返回会话文件路径。 func SessionPath(id string) (string, error) { dir, err := sessionsDir() if err != nil { return "", err } return filepath.Join(dir, id+".json"), nil } // SaveSession 脱敏后写入磁盘。 func SaveSession(s Session) error { if s.ID == "" { return fmt.Errorf("session id required") } s.Messages = RedactMessages(s.Messages) s.UpdatedAt = time.Now().Format(time.RFC3339) if s.CreatedAt == "" { s.CreatedAt = s.UpdatedAt } path, err := SessionPath(s.ID) if err != nil { return err } if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { return err } b, err := json.MarshalIndent(s, "", " ") if err != nil { return err } return os.WriteFile(path, b, 0600) } // LoadSession 读会话;不存在返回 os.ErrNotExist。 func LoadSession(id string) (Session, error) { path, err := SessionPath(id) if err != nil { return Session{}, err } b, err := os.ReadFile(path) if err != nil { return Session{}, err } var s Session if err := json.Unmarshal(b, &s); err != nil { return Session{}, err } return s, nil }