Version: 0.9.51.dev.260428
后端: 1. schedule 读工具结果正式切到结构化 `schedule.read_result` 视图——`get_overview` / `query_range` / `query_available_slots` / `query_target_tasks` / `get_task_info` / `queue_status` 新增独立 handler,`ToolExecutionResult` 继续保留 `ObservationText` 给 LLM,但前端展示改走 `result_view` 的 collapsed / expanded 结构,统一输出 metrics / items / sections / machine_payload 2. `execution_result` 补齐第二批读工具参数本地化展示——扩展 `task_ids` / `task_item_ids` / `status` / `category` / `day_scope` / `week_filter` / `slot_types` / `include_pending` / `detail` / `dimensions` 等参数的排序权重、中文标签与展示格式,支持列表 / 布尔 / 周次 / 星期 / 节次等 `argument_view` 渲染 3. ToolRegistry 继续从内联注册收口到专属 handler——schedule 读工具从 `wrapLegacyToolHandler` 切到 `NewXxxToolHandler`,旧 `schedule` 子包里的 observation 生成逻辑暂时保留,当前切流点已落在 `newAgent/tools` 结构化适配层 4. 新增《工具结果结构化交接文档》,明确第二批 read 工具已迁移范围、`schedule.read_result` 协议、当前旧实现保留边界,以及下一轮建议迁移的 `schedule_analysis` 方向
This commit is contained in:
@@ -471,18 +471,28 @@ func buildArgumentFields(toolName string, args map[string]any, state *schedule.S
|
|||||||
|
|
||||||
func argumentDisplayRank(key string) int {
|
func argumentDisplayRank(key string) int {
|
||||||
switch strings.TrimSpace(key) {
|
switch strings.TrimSpace(key) {
|
||||||
case "task_id", "task_a", "task_b":
|
case "task_id", "task_ids", "task_item_id", "task_item_ids", "task_a", "task_b":
|
||||||
return 10
|
return 10
|
||||||
case "day", "new_day":
|
case "status", "category":
|
||||||
return 20
|
return 20
|
||||||
case "slot_start", "new_slot_start":
|
case "day", "new_day", "day_start", "day_end", "day_scope", "day_of_week":
|
||||||
return 30
|
return 30
|
||||||
case "moves":
|
case "week", "week_filter", "week_from", "week_to":
|
||||||
return 40
|
return 40
|
||||||
case "reason":
|
case "slot_start", "new_slot_start", "slot_type", "slot_types", "exclude_sections", "after_section", "before_section", "section_from", "section_to":
|
||||||
return 50
|
return 50
|
||||||
default:
|
case "span", "duration":
|
||||||
|
return 60
|
||||||
|
case "allow_embed", "enqueue", "reset_queue", "detail", "dimensions", "threshold", "include_pending", "hard_categories", "limit":
|
||||||
|
return 70
|
||||||
|
case "moves":
|
||||||
|
return 80
|
||||||
|
case "reason":
|
||||||
|
return 90
|
||||||
|
case "query", "url":
|
||||||
return 100
|
return 100
|
||||||
|
default:
|
||||||
|
return 120
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -517,6 +527,12 @@ func resolveArgumentLabelCN(key string) string {
|
|||||||
switch strings.TrimSpace(key) {
|
switch strings.TrimSpace(key) {
|
||||||
case "task_id":
|
case "task_id":
|
||||||
return "任务"
|
return "任务"
|
||||||
|
case "task_ids":
|
||||||
|
return "任务列表"
|
||||||
|
case "task_item_id":
|
||||||
|
return "任务项"
|
||||||
|
case "task_item_ids":
|
||||||
|
return "任务项列表"
|
||||||
case "task_a":
|
case "task_a":
|
||||||
return "任务A"
|
return "任务A"
|
||||||
case "task_b":
|
case "task_b":
|
||||||
@@ -525,24 +541,79 @@ func resolveArgumentLabelCN(key string) string {
|
|||||||
return "目标日期"
|
return "目标日期"
|
||||||
case "new_day":
|
case "new_day":
|
||||||
return "目标日期"
|
return "目标日期"
|
||||||
|
case "day_start":
|
||||||
|
return "起始日期"
|
||||||
|
case "day_end":
|
||||||
|
return "结束日期"
|
||||||
|
case "day_scope":
|
||||||
|
return "日期范围"
|
||||||
|
case "day_of_week":
|
||||||
|
return "星期过滤"
|
||||||
|
case "week":
|
||||||
|
return "周次"
|
||||||
|
case "week_filter":
|
||||||
|
return "周次过滤"
|
||||||
|
case "week_from":
|
||||||
|
return "起始周"
|
||||||
|
case "week_to":
|
||||||
|
return "结束周"
|
||||||
case "slot_start":
|
case "slot_start":
|
||||||
return "目标时段"
|
return "目标时段"
|
||||||
case "new_slot_start":
|
case "new_slot_start":
|
||||||
return "目标时段"
|
return "目标时段"
|
||||||
|
case "span":
|
||||||
|
return "连续时长"
|
||||||
|
case "duration":
|
||||||
|
return "时长"
|
||||||
|
case "allow_embed":
|
||||||
|
return "允许嵌入补位"
|
||||||
|
case "slot_type":
|
||||||
|
return "时段类型"
|
||||||
|
case "slot_types":
|
||||||
|
return "时段类型过滤"
|
||||||
|
case "exclude_sections":
|
||||||
|
return "排除节次"
|
||||||
|
case "after_section":
|
||||||
|
return "晚于节次"
|
||||||
|
case "before_section":
|
||||||
|
return "早于节次"
|
||||||
|
case "section_from":
|
||||||
|
return "起始节次"
|
||||||
|
case "section_to":
|
||||||
|
return "结束节次"
|
||||||
case "moves":
|
case "moves":
|
||||||
return "移动列表"
|
return "移动列表"
|
||||||
case "reason":
|
case "reason":
|
||||||
return "原因"
|
return "原因"
|
||||||
case "status":
|
case "status":
|
||||||
return "状态"
|
return "状态"
|
||||||
|
case "category":
|
||||||
|
return "类别"
|
||||||
case "limit":
|
case "limit":
|
||||||
return "数量"
|
return "数量上限"
|
||||||
|
case "enqueue":
|
||||||
|
return "加入队列"
|
||||||
|
case "reset_queue":
|
||||||
|
return "重置队列"
|
||||||
|
case "detail":
|
||||||
|
return "详情级别"
|
||||||
|
case "dimensions":
|
||||||
|
return "分析维度"
|
||||||
|
case "threshold":
|
||||||
|
return "阈值"
|
||||||
|
case "include_pending":
|
||||||
|
return "包含待安排"
|
||||||
|
case "hard_categories":
|
||||||
|
return "强约束类别"
|
||||||
case "query":
|
case "query":
|
||||||
return "查询内容"
|
return "查询内容"
|
||||||
case "url":
|
case "url":
|
||||||
return "链接"
|
return "链接"
|
||||||
default:
|
default:
|
||||||
return "参数"
|
if strings.TrimSpace(key) == "" {
|
||||||
|
return "参数"
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -553,14 +624,33 @@ func formatArgumentDisplay(
|
|||||||
args map[string]any,
|
args map[string]any,
|
||||||
state *schedule.ScheduleState,
|
state *schedule.ScheduleState,
|
||||||
) string {
|
) string {
|
||||||
|
_ = toolName
|
||||||
switch key {
|
switch key {
|
||||||
case "task_id", "task_a", "task_b":
|
case "task_id", "task_item_id", "task_a", "task_b":
|
||||||
if taskID, ok := toInt(value); ok {
|
if taskID, ok := toInt(value); ok {
|
||||||
return resolveTaskLabelByID(state, taskID, true)
|
return resolveTaskLabelByID(state, taskID, true)
|
||||||
}
|
}
|
||||||
case "day", "new_day":
|
case "task_ids", "task_item_ids":
|
||||||
|
return formatTaskIDListArgumentCN(value, state)
|
||||||
|
case "day", "new_day", "day_start", "day_end":
|
||||||
if day, ok := toInt(value); ok {
|
if day, ok := toInt(value); ok {
|
||||||
return formatDayLabelCN(day)
|
return formatScheduleDayCN(state, day)
|
||||||
|
}
|
||||||
|
case "day_scope":
|
||||||
|
if text, ok := value.(string); ok {
|
||||||
|
return formatDayScopeLabelCN(text)
|
||||||
|
}
|
||||||
|
case "day_of_week":
|
||||||
|
return formatWeekdaySliceArgumentCN(value)
|
||||||
|
case "week":
|
||||||
|
if week, ok := toInt(value); ok {
|
||||||
|
return fmt.Sprintf("第%d周", week)
|
||||||
|
}
|
||||||
|
case "week_filter":
|
||||||
|
return formatWeekSliceArgumentCN(value)
|
||||||
|
case "week_from", "week_to":
|
||||||
|
if week, ok := toInt(value); ok {
|
||||||
|
return fmt.Sprintf("第%d周", week)
|
||||||
}
|
}
|
||||||
case "slot_start", "new_slot_start":
|
case "slot_start", "new_slot_start":
|
||||||
if slotStart, ok := toInt(value); ok {
|
if slotStart, ok := toInt(value); ok {
|
||||||
@@ -569,15 +659,58 @@ func formatArgumentDisplay(
|
|||||||
if task := stateTaskByID(state, taskID); task != nil && task.Duration > 1 {
|
if task := stateTaskByID(state, taskID); task != nil && task.Duration > 1 {
|
||||||
slotEnd = slotStart + task.Duration - 1
|
slotEnd = slotStart + task.Duration - 1
|
||||||
}
|
}
|
||||||
|
} else if duration, ok := toInt(args["duration"]); ok && duration > 1 {
|
||||||
|
slotEnd = slotStart + duration - 1
|
||||||
|
} else if span, ok := toInt(args["span"]); ok && span > 1 {
|
||||||
|
slotEnd = slotStart + span - 1
|
||||||
}
|
}
|
||||||
if day, ok := toInt(args["day"]); ok {
|
if day, ok := toInt(args["day"]); ok {
|
||||||
return fmt.Sprintf("%s%s", formatDayLabelCN(day), formatSlotRangeCN(slotStart, slotEnd))
|
return fmt.Sprintf("%s %s", formatScheduleDayCN(state, day), formatSlotRangeCN(slotStart, slotEnd))
|
||||||
}
|
}
|
||||||
if day, ok := toInt(args["new_day"]); ok {
|
if day, ok := toInt(args["new_day"]); ok {
|
||||||
return fmt.Sprintf("%s%s", formatDayLabelCN(day), formatSlotRangeCN(slotStart, slotEnd))
|
return fmt.Sprintf("%s %s", formatScheduleDayCN(state, day), formatSlotRangeCN(slotStart, slotEnd))
|
||||||
}
|
}
|
||||||
return formatSlotRangeCN(slotStart, slotEnd)
|
return formatSlotRangeCN(slotStart, slotEnd)
|
||||||
}
|
}
|
||||||
|
case "slot_type":
|
||||||
|
if text, ok := value.(string); ok {
|
||||||
|
return formatSlotTypeLabelCN(text)
|
||||||
|
}
|
||||||
|
case "slot_types":
|
||||||
|
return formatSlotTypeListArgumentCN(value)
|
||||||
|
case "exclude_sections":
|
||||||
|
return formatSectionSliceArgumentCN(value)
|
||||||
|
case "after_section", "before_section", "section_from", "section_to":
|
||||||
|
if section, ok := toInt(value); ok {
|
||||||
|
return fmt.Sprintf("第%d节", section)
|
||||||
|
}
|
||||||
|
case "span", "duration":
|
||||||
|
if count, ok := toInt(value); ok {
|
||||||
|
return fmt.Sprintf("%d 节", count)
|
||||||
|
}
|
||||||
|
case "allow_embed", "enqueue", "reset_queue", "include_pending":
|
||||||
|
if enabled, ok := toBool(value); ok {
|
||||||
|
return formatBoolLabelCN(enabled)
|
||||||
|
}
|
||||||
|
case "status":
|
||||||
|
if text, ok := value.(string); ok {
|
||||||
|
return formatTargetPoolStatusCN(text)
|
||||||
|
}
|
||||||
|
case "category":
|
||||||
|
return fallbackText(formatAnyValueCN(value), "未分类")
|
||||||
|
case "detail":
|
||||||
|
if text, ok := value.(string); ok {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(text)) {
|
||||||
|
case "summary":
|
||||||
|
return "摘要"
|
||||||
|
case "full":
|
||||||
|
return "完整"
|
||||||
|
default:
|
||||||
|
return fallbackText(text, "未标注")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "dimensions", "hard_categories":
|
||||||
|
return formatStringSliceArgumentCN(value)
|
||||||
case "moves":
|
case "moves":
|
||||||
return formatMovesArgumentCN(value, state)
|
return formatMovesArgumentCN(value, state)
|
||||||
}
|
}
|
||||||
@@ -646,8 +779,39 @@ func formatAnyValueCN(value any) string {
|
|||||||
return "是"
|
return "是"
|
||||||
}
|
}
|
||||||
return "否"
|
return "否"
|
||||||
|
case []string:
|
||||||
|
return formatStringSliceArgumentCN(typed)
|
||||||
|
case []int:
|
||||||
|
if len(typed) == 0 {
|
||||||
|
return "空"
|
||||||
|
}
|
||||||
|
parts := make([]string, 0, len(typed))
|
||||||
|
for _, item := range typed {
|
||||||
|
parts = append(parts, fmt.Sprintf("%d", item))
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "、")
|
||||||
|
case []float64:
|
||||||
|
if len(typed) == 0 {
|
||||||
|
return "空"
|
||||||
|
}
|
||||||
|
parts := make([]string, 0, len(typed))
|
||||||
|
for _, item := range typed {
|
||||||
|
parts = append(parts, fmt.Sprintf("%g", item))
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "、")
|
||||||
case []any:
|
case []any:
|
||||||
return fmt.Sprintf("%d 项", len(typed))
|
if len(typed) == 0 {
|
||||||
|
return "空"
|
||||||
|
}
|
||||||
|
parts := make([]string, 0, len(typed))
|
||||||
|
for index, item := range typed {
|
||||||
|
if index >= 4 {
|
||||||
|
parts = append(parts, fmt.Sprintf("等 %d 项", len(typed)))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
parts = append(parts, formatAnyValueCN(item))
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "、")
|
||||||
default:
|
default:
|
||||||
if value == nil {
|
if value == nil {
|
||||||
return "空"
|
return "空"
|
||||||
@@ -660,6 +824,143 @@ func formatAnyValueCN(value any) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func formatTaskIDListArgumentCN(value any, state *schedule.ScheduleState) string {
|
||||||
|
ids := toIntSliceAny(value)
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return formatAnyValueCN(value)
|
||||||
|
}
|
||||||
|
parts := make([]string, 0, len(ids))
|
||||||
|
for index, id := range ids {
|
||||||
|
if index >= 4 {
|
||||||
|
parts = append(parts, fmt.Sprintf("等 %d 项", len(ids)))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
parts = append(parts, resolveTaskLabelByID(state, id, true))
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "、")
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatWeekdaySliceArgumentCN(value any) string {
|
||||||
|
days := toIntSliceAny(value)
|
||||||
|
if len(days) == 0 {
|
||||||
|
return formatAnyValueCN(value)
|
||||||
|
}
|
||||||
|
return formatWeekdayListCN(days)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatWeekSliceArgumentCN(value any) string {
|
||||||
|
weeks := toIntSliceAny(value)
|
||||||
|
if len(weeks) == 0 {
|
||||||
|
return formatAnyValueCN(value)
|
||||||
|
}
|
||||||
|
return formatScheduleWeekListCN(weeks)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatSectionSliceArgumentCN(value any) string {
|
||||||
|
sections := toIntSliceAny(value)
|
||||||
|
if len(sections) == 0 {
|
||||||
|
return formatAnyValueCN(value)
|
||||||
|
}
|
||||||
|
return formatScheduleSectionListCN(sections)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatSlotTypeListArgumentCN(value any) string {
|
||||||
|
items := toStringSliceAny(value)
|
||||||
|
if len(items) == 0 {
|
||||||
|
if text, ok := value.(string); ok && strings.TrimSpace(text) != "" {
|
||||||
|
return formatSlotTypeLabelCN(text)
|
||||||
|
}
|
||||||
|
return "空"
|
||||||
|
}
|
||||||
|
parts := make([]string, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
parts = append(parts, formatSlotTypeLabelCN(item))
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "、")
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatStringSliceArgumentCN(value any) string {
|
||||||
|
items := toStringSliceAny(value)
|
||||||
|
if len(items) == 0 {
|
||||||
|
if text, ok := value.(string); ok && strings.TrimSpace(text) != "" {
|
||||||
|
return strings.TrimSpace(text)
|
||||||
|
}
|
||||||
|
return "空"
|
||||||
|
}
|
||||||
|
return strings.Join(items, "、")
|
||||||
|
}
|
||||||
|
|
||||||
|
func toIntSliceAny(value any) []int {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case []int:
|
||||||
|
out := make([]int, len(typed))
|
||||||
|
copy(out, typed)
|
||||||
|
return out
|
||||||
|
case []float64:
|
||||||
|
out := make([]int, 0, len(typed))
|
||||||
|
for _, item := range typed {
|
||||||
|
out = append(out, int(item))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
case []any:
|
||||||
|
out := make([]int, 0, len(typed))
|
||||||
|
for _, item := range typed {
|
||||||
|
number, ok := toInt(item)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, number)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toStringSliceAny(value any) []string {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case []string:
|
||||||
|
out := make([]string, 0, len(typed))
|
||||||
|
for _, item := range typed {
|
||||||
|
if strings.TrimSpace(item) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, strings.TrimSpace(item))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
case []any:
|
||||||
|
out := make([]string, 0, len(typed))
|
||||||
|
for _, item := range typed {
|
||||||
|
text := strings.TrimSpace(fmt.Sprintf("%v", item))
|
||||||
|
if text == "" || text == "<nil>" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, text)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toBool(value any) (bool, bool) {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case bool:
|
||||||
|
return typed, true
|
||||||
|
case string:
|
||||||
|
switch strings.ToLower(strings.TrimSpace(typed)) {
|
||||||
|
case "true", "1", "yes":
|
||||||
|
return true, true
|
||||||
|
case "false", "0", "no":
|
||||||
|
return false, true
|
||||||
|
default:
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func resolveTaskLabelByID(state *schedule.ScheduleState, taskID int, withID bool) string {
|
func resolveTaskLabelByID(state *schedule.ScheduleState, taskID int, withID bool) string {
|
||||||
if taskID <= 0 {
|
if taskID <= 0 {
|
||||||
return "未知任务"
|
return "未知任务"
|
||||||
|
|||||||
@@ -306,38 +306,25 @@ func registerScheduleReadTools(r *ToolRegistry) {
|
|||||||
"get_overview",
|
"get_overview",
|
||||||
"获取当前窗口总览:保留课程占位统计,展开任务清单。",
|
"获取当前窗口总览:保留课程占位统计,展开任务清单。",
|
||||||
`{"name":"get_overview","parameters":{}}`,
|
`{"name":"get_overview","parameters":{}}`,
|
||||||
wrapLegacyToolHandler("get_overview", func(state *schedule.ScheduleState, args map[string]any) string {
|
NewGetOverviewToolHandler(),
|
||||||
_ = args
|
|
||||||
return schedule.GetOverview(state)
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
r.Register(
|
r.Register(
|
||||||
"query_range",
|
"query_range",
|
||||||
"查看某天或某时段的占用详情。day 必填,slot_start/slot_end 选填。",
|
"查看某天或某时段的占用详情。day 必填,slot_start/slot_end 选填。",
|
||||||
`{"name":"query_range","parameters":{"day":{"type":"int","required":true},"slot_start":{"type":"int"},"slot_end":{"type":"int"}}}`,
|
`{"name":"query_range","parameters":{"day":{"type":"int","required":true},"slot_start":{"type":"int"},"slot_end":{"type":"int"}}}`,
|
||||||
wrapLegacyToolHandler("query_range", func(state *schedule.ScheduleState, args map[string]any) string {
|
NewQueryRangeToolHandler(),
|
||||||
day, ok := schedule.ArgsInt(args, "day")
|
|
||||||
if !ok {
|
|
||||||
return "查询失败:缺少必填参数 day。"
|
|
||||||
}
|
|
||||||
return schedule.QueryRange(state, day, schedule.ArgsIntPtr(args, "slot_start"), schedule.ArgsIntPtr(args, "slot_end"))
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
r.Register(
|
r.Register(
|
||||||
"query_available_slots",
|
"query_available_slots",
|
||||||
"查询候选空位池,适合 move 前筛落点。",
|
"查询候选空位池,适合 move 前筛落点。",
|
||||||
`{"name":"query_available_slots","parameters":{"span":{"type":"int"},"duration":{"type":"int"},"limit":{"type":"int"},"allow_embed":{"type":"bool"},"day":{"type":"int"},"day_start":{"type":"int"},"day_end":{"type":"int"},"day_scope":{"type":"string","enum":["all","workday","weekend"]},"day_of_week":{"type":"array","items":{"type":"int"}},"week":{"type":"int"},"week_filter":{"type":"array","items":{"type":"int"}},"week_from":{"type":"int"},"week_to":{"type":"int"},"slot_type":{"type":"string"},"slot_types":{"type":"array","items":{"type":"string"}},"exclude_sections":{"type":"array","items":{"type":"int"}},"after_section":{"type":"int"},"before_section":{"type":"int"},"section_from":{"type":"int"},"section_to":{"type":"int"}}}`,
|
`{"name":"query_available_slots","parameters":{"span":{"type":"int"},"duration":{"type":"int"},"limit":{"type":"int"},"allow_embed":{"type":"bool"},"day":{"type":"int"},"day_start":{"type":"int"},"day_end":{"type":"int"},"day_scope":{"type":"string","enum":["all","workday","weekend"]},"day_of_week":{"type":"array","items":{"type":"int"}},"week":{"type":"int"},"week_filter":{"type":"array","items":{"type":"int"}},"week_from":{"type":"int"},"week_to":{"type":"int"},"slot_type":{"type":"string"},"slot_types":{"type":"array","items":{"type":"string"}},"exclude_sections":{"type":"array","items":{"type":"int"}},"after_section":{"type":"int"},"before_section":{"type":"int"},"section_from":{"type":"int"},"section_to":{"type":"int"}}}`,
|
||||||
wrapLegacyToolHandler("query_available_slots", func(state *schedule.ScheduleState, args map[string]any) string {
|
NewQueryAvailableSlotsToolHandler(),
|
||||||
return schedule.QueryAvailableSlots(state, args)
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
r.Register(
|
r.Register(
|
||||||
"query_target_tasks",
|
"query_target_tasks",
|
||||||
"查询候选任务集合,可按 status/week/day/task_id/category 筛选;支持 enqueue。",
|
"查询候选任务集合,可按 status/week/day/task_id/category 筛选;支持 enqueue。",
|
||||||
`{"name":"query_target_tasks","parameters":{"status":{"type":"string","enum":["all","existing","suggested","pending"]},"category":{"type":"string"},"limit":{"type":"int"},"day_scope":{"type":"string","enum":["all","workday","weekend"]},"day":{"type":"int"},"day_start":{"type":"int"},"day_end":{"type":"int"},"day_of_week":{"type":"array","items":{"type":"int"}},"week":{"type":"int"},"week_filter":{"type":"array","items":{"type":"int"}},"week_from":{"type":"int"},"week_to":{"type":"int"},"task_ids":{"type":"array","items":{"type":"int"}},"task_id":{"type":"int"},"task_item_ids":{"type":"array","items":{"type":"int"}},"task_item_id":{"type":"int"},"enqueue":{"type":"bool"},"reset_queue":{"type":"bool"}}}`,
|
`{"name":"query_target_tasks","parameters":{"status":{"type":"string","enum":["all","existing","suggested","pending"]},"category":{"type":"string"},"limit":{"type":"int"},"day_scope":{"type":"string","enum":["all","workday","weekend"]},"day":{"type":"int"},"day_start":{"type":"int"},"day_end":{"type":"int"},"day_of_week":{"type":"array","items":{"type":"int"}},"week":{"type":"int"},"week_filter":{"type":"array","items":{"type":"int"}},"week_from":{"type":"int"},"week_to":{"type":"int"},"task_ids":{"type":"array","items":{"type":"int"}},"task_id":{"type":"int"},"task_item_ids":{"type":"array","items":{"type":"int"}},"task_item_id":{"type":"int"},"enqueue":{"type":"bool"},"reset_queue":{"type":"bool"}}}`,
|
||||||
wrapLegacyToolHandler("query_target_tasks", func(state *schedule.ScheduleState, args map[string]any) string {
|
NewQueryTargetTasksToolHandler(),
|
||||||
return schedule.QueryTargetTasks(state, args)
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
r.Register(
|
r.Register(
|
||||||
"queue_pop_head",
|
"queue_pop_head",
|
||||||
@@ -351,21 +338,13 @@ func registerScheduleReadTools(r *ToolRegistry) {
|
|||||||
"queue_status",
|
"queue_status",
|
||||||
"查看当前队列状态(pending/current/completed/skipped)。",
|
"查看当前队列状态(pending/current/completed/skipped)。",
|
||||||
`{"name":"queue_status","parameters":{}}`,
|
`{"name":"queue_status","parameters":{}}`,
|
||||||
wrapLegacyToolHandler("queue_status", func(state *schedule.ScheduleState, args map[string]any) string {
|
NewQueueStatusToolHandler(),
|
||||||
return schedule.QueueStatus(state, args)
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
r.Register(
|
r.Register(
|
||||||
"get_task_info",
|
"get_task_info",
|
||||||
"查看单个任务详情,包括类别、状态与落位。",
|
"查看单个任务详情,包括类别、状态与落位。",
|
||||||
`{"name":"get_task_info","parameters":{"task_id":{"type":"int","required":true}}}`,
|
`{"name":"get_task_info","parameters":{"task_id":{"type":"int","required":true}}}`,
|
||||||
wrapLegacyToolHandler("get_task_info", func(state *schedule.ScheduleState, args map[string]any) string {
|
NewGetTaskInfoToolHandler(),
|
||||||
taskID, ok := schedule.ArgsInt(args, "task_id")
|
|
||||||
if !ok {
|
|
||||||
return "查询失败:缺少必填参数 task_id。"
|
|
||||||
}
|
|
||||||
return schedule.GetTaskInfo(state, taskID)
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
383
backend/newAgent/tools/schedule_read_overview_queue_handlers.go
Normal file
383
backend/newAgent/tools/schedule_read_overview_queue_handlers.go
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
package newagenttools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewGetOverviewToolHandler 为 get_overview 生成结构化读结果。
|
||||||
|
func NewGetOverviewToolHandler() ToolHandler {
|
||||||
|
return func(state *schedule.ScheduleState, args map[string]any) ToolExecutionResult {
|
||||||
|
if state == nil {
|
||||||
|
return buildScheduleReadSimpleFailureResult("get_overview", args, nil, "查看总览失败:日程状态为空,无法读取总览。")
|
||||||
|
}
|
||||||
|
|
||||||
|
observation := schedule.GetOverview(state)
|
||||||
|
totalSlots := state.Window.TotalDays * 12
|
||||||
|
totalOccupied := 0
|
||||||
|
taskExistingCount := 0
|
||||||
|
taskSuggestedCount := 0
|
||||||
|
taskPendingCount := 0
|
||||||
|
courseExistingCount := 0
|
||||||
|
|
||||||
|
for i := range state.Tasks {
|
||||||
|
task := 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([]map[string]any, 0, state.Window.TotalDays)
|
||||||
|
for day := 1; day <= state.Window.TotalDays; day++ {
|
||||||
|
totalDayOccupied := countScheduleDayOccupiedForRead(state, day)
|
||||||
|
taskDayOccupied := countScheduleDayTaskOccupiedForRead(state, day)
|
||||||
|
taskEntries := listScheduleTasksOnDayForRead(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, buildScheduleReadItem(
|
||||||
|
formatScheduleDayCN(state, day),
|
||||||
|
fmt.Sprintf("总占用 %d/12 节,任务占用 %d/12 节", totalDayOccupied, taskDayOccupied),
|
||||||
|
[]string{fmt.Sprintf("任务 %d 项", len(taskEntries))},
|
||||||
|
detailLines,
|
||||||
|
map[string]any{"day": day},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
taskItems := make([]map[string]any, 0, len(state.Tasks))
|
||||||
|
for i := range state.Tasks {
|
||||||
|
task := state.Tasks[i]
|
||||||
|
if isCourseScheduleTaskForRead(task) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
detailLines := []string{
|
||||||
|
"时段:" + formatScheduleTaskSlotsBriefCN(state, task.Slots),
|
||||||
|
"来源:" + formatScheduleTaskSourceCN(task),
|
||||||
|
}
|
||||||
|
if task.TaskClassID > 0 {
|
||||||
|
detailLines = append(detailLines, fmt.Sprintf("任务类 ID:%d", task.TaskClassID))
|
||||||
|
}
|
||||||
|
taskItems = append(taskItems, buildScheduleReadItem(
|
||||||
|
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"].(map[string]any)["task_id"])
|
||||||
|
rightID, _ := toInt(taskItems[j]["meta"].(map[string]any)["task_id"])
|
||||||
|
return leftID < rightID
|
||||||
|
})
|
||||||
|
|
||||||
|
taskClassItems := make([]map[string]any, 0, len(state.TaskClasses))
|
||||||
|
for _, meta := range 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, buildScheduleReadItem(
|
||||||
|
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{
|
||||||
|
buildScheduleReadKVSection("窗口概况", []map[string]any{
|
||||||
|
buildScheduleReadKV("规划天数", fmt.Sprintf("%d 天", state.Window.TotalDays)),
|
||||||
|
buildScheduleReadKV("总时段", fmt.Sprintf("%d 节", totalSlots)),
|
||||||
|
buildScheduleReadKV("已占用", fmt.Sprintf("%d 节", totalOccupied)),
|
||||||
|
buildScheduleReadKV("空闲", fmt.Sprintf("%d 节", totalFree)),
|
||||||
|
buildScheduleReadKV("课程占位", fmt.Sprintf("%d 项", courseExistingCount)),
|
||||||
|
buildScheduleReadKV("已安排任务", fmt.Sprintf("%d 项", taskExistingCount)),
|
||||||
|
buildScheduleReadKV("已预排任务", fmt.Sprintf("%d 项", taskSuggestedCount)),
|
||||||
|
buildScheduleReadKV("待安排任务", fmt.Sprintf("%d 项", taskPendingCount)),
|
||||||
|
}),
|
||||||
|
buildScheduleReadItemsSection("每日概况", dailyItems),
|
||||||
|
buildScheduleReadItemsSection("任务清单", taskItems),
|
||||||
|
}
|
||||||
|
if len(taskClassItems) > 0 {
|
||||||
|
sections = append(sections, buildScheduleReadItemsSection("任务类约束", taskClassItems))
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildScheduleReadResult(
|
||||||
|
"get_overview",
|
||||||
|
args,
|
||||||
|
state,
|
||||||
|
observation,
|
||||||
|
ToolStatusDone,
|
||||||
|
"当前排程总览",
|
||||||
|
fmt.Sprintf("%d 天窗口,已占用 %d/%d 节,待安排 %d 项。", state.Window.TotalDays, totalOccupied, totalSlots, taskPendingCount),
|
||||||
|
[]map[string]any{
|
||||||
|
buildScheduleReadMetric("已占用", fmt.Sprintf("%d 节", totalOccupied)),
|
||||||
|
buildScheduleReadMetric("空闲", fmt.Sprintf("%d 节", totalFree)),
|
||||||
|
buildScheduleReadMetric("待安排", fmt.Sprintf("%d 项", taskPendingCount)),
|
||||||
|
buildScheduleReadMetric("课程占位", fmt.Sprintf("%d 项", courseExistingCount)),
|
||||||
|
},
|
||||||
|
dailyItems,
|
||||||
|
sections,
|
||||||
|
map[string]any{
|
||||||
|
"total_days": 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,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewQueueStatusToolHandler 为 queue_status 生成结构化读结果。
|
||||||
|
func NewQueueStatusToolHandler() ToolHandler {
|
||||||
|
return func(state *schedule.ScheduleState, args map[string]any) ToolExecutionResult {
|
||||||
|
observation := schedule.QueueStatus(state, args)
|
||||||
|
if state == nil {
|
||||||
|
return buildScheduleReadSimpleFailureResult("queue_status", args, nil, observation)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, machinePayload := decodeQueueStatusPayload(observation)
|
||||||
|
items := make([]map[string]any, 0, 1+len(payload.NextTaskIDs))
|
||||||
|
sections := make([]map[string]any, 0, 4)
|
||||||
|
if payload.Current != nil {
|
||||||
|
currentItem := buildQueueCurrentItem(state, payload.Current, payload.CurrentAttempt)
|
||||||
|
items = append(items, currentItem)
|
||||||
|
sections = append(sections, buildScheduleReadItemsSection("当前处理", []map[string]any{currentItem}))
|
||||||
|
}
|
||||||
|
|
||||||
|
nextItems := make([]map[string]any, 0, len(payload.NextTaskIDs))
|
||||||
|
for index, taskID := range payload.NextTaskIDs {
|
||||||
|
nextItems = append(nextItems, buildQueuePendingItem(state, taskID, index))
|
||||||
|
}
|
||||||
|
items = append(items, nextItems...)
|
||||||
|
if len(nextItems) > 0 {
|
||||||
|
sections = append(sections, buildScheduleReadItemsSection("待处理队列", nextItems))
|
||||||
|
}
|
||||||
|
|
||||||
|
sections = append(sections, buildScheduleReadKVSection("运行概况", []map[string]any{
|
||||||
|
buildScheduleReadKV("待处理", fmt.Sprintf("%d 项", payload.PendingCount)),
|
||||||
|
buildScheduleReadKV("已完成", fmt.Sprintf("%d 项", payload.CompletedCount)),
|
||||||
|
buildScheduleReadKV("已跳过", fmt.Sprintf("%d 项", payload.SkippedCount)),
|
||||||
|
buildScheduleReadKV("当前任务", resolveTaskQueueLabelByID(state, payload.CurrentTaskID)),
|
||||||
|
}))
|
||||||
|
if strings.TrimSpace(payload.LastError) != "" {
|
||||||
|
sections = append(sections, buildScheduleReadCalloutSection(
|
||||||
|
"最近一次失败",
|
||||||
|
"队列中保留了上一轮 apply 的失败原因。",
|
||||||
|
"warning",
|
||||||
|
[]string{strings.TrimSpace(payload.LastError)},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
title := fmt.Sprintf("队列待处理 %d 项", payload.PendingCount)
|
||||||
|
if payload.PendingCount == 0 && payload.CurrentTaskID == 0 {
|
||||||
|
title = "当前队列为空"
|
||||||
|
}
|
||||||
|
return buildScheduleReadResult(
|
||||||
|
"queue_status",
|
||||||
|
args,
|
||||||
|
state,
|
||||||
|
observation,
|
||||||
|
ToolStatusDone,
|
||||||
|
title,
|
||||||
|
buildQueueStatusSubtitle(state, payload),
|
||||||
|
[]map[string]any{
|
||||||
|
buildScheduleReadMetric("待处理", fmt.Sprintf("%d 项", payload.PendingCount)),
|
||||||
|
buildScheduleReadMetric("已完成", fmt.Sprintf("%d 项", payload.CompletedCount)),
|
||||||
|
buildScheduleReadMetric("已跳过", fmt.Sprintf("%d 项", payload.SkippedCount)),
|
||||||
|
},
|
||||||
|
items,
|
||||||
|
sections,
|
||||||
|
machinePayload,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeQueueStatusPayload(observation string) (scheduleReadQueueStatusPayload, map[string]any) {
|
||||||
|
var payload scheduleReadQueueStatusPayload
|
||||||
|
_ = json.Unmarshal([]byte(strings.TrimSpace(observation)), &payload)
|
||||||
|
raw, _ := parseObservationJSON(strings.TrimSpace(observation))
|
||||||
|
return payload, raw
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildQueueStatusSubtitle(state *schedule.ScheduleState, payload scheduleReadQueueStatusPayload) 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 "没有待处理任务,也没有正在处理的任务。"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 这里没有强抽成公共 task builder,因为 queue_status 既要兼容 payload 快照,
|
||||||
|
// 也要兼容通过 state 按 task_id 兜底,两类输入结构不同,硬抽反而会增加适配噪音。
|
||||||
|
func buildQueueCurrentItem(state *schedule.ScheduleState, payload *scheduleReadQueueTaskSnapshot, attempt int) map[string]any {
|
||||||
|
detailLines := buildQueueCurrentDetailLines(state, payload)
|
||||||
|
detailLines = append(detailLines, fmt.Sprintf("当前尝试:第 %d 次", maxInt(attempt, 1)))
|
||||||
|
return buildScheduleReadItem(
|
||||||
|
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) map[string]any {
|
||||||
|
task := state.TaskByStateID(taskID)
|
||||||
|
if task == nil {
|
||||||
|
return buildScheduleReadItem(
|
||||||
|
fmt.Sprintf("[%d]任务", taskID),
|
||||||
|
fmt.Sprintf("队列第 %d 位", index+1),
|
||||||
|
[]string{"待处理"},
|
||||||
|
[]string{"当前状态快照中未找到更多任务详情。"},
|
||||||
|
map[string]any{"task_id": taskID, "queue_index": index},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return buildScheduleReadItem(
|
||||||
|
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 := []string{}
|
||||||
|
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 *scheduleReadQueueTaskSnapshot) string {
|
||||||
|
if payload == nil {
|
||||||
|
return "当前处理"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s|%s", fallbackText(payload.Category, "未分类"), formatTargetTaskStatusCN(payload.Status))
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildQueueCurrentDetailLines(state *schedule.ScheduleState, payload *scheduleReadQueueTaskSnapshot) []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, "默认")
|
||||||
|
}
|
||||||
|
}
|
||||||
624
backend/newAgent/tools/schedule_read_result_common.go
Normal file
624
backend/newAgent/tools/schedule_read_result_common.go
Normal file
@@ -0,0 +1,624 @@
|
|||||||
|
package newagenttools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||||||
|
)
|
||||||
|
|
||||||
|
// buildScheduleReadResult 统一封装第二批 read 工具的结构化返回。
|
||||||
|
//
|
||||||
|
// 职责边界:
|
||||||
|
// 1. 负责保留原始 ObservationText,确保 LLM 看到的 observation 不变。
|
||||||
|
// 2. 负责把各工具已经算好的中文标题、指标、分区组装成统一的 ResultView。
|
||||||
|
// 3. 不负责具体业务解释,具体内容由各 read handler 先计算后传入。
|
||||||
|
func buildScheduleReadResult(
|
||||||
|
toolName string,
|
||||||
|
args map[string]any,
|
||||||
|
state *schedule.ScheduleState,
|
||||||
|
observation string,
|
||||||
|
status string,
|
||||||
|
title string,
|
||||||
|
subtitle string,
|
||||||
|
metrics []map[string]any,
|
||||||
|
items []map[string]any,
|
||||||
|
sections []map[string]any,
|
||||||
|
machinePayload map[string]any,
|
||||||
|
) ToolExecutionResult {
|
||||||
|
result := LegacyResultWithState(toolName, args, state, observation)
|
||||||
|
|
||||||
|
normalizedStatus := normalizeToolStatus(status)
|
||||||
|
if normalizedStatus == "" {
|
||||||
|
normalizedStatus = ToolStatusDone
|
||||||
|
}
|
||||||
|
if metrics == nil {
|
||||||
|
metrics = make([]map[string]any, 0)
|
||||||
|
}
|
||||||
|
if items == nil {
|
||||||
|
items = make([]map[string]any, 0)
|
||||||
|
}
|
||||||
|
if sections == nil {
|
||||||
|
sections = make([]map[string]any, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
expanded := map[string]any{
|
||||||
|
"items": items,
|
||||||
|
"sections": sections,
|
||||||
|
"raw_text": strings.TrimSpace(observation),
|
||||||
|
}
|
||||||
|
if len(machinePayload) > 0 {
|
||||||
|
expanded["machine_payload"] = cloneAnyMap(machinePayload)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Status = normalizedStatus
|
||||||
|
result.Success = normalizedStatus == ToolStatusDone
|
||||||
|
result.Summary = strings.TrimSpace(title)
|
||||||
|
result.ResultView = &ToolDisplayView{
|
||||||
|
ViewType: scheduleReadResultViewType,
|
||||||
|
Version: 1,
|
||||||
|
Collapsed: map[string]any{
|
||||||
|
"title": strings.TrimSpace(title),
|
||||||
|
"subtitle": strings.TrimSpace(subtitle),
|
||||||
|
"status": normalizedStatus,
|
||||||
|
"status_label": resolveToolStatusLabelCN(normalizedStatus),
|
||||||
|
"metrics": metrics,
|
||||||
|
},
|
||||||
|
Expanded: expanded,
|
||||||
|
}
|
||||||
|
if !result.Success {
|
||||||
|
errorCode, errorMessage := extractToolErrorInfo(observation, normalizedStatus)
|
||||||
|
if strings.TrimSpace(result.ErrorCode) == "" {
|
||||||
|
result.ErrorCode = errorCode
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(result.ErrorMessage) == "" {
|
||||||
|
result.ErrorMessage = errorMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return EnsureToolResultDefaults(result, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildScheduleReadMetric(label string, value string) map[string]any {
|
||||||
|
return map[string]any{
|
||||||
|
"label": strings.TrimSpace(label),
|
||||||
|
"value": strings.TrimSpace(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildScheduleReadItem(title string, subtitle string, tags []string, detailLines []string, meta map[string]any) map[string]any {
|
||||||
|
item := map[string]any{
|
||||||
|
"title": strings.TrimSpace(title),
|
||||||
|
"subtitle": strings.TrimSpace(subtitle),
|
||||||
|
"tags": normalizeStringSlice(tags),
|
||||||
|
"detail_lines": normalizeStringSlice(detailLines),
|
||||||
|
}
|
||||||
|
if len(meta) > 0 {
|
||||||
|
item["meta"] = cloneAnyMap(meta)
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildScheduleReadKV(label string, value string) map[string]any {
|
||||||
|
return map[string]any{
|
||||||
|
"label": strings.TrimSpace(label),
|
||||||
|
"value": strings.TrimSpace(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildScheduleReadItemsSection(title string, items []map[string]any) map[string]any {
|
||||||
|
if items == nil {
|
||||||
|
items = make([]map[string]any, 0)
|
||||||
|
}
|
||||||
|
return map[string]any{
|
||||||
|
"type": "items",
|
||||||
|
"title": strings.TrimSpace(title),
|
||||||
|
"items": items,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildScheduleReadKVSection(title string, fields []map[string]any) map[string]any {
|
||||||
|
if fields == nil {
|
||||||
|
fields = make([]map[string]any, 0)
|
||||||
|
}
|
||||||
|
return map[string]any{
|
||||||
|
"type": "kv",
|
||||||
|
"title": strings.TrimSpace(title),
|
||||||
|
"fields": fields,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildScheduleReadCalloutSection(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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildScheduleReadArgsSection(title string, view *ToolArgumentView) map[string]any {
|
||||||
|
if view == nil || view.Expanded == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
rawFields, ok := view.Expanded["fields"].([]map[string]any)
|
||||||
|
if ok {
|
||||||
|
fields := make([]map[string]any, 0, len(rawFields))
|
||||||
|
for _, raw := range rawFields {
|
||||||
|
label, _ := raw["label"].(string)
|
||||||
|
display, _ := raw["display"].(string)
|
||||||
|
if strings.TrimSpace(label) == "" || strings.TrimSpace(display) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fields = append(fields, buildScheduleReadKV(label, display))
|
||||||
|
}
|
||||||
|
if len(fields) > 0 {
|
||||||
|
return buildScheduleReadKVSection(title, fields)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rawAny, ok := view.Expanded["fields"].([]any)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fields := make([]map[string]any, 0, len(rawAny))
|
||||||
|
for _, current := range rawAny {
|
||||||
|
row, ok := current.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
label, _ := row["label"].(string)
|
||||||
|
display, _ := row["display"].(string)
|
||||||
|
if strings.TrimSpace(label) == "" || strings.TrimSpace(display) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fields = append(fields, buildScheduleReadKV(label, display))
|
||||||
|
}
|
||||||
|
if len(fields) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return buildScheduleReadKVSection(title, fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildReadFailureSections(argView *ToolArgumentView, observation string) []map[string]any {
|
||||||
|
message := trimFailureText(observation, "读取结果失败,请检查参数后重试。")
|
||||||
|
sections := []map[string]any{
|
||||||
|
buildScheduleReadCalloutSection("执行失败", message, "danger", []string{message}),
|
||||||
|
}
|
||||||
|
appendSectionIfPresent(§ions, buildScheduleReadArgsSection("查询条件", argView))
|
||||||
|
return sections
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildScheduleReadSimpleFailureResult(toolName string, args map[string]any, state *schedule.ScheduleState, observation string) ToolExecutionResult {
|
||||||
|
legacy := LegacyResultWithState(toolName, args, state, observation)
|
||||||
|
return buildScheduleReadResult(
|
||||||
|
toolName,
|
||||||
|
args,
|
||||||
|
state,
|
||||||
|
observation,
|
||||||
|
ToolStatusFailed,
|
||||||
|
fmt.Sprintf("%s失败", resolveToolLabelCN(toolName)),
|
||||||
|
trimFailureText(observation, "请检查筛选条件后重试。"),
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
buildReadFailureSections(legacy.ArgumentView, observation),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendSectionIfPresent(target *[]map[string]any, section map[string]any) {
|
||||||
|
if section == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*target = append(*target, section)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
status, _ := resolveToolStatusAndSuccess(observation)
|
||||||
|
_, message := extractToolErrorInfo(observation, status)
|
||||||
|
if 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
|
||||||
|
}
|
||||||
|
|
||||||
|
func listScheduleTasksOnDayForRead(state *schedule.ScheduleState, day int, includeCourse bool) []scheduleReadTaskOnDay {
|
||||||
|
if state == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
items := make([]scheduleReadTaskOnDay, 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, scheduleReadTaskOnDay{
|
||||||
|
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) []scheduleReadTaskOnDay {
|
||||||
|
items := listScheduleTasksOnDayForRead(state, day, includeCourse)
|
||||||
|
filtered := make([]scheduleReadTaskOnDay, 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) []scheduleReadFreeRange {
|
||||||
|
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([]scheduleReadFreeRange, 0)
|
||||||
|
start := 0
|
||||||
|
for section := 1; section <= 12; section++ {
|
||||||
|
if !occupied[section] {
|
||||||
|
if start == 0 {
|
||||||
|
start = section
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if start > 0 {
|
||||||
|
ranges = append(ranges, scheduleReadFreeRange{Day: day, SlotStart: start, SlotEnd: section - 1})
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if start > 0 {
|
||||||
|
ranges = append(ranges, scheduleReadFreeRange{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)
|
||||||
|
}
|
||||||
112
backend/newAgent/tools/schedule_read_result_types.go
Normal file
112
backend/newAgent/tools/schedule_read_result_types.go
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package newagenttools
|
||||||
|
|
||||||
|
import "github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||||||
|
|
||||||
|
const scheduleReadResultViewType = "schedule.read_result"
|
||||||
|
|
||||||
|
type scheduleReadTaskOnDay struct {
|
||||||
|
Task *schedule.ScheduleTask
|
||||||
|
SlotStart int
|
||||||
|
SlotEnd int
|
||||||
|
}
|
||||||
|
|
||||||
|
type scheduleReadFreeRange struct {
|
||||||
|
Day int
|
||||||
|
SlotStart int
|
||||||
|
SlotEnd int
|
||||||
|
}
|
||||||
|
|
||||||
|
type scheduleReadAvailableSlotsPayload 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 []scheduleReadAvailableSlotRecord `json:"slots"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type scheduleReadAvailableSlotRecord 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type scheduleReadTargetTasksPayload 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 *scheduleReadTargetTasksQueueRecord `json:"queue"`
|
||||||
|
Items []scheduleReadTargetTaskRecord `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type scheduleReadTargetTasksQueueRecord 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type scheduleReadTargetTaskRecord 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 []scheduleReadTargetTaskSlotInfo `json:"slots"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type scheduleReadTargetTaskSlotInfo 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type scheduleReadQueueStatusPayload 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 *scheduleReadQueueTaskSnapshot `json:"current"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type scheduleReadQueueTaskSnapshot 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 []scheduleReadTargetTaskSlotInfo `json:"slots"`
|
||||||
|
}
|
||||||
409
backend/newAgent/tools/schedule_read_slots_handlers.go
Normal file
409
backend/newAgent/tools/schedule_read_slots_handlers.go
Normal file
@@ -0,0 +1,409 @@
|
|||||||
|
package newagenttools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewQueryAvailableSlotsToolHandler 为 query_available_slots 生成结构化读结果。
|
||||||
|
func NewQueryAvailableSlotsToolHandler() ToolHandler {
|
||||||
|
return func(state *schedule.ScheduleState, args map[string]any) ToolExecutionResult {
|
||||||
|
observation := schedule.QueryAvailableSlots(state, args)
|
||||||
|
status, _ := resolveToolStatusAndSuccess(observation)
|
||||||
|
if status != ToolStatusDone {
|
||||||
|
return buildScheduleReadSimpleFailureResult("query_available_slots", args, state, observation)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, machinePayload := decodeAvailableSlotsPayload(observation)
|
||||||
|
items := make([]map[string]any, 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(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(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, buildScheduleReadItem(
|
||||||
|
formatScheduleDaySlotCN(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 := []map[string]any{
|
||||||
|
buildScheduleReadMetric("候选时段", fmt.Sprintf("%d 个", payload.Count)),
|
||||||
|
buildScheduleReadMetric("纯空位", fmt.Sprintf("%d 个", payload.StrictCount)),
|
||||||
|
}
|
||||||
|
if payload.AllowEmbed {
|
||||||
|
metrics = append(metrics, buildScheduleReadMetric("可嵌入候选", fmt.Sprintf("%d 个", payload.EmbeddedCount)))
|
||||||
|
}
|
||||||
|
|
||||||
|
sections := []map[string]any{
|
||||||
|
buildScheduleReadKVSection("查询概况", []map[string]any{
|
||||||
|
buildScheduleReadKV("查询跨度", fmt.Sprintf("%d 节连续时段", maxInt(payload.Span, 1))),
|
||||||
|
buildScheduleReadKV("日期范围", formatDayScopeLabelCN(payload.DayScope)),
|
||||||
|
buildScheduleReadKV("星期过滤", formatWeekdayListCN(payload.DayOfWeek)),
|
||||||
|
buildScheduleReadKV("周次范围", buildWeekRangeLabelCN(payload.WeekFrom, payload.WeekTo, payload.WeekFilter)),
|
||||||
|
buildScheduleReadKV("允许嵌入补位", formatBoolLabelCN(payload.AllowEmbed)),
|
||||||
|
buildScheduleReadKV("排除节次", formatScheduleSectionListCN(payload.ExcludeSections)),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
appendSectionIfPresent(§ions, buildScheduleReadArgsSection("筛选条件", LegacyResultWithState("query_available_slots", args, state, observation).ArgumentView))
|
||||||
|
if len(items) > 0 {
|
||||||
|
sections = append(sections, buildScheduleReadItemsSection("候选时段", items))
|
||||||
|
} else {
|
||||||
|
sections = append(sections, buildScheduleReadCalloutSection(
|
||||||
|
"没有找到可用时段",
|
||||||
|
"当前筛选条件下没有命中的候选落点。",
|
||||||
|
"info",
|
||||||
|
[]string{"可以调整周次、星期、节次范围或是否允许嵌入补位。"},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
title := fmt.Sprintf("找到 %d 个可用时段", payload.Count)
|
||||||
|
if payload.Count == 0 {
|
||||||
|
title = "未找到可用时段"
|
||||||
|
}
|
||||||
|
return buildScheduleReadResult(
|
||||||
|
"query_available_slots",
|
||||||
|
args,
|
||||||
|
state,
|
||||||
|
observation,
|
||||||
|
ToolStatusDone,
|
||||||
|
title,
|
||||||
|
buildAvailableSlotsSubtitle(payload),
|
||||||
|
metrics,
|
||||||
|
items,
|
||||||
|
sections,
|
||||||
|
machinePayload,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewQueryRangeToolHandler 为 query_range 生成结构化读结果。
|
||||||
|
func NewQueryRangeToolHandler() ToolHandler {
|
||||||
|
return func(state *schedule.ScheduleState, args map[string]any) ToolExecutionResult {
|
||||||
|
day, ok := schedule.ArgsInt(args, "day")
|
||||||
|
if !ok {
|
||||||
|
return buildScheduleReadSimpleFailureResult("query_range", args, state, "查询失败:缺少必填参数 day。")
|
||||||
|
}
|
||||||
|
if state == nil {
|
||||||
|
return buildScheduleReadSimpleFailureResult("query_range", args, nil, "查询失败:日程状态为空,无法读取时间范围。")
|
||||||
|
}
|
||||||
|
|
||||||
|
slotStart := schedule.ArgsIntPtr(args, "slot_start")
|
||||||
|
slotEnd := schedule.ArgsIntPtr(args, "slot_end")
|
||||||
|
observation := schedule.QueryRange(state, day, slotStart, slotEnd)
|
||||||
|
status, _ := resolveToolStatusAndSuccess(observation)
|
||||||
|
if status != ToolStatusDone {
|
||||||
|
return buildScheduleReadSimpleFailureResult("query_range", args, state, observation)
|
||||||
|
}
|
||||||
|
if slotStart == nil || slotEnd == nil {
|
||||||
|
return buildFullDayRangeReadResult(args, state, observation, day)
|
||||||
|
}
|
||||||
|
return buildSpecificRangeReadResult(args, state, observation, day, *slotStart, *slotEnd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildFullDayRangeReadResult(args map[string]any, state *schedule.ScheduleState, observation string, day int) ToolExecutionResult {
|
||||||
|
totalOccupied := countScheduleDayOccupiedForRead(state, day)
|
||||||
|
taskOccupied := countScheduleDayTaskOccupiedForRead(state, day)
|
||||||
|
freeRanges := findScheduleFreeRangesOnDayForRead(state, day)
|
||||||
|
|
||||||
|
bandItems := make([]map[string]any, 0, 6)
|
||||||
|
for start := 1; start <= 11; start += 2 {
|
||||||
|
end := start + 1
|
||||||
|
occupants := listScheduleTasksInRangeForRead(state, 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, buildScheduleReadItem(
|
||||||
|
formatScheduleSlotRangeCN(start, end),
|
||||||
|
subtitle,
|
||||||
|
tags,
|
||||||
|
detailLines,
|
||||||
|
map[string]any{
|
||||||
|
"day": day,
|
||||||
|
"slot_start": start,
|
||||||
|
"slot_end": end,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
freeItems := make([]map[string]any, 0, len(freeRanges))
|
||||||
|
for _, freeRange := range freeRanges {
|
||||||
|
freeItems = append(freeItems, buildScheduleReadItem(
|
||||||
|
formatScheduleSlotRangeCN(freeRange.SlotStart, freeRange.SlotEnd),
|
||||||
|
fmt.Sprintf("%d 节连续空闲", freeRange.SlotEnd-freeRange.SlotStart+1),
|
||||||
|
[]string{"连续空闲"},
|
||||||
|
[]string{fmt.Sprintf("位置:%s", formatScheduleDaySlotCN(state, day, freeRange.SlotStart, freeRange.SlotEnd))},
|
||||||
|
map[string]any{
|
||||||
|
"day": day,
|
||||||
|
"slot_start": freeRange.SlotStart,
|
||||||
|
"slot_end": freeRange.SlotEnd,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
taskEntries := listScheduleTasksOnDayForRead(state, day, false)
|
||||||
|
taskItems := make([]map[string]any, 0, len(taskEntries))
|
||||||
|
for _, entry := range taskEntries {
|
||||||
|
taskItems = append(taskItems, buildScheduleReadItem(
|
||||||
|
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{
|
||||||
|
buildScheduleReadKVSection("当日概况", []map[string]any{
|
||||||
|
buildScheduleReadKV("总占用", fmt.Sprintf("%d/12 节", totalOccupied)),
|
||||||
|
buildScheduleReadKV("任务占用", fmt.Sprintf("%d/12 节", taskOccupied)),
|
||||||
|
buildScheduleReadKV("连续空闲段", fmt.Sprintf("%d 段", len(freeRanges))),
|
||||||
|
}),
|
||||||
|
buildScheduleReadItemsSection("时段分布", bandItems),
|
||||||
|
}
|
||||||
|
if len(freeItems) > 0 {
|
||||||
|
sections = append(sections, buildScheduleReadItemsSection("连续空闲区", freeItems))
|
||||||
|
}
|
||||||
|
if embeddableItems := buildEmbeddableItemsForDay(state, day); len(embeddableItems) > 0 {
|
||||||
|
sections = append(sections, buildScheduleReadItemsSection("可嵌入时段", embeddableItems))
|
||||||
|
}
|
||||||
|
if len(taskItems) > 0 {
|
||||||
|
sections = append(sections, buildScheduleReadItemsSection("当日任务", taskItems))
|
||||||
|
}
|
||||||
|
appendSectionIfPresent(§ions, buildScheduleReadArgsSection("查询条件", LegacyResultWithState("query_range", args, state, observation).ArgumentView))
|
||||||
|
|
||||||
|
return buildScheduleReadResult(
|
||||||
|
"query_range",
|
||||||
|
args,
|
||||||
|
state,
|
||||||
|
observation,
|
||||||
|
ToolStatusDone,
|
||||||
|
fmt.Sprintf("%s全日概况", formatScheduleDayCN(state, day)),
|
||||||
|
fmt.Sprintf("已占用 %d/12 节,连续空闲 %d 段。", totalOccupied, len(freeRanges)),
|
||||||
|
[]map[string]any{
|
||||||
|
buildScheduleReadMetric("总占用", fmt.Sprintf("%d/12", totalOccupied)),
|
||||||
|
buildScheduleReadMetric("任务占用", fmt.Sprintf("%d/12", taskOccupied)),
|
||||||
|
buildScheduleReadMetric("空闲段", fmt.Sprintf("%d 段", len(freeRanges))),
|
||||||
|
},
|
||||||
|
bandItems,
|
||||||
|
sections,
|
||||||
|
map[string]any{
|
||||||
|
"mode": "full_day",
|
||||||
|
"day": day,
|
||||||
|
"occupied_slots": totalOccupied,
|
||||||
|
"task_occupied_slots": taskOccupied,
|
||||||
|
"free_range_count": len(freeRanges),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSpecificRangeReadResult(args map[string]any, state *schedule.ScheduleState, observation string, day int, slotStart int, slotEnd int) ToolExecutionResult {
|
||||||
|
total := slotEnd - slotStart + 1
|
||||||
|
freeCount := 0
|
||||||
|
slotItems := make([]map[string]any, 0, total)
|
||||||
|
for section := slotStart; section <= slotEnd; section++ {
|
||||||
|
occupants := listScheduleTasksInRangeForRead(state, 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, buildScheduleReadItem(
|
||||||
|
fmt.Sprintf("第%d节", section),
|
||||||
|
subtitle,
|
||||||
|
tags,
|
||||||
|
detailLines,
|
||||||
|
map[string]any{
|
||||||
|
"day": day,
|
||||||
|
"slot_start": section,
|
||||||
|
"slot_end": section,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := make(map[int]struct{})
|
||||||
|
rangeTaskItems := make([]map[string]any, 0)
|
||||||
|
for _, occupant := range listScheduleTasksInRangeForRead(state, day, slotStart, slotEnd, true) {
|
||||||
|
if _, exists := seen[occupant.Task.StateID]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[occupant.Task.StateID] = struct{}{}
|
||||||
|
rangeTaskItems = append(rangeTaskItems, buildScheduleReadItem(
|
||||||
|
fmt.Sprintf("[%d]%s", occupant.Task.StateID, fallbackText(occupant.Task.Name, "未命名任务")),
|
||||||
|
formatScheduleTaskStatusCN(*occupant.Task),
|
||||||
|
[]string{fallbackText(occupant.Task.Category, "未分类")},
|
||||||
|
[]string{
|
||||||
|
fmt.Sprintf("覆盖范围:%s", formatScheduleDaySlotCN(state, 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{
|
||||||
|
buildScheduleReadKVSection("范围概况", []map[string]any{
|
||||||
|
buildScheduleReadKV("查询范围", formatScheduleSlotRangeCN(slotStart, slotEnd)),
|
||||||
|
buildScheduleReadKV("总节数", fmt.Sprintf("%d 节", total)),
|
||||||
|
buildScheduleReadKV("空闲节数", fmt.Sprintf("%d 节", freeCount)),
|
||||||
|
buildScheduleReadKV("占用节数", fmt.Sprintf("%d 节", total-freeCount)),
|
||||||
|
}),
|
||||||
|
buildScheduleReadItemsSection("逐节情况", slotItems),
|
||||||
|
}
|
||||||
|
if len(rangeTaskItems) > 0 {
|
||||||
|
sections = append(sections, buildScheduleReadItemsSection("范围内事项", rangeTaskItems))
|
||||||
|
}
|
||||||
|
appendSectionIfPresent(§ions, buildScheduleReadArgsSection("查询条件", LegacyResultWithState("query_range", args, state, observation).ArgumentView))
|
||||||
|
|
||||||
|
return buildScheduleReadResult(
|
||||||
|
"query_range",
|
||||||
|
args,
|
||||||
|
state,
|
||||||
|
observation,
|
||||||
|
ToolStatusDone,
|
||||||
|
fmt.Sprintf("%s %s", formatScheduleDayCN(state, day), formatScheduleSlotRangeCN(slotStart, slotEnd)),
|
||||||
|
fmt.Sprintf("共 %d 节,空闲 %d 节,占用 %d 节。", total, freeCount, total-freeCount),
|
||||||
|
[]map[string]any{
|
||||||
|
buildScheduleReadMetric("总节数", fmt.Sprintf("%d 节", total)),
|
||||||
|
buildScheduleReadMetric("空闲", fmt.Sprintf("%d 节", freeCount)),
|
||||||
|
buildScheduleReadMetric("事项", fmt.Sprintf("%d 个", len(rangeTaskItems))),
|
||||||
|
},
|
||||||
|
slotItems,
|
||||||
|
sections,
|
||||||
|
map[string]any{
|
||||||
|
"mode": "specific_range",
|
||||||
|
"day": day,
|
||||||
|
"slot_start": slotStart,
|
||||||
|
"slot_end": slotEnd,
|
||||||
|
"free_count": freeCount,
|
||||||
|
"occupied_count": total - freeCount,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeAvailableSlotsPayload(observation string) (scheduleReadAvailableSlotsPayload, map[string]any) {
|
||||||
|
var payload scheduleReadAvailableSlotsPayload
|
||||||
|
_ = json.Unmarshal([]byte(strings.TrimSpace(observation)), &payload)
|
||||||
|
raw, _ := parseObservationJSON(strings.TrimSpace(observation))
|
||||||
|
return payload, raw
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAvailableSlotsSubtitle(payload scheduleReadAvailableSlotsPayload) 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) []map[string]any {
|
||||||
|
if state == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
items := make([]map[string]any, 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, buildScheduleReadItem(
|
||||||
|
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, "未分类"),
|
||||||
|
)
|
||||||
|
}
|
||||||
300
backend/newAgent/tools/schedule_read_tasks_handlers.go
Normal file
300
backend/newAgent/tools/schedule_read_tasks_handlers.go
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
package newagenttools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewQueryTargetTasksToolHandler 为 query_target_tasks 生成结构化读结果。
|
||||||
|
func NewQueryTargetTasksToolHandler() ToolHandler {
|
||||||
|
return func(state *schedule.ScheduleState, args map[string]any) ToolExecutionResult {
|
||||||
|
observation := schedule.QueryTargetTasks(state, args)
|
||||||
|
status, _ := resolveToolStatusAndSuccess(observation)
|
||||||
|
if status != ToolStatusDone {
|
||||||
|
return buildScheduleReadSimpleFailureResult("query_target_tasks", args, state, observation)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, machinePayload := decodeTargetTasksPayload(observation)
|
||||||
|
items := make([]map[string]any, 0, len(payload.Items))
|
||||||
|
for _, item := range payload.Items {
|
||||||
|
items = append(items, buildScheduleReadItem(
|
||||||
|
fmt.Sprintf("[%d]%s", item.TaskID, fallbackText(item.Name, "未命名任务")),
|
||||||
|
buildTargetTaskSubtitle(item),
|
||||||
|
buildTargetTaskTags(item),
|
||||||
|
buildTargetTaskDetailLines(state, item),
|
||||||
|
map[string]any{
|
||||||
|
"task_id": item.TaskID,
|
||||||
|
"category": item.Category,
|
||||||
|
"status": item.Status,
|
||||||
|
"duration": item.Duration,
|
||||||
|
"task_class_id": item.TaskClassID,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
metrics := []map[string]any{
|
||||||
|
buildScheduleReadMetric("候选任务", fmt.Sprintf("%d 项", payload.Count)),
|
||||||
|
buildScheduleReadMetric("任务池", formatTargetPoolStatusCN(payload.Status)),
|
||||||
|
}
|
||||||
|
if payload.Enqueue {
|
||||||
|
metrics = append(metrics, buildScheduleReadMetric("已入队", fmt.Sprintf("%d 项", payload.Enqueued)))
|
||||||
|
}
|
||||||
|
|
||||||
|
sections := []map[string]any{
|
||||||
|
buildScheduleReadKVSection("筛选概况", []map[string]any{
|
||||||
|
buildScheduleReadKV("任务池", formatTargetPoolStatusCN(payload.Status)),
|
||||||
|
buildScheduleReadKV("日期范围", formatDayScopeLabelCN(payload.DayScope)),
|
||||||
|
buildScheduleReadKV("星期过滤", formatWeekdayListCN(payload.DayOfWeek)),
|
||||||
|
buildScheduleReadKV("周次范围", buildWeekRangeLabelCN(payload.WeekFrom, payload.WeekTo, payload.WeekFilter)),
|
||||||
|
buildScheduleReadKV("是否入队", formatBoolLabelCN(payload.Enqueue)),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
appendSectionIfPresent(§ions, buildScheduleReadArgsSection("筛选条件", LegacyResultWithState("query_target_tasks", args, state, observation).ArgumentView))
|
||||||
|
if payload.Queue != nil {
|
||||||
|
sections = append(sections, buildScheduleReadKVSection("队列状态", []map[string]any{
|
||||||
|
buildScheduleReadKV("待处理", fmt.Sprintf("%d 项", payload.Queue.PendingCount)),
|
||||||
|
buildScheduleReadKV("已完成", fmt.Sprintf("%d 项", payload.Queue.CompletedCount)),
|
||||||
|
buildScheduleReadKV("已跳过", fmt.Sprintf("%d 项", payload.Queue.SkippedCount)),
|
||||||
|
buildScheduleReadKV("当前任务", resolveTaskQueueLabelByID(state, payload.Queue.CurrentTaskID)),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
if len(items) > 0 {
|
||||||
|
sections = append(sections, buildScheduleReadItemsSection("候选任务", items))
|
||||||
|
} else {
|
||||||
|
sections = append(sections, buildScheduleReadCalloutSection(
|
||||||
|
"没有命中任务",
|
||||||
|
"当前筛选条件下没有找到候选任务。",
|
||||||
|
"info",
|
||||||
|
[]string{"可以放宽状态、日期或任务 ID 过滤条件后再试。"},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
title := fmt.Sprintf("找到 %d 个候选任务", payload.Count)
|
||||||
|
if payload.Count == 0 {
|
||||||
|
title = "未找到候选任务"
|
||||||
|
}
|
||||||
|
return buildScheduleReadResult(
|
||||||
|
"query_target_tasks",
|
||||||
|
args,
|
||||||
|
state,
|
||||||
|
observation,
|
||||||
|
ToolStatusDone,
|
||||||
|
title,
|
||||||
|
buildTargetTasksSummarySubtitle(payload),
|
||||||
|
metrics,
|
||||||
|
items,
|
||||||
|
sections,
|
||||||
|
machinePayload,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGetTaskInfoToolHandler 为 get_task_info 生成结构化读结果。
|
||||||
|
func NewGetTaskInfoToolHandler() ToolHandler {
|
||||||
|
return func(state *schedule.ScheduleState, args map[string]any) ToolExecutionResult {
|
||||||
|
taskID, ok := schedule.ArgsInt(args, "task_id")
|
||||||
|
if !ok {
|
||||||
|
return buildScheduleReadSimpleFailureResult("get_task_info", args, state, "查询失败:缺少必填参数 task_id。")
|
||||||
|
}
|
||||||
|
if state == nil {
|
||||||
|
return buildScheduleReadSimpleFailureResult("get_task_info", args, nil, "查询失败:日程状态为空,无法读取任务详情。")
|
||||||
|
}
|
||||||
|
|
||||||
|
observation := schedule.GetTaskInfo(state, taskID)
|
||||||
|
task := state.TaskByStateID(taskID)
|
||||||
|
if task == nil {
|
||||||
|
return buildScheduleReadSimpleFailureResult("get_task_info", args, state, observation)
|
||||||
|
}
|
||||||
|
|
||||||
|
slotItems := make([]map[string]any, 0, len(task.Slots))
|
||||||
|
for _, slot := range cloneAndSortTaskSlots(task.Slots) {
|
||||||
|
slotItems = append(slotItems, buildScheduleReadItem(
|
||||||
|
formatScheduleDaySlotCN(state, slot.Day, slot.SlotStart, slot.SlotEnd),
|
||||||
|
formatScheduleTaskStatusCN(*task),
|
||||||
|
[]string{fallbackText(task.Category, "未分类")},
|
||||||
|
[]string{
|
||||||
|
fmt.Sprintf("来源:%s", formatScheduleTaskSourceCN(*task)),
|
||||||
|
fmt.Sprintf("时长:%d 节", slot.SlotEnd-slot.SlotStart+1),
|
||||||
|
},
|
||||||
|
map[string]any{
|
||||||
|
"day": slot.Day,
|
||||||
|
"slot_start": slot.SlotStart,
|
||||||
|
"slot_end": slot.SlotEnd,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := []map[string]any{
|
||||||
|
buildScheduleReadKV("类别", fallbackText(task.Category, "未分类")),
|
||||||
|
buildScheduleReadKV("状态", formatScheduleTaskStatusCN(*task)),
|
||||||
|
buildScheduleReadKV("来源", formatScheduleTaskSourceCN(*task)),
|
||||||
|
buildScheduleReadKV("落位情况", buildTaskPlacementLabel(task)),
|
||||||
|
buildScheduleReadKV("时长需求", buildTaskDurationLabel(task)),
|
||||||
|
}
|
||||||
|
if task.TaskClassID > 0 {
|
||||||
|
fields = append(fields, buildScheduleReadKV("任务类 ID", fmt.Sprintf("%d", task.TaskClassID)))
|
||||||
|
}
|
||||||
|
if task.CanEmbed {
|
||||||
|
fields = append(fields, buildScheduleReadKV("可作为宿主", "是"))
|
||||||
|
}
|
||||||
|
|
||||||
|
sections := []map[string]any{
|
||||||
|
buildScheduleReadKVSection("基本信息", fields),
|
||||||
|
}
|
||||||
|
if len(slotItems) > 0 {
|
||||||
|
sections = append(sections, buildScheduleReadItemsSection("占用时段", slotItems))
|
||||||
|
}
|
||||||
|
if relationLines := buildTaskRelationLines(state, task); len(relationLines) > 0 {
|
||||||
|
sections = append(sections, buildScheduleReadCalloutSection("嵌入关系", "当前任务存在宿主/客体关系。", "info", relationLines))
|
||||||
|
}
|
||||||
|
appendSectionIfPresent(§ions, buildScheduleReadArgsSection("查询条件", LegacyResultWithState("get_task_info", args, state, observation).ArgumentView))
|
||||||
|
|
||||||
|
return buildScheduleReadResult(
|
||||||
|
"get_task_info",
|
||||||
|
args,
|
||||||
|
state,
|
||||||
|
observation,
|
||||||
|
ToolStatusDone,
|
||||||
|
fmt.Sprintf("[%d]%s", task.StateID, fallbackText(task.Name, "未命名任务")),
|
||||||
|
fmt.Sprintf("%s|%s", fallbackText(task.Category, "未分类"), formatScheduleTaskStatusCN(*task)),
|
||||||
|
[]map[string]any{
|
||||||
|
buildScheduleReadMetric("状态", formatScheduleTaskStatusCN(*task)),
|
||||||
|
buildScheduleReadMetric("时长", buildTaskDurationLabel(task)),
|
||||||
|
buildScheduleReadMetric("落位", buildTaskPlacementLabel(task)),
|
||||||
|
},
|
||||||
|
slotItems,
|
||||||
|
sections,
|
||||||
|
map[string]any{
|
||||||
|
"task_id": task.StateID,
|
||||||
|
"source": task.Source,
|
||||||
|
"status": task.Status,
|
||||||
|
"task_class_id": task.TaskClassID,
|
||||||
|
"can_embed": task.CanEmbed,
|
||||||
|
"embedded_by": task.EmbeddedBy,
|
||||||
|
"embed_host": task.EmbedHost,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeTargetTasksPayload(observation string) (scheduleReadTargetTasksPayload, map[string]any) {
|
||||||
|
var payload scheduleReadTargetTasksPayload
|
||||||
|
_ = json.Unmarshal([]byte(strings.TrimSpace(observation)), &payload)
|
||||||
|
raw, _ := parseObservationJSON(strings.TrimSpace(observation))
|
||||||
|
return payload, raw
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildTargetTasksSummarySubtitle(payload scheduleReadTargetTasksPayload) string {
|
||||||
|
parts := []string{
|
||||||
|
formatTargetPoolStatusCN(payload.Status),
|
||||||
|
formatDayScopeLabelCN(payload.DayScope),
|
||||||
|
buildWeekRangeLabelCN(payload.WeekFrom, payload.WeekTo, payload.WeekFilter),
|
||||||
|
}
|
||||||
|
if len(payload.DayOfWeek) > 0 {
|
||||||
|
parts = append(parts, formatWeekdayListCN(payload.DayOfWeek))
|
||||||
|
}
|
||||||
|
if payload.Enqueue {
|
||||||
|
parts = append(parts, fmt.Sprintf("已入队 %d 项", payload.Enqueued))
|
||||||
|
}
|
||||||
|
return strings.Join(parts, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildTargetTaskSubtitle(item scheduleReadTargetTaskRecord) string {
|
||||||
|
return fmt.Sprintf("%s|%s", fallbackText(item.Category, "未分类"), formatTargetTaskStatusCN(item.Status))
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildTargetTaskTags(item scheduleReadTargetTaskRecord) []string {
|
||||||
|
tags := []string{formatTargetTaskStatusCN(item.Status)}
|
||||||
|
if item.Duration > 0 {
|
||||||
|
tags = append(tags, fmt.Sprintf("%d 节", item.Duration))
|
||||||
|
}
|
||||||
|
if item.TaskClassID > 0 {
|
||||||
|
tags = append(tags, fmt.Sprintf("任务类 %d", item.TaskClassID))
|
||||||
|
}
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildTargetTaskDetailLines(state *schedule.ScheduleState, item scheduleReadTargetTaskRecord) []string {
|
||||||
|
lines := make([]string, 0, 3)
|
||||||
|
if len(item.Slots) == 0 {
|
||||||
|
lines = append(lines, fmt.Sprintf("当前未落位,仍需要 %s。", buildTaskDurationText(item.Duration)))
|
||||||
|
} else {
|
||||||
|
slotParts := make([]string, 0, len(item.Slots))
|
||||||
|
for _, slot := range item.Slots {
|
||||||
|
slotParts = append(slotParts, formatScheduleDaySlotCN(state, slot.Day, slot.SlotStart, slot.SlotEnd))
|
||||||
|
}
|
||||||
|
lines = append(lines, "时段:"+strings.Join(slotParts, ";"))
|
||||||
|
}
|
||||||
|
if item.TaskClassID > 0 {
|
||||||
|
lines = append(lines, fmt.Sprintf("任务类 ID:%d", item.TaskClassID))
|
||||||
|
}
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveTaskQueueLabelByID(state *schedule.ScheduleState, taskID int) string {
|
||||||
|
if taskID <= 0 {
|
||||||
|
return "无"
|
||||||
|
}
|
||||||
|
if state == nil {
|
||||||
|
return fmt.Sprintf("[%d]任务", taskID)
|
||||||
|
}
|
||||||
|
task := state.TaskByStateID(taskID)
|
||||||
|
if task == nil {
|
||||||
|
return fmt.Sprintf("[%d]任务", taskID)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("[%d]%s", task.StateID, fallbackText(task.Name, "未命名任务"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildTaskDurationLabel(task *schedule.ScheduleTask) string {
|
||||||
|
if task == nil {
|
||||||
|
return "未标注"
|
||||||
|
}
|
||||||
|
if task.Duration > 0 {
|
||||||
|
return fmt.Sprintf("%d 节", task.Duration)
|
||||||
|
}
|
||||||
|
total := 0
|
||||||
|
for _, slot := range task.Slots {
|
||||||
|
total += slot.SlotEnd - slot.SlotStart + 1
|
||||||
|
}
|
||||||
|
if total <= 0 {
|
||||||
|
return "未标注"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d 节", total)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildTaskDurationText(duration int) string {
|
||||||
|
if duration <= 0 {
|
||||||
|
return "未标注时长"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d 节连续时段", duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildTaskPlacementLabel(task *schedule.ScheduleTask) string {
|
||||||
|
if task == nil || len(task.Slots) == 0 {
|
||||||
|
return "尚未落位"
|
||||||
|
}
|
||||||
|
if len(task.Slots) == 1 {
|
||||||
|
slot := task.Slots[0]
|
||||||
|
return fmt.Sprintf("1 段(第%d天 第%d-%d节)", slot.Day, slot.SlotStart, slot.SlotEnd)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d 段", len(task.Slots))
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildTaskRelationLines(state *schedule.ScheduleState, task *schedule.ScheduleTask) []string {
|
||||||
|
if task == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
lines := make([]string, 0, 2)
|
||||||
|
if task.EmbeddedBy != nil {
|
||||||
|
lines = append(lines, "当前已嵌入任务:"+resolveTaskQueueLabelByID(state, *task.EmbeddedBy))
|
||||||
|
} else if task.CanEmbed {
|
||||||
|
lines = append(lines, "当前没有嵌入其他任务。")
|
||||||
|
}
|
||||||
|
if task.EmbedHost != nil {
|
||||||
|
lines = append(lines, "嵌入宿主:"+resolveTaskQueueLabelByID(state, *task.EmbedHost))
|
||||||
|
}
|
||||||
|
return lines
|
||||||
|
}
|
||||||
258
backend/newAgent/tools/工具结果结构化交接文档.md
Normal file
258
backend/newAgent/tools/工具结果结构化交接文档.md
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
# 工具结果结构化交接文档
|
||||||
|
|
||||||
|
## 当前状态
|
||||||
|
|
||||||
|
本轮已经完成第二批 read 事实域工具的后端结构化结果改造。外层协议仍然是 `ToolExecutionResult`,LLM 观察文本继续走 `ObservationText`,前端展示信息走 `ResultView` / `ArgumentView`。
|
||||||
|
|
||||||
|
已经直接切到 `result_view.view_type = "schedule.read_result"` 的工具:
|
||||||
|
|
||||||
|
1. `query_available_slots`
|
||||||
|
2. `query_target_tasks`
|
||||||
|
3. `query_range`
|
||||||
|
4. `get_overview`
|
||||||
|
5. `get_task_info`
|
||||||
|
6. `queue_status`
|
||||||
|
|
||||||
|
尚未进入本轮的工具继续走 `LegacyResult`,不要为了“统一外观”在本轮顺手迁移其它工具。
|
||||||
|
|
||||||
|
当前新增/修改文件:
|
||||||
|
|
||||||
|
1. `backend/newAgent/tools/schedule_read_result_types.go`
|
||||||
|
- read 结果常量、payload 结构、轻量内部结构。
|
||||||
|
2. `backend/newAgent/tools/schedule_read_result_common.go`
|
||||||
|
- 统一 `schedule.read_result` builder、失败卡片、中文格式化、跨工具统计 helper。
|
||||||
|
- 当前文件偏重,约 600 行,是下一轮整理的重点。
|
||||||
|
3. `backend/newAgent/tools/schedule_read_slots_handlers.go`
|
||||||
|
- `query_available_slots`、`query_range`。
|
||||||
|
4. `backend/newAgent/tools/schedule_read_tasks_handlers.go`
|
||||||
|
- `query_target_tasks`、`get_task_info`。
|
||||||
|
5. `backend/newAgent/tools/schedule_read_overview_queue_handlers.go`
|
||||||
|
- `get_overview`、`queue_status`。
|
||||||
|
6. `backend/newAgent/tools/execution_result.go`
|
||||||
|
- 补齐 read/analyze 相关参数的中文 `argument_view` 标签和展示值。
|
||||||
|
7. `backend/newAgent/tools/registry.go`
|
||||||
|
- 只把 6 个 read 工具的注册入口替换成新的 `NewXxxToolHandler()`。
|
||||||
|
|
||||||
|
## 第二批协议
|
||||||
|
|
||||||
|
第二批新增的前端 view type 只有一个:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"view_type": "schedule.read_result",
|
||||||
|
"version": 1,
|
||||||
|
"collapsed": {},
|
||||||
|
"expanded": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`collapsed` 面向卡片折叠态:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"title": "找到 6 个目标任务",
|
||||||
|
"subtitle": "建议安排任务,已入队 6 个",
|
||||||
|
"status": "done",
|
||||||
|
"status_label": "已完成",
|
||||||
|
"metrics": [
|
||||||
|
{ "label": "任务数", "value": "6 个" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`expanded` 面向卡片展开态:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"title": "[12]英语作文",
|
||||||
|
"subtitle": "待安排,2 节",
|
||||||
|
"tags": ["待安排", "学习"],
|
||||||
|
"detail_lines": ["位置:未安排"],
|
||||||
|
"meta": {
|
||||||
|
"task_id": 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"type": "items",
|
||||||
|
"title": "候选任务",
|
||||||
|
"items": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"raw_text": "原始 observation 文本",
|
||||||
|
"machine_payload": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
C 端默认展示字段:
|
||||||
|
|
||||||
|
1. `collapsed.title`
|
||||||
|
2. `collapsed.subtitle`
|
||||||
|
3. `collapsed.status_label`
|
||||||
|
4. `collapsed.metrics[].label/value`
|
||||||
|
5. `expanded.items[].title/subtitle/tags/detail_lines`
|
||||||
|
6. `expanded.sections[].title/summary/items`
|
||||||
|
|
||||||
|
默认不要展示的机器字段:
|
||||||
|
|
||||||
|
1. `expanded.machine_payload`
|
||||||
|
2. `expanded.raw_text`
|
||||||
|
3. `items[].meta`
|
||||||
|
4. `task_id`、`day`、`slot_start`、`week_filter` 等机器参数名
|
||||||
|
|
||||||
|
这些字段只给调试、回传、后续交互使用。
|
||||||
|
|
||||||
|
## 整理任务
|
||||||
|
|
||||||
|
当前第二批代码虽然已经拆成多个文件,但仍然平铺在 `backend/newAgent/tools` 根包里。后续整理目标是把 read 结果构造逻辑收到子目录,避免根目录继续膨胀。
|
||||||
|
|
||||||
|
不要直接把现有 `.go` 文件机械移动到子目录。Go 里子目录就是新 package;当前文件依赖父包里的 `ToolHandler`、`ToolExecutionResult`、`ToolDisplayView`、`LegacyResultWithState` 等类型,直接移动会造成 import cycle。
|
||||||
|
|
||||||
|
推荐整理结构:
|
||||||
|
|
||||||
|
```text
|
||||||
|
backend/newAgent/tools/
|
||||||
|
registry.go
|
||||||
|
schedule_read_handlers.go
|
||||||
|
schedule_read/
|
||||||
|
types.go
|
||||||
|
common.go
|
||||||
|
slots.go
|
||||||
|
tasks.go
|
||||||
|
overview_queue.go
|
||||||
|
```
|
||||||
|
|
||||||
|
职责边界:
|
||||||
|
|
||||||
|
1. `backend/newAgent/tools/schedule_read/**`
|
||||||
|
- 子包只做纯 read 展示数据构造。
|
||||||
|
- 不 import 父包 `newagenttools`。
|
||||||
|
- 不返回 `ToolExecutionResult`。
|
||||||
|
- 返回类似 `ReadResultView` 的纯数据结构:`ViewType`、`Collapsed`、`Expanded`、`MachinePayload`。
|
||||||
|
2. `backend/newAgent/tools/schedule_read_handlers.go`
|
||||||
|
- 留在父包 `newagenttools`。
|
||||||
|
- 只做薄 adapter:调用 `schedule_read` 子包构造展示数据,再包成 `ToolExecutionResult`。
|
||||||
|
- 继续保证 `ObservationText` 原样给 LLM。
|
||||||
|
3. `registry.go`
|
||||||
|
- 只保留注册入口,不放业务逻辑。
|
||||||
|
|
||||||
|
如果整理时发现 `schedule_read_result_common.go` 里的 helper 同时被第三批 analysis 使用,再考虑抽更中性的公共包:
|
||||||
|
|
||||||
|
```text
|
||||||
|
backend/newAgent/tools/toolview/
|
||||||
|
```
|
||||||
|
|
||||||
|
但不要提前大抽象;只有 read 和 analysis 都真实复用同一批结构后再抽。
|
||||||
|
|
||||||
|
## 第三批计划
|
||||||
|
|
||||||
|
第三批建议处理 schedule 诊断分析域:
|
||||||
|
|
||||||
|
1. `analyze_health`
|
||||||
|
2. `analyze_rhythm`
|
||||||
|
|
||||||
|
建议新增:
|
||||||
|
|
||||||
|
```text
|
||||||
|
backend/newAgent/tools/schedule_analysis/
|
||||||
|
```
|
||||||
|
|
||||||
|
建议新增 view type:
|
||||||
|
|
||||||
|
```text
|
||||||
|
schedule.analysis_result
|
||||||
|
```
|
||||||
|
|
||||||
|
外层协议继续不变:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"result_view": {
|
||||||
|
"view_type": "schedule.analysis_result",
|
||||||
|
"version": 1,
|
||||||
|
"collapsed": {},
|
||||||
|
"expanded": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`schedule.analysis_result` 仍复用通用卡片结构,但语义上区别于 read:
|
||||||
|
|
||||||
|
1. `collapsed.title`
|
||||||
|
- 例:`综合体检:建议继续微调`、`学习节律分析`
|
||||||
|
2. `collapsed.subtitle`
|
||||||
|
- 例:主问题、裁决摘要、风险摘要。
|
||||||
|
3. `collapsed.metrics`
|
||||||
|
- 例:高认知相邻天数、可局部移动任务数、推荐动作。
|
||||||
|
4. `expanded.sections`
|
||||||
|
- `裁决结论`
|
||||||
|
- `关键指标`
|
||||||
|
- `问题清单`
|
||||||
|
- `候选操作`
|
||||||
|
- `建议后续动作`
|
||||||
|
5. `expanded.items`
|
||||||
|
- 候选动作或风险日列表。
|
||||||
|
6. `expanded.machine_payload`
|
||||||
|
- 原始 JSON、候选动作参数、required_reads 等机器字段,只给调试/交互。
|
||||||
|
|
||||||
|
第三批不要和 read 整理同时改同一个公共 helper 文件。推荐顺序:
|
||||||
|
|
||||||
|
1. 先完成 read 整理,确定子包边界。
|
||||||
|
2. 再做 `schedule_analysis` 子包。
|
||||||
|
3. 最后只在父包 adapter 和 `registry.go` 接入 `analyze_health/analyze_rhythm`。
|
||||||
|
|
||||||
|
## 后续批次
|
||||||
|
|
||||||
|
第四批建议处理非 schedule read/analysis 主链:
|
||||||
|
|
||||||
|
1. `web_search`
|
||||||
|
2. `web_fetch`
|
||||||
|
3. `upsert_task_class`
|
||||||
|
4. `context_tools_add`
|
||||||
|
5. `context_tools_remove`
|
||||||
|
|
||||||
|
可考虑的 view type:
|
||||||
|
|
||||||
|
1. `web.search_result`
|
||||||
|
2. `web.fetch_result`
|
||||||
|
3. `taskclass.write_result`
|
||||||
|
4. `tool.context_result`
|
||||||
|
|
||||||
|
这些工具不建议混入 `schedule.read_result` 或 `schedule.analysis_result`,否则前端语义会越来越模糊。
|
||||||
|
|
||||||
|
## 前端补丁提示
|
||||||
|
|
||||||
|
第二批后端已经新增 `schedule.read_result`。前端最低要求:
|
||||||
|
|
||||||
|
1. 头部继续优先读:
|
||||||
|
- `result_view.collapsed.title`
|
||||||
|
- `result_view.collapsed.subtitle`
|
||||||
|
- `result_view.collapsed.status_label`
|
||||||
|
- `result_view.collapsed.metrics`
|
||||||
|
2. 展开态新增通用 sections/items renderer:
|
||||||
|
- 支持 `expanded.items`
|
||||||
|
- 支持 `expanded.sections`
|
||||||
|
- section 类型至少兼容 `items`、`kv`、`callout`
|
||||||
|
3. 默认不展示:
|
||||||
|
- `expanded.machine_payload`
|
||||||
|
- `items[].meta`
|
||||||
|
- 原始英文 key/value
|
||||||
|
4. `raw_text` 只放 debug 折叠区。
|
||||||
|
|
||||||
|
前端如果暂时不识别 `schedule.read_result`,至少能显示折叠态;但展开态会退回 raw text,不符合 C 端目标。
|
||||||
|
|
||||||
|
## 验收清单
|
||||||
|
|
||||||
|
后续代理继续处理前必须先确认:
|
||||||
|
|
||||||
|
1. 不回滚、覆盖、删除用户或其它代理的工作区改动。
|
||||||
|
2. 不碰前端,除非用户明确要求。
|
||||||
|
3. `ObservationText` 不能被展示层改写。
|
||||||
|
4. 工具结果必须继续通过 `ToolExecutionResult -> SSE extra -> timeline payload` 传递。
|
||||||
|
5. 每次跑 `go test` 后必须删除根目录 `.gocache`。
|
||||||
|
6. 如果新增临时 `*_test.go`,跑完测试后必须删除。
|
||||||
|
7. 结构迁移最终答复要说明:迁了什么、旧实现保留什么、切流点在哪里、下一轮建议迁什么。
|
||||||
|
|
||||||
Reference in New Issue
Block a user