217 lines
11 KiB
Go
217 lines
11 KiB
Go
package httpapi
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"errors"
|
||
|
|
"io"
|
||
|
|
"net/http"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"data-terminal-cloud/internal/deviceauth"
|
||
|
|
"data-terminal-cloud/internal/hardwarelicense"
|
||
|
|
"data-terminal-cloud/internal/hardwareprovision"
|
||
|
|
"data-terminal-cloud/internal/orchestration"
|
||
|
|
)
|
||
|
|
|
||
|
|
type HardwareAgentSessionService interface {
|
||
|
|
CreateHardwareAgent(context.Context, deviceauth.Identity, string, string, []byte, orchestration.HardwareAgentCreateInput) (orchestration.HardwareAgentCreateResponse, bool, error)
|
||
|
|
ProcessHardwareAgentEvent(context.Context, deviceauth.Identity, string, []byte, orchestration.HardwareAgentEventInput) (orchestration.HardwareAgentEventResponse, error)
|
||
|
|
}
|
||
|
|
|
||
|
|
type HardwareAgentProvisioningService interface {
|
||
|
|
Provision(context.Context, deviceauth.Identity, string, hardwareprovision.ProvisionInput) (hardwareprovision.ProvisionResponse, error)
|
||
|
|
}
|
||
|
|
|
||
|
|
type HardwareAgentLicenseService interface {
|
||
|
|
Acknowledge(context.Context, deviceauth.Identity, string, hardwarelicense.AcknowledgeInput) error
|
||
|
|
}
|
||
|
|
|
||
|
|
func AcknowledgeHardwareAgentLicenseHandler(service HardwareAgentLicenseService) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
identity, ok := deviceauth.FromContext(r.Context())
|
||
|
|
if !ok {
|
||
|
|
WriteError(w, r, http.StatusUnauthorized, "DEVICE_AUTHENTICATION_FAILED", "设备身份验证失败")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
var body struct {
|
||
|
|
LicenseRef string `json:"license_ref"`
|
||
|
|
Digest string `json:"digest"`
|
||
|
|
Result string `json:"result"`
|
||
|
|
ErrorCode string `json:"error_code"`
|
||
|
|
}
|
||
|
|
if !decodeWebJSON(w, r, &body, "License确认参数不正确") {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
err := service.Acknowledge(r.Context(), identity, RequestID(r.Context()), hardwarelicense.AcknowledgeInput{LicenseRef: body.LicenseRef, Digest: body.Digest, Result: body.Result, ErrorCode: body.ErrorCode})
|
||
|
|
if err != nil {
|
||
|
|
switch {
|
||
|
|
case errors.Is(err, hardwarelicense.ErrAssignment), errors.Is(err, hardwarelicense.ErrAcknowledgement):
|
||
|
|
WriteError(w, r, http.StatusConflict, "HARDWARE_LICENSE_ACK_CONFLICT", "License不属于当前设备或摘要不一致")
|
||
|
|
case errors.Is(err, hardwarelicense.ErrExpired):
|
||
|
|
WriteError(w, r, http.StatusConflict, "HARDWARE_LICENSE_EXPIRED", "License已经到期")
|
||
|
|
default:
|
||
|
|
WriteAPIError(w, r, http.StatusServiceUnavailable, Error{Code: "HARDWARE_LICENSE_ACK_FAILED", Message: "License确认暂时失败", Retryable: true})
|
||
|
|
}
|
||
|
|
return
|
||
|
|
}
|
||
|
|
w.Header().Set("Cache-Control", "no-store")
|
||
|
|
WriteJSON(w, r, http.StatusOK, map[string]any{"license_ref": body.LicenseRef, "status": strings.ToUpper(strings.TrimSpace(body.Result))})
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func ProvisionHardwareAgentCredentialHandler(service HardwareAgentProvisioningService) http.Handler {
|
||
|
|
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||
|
|
writer.Header().Set("Cache-Control", "no-store")
|
||
|
|
writer.Header().Set("Pragma", "no-cache")
|
||
|
|
identity, ok := deviceauth.FromContext(request.Context())
|
||
|
|
if !ok {
|
||
|
|
WriteError(writer, request, http.StatusUnauthorized, "DEVICE_AUTHENTICATION_FAILED", "设备身份验证失败")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
key := strings.TrimSpace(request.Header.Get("Idempotency-Key"))
|
||
|
|
if len(key) < 16 || len(key) > 128 {
|
||
|
|
WriteError(writer, request, http.StatusBadRequest, "VALIDATION_FAILED", "缺少有效的重复请求识别码")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
decoder := json.NewDecoder(request.Body)
|
||
|
|
decoder.DisallowUnknownFields()
|
||
|
|
var input hardwareprovision.ProvisionInput
|
||
|
|
if err := decoder.Decode(&input); err != nil || ensureJSONEOF(decoder) != nil {
|
||
|
|
WriteError(writer, request, http.StatusBadRequest, "VALIDATION_FAILED", "火山设备注册参数不正确")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
response, err := service.Provision(request.Context(), identity, RequestID(request.Context()), input)
|
||
|
|
if err != nil {
|
||
|
|
switch {
|
||
|
|
case errors.Is(err, hardwareprovision.ErrUnavailable):
|
||
|
|
WriteAPIError(writer, request, http.StatusServiceUnavailable, Error{Code: "HARDWARE_AGENT_PROVISIONING_UNAVAILABLE", Message: "平台尚未配置火山设备注册服务", Retryable: false})
|
||
|
|
case errors.Is(err, hardwareprovision.ErrInvalid):
|
||
|
|
WriteError(writer, request, http.StatusBadRequest, "VALIDATION_FAILED", "火山硬件标识不正确")
|
||
|
|
case errors.Is(err, hardwareprovision.ErrBindingMissing):
|
||
|
|
WriteError(writer, request, http.StatusConflict, "HARDWARE_AGENT_BINDING_REQUIRED", "设备尚未绑定火山硬件智能体")
|
||
|
|
case errors.Is(err, hardwareprovision.ErrBindingMismatch):
|
||
|
|
WriteError(writer, request, http.StatusConflict, "HARDWARE_AGENT_PRODUCT_MISMATCH", "设备绑定产品与平台注册产品不一致")
|
||
|
|
case errors.Is(err, hardwareprovision.ErrHardwareMismatch):
|
||
|
|
WriteError(writer, request, http.StatusConflict, "HARDWARE_AGENT_HARDWARE_MISMATCH", "硬件标识与已注册凭据不一致")
|
||
|
|
case errors.Is(err, hardwareprovision.ErrActivationState):
|
||
|
|
WriteError(writer, request, http.StatusConflict, "DEVICE_ACTIVATION_REQUIRED", "设备必须先完成用户绑定,才能申请火山运行凭据")
|
||
|
|
default:
|
||
|
|
WriteAPIError(writer, request, http.StatusServiceUnavailable, Error{Code: "HARDWARE_AGENT_PROVISIONING_FAILED", Message: "火山设备注册暂时失败", Retryable: true})
|
||
|
|
}
|
||
|
|
return
|
||
|
|
}
|
||
|
|
WriteJSON(writer, request, http.StatusOK, response)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func CreateHardwareAgentSessionHandler(service HardwareAgentSessionService) 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
|
||
|
|
}
|
||
|
|
key := strings.TrimSpace(request.Header.Get("Idempotency-Key"))
|
||
|
|
if len(key) < 16 || len(key) > 128 {
|
||
|
|
WriteError(writer, request, http.StatusBadRequest, "VALIDATION_FAILED", "缺少有效的重复请求识别码")
|
||
|
|
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.HardwareAgentCreateInput
|
||
|
|
if err = decoder.Decode(&input); err != nil || ensureJSONEOF(decoder) != nil {
|
||
|
|
WriteError(writer, request, http.StatusBadRequest, "VALIDATION_FAILED", "硬件智能体会话参数不正确")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if input.DeviceTime.IsZero() || input.ConfigRevision < 1 || !input.Capabilities.HardwareAgent ||
|
||
|
|
!input.Capabilities.TranscriptUpload || !input.Capabilities.DynamicSessionConfig ||
|
||
|
|
!input.Capabilities.CredentialRotation || strings.TrimSpace(input.Capabilities.SDKVersion) == "" ||
|
||
|
|
len(input.Capabilities.SDKVersion) > 80 || strings.TrimSpace(input.Capabilities.InputAudioFormat) == "" ||
|
||
|
|
len(input.Capabilities.InputAudioFormat) > 80 || strings.TrimSpace(input.Capabilities.OutputAudioFormat) == "" ||
|
||
|
|
len(input.Capabilities.OutputAudioFormat) > 80 || input.RequestedGoal != "open_conversation" {
|
||
|
|
WriteError(writer, request, http.StatusBadRequest, "VALIDATION_FAILED", "设备时间、配置版本或硬件智能体能力不正确")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
validTransport := false
|
||
|
|
for _, transport := range input.Capabilities.Transports {
|
||
|
|
if transport != "high_quality" && transport != "low_load" {
|
||
|
|
WriteError(writer, request, http.StatusBadRequest, "VALIDATION_FAILED", "硬件智能体传输模式不受支持")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
validTransport = true
|
||
|
|
}
|
||
|
|
if !validTransport {
|
||
|
|
WriteError(writer, request, http.StatusBadRequest, "VALIDATION_FAILED", "设备必须声明至少一种支持的硬件智能体传输模式")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
response, replayed, err := service.CreateHardwareAgent(request.Context(), identity, key, 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 HardwareAgentEventHandler(service HardwareAgentSessionService) 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
|
||
|
|
}
|
||
|
|
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.HardwareAgentEventInput
|
||
|
|
if err = decoder.Decode(&input); err != nil || ensureJSONEOF(decoder) != nil ||
|
||
|
|
strings.TrimSpace(input.EventID) == "" || input.SequenceNo < 1 || input.OccurredAt.IsZero() ||
|
||
|
|
input.OccurredAt.After(time.Now().UTC().Add(5*time.Minute)) || strings.TrimSpace(input.ProviderSessionRef) == "" ||
|
||
|
|
strings.TrimSpace(input.SessionNonce) == "" {
|
||
|
|
WriteError(writer, request, http.StatusBadRequest, "VALIDATION_FAILED", "硬件智能体事件参数不正确")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
switch input.EventType {
|
||
|
|
case "session_started", "session_ended", "session_failed", "session_updated":
|
||
|
|
default:
|
||
|
|
WriteError(writer, request, http.StatusBadRequest, "VALIDATION_FAILED", "不支持的硬件智能体事件类型")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if input.EventType == "session_started" && (!input.DynamicConfigApplied || strings.TrimSpace(input.ProviderConnectionRef) == "" ||
|
||
|
|
strings.TrimSpace(input.BotID) == "" || strings.TrimSpace(input.Transport) == "" || strings.TrimSpace(input.ConfigDigest) == "") {
|
||
|
|
WriteError(writer, request, http.StatusBadRequest, "VALIDATION_FAILED", "会话启动前必须证明动态人设配置已生效")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
response, err := service.ProcessHardwareAgentEvent(request.Context(), identity, request.PathValue("id"), rawBody, input)
|
||
|
|
if err != nil {
|
||
|
|
switch {
|
||
|
|
case errors.Is(err, orchestration.ErrConversationNotFound):
|
||
|
|
WriteError(writer, request, http.StatusNotFound, "CONVERSATION_NOT_FOUND", "通话不存在或不属于当前设备")
|
||
|
|
case errors.Is(err, orchestration.ErrHardwareSessionProof):
|
||
|
|
WriteError(writer, request, http.StatusUnauthorized, "HARDWARE_SESSION_PROOF_INVALID", "硬件智能体会话凭证无效")
|
||
|
|
case errors.Is(err, orchestration.ErrHardwareEventConflict), errors.Is(err, orchestration.ErrConversationStateConflict):
|
||
|
|
WriteError(writer, request, http.StatusConflict, "HARDWARE_AGENT_EVENT_CONFLICT", "硬件智能体事件与当前会话状态冲突")
|
||
|
|
default:
|
||
|
|
WriteAPIError(writer, request, http.StatusServiceUnavailable, Error{Code: "HARDWARE_AGENT_EVENT_UNAVAILABLE", Message: "暂时无法处理硬件智能体事件", Retryable: true})
|
||
|
|
}
|
||
|
|
return
|
||
|
|
}
|
||
|
|
WriteJSON(writer, request, http.StatusOK, response)
|
||
|
|
})
|
||
|
|
}
|