feat(history): 消息正向增量拉取(A5) — GET /sessions/{id}/messages?after=
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- history/list_after.go: ListAfter(升序, created_at>after, 走覆盖索引, rowid兜底同毫秒)
- HistoryStore 新增 ListAfter(store+fakes同步)
- gateway history.go: handleListMessages解析after分流; after+before互斥400; 负数/非数字400
- 新增测试(list_after/history_after), build/vet/test绿+交叉编译
- Docs/api-client v0.26 + api-reference §3.4 同步
This commit is contained in:
AskaEth committed 2026-09-08 21:49:12 +08:00
1 parent 026b27b310
commit f43751fd07
11 files changed
+407 -35

No files matched your search

+44 -4
View File
@@ -111,9 +111,19 @@ func (s *Server) handleCreateSubSession(w http.ResponseWriter, r *http.Request)
writeJSON(w, http.StatusCreated, map[string]any{"sessionId": id})
}
// handleListMessages returns a page of messages for a session, newest first, with cursor
// pagination. It is the hot-paging history endpoint (design/16 §2.2, §5.1): limit defaults
// to 50 (clamped to 200), before is the oldest message's created_at from the previous page.
// handleListMessages returns a page of messages for a session with cursor pagination.
// It is the hot-paging history endpoint (design/16 §2.2, §5.1). Two mutually exclusive
// cursors are supported:
// - before (millisecond createdAt): newest-first page of messages strictly older than
// before — the default, used to page backwards through history;
// - after (millisecond createdAt): oldest-first page of messages strictly newer than
// after — forward incremental catch-up, so a client that reconnected can pull
// everything it missed from the newest createdAt it already has, without the
// one-page limit of a backward walk.
//
// limit defaults to 50 (clamped to 200) for both directions. platform filters the page
// by source platform in either direction. Supplying both before and after (both > 0)
// answers 400, as does a non-numeric/negative after or an invalid limit/before.
func (s *Server) handleListMessages(w http.ResponseWriter, r *http.Request) {
if s.history == nil {
writeError(w, &apperr.AppError{Code: "not_implemented", Message: "会话历史服务未配置(未注入 HistoryStore)", HTTPCode: http.StatusNotImplemented})
@@ -143,9 +153,24 @@ func (s *Server) handleListMessages(w http.ResponseWriter, r *http.Request) {
}
before = v
}
after, ok := parseAfterCursor(r)
if !ok {
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "invalid after"))
return
}
if before > 0 && after > 0 {
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "before and after are mutually exclusive"))
return
}
platform := r.URL.Query().Get("platform")
msgs, err := s.history.List(r.Context(), id, before, limit, platform)
var msgs []history.Message
var err error
if after > 0 {
msgs, err = s.history.ListAfter(r.Context(), id, after, limit, platform)
} else {
msgs, err = s.history.List(r.Context(), id, before, limit, platform)
}
if err != nil {
s.log.Error("history list messages", "err", err, "session", id)
writeError(w, apperr.Wrap(apperr.ErrInternal, "messages_failed", "failed to list messages"))
@@ -158,6 +183,21 @@ func (s *Server) handleListMessages(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, out)
}
// parseAfterCursor reads the non-negative "after" query param (a millisecond createdAt
// cursor). An absent/empty param yields (0, true) — no forward bound. A non-numeric or
// negative value yields (0, false) so the caller answers 400.
func parseAfterCursor(r *http.Request) (int64, bool) {
raw := r.URL.Query().Get("after")
if raw == "" {
return 0, true
}
v, err := strconv.ParseInt(raw, 10, 64)
if err != nil || v < 0 {
return 0, false
}
return v, true
}
// handleChannelPlatformLog returns up to limit messages for an IM platform across its
// isolated (group/stranger) sessions, newest first, for the admin adapter log page
// (design/16 §4.6 §4). It excludes the fixed main web session, so only the platform's