feat: Jenkins Job 批量发布 + 全局 AI 智能助手 (v0.3.0)
参考 opsx-jenkins 发布清单: - 批量发布页新增「粘贴清单」模式:多行 Job 名粘贴(#注释/内联对/ 名称版本交替)、TAG/分支切换、分支快捷 chips、700ms 防抖解析预览、 串行/并发发布;未导入的 Jenkins Job 自动创建流水线后触发 - 解析器移植 opsx-jenkins parser(参数头识别/成对校验/重名检测) 全局 AI 智能助手(右下角悬浮): - 自然语言「发布一下 cex-agent-server 等 5 个」→ 解析服务+分支+环境 → 发布计划卡片(匹配状态着色)→ 确认 → 真实触发 → runId 回显 + 跳转发布记录 - LLM 接入:OpenAI 兼容接口(DeepSeek/GLM/Qwen),系统设置页配置 API Key(加密存储)+ 测试连接;未配置时自动降级本地规则模式, 服务名/分支关键词匹配,功能不中断 - 候选池 = 流水线 + Jenkins Job(60s 缓存);精确/忽略大小写/包含 三级匹配 新端点:POST /api/ai/chat、/api/ai/release、/api/batch/parse、 POST /api/settings/test-llm;SystemSetting 扩展 LLM 字段
This commit is contained in:
1 parent
88d8fc282e
commit
581d0a8351
12 files changed
+1415
-2
No files matched your search
@@ -1,12 +1,15 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"flowstack/internal/crypto"
|
||||
"flowstack/internal/database"
|
||||
"flowstack/internal/llm"
|
||||
"flowstack/internal/model"
|
||||
)
|
||||
|
||||
@@ -83,9 +86,10 @@ func GetSummary(c *gin.Context) {
|
||||
func GetSettings(c *gin.Context) {
|
||||
var s model.SystemSetting
|
||||
if err := database.DB.First(&s, 1).Error; err != nil {
|
||||
s = model.SystemSetting{ID: 1}
|
||||
s = model.SystemSetting{ID: 1, LlmEndpoint: "https://api.deepseek.com/v1", LlmModel: "deepseek-chat", LlmMaxTokens: 1200}
|
||||
database.DB.Create(&s)
|
||||
}
|
||||
s.LlmApiKeySet = s.LlmApiKey != ""
|
||||
c.JSON(http.StatusOK, s)
|
||||
}
|
||||
|
||||
@@ -114,10 +118,61 @@ func UpdateSettings(c *gin.Context) {
|
||||
if v, ok := body["extra"].(string); ok {
|
||||
updates["extra"] = v
|
||||
}
|
||||
if v, ok := body["llmEnabled"].(bool); ok {
|
||||
updates["llm_enabled"] = v
|
||||
}
|
||||
if v, ok := body["llmEndpoint"].(string); ok {
|
||||
updates["llm_endpoint"] = v
|
||||
}
|
||||
if v, ok := body["llmModel"].(string); ok {
|
||||
updates["llm_model"] = v
|
||||
}
|
||||
if v, ok := body["llmMaxTokens"].(float64); ok {
|
||||
updates["llm_max_tokens"] = int(v)
|
||||
}
|
||||
// API Key 只写不读:非空才更新
|
||||
if v, ok := body["llmApiKey"].(string); ok && v != "" {
|
||||
enc, err := crypto.EncryptField(v)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "API Key 加密失败"})
|
||||
return
|
||||
}
|
||||
updates["llm_api_key"] = enc
|
||||
}
|
||||
database.DB.Model(&s).Updates(updates)
|
||||
database.DB.First(&s, 1)
|
||||
s.LlmApiKeySet = s.LlmApiKey != ""
|
||||
c.JSON(http.StatusOK, s)
|
||||
}
|
||||
|
||||
// TestLLM POST /api/settings/test-llm —— 用已保存配置做 ping 对话
|
||||
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
|
||||
}
|
||||
if !s.LlmEnabled || s.LlmApiKey == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "AI 未启用或未配置 API Key"})
|
||||
return
|
||||
}
|
||||
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)
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
reply, err := client.Chat(ctx, "你是连通性检查助手,只回复:连接成功。", "ping")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "测试失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": reply})
|
||||
}
|
||||
|
||||
// CreatePipeline POST /api/pipelines
|
||||
func CreatePipeline(c *gin.Context) {
|
||||
var input struct {
|
||||
|
||||
Reference in new issue
Block a user