✨ feat(task): 新增四象限任务懒触发自动平移链路(读时派生 + Outbox 异步收敛) - 🧩 为 `Task` 模型新增 `urgency_threshold_at` 字段,并补充复合索引 `user_id,is_completed,urgency_threshold_at,priority` 及相关事件 payload - ♻️ 重构 `TaskService.GetUserTasks`:调整为“缓存/DB 读取原始任务 -> 读时派生优先级(`2 -> 1`、`4 -> 3`)-> 通过 `SETNX` 去重后发布平移事件”的处理链路 - 🚚 新增任务平移事件链路: - `service/events/task_urgency_promote.go` - 事件类型:`task.urgency.promote.requested` - 支持 `Publish` + `RegisterHandler` + `ConsumeAndMarkConsumed` 的事务化消费流程 - 🛡️ 为 `TaskDAO` 新增幂等批量更新能力 `PromoteTaskUrgencyByIDs`,采用条件更新策略,仅对“达到阈值且未完成”的任务生效 - 🔌 更新启动接线逻辑:注册任务平移 handler,并将 `eventBus` 注入 `NewTaskService` - 🧹 修复并升级任务缓存层,统一为 `[]model.Task` 原始模型缓存;同时清理误导性注释,并补充详细中文步骤化注释 - 🔗 打通 `QuickNote` 链路中的 `urgency_threshold_at` 透传与校验,覆盖 `state` / `tool` / `nodes` / `prompt` / `agent_quick_note` 全链路 - 💾 写库时补充落库 `task.UrgencyThresholdAt` - 📝 新增功能决策记录 之前画的饼正在一块块填上~这一块饼填上之后,第一批开发的后端部分基本已经搞定了。后面的功能全都是天马行空的拓展功能。
175 lines
7.2 KiB
Go
175 lines
7.2 KiB
Go
package quicknote
|
||
|
||
import "time"
|
||
|
||
const (
|
||
// QuickNoteDatetimeMinuteLayout 是“随口记”链路内部统一的分钟级时间格式。
|
||
// 说明:
|
||
// 1) 用于把“当前时间基准”传给模型,避免模型在相对时间推断时出现秒级抖动。
|
||
// 2) 用于日志和调试,读起来比 RFC3339 更直观。
|
||
QuickNoteDatetimeMinuteLayout = "2006-01-02 15:04"
|
||
|
||
// quickNoteTimezoneName 是随口记链路默认业务时区。
|
||
// 这里固定为东八区,避免容器运行在 UTC 时把“明天/今晚”解释偏移到错误日期。
|
||
quickNoteTimezoneName = "Asia/Shanghai"
|
||
|
||
// QuickNotePriorityImportantUrgent 对应四象限里的“重要且紧急”。
|
||
// 在当前 tasks 表中映射为 priority=1(数值越小优先级越高)。
|
||
QuickNotePriorityImportantUrgent = 1
|
||
// QuickNotePriorityImportantNotUrgent 对应“重要不紧急”。
|
||
QuickNotePriorityImportantNotUrgent = 2
|
||
// QuickNotePrioritySimpleNotImportant 对应“简单不重要”。
|
||
QuickNotePrioritySimpleNotImportant = 3
|
||
// QuickNotePriorityComplexNotImportant 对应“不简单不重要”。
|
||
QuickNotePriorityComplexNotImportant = 4
|
||
)
|
||
|
||
// IsValidTaskPriority 判断优先级是否合法。
|
||
// 目前后端任务模型限定为 1~4。
|
||
func IsValidTaskPriority(priority int) bool {
|
||
return priority >= QuickNotePriorityImportantUrgent && priority <= QuickNotePriorityComplexNotImportant
|
||
}
|
||
|
||
// PriorityLabelCN 把优先级数值转换为中文标签,便于拼接给用户的自然语言回复。
|
||
func PriorityLabelCN(priority int) string {
|
||
switch priority {
|
||
case QuickNotePriorityImportantUrgent:
|
||
return "重要且紧急"
|
||
case QuickNotePriorityImportantNotUrgent:
|
||
return "重要不紧急"
|
||
case QuickNotePrioritySimpleNotImportant:
|
||
return "简单不重要"
|
||
case QuickNotePriorityComplexNotImportant:
|
||
return "不简单不重要"
|
||
default:
|
||
return "未知优先级"
|
||
}
|
||
}
|
||
|
||
// QuickNoteState 是“AI随口记”链路在 graph 节点间传递的统一状态容器。
|
||
// 设计目标:
|
||
// 1) 把本次请求的上下文收拢到一个结构里,降低节点函数参数散落;
|
||
// 2) 让“识别、评估、写库、重试、回复”每一步都可追踪;
|
||
// 3) 便于后续扩展打点和可观测字段(例如时间解析失败原因)。
|
||
type QuickNoteState struct {
|
||
// 基础上下文:用于日志关联与用户隔离。
|
||
TraceID string
|
||
UserID int
|
||
ConversationID string
|
||
|
||
// RequestNow 记录“请求进入随口记链路时”的时间基准(分钟级)。
|
||
// 所有相对时间(明天/后天/下周一)都必须基于这个时间计算,
|
||
// 这样同一次请求内不会因为时间流逝产生口径漂移。
|
||
RequestNow time.Time
|
||
// RequestNowText 是 RequestNow 的字符串形式,主要用于 prompt 注入。
|
||
RequestNowText string
|
||
|
||
// 用户原始输入(例如:提醒我下周日之前完成大作业)。
|
||
UserInput string
|
||
|
||
// 意图判定结果。
|
||
IsQuickNoteIntent bool
|
||
IntentJudgeReason string
|
||
|
||
// 结构化抽取结果:由“意图识别/信息抽取”节点写入。
|
||
ExtractedTitle string
|
||
ExtractedDeadline *time.Time
|
||
ExtractedDeadlineText string
|
||
// ExtractedUrgencyThreshold 表示“进入紧急象限的分界时间”。
|
||
//
|
||
// 语义说明:
|
||
// 1. 该时间由模型规划后给出,并在后端做解析校验;
|
||
// 2. 到达该时间后,任务可在“读时派生 + 异步落库”链路中被自动平移;
|
||
// 3. 为空表示该任务不参与自动平移。
|
||
ExtractedUrgencyThreshold *time.Time
|
||
ExtractedPriority int
|
||
// ExtractedBanter 是聚合规划阶段生成的“轻松跟进句”。
|
||
// 该字段非空时,最终回复阶段可直接复用,避免再触发一次独立润色模型调用。
|
||
ExtractedBanter string
|
||
// PlannedBySingleCall 标记本次是否走了“单请求聚合规划”快路径。
|
||
// 用于在后续节点做更激进的性能策略(例如缺失字段时直接本地兜底,避免再触发模型调用)。
|
||
PlannedBySingleCall bool
|
||
|
||
// ExtractedPriorityReason 记录优先级评估理由,便于后续排查模型判断是否符合预期。
|
||
ExtractedPriorityReason string
|
||
// DeadlineValidationError 记录时间校验失败原因。
|
||
// 只要该字段非空,就说明用户提供了无法解析的时间表达,本次请求不应落库。
|
||
DeadlineValidationError string
|
||
|
||
// 工具调用过程状态:用于重试与故障回溯。
|
||
ToolAttemptCount int
|
||
MaxToolRetry int
|
||
LastToolError string
|
||
|
||
// 最终持久化结果:由“写库工具”节点回填。
|
||
PersistedTaskID int
|
||
Persisted bool
|
||
|
||
// AssistantReply 是 graph 最终给用户的回复文案。
|
||
AssistantReply string
|
||
}
|
||
|
||
// NewQuickNoteState 创建随口记状态对象并初始化默认重试次数。
|
||
func NewQuickNoteState(traceID string, userID int, conversationID, userInput string) *QuickNoteState {
|
||
// 1. 在“进入链路”这一刻固化时间基准,后续所有相对时间都以它为准。
|
||
requestNow := quickNoteNowToMinute()
|
||
return &QuickNoteState{
|
||
TraceID: traceID,
|
||
UserID: userID,
|
||
ConversationID: conversationID,
|
||
RequestNow: requestNow,
|
||
RequestNowText: formatQuickNoteTimeToMinute(requestNow),
|
||
UserInput: userInput,
|
||
MaxToolRetry: 3,
|
||
}
|
||
}
|
||
|
||
// CanRetryTool 判断当前是否还能继续重试工具调用。
|
||
func (s *QuickNoteState) CanRetryTool() bool {
|
||
// 规则:已尝试次数 < 最大重试次数 才允许继续。
|
||
// 这里不做 <=,是为了让“第 MaxToolRetry 次失败后”及时停机并给用户明确反馈。
|
||
return s.ToolAttemptCount < s.MaxToolRetry
|
||
}
|
||
|
||
// RecordToolError 记录一次工具调用失败。
|
||
func (s *QuickNoteState) RecordToolError(errMsg string) {
|
||
// 1. 每失败一次都要累加计数,供分支节点判断是否继续重试。
|
||
s.ToolAttemptCount++
|
||
// 2. 保留最后一次错误,便于日志与排障定位“最终失败原因”。
|
||
s.LastToolError = errMsg
|
||
}
|
||
|
||
// RecordToolSuccess 记录一次工具调用成功。
|
||
func (s *QuickNoteState) RecordToolSuccess(taskID int) {
|
||
// 1. 成功同样计入尝试次数,便于还原完整调用轨迹。
|
||
s.ToolAttemptCount++
|
||
// 2. 回填 task_id 和成功标志,供后续节点拼接成功回复。
|
||
s.PersistedTaskID = taskID
|
||
s.Persisted = true
|
||
// 3. 成功后清空错误,避免后续误读历史失败信息。
|
||
s.LastToolError = ""
|
||
}
|
||
|
||
// quickNoteLocation 返回随口记链路使用的业务时区。
|
||
func quickNoteLocation() *time.Location {
|
||
// 1. 优先加载业务固定时区,保证“明天/今晚”等语义与用户预期一致。
|
||
loc, err := time.LoadLocation(quickNoteTimezoneName)
|
||
if err != nil {
|
||
// 2. 极端情况下回退到系统本地时区,避免因时区加载失败导致链路整体不可用。
|
||
return time.Local
|
||
}
|
||
return loc
|
||
}
|
||
|
||
// quickNoteNowToMinute 返回当前时间并截断到分钟级。
|
||
func quickNoteNowToMinute() time.Time {
|
||
// 统一截断到分钟,避免秒级抖动导致“同一次请求前后解析口径不一致”。
|
||
return time.Now().In(quickNoteLocation()).Truncate(time.Minute)
|
||
}
|
||
|
||
// formatQuickNoteTimeToMinute 将时间格式化为分钟级字符串。
|
||
func formatQuickNoteTimeToMinute(t time.Time) string {
|
||
// 输出前统一转换到业务时区,避免日志和 prompt 出现跨时区混淆。
|
||
return t.In(quickNoteLocation()).Format(QuickNoteDatetimeMinuteLayout)
|
||
}
|