Version: 0.9.51.dev.260428

后端:
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` 方向
This commit is contained in:
LoveLosita
2026-04-28 15:52:13 +08:00
parent 509e266626
commit 1a5b2ecd73
8 changed files with 2407 additions and 41 deletions

View File

@@ -0,0 +1,300 @@
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(&sections, 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(&sections, 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
}