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:
@@ -181,11 +181,31 @@ func (s *Server) createKelingOmniVideo(w http.ResponseWriter, r *http.Request) {
|
||||
writeKelingCompatError(w, requestID, newKelingCompatError(http.StatusInternalServerError, 5000, "create task failed"))
|
||||
return
|
||||
}
|
||||
if err := s.store.SetTaskCompatibilitySubmission(r.Context(), task.ID, store.CompatibilitySubmission{
|
||||
TargetProtocol: clients.ProtocolKlingV1Omni,
|
||||
PublicID: task.ID,
|
||||
}); err != nil {
|
||||
writeKelingCompatError(w, requestID, newKelingCompatError(http.StatusInternalServerError, 5000, "create compatibility metadata failed"))
|
||||
return
|
||||
}
|
||||
if err := s.runner.EnqueueAsyncTask(r.Context(), task); err != nil {
|
||||
s.logger.Error("enqueue Kling-compatible task failed", "taskId", task.ID, "error", err)
|
||||
writeKelingCompatError(w, requestID, newKelingCompatError(http.StatusServiceUnavailable, 5001, "video task queue is unavailable"))
|
||||
return
|
||||
}
|
||||
task, createErr = s.waitForCompatibilitySubmission(r, task)
|
||||
if createErr != nil {
|
||||
if wire := clients.ErrorWireResponse(createErr); wireResponseMatches(wire, clients.ProtocolKlingV1Omni) {
|
||||
writeWireResponse(w, wire)
|
||||
return
|
||||
}
|
||||
writeKelingCompatError(w, requestID, kelingCompatGatewayError(createErr))
|
||||
return
|
||||
}
|
||||
if kelingTaskUsesNativeProtocol(task) && len(task.CompatibilitySubmitBody) > 0 {
|
||||
writeJSON(w, task.CompatibilitySubmitHTTPStatus, task.CompatibilitySubmitBody)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, KelingCompatibleEnvelope{
|
||||
Code: 0,
|
||||
Message: "SUCCEED",
|
||||
@@ -214,7 +234,10 @@ func (s *Server) getKelingOmniVideo(w http.ResponseWriter, r *http.Request) {
|
||||
writeKelingCompatError(w, requestID, newKelingCompatError(http.StatusUnauthorized, 1002, "Authorization is invalid"))
|
||||
return
|
||||
}
|
||||
task, err := s.store.GetTask(r.Context(), strings.TrimSpace(r.PathValue("taskID")))
|
||||
task, err := s.store.GetCompatibilityTask(r.Context(), clients.ProtocolKlingV1Omni, strings.TrimSpace(r.PathValue("taskID")))
|
||||
if err != nil && store.IsNotFound(err) {
|
||||
task, err = s.store.GetTask(r.Context(), strings.TrimSpace(r.PathValue("taskID")))
|
||||
}
|
||||
if err != nil {
|
||||
if store.IsNotFound(err) {
|
||||
writeKelingCompatError(w, requestID, newKelingCompatError(http.StatusNotFound, 1203, "task not found"))
|
||||
@@ -228,6 +251,16 @@ func (s *Server) getKelingOmniVideo(w http.ResponseWriter, r *http.Request) {
|
||||
writeKelingCompatError(w, requestID, newKelingCompatError(http.StatusNotFound, 1203, "task not found"))
|
||||
return
|
||||
}
|
||||
if kelingTaskUsesNativeProtocol(task) {
|
||||
if raw, ok := task.Result["raw"].(map[string]any); ok && len(raw) > 0 {
|
||||
writeJSON(w, http.StatusOK, raw)
|
||||
return
|
||||
}
|
||||
if len(task.RemoteTaskPayload) > 0 {
|
||||
writeJSON(w, http.StatusOK, task.RemoteTaskPayload)
|
||||
return
|
||||
}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, KelingCompatibleEnvelope{
|
||||
Code: 0,
|
||||
Message: "SUCCEED",
|
||||
@@ -370,20 +403,21 @@ func normalizeKelingOmniRequest(input map[string]any) (map[string]any, *kelingCo
|
||||
}
|
||||
externalTaskID := strings.TrimSpace(stringFromKelingCompat(input["external_task_id"]))
|
||||
normalized := map[string]any{
|
||||
"model": model,
|
||||
"model_name": requestedModel,
|
||||
"modelType": "omni_video",
|
||||
"runMode": "real",
|
||||
"content": content,
|
||||
"resolution": resolution,
|
||||
"mode": mode,
|
||||
"sound": sound,
|
||||
"audio": sound == "on",
|
||||
"multi_shot": multiShot,
|
||||
"watermark": watermarkEnabled,
|
||||
"watermark_info": map[string]any{"enabled": watermarkEnabled},
|
||||
"external_task_id": externalTaskID,
|
||||
"_gateway_compatibility": kelingOmniCompatibilityMarker,
|
||||
"model": model,
|
||||
"model_name": requestedModel,
|
||||
"modelType": "omni_video",
|
||||
"runMode": "real",
|
||||
"content": content,
|
||||
"resolution": resolution,
|
||||
"mode": mode,
|
||||
"sound": sound,
|
||||
"audio": sound == "on",
|
||||
"multi_shot": multiShot,
|
||||
"watermark": watermarkEnabled,
|
||||
"watermark_info": map[string]any{"enabled": watermarkEnabled},
|
||||
"external_task_id": externalTaskID,
|
||||
"_gateway_compatibility": kelingOmniCompatibilityMarker,
|
||||
"_gateway_target_protocol": clients.ProtocolKlingV1Omni,
|
||||
}
|
||||
if prompt != "" {
|
||||
normalized["prompt"] = prompt
|
||||
@@ -648,7 +682,7 @@ func stringFromKelingCompat(value any) string {
|
||||
|
||||
func kelingCompatTaskData(task store.GatewayTask) map[string]any {
|
||||
data := map[string]any{
|
||||
"task_id": task.ID,
|
||||
"task_id": kelingCompatPublicID(task),
|
||||
"task_status": kelingCompatTaskStatus(task.Status),
|
||||
"task_info": map[string]any{
|
||||
"external_task_id": strings.TrimSpace(stringFromKelingCompat(task.Request["external_task_id"])),
|
||||
@@ -668,12 +702,20 @@ func kelingCompatTaskData(task store.GatewayTask) map[string]any {
|
||||
if videos := kelingCompatTaskVideos(task.Result); len(videos) > 0 {
|
||||
data["task_result"] = map[string]any{"videos": videos}
|
||||
}
|
||||
if task.FinalChargeAmount > 0 {
|
||||
data["final_unit_deduction"] = strconv.FormatFloat(task.FinalChargeAmount, 'f', -1, 64)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func kelingCompatPublicID(task store.GatewayTask) string {
|
||||
if publicID := strings.TrimSpace(task.CompatibilityPublicID); publicID != "" {
|
||||
return publicID
|
||||
}
|
||||
return task.ID
|
||||
}
|
||||
|
||||
func kelingTaskUsesNativeProtocol(task store.GatewayTask) bool {
|
||||
return task.CompatibilityProtocol == clients.ProtocolKlingV1Omni && task.CompatibilitySourceProtocol == clients.ProtocolKlingV1Omni
|
||||
}
|
||||
|
||||
func kelingCompatTaskStatus(status string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(status)) {
|
||||
case "succeeded", "success", "completed":
|
||||
|
||||
Reference in New Issue
Block a user