Files
story/internal/httpapi/volc_subtitle_callbacks.go
Codex Local 0c2151ace9
ci / verify (push) Canceled after 0s
chore: checkpoint 第一版
2026-09-06 17:04:11 +08:00

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"))
})
}