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,264 @@
package model
import (
"time"
"gorm.io/gorm"
)
const (
// ActiveScheduleJobStatusPending 表示 job 已创建,等待到达 trigger_at 后扫描。
ActiveScheduleJobStatusPending = "pending"
// ActiveScheduleJobStatusTriggered 表示 job 已生成正式 trigger后续由 trigger 串联状态。
ActiveScheduleJobStatusTriggered = "triggered"
// ActiveScheduleJobStatusCanceled 表示任务已完成或被取消job 不再触发。
ActiveScheduleJobStatusCanceled = "canceled"
// ActiveScheduleJobStatusSkipped 表示扫描时发现已无需主动调度。
ActiveScheduleJobStatusSkipped = "skipped"
// ActiveScheduleJobStatusFailed 表示扫描或触发写入失败,保留错误供重试/排障。
ActiveScheduleJobStatusFailed = "failed"
)
const (
// ActiveScheduleTriggerStatusPending 表示触发信号已持久化,等待 worker 消费。
ActiveScheduleTriggerStatusPending = "pending"
// ActiveScheduleTriggerStatusProcessing 表示 worker 正在处理该触发信号。
ActiveScheduleTriggerStatusProcessing = "processing"
// ActiveScheduleTriggerStatusPreviewGenerated 表示已生成可查询的预览。
ActiveScheduleTriggerStatusPreviewGenerated = "preview_generated"
// ActiveScheduleTriggerStatusSkipped 表示本次触发被判定无需继续处理。
ActiveScheduleTriggerStatusSkipped = "skipped"
// ActiveScheduleTriggerStatusClosed 表示主动观测结论为关闭,不生成预览。
ActiveScheduleTriggerStatusClosed = "closed"
// ActiveScheduleTriggerStatusFailed 表示链路处理失败,可根据错误分类决定是否重试。
ActiveScheduleTriggerStatusFailed = "failed"
// ActiveScheduleTriggerStatusRejected 表示参数或归属校验失败,不进入 pipeline。
ActiveScheduleTriggerStatusRejected = "rejected"
)
const (
// ActiveSchedulePreviewStatusPending 表示预览正在组装,不应展示为可确认。
ActiveSchedulePreviewStatusPending = "pending"
// ActiveSchedulePreviewStatusReady 表示预览可查看、可确认。
ActiveSchedulePreviewStatusReady = "ready"
// ActiveSchedulePreviewStatusApplied 表示用户已确认并成功应用。
ActiveSchedulePreviewStatusApplied = "applied"
// ActiveSchedulePreviewStatusIgnored 表示用户明确忽略本次建议。
ActiveSchedulePreviewStatusIgnored = "ignored"
// ActiveSchedulePreviewStatusExpired 表示预览已过期,不再允许确认。
ActiveSchedulePreviewStatusExpired = "expired"
// ActiveSchedulePreviewStatusFailed 表示预览生成或回写失败。
ActiveSchedulePreviewStatusFailed = "failed"
)
const (
// ActiveScheduleApplyStatusNone 表示尚未发起确认应用。
ActiveScheduleApplyStatusNone = "none"
// ActiveScheduleApplyStatusApplying 表示确认请求正在事务应用中。
ActiveScheduleApplyStatusApplying = "applying"
// ActiveScheduleApplyStatusApplied 表示确认应用成功。
ActiveScheduleApplyStatusApplied = "applied"
// ActiveScheduleApplyStatusFailed 表示应用失败,正式日程不应产生半写状态。
ActiveScheduleApplyStatusFailed = "failed"
// ActiveScheduleApplyStatusRejected 表示请求因过期、幂等冲突等业务规则被拒绝。
ActiveScheduleApplyStatusRejected = "rejected"
// ActiveScheduleApplyStatusExpired 表示预览过期导致不可应用。
ActiveScheduleApplyStatusExpired = "expired"
)
const (
// NotificationRecordStatusPending 表示通知记录已落库,等待投递。
NotificationRecordStatusPending = "pending"
// NotificationRecordStatusSending 表示当前 worker 正在调用 provider。
NotificationRecordStatusSending = "sending"
// NotificationRecordStatusSent 表示 provider 明确返回成功。
NotificationRecordStatusSent = "sent"
// NotificationRecordStatusFailed 表示本次投递失败,但仍可重试。
NotificationRecordStatusFailed = "failed"
// NotificationRecordStatusDead 表示达到重试上限或不可恢复错误。
NotificationRecordStatusDead = "dead"
// NotificationRecordStatusSkipped 表示命中去重或配置关闭,本次不投递。
NotificationRecordStatusSkipped = "skipped"
)
const (
// ActiveScheduleTriggerTypeImportantUrgentTask 是重要且紧急任务到线触发。
ActiveScheduleTriggerTypeImportantUrgentTask = "important_urgent_task"
// ActiveScheduleTriggerTypeUnfinishedFeedback 是用户明确反馈已排任务未完成触发。
ActiveScheduleTriggerTypeUnfinishedFeedback = "unfinished_feedback"
// ActiveScheduleSourceWorkerDueJob 表示后台到期 job 扫描触发。
ActiveScheduleSourceWorkerDueJob = "worker_due_job"
// ActiveScheduleSourceAPITrigger 表示测试/开发 API 正式触发。
ActiveScheduleSourceAPITrigger = "api_trigger"
// ActiveScheduleSourceAPIDryRun 表示测试/开发 API dry-run不应发布正式事件。
ActiveScheduleSourceAPIDryRun = "api_dry_run"
// ActiveScheduleSourceUserFeedback 表示用户反馈入口触发。
ActiveScheduleSourceUserFeedback = "user_feedback"
// ActiveScheduleTargetTypeTaskPool 表示 target_id 指向 tasks.id。
ActiveScheduleTargetTypeTaskPool = "task_pool"
// ActiveScheduleTargetTypeScheduleEvent 表示 target_id 指向 schedule_events.id。
ActiveScheduleTargetTypeScheduleEvent = "schedule_event"
// ActiveScheduleTargetTypeTaskItem 表示 target_id 指向 task_items.id。
ActiveScheduleTargetTypeTaskItem = "task_item"
)
// ActiveScheduleJob 是主动调度 due job 表模型。
//
// 职责边界:
// 1. 负责记录 task 到达 urgency_threshold_at 后是否需要生成主动调度触发;
// 2. 不负责判断 task 当前是否仍重要且紧急,该判断由 worker 扫描时重新读取真实任务状态;
// 3. 不负责发布 outbox 事件,只保存扫描和排障所需状态。
type ActiveScheduleJob struct {
ID string `gorm:"column:id;type:varchar(64);primaryKey"`
UserID int `gorm:"column:user_id;not null;index:idx_active_jobs_user_status_trigger,priority:1;index:idx_active_jobs_task_status,priority:1"`
TaskID int `gorm:"column:task_id;not null;index:idx_active_jobs_task_status,priority:2;comment:对应 tasks.id"`
TriggerType string `gorm:"column:trigger_type;type:varchar(64);not null;default:'important_urgent_task';comment:触发类型"`
Status string `gorm:"column:status;type:varchar(32);not null;default:'pending';index:idx_active_jobs_user_status_trigger,priority:2;index:idx_active_jobs_task_status,priority:3;comment:pending/triggered/canceled/skipped/failed"`
TriggerAt time.Time `gorm:"column:trigger_at;not null;index:idx_active_jobs_user_status_trigger,priority:3;comment:到期触发时间"`
DedupeKey string `gorm:"column:dedupe_key;type:varchar(191);index:idx_active_jobs_dedupe;comment:触发去重窗口键"`
LastTriggerID *string `gorm:"column:last_trigger_id;type:varchar(64);index:idx_active_jobs_last_trigger;comment:最近一次生成的 trigger_id"`
LastErrorCode *string `gorm:"column:last_error_code;type:varchar(64);comment:最近一次扫描错误码"`
LastError *string `gorm:"column:last_error;type:text;comment:最近一次扫描错误详情"`
LastScannedAt *time.Time `gorm:"column:last_scanned_at;comment:最近一次被 worker 扫描时间"`
TraceID string `gorm:"column:trace_id;type:varchar(64);index:idx_active_jobs_trace_id"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index"`
}
func (ActiveScheduleJob) TableName() string { return "active_schedule_jobs" }
// ActiveScheduleTrigger 是主动调度统一触发信号表模型。
//
// 职责边界:
// 1. 负责持久化 worker/API/用户反馈归一后的触发事实;
// 2. 负责串联 trigger -> preview -> notification -> apply 的审计主线;
// 3. 不承载候选生成、LLM 选择或通知投递的业务实现。
type ActiveScheduleTrigger struct {
ID string `gorm:"column:id;type:varchar(64);primaryKey"`
UserID int `gorm:"column:user_id;not null;index:idx_active_triggers_user_created,priority:1"`
TriggerType string `gorm:"column:trigger_type;type:varchar(64);not null;index:idx_active_triggers_dedupe,priority:2"`
Source string `gorm:"column:source;type:varchar(64);not null;comment:worker_due_job/api_trigger/api_dry_run/user_feedback"`
TargetType string `gorm:"column:target_type;type:varchar(64);not null;index:idx_active_triggers_target,priority:1"`
TargetID int `gorm:"column:target_id;not null;index:idx_active_triggers_target,priority:2"`
FeedbackID string `gorm:"column:feedback_id;type:varchar(128);index:idx_active_triggers_feedback;comment:用户反馈来源ID可为空"`
JobID *string `gorm:"column:job_id;type:varchar(64);index:idx_active_triggers_job_id"`
IdempotencyKey string `gorm:"column:idempotency_key;type:varchar(191);index:idx_active_triggers_idempotency;comment:API/用户反馈幂等键"`
DedupeKey string `gorm:"column:dedupe_key;type:varchar(191);index:idx_active_triggers_dedupe,priority:1;comment:触发去重窗口键"`
Status string `gorm:"column:status;type:varchar(32);not null;default:'pending';index:idx_active_triggers_status_updated,priority:1"`
MockNow *time.Time `gorm:"column:mock_now;comment:测试触发模拟时间"`
IsMockTime bool `gorm:"column:is_mock_time;not null;default:false;comment:是否使用模拟时间"`
RequestedAt time.Time `gorm:"column:requested_at;not null;comment:触发请求时间"`
PayloadJSON *string `gorm:"column:payload_json;type:json;comment:触发来源补充信息"`
PreviewID *string `gorm:"column:preview_id;type:varchar(64);index:idx_active_triggers_preview_id"`
LastErrorCode *string `gorm:"column:last_error_code;type:varchar(64);comment:链路错误码"`
LastError *string `gorm:"column:last_error;type:text;comment:链路错误详情"`
ProcessedAt *time.Time `gorm:"column:processed_at;comment:worker 开始处理时间"`
CompletedAt *time.Time `gorm:"column:completed_at;comment:本触发进入终态时间"`
TraceID string `gorm:"column:trace_id;type:varchar(64);index:idx_active_triggers_trace_id"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;index:idx_active_triggers_user_created,priority:2"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;index:idx_active_triggers_status_updated,priority:2"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index"`
}
func (ActiveScheduleTrigger) TableName() string { return "active_schedule_triggers" }
// ActiveSchedulePreview 是主动调度可确认预览表模型。
//
// 职责边界:
// 1. 负责保存主动调度生成的候选、解释、before/after 摘要与过期时间;
// 2. 负责保存一次确认应用的轻量状态,不新增 apply request 表;
// 3. 不负责正式日程写入,正式写入仍由后续 apply/service port 完成。
type ActiveSchedulePreview struct {
ID string `gorm:"column:preview_id;type:varchar(64);primaryKey;uniqueIndex:uk_active_previews_apply_idempotency,priority:1"`
UserID int `gorm:"column:user_id;not null;index:idx_active_previews_user_created_at,priority:1"`
TriggerID string `gorm:"column:trigger_id;type:varchar(64);not null;index:idx_active_previews_trigger_id"`
TriggerType string `gorm:"column:trigger_type;type:varchar(64);not null"`
TargetType string `gorm:"column:target_type;type:varchar(64);not null"`
TargetID int `gorm:"column:target_id;not null"`
Status string `gorm:"column:status;type:varchar(32);not null;default:'pending';comment:pending/ready/applied/ignored/expired/failed"`
SelectedCandidateID string `gorm:"column:selected_candidate_id;type:varchar(64);comment:LLM 或后端 fallback 选中的候选ID"`
CandidateCount int `gorm:"column:candidate_count;not null;default:0"`
SelectedCandidateJSON *string `gorm:"column:selected_candidate_json;type:json"`
CandidatesJSON *string `gorm:"column:candidates_json;type:json"`
DecisionJSON *string `gorm:"column:decision_json;type:json"`
MetricsJSON *string `gorm:"column:metrics_json;type:json"`
IssuesJSON *string `gorm:"column:issues_json;type:json"`
ContextSummaryJSON *string `gorm:"column:context_summary_json;type:json"`
BeforeSummaryJSON *string `gorm:"column:before_summary_json;type:json"`
PreviewChangesJSON *string `gorm:"column:preview_changes_json;type:json"`
AfterSummaryJSON *string `gorm:"column:after_summary_json;type:json"`
RiskJSON *string `gorm:"column:risk_json;type:json"`
ExplanationText string `gorm:"column:explanation_text;type:text"`
NotificationSummary string `gorm:"column:notification_summary;type:text"`
BaseVersion string `gorm:"column:base_version;type:varchar(128);not null;comment:确认前重校验基准版本"`
ExpiresAt time.Time `gorm:"column:expires_at;not null;index:idx_active_previews_expires_at"`
GeneratedAt time.Time `gorm:"column:generated_at;not null"`
ApplyID *string `gorm:"column:apply_id;type:varchar(64);index:idx_active_previews_apply_id"`
ApplyStatus string `gorm:"column:apply_status;type:varchar(32);not null;default:'none';comment:none/applying/applied/failed/rejected/expired"`
ApplyCandidateID string `gorm:"column:apply_candidate_id;type:varchar(64)"`
ApplyIdempotencyKey string `gorm:"column:apply_idempotency_key;type:varchar(191);uniqueIndex:uk_active_previews_apply_idempotency,priority:2"`
ApplyRequestHash string `gorm:"column:apply_request_hash;type:varchar(128);comment:确认请求体摘要"`
AppliedChangesJSON *string `gorm:"column:applied_changes_json;type:json"`
AppliedEventIDsJSON *string `gorm:"column:applied_event_ids_json;type:json"`
ApplyError *string `gorm:"column:apply_error;type:text"`
AppliedAt *time.Time `gorm:"column:applied_at"`
TraceID string `gorm:"column:trace_id;type:varchar(64);index:idx_active_previews_trace_id"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;index:idx_active_previews_user_created_at,priority:2"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index"`
}
func (ActiveSchedulePreview) TableName() string { return "active_schedule_previews" }
// NotificationRecord 是通知投递记录表模型。
//
// 职责边界:
// 1. 负责记录飞书等通知渠道的幂等、状态流转和 provider 返回;
// 2. 不负责决定是否生成调度预览,也不负责 apply 状态;
// 3. 重试时复用同一条记录,避免短时间重复打扰用户。
type NotificationRecord struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement"`
Channel string `gorm:"column:channel;type:varchar(32);not null;uniqueIndex:uk_notification_dedupe,priority:1;comment:通知渠道"`
UserID int `gorm:"column:user_id;not null;index:idx_notification_user_created,priority:1"`
TriggerID string `gorm:"column:trigger_id;type:varchar(64);not null;index:idx_notification_trigger"`
PreviewID string `gorm:"column:preview_id;type:varchar(64);not null;index:idx_notification_preview"`
TriggerType string `gorm:"column:trigger_type;type:varchar(64);not null"`
TargetType string `gorm:"column:target_type;type:varchar(64);not null"`
TargetID int `gorm:"column:target_id;not null"`
DedupeKey string `gorm:"column:dedupe_key;type:varchar(191);not null;uniqueIndex:uk_notification_dedupe,priority:2"`
TargetURL string `gorm:"column:target_url;type:varchar(255);not null;comment:站内预览链接"`
SummaryText string `gorm:"column:summary_text;type:text"`
FallbackText string `gorm:"column:fallback_text;type:text"`
FallbackUsed bool `gorm:"column:fallback_used;not null;default:false"`
Status string `gorm:"column:status;type:varchar(32);not null;default:'pending';index:idx_notification_status_retry,priority:1;comment:pending/sending/sent/failed/dead/skipped"`
AttemptCount int `gorm:"column:attempt_count;not null;default:0"`
MaxAttempts int `gorm:"column:max_attempts;not null;default:5"`
NextRetryAt *time.Time `gorm:"column:next_retry_at;index:idx_notification_status_retry,priority:2"`
LastErrorCode *string `gorm:"column:last_error_code;type:varchar(64)"`
LastError *string `gorm:"column:last_error;type:text"`
ProviderMessageID *string `gorm:"column:provider_message_id;type:varchar(128)"`
ProviderRequestJSON *string `gorm:"column:provider_request_json;type:json"`
ProviderResponseJSON *string `gorm:"column:provider_response_json;type:json"`
SentAt *time.Time `gorm:"column:sent_at"`
TraceID string `gorm:"column:trace_id;type:varchar(64);index:idx_notification_trace_id"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;index:idx_notification_user_created,priority:2"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index"`
}
func (NotificationRecord) TableName() string { return "notification_records" }

View File

@@ -3,15 +3,35 @@ package model
import "time"
type ScheduleEvent struct {
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
UserID int `gorm:"column:user_id;index:idx_user_events;not null" json:"user_id"`
Name string `gorm:"column:name;type:varchar(255);not null;comment:课程或任务名称" json:"name"`
Location *string `gorm:"column:location;type:varchar(255);default:'';comment:地点 (教学楼/会议室)" json:"location"`
Type string `gorm:"column:type;type:enum('course','task');not null;comment:日程类型" json:"type"`
RelID *int `gorm:"column:rel_id;comment:关联原始数据ID (如教务系统的课程ID)" json:"rel_id"`
CanBeEmbedded bool `gorm:"column:can_be_embedded;not null;default:0;comment:是否允许在此时段嵌入其他任务" json:"can_be_embedded"`
StartTime time.Time `gorm:"column:start_time;type:time;comment:开始时间" json:"start_time"`
EndTime time.Time `gorm:"column:end_time;type:time;comment:结束时间" json:"end_time"`
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
UserID int `gorm:"column:user_id;index:idx_user_events;not null" json:"user_id"`
Name string `gorm:"column:name;type:varchar(255);not null;comment:课程或任务名称" json:"name"`
Location *string `gorm:"column:location;type:varchar(255);default:'';comment:地点 (教学楼/会议室)" json:"location"`
Type string `gorm:"column:type;type:enum('course','task');not null;comment:日程类型" json:"type"`
RelID *int `gorm:"column:rel_id;comment:关联原始数据ID (如教务系统的课程ID)" json:"rel_id"`
// TaskSourceType 标记 type=task 时 rel_id 指向哪个任务来源。
//
// 职责边界:
// 1. 只表达任务日程块的数据来源,不改变 Type 的 course/task 展示语义;
// 2. task_item 表示 rel_id 指向 task_items.idtask_pool 表示 rel_id 指向 tasks.id
// 3. 课程事件保持空值,由迁移回填历史 task 事件,避免影响现有课程逻辑。
TaskSourceType string `gorm:"column:task_source_type;type:varchar(32);not null;default:'';index:idx_schedule_event_task_source;comment:任务来源 task_item/task_pool" json:"task_source_type,omitempty"`
// MakeupForEventID 记录补做块来源事件,用于用户反馈未完成后的审计串联。
//
// 说明:
// 1. 只有主动调度生成补做块时写入;
// 2. 不负责校验目标事件是否仍存在,正式 apply 链路需要在事务内重校验;
// 3. 为空表示普通日程块或非补做块。
MakeupForEventID *int `gorm:"column:makeup_for_event_id;index:idx_schedule_event_makeup_for;comment:补做块对应的原 schedule_event.id" json:"makeup_for_event_id,omitempty"`
// ActivePreviewID 记录主动调度预览来源,方便从正式日程反查触发链路。
//
// 说明:
// 1. 该字段只做审计与排障,不作为正式日程主键;
// 2. preview 详情仍归 active_schedule_previews 表所有。
ActivePreviewID *string `gorm:"column:active_preview_id;type:varchar(64);index:idx_schedule_event_active_preview;comment:主动调度预览ID" json:"active_preview_id,omitempty"`
CanBeEmbedded bool `gorm:"column:can_be_embedded;not null;default:0;comment:是否允许在此时段嵌入其他任务" json:"can_be_embedded"`
StartTime time.Time `gorm:"column:start_time;type:time;comment:开始时间" json:"start_time"`
EndTime time.Time `gorm:"column:end_time;type:time;comment:结束时间" json:"end_time"`
}
type Schedule struct {

View File

@@ -39,6 +39,13 @@ type Task struct {
// 7.3 为空表示该任务不参与自动平移;
// 7.4 该字段参与"懒触发平移"复合索引。
UrgencyThresholdAt *time.Time `gorm:"column:urgency_threshold_at;index:idx_user_done_threshold_priority,priority:3"`
// 8. 任务预计占用节数。
//
// 说明:
// 8.1 主动调度只消费该字段,不在调度阶段重新推断任务复杂度;
// 8.2 MVP 约定有效范围为 1~4模型层仅提供默认值具体截断由主动调度上下文构造负责
// 8.3 默认 1 节,兼容历史任务与未显式填写的任务。
EstimatedSections int `gorm:"column:estimated_sections;not null;default:1"`
}
type UserAddTaskResponse struct {