Files
smartmate/backend/newAgent/tools/schedule_operation_handlers.go
LoveLosita 509e266626 Version: 0.9.50.dev.260428
后端:
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 新增协作约束——禁止擅自回滚、覆盖或删除用户/其他代理产生的工作区改动
2026-04-28 11:55:34 +08:00

731 lines
22 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package 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)
}
}