Version: 0.9.46.dev.260427
后端: 1. taskclass 执行闭环继续收紧——Plan / Execute 全面切到“最小工具闭环”视角,明确学习目标/总节数/禁排时段/排除星期默认停留 taskclass 域;未给日期范围时禁止擅自补 start_date/end_date,upsert_task_class 重试前先做写前检查并区分“内部表示修正”与“必须追问用户”的关键时间事实 2. QuickTask / TaskQuery 轻量链路继续收敛——新增 model/taskquery_contract.go 统一查询协议,QuickTaskDeps / start.go 改用 model 层参数;删除 query_tasks / quick_note_create 旧工具实现,避免任务查询与随口记再回流 execute 工具链 3. schedule 微调工具继续瘦身——下线 spread_even / min_context_switch 及其复合规划逻辑,清理 analyze_load / analyze_subjects / analyze_context / analyze_tolerance 等历史能力;execute 顺序策略收敛为局部 move / swap,提示词与工具目录仅暴露当前真实可用工具 4. 执行与时间线体验补齐——execute 为流式 speak 补发归一化尾部,避免 deliver 文案黏连;前端时间线新增 interrupt / status 协议识别、工具事件归并与状态过滤,减少 ToolTrace 重复和会话重建误判 前端: 5. AssistantPanel 适配新版 timeline extra 事件——schedule_agent.ts 补齐 interrupt / status kind,工具调用与结果按摘要/参数/工具名合并,恢复历史时不再把协议事件误判成用户消息
This commit is contained in:
@@ -1,139 +0,0 @@
|
||||
package newagenttools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
newagentshared "github.com/LoveLosita/smartflow/backend/newAgent/shared"
|
||||
"github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||||
)
|
||||
|
||||
// QuickNoteDeps 描述随口记工具所需的外部依赖。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. CreateTask 负责真正写库,工具层不直接依赖 DAO;
|
||||
// 2. UserID 由 execute 节点通过 args["_user_id"] 注入,工具层不自行解析会话身份。
|
||||
type QuickNoteDeps struct {
|
||||
// CreateTask 将解析后的任务字段写入数据库。
|
||||
// 调用目的:解耦工具层与 DAO 层,方便测试和替换。
|
||||
CreateTask func(userID int, title string, priorityGroup int, deadlineAt *time.Time) (taskID int, err error)
|
||||
}
|
||||
|
||||
// QuickNoteCreateResult 是 quick_note_create 工具的结构化返回。
|
||||
type QuickNoteCreateResult struct {
|
||||
TaskID int `json:"task_id"`
|
||||
Title string `json:"title"`
|
||||
PriorityLabel string `json:"priority_label"`
|
||||
DeadlineAt string `json:"deadline_at,omitempty"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// quickNoteFallbackPriority 根据截止时间推断默认优先级。
|
||||
//
|
||||
// 推断规则:
|
||||
// 1. 有截止时间且距今 ≤48h → 1(重要且紧急);
|
||||
// 2. 有截止时间且距今 >48h → 2(重要不紧急);
|
||||
// 3. 无截止时间 → 3(简单不重要)。
|
||||
func quickNoteFallbackPriority(deadline *time.Time) int {
|
||||
if deadline != nil {
|
||||
if time.Until(*deadline) <= 48*time.Hour {
|
||||
return newagentshared.QuickNotePriorityImportantUrgent
|
||||
}
|
||||
return newagentshared.QuickNotePriorityImportantNotUrgent
|
||||
}
|
||||
return newagentshared.QuickNotePrioritySimpleNotImportant
|
||||
}
|
||||
|
||||
// NewQuickNoteToolHandler 创建 quick_note_create 工具的 handler 闭包。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责参数校验、时间解析、优先级推断、调 deps 写库、组装返回;
|
||||
// 2. 不负责 LLM 交互和会话管理。
|
||||
// 3. state 参数忽略——随口记不需要 ScheduleState,已注册到 scheduleFreeTools。
|
||||
func NewQuickNoteToolHandler(deps QuickNoteDeps) ToolHandler {
|
||||
return func(state *schedule.ScheduleState, args map[string]any) string {
|
||||
_ = state
|
||||
|
||||
// 1. 提取 _user_id(由 execute 节点在调用前注入)。
|
||||
userID := 0
|
||||
if uid, ok := args["_user_id"].(int); ok {
|
||||
userID = uid
|
||||
}
|
||||
if userID <= 0 {
|
||||
return "工具调用失败:无法识别用户身份。"
|
||||
}
|
||||
|
||||
// 2. 提取必填参数 title。
|
||||
title := ""
|
||||
if t, ok := args["title"].(string); ok {
|
||||
title = strings.TrimSpace(t)
|
||||
}
|
||||
if title == "" {
|
||||
return "工具调用失败:缺少必填参数 title(任务标题)。"
|
||||
}
|
||||
|
||||
// 3. 提取可选参数 deadline_at,复用旧链路时间解析能力。
|
||||
var deadline *time.Time
|
||||
if raw, ok := args["deadline_at"].(string); ok {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw != "" {
|
||||
// 调用目的:复用旧链路成熟的中文相对时间解析器,支持"明天下午3点"等格式。
|
||||
parsed, err := newagentshared.ParseOptionalDeadline(raw)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("工具调用失败:截止时间格式无法解析(%s)。支持格式:2026-04-20 18:00、明天下午3点、下周一上午9点。", err)
|
||||
}
|
||||
deadline = parsed
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 提取可选参数 priority_group;未提供时按截止时间自动推断。
|
||||
priorityGroup := 0
|
||||
if pg, ok := args["priority_group"].(float64); ok {
|
||||
priorityGroup = int(pg)
|
||||
}
|
||||
if !newagentshared.IsValidTaskPriority(priorityGroup) {
|
||||
priorityGroup = quickNoteFallbackPriority(deadline)
|
||||
}
|
||||
|
||||
// 5. 调用依赖写库。
|
||||
taskID, err := deps.CreateTask(userID, title, priorityGroup, deadline)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("工具调用失败:写入任务时出错(%s)。", err)
|
||||
}
|
||||
if taskID <= 0 {
|
||||
return "工具调用失败:写入任务后未返回有效 task_id。"
|
||||
}
|
||||
|
||||
// 6. 组装结构化返回,包含 banter 提示引导 LLM 自然生成调侃。
|
||||
priorityLabel := newagentshared.PriorityLabelCN(priorityGroup)
|
||||
deadlineStr := ""
|
||||
if deadline != nil {
|
||||
deadlineStr = deadline.In(newagentshared.ShanghaiLocation()).Format("2006-01-02 15:04")
|
||||
}
|
||||
|
||||
result := QuickNoteCreateResult{
|
||||
TaskID: taskID,
|
||||
Title: title,
|
||||
PriorityLabel: priorityLabel,
|
||||
DeadlineAt: deadlineStr,
|
||||
}
|
||||
|
||||
// 6.1 成功事实 + banter 提示:通过工具返回值引导 ReAct LLM 在 speak 中自然加入轻松跟进。
|
||||
if deadlineStr != "" {
|
||||
result.Message = fmt.Sprintf("已记录:%s(%s,截止 %s)。回复时请用轻松友好的语气,加一句与任务内容相关的俏皮话(不超过30字)。",
|
||||
title, priorityLabel, deadlineStr)
|
||||
} else {
|
||||
result.Message = fmt.Sprintf("已记录:%s(%s)。回复时请用轻松友好的语气,加一句与任务内容相关的俏皮话(不超过30字)。",
|
||||
title, priorityLabel)
|
||||
}
|
||||
|
||||
jsonBytes, marshalErr := json.Marshal(result)
|
||||
if marshalErr != nil {
|
||||
// 6.2 JSON 序列化失败时降级为纯文本,确保 LLM 仍能拿到关键信息。
|
||||
return result.Message
|
||||
}
|
||||
return string(jsonBytes)
|
||||
}
|
||||
}
|
||||
@@ -52,14 +52,7 @@ type ToolRegistry struct {
|
||||
// 1. 这些工具仍保留定义,避免 prompt / 旧链路 / 历史日志里出现悬空名字;
|
||||
// 2. execute 会在调用前统一阻断,并向模型返回纠错提示;
|
||||
// 3. ToolNames / Schemas 也会默认隐藏它们,避免继续污染 msg0。
|
||||
var temporaryDisabledTools = map[string]bool{
|
||||
"min_context_switch": true,
|
||||
"spread_even": true,
|
||||
"analyze_load": true,
|
||||
"analyze_subjects": true,
|
||||
"analyze_context": true,
|
||||
"analyze_tolerance": true,
|
||||
}
|
||||
var temporaryDisabledTools = map[string]bool{}
|
||||
|
||||
// IsTemporarilyDisabledTool 判断工具是否在当前阶段被临时禁用。
|
||||
func IsTemporarilyDisabledTool(name string) bool {
|
||||
@@ -232,8 +225,6 @@ var writeTools = map[string]bool{
|
||||
"swap": true,
|
||||
"batch_move": true,
|
||||
"queue_apply_head_move": true,
|
||||
"spread_even": true,
|
||||
"min_context_switch": true,
|
||||
"unplace": true,
|
||||
"upsert_task_class": true,
|
||||
}
|
||||
@@ -244,8 +235,6 @@ var scheduleMutationTools = map[string]bool{
|
||||
"swap": true,
|
||||
"batch_move": true,
|
||||
"queue_apply_head_move": true,
|
||||
"spread_even": true,
|
||||
"min_context_switch": true,
|
||||
"unplace": true,
|
||||
}
|
||||
|
||||
@@ -368,30 +357,6 @@ func registerScheduleReadTools(r *ToolRegistry) {
|
||||
}
|
||||
|
||||
func registerScheduleAnalyzeTools(r *ToolRegistry) {
|
||||
r.Register(
|
||||
"analyze_load",
|
||||
"分析整体负载分布(当前阶段已临时禁用,仅保留定义)。",
|
||||
`{"name":"analyze_load","parameters":{"scope":{"type":"string","enum":["full","week","day_range"]},"week_from":{"type":"int"},"week_to":{"type":"int"},"day_from":{"type":"int"},"day_to":{"type":"int"},"granularity":{"type":"string","enum":["day","week","time_of_day"]},"detail":{"type":"string","enum":["summary","full"]}}}`,
|
||||
func(state *schedule.ScheduleState, args map[string]any) string {
|
||||
return schedule.AnalyzeLoad(state, args)
|
||||
},
|
||||
)
|
||||
r.Register(
|
||||
"analyze_subjects",
|
||||
"分析学科分布与连贯性(当前阶段已临时禁用,仅保留定义)。",
|
||||
`{"name":"analyze_subjects","parameters":{"category":{"type":"string"},"include_pending":{"type":"bool"},"detail":{"type":"string","enum":["summary","full"]}}}`,
|
||||
func(state *schedule.ScheduleState, args map[string]any) string {
|
||||
return schedule.AnalyzeSubjects(state, args)
|
||||
},
|
||||
)
|
||||
r.Register(
|
||||
"analyze_context",
|
||||
"分析上下文切换与相邻关系(当前阶段已临时禁用,仅保留定义)。",
|
||||
`{"name":"analyze_context","parameters":{"day_from":{"type":"int"},"day_to":{"type":"int"},"detail":{"type":"string","enum":["summary","day_detail"]},"hard_categories":{"type":"array","items":{"type":"string"}}}}`,
|
||||
func(state *schedule.ScheduleState, args map[string]any) string {
|
||||
return schedule.AnalyzeContext(state, args)
|
||||
},
|
||||
)
|
||||
r.Register(
|
||||
"analyze_rhythm",
|
||||
"分析学习节奏与切换情况。",
|
||||
@@ -400,14 +365,6 @@ func registerScheduleAnalyzeTools(r *ToolRegistry) {
|
||||
return schedule.AnalyzeRhythm(state, args)
|
||||
},
|
||||
)
|
||||
r.Register(
|
||||
"analyze_tolerance",
|
||||
"分析局部容错与调整空间。",
|
||||
`{"name":"analyze_tolerance","parameters":{"scope":{"type":"string","enum":["full","week","day_range"]},"week_from":{"type":"int"},"week_to":{"type":"int"},"day_from":{"type":"int"},"day_to":{"type":"int"},"min_usable_size":{"type":"int"},"min_daily_buffer":{"type":"int"},"detail":{"type":"string","enum":["summary","full"]}}}`,
|
||||
func(state *schedule.ScheduleState, args map[string]any) string {
|
||||
return schedule.AnalyzeTolerance(state, args)
|
||||
},
|
||||
)
|
||||
r.Register(
|
||||
"analyze_health",
|
||||
"主动优化裁判入口:聚焦 rhythm/semantic_profile/tightness,判断当前是否还值得继续优化,并给出候选。",
|
||||
@@ -503,30 +460,6 @@ func registerScheduleMutationTools(r *ToolRegistry) {
|
||||
return schedule.QueueSkipHead(state, args)
|
||||
},
|
||||
)
|
||||
r.Register(
|
||||
"min_context_switch",
|
||||
"在指定任务集合内减少上下文切换(当前阶段已临时禁用,仅保留定义)。",
|
||||
`{"name":"min_context_switch","parameters":{"task_ids":{"type":"array","required":true,"items":{"type":"int"}},"task_id":{"type":"int"}}}`,
|
||||
func(state *schedule.ScheduleState, args map[string]any) string {
|
||||
taskIDs, err := schedule.ParseMinContextSwitchTaskIDs(args)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("减少上下文切换失败:%s。", err.Error())
|
||||
}
|
||||
return schedule.MinContextSwitch(state, taskIDs)
|
||||
},
|
||||
)
|
||||
r.Register(
|
||||
"spread_even",
|
||||
"在给定任务集合内做均匀化铺开(当前阶段已临时禁用,仅保留定义)。",
|
||||
`{"name":"spread_even","parameters":{"task_ids":{"type":"array","required":true,"items":{"type":"int"}},"task_id":{"type":"int"},"limit":{"type":"int"},"allow_embed":{"type":"bool"},"day":{"type":"int"},"day_start":{"type":"int"},"day_end":{"type":"int"},"day_scope":{"type":"string","enum":["all","workday","weekend"]},"day_of_week":{"type":"array","items":{"type":"int"}},"week":{"type":"int"},"week_filter":{"type":"array","items":{"type":"int"}},"week_from":{"type":"int"},"week_to":{"type":"int"},"slot_type":{"type":"string"},"slot_types":{"type":"array","items":{"type":"string"}},"exclude_sections":{"type":"array","items":{"type":"int"}},"after_section":{"type":"int"},"before_section":{"type":"int"}}}`,
|
||||
func(state *schedule.ScheduleState, args map[string]any) string {
|
||||
taskIDs, err := schedule.ParseSpreadEvenTaskIDs(args)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("均匀化调整失败:%s。", err.Error())
|
||||
}
|
||||
return schedule.SpreadEven(state, taskIDs, args)
|
||||
},
|
||||
)
|
||||
r.Register(
|
||||
"unplace",
|
||||
"将一个已落位任务移除,恢复为待安排状态。task_id 必填。",
|
||||
|
||||
@@ -165,26 +165,6 @@ type analyzeHealthMetrics struct {
|
||||
CanClose bool `json:"can_close"`
|
||||
}
|
||||
|
||||
// AnalyzeLoad 已退出主动优化主链路。
|
||||
func AnalyzeLoad(state *ScheduleState, args map[string]any) string {
|
||||
return encodeAnalyzeFailure("analyze_load", "deprecated", "analyze_load 已退出主动优化链路")
|
||||
}
|
||||
|
||||
// AnalyzeSubjects 已被 analyze_rhythm 吸收。
|
||||
func AnalyzeSubjects(state *ScheduleState, args map[string]any) string {
|
||||
return encodeAnalyzeFailure("analyze_subjects", "deprecated", "analyze_subjects 已被 analyze_rhythm 吸收")
|
||||
}
|
||||
|
||||
// AnalyzeContext 已被 analyze_rhythm 吸收。
|
||||
func AnalyzeContext(state *ScheduleState, args map[string]any) string {
|
||||
return encodeAnalyzeFailure("analyze_context", "deprecated", "analyze_context 已被 analyze_rhythm 吸收")
|
||||
}
|
||||
|
||||
// AnalyzeTolerance 已退出主动优化主链路。
|
||||
func AnalyzeTolerance(state *ScheduleState, args map[string]any) string {
|
||||
return encodeAnalyzeFailure("analyze_tolerance", "deprecated", "analyze_tolerance 已退出主动优化链路")
|
||||
}
|
||||
|
||||
// AnalyzeRhythm 输出认知节奏层面的结构化观察。
|
||||
func AnalyzeRhythm(state *ScheduleState, args map[string]any) string {
|
||||
if state == nil {
|
||||
@@ -958,164 +938,6 @@ func buildSemanticProfileIssues(metrics analyzeSemanticProfileMetrics) []analyze
|
||||
}}
|
||||
}
|
||||
|
||||
func buildAnalyzeHealthDecision(
|
||||
state *ScheduleState,
|
||||
snapshot analyzeHealthSnapshot,
|
||||
) analyzeHealthDecision {
|
||||
base := buildAnalyzeHealthDecisionBase(state, snapshot)
|
||||
decision := analyzeHealthDecision{
|
||||
ShouldContinueOptimize: base.ShouldContinueOptimize,
|
||||
PrimaryProblem: base.PrimaryProblem,
|
||||
ProblemScope: base.ProblemScope,
|
||||
IsForcedImperfection: base.IsForcedImperfection,
|
||||
RecommendedOperation: base.RecommendedOperation,
|
||||
ImprovementSignal: buildHealthImprovementSignal(
|
||||
snapshot.Rhythm,
|
||||
snapshot.Tightness,
|
||||
base.ProblemScope,
|
||||
base.RecommendedOperation,
|
||||
snapshot.Profile,
|
||||
snapshot.Feasibility,
|
||||
),
|
||||
}
|
||||
|
||||
// 1. 只有“高认知相邻”这类当前 P1 真正能靠确定性候选修复的问题,才进入候选枚举。
|
||||
// 2. 若所有合法候选都只是平移/无增益/恶化,则直接回到 close,避免把 LLM 逼成苦力工。
|
||||
// 3. close 永远保留为兜底选项,让 LLM 可以自然收口,而不是为了完成任务感继续乱挪。
|
||||
problem, ok := pickPrimaryHealthProblem(state, snapshot)
|
||||
if !ok || problem.Kind != healthProblemHeavyAdjacent || problem.Pair == nil {
|
||||
decision.Candidates = []analyzeHealthCandidate{
|
||||
buildHealthCloseCandidate("保持当前安排并收口:当前没有可继续处理的候选认知问题。", snapshot, base),
|
||||
}
|
||||
decision.ShouldContinueOptimize = false
|
||||
decision.RecommendedOperation = "close"
|
||||
decision.ImprovementSignal = buildHealthImprovementSignal(
|
||||
snapshot.Rhythm,
|
||||
snapshot.Tightness,
|
||||
decision.ProblemScope,
|
||||
decision.RecommendedOperation,
|
||||
snapshot.Profile,
|
||||
snapshot.Feasibility,
|
||||
)
|
||||
return decision
|
||||
}
|
||||
|
||||
beneficial := buildHealthCandidatesForProblem(state, snapshot, problem)
|
||||
if len(beneficial) == 0 {
|
||||
decision.Candidates = []analyzeHealthCandidate{
|
||||
buildHealthCloseCandidate("保持当前安排并收口:当前所有合法 move / swap 都只会平移、无增益或恶化问题。", snapshot, base),
|
||||
}
|
||||
decision.ShouldContinueOptimize = false
|
||||
decision.RecommendedOperation = "close"
|
||||
if snapshot.Tightness.TightnessLevel == "locked" || snapshot.Tightness.TightnessLevel == "tight" {
|
||||
decision.IsForcedImperfection = true
|
||||
}
|
||||
decision.ImprovementSignal = buildHealthImprovementSignal(
|
||||
snapshot.Rhythm,
|
||||
snapshot.Tightness,
|
||||
decision.ProblemScope,
|
||||
decision.RecommendedOperation,
|
||||
snapshot.Profile,
|
||||
snapshot.Feasibility,
|
||||
)
|
||||
return decision
|
||||
}
|
||||
|
||||
decision.Candidates = append(decision.Candidates, beneficial...)
|
||||
decision.Candidates = append(decision.Candidates,
|
||||
buildHealthCloseCandidate("如果不想继续挪动,也可以保持当前安排并直接收口。", snapshot, base),
|
||||
)
|
||||
decision.ShouldContinueOptimize = true
|
||||
decision.RecommendedOperation = strings.TrimSpace(beneficial[0].Tool)
|
||||
decision.ImprovementSignal = buildHealthImprovementSignal(
|
||||
snapshot.Rhythm,
|
||||
snapshot.Tightness,
|
||||
decision.ProblemScope,
|
||||
decision.RecommendedOperation,
|
||||
snapshot.Profile,
|
||||
snapshot.Feasibility,
|
||||
)
|
||||
return decision
|
||||
}
|
||||
|
||||
func pickPrimaryRhythmProblem(
|
||||
rhythm analyzeRhythmMetrics,
|
||||
tightness analyzeTightnessMetrics,
|
||||
) (summary string, scope *analyzeProblemScope, operation string, ok bool) {
|
||||
type rhythmCandidate struct {
|
||||
score int
|
||||
summary string
|
||||
scope *analyzeProblemScope
|
||||
preferSwap bool
|
||||
}
|
||||
|
||||
candidates := make([]rhythmCandidate, 0, len(rhythm.Days)*2)
|
||||
for _, day := range rhythm.Days {
|
||||
if day.HeavyAdjacent && !shouldTreatHeavyAdjacencyAsAcceptable(rhythm, day) {
|
||||
score := 300 + day.SwitchCount*8 + int(day.Fragmentation*20)
|
||||
candidates = append(candidates, rhythmCandidate{
|
||||
score: score,
|
||||
summary: fmt.Sprintf("第 %d 天存在高认知强度任务相邻,学起来会发紧", day.DayIndex),
|
||||
scope: &analyzeProblemScope{DayRange: []int{day.DayIndex}},
|
||||
preferSwap: true,
|
||||
})
|
||||
}
|
||||
if day.SwitchCount >= 5 && day.Fragmentation >= 0.75 {
|
||||
score := 220 + day.SwitchCount*10 + int(day.Fragmentation*100)
|
||||
candidates = append(candidates, rhythmCandidate{
|
||||
score: score,
|
||||
summary: fmt.Sprintf("第 %d 天切换次数偏多,学习节奏明显发碎", day.DayIndex),
|
||||
scope: &analyzeProblemScope{DayRange: []int{day.DayIndex}},
|
||||
preferSwap: false,
|
||||
})
|
||||
}
|
||||
if day.MaxBlock >= 5 {
|
||||
score := 140 + day.MaxBlock*10
|
||||
candidates = append(candidates, rhythmCandidate{
|
||||
score: score,
|
||||
summary: fmt.Sprintf("第 %d 天连续同科目学习块过长,节奏略显单一", day.DayIndex),
|
||||
scope: &analyzeProblemScope{DayRange: []int{day.DayIndex}},
|
||||
preferSwap: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
return "", nil, "close", false
|
||||
}
|
||||
sort.SliceStable(candidates, func(i, j int) bool {
|
||||
if candidates[i].score != candidates[j].score {
|
||||
return candidates[i].score > candidates[j].score
|
||||
}
|
||||
leftDay := 1 << 30
|
||||
rightDay := 1 << 30
|
||||
if candidates[i].scope != nil && len(candidates[i].scope.DayRange) > 0 {
|
||||
leftDay = candidates[i].scope.DayRange[0]
|
||||
}
|
||||
if candidates[j].scope != nil && len(candidates[j].scope.DayRange) > 0 {
|
||||
rightDay = candidates[j].scope.DayRange[0]
|
||||
}
|
||||
return leftDay < rightDay
|
||||
})
|
||||
best := candidates[0]
|
||||
operation = chooseHealthOperation(tightness, best.preferSwap)
|
||||
return best.summary, best.scope, operation, true
|
||||
}
|
||||
|
||||
func chooseHealthOperation(tightness analyzeTightnessMetrics, preferSwap bool) string {
|
||||
switch {
|
||||
case tightness.TightnessLevel == "locked":
|
||||
return "close"
|
||||
case preferSwap && tightness.CrossClassSwapOptions > 0:
|
||||
return "swap"
|
||||
case tightness.LocallyMovableTaskCount > 0:
|
||||
return "move"
|
||||
case tightness.CrossClassSwapOptions > 0:
|
||||
return "swap"
|
||||
default:
|
||||
return "close"
|
||||
}
|
||||
}
|
||||
|
||||
func shouldTreatHeavyAdjacencyAsAcceptable(rhythm analyzeRhythmMetrics, day analyzeContextDay) bool {
|
||||
// 1. 若整体切换本来就少、同类型切换占比很高,说明当前节奏更像“同类硬课顺着学”,
|
||||
// 这类情况不该因为“高认知相邻”四个字就被反复优化。
|
||||
|
||||
@@ -1,707 +0,0 @@
|
||||
package schedule
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
compositelogic "github.com/LoveLosita/smartflow/backend/logic"
|
||||
)
|
||||
|
||||
var spreadEvenAllowedArgs = []string{
|
||||
"task_ids",
|
||||
"task_id",
|
||||
"limit",
|
||||
"allow_embed",
|
||||
"day",
|
||||
"day_start",
|
||||
"day_end",
|
||||
"day_scope",
|
||||
"day_of_week",
|
||||
"week",
|
||||
"week_filter",
|
||||
"week_from",
|
||||
"week_to",
|
||||
"slot_type",
|
||||
"slot_types",
|
||||
"exclude_sections",
|
||||
"after_section",
|
||||
"before_section",
|
||||
}
|
||||
|
||||
// minContextSnapshot 记录任务在复合重排前后的最小快照,用于输出摘要。
|
||||
type minContextSnapshot struct {
|
||||
StateID int
|
||||
Name string
|
||||
ContextTag string
|
||||
Slot TaskSlot
|
||||
}
|
||||
|
||||
// refineTaskCandidate 是复合规划器使用的任务输入。
|
||||
type refineTaskCandidate struct {
|
||||
TaskID int
|
||||
Week int
|
||||
DayOfWeek int
|
||||
SectionFrom int
|
||||
SectionTo int
|
||||
Name string
|
||||
ContextTag string
|
||||
OriginRank int
|
||||
}
|
||||
|
||||
// compositeIDMapper 负责维护 state_id 与 logic 规划入参 ID 的双向映射。
|
||||
//
|
||||
// 说明:
|
||||
// 1. 当前阶段使用等值映射(logicID=stateID),保证行为不变;
|
||||
// 2. 保留独立适配层,后续若切到真实 task_item_id,只需改这里;
|
||||
// 3. 通过双向映射保证“入参转换 + 结果回填”一致。
|
||||
type compositeIDMapper struct {
|
||||
stateToLogic map[int]int
|
||||
logicToState map[int]int
|
||||
}
|
||||
|
||||
// buildCompositeIDMapper 构建并校验本轮复合工具的 ID 映射。
|
||||
func buildCompositeIDMapper(stateIDs []int) (*compositeIDMapper, error) {
|
||||
mapper := &compositeIDMapper{
|
||||
stateToLogic: make(map[int]int, len(stateIDs)),
|
||||
logicToState: make(map[int]int, len(stateIDs)),
|
||||
}
|
||||
for _, stateID := range stateIDs {
|
||||
if stateID <= 0 {
|
||||
return nil, fmt.Errorf("存在非法 state_id=%d", stateID)
|
||||
}
|
||||
if _, exists := mapper.stateToLogic[stateID]; exists {
|
||||
return nil, fmt.Errorf("state_id=%d 重复", stateID)
|
||||
}
|
||||
// 当前迁移阶段采用等值映射,先把“映射机制”跑通。
|
||||
logicID := stateID
|
||||
mapper.stateToLogic[stateID] = logicID
|
||||
mapper.logicToState[logicID] = stateID
|
||||
}
|
||||
return mapper, nil
|
||||
}
|
||||
|
||||
// MinContextSwitch 在给定任务集合内重排 suggested 任务,尽量减少上下文切换次数。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只处理“已落位的 suggested 任务”重排,不负责粗排;
|
||||
// 2. 仅在给定 task_ids 集合内部重排,不改动集合外任务;
|
||||
// 3. 采用原子提交:任一校验失败则整体不生效。
|
||||
func MinContextSwitch(state *ScheduleState, taskIDs []int) string {
|
||||
if state == nil {
|
||||
return "减少上下文切换失败:日程状态为空。"
|
||||
}
|
||||
|
||||
// 1. 收集任务并做前置校验,确保规划输入可用。
|
||||
plannerTasks, beforeByID, excludeIDs, idMapper, err := collectCompositePlannerTasks(state, taskIDs, "减少上下文切换")
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
logicTasks, err := toLogicPlannerTasks(plannerTasks, idMapper)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("减少上下文切换失败:%s。", err.Error())
|
||||
}
|
||||
|
||||
// 2. 该工具固定在“当前任务已占坑位集合”内重排,不向外扩张候选位。
|
||||
currentSlots := buildCurrentSlotsFromPlannerTasks(logicTasks)
|
||||
plannedMoves, err := compositelogic.PlanMinContextSwitchMoves(logicTasks, currentSlots, compositelogic.RefineCompositePlanOptions{})
|
||||
if err != nil {
|
||||
return fmt.Sprintf("减少上下文切换失败:%s。", err.Error())
|
||||
}
|
||||
|
||||
// 3. 映射回工具态坐标并在提交前做完整校验。
|
||||
afterByID, err := buildAfterSnapshotsFromPlannedMoves(state, beforeByID, plannedMoves, idMapper)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("减少上下文切换失败:%s。", err.Error())
|
||||
}
|
||||
for taskID, after := range afterByID {
|
||||
before := beforeByID[taskID]
|
||||
if err := validateDay(state, after.Slot.Day); err != nil {
|
||||
return fmt.Sprintf("减少上下文切换失败:任务 [%d]%s 目标天非法:%s。", before.StateID, before.Name, err.Error())
|
||||
}
|
||||
if err := validateSlotRange(after.Slot.SlotStart, after.Slot.SlotEnd); err != nil {
|
||||
return fmt.Sprintf("减少上下文切换失败:任务 [%d]%s 目标节次非法:%s。", before.StateID, before.Name, err.Error())
|
||||
}
|
||||
if conflict := findConflict(state, after.Slot.Day, after.Slot.SlotStart, after.Slot.SlotEnd, excludeIDs...); conflict != nil {
|
||||
return fmt.Sprintf(
|
||||
"减少上下文切换失败:任务 [%d]%s 目标位置 %s 与 [%d]%s 冲突。",
|
||||
before.StateID,
|
||||
before.Name,
|
||||
formatDaySlotLabel(state, after.Slot.Day, after.Slot.SlotStart, after.Slot.SlotEnd),
|
||||
conflict.StateID,
|
||||
conflict.Name,
|
||||
)
|
||||
}
|
||||
}
|
||||
minContextProposals := make(map[int][]TaskSlot, len(afterByID))
|
||||
for taskID, after := range afterByID {
|
||||
minContextProposals[taskID] = []TaskSlot{after.Slot}
|
||||
}
|
||||
if err := validateLocalOrderBatchPlacement(state, minContextProposals); err != nil {
|
||||
return fmt.Sprintf("减少上下文切换失败:%s。", err.Error())
|
||||
}
|
||||
|
||||
// 4. 全量通过后再原子提交,避免半成品状态。
|
||||
clone := state.Clone()
|
||||
for taskID, after := range afterByID {
|
||||
task := clone.TaskByStateID(taskID)
|
||||
if task == nil {
|
||||
return fmt.Sprintf("减少上下文切换失败:任务ID %d 在提交阶段不存在。", taskID)
|
||||
}
|
||||
task.Slots = []TaskSlot{after.Slot}
|
||||
}
|
||||
state.Tasks = clone.Tasks
|
||||
|
||||
beforeOrdered := sortMinContextSnapshots(beforeByID)
|
||||
afterOrdered := sortMinContextSnapshots(afterByID)
|
||||
beforeSwitches := countMinContextSwitches(beforeOrdered)
|
||||
afterSwitches := countMinContextSwitches(afterOrdered)
|
||||
|
||||
changedLines := make([]string, 0, len(beforeOrdered))
|
||||
affectedDays := make(map[int]bool, len(beforeOrdered)*2)
|
||||
for _, before := range beforeOrdered {
|
||||
after := afterByID[before.StateID]
|
||||
if sameTaskSlot(before.Slot, after.Slot) {
|
||||
continue
|
||||
}
|
||||
changedLines = append(changedLines, fmt.Sprintf(
|
||||
" [%d]%s:%s -> %s",
|
||||
before.StateID,
|
||||
before.Name,
|
||||
formatDaySlotLabel(state, before.Slot.Day, before.Slot.SlotStart, before.Slot.SlotEnd),
|
||||
formatDaySlotLabel(state, after.Slot.Day, after.Slot.SlotStart, after.Slot.SlotEnd),
|
||||
))
|
||||
affectedDays[before.Slot.Day] = true
|
||||
affectedDays[after.Slot.Day] = true
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf(
|
||||
"最少上下文切换重排完成:共处理 %d 个任务,上下文切换次数 %d -> %d。\n",
|
||||
len(beforeByID), beforeSwitches, afterSwitches,
|
||||
))
|
||||
if len(changedLines) == 0 {
|
||||
sb.WriteString("当前任务顺序已是较优结果,无需调整。")
|
||||
return sb.String()
|
||||
}
|
||||
sb.WriteString("本次调整:\n")
|
||||
for _, line := range changedLines {
|
||||
sb.WriteString(line + "\n")
|
||||
}
|
||||
for _, day := range sortedKeys(affectedDays) {
|
||||
sb.WriteString(formatDayOccupancy(state, day) + "\n")
|
||||
}
|
||||
return strings.TrimSpace(sb.String())
|
||||
}
|
||||
|
||||
// SpreadEven 在给定任务集合内执行“均匀化铺开”。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 仅处理 suggested 且已落位任务;
|
||||
// 2. 先按筛选条件收集候选坑位,再调用确定性规划器;
|
||||
// 3. 通过统一校验后原子提交,失败不落地。
|
||||
func SpreadEven(state *ScheduleState, taskIDs []int, args map[string]any) string {
|
||||
if state == nil {
|
||||
return "均匀化调整失败:日程状态为空。"
|
||||
}
|
||||
// 0. 参数白名单校验:未知字段直接失败,避免静默忽略导致候选范围漂移。
|
||||
if err := validateToolArgsStrict(args, spreadEvenAllowedArgs); err != nil {
|
||||
return fmt.Sprintf("均匀化调整失败:%s。", err.Error())
|
||||
}
|
||||
|
||||
// 1. 先做任务侧校验,避免后续规划在脏输入上执行。
|
||||
plannerTasks, beforeByID, excludeIDs, idMapper, err := collectCompositePlannerTasks(state, taskIDs, "均匀化调整")
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
logicTasks, err := toLogicPlannerTasks(plannerTasks, idMapper)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("均匀化调整失败:%s。", err.Error())
|
||||
}
|
||||
|
||||
// 2. 按跨度需求收集候选坑位,确保每类跨度都有可用池。
|
||||
spanNeed := make(map[int]int, len(logicTasks))
|
||||
for _, task := range logicTasks {
|
||||
spanNeed[task.SectionTo-task.SectionFrom+1]++
|
||||
}
|
||||
candidateSlots, err := collectSpreadEvenCandidateSlotsBySpan(state, args, spanNeed)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("均匀化调整失败:%s。", err.Error())
|
||||
}
|
||||
|
||||
// 3. 用“范围内既有负载”作为打分基线,让结果更接近均匀分布。
|
||||
dayLoadBaseline := buildSpreadEvenDayLoadBaseline(state, excludeIDs, candidateSlots)
|
||||
plannedMoves, err := compositelogic.PlanEvenSpreadMoves(logicTasks, candidateSlots, compositelogic.RefineCompositePlanOptions{
|
||||
ExistingDayLoad: dayLoadBaseline,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Sprintf("均匀化调整失败:%s。", err.Error())
|
||||
}
|
||||
|
||||
// 4. 回填 + 校验 + 原子提交。
|
||||
afterByID, err := buildAfterSnapshotsFromPlannedMoves(state, beforeByID, plannedMoves, idMapper)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("均匀化调整失败:%s。", err.Error())
|
||||
}
|
||||
for taskID, after := range afterByID {
|
||||
before := beforeByID[taskID]
|
||||
if err := validateDay(state, after.Slot.Day); err != nil {
|
||||
return fmt.Sprintf("均匀化调整失败:任务 [%d]%s 目标天非法:%s。", before.StateID, before.Name, err.Error())
|
||||
}
|
||||
if err := validateSlotRange(after.Slot.SlotStart, after.Slot.SlotEnd); err != nil {
|
||||
return fmt.Sprintf("均匀化调整失败:任务 [%d]%s 目标节次非法:%s。", before.StateID, before.Name, err.Error())
|
||||
}
|
||||
if conflict := findConflict(state, after.Slot.Day, after.Slot.SlotStart, after.Slot.SlotEnd, excludeIDs...); conflict != nil {
|
||||
return fmt.Sprintf(
|
||||
"均匀化调整失败:任务 [%d]%s 目标位置 %s 与 [%d]%s 冲突。",
|
||||
before.StateID,
|
||||
before.Name,
|
||||
formatDaySlotLabel(state, after.Slot.Day, after.Slot.SlotStart, after.Slot.SlotEnd),
|
||||
conflict.StateID,
|
||||
conflict.Name,
|
||||
)
|
||||
}
|
||||
}
|
||||
spreadEvenProposals := make(map[int][]TaskSlot, len(afterByID))
|
||||
for taskID, after := range afterByID {
|
||||
spreadEvenProposals[taskID] = []TaskSlot{after.Slot}
|
||||
}
|
||||
if err := validateLocalOrderBatchPlacement(state, spreadEvenProposals); err != nil {
|
||||
return fmt.Sprintf("均匀化调整失败:%s。", err.Error())
|
||||
}
|
||||
|
||||
clone := state.Clone()
|
||||
for taskID, after := range afterByID {
|
||||
task := clone.TaskByStateID(taskID)
|
||||
if task == nil {
|
||||
return fmt.Sprintf("均匀化调整失败:任务ID %d 在提交阶段不存在。", taskID)
|
||||
}
|
||||
task.Slots = []TaskSlot{after.Slot}
|
||||
}
|
||||
state.Tasks = clone.Tasks
|
||||
|
||||
beforeOrdered := sortMinContextSnapshots(beforeByID)
|
||||
changedLines := make([]string, 0, len(beforeOrdered))
|
||||
affectedDays := make(map[int]bool, len(beforeOrdered)*2)
|
||||
for _, before := range beforeOrdered {
|
||||
after := afterByID[before.StateID]
|
||||
if sameTaskSlot(before.Slot, after.Slot) {
|
||||
continue
|
||||
}
|
||||
changedLines = append(changedLines, fmt.Sprintf(
|
||||
" [%d]%s:%s -> %s",
|
||||
before.StateID,
|
||||
before.Name,
|
||||
formatDaySlotLabel(state, before.Slot.Day, before.Slot.SlotStart, before.Slot.SlotEnd),
|
||||
formatDaySlotLabel(state, after.Slot.Day, after.Slot.SlotStart, after.Slot.SlotEnd),
|
||||
))
|
||||
affectedDays[before.Slot.Day] = true
|
||||
affectedDays[after.Slot.Day] = true
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf(
|
||||
"均匀化调整完成:共处理 %d 个任务,候选坑位 %d 个。\n",
|
||||
len(beforeByID), len(candidateSlots),
|
||||
))
|
||||
if len(changedLines) == 0 {
|
||||
sb.WriteString("规划结果与当前落位一致,无需调整。")
|
||||
return sb.String()
|
||||
}
|
||||
sb.WriteString("本次调整:\n")
|
||||
for _, line := range changedLines {
|
||||
sb.WriteString(line + "\n")
|
||||
}
|
||||
for _, day := range sortedKeys(affectedDays) {
|
||||
sb.WriteString(formatDayOccupancy(state, day) + "\n")
|
||||
}
|
||||
return strings.TrimSpace(sb.String())
|
||||
}
|
||||
|
||||
func ParseMinContextSwitchTaskIDs(args map[string]any) ([]int, error) {
|
||||
return ParseCompositeTaskIDs(args)
|
||||
}
|
||||
|
||||
func ParseSpreadEvenTaskIDs(args map[string]any) ([]int, error) {
|
||||
return ParseCompositeTaskIDs(args)
|
||||
}
|
||||
|
||||
func ParseCompositeTaskIDs(args map[string]any) ([]int, error) {
|
||||
if ids, ok := ArgsIntSlice(args, "task_ids"); ok && len(ids) > 0 {
|
||||
return ids, nil
|
||||
}
|
||||
if id, ok := ArgsInt(args, "task_id"); ok {
|
||||
return []int{id}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("缺少必填参数 task_ids(兼容单值 task_id)")
|
||||
}
|
||||
|
||||
// collectCompositePlannerTasks 统一收集复合工具输入任务,并做“可移动 suggested”校验。
|
||||
func collectCompositePlannerTasks(
|
||||
state *ScheduleState,
|
||||
taskIDs []int,
|
||||
toolLabel string,
|
||||
) ([]refineTaskCandidate, map[int]minContextSnapshot, []int, *compositeIDMapper, error) {
|
||||
normalizedIDs := uniquePositiveInts(taskIDs)
|
||||
if len(normalizedIDs) < 2 {
|
||||
return nil, nil, nil, nil, fmt.Errorf("%s失败:task_ids 至少需要 2 个有效任务 ID", toolLabel)
|
||||
}
|
||||
|
||||
idMapper, err := buildCompositeIDMapper(normalizedIDs)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, fmt.Errorf("%s失败:ID 映射构建失败:%s", toolLabel, err.Error())
|
||||
}
|
||||
|
||||
plannerTasks := make([]refineTaskCandidate, 0, len(normalizedIDs))
|
||||
beforeByID := make(map[int]minContextSnapshot, len(normalizedIDs))
|
||||
excludeIDs := make([]int, 0, len(normalizedIDs))
|
||||
|
||||
for rank, taskID := range normalizedIDs {
|
||||
task := state.TaskByStateID(taskID)
|
||||
if task == nil {
|
||||
return nil, nil, nil, nil, fmt.Errorf("%s失败:任务ID %d 不存在", toolLabel, taskID)
|
||||
}
|
||||
if !IsSuggestedTask(*task) {
|
||||
return nil, nil, nil, nil, fmt.Errorf("%s失败:[%d]%s 不是 suggested 任务,仅 suggested 可参与该工具", toolLabel, task.StateID, task.Name)
|
||||
}
|
||||
if err := checkLocked(*task); err != nil {
|
||||
return nil, nil, nil, nil, fmt.Errorf("%s失败:%s", toolLabel, err.Error())
|
||||
}
|
||||
if len(task.Slots) != 1 {
|
||||
return nil, nil, nil, nil, fmt.Errorf("%s失败:[%d]%s 当前包含 %d 段时段,暂不支持该形态", toolLabel, task.StateID, task.Name, len(task.Slots))
|
||||
}
|
||||
|
||||
slot := task.Slots[0]
|
||||
if err := validateDay(state, slot.Day); err != nil {
|
||||
return nil, nil, nil, nil, fmt.Errorf("%s失败:[%d]%s 的时段非法:%s", toolLabel, task.StateID, task.Name, err.Error())
|
||||
}
|
||||
if err := validateSlotRange(slot.SlotStart, slot.SlotEnd); err != nil {
|
||||
return nil, nil, nil, nil, fmt.Errorf("%s失败:[%d]%s 的节次非法:%s", toolLabel, task.StateID, task.Name, err.Error())
|
||||
}
|
||||
week, dayOfWeek, ok := state.DayToWeekDay(slot.Day)
|
||||
if !ok {
|
||||
return nil, nil, nil, nil, fmt.Errorf("%s失败:[%d]%s 的 day=%d 无法映射到 week/day_of_week", toolLabel, task.StateID, task.Name, slot.Day)
|
||||
}
|
||||
|
||||
contextTag := normalizeMinContextTag(*task)
|
||||
beforeByID[task.StateID] = minContextSnapshot{
|
||||
StateID: task.StateID,
|
||||
Name: task.Name,
|
||||
ContextTag: contextTag,
|
||||
Slot: slot,
|
||||
}
|
||||
excludeIDs = append(excludeIDs, task.StateID)
|
||||
plannerTasks = append(plannerTasks, refineTaskCandidate{
|
||||
TaskID: task.StateID,
|
||||
Week: week,
|
||||
DayOfWeek: dayOfWeek,
|
||||
SectionFrom: slot.SlotStart,
|
||||
SectionTo: slot.SlotEnd,
|
||||
Name: strings.TrimSpace(task.Name),
|
||||
ContextTag: contextTag,
|
||||
OriginRank: rank + 1,
|
||||
})
|
||||
}
|
||||
|
||||
return plannerTasks, beforeByID, excludeIDs, idMapper, nil
|
||||
}
|
||||
|
||||
// toLogicPlannerTasks 将工具层任务结构映射为 logic 规划器输入。
|
||||
func toLogicPlannerTasks(tasks []refineTaskCandidate, idMapper *compositeIDMapper) ([]compositelogic.RefineTaskCandidate, error) {
|
||||
if len(tasks) == 0 {
|
||||
return nil, fmt.Errorf("任务列表为空")
|
||||
}
|
||||
if idMapper == nil {
|
||||
return nil, fmt.Errorf("ID 映射为空")
|
||||
}
|
||||
result := make([]compositelogic.RefineTaskCandidate, 0, len(tasks))
|
||||
for _, task := range tasks {
|
||||
logicID, ok := idMapper.stateToLogic[task.TaskID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("任务 state_id=%d 缺少 logic 映射", task.TaskID)
|
||||
}
|
||||
result = append(result, compositelogic.RefineTaskCandidate{
|
||||
TaskItemID: logicID,
|
||||
Week: task.Week,
|
||||
DayOfWeek: task.DayOfWeek,
|
||||
SectionFrom: task.SectionFrom,
|
||||
SectionTo: task.SectionTo,
|
||||
Name: task.Name,
|
||||
ContextTag: task.ContextTag,
|
||||
OriginRank: task.OriginRank,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func buildCurrentSlotsFromPlannerTasks(tasks []compositelogic.RefineTaskCandidate) []compositelogic.RefineSlotCandidate {
|
||||
slots := make([]compositelogic.RefineSlotCandidate, 0, len(tasks))
|
||||
for _, task := range tasks {
|
||||
slots = append(slots, compositelogic.RefineSlotCandidate{
|
||||
Week: task.Week,
|
||||
DayOfWeek: task.DayOfWeek,
|
||||
SectionFrom: task.SectionFrom,
|
||||
SectionTo: task.SectionTo,
|
||||
})
|
||||
}
|
||||
return slots
|
||||
}
|
||||
|
||||
func buildAfterSnapshotsFromPlannedMoves(
|
||||
state *ScheduleState,
|
||||
beforeByID map[int]minContextSnapshot,
|
||||
plannedMoves []compositelogic.RefineMovePlanItem,
|
||||
idMapper *compositeIDMapper,
|
||||
) (map[int]minContextSnapshot, error) {
|
||||
if len(plannedMoves) == 0 {
|
||||
return nil, fmt.Errorf("规划结果为空")
|
||||
}
|
||||
if idMapper == nil {
|
||||
return nil, fmt.Errorf("ID 映射为空")
|
||||
}
|
||||
|
||||
moveByID := make(map[int]compositelogic.RefineMovePlanItem, len(plannedMoves))
|
||||
for _, move := range plannedMoves {
|
||||
stateID, ok := idMapper.logicToState[move.TaskItemID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("规划结果包含未知 logic 任务 id=%d", move.TaskItemID)
|
||||
}
|
||||
if _, exists := moveByID[stateID]; exists {
|
||||
return nil, fmt.Errorf("规划结果包含重复任务 id=%d", stateID)
|
||||
}
|
||||
moveByID[stateID] = move
|
||||
}
|
||||
|
||||
afterByID := make(map[int]minContextSnapshot, len(beforeByID))
|
||||
for taskID, before := range beforeByID {
|
||||
move, ok := moveByID[taskID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("规划结果不完整:缺少任务 id=%d", taskID)
|
||||
}
|
||||
day, ok := state.WeekDayToDay(move.ToWeek, move.ToDay)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("任务 id=%d 目标 week/day 无法映射到 day_index:W%dD%d", taskID, move.ToWeek, move.ToDay)
|
||||
}
|
||||
afterByID[taskID] = minContextSnapshot{
|
||||
StateID: before.StateID,
|
||||
Name: before.Name,
|
||||
ContextTag: before.ContextTag,
|
||||
Slot: TaskSlot{
|
||||
Day: day,
|
||||
SlotStart: move.ToSectionFrom,
|
||||
SlotEnd: move.ToSectionTo,
|
||||
},
|
||||
}
|
||||
}
|
||||
return afterByID, nil
|
||||
}
|
||||
|
||||
func collectSpreadEvenCandidateSlotsBySpan(
|
||||
state *ScheduleState,
|
||||
args map[string]any,
|
||||
spanNeed map[int]int,
|
||||
) ([]compositelogic.RefineSlotCandidate, error) {
|
||||
if len(spanNeed) == 0 {
|
||||
return nil, fmt.Errorf("未识别到任务跨度需求")
|
||||
}
|
||||
|
||||
spans := make([]int, 0, len(spanNeed))
|
||||
for span := range spanNeed {
|
||||
spans = append(spans, span)
|
||||
}
|
||||
sort.Ints(spans)
|
||||
|
||||
allSlots := make([]compositelogic.RefineSlotCandidate, 0, 16)
|
||||
seen := make(map[string]struct{}, 64)
|
||||
for _, span := range spans {
|
||||
required := spanNeed[span]
|
||||
queryArgs := buildSpreadEvenSlotQueryArgs(args, span, required)
|
||||
raw := QueryAvailableSlots(state, queryArgs)
|
||||
|
||||
var failed struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
_ = json.Unmarshal([]byte(raw), &failed)
|
||||
if strings.TrimSpace(failed.Error) != "" {
|
||||
return nil, fmt.Errorf("查询跨度=%d 的候选坑位失败:%s", span, strings.TrimSpace(failed.Error))
|
||||
}
|
||||
|
||||
var payload queryAvailableSlotsResult
|
||||
if err := json.Unmarshal([]byte(raw), &payload); err != nil {
|
||||
return nil, fmt.Errorf("解析跨度=%d 的候选坑位结果失败:%v", span, err)
|
||||
}
|
||||
if len(payload.Slots) < required {
|
||||
return nil, fmt.Errorf("跨度=%d 可用坑位不足:required=%d, got=%d", span, required, len(payload.Slots))
|
||||
}
|
||||
|
||||
for _, slot := range payload.Slots {
|
||||
key := fmt.Sprintf("%d-%d-%d-%d", slot.Week, slot.DayOfWeek, slot.SlotStart, slot.SlotEnd)
|
||||
if _, exists := seen[key]; exists {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
allSlots = append(allSlots, compositelogic.RefineSlotCandidate{
|
||||
Week: slot.Week,
|
||||
DayOfWeek: slot.DayOfWeek,
|
||||
SectionFrom: slot.SlotStart,
|
||||
SectionTo: slot.SlotEnd,
|
||||
})
|
||||
}
|
||||
}
|
||||
return allSlots, nil
|
||||
}
|
||||
|
||||
func buildSpreadEvenSlotQueryArgs(args map[string]any, span int, required int) map[string]any {
|
||||
query := make(map[string]any, 16)
|
||||
query["span"] = span
|
||||
|
||||
limit := required * 6
|
||||
if limit < required {
|
||||
limit = required
|
||||
}
|
||||
if customLimit, ok := readIntAny(args, "limit"); ok && customLimit > limit {
|
||||
limit = customLimit
|
||||
}
|
||||
query["limit"] = limit
|
||||
query["allow_embed"] = readBoolAnyWithDefault(args, true, "allow_embed", "allow_embedding")
|
||||
|
||||
for _, key := range []string{"day", "day_start", "day_end", "week", "week_from", "week_to", "day_scope", "after_section", "before_section"} {
|
||||
if value, ok := args[key]; ok {
|
||||
query[key] = value
|
||||
}
|
||||
}
|
||||
if week, ok := readIntAny(args, "to_week", "target_week", "new_week"); ok {
|
||||
query["week"] = week
|
||||
}
|
||||
if day, ok := readIntAny(args, "to_day", "target_day", "target_day_of_week", "new_day"); ok {
|
||||
query["day_of_week"] = []int{day}
|
||||
}
|
||||
|
||||
if values := uniquePositiveInts(readIntSliceAny(args, "week_filter", "weeks")); len(values) > 0 {
|
||||
query["week_filter"] = values
|
||||
}
|
||||
if values := uniqueInts(readIntSliceAny(args, "day_of_week", "days", "day_filter")); len(values) > 0 {
|
||||
query["day_of_week"] = values
|
||||
}
|
||||
if values := uniqueInts(readIntSliceAny(args, "exclude_sections", "exclude_section")); len(values) > 0 {
|
||||
query["exclude_sections"] = values
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
func buildSpreadEvenDayLoadBaseline(
|
||||
state *ScheduleState,
|
||||
excludeTaskIDs []int,
|
||||
slots []compositelogic.RefineSlotCandidate,
|
||||
) map[string]int {
|
||||
if len(slots) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
targetDays := make(map[string]struct{}, len(slots))
|
||||
for _, slot := range slots {
|
||||
targetDays[composeDayKey(slot.Week, slot.DayOfWeek)] = struct{}{}
|
||||
}
|
||||
if len(targetDays) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
excludeSet := make(map[int]struct{}, len(excludeTaskIDs))
|
||||
for _, id := range excludeTaskIDs {
|
||||
excludeSet[id] = struct{}{}
|
||||
}
|
||||
|
||||
load := make(map[string]int, len(targetDays))
|
||||
for _, task := range state.Tasks {
|
||||
if !IsSuggestedTask(task) {
|
||||
continue
|
||||
}
|
||||
if _, excluded := excludeSet[task.StateID]; excluded {
|
||||
continue
|
||||
}
|
||||
for _, slot := range task.Slots {
|
||||
week, dayOfWeek, ok := state.DayToWeekDay(slot.Day)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
key := composeDayKey(week, dayOfWeek)
|
||||
if _, inTarget := targetDays[key]; !inTarget {
|
||||
continue
|
||||
}
|
||||
load[key]++
|
||||
}
|
||||
}
|
||||
return load
|
||||
}
|
||||
|
||||
func composeDayKey(week, day int) string {
|
||||
return fmt.Sprintf("%d-%d", week, day)
|
||||
}
|
||||
|
||||
func uniquePositiveInts(values []int) []int {
|
||||
seen := make(map[int]struct{}, len(values))
|
||||
result := make([]int, 0, len(values))
|
||||
for _, value := range values {
|
||||
if value <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[value]; exists {
|
||||
continue
|
||||
}
|
||||
seen[value] = struct{}{}
|
||||
result = append(result, value)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func normalizeMinContextTag(task ScheduleTask) string {
|
||||
if tag := strings.TrimSpace(task.Category); tag != "" {
|
||||
return tag
|
||||
}
|
||||
if tag := strings.TrimSpace(task.Name); tag != "" {
|
||||
return tag
|
||||
}
|
||||
return "General"
|
||||
}
|
||||
|
||||
func sortMinContextSnapshots(snapshotByID map[int]minContextSnapshot) []minContextSnapshot {
|
||||
items := make([]minContextSnapshot, 0, len(snapshotByID))
|
||||
for _, item := range snapshotByID {
|
||||
items = append(items, item)
|
||||
}
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
if items[i].Slot.Day != items[j].Slot.Day {
|
||||
return items[i].Slot.Day < items[j].Slot.Day
|
||||
}
|
||||
if items[i].Slot.SlotStart != items[j].Slot.SlotStart {
|
||||
return items[i].Slot.SlotStart < items[j].Slot.SlotStart
|
||||
}
|
||||
if items[i].Slot.SlotEnd != items[j].Slot.SlotEnd {
|
||||
return items[i].Slot.SlotEnd < items[j].Slot.SlotEnd
|
||||
}
|
||||
return items[i].StateID < items[j].StateID
|
||||
})
|
||||
return items
|
||||
}
|
||||
|
||||
func countMinContextSwitches(ordered []minContextSnapshot) int {
|
||||
if len(ordered) < 2 {
|
||||
return 0
|
||||
}
|
||||
switches := 0
|
||||
prevTag := strings.TrimSpace(ordered[0].ContextTag)
|
||||
for i := 1; i < len(ordered); i++ {
|
||||
currentTag := strings.TrimSpace(ordered[i].ContextTag)
|
||||
if currentTag != prevTag {
|
||||
switches++
|
||||
}
|
||||
prevTag = currentTag
|
||||
}
|
||||
return switches
|
||||
}
|
||||
|
||||
func sameTaskSlot(a, b TaskSlot) bool {
|
||||
return a.Day == b.Day && a.SlotStart == b.SlotStart && a.SlotEnd == b.SlotEnd
|
||||
}
|
||||
@@ -20,7 +20,7 @@ func validateLocalOrderForSinglePlacement(state *ScheduleState, taskID int, targ
|
||||
// validateLocalOrderBatchPlacement 在“多任务同时变更”的假设下做顺序约束校验。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 先把所有候选落位一次性写入克隆态,再统一校验,避免 swap/batch/spread_even 出现伪冲突;
|
||||
// 1. 先把所有候选落位一次性写入克隆态,再统一校验,避免批量局部调整时出现伪冲突;
|
||||
// 2. 只校验 proposals 中涉及的任务,因为只要这些任务仍处于各自前驱/后继之间,就不会破坏同类整体顺序;
|
||||
// 3. 返回首个命中的中文错误,供写工具直接透传给 LLM。
|
||||
func validateLocalOrderBatchPlacement(state *ScheduleState, proposals map[int][]TaskSlot) error {
|
||||
|
||||
@@ -1,320 +0,0 @@
|
||||
package newagenttools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
|
||||
)
|
||||
|
||||
// ==================== 常量 ====================
|
||||
|
||||
const (
|
||||
// defaultTaskQueryLimit 是任务查询默认返回条数。
|
||||
defaultTaskQueryLimit = 5
|
||||
// maxTaskQueryLimit 是任务查询允许的最大返回条数,用于限制 LLM 输出范围。
|
||||
maxTaskQueryLimit = 20
|
||||
)
|
||||
|
||||
// ==================== 优先级中文映射 ====================
|
||||
|
||||
// taskQueryPriorityLabelCN 将象限编号转为中文标签。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只负责 1~4 的合法映射,超出范围返回"未知"。
|
||||
// 2. 不依赖旧链路 agentmodel.PriorityLabelCN,保持新工具自包含。
|
||||
func taskQueryPriorityLabelCN(priority int) string {
|
||||
switch priority {
|
||||
case 1:
|
||||
return "重要且紧急"
|
||||
case 2:
|
||||
return "重要不紧急"
|
||||
case 3:
|
||||
return "简单不重要"
|
||||
case 4:
|
||||
return "复杂不重要"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
// TaskQueryDeps 描述任务查询工具所需的外部依赖。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. QueryTasks 负责真正查库,工具层不直接依赖 DAO;
|
||||
// 2. UserID 由 execute 节点通过 args["_user_id"] 注入,工具层不自行解析会话身份。
|
||||
type TaskQueryDeps struct {
|
||||
// QueryTasks 将解析后的查询参数传入业务层,返回匹配的任务列表。
|
||||
// 调用目的:解耦工具层与 DAO 层,方便测试和替换。
|
||||
QueryTasks func(ctx context.Context, userID int, params TaskQueryParams) ([]TaskQueryResult, error)
|
||||
}
|
||||
|
||||
// TaskQueryParams 描述任务查询工具传给业务层的内部查询参数。
|
||||
//
|
||||
// 输入输出语义:
|
||||
// 1. 所有筛选条件均为可选,Quadrant 为 nil 表示不限象限。
|
||||
// 2. 时间边界为 nil 表示不限时间范围。
|
||||
type TaskQueryParams struct {
|
||||
Quadrant *int
|
||||
SortBy string // deadline | priority | id
|
||||
Order string // asc | desc
|
||||
Limit int
|
||||
IncludeCompleted bool
|
||||
Keyword string
|
||||
DeadlineBefore *time.Time
|
||||
DeadlineAfter *time.Time
|
||||
}
|
||||
|
||||
// TaskQueryResult 描述任务查询工具返回给 LLM 的轻量任务视图。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只承载展示所需字段,避免暴露底层数据库结构。
|
||||
// 2. JSON 序列化后直接作为工具 observation 返回给 LLM。
|
||||
type TaskQueryResult struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
PriorityGroup int `json:"priority_group"`
|
||||
PriorityLabel string `json:"priority_label"`
|
||||
IsCompleted bool `json:"is_completed"`
|
||||
DeadlineAt string `json:"deadline_at,omitempty"`
|
||||
}
|
||||
|
||||
// ==================== 时间解析 ====================
|
||||
|
||||
// taskQueryTimeLayouts 支持的时间格式列表,按优先级尝试解析。
|
||||
var taskQueryTimeLayouts = []string{
|
||||
time.RFC3339,
|
||||
"2006-01-02 15:04:05",
|
||||
"2006-01-02 15:04",
|
||||
"2006-01-02",
|
||||
}
|
||||
|
||||
// parseTaskQueryBoundaryTime 解析截止时间上下界。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. isUpper=true 时,纯日期补到当天 23:59:59。
|
||||
// 2. isUpper=false 时,纯日期补到当天 00:00:00。
|
||||
// 3. 不支持的格式直接返回错误,由调用方决定是否回退。
|
||||
func parseTaskQueryBoundaryTime(raw string, isUpper bool) (*time.Time, error) {
|
||||
text := strings.TrimSpace(raw)
|
||||
if text == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
loc := time.Local
|
||||
for _, layout := range taskQueryTimeLayouts {
|
||||
var (
|
||||
parsed time.Time
|
||||
err error
|
||||
)
|
||||
if layout == time.RFC3339 {
|
||||
parsed, err = time.Parse(layout, text)
|
||||
if err == nil {
|
||||
parsed = parsed.In(loc)
|
||||
}
|
||||
} else {
|
||||
parsed, err = time.ParseInLocation(layout, text, loc)
|
||||
}
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 1. 纯日期格式需要根据上下界补齐时分秒,保证时间区间语义正确。
|
||||
// 2. 若用户输入"2026-04-20"作为上界,意图是"截止到那天结束",
|
||||
// 所以补 23:59:59;作为下界则补 00:00:00。
|
||||
if layout == "2006-01-02" {
|
||||
if isUpper {
|
||||
parsed = time.Date(parsed.Year(), parsed.Month(), parsed.Day(), 23, 59, 59, 0, loc)
|
||||
} else {
|
||||
parsed = time.Date(parsed.Year(), parsed.Month(), parsed.Day(), 0, 0, 0, 0, loc)
|
||||
}
|
||||
}
|
||||
return &parsed, nil
|
||||
}
|
||||
return nil, fmt.Errorf("时间格式不支持: %s", text)
|
||||
}
|
||||
|
||||
// formatTaskQueryTime 将内部时间格式化为给模型展示的分钟级文本。
|
||||
func formatTaskQueryTime(value *time.Time) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return value.In(time.Local).Format("2006-01-02 15:04")
|
||||
}
|
||||
|
||||
// ==================== 工具 Handler ====================
|
||||
|
||||
// NewTaskQueryToolHandler 创建 query_tasks 工具的 handler 闭包。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责参数校验、时间解析、调 deps 查库、组装返回;
|
||||
// 2. 不负责 LLM 交互和会话管理。
|
||||
// 3. state 参数忽略——任务查询不需要 ScheduleState,已注册到 scheduleFreeTools。
|
||||
func NewTaskQueryToolHandler(deps TaskQueryDeps) ToolHandler {
|
||||
return func(state *schedule.ScheduleState, args map[string]any) string {
|
||||
_ = state
|
||||
|
||||
// 1. 提取 _user_id(由 execute 节点在调用前注入)。
|
||||
userID := 0
|
||||
if uid, ok := args["_user_id"].(int); ok {
|
||||
userID = uid
|
||||
}
|
||||
if userID <= 0 {
|
||||
return "工具调用失败:无法识别用户身份。"
|
||||
}
|
||||
|
||||
// 2. 提取并校验查询参数。
|
||||
params, err := extractTaskQueryParams(args)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("工具调用失败:%s", err)
|
||||
}
|
||||
|
||||
// 3. 调用依赖查库。
|
||||
results, err := deps.QueryTasks(context.Background(), userID, params)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("工具调用失败:查询任务时出错(%s)。", err)
|
||||
}
|
||||
|
||||
// 4. 为每条结果填充优先级中文标签。
|
||||
for i := range results {
|
||||
results[i].PriorityLabel = taskQueryPriorityLabelCN(results[i].PriorityGroup)
|
||||
}
|
||||
|
||||
// 5. 返回结构化 JSON。
|
||||
if len(results) == 0 {
|
||||
return `{"total":0,"items":[],"message":"当前没有匹配的任务。"}`
|
||||
}
|
||||
|
||||
output := struct {
|
||||
Total int `json:"total"`
|
||||
Items []TaskQueryResult `json:"items"`
|
||||
Message string `json:"message"`
|
||||
}{
|
||||
Total: len(results),
|
||||
Items: results,
|
||||
Message: fmt.Sprintf("找到 %d 条匹配任务。", len(results)),
|
||||
}
|
||||
|
||||
jsonBytes, marshalErr := json.Marshal(output)
|
||||
if marshalErr != nil {
|
||||
// JSON 序列化失败时降级为纯文本,确保 LLM 仍能拿到关键信息。
|
||||
return fmt.Sprintf("找到 %d 条匹配任务。", len(results))
|
||||
}
|
||||
return string(jsonBytes)
|
||||
}
|
||||
}
|
||||
|
||||
// extractTaskQueryParams 从 args 提取并校验任务查询参数。
|
||||
//
|
||||
// 步骤说明:
|
||||
// 1. 先准备默认值,保证空参数也能执行一次合理查询。
|
||||
// 2. 再校验象限、排序、条数和时间区间,阻止非法参数下沉到业务层。
|
||||
// 3. 若上下界冲突,则直接返回错误。
|
||||
func extractTaskQueryParams(args map[string]any) (TaskQueryParams, error) {
|
||||
params := TaskQueryParams{
|
||||
SortBy: "deadline",
|
||||
Order: "asc",
|
||||
Limit: defaultTaskQueryLimit,
|
||||
IncludeCompleted: false,
|
||||
}
|
||||
|
||||
// 2.1 象限:1~4,超出范围拒绝。
|
||||
if v, ok := args["quadrant"]; ok {
|
||||
switch val := v.(type) {
|
||||
case float64:
|
||||
q := int(val)
|
||||
if q < 1 || q > 4 {
|
||||
return TaskQueryParams{}, fmt.Errorf("quadrant=%d 非法,必须在 1~4", q)
|
||||
}
|
||||
params.Quadrant = &q
|
||||
case int:
|
||||
if val < 1 || val > 4 {
|
||||
return TaskQueryParams{}, fmt.Errorf("quadrant=%d 非法,必须在 1~4", val)
|
||||
}
|
||||
params.Quadrant = &val
|
||||
}
|
||||
}
|
||||
|
||||
// 2.2 排序字段:仅支持 deadline/priority/id。
|
||||
if v, ok := args["sort_by"].(string); ok {
|
||||
sortBy := strings.ToLower(strings.TrimSpace(v))
|
||||
if sortBy != "" {
|
||||
switch sortBy {
|
||||
case "deadline", "priority", "id":
|
||||
params.SortBy = sortBy
|
||||
default:
|
||||
return TaskQueryParams{}, fmt.Errorf("sort_by=%s 非法,仅支持 deadline|priority|id", sortBy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2.3 排序方向:仅支持 asc/desc。
|
||||
if v, ok := args["order"].(string); ok {
|
||||
order := strings.ToLower(strings.TrimSpace(v))
|
||||
if order != "" {
|
||||
switch order {
|
||||
case "asc", "desc":
|
||||
params.Order = order
|
||||
default:
|
||||
return TaskQueryParams{}, fmt.Errorf("order=%s 非法,仅支持 asc|desc", order)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2.4 条数:默认 5,上限 20。
|
||||
if v, ok := args["limit"]; ok {
|
||||
switch val := v.(type) {
|
||||
case float64:
|
||||
params.Limit = int(val)
|
||||
case int:
|
||||
params.Limit = val
|
||||
}
|
||||
}
|
||||
if params.Limit <= 0 {
|
||||
params.Limit = defaultTaskQueryLimit
|
||||
}
|
||||
if params.Limit > maxTaskQueryLimit {
|
||||
params.Limit = maxTaskQueryLimit
|
||||
}
|
||||
|
||||
// 2.5 是否包含已完成任务。
|
||||
if v, ok := args["include_completed"]; ok {
|
||||
switch val := v.(type) {
|
||||
case bool:
|
||||
params.IncludeCompleted = val
|
||||
}
|
||||
}
|
||||
|
||||
// 2.6 关键词。
|
||||
if v, ok := args["keyword"].(string); ok {
|
||||
params.Keyword = strings.TrimSpace(v)
|
||||
}
|
||||
|
||||
// 2.7 时间边界解析,解析失败直接报错,避免查出无意义的结果。
|
||||
beforeRaw, _ := args["deadline_before"].(string)
|
||||
before, err := parseTaskQueryBoundaryTime(beforeRaw, true)
|
||||
if err != nil {
|
||||
return TaskQueryParams{}, fmt.Errorf("deadline_before 格式错误: %s", err)
|
||||
}
|
||||
params.DeadlineBefore = before
|
||||
|
||||
afterRaw, _ := args["deadline_after"].(string)
|
||||
after, err := parseTaskQueryBoundaryTime(afterRaw, false)
|
||||
if err != nil {
|
||||
return TaskQueryParams{}, fmt.Errorf("deadline_after 格式错误: %s", err)
|
||||
}
|
||||
params.DeadlineAfter = after
|
||||
|
||||
// 2.8 时间区间合法性校验:下界不能晚于上界。
|
||||
if params.DeadlineBefore != nil && params.DeadlineAfter != nil &&
|
||||
params.DeadlineAfter.After(*params.DeadlineBefore) {
|
||||
return TaskQueryParams{}, fmt.Errorf("deadline_after 不能晚于 deadline_before")
|
||||
}
|
||||
|
||||
return params, nil
|
||||
}
|
||||
@@ -54,16 +54,13 @@ var toolProfileByName = map[string]toolProfile{
|
||||
"queue_apply_head_move": {Domain: ToolDomainSchedule, Pack: ToolPackQueue},
|
||||
"queue_skip_head": {Domain: ToolDomainSchedule, Pack: ToolPackQueue},
|
||||
|
||||
"place": {Domain: ToolDomainSchedule, Pack: ToolPackMutation},
|
||||
"move": {Domain: ToolDomainSchedule, Pack: ToolPackMutation},
|
||||
"swap": {Domain: ToolDomainSchedule, Pack: ToolPackMutation},
|
||||
"batch_move": {Domain: ToolDomainSchedule, Pack: ToolPackMutation},
|
||||
"spread_even": {Domain: ToolDomainSchedule, Pack: ToolPackMutation},
|
||||
"min_context_switch": {Domain: ToolDomainSchedule, Pack: ToolPackMutation},
|
||||
"unplace": {Domain: ToolDomainSchedule, Pack: ToolPackMutation},
|
||||
"place": {Domain: ToolDomainSchedule, Pack: ToolPackMutation},
|
||||
"move": {Domain: ToolDomainSchedule, Pack: ToolPackMutation},
|
||||
"swap": {Domain: ToolDomainSchedule, Pack: ToolPackMutation},
|
||||
"batch_move": {Domain: ToolDomainSchedule, Pack: ToolPackMutation},
|
||||
"unplace": {Domain: ToolDomainSchedule, Pack: ToolPackMutation},
|
||||
|
||||
"analyze_rhythm": {Domain: ToolDomainSchedule, Pack: ToolPackDeepAnalyze},
|
||||
"analyze_tolerance": {Domain: ToolDomainSchedule, Pack: ToolPackDeepAnalyze},
|
||||
"analyze_rhythm": {Domain: ToolDomainSchedule, Pack: ToolPackDeepAnalyze},
|
||||
|
||||
"web_search": {Domain: ToolDomainSchedule, Pack: ToolPackWeb},
|
||||
"web_fetch": {Domain: ToolDomainSchedule, Pack: ToolPackWeb},
|
||||
|
||||
Reference in New Issue
Block a user