Files
smartmate/backend/newAgent/tools/schedule/queue_tools.go
Losita 070d4c3459 Version: 0.9.15.dev.260412
后端:
1. 排程工具从 tools/ 根目录拆分为 tools/schedule 独立子包
- 12 个排程工具文件等价迁入 tools/schedule/,tools/ 根目录仅保留 registry.go 作为统一注册入口
- 所有依赖方(conv / model / node / prompt / service)import 统一切到 schedule 子包
2. Web 搜索工具链落地(tools/web 子包)
- 新增 web_search(结构化检索)与 web_fetch(正文抓取)两个读工具,支持博查 API / mock 降级
- 启动流程按配置选择 provider,未识别类型自动降级为 mock,不阻断主流程
- 执行提示补齐 web 工具使用约束与返回值示例
- config.example.yaml 补齐 websearch 配置段
前端:无
仓库:无
2026-04-12 19:02:54 +08:00

273 lines
9.3 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 schedule
import (
"encoding/json"
"fmt"
"strings"
)
type queueTaskSlot 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 queueTaskItem struct {
TaskID int `json:"task_id"`
Name string `json:"name"`
Category string `json:"category,omitempty"`
Status string `json:"status"`
Duration int `json:"duration,omitempty"`
TaskClassID int `json:"task_class_id,omitempty"`
Slots []queueTaskSlot `json:"slots,omitempty"`
}
type queuePopHeadResult struct {
Tool string `json:"tool"`
HasHead bool `json:"has_head"`
PendingCount int `json:"pending_count"`
CompletedCount int `json:"completed_count"`
SkippedCount int `json:"skipped_count"`
Current *queueTaskItem `json:"current,omitempty"`
LastError string `json:"last_error,omitempty"`
}
type queueApplyHeadMoveResult struct {
Tool string `json:"tool"`
Success bool `json:"success"`
TaskID int `json:"task_id,omitempty"`
CurrentAttempt int `json:"current_attempt,omitempty"`
PendingCount int `json:"pending_count"`
CompletedCount int `json:"completed_count"`
SkippedCount int `json:"skipped_count"`
Result string `json:"result"`
}
type queueSkipHeadResult struct {
Tool string `json:"tool"`
Success bool `json:"success"`
SkippedTaskID int `json:"skipped_task_id,omitempty"`
PendingCount int `json:"pending_count"`
SkippedCount int `json:"skipped_count"`
Reason string `json:"reason,omitempty"`
}
type queueStatusResult struct {
Tool string `json:"tool"`
PendingCount int `json:"pending_count"`
CompletedCount int `json:"completed_count"`
SkippedCount int `json:"skipped_count"`
CurrentTaskID int `json:"current_task_id,omitempty"`
CurrentAttempt int `json:"current_attempt,omitempty"`
LastError string `json:"last_error,omitempty"`
NextTaskIDs []int `json:"next_task_ids,omitempty"`
Current *queueTaskItem `json:"current,omitempty"`
}
// QueuePopHead 从队列弹出队首任务(若已有 current 则复用),并返回当前处理对象。
//
// 步骤化说明:
// 1. 先保证队列容器存在,避免空指针;
// 2. 若 current 已存在,直接复用,确保 apply/skip 前不会切换处理对象;
// 3. 若 current 为空则从 pending 弹出队首;
// 4. 若没有可处理任务,返回 has_head=false由 LLM 收口或重筛选。
func QueuePopHead(state *ScheduleState, _ map[string]any) string {
if state == nil {
return `{"tool":"queue_pop_head","has_head":false,"error":"state is nil"}`
}
queue := ensureTaskProcessingQueue(state)
taskID := popOrGetCurrentTaskID(state)
result := queuePopHeadResult{
Tool: "queue_pop_head",
HasHead: taskID > 0,
PendingCount: len(queue.PendingTaskIDs),
CompletedCount: len(queue.CompletedTaskIDs),
SkippedCount: len(queue.SkippedTaskIDs),
LastError: strings.TrimSpace(queue.LastError),
}
if taskID > 0 {
result.Current = buildQueueTaskItem(state, taskID)
}
return mustJSON(result, "queue_pop_head")
}
// QueueApplyHeadMove 将当前队首任务移动到指定位置,成功后自动完成并出队。
//
// 步骤化说明:
// 1. 只能处理 current 任务,禁止越级指定 task_id避免 LLM 绕过队列直接乱改;
// 2. 成功时标记 completed 并清空 current
// 3. 失败时保留 current 并累加 attempt让 LLM 继续换坑位重试或 skip。
func QueueApplyHeadMove(state *ScheduleState, args map[string]any) string {
if state == nil {
return `{"tool":"queue_apply_head_move","success":false,"result":"state is nil"}`
}
queue := ensureTaskProcessingQueue(state)
currentID := queue.CurrentTaskID
if currentID <= 0 {
return mustJSON(queueApplyHeadMoveResult{
Tool: "queue_apply_head_move",
Success: false,
PendingCount: len(queue.PendingTaskIDs),
CompletedCount: len(queue.CompletedTaskIDs),
SkippedCount: len(queue.SkippedTaskIDs),
Result: "队列中没有正在处理的任务。请先调用 queue_pop_head。",
}, "queue_apply_head_move")
}
newDay, ok := ArgsInt(args, "new_day")
if !ok {
return mustJSON(queueApplyHeadMoveResult{
Tool: "queue_apply_head_move",
Success: false,
TaskID: currentID,
CurrentAttempt: queue.CurrentAttempts,
PendingCount: len(queue.PendingTaskIDs),
CompletedCount: len(queue.CompletedTaskIDs),
SkippedCount: len(queue.SkippedTaskIDs),
Result: "缺少必填参数 new_day。",
}, "queue_apply_head_move")
}
newSlotStart, ok := ArgsInt(args, "new_slot_start")
if !ok {
return mustJSON(queueApplyHeadMoveResult{
Tool: "queue_apply_head_move",
Success: false,
TaskID: currentID,
CurrentAttempt: queue.CurrentAttempts,
PendingCount: len(queue.PendingTaskIDs),
CompletedCount: len(queue.CompletedTaskIDs),
SkippedCount: len(queue.SkippedTaskIDs),
Result: "缺少必填参数 new_slot_start。",
}, "queue_apply_head_move")
}
// 1. 真正执行仍复用既有 move 校验链路,避免重复实现一套冲突判断。
// 2. 失败时仅更新队列 attempt不改 current确保同一任务可继续重试。
resultText := Move(state, currentID, newDay, newSlotStart)
success := !strings.Contains(resultText, "移动失败")
if success {
markCurrentTaskCompleted(state)
} else {
bumpCurrentTaskAttempt(state, resultText)
}
queue = ensureTaskProcessingQueue(state)
return mustJSON(queueApplyHeadMoveResult{
Tool: "queue_apply_head_move",
Success: success,
TaskID: currentID,
CurrentAttempt: queue.CurrentAttempts,
PendingCount: len(queue.PendingTaskIDs),
CompletedCount: len(queue.CompletedTaskIDs),
SkippedCount: len(queue.SkippedTaskIDs),
Result: strings.TrimSpace(resultText),
}, "queue_apply_head_move")
}
// QueueSkipHead 跳过当前队首任务。
//
// 职责边界:
// 1. 只修改队列运行态,不改排程结果;
// 2. current 必须存在,否则返回失败提示;
// 3. 跳过后由下一轮 queue_pop_head 继续取下一项。
func QueueSkipHead(state *ScheduleState, args map[string]any) string {
if state == nil {
return `{"tool":"queue_skip_head","success":false,"reason":"state is nil"}`
}
queue := ensureTaskProcessingQueue(state)
currentID := queue.CurrentTaskID
if currentID <= 0 {
return mustJSON(queueSkipHeadResult{
Tool: "queue_skip_head",
Success: false,
PendingCount: len(queue.PendingTaskIDs),
SkippedCount: len(queue.SkippedTaskIDs),
Reason: "没有可跳过的 current 任务,请先 queue_pop_head。",
}, "queue_skip_head")
}
reason := ""
if raw, ok := ArgsString(args, "reason"); ok {
reason = strings.TrimSpace(raw)
}
markCurrentTaskSkipped(state)
queue = ensureTaskProcessingQueue(state)
return mustJSON(queueSkipHeadResult{
Tool: "queue_skip_head",
Success: true,
SkippedTaskID: currentID,
PendingCount: len(queue.PendingTaskIDs),
SkippedCount: len(queue.SkippedTaskIDs),
Reason: reason,
}, "queue_skip_head")
}
// QueueStatus 查询当前队列状态。
func QueueStatus(state *ScheduleState, _ map[string]any) string {
if state == nil {
return `{"tool":"queue_status","pending_count":0,"completed_count":0,"skipped_count":0,"last_error":"state is nil"}`
}
queue := ensureTaskProcessingQueue(state)
nextIDs := queue.PendingTaskIDs
if len(nextIDs) > 5 {
nextIDs = nextIDs[:5]
}
result := queueStatusResult{
Tool: "queue_status",
PendingCount: len(queue.PendingTaskIDs),
CompletedCount: len(queue.CompletedTaskIDs),
SkippedCount: len(queue.SkippedTaskIDs),
CurrentTaskID: queue.CurrentTaskID,
CurrentAttempt: queue.CurrentAttempts,
LastError: strings.TrimSpace(queue.LastError),
NextTaskIDs: append([]int(nil), nextIDs...),
}
if queue.CurrentTaskID > 0 {
result.Current = buildQueueTaskItem(state, queue.CurrentTaskID)
}
return mustJSON(result, "queue_status")
}
// buildQueueTaskItem 构造队列任务快照,供 pop/status 返回。
func buildQueueTaskItem(state *ScheduleState, taskID int) *queueTaskItem {
task := state.TaskByStateID(taskID)
if task == nil {
return nil
}
item := &queueTaskItem{
TaskID: task.StateID,
Name: strings.TrimSpace(task.Name),
Category: strings.TrimSpace(task.Category),
Status: buildTaskStatusLabel(*task),
Duration: task.Duration,
TaskClassID: task.TaskClassID,
Slots: make([]queueTaskSlot, 0, len(task.Slots)),
}
for _, slot := range task.Slots {
week, dayOfWeek, ok := state.DayToWeekDay(slot.Day)
if !ok {
continue
}
item.Slots = append(item.Slots, queueTaskSlot{
Day: slot.Day,
Week: week,
DayOfWeek: dayOfWeek,
SlotStart: slot.SlotStart,
SlotEnd: slot.SlotEnd,
})
}
return item
}
func mustJSON(v any, toolName string) string {
raw, err := json.Marshal(v)
if err != nil {
return fmt.Sprintf(`{"tool":"%s","success":false,"error":"json encode failed"}`, toolName)
}
return string(raw)
}