统一任务成功、Attempt 与结算 Outbox 的事务边界,增加多实例安全结算、人工复核、请求幂等与执行租约。钱包决策使用九位精确金额,并通过审计保护约束保留流水事实;同时补充管理接口、指标与 PostgreSQL 集成测试。
86 lines
2.5 KiB
Go
86 lines
2.5 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
|
)
|
|
|
|
var errInvalidTaskIdempotencyKey = errors.New("invalid Idempotency-Key header")
|
|
|
|
func optionalTaskIdempotencyKey(r *http.Request) (string, bool, error) {
|
|
values := r.Header.Values("Idempotency-Key")
|
|
if len(values) == 0 {
|
|
return "", false, nil
|
|
}
|
|
if len(values) != 1 {
|
|
return "", false, errInvalidTaskIdempotencyKey
|
|
}
|
|
value := strings.TrimSpace(values[0])
|
|
if value == "" || len(value) > 255 || strings.Contains(value, ",") {
|
|
return "", false, errInvalidTaskIdempotencyKey
|
|
}
|
|
return value, true, nil
|
|
}
|
|
|
|
func taskIdempotencyKeyHash(key string) string {
|
|
digest := sha256.Sum256([]byte(key))
|
|
return hex.EncodeToString(digest[:])
|
|
}
|
|
|
|
func taskIdempotencyRequestHash(kind string, async bool, stream bool, body map[string]any) string {
|
|
payload, _ := json.Marshal(map[string]any{
|
|
"kind": kind, "async": async, "stream": stream, "request": body,
|
|
})
|
|
digest := sha256.Sum256(payload)
|
|
return hex.EncodeToString(digest[:])
|
|
}
|
|
|
|
func writeIdempotentTaskReplay(w http.ResponseWriter, task store.GatewayTask, compatible bool) {
|
|
w.Header().Set("Idempotent-Replayed", "true")
|
|
w.Header().Set("X-Gateway-Task-Id", task.ID)
|
|
if !compatible || task.AsyncMode || (task.Status != "succeeded" && task.Status != "failed" && task.Status != "cancelled") {
|
|
writeTaskAccepted(w, task)
|
|
return
|
|
}
|
|
if task.Status == "succeeded" {
|
|
writeJSON(w, http.StatusOK, task.Result)
|
|
return
|
|
}
|
|
status := storedTaskErrorStatus(task.ErrorCode)
|
|
message := strings.TrimSpace(task.ErrorMessage)
|
|
if message == "" {
|
|
message = strings.TrimSpace(task.Error)
|
|
}
|
|
if message == "" {
|
|
message = "task failed"
|
|
}
|
|
code := strings.TrimSpace(task.ErrorCode)
|
|
if code == "" {
|
|
code = "task_failed"
|
|
}
|
|
writeError(w, status, message, code)
|
|
}
|
|
|
|
func storedTaskErrorStatus(code string) int {
|
|
switch strings.TrimSpace(code) {
|
|
case "pricing_unavailable", "response_chain_unavailable", "billing_hold":
|
|
return http.StatusServiceUnavailable
|
|
case "insufficient_balance":
|
|
return http.StatusPaymentRequired
|
|
case "bad_request", "invalid_parameter", "invalid_previous_response_id", "unsupported_operation":
|
|
return http.StatusBadRequest
|
|
case "no_model_candidate", "cloned_voice_not_found":
|
|
return http.StatusNotFound
|
|
case "rate_limit", "platform_cooling_down", "model_cooling_down":
|
|
return http.StatusTooManyRequests
|
|
default:
|
|
return http.StatusBadGateway
|
|
}
|
|
}
|