Files
smartmate/backend/newAgent/tools/schedule_read/tasks.go
Losita d89e2830a9 Version: 0.9.52.dev.260428
后端:
1. 工具结果结构化切流继续推进:schedule 读工具改为“父包 adapter + 子包 view builder”,`queue_pop_head` / `queue_skip_head` 脱离 legacy wrapper,`analyze_health` / `analyze_rhythm` 补齐 `schedule.analysis_result` 诊断卡片。
2. 非 schedule 工具补齐专属结果协议:`web_search` / `web_fetch`、`upsert_task_class`、`context_tools_add` / `context_tools_remove` 全部接入结构化 `ResultView`,注册表继续去 legacy wrapper,同时保持原始 `ObservationText` 供模型链路复用。
3. 工具展示细节继续收口:参数本地化补齐 `domain` / `packs` / `mode` / `all`,deliver 阶段补发段落分隔,避免 execute 与总结正文黏连。

前端:
4. `ToolCardRenderer` 升级为多协议通用渲染器,补齐 read / analysis / web / taskclass / context 卡片渲染、参数折叠区、未知协议兜底与操作明细展示。
5. `AssistantPanel` 修正 `tool_result` 结果回填与卡片布局宽度问题,并新增结构化卡片 fixture / mock 调试入口,便于整体验收。

仓库:
6. 更新工具结果结构化交接文档,补记第四批切流范围、当前切流点与后续收尾建议。
2026-04-28 20:22:22 +08:00

302 lines
9.9 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"
"strings"
"github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
)
// BuildTargetTasksView 构造 query_target_tasks 的纯展示视图。
func BuildTargetTasksView(input TargetTasksViewInput) ReadResultView {
payload, machinePayload, ok := DecodeTargetTasksPayload(input.Observation)
if !ok || !payload.Success {
return BuildFailureView(BuildFailureViewInput{
ToolName: "query_target_tasks",
Observation: input.Observation,
ArgFields: input.ArgFields,
})
}
items := make([]ItemView, 0, len(payload.Items))
for _, item := range payload.Items {
items = append(items, BuildItem(
fmt.Sprintf("[%d]%s", item.TaskID, fallbackText(item.Name, "未命名任务")),
buildTargetTaskSubtitle(item),
buildTargetTaskTags(item),
buildTargetTaskDetailLines(input.State, item),
map[string]any{
"task_id": item.TaskID,
"category": item.Category,
"status": item.Status,
"duration": item.Duration,
"task_class_id": item.TaskClassID,
},
))
}
metrics := []MetricField{
BuildMetric("候选任务", fmt.Sprintf("%d 项", payload.Count)),
BuildMetric("任务池", formatTargetPoolStatusCN(payload.Status)),
}
if payload.Enqueue {
metrics = append(metrics, BuildMetric("已入队", fmt.Sprintf("%d 项", payload.Enqueued)))
}
sections := []map[string]any{
BuildKVSection("筛选概况", []KVField{
BuildKVField("任务池", formatTargetPoolStatusCN(payload.Status)),
BuildKVField("日期范围", formatDayScopeLabelCN(payload.DayScope)),
BuildKVField("星期过滤", formatWeekdayListCN(payload.DayOfWeek)),
BuildKVField("周次范围", buildWeekRangeLabelCN(payload.WeekFrom, payload.WeekTo, payload.WeekFilter)),
BuildKVField("是否入队", formatBoolLabelCN(payload.Enqueue)),
}),
}
appendSectionIfPresent(&sections, BuildArgsSection("筛选条件", input.ArgFields))
if payload.Queue != nil {
sections = append(sections, BuildKVSection("队列状态", []KVField{
BuildKVField("待处理", fmt.Sprintf("%d 项", payload.Queue.PendingCount)),
BuildKVField("已完成", fmt.Sprintf("%d 项", payload.Queue.CompletedCount)),
BuildKVField("已跳过", fmt.Sprintf("%d 项", payload.Queue.SkippedCount)),
BuildKVField("当前任务", resolveTaskQueueLabelByID(input.State, payload.Queue.CurrentTaskID)),
}))
}
if len(items) > 0 {
sections = append(sections, BuildItemsSection("候选任务", items))
} else {
sections = append(sections, BuildCalloutSection(
"没有命中任务",
"当前筛选条件下没有找到候选任务。",
"info",
[]string{"可以放宽状态、日期或任务 ID 过滤条件后再试。"},
))
}
title := fmt.Sprintf("找到 %d 个候选任务", payload.Count)
if payload.Count == 0 {
title = "未找到候选任务"
}
return BuildResultView(BuildResultViewInput{
Status: StatusDone,
Title: title,
Subtitle: buildTargetTasksSummarySubtitle(payload),
Metrics: metrics,
Items: items,
Sections: sections,
Observation: input.Observation,
MachinePayload: machinePayload,
})
}
// BuildTaskInfoView 构造 get_task_info 的纯展示视图。
func BuildTaskInfoView(input TaskInfoViewInput) ReadResultView {
if input.State == nil {
return BuildFailureView(BuildFailureViewInput{
ToolName: "get_task_info",
Observation: input.Observation,
ArgFields: input.ArgFields,
})
}
task := input.State.TaskByStateID(input.TaskID)
if task == nil {
return BuildFailureView(BuildFailureViewInput{
ToolName: "get_task_info",
Observation: input.Observation,
ArgFields: input.ArgFields,
})
}
slotItems := make([]ItemView, 0, len(task.Slots))
for _, slot := range cloneAndSortTaskSlots(task.Slots) {
slotItems = append(slotItems, BuildItem(
formatScheduleDaySlotCN(input.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 := []KVField{
BuildKVField("类别", fallbackText(task.Category, "未分类")),
BuildKVField("状态", formatScheduleTaskStatusCN(*task)),
BuildKVField("来源", formatScheduleTaskSourceCN(*task)),
BuildKVField("落位情况", buildTaskPlacementLabel(task)),
BuildKVField("时长需求", buildTaskDurationLabel(task)),
}
if task.TaskClassID > 0 {
fields = append(fields, BuildKVField("任务类 ID", fmt.Sprintf("%d", task.TaskClassID)))
}
if task.CanEmbed {
fields = append(fields, BuildKVField("可作为宿主", "是"))
}
sections := []map[string]any{
BuildKVSection("基本信息", fields),
}
if len(slotItems) > 0 {
sections = append(sections, BuildItemsSection("占用时段", slotItems))
}
if relationLines := buildTaskRelationLines(input.State, task); len(relationLines) > 0 {
sections = append(sections, BuildCalloutSection("嵌入关系", "当前任务存在宿主或宿体关系。", "info", relationLines))
}
appendSectionIfPresent(&sections, BuildArgsSection("查询条件", input.ArgFields))
return BuildResultView(BuildResultViewInput{
Status: StatusDone,
Title: fmt.Sprintf("[%d]%s", task.StateID, fallbackText(task.Name, "未命名任务")),
Subtitle: fmt.Sprintf("%s%s", fallbackText(task.Category, "未分类"), formatScheduleTaskStatusCN(*task)),
Metrics: []MetricField{
BuildMetric("状态", formatScheduleTaskStatusCN(*task)),
BuildMetric("时长", buildTaskDurationLabel(task)),
BuildMetric("落位", buildTaskPlacementLabel(task)),
},
Items: slotItems,
Sections: sections,
Observation: input.Observation,
MachinePayload: map[string]any{
"task_id": task.StateID,
"source": task.Source,
"status": task.Status,
"task_class_id": task.TaskClassID,
"can_embed": task.CanEmbed,
"embedded_by": optionalIntValue(task.EmbeddedBy),
"embed_host": optionalIntValue(task.EmbedHost),
},
})
}
// DecodeTargetTasksPayload 解析 query_target_tasks 的 JSON observation。
func DecodeTargetTasksPayload(observation string) (TargetTasksPayload, map[string]any, bool) {
var payload TargetTasksPayload
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 buildTargetTasksSummarySubtitle(payload TargetTasksPayload) 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 TargetTaskRecord) string {
return fmt.Sprintf("%s%s", fallbackText(item.Category, "未分类"), formatTargetTaskStatusCN(item.Status))
}
func buildTargetTaskTags(item TargetTaskRecord) []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 TargetTaskRecord) []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
}