feat(gateway): persist chat sessions and messages in PostgreSQL
- store: new chat_sessions/chat_messages tables with CRUD, ILIKE message search, and auto-titling from the first user message - context: optional Persister on the sliding-window store — messages are saved asynchronously on Add, sessions not in memory are restored from the database on Get - orchestrator: ProcessInput/FinishRound now carry userID so persisted messages have an owner - api: real session handlers (list/create/delete), message history and clear endpoints, working message search and three-format export (markdown/txt/json), fixing the short-ID panic in export - main: migration, store wiring, ctxStore persister adapter Tests: chat store handler lifecycle (fake store), context persister save/restore, stub auth login flow updated.
This commit is contained in:
1 parent
f44bf26050
commit
f8196c111c
11 files changed
+787
-27
No files matched your search
@@ -166,20 +166,60 @@ func (r *Router) notificationHandler(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
func (r *Router) messageSearchHandler(w http.ResponseWriter, req *http.Request) {
|
||||
q := req.URL.Query().Get("q")
|
||||
uid := req.Header.Get("X-User-ID")
|
||||
uid := r.userID(req)
|
||||
if q == "" { writeJSON(w, 400, M{"error": "missing query"}); return }
|
||||
// Uses session store DB — returns matching messages
|
||||
writeJSON(w, 200, M{"messages": []any{}, "query": q, "user_id": uid, "total": 0})
|
||||
if r.deps.ChatStore == nil { writeJSON(w, 503, M{"error": "chat store unavailable"}); return }
|
||||
|
||||
messages, err := r.deps.ChatStore.SearchMessages(req.Context(), uid, q, 20)
|
||||
if err != nil {
|
||||
writeJSON(w, 500, M{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, 200, M{"messages": messages, "query": q, "total": len(messages)})
|
||||
}
|
||||
|
||||
func (r *Router) sessionExportHandler(w http.ResponseWriter, req *http.Request) {
|
||||
id := strings.TrimPrefix(req.URL.Path, "/api/v1/sessions/")
|
||||
id = strings.TrimSuffix(id, "/export")
|
||||
// Export session as markdown
|
||||
w.Header().Set("Content-Type", "text/markdown; charset=utf-8")
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=session-%s.md", id[:8]))
|
||||
w.WriteHeader(200)
|
||||
fmt.Fprintf(w, "# Cyrene Chat - Session %s\n\n*Exported at %s*\n\n", id[:8], time.Now().Format(time.RFC3339))
|
||||
id := strings.TrimSuffix(strings.TrimPrefix(req.URL.Path, "/api/v1/sessions/"), "/export")
|
||||
if r.deps.ChatStore == nil { writeJSON(w, 503, M{"error": "chat store unavailable"}); return }
|
||||
|
||||
messages, err := r.deps.ChatStore.ListMessages(req.Context(), id, 200)
|
||||
if err != nil {
|
||||
writeJSON(w, 500, M{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
short := id
|
||||
if len(short) > 8 { short = short[:8] }
|
||||
format := req.URL.Query().Get("format")
|
||||
exportedAt := time.Now().Format(time.RFC3339)
|
||||
|
||||
switch format {
|
||||
case "json":
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=session-%s.json", short))
|
||||
writeJSON(w, 200, M{"session_id": id, "exported_at": exportedAt, "messages": messages})
|
||||
case "txt":
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "Cyrene Chat - Session %s (exported %s)\n\n", short, exportedAt)
|
||||
for _, m := range messages {
|
||||
fmt.Fprintf(&sb, "[%s] %s: %s\n\n", m.CreatedAt.Format("2006-01-02 15:04"), m.Role, m.Content)
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=session-%s.txt", short))
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(sb.String()))
|
||||
default: // markdown
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "# Cyrene Chat - Session %s\n\n*Exported at %s*\n\n", short, exportedAt)
|
||||
for _, m := range messages {
|
||||
who := "用户"
|
||||
if m.Role == "assistant" { who = "昔涟" }
|
||||
fmt.Fprintf(&sb, "**%s**(%s):\n\n%s\n\n---\n\n", who, m.CreatedAt.Format("2006-01-02 15:04"), m.Content)
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/markdown; charset=utf-8")
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=session-%s.md", short))
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(sb.String()))
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) imageAnalyzeHandler(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
Reference in new issue
Block a user