From 8c2c348657a011569ba233e7872fd0ea3322fe3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=AE=89=E5=93=B2?= Date: Mon, 4 May 2026 23:45:10 +0800 Subject: [PATCH] Align doctor with reportable session sources --- cmd/agenttrace/doctor.go | 57 +++++++++++++++++++++--- cmd/agenttrace/doctor_test.go | 81 ++++++++++++++++++++++++++++++++++- internal/i18n/i18n.go | 4 +- 3 files changed, 133 insertions(+), 9 deletions(-) diff --git a/cmd/agenttrace/doctor.go b/cmd/agenttrace/doctor.go index 9d8b648..977b6f6 100644 --- a/cmd/agenttrace/doctor.go +++ b/cmd/agenttrace/doctor.go @@ -18,6 +18,7 @@ type doctorReport struct { CacheEntries int `json:"cache_entries"` CacheDirs int `json:"cache_dirs"` CachedValid int `json:"cached_valid"` + Sessions int `json:"sessions"` SessionFiles int `json:"session_files"` Directories []doctorDirReport `json:"directories"` Recommendations []string `json:"recommendations"` @@ -44,7 +45,11 @@ func renderDoctorReport(dir string, demo bool, format string) (string, error) { func buildDoctorReport(dir string, demo bool) doctorReport { cache := engine.LoadSessionCache() - files := engine.FindSessionFilesCached(dir, cache) + files := engine.FindReportableSessionFilesCached(dir, cache) + var sqliteSessions []engine.Session + if dir == "" && !demo { + sqliteSessions = engine.LoadSQLiteBackedSessions() + } valid := engine.ValidCachedSessionCount(files, cache) mode := i18n.T("doctor_mode_auto") @@ -62,14 +67,15 @@ func buildDoctorReport(dir string, demo bool) doctorReport { CacheEntries: cache.EntryCount(), CacheDirs: len(cache.Dirs), CachedValid: valid, + Sessions: len(files) + len(sqliteSessions), SessionFiles: len(files), - Directories: doctorDirectories(dir, files), + Directories: doctorDirectories(dir, files, sqliteSessions), } report.Recommendations = doctorRecommendations(report, dir, demo) return report } -func doctorDirectories(dir string, files []string) []doctorDirReport { +func doctorDirectories(dir string, files []string, sqliteSessions []engine.Session) []doctorDirReport { var dirs []doctorDirReport if dir != "" { abs, err := filepath.Abs(dir) @@ -103,11 +109,47 @@ func doctorDirectories(dir string, files []string) []doctorDirReport { Files: countByRoot[candidate.Path], }) } + dirs = append(dirs, doctorSQLiteDirectories(sqliteSessions)...) + return dirs +} + +func doctorSQLiteDirectories(sessions []engine.Session) []doctorDirReport { + home, _ := os.UserHomeDir() + if home == "" { + return nil + } + countByTool := make(map[string]int) + for _, session := range sessions { + countByTool[session.Metrics.SourceTool]++ + } + + candidates := []struct { + tool string + name string + path string + }{ + {tool: "hermes_db", name: "Hermes Agent (DB)", path: filepath.Join(home, ".hermes", "state.db")}, + {tool: "opencode_db", name: "OpenCode (DB)", path: filepath.Join(home, ".local", "share", "opencode", "opencode.db")}, + } + + var dirs []doctorDirReport + for _, candidate := range candidates { + exists := fileExists(candidate.path) + if !exists && countByTool[candidate.tool] == 0 { + continue + } + dirs = append(dirs, doctorDirReport{ + Name: candidate.name, + Path: candidate.path, + Exists: exists, + Files: countByTool[candidate.tool], + }) + } return dirs } func doctorRecommendations(report doctorReport, dir string, demo bool) []string { - if report.SessionFiles == 0 { + if report.Sessions == 0 { if dir != "" { return []string{i18n.T("doctor_next_custom")} } @@ -129,7 +171,7 @@ func doctorReportText(report doctorReport) string { fmt.Fprintf(&b, "%s\n", i18n.T("doctor_title")) fmt.Fprintf(&b, "%s: %s\n", i18n.T("doctor_version"), report.Version) fmt.Fprintf(&b, "%s: %s\n", i18n.T("doctor_mode"), report.Mode) - fmt.Fprintf(&b, "%s: %d\n", i18n.T("doctor_session_files"), report.SessionFiles) + fmt.Fprintf(&b, "%s: %d\n", i18n.T("doctor_session_files"), report.Sessions) fmt.Fprintf(&b, "%s: %s\n", i18n.T("doctor_cache"), report.CachePath) fmt.Fprintf(&b, " "+i18n.T("doctor_cache_detail")+"\n", report.CacheEntries, report.CachedValid, report.CacheDirs) fmt.Fprintf(&b, "\n%s:\n", i18n.T("doctor_directories")) @@ -151,3 +193,8 @@ func dirExists(path string) bool { info, err := os.Stat(path) return err == nil && info.IsDir() } + +func fileExists(path string) bool { + info, err := os.Stat(path) + return err == nil && !info.IsDir() +} diff --git a/cmd/agenttrace/doctor_test.go b/cmd/agenttrace/doctor_test.go index 3e73395..22fa79b 100644 --- a/cmd/agenttrace/doctor_test.go +++ b/cmd/agenttrace/doctor_test.go @@ -1,12 +1,17 @@ package main import ( + "database/sql" "encoding/json" + "os" + "path/filepath" "strings" "testing" "github.com/luoyuctl/agenttrace/internal/engine" "github.com/luoyuctl/agenttrace/internal/i18n" + + _ "modernc.org/sqlite" ) func TestDoctorReportWithDemoSessions(t *testing.T) { @@ -24,7 +29,7 @@ func TestDoctorReportWithDemoSessions(t *testing.T) { if err := json.Unmarshal([]byte(out), &report); err != nil { t.Fatal(err) } - if report.Version != engine.Version || report.SessionFiles != 3 || len(report.Directories) != 1 { + if report.Version != engine.Version || report.Sessions != 3 || report.SessionFiles != 3 || len(report.Directories) != 1 { t.Fatalf("unexpected doctor report: %+v", report) } if !report.Directories[0].Exists || report.Directories[0].Files != 3 { @@ -35,6 +40,45 @@ func TestDoctorReportWithDemoSessions(t *testing.T) { } } +func TestDoctorReportUsesReportableSQLiteBackedSources(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config")) + t.Setenv("XDG_CACHE_HOME", filepath.Join(home, ".cache")) + + sessionsDir := filepath.Join(home, ".hermes", "sessions") + if err := os.MkdirAll(sessionsDir, 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(sessionsDir, "legacy.jsonl"), []byte("{}\n"), 0644); err != nil { + t.Fatal(err) + } + if err := writeDoctorHermesStateDBForTest(filepath.Join(home, ".hermes", "state.db")); err != nil { + t.Fatal(err) + } + + report := buildDoctorReport("", false) + if report.Sessions != 1 || report.SessionFiles != 0 { + t.Fatalf("doctor should split sessions from reportable files, got sessions=%d files=%d", report.Sessions, report.SessionFiles) + } + + var hermesFileRow, hermesDBRow *doctorDirReport + for i := range report.Directories { + switch report.Directories[i].Name { + case "Hermes Agent": + hermesFileRow = &report.Directories[i] + case "Hermes Agent (DB)": + hermesDBRow = &report.Directories[i] + } + } + if hermesFileRow == nil || hermesFileRow.Files != 0 { + t.Fatalf("sqlite-backed legacy file dir should be skipped, got %+v", hermesFileRow) + } + if hermesDBRow == nil || !hermesDBRow.Exists || hermesDBRow.Files != 1 { + t.Fatalf("sqlite-backed source should be shown, got %+v", hermesDBRow) + } +} + func TestDoctorReportChineseText(t *testing.T) { prev := i18n.Current i18n.SetLang(i18n.ZH) @@ -44,9 +88,42 @@ func TestDoctorReportChineseText(t *testing.T) { if err != nil { t.Fatal(err) } - for _, want := range []string{"AGENTTRACE 诊断", "会话文件", "建议"} { + for _, want := range []string{"AGENTTRACE 诊断", "会话", "建议"} { if !strings.Contains(out, want) { t.Fatalf("doctor output missing %q:\n%s", want, out) } } } + +func writeDoctorHermesStateDBForTest(path string) error { + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + return err + } + db, err := sql.Open("sqlite", path) + if err != nil { + return err + } + defer db.Close() + if _, err := db.Exec(`create table sessions ( + id text primary key, + model text, + started_at real, + ended_at real, + message_count integer, + tool_call_count integer, + input_tokens integer, + output_tokens integer, + cache_read_tokens integer, + cache_write_tokens integer + )`); err != nil { + return err + } + if _, err := db.Exec(`create table messages (session_id text, role text)`); err != nil { + return err + } + if _, err := db.Exec(`insert into sessions values ('db-session', 'gpt-5.1', 1760000000, 1760000060, 2, 1, 1000, 200, 50, 25)`); err != nil { + return err + } + _, err = db.Exec(`insert into messages values ('db-session', 'user'), ('db-session', 'assistant')`) + return err +} diff --git a/internal/i18n/i18n.go b/internal/i18n/i18n.go index dce8db9..2b5e1fb 100644 --- a/internal/i18n/i18n.go +++ b/internal/i18n/i18n.go @@ -1305,10 +1305,10 @@ var translations = map[string]map[Lang]string{ "doctor_mode_custom": {EN: "custom directory", ZH: "指定目录"}, "doctor_mode_demo": {EN: "demo sessions", ZH: "演示会话"}, "doctor_version": {EN: "Version", ZH: "版本"}, - "doctor_session_files": {EN: "Session files", ZH: "会话文件"}, + "doctor_session_files": {EN: "Sessions", ZH: "会话"}, "doctor_cache": {EN: "Cache", ZH: "缓存"}, "doctor_cache_detail": {EN: "%d entries, %d valid for current scan, %d cached directories", ZH: "%d 条记录,当前扫描命中 %d 条,%d 个目录缓存"}, - "doctor_directories": {EN: "Directories", ZH: "目录"}, + "doctor_directories": {EN: "Sources", ZH: "来源"}, "doctor_found": {EN: "found", ZH: "存在"}, "doctor_missing": {EN: "missing", ZH: "缺失"}, "doctor_recommendations": {EN: "Recommendations", ZH: "建议"},