Version: 0.7.1.dev.260321
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(总分流图 + 智能排程流转图)并补充决策文档 - ⚠️ 新增“连续微调复用”链路我尚未完成测试,且文档状态目前较为混乱,待连续对话微调功能真正测试完成后再统一更新
This commit is contained in:
@@ -40,8 +40,9 @@ func NewAgentServiceWithSchedule(
|
||||
|
||||
// 注入排程依赖:将 service 层方法包装为函数闭包,避免循环依赖。
|
||||
if scheduleSvc != nil {
|
||||
svc.SmartPlanningRawFunc = scheduleSvc.SmartPlanningRaw
|
||||
svc.HybridScheduleWithPlanFunc = scheduleSvc.HybridScheduleWithPlan
|
||||
svc.SmartPlanningMultiRawFunc = scheduleSvc.SmartPlanningMultiRaw
|
||||
svc.HybridScheduleWithPlanMultiFunc = scheduleSvc.HybridScheduleWithPlanMulti
|
||||
svc.ResolvePlanningWindowFunc = scheduleSvc.ResolvePlanningWindowByTaskClasses
|
||||
}
|
||||
|
||||
return svc
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/LoveLosita/smartflow/backend/inits"
|
||||
"github.com/LoveLosita/smartflow/backend/model"
|
||||
"github.com/LoveLosita/smartflow/backend/pkg"
|
||||
"github.com/LoveLosita/smartflow/backend/respond"
|
||||
eventsvc "github.com/LoveLosita/smartflow/backend/service/events"
|
||||
"github.com/cloudwego/eino-ext/components/model/ark"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
@@ -29,12 +30,20 @@ type AgentService struct {
|
||||
|
||||
// ── 排程计划依赖(函数注入,避免 service 包循环依赖)──
|
||||
|
||||
// SmartPlanningRawFunc 调用粗排算法,同时返回展示结构和已分配的任务项。
|
||||
// 由 service/agent_bridge.go 在构造时注入 ScheduleService.SmartPlanningRaw。
|
||||
SmartPlanningRawFunc func(ctx context.Context, userID, taskClassID int) ([]model.UserWeekSchedule, []model.TaskClassItem, error)
|
||||
// HybridScheduleWithPlanFunc 构建混合日程(既有日程 + 粗排建议),供 ReAct 精排使用。
|
||||
// 由 service/agent_bridge.go 在构造时注入。
|
||||
HybridScheduleWithPlanFunc func(ctx context.Context, userID, taskClassID int) ([]model.HybridScheduleEntry, []model.TaskClassItem, error)
|
||||
// SmartPlanningMultiRawFunc 是可选注入能力:
|
||||
// 1. 负责多任务类粗排;
|
||||
// 2. 当前主链路主要依赖 HybridScheduleWithPlanMultiFunc,可不强制使用。
|
||||
SmartPlanningMultiRawFunc func(ctx context.Context, userID int, taskClassIDs []int) ([]model.UserWeekSchedule, []model.TaskClassItem, error)
|
||||
// HybridScheduleWithPlanMultiFunc 是排程链路核心依赖:
|
||||
// 1. 负责把“多任务类粗排结果 + 既有日程”合并成 HybridEntries;
|
||||
// 2. daily/weekly ReAct 全部基于这个结果继续优化。
|
||||
HybridScheduleWithPlanMultiFunc func(ctx context.Context, userID int, taskClassIDs []int) ([]model.HybridScheduleEntry, []model.TaskClassItem, error)
|
||||
// ResolvePlanningWindowFunc 负责把 task_class_ids 解析成“全局排程窗口”的相对周/天边界。
|
||||
//
|
||||
// 作用:
|
||||
// 1. 给周级 Move 增加硬边界,避免首尾不足一周时移出有效日期范围;
|
||||
// 2. 该函数只做“窗口解析”,不负责粗排与混排计算。
|
||||
ResolvePlanningWindowFunc func(ctx context.Context, userID int, taskClassIDs []int) (startWeek, startDay, endWeek, endDay int, err error)
|
||||
}
|
||||
|
||||
// NewAgentService 构造 AgentService。
|
||||
@@ -302,6 +311,12 @@ func (s *AgentService) AgentChat(ctx context.Context, userMessage string, ifThin
|
||||
|
||||
// 3.1 先走轻量路由,拿到统一 action。
|
||||
routing := s.decideActionRouting(requestCtx, selectedModel, userMessage)
|
||||
if routing.RouteFailed {
|
||||
// 3.1.1 路由码失败不再回落聊天。
|
||||
// 3.1.2 直接返回内部错误,避免误进入业务分支导致“吐错内容”(例如吐排程 JSON)。
|
||||
pushErrNonBlocking(errChan, respond.RouteControlInternalError)
|
||||
return
|
||||
}
|
||||
|
||||
// 3.2 chat:直接走普通聊天主链路。
|
||||
if routing.Action == route.ActionChat {
|
||||
|
||||
@@ -73,6 +73,9 @@ func (e *quickNoteProgressEmitter) Emit(stage, detail string) {
|
||||
if detail != "" {
|
||||
reasoning += "\n" + detail
|
||||
}
|
||||
// 2.1 每条阶段消息末尾补双换行,避免客户端把多条 chunk 紧贴在同一行显示。
|
||||
// 这里统一在 emitter 层处理,所有接入 emitStage 的链路都会受益。
|
||||
reasoning += "\n\n"
|
||||
|
||||
// 3. 复用 OpenAI 兼容封装:把阶段文本伪装成 reasoning_content。
|
||||
chunk, err := chat.ToOpenAIStream(&schema.Message{ReasoningContent: reasoning}, e.requestID, e.modelName, e.created, false)
|
||||
|
||||
@@ -19,7 +19,7 @@ type actionRoutingDecision = route.RoutingDecision
|
||||
// 职责边界:
|
||||
// 1. 只负责调用 route 包拿分流结论;
|
||||
// 2. 不负责执行任何业务节点;
|
||||
// 3. route 层失败时的兜底策略由 route 包内部统一处理(当前为回落 chat)。
|
||||
// 3. route 层失败会通过 RoutingDecision.RouteFailed 向上层显式暴露。
|
||||
func (s *AgentService) decideActionRouting(ctx context.Context, selectedModel *ark.ChatModel, userMessage string) actionRoutingDecision {
|
||||
// 这里保留方法封装,是为了避免上层直接依赖 route 包,降低耦合。
|
||||
_ = s
|
||||
|
||||
@@ -8,18 +8,21 @@ import (
|
||||
|
||||
"github.com/LoveLosita/smartflow/backend/agent/scheduleplan"
|
||||
"github.com/LoveLosita/smartflow/backend/conv"
|
||||
"github.com/LoveLosita/smartflow/backend/model"
|
||||
"github.com/LoveLosita/smartflow/backend/pkg"
|
||||
"github.com/cloudwego/eino-ext/components/model/ark"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// runSchedulePlanFlow 执行"智能排程"分支。
|
||||
// runSchedulePlanFlow 执行“智能排程”分支。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责把本次请求接入 scheduleplan 执行器;
|
||||
// 2. 负责注入排程依赖(SmartPlanning / BatchApplyPlans / GetTaskClassByID);
|
||||
// 3. 负责对话历史获取,支持连续对话微调;
|
||||
// 4. 不负责聊天持久化(由 AgentChat 主流程统一收口)。
|
||||
// 1. 负责把本次请求接入 scheduleplan graph,并注入运行依赖。
|
||||
// 2. 负责读取对话历史(优先 Redis,未命中再回源 DB)用于连续对话微调。
|
||||
// 3. 负责把排程预览快照写入 Redis(供查询接口拉取 JSON)。
|
||||
// 4. 负责返回给上层“可直接发给用户的最终文本回复”。
|
||||
// 5. 不负责聊天持久化(由 AgentChat 主链路统一处理)。
|
||||
func (s *AgentService) runSchedulePlanFlow(
|
||||
ctx context.Context,
|
||||
selectedModel *ark.ChatModel,
|
||||
@@ -32,16 +35,37 @@ func (s *AgentService) runSchedulePlanFlow(
|
||||
outChan chan<- string,
|
||||
modelName string,
|
||||
) (string, error) {
|
||||
// 1. 依赖预检:排程依赖函数必须注入,否则无法完成排程链路。
|
||||
if s.SmartPlanningRawFunc == nil || s.HybridScheduleWithPlanFunc == nil {
|
||||
// 1. 依赖预检:缺硬依赖时直接失败,避免进入 graph 后才出现空指针或半途失败。
|
||||
// 1.1 SmartPlanningMultiRaw / HybridScheduleWithPlanMulti / ResolvePlanningWindow 任一缺失都无法继续。
|
||||
// 1.2 selectedModel 为空时无法执行 LLM 节点,直接返回错误由上层处理。
|
||||
if s.SmartPlanningMultiRawFunc == nil || s.HybridScheduleWithPlanMultiFunc == nil || s.ResolvePlanningWindowFunc == nil {
|
||||
return "", errors.New("schedule plan service dependencies are not ready")
|
||||
}
|
||||
if selectedModel == nil {
|
||||
return "", errors.New("schedule plan model is nil")
|
||||
}
|
||||
|
||||
// 2. 获取对话历史,用于连续对话微调场景。
|
||||
// 优先从 Redis 读取,未命中时回源 DB。
|
||||
// 2. 连续对话微调前置处理:先尝试读取“上一版预览快照”,再清理旧 key。
|
||||
// 2.1 先读后删的原因:
|
||||
// 2.1.1 若先删再读,会丢失“连续微调起点”;
|
||||
// 2.1.2 先读可让本轮在内存中复用上轮 HybridEntries。
|
||||
// 2.2 清理旧 key 仍然保留,避免前端在本轮进行中误读到旧结果。
|
||||
var previousPreview *model.SchedulePlanPreviewCache
|
||||
if s.agentCache != nil {
|
||||
preview, getErr := s.agentCache.GetSchedulePlanPreview(ctx, userID, chatID)
|
||||
if getErr != nil {
|
||||
log.Printf("读取上一版排程预览失败 chat_id=%s: %v", chatID, getErr)
|
||||
} else {
|
||||
previousPreview = preview
|
||||
}
|
||||
if delErr := s.agentCache.DeleteSchedulePlanPreview(ctx, userID, chatID); delErr != nil {
|
||||
log.Printf("清理旧排程预览失败 chat_id=%s: %v", chatID, delErr)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 读取对话历史:先快后稳。
|
||||
// 3.1 先查 Redis,命中则避免回源 DB,降低请求时延。
|
||||
// 3.2 Redis 异常仅记录日志,不中断主流程(回源 DB 兜底)。
|
||||
var chatHistory []*schema.Message
|
||||
if s.agentCache != nil {
|
||||
history, err := s.agentCache.GetHistory(ctx, chatID)
|
||||
@@ -52,7 +76,8 @@ func (s *AgentService) runSchedulePlanFlow(
|
||||
}
|
||||
}
|
||||
|
||||
// 2.1 缓存未命中时回源 DB。
|
||||
// 3.3 Redis 未命中时回源 DB,保证链路在缓存波动时仍可用。
|
||||
// 3.4 DB 回源失败同样只记日志并继续,让 graph 按“无历史”降级运行。
|
||||
if chatHistory == nil && s.repo != nil {
|
||||
histories, hisErr := s.repo.GetUserChatHistories(ctx, userID, pkg.HistoryFetchLimitByModel("worker"), chatID)
|
||||
if hisErr != nil {
|
||||
@@ -62,38 +87,56 @@ func (s *AgentService) runSchedulePlanFlow(
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 初始化排程状态对象。
|
||||
// 4. 执行 graph 主流程。
|
||||
// 4.1 这里只负责参数拼装与调用,不在 service 层重复实现 graph 节点逻辑。
|
||||
// 4.2 并发度/预算从配置注入,避免把调优参数写死在代码中。
|
||||
state := scheduleplan.NewSchedulePlanState(traceID, userID, chatID)
|
||||
|
||||
// 4. 构建依赖注入并执行 graph。
|
||||
// 4.3 连续对话微调注入:
|
||||
// 4.3.1 若命中上轮预览,则把任务类/混合条目/分配结果注入 state;
|
||||
// 4.3.2 这样 rough_build 可按需复用旧底板,避免每轮都重新粗排。
|
||||
if previousPreview != nil {
|
||||
state.HasPreviousPreview = true
|
||||
state.PreviousTaskClassIDs = append([]int(nil), previousPreview.TaskClassIDs...)
|
||||
state.PreviousHybridEntries = cloneHybridEntries(previousPreview.HybridEntries)
|
||||
state.PreviousAllocatedItems = cloneTaskClassItems(previousPreview.AllocatedItems)
|
||||
}
|
||||
finalState, runErr := scheduleplan.RunSchedulePlanGraph(ctx, scheduleplan.SchedulePlanGraphRunInput{
|
||||
Model: selectedModel,
|
||||
State: state,
|
||||
Deps: scheduleplan.SchedulePlanToolDeps{
|
||||
SmartPlanningRaw: s.SmartPlanningRawFunc,
|
||||
HybridScheduleWithPlan: s.HybridScheduleWithPlanFunc,
|
||||
SmartPlanningMultiRaw: s.SmartPlanningMultiRawFunc,
|
||||
HybridScheduleWithPlanMulti: s.HybridScheduleWithPlanMultiFunc,
|
||||
ResolvePlanningWindow: s.ResolvePlanningWindowFunc,
|
||||
},
|
||||
UserMessage: userMessage,
|
||||
Extra: extra,
|
||||
ChatHistory: chatHistory,
|
||||
EmitStage: emitStage,
|
||||
OutChan: outChan,
|
||||
ModelName: modelName,
|
||||
UserMessage: userMessage,
|
||||
Extra: extra,
|
||||
ChatHistory: chatHistory,
|
||||
EmitStage: emitStage,
|
||||
OutChan: outChan,
|
||||
ModelName: modelName,
|
||||
DailyRefineConcurrency: viper.GetInt("agent.dailyRefineConcurrency"),
|
||||
WeeklyAdjustBudget: viper.GetInt("agent.weeklyAdjustBudget"),
|
||||
})
|
||||
|
||||
if runErr != nil {
|
||||
// 4.3 graph 失败直接上抛,由上层决定回落或报错。
|
||||
return "", runErr
|
||||
}
|
||||
|
||||
// 5. 提取最终回复。
|
||||
// 5. 组装最终回复文本。
|
||||
// 5.1 明确移除“把排程结果序列化成 JSON 文本直接回传”的抽象,
|
||||
// 避免在 SSE 聊天链路里吐出原始 JSON,影响前端展示与用户体验。
|
||||
// 5.2 当 finalState 为空或 summary 为空时,返回统一兜底文案,保证接口有稳定输出。
|
||||
if finalState == nil {
|
||||
return "排程流程异常,请稍后重试。", nil
|
||||
}
|
||||
|
||||
reply := strings.TrimSpace(finalState.FinalSummary)
|
||||
if reply == "" {
|
||||
reply = "排程流程已完成,但未生成结果摘要。"
|
||||
}
|
||||
|
||||
// 6. 旁路写入排程预览缓存(结构化 JSON),给查询接口拉取。
|
||||
// 6.1 失败只记日志,不影响本次对话回复;
|
||||
// 6.2 成功后前端可通过 conversation_id 获取 candidate_plans。
|
||||
s.saveSchedulePlanPreview(ctx, userID, chatID, finalState)
|
||||
return reply, nil
|
||||
}
|
||||
|
||||
162
backend/service/agentsvc/agent_schedule_preview.go
Normal file
162
backend/service/agentsvc/agent_schedule_preview.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package agentsvc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/LoveLosita/smartflow/backend/agent/scheduleplan"
|
||||
"github.com/LoveLosita/smartflow/backend/model"
|
||||
"github.com/LoveLosita/smartflow/backend/respond"
|
||||
)
|
||||
|
||||
// saveSchedulePlanPreview 把排程结果以结构化 JSON 快照写入 Redis。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责把 finalState 中的 summary + candidate_plans 收敛为缓存 DTO;
|
||||
// 2. 负责以“失败不阻断聊天主链路”的策略执行写入;
|
||||
// 3. 不负责 SSE 返回协议,不负责数据库落库。
|
||||
func (s *AgentService) saveSchedulePlanPreview(ctx context.Context, userID int, chatID string, finalState *scheduleplan.SchedulePlanState) {
|
||||
// 1. 基础前置校验:任何关键依赖缺失都直接返回,避免产生无意义错误日志。
|
||||
if s == nil || s.agentCache == nil || finalState == nil {
|
||||
return
|
||||
}
|
||||
normalizedChatID := strings.TrimSpace(chatID)
|
||||
if normalizedChatID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 组装缓存快照:
|
||||
// 2.1 summary 优先取 final summary,空值时使用统一兜底文案;
|
||||
// 2.2 candidate_plans 做切片拷贝,避免后续引用共享导致意外覆盖;
|
||||
// 2.3 generated_at 用于前端判断“当前预览的新鲜度”。
|
||||
summary := strings.TrimSpace(finalState.FinalSummary)
|
||||
if summary == "" {
|
||||
summary = "排程流程已完成,但未生成结果摘要。"
|
||||
}
|
||||
preview := &model.SchedulePlanPreviewCache{
|
||||
UserID: userID,
|
||||
ConversationID: normalizedChatID,
|
||||
TraceID: strings.TrimSpace(finalState.TraceID),
|
||||
Summary: summary,
|
||||
CandidatePlans: cloneWeekSchedules(finalState.CandidatePlans),
|
||||
TaskClassIDs: append([]int(nil), finalState.TaskClassIDs...),
|
||||
HybridEntries: cloneHybridEntries(finalState.HybridEntries),
|
||||
AllocatedItems: cloneTaskClassItems(finalState.AllocatedItems),
|
||||
GeneratedAt: time.Now(),
|
||||
}
|
||||
|
||||
// 3. 尝试写入缓存:
|
||||
// 3.1 写入失败仅打日志,不上抛错误,保证聊天接口协议与可用性不受影响;
|
||||
// 3.2 兜底策略是“用户仍可收到文本摘要”,只是暂时无法通过新接口拉取结构化预览。
|
||||
if err := s.agentCache.SetSchedulePlanPreview(ctx, userID, normalizedChatID, preview); err != nil {
|
||||
log.Printf("写入排程预览缓存失败 chat_id=%s: %v", normalizedChatID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// GetSchedulePlanPreview 按 conversation_id 读取结构化排程预览。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责参数归一化、缓存读取与会话归属校验;
|
||||
// 2. 负责把缓存 DTO 转成 API 响应 DTO;
|
||||
// 3. 不负责触发排程,不负责补算缓存。
|
||||
func (s *AgentService) GetSchedulePlanPreview(ctx context.Context, userID int, chatID string) (*model.GetSchedulePlanPreviewResponse, error) {
|
||||
// 1. 参数校验:conversation_id 为空直接返回参数错误,避免无效 Redis 请求。
|
||||
normalizedChatID := strings.TrimSpace(chatID)
|
||||
if normalizedChatID == "" {
|
||||
return nil, respond.MissingParam
|
||||
}
|
||||
if s == nil || s.agentCache == nil {
|
||||
return nil, errors.New("agent cache is not initialized")
|
||||
}
|
||||
|
||||
// 2. 查询缓存并校验归属:
|
||||
// 2.1 缓存未命中:统一返回“预览不存在/已过期”;
|
||||
// 2.2 命中但 user_id 不一致:按未命中处理,避免泄露他人会话信息;
|
||||
// 2.3 失败兜底:缓存读异常直接上抛,由 API 层统一错误处理。
|
||||
preview, err := s.agentCache.GetSchedulePlanPreview(ctx, userID, normalizedChatID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if preview == nil {
|
||||
return nil, respond.SchedulePlanPreviewNotFound
|
||||
}
|
||||
if preview.UserID > 0 && preview.UserID != userID {
|
||||
return nil, respond.SchedulePlanPreviewNotFound
|
||||
}
|
||||
|
||||
// 3. 映射响应结构,保证输出字段稳定。
|
||||
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,
|
||||
GeneratedAt: preview.GeneratedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// cloneWeekSchedules 对周视图排程结果做深拷贝,避免切片引用共享。
|
||||
func cloneWeekSchedules(src []model.UserWeekSchedule) []model.UserWeekSchedule {
|
||||
if len(src) == 0 {
|
||||
return nil
|
||||
}
|
||||
dst := make([]model.UserWeekSchedule, 0, len(src))
|
||||
for _, week := range src {
|
||||
eventsCopy := make([]model.WeeklyEventBrief, len(week.Events))
|
||||
copy(eventsCopy, week.Events)
|
||||
dst = append(dst, model.UserWeekSchedule{
|
||||
Week: week.Week,
|
||||
Events: eventsCopy,
|
||||
})
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// cloneHybridEntries 深拷贝混合条目切片,避免缓存/状态之间相互污染。
|
||||
func cloneHybridEntries(src []model.HybridScheduleEntry) []model.HybridScheduleEntry {
|
||||
if len(src) == 0 {
|
||||
return nil
|
||||
}
|
||||
dst := make([]model.HybridScheduleEntry, len(src))
|
||||
copy(dst, src)
|
||||
return dst
|
||||
}
|
||||
|
||||
// cloneTaskClassItems 深拷贝任务块切片(包含指针字段),避免跨请求引用共享。
|
||||
func cloneTaskClassItems(src []model.TaskClassItem) []model.TaskClassItem {
|
||||
if len(src) == 0 {
|
||||
return nil
|
||||
}
|
||||
dst := make([]model.TaskClassItem, 0, len(src))
|
||||
for _, item := range src {
|
||||
copied := item
|
||||
if item.CategoryID != nil {
|
||||
v := *item.CategoryID
|
||||
copied.CategoryID = &v
|
||||
}
|
||||
if item.Order != nil {
|
||||
v := *item.Order
|
||||
copied.Order = &v
|
||||
}
|
||||
if item.Content != nil {
|
||||
v := *item.Content
|
||||
copied.Content = &v
|
||||
}
|
||||
if item.Status != nil {
|
||||
v := *item.Status
|
||||
copied.Status = &v
|
||||
}
|
||||
if item.EmbeddedTime != nil {
|
||||
t := *item.EmbeddedTime
|
||||
copied.EmbeddedTime = &t
|
||||
}
|
||||
dst = append(dst, copied)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -412,9 +413,9 @@ func (ss *ScheduleService) SmartPlanning(ctx context.Context, userID, taskClassI
|
||||
// SmartPlanningRaw 执行粗排算法并同时返回展示结构和已分配的任务项。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1) 与 SmartPlanning 共享完全相同的前置校验和粗排逻辑;
|
||||
// 2) 额外返回 allocatedItems(每项的 EmbeddedTime 已由算法回填),
|
||||
// 供 Agent 排程链路直接转换为 BatchApplyPlans 请求,无需再让模型"二次分配"。
|
||||
// 1. 与 SmartPlanning 共享完全相同的前置校验和粗排逻辑;
|
||||
// 2. 额外返回 allocatedItems(每项的 EmbeddedTime 已由算法回填),
|
||||
// 供 Agent 排程链路直接转换为 BatchApplyPlans 请求,无需再让模型"二次分配"。
|
||||
func (ss *ScheduleService) SmartPlanningRaw(ctx context.Context, userID, taskClassID int) ([]model.UserWeekSchedule, []model.TaskClassItem, error) {
|
||||
// 1. 获取任务类详情。
|
||||
taskClass, err := ss.taskClassDAO.GetCompleteTaskClassByID(ctx, taskClassID, userID)
|
||||
@@ -445,21 +446,222 @@ func (ss *ScheduleService) SmartPlanningRaw(ctx context.Context, userID, taskCla
|
||||
return displayResult, allocatedItems, nil
|
||||
}
|
||||
|
||||
// HybridScheduleWithPlan 构建"混合日程":将既有日程与粗排建议合并为统一结构。
|
||||
// SmartPlanningMulti 执行“多任务类智能粗排”,仅返回前端展示结构。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1) 获取 TaskClass 时间范围内的既有日程(课程 + 已落库任务);
|
||||
// 2) 调用粗排算法获取建议分配;
|
||||
// 3) 将两者合并为 []HybridScheduleEntry,供 ReAct 精排引擎在内存中操作。
|
||||
// 1. 负责把多任务类请求收口到统一粗排流程;
|
||||
// 2. 负责返回展示结构;
|
||||
// 3. 不返回底层分配细节(由 SmartPlanningMultiRaw 提供)。
|
||||
func (ss *ScheduleService) SmartPlanningMulti(ctx context.Context, userID int, taskClassIDs []int) ([]model.UserWeekSchedule, error) {
|
||||
displayResult, _, err := ss.SmartPlanningMultiRaw(ctx, userID, taskClassIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return displayResult, nil
|
||||
}
|
||||
|
||||
// SmartPlanningMultiRaw 执行“多任务类智能粗排”,同时返回展示结构和已分配任务项。
|
||||
//
|
||||
// 返回值:
|
||||
// - entries:混合日程条目(existing + suggested)
|
||||
// - allocatedItems:粗排已分配的任务项(用于后续落库)
|
||||
// - error
|
||||
// 职责边界:
|
||||
// 1. 负责多任务类请求的完整前置处理(归一化/校验/排序/时间窗收敛);
|
||||
// 2. 负责调用多任务类粗排主逻辑(共享资源池);
|
||||
// 3. 只计算建议,不负责落库。
|
||||
func (ss *ScheduleService) SmartPlanningMultiRaw(ctx context.Context, userID int, taskClassIDs []int) ([]model.UserWeekSchedule, []model.TaskClassItem, error) {
|
||||
// 1. 输入归一化。
|
||||
normalizedIDs := normalizeTaskClassIDsForMultiPlanning(taskClassIDs)
|
||||
if len(normalizedIDs) == 0 {
|
||||
return nil, nil, respond.WrongTaskClassID
|
||||
}
|
||||
|
||||
// 2. 批量读取完整任务类(含 Items)。
|
||||
taskClasses, err := ss.taskClassDAO.GetCompleteTaskClassesByIDs(ctx, userID, normalizedIDs)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// 3. 校验任务类并计算全局时间窗。
|
||||
orderedTaskClasses, globalStartDate, globalEndDate, err := prepareTaskClassesForMultiPlanning(taskClasses, normalizedIDs)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// 4. 拉取全局时间窗内的既有日程底板。
|
||||
schedules, err := ss.scheduleDAO.GetUserSchedulesByTimeRange(
|
||||
ctx,
|
||||
userID,
|
||||
conv.CalculateFirstDayOfWeek(globalStartDate),
|
||||
conv.CalculateLastDayOfWeek(globalEndDate),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// 5. 执行多任务类粗排(共享资源池 + 增量占位)。
|
||||
allocatedItems, err := logic.SmartPlanningRawItemsMulti(schedules, orderedTaskClasses)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// 6. 转换前端展示结构。
|
||||
displayResult := conv.PlanningResultToUserWeekSchedules(schedules, allocatedItems)
|
||||
return displayResult, allocatedItems, nil
|
||||
}
|
||||
|
||||
// ResolvePlanningWindowByTaskClasses 解析“多任务类排程窗口”的相对周/天边界。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只负责根据 task_class_ids 计算全局起止日期并转换成相对周/天;
|
||||
// 2. 不执行粗排、不查询课表、不生成 HybridEntries;
|
||||
// 3. 供 Agent 周级 Move 工具做硬边界校验,防止越界移动。
|
||||
//
|
||||
// 返回语义:
|
||||
// 1. startWeek/startDay:允许排程的起点(含);
|
||||
// 2. endWeek/endDay:允许排程的终点(含);
|
||||
// 3. error:任何校验或日期转换失败都返回错误。
|
||||
func (ss *ScheduleService) ResolvePlanningWindowByTaskClasses(ctx context.Context, userID int, taskClassIDs []int) (int, int, int, int, error) {
|
||||
// 1. 输入归一化:过滤非法值并去重。
|
||||
normalizedIDs := normalizeTaskClassIDsForMultiPlanning(taskClassIDs)
|
||||
if len(normalizedIDs) == 0 {
|
||||
return 0, 0, 0, 0, respond.WrongTaskClassID
|
||||
}
|
||||
|
||||
// 2. 批量查询任务类并复用统一校验逻辑,拿到全局起止日期。
|
||||
taskClasses, err := ss.taskClassDAO.GetCompleteTaskClassesByIDs(ctx, userID, normalizedIDs)
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, err
|
||||
}
|
||||
_, globalStartDate, globalEndDate, err := prepareTaskClassesForMultiPlanning(taskClasses, normalizedIDs)
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, err
|
||||
}
|
||||
|
||||
// 3. 把绝对日期转换为“相对周/天”。
|
||||
// 3.1 这里统一复用 conv.RealDateToRelativeDate,确保和现有排程口径一致;
|
||||
// 3.2 若日期超出学期配置范围,直接返回错误,避免错误边界进入工具层。
|
||||
startWeek, startDay, err := conv.RealDateToRelativeDate(globalStartDate.Format(conv.DateFormat))
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, err
|
||||
}
|
||||
endWeek, endDay, err := conv.RealDateToRelativeDate(globalEndDate.Format(conv.DateFormat))
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, err
|
||||
}
|
||||
if endWeek < startWeek || (endWeek == startWeek && endDay < startDay) {
|
||||
return 0, 0, 0, 0, respond.InvalidDateRange
|
||||
}
|
||||
return startWeek, startDay, endWeek, endDay, nil
|
||||
}
|
||||
|
||||
// normalizeTaskClassIDsForMultiPlanning 归一化 task_class_ids(过滤非法值、去重并保序)。
|
||||
func normalizeTaskClassIDsForMultiPlanning(ids []int) []int {
|
||||
if len(ids) == 0 {
|
||||
return []int{}
|
||||
}
|
||||
normalized := make([]int, 0, len(ids))
|
||||
seen := make(map[int]struct{}, len(ids))
|
||||
for _, id := range ids {
|
||||
if id <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[id]; exists {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
normalized = append(normalized, id)
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
// prepareTaskClassesForMultiPlanning 把 DAO 结果转成可直接粗排的数据集。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 校验每个任务类可参与自动排程;
|
||||
// 2. 计算全局时间窗(最早开始 ~ 最晚结束);
|
||||
// 3. 执行多任务类排序策略。
|
||||
func prepareTaskClassesForMultiPlanning(taskClasses []model.TaskClass, orderedIDs []int) ([]*model.TaskClass, time.Time, time.Time, error) {
|
||||
if len(orderedIDs) == 0 {
|
||||
return nil, time.Time{}, time.Time{}, respond.WrongTaskClassID
|
||||
}
|
||||
|
||||
classByID := make(map[int]*model.TaskClass, len(taskClasses))
|
||||
for i := range taskClasses {
|
||||
tc := &taskClasses[i]
|
||||
classByID[tc.ID] = tc
|
||||
}
|
||||
|
||||
ordered := make([]*model.TaskClass, 0, len(orderedIDs))
|
||||
var globalStart time.Time
|
||||
var globalEnd time.Time
|
||||
for idx, id := range orderedIDs {
|
||||
taskClass, exists := classByID[id]
|
||||
if !exists || taskClass == nil {
|
||||
return nil, time.Time{}, time.Time{}, respond.WrongTaskClassID
|
||||
}
|
||||
if taskClass.Mode == nil || *taskClass.Mode != "auto" {
|
||||
return nil, time.Time{}, time.Time{}, respond.TaskClassModeNotAuto
|
||||
}
|
||||
if taskClass.StartDate == nil || taskClass.EndDate == nil {
|
||||
return nil, time.Time{}, time.Time{}, respond.InvalidDateRange
|
||||
}
|
||||
start := *taskClass.StartDate
|
||||
end := *taskClass.EndDate
|
||||
if end.Before(start) {
|
||||
return nil, time.Time{}, time.Time{}, respond.InvalidDateRange
|
||||
}
|
||||
if idx == 0 || start.Before(globalStart) {
|
||||
globalStart = start
|
||||
}
|
||||
if idx == 0 || end.After(globalEnd) {
|
||||
globalEnd = end
|
||||
}
|
||||
ordered = append(ordered, taskClass)
|
||||
}
|
||||
|
||||
sortTaskClassesForMultiPlanning(ordered, orderedIDs)
|
||||
return ordered, globalStart, globalEnd, nil
|
||||
}
|
||||
|
||||
// sortTaskClassesForMultiPlanning 执行稳定排序:
|
||||
// 1. end_date 早优先;
|
||||
// 2. rapid 优先于 steady;
|
||||
// 3. 输入顺序兜底。
|
||||
func sortTaskClassesForMultiPlanning(taskClasses []*model.TaskClass, inputOrder []int) {
|
||||
if len(taskClasses) <= 1 {
|
||||
return
|
||||
}
|
||||
orderIndex := make(map[int]int, len(inputOrder))
|
||||
for idx, id := range inputOrder {
|
||||
orderIndex[id] = idx
|
||||
}
|
||||
|
||||
sort.SliceStable(taskClasses, func(i, j int) bool {
|
||||
left := taskClasses[i]
|
||||
right := taskClasses[j]
|
||||
if left == nil || right == nil {
|
||||
return left != nil
|
||||
}
|
||||
if left.EndDate != nil && right.EndDate != nil && !left.EndDate.Equal(*right.EndDate) {
|
||||
return left.EndDate.Before(*right.EndDate)
|
||||
}
|
||||
leftRapid := left.Strategy != nil && *left.Strategy == "rapid"
|
||||
rightRapid := right.Strategy != nil && *right.Strategy == "rapid"
|
||||
if leftRapid != rightRapid {
|
||||
return leftRapid
|
||||
}
|
||||
leftOrder, leftOK := orderIndex[left.ID]
|
||||
rightOrder, rightOK := orderIndex[right.ID]
|
||||
if leftOK && rightOK && leftOrder != rightOrder {
|
||||
return leftOrder < rightOrder
|
||||
}
|
||||
return left.ID < right.ID
|
||||
})
|
||||
}
|
||||
|
||||
// HybridScheduleWithPlan 构建“单任务类”混合日程(existing + suggested)。
|
||||
func (ss *ScheduleService) HybridScheduleWithPlan(
|
||||
ctx context.Context, userID, taskClassID int,
|
||||
) ([]model.HybridScheduleEntry, []model.TaskClassItem, error) {
|
||||
// 1. 获取任务类详情。
|
||||
// 1. 校验并读取任务类。
|
||||
taskClass, err := ss.taskClassDAO.GetCompleteTaskClassByID(ctx, taskClassID, userID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -467,11 +669,14 @@ func (ss *ScheduleService) HybridScheduleWithPlan(
|
||||
if taskClass == nil {
|
||||
return nil, nil, respond.WrongTaskClassID
|
||||
}
|
||||
if *taskClass.Mode != "auto" {
|
||||
if taskClass.Mode == nil || *taskClass.Mode != "auto" {
|
||||
return nil, nil, respond.TaskClassModeNotAuto
|
||||
}
|
||||
if taskClass.StartDate == nil || taskClass.EndDate == nil {
|
||||
return nil, nil, respond.InvalidDateRange
|
||||
}
|
||||
|
||||
// 2. 获取时间范围内的既有日程。
|
||||
// 2. 拉取时间窗内既有日程。
|
||||
schedules, err := ss.scheduleDAO.GetUserSchedulesByTimeRange(
|
||||
ctx, userID,
|
||||
conv.CalculateFirstDayOfWeek(*taskClass.StartDate),
|
||||
@@ -481,20 +686,79 @@ func (ss *ScheduleService) HybridScheduleWithPlan(
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// 3. 执行粗排算法。
|
||||
// 3. 执行粗排。
|
||||
allocatedItems, err := logic.SmartPlanningRawItems(schedules, taskClass)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// 4. 合并为 HybridScheduleEntry 切片。
|
||||
// 4. 统一合并。
|
||||
entries := buildHybridEntriesFromSchedulesAndAllocated(schedules, allocatedItems)
|
||||
return entries, allocatedItems, nil
|
||||
}
|
||||
|
||||
// HybridScheduleWithPlanMulti 构建“多任务类”混合日程(existing + suggested)。
|
||||
func (ss *ScheduleService) HybridScheduleWithPlanMulti(
|
||||
ctx context.Context,
|
||||
userID int,
|
||||
taskClassIDs []int,
|
||||
) ([]model.HybridScheduleEntry, []model.TaskClassItem, error) {
|
||||
// 1. 归一化任务类 ID。
|
||||
normalizedIDs := normalizeTaskClassIDsForMultiPlanning(taskClassIDs)
|
||||
if len(normalizedIDs) == 0 {
|
||||
return nil, nil, respond.WrongTaskClassID
|
||||
}
|
||||
|
||||
// 2. 拉取任务类并做校验/排序。
|
||||
taskClasses, err := ss.taskClassDAO.GetCompleteTaskClassesByIDs(ctx, userID, normalizedIDs)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
orderedTaskClasses, globalStartDate, globalEndDate, err := prepareTaskClassesForMultiPlanning(taskClasses, normalizedIDs)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// 3. 拉取全局时间窗内既有日程。
|
||||
schedules, err := ss.scheduleDAO.GetUserSchedulesByTimeRange(
|
||||
ctx,
|
||||
userID,
|
||||
conv.CalculateFirstDayOfWeek(globalStartDate),
|
||||
conv.CalculateLastDayOfWeek(globalEndDate),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// 4. 多任务类粗排。
|
||||
allocatedItems, err := logic.SmartPlanningRawItemsMulti(schedules, orderedTaskClasses)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// 5. 统一合并。
|
||||
entries := buildHybridEntriesFromSchedulesAndAllocated(schedules, allocatedItems)
|
||||
return entries, allocatedItems, nil
|
||||
}
|
||||
|
||||
// buildHybridEntriesFromSchedulesAndAllocated 合并 existing/suggested 条目。
|
||||
//
|
||||
// 说明:
|
||||
// 1. existing 按“事件 + 天 + 可嵌入语义 + 阻塞语义”分组,再按连续节次切块;
|
||||
// 2. suggested 直接根据 allocatedItems 生成;
|
||||
// 3. 仅做内存组装,不做数据库操作。
|
||||
func buildHybridEntriesFromSchedulesAndAllocated(
|
||||
schedules []model.Schedule,
|
||||
allocatedItems []model.TaskClassItem,
|
||||
) []model.HybridScheduleEntry {
|
||||
entries := make([]model.HybridScheduleEntry, 0, len(schedules)/2+len(allocatedItems))
|
||||
|
||||
// 4.1 既有日程:按 EventID+Week+DayOfWeek 分组,合并连续节次。
|
||||
type eventGroupKey struct {
|
||||
EventID int
|
||||
Week int
|
||||
DayOfWeek int
|
||||
EventID int
|
||||
Week int
|
||||
DayOfWeek int
|
||||
CanBeEmbedded bool
|
||||
BlockForSuggested bool
|
||||
}
|
||||
type eventGroup struct {
|
||||
Key eventGroupKey
|
||||
@@ -503,48 +767,82 @@ func (ss *ScheduleService) HybridScheduleWithPlan(
|
||||
Sections []int
|
||||
}
|
||||
groupMap := make(map[eventGroupKey]*eventGroup)
|
||||
|
||||
// 1. 先处理 existing。
|
||||
for _, s := range schedules {
|
||||
key := eventGroupKey{EventID: s.EventID, Week: s.Week, DayOfWeek: s.DayOfWeek}
|
||||
g, ok := groupMap[key]
|
||||
name := "未知"
|
||||
typ := "course"
|
||||
canBeEmbedded := false
|
||||
if s.Event != nil {
|
||||
name = s.Event.Name
|
||||
typ = s.Event.Type
|
||||
canBeEmbedded = s.Event.CanBeEmbedded
|
||||
}
|
||||
|
||||
// 1.1 阻塞语义:
|
||||
// 1.1.1 task 默认阻塞;
|
||||
// 1.1.2 course 且不可嵌入时阻塞;
|
||||
// 1.1.3 course 且可嵌入时,若当前原子格未被 embedded_task 占用,则不阻塞。
|
||||
blockForSuggested := true
|
||||
if typ == "course" && canBeEmbedded && s.EmbeddedTaskID == nil {
|
||||
blockForSuggested = false
|
||||
}
|
||||
|
||||
key := eventGroupKey{
|
||||
EventID: s.EventID,
|
||||
Week: s.Week,
|
||||
DayOfWeek: s.DayOfWeek,
|
||||
CanBeEmbedded: canBeEmbedded,
|
||||
BlockForSuggested: blockForSuggested,
|
||||
}
|
||||
group, ok := groupMap[key]
|
||||
if !ok {
|
||||
name := "未知"
|
||||
typ := "course"
|
||||
if s.Event != nil {
|
||||
name = s.Event.Name
|
||||
typ = s.Event.Type
|
||||
group = &eventGroup{
|
||||
Key: key,
|
||||
Name: name,
|
||||
Type: typ,
|
||||
}
|
||||
g = &eventGroup{Key: key, Name: name, Type: typ}
|
||||
groupMap[key] = g
|
||||
groupMap[key] = group
|
||||
}
|
||||
g.Sections = append(g.Sections, s.Section)
|
||||
}
|
||||
for _, g := range groupMap {
|
||||
if len(g.Sections) == 0 {
|
||||
continue
|
||||
}
|
||||
// 排序后取首尾作为 SectionFrom/SectionTo
|
||||
minS, maxS := g.Sections[0], g.Sections[0]
|
||||
for _, s := range g.Sections[1:] {
|
||||
if s < minS {
|
||||
minS = s
|
||||
}
|
||||
if s > maxS {
|
||||
maxS = s
|
||||
}
|
||||
}
|
||||
entries = append(entries, model.HybridScheduleEntry{
|
||||
Week: g.Key.Week,
|
||||
DayOfWeek: g.Key.DayOfWeek,
|
||||
SectionFrom: minS,
|
||||
SectionTo: maxS,
|
||||
Name: g.Name,
|
||||
Type: g.Type,
|
||||
Status: "existing",
|
||||
EventID: g.Key.EventID,
|
||||
})
|
||||
group.Sections = append(group.Sections, s.Section)
|
||||
}
|
||||
|
||||
// 4.2 粗排建议:每个已分配的 TaskClassItem 转为一条 suggested 条目。
|
||||
for _, group := range groupMap {
|
||||
if len(group.Sections) == 0 {
|
||||
continue
|
||||
}
|
||||
sort.Ints(group.Sections)
|
||||
|
||||
runStart := group.Sections[0]
|
||||
prev := group.Sections[0]
|
||||
flushRun := func(from, to int) {
|
||||
entries = append(entries, model.HybridScheduleEntry{
|
||||
Week: group.Key.Week,
|
||||
DayOfWeek: group.Key.DayOfWeek,
|
||||
SectionFrom: from,
|
||||
SectionTo: to,
|
||||
Name: group.Name,
|
||||
Type: group.Type,
|
||||
Status: "existing",
|
||||
EventID: group.Key.EventID,
|
||||
CanBeEmbedded: group.Key.CanBeEmbedded,
|
||||
BlockForSuggested: group.Key.BlockForSuggested,
|
||||
})
|
||||
}
|
||||
for i := 1; i < len(group.Sections); i++ {
|
||||
cur := group.Sections[i]
|
||||
if cur == prev+1 {
|
||||
prev = cur
|
||||
continue
|
||||
}
|
||||
flushRun(runStart, prev)
|
||||
runStart = cur
|
||||
prev = cur
|
||||
}
|
||||
flushRun(runStart, prev)
|
||||
}
|
||||
|
||||
// 2. 再处理 suggested。
|
||||
for _, item := range allocatedItems {
|
||||
if item.EmbeddedTime == nil {
|
||||
continue
|
||||
@@ -554,16 +852,17 @@ func (ss *ScheduleService) HybridScheduleWithPlan(
|
||||
name = strings.TrimSpace(*item.Content)
|
||||
}
|
||||
entries = append(entries, model.HybridScheduleEntry{
|
||||
Week: item.EmbeddedTime.Week,
|
||||
DayOfWeek: item.EmbeddedTime.DayOfWeek,
|
||||
SectionFrom: item.EmbeddedTime.SectionFrom,
|
||||
SectionTo: item.EmbeddedTime.SectionTo,
|
||||
Name: name,
|
||||
Type: "task",
|
||||
Status: "suggested",
|
||||
TaskItemID: item.ID,
|
||||
Week: item.EmbeddedTime.Week,
|
||||
DayOfWeek: item.EmbeddedTime.DayOfWeek,
|
||||
SectionFrom: item.EmbeddedTime.SectionFrom,
|
||||
SectionTo: item.EmbeddedTime.SectionTo,
|
||||
Name: name,
|
||||
Type: "task",
|
||||
Status: "suggested",
|
||||
TaskItemID: item.ID,
|
||||
BlockForSuggested: true,
|
||||
})
|
||||
}
|
||||
|
||||
return entries, allocatedItems, nil
|
||||
return entries
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user