package orbit import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "strconv" ) // APIError 提供可分支处理的错误码和请求追踪标识,不包含内部数据库错误。 type APIError struct { StatusCode int Code string Message string RequestID string } func (e *APIError) Error() string { return fmt.Sprintf("orbit %s (HTTP %d, request %s): %s", e.Code, e.StatusCode, e.RequestID, e.Message) } func (e *APIError) Unwrap() error { return &HTTPError{StatusCode: e.StatusCode} } // SubmitTask 使用调用方持久化的幂等键;SDK 不自动生成新键或自动重试写请求。 func (c *Client) SubmitTask(ctx context.Context, key string, in SubmitTaskRequest) (SubmitTaskResult, error) { var out SubmitTaskResult err := c.call(ctx, "POST", "/v1/tasks", key, in, &out) return out, err } func (c *Client) GetTask(ctx context.Context, id string) (Task, error) { var out Task if !validID(id) { return out, errors.New("invalid task id") } err := c.call(ctx, "GET", "/v1/tasks/"+id, "", nil, &out) return out, err } func (c *Client) ListTasks(ctx context.Context, in ListTasksRequest) (TaskPage, error) { var out TaskPage q := url.Values{} q.Set("queue_id", in.QueueID) if in.State != "" { q.Set("state", string(in.State)) } if in.Limit != 0 { q.Set("limit", strconv.Itoa(in.Limit)) } if in.Cursor != "" { q.Set("cursor", in.Cursor) } err := c.call(ctx, "GET", "/v1/tasks?"+q.Encode(), "", nil, &out) return out, err } // CancelTask 当前只取消 WAITING 状态且不归属流程的任务;重复请求使用原键与原版本。 func (c *Client) CancelTask(ctx context.Context, id, key string, in CancelTaskRequest) (CancelTaskResult, error) { var out CancelTaskResult if !validID(id) { return out, errors.New("invalid task id") } err := c.call(ctx, "POST", "/v1/tasks/"+id+":cancel", key, in, &out) return out, err } func (c *Client) CreateQueue(ctx context.Context, key string, in CreateQueueRequest) (CreateQueueResult, error) { var out CreateQueueResult err := c.call(ctx, "POST", "/v1/queues", key, in, &out) return out, err } func (c *Client) GetQueue(ctx context.Context, id string) (Queue, error) { var out Queue if !validID(id) { return out, errors.New("invalid queue id") } err := c.call(ctx, "GET", "/v1/queues/"+id, "", nil, &out) return out, err } func validID(id string) bool { if len(id) != 32 { return false } for _, ch := range id { if !(ch >= '0' && ch <= '9' || ch >= 'a' && ch <= 'f') { return false } } return true } func (c *Client) call(ctx context.Context, method, path, key string, in, out any) error { if c.token == "" { return errors.New("Orbit token is required for business APIs") } if method != "GET" { if len(key) < 1 || len(key) > 128 { return errors.New("a stable Idempotency-Key of 1 to 128 ASCII characters is required") } for _, ch := range key { if ch < 33 || ch > 126 { return errors.New("Idempotency-Key must not contain spaces or control characters") } } } var body []byte var err error if in != nil { body, err = marshalWire(in) if err != nil { return err } } if len(body) > 512<<10 { return errors.New("Orbit request exceeds 512 KiB") } req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, bytes.NewReader(body)) if err != nil { return err } req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", "Bearer "+c.token) if in != nil { req.Header.Set("Content-Type", "application/json") } if key != "" { req.Header.Set("Idempotency-Key", key) } response, err := c.http.Do(req) if err != nil { return err } defer response.Body.Close() const maxResponse = 1 << 20 raw, err := io.ReadAll(io.LimitReader(response.Body, maxResponse+1)) if err != nil { return err } if len(raw) > maxResponse { return errors.New("Orbit response exceeds 1 MiB") } var envelope struct { RequestID string `json:"request_id"` Data json.RawMessage `json:"data"` Error *struct { Code string `json:"code"` Message string `json:"message"` } `json:"error"` } decoded := json.Unmarshal(raw, &envelope) if response.StatusCode < 200 || response.StatusCode >= 300 { if decoded == nil && envelope.Error != nil && len(envelope.Error.Code) <= 64 && len(envelope.Error.Message) <= 1024 && validID(envelope.RequestID) { return &APIError{response.StatusCode, envelope.Error.Code, envelope.Error.Message, envelope.RequestID} } return &HTTPError{StatusCode: response.StatusCode} } if decoded != nil || len(envelope.Data) == 0 || string(envelope.Data) == "null" || envelope.Error != nil || !validID(envelope.RequestID) { return errors.New("invalid Orbit response envelope") } return json.Unmarshal(envelope.Data, out) }