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. 更新工具结果结构化交接文档,补记第四批切流范围、当前切流点与后续收尾建议。
This commit is contained in:
796
backend/newAgent/tools/schedule_read/common.go
Normal file
796
backend/newAgent/tools/schedule_read/common.go
Normal file
@@ -0,0 +1,796 @@
|
||||
package schedule_read
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||||
)
|
||||
|
||||
// BuildResultView 统一封装 schedule.read_result 结构。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责把已经计算好的折叠态/展开态内容组装成标准视图。
|
||||
// 2. 负责在子包内补齐 status / status_label,避免依赖父包常量。
|
||||
// 3. 不负责 ToolExecutionResult 外层协议,也不改写 observation 原文。
|
||||
func BuildResultView(input BuildResultViewInput) ReadResultView {
|
||||
status := normalizeStatus(input.Status)
|
||||
if status == "" {
|
||||
status = StatusDone
|
||||
}
|
||||
|
||||
collapsed := CollapsedView{
|
||||
Title: input.Title,
|
||||
Subtitle: input.Subtitle,
|
||||
Status: status,
|
||||
StatusLabel: resolveStatusLabelCN(status),
|
||||
Metrics: appendMetricCopy(input.Metrics),
|
||||
}
|
||||
expanded := ExpandedView{
|
||||
Items: appendItemCopy(input.Items),
|
||||
Sections: cloneSectionList(input.Sections),
|
||||
RawText: input.Observation,
|
||||
MachinePayload: cloneAnyMap(input.MachinePayload),
|
||||
}
|
||||
|
||||
return ReadResultView{
|
||||
ViewType: ViewTypeReadResult,
|
||||
Version: ViewVersionReadResult,
|
||||
Collapsed: collapsed.Map(),
|
||||
Expanded: expanded.Map(),
|
||||
}
|
||||
}
|
||||
|
||||
// BuildFailureView 统一生成 read 工具失败卡片视图。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责把失败 observation 提炼成展开态提示与参数回显。
|
||||
// 2. 不负责决定是否要失败;调用方需要在进入这里前确认失败条件。
|
||||
// 3. 若标题、副标题未显式传入,则按工具名与 observation 兜底生成。
|
||||
func BuildFailureView(input BuildFailureViewInput) ReadResultView {
|
||||
status := normalizeStatus(input.Status)
|
||||
if status == "" {
|
||||
status = StatusFailed
|
||||
}
|
||||
|
||||
title := strings.TrimSpace(input.Title)
|
||||
if title == "" {
|
||||
title = fmt.Sprintf("%s失败", resolveToolLabelCN(input.ToolName))
|
||||
}
|
||||
|
||||
subtitle := strings.TrimSpace(input.Subtitle)
|
||||
if subtitle == "" {
|
||||
subtitle = trimFailureText(input.Observation, "请检查筛选条件后重试。")
|
||||
}
|
||||
|
||||
return BuildResultView(BuildResultViewInput{
|
||||
Status: status,
|
||||
Title: title,
|
||||
Subtitle: subtitle,
|
||||
Sections: buildReadFailureSections(input.ArgFields, input.Observation),
|
||||
Observation: input.Observation,
|
||||
})
|
||||
}
|
||||
|
||||
// BuildMetric 是 collapsed.metrics 的便捷构造器。
|
||||
func BuildMetric(label string, value string) MetricField {
|
||||
return MetricField{
|
||||
Label: strings.TrimSpace(label),
|
||||
Value: strings.TrimSpace(value),
|
||||
}
|
||||
}
|
||||
|
||||
// BuildKVField 是 kv section 的便捷构造器。
|
||||
func BuildKVField(label string, value string) KVField {
|
||||
return KVField{
|
||||
Label: strings.TrimSpace(label),
|
||||
Value: strings.TrimSpace(value),
|
||||
}
|
||||
}
|
||||
|
||||
// BuildItem 是 items 的便捷构造器。
|
||||
func BuildItem(title string, subtitle string, tags []string, detailLines []string, meta map[string]any) ItemView {
|
||||
return ItemView{
|
||||
Title: strings.TrimSpace(title),
|
||||
Subtitle: strings.TrimSpace(subtitle),
|
||||
Tags: normalizeStringSlice(tags),
|
||||
DetailLines: normalizeStringSlice(detailLines),
|
||||
Meta: cloneAnyMap(meta),
|
||||
}
|
||||
}
|
||||
|
||||
// BuildItemsSection 把条目列表包装成 items section。
|
||||
func BuildItemsSection(title string, items []ItemView) map[string]any {
|
||||
normalized := make([]map[string]any, 0, len(items))
|
||||
for _, item := range items {
|
||||
normalized = append(normalized, item.Map())
|
||||
}
|
||||
return map[string]any{
|
||||
"type": "items",
|
||||
"title": strings.TrimSpace(title),
|
||||
"items": normalized,
|
||||
}
|
||||
}
|
||||
|
||||
// BuildKVSection 把 kv 列表包装成 kv section。
|
||||
func BuildKVSection(title string, fields []KVField) map[string]any {
|
||||
normalized := make([]map[string]any, 0, len(fields))
|
||||
for _, field := range fields {
|
||||
label := strings.TrimSpace(field.Label)
|
||||
value := strings.TrimSpace(field.Value)
|
||||
if label == "" || value == "" {
|
||||
continue
|
||||
}
|
||||
normalized = append(normalized, map[string]any{
|
||||
"label": label,
|
||||
"value": value,
|
||||
})
|
||||
}
|
||||
return map[string]any{
|
||||
"type": "kv",
|
||||
"title": strings.TrimSpace(title),
|
||||
"fields": normalized,
|
||||
}
|
||||
}
|
||||
|
||||
// BuildCalloutSection 把提示块包装成 callout section。
|
||||
func BuildCalloutSection(title string, subtitle string, tone string, detailLines []string) map[string]any {
|
||||
return map[string]any{
|
||||
"type": "callout",
|
||||
"title": strings.TrimSpace(title),
|
||||
"subtitle": strings.TrimSpace(subtitle),
|
||||
"tone": strings.TrimSpace(tone),
|
||||
"detail_lines": normalizeStringSlice(detailLines),
|
||||
}
|
||||
}
|
||||
|
||||
// BuildArgsSection 负责把父包已经格式化好的参数字段拼成查询条件 section。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 这里只接受纯 KVField,不依赖父包 ToolArgumentView。
|
||||
// 2. 只过滤空 label / value,不补充额外解释文案。
|
||||
// 3. 没有有效字段时返回 nil,交给调用方决定是否追加 section。
|
||||
func BuildArgsSection(title string, fields []KVField) map[string]any {
|
||||
if len(fields) == 0 {
|
||||
return nil
|
||||
}
|
||||
valid := make([]KVField, 0, len(fields))
|
||||
for _, field := range fields {
|
||||
label := strings.TrimSpace(field.Label)
|
||||
value := strings.TrimSpace(field.Value)
|
||||
if label == "" || value == "" {
|
||||
continue
|
||||
}
|
||||
valid = append(valid, BuildKVField(label, value))
|
||||
}
|
||||
if len(valid) == 0 {
|
||||
return nil
|
||||
}
|
||||
return BuildKVSection(title, valid)
|
||||
}
|
||||
|
||||
func buildReadFailureSections(argFields []KVField, observation string) []map[string]any {
|
||||
message := trimFailureText(observation, "读取结果失败,请检查参数后重试。")
|
||||
sections := []map[string]any{
|
||||
BuildCalloutSection("执行失败", message, "danger", []string{message}),
|
||||
}
|
||||
appendSectionIfPresent(§ions, BuildArgsSection("查询条件", argFields))
|
||||
return sections
|
||||
}
|
||||
|
||||
func appendSectionIfPresent(target *[]map[string]any, section map[string]any) {
|
||||
if section == nil {
|
||||
return
|
||||
}
|
||||
*target = append(*target, section)
|
||||
}
|
||||
|
||||
func appendMetricCopy(metrics []MetricField) []MetricField {
|
||||
if len(metrics) == 0 {
|
||||
return make([]MetricField, 0)
|
||||
}
|
||||
out := make([]MetricField, 0, len(metrics))
|
||||
for _, metric := range metrics {
|
||||
label := strings.TrimSpace(metric.Label)
|
||||
value := strings.TrimSpace(metric.Value)
|
||||
if label == "" || value == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, MetricField{Label: label, Value: value})
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return make([]MetricField, 0)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func appendItemCopy(items []ItemView) []ItemView {
|
||||
if len(items) == 0 {
|
||||
return make([]ItemView, 0)
|
||||
}
|
||||
out := make([]ItemView, 0, len(items))
|
||||
for _, item := range items {
|
||||
out = append(out, BuildItem(item.Title, item.Subtitle, item.Tags, item.DetailLines, item.Meta))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeStatus(status string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(status)) {
|
||||
case StatusDone:
|
||||
return StatusDone
|
||||
case StatusBlocked:
|
||||
return StatusBlocked
|
||||
case StatusFailed:
|
||||
return StatusFailed
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func resolveStatusLabelCN(status string) string {
|
||||
switch normalizeStatus(status) {
|
||||
case StatusDone:
|
||||
return "已完成"
|
||||
case StatusBlocked:
|
||||
return "已阻断"
|
||||
default:
|
||||
return "失败"
|
||||
}
|
||||
}
|
||||
|
||||
func resolveToolLabelCN(toolName string) string {
|
||||
switch strings.TrimSpace(toolName) {
|
||||
case "query_available_slots":
|
||||
return "查询可用时段"
|
||||
case "query_range":
|
||||
return "查询范围"
|
||||
case "query_target_tasks":
|
||||
return "查询目标任务"
|
||||
case "get_task_info":
|
||||
return "查询任务详情"
|
||||
case "get_overview":
|
||||
return "查看排程总览"
|
||||
case "queue_status":
|
||||
return "查看队列状态"
|
||||
default:
|
||||
return "读取结果"
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeStringSlice(values []string) []string {
|
||||
if len(values) == 0 {
|
||||
return make([]string, 0)
|
||||
}
|
||||
out := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
text := strings.TrimSpace(value)
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, text)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return make([]string, 0)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func trimFailureText(observation string, fallback string) string {
|
||||
if payload, ok := parseObservationJSON(observation); ok {
|
||||
if message, ok := readStringFromMap(payload, "error", "err", "message", "reason"); ok && strings.TrimSpace(message) != "" {
|
||||
return strings.TrimSpace(message)
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(observation) != "" {
|
||||
return strings.TrimSpace(observation)
|
||||
}
|
||||
return strings.TrimSpace(fallback)
|
||||
}
|
||||
|
||||
func formatScheduleDayCN(state *schedule.ScheduleState, day int) string {
|
||||
if day <= 0 {
|
||||
return "未知日期"
|
||||
}
|
||||
if state != nil {
|
||||
if week, dayOfWeek, ok := state.DayToWeekDay(day); ok {
|
||||
return fmt.Sprintf("第%d天(第%d周 %s)", day, week, formatScheduleWeekdayCN(dayOfWeek))
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("第%d天", day)
|
||||
}
|
||||
|
||||
func formatScheduleWeekdayCN(dayOfWeek int) string {
|
||||
switch dayOfWeek {
|
||||
case 1:
|
||||
return "周一"
|
||||
case 2:
|
||||
return "周二"
|
||||
case 3:
|
||||
return "周三"
|
||||
case 4:
|
||||
return "周四"
|
||||
case 5:
|
||||
return "周五"
|
||||
case 6:
|
||||
return "周六"
|
||||
case 7:
|
||||
return "周日"
|
||||
default:
|
||||
return fmt.Sprintf("周%d", dayOfWeek)
|
||||
}
|
||||
}
|
||||
|
||||
func formatScheduleSlotRangeCN(start int, end int) string {
|
||||
if start <= 0 {
|
||||
return "未知节次"
|
||||
}
|
||||
if end <= 0 || end < start {
|
||||
end = start
|
||||
}
|
||||
return fmt.Sprintf("第%d-%d节", start, end)
|
||||
}
|
||||
|
||||
func formatScheduleDaySlotCN(state *schedule.ScheduleState, day int, start int, end int) string {
|
||||
return fmt.Sprintf("%s %s", formatScheduleDayCN(state, day), formatScheduleSlotRangeCN(start, end))
|
||||
}
|
||||
|
||||
func formatScheduleWeekListCN(weeks []int) string {
|
||||
if len(weeks) == 0 {
|
||||
return "不限周次"
|
||||
}
|
||||
parts := make([]string, 0, len(weeks))
|
||||
for _, week := range weeks {
|
||||
if week <= 0 {
|
||||
continue
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("第%d周", week))
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return "不限周次"
|
||||
}
|
||||
return strings.Join(parts, "、")
|
||||
}
|
||||
|
||||
func formatScheduleSectionListCN(sections []int) string {
|
||||
if len(sections) == 0 {
|
||||
return "无"
|
||||
}
|
||||
parts := make([]string, 0, len(sections))
|
||||
for _, section := range sections {
|
||||
if section <= 0 {
|
||||
continue
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("第%d节", section))
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return "无"
|
||||
}
|
||||
return strings.Join(parts, "、")
|
||||
}
|
||||
|
||||
func formatScheduleTaskStatusCN(task schedule.ScheduleTask) string {
|
||||
switch {
|
||||
case schedule.IsPendingTask(task):
|
||||
return "待安排"
|
||||
case schedule.IsSuggestedTask(task):
|
||||
return "已预排"
|
||||
default:
|
||||
if task.Locked {
|
||||
return "已安排(固定)"
|
||||
}
|
||||
return "已安排"
|
||||
}
|
||||
}
|
||||
|
||||
func formatTargetTaskStatusCN(status string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(status)) {
|
||||
case "existing":
|
||||
return "已安排"
|
||||
case "suggested":
|
||||
return "已预排"
|
||||
case "pending":
|
||||
return "待安排"
|
||||
default:
|
||||
return fallbackText(status, "未标注")
|
||||
}
|
||||
}
|
||||
|
||||
func formatTargetPoolStatusCN(status string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(status)) {
|
||||
case "all":
|
||||
return "全部任务"
|
||||
case "existing":
|
||||
return "已安排任务"
|
||||
case "suggested":
|
||||
return "已预排任务"
|
||||
case "pending":
|
||||
return "待安排任务"
|
||||
default:
|
||||
return fallbackText(status, "任务池")
|
||||
}
|
||||
}
|
||||
|
||||
func formatSlotTypeLabelCN(slotType string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(slotType)) {
|
||||
case "", "empty", "strict":
|
||||
return "纯空位"
|
||||
case "embedded_candidate", "embedded", "embed":
|
||||
return "可嵌入候选"
|
||||
default:
|
||||
return strings.TrimSpace(slotType)
|
||||
}
|
||||
}
|
||||
|
||||
func formatDayScopeLabelCN(scope string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(scope)) {
|
||||
case "workday":
|
||||
return "工作日"
|
||||
case "weekend":
|
||||
return "周末"
|
||||
default:
|
||||
return "全部日期"
|
||||
}
|
||||
}
|
||||
|
||||
func buildWeekRangeLabelCN(weekFrom int, weekTo int, weekFilter []int) string {
|
||||
if len(weekFilter) > 0 {
|
||||
return formatScheduleWeekListCN(weekFilter)
|
||||
}
|
||||
if weekFrom > 0 && weekTo > 0 {
|
||||
if weekFrom == weekTo {
|
||||
return fmt.Sprintf("第%d周", weekFrom)
|
||||
}
|
||||
return fmt.Sprintf("第%d-%d周", weekFrom, weekTo)
|
||||
}
|
||||
return "全部周次"
|
||||
}
|
||||
|
||||
func formatBoolLabelCN(value bool) string {
|
||||
if value {
|
||||
return "是"
|
||||
}
|
||||
return "否"
|
||||
}
|
||||
|
||||
func formatWeekdayListCN(days []int) string {
|
||||
if len(days) == 0 {
|
||||
return "不限星期"
|
||||
}
|
||||
parts := make([]string, 0, len(days))
|
||||
for _, day := range days {
|
||||
parts = append(parts, formatScheduleWeekdayCN(day))
|
||||
}
|
||||
return strings.Join(parts, "、")
|
||||
}
|
||||
|
||||
func formatScheduleTaskSourceCN(task schedule.ScheduleTask) string {
|
||||
switch strings.TrimSpace(task.Source) {
|
||||
case "event":
|
||||
if isCourseScheduleTaskForRead(task) {
|
||||
return "课程表"
|
||||
}
|
||||
return "日程事件"
|
||||
case "task_item":
|
||||
return "任务项"
|
||||
default:
|
||||
return fallbackText(task.Source, "未知来源")
|
||||
}
|
||||
}
|
||||
|
||||
func formatScheduleTaskSlotsBriefCN(state *schedule.ScheduleState, slots []schedule.TaskSlot) string {
|
||||
if len(slots) == 0 {
|
||||
return "尚未落位"
|
||||
}
|
||||
parts := make([]string, 0, len(slots))
|
||||
for _, slot := range cloneAndSortTaskSlots(slots) {
|
||||
parts = append(parts, formatScheduleDaySlotCN(state, slot.Day, slot.SlotStart, slot.SlotEnd))
|
||||
}
|
||||
return strings.Join(parts, ";")
|
||||
}
|
||||
|
||||
func countScheduleDayOccupiedForRead(state *schedule.ScheduleState, day int) int {
|
||||
if state == nil {
|
||||
return 0
|
||||
}
|
||||
occupied := 0
|
||||
for i := range state.Tasks {
|
||||
task := state.Tasks[i]
|
||||
if task.EmbedHost != nil {
|
||||
continue
|
||||
}
|
||||
for _, slot := range task.Slots {
|
||||
if slot.Day == day {
|
||||
occupied += slot.SlotEnd - slot.SlotStart + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return occupied
|
||||
}
|
||||
|
||||
func countScheduleDayTaskOccupiedForRead(state *schedule.ScheduleState, day int) int {
|
||||
if state == nil {
|
||||
return 0
|
||||
}
|
||||
occupied := 0
|
||||
for i := range state.Tasks {
|
||||
task := state.Tasks[i]
|
||||
if isCourseScheduleTaskForRead(task) || task.EmbedHost != nil {
|
||||
continue
|
||||
}
|
||||
for _, slot := range task.Slots {
|
||||
if slot.Day == day {
|
||||
occupied += slot.SlotEnd - slot.SlotStart + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return occupied
|
||||
}
|
||||
|
||||
type taskOnDay struct {
|
||||
Task *schedule.ScheduleTask
|
||||
SlotStart int
|
||||
SlotEnd int
|
||||
}
|
||||
|
||||
type freeRange struct {
|
||||
Day int
|
||||
SlotStart int
|
||||
SlotEnd int
|
||||
}
|
||||
|
||||
func listScheduleTasksOnDayForRead(state *schedule.ScheduleState, day int, includeCourse bool) []taskOnDay {
|
||||
if state == nil {
|
||||
return nil
|
||||
}
|
||||
items := make([]taskOnDay, 0)
|
||||
for i := range state.Tasks {
|
||||
task := &state.Tasks[i]
|
||||
if !includeCourse && isCourseScheduleTaskForRead(*task) {
|
||||
continue
|
||||
}
|
||||
for _, slot := range task.Slots {
|
||||
if slot.Day != day {
|
||||
continue
|
||||
}
|
||||
items = append(items, taskOnDay{
|
||||
Task: task,
|
||||
SlotStart: slot.SlotStart,
|
||||
SlotEnd: slot.SlotEnd,
|
||||
})
|
||||
}
|
||||
}
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
if items[i].SlotStart != items[j].SlotStart {
|
||||
return items[i].SlotStart < items[j].SlotStart
|
||||
}
|
||||
if items[i].SlotEnd != items[j].SlotEnd {
|
||||
return items[i].SlotEnd < items[j].SlotEnd
|
||||
}
|
||||
return items[i].Task.StateID < items[j].Task.StateID
|
||||
})
|
||||
return items
|
||||
}
|
||||
|
||||
func listScheduleTasksInRangeForRead(state *schedule.ScheduleState, day int, start int, end int, includeCourse bool) []taskOnDay {
|
||||
items := listScheduleTasksOnDayForRead(state, day, includeCourse)
|
||||
filtered := make([]taskOnDay, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item.SlotStart <= end && item.SlotEnd >= start {
|
||||
filtered = append(filtered, item)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func findScheduleFreeRangesOnDayForRead(state *schedule.ScheduleState, day int) []freeRange {
|
||||
if state == nil {
|
||||
return nil
|
||||
}
|
||||
occupied := make([]bool, 13)
|
||||
for i := range state.Tasks {
|
||||
task := state.Tasks[i]
|
||||
if task.EmbedHost != nil {
|
||||
continue
|
||||
}
|
||||
for _, slot := range task.Slots {
|
||||
if slot.Day != day {
|
||||
continue
|
||||
}
|
||||
for section := slot.SlotStart; section <= slot.SlotEnd; section++ {
|
||||
if section >= 1 && section <= 12 {
|
||||
occupied[section] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ranges := make([]freeRange, 0)
|
||||
start := 0
|
||||
for section := 1; section <= 12; section++ {
|
||||
if !occupied[section] {
|
||||
if start == 0 {
|
||||
start = section
|
||||
}
|
||||
continue
|
||||
}
|
||||
if start > 0 {
|
||||
ranges = append(ranges, freeRange{Day: day, SlotStart: start, SlotEnd: section - 1})
|
||||
start = 0
|
||||
}
|
||||
}
|
||||
if start > 0 {
|
||||
ranges = append(ranges, freeRange{Day: day, SlotStart: start, SlotEnd: 12})
|
||||
}
|
||||
return ranges
|
||||
}
|
||||
|
||||
func findScheduleHostTaskBySlotForRead(state *schedule.ScheduleState, day int, section int) *schedule.ScheduleTask {
|
||||
if state == nil {
|
||||
return nil
|
||||
}
|
||||
for i := range state.Tasks {
|
||||
task := &state.Tasks[i]
|
||||
if task.EmbedHost != nil {
|
||||
continue
|
||||
}
|
||||
for _, slot := range task.Slots {
|
||||
if slot.Day == day && section >= slot.SlotStart && section <= slot.SlotEnd {
|
||||
return task
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isCourseScheduleTaskForRead(task schedule.ScheduleTask) bool {
|
||||
if strings.TrimSpace(task.Source) != "event" {
|
||||
return false
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(task.EventType), "course") {
|
||||
return true
|
||||
}
|
||||
return strings.TrimSpace(task.Category) == "课程"
|
||||
}
|
||||
|
||||
func cloneAndSortTaskSlots(slots []schedule.TaskSlot) []schedule.TaskSlot {
|
||||
if len(slots) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]schedule.TaskSlot, len(slots))
|
||||
copy(out, slots)
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
if out[i].Day != out[j].Day {
|
||||
return out[i].Day < out[j].Day
|
||||
}
|
||||
if out[i].SlotStart != out[j].SlotStart {
|
||||
return out[i].SlotStart < out[j].SlotStart
|
||||
}
|
||||
return out[i].SlotEnd < out[j].SlotEnd
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func fallbackText(text string, fallback string) string {
|
||||
if strings.TrimSpace(text) == "" {
|
||||
return strings.TrimSpace(fallback)
|
||||
}
|
||||
return strings.TrimSpace(text)
|
||||
}
|
||||
|
||||
func parseObservationJSON(text string) (map[string]any, bool) {
|
||||
trimmed := strings.TrimSpace(text)
|
||||
if trimmed == "" || !strings.HasPrefix(trimmed, "{") {
|
||||
return nil, false
|
||||
}
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal([]byte(trimmed), &payload); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return payload, true
|
||||
}
|
||||
|
||||
func readStringFromMap(payload map[string]any, keys ...string) (string, bool) {
|
||||
if len(payload) == 0 {
|
||||
return "", false
|
||||
}
|
||||
for _, key := range keys {
|
||||
raw, ok := payload[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
value, ok := raw.(string)
|
||||
if ok {
|
||||
return value, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func cloneSectionList(sections []map[string]any) []map[string]any {
|
||||
if len(sections) == 0 {
|
||||
return make([]map[string]any, 0)
|
||||
}
|
||||
out := make([]map[string]any, 0, len(sections))
|
||||
for _, section := range sections {
|
||||
out = append(out, cloneAnyMap(section))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneAnyMap(input map[string]any) map[string]any {
|
||||
if len(input) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]any, len(input))
|
||||
for key, value := range input {
|
||||
out[key] = cloneAnyValue(value)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneAnyValue(value any) any {
|
||||
switch current := value.(type) {
|
||||
case map[string]any:
|
||||
return cloneAnyMap(current)
|
||||
case []map[string]any:
|
||||
out := make([]map[string]any, 0, len(current))
|
||||
for _, item := range current {
|
||||
out = append(out, cloneAnyMap(item))
|
||||
}
|
||||
return out
|
||||
case []any:
|
||||
out := make([]any, 0, len(current))
|
||||
for _, item := range current {
|
||||
out = append(out, cloneAnyValue(item))
|
||||
}
|
||||
return out
|
||||
case []string:
|
||||
out := make([]string, len(current))
|
||||
copy(out, current)
|
||||
return out
|
||||
case []int:
|
||||
out := make([]int, len(current))
|
||||
copy(out, current)
|
||||
return out
|
||||
default:
|
||||
return current
|
||||
}
|
||||
}
|
||||
|
||||
func maxInt(values ...int) int {
|
||||
if len(values) == 0 {
|
||||
return 0
|
||||
}
|
||||
best := values[0]
|
||||
for _, value := range values[1:] {
|
||||
if value > best {
|
||||
best = value
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
func toInt(value any) (int, bool) {
|
||||
switch current := value.(type) {
|
||||
case int:
|
||||
return current, true
|
||||
case int32:
|
||||
return int(current), true
|
||||
case int64:
|
||||
return int(current), true
|
||||
case float64:
|
||||
return int(current), true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func optionalIntValue(value *int) any {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return *value
|
||||
}
|
||||
392
backend/newAgent/tools/schedule_read/overview_queue.go
Normal file
392
backend/newAgent/tools/schedule_read/overview_queue.go
Normal file
@@ -0,0 +1,392 @@
|
||||
package schedule_read
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||||
)
|
||||
|
||||
// BuildOverviewView 构造 get_overview 的纯展示视图。
|
||||
func BuildOverviewView(input OverviewViewInput) ReadResultView {
|
||||
if input.State == nil {
|
||||
return BuildFailureView(BuildFailureViewInput{
|
||||
ToolName: "get_overview",
|
||||
Observation: input.Observation,
|
||||
ArgFields: input.ArgFields,
|
||||
})
|
||||
}
|
||||
|
||||
totalSlots := input.State.Window.TotalDays * 12
|
||||
totalOccupied := 0
|
||||
taskExistingCount := 0
|
||||
taskSuggestedCount := 0
|
||||
taskPendingCount := 0
|
||||
courseExistingCount := 0
|
||||
|
||||
for i := range input.State.Tasks {
|
||||
task := input.State.Tasks[i]
|
||||
if task.EmbedHost == nil {
|
||||
for _, slot := range task.Slots {
|
||||
totalOccupied += slot.SlotEnd - slot.SlotStart + 1
|
||||
}
|
||||
}
|
||||
if isCourseScheduleTaskForRead(task) {
|
||||
if schedule.IsExistingTask(task) {
|
||||
courseExistingCount++
|
||||
}
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case schedule.IsPendingTask(task):
|
||||
taskPendingCount++
|
||||
case schedule.IsSuggestedTask(task):
|
||||
taskSuggestedCount++
|
||||
default:
|
||||
taskExistingCount++
|
||||
}
|
||||
}
|
||||
|
||||
dailyItems := make([]ItemView, 0, input.State.Window.TotalDays)
|
||||
for day := 1; day <= input.State.Window.TotalDays; day++ {
|
||||
totalDayOccupied := countScheduleDayOccupiedForRead(input.State, day)
|
||||
taskDayOccupied := countScheduleDayTaskOccupiedForRead(input.State, day)
|
||||
taskEntries := listScheduleTasksOnDayForRead(input.State, day, false)
|
||||
detailLines := make([]string, 0, len(taskEntries))
|
||||
for _, entry := range taskEntries {
|
||||
detailLines = append(detailLines, fmt.Sprintf(
|
||||
"[%d]%s,%s,%s",
|
||||
entry.Task.StateID,
|
||||
fallbackText(entry.Task.Name, "未命名任务"),
|
||||
formatScheduleTaskStatusCN(*entry.Task),
|
||||
formatScheduleSlotRangeCN(entry.SlotStart, entry.SlotEnd),
|
||||
))
|
||||
}
|
||||
if len(detailLines) == 0 {
|
||||
detailLines = append(detailLines, "当天没有任务明细。")
|
||||
}
|
||||
dailyItems = append(dailyItems, BuildItem(
|
||||
formatScheduleDayCN(input.State, day),
|
||||
fmt.Sprintf("总占用 %d/12 节,任务占用 %d/12 节", totalDayOccupied, taskDayOccupied),
|
||||
[]string{fmt.Sprintf("任务 %d 项", len(taskEntries))},
|
||||
detailLines,
|
||||
map[string]any{"day": day},
|
||||
))
|
||||
}
|
||||
|
||||
taskItems := make([]ItemView, 0, len(input.State.Tasks))
|
||||
for i := range input.State.Tasks {
|
||||
task := input.State.Tasks[i]
|
||||
if isCourseScheduleTaskForRead(task) {
|
||||
continue
|
||||
}
|
||||
detailLines := []string{
|
||||
"时段:" + formatScheduleTaskSlotsBriefCN(input.State, task.Slots),
|
||||
"来源:" + formatScheduleTaskSourceCN(task),
|
||||
}
|
||||
if task.TaskClassID > 0 {
|
||||
detailLines = append(detailLines, fmt.Sprintf("任务类 ID:%d", task.TaskClassID))
|
||||
}
|
||||
taskItems = append(taskItems, BuildItem(
|
||||
fmt.Sprintf("[%d]%s", task.StateID, fallbackText(task.Name, "未命名任务")),
|
||||
fmt.Sprintf("%s,%s", fallbackText(task.Category, "未分类"), formatScheduleTaskStatusCN(task)),
|
||||
[]string{formatScheduleTaskStatusCN(task)},
|
||||
detailLines,
|
||||
map[string]any{
|
||||
"task_id": task.StateID,
|
||||
"task_class_id": task.TaskClassID,
|
||||
"status": task.Status,
|
||||
},
|
||||
))
|
||||
}
|
||||
sort.Slice(taskItems, func(i, j int) bool {
|
||||
leftID, _ := toInt(taskItems[i].Meta["task_id"])
|
||||
rightID, _ := toInt(taskItems[j].Meta["task_id"])
|
||||
return leftID < rightID
|
||||
})
|
||||
|
||||
taskClassItems := make([]ItemView, 0, len(input.State.TaskClasses))
|
||||
for _, meta := range input.State.TaskClasses {
|
||||
detailLines := []string{
|
||||
fmt.Sprintf("排程策略:%s", formatTaskClassStrategyCN(meta.Strategy)),
|
||||
fmt.Sprintf("总预算:%d 节", meta.TotalSlots),
|
||||
fmt.Sprintf("允许嵌入水课:%s", formatBoolLabelCN(meta.AllowFillerCourse)),
|
||||
}
|
||||
if len(meta.ExcludedSlots) > 0 {
|
||||
detailLines = append(detailLines, "排除节次:"+formatScheduleSectionListCN(meta.ExcludedSlots))
|
||||
}
|
||||
if len(meta.ExcludedDaysOfWeek) > 0 {
|
||||
detailLines = append(detailLines, "排除星期:"+formatWeekdayListCN(meta.ExcludedDaysOfWeek))
|
||||
}
|
||||
taskClassItems = append(taskClassItems, BuildItem(
|
||||
fallbackText(meta.Name, "未命名任务类"),
|
||||
formatTaskClassStrategyCN(meta.Strategy),
|
||||
nil,
|
||||
detailLines,
|
||||
map[string]any{
|
||||
"task_class_id": meta.ID,
|
||||
"strategy": meta.Strategy,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
totalFree := totalSlots - totalOccupied
|
||||
if totalFree < 0 {
|
||||
totalFree = 0
|
||||
}
|
||||
sections := []map[string]any{
|
||||
BuildKVSection("窗口概况", []KVField{
|
||||
BuildKVField("规划天数", fmt.Sprintf("%d 天", input.State.Window.TotalDays)),
|
||||
BuildKVField("总时段", fmt.Sprintf("%d 节", totalSlots)),
|
||||
BuildKVField("已占用", fmt.Sprintf("%d 节", totalOccupied)),
|
||||
BuildKVField("空闲", fmt.Sprintf("%d 节", totalFree)),
|
||||
BuildKVField("课程占位", fmt.Sprintf("%d 项", courseExistingCount)),
|
||||
BuildKVField("已安排任务", fmt.Sprintf("%d 项", taskExistingCount)),
|
||||
BuildKVField("已预排任务", fmt.Sprintf("%d 项", taskSuggestedCount)),
|
||||
BuildKVField("待安排任务", fmt.Sprintf("%d 项", taskPendingCount)),
|
||||
}),
|
||||
BuildItemsSection("每日概况", dailyItems),
|
||||
BuildItemsSection("任务清单", taskItems),
|
||||
}
|
||||
if len(taskClassItems) > 0 {
|
||||
sections = append(sections, BuildItemsSection("任务类约束", taskClassItems))
|
||||
}
|
||||
appendSectionIfPresent(§ions, BuildArgsSection("查询条件", input.ArgFields))
|
||||
|
||||
return BuildResultView(BuildResultViewInput{
|
||||
Status: StatusDone,
|
||||
Title: "当前排程总览",
|
||||
Subtitle: fmt.Sprintf("%d 天窗口,已占用 %d/%d 节,待安排 %d 项。", input.State.Window.TotalDays, totalOccupied, totalSlots, taskPendingCount),
|
||||
Metrics: []MetricField{
|
||||
BuildMetric("已占用", fmt.Sprintf("%d 节", totalOccupied)),
|
||||
BuildMetric("空闲", fmt.Sprintf("%d 节", totalFree)),
|
||||
BuildMetric("待安排", fmt.Sprintf("%d 项", taskPendingCount)),
|
||||
BuildMetric("课程占位", fmt.Sprintf("%d 项", courseExistingCount)),
|
||||
},
|
||||
Items: dailyItems,
|
||||
Sections: sections,
|
||||
Observation: input.Observation,
|
||||
MachinePayload: map[string]any{
|
||||
"total_days": input.State.Window.TotalDays,
|
||||
"total_slots": totalSlots,
|
||||
"total_occupied": totalOccupied,
|
||||
"task_existing_count": taskExistingCount,
|
||||
"task_suggested_count": taskSuggestedCount,
|
||||
"task_pending_count": taskPendingCount,
|
||||
"course_existing_count": courseExistingCount,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// BuildQueueStatusView 构造 queue_status 的纯展示视图。
|
||||
func BuildQueueStatusView(input QueueStatusViewInput) ReadResultView {
|
||||
if input.State == nil {
|
||||
return BuildFailureView(BuildFailureViewInput{
|
||||
ToolName: "queue_status",
|
||||
Observation: input.Observation,
|
||||
ArgFields: input.ArgFields,
|
||||
})
|
||||
}
|
||||
|
||||
payload, machinePayload, ok := DecodeQueueStatusPayload(input.Observation)
|
||||
if !ok {
|
||||
return BuildFailureView(BuildFailureViewInput{
|
||||
ToolName: "queue_status",
|
||||
Observation: input.Observation,
|
||||
ArgFields: input.ArgFields,
|
||||
})
|
||||
}
|
||||
|
||||
items := make([]ItemView, 0, 1+len(payload.NextTaskIDs))
|
||||
sections := make([]map[string]any, 0, 4)
|
||||
if payload.Current != nil {
|
||||
currentItem := buildQueueCurrentItem(input.State, payload.Current, payload.CurrentAttempt)
|
||||
items = append(items, currentItem)
|
||||
sections = append(sections, BuildItemsSection("当前处理", []ItemView{currentItem}))
|
||||
}
|
||||
|
||||
nextItems := make([]ItemView, 0, len(payload.NextTaskIDs))
|
||||
for index, taskID := range payload.NextTaskIDs {
|
||||
nextItems = append(nextItems, buildQueuePendingItem(input.State, taskID, index))
|
||||
}
|
||||
items = append(items, nextItems...)
|
||||
if len(nextItems) > 0 {
|
||||
sections = append(sections, BuildItemsSection("待处理队列", nextItems))
|
||||
}
|
||||
|
||||
sections = append(sections, BuildKVSection("运行概况", []KVField{
|
||||
BuildKVField("待处理", fmt.Sprintf("%d 项", payload.PendingCount)),
|
||||
BuildKVField("已完成", fmt.Sprintf("%d 项", payload.CompletedCount)),
|
||||
BuildKVField("已跳过", fmt.Sprintf("%d 项", payload.SkippedCount)),
|
||||
BuildKVField("当前任务", resolveTaskQueueLabelByID(input.State, payload.CurrentTaskID)),
|
||||
}))
|
||||
if strings.TrimSpace(payload.LastError) != "" {
|
||||
sections = append(sections, BuildCalloutSection(
|
||||
"最近一次失败",
|
||||
"队列中保留了上一轮 apply 的失败原因。",
|
||||
"warning",
|
||||
[]string{strings.TrimSpace(payload.LastError)},
|
||||
))
|
||||
}
|
||||
appendSectionIfPresent(§ions, BuildArgsSection("查询条件", input.ArgFields))
|
||||
|
||||
title := fmt.Sprintf("队列待处理 %d 项", payload.PendingCount)
|
||||
if payload.PendingCount == 0 && payload.CurrentTaskID == 0 {
|
||||
title = "当前队列为空"
|
||||
}
|
||||
return BuildResultView(BuildResultViewInput{
|
||||
Status: StatusDone,
|
||||
Title: title,
|
||||
Subtitle: buildQueueStatusSubtitle(payload),
|
||||
Metrics: []MetricField{BuildMetric("待处理", fmt.Sprintf("%d 项", payload.PendingCount)), BuildMetric("已完成", fmt.Sprintf("%d 项", payload.CompletedCount)), BuildMetric("已跳过", fmt.Sprintf("%d 项", payload.SkippedCount))},
|
||||
Items: items,
|
||||
Sections: sections,
|
||||
Observation: input.Observation,
|
||||
MachinePayload: machinePayload,
|
||||
})
|
||||
}
|
||||
|
||||
// DecodeQueueStatusPayload 解析 queue_status 的 JSON observation。
|
||||
func DecodeQueueStatusPayload(observation string) (QueueStatusPayload, map[string]any, bool) {
|
||||
var payload QueueStatusPayload
|
||||
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 buildQueueStatusSubtitle(payload QueueStatusPayload) string {
|
||||
if payload.Current != nil {
|
||||
return fmt.Sprintf(
|
||||
"当前处理:[%d]%s,第 %d 次尝试。",
|
||||
payload.Current.TaskID,
|
||||
fallbackText(payload.Current.Name, "未命名任务"),
|
||||
maxInt(payload.CurrentAttempt, 1),
|
||||
)
|
||||
}
|
||||
if payload.PendingCount > 0 {
|
||||
return fmt.Sprintf("队列里还有 %d 项待处理,尚未弹出当前任务。", payload.PendingCount)
|
||||
}
|
||||
return "没有待处理任务,也没有正在处理的任务。"
|
||||
}
|
||||
|
||||
// 1. 这里没有强抽成通用 task builder,因为 queue_status 既要兼容 payload 快照,
|
||||
// 2. 也要兼容通过 state 按 task_id 兜底,两类输入结构不同,硬抽反而会增加适配噪音。
|
||||
func buildQueueCurrentItem(state *schedule.ScheduleState, payload *QueueTaskSnapshot, attempt int) ItemView {
|
||||
detailLines := buildQueueCurrentDetailLines(state, payload)
|
||||
detailLines = append(detailLines, fmt.Sprintf("当前尝试:第 %d 次", maxInt(attempt, 1)))
|
||||
return BuildItem(
|
||||
fmt.Sprintf("[%d]%s", payload.TaskID, fallbackText(payload.Name, "未命名任务")),
|
||||
buildQueueCurrentSubtitle(payload),
|
||||
[]string{"当前处理"},
|
||||
detailLines,
|
||||
map[string]any{
|
||||
"task_id": payload.TaskID,
|
||||
"status": payload.Status,
|
||||
"task_class_id": payload.TaskClassID,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func buildQueuePendingItem(state *schedule.ScheduleState, taskID int, index int) ItemView {
|
||||
task := state.TaskByStateID(taskID)
|
||||
if task == nil {
|
||||
return BuildItem(
|
||||
fmt.Sprintf("[%d]任务", taskID),
|
||||
fmt.Sprintf("队列第 %d 位", index+1),
|
||||
[]string{"待处理"},
|
||||
[]string{"当前状态快照中未找到更多任务详情。"},
|
||||
map[string]any{"task_id": taskID, "queue_index": index},
|
||||
)
|
||||
}
|
||||
return BuildItem(
|
||||
fmt.Sprintf("[%d]%s", task.StateID, fallbackText(task.Name, "未命名任务")),
|
||||
buildQueueTaskSubtitle(task),
|
||||
buildQueueTaskTags(task, false),
|
||||
buildQueueTaskDetailLines(state, task),
|
||||
map[string]any{
|
||||
"task_id": task.StateID,
|
||||
"queue_index": index,
|
||||
"status": task.Status,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func buildQueueTaskSubtitle(task *schedule.ScheduleTask) string {
|
||||
if task == nil {
|
||||
return "待处理"
|
||||
}
|
||||
return fmt.Sprintf("%s,%s", fallbackText(task.Category, "未分类"), formatScheduleTaskStatusCN(*task))
|
||||
}
|
||||
|
||||
func buildQueueTaskTags(task *schedule.ScheduleTask, isCurrent bool) []string {
|
||||
tags := make([]string, 0, 2)
|
||||
if isCurrent {
|
||||
tags = append(tags, "当前处理")
|
||||
} else {
|
||||
tags = append(tags, "待处理")
|
||||
}
|
||||
if task != nil && task.Duration > 0 {
|
||||
tags = append(tags, fmt.Sprintf("%d 节", task.Duration))
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
func buildQueueTaskDetailLines(state *schedule.ScheduleState, task *schedule.ScheduleTask) []string {
|
||||
if task == nil {
|
||||
return nil
|
||||
}
|
||||
lines := []string{"时段:" + formatScheduleTaskSlotsBriefCN(state, task.Slots)}
|
||||
if task.TaskClassID > 0 {
|
||||
lines = append(lines, fmt.Sprintf("任务类 ID:%d", task.TaskClassID))
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func buildQueueCurrentSubtitle(payload *QueueTaskSnapshot) string {
|
||||
if payload == nil {
|
||||
return "当前处理"
|
||||
}
|
||||
return fmt.Sprintf("%s,%s", fallbackText(payload.Category, "未分类"), formatTargetTaskStatusCN(payload.Status))
|
||||
}
|
||||
|
||||
func buildQueueCurrentDetailLines(state *schedule.ScheduleState, payload *QueueTaskSnapshot) []string {
|
||||
if payload == nil {
|
||||
return nil
|
||||
}
|
||||
lines := make([]string, 0, 3)
|
||||
if len(payload.Slots) > 0 {
|
||||
slotParts := make([]string, 0, len(payload.Slots))
|
||||
for _, slot := range payload.Slots {
|
||||
slotParts = append(slotParts, formatScheduleDaySlotCN(state, slot.Day, slot.SlotStart, slot.SlotEnd))
|
||||
}
|
||||
lines = append(lines, "时段:"+strings.Join(slotParts, ";"))
|
||||
} else {
|
||||
lines = append(lines, "当前还未落位。")
|
||||
}
|
||||
if payload.TaskClassID > 0 {
|
||||
lines = append(lines, fmt.Sprintf("任务类 ID:%d", payload.TaskClassID))
|
||||
}
|
||||
if payload.Duration > 0 {
|
||||
lines = append(lines, fmt.Sprintf("时长需求:%d 节", payload.Duration))
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func formatTaskClassStrategyCN(strategy string) string {
|
||||
switch strings.TrimSpace(strategy) {
|
||||
case "steady":
|
||||
return "均匀分布"
|
||||
case "rapid":
|
||||
return "集中突击"
|
||||
default:
|
||||
return fallbackText(strategy, "默认")
|
||||
}
|
||||
}
|
||||
427
backend/newAgent/tools/schedule_read/slots.go
Normal file
427
backend/newAgent/tools/schedule_read/slots.go
Normal file
@@ -0,0 +1,427 @@
|
||||
package schedule_read
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/LoveLosita/smartflow/backend/newAgent/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(§ions, 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(§ions, 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(§ions, 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, "未分类"),
|
||||
)
|
||||
}
|
||||
301
backend/newAgent/tools/schedule_read/tasks.go
Normal file
301
backend/newAgent/tools/schedule_read/tasks.go
Normal file
@@ -0,0 +1,301 @@
|
||||
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
|
||||
}
|
||||
312
backend/newAgent/tools/schedule_read/types.go
Normal file
312
backend/newAgent/tools/schedule_read/types.go
Normal file
@@ -0,0 +1,312 @@
|
||||
package schedule_read
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||||
)
|
||||
|
||||
const (
|
||||
// ViewTypeReadResult 固定为第二批 read 结果卡片的前端识别类型。
|
||||
ViewTypeReadResult = "schedule.read_result"
|
||||
|
||||
// ViewVersionReadResult 固定为当前 read 结果结构版本。
|
||||
ViewVersionReadResult = 1
|
||||
|
||||
// 这里不依赖父包状态常量,避免子包反向 import tools 形成循环依赖。
|
||||
StatusDone = "done"
|
||||
StatusFailed = "failed"
|
||||
StatusBlocked = "blocked"
|
||||
)
|
||||
|
||||
// ReadResultView 是子包暴露给父包 adapter 的纯展示结构。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责承载 view_type / version / collapsed / expanded 四段展示数据。
|
||||
// 2. 不负责 ToolExecutionResult、SSE、registry 等父包协议。
|
||||
// 3. collapsed / expanded 继续保留 map 形态,方便父包直接桥接到现有展示协议。
|
||||
type ReadResultView struct {
|
||||
ViewType string `json:"view_type"`
|
||||
Version int `json:"version"`
|
||||
Collapsed map[string]any `json:"collapsed"`
|
||||
Expanded map[string]any `json:"expanded"`
|
||||
}
|
||||
|
||||
// CollapsedView 表示折叠态卡片数据。
|
||||
type CollapsedView struct {
|
||||
Title string `json:"title"`
|
||||
Subtitle string `json:"subtitle"`
|
||||
Status string `json:"status"`
|
||||
StatusLabel string `json:"status_label"`
|
||||
Metrics []MetricField `json:"metrics"`
|
||||
}
|
||||
|
||||
// ExpandedView 表示展开态卡片数据。
|
||||
type ExpandedView struct {
|
||||
Items []ItemView `json:"items"`
|
||||
Sections []map[string]any `json:"sections"`
|
||||
RawText string `json:"raw_text"`
|
||||
MachinePayload map[string]any `json:"machine_payload,omitempty"`
|
||||
}
|
||||
|
||||
// MetricField 是 collapsed.metrics 的轻量键值结构。
|
||||
type MetricField struct {
|
||||
Label string `json:"label"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// KVField 是展开态 kv section 的轻量键值结构。
|
||||
type KVField struct {
|
||||
Label string `json:"label"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// ItemView 是展开态 items 的通用结构。
|
||||
type ItemView struct {
|
||||
Title string `json:"title"`
|
||||
Subtitle string `json:"subtitle"`
|
||||
Tags []string `json:"tags"`
|
||||
DetailLines []string `json:"detail_lines"`
|
||||
Meta map[string]any `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
// BuildResultViewInput 是通用 read 结果视图 builder 的输入。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责承载已经计算好的标题、副标题、指标、列表、分区。
|
||||
// 2. 不负责判断工具是否执行成功;调用方需要在进入这里前确定 status。
|
||||
// 3. observation 会原样写入 raw_text,不能在这里改写给 LLM 的观察文本语义。
|
||||
type BuildResultViewInput struct {
|
||||
Status string
|
||||
Title string
|
||||
Subtitle string
|
||||
Metrics []MetricField
|
||||
Items []ItemView
|
||||
Sections []map[string]any
|
||||
Observation string
|
||||
MachinePayload map[string]any
|
||||
}
|
||||
|
||||
// BuildFailureViewInput 是通用失败视图 builder 的输入。
|
||||
type BuildFailureViewInput struct {
|
||||
ToolName string
|
||||
Status string
|
||||
Title string
|
||||
Subtitle string
|
||||
Observation string
|
||||
ArgFields []KVField
|
||||
}
|
||||
|
||||
// AvailableSlotsViewInput 是 query_available_slots 视图构造输入。
|
||||
type AvailableSlotsViewInput struct {
|
||||
State *schedule.ScheduleState
|
||||
Observation string
|
||||
ArgFields []KVField
|
||||
}
|
||||
|
||||
// RangeViewInput 是 query_range 统一入口输入。
|
||||
type RangeViewInput struct {
|
||||
State *schedule.ScheduleState
|
||||
Observation string
|
||||
Day int
|
||||
SlotStart *int
|
||||
SlotEnd *int
|
||||
ArgFields []KVField
|
||||
}
|
||||
|
||||
// RangeFullDayViewInput 是 query_range 整天模式输入。
|
||||
type RangeFullDayViewInput struct {
|
||||
State *schedule.ScheduleState
|
||||
Observation string
|
||||
Day int
|
||||
ArgFields []KVField
|
||||
}
|
||||
|
||||
// RangeSpecificViewInput 是 query_range 指定时段模式输入。
|
||||
type RangeSpecificViewInput struct {
|
||||
State *schedule.ScheduleState
|
||||
Observation string
|
||||
Day int
|
||||
SlotStart int
|
||||
SlotEnd int
|
||||
ArgFields []KVField
|
||||
}
|
||||
|
||||
// TargetTasksViewInput 是 query_target_tasks 视图构造输入。
|
||||
type TargetTasksViewInput struct {
|
||||
State *schedule.ScheduleState
|
||||
Observation string
|
||||
ArgFields []KVField
|
||||
}
|
||||
|
||||
// TaskInfoViewInput 是 get_task_info 视图构造输入。
|
||||
type TaskInfoViewInput struct {
|
||||
State *schedule.ScheduleState
|
||||
Observation string
|
||||
TaskID int
|
||||
ArgFields []KVField
|
||||
}
|
||||
|
||||
// OverviewViewInput 是 get_overview 视图构造输入。
|
||||
type OverviewViewInput struct {
|
||||
State *schedule.ScheduleState
|
||||
Observation string
|
||||
ArgFields []KVField
|
||||
}
|
||||
|
||||
// QueueStatusViewInput 是 queue_status 视图构造输入。
|
||||
type QueueStatusViewInput struct {
|
||||
State *schedule.ScheduleState
|
||||
Observation string
|
||||
ArgFields []KVField
|
||||
}
|
||||
|
||||
// AvailableSlotsPayload 是 query_available_slots 的结构化结果。
|
||||
type AvailableSlotsPayload struct {
|
||||
Tool string `json:"tool"`
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error"`
|
||||
Count int `json:"count"`
|
||||
StrictCount int `json:"strict_count"`
|
||||
EmbeddedCount int `json:"embedded_count"`
|
||||
FallbackUsed bool `json:"fallback_used"`
|
||||
DayScope string `json:"day_scope"`
|
||||
DayOfWeek []int `json:"day_of_week"`
|
||||
WeekFilter []int `json:"week_filter"`
|
||||
WeekFrom int `json:"week_from"`
|
||||
WeekTo int `json:"week_to"`
|
||||
Span int `json:"span"`
|
||||
AllowEmbed bool `json:"allow_embed"`
|
||||
ExcludeSections []int `json:"exclude_sections"`
|
||||
Slots []AvailableSlotRecord `json:"slots"`
|
||||
}
|
||||
|
||||
// AvailableSlotRecord 是 query_available_slots 单条时段记录。
|
||||
type AvailableSlotRecord struct {
|
||||
Day int `json:"day"`
|
||||
Week int `json:"week"`
|
||||
DayOfWeek int `json:"day_of_week"`
|
||||
SlotStart int `json:"slot_start"`
|
||||
SlotEnd int `json:"slot_end"`
|
||||
SlotType string `json:"slot_type"`
|
||||
}
|
||||
|
||||
// TargetTasksPayload 是 query_target_tasks 的结构化结果。
|
||||
type TargetTasksPayload struct {
|
||||
Tool string `json:"tool"`
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error"`
|
||||
Count int `json:"count"`
|
||||
Status string `json:"status"`
|
||||
DayScope string `json:"day_scope"`
|
||||
DayOfWeek []int `json:"day_of_week"`
|
||||
WeekFilter []int `json:"week_filter"`
|
||||
WeekFrom int `json:"week_from"`
|
||||
WeekTo int `json:"week_to"`
|
||||
Enqueue bool `json:"enqueue"`
|
||||
Enqueued int `json:"enqueued"`
|
||||
Queue *TargetTasksQueueRecord `json:"queue"`
|
||||
Items []TargetTaskRecord `json:"items"`
|
||||
}
|
||||
|
||||
// TargetTasksQueueRecord 是目标任务查询里的队列快照。
|
||||
type TargetTasksQueueRecord struct {
|
||||
PendingCount int `json:"pending_count"`
|
||||
CompletedCount int `json:"completed_count"`
|
||||
SkippedCount int `json:"skipped_count"`
|
||||
CurrentTaskID int `json:"current_task_id"`
|
||||
CurrentAttempt int `json:"current_attempt"`
|
||||
}
|
||||
|
||||
// TargetTaskRecord 是 query_target_tasks 单条任务记录。
|
||||
type TargetTaskRecord struct {
|
||||
TaskID int `json:"task_id"`
|
||||
Name string `json:"name"`
|
||||
Category string `json:"category"`
|
||||
Status string `json:"status"`
|
||||
Duration int `json:"duration"`
|
||||
TaskClassID int `json:"task_class_id"`
|
||||
Slots []TargetTaskSlotInfo `json:"slots"`
|
||||
}
|
||||
|
||||
// TargetTaskSlotInfo 是目标任务时段信息。
|
||||
type TargetTaskSlotInfo struct {
|
||||
Day int `json:"day"`
|
||||
Week int `json:"week"`
|
||||
DayOfWeek int `json:"day_of_week"`
|
||||
SlotStart int `json:"slot_start"`
|
||||
SlotEnd int `json:"slot_end"`
|
||||
}
|
||||
|
||||
// QueueStatusPayload 是 queue_status 的结构化结果。
|
||||
type QueueStatusPayload struct {
|
||||
Tool string `json:"tool"`
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error"`
|
||||
PendingCount int `json:"pending_count"`
|
||||
CompletedCount int `json:"completed_count"`
|
||||
SkippedCount int `json:"skipped_count"`
|
||||
CurrentTaskID int `json:"current_task_id"`
|
||||
CurrentAttempt int `json:"current_attempt"`
|
||||
LastError string `json:"last_error"`
|
||||
NextTaskIDs []int `json:"next_task_ids"`
|
||||
Current *QueueTaskSnapshot `json:"current"`
|
||||
}
|
||||
|
||||
// QueueTaskSnapshot 是 queue_status 当前任务快照。
|
||||
type QueueTaskSnapshot struct {
|
||||
TaskID int `json:"task_id"`
|
||||
Name string `json:"name"`
|
||||
Category string `json:"category"`
|
||||
Status string `json:"status"`
|
||||
Duration int `json:"duration"`
|
||||
TaskClassID int `json:"task_class_id"`
|
||||
Slots []TargetTaskSlotInfo `json:"slots"`
|
||||
}
|
||||
|
||||
func (view CollapsedView) Map() map[string]any {
|
||||
metrics := make([]map[string]any, 0, len(view.Metrics))
|
||||
for _, metric := range view.Metrics {
|
||||
metrics = append(metrics, map[string]any{
|
||||
"label": strings.TrimSpace(metric.Label),
|
||||
"value": strings.TrimSpace(metric.Value),
|
||||
})
|
||||
}
|
||||
return map[string]any{
|
||||
"title": strings.TrimSpace(view.Title),
|
||||
"subtitle": strings.TrimSpace(view.Subtitle),
|
||||
"status": normalizeStatus(view.Status),
|
||||
"status_label": strings.TrimSpace(view.StatusLabel),
|
||||
"metrics": metrics,
|
||||
}
|
||||
}
|
||||
|
||||
func (view ExpandedView) Map() map[string]any {
|
||||
items := make([]map[string]any, 0, len(view.Items))
|
||||
for _, item := range view.Items {
|
||||
items = append(items, item.Map())
|
||||
}
|
||||
|
||||
out := map[string]any{
|
||||
"items": items,
|
||||
"sections": cloneSectionList(view.Sections),
|
||||
"raw_text": view.RawText,
|
||||
}
|
||||
if len(view.MachinePayload) > 0 {
|
||||
out["machine_payload"] = cloneAnyMap(view.MachinePayload)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (view ItemView) Map() map[string]any {
|
||||
item := map[string]any{
|
||||
"title": strings.TrimSpace(view.Title),
|
||||
"subtitle": strings.TrimSpace(view.Subtitle),
|
||||
"tags": normalizeStringSlice(view.Tags),
|
||||
"detail_lines": normalizeStringSlice(view.DetailLines),
|
||||
}
|
||||
if len(view.Meta) > 0 {
|
||||
item["meta"] = cloneAnyMap(view.Meta)
|
||||
}
|
||||
return item
|
||||
}
|
||||
Reference in New Issue
Block a user