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 单元测试(视觉回退分支、陈旧索引判定)
This commit is contained in:
2026-06-26 10:17:02 +08:00
parent a926ee6b1b
commit 1517be2a25
10 changed files with 936 additions and 984 deletions

View File

@@ -3871,3 +3871,46 @@ func TestLongKnowledgeQueryFindsLateSectionByChinesePhrase(t *testing.T) {
t.Fatalf("expected neighbor section to be included, got %s", text)
}
}
// 磁盘索引与当前配置不一致(含旧版本无模型名的情况)应判为陈旧,触发清空向量回退关键词。
func TestEmbeddingIndexStaleReason(t *testing.T) {
cases := []struct {
name string
idx EmbeddingIndex
retrieval config.RetrievalConfig
wantStale bool
}{
{
name: "same model and dims",
idx: EmbeddingIndex{Model: "text-embedding-v4", Dimensions: 512},
retrieval: config.RetrievalConfig{EmbeddingModel: "text-embedding-v4", EmbeddingDimensions: 512},
wantStale: false,
},
{
name: "different model",
idx: EmbeddingIndex{Model: "text-embedding-v4", Dimensions: 512},
retrieval: config.RetrievalConfig{EmbeddingModel: "wanchuan-embed", EmbeddingDimensions: 512},
wantStale: true,
},
{
name: "legacy index without model name",
idx: EmbeddingIndex{Model: "", Dimensions: 0},
retrieval: config.RetrievalConfig{EmbeddingModel: "wanchuan-embed", EmbeddingDimensions: 1024},
wantStale: true,
},
{
name: "different dimensions same model",
idx: EmbeddingIndex{Model: "text-embedding-v4", Dimensions: 512},
retrieval: config.RetrievalConfig{EmbeddingModel: "text-embedding-v4", EmbeddingDimensions: 1024},
wantStale: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
reason := embeddingIndexStaleReason(tc.idx, tc.retrieval)
if (reason != "") != tc.wantStale {
t.Fatalf("embeddingIndexStaleReason() = %q, wantStale=%v", reason, tc.wantStale)
}
})
}
}