Files
smartmate/backend/services/agent/tools/schedule_read/overview_queue.go
Losita d7184b776b Version: 0.9.75.dev.260505
后端:
1.收口阶段 6 agent 结构迁移,将 newAgent 内核与 agentsvc 编排层迁入 services/agent
- 切换 Agent 启动装配与 HTTP handler 直连 agent sv,移除旧 service agent bridge
- 补齐 Agent 对 memory、task、task-class、schedule 的 RPC 适配与契约字段
- 扩展 schedule、task、task-class RPC/contract 支撑 Agent 查询、写入与 provider 切流
- 更新迁移文档、README 与相关注释,明确 agent 当前切流点和剩余 memory 迁移面
2026-05-05 16:00:57 +08:00

393 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package schedule_read
import (
"encoding/json"
"fmt"
"sort"
"strings"
"github.com/LoveLosita/smartflow/backend/services/agent/tools/schedule"
)
// BuildOverviewView 构造 get_overview 的纯展示视图。
func BuildOverviewView(input OverviewViewInput) ReadResultView {
if input.State == nil {
return BuildFailureView(BuildFailureViewInput{
ToolName: "get_overview",
Observation: input.Observation,
ArgFields: input.ArgFields,
})
}
totalSlots := input.State.Window.TotalDays * 12
totalOccupied := 0
taskExistingCount := 0
taskSuggestedCount := 0
taskPendingCount := 0
courseExistingCount := 0
for i := range input.State.Tasks {
task := input.State.Tasks[i]
if task.EmbedHost == nil {
for _, slot := range task.Slots {
totalOccupied += slot.SlotEnd - slot.SlotStart + 1
}
}
if isCourseScheduleTaskForRead(task) {
if schedule.IsExistingTask(task) {
courseExistingCount++
}
continue
}
switch {
case schedule.IsPendingTask(task):
taskPendingCount++
case schedule.IsSuggestedTask(task):
taskSuggestedCount++
default:
taskExistingCount++
}
}
dailyItems := make([]ItemView, 0, input.State.Window.TotalDays)
for day := 1; day <= input.State.Window.TotalDays; day++ {
totalDayOccupied := countScheduleDayOccupiedForRead(input.State, day)
taskDayOccupied := countScheduleDayTaskOccupiedForRead(input.State, day)
taskEntries := listScheduleTasksOnDayForRead(input.State, day, false)
detailLines := make([]string, 0, len(taskEntries))
for _, entry := range taskEntries {
detailLines = append(detailLines, fmt.Sprintf(
"[%d]%s%s%s",
entry.Task.StateID,
fallbackText(entry.Task.Name, "未命名任务"),
formatScheduleTaskStatusCN(*entry.Task),
formatScheduleSlotRangeCN(entry.SlotStart, entry.SlotEnd),
))
}
if len(detailLines) == 0 {
detailLines = append(detailLines, "当天没有任务明细。")
}
dailyItems = append(dailyItems, BuildItem(
formatScheduleDayCN(input.State, day),
fmt.Sprintf("总占用 %d/12 节,任务占用 %d/12 节", totalDayOccupied, taskDayOccupied),
[]string{fmt.Sprintf("任务 %d 项", len(taskEntries))},
detailLines,
map[string]any{"day": day},
))
}
taskItems := make([]ItemView, 0, len(input.State.Tasks))
for i := range input.State.Tasks {
task := input.State.Tasks[i]
if isCourseScheduleTaskForRead(task) {
continue
}
detailLines := []string{
"时段:" + formatScheduleTaskSlotsBriefCN(input.State, task.Slots),
"来源:" + formatScheduleTaskSourceCN(task),
}
if task.TaskClassID > 0 {
detailLines = append(detailLines, fmt.Sprintf("任务类 ID%d", task.TaskClassID))
}
taskItems = append(taskItems, BuildItem(
fmt.Sprintf("[%d]%s", task.StateID, fallbackText(task.Name, "未命名任务")),
fmt.Sprintf("%s%s", fallbackText(task.Category, "未分类"), formatScheduleTaskStatusCN(task)),
[]string{formatScheduleTaskStatusCN(task)},
detailLines,
map[string]any{
"task_id": task.StateID,
"task_class_id": task.TaskClassID,
"status": task.Status,
},
))
}
sort.Slice(taskItems, func(i, j int) bool {
leftID, _ := toInt(taskItems[i].Meta["task_id"])
rightID, _ := toInt(taskItems[j].Meta["task_id"])
return leftID < rightID
})
taskClassItems := make([]ItemView, 0, len(input.State.TaskClasses))
for _, meta := range input.State.TaskClasses {
detailLines := []string{
fmt.Sprintf("排程策略:%s", formatTaskClassStrategyCN(meta.Strategy)),
fmt.Sprintf("总预算:%d 节", meta.TotalSlots),
fmt.Sprintf("允许嵌入水课:%s", formatBoolLabelCN(meta.AllowFillerCourse)),
}
if len(meta.ExcludedSlots) > 0 {
detailLines = append(detailLines, "排除节次:"+formatScheduleSectionListCN(meta.ExcludedSlots))
}
if len(meta.ExcludedDaysOfWeek) > 0 {
detailLines = append(detailLines, "排除星期:"+formatWeekdayListCN(meta.ExcludedDaysOfWeek))
}
taskClassItems = append(taskClassItems, BuildItem(
fallbackText(meta.Name, "未命名任务类"),
formatTaskClassStrategyCN(meta.Strategy),
nil,
detailLines,
map[string]any{
"task_class_id": meta.ID,
"strategy": meta.Strategy,
},
))
}
totalFree := totalSlots - totalOccupied
if totalFree < 0 {
totalFree = 0
}
sections := []map[string]any{
BuildKVSection("窗口概况", []KVField{
BuildKVField("规划天数", fmt.Sprintf("%d 天", input.State.Window.TotalDays)),
BuildKVField("总时段", fmt.Sprintf("%d 节", totalSlots)),
BuildKVField("已占用", fmt.Sprintf("%d 节", totalOccupied)),
BuildKVField("空闲", fmt.Sprintf("%d 节", totalFree)),
BuildKVField("课程占位", fmt.Sprintf("%d 项", courseExistingCount)),
BuildKVField("已安排任务", fmt.Sprintf("%d 项", taskExistingCount)),
BuildKVField("已预排任务", fmt.Sprintf("%d 项", taskSuggestedCount)),
BuildKVField("待安排任务", fmt.Sprintf("%d 项", taskPendingCount)),
}),
BuildItemsSection("每日概况", dailyItems),
BuildItemsSection("任务清单", taskItems),
}
if len(taskClassItems) > 0 {
sections = append(sections, BuildItemsSection("任务类约束", taskClassItems))
}
appendSectionIfPresent(&sections, BuildArgsSection("查询条件", input.ArgFields))
return BuildResultView(BuildResultViewInput{
Status: StatusDone,
Title: "当前排程总览",
Subtitle: fmt.Sprintf("%d 天窗口,已占用 %d/%d 节,待安排 %d 项。", input.State.Window.TotalDays, totalOccupied, totalSlots, taskPendingCount),
Metrics: []MetricField{
BuildMetric("已占用", fmt.Sprintf("%d 节", totalOccupied)),
BuildMetric("空闲", fmt.Sprintf("%d 节", totalFree)),
BuildMetric("待安排", fmt.Sprintf("%d 项", taskPendingCount)),
BuildMetric("课程占位", fmt.Sprintf("%d 项", courseExistingCount)),
},
Items: dailyItems,
Sections: sections,
Observation: input.Observation,
MachinePayload: map[string]any{
"total_days": input.State.Window.TotalDays,
"total_slots": totalSlots,
"total_occupied": totalOccupied,
"task_existing_count": taskExistingCount,
"task_suggested_count": taskSuggestedCount,
"task_pending_count": taskPendingCount,
"course_existing_count": courseExistingCount,
},
})
}
// BuildQueueStatusView 构造 queue_status 的纯展示视图。
func BuildQueueStatusView(input QueueStatusViewInput) ReadResultView {
if input.State == nil {
return BuildFailureView(BuildFailureViewInput{
ToolName: "queue_status",
Observation: input.Observation,
ArgFields: input.ArgFields,
})
}
payload, machinePayload, ok := DecodeQueueStatusPayload(input.Observation)
if !ok {
return BuildFailureView(BuildFailureViewInput{
ToolName: "queue_status",
Observation: input.Observation,
ArgFields: input.ArgFields,
})
}
items := make([]ItemView, 0, 1+len(payload.NextTaskIDs))
sections := make([]map[string]any, 0, 4)
if payload.Current != nil {
currentItem := buildQueueCurrentItem(input.State, payload.Current, payload.CurrentAttempt)
items = append(items, currentItem)
sections = append(sections, BuildItemsSection("当前处理", []ItemView{currentItem}))
}
nextItems := make([]ItemView, 0, len(payload.NextTaskIDs))
for index, taskID := range payload.NextTaskIDs {
nextItems = append(nextItems, buildQueuePendingItem(input.State, taskID, index))
}
items = append(items, nextItems...)
if len(nextItems) > 0 {
sections = append(sections, BuildItemsSection("待处理队列", nextItems))
}
sections = append(sections, BuildKVSection("运行概况", []KVField{
BuildKVField("待处理", fmt.Sprintf("%d 项", payload.PendingCount)),
BuildKVField("已完成", fmt.Sprintf("%d 项", payload.CompletedCount)),
BuildKVField("已跳过", fmt.Sprintf("%d 项", payload.SkippedCount)),
BuildKVField("当前任务", resolveTaskQueueLabelByID(input.State, payload.CurrentTaskID)),
}))
if strings.TrimSpace(payload.LastError) != "" {
sections = append(sections, BuildCalloutSection(
"最近一次失败",
"队列中保留了上一轮 apply 的失败原因。",
"warning",
[]string{strings.TrimSpace(payload.LastError)},
))
}
appendSectionIfPresent(&sections, BuildArgsSection("查询条件", input.ArgFields))
title := fmt.Sprintf("队列待处理 %d 项", payload.PendingCount)
if payload.PendingCount == 0 && payload.CurrentTaskID == 0 {
title = "当前队列为空"
}
return BuildResultView(BuildResultViewInput{
Status: StatusDone,
Title: title,
Subtitle: buildQueueStatusSubtitle(payload),
Metrics: []MetricField{BuildMetric("待处理", fmt.Sprintf("%d 项", payload.PendingCount)), BuildMetric("已完成", fmt.Sprintf("%d 项", payload.CompletedCount)), BuildMetric("已跳过", fmt.Sprintf("%d 项", payload.SkippedCount))},
Items: items,
Sections: sections,
Observation: input.Observation,
MachinePayload: machinePayload,
})
}
// DecodeQueueStatusPayload 解析 queue_status 的 JSON observation。
func DecodeQueueStatusPayload(observation string) (QueueStatusPayload, map[string]any, bool) {
var payload QueueStatusPayload
trimmed := strings.TrimSpace(observation)
if trimmed == "" {
return payload, nil, false
}
if err := json.Unmarshal([]byte(trimmed), &payload); err != nil {
return payload, nil, false
}
raw, ok := parseObservationJSON(trimmed)
return payload, raw, ok
}
func buildQueueStatusSubtitle(payload QueueStatusPayload) string {
if payload.Current != nil {
return fmt.Sprintf(
"当前处理:[%d]%s第 %d 次尝试。",
payload.Current.TaskID,
fallbackText(payload.Current.Name, "未命名任务"),
maxInt(payload.CurrentAttempt, 1),
)
}
if payload.PendingCount > 0 {
return fmt.Sprintf("队列里还有 %d 项待处理,尚未弹出当前任务。", payload.PendingCount)
}
return "没有待处理任务,也没有正在处理的任务。"
}
// 1. 这里没有强抽成通用 task builder因为 queue_status 既要兼容 payload 快照,
// 2. 也要兼容通过 state 按 task_id 兜底,两类输入结构不同,硬抽反而会增加适配噪音。
func buildQueueCurrentItem(state *schedule.ScheduleState, payload *QueueTaskSnapshot, attempt int) ItemView {
detailLines := buildQueueCurrentDetailLines(state, payload)
detailLines = append(detailLines, fmt.Sprintf("当前尝试:第 %d 次", maxInt(attempt, 1)))
return BuildItem(
fmt.Sprintf("[%d]%s", payload.TaskID, fallbackText(payload.Name, "未命名任务")),
buildQueueCurrentSubtitle(payload),
[]string{"当前处理"},
detailLines,
map[string]any{
"task_id": payload.TaskID,
"status": payload.Status,
"task_class_id": payload.TaskClassID,
},
)
}
func buildQueuePendingItem(state *schedule.ScheduleState, taskID int, index int) ItemView {
task := state.TaskByStateID(taskID)
if task == nil {
return BuildItem(
fmt.Sprintf("[%d]任务", taskID),
fmt.Sprintf("队列第 %d 位", index+1),
[]string{"待处理"},
[]string{"当前状态快照中未找到更多任务详情。"},
map[string]any{"task_id": taskID, "queue_index": index},
)
}
return BuildItem(
fmt.Sprintf("[%d]%s", task.StateID, fallbackText(task.Name, "未命名任务")),
buildQueueTaskSubtitle(task),
buildQueueTaskTags(task, false),
buildQueueTaskDetailLines(state, task),
map[string]any{
"task_id": task.StateID,
"queue_index": index,
"status": task.Status,
},
)
}
func buildQueueTaskSubtitle(task *schedule.ScheduleTask) string {
if task == nil {
return "待处理"
}
return fmt.Sprintf("%s%s", fallbackText(task.Category, "未分类"), formatScheduleTaskStatusCN(*task))
}
func buildQueueTaskTags(task *schedule.ScheduleTask, isCurrent bool) []string {
tags := make([]string, 0, 2)
if isCurrent {
tags = append(tags, "当前处理")
} else {
tags = append(tags, "待处理")
}
if task != nil && task.Duration > 0 {
tags = append(tags, fmt.Sprintf("%d 节", task.Duration))
}
return tags
}
func buildQueueTaskDetailLines(state *schedule.ScheduleState, task *schedule.ScheduleTask) []string {
if task == nil {
return nil
}
lines := []string{"时段:" + formatScheduleTaskSlotsBriefCN(state, task.Slots)}
if task.TaskClassID > 0 {
lines = append(lines, fmt.Sprintf("任务类 ID%d", task.TaskClassID))
}
return lines
}
func buildQueueCurrentSubtitle(payload *QueueTaskSnapshot) string {
if payload == nil {
return "当前处理"
}
return fmt.Sprintf("%s%s", fallbackText(payload.Category, "未分类"), formatTargetTaskStatusCN(payload.Status))
}
func buildQueueCurrentDetailLines(state *schedule.ScheduleState, payload *QueueTaskSnapshot) []string {
if payload == nil {
return nil
}
lines := make([]string, 0, 3)
if len(payload.Slots) > 0 {
slotParts := make([]string, 0, len(payload.Slots))
for _, slot := range payload.Slots {
slotParts = append(slotParts, formatScheduleDaySlotCN(state, slot.Day, slot.SlotStart, slot.SlotEnd))
}
lines = append(lines, "时段:"+strings.Join(slotParts, ""))
} else {
lines = append(lines, "当前还未落位。")
}
if payload.TaskClassID > 0 {
lines = append(lines, fmt.Sprintf("任务类 ID%d", payload.TaskClassID))
}
if payload.Duration > 0 {
lines = append(lines, fmt.Sprintf("时长需求:%d 节", payload.Duration))
}
return lines
}
func formatTaskClassStrategyCN(strategy string) string {
switch strings.TrimSpace(strategy) {
case "steady":
return "均匀分布"
case "rapid":
return "集中突击"
default:
return fallbackText(strategy, "默认")
}
}