Files
ttstd-infinite-canvas/backend/internal/app/cloud_agent_event_store.go
T

149 lines
5.7 KiB
Go

package app
import (
"encoding/json"
"fmt"
"log"
"yingce/backend/internal/model"
"yingce/backend/internal/repository"
)
const (
// cloudAgentRunEventDeltaLimit 是增量读取(sinceSeq)单次返回的条数上限,
// 也是 HTTP 上 eventLimit 允许的最大值(越界的请求由 handler 拒绝)。
// 内部调用方不受它约束:续轮收束按 cloudAgentContinuationEventLimit 显式取更大的页。
cloudAgentRunEventDeltaLimit = 500
// cloudAgentRunEventPageLimit 是运行详情默认返回的事件条数:等于内存里的尾部窗口,
// 因此默认读取不额外查事件表;要更早的记录由客户端通过 eventLimit 显式索取。
cloudAgentRunEventPageLimit = repository.CloudAgentJournalWindow
// cloudAgentEventWindowSanityLimit 是内存事件窗口的健全上限(窗口本身加一次转移
// 新产生的事件),超过它说明窗口没有按 CloudAgentJournalWindow 载入。
cloudAgentEventWindowSanityLimit = 512
)
// cloudAgentRunEventsForView 组装运行详情要返回的事件。
//
// 事件全量在 cloud_agent_event_records(一行一条 EventJSON,主键 run_id + sequence),
// 内存里只有最近一窗(repository.CloudAgentJournalWindow),所以两条读路径都要显式
// 说明"这一页在整条日志里的位置":
//
// - sinceSeq > 0:只返回该序号之后的增量(SSE 断线重连 / 滚动拉取),走
// `sequence > ? ORDER BY sequence LIMIT ?` 的 keyset 读,不载入更早的行。
// - sinceSeq == 0:返回最近 cloudAgentRunEventPageLimit 条;窗口不够时(eventLimit
// 大于窗口)再从事件表往前补一页,仍然只按 seq 读。
//
// 事件表里没有该运行的行时(升级前的旧检查点把事件放在 state_json 里),退回内存窗口,
// 保证老运行仍可读。
func (s *Service) cloudAgentRunEventsForView(userID string, run *model.CloudAgentExecution, state *cloudAgentRuntime, sinceSeq, limit int) []CloudAgentEvent {
if sinceSeq > 0 {
pageLimit := cloudAgentRunEventDeltaLimit
if limit > 0 && limit < pageLimit {
pageLimit = limit
}
rows, err := s.repo.CloudAgentEventRecords(userID, run.ID, sinceSeq, pageLimit)
if err != nil {
log.Printf("agent event delta %s: %v", run.ID, err)
rows = nil
}
events := make([]CloudAgentEvent, 0, pageLimit)
for _, row := range rows {
events = append(events, cloudAgentEventFromRecord(row))
}
// 数据库页已达到上限时不能再拼接内存窗口,否则可能超过请求的页大小,
// 或在表中间隔着未返回的历史记录直接跳到窗口尾部,制造序号缺口。
if len(events) == pageLimit {
return events
}
last := sinceSeq
if len(rows) > 0 {
last = rows[len(rows)-1].Sequence
}
// 表未填满这一页时,内存里可能还有本次转移尚未落库的事件,补在末尾。
for _, event := range state.Events {
if event.Seq > last {
events = append(events, event)
if len(events) == pageLimit {
break
}
}
}
if len(events) > 0 {
return events
}
// 表与窗口都没有更新的内容:按游标过滤内存窗口(旧检查点的运行走这里)。
pending := make([]CloudAgentEvent, 0, len(state.Events))
for _, event := range state.Events {
if event.Seq > sinceSeq {
pending = append(pending, event)
if len(pending) == pageLimit {
break
}
}
}
return pending
}
tail := state.Events
if limit <= 0 {
limit = cloudAgentRunEventPageLimit
}
if len(tail) >= limit {
return append([]CloudAgentEvent(nil), tail[len(tail)-limit:]...)
}
// 窗口已经是全量(水位 0):没有更早的记录可补,直接返回。
if state.EventSeqBase == 0 {
return append([]CloudAgentEvent(nil), tail...)
}
rows, err := s.repo.CloudAgentEventRecordsBefore(userID, run.ID, state.EventSeqBase+1, limit-len(tail))
if err != nil {
log.Printf("agent event page %s: %v", run.ID, err)
return append([]CloudAgentEvent(nil), tail...)
}
events := make([]CloudAgentEvent, 0, len(rows)+len(tail))
for _, row := range rows {
events = append(events, cloudAgentEventFromRecord(row))
}
return append(events, tail...)
}
// cloudAgentEventFromRecord 把事件行解析成对外契约结构。
func cloudAgentEventFromRecord(row model.CloudAgentEventRecord) CloudAgentEvent {
var event CloudAgentEvent
if err := json.Unmarshal([]byte(row.EventJSON), &event); err != nil || event.EventID == "" {
// 行存在但解不出来:仍按行给出身份与占位类型,避免整页读取失败。
// 这条事件不代表运行的真实动作,客户端应以服务端画布与任务状态为准。
return CloudAgentEvent{
EventID: fmt.Sprintf("%s:%d", row.RunID, row.Sequence), RunID: row.RunID, Seq: row.Sequence,
Type: "unreadable_event", Payload: map[string]any{"text": "该事件记录无法解析,请以服务端画布与任务状态为准"}, CreatedAt: row.CreatedAt,
}
}
if event.RunID == "" {
event.RunID = row.RunID
}
if event.Seq == 0 {
event.Seq = row.Sequence
}
if event.CreatedAt.IsZero() {
event.CreatedAt = row.CreatedAt
}
return event
}
// cloudAgentRunEventCount 给出"这个运行一共产生过多少事件":以事件表条数为准,再补上
// 本次转移还没落库的部分(内存里 seq 高于水位的窗口)与检查点水位,取三者最大值。
func (s *Service) cloudAgentRunEventCount(userID string, run *model.CloudAgentExecution, state *cloudAgentRuntime) int {
stored, err := s.repo.CloudAgentEventRecordCount(userID, run.ID)
if err != nil {
log.Printf("agent event count %s: %v", run.ID, err)
stored = 0
}
total := int(stored)
if pending := state.EventSeqBase + len(state.Events); pending > total {
total = pending
}
if run != nil && run.EventCount > total {
total = run.EventCount
}
return total
}