后端: 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 配置段 前端:无 仓库:无
273 lines
9.3 KiB
Go
273 lines
9.3 KiB
Go
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)
|
||
}
|