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.ErrOnboardingState): WriteError(writer,request,http.StatusConflict,"ONBOARDING_STATE_CONFLICT","绑定状态或首通许可已变化,请同步当前设备状态") 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.ErrHardwareSDKUpdateRequired): WriteError(writer, request, http.StatusUpgradeRequired, "HARDWARE_AGENT_SDK_UPDATE_REQUIRED", "火山硬件智能体SDK低于当前准入版本,请先完成升级") 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, }) } }