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:
2026-07-22 15:34:59 +08:00
parent 42e8b517fd
commit e07a997aa9
32 changed files with 2436 additions and 591 deletions
+58 -15
View File
@@ -46,12 +46,18 @@ func (c VolcesClient) runImage(ctx context.Context, request Request, apiKey stri
req.Header.Set("Authorization", "Bearer "+apiKey)
responseStartedAt := time.Now()
if err := notifySubmissionStarted(request); err != nil {
return Response{}, err
}
resp, err := httpClient(request.HTTPClient, c.HTTPClient).Do(req)
if err != nil {
return Response{}, &ClientError{Code: "network", Message: err.Error(), Retryable: true}
}
if err := notifyResponseReceived(request); err != nil {
return Response{}, err
}
requestID := requestIDFromHTTPResponse(resp)
result, err := decodeHTTPResponse(resp)
result, wire, err := decodeHTTPResponseForProtocol(resp, ProtocolOpenAIImages)
responseFinishedAt := time.Now()
if err != nil {
return Response{}, annotateResponseError(err, requestID, responseStartedAt, responseFinishedAt)
@@ -67,6 +73,8 @@ func (c VolcesClient) runImage(ctx context.Context, request Request, apiKey stri
ResponseStartedAt: responseStartedAt,
ResponseFinishedAt: responseFinishedAt,
ResponseDurationMS: responseDurationMS(responseStartedAt, responseFinishedAt),
UpstreamProtocol: ProtocolOpenAIImages,
Wire: wire,
}, nil
}
@@ -80,7 +88,7 @@ func (c VolcesClient) runVideo(ctx context.Context, request Request, apiKey stri
if err := validateVolcesVideoTaskBody(body); err != nil {
return Response{}, err
}
submitResult, requestID, err := c.postJSON(ctx, request, request.Candidate.BaseURL, taskPath, apiKey, body)
submitResult, requestID, _, err := c.postJSON(ctx, request, request.Candidate.BaseURL, taskPath, apiKey, body)
submitRequestID = requestID
if err != nil {
return Response{}, annotateResponseError(err, submitRequestID, submitStartedAt, time.Now())
@@ -119,7 +127,7 @@ func (c VolcesClient) runVideo(ctx context.Context, request Request, apiKey stri
}
case <-nextPoll.C:
pollStartedAt := time.Now()
pollResult, pollRequestID, err := c.getJSON(ctx, request, request.Candidate.BaseURL, taskPath+"/"+upstreamTaskID, apiKey)
pollResult, pollRequestID, pollWire, err := c.getJSON(ctx, request, request.Candidate.BaseURL, taskPath+"/"+upstreamTaskID, apiKey)
pollFinishedAt := time.Now()
requestID := firstNonEmpty(pollRequestID, submitRequestID, upstreamTaskID)
lastRequestID = requestID
@@ -151,6 +159,8 @@ func (c VolcesClient) runVideo(ctx context.Context, request Request, apiKey stri
ResponseStartedAt: submitStartedAt,
ResponseFinishedAt: pollFinishedAt,
ResponseDurationMS: responseDurationMS(submitStartedAt, pollFinishedAt),
UpstreamProtocol: ProtocolVolcesContents,
Wire: pollWire,
}, nil
case "failed", "cancelled":
return Response{}, &ClientError{
@@ -214,44 +224,73 @@ func volcesVideoTaskPath(request Request) string {
return strings.TrimRight(path, "/")
}
func (c VolcesClient) postJSON(ctx context.Context, request Request, baseURL string, path string, apiKey string, body map[string]any) (map[string]any, string, error) {
func (c VolcesClient) postJSON(ctx context.Context, request Request, baseURL string, path string, apiKey string, body map[string]any) (map[string]any, string, *WireResponse, error) {
raw, _ := json.Marshal(body)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, joinURL(baseURL, path), bytes.NewReader(raw))
if err != nil {
return nil, "", err
return nil, "", nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+apiKey)
if err := notifySubmissionStarted(request); err != nil {
return nil, "", nil, err
}
resp, err := httpClient(request.HTTPClient, c.HTTPClient).Do(req)
if err != nil {
return nil, "", &ClientError{Code: "network", Message: err.Error(), Retryable: true}
return nil, "", nil, &ClientError{Code: "network", Message: err.Error(), Retryable: true}
}
if err := notifyResponseReceived(request); err != nil {
return nil, "", nil, err
}
requestID := requestIDFromHTTPResponse(resp)
result, err := decodeHTTPResponse(resp)
result, wire, err := decodeHTTPResponseForProtocol(resp, ProtocolVolcesContents)
if err != nil {
return result, requestID, err
if notifyErr := notifyWireResponse(request, wire); notifyErr != nil {
return result, requestID, wire, notifyErr
}
return result, requestID, wire, err
}
original := result
result, envelopeRequestID, err := normalizeVolcesCompatibleResult(result)
return result, firstNonEmpty(requestID, envelopeRequestID), err
if wire != nil {
wire.Converted = volcesCompatibleResultConverted(original)
}
if notifyErr := notifyWireResponse(request, wire); notifyErr != nil {
return result, firstNonEmpty(requestID, envelopeRequestID), wire, notifyErr
}
return result, firstNonEmpty(requestID, envelopeRequestID), wire, err
}
func (c VolcesClient) getJSON(ctx context.Context, request Request, baseURL string, path string, apiKey string) (map[string]any, string, error) {
func (c VolcesClient) getJSON(ctx context.Context, request Request, baseURL string, path string, apiKey string) (map[string]any, string, *WireResponse, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, joinURL(baseURL, path), nil)
if err != nil {
return nil, "", err
return nil, "", nil, err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := httpClient(request.HTTPClient, c.HTTPClient).Do(req)
if err != nil {
return nil, "", &ClientError{Code: "network", Message: err.Error(), Retryable: true}
return nil, "", nil, &ClientError{Code: "network", Message: err.Error(), Retryable: true}
}
requestID := requestIDFromHTTPResponse(resp)
result, err := decodeHTTPResponse(resp)
result, wire, err := decodeHTTPResponseForProtocol(resp, ProtocolVolcesContents)
if err != nil {
return result, requestID, err
return result, requestID, wire, err
}
original := result
result, envelopeRequestID, err := normalizeVolcesCompatibleResult(result)
return result, firstNonEmpty(requestID, envelopeRequestID), err
if wire != nil {
wire.Converted = volcesCompatibleResultConverted(original)
}
return result, firstNonEmpty(requestID, envelopeRequestID), wire, err
}
func volcesCompatibleResultConverted(result map[string]any) bool {
if _, ok := result["error"].(map[string]any); ok {
return true
}
_, hasCode := result["code"]
_, hasData := result["data"].(map[string]any)
return hasCode && hasData
}
func normalizeVolcesCompatibleResult(result map[string]any) (map[string]any, string, error) {
@@ -354,6 +393,10 @@ func cleanProviderBody(body map[string]any) map[string]any {
"poll_interval_ms",
"pollTimeoutSeconds",
"poll_timeout_seconds",
"_gateway_compatibility",
"_gateway_target_protocol",
"_compat_provider",
"_kling_compat_version",
} {
delete(out, key)
}