103 lines
3.9 KiB
Go
103 lines
3.9 KiB
Go
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"))
|
|
})
|
|
}
|