Initial qiwei secondary development handoff
This commit is contained in:
487
helper/auto_reply_handoff.go
Normal file
487
helper/auto_reply_handoff.go
Normal file
@@ -0,0 +1,487 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"qiweimanager/config"
|
||||
)
|
||||
|
||||
func (e *AutoReplyEngine) handoff(msg autoReplyMessage, reason string, hits []KnowledgeChunk) {
|
||||
e.handoffWithTimings(msg, reason, hits, autoReplyTimings{})
|
||||
}
|
||||
|
||||
func (e *AutoReplyEngine) handoffWithTimings(msg autoReplyMessage, reason string, hits []KnowledgeChunk, timings autoReplyTimings) {
|
||||
if msg.isInternalSender() {
|
||||
e.replyInternalNoHandoff(msg, reason, timings)
|
||||
return
|
||||
}
|
||||
if e.shouldHoldUnknownHandoff(msg) {
|
||||
e.replyUnknownNoHandoff(msg, reason, timings)
|
||||
return
|
||||
}
|
||||
if isManualHandoffReason(reason) {
|
||||
e.customerHandoffWithTimings(msg, reason, hits, timings)
|
||||
return
|
||||
}
|
||||
e.textHandoffWithTimings(msg, reason, reason, hits, timings, "")
|
||||
}
|
||||
|
||||
func (e *AutoReplyEngine) textHandoffWithTimings(msg autoReplyMessage, notificationReason string, recordReason string, hits []KnowledgeChunk, timings autoReplyTimings, cardStatus string) {
|
||||
if err := e.sendHandoffMessage(msg, notificationReason, hits); err != nil {
|
||||
e.setLastErrorWithScope(autoReplyErrorScopeHandoff, "转人工发送失败: "+err.Error())
|
||||
e.addRecord(AutoReplyRecord{
|
||||
RobotID: msg.RobotID,
|
||||
ClientID: msg.ClientID,
|
||||
UserID: msg.RobotID,
|
||||
ConversationID: msg.ConversationID,
|
||||
Source: msg.sourceLabel(),
|
||||
FromWxID: msg.FromWxID,
|
||||
FromNickName: msg.FromNickName,
|
||||
Question: msg.Content,
|
||||
Action: "failed",
|
||||
Reason: recordReason + "; handoff_failed: " + err.Error(),
|
||||
SenderIdentity: msg.SenderIdentity,
|
||||
IdentitySource: msg.IdentitySource,
|
||||
CardStatus: cardStatus,
|
||||
KeywordScore: timings.KeywordScore,
|
||||
VectorScore: timings.VectorScore,
|
||||
RerankScore: timings.RerankScore,
|
||||
RetrievalMode: timings.RetrievalMode,
|
||||
UsedKnowledgeSources: strings.Join(timings.UsedKnowledgeSources, ", "),
|
||||
KnowledgeDurationMS: timings.KnowledgeDurationMS,
|
||||
AIDurationMS: timings.AIDurationMS,
|
||||
TotalDurationMS: timings.TotalDurationMS,
|
||||
})
|
||||
return
|
||||
}
|
||||
e.markCooldown(msg)
|
||||
e.incStatus("handoff")
|
||||
score := 0.0
|
||||
if len(hits) > 0 {
|
||||
score = hits[0].Score
|
||||
}
|
||||
e.addRecord(AutoReplyRecord{
|
||||
RobotID: msg.RobotID,
|
||||
ClientID: msg.ClientID,
|
||||
UserID: msg.RobotID,
|
||||
ConversationID: msg.ConversationID,
|
||||
Source: msg.sourceLabel(),
|
||||
FromWxID: msg.FromWxID,
|
||||
FromNickName: msg.FromNickName,
|
||||
Question: msg.Content,
|
||||
Action: "handoff",
|
||||
Reason: recordReason,
|
||||
SenderIdentity: msg.SenderIdentity,
|
||||
IdentitySource: msg.IdentitySource,
|
||||
CardStatus: cardStatus,
|
||||
Score: score,
|
||||
KeywordScore: timings.KeywordScore,
|
||||
VectorScore: timings.VectorScore,
|
||||
RerankScore: timings.RerankScore,
|
||||
RetrievalMode: timings.RetrievalMode,
|
||||
UsedKnowledgeSources: strings.Join(timings.UsedKnowledgeSources, ", "),
|
||||
KnowledgeDurationMS: timings.KnowledgeDurationMS,
|
||||
KeywordDurationMS: timings.KeywordDurationMS,
|
||||
VectorDurationMS: timings.VectorDurationMS,
|
||||
RerankDurationMS: timings.RerankDurationMS,
|
||||
AIDurationMS: timings.AIDurationMS,
|
||||
TotalDurationMS: timings.TotalDurationMS,
|
||||
})
|
||||
}
|
||||
|
||||
func (e *AutoReplyEngine) customerHandoffWithTimings(msg autoReplyMessage, reason string, hits []KnowledgeChunk, timings autoReplyTimings) {
|
||||
cardResult := e.sendHandoffCards(msg)
|
||||
recordReason := reason
|
||||
if suffix := cardResult.reasonSuffix(); suffix != "" {
|
||||
recordReason += "; " + suffix
|
||||
}
|
||||
e.textHandoffWithTimings(msg, reason, recordReason, hits, timings, cardResult.summary())
|
||||
}
|
||||
|
||||
func (e *AutoReplyEngine) replyUnknownNoHandoff(msg autoReplyMessage, originalReason string, timings autoReplyTimings) {
|
||||
cfg := e.getConfig()
|
||||
e.startUnknownIdentityLookup(msg, originalReason)
|
||||
answer := strings.TrimSpace(cfg.Identity.UnknownNoHandoffReply)
|
||||
if answer == "" {
|
||||
answer = "正在核验联系人身份,暂不触发转人工。如需协助请直接联系对应同事。"
|
||||
}
|
||||
reason := "identity_unknown_no_handoff"
|
||||
if originalReason != "" {
|
||||
reason += "; original_reason: " + originalReason
|
||||
}
|
||||
e.noteReason("identity_unknown_no_handoff")
|
||||
if err := sendAutoReplyText(uint32(msg.ClientID), msg.ConversationID, answer); err != nil {
|
||||
e.setLastErrorWithScope(autoReplyErrorScopeIdentity, "未知身份拦截回复失败: "+err.Error())
|
||||
e.addRecord(AutoReplyRecord{
|
||||
RobotID: msg.RobotID,
|
||||
ClientID: msg.ClientID,
|
||||
UserID: msg.RobotID,
|
||||
ConversationID: msg.ConversationID,
|
||||
Source: msg.sourceLabel(),
|
||||
FromWxID: msg.FromWxID,
|
||||
FromNickName: msg.FromNickName,
|
||||
Question: msg.Content,
|
||||
Action: "failed",
|
||||
Reason: reason + "; send_unknown_no_handoff_failed: " + err.Error(),
|
||||
SenderIdentity: msg.SenderIdentity,
|
||||
IdentitySource: msg.IdentitySource,
|
||||
KeywordScore: timings.KeywordScore,
|
||||
VectorScore: timings.VectorScore,
|
||||
RerankScore: timings.RerankScore,
|
||||
RetrievalMode: timings.RetrievalMode,
|
||||
UsedKnowledgeSources: strings.Join(timings.UsedKnowledgeSources, ", "),
|
||||
KnowledgeDurationMS: timings.KnowledgeDurationMS,
|
||||
KeywordDurationMS: timings.KeywordDurationMS,
|
||||
VectorDurationMS: timings.VectorDurationMS,
|
||||
RerankDurationMS: timings.RerankDurationMS,
|
||||
AIDurationMS: timings.AIDurationMS,
|
||||
TotalDurationMS: timings.TotalDurationMS,
|
||||
})
|
||||
return
|
||||
}
|
||||
e.rememberAutoSentMessage(uint32(msg.ClientID), msg.ConversationID, answer)
|
||||
e.markCooldown(msg)
|
||||
e.incStatus("replied")
|
||||
e.addRecord(AutoReplyRecord{
|
||||
RobotID: msg.RobotID,
|
||||
ClientID: msg.ClientID,
|
||||
UserID: msg.RobotID,
|
||||
ConversationID: msg.ConversationID,
|
||||
Source: msg.sourceLabel(),
|
||||
FromWxID: msg.FromWxID,
|
||||
FromNickName: msg.FromNickName,
|
||||
Question: msg.Content,
|
||||
Action: "replied",
|
||||
Reason: reason,
|
||||
Answer: answer,
|
||||
SenderIdentity: msg.SenderIdentity,
|
||||
IdentitySource: msg.IdentitySource,
|
||||
KeywordScore: timings.KeywordScore,
|
||||
VectorScore: timings.VectorScore,
|
||||
RerankScore: timings.RerankScore,
|
||||
RetrievalMode: timings.RetrievalMode,
|
||||
UsedKnowledgeSources: strings.Join(timings.UsedKnowledgeSources, ", "),
|
||||
KnowledgeDurationMS: timings.KnowledgeDurationMS,
|
||||
KeywordDurationMS: timings.KeywordDurationMS,
|
||||
VectorDurationMS: timings.VectorDurationMS,
|
||||
RerankDurationMS: timings.RerankDurationMS,
|
||||
AIDurationMS: timings.AIDurationMS,
|
||||
TotalDurationMS: timings.TotalDurationMS,
|
||||
})
|
||||
}
|
||||
|
||||
func (e *AutoReplyEngine) replyInternalNoHandoff(msg autoReplyMessage, originalReason string, timings autoReplyTimings) {
|
||||
cfg := e.getConfig()
|
||||
answer := strings.TrimSpace(cfg.Identity.InternalNoHandoffReply)
|
||||
if answer == "" {
|
||||
answer = "内部员工消息不触发转人工,如需协助请直接联系对应同事。"
|
||||
}
|
||||
reason := "internal_employee_no_handoff"
|
||||
if originalReason != "" {
|
||||
reason += "; original_reason: " + originalReason
|
||||
}
|
||||
e.noteReason("internal_employee_no_handoff")
|
||||
if err := sendAutoReplyText(uint32(msg.ClientID), msg.ConversationID, answer); err != nil {
|
||||
e.setLastErrorWithScope(autoReplyErrorScopeIdentity, "内部员工拦截回复失败: "+err.Error())
|
||||
e.addRecord(AutoReplyRecord{
|
||||
RobotID: msg.RobotID,
|
||||
ClientID: msg.ClientID,
|
||||
UserID: msg.RobotID,
|
||||
ConversationID: msg.ConversationID,
|
||||
Source: msg.sourceLabel(),
|
||||
FromWxID: msg.FromWxID,
|
||||
FromNickName: msg.FromNickName,
|
||||
Question: msg.Content,
|
||||
Action: "failed",
|
||||
Reason: reason + "; send_internal_no_handoff_failed: " + err.Error(),
|
||||
SenderIdentity: msg.SenderIdentity,
|
||||
IdentitySource: msg.IdentitySource,
|
||||
KeywordScore: timings.KeywordScore,
|
||||
VectorScore: timings.VectorScore,
|
||||
RerankScore: timings.RerankScore,
|
||||
RetrievalMode: timings.RetrievalMode,
|
||||
UsedKnowledgeSources: strings.Join(timings.UsedKnowledgeSources, ", "),
|
||||
KnowledgeDurationMS: timings.KnowledgeDurationMS,
|
||||
KeywordDurationMS: timings.KeywordDurationMS,
|
||||
VectorDurationMS: timings.VectorDurationMS,
|
||||
RerankDurationMS: timings.RerankDurationMS,
|
||||
AIDurationMS: timings.AIDurationMS,
|
||||
TotalDurationMS: timings.TotalDurationMS,
|
||||
})
|
||||
return
|
||||
}
|
||||
e.rememberAutoSentMessage(uint32(msg.ClientID), msg.ConversationID, answer)
|
||||
e.markCooldown(msg)
|
||||
e.incStatus("replied")
|
||||
e.addRecord(AutoReplyRecord{
|
||||
RobotID: msg.RobotID,
|
||||
ClientID: msg.ClientID,
|
||||
UserID: msg.RobotID,
|
||||
ConversationID: msg.ConversationID,
|
||||
Source: msg.sourceLabel(),
|
||||
FromWxID: msg.FromWxID,
|
||||
FromNickName: msg.FromNickName,
|
||||
Question: msg.Content,
|
||||
Action: "replied",
|
||||
Reason: reason,
|
||||
Answer: answer,
|
||||
SenderIdentity: msg.SenderIdentity,
|
||||
IdentitySource: msg.IdentitySource,
|
||||
KeywordScore: timings.KeywordScore,
|
||||
VectorScore: timings.VectorScore,
|
||||
RerankScore: timings.RerankScore,
|
||||
RetrievalMode: timings.RetrievalMode,
|
||||
UsedKnowledgeSources: strings.Join(timings.UsedKnowledgeSources, ", "),
|
||||
KnowledgeDurationMS: timings.KnowledgeDurationMS,
|
||||
KeywordDurationMS: timings.KeywordDurationMS,
|
||||
VectorDurationMS: timings.VectorDurationMS,
|
||||
RerankDurationMS: timings.RerankDurationMS,
|
||||
AIDurationMS: timings.AIDurationMS,
|
||||
TotalDurationMS: timings.TotalDurationMS,
|
||||
})
|
||||
}
|
||||
|
||||
func (e *AutoReplyEngine) sendHandoffMessage(msg autoReplyMessage, reason string, hits []KnowledgeChunk) error {
|
||||
cfg := e.getConfig()
|
||||
conversationID, err := e.resolveHumanConversationID(msg, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content := renderHandoffTemplate(cfg.Handoff.MessageTemplate, msg, reason)
|
||||
if cfg.Handoff.IncludeKnowledgeHits && len(hits) > 0 {
|
||||
content += "\n\n知识库候选:"
|
||||
for i, hit := range hits {
|
||||
if i >= 3 {
|
||||
break
|
||||
}
|
||||
content += fmt.Sprintf("\n%d. %s / score=%.3f", i+1, hit.Source, hit.Score)
|
||||
}
|
||||
}
|
||||
if err := sendAutoReplyText(uint32(msg.ClientID), conversationID, content); err != nil {
|
||||
return err
|
||||
}
|
||||
e.rememberAutoSentMessage(uint32(msg.ClientID), conversationID, content)
|
||||
return nil
|
||||
}
|
||||
|
||||
type handoffCardResult struct {
|
||||
statuses []string
|
||||
errors []string
|
||||
}
|
||||
|
||||
func (r handoffCardResult) summary() string {
|
||||
items := append([]string{}, r.statuses...)
|
||||
items = append(items, r.errors...)
|
||||
return strings.Join(items, "; ")
|
||||
}
|
||||
|
||||
func (r handoffCardResult) reasonSuffix() string {
|
||||
return strings.Join(r.errors, "; ")
|
||||
}
|
||||
|
||||
func (e *AutoReplyEngine) sendHandoffCards(msg autoReplyMessage) handoffCardResult {
|
||||
cfg := e.getConfig()
|
||||
result := handoffCardResult{}
|
||||
if cfg.Handoff.SendHumanCardToCustomer {
|
||||
humanID := e.resolveHumanUserID(msg, cfg)
|
||||
if humanID == "" {
|
||||
result.errors = append(result.errors, "human_card_missing_user_id")
|
||||
e.noteReason("human_card_missing_user_id")
|
||||
} else if err := sendAutoReplyCard(uint32(msg.ClientID), msg.ConversationID, humanID); err != nil {
|
||||
result.errors = append(result.errors, "human_card_failed: "+err.Error())
|
||||
e.noteReason("human_card_failed")
|
||||
} else {
|
||||
result.statuses = append(result.statuses, "human_card_sent")
|
||||
e.noteReason("human_card_sent")
|
||||
if err := e.sendCustomerHandoffNotice(msg, cfg); err != nil {
|
||||
result.errors = append(result.errors, "customer_notice_failed: "+err.Error())
|
||||
e.noteReason("customer_notice_failed")
|
||||
} else {
|
||||
result.statuses = append(result.statuses, "customer_notice_sent")
|
||||
e.noteReason("customer_notice_sent")
|
||||
}
|
||||
}
|
||||
}
|
||||
if cfg.Handoff.SendCustomerCardToHuman {
|
||||
conversationID, err := e.resolveHumanConversationID(msg, cfg)
|
||||
switch {
|
||||
case err != nil:
|
||||
result.errors = append(result.errors, "customer_card_failed: "+err.Error())
|
||||
e.noteReason("customer_card_failed")
|
||||
case strings.TrimSpace(msg.FromWxID) == "":
|
||||
result.errors = append(result.errors, "customer_card_failed: 缺少客户user_id")
|
||||
e.noteReason("customer_card_failed")
|
||||
default:
|
||||
if err := sendAutoReplyCard(uint32(msg.ClientID), conversationID, msg.FromWxID); err != nil {
|
||||
result.errors = append(result.errors, "customer_card_failed: "+err.Error())
|
||||
e.noteReason("customer_card_failed")
|
||||
} else {
|
||||
result.statuses = append(result.statuses, "customer_card_sent")
|
||||
e.noteReason("customer_card_sent")
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (e *AutoReplyEngine) sendCustomerHandoffNotice(msg autoReplyMessage, cfg config.AutoReplyConfig) error {
|
||||
notice := strings.TrimSpace(cfg.Handoff.CustomerHandoffNotice)
|
||||
if notice == "" {
|
||||
notice = config.NewDefaultAutoReplyConfig().Handoff.CustomerHandoffNotice
|
||||
}
|
||||
if err := sendAutoReplyText(uint32(msg.ClientID), msg.ConversationID, notice); err != nil {
|
||||
return err
|
||||
}
|
||||
e.rememberAutoSentMessage(uint32(msg.ClientID), msg.ConversationID, notice)
|
||||
return nil
|
||||
}
|
||||
|
||||
func isManualHandoffReason(reason string) bool {
|
||||
return strings.HasPrefix(strings.TrimSpace(reason), "manual_keyword:")
|
||||
}
|
||||
|
||||
func (e *AutoReplyEngine) shouldSendHandoffCards(_ autoReplyMessage, reason string) bool {
|
||||
return isManualHandoffReason(reason)
|
||||
}
|
||||
|
||||
func (e *AutoReplyEngine) resolveHumanConversationID(msg autoReplyMessage, cfg config.AutoReplyConfig) (string, error) {
|
||||
conversationID := strings.TrimSpace(cfg.Handoff.HumanConversationID)
|
||||
if conversationID != "" {
|
||||
return conversationID, nil
|
||||
}
|
||||
humanID := e.resolveHumanUserID(msg, cfg)
|
||||
if humanID == "" {
|
||||
return "", fmt.Errorf("未配置人工接管同事")
|
||||
}
|
||||
robotID := msg.RobotID
|
||||
if robotID == "" {
|
||||
clientIdMutex.Lock()
|
||||
robotID = globalClientMap[uint32(msg.ClientID)]
|
||||
clientIdMutex.Unlock()
|
||||
}
|
||||
if robotID == "" || strings.HasPrefix(robotID, "client:") {
|
||||
return "", fmt.Errorf("无法推导人工私信会话,缺少当前接管账号ID")
|
||||
}
|
||||
return fmt.Sprintf("S:%s_%s", robotID, humanID), nil
|
||||
}
|
||||
|
||||
func (e *AutoReplyEngine) resolveHumanUserID(msg autoReplyMessage, cfg config.AutoReplyConfig) string {
|
||||
if humanID := strings.TrimSpace(cfg.Handoff.HumanUserID); humanID != "" {
|
||||
return humanID
|
||||
}
|
||||
return extractPeerIDFromConversation(cfg.Handoff.HumanConversationID, msg.RobotID)
|
||||
}
|
||||
|
||||
func (m autoReplyMessage) isInternalSender() bool {
|
||||
return m.SenderIdentity == senderIdentityInternal
|
||||
}
|
||||
|
||||
func (e *AutoReplyEngine) testHandoff() error {
|
||||
msg := autoReplyMessage{
|
||||
ClientID: int32(GetGlobalClientId()),
|
||||
RobotID: globalClientMap[GetGlobalClientId()],
|
||||
ConversationID: "test",
|
||||
FromWxID: "test-customer",
|
||||
FromNickName: "测试客户",
|
||||
Content: "这是一条自动客服转人工测试消息。",
|
||||
}
|
||||
if msg.ClientID == 0 {
|
||||
for clientID, userID := range globalClientMap {
|
||||
msg.ClientID = int32(clientID)
|
||||
msg.RobotID = userID
|
||||
break
|
||||
}
|
||||
}
|
||||
if msg.ClientID == 0 {
|
||||
return fmt.Errorf("没有活跃企微账号,无法测试发送")
|
||||
}
|
||||
return e.sendHandoffMessage(msg, "test_handoff", nil)
|
||||
}
|
||||
|
||||
func renderHandoffTemplate(template string, msg autoReplyMessage, reason string) string {
|
||||
if strings.TrimSpace(template) == "" || isLegacyHandoffTemplate(template) {
|
||||
template = defaultHandoffTemplate(msg)
|
||||
}
|
||||
messageTime := strings.TrimSpace(msg.MessageTime)
|
||||
if messageTime == "" {
|
||||
messageTime = time.Now().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
groupName := strings.TrimSpace(msg.GroupName)
|
||||
if groupName == "" && msg.IsGroup {
|
||||
groupName = msg.ConversationID
|
||||
}
|
||||
replacements := map[string]string{
|
||||
"{{customerName}}": fallbackString(msg.FromNickName, "未知客户"),
|
||||
"{{fromWxId}}": msg.FromWxID,
|
||||
"{{source}}": msg.sourceDisplayLabel(),
|
||||
"{{sourceLabel}}": msg.sourceDisplayLabel(),
|
||||
"{{conversationId}}": msg.ConversationID,
|
||||
"{{groupName}}": groupName,
|
||||
"{{question}}": msg.Content,
|
||||
"{{reason}}": handoffReasonLabel(reason),
|
||||
"{{reasonCode}}": reason,
|
||||
"{{messageTime}}": messageTime,
|
||||
"{{time}}": messageTime,
|
||||
}
|
||||
for key, value := range replacements {
|
||||
template = strings.ReplaceAll(template, key, value)
|
||||
}
|
||||
return template
|
||||
}
|
||||
|
||||
func defaultHandoffTemplate(msg autoReplyMessage) string {
|
||||
if msg.IsGroup {
|
||||
return "群聊问题待处理\n\n群聊:{{groupName}}\n客户:{{customerName}}\n客户ID:{{fromWxId}}\n来源:{{sourceLabel}}\n时间:{{messageTime}}\n问题:{{question}}\n原因:{{reason}}\n会话ID:{{conversationId}}"
|
||||
}
|
||||
return "客户问题待处理\n\n客户:{{customerName}}\n客户ID:{{fromWxId}}\n来源:{{sourceLabel}}\n时间:{{messageTime}}\n问题:{{question}}\n原因:{{reason}}\n会话ID:{{conversationId}}"
|
||||
}
|
||||
|
||||
func isLegacyHandoffTemplate(template string) bool {
|
||||
return strings.Contains(template, "客户问题需要人工处理") &&
|
||||
strings.Contains(template, "{{customerName}}") &&
|
||||
strings.Contains(template, "{{question}}")
|
||||
}
|
||||
|
||||
func handoffReasonLabel(reason string) string {
|
||||
reason = strings.TrimSpace(reason)
|
||||
switch {
|
||||
case reason == "knowledge_low_score":
|
||||
return "知识库未匹配到答案"
|
||||
case reason == "question_too_long":
|
||||
return "问题过长"
|
||||
case reason == "non_text_message":
|
||||
return "非文本消息"
|
||||
case reason == "ai_no_answer":
|
||||
return "AI 无法回答"
|
||||
case strings.HasPrefix(reason, "ai_error:"):
|
||||
lower := strings.ToLower(reason)
|
||||
if strings.Contains(lower, "deadline exceeded") || strings.Contains(lower, "timeout") || strings.Contains(lower, "timed out") {
|
||||
return "AI 请求超时"
|
||||
}
|
||||
return "AI 请求失败"
|
||||
case strings.HasPrefix(reason, "send_reply_failed:"):
|
||||
return "自动回复发送失败"
|
||||
case strings.HasPrefix(reason, "send_greeting_failed:"):
|
||||
return "问候回复发送失败"
|
||||
case strings.HasPrefix(reason, "manual_keyword:"):
|
||||
keyword := strings.TrimSpace(strings.TrimPrefix(reason, "manual_keyword:"))
|
||||
if keyword == "" {
|
||||
return "命中敏感关键词"
|
||||
}
|
||||
return "命中敏感关键词:" + keyword
|
||||
default:
|
||||
if reason == "" {
|
||||
return "未知原因"
|
||||
}
|
||||
return reason
|
||||
}
|
||||
}
|
||||
|
||||
func fallbackString(value string, fallback string) string {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
Reference in New Issue
Block a user