新增: AI 摘要生成 (claude -p)

扫描完成后自动逐个调用 claude -p 生成会话标题,
摘要缓存到 ~/.u-tabs/session-cache.json, 下次免重复调用
This commit is contained in:
2026-05-16 23:33:06 +08:00
parent 3e81d4510f
commit e4ef8e02aa
2 changed files with 124 additions and 2 deletions

View File

@@ -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) {

View File

@@ -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)
}