package service import ( "context" "encoding/json" "time" "git.yuntuo.hk/k2safe/orbit/internal/fault" core "git.yuntuo.hk/k2safe/orbit/model" "git.yuntuo.hk/k2safe/orbit/platform/auth" pulse "git.yuntuo.hk/k2safe/orbit/pulse/model" ) // HistoryPageInput 用于任务执行历史的有界分页;游标必须沿原父资源继续使用。 type HistoryPageInput struct { Limit int // Limit 默认 20,最大 100;列表不返回业务结果正文。 Cursor string // Cursor 服务端不透明游标,最长 1024 字节;为空从最新记录开始。 } // AttemptSummary 是执行尝试的查询快照,不提供当前执行授权。 type AttemptSummary struct { ConcurrencyToken uint64 `json:"concurrency_token,omitempty,string"` // ConcurrencyToken 资源键的单调许可序号;无资源键时为零,不能代替当前授权。 ID core.ID `json:"id"` // ID 尝试标识,不是执行秘密。 TaskID core.ID `json:"task_id"` // TaskID 所属任务;查询始终限定在该任务下。 Epoch uint64 `json:"epoch,string"` // Epoch 单任务执行代次,JSON 为十进制字符串。 State pulse.AttemptState `json:"state"` // State 尝试状态,与整个任务状态分开。 Revision uint64 `json:"revision,string"` // Revision 当前记录版本,可因续租/恢复变化。 LeaseUntil time.Time `json:"lease_until"` // LeaseUntil 数据库记录的租约期限;查询不能授权业务执行。 AuthorizedAt *time.Time `json:"authorized_at,omitempty"` // AuthorizedAt 开始批准时间;空表示未批准,不证明副作用已经发生。 EndedAt *time.Time `json:"ended_at,omitempty"` // EndedAt 尝试结束时间;仍在执行时为空。 Retryable bool `json:"retryable,omitempty"` // Retryable 该明确失败报告是否声明可安全重试;不代表平台必定安排重试。 ErrorCode string `json:"error_code,omitempty"` // ErrorCode 稳定错误分类;无错误时为空。 CreatedAt time.Time `json:"created_at"` // CreatedAt 创建时间,UTC 微秒精度。 UpdatedAt time.Time `json:"updated_at"` // UpdatedAt 最新变更时间,不能代替版本校验。 } // AttemptView 增加当前归属与已保存结果,不返回令牌、摘要或原始请求秘密。 type AttemptView struct { AttemptSummary // AttemptSummary 尝试状态、执行代次与时间快照。 SessionID core.ID `json:"session_id"` // SessionID 当前或最后归属会话;重连迁移后可变化。 WorkerInstanceID core.ID `json:"worker_instance_id"` // WorkerInstanceID 对应进程身份,不是认证凭据。 Result json.RawMessage `json:"result,omitempty"` // Result 已保存的尝试结果,未保存时省略;不包含迟到证据。 } // AttemptPage 返回一页尝试摘要及可选的后续位置。 type AttemptPage struct { Items []AttemptSummary `json:"items"` // Items 按 Epoch 降序排列的摘要,空列表返回 []。 NextCursor string `json:"next_cursor,omitempty"` // NextCursor 后续页游标,空表示本次已读到末尾。 } // EvidenceSummary 描述追加证据,原报告并未改变权威任务结果。 type EvidenceSummary struct { ID core.ID `json:"id"` // ID 不可变证据标识。 TaskID core.ID `json:"task_id"` // TaskID 所属任务,从已验证的父关联确定。 AttemptID core.ID `json:"attempt_id"` // AttemptID 所属执行尝试。 Outcome string `json:"outcome"` // Outcome Worker 声明的 SUCCEEDED、FAILED 或 UNKNOWN,不代表平台认可。 ErrorCode string `json:"error_code,omitempty"` // ErrorCode 原报告的稳定分类,可为空。 Retryable bool `json:"retryable,omitempty"` // Retryable 原报告的重试声明;证据不触发重试。 Reason string `json:"reason"` // Reason LEASE_LOST、NOT_STARTED 或 RESULT_CONFLICT。 Disposition string `json:"disposition"` // Disposition 固定为 EVIDENCE_ONLY,表示采集时仅保存证据。 CreatedAt time.Time `json:"created_at"` // CreatedAt 首次收到时间,重复报告不改变它。 } // EvidenceView 提供单条追加证据的原报告身份和正文。 type EvidenceView struct { EvidenceSummary // EvidenceSummary 原报告分类及采集时间。 MessageID core.ID `json:"message_id"` // MessageID 原结果报告身份,用于追踪同一次重报。 Payload json.RawMessage `json:"payload"` // Payload 原始结果/证据引用 JSON,保留数字文本;不提升为任务结果。 } // EvidencePage 返回一页证据摘要,正文通过详情单独读取。 type EvidencePage struct { Items []EvidenceSummary `json:"items"` // Items 按 created_at、ID 降序的证据摘要,不含大正文。 NextCursor string `json:"next_cursor,omitempty"` // NextCursor 必须沿同一任务及尝试继续使用。 } func normalizeHistoryPage(in HistoryPageInput) (HistoryPageInput, error) { if in.Limit == 0 { in.Limit = 20 } if in.Limit < 1 || in.Limit > 100 || len(in.Cursor) > 1024 { return in, fault.Invalid("invalid history page limit or cursor") } return in, nil } func validateHistoryIDs(ids ...core.ID) error { for _, id := range ids { if id.Validate() != nil { return fault.Invalid("invalid history resource id") } } return nil } func (s *Service) ListTaskAttempts(ctx context.Context, t auth.Token, task core.ID, in HistoryPageInput) (AttemptPage, error) { if err := validateHistoryIDs(task); err != nil { return AttemptPage{}, err } in, err := normalizeHistoryPage(in) if err != nil { return AttemptPage{}, err } return s.store.ListTaskAttempts(ctx, t, task, in) } func (s *Service) GetTaskAttempt(ctx context.Context, t auth.Token, task, attempt core.ID) (AttemptView, error) { if err := validateHistoryIDs(task, attempt); err != nil { return AttemptView{}, err } return s.store.GetTaskAttempt(ctx, t, task, attempt) } func (s *Service) ListAttemptEvidence(ctx context.Context, t auth.Token, task, attempt core.ID, in HistoryPageInput) (EvidencePage, error) { if err := validateHistoryIDs(task, attempt); err != nil { return EvidencePage{}, err } in, err := normalizeHistoryPage(in) if err != nil { return EvidencePage{}, err } return s.store.ListAttemptEvidence(ctx, t, task, attempt, in) } func (s *Service) GetAttemptEvidence(ctx context.Context, t auth.Token, task, attempt, evidence core.ID) (EvidenceView, error) { if err := validateHistoryIDs(task, attempt, evidence); err != nil { return EvidenceView{}, err } return s.store.GetAttemptEvidence(ctx, t, task, attempt, evidence) }