Version: 0.9.59.dev.260430

后端:
1. 主动调度预览确认主链路落地——新增主动调度数据模型、DAO 与事件契约;接入 dry-run pipeline 与任务触发的 job upsert/cancel;新增 preview 查询与 confirm API,支持 apply_id 幂等确认并同步写入 task_pool 日程
2. 同步更新主动调度实施文档的阶段状态与验收记录

前端:
3. AssistantPanel 脚本层继续解耦——私有类型迁移到独立类型文件,并抽离会话、工具轨迹、思考摘要、任务表单等纯函数辅助逻辑;保持助手面板模板与样式不变,降低表现层回归风险
This commit is contained in:
LoveLosita
2026-04-30 12:05:15 +08:00
parent 1555042e80
commit e945578fbf
38 changed files with 10267 additions and 580 deletions

View File

@@ -0,0 +1,220 @@
package schedulercontext
import (
"context"
"errors"
"time"
"github.com/LoveLosita/smartflow/backend/active_scheduler/ports"
"github.com/LoveLosita/smartflow/backend/active_scheduler/trigger"
)
// Builder 负责把统一 trigger 转成主动调度只读事实快照。
//
// 职责边界:
// 1. 只通过 ports 读取外部事实;
// 2. 不生成 candidates不调用 LLM不写 preview
// 3. 缺少业务事实时尽量写入 MissingInfo让 observe 阶段裁决 ask_user。
type Builder struct {
readers ports.Readers
clock func() time.Time
}
func NewBuilder(readers ports.Readers) (*Builder, error) {
if readers.ScheduleReader == nil {
return nil, errors.New("ScheduleReader 不能为空")
}
if readers.TaskReader == nil {
return nil, errors.New("TaskReader 不能为空")
}
if readers.FeedbackReader == nil {
return nil, errors.New("FeedbackReader 不能为空")
}
return &Builder{
readers: readers,
clock: time.Now,
}, nil
}
// SetClock 允许测试注入稳定时钟。
//
// 职责边界:
// 1. 仅影响 real_now
// 2. 不覆盖 trigger.MockNow 的业务语义;
// 3. nil 会被忽略,避免测试误把时钟置空。
func (b *Builder) SetClock(clock func() time.Time) {
if clock != nil {
b.clock = clock
}
}
// BuildContext 执行 dry-run 主链路第一步:构造主动调度上下文。
func (b *Builder) BuildContext(ctx context.Context, trig trigger.ActiveScheduleTrigger) (*ActiveScheduleContext, error) {
if err := trig.Validate(); err != nil {
return nil, err
}
realNow := b.clock()
effectiveNow := trig.EffectiveNow(realNow)
windowStart := effectiveNow
windowEnd := effectiveNow.Add(24 * time.Hour)
result := &ActiveScheduleContext{
Trigger: trig,
User: UserFacts{
UserID: trig.UserID,
Timezone: effectiveNow.Location().String(),
},
Now: NowFacts{
RealNow: realNow,
EffectiveNow: effectiveNow,
},
Window: WindowFacts{
StartAt: windowStart,
EndAt: windowEnd,
WindowReason: WindowReasonRolling24H,
},
Target: TargetFacts{SourceType: trig.TargetType},
Trace: TraceFacts{
TraceID: trig.TraceID,
BuildSteps: []string{
"1. 校验 trigger 并确定 real_now / effective_now。",
"2. 构造滚动 24 小时时间窗,后续读取均基于同一窗口。",
},
},
}
switch trig.TriggerType {
case trigger.TriggerTypeImportantUrgentTask:
if err := b.fillTaskPoolFacts(ctx, result); err != nil {
return nil, err
}
case trigger.TriggerTypeUnfinishedFeedback:
if err := b.fillFeedbackFacts(ctx, result); err != nil {
return nil, err
}
}
if err := b.fillScheduleFacts(ctx, result); err != nil {
return nil, err
}
b.fillDerivedFacts(result)
return result, nil
}
func (b *Builder) fillTaskPoolFacts(ctx context.Context, result *ActiveScheduleContext) error {
task, found, err := b.readers.TaskReader.GetTaskForActiveSchedule(ctx, ports.TaskRequest{
UserID: result.Trigger.UserID,
TaskID: result.Trigger.TargetID,
Now: result.Now.EffectiveNow,
})
if err != nil {
return err
}
if !found {
result.DerivedFacts.MissingInfo = append(result.DerivedFacts.MissingInfo, "target_task")
result.Trace.Warnings = append(result.Trace.Warnings, "未读取到目标 task_pool 任务,后续应转为 ask_user。")
return nil
}
estimatedSections := task.EstimatedSections
if estimatedSections <= 0 {
// 1. 旧数据可能没有 estimated_sectionsMVP 兜底为 1 节,避免空值阻断 dry-run。
// 2. 正式 adapter 后续应尽量提供真实字段,减少兜底带来的预览偏差。
estimatedSections = 1
}
result.TaskPoolFacts.TargetTask = &task
result.Target.TaskID = task.ID
result.Target.Title = task.Title
result.Target.EstimatedSections = estimatedSections
result.Target.DeadlineAt = task.DeadlineAt
result.Target.UrgencyThresholdAt = task.UrgencyThresholdAt
result.Target.Priority = task.Priority
if task.IsCompleted {
result.Target.Status = "completed"
} else {
result.Target.Status = "pending"
}
result.Trace.BuildSteps = append(result.Trace.BuildSteps, "3. 通过 TaskReader 读取 task_pool 目标事实。")
return nil
}
func (b *Builder) fillFeedbackFacts(ctx context.Context, result *ActiveScheduleContext) error {
feedback, found, err := b.readers.FeedbackReader.GetFeedbackSignal(ctx, ports.FeedbackRequest{
UserID: result.Trigger.UserID,
FeedbackID: result.Trigger.FeedbackID,
IdempotencyKey: result.Trigger.IdempotencyKey,
TargetType: string(result.Trigger.TargetType),
TargetID: result.Trigger.TargetID,
})
if err != nil {
return err
}
if !found {
result.DerivedFacts.MissingInfo = append(result.DerivedFacts.MissingInfo, "feedback_signal")
result.Trace.Warnings = append(result.Trace.Warnings, "未读取到反馈信号,后续应转为 ask_user。")
return nil
}
result.FeedbackFacts = FeedbackFacts{
FeedbackID: feedback.FeedbackID,
FeedbackText: feedback.Text,
TargetKnown: feedback.TargetKnown,
TargetEventID: feedback.TargetEventID,
TargetTaskItemID: feedback.TargetTaskItemID,
FeedbackTarget: feedback.TargetTitle,
}
result.Target.ScheduleEventID = feedback.TargetEventID
result.Target.TaskItemID = feedback.TargetTaskItemID
result.Target.Title = feedback.TargetTitle
result.Target.EstimatedSections = 1
if !feedback.TargetKnown {
result.DerivedFacts.MissingInfo = append(result.DerivedFacts.MissingInfo, "feedback_target")
}
result.Trace.BuildSteps = append(result.Trace.BuildSteps, "3. 通过 FeedbackReader 读取 unfinished_feedback 信号。")
return nil
}
func (b *Builder) fillScheduleFacts(ctx context.Context, result *ActiveScheduleContext) error {
facts, err := b.readers.ScheduleReader.GetScheduleFactsByWindow(ctx, ports.ScheduleWindowRequest{
UserID: result.Trigger.UserID,
TargetType: string(result.Trigger.TargetType),
TargetID: result.Trigger.TargetID,
WindowStart: result.Window.StartAt,
WindowEnd: result.Window.EndAt,
Now: result.Now.EffectiveNow,
})
if err != nil {
return err
}
result.ScheduleFacts = ScheduleFacts{
Events: facts.Events,
OccupiedSlots: facts.OccupiedSlots,
FreeSlots: facts.FreeSlots,
NextDynamicTask: facts.NextDynamicTask,
}
result.Window.RelativeSlots = append(result.Window.RelativeSlots, facts.OccupiedSlots...)
result.Window.RelativeSlots = append(result.Window.RelativeSlots, facts.FreeSlots...)
result.DerivedFacts.TargetAlreadyScheduled = facts.TargetAlreadyScheduled
result.Trace.BuildSteps = append(result.Trace.BuildSteps, "4. 通过 ScheduleReader 读取滚动 24 小时日程事实。")
return nil
}
func (b *Builder) fillDerivedFacts(result *ActiveScheduleContext) {
result.DerivedFacts.AvailableCapacity = len(result.ScheduleFacts.FreeSlots)
if result.TaskPoolFacts.TargetTask != nil {
result.DerivedFacts.TargetCompleted = result.TaskPoolFacts.TargetTask.IsCompleted
}
if result.Trigger.TriggerType == trigger.TriggerTypeUnfinishedFeedback && !result.FeedbackFacts.TargetKnown {
result.DerivedFacts.MissingInfo = appendMissing(result.DerivedFacts.MissingInfo, "feedback_target")
}
result.Trace.BuildSteps = append(result.Trace.BuildSteps, "5. 汇总完成状态、已排状态、可用容量与缺失事实。")
}
func appendMissing(values []string, next string) []string {
for _, value := range values {
if value == next {
return values
}
}
return append(values, next)
}