From d4ebdb71077faa05c1eb9734a49b22a99c48f347 Mon Sep 17 00:00:00 2001 From: Huangting-xy <266665499+Huangting-xy@users.noreply.github.com> Date: Wed, 25 Mar 2026 20:14:52 +0800 Subject: [PATCH] fix(api): show all channel sessions in web UI, not just web/pico - Parse session key format to extract channel from all session types - Add 'channel' field to sessionListItem for filtering/grouping - Support sessions from telegram, discord, and other channels Fixes #1996 --- web/backend/api/session.go | 130 ++++++++++++++++++++++++++++++------- 1 file changed, 107 insertions(+), 23 deletions(-) diff --git a/web/backend/api/session.go b/web/backend/api/session.go index 42d451a05..98243a605 100644 --- a/web/backend/api/session.go +++ b/web/backend/api/session.go @@ -4,6 +4,7 @@ import ( "bufio" "encoding/json" "errors" + "fmt" "net/http" "os" "path/filepath" @@ -35,6 +36,7 @@ type sessionFile struct { // sessionListItem is a lightweight summary returned by GET /api/sessions. type sessionListItem struct { ID string `json:"id"` + Channel string `json:"channel"` Title string `json:"title"` Preview string `json:"preview"` MessageCount int `json:"message_count"` @@ -66,6 +68,59 @@ const ( maxSessionTitleRunes = 60 ) +// extractChannelFromSessionKey extracts the channel name from a session key. +// Supports formats: +// - agent:main:pico:direct:pico: -> "pico" +// - agent:main:telegram:direct: -> "telegram" +// - agent:main:discord:direct: -> "discord" +// - agent:main::group: -> +func extractChannelFromSessionKey(key string) string { + // Parse the key using the routing package's parser + parts := strings.SplitN(key, ":", 4) + if len(parts) >= 3 { + // Format is: agent:::... + channel := parts[2] + if channel == "pico" { + return "web" + } + return channel + } + return "unknown" +} + +// extractSessionIDFromSanitizedKey extracts session ID from sanitized filename. +// Returns the full session ID (including agent context) and the session UUID. +// For example, "agent_main_telegram_direct_123" -> "telegram:123" +func extractSessionIDFromSanitizedKey(key string) (fullID, channel, uuid string, ok bool) { + // Try pico session first + if strings.HasPrefix(key, sanitizedPicoSessionPrefix) { + picoUUID := strings.TrimPrefix(key, sanitizedPicoSessionPrefix) + return picoUUID, "web", picoUUID, true + } + // Try other channel formats: agent_main___ + // e.g., agent_main_telegram_direct_1200880918 + rest := strings.TrimPrefix(key, "agent_main_") + if rest == key { + return "", "", "", false // Not a session file + } + // Split by underscores: channel_kind_peer + parts := strings.SplitN(rest, "_", 3) + if len(parts) >= 3 { + channel := parts[0] + peer := parts[2] + if channel == "pico" { + return peer, "web", peer, true + } + return peer, channel, peer, true + } + return "", "", "", false +} + +// isSessionJSONLFile returns true if the filename is a JSONL session file. +func isSessionJSONLFile(name string) bool { + return strings.HasSuffix(name, ".jsonl") +} + // extractPicoSessionID extracts the session UUID from a full session key. // Returns the UUID and true if the key matches the Pico session pattern. func extractPicoSessionID(key string) (string, bool) { @@ -193,6 +248,10 @@ func (h *Handler) readJSONLSession(dir, sessionID string) (sessionFile, error) { } func buildSessionListItem(sessionID string, sess sessionFile) sessionListItem { + return buildSessionListItemWithChannel(sessionID, "web", sess) +} + +func buildSessionListItemWithChannel(sessionID, channel string, sess sessionFile) sessionListItem { preview := "" for _, msg := range sess.Messages { if msg.Role == "user" && strings.TrimSpace(msg.Content) != "" { @@ -224,6 +283,7 @@ func buildSessionListItem(sessionID string, sess sessionFile) sessionListItem { return sessionListItem{ ID: sessionID, + Channel: channel, Title: title, Preview: preview, MessageCount: validMessageCount, @@ -302,19 +362,34 @@ func (h *Handler) handleListSessions(w http.ResponseWriter, r *http.Request) { name := entry.Name() var ( - sessionID string - sess sessionFile - loadErr error - ok bool + fullSessionID string // Full session ID including channel context + channel string // Channel name (web, telegram, discord, etc.) + uuid string // Session UUID + sess sessionFile + loadErr error + ok bool ) switch { - case strings.HasSuffix(name, ".jsonl"): - sessionID, ok = extractPicoSessionIDFromSanitizedKey(strings.TrimSuffix(name, ".jsonl")) + case isSessionJSONLFile(name): + // Try to parse as JSONL session + fullSessionID, channel, uuid, ok = extractSessionIDFromSanitizedKey(strings.TrimSuffix(name, ".jsonl")) if !ok { - continue + // Fallback to legacy pico parsing - use a local variable + fallbackSessionID, ok := extractPicoSessionIDFromSanitizedKey(strings.TrimSuffix(name, ".jsonl")) + if !ok { + continue + } + channel = "web" + uuid = fallbackSessionID } - sess, loadErr = h.readJSONLSession(dir, sessionID) + // For non-pico sessions, we need to reconstruct the full key + if channel != "web" { + fullSessionID = fmt.Sprintf("%s:%s", channel, uuid) + } else { + fullSessionID = uuid + } + sess, loadErr = h.readJSONLSession(dir, uuid) if loadErr == nil && isEmptySession(sess) { continue } @@ -322,16 +397,9 @@ func (h *Handler) handleListSessions(w http.ResponseWriter, r *http.Request) { continue case filepath.Ext(name) == ".json": base := strings.TrimSuffix(name, ".json") + // Skip if there's a JSONL version if _, statErr := os.Stat(filepath.Join(dir, base+".jsonl")); statErr == nil { - if jsonlSessionID, found := extractPicoSessionIDFromSanitizedKey(base); found { - if jsonlSess, jsonlErr := h.readJSONLSession( - dir, - jsonlSessionID, - ); jsonlErr == nil && - !isEmptySession(jsonlSess) { - continue - } - } + continue } data, err := os.ReadFile(filepath.Join(dir, name)) if err != nil { @@ -343,11 +411,27 @@ func (h *Handler) handleListSessions(w http.ResponseWriter, r *http.Request) { if isEmptySession(sess) { continue } - sessionID, ok = extractPicoSessionID(sess.Key) - if !ok { + // Extract session ID from the key field + if strings.HasPrefix(sess.Key, "agent:") { + parsed := strings.SplitN(sess.Key, ":", 3) + if len(parsed) >= 3 { + fullSessionID = strings.TrimPrefix(sess.Key, "agent:") + // Extract channel from format: main::... or main:pico:... + rest := parsed[2] + if strings.HasPrefix(rest, "pico:") { + channel = "web" + } else { + channelParts := strings.SplitN(rest, ":", 2) + if len(channelParts) >= 1 { + channel = channelParts[0] + } + } + uuid = fullSessionID + } + } else { continue } - if _, exists := seen[sessionID]; exists { + if _, exists := seen[fullSessionID]; exists { continue } default: @@ -357,12 +441,12 @@ func (h *Handler) handleListSessions(w http.ResponseWriter, r *http.Request) { if loadErr != nil { continue } - if _, exists := seen[sessionID]; exists { + if _, exists := seen[fullSessionID]; exists { continue } - seen[sessionID] = struct{}{} - items = append(items, buildSessionListItem(sessionID, sess)) + seen[fullSessionID] = struct{}{} + items = append(items, buildSessionListItemWithChannel(fullSessionID, channel, sess)) } // Sort by updated descending (most recent first)