package config import ( "strings" "time" ) const defaultVisionModel = "qwen3-vl-plus" const defaultAISystemPrompt = "你是一名企业微信智能客服。" // CallbackConfig stores local callback and helper API settings. type CallbackConfig struct { CallbackURL string `json:"callbackUrl"` CallbackToken string `json:"callbackToken"` HTTPPort string `json:"httpPort"` EnableCallback bool `json:"enableCallback"` EnableCloudAuth bool `json:"enableCloudAuth"` FileUploadUrl string `json:"fileUploadUrl"` DeviceCode string `json:"deviceCode"` } // AutoReplyConfig stores the local automatic customer-service settings. type AutoReplyConfig struct { Enabled bool `json:"enabled"` Listen ListenConfig `json:"listen"` Knowledge KnowledgeConfig `json:"knowledge"` Retrieval RetrievalConfig `json:"retrieval"` AI AIConfig `json:"ai"` Materials MaterialsConfig `json:"materials"` HumanAssist HumanAssistConfig `json:"humanAssist"` Collaboration CollaborationConfig `json:"collaboration"` Handoff HandoffConfig `json:"handoff"` Identity IdentityConfig `json:"identity"` ReplyPolicy ReplyPolicyConfig `json:"replyPolicy"` ReplyStyle string `json:"replyStyle"` } type ListenConfig struct { EnablePrivateChat bool `json:"enablePrivateChat"` EnableGroupChat bool `json:"enableGroupChat"` GroupTriggerMode string `json:"groupTriggerMode"` IgnoreSelfMessage bool `json:"ignoreSelfMessage"` DeduplicateSeconds int `json:"deduplicateSeconds"` } type KnowledgeConfig struct { Directory string `json:"directory"` IndexPath string `json:"indexPath"` SupportedExtensions []string `json:"supportedExtensions"` TopK int `json:"topK"` MinScore float64 `json:"minScore"` AutoRebuildOnStart bool `json:"autoRebuildOnStart"` } type RetrievalConfig struct { RetrievalMode string `json:"retrievalMode"` EmbeddingIndexPath string `json:"embeddingIndexPath"` EmbeddingModel string `json:"embeddingModel"` EmbeddingBaseURL string `json:"embeddingBaseUrl"` EmbeddingAPIKey string `json:"embeddingApiKey"` EmbeddingDimensions int `json:"embeddingDimensions"` RerankModel string `json:"rerankModel"` RerankBaseURL string `json:"rerankBaseUrl"` RerankAPIKey string `json:"rerankApiKey"` RecallTopK int `json:"recallTopK"` RerankTopK int `json:"rerankTopK"` FinalTopK int `json:"finalTopK"` } type MaterialsConfig struct { Directory string `json:"directory"` IndexPath string `json:"indexPath"` AutoSendEnabled bool `json:"autoSendEnabled"` MaxPerReply int `json:"maxPerReply"` } type HumanAssistConfig struct { Enabled bool `json:"enabled"` WaitSeconds int `json:"waitSeconds"` AfterHumanReplyDelaySeconds int `json:"afterHumanReplyDelaySeconds"` SupplementMode string `json:"supplementMode"` IgnoreLikelyAutoSentEcho bool `json:"ignoreLikelyAutoSentEcho"` MinimumHumanReplyLengthRunes int `json:"minimumHumanReplyLengthRunes"` } type CollaborationConfig struct { Enabled bool `json:"enabled"` HumanWaitSeconds int `json:"humanWaitSeconds"` AfterHumanReplyDelaySeconds int `json:"afterHumanReplyDelaySeconds"` TakeoverIdleExitSeconds int `json:"takeoverIdleExitSeconds"` SupplementTarget string `json:"supplementTarget"` EngineerReturnPolicy string `json:"engineerReturnPolicy"` } type AIConfig struct { Provider string `json:"provider"` BaseURL string `json:"baseUrl"` APIKey string `json:"apiKey"` Model string `json:"model"` SystemPrompt string `json:"systemPrompt"` VisionModel string `json:"visionModel"` VisionBaseURL string `json:"visionBaseUrl"` VisionAPIKey string `json:"visionApiKey"` AudioProvider string `json:"audioProvider"` AudioMode string `json:"audioMode"` AudioModel string `json:"audioModel"` AudioBaseURL string `json:"audioBaseUrl"` AudioAPIKey string `json:"audioApiKey"` TimeoutSeconds int `json:"timeoutSeconds"` EnableThinking bool `json:"enableThinking"` ReplyDetail string `json:"replyDetail"` Temperature float64 `json:"temperature"` MaxTokens int `json:"maxTokens"` } type HandoffConfig struct { HumanUserID string `json:"humanUserId"` HumanConversationID string `json:"humanConversationId"` MessageTemplate string `json:"messageTemplate"` CustomerHandoffNotice string `json:"customerHandoffNotice"` IncludeKnowledgeHits bool `json:"includeKnowledgeHits"` SendHumanCardToCustomer bool `json:"sendHumanCardToCustomer"` SendCustomerCardToHuman bool `json:"sendCustomerCardToHuman"` CardTriggerMode string `json:"cardTriggerMode"` ManualTriggerKeywords []string `json:"manualTriggerKeywords"` CardKeywords []string `json:"cardKeywords"` } type IdentityConfig struct { UnknownPolicy string `json:"unknownPolicy"` UnknownHandoffPolicy string `json:"unknownHandoffPolicy"` RefreshOnStart bool `json:"refreshOnStart"` RefreshIntervalMinutes int `json:"refreshIntervalMinutes"` PageSize int `json:"pageSize"` InternalNoHandoffReply string `json:"internalNoHandoffReply"` UnknownNoHandoffReply string `json:"unknownNoHandoffReply"` InternalUserIDs []string `json:"internalUserIds"` ExternalUserIDs []string `json:"externalUserIds"` InternalGroupConversationIDs []string `json:"internalGroupConversationIds"` InternalGroupIDsByScope map[string][]string `json:"internalGroupConversationIdsByScope"` InternalUserLabels map[string]string `json:"internalUserLabels"` ExternalUserLabels map[string]string `json:"externalUserLabels"` } type ReplyPolicyConfig struct { UnknownAnswerToken string `json:"unknownAnswerToken"` MaxQuestionLength int `json:"maxQuestionLength"` CooldownSeconds int `json:"cooldownSeconds"` SensitiveKeywords []string `json:"sensitiveKeywords"` } // PlatformConfig stores Wanchuan platform credentials. type PlatformConfig struct { BaseURL string `json:"baseUrl"` Username string `json:"username"` Password string `json:"password"` } // Config stores the application configuration. type Config struct { CallbackConfig CallbackConfig `json:"callbackConfig"` AutoReplyConfig AutoReplyConfig `json:"autoReplyConfig"` PlatformConfig PlatformConfig `json:"platformConfig"` LastUpdated int64 `json:"lastUpdated"` } // NewDefaultConfig creates a local-only default configuration. func NewDefaultConfig() *Config { return &Config{ CallbackConfig: CallbackConfig{ CallbackURL: "", CallbackToken: "", HTTPPort: "10001", EnableCallback: false, EnableCloudAuth: false, FileUploadUrl: "", DeviceCode: "", }, AutoReplyConfig: NewDefaultAutoReplyConfig(), PlatformConfig: PlatformConfig{ BaseURL: "", Username: "", Password: "", }, LastUpdated: time.Now().Unix(), } } // NewDefaultAutoReplyConfig creates disabled-but-ready automatic reply settings. func NewDefaultAutoReplyConfig() AutoReplyConfig { cfg := AutoReplyConfig{ Enabled: false, Listen: ListenConfig{ EnablePrivateChat: true, EnableGroupChat: true, GroupTriggerMode: "mention_only", IgnoreSelfMessage: true, DeduplicateSeconds: 300, }, Knowledge: KnowledgeConfig{ Directory: "config/knowledge", IndexPath: "config/knowledge/index.json", SupportedExtensions: []string{".md", ".txt", ".csv", ".xlsx", ".docx", ".pdf"}, TopK: 8, MinScore: 0.40, AutoRebuildOnStart: false, }, Retrieval: RetrievalConfig{ RetrievalMode: "hybrid_rerank", EmbeddingIndexPath: "config/knowledge/embedding_index.json", EmbeddingModel: "text-embedding-v4", EmbeddingDimensions: 512, RerankModel: "qwen3-rerank", RecallTopK: 50, RerankTopK: 30, FinalTopK: 8, }, Materials: MaterialsConfig{ Directory: "config/materials", IndexPath: "config/materials/materials.json", AutoSendEnabled: true, MaxPerReply: 2, }, HumanAssist: HumanAssistConfig{ Enabled: false, WaitSeconds: 15, AfterHumanReplyDelaySeconds: 3, SupplementMode: "supplement", IgnoreLikelyAutoSentEcho: true, MinimumHumanReplyLengthRunes: 4, }, Collaboration: CollaborationConfig{ Enabled: false, HumanWaitSeconds: 180, AfterHumanReplyDelaySeconds: 3, TakeoverIdleExitSeconds: 300, SupplementTarget: "customer", EngineerReturnPolicy: "review", }, AI: AIConfig{ Provider: "openai_compatible", BaseURL: "", APIKey: "", Model: "qwen-turbo", SystemPrompt: defaultAISystemPrompt, VisionModel: defaultVisionModel, VisionBaseURL: "", VisionAPIKey: "", AudioProvider: "auto", AudioMode: "openai_audio_chat", AudioModel: "qwen3-asr-flash", AudioBaseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1", AudioAPIKey: "", TimeoutSeconds: 20, EnableThinking: false, ReplyDetail: "medium", Temperature: 0, MaxTokens: 700, }, Handoff: HandoffConfig{ HumanUserID: "", HumanConversationID: "", MessageTemplate: "", CustomerHandoffNotice: "已为您通知人工客服添加您的好友,请稍等。若 2 分钟内仍未收到好友申请,请点击上方名片主动添加人工客服。", IncludeKnowledgeHits: true, SendHumanCardToCustomer: true, SendCustomerCardToHuman: true, CardTriggerMode: "manual_keywords", ManualTriggerKeywords: []string{"人工", "客服", "转人工", "人工客服", "真人", "真人客服"}, CardKeywords: []string{"人工", "客服", "转人工", "人工客服", "真人", "真人客服"}, }, Identity: IdentityConfig{ UnknownPolicy: "customer", UnknownHandoffPolicy: "hold", RefreshOnStart: true, RefreshIntervalMinutes: 30, PageSize: 200, InternalNoHandoffReply: "内部员工消息不触发转人工,如需协助请直接联系对应同事。", UnknownNoHandoffReply: "正在核验联系人身份,暂不触发转人工。如需协助请直接联系对应同事。", InternalUserIDs: []string{}, ExternalUserIDs: []string{}, InternalGroupConversationIDs: []string{}, InternalGroupIDsByScope: map[string][]string{}, InternalUserLabels: map[string]string{}, ExternalUserLabels: map[string]string{}, }, ReplyPolicy: ReplyPolicyConfig{ UnknownAnswerToken: "NO_ANSWER", MaxQuestionLength: 1000, CooldownSeconds: 3, SensitiveKeywords: []string{"人工", "转人工", "人工客服", "真人客服", "投诉", "退款", "退货", "合同", "发票", "赔偿", "价格审批"}, }, } cfg.ReplyStyle = "natural_professional" return cfg } // ApplyDefaults fills missing values for configs loaded from older files. func (c *Config) ApplyDefaults() { if c == nil { return } defaultConfig := NewDefaultConfig() if c.CallbackConfig.HTTPPort == "" { c.CallbackConfig.HTTPPort = defaultConfig.CallbackConfig.HTTPPort } defaultAuto := NewDefaultAutoReplyConfig() if c.AutoReplyConfig.Listen.GroupTriggerMode == "" { c.AutoReplyConfig.Listen.GroupTriggerMode = defaultAuto.Listen.GroupTriggerMode } if !c.AutoReplyConfig.Listen.EnablePrivateChat && !c.AutoReplyConfig.Listen.EnableGroupChat { c.AutoReplyConfig.Listen.EnablePrivateChat = defaultAuto.Listen.EnablePrivateChat c.AutoReplyConfig.Listen.EnableGroupChat = defaultAuto.Listen.EnableGroupChat } if c.AutoReplyConfig.Listen.DeduplicateSeconds <= 0 { c.AutoReplyConfig.Listen.DeduplicateSeconds = defaultAuto.Listen.DeduplicateSeconds } if c.AutoReplyConfig.Knowledge.Directory == "" { c.AutoReplyConfig.Knowledge.Directory = defaultAuto.Knowledge.Directory } if c.AutoReplyConfig.Knowledge.IndexPath == "" { c.AutoReplyConfig.Knowledge.IndexPath = defaultAuto.Knowledge.IndexPath } if len(c.AutoReplyConfig.Knowledge.SupportedExtensions) == 0 { c.AutoReplyConfig.Knowledge.SupportedExtensions = defaultAuto.Knowledge.SupportedExtensions } if c.AutoReplyConfig.Knowledge.TopK <= 0 { c.AutoReplyConfig.Knowledge.TopK = defaultAuto.Knowledge.TopK } else if c.AutoReplyConfig.Knowledge.TopK < defaultAuto.Knowledge.TopK { c.AutoReplyConfig.Knowledge.TopK = defaultAuto.Knowledge.TopK } if c.AutoReplyConfig.Knowledge.MinScore <= 0 { c.AutoReplyConfig.Knowledge.MinScore = defaultAuto.Knowledge.MinScore } if c.AutoReplyConfig.Retrieval.RetrievalMode == "" { c.AutoReplyConfig.Retrieval.RetrievalMode = defaultAuto.Retrieval.RetrievalMode } if c.AutoReplyConfig.Retrieval.EmbeddingIndexPath == "" { c.AutoReplyConfig.Retrieval.EmbeddingIndexPath = defaultAuto.Retrieval.EmbeddingIndexPath } if c.AutoReplyConfig.Retrieval.EmbeddingModel == "" { c.AutoReplyConfig.Retrieval.EmbeddingModel = defaultAuto.Retrieval.EmbeddingModel } // 检测用户是否错误地将 Rerank 模型填到了 Embedding 模型字段 if isRerankModelName(c.AutoReplyConfig.Retrieval.EmbeddingModel) { c.AutoReplyConfig.Retrieval.EmbeddingModel = defaultAuto.Retrieval.EmbeddingModel } if c.AutoReplyConfig.Retrieval.EmbeddingDimensions <= 0 { c.AutoReplyConfig.Retrieval.EmbeddingDimensions = defaultAuto.Retrieval.EmbeddingDimensions } if c.AutoReplyConfig.Retrieval.RerankModel == "" { c.AutoReplyConfig.Retrieval.RerankModel = defaultAuto.Retrieval.RerankModel } // 检测用户是否错误地将 Embedding 模型填到了 Rerank 模型字段 if isEmbeddingModelName(c.AutoReplyConfig.Retrieval.RerankModel) { c.AutoReplyConfig.Retrieval.RerankModel = defaultAuto.Retrieval.RerankModel } if c.AutoReplyConfig.Retrieval.RecallTopK <= 0 { c.AutoReplyConfig.Retrieval.RecallTopK = defaultAuto.Retrieval.RecallTopK } else if c.AutoReplyConfig.Retrieval.RecallTopK < defaultAuto.Retrieval.RecallTopK { c.AutoReplyConfig.Retrieval.RecallTopK = defaultAuto.Retrieval.RecallTopK } if c.AutoReplyConfig.Retrieval.RerankTopK <= 0 { c.AutoReplyConfig.Retrieval.RerankTopK = defaultAuto.Retrieval.RerankTopK } else if c.AutoReplyConfig.Retrieval.RerankTopK < defaultAuto.Retrieval.RerankTopK { c.AutoReplyConfig.Retrieval.RerankTopK = defaultAuto.Retrieval.RerankTopK } if c.AutoReplyConfig.Retrieval.FinalTopK <= 0 { c.AutoReplyConfig.Retrieval.FinalTopK = defaultAuto.Retrieval.FinalTopK } else if c.AutoReplyConfig.Retrieval.FinalTopK < defaultAuto.Retrieval.FinalTopK { c.AutoReplyConfig.Retrieval.FinalTopK = defaultAuto.Retrieval.FinalTopK } if c.AutoReplyConfig.Materials.Directory == "" { c.AutoReplyConfig.Materials.Directory = defaultAuto.Materials.Directory } if c.AutoReplyConfig.Materials.IndexPath == "" { c.AutoReplyConfig.Materials.IndexPath = defaultAuto.Materials.IndexPath } if c.AutoReplyConfig.Materials.MaxPerReply <= 0 { c.AutoReplyConfig.Materials.MaxPerReply = defaultAuto.Materials.MaxPerReply } if c.AutoReplyConfig.HumanAssist.WaitSeconds <= 0 { c.AutoReplyConfig.HumanAssist.WaitSeconds = defaultAuto.HumanAssist.WaitSeconds } if c.AutoReplyConfig.HumanAssist.AfterHumanReplyDelaySeconds < 0 { c.AutoReplyConfig.HumanAssist.AfterHumanReplyDelaySeconds = defaultAuto.HumanAssist.AfterHumanReplyDelaySeconds } if c.AutoReplyConfig.HumanAssist.SupplementMode == "" { c.AutoReplyConfig.HumanAssist.SupplementMode = defaultAuto.HumanAssist.SupplementMode } if c.AutoReplyConfig.HumanAssist.MinimumHumanReplyLengthRunes <= 0 { c.AutoReplyConfig.HumanAssist.MinimumHumanReplyLengthRunes = defaultAuto.HumanAssist.MinimumHumanReplyLengthRunes } if c.AutoReplyConfig.Collaboration.HumanWaitSeconds <= 0 { c.AutoReplyConfig.Collaboration.HumanWaitSeconds = defaultAuto.Collaboration.HumanWaitSeconds } if c.AutoReplyConfig.Collaboration.AfterHumanReplyDelaySeconds < 0 { c.AutoReplyConfig.Collaboration.AfterHumanReplyDelaySeconds = defaultAuto.Collaboration.AfterHumanReplyDelaySeconds } if c.AutoReplyConfig.Collaboration.TakeoverIdleExitSeconds <= 0 { c.AutoReplyConfig.Collaboration.TakeoverIdleExitSeconds = defaultAuto.Collaboration.TakeoverIdleExitSeconds } if strings.TrimSpace(c.AutoReplyConfig.Collaboration.SupplementTarget) == "" { c.AutoReplyConfig.Collaboration.SupplementTarget = defaultAuto.Collaboration.SupplementTarget } if strings.TrimSpace(c.AutoReplyConfig.Collaboration.EngineerReturnPolicy) == "" { c.AutoReplyConfig.Collaboration.EngineerReturnPolicy = defaultAuto.Collaboration.EngineerReturnPolicy } if c.AutoReplyConfig.AI.Provider == "" { c.AutoReplyConfig.AI.Provider = defaultAuto.AI.Provider } if c.AutoReplyConfig.AI.Model == "" { c.AutoReplyConfig.AI.Model = defaultAuto.AI.Model } if strings.TrimSpace(c.AutoReplyConfig.AI.SystemPrompt) == "" { c.AutoReplyConfig.AI.SystemPrompt = defaultAuto.AI.SystemPrompt } visionGateway := strings.TrimSpace(c.AutoReplyConfig.AI.VisionBaseURL) if visionGateway == "" { visionGateway = strings.TrimSpace(c.AutoReplyConfig.AI.BaseURL) } if isDashScopeGateway(visionGateway) { // DashScope 网关:空/文本模型一律回退到专用视觉模型 qwen3-vl-plus if c.AutoReplyConfig.AI.VisionModel == "" || (strings.EqualFold(c.AutoReplyConfig.AI.VisionModel, c.AutoReplyConfig.AI.Model) && !isVisionCapableModelName(c.AutoReplyConfig.AI.VisionModel)) || isLikelyTextOnlyQwenModel(c.AutoReplyConfig.AI.VisionModel) { c.AutoReplyConfig.AI.VisionModel = defaultAuto.AI.VisionModel } } else if strings.TrimSpace(c.AutoReplyConfig.AI.VisionBaseURL) == "" { // 非 DashScope 且没有独立视觉网关(如万川统一网关): // 视觉留空时清空 VisionModel,让请求期 fallbackString(VisionModel, Model) 动态复用聊天模型, // 这样后续用户改聊天模型,视觉会自动跟随,不会被锁死在旧值。 // 仅当用户在同一网关上显式填了与聊天模型不同的视觉模型时才保留其选择。 if strings.EqualFold(strings.TrimSpace(c.AutoReplyConfig.AI.VisionModel), strings.TrimSpace(c.AutoReplyConfig.AI.Model)) { c.AutoReplyConfig.AI.VisionModel = "" } } if c.AutoReplyConfig.AI.AudioProvider == "" { c.AutoReplyConfig.AI.AudioProvider = defaultAuto.AI.AudioProvider } if c.AutoReplyConfig.AI.AudioMode == "" { c.AutoReplyConfig.AI.AudioMode = defaultAuto.AI.AudioMode } if c.AutoReplyConfig.AI.AudioModel == "" { c.AutoReplyConfig.AI.AudioModel = defaultAuto.AI.AudioModel } if c.AutoReplyConfig.AI.TimeoutSeconds <= 0 { c.AutoReplyConfig.AI.TimeoutSeconds = defaultAuto.AI.TimeoutSeconds } if c.AutoReplyConfig.AI.MaxTokens <= 0 { c.AutoReplyConfig.AI.MaxTokens = defaultAuto.AI.MaxTokens } if c.AutoReplyConfig.AI.ReplyDetail == "" { c.AutoReplyConfig.AI.ReplyDetail = defaultAuto.AI.ReplyDetail } if c.AutoReplyConfig.Handoff.MessageTemplate == "" { c.AutoReplyConfig.Handoff.MessageTemplate = defaultAuto.Handoff.MessageTemplate } if c.AutoReplyConfig.Handoff.CustomerHandoffNotice == "" { c.AutoReplyConfig.Handoff.CustomerHandoffNotice = defaultAuto.Handoff.CustomerHandoffNotice } if c.AutoReplyConfig.Handoff.CardTriggerMode == "" { c.AutoReplyConfig.Handoff.SendHumanCardToCustomer = defaultAuto.Handoff.SendHumanCardToCustomer c.AutoReplyConfig.Handoff.SendCustomerCardToHuman = defaultAuto.Handoff.SendCustomerCardToHuman c.AutoReplyConfig.Handoff.CardTriggerMode = defaultAuto.Handoff.CardTriggerMode } if len(c.AutoReplyConfig.Handoff.ManualTriggerKeywords) == 0 { c.AutoReplyConfig.Handoff.ManualTriggerKeywords = defaultAuto.Handoff.ManualTriggerKeywords } c.AutoReplyConfig.Handoff.ManualTriggerKeywords = dedupeStrings(append( append([]string{}, c.AutoReplyConfig.Handoff.ManualTriggerKeywords...), c.AutoReplyConfig.Handoff.CardKeywords..., )) c.AutoReplyConfig.Handoff.CardKeywords = c.AutoReplyConfig.Handoff.ManualTriggerKeywords if c.AutoReplyConfig.Identity.UnknownPolicy == "" { c.AutoReplyConfig.Identity = defaultAuto.Identity } if c.AutoReplyConfig.Identity.UnknownHandoffPolicy == "" { c.AutoReplyConfig.Identity.UnknownHandoffPolicy = defaultAuto.Identity.UnknownHandoffPolicy } if c.AutoReplyConfig.Identity.RefreshIntervalMinutes <= 0 { c.AutoReplyConfig.Identity.RefreshIntervalMinutes = defaultAuto.Identity.RefreshIntervalMinutes } if c.AutoReplyConfig.Identity.PageSize <= 0 { c.AutoReplyConfig.Identity.PageSize = defaultAuto.Identity.PageSize } if c.AutoReplyConfig.Identity.InternalNoHandoffReply == "" { c.AutoReplyConfig.Identity.InternalNoHandoffReply = defaultAuto.Identity.InternalNoHandoffReply } if c.AutoReplyConfig.Identity.UnknownNoHandoffReply == "" { c.AutoReplyConfig.Identity.UnknownNoHandoffReply = defaultAuto.Identity.UnknownNoHandoffReply } if c.AutoReplyConfig.Identity.InternalUserIDs == nil { c.AutoReplyConfig.Identity.InternalUserIDs = defaultAuto.Identity.InternalUserIDs } if c.AutoReplyConfig.Identity.ExternalUserIDs == nil { c.AutoReplyConfig.Identity.ExternalUserIDs = defaultAuto.Identity.ExternalUserIDs } if c.AutoReplyConfig.Identity.InternalGroupConversationIDs == nil { c.AutoReplyConfig.Identity.InternalGroupConversationIDs = defaultAuto.Identity.InternalGroupConversationIDs } if c.AutoReplyConfig.Identity.InternalGroupIDsByScope == nil { c.AutoReplyConfig.Identity.InternalGroupIDsByScope = defaultAuto.Identity.InternalGroupIDsByScope } if c.AutoReplyConfig.Identity.InternalUserLabels == nil { c.AutoReplyConfig.Identity.InternalUserLabels = defaultAuto.Identity.InternalUserLabels } if c.AutoReplyConfig.Identity.ExternalUserLabels == nil { c.AutoReplyConfig.Identity.ExternalUserLabels = defaultAuto.Identity.ExternalUserLabels } if c.AutoReplyConfig.ReplyPolicy.UnknownAnswerToken == "" { c.AutoReplyConfig.ReplyPolicy.UnknownAnswerToken = defaultAuto.ReplyPolicy.UnknownAnswerToken } if c.AutoReplyConfig.ReplyPolicy.MaxQuestionLength <= 0 { c.AutoReplyConfig.ReplyPolicy.MaxQuestionLength = defaultAuto.ReplyPolicy.MaxQuestionLength } if c.AutoReplyConfig.ReplyPolicy.CooldownSeconds <= 0 { c.AutoReplyConfig.ReplyPolicy.CooldownSeconds = defaultAuto.ReplyPolicy.CooldownSeconds } if len(c.AutoReplyConfig.ReplyPolicy.SensitiveKeywords) == 0 { c.AutoReplyConfig.ReplyPolicy.SensitiveKeywords = defaultAuto.ReplyPolicy.SensitiveKeywords } if strings.TrimSpace(c.AutoReplyConfig.ReplyStyle) == "" { c.AutoReplyConfig.ReplyStyle = "natural_professional" } } func dedupeStrings(items []string) []string { seen := make(map[string]bool, len(items)) result := make([]string, 0, len(items)) for _, item := range items { item = strings.TrimSpace(item) if item == "" || seen[item] { continue } seen[item] = true result = append(result, item) } return result } // isDashScopeGateway 判断网关是否为阿里云 DashScope(qwen3-vl-plus 等默认模型仅对其有意义) func isDashScopeGateway(url string) bool { return strings.Contains(strings.ToLower(strings.TrimSpace(url)), "dashscope.aliyuncs.com") } func isVisionCapableModelName(model string) bool { name := strings.ToLower(strings.TrimSpace(model)) return strings.Contains(name, "vl") || strings.Contains(name, "vision") || strings.Contains(name, "qvq") || strings.Contains(name, "omni") } func isLikelyTextOnlyQwenModel(model string) bool { name := strings.ToLower(strings.TrimSpace(model)) if name == "" || isVisionCapableModelName(name) { return false } switch name { case "qwen-turbo", "qwen-plus", "qwen-max", "qwen-long": return true } return strings.HasPrefix(name, "qwen") && (strings.Contains(name, "turbo") || strings.Contains(name, "plus") || strings.Contains(name, "max") || strings.Contains(name, "long") || strings.Contains(name, "coder") || strings.Contains(name, "math") || strings.Contains(name, "instruct")) } // isRerankModelName 检测模型名是否是 Rerank 模型 func isRerankModelName(model string) bool { name := strings.ToLower(strings.TrimSpace(model)) if name == "" { return false } return strings.Contains(name, "rerank") || strings.Contains(name, "gte-rerank") || strings.Contains(name, "bge-rerank") } // isEmbeddingModelName 检测模型名是否是 Embedding 模型 func isEmbeddingModelName(model string) bool { name := strings.ToLower(strings.TrimSpace(model)) if name == "" { return false } return strings.Contains(name, "embedding") || strings.Contains(name, "text-embedding") || strings.Contains(name, "bge-") || strings.Contains(name, "gte-") && !strings.Contains(name, "rerank") }