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
+65 -5
View File
@@ -25,7 +25,7 @@ const (
func (s *Server) registerKlingCompatibilityRoutes(mux *http.ServeMux) {
handler := func(next http.HandlerFunc) http.Handler {
return s.requireUser(auth.PermissionBasic, http.HandlerFunc(next))
return s.requireProtocolUser(clients.ProtocolKlingV1Omni, http.HandlerFunc(next))
}
// /api/v1 is the canonical public prefix. The historical /kling paths
// remain registered below so existing clients can migrate without downtime.
@@ -60,7 +60,7 @@ func (s *Server) registerKlingCompatibilityRoutes(mux *http.ServeMux) {
// @Param input body map[string]interface{} true "可灵 V1 Omni 请求"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
// @Failure 401 {object} ErrorEnvelope
// @Failure 401 {object} KlingErrorEnvelope
// @Router /api/v1/kling/v1/videos/omni-video [post]
func (s *Server) klingV1CreateOmniVideo(w http.ResponseWriter, r *http.Request) {
var native map[string]any
@@ -86,7 +86,7 @@ func (s *Server) klingV1CreateOmniVideo(w http.ResponseWriter, r *http.Request)
// @Param input body map[string]interface{} true "可灵 API 2.0 请求"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
// @Failure 401 {object} ErrorEnvelope
// @Failure 401 {object} KlingErrorEnvelope
// @Router /api/v1/kling/v2/omni-video/{model} [post]
func (s *Server) klingV2CreateOmniVideo(w http.ResponseWriter, r *http.Request) {
model, ok := klingV2ProviderModel(r.PathValue("model"))
@@ -151,11 +151,28 @@ func (s *Server) createKlingCompatTask(w http.ResponseWriter, r *http.Request, v
return
}
task := created.Task
targetProtocol := clients.ProtocolKlingV1Omni
if version == "v2" {
targetProtocol = clients.ProtocolKlingV2Omni
}
if !created.Replayed {
if err := s.store.SetTaskCompatibilitySubmission(r.Context(), task.ID, store.CompatibilitySubmission{TargetProtocol: targetProtocol, PublicID: task.ID}); err != nil {
writeKlingCompatError(w, http.StatusInternalServerError, "create compatibility metadata failed", "task_create_failed")
return
}
if err := s.runner.EnqueueAsyncTask(r.Context(), task); err != nil {
writeKlingCompatError(w, http.StatusInternalServerError, err.Error(), "enqueue_failed")
return
}
task, err = s.waitForCompatibilitySubmission(r, task)
if err != nil {
if wire := clients.ErrorWireResponse(err); wireResponseMatches(wire, targetProtocol) {
writeWireResponse(w, wire)
return
}
writeKlingCompatError(w, statusFromRunError(err), err.Error(), clients.ErrorCode(err))
return
}
} else {
w.Header().Set("Idempotent-Replayed", "true")
}
@@ -181,6 +198,11 @@ func klingCompatTaskBody(version string, model string, native map[string]any) (m
body["modelType"] = "omni_video"
body["_compat_provider"] = klingCompatProvider
body["_kling_compat_version"] = version
if version == "v2" {
body["_gateway_target_protocol"] = clients.ProtocolKlingV2Omni
} else {
body["_gateway_target_protocol"] = clients.ProtocolKlingV1Omni
}
body["content"] = klingLegacyContent(body)
mode := strings.TrimSpace(stringFromRequestAny(body["mode"]))
if mode == "" && version == "v1" {
@@ -590,12 +612,23 @@ func (s *Server) klingV2ListTasks(w http.ResponseWriter, r *http.Request) {
}
func klingV1Envelope(task store.GatewayTask) map[string]any {
if klingTaskUsesNativeV1Protocol(task) {
if raw := klingMap(task.Result["raw"]); len(raw) > 0 {
return raw
}
if len(task.RemoteTaskPayload) > 0 {
return task.RemoteTaskPayload
}
if raw := klingMap(task.CompatibilitySubmitBody); len(raw) > 0 {
return raw
}
}
return map[string]any{"code": 0, "message": "success", "request_id": firstNonEmpty(task.RequestID, task.ID), "data": klingV1TaskData(task)}
}
func klingV1TaskData(task store.GatewayTask) map[string]any {
data := map[string]any{
"task_id": task.ID, "task_status": klingV1Status(task.Status),
"task_id": klingCompatibilityPublicID(task), "task_status": klingV1Status(task.Status),
"task_info": map[string]any{"external_task_id": task.ExternalTaskID},
"created_at": task.CreatedAt.UnixMilli(), "updated_at": task.UpdatedAt.UnixMilli(),
}
@@ -613,12 +646,23 @@ func klingV1TaskData(task store.GatewayTask) map[string]any {
}
func klingV2Envelope(task store.GatewayTask) map[string]any {
if task.CompatibilityProtocol == clients.ProtocolKlingV2Omni && task.CompatibilitySourceProtocol == clients.ProtocolKlingV2Omni {
if raw := klingMap(task.Result["raw"]); len(raw) > 0 {
return raw
}
if len(task.RemoteTaskPayload) > 0 {
return task.RemoteTaskPayload
}
if len(task.CompatibilitySubmitBody) > 0 {
return task.CompatibilitySubmitBody
}
}
return map[string]any{"code": 0, "message": "success", "request_id": firstNonEmpty(task.RequestID, task.ID), "data": klingV2TaskData(task)}
}
func klingV2TaskData(task store.GatewayTask) map[string]any {
data := map[string]any{
"id": task.ID, "status": klingV2Status(task.Status),
"id": klingCompatibilityPublicID(task), "status": klingV2Status(task.Status),
"create_time": task.CreatedAt.UnixMilli(), "update_time": task.UpdatedAt.UnixMilli(),
"external_id": task.ExternalTaskID,
}
@@ -631,6 +675,22 @@ func klingV2TaskData(task store.GatewayTask) map[string]any {
return data
}
func klingCompatibilityPublicID(task store.GatewayTask) string {
if publicID := strings.TrimSpace(task.CompatibilityPublicID); publicID != "" {
return publicID
}
return task.ID
}
func klingTaskUsesNativeV1Protocol(task store.GatewayTask) bool {
return task.CompatibilityProtocol == clients.ProtocolKlingV1Omni && task.CompatibilitySourceProtocol == clients.ProtocolKlingV1Omni
}
func klingMap(value any) map[string]any {
result, _ := value.(map[string]any)
return result
}
func klingTaskVideos(task store.GatewayTask) []any {
items, _ := task.Result["data"].([]any)
videos := make([]any, 0, len(items))