Version: 0.9.27.dev.260418

后端:
1. SSE 心跳保活——解决 Vite dev proxy 在 LLM thinking 静默期判 idle 断连
- api/agent.go:ChatAgent 新增 5 秒 heartbeat ticker,select 增加 heartbeat.C 分支,每 5 秒写入 SSE 注释行 : ping\n\n 并 Flush
- service/agentsvc/agent_newagent.go:graph 执行失败时增加 context.Canceled / requestCtx.Err() 判断,客户端断连只记 warn 不推 errChan 也不跑 fallback,消除 "错误通道已满" 日志噪音
2. 随口记工具(quick_note_create)接入新 Agent 链路
- agent/node/quicknote.go:parseOptionalDeadlineWithNow / quickNoteLocation 首字母大写导出,供新链路复用旧链路成熟的时间解析和时区能力
- agent/node/quicknote_tool.go:parseOptionalDeadline / quickNoteLocation 同步导出,补充调用目的注释
- newAgent/tools/quicknote.go:新增 QuickNoteToolHandler,实现新链路 quick_note_create 工具的参数校验、时间解析、写库调用
- newAgent/tools/registry.go:DefaultRegistryDeps 新增 QuickNote 字段;新增 RequiresScheduleState 方法和 scheduleFreeTools 集合;注册 quick_note_create 工具(不加入 writeTools,不走 confirm 确认)
- cmd/start.go:NewDefaultRegistryWithDeps 注入 QuickNote.CreateTask 闭包,捕获 taskRepo 实例写库
3. Execute 节点随口记 speak 清空 + 非 ScheduleState 工具支持
- newAgent/node/execute.go:新增非写工具 confirm→continue 自动降级逻辑;新增 quick_note_create speak 强制清空,收口统一交给 deliver,避免 execute + deliver 重复废话
- newAgent/node/execute.go:executeToolCall / executePendingTool 中 scheduleState nil 检查改为仅拦截 RequiresScheduleState 的工具;为不依赖 ScheduleState 的工具自动注入 _user_id 参数
- newAgent/prompt/execute.go:有 plan / ReAct 两套系统 prompt 中,"写操作"规则细化为"日程写操作";新增 quick_note_create 专属执行规则:speak 必须留空,收口由 deliver 完成,调用成功后可 continue 处理多任务
- newAgent/prompt/chat.go:execute 路由描述补充"记录任务/提醒"场景

前端:
1. Vite dev proxy SSE 透传配置
- vite.config.ts:/api 代理新增 configure 回调,设置 x-accel-buffering: no 和 cache-control: no-cache,禁用代理缓冲
2.SSE 流式处理修复
- AssistantPanel.vue:reasoning_content 守卫放宽,移除 !assistantMessage.content.trim() 外层条件,正文回流后仍允许追加 reasoning(工具调用摘要、阶段状态等),不再吞掉 execute/deliver 的 reasoning_content
- AssistantPanel.vue:流式完成后跳过 loadConversationMessages,避免 persistVisibleMessage 尚未落库时 merge 产生重复或丢失

仓库:无
This commit is contained in:
Losita
2026-04-18 11:20:49 +08:00
parent d8280cc647
commit 4fbf9397d2
13 changed files with 549 additions and 1416 deletions

View File

@@ -0,0 +1,140 @@
package newagenttools
import (
"encoding/json"
"fmt"
"strings"
"time"
agentmodel "github.com/LoveLosita/smartflow/backend/agent/model"
agentnode "github.com/LoveLosita/smartflow/backend/agent/node"
"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 agentmodel.QuickNotePriorityImportantUrgent
}
return agentmodel.QuickNotePriorityImportantNotUrgent
}
return agentmodel.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 := agentnode.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 !agentmodel.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 := agentmodel.PriorityLabelCN(priorityGroup)
deadlineStr := ""
if deadline != nil {
deadlineStr = deadline.In(agentnode.QuickNoteLocation()).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)
}
}

View File

@@ -31,6 +31,9 @@ type DefaultRegistryDeps struct {
// WebSearchProvider Web 搜索供应商。为 nil 时 web_search / web_fetch 返回"暂未启用",不阻断主流程。
WebSearchProvider web.SearchProvider
// QuickNote 随口记工具依赖。CreateTask 为 nil 时 quick_note_create 返回错误提示,不阻断主流程。
QuickNote QuickNoteDeps
}
// ToolRegistry 管理工具注册、查找与执行。
@@ -100,6 +103,12 @@ func (r *ToolRegistry) IsWriteTool(name string) bool {
return writeTools[name]
}
// RequiresScheduleState 判断工具是否依赖 ScheduleState。
// 调用目的execute 节点据此决定是否允许在 ScheduleState 为 nil 时调用该工具。
func (r *ToolRegistry) RequiresScheduleState(name string) bool {
return !scheduleFreeTools[name]
}
// ==================== 写工具集合 ====================
var writeTools = map[string]bool{
@@ -113,6 +122,15 @@ var writeTools = map[string]bool{
"unplace": true,
}
// ==================== 不依赖 ScheduleState 的工具集合 ====================
// 调用目的这些工具不需要日程状态即可执行execute 节点在 ScheduleState 为 nil 时允许调用。
var scheduleFreeTools = map[string]bool{
"quick_note_create": true,
"web_search": true,
"web_fetch": true,
}
// ==================== 默认注册表 ====================
// NewDefaultRegistry 创建默认日程工具注册表。
@@ -310,6 +328,18 @@ func NewDefaultRegistryWithDeps(deps DefaultRegistryDeps) *ToolRegistry {
},
)
// --- 随口记工具 ---
// 调用目的:将"帮我记一下明天开会"等随口任务请求直接写入数据库,无需 ScheduleState。
// 不加入 writeTools随口记是用户明确指令不需要 confirm 节点二次确认。
if deps.QuickNote.CreateTask != nil {
quickNoteHandler := NewQuickNoteToolHandler(deps.QuickNote)
r.Register("quick_note_create",
"记录一条任务/提醒/待办事项到用户的任务列表。支持中文相对时间如“明天下午3点”、“下周一”。title 必填。记录成功后回复时应包含一句与任务内容相关的轻松跟进话术不超过30字类似朋友间的友好调侃。",
`{"name":"quick_note_create","parameters":{"title":{"type":"string","required":true,"description":"任务标题,简洁明确"},"deadline_at":{"type":"string","description":"可选截止时间,支持 yyyy-MM-dd HH:mm 或中文相对时间(明天/下周一/后天等)"},"priority_group":{"type":"int","description":"优先级(1重要且紧急,2重要不紧急,3简单不重要,4复杂不重要);信息足够时请显式填写,不确定时可不填,由工具层自动推断"}}}`,
quickNoteHandler,
)
}
// --- Web 搜索读工具 ---
// 1. provider 为 nil 时 handler 返回"暂未启用"的 observation不会阻断主流程
// 2. 两个工具均为读操作,走 action=continue + tool_call 模式。