@@ -0,0 +1,102 @@
|
||||
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"))
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user