Version: 0.9.1.dev.260406
后端: 1.新建conv/schedule_persist.go:ScheduleState Diff 持久化,事务内逐变更写库,支持 place/move/unplace 三种操作(当前 event source) 2.新建conv/schedule_provider.go:ScheduleState 加载适配,从 DB 合并 existing events + pending task items 3.新建dao/agent_state_store_adapter.go:Redis 状态快照存取适配,实现 AgentStateStore 接口 4.新建service/agentsvc/agent_newagent.go:newAgent service 集成层,串联 LLM 客户端、ScheduleProvider、SchedulePersistor 和 ChunkEmitter 5.更新node/execute.go:接入 SchedulePersistor(写操作确认后持久化)、完善 confirm resume 路径(PendingConfirmTool 恢复分支)、correction 机制增加连续失败计数上限 6.更新api/agent.go + cmd/start.go:接入 newAgent service,完成 API 层路由注册 7.新建node/execute_confirm_flow_test.go + llm_tool_orchestration_test.go:确认回路 7 个测试 + 端到端排课 5 个测试全部通过 8.新建newAgent/ARCHITECTURE.md + ROADMAP.md:全链路架构文档和缺口分析 9.代码审查整理:提取 prompt/base.go(通用 buildStageMessages 等5个辅助)、tools/args.go(参数解析辅助);write_tools 尾部辅助移入 write_helpers;修复 queryRangeSpecific sb.Reset() 逻辑缺陷和 Unplace guest Duration 未恢复;ScheduleStateProvider/SchedulePersistor 归入 state_store.go;emitter 内部 Build*Text 函数降级为私有 前端:无 仓库:无
This commit is contained in:
174
backend/conv/schedule_persist.go
Normal file
174
backend/conv/schedule_persist.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package conv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/LoveLosita/smartflow/backend/dao"
|
||||
"github.com/LoveLosita/smartflow/backend/model"
|
||||
newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools"
|
||||
)
|
||||
|
||||
// SchedulePersistorAdapter 实现 model.SchedulePersistor 接口。
|
||||
// 组合 RepoManager,调用 PersistScheduleChanges 持久化变更。
|
||||
type SchedulePersistorAdapter struct {
|
||||
manager *dao.RepoManager
|
||||
}
|
||||
|
||||
// NewSchedulePersistorAdapter 创建持久化适配器。
|
||||
func NewSchedulePersistorAdapter(manager *dao.RepoManager) *SchedulePersistorAdapter {
|
||||
return &SchedulePersistorAdapter{manager: manager}
|
||||
}
|
||||
|
||||
// PersistScheduleChanges 实现 model.SchedulePersistor 接口。
|
||||
func (a *SchedulePersistorAdapter) PersistScheduleChanges(ctx context.Context, original, modified *newagenttools.ScheduleState, userID int) error {
|
||||
return PersistScheduleChanges(ctx, a.manager, original, modified, userID)
|
||||
}
|
||||
|
||||
// PersistScheduleChanges 将内存中的 ScheduleState 变更持久化到数据库。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 调用 DiffScheduleState 计算变更;
|
||||
// 2. 在事务中逐个应用变更到数据库;
|
||||
// 3. 全部成功或全部回滚,保证原子性。
|
||||
func PersistScheduleChanges(
|
||||
ctx context.Context,
|
||||
manager *dao.RepoManager,
|
||||
original *newagenttools.ScheduleState,
|
||||
modified *newagenttools.ScheduleState,
|
||||
userID int,
|
||||
) error {
|
||||
changes := DiffScheduleState(original, modified)
|
||||
if len(changes) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return manager.Transaction(ctx, func(txM *dao.RepoManager) error {
|
||||
for _, change := range changes {
|
||||
if err := applyScheduleChange(ctx, txM, change, userID); err != nil {
|
||||
return fmt.Errorf("应用变更失败 [%s %s]: %w", change.Type, change.Name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// applyScheduleChange 应用单个变更到数据库。
|
||||
func applyScheduleChange(ctx context.Context, manager *dao.RepoManager, change ScheduleChange, userID int) error {
|
||||
switch change.Type {
|
||||
case ChangePlace:
|
||||
return applyPlaceChange(ctx, manager, change, userID)
|
||||
case ChangeMove:
|
||||
return applyMoveChange(ctx, manager, change, userID)
|
||||
case ChangeUnplace:
|
||||
return applyUnplaceChange(ctx, manager, change, userID)
|
||||
default:
|
||||
return fmt.Errorf("未知变更类型: %s", change.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// applyPlaceChange 应用放置变更。
|
||||
func applyPlaceChange(ctx context.Context, manager *dao.RepoManager, change ScheduleChange, userID int) error {
|
||||
// Place:pending → placed,为现有 Event 创建 Schedule
|
||||
// 前提:Event 已经存在(SourceID 是 ScheduleEvent.ID)
|
||||
// NewCoords 包含所有需要放置的位置(可能多天/多节)
|
||||
|
||||
if len(change.NewCoords) == 0 {
|
||||
return fmt.Errorf("place 变更缺少目标位置")
|
||||
}
|
||||
|
||||
if change.Source != "event" || change.SourceID == 0 {
|
||||
return fmt.Errorf("place 变更需要有效的 event source")
|
||||
}
|
||||
|
||||
// 按周天分组,压缩成 slot ranges
|
||||
groups := groupCoordsByWeekDay(change.NewCoords)
|
||||
for week, dayGroups := range groups {
|
||||
for dayOfWeek, coords := range dayGroups {
|
||||
startSection, endSection := minMaxSection(coords)
|
||||
|
||||
// 创建 schedule 记录(event 已存在,只创建 schedule)
|
||||
schedules := make([]model.Schedule, endSection-startSection+1)
|
||||
for sec := startSection; sec <= endSection; sec++ {
|
||||
schedules[sec-startSection] = model.Schedule{
|
||||
UserID: userID,
|
||||
Week: week,
|
||||
DayOfWeek: dayOfWeek,
|
||||
Section: sec,
|
||||
EventID: change.SourceID,
|
||||
}
|
||||
}
|
||||
|
||||
// 批量创建
|
||||
_, err := manager.Schedule.AddSchedules(schedules)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建 schedule 失败: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyMoveChange 应用移动变更。
|
||||
func applyMoveChange(ctx context.Context, manager *dao.RepoManager, change ScheduleChange, userID int) error {
|
||||
// Move:已有 schedule,只更新位置
|
||||
// 需要删除旧位置的 schedule,在新位置创建新 schedule
|
||||
|
||||
// 1. 删除旧位置
|
||||
if change.Source == "event" && change.SourceID != 0 {
|
||||
if err := manager.Schedule.DeleteScheduleEventAndSchedule(ctx, change.SourceID, userID); err != nil {
|
||||
return fmt.Errorf("删除旧位置失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 创建新位置(复用 place 逻辑)
|
||||
return applyPlaceChange(ctx, manager, change, userID)
|
||||
}
|
||||
|
||||
// applyUnplaceChange 应用移除变更。
|
||||
func applyUnplaceChange(ctx context.Context, manager *dao.RepoManager, change ScheduleChange, userID int) error {
|
||||
// Unplace:删除 schedule,任务恢复为 pending
|
||||
if change.Source == "event" && change.SourceID != 0 {
|
||||
return manager.Schedule.DeleteScheduleEventAndSchedule(ctx, change.SourceID, userID)
|
||||
}
|
||||
return fmt.Errorf("unplace 变更的 source 不是 event: %s", change.Source)
|
||||
}
|
||||
|
||||
// ==================== 辅助函数 ====================
|
||||
|
||||
// intPtr 返回 int 指针,零值返回 nil。
|
||||
func intPtr(v int) *int {
|
||||
if v == 0 {
|
||||
return nil
|
||||
}
|
||||
return &v
|
||||
}
|
||||
|
||||
// groupCoordsByWeekDay 按周天分组坐标。
|
||||
func groupCoordsByWeekDay(coords []SlotCoord) map[int]map[int][]SlotCoord {
|
||||
result := make(map[int]map[int][]SlotCoord)
|
||||
for _, coord := range coords {
|
||||
if result[coord.Week] == nil {
|
||||
result[coord.Week] = make(map[int][]SlotCoord)
|
||||
}
|
||||
result[coord.Week][coord.DayOfWeek] = append(result[coord.Week][coord.DayOfWeek], coord)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// minMaxSection 返回坐标列表中的最小和最大节次。
|
||||
func minMaxSection(coords []SlotCoord) (min, max int) {
|
||||
if len(coords) == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
min, max = coords[0].Section, coords[0].Section
|
||||
for _, c := range coords[1:] {
|
||||
if c.Section < min {
|
||||
min = c.Section
|
||||
}
|
||||
if c.Section > max {
|
||||
max = c.Section
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
112
backend/conv/schedule_provider.go
Normal file
112
backend/conv/schedule_provider.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package conv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/LoveLosita/smartflow/backend/dao"
|
||||
"github.com/LoveLosita/smartflow/backend/model"
|
||||
newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools"
|
||||
)
|
||||
|
||||
// ScheduleProvider 实现 model.ScheduleStateProvider 接口。
|
||||
// 通过 DAO 层加载用户的日程和任务数据,调用 LoadScheduleState 构建内存状态。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只负责"从 DB 查数据 + 调 LoadScheduleState 转换",不含业务逻辑;
|
||||
// 2. 不负责缓存(由上层 Service 决定是否缓存);
|
||||
// 3. 不负责 Diff 和持久化(由 Confirm 流程负责)。
|
||||
type ScheduleProvider struct {
|
||||
scheduleDAO *dao.ScheduleDAO
|
||||
taskClassDAO *dao.TaskClassDAO
|
||||
}
|
||||
|
||||
// NewScheduleProvider 创建 ScheduleProvider。
|
||||
func NewScheduleProvider(scheduleDAO *dao.ScheduleDAO, taskClassDAO *dao.TaskClassDAO) *ScheduleProvider {
|
||||
return &ScheduleProvider{
|
||||
scheduleDAO: scheduleDAO,
|
||||
taskClassDAO: taskClassDAO,
|
||||
}
|
||||
}
|
||||
|
||||
// LoadScheduleState 实现 model.ScheduleStateProvider 接口。
|
||||
// 加载用户当前周的日程和所有待安排任务,构建 ScheduleState。
|
||||
func (p *ScheduleProvider) LoadScheduleState(ctx context.Context, userID int) (*newagenttools.ScheduleState, error) {
|
||||
// 1. 确定当前周。
|
||||
now := time.Now()
|
||||
week, _, err := RealDateToRelativeDate(now.Format(DateFormat))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析当前日期失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 加载当前周的所有日程(含 Event + EmbeddedTask 预加载)。
|
||||
schedules, err := p.scheduleDAO.GetUserWeeklySchedule(ctx, userID, week)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加载用户周日程失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 加载用户所有任务类(含 Items 预加载)。
|
||||
// 两步:先拿 ID 列表,再批量获取完整数据(含 Items)。
|
||||
taskClasses, err := p.loadCompleteTaskClasses(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 4. 构建 WindowDay 列表(当前周 7 天)。
|
||||
windowDays := make([]WindowDay, 7)
|
||||
for i := 0; i < 7; i++ {
|
||||
windowDays[i] = WindowDay{Week: week, DayOfWeek: i + 1}
|
||||
}
|
||||
|
||||
// 5. 构建额外 item category 映射(已加载全部 taskClass,通常为空)。
|
||||
extraItemCategories := buildExtraItemCategories(schedules, taskClasses)
|
||||
|
||||
// 6. 调用已有的 LoadScheduleState 构建内存状态。
|
||||
return LoadScheduleState(schedules, taskClasses, extraItemCategories, windowDays), nil
|
||||
}
|
||||
|
||||
// loadCompleteTaskClasses 批量加载用户所有任务类(含 Items 预加载)。
|
||||
func (p *ScheduleProvider) loadCompleteTaskClasses(ctx context.Context, userID int) ([]model.TaskClass, error) {
|
||||
basicClasses, err := p.taskClassDAO.GetUserTaskClasses(userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加载用户任务类失败: %w", err)
|
||||
}
|
||||
if len(basicClasses) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ids := make([]int, len(basicClasses))
|
||||
for i, tc := range basicClasses {
|
||||
ids[i] = tc.ID
|
||||
}
|
||||
|
||||
complete, err := p.taskClassDAO.GetCompleteTaskClassesByIDs(ctx, userID, ids)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加载完整任务类失败: %w", err)
|
||||
}
|
||||
return complete, nil
|
||||
}
|
||||
|
||||
// buildExtraItemCategories 从已有日程中提取不属于给定 taskClasses 的 task event 的 category 映射。
|
||||
// 当加载全部 taskClass 时,通常返回空 map。
|
||||
func buildExtraItemCategories(schedules []model.Schedule, taskClasses []model.TaskClass) map[int]string {
|
||||
knownItemIDs := make(map[int]bool)
|
||||
for _, tc := range taskClasses {
|
||||
for _, item := range tc.Items {
|
||||
knownItemIDs[item.ID] = true
|
||||
}
|
||||
}
|
||||
|
||||
categories := make(map[int]string)
|
||||
for _, s := range schedules {
|
||||
if s.Event == nil || s.Event.Type != "task" || s.Event.RelID == nil {
|
||||
continue
|
||||
}
|
||||
itemID := *s.Event.RelID
|
||||
if !knownItemIDs[itemID] {
|
||||
categories[itemID] = "任务"
|
||||
}
|
||||
}
|
||||
return categories
|
||||
}
|
||||
Reference in New Issue
Block a user