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 迁移面
This commit is contained in:
Losita
2026-05-05 16:00:57 +08:00
parent e1819c5653
commit d7184b776b
174 changed files with 2189 additions and 1236 deletions

View File

@@ -0,0 +1,427 @@
package schedule_read
import (
"encoding/json"
"fmt"
"strings"
"github.com/LoveLosita/smartflow/backend/services/agent/tools/schedule"
)
// BuildAvailableSlotsView 构造 query_available_slots 的纯展示视图。
func BuildAvailableSlotsView(input AvailableSlotsViewInput) ReadResultView {
payload, machinePayload, ok := DecodeAvailableSlotsPayload(input.Observation)
if !ok || !payload.Success {
return BuildFailureView(BuildFailureViewInput{
ToolName: "query_available_slots",
Observation: input.Observation,
ArgFields: input.ArgFields,
})
}
items := make([]ItemView, 0, len(payload.Slots))
for _, slot := range payload.Slots {
tags := []string{
fmt.Sprintf("第%d周", slot.Week),
formatScheduleWeekdayCN(slot.DayOfWeek),
formatSlotTypeLabelCN(slot.SlotType),
}
detailLines := []string{
fmt.Sprintf("位置:%s", formatScheduleDaySlotCN(input.State, slot.Day, slot.SlotStart, slot.SlotEnd)),
fmt.Sprintf("跨度:%d 节", slot.SlotEnd-slot.SlotStart+1),
}
if strings.Contains(strings.ToLower(strings.TrimSpace(slot.SlotType)), "embed") {
if host := findScheduleHostTaskBySlotForRead(input.State, slot.Day, slot.SlotStart); host != nil {
detailLines = append(detailLines, fmt.Sprintf(
"宿主:[%d]%s%s",
host.StateID,
fallbackText(host.Name, "未命名任务"),
formatScheduleTaskStatusCN(*host),
))
}
}
items = append(items, BuildItem(
formatScheduleDaySlotCN(input.State, slot.Day, slot.SlotStart, slot.SlotEnd),
formatSlotTypeLabelCN(slot.SlotType),
tags,
detailLines,
map[string]any{
"day": slot.Day,
"week": slot.Week,
"day_of_week": slot.DayOfWeek,
"slot_start": slot.SlotStart,
"slot_end": slot.SlotEnd,
"slot_type": slot.SlotType,
},
))
}
metrics := []MetricField{
BuildMetric("候选时段", fmt.Sprintf("%d 个", payload.Count)),
BuildMetric("纯空位", fmt.Sprintf("%d 个", payload.StrictCount)),
}
if payload.AllowEmbed {
metrics = append(metrics, BuildMetric("可嵌入候选", fmt.Sprintf("%d 个", payload.EmbeddedCount)))
}
sections := []map[string]any{
BuildKVSection("查询概况", []KVField{
BuildKVField("查询跨度", fmt.Sprintf("%d 节连续时段", maxInt(payload.Span, 1))),
BuildKVField("日期范围", formatDayScopeLabelCN(payload.DayScope)),
BuildKVField("星期过滤", formatWeekdayListCN(payload.DayOfWeek)),
BuildKVField("周次范围", buildWeekRangeLabelCN(payload.WeekFrom, payload.WeekTo, payload.WeekFilter)),
BuildKVField("允许嵌入补位", formatBoolLabelCN(payload.AllowEmbed)),
BuildKVField("排除节次", formatScheduleSectionListCN(payload.ExcludeSections)),
}),
}
appendSectionIfPresent(&sections, BuildArgsSection("筛选条件", input.ArgFields))
if len(items) > 0 {
sections = append(sections, BuildItemsSection("候选时段", items))
} else {
sections = append(sections, BuildCalloutSection(
"没有找到可用时段",
"当前筛选条件下没有命中的候选落点。",
"info",
[]string{"可以调整周次、星期、节次范围,或修改是否允许嵌入补位。"},
))
}
title := fmt.Sprintf("找到 %d 个可用时段", payload.Count)
if payload.Count == 0 {
title = "未找到可用时段"
}
return BuildResultView(BuildResultViewInput{
Status: StatusDone,
Title: title,
Subtitle: buildAvailableSlotsSubtitle(payload),
Metrics: metrics,
Items: items,
Sections: sections,
Observation: input.Observation,
MachinePayload: machinePayload,
})
}
// BuildRangeView 根据是否传入 slot_start / slot_end 选择整天或指定范围视图。
func BuildRangeView(input RangeViewInput) ReadResultView {
if input.State == nil {
return BuildFailureView(BuildFailureViewInput{
ToolName: "query_range",
Observation: input.Observation,
ArgFields: input.ArgFields,
})
}
if input.SlotStart == nil || input.SlotEnd == nil {
return BuildRangeFullDayView(RangeFullDayViewInput{
State: input.State,
Observation: input.Observation,
Day: input.Day,
ArgFields: input.ArgFields,
})
}
return BuildRangeSpecificView(RangeSpecificViewInput{
State: input.State,
Observation: input.Observation,
Day: input.Day,
SlotStart: *input.SlotStart,
SlotEnd: *input.SlotEnd,
ArgFields: input.ArgFields,
})
}
// BuildRangeFullDayView 构造 query_range 整天模式视图。
func BuildRangeFullDayView(input RangeFullDayViewInput) ReadResultView {
if input.State == nil {
return BuildFailureView(BuildFailureViewInput{
ToolName: "query_range",
Observation: input.Observation,
ArgFields: input.ArgFields,
})
}
totalOccupied := countScheduleDayOccupiedForRead(input.State, input.Day)
taskOccupied := countScheduleDayTaskOccupiedForRead(input.State, input.Day)
freeRanges := findScheduleFreeRangesOnDayForRead(input.State, input.Day)
bandItems := make([]ItemView, 0, 6)
for start := 1; start <= 11; start += 2 {
end := start + 1
occupants := listScheduleTasksInRangeForRead(input.State, input.Day, start, end, true)
detailLines := make([]string, 0, len(occupants))
for _, occupant := range occupants {
detailLines = append(detailLines, buildRangeOccupantLine(*occupant.Task))
}
subtitle := "空闲"
tags := []string{"2 节"}
if len(occupants) > 0 {
subtitle = fmt.Sprintf("%d 个事项", len(occupants))
tags = append(tags, "已占用")
} else {
tags = append(tags, "空闲")
detailLines = append(detailLines, "这一段当前可直接安排任务。")
}
bandItems = append(bandItems, BuildItem(
formatScheduleSlotRangeCN(start, end),
subtitle,
tags,
detailLines,
map[string]any{
"day": input.Day,
"slot_start": start,
"slot_end": end,
},
))
}
freeItems := make([]ItemView, 0, len(freeRanges))
for _, freeRange := range freeRanges {
freeItems = append(freeItems, BuildItem(
formatScheduleSlotRangeCN(freeRange.SlotStart, freeRange.SlotEnd),
fmt.Sprintf("%d 节连续空闲", freeRange.SlotEnd-freeRange.SlotStart+1),
[]string{"连续空闲"},
[]string{fmt.Sprintf("位置:%s", formatScheduleDaySlotCN(input.State, input.Day, freeRange.SlotStart, freeRange.SlotEnd))},
map[string]any{
"day": input.Day,
"slot_start": freeRange.SlotStart,
"slot_end": freeRange.SlotEnd,
},
))
}
taskEntries := listScheduleTasksOnDayForRead(input.State, input.Day, false)
taskItems := make([]ItemView, 0, len(taskEntries))
for _, entry := range taskEntries {
taskItems = append(taskItems, BuildItem(
fmt.Sprintf("[%d]%s", entry.Task.StateID, fallbackText(entry.Task.Name, "未命名任务")),
formatScheduleTaskStatusCN(*entry.Task),
[]string{fallbackText(entry.Task.Category, "未分类")},
[]string{
fmt.Sprintf("时段:%s", formatScheduleSlotRangeCN(entry.SlotStart, entry.SlotEnd)),
fmt.Sprintf("来源:%s", formatScheduleTaskSourceCN(*entry.Task)),
},
map[string]any{
"task_id": entry.Task.StateID,
"slot_start": entry.SlotStart,
"slot_end": entry.SlotEnd,
"task_status": entry.Task.Status,
},
))
}
sections := []map[string]any{
BuildKVSection("当日概况", []KVField{
BuildKVField("总占用", fmt.Sprintf("%d/12 节", totalOccupied)),
BuildKVField("任务占用", fmt.Sprintf("%d/12 节", taskOccupied)),
BuildKVField("连续空闲段", fmt.Sprintf("%d 段", len(freeRanges))),
}),
BuildItemsSection("时段分布", bandItems),
}
if len(freeItems) > 0 {
sections = append(sections, BuildItemsSection("连续空闲区", freeItems))
}
if embeddableItems := buildEmbeddableItemsForDay(input.State, input.Day); len(embeddableItems) > 0 {
sections = append(sections, BuildItemsSection("可嵌入时段", embeddableItems))
}
if len(taskItems) > 0 {
sections = append(sections, BuildItemsSection("当日任务", taskItems))
}
appendSectionIfPresent(&sections, BuildArgsSection("查询条件", input.ArgFields))
return BuildResultView(BuildResultViewInput{
Status: StatusDone,
Title: fmt.Sprintf("%s全日概况", formatScheduleDayCN(input.State, input.Day)),
Subtitle: fmt.Sprintf("已占用 %d/12 节,连续空闲 %d 段。", totalOccupied, len(freeRanges)),
Metrics: []MetricField{
BuildMetric("总占用", fmt.Sprintf("%d/12", totalOccupied)),
BuildMetric("任务占用", fmt.Sprintf("%d/12", taskOccupied)),
BuildMetric("空闲段", fmt.Sprintf("%d 段", len(freeRanges))),
},
Items: bandItems,
Sections: sections,
Observation: input.Observation,
MachinePayload: map[string]any{
"mode": "full_day",
"day": input.Day,
"occupied_slots": totalOccupied,
"task_occupied_slots": taskOccupied,
"free_range_count": len(freeRanges),
},
})
}
// BuildRangeSpecificView 构造 query_range 指定范围模式视图。
func BuildRangeSpecificView(input RangeSpecificViewInput) ReadResultView {
if input.State == nil {
return BuildFailureView(BuildFailureViewInput{
ToolName: "query_range",
Observation: input.Observation,
ArgFields: input.ArgFields,
})
}
total := input.SlotEnd - input.SlotStart + 1
freeCount := 0
slotItems := make([]ItemView, 0, total)
for section := input.SlotStart; section <= input.SlotEnd; section++ {
occupants := listScheduleTasksInRangeForRead(input.State, input.Day, section, section, true)
detailLines := make([]string, 0, len(occupants))
for _, occupant := range occupants {
detailLines = append(detailLines, buildRangeOccupantLine(*occupant.Task))
}
subtitle := "空闲"
tags := []string{"空闲"}
if len(occupants) > 0 {
subtitle = fmt.Sprintf("%d 个事项", len(occupants))
tags = []string{"已占用"}
} else {
freeCount++
detailLines = append(detailLines, "这一节当前为空。")
}
slotItems = append(slotItems, BuildItem(
fmt.Sprintf("第%d节", section),
subtitle,
tags,
detailLines,
map[string]any{
"day": input.Day,
"slot_start": section,
"slot_end": section,
},
))
}
seen := make(map[int]struct{})
rangeTaskItems := make([]ItemView, 0)
for _, occupant := range listScheduleTasksInRangeForRead(input.State, input.Day, input.SlotStart, input.SlotEnd, true) {
if _, exists := seen[occupant.Task.StateID]; exists {
continue
}
seen[occupant.Task.StateID] = struct{}{}
rangeTaskItems = append(rangeTaskItems, BuildItem(
fmt.Sprintf("[%d]%s", occupant.Task.StateID, fallbackText(occupant.Task.Name, "未命名任务")),
formatScheduleTaskStatusCN(*occupant.Task),
[]string{fallbackText(occupant.Task.Category, "未分类")},
[]string{
fmt.Sprintf("覆盖范围:%s", formatScheduleDaySlotCN(input.State, input.Day, occupant.SlotStart, occupant.SlotEnd)),
fmt.Sprintf("来源:%s", formatScheduleTaskSourceCN(*occupant.Task)),
},
map[string]any{
"task_id": occupant.Task.StateID,
"slot_start": occupant.SlotStart,
"slot_end": occupant.SlotEnd,
"task_status": occupant.Task.Status,
},
))
}
sections := []map[string]any{
BuildKVSection("范围概况", []KVField{
BuildKVField("查询范围", formatScheduleSlotRangeCN(input.SlotStart, input.SlotEnd)),
BuildKVField("总节数", fmt.Sprintf("%d 节", total)),
BuildKVField("空闲节数", fmt.Sprintf("%d 节", freeCount)),
BuildKVField("占用节数", fmt.Sprintf("%d 节", total-freeCount)),
}),
BuildItemsSection("逐节情况", slotItems),
}
if len(rangeTaskItems) > 0 {
sections = append(sections, BuildItemsSection("范围内事项", rangeTaskItems))
}
appendSectionIfPresent(&sections, BuildArgsSection("查询条件", input.ArgFields))
return BuildResultView(BuildResultViewInput{
Status: StatusDone,
Title: fmt.Sprintf("%s %s", formatScheduleDayCN(input.State, input.Day), formatScheduleSlotRangeCN(input.SlotStart, input.SlotEnd)),
Subtitle: fmt.Sprintf("共 %d 节,空闲 %d 节,占用 %d 节。", total, freeCount, total-freeCount),
Metrics: []MetricField{
BuildMetric("总节数", fmt.Sprintf("%d 节", total)),
BuildMetric("空闲", fmt.Sprintf("%d 节", freeCount)),
BuildMetric("事项", fmt.Sprintf("%d 个", len(rangeTaskItems))),
},
Items: slotItems,
Sections: sections,
Observation: input.Observation,
MachinePayload: map[string]any{
"mode": "specific_range",
"day": input.Day,
"slot_start": input.SlotStart,
"slot_end": input.SlotEnd,
"free_count": freeCount,
"occupied_count": total - freeCount,
},
})
}
// DecodeAvailableSlotsPayload 解析 query_available_slots 的 JSON observation。
func DecodeAvailableSlotsPayload(observation string) (AvailableSlotsPayload, map[string]any, bool) {
var payload AvailableSlotsPayload
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 buildAvailableSlotsSubtitle(payload AvailableSlotsPayload) string {
parts := []string{
fmt.Sprintf("%d 节连续时段", maxInt(payload.Span, 1)),
formatDayScopeLabelCN(payload.DayScope),
buildWeekRangeLabelCN(payload.WeekFrom, payload.WeekTo, payload.WeekFilter),
}
if len(payload.DayOfWeek) > 0 {
parts = append(parts, formatWeekdayListCN(payload.DayOfWeek))
}
if payload.AllowEmbed {
parts = append(parts, "允许补充可嵌入候选")
} else {
parts = append(parts, "仅查看纯空位")
}
return strings.Join(parts, "")
}
func buildEmbeddableItemsForDay(state *schedule.ScheduleState, day int) []ItemView {
if state == nil {
return nil
}
items := make([]ItemView, 0)
for i := range state.Tasks {
task := state.Tasks[i]
if !task.CanEmbed || task.EmbeddedBy != nil || task.EmbedHost != nil {
continue
}
for _, slot := range task.Slots {
if slot.Day != day {
continue
}
items = append(items, BuildItem(
formatScheduleSlotRangeCN(slot.SlotStart, slot.SlotEnd),
fmt.Sprintf("可嵌入到 [%d]%s", task.StateID, fallbackText(task.Name, "未命名任务")),
[]string{fallbackText(task.Category, "未分类"), formatScheduleTaskStatusCN(task)},
[]string{
fmt.Sprintf("宿主时段:%s", formatScheduleDaySlotCN(state, day, slot.SlotStart, slot.SlotEnd)),
"该时段允许放入更短的嵌入任务。",
},
map[string]any{
"host_task_id": task.StateID,
"day": day,
"slot_start": slot.SlotStart,
"slot_end": slot.SlotEnd,
},
))
}
}
return items
}
func buildRangeOccupantLine(task schedule.ScheduleTask) string {
return fmt.Sprintf(
"[%d]%s%s%s",
task.StateID,
fallbackText(task.Name, "未命名任务"),
formatScheduleTaskStatusCN(task),
fallbackText(task.Category, "未分类"),
)
}