feat(agent): ✨ 重构智能排程分流与双通道交付,补齐周级预算并接入连续微调复用 - 🔀 通用路由升级为 action 分流(chat/quick_note_create/task_query/schedule_plan),路由失败直接返回内部错误,不再回落聊天 - 🧭 智能排程链路重构:统一图编排与节点职责,完善日级/周级调优协作与提示词约束 - 📊 周级预算改为“有效周保底 + 负载加权分配”,避免有效周零预算并提升资源利用率 - ⚙️ 日级并发优化细化:按天拆分 DayGroup 并发执行,低收益天(suggested<=2)跳过,单天失败仅回退该天结果并继续全局 - 🧵 周级并发优化细化:按周并发 worker 执行,单周“单步动作”循环(每轮仅 1 个 Move/Swap 或 done),失败周保留原方案不影响其它周 - 🛰️ 新增排程预览双通道:聊天主链路输出终审文本,结构化 candidate_plans 通过 /api/v1/agent/schedule-preview 拉取 - 🗃️ 增补 Redis 预览缓存读写与清理逻辑,新增对应 API、路由、模型与错误码支持 - ♻️ 接入连续对话微调复用:命中同会话历史预览时复用上轮 HybridEntries,避免每轮重跑粗排 - 🛡️ 增加复用保护:仅当本轮与上轮 task_class_ids 集合一致才复用;不一致回退全量粗排 - 🧰 扩展预览缓存字段(task_class_ids/hybrid_entries/allocated_items),支撑微调承接链路 - 🗺️ 更新 README 5.4 Mermaid(总分流图 + 智能排程流转图)并补充决策文档 - ⚠️ 新增“连续微调复用”链路我尚未完成测试,且文档状态目前较为混乱,待连续对话微调功能真正测试完成后再统一更新
87 lines
2.7 KiB
Go
87 lines
2.7 KiB
Go
package scheduleplan
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
|
||
"github.com/LoveLosita/smartflow/backend/model"
|
||
)
|
||
|
||
// runMergeNode 负责“合并日内结果 + 冲突校验 + 回退快照”。
|
||
//
|
||
// 职责边界:
|
||
// 1. 负责把 DailyResults 合并回全量 HybridEntries;
|
||
// 2. 负责执行时间冲突检测;
|
||
// 3. 负责在冲突时回退原始数据;
|
||
// 4. 负责产出 MergeSnapshot,供 final_check 失败时回退。
|
||
func runMergeNode(
|
||
ctx context.Context,
|
||
st *SchedulePlanState,
|
||
emitStage func(stage, detail string),
|
||
) (*SchedulePlanState, error) {
|
||
_ = ctx
|
||
if st == nil || len(st.DailyResults) == 0 {
|
||
return st, nil
|
||
}
|
||
|
||
emitStage("schedule_plan.merge.start", "正在合并日内优化结果。")
|
||
|
||
// 1. 先保存 merge 前原始数据,作为冲突时的第一层回退兜底。
|
||
originalEntries := deepCopyEntries(st.HybridEntries)
|
||
|
||
// 2. 展平 daily results。
|
||
merged := make([]model.HybridScheduleEntry, 0)
|
||
for _, dayMap := range st.DailyResults {
|
||
for _, dayEntries := range dayMap {
|
||
merged = append(merged, dayEntries...)
|
||
}
|
||
}
|
||
|
||
// 3. 冲突校验。
|
||
//
|
||
// 3.1 判断依据:同一 (week, day, section) 只能有一个条目占用;
|
||
// 3.2 失败处理:一旦冲突,整批回退到 merge 前原始结果;
|
||
// 3.3 回退策略:回退后仍继续链路,避免请求直接失败。
|
||
if conflict := detectConflicts(merged); conflict != "" {
|
||
st.HybridEntries = originalEntries
|
||
emitStage("schedule_plan.merge.conflict", fmt.Sprintf("检测到冲突并回退:%s", conflict))
|
||
} else {
|
||
st.HybridEntries = merged
|
||
emitStage("schedule_plan.merge.done", fmt.Sprintf("合并完成,共 %d 个条目。", len(merged)))
|
||
}
|
||
|
||
// 4. 无论是否冲突,都生成“可回退快照”。
|
||
st.MergeSnapshot = deepCopyEntries(st.HybridEntries)
|
||
return st, nil
|
||
}
|
||
|
||
// detectConflicts 检测条目是否存在时间冲突。
|
||
//
|
||
// 返回语义:
|
||
// 1. 返回空字符串:无冲突;
|
||
// 2. 返回非空字符串:冲突描述,可直接用于日志/阶段提示。
|
||
func detectConflicts(entries []model.HybridScheduleEntry) string {
|
||
type slotKey struct {
|
||
week, day, section int
|
||
}
|
||
occupied := make(map[slotKey]string)
|
||
for _, entry := range entries {
|
||
// 1. 仅“阻塞建议任务”的条目参与冲突校验。
|
||
// 2. 可嵌入且当前未占用的课程槽位不应被判定为冲突。
|
||
if !entryBlocksSuggested(entry) {
|
||
continue
|
||
}
|
||
for section := entry.SectionFrom; section <= entry.SectionTo; section++ {
|
||
key := slotKey{week: entry.Week, day: entry.DayOfWeek, section: section}
|
||
if prevName, exists := occupied[key]; exists {
|
||
return fmt.Sprintf(
|
||
"W%dD%d 第%d节 冲突:[%s] 与 [%s]",
|
||
entry.Week, entry.DayOfWeek, section, prevName, entry.Name,
|
||
)
|
||
}
|
||
occupied[key] = entry.Name
|
||
}
|
||
}
|
||
return ""
|
||
}
|