feat(t216): deliver recoverable device order commands

This commit is contained in:
QiuSW
2026-07-28 13:23:18 +08:00
parent 75fdc63db2
commit 2372ab2280
32 changed files with 2415 additions and 65 deletions
@@ -20,10 +20,12 @@ type DeviceServices struct {
Lifecycle *usecase.LifecycleService
Assets *usecase.AssetService
Results *usecase.ExecutionResultService
Commands *usecase.DeviceOrderCommandService
}
func (services DeviceServices) validate() error {
if services.Lifecycle == nil || services.Assets == nil || services.Results == nil {
if services.Lifecycle == nil || services.Assets == nil ||
services.Results == nil || services.Commands == nil {
return errors.New("device services are required")
}
return nil
@@ -73,12 +75,125 @@ func NewDeviceRouteRegistrar(
routes.POST("/api/v1/tasks/:id/evidence", handler.uploadEvidence)
routes.POST("/api/v1/tasks/:id/candidates", handler.storeCandidates)
routes.POST("/api/v1/tasks/:id/human-reviews", handler.storeHumanReview)
routes.POST(
"/api/v1/tasks/:id/commands/next",
handler.pullOrderCommand,
)
routes.POST(
"/api/v1/tasks/:id/commands/:command_id/ack",
handler.acknowledgeOrderCommand,
)
routes.POST("/api/v1/tasks/:id/complete", handler.completeTask)
routes.POST("/api/v1/tasks/:id/fail", handler.failTask)
return nil
}, nil
}
func (handler *deviceHandlers) pullOrderCommand(ctx *gin.Context) {
principal, ok := devicePrincipal(ctx)
if !ok {
return
}
var request struct {
DeviceID string `json:"device_id"`
ExecutionID string `json:"execution_id"`
ClaimGeneration int64 `json:"claim_generation"`
}
if !decodeDeviceJSON(ctx, &request) ||
!deviceIDMatches(ctx, request.DeviceID, principal.DeviceID) {
return
}
command, err := handler.services.Commands.Pull(
ctx.Request.Context(),
usecase.PullDeviceOrderCommand{
UserID: principal.UserID,
DeviceID: principal.DeviceID,
TaskID: ctx.Param("id"),
ExecutionID: request.ExecutionID,
ClaimGeneration: request.ClaimGeneration,
ClaimToken: ctx.GetHeader(claimTokenHeader),
},
)
if err != nil {
writeUsecaseError(ctx, err)
return
}
ctx.Header("Cache-Control", "no-store")
if command == nil {
ctx.Status(http.StatusNoContent)
return
}
ctx.JSON(http.StatusOK, deviceOrderCommandResponse(*command))
}
func (handler *deviceHandlers) acknowledgeOrderCommand(ctx *gin.Context) {
principal, ok := devicePrincipal(ctx)
if !ok {
return
}
var request struct {
DeviceID string `json:"device_id"`
ExecutionID string `json:"execution_id"`
ClaimGeneration int64 `json:"claim_generation"`
CommandSHA256 string `json:"command_sha256"`
}
if !decodeDeviceJSON(ctx, &request) ||
!deviceIDMatches(ctx, request.DeviceID, principal.DeviceID) {
return
}
result, err := handler.services.Commands.Acknowledge(
ctx.Request.Context(),
usecase.AcknowledgeDeviceOrderCommand{
UserID: principal.UserID,
DeviceID: principal.DeviceID,
TaskID: ctx.Param("id"),
ExecutionID: request.ExecutionID,
AuthorizationID: ctx.Param("command_id"),
ClaimGeneration: request.ClaimGeneration,
ClaimToken: ctx.GetHeader(claimTokenHeader),
CommandSHA256: request.CommandSHA256,
IdempotencyKey: ctx.GetHeader("Idempotency-Key"),
},
)
if err != nil {
writeUsecaseError(ctx, err)
return
}
ctx.Header("Cache-Control", "no-store")
ctx.JSON(http.StatusOK, gin.H{
"command_id": result.Authorization.ID,
"status": result.Authorization.Status,
"replayed": result.Replayed,
})
}
func deviceOrderCommandResponse(command domain.DeviceOrderCommand) gin.H {
return gin.H{
"id": command.ID,
"schema_version": command.SchemaVersion,
"type": command.Type,
"authorization_version": command.AuthorizationVersion,
"task_id": command.TaskID,
"execution_id": command.ExecutionID,
"task_content_sha256": command.TaskContentSHA256,
"original_sku": command.OriginalSKU,
"quantity": command.Quantity,
"candidate": gin.H{
"candidate_key": command.CandidateKey,
"observed_ordinal": command.ObservedOrdinal,
"title": command.CandidateTitle,
"sku_text": command.CandidateSKUText,
"price_text": command.CandidatePriceText,
"card_signature": command.CardSignature,
"detail_signature": command.DetailSignature,
"detail_evidence_sha256": command.DetailEvidenceSHA256,
"specification_evidence_sha256": command.SpecificationEvidenceSHA256,
},
"command_sha256": command.CommandSHA256,
"authorization_status": command.AuthorizationStatus,
}
}
func (handler *deviceHandlers) referenceImage(ctx *gin.Context) {
principal, ok := devicePrincipal(ctx)
if !ok {