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 配置段 前端:无 仓库:无
This commit is contained in:
272
backend/newAgent/tools/schedule/queue_tools.go
Normal file
272
backend/newAgent/tools/schedule/queue_tools.go
Normal file
@@ -0,0 +1,272 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user