feat(storage): 统一二进制对象存储与公开错误
新增 Aliyun OSS 与 S3 协议、通道内重试和按优先级跨通道切换,保留 server-main 兼容与环境 OSS 内存通道。 将请求及结果中的 Base64、Data URI、Buffer、multipart 和内联二进制统一对象化,生产路径不再写入本机静态目录,历史本地资源仅保留只读兼容。 引入 PublicErrorV1 并统一 API、异步查询、兼容协议和失败回调的安全错误输出,同时补充迁移、管理端、指标、OpenAPI 与本地模拟验收。 验证:go test ./... -count=1;go vet ./...;pnpm lint;pnpm test;pnpm build;pnpm openapi;tests/ci/migrations-test.sh。
This commit is contained in:
+122
-101
@@ -14,6 +14,7 @@ import (
|
||||
"unicode"
|
||||
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/publicerror"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
@@ -568,73 +569,74 @@ type CreateTaskResult struct {
|
||||
}
|
||||
|
||||
type GatewayTask struct {
|
||||
ID string `json:"id"`
|
||||
ExternalTaskID string `json:"externalTaskId,omitempty"`
|
||||
Kind string `json:"kind"`
|
||||
RunMode string `json:"runMode"`
|
||||
AcceptanceRunID string `json:"acceptanceRunId,omitempty"`
|
||||
UserID string `json:"userId"`
|
||||
GatewayUserID string `json:"gatewayUserId,omitempty"`
|
||||
UserSource string `json:"userSource,omitempty"`
|
||||
GatewayTenantID string `json:"gatewayTenantId,omitempty"`
|
||||
TenantID string `json:"tenantId,omitempty"`
|
||||
TenantKey string `json:"tenantKey,omitempty"`
|
||||
APIKeyID string `json:"apiKeyId,omitempty"`
|
||||
APIKeyName string `json:"apiKeyName,omitempty"`
|
||||
APIKeyPrefix string `json:"apiKeyPrefix,omitempty"`
|
||||
UserGroupID string `json:"userGroupId,omitempty"`
|
||||
UserGroupKey string `json:"userGroupKey,omitempty"`
|
||||
Model string `json:"model"`
|
||||
ModelType string `json:"modelType,omitempty"`
|
||||
RequestedModel string `json:"requestedModel,omitempty"`
|
||||
ResolvedModel string `json:"resolvedModel,omitempty"`
|
||||
RequestID string `json:"requestId,omitempty"`
|
||||
ConversationID string `json:"conversationId,omitempty"`
|
||||
NewMessageCount int `json:"newMessageCount,omitempty"`
|
||||
Request map[string]any `json:"request,omitempty"`
|
||||
AsyncMode bool `json:"asyncMode"`
|
||||
RiverJobID int64 `json:"riverJobId,omitempty"`
|
||||
Status string `json:"status"`
|
||||
QueueKey string `json:"-"`
|
||||
Priority int `json:"-"`
|
||||
Cancellable *bool `json:"cancellable,omitempty"`
|
||||
Submitted *bool `json:"submitted,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
AttemptCount int `json:"attemptCount"`
|
||||
RemoteTaskID string `json:"remoteTaskId,omitempty"`
|
||||
RemoteTaskPayload map[string]any `json:"-"`
|
||||
Result map[string]any `json:"result,omitempty"`
|
||||
Billings []any `json:"billings,omitempty"`
|
||||
Usage map[string]any `json:"usage"`
|
||||
Metrics map[string]any `json:"metrics"`
|
||||
BillingSummary map[string]any `json:"billingSummary"`
|
||||
FinalChargeAmount float64 `json:"finalChargeAmount"`
|
||||
BillingVersion string `json:"billingVersion"`
|
||||
BillingStatus string `json:"billingStatus"`
|
||||
BillingCurrency string `json:"billingCurrency"`
|
||||
PricingSnapshot map[string]any `json:"pricingSnapshot,omitempty"`
|
||||
RequestFingerprint string `json:"requestFingerprint,omitempty"`
|
||||
ReservationAmount float64 `json:"reservationAmount"`
|
||||
ExecutionToken string `json:"-"`
|
||||
ExecutionLeaseUntil string `json:"executionLeaseExpiresAt,omitempty"`
|
||||
BillingUpdatedAt string `json:"billingUpdatedAt,omitempty"`
|
||||
BillingSettledAt string `json:"billingSettledAt,omitempty"`
|
||||
ResponseStartedAt string `json:"responseStartedAt,omitempty"`
|
||||
ResponseFinishedAt string `json:"responseFinishedAt,omitempty"`
|
||||
ResponseDurationMS int64 `json:"responseDurationMs"`
|
||||
FinishedAt string `json:"finishedAt,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
ErrorCode string `json:"errorCode,omitempty"`
|
||||
ErrorMessage string `json:"errorMessage,omitempty"`
|
||||
CompatibilityProtocol string `json:"compatibilityProtocol,omitempty"`
|
||||
CompatibilityPublicID string `json:"compatibilityPublicId,omitempty"`
|
||||
CompatibilitySourceProtocol string `json:"compatibilitySourceProtocol,omitempty"`
|
||||
CompatibilitySubmitHTTPStatus int `json:"compatibilitySubmitHttpStatus,omitempty"`
|
||||
CompatibilitySubmitHeaders map[string]any `json:"compatibilitySubmitHeaders,omitempty"`
|
||||
CompatibilitySubmitBody map[string]any `json:"compatibilitySubmitBody,omitempty"`
|
||||
Attempts []TaskAttempt `json:"attempts,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
ID string `json:"id"`
|
||||
ExternalTaskID string `json:"externalTaskId,omitempty"`
|
||||
Kind string `json:"kind"`
|
||||
RunMode string `json:"runMode"`
|
||||
AcceptanceRunID string `json:"acceptanceRunId,omitempty"`
|
||||
UserID string `json:"userId"`
|
||||
GatewayUserID string `json:"gatewayUserId,omitempty"`
|
||||
UserSource string `json:"userSource,omitempty"`
|
||||
GatewayTenantID string `json:"gatewayTenantId,omitempty"`
|
||||
TenantID string `json:"tenantId,omitempty"`
|
||||
TenantKey string `json:"tenantKey,omitempty"`
|
||||
APIKeyID string `json:"apiKeyId,omitempty"`
|
||||
APIKeyName string `json:"apiKeyName,omitempty"`
|
||||
APIKeyPrefix string `json:"apiKeyPrefix,omitempty"`
|
||||
UserGroupID string `json:"userGroupId,omitempty"`
|
||||
UserGroupKey string `json:"userGroupKey,omitempty"`
|
||||
Model string `json:"model"`
|
||||
ModelType string `json:"modelType,omitempty"`
|
||||
RequestedModel string `json:"requestedModel,omitempty"`
|
||||
ResolvedModel string `json:"resolvedModel,omitempty"`
|
||||
RequestID string `json:"requestId,omitempty"`
|
||||
ConversationID string `json:"conversationId,omitempty"`
|
||||
NewMessageCount int `json:"newMessageCount,omitempty"`
|
||||
Request map[string]any `json:"request,omitempty"`
|
||||
AsyncMode bool `json:"asyncMode"`
|
||||
RiverJobID int64 `json:"riverJobId,omitempty"`
|
||||
Status string `json:"status"`
|
||||
QueueKey string `json:"-"`
|
||||
Priority int `json:"-"`
|
||||
Cancellable *bool `json:"cancellable,omitempty"`
|
||||
Submitted *bool `json:"submitted,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
AttemptCount int `json:"attemptCount"`
|
||||
RemoteTaskID string `json:"remoteTaskId,omitempty"`
|
||||
RemoteTaskPayload map[string]any `json:"-"`
|
||||
Result map[string]any `json:"result,omitempty"`
|
||||
Billings []any `json:"billings,omitempty"`
|
||||
Usage map[string]any `json:"usage"`
|
||||
Metrics map[string]any `json:"metrics"`
|
||||
BillingSummary map[string]any `json:"billingSummary"`
|
||||
FinalChargeAmount float64 `json:"finalChargeAmount"`
|
||||
BillingVersion string `json:"billingVersion"`
|
||||
BillingStatus string `json:"billingStatus"`
|
||||
BillingCurrency string `json:"billingCurrency"`
|
||||
PricingSnapshot map[string]any `json:"pricingSnapshot,omitempty"`
|
||||
RequestFingerprint string `json:"requestFingerprint,omitempty"`
|
||||
ReservationAmount float64 `json:"reservationAmount"`
|
||||
ExecutionToken string `json:"-"`
|
||||
ExecutionLeaseUntil string `json:"executionLeaseExpiresAt,omitempty"`
|
||||
BillingUpdatedAt string `json:"billingUpdatedAt,omitempty"`
|
||||
BillingSettledAt string `json:"billingSettledAt,omitempty"`
|
||||
ResponseStartedAt string `json:"responseStartedAt,omitempty"`
|
||||
ResponseFinishedAt string `json:"responseFinishedAt,omitempty"`
|
||||
ResponseDurationMS int64 `json:"responseDurationMs"`
|
||||
FinishedAt string `json:"finishedAt,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
ErrorCode string `json:"errorCode,omitempty"`
|
||||
ErrorMessage string `json:"errorMessage,omitempty"`
|
||||
PublicError *publicerror.Error `json:"publicError,omitempty"`
|
||||
CompatibilityProtocol string `json:"compatibilityProtocol,omitempty"`
|
||||
CompatibilityPublicID string `json:"compatibilityPublicId,omitempty"`
|
||||
CompatibilitySourceProtocol string `json:"compatibilitySourceProtocol,omitempty"`
|
||||
CompatibilitySubmitHTTPStatus int `json:"compatibilitySubmitHttpStatus,omitempty"`
|
||||
CompatibilitySubmitHeaders map[string]any `json:"compatibilitySubmitHeaders,omitempty"`
|
||||
CompatibilitySubmitBody map[string]any `json:"compatibilitySubmitBody,omitempty"`
|
||||
Attempts []TaskAttempt `json:"attempts,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
const gatewayTaskColumns = `
|
||||
@@ -656,7 +658,7 @@ COALESCE(reservation_amount, 0)::float8, COALESCE(execution_token::text, ''),
|
||||
COALESCE(execution_lease_expires_at::text, ''), COALESCE(billing_updated_at::text, ''),
|
||||
COALESCE(billing_settled_at::text, ''), COALESCE(response_started_at::text, ''),
|
||||
COALESCE(response_finished_at::text, ''), COALESCE(response_duration_ms, 0), COALESCE(error, ''),
|
||||
COALESCE(error_code, ''), COALESCE(error_message, ''),
|
||||
COALESCE(error_code, ''), COALESCE(error_message, ''), COALESCE(public_error, '{}'::jsonb),
|
||||
COALESCE(compatibility_protocol, ''), COALESCE(compatibility_public_id, ''),
|
||||
COALESCE(compatibility_source_protocol, ''), COALESCE(compatibility_submit_http_status, 0),
|
||||
COALESCE(compatibility_submit_headers, '{}'::jsonb), COALESCE(compatibility_submit_body, '{}'::jsonb),
|
||||
@@ -679,39 +681,40 @@ type TaskEvent struct {
|
||||
}
|
||||
|
||||
type TaskAttempt struct {
|
||||
ID string `json:"id"`
|
||||
TaskID string `json:"taskId"`
|
||||
AttemptNo int `json:"attemptNo"`
|
||||
PlatformID string `json:"platformId,omitempty"`
|
||||
PlatformName string `json:"platformName,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
PlatformModelID string `json:"platformModelId,omitempty"`
|
||||
ModelName string `json:"modelName,omitempty"`
|
||||
ProviderModelName string `json:"providerModelName,omitempty"`
|
||||
ModelAlias string `json:"modelAlias,omitempty"`
|
||||
ModelType string `json:"modelType,omitempty"`
|
||||
ClientID string `json:"clientId,omitempty"`
|
||||
QueueKey string `json:"queueKey"`
|
||||
Status string `json:"status"`
|
||||
Retryable bool `json:"retryable"`
|
||||
Simulated bool `json:"simulated"`
|
||||
RequestID string `json:"requestId,omitempty"`
|
||||
StatusCode int `json:"statusCode,omitempty"`
|
||||
Usage map[string]any `json:"usage,omitempty"`
|
||||
Metrics map[string]any `json:"metrics,omitempty"`
|
||||
RequestSnapshot map[string]any `json:"requestSnapshot,omitempty"`
|
||||
ResponseSnapshot map[string]any `json:"responseSnapshot,omitempty"`
|
||||
ResponseStartedAt string `json:"responseStartedAt,omitempty"`
|
||||
ResponseFinishedAt string `json:"responseFinishedAt,omitempty"`
|
||||
ResponseDurationMS int64 `json:"responseDurationMs"`
|
||||
PricingSnapshot map[string]any `json:"pricingSnapshot,omitempty"`
|
||||
RequestFingerprint string `json:"requestFingerprint,omitempty"`
|
||||
UpstreamSubmissionStatus string `json:"upstreamSubmissionStatus"`
|
||||
UpstreamSubmissionUpdatedAt string `json:"upstreamSubmissionUpdatedAt,omitempty"`
|
||||
ErrorCode string `json:"errorCode,omitempty"`
|
||||
ErrorMessage string `json:"errorMessage,omitempty"`
|
||||
StartedAt time.Time `json:"startedAt"`
|
||||
FinishedAt string `json:"finishedAt,omitempty"`
|
||||
ID string `json:"id"`
|
||||
TaskID string `json:"taskId"`
|
||||
AttemptNo int `json:"attemptNo"`
|
||||
PlatformID string `json:"platformId,omitempty"`
|
||||
PlatformName string `json:"platformName,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
PlatformModelID string `json:"platformModelId,omitempty"`
|
||||
ModelName string `json:"modelName,omitempty"`
|
||||
ProviderModelName string `json:"providerModelName,omitempty"`
|
||||
ModelAlias string `json:"modelAlias,omitempty"`
|
||||
ModelType string `json:"modelType,omitempty"`
|
||||
ClientID string `json:"clientId,omitempty"`
|
||||
QueueKey string `json:"queueKey"`
|
||||
Status string `json:"status"`
|
||||
Retryable bool `json:"retryable"`
|
||||
Simulated bool `json:"simulated"`
|
||||
RequestID string `json:"requestId,omitempty"`
|
||||
StatusCode int `json:"statusCode,omitempty"`
|
||||
Usage map[string]any `json:"usage,omitempty"`
|
||||
Metrics map[string]any `json:"metrics,omitempty"`
|
||||
RequestSnapshot map[string]any `json:"requestSnapshot,omitempty"`
|
||||
ResponseSnapshot map[string]any `json:"responseSnapshot,omitempty"`
|
||||
ResponseStartedAt string `json:"responseStartedAt,omitempty"`
|
||||
ResponseFinishedAt string `json:"responseFinishedAt,omitempty"`
|
||||
ResponseDurationMS int64 `json:"responseDurationMs"`
|
||||
PricingSnapshot map[string]any `json:"pricingSnapshot,omitempty"`
|
||||
RequestFingerprint string `json:"requestFingerprint,omitempty"`
|
||||
UpstreamSubmissionStatus string `json:"upstreamSubmissionStatus"`
|
||||
UpstreamSubmissionUpdatedAt string `json:"upstreamSubmissionUpdatedAt,omitempty"`
|
||||
ErrorCode string `json:"errorCode,omitempty"`
|
||||
ErrorMessage string `json:"errorMessage,omitempty"`
|
||||
PublicError *publicerror.Error `json:"publicError,omitempty"`
|
||||
StartedAt time.Time `json:"startedAt"`
|
||||
FinishedAt string `json:"finishedAt,omitempty"`
|
||||
}
|
||||
|
||||
type TaskParamPreprocessingLog struct {
|
||||
@@ -2240,6 +2243,7 @@ func scanGatewayTask(scanner taskScanner) (GatewayTask, error) {
|
||||
var remoteTaskPayloadBytes []byte
|
||||
var compatibilitySubmitHeadersBytes []byte
|
||||
var compatibilitySubmitBodyBytes []byte
|
||||
var publicErrorBytes []byte
|
||||
if err := scanner.Scan(
|
||||
&task.ID,
|
||||
&task.ExternalTaskID,
|
||||
@@ -2295,6 +2299,7 @@ func scanGatewayTask(scanner taskScanner) (GatewayTask, error) {
|
||||
&task.Error,
|
||||
&task.ErrorCode,
|
||||
&task.ErrorMessage,
|
||||
&publicErrorBytes,
|
||||
&task.CompatibilityProtocol,
|
||||
&task.CompatibilityPublicID,
|
||||
&task.CompatibilitySourceProtocol,
|
||||
@@ -2317,9 +2322,25 @@ func scanGatewayTask(scanner taskScanner) (GatewayTask, error) {
|
||||
task.PricingSnapshot = decodeObject(pricingSnapshotBytes)
|
||||
task.CompatibilitySubmitHeaders = decodeObject(compatibilitySubmitHeadersBytes)
|
||||
task.CompatibilitySubmitBody = decodeObject(compatibilitySubmitBodyBytes)
|
||||
if len(publicErrorBytes) > 0 {
|
||||
var snapshot publicerror.Error
|
||||
if json.Unmarshal(publicErrorBytes, &snapshot) == nil && snapshot.Code != "" {
|
||||
task.PublicError = &snapshot
|
||||
}
|
||||
}
|
||||
return task, nil
|
||||
}
|
||||
|
||||
func encodePublicErrorSnapshot(code string, message string, status int, retryable bool, requestID string, taskID string) []byte {
|
||||
if strings.TrimSpace(code) == "" && strings.TrimSpace(message) == "" {
|
||||
return []byte("null")
|
||||
}
|
||||
snapshot := publicerror.FromFields(code, message, status, retryable)
|
||||
snapshot = publicerror.WithIDs(snapshot, requestID, taskID)
|
||||
payload, _ := json.Marshal(snapshot)
|
||||
return payload
|
||||
}
|
||||
|
||||
func (s *Store) ListTaskEvents(ctx context.Context, taskID string) ([]TaskEvent, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id::text, task_id::text, seq, event_type, COALESCE(status, ''), COALESCE(phase, ''),
|
||||
|
||||
Reference in New Issue
Block a user