后端: 1. schedule 读工具结果正式切到结构化 `schedule.read_result` 视图——`get_overview` / `query_range` / `query_available_slots` / `query_target_tasks` / `get_task_info` / `queue_status` 新增独立 handler,`ToolExecutionResult` 继续保留 `ObservationText` 给 LLM,但前端展示改走 `result_view` 的 collapsed / expanded 结构,统一输出 metrics / items / sections / machine_payload 2. `execution_result` 补齐第二批读工具参数本地化展示——扩展 `task_ids` / `task_item_ids` / `status` / `category` / `day_scope` / `week_filter` / `slot_types` / `include_pending` / `detail` / `dimensions` 等参数的排序权重、中文标签与展示格式,支持列表 / 布尔 / 周次 / 星期 / 节次等 `argument_view` 渲染 3. ToolRegistry 继续从内联注册收口到专属 handler——schedule 读工具从 `wrapLegacyToolHandler` 切到 `NewXxxToolHandler`,旧 `schedule` 子包里的 observation 生成逻辑暂时保留,当前切流点已落在 `newAgent/tools` 结构化适配层 4. 新增《工具结果结构化交接文档》,明确第二批 read 工具已迁移范围、`schedule.read_result` 协议、当前旧实现保留边界,以及下一轮建议迁移的 `schedule_analysis` 方向
301 lines
10 KiB
Go
301 lines
10 KiB
Go
package newagenttools
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"strings"
|
||
|
||
"github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||
)
|
||
|
||
// NewQueryTargetTasksToolHandler 为 query_target_tasks 生成结构化读结果。
|
||
func NewQueryTargetTasksToolHandler() ToolHandler {
|
||
return func(state *schedule.ScheduleState, args map[string]any) ToolExecutionResult {
|
||
observation := schedule.QueryTargetTasks(state, args)
|
||
status, _ := resolveToolStatusAndSuccess(observation)
|
||
if status != ToolStatusDone {
|
||
return buildScheduleReadSimpleFailureResult("query_target_tasks", args, state, observation)
|
||
}
|
||
|
||
payload, machinePayload := decodeTargetTasksPayload(observation)
|
||
items := make([]map[string]any, 0, len(payload.Items))
|
||
for _, item := range payload.Items {
|
||
items = append(items, buildScheduleReadItem(
|
||
fmt.Sprintf("[%d]%s", item.TaskID, fallbackText(item.Name, "未命名任务")),
|
||
buildTargetTaskSubtitle(item),
|
||
buildTargetTaskTags(item),
|
||
buildTargetTaskDetailLines(state, item),
|
||
map[string]any{
|
||
"task_id": item.TaskID,
|
||
"category": item.Category,
|
||
"status": item.Status,
|
||
"duration": item.Duration,
|
||
"task_class_id": item.TaskClassID,
|
||
},
|
||
))
|
||
}
|
||
|
||
metrics := []map[string]any{
|
||
buildScheduleReadMetric("候选任务", fmt.Sprintf("%d 项", payload.Count)),
|
||
buildScheduleReadMetric("任务池", formatTargetPoolStatusCN(payload.Status)),
|
||
}
|
||
if payload.Enqueue {
|
||
metrics = append(metrics, buildScheduleReadMetric("已入队", fmt.Sprintf("%d 项", payload.Enqueued)))
|
||
}
|
||
|
||
sections := []map[string]any{
|
||
buildScheduleReadKVSection("筛选概况", []map[string]any{
|
||
buildScheduleReadKV("任务池", formatTargetPoolStatusCN(payload.Status)),
|
||
buildScheduleReadKV("日期范围", formatDayScopeLabelCN(payload.DayScope)),
|
||
buildScheduleReadKV("星期过滤", formatWeekdayListCN(payload.DayOfWeek)),
|
||
buildScheduleReadKV("周次范围", buildWeekRangeLabelCN(payload.WeekFrom, payload.WeekTo, payload.WeekFilter)),
|
||
buildScheduleReadKV("是否入队", formatBoolLabelCN(payload.Enqueue)),
|
||
}),
|
||
}
|
||
appendSectionIfPresent(§ions, buildScheduleReadArgsSection("筛选条件", LegacyResultWithState("query_target_tasks", args, state, observation).ArgumentView))
|
||
if payload.Queue != nil {
|
||
sections = append(sections, buildScheduleReadKVSection("队列状态", []map[string]any{
|
||
buildScheduleReadKV("待处理", fmt.Sprintf("%d 项", payload.Queue.PendingCount)),
|
||
buildScheduleReadKV("已完成", fmt.Sprintf("%d 项", payload.Queue.CompletedCount)),
|
||
buildScheduleReadKV("已跳过", fmt.Sprintf("%d 项", payload.Queue.SkippedCount)),
|
||
buildScheduleReadKV("当前任务", resolveTaskQueueLabelByID(state, payload.Queue.CurrentTaskID)),
|
||
}))
|
||
}
|
||
if len(items) > 0 {
|
||
sections = append(sections, buildScheduleReadItemsSection("候选任务", items))
|
||
} else {
|
||
sections = append(sections, buildScheduleReadCalloutSection(
|
||
"没有命中任务",
|
||
"当前筛选条件下没有找到候选任务。",
|
||
"info",
|
||
[]string{"可以放宽状态、日期或任务 ID 过滤条件后再试。"},
|
||
))
|
||
}
|
||
|
||
title := fmt.Sprintf("找到 %d 个候选任务", payload.Count)
|
||
if payload.Count == 0 {
|
||
title = "未找到候选任务"
|
||
}
|
||
return buildScheduleReadResult(
|
||
"query_target_tasks",
|
||
args,
|
||
state,
|
||
observation,
|
||
ToolStatusDone,
|
||
title,
|
||
buildTargetTasksSummarySubtitle(payload),
|
||
metrics,
|
||
items,
|
||
sections,
|
||
machinePayload,
|
||
)
|
||
}
|
||
}
|
||
|
||
// NewGetTaskInfoToolHandler 为 get_task_info 生成结构化读结果。
|
||
func NewGetTaskInfoToolHandler() ToolHandler {
|
||
return func(state *schedule.ScheduleState, args map[string]any) ToolExecutionResult {
|
||
taskID, ok := schedule.ArgsInt(args, "task_id")
|
||
if !ok {
|
||
return buildScheduleReadSimpleFailureResult("get_task_info", args, state, "查询失败:缺少必填参数 task_id。")
|
||
}
|
||
if state == nil {
|
||
return buildScheduleReadSimpleFailureResult("get_task_info", args, nil, "查询失败:日程状态为空,无法读取任务详情。")
|
||
}
|
||
|
||
observation := schedule.GetTaskInfo(state, taskID)
|
||
task := state.TaskByStateID(taskID)
|
||
if task == nil {
|
||
return buildScheduleReadSimpleFailureResult("get_task_info", args, state, observation)
|
||
}
|
||
|
||
slotItems := make([]map[string]any, 0, len(task.Slots))
|
||
for _, slot := range cloneAndSortTaskSlots(task.Slots) {
|
||
slotItems = append(slotItems, buildScheduleReadItem(
|
||
formatScheduleDaySlotCN(state, slot.Day, slot.SlotStart, slot.SlotEnd),
|
||
formatScheduleTaskStatusCN(*task),
|
||
[]string{fallbackText(task.Category, "未分类")},
|
||
[]string{
|
||
fmt.Sprintf("来源:%s", formatScheduleTaskSourceCN(*task)),
|
||
fmt.Sprintf("时长:%d 节", slot.SlotEnd-slot.SlotStart+1),
|
||
},
|
||
map[string]any{
|
||
"day": slot.Day,
|
||
"slot_start": slot.SlotStart,
|
||
"slot_end": slot.SlotEnd,
|
||
},
|
||
))
|
||
}
|
||
|
||
fields := []map[string]any{
|
||
buildScheduleReadKV("类别", fallbackText(task.Category, "未分类")),
|
||
buildScheduleReadKV("状态", formatScheduleTaskStatusCN(*task)),
|
||
buildScheduleReadKV("来源", formatScheduleTaskSourceCN(*task)),
|
||
buildScheduleReadKV("落位情况", buildTaskPlacementLabel(task)),
|
||
buildScheduleReadKV("时长需求", buildTaskDurationLabel(task)),
|
||
}
|
||
if task.TaskClassID > 0 {
|
||
fields = append(fields, buildScheduleReadKV("任务类 ID", fmt.Sprintf("%d", task.TaskClassID)))
|
||
}
|
||
if task.CanEmbed {
|
||
fields = append(fields, buildScheduleReadKV("可作为宿主", "是"))
|
||
}
|
||
|
||
sections := []map[string]any{
|
||
buildScheduleReadKVSection("基本信息", fields),
|
||
}
|
||
if len(slotItems) > 0 {
|
||
sections = append(sections, buildScheduleReadItemsSection("占用时段", slotItems))
|
||
}
|
||
if relationLines := buildTaskRelationLines(state, task); len(relationLines) > 0 {
|
||
sections = append(sections, buildScheduleReadCalloutSection("嵌入关系", "当前任务存在宿主/客体关系。", "info", relationLines))
|
||
}
|
||
appendSectionIfPresent(§ions, buildScheduleReadArgsSection("查询条件", LegacyResultWithState("get_task_info", args, state, observation).ArgumentView))
|
||
|
||
return buildScheduleReadResult(
|
||
"get_task_info",
|
||
args,
|
||
state,
|
||
observation,
|
||
ToolStatusDone,
|
||
fmt.Sprintf("[%d]%s", task.StateID, fallbackText(task.Name, "未命名任务")),
|
||
fmt.Sprintf("%s|%s", fallbackText(task.Category, "未分类"), formatScheduleTaskStatusCN(*task)),
|
||
[]map[string]any{
|
||
buildScheduleReadMetric("状态", formatScheduleTaskStatusCN(*task)),
|
||
buildScheduleReadMetric("时长", buildTaskDurationLabel(task)),
|
||
buildScheduleReadMetric("落位", buildTaskPlacementLabel(task)),
|
||
},
|
||
slotItems,
|
||
sections,
|
||
map[string]any{
|
||
"task_id": task.StateID,
|
||
"source": task.Source,
|
||
"status": task.Status,
|
||
"task_class_id": task.TaskClassID,
|
||
"can_embed": task.CanEmbed,
|
||
"embedded_by": task.EmbeddedBy,
|
||
"embed_host": task.EmbedHost,
|
||
},
|
||
)
|
||
}
|
||
}
|
||
|
||
func decodeTargetTasksPayload(observation string) (scheduleReadTargetTasksPayload, map[string]any) {
|
||
var payload scheduleReadTargetTasksPayload
|
||
_ = json.Unmarshal([]byte(strings.TrimSpace(observation)), &payload)
|
||
raw, _ := parseObservationJSON(strings.TrimSpace(observation))
|
||
return payload, raw
|
||
}
|
||
|
||
func buildTargetTasksSummarySubtitle(payload scheduleReadTargetTasksPayload) string {
|
||
parts := []string{
|
||
formatTargetPoolStatusCN(payload.Status),
|
||
formatDayScopeLabelCN(payload.DayScope),
|
||
buildWeekRangeLabelCN(payload.WeekFrom, payload.WeekTo, payload.WeekFilter),
|
||
}
|
||
if len(payload.DayOfWeek) > 0 {
|
||
parts = append(parts, formatWeekdayListCN(payload.DayOfWeek))
|
||
}
|
||
if payload.Enqueue {
|
||
parts = append(parts, fmt.Sprintf("已入队 %d 项", payload.Enqueued))
|
||
}
|
||
return strings.Join(parts, ",")
|
||
}
|
||
|
||
func buildTargetTaskSubtitle(item scheduleReadTargetTaskRecord) string {
|
||
return fmt.Sprintf("%s|%s", fallbackText(item.Category, "未分类"), formatTargetTaskStatusCN(item.Status))
|
||
}
|
||
|
||
func buildTargetTaskTags(item scheduleReadTargetTaskRecord) []string {
|
||
tags := []string{formatTargetTaskStatusCN(item.Status)}
|
||
if item.Duration > 0 {
|
||
tags = append(tags, fmt.Sprintf("%d 节", item.Duration))
|
||
}
|
||
if item.TaskClassID > 0 {
|
||
tags = append(tags, fmt.Sprintf("任务类 %d", item.TaskClassID))
|
||
}
|
||
return tags
|
||
}
|
||
|
||
func buildTargetTaskDetailLines(state *schedule.ScheduleState, item scheduleReadTargetTaskRecord) []string {
|
||
lines := make([]string, 0, 3)
|
||
if len(item.Slots) == 0 {
|
||
lines = append(lines, fmt.Sprintf("当前未落位,仍需要 %s。", buildTaskDurationText(item.Duration)))
|
||
} else {
|
||
slotParts := make([]string, 0, len(item.Slots))
|
||
for _, slot := range item.Slots {
|
||
slotParts = append(slotParts, formatScheduleDaySlotCN(state, slot.Day, slot.SlotStart, slot.SlotEnd))
|
||
}
|
||
lines = append(lines, "时段:"+strings.Join(slotParts, ";"))
|
||
}
|
||
if item.TaskClassID > 0 {
|
||
lines = append(lines, fmt.Sprintf("任务类 ID:%d", item.TaskClassID))
|
||
}
|
||
return lines
|
||
}
|
||
|
||
func resolveTaskQueueLabelByID(state *schedule.ScheduleState, taskID int) string {
|
||
if taskID <= 0 {
|
||
return "无"
|
||
}
|
||
if state == nil {
|
||
return fmt.Sprintf("[%d]任务", taskID)
|
||
}
|
||
task := state.TaskByStateID(taskID)
|
||
if task == nil {
|
||
return fmt.Sprintf("[%d]任务", taskID)
|
||
}
|
||
return fmt.Sprintf("[%d]%s", task.StateID, fallbackText(task.Name, "未命名任务"))
|
||
}
|
||
|
||
func buildTaskDurationLabel(task *schedule.ScheduleTask) string {
|
||
if task == nil {
|
||
return "未标注"
|
||
}
|
||
if task.Duration > 0 {
|
||
return fmt.Sprintf("%d 节", task.Duration)
|
||
}
|
||
total := 0
|
||
for _, slot := range task.Slots {
|
||
total += slot.SlotEnd - slot.SlotStart + 1
|
||
}
|
||
if total <= 0 {
|
||
return "未标注"
|
||
}
|
||
return fmt.Sprintf("%d 节", total)
|
||
}
|
||
|
||
func buildTaskDurationText(duration int) string {
|
||
if duration <= 0 {
|
||
return "未标注时长"
|
||
}
|
||
return fmt.Sprintf("%d 节连续时段", duration)
|
||
}
|
||
|
||
func buildTaskPlacementLabel(task *schedule.ScheduleTask) string {
|
||
if task == nil || len(task.Slots) == 0 {
|
||
return "尚未落位"
|
||
}
|
||
if len(task.Slots) == 1 {
|
||
slot := task.Slots[0]
|
||
return fmt.Sprintf("1 段(第%d天 第%d-%d节)", slot.Day, slot.SlotStart, slot.SlotEnd)
|
||
}
|
||
return fmt.Sprintf("%d 段", len(task.Slots))
|
||
}
|
||
|
||
func buildTaskRelationLines(state *schedule.ScheduleState, task *schedule.ScheduleTask) []string {
|
||
if task == nil {
|
||
return nil
|
||
}
|
||
lines := make([]string, 0, 2)
|
||
if task.EmbeddedBy != nil {
|
||
lines = append(lines, "当前已嵌入任务:"+resolveTaskQueueLabelByID(state, *task.EmbeddedBy))
|
||
} else if task.CanEmbed {
|
||
lines = append(lines, "当前没有嵌入其他任务。")
|
||
}
|
||
if task.EmbedHost != nil {
|
||
lines = append(lines, "嵌入宿主:"+resolveTaskQueueLabelByID(state, *task.EmbedHost))
|
||
}
|
||
return lines
|
||
}
|