Files
qiweimanager-master/config/types.go
yuanzhipeng 1517be2a25 feat(auto-reply): 接入万川平台模型配置 + 各模型独立网关回退
万川平台对接
- 新增 wanchuan_proxy.go:WanchuanLogin/WanchuanGetModel 代理登录与按 code 拉取模型,
  日志对 password/token/apiKey 打码(含 encryptedConfig 二次解析)
- 新增 PlatformConfig(baseUrl/username/password)及 Get/SavePlatformConfig 持久化
- 前端万川卡片:登录→拉取 chat/vision/embedding/rerank/voice→回填 form 并保存→必要时重建向量索引

各模型独立网关(url+key),留空回退聊天网关
- RetrievalConfig 新增 embeddingBaseUrl/embeddingApiKey、rerankBaseUrl/rerankApiKey
- embeddingRequestConfig/rerankRequestConfig:优先独立网关,未配置回退 AI.BaseURL/APIKey
- vision/audio 同模式:非 DashScope 网关下视觉/语音模型留空时不再锁死或强写 DashScope,
  运行期由 fallbackString(VisionModel, Model) 动态复用聊天模型

陈旧向量空间防护
- loadEmbeddingIndex 检测磁盘索引与当前 embedding 模型/维度不一致时清空向量、回退关键词检索,
  并提示重建(embeddingIndexStaleReason,兼容旧版无模型名索引)

UI 状态修复
- 登录拉模型期间统一置全局 busy,禁用闸门收敛为 busy(与刷新联系人等按钮同范式),
  platformBusy 仅保留用于按钮「处理中…」文案,杜绝并发读写 form 与反向可点洞

其他
- 删除遗留 helper/auto_reply_ai.go.bak
- 补充 config/helper 单元测试(视觉回退分支、陈旧索引判定)
2026-06-26 10:17:02 +08:00

598 lines
25 KiB
Go
Raw 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: "detailed",
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")
}