package httpapi import ( "context" "crypto/subtle" "encoding/json" "errors" "io" "log/slog" "net/http" "data-terminal-cloud/internal/orchestration" "data-terminal-cloud/internal/volcsubtitle" "github.com/google/uuid" ) type NativeSubtitleProcessor interface { ProcessNativeSubtitle(context.Context, string, volcsubtitle.Packet) (orchestration.NativeSubtitleResult, error) } type NativeSubtitleDeviceProbe interface { ProbeNativeSubtitleDevice(context.Context, volcsubtitle.Packet) error } func NativeSubtitleCallbackHandler(secret []byte, processor NativeSubtitleProcessor) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { id := r.URL.Query().Get("conversation_id") var envelope volcsubtitle.Envelope raw, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 384*1024)) if err != nil || json.Unmarshal(raw, &envelope) != nil { WriteError(w, r, 400, "INVALID_SUBTITLE", "字幕格式不正确") return } if _, err := uuid.Parse(id); err != nil { if len(secret) < 32 || subtle.ConstantTimeCompare([]byte(envelope.Signature), secret) != 1 { WriteError(w, r, 401, "SUBTITLE_AUTHENTICATION_FAILED", "Subtitle authentication failed") return } packet, err := volcsubtitle.Decode(envelope.Message) if err != nil || packet.DeviceName == "" || packet.ProductKey == "" { WriteError(w, r, 400, "SUBTITLE_SESSION_REQUIRED", "Subtitle session identity missing") return } probe, ok := processor.(NativeSubtitleDeviceProbe) if !ok { WriteError(w, r, 503, "SUBTITLE_UNAVAILABLE", "Subtitle probe unavailable") return } if err = probe.ProbeNativeSubtitleDevice(r.Context(), packet); err != nil { status := 503 if errors.Is(err, orchestration.ErrConversationNotFound) { status = 404 } WriteError(w, r, status, "SUBTITLE_DEVICE_UNAVAILABLE", "Subtitle device unavailable") return } // Never associate a callback with the newest device session: delayed // callbacks from before reboot cannot be distinguished in this format. slog.InfoContext(r.Context(), "native hardware subtitle verified", "mode", "device_probe", "device_name", packet.DeviceName, "segments", len(packet.Data), "stored", false) w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("ok")) return } expected := volcsubtitle.Signature(secret, id) if len(secret) < 32 || subtle.ConstantTimeCompare([]byte(envelope.Signature), []byte(expected)) != 1 { WriteError(w, r, 401, "SUBTITLE_AUTHENTICATION_FAILED", "字幕回调鉴权失败") return } packet, err := volcsubtitle.Decode(envelope.Message) if err != nil { WriteError(w, r, 400, "INVALID_SUBTITLE", "字幕格式不正确") return } result, err := processor.ProcessNativeSubtitle(r.Context(), id, packet) if err != nil { status, code := 503, "SUBTITLE_UNAVAILABLE" if errors.Is(err, orchestration.ErrConversationNotFound) { status, code = 404, "SUBTITLE_SESSION_NOT_FOUND" } if errors.Is(err, orchestration.ErrTranscriptStorageNotAllowed) { status, code = 403, "TRANSCRIPT_STORAGE_NOT_ALLOWED" } if errors.Is(err, orchestration.ErrNativeSubtitleSpeaker) { status, code = 422, "SUBTITLE_SPEAKER_UNKNOWN" } if errors.Is(err, orchestration.ErrHardwareEventConflict) { status, code = 409, "SUBTITLE_CONFLICT" } if errors.Is(err, orchestration.ErrConversationStateConflict) { status, code = 409, "SUBTITLE_SESSION_STATE" } slog.WarnContext(r.Context(), "native subtitle rejected", "conversation_id", id, "code", code) WriteError(w, r, status, code, "字幕回调暂未接收") return } slog.InfoContext(r.Context(), "native subtitle accepted", "conversation_id", id, "mode", result.Mode, "segments", len(packet.Data), "accepted", result.Accepted, "duplicates", result.Duplicates) w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("ok")) }) }