package services import ( "context" "regexp" "slices" "strings" "code.tczkiot.com/wlw/ai-agent/identity" "code.tczkiot.com/wlw/ai-agent/internal/models" "code.tczkiot.com/wlw/ai-agent/internal/pkg/enums" "code.tczkiot.com/wlw/ai-agent/internal/pkg/utils" ) var quickActionBusinessIdentifierPattern = regexp.MustCompile(`[A-Za-z0-9][A-Za-z0-9:_-]{5,63}`) // resolveQuickActionConversation builds the business context used by the H5 // quick-service menu without changing the conversation's owner identity. Web // visitors must remain guests for authorization, while an already supplied or // previously recognised card/device number determines which actions are shown. func resolveQuickActionConversation(ctx context.Context, conversation *models.Conversation) *models.Conversation { if conversation == nil || quickActionBoundBusinessType(conversation.CustomerType) { return conversation } hints := quickActionIdentityHints(conversation) for _, hint := range hints { subject, ok, err := resolveQuickActionBusinessSubject(ctx, hint) if err != nil || !ok { continue } resolved := *conversation resolved.CustomerType = string(subject.Type) resolved.CustomerID = subject.ID resolved.CustomerExternalID = strings.TrimSpace(subject.Identifier) if subject.Type == identity.SubjectCard && strings.TrimSpace(subject.Username) != "" { resolved.CustomerExternalID = strings.TrimSpace(subject.Username) } resolved.CustomerName = strings.TrimSpace(subject.Name) return &resolved } return conversation } type quickActionIdentityHint struct { identifier string types []identity.SubjectType } func quickActionIdentityHints(conversation *models.Conversation) []quickActionIdentityHint { if conversation == nil { return nil } hints := make([]quickActionIdentityHint, 0, 4) if value, ok := strings.CutPrefix(strings.TrimSpace(conversation.CustomerExternalID), "card:"); ok && strings.TrimSpace(value) != "" { hints = append(hints, quickActionIdentityHint{identifier: strings.TrimSpace(value), types: []identity.SubjectType{identity.SubjectCard}}) } if value, ok := strings.CutPrefix(strings.TrimSpace(conversation.CustomerExternalID), "device:"); ok && strings.TrimSpace(value) != "" { hints = append(hints, quickActionIdentityHint{identifier: strings.TrimSpace(value), types: []identity.SubjectType{identity.SubjectDevice}}) } history, _, _ := MessageService.FindByConversationIDCursor( conversation.ID, 0, 20, string(enums.IMSenderTypeCustomer), "", ) for index := len(history) - 1; index >= 0; index-- { item := history[index] if item.MessageType != enums.IMMessageTypeText && item.MessageType != enums.IMMessageTypeHTML { continue } content := strings.TrimSpace(utils.BuildRuntimeMessageText(item.MessageType, item.Content)) candidates, explicit, types := quickActionBusinessCandidates(content) if !explicit { continue } for _, candidate := range candidates { hints = append(hints, quickActionIdentityHint{identifier: candidate, types: types}) } } return hints } func quickActionBusinessCandidates(content string) ([]string, bool, []identity.SubjectType) { content = strings.TrimSpace(content) if content == "" { return nil, false, nil } candidates := slices.Compact(quickActionBusinessIdentifierPattern.FindAllString(content, -1)) lower := strings.ToLower(content) explicit := strings.Contains(content, "卡号") || strings.Contains(content, "卡板") || strings.Contains(content, "设备号") || strings.Contains(lower, "iccid") || strings.Contains(lower, "imei") if len(candidates) == 1 && candidates[0] == content { explicit = true } types := []identity.SubjectType{identity.SubjectCard, identity.SubjectDevice} switch { case strings.Contains(content, "设备") || strings.Contains(lower, "imei"): types = []identity.SubjectType{identity.SubjectDevice} case strings.Contains(content, "卡号") || strings.Contains(content, "卡板") || strings.Contains(lower, "iccid"): types = []identity.SubjectType{identity.SubjectCard} } return candidates, explicit, types } func resolveQuickActionBusinessSubject(ctx context.Context, hint quickActionIdentityHint) (identity.Subject, bool, error) { for _, subjectType := range hint.types { subjects, err := SubjectService.Query(ctx, identity.Query{ Types: []identity.SubjectType{subjectType}, Keyword: hint.identifier, EnabledOnly: true, }) if err != nil { return identity.Subject{}, false, err } for _, subject := range subjects { if subject.Type != subjectType || !subject.Enabled { continue } if strings.EqualFold(strings.TrimSpace(subject.Identifier), hint.identifier) || strings.EqualFold(strings.TrimSpace(subject.Username), hint.identifier) { return subject, true, nil } } } return identity.Subject{}, false, nil } func quickActionBoundBusinessType(customerType string) bool { switch identity.SubjectType(strings.TrimSpace(customerType)) { case identity.SubjectCard, identity.SubjectDevice, identity.SubjectMallUser: return true default: return false } }