Version: 0.9.28.dev.260418

后端:
1. 查任务功能(query_tasks)从旧 Agent 链路迁移为新 execute 工具
- 新增 newAgent/tools/taskquery.go:自包含 TaskQueryToolHandler,零引用旧 agent 包;参数校验(象限1~4、排序白名单、limit上限20)、时间边界解析(四种格式自动补齐)、结构化 JSON 结果
- newAgent/tools/registry.go:DefaultRegistryDeps 新增 TaskQuery 字段;scheduleFreeTools 新增 query_tasks;注册 query_tasks 读工具(无需 confirm,不依赖 ScheduleState)
- newAgent/prompt/execute.go:有 plan / ReAct 两套系统 prompt 执行规则新增 query_tasks 读操作说明,支持按象限、关键词、截止时间筛选排序
- service/agentsvc/agent_task_query.go:queryTasksForAgent 导出为 QueryTasksForTool,供启动层闭包调用;内部调用同步改为 QueryTasksForTool
- cmd/start.go:NewDefaultRegistryWithDeps 注入 TaskQuery 闭包,桥接新工具参数到旧 service 层查询能力,复用已有过滤/排序/紧急度提升逻辑;旧链路全部保留不动
2. order_guard 条件触发——仅日程写操作后走守卫节点
- newAgent/model/common_state.go:新增 HasScheduleWriteOps 标记字段;ResetForNextRun 追加清理
- newAgent/node/execute.go:executeToolCall / executePendingTool 两处写工具执行后,通过 registry.IsWriteTool 判断并置 HasScheduleWriteOps=true
- newAgent/graph/common_graph.go:branchAfterExecute 分支条件新增 HasScheduleWriteOps 判断,非日程操作(query_tasks / quick_note_create / web_search 等)直接 deliver 跳过 order_guard;branchAfterRoughBuild 不变,粗排天然是写操作

前端:
1. 助手面板新增 SSE 流式请求停止按钮
- AssistantPanel.vue:新增 streamAbortController ref 和 stopStreaming 方法;fetchChatStream / streamAssistantReply 透传 AbortSignal;sendMessage 创建 AbortController,catch 区分用户主动中断与异常;流式期间显示红色停止按钮替代发送按钮
2. 象限卡片任务列表取消硬截断,改为滚动查看
- TaskQuadrantCard.vue:visibleTasks 不再 slice(0,4),全部展示;quadrant-list 新增 max-height + overflow-y + 自定义滚动条样式

仓库:无
This commit is contained in:
Losita
2026-04-18 13:32:26 +08:00
parent 4fbf9397d2
commit a5d301ceb9
10 changed files with 498 additions and 26 deletions

View File

@@ -0,0 +1,320 @@
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
}