Files
qiweimanager-master/wanchuan_proxy.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

243 lines
7.1 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 main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"qiweimanager/config"
)
// WanchuanLogin 登录万川平台并返回原始响应
func (a *App) WanchuanLogin(baseURL, username, password string) string {
startTime := time.Now()
// 构建请求体
loginData := map[string]interface{}{
"username": username,
"password": password,
"loginType": "user",
}
jsonData, err := json.Marshal(loginData)
if err != nil {
globalLogger.Error("[WanchuanLogin] 序列化登录数据失败: %v", err)
return fmt.Sprintf(`{"success": false, "error": "序列化登录数据失败: %v"}`, err)
}
// 打印日志(密码打码)
globalLogger.Info("[WanchuanLogin] 登录请求 - URL: %s/api/login, 用户名: %s, 密码: %s",
baseURL, username, maskString(password))
// 发送 POST 请求
url := fmt.Sprintf("%s/api/login", strings.TrimRight(baseURL, "/"))
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
globalLogger.Error("[WanchuanLogin] 创建请求失败: %v", err)
return fmt.Sprintf(`{"success": false, "error": "创建请求失败: %v"}`, err)
}
req.Header.Set("Content-Type", "application/json;charset=UTF-8")
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
globalLogger.Error("[WanchuanLogin] 请求失败: %v", err)
return fmt.Sprintf(`{"success": false, "error": "请求失败: %v"}`, err)
}
defer resp.Body.Close()
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
globalLogger.Error("[WanchuanLogin] 读取响应失败: %v", err)
return fmt.Sprintf(`{"success": false, "error": "读取响应失败: %v"}`, err)
}
responseStr := string(body)
// 打印日志token 打码)
maskedResponse := maskToken(responseStr)
duration := time.Since(startTime).Milliseconds()
globalLogger.Info("[WanchuanLogin] 登录完成 - 耗时: %dms, 响应: %s", duration, maskedResponse)
a.AddLogEntry("Wanchuan", "info", fmt.Sprintf("平台登录完成 - 用户: %s", username), duration)
// 原样返回平台响应
return responseStr
}
// WanchuanGetModel 获取模型配置并返回原始响应
func (a *App) WanchuanGetModel(baseURL, code, token string) string {
startTime := time.Now()
globalLogger.Info("[WanchuanGetModel] 获取模型 - URL: %s/api/system/model/getByCode/%s, Token: %s",
baseURL, code, maskString(token))
// 发送 GET 请求
url := fmt.Sprintf("%s/api/system/model/getByCode/%s", strings.TrimRight(baseURL, "/"), code)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
globalLogger.Error("[WanchuanGetModel] 创建请求失败: %v", err)
return fmt.Sprintf(`{"success": false, "error": "创建请求失败: %v"}`, err)
}
// 设置认证头(平台同时支持两种)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("token", fmt.Sprintf("Bearer %s", token))
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
globalLogger.Error("[WanchuanGetModel] 请求失败: %v", err)
return fmt.Sprintf(`{"success": false, "error": "请求失败: %v"}`, err)
}
defer resp.Body.Close()
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
globalLogger.Error("[WanchuanGetModel] 读取响应失败: %v", err)
return fmt.Sprintf(`{"success": false, "error": "读取响应失败: %v"}`, err)
}
responseStr := string(body)
// 打印日志apiKey 打码)
maskedResponse := maskApiKey(responseStr)
duration := time.Since(startTime).Milliseconds()
globalLogger.Info("[WanchuanGetModel] 获取模型完成 - code: %s, 耗时: %dms, 响应: %s", code, duration, maskedResponse)
a.AddLogEntry("Wanchuan", "info", fmt.Sprintf("获取模型配置 - code: %s", code), duration)
// 原样返回平台响应
return responseStr
}
// GetPlatformConfig 获取平台配置
func (a *App) GetPlatformConfig() interface{} {
globalLogger.Info("[GetPlatformConfig] 读取平台配置")
appConfig := config.GetGlobalConfig()
if appConfig == nil {
globalLogger.Warn("[GetPlatformConfig] 全局配置不存在")
return config.PlatformConfig{}
}
return appConfig.PlatformConfig
}
// SavePlatformConfig 保存平台配置
func (a *App) SavePlatformConfig(jsonData string) (bool, string) {
startTime := time.Now()
globalLogger.Info("[SavePlatformConfig] 保存平台配置 - 数据: %s", maskPlatformConfig(jsonData))
var platformConfig config.PlatformConfig
if err := json.Unmarshal([]byte(jsonData), &platformConfig); err != nil {
msg := fmt.Sprintf("解析平台配置失败: %v", err)
globalLogger.Error("%s", msg)
return false, msg
}
if err := config.UpdatePlatformConfig(platformConfig); err != nil {
msg := fmt.Sprintf("保存平台配置失败: %v", err)
globalLogger.Error("%s", msg)
return false, msg
}
duration := time.Since(startTime).Milliseconds()
globalLogger.Info("[SavePlatformConfig] 保存成功 - 耗时: %dms", duration)
a.AddLogEntry("Wanchuan", "info", "平台配置已保存", duration)
return true, "success"
}
// maskString 对字符串打码(保留首尾,中间用 * 替换)
func maskString(s string) string {
if len(s) <= 8 {
return "***"
}
return s[:4] + "****" + s[len(s)-4:]
}
// maskToken 对 JSON 响应中的 token 打码
func maskToken(jsonStr string) string {
var data map[string]interface{}
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
return jsonStr
}
// 递归打码 token 字段
maskTokenInMap(data)
masked, _ := json.Marshal(data)
return string(masked)
}
// maskApiKey 对 JSON 响应中的 apiKey 打码
func maskApiKey(jsonStr string) string {
var data map[string]interface{}
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
return jsonStr
}
// 递归打码 apiKey 字段
maskApiKeyInMap(data)
masked, _ := json.Marshal(data)
return string(masked)
}
// maskPlatformConfig 对平台配置中的密码打码
func maskPlatformConfig(jsonStr string) string {
var data map[string]interface{}
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
return jsonStr
}
if pwd, ok := data["password"].(string); ok && pwd != "" {
data["password"] = maskString(pwd)
}
masked, _ := json.Marshal(data)
return string(masked)
}
func maskTokenInMap(m map[string]interface{}) {
for k, v := range m {
if k == "token" || k == "access_token" {
if s, ok := v.(string); ok && s != "" {
m[k] = maskString(s)
}
} else if subMap, ok := v.(map[string]interface{}); ok {
maskTokenInMap(subMap)
}
}
}
func maskApiKeyInMap(m map[string]interface{}) {
for k, v := range m {
if k == "apiKey" || k == "api_key" {
if s, ok := v.(string); ok && s != "" {
m[k] = maskString(s)
}
} else if k == "encryptedConfig" {
if s, ok := v.(string); ok && s != "" {
// encryptedConfig 是 JSON 字符串,需要二次解析
var configMap map[string]interface{}
if err := json.Unmarshal([]byte(s), &configMap); err == nil {
maskApiKeyInMap(configMap)
masked, _ := json.Marshal(configMap)
m[k] = string(masked)
}
}
} else if subMap, ok := v.(map[string]interface{}); ok {
maskApiKeyInMap(subMap)
}
}
}