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
This commit is contained in:
parent
e4f4afcd4d
commit
d4ebdb7107
1 changed files with 107 additions and 23 deletions
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
@ -35,6 +36,7 @@ type sessionFile struct {
|
||||||
// sessionListItem is a lightweight summary returned by GET /api/sessions.
|
// sessionListItem is a lightweight summary returned by GET /api/sessions.
|
||||||
type sessionListItem struct {
|
type sessionListItem struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Preview string `json:"preview"`
|
Preview string `json:"preview"`
|
||||||
MessageCount int `json:"message_count"`
|
MessageCount int `json:"message_count"`
|
||||||
|
|
@ -66,6 +68,59 @@ const (
|
||||||
maxSessionTitleRunes = 60
|
maxSessionTitleRunes = 60
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// extractChannelFromSessionKey extracts the channel name from a session key.
|
||||||
|
// Supports formats:
|
||||||
|
// - agent:main:pico:direct:pico:<uuid> -> "pico"
|
||||||
|
// - agent:main:telegram:direct:<peer> -> "telegram"
|
||||||
|
// - agent:main:discord:direct:<peer> -> "discord"
|
||||||
|
// - agent:main:<channel>:group:<peer> -> <channel>
|
||||||
|
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:<agentId>:<channel>:...
|
||||||
|
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_<channel>_<kind>_<peer>
|
||||||
|
// 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.
|
// extractPicoSessionID extracts the session UUID from a full session key.
|
||||||
// Returns the UUID and true if the key matches the Pico session pattern.
|
// Returns the UUID and true if the key matches the Pico session pattern.
|
||||||
func extractPicoSessionID(key string) (string, bool) {
|
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 {
|
func buildSessionListItem(sessionID string, sess sessionFile) sessionListItem {
|
||||||
|
return buildSessionListItemWithChannel(sessionID, "web", sess)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSessionListItemWithChannel(sessionID, channel string, sess sessionFile) sessionListItem {
|
||||||
preview := ""
|
preview := ""
|
||||||
for _, msg := range sess.Messages {
|
for _, msg := range sess.Messages {
|
||||||
if msg.Role == "user" && strings.TrimSpace(msg.Content) != "" {
|
if msg.Role == "user" && strings.TrimSpace(msg.Content) != "" {
|
||||||
|
|
@ -224,6 +283,7 @@ func buildSessionListItem(sessionID string, sess sessionFile) sessionListItem {
|
||||||
|
|
||||||
return sessionListItem{
|
return sessionListItem{
|
||||||
ID: sessionID,
|
ID: sessionID,
|
||||||
|
Channel: channel,
|
||||||
Title: title,
|
Title: title,
|
||||||
Preview: preview,
|
Preview: preview,
|
||||||
MessageCount: validMessageCount,
|
MessageCount: validMessageCount,
|
||||||
|
|
@ -302,19 +362,34 @@ func (h *Handler) handleListSessions(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
name := entry.Name()
|
name := entry.Name()
|
||||||
var (
|
var (
|
||||||
sessionID string
|
fullSessionID string // Full session ID including channel context
|
||||||
|
channel string // Channel name (web, telegram, discord, etc.)
|
||||||
|
uuid string // Session UUID
|
||||||
sess sessionFile
|
sess sessionFile
|
||||||
loadErr error
|
loadErr error
|
||||||
ok bool
|
ok bool
|
||||||
)
|
)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case strings.HasSuffix(name, ".jsonl"):
|
case isSessionJSONLFile(name):
|
||||||
sessionID, ok = extractPicoSessionIDFromSanitizedKey(strings.TrimSuffix(name, ".jsonl"))
|
// Try to parse as JSONL session
|
||||||
|
fullSessionID, channel, uuid, ok = extractSessionIDFromSanitizedKey(strings.TrimSuffix(name, ".jsonl"))
|
||||||
|
if !ok {
|
||||||
|
// Fallback to legacy pico parsing - use a local variable
|
||||||
|
fallbackSessionID, ok := extractPicoSessionIDFromSanitizedKey(strings.TrimSuffix(name, ".jsonl"))
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
sess, loadErr = h.readJSONLSession(dir, sessionID)
|
channel = "web"
|
||||||
|
uuid = fallbackSessionID
|
||||||
|
}
|
||||||
|
// 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) {
|
if loadErr == nil && isEmptySession(sess) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -322,17 +397,10 @@ func (h *Handler) handleListSessions(w http.ResponseWriter, r *http.Request) {
|
||||||
continue
|
continue
|
||||||
case filepath.Ext(name) == ".json":
|
case filepath.Ext(name) == ".json":
|
||||||
base := strings.TrimSuffix(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 _, 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))
|
data, err := os.ReadFile(filepath.Join(dir, name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
|
|
@ -343,11 +411,27 @@ func (h *Handler) handleListSessions(w http.ResponseWriter, r *http.Request) {
|
||||||
if isEmptySession(sess) {
|
if isEmptySession(sess) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
sessionID, ok = extractPicoSessionID(sess.Key)
|
// Extract session ID from the key field
|
||||||
if !ok {
|
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:<channel>:... 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
|
continue
|
||||||
}
|
}
|
||||||
if _, exists := seen[sessionID]; exists {
|
if _, exists := seen[fullSessionID]; exists {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|
@ -357,12 +441,12 @@ func (h *Handler) handleListSessions(w http.ResponseWriter, r *http.Request) {
|
||||||
if loadErr != nil {
|
if loadErr != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, exists := seen[sessionID]; exists {
|
if _, exists := seen[fullSessionID]; exists {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
seen[sessionID] = struct{}{}
|
seen[fullSessionID] = struct{}{}
|
||||||
items = append(items, buildSessionListItem(sessionID, sess))
|
items = append(items, buildSessionListItemWithChannel(fullSessionID, channel, sess))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by updated descending (most recent first)
|
// Sort by updated descending (most recent first)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue