后端: 1. taskclass 执行闭环继续收紧——Plan / Execute 全面切到“最小工具闭环”视角,明确学习目标/总节数/禁排时段/排除星期默认停留 taskclass 域;未给日期范围时禁止擅自补 start_date/end_date,upsert_task_class 重试前先做写前检查并区分“内部表示修正”与“必须追问用户”的关键时间事实 2. QuickTask / TaskQuery 轻量链路继续收敛——新增 model/taskquery_contract.go 统一查询协议,QuickTaskDeps / start.go 改用 model 层参数;删除 query_tasks / quick_note_create 旧工具实现,避免任务查询与随口记再回流 execute 工具链 3. schedule 微调工具继续瘦身——下线 spread_even / min_context_switch 及其复合规划逻辑,清理 analyze_load / analyze_subjects / analyze_context / analyze_tolerance 等历史能力;execute 顺序策略收敛为局部 move / swap,提示词与工具目录仅暴露当前真实可用工具 4. 执行与时间线体验补齐——execute 为流式 speak 补发归一化尾部,避免 deliver 文案黏连;前端时间线新增 interrupt / status 协议识别、工具事件归并与状态过滤,减少 ToolTrace 重复和会话重建误判 前端: 5. AssistantPanel 适配新版 timeline extra 事件——schedule_agent.ts 补齐 interrupt / status kind,工具调用与结果按摘要/参数/工具名合并,恢复历史时不再把协议事件误判成用户消息
185 lines
5.9 KiB
Go
185 lines
5.9 KiB
Go
package schedule
|
||
|
||
import "fmt"
|
||
|
||
// validateLocalOrderForSinglePlacement 校验单个任务落到目标时段后,是否仍满足同任务类内部顺序约束。
|
||
//
|
||
// 职责边界:
|
||
// 1. 只负责“同任务类内部顺序”这一条规则,不负责冲突、锁定、范围合法性;
|
||
// 2. 采用“克隆态 + 假设落位”方式校验,避免直接污染真实 state;
|
||
// 3. 若任务不属于 task_item / 缺少 task_order / 当前无边界约束,直接放行。
|
||
func validateLocalOrderForSinglePlacement(state *ScheduleState, taskID int, targetSlots []TaskSlot) error {
|
||
if len(targetSlots) == 0 {
|
||
return nil
|
||
}
|
||
return validateLocalOrderBatchPlacement(state, map[int][]TaskSlot{
|
||
taskID: cloneScheduleTaskSlots(targetSlots),
|
||
})
|
||
}
|
||
|
||
// validateLocalOrderBatchPlacement 在“多任务同时变更”的假设下做顺序约束校验。
|
||
//
|
||
// 职责边界:
|
||
// 1. 先把所有候选落位一次性写入克隆态,再统一校验,避免批量局部调整时出现伪冲突;
|
||
// 2. 只校验 proposals 中涉及的任务,因为只要这些任务仍处于各自前驱/后继之间,就不会破坏同类整体顺序;
|
||
// 3. 返回首个命中的中文错误,供写工具直接透传给 LLM。
|
||
func validateLocalOrderBatchPlacement(state *ScheduleState, proposals map[int][]TaskSlot) error {
|
||
if state == nil || len(proposals) == 0 {
|
||
return nil
|
||
}
|
||
|
||
clone := state.Clone()
|
||
for taskID, slots := range proposals {
|
||
task := clone.TaskByStateID(taskID)
|
||
if task == nil {
|
||
return fmt.Errorf("顺序约束校验失败:任务ID %d 不存在", taskID)
|
||
}
|
||
task.Slots = cloneScheduleTaskSlots(slots)
|
||
}
|
||
|
||
for taskID := range proposals {
|
||
if err := validateTaskLocalOrderOnState(clone, taskID); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// validateTaskLocalOrderOnState 判断某个任务在当前假设态下,是否仍处于同任务类前驱/后继之间。
|
||
func validateTaskLocalOrderOnState(state *ScheduleState, taskID int) error {
|
||
task := state.TaskByStateID(taskID)
|
||
if task == nil {
|
||
return fmt.Errorf("顺序约束校验失败:任务ID %d 不存在", taskID)
|
||
}
|
||
if !shouldEnforceTaskLocalOrder(*task) || len(task.Slots) == 0 {
|
||
return nil
|
||
}
|
||
|
||
prevTask, nextTask := findTaskClassNeighbors(state, *task)
|
||
targetStartDay, targetStartSlot, _ := earliestScheduleTaskSlot(task.Slots)
|
||
targetEndDay, _, targetEndSlot := latestScheduleTaskSlot(task.Slots)
|
||
|
||
if prevTask != nil && len(prevTask.Slots) > 0 {
|
||
prevEndDay, _, prevEndSlot := latestScheduleTaskSlot(prevTask.Slots)
|
||
if !isStrictlyAfter(targetStartDay, targetStartSlot, prevEndDay, prevEndSlot) {
|
||
return fmt.Errorf(
|
||
"顺序约束不满足:[%d]%s 不能放到%s。它必须晚于同任务类前一个任务 %s 的结束位置(%s)。",
|
||
task.StateID,
|
||
task.Name,
|
||
formatTaskSlotsBriefWithState(state, task.Slots),
|
||
formatTaskLabel(*prevTask),
|
||
formatTaskSlotsBriefWithState(state, prevTask.Slots),
|
||
)
|
||
}
|
||
}
|
||
|
||
if nextTask != nil && len(nextTask.Slots) > 0 {
|
||
nextStartDay, nextStartSlot, _ := earliestScheduleTaskSlot(nextTask.Slots)
|
||
if !isStrictlyBefore(targetEndDay, targetEndSlot, nextStartDay, nextStartSlot) {
|
||
return fmt.Errorf(
|
||
"顺序约束不满足:[%d]%s 不能放到%s。它必须早于同任务类后一个任务 %s 的开始位置(%s)。",
|
||
task.StateID,
|
||
task.Name,
|
||
formatTaskSlotsBriefWithState(state, task.Slots),
|
||
formatTaskLabel(*nextTask),
|
||
formatTaskSlotsBriefWithState(state, nextTask.Slots),
|
||
)
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// shouldEnforceTaskLocalOrder 判断任务是否需要参与“同任务类内部顺序”约束。
|
||
func shouldEnforceTaskLocalOrder(task ScheduleTask) bool {
|
||
return task.Source == "task_item" && task.TaskClassID > 0 && task.TaskOrder > 0
|
||
}
|
||
|
||
// findTaskClassNeighbors 查找同任务类中 order 紧邻当前任务的前驱与后继。
|
||
func findTaskClassNeighbors(state *ScheduleState, task ScheduleTask) (prevTask *ScheduleTask, nextTask *ScheduleTask) {
|
||
if state == nil || !shouldEnforceTaskLocalOrder(task) {
|
||
return nil, nil
|
||
}
|
||
|
||
for i := range state.Tasks {
|
||
candidate := &state.Tasks[i]
|
||
if candidate.StateID == task.StateID {
|
||
continue
|
||
}
|
||
if !shouldEnforceTaskLocalOrder(*candidate) {
|
||
continue
|
||
}
|
||
if candidate.TaskClassID != task.TaskClassID {
|
||
continue
|
||
}
|
||
|
||
if candidate.TaskOrder < task.TaskOrder {
|
||
if prevTask == nil || candidate.TaskOrder > prevTask.TaskOrder {
|
||
prevTask = candidate
|
||
}
|
||
continue
|
||
}
|
||
if candidate.TaskOrder > task.TaskOrder {
|
||
if nextTask == nil || candidate.TaskOrder < nextTask.TaskOrder {
|
||
nextTask = candidate
|
||
}
|
||
}
|
||
}
|
||
return prevTask, nextTask
|
||
}
|
||
|
||
func earliestScheduleTaskSlot(slots []TaskSlot) (day int, slotStart int, slotEnd int) {
|
||
if len(slots) == 0 {
|
||
return 0, 0, 0
|
||
}
|
||
best := slots[0]
|
||
for i := 1; i < len(slots); i++ {
|
||
current := slots[i]
|
||
if current.Day < best.Day ||
|
||
(current.Day == best.Day && current.SlotStart < best.SlotStart) ||
|
||
(current.Day == best.Day && current.SlotStart == best.SlotStart && current.SlotEnd < best.SlotEnd) {
|
||
best = current
|
||
}
|
||
}
|
||
return best.Day, best.SlotStart, best.SlotEnd
|
||
}
|
||
|
||
func latestScheduleTaskSlot(slots []TaskSlot) (day int, slotStart int, slotEnd int) {
|
||
if len(slots) == 0 {
|
||
return 0, 0, 0
|
||
}
|
||
best := slots[0]
|
||
for i := 1; i < len(slots); i++ {
|
||
current := slots[i]
|
||
if current.Day > best.Day ||
|
||
(current.Day == best.Day && current.SlotEnd > best.SlotEnd) ||
|
||
(current.Day == best.Day && current.SlotEnd == best.SlotEnd && current.SlotStart > best.SlotStart) {
|
||
best = current
|
||
}
|
||
}
|
||
return best.Day, best.SlotStart, best.SlotEnd
|
||
}
|
||
|
||
func isStrictlyAfter(dayA, slotA, dayB, slotB int) bool {
|
||
if dayA != dayB {
|
||
return dayA > dayB
|
||
}
|
||
return slotA > slotB
|
||
}
|
||
|
||
func isStrictlyBefore(dayA, slotA, dayB, slotB int) bool {
|
||
if dayA != dayB {
|
||
return dayA < dayB
|
||
}
|
||
return slotA < slotB
|
||
}
|
||
|
||
func cloneScheduleTaskSlots(src []TaskSlot) []TaskSlot {
|
||
if len(src) == 0 {
|
||
return nil
|
||
}
|
||
dst := make([]TaskSlot, len(src))
|
||
copy(dst, src)
|
||
return dst
|
||
}
|