Files
story/internal/httpapi/conversation_handlers.go
T

591 lines
27 KiB
Go
Raw Normal View History

2026-08-26 06:41:07 +08:00
package httpapi
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"unicode/utf8"
"data-terminal-cloud/internal/conversation"
"data-terminal-cloud/internal/deviceauth"
"data-terminal-cloud/internal/idempotency"
"data-terminal-cloud/internal/orchestration"
)
type ConversationCreator interface {
Create(context.Context, deviceauth.Identity, string, string, []byte, orchestration.CreateInput) (orchestration.CreateResponse, bool, error)
}
type ConversationActivator interface {
Activate(context.Context, deviceauth.Identity, string, string, string, []byte, orchestration.ActivateInput) (orchestration.ActivateResponse, bool, error)
}
type DeviceConversationStateReader interface {
GetDeviceState(context.Context, deviceauth.Identity, string) (orchestration.DeviceConversationState, error)
}
type ConversationHeartbeater interface {
Heartbeat(context.Context, deviceauth.Identity, string, orchestration.HeartbeatInput) (orchestration.HeartbeatResponse, error)
}
type ConversationStopper interface {
Stop(context.Context, deviceauth.Identity, string, string, string, []byte, orchestration.StopInput) (orchestration.StopResponse, bool, error)
}
type ConversationTranscriptSubmitter interface {
Submit(context.Context, deviceauth.Identity, string, string, string, []byte, orchestration.TranscriptBatchInput) (orchestration.TranscriptBatchResponse, bool, error)
}
func CreateConversationHandler(service ConversationCreator) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
identity, ok := deviceauth.FromContext(request.Context())
if !ok {
WriteError(writer, request, http.StatusUnauthorized, "DEVICE_AUTHENTICATION_FAILED", "设备身份验证失败")
return
}
idempotencyKey := strings.TrimSpace(request.Header.Get("Idempotency-Key"))
if len(idempotencyKey) < 16 || len(idempotencyKey) > 128 {
WriteAPIError(writer, request, http.StatusBadRequest, Error{
Code: "VALIDATION_FAILED", Message: "缺少有效的重复请求识别码",
FieldErrors: []FieldError{{Field: "Idempotency-Key", Message: "长度必须为16至128字符"}},
})
return
}
rawBody, err := io.ReadAll(request.Body)
if err != nil {
WriteError(writer, request, http.StatusBadRequest, "INVALID_REQUEST_BODY", "请求正文无法读取")
return
}
decoder := json.NewDecoder(bytes.NewReader(rawBody))
decoder.DisallowUnknownFields()
var input orchestration.CreateInput
if err := decoder.Decode(&input); err != nil {
WriteAPIError(writer, request, http.StatusBadRequest, Error{
Code: "VALIDATION_FAILED", Message: "创建通话参数不正确",
FieldErrors: []FieldError{{Field: "body", Message: "必须是符合契约的JSON对象"}},
})
return
}
if err := ensureJSONEOF(decoder); err != nil {
WriteError(writer, request, http.StatusBadRequest, "INVALID_REQUEST_BODY", "请求正文只能包含一个JSON对象")
return
}
if fieldErrors := validateCreateConversation(input); len(fieldErrors) > 0 {
WriteAPIError(writer, request, http.StatusBadRequest, Error{
Code: "VALIDATION_FAILED", Message: "创建通话参数不正确", FieldErrors: fieldErrors,
})
return
}
response, replayed, err := service.Create(request.Context(), identity, idempotencyKey, RequestID(request.Context()), rawBody, input)
if replayed {
writer.Header().Set("Idempotency-Replayed", "true")
}
if err != nil {
writeCreateConversationError(writer, request, err)
return
}
WriteJSON(writer, request, http.StatusCreated, response)
})
}
func ActivateConversationHandler(service ConversationActivator) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
identity, ok := deviceauth.FromContext(request.Context())
if !ok {
WriteError(writer, request, http.StatusUnauthorized, "DEVICE_AUTHENTICATION_FAILED", "设备身份验证失败")
return
}
idempotencyKey := strings.TrimSpace(request.Header.Get("Idempotency-Key"))
if len(idempotencyKey) < 16 || len(idempotencyKey) > 128 {
WriteAPIError(writer, request, http.StatusBadRequest, Error{
Code: "VALIDATION_FAILED", Message: "缺少有效的重复请求识别码",
FieldErrors: []FieldError{{Field: "Idempotency-Key", Message: "长度必须为16至128字符"}},
})
return
}
rawBody, err := io.ReadAll(request.Body)
if err != nil {
WriteError(writer, request, http.StatusBadRequest, "INVALID_REQUEST_BODY", "请求正文无法读取")
return
}
decoder := json.NewDecoder(bytes.NewReader(rawBody))
decoder.DisallowUnknownFields()
var input orchestration.ActivateInput
if err := decoder.Decode(&input); err != nil || ensureJSONEOF(decoder) != nil {
WriteAPIError(writer, request, http.StatusBadRequest, Error{
Code: "VALIDATION_FAILED", Message: "确认入房参数不正确",
FieldErrors: []FieldError{{Field: "body", Message: "必须是符合契约的JSON对象"}},
})
return
}
fieldErrors := make([]FieldError, 0)
if input.JoinedAt.IsZero() {
fieldErrors = append(fieldErrors, FieldError{Field: "joined_at", Message: "必须是RFC3339时间"})
}
if input.RoomID == "" || len(input.RoomID) > 200 {
fieldErrors = append(fieldErrors, FieldError{Field: "room_id", Message: "不能为空且最长200字符"})
}
if input.RTCUserID == "" || len(input.RTCUserID) > 200 {
fieldErrors = append(fieldErrors, FieldError{Field: "rtc_user_id", Message: "不能为空且最长200字符"})
}
if input.RTCJoinResult != "success" {
fieldErrors = append(fieldErrors, FieldError{Field: "rtc_join_result", Message: "只接受success"})
}
if input.ConfigRevision < 1 {
fieldErrors = append(fieldErrors, FieldError{Field: "config_revision", Message: "必须大于0"})
}
if len(input.ConnectionReference) > 255 {
fieldErrors = append(fieldErrors, FieldError{Field: "connection_reference", Message: "最长255字符"})
}
if len(fieldErrors) > 0 {
WriteAPIError(writer, request, http.StatusBadRequest, Error{Code: "VALIDATION_FAILED", Message: "确认入房参数不正确", FieldErrors: fieldErrors})
return
}
response, replayed, err := service.Activate(
request.Context(), identity, request.PathValue("id"), idempotencyKey,
RequestID(request.Context()), rawBody, input,
)
if err != nil {
writeActivateConversationError(writer, request, err)
return
}
if replayed {
writer.Header().Set("Idempotency-Replayed", "true")
}
WriteJSON(writer, request, http.StatusAccepted, response)
})
}
func GetDeviceConversationStateHandler(service DeviceConversationStateReader) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
identity, ok := deviceauth.FromContext(request.Context())
if !ok {
WriteError(writer, request, http.StatusUnauthorized, "DEVICE_AUTHENTICATION_FAILED", "设备身份验证失败")
return
}
conversationID := request.PathValue("id")
if !validUUID(conversationID) {
WriteAPIError(writer, request, http.StatusBadRequest, Error{
Code: "VALIDATION_FAILED", Message: "通话编号不正确",
FieldErrors: []FieldError{{Field: "id", Message: "必须是UUID"}},
})
return
}
state, err := service.GetDeviceState(request.Context(), identity, conversationID)
if err != nil {
switch {
case errors.Is(err, orchestration.ErrConversationNotFound):
WriteError(writer, request, http.StatusNotFound, "CONVERSATION_NOT_FOUND", "通话不存在或不属于当前设备")
default:
WriteAPIError(writer, request, http.StatusServiceUnavailable, Error{Code: "CONVERSATION_STATE_UNAVAILABLE", Message: "暂时无法读取通话状态", Retryable: true})
}
return
}
WriteJSON(writer, request, http.StatusOK, state)
})
}
func HeartbeatConversationHandler(service ConversationHeartbeater) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
identity, ok := deviceauth.FromContext(request.Context())
if !ok {
WriteError(writer, request, http.StatusUnauthorized, "DEVICE_AUTHENTICATION_FAILED", "设备身份验证失败")
return
}
decoder := json.NewDecoder(request.Body)
decoder.DisallowUnknownFields()
var input orchestration.HeartbeatInput
if err := decoder.Decode(&input); err != nil || ensureJSONEOF(decoder) != nil {
WriteAPIError(writer, request, http.StatusBadRequest, Error{
Code: "VALIDATION_FAILED", Message: "通话心跳参数不正确",
FieldErrors: []FieldError{{Field: "body", Message: "必须是符合契约的JSON对象"}},
})
return
}
if fieldErrors := validateHeartbeat(input); len(fieldErrors) > 0 {
WriteAPIError(writer, request, http.StatusBadRequest, Error{
Code: "VALIDATION_FAILED", Message: "通话心跳参数不正确", FieldErrors: fieldErrors,
})
return
}
response, err := service.Heartbeat(request.Context(), identity, request.PathValue("id"), input)
if err != nil {
writeHeartbeatError(writer, request, err)
return
}
WriteJSON(writer, request, http.StatusOK, response)
})
}
func StopConversationHandler(service ConversationStopper) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
identity, ok := deviceauth.FromContext(request.Context())
if !ok {
WriteError(writer, request, http.StatusUnauthorized, "DEVICE_AUTHENTICATION_FAILED", "设备身份验证失败")
return
}
idempotencyKey := strings.TrimSpace(request.Header.Get("Idempotency-Key"))
if len(idempotencyKey) < 16 || len(idempotencyKey) > 128 {
WriteAPIError(writer, request, http.StatusBadRequest, Error{
Code: "VALIDATION_FAILED", Message: "缺少有效的重复请求识别码",
FieldErrors: []FieldError{{Field: "Idempotency-Key", Message: "长度必须为16至128字符"}},
})
return
}
rawBody, err := io.ReadAll(request.Body)
if err != nil {
WriteError(writer, request, http.StatusBadRequest, "INVALID_REQUEST_BODY", "请求正文无法读取")
return
}
decoder := json.NewDecoder(bytes.NewReader(rawBody))
decoder.DisallowUnknownFields()
var input orchestration.StopInput
if err := decoder.Decode(&input); err != nil || ensureJSONEOF(decoder) != nil {
WriteAPIError(writer, request, http.StatusBadRequest, Error{
Code: "VALIDATION_FAILED", Message: "停止通话参数不正确",
FieldErrors: []FieldError{{Field: "body", Message: "必须是符合契约的JSON对象"}},
})
return
}
fieldErrors := make([]FieldError, 0)
if input.Reason == "" || len(input.Reason) > 64 || !isSafeErrorCode(input.Reason) {
fieldErrors = append(fieldErrors, FieldError{Field: "reason", Message: "必须是最长64字符的原因代码"})
}
if input.DeviceTime.IsZero() {
fieldErrors = append(fieldErrors, FieldError{Field: "device_time", Message: "必须是RFC3339时间"})
}
if input.LastHeartbeatSequence < 0 {
fieldErrors = append(fieldErrors, FieldError{Field: "last_heartbeat_sequence", Message: "不能小于0"})
}
if input.Recording.NearSegments < 0 || input.Recording.FarSegments < 0 {
fieldErrors = append(fieldErrors, FieldError{Field: "recording", Message: "分片数量不能为负数"})
}
if len(fieldErrors) > 0 {
WriteAPIError(writer, request, http.StatusBadRequest, Error{
Code: "VALIDATION_FAILED", Message: "停止通话参数不正确", FieldErrors: fieldErrors,
})
return
}
response, replayed, err := service.Stop(
request.Context(), identity, request.PathValue("id"), idempotencyKey,
RequestID(request.Context()), rawBody, input,
)
if err != nil {
writeStopConversationError(writer, request, err)
return
}
if replayed {
writer.Header().Set("Idempotency-Replayed", "true")
}
WriteJSON(writer, request, http.StatusAccepted, response)
})
}
func SubmitConversationTurnsHandler(service ConversationTranscriptSubmitter) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
identity, ok := deviceauth.FromContext(request.Context())
if !ok {
WriteError(writer, request, http.StatusUnauthorized, "DEVICE_AUTHENTICATION_FAILED", "设备身份验证失败")
return
}
idempotencyKey := strings.TrimSpace(request.Header.Get("Idempotency-Key"))
if len(idempotencyKey) < 16 || len(idempotencyKey) > 128 {
WriteAPIError(writer, request, http.StatusBadRequest, Error{
Code: "VALIDATION_FAILED", Message: "缺少有效的重复请求识别码",
FieldErrors: []FieldError{{Field: "Idempotency-Key", Message: "长度必须为16至128字符"}},
})
return
}
rawBody, err := io.ReadAll(request.Body)
if err != nil {
WriteError(writer, request, http.StatusBadRequest, "INVALID_REQUEST_BODY", "请求正文无法读取")
return
}
decoder := json.NewDecoder(bytes.NewReader(rawBody))
decoder.DisallowUnknownFields()
var input orchestration.TranscriptBatchInput
if err := decoder.Decode(&input); err != nil || ensureJSONEOF(decoder) != nil {
WriteAPIError(writer, request, http.StatusBadRequest, Error{
Code: "VALIDATION_FAILED", Message: "终稿补传参数不正确",
FieldErrors: []FieldError{{Field: "body", Message: "必须是符合契约的JSON对象"}},
})
return
}
if fieldErrors := validateTranscriptBatch(input); len(fieldErrors) > 0 {
WriteAPIError(writer, request, http.StatusBadRequest, Error{
Code: "VALIDATION_FAILED", Message: "终稿补传参数不正确", FieldErrors: fieldErrors,
})
return
}
response, replayed, err := service.Submit(
request.Context(), identity, request.PathValue("id"), idempotencyKey,
RequestID(request.Context()), rawBody, input,
)
if err != nil {
writeTranscriptBatchError(writer, request, err)
return
}
if replayed {
writer.Header().Set("Idempotency-Replayed", "true")
}
WriteJSON(writer, request, http.StatusOK, response)
})
}
func validateTranscriptBatch(input orchestration.TranscriptBatchInput) []FieldError {
fieldErrors := make([]FieldError, 0)
if len(input.Turns) < 1 || len(input.Turns) > 500 {
return append(fieldErrors, FieldError{Field: "turns", Message: "必须包含1至500条终稿"})
}
turnKeys := make(map[string]struct{}, len(input.Turns))
sequenceNumbers := make(map[int64]struct{}, len(input.Turns))
for index, turn := range input.Turns {
prefix := "turns[" + fmt.Sprint(index) + "]"
turnKey := strings.TrimSpace(turn.TurnKey)
if turnKey == "" || utf8.RuneCountInString(turnKey) > 255 {
fieldErrors = append(fieldErrors, FieldError{Field: prefix + ".turn_key", Message: "不能为空且最长255字符"})
} else if _, exists := turnKeys[turnKey]; exists {
fieldErrors = append(fieldErrors, FieldError{Field: prefix + ".turn_key", Message: "请求内不能重复"})
} else {
turnKeys[turnKey] = struct{}{}
}
if turn.SequenceNo < 1 {
fieldErrors = append(fieldErrors, FieldError{Field: prefix + ".sequence_no", Message: "必须大于0"})
} else if _, exists := sequenceNumbers[turn.SequenceNo]; exists {
fieldErrors = append(fieldErrors, FieldError{Field: prefix + ".sequence_no", Message: "请求内不能重复"})
} else {
sequenceNumbers[turn.SequenceNo] = struct{}{}
}
if turn.Speaker != "subject" && turn.Speaker != "agent" {
fieldErrors = append(fieldErrors, FieldError{Field: prefix + ".speaker", Message: "只允许subject或agent"})
}
if turn.StartAt.IsZero() {
fieldErrors = append(fieldErrors, FieldError{Field: prefix + ".start_at", Message: "必须是RFC3339时间"})
}
if turn.EndAt.IsZero() || (!turn.StartAt.IsZero() && turn.EndAt.Before(turn.StartAt)) {
fieldErrors = append(fieldErrors, FieldError{Field: prefix + ".end_at", Message: "必须是不早于start_at的RFC3339时间"})
}
finalText := strings.TrimSpace(turn.FinalText)
if finalText == "" || utf8.RuneCountInString(finalText) > 20000 {
fieldErrors = append(fieldErrors, FieldError{Field: prefix + ".final_text", Message: "不能为空且最长20000字符"})
}
}
return fieldErrors
}
func validateHeartbeat(input orchestration.HeartbeatInput) []FieldError {
fieldErrors := make([]FieldError, 0)
if input.SequenceNo < 1 {
fieldErrors = append(fieldErrors, FieldError{Field: "sequence_no", Message: "必须大于0"})
}
if input.DeviceTime.IsZero() {
fieldErrors = append(fieldErrors, FieldError{Field: "device_time", Message: "必须是RFC3339时间"})
}
if input.MonotonicMS == nil || *input.MonotonicMS < 0 {
fieldErrors = append(fieldErrors, FieldError{Field: "monotonic_ms", Message: "必须为非负整数"})
}
if !conversation.IsKnown(input.ConversationStatus) {
fieldErrors = append(fieldErrors, FieldError{Field: "conversation_status", Message: "不是有效的通话状态"})
}
if strings.TrimSpace(input.Network.Type) == "" || len(input.Network.Type) > 40 {
fieldErrors = append(fieldErrors, FieldError{Field: "network.type", Message: "不能为空且最长40字符"})
}
if strings.TrimSpace(input.FirmwareVersion) == "" || len(input.FirmwareVersion) > 80 {
fieldErrors = append(fieldErrors, FieldError{Field: "firmware_version", Message: "不能为空且最长80字符"})
}
if input.ReportedConfigRevision < 1 {
fieldErrors = append(fieldErrors, FieldError{Field: "reported_config_revision", Message: "必须大于0"})
}
if input.RecordingQueue.PendingSegments < 0 || input.RecordingQueue.FailedSegments < 0 {
fieldErrors = append(fieldErrors, FieldError{Field: "recording_queue", Message: "分片数量不能为负数"})
}
if len(input.RecordingQueue.LastFailureCode) > 80 || !isSafeErrorCode(input.RecordingQueue.LastFailureCode) {
fieldErrors = append(fieldErrors, FieldError{Field: "recording_queue.last_failure_code", Message: "只能是最长80字符的错误码"})
}
if input.RecordingQueue.LastFailureAt != nil && input.RecordingQueue.LastFailureAt.IsZero() {
fieldErrors = append(fieldErrors, FieldError{Field: "recording_queue.last_failure_at", Message: "必须是RFC3339时间"})
}
if input.RTCCredentialExpiresAt != nil && input.RTCCredentialExpiresAt.IsZero() {
fieldErrors = append(fieldErrors, FieldError{Field: "rtc_credential_expires_at", Message: "必须是RFC3339时间"})
}
if len(input.LastError) > 80 || !isSafeErrorCode(input.LastError) {
fieldErrors = append(fieldErrors, FieldError{Field: "last_error", Message: "只能是最长80字符的错误码"})
}
if input.CommandResults == nil || len(input.CommandResults) > 20 {
fieldErrors = append(fieldErrors, FieldError{Field: "command_results", Message: "必须是最多20项的数组"})
}
return fieldErrors
}
func isSafeErrorCode(value string) bool {
for _, character := range value {
if (character >= 'a' && character <= 'z') || (character >= 'A' && character <= 'Z') ||
(character >= '0' && character <= '9') || strings.ContainsRune("_.:-", character) {
continue
}
return false
}
return true
}
func validateCreateConversation(input orchestration.CreateInput) []FieldError {
fieldErrors := make([]FieldError, 0)
if input.DeviceTime.IsZero() {
fieldErrors = append(fieldErrors, FieldError{Field: "device_time", Message: "必须是RFC3339时间"})
}
if input.MonotonicMS != nil && *input.MonotonicMS < 0 {
fieldErrors = append(fieldErrors, FieldError{Field: "monotonic_ms", Message: "不能小于0"})
}
if !input.Capabilities.RTC {
fieldErrors = append(fieldErrors, FieldError{Field: "capabilities.rtc", Message: "设备必须支持RTC"})
}
if input.Capabilities.UploadResume == nil {
fieldErrors = append(fieldErrors, FieldError{Field: "capabilities.upload_resume", Message: "不能为空"})
}
if len(input.Capabilities.AudioTracks) == 0 {
fieldErrors = append(fieldErrors, FieldError{Field: "capabilities.audio_tracks", Message: "至少包含一个轨道"})
}
for _, track := range input.Capabilities.AudioTracks {
if track != "near" && track != "far" {
fieldErrors = append(fieldErrors, FieldError{Field: "capabilities.audio_tracks", Message: "只允许near或far"})
break
}
}
if strings.TrimSpace(input.Network.Type) == "" || len(input.Network.Type) > 40 {
fieldErrors = append(fieldErrors, FieldError{Field: "network.type", Message: "不能为空且最长40字符"})
}
if input.RequestedGoal != "open_conversation" {
fieldErrors = append(fieldErrors, FieldError{Field: "requested_goal", Message: "首期只允许open_conversation"})
}
return fieldErrors
}
func ensureJSONEOF(decoder *json.Decoder) error {
var extra any
err := decoder.Decode(&extra)
if errors.Is(err, io.EOF) {
return nil
}
return errors.New("additional JSON value")
}
func writeCreateConversationError(writer http.ResponseWriter, request *http.Request, err error) {
var creationFailure *orchestration.CreateFailureError
switch {
case errors.As(err, &creationFailure):
WriteAPIError(writer, request, http.StatusUnprocessableEntity, Error{
Code: "AGENT_CONTEXT_PREPARATION_FAILED", Message: "背景资料准备失败,本次通话未创建语音房间",
Retryable: false, Details: map[string]any{
"conversation_id": creationFailure.ConversationID,
"status": "FAILED",
"failure_code": creationFailure.FailureCode,
},
})
case errors.Is(err, orchestration.ErrDeviceNotBound):
WriteError(writer, request, http.StatusConflict, "DEVICE_NOT_BOUND", "设备尚未绑定有效长者")
case errors.Is(err, orchestration.ErrConsentRequired):
WriteError(writer, request, http.StatusForbidden, "CONSENT_REQUIRED", "缺少本次语音通话所需授权")
case errors.Is(err, orchestration.ErrFirmwareUpdateRequired):
WriteError(writer, request, http.StatusUpgradeRequired, "FIRMWARE_UPDATE_REQUIRED", "设备固件低于当前通话最低版本,请先完成升级")
case errors.Is(err, orchestration.ErrConversationAlreadyActive):
WriteError(writer, request, http.StatusConflict, "CONVERSATION_ALREADY_ACTIVE", "设备已有未结束通话")
case errors.Is(err, orchestration.ErrUsageQuotaExceeded):
WriteError(writer, request, http.StatusTooManyRequests, "USAGE_QUOTA_EXCEEDED", "客户组织或设备用量额度已达到上限,暂时不能创建新通话")
case errors.Is(err, orchestration.ErrRuntimeConfigUnavailable):
WriteAPIError(writer, request, http.StatusServiceUnavailable, Error{Code: "RUNTIME_CONFIG_UNAVAILABLE", Message: "当前没有可用于该设备的已发布语音路由配置", Retryable: true})
case errors.Is(err, idempotency.ErrKeyConflict):
WriteError(writer, request, http.StatusConflict, "IDEMPOTENCY_KEY_REUSED", "重复请求识别码已用于不同请求")
case errors.Is(err, idempotency.ErrInProgress):
WriteAPIError(writer, request, http.StatusConflict, Error{
Code: "REQUEST_IN_PROGRESS", Message: "相同请求正在处理中", Retryable: true,
})
default:
WriteAPIError(writer, request, http.StatusServiceUnavailable, Error{
Code: "CONVERSATION_CREATE_UNAVAILABLE", Message: "暂时无法创建通话", Retryable: true,
})
}
}
func writeActivateConversationError(writer http.ResponseWriter, request *http.Request, err error) {
switch {
case errors.Is(err, orchestration.ErrConversationNotFound):
WriteError(writer, request, http.StatusNotFound, "CONVERSATION_NOT_FOUND", "通话不存在")
case errors.Is(err, orchestration.ErrAllocationExpired):
WriteError(writer, request, http.StatusConflict, "ROOM_ALLOCATION_EXPIRED", "房间预留已经过期")
case errors.Is(err, orchestration.ErrRTCIdentityMismatch):
WriteError(writer, request, http.StatusConflict, "RTC_ALLOCATION_MISMATCH", "入房信息与云端分配不一致")
case errors.Is(err, orchestration.ErrConfigVersionConflict):
WriteError(writer, request, http.StatusConflict, "CONFIG_VERSION_CONFLICT", "设备配置版本与当前期望版本不一致")
case errors.Is(err, orchestration.ErrConversationStateConflict):
WriteError(writer, request, http.StatusConflict, "CONVERSATION_STATE_CONFLICT", "当前通话状态不允许确认入房")
case errors.Is(err, idempotency.ErrKeyConflict):
WriteError(writer, request, http.StatusConflict, "IDEMPOTENCY_KEY_REUSED", "重复请求识别码已用于不同请求")
case errors.Is(err, idempotency.ErrInProgress):
WriteAPIError(writer, request, http.StatusConflict, Error{Code: "REQUEST_IN_PROGRESS", Message: "相同请求正在处理中", Retryable: true})
default:
WriteAPIError(writer, request, http.StatusServiceUnavailable, Error{Code: "CONVERSATION_ACTIVATE_UNAVAILABLE", Message: "暂时无法确认设备入房", Retryable: true})
}
}
func writeHeartbeatError(writer http.ResponseWriter, request *http.Request, err error) {
switch {
case errors.Is(err, orchestration.ErrConversationNotFound):
WriteError(writer, request, http.StatusNotFound, "CONVERSATION_NOT_FOUND", "通话不存在")
case errors.Is(err, orchestration.ErrConfigVersionConflict):
WriteError(writer, request, http.StatusConflict, "CONFIG_VERSION_CONFLICT", "设备配置版本高于云端期望版本")
case errors.Is(err, orchestration.ErrConversationStateConflict):
WriteError(writer, request, http.StatusConflict, "CONVERSATION_STATE_CONFLICT", "当前通话状态不接受心跳")
default:
WriteAPIError(writer, request, http.StatusServiceUnavailable, Error{
Code: "CONVERSATION_HEARTBEAT_UNAVAILABLE", Message: "暂时无法处理通话心跳", Retryable: true,
})
}
}
func writeStopConversationError(writer http.ResponseWriter, request *http.Request, err error) {
switch {
case errors.Is(err, orchestration.ErrConversationNotFound):
WriteError(writer, request, http.StatusNotFound, "CONVERSATION_NOT_FOUND", "通话不存在")
case errors.Is(err, orchestration.ErrConversationStateConflict):
WriteError(writer, request, http.StatusConflict, "CONVERSATION_STATE_CONFLICT", "当前通话状态不允许停止")
case errors.Is(err, idempotency.ErrKeyConflict):
WriteError(writer, request, http.StatusConflict, "IDEMPOTENCY_KEY_REUSED", "重复请求识别码已用于不同请求")
case errors.Is(err, idempotency.ErrInProgress):
WriteAPIError(writer, request, http.StatusConflict, Error{Code: "REQUEST_IN_PROGRESS", Message: "相同请求正在处理中", Retryable: true})
default:
WriteAPIError(writer, request, http.StatusServiceUnavailable, Error{
Code: "CONVERSATION_STOP_UNAVAILABLE", Message: "暂时无法停止通话", Retryable: true,
})
}
}
func writeTranscriptBatchError(writer http.ResponseWriter, request *http.Request, err error) {
var conflict *orchestration.TranscriptConflictError
switch {
case errors.Is(err, orchestration.ErrConversationNotFound):
WriteError(writer, request, http.StatusNotFound, "CONVERSATION_NOT_FOUND", "通话不存在")
case errors.Is(err, orchestration.ErrConversationStateConflict):
WriteError(writer, request, http.StatusConflict, "CONVERSATION_STATE_CONFLICT", "当前通话状态不允许补传终稿")
case errors.Is(err, orchestration.ErrTranscriptStorageNotAllowed):
WriteError(writer, request, http.StatusForbidden, "TRANSCRIPT_STORAGE_NOT_ALLOWED", "当前留存策略或授权不允许保存文字终稿")
case errors.As(err, &conflict):
WriteAPIError(writer, request, http.StatusConflict, Error{
Code: "TRANSCRIPT_TURN_CONFLICT", Message: "补传终稿与已保存终稿不一致",
Details: map[string]any{"conflicts": conflict.Conflicts},
})
case errors.Is(err, idempotency.ErrKeyConflict):
WriteError(writer, request, http.StatusConflict, "IDEMPOTENCY_KEY_REUSED", "重复请求识别码已用于不同请求")
case errors.Is(err, idempotency.ErrInProgress):
WriteAPIError(writer, request, http.StatusConflict, Error{Code: "REQUEST_IN_PROGRESS", Message: "相同请求正在处理中", Retryable: true})
default:
WriteAPIError(writer, request, http.StatusServiceUnavailable, Error{
Code: "TRANSCRIPT_BATCH_UNAVAILABLE", Message: "暂时无法处理终稿补传", Retryable: true,
})
}
}