Files
smartmate/backend/active_scheduler/preview/dto.go
Losita a3eaa9b2c2 Version: 0.9.61.dev.260501
后端:
1. 主动调度 graph + session bridge 收口——把 dry-run / select / preview / confirm / rerun 串成受限 graph,新增 active_schedule_sessions 缓存与聊天拦截,ready_preview 后释放回自由聊天
2. 会话与通知链路对齐——notification 统一绑定 conversation_id,action_url 指向 /assistant/{conversation_id},会话不存在改回 404 语义,避免 wrong param type 误导排障
3. estimated_sections 写入与主动调度消费链路补齐——任务创建、quick task 与随口记入口都透传估计节数,主动调度只消费落库值

前端:
4. AssistantPanel 最小适配主动调度预览与失败态——复用主动调度卡片/微调弹窗,补历史加载失败可见提示与跨账号会话拦截

文档:
5. 更新主动调度缺口分阶段实施计划和实现方案,标记阶段 0-2 收口并同步接力状态
2026-05-01 20:48:32 +08:00

235 lines
9.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package preview
import (
"encoding/json"
"time"
"github.com/LoveLosita/smartflow/backend/active_scheduler/candidate"
schedulercontext "github.com/LoveLosita/smartflow/backend/active_scheduler/context"
"github.com/LoveLosita/smartflow/backend/active_scheduler/observe"
"github.com/LoveLosita/smartflow/backend/active_scheduler/ports"
)
// CreatePreviewRequest 是把 dry-run 结果固化成主动调度预览的请求 DTO。
//
// 职责边界:
// 1. 负责承载 preview 写库所需的 dry-run 结果与可选覆盖字段;
// 2. 不承载 confirm/apply 请求,也不允许调用方传入正式日程写入参数;
// 3. GeneratedAt 为空时由 Service 时钟生成ExpiresAt 固定由 generated_at + 1h 推导。
type CreatePreviewRequest struct {
ActiveContext *schedulercontext.ActiveScheduleContext `json:"-"`
Observation observe.Result `json:"-"`
Candidates []candidate.Candidate `json:"-"`
PreviewID string `json:"preview_id,omitempty"`
TriggerID string `json:"trigger_id,omitempty"`
SelectedCandidateID string `json:"selected_candidate_id,omitempty"`
BaseVersion string `json:"base_version,omitempty"`
GeneratedAt time.Time `json:"generated_at,omitempty"`
ExplanationText string `json:"explanation_text,omitempty"`
NotificationSummary string `json:"notification_summary,omitempty"`
FallbackUsed bool `json:"fallback_used,omitempty"`
}
// CreatePreviewResponse 是写入 preview 后可直接返回给 API 的响应 DTO。
type CreatePreviewResponse struct {
Detail ActiveSchedulePreviewDetail `json:"detail"`
}
// GetPreviewRequest 是查询 preview 详情的请求 DTO。
//
// 职责边界:
// 1. UserID 来自鉴权上下文,不能信任前端透传;
// 2. PreviewID 来自路由参数;
// 3. 查询只返回预览快照,不执行过期状态回写、不触发 apply。
type GetPreviewRequest struct {
UserID int `json:"user_id"`
PreviewID string `json:"preview_id"`
}
// ActiveSchedulePreviewDetail 是主动调度预览详情页响应 DTO。
//
// 职责边界:
// 1. 负责把 active_schedule_previews 中的 JSON 快照还原成前端可展示结构;
// 2. 不包含正式写日程能力,不代表 confirm 请求已通过校验;
// 3. CanConfirm 只表达当前快照状态可发起确认,最终是否能应用仍由 confirm/apply 链路重校验。
type ActiveSchedulePreviewDetail struct {
PreviewID string `json:"preview_id"`
Status string `json:"status"`
ApplyStatus string `json:"apply_status"`
ExpiresAt time.Time `json:"expires_at"`
GeneratedAt time.Time `json:"generated_at"`
Expired bool `json:"expired"`
Trigger PreviewTriggerDTO `json:"trigger"`
Explanation string `json:"explanation"`
Notification string `json:"notification_summary"`
SelectedCandidate CandidateDTO `json:"selected_candidate"`
Candidates []CandidateDTO `json:"candidates"`
Decision observe.Decision `json:"decision"`
Metrics observe.Metrics `json:"metrics"`
Issues []observe.Issue `json:"issues"`
ContextSummary ContextSummaryDTO `json:"context_summary"`
Before SchedulePreviewVersion `json:"before"`
After SchedulePreviewVersion `json:"after"`
Changes []ActiveScheduleChangeItem `json:"changes"`
Risk RiskDTO `json:"risk"`
BaseVersion string `json:"base_version"`
CanConfirm bool `json:"can_confirm"`
CanIgnore bool `json:"can_ignore"`
TraceID string `json:"trace_id"`
}
type PreviewTriggerDTO struct {
TriggerID string `json:"trigger_id"`
TriggerType string `json:"trigger_type"`
Source string `json:"source"`
TargetType string `json:"target_type"`
TargetID int `json:"target_id"`
RequestedAt time.Time `json:"requested_at"`
}
type CandidateDTO struct {
CandidateID string `json:"candidate_id"`
CandidateType string `json:"candidate_type"`
Title string `json:"title"`
Summary string `json:"summary"`
Target CandidateTargetDTO `json:"target"`
Changes []ActiveScheduleChangeItem `json:"changes"`
BeforeSummary string `json:"before_summary"`
AfterSummary string `json:"after_summary"`
Risk string `json:"risk"`
Score int `json:"score"`
Validation candidate.Validation `json:"validation"`
Source string `json:"source"`
}
type CandidateTargetDTO struct {
TargetType string `json:"target_type"`
TargetID int `json:"target_id"`
Title string `json:"title"`
}
type ContextSummaryDTO struct {
UserID int `json:"user_id"`
Timezone string `json:"timezone"`
TriggerSource string `json:"trigger_source"`
RequestedAt time.Time `json:"requested_at"`
WindowStart time.Time `json:"window_start"`
WindowEnd time.Time `json:"window_end"`
WindowReason string `json:"window_reason"`
TargetType string `json:"target_type"`
TargetID int `json:"target_id"`
TargetTitle string `json:"target_title"`
MissingInfo []string `json:"missing_info"`
TraceSteps []string `json:"trace_steps"`
Warnings []string `json:"warnings"`
}
type SchedulePreviewVersion struct {
Title string `json:"title"`
WindowStart time.Time `json:"window_start"`
WindowEnd time.Time `json:"window_end"`
Entries []SchedulePreviewEntry `json:"entries"`
SummaryLines []string `json:"summary_lines"`
}
type SchedulePreviewEntry struct {
EntryID string `json:"entry_id"`
SourceType string `json:"source_type"`
SourceID int `json:"source_id"`
Title string `json:"title"`
StartAt time.Time `json:"start_at,omitempty"`
EndAt time.Time `json:"end_at,omitempty"`
Week int `json:"week,omitempty"`
DayOfWeek int `json:"day_of_week,omitempty"`
SectionFrom int `json:"section_from,omitempty"`
SectionTo int `json:"section_to,omitempty"`
Status string `json:"status"`
Editable bool `json:"editable"`
}
type ActiveScheduleChangeItem struct {
ChangeID string `json:"change_id"`
ChangeType string `json:"change_type"`
TargetType string `json:"target_type"`
TargetID int `json:"target_id"`
FromSlot *SlotDTO `json:"from_slot,omitempty"`
ToSlot *SlotSpanDTO `json:"to_slot,omitempty"`
DurationSections int `json:"duration_sections"`
AffectedEventIDs []int `json:"affected_event_ids"`
EditedAllowed bool `json:"edited_allowed"`
Metadata map[string]string `json:"metadata,omitempty"`
}
type SlotDTO struct {
Week int `json:"week"`
DayOfWeek int `json:"day_of_week"`
Section int `json:"section"`
StartAt time.Time `json:"start_at,omitempty"`
EndAt time.Time `json:"end_at,omitempty"`
}
type SlotSpanDTO struct {
Start SlotDTO `json:"start"`
End SlotDTO `json:"end"`
DurationSections int `json:"duration_sections"`
}
type RiskDTO struct {
Level string `json:"level"`
Summary string `json:"summary"`
Validation candidate.Validation `json:"validation"`
RiskMetrics observe.RiskMetrics `json:"risk_metrics"`
AffectedIDs []int `json:"affected_event_ids"`
RequiresLLM bool `json:"requires_llm"`
FallbackUsed bool `json:"fallback_used"`
}
// rawPreviewSnapshot 聚合需要写入 active_schedule_previews JSON 字段的快照。
type rawPreviewSnapshot struct {
selectedCandidate CandidateDTO
candidates []CandidateDTO
decision observe.Decision
metrics observe.Metrics
issues []observe.Issue
contextSummary ContextSummaryDTO
before SchedulePreviewVersion
changes []ActiveScheduleChangeItem
after SchedulePreviewVersion
risk RiskDTO
}
func slotDTO(slot ports.Slot) SlotDTO {
return SlotDTO{
Week: slot.Week,
DayOfWeek: slot.DayOfWeek,
Section: slot.Section,
StartAt: slot.StartAt,
EndAt: slot.EndAt,
}
}
func slotSpanDTO(span ports.SlotSpan) SlotSpanDTO {
return SlotSpanDTO{
Start: slotDTO(span.Start),
End: slotDTO(span.End),
DurationSections: span.DurationSections,
}
}
// decodeJSONField 只负责 preview 包内部 DTO 解码。
//
// 说明:
// 1. 当前任务限制只允许修改 preview 目录,不能把 JSON helper 下沉到公共层;
// 2. 因此这里暂时保留包内小函数,后续若第二个 active_scheduler 子包也需要同类能力,再按 AGENTS 规则抽公共层;
// 3. 解码失败返回原始错误,避免把损坏快照静默展示给用户。
func decodeJSONField[T any](raw *string, fallback T) (T, error) {
if raw == nil || *raw == "" {
return fallback, nil
}
var value T
if err := json.Unmarshal([]byte(*raw), &value); err != nil {
return fallback, err
}
return value, nil
}