feat: v0.4.0 主题切换 + LLM 全面优化 + 登录会话 + 用户管理/LDAP
主题: - 深色/浅色/跟随系统(useTheme:本地缓存先行+远端同步+matchMedia), 侧边栏快捷切换,设置页三档即选即生效 LLM(参考 opsxdb/aiops): - llm.Client.ChatStream 流式(SSE 增量解析,透传 reasoning_content, ctx 控制超时);POST /api/ai/chat/stream(平台实时数据注入降幻觉, 未配 LLM 降级规则 JSON,指令解析 action 不变) - test-llm 支持草稿直测(保存前验证,apiKey 空/掩码回退已存) - 发布失败自动 AI 分析日志尾部(结论/根因/建议),发布记录与发布 Tab 展示分析卡片 - 设置页 Provider 预设(DeepSeek/GLM/GLM编码/Qwen/Kimi/Ollama/自定义) 自动填 endpoint+模型+获取 Key 直达链接 登录与会话: - HMAC 签名 token(7 天,密钥派生自 encryption.key); authMiddleware 保护 /api/*(豁免登录/健康/SSE日志流/入站webhook) - 登录页 /login + 未登录跳转 + 登出;401 自动清理跳登录 用户管理与 LDAP(借鉴 LokiStack): - /users 页面:本地用户 CRUD、一次性密码、角色切换、来源徽章 (本地·蓝 | LDAP导入·紫);LDAP 导入按钮仅在启用且配置齐备时显示 - LDAP 配置卡(未启用仅提示语)、测试连接草稿直测+可选测试账号 - go-ldap:管理 Bind/用户搜索(防注入)/用户 DN 直连验证; 批量导入冲突策略(已LDAP跳过/本地同名转LDAP/新建); 登录分流本地 argon2id 或 LDAP 目录验证 + 首登 JIT 建号
This commit is contained in:
1 parent
555934cc64
commit
97193eb090
24 files changed
+2422
-98
No files matched your search
@@ -2,6 +2,7 @@ package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -91,6 +92,7 @@ func GetSettings(c *gin.Context) {
|
||||
}
|
||||
s.LlmApiKeySet = s.LlmApiKey != ""
|
||||
s.LarkSecretSet = s.LarkSecret != ""
|
||||
s.LdapBindPasswordSet = s.LdapBindPassword != ""
|
||||
c.JSON(http.StatusOK, s)
|
||||
}
|
||||
|
||||
@@ -158,27 +160,50 @@ func UpdateSettings(c *gin.Context) {
|
||||
database.DB.First(&s, 1)
|
||||
s.LlmApiKeySet = s.LlmApiKey != ""
|
||||
s.LarkSecretSet = s.LarkSecret != ""
|
||||
s.LdapBindPasswordSet = s.LdapBindPassword != ""
|
||||
c.JSON(http.StatusOK, s)
|
||||
}
|
||||
|
||||
// TestLLM POST /api/settings/test-llm —— 用已保存配置做 ping 对话
|
||||
// TestLLM POST /api/settings/test-llm —— 支持草稿直测:body 带表单值时优先(apiKey 空=用已存)
|
||||
func TestLLM(c *gin.Context) {
|
||||
var s model.SystemSetting
|
||||
if err := database.DB.First(&s, 1).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "请先保存设置"})
|
||||
return
|
||||
s = model.SystemSetting{}
|
||||
}
|
||||
if !s.LlmEnabled || s.LlmApiKey == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "AI 未启用或未配置 API Key"})
|
||||
return
|
||||
var body struct {
|
||||
LlmEnabled *bool `json:"llmEnabled"`
|
||||
LlmEndpoint string `json:"llmEndpoint"`
|
||||
LlmModel string `json:"llmModel"`
|
||||
LlmApiKey string `json:"llmApiKey"`
|
||||
}
|
||||
apiKey := s.LlmApiKey
|
||||
if crypto.IsEncrypted(apiKey) {
|
||||
if plain, err := crypto.DecryptField(apiKey); err == nil {
|
||||
apiKey = plain
|
||||
_ = c.ShouldBindJSON(&body)
|
||||
|
||||
enabled := s.LlmEnabled
|
||||
if body.LlmEnabled != nil {
|
||||
enabled = *body.LlmEnabled
|
||||
}
|
||||
endpoint := body.LlmEndpoint
|
||||
if endpoint == "" {
|
||||
endpoint = s.LlmEndpoint
|
||||
}
|
||||
model_ := body.LlmModel
|
||||
if model_ == "" {
|
||||
model_ = s.LlmModel
|
||||
}
|
||||
apiKey := body.LlmApiKey
|
||||
if apiKey == "" || strings.Contains(apiKey, "****") {
|
||||
apiKey = s.LlmApiKey
|
||||
if crypto.IsEncrypted(apiKey) {
|
||||
if plain, err := crypto.DecryptField(apiKey); err == nil {
|
||||
apiKey = plain
|
||||
}
|
||||
}
|
||||
}
|
||||
client := llm.NewClient(s.LlmEndpoint, apiKey, s.LlmModel, 128)
|
||||
if !enabled || apiKey == "" || endpoint == "" || model_ == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "请填写启用/接口/模型/API Key(或先保存已有配置)"})
|
||||
return
|
||||
}
|
||||
client := llm.NewClient(endpoint, apiKey, model_, 128)
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
reply, err := client.Chat(ctx, "你是连通性检查助手,只回复:连接成功。", "ping")
|
||||
|
||||
Reference in new issue
Block a user