新增: AI 摘要生成 (claude -p)
扫描完成后自动逐个调用 claude -p 生成会话标题, 摘要缓存到 ~/.u-tabs/session-cache.json, 下次免重复调用
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user