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

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