后端: 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. 更新工具结果结构化交接文档,补记第四批切流范围、当前切流点与后续收尾建议。
302 lines
9.9 KiB
Go
302 lines
9.9 KiB
Go
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(§ions, 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(§ions, 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
|
||
}
|