feat: establish v0.70 platform milestone
ci / verify (push) Canceled after 0s

This commit is contained in:
Codex Local
2026-08-29 21:56:51 +08:00
parent d97bbf7cf5
commit e652516147
133 changed files with 12907 additions and 801 deletions
+268 -12
View File
@@ -729,6 +729,9 @@ func ClaimDeviceHandler(service *saas.Service) http.Handler {
if len(input.ClaimCode) < 16 || len(input.ClaimCode) > 128 {
fieldErrors = append(fieldErrors, FieldError{Field: "claim_code", Message: "长度必须为16至128字符"})
}
if validUUID(input.ClaimCode) {
fieldErrors = append(fieldErrors, FieldError{Field: "claim_code", Message: "不能填写设备编号,请输入设备屏幕或维护端生成的一次性绑定码"})
}
if !validUUID(input.HouseholdID) {
fieldErrors = append(fieldErrors, FieldError{Field: "household_id", Message: "必须是UUID"})
}
@@ -766,6 +769,86 @@ func GetManagedDeviceHandler(service *saas.Service) http.Handler {
})
}
func GetDeviceBindingImpactHandler(service *saas.Service) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
principal, ok := resourcePrincipal(writer, request, "id")
if !ok {
return
}
action := strings.TrimSpace(request.URL.Query().Get("action"))
targetSubjectID := strings.TrimSpace(request.URL.Query().Get("target_subject_id"))
fieldErrors := make([]FieldError, 0)
if action != saas.DeviceBindingActionUnbind && action != saas.DeviceBindingActionTransfer {
fieldErrors = append(fieldErrors, FieldError{Field: "action", Message: "必须是unbind或transfer"})
}
if action == saas.DeviceBindingActionTransfer && !validUUID(targetSubjectID) {
fieldErrors = append(fieldErrors, FieldError{Field: "target_subject_id", Message: "转移时必须提供目标长者UUID"})
}
if len(fieldErrors) > 0 {
WriteAPIError(writer, request, http.StatusBadRequest, Error{Code: "VALIDATION_FAILED", Message: "设备绑定影响检查参数不正确", FieldErrors: fieldErrors})
return
}
result, err := service.GetDeviceBindingImpact(request.Context(), principal, request.PathValue("id"), action, targetSubjectID)
if err != nil {
writeSaaSError(writer, request, err)
return
}
writer.Header().Set("Cache-Control", "no-store")
WriteJSON(writer, request, http.StatusOK, result)
})
}
func ChangeDeviceBindingHandler(service *saas.Service) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
principal, ok := resourcePrincipal(writer, request, "id")
if !ok {
return
}
var input struct {
Action string `json:"action"`
ExpectedBindingID string `json:"expected_binding_id"`
TargetSubjectID string `json:"target_subject_id"`
Reason string `json:"reason"`
Acknowledged bool `json:"acknowledged"`
}
if !decodeWebJSON(writer, request, &input, "设备绑定变更参数不正确") {
return
}
input.Action = strings.TrimSpace(input.Action)
input.ExpectedBindingID = strings.TrimSpace(input.ExpectedBindingID)
input.TargetSubjectID = strings.TrimSpace(input.TargetSubjectID)
input.Reason = strings.TrimSpace(input.Reason)
fieldErrors := make([]FieldError, 0)
if input.Action != saas.DeviceBindingActionUnbind && input.Action != saas.DeviceBindingActionTransfer {
fieldErrors = append(fieldErrors, FieldError{Field: "action", Message: "必须是unbind或transfer"})
}
if !validUUID(input.ExpectedBindingID) {
fieldErrors = append(fieldErrors, FieldError{Field: "expected_binding_id", Message: "必须是影响检查返回的当前绑定UUID"})
}
if input.Action == saas.DeviceBindingActionTransfer && !validUUID(input.TargetSubjectID) {
fieldErrors = append(fieldErrors, FieldError{Field: "target_subject_id", Message: "转移时必须提供目标长者UUID"})
}
if len(input.Reason) < 3 || len(input.Reason) > 300 {
fieldErrors = append(fieldErrors, FieldError{Field: "reason", Message: "长度必须为3至300字符"})
}
if !input.Acknowledged {
fieldErrors = append(fieldErrors, FieldError{Field: "acknowledged", Message: "必须确认已核对影响范围"})
}
if len(fieldErrors) > 0 {
WriteAPIError(writer, request, http.StatusBadRequest, Error{Code: "VALIDATION_FAILED", Message: "设备绑定变更参数不正确", FieldErrors: fieldErrors})
return
}
result, err := service.ChangeDeviceBinding(request.Context(), principal, request.PathValue("id"), RequestID(request.Context()), saas.ChangeDeviceBindingInput{
Action: input.Action, ExpectedBindingID: input.ExpectedBindingID, TargetSubjectID: input.TargetSubjectID, Reason: input.Reason, Acknowledged: input.Acknowledged,
})
if err != nil {
writeSaaSError(writer, request, err)
return
}
WriteJSON(writer, request, http.StatusOK, result)
})
}
func GetDevicePresenceHandler(service *saas.Service) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
principal, ok := resourcePrincipal(writer, request, "id")
@@ -1009,21 +1092,104 @@ func PatchStoryHandler(service *saas.Service) http.Handler {
return
}
var input struct {
Content string `json:"content"`
Reason string `json:"reason"`
Content string `json:"content"`
Reason string `json:"reason"`
ErrorType string `json:"error_type"`
}
if !decodeWebJSON(writer, request, &input, "故事修订参数不正确") {
return
}
input.Content = strings.TrimSpace(input.Content)
input.Reason = strings.TrimSpace(input.Reason)
input.ErrorType = strings.TrimSpace(input.ErrorType)
if input.Content == "" || len([]rune(input.Content)) > 50000 || input.Reason == "" || len([]rune(input.Reason)) > 500 {
writeValidation(writer, request, "故事修订参数不正确",
FieldError{Field: "content", Message: "不能为空且最长50000字符"},
FieldError{Field: "reason", Message: "不能为空且最长500字符"})
return
}
result, err := service.ReviseStory(request.Context(), principal, request.PathValue("id"), input.Content, input.Reason, RequestID(request.Context()), expected)
if input.ErrorType != "" && !saas.IsContentErrorType(input.ErrorType) {
writeValidation(writer, request, "故事修订参数不正确", FieldError{Field: "error_type", Message: "质量问题类型不正确"})
return
}
result, err := service.ReviseStory(request.Context(), principal, request.PathValue("id"), input.Content, input.Reason, RequestID(request.Context()), expected, input.ErrorType)
if err != nil {
writeSaaSError(writer, request, err)
return
}
writer.Header().Set("ETag", concurrency.FormatETag(result.Version))
WriteJSON(writer, request, http.StatusOK, result)
})
}
func UpdateFactCandidateHandler(service *saas.Service) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
principal, ok := resourcePrincipal(writer, request, "id")
if !ok {
return
}
if !validUUID(request.PathValue("fact_id")) {
writeValidation(writer, request, "事实候选编号不正确", FieldError{Field: "fact_id", Message: "必须是UUID"})
return
}
expected, err := concurrency.ParseIfMatch(request)
if err != nil {
writeETagError(writer, request, err)
return
}
var input struct {
FactType *string `json:"fact_type"`
Content *string `json:"content"`
Sensitivity *string `json:"sensitivity"`
Status *string `json:"status"`
ErrorType *string `json:"error_type"`
Reason string `json:"reason"`
}
if !decodeWebJSON(writer, request, &input, "事实候选审核参数不正确") {
return
}
input.Reason = strings.TrimSpace(input.Reason)
fieldErrors := make([]FieldError, 0)
if input.Reason == "" || len([]rune(input.Reason)) > 500 {
fieldErrors = append(fieldErrors, FieldError{Field: "reason", Message: "不能为空且最长500字符"})
}
if input.FactType != nil {
value := strings.TrimSpace(*input.FactType)
input.FactType = &value
if value == "" || len(value) > 64 {
fieldErrors = append(fieldErrors, FieldError{Field: "fact_type", Message: "不能为空且最长64字符"})
}
}
if input.Content != nil {
value := strings.TrimSpace(*input.Content)
input.Content = &value
if value == "" || len([]rune(value)) > 4000 {
fieldErrors = append(fieldErrors, FieldError{Field: "content", Message: "不能为空且最长4000字符"})
}
}
if input.Sensitivity != nil && *input.Sensitivity != "normal" && *input.Sensitivity != "sensitive" && *input.Sensitivity != "high" {
fieldErrors = append(fieldErrors, FieldError{Field: "sensitivity", Message: "必须为normal、sensitive或high"})
}
if input.Status != nil && *input.Status != "CANDIDATE" && *input.Status != "CONFIRMED" && *input.Status != "REJECTED" && *input.Status != "SUPERSEDED" {
fieldErrors = append(fieldErrors, FieldError{Field: "status", Message: "事实候选状态不正确"})
}
if input.ErrorType != nil {
value := strings.TrimSpace(*input.ErrorType)
input.ErrorType = &value
if value == "" || !saas.IsContentErrorType(value) {
fieldErrors = append(fieldErrors, FieldError{Field: "error_type", Message: "质量问题类型不正确"})
}
}
if input.FactType == nil && input.Content == nil && input.Sensitivity == nil && input.Status == nil {
fieldErrors = append(fieldErrors, FieldError{Field: "body", Message: "至少提供一个需要修改或审核的字段"})
}
if len(fieldErrors) > 0 {
WriteAPIError(writer, request, http.StatusBadRequest, Error{Code: "VALIDATION_FAILED", Message: "事实候选审核参数不正确", FieldErrors: fieldErrors})
return
}
result, err := service.UpdateFactCandidate(request.Context(), principal, request.PathValue("id"), request.PathValue("fact_id"), RequestID(request.Context()), expected, saas.UpdateFactCandidateInput{
FactType: input.FactType, Content: input.Content, Sensitivity: input.Sensitivity, Status: input.Status, ErrorType: input.ErrorType, Reason: input.Reason,
})
if err != nil {
writeSaaSError(writer, request, err)
return
@@ -1053,16 +1219,22 @@ func storyDecisionHandler(service *saas.Service, confirm bool) http.Handler {
return
}
var input struct {
Reason string `json:"reason"`
Reason string `json:"reason"`
ErrorType string `json:"error_type"`
}
if !decodeWebJSON(writer, request, &input, "故事审核参数不正确") {
return
}
input.Reason = strings.TrimSpace(input.Reason)
input.ErrorType = strings.TrimSpace(input.ErrorType)
if input.Reason == "" || len([]rune(input.Reason)) > 500 {
writeValidation(writer, request, "故事审核参数不正确", FieldError{Field: "reason", Message: "不能为空且最长500字符"})
return
}
if input.ErrorType != "" && !saas.IsContentErrorType(input.ErrorType) {
writeValidation(writer, request, "故事审核参数不正确", FieldError{Field: "error_type", Message: "质量问题类型不正确"})
return
}
if confirm {
story, memory, err := service.ConfirmStory(request.Context(), principal, request.PathValue("id"), input.Reason, RequestID(request.Context()), expected)
if err != nil {
@@ -1073,7 +1245,7 @@ func storyDecisionHandler(service *saas.Service, confirm bool) http.Handler {
WriteJSON(writer, request, http.StatusOK, map[string]any{"story": story, "memory": memory})
return
}
story, err := service.RejectStory(request.Context(), principal, request.PathValue("id"), input.Reason, RequestID(request.Context()), expected)
story, err := service.RejectStory(request.Context(), principal, request.PathValue("id"), input.Reason, RequestID(request.Context()), expected, input.ErrorType)
if err != nil {
writeSaaSError(writer, request, err)
return
@@ -1131,9 +1303,10 @@ func PatchMemoryHandler(service *saas.Service) http.Handler {
return
}
var input struct {
Content *string `json:"content"`
Status *string `json:"status"`
Reason string `json:"reason"`
Content *string `json:"content"`
Status *string `json:"status"`
ErrorType *string `json:"error_type"`
Reason string `json:"reason"`
}
if !decodeWebJSON(writer, request, &input, "记忆修改参数不正确") {
return
@@ -1153,6 +1326,13 @@ func PatchMemoryHandler(service *saas.Service) http.Handler {
if input.Status != nil && !oneOf(*input.Status, "ACTIVE", "DISABLED") {
fieldErrors = append(fieldErrors, FieldError{Field: "status", Message: "只允许ACTIVE或DISABLED"})
}
if input.ErrorType != nil {
value := strings.TrimSpace(*input.ErrorType)
input.ErrorType = &value
if value == "" || !saas.IsContentErrorType(value) {
fieldErrors = append(fieldErrors, FieldError{Field: "error_type", Message: "质量问题类型不正确"})
}
}
if input.Reason == "" || len([]rune(input.Reason)) > 500 {
fieldErrors = append(fieldErrors, FieldError{Field: "reason", Message: "不能为空且最长500字符"})
}
@@ -1160,7 +1340,7 @@ func PatchMemoryHandler(service *saas.Service) http.Handler {
WriteAPIError(writer, request, http.StatusBadRequest, Error{Code: "VALIDATION_FAILED", Message: "记忆修改参数不正确", FieldErrors: fieldErrors})
return
}
result, err := service.UpdateMemory(request.Context(), principal, request.PathValue("id"), RequestID(request.Context()), expected, saas.UpdateMemoryInput{Content: input.Content, Status: input.Status, Reason: input.Reason})
result, err := service.UpdateMemory(request.Context(), principal, request.PathValue("id"), RequestID(request.Context()), expected, saas.UpdateMemoryInput{Content: input.Content, Status: input.Status, ErrorType: input.ErrorType, Reason: input.Reason})
if err != nil {
writeSaaSError(writer, request, err)
return
@@ -1233,15 +1413,23 @@ func CreatePrivacyRequestHandler(service *saas.Service) http.Handler {
Reason string `json:"reason"`
Confirmation string `json:"confirmation"`
Scope struct {
Categories []string `json:"categories"`
Categories []string `json:"categories"`
ConsentTypes []string `json:"consent_types"`
Corrections struct {
PreferredName *string `json:"preferred_name"`
Language *string `json:"language"`
Timezone *string `json:"timezone"`
ProfileData *map[string]any `json:"profile_data"`
ConversationPreferences *map[string]any `json:"conversation_preferences"`
} `json:"corrections"`
} `json:"scope"`
}
if !decodeWebJSON(writer, request, &input, "隐私请求参数不正确") {
return
}
input.RequestType = strings.ToUpper(strings.TrimSpace(input.RequestType))
if input.RequestType != "ACCESS" && input.RequestType != "DELETE" {
writeValidation(writer, request, "隐私请求参数不正确", FieldError{Field: "request_type", Message: "必须为ACCESS或DELETE"})
if input.RequestType != "ACCESS" && input.RequestType != "CORRECTION" && input.RequestType != "CONSENT_WITHDRAWAL" && input.RequestType != "DELETE" {
writeValidation(writer, request, "隐私请求参数不正确", FieldError{Field: "request_type", Message: "必须为ACCESS、CORRECTION、CONSENT_WITHDRAWAL或DELETE"})
return
}
if input.RequestType == "DELETE" {
@@ -1274,6 +1462,68 @@ func CreatePrivacyRequestHandler(service *saas.Service) http.Handler {
if len(categories) > 0 {
scope["categories"] = categories
}
if input.RequestType == "CORRECTION" {
corrections := map[string]any{}
if input.Scope.Corrections.PreferredName != nil {
value := strings.TrimSpace(*input.Scope.Corrections.PreferredName)
if len([]rune(value)) < 1 || len([]rune(value)) > 120 {
writeValidation(writer, request, "更正申请参数不正确", FieldError{Field: "scope.corrections.preferred_name", Message: "长度必须为1至120字符"})
return
}
corrections["preferred_name"] = value
}
if input.Scope.Corrections.Language != nil {
value := strings.TrimSpace(*input.Scope.Corrections.Language)
if len(value) < 2 || len(value) > 20 {
writeValidation(writer, request, "更正申请参数不正确", FieldError{Field: "scope.corrections.language", Message: "长度必须为2至20字符"})
return
}
corrections["language"] = value
}
if input.Scope.Corrections.Timezone != nil {
value := strings.TrimSpace(*input.Scope.Corrections.Timezone)
if _, err := time.LoadLocation(value); err != nil {
writeValidation(writer, request, "更正申请参数不正确", FieldError{Field: "scope.corrections.timezone", Message: "必须是有效IANA时区"})
return
}
corrections["timezone"] = value
}
if input.Scope.Corrections.ProfileData != nil {
corrections["profile_data"] = *input.Scope.Corrections.ProfileData
}
if input.Scope.Corrections.ConversationPreferences != nil {
corrections["conversation_preferences"] = *input.Scope.Corrections.ConversationPreferences
}
if len(corrections) == 0 {
writeValidation(writer, request, "更正申请参数不正确", FieldError{Field: "scope.corrections", Message: "至少提供一个需要更正的字段"})
return
}
scope = map[string]any{"corrections": corrections}
}
if input.RequestType == "CONSENT_WITHDRAWAL" {
allowedConsentTypes := map[string]bool{
saas.ConsentTranscriptStorage: true, saas.ConsentRawAudioStorage: true, saas.ConsentStoryExtraction: true,
saas.ConsentPersonalMemory: true, saas.ConsentFamilySharing: true,
}
seenConsentTypes := map[string]bool{}
consentTypes := make([]string, 0, len(input.Scope.ConsentTypes))
for index, raw := range input.Scope.ConsentTypes {
consentType := strings.TrimSpace(raw)
if !allowedConsentTypes[consentType] {
writeValidation(writer, request, "授权撤回申请参数不正确", FieldError{Field: fmt.Sprintf("scope.consent_types[%d]", index), Message: "授权类型不正确"})
return
}
if !seenConsentTypes[consentType] {
seenConsentTypes[consentType] = true
consentTypes = append(consentTypes, consentType)
}
}
if len(consentTypes) == 0 {
writeValidation(writer, request, "授权撤回申请参数不正确", FieldError{Field: "scope.consent_types", Message: "至少选择一种授权"})
return
}
scope = map[string]any{"consent_types": consentTypes}
}
result, created, err := service.CreatePrivacyRequest(request.Context(), principal, request.PathValue("id"), idempotencyKey, RequestID(request.Context()), saas.CreatePrivacyRequestInput{RequestType: input.RequestType, Scope: scope, Reason: input.Reason, Confirmation: input.Confirmation})
if err != nil {
writeSaaSError(writer, request, err)
@@ -1633,6 +1883,10 @@ func writeSaaSError(writer http.ResponseWriter, request *http.Request, err error
WriteError(writer, request, http.StatusConflict, "DEVICE_CLAIM_ALREADY_USED", "设备绑定码已经用于其他绑定")
case errors.Is(err, saas.ErrDeviceAlreadyBound):
WriteError(writer, request, http.StatusConflict, "DEVICE_ALREADY_BOUND", "设备已经存在有效绑定")
case errors.Is(err, saas.ErrDeviceBindingBlocked):
WriteError(writer, request, http.StatusConflict, "DEVICE_BINDING_BLOCKED", "设备仍有活动通话、未完成命令或固件任务,请先处理阻断项")
case errors.Is(err, saas.ErrDeviceBindingStale):
WriteError(writer, request, http.StatusConflict, "DEVICE_BINDING_STALE", "设备绑定已发生变化,请重新执行影响检查")
case errors.Is(err, saas.ErrIdempotencyConflict):
WriteError(writer, request, http.StatusConflict, "IDEMPOTENCY_KEY_CONFLICT", "重复请求识别码已用于不同的请求内容")
case errors.Is(err, saas.ErrSubjectDeleteConflict):
@@ -1665,6 +1919,8 @@ func writeSaaSError(writer http.ResponseWriter, request *http.Request, err error
WriteError(writer, request, http.StatusConflict, "PERSONALIZED_MEMORY_CONSENT_REQUIRED", "生成个性化记忆前必须取得当前有效授权")
case errors.Is(err, saas.ErrReviewStateConflict):
WriteError(writer, request, http.StatusConflict, "STORY_REVIEW_STATE_CONFLICT", "当前故事审核状态不允许此操作")
case errors.Is(err, saas.ErrFactReviewStateConflict):
WriteError(writer, request, http.StatusConflict, "FACT_REVIEW_STATE_CONFLICT", "当前事实候选状态不允许此操作")
case errors.Is(err, pagination.ErrInvalidCursor):
writeValidation(writer, request, "分页游标无效", FieldError{Field: "cursor", Message: "游标无效、已损坏或不是本服务签发"})
default: