后端:
1.Execute 上下文修复(无限循环 / 重复确认根治)
- 更新node/execute.go:speak 写入历史(修复旧 TODO);confirm 动作 speak 不再丢失;
continue 无工具调用时写 reason 保证上下文推进;区分 tool_call 数组/JSON损坏两种
correction hint;goal_check hint 区分 plan/ReAct 模式
- 更新node/execute.go:新增 AlwaysExecute 字段,extra.always_execute=true 时写工具
跳过确认闸门直接执行并持久化
- 更新model/graph_run_state.go:AgentGraphRequest 新增 AlwaysExecute;新增
WriteSchedulePreviewFunc 类型和 WriteSchedulePreview Dep
- 更新service/agentsvc/agent.go:新增 readAgentExtraBool 辅助
2.粗排全链路修复
- 更新service/agentsvc/agent_newagent.go:makeRoughBuildFunc 改用 HybridScheduleEntry
而非 TaskClassItem.EmbeddedTime,普通时段放置不再被丢弃
- 更新conv/schedule_provider.go:LoadScheduleState 从 task class 日期范围推算多周
规划窗口,不再硬编码当前周 7 天;DayMapping 覆盖全部相关周,粗排跨周结果不再
被 WeekDayToDay 静默丢弃
- 更新node/rough_build.go:pinned block 区分有/无未覆盖 pending 任务两种情况,
有 pending 时明确操作顺序(find_free→place)和完成判定,防止 LLM 重复调
list_tasks;新增 countPendingTasks 辅助(只统计 Slots 为空的真正未覆盖任务)
- 更新model/common_state.go:新增 StartDirectExecute(),Chat 直接路由 execute 时
清空旧 PlanSteps,修复跨会话 HasPlan() 误判导致 ReAct 走 plan 模式的 bug
- 更新node/chat.go:handleRouteExecute 改用 StartDirectExecute()
3.排程预览缓存迁移至 Deliver 节点
- 更新node/agent_nodes.go:Deliver 节点完成后调用 WriteSchedulePreview,只有任务
真正完成才写预览缓存,中断路径不写中间态
- 更新service/agentsvc/agent_newagent.go:注入 makeWriteSchedulePreviewFunc;移除
graph 结束后的内联写入;makeRoughBuildFunc 注释修正
- 更新conv/schedule_preview.go:ScheduleStateToPreview 补设 GeneratedAt
- 更新model/agent.go:GetSchedulePlanPreviewResponse 新增 HybridEntries 字段
- 更新service/agentsvc/agent_schedule_preview.go:GET handler Redis/MySQL 两条路径
均透传 HybridEntries
4.Execute thinking 模式修复
- 更新newAgent/llm/ark_adapter.go:thinking 开启时强制 temperature=1,MaxTokens 自
动托底至 16000,调用方与适配层行为对齐
- 更新node/execute.go:调用参数同步改为 temperature=1.0 / MaxTokens=16000
undo:
1.流式推送换行未修复(undo)
2.上下文依然待审视
前端:无
仓库:无
228 lines
9.2 KiB
Go
228 lines
9.2 KiB
Go
package agentsvc
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"log"
|
||
"strings"
|
||
"time"
|
||
|
||
agentmodel "github.com/LoveLosita/smartflow/backend/agent/model"
|
||
agentshared "github.com/LoveLosita/smartflow/backend/agent/shared"
|
||
"github.com/LoveLosita/smartflow/backend/model"
|
||
"github.com/LoveLosita/smartflow/backend/respond"
|
||
)
|
||
|
||
// saveSchedulePlanPreview 负责把排程结果同步写入“查询预览”所需的缓存与快照。
|
||
//
|
||
// 职责边界:
|
||
// 1. 负责把 graph 最终状态映射为统一预览 DTO,并先写 Redis、再写 MySQL 快照。
|
||
// 2. 负责执行“失败不阻断主回复”的旁路持久化策略,避免影响聊天主链路。
|
||
// 3. 不负责 SSE 输出,不负责聊天消息落库,也不负责 refine 状态到 plan 状态的转换。
|
||
func (s *AgentService) saveSchedulePlanPreview(ctx context.Context, userID int, chatID string, finalState *agentmodel.SchedulePlanState) {
|
||
// 1. 先做最小前置校验,避免把空状态或空会话写成脏快照。
|
||
if s == nil || finalState == nil {
|
||
return
|
||
}
|
||
normalizedChatID := strings.TrimSpace(chatID)
|
||
if normalizedChatID == "" {
|
||
return
|
||
}
|
||
|
||
// 2. 组装统一预览缓存结构。
|
||
// 2.1 summary 为空时使用统一兜底文案,保证查询接口始终有稳定输出。
|
||
// 2.2 所有切片字段都做深拷贝,避免缓存与 graph state 共享底层数组。
|
||
preview := &model.SchedulePlanPreviewCache{
|
||
UserID: userID,
|
||
ConversationID: normalizedChatID,
|
||
TraceID: strings.TrimSpace(finalState.TraceID),
|
||
Summary: schedulePlanSummaryOrFallback(strings.TrimSpace(finalState.FinalSummary)),
|
||
CandidatePlans: cloneWeekSchedules(finalState.CandidatePlans),
|
||
TaskClassIDs: append([]int(nil), finalState.TaskClassIDs...),
|
||
HybridEntries: cloneHybridEntries(finalState.HybridEntries),
|
||
AllocatedItems: cloneTaskClassItems(finalState.AllocatedItems),
|
||
GeneratedAt: time.Now(),
|
||
}
|
||
|
||
// 3. 先写 Redis 预览,保证前端查询链路优先命中低时延缓存。
|
||
// 3.1 Redis 写失败只记日志,不中断主流程;
|
||
// 3.2 真正兜底由后续 MySQL 快照承担。
|
||
if s.cacheDAO != nil {
|
||
if err := s.cacheDAO.SetSchedulePlanPreviewToCache(ctx, userID, normalizedChatID, preview); err != nil {
|
||
log.Printf("写入排程预览缓存失败 chat_id=%s: %v", normalizedChatID, err)
|
||
}
|
||
}
|
||
|
||
// 4. 再写 MySQL 快照,保证缓存失效后仍能恢复预览与连续微调上下文。
|
||
// 4.1 这里继续采用“同步写快照”的策略,因为下一轮 refine 依赖强一致读取;
|
||
// 4.2 写库失败同样只记日志,避免让用户侧回复因为旁路持久化失败而中断。
|
||
if s.repo != nil {
|
||
snapshot := buildSchedulePlanSnapshotFromState(userID, normalizedChatID, finalState)
|
||
if err := s.repo.UpsertScheduleStateSnapshot(ctx, snapshot); err != nil {
|
||
log.Printf("写入排程状态快照失败 chat_id=%s: %v", normalizedChatID, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// GetSchedulePlanPreview 按 conversation_id 读取结构化排程预览。
|
||
//
|
||
// 职责边界:
|
||
// 1. 负责参数归一化、缓存优先读取、会话归属校验和 DB 兜底。
|
||
// 2. 负责把缓存/快照 DTO 转成接口响应 DTO。
|
||
// 3. 不负责触发排程,不负责补算结果,也不负责消息链路落库。
|
||
func (s *AgentService) GetSchedulePlanPreview(ctx context.Context, userID int, chatID string) (*model.GetSchedulePlanPreviewResponse, error) {
|
||
// 1. 先校验会话参数,避免无效请求打到缓存或数据库。
|
||
normalizedChatID := strings.TrimSpace(chatID)
|
||
if normalizedChatID == "" {
|
||
return nil, respond.MissingParam
|
||
}
|
||
if s == nil {
|
||
return nil, errors.New("agent service is not initialized")
|
||
}
|
||
|
||
// 2. 优先查 Redis。
|
||
// 2.1 命中后立即校验 user_id,避免把别人的会话预览泄露给当前用户;
|
||
// 2.2 缓存异常直接上抛,由接口层统一处理错误响应。
|
||
if s.cacheDAO != nil {
|
||
preview, err := s.cacheDAO.GetSchedulePlanPreviewFromCache(ctx, userID, normalizedChatID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if preview != nil {
|
||
if preview.UserID > 0 && preview.UserID != userID {
|
||
return nil, respond.SchedulePlanPreviewNotFound
|
||
}
|
||
plans := cloneWeekSchedules(preview.CandidatePlans)
|
||
if plans == nil {
|
||
plans = make([]model.UserWeekSchedule, 0)
|
||
}
|
||
return &model.GetSchedulePlanPreviewResponse{
|
||
ConversationID: normalizedChatID,
|
||
TraceID: strings.TrimSpace(preview.TraceID),
|
||
Summary: strings.TrimSpace(preview.Summary),
|
||
CandidatePlans: plans,
|
||
HybridEntries: cloneHybridEntries(preview.HybridEntries),
|
||
GeneratedAt: preview.GeneratedAt,
|
||
}, nil
|
||
}
|
||
}
|
||
|
||
// 3. Redis 未命中时回源 MySQL。
|
||
// 3.1 命中快照后顺手回填 Redis,提高后续命中率;
|
||
// 3.2 DB 未命中才真正返回 not found,避免缓存过期造成假阴性。
|
||
if s.repo != nil {
|
||
snapshot, err := s.repo.GetScheduleStateSnapshot(ctx, userID, normalizedChatID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if snapshot != nil {
|
||
response := snapshotToSchedulePlanPreviewResponse(snapshot)
|
||
if s.cacheDAO != nil {
|
||
cachePreview := snapshotToSchedulePlanPreviewCache(snapshot)
|
||
if setErr := s.cacheDAO.SetSchedulePlanPreviewToCache(ctx, userID, normalizedChatID, cachePreview); setErr != nil {
|
||
log.Printf("回填排程预览缓存失败 chat_id=%s: %v", normalizedChatID, setErr)
|
||
}
|
||
}
|
||
return response, nil
|
||
}
|
||
}
|
||
|
||
return nil, respond.SchedulePlanPreviewNotFound
|
||
}
|
||
|
||
// cloneWeekSchedules 负责深拷贝周视图排程,避免缓存与运行态共享底层切片。
|
||
func cloneWeekSchedules(src []model.UserWeekSchedule) []model.UserWeekSchedule {
|
||
return agentshared.CloneWeekSchedules(src)
|
||
}
|
||
|
||
// cloneHybridEntries 负责深拷贝混合排程条目,避免跨请求污染。
|
||
func cloneHybridEntries(src []model.HybridScheduleEntry) []model.HybridScheduleEntry {
|
||
return agentshared.CloneHybridEntries(src)
|
||
}
|
||
|
||
// cloneTaskClassItems 负责深拷贝任务项切片,包含内部指针字段的安全复制。
|
||
func cloneTaskClassItems(src []model.TaskClassItem) []model.TaskClassItem {
|
||
return agentshared.CloneTaskClassItems(src)
|
||
}
|
||
|
||
// buildSchedulePlanSnapshotFromState 把 graph 最终状态映射成可持久化的快照 DTO。
|
||
//
|
||
// 职责边界:
|
||
// 1. 负责字段归一化、深拷贝和 state_version 补齐。
|
||
// 2. 不负责数据库写入,也不负责生成业务摘要文案。
|
||
func buildSchedulePlanSnapshotFromState(userID int, conversationID string, st *agentmodel.SchedulePlanState) *model.SchedulePlanStateSnapshot {
|
||
if st == nil {
|
||
return nil
|
||
}
|
||
return &model.SchedulePlanStateSnapshot{
|
||
UserID: userID,
|
||
ConversationID: conversationID,
|
||
StateVersion: model.SchedulePlanStateVersionV1,
|
||
TaskClassIDs: append([]int(nil), st.TaskClassIDs...),
|
||
Constraints: append([]string(nil), st.Constraints...),
|
||
HybridEntries: cloneHybridEntries(st.HybridEntries),
|
||
AllocatedItems: cloneTaskClassItems(st.AllocatedItems),
|
||
CandidatePlans: cloneWeekSchedules(st.CandidatePlans),
|
||
UserIntent: strings.TrimSpace(st.UserIntent),
|
||
Strategy: strings.TrimSpace(st.Strategy),
|
||
AdjustmentScope: strings.TrimSpace(st.AdjustmentScope),
|
||
RestartRequested: st.RestartRequested,
|
||
FinalSummary: strings.TrimSpace(st.FinalSummary),
|
||
Completed: st.Completed,
|
||
TraceID: strings.TrimSpace(st.TraceID),
|
||
}
|
||
}
|
||
|
||
// snapshotToSchedulePlanPreviewCache 把 MySQL 快照映射成 Redis 预览缓存结构。
|
||
func snapshotToSchedulePlanPreviewCache(snapshot *model.SchedulePlanStateSnapshot) *model.SchedulePlanPreviewCache {
|
||
if snapshot == nil {
|
||
return nil
|
||
}
|
||
generatedAt := snapshot.UpdatedAt
|
||
if generatedAt.IsZero() {
|
||
generatedAt = time.Now()
|
||
}
|
||
return &model.SchedulePlanPreviewCache{
|
||
UserID: snapshot.UserID,
|
||
ConversationID: snapshot.ConversationID,
|
||
TraceID: strings.TrimSpace(snapshot.TraceID),
|
||
Summary: schedulePlanSummaryOrFallback(strings.TrimSpace(snapshot.FinalSummary)),
|
||
CandidatePlans: cloneWeekSchedules(snapshot.CandidatePlans),
|
||
TaskClassIDs: append([]int(nil), snapshot.TaskClassIDs...),
|
||
HybridEntries: cloneHybridEntries(snapshot.HybridEntries),
|
||
AllocatedItems: cloneTaskClassItems(snapshot.AllocatedItems),
|
||
GeneratedAt: generatedAt,
|
||
}
|
||
}
|
||
|
||
// snapshotToSchedulePlanPreviewResponse 把 MySQL 快照映射成查询接口响应结构。
|
||
func snapshotToSchedulePlanPreviewResponse(snapshot *model.SchedulePlanStateSnapshot) *model.GetSchedulePlanPreviewResponse {
|
||
if snapshot == nil {
|
||
return nil
|
||
}
|
||
plans := cloneWeekSchedules(snapshot.CandidatePlans)
|
||
if plans == nil {
|
||
plans = make([]model.UserWeekSchedule, 0)
|
||
}
|
||
generatedAt := snapshot.UpdatedAt
|
||
if generatedAt.IsZero() {
|
||
generatedAt = time.Now()
|
||
}
|
||
return &model.GetSchedulePlanPreviewResponse{
|
||
ConversationID: snapshot.ConversationID,
|
||
TraceID: strings.TrimSpace(snapshot.TraceID),
|
||
Summary: schedulePlanSummaryOrFallback(strings.TrimSpace(snapshot.FinalSummary)),
|
||
CandidatePlans: plans,
|
||
HybridEntries: cloneHybridEntries(snapshot.HybridEntries),
|
||
GeneratedAt: generatedAt,
|
||
}
|
||
}
|
||
|
||
// schedulePlanSummaryOrFallback 统一收口排程摘要兜底文案,避免各处重复维护默认值。
|
||
func schedulePlanSummaryOrFallback(summary string) string {
|
||
if strings.TrimSpace(summary) == "" {
|
||
return "排程流程已完成,但未生成结果摘要。"
|
||
}
|
||
return summary
|
||
}
|