feat(api): 统一官方兼容接口响应协议
兼容接口现在以入口协议作为最终响应协议,同协议保留官方 Wire 响应,跨协议统一转换成功、任务状态与错误结构。 同时修正异步提交状态边界,持久化兼容公开任务标识和官方提交响应,并新增迁移、流式响应及协议契约测试。 验证:go vet ./...;go test ./...;govulncheck ./...;pnpm lint;pnpm test;pnpm build;pnpm audit --audit-level high;pnpm openapi;全部 CI 脚本。
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type CompatibilitySubmission struct {
|
||||
TargetProtocol string
|
||||
PublicID string
|
||||
SourceProtocol string
|
||||
HTTPStatus int
|
||||
Headers map[string]any
|
||||
Body map[string]any
|
||||
}
|
||||
|
||||
func (s *Store) SetTaskCompatibilitySubmission(ctx context.Context, taskID string, submission CompatibilitySubmission) error {
|
||||
headers, _ := json.Marshal(emptyObjectIfNil(submission.Headers))
|
||||
body, _ := json.Marshal(emptyObjectIfNil(submission.Body))
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
UPDATE gateway_tasks
|
||||
SET compatibility_protocol = NULLIF($2, ''),
|
||||
compatibility_public_id = NULLIF($3, ''),
|
||||
compatibility_source_protocol = NULLIF($4, ''),
|
||||
compatibility_submit_http_status = NULLIF($5, 0),
|
||||
compatibility_submit_headers = $6::jsonb,
|
||||
compatibility_submit_body = $7::jsonb,
|
||||
updated_at = now()
|
||||
WHERE id = $1::uuid`,
|
||||
taskID,
|
||||
strings.TrimSpace(submission.TargetProtocol),
|
||||
strings.TrimSpace(submission.PublicID),
|
||||
strings.TrimSpace(submission.SourceProtocol),
|
||||
submission.HTTPStatus,
|
||||
string(headers),
|
||||
string(body),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) GetCompatibilityTask(ctx context.Context, protocol string, publicID string) (GatewayTask, error) {
|
||||
task, err := scanGatewayTask(s.pool.QueryRow(ctx, `
|
||||
SELECT `+gatewayTaskColumns+`
|
||||
FROM gateway_tasks
|
||||
WHERE compatibility_protocol = $1
|
||||
AND compatibility_public_id = $2
|
||||
LIMIT 1`, strings.TrimSpace(protocol), strings.TrimSpace(publicID)))
|
||||
if err != nil {
|
||||
return GatewayTask{}, err
|
||||
}
|
||||
attempts, err := s.ListTaskAttempts(ctx, task.ID)
|
||||
if err != nil {
|
||||
return GatewayTask{}, err
|
||||
}
|
||||
task.Attempts = attempts
|
||||
return task, nil
|
||||
}
|
||||
@@ -486,64 +486,70 @@ type CreateTaskResult struct {
|
||||
}
|
||||
|
||||
type GatewayTask struct {
|
||||
ID string `json:"id"`
|
||||
ExternalTaskID string `json:"externalTaskId,omitempty"`
|
||||
Kind string `json:"kind"`
|
||||
RunMode string `json:"runMode"`
|
||||
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"`
|
||||
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"`
|
||||
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"`
|
||||
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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
const gatewayTaskColumns = `
|
||||
@@ -564,6 +570,9 @@ COALESCE(execution_lease_expires_at::text, ''), COALESCE(billing_updated_at::tex
|
||||
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(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),
|
||||
created_at, updated_at, COALESCE(finished_at::text, '')`
|
||||
|
||||
type TaskEvent struct {
|
||||
@@ -2055,6 +2064,8 @@ func scanGatewayTask(scanner taskScanner) (GatewayTask, error) {
|
||||
var billingSummaryBytes []byte
|
||||
var pricingSnapshotBytes []byte
|
||||
var remoteTaskPayloadBytes []byte
|
||||
var compatibilitySubmitHeadersBytes []byte
|
||||
var compatibilitySubmitBodyBytes []byte
|
||||
if err := scanner.Scan(
|
||||
&task.ID,
|
||||
&task.ExternalTaskID,
|
||||
@@ -2107,6 +2118,12 @@ func scanGatewayTask(scanner taskScanner) (GatewayTask, error) {
|
||||
&task.Error,
|
||||
&task.ErrorCode,
|
||||
&task.ErrorMessage,
|
||||
&task.CompatibilityProtocol,
|
||||
&task.CompatibilityPublicID,
|
||||
&task.CompatibilitySourceProtocol,
|
||||
&task.CompatibilitySubmitHTTPStatus,
|
||||
&compatibilitySubmitHeadersBytes,
|
||||
&compatibilitySubmitBodyBytes,
|
||||
&task.CreatedAt,
|
||||
&task.UpdatedAt,
|
||||
&task.FinishedAt,
|
||||
@@ -2121,6 +2138,8 @@ func scanGatewayTask(scanner taskScanner) (GatewayTask, error) {
|
||||
task.Metrics = decodeObject(metricsBytes)
|
||||
task.BillingSummary = decodeObject(billingSummaryBytes)
|
||||
task.PricingSnapshot = decodeObject(pricingSnapshotBytes)
|
||||
task.CompatibilitySubmitHeaders = decodeObject(compatibilitySubmitHeadersBytes)
|
||||
task.CompatibilitySubmitBody = decodeObject(compatibilitySubmitBodyBytes)
|
||||
return task, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -177,7 +177,7 @@ WHERE (
|
||||
AND (NULLIF($3, '') IS NULL OR api_key_id = $3)
|
||||
AND request->>'_compat_provider' = $4
|
||||
AND request->>'_kling_compat_version' = $5
|
||||
AND (id::text = $6 OR external_task_id = $6)
|
||||
AND (id::text = $6 OR external_task_id = $6 OR remote_task_id = $6 OR compatibility_public_id = $6)
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1`, gatewayUserID, userID, apiKeyID, strings.TrimSpace(provider), strings.TrimSpace(version), strings.TrimSpace(identifier)))
|
||||
if err != nil {
|
||||
|
||||
@@ -81,7 +81,11 @@ WHERE (
|
||||
AND request->>'_gateway_compatibility' = $4
|
||||
AND (NULLIF($5, '') IS NULL OR LOWER(status) = $5)
|
||||
AND (NULLIF($6, '') IS NULL OR model = $6 OR resolved_model = $6)
|
||||
AND (COALESCE(array_length($7::text[], 1), 0) = 0 OR id::text = ANY($7::text[]))`
|
||||
AND (
|
||||
COALESCE(array_length($7::text[], 1), 0) = 0
|
||||
OR id::text = ANY($7::text[])
|
||||
OR remote_task_id = ANY($7::text[])
|
||||
)`
|
||||
var total int
|
||||
if err := s.pool.QueryRow(ctx, `SELECT count(*) FROM gateway_tasks `+whereSQL, args...).Scan(&total); err != nil {
|
||||
return TaskListResult{}, err
|
||||
@@ -107,5 +111,45 @@ LIMIT $8 OFFSET $9`, append(args, pageSize, (page-1)*pageSize)...)
|
||||
if err := rows.Err(); err != nil {
|
||||
return TaskListResult{}, err
|
||||
}
|
||||
items, err = s.attachTaskAttempts(ctx, items)
|
||||
if err != nil {
|
||||
return TaskListResult{}, err
|
||||
}
|
||||
return TaskListResult{Items: items, Total: total, Page: page, PageSize: pageSize}, nil
|
||||
}
|
||||
|
||||
// GetVolcesCompatibleTask resolves both the gateway UUID used for converted
|
||||
// tasks and the upstream Ark task ID exposed by native passthrough tasks.
|
||||
func (s *Store) GetVolcesCompatibleTask(ctx context.Context, user *auth.User, compatibilityMarker string, identifier string) (GatewayTask, error) {
|
||||
gatewayUserID := localGatewayUserID(user)
|
||||
userID, apiKeyID := "", ""
|
||||
if user != nil {
|
||||
userID = strings.TrimSpace(user.ID)
|
||||
apiKeyID = strings.TrimSpace(user.APIKeyID)
|
||||
}
|
||||
if gatewayUserID == "" && userID == "" {
|
||||
return GatewayTask{}, ErrLocalUserRequired
|
||||
}
|
||||
task, err := scanGatewayTask(s.pool.QueryRow(ctx, `
|
||||
SELECT `+gatewayTaskColumns+`
|
||||
FROM gateway_tasks
|
||||
WHERE (
|
||||
(NULLIF($1, '')::uuid IS NOT NULL AND gateway_user_id = NULLIF($1, '')::uuid)
|
||||
OR (NULLIF($1, '')::uuid IS NULL AND NULLIF($2, '') IS NOT NULL AND user_id = $2)
|
||||
)
|
||||
AND (NULLIF($3, '') IS NULL OR api_key_id = $3)
|
||||
AND kind = 'videos.generations'
|
||||
AND request->>'_gateway_compatibility' = $4
|
||||
AND (id::text = $5 OR remote_task_id = $5)
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1`, gatewayUserID, userID, apiKeyID, strings.TrimSpace(compatibilityMarker), strings.TrimSpace(identifier)))
|
||||
if err != nil {
|
||||
return GatewayTask{}, err
|
||||
}
|
||||
attempts, err := s.ListTaskAttempts(ctx, task.ID)
|
||||
if err != nil {
|
||||
return GatewayTask{}, err
|
||||
}
|
||||
task.Attempts = attempts
|
||||
return task, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user