feat(history): 会话消息持久化 + 主/次级会话(design/16)
- runtime/history: Store + HistoryStore 接口(Append/List 游标分页/Recent/DeleteSession/NewSubSession/ListSessions),主会话固定 web、次级 web:<uuid>
- db 迁移 0003: chat_sessions + chat_messages + idx_chat_msgs_session_created(事务幂等)
- gateway: GET/POST /sessions、GET /sessions/{id}/messages 热分页、DELETE /sessions/{id}(主不可删 403);Deps.History 可空注入
- 编排层落库: session_persist.go(submitInternal 写用户消息、completeTurn 写助手消息,拒绝的消息不落库)
- WS: wsEvent 加 sessionId 定向 + history 回放帧(握手上行后回放主会话最新 50 条,thinking 剥离)
- wire.go 装配 History 到 gateway.Deps.History
- 端到端实测: POST /sessions 建次级 201 / GET /sessions 列表 / POST /chat 后 /messages 读到 user+assistant / DELETE 次级成功 / DELETE 主会话 403
This commit is contained in:
1 parent
cc3eecacac
commit
48421923f6
15 files changed
+1305
-18
No files matched your search
@@ -37,6 +37,7 @@ import (
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/persona/loader"
|
||||
personareg "git.yeij.top/AskaEth/Cyrene_v2/internal/persona/registry"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/knowledge"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory"
|
||||
@@ -102,6 +103,19 @@ type MemoryStore interface {
|
||||
List(ctx context.Context) ([]memory.MemoryItem, error)
|
||||
}
|
||||
|
||||
// HistoryStore is the persistent chat-history store (design/16). *runtime/history.Store
|
||||
// is the production implementation. When nil the session/message endpoints degrade: list
|
||||
// sessions falls back to the channel-session view, and the create/messages/delete
|
||||
// endpoints answer with a clear 501.
|
||||
type HistoryStore interface {
|
||||
Append(ctx context.Context, msg history.Message) error
|
||||
List(ctx context.Context, sessionID string, before int64, limit int) ([]history.Message, error)
|
||||
Recent(ctx context.Context, sessionID string, n int) ([]history.Message, error)
|
||||
DeleteSession(ctx context.Context, sessionID string) error
|
||||
NewSubSession(ctx context.Context, ownerID string) (string, error)
|
||||
ListSessions(ctx context.Context) ([]history.SessionMeta, error)
|
||||
}
|
||||
|
||||
// IoTStore lists IoT devices. *iot.Manager is the production implementation.
|
||||
type IoTStore interface {
|
||||
List(ctx context.Context) ([]iot.Device, error)
|
||||
@@ -179,6 +193,9 @@ type Deps struct {
|
||||
Personas PersonaStore
|
||||
// Memories lists memory entries. Optional.
|
||||
Memories MemoryStore
|
||||
// History is the persistent chat-history store (design/16). Optional; when nil the
|
||||
// session/message endpoints degrade (list falls back to channel sessions, others 501).
|
||||
History HistoryStore
|
||||
// IoT lists IoT devices. Optional.
|
||||
IoT IoTStore
|
||||
// Scheduler audits sub-agent context for the subagent_ask WS route.
|
||||
@@ -229,6 +246,7 @@ type Server struct {
|
||||
sessions SessionStore
|
||||
personas PersonaStore
|
||||
memories MemoryStore
|
||||
history HistoryStore
|
||||
iot IoTStore
|
||||
auditor SubagentAuditor
|
||||
taskOutputs TaskOutputStore
|
||||
@@ -270,6 +288,7 @@ func New(cfg *config.Config, deps Deps) (*Server, error) {
|
||||
sessions: deps.Session,
|
||||
personas: deps.Personas,
|
||||
memories: deps.Memories,
|
||||
history: deps.History,
|
||||
iot: deps.IoT,
|
||||
auditor: deps.Scheduler,
|
||||
taskOutputs: deps.Tasks,
|
||||
@@ -305,6 +324,15 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.Handle("GET /api/v1/sessions", readAuth(http.HandlerFunc(s.handleListSessions)))
|
||||
mux.Handle("GET /api/v1/personas", readAuth(http.HandlerFunc(s.handleListPersonas)))
|
||||
|
||||
// Session/model reads and writes are the owner's private history; owner/admin only
|
||||
// (chat uses the same roles). Session metadata in GET /sessions stays readAuth for
|
||||
// any authenticated role, but the message content (and thinking) is owner/admin-only
|
||||
// per design/16 §6. Creating and deleting a sub session mutate persistent state.
|
||||
historyAuth := s.auth.Authorize(auth.RoleOwner, auth.RoleAdmin)
|
||||
mux.Handle("GET /api/v1/sessions/{id}/messages", historyAuth(http.HandlerFunc(s.handleListMessages)))
|
||||
mux.Handle("POST /api/v1/sessions", historyAuth(http.HandlerFunc(s.handleCreateSubSession)))
|
||||
mux.Handle("DELETE /api/v1/sessions/{id}", historyAuth(http.HandlerFunc(s.handleDeleteSession)))
|
||||
|
||||
// Realtime.
|
||||
mux.HandleFunc("/ws/chat", s.handleWS)
|
||||
|
||||
@@ -425,9 +453,10 @@ func newOrchestrator(s *Server) *Orchestrator {
|
||||
return s.hub.runTurn(ctx, sessionID, messages, env)
|
||||
},
|
||||
Emit: func(sessionID, typ string, data map[string]any) {
|
||||
s.hub.broadcast(wsEvent{Type: typ, Data: data})
|
||||
s.hub.broadcast(wsEvent{Type: typ, SessionID: sessionID, Data: data})
|
||||
},
|
||||
Log: s.log,
|
||||
Log: s.log,
|
||||
History: s.history,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user