Files
qiweimanager-master/config/types.go
yuanzhipeng 849090a627 feat(auto-reply): 优化自动回复逻辑和知识库功能
- 将默认回复详细程度从"detailed"调整为"medium",前后端保持一致
- 新增话题切换检测逻辑,当用户主动要求换话题时提供引导回复
- 优化上下文处理机制,仅在指代型追问时注入历史对话,避免模型复读旧内容
- 改进知识库检索逻辑,区分自包含问题和指代型问题的上下文需求
- 完善知识库完整性指令,确保回复详细程度与知识展开程度一致
- 重构知识库重建逻辑,支持递归扫描子目录中的文件,修复索引为空的问题
- 增强素材匹配算法,引入强信号检测机制,避免仅凭模糊匹配误发素材
- 新增素材开场白AI生成功能,支持图片、视频、文档等类型智能描述
- 改进知识库重建通知,显示具体的文件数、分片数及失败统计信息
2026-06-26 14:25:35 +08:00

598 lines
25 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 判断网关是否为阿里云 DashScopeqwen3-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")
}