Files
smartmate/backend/service/agentsvc/agent_schedule_state.go
LoveLosita 73ab0f43aa Version: 0.9.34.dev.260421
后端:
1. 旧 Agent 管线(agent/)全面下线,共享逻辑迁移至 newAgent/
- 删除 backend/agent/ 整个目录(44 个 Go 文件),5 条旧专用流程已由 newAgent 统一 graph 取代
- 共享逻辑迁入 newAgent/:clone(shared/clone.go)、时间解析(shared/deadline.go)、优先级常量(shared/task_priority.go)、TaskQuery 类型(model/taskquery_types.go)、SystemPrompt(prompt/system.go)、Usage 合并(stream/usage.go)
2. service 层清除 agent/ 全部依赖
- 删除 4 个旧流程入口文件(agent_route / agent_quick_note / agent_schedule_plan / agent_schedule_refine)
- agent_task_query.go 删除 runTaskQueryFlow,参数类型切到 newagentmodel
- agent.go / agent_newagent.go / agent_schedule_preview.go / agent_schedule_state.go / cmd/start.go / quicknote.go:agent* 引用全部替换为 newagent*
3. 流式降级回退路径内联到 service 层(agent_stream_fallback.go),消除最后一条 agent/chat 依赖

前端:
1. ScheduleFineTuneModal 幂等键追加 classId 后缀,修复多任务类并行保存 key 重复
2026-04-21 20:10:16 +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 agentsvc
import (
"context"
"errors"
"fmt"
"log"
"strings"
"github.com/LoveLosita/smartflow/backend/model"
newagentconv "github.com/LoveLosita/smartflow/backend/newAgent/conv"
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
newagentshared "github.com/LoveLosita/smartflow/backend/newAgent/shared"
"github.com/LoveLosita/smartflow/backend/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 := newagentconv.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 *newagentmodel.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 := newagentconv.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 = newagentshared.CloneWeekSchedules(existingPreview.CandidatePlans)
}
if len(existingPreview.AllocatedItems) > 0 {
preview.AllocatedItems = newagentshared.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
}