后端: 1. conv 并行迁移与切流接线(旧目录下沉到 newAgent/conv) - 新建 newAgent/conv/schedule_provider.go、schedule_state.go、schedule_preview.go、schedule_persist.go,保持原有排程转换/预览/持久化能力; - 删除旧目录 conv/schedule_provider.go、schedule_state.go、schedule_preview.go、schedule_persist.go; - 更新 cmd/start.go 与 service/agentsvc/agent_newagent.go,ScheduleProvider/SchedulePersistor 与 preview 转换统一切到 newAgent/conv; - 删除旧 conv/schedule_state_test.go(迁移期测试文件清理)。 2. execute 循环上下文收口增强(历史归档 + 当前轮清晰化) - 更新 node/chat.go:仅在 completed 收口时写 execute_loop_closed marker,供后续 prompt 分层归档; - 更新 prompt/execute_context.go:msg1/msg2 升级为 V3,按收口标记拆分“历史归档 loop / 当前活跃 loop”,并增加 msg1 长度预算裁剪; - 更新 node/execute.go:新增 execute 置顶上下文同步(execution_context/current_step),在轮次开始与 next_plan 后即时刷新; - 更新 prompt/execute.go + execute_context.go:补齐“当前计划步骤 + done_when”强约束,禁止未达成判定时提前 next_plan。 3. 图路由与执行策略微调 - 更新 graph/common_graph.go:Plan/Confirm 分支允许直接进入 Deliver 收口; - 更新 node/plan.go:always_execute 链路下补发计划摘要并写入历史,保证自动执行与手动确认文案一致; - 更新 model/common_state.go:DefaultMaxRounds 从 30 提升到 60。 4. 复合工具规划器重构(去重实现,复用 logic 公共能力) - 更新 tools/compound_tools.go:min_context_switch / spread_even 改为调用 backend/logic 规划器(PlanMinContextSwitchMoves / PlanEvenSpreadMoves); - 新增 state_id↔logic_id 映射层,统一入参与回填,避免工具层与规划层 ID 语义耦合; - 删除 compound_tools 内部重复的规划/归一化/分组/打分实现,减少第三份复制逻辑。 5. 同步调试与文档 - 更新 newAgent/Log.txt 调试日志; - 新增 memory/记忆模块实施计划.md(面试优先版到产品可用版的落地路线)。 前端:无 仓库:无
281 lines
9.3 KiB
Go
281 lines
9.3 KiB
Go
package newagentconv
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
|
||
baseconv "github.com/LoveLosita/smartflow/backend/conv"
|
||
"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 := baseconv.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
|
||
}
|