Files
smartmate/backend/middleware/cache_deleter.go
Losita 0f749e9f5a Version: 0.9.32.dev.260419
后端:
1. 会话历史接口切换为统一时间线读取,并兼容 extra.resume 恢复协议
  - api/agent.go:新增 resume->confirm_action 映射(approve/reject/cancel),恢复请求缺 conversation_id 时拦截;GetConversationHistory 改为 GetConversationTimeline
  - routers/routers.go:路由从 GET /conversation-history 切换为 GET /conversation-timeline
  - model/agent.go:删除 GetConversationHistoryItem 旧 DTO
2. 新增会话时间线持久化链路(MySQL + Redis)
  - 新增 model/agent_timeline.go:定义 timeline kind、AgentTimelineEvent、持久化/返回结构
  - 新增 dao/agent_timeline.go:写入事件、按 seq 查询、查询 max seq
  - inits/mysql.go:AutoMigrate 增加 AgentTimelineEvent
  - dao/cache.go:新增 timeline list/seq key,支持 incr/set seq、append/list、全量回填与删除
  - 新增 service/agentsvc/agent_timeline.go:时间线读写编排(Redis 优先、DB 回源、seq 分配与冲突重试、extra 事件映射)
3. 聊天主链路改为写入 timeline,旧 history 服务下线
  - service/agentsvc/agent.go:普通聊天用户/助手消息改为 appendConversationTimelineEvent
  - service/agentsvc/agent_newagent.go:透传 resume_interaction_id;注入 emitter extra hook 持久化卡片事件;正文写入 timeline
  - 删除 service/agentsvc/agent_history.go:下线 conversation-history 旧缓存编排
4. newAgent 恢复与确认防串单增强
  - newAgent/model/graph_run_state.go:AgentGraphRequest 新增 ResumeInteractionID
  - newAgent/node/agent_nodes.go:透传 ResumeInteractionID
  - newAgent/node/chat.go:增加 stale_resume 校验;accept/reject 兼容 approve/cancel;非法动作返回 invalid_confirm_action
  - newAgent/stream/emitter.go:新增 extraEventHook / SetExtraEventHook,在 extra-only 与 confirm 事件触发
5. 日程暂存后同步刷新预览缓存,避免读到拖拽前旧数据
  - service/agentsvc/agent_schedule_state.go:Save 后重建并覆盖 preview 缓存,保留 trace/candidate 等字段
6. 缓存失效策略调整到 timeline 口径
  - middleware/cache_deleter.go:移除 conversation-history 失效逻辑;ChatHistory/AgentChat/AgentTimelineEvent 加入忽略集合

前端:
7. 新增时间线接口与类型定义
  - frontend/src/api/schedule_agent.ts:新增 TimelineEvent/TimelineToolPayload/TimelineConfirmPayload 与 getConversationTimeline
8. AssistantPanel 全面对接 timeline 重建消息与卡片
  - frontend/src/components/dashboard/AssistantPanel.vue:移除旧 history merge/normalize,新增 rebuildStateFromTimeline;支持 execution mode(always_execute);支持 resume-only 发送;修复 confirm 弹层手动关闭后重复弹出;会话标题显示放宽;流式中隐藏 action bar
9. 精排弹窗健壮性与交互动效优化
  - frontend/src/components/assistant/ScheduleFineTuneModal.vue:previewData 支持 nullable,新增 visible 控制与 watch 初始化,补齐空值保护并调整弹窗动画

仓库:
10. 新增前端时间线接入说明文档
  - docs/frontend/newagent_timeline_对接说明.md:接口、kind、payload、刷新重建与迁移建议
2026-04-19 19:03:41 +08:00

173 lines
6.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package middleware
import (
"context"
"log"
"reflect"
"strings"
"github.com/LoveLosita/smartflow/backend/dao"
"github.com/LoveLosita/smartflow/backend/model"
"gorm.io/gorm"
)
// GormCachePlugin 负责在 GORM 写操作成功后,按模型类型触发对应缓存失效。
//
// 职责边界:
// 1. 只负责“识别模型 -> 调用对应缓存删除逻辑”;
// 2. 不负责业务事务提交,也不负责缓存回填;
// 3. 只处理当前项目真正依赖的前台读取缓存,未接缓存的模型应静默忽略。
type GormCachePlugin struct {
cacheDAO *dao.CacheDAO
}
func NewGormCachePlugin(dao *dao.CacheDAO) *GormCachePlugin {
return &GormCachePlugin{
cacheDAO: dao,
}
}
// Name 返回 GORM 插件名。
func (p *GormCachePlugin) Name() string {
return "GormCachePlugin"
}
// Initialize 注册 create/update/delete 成功后的统一失效钩子。
func (p *GormCachePlugin) Initialize(db *gorm.DB) error {
_ = db.Callback().Create().After("gorm:create").Register("clear_related_cache_after_create", p.afterWrite)
_ = db.Callback().Update().After("gorm:update").Register("clear_related_cache_after_update", p.afterWrite)
_ = db.Callback().Delete().After("gorm:delete").Register("clear_related_cache_after_delete", p.afterWrite)
return nil
}
func (p *GormCachePlugin) afterWrite(db *gorm.DB) {
if db.Error != nil || db.Statement.Schema == nil {
return
}
// 1. 先剥掉所有指针,拿到真实模型值。
// 2. 若本次写入的是切片,按“切片元素类型”分发缓存逻辑即可。
val := reflect.Indirect(reflect.ValueOf(db.Statement.Model))
if val.Kind() == reflect.Slice {
if val.Len() > 0 {
p.dispatchCacheLogic(val.Index(0).Interface())
}
return
}
p.dispatchCacheLogic(val.Interface())
}
// dispatchCacheLogic 根据模型类型决定是否需要缓存失效。
//
// 步骤说明:
// 1. 先匹配真正有前台缓存读取依赖的模型,命中后执行对应删除逻辑;
// 2. 对已确认“不需要缓存失效”的模型显式静默忽略,避免正常链路反复刷屏;
// 3. 只有未知模型才打印日志,方便后续补齐遗漏的缓存策略。
func (p *GormCachePlugin) dispatchCacheLogic(modelObj interface{}) {
switch m := modelObj.(type) {
case model.Schedule:
p.invalidScheduleCache(m.UserID, m.Week)
case model.TaskClass:
p.invalidTaskClassCache(*m.UserID)
case model.Task:
p.invalidTaskCache(m.UserID)
case model.AgentScheduleState:
p.invalidSchedulePlanPreviewCache(m.UserID, m.ConversationID)
case model.MemoryItem:
// 1. 管理面删除/修改/恢复/新增记忆时,自动失效该用户所有会话的预取缓存;
// 2. repo 方法通过 Model(&model.MemoryItem{UserID: userID}) 携带 userID
// 此处从模型实例中提取 UserID 进行精准失效;
// 3. 若 UserID 为 0无 userID 参数的 repo 方法invalidMemoryPrefetchCache 内部守卫会直接跳过。
p.invalidMemoryPrefetchCache(m.UserID)
case model.AgentOutboxMessage,
model.User,
model.ChatHistory,
model.AgentChat,
model.AgentTimelineEvent,
model.AgentStateSnapshotRecord,
model.MemoryJob,
model.MemoryAuditLog,
model.MemoryUserSetting:
// 这些模型当前没有前台缓存读取链路依赖,故意静默忽略。
return
default:
log.Printf("[GORM-Cache] No logic defined for model: %T", modelObj)
}
}
func (p *GormCachePlugin) invalidScheduleCache(userID int, week int) {
if userID == 0 || week == 0 {
return
}
// 1. 异步删除缓存,避免阻塞主事务提交。
// 2. 周视图变化后,同时清今天/最近完成/进行中缓存,保证口径一致。
go func() {
_ = p.cacheDAO.DeleteUserWeeklyScheduleFromCache(context.Background(), userID, week)
_ = p.cacheDAO.DeleteUserTodayScheduleFromCache(context.Background(), userID)
_ = p.cacheDAO.DeleteUserRecentCompletedSchedulesFromCache(context.Background(), userID)
_ = p.cacheDAO.DeleteUserOngoingScheduleFromCache(context.Background(), userID)
log.Printf("[GORM-Cache] Invalidated cache for user %d, week %d", userID, week)
}()
}
func (p *GormCachePlugin) invalidTaskClassCache(userID int) {
if userID == 0 {
return
}
go func() {
_ = p.cacheDAO.DeleteTaskClassList(context.Background(), userID)
log.Printf("[GORM-Cache] Invalidated task class list cache for user %d", userID)
}()
}
func (p *GormCachePlugin) invalidTaskCache(userID int) {
if userID == 0 {
return
}
go func() {
_ = p.cacheDAO.DeleteUserTasksFromCache(context.Background(), userID)
log.Printf("[GORM-Cache] Invalidated task list cache for user %d", userID)
}()
}
func (p *GormCachePlugin) invalidSchedulePlanPreviewCache(userID int, conversationID string) {
normalizedConversationID := strings.TrimSpace(conversationID)
if userID == 0 || normalizedConversationID == "" {
return
}
go func() {
// 1. 排程快照被覆盖后,预览缓存必须同步删除,避免 Redis 里继续挂旧结果。
// 2. 删除失败只记日志,不影响主事务,因为缓存永远是可回源的副本。
if err := p.cacheDAO.DeleteSchedulePlanPreviewFromCache(context.Background(), userID, normalizedConversationID); err != nil {
log.Printf("[GORM-Cache] Failed to invalidate schedule preview cache for user %d conversation %s: %v", userID, normalizedConversationID, err)
return
}
log.Printf("[GORM-Cache] Invalidated schedule preview cache for user %d conversation %s", userID, normalizedConversationID)
}()
}
// invalidMemoryPrefetchCache 失效指定用户所有会话的记忆预取缓存。
//
// 步骤化说明:
// 1. 先守卫 userID==0无 userID 的 repo 方法(如 UpdateContentByID触发 callback 时直接跳过;
// 2. 异步调用 DeleteMemoryPrefetchCacheByUser按模式 smartflow:memory_prefetch:u:{userID}:c:* 批量删除;
// 3. 失败只记日志不阻塞主事务30 分钟 TTL 自然过期兜底。
func (p *GormCachePlugin) invalidMemoryPrefetchCache(userID int) {
if userID == 0 {
return
}
go func() {
if err := p.cacheDAO.DeleteMemoryPrefetchCacheByUser(context.Background(), userID); err != nil {
log.Printf("[GORM-Cache] Failed to invalidate memory prefetch cache for user %d: %v", userID, err)
return
}
log.Printf("[GORM-Cache] Invalidated memory prefetch cache for user %d", userID)
}()
}