Files
smartmate/backend/services/agent/sv/agent_schedule_state.go
Losita 3b6fca44a6 Version: 0.9.77.dev.260505
后端:
1.阶段 6 CP4/CP5 目录收口与共享边界纯化
- 将 backend 根目录收口为 services、client、gateway、cmd、shared 五个一级目录
- 收拢 bootstrap、inits、infra/kafka、infra/outbox、conv、respond、pkg、middleware,移除根目录旧实现与空目录
- 将 utils 下沉到 services/userauth/internal/auth,将 logic 下沉到 services/schedule/core/planning
- 将迁移期 runtime 桥接实现统一收拢到 services/runtime/{conv,dao,eventsvc,model},删除 shared/legacy 与未再被 import 的旧 service 实现
- 将 gateway/shared/respond 收口为 HTTP/Gin 错误写回适配,shared/respond 仅保留共享错误语义与状态映射
- 将 HTTP IdempotencyMiddleware 与 RateLimitMiddleware 收口到 gateway/middleware
- 将 GormCachePlugin 下沉到 shared/infra/gormcache,将共享 RateLimiter 下沉到 shared/infra/ratelimit,将 agent token budget 下沉到 services/agent/shared
- 删除 InitEino 兼容壳,收缩 cmd/internal/coreinit 仅保留旧组合壳残留域初始化语义
- 更新微服务迁移计划与桌面 checklist,补齐 CP4/CP5 当前切流点、目录终态与验证结果
- 完成 go test ./...、git diff --check 与最终真实 smoke;health、register/login、task/create+get、schedule/today、task-class/list、memory/items、agent chat/meta/timeline/context-stats 全部 200,SSE 合并结果为 CP5_OK 且 [DONE] 只有 1 个
2026-05-05 23:25:07 +08:00

144 lines
5.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 sv
import (
"context"
"errors"
"fmt"
"log"
"strings"
agentconv "github.com/LoveLosita/smartflow/backend/services/agent/conv"
agentmodel "github.com/LoveLosita/smartflow/backend/services/agent/model"
agentshared "github.com/LoveLosita/smartflow/backend/services/agent/shared"
"github.com/LoveLosita/smartflow/backend/services/runtime/model"
"github.com/LoveLosita/smartflow/backend/shared/respond"
)
// SaveScheduleState 处理前端拖拽后的“暂存排程状态”请求。
//
// 职责边界:
// 1. 负责把前端绝对坐标写回当前会话的 ScheduleState 快照;
// 2. 负责刷新 Redis 预览缓存,保证后续预览读取与最新拖拽一致;
// 3. 不负责写 MySQL 正式课表,也不负责触发新一轮 graph 执行。
func (s *AgentService) SaveScheduleState(
ctx context.Context,
userID int,
conversationID string,
items []model.SaveScheduleStatePlacedItem,
) error {
// 1. 加载会话快照;没有快照说明当前会话不在可微调窗口内。
if s.agentStateStore == nil {
return errors.New("agent state store 未初始化")
}
snapshot, ok, err := s.agentStateStore.Load(ctx, conversationID)
if err != nil {
return fmt.Errorf("加载快照失败: %w", err)
}
if !ok || snapshot == nil || snapshot.ScheduleState == nil {
return respond.ScheduleStateSnapshotNotFound
}
// 2. 做会话归属校验,防止跨用户写入别人的会话快照。
if snapshot.RuntimeState != nil {
cs := snapshot.RuntimeState.EnsureCommonState()
if cs.UserID != 0 && cs.UserID != userID {
return fmt.Errorf("会话归属校验失败:快照 user_id=%d请求 user_id=%d", cs.UserID, userID)
}
}
// 3. 将前端绝对坐标应用到内存态 ScheduleState。
// 3.1 这里只修改 source=task_item 任务;
// 3.2 source=event 课程位保持不变;
// 3.3 坐标非法时由 ApplyPlacedItems 返回明确错误。
if err := agentconv.ApplyPlacedItems(snapshot.ScheduleState, items); err != nil {
return err
}
// 4. 先写回运行态快照,确保“拖拽后的状态”成为后续读链路真值。
if err := s.agentStateStore.Save(ctx, conversationID, snapshot); err != nil {
return fmt.Errorf("保存快照失败: %w", err)
}
// 5. 再刷新预览缓存,避免 GetSchedulePlanPreview 读到拖拽前旧缓存。
if err := s.refreshSchedulePreviewAfterStateSave(ctx, userID, conversationID, snapshot); err != nil {
return err
}
log.Printf("[INFO] schedule state saved chat=%s user=%d item_count=%d", conversationID, userID, len(items))
return nil
}
// refreshSchedulePreviewAfterStateSave 按“最新快照”重建并覆盖 Redis 预览缓存。
//
// 职责边界:
// 1. 只处理 Redis 预览缓存,不负责 MySQL 快照;
// 2. 以最新 ScheduleState 为准,修复“预览读到旧拖拽结果”的回滚问题;
// 3. 尽量保留旧预览中的 trace_id/candidate_plans避免前端字段突变。
func (s *AgentService) refreshSchedulePreviewAfterStateSave(
ctx context.Context,
userID int,
conversationID string,
snapshot *agentmodel.AgentStateSnapshot,
) error {
// 1. 依赖不完整时直接跳过,避免写入不完整缓存。
if s == nil || s.cacheDAO == nil || snapshot == nil || snapshot.ScheduleState == nil {
return nil
}
normalizedConversationID := strings.TrimSpace(conversationID)
if normalizedConversationID == "" {
return nil
}
// 2. 从运行态提取 task_class_ids保证预览过滤口径与会话一致。
taskClassIDs := make([]int, 0)
if snapshot.RuntimeState != nil {
flowState := snapshot.RuntimeState.EnsureCommonState()
taskClassIDs = append(taskClassIDs, flowState.TaskClassIDs...)
}
// 3. 基于最新 ScheduleState 生成预览主干hybrid_entries 为最新真值)。
preview := agentconv.ScheduleStateToPreview(
snapshot.ScheduleState,
userID,
normalizedConversationID,
taskClassIDs,
"",
)
if preview == nil {
return nil
}
// 4. 合并旧预览里需要保留的字段,避免前端依赖字段突然丢失。
existingPreview, err := s.cacheDAO.GetSchedulePlanPreviewFromCache(ctx, userID, normalizedConversationID)
if err != nil {
return fmt.Errorf("读取排程预览缓存失败: %w", err)
}
if existingPreview != nil {
preview.TraceID = strings.TrimSpace(existingPreview.TraceID)
if len(existingPreview.CandidatePlans) > 0 {
preview.CandidatePlans = agentshared.CloneWeekSchedules(existingPreview.CandidatePlans)
}
if len(existingPreview.AllocatedItems) > 0 {
preview.AllocatedItems = agentshared.CloneTaskClassItems(existingPreview.AllocatedItems)
}
if len(preview.TaskClassIDs) == 0 && len(existingPreview.TaskClassIDs) > 0 {
preview.TaskClassIDs = append([]int(nil), existingPreview.TaskClassIDs...)
}
}
if preview.CandidatePlans == nil {
preview.CandidatePlans = make([]model.UserWeekSchedule, 0)
}
if preview.HybridEntries == nil {
preview.HybridEntries = make([]model.HybridScheduleEntry, 0)
}
if preview.TaskClassIDs == nil {
preview.TaskClassIDs = make([]int, 0)
}
// 5. 回写 Redis 预览缓存;失败则返回错误,让前端可感知并重试。
if err := s.cacheDAO.SetSchedulePlanPreviewToCache(ctx, userID, normalizedConversationID, preview); err != nil {
return fmt.Errorf("刷新排程预览缓存失败: %w", err)
}
return nil
}