Files
story/internal/httpapi/hardware_agent_handlers.go
T
Codex Local bf1233d5df
ci / verify (push) Canceled after 0s
feat: expand companion platform, device controls, and memoir workflows
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
2026-10-08 10:06:44 +08:00

220 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/devicebinding"
"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, devicebinding.ErrConfirmationRequired):
WriteError(writer, request, http.StatusConflict, "ONBOARDING_ACTIVATION_REQUIRED", "请通过首次接入激活任务获取运行配置")
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", "first_audio_received":
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), errors.Is(err, orchestration.ErrOnboardingState):
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)
})
}