diff --git a/config/config_manager.go b/config/config_manager.go index edc48d7..1c0588d 100644 --- a/config/config_manager.go +++ b/config/config_manager.go @@ -162,6 +162,15 @@ func UpdateAutoReplyConfig(autoReplyConfig AutoReplyConfig) error { return nil } +// UpdatePlatformConfig updates Wanchuan platform configuration. +func UpdatePlatformConfig(platformConfig PlatformConfig) error { + if globalConfig != nil { + globalConfig.PlatformConfig = platformConfig + return SaveGlobalConfig() + } + return nil +} + // ReloadGlobalConfig reloads config.json from disk. func ReloadGlobalConfig() (*Config, error) { if globalConfigManager == nil { diff --git a/config/types.go b/config/types.go index 839718f..c3cbf6f 100644 --- a/config/types.go +++ b/config/types.go @@ -56,8 +56,12 @@ 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"` @@ -145,10 +149,18 @@ type ReplyPolicyConfig struct { 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"` } @@ -165,7 +177,12 @@ func NewDefaultConfig() *Config { DeviceCode: "", }, AutoReplyConfig: NewDefaultAutoReplyConfig(), - LastUpdated: time.Now().Unix(), + PlatformConfig: PlatformConfig{ + BaseURL: "", + Username: "", + Password: "", + }, + LastUpdated: time.Now().Unix(), } } @@ -399,11 +416,26 @@ func (c *Config) ApplyDefaults() { if strings.TrimSpace(c.AutoReplyConfig.AI.SystemPrompt) == "" { c.AutoReplyConfig.AI.SystemPrompt = defaultAuto.AI.SystemPrompt } - 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 + 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 @@ -509,6 +541,11 @@ func dedupeStrings(items []string) []string { 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") || diff --git a/config/types_test.go b/config/types_test.go index f2cf539..60b7104 100644 --- a/config/types_test.go +++ b/config/types_test.go @@ -99,3 +99,49 @@ func TestApplyDefaultsFixesWrongRerankConfig(t *testing.T) { t.Errorf("Expected rerank model to be corrected to 'qwen3-rerank', got %q", cfg.AutoReplyConfig.Retrieval.RerankModel) } } + +// 非 DashScope 统一网关(如万川)且无独立视觉网关时:视觉模型 == 聊天模型应被清空, +// 以便运行期 fallbackString(VisionModel, Model) 动态跟随聊天模型,不锁死旧值。 +func TestApplyDefaultsNonDashScopeVisionFollowsChat(t *testing.T) { + cfg := NewDefaultConfig() + cfg.AutoReplyConfig.AI.BaseURL = "https://wanchuan.example/v1" + cfg.AutoReplyConfig.AI.Model = "wanchuan-chat" + cfg.AutoReplyConfig.AI.VisionBaseURL = "" + cfg.AutoReplyConfig.AI.VisionModel = "wanchuan-chat" // 与聊天模型相同(之前回填留下的值) + + cfg.ApplyDefaults() + + if cfg.AutoReplyConfig.AI.VisionModel != "" { + t.Errorf("Expected vision model cleared to follow chat on non-DashScope unified gateway, got %q", cfg.AutoReplyConfig.AI.VisionModel) + } +} + +// 非 DashScope 网关上用户在同一网关显式填了不同的视觉模型时,应保留其选择。 +func TestApplyDefaultsNonDashScopeKeepsExplicitVision(t *testing.T) { + cfg := NewDefaultConfig() + cfg.AutoReplyConfig.AI.BaseURL = "https://wanchuan.example/v1" + cfg.AutoReplyConfig.AI.Model = "wanchuan-chat" + cfg.AutoReplyConfig.AI.VisionBaseURL = "" + cfg.AutoReplyConfig.AI.VisionModel = "wanchuan-vl" // 与聊天模型不同,属用户显式选择 + + cfg.ApplyDefaults() + + if cfg.AutoReplyConfig.AI.VisionModel != "wanchuan-vl" { + t.Errorf("Expected explicit different vision model preserved, got %q", cfg.AutoReplyConfig.AI.VisionModel) + } +} + +// DashScope 网关:视觉模型为空或文本模型时仍应回退到专用视觉模型,不受上面改动影响。 +func TestApplyDefaultsDashScopeStillFallsBackToVisionModel(t *testing.T) { + cfg := NewDefaultConfig() + cfg.AutoReplyConfig.AI.BaseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1" + cfg.AutoReplyConfig.AI.Model = "qwen-turbo" + cfg.AutoReplyConfig.AI.VisionBaseURL = "" + cfg.AutoReplyConfig.AI.VisionModel = "" + + cfg.ApplyDefaults() + + if cfg.AutoReplyConfig.AI.VisionModel != defaultVisionModel { + t.Errorf("Expected DashScope vision fallback to %q, got %q", defaultVisionModel, cfg.AutoReplyConfig.AI.VisionModel) + } +} diff --git a/frontend/src/components/AutoReply.vue b/frontend/src/components/AutoReply.vue index 6e14341..ffc4eba 100644 --- a/frontend/src/components/AutoReply.vue +++ b/frontend/src/components/AutoReply.vue @@ -54,7 +54,8 @@
身份缓存 - {{ status.identityRefreshing ? '刷新中' : formatUnixTime(status.identityLastRefreshAt) }} + {{ status.identityRefreshing ? '刷新中' : + formatUnixTime(status.identityLastRefreshAt) }}
协同等待中 @@ -79,13 +80,8 @@
@@ -167,13 +163,49 @@
{{ form.ai.provider === 'local' ? '本地模型' : '兼容接口' }} 文本:{{ form.ai.model || '未配置' }} - 图片:{{ form.ai.visionModel || defaultVisionModel }} + 图片:{{ form.ai.visionModel || form.ai.model || defaultVisionModel }}{{ form.ai.visionModel ? '' : + '(复用文本模型)' }} 语音:{{ form.ai.audioModel || '未配置' }}
{{ alert }}
+ + +
+
+ 万川平台 + 从万川 AI 平台自动获取模型配置 +
+
+ + + +
+ + + +
+
+
{{ platformMessage }}
+
+
@@ -342,6 +374,17 @@ 用于文本向量化,例如:text-embedding-v4, text-embedding-v3 + + + +
@@ -499,7 +544,8 @@
@@ -550,20 +596,24 @@ - +
先点击“刷新群列表”,再选择企业总群;未选择总群时只同步企微联系人列表。
-
+
{{ item.name || '未命名群聊' }} {{ item.conversationId }} {{ item.source || '群列表' }} - +
暂无内部成员同步群
@@ -624,23 +674,21 @@ {{ identityOptionLabel(item) }} - +
{{ item.name || '未填写名称' }} {{ item.userId }} {{ identitySourceLabel(item.source) }} - + @keydown.enter.prevent="commitIdentityLabelDraft('internal', item.userId)"> - +
暂无手动内部员工
@@ -658,30 +706,29 @@ {{ identityOptionLabel(item) }} - +
{{ item.name || '未填写名称' }} {{ item.userId }} {{ identitySourceLabel(item.source) }} - + @keydown.enter.prevent="commitIdentityLabelDraft('external', item.userId)"> - +
暂无手动外部客户