326 lines
8.5 KiB
Go
326 lines
8.5 KiB
Go
package webui
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
|
|
"cmroubao/backend-api/internal/domain"
|
|
"cmroubao/backend-api/internal/transport/authcommon"
|
|
"cmroubao/backend-api/internal/usecase"
|
|
)
|
|
|
|
const localAdminSubject = "local-admin"
|
|
|
|
type UsecaseAdapter struct {
|
|
tasks *usecase.TaskService
|
|
assets *usecase.AssetService
|
|
}
|
|
|
|
func NewUsecaseAdapter(
|
|
tasks *usecase.TaskService,
|
|
assets *usecase.AssetService,
|
|
) (*UsecaseAdapter, error) {
|
|
if tasks == nil || assets == nil {
|
|
return nil, errors.New("admin web use cases are required")
|
|
}
|
|
return &UsecaseAdapter{
|
|
tasks: tasks,
|
|
assets: assets,
|
|
}, nil
|
|
}
|
|
|
|
func (adapter *UsecaseAdapter) ListTasks(
|
|
ctx context.Context,
|
|
input ListTasksInput,
|
|
) (TaskList, error) {
|
|
var status *string
|
|
if input.Status != "" {
|
|
value := input.Status
|
|
status = &value
|
|
}
|
|
page, err := adapter.tasks.List(ctx, usecase.ListTasksQuery{
|
|
CreatorSubject: localAdminSubject,
|
|
Status: status,
|
|
Query: input.Query,
|
|
Limit: input.Limit,
|
|
Cursor: input.Cursor,
|
|
})
|
|
if err != nil {
|
|
return TaskList{}, mapUsecaseError(err)
|
|
}
|
|
items := make([]TaskSummary, 0, len(page.Items))
|
|
for _, item := range page.Items {
|
|
items = append(items, taskSummaryFrom(item))
|
|
}
|
|
return TaskList{
|
|
Items: items,
|
|
NextCursor: page.NextCursor,
|
|
}, nil
|
|
}
|
|
|
|
func (adapter *UsecaseAdapter) GetTask(
|
|
ctx context.Context,
|
|
taskID string,
|
|
) (Task, error) {
|
|
detail, err := adapter.tasks.Get(ctx, localAdminSubject, taskID)
|
|
if err != nil {
|
|
return Task{}, mapUsecaseError(err)
|
|
}
|
|
return taskFromDetail(detail), nil
|
|
}
|
|
|
|
func (adapter *UsecaseAdapter) UploadReference(
|
|
ctx context.Context,
|
|
input UploadReferenceInput,
|
|
) (UploadedAsset, error) {
|
|
result, err := adapter.assets.UploadTaskReference(
|
|
ctx,
|
|
usecase.UploadTaskReferenceCommand{
|
|
CreatorSubject: localAdminSubject,
|
|
IdempotencyKey: input.IdempotencyKey,
|
|
DeclaredMediaType: input.DeclaredType,
|
|
Content: input.Content,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return UploadedAsset{}, mapUsecaseError(err)
|
|
}
|
|
return UploadedAsset{
|
|
ID: result.Asset.ID,
|
|
}, nil
|
|
}
|
|
|
|
func (adapter *UsecaseAdapter) CreateTask(
|
|
ctx context.Context,
|
|
input CreateTaskInput,
|
|
) (Task, error) {
|
|
maxInt := int64(^uint(0) >> 1)
|
|
if input.Quantity > maxInt {
|
|
return Task{}, ErrValidation
|
|
}
|
|
var budget *string
|
|
if input.MaxBudget != "" {
|
|
value := input.MaxBudget
|
|
budget = &value
|
|
}
|
|
result, err := adapter.tasks.Create(ctx, usecase.CreateTaskCommand{
|
|
CreatorSubject: localAdminSubject,
|
|
ActorUserID: actorUserID(ctx),
|
|
IdempotencyKey: input.IdempotencyKey,
|
|
Title: input.Title,
|
|
Description: input.Description,
|
|
SKU: input.SKU,
|
|
ImageAssetID: input.ImageAssetID,
|
|
Quantity: int(input.Quantity),
|
|
MaxBudget: budget,
|
|
})
|
|
if err != nil {
|
|
return Task{}, mapUsecaseError(err)
|
|
}
|
|
return taskFromPurchase(result.Task), nil
|
|
}
|
|
|
|
func actorUserID(ctx context.Context) string {
|
|
principal, ok := authcommon.Principal(ctx)
|
|
if !ok || principal.Role != domain.UserRoleAdmin {
|
|
return ""
|
|
}
|
|
return principal.UserID
|
|
}
|
|
|
|
func (adapter *UsecaseAdapter) CancelTask(
|
|
ctx context.Context,
|
|
input CancelTaskInput,
|
|
) (Task, error) {
|
|
task, err := adapter.tasks.Cancel(ctx, usecase.CancelTaskCommand{
|
|
CreatorSubject: localAdminSubject,
|
|
ActorUserID: actorUserID(ctx),
|
|
TaskID: input.TaskID,
|
|
Reason: "管理员取消",
|
|
})
|
|
if err != nil {
|
|
return Task{}, mapUsecaseError(err)
|
|
}
|
|
return taskFromPurchase(task), nil
|
|
}
|
|
|
|
func taskSummaryFrom(task domain.PurchaseTask) TaskSummary {
|
|
return TaskSummary{
|
|
ID: task.ID,
|
|
Title: task.Title,
|
|
SKU: task.SKU,
|
|
Status: string(task.Status),
|
|
UpdatedAt: task.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
func taskFromPurchase(task domain.PurchaseTask) Task {
|
|
budget := domain.FormatOptionalCNY(task.MaxBudgetCents)
|
|
result := Task{
|
|
ID: task.ID,
|
|
Title: task.Title,
|
|
SKU: task.SKU,
|
|
Description: task.Description,
|
|
Quantity: int64(task.Quantity),
|
|
Status: string(task.Status),
|
|
ReferenceAssetID: task.ImageAssetID,
|
|
CreatedAt: task.CreatedAt,
|
|
UpdatedAt: task.UpdatedAt,
|
|
}
|
|
if budget != nil {
|
|
result.MaxBudget = *budget
|
|
}
|
|
return result
|
|
}
|
|
|
|
func taskFromDetail(detail domain.TaskDetail) Task {
|
|
task := taskFromPurchase(detail.Task)
|
|
task.ReferenceAssetID = detail.Asset.ID
|
|
task.ExecutionReport = executionReportFrom(detail.Report)
|
|
return task
|
|
}
|
|
|
|
func executionReportFrom(report *domain.ExecutionReport) *ExecutionReport {
|
|
if report == nil {
|
|
return nil
|
|
}
|
|
result := &ExecutionReport{
|
|
Events: make([]ExecutionReportEvent, 0, len(report.Events)),
|
|
Evidence: make([]ExecutionReportEvidence, 0, len(report.EvidenceAssets)),
|
|
}
|
|
for _, event := range report.Events {
|
|
result.Events = append(result.Events, ExecutionReportEvent{
|
|
Step: event.Step,
|
|
Type: event.Type,
|
|
Message: event.Message,
|
|
OccurredAt: event.OccurredAt,
|
|
ReceivedAfterExecutionExpiry: event.ReceivedAfterExecutionExpiry,
|
|
})
|
|
}
|
|
for _, evidence := range report.EvidenceAssets {
|
|
result.Evidence = append(result.Evidence, ExecutionReportEvidence{
|
|
ID: evidence.ID,
|
|
ContentURL: "/api/v1/tasks/" + evidence.TaskID + "/evidence/" + evidence.ID + "/content",
|
|
SHA256: evidence.SHA256,
|
|
SizeBytes: evidence.SizeBytes,
|
|
CreatedAt: evidence.CreatedAt,
|
|
ReceivedAfterExecutionExpiry: evidence.ReceivedAfterExecutionExpiry,
|
|
})
|
|
}
|
|
if batch := report.CandidateBatch; batch != nil {
|
|
result.Mode = batch.ExecutionMode
|
|
result.SearchQuery = batch.SearchQuery
|
|
result.Provenance = prettyAuditJSON(batch.ProvenanceJSON)
|
|
result.Candidates = prettyAuditJSON(&batch.CandidatesJSON)
|
|
result.Recommendation = prettyAuditJSON(batch.RecommendationJSON)
|
|
}
|
|
if dataset := report.DecisionDataset; dataset != nil {
|
|
result.Observations = prettyValueJSON(dataset.Observations)
|
|
result.ModelPredictions = prettyValueJSON(map[string]any{
|
|
"model_run": dataset.ModelRun,
|
|
"evaluations": dataset.Evaluations,
|
|
})
|
|
result.DeterministicRecommendation = prettyValueJSON(
|
|
dataset.Recommendation,
|
|
)
|
|
result.HumanReviews = prettyValueJSON(dataset.HumanReviews)
|
|
}
|
|
if outcome := report.Outcome; outcome != nil {
|
|
result.Outcome = &ExecutionReportOutcome{
|
|
ResultType: outcome.ResultType,
|
|
Outcome: stringValue(outcome.Outcome),
|
|
OperatorReason: stringValue(outcome.OperatorReason),
|
|
ErrorCode: stringValue(outcome.ErrorCode),
|
|
ErrorMessage: stringValue(outcome.ErrorMessage),
|
|
ErrorStep: stringValue(outcome.ErrorStep),
|
|
OrderSubmitted: outcome.OrderSubmitted,
|
|
ReceivedAt: outcome.ReceivedAt,
|
|
ReceivedAfterExecutionExpiry: outcome.ReceivedAfterExecutionExpiry,
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func prettyAuditJSON(value *string) string {
|
|
if value == nil || strings.TrimSpace(*value) == "" {
|
|
return ""
|
|
}
|
|
var decoded any
|
|
if err := json.Unmarshal([]byte(*value), &decoded); err != nil {
|
|
return ""
|
|
}
|
|
formatted, err := json.MarshalIndent(decoded, "", " ")
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return string(formatted)
|
|
}
|
|
|
|
func prettyValueJSON(value any) string {
|
|
if value == nil {
|
|
return ""
|
|
}
|
|
formatted, err := json.MarshalIndent(value, "", " ")
|
|
if err != nil || string(formatted) == "null" || string(formatted) == "[]" {
|
|
return ""
|
|
}
|
|
return string(formatted)
|
|
}
|
|
|
|
func stringValue(value *string) string {
|
|
if value == nil {
|
|
return ""
|
|
}
|
|
return *value
|
|
}
|
|
|
|
func mapUsecaseError(err error) error {
|
|
var typed *usecase.Error
|
|
if !errors.As(err, &typed) {
|
|
return err
|
|
}
|
|
var public error
|
|
switch typed.Kind {
|
|
case usecase.ErrorKindInvalid:
|
|
if typed.Code == "TASK_CANCEL_INVALID" {
|
|
public = ErrNotFound
|
|
} else if typed.Code == "ASSET_TOO_LARGE" ||
|
|
typed.Code == "ASSET_MEDIA_TYPE_UNSUPPORTED" ||
|
|
typed.Code == "ASSET_IMAGE_INVALID" ||
|
|
typed.Code == "ASSET_FILE_REQUIRED" {
|
|
public = ErrInvalidFile
|
|
} else {
|
|
public = ErrValidation
|
|
}
|
|
case usecase.ErrorKindNotFound:
|
|
public = ErrNotFound
|
|
case usecase.ErrorKindConflict:
|
|
public = ErrConflict
|
|
case usecase.ErrorKindUnavailable:
|
|
public = ErrUnavailable
|
|
default:
|
|
return err
|
|
}
|
|
return &adapterError{
|
|
public: public,
|
|
cause: err,
|
|
}
|
|
}
|
|
|
|
type adapterError struct {
|
|
public error
|
|
cause error
|
|
}
|
|
|
|
func (err *adapterError) Error() string {
|
|
return err.public.Error()
|
|
}
|
|
|
|
func (err *adapterError) Unwrap() []error {
|
|
return []error{err.public, err.cause}
|
|
}
|
|
|
|
var _ Service = (*UsecaseAdapter)(nil)
|