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:
242
wanchuan_proxy.go
Normal file
242
wanchuan_proxy.go
Normal file
@@ -0,0 +1,242 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user