Version: 0.9.48.dev.260428
后端: 1.新增任务批量状态查询能力,补齐入参归一化、单次上限控制、按当前用户隔离与空结果兼容。 2.QuickTask 从纯文本升级为“正文 + business_card”输出,覆盖 task_record/task_query 两类卡片语义。 3.查询链路新增时间窗边界筛选与异常窗口兜底,SSE/timeline 同步扩展 business_card 事件并持久化。 前端: 1.助手面板接入任务状态 hydration 与增量同步,卡片状态可实时联动(完成/撤销、编辑、删除、同步中)。 2.TaskRecord/TaskQuery 卡片升级为可交互任务卡,并新增对话页任务编辑弹窗与回写闭环。 3.助手路由升级为 /assistant/:id?,支持 URL 驱动会话切换与刷新恢复。 仓库: 同步更新 business card 前端对接说明文档。
This commit is contained in:
@@ -18,8 +18,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
quickTaskStageName = "quick_task"
|
||||
quickTaskBlockID = "qt_main"
|
||||
quickTaskStageName = "quick_task"
|
||||
quickTaskBlockID = "qt_main"
|
||||
quickTaskResultCardID = "quick_task.result"
|
||||
taskRecordSourceQuickNote = "quick_note"
|
||||
)
|
||||
|
||||
// QuickTaskNodeInput 描述快捷任务节点的输入。
|
||||
@@ -43,14 +45,27 @@ type quickTaskDecision struct {
|
||||
TaskID *int `json:"task_id,omitempty"`
|
||||
|
||||
// query 参数
|
||||
Quadrant *int `json:"quadrant,omitempty"`
|
||||
Keyword string `json:"keyword,omitempty"`
|
||||
Limit *int `json:"limit,omitempty"`
|
||||
Quadrant *int `json:"quadrant,omitempty"`
|
||||
Keyword string `json:"keyword,omitempty"`
|
||||
Limit *int `json:"limit,omitempty"`
|
||||
DeadlineAfter string `json:"deadline_after,omitempty"`
|
||||
DeadlineBefore string `json:"deadline_before,omitempty"`
|
||||
|
||||
// ask 参数
|
||||
Question string `json:"question,omitempty"`
|
||||
}
|
||||
|
||||
// quickTaskActionResult 是 quick_task 执行动作后的统一回包。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. AssistantText 是本轮要补发给用户的短正文;
|
||||
// 2. BusinessCard 仅在“业务真实成功”时携带,失败/追问场景必须为空;
|
||||
// 3. 不负责直接发射,发射时机由 RunQuickTaskNode 统一控制。
|
||||
type quickTaskActionResult struct {
|
||||
AssistantText string
|
||||
BusinessCard *newagentstream.StreamBusinessCardExtra
|
||||
}
|
||||
|
||||
// RunQuickTaskNode 执行快捷任务节点:流式 LLM 提取意图 → 直接调 service → 追加结果。
|
||||
func RunQuickTaskNode(ctx context.Context, input QuickTaskNodeInput) error {
|
||||
flowState := input.RuntimeState.EnsureCommonState()
|
||||
@@ -158,7 +173,8 @@ func RunQuickTaskNode(ctx context.Context, input QuickTaskNodeInput) error {
|
||||
if decision == nil {
|
||||
finalText := fullText.String()
|
||||
if strings.TrimSpace(finalText) == "" {
|
||||
_ = emitter.EmitAssistantText(quickTaskBlockID, quickTaskStageName, "抱歉,处理任务时出了点问题,请重试。", true)
|
||||
finalText = "抱歉,处理任务时出了点问题,请重试。"
|
||||
_ = emitter.EmitAssistantText(quickTaskBlockID, quickTaskStageName, finalText, true)
|
||||
}
|
||||
msg := schema.AssistantMessage(finalText, nil)
|
||||
input.ConversationContext.AppendHistory(msg)
|
||||
@@ -170,32 +186,52 @@ func RunQuickTaskNode(ctx context.Context, input QuickTaskNodeInput) error {
|
||||
log.Printf("[DEBUG] quick_task: chat=%s action=%s raw_title=%s", flowState.ConversationID, decision.Action, decision.Title)
|
||||
|
||||
// 5. 根据意图执行操作。
|
||||
var resultText string
|
||||
result := quickTaskActionResult{}
|
||||
switch decision.Action {
|
||||
case "create":
|
||||
resultText = handleQuickTaskCreate(ctx, input, decision, flowState)
|
||||
result = handleQuickTaskCreate(ctx, input, decision, flowState)
|
||||
case "query":
|
||||
resultText = handleQuickTaskQuery(ctx, input, decision, flowState)
|
||||
result = handleQuickTaskQuery(ctx, input, decision, flowState)
|
||||
case "ask":
|
||||
resultText = decision.Question
|
||||
if resultText == "" {
|
||||
resultText = "你想记录什么呢?告诉我具体内容吧。"
|
||||
result.AssistantText = decision.Question
|
||||
if result.AssistantText == "" {
|
||||
result.AssistantText = "你想记录什么呢?告诉我具体内容吧。"
|
||||
}
|
||||
default:
|
||||
resultText = "抱歉,我没有理解你的意思。你可以试试说「记一下明天开会」或「看看我的任务」。"
|
||||
result.AssistantText = "抱歉,我没有理解你的意思。你可以试试说「记一下明天开会」或「看看我的任务」。"
|
||||
}
|
||||
|
||||
// 6. 追加操作结果文本。
|
||||
if resultText != "" {
|
||||
_ = emitter.EmitAssistantText(quickTaskBlockID, quickTaskStageName, resultText, false)
|
||||
fullText.WriteString(resultText)
|
||||
// 6. 追加操作结果正文。
|
||||
if result.AssistantText != "" {
|
||||
_ = emitter.EmitAssistantText(quickTaskBlockID, quickTaskStageName, result.AssistantText, false)
|
||||
fullText.WriteString(result.AssistantText)
|
||||
}
|
||||
|
||||
// 7. 写入对话历史。
|
||||
finalText := fullText.String()
|
||||
msg := schema.AssistantMessage(finalText, nil)
|
||||
input.ConversationContext.AppendHistory(msg)
|
||||
persistVisibleAssistantMessage(ctx, input.PersistVisibleMessage, flowState, msg)
|
||||
messagePersisted := false
|
||||
// 7.1 有业务卡片时,先落正文,再发卡片,保证 timeline 顺序与前端展示一致。
|
||||
// 1. 先持久化正文,确保 timeline 里的 assistant_text seq 一定早于 business_card;
|
||||
// 2. 再发 business_card,保证“短正文 + 紧跟卡片”的时序契约;
|
||||
// 3. 卡片发射失败只记日志,不回滚正文,避免用户侧出现“看不到结果文本”的回退。
|
||||
if result.BusinessCard != nil {
|
||||
finalText := fullText.String()
|
||||
if strings.TrimSpace(finalText) != "" {
|
||||
msg := schema.AssistantMessage(finalText, nil)
|
||||
input.ConversationContext.AppendHistory(msg)
|
||||
persistVisibleAssistantMessage(ctx, input.PersistVisibleMessage, flowState, msg)
|
||||
messagePersisted = true
|
||||
}
|
||||
if emitErr := emitter.EmitBusinessCard(quickTaskResultCardID, quickTaskStageName, result.BusinessCard); emitErr != nil {
|
||||
log.Printf("[WARN] quick_task emit business_card error chat=%s err=%v", flowState.ConversationID, emitErr)
|
||||
}
|
||||
}
|
||||
|
||||
// 7.2 非卡片路径沿用原有收口:本轮正文统一一次性写入 history。
|
||||
if !messagePersisted {
|
||||
finalText := fullText.String()
|
||||
msg := schema.AssistantMessage(finalText, nil)
|
||||
input.ConversationContext.AppendHistory(msg)
|
||||
persistVisibleAssistantMessage(ctx, input.PersistVisibleMessage, flowState, msg)
|
||||
}
|
||||
|
||||
flowState.Phase = newagentmodel.PhaseDone
|
||||
return nil
|
||||
@@ -207,17 +243,18 @@ func handleQuickTaskCreate(
|
||||
input QuickTaskNodeInput,
|
||||
decision *quickTaskDecision,
|
||||
flowState *newagentmodel.CommonState,
|
||||
) string {
|
||||
) quickTaskActionResult {
|
||||
_ = ctx
|
||||
title := strings.TrimSpace(decision.Title)
|
||||
if title == "" {
|
||||
return "你想记录什么呢?告诉我具体内容吧。"
|
||||
return quickTaskActionResult{AssistantText: "你想记录什么呢?告诉我具体内容吧。"}
|
||||
}
|
||||
|
||||
var deadline *time.Time
|
||||
if raw := strings.TrimSpace(decision.DeadlineAt); raw != "" {
|
||||
parsed, err := newagentshared.ParseOptionalDeadline(raw)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("截止时间格式不太对(%s),不过我先把任务记下来啦。", err)
|
||||
return quickTaskActionResult{AssistantText: fmt.Sprintf("截止时间格式不太对(%s),不过我先把任务记下来啦。", err)}
|
||||
}
|
||||
deadline = parsed
|
||||
}
|
||||
@@ -245,23 +282,16 @@ func handleQuickTaskCreate(
|
||||
|
||||
log.Printf("[DEBUG] quick_task: CreateTask 参数 chat=%s title=%s priorityGroup=%d deadline=%v urgencyThreshold=%v urgency_raw=%q",
|
||||
flowState.ConversationID, title, priorityGroup, deadline, urgencyThreshold, decision.UrgencyThresholdAt)
|
||||
_, err := input.QuickTaskDeps.CreateTask(flowState.UserID, title, priorityGroup, deadline, urgencyThreshold)
|
||||
taskID, err := input.QuickTaskDeps.CreateTask(flowState.UserID, title, priorityGroup, deadline, urgencyThreshold)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("记录失败了(%s),稍后再试试?", err)
|
||||
return quickTaskActionResult{AssistantText: fmt.Sprintf("记录失败了(%s),稍后再试试?", err)}
|
||||
}
|
||||
|
||||
flowState.UsedQuickNote = true
|
||||
|
||||
priorityLabel := newagentshared.PriorityLabelCN(priorityGroup)
|
||||
deadlineStr := ""
|
||||
if deadline != nil {
|
||||
deadlineStr = deadline.In(newagentshared.ShanghaiLocation()).Format("2006-01-02 15:04")
|
||||
return quickTaskActionResult{
|
||||
AssistantText: "已帮你记下这条任务。",
|
||||
BusinessCard: buildTaskRecordBusinessCard(taskID, title, priorityGroup, deadline, urgencyThreshold),
|
||||
}
|
||||
|
||||
if deadlineStr != "" {
|
||||
return fmt.Sprintf("已记录:%s(%s,截止 %s)", title, priorityLabel, deadlineStr)
|
||||
}
|
||||
return fmt.Sprintf("已记录:%s(%s)", title, priorityLabel)
|
||||
}
|
||||
|
||||
// handleQuickTaskQuery 处理任务查询。
|
||||
@@ -270,7 +300,7 @@ func handleQuickTaskQuery(
|
||||
input QuickTaskNodeInput,
|
||||
decision *quickTaskDecision,
|
||||
flowState *newagentmodel.CommonState,
|
||||
) string {
|
||||
) quickTaskActionResult {
|
||||
params := newagentmodel.TaskQueryParams{
|
||||
SortBy: "deadline",
|
||||
Order: "asc",
|
||||
@@ -287,32 +317,254 @@ func handleQuickTaskQuery(
|
||||
if decision.Limit != nil && *decision.Limit > 0 && *decision.Limit <= 20 {
|
||||
params.Limit = *decision.Limit
|
||||
}
|
||||
params.DeadlineAfter = parseQuickTaskQueryDeadlineBoundary(decision.DeadlineAfter, "deadline_after", flowState)
|
||||
params.DeadlineBefore = parseQuickTaskQueryDeadlineBoundary(decision.DeadlineBefore, "deadline_before", flowState)
|
||||
// 1. 若模型给出了颠倒的时间窗(before<=after),当前轮降级为“不加时间窗”继续查询;
|
||||
// 2. 这样能避免误筛选成空结果,同时把异常留给日志排查;
|
||||
// 3. 这里只做兜底,不尝试替模型自动纠正语义,避免引入额外猜测。
|
||||
if params.DeadlineAfter != nil && params.DeadlineBefore != nil && !params.DeadlineBefore.After(*params.DeadlineAfter) {
|
||||
log.Printf("[WARN] quick_task: query 时间窗无效 chat=%s after=%s before=%s,已降级为无时间窗筛选",
|
||||
flowState.ConversationID,
|
||||
formatQuickTaskTime(params.DeadlineAfter),
|
||||
formatQuickTaskTime(params.DeadlineBefore),
|
||||
)
|
||||
params.DeadlineAfter = nil
|
||||
params.DeadlineBefore = nil
|
||||
}
|
||||
|
||||
results, err := input.QuickTaskDeps.QueryTasks(ctx, flowState.UserID, params)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("查询失败了(%s),稍后再试试?", err)
|
||||
return quickTaskActionResult{AssistantText: fmt.Sprintf("查询失败了(%s),稍后再试试?", err)}
|
||||
}
|
||||
|
||||
card := buildTaskQueryBusinessCard(params, results)
|
||||
if len(results) == 0 {
|
||||
return "当前没有匹配的任务。"
|
||||
return quickTaskActionResult{
|
||||
AssistantText: "我这边没查到匹配任务。",
|
||||
BusinessCard: card,
|
||||
}
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("找到 %d 条任务:\n", len(results)))
|
||||
for _, t := range results {
|
||||
label := newagentshared.PriorityLabelCN(t.PriorityGroup)
|
||||
line := fmt.Sprintf("- %s(%s", t.Title, label)
|
||||
if t.DeadlineAt != "" {
|
||||
line += fmt.Sprintf(",截止 %s", t.DeadlineAt)
|
||||
}
|
||||
line += ")"
|
||||
if t.IsCompleted {
|
||||
line += " ✅"
|
||||
}
|
||||
sb.WriteString(line + "\n")
|
||||
return quickTaskActionResult{
|
||||
AssistantText: fmt.Sprintf("我找到 %d 条任务,整理成卡片给你。", len(results)),
|
||||
BusinessCard: card,
|
||||
}
|
||||
}
|
||||
|
||||
func buildTaskRecordBusinessCard(taskID int, title string, priorityGroup int, deadline *time.Time, urgencyThreshold *time.Time) *newagentstream.StreamBusinessCardExtra {
|
||||
data := map[string]any{
|
||||
"id": taskID,
|
||||
"title": strings.TrimSpace(title),
|
||||
"priority_group": priorityGroup,
|
||||
"priority_label": newagentshared.PriorityLabelCN(priorityGroup),
|
||||
"status": "todo",
|
||||
}
|
||||
if formatted := formatQuickTaskTime(deadline); formatted != "" {
|
||||
data["deadline_at"] = formatted
|
||||
}
|
||||
if formatted := formatQuickTaskTime(urgencyThreshold); formatted != "" {
|
||||
data["urgency_threshold_at"] = formatted
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
// 说明:
|
||||
// 1. quick_task 当前只有 action=create,未显式区分“随口记 / 正式创建任务”;
|
||||
// 2. 仅凭当前 prompt 决策无法稳定判断 source=create_task,会引入误判;
|
||||
// 3. 本轮按最小安全口径固定为 quick_note,等后续补稳定判别字段再切分。
|
||||
return &newagentstream.StreamBusinessCardExtra{
|
||||
CardType: "task_record",
|
||||
Title: "已帮你记下",
|
||||
Summary: "一条轻量提醒已写入任务系统",
|
||||
Source: taskRecordSourceQuickNote,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func buildTaskQueryBusinessCard(params newagentmodel.TaskQueryParams, results []newagentmodel.TaskQueryResult) *newagentstream.StreamBusinessCardExtra {
|
||||
taskItems := make([]map[string]any, 0, len(results))
|
||||
for _, task := range results {
|
||||
item := map[string]any{
|
||||
"id": task.ID,
|
||||
"title": strings.TrimSpace(task.Title),
|
||||
"priority_group": task.PriorityGroup,
|
||||
"priority_label": newagentshared.PriorityLabelCN(task.PriorityGroup),
|
||||
"is_completed": task.IsCompleted,
|
||||
}
|
||||
if deadline := strings.TrimSpace(task.DeadlineAt); deadline != "" {
|
||||
item["deadline_at"] = deadline
|
||||
}
|
||||
taskItems = append(taskItems, item)
|
||||
}
|
||||
|
||||
title := fmt.Sprintf("找到 %d 条任务", len(results))
|
||||
if len(results) == 0 {
|
||||
title = "未找到匹配任务"
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
"result_count": len(results),
|
||||
"shown_count": len(results),
|
||||
"tasks": taskItems,
|
||||
}
|
||||
queryFilters := buildTaskQueryFilters(params)
|
||||
if len(queryFilters) > 0 {
|
||||
data["query_filters"] = queryFilters
|
||||
}
|
||||
querySummary := buildTaskQuerySummary(queryFilters)
|
||||
if querySummary != "" {
|
||||
data["query_summary"] = querySummary
|
||||
}
|
||||
|
||||
return &newagentstream.StreamBusinessCardExtra{
|
||||
CardType: "task_query",
|
||||
Title: title,
|
||||
Summary: querySummary,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
// buildTaskQueryFilter 生成查询条件的稳定结构化描述。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. key/operator/value 提供前端可依赖的机器语义;
|
||||
// 2. label/display_text 提供前端可直接展示的中文文案;
|
||||
// 3. query_summary 只能从 display_text 派生,前端不要再反向解析 summary。
|
||||
func buildTaskQueryFilter(key string, label string, value any, operator string, displayText string) map[string]any {
|
||||
filter := map[string]any{
|
||||
"key": key,
|
||||
"label": label,
|
||||
"value": value,
|
||||
"display_text": strings.TrimSpace(displayText),
|
||||
}
|
||||
if strings.TrimSpace(operator) != "" {
|
||||
filter["operator"] = strings.TrimSpace(operator)
|
||||
}
|
||||
return filter
|
||||
}
|
||||
|
||||
func buildTaskQueryFilters(params newagentmodel.TaskQueryParams) []map[string]any {
|
||||
filters := make([]map[string]any, 0, 6)
|
||||
if params.Quadrant != nil && *params.Quadrant >= 1 && *params.Quadrant <= 4 {
|
||||
label := newagentshared.PriorityLabelCN(*params.Quadrant)
|
||||
filters = append(filters, buildTaskQueryFilter(
|
||||
"quadrant",
|
||||
"象限",
|
||||
*params.Quadrant,
|
||||
"eq",
|
||||
fmt.Sprintf("象限:%s", label),
|
||||
))
|
||||
}
|
||||
if kw := strings.TrimSpace(params.Keyword); kw != "" {
|
||||
filters = append(filters, buildTaskQueryFilter(
|
||||
"keyword",
|
||||
"关键词",
|
||||
kw,
|
||||
"contains",
|
||||
fmt.Sprintf("关键词:%s", kw),
|
||||
))
|
||||
}
|
||||
if params.DeadlineAfter != nil {
|
||||
formatted := formatQuickTaskTime(params.DeadlineAfter)
|
||||
filters = append(filters, buildTaskQueryFilter(
|
||||
"deadline_after",
|
||||
"截止起始",
|
||||
formatted,
|
||||
"gte",
|
||||
fmt.Sprintf("截止时间≥%s", formatted),
|
||||
))
|
||||
}
|
||||
if params.DeadlineBefore != nil {
|
||||
formatted := formatQuickTaskTime(params.DeadlineBefore)
|
||||
filters = append(filters, buildTaskQueryFilter(
|
||||
"deadline_before",
|
||||
"截止结束",
|
||||
formatted,
|
||||
"lt",
|
||||
fmt.Sprintf("截止时间<%s", formatted),
|
||||
))
|
||||
}
|
||||
if !params.IncludeCompleted {
|
||||
filters = append(filters, buildTaskQueryFilter(
|
||||
"include_completed",
|
||||
"完成状态",
|
||||
false,
|
||||
"eq",
|
||||
"仅未完成",
|
||||
))
|
||||
}
|
||||
|
||||
sortValue := "deadline_asc"
|
||||
sortDisplay := "按截止时间升序"
|
||||
switch strings.ToLower(strings.TrimSpace(params.SortBy)) {
|
||||
case "priority":
|
||||
if strings.ToLower(strings.TrimSpace(params.Order)) == "desc" {
|
||||
sortValue = "priority_desc"
|
||||
sortDisplay = "按优先级降序"
|
||||
} else {
|
||||
sortValue = "priority_asc"
|
||||
sortDisplay = "按优先级升序"
|
||||
}
|
||||
case "id":
|
||||
if strings.ToLower(strings.TrimSpace(params.Order)) == "desc" {
|
||||
sortValue = "id_desc"
|
||||
sortDisplay = "按创建顺序倒序"
|
||||
} else {
|
||||
sortValue = "id_asc"
|
||||
sortDisplay = "按创建顺序正序"
|
||||
}
|
||||
default:
|
||||
if strings.ToLower(strings.TrimSpace(params.Order)) == "desc" {
|
||||
sortValue = "deadline_desc"
|
||||
sortDisplay = "按截止时间降序"
|
||||
}
|
||||
}
|
||||
filters = append(filters, buildTaskQueryFilter(
|
||||
"sort",
|
||||
"排序",
|
||||
sortValue,
|
||||
"eq",
|
||||
sortDisplay,
|
||||
))
|
||||
return filters
|
||||
}
|
||||
|
||||
func buildTaskQuerySummary(filters []map[string]any) string {
|
||||
parts := make([]string, 0, len(filters))
|
||||
for _, filter := range filters {
|
||||
if text, ok := filter["display_text"].(string); ok && strings.TrimSpace(text) != "" {
|
||||
parts = append(parts, strings.TrimSpace(text))
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, ";")
|
||||
}
|
||||
|
||||
// parseQuickTaskQueryDeadlineBoundary 解析 quick_task 查询时间窗边界。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只负责把 query 的 deadline_after/deadline_before 文本解析成时间;
|
||||
// 2. 解析失败时仅记录日志并返回 nil,不中断查询主链路;
|
||||
// 3. 不负责时间窗合法性校验(如 before<=after),该校验由调用方统一处理。
|
||||
func parseQuickTaskQueryDeadlineBoundary(raw string, field string, flowState *newagentmodel.CommonState) *time.Time {
|
||||
value := strings.TrimSpace(raw)
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
parsed, err := newagentshared.ParseOptionalDeadline(value)
|
||||
if err != nil {
|
||||
chatID := ""
|
||||
if flowState != nil {
|
||||
chatID = flowState.ConversationID
|
||||
}
|
||||
log.Printf("[WARN] quick_task: query %s 解析失败 chat=%s raw=%q err=%v,已降级为无该筛选条件", field, chatID, value, err)
|
||||
return nil
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
func formatQuickTaskTime(t *time.Time) string {
|
||||
if t == nil {
|
||||
return ""
|
||||
}
|
||||
return t.In(newagentshared.ShanghaiLocation()).Format("2006-01-02 15:04")
|
||||
}
|
||||
|
||||
// quickNoteFallbackPriority 根据截止时间推断默认优先级。
|
||||
|
||||
@@ -14,20 +14,21 @@ const chatRoutingSystemPrompt = `
|
||||
|
||||
路由规则:
|
||||
- direct_reply:纯闲聊、简单问答、轻量生活建议、打招呼、感谢等不需要工具、也不需要长链路思考的请求。控制码后直接输出完整回复。
|
||||
- quick_task:用户明确想记录/添加/修改/删除一个待办或提醒(如"记一下""提醒我""帮我记"),或查看/筛选任务列表(如"我有什么任务""待办清单""最近急事")。该路由走轻量快捷路径,延迟低、废话少。控制码后不要输出任何内容。
|
||||
- execute:需要用工具处理的日程类请求(查询日程、移动课程、排课等),但不需要先制定计划。控制码后输出简短确认。
|
||||
- quick_task:用户明确想记录/添加一个待办或提醒(如"记一下""提醒我""帮我记"),或查看/筛选任务列表(如"我有什么任务""待办清单""最近急事""今天/明天/本周有什么事要做")。该路由走轻量快捷路径,延迟低、废话少。控制码后不要输出任何内容。
|
||||
- execute:需要用工具处理的日程编排请求(明确查课表/日程块、移动课程、排课等),但不需要先制定计划。控制码后输出简短确认。
|
||||
- deep_answer:复杂问题但不需要工具(如分析建议、知识解释、方案比较、深度讨论等),需要深度思考后回答。控制码后不要输出任何占位过渡语,后端会直接进入第二次正式回答。
|
||||
- plan:用户明确要求先制定计划,或涉及多阶段复杂规划。控制码后输出简短确认。
|
||||
|
||||
quick_task 判别要点:
|
||||
- 用户明确要"记/添加/提醒"一个待办 → quick_task
|
||||
- 用户要查看/筛选/列出任务清单 → quick_task
|
||||
- 用户要修改/删除某个任务 → quick_task
|
||||
- 用户问"今天/明天/本周有什么待办/任务/事情要做"这类时间窗任务查询 → quick_task
|
||||
- 用户明确在查课表/日程块、排课、移动安排 → execute
|
||||
- 但如果用户同时提了日程排布(如"把明天的课调一下,再记一下周五开会"),混合操作走 execute
|
||||
- 如果信息不足(如"帮我记一下"但没说记什么),走 direct_reply 追问
|
||||
|
||||
任务类设计路由要点:
|
||||
- 普通"创建/修改任务类"默认走 execute(由 execute 负责补字段与写入)。
|
||||
- 普通"创建/修改任务类配置(task class)"默认走 execute(由 execute 负责补字段与写入)。
|
||||
- 仅当用户明确要"补课程学习资料/学习建议/学习路径(需要外部知识)"时,走 plan(后续可使用 web_search)。
|
||||
- 考试时间、DDL、课程具体时间安排、个人可用时段等时间信息,必须向用户本人确认,不能作为 web 搜索补齐目标。
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ const quickTaskSystemPrompt = `
|
||||
JSON 字段说明:
|
||||
- action:只能是 create / query / ask
|
||||
- create 时:title 必填,deadline_at 必填,priority_group 必填,范围 1-4;urgency_threshold_at 满足条件时填写,条件在下面
|
||||
- query 时:quadrant 可选 1-4,keyword 可选,limit 可选
|
||||
- query 时:quadrant 可选 1-4,keyword 可选,limit 可选,deadline_after/deadline_before 可选(用于截止时间窗口筛选)
|
||||
- ask 时:question 必填
|
||||
|
||||
规则:
|
||||
@@ -33,6 +33,7 @@ JSON 字段说明:
|
||||
4. 未提供的可选字段直接省略,不要填 null 或空字符串
|
||||
5. JSON 中不要包含 speak 字段,给用户看的话放在 </SMARTFLOW_DECISION> 标签之后
|
||||
6. 紧急分界时间,即任务从"重要不紧急"自动轮换到"重要且紧急"的时间点;格式同 deadline_at ——当 priority_group=2 时必填,你必须根据 deadline 自动推算一个合理的紧急分界时间(通常为 deadline 前 24-48 小时),不要等用户提供;priority_group 为 1、3、4 或无截止时间时不要输出此字段
|
||||
7. query 里出现相对日期窗口(如"明天有什么事要做")时,优先输出明确边界:deadline_after="明天 00:00",deadline_before="后天 00:00",按 [after, before) 语义筛选
|
||||
|
||||
示例:
|
||||
|
||||
@@ -44,6 +45,9 @@ JSON 字段说明:
|
||||
<SMARTFLOW_DECISION>{"action":"query","limit":5}</SMARTFLOW_DECISION>
|
||||
我帮你查一下当前的任务。
|
||||
|
||||
<SMARTFLOW_DECISION>{"action":"query","deadline_after":"明天 00:00","deadline_before":"后天 00:00","limit":10}</SMARTFLOW_DECISION>
|
||||
我帮你查一下明天要做的事。
|
||||
|
||||
<SMARTFLOW_DECISION>{"action":"ask","question":"你想记录什么呢?告诉我具体内容吧。"}</SMARTFLOW_DECISION>
|
||||
你想记录什么呢?告诉我具体内容吧。`
|
||||
|
||||
|
||||
@@ -358,6 +358,18 @@ func (e *ChunkEmitter) EmitScheduleCompleted(blockID, stage string) error {
|
||||
return e.emitExtraOnly(NewScheduleCompletedExtra(blockID, stage))
|
||||
}
|
||||
|
||||
// EmitBusinessCard 输出一次业务结果卡片事件。
|
||||
//
|
||||
// 协议约束:
|
||||
// 1. 只走 extra,不附带 content/reasoning;
|
||||
// 2. card 为空时直接跳过,避免发出缺少关键字段的空卡片。
|
||||
func (e *ChunkEmitter) EmitBusinessCard(blockID, stage string, card *StreamBusinessCardExtra) error {
|
||||
if e == nil || e.emit == nil || card == nil {
|
||||
return nil
|
||||
}
|
||||
return e.emitExtraOnly(NewBusinessCardExtra(blockID, stage, card))
|
||||
}
|
||||
|
||||
// EmitFinish 统一输出 stop 结束块,并带上 finish extra。
|
||||
func (e *ChunkEmitter) EmitFinish(blockID, stage string) error {
|
||||
if e == nil || e.emit == nil {
|
||||
|
||||
@@ -46,6 +46,7 @@ const (
|
||||
StreamExtraKindToolResult StreamExtraKind = "tool_result"
|
||||
StreamExtraKindConfirm StreamExtraKind = "confirm_request"
|
||||
StreamExtraKindInterrupt StreamExtraKind = "interrupt"
|
||||
StreamExtraKindBusinessCard StreamExtraKind = "business_card"
|
||||
StreamExtraKindFinish StreamExtraKind = "finish"
|
||||
StreamExtraKindScheduleCompleted StreamExtraKind = "schedule_completed"
|
||||
)
|
||||
@@ -63,18 +64,19 @@ const (
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. Kind / Stage / BlockID 提供前端排版和分组所需的最小元信息;
|
||||
// 2. Status / Tool / Confirm / Interrupt 只存展示层真正需要的摘要,不直接耦合后端完整状态对象;
|
||||
// 2. Status / Tool / Confirm / Interrupt / BusinessCard 只存展示层真正需要的摘要,不直接耦合后端完整状态对象;
|
||||
// 3. Meta 留给后续做灰度扩展,避免每加一种小字段都要立刻改 DTO 结构。
|
||||
type OpenAIChunkExtra struct {
|
||||
Kind StreamExtraKind `json:"kind,omitempty"`
|
||||
BlockID string `json:"block_id,omitempty"`
|
||||
Stage string `json:"stage,omitempty"`
|
||||
DisplayMode StreamDisplayMode `json:"display_mode,omitempty"`
|
||||
Status *StreamStatusExtra `json:"status,omitempty"`
|
||||
Tool *StreamToolExtra `json:"tool,omitempty"`
|
||||
Confirm *StreamConfirmExtra `json:"confirm,omitempty"`
|
||||
Interrupt *StreamInterruptExtra `json:"interrupt,omitempty"`
|
||||
Meta map[string]any `json:"meta,omitempty"`
|
||||
Kind StreamExtraKind `json:"kind,omitempty"`
|
||||
BlockID string `json:"block_id,omitempty"`
|
||||
Stage string `json:"stage,omitempty"`
|
||||
DisplayMode StreamDisplayMode `json:"display_mode,omitempty"`
|
||||
Status *StreamStatusExtra `json:"status,omitempty"`
|
||||
Tool *StreamToolExtra `json:"tool,omitempty"`
|
||||
Confirm *StreamConfirmExtra `json:"confirm,omitempty"`
|
||||
Interrupt *StreamInterruptExtra `json:"interrupt,omitempty"`
|
||||
BusinessCard *StreamBusinessCardExtra `json:"business_card,omitempty"`
|
||||
Meta map[string]any `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
// StreamStatusExtra 表示普通阶段状态或提示性事件。
|
||||
@@ -105,6 +107,20 @@ type StreamInterruptExtra struct {
|
||||
Summary string `json:"summary,omitempty"`
|
||||
}
|
||||
|
||||
// StreamBusinessCardExtra 表示一张业务结果卡片。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. CardType 只允许前端已约定的卡片类型(task_query/task_record);
|
||||
// 2. Source 仅在 task_record 时有语义,其他卡片类型可为空;
|
||||
// 3. Data 承载“可直接渲染的最小快照”,避免前端再二次补拉才能看到结果。
|
||||
type StreamBusinessCardExtra struct {
|
||||
CardType string `json:"card_type,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Data map[string]any `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// ToOpenAIStream 把 Eino message 转成 OpenAI 兼容 chunk。
|
||||
func ToOpenAIStream(chunk *schema.Message, requestID, modelName string, created int64, includeRole bool) (string, error) {
|
||||
return ToOpenAIStreamWithExtra(chunk, requestID, modelName, created, includeRole, nil)
|
||||
@@ -263,6 +279,17 @@ func NewInterruptExtra(blockID, stage, interactionID, interactionType, summary s
|
||||
}
|
||||
}
|
||||
|
||||
// NewBusinessCardExtra 创建“业务结果卡片”事件的 extra。
|
||||
func NewBusinessCardExtra(blockID, stage string, businessCard *StreamBusinessCardExtra) *OpenAIChunkExtra {
|
||||
return &OpenAIChunkExtra{
|
||||
Kind: StreamExtraKindBusinessCard,
|
||||
BlockID: blockID,
|
||||
Stage: stage,
|
||||
DisplayMode: StreamDisplayModeCard,
|
||||
BusinessCard: businessCard,
|
||||
}
|
||||
}
|
||||
|
||||
// NewScheduleCompletedExtra 创建”排程完毕”卡片事件的 extra。
|
||||
//
|
||||
// 职责边界:
|
||||
@@ -331,5 +358,6 @@ func hasStreamExtra(extra *OpenAIChunkExtra) bool {
|
||||
extra.Tool != nil ||
|
||||
extra.Confirm != nil ||
|
||||
extra.Interrupt != nil ||
|
||||
extra.BusinessCard != nil ||
|
||||
len(extra.Meta) > 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user