package runtime 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" svc "code.tczkiot.com/wlw/ai-agent/internal/services" ) const ( missingBusinessIdentityReply = "暂未识别到您要咨询的业务对象。\n\n" + "卡板用户:请发送“卡号 + 您的卡号”\n" + "设备用户:请发送“设备号 + 您的设备号”\n" + "商城用户:请先登录商城,再从商城的客服入口进入。\n\n" + "识别成功后即可继续查询;需要人工协助可回复“人工客服”。" invalidBusinessIdentityReply = "没有查询到您发送的卡号或设备号,请核对后重新发送。\n\n" + "卡板用户:发送“卡号 + 您的卡号”\n" + "设备用户:发送“设备号 + 您的设备号”\n" + "商城用户:请登录商城后从客服入口进入。" identityLookupFailedReply = "业务身份识别暂时不可用,请稍后重试,或回复“人工客服”。" ) var businessIdentifierPattern = regexp.MustCompile(`[A-Za-z0-9][A-Za-z0-9:_-]{5,63}`) type guestBusinessIdentityResolution struct { Conversation models.Conversation NeedsPrompt bool CandidateProvided bool } func resolveGuestBusinessIdentity(ctx context.Context, conversation models.Conversation, message models.Message) (guestBusinessIdentityResolution, error) { resolution := guestBusinessIdentityResolution{Conversation: conversation} messageContent := businessIdentityMessageContent(message) if isBoundBusinessCustomerType(conversation.CustomerType) || isHumanHandoffMessage(messageContent) { return resolution, nil } currentCandidates, currentCandidateProvided := businessIdentityCandidates(messageContent) resolution.CandidateProvided = currentCandidateProvided if subject, ok, err := resolveBusinessSubject(ctx, messageContent, currentCandidates); err != nil { return resolution, err } else if ok { resolution.Conversation = conversationWithBusinessSubject(conversation, subject) return resolution, nil } history, _, _ := svc.MessageService.FindByConversationIDCursor( conversation.ID, 0, 20, string(enums.IMSenderTypeCustomer), "", ) for index := len(history) - 1; index >= 0; index-- { item := history[index] if item.ID == message.ID { continue } if item.MessageType != enums.IMMessageTypeText && item.MessageType != enums.IMMessageTypeHTML { continue } content := businessIdentityMessageContent(item) candidates, _ := businessIdentityCandidates(content) if subject, ok, err := resolveBusinessSubject(ctx, content, candidates); err != nil { return resolution, err } else if ok { resolution.Conversation = conversationWithBusinessSubject(conversation, subject) return resolution, nil } } resolution.NeedsPrompt = true return resolution, nil } func businessIdentityMessageContent(message models.Message) string { return utils.BuildRuntimeMessageText(message.MessageType, message.Content) } func isBoundBusinessCustomerType(customerType string) bool { switch identity.SubjectType(strings.TrimSpace(customerType)) { case identity.SubjectCard, identity.SubjectDevice, identity.SubjectMallUser: return true default: return false } } func isHumanHandoffMessage(content string) bool { content = strings.ToLower(strings.TrimSpace(content)) return strings.Contains(content, "人工") || strings.Contains(content, "转接客服") || strings.Contains(content, "human agent") } func businessIdentityCandidates(content string) ([]string, bool) { content = strings.TrimSpace(content) if content == "" { return nil, false } candidates := businessIdentifierPattern.FindAllString(content, -1) candidates = slices.Compact(candidates) 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 } return candidates, explicit } func resolveBusinessSubject(ctx context.Context, content string, candidates []string) (identity.Subject, bool, error) { if len(candidates) == 0 { return identity.Subject{}, false, nil } types := []identity.SubjectType{identity.SubjectCard, identity.SubjectDevice} lower := strings.ToLower(content) switch { case strings.Contains(content, "设备") || strings.Contains(lower, "imei"): types = []identity.SubjectType{identity.SubjectDevice} case strings.Contains(content, "卡号") || strings.Contains(lower, "iccid"): types = []identity.SubjectType{identity.SubjectCard} } for _, candidate := range candidates { for _, subjectType := range types { subjects, err := svc.SubjectService.Query(ctx, identity.Query{ Types: []identity.SubjectType{subjectType}, Keyword: candidate, EnabledOnly: true, }) if err != nil { return identity.Subject{}, false, err } for _, subject := range subjects { if businessSubjectMatchesIdentifier(subject, candidate) { return subject, true, nil } } } } return identity.Subject{}, false, nil } func businessSubjectMatchesIdentifier(subject identity.Subject, candidate string) bool { candidate = strings.TrimSpace(candidate) return strings.EqualFold(strings.TrimSpace(subject.Identifier), candidate) || strings.EqualFold(strings.TrimSpace(subject.Username), candidate) } func conversationWithBusinessSubject(conversation models.Conversation, subject identity.Subject) models.Conversation { conversation.CustomerType = string(subject.Type) conversation.CustomerID = subject.ID externalID := strings.TrimSpace(subject.Identifier) if subject.Type == identity.SubjectCard && strings.TrimSpace(subject.Username) != "" { externalID = strings.TrimSpace(subject.Username) } conversation.CustomerExternalID = externalID conversation.CustomerName = strings.TrimSpace(subject.Name) return conversation } func guestBusinessIdentityPrompt(resolution guestBusinessIdentityResolution, err error) string { if err != nil { return identityLookupFailedReply } if resolution.CandidateProvided { return invalidBusinessIdentityReply } return missingBusinessIdentityReply }