将上游 URL 直接持久化,内联媒体经对象存储后仅保留 URL 与内部定位元数据;异步轮询、任务详情和幂等重放统一使用零对象读取的 URL 投影,并增加 64KiB 响应门禁。 OpenAI 图片接口接受 url 与 b64_json,同步 Base64 限制为 20MiB 和每 Pod 2 并发;新增历史结果迁移清零门禁、结果指标和 API GOMEMLIMIT。 验证:API go test ./...、go vet、聚焦 race、pnpm openapi、pnpm lint/test/build、迁移安全检查与 docker compose config 均通过。
182 lines
5.4 KiB
Go
182 lines
5.4 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/easyai/easyai-ai-gateway/apps/api/internal/publicerror"
|
|
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
|
)
|
|
|
|
func publicGatewayTask(task store.GatewayTask) store.GatewayTask {
|
|
if task.Status == "failed" || task.Status == "cancelled" || task.ErrorCode != "" || task.ErrorMessage != "" {
|
|
value := publicerror.FromFields(task.ErrorCode, firstNonEmpty(task.ErrorMessage, task.Error), taskErrorHTTPStatus(task), false)
|
|
value = mergeStoredPublicError(task.PublicError, value)
|
|
value = publicerror.WithIDs(value, task.RequestID, task.ID)
|
|
publicerror.Observe(value)
|
|
task.PublicError = &value
|
|
task.ErrorCode = value.Code
|
|
task.ErrorMessage = value.Message
|
|
task.Error = value.Message
|
|
}
|
|
task.Attempts = append([]store.TaskAttempt(nil), task.Attempts...)
|
|
for index := range task.Attempts {
|
|
attempt := &task.Attempts[index]
|
|
// User-facing task details expose the canonical task result. Attempt
|
|
// snapshots can duplicate large input media or retain raw provider output,
|
|
// so keep them available only on dedicated administrative surfaces.
|
|
attempt.RequestSnapshot = nil
|
|
attempt.ResponseSnapshot = nil
|
|
if attempt.ErrorCode == "" && attempt.ErrorMessage == "" {
|
|
continue
|
|
}
|
|
status := attempt.StatusCode
|
|
if status <= 0 {
|
|
status = http.StatusBadGateway
|
|
}
|
|
value := publicerror.FromFields(attempt.ErrorCode, attempt.ErrorMessage, status, attempt.Retryable)
|
|
value = mergeStoredPublicError(attempt.PublicError, value)
|
|
value = publicerror.WithIDs(value, attempt.RequestID, task.ID)
|
|
attempt.PublicError = &value
|
|
attempt.ErrorCode = value.Code
|
|
attempt.ErrorMessage = value.Message
|
|
}
|
|
return task
|
|
}
|
|
|
|
func publicTaskError(task store.GatewayTask) publicerror.Error {
|
|
status := taskErrorHTTPStatus(task)
|
|
if status <= 0 {
|
|
status = http.StatusBadGateway
|
|
}
|
|
value := publicerror.FromFields(task.ErrorCode, firstNonEmpty(task.ErrorMessage, task.Error, task.Message), status, false)
|
|
value = mergeStoredPublicError(task.PublicError, value)
|
|
return publicerror.WithIDs(value, task.RequestID, task.ID)
|
|
}
|
|
|
|
func mergeStoredPublicError(stored *publicerror.Error, derived publicerror.Error) publicerror.Error {
|
|
if stored == nil || stored.Code == "" || stored.Source == "" {
|
|
return derived
|
|
}
|
|
value := *stored
|
|
if len(value.Details) == 0 && len(derived.Details) > 0 {
|
|
value.Details = derived.Details
|
|
}
|
|
if message := safeDerivedUpstreamMessage(derived); message != "" {
|
|
value.Message = message
|
|
}
|
|
return value
|
|
}
|
|
|
|
func safeDerivedUpstreamMessage(value publicerror.Error) string {
|
|
upstream, _ := value.Details["upstreamError"].(map[string]any)
|
|
message, _ := upstream["message"].(string)
|
|
return strings.TrimSpace(message)
|
|
}
|
|
|
|
func taskErrorHTTPStatus(task store.GatewayTask) int {
|
|
if task.PublicError != nil && task.PublicError.Source != "" && task.PublicError.HTTPStatus >= 400 {
|
|
return task.PublicError.HTTPStatus
|
|
}
|
|
for index := len(task.Attempts) - 1; index >= 0; index-- {
|
|
if status := task.Attempts[index].StatusCode; status >= 400 && status <= 599 {
|
|
return status
|
|
}
|
|
}
|
|
if status := publicErrorInt(task.Metrics["statusCode"]); status >= 400 && status <= 599 {
|
|
return status
|
|
}
|
|
return storedTaskErrorStatus(task.ErrorCode)
|
|
}
|
|
|
|
func publicErrorInt(value any) int {
|
|
switch typed := value.(type) {
|
|
case int:
|
|
return typed
|
|
case int32:
|
|
return int(typed)
|
|
case int64:
|
|
return int(typed)
|
|
case float32:
|
|
return int(typed)
|
|
case float64:
|
|
return int(typed)
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func publicTaskList(items []store.GatewayTask) []store.GatewayTask {
|
|
out := make([]store.GatewayTask, len(items))
|
|
for index, task := range items {
|
|
out[index] = publicGatewayTask(task)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func publicErrorMap(value publicerror.Error) map[string]any {
|
|
out := map[string]any{
|
|
"code": value.Code, "message": value.Message, "category": value.Category, "source": value.Source,
|
|
"httpStatus": value.HTTPStatus, "retryable": value.Retryable, "action": value.Action,
|
|
"version": value.Version,
|
|
}
|
|
if value.RetryAfterSeconds > 0 {
|
|
out["retryAfterSeconds"] = value.RetryAfterSeconds
|
|
}
|
|
if requestID := strings.TrimSpace(value.RequestID); requestID != "" {
|
|
out["requestId"] = requestID
|
|
}
|
|
if taskID := strings.TrimSpace(value.TaskID); taskID != "" {
|
|
out["taskId"] = taskID
|
|
}
|
|
if len(value.Details) > 0 {
|
|
out["details"] = value.Details
|
|
}
|
|
return out
|
|
}
|
|
|
|
func publicErrorWithRetryAfter(value publicerror.Error, details map[string]any) publicerror.Error {
|
|
if value.RetryAfterSeconds > 0 || details == nil {
|
|
return value
|
|
}
|
|
switch typed := details["retryAfterSeconds"].(type) {
|
|
case int:
|
|
value.RetryAfterSeconds = typed
|
|
case int32:
|
|
value.RetryAfterSeconds = int(typed)
|
|
case int64:
|
|
value.RetryAfterSeconds = int(typed)
|
|
case float64:
|
|
value.RetryAfterSeconds = int(typed)
|
|
}
|
|
if value.RetryAfterSeconds < 0 {
|
|
value.RetryAfterSeconds = 0
|
|
}
|
|
return value
|
|
}
|
|
|
|
func safePublicErrorDetails(details map[string]any, value publicerror.Error, includePublicError bool) map[string]any {
|
|
out := map[string]any{}
|
|
for _, key := range []string{"param", "retryAfterSeconds", "recoveryAt", "rateLimit", "pricing"} {
|
|
if item, ok := details[key]; ok && item != nil {
|
|
out[key] = item
|
|
}
|
|
}
|
|
if value.Category == "request" {
|
|
for _, key := range []string{"reason", "diagnosticId"} {
|
|
if item, ok := details[key]; ok && item != nil {
|
|
out[key] = item
|
|
}
|
|
}
|
|
}
|
|
for key, item := range value.Details {
|
|
if item != nil {
|
|
out[key] = item
|
|
}
|
|
}
|
|
if includePublicError {
|
|
out["publicError"] = value
|
|
}
|
|
return out
|
|
}
|