后端: 1. 工具执行结果协议升级为结构化 ToolExecutionResult——execute/tool_runtime、ToolRegistry、stream extra 与 timeline 持久化统一改为透传 observation_text / summary / argument_view / result_view,不再只回写纯文本结果;context_tools、upsert_task_class 与旧 schedule/web 工具通过兼容包装接入新协议 2. 日程写工具注册继续收口——place / move / swap / batch_move / unplace / queue_apply_head_move 从 registry 内联实现下沉为独立 handler,降低注册表内参数解析与业务逻辑混写 3. 工具结果展示基础能力补齐——新增 execution_result / schedule_operation_handlers 公共件,为日程操作结果、参数本地化展示、blocked/failed/done 状态统一建模 前端: 4. AssistantPanel 接入结构化工具卡片渲染——新增 ToolCardRenderer,tool_call / tool_result 支持 argument_view / result_view 展示;schedule_completed 恢复为时间线内的占位卡片块,避免排程卡片脱离原消息顺序 5. 时间线类型与渲染收敛——schedule_agent.ts 补齐 ToolView 协议,AssistantPanel 改为按块渲染 tool / schedule_card / business_card,并移除旧 demo/prototype 路由与页面,收束正式面板代码路径 仓库: 6. AGENTS.md 新增协作约束——禁止擅自回滚、覆盖或删除用户/其他代理产生的工作区改动
731 lines
22 KiB
Go
731 lines
22 KiB
Go
package newagenttools
|
||
|
||
import (
|
||
"fmt"
|
||
"sort"
|
||
"strings"
|
||
|
||
"github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||
)
|
||
|
||
type scheduleTaskSnapshot struct {
|
||
Exists bool
|
||
TaskID int
|
||
Name string
|
||
Status string
|
||
Slots []schedule.TaskSlot
|
||
DayInfo map[int]schedule.DayMapping
|
||
}
|
||
|
||
type scheduleQueueSnapshot struct {
|
||
PendingCount int
|
||
CompletedCount int
|
||
SkippedCount int
|
||
CurrentTaskID int
|
||
CurrentAttempt int
|
||
LastError string
|
||
}
|
||
|
||
// NewPlaceToolHandler 返回 place 的结构化结果 handler(第一轮真实 result_view)。
|
||
func NewPlaceToolHandler() ToolHandler {
|
||
return func(state *schedule.ScheduleState, args map[string]any) ToolExecutionResult {
|
||
taskID, ok := schedule.ArgsInt(args, "task_id")
|
||
if !ok {
|
||
return buildScheduleArgErrorResult("place", args, "缺少必填参数 task_id。", state)
|
||
}
|
||
day, ok := schedule.ArgsInt(args, "day")
|
||
if !ok {
|
||
return buildScheduleArgErrorResult("place", args, "缺少必填参数 day。", state)
|
||
}
|
||
slotStart, ok := schedule.ArgsInt(args, "slot_start")
|
||
if !ok {
|
||
return buildScheduleArgErrorResult("place", args, "缺少必填参数 slot_start。", state)
|
||
}
|
||
if state == nil {
|
||
return buildScheduleArgErrorResult("place", args, "日程状态为空,无法执行预排。", nil)
|
||
}
|
||
|
||
beforeState := state.Clone()
|
||
observation := schedule.Place(state, taskID, day, slotStart)
|
||
afterState := state.Clone()
|
||
|
||
before := snapshotTask(beforeState, taskID)
|
||
after := snapshotTask(afterState, taskID)
|
||
success := after.Exists && taskHasSlotAt(after, day, slotStart) && !sameSlots(before.Slots, after.Slots)
|
||
|
||
changes := []map[string]any{
|
||
buildTaskChange("place", before, after),
|
||
}
|
||
affectedDays := collectAffectedDays(changes)
|
||
return buildScheduleOperationResult("place", args, afterState, observation, success, affectedDays, changes, nil, "")
|
||
}
|
||
}
|
||
|
||
// NewMoveToolHandler 返回 move 的结构化结果 handler(第一轮真实 result_view)。
|
||
func NewMoveToolHandler() ToolHandler {
|
||
return func(state *schedule.ScheduleState, args map[string]any) ToolExecutionResult {
|
||
taskID, ok := schedule.ArgsInt(args, "task_id")
|
||
if !ok {
|
||
return buildScheduleArgErrorResult("move", args, "缺少必填参数 task_id。", state)
|
||
}
|
||
newDay, ok := schedule.ArgsInt(args, "new_day")
|
||
if !ok {
|
||
return buildScheduleArgErrorResult("move", args, "缺少必填参数 new_day。", state)
|
||
}
|
||
newSlotStart, ok := schedule.ArgsInt(args, "new_slot_start")
|
||
if !ok {
|
||
return buildScheduleArgErrorResult("move", args, "缺少必填参数 new_slot_start。", state)
|
||
}
|
||
if state == nil {
|
||
return buildScheduleArgErrorResult("move", args, "日程状态为空,无法执行移动。", nil)
|
||
}
|
||
|
||
beforeState := state.Clone()
|
||
observation := schedule.Move(state, taskID, newDay, newSlotStart)
|
||
afterState := state.Clone()
|
||
|
||
before := snapshotTask(beforeState, taskID)
|
||
after := snapshotTask(afterState, taskID)
|
||
success := before.Exists && after.Exists && taskHasSlotAt(after, newDay, newSlotStart) && !sameSlots(before.Slots, after.Slots)
|
||
|
||
changes := []map[string]any{
|
||
buildTaskChange("move", before, after),
|
||
}
|
||
affectedDays := collectAffectedDays(changes)
|
||
return buildScheduleOperationResult("move", args, afterState, observation, success, affectedDays, changes, nil, "")
|
||
}
|
||
}
|
||
|
||
// NewSwapToolHandler 返回 swap 的结构化结果 handler(第一轮真实 result_view)。
|
||
func NewSwapToolHandler() ToolHandler {
|
||
return func(state *schedule.ScheduleState, args map[string]any) ToolExecutionResult {
|
||
taskA, ok := schedule.ArgsInt(args, "task_a")
|
||
if !ok {
|
||
return buildScheduleArgErrorResult("swap", args, "缺少必填参数 task_a。", state)
|
||
}
|
||
taskB, ok := schedule.ArgsInt(args, "task_b")
|
||
if !ok {
|
||
return buildScheduleArgErrorResult("swap", args, "缺少必填参数 task_b。", state)
|
||
}
|
||
if state == nil {
|
||
return buildScheduleArgErrorResult("swap", args, "日程状态为空,无法执行交换。", nil)
|
||
}
|
||
|
||
beforeState := state.Clone()
|
||
observation := schedule.Swap(state, taskA, taskB)
|
||
afterState := state.Clone()
|
||
|
||
beforeA := snapshotTask(beforeState, taskA)
|
||
afterA := snapshotTask(afterState, taskA)
|
||
beforeB := snapshotTask(beforeState, taskB)
|
||
afterB := snapshotTask(afterState, taskB)
|
||
|
||
success := beforeA.Exists &&
|
||
beforeB.Exists &&
|
||
afterA.Exists &&
|
||
afterB.Exists &&
|
||
sameSlots(beforeA.Slots, afterB.Slots) &&
|
||
sameSlots(beforeB.Slots, afterA.Slots)
|
||
|
||
changes := []map[string]any{
|
||
buildTaskChange("swap", beforeA, afterA),
|
||
buildTaskChange("swap", beforeB, afterB),
|
||
}
|
||
affectedDays := collectAffectedDays(changes)
|
||
return buildScheduleOperationResult("swap", args, afterState, observation, success, affectedDays, changes, nil, "")
|
||
}
|
||
}
|
||
|
||
// NewBatchMoveToolHandler 返回 batch_move 的结构化结果 handler(第一轮真实 result_view)。
|
||
func NewBatchMoveToolHandler() ToolHandler {
|
||
return func(state *schedule.ScheduleState, args map[string]any) ToolExecutionResult {
|
||
if state == nil {
|
||
return buildScheduleArgErrorResult("batch_move", args, "日程状态为空,无法执行批量移动。", nil)
|
||
}
|
||
moves, err := schedule.ArgsMoveList(args)
|
||
if err != nil {
|
||
return buildScheduleArgErrorResult("batch_move", args, err.Error(), state)
|
||
}
|
||
|
||
beforeState := state.Clone()
|
||
observation := schedule.BatchMove(state, moves)
|
||
afterState := state.Clone()
|
||
|
||
changes := make([]map[string]any, 0, len(moves))
|
||
success := len(moves) > 0
|
||
for _, move := range moves {
|
||
before := snapshotTask(beforeState, move.TaskID)
|
||
after := snapshotTask(afterState, move.TaskID)
|
||
changes = append(changes, buildTaskChange("batch_move", before, after))
|
||
if !after.Exists || !taskHasSlotAt(after, move.NewDay, move.NewSlotStart) {
|
||
success = false
|
||
}
|
||
}
|
||
|
||
affectedDays := collectAffectedDays(changes)
|
||
return buildScheduleOperationResult("batch_move", args, afterState, observation, success, affectedDays, changes, nil, "")
|
||
}
|
||
}
|
||
|
||
// NewUnplaceToolHandler 返回 unplace 的结构化结果 handler(第一轮真实 result_view)。
|
||
func NewUnplaceToolHandler() ToolHandler {
|
||
return func(state *schedule.ScheduleState, args map[string]any) ToolExecutionResult {
|
||
taskID, ok := schedule.ArgsInt(args, "task_id")
|
||
if !ok {
|
||
return buildScheduleArgErrorResult("unplace", args, "缺少必填参数 task_id。", state)
|
||
}
|
||
if state == nil {
|
||
return buildScheduleArgErrorResult("unplace", args, "日程状态为空,无法执行移出。", nil)
|
||
}
|
||
|
||
beforeState := state.Clone()
|
||
observation := schedule.Unplace(state, taskID)
|
||
afterState := state.Clone()
|
||
|
||
before := snapshotTask(beforeState, taskID)
|
||
after := snapshotTask(afterState, taskID)
|
||
success := before.Exists && len(before.Slots) > 0 && len(after.Slots) == 0 && strings.EqualFold(strings.TrimSpace(after.Status), schedule.TaskStatusPending)
|
||
|
||
changes := []map[string]any{
|
||
buildTaskChange("unplace", before, after),
|
||
}
|
||
affectedDays := collectAffectedDays(changes)
|
||
return buildScheduleOperationResult("unplace", args, afterState, observation, success, affectedDays, changes, nil, "")
|
||
}
|
||
}
|
||
|
||
// NewQueueApplyHeadMoveToolHandler 返回 queue_apply_head_move 的结构化结果 handler(第一轮真实 result_view)。
|
||
func NewQueueApplyHeadMoveToolHandler() ToolHandler {
|
||
return func(state *schedule.ScheduleState, args map[string]any) ToolExecutionResult {
|
||
newDay, dayOK := schedule.ArgsInt(args, "new_day")
|
||
newSlotStart, slotOK := schedule.ArgsInt(args, "new_slot_start")
|
||
if state == nil {
|
||
return buildScheduleArgErrorResult("queue_apply_head_move", args, "日程状态为空,无法执行队首任务应用。", nil)
|
||
}
|
||
|
||
// 1. 执行前先记录 current 任务与队列快照,保证成功/失败都可构造稳定结构化视图。
|
||
// 2. 再执行工具并抓取执行后快照,基于 before/after 计算差异,不依赖自然语言解析。
|
||
// 3. 如果快照构造异常,外层仍会回退 LegacyResult,保证工具主链路不被展示层影响。
|
||
beforeState := state.Clone()
|
||
beforeQueue := snapshotQueue(beforeState)
|
||
currentTaskID := 0
|
||
if beforeState != nil && beforeState.RuntimeQueue != nil {
|
||
currentTaskID = beforeState.RuntimeQueue.CurrentTaskID
|
||
}
|
||
beforeTask := snapshotTask(beforeState, currentTaskID)
|
||
|
||
observation := schedule.QueueApplyHeadMove(state, args)
|
||
|
||
afterState := state.Clone()
|
||
afterQueue := snapshotQueue(afterState)
|
||
afterTask := snapshotTask(afterState, currentTaskID)
|
||
|
||
success := false
|
||
if payload, ok := parseObservationJSON(strings.TrimSpace(observation)); ok {
|
||
if parsedSuccess, exists := payload["success"].(bool); exists {
|
||
success = parsedSuccess
|
||
}
|
||
}
|
||
if !success {
|
||
success = currentTaskID > 0 &&
|
||
(afterQueue.CompletedCount > beforeQueue.CompletedCount) &&
|
||
(afterQueue.CurrentTaskID != currentTaskID)
|
||
if dayOK && slotOK && success {
|
||
success = taskHasSlotAt(afterTask, newDay, newSlotStart)
|
||
}
|
||
}
|
||
|
||
changes := []map[string]any{
|
||
buildTaskChange("queue_apply_head_move", beforeTask, afterTask),
|
||
}
|
||
affectedDays := collectAffectedDays(changes)
|
||
queueSnapshot := buildQueueSnapshotWithLabels(beforeQueue, afterQueue)
|
||
return buildScheduleOperationResult(
|
||
"queue_apply_head_move",
|
||
args,
|
||
afterState,
|
||
observation,
|
||
success,
|
||
affectedDays,
|
||
changes,
|
||
queueSnapshot,
|
||
pickFailureReason(observation, success),
|
||
)
|
||
}
|
||
}
|
||
|
||
func buildScheduleArgErrorResult(toolName string, args map[string]any, reason string, state *schedule.ScheduleState) ToolExecutionResult {
|
||
observation := fmt.Sprintf("%s失败:%s", scheduleOperationFailurePrefix(toolName), strings.TrimSpace(reason))
|
||
return buildScheduleOperationResult(
|
||
toolName,
|
||
args,
|
||
state,
|
||
observation,
|
||
false,
|
||
nil,
|
||
nil,
|
||
nil,
|
||
strings.TrimSpace(reason),
|
||
)
|
||
}
|
||
|
||
func buildScheduleOperationResult(
|
||
toolName string,
|
||
args map[string]any,
|
||
displayState *schedule.ScheduleState,
|
||
observation string,
|
||
success bool,
|
||
affectedDays []int,
|
||
changes []map[string]any,
|
||
queueSnapshot map[string]any,
|
||
failureReason string,
|
||
) ToolExecutionResult {
|
||
result := LegacyResultWithState(toolName, args, displayState, observation)
|
||
|
||
status := ToolStatusFailed
|
||
if success {
|
||
status = ToolStatusDone
|
||
}
|
||
operationLabel := resolveOperationLabelCN(toolName)
|
||
title := fmt.Sprintf("%s%s", operationLabel, resolveResultTitleSuffix(status))
|
||
subtitle := buildScheduleSubtitle(toolName, changes, success)
|
||
metrics := []map[string]any{
|
||
{"label": "任务数量", "value": fmt.Sprintf("%d个", maxInt(len(changes), countMovesFromArgs(args)))},
|
||
{"label": "影响天数", "value": fmt.Sprintf("%d天", len(affectedDays))},
|
||
}
|
||
|
||
collapsed := map[string]any{
|
||
"title": title,
|
||
"subtitle": subtitle,
|
||
"status": status,
|
||
"status_label": resolveToolStatusLabelCN(status),
|
||
"operation": strings.TrimSpace(toolName),
|
||
"operation_label": operationLabel,
|
||
"task_count": maxInt(len(changes), countMovesFromArgs(args)),
|
||
"affected_days_count": len(affectedDays),
|
||
"metrics": metrics,
|
||
}
|
||
|
||
expanded := map[string]any{
|
||
"operation": strings.TrimSpace(toolName),
|
||
"operation_label": operationLabel,
|
||
"changes": changes,
|
||
"affected_days": affectedDays,
|
||
"affected_days_label": formatAffectedDaysLabel(affectedDays),
|
||
"raw_text": observation,
|
||
}
|
||
if queueSnapshot != nil {
|
||
expanded["queue_snapshot"] = queueSnapshot
|
||
}
|
||
if !success {
|
||
expanded["failure_reason"] = strings.TrimSpace(pickFailureReason(failureReason, false))
|
||
}
|
||
|
||
result.Status = status
|
||
result.Success = success
|
||
result.Summary = title
|
||
result.ArgumentsPreview = readArgumentSummary(result.ArgumentView)
|
||
result.ResultView = &ToolDisplayView{
|
||
ViewType: "schedule.operation_result",
|
||
Version: 1,
|
||
Collapsed: collapsed,
|
||
Expanded: expanded,
|
||
}
|
||
if !success {
|
||
result.ErrorCode = "schedule_operation_failed"
|
||
if strings.TrimSpace(result.ErrorMessage) == "" {
|
||
result.ErrorMessage = strings.TrimSpace(pickFailureReason(failureReason, false))
|
||
}
|
||
}
|
||
return EnsureToolResultDefaults(result, args)
|
||
}
|
||
|
||
func buildScheduleSubtitle(operation string, changes []map[string]any, success bool) string {
|
||
if len(changes) == 0 {
|
||
if success {
|
||
return fmt.Sprintf("%s已完成", resolveOperationLabelCN(operation))
|
||
}
|
||
return fmt.Sprintf("%s执行失败", resolveOperationLabelCN(operation))
|
||
}
|
||
|
||
firstTask := readStringMap(changes[0], "task_label")
|
||
firstBefore := readStringMap(changes[0], "before_label")
|
||
firstAfter := readStringMap(changes[0], "after_label")
|
||
|
||
switch strings.TrimSpace(operation) {
|
||
case "move":
|
||
return fmt.Sprintf("%s:从%s移动到%s", firstTask, firstBefore, firstAfter)
|
||
case "place":
|
||
return fmt.Sprintf("%s:预排到%s", firstTask, firstAfter)
|
||
case "unplace":
|
||
return fmt.Sprintf("%s:已从%s移出", firstTask, firstBefore)
|
||
case "swap":
|
||
if len(changes) >= 2 {
|
||
secondTask := readStringMap(changes[1], "task_label")
|
||
return fmt.Sprintf("%s 与 %s 已交换位置", firstTask, secondTask)
|
||
}
|
||
return fmt.Sprintf("%s已交换位置", firstTask)
|
||
case "batch_move":
|
||
return fmt.Sprintf("批量移动 %d 个任务", len(changes))
|
||
case "queue_apply_head_move":
|
||
if success {
|
||
return fmt.Sprintf("队首任务已移动到%s", firstAfter)
|
||
}
|
||
return fmt.Sprintf("队首任务移动失败:%s", firstTask)
|
||
default:
|
||
return fmt.Sprintf("%s:%s", resolveOperationLabelCN(operation), firstTask)
|
||
}
|
||
}
|
||
|
||
func resolveResultTitleSuffix(status string) string {
|
||
switch normalizeToolStatus(status) {
|
||
case ToolStatusDone:
|
||
return "成功"
|
||
case ToolStatusBlocked:
|
||
return "已阻断"
|
||
default:
|
||
return "失败"
|
||
}
|
||
}
|
||
|
||
func pickFailureReason(raw string, success bool) string {
|
||
if success {
|
||
return ""
|
||
}
|
||
trimmed := strings.TrimSpace(raw)
|
||
if trimmed == "" {
|
||
return "操作失败,请查看原始结果。"
|
||
}
|
||
if payload, ok := parseObservationJSON(trimmed); ok {
|
||
if text, ok := readStringFromMap(payload, "result", "error", "reason", "message", "err"); ok {
|
||
return strings.TrimSpace(text)
|
||
}
|
||
}
|
||
return trimmed
|
||
}
|
||
|
||
func snapshotTask(state *schedule.ScheduleState, taskID int) scheduleTaskSnapshot {
|
||
if state == nil || taskID <= 0 {
|
||
return scheduleTaskSnapshot{
|
||
Exists: false,
|
||
TaskID: taskID,
|
||
DayInfo: buildDayInfo(state),
|
||
}
|
||
}
|
||
task := state.TaskByStateID(taskID)
|
||
if task == nil {
|
||
return scheduleTaskSnapshot{
|
||
Exists: false,
|
||
TaskID: taskID,
|
||
DayInfo: buildDayInfo(state),
|
||
}
|
||
}
|
||
return scheduleTaskSnapshot{
|
||
Exists: true,
|
||
TaskID: task.StateID,
|
||
Name: strings.TrimSpace(task.Name),
|
||
Status: strings.TrimSpace(task.Status),
|
||
Slots: cloneSlots(task.Slots),
|
||
DayInfo: buildDayInfo(state),
|
||
}
|
||
}
|
||
|
||
func snapshotQueue(state *schedule.ScheduleState) scheduleQueueSnapshot {
|
||
if state == nil || state.RuntimeQueue == nil {
|
||
return scheduleQueueSnapshot{}
|
||
}
|
||
return scheduleQueueSnapshot{
|
||
PendingCount: len(state.RuntimeQueue.PendingTaskIDs),
|
||
CompletedCount: len(state.RuntimeQueue.CompletedTaskIDs),
|
||
SkippedCount: len(state.RuntimeQueue.SkippedTaskIDs),
|
||
CurrentTaskID: state.RuntimeQueue.CurrentTaskID,
|
||
CurrentAttempt: state.RuntimeQueue.CurrentAttempts,
|
||
LastError: strings.TrimSpace(state.RuntimeQueue.LastError),
|
||
}
|
||
}
|
||
|
||
func buildQueueSnapshotWithLabels(before scheduleQueueSnapshot, after scheduleQueueSnapshot) map[string]any {
|
||
return map[string]any{
|
||
"before": queueSnapshotToMap(before),
|
||
"after": queueSnapshotToMap(after),
|
||
"before_label": queueSummaryLabel(before),
|
||
"after_label": queueSummaryLabel(after),
|
||
"summary_label": queueSummaryLabel(after),
|
||
"last_error_label": strings.TrimSpace(after.LastError),
|
||
}
|
||
}
|
||
|
||
func queueSnapshotToMap(s scheduleQueueSnapshot) map[string]any {
|
||
return map[string]any{
|
||
"pending_count": s.PendingCount,
|
||
"completed_count": s.CompletedCount,
|
||
"skipped_count": s.SkippedCount,
|
||
"current_task_id": s.CurrentTaskID,
|
||
"current_attempt": s.CurrentAttempt,
|
||
"last_error": s.LastError,
|
||
}
|
||
}
|
||
|
||
func queueSummaryLabel(s scheduleQueueSnapshot) string {
|
||
return fmt.Sprintf(
|
||
"待处理%d个,已完成%d个,已跳过%d个,当前任务%d,尝试%d次",
|
||
s.PendingCount,
|
||
s.CompletedCount,
|
||
s.SkippedCount,
|
||
s.CurrentTaskID,
|
||
s.CurrentAttempt,
|
||
)
|
||
}
|
||
|
||
func buildTaskChange(operation string, before scheduleTaskSnapshot, after scheduleTaskSnapshot) map[string]any {
|
||
taskLabel := resolveChangeTaskLabel(before, after)
|
||
beforeStatusLabel := resolveTaskStatusLabelCN(before.Status)
|
||
afterStatusLabel := resolveTaskStatusLabelCN(after.Status)
|
||
beforeLabel := formatPlacementLabel(operation, before.Slots, before.Status, false, false)
|
||
afterLabel := formatPlacementLabel(operation, after.Slots, after.Status, true, len(before.Slots) > 0)
|
||
|
||
change := map[string]any{
|
||
"task_id": before.TaskID,
|
||
"name": firstNonEmpty(before.Name, after.Name),
|
||
"status": map[string]any{
|
||
"before": before.Status,
|
||
"after": after.Status,
|
||
},
|
||
"before_slots": slotsToView(before.Slots, before.DayInfo),
|
||
"after_slots": slotsToView(after.Slots, after.DayInfo),
|
||
"task_label": taskLabel,
|
||
"before_label": beforeLabel,
|
||
"after_label": afterLabel,
|
||
"status_label": fmt.Sprintf("%s -> %s", beforeStatusLabel, afterStatusLabel),
|
||
"operation_key": operation,
|
||
}
|
||
return change
|
||
}
|
||
|
||
func resolveChangeTaskLabel(before scheduleTaskSnapshot, after scheduleTaskSnapshot) string {
|
||
name := firstNonEmpty(before.Name, after.Name)
|
||
if name == "" {
|
||
if before.TaskID > 0 {
|
||
return fmt.Sprintf("[%d]任务", before.TaskID)
|
||
}
|
||
return "任务"
|
||
}
|
||
if before.TaskID > 0 {
|
||
return fmt.Sprintf("[%d]%s", before.TaskID, name)
|
||
}
|
||
return name
|
||
}
|
||
|
||
func resolveTaskStatusLabelCN(status string) string {
|
||
switch strings.ToLower(strings.TrimSpace(status)) {
|
||
case schedule.TaskStatusPending:
|
||
return "待安排"
|
||
case schedule.TaskStatusSuggested:
|
||
return "已预排"
|
||
case schedule.TaskStatusExisting:
|
||
return "已安排"
|
||
default:
|
||
return "未知状态"
|
||
}
|
||
}
|
||
|
||
func formatPlacementLabel(operation string, slots []schedule.TaskSlot, status string, isAfter bool, hadBefore bool) string {
|
||
if len(slots) > 0 {
|
||
return formatSlotsLabelCN(slots)
|
||
}
|
||
if isAfter && strings.TrimSpace(operation) == "unplace" && hadBefore {
|
||
return "已移出"
|
||
}
|
||
if strings.EqualFold(strings.TrimSpace(status), schedule.TaskStatusPending) {
|
||
return "未安排"
|
||
}
|
||
return "未安排"
|
||
}
|
||
|
||
func formatSlotsLabelCN(slots []schedule.TaskSlot) string {
|
||
if len(slots) == 0 {
|
||
return "未安排"
|
||
}
|
||
parts := make([]string, 0, len(slots))
|
||
for _, slot := range slots {
|
||
parts = append(parts, fmt.Sprintf("%s %s", formatDayLabelCN(slot.Day), formatSlotRangeCN(slot.SlotStart, slot.SlotEnd)))
|
||
}
|
||
return strings.Join(parts, "、")
|
||
}
|
||
|
||
func formatAffectedDaysLabel(affectedDays []int) string {
|
||
if len(affectedDays) == 0 {
|
||
return "无"
|
||
}
|
||
parts := make([]string, 0, len(affectedDays))
|
||
for _, day := range affectedDays {
|
||
parts = append(parts, formatDayLabelCN(day))
|
||
}
|
||
return strings.Join(parts, "、")
|
||
}
|
||
|
||
func slotsToView(slots []schedule.TaskSlot, dayInfo map[int]schedule.DayMapping) []map[string]any {
|
||
if len(slots) == 0 {
|
||
return make([]map[string]any, 0)
|
||
}
|
||
result := make([]map[string]any, 0, len(slots))
|
||
for _, slot := range slots {
|
||
entry := map[string]any{
|
||
"day": slot.Day,
|
||
"slot_start": slot.SlotStart,
|
||
"slot_end": slot.SlotEnd,
|
||
}
|
||
if info, ok := dayInfo[slot.Day]; ok {
|
||
entry["week"] = info.Week
|
||
entry["day_of_week"] = info.DayOfWeek
|
||
}
|
||
result = append(result, entry)
|
||
}
|
||
return result
|
||
}
|
||
|
||
func collectAffectedDays(changes []map[string]any) []int {
|
||
if len(changes) == 0 {
|
||
return make([]int, 0)
|
||
}
|
||
set := make(map[int]struct{})
|
||
for _, change := range changes {
|
||
collectDaysFromSlotView(set, change["before_slots"])
|
||
collectDaysFromSlotView(set, change["after_slots"])
|
||
}
|
||
days := make([]int, 0, len(set))
|
||
for day := range set {
|
||
days = append(days, day)
|
||
}
|
||
sort.Ints(days)
|
||
return days
|
||
}
|
||
|
||
func collectDaysFromSlotView(target map[int]struct{}, raw any) {
|
||
list, ok := raw.([]map[string]any)
|
||
if ok {
|
||
for _, item := range list {
|
||
day, ok := item["day"].(int)
|
||
if ok {
|
||
target[day] = struct{}{}
|
||
}
|
||
}
|
||
return
|
||
}
|
||
|
||
anyList, ok := raw.([]any)
|
||
if !ok {
|
||
return
|
||
}
|
||
for _, item := range anyList {
|
||
itemMap, ok := item.(map[string]any)
|
||
if !ok {
|
||
continue
|
||
}
|
||
switch day := itemMap["day"].(type) {
|
||
case int:
|
||
target[day] = struct{}{}
|
||
case float64:
|
||
target[int(day)] = struct{}{}
|
||
}
|
||
}
|
||
}
|
||
|
||
func buildDayInfo(state *schedule.ScheduleState) map[int]schedule.DayMapping {
|
||
if state == nil || len(state.Window.DayMapping) == 0 {
|
||
return map[int]schedule.DayMapping{}
|
||
}
|
||
info := make(map[int]schedule.DayMapping, len(state.Window.DayMapping))
|
||
for _, item := range state.Window.DayMapping {
|
||
info[item.DayIndex] = item
|
||
}
|
||
return info
|
||
}
|
||
|
||
func cloneSlots(slots []schedule.TaskSlot) []schedule.TaskSlot {
|
||
if len(slots) == 0 {
|
||
return nil
|
||
}
|
||
out := make([]schedule.TaskSlot, len(slots))
|
||
copy(out, slots)
|
||
return out
|
||
}
|
||
|
||
func sameSlots(a []schedule.TaskSlot, b []schedule.TaskSlot) bool {
|
||
if len(a) != len(b) {
|
||
return false
|
||
}
|
||
for i := range a {
|
||
if a[i].Day != b[i].Day || a[i].SlotStart != b[i].SlotStart || a[i].SlotEnd != b[i].SlotEnd {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
func taskHasSlotAt(snapshot scheduleTaskSnapshot, day int, slotStart int) bool {
|
||
for _, slot := range snapshot.Slots {
|
||
if slot.Day == day && slot.SlotStart == slotStart {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func readStringMap(input map[string]any, key string) string {
|
||
value, ok := input[key]
|
||
if !ok || value == nil {
|
||
return ""
|
||
}
|
||
text, _ := value.(string)
|
||
return strings.TrimSpace(text)
|
||
}
|
||
|
||
func countMovesFromArgs(args map[string]any) int {
|
||
moves, ok := args["moves"].([]any)
|
||
if !ok {
|
||
return 0
|
||
}
|
||
return len(moves)
|
||
}
|
||
|
||
func maxInt(values ...int) int {
|
||
if len(values) == 0 {
|
||
return 0
|
||
}
|
||
maxValue := values[0]
|
||
for _, value := range values[1:] {
|
||
if value > maxValue {
|
||
maxValue = value
|
||
}
|
||
}
|
||
return maxValue
|
||
}
|
||
|
||
func firstNonEmpty(values ...string) string {
|
||
for _, value := range values {
|
||
trimmed := strings.TrimSpace(value)
|
||
if trimmed != "" {
|
||
return trimmed
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func scheduleOperationFailurePrefix(toolName string) string {
|
||
switch strings.TrimSpace(toolName) {
|
||
case "place":
|
||
return "放置"
|
||
case "move", "queue_apply_head_move":
|
||
return "移动"
|
||
case "swap":
|
||
return "交换"
|
||
case "batch_move":
|
||
return "批量移动"
|
||
case "unplace":
|
||
return "移除"
|
||
default:
|
||
return strings.TrimSpace(toolName)
|
||
}
|
||
}
|