diff --git a/backend/newAgent/HANDOFF_execute_history_reform.md b/backend/newAgent/HANDOFF_execute_history_reform.md new file mode 100644 index 0000000..35c5d7a --- /dev/null +++ b/backend/newAgent/HANDOFF_execute_history_reform.md @@ -0,0 +1,93 @@ +# Execute 阶段历史消息注入改造 Handoff + +## 背景 + +execute 阶段给 LLM 的 4 条消息(msg0-msg3)全部是人工构造的摘要,原始对话历史从未被直接注入。 +导致断连恢复后 "继续" 成为孤立的 currentGoal,LLM 无法从上下文推断原始意图。 + +## 现状 + +### 消息结构 (`execute_context.go:50-67`) + +``` +msg0 (System): 规则 + 工具简表 +msg1 (Assistant): 历史摘要 — 用 pickExecuteUserInputs 提取 firstUser/lastUser 拼成一行 +msg2 (Assistant): 当轮 ReAct Loop 窗口 — thought/tool_call/observation 详细记录 +msg3 (System): 执行状态 + 锚点 — 又用 firstUser/lastUser 拼 "当前用户诉求"/"首轮目标来源" +``` + +### History 存储方式 (`execute.go`) + +每一轮 ReAct loop 往 ConversationContext.History 写 3 条消息: +1. `assistant` + speak(自然语言,如 "我先查看当前安排。") +2. `assistant` + ToolCalls(工具调用 JSON,content 为空) +3. `tool` + observation(工具返回,可能很长) + +### 问题函数 + +- `pickExecuteUserInputs()` (execute_context.go:772) — 从全量 history 取第一条和最后一条 user message +- `extractExecuteGoalAnchors()` (execute_context.go:751) — 用上面的结果作为 initial/current goal +- `buildExecuteMessage1V3()` (execute_context.go:269) — 把 goal 拼成摘要行 +- `buildExecuteMessage3()` (execute_context.go:351) — 把 goal 拼成执行锚点 + +## 改造方案 + +### 核心思路 + +msg1 从"人工提炼的摘要"变成"真实对话流"。只注入 **user + assistant speak**,不含 tool_call / observation(这些已由 msg2 承载)。 + +### 改造后的消息结构 + +``` +msg0 (System): 规则 + 工具简表(不变) +msg1 (Assistant): 真实对话历史(user + assistant speak 交替) +msg2 (Assistant): 当轮 ReAct Loop 窗口(不变) +msg3 (System): 执行状态(删掉 goal 锚点,因为 msg1 已包含完整意图链) +``` + +### msg1 改造示例 + +现在: +``` +历史上下文(仅供参考): +- 用户目标:帮我排一下这些任务类;最近补充:继续 +- 历史归档 ReAct 摘要:已折叠 15 条旧记录... +``` + +改造后: +``` +对话历史: +user: "帮我把周末的课整到工作日" +assistant: "好的,我来查看当前安排。" +assistant: "已找到6个周末任务,开始逐个移动。" +assistant: "第一个任务已移动完成。" +user: "继续" +assistant: "继续处理剩余任务。" + +历史归档 ReAct 摘要:已折叠 15 条旧记录... +``` + +### msg3 改造 + +删掉以下由 firstUser/lastUser 驱动的锚点: +- `当前用户诉求:xxx` +- `首轮目标来源:xxx` + +保留其他锚点(轮次、模式、plan 步骤、任务类等)。 + +### 上下文开销 + +speak 都是短句(1-2 句),60 轮 execute 约 60 条 speak,远小于 msg2 里的工具结果 JSON。 +可考虑加条数上限(如最近 30 条 user+assistant),超出部分走归档摘要。 + +## 涉及文件 + +| 文件 | 改动 | +|---|---| +| `prompt/execute_context.go` | 重写 `buildExecuteMessage1V3`,新增从 history 提取 user+speak 的函数;删 `pickExecuteUserInputs` / `extractExecuteGoalAnchors`;简化 `buildExecuteMessage3` | +| `prompt/execute.go` | 无改动(系统规则不含 list_tasks 相关内容,已清理) | +| `node/execute.go` | 无改动(history 写入逻辑已经存了 speak,无需修改) | + +## 已完成的前置工作 + +- `list_tasks` 工具已删除(registry / prompt / context 全部清理干净,编译通过) diff --git a/backend/newAgent/prompt/execute.go b/backend/newAgent/prompt/execute.go index 684a152..edfdcf8 100644 --- a/backend/newAgent/prompt/execute.go +++ b/backend/newAgent/prompt/execute.go @@ -9,33 +9,31 @@ import ( ) const executeSystemPromptWithPlan = ` -你是 SmartFlow NewAgent 的执行器。你需要在“当前 plan 步骤”约束下推进任务。 +你是 SmartFlow NewAgent 的执行器。你需要在"当前 plan 步骤"约束下推进任务。 你可以做什么: 1. 只围绕当前步骤推进,先读后写,逐步完成当前步骤。 2. 可调用读工具补充事实,再决定下一步。 3. 需要写操作时输出 action=confirm 并附带 tool_call,等待用户确认。 -4. 若用户给出了“二次微调方向”(如负载均衡、某天减负、某类任务后移),优先围绕该方向推进,并在 goal_check 说明满足情况。 +4. 若用户给出了"二次微调方向"(如负载均衡、某天减负、某类任务后移),优先围绕该方向推进,并在 goal_check 说明满足情况。 5. 只有在用户明确允许打乱顺序时,才可使用 min_context_switch 做重排。 6. 多任务微调时默认走队列链路:query_target_tasks(enqueue=true) → queue_pop_head → query_available_slots → queue_apply_head_move / queue_skip_head。 你不要做什么: 1. 不要跳到其他 plan 步骤,不要越级执行。 2. 不要伪造工具结果。 -3. 如果上下文明确“粗排已完成/rough_build_done”,不要把任务当成未排入,不要重新逐个手动 place。 -4. 如果上下文明确“当前未收到明确微调偏好/本轮先收口”,不要继续微调,直接输出 action=done。 +3. 如果上下文明确"粗排已完成/rough_build_done",不要把任务当成未排入,不要重新逐个手动 place。 +4. 如果上下文明确"当前未收到明确微调偏好/本轮先收口",不要继续微调,直接输出 action=done。 5. 不要连续重复同类查询而没有推进;连续两轮同类读查询后,必须转入执行、ask_user,或明确阻塞原因。 -6. list_tasks 的 status 只允许单值:all / existing / suggested / pending。禁止使用 "existing,suggested" 这类拼接值。 -7. 若工具结果与已知事实明显冲突(如无写操作却从“有任务”变成“0任务”),先自我纠错并重查一次,不要直接 ask_user。 -8. 不要连续两轮调用“同一读工具 + 等价 arguments”;若上一轮已成功返回,下一轮必须换工具或进入 confirm。 -9. list_tasks.category 只接受任务类名称,不接受 task_class_ids(如 "1,2,3")。 -10. 不要忽略用户最新补充的微调方向;若与旧目标冲突,以最新用户要求为准。 -11. 若当前顺序策略是“默认保持顺序”,禁止调用 min_context_switch。 -12. 不要把超过 2 条任务打包到 batch_move;大批量调整请改走队列逐项处理。 -13. 不要在未获取队首(queue_pop_head)时直接调用 queue_apply_head_move。 -14. 工具参数必须严格使用 schema 字段,禁止自造别名;例如 day_from/day_to 非法,必须改用 day_start/day_end。 - 15. web_search 仅在“制定学习计划需要查外部资料”时使用(如考试日期、课程信息、校历政策等);日程排布本身(place/move/swap)不需要搜索。 - 16. web_search 拿到 summary 后通常已够用;仅当需要页面详细内容时才调用 web_fetch。 +6. 若工具结果与已知事实明显冲突(如无写操作却从"有任务"变成"0任务"),先自我纠错并重查一次,不要直接 ask_user。 +7. 不要连续两轮调用"同一读工具 + 等价 arguments";若上一轮已成功返回,下一轮必须换工具或进入 confirm。 +8. 不要忽略用户最新补充的微调方向;若与旧目标冲突,以最新用户要求为准。 +9. 若当前顺序策略是"默认保持顺序",禁止调用 min_context_switch。 +10. 不要把超过 2 条任务打包到 batch_move;大批量调整请改走队列逐项处理。 +11. 不要在未获取队首(queue_pop_head)时直接调用 queue_apply_head_move。 +12. 工具参数必须严格使用 schema 字段,禁止自造别名;例如 day_from/day_to 非法,必须改用 day_start/day_end。 +13. web_search 仅在"制定学习计划需要查外部资料"时使用(如考试日期、课程信息、校历政策等);日程排布本身(place/move/swap)不需要搜索。 +14. web_search 拿到 summary 后通常已够用;仅当需要页面详细内容时才调用 web_fetch。 执行规则: 1. 只输出严格 JSON,不要输出 markdown,不要在 JSON 外补充文本。 @@ -43,22 +41,22 @@ const executeSystemPromptWithPlan = ` 3. 写操作:action=confirm + tool_call。 4. 缺关键上下文且无法通过工具补齐:action=ask_user。 5. 仅当当前步骤完成时输出 action=next_plan,并在 goal_check 对照 done_when 给出证据。 - 6. 仅当整体任务完成时输出 action=done,并在 goal_check 总结完成证据。 - 7. 流程应正式终止时输出 action=abort。` +6. 仅当整体任务完成时输出 action=done,并在 goal_check 总结完成证据。 +7. 流程应正式终止时输出 action=abort。` const executeSystemPromptReAct = ` 你是 SmartFlow NewAgent 的执行器,当前处于自由执行模式(无预定义 plan 步骤)。 阶段事实(强约束): -1. 若上下文给出“粗排已完成/rough_build_done”,表示目标任务类已经进入 suggested/existing,不是待排入状态。 -2. 当前阶段目标是“微调”,不是“重新粗排”。 -3. 若上下文明确“当前未收到明确微调偏好/本轮先收口”,应直接结束而不是继续优化循环。 +1. 若上下文给出"粗排已完成/rough_build_done",表示目标任务类已经进入 suggested/existing,不是待排入状态。 +2. 当前阶段目标是"微调",不是"重新粗排"。 +3. 若上下文明确"当前未收到明确微调偏好/本轮先收口",应直接结束而不是继续优化循环。 4. 若用户提出了二次微调方向,本轮优先目标就是满足该方向。 你可以做什么: 1. 你可以基于用户给定的二次微调方向,对 suggested 做定向微调。 2. existing 属于已安排事实层,可用于冲突判断和参考,不作为 move/batch_move/spread_even 的目标。 -3. 你可以先调用读工具补充必要事实(例如 get_overview/list_tasks/query_target_tasks/query_available_slots/get_task_info)。 +3. 你可以先调用读工具补充必要事实(例如 get_overview/query_target_tasks/query_available_slots/get_task_info)。 4. 你可以在需要改动时提出 confirm(move/swap/unplace/batch_move/spread_even)。 5. 只有用户明确允许打乱顺序时,才可使用 min_context_switch。 6. 多任务处理默认使用队列链路:先 query_target_tasks(enqueue=true) 入队,再 queue_pop_head 逐项处理。 @@ -67,18 +65,16 @@ const executeSystemPromptReAct = ` 1. 不要假设任务还没排进去,然后改成逐个手动 place。 2. 不要伪造工具结果。 3. 不要重复做同类查询而没有新增结论;连续两轮同类读查询后,必须转入执行、ask_user,或明确阻塞原因。 -4. list_tasks 的 status 只允许单值:all / existing / suggested / pending。禁止使用 "existing,suggested" 这类拼接值。 -5. 若工具结果与已知事实明显冲突(如无写操作却从“有任务”变成“0任务”),先自我纠错并重查一次,不要直接 ask_user。 -6. 不要连续两轮调用“同一读工具 + 等价 arguments”;若上一轮已成功返回,下一轮必须换工具或进入 confirm。 -7. list_tasks.category 只接受任务类名称,不接受 task_class_ids(如 "1,2,3")。 -8. 若已明确“本轮先收口”,不要继续调用 list_tasks/query_available_slots/move 做无目标微调。 -9. 若用户明确了微调方向,不要只做“局部看起来更空”的随机调整;每次改动都要能对应到该方向。 -10. 若顺序策略为“保持顺序”,禁止调用 min_context_switch。 -11. 不要在同一轮构造大规模 batch_move;batch_move 最多 2 条,超过请走队列逐项处理。 -12. 未调用 queue_pop_head 获取 current 前,不要调用 queue_apply_head_move。 -13. 工具参数必须严格使用 schema 字段,禁止自造别名;例如 day_from/day_to 非法,必须改用 day_start/day_end。 -14. web_search 仅在"制定学习计划需要查外部资料"时使用(如考试日期、课程信息、校历政策等);日程排布本身(place/move/swap)不需要搜索。 -15. web_search 拿到 summary 后通常已够用;仅当需要页面详细内容时才调用 web_fetch。 +4. 若工具结果与已知事实明显冲突(如无写操作却从"有任务"变成"0任务"),先自我纠错并重查一次,不要直接 ask_user。 +5. 不要连续两轮调用"同一读工具 + 等价 arguments";若上一轮已成功返回,下一轮必须换工具或进入 confirm。 +6. 若已明确"本轮先收口",不要继续调用 query_available_slots/move 做无目标微调。 +7. 若用户明确了微调方向,不要只做"局部看起来更空"的随机调整;每次改动都要能对应到该方向。 +8. 若顺序策略为"保持顺序",禁止调用 min_context_switch。 +9. 不要在同一轮构造大规模 batch_move;batch_move 最多 2 条,超过请走队列逐项处理。 +10. 未调用 queue_pop_head 获取 current 前,不要调用 queue_apply_head_move。 +11. 工具参数必须严格使用 schema 字段,禁止自造别名;例如 day_from/day_to 非法,必须改用 day_start/day_end。 +12. web_search 仅在"制定学习计划需要查外部资料"时使用(如考试日期、课程信息、校历政策等);日程排布本身(place/move/swap)不需要搜索。 +13. web_search 拿到 summary 后通常已够用;仅当需要页面详细内容时才调用 web_fetch。 执行规则: 1. 只输出严格 JSON,不要输出 markdown,不要在 JSON 外补充文本。 @@ -338,10 +334,10 @@ func BuildExecuteMessages(state *newagentmodel.CommonState, ctx *newagentmodel.C ) } -// buildExecuteStrictJSONUserPromptWithPlan 在通用 JSON 约束上补充“当前计划步骤”强约束。 +// buildExecuteStrictJSONUserPromptWithPlan 在通用 JSON 约束上补充"当前计划步骤"强约束。 // // 职责边界: -// 1. 负责把“当前是第几步、当前步骤内容、done_when 判定”明确写进用户指令; +// 1. 负责把"当前是第几步、当前步骤内容、done_when 判定"明确写进用户指令; // 2. 不负责替代系统提示词中的工具规则和安全边界; // 3. 当 state 无法提供有效当前步骤时,仅追加兜底提示,不在此处推进流程状态。 func buildExecuteStrictJSONUserPromptWithPlan(state *newagentmodel.CommonState) string { @@ -416,14 +412,12 @@ func buildExecuteStrictJSONUserPrompt() string { - 不要写 {"tool_call":{"name":"工具名","parameters":{...}}} - 非 abort 动作不要输出 abort 字段 - action 为 continue / ask_user / confirm 时,必须输出非空 speak -- list_tasks.arguments.status 仅允许 all / existing / suggested / pending 的单值;如需看 existing+suggested,请用 all -- list_tasks.arguments.category 仅接受任务类名称,不要传 task_class_ids(如 "1,2,3") - 若读工具结果与已知事实明显冲突,先修正参数并重查一次,再决定是否 ask_user -- 不要连续两轮调用“同一读工具 + 等价 arguments”;若上一轮已成功返回,下一轮必须换工具或进入 confirm +- 不要连续两轮调用"同一读工具 + 等价 arguments";若上一轮已成功返回,下一轮必须换工具或进入 confirm - 若用户本轮给了二次微调方向,优先满足该方向,再考虑通用均衡优化 -- 若上下文已明确“当前未收到微调偏好,本轮先收口”,请直接输出 action=done +- 若上下文已明确"当前未收到微调偏好,本轮先收口",请直接输出 action=done - 仅当顺序策略明确允许打乱顺序时,才可以调用 min_context_switch -- spread_even 用于“范围内均匀化”,必须先用 query_target_tasks 明确目标任务集合 +- spread_even 用于"范围内均匀化",必须先用 query_target_tasks 明确目标任务集合 - 多任务调整默认先调用 query_target_tasks(enqueue=true),再用 queue_pop_head 逐项处理 - queue_apply_head_move 只能用于 current 任务;若当前任务无法落位,调用 queue_skip_head 后继续 - batch_move 一次最多 2 条;超过 2 条必须改走队列逐项处理 diff --git a/backend/newAgent/prompt/execute_context.go b/backend/newAgent/prompt/execute_context.go index e9902de..04c5ef5 100644 --- a/backend/newAgent/prompt/execute_context.go +++ b/backend/newAgent/prompt/execute_context.go @@ -475,8 +475,6 @@ func renderExecuteToolReturnHint(toolName string) (returnType string, sample str switch strings.ToLower(strings.TrimSpace(toolName)) { case "get_overview": return returnType, "规划窗口共27天...课程占位条目34个...任务清单(全量,已过滤课程)..." - case "list_tasks": - return returnType, "已预排任务共24个: [35]第一章随机事件与概率 — 已预排至 第3天第5-6节..." case "get_task_info": return returnType, "[35]第一章随机事件与概率 | 状态:已预排(suggested) | 占用时段:第3天第5-6节" case "query_available_slots": diff --git a/backend/newAgent/tools/registry.go b/backend/newAgent/tools/registry.go index 95741f6..92b0612 100644 --- a/backend/newAgent/tools/registry.go +++ b/backend/newAgent/tools/registry.go @@ -177,14 +177,6 @@ func NewDefaultRegistryWithDeps(deps DefaultRegistryDeps) *ToolRegistry { }, ) - r.Register("list_tasks", - "列出任务清单,可按类别和状态过滤。category 传任务类名称,status 仅支持单值 all/existing/suggested/pending。", - `{"name":"list_tasks","parameters":{"category":{"type":"string"},"status":{"type":"string","enum":["all","existing","suggested","pending"]}}}`, - func(state *schedule.ScheduleState, args map[string]any) string { - return schedule.ListTasks(state, schedule.ArgsStringPtr(args, "category"), schedule.ArgsStringPtr(args, "status")) - }, - ) - r.Register("get_task_info", "查询单个任务详细信息,包括类别、状态、占用时段、嵌入关系。", `{"name":"get_task_info","parameters":{"task_id":{"type":"int","required":true}}}`, diff --git a/backend/newAgent/tools/schedule/read_helpers.go b/backend/newAgent/tools/schedule/read_helpers.go index 899f534..f34023f 100644 --- a/backend/newAgent/tools/schedule/read_helpers.go +++ b/backend/newAgent/tools/schedule/read_helpers.go @@ -42,7 +42,7 @@ func formatTaskLabel(task ScheduleTask) string { // formatTaskLabelWithCategory 输出带类别和锁定标记的标签。 // 如 "[1]高等数学(课程,固定)" 或 "[2]英语(课程)"。 -// 用于 get_overview 和 list_tasks 的概要输出。 +// 用于 get_overview 的概要输出。 func formatTaskLabelWithCategory(task ScheduleTask) string { label := fmt.Sprintf("[%d]%s(%s", task.StateID, task.Name, task.Category) if task.Locked { diff --git a/backend/newAgent/tools/schedule/read_tools.go b/backend/newAgent/tools/schedule/read_tools.go index 136bbc5..e15741f 100644 --- a/backend/newAgent/tools/schedule/read_tools.go +++ b/backend/newAgent/tools/schedule/read_tools.go @@ -500,176 +500,6 @@ func findFirstEmbeddablePosition(state *ScheduleState, day, duration int) (*Sche return best.task, best.slotStart, best.slotEnd } -// ListTasks 列出任务清单,可按类别和状态过滤。 -// category 选填(nil 不过滤),status 选填(nil 默认 "all")。 -// 输出按状态分组:已安排 -> 已预排 -> 待安排,组内按 stateID 升序。 -func ListTasks(state *ScheduleState, category, status *string) string { - // 1. 确定过滤状态。 - statusFilter := "all" - if status != nil { - statusFilter = *status - } - statusFilter = strings.ToLower(strings.TrimSpace(statusFilter)) - if statusFilter == "" { - statusFilter = "all" - } - if err := validateListTasksStatus(statusFilter); err != nil { - return fmt.Sprintf("查询失败:%s", err.Error()) - } - categoryFilter := "" - if category != nil { - categoryFilter = strings.TrimSpace(*category) - } - hasCategoryFilter := categoryFilter != "" - - // 2. 过滤 + 分组。 - var existingTasks, suggestedTasks, pendingTasks []ScheduleTask - for i := range state.Tasks { - t := state.Tasks[i] - // 类别过滤。 - if hasCategoryFilter && t.Category != categoryFilter { - continue - } - - switch { - case IsPendingTask(t): - if statusFilter != "all" && statusFilter != "pending" { - continue - } - pendingTasks = append(pendingTasks, t) - case IsSuggestedTask(t): - if statusFilter != "all" && statusFilter != "suggested" { - continue - } - suggestedTasks = append(suggestedTasks, t) - default: - if statusFilter != "all" && statusFilter != "existing" { - continue - } - existingTasks = append(existingTasks, t) - } - } - - // 3. 按 stateID 排序。 - sort.Slice(existingTasks, func(i, j int) bool { return existingTasks[i].StateID < existingTasks[j].StateID }) - sort.Slice(suggestedTasks, func(i, j int) bool { return suggestedTasks[i].StateID < suggestedTasks[j].StateID }) - sort.Slice(pendingTasks, func(i, j int) bool { return pendingTasks[i].StateID < pendingTasks[j].StateID }) - - // 4. 纯待安排模式:只输出待安排任务。 - if statusFilter == "pending" { - if len(pendingTasks) == 0 { - return formatListTasksEmptyResult(statusFilter, categoryFilter) - } - return formatPendingList(pendingTasks) - } - - // 5. 纯已预排模式:只输出已预排任务。 - if statusFilter == "suggested" { - if len(suggestedTasks) == 0 { - return formatListTasksEmptyResult(statusFilter, categoryFilter) - } - return formatSuggestedList(state, suggestedTasks) - } - - // 6. 纯已安排模式:只输出已安排任务。 - if statusFilter == "existing" { - if len(existingTasks) == 0 { - return formatListTasksEmptyResult(statusFilter, categoryFilter) - } - return formatExistingList(state, existingTasks) - } - - // 7. 全部模式:统计 + 分组输出。 - total := len(existingTasks) + len(suggestedTasks) + len(pendingTasks) - var sb strings.Builder - sb.WriteString(fmt.Sprintf("共%d个任务,已安排(existing)%d个,已预排(suggested)%d个,待安排(pending)%d个。\n", total, len(existingTasks), len(suggestedTasks), len(pendingTasks))) - - if len(existingTasks) > 0 { - sb.WriteString("\n已安排(existing):\n") - sb.WriteString(formatExistingList(state, existingTasks)) - } - if len(suggestedTasks) > 0 { - sb.WriteString("\n已预排(suggested):\n") - sb.WriteString(formatSuggestedList(state, suggestedTasks)) - } - if len(pendingTasks) > 0 { - sb.WriteString("\n待安排(pending):\n") - sb.WriteString(formatPendingList(pendingTasks)) - } - - return sb.String() -} - -// formatListTasksEmptyResult 统一构造 list_tasks 空结果文案。 -// -// 设计意图: -// 1. 明确告诉模型“为什么为空”,避免把空字符串误解为工具异常或上下文缺失; -// 2. 对常见误用 category=ID 列表给出直接纠偏提示,减少死循环重试。 -func formatListTasksEmptyResult(statusFilter, categoryFilter string) string { - statusLabel := map[string]string{ - "all": "任意状态", - "existing": "已安排(existing)", - "suggested": "已预排(suggested)", - "pending": "待安排(pending)", - } - target := statusLabel[statusFilter] - if target == "" { - target = statusFilter - } - - if strings.TrimSpace(categoryFilter) == "" { - return fmt.Sprintf("查询结果为空:当前没有%s任务。", target) - } - if looksLikeTaskClassIDList(categoryFilter) { - return fmt.Sprintf("查询结果为空:category=%q 未匹配到任务。category 参数按任务类名称匹配,不支持 task_class_ids 列表。", categoryFilter) - } - return fmt.Sprintf("查询结果为空:category=%q 下没有%s任务。", categoryFilter, target) -} - -// looksLikeTaskClassIDList 判断 category 文本是否像“逗号分隔的数字 ID 列表”。 -func looksLikeTaskClassIDList(value string) bool { - value = strings.TrimSpace(value) - if value == "" { - return false - } - parts := strings.Split(value, ",") - if len(parts) == 0 { - return false - } - for _, part := range parts { - part = strings.TrimSpace(part) - if part == "" { - return false - } - for _, r := range part { - if r < '0' || r > '9' { - return false - } - } - } - return true -} - -// validateListTasksStatus 校验 list_tasks.status 的输入值。 -// -// 职责边界: -// 1. 负责拦截非法 status,避免“静默返回 0 条”误导模型; -// 2. 不负责自动拆分或容错纠偏(如 existing,suggested),统一要求调用方改成合法单值。 -func validateListTasksStatus(status string) error { - // 1. status 已在调用方归一化为小写并去空格。 - // 2. 合法值仅允许 all / existing / suggested / pending。 - switch status { - case "all", "existing", "suggested", "pending": - return nil - } - - // 3. 对最常见误用给出明确修复建议,避免模型继续循环错误调用。 - if strings.Contains(status, ",") { - return fmt.Errorf("status 只支持单值 all/existing/suggested/pending,不支持 \"%s\"。如需同时查看 existing+suggested,请使用 all", status) - } - return fmt.Errorf("status=%q 非法,仅支持 all/existing/suggested/pending", status) -} - // GetTaskInfo 查询单个任务的详细信息。 // taskID 必填,为 state 内的 state_id。 // 不存在时返回错误信息字符串。 diff --git a/backend/newAgent/tools/schedule/status.go b/backend/newAgent/tools/schedule/status.go index 7b30e5a..8aa9184 100644 --- a/backend/newAgent/tools/schedule/status.go +++ b/backend/newAgent/tools/schedule/status.go @@ -50,7 +50,7 @@ func IsSuggestedTask(task ScheduleTask) bool { // // 说明: // 1. 这里会主动排除 suggested 兼容形态,避免旧快照里的 existing+Duration>0 被误当成已确定任务; -// 2. 这样 list_tasks / get_overview 才能稳定区分“事实层 existing”和“建议层 suggested”。 +// 2. 这样 get_overview 等工具才能稳定区分”事实层 existing”和”建议层 suggested”。 func IsExistingTask(task ScheduleTask) bool { return task.Status == TaskStatusExisting && !IsSuggestedTask(task) }