Version: 0.9.77.dev.260505

后端:
1.阶段 6 CP4/CP5 目录收口与共享边界纯化
- 将 backend 根目录收口为 services、client、gateway、cmd、shared 五个一级目录
- 收拢 bootstrap、inits、infra/kafka、infra/outbox、conv、respond、pkg、middleware,移除根目录旧实现与空目录
- 将 utils 下沉到 services/userauth/internal/auth,将 logic 下沉到 services/schedule/core/planning
- 将迁移期 runtime 桥接实现统一收拢到 services/runtime/{conv,dao,eventsvc,model},删除 shared/legacy 与未再被 import 的旧 service 实现
- 将 gateway/shared/respond 收口为 HTTP/Gin 错误写回适配,shared/respond 仅保留共享错误语义与状态映射
- 将 HTTP IdempotencyMiddleware 与 RateLimitMiddleware 收口到 gateway/middleware
- 将 GormCachePlugin 下沉到 shared/infra/gormcache,将共享 RateLimiter 下沉到 shared/infra/ratelimit,将 agent token budget 下沉到 services/agent/shared
- 删除 InitEino 兼容壳,收缩 cmd/internal/coreinit 仅保留旧组合壳残留域初始化语义
- 更新微服务迁移计划与桌面 checklist,补齐 CP4/CP5 当前切流点、目录终态与验证结果
- 完成 go test ./...、git diff --check 与最终真实 smoke;health、register/login、task/create+get、schedule/today、task-class/list、memory/items、agent chat/meta/timeline/context-stats 全部 200,SSE 合并结果为 CP5_OK 且 [DONE] 只有 1 个
This commit is contained in:
Losita
2026-05-05 23:25:07 +08:00
parent 2a96f4c6f9
commit 3b6fca44a6
226 changed files with 731 additions and 3497 deletions

View File

@@ -0,0 +1,208 @@
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 (
// 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" }

View File

@@ -0,0 +1,82 @@
package model
import "time"
const (
// ActiveScheduleSessionStatusWaitingUserReply 表示当前会话正在等待用户补充信息,后端应拦截普通聊天。
ActiveScheduleSessionStatusWaitingUserReply = "waiting_user_reply"
// ActiveScheduleSessionStatusRerunning 表示用户已回复,主动调度图正在重跑,后端仍需拦截普通聊天。
ActiveScheduleSessionStatusRerunning = "rerunning"
// ActiveScheduleSessionStatusReadyPreview 表示已生成可展示预览,当前会话可以释放回普通聊天。
ActiveScheduleSessionStatusReadyPreview = "ready_preview"
// ActiveScheduleSessionStatusApplied 表示用户已确认应用,主动调度会话已经收口。
ActiveScheduleSessionStatusApplied = "applied"
// ActiveScheduleSessionStatusIgnored 表示用户明确忽略本次建议。
ActiveScheduleSessionStatusIgnored = "ignored"
// ActiveScheduleSessionStatusExpired 表示会话已过期,不再承担路由管辖权。
ActiveScheduleSessionStatusExpired = "expired"
// ActiveScheduleSessionStatusFailed 表示会话在绑定、重跑或写回过程中失败。
ActiveScheduleSessionStatusFailed = "failed"
)
// ActiveScheduleSession 是“主动调度会话路由桥”的持久化模型。
//
// 职责边界:
// 1. 只保存会话级路由权与轻量状态,不承载 preview 主表的完整业务内容;
// 2. conversation_id 允许在通知前为空,等站内会话绑定完成后再写入;
// 3. state_json 只存轻量状态,避免把重对象和历史消息继续塞进 session 表。
type ActiveScheduleSession struct {
SessionID string `gorm:"column:session_id;type:varchar(64);primaryKey"`
// 1. user_id + conversation_id 用于在聊天入口侧定位当前管辖中的主动调度会话。
// 2. conversation_id 允许为空,因此这里使用可空列,方便先建 session 再绑定会话。
UserID int `gorm:"column:user_id;not null;index:idx_active_schedule_sessions_user_conv,priority:1;index:idx_active_schedule_sessions_user_status_updated,priority:1"`
ConversationID *string `gorm:"column:conversation_id;type:varchar(128);index:idx_active_schedule_sessions_user_conv,priority:2;index:idx_active_schedule_sessions_conversation_status_updated,priority:1"`
// 3. trigger_id / current_preview_id 分别串起触发源与当前预览,方便后续审计和回放。
TriggerID string `gorm:"column:trigger_id;type:varchar(64);not null;index:idx_active_schedule_sessions_trigger_id"`
CurrentPreviewID *string `gorm:"column:current_preview_id;type:varchar(64);index:idx_active_schedule_sessions_preview_id"`
Status string `gorm:"column:status;type:varchar(32);not null;default:'waiting_user_reply';index:idx_active_schedule_sessions_user_status_updated,priority:2;index:idx_active_schedule_sessions_status_updated,priority:1;index:idx_active_schedule_sessions_conversation_status_updated,priority:2"`
StateJSON string `gorm:"column:state_json;type:json;not null"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;index:idx_active_schedule_sessions_user_status_updated,priority:3;index:idx_active_schedule_sessions_status_updated,priority:2;index:idx_active_schedule_sessions_conversation_status_updated,priority:3"`
}
// TableName 返回主动调度会话表名。
func (ActiveScheduleSession) TableName() string {
return "active_schedule_sessions"
}
// ActiveScheduleSessionState 是 session 表内 state_json 对应的轻量状态。
//
// 职责边界:
// 1. 这里只放“路由和补信息闭环”需要的少量字段;
// 2. 不承载完整 preview、正文历史或大块工具结果
// 3. 便于 cache / DAO 之间直接复用同一份 JSON 语义。
type ActiveScheduleSessionState struct {
PendingQuestion string `json:"pending_question,omitempty"`
MissingInfo []string `json:"missing_info,omitempty"`
LastCandidateID string `json:"last_candidate_id,omitempty"`
LastNotificationID string `json:"last_notification_id,omitempty"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
FailedReason string `json:"failed_reason,omitempty"`
}
// ActiveScheduleSessionSnapshot 是 service、DAO、cache 之间共享的会话快照 DTO。
//
// 职责边界:
// 1. 负责在三层之间传递强类型会话状态;
// 2. 不负责业务决策,不负责拦截判定;
// 3. DAO 再把它拆成数据库列和 state_jsoncache 则直接按 JSON 存取。
type ActiveScheduleSessionSnapshot struct {
SessionID string
UserID int
ConversationID string
TriggerID string
CurrentPreviewID string
Status string
State ActiveScheduleSessionState
CreatedAt time.Time
UpdatedAt time.Time
}

View File

@@ -0,0 +1,321 @@
package model
import (
"encoding/json"
"fmt"
"strings"
"time"
)
// AgentResumeType 表示本轮请求想恢复哪一类挂起交互。
type AgentResumeType string
const (
AgentResumeTypeAskUser AgentResumeType = "ask_user"
AgentResumeTypeConfirm AgentResumeType = "confirm"
AgentResumeTypeConnectionRecover AgentResumeType = "connection_recover"
)
// AgentResumeAction 表示用户这次恢复请求携带的动作类型。
type AgentResumeAction string
const (
AgentResumeActionReply AgentResumeAction = "reply"
AgentResumeActionApprove AgentResumeAction = "approve"
AgentResumeActionReject AgentResumeAction = "reject"
AgentResumeActionCancel AgentResumeAction = "cancel"
AgentResumeActionResume AgentResumeAction = "resume"
)
// AgentResumeRequest 是 extra.resume 的统一结构。
//
// 设计目的:
// 1. 继续复用现有聊天入口,不再额外新增一条“确认专用接口”;
// 2. 前端只提交“我要恢复哪次交互、这次动作是什么”,不直接改后端 state
// 3. 后端进入聊天主链路前,先读取这份结构,再决定走 confirm / ask_user / connection_recover 哪条恢复路径。
//
// 推荐前端请求形态:
//
// {
// "message": "",
// "extra": {
// "resume": {
// "interaction_id": "xxx",
// "type": "confirm",
// "action": "approve"
// }
// }
// }
//
// TODO(agent/api): 进入聊天主流程前,优先调用 req.ResumeRequest();若命中恢复协议,则不要把本轮请求按普通聊天处理。
type AgentResumeRequest struct {
InteractionID string `json:"interaction_id"`
Type AgentResumeType `json:"type,omitempty"`
Action AgentResumeAction `json:"action"`
}
type UserSendMessageRequest struct {
ConversationID string `json:"conversation_id,omitempty"`
Message string `json:"message" binding:"required"`
Model string `json:"model,omitempty"`
Thinking string `json:"thinking,omitempty"`
Extra map[string]any `json:"extra,omitempty"`
}
// ResumeRequest 从 extra.resume 中解析结构化恢复请求。
//
// 步骤说明:
// 1. 若 extra 或 extra.resume 不存在,则直接返回 nil表示本轮是普通聊天请求
// 2. 先把任意 map/struct 形态统一转成 JSON再反序列化到强类型结构避免入口层到处手写断言
// 3. 解析成功后先做 Normalize再做最小必要校验防止后续业务层拿到脏协议继续流转
// 4. 这里只负责协议解析与基本校验,不负责真正恢复状态,也不负责改 Redis/MySQL。
func (r *UserSendMessageRequest) ResumeRequest() (*AgentResumeRequest, error) {
if r == nil || len(r.Extra) == 0 {
return nil, nil
}
rawResume, ok := r.Extra["resume"]
if !ok || rawResume == nil {
return nil, nil
}
data, err := json.Marshal(rawResume)
if err != nil {
return nil, fmt.Errorf("序列化 extra.resume 失败: %w", err)
}
var resume AgentResumeRequest
if err := json.Unmarshal(data, &resume); err != nil {
return nil, fmt.Errorf("解析 extra.resume 失败: %w", err)
}
resume.Normalize()
if err := resume.Validate(); err != nil {
return nil, err
}
return &resume, nil
}
// Normalize 统一清洗恢复协议中的字符串字段。
func (r *AgentResumeRequest) Normalize() {
if r == nil {
return
}
r.InteractionID = strings.TrimSpace(r.InteractionID)
r.Type = AgentResumeType(strings.TrimSpace(string(r.Type)))
r.Action = AgentResumeAction(strings.TrimSpace(string(r.Action)))
}
// Validate 校验恢复协议的最小合法性。
//
// 职责边界:
// 1. 只校验“是否像一份合法的恢复协议”,不校验 interaction_id 是否真实存在;
// 2. confirm / ask_user / connection_recover 共用一条入口,但动作集合不同,所以这里做显式分流校验;
// 3. 对于 ask_user 回复,真正的回答正文仍建议优先放在顶层 message这里不强制要求额外 answer 字段。
func (r *AgentResumeRequest) Validate() error {
if r == nil {
return nil
}
if r.InteractionID == "" {
return fmt.Errorf("extra.resume.interaction_id 不能为空")
}
if r.Action == "" {
return fmt.Errorf("extra.resume.action 不能为空")
}
switch r.Type {
case "", AgentResumeTypeConfirm:
switch r.Action {
case AgentResumeActionApprove, AgentResumeActionReject, AgentResumeActionCancel:
return nil
default:
return fmt.Errorf("confirm 恢复动作非法: %s", r.Action)
}
case AgentResumeTypeAskUser:
switch r.Action {
case AgentResumeActionReply, AgentResumeActionCancel:
return nil
default:
return fmt.Errorf("ask_user 恢复动作非法: %s", r.Action)
}
case AgentResumeTypeConnectionRecover:
switch r.Action {
case AgentResumeActionResume, AgentResumeActionCancel:
return nil
default:
return fmt.Errorf("connection_recover 恢复动作非法: %s", r.Action)
}
default:
return fmt.Errorf("extra.resume.type 非法: %s", r.Type)
}
}
// IsConfirmResume 判断当前恢复请求是否属于 confirm 分支。
func (r *AgentResumeRequest) IsConfirmResume() bool {
if r == nil {
return false
}
return r.Type == "" || r.Type == AgentResumeTypeConfirm
}
// IsAskUserResume 判断当前恢复请求是否属于 ask_user 分支。
func (r *AgentResumeRequest) IsAskUserResume() bool {
if r == nil {
return false
}
return r.Type == AgentResumeTypeAskUser
}
// IsConnectionRecoverResume 判断当前恢复请求是否属于 connection_recover 分支。
func (r *AgentResumeRequest) IsConnectionRecoverResume() bool {
if r == nil {
return false
}
return r.Type == AgentResumeTypeConnectionRecover
}
type ChatHistoryPersistPayload struct {
UserID int `json:"user_id"`
ConversationID string `json:"conversation_id"`
Role string `json:"role"`
Message string `json:"message"`
ReasoningContent string `json:"reasoning_content,omitempty"`
ReasoningDurationSeconds int `json:"reasoning_duration_seconds,omitempty"`
TokensConsumed int `json:"tokens_consumed"`
}
type ChatTokenUsageAdjustPayload struct {
UserID int `json:"user_id"`
ConversationID string `json:"conversation_id"`
TokensDelta int `json:"tokens_delta"`
Reason string `json:"reason"`
TriggeredAt time.Time `json:"triggered_at"`
}
type GetConversationMetaResponse struct {
ConversationID string `json:"conversation_id"`
Title string `json:"title"`
HasTitle bool `json:"has_title"`
MessageCount int `json:"message_count"`
LastMessageAt *time.Time `json:"last_message_at,omitempty"`
Status string `json:"status"`
}
type GetConversationListItem struct {
ConversationID string `json:"conversation_id"`
Title string `json:"title"`
HasTitle bool `json:"has_title"`
MessageCount int `json:"message_count"`
LastMessageAt *time.Time `json:"last_message_at,omitempty"`
Status string `json:"status"`
CreatedAt *time.Time `json:"created_at,omitempty"`
}
type GetConversationListResponse struct {
List []GetConversationListItem `json:"list"`
Page int `json:"page"`
PageSize int `json:"page_size"`
Limit int `json:"limit"`
Total int64 `json:"total"`
HasMore bool `json:"has_more"`
}
type SchedulePlanPreviewCache struct {
UserID int `json:"user_id"`
ConversationID string `json:"conversation_id"`
TraceID string `json:"trace_id,omitempty"`
Summary string `json:"summary"`
CandidatePlans []UserWeekSchedule `json:"candidate_plans"`
TaskClassIDs []int `json:"task_class_ids,omitempty"`
HybridEntries []HybridScheduleEntry `json:"hybrid_entries,omitempty"`
AllocatedItems []TaskClassItem `json:"allocated_items,omitempty"`
GeneratedAt time.Time `json:"generated_at"`
}
type GetSchedulePlanPreviewResponse struct {
ConversationID string `json:"conversation_id"`
TraceID string `json:"trace_id,omitempty"`
Summary string `json:"summary"`
CandidatePlans []UserWeekSchedule `json:"candidate_plans"`
HybridEntries []HybridScheduleEntry `json:"hybrid_entries,omitempty"`
TaskClassIDs []int `json:"task_class_ids,omitempty"`
GeneratedAt time.Time `json:"generated_at"`
}
type SSEResponse struct {
Event string `json:"event"`
ID int `json:"id,omitempty"`
Retry int64 `json:"retry,omitempty"`
Data SSEMessageData `json:"data"`
}
type SSEMessageData struct {
Step int `json:"step,omitempty"`
Message string `json:"message,omitempty"`
}
type AgentChat struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement;comment:自增ID"`
ChatID string `gorm:"column:chat_id;type:varchar(36);not null;uniqueIndex:uk_chat_id;comment:会话UUID"`
UserID int `gorm:"column:user_id;not null;index:idx_user_last,priority:1;index:idx_user_status,priority:1;comment:所属用户ID"`
Title *string `gorm:"column:title;type:varchar(255);comment:会话标题"`
SystemPrompt *string `gorm:"column:system_prompt;type:text;comment:系统提示词"`
Model *string `gorm:"column:model;type:varchar(100);comment:模型标识"`
MessageCount int `gorm:"column:message_count;not null;default:0;comment:消息总数"`
TokensTotal int `gorm:"column:tokens_total;not null;default:0;comment:累计Token"`
LastHistoryEventID *string `gorm:"column:last_history_event_id;type:varchar(64);comment:最后一次聊天历史持久化事件ID"`
LastTokenAdjustEventID *string `gorm:"column:last_token_adjust_event_id;type:varchar(64);comment:最后一次会话token调整事件ID"`
LastMessageAt *time.Time `gorm:"column:last_message_at;comment:最后消息时间"`
Status string `gorm:"column:status;type:varchar(32);not null;default:active;index:idx_user_status,priority:2;comment:会话状态"`
CompactionSummary *string `gorm:"column:compaction_summary;type:text;comment:历史上下文压缩摘要"`
CompactionWatermark int `gorm:"column:compaction_watermark;not null;default:0;comment:压缩水位线最后被压缩的消息ID"`
ContextTokenStats *string `gorm:"column:context_token_stats;type:json;comment:上下文窗口实时token分布"`
CreatedAt *time.Time `gorm:"column:created_at;autoCreateTime"`
UpdatedAt *time.Time `gorm:"column:updated_at;autoUpdateTime"`
DeletedAt *time.Time `gorm:"column:deleted_at;comment:软删除时间"`
}
func (AgentChat) TableName() string { return "agent_chats" }
type ChatHistory struct {
ID int `gorm:"column:id;primaryKey;autoIncrement"`
ChatID string `gorm:"column:chat_id;type:varchar(36);not null;index:idx_user_chat,priority:2;index:idx_chat_id;comment:会话UUID"`
UserID int `gorm:"column:user_id;not null;index:idx_user_chat,priority:1"`
SourceEventID *string `gorm:"column:source_event_id;type:varchar(64);uniqueIndex:uk_chat_history_source_event;comment:来源事件ID"`
MessageContent *string `gorm:"column:message_content;type:text;comment:消息内容"`
ReasoningContent *string `gorm:"column:reasoning_content;type:text;comment:deep reasoning text"`
ReasoningDurationSeconds int `gorm:"column:reasoning_duration_seconds;not null;default:0;comment:deep reasoning duration seconds"`
RetryGroupID *string `gorm:"column:retry_group_id;type:varchar(64);index:idx_retry_group;comment:retry group id"`
RetryIndex *int `gorm:"column:retry_index;comment:retry page index"`
RetryFromUserMessageID *int `gorm:"column:retry_from_user_message_id;comment:source user message id"`
RetryFromAssistantMessageID *int `gorm:"column:retry_from_assistant_message_id;comment:source assistant message id"`
Role *string `gorm:"column:role;type:varchar(32);comment:消息角色"`
TokensConsumed int `gorm:"column:tokens_consumed;not null;default:0;comment:本轮消耗Token"`
CreatedAt *time.Time `gorm:"column:created_at;autoCreateTime"`
Chat AgentChat `gorm:"foreignKey:ChatID;references:ChatID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
}
func (ChatHistory) TableName() string { return "chat_histories" }
// SaveScheduleStatePlacedItem 描述一个已放置的 task_item 的绝对时间位置。
// 与 apply-batch 的 SingleTaskClassItem 格式统一,前端两个按钮共享同一数据格式。
type SaveScheduleStatePlacedItem struct {
TaskItemID int `json:"task_item_id" binding:"required"`
Week int `json:"week" binding:"required,min=1"`
DayOfWeek int `json:"day_of_week" binding:"required,min=1,max=7"`
StartSection int `json:"start_section" binding:"required,min=1"`
EndSection int `json:"end_section" binding:"required,min=1,gtefield=StartSection"`
EmbedCourseEventID int `json:"embed_course_event_id"`
}
// SaveScheduleStateRequest 前端暂存日程调整的请求体。
//
// 职责边界:
// 1. 只承载 conversation_id 和已放置的 task_item 列表(绝对时间格式);
// 2. 后端将绝对坐标转换为 ScheduleState 内部的相对 day_index
// 3. source=event 的课程不受影响,天然过滤。
type SaveScheduleStateRequest struct {
ConversationID string `json:"conversation_id" binding:"required"`
Items []SaveScheduleStatePlacedItem `json:"items" binding:"required,dive,required"`
}

View File

@@ -0,0 +1,85 @@
package model
import "time"
const (
// SchedulePlanStateVersionV1 表示当前 schedule_plan 快照结构版本。
//
// 设计说明:
// 1. 当后续快照字段发生不兼容变更时,版本号用于区分反序列化逻辑;
// 2. 当前版本先固定为 1后续升级时由写入端递增
// 3. 读取端可依据版本做兼容兜底,避免历史快照直接失效。
SchedulePlanStateVersionV1 = 1
)
// AgentScheduleState 是“单用户单会话”的智能排程状态快照持久化模型。
//
// 职责边界:
// 1. 负责保存“可恢复的排程中间状态与最终预览”,用于连续对话微调承接;
// 2. 负责承载结构化 JSON 快照(任务类、混合条目、候选方案等);
// 3. 不负责正式日程落库(正式落库仍走你现有的确认/应用链路);
// 4. 不负责消息总线投递(该快照要求强实时可读,直接写 MySQL
type AgentScheduleState struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement"`
// 1. 一对话一状态:同 user_id + conversation_id 永远只保留最新快照。
// 2. revision 在 upsert 更新时自增,便于排查“同会话被覆盖了几次”。
UserID int `gorm:"column:user_id;not null;uniqueIndex:uk_schedule_state_user_conv,priority:1;index:idx_schedule_state_user_updated,priority:1"`
ConversationID string `gorm:"column:conversation_id;type:varchar(36);not null;uniqueIndex:uk_schedule_state_user_conv,priority:2"`
Revision int `gorm:"column:revision;not null;default:1"`
StateVersion int `gorm:"column:state_version;not null;default:1"`
// 3. 为了避免跨层结构体强耦合,复杂切片统一序列化为 JSON 字符串存储。
TaskClassIDsJSON string `gorm:"column:task_class_ids;type:json;not null"`
ConstraintsJSON string `gorm:"column:constraints;type:json;not null"`
HybridEntriesJSON string `gorm:"column:hybrid_entries;type:json;not null"`
AllocatedItemsJSON string `gorm:"column:allocated_items;type:json;not null"`
CandidatePlansJSON string `gorm:"column:candidate_plans;type:json;not null"`
// 4. 这组字段用于恢复“本轮策略语义”,支持后续在会话内连续微调。
UserIntent string `gorm:"column:user_intent;type:text"`
Strategy string `gorm:"column:strategy;type:varchar(32);not null;default:steady"`
AdjustmentScope string `gorm:"column:adjustment_scope;type:varchar(16);not null;default:large"`
RestartRequested bool `gorm:"column:restart_requested;not null;default:false"`
// 5. 这组字段用于预览展示与链路排障。
FinalSummary string `gorm:"column:final_summary;type:text"`
Completed bool `gorm:"column:completed;not null;default:false"`
TraceID string `gorm:"column:trace_id;type:varchar(64);index:idx_schedule_state_trace_id"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;index:idx_schedule_state_user_updated,priority:2"`
}
func (AgentScheduleState) TableName() string {
return "agent_schedule_states"
}
// SchedulePlanStateSnapshot 是服务层与 DAO 之间的快照传输结构DTO
//
// 职责边界:
// 1. 负责在 service 与 dao 之间传递“强类型快照”;
// 2. 由 DAO 负责把该结构序列化/反序列化为数据库 JSON 字段;
// 3. 不承载运行期临时字段如并发信号、chan、上下文对象等
type SchedulePlanStateSnapshot struct {
UserID int
ConversationID string
Revision int
StateVersion int
TaskClassIDs []int
Constraints []string
HybridEntries []HybridScheduleEntry
AllocatedItems []TaskClassItem
CandidatePlans []UserWeekSchedule
UserIntent string
Strategy string
AdjustmentScope string
RestartRequested bool
FinalSummary string
Completed bool
TraceID string
UpdatedAt time.Time
}

View File

@@ -0,0 +1,24 @@
package model
import "time"
// AgentStateSnapshotRecord 是 agent 运行态快照的 MySQL 持久化模型。
//
// 设计说明:
// 1. 通过 outbox 异步写入Redis 快照到期后仍可从此表恢复;
// 2. 按 conversation_id 索引,支持按会话查询最近快照;
// 3. phase 字段便于按阶段过滤和清理;
// 4. 不做历史版本管理(覆盖写),同一会话只保留最新快照。
type AgentStateSnapshotRecord struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement"`
ConversationID string `gorm:"column:conversation_id;type:varchar(128);not null;uniqueIndex:idx_conversation_snapshot"`
UserID int `gorm:"column:user_id;not null;index:idx_user_snapshot"`
Phase string `gorm:"column:phase;type:varchar(32);not null"`
SnapshotJSON string `gorm:"column:snapshot_json;type:longtext;not null"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"`
}
func (AgentStateSnapshotRecord) TableName() string {
return "agent_state_snapshot_records"
}

View File

@@ -0,0 +1,147 @@
package model
import (
"strings"
"time"
)
// AgentTimelineKind 定义会话时间线事件类型。
//
// 说明:
// 1. 这些类型面向前端渲染,要求语义稳定,不随节点内部实现细节频繁变化;
// 2. 文本消息和卡片事件共用一条时间线,前端只按 seq 顺序渲染;
// 3. token 统计仍以 chat_histories / agent_chats 为准,时间线只负责展示顺序与结构承载。
const (
AgentTimelineKindUserText = "user_text"
AgentTimelineKindAssistantText = "assistant_text"
AgentTimelineKindToolCall = "tool_call"
AgentTimelineKindToolResult = "tool_result"
AgentTimelineKindConfirmRequest = "confirm_request"
AgentTimelineKindBusinessCard = "business_card"
AgentTimelineKindScheduleCompleted = "schedule_completed"
AgentTimelineKindThinkingSummary = "thinking_summary"
)
// AgentTimelineEvent 表示会话里“可展示事件”的统一持久化记录。
//
// 职责边界:
// 1. 只承载“顺序 + 展示信息”,不替代 chat_histories 的消息账本职责;
// 2. seq 是同一会话内的单调递增顺序号,用于刷新后重建展示顺序;
// 3. payload 只保存前端渲染需要的结构化信息,不存整个运行时快照。
type AgentTimelineEvent struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement"`
UserID int `gorm:"column:user_id;not null;uniqueIndex:uk_timeline_user_chat_seq,priority:1;index:idx_timeline_user_chat_created,priority:1;comment:所属用户ID"`
ChatID string `gorm:"column:chat_id;type:varchar(36);not null;uniqueIndex:uk_timeline_user_chat_seq,priority:2;index:idx_timeline_user_chat_created,priority:2;comment:会话UUID"`
Seq int64 `gorm:"column:seq;not null;uniqueIndex:uk_timeline_user_chat_seq,priority:3;comment:会话内顺序号"`
Kind string `gorm:"column:kind;type:varchar(64);not null;comment:事件类型"`
Role *string `gorm:"column:role;type:varchar(32);comment:消息角色"`
Content *string `gorm:"column:content;type:text;comment:正文内容"`
Payload *string `gorm:"column:payload;type:json;comment:结构化负载"`
TokensConsumed int `gorm:"column:tokens_consumed;not null;default:0;comment:该事件关联 token默认 0"`
CreatedAt *time.Time `gorm:"column:created_at;autoCreateTime;index:idx_timeline_user_chat_created,priority:3"`
}
func (AgentTimelineEvent) TableName() string { return "agent_timeline_events" }
// ChatTimelinePersistPayload 定义时间线单条事件落库输入。
//
// 职责边界:
// 1. 只表达一次“写入 agent_timeline_events”的最小字段集合
// 2. Content 面向纯文本类事件,结构化事件更多依赖 PayloadJSON
// 3. thinking_summary 事件要求 PayloadJSON 内只保留 detail_summary 与必要 metadata。
type ChatTimelinePersistPayload struct {
UserID int `json:"user_id"`
ConversationID string `json:"conversation_id"`
Seq int64 `json:"seq"`
Kind string `json:"kind"`
Role string `json:"role,omitempty"`
Content string `json:"content,omitempty"`
PayloadJSON string `json:"payload_json,omitempty"`
TokensConsumed int `json:"tokens_consumed"`
}
// Normalize 负责收敛时间线持久化载荷的基础口径。
//
// 职责边界:
// 1. 只做字符串 trim 和非负数兜底;
// 2. 不负责 thinking_summary 的业务裁剪;
// 3. 返回副本,避免调用方意外修改原对象。
func (p ChatTimelinePersistPayload) Normalize() ChatTimelinePersistPayload {
p.ConversationID = strings.TrimSpace(p.ConversationID)
p.Kind = strings.TrimSpace(p.Kind)
p.Role = strings.TrimSpace(p.Role)
p.Content = strings.TrimSpace(p.Content)
p.PayloadJSON = strings.TrimSpace(p.PayloadJSON)
if p.Seq < 0 {
p.Seq = 0
}
if p.TokensConsumed < 0 {
p.TokensConsumed = 0
}
return p
}
// HasValidIdentity 判断 payload 是否具备最小可持久化主键语义。
func (p ChatTimelinePersistPayload) HasValidIdentity() bool {
normalized := p.Normalize()
return normalized.UserID > 0 &&
normalized.ConversationID != "" &&
normalized.Seq > 0 &&
normalized.Kind != ""
}
// MatchesStoredEvent 判断 payload 与库中事件是否可视为“同一条业务事件”。
//
// 说明:
// 1. 主要用于 outbox 重放时识别“唯一键冲突但其实已经成功落库”的场景;
// 2. 只比较持久化字段,不比较 created_at / id 这类存储侧派生值;
// 3. 返回 true 时,上层可以把 seq 冲突视为幂等成功。
func (p ChatTimelinePersistPayload) MatchesStoredEvent(event AgentTimelineEvent) bool {
normalized := p.Normalize()
return event.UserID == normalized.UserID &&
strings.TrimSpace(event.ChatID) == normalized.ConversationID &&
event.Seq == normalized.Seq &&
strings.TrimSpace(event.Kind) == normalized.Kind &&
trimTimelinePointerString(event.Role) == normalized.Role &&
trimTimelinePointerString(event.Content) == normalized.Content &&
trimTimelinePointerString(event.Payload) == normalized.PayloadJSON &&
event.TokensConsumed == normalized.TokensConsumed
}
// IsTimelineSeqConflictError 判断 error 是否属于时间线 seq 唯一键冲突。
//
// 说明:
// 1. MySQL / PostgreSQL / SQLite 的重复键报错文案并不完全一致,这里用宽松文本匹配;
// 2. 该函数只用于“是否进入幂等/补 seq 分支”的判断,不承担精确错误分类职责;
// 3. 若未来统一抽数据库错误码适配层,应优先替换这里而不是继续复制判断逻辑。
func IsTimelineSeqConflictError(err error) bool {
if err == nil {
return false
}
lower := strings.ToLower(err.Error())
return strings.Contains(lower, "duplicate entry") ||
strings.Contains(lower, "duplicate key") ||
strings.Contains(lower, "unique constraint") ||
strings.Contains(lower, "unique violation") ||
strings.Contains(lower, "error 1062") ||
strings.Contains(lower, "uk_timeline_user_chat_seq")
}
// GetConversationTimelineItem 定义前端读取时间线接口的单条返回项。
type GetConversationTimelineItem struct {
ID int64 `json:"id,omitempty"`
Seq int64 `json:"seq"`
Kind string `json:"kind"`
Role string `json:"role,omitempty"`
Content string `json:"content,omitempty"`
Payload map[string]any `json:"payload,omitempty"`
TokensConsumed int `json:"tokens_consumed,omitempty"`
CreatedAt *time.Time `json:"created_at,omitempty"`
}
func trimTimelinePointerString(value *string) string {
if value == nil {
return ""
}
return strings.TrimSpace(*value)
}

View File

@@ -0,0 +1,19 @@
package model
type UserImportCoursesRequest struct {
Courses []UserCheckCourseRequest `json:"courses"`
}
type UserCheckCourseRequest struct {
CourseName string `json:"course_name"`
Location string `json:"location"`
IsAllowTasks bool `json:"is_allow_tasks"`
Arrangements []struct {
StartWeek int `json:"start_week"`
EndWeek int `json:"end_week"`
DayOfWeek int `json:"day_of_week"`
StartSection int `json:"start_section"`
EndSection int `json:"end_section"`
WeekType string `json:"week_type"`
} `json:"arrangements"`
}

View File

@@ -0,0 +1,38 @@
package model
type CourseImageParseDraftStatus string
const (
CourseImageParseDraftStatusSuccess CourseImageParseDraftStatus = "success"
CourseImageParseDraftStatusPartial CourseImageParseDraftStatus = "partial"
CourseImageParseDraftStatusReject CourseImageParseDraftStatus = "reject"
)
type CourseImageParseRow struct {
RowID string `json:"row_id"`
CourseName string `json:"course_name"`
Location string `json:"location"`
IsAllowTasks bool `json:"is_allow_tasks"`
StartWeek *int `json:"start_week"`
EndWeek *int `json:"end_week"`
DayOfWeek *int `json:"day_of_week"`
StartSection *int `json:"start_section"`
EndSection *int `json:"end_section"`
WeekType string `json:"week_type"`
Confidence float64 `json:"confidence"`
RawText string `json:"raw_text"`
RowWarnings []string `json:"row_warnings"`
}
type CourseImageParseResponse struct {
DraftStatus CourseImageParseDraftStatus `json:"draft_status"`
Message string `json:"message"`
Warnings []string `json:"warnings"`
Rows []CourseImageParseRow `json:"rows"`
}
type CourseImageParseRequest struct {
Filename string
MIMEType string
ImageBytes []byte
}

View File

@@ -0,0 +1,165 @@
package model
import "time"
const (
// MemoryItemStatusActive 表示记忆条目可参与检索与注入。
MemoryItemStatusActive = "active"
// MemoryItemStatusArchived 表示记忆条目被归档,不再默认参与注入。
MemoryItemStatusArchived = "archived"
// MemoryItemStatusDeleted 表示记忆条目已软删除。
MemoryItemStatusDeleted = "deleted"
)
const (
// MemoryJobTypeExtract 表示“候选事实抽取”任务。
MemoryJobTypeExtract = "extract"
// MemoryJobTypeEmbed 表示“向量化同步”任务Day1 仅预留)。
MemoryJobTypeEmbed = "embed"
// MemoryJobTypeReconcile 表示“冲突消解”任务Day1 仅预留)。
MemoryJobTypeReconcile = "reconcile"
)
const (
// MemoryJobStatusPending 表示任务待执行。
MemoryJobStatusPending = "pending"
// MemoryJobStatusProcessing 表示任务执行中。
MemoryJobStatusProcessing = "processing"
// MemoryJobStatusSuccess 表示任务执行成功(最终态)。
MemoryJobStatusSuccess = "success"
// MemoryJobStatusFailed 表示任务执行失败但可重试。
MemoryJobStatusFailed = "failed"
// MemoryJobStatusDead 表示任务不可恢复失败(最终态)。
MemoryJobStatusDead = "dead"
)
// MemoryItem 对应 memory_items 表,用于保存长期可注入记忆。
//
// 职责边界:
// 1. 该模型只定义存储结构,不承载抽取/决策业务逻辑;
// 2. Day1 先建表与基础字段Day2 再补读取注入链路;
// 3. 向量字段vector_status/vector_id仅做状态桥接不等于向量库真值。
type MemoryItem struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement"`
UserID int `gorm:"column:user_id;not null;index:idx_memory_items_user_status_type,priority:1;index:idx_memory_items_user_conv_status,priority:1;index:idx_memory_items_user_asst_run_status,priority:1;index:idx_memory_items_user_type_hash,priority:1;comment:用户ID"`
ConversationID *string `gorm:"column:conversation_id;type:varchar(64);index:idx_memory_items_user_conv_status,priority:2;comment:会话ID"`
AssistantID *string `gorm:"column:assistant_id;type:varchar(64);index:idx_memory_items_user_asst_run_status,priority:2;comment:助手ID"`
RunID *string `gorm:"column:run_id;type:varchar(64);index:idx_memory_items_user_asst_run_status,priority:3;comment:运行ID"`
MemoryType string `gorm:"column:memory_type;type:varchar(32);not null;index:idx_memory_items_user_status_type,priority:3;index:idx_memory_items_user_type_hash,priority:2;comment:preference/constraint/fact"`
Title string `gorm:"column:title;type:varchar(128);not null;comment:记忆标题"`
Content string `gorm:"column:content;type:text;not null;comment:记忆内容"`
NormalizedContent *string `gorm:"column:normalized_content;type:text;comment:标准化内容"`
ContentHash *string `gorm:"column:content_hash;type:varchar(64);index:idx_memory_items_user_type_hash,priority:3;comment:幂等去重哈希"`
Confidence float64 `gorm:"column:confidence;type:decimal(5,4);not null;default:0.6;comment:置信度"`
Importance float64 `gorm:"column:importance;type:decimal(5,4);not null;default:0.5;comment:重要度"`
SensitivityLevel int `gorm:"column:sensitivity_level;not null;default:0;comment:敏感级别"`
SourceMessageID *int64 `gorm:"column:source_message_id;index:idx_memory_items_source_message;comment:来源消息ID"`
SourceEventID *string `gorm:"column:source_event_id;type:varchar(64);comment:来源事件ID"`
IsExplicit bool `gorm:"column:is_explicit;not null;default:false;comment:是否显式记忆"`
Status string `gorm:"column:status;type:varchar(16);not null;default:active;index:idx_memory_items_user_status_type,priority:2;index:idx_memory_items_user_conv_status,priority:3;index:idx_memory_items_user_asst_run_status,priority:4;comment:active/archived/deleted"`
TTLAt *time.Time `gorm:"column:ttl_at;index:idx_memory_items_ttl;comment:过期时间"`
LastAccessAt *time.Time `gorm:"column:last_access_at;comment:最后访问时间"`
CreatedAt *time.Time `gorm:"column:created_at;autoCreateTime"`
UpdatedAt *time.Time `gorm:"column:updated_at;autoUpdateTime"`
VectorStatus string `gorm:"column:vector_status;type:varchar(16);not null;default:pending;comment:pending/synced/failed"`
VectorID *string `gorm:"column:vector_id;type:varchar(128);comment:向量库映射ID"`
}
func (MemoryItem) TableName() string {
return "memory_items"
}
// MemoryJob 对应 memory_jobs 表,用于承接异步任务。
//
// 职责边界:
// 1. 该表是“可重试状态机”,不是业务事实库;
// 2. payload_json 只存任务执行最小上下文;
// 3. status/retry_count/next_retry_at 组合定义可重试行为。
type MemoryJob struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement"`
UserID int `gorm:"column:user_id;not null;index:idx_memory_jobs_user_created,priority:1;comment:用户ID"`
ConversationID *string `gorm:"column:conversation_id;type:varchar(64);comment:会话ID"`
SourceMessageID *int64 `gorm:"column:source_message_id;comment:来源消息ID"`
SourceEventID *string `gorm:"column:source_event_id;type:varchar(64);index:idx_memory_jobs_source_event;comment:来源事件ID"`
JobType string `gorm:"column:job_type;type:varchar(32);not null;comment:extract/embed/reconcile"`
IdempotencyKey string `gorm:"column:idempotency_key;type:varchar(128);not null;uniqueIndex:uk_memory_jobs_idempotency;comment:幂等键"`
PayloadJSON string `gorm:"column:payload_json;type:longtext;not null;comment:任务载荷JSON"`
Status string `gorm:"column:status;type:varchar(16);not null;index:idx_memory_jobs_status_next,priority:1;comment:pending/processing/success/failed/dead"`
RetryCount int `gorm:"column:retry_count;not null;default:0;comment:已重试次数"`
MaxRetry int `gorm:"column:max_retry;not null;default:6;comment:最大重试次数"`
NextRetryAt *time.Time `gorm:"column:next_retry_at;index:idx_memory_jobs_status_next,priority:2;comment:下次重试时间"`
LastError *string `gorm:"column:last_error;type:varchar(2000);comment:最后错误"`
CreatedAt *time.Time `gorm:"column:created_at;autoCreateTime;index:idx_memory_jobs_user_created,priority:2"`
UpdatedAt *time.Time `gorm:"column:updated_at;autoUpdateTime"`
}
func (MemoryJob) TableName() string {
return "memory_jobs"
}
// MemoryAuditLog 对应 memory_audit_logs 表,用于记忆变更审计。
type MemoryAuditLog struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement"`
MemoryID int64 `gorm:"column:memory_id;not null;index:idx_memory_audit_memory_id;comment:记忆ID"`
UserID int `gorm:"column:user_id;not null;index:idx_memory_audit_user_id;comment:用户ID"`
Operation string `gorm:"column:operation;type:varchar(32);not null;comment:create/update/archive/delete/restore"`
OperatorType string `gorm:"column:operator_type;type:varchar(16);not null;comment:system/user"`
Reason string `gorm:"column:reason;type:varchar(255);not null;default:'';comment:操作原因"`
BeforeJSON *string `gorm:"column:before_json;type:longtext;comment:变更前快照"`
AfterJSON *string `gorm:"column:after_json;type:longtext;comment:变更后快照"`
CreatedAt *time.Time `gorm:"column:created_at;autoCreateTime"`
}
func (MemoryAuditLog) TableName() string {
return "memory_audit_logs"
}
// MemoryUserSetting 对应 memory_user_settings 表,用于用户记忆开关控制。
type MemoryUserSetting struct {
UserID int `gorm:"column:user_id;primaryKey;comment:用户ID"`
MemoryEnabled bool `gorm:"column:memory_enabled;not null;default:true;comment:总开关"`
ImplicitMemoryEnabled bool `gorm:"column:implicit_memory_enabled;not null;default:true;comment:隐式记忆开关"`
SensitiveMemoryEnabled bool `gorm:"column:sensitive_memory_enabled;not null;default:false;comment:敏感记忆开关"`
UpdatedAt *time.Time `gorm:"column:updated_at;autoUpdateTime"`
}
func (MemoryUserSetting) TableName() string {
return "memory_user_settings"
}
// MemoryExtractRequestedPayload 是 memory.extract.requested(v1) 事件载荷。
//
// 说明:
// 1. Day1 先承载最小可执行字段;
// 2. assistant_id/run_id/source_message_id/trace_id 允许为空,后续链路补齐;
// 3. idempotency_key 必填,用于 memory_jobs 去重与无副作用消费。
type MemoryExtractRequestedPayload struct {
UserID int `json:"user_id"`
ConversationID string `json:"conversation_id"`
AssistantID string `json:"assistant_id,omitempty"`
RunID string `json:"run_id,omitempty"`
SourceMessageID int64 `json:"source_message_id,omitempty"`
SourceRole string `json:"source_role"`
SourceText string `json:"source_text"`
OccurredAt time.Time `json:"occurred_at"`
TraceID string `json:"trace_id,omitempty"`
IdempotencyKey string `json:"idempotency_key"`
}

View File

@@ -0,0 +1,105 @@
package model
import "time"
// MemoryGetItemRequest 描述“查看我的某条记忆”所需的最小参数。
type MemoryGetItemRequest struct {
UserID int
MemoryID int64
}
// MemoryCreateItemRequest 描述“手动新增一条记忆”的输入。
type MemoryCreateItemRequest struct {
UserID int `json:"-"`
ConversationID string `json:"conversation_id,omitempty"`
AssistantID string `json:"assistant_id,omitempty"`
RunID string `json:"run_id,omitempty"`
MemoryType string `json:"memory_type"`
Title string `json:"title"`
Content string `json:"content"`
Confidence *float64 `json:"confidence,omitempty"`
Importance *float64 `json:"importance,omitempty"`
SensitivityLevel *int `json:"sensitivity_level,omitempty"`
IsExplicit *bool `json:"is_explicit,omitempty"`
TTLAt *time.Time `json:"ttl_at,omitempty"`
Reason string `json:"reason,omitempty"`
OperatorType string `json:"-"`
}
// MemoryUpdateItemRequest 描述“手动修改一条记忆”的 Patch 输入。
//
// 说明:
// 1. 使用指针区分“未传字段”和“显式传零值”;
// 2. ClearTTL 用于表达“显式清空 ttl_at”
// 3. 当前仍只允许修改内容侧字段,不开放跨用户、跨归属字段改写。
type MemoryUpdateItemRequest struct {
UserID int `json:"-"`
MemoryID int64 `json:"-"`
MemoryType *string `json:"memory_type,omitempty"`
Title *string `json:"title,omitempty"`
Content *string `json:"content,omitempty"`
Confidence *float64 `json:"confidence,omitempty"`
Importance *float64 `json:"importance,omitempty"`
SensitivityLevel *int `json:"sensitivity_level,omitempty"`
IsExplicit *bool `json:"is_explicit,omitempty"`
TTLAt *time.Time `json:"ttl_at,omitempty"`
ClearTTL bool `json:"clear_ttl,omitempty"`
Reason string `json:"reason,omitempty"`
OperatorType string `json:"-"`
}
// MemoryDeleteItemRequest 描述“删除我的一条记忆”的输入。
type MemoryDeleteItemRequest struct {
UserID int
MemoryID int64
Reason string
OperatorType string
}
// MemoryRestoreItemRequest 描述“恢复我的一条记忆”的输入。
type MemoryRestoreItemRequest struct {
UserID int
MemoryID int64
Reason string
OperatorType string
}
// MemoryDedupCleanupRequest 描述离线去重治理任务的执行参数。
type MemoryDedupCleanupRequest struct {
UserID int
Limit int
DryRun bool
Reason string
OperatorType string
}
// MemoryDedupCleanupResult 描述一次离线去重治理的汇总结果。
type MemoryDedupCleanupResult struct {
ScannedGroupCount int `json:"scanned_group_count"`
DedupedGroupCount int `json:"deduped_group_count"`
KeptCount int `json:"kept_count"`
ArchivedCount int `json:"archived_count"`
ArchivedIDs []int64 `json:"archived_ids,omitempty"`
DryRun bool `json:"dry_run"`
}
// MemoryItemView 是前端可见的记忆条目视图。
type MemoryItemView struct {
ID int64 `json:"id"`
UserID int `json:"user_id"`
ConversationID string `json:"conversation_id,omitempty"`
AssistantID string `json:"assistant_id,omitempty"`
RunID string `json:"run_id,omitempty"`
MemoryType string `json:"memory_type"`
Title string `json:"title"`
Content string `json:"content"`
ContentHash string `json:"content_hash,omitempty"`
Confidence float64 `json:"confidence"`
Importance float64 `json:"importance"`
SensitivityLevel int `json:"sensitivity_level"`
IsExplicit bool `json:"is_explicit"`
Status string `json:"status"`
TTLAt *time.Time `json:"ttl_at,omitempty"`
CreatedAt *time.Time `json:"created_at,omitempty"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
}

View File

@@ -0,0 +1,48 @@
package model
import "time"
const (
// OutboxStatusPending 表示消息已写入 outbox等待投递或重试窗口到达。
OutboxStatusPending = "pending"
// OutboxStatusPublished 表示消息已成功写入 Kafka但业务消费尚未完成。
OutboxStatusPublished = "published"
// OutboxStatusConsumed 表示消息对应业务处理已成功完成(最终态)。
OutboxStatusConsumed = "consumed"
// OutboxStatusDead 表示达到重试上限或出现不可恢复错误(最终态)。
OutboxStatusDead = "dead"
)
// AgentOutboxMessage 是 outbox 状态机表模型。
//
// 关键说明:
// 1. EventType 映射到数据库 `biz_type` 列(为兼容历史表结构,不改 DDL
// 2. Payload 保存统一事件外壳 JSON
// 3. Status/RetryCount/NextRetryAt 组成重试状态机。
type AgentOutboxMessage struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement"`
EventType string `gorm:"column:biz_type;type:varchar(64);not null;index:idx_outbox_status_next,priority:3;comment:事件类型"`
ServiceName string `gorm:"column:service_name;type:varchar(64);not null;default:'';index:idx_outbox_service_name,priority:1;comment:所属服务"`
Topic string `gorm:"column:topic;type:varchar(128);not null;comment:Kafka Topic"`
MessageKey string `gorm:"column:message_key;type:varchar(128);not null;comment:Kafka 消息键"`
Payload string `gorm:"column:payload;type:longtext;not null;comment:业务载荷(JSON)"`
Status string `gorm:"column:status;type:varchar(32);not null;index:idx_outbox_status_next,priority:1;comment:pending/published/consumed/dead"`
RetryCount int `gorm:"column:retry_count;not null;default:0;comment:已重试次数"`
MaxRetry int `gorm:"column:max_retry;not null;default:20;comment:最大重试次数"`
NextRetryAt *time.Time `gorm:"column:next_retry_at;index:idx_outbox_status_next,priority:2;comment:下次重试时间"`
LastError *string `gorm:"column:last_error;type:text;comment:最后一次错误"`
PublishedAt *time.Time `gorm:"column:published_at;comment:投递到 Kafka 时间"`
ConsumedAt *time.Time `gorm:"column:consumed_at;comment:消费完成时间"`
CreatedAt *time.Time `gorm:"column:created_at;autoCreateTime"`
UpdatedAt *time.Time `gorm:"column:updated_at;autoUpdateTime"`
}
func (AgentOutboxMessage) TableName() string {
// 1. 这里保留历史兼容默认表名,避免非 outbox 基础设施调用直接失效。
// 2. 服务级多表路由由 backend/infra/outbox 显式通过 db.Table(...) 控制。
// 3. 这样既能兼容旧代码,也不会把共享单表当成终态。
return "agent_outbox_messages"
}

View File

@@ -0,0 +1,194 @@
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"`
// 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 {
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
EventID int `gorm:"column:event_id;index:idx_event_id;not null;comment:关联元数据ID" json:"event_id"`
UserID int `gorm:"column:user_id;uniqueIndex:idx_user_slot_atomic,priority:1;not null;comment:冗余UID方便直接查询" json:"user_id"`
Week int `gorm:"column:week;uniqueIndex:idx_user_slot_atomic,priority:2;not null;comment:周次 (1-25)" json:"week"`
DayOfWeek int `gorm:"column:day_of_week;uniqueIndex:idx_user_slot_atomic,priority:3;not null;comment:星期 (1-7)" json:"day_of_week"`
Section int `gorm:"column:section;uniqueIndex:idx_user_slot_atomic,priority:4;not null;comment:原子化节次 (1-12)" json:"section"`
EmbeddedTaskID *int `gorm:"column:embedded_task_id;comment:若为水课嵌入记录具体的任务项ID" json:"embedded_task_id"`
Status string `gorm:"column:status;type:enum('normal','interrupted');default:'normal';comment:状态: 正常/因故中断" json:"status"`
// 💡 必须加上这一行,告诉 GORM 如何关联元数据
Event *ScheduleEvent `gorm:"foreignKey:EventID" json:"event"`
EmbeddedTask *TaskClassItem `gorm:"foreignKey:EmbeddedTaskID" json:"embedded_task"`
}
type ScheduleConflictDetail struct {
EventID int `json:"event_id"`
Name string `json:"name"`
Location string `json:"location"`
DayOfWeek int `json:"day_of_week"`
Week int `json:"week"`
Sections []int `json:"sections"`
StartSection int `json:"start_section"`
EndSection int `json:"end_section"`
Type string `json:"type"`
EmbeddedTasks []ScheduleEmbeddedTask `json:"embedded_tasks"`
}
type ScheduleEmbeddedTask struct {
Section int `json:"section"`
TaskID int `json:"task_id"`
}
type UserTodaySchedule struct {
DayOfWeek int `json:"day_of_week"`
Week int `json:"week"`
Events []EventBrief `json:"events"`
}
type EventBrief struct {
ID int `json:"id"` // 这个 ID 是 ScheduleEvent 的 ID不是 Schedule 的 ID
Order int `json:"order"` // order 用于区分它们的显示顺序
Name string `json:"name"`
StartTime string `json:"start_time"`
EndTime string `json:"end_time"`
Location string `json:"location"`
Type string `json:"type"`
Span int `json:"span"` // 跨越的节数,给前端用来渲染宽度/高度
EmbeddedTaskInfo TaskBrief `json:"embedded_task_info,omitempty"`
}
type TaskBrief struct {
ID int `json:"id"` // 这个 ID 是 ScheduleEvent 的 ID不是 Schedule 的 ID
Name string `json:"name"`
/*StartTime string `json:"start_time"`
EndTime string `json:"end_time"`*/
Type string `json:"type"`
}
type UserWeekSchedule struct {
Week int `json:"week"`
Events []WeeklyEventBrief `json:"events"`
}
type WeeklyEventBrief struct {
ID int `json:"id"` // 这个 ID 是 ScheduleEvent 的 ID不是 Schedule 的 ID
Order int `json:"order"` // order 用于区分它们在一天中的显示顺序
DayOfWeek int `json:"day_of_week"`
Name string `json:"name"`
StartTime string `json:"start_time"`
EndTime string `json:"end_time"`
Location string `json:"location"`
Type string `json:"type"`
Span int `json:"span"` // 跨越的节数,给前端用来渲染宽度/高度
Status string `json:"status"`
EmbeddedTaskInfo TaskBrief `json:"embedded_task_info,omitempty"`
}
type UserDeleteScheduleEvent struct {
ID int `json:"id"` // 这个 ID 是 ScheduleEvent 的 ID不是 Schedule 的 ID
DeleteCourse bool `json:"delete_course"`
DeleteEmbeddedTask bool `json:"delete_embedded_task"`
}
// UserSmartPlanningMultiRequest 是“多任务类智能粗排”接口的请求体。
//
// 设计说明:
// 1. TaskClassIDs 至少包含 1 个任务类 ID
// 2. 实际业务建议传入 >=2 个,用于多任务类混排;
// 3. 服务层会做去重与合法值过滤,接口层只做基础绑定校验。
type UserSmartPlanningMultiRequest struct {
TaskClassIDs []int `json:"task_class_ids" binding:"required,min=1,dive,min=1"`
}
type UserRecentCompletedScheduleResponse struct {
Events []RecentCompletedEventBrief `json:"events"`
}
type RecentCompletedEventBrief struct {
ID int `json:"id"` //如果是嵌入的任务事件这个ID是TaskClassItem的ID如果是课程事件这个ID是ScheduleEvent的ID
Name string `json:"name"`
Type string `json:"type"`
CompletedTime string `json:"completed_time"`
}
type OngoingSchedule struct {
ID int `json:"id"` // 这个 ID 是 ScheduleEvent 的 ID不是 Schedule 的 ID
Name string `json:"name"`
Location string `json:"location"`
Type string `json:"type"`
TimeStatus string `json:"time_status"` // "upcoming", "ongoing"
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
}
// HybridScheduleEntry 表示"混合日程"中的一个时间块。
//
// 设计目标:
// 将既有日程(课程/已落库任务)与粗排建议的任务统一到同一结构中,
// 供 ReAct 精排引擎在内存中操作。
//
// Status 语义:
// - "existing"已确定的日程LLM 不可移动;
// - "suggested"粗排建议的任务LLM 可通过 Tool 调整时间。
type HybridScheduleEntry struct {
Week int `json:"week"`
DayOfWeek int `json:"day_of_week"`
SectionFrom int `json:"section_from"`
SectionTo int `json:"section_to"`
Name string `json:"name"`
Type string `json:"type"` // "course" | "task"
Status string `json:"status"` // "existing" | "suggested"
TaskItemID int `json:"task_item_id,omitempty"` // 仅 suggested 的 task 有值
TaskClassID int `json:"task_class_id,omitempty"` // 仅 suggested 的 task 有值,对应 TaskClass.ID
EventID int `json:"event_id,omitempty"` // 仅 existing 有值
// CanBeEmbedded 表示该条 existing 课程块是否允许嵌入任务。
// 仅课程条目有意义task 条目默认 false。
CanBeEmbedded bool `json:"can_be_embedded,omitempty"`
// BlockForSuggested 表示该条目是否应当阻塞 suggested 任务占位。
//
// 语义说明:
// 1. suggested 条目默认 true任务之间不能重叠
// 2. existing 课程若是“可嵌入且当前格子未被嵌入任务占用”,则为 false
// 3. existing 课程若不可嵌入,或该格子已有嵌入任务,则为 true。
//
// 该字段用于工具层冲突判断,避免把“可嵌入课位”误判为硬冲突。
BlockForSuggested bool `json:"block_for_suggested,omitempty"`
// ContextTag 是任务认知类型标签,仅在 suggested 任务中使用。
// 该标签用于日内优化时的“认知负荷分配”,例如:
// 1. High-Logic数学、编程、逻辑推理
// 2. Memory记忆/背诵类;
// 3. Review复习/回顾类;
// 4. General通用任务。
ContextTag string `json:"context_tag,omitempty"`
}
func (ScheduleEvent) TableName() string { return "schedule_events" }
func (Schedule) TableName() string { return "schedules" }

View File

@@ -0,0 +1,203 @@
package model
import (
"database/sql/driver"
"encoding/json"
"fmt"
"time"
)
// TaskClass 用于和数据库中的 task_classes 表进行映射
type TaskClass struct {
//section 1
ID int `gorm:"column:id;primaryKey;autoIncrement"`
UserID *int `gorm:"column:user_id;index:idx_task_classes_user_id"`
//section 2
Name *string `gorm:"column:name;size:255"`
Mode *string `gorm:"column:mode;type:enum('auto','manual')"`
StartDate *time.Time `gorm:"column:start_date"`
EndDate *time.Time `gorm:"column:end_date"`
SubjectType *string `gorm:"column:subject_type;size:32;comment:学科类型 quantitative|memory|reading|mixed"`
DifficultyLevel *string `gorm:"column:difficulty_level;size:16;comment:难度等级 low|medium|high"`
CognitiveIntensity *string `gorm:"column:cognitive_intensity;size:16;comment:认知强度 low|medium|high"`
//section 3
TotalSlots *int `gorm:"column:total_slots;comment:分配的总节数"`
AllowFillerCourse *bool `gorm:"column:allow_filler_course;default:true"`
Strategy *string `gorm:"column:strategy;type:enum('steady','rapid')"`
ExcludedSlots IntSlice `gorm:"column:excluded_slots;type:json;comment:不想要的时段切片"`
ExcludedDaysOfWeek IntSlice `gorm:"column:excluded_days_of_week;type:json;comment:不想要的星期几切片(1-7)"`
Items []TaskClassItem `gorm:"foreignKey:CategoryID;references:ID"` // 一对多关联:一个 TaskClass 有多个 TaskClassItem
}
// IntSlice 用于把 []int 以 JSON 形式存入/读出数据库 json 字段
type IntSlice []int
func (s IntSlice) Value() (driver.Value, error) {
// nil -> NULL空切片 -> "[]"
if s == nil {
return nil, nil
}
return json.Marshal([]int(s))
}
func (s *IntSlice) Scan(value any) error {
if value == nil {
*s = nil
return nil
}
var data []byte
switch v := value.(type) {
case []byte:
data = v
case string:
data = []byte(v)
default:
return fmt.Errorf("IntSlice: 不支持的扫描类型: %T", value)
}
var out []int
if err := json.Unmarshal(data, &out); err != nil {
return err
}
*s = IntSlice(out)
return nil
}
// TaskClassItem 用于和数据库中的 task_items 表进行映射
type TaskClassItem struct {
//section 1
ID int `gorm:"column:id;primaryKey;autoIncrement"`
CategoryID *int `gorm:"column:category_id"` //对应 TaskClass 的 ID
//section 2
Order *int `gorm:"column:order"`
Content *string `gorm:"column:content;type:text"`
EmbeddedTime *TargetTime `gorm:"column:embedded_time;type:json;comment:目标时间{date,section_from,section_to}"`
Status *int `gorm:"column:status;comment:1:未安排, 2:已应用"`
}
// UserAddTaskClassRequest 用于处理用户添加任务类别的请求
type UserAddTaskClassRequest struct {
Name string `json:"name" binding:"required"`
StartDate string `json:"start_date" binding:"required"` // YYYY-MM-DD
EndDate string `json:"end_date" binding:"required"` // YYYY-MM-DD
Mode string `json:"mode" binding:"required,oneof=auto manual"`
SubjectType string `json:"subject_type,omitempty"`
DifficultyLevel string `json:"difficulty_level,omitempty"`
CognitiveIntensity string `json:"cognitive_intensity,omitempty"`
Config UserAddTaskClassConfig `json:"config" binding:"required"`
Items []UserAddTaskClassItemRequest `json:"items" binding:"required"`
}
// UserAddTaskClassConfig 用于处理用户添加任务类别时的配置部分
type UserAddTaskClassConfig struct {
TotalSlots int `json:"total_slots" binding:"required,min=1"`
AllowFillerCourse bool `json:"allow_filler_course"`
Strategy string `json:"strategy" binding:"required,oneof=steady rapid"`
ExcludedSlots []int `json:"excluded_slots"`
ExcludedDaysOfWeek []int `json:"excluded_days_of_week"`
}
// UserAddTaskClassItemRequest 用于处理用户添加任务类别时的任务块部分
type UserAddTaskClassItemRequest struct {
ID int `json:"id,omitempty"` // 任务块的数据库主键 ID查询时返回创建时可省略
Order int `json:"order" binding:"required,min=1"`
Content string `json:"content" binding:"required"`
EmbeddedTime *TargetTime `json:"embedded_time"` // 例: 2025-12-22 1-2节; nil 表示未安排
}
// TargetTime 表示任务块的目标时间
type TargetTime struct {
Week int `json:"week"` // 周次
DayOfWeek int `json:"day_of_week"` // 星期几
SectionFrom int `json:"section_from"` // 起始节次
SectionTo int `json:"section_to"` // 结束节次
}
// UserGetTaskClassesResponse 用于返回用户的任务类列表,展示简要信息
type UserGetTaskClassesResponse struct {
TaskClasses []TaskClassSummary `json:"task_classes"`
}
// TaskClassSummary 提供任务类别的简要信息
type TaskClassSummary struct {
ID int `json:"id"`
Name string `json:"name"`
Mode string `json:"mode"`
Strategy string `json:"strategy"`
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
TotalSlots int `json:"total_slots"`
SubjectType string `json:"subject_type,omitempty"`
DifficultyLevel string `json:"difficulty_level,omitempty"`
CognitiveIntensity string `json:"cognitive_intensity,omitempty"`
}
type UserInsertTaskClassItemToScheduleRequest struct {
Week int `json:"week" binding:"required,min=1"`
DayOfWeek int `json:"day_of_week" binding:"required,min=1,max=7"`
StartSection int `json:"start_section" binding:"required,min=1"`
EndSection int `json:"end_section" binding:"required,min=1,gtefield=StartSection"`
EmbedCourseEventID int `json:"embed_course_event_id"` // 可选,嵌入的课程日程事件 ID
}
type UserInsertTaskClassItemToScheduleRequestBatch struct {
TaskClassID int `json:"task_class_id" binding:"required"`
Items []SingleTaskClassItem `json:"items" binding:"required,dive,required"`
}
type SingleTaskClassItem struct {
TaskItemID int `json:"task_item_id" binding:"required"`
Week int `json:"week" binding:"required,min=1"`
DayOfWeek int `json:"day_of_week" binding:"required,min=1,max=7"`
StartSection int `json:"start_section" binding:"required,min=1"`
EndSection int `json:"end_section" binding:"required,min=1,gtefield=StartSection"`
EmbedCourseEventID int `json:"embed_course_event_id"` // 可选,嵌入的课程日程事件 ID
}
// Value 实现 driver.Valuer 接口,负责将 TargetTime 转换为数据库存储的格式
func (t *TargetTime) Value() (driver.Value, error) {
if t == nil {
return nil, nil
}
// 💡 关键:调用 json.Marshal 将结构体转为 []byte
// 这样 GORM 就能把这一串 JSON 存进数据库的 text/json 字段了
return json.Marshal(t)
}
// Scan 实现 sql.Scanner 接口,负责将数据库中的值转换为 TargetTime 结构体
func (t *TargetTime) Scan(value any) error {
if value == nil {
// 如果数据库是 NULL保持指针对应的对象为零值即可
// 或者在业务层判断 nil
return nil
}
var data []byte
switch v := value.(type) {
case []byte:
data = v
case string:
data = []byte(v)
default:
return fmt.Errorf("TargetTime: 不支持的扫描类型: %T", value)
}
return json.Unmarshal(data, t)
}
// TableName 指定 TaskClass 对应的数据库表名
func (TaskClass) TableName() string {
return "task_classes"
}
// TableName 指定 TaskClassItem 对应的数据库表名
func (TaskClassItem) TableName() string {
return "task_items"
}
// 任务块状态常量
const (
TaskItemStatusUnscheduled = 1 // 未安排
TaskItemStatusApplied = 2 // 已应用
)

View File

@@ -0,0 +1,199 @@
package model
import "time"
// Task 是任务表的领域模型。
//
// 职责边界:
// 1. 负责映射 tasks 表字段;
// 2. 不负责接口入参校验和业务规则判断;
// 3. 不负责"自动平移"执行(自动平移由 Service + Outbox 事件链路负责)。
type Task struct {
// 1. 主键。
ID int `gorm:"primaryKey;autoIncrement"`
// 2. 归属用户 ID。
// 2.1 单列索引用于常规按用户查任务;
// 2.2 同时参与"懒触发平移"复合索引的最左前缀。
UserID int `gorm:"column:user_id;index;index:idx_user_done_threshold_priority,priority:1"`
// 3. 任务标题。
Title string `gorm:"type:varchar(255)"`
// 4. 四象限优先级:
// 4.1 1=重要且紧急;
// 4.2 2=重要不紧急;
// 4.3 3=简单不重要;
// 4.4 4=不简单不重要。
//
// 说明:该字段参与"懒触发平移"复合索引。
Priority int `gorm:"not null;index:idx_user_done_threshold_priority,priority:4"`
// 5. 完成状态。
//
// 说明:已完成任务不参与自动平移;该字段参与复合索引。
IsCompleted bool `gorm:"column:is_completed;default:false;index:idx_user_done_threshold_priority,priority:2"`
// 6. 任务业务截止时间。
DeadlineAt *time.Time `gorm:"column:deadline_at"`
// 7. 紧急分界时间(自动平移阈值)。
//
// 规则:
// 7.1 到达该时间后,任务可从"不紧急象限"自动平移到"紧急象限"
// 7.2 该值由上游(例如 LLM 规划)给出,不在模型层做推断;
// 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"`
}
// NormalizeEstimatedSections 将预计节数收敛到 MVP 允许范围。
//
// 职责边界:
// 1. 只处理默认值与越界收敛,不判断业务优先级,也不关心调用方来源;
// 2. nil、0、负数统一回退到 1超过 4 的值收敛到 4保证写库与读回口径一致。
func NormalizeEstimatedSections(raw *int) int {
if raw == nil {
return 1
}
value := *raw
if value < 1 {
return 1
}
if value > 4 {
return 4
}
return value
}
type UserAddTaskResponse struct {
ID int `json:"id"`
Title string `json:"title"`
PriorityGroup int `json:"priority_group"`
EstimatedSections int `json:"estimated_sections"`
DeadlineAt *time.Time `json:"deadline_at"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
}
type UserAddTaskRequest struct {
Title string `json:"title"`
PriorityGroup int `json:"priority_group"`
EstimatedSections int `json:"estimated_sections"`
DeadlineAt *time.Time `json:"deadline_at"`
UrgencyThresholdAt *time.Time `json:"urgency_threshold_at"`
}
// UserCompleteTaskRequest 是"标记任务完成"接口的请求体。
//
// 职责边界:
// 1. 只承载目标任务 ID
// 2. 不承载 user_iduser_id 一律由鉴权中间件注入,避免越权)。
type UserCompleteTaskRequest struct {
TaskID int `json:"task_id"`
}
// UserCompleteTaskResponse 是"标记任务完成"接口的响应体。
//
// 字段语义:
// 1. TaskID本次操作的目标任务
// 2. IsCompleted操作后的完成状态成功时恒为 true
// 3. AlreadyCompleted
// 3.1 true任务原本就已完成本次请求命中幂等语义
// 3.2 false任务由未完成切换为完成
// 4. Status给前端的简短状态文案。
type UserCompleteTaskResponse struct {
TaskID int `json:"task_id"`
IsCompleted bool `json:"is_completed"`
AlreadyCompleted bool `json:"already_completed"`
Status string `json:"status"`
}
// UserUndoCompleteTaskRequest 是"取消任务已完成勾选"接口请求体。
//
// 职责边界:
// 1. 只承载目标 task_id
// 2. 不承载 user_iduser_id 始终由鉴权中间件注入,防止越权操作)。
type UserUndoCompleteTaskRequest struct {
TaskID int `json:"task_id"`
}
// UserUndoCompleteTaskResponse 是"取消任务已完成勾选"接口响应体。
//
// 字段语义:
// 1. TaskID本次操作目标任务
// 2. IsCompleted操作后完成状态成功时恒为 false
// 3. Status给前端的简短状态文案。
type UserUndoCompleteTaskResponse struct {
TaskID int `json:"task_id"`
IsCompleted bool `json:"is_completed"`
Status string `json:"status"`
}
type GetUserTaskResp struct {
ID int `json:"id"`
UserID int `json:"user_id"`
Title string `json:"title"`
PriorityGroup int `json:"priority_group"`
EstimatedSections int `json:"estimated_sections"`
Status string `json:"status"`
Deadline string `json:"deadline"`
IsCompleted bool `json:"is_completed"`
UrgencyThresholdAt string `json:"urgency_threshold_at,omitempty"`
}
// BatchTaskStatusRequest 是任务批量状态查询请求体。
//
// 职责边界:
// 1. 只承载前端从历史卡片中提取的任务 ID 列表;
// 2. 不承载 user_id用户身份必须来自鉴权上下文避免越权查询
// 3. 不表达任务是否必须存在,不存在或无权访问的任务由 Service 静默过滤。
type BatchTaskStatusRequest struct {
IDs []int `json:"ids"`
}
// BatchTaskStatusItem 是单个任务当前完成状态快照。
//
// 说明:
// 1. 当前 Task 模型未维护 UpdatedAt 字段,因此这里只返回可用的 id/is_completed
// 2. 该结构表示"当前状态",不用于反写 NewAgent timeline 历史 payload。
type BatchTaskStatusItem struct {
ID int `json:"id"`
IsCompleted bool `json:"is_completed"`
}
// BatchTaskStatusResponse 是批量任务状态查询响应体。
//
// 职责边界:
// 1. items 只包含当前登录用户有权访问且仍存在的任务;
// 2. ids 为空、非法 ID 全部被过滤、或无匹配任务时items 为空切片而不是业务错误。
type BatchTaskStatusResponse struct {
Items []BatchTaskStatusItem `json:"items"`
}
// UserUpdateTaskRequest 是"更新任务属性"接口的请求体。
//
// 职责边界:
// 1. 指针字段表示"部分更新"语义nil 表示不修改,非 nil 表示更新为指定值;
// 2. TaskID 为必填;
// 3. 不承载 user_id由鉴权中间件注入防止越权
type UserUpdateTaskRequest struct {
TaskID int `json:"task_id"`
Title *string `json:"title"`
PriorityGroup *int `json:"priority_group"`
DeadlineAt *time.Time `json:"deadline_at"`
UrgencyThresholdAt *time.Time `json:"urgency_threshold_at"`
}
// TaskUrgencyPromoteRequestedPayload 是"任务紧急性平移请求"事件载荷。
//
// 职责边界:
// 1. 只承载"哪个用户的哪些任务需要尝试平移"
// 2. 不包含 outbox/kafka 协议字段(这些由基础设施层统一封装);
// 3. TriggeredAt 只用于追踪触发时间,最终是否更新仍以消费时数据库条件为准。
type TaskUrgencyPromoteRequestedPayload struct {
UserID int `json:"user_id"`
TaskIDs []int `json:"task_ids"`
TriggeredAt time.Time `json:"triggered_at"`
}