Version: 0.9.5.dev.260407

后端:
1.粗排链路收口(按 task_class_ids 精确加载 ScheduleState + 规划窗口抗脏数据)
  - 更新conv/schedule_provider.go:新增 LoadScheduleStateForTaskClasses;优先按本轮任务类加载窗口;buildWindowFromTaskClasses 改为逐条过滤坏日期,避免 DayMapping 被全量任务类污染
  - 更新model/state_store.go:新增 ScopedScheduleStateProvider 可选接口
  - 更新model/graph_run_state.go:EnsureScheduleState 首次加载时优先走 scoped provider,再做 scope 裁剪
2.粗排建议态语义统一(pending/existing → pending/suggested/existing)
  - 新建tools/status.go:统一 IsPendingTask / IsSuggestedTask / IsExistingTask / scope 过滤逻辑
  - 更新node/rough_build.go:粗排回写后任务显式转 suggested;pending 统计仅看“真实 pending”
  - 更新tools/state.go:ScheduleTask.Status/Slots/Duration 注释补齐 suggested 语义
  - 更新tools/read_helpers.go + read_tools.go:overview/list_tasks/task_info 支持 suggested 展示;占用计算按“已落位任务”统一处理
  - 更新tools/write_helpers.go + write_tools.go:place/move/swap/unplace 全量切到 suggested/existing/pending 新语义
  - 更新tools/registry.go + SCHEDULE_TOOLS.md:工具描述、参数枚举、文档口径同步到 suggested 语义
  - 更新conv/schedule_preview.go:预览层统一通过 IsSuggestedTask 输出 suggested,兼容旧快照
  - 更新service/agentsvc/agent_newagent.go:预览 debug 摘要改为 pending/suggested/existing 三态统计
3.粗排调试增强
  - 更新node/rough_build.go:新增 applied/day_mapping_miss/task_item_match_miss 统计及样本日志,便于排查 placement 未落回 state 的根因
前端:无 仓库:无
This commit is contained in:
Losita
2026-04-07 23:58:00 +08:00
parent 07d307fe07
commit cdedd3c968
14 changed files with 599 additions and 174 deletions

View File

@@ -20,8 +20,8 @@ type MoveRequest struct {
// ==================== Place ====================
// Place 将一个待安排任务到指定位置。
// taskID 必须是 pending 状态的任务。
// Place 将一个待安排任务预排到指定位置。
// taskID 必须是真实 pending(无 Slots状态的任务。
// 如果目标位置有可嵌入宿主can_embed=true 且未被嵌入),自动走嵌入逻辑。
func Place(state *ScheduleState, taskID, day, slotStart int) string {
// 1. 查找任务。
@@ -31,7 +31,10 @@ func Place(state *ScheduleState, taskID, day, slotStart int) string {
}
// 2. 校验状态。
if task.Status != "pending" {
// 2.1 只有“真实 pending”才允许 place
// 2.2 suggested / existing 都说明任务已经有落位,继续 place 会破坏当前方案语义;
// 2.3 旧快照里的 pending+Slots 也会被 IsPendingTask 排除,避免重复补排。
if !IsPendingTask(*task) {
return fmt.Sprintf("放置失败:[%d]%s 不是待安排任务,无法放置。", task.StateID, task.Name)
}
@@ -63,33 +66,33 @@ func Place(state *ScheduleState, taskID, day, slotStart int) string {
// 6. 执行变更。
if host != nil {
// 嵌入路径:设置双向嵌入关系。
// 嵌入路径:设置双向嵌入关系,并把任务提升为 suggested
guestID := task.StateID
hostID := host.StateID
task.EmbedHost = &hostID
host.EmbeddedBy = &guestID
task.Slots = []TaskSlot{{Day: day, SlotStart: slotStart, SlotEnd: slotEnd}}
task.Status = "existing"
task.Status = TaskStatusSuggested
return fmt.Sprintf("已将 [%d]%s 嵌入到第%d天第%s宿主[%d]%s。\n%s\n待安排任务剩余%d个。",
return fmt.Sprintf("已将 [%d]%s 预排并嵌入到第%d天第%s宿主[%d]%s。\n%s\n待安排任务剩余%d个。",
task.StateID, task.Name, day, formatSlotRange(slotStart, slotEnd),
host.StateID, host.Name,
formatDayOccupancy(state, day), countPending(state))
}
// 普通路径:直接放置。
// 普通路径:直接放置,并标记为 suggested
task.Slots = []TaskSlot{{Day: day, SlotStart: slotStart, SlotEnd: slotEnd}}
task.Status = "existing"
task.Status = TaskStatusSuggested
return fmt.Sprintf("已将 [%d]%s 到第%d天第%s。\n%s\n待安排任务剩余%d个。",
return fmt.Sprintf("已将 [%d]%s 预排到第%d天第%s。\n%s\n待安排任务剩余%d个。",
task.StateID, task.Name, day, formatSlotRange(slotStart, slotEnd),
formatDayOccupancy(state, day), countPending(state))
}
// ==================== Move ====================
// Move 将一个已安排任务移动到新位置。
// taskID 必须是 existing 状态且非锁定
// Move 将一个已落位任务移动到新位置。
// taskID 允许是 suggested / existing但不能是真实 pending
func Move(state *ScheduleState, taskID, newDay, newSlotStart int) string {
// 1. 查找任务。
task := state.TaskByStateID(taskID)
@@ -98,7 +101,7 @@ func Move(state *ScheduleState, taskID, newDay, newSlotStart int) string {
}
// 2. 校验状态。
if task.Status == "pending" {
if IsPendingTask(*task) {
return fmt.Sprintf("移动失败:[%d]%s 当前为待安排状态,请使用 place 放置。", task.StateID, task.Name)
}
@@ -148,8 +151,8 @@ func Move(state *ScheduleState, taskID, newDay, newSlotStart int) string {
// ==================== Swap ====================
// Swap 交换两个已安排任务的位置。
// 两个任务都必须是 existing 状态、非锁定、总时长相同。
// Swap 交换两个已落位任务的位置。
// 两个任务都必须是 suggested / existing、非锁定、总时长相同。
func Swap(state *ScheduleState, taskAID, taskBID int) string {
// 1. 查找两个任务。
taskA := state.TaskByStateID(taskAID)
@@ -166,11 +169,11 @@ func Swap(state *ScheduleState, taskAID, taskBID int) string {
}
// 2. 校验状态。
if taskA.Status != "existing" {
return fmt.Sprintf("交换失败:[%d]%s 不是已安排任务。", taskA.StateID, taskA.Name)
if !IsPlacedTask(*taskA) {
return fmt.Sprintf("交换失败:[%d]%s 不是已落位任务。", taskA.StateID, taskA.Name)
}
if taskB.Status != "existing" {
return fmt.Sprintf("交换失败:[%d]%s 不是已安排任务。", taskB.StateID, taskB.Name)
if !IsPlacedTask(*taskB) {
return fmt.Sprintf("交换失败:[%d]%s 不是已落位任务。", taskB.StateID, taskB.Name)
}
// 3. 校验锁定。
@@ -257,7 +260,7 @@ func BatchMove(state *ScheduleState, moves []MoveRequest) string {
if task == nil {
return fmt.Sprintf("批量移动失败,全部回滚,无任何变更。\n任务ID %d 不存在(第%d条移动请求。", m.TaskID, i+1)
}
if task.Status == "pending" {
if IsPendingTask(*task) {
return fmt.Sprintf("批量移动失败,全部回滚,无任何变更。\n[%d]%s 当前为待安排状态,请使用 place第%d条移动请求。",
task.StateID, task.Name, i+1)
}
@@ -327,8 +330,8 @@ func BatchMove(state *ScheduleState, moves []MoveRequest) string {
// ==================== Unplace ====================
// Unplace 将一个已安排任务移除,恢复为待安排状态。
// taskID 必须是 existing 状态且非锁定
// Unplace 将一个已落位任务移除,恢复为待安排状态。
// taskID 允许是 suggested / existing但不能是真实 pending
// 如果任务有嵌入关系,会自动清理双向指针。
func Unplace(state *ScheduleState, taskID int) string {
// 1. 查找任务。
@@ -338,7 +341,7 @@ func Unplace(state *ScheduleState, taskID int) string {
}
// 2. 校验状态。
if task.Status == "pending" {
if IsPendingTask(*task) {
return fmt.Sprintf("移除失败:[%d]%s 已经是待安排状态。", task.StateID, task.Name)
}
@@ -372,14 +375,14 @@ func Unplace(state *ScheduleState, taskID int) string {
}
guest.EmbedHost = nil
guest.Slots = nil
guest.Status = "pending"
guest.Status = TaskStatusPending
}
task.EmbeddedBy = nil
}
// 6. 执行变更。
task.Slots = nil
task.Status = "pending"
task.Status = TaskStatusPending
// 7. 收集涉及的天。
affectedDays := collectAffectedDaysFromSlots(oldSlots)