diff --git a/internal/app.go b/internal/app.go index 6048cd2..6cbe486 100644 --- a/internal/app.go +++ b/internal/app.go @@ -56,7 +56,10 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.history.Projects = msg.Projects m.history.Loaded = true m.history.Scanning = false - return m, nil + return m, m.nextSummaryCmd() + case SummaryResultMsg: + m.applySummary(msg.SessionID, msg.Summary) + return m, m.nextSummaryCmd() } return m, nil } @@ -222,6 +225,34 @@ func (m *Model) resumeSelected() (*Model, tea.Cmd) { return m, nil } + +// --- AI 摘要 --- + +func (m *Model) applySummary(sessionID, summary string) { + if summary == "" { + return + } + for _, pd := range m.history.Projects { + for _, s := range pd.Sessions { + if s.ID == sessionID { + s.AISummary = summary + saveSummaryToCache(sessionID, summary) + return + } + } + } +} + +func (m *Model) nextSummaryCmd() tea.Cmd { + for _, pd := range m.history.Projects { + for _, s := range pd.Sessions { + if s.AISummary == "" && s.FilePath != "" && s.FirstMsg != "" { + return generateSummaryCmd(s.FilePath, s.ID) + } + } + } + return nil +} // --- Workspace 光标 --- func (m *Model) moveCursor(dir int) { diff --git a/internal/history.go b/internal/history.go index 0ba4b54..590cc7a 100644 --- a/internal/history.go +++ b/internal/history.go @@ -3,9 +3,11 @@ package internal import ( "bufio" "bytes" + "context" "encoding/json" "log" "os" + "os/exec" "path/filepath" "sort" "strings" @@ -25,6 +27,7 @@ type Session struct { FirstMsg string // 首条用户消息 (截断) AwaySummary string // away_summary 系统摘要 AISummary string // AI 生成的摘要 (缓存) + FilePath string // JSONL 文件完整路径 } // ProjectDir 按目录分组的会话集合 @@ -167,7 +170,7 @@ func scanSessionFile(path string, sessionID string) *Session { } defer f.Close() - s := &Session{ID: sessionID} + s := &Session{ID: sessionID, FilePath: path} scanner := bufio.NewScanner(f) scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) @@ -345,6 +348,7 @@ type cacheEntry struct { FirstMsg string `json:"firstMsg"` AwaySummary string `json:"awaySummary"` AISummary string `json:"aiSummary"` + FilePath string `json:"filePath"` } func (e *cacheEntry) ToSession(id string) *Session { @@ -353,6 +357,7 @@ func (e *cacheEntry) ToSession(id string) *Session { StartTime: e.StartTime, EndTime: e.EndTime, MsgCount: e.MsgCount, FirstMsg: e.FirstMsg, AwaySummary: e.AwaySummary, AISummary: e.AISummary, + FilePath: e.FilePath, } } @@ -362,6 +367,7 @@ func cacheEntryFrom(s *Session, modTime time.Time) *cacheEntry { StartTime: s.StartTime, EndTime: s.EndTime, MsgCount: s.MsgCount, FirstMsg: s.FirstMsg, AwaySummary: s.AwaySummary, AISummary: s.AISummary, + FilePath: s.FilePath, } } @@ -404,3 +410,88 @@ func truncStr(s string, maxRunes int) string { } return string(runes[:maxRunes]) + "..." } + +// --- AI 摘要生成 --- + +type SummaryResultMsg struct { + SessionID string + Summary string +} + +func generateSummaryCmd(filePath, sessionID string) tea.Cmd { + return func() tea.Msg { + summary := generateAISummary(filePath) + return SummaryResultMsg{SessionID: sessionID, Summary: summary} + } +} + +func generateAISummary(filePath string) string { + messages := extractMessagesForSummary(filePath) + if messages == "" { + return "" + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + prompt := "请用20字以内中文概括以下对话的主题,只输出概括文字,不加引号、标点或markdown格式:\n\n" + messages + cmd := exec.CommandContext(ctx, "claude", "-p", prompt) + output, err := cmd.Output() + if err != nil { + return "" + } + + summary := strings.TrimSpace(string(output)) + summary = strings.Trim(summary, "\"'`*") + summary = strings.TrimPrefix(summary, "概括:") + summary = strings.TrimPrefix(summary, "主题:") + if len(summary) > 60 { + summary = truncStr(summary, 60) + } + return summary +} + +func extractMessagesForSummary(path string) string { + f, err := os.Open(path) + if err != nil { + return "" + } + defer f.Close() + + var msgs []string + scanner := bufio.NewScanner(f) + scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) + + userCount := 0 + for scanner.Scan() { + if userCount >= 5 { + break + } + line := scanner.Bytes() + if !hasType(line) { + continue + } + if matchType(line, "user") { + if msg := extractUserMsg(line); msg != "" { + msgs = append(msgs, "用户: "+msg) + userCount++ + } + } + } + + return strings.Join(msgs, "\n") +} + +func saveSummaryToCache(sessionID, summary string) { + home, err := os.UserHomeDir() + if err != nil { + return + } + cache := loadCache(home) + entry, ok := cache[sessionID] + if !ok { + return + } + entry.AISummary = summary + saveCache(home, cache) +}