feat: expand companion platform, device controls, and memoir workflows
ci / verify (push) Canceled after 0s
ci / verify (push) Canceled after 0s
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.
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"data-terminal-cloud/internal/concurrency"
|
||||
"data-terminal-cloud/internal/devicebinding"
|
||||
"data-terminal-cloud/internal/deviceconfig"
|
||||
"data-terminal-cloud/internal/identity"
|
||||
"data-terminal-cloud/internal/pagination"
|
||||
"data-terminal-cloud/internal/saas"
|
||||
@@ -421,6 +422,25 @@ func GetSubjectHandler(service *saas.Service) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func EnsureSubjectVoiceAgentHandler(service *saas.Service) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
principal, ok := resourcePrincipal(w, r, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
info, err := service.EnsureSubjectVoiceAgent(r.Context(), principal, r.PathValue("id"))
|
||||
if err != nil {
|
||||
if info.Status == "FAILED" {
|
||||
WriteAPIError(w, r, http.StatusBadGateway, Error{Code: "VOICE_AGENT_CREATE_FAILED", Message: info.Message})
|
||||
return
|
||||
}
|
||||
writeSaaSError(w, r, err)
|
||||
return
|
||||
}
|
||||
WriteJSON(w, r, http.StatusOK, info)
|
||||
})
|
||||
}
|
||||
|
||||
func GetConversationReadinessHandler(service *saas.Service) http.Handler {
|
||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
principal, ok := resourcePrincipal(writer, request, "id")
|
||||
@@ -907,7 +927,7 @@ func PatchDeviceTwinHandler(service *saas.Service) http.Handler {
|
||||
writeValidation(writer, request, "设备配置包含禁止字段", FieldError{Field: field, Message: "不得下发密钥、令牌、口令或凭据"})
|
||||
return
|
||||
}
|
||||
allowed := map[string]bool{"volume": true, "interaction": true, "upload": true, "rtc": true, "policy_refs": true}
|
||||
allowed := map[string]bool{"power": true, "identity": true, "volume": true, "interaction": true, "upload": true, "rtc": true, "policy_refs": true}
|
||||
for key := range input.DesiredConfig {
|
||||
if !allowed[key] {
|
||||
writeValidation(writer, request, "设备配置参数不正确", FieldError{Field: "desired_config." + key, Message: "不是允许下发的非敏感配置分类"})
|
||||
@@ -1398,6 +1418,21 @@ func DeleteMemoryHandler(service *saas.Service) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func ListHouseholdPrivacyRequestsHandler(service *saas.Service) http.Handler {
|
||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
principal, ok := resourcePrincipal(writer, request, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
items, err := service.ListHouseholdPrivacyRequests(request.Context(), principal, request.PathValue("id"))
|
||||
if err != nil {
|
||||
writeSaaSError(writer, request, err)
|
||||
return
|
||||
}
|
||||
WriteJSON(writer, request, http.StatusOK, map[string]any{"items": items})
|
||||
})
|
||||
}
|
||||
|
||||
func ListPrivacyRequestsHandler(service *saas.Service) http.Handler {
|
||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
principal, ok := resourcePrincipal(writer, request, "id")
|
||||
@@ -1703,6 +1738,7 @@ func validateSubjectContext(profile, preferences map[string]any) []FieldError {
|
||||
result = append(result, validateOptionalContextList(profile, "interests", "profile_data.interests", 8, 50)...)
|
||||
result = append(result, validateOptionalContextString(preferences, "conversation_goal", "conversation_preferences.conversation_goal", 200)...)
|
||||
result = append(result, validateOptionalContextList(preferences, "preferred_topics", "conversation_preferences.preferred_topics", 8, 50)...)
|
||||
result = append(result, validateOptionalContextList(preferences, "avoided_topics", "conversation_preferences.avoided_topics", 8, 50)...)
|
||||
if preferences != nil {
|
||||
if raw, exists := preferences["pace"]; exists {
|
||||
pace, ok := raw.(string)
|
||||
@@ -1846,6 +1882,20 @@ func forbiddenDeviceConfigField(value any, path string) (string, bool) {
|
||||
}
|
||||
|
||||
func validateDesiredDeviceConfig(config map[string]any) []FieldError {
|
||||
if value, exists := config["power"]; exists {
|
||||
object, ok := value.(map[string]any)
|
||||
mode, isString := object["mode"].(string)
|
||||
if !ok || len(object) != 1 || !isString || (mode != "active" && mode != "standby") {
|
||||
return []FieldError{{Field: "desired_config.power.mode", Message: "必须是active或standby"}}
|
||||
}
|
||||
}
|
||||
if value, exists := config["identity"]; exists {
|
||||
object, ok := value.(map[string]any)
|
||||
name, isString := object["display_name"].(string)
|
||||
if !ok || len(object) != 1 || !isString || !deviceconfig.ValidDisplayName(name) {
|
||||
return []FieldError{{Field: "desired_config.identity.display_name", Message: "名称最多96字节,不含首尾空白或控制字符;留空恢复默认名称"}}
|
||||
}
|
||||
}
|
||||
volume, exists := config["volume"]
|
||||
if !exists {
|
||||
return nil
|
||||
@@ -1883,6 +1933,8 @@ func writeETagError(writer http.ResponseWriter, request *http.Request, err error
|
||||
|
||||
func writeSaaSError(writer http.ResponseWriter, request *http.Request, err error) {
|
||||
switch {
|
||||
case errors.Is(err, saas.ErrDevicePowerUnsupported):
|
||||
WriteError(writer, request, http.StatusConflict, "DEVICE_POWER_UNSUPPORTED", "设备尚未确认支持远程待机,请先更新固件")
|
||||
case errors.Is(err, devicebinding.ErrConfirmationRequired):
|
||||
WriteError(writer, request, http.StatusConflict, "DEVICE_CONFIRMATION_REQUIRED", "请先授权解绑,再从设备设置入口申请并按键确认绑定")
|
||||
case errors.Is(err, saas.ErrAccessDenied):
|
||||
@@ -1895,6 +1947,8 @@ func writeSaaSError(writer http.ResponseWriter, request *http.Request, err error
|
||||
WriteError(writer, request, http.StatusConflict, "EFFECTIVE_CONSENT_EXISTS", "该类型已有当前有效的授权")
|
||||
case errors.Is(err, saas.ErrPolicyVersionMismatch):
|
||||
WriteError(writer, request, http.StatusConflict, "POLICY_VERSION_MISMATCH", "授权政策版本已经更新,请重新阅读并确认")
|
||||
case errors.Is(err, saas.ErrInitialConsentInvalid):
|
||||
WriteError(writer, request, http.StatusBadRequest, "VALIDATION_FAILED", "请逐项选择授权并确认用途")
|
||||
case errors.Is(err, saas.ErrDeviceClaimInvalid):
|
||||
WriteError(writer, request, http.StatusConflict, "DEVICE_CLAIM_INVALID", "设备绑定码无效、已过期或已撤销")
|
||||
case errors.Is(err, saas.ErrDeviceClaimUsed):
|
||||
|
||||
Reference in New Issue
Block a user