后端:
1.Chat 四路由升级(二分类 chat/task → 四路由 direct_reply/execute/deep_answer/plan)
- 新建model/chat_contract.go:路由决策模型,含 NeedsRoughBuild 粗排标记
- 更新node/chat.go:四路由分流;新增 deep_answer 深度回答路径(二次 LLM 开 thinking)
- 更新prompt/chat.go:意图分类 prompt 升级为四路由 prompt;新增 deep_answer prompt
2.粗排节点(RoughBuild)全链路
- 新建node/rough_build.go:粗排节点,调用注入的算法函数,结果写入 ScheduleState 后进 Execute 微调
- 更新graph/common_graph.go:注册 RoughBuild 节点;Chat/Confirm 后可路由至粗排
- 更新model/graph_run_state.go:新增 RoughBuildPlacement/RoughBuildFunc 类型;Deps 注入入口
- 更新model/plan_contract.go:PlanDecision 新增 NeedsRoughBuild/TaskClassIDs 字段
- 更新node/plan.go:plan_done 时写入粗排标记和 TaskClassIDs
3.任务类约束元数据(TaskClassMeta)贯穿 prompt → tools → 持久化
- 更新tools/state.go:新增 TaskClassMeta;ScheduleState.TaskClasses;ScheduleTask.TaskClassID;Clone 深拷贝
- 更新conv/schedule_state.go:加载时构建 TaskClassMeta;Diff 支持 HostEventID 嵌入关系
- 更新conv/schedule_provider.go:新增 LoadTaskClassMetas 按需加载
- 更新model/state_store.go:ScheduleStateProvider 接口新增 LoadTaskClassMetas
- 更新prompt/base.go:renderStateSummary 渲染任务类约束
- 更新prompt/plan.go:注入任务类 ID 上下文和粗排识别规则
- 更新tools/read_tools.go:GetOverview 展示任务类约束
- 更新model/common_state.go:CommonState 新增 TaskClassIDs/TaskClasses/NeedsRoughBuild
4.Execute 健壮性增强(correction 重试 + 纯 ReAct 模式)
- 更新node/execute.go:未知工具名/空文本走 correction 重试而非 fatal;maxConsecutiveCorrections 提升为包级常量;新增无 plan 纯ReAct 模式;工具结果截断;speak 排除 ask_user/confirm
- 更新prompt/execute.go:新增 ReAct 模式 system prompt 和 contract
5.写入持久化完善(task_item source + 嵌入水课)
- 更新conv/schedule_persist.go:place/move/unplace 支持 task_item source,含嵌入水课和普通 task event 两条路径
- 新建conv/schedule_preview.go:ScheduleState → 排程预览缓存,复用旧格式,前端无需改动
6.状态持久化体系(Redis → MySQL outbox 异步)
- 更新dao/cache.go:Redis 快照 TTL 从 24h 改为 2h,配合 MySQL outbox
- 新建model/agent_state_snapshot_record.go:快照 MySQL 记录模型
- 新建service/events/agent_state_persist.go:outbox 异步持久化处理器
- 更新cmd/start.go + inits/mysql.go:注册快照事件处理器 + AutoMigrate
- 更新service/agentsvc/agent_newagent.go:注入 RoughBuildFunc;outbox 异步写快照;排程结果写 Redis 预览缓存
7.基础设施与稳定性
- 更新stream/sse_adapter.go:outChan 满时静默丢弃,保证持久化不被 SSE 阻断
- 更新service/agentsvc/agent.go:新增 readAgentExtraIntSlice;outChan 容量 8→256
- 更新node/agent_nodes.go:Chat 注入工具 schema;Deliver 改 saveAgentState 替代 deleteAgentState
前端:无
仓库:无
280 lines
9.2 KiB
Go
280 lines
9.2 KiB
Go
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 {
|
||
if len(change.NewCoords) == 0 {
|
||
return fmt.Errorf("place 变更缺少目标位置")
|
||
}
|
||
switch change.Source {
|
||
case "event":
|
||
return applyPlaceEventSource(ctx, manager, change, userID)
|
||
case "task_item":
|
||
return applyPlaceTaskItem(ctx, manager, change, userID)
|
||
default:
|
||
return fmt.Errorf("place 变更不支持的 source: %s", change.Source)
|
||
}
|
||
}
|
||
|
||
// applyPlaceEventSource 处理 source=event 的放置(为已有 Event 创建 Schedule 记录)。
|
||
func applyPlaceEventSource(ctx context.Context, manager *dao.RepoManager, change ScheduleChange, userID int) error {
|
||
if change.SourceID == 0 {
|
||
return fmt.Errorf("place event 变更需要有效的 source_id")
|
||
}
|
||
groups := groupCoordsByWeekDay(change.NewCoords)
|
||
for week, dayGroups := range groups {
|
||
for dayOfWeek, coords := range dayGroups {
|
||
startSection, endSection := minMaxSection(coords)
|
||
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,
|
||
}
|
||
}
|
||
if _, err := manager.Schedule.AddSchedules(schedules); err != nil {
|
||
return fmt.Errorf("创建 schedule 失败: %w", err)
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// applyPlaceTaskItem 处理 source=task_item 的放置。
|
||
//
|
||
// 两条路径:
|
||
// 1. 嵌入水课(HostEventID != 0):在宿主 Schedule 记录上设置 embedded_task_id。
|
||
// 2. 普通放置(HostEventID == 0):新建 ScheduleEvent(type=task) + Schedule 记录。
|
||
// 两条路径最终都更新 task_items.embedded_time。
|
||
func applyPlaceTaskItem(ctx context.Context, manager *dao.RepoManager, change ScheduleChange, userID int) error {
|
||
if change.SourceID == 0 {
|
||
return fmt.Errorf("place task_item 变更需要有效的 source_id")
|
||
}
|
||
|
||
// task_item 只占一段连续时段,取第一个 coord 的 week/dayOfWeek
|
||
first := change.NewCoords[0]
|
||
week, dayOfWeek := first.Week, first.DayOfWeek
|
||
startSection, endSection := minMaxSection(change.NewCoords)
|
||
|
||
targetTime := &model.TargetTime{
|
||
Week: week,
|
||
DayOfWeek: dayOfWeek,
|
||
SectionFrom: startSection,
|
||
SectionTo: endSection,
|
||
}
|
||
|
||
if change.HostEventID != 0 {
|
||
// 嵌入路径:更新宿主 Schedule 记录的 embedded_task_id
|
||
if err := manager.Schedule.EmbedTaskIntoSchedule(
|
||
startSection, endSection, dayOfWeek, week, userID, change.SourceID,
|
||
); err != nil {
|
||
return fmt.Errorf("嵌入水课失败: %w", err)
|
||
}
|
||
} else {
|
||
// 普通路径:新建 ScheduleEvent + Schedule 记录
|
||
startTime, endTime, err := RelativeTimeToRealTime(week, dayOfWeek, startSection, endSection)
|
||
if err != nil {
|
||
return fmt.Errorf("时间转换失败: %w", err)
|
||
}
|
||
relID := change.SourceID
|
||
event := model.ScheduleEvent{
|
||
UserID: userID,
|
||
Name: change.Name,
|
||
Type: "task",
|
||
RelID: &relID,
|
||
CanBeEmbedded: false,
|
||
StartTime: startTime,
|
||
EndTime: endTime,
|
||
}
|
||
eventID, err := manager.Schedule.AddScheduleEvent(&event)
|
||
if err != nil {
|
||
return fmt.Errorf("创建 schedule_event 失败: %w", err)
|
||
}
|
||
schedules := make([]model.Schedule, endSection-startSection+1)
|
||
for i, sec := 0, startSection; sec <= endSection; i, sec = i+1, sec+1 {
|
||
schedules[i] = model.Schedule{
|
||
UserID: userID,
|
||
Week: week,
|
||
DayOfWeek: dayOfWeek,
|
||
Section: sec,
|
||
EventID: eventID,
|
||
Status: "normal",
|
||
}
|
||
}
|
||
if _, err := manager.Schedule.AddSchedules(schedules); err != nil {
|
||
return fmt.Errorf("创建 schedule 记录失败: %w", err)
|
||
}
|
||
}
|
||
|
||
if err := manager.TaskClass.UpdateTaskClassItemEmbeddedTime(ctx, change.SourceID, targetTime); err != nil {
|
||
return fmt.Errorf("更新 task_item embedded_time 失败: %w", err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// applyMoveChange 应用移动变更。
|
||
func applyMoveChange(ctx context.Context, manager *dao.RepoManager, change ScheduleChange, userID int) error {
|
||
switch change.Source {
|
||
case "event":
|
||
if change.SourceID != 0 {
|
||
if err := manager.Schedule.DeleteScheduleEventAndSchedule(ctx, change.SourceID, userID); err != nil {
|
||
return fmt.Errorf("删除旧位置失败: %w", err)
|
||
}
|
||
}
|
||
case "task_item":
|
||
// 清理旧位置
|
||
if change.OldHostEventID != 0 {
|
||
// 旧位置是嵌入:清空宿主的 embedded_task_id
|
||
if _, err := manager.Schedule.SetScheduleEmbeddedTaskIDToNull(ctx, change.OldHostEventID); err != nil {
|
||
return fmt.Errorf("清除旧嵌入关系失败: %w", err)
|
||
}
|
||
} else {
|
||
// 旧位置是普通 task event:按 task_item_id 删除
|
||
if err := manager.Schedule.DeleteScheduleEventByTaskItemID(ctx, change.SourceID); err != nil {
|
||
return fmt.Errorf("删除旧 task_item 日程失败: %w", err)
|
||
}
|
||
}
|
||
}
|
||
return applyPlaceChange(ctx, manager, change, userID)
|
||
}
|
||
|
||
// applyUnplaceChange 应用移除变更。
|
||
func applyUnplaceChange(ctx context.Context, manager *dao.RepoManager, change ScheduleChange, userID int) error {
|
||
switch change.Source {
|
||
case "event":
|
||
if change.SourceID == 0 {
|
||
return fmt.Errorf("unplace event 变更需要有效的 source_id")
|
||
}
|
||
return manager.Schedule.DeleteScheduleEventAndSchedule(ctx, change.SourceID, userID)
|
||
case "task_item":
|
||
if change.SourceID == 0 {
|
||
return fmt.Errorf("unplace task_item 变更需要有效的 source_id")
|
||
}
|
||
if change.HostEventID != 0 {
|
||
// 是嵌入:清空宿主 Schedule 的 embedded_task_id
|
||
if _, err := manager.Schedule.SetScheduleEmbeddedTaskIDToNull(ctx, change.HostEventID); err != nil {
|
||
return fmt.Errorf("清除嵌入关系失败: %w", err)
|
||
}
|
||
} else {
|
||
// 普通 task event:按 task_item_id 删除
|
||
if err := manager.Schedule.DeleteScheduleEventByTaskItemID(ctx, change.SourceID); err != nil {
|
||
return fmt.Errorf("删除 task_item 日程失败: %w", err)
|
||
}
|
||
}
|
||
if err := manager.TaskClass.DeleteTaskClassItemEmbeddedTime(ctx, change.SourceID); err != nil {
|
||
return fmt.Errorf("清除 task_item embedded_time 失败: %w", err)
|
||
}
|
||
return nil
|
||
default:
|
||
return fmt.Errorf("unplace 变更不支持的 source: %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
|
||
}
|