万川平台对接 - 新增 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 单元测试(视觉回退分支、陈旧索引判定)
243 lines
7.1 KiB
Go
243 lines
7.1 KiB
Go
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)
|
||
}
|
||
}
|
||
}
|