mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-30 16:05:30 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
832d367428 | ||
|
|
e6c50b89ba | ||
|
|
f2aa51066d | ||
|
|
3f594bd7a2 | ||
|
|
02031e4d7c | ||
|
|
2f7bab67b3 | ||
|
|
2d473042fc | ||
|
|
8885463a75 | ||
|
|
01feaa6059 | ||
|
|
ecb264f087 | ||
|
|
8bae2f6779 | ||
|
|
87ab399bcc | ||
|
|
ecedaf8c21 | ||
|
|
d7c42e3e8b | ||
|
|
d32cbb40f6 | ||
|
|
8684a26459 | ||
|
|
1798f9768e | ||
|
|
eb4beb50c4 | ||
|
|
3a7cd211af | ||
|
|
bd0bc0079e | ||
|
|
93ed58d784 | ||
|
|
8d320ef2ca | ||
|
|
b3c1e0df42 | ||
|
|
5346d5f58a | ||
|
|
ac09426f10 | ||
|
|
19707a68dd | ||
|
|
7c7fd492c4 | ||
|
|
fbf66766c2 | ||
|
|
d7355f3ccc | ||
|
|
93ca724985 | ||
|
|
9481a7535d | ||
|
|
14be5b7870 | ||
|
|
c4b76a8e89 | ||
|
|
9295f4ef8a | ||
|
|
c925184bf6 | ||
|
|
87828b03ae | ||
|
|
e7b4146e1b | ||
|
|
ec998552b9 | ||
|
|
1af237944d | ||
|
|
f9f9169a4d | ||
|
|
1e01761ba3 | ||
|
|
15283fde34 | ||
|
|
49f8e7f040 | ||
|
|
98b27af6a0 | ||
|
|
a393224729 | ||
|
|
b851c84290 | ||
|
|
372855157d | ||
|
|
577d771495 | ||
|
|
ca3e6511e5 | ||
|
|
8af14eae25 | ||
|
|
175fbaf3f2 | ||
|
|
13199efd62 | ||
|
|
04099ae2a2 | ||
|
|
c695a2a408 | ||
|
|
690cb48280 | ||
|
|
f226fed8cd | ||
|
|
110c292856 | ||
|
|
8e865c2c46 | ||
|
|
17b8f472e7 | ||
|
|
d9dc5d7aa9 | ||
|
|
547f9775b0 | ||
|
|
f37afb1a37 | ||
|
|
3b89d83554 | ||
|
|
12bfe6a1bd | ||
|
|
aaf70cfc41 | ||
|
|
1ecd4f7f0c | ||
|
|
a8ccdcade7 | ||
|
|
4a89c0c031 | ||
|
|
f21ebb1a36 | ||
|
|
3011383b07 | ||
|
|
fd12866897 | ||
|
|
bc6cf519ce | ||
|
|
f9c43989fe | ||
|
|
e90b201001 | ||
|
|
f7394938f7 | ||
|
|
dada7294dd | ||
|
|
afcb94f6ef | ||
|
|
5a2e430dff | ||
|
|
f2ca4f56e6 | ||
|
|
b6f609aaee | ||
|
|
c960c0137d | ||
|
|
09a901294e | ||
|
|
4d481a8f31 | ||
|
|
f1e0aa3391 | ||
|
|
f475500543 | ||
|
|
8415a1ce28 | ||
|
|
faf4933a02 | ||
|
|
c692a9f5e3 | ||
|
|
e7f0c21715 | ||
|
|
da7e8f1dbf | ||
|
|
486cd5d0ad | ||
|
|
00ce5a7b36 | ||
|
|
c17f055e06 | ||
|
|
f22df4b29c | ||
|
|
b9bab90a14 | ||
|
|
ce14a104cc | ||
|
|
2eaabccf94 | ||
|
|
e119239d3d | ||
|
|
14a57af4e9 | ||
|
|
2701af9054 | ||
|
|
4954067736 | ||
|
|
b962144e7d | ||
|
|
8f1be42b6b | ||
|
|
c6969f231b | ||
|
|
3571fb8d86 | ||
|
|
d21288788e | ||
|
|
f65e4b06ab | ||
|
|
d26bc9a3a4 | ||
|
|
7088a903fe | ||
|
|
864ebeed82 | ||
|
|
c1b2b92708 | ||
|
|
2189d9229a | ||
|
|
d0183f460e | ||
|
|
d27d6db1ea | ||
|
|
d2bb3813d9 | ||
|
|
cac73e6d45 | ||
|
|
4712ceaf2c | ||
|
|
65243c68c4 | ||
|
|
256e79ca81 | ||
|
|
4861eb69cb | ||
|
|
fb8cf15537 | ||
|
|
c4a6791271 | ||
|
|
f58e147636 | ||
|
|
387e9fbeed | ||
|
|
4eb627bc79 | ||
|
|
850c86229c | ||
|
|
f984917a66 | ||
|
|
8588217fbf | ||
|
|
a2307c5f64 | ||
|
|
5923290de8 | ||
|
|
1c994fba4a | ||
|
|
415ab96aab | ||
|
|
19bb823b05 | ||
|
|
3a5371652e | ||
|
|
a267b4148a | ||
|
|
36a01eb60d | ||
|
|
65f6fdd045 | ||
|
|
75b60b32e4 | ||
|
|
6cb65e2290 | ||
|
|
0bad1b471f | ||
|
|
3814525edd | ||
|
|
8162dd1856 | ||
|
|
e833787020 | ||
|
|
673ffac516 | ||
|
|
e864610015 | ||
|
|
78402e1b7d | ||
|
|
782bc1e67c | ||
|
|
86e4ed6f64 | ||
|
|
ee8bac39af | ||
|
|
fe742b9f41 | ||
|
|
9a5bd9bcba | ||
|
|
b9c8e39560 | ||
|
|
6b20ff0b13 | ||
|
|
89bd32b6d4 | ||
|
|
005f240fb7 | ||
|
|
75da53e374 |
@@ -826,6 +826,26 @@ func (b *BaseApi) UpdateAgentFeishuConfig(c *gin.Context) {
|
|||||||
helper.Success(c)
|
helper.Success(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags AI
|
||||||
|
// @Summary Switch to the bundled OpenClaw Feishu plugin
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.AgentFeishuSwitchReq true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /ai/agents/channel/feishu/switch [post]
|
||||||
|
func (b *BaseApi) SwitchAgentFeishuPlugin(c *gin.Context) {
|
||||||
|
var req dto.AgentFeishuSwitchReq
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := agentService.SwitchFeishuPlugin(req); err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
// @Tags AI
|
// @Tags AI
|
||||||
// @Summary Get Agent Telegram channel config
|
// @Summary Get Agent Telegram channel config
|
||||||
// @Accept json
|
// @Accept json
|
||||||
|
|||||||
+12
-16
@@ -72,22 +72,6 @@ func (b *BaseApi) CloseOllamaModel(c *gin.Context) {
|
|||||||
helper.SuccessWithData(c, nil)
|
helper.SuccessWithData(c, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags AI
|
|
||||||
// @Summary Sync Ollama model list
|
|
||||||
// @Success 200 {array} dto.OllamaModelDropList
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Security Timestamp
|
|
||||||
// @Router /ai/ollama/model/sync [post]
|
|
||||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 Ollama 模型列表","formatEN":"sync Ollama model list"}
|
|
||||||
func (b *BaseApi) SyncOllamaModel(c *gin.Context) {
|
|
||||||
list, err := aiToolService.Sync()
|
|
||||||
if err != nil {
|
|
||||||
helper.BadRequest(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
helper.SuccessWithData(c, list)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Tags AI
|
// @Tags AI
|
||||||
// @Summary Page Ollama models
|
// @Summary Page Ollama models
|
||||||
// @Accept json
|
// @Accept json
|
||||||
@@ -114,6 +98,18 @@ func (b *BaseApi) SearchOllamaModel(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *BaseApi) SyncOllamaModelAccount(c *gin.Context) {
|
||||||
|
var req dto.OllamaModelAccountSync
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := aiToolService.SyncAccount(req); err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
// @Tags AI
|
// @Tags AI
|
||||||
// @Summary Page Ollama models
|
// @Summary Page Ollama models
|
||||||
// @Accept json
|
// @Accept json
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/service"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (b *BaseApi) AIBenchmarkSources(c *gin.Context) {
|
||||||
|
sources, err := service.AIBenchmarkSources(c.Query("kind"), c.Query("selected"))
|
||||||
|
if err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, sources)
|
||||||
|
}
|
||||||
@@ -10,6 +10,9 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/webhook_sender"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -290,7 +293,7 @@ func (b *BaseApi) PageAlertConfig(c *gin.Context) {
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /alert/config/update [post]
|
// @Router /alert/config/update [post]
|
||||||
// @x-panel-log {"bodyKeys":["id","displayName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置 [id][displayName]","formatEN":"update alert config [id][displayName]"}
|
// @x-panel-log {"bodyKeys":["displayName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置 [displayName]","formatEN":"update alert config [displayName]"}
|
||||||
func (b *BaseApi) UpdateAlertConfig(c *gin.Context) {
|
func (b *BaseApi) UpdateAlertConfig(c *gin.Context) {
|
||||||
var req dto.AlertConfigUpdate
|
var req dto.AlertConfigUpdate
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
@@ -318,13 +321,15 @@ func (b *BaseApi) UpdateAlertConfig(c *gin.Context) {
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /alert/config/status [post]
|
// @Router /alert/config/status [post]
|
||||||
// @x-panel-log {"bodyKeys":["id","status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置状态 [id][status]","formatEN":"update alert config status [id][status]"}
|
// @x-panel-log {"bodyKeys":["status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置状态 [name][status]","formatEN":"update alert config status [name][status]"}
|
||||||
func (b *BaseApi) UpdateAlertConfigStatus(c *gin.Context) {
|
func (b *BaseApi) UpdateAlertConfigStatus(c *gin.Context) {
|
||||||
var req dto.AlertConfigStatusUpdate
|
var req dto.AlertConfigStatusUpdate
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := alertService.UpdateAlertConfigStatus(req, loadAuditUser(c)); err != nil {
|
name, err := alertService.UpdateAlertConfigStatus(req, loadAuditUser(c))
|
||||||
|
helper.SetOperationLogValues(c, map[string]string{"name": name})
|
||||||
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -350,13 +355,14 @@ func loadAuditUser(c *gin.Context) string {
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /alert/config/del [post]
|
// @Router /alert/config/del [post]
|
||||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除告警配置 [id]","formatEN":"delete alert config [id]"}
|
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除告警配置 [name]","formatEN":"delete alert config [name]"}
|
||||||
func (b *BaseApi) DeleteAlertConfig(c *gin.Context) {
|
func (b *BaseApi) DeleteAlertConfig(c *gin.Context) {
|
||||||
var req dto.DeleteRequest
|
var req dto.DeleteRequest
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := alertService.DeleteAlertConfig(req.ID)
|
name, err := alertService.DeleteAlertConfig(req.ID)
|
||||||
|
helper.SetOperationLogValues(c, map[string]string{"name": name})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -378,10 +384,24 @@ func (b *BaseApi) TestAlertConfig(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if req.Type == constant.Custom {
|
if req.Type == constant.Custom {
|
||||||
result, err := alertService.TestCustomAlertConfig(req)
|
lang := strings.TrimSpace(c.GetHeader("Accept-Language"))
|
||||||
|
if lang == "" {
|
||||||
|
lang = i18n.GetLanguageFromDB()
|
||||||
|
}
|
||||||
|
result, err := alertService.TestCustomAlertConfig(req, lang)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
if global.LOG != nil {
|
||||||
return
|
global.LOG.Warnf("webhook test preparation failed: configID=%d error=%q", req.ID,
|
||||||
|
webhook_sender.DiagnosticText(err.Error(), webhook_sender.Request{
|
||||||
|
Format: webhook_sender.BodyJSON, Body: []byte(req.Config),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
result = dto.AlertConfigTestResult{Success: false}
|
||||||
|
}
|
||||||
|
result.Response = ""
|
||||||
|
result.Message = ""
|
||||||
|
if !result.Success {
|
||||||
|
result.Message = i18n.GetMsgByKeyAndLang(lang, "ErrWebhookTestFailed")
|
||||||
}
|
}
|
||||||
helper.SuccessWithData(c, result)
|
helper.SuccessWithData(c, result)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ func (b *BaseApi) GetAppListUpdate(c *gin.Context) {
|
|||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param key path string true "app key"
|
// @Param key path string true "app key"
|
||||||
// @Success 200 {file} file "app icon"
|
// @Success 200 {file} file "app icon"
|
||||||
|
// @Success 302 "redirect to the official icon URL"
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /apps/icon/:key [get]
|
// @Router /apps/icon/:key [get]
|
||||||
@@ -216,12 +217,18 @@ func (b *BaseApi) GetAppIcon(c *gin.Context) {
|
|||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
iconBytes, filename, etag, err := appService.GetAppIcon(appKey)
|
iconBytes, source, etag, err := appService.GetAppIcon(appKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if appicon.IsRemoteURL(source) {
|
||||||
|
c.Header("Cache-Control", "no-cache")
|
||||||
|
c.Redirect(http.StatusFound, source)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if len(iconBytes) == 0 {
|
if len(iconBytes) == 0 {
|
||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
return
|
return
|
||||||
@@ -229,7 +236,7 @@ func (b *BaseApi) GetAppIcon(c *gin.Context) {
|
|||||||
|
|
||||||
c.Header("Cache-Control", "public, max-age=2592000")
|
c.Header("Cache-Control", "public, max-age=2592000")
|
||||||
|
|
||||||
if etag != "" && filename != "" {
|
if etag != "" && source != "" {
|
||||||
c.Header("ETag", etag)
|
c.Header("ETag", etag)
|
||||||
if c.GetHeader("If-None-Match") == etag {
|
if c.GetHeader("If-None-Match") == etag {
|
||||||
c.Status(http.StatusNotModified)
|
c.Status(http.StatusNotModified)
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/appicon"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/glebarez/sqlite"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAppIconEndpointPreservesRemoteAndHistoricalSources(t *testing.T) {
|
||||||
|
db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "icons.db")), &gorm.Config{
|
||||||
|
Logger: logger.Default.LogMode(logger.Silent),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := db.AutoMigrate(&model.App{}, &model.AppDetail{}, &model.AppTag{}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
oldDB, oldDir, oldLog := global.DB, global.Dir, global.LOG
|
||||||
|
global.DB = db
|
||||||
|
global.Dir.IconCacheDir = t.TempDir()
|
||||||
|
global.LOG = logrus.New()
|
||||||
|
global.LOG.SetOutput(io.Discard)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
global.DB, global.Dir, global.LOG = oldDB, oldDir, oldLog
|
||||||
|
sqlDB, _ := db.DB()
|
||||||
|
_ = sqlDB.Close()
|
||||||
|
})
|
||||||
|
|
||||||
|
var imageRequests atomic.Int32
|
||||||
|
cdn := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
imageRequests.Add(1)
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
}))
|
||||||
|
t.Cleanup(cdn.Close)
|
||||||
|
remoteURL := cdn.URL + "/icon.png?token=a%2Bb&v=42#image"
|
||||||
|
image := "\x89PNG\r\n\x1a\noriginal-icon"
|
||||||
|
fileName := "app_php.png"
|
||||||
|
if err := os.WriteFile(filepath.Join(global.Dir.IconCacheDir, fileName), []byte(image), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
app := model.App{Key: "php", Resource: constant.AppResourceRemote}
|
||||||
|
if err := db.Create(&app).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
router := gin.New()
|
||||||
|
router.GET("/api/v2/apps/icon/:key", (&BaseApi{}).GetAppIcon)
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
icon string
|
||||||
|
ifNoneMatch string
|
||||||
|
status int
|
||||||
|
body string
|
||||||
|
etag string
|
||||||
|
}{
|
||||||
|
{"remote URL", remoteURL, "", http.StatusFound, "", ""},
|
||||||
|
{"remote URL ignores legacy etag", remoteURL, `"old"`, http.StatusFound, "", ""},
|
||||||
|
{"historical file", appicon.BuildIconField(fileName, `"42"`), "", http.StatusOK, image, `"42"`},
|
||||||
|
{"historical file cached", appicon.BuildIconField(fileName, `"42"`), `"42"`, http.StatusNotModified, "", `"42"`},
|
||||||
|
{"historical file changed", appicon.BuildIconField(fileName, `"42"`), `"old"`, http.StatusOK, image, `"42"`},
|
||||||
|
{"historical base64", base64.StdEncoding.EncodeToString([]byte(image)), "", http.StatusOK, image, ""},
|
||||||
|
{"missing file", "app_missing.png?etag=old", "", http.StatusNoContent, "", ""},
|
||||||
|
{"invalid base64", "not-an-icon", "", http.StatusNoContent, "", ""},
|
||||||
|
{"invalid URL", "javascript:alert(1)", "", http.StatusNoContent, "", ""},
|
||||||
|
{"empty icon", "", "", http.StatusNoContent, "", ""},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if err := db.Model(&app).UpdateColumn("icon", tc.icon).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/v2/apps/icon/php?operateNode=child", nil)
|
||||||
|
req.Header.Set("If-None-Match", tc.ifNoneMatch)
|
||||||
|
res := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(res, req)
|
||||||
|
if res.Code != tc.status {
|
||||||
|
t.Fatalf("status = %d, want %d; body = %s", res.Code, tc.status, res.Body.String())
|
||||||
|
}
|
||||||
|
if res.Header().Get("ETag") != tc.etag {
|
||||||
|
t.Fatalf("etag = %q, want %q", res.Header().Get("ETag"), tc.etag)
|
||||||
|
}
|
||||||
|
if tc.status == http.StatusFound {
|
||||||
|
if res.Header().Get("Location") != remoteURL || res.Header().Get("Cache-Control") != "no-cache" {
|
||||||
|
t.Fatalf("redirect URL or cache policy was changed: %v", res.Header())
|
||||||
|
}
|
||||||
|
} else if res.Body.String() != tc.body {
|
||||||
|
t.Fatalf("body = %q, want %q", res.Body.String(), tc.body)
|
||||||
|
}
|
||||||
|
if imageRequests.Load() != 0 {
|
||||||
|
t.Fatalf("icon endpoint fetched the image %d times", imageRequests.Load())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -203,6 +203,7 @@ func (b *BaseApi) GetServices(c *gin.Context) {
|
|||||||
// @Tags App
|
// @Tags App
|
||||||
// @Summary Search app update version by install id
|
// @Summary Search app update version by install id
|
||||||
// @Accept json
|
// @Accept json
|
||||||
|
// @Param request body request.AppUpdateVersion true "request"
|
||||||
// @Success 200 {array} dto.AppVersion
|
// @Success 200 {array} dto.AppVersion
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func (b *BaseApi) SearchContainer(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
total, list, err := containerService.Page(req)
|
total, list, err := containerService.Page(c.Request.Context(), req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -249,9 +249,10 @@ func (b *BaseApi) ListContainerByImage(c *gin.Context) {
|
|||||||
// @Success 200 {object} dto.ContainerStatus
|
// @Success 200 {object} dto.ContainerStatus
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
|
// @Param containersOnly query boolean false "Only count containers"
|
||||||
// @Router /containers/status [get]
|
// @Router /containers/status [get]
|
||||||
func (b *BaseApi) LoadContainerStatus(c *gin.Context) {
|
func (b *BaseApi) LoadContainerStatus(c *gin.Context) {
|
||||||
data, err := containerService.LoadStatus()
|
data, err := containerService.LoadStatus(c.Request.Context(), c.Query("containersOnly") == "true")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -415,9 +416,14 @@ func (b *BaseApi) LoadResourceLimit(c *gin.Context) {
|
|||||||
// @Success 200 {array} dto.ContainerListStats
|
// @Success 200 {array} dto.ContainerListStats
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
|
// @Param ids query string false "Comma-separated container IDs; omitted selects all containers"
|
||||||
// @Router /containers/list/stats [get]
|
// @Router /containers/list/stats [get]
|
||||||
func (b *BaseApi) ContainerListStats(c *gin.Context) {
|
func (b *BaseApi) ContainerListStats(c *gin.Context) {
|
||||||
data, err := containerService.ContainerListStats()
|
var ids []string
|
||||||
|
if _, supplied := c.Request.URL.Query()["ids"]; supplied {
|
||||||
|
ids = strings.FieldsFunc(c.Query("ids"), func(r rune) bool { return r == ',' })
|
||||||
|
}
|
||||||
|
data, err := containerService.ContainerListStats(c.Request.Context(), ids)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ func (b *BaseApi) CreateMysql(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
req.Password = string(password)
|
req.Password = string(password)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := mysqlService.Create(context.Background(), req); err != nil {
|
if _, err := mysqlService.Create(context.Background(), req); err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -86,17 +86,23 @@ func (b *BaseApi) CheckHasCli(c *gin.Context) {
|
|||||||
|
|
||||||
// @Tags Database Redis
|
// @Tags Database Redis
|
||||||
// @Summary Install redis-cli
|
// @Summary Install redis-cli
|
||||||
// @Success 200
|
// @Accept json
|
||||||
|
// @Param request body dto.RedisCliInstall true "request"
|
||||||
|
// @Success 200 {string} string
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /databases/redis/install/cli [post]
|
// @Router /databases/redis/install/cli [post]
|
||||||
func (b *BaseApi) InstallCli(c *gin.Context) {
|
func (b *BaseApi) InstallCli(c *gin.Context) {
|
||||||
if err := redisService.InstallCli(); err != nil {
|
var req dto.RedisCliInstall
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := redisService.InstallCli(req)
|
||||||
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
helper.SuccessWithData(c, data)
|
||||||
helper.Success(c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Database Redis
|
// @Tags Database Redis
|
||||||
|
|||||||
+164
-168
@@ -2,14 +2,14 @@ package v2
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/service"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -60,7 +60,7 @@ func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) {
|
|||||||
// @Summary Operate firewall
|
// @Summary Operate firewall
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.FirewallLifecycleOperation true "request"
|
// @Param request body dto.FirewallLifecycleOperation true "request"
|
||||||
// @Success 200
|
// @Success 200 {object} dto.FirewallLifecycleOperationResponse
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/firewall/operate [post]
|
// @Router /hosts/firewall/operate [post]
|
||||||
@@ -71,12 +71,13 @@ func (b *BaseApi) OperateFirewall(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := firewallService.OperateFirewall(request); err != nil {
|
result, err := firewallService.QueueFirewallOperation(request)
|
||||||
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
helper.Success(c)
|
helper.SuccessWithData(c, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
@@ -87,7 +88,7 @@ func (b *BaseApi) OperateFirewall(c *gin.Context) {
|
|||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/firewall/forward/base [post]
|
// @Router /hosts/firewall/forward/base [post]
|
||||||
func (b *BaseApi) LoadForwardingBaseInfo(c *gin.Context) {
|
func (b *BaseApi) LoadForwardingBaseInfo(c *gin.Context) {
|
||||||
data, err := forwardingService.LoadBaseInfo()
|
data, err := forwardingService.LoadBaseInfo(c.Request.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -108,7 +109,7 @@ func (b *BaseApi) SearchForwardingRules(c *gin.Context) {
|
|||||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
total, items, err := forwardingService.SearchRules(request)
|
total, items, err := forwardingService.SearchRules(c.Request.Context(), request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -259,28 +260,6 @@ func (b *BaseApi) LoadFirewallNativeDetail(c *gin.Context) {
|
|||||||
helper.SuccessWithData(c, info)
|
helper.SuccessWithData(c, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Firewall
|
|
||||||
// @Summary Adopt an external firewall rule
|
|
||||||
// @Accept json
|
|
||||||
// @Param request body dto.FirewallRuleAdopt true "request"
|
|
||||||
// @Success 200
|
|
||||||
// @Failure 400 {object} dto.Response
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Security Timestamp
|
|
||||||
// @Router /hosts/firewall/rules/adopt [post]
|
|
||||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"纳管防火墙规则","formatEN":"adopt firewall rule"}
|
|
||||||
func (b *BaseApi) AdoptFirewallRule(c *gin.Context) {
|
|
||||||
var request dto.FirewallRuleAdopt
|
|
||||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := firewallService.Adopt(c.Request.Context(), request); err != nil {
|
|
||||||
handleFirewallRuleError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
helper.Success(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
// @Summary Queue firewall rule creation
|
// @Summary Queue firewall rule creation
|
||||||
// @Description Creation and import return a taskID immediately; validation and execution results are written to the task log.
|
// @Description Creation and import return a taskID immediately; validation and execution results are written to the task log.
|
||||||
@@ -307,67 +286,8 @@ func (b *BaseApi) CreateFirewallRules(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
// @Summary Preview firewall rule synchronization
|
// @Summary Queue firewall rule deletion
|
||||||
// @Accept json
|
// @Description Deletes non-whitelist rules by scope and instance key. Returns a taskID immediately; results are written to the task log.
|
||||||
// @Param request body dto.FirewallRuleSyncRequest true "request"
|
|
||||||
// @Success 200 {object} dto.FirewallRuleSyncPreview
|
|
||||||
// @Failure 400 {object} dto.Response
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Security Timestamp
|
|
||||||
// @Router /hosts/firewall/rules/sync/preview [post]
|
|
||||||
func (b *BaseApi) PreviewFirewallRuleSync(c *gin.Context) {
|
|
||||||
var request dto.FirewallRuleSyncRequest
|
|
||||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
result, err := firewallService.PreviewRuleSync(c.Request.Context(), c.ClientIP(), request)
|
|
||||||
if err != nil {
|
|
||||||
handleFirewallRuleError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
helper.SuccessWithData(c, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Tags Firewall
|
|
||||||
// @Summary Load the currently executing firewall rule synchronization task
|
|
||||||
// @Success 200 {object} dto.FirewallRuleSyncTask
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Security Timestamp
|
|
||||||
// @Router /hosts/firewall/rules/sync/task [get]
|
|
||||||
func (b *BaseApi) LoadFirewallRuleSyncTask(c *gin.Context) {
|
|
||||||
result, err := firewallService.CurrentRuleSyncTask()
|
|
||||||
if err != nil {
|
|
||||||
helper.InternalServer(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
helper.SuccessWithData(c, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Tags Firewall
|
|
||||||
// @Summary Synchronize firewall rules to a target backend
|
|
||||||
// @Accept json
|
|
||||||
// @Param request body dto.FirewallRuleSyncRequest true "request"
|
|
||||||
// @Success 200 {object} dto.FirewallRuleSyncResult
|
|
||||||
// @Failure 400 {object} dto.Response
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Security Timestamp
|
|
||||||
// @Router /hosts/firewall/rules/sync [post]
|
|
||||||
// @x-panel-log {"bodyKeys":["subsystem","sourceProvider","targetProvider"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 [subsystem] 防火墙规则到 [targetProvider]","formatEN":"sync [subsystem] firewall rules to [targetProvider]"}
|
|
||||||
func (b *BaseApi) SyncFirewallRules(c *gin.Context) {
|
|
||||||
var request dto.FirewallRuleSyncRequest
|
|
||||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
result, err := firewallService.SyncRules(c.Request.Context(), c.ClientIP(), request)
|
|
||||||
if err != nil {
|
|
||||||
handleFirewallRuleError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
helper.SuccessWithData(c, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Tags Firewall
|
|
||||||
// @Summary Delete managed unified firewall v2 rules
|
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.FirewallRuleDelete true "request"
|
// @Param request body dto.FirewallRuleDelete true "request"
|
||||||
// @Success 200 {object} dto.FirewallRuleDeleteResponse
|
// @Success 200 {object} dto.FirewallRuleDeleteResponse
|
||||||
@@ -390,7 +310,7 @@ func (b *BaseApi) DeleteFirewallRules(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
// @Summary Update a managed unified firewall v2 rule
|
// @Summary Update a firewall rule
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.FirewallRuleUpdate true "request"
|
// @Param request body dto.FirewallRuleUpdate true "request"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
@@ -398,16 +318,13 @@ func (b *BaseApi) DeleteFirewallRules(c *gin.Context) {
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/firewall/rules/update [post]
|
// @Router /hosts/firewall/rules/update [post]
|
||||||
// @x-panel-log {"bodyKeys":["uuid"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙规则 [uuid]","formatEN":"update firewall rule [uuid]"}
|
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙规则","formatEN":"update firewall rule"}
|
||||||
func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
|
func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
|
||||||
var request dto.FirewallRuleUpdate
|
var request dto.FirewallRuleUpdate
|
||||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !normalizeFirewallRuleUUID(c, &request.UUID) {
|
if err := firewallService.Update(c.Request.Context(), request); err != nil {
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := firewallService.Update(c.Request.Context(), c.ClientIP(), request); err != nil {
|
|
||||||
handleFirewallRuleError(c, err)
|
handleFirewallRuleError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -415,7 +332,7 @@ func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
// @Summary Reorder a managed unified firewall v2 rule
|
// @Summary Reorder a firewall rule
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.FirewallRuleReorder true "request"
|
// @Param request body dto.FirewallRuleReorder true "request"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
@@ -423,53 +340,41 @@ func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/firewall/rules/reorder [post]
|
// @Router /hosts/firewall/rules/reorder [post]
|
||||||
// @x-panel-log {"bodyKeys":["uuid"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"调整防火墙规则顺序 [uuid]","formatEN":"reorder firewall rule [uuid]"}
|
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"调整防火墙规则顺序","formatEN":"reorder firewall rule"}
|
||||||
func (b *BaseApi) ReorderFirewallRule(c *gin.Context) {
|
func (b *BaseApi) ReorderFirewallRule(c *gin.Context) {
|
||||||
var request dto.FirewallRuleReorder
|
var request dto.FirewallRuleReorder
|
||||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !normalizeFirewallRuleUUID(c, &request.UUID) {
|
if err := firewallService.Reorder(c.Request.Context(), request); err != nil {
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := firewallService.Reorder(c.Request.Context(), c.ClientIP(), request); err != nil {
|
|
||||||
handleFirewallRuleError(c, err)
|
handleFirewallRuleError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
helper.Success(c)
|
helper.Success(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeFirewallRuleUUID(c *gin.Context, value *string) bool {
|
|
||||||
if value == nil {
|
|
||||||
helper.BadRequest(c, repo.ErrFirewallPersistenceInvalid)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
*value = strings.TrimSpace(*value)
|
|
||||||
if *value == "" {
|
|
||||||
helper.BadRequest(c, repo.ErrFirewallPersistenceInvalid)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleFirewallRuleError(c *gin.Context, err error) {
|
func handleFirewallRuleError(c *gin.Context, err error) {
|
||||||
|
var businessErr buserr.BusinessError
|
||||||
|
isBusinessError := errors.As(err, &businessErr)
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, filter.ErrProtectedRule):
|
case errors.Is(err, filter.ErrProtectedRule):
|
||||||
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_LOCKOUT_RISK", "ErrInvalidParams", err)
|
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_LOCKOUT_RISK", "ErrInvalidParams", err)
|
||||||
case errors.Is(err, filter.ErrRuleStale):
|
case errors.Is(err, filter.ErrRuleStale):
|
||||||
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_STALE", "ErrInvalidParams", err)
|
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_STALE", "ErrInvalidParams", err)
|
||||||
case errors.Is(err, repo.ErrFirewallRuleRevisionConflict):
|
|
||||||
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_REVISION_CONFLICT", "ErrInvalidParams", err)
|
|
||||||
case errors.Is(err, filter.ErrManagedScopeChange):
|
|
||||||
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_SCOPE_UNSUPPORTED", "ErrFirewallRuleScopeChange", err)
|
|
||||||
case errors.Is(err, filter.ErrUnsupportedScope), errors.Is(err, filter.ErrInvalidScope),
|
case errors.Is(err, filter.ErrUnsupportedScope), errors.Is(err, filter.ErrInvalidScope),
|
||||||
errors.Is(err, filter.ErrProviderUnavailable), errors.Is(err, filter.ErrAdapterUnavailable):
|
errors.Is(err, filter.ErrProviderUnavailable), errors.Is(err, filter.ErrAdapterUnavailable):
|
||||||
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_SCOPE_UNSUPPORTED", "ErrInvalidParams", err)
|
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_SCOPE_UNSUPPORTED", "ErrInvalidParams", err)
|
||||||
case errors.Is(err, filter.ErrInvalidRule), errors.Is(err, filter.ErrRuleOperation), errors.Is(err, filter.ErrRuleConflict),
|
case errors.Is(err, filter.ErrInvalidRule), errors.Is(err, filter.ErrRuleOperation):
|
||||||
errors.Is(err, repo.ErrFirewallPersistenceInvalid):
|
|
||||||
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_RULE_UNSUPPORTED", "ErrInvalidParams", err)
|
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_RULE_UNSUPPORTED", "ErrInvalidParams", err)
|
||||||
case errors.Is(err, filter.ErrVerificationFailed):
|
case isBusinessError && businessErr.Msg == "ErrRecordExist":
|
||||||
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_VERIFY_FAILED", "ErrInternalServer", err)
|
c.JSON(http.StatusOK, dto.Response{Code: http.StatusConflict, ErrorCode: "FW_RULE_DUPLICATE", Message: err.Error()})
|
||||||
|
c.Abort()
|
||||||
|
case isBusinessError && businessErr.Msg == "ErrFirewallRuleConflict":
|
||||||
|
c.JSON(http.StatusOK, dto.Response{Code: http.StatusConflict, ErrorCode: "FW_RULE_CONFLICT", Message: err.Error()})
|
||||||
|
c.Abort()
|
||||||
|
case isBusinessError && businessErr.Msg == "ErrInvalidParams":
|
||||||
|
c.JSON(http.StatusOK, dto.Response{Code: http.StatusBadRequest, ErrorCode: "FW_RULE_UNSUPPORTED", Message: err.Error()})
|
||||||
|
c.Abort()
|
||||||
default:
|
default:
|
||||||
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_APPLY_FAILED", "ErrInternalServer", err)
|
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_APPLY_FAILED", "ErrInternalServer", err)
|
||||||
}
|
}
|
||||||
@@ -491,26 +396,69 @@ func (b *BaseApi) LoadFirewallSettings(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
// @Summary Queue firewall port whitelist update
|
// @Summary Create firewall port whitelist rules
|
||||||
// @Description Returns a taskID; configuration save and per-rule results are recorded in the task log.
|
// @Description Saves whitelist configuration and applies missing allowances; existing rules are not removed.
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.FirewallPortWhitelistUpdate true "request"
|
// @Param request body dto.FirewallPortWhitelistCreate true "request"
|
||||||
// @Success 200 {object} dto.FilterChainOperationResponse
|
// @Success 200
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/firewall/settings/whitelist [post]
|
// @Router /hosts/firewall/settings/whitelist [post]
|
||||||
// @x-panel-log {"bodyKeys":["value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙端口白名单 [value]","formatEN":"update firewall port whitelist [value]"}
|
// @x-panel-log {"bodyKeys":["rule"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建防火墙端口白名单","formatEN":"create firewall port whitelist"}
|
||||||
|
func (b *BaseApi) CreateFirewallPortWhitelist(c *gin.Context) {
|
||||||
|
var request dto.FirewallPortWhitelistCreate
|
||||||
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := firewallSettingService.CreatePortWhitelist(c.Request.Context(), request); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Firewall
|
||||||
|
// @Summary Update firewall port whitelist rules
|
||||||
|
// @Description Saves whitelist configuration and applies missing allowances; existing rules are not removed.
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.FirewallPortWhitelistUpdate true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/settings/whitelist/update [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["oldRule","rule"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"编辑防火墙端口白名单","formatEN":"update firewall port whitelist"}
|
||||||
func (b *BaseApi) UpdateFirewallPortWhitelist(c *gin.Context) {
|
func (b *BaseApi) UpdateFirewallPortWhitelist(c *gin.Context) {
|
||||||
var request dto.FirewallPortWhitelistUpdate
|
var request dto.FirewallPortWhitelistUpdate
|
||||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
result, err := firewallSettingService.QueuePortWhitelist(request.Value)
|
if err := firewallSettingService.UpdatePortWhitelist(c.Request.Context(), request); err != nil {
|
||||||
if err != nil {
|
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
helper.SuccessWithData(c, result)
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Firewall
|
||||||
|
// @Summary Delete firewall port whitelist rules
|
||||||
|
// @Description Removes whitelist configuration; existing firewall rules are not removed.
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.FirewallPortWhitelistDelete true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/settings/whitelist/delete [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["rules"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除防火墙端口白名单","formatEN":"delete firewall port whitelist"}
|
||||||
|
func (b *BaseApi) DeleteFirewallPortWhitelist(c *gin.Context) {
|
||||||
|
var request dto.FirewallPortWhitelistDelete
|
||||||
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := firewallSettingService.DeletePortWhitelist(c.Request.Context(), request); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
@@ -528,14 +476,10 @@ func (b *BaseApi) OperateFirewallBackend(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := firewallSettingService.Operate(c.Request.Context(), request); err != nil {
|
if err := firewallSettingService.Operate(c.Request.Context(), request); err != nil {
|
||||||
if errors.Is(err, service.ErrFirewallBackendCleanupRequired) {
|
var businessErr buserr.BusinessError
|
||||||
helper.ErrorWithBusinessCode(
|
if errors.As(err, &businessErr) && businessErr.Msg == "ErrFirewallBackendCleanupRequired" {
|
||||||
c,
|
c.JSON(http.StatusOK, dto.Response{Code: http.StatusConflict, ErrorCode: "FW_BACKEND_CLEANUP_REQUIRED", Message: err.Error()})
|
||||||
http.StatusConflict,
|
c.Abort()
|
||||||
"FW_BACKEND_CLEANUP_REQUIRED",
|
|
||||||
"ErrInvalidParams",
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
@@ -574,21 +518,6 @@ func (b *BaseApi) ListDockerPublishedPorts(c *gin.Context) {
|
|||||||
helper.SuccessWithData(c, data)
|
helper.SuccessWithData(c, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Firewall
|
|
||||||
// @Summary Sync Docker port guard rules
|
|
||||||
// @Success 200
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Security Timestamp
|
|
||||||
// @Router /hosts/firewall/docker/sync [post]
|
|
||||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 Docker 端口防护规则","formatEN":"sync Docker port guard rules"}
|
|
||||||
func (b *BaseApi) SyncDockerPortGuard(c *gin.Context) {
|
|
||||||
if err := dockerPortGuardService.Reconcile(c.Request.Context()); err != nil {
|
|
||||||
handleDockerPortGuardError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
helper.Success(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
// @Summary Operate Docker port guard
|
// @Summary Operate Docker port guard
|
||||||
// @Accept json
|
// @Accept json
|
||||||
@@ -627,7 +556,7 @@ func (b *BaseApi) OperateDockerPortGuard(c *gin.Context) {
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/firewall/docker/policies/delete/batch [post]
|
// @Router /hosts/firewall/docker/policies/delete/batch [post]
|
||||||
// @x-panel-log {"bodyKeys":["uuids"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除 Docker 端口防护策略 [uuids]","formatEN":"delete Docker port guard policies [uuids]"}
|
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除 Docker 端口防护策略","formatEN":"delete Docker port guard policies"}
|
||||||
func (b *BaseApi) DeleteDockerPortGuardPolicies(c *gin.Context) {
|
func (b *BaseApi) DeleteDockerPortGuardPolicies(c *gin.Context) {
|
||||||
var request dto.DockerPortGuardPolicyBatchDelete
|
var request dto.DockerPortGuardPolicyBatchDelete
|
||||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
@@ -664,21 +593,88 @@ func (b *BaseApi) UpsertDockerPortGuardPolicies(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleDockerPortGuardError(c *gin.Context, err error) {
|
func handleDockerPortGuardError(c *gin.Context, err error) {
|
||||||
if errors.Is(err, service.ErrDockerIptablesChainUnavailable) {
|
var businessErr buserr.BusinessError
|
||||||
helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_IPTABLES_CHAIN_UNAVAILABLE", "ErrDockerIptablesChainUnavailable", err)
|
if errors.As(err, &businessErr) {
|
||||||
return
|
code, errorCode := http.StatusInternalServerError, ""
|
||||||
|
switch businessErr.Msg {
|
||||||
|
case "ErrDockerIptablesChainUnavailable":
|
||||||
|
code, errorCode = http.StatusServiceUnavailable, "FW_DOCKER_IPTABLES_CHAIN_UNAVAILABLE"
|
||||||
|
case "ErrDockerNftablesChainUnavailable":
|
||||||
|
code, errorCode = http.StatusServiceUnavailable, "FW_DOCKER_NFTABLES_CHAIN_UNAVAILABLE"
|
||||||
|
case "ErrInvalidParams":
|
||||||
|
code, errorCode = http.StatusBadRequest, "FW_DOCKER_GUARD_INVALID"
|
||||||
|
case "ErrDockerFailed":
|
||||||
|
code, errorCode = http.StatusServiceUnavailable, "FW_DOCKER_UNAVAILABLE"
|
||||||
|
}
|
||||||
|
if errorCode != "" {
|
||||||
|
c.JSON(http.StatusOK, dto.Response{Code: code, ErrorCode: errorCode, Message: err.Error()})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if errors.Is(err, service.ErrDockerNftablesChainUnavailable) {
|
if errors.Is(err, docker.ErrUnavailable) {
|
||||||
helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_NFTABLES_CHAIN_UNAVAILABLE", "ErrDockerNftablesChainUnavailable", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if errors.Is(err, service.ErrDockerGuardInvalid) {
|
|
||||||
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_DOCKER_GUARD_INVALID", "ErrInvalidParams", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if errors.Is(err, service.ErrDockerUnavailable) {
|
|
||||||
helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_UNAVAILABLE", "ErrDockerFailed", err)
|
helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_UNAVAILABLE", "ErrDockerFailed", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_DOCKER_GUARD_FAILED", "ErrInternalServer", err)
|
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_DOCKER_GUARD_FAILED", "ErrInternalServer", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags Firewall
|
||||||
|
// @Summary List firewall rule backups
|
||||||
|
// @Param subsystem query string false "Firewall subsystem" Enums(system,forwarding,docker) default(system)
|
||||||
|
// @Success 200 {object} dto.FirewallRuleBackups
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/rules/backups [get]
|
||||||
|
func (b *BaseApi) ListFirewallRuleBackups(c *gin.Context) {
|
||||||
|
result, err := firewallService.ListRuleBackups(c.Request.Context(), c.DefaultQuery("subsystem", "system"))
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Firewall
|
||||||
|
// @Summary Initialize, repair or bind one firewall address family
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.FirewallFamilyOperation true "request"
|
||||||
|
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/family/operate [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["subsystem","family","operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] [subsystem] [family] 防火墙链","formatEN":"[operation] [subsystem] [family] firewall chains"}
|
||||||
|
func (b *BaseApi) OperateFirewallFamily(c *gin.Context) {
|
||||||
|
var request dto.FirewallFamilyOperation
|
||||||
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err := firewallSettingService.OperateFamily(request)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Firewall
|
||||||
|
// @Summary Update firewall IPv6 support
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.FirewallIPv6Operation true "request"
|
||||||
|
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/settings/ipv6 [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"设置防火墙 IPv6 支持为 [status]","formatEN":"Set firewall IPv6 support to [status]"}
|
||||||
|
func (b *BaseApi) OperateFirewallIPv6(c *gin.Context) {
|
||||||
|
var request dto.FirewallIPv6Operation
|
||||||
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err := firewallSettingService.OperateIPv6(request)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, result)
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,11 +3,16 @@ package v2
|
|||||||
import (
|
import (
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/service"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/accelerator"
|
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/accelerator"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (b *BaseApi) LoadModelGPUDevices(c *gin.Context) {
|
||||||
|
helper.SuccessWithData(c, service.LoadModelGPUDevices())
|
||||||
|
}
|
||||||
|
|
||||||
// @Tags AI
|
// @Tags AI
|
||||||
// @Summary Load gpu / xpu info
|
// @Summary Load gpu / xpu info
|
||||||
// @Accept json
|
// @Accept json
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package helper
|
|||||||
import (
|
import (
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -11,6 +12,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
@@ -21,6 +23,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) {
|
func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) {
|
||||||
|
mcpapi.SetBusinessErrorHeader(ctx.Request, ctx.Writer.Header(), err)
|
||||||
res := dto.Response{
|
res := dto.Response{
|
||||||
Code: code,
|
Code: code,
|
||||||
Message: "",
|
Message: "",
|
||||||
@@ -31,6 +34,7 @@ func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ErrorWithBusinessCode(ctx *gin.Context, code int, businessCode, msgKey string, err error) {
|
func ErrorWithBusinessCode(ctx *gin.Context, code int, businessCode, msgKey string, err error) {
|
||||||
|
mcpapi.SetBusinessErrorHeader(ctx.Request, ctx.Writer.Header(), err)
|
||||||
res := dto.Response{
|
res := dto.Response{
|
||||||
Code: code,
|
Code: code,
|
||||||
ErrorCode: businessCode,
|
ErrorCode: businessCode,
|
||||||
@@ -41,6 +45,7 @@ func ErrorWithBusinessCode(ctx *gin.Context, code int, businessCode, msgKey stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ErrorWithDetailAndData(ctx *gin.Context, code int, msgKey string, err error, data interface{}) {
|
func ErrorWithDetailAndData(ctx *gin.Context, code int, msgKey string, err error, data interface{}) {
|
||||||
|
mcpapi.SetBusinessErrorHeader(ctx.Request, ctx.Writer.Header(), err)
|
||||||
res := dto.Response{
|
res := dto.Response{
|
||||||
Code: code,
|
Code: code,
|
||||||
Data: data,
|
Data: data,
|
||||||
@@ -184,3 +189,23 @@ func GetParamInt32(paramName string, c *gin.Context) (int32, error) {
|
|||||||
intNum, _ := strconv.Atoi(idParam)
|
intNum, _ := strconv.Atoi(idParam)
|
||||||
return int32(intNum), nil
|
return int32(intNum), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetOperationLogValues supplies server-resolved values to Core's operation log.
|
||||||
|
// Call before writing the response. Only pass public audit fields, never credentials.
|
||||||
|
func SetOperationLogValues(c *gin.Context, values map[string]string) {
|
||||||
|
const header = "X-Op-Resolved"
|
||||||
|
resolved := make(map[string]string)
|
||||||
|
if data, err := base64.RawURLEncoding.DecodeString(c.Writer.Header().Get(header)); err == nil {
|
||||||
|
_ = json.Unmarshal(data, &resolved)
|
||||||
|
}
|
||||||
|
if resolved == nil {
|
||||||
|
resolved = make(map[string]string)
|
||||||
|
}
|
||||||
|
for key, value := range values {
|
||||||
|
resolved[key] = value
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(resolved)
|
||||||
|
if err == nil {
|
||||||
|
c.Header(header, base64.RawURLEncoding.EncodeToString(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/service"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mcpBusinessAPI struct {
|
||||||
|
installApp func(request.AppInstallCreate, bool) (*model.AppInstall, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MCPBusinessDispatch() gin.HandlerFunc {
|
||||||
|
return (&mcpBusinessAPI{installApp: (&service.AppService{}).InstallForMCP}).dispatch()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *mcpBusinessAPI) dispatch() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
var handler gin.HandlerFunc
|
||||||
|
switch c.Request.Method + " " + c.FullPath() {
|
||||||
|
case "POST /api/v2/databases":
|
||||||
|
handler = api.createMysql
|
||||||
|
case "POST /api/v2/databases/status":
|
||||||
|
handler = api.mysqlStatus
|
||||||
|
case "GET /api/v2/databases/db/list/:type":
|
||||||
|
handler = api.databases
|
||||||
|
case "POST /api/v2/apps/install":
|
||||||
|
handler = api.install
|
||||||
|
case "GET /api/v2/apps/installed/info/:appInstallId":
|
||||||
|
handler = api.appInfo
|
||||||
|
default:
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
present, trusted := mcpapi.Check(c.Request)
|
||||||
|
if !present {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !trusted {
|
||||||
|
c.AbortWithStatus(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handler(c)
|
||||||
|
c.Abort()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *mcpBusinessAPI) createMysql(c *gin.Context) {
|
||||||
|
var req dto.MysqlDBCreate
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(req.Password) != 0 {
|
||||||
|
password, err := base64.StdEncoding.DecodeString(req.Password)
|
||||||
|
if err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.Password = string(password)
|
||||||
|
}
|
||||||
|
created, err := (&service.MysqlService{}).CreateForMCP(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nodeID, _ := mcpapi.NodeID(c.Request)
|
||||||
|
helper.SuccessWithData(c, map[string]any{"id": created.ID, "database": created.MysqlName, "name": created.Name, "nodeId": nodeID})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *mcpBusinessAPI) mysqlStatus(c *gin.Context) {
|
||||||
|
var req dto.OperationWithNameAndType
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := (&service.MysqlService{}).LoadStatusForMCP(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *mcpBusinessAPI) databases(c *gin.Context) {
|
||||||
|
dbType, err := helper.GetStrParamByKey(c, "type")
|
||||||
|
if err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
list, err := (&service.DatabaseService{}).ListForMCP(c.Request.Context(), dbType)
|
||||||
|
if err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, list)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *mcpBusinessAPI) install(c *gin.Context) {
|
||||||
|
var req request.AppInstallCreate
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if value, exists := req.Params["WEBSITE_DIR"]; exists {
|
||||||
|
directory, ok := value.(string)
|
||||||
|
if !ok || directory == "" || directory != settingService.GetWebsiteDir() {
|
||||||
|
c.AbortWithStatus(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
installed, err := api.installApp(req, true)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, installed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *mcpBusinessAPI) appInfo(c *gin.Context) {
|
||||||
|
id, err := helper.GetIntParamByKey(c, "appInstallId")
|
||||||
|
if err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
info, err := (&service.AppInstallService{}).GetAppInstallInfoForMCP(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, info)
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/taskscope"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PanelMCPTaskAccess() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
if hasPanelMCPTaskAccess(c.Request) {
|
||||||
|
c.Request = c.Request.WithContext(taskscope.WithGrants(c.Request.Context(), constant.TaskScopePanelMCP))
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasPanelMCPTaskAccess(req *http.Request) bool {
|
||||||
|
if !global.IsMaster {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
local, ok := req.Context().Value(http.LocalAddrContextKey).(net.Addr)
|
||||||
|
values := req.Header.Values("X-Panel-MCP-Task-Access")
|
||||||
|
return ok && local.Network() == "unix" && len(values) == 1 && values[0] == "1"
|
||||||
|
}
|
||||||
+10
-23
@@ -1,13 +1,9 @@
|
|||||||
package v2
|
package v2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/shirou/gopsutil/v4/disk"
|
|
||||||
"github.com/shirou/gopsutil/v4/net"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// @Tags Monitor
|
// @Tags Monitor
|
||||||
@@ -32,14 +28,19 @@ func (b *BaseApi) LoadMonitor(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Monitor
|
// @Tags Monitor
|
||||||
// @Summary Clean monitor data
|
// @Summary Clean host or GPU monitor data
|
||||||
|
// @Param request body dto.MonitorClean true "request"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/monitor/clean [post]
|
// @Router /hosts/monitor/clean [post]
|
||||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空监控数据","formatEN":"clean monitor datas"}
|
// @x-panel-log {"bodyKeys":["type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空 [type] 监控数据","formatEN":"clean [type] monitoring data"}
|
||||||
func (b *BaseApi) CleanMonitor(c *gin.Context) {
|
func (b *BaseApi) CleanMonitor(c *gin.Context) {
|
||||||
if err := monitorService.CleanData(); err != nil {
|
var req dto.MonitorClean
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := monitorService.CleanData(req.Type); err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -91,14 +92,7 @@ func (b *BaseApi) UpdateMonitorSetting(c *gin.Context) {
|
|||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/monitor/netoptions [get]
|
// @Router /hosts/monitor/netoptions [get]
|
||||||
func (b *BaseApi) GetNetworkOptions(c *gin.Context) {
|
func (b *BaseApi) GetNetworkOptions(c *gin.Context) {
|
||||||
netStat, _ := net.IOCounters(true)
|
helper.SuccessWithData(c, monitorService.LoadNetworkOptions())
|
||||||
var options []string
|
|
||||||
options = append(options, "all")
|
|
||||||
for _, net := range netStat {
|
|
||||||
options = append(options, net.Name)
|
|
||||||
}
|
|
||||||
sort.Strings(options)
|
|
||||||
helper.SuccessWithData(c, options)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Monitor
|
// @Tags Monitor
|
||||||
@@ -108,12 +102,5 @@ func (b *BaseApi) GetNetworkOptions(c *gin.Context) {
|
|||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/monitor/iooptions [get]
|
// @Router /hosts/monitor/iooptions [get]
|
||||||
func (b *BaseApi) GetIOOptions(c *gin.Context) {
|
func (b *BaseApi) GetIOOptions(c *gin.Context) {
|
||||||
diskStat, _ := disk.IOCounters()
|
helper.SuccessWithData(c, monitorService.LoadIOOptions())
|
||||||
var options []string
|
|
||||||
options = append(options, "all")
|
|
||||||
for _, net := range diskStat {
|
|
||||||
options = append(options, net.Name)
|
|
||||||
}
|
|
||||||
sort.Strings(options)
|
|
||||||
helper.SuccessWithData(c, options)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (b *BaseApi) LoadVLLMMonitor(c *gin.Context) {
|
||||||
|
var req dto.MonitorVLLMSearch
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := monitorService.LoadVLLMMonitorData(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseApi) LoadVLLMCurrent(c *gin.Context) {
|
||||||
|
var req dto.MonitorVLLMCurrent
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := monitorService.LoadVLLMCurrent(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseApi) CleanVLLMMonitor(c *gin.Context) {
|
||||||
|
var req dto.MonitorVLLMClean
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := monitorService.CleanVLLMMonitor(req); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @Tags Dashboard
|
||||||
|
// @Summary List quick directories
|
||||||
|
// @Success 200 {array} model.QuickDirectory
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /dashboard/quick/directory [get]
|
||||||
|
func (b *BaseApi) ListQuickDirectories(c *gin.Context) {
|
||||||
|
items, err := dashboardService.ListQuickDirectories()
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, items)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Dashboard
|
||||||
|
// @Summary Create quick directory
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.QuickDirectoryCreate true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /dashboard/quick/directory [post]
|
||||||
|
func (b *BaseApi) CreateQuickDirectory(c *gin.Context) {
|
||||||
|
var req dto.QuickDirectoryCreate
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item, err := dashboardService.CreateQuickDirectory(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Dashboard
|
||||||
|
// @Summary Update quick directory
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.QuickDirectoryUpdate true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /dashboard/quick/directory/update [post]
|
||||||
|
func (b *BaseApi) UpdateQuickDirectory(c *gin.Context) {
|
||||||
|
var req dto.QuickDirectoryUpdate
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item, err := dashboardService.UpdateQuickDirectory(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Dashboard
|
||||||
|
// @Summary Delete quick directory
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.QuickDirectoryDelete true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /dashboard/quick/directory/del [post]
|
||||||
|
func (b *BaseApi) DeleteQuickDirectory(c *gin.Context) {
|
||||||
|
var req dto.QuickDirectoryDelete
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := dashboardService.DeleteQuickDirectory(req); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
@@ -61,13 +61,14 @@ func (b *BaseApi) CreateRuntime(c *gin.Context) {
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /runtimes/del [post]
|
// @Router /runtimes/del [post]
|
||||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除运行环境 [id]","formatEN":"Delete runtime [id]"}
|
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除运行环境 [name]","formatEN":"Delete runtime [name]"}
|
||||||
func (b *BaseApi) DeleteRuntime(c *gin.Context) {
|
func (b *BaseApi) DeleteRuntime(c *gin.Context) {
|
||||||
var req request.RuntimeDelete
|
var req request.RuntimeDelete
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := runtimeService.Delete(req)
|
name, err := runtimeService.Delete(req)
|
||||||
|
helper.SetOperationLogValues(c, map[string]string{"name": name})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -169,7 +170,7 @@ func (b *BaseApi) GetNodePackageRunScript(c *gin.Context) {
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /runtimes/operate [post]
|
// @Router /runtimes/operate [post]
|
||||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"操作运行环境 [id]","formatEN":"Operate runtime [id]"}
|
// @x-panel-log {"bodyKeys":["ID"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ID","isList":false,"db":"runtimes","output_column":"name","output_value":"name"}],"formatZH":"操作运行环境 [name]","formatEN":"Operate runtime [name]"}
|
||||||
func (b *BaseApi) OperateRuntime(c *gin.Context) {
|
func (b *BaseApi) OperateRuntime(c *gin.Context) {
|
||||||
var req request.RuntimeOperate
|
var req request.RuntimeOperate
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ func (b *BaseApi) CreateRootCert(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
if err := loadCertAfterDecrypt(&req); err != nil {
|
if err := loadCertAfterDecrypt(&req); err != nil {
|
||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if err := sshService.CreateRootCert(req); err != nil {
|
if err := sshService.CreateRootCert(req); err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
@@ -107,6 +108,7 @@ func (b *BaseApi) EditRootCert(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
if err := loadCertAfterDecrypt(&req); err != nil {
|
if err := loadCertAfterDecrypt(&req); err != nil {
|
||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if err := sshService.EditRootCert(req); err != nil {
|
if err := sshService.EditRootCert(req); err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
|
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/service"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/taskscope"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,7 +23,7 @@ func (b *BaseApi) PageTasks(c *gin.Context) {
|
|||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
total, list, err := taskService.Page(req)
|
total, list, err := taskServiceForRequest(c).Page(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -45,7 +47,7 @@ func (b *BaseApi) ReadTaskLogByLine(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var res *response.FileLineContent
|
var res *response.FileLineContent
|
||||||
res, err := taskService.ReadByLine(req)
|
res, err := taskServiceForRequest(c).ReadByLine(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -64,10 +66,17 @@ func (b *BaseApi) ReadTaskLogByLine(c *gin.Context) {
|
|||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /logs/tasks/executing/count [get]
|
// @Router /logs/tasks/executing/count [get]
|
||||||
func (b *BaseApi) CountExecutingTasks(c *gin.Context) {
|
func (b *BaseApi) CountExecutingTasks(c *gin.Context) {
|
||||||
count, err := taskService.CountExecutingTask()
|
count, err := taskServiceForRequest(c).CountExecutingTask()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
helper.SuccessWithData(c, count)
|
helper.SuccessWithData(c, count)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func taskServiceForRequest(c *gin.Context) service.ITaskLogService {
|
||||||
|
if granted := taskscope.Grants(c.Request.Context()); len(granted) != 0 {
|
||||||
|
return service.NewITaskService(granted...)
|
||||||
|
}
|
||||||
|
return taskService
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import (
|
|||||||
// @Summary Ws local terminal
|
// @Summary Ws local terminal
|
||||||
// @Param command query string false "command"
|
// @Param command query string false "command"
|
||||||
// @Param session query string false "session id to reattach"
|
// @Param session query string false "session id to reattach"
|
||||||
|
// @Param terminalPersistent query boolean false "allow recovery after an unexpected disconnect"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
@@ -43,6 +44,7 @@ func (b *BaseApi) WsLocalTerminal(c *gin.Context) {
|
|||||||
// @Param command query string false "command"
|
// @Param command query string false "command"
|
||||||
// @Param session query string false "session id to reattach"
|
// @Param session query string false "session id to reattach"
|
||||||
// @Param title query string false "session title shown in the session list"
|
// @Param title query string false "session title shown in the session list"
|
||||||
|
// @Param terminalPersistent query boolean false "allow recovery after an unexpected disconnect"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
@@ -146,13 +148,14 @@ func (b *BaseApi) runSSHSession(c *gin.Context, kind string, connect func() (*ss
|
|||||||
hostID, _ = strconv.Atoi(c.DefaultQuery("id", "0"))
|
hostID, _ = strconv.Atoi(c.DefaultQuery("id", "0"))
|
||||||
}
|
}
|
||||||
opts := terminal.SessionOptions{
|
opts := terminal.SessionOptions{
|
||||||
Identity: identity,
|
Identity: identity,
|
||||||
Kind: kind,
|
Kind: kind,
|
||||||
Title: sanitizeTerminalTitle(c.Query("title")),
|
Title: sanitizeTerminalTitle(c.Query("title")),
|
||||||
HostID: uint(max(hostID, 0)),
|
Persistent: c.Query("terminalPersistent") == "true",
|
||||||
Cols: cols,
|
HostID: uint(max(hostID, 0)),
|
||||||
Rows: rows,
|
Cols: cols,
|
||||||
InitCmd: command,
|
Rows: rows,
|
||||||
|
InitCmd: command,
|
||||||
}
|
}
|
||||||
err := terminal.Serve(wsConn, strings.TrimSpace(c.Query("session")), opts, func() (*gossh.Client, error) {
|
err := terminal.Serve(wsConn, strings.TrimSpace(c.Query("session")), opts, func() (*gossh.Client, error) {
|
||||||
client, err := connect()
|
client, err := connect()
|
||||||
@@ -240,6 +243,16 @@ func loadTerminalIdentity(c *gin.Context) (terminal.Identity, bool) {
|
|||||||
UserID: strings.TrimSpace(c.GetHeader(terminal.HeaderUserID)),
|
UserID: strings.TrimSpace(c.GetHeader(terminal.HeaderUserID)),
|
||||||
AuthSessionID: strings.TrimSpace(c.GetHeader(terminal.HeaderAuthSessionID)),
|
AuthSessionID: strings.TrimSpace(c.GetHeader(terminal.HeaderAuthSessionID)),
|
||||||
}
|
}
|
||||||
|
if value := c.GetHeader(terminal.HeaderAuthLeaseUntil); value != "" {
|
||||||
|
millis, err := strconv.ParseInt(value, 10, 64)
|
||||||
|
if err != nil || millis <= 0 {
|
||||||
|
return terminal.Identity{}, false
|
||||||
|
}
|
||||||
|
identity.AuthLeaseUntil = time.UnixMilli(millis)
|
||||||
|
if maximum := time.Now().Add(90 * time.Second); identity.AuthLeaseUntil.After(maximum) {
|
||||||
|
identity.AuthLeaseUntil = maximum
|
||||||
|
}
|
||||||
|
}
|
||||||
return identity, identity.Valid()
|
return identity, identity.Valid()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (b *BaseApi) TerminalCapabilities(c *gin.Context) {
|
||||||
|
helper.SuccessWithData(c, gin.H{"apiKeyLeaseVersion": 1})
|
||||||
|
}
|
||||||
@@ -402,7 +402,7 @@ func (b *BaseApi) ChangeDefaultServer(c *gin.Context) {
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /websites/php/version [post]
|
// @Router /websites/php/version [post]
|
||||||
// @x-panel-log {"bodyKeys":["websiteId"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"websiteId","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"php 版本变更 [domain]","formatEN":"php version update [domain]"}
|
// @x-panel-log {"bodyKeys":["websiteID"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"websiteID","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"php 版本变更 [domain]","formatEN":"php version update [domain]"}
|
||||||
func (b *BaseApi) ChangePHPVersion(c *gin.Context) {
|
func (b *BaseApi) ChangePHPVersion(c *gin.Context) {
|
||||||
var req request.WebsitePHPVersionReq
|
var req request.WebsitePHPVersionReq
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
package v2
|
package v2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -176,3 +179,64 @@ func (b *BaseApi) DownloadCAFile(c *gin.Context) {
|
|||||||
c.Header("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(info.Name()))
|
c.Header("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(info.Name()))
|
||||||
http.ServeContent(c.Writer, c.Request, info.Name(), info.ModTime(), file)
|
http.ServeContent(c.Writer, c.Request, info.Name(), info.ModTime(), file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags Website CA
|
||||||
|
// @Summary Upload website CA archive
|
||||||
|
// @Accept multipart/form-data
|
||||||
|
// @Param file formData file true "ZIP containing ca.crt and ca.key"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /websites/ca/upload [post]
|
||||||
|
func (b *BaseApi) UploadWebsiteCA(c *gin.Context) {
|
||||||
|
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 11<<20)
|
||||||
|
if err := c.Request.ParseMultipartForm(11 << 20); err != nil {
|
||||||
|
helper.InternalServer(c, buserr.New("ErrCAUpload"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if c.Request.MultipartForm != nil {
|
||||||
|
defer c.Request.MultipartForm.RemoveAll()
|
||||||
|
}
|
||||||
|
file, err := c.FormFile("file")
|
||||||
|
if err != nil || file.Size > 10<<20 || !strings.HasSuffix(strings.ToLower(file.Filename), ".zip") {
|
||||||
|
helper.InternalServer(c, buserr.New("ErrCAUpload"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stream, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer stream.Close()
|
||||||
|
data, err := io.ReadAll(io.LimitReader(stream, (10<<20)+1))
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = websiteCAService.Upload(file.Filename[:len(file.Filename)-len(".zip")], data); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Website CA
|
||||||
|
// @Summary Import website CA from PEM or server files
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body request.WebsiteCAImport true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /websites/ca/import [post]
|
||||||
|
func (b *BaseApi) ImportWebsiteCA(c *gin.Context) {
|
||||||
|
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 3<<20)
|
||||||
|
var req request.WebsiteCAImport
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := websiteCAService.Import(req); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|||||||
+63
-30
@@ -9,7 +9,8 @@ type AgentCreateReq struct {
|
|||||||
WebUIPort int `json:"webUIPort" validate:"required,min=1,max=65535"`
|
WebUIPort int `json:"webUIPort" validate:"required,min=1,max=65535"`
|
||||||
BridgePort int `json:"bridgePort"`
|
BridgePort int `json:"bridgePort"`
|
||||||
AllowedOrigins []string `json:"allowedOrigins"`
|
AllowedOrigins []string `json:"allowedOrigins"`
|
||||||
AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent"`
|
AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent deepseek-harness octop"`
|
||||||
|
AccessHost string `json:"accessHost"`
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
AccountID uint `json:"accountId"`
|
AccountID uint `json:"accountId"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
@@ -110,6 +111,7 @@ type AgentBatchOperateResult struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AgentItem struct {
|
type AgentItem struct {
|
||||||
|
AccessHost string `json:"accessHost"`
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
@@ -162,16 +164,25 @@ type AgentWebsiteBindReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AgentModelConfigUpdateReq struct {
|
type AgentModelConfigUpdateReq struct {
|
||||||
AgentID uint `json:"agentId" validate:"required"`
|
AgentID uint `json:"agentId" validate:"required"`
|
||||||
AccountID uint `json:"accountId" validate:"required"`
|
AccountID uint `json:"accountId" validate:"required"`
|
||||||
Model string `json:"model" validate:"required"`
|
Model string `json:"model" validate:"required"`
|
||||||
Fallbacks []string `json:"fallbacks"`
|
Fallbacks []string `json:"fallbacks"`
|
||||||
|
Metadata []AgentModelMetadata `json:"metadata" validate:"dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentModelConfig struct {
|
type AgentModelConfig struct {
|
||||||
AccountID uint `json:"accountId"`
|
AccountID uint `json:"accountId"`
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
Fallbacks []string `json:"fallbacks"`
|
Fallbacks []string `json:"fallbacks"`
|
||||||
|
Metadata []AgentModelMetadata `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentModelMetadata struct {
|
||||||
|
Model string `json:"model" validate:"required"`
|
||||||
|
InputMode string `json:"inputMode" validate:"required,oneof=auto text image"`
|
||||||
|
ContextWindow int `json:"contextWindow" validate:"min=0"`
|
||||||
|
MaxTokens int `json:"maxTokens" validate:"min=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentHermesChatSessionItem struct {
|
type AgentHermesChatSessionItem struct {
|
||||||
@@ -375,6 +386,7 @@ type AgentAccountProviderCountReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AgentAccountInfo struct {
|
type AgentAccountInfo struct {
|
||||||
|
AppInstallID uint `json:"appInstallId"`
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
MasterAccountID uint `json:"masterAccountId"`
|
MasterAccountID uint `json:"masterAccountId"`
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
@@ -421,32 +433,53 @@ type AgentFeishuConfigReq struct {
|
|||||||
AgentID uint `json:"agentId" validate:"required"`
|
AgentID uint `json:"agentId" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AgentFeishuTransition struct {
|
||||||
|
Confirmed bool `json:"confirmed"`
|
||||||
|
Streaming bool `json:"streaming"`
|
||||||
|
GroupSessionScope string `json:"groupSessionScope"`
|
||||||
|
RequireMention string `json:"requireMention"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentFeishuSwitchReq struct {
|
||||||
|
AgentID uint `json:"agentId" validate:"required"`
|
||||||
|
TaskID string `json:"taskID" validate:"required"`
|
||||||
|
Streaming bool `json:"streaming"`
|
||||||
|
GroupSessionScope string `json:"groupSessionScope" validate:"required,oneof=group group_sender group_topic group_topic_sender"`
|
||||||
|
RequireMention string `json:"requireMention" validate:"required,oneof=true false"`
|
||||||
|
}
|
||||||
|
|
||||||
type AgentFeishuConfigUpdateReq struct {
|
type AgentFeishuConfigUpdateReq struct {
|
||||||
AgentID uint `json:"agentId" validate:"required"`
|
GroupSessionScope string `json:"groupSessionScope" validate:"omitempty,oneof=group group_sender group_topic group_topic_sender"`
|
||||||
Enabled bool `json:"enabled"`
|
AgentID uint `json:"agentId" validate:"required"`
|
||||||
ThreadSession bool `json:"threadSession"`
|
Enabled bool `json:"enabled"`
|
||||||
ReplyMode string `json:"replyMode" validate:"required"`
|
ThreadSession bool `json:"threadSession"`
|
||||||
Streaming bool `json:"streaming"`
|
ReplyMode string `json:"replyMode" validate:"required"`
|
||||||
RequireMention string `json:"requireMention" validate:"required,oneof=true false open"`
|
Streaming bool `json:"streaming"`
|
||||||
GroupPolicy string `json:"groupPolicy" validate:"required,oneof=open allowlist disabled"`
|
RequireMention string `json:"requireMention" validate:"required,oneof=true false open"`
|
||||||
GroupAllowFrom []string `json:"groupAllowFrom"`
|
GroupPolicy string `json:"groupPolicy" validate:"required,oneof=open allowlist disabled"`
|
||||||
Domain string `json:"domain"`
|
GroupAllowFrom []string `json:"groupAllowFrom"`
|
||||||
ConnectionMode string `json:"connectionMode"`
|
Domain string `json:"domain"`
|
||||||
Bots []AgentFeishuBot `json:"bots" validate:"required,min=1"`
|
ConnectionMode string `json:"connectionMode"`
|
||||||
|
Bots []AgentFeishuBot `json:"bots" validate:"required,min=1"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentFeishuConfig struct {
|
type AgentFeishuConfig struct {
|
||||||
Enabled bool `json:"enabled"`
|
BuiltinManaged bool `json:"builtinManaged"`
|
||||||
ThreadSession bool `json:"threadSession"`
|
LegacyConfigured bool `json:"legacyConfigured"`
|
||||||
ReplyMode string `json:"replyMode"`
|
GroupSessionScope string `json:"groupSessionScope"`
|
||||||
Streaming bool `json:"streaming"`
|
BuiltinAvailable bool `json:"builtinAvailable"`
|
||||||
RequireMention string `json:"requireMention"`
|
Legacy bool `json:"legacy"`
|
||||||
GroupPolicy string `json:"groupPolicy"`
|
Enabled bool `json:"enabled"`
|
||||||
GroupAllowFrom []string `json:"groupAllowFrom"`
|
ThreadSession bool `json:"threadSession"`
|
||||||
Domain string `json:"domain"`
|
ReplyMode string `json:"replyMode"`
|
||||||
ConnectionMode string `json:"connectionMode"`
|
Streaming bool `json:"streaming"`
|
||||||
Bots []AgentFeishuBot `json:"bots"`
|
RequireMention string `json:"requireMention"`
|
||||||
Installed bool `json:"installed"`
|
GroupPolicy string `json:"groupPolicy"`
|
||||||
|
GroupAllowFrom []string `json:"groupAllowFrom"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
ConnectionMode string `json:"connectionMode"`
|
||||||
|
Bots []AgentFeishuBot `json:"bots"`
|
||||||
|
Installed bool `json:"installed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentTelegramConfigReq struct {
|
type AgentTelegramConfigReq struct {
|
||||||
|
|||||||
+28
-5
@@ -2,6 +2,28 @@ package dto
|
|||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
|
type ModelGPUDevice struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
UUID string `json:"uuid,omitempty"`
|
||||||
|
Available bool `json:"available"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModelGPUDevices struct {
|
||||||
|
Devices []ModelGPUDevice `json:"devices"`
|
||||||
|
Warnings []string `json:"warnings"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModelGPUConfig struct {
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
Count int `json:"count,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
DeviceIDs []string `json:"deviceIDs"`
|
||||||
|
Capabilities []string `json:"capabilities"`
|
||||||
|
}
|
||||||
|
|
||||||
type OllamaModelInfo struct {
|
type OllamaModelInfo struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -14,16 +36,17 @@ type OllamaModelInfo struct {
|
|||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OllamaModelDropList struct {
|
|
||||||
ID uint `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type OllamaModelName struct {
|
type OllamaModelName struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
TaskID string `json:"taskID"`
|
TaskID string `json:"taskID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OllamaModelAccountSync struct {
|
||||||
|
ID uint `json:"id" validate:"required"`
|
||||||
|
ModelAccountBaseURLType string `json:"modelAccountBaseURLType" validate:"required,oneof=container localhost systemIP custom"`
|
||||||
|
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||||
|
}
|
||||||
|
|
||||||
type OllamaBindDomain struct {
|
type OllamaBindDomain struct {
|
||||||
Domain string `json:"domain" validate:"required"`
|
Domain string `json:"domain" validate:"required"`
|
||||||
AppInstallID uint `json:"appInstallID" validate:"required"`
|
AppInstallID uint `json:"appInstallID" validate:"required"`
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type AIBenchmarkSource struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
BaseURL string `json:"baseUrl"`
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
Models []string `json:"models"`
|
||||||
|
Tokenizer string `json:"tokenizer"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ type AlertBase struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PushAlert struct {
|
type PushAlert struct {
|
||||||
|
Result string `json:"result,omitempty"`
|
||||||
TaskName string `json:"taskName"`
|
TaskName string `json:"taskName"`
|
||||||
AlertType string `json:"alertType"`
|
AlertType string `json:"alertType"`
|
||||||
EntryID uint `json:"entryID"`
|
EntryID uint `json:"entryID"`
|
||||||
@@ -53,6 +54,7 @@ type AlertDTO struct {
|
|||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Project string `json:"project"`
|
Project string `json:"project"`
|
||||||
|
TaskName string `json:"taskName,omitempty"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
SendCount uint `json:"sendCount"`
|
SendCount uint `json:"sendCount"`
|
||||||
AdvancedParams string `json:"advancedParams"`
|
AdvancedParams string `json:"advancedParams"`
|
||||||
|
|||||||
@@ -43,9 +43,11 @@ type AppOssConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AppVersion struct {
|
type AppVersion struct {
|
||||||
Version string `json:"version"`
|
FeishuLegacy bool `json:"feishuLegacy,omitempty"`
|
||||||
DetailId uint `json:"detailId"`
|
FeishuConfigured bool `json:"feishuConfigured,omitempty"`
|
||||||
DockerCompose string `json:"dockerCompose"`
|
Version string `json:"version"`
|
||||||
|
DetailId uint `json:"detailId"`
|
||||||
|
DockerCompose string `json:"dockerCompose"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppList struct {
|
type AppList struct {
|
||||||
|
|||||||
@@ -171,7 +171,8 @@ type ContainerItemStats struct {
|
|||||||
BuildCacheReclaimable int64 `json:"buildCacheReclaimable"`
|
BuildCacheReclaimable int64 `json:"buildCacheReclaimable"`
|
||||||
}
|
}
|
||||||
type ContainerListStats struct {
|
type ContainerListStats struct {
|
||||||
ContainerID string `json:"containerID"`
|
ContainerID string `json:"containerID"`
|
||||||
|
ContainerName string `json:"containerName"`
|
||||||
|
|
||||||
CPUTotalUsage uint64 `json:"cpuTotalUsage"`
|
CPUTotalUsage uint64 `json:"cpuTotalUsage"`
|
||||||
SystemUsage uint64 `json:"systemUsage"`
|
SystemUsage uint64 `json:"systemUsage"`
|
||||||
|
|||||||
@@ -51,9 +51,10 @@ type CronjobOperate struct {
|
|||||||
Secret string `json:"secret"`
|
Secret string `json:"secret"`
|
||||||
Args string `json:"args"`
|
Args string `json:"args"`
|
||||||
|
|
||||||
AlertCount uint `json:"alertCount"`
|
AlertCount uint `json:"alertCount"`
|
||||||
AlertTitle string `json:"alertTitle"`
|
AlertTitle string `json:"alertTitle"`
|
||||||
AlertMethod string `json:"alertMethod"`
|
AlertMethod string `json:"alertMethod"`
|
||||||
|
AlertTriggerMode string `json:"alertTriggerMode" validate:"omitempty,oneof=failed success both"`
|
||||||
|
|
||||||
CleanLogConfig
|
CleanLogConfig
|
||||||
}
|
}
|
||||||
@@ -126,7 +127,8 @@ type CronjobInfo struct {
|
|||||||
Secret string `json:"secret"`
|
Secret string `json:"secret"`
|
||||||
Args string `json:"args"`
|
Args string `json:"args"`
|
||||||
|
|
||||||
AlertCount uint `json:"alertCount"`
|
AlertCount uint `json:"alertCount"`
|
||||||
|
AlertTriggerMode string `json:"alertTriggerMode"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CronjobImport struct {
|
type CronjobImport struct {
|
||||||
@@ -169,9 +171,10 @@ type CronjobTrans struct {
|
|||||||
SourceAccounts []string `json:"sourceAccounts"`
|
SourceAccounts []string `json:"sourceAccounts"`
|
||||||
DownloadAccount string `json:"downloadAccount"`
|
DownloadAccount string `json:"downloadAccount"`
|
||||||
|
|
||||||
AlertCount uint `json:"alertCount"`
|
AlertCount uint `json:"alertCount"`
|
||||||
AlertTitle string `json:"alertTitle"`
|
AlertTitle string `json:"alertTitle"`
|
||||||
AlertMethod string `json:"alertMethod"`
|
AlertMethod string `json:"alertMethod"`
|
||||||
|
AlertTriggerMode string `json:"alertTriggerMode" validate:"omitempty,oneof=failed success both"`
|
||||||
}
|
}
|
||||||
type TransHelper struct {
|
type TransHelper struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|||||||
@@ -31,7 +31,27 @@ type DashboardBase struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ChangeQuicks struct {
|
type ChangeQuicks struct {
|
||||||
Quicks []QuickJump `json:"quicks"`
|
Quicks []QuickVisibility `json:"quicks" validate:"required,dive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuickVisibility struct {
|
||||||
|
ID uint `json:"id" validate:"required"`
|
||||||
|
IsShow bool `json:"isShow"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuickDirectoryCreate struct {
|
||||||
|
Path string `json:"path" validate:"required"`
|
||||||
|
Alias string `json:"alias"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuickDirectoryDelete struct {
|
||||||
|
ID uint `json:"id" validate:"required"`
|
||||||
|
Legacy bool `json:"legacy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuickDirectoryUpdate struct {
|
||||||
|
QuickDirectoryDelete
|
||||||
|
QuickDirectoryCreate
|
||||||
}
|
}
|
||||||
|
|
||||||
type QuickJump struct {
|
type QuickJump struct {
|
||||||
@@ -157,6 +177,7 @@ type DiskInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GPUInfo struct {
|
type GPUInfo struct {
|
||||||
|
SharedMemory bool `json:"sharedMemory"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Index uint `json:"index"`
|
Index uint `json:"index"`
|
||||||
NPUIndex uint `json:"npuIndex"`
|
NPUIndex uint `json:"npuIndex"`
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ type DBBaseInfo struct {
|
|||||||
Port int64 `json:"port"`
|
Port int64 `json:"port"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RedisCliInstall struct {
|
||||||
|
TaskID string `json:"taskID" validate:"omitempty,uuid"`
|
||||||
|
}
|
||||||
|
|
||||||
// mysql
|
// mysql
|
||||||
type MysqlDBSearch struct {
|
type MysqlDBSearch struct {
|
||||||
PageInfo
|
PageInfo
|
||||||
|
|||||||
+124
-132
@@ -1,11 +1,14 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||||
|
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||||
firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync"
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FirewallSubsystemStatus struct {
|
type FirewallSubsystemStatus struct {
|
||||||
|
IPv6Enabled bool `json:"ipv6Enabled"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Backend string `json:"backend"`
|
Backend string `json:"backend"`
|
||||||
ConflictBackend string `json:"conflictBackend,omitempty"`
|
ConflictBackend string `json:"conflictBackend,omitempty"`
|
||||||
@@ -17,16 +20,22 @@ type FirewallSubsystemStatus struct {
|
|||||||
PingStatus string `json:"pingStatus"`
|
PingStatus string `json:"pingStatus"`
|
||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
Reason string `json:"reason,omitempty"`
|
Reason string `json:"reason,omitempty"`
|
||||||
SyncError string `json:"syncError,omitempty"`
|
LifecycleTaskID string `json:"lifecycleTaskID,omitempty"`
|
||||||
IPv4 FirewallBackendFamilyStatus `json:"ipv4"`
|
IPv4 FirewallBackendFamilyStatus `json:"ipv4"`
|
||||||
IPv6 FirewallBackendFamilyStatus `json:"ipv6"`
|
IPv6 FirewallBackendFamilyStatus `json:"ipv6"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallLifecycleOperation struct {
|
type FirewallLifecycleOperation struct {
|
||||||
|
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
|
||||||
Operation string `json:"operation" validate:"required,oneof=start stop restart disableBanPing enableBanPing"`
|
Operation string `json:"operation" validate:"required,oneof=start stop restart disableBanPing enableBanPing"`
|
||||||
WithDockerRestart bool `json:"withDockerRestart"`
|
WithDockerRestart bool `json:"withDockerRestart"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FirewallLifecycleOperationResponse struct {
|
||||||
|
TaskID string `json:"taskID,omitempty"`
|
||||||
|
Queued bool `json:"queued"`
|
||||||
|
}
|
||||||
|
|
||||||
type FirewallBackendOption struct {
|
type FirewallBackendOption struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Installed bool `json:"installed"`
|
Installed bool `json:"installed"`
|
||||||
@@ -42,10 +51,12 @@ type FirewallBackendOption struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FirewallBackendFamilyStatus struct {
|
type FirewallBackendFamilyStatus struct {
|
||||||
Available bool `json:"available"`
|
Partial bool `json:"partial"`
|
||||||
Initialized bool `json:"initialized"`
|
Available bool `json:"available"`
|
||||||
Bound bool `json:"bound"`
|
Initialized bool `json:"initialized"`
|
||||||
Reason string `json:"reason,omitempty"`
|
Bound bool `json:"bound"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
RAInterfaces []string `json:"raInterfaces,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallBackendGroup struct {
|
type FirewallBackendGroup struct {
|
||||||
@@ -55,15 +66,27 @@ type FirewallBackendGroup struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FirewallSettings struct {
|
type FirewallSettings struct {
|
||||||
System FirewallBackendGroup `json:"system"`
|
IPv6Enabled bool `json:"ipv6Enabled"`
|
||||||
Forwarding FirewallBackendGroup `json:"forwarding"`
|
System FirewallBackendGroup `json:"system"`
|
||||||
Docker FirewallBackendGroup `json:"docker"`
|
Forwarding FirewallBackendGroup `json:"forwarding"`
|
||||||
PingStatus string `json:"pingStatus"`
|
Docker FirewallBackendGroup `json:"docker"`
|
||||||
PortWhitelist string `json:"portWhiteList"`
|
PingStatus string `json:"pingStatus"`
|
||||||
|
PortWhitelist []filter.PortWhitelist `json:"portWhiteList"`
|
||||||
|
PanelPort string `json:"panelPort"`
|
||||||
|
SSHPort string `json:"sshPort"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallPortWhitelistCreate struct {
|
||||||
|
Rule filter.PortWhitelist `json:"rule" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallPortWhitelistUpdate struct {
|
type FirewallPortWhitelistUpdate struct {
|
||||||
Value string `json:"value" validate:"required"`
|
OldRule filter.PortWhitelist `json:"oldRule" validate:"required"`
|
||||||
|
Rule filter.PortWhitelist `json:"rule" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallPortWhitelistDelete struct {
|
||||||
|
Rule *filter.PortWhitelist `json:"rule" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallBackendOperation struct {
|
type FirewallBackendOperation struct {
|
||||||
@@ -72,6 +95,17 @@ type FirewallBackendOperation struct {
|
|||||||
Operation string `json:"operation" validate:"required,oneof=select initialize cleanup"`
|
Operation string `json:"operation" validate:"required,oneof=select initialize cleanup"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FirewallIPv6Operation struct {
|
||||||
|
Status string `json:"status" validate:"required,oneof=Enable Disable"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallFamilyOperation struct {
|
||||||
|
Subsystem string `json:"subsystem" validate:"required,oneof=system forwarding docker"`
|
||||||
|
Backend string `json:"backend" validate:"required,oneof=iptables nftables"`
|
||||||
|
Family string `json:"family" validate:"required,oneof=ipv4 ipv6"`
|
||||||
|
Operation string `json:"operation" validate:"required,oneof=initialize repair bind"`
|
||||||
|
}
|
||||||
|
|
||||||
type FilterChainOperation struct {
|
type FilterChainOperation struct {
|
||||||
Name string `json:"name" validate:"required,eq=1PANEL_BASIC"`
|
Name string `json:"name" validate:"required,eq=1PANEL_BASIC"`
|
||||||
Operate string `json:"operate" validate:"required,oneof=init-base bind-base unbind-base"`
|
Operate string `json:"operate" validate:"required,oneof=init-base bind-base unbind-base"`
|
||||||
@@ -84,45 +118,55 @@ type FilterChainOperationResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FirewallInitializationTask struct {
|
type FirewallInitializationTask struct {
|
||||||
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
|
||||||
|
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallSystemPort struct {
|
type FirewallSystemPort = firewall.SystemPort
|
||||||
Family string
|
|
||||||
Port string
|
|
||||||
Protocol string
|
|
||||||
}
|
|
||||||
|
|
||||||
type FirewallRuleInventoryResponse struct {
|
type FirewallRuleInventoryResponse struct {
|
||||||
IPv4Range filter.PositionRange `json:"ipv4Range"`
|
IPv4Range filter.PositionRange `json:"ipv4Range"`
|
||||||
IPv6Range filter.PositionRange `json:"ipv6Range"`
|
IPv6Range filter.PositionRange `json:"ipv6Range"`
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
AllTotal int64 `json:"allTotal"`
|
AllTotal int64 `json:"allTotal"`
|
||||||
ManagedTotal int64 `json:"managedTotal"`
|
Items []filter.InventoryItem `json:"items"`
|
||||||
Items []filter.InventoryItem `json:"items"`
|
Notices []filter.ScopeNotice `json:"notices,omitempty"`
|
||||||
Notices []filter.ScopeNotice `json:"notices,omitempty"`
|
}
|
||||||
|
|
||||||
|
type FirewallRuleBackup struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Provider filter.Provider `json:"provider"`
|
||||||
|
RuleCount int `json:"ruleCount"`
|
||||||
|
ModifiedAt int64 `json:"modifiedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleBackups struct {
|
||||||
|
Directory string `json:"directory"`
|
||||||
|
Files []FirewallRuleBackup `json:"files"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallRuleResetResponse struct {
|
type FirewallRuleResetResponse struct {
|
||||||
Removed int `json:"removed"`
|
BackupPath string `json:"backupPath"`
|
||||||
Disabled bool `json:"disabled"`
|
Removed int `json:"removed"`
|
||||||
|
Disabled bool `json:"disabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallRuleReset struct {
|
type FirewallRuleReset struct {
|
||||||
|
Subsystem string `json:"subsystem,omitempty" validate:"omitempty,oneof=system forwarding docker"`
|
||||||
|
Backup *bool `json:"backup,omitempty" default:"true"`
|
||||||
Provider filter.Provider `json:"provider,omitempty" validate:"omitempty,oneof=firewalld ufw iptables nftables"`
|
Provider filter.Provider `json:"provider,omitempty" validate:"omitempty,oneof=firewalld ufw iptables nftables"`
|
||||||
WithDockerRestart bool `json:"withDockerRestart"`
|
WithDockerRestart bool `json:"withDockerRestart"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallRuleInventory struct {
|
type FirewallRuleInventory struct {
|
||||||
PageInfo
|
PageInfo
|
||||||
Scope filter.Scope `json:"scope,omitempty"`
|
Scope filter.Scope `json:"scope,omitempty"`
|
||||||
Scopes []filter.Scope `json:"scopes,omitempty" validate:"max=16"`
|
Scopes []filter.Scope `json:"scopes,omitempty" validate:"max=16"`
|
||||||
All bool `json:"all,omitempty"`
|
All bool `json:"all,omitempty"`
|
||||||
Info string `json:"info"`
|
Info string `json:"info"`
|
||||||
Families []filter.Family `json:"families,omitempty" validate:"omitempty,dive,oneof=ipv4 ipv6"`
|
Families []filter.Family `json:"families,omitempty" validate:"omitempty,dive,oneof=ipv4 ipv6"`
|
||||||
Actions []string `json:"actions,omitempty" validate:"omitempty,dive,oneof=accept deny"`
|
Actions []string `json:"actions,omitempty" validate:"omitempty,dive,oneof=accept deny"`
|
||||||
States []filter.InventoryState `json:"states,omitempty" validate:"omitempty,dive,oneof=managed adopted external drifted protected"`
|
ExcludeChains []string `json:"excludeChains,omitempty" validate:"omitempty,dive,oneof=1PANEL_BASIC_BEFORE 1PANEL_BASIC 1PANEL_BASIC_AFTER"`
|
||||||
ExcludeChains []string `json:"excludeChains,omitempty" validate:"omitempty,dive,oneof=1PANEL_BASIC_BEFORE 1PANEL_BASIC 1PANEL_BASIC_AFTER"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallNativeDetail struct {
|
type FirewallNativeDetail struct {
|
||||||
@@ -133,6 +177,7 @@ type FirewallNativeDetail struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DockerPortGuardBase struct {
|
type DockerPortGuardBase struct {
|
||||||
|
IPv6Enabled bool `json:"ipv6Enabled"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
IsExist bool `json:"isExist"`
|
IsExist bool `json:"isExist"`
|
||||||
@@ -145,6 +190,7 @@ type DockerPortGuardBase struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DockerPortGuardFamilyStatus struct {
|
type DockerPortGuardFamilyStatus struct {
|
||||||
|
Partial bool `json:"partial"`
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
Reason string `json:"reason,omitempty"`
|
Reason string `json:"reason,omitempty"`
|
||||||
Initialized bool `json:"initialized"`
|
Initialized bool `json:"initialized"`
|
||||||
@@ -165,11 +211,8 @@ type DockerPortGuardEndpoint struct {
|
|||||||
Application string `json:"application,omitempty"`
|
Application string `json:"application,omitempty"`
|
||||||
PolicyUUID string `json:"policyUUID,omitempty"`
|
PolicyUUID string `json:"policyUUID,omitempty"`
|
||||||
Mode string `json:"mode,omitempty"`
|
Mode string `json:"mode,omitempty"`
|
||||||
NativeAction string `json:"nativeAction,omitempty"`
|
|
||||||
ReadOnly bool `json:"readOnly,omitempty"`
|
|
||||||
Sources []string `json:"sources"`
|
Sources []string `json:"sources"`
|
||||||
Effective bool `json:"effective"`
|
Effective bool `json:"effective"`
|
||||||
Description string `json:"description,omitempty"`
|
|
||||||
TrafficPath string `json:"trafficPath"`
|
TrafficPath string `json:"trafficPath"`
|
||||||
ManagementTarget string `json:"managementTarget"`
|
ManagementTarget string `json:"managementTarget"`
|
||||||
ManagementReason string `json:"managementReason,omitempty"`
|
ManagementReason string `json:"managementReason,omitempty"`
|
||||||
@@ -206,6 +249,7 @@ type DockerPortGuardEndpointIdentity struct {
|
|||||||
|
|
||||||
type DockerPortGuardPolicyBatch struct {
|
type DockerPortGuardPolicyBatch struct {
|
||||||
Policies []DockerPortGuardPolicy `json:"policies" validate:"required,min=1,dive"`
|
Policies []DockerPortGuardPolicy `json:"policies" validate:"required,min=1,dive"`
|
||||||
|
Import bool `json:"import"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DockerPortGuardPolicyBatchDelete struct {
|
type DockerPortGuardPolicyBatchDelete struct {
|
||||||
@@ -214,29 +258,27 @@ type DockerPortGuardPolicyBatchDelete struct {
|
|||||||
|
|
||||||
type DockerPortGuardPolicy struct {
|
type DockerPortGuardPolicy struct {
|
||||||
DockerPortGuardEndpointIdentity
|
DockerPortGuardEndpointIdentity
|
||||||
Mode string `json:"mode" validate:"required,oneof=deny_sources allow_sources deny_all"`
|
Mode string `json:"mode" validate:"required,oneof=deny_sources allow_sources deny_all accept_sources accept_all"`
|
||||||
Sources []string `json:"sources" validate:"dive,required,max=64"`
|
Sources []string `json:"sources" validate:"dive,required,max=64"`
|
||||||
Description string `json:"description" validate:"max=256"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type DockerPortGuardOperation struct {
|
type DockerPortGuardOperation struct {
|
||||||
Operation string `json:"operation" validate:"required,oneof=initialize bind unbind"`
|
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
|
||||||
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
Operation string `json:"operation" validate:"required,oneof=initialize bind unbind"`
|
||||||
}
|
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
||||||
|
|
||||||
type FirewallRuleAdopt struct {
|
|
||||||
Scope filter.Scope `json:"scope" validate:"required"`
|
|
||||||
InstanceKey string `json:"instanceKey" validate:"required,max=128"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallRuleCreateItem struct {
|
type FirewallRuleCreateItem struct {
|
||||||
Rule filter.FirewallRule `json:"rule" validate:"required"`
|
Raw string `json:"raw,omitempty"`
|
||||||
SourceKind string `json:"sourceKind" validate:"omitempty,oneof=user imported"`
|
ParseStatus filter.ParseStatus `json:"parseStatus,omitempty"`
|
||||||
SourceID string `json:"sourceID"`
|
Rule filter.FirewallRule `json:"rule" validate:"required"`
|
||||||
|
SourceKind string `json:"sourceKind" validate:"omitempty,oneof=user imported"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallRuleCreate struct {
|
type FirewallRuleCreate struct {
|
||||||
Items []FirewallRuleCreateItem `json:"items" validate:"required,min=1,dive"`
|
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
|
||||||
|
Initialize bool `json:"initialize"`
|
||||||
|
Items []FirewallRuleCreateItem `json:"items" validate:"dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallRuleCreateResponse struct {
|
type FirewallRuleCreateResponse struct {
|
||||||
@@ -255,81 +297,36 @@ type FirewallRuleCreateFailure struct {
|
|||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallRuleSyncRequest struct {
|
|
||||||
Subsystem string `json:"subsystem" validate:"omitempty,oneof=system forwarding docker"`
|
|
||||||
SourceProvider filter.Provider `json:"sourceProvider,omitempty" validate:"omitempty,oneof=firewalld ufw iptables nftables"`
|
|
||||||
TargetProvider filter.Provider `json:"targetProvider" validate:"required,oneof=firewalld ufw iptables nftables"`
|
|
||||||
ResetSource bool `json:"resetSource"`
|
|
||||||
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FirewallRuleSyncItem struct {
|
|
||||||
SourceUUID string `json:"sourceUUID"`
|
|
||||||
Rule *filter.FirewallRule `json:"rule,omitempty"`
|
|
||||||
ForwardRule *ForwardRule `json:"forwardRule,omitempty"`
|
|
||||||
DockerRule *DockerPortGuardEndpoint `json:"dockerRule,omitempty"`
|
|
||||||
Status firewallsync.Status `json:"status"`
|
|
||||||
ReasonCode firewallsync.ReasonCode `json:"reasonCode,omitempty"`
|
|
||||||
Reason string `json:"reason,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FirewallRuleSyncPreview struct {
|
|
||||||
Subsystem string `json:"subsystem"`
|
|
||||||
SourceProvider filter.Provider `json:"sourceProvider,omitempty"`
|
|
||||||
TargetProvider filter.Provider `json:"targetProvider"`
|
|
||||||
Total int `json:"total"`
|
|
||||||
Ready int `json:"ready"`
|
|
||||||
Existing int `json:"existing"`
|
|
||||||
Removed int `json:"removed"`
|
|
||||||
Blocked int `json:"blocked"`
|
|
||||||
Items []FirewallRuleSyncItem `json:"items"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FirewallRuleSyncResult struct {
|
|
||||||
Subsystem string `json:"subsystem"`
|
|
||||||
SourceProvider filter.Provider `json:"sourceProvider,omitempty"`
|
|
||||||
TargetProvider filter.Provider `json:"targetProvider"`
|
|
||||||
Total int `json:"total"`
|
|
||||||
Succeeded int `json:"succeeded"`
|
|
||||||
Skipped int `json:"skipped"`
|
|
||||||
Removed int `json:"removed"`
|
|
||||||
Failed int `json:"failed"`
|
|
||||||
Errors []FirewallRuleSyncFailure `json:"errors,omitempty"`
|
|
||||||
TaskID string `json:"taskID,omitempty"`
|
|
||||||
Queued bool `json:"queued,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FirewallRuleSyncTask struct {
|
|
||||||
TaskID string `json:"taskID,omitempty"`
|
|
||||||
Executing bool `json:"executing"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FirewallRuleSyncFailure struct {
|
|
||||||
SourceUUID string `json:"sourceUUID"`
|
|
||||||
Rule *filter.FirewallRule `json:"rule,omitempty"`
|
|
||||||
ForwardRule *ForwardRule `json:"forwardRule,omitempty"`
|
|
||||||
DockerRule *DockerPortGuardEndpoint `json:"dockerRule,omitempty"`
|
|
||||||
Error string `json:"error"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FirewallRuleDelete struct {
|
type FirewallRuleDelete struct {
|
||||||
UUIDs []string `json:"uuids" validate:"required,min=1,dive,required,max=64"`
|
Targets []FirewallRuleDeleteItem `json:"targets" validate:"required,min=1,dive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleDeleteItem struct {
|
||||||
|
FirewallRuleDeleteTarget
|
||||||
|
Observed filter.ObservedRule `json:"observed" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleDeleteTarget struct {
|
||||||
|
Scope filter.Scope `json:"scope" validate:"required"`
|
||||||
|
InstanceKey string `json:"instanceKey" validate:"required,max=128"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallRuleDeleteResponse struct {
|
type FirewallRuleDeleteResponse struct {
|
||||||
|
TaskID string `json:"taskID,omitempty"`
|
||||||
|
Queued bool `json:"queued,omitempty"`
|
||||||
Succeeded int `json:"succeeded"`
|
Succeeded int `json:"succeeded"`
|
||||||
Failed int `json:"failed"`
|
Failed int `json:"failed"`
|
||||||
Errors []FirewallRuleDeleteFailure `json:"errors,omitempty"`
|
Errors []FirewallRuleDeleteFailure `json:"errors,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallRuleDeleteFailure struct {
|
type FirewallRuleDeleteFailure struct {
|
||||||
Index int `json:"index"`
|
Index int `json:"index"`
|
||||||
UUID string `json:"uuid"`
|
InstanceKey string `json:"instanceKey"`
|
||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallRuleUpdate struct {
|
type FirewallRuleUpdate struct {
|
||||||
UUID string `json:"uuid" validate:"required,max=64"`
|
FirewallRuleDeleteTarget
|
||||||
Rule *filter.FirewallRule `json:"rule,omitempty" validate:"required_without_all=Description OrderIndex Priority,excluded_with=Description OrderIndex Priority"`
|
Rule *filter.FirewallRule `json:"rule,omitempty" validate:"required_without_all=Description OrderIndex Priority,excluded_with=Description OrderIndex Priority"`
|
||||||
Description *string `json:"description,omitempty" validate:"excluded_with=Rule"`
|
Description *string `json:"description,omitempty" validate:"excluded_with=Rule"`
|
||||||
OrderIndex *int64 `json:"orderIndex,omitempty" validate:"excluded_with=Rule Priority"`
|
OrderIndex *int64 `json:"orderIndex,omitempty" validate:"excluded_with=Rule Priority"`
|
||||||
@@ -337,26 +334,21 @@ type FirewallRuleUpdate struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FirewallRuleReorder struct {
|
type FirewallRuleReorder struct {
|
||||||
UUID string `json:"uuid" validate:"required,max=64"`
|
FirewallRuleDeleteTarget
|
||||||
TargetPosition *int64 `json:"targetPosition"`
|
TargetPosition *int64 `json:"targetPosition"`
|
||||||
Priority *int `json:"priority"`
|
Priority *int `json:"priority"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *FirewallRuleSyncPreview) Add(item FirewallRuleSyncItem) {
|
type FirewallRuleExportItem struct {
|
||||||
p.Items = append(p.Items, item)
|
filter.FirewallRule
|
||||||
switch item.Status {
|
Raw string `json:"raw,omitempty"`
|
||||||
case firewallsync.StatusReady:
|
ParseStatus filter.ParseStatus `json:"parseStatus,omitempty"`
|
||||||
p.Ready++
|
}
|
||||||
p.Total++
|
|
||||||
case firewallsync.StatusExisting:
|
type FirewallSubsystemBackup struct {
|
||||||
p.Existing++
|
Families []string `json:"families,omitempty"`
|
||||||
p.Total++
|
Subsystem string `json:"subsystem"`
|
||||||
case firewallsync.StatusRemove:
|
Provider filter.Provider `json:"provider"`
|
||||||
p.Removed++
|
Forwarding []forwarding.Rule `json:"forwarding"`
|
||||||
case firewallsync.StatusBlocked:
|
Docker *dockerfirewall.PolicyInventory `json:"docker,omitempty"`
|
||||||
p.Blocked++
|
|
||||||
if item.ReasonCode != firewallsync.ReasonReadOnlyRule {
|
|
||||||
p.Total++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,15 +24,11 @@ type ForwardRule struct {
|
|||||||
|
|
||||||
UsedStatus string `json:"usedStatus"`
|
UsedStatus string `json:"usedStatus"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
|
||||||
IsDesired bool `json:"isDesired"`
|
|
||||||
IsRuntime bool `json:"isRuntime"`
|
|
||||||
SyncStatus string `json:"syncStatus"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ForwardRuleOperate struct {
|
type ForwardRuleOperate struct {
|
||||||
ForceDelete bool `json:"forceDelete"`
|
Import bool `json:"import"`
|
||||||
Rules []ForwardRuleOperation `json:"rules" validate:"required,min=1,dive"`
|
Rules []ForwardRuleOperation `json:"rules" validate:"required,min=1,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ForwardRuleOperation struct {
|
type ForwardRuleOperation struct {
|
||||||
|
|||||||
+104
-27
@@ -1,6 +1,11 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
)
|
||||||
|
|
||||||
type MonitorSearch struct {
|
type MonitorSearch struct {
|
||||||
Param string `json:"param" validate:"required,oneof=all cpu memory load io network"`
|
Param string `json:"param" validate:"required,oneof=all cpu memory load io network"`
|
||||||
@@ -26,52 +31,93 @@ type Process struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MonitorSetting struct {
|
type MonitorSetting struct {
|
||||||
MonitorStatus string `json:"monitorStatus"`
|
GPUMonitorStatus string `json:"gpuMonitorStatus"`
|
||||||
MonitorStoreDays string `json:"monitorStoreDays"`
|
GPUMonitorStoreDays string `json:"gpuMonitorStoreDays"`
|
||||||
MonitorInterval string `json:"monitorInterval"`
|
GPUMonitorInterval string `json:"gpuMonitorInterval"`
|
||||||
DefaultNetwork string `json:"defaultNetwork"`
|
VLLMMonitorStatus string `json:"vllmMonitorStatus"`
|
||||||
DefaultIO string `json:"defaultIO"`
|
VLLMMonitorStoreDays string `json:"vllmMonitorStoreDays"`
|
||||||
|
VLLMMonitorInterval string `json:"vllmMonitorInterval"`
|
||||||
|
MonitorStatus string `json:"monitorStatus"`
|
||||||
|
MonitorStoreDays string `json:"monitorStoreDays"`
|
||||||
|
MonitorInterval string `json:"monitorInterval"`
|
||||||
|
DefaultNetwork string `json:"defaultNetwork"`
|
||||||
|
DefaultIO string `json:"defaultIO"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MonitorSettingUpdate struct {
|
type MonitorSettingUpdate struct {
|
||||||
Key string `json:"key" validate:"required,oneof=MonitorStatus MonitorStoreDays MonitorInterval DefaultNetwork DefaultIO"`
|
Key string `json:"key" validate:"required,oneof=MonitorStatus MonitorStoreDays MonitorInterval GPUMonitorStatus GPUMonitorStoreDays GPUMonitorInterval VLLMMonitorStatus VLLMMonitorStoreDays VLLMMonitorInterval DefaultNetwork DefaultIO"`
|
||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MonitorGPUOptions struct {
|
type MonitorGPUOptions struct {
|
||||||
|
Supported bool `json:"supported"`
|
||||||
GPUType string `json:"gpuType"`
|
GPUType string `json:"gpuType"`
|
||||||
ChartHide []GPUChartHide `json:"chartHide"`
|
ChartHide []GPUChartHide `json:"chartHide"`
|
||||||
Options []string `json:"options"`
|
Options []string `json:"options"`
|
||||||
}
|
}
|
||||||
type GPUChartHide struct {
|
type GPUChartHide struct {
|
||||||
ProductName string `json:"productName"`
|
SharedMemory bool `json:"sharedMemory"`
|
||||||
Type string `json:"type"`
|
DeviceID string `json:"deviceID"`
|
||||||
Process bool `json:"process"`
|
Legacy bool `json:"legacy"`
|
||||||
GPU bool `json:"gpu"`
|
ProductName string `json:"productName"`
|
||||||
Memory bool `json:"memory"`
|
Type string `json:"type"`
|
||||||
Power bool `json:"power"`
|
Process bool `json:"process"`
|
||||||
PowerLimit bool `json:"powerLimit"`
|
GPU bool `json:"gpu"`
|
||||||
Temperature bool `json:"temperature"`
|
Memory bool `json:"memory"`
|
||||||
Speed bool `json:"speed"`
|
Power bool `json:"power"`
|
||||||
|
PowerLimit bool `json:"powerLimit"`
|
||||||
|
Temperature bool `json:"temperature"`
|
||||||
|
Speed bool `json:"speed"`
|
||||||
}
|
}
|
||||||
type MonitorGPUSearch struct {
|
type MonitorGPUSearch struct {
|
||||||
|
Aggregation string `json:"aggregation" validate:"omitempty,oneof=avg max"`
|
||||||
|
DeviceID string `json:"deviceID"`
|
||||||
|
Legacy bool `json:"legacy"`
|
||||||
ProductName string `json:"productName"`
|
ProductName string `json:"productName"`
|
||||||
StartTime time.Time `json:"startTime"`
|
StartTime time.Time `json:"startTime"`
|
||||||
EndTime time.Time `json:"endTime"`
|
EndTime time.Time `json:"endTime"`
|
||||||
}
|
}
|
||||||
type MonitorGPUData struct {
|
type MonitorGPUData struct {
|
||||||
Date []time.Time `json:"date"`
|
MemoryActivity []*float64 `json:"memoryActivity"`
|
||||||
GPUValue []float64 `json:"gpuValue"`
|
EncoderUtil []*float64 `json:"encoderUtil"`
|
||||||
TemperatureValue []float64 `json:"temperatureValue"`
|
DecoderUtil []*float64 `json:"decoderUtil"`
|
||||||
PowerTotal []float64 `json:"powerTotal"`
|
JPEGUtil []*float64 `json:"jpegUtil"`
|
||||||
PowerUsed []float64 `json:"powerUsed"`
|
OFAUtil []*float64 `json:"ofaUtil"`
|
||||||
PowerPercent []float64 `json:"powerPercent"`
|
MediaUtil []*float64 `json:"mediaUtil"`
|
||||||
MemoryTotal []float64 `json:"memoryTotal"`
|
ComputeUtil []*float64 `json:"computeUtil"`
|
||||||
MemoryUsed []float64 `json:"memoryUsed"`
|
CopyUtil []*float64 `json:"copyUtil"`
|
||||||
MemoryPercent []float64 `json:"memoryPercent"`
|
HotspotTemperature []*float64 `json:"hotspotTemperature"`
|
||||||
SpeedValue []int `json:"speedValue"`
|
FanRPM []*float64 `json:"fanRPM"`
|
||||||
|
AICPUUtil []*float64 `json:"aiCPUUtil"`
|
||||||
|
CtrlCPUUtil []*float64 `json:"ctrlCPUUtil"`
|
||||||
|
DDRUsed []*float64 `json:"ddrUsed"`
|
||||||
|
DDRTotal []*float64 `json:"ddrTotal"`
|
||||||
|
HBMUsed []*float64 `json:"hbmUsed"`
|
||||||
|
HBMTotal []*float64 `json:"hbmTotal"`
|
||||||
|
DDRBandwidth []*float64 `json:"ddrBandwidth"`
|
||||||
|
HBMBandwidth []*float64 `json:"hbmBandwidth"`
|
||||||
|
MemoryBandwidth []*float64 `json:"memoryBandwidth"`
|
||||||
|
MediaFrequency []*float64 `json:"mediaFrequency"`
|
||||||
|
HugepagesUsed []*float64 `json:"hugepagesUsed"`
|
||||||
|
HugepagesTotal []*float64 `json:"hugepagesTotal"`
|
||||||
|
|
||||||
ProcessCount []int `json:"processCount"`
|
BucketSeconds int64 `json:"bucketSeconds"`
|
||||||
|
SampleCount int64 `json:"sampleCount"`
|
||||||
|
MemoryTemperatureValue []*float64 `json:"memoryTemperatureValue"`
|
||||||
|
FrequencyValue []*float64 `json:"frequencyValue"`
|
||||||
|
MemoryFrequencyValue []*float64 `json:"memoryFrequencyValue"`
|
||||||
|
Date []time.Time `json:"date"`
|
||||||
|
GPUValue []*float64 `json:"gpuValue"`
|
||||||
|
TemperatureValue []*float64 `json:"temperatureValue"`
|
||||||
|
PowerTotal []*float64 `json:"powerTotal"`
|
||||||
|
PowerUsed []*float64 `json:"powerUsed"`
|
||||||
|
PowerPercent []*float64 `json:"powerPercent"`
|
||||||
|
MemoryTotal []*float64 `json:"memoryTotal"`
|
||||||
|
MemoryUsed []*float64 `json:"memoryUsed"`
|
||||||
|
MemoryPercent []*float64 `json:"memoryPercent"`
|
||||||
|
SpeedValue []*float64 `json:"speedValue"`
|
||||||
|
|
||||||
|
ProcessCount []*float64 `json:"processCount"`
|
||||||
GPUProcesses [][]GPUProcess `json:"gpuProcesses"`
|
GPUProcesses [][]GPUProcess `json:"gpuProcesses"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,3 +127,34 @@ type GPUProcess struct {
|
|||||||
ProcessName string `json:"processName"`
|
ProcessName string `json:"processName"`
|
||||||
UsedMemory string `json:"usedMemory"`
|
UsedMemory string `json:"usedMemory"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MonitorVLLMSearch struct {
|
||||||
|
Group string `json:"group" validate:"omitempty,oneof=overview latency requests"`
|
||||||
|
AppInstallID uint `json:"appInstallID" validate:"required"`
|
||||||
|
StartTime time.Time `json:"startTime" validate:"required"`
|
||||||
|
EndTime time.Time `json:"endTime" validate:"required"`
|
||||||
|
Aggregation string `json:"aggregation" validate:"omitempty,oneof=avg max"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MonitorVLLMData struct {
|
||||||
|
SampleCount int64 `json:"sampleCount"`
|
||||||
|
BucketSeconds int64 `json:"bucketSeconds"`
|
||||||
|
Points []model.MonitorVLLM `json:"points"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MonitorVLLMCurrent struct {
|
||||||
|
AppInstallID uint `json:"appInstallID" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MonitorVLLMCurrentData struct {
|
||||||
|
model.MonitorVLLM
|
||||||
|
Metrics map[string]json.RawMessage `json:"metrics,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MonitorVLLMClean struct {
|
||||||
|
AppInstallID uint `json:"appInstallID" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MonitorClean struct {
|
||||||
|
Type string `json:"type" validate:"required,oneof=host gpu"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ type AppInstallCreate struct {
|
|||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
Services map[string]string `json:"services"`
|
Services map[string]string `json:"services"`
|
||||||
TaskID string `json:"taskID"`
|
TaskID string `json:"taskID"`
|
||||||
|
GPU *dto.ModelGPUConfig `json:"gpu"`
|
||||||
|
|
||||||
|
GPUImageType string `json:"-"`
|
||||||
|
|
||||||
|
SyncModelAccount *bool `json:"syncModelAccount"`
|
||||||
|
ModelAccountBaseURLType string `json:"modelAccountBaseURLType" validate:"omitempty,oneof=container localhost systemIP custom"`
|
||||||
|
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||||
|
|
||||||
AppContainerConfig
|
AppContainerConfig
|
||||||
NodePushConfig
|
NodePushConfig
|
||||||
@@ -83,31 +90,33 @@ type AppBackupDelete struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AppInstalledOperate struct {
|
type AppInstalledOperate struct {
|
||||||
InstallId uint `json:"installId" validate:"required"`
|
FeishuTransition *dto.AgentFeishuTransition `json:"feishuTransition,omitempty"`
|
||||||
BackupId uint `json:"backupId"`
|
InstallId uint `json:"installId" validate:"required"`
|
||||||
DetailId uint `json:"detailId"`
|
BackupId uint `json:"backupId"`
|
||||||
Operate constant.AppOperate `json:"operate" validate:"required"`
|
DetailId uint `json:"detailId"`
|
||||||
ForceDelete bool `json:"forceDelete"`
|
Operate constant.AppOperate `json:"operate" validate:"required"`
|
||||||
DeleteBackup bool `json:"deleteBackup"`
|
ForceDelete bool `json:"forceDelete"`
|
||||||
DeleteDB bool `json:"deleteDB"`
|
DeleteBackup bool `json:"deleteBackup"`
|
||||||
Backup bool `json:"backup"`
|
DeleteDB bool `json:"deleteDB"`
|
||||||
PullImage bool `json:"pullImage"`
|
Backup bool `json:"backup"`
|
||||||
DockerCompose string `json:"dockerCompose"`
|
PullImage bool `json:"pullImage"`
|
||||||
TaskID string `json:"taskID"`
|
DockerCompose string `json:"dockerCompose"`
|
||||||
DeleteImage bool `json:"deleteImage"`
|
TaskID string `json:"taskID"`
|
||||||
Favorite bool `json:"favorite"`
|
DeleteImage bool `json:"deleteImage"`
|
||||||
|
Favorite bool `json:"favorite"`
|
||||||
|
|
||||||
UseLifecycleScripts bool `json:"-"`
|
UseLifecycleScripts bool `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppInstallUpgrade struct {
|
type AppInstallUpgrade struct {
|
||||||
InstallID uint `json:"installId"`
|
FeishuTransition *dto.AgentFeishuTransition `json:"feishuTransition,omitempty"`
|
||||||
DetailID uint `json:"detailId"`
|
InstallID uint `json:"installId"`
|
||||||
Backup bool `json:"backup"`
|
DetailID uint `json:"detailId"`
|
||||||
PullImage bool `json:"pullImage"`
|
Backup bool `json:"backup"`
|
||||||
DeleteImage bool `json:"deleteImage"`
|
PullImage bool `json:"pullImage"`
|
||||||
DockerCompose string `json:"dockerCompose"`
|
DeleteImage bool `json:"deleteImage"`
|
||||||
TaskID string `json:"taskID"`
|
DockerCompose string `json:"dockerCompose"`
|
||||||
|
TaskID string `json:"taskID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppInstallDelete struct {
|
type AppInstallDelete struct {
|
||||||
@@ -122,9 +131,13 @@ type AppInstallDelete struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AppInstalledUpdate struct {
|
type AppInstalledUpdate struct {
|
||||||
InstallId uint `json:"installId" validate:"required"`
|
GPU *dto.ModelGPUConfig `json:"gpu"`
|
||||||
Params map[string]interface{} `json:"params" validate:"required"`
|
SyncModelAccount *bool `json:"syncModelAccount"`
|
||||||
TaskID string `json:"-"`
|
ModelAccountBaseURLType string `json:"modelAccountBaseURLType" validate:"omitempty,oneof=container localhost systemIP custom"`
|
||||||
|
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||||
|
InstallId uint `json:"installId" validate:"required"`
|
||||||
|
Params map[string]interface{} `json:"params" validate:"required"`
|
||||||
|
TaskID string `json:"-"`
|
||||||
AppContainerConfig
|
AppContainerConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,6 +160,7 @@ type PortUpdate struct {
|
|||||||
type AppUpdateVersion struct {
|
type AppUpdateVersion struct {
|
||||||
AppInstallID uint `json:"appInstallID" validate:"required"`
|
AppInstallID uint `json:"appInstallID" validate:"required"`
|
||||||
UpdateVersion string `json:"updateVersion"`
|
UpdateVersion string `json:"updateVersion"`
|
||||||
|
LoadCompose *bool `json:"loadCompose,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppInstallSortItem struct {
|
type AppInstallSortItem struct {
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ type FileWget struct {
|
|||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
IgnoreCertificate bool `json:"ignoreCertificate"`
|
IgnoreCertificate bool `json:"ignoreCertificate"`
|
||||||
UseProxy bool `json:"useProxy"`
|
UseProxy bool `json:"useProxy"`
|
||||||
|
UseServerFilename bool `json:"useServerFilename"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileMove struct {
|
type FileMove struct {
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package request
|
||||||
|
|
||||||
|
type MCPWebsiteBinding struct {
|
||||||
|
Operation string `json:"operation"`
|
||||||
|
RequestID string `json:"requestId"`
|
||||||
|
BindingID string `json:"bindingId"`
|
||||||
|
WebsiteID uint `json:"websiteId"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
EnableSSL bool `json:"enableSSL"`
|
||||||
|
SSLID uint `json:"sslID"`
|
||||||
|
Upstream string `json:"upstream"`
|
||||||
|
CoreHost string `json:"coreHost"`
|
||||||
|
ProxyKey string `json:"proxyKey"`
|
||||||
|
}
|
||||||
@@ -8,14 +8,15 @@ type TensorRTLLMSearch struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TensorRTLLMCreate struct {
|
type TensorRTLLMCreate struct {
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
ContainerName string `json:"containerName" validate:"required"`
|
ContainerName string `json:"containerName" validate:"required"`
|
||||||
Version string `json:"version" validate:"required"`
|
Version string `json:"version" validate:"required"`
|
||||||
ModelDir string `json:"modelDir" validate:"required"`
|
ModelDir string `json:"modelDir" validate:"required"`
|
||||||
Image string `json:"image" validate:"required"`
|
Image string `json:"image" validate:"required"`
|
||||||
Command string `json:"command" validate:"required"`
|
Command string `json:"command" validate:"required"`
|
||||||
ModelType string `json:"modelType"`
|
ModelType string `json:"modelType"`
|
||||||
ModelSpeedup bool `json:"modelSpeedup"`
|
ModelSpeedup bool `json:"modelSpeedup"`
|
||||||
|
GPU *dto.ModelGPUConfig `json:"gpu"`
|
||||||
DockerConfig
|
DockerConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -182,3 +182,12 @@ type WebsiteSSLFileUpload struct {
|
|||||||
PushNode bool `json:"pushNode"`
|
PushNode bool `json:"pushNode"`
|
||||||
Nodes string `json:"nodes"`
|
Nodes string `json:"nodes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WebsiteCAImport struct {
|
||||||
|
Name string `json:"name" validate:"required,max=128"`
|
||||||
|
Type string `json:"type" validate:"required,oneof=paste local"`
|
||||||
|
Certificate string `json:"certificate" validate:"required_if=Type paste"`
|
||||||
|
PrivateKey string `json:"privateKey" validate:"required_if=Type paste"`
|
||||||
|
CertificatePath string `json:"certificatePath" validate:"required_if=Type local"`
|
||||||
|
PrivateKeyPath string `json:"privateKeyPath" validate:"required_if=Type local"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ type AppDTO struct {
|
|||||||
type AppItem struct {
|
type AppItem struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
|
Icon string `json:"icon,omitempty"`
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
@@ -50,19 +51,21 @@ type TagDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AppInstalledCheck struct {
|
type AppInstalledCheck struct {
|
||||||
IsExist bool `json:"isExist"`
|
ModelAccountBaseURLType string `json:"modelAccountBaseURLType"`
|
||||||
Name string `json:"name"`
|
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||||
App string `json:"app"`
|
IsExist bool `json:"isExist"`
|
||||||
Version string `json:"version"`
|
Name string `json:"name"`
|
||||||
Status string `json:"status"`
|
App string `json:"app"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
Version string `json:"version"`
|
||||||
LastBackupAt string `json:"lastBackupAt"`
|
Status string `json:"status"`
|
||||||
AppInstallID uint `json:"appInstallId"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
ContainerName string `json:"containerName"`
|
LastBackupAt string `json:"lastBackupAt"`
|
||||||
InstallPath string `json:"installPath"`
|
AppInstallID uint `json:"appInstallId"`
|
||||||
HttpPort int `json:"httpPort"`
|
ContainerName string `json:"containerName"`
|
||||||
HttpsPort int `json:"httpsPort"`
|
InstallPath string `json:"installPath"`
|
||||||
WebsiteDir string `json:"websiteDir"`
|
HttpPort int `json:"httpPort"`
|
||||||
|
HttpsPort int `json:"httpsPort"`
|
||||||
|
WebsiteDir string `json:"websiteDir"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppDetailDTO struct {
|
type AppDetailDTO struct {
|
||||||
@@ -184,7 +187,15 @@ type AppParam struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AppConfig struct {
|
type AppConfig struct {
|
||||||
Params []AppParam `json:"params"`
|
GPU *dto.ModelGPUConfig `json:"gpu,omitempty"`
|
||||||
RawCompose string `json:"rawCompose"`
|
AppKey string `json:"appKey"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
HttpPort int `json:"httpPort"`
|
||||||
|
SyncModelAccount bool `json:"syncModelAccount"`
|
||||||
|
ModelAccountBaseURLType string `json:"modelAccountBaseURLType"`
|
||||||
|
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||||
|
Params []AppParam `json:"params"`
|
||||||
|
RawCompose string `json:"rawCompose"`
|
||||||
|
IsEdit bool `json:"isEdit"`
|
||||||
request.AppContainerConfig
|
request.AppContainerConfig
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
type MCPWebsiteBinding struct {
|
||||||
|
State string `json:"state"`
|
||||||
|
WebsiteID uint `json:"websiteId"`
|
||||||
|
BindingID string `json:"bindingId"`
|
||||||
|
RequestID string `json:"requestId"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
EnableSSL bool `json:"enableSSL"`
|
||||||
|
SSLID uint `json:"sslID"`
|
||||||
|
WebsiteStatus string `json:"websiteStatus"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
Code string `json:"code,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
Definitive bool `json:"definitive,omitempty"`
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package response
|
package response
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
)
|
)
|
||||||
@@ -12,12 +13,13 @@ type TensorRTLLMsRes struct {
|
|||||||
|
|
||||||
type TensorRTLLMDTO struct {
|
type TensorRTLLMDTO struct {
|
||||||
model.TensorRTLLM
|
model.TensorRTLLM
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
Dir string `json:"dir"`
|
Dir string `json:"dir"`
|
||||||
ModelDir string `json:"modelDir"`
|
ModelDir string `json:"modelDir"`
|
||||||
Image string `json:"image"`
|
Image string `json:"image"`
|
||||||
Command string `json:"command"`
|
Command string `json:"command"`
|
||||||
|
GPU *dto.ModelGPUConfig `json:"gpu"`
|
||||||
|
|
||||||
ExposedPorts []request.ExposedPort `json:"exposedPorts"`
|
ExposedPorts []request.ExposedPort `json:"exposedPorts"`
|
||||||
Environments []request.Environment `json:"environments"`
|
Environments []request.Environment `json:"environments"`
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ type RootCertOperate struct {
|
|||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Mode string `json:"mode"`
|
Mode string `json:"mode"`
|
||||||
EncryptionMode string `json:"encryptionMode" validate:"required,oneof=rsa ed25519 ecdsa dsa"`
|
EncryptionMode string `json:"encryptionMode"`
|
||||||
PassPhrase string `json:"passPhrase"`
|
PassPhrase string `json:"passPhrase"`
|
||||||
PublicKey string `json:"publicKey"`
|
PublicKey string `json:"publicKey"`
|
||||||
PrivateKey string `json:"privateKey"`
|
PrivateKey string `json:"privateKey"`
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package model
|
|||||||
|
|
||||||
type AgentAccount struct {
|
type AgentAccount struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
|
AppInstallID uint `json:"appInstallId"`
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
APIKey string `json:"apiKey"`
|
APIKey string `json:"apiKey"`
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ type AppInstall struct {
|
|||||||
Favorite bool `json:"favorite"`
|
Favorite bool `json:"favorite"`
|
||||||
SortOrder int `json:"sortOrder" gorm:"default:0"`
|
SortOrder int `json:"sortOrder" gorm:"default:0"`
|
||||||
|
|
||||||
|
SyncModelAccount bool `json:"syncModelAccount"`
|
||||||
|
ModelAccountBaseURLType string `json:"modelAccountBaseURLType"`
|
||||||
|
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||||
|
|
||||||
App App `json:"app" gorm:"-:migration"`
|
App App `json:"app" gorm:"-:migration"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,13 @@ type AppLauncher struct {
|
|||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type QuickDirectory struct {
|
||||||
|
BaseModel
|
||||||
|
Path string `json:"path"`
|
||||||
|
Alias string `json:"alias"`
|
||||||
|
Legacy bool `json:"legacy" gorm:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
type QuickJump struct {
|
type QuickJump struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|||||||
@@ -1,222 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
|
||||||
)
|
|
||||||
|
|
||||||
const FirewallRuleSequenceStep int64 = 1 << 32
|
|
||||||
|
|
||||||
type DockerPortGuardPolicy struct {
|
|
||||||
BaseModel
|
|
||||||
|
|
||||||
UUID string `gorm:"size:64;not null;uniqueIndex" json:"uuid"`
|
|
||||||
ReadOnly bool `gorm:"not null;default:false;uniqueIndex:idx_docker_port_guard_endpoint" json:"-"`
|
|
||||||
Family string `gorm:"size:16;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"family"`
|
|
||||||
HostIP string `gorm:"size:64;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"hostIP"`
|
|
||||||
HostPort uint16 `gorm:"not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"hostPort"`
|
|
||||||
Protocol string `gorm:"size:8;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"protocol"`
|
|
||||||
Mode string `gorm:"size:32;not null" json:"mode"`
|
|
||||||
Sources string `gorm:"type:text" json:"-"`
|
|
||||||
Description string `gorm:"type:text" json:"description"`
|
|
||||||
NativeAction string `gorm:"size:32;not null;default:''" json:"-"`
|
|
||||||
NativeRules string `gorm:"type:text" json:"-"`
|
|
||||||
Sequence int64 `gorm:"not null;default:0" json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ForwardingRule struct {
|
|
||||||
BaseModel
|
|
||||||
|
|
||||||
Family string `gorm:"size:16;not null;uniqueIndex:idx_forwarding_rule_identity" json:"family"`
|
|
||||||
Protocol string `gorm:"size:8;not null;uniqueIndex:idx_forwarding_rule_identity" json:"protocol"`
|
|
||||||
Port string `gorm:"size:32;not null;uniqueIndex:idx_forwarding_rule_identity" json:"port"`
|
|
||||||
TargetIP string `gorm:"size:64;not null;uniqueIndex:idx_forwarding_rule_identity" json:"targetIP"`
|
|
||||||
TargetPort string `gorm:"size:32;not null;uniqueIndex:idx_forwarding_rule_identity" json:"targetPort"`
|
|
||||||
Interface string `gorm:"size:32;not null;default:'';uniqueIndex:idx_forwarding_rule_identity" json:"interface"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FirewallRule struct {
|
|
||||||
UUID string `gorm:"size:64;primaryKey" json:"uuid"`
|
|
||||||
Family string `gorm:"size:16;not null" json:"family"`
|
|
||||||
|
|
||||||
Protocol string `gorm:"size:32;not null" json:"protocol"`
|
|
||||||
SourceAddress string `gorm:"size:255" json:"sourceAddress"`
|
|
||||||
SourcePort string `gorm:"size:64" json:"sourcePort"`
|
|
||||||
DestinationAddress string `gorm:"size:255" json:"destinationAddress"`
|
|
||||||
DestinationPort string `gorm:"size:64" json:"destinationPort"`
|
|
||||||
Interface string `gorm:"size:128" json:"interface"`
|
|
||||||
ConnectionStates string `gorm:"type:text" json:"connectionStates"`
|
|
||||||
Action string `gorm:"size:32;not null" json:"action"`
|
|
||||||
Description string `gorm:"type:text" json:"description"`
|
|
||||||
CompatibilityError string `gorm:"type:text" json:"compatibilityError,omitempty"`
|
|
||||||
Priority *int `json:"priority,omitempty"`
|
|
||||||
Sequence *int64 `gorm:"index" json:"sequence,omitempty"`
|
|
||||||
|
|
||||||
Origin string `gorm:"size:32;not null" json:"origin"`
|
|
||||||
Owner string `gorm:"size:320;not null" json:"owner"`
|
|
||||||
Revision uint `gorm:"not null;default:1" json:"revision"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func FirewallRuleOwner(sourceKind, sourceID string) string {
|
|
||||||
sourceKind = strings.TrimSpace(sourceKind)
|
|
||||||
sourceID = strings.TrimSpace(sourceID)
|
|
||||||
if sourceID == "" {
|
|
||||||
return sourceKind
|
|
||||||
}
|
|
||||||
return sourceKind + ":" + sourceID
|
|
||||||
}
|
|
||||||
|
|
||||||
func FirewallRuleFromDomain(rule filter.FirewallRule) (FirewallRule, error) {
|
|
||||||
normalized, err := filter.NormalizeRule(rule)
|
|
||||||
if err != nil {
|
|
||||||
return FirewallRule{}, err
|
|
||||||
}
|
|
||||||
switch normalized.NativeKind {
|
|
||||||
case "", filter.NativeKindRule, filter.NativeKindZonePort, filter.NativeKindRichRule, filter.NativeKindUFWRule:
|
|
||||||
default:
|
|
||||||
return FirewallRule{}, fmt.Errorf("%w: native rule %q cannot be stored as a provider-neutral policy", filter.ErrUnsupportedScope, normalized.NativeKind)
|
|
||||||
}
|
|
||||||
record := FirewallRule{
|
|
||||||
Family: string(normalized.Scope.Family),
|
|
||||||
Protocol: normalized.Protocol,
|
|
||||||
SourceAddress: normalized.SourceAddress,
|
|
||||||
SourcePort: normalized.SourcePort,
|
|
||||||
DestinationAddress: normalized.DestinationAddress,
|
|
||||||
DestinationPort: normalized.DestinationPort,
|
|
||||||
Interface: normalized.Interface,
|
|
||||||
ConnectionStates: strings.Join(normalized.ConnectionStates, ","),
|
|
||||||
Action: string(normalized.Action),
|
|
||||||
Description: normalized.Description,
|
|
||||||
}
|
|
||||||
if normalized.Scope.Provider == filter.ProviderFirewalld {
|
|
||||||
record.Priority = normalized.Priority
|
|
||||||
}
|
|
||||||
return record, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rule FirewallRule) PolicyKey() string {
|
|
||||||
payload, _ := json.Marshal(struct {
|
|
||||||
Family string `json:"family"`
|
|
||||||
Protocol string `json:"protocol"`
|
|
||||||
SourceAddress string `json:"sourceAddress,omitempty"`
|
|
||||||
SourcePort string `json:"sourcePort,omitempty"`
|
|
||||||
DestinationAddress string `json:"destinationAddress,omitempty"`
|
|
||||||
DestinationPort string `json:"destinationPort,omitempty"`
|
|
||||||
Interface string `json:"interface,omitempty"`
|
|
||||||
ConnectionStates string `json:"connectionStates,omitempty"`
|
|
||||||
Action string `json:"action"`
|
|
||||||
}{
|
|
||||||
Family: rule.Family, Protocol: rule.Protocol,
|
|
||||||
SourceAddress: rule.SourceAddress, SourcePort: rule.SourcePort,
|
|
||||||
DestinationAddress: rule.DestinationAddress, DestinationPort: rule.DestinationPort,
|
|
||||||
Interface: rule.Interface, ConnectionStates: rule.ConnectionStates, Action: rule.Action,
|
|
||||||
})
|
|
||||||
sum := sha256.Sum256(payload)
|
|
||||||
return hex.EncodeToString(sum[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rule FirewallRule) RulesForProvider(provider filter.Provider) ([]filter.FirewallRule, error) {
|
|
||||||
if rule.CompatibilityError != "" {
|
|
||||||
return nil, fmt.Errorf("%w: %s", filter.ErrUnsupportedScope, rule.CompatibilityError)
|
|
||||||
}
|
|
||||||
connectionStates := make([]string, 0)
|
|
||||||
if rule.ConnectionStates != "" {
|
|
||||||
connectionStates = strings.Split(rule.ConnectionStates, ",")
|
|
||||||
}
|
|
||||||
base := filter.FirewallRule{
|
|
||||||
Protocol: rule.Protocol, SourceAddress: rule.SourceAddress, SourcePort: rule.SourcePort,
|
|
||||||
DestinationAddress: rule.DestinationAddress, DestinationPort: rule.DestinationPort,
|
|
||||||
Interface: rule.Interface, ConnectionStates: connectionStates,
|
|
||||||
Action: filter.Action(rule.Action), Description: rule.Description,
|
|
||||||
}
|
|
||||||
if provider != filter.ProviderUFW && strings.EqualFold(strings.TrimSpace(base.Protocol), "all") &&
|
|
||||||
strings.TrimSpace(base.SourcePort) == "" && strings.TrimSpace(base.DestinationPort) != "" {
|
|
||||||
base.Protocol = "tcp/udp"
|
|
||||||
}
|
|
||||||
if provider == filter.ProviderFirewalld {
|
|
||||||
base.Priority = rule.Priority
|
|
||||||
}
|
|
||||||
families := []filter.Family{filter.Family(rule.Family)}
|
|
||||||
if provider != filter.ProviderFirewalld && families[0] == filter.FamilyInet {
|
|
||||||
hasIPv4, hasIPv6 := ruleAddressFamilies(base)
|
|
||||||
switch {
|
|
||||||
case hasIPv4 && hasIPv6:
|
|
||||||
return nil, fmt.Errorf("%w: inet policy contains both IPv4 and IPv6 addresses", filter.ErrUnsupportedScope)
|
|
||||||
case hasIPv6 || strings.EqualFold(base.Protocol, "icmpv6"):
|
|
||||||
families = []filter.Family{filter.FamilyIPv6}
|
|
||||||
case hasIPv4:
|
|
||||||
families = []filter.Family{filter.FamilyIPv4}
|
|
||||||
default:
|
|
||||||
families = []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result := make([]filter.FirewallRule, 0, len(families))
|
|
||||||
for _, family := range families {
|
|
||||||
compiled := base
|
|
||||||
compiled.Scope = filter.Scope{Provider: provider, Family: family, Direction: filter.DirectionInput}
|
|
||||||
switch provider {
|
|
||||||
case filter.ProviderIptables, filter.ProviderNftables:
|
|
||||||
compiled.Scope.Table, compiled.Scope.Chain = "filter", filter.IptablesInputChain
|
|
||||||
case filter.ProviderFirewalld:
|
|
||||||
compiled.Scope.Zone = filter.FirewalldInputZone
|
|
||||||
case filter.ProviderUFW:
|
|
||||||
compiled.Scope.Chain = filter.UFWInputChain
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("%w: unsupported firewall provider %q", filter.ErrProviderUnavailable, provider)
|
|
||||||
}
|
|
||||||
expanded, err := filter.ExpandAtomicRules(compiled)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
result = append(result, expanded...)
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func SortFirewallRules(rules []FirewallRule, provider filter.Provider) {
|
|
||||||
sort.SliceStable(rules, func(i, j int) bool {
|
|
||||||
left, right := rules[i], rules[j]
|
|
||||||
if provider == filter.ProviderFirewalld {
|
|
||||||
switch {
|
|
||||||
case left.Priority == nil && right.Priority != nil:
|
|
||||||
return false
|
|
||||||
case left.Priority != nil && right.Priority == nil:
|
|
||||||
return true
|
|
||||||
case left.Priority != nil && right.Priority != nil && *left.Priority != *right.Priority:
|
|
||||||
return *left.Priority < *right.Priority
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch {
|
|
||||||
case left.Sequence == nil && right.Sequence != nil:
|
|
||||||
return false
|
|
||||||
case left.Sequence != nil && right.Sequence == nil:
|
|
||||||
return true
|
|
||||||
case left.Sequence != nil && right.Sequence != nil && *left.Sequence != *right.Sequence:
|
|
||||||
return *left.Sequence < *right.Sequence
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return left.UUID < right.UUID
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func ruleAddressFamilies(rule filter.FirewallRule) (bool, bool) {
|
|
||||||
hasIPv4, hasIPv6 := false, false
|
|
||||||
for _, address := range []string{rule.SourceAddress, rule.DestinationAddress} {
|
|
||||||
address = strings.TrimSpace(address)
|
|
||||||
if address == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.Contains(address, ":") {
|
|
||||||
hasIPv6 = true
|
|
||||||
} else {
|
|
||||||
hasIPv4 = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return hasIPv4, hasIPv6
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
type MonitorBase struct {
|
type MonitorBase struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
Cpu float64 `json:"cpu"`
|
Cpu float64 `json:"cpu"`
|
||||||
@@ -33,14 +35,88 @@ type MonitorNetwork struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MonitorGPU struct {
|
type MonitorGPU struct {
|
||||||
|
MemoryUtil *float64 `json:"memoryUtil"`
|
||||||
|
MemoryActivity *float64 `json:"memoryActivity"`
|
||||||
|
EncoderUtil *float64 `json:"encoderUtil"`
|
||||||
|
DecoderUtil *float64 `json:"decoderUtil"`
|
||||||
|
JPEGUtil *float64 `json:"jpegUtil"`
|
||||||
|
OFAUtil *float64 `json:"ofaUtil"`
|
||||||
|
MediaUtil *float64 `json:"mediaUtil"`
|
||||||
|
ComputeUtil *float64 `json:"computeUtil"`
|
||||||
|
CopyUtil *float64 `json:"copyUtil"`
|
||||||
|
HotspotTemperature *float64 `json:"hotspotTemperature"`
|
||||||
|
FanRPM *float64 `json:"fanRPM"`
|
||||||
|
AICPUUtil *float64 `json:"aiCPUUtil"`
|
||||||
|
CtrlCPUUtil *float64 `json:"ctrlCPUUtil"`
|
||||||
|
DDRUsed *float64 `json:"ddrUsed"`
|
||||||
|
DDRTotal *float64 `json:"ddrTotal"`
|
||||||
|
HBMUsed *float64 `json:"hbmUsed"`
|
||||||
|
HBMTotal *float64 `json:"hbmTotal"`
|
||||||
|
DDRBandwidth *float64 `json:"ddrBandwidth"`
|
||||||
|
HBMBandwidth *float64 `json:"hbmBandwidth"`
|
||||||
|
MemoryBandwidth *float64 `json:"memoryBandwidth"`
|
||||||
|
MediaFrequency *float64 `json:"mediaFrequency"`
|
||||||
|
HugepagesUsed *float64 `json:"hugepagesUsed"`
|
||||||
|
HugepagesTotal *float64 `json:"hugepagesTotal"`
|
||||||
|
|
||||||
|
MemoryTemperature *float64 `json:"memoryTemperature"`
|
||||||
|
DeviceID string `json:"deviceID"`
|
||||||
|
DeviceType string `json:"deviceType"`
|
||||||
|
ProcessStatus string `json:"processStatus"`
|
||||||
|
Frequency *float64 `json:"frequency"`
|
||||||
|
MemoryFrequency *float64 `json:"memoryFrequency"`
|
||||||
|
IntervalSeconds int `json:"intervalSeconds"`
|
||||||
BaseModel
|
BaseModel
|
||||||
ProductName string `json:"productName"`
|
ProductName string `json:"productName"`
|
||||||
GPUUtil float64 `json:"gpuUtil"`
|
GPUUtil *float64 `json:"gpuUtil"`
|
||||||
Temperature float64 `json:"temperature"`
|
Temperature *float64 `json:"temperature"`
|
||||||
PowerDraw float64 `json:"powerDraw"`
|
PowerDraw *float64 `json:"powerDraw"`
|
||||||
MaxPowerLimit float64 `json:"maxPowerLimit"`
|
MaxPowerLimit *float64 `json:"maxPowerLimit"`
|
||||||
MemUsed float64 `json:"memUsed"`
|
MemUsed *float64 `json:"memUsed"`
|
||||||
MemTotal float64 `json:"memTotal"`
|
MemTotal *float64 `json:"memTotal"`
|
||||||
FanSpeed int `json:"fanSpeed"`
|
FanSpeed *float64 `json:"fanSpeed"`
|
||||||
Processes string `json:"processes"`
|
Processes string `json:"processes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MonitorVLLM struct {
|
||||||
|
ID uint `json:"-" gorm:"primarykey;autoIncrement"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
AppInstallID uint `json:"appInstallID"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
RawMetrics string `json:"-"`
|
||||||
|
HistogramDeltas string `json:"-"`
|
||||||
|
IntervalSeconds float64 `json:"intervalSeconds" gorm:"-"`
|
||||||
|
SampleIntervalSeconds int `json:"-"`
|
||||||
|
RequestDeltas string `json:"-"`
|
||||||
|
CompletedRequests *float64 `json:"completedRequests" gorm:"-"`
|
||||||
|
QueueTime *float64 `json:"queueTime"`
|
||||||
|
AverageGenerationTokens *float64 `json:"averageGenerationTokens"`
|
||||||
|
PrefixCacheHitRate *float64 `json:"prefixCacheHitRate"`
|
||||||
|
FinishReasons map[string]float64 `json:"finishReasons" gorm:"-"`
|
||||||
|
PromptDistribution map[string]float64 `json:"promptDistribution" gorm:"-"`
|
||||||
|
GenerationDistribution map[string]float64 `json:"generationDistribution" gorm:"-"`
|
||||||
|
|
||||||
|
Running *float64 `json:"running"`
|
||||||
|
Waiting *float64 `json:"waiting"`
|
||||||
|
CacheUsage *float64 `json:"cacheUsage"`
|
||||||
|
PromptThroughput *float64 `json:"promptThroughput"`
|
||||||
|
GenerationThroughput *float64 `json:"generationThroughput"`
|
||||||
|
RequestThroughput *float64 `json:"requestThroughput"`
|
||||||
|
TimeToFirstToken *float64 `json:"timeToFirstToken"`
|
||||||
|
TimePerOutputToken *float64 `json:"timePerOutputToken"`
|
||||||
|
RequestLatency *float64 `json:"requestLatency"`
|
||||||
|
PrefillTime *float64 `json:"prefillTime"`
|
||||||
|
DecodeTime *float64 `json:"decodeTime"`
|
||||||
|
TimeToFirstTokenP50 *float64 `json:"timeToFirstTokenP50"`
|
||||||
|
TimeToFirstTokenP90 *float64 `json:"timeToFirstTokenP90"`
|
||||||
|
TimeToFirstTokenP95 *float64 `json:"timeToFirstTokenP95"`
|
||||||
|
TimeToFirstTokenP99 *float64 `json:"timeToFirstTokenP99"`
|
||||||
|
TimePerOutputTokenP50 *float64 `json:"timePerOutputTokenP50"`
|
||||||
|
TimePerOutputTokenP90 *float64 `json:"timePerOutputTokenP90"`
|
||||||
|
TimePerOutputTokenP95 *float64 `json:"timePerOutputTokenP95"`
|
||||||
|
TimePerOutputTokenP99 *float64 `json:"timePerOutputTokenP99"`
|
||||||
|
RequestLatencyP50 *float64 `json:"requestLatencyP50"`
|
||||||
|
RequestLatencyP90 *float64 `json:"requestLatencyP90"`
|
||||||
|
RequestLatencyP95 *float64 `json:"requestLatencyP95"`
|
||||||
|
RequestLatencyP99 *float64 `json:"requestLatencyP99"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ var catalog = map[string]Meta{
|
|||||||
{APIType: "openai-responses", BaseURL: "https://api.deepseek.com"},
|
{APIType: "openai-responses", BaseURL: "https://api.deepseek.com"},
|
||||||
anthropicAPIConfig("https://api.deepseek.com/anthropic", AuthModeXAPIKey),
|
anthropicAPIConfig("https://api.deepseek.com/anthropic", AuthModeXAPIKey),
|
||||||
},
|
},
|
||||||
Models: []Model{{ID: "deepseek-v4-flash", Name: "deepseek-v4-flash"}, {ID: "deepseek-v4-pro", Name: "deepseek-v4-pro"}},
|
Models: []Model{{ID: "deepseek-flash", Name: "deepseek-flash"}, {ID: "deepseek-v4-pro", Name: "deepseek-v4-pro"}},
|
||||||
},
|
},
|
||||||
"bailian-coding-plan": {
|
"bailian-coding-plan": {
|
||||||
Key: "bailian-coding-plan", DisplayNameKey: "AIProviderBailianCodingPlan", Sort: 30, DefaultAPIType: "openai-completions", EnvKey: "QWEN_API_KEY",
|
Key: "bailian-coding-plan", DisplayNameKey: "AIProviderBailianCodingPlan", Sort: 30, DefaultAPIType: "openai-completions", EnvKey: "QWEN_API_KEY",
|
||||||
@@ -82,7 +82,6 @@ var catalog = map[string]Meta{
|
|||||||
{ID: "kimi-k2.5", Name: "Kimi K2.5"},
|
{ID: "kimi-k2.5", Name: "Kimi K2.5"},
|
||||||
{ID: "qwen3.5-plus", Name: "Qwen3.5-Plus"},
|
{ID: "qwen3.5-plus", Name: "Qwen3.5-Plus"},
|
||||||
{ID: "glm-5", Name: "GLM-5"},
|
{ID: "glm-5", Name: "GLM-5"},
|
||||||
{ID: "MiniMax-M2.5", Name: "MiniMax M2.5"},
|
|
||||||
{ID: "qwen3.6-plus", Name: "Qwen3.6-Plus"},
|
{ID: "qwen3.6-plus", Name: "Qwen3.6-Plus"},
|
||||||
{ID: "qwen3.7-plus", Name: "Qwen3.7-Plus"},
|
{ID: "qwen3.7-plus", Name: "Qwen3.7-Plus"},
|
||||||
},
|
},
|
||||||
@@ -114,7 +113,7 @@ var catalog = map[string]Meta{
|
|||||||
{APIType: "openai-completions", BaseURL: "https://api.minimaxi.com/v1"},
|
{APIType: "openai-completions", BaseURL: "https://api.minimaxi.com/v1"},
|
||||||
{APIType: "minimax-images", BaseURL: "https://api.minimaxi.com"},
|
{APIType: "minimax-images", BaseURL: "https://api.minimaxi.com"},
|
||||||
},
|
},
|
||||||
Models: []Model{{ID: "MiniMax-M3", Name: "MiniMax M3"}, {ID: "MiniMax-M2.7", Name: "MiniMax M2.7"}, {ID: "MiniMax-M2.7-highspeed", Name: "MiniMax M2.7 highspeed"}},
|
Models: []Model{{ID: "MiniMax-M3.1-Flash", Name: "MiniMax M3.1 Flash"}, {ID: "MiniMax-M3", Name: "MiniMax M3"}},
|
||||||
},
|
},
|
||||||
"xiaomi": {
|
"xiaomi": {
|
||||||
Key: "xiaomi", DisplayName: "Xiaomi", Sort: 46, DefaultAPIType: "openai-completions", EnvKey: "XIAOMI_API_KEY",
|
Key: "xiaomi", DisplayName: "Xiaomi", Sort: 46, DefaultAPIType: "openai-completions", EnvKey: "XIAOMI_API_KEY",
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMiniMaxCatalog(t *testing.T) {
|
||||||
|
meta, ok := Get("minimax")
|
||||||
|
if !ok || !reflect.DeepEqual(meta.Models, []Model{{ID: "MiniMax-M3.1-Flash", Name: "MiniMax M3.1 Flash"}, {ID: "MiniMax-M3", Name: "MiniMax M3"}}) {
|
||||||
|
t.Fatalf("unexpected MiniMax models: %+v", meta.Models)
|
||||||
|
}
|
||||||
|
if meta.DefaultAPIType != "anthropic-messages" {
|
||||||
|
t.Fatalf("changed default protocol: %s", meta.DefaultAPIType)
|
||||||
|
}
|
||||||
|
for _, provider := range All() {
|
||||||
|
for _, model := range provider.Models {
|
||||||
|
if strings.Contains(strings.ToLower(model.ID), "minimax") && model.ID != "MiniMax-M3.1-Flash" && model.ID != "MiniMax-M3" {
|
||||||
|
t.Errorf("legacy suggestion in %s: %s", provider.Key, model.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiniMaxVerificationRequests(t *testing.T) {
|
||||||
|
for _, region := range []string{"https://api.minimaxi.com", "https://api.minimax.io"} {
|
||||||
|
for _, protocol := range []string{"anthropic-messages", "openai-completions"} {
|
||||||
|
for _, auth := range []string{AuthModeXAPIKey, AuthModeBearer} {
|
||||||
|
for _, model := range []string{"MiniMax-M3.1-Flash", "MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.7-highspeed", "custom-model"} {
|
||||||
|
t.Run(region+"/"+protocol+"/"+auth+"/"+model, func(t *testing.T) {
|
||||||
|
base, endpoint := region+"/v1", "/chat/completions"
|
||||||
|
if protocol == "anthropic-messages" {
|
||||||
|
base, endpoint = region+"/anthropic", "/v1/messages"
|
||||||
|
}
|
||||||
|
built := BuildVerifyRequest("minimax", protocol, auth, base, "test-key", model)
|
||||||
|
if built.URL != base+endpoint {
|
||||||
|
t.Fatalf("URL = %s", built.URL)
|
||||||
|
}
|
||||||
|
var captured map[string]interface{}
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost || r.URL.Path != strings.TrimPrefix(base, region)+endpoint {
|
||||||
|
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||||
|
}
|
||||||
|
if protocol == "anthropic-messages" && auth == AuthModeXAPIKey {
|
||||||
|
if r.Header.Get("x-api-key") != "test-key" || r.Header.Get("Authorization") != "" {
|
||||||
|
t.Error("incorrect API key authentication")
|
||||||
|
}
|
||||||
|
} else if r.Header.Get("Authorization") != "Bearer test-key" {
|
||||||
|
t.Error("incorrect bearer authentication")
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&captured); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
if err := VerifyAccount("minimax", protocol, auth, server.URL+strings.TrimPrefix(base, region), "test-key", model); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if captured["model"] != model || captured["stream"] != false || captured["max_tokens"] != float64(defaultVerifyMaxTokens) {
|
||||||
|
t.Fatalf("unexpected serialized body: %#v", captured)
|
||||||
|
}
|
||||||
|
if model == "MiniMax-M3.1-Flash" {
|
||||||
|
if protocol == "anthropic-messages" {
|
||||||
|
if !reflect.DeepEqual(captured["thinking"], map[string]interface{}{"type": "adaptive"}) || !reflect.DeepEqual(captured["output_config"], map[string]interface{}{"effort": "max"}) || captured["reasoning_effort"] != nil {
|
||||||
|
t.Fatalf("invalid Anthropic Flash reasoning: %#v", captured)
|
||||||
|
}
|
||||||
|
} else if captured["reasoning_effort"] != "max" || captured["thinking"] != nil || captured["output_config"] != nil {
|
||||||
|
t.Fatalf("invalid OpenAI Flash reasoning: %#v", captured)
|
||||||
|
}
|
||||||
|
} else if captured["reasoning_effort"] != nil || captured["thinking"] != nil || captured["output_config"] != nil {
|
||||||
|
t.Fatalf("changed legacy/custom reasoning: %#v", captured)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiniMaxExplicitGatewayAndCustomModels(t *testing.T) {
|
||||||
|
for _, provider := range []string{"bailian-coding-plan", "custom", "openrouter"} {
|
||||||
|
for _, protocol := range []string{"openai-completions", "anthropic-messages"} {
|
||||||
|
for _, model := range []string{"MiniMax-M2.5", "MiniMax-M3.1-Flash", "my-custom-model"} {
|
||||||
|
request := BuildVerifyRequest(provider, protocol, AuthModeBearer, "https://example.test/v1", "test-key", model)
|
||||||
|
var body map[string]interface{}
|
||||||
|
if err := json.Unmarshal(request.Body, &body); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if body["model"] != model || body["reasoning_effort"] != nil || body["output_config"] != nil || body["thinking"] != nil {
|
||||||
|
t.Fatalf("changed explicit %s request: %#v", provider, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -118,10 +118,15 @@ func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model stri
|
|||||||
headers["x-api-key"] = apiKey
|
headers["x-api-key"] = apiKey
|
||||||
}
|
}
|
||||||
headers["anthropic-version"] = "2023-06-01"
|
headers["anthropic-version"] = "2023-06-01"
|
||||||
request.Body = mustJSON(map[string]interface{}{
|
body := map[string]interface{}{
|
||||||
"model": model, "max_tokens": defaultVerifyMaxTokens, "stream": false,
|
"model": model, "max_tokens": defaultVerifyMaxTokens, "stream": false,
|
||||||
"messages": []map[string]interface{}{{"role": "user", "content": []map[string]string{{"type": "text", "text": "test"}}}},
|
"messages": []map[string]interface{}{{"role": "user", "content": []map[string]string{{"type": "text", "text": "test"}}}},
|
||||||
})
|
}
|
||||||
|
if provider == "minimax" && model == "MiniMax-M3.1-Flash" {
|
||||||
|
body["thinking"] = map[string]string{"type": "adaptive"}
|
||||||
|
body["output_config"] = map[string]string{"effort": "max"}
|
||||||
|
}
|
||||||
|
request.Body = mustJSON(body)
|
||||||
case "openai-responses":
|
case "openai-responses":
|
||||||
request.URL = baseURL + "/responses"
|
request.URL = baseURL + "/responses"
|
||||||
headers["Authorization"] = "Bearer " + apiKey
|
headers["Authorization"] = "Bearer " + apiKey
|
||||||
@@ -131,9 +136,13 @@ func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model stri
|
|||||||
if (provider != "ollama" && provider != "llmman") || strings.TrimSpace(apiKey) != "" {
|
if (provider != "ollama" && provider != "llmman") || strings.TrimSpace(apiKey) != "" {
|
||||||
headers["Authorization"] = "Bearer " + apiKey
|
headers["Authorization"] = "Bearer " + apiKey
|
||||||
}
|
}
|
||||||
request.Body = mustJSON(map[string]interface{}{
|
body := map[string]interface{}{
|
||||||
"model": model, "messages": []map[string]string{{"role": "user", "content": "test"}}, "max_tokens": defaultVerifyMaxTokens, "stream": false,
|
"model": model, "messages": []map[string]string{{"role": "user", "content": "test"}}, "max_tokens": defaultVerifyMaxTokens, "stream": false,
|
||||||
})
|
}
|
||||||
|
if provider == "minimax" && model == "MiniMax-M3.1-Flash" {
|
||||||
|
body["reasoning_effort"] = "max"
|
||||||
|
}
|
||||||
|
request.Body = mustJSON(body)
|
||||||
}
|
}
|
||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ type IAgentAccountRepo interface {
|
|||||||
DeleteByID(id uint) error
|
DeleteByID(id uint) error
|
||||||
List(opts ...DBOption) ([]model.AgentAccount, error)
|
List(opts ...DBOption) ([]model.AgentAccount, error)
|
||||||
CountTextByProviders(providers []string) (map[string]int64, error)
|
CountTextByProviders(providers []string) (map[string]int64, error)
|
||||||
|
SaveLocal(account *model.AgentAccount, models []model.AgentAccountModel) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIAgentAccountRepo() IAgentAccountRepo {
|
func NewIAgentAccountRepo() IAgentAccountRepo {
|
||||||
@@ -97,6 +99,25 @@ func (a AgentAccountRepo) CountTextByProviders(providers []string) (map[string]i
|
|||||||
return counts, nil
|
return counts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a AgentAccountRepo) SaveLocal(account *model.AgentAccount, models []model.AgentAccountModel) error {
|
||||||
|
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err := tx.Save(account).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.Where("account_id = ?", account.ID).Delete(&model.AgentAccountModel{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(models) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for i := range models {
|
||||||
|
models[i].AccountID = account.ID
|
||||||
|
models[i].SortOrder = i + 1
|
||||||
|
}
|
||||||
|
return tx.Create(&models).Error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeProviders(providers []string) []string {
|
func normalizeProviders(providers []string) []string {
|
||||||
seen := make(map[string]struct{}, len(providers))
|
seen := make(map[string]struct{}, len(providers))
|
||||||
result := make([]string, 0, len(providers))
|
result := make([]string, 0, len(providers))
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
)
|
)
|
||||||
@@ -14,6 +16,7 @@ type IAiRepo interface {
|
|||||||
Create(cronjob *model.OllamaModel) error
|
Create(cronjob *model.OllamaModel) error
|
||||||
Update(id uint, vars map[string]interface{}) error
|
Update(id uint, vars map[string]interface{}) error
|
||||||
Delete(opts ...DBOption) error
|
Delete(opts ...DBOption) error
|
||||||
|
DeleteAll(ctx context.Context) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIAiRepo() IAiRepo {
|
func NewIAiRepo() IAiRepo {
|
||||||
@@ -67,3 +70,7 @@ func (u *AiRepo) Delete(opts ...DBOption) error {
|
|||||||
}
|
}
|
||||||
return db.Delete(&model.OllamaModel{}).Error
|
return db.Delete(&model.OllamaModel{}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *AiRepo) DeleteAll(ctx context.Context) error {
|
||||||
|
return getTx(ctx).Where("1 = 1").Delete(&model.OllamaModel{}).Error
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
@@ -30,6 +31,7 @@ type IAppRepo interface {
|
|||||||
BatchCreate(ctx context.Context, apps []model.App) error
|
BatchCreate(ctx context.Context, apps []model.App) error
|
||||||
Create(ctx context.Context, app *model.App) error
|
Create(ctx context.Context, app *model.App) error
|
||||||
Save(ctx context.Context, app *model.App) error
|
Save(ctx context.Context, app *model.App) error
|
||||||
|
UpdateIcon(id uint, icon string) error
|
||||||
BatchDelete(ctx context.Context, apps []model.App) error
|
BatchDelete(ctx context.Context, apps []model.App) error
|
||||||
DeleteByIDs(ctx context.Context, ids []uint) error
|
DeleteByIDs(ctx context.Context, ids []uint) error
|
||||||
DeleteBy(opts ...DBOption) error
|
DeleteBy(opts ...DBOption) error
|
||||||
@@ -41,6 +43,10 @@ func NewIAppRepo() IAppRepo {
|
|||||||
return &AppRepo{}
|
return &AppRepo{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a AppRepo) UpdateIcon(id uint, icon string) error {
|
||||||
|
return getDb(WithByID(id), a.WithResource(constant.AppResourceRemote)).Model(&model.App{}).UpdateColumn("icon", icon).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (a AppRepo) WithByLikeName(name string) DBOption {
|
func (a AppRepo) WithByLikeName(name string) DBOption {
|
||||||
return func(g *gorm.DB) *gorm.DB {
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
if len(name) == 0 {
|
if len(name) == 0 {
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ type IAppDetailRepo interface {
|
|||||||
WithIgnored() DBOption
|
WithIgnored() DBOption
|
||||||
GetFirst(opts ...DBOption) (model.AppDetail, error)
|
GetFirst(opts ...DBOption) (model.AppDetail, error)
|
||||||
Update(ctx context.Context, detail model.AppDetail) error
|
Update(ctx context.Context, detail model.AppDetail) error
|
||||||
|
CacheCompose(detail model.AppDetail, compose string) (bool, error)
|
||||||
|
MarkDownloaded(detail model.AppDetail) (bool, error)
|
||||||
BatchCreate(ctx context.Context, details []model.AppDetail) error
|
BatchCreate(ctx context.Context, details []model.AppDetail) error
|
||||||
DeleteByAppIds(ctx context.Context, appIds []uint) error
|
DeleteByAppIds(ctx context.Context, appIds []uint) error
|
||||||
DeleteByIDs(ctx context.Context, appIds []uint) error
|
DeleteByIDs(ctx context.Context, appIds []uint) error
|
||||||
@@ -57,6 +59,20 @@ func (a AppDetailRepo) Update(ctx context.Context, detail model.AppDetail) error
|
|||||||
return getTx(ctx).Save(&detail).Error
|
return getTx(ctx).Save(&detail).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a AppDetailRepo) CacheCompose(detail model.AppDetail, compose string) (bool, error) {
|
||||||
|
result := getDb().Model(&model.AppDetail{}).
|
||||||
|
Where("id = ? AND last_modified = ? AND params = ? AND download_url = ?", detail.ID, detail.LastModified, detail.Params, detail.DownloadUrl).
|
||||||
|
UpdateColumn("docker_compose", compose)
|
||||||
|
return result.RowsAffected > 0, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AppDetailRepo) MarkDownloaded(detail model.AppDetail) (bool, error) {
|
||||||
|
result := getDb().Model(&model.AppDetail{}).
|
||||||
|
Where("id = ? AND last_modified = ? AND params = ? AND download_url = ?", detail.ID, detail.LastModified, detail.Params, detail.DownloadUrl).
|
||||||
|
UpdateColumn("update", false)
|
||||||
|
return result.RowsAffected > 0, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
func (a AppDetailRepo) BatchCreate(ctx context.Context, details []model.AppDetail) error {
|
func (a AppDetailRepo) BatchCreate(ctx context.Context, details []model.AppDetail) error {
|
||||||
return getTx(ctx).Model(&model.AppDetail{}).Create(&details).Error
|
return getTx(ctx).Model(&model.AppDetail{}).Create(&details).Error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ type IAppInstallRepo interface {
|
|||||||
LoadBaseInfo(key string, name string) (*RootInfo, error)
|
LoadBaseInfo(key string, name string) (*RootInfo, error)
|
||||||
LoadInstallAppByKeyAndName(key string, name string) (*model.AppInstall, error)
|
LoadInstallAppByKeyAndName(key string, name string) (*model.AppInstall, error)
|
||||||
GetFirstByCtx(ctx context.Context, opts ...DBOption) (model.AppInstall, error)
|
GetFirstByCtx(ctx context.Context, opts ...DBOption) (model.AppInstall, error)
|
||||||
|
GetSnapshot(ctx context.Context, id uint) (model.AppInstall, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIAppInstallRepo() IAppInstallRepo {
|
func NewIAppInstallRepo() IAppInstallRepo {
|
||||||
@@ -131,6 +132,12 @@ func (a *AppInstallRepo) GetFirstByCtx(ctx context.Context, opts ...DBOption) (m
|
|||||||
return install, err
|
return install, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *AppInstallRepo) GetSnapshot(ctx context.Context, id uint) (model.AppInstall, error) {
|
||||||
|
var install model.AppInstall
|
||||||
|
err := getTx(ctx).WithContext(ctx).Preload("App").Where("id = ?", id).First(&install).Error
|
||||||
|
return install, err
|
||||||
|
}
|
||||||
|
|
||||||
func (a *AppInstallRepo) Create(ctx context.Context, install *model.AppInstall) error {
|
func (a *AppInstallRepo) Create(ctx context.Context, install *model.AppInstall) error {
|
||||||
db := getTx(ctx).Model(&model.AppInstall{})
|
db := getTx(ctx).Model(&model.AppInstall{})
|
||||||
return db.Omit(clause.Associations).Create(&install).Error
|
return db.Omit(clause.Associations).Create(&install).Error
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package repo
|
|||||||
import (
|
import (
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LauncherRepo struct{}
|
type LauncherRepo struct{}
|
||||||
@@ -17,6 +18,11 @@ type ILauncherRepo interface {
|
|||||||
GetQuickJump(opts ...DBOption) (model.QuickJump, error)
|
GetQuickJump(opts ...DBOption) (model.QuickJump, error)
|
||||||
ListQuickJump(withAll bool) []model.QuickJump
|
ListQuickJump(withAll bool) []model.QuickJump
|
||||||
UpdateQuicks(quicks []model.QuickJump) error
|
UpdateQuicks(quicks []model.QuickJump) error
|
||||||
|
ListQuickDirectories() ([]model.QuickDirectory, error)
|
||||||
|
CountQuickDirectories() (int64, error)
|
||||||
|
CreateQuickDirectory(item *model.QuickDirectory) error
|
||||||
|
UpdateQuickDirectory(item *model.QuickDirectory) error
|
||||||
|
DeleteQuickDirectory(id uint, legacy bool) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewILauncherRepo() ILauncherRepo {
|
func NewILauncherRepo() ILauncherRepo {
|
||||||
@@ -95,13 +101,73 @@ func (u *LauncherRepo) UpdateQuicks(quicks []model.QuickJump) error {
|
|||||||
for _, item := range quicks {
|
for _, item := range quicks {
|
||||||
if err := tx.Model(&model.QuickJump{}).Where("id = ?", item.ID).Updates(map[string]interface{}{
|
if err := tx.Model(&model.QuickJump{}).Where("id = ?", item.ID).Updates(map[string]interface{}{
|
||||||
"is_show": item.IsShow,
|
"is_show": item.IsShow,
|
||||||
"detail": item.Detail,
|
|
||||||
"alias": item.Alias,
|
|
||||||
}).Error; err != nil {
|
}).Error; err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tx.Commit()
|
return tx.Commit().Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy entries remain in the overview configuration until explicitly deleted.
|
||||||
|
func legacyQuickDirectories(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Model(&model.QuickJump{}).Where("name = ? AND is_show = ? AND detail <> ?", "File", true, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *LauncherRepo) ListQuickDirectories() ([]model.QuickDirectory, error) {
|
||||||
|
items := make([]model.QuickDirectory, 0)
|
||||||
|
if err := global.DB.Order("id ASC").Find(&items).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var quicks []model.QuickJump
|
||||||
|
if err := legacyQuickDirectories(global.DB).Order("id ASC").Find(&quicks).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, quick := range quicks {
|
||||||
|
items = append(items, model.QuickDirectory{BaseModel: quick.BaseModel, Path: quick.Detail, Alias: quick.Alias, Legacy: true})
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
func (u *LauncherRepo) CountQuickDirectories() (int64, error) {
|
||||||
|
var count, legacyCount int64
|
||||||
|
if err := global.DB.Model(&model.QuickDirectory{}).Count(&count).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if err := legacyQuickDirectories(global.DB).Count(&legacyCount).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return count + legacyCount, nil
|
||||||
|
}
|
||||||
|
func (u *LauncherRepo) CreateQuickDirectory(item *model.QuickDirectory) error {
|
||||||
|
return global.DB.Create(item).Error
|
||||||
|
}
|
||||||
|
func (u *LauncherRepo) UpdateQuickDirectory(item *model.QuickDirectory) error {
|
||||||
|
var result *gorm.DB
|
||||||
|
if item.Legacy {
|
||||||
|
result = legacyQuickDirectories(global.DB).Where("id = ?", item.ID).Updates(map[string]interface{}{"detail": item.Path, "alias": item.Alias})
|
||||||
|
} else {
|
||||||
|
result = global.DB.Model(&model.QuickDirectory{}).Where("id = ?", item.ID).Updates(map[string]interface{}{"path": item.Path, "alias": item.Alias})
|
||||||
|
}
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (u *LauncherRepo) DeleteQuickDirectory(id uint, legacy bool) error {
|
||||||
|
var result *gorm.DB
|
||||||
|
if legacy {
|
||||||
|
result = legacyQuickDirectories(global.DB).Where("id = ?", id).Updates(map[string]interface{}{"detail": nil, "alias": nil})
|
||||||
|
} else {
|
||||||
|
result = global.DB.Delete(&model.QuickDirectory{}, id)
|
||||||
|
}
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
|
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -20,6 +21,8 @@ type IDatabaseRepo interface {
|
|||||||
Update(id uint, vars map[string]interface{}) error
|
Update(id uint, vars map[string]interface{}) error
|
||||||
Delete(ctx context.Context, opts ...DBOption) error
|
Delete(ctx context.Context, opts ...DBOption) error
|
||||||
Get(opts ...DBOption) (model.Database, error)
|
Get(opts ...DBOption) (model.Database, error)
|
||||||
|
ListInstalledLocalMysql(ctx context.Context) ([]InstalledLocalMysql, error)
|
||||||
|
GetInstalledLocalMysql(ctx context.Context, name string) (InstalledLocalMysql, error)
|
||||||
WithoutByFrom(from string) DBOption
|
WithoutByFrom(from string) DBOption
|
||||||
WithAppInstallID(appInstallID uint) DBOption
|
WithAppInstallID(appInstallID uint) DBOption
|
||||||
WithTypeList(dbType string) DBOption
|
WithTypeList(dbType string) DBOption
|
||||||
@@ -29,6 +32,32 @@ func NewIDatabaseRepo() IDatabaseRepo {
|
|||||||
return &DatabaseRepo{}
|
return &DatabaseRepo{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InstalledLocalMysql struct {
|
||||||
|
ID uint
|
||||||
|
Type, From, Database string
|
||||||
|
ContainerName, Version, Env string
|
||||||
|
}
|
||||||
|
|
||||||
|
func installedLocalMysqlQuery(ctx context.Context) *gorm.DB {
|
||||||
|
return getTx(ctx).WithContext(ctx).Table("databases AS d").
|
||||||
|
Joins("JOIN app_installs AS ai ON ai.id = d.app_install_id AND ai.name = d.name").
|
||||||
|
Joins("JOIN apps AS a ON a.id = ai.app_id").
|
||||||
|
Where("d.type = ? AND d.\"from\" = ? AND a.key = ?", constant.AppMysql, "local", constant.AppMysql).
|
||||||
|
Where("ai.status <> ? AND ai.container_name <> ?", constant.StatusDeleted, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DatabaseRepo) ListInstalledLocalMysql(ctx context.Context) ([]InstalledLocalMysql, error) {
|
||||||
|
result := make([]InstalledLocalMysql, 0)
|
||||||
|
err := installedLocalMysqlQuery(ctx).Select("d.id, d.type, d.\"from\", d.name AS database, ai.version").Order("d.id").Scan(&result).Error
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DatabaseRepo) GetInstalledLocalMysql(ctx context.Context, name string) (InstalledLocalMysql, error) {
|
||||||
|
var result InstalledLocalMysql
|
||||||
|
err := installedLocalMysqlQuery(ctx).Where("d.name = ?", name).Select("d.id, ai.container_name, ai.version, ai.env").Take(&result).Error
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
func (d *DatabaseRepo) Get(opts ...DBOption) (model.Database, error) {
|
func (d *DatabaseRepo) Get(opts ...DBOption) (model.Database, error) {
|
||||||
var database model.Database
|
var database model.Database
|
||||||
db := global.DB
|
db := global.DB
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type IMysqlRepo interface {
|
|||||||
List(opts ...DBOption) ([]model.DatabaseMysql, error)
|
List(opts ...DBOption) ([]model.DatabaseMysql, error)
|
||||||
Page(limit, offset int, opts ...DBOption) (int64, []model.DatabaseMysql, error)
|
Page(limit, offset int, opts ...DBOption) (int64, []model.DatabaseMysql, error)
|
||||||
Create(ctx context.Context, mysql *model.DatabaseMysql) error
|
Create(ctx context.Context, mysql *model.DatabaseMysql) error
|
||||||
|
CreateMetadata(ctx context.Context, mysql *model.DatabaseMysql) error
|
||||||
Delete(ctx context.Context, opts ...DBOption) error
|
Delete(ctx context.Context, opts ...DBOption) error
|
||||||
Update(id uint, vars map[string]interface{}) error
|
Update(id uint, vars map[string]interface{}) error
|
||||||
DeleteLocal(ctx context.Context) error
|
DeleteLocal(ctx context.Context) error
|
||||||
@@ -94,6 +95,10 @@ func (u *MysqlRepo) Create(ctx context.Context, mysql *model.DatabaseMysql) erro
|
|||||||
return getTx(ctx).Create(mysql).Error
|
return getTx(ctx).Create(mysql).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *MysqlRepo) CreateMetadata(ctx context.Context, mysql *model.DatabaseMysql) error {
|
||||||
|
return getTx(ctx).WithContext(ctx).Create(mysql).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (u *MysqlRepo) Delete(ctx context.Context, opts ...DBOption) error {
|
func (u *MysqlRepo) Delete(ctx context.Context, opts ...DBOption) error {
|
||||||
return getTx(ctx, opts...).Delete(&model.DatabaseMysql{}).Error
|
return getTx(ctx, opts...).Delete(&model.DatabaseMysql{}).Error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ type DatabaseUserRepo struct{}
|
|||||||
|
|
||||||
type IDatabaseUserRepo interface {
|
type IDatabaseUserRepo interface {
|
||||||
Get(opts ...DBOption) (model.DatabaseUser, error)
|
Get(opts ...DBOption) (model.DatabaseUser, error)
|
||||||
|
GetEncryptedWithContext(ctx context.Context, opts ...DBOption) (model.DatabaseUser, error)
|
||||||
List(opts ...DBOption) ([]model.DatabaseUser, error)
|
List(opts ...DBOption) ([]model.DatabaseUser, error)
|
||||||
Save(user *model.DatabaseUser) error
|
Save(user *model.DatabaseUser) error
|
||||||
|
SaveEncryptedWithContext(ctx context.Context, user *model.DatabaseUser) error
|
||||||
Delete(opts ...DBOption) error
|
Delete(opts ...DBOption) error
|
||||||
DeleteBy(ctx context.Context, opts ...DBOption) error
|
DeleteBy(ctx context.Context, opts ...DBOption) error
|
||||||
Update(vars map[string]interface{}, opts ...DBOption) error
|
Update(vars map[string]interface{}, opts ...DBOption) error
|
||||||
@@ -45,6 +47,13 @@ func (u *DatabaseUserRepo) Get(opts ...DBOption) (model.DatabaseUser, error) {
|
|||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetEncryptedWithContext leaves credential transformation to the caller.
|
||||||
|
func (u *DatabaseUserRepo) GetEncryptedWithContext(ctx context.Context, opts ...DBOption) (model.DatabaseUser, error) {
|
||||||
|
var user model.DatabaseUser
|
||||||
|
err := getTx(ctx, opts...).WithContext(ctx).First(&user).Error
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
|
||||||
func (u *DatabaseUserRepo) List(opts ...DBOption) ([]model.DatabaseUser, error) {
|
func (u *DatabaseUserRepo) List(opts ...DBOption) ([]model.DatabaseUser, error) {
|
||||||
var users []model.DatabaseUser
|
var users []model.DatabaseUser
|
||||||
db := global.DB.Model(&model.DatabaseUser{})
|
db := global.DB.Model(&model.DatabaseUser{})
|
||||||
@@ -75,6 +84,10 @@ func (u *DatabaseUserRepo) Save(user *model.DatabaseUser) error {
|
|||||||
return global.DB.Save(user).Error
|
return global.DB.Save(user).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *DatabaseUserRepo) SaveEncryptedWithContext(ctx context.Context, user *model.DatabaseUser) error {
|
||||||
|
return getTx(ctx).WithContext(ctx).Save(user).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (u *DatabaseUserRepo) Delete(opts ...DBOption) error {
|
func (u *DatabaseUserRepo) Delete(opts ...DBOption) error {
|
||||||
db := global.DB
|
db := global.DB
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type IDatabaseUserGrantRepo interface {
|
|||||||
Get(opts ...DBOption) (model.DatabaseUserGrant, error)
|
Get(opts ...DBOption) (model.DatabaseUserGrant, error)
|
||||||
List(opts ...DBOption) ([]model.DatabaseUserGrant, error)
|
List(opts ...DBOption) ([]model.DatabaseUserGrant, error)
|
||||||
Save(grant *model.DatabaseUserGrant) error
|
Save(grant *model.DatabaseUserGrant) error
|
||||||
|
CreateIfAbsent(ctx context.Context, grant *model.DatabaseUserGrant) error
|
||||||
Replace(dbType, database string, grants []model.DatabaseUserGrant) error
|
Replace(dbType, database string, grants []model.DatabaseUserGrant) error
|
||||||
Delete(opts ...DBOption) error
|
Delete(opts ...DBOption) error
|
||||||
DeleteBy(ctx context.Context, opts ...DBOption) error
|
DeleteBy(ctx context.Context, opts ...DBOption) error
|
||||||
@@ -52,6 +53,12 @@ func (u *DatabaseUserGrantRepo) Save(grant *model.DatabaseUserGrant) error {
|
|||||||
return global.DB.Save(grant).Error
|
return global.DB.Save(grant).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *DatabaseUserGrantRepo) CreateIfAbsent(ctx context.Context, grant *model.DatabaseUserGrant) error {
|
||||||
|
return getTx(ctx).WithContext(ctx).
|
||||||
|
Where("type = ? AND database = ? AND db_name = ? AND username = ? AND host = ?", grant.Type, grant.Database, grant.DBName, grant.Username, grant.Host).
|
||||||
|
FirstOrCreate(grant).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (u *DatabaseUserGrantRepo) Replace(dbType, database string, grants []model.DatabaseUserGrant) error {
|
func (u *DatabaseUserGrantRepo) Replace(dbType, database string, grants []model.DatabaseUserGrant) error {
|
||||||
return global.DB.Transaction(func(tx *gorm.DB) error {
|
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
if err := tx.Where("`type` = ? AND database = ?", dbType, database).Delete(&model.DatabaseUserGrant{}).Error; err != nil {
|
if err := tx.Where("`type` = ? AND database = ?", dbType, database).Delete(&model.DatabaseUserGrant{}).Error; err != nil {
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
package repo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
)
|
|
||||||
|
|
||||||
type IDockerPortGuardRepo interface {
|
|
||||||
ListManaged(context.Context) ([]model.DockerPortGuardPolicy, error)
|
|
||||||
ListRuntimeReadOnly(context.Context) ([]model.DockerPortGuardPolicy, error)
|
|
||||||
DeleteBatch(context.Context, []string) error
|
|
||||||
UpsertBatch(context.Context, []model.DockerPortGuardPolicy) error
|
|
||||||
ReplaceRuntimeReadOnly(context.Context, []model.DockerPortGuardPolicy) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type DockerPortGuardRepo struct{}
|
|
||||||
|
|
||||||
func NewIDockerPortGuardRepo() IDockerPortGuardRepo { return &DockerPortGuardRepo{} }
|
|
||||||
|
|
||||||
func (r *DockerPortGuardRepo) ListManaged(ctx context.Context) ([]model.DockerPortGuardPolicy, error) {
|
|
||||||
var policies []model.DockerPortGuardPolicy
|
|
||||||
err := global.DB.WithContext(ctx).
|
|
||||||
Where("read_only = ?", false).
|
|
||||||
Order("family, host_ip, host_port, protocol").
|
|
||||||
Find(&policies).Error
|
|
||||||
return policies, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *DockerPortGuardRepo) ListRuntimeReadOnly(ctx context.Context) ([]model.DockerPortGuardPolicy, error) {
|
|
||||||
var policies []model.DockerPortGuardPolicy
|
|
||||||
err := global.DB.WithContext(ctx).
|
|
||||||
Where("read_only = ?", true).
|
|
||||||
Order("family, sequence, host_ip, host_port, protocol").
|
|
||||||
Find(&policies).Error
|
|
||||||
return policies, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *DockerPortGuardRepo) DeleteBatch(ctx context.Context, uuids []string) error {
|
|
||||||
return global.DB.WithContext(ctx).
|
|
||||||
Where("read_only = ? AND uuid IN ?", false, uuids).
|
|
||||||
Delete(&model.DockerPortGuardPolicy{}).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *DockerPortGuardRepo) UpsertBatch(ctx context.Context, policies []model.DockerPortGuardPolicy) error {
|
|
||||||
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
||||||
for i := range policies {
|
|
||||||
policies[i].ReadOnly = false
|
|
||||||
if err := tx.Clauses(clause.OnConflict{
|
|
||||||
Columns: []clause.Column{{Name: "read_only"}, {Name: "family"}, {Name: "host_ip"}, {Name: "host_port"}, {Name: "protocol"}},
|
|
||||||
DoUpdates: clause.AssignmentColumns([]string{"mode", "sources", "description", "updated_at"}),
|
|
||||||
}).Create(&policies[i]).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *DockerPortGuardRepo) ReplaceRuntimeReadOnly(ctx context.Context, policies []model.DockerPortGuardPolicy) error {
|
|
||||||
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
||||||
if err := tx.Where("read_only = ?", true).
|
|
||||||
Delete(&model.DockerPortGuardPolicy{}).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(policies) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for i := range policies {
|
|
||||||
policies[i].ReadOnly = true
|
|
||||||
}
|
|
||||||
return tx.Create(&policies).Error
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
package repo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrFirewallRuleRevisionConflict = errors.New("firewall rule revision conflict")
|
|
||||||
ErrFirewallPersistenceInvalid = errors.New("invalid firewall persistence record")
|
|
||||||
)
|
|
||||||
|
|
||||||
type IFirewallRuleRepo interface {
|
|
||||||
Create(context.Context, *model.FirewallRule) error
|
|
||||||
GetByUUID(context.Context, string) (model.FirewallRule, error)
|
|
||||||
List(context.Context, ...DBOption) ([]model.FirewallRule, error)
|
|
||||||
UpdateWithRevision(context.Context, string, uint, map[string]interface{}) error
|
|
||||||
DeleteWithRevision(context.Context, string, uint) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type FirewallRuleRepo struct {
|
|
||||||
db *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithFirewallRuleSource(kind, id string) DBOption {
|
|
||||||
return func(db *gorm.DB) *gorm.DB {
|
|
||||||
return db.Where("owner = ?", model.FirewallRuleOwner(kind, id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewIFirewallRuleRepo() IFirewallRuleRepo {
|
|
||||||
return &FirewallRuleRepo{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewFirewallRuleRepo(db *gorm.DB) *FirewallRuleRepo {
|
|
||||||
return &FirewallRuleRepo{db: db}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FirewallRuleRepo) Create(ctx context.Context, rule *model.FirewallRule) error {
|
|
||||||
if err := prepareFirewallRule(rule); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return r.dbFor(ctx).Create(rule).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FirewallRuleRepo) GetByUUID(ctx context.Context, ruleUUID string) (model.FirewallRule, error) {
|
|
||||||
var rule model.FirewallRule
|
|
||||||
err := r.dbFor(ctx).Where("uuid = ?", ruleUUID).First(&rule).Error
|
|
||||||
return rule, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FirewallRuleRepo) List(ctx context.Context, opts ...DBOption) ([]model.FirewallRule, error) {
|
|
||||||
var rules []model.FirewallRule
|
|
||||||
db := r.dbFor(ctx).Model(&model.FirewallRule{})
|
|
||||||
for _, opt := range opts {
|
|
||||||
db = opt(db)
|
|
||||||
}
|
|
||||||
return rules, db.Find(&rules).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FirewallRuleRepo) UpdateWithRevision(ctx context.Context, ruleUUID string, expectedRevision uint, updates map[string]interface{}) error {
|
|
||||||
updates = sanitizeRuleUpdates(updates)
|
|
||||||
updates["revision"] = gorm.Expr("revision + 1")
|
|
||||||
result := r.dbFor(ctx).Model(&model.FirewallRule{}).
|
|
||||||
Where("uuid = ? AND revision = ?", ruleUUID, expectedRevision).
|
|
||||||
Updates(updates)
|
|
||||||
if result.Error != nil {
|
|
||||||
return result.Error
|
|
||||||
}
|
|
||||||
if result.RowsAffected == 0 {
|
|
||||||
return ErrFirewallRuleRevisionConflict
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FirewallRuleRepo) DeleteWithRevision(ctx context.Context, ruleUUID string, expectedRevision uint) error {
|
|
||||||
result := r.dbFor(ctx).
|
|
||||||
Where("uuid = ? AND revision = ?", ruleUUID, expectedRevision).
|
|
||||||
Delete(&model.FirewallRule{})
|
|
||||||
if result.Error != nil {
|
|
||||||
return result.Error
|
|
||||||
}
|
|
||||||
if result.RowsAffected == 0 {
|
|
||||||
return ErrFirewallRuleRevisionConflict
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FirewallRuleRepo) dbFor(ctx context.Context) *gorm.DB {
|
|
||||||
return firewallDB(ctx, r.db)
|
|
||||||
}
|
|
||||||
|
|
||||||
func firewallDB(ctx context.Context, fallback *gorm.DB) *gorm.DB {
|
|
||||||
if ctx == nil {
|
|
||||||
ctx = context.Background()
|
|
||||||
}
|
|
||||||
if tx, ok := ctx.Value(constant.DB).(*gorm.DB); ok && tx != nil {
|
|
||||||
return tx.WithContext(ctx)
|
|
||||||
}
|
|
||||||
if fallback == nil {
|
|
||||||
fallback = global.DB
|
|
||||||
}
|
|
||||||
return fallback.WithContext(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func prepareFirewallRule(rule *model.FirewallRule) error {
|
|
||||||
if rule == nil {
|
|
||||||
return fmt.Errorf("%w: rule is nil", ErrFirewallPersistenceInvalid)
|
|
||||||
}
|
|
||||||
if rule.Family == "" || rule.Protocol == "" || rule.Action == "" {
|
|
||||||
return fmt.Errorf("%w: atomic rule identity fields are required", ErrFirewallPersistenceInvalid)
|
|
||||||
}
|
|
||||||
if rule.UUID == "" {
|
|
||||||
rule.UUID = uuid.NewString()
|
|
||||||
}
|
|
||||||
if rule.Revision == 0 {
|
|
||||||
rule.Revision = 1
|
|
||||||
}
|
|
||||||
if rule.Origin == "" {
|
|
||||||
rule.Origin = constant.FirewallRuleOriginCreated
|
|
||||||
}
|
|
||||||
if rule.Owner == "" {
|
|
||||||
rule.Owner = constant.FirewallRuleSourceUser
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func sanitizeRuleUpdates(updates map[string]interface{}) map[string]interface{} {
|
|
||||||
result := cloneUpdates(updates)
|
|
||||||
delete(result, "id")
|
|
||||||
delete(result, "uuid")
|
|
||||||
delete(result, "revision")
|
|
||||||
delete(result, "created_at")
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func cloneUpdates(updates map[string]interface{}) map[string]interface{} {
|
|
||||||
result := make(map[string]interface{}, len(updates)+1)
|
|
||||||
for key, value := range updates {
|
|
||||||
result[key] = value
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package repo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type IForwardingRuleRepo interface {
|
|
||||||
List(context.Context) ([]model.ForwardingRule, error)
|
|
||||||
ReplaceAll(context.Context, []model.ForwardingRule) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type ForwardingRuleRepo struct{}
|
|
||||||
|
|
||||||
func NewIForwardingRuleRepo() IForwardingRuleRepo { return &ForwardingRuleRepo{} }
|
|
||||||
|
|
||||||
func (r *ForwardingRuleRepo) List(ctx context.Context) ([]model.ForwardingRule, error) {
|
|
||||||
var rules []model.ForwardingRule
|
|
||||||
err := global.DB.WithContext(ctx).Order("id ASC").Find(&rules).Error
|
|
||||||
return rules, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ForwardingRuleRepo) ReplaceAll(ctx context.Context, rules []model.ForwardingRule) error {
|
|
||||||
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
||||||
if err := tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&model.ForwardingRule{}).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(rules) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return tx.Create(&rules).Error
|
|
||||||
})
|
|
||||||
}
|
|
||||||
+103
-13
@@ -1,6 +1,8 @@
|
|||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
@@ -10,11 +12,25 @@ import (
|
|||||||
|
|
||||||
type MonitorRepo struct{}
|
type MonitorRepo struct{}
|
||||||
|
|
||||||
|
type GPUHistoryPoint struct {
|
||||||
|
model.MonitorGPU
|
||||||
|
Bucket int64
|
||||||
|
PowerPercent *float64
|
||||||
|
MemoryPercent *float64
|
||||||
|
ProcessCount *float64
|
||||||
|
}
|
||||||
|
|
||||||
type IMonitorRepo interface {
|
type IMonitorRepo interface {
|
||||||
|
CleanHost() error
|
||||||
|
CleanGPU() error
|
||||||
GetBase(opts ...DBOption) ([]model.MonitorBase, error)
|
GetBase(opts ...DBOption) ([]model.MonitorBase, error)
|
||||||
GetGPU(opts ...DBOption) ([]model.MonitorGPU, error)
|
CountGPU(opts ...DBOption) (int64, error)
|
||||||
|
GetGPUHistory(start time.Time, bucketSeconds int64, aggregation string, opts ...DBOption) ([]GPUHistoryPoint, error)
|
||||||
|
GetGPUDevices() ([]model.MonitorGPU, error)
|
||||||
GetIO(opts ...DBOption) ([]model.MonitorIO, error)
|
GetIO(opts ...DBOption) ([]model.MonitorIO, error)
|
||||||
GetNetwork(opts ...DBOption) ([]model.MonitorNetwork, error)
|
GetNetwork(opts ...DBOption) ([]model.MonitorNetwork, error)
|
||||||
|
GetIONames() ([]string, error)
|
||||||
|
GetNetworkNames() ([]string, error)
|
||||||
|
|
||||||
CreateMonitorBase(model model.MonitorBase) error
|
CreateMonitorBase(model model.MonitorBase) error
|
||||||
BatchCreateMonitorGPU(list []model.MonitorGPU) error
|
BatchCreateMonitorGPU(list []model.MonitorGPU) error
|
||||||
@@ -25,13 +41,26 @@ type IMonitorRepo interface {
|
|||||||
DelMonitorIO(timeForDelete time.Time) error
|
DelMonitorIO(timeForDelete time.Time) error
|
||||||
DelMonitorNet(timeForDelete time.Time) error
|
DelMonitorNet(timeForDelete time.Time) error
|
||||||
|
|
||||||
WithByProductName(name string) DBOption
|
WithByGPUDevice(deviceID, name string, legacy bool) DBOption
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIMonitorRepo() IMonitorRepo {
|
func NewIMonitorRepo() IMonitorRepo {
|
||||||
return &MonitorRepo{}
|
return &MonitorRepo{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *MonitorRepo) CleanHost() error {
|
||||||
|
for _, item := range []interface{}{&model.MonitorBase{}, &model.MonitorIO{}, &model.MonitorNetwork{}} {
|
||||||
|
if err := global.MonitorDB.Where("1 = 1").Delete(item).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonitorRepo) CleanGPU() error {
|
||||||
|
return global.GPUMonitorDB.Where("1 = 1").Delete(&model.MonitorGPU{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (u *MonitorRepo) GetBase(opts ...DBOption) ([]model.MonitorBase, error) {
|
func (u *MonitorRepo) GetBase(opts ...DBOption) ([]model.MonitorBase, error) {
|
||||||
var data []model.MonitorBase
|
var data []model.MonitorBase
|
||||||
db := global.MonitorDB
|
db := global.MonitorDB
|
||||||
@@ -59,14 +88,16 @@ func (u *MonitorRepo) GetNetwork(opts ...DBOption) ([]model.MonitorNetwork, erro
|
|||||||
err := db.Find(&data).Error
|
err := db.Find(&data).Error
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
func (u *MonitorRepo) GetGPU(opts ...DBOption) ([]model.MonitorGPU, error) {
|
func (u *MonitorRepo) GetIONames() ([]string, error) {
|
||||||
var data []model.MonitorGPU
|
var names []string
|
||||||
db := global.GPUMonitorDB
|
err := global.MonitorDB.Model(&model.MonitorIO{}).Distinct().Pluck("name", &names).Error
|
||||||
for _, opt := range opts {
|
return names, err
|
||||||
db = opt(db)
|
}
|
||||||
}
|
|
||||||
err := db.Find(&data).Error
|
func (u *MonitorRepo) GetNetworkNames() ([]string, error) {
|
||||||
return data, err
|
var names []string
|
||||||
|
err := global.MonitorDB.Model(&model.MonitorNetwork{}).Distinct().Pluck("name", &names).Error
|
||||||
|
return names, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *MonitorRepo) CreateMonitorBase(model model.MonitorBase) error {
|
func (u *MonitorRepo) CreateMonitorBase(model model.MonitorBase) error {
|
||||||
@@ -97,8 +128,67 @@ func (s *MonitorRepo) DelMonitorGPU(timeForDelete time.Time) error {
|
|||||||
return global.GPUMonitorDB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorGPU{}).Error
|
return global.GPUMonitorDB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorGPU{}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MonitorRepo) WithByProductName(name string) DBOption {
|
func (u *MonitorRepo) GetGPUDevices() ([]model.MonitorGPU, error) {
|
||||||
return func(g *gorm.DB) *gorm.DB {
|
var data []model.MonitorGPU
|
||||||
return g.Where("product_name = ?", name)
|
err := global.GPUMonitorDB.Model(&model.MonitorGPU{}).Select("device_id, product_name, device_type").Group("device_id, product_name, device_type").Order("product_name, device_id").Find(&data).Error
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *MonitorRepo) WithByGPUDevice(deviceID, name string, legacy bool) DBOption {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
if deviceID != "" {
|
||||||
|
return db.Where("device_id = ?", deviceID)
|
||||||
|
}
|
||||||
|
db = db.Where("product_name = ?", name)
|
||||||
|
if legacy {
|
||||||
|
db = db.Where("device_id IS NULL OR device_id = ''")
|
||||||
|
}
|
||||||
|
return db
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *MonitorRepo) CountGPU(opts ...DBOption) (int64, error) {
|
||||||
|
db := global.GPUMonitorDB.Model(&model.MonitorGPU{})
|
||||||
|
for _, opt := range opts {
|
||||||
|
db = opt(db)
|
||||||
|
}
|
||||||
|
var count int64
|
||||||
|
err := db.Count(&count).Error
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *MonitorRepo) GetGPUHistory(start time.Time, bucketSeconds int64, aggregation string, opts ...DBOption) ([]GPUHistoryPoint, error) {
|
||||||
|
db := global.GPUMonitorDB.Model(&model.MonitorGPU{})
|
||||||
|
for _, opt := range opts {
|
||||||
|
db = opt(db)
|
||||||
|
}
|
||||||
|
expressions := []string{
|
||||||
|
"CASE WHEN max_power_limit > 0 THEN 100.0 * power_draw / max_power_limit END",
|
||||||
|
"CASE WHEN mem_total > 0 AND mem_used IS NOT NULL THEN 100.0 * mem_used / mem_total ELSE memory_util END",
|
||||||
|
"CASE WHEN (process_status = 'ok' OR process_status IS NULL OR process_status = '') AND json_valid(processes) THEN CASE WHEN json_type(processes) = 'array' THEN json_array_length(processes) END END",
|
||||||
|
}
|
||||||
|
aliases := []string{"power_percent", "memory_percent", "process_count"}
|
||||||
|
columns := []string{"*"}
|
||||||
|
if bucketSeconds > 0 {
|
||||||
|
operation := "AVG"
|
||||||
|
if aggregation == "max" {
|
||||||
|
operation = "MAX"
|
||||||
|
}
|
||||||
|
columns = []string{fmt.Sprintf("(CAST(strftime('%%s', created_at) AS INTEGER) - %d) / %d AS bucket", start.Unix(), bucketSeconds)}
|
||||||
|
for _, column := range []string{"memory_activity", "encoder_util", "decoder_util", "jpeg_util", "ofa_util", "media_util", "compute_util", "copy_util", "hotspot_temperature", "fan_rpm", "ai_cpu_util", "ctrl_cpu_util", "ddr_used", "ddr_total", "hbm_used", "hbm_total", "ddr_bandwidth", "hbm_bandwidth", "memory_bandwidth", "media_frequency", "hugepages_used", "hugepages_total", "gpu_util", "temperature", "memory_temperature", "power_draw", "max_power_limit", "mem_used", "mem_total", "frequency", "memory_frequency", "fan_speed"} {
|
||||||
|
columns = append(columns, operation+"("+column+") AS "+column)
|
||||||
|
}
|
||||||
|
for i := range expressions {
|
||||||
|
expressions[i] = operation + "(" + expressions[i] + ")"
|
||||||
|
}
|
||||||
|
db = db.Group("bucket").Order("bucket ASC")
|
||||||
|
} else {
|
||||||
|
db = db.Order("created_at ASC, id ASC")
|
||||||
|
}
|
||||||
|
for i, expression := range expressions {
|
||||||
|
columns = append(columns, expression+" AS "+aliases[i])
|
||||||
|
}
|
||||||
|
var data []GPUHistoryPoint
|
||||||
|
err := db.Select(strings.Join(columns, ", ")).Scan(&data).Error
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VLLMMonitorRepo struct{}
|
||||||
|
|
||||||
|
type VLLMHistoryPoint struct {
|
||||||
|
model.MonitorVLLM
|
||||||
|
Bucket int64
|
||||||
|
CompletedRequests *float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VLLMMonitorRepo) Create(point *model.MonitorVLLM) error {
|
||||||
|
return global.VLLMMonitorDB.Create(point).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VLLMMonitorRepo) Latest(id uint) (model.MonitorVLLM, error) {
|
||||||
|
var point model.MonitorVLLM
|
||||||
|
db := global.VLLMMonitorDB.Where("app_install_id = ?", id)
|
||||||
|
err := db.Order("created_at DESC, id DESC").First(&point).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return point, nil
|
||||||
|
}
|
||||||
|
return point, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VLLMMonitorRepo) CleanTarget(id uint) error {
|
||||||
|
return global.VLLMMonitorDB.Where("app_install_id = ?", id).Delete(&model.MonitorVLLM{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VLLMMonitorRepo) DeleteBefore(before time.Time) error {
|
||||||
|
return global.VLLMMonitorDB.Where("created_at < ?", before).Delete(&model.MonitorVLLM{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VLLMMonitorRepo) Count(ctx context.Context, id uint, start, end time.Time) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
db := global.VLLMMonitorDB.WithContext(ctx).Model(&model.MonitorVLLM{}).Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end)
|
||||||
|
err := db.Count(&count).Error
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VLLMMonitorRepo) History(ctx context.Context, id uint, start, end time.Time, seconds int64, aggregation, group string) ([]VLLMHistoryPoint, error) {
|
||||||
|
db := global.VLLMMonitorDB.WithContext(ctx).Model(&model.MonitorVLLM{}).Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end)
|
||||||
|
var metrics []string
|
||||||
|
if group == "" || group == "overview" {
|
||||||
|
metrics = append(metrics, "running", "waiting", "cache_usage", "prompt_throughput", "generation_throughput", "request_throughput")
|
||||||
|
if seconds <= 0 || aggregation == "max" {
|
||||||
|
metrics = append(metrics, "prefix_cache_hit_rate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if seconds <= 0 || aggregation == "max" {
|
||||||
|
if group == "" || group == "latency" {
|
||||||
|
metrics = append(metrics, "queue_time", "time_to_first_token", "time_per_output_token", "request_latency", "prefill_time", "decode_time", "time_to_first_token_p50", "time_to_first_token_p90", "time_to_first_token_p95", "time_to_first_token_p99", "time_per_output_token_p50", "time_per_output_token_p90", "time_per_output_token_p95", "time_per_output_token_p99", "request_latency_p50", "request_latency_p90", "request_latency_p95", "request_latency_p99")
|
||||||
|
}
|
||||||
|
if group == "" || group == "requests" {
|
||||||
|
metrics = append(metrics, "average_generation_tokens")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var columns []string
|
||||||
|
if seconds > 0 {
|
||||||
|
operation := "AVG"
|
||||||
|
if aggregation == "max" {
|
||||||
|
operation = "MAX"
|
||||||
|
}
|
||||||
|
columns = []string{fmt.Sprintf("(CAST(strftime('%%s', created_at) AS INTEGER) - %d) / %d AS bucket", start.Unix(), seconds)}
|
||||||
|
for _, column := range metrics {
|
||||||
|
columns = append(columns, operation+"("+column+") AS "+column)
|
||||||
|
}
|
||||||
|
db = db.Group("bucket").Order("bucket ASC")
|
||||||
|
} else {
|
||||||
|
columns = append([]string{"id", "created_at", "app_install_id", "status", "sample_interval_seconds"}, metrics...)
|
||||||
|
if group == "" || group == "requests" {
|
||||||
|
columns = append(columns, "histogram_deltas", "request_deltas")
|
||||||
|
}
|
||||||
|
db = db.Order("created_at ASC, id ASC")
|
||||||
|
}
|
||||||
|
if group == "" || group == "overview" {
|
||||||
|
count := "CASE WHEN json_valid(request_deltas) THEN CASE WHEN json_extract(request_deltas, '$.seconds') > 0 THEN ROUND(request_throughput * json_extract(request_deltas, '$.seconds')) END END"
|
||||||
|
if seconds > 0 {
|
||||||
|
count = "SUM(" + count + ")"
|
||||||
|
}
|
||||||
|
columns = append(columns, count+" AS completed_requests")
|
||||||
|
}
|
||||||
|
var points []VLLMHistoryPoint
|
||||||
|
err := db.Select(strings.Join(columns, ", ")).Scan(&points).Error
|
||||||
|
return points, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VLLMMonitorRepo) WalkDeltas(ctx context.Context, id uint, start, end time.Time, seconds int64, aggregation, group string, visit func(int64, string, string) error) error {
|
||||||
|
histograms, requests := "''", "''"
|
||||||
|
if group == "" || group == "requests" || (group == "latency" && aggregation != "max") {
|
||||||
|
histograms = "COALESCE(histogram_deltas, '')"
|
||||||
|
}
|
||||||
|
if group == "" || group == "requests" || (group == "overview" && aggregation != "max") {
|
||||||
|
requests = "COALESCE(request_deltas, '')"
|
||||||
|
}
|
||||||
|
if histograms == "''" && requests == "''" {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
bucket := fmt.Sprintf("(CAST(strftime('%%s', created_at) AS INTEGER) - %d) / %d", start.Unix(), seconds)
|
||||||
|
rows, err := global.VLLMMonitorDB.WithContext(ctx).Model(&model.MonitorVLLM{}).
|
||||||
|
Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end).
|
||||||
|
Select(bucket + ", " + histograms + ", " + requests).Rows()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var index int64
|
||||||
|
var histograms, requests string
|
||||||
|
if err := rows.Scan(&index, &histograms, &requests); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := visit(index, histograms, requests); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows.Err()
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SettingRepo struct{}
|
type SettingRepo struct{}
|
||||||
@@ -13,18 +15,26 @@ type SettingRepo struct{}
|
|||||||
type ISettingRepo interface {
|
type ISettingRepo interface {
|
||||||
GetList(opts ...DBOption) ([]model.Setting, error)
|
GetList(opts ...DBOption) ([]model.Setting, error)
|
||||||
Get(opts ...DBOption) (model.Setting, error)
|
Get(opts ...DBOption) (model.Setting, error)
|
||||||
|
GetWithContext(ctx context.Context, opts ...DBOption) (model.Setting, error)
|
||||||
GetValueByKey(key string) (string, error)
|
GetValueByKey(key string) (string, error)
|
||||||
|
GetCoreValueByKey(key string) (string, error)
|
||||||
Create(key, value string) error
|
Create(key, value string) error
|
||||||
|
CreateWithContext(ctx context.Context, setting *model.Setting) error
|
||||||
Update(key, value string) error
|
Update(key, value string) error
|
||||||
|
UpdateIfMatch(ctx context.Context, id uint, expectedValue, value string) (int64, error)
|
||||||
WithByKey(key string) DBOption
|
WithByKey(key string) DBOption
|
||||||
|
|
||||||
UpdateOrCreate(key, value string) error
|
UpdateOrCreate(key, value string) error
|
||||||
|
UpdateValues(map[string]string) error
|
||||||
|
|
||||||
GetDescription(opts ...DBOption) (model.CommonDescription, error)
|
GetDescription(opts ...DBOption) (model.CommonDescription, error)
|
||||||
GetDescriptionList(opts ...DBOption) ([]model.CommonDescription, error)
|
GetDescriptionList(opts ...DBOption) ([]model.CommonDescription, error)
|
||||||
CreateDescription(data *model.CommonDescription) error
|
CreateDescription(data *model.CommonDescription) error
|
||||||
|
SaveDescriptions(context.Context, []model.CommonDescription) error
|
||||||
UpdateDescription(id string, val map[string]interface{}) error
|
UpdateDescription(id string, val map[string]interface{}) error
|
||||||
DelDescription(id string) error
|
DelDescription(id string) error
|
||||||
|
DeleteDescriptions(context.Context, string, []string, bool) (int64, error)
|
||||||
|
WithDescriptionIDs(ids []string) DBOption
|
||||||
WithByDescriptionID(id string) DBOption
|
WithByDescriptionID(id string) DBOption
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +70,22 @@ func (s *SettingRepo) Get(opts ...DBOption) (model.Setting, error) {
|
|||||||
return settings, err
|
return settings, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingRepo) GetWithContext(ctx context.Context, opts ...DBOption) (model.Setting, error) {
|
||||||
|
var setting model.Setting
|
||||||
|
err := getTx(ctx, opts...).WithContext(ctx).First(&setting).Error
|
||||||
|
return setting, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingRepo) CreateWithContext(ctx context.Context, setting *model.Setting) error {
|
||||||
|
return getTx(ctx).WithContext(ctx).Create(setting).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingRepo) UpdateIfMatch(ctx context.Context, id uint, expectedValue, value string) (int64, error) {
|
||||||
|
result := getTx(ctx).WithContext(ctx).Model(&model.Setting{}).
|
||||||
|
Where("id = ? AND value = ?", id, expectedValue).Update("value", value)
|
||||||
|
return result.RowsAffected, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SettingRepo) GetValueByKey(key string) (string, error) {
|
func (s *SettingRepo) GetValueByKey(key string) (string, error) {
|
||||||
var setting model.Setting
|
var setting model.Setting
|
||||||
if err := global.DB.Model(&model.Setting{}).Where("key = ?", key).First(&setting).Error; err != nil {
|
if err := global.DB.Model(&model.Setting{}).Where("key = ?", key).First(&setting).Error; err != nil {
|
||||||
@@ -68,6 +94,17 @@ func (s *SettingRepo) GetValueByKey(key string) (string, error) {
|
|||||||
return setting.Value, nil
|
return setting.Value, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingRepo) GetCoreValueByKey(key string) (string, error) {
|
||||||
|
if global.CoreDB == nil {
|
||||||
|
return "", errors.New("core database is unavailable")
|
||||||
|
}
|
||||||
|
var setting model.Setting
|
||||||
|
if err := global.CoreDB.Where("key = ?", key).First(&setting).Error; err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return setting.Value, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SettingRepo) WithByKey(key string) DBOption {
|
func (s *SettingRepo) WithByKey(key string) DBOption {
|
||||||
return func(g *gorm.DB) *gorm.DB {
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
return g.Where("key = ?", key)
|
return g.Where("key = ?", key)
|
||||||
@@ -90,6 +127,25 @@ func (s *SettingRepo) UpdateOrCreate(key, value string) error {
|
|||||||
return global.DB.Model(&setting).UpdateColumn("value", value).Error
|
return global.DB.Model(&setting).UpdateColumn("value", value).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingRepo) UpdateValues(values map[string]string) error {
|
||||||
|
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
for key, value := range values {
|
||||||
|
var setting model.Setting
|
||||||
|
err := tx.Where("key = ?", key).First(&setting).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
if err := tx.Create(&model.Setting{Key: key, Value: value}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
} else if err := tx.Model(&setting).UpdateColumn("value", value).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SettingRepo) GetDescriptionList(opts ...DBOption) ([]model.CommonDescription, error) {
|
func (s *SettingRepo) GetDescriptionList(opts ...DBOption) ([]model.CommonDescription, error) {
|
||||||
var lists []model.CommonDescription
|
var lists []model.CommonDescription
|
||||||
db := global.DB.Model(&model.CommonDescription{})
|
db := global.DB.Model(&model.CommonDescription{})
|
||||||
@@ -111,14 +167,42 @@ func (s *SettingRepo) GetDescription(opts ...DBOption) (model.CommonDescription,
|
|||||||
func (s *SettingRepo) CreateDescription(data *model.CommonDescription) error {
|
func (s *SettingRepo) CreateDescription(data *model.CommonDescription) error {
|
||||||
return global.DB.Create(data).Error
|
return global.DB.Create(data).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingRepo) SaveDescriptions(ctx context.Context, descriptions []model.CommonDescription) error {
|
||||||
|
return global.DB.WithContext(ctx).Clauses(clause.OnConflict{
|
||||||
|
Columns: []clause.Column{{Name: "id"}},
|
||||||
|
DoUpdates: clause.AssignmentColumns([]string{"description"}),
|
||||||
|
}).CreateInBatches(&descriptions, 100).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SettingRepo) UpdateDescription(id string, val map[string]interface{}) error {
|
func (s *SettingRepo) UpdateDescription(id string, val map[string]interface{}) error {
|
||||||
return global.DB.Model(&model.CommonDescription{}).Where("id = ?", id).Updates(val).Error
|
return global.DB.Model(&model.CommonDescription{}).Where("id = ?", id).Updates(val).Error
|
||||||
}
|
}
|
||||||
func (s *SettingRepo) DelDescription(id string) error {
|
func (s *SettingRepo) DelDescription(id string) error {
|
||||||
return global.DB.Where("id = ?", id).Delete(&model.CommonDescription{}).Error
|
return global.DB.Where("id = ?", id).Delete(&model.CommonDescription{}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingRepo) DeleteDescriptions(ctx context.Context, kind string, ids []string, emptyOnly bool) (int64, error) {
|
||||||
|
var deleted int64
|
||||||
|
for start := 0; start < len(ids); start += 500 {
|
||||||
|
query := global.DB.WithContext(ctx).Where("type = ? AND id IN ?", kind, ids[start:min(start+500, len(ids))])
|
||||||
|
if emptyOnly {
|
||||||
|
query = query.Where("description = ? AND is_pinned = ?", "", false)
|
||||||
|
}
|
||||||
|
result := query.Delete(&model.CommonDescription{})
|
||||||
|
deleted += result.RowsAffected
|
||||||
|
if result.Error != nil {
|
||||||
|
return deleted, result.Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deleted, nil
|
||||||
|
}
|
||||||
func (s *SettingRepo) WithByDescriptionID(id string) DBOption {
|
func (s *SettingRepo) WithByDescriptionID(id string) DBOption {
|
||||||
return func(g *gorm.DB) *gorm.DB {
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
return g.Where("id = ?", id)
|
return g.Where("id = ?", id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingRepo) WithDescriptionIDs(ids []string) DBOption {
|
||||||
|
return func(db *gorm.DB) *gorm.DB { return db.Where("id IN ?", ids) }
|
||||||
|
}
|
||||||
|
|||||||
+22
-6
@@ -2,6 +2,7 @@ package repo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
@@ -18,8 +19,8 @@ type ITaskRepo interface {
|
|||||||
GetFirst(opts ...DBOption) (model.Task, error)
|
GetFirst(opts ...DBOption) (model.Task, error)
|
||||||
Page(page, size int, opts ...DBOption) (int64, []model.Task, error)
|
Page(page, size int, opts ...DBOption) (int64, []model.Task, error)
|
||||||
Update(ctx context.Context, task *model.Task) error
|
Update(ctx context.Context, task *model.Task) error
|
||||||
UpdateRunningTaskToFailed() error
|
UpdateRunningTaskToFailed(opts ...DBOption) error
|
||||||
CountExecutingTask() (int64, error)
|
CountExecutingTask(opts ...DBOption) (int64, error)
|
||||||
Delete(opts ...DBOption) error
|
Delete(opts ...DBOption) error
|
||||||
DeleteAll() error
|
DeleteAll() error
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ type ITaskRepo interface {
|
|||||||
WithResourceID(id uint) DBOption
|
WithResourceID(id uint) DBOption
|
||||||
WithOperate(taskOperate string) DBOption
|
WithOperate(taskOperate string) DBOption
|
||||||
WithByStatus(status string) DBOption
|
WithByStatus(status string) DBOption
|
||||||
|
WithoutScopes(scopes ...string) DBOption
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewITaskRepo() ITaskRepo {
|
func NewITaskRepo() ITaskRepo {
|
||||||
@@ -109,16 +111,30 @@ func (t TaskRepo) Update(ctx context.Context, task *model.Task) error {
|
|||||||
return getTaskTx(ctx).Save(&task).Error
|
return getTaskTx(ctx).Save(&task).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t TaskRepo) UpdateRunningTaskToFailed() error {
|
func (t TaskRepo) UpdateRunningTaskToFailed(opts ...DBOption) error {
|
||||||
return getTaskDb(t.WithByStatus(constant.StatusExecuting)).Model(&model.Task{}).Updates(map[string]interface{}{"status": constant.StatusFailed, "error_msg": "1Panel restart causes failure"}).Error
|
opts = append(opts, t.WithByStatus(constant.StatusExecuting))
|
||||||
|
return getTaskDb(opts...).Model(&model.Task{}).Updates(map[string]interface{}{
|
||||||
|
"status": constant.StatusFailed, "error_msg": constant.InterruptedMsg, "end_at": time.Now(),
|
||||||
|
}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t TaskRepo) CountExecutingTask() (int64, error) {
|
func (t TaskRepo) CountExecutingTask(opts ...DBOption) (int64, error) {
|
||||||
var count int64
|
var count int64
|
||||||
err := getTaskDb(t.WithByStatus(constant.StatusExecuting)).Model(&model.Task{}).Count(&count).Error
|
opts = append(opts, t.WithByStatus(constant.StatusExecuting))
|
||||||
|
err := getTaskDb(opts...).Model(&model.Task{}).Count(&count).Error
|
||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t TaskRepo) WithoutScopes(scopes ...string) DBOption {
|
||||||
|
excluded := append([]string(nil), scopes...)
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
if len(excluded) == 0 {
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
return db.Where("type IS NULL OR type NOT IN ?", excluded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (t TaskRepo) Delete(opts ...DBOption) error {
|
func (t TaskRepo) Delete(opts ...DBOption) error {
|
||||||
db := global.TaskDB
|
db := global.TaskDB
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
|
|||||||
+56
-10
@@ -88,6 +88,7 @@ type IAgentService interface {
|
|||||||
|
|
||||||
GetFeishuConfig(req dto.AgentFeishuConfigReq) (*dto.AgentFeishuConfig, error)
|
GetFeishuConfig(req dto.AgentFeishuConfigReq) (*dto.AgentFeishuConfig, error)
|
||||||
UpdateFeishuConfig(req dto.AgentFeishuConfigUpdateReq) error
|
UpdateFeishuConfig(req dto.AgentFeishuConfigUpdateReq) error
|
||||||
|
SwitchFeishuPlugin(req dto.AgentFeishuSwitchReq) error
|
||||||
GetTelegramConfig(req dto.AgentTelegramConfigReq) (*dto.AgentTelegramConfig, error)
|
GetTelegramConfig(req dto.AgentTelegramConfigReq) (*dto.AgentTelegramConfig, error)
|
||||||
UpdateTelegramConfig(req dto.AgentTelegramConfigUpdateReq) error
|
UpdateTelegramConfig(req dto.AgentTelegramConfigUpdateReq) error
|
||||||
GetDiscordConfig(req dto.AgentIDReq) (*dto.AgentDiscordConfig, error)
|
GetDiscordConfig(req dto.AgentIDReq) (*dto.AgentDiscordConfig, error)
|
||||||
@@ -142,6 +143,9 @@ const (
|
|||||||
|
|
||||||
func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||||
agentType := req.AgentType
|
agentType := req.AgentType
|
||||||
|
if err := validateNativeAgentCreate(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if err := checkPortExist(req.WebUIPort); err != nil {
|
if err := checkPortExist(req.WebUIPort); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -237,6 +241,8 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
|||||||
}
|
}
|
||||||
} else if agentType == constant.AppCopaw {
|
} else if agentType == constant.AppCopaw {
|
||||||
dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
||||||
|
} else if agentType == constant.AppDeepSeekHarness || agentType == constant.AppOctop {
|
||||||
|
dashboardAuth = agentDashboardAuth{Username: req.DashboardUsername, Password: req.DashboardPassword}
|
||||||
}
|
}
|
||||||
|
|
||||||
params := map[string]interface{}{
|
params := map[string]interface{}{
|
||||||
@@ -245,6 +251,12 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
|||||||
constant.HostIP: "",
|
constant.HostIP: "",
|
||||||
}
|
}
|
||||||
setAgentWebUIParams(params, agentType, detail.Version, req.WebUIPort)
|
setAgentWebUIParams(params, agentType, detail.Version, req.WebUIPort)
|
||||||
|
if agentType == constant.AppDeepSeekHarness {
|
||||||
|
params["HTTPS_ACCESS_HOST"] = req.AccessHost
|
||||||
|
}
|
||||||
|
if agentType == constant.AppOctop {
|
||||||
|
params["OCTOP_LOG_LEVEL"] = "info"
|
||||||
|
}
|
||||||
if agentType == constant.AppOpenclaw {
|
if agentType == constant.AppOpenclaw {
|
||||||
if allowedOrigin := firstAllowedOrigin(allowedOrigins); allowedOrigin != "" {
|
if allowedOrigin := firstAllowedOrigin(allowedOrigins); allowedOrigin != "" {
|
||||||
params["ALLOWED_ORIGIN"] = allowedOrigin
|
params["ALLOWED_ORIGIN"] = allowedOrigin
|
||||||
@@ -738,7 +750,7 @@ func requiresAgentAccount(agentType string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func setAgentWebUIParams(params map[string]interface{}, agentType, appVersion string, webUIPort int) {
|
func setAgentWebUIParams(params map[string]interface{}, agentType, appVersion string, webUIPort int) {
|
||||||
if agentType == constant.AppOpenclaw && isOpenclawHTTPSWindowVersion(appVersion) {
|
if agentType == constant.AppDeepSeekHarness || agentType == constant.AppOpenclaw && isOpenclawHTTPSWindowVersion(appVersion) {
|
||||||
params["PANEL_APP_PORT_HTTPS"] = webUIPort
|
params["PANEL_APP_PORT_HTTPS"] = webUIPort
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -936,6 +948,7 @@ func (a AgentService) GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig,
|
|||||||
AccountID: agent.AccountID,
|
AccountID: agent.AccountID,
|
||||||
Model: model,
|
Model: model,
|
||||||
Fallbacks: extractOpenclawFallbackModelIDs(conf, account, models, model),
|
Fallbacks: extractOpenclawFallbackModelIDs(conf, account, models, model),
|
||||||
|
Metadata: extractOpenclawModelMetadata(conf, account, models),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -967,7 +980,7 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
|
|||||||
if agent.AgentType != constant.AppOpenclaw {
|
if agent.AgentType != constant.AppOpenclaw {
|
||||||
return fmt.Errorf("%s does not support", agent.AgentType)
|
return fmt.Errorf("%s does not support", agent.AgentType)
|
||||||
}
|
}
|
||||||
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, req.Fallbacks); err != nil {
|
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, req.Fallbacks, req.Metadata); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1043,7 +1056,7 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability
|
validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability
|
||||||
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability)
|
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1096,12 +1109,15 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
|||||||
if strings.TrimSpace(requestedVerifyModel) == "" {
|
if strings.TrimSpace(requestedVerifyModel) == "" {
|
||||||
requestedVerifyModel = account.VerifyModel
|
requestedVerifyModel = account.VerifyModel
|
||||||
}
|
}
|
||||||
verifyModel, err := resolveAgentAccountVerifyModel(provider, requestedVerifyModel, models)
|
verifyModel := ""
|
||||||
if err != nil {
|
if account.AppInstallID == 0 || len(models) > 0 {
|
||||||
return err
|
verifyModel, err = resolveAgentAccountVerifyModel(provider, requestedVerifyModel, models)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability
|
validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability
|
||||||
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability)
|
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability, account.AppInstallID != 0 && len(models) == 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1115,7 +1131,7 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
|||||||
account.Remark = req.Remark
|
account.Remark = req.Remark
|
||||||
account.Verified = true
|
account.Verified = true
|
||||||
|
|
||||||
if err := global.DB.Save(account).Error; err != nil {
|
if err := agentAccountRepo.Save(account); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
terminalai.InvalidateTerminalRuntimeCache()
|
terminalai.InvalidateTerminalRuntimeCache()
|
||||||
@@ -1146,13 +1162,34 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
|
installIDs := make([]uint, 0, len(list))
|
||||||
|
for _, account := range list {
|
||||||
|
if account.AppInstallID != 0 {
|
||||||
|
installIDs = append(installIDs, account.AppInstallID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
localInstalls := make(map[uint]bool, len(installIDs))
|
||||||
|
if len(installIDs) > 0 {
|
||||||
|
installs, err := appInstallRepo.ListBy(context.Background(), repo.WithByIDs(installIDs))
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
for _, install := range installs {
|
||||||
|
localInstalls[install.ID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
items := make([]dto.AgentAccountInfo, 0, len(list))
|
items := make([]dto.AgentAccountInfo, 0, len(list))
|
||||||
for _, item := range list {
|
for _, item := range list {
|
||||||
apiKey := ""
|
apiKey := ""
|
||||||
if item.RememberAPIKey {
|
if item.RememberAPIKey {
|
||||||
apiKey = item.APIKey
|
apiKey = item.APIKey
|
||||||
}
|
}
|
||||||
|
appInstallID := uint(0)
|
||||||
|
if localInstalls[item.AppInstallID] {
|
||||||
|
appInstallID = item.AppInstallID
|
||||||
|
}
|
||||||
items = append(items, dto.AgentAccountInfo{
|
items = append(items, dto.AgentAccountInfo{
|
||||||
|
AppInstallID: appInstallID,
|
||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
MasterAccountID: item.MasterAccountID,
|
MasterAccountID: item.MasterAccountID,
|
||||||
Provider: item.Provider,
|
Provider: item.Provider,
|
||||||
@@ -1350,13 +1387,18 @@ func (a AgentService) SyncAgentsByAccount(account *model.AgentAccount) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error {
|
func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error {
|
||||||
_, err := resolveAgentAccountInput(req.Provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, req.Model, true)
|
_, err := resolveAgentAccountInput(req.Provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, req.Model, true, false)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error {
|
func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error {
|
||||||
agentAccountMutationMu.Lock()
|
agentAccountMutationMu.Lock()
|
||||||
defer agentAccountMutationMu.Unlock()
|
defer agentAccountMutationMu.Unlock()
|
||||||
|
_, err := agentAccountRepo.GetFirst(repo.WithByID(req.ID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := global.DB.Transaction(func(tx *gorm.DB) error {
|
if err := global.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
var agentCount int64
|
var agentCount int64
|
||||||
if err := tx.Model(&model.Agent{}).Where("account_id = ?", req.ID).Count(&agentCount).Error; err != nil {
|
if err := tx.Model(&model.Agent{}).Where("account_id = ?", req.ID).Count(&agentCount).Error; err != nil {
|
||||||
@@ -1541,6 +1583,10 @@ func (a AgentService) UpdateConfigFile(req dto.AgentConfigFileUpdateReq) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if _, busy := feishuSwitches.LoadOrStore(agent.ConfigPath, true); busy {
|
||||||
|
return buserr.New("TaskIsExecuting")
|
||||||
|
}
|
||||||
|
defer feishuSwitches.Delete(agent.ConfigPath)
|
||||||
if err := validateAgentConfigFileContent(agent.AgentType, req.Content); err != nil {
|
if err := validateAgentConfigFileContent(agent.AgentType, req.Content); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1684,7 +1730,7 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fallbacks := extractOpenclawFallbackModelIDs(conf, account, accountModels, selectedAccountModel.ID)
|
fallbacks := extractOpenclawFallbackModelIDs(conf, account, accountModels, selectedAccountModel.ID)
|
||||||
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, fallbacks); err != nil {
|
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, fallbacks, nil); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case constant.AppHermesAgent:
|
case constant.AppHermesAgent:
|
||||||
|
|||||||
@@ -45,9 +45,29 @@ func (a AgentService) GetFeishuConfig(req dto.AgentFeishuConfigReq) (*dto.AgentF
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if agent.AgentType != constant.AppOpenclaw || !isOpenclawBundledFeishuVersion(install.Version) {
|
||||||
|
result := extractFeishuConfig(conf)
|
||||||
|
result.Installed, _ = checkPluginInstalled(install.GetPath(), "feishu")
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
legacy, configured, err := classifyLegacyFeishu(conf, install.GetPath())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
result := extractFeishuConfig(conf)
|
result := extractFeishuConfig(conf)
|
||||||
installed, _ := checkPluginInstalled(install.GetPath(), "feishu")
|
if !legacy {
|
||||||
result.Installed = installed
|
result = extractBundledFeishuConfig(conf)
|
||||||
|
}
|
||||||
|
result.BuiltinManaged = true
|
||||||
|
result.BuiltinAvailable = hasBundledFeishu(install.ContainerName)
|
||||||
|
result.Installed = result.BuiltinAvailable
|
||||||
|
result.Legacy, result.LegacyConfigured = legacy, configured
|
||||||
|
if legacy {
|
||||||
|
result.GroupSessionScope = "group"
|
||||||
|
if result.ThreadSession {
|
||||||
|
result.GroupSessionScope = "group_topic"
|
||||||
|
}
|
||||||
|
}
|
||||||
return &result, nil
|
return &result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +92,9 @@ func (a AgentService) UpdateFeishuConfig(req dto.AgentFeishuConfigUpdateReq) err
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if agent.AgentType == constant.AppOpenclaw && isOpenclawBundledFeishuVersion(install.Version) {
|
||||||
|
return saveBundledFeishuConfig(agent, install, req)
|
||||||
|
}
|
||||||
return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error {
|
return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error {
|
||||||
config := dto.AgentFeishuConfig{
|
config := dto.AgentFeishuConfig{
|
||||||
Enabled: req.Enabled,
|
Enabled: req.Enabled,
|
||||||
@@ -358,6 +381,9 @@ func (a AgentService) InstallPlugin(req dto.AgentPluginInstallReq) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if req.Type == "feishu" && agent.AgentType == constant.AppOpenclaw && isOpenclawBundledFeishuVersion(install.Version) {
|
||||||
|
return buserr.New("ErrFeishuManaged")
|
||||||
|
}
|
||||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
|
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -403,6 +429,9 @@ func (a AgentService) UpgradePlugin(req dto.AgentPluginUpgradeReq) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if req.Type == "feishu" && agent.AgentType == constant.AppOpenclaw && isOpenclawBundledFeishuVersion(install.Version) {
|
||||||
|
return buserr.New("ErrFeishuManaged")
|
||||||
|
}
|
||||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
|
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -443,6 +472,9 @@ func (a AgentService) UninstallPlugin(req dto.AgentPluginUninstallReq) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if req.Type == "feishu" && agent.AgentType == constant.AppOpenclaw && isOpenclawBundledFeishuVersion(install.Version) {
|
||||||
|
return buserr.New("ErrFeishuManaged")
|
||||||
|
}
|
||||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
|
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -538,10 +570,13 @@ func (a AgentService) DeleteChannelConfig(req dto.AgentChannelDeleteReq) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, error) {
|
func (a AgentService) CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, error) {
|
||||||
_, install, err := a.loadAgentAndInstall(req.AgentID)
|
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if req.Type == "feishu" && agent.AgentType == constant.AppOpenclaw && isOpenclawBundledFeishuVersion(install.Version) {
|
||||||
|
return &dto.AgentPluginStatus{Installed: hasBundledFeishu(install.ContainerName)}, nil
|
||||||
|
}
|
||||||
installed, err := checkPluginInstalled(install.GetPath(), req.Type)
|
installed, err := checkPluginInstalled(install.GetPath(), req.Type)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1354,6 +1389,10 @@ func installOpenclawPlugin(mgr *cmd.CommandHelper, containerName, spec, pluginID
|
|||||||
} else if slices.Contains(options, "--dangerously-force-unsafe-install") {
|
} else if slices.Contains(options, "--dangerously-force-unsafe-install") {
|
||||||
args = append(args, "--dangerously-force-unsafe-install")
|
args = append(args, "--dangerously-force-unsafe-install")
|
||||||
}
|
}
|
||||||
|
// Source confirmation does not grant the selected channel plugin's capabilities.
|
||||||
|
if slices.Contains(options, "--accept-capabilities") {
|
||||||
|
args = append(args, "--accept-capabilities")
|
||||||
|
}
|
||||||
return mgr.Run("docker", args...)
|
return mgr.Run("docker", args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,677 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Keep panel configuration writes out of the backup/activation/recovery window.
|
||||||
|
var feishuSwitches sync.Map
|
||||||
|
|
||||||
|
func isOpenclawBundledFeishuVersion(version string) bool {
|
||||||
|
return common.CompareAppVersion(version, "2026.10.7")
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasBundledFeishu(container string) bool {
|
||||||
|
image, err := feishuContainerImage(container)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, err = bundledFeishuImage(image)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func usesBundledFeishu(conf map[string]interface{}) bool {
|
||||||
|
f := getChannelConfig(conf, "feishu")
|
||||||
|
for _, key := range []string{"threadSession", "replyMode"} {
|
||||||
|
if _, old := f[key]; old {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, old := f["streaming"].(bool); old {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
entries := childMap(childMap(conf, "plugins"), "entries")
|
||||||
|
if extractBoolValue(childMap(entries, "openclaw-lark")["enabled"], false) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, streaming := f["streaming"].(map[string]interface{})
|
||||||
|
_, selected := entries["feishu"]
|
||||||
|
return selected && (extractBoolValue(childMap(entries, "feishu")["enabled"], false) || f["groupSessionScope"] != nil || streaming)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasLegacyFeishu(conf map[string]interface{}, installPath string) bool {
|
||||||
|
legacy, _, err := classifyLegacyFeishu(conf, installPath)
|
||||||
|
return err != nil || legacy
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectBundledFeishu(conf map[string]interface{}, enabled bool) {
|
||||||
|
plugins := ensureChildMap(conf, "plugins")
|
||||||
|
entries := ensureChildMap(plugins, "entries")
|
||||||
|
if legacy, ok := entries["openclaw-lark"]; ok {
|
||||||
|
if entry, ok := legacy.(map[string]interface{}); ok {
|
||||||
|
entry["enabled"] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ensureChildMap(entries, "feishu")["enabled"] = enabled
|
||||||
|
// Preserve an absent allowlist; creating one would disable unrelated plugins.
|
||||||
|
if allow, ok := plugins["allow"]; ok && len(extractStringList(allow)) > 0 {
|
||||||
|
ids := slices.DeleteFunc(extractStringList(allow), func(id string) bool { return id == "openclaw-lark" })
|
||||||
|
if !slices.Contains(ids, "feishu") {
|
||||||
|
ids = append(ids, "feishu")
|
||||||
|
}
|
||||||
|
plugins["allow"] = ids
|
||||||
|
}
|
||||||
|
if deny, ok := plugins["deny"]; ok {
|
||||||
|
plugins["deny"] = slices.DeleteFunc(extractStringList(deny), func(id string) bool { return id == "feishu" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func feishuConfigError(field string) error { return buserr.WithDetail("ErrFeishuConfig", field, nil) }
|
||||||
|
|
||||||
|
func convertLegacyFeishu(conf map[string]interface{}, req dto.AgentFeishuSwitchReq) error {
|
||||||
|
f := getChannelConfig(conf, "feishu")
|
||||||
|
if len(f) == 0 {
|
||||||
|
if raw, exists := childMap(conf, "channels")["feishu"]; exists && raw != nil {
|
||||||
|
if _, ok := raw.(map[string]interface{}); !ok {
|
||||||
|
return feishuConfigError("channels.feishu")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f = ensureChildMap(ensureChildMap(conf, "channels"), "feishu")
|
||||||
|
f["enabled"], f["dmPolicy"], f["groupPolicy"] = false, "pairing", "allowlist"
|
||||||
|
}
|
||||||
|
if err := checkLegacyFeishuFields(f, "channels.feishu", false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := convertFeishuAccess(f, "channels.feishu"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
delete(f, "threadSession")
|
||||||
|
delete(f, "replyMode")
|
||||||
|
f["streaming"] = map[string]interface{}{"mode": "off"}
|
||||||
|
if req.Streaming {
|
||||||
|
f["streaming"] = map[string]interface{}{"mode": "partial"}
|
||||||
|
}
|
||||||
|
f["groupSessionScope"] = req.GroupSessionScope
|
||||||
|
f["requireMention"] = req.RequireMention == "true"
|
||||||
|
delete(f, "topicSessionMode")
|
||||||
|
// Preserve legacy defaults explicitly; bundled defaults are more restrictive.
|
||||||
|
if _, ok := f["groupPolicy"]; !ok {
|
||||||
|
f["groupPolicy"] = "open"
|
||||||
|
}
|
||||||
|
entries := ensureChildMap(ensureChildMap(conf, "plugins"), "entries")
|
||||||
|
ensureChildMap(entries, "openclaw-lark")["enabled"] = false
|
||||||
|
selectBundledFeishu(conf, extractBoolValue(f["enabled"], true))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit automatic conversion to the shared channel fields; custom plugin options require review.
|
||||||
|
func checkLegacyFeishuFields(f map[string]interface{}, field string, group bool) error {
|
||||||
|
allowed := strings.Fields("enabled name appId appSecret encryptKey verificationToken domain connectionMode webhookPath defaultAccount dmPolicy allowFrom groupPolicy groupAllowFrom groupSenderAllowFrom requireMention accounts groups streaming threadSession replyMode groupSessionScope topicSessionMode replyInThread")
|
||||||
|
if group {
|
||||||
|
allowed = strings.Fields("enabled requireMention allowFrom groupSessionScope topicSessionMode replyInThread")
|
||||||
|
}
|
||||||
|
for _, key := range sortedChildKeys(f) {
|
||||||
|
if !slices.Contains(allowed, key) {
|
||||||
|
return feishuConfigError(field + "." + key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if enabled, exists := f["enabled"]; exists {
|
||||||
|
if _, ok := enabled.(bool); !ok {
|
||||||
|
return feishuConfigError(field + ".enabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, key := range []string{"dmPolicy", "groupPolicy"} {
|
||||||
|
if raw, exists := f[key]; exists {
|
||||||
|
value, ok := raw.(string)
|
||||||
|
options := []string{"open", "allowlist", "disabled"}
|
||||||
|
if key == "dmPolicy" {
|
||||||
|
options = append(options, "pairing")
|
||||||
|
}
|
||||||
|
if !ok || !slices.Contains(options, value) {
|
||||||
|
return feishuConfigError(field + "." + key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, key := range []string{"allowFrom", "groupSenderAllowFrom"} {
|
||||||
|
if raw, exists := f[key]; exists {
|
||||||
|
values, ok := raw.([]interface{})
|
||||||
|
if !ok {
|
||||||
|
return feishuConfigError(field + "." + key)
|
||||||
|
}
|
||||||
|
for _, value := range values {
|
||||||
|
if _, ok := value.(string); !ok {
|
||||||
|
return feishuConfigError(field + "." + key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, section := range []string{"accounts", "groups"} {
|
||||||
|
raw, exists := f[section]
|
||||||
|
if !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entries, ok := raw.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return feishuConfigError(field + "." + section)
|
||||||
|
}
|
||||||
|
for _, id := range sortedChildKeys(entries) {
|
||||||
|
entry, ok := entries[id].(map[string]interface{})
|
||||||
|
name := field + "." + section + "." + id
|
||||||
|
if !ok {
|
||||||
|
return feishuConfigError(name)
|
||||||
|
}
|
||||||
|
if err := checkLegacyFeishuFields(entry, name, section == "groups"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertFeishuAccess(f map[string]interface{}, field string) error {
|
||||||
|
if raw, ok := f["groupAllowFrom"]; ok {
|
||||||
|
values, ok := raw.([]interface{})
|
||||||
|
if !ok {
|
||||||
|
return feishuConfigError(field + ".groupAllowFrom")
|
||||||
|
}
|
||||||
|
groups, senders := []string{}, []string{}
|
||||||
|
for _, value := range values {
|
||||||
|
id, ok := value.(string)
|
||||||
|
if !ok {
|
||||||
|
return feishuConfigError(field + ".groupAllowFrom")
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(id, "oc_"):
|
||||||
|
groups = append(groups, id)
|
||||||
|
case strings.HasPrefix(id, "ou_"):
|
||||||
|
senders = append(senders, id)
|
||||||
|
default:
|
||||||
|
return feishuConfigError(field + ".groupAllowFrom")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A sender-only legacy allowlist has different group admission semantics.
|
||||||
|
if len(senders) > 0 && (len(groups) == 0 || f["groupSenderAllowFrom"] != nil) {
|
||||||
|
return feishuConfigError(field + ".groupAllowFrom")
|
||||||
|
}
|
||||||
|
f["groupAllowFrom"] = groups
|
||||||
|
if len(senders) > 0 {
|
||||||
|
f["groupSenderAllowFrom"] = senders
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, section := range []string{"accounts", "groups"} {
|
||||||
|
for id, value := range childMap(f, section) {
|
||||||
|
entry, ok := value.(map[string]interface{})
|
||||||
|
name := field + "." + section + "." + id
|
||||||
|
if !ok {
|
||||||
|
return feishuConfigError(name)
|
||||||
|
}
|
||||||
|
// Account/group overrides must be resolved explicitly, not flattened.
|
||||||
|
for _, key := range []string{"threadSession", "replyMode"} {
|
||||||
|
if _, ok := entry[key]; ok {
|
||||||
|
return feishuConfigError(name + "." + key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mention, ok := entry["requireMention"]; ok {
|
||||||
|
if _, ok := mention.(bool); !ok {
|
||||||
|
return feishuConfigError(name + ".requireMention")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if raw, exists := entry["streaming"]; exists {
|
||||||
|
streaming, ok := raw.(bool)
|
||||||
|
if !ok || streaming {
|
||||||
|
return feishuConfigError(name + ".streaming")
|
||||||
|
}
|
||||||
|
entry["streaming"] = map[string]interface{}{"mode": "off"}
|
||||||
|
}
|
||||||
|
if err := convertFeishuAccess(entry, name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractBundledFeishuConfig(conf map[string]interface{}) dto.AgentFeishuConfig {
|
||||||
|
result := extractFeishuConfig(conf)
|
||||||
|
f := getChannelConfig(conf, "feishu")
|
||||||
|
result.Enabled = extractBoolValue(f["enabled"], true) && extractBoolValue(childMap(childMap(childMap(conf, "plugins"), "entries"), "feishu")["enabled"], true)
|
||||||
|
result.GroupSessionScope = extractStringValue(f["groupSessionScope"])
|
||||||
|
if result.GroupSessionScope == "" {
|
||||||
|
result.GroupSessionScope = "group"
|
||||||
|
}
|
||||||
|
result.Streaming = extractStringValue(childMap(f, "streaming")["mode"]) != "off"
|
||||||
|
result.GroupPolicy = extractStringValue(f["groupPolicy"])
|
||||||
|
if result.GroupPolicy == "" {
|
||||||
|
result.GroupPolicy = "allowlist"
|
||||||
|
}
|
||||||
|
if len(f) == 0 {
|
||||||
|
result.Streaming = false
|
||||||
|
result.RequireMention = "true"
|
||||||
|
} else if _, ok := f["requireMention"]; !ok {
|
||||||
|
result.RequireMention = "open"
|
||||||
|
}
|
||||||
|
accounts := childMap(f, "accounts")
|
||||||
|
ids := sortedChildKeys(accounts)
|
||||||
|
if (f["appId"] != nil || f["appSecret"] != nil) && !slices.Contains(ids, "default") {
|
||||||
|
ids = append([]string{"default"}, ids...)
|
||||||
|
}
|
||||||
|
result.Bots = []dto.AgentFeishuBot{}
|
||||||
|
for _, id := range ids {
|
||||||
|
effective := make(map[string]interface{}, len(f))
|
||||||
|
for k, v := range f {
|
||||||
|
effective[k] = v
|
||||||
|
}
|
||||||
|
for k, v := range childMap(accounts, id) {
|
||||||
|
effective[k] = v
|
||||||
|
}
|
||||||
|
bot := defaultFeishuBot()
|
||||||
|
bot.AccountID, bot.IsDefault = id, id == "default"
|
||||||
|
bot.Name = extractDisplayName(childMap(accounts, id), "", id)
|
||||||
|
bot.Enabled = extractBoolValue(childMap(accounts, id)["enabled"], true)
|
||||||
|
bot.AppID, bot.AppSecret = extractStringValue(effective["appId"]), extractStringValue(effective["appSecret"])
|
||||||
|
bot.DmPolicy = extractStringValue(effective["dmPolicy"])
|
||||||
|
if bot.DmPolicy == "" {
|
||||||
|
bot.DmPolicy = "pairing"
|
||||||
|
}
|
||||||
|
bot.AllowFrom = extractStringList(effective["allowFrom"])
|
||||||
|
result.Bots = append(result.Bots, bot)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func setBundledFeishuConfig(conf map[string]interface{}, req dto.AgentFeishuConfigUpdateReq) error {
|
||||||
|
if !slices.Contains([]string{"group", "group_sender", "group_topic", "group_topic_sender"}, req.GroupSessionScope) {
|
||||||
|
return feishuConfigError("groupSessionScope")
|
||||||
|
}
|
||||||
|
f := ensureChildMap(ensureChildMap(conf, "channels"), "feishu")
|
||||||
|
accounts := childMap(f, "accounts")
|
||||||
|
seen := map[string]bool{}
|
||||||
|
appIDs := map[string]bool{}
|
||||||
|
for _, bot := range req.Bots {
|
||||||
|
if bot.AccountID == "" || bot.AccountID != strings.ToLower(strings.TrimSpace(bot.AccountID)) || seen[bot.AccountID] || appIDs[bot.AppID] || bot.AppID == "" || !slices.Contains([]string{"pairing", "open", "allowlist", "disabled"}, bot.DmPolicy) || (bot.DmPolicy == "allowlist" && len(bot.AllowFrom) == 0) {
|
||||||
|
return feishuConfigError("accounts")
|
||||||
|
}
|
||||||
|
seen[bot.AccountID] = true
|
||||||
|
appIDs[bot.AppID] = true
|
||||||
|
entry := childMap(accounts, bot.AccountID)
|
||||||
|
if _, exists := entry["appSecret"]; !exists && bot.AppSecret == "" {
|
||||||
|
entry["appSecret"] = f["appSecret"]
|
||||||
|
}
|
||||||
|
// SecretRef credentials cannot be represented by the form: keep them intact.
|
||||||
|
if _, secretRef := entry["appSecret"].(map[string]interface{}); !secretRef && bot.AppSecret == "" {
|
||||||
|
return feishuConfigError("accounts." + bot.AccountID + ".appSecret")
|
||||||
|
}
|
||||||
|
entry["appId"], entry["name"], entry["enabled"], entry["dmPolicy"] = bot.AppID, bot.Name, bot.Enabled, bot.DmPolicy
|
||||||
|
if bot.AppSecret != "" {
|
||||||
|
entry["appSecret"] = bot.AppSecret
|
||||||
|
}
|
||||||
|
switch bot.DmPolicy {
|
||||||
|
case "open":
|
||||||
|
entry["allowFrom"] = []string{"*"}
|
||||||
|
case "allowlist":
|
||||||
|
entry["allowFrom"] = bot.AllowFrom
|
||||||
|
default:
|
||||||
|
if len(bot.AllowFrom) > 0 {
|
||||||
|
entry["allowFrom"] = bot.AllowFrom
|
||||||
|
} else {
|
||||||
|
entry["allowFrom"] = []string{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
accounts[bot.AccountID] = entry
|
||||||
|
}
|
||||||
|
for id := range accounts {
|
||||||
|
if !seen[id] {
|
||||||
|
delete(accounts, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !seen["default"] {
|
||||||
|
delete(f, "appId")
|
||||||
|
delete(f, "appSecret")
|
||||||
|
}
|
||||||
|
if id := extractStringValue(f["defaultAccount"]); id != "" && !seen[id] {
|
||||||
|
return feishuConfigError("defaultAccount")
|
||||||
|
}
|
||||||
|
if len(accounts) > 0 {
|
||||||
|
f["accounts"] = accounts
|
||||||
|
} else {
|
||||||
|
delete(f, "accounts")
|
||||||
|
}
|
||||||
|
f["enabled"], f["groupSessionScope"], f["groupPolicy"] = req.Enabled, req.GroupSessionScope, req.GroupPolicy
|
||||||
|
if req.RequireMention == "open" {
|
||||||
|
delete(f, "requireMention")
|
||||||
|
} else {
|
||||||
|
f["requireMention"] = req.RequireMention == "true"
|
||||||
|
}
|
||||||
|
if req.GroupPolicy == "allowlist" {
|
||||||
|
f["groupAllowFrom"] = req.GroupAllowFrom
|
||||||
|
} else {
|
||||||
|
delete(f, "groupAllowFrom")
|
||||||
|
}
|
||||||
|
streaming := ensureChildMap(f, "streaming")
|
||||||
|
streaming["mode"] = "off"
|
||||||
|
if req.Streaming {
|
||||||
|
streaming["mode"] = "partial"
|
||||||
|
}
|
||||||
|
selectBundledFeishu(conf, req.Enabled && hasEnabledBots(req.Bots))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write beside the original so validation and rename preserve the mounted filesystem and owner.
|
||||||
|
func stageFeishuConfig(configPath string, payload []byte) (string, error) {
|
||||||
|
info, err := os.Lstat(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if !info.Mode().IsRegular() {
|
||||||
|
return "", feishuConfigError("openclaw.json")
|
||||||
|
}
|
||||||
|
f, err := os.CreateTemp(filepath.Dir(configPath), ".feishu-*.json")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
name := f.Name()
|
||||||
|
defer func() {
|
||||||
|
f.Close()
|
||||||
|
if err != nil {
|
||||||
|
os.Remove(name)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
|
||||||
|
err = f.Chown(int(stat.Uid), int(stat.Gid))
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
err = f.Chmod(info.Mode().Perm())
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
_, err = f.Write(payload)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
err = f.Sync()
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
err = f.Close()
|
||||||
|
}
|
||||||
|
return name, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateFeishuConfig(container, configPath string) error {
|
||||||
|
out, err := cmd.RunDockerExecWithStdout(time.Minute, container, "env", "OPENCLAW_CONFIG_PATH=/home/node/.openclaw/"+filepath.Base(configPath), "openclaw", "config", "validate", "--json")
|
||||||
|
return feishuValidationResult(out, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func replaceFeishuConfig(configPath string, expected, payload []byte) error {
|
||||||
|
tmp, err := stageFeishuConfig(configPath, payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.Remove(tmp)
|
||||||
|
current, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !bytes.Equal(current, expected) {
|
||||||
|
return buserr.New("ErrFeishuConfigChanged")
|
||||||
|
}
|
||||||
|
return os.Rename(tmp, configPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AgentService) SwitchFeishuPlugin(req dto.AgentFeishuSwitchReq) error {
|
||||||
|
agent, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !isOpenclawBundledFeishuVersion(install.Version) {
|
||||||
|
return buserr.New("ErrFeishuImageRequired")
|
||||||
|
}
|
||||||
|
if _, busy := feishuSwitches.LoadOrStore(agent.ConfigPath, true); busy {
|
||||||
|
return buserr.New("TaskIsExecuting")
|
||||||
|
}
|
||||||
|
started := false
|
||||||
|
defer func() {
|
||||||
|
if !started {
|
||||||
|
feishuSwitches.Delete(agent.ConfigPath)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !hasBundledFeishu(install.ContainerName) {
|
||||||
|
return buserr.New("ErrFeishuImageRequired")
|
||||||
|
}
|
||||||
|
conf, err := readOpenclawConfig(agent.ConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
legacy, configured, err := classifyLegacyFeishu(conf, install.GetPath())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !legacy {
|
||||||
|
return buserr.New("ErrFeishuAlreadySwitched")
|
||||||
|
}
|
||||||
|
if err := prepareLegacyFeishu(conf, configured, &dto.AgentFeishuTransition{Confirmed: true, Streaming: req.Streaming, GroupSessionScope: req.GroupSessionScope, RequireMention: req.RequireMention}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switchTask, err := task.NewTaskWithOps("Feishu", task.TaskUpdate, task.TaskScopeAI, req.TaskID, req.AgentID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switchTask.AddSubTaskWithAlias("AgentFeishuSwitch", func(t *task.Task) error { return a.runFeishuSwitch(agent, install, req, t) }, nil)
|
||||||
|
started = true
|
||||||
|
go func() { defer feishuSwitches.Delete(agent.ConfigPath); executeAgentPluginTask(switchTask) }()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AgentService) runFeishuSwitch(agent *model.Agent, install *model.AppInstall, req dto.AgentFeishuSwitchReq, t *task.Task) (result error) {
|
||||||
|
original, conf, err := readFeishuConfigBytes(agent.ConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
legacy, configured, err := classifyLegacyFeishu(conf, install.GetPath())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !legacy {
|
||||||
|
return buserr.New("ErrFeishuAlreadySwitched")
|
||||||
|
}
|
||||||
|
image, err := feishuContainerImage(install.ContainerName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
image, err = bundledFeishuImage(image)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = prepareLegacyFeishu(conf, configured, &dto.AgentFeishuTransition{Confirmed: true, Streaming: req.Streaming, GroupSessionScope: req.GroupSessionScope, RequireMention: req.RequireMention}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
payload, err := json.MarshalIndent(conf, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
recovery, err := feishuRecoveryPayload(payload, configured)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Validate both candidates without invoking Doctor or mutating retained state.
|
||||||
|
for _, candidate := range [][]byte{payload, recovery} {
|
||||||
|
staged, stageErr := stageFeishuConfig(agent.ConfigPath, candidate)
|
||||||
|
if stageErr != nil {
|
||||||
|
return stageErr
|
||||||
|
}
|
||||||
|
validateErr := validateFeishuImageConfig(image, staged)
|
||||||
|
os.Remove(staged)
|
||||||
|
if validateErr != nil {
|
||||||
|
return validateErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err = compose.Stop(install.GetComposePath()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
replaced := false
|
||||||
|
defer func() {
|
||||||
|
if !replaced {
|
||||||
|
if _, startErr := compose.Operate(install.GetComposePath(), "start"); startErr != nil {
|
||||||
|
result = errors.Join(result, buserr.WithErr("ErrFeishuRestore", startErr))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, stopErr := compose.Stop(install.GetComposePath()); stopErr != nil {
|
||||||
|
result = errors.Join(result, buserr.WithErr("ErrFeishuRestore", stopErr))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if restoreErr := replaceFeishuConfig(agent.ConfigPath, payload, recovery); restoreErr != nil {
|
||||||
|
result = errors.Join(result, buserr.WithErr("ErrFeishuRestore", restoreErr))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, startErr := compose.Operate(install.GetComposePath(), "start"); startErr != nil {
|
||||||
|
result = errors.Join(result, buserr.WithErr("ErrFeishuRestore", startErr))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if healthErr := verifyFeishuActivation(install.ContainerName, false); healthErr != nil {
|
||||||
|
result = errors.Join(result, buserr.WithErr("ErrFeishuRestore", healthErr))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Log(i18n.GetMsgByKey("AgentFeishuRecoveryDisabled"))
|
||||||
|
}()
|
||||||
|
current, err := os.ReadFile(agent.ConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !bytes.Equal(current, original) {
|
||||||
|
return buserr.New("ErrFeishuConfigChanged")
|
||||||
|
}
|
||||||
|
backup, err := backupFeishuConfig(agent.ConfigPath, original)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.Log(i18n.GetWithName("AgentFeishuBackup", filepath.Base(backup)))
|
||||||
|
if err = replaceFeishuConfig(agent.ConfigPath, original, payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
replaced = true
|
||||||
|
if _, err = compose.Operate(install.GetComposePath(), "start"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
enabled := configured && extractBoolValue(getChannelConfig(conf, "feishu")["enabled"], true)
|
||||||
|
if err = verifyFeishuActivation(install.ContainerName, enabled); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if enabled {
|
||||||
|
probeFeishuConnection(install.ContainerName, t)
|
||||||
|
}
|
||||||
|
if configured {
|
||||||
|
t.Log(i18n.GetMsgByKey("AgentFeishuVerifyMessages"))
|
||||||
|
} else {
|
||||||
|
t.Log(i18n.GetMsgByKey("AgentFeishuRecoveryDisabled"))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveBundledFeishuConfig(agent *model.Agent, install *model.AppInstall, req dto.AgentFeishuConfigUpdateReq) error {
|
||||||
|
if !isOpenclawBundledFeishuVersion(install.Version) {
|
||||||
|
return buserr.New("ErrFeishuImageRequired")
|
||||||
|
}
|
||||||
|
if _, busy := feishuSwitches.LoadOrStore(agent.ConfigPath, true); busy {
|
||||||
|
return buserr.New("TaskIsExecuting")
|
||||||
|
}
|
||||||
|
defer feishuSwitches.Delete(agent.ConfigPath)
|
||||||
|
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !hasBundledFeishu(install.ContainerName) {
|
||||||
|
return buserr.New("ErrFeishuImageRequired")
|
||||||
|
}
|
||||||
|
original, err := os.ReadFile(agent.ConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var conf map[string]interface{}
|
||||||
|
if err = json.Unmarshal(original, &conf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if hasLegacyFeishu(conf, install.GetPath()) {
|
||||||
|
return buserr.New("ErrFeishuSwitchRequired")
|
||||||
|
}
|
||||||
|
if err = setBundledFeishuConfig(conf, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
payload, err := json.MarshalIndent(conf, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
staged, err := stageFeishuConfig(agent.ConfigPath, payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.Remove(staged)
|
||||||
|
if err = validateFeishuConfig(install.ContainerName, staged); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return replaceFeishuConfig(agent.ConfigPath, original, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
func feishuProbeOK(output string) bool {
|
||||||
|
raw, err := extractEmbeddedJSON(output)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var status struct {
|
||||||
|
ChannelAccounts map[string][]struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Configured bool `json:"configured"`
|
||||||
|
Running bool `json:"running"`
|
||||||
|
Probe struct {
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
} `json:"probe"`
|
||||||
|
} `json:"channelAccounts"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(raw, &status) != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
accounts, ok := status.ChannelAccounts["feishu"]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, account := range accounts {
|
||||||
|
if account.Enabled && (!account.Configured || !account.Running || !account.Probe.OK) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func feishuGatewayReady(output string, enabled bool) bool {
|
||||||
|
raw, err := extractEmbeddedJSON(output)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var health struct {
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
Plugins struct {
|
||||||
|
Loaded []string `json:"loaded"`
|
||||||
|
} `json:"plugins"`
|
||||||
|
}
|
||||||
|
return json.Unmarshal(raw, &health) == nil && health.OK && !slices.Contains(health.Plugins.Loaded, "openclaw-lark") && (!enabled || slices.Contains(health.Plugins.Loaded, "feishu"))
|
||||||
|
}
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Inspect a stopped disposable container; its entrypoint never touches retained state.
|
||||||
|
func bundledFeishuImage(image string) (string, error) {
|
||||||
|
cli, err := docker.NewDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer cli.Close()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
info, _, err := cli.ImageInspectWithRaw(ctx, image)
|
||||||
|
if err != nil {
|
||||||
|
return "", buserr.New("ErrFeishuImageRequired")
|
||||||
|
}
|
||||||
|
created, err := cli.ContainerCreate(ctx, &container.Config{Image: info.ID, Entrypoint: []string{"/bin/true"}}, &container.HostConfig{NetworkMode: "none"}, nil, nil, "")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
cleanup, done := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer done()
|
||||||
|
_ = cli.ContainerRemove(cleanup, created.ID, container.RemoveOptions{Force: true, RemoveVolumes: true})
|
||||||
|
}()
|
||||||
|
if _, err = cli.ContainerStatPath(ctx, created.ID, "/app/.1panel-feishu-bundled-v1"); err != nil {
|
||||||
|
return "", buserr.New("ErrFeishuImageRequired")
|
||||||
|
}
|
||||||
|
return info.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func feishuContainerImage(name string) (string, error) {
|
||||||
|
cli, err := docker.NewDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer cli.Close()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
info, err := cli.ContainerInspect(ctx, name)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return info.Image, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// The original state is read-only and the normal Doctor entrypoint is bypassed.
|
||||||
|
func validateFeishuImageConfig(image, configPath string) error {
|
||||||
|
info, err := os.Stat(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
uid, gid := uint32(1000), uint32(1000)
|
||||||
|
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
|
||||||
|
uid, gid = stat.Uid, stat.Gid
|
||||||
|
}
|
||||||
|
user := fmt.Sprintf("%d:%d", uid, gid)
|
||||||
|
tmpfs := fmt.Sprintf("/tmp:rw,mode=0700,uid=%d,gid=%d", uid, gid)
|
||||||
|
name := "1panel-feishu-" + strings.TrimSuffix(filepath.Base(configPath), ".json")
|
||||||
|
mgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Minute))
|
||||||
|
defer func() { _ = cmd.NewCommandMgr(cmd.WithTimeout(15*time.Second)).Run("docker", "rm", "-f", "-v", name) }()
|
||||||
|
out, runErr := mgr.RunWithStdout("docker", "run", "--name", name, "--rm", "--network", "none", "--read-only", "--user", user,
|
||||||
|
"--tmpfs", tmpfs,
|
||||||
|
"--mount", "type=bind,src="+filepath.Dir(configPath)+",dst=/home/node/.openclaw,readonly",
|
||||||
|
"--env", "HOME=/home/node", "--env", "TMPDIR=/tmp", "--env", "OPENCLAW_STATE_DIR=/tmp/state",
|
||||||
|
"--env", "OPENCLAW_CONFIG_PATH=/home/node/.openclaw/"+filepath.Base(configPath),
|
||||||
|
"--entrypoint", "sh", image, "-c", "mkdir -m 700 /tmp/state && ln -s /home/node/.openclaw/extensions /tmp/state/extensions && exec node /app/openclaw.mjs config validate --json")
|
||||||
|
return feishuValidationResult(out, runErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func feishuValidationResult(out string, runErr error) error {
|
||||||
|
raw, _ := extractEmbeddedJSON(out)
|
||||||
|
var result struct {
|
||||||
|
Valid bool `json:"valid"`
|
||||||
|
Issues []struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
} `json:"issues"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(raw, &result) == nil && result.Valid && runErr == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fields := []string{}
|
||||||
|
for _, issue := range result.Issues {
|
||||||
|
if issue.Path != "" {
|
||||||
|
fields = append(fields, issue.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(fields) == 0 {
|
||||||
|
fields = append(fields, "openclaw.json")
|
||||||
|
}
|
||||||
|
return feishuConfigError(strings.Join(fields, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
func backupFeishuConfig(configPath string, original []byte) (string, error) {
|
||||||
|
f, err := os.CreateTemp(filepath.Dir(configPath), ".feishu-backup-*.json")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if _, err = f.Write(original); err == nil {
|
||||||
|
err = f.Sync()
|
||||||
|
}
|
||||||
|
closeErr := f.Close()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if closeErr != nil {
|
||||||
|
return "", closeErr
|
||||||
|
}
|
||||||
|
return f.Name(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Doctor can spend 94 seconds waiting for the previous container's owner lease.
|
||||||
|
func waitOpenclawReady(name string) error {
|
||||||
|
script := `const fs=require('fs');const c=JSON.parse(fs.readFileSync('/home/node/.openclaw/openclaw.json','utf8'));const tls=c.gateway?.tls?.enabled===true;const h=require(tls?'https':'http');function check(p){const r=h.get({hostname:'127.0.0.1',port:c.gateway?.port||18789,path:p,rejectUnauthorized:false,timeout:3000},s=>{s.resume();if(s.statusCode===404&&p==='/readyz')return check('/healthz');process.exitCode=s.statusCode===200?0:1});r.on('timeout',()=>r.destroy());r.on('error',()=>process.exitCode=1)}check('/readyz')`
|
||||||
|
deadline := time.Now().Add(4 * time.Minute)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if cmd.RunDockerExec(8*time.Second, name, "node", "-e", script) == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
}
|
||||||
|
return buserr.New("ErrFeishuActivation")
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyFeishuActivation(name string, enabled bool) error {
|
||||||
|
if err := waitOpenclawReady(name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
output, err := cmd.RunDockerExecWithStdout(time.Minute, name, "openclaw", "plugins", "list", "--json")
|
||||||
|
if err != nil {
|
||||||
|
return buserr.New("ErrFeishuActivation")
|
||||||
|
}
|
||||||
|
raw, _ := extractEmbeddedJSON(output)
|
||||||
|
var result struct {
|
||||||
|
Plugins []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Origin string `json:"origin"`
|
||||||
|
} `json:"plugins"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(raw, &result) != nil {
|
||||||
|
return buserr.New("ErrFeishuActivation")
|
||||||
|
}
|
||||||
|
found := false
|
||||||
|
for _, p := range result.Plugins {
|
||||||
|
if p.ID == "openclaw-lark" && p.Status != "disabled" {
|
||||||
|
return buserr.New("ErrFeishuActivation")
|
||||||
|
}
|
||||||
|
if p.ID == "feishu" && p.Origin == "bundled" && (p.Status == "loaded" || (!enabled && p.Status == "disabled")) {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return buserr.New("ErrFeishuActivation")
|
||||||
|
}
|
||||||
|
health, err := cmd.RunDockerExecWithStdout(20*time.Second, name, "openclaw", "gateway", "health", "--json")
|
||||||
|
if err != nil || !feishuGatewayReady(health, enabled) {
|
||||||
|
return buserr.New("ErrFeishuActivation")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readFeishuConfigBytes(file string) ([]byte, map[string]interface{}, error) {
|
||||||
|
info, err := os.Lstat(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if !info.Mode().IsRegular() {
|
||||||
|
return nil, nil, feishuConfigError("openclaw.json")
|
||||||
|
}
|
||||||
|
f, err := os.Open(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
raw, err := io.ReadAll(f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
var conf map[string]interface{}
|
||||||
|
if err = json.Unmarshal(raw, &conf); err != nil || conf == nil {
|
||||||
|
return nil, nil, feishuConfigError("openclaw.json")
|
||||||
|
}
|
||||||
|
return raw, conf, nil
|
||||||
|
}
|
||||||
@@ -8,11 +8,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const hermesWeixinLoginScript = `import asyncio
|
const hermesWeixinLoginScript = `import asyncio
|
||||||
from gateway.platforms.weixin import qr_login
|
try:
|
||||||
|
from plugins.platforms.weixin.adapter import qr_login
|
||||||
|
except ModuleNotFoundError as exc:
|
||||||
|
if exc.name not in ('plugins', 'plugins.platforms', 'plugins.platforms.weixin', 'plugins.platforms.weixin.adapter'):
|
||||||
|
raise
|
||||||
|
from gateway.platforms.weixin import qr_login
|
||||||
from hermes_cli.config import save_env_value
|
from hermes_cli.config import save_env_value
|
||||||
|
|
||||||
creds = asyncio.run(qr_login('/opt/data'))
|
creds = asyncio.run(qr_login('/opt/data'))
|
||||||
print('RESULT =', creds)
|
|
||||||
|
|
||||||
if not creds:
|
if not creds:
|
||||||
raise SystemExit(1)
|
raise SystemExit(1)
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
terminalai "github.com/1Panel-dev/1Panel/agent/utils/terminal/ai"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SyncLocalModelAccount(install model.AppInstall, accountID uint, baseURL string, models []dto.AgentAccountModel, updateBaseURL, createIfMissing bool) (*model.AgentAccount, error) {
|
||||||
|
if install.ID == 0 || (install.App.Key != "ollama" && install.App.Key != "vllm") {
|
||||||
|
return nil, fmt.Errorf("invalid local model installation")
|
||||||
|
}
|
||||||
|
agentAccountMutationMu.Lock()
|
||||||
|
defer agentAccountMutationMu.Unlock()
|
||||||
|
account, err := agentAccountRepo.GetFirst(repo.WithByAppInstallID(install.ID))
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) && accountID != 0 {
|
||||||
|
account, err = agentAccountRepo.GetFirst(repo.WithByID(accountID), repo.WithByProvider(install.App.Key))
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err == nil && account.AppInstallID != 0 && account.AppInstallID != install.ID {
|
||||||
|
return nil, fmt.Errorf("model account belongs to another installation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
if !createIfMissing {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
name := install.Name
|
||||||
|
for suffix := 1; ; suffix++ {
|
||||||
|
existing, err := agentAccountRepo.List(repo.WithByProvider(install.App.Key), repo.WithByName(name))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(existing) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
name = fmt.Sprintf("%s-%d", install.Name, suffix)
|
||||||
|
}
|
||||||
|
apiType, apiKey := "openai-completions", "vllm-local"
|
||||||
|
if install.App.Key == "ollama" {
|
||||||
|
apiType, apiKey = "openai-responses", "ollama-local"
|
||||||
|
}
|
||||||
|
account = &model.AgentAccount{Name: name, Provider: install.App.Key, BaseURL: baseURL, APIType: apiType, APIKey: apiKey, RememberAPIKey: true, Verified: true}
|
||||||
|
}
|
||||||
|
accountChanged := account.ID == 0 || account.AppInstallID != install.ID || (updateBaseURL && account.BaseURL != baseURL)
|
||||||
|
account.AppInstallID = install.ID
|
||||||
|
if updateBaseURL {
|
||||||
|
account.BaseURL = baseURL
|
||||||
|
}
|
||||||
|
rows := make([]model.AgentAccountModel, 0, len(models))
|
||||||
|
verifyModelExists := account.VerifyModel == ""
|
||||||
|
for _, item := range models {
|
||||||
|
rows = append(rows, model.AgentAccountModel{Model: item.ID, Name: item.Name})
|
||||||
|
if item.ID == account.VerifyModel {
|
||||||
|
verifyModelExists = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !verifyModelExists {
|
||||||
|
account.VerifyModel = ""
|
||||||
|
accountChanged = true
|
||||||
|
}
|
||||||
|
var previous []model.AgentAccountModel
|
||||||
|
if account.ID != 0 {
|
||||||
|
previous, err = agentAccountModelRepo.List(repo.WithByAccountID(account.ID), repo.WithOrderAsc("sort_order"), repo.WithOrderAsc("id"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modelsChanged := len(previous) != len(rows)
|
||||||
|
if !modelsChanged {
|
||||||
|
for i := range rows {
|
||||||
|
if previous[i].Model != rows[i].Model || previous[i].Name != rows[i].Name {
|
||||||
|
modelsChanged = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if modelsChanged {
|
||||||
|
err = agentAccountRepo.SaveLocal(account, rows)
|
||||||
|
} else if accountChanged {
|
||||||
|
err = agentAccountRepo.Save(account)
|
||||||
|
} else {
|
||||||
|
return account, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
terminalai.InvalidateTerminalRuntimeCache()
|
||||||
|
terminalai.InvalidateFileAIRuntimeCache()
|
||||||
|
return account, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SyncOllamaModelAccount(createIfMissing bool) error {
|
||||||
|
app, err := appRepo.GetFirst(appRepo.WithKey("ollama"))
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
install, err := appInstallRepo.GetFirst(appInstallRepo.WithAppId(app.ID))
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !install.SyncModelAccount {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return syncOllamaModelAccount(install, fmt.Sprintf("http://%s:11434/v1", install.ContainerName), false, createIfMissing)
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncOllamaModelAccount(install model.AppInstall, baseURL string, updateBaseURL, createIfMissing bool) error {
|
||||||
|
models, err := aiRepo.List(repo.WithByStatus(constant.StatusSuccess), repo.WithOrderAsc("name"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
items := make([]dto.AgentAccountModel, 0, len(models))
|
||||||
|
for _, item := range models {
|
||||||
|
items = append(items, dto.AgentAccountModel{ID: item.Name, Name: item.Name})
|
||||||
|
}
|
||||||
|
_, err = SyncLocalModelAccount(install, 0, baseURL, items, updateBaseURL, createIfMissing)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadOllamaModelAccountAddress(install model.AppInstall) (string, string, error) {
|
||||||
|
addressType := install.ModelAccountBaseURLType
|
||||||
|
if addressType == "" {
|
||||||
|
addressType = "container"
|
||||||
|
}
|
||||||
|
account, err := agentAccountRepo.GetFirst(repo.WithByAppInstallID(install.ID), repo.WithByProvider("ollama"))
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return addressType, install.ModelAccountBaseURL, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
if addressType != "custom" {
|
||||||
|
expectedURL, err := ResolveLocalModelAccountBaseURL(addressType, "", install.ContainerName, "11434", install.HttpPort)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
if strings.TrimRight(account.BaseURL, "/") != expectedURL {
|
||||||
|
addressType = "custom"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return addressType, account.BaseURL, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CleanupLocalModelAccount(installID uint) {
|
||||||
|
if installID == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
accounts, err := agentAccountRepo.List(repo.WithByAppInstallID(installID))
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Errorf("load local model accounts: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, account := range accounts {
|
||||||
|
if err := (AgentService{}).DeleteAccount(dto.AgentAccountDeleteReq{ID: account.ID}); err != nil {
|
||||||
|
global.LOG.Warnf("keep local model account %d after uninstall: %v", account.ID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResolveLocalModelAccountBaseURL(addressType, customURL, containerName, containerPort string, hostPort int) (string, error) {
|
||||||
|
switch addressType {
|
||||||
|
case "custom":
|
||||||
|
baseURL := strings.TrimRight(strings.TrimSpace(customURL), "/")
|
||||||
|
if !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") {
|
||||||
|
return "", fmt.Errorf("model account base URL must start with http:// or https://")
|
||||||
|
}
|
||||||
|
return baseURL, nil
|
||||||
|
case "localhost":
|
||||||
|
return fmt.Sprintf("http://127.0.0.1:%d/v1", hostPort), nil
|
||||||
|
case "systemIP":
|
||||||
|
systemIP, err := settingRepo.GetValueByKey("SystemIP")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
systemIP = strings.TrimSpace(systemIP)
|
||||||
|
if systemIP == "" {
|
||||||
|
systemIP = "127.0.0.1"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("http://%s:%d/v1", systemIP, hostPort), nil
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("http://%s:%s/v1", containerName, containerPort), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
providercatalog "github.com/1Panel-dev/1Panel/agent/app/provider"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMiniMaxOpenclawModelMetadata(t *testing.T) {
|
||||||
|
for _, provider := range []string{"minimax", "custom", "bailian-coding-plan"} {
|
||||||
|
for _, protocol := range []string{"anthropic-messages", "openai-completions"} {
|
||||||
|
for _, id := range []string{"MiniMax-M3.1-Flash", "MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.5", "saved-custom"} {
|
||||||
|
t.Run(provider+"/"+protocol+"/"+id, func(t *testing.T) {
|
||||||
|
account := &model.AgentAccount{Provider: provider, APIType: protocol, BaseURL: "https://example.test/v1", APIKey: "test-key"}
|
||||||
|
_, entry, _, _, err := buildOpenclawAccountModelConfig(account, dto.AgentAccountModel{ID: id, Name: "Saved name"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
payload, err := json.Marshal(entry)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var actual modelEntry
|
||||||
|
if err := json.Unmarshal(payload, &actual); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if actual.ID != id || actual.Name != "Saved name" {
|
||||||
|
t.Fatalf("changed saved identity: %s", payload)
|
||||||
|
}
|
||||||
|
if provider == "minimax" && id == "MiniMax-M3.1-Flash" {
|
||||||
|
if actual.ContextWindow != 1000000 || actual.MaxTokens != 524288 || !reflect.DeepEqual(actual.Input, []string{"text", "image"}) {
|
||||||
|
t.Fatalf("incorrect direct Flash metadata: %s", payload)
|
||||||
|
}
|
||||||
|
} else if actual.ContextWindow != 0 || actual.MaxTokens != 0 {
|
||||||
|
t.Fatalf("changed existing/gateway limits: %s", payload)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiniMaxPreservesSavedOpenclawMetadata(t *testing.T) {
|
||||||
|
current := &modelsConfig{Mode: "merge", Providers: map[string]modelProvider{
|
||||||
|
"minimax": {Models: []modelEntry{{ID: "MiniMax-M3.1-Flash", Input: []string{"text"}, ContextWindow: 32000, MaxTokens: 4096}}},
|
||||||
|
}}
|
||||||
|
conf := map[string]interface{}{}
|
||||||
|
if err := applyOpenclawModelsConfig(conf, current); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
next := &modelsConfig{Mode: "merge", Providers: map[string]modelProvider{
|
||||||
|
"minimax": {Models: []modelEntry{{ID: "MiniMax-M3.1-Flash", Input: []string{"text", "image"}, ContextWindow: 1000000, MaxTokens: 524288}}},
|
||||||
|
}}
|
||||||
|
preserveOpenclawModelMetadata(conf, next)
|
||||||
|
if !reflect.DeepEqual(next.Providers["minimax"].Models, current.Providers["minimax"].Models) {
|
||||||
|
t.Fatalf("saved metadata changed: %+v", next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiniMaxDefaultAccountRequest(t *testing.T) {
|
||||||
|
for _, protocol := range []string{"anthropic-messages", "openai-completions"} {
|
||||||
|
account := &model.AgentAccount{Provider: "minimax", APIType: protocol}
|
||||||
|
models, err := buildInitialAgentAccountModels(account, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
selected, err := resolveAgentAccountVerifyModel("minimax", "", models)
|
||||||
|
if err != nil || selected != "MiniMax-M3.1-Flash" || len(models) != 2 || models[1].ID != "MiniMax-M3" {
|
||||||
|
t.Fatalf("unexpected default: %s, %+v, %v", selected, models, err)
|
||||||
|
}
|
||||||
|
request := providercatalog.BuildVerifyRequest("minimax", protocol, "", "https://example.test", "test-key", selected)
|
||||||
|
var body map[string]interface{}
|
||||||
|
if err := json.Unmarshal(request.Body, &body); err != nil || body["model"] != selected {
|
||||||
|
t.Fatalf("default request: %s, %v", request.Body, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiniMaxHermesConfiguration(t *testing.T) {
|
||||||
|
for _, protocol := range []string{"anthropic-messages", "openai-completions"} {
|
||||||
|
for _, id := range []string{"MiniMax-M3.1-Flash", "MiniMax-M3", "MiniMax-M2.7", "my-custom-model"} {
|
||||||
|
dir := t.TempDir()
|
||||||
|
account := &model.AgentAccount{Provider: "minimax", APIType: protocol, BaseURL: "https://api.minimaxi.com/anthropic", APIKey: "test-key"}
|
||||||
|
if err := writeHermesConfig(dir, account, id, ""); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
conf, err := readHermesConfig(filepath.Join(dir, "config.yaml"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if conf.Model.Default != id || conf.Model.Provider != "minimax-cn" || conf.Model.BaseURL != account.BaseURL {
|
||||||
|
t.Fatalf("changed Hermes selection: %+v", conf.Model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiniMaxExplicitAccountModels(t *testing.T) {
|
||||||
|
for _, provider := range []string{"minimax", "bailian-coding-plan", "custom"} {
|
||||||
|
account := &model.AgentAccount{Provider: provider, APIType: "openai-completions"}
|
||||||
|
requested := []dto.AgentAccountModel{{ID: "MiniMax-M2.5", Name: "Saved legacy"}, {ID: "my-custom-model", Name: "Saved custom"}}
|
||||||
|
actual, err := buildInitialAgentAccountModels(account, requested)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(actual, requested) {
|
||||||
|
t.Fatalf("changed explicit %s models: %+v", provider, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
var deepSeekUsernamePattern = regexp.MustCompile(`^[A-Za-z0-9._-]+$`)
|
||||||
|
var deepSeekHostLabelPattern = regexp.MustCompile(`^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$`)
|
||||||
|
|
||||||
|
func isDeepSeekAccessHost(value string) bool {
|
||||||
|
if ip := net.ParseIP(value); ip != nil {
|
||||||
|
return ip.To4() != nil && !strings.Contains(value, ":")
|
||||||
|
}
|
||||||
|
if value == "" || len(value) > 253 || strings.Trim(value, "0123456789.") == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, label := range strings.Split(value, ".") {
|
||||||
|
if !deepSeekHostLabelPattern.MatchString(label) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateNativeAgentCreate(req dto.AgentCreateReq) error {
|
||||||
|
if req.AgentType != constant.AppDeepSeekHarness && req.AgentType != constant.AppOctop {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(req.DashboardUsername) == "" || strings.ContainsAny(req.DashboardUsername, "\r\n\x00") {
|
||||||
|
return buserr.WithDetail("ErrInvalidParams", "dashboardUsername", nil)
|
||||||
|
}
|
||||||
|
if strings.ContainsAny(req.DashboardPassword, "\r\n\x00") || req.DashboardPassword != strings.TrimSpace(req.DashboardPassword) {
|
||||||
|
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
|
||||||
|
}
|
||||||
|
if req.AgentType == constant.AppDeepSeekHarness {
|
||||||
|
if !isDeepSeekAccessHost(req.AccessHost) {
|
||||||
|
return buserr.WithDetail("ErrInvalidParams", "accessHost", nil)
|
||||||
|
}
|
||||||
|
if !deepSeekUsernamePattern.MatchString(req.DashboardUsername) {
|
||||||
|
return buserr.WithDetail("ErrInvalidParams", "dashboardUsername", nil)
|
||||||
|
}
|
||||||
|
if utf8.RuneCountInString(req.DashboardPassword) < 12 {
|
||||||
|
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
hasLetter, hasDigit := false, false
|
||||||
|
for _, char := range req.DashboardPassword {
|
||||||
|
hasLetter = hasLetter || unicode.IsLetter(char)
|
||||||
|
hasDigit = hasDigit || unicode.IsDigit(char)
|
||||||
|
}
|
||||||
|
if utf8.RuneCountInString(req.DashboardPassword) < 8 || !hasLetter || !hasDigit {
|
||||||
|
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
|
||||||
|
}
|
||||||
|
switch strings.ToLower(req.DashboardPassword) {
|
||||||
|
case "password", "password1", "password12", "password123", "12345678", "123456789", "qwerty123",
|
||||||
|
"admin123", "welcome1", "letmein1", "changeme1", "octop123", "abc12345", "iloveyou1":
|
||||||
|
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -107,6 +107,9 @@ func (a AgentService) InstallMarketPlugin(req dto.AgentPluginMarketInstallReq) e
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if isOpenclawBundledFeishuVersion(install.Version) && (strings.TrimSpace(req.Package) == "@larksuite/openclaw-lark" || strings.TrimSpace(req.Package) == "@openclaw/feishu") {
|
||||||
|
return buserr.New("ErrFeishuManaged")
|
||||||
|
}
|
||||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
|
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -132,6 +135,9 @@ func (a AgentService) OperatePlugin(req dto.AgentPluginOperateReq) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if isOpenclawBundledFeishuVersion(install.Version) && (req.PluginID == "openclaw-lark" || req.PluginID == "feishu") {
|
||||||
|
return buserr.New("ErrFeishuManaged")
|
||||||
|
}
|
||||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
|
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -97,7 +98,7 @@ func ensureContainerRunning(containerName string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, modelID string, validateAvailability bool) (resolvedAgentAccountInput, error) {
|
func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, modelID string, validateAvailability, allowEmptyModel bool) (resolvedAgentAccountInput, error) {
|
||||||
resolvedAPIKey := strings.TrimSpace(apiKey)
|
resolvedAPIKey := strings.TrimSpace(apiKey)
|
||||||
resolvedAPIType := strings.TrimSpace(apiType)
|
resolvedAPIType := strings.TrimSpace(apiType)
|
||||||
resolvedAuthMode, err := providercatalog.ResolveAuthMode(provider, resolvedAPIType, authMode)
|
resolvedAuthMode, err := providercatalog.ResolveAuthMode(provider, resolvedAPIType, authMode)
|
||||||
@@ -112,7 +113,7 @@ func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, mode
|
|||||||
return resolvedAgentAccountInput{}, err
|
return resolvedAgentAccountInput{}, err
|
||||||
}
|
}
|
||||||
modelID = strings.TrimSpace(modelID)
|
modelID = strings.TrimSpace(modelID)
|
||||||
if modelID == "" {
|
if modelID == "" && !allowEmptyModel {
|
||||||
return resolvedAgentAccountInput{}, buserr.New("ErrAgentAccountModelsRequired")
|
return resolvedAgentAccountInput{}, buserr.New("ErrAgentAccountModelsRequired")
|
||||||
}
|
}
|
||||||
imageAPI := providercatalog.IsImageAPIType(resolvedAPIType)
|
imageAPI := providercatalog.IsImageAPIType(resolvedAPIType)
|
||||||
@@ -147,6 +148,10 @@ func readOpenclawConfig(configPath string) (map[string]interface{}, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func writeOpenclawConfigRaw(configPath string, conf map[string]interface{}) error {
|
func writeOpenclawConfigRaw(configPath string, conf map[string]interface{}) error {
|
||||||
|
if _, busy := feishuSwitches.LoadOrStore(configPath, true); busy {
|
||||||
|
return buserr.New("TaskIsExecuting")
|
||||||
|
}
|
||||||
|
defer feishuSwitches.Delete(configPath)
|
||||||
ensureGatewaySecurityDefaults(conf)
|
ensureGatewaySecurityDefaults(conf)
|
||||||
ensureOpenclawUpdateDefaults(conf)
|
ensureOpenclawUpdateDefaults(conf)
|
||||||
payload, err := json.MarshalIndent(conf, "", " ")
|
payload, err := json.MarshalIndent(conf, "", " ")
|
||||||
@@ -414,6 +419,15 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
|
|||||||
} else {
|
} else {
|
||||||
item.WebUIPort = appInstall.HttpPort
|
item.WebUIPort = appInstall.HttpPort
|
||||||
}
|
}
|
||||||
|
if agentType == constant.AppDeepSeekHarness {
|
||||||
|
item.WebUIPort = appInstall.HttpsPort
|
||||||
|
if values, err := readAgentEnvMap(appInstall.GetEnvPath()); err == nil {
|
||||||
|
accessHost := values["HTTPS_ACCESS_HOST"]
|
||||||
|
if isDeepSeekAccessHost(accessHost) {
|
||||||
|
item.AccessHost = accessHost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
item.Path = appInstall.GetPath()
|
item.Path = appInstall.GetPath()
|
||||||
item.Status = appInstall.Status
|
item.Status = appInstall.Status
|
||||||
item.Message = appInstall.Message
|
item.Message = appInstall.Message
|
||||||
@@ -441,7 +455,8 @@ func localizedAgentProviderName(provider string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isAgentAppKey(appKey string) bool {
|
func isAgentAppKey(appKey string) bool {
|
||||||
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent
|
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent ||
|
||||||
|
appKey == constant.AppDeepSeekHarness || appKey == constant.AppOctop
|
||||||
}
|
}
|
||||||
|
|
||||||
func isOpenclawLegacyHTTPVersion(version string) bool {
|
func isOpenclawLegacyHTTPVersion(version string) bool {
|
||||||
@@ -651,7 +666,7 @@ func checkAgentUpgradable(install model.AppInstall) bool {
|
|||||||
if len(ignores) > 0 {
|
if len(ignores) > 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if common.IsCrossVersion(install.Version, item.Version) && !install.App.CrossVersionUpdate {
|
if isCrossAppVersion(install.App.Key, install.Version, item.Version) && !install.App.CrossVersionUpdate {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
versions = append(versions, item.Version)
|
versions = append(versions, item.Version)
|
||||||
@@ -659,12 +674,12 @@ func checkAgentUpgradable(install model.AppInstall) bool {
|
|||||||
if len(versions) == 0 {
|
if len(versions) == 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
versions = common.GetSortedVersions(versions)
|
versions = sortAppVersions(install.App.Key, versions)
|
||||||
lastVersion := versions[0]
|
lastVersion := versions[0]
|
||||||
if common.IsCrossVersion(install.Version, lastVersion) {
|
if isCrossAppVersion(install.App.Key, install.Version, lastVersion) {
|
||||||
return install.App.CrossVersionUpdate
|
return install.App.CrossVersionUpdate
|
||||||
}
|
}
|
||||||
return common.CompareVersion(lastVersion, install.Version)
|
return isNewerAppVersion(install.App.Key, lastVersion, install.Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
type openclawConfig struct {
|
type openclawConfig struct {
|
||||||
@@ -737,9 +752,11 @@ type modelProvider struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type modelEntry struct {
|
type modelEntry struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Input []string `json:"input,omitempty"`
|
Input []string `json:"input,omitempty"`
|
||||||
|
ContextWindow int `json:"contextWindow,omitempty"`
|
||||||
|
MaxTokens int `json:"maxTokens,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func requiresOpenclawProviderModels(provider string) bool {
|
func requiresOpenclawProviderModels(provider string) bool {
|
||||||
@@ -767,7 +784,7 @@ type browserConfig struct {
|
|||||||
DefaultProfile string `json:"defaultProfile"`
|
DefaultProfile string `json:"defaultProfile"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, token string, allowedOrigins []string, fallbacks []string) error {
|
func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, token string, allowedOrigins []string, fallbacks []string, metadata []dto.AgentModelMetadata) error {
|
||||||
if strings.TrimSpace(confDir) == "" {
|
if strings.TrimSpace(confDir) == "" {
|
||||||
return fmt.Errorf("config dir is required")
|
return fmt.Errorf("config dir is required")
|
||||||
}
|
}
|
||||||
@@ -852,6 +869,7 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
|
|||||||
}
|
}
|
||||||
conf = initial
|
conf = initial
|
||||||
} else {
|
} else {
|
||||||
|
preserveOpenclawModelMetadata(conf, cfg.Models)
|
||||||
if err := applyOpenclawModelsConfig(conf, cfg.Models); err != nil {
|
if err := applyOpenclawModelsConfig(conf, cfg.Models); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -906,6 +924,9 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
|
|||||||
if allowedOrigins != nil {
|
if allowedOrigins != nil {
|
||||||
setSecurityConfig(conf, dto.AgentSecurityConfig{AllowedOrigins: allowedOrigins})
|
setSecurityConfig(conf, dto.AgentSecurityConfig{AllowedOrigins: allowedOrigins})
|
||||||
}
|
}
|
||||||
|
if err := applyOpenclawModelMetadata(conf, account, metadata); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := writeOpenclawConfigRaw(configPath, conf); err != nil {
|
if err := writeOpenclawConfigRaw(configPath, conf); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -920,6 +941,144 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
|
|||||||
return writeAgentEnvMap(path.Join(confDir, ".env"), envMap, order)
|
return writeAgentEnvMap(path.Join(confDir, ".env"), envMap, order)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func readOpenclawModelsConfig(conf map[string]interface{}) *modelsConfig {
|
||||||
|
raw, ok := conf["models"]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
payload, err := json.Marshal(raw)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var models modelsConfig
|
||||||
|
if err := json.Unmarshal(payload, &models); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &models
|
||||||
|
}
|
||||||
|
|
||||||
|
func preserveOpenclawModelMetadata(conf map[string]interface{}, next *modelsConfig) {
|
||||||
|
current := readOpenclawModelsConfig(conf)
|
||||||
|
if current == nil || next == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for providerID, nextProvider := range next.Providers {
|
||||||
|
currentProvider, ok := current.Providers[providerID]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
byID := make(map[string]modelEntry, len(currentProvider.Models))
|
||||||
|
for _, entry := range currentProvider.Models {
|
||||||
|
byID[entry.ID] = entry
|
||||||
|
}
|
||||||
|
for index := range nextProvider.Models {
|
||||||
|
currentEntry, ok := byID[nextProvider.Models[index].ID]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nextProvider.Models[index].Input = currentEntry.Input
|
||||||
|
nextProvider.Models[index].ContextWindow = currentEntry.ContextWindow
|
||||||
|
nextProvider.Models[index].MaxTokens = currentEntry.MaxTokens
|
||||||
|
}
|
||||||
|
next.Providers[providerID] = nextProvider
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractOpenclawModelMetadata(conf map[string]interface{}, account *model.AgentAccount, accountModels []dto.AgentAccountModel) []dto.AgentModelMetadata {
|
||||||
|
result := make([]dto.AgentModelMetadata, 0, len(accountModels))
|
||||||
|
configured := readOpenclawModelsConfig(conf)
|
||||||
|
for _, item := range accountModels {
|
||||||
|
_, inferred, providerID, _, err := buildOpenclawAccountModelConfig(account, item)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
metadata := dto.AgentModelMetadata{Model: item.ID, InputMode: "auto"}
|
||||||
|
if configured != nil {
|
||||||
|
for _, entry := range configured.Providers[providerID].Models {
|
||||||
|
if entry.ID != inferred.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
metadata.ContextWindow = entry.ContextWindow
|
||||||
|
metadata.MaxTokens = entry.MaxTokens
|
||||||
|
if len(entry.Input) > 0 && !slices.Equal(entry.Input, inferred.Input) {
|
||||||
|
if slices.Contains(entry.Input, "image") {
|
||||||
|
metadata.InputMode = "image"
|
||||||
|
} else {
|
||||||
|
metadata.InputMode = "text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, metadata)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyOpenclawModelMetadata(conf map[string]interface{}, account *model.AgentAccount, requested []dto.AgentModelMetadata) error {
|
||||||
|
if len(requested) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
configured := readOpenclawModelsConfig(conf)
|
||||||
|
if configured == nil {
|
||||||
|
return fmt.Errorf("model metadata is not supported for provider %s", account.Provider)
|
||||||
|
}
|
||||||
|
accountModels, err := loadAgentAccountModels(account)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
available := make(map[string]dto.AgentAccountModel, len(accountModels))
|
||||||
|
for _, item := range accountModels {
|
||||||
|
available[item.ID] = item
|
||||||
|
}
|
||||||
|
seen := make(map[string]struct{}, len(requested))
|
||||||
|
for _, metadata := range requested {
|
||||||
|
item, ok := available[metadata.Model]
|
||||||
|
if !ok {
|
||||||
|
return buserr.New("ErrAgentModelNotInAccount")
|
||||||
|
}
|
||||||
|
if _, ok := seen[metadata.Model]; ok {
|
||||||
|
return fmt.Errorf("duplicate model metadata: %s", metadata.Model)
|
||||||
|
}
|
||||||
|
seen[metadata.Model] = struct{}{}
|
||||||
|
_, inferred, providerID, _, err := buildOpenclawAccountModelConfig(account, item)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
provider := configured.Providers[providerID]
|
||||||
|
found := false
|
||||||
|
for index := range provider.Models {
|
||||||
|
if provider.Models[index].ID != inferred.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
found = true
|
||||||
|
provider.Models[index].ContextWindow = metadata.ContextWindow
|
||||||
|
provider.Models[index].MaxTokens = metadata.MaxTokens
|
||||||
|
switch metadata.InputMode {
|
||||||
|
case "auto":
|
||||||
|
provider.Models[index].Input = inferred.Input
|
||||||
|
case "text":
|
||||||
|
provider.Models[index].Input = []string{"text"}
|
||||||
|
case "image":
|
||||||
|
provider.Models[index].Input = []string{"text", "image"}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported model input mode: %s", metadata.InputMode)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return buserr.New("ErrAgentModelNotInAccount")
|
||||||
|
}
|
||||||
|
configured.Providers[providerID] = provider
|
||||||
|
}
|
||||||
|
modelsMap, err := structToMap(configured)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
conf["models"] = modelsMap
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func resolveOpenclawFallbackModels(account *model.AgentAccount, primaryModel string, fallbackIDs []string) ([]string, error) {
|
func resolveOpenclawFallbackModels(account *model.AgentAccount, primaryModel string, fallbackIDs []string) ([]string, error) {
|
||||||
accountModels, err := loadAgentAccountModels(account)
|
accountModels, err := loadAgentAccountModels(account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1041,7 +1200,7 @@ func prepareOpenclawInstallFiles(appInstall *model.AppInstall, account *model.Ag
|
|||||||
return fmt.Errorf("app install is required")
|
return fmt.Errorf("app install is required")
|
||||||
}
|
}
|
||||||
confDir := path.Join(appInstall.GetPath(), "data", "conf")
|
confDir := path.Join(appInstall.GetPath(), "data", "conf")
|
||||||
if err := writeOpenclawConfig(confDir, account, modelName, token, allowedOrigins, nil); err != nil {
|
if err := writeOpenclawConfig(confDir, account, modelName, token, allowedOrigins, nil, nil); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
dataDir := path.Join(appInstall.GetPath(), "data")
|
dataDir := path.Join(appInstall.GetPath(), "data")
|
||||||
@@ -1108,7 +1267,12 @@ func buildOpenclawAccountModelConfig(account *model.AgentAccount, model dto.Agen
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", modelEntry{}, "", modelProvider{}, err
|
return "", modelEntry{}, "", modelProvider{}, err
|
||||||
}
|
}
|
||||||
return providerPatch.PrimaryModel, buildOpenclawModelEntry(providerPatch.ModelID, model), providerPatch.ProviderKey, modelProvider{
|
entry := buildOpenclawModelEntry(providerPatch.ModelID, model)
|
||||||
|
if account.Provider == "minimax" && providerPatch.ModelID == "MiniMax-M3.1-Flash" {
|
||||||
|
entry.ContextWindow = 1000000
|
||||||
|
entry.MaxTokens = 524288
|
||||||
|
}
|
||||||
|
return providerPatch.PrimaryModel, entry, providerPatch.ProviderKey, modelProvider{
|
||||||
ApiKey: providerPatch.APIKey,
|
ApiKey: providerPatch.APIKey,
|
||||||
BaseUrl: providerPatch.BaseURL,
|
BaseUrl: providerPatch.BaseURL,
|
||||||
Api: providerPatch.APIType,
|
Api: providerPatch.APIType,
|
||||||
@@ -1496,6 +1660,10 @@ func agentDashboardAuthEnvKeys(agentType string) (string, string, bool) {
|
|||||||
return hermesDashboardUsernameEnvKey, hermesDashboardPasswordEnvKey, true
|
return hermesDashboardUsernameEnvKey, hermesDashboardPasswordEnvKey, true
|
||||||
case constant.AppCopaw:
|
case constant.AppCopaw:
|
||||||
return qwenPawAuthUsernameEnvKey, qwenPawAuthPasswordEnvKey, true
|
return qwenPawAuthUsernameEnvKey, qwenPawAuthPasswordEnvKey, true
|
||||||
|
case constant.AppDeepSeekHarness:
|
||||||
|
return "DSH_AUTH_USERNAME", "DSH_AUTH_PASSWORD", true
|
||||||
|
case constant.AppOctop:
|
||||||
|
return "OCTOP_ADMIN_USERNAME", "OCTOP_DEFAULT_PASSWORD", true
|
||||||
default:
|
default:
|
||||||
return "", "", false
|
return "", "", false
|
||||||
}
|
}
|
||||||
|
|||||||
+107
-47
@@ -8,6 +8,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -28,13 +29,15 @@ import (
|
|||||||
|
|
||||||
type AIToolService struct{}
|
type AIToolService struct{}
|
||||||
|
|
||||||
|
var ollamaModelMu sync.Mutex
|
||||||
|
|
||||||
type IAIToolService interface {
|
type IAIToolService interface {
|
||||||
Search(search dto.SearchWithPage) (int64, []dto.OllamaModelInfo, error)
|
Search(search dto.SearchWithPage) (int64, []dto.OllamaModelInfo, error)
|
||||||
Create(req dto.OllamaModelName) error
|
Create(req dto.OllamaModelName) error
|
||||||
Close(name string) error
|
Close(name string) error
|
||||||
Recreate(req dto.OllamaModelName) error
|
Recreate(req dto.OllamaModelName) error
|
||||||
Delete(req dto.ForceDelete) error
|
Delete(req dto.ForceDelete) error
|
||||||
Sync() ([]dto.OllamaModelDropList, error)
|
SyncAccount(req dto.OllamaModelAccountSync) error
|
||||||
LoadDetail(name string) (string, error)
|
LoadDetail(name string) (string, error)
|
||||||
BindDomain(req dto.OllamaBindDomain) error
|
BindDomain(req dto.OllamaBindDomain) error
|
||||||
GetBindDomain(req dto.OllamaBindDomainReq) (*dto.OllamaBindDomainRes, error)
|
GetBindDomain(req dto.OllamaBindDomainReq) (*dto.OllamaBindDomainRes, error)
|
||||||
@@ -46,6 +49,7 @@ func NewIAIToolService() IAIToolService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *AIToolService) Search(req dto.SearchWithPage) (int64, []dto.OllamaModelInfo, error) {
|
func (u *AIToolService) Search(req dto.SearchWithPage) (int64, []dto.OllamaModelInfo, error) {
|
||||||
|
syncErr := syncOllamaModels()
|
||||||
var options []repo.DBOption
|
var options []repo.DBOption
|
||||||
if len(req.Info) != 0 {
|
if len(req.Info) != 0 {
|
||||||
options = append(options, repo.WithByLikeName(req.Info))
|
options = append(options, repo.WithByLikeName(req.Info))
|
||||||
@@ -60,6 +64,11 @@ func (u *AIToolService) Search(req dto.SearchWithPage) (int64, []dto.OllamaModel
|
|||||||
if err := copier.Copy(&item, &itemModel); err != nil {
|
if err := copier.Copy(&item, &itemModel); err != nil {
|
||||||
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
||||||
}
|
}
|
||||||
|
if syncErr != nil && item.Status != constant.StatusWaiting {
|
||||||
|
item.Status = constant.StatusUnknown
|
||||||
|
item.Message = syncErr.Error()
|
||||||
|
item.Size = ""
|
||||||
|
}
|
||||||
taskModel, _ := taskRepo.GetFirst(taskRepo.WithResourceID(item.ID), repo.WithByType(task.TaskScopeAI))
|
taskModel, _ := taskRepo.GetFirst(taskRepo.WithResourceID(item.ID), repo.WithByType(task.TaskScopeAI))
|
||||||
if len(taskModel.ID) != 0 {
|
if len(taskModel.ID) != 0 {
|
||||||
item.LogFileExist = true
|
item.LogFileExist = true
|
||||||
@@ -85,6 +94,8 @@ func (u *AIToolService) LoadDetail(name string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *AIToolService) Create(req dto.OllamaModelName) error {
|
func (u *AIToolService) Create(req dto.OllamaModelName) error {
|
||||||
|
ollamaModelMu.Lock()
|
||||||
|
defer ollamaModelMu.Unlock()
|
||||||
if cmd.CheckIllegal(req.Name) {
|
if cmd.CheckIllegal(req.Name) {
|
||||||
return buserr.New("ErrCmdIllegal")
|
return buserr.New("ErrCmdIllegal")
|
||||||
}
|
}
|
||||||
@@ -125,6 +136,9 @@ func (u *AIToolService) Create(req dto.OllamaModelName) error {
|
|||||||
if err := taskItem.Execute(); err != nil {
|
if err := taskItem.Execute(); err != nil {
|
||||||
_ = aiRepo.Update(info.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
_ = aiRepo.Update(info.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
||||||
}
|
}
|
||||||
|
if err := SyncOllamaModelAccount(false); err != nil {
|
||||||
|
global.LOG.Errorf("sync Ollama model account after pull: %v", err)
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -144,6 +158,8 @@ func (u *AIToolService) Close(name string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *AIToolService) Recreate(req dto.OllamaModelName) error {
|
func (u *AIToolService) Recreate(req dto.OllamaModelName) error {
|
||||||
|
ollamaModelMu.Lock()
|
||||||
|
defer ollamaModelMu.Unlock()
|
||||||
if cmd.CheckIllegal(req.Name) {
|
if cmd.CheckIllegal(req.Name) {
|
||||||
return buserr.New("ErrCmdIllegal")
|
return buserr.New("ErrCmdIllegal")
|
||||||
}
|
}
|
||||||
@@ -179,11 +195,16 @@ func (u *AIToolService) Recreate(req dto.OllamaModelName) error {
|
|||||||
if err := taskItem.Execute(); err != nil {
|
if err := taskItem.Execute(); err != nil {
|
||||||
_ = aiRepo.Update(modelInfo.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
_ = aiRepo.Update(modelInfo.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
||||||
}
|
}
|
||||||
|
if err := SyncOllamaModelAccount(false); err != nil {
|
||||||
|
global.LOG.Errorf("sync Ollama model account after pull: %v", err)
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *AIToolService) Delete(req dto.ForceDelete) error {
|
func (u *AIToolService) Delete(req dto.ForceDelete) error {
|
||||||
|
ollamaModelMu.Lock()
|
||||||
|
defer ollamaModelMu.Unlock()
|
||||||
ollamaList, _ := aiRepo.List(repo.WithByIDs(req.IDs))
|
ollamaList, _ := aiRepo.List(repo.WithByIDs(req.IDs))
|
||||||
if len(ollamaList) == 0 {
|
if len(ollamaList) == 0 {
|
||||||
return buserr.New("ErrRecordNotFound")
|
return buserr.New("ErrRecordNotFound")
|
||||||
@@ -202,55 +223,28 @@ func (u *AIToolService) Delete(req dto.ForceDelete) error {
|
|||||||
logItem := path.Join(global.Dir.DataDir, "log", "AITools", item.Name)
|
logItem := path.Join(global.Dir.DataDir, "log", "AITools", item.Name)
|
||||||
_ = os.Remove(logItem)
|
_ = os.Remove(logItem)
|
||||||
}
|
}
|
||||||
return nil
|
return SyncOllamaModelAccount(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *AIToolService) Sync() ([]dto.OllamaModelDropList, error) {
|
func (u *AIToolService) SyncAccount(req dto.OllamaModelAccountSync) error {
|
||||||
containerName, err := LoadContainerName()
|
install, err := appInstallRepo.GetFirst(repo.WithByID(req.ID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
stdout, err := cmd.RunDockerExecWithStdout(20*time.Second, containerName, "ollama", "list")
|
if install.App.Key != "ollama" {
|
||||||
|
return buserr.New("ErrInvalidParams")
|
||||||
|
}
|
||||||
|
baseURL, err := ResolveLocalModelAccountBaseURL(req.ModelAccountBaseURLType, req.ModelAccountBaseURL, install.ContainerName, "11434", install.HttpPort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
var list []model.OllamaModel
|
if err := syncOllamaModelAccount(install, baseURL, true, true); err != nil {
|
||||||
lines := strings.Split(stdout, "\n")
|
return err
|
||||||
for _, line := range lines {
|
|
||||||
parts := strings.Fields(line)
|
|
||||||
if len(parts) < 5 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if parts[0] == "NAME" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
list = append(list, model.OllamaModel{Name: parts[0], Size: parts[2] + " " + parts[3]})
|
|
||||||
}
|
}
|
||||||
listInDB, _ := aiRepo.List()
|
return appInstallRepo.BatchUpdateBy(map[string]interface{}{
|
||||||
var dropList []dto.OllamaModelDropList
|
"model_account_base_url_type": req.ModelAccountBaseURLType,
|
||||||
for _, itemModel := range listInDB {
|
"model_account_base_url": baseURL,
|
||||||
isExit := false
|
}, repo.WithByID(install.ID))
|
||||||
for i := 0; i < len(list); i++ {
|
|
||||||
if list[i].Name == itemModel.Name {
|
|
||||||
_ = aiRepo.Update(itemModel.ID, map[string]interface{}{"status": constant.StatusSuccess, "message": "", "size": list[i].Size})
|
|
||||||
list = append(list[:i], list[(i+1):]...)
|
|
||||||
isExit = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !isExit && itemModel.Status != constant.StatusWaiting {
|
|
||||||
_ = aiRepo.Update(itemModel.ID, map[string]interface{}{"status": constant.StatusDeleted, "message": "not exist", "size": ""})
|
|
||||||
dropList = append(dropList, dto.OllamaModelDropList{ID: itemModel.ID, Name: itemModel.Name})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, item := range list {
|
|
||||||
item.Status = constant.StatusSuccess
|
|
||||||
item.From = "remote"
|
|
||||||
_ = aiRepo.Create(&item)
|
|
||||||
}
|
|
||||||
|
|
||||||
return dropList, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *AIToolService) BindDomain(req dto.OllamaBindDomain) error {
|
func (u *AIToolService) BindDomain(req dto.OllamaBindDomain) error {
|
||||||
@@ -370,6 +364,72 @@ func (u *AIToolService) UpdateBindDomain(req dto.OllamaBindDomain) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func syncOllamaModels() error {
|
||||||
|
ollamaModelMu.Lock()
|
||||||
|
defer ollamaModelMu.Unlock()
|
||||||
|
containerName, err := LoadContainerName()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stdout, err := cmd.RunDockerExecWithStdout(20*time.Second, containerName, "ollama", "list")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
models := make(map[string]string)
|
||||||
|
lines := strings.Split(strings.TrimSpace(stdout), "\n")
|
||||||
|
if len(lines) == 0 || !strings.HasPrefix(strings.TrimSpace(lines[0]), "NAME") {
|
||||||
|
return fmt.Errorf("invalid ollama list output: %s", stdout)
|
||||||
|
}
|
||||||
|
for _, line := range lines[1:] {
|
||||||
|
if strings.TrimSpace(line) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.Fields(line)
|
||||||
|
if len(parts) < 5 {
|
||||||
|
return fmt.Errorf("invalid ollama model entry: %s", line)
|
||||||
|
}
|
||||||
|
models[parts[0]] = parts[2] + " " + parts[3]
|
||||||
|
}
|
||||||
|
list, err := aiRepo.List()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
known := make(map[string]bool, len(list))
|
||||||
|
for _, item := range list {
|
||||||
|
name := item.Name
|
||||||
|
if !strings.Contains(path.Base(name), ":") {
|
||||||
|
name += ":latest"
|
||||||
|
}
|
||||||
|
size, exists := models[name]
|
||||||
|
known[name] = true
|
||||||
|
if item.Status == constant.StatusWaiting {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
status, message := constant.StatusSuccess, ""
|
||||||
|
if !exists {
|
||||||
|
if item.Status == constant.StatusFailed || item.Status == constant.StatusCanceled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
status, message = constant.StatusDeleted, "not exist"
|
||||||
|
}
|
||||||
|
if item.Status != status || item.Size != size || item.Message != message {
|
||||||
|
if err := aiRepo.Update(item.ID, map[string]interface{}{"status": status, "size": size, "message": message}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for name, size := range models {
|
||||||
|
if known[name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
item := model.OllamaModel{Name: name, Size: size, From: "remote", Status: constant.StatusSuccess}
|
||||||
|
if err := aiRepo.Create(&item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func LoadContainerName() (string, error) {
|
func LoadContainerName() (string, error) {
|
||||||
ollamaBaseInfo, err := appInstallRepo.LoadBaseInfo("ollama", "")
|
ollamaBaseInfo, err := appInstallRepo.LoadBaseInfo("ollama", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -387,12 +447,12 @@ func loadModelSize(name string, containerName string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
lines := strings.Split(stdout, "\n")
|
lines := strings.Split(stdout, "\n")
|
||||||
|
if !strings.Contains(path.Base(name), ":") {
|
||||||
|
name += ":latest"
|
||||||
|
}
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
if !strings.Contains(line, name) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
parts := strings.Fields(line)
|
parts := strings.Fields(line)
|
||||||
if len(parts) < 5 {
|
if len(parts) < 5 || parts[0] != name {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return parts[2] + " " + parts[3], nil
|
return parts[2] + " " + parts[3], nil
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||||
|
"github.com/mattn/go-shellwords"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AIBenchmarkSources(kind, selected string) ([]dto.AIBenchmarkSource, error) {
|
||||||
|
if kind != "vllm" && kind != "ollama" {
|
||||||
|
return nil, errors.New("unsupported benchmark source")
|
||||||
|
}
|
||||||
|
selectedID := strings.SplitN(selected, ":", 2)[0]
|
||||||
|
type instance struct {
|
||||||
|
id uint
|
||||||
|
name, container string
|
||||||
|
port int
|
||||||
|
}
|
||||||
|
instances := []instance{}
|
||||||
|
apps, err := appRepo.GetBy(appRepo.WithKey(kind))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, app := range apps {
|
||||||
|
installs, err := appInstallRepo.ListBy(context.Background(), appInstallRepo.WithAppId(app.ID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, install := range installs {
|
||||||
|
if selected != "" && strconv.FormatUint(uint64(install.ID), 10) != selectedID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
instances = append(instances, instance{id: install.ID, name: install.Name, container: install.ContainerName, port: install.HttpPort})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results := []dto.AIBenchmarkSource{}
|
||||||
|
if len(instances) == 0 {
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
client, err := docker.NewDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
if !strings.HasPrefix(client.DaemonHost(), "unix://") {
|
||||||
|
return nil, errors.New("benchmark shortcuts require a Docker daemon on the main node; enter the remote service address manually")
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
for _, instance := range instances {
|
||||||
|
base := dto.AIBenchmarkSource{Kind: kind, Name: instance.name, Endpoint: "/v1/chat/completions", Models: []string{}, Status: "unknown"}
|
||||||
|
inspected, err := client.ContainerInspect(ctx, instance.container)
|
||||||
|
if err != nil {
|
||||||
|
base.ID = strconv.FormatUint(uint64(instance.id), 10)
|
||||||
|
base.Message = "container inspection unavailable"
|
||||||
|
results = append(results, base)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if inspected.State != nil {
|
||||||
|
base.Status = inspected.State.Status
|
||||||
|
}
|
||||||
|
args := []string{}
|
||||||
|
if inspected.Config != nil {
|
||||||
|
args = append(args, inspected.Config.Entrypoint...)
|
||||||
|
args = append(args, inspected.Config.Cmd...)
|
||||||
|
}
|
||||||
|
if len(args) >= 3 && (args[0] == "sh" || args[0] == "bash" || strings.HasSuffix(args[0], "/sh") || strings.HasSuffix(args[0], "/bash")) && args[1] == "-c" {
|
||||||
|
parser := shellwords.NewParser()
|
||||||
|
parser.ParseEnv = false
|
||||||
|
parser.ParseBacktick = false
|
||||||
|
if parsed, err := parser.Parse(args[2]); err == nil {
|
||||||
|
args = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flags := map[string]string{}
|
||||||
|
for i, arg := range args {
|
||||||
|
if key, value, ok := strings.Cut(arg, "="); ok && strings.HasPrefix(key, "--") {
|
||||||
|
flags[key] = value
|
||||||
|
} else if strings.HasPrefix(arg, "--") && i+1 < len(args) && !strings.HasPrefix(args[i+1], "--") {
|
||||||
|
flags[arg] = args[i+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if name := flags["--served-model-name"]; name != "" {
|
||||||
|
base.Models = []string{name}
|
||||||
|
}
|
||||||
|
tokenizer := flags["--tokenizer"]
|
||||||
|
if tokenizer == "" {
|
||||||
|
tokenizer = flags["--model"]
|
||||||
|
for i, arg := range args {
|
||||||
|
if tokenizer == "" && arg == "serve" && i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
|
||||||
|
tokenizer = args[i+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if kind != "ollama" && tokenizer != "" {
|
||||||
|
if !filepath.IsAbs(tokenizer) {
|
||||||
|
base.Tokenizer = tokenizer
|
||||||
|
} else {
|
||||||
|
longest := 0
|
||||||
|
for _, mount := range inspected.Mounts {
|
||||||
|
relative, err := filepath.Rel(mount.Destination, tokenizer)
|
||||||
|
if err == nil && relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator)) && len(mount.Destination) > longest {
|
||||||
|
candidate := filepath.Join(mount.Source, relative)
|
||||||
|
if info, err := os.Stat(candidate); err == nil && info.IsDir() {
|
||||||
|
base.Tokenizer = candidate
|
||||||
|
longest = len(mount.Destination)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ports := map[int]string{}
|
||||||
|
if instance.port > 0 {
|
||||||
|
ports[instance.port] = "127.0.0.1"
|
||||||
|
}
|
||||||
|
if inspected.NetworkSettings != nil {
|
||||||
|
for _, bindings := range inspected.NetworkSettings.Ports {
|
||||||
|
for _, binding := range bindings {
|
||||||
|
port, _ := strconv.Atoi(binding.HostPort)
|
||||||
|
if port > 0 {
|
||||||
|
host := binding.HostIP
|
||||||
|
if host == "" || host == "0.0.0.0" || host == "::" {
|
||||||
|
host = "127.0.0.1"
|
||||||
|
}
|
||||||
|
ports[port] = host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(ports) == 0 && inspected.HostConfig != nil && inspected.HostConfig.NetworkMode == "host" {
|
||||||
|
port, _ := strconv.Atoi(flags["--port"])
|
||||||
|
if port > 0 {
|
||||||
|
ports[port] = "127.0.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ordered := []int{}
|
||||||
|
for port := range ports {
|
||||||
|
ordered = append(ordered, port)
|
||||||
|
}
|
||||||
|
sort.Ints(ordered)
|
||||||
|
if len(ordered) == 0 {
|
||||||
|
ordered = append(ordered, 0)
|
||||||
|
}
|
||||||
|
for _, port := range ordered {
|
||||||
|
source := base
|
||||||
|
source.ID = fmt.Sprintf("%d:%d", instance.id, port)
|
||||||
|
if selected != "" && source.ID != selected {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if port > 0 {
|
||||||
|
source.BaseURL = "http://" + net.JoinHostPort(ports[port], strconv.Itoa(port))
|
||||||
|
}
|
||||||
|
if selected == source.ID && source.BaseURL != "" && source.Status == "running" {
|
||||||
|
endpoint := "/v1/models"
|
||||||
|
if kind == "ollama" {
|
||||||
|
endpoint = "/api/tags"
|
||||||
|
}
|
||||||
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, source.BaseURL+endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
response, err := (&http.Client{Timeout: 3 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}).Do(request)
|
||||||
|
if err != nil {
|
||||||
|
source.Message = "model list unavailable; enter the served model name"
|
||||||
|
} else {
|
||||||
|
raw, readErr := io.ReadAll(io.LimitReader(response.Body, 2*1024*1024))
|
||||||
|
response.Body.Close()
|
||||||
|
if response.StatusCode == http.StatusOK && readErr == nil {
|
||||||
|
var data struct {
|
||||||
|
Data []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `json:"data"`
|
||||||
|
Models []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"models"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &data); err == nil {
|
||||||
|
source.Models = []string{}
|
||||||
|
for _, model := range data.Data {
|
||||||
|
source.Models = append(source.Models, model.ID)
|
||||||
|
}
|
||||||
|
for _, model := range data.Models {
|
||||||
|
source.Models = append(source.Models, model.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
source.Message = fmt.Sprintf("model list HTTP %d; provide model name and credentials if required", response.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results = append(results, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||||
|
)
|
||||||
|
|
||||||
|
func prepareAIGatewayAppRecover(install *model.AppInstall, recoverFile, secret string) (string, func(), error) {
|
||||||
|
noop := func() {}
|
||||||
|
if !isAIGatewayApp(install.App.Key) {
|
||||||
|
return "", noop, nil
|
||||||
|
}
|
||||||
|
tmpDir, err := os.MkdirTemp(global.Dir.TmpDir, "ai-gateway-recover-")
|
||||||
|
if err != nil {
|
||||||
|
return "", noop, err
|
||||||
|
}
|
||||||
|
cleanup := func() { _ = os.RemoveAll(tmpDir) }
|
||||||
|
fail := func(err error) (string, func(), error) {
|
||||||
|
cleanup()
|
||||||
|
return "", noop, err
|
||||||
|
}
|
||||||
|
if err := files.NewFileOp().TarGzExtractPro(recoverFile, tmpDir, secret); err != nil {
|
||||||
|
return fail(err)
|
||||||
|
}
|
||||||
|
backupDir := filepath.Join(tmpDir, strings.ReplaceAll(filepath.Base(recoverFile), ".tar.gz", ""))
|
||||||
|
for _, name := range []string{"app.json", "app.tar.gz"} {
|
||||||
|
info, err := os.Lstat(filepath.Join(backupDir, name))
|
||||||
|
if err != nil || !info.Mode().IsRegular() {
|
||||||
|
return fail(buserr.New("AppBackupFileIncomplete"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
metadata, err := os.ReadFile(filepath.Join(backupDir, "app.json"))
|
||||||
|
if err != nil {
|
||||||
|
return fail(err)
|
||||||
|
}
|
||||||
|
var backup model.AppInstall
|
||||||
|
if err := json.Unmarshal(metadata, &backup); err != nil {
|
||||||
|
return fail(fmt.Errorf("unmarshal app.json failed: %w", err))
|
||||||
|
}
|
||||||
|
if backup.App.Key != install.App.Key || backup.Name != install.Name {
|
||||||
|
return fail(buserr.New("AppAttributesNotMatch"))
|
||||||
|
}
|
||||||
|
// Historical backups already include Version. Unsuffixed releases are
|
||||||
|
// SQLite; no new metadata field is required. Missing versions are invalid,
|
||||||
|
// not evidence that an archive contains SQLite data.
|
||||||
|
if strings.TrimSpace(backup.Version) == "" || strings.TrimSpace(install.Version) == "" {
|
||||||
|
return fail(buserr.New("AppBackupFileIncomplete"))
|
||||||
|
}
|
||||||
|
// Deliberately do not compare version order: restoring an older version of
|
||||||
|
// the same database type is required for failed-upgrade rollbacks.
|
||||||
|
if !isAIGatewayUpgradeVersionAllowed(install.App.Key, install.Version, backup.Version) {
|
||||||
|
return fail(buserr.New("ErrAIGatewayRecoverDatabaseTypeMismatch"))
|
||||||
|
}
|
||||||
|
return backupDir, cleanup, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
const aiGatewayAppKey = "ai-gateway"
|
||||||
|
|
||||||
|
func isAIGatewayApp(appKey string) bool {
|
||||||
|
return appKey == aiGatewayAppKey || appKey == "local"+aiGatewayAppKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAIGatewayPostgresVersion(version string) bool {
|
||||||
|
return strings.HasSuffix(version, "-pg") || strings.HasSuffix(version, "-postgres")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Database variants are deployment choices, not interchangeable upgrades.
|
||||||
|
func isAIGatewayUpgradeVersionAllowed(appKey, currentVersion, targetVersion string) bool {
|
||||||
|
return !isAIGatewayApp(appKey) ||
|
||||||
|
isAIGatewayPostgresVersion(currentVersion) == isAIGatewayPostgresVersion(targetVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortAIGatewayVersions(versions []string) {
|
||||||
|
sort.SliceStable(versions, func(i, j int) bool {
|
||||||
|
if common.CompareVersion(versions[i], versions[j]) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if common.CompareVersion(versions[j], versions[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Prefer SQLite only when the numeric versions are equal.
|
||||||
|
return !isAIGatewayPostgresVersion(versions[i]) && isAIGatewayPostgresVersion(versions[j])
|
||||||
|
})
|
||||||
|
}
|
||||||
+135
-135
@@ -4,11 +4,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"mime"
|
"mime"
|
||||||
"sort"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
@@ -17,14 +14,13 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||||
|
alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert"
|
||||||
alertconfig "github.com/1Panel-dev/1Panel/agent/utils/alert_config"
|
alertconfig "github.com/1Panel-dev/1Panel/agent/utils/alert_config"
|
||||||
alertwebhook "github.com/1Panel-dev/1Panel/agent/utils/alert_webhook"
|
alertwebhook "github.com/1Panel-dev/1Panel/agent/utils/alert_webhook"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/copier"
|
"github.com/1Panel-dev/1Panel/agent/utils/copier"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/email"
|
"github.com/1Panel-dev/1Panel/agent/utils/email"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
|
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/xpack/providers"
|
"github.com/1Panel-dev/1Panel/agent/utils/xpack/providers"
|
||||||
"github.com/shirou/gopsutil/v4/disk"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type AlertService struct{}
|
type AlertService struct{}
|
||||||
@@ -78,10 +74,10 @@ type IAlertService interface {
|
|||||||
GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertConfig, error)
|
GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertConfig, error)
|
||||||
PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error)
|
PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error)
|
||||||
UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error
|
UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error
|
||||||
UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) error
|
UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) (string, error)
|
||||||
DeleteAlertConfig(id uint) error
|
DeleteAlertConfig(id uint) (string, error)
|
||||||
TestAlertConfig(req dto.AlertConfigTest) (bool, error)
|
TestAlertConfig(req dto.AlertConfigTest) (bool, error)
|
||||||
TestCustomAlertConfig(req dto.AlertConfigTest) (dto.AlertConfigTestResult, error)
|
TestCustomAlertConfig(req dto.AlertConfigTest, lang string) (dto.AlertConfigTestResult, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIAlertService() IAlertService {
|
func NewIAlertService() IAlertService {
|
||||||
@@ -109,7 +105,38 @@ func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO,
|
|||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cronjobProjects := make(map[string]uint)
|
||||||
|
var cronjobIDs []uint
|
||||||
for _, item := range alerts {
|
for _, item := range alerts {
|
||||||
|
if alertUtil.GetCronJobType(item.Type) != "cronJob" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := cronjobProjects[item.Project]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
id, parseErr := strconv.ParseUint(item.Project, 10, strconv.IntSize)
|
||||||
|
if parseErr != nil || id == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cronjobProjects[item.Project] = uint(id)
|
||||||
|
cronjobIDs = append(cronjobIDs, uint(id))
|
||||||
|
}
|
||||||
|
cronjobsByID := make(map[uint]model.Cronjob)
|
||||||
|
if len(cronjobIDs) > 0 {
|
||||||
|
cronjobs, err := cronjobRepo.List(repo.WithByIDs(cronjobIDs))
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
for _, cronjob := range cronjobs {
|
||||||
|
cronjobsByID[cronjob.ID] = cronjob
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range alerts {
|
||||||
|
var taskName string
|
||||||
|
if cronjob, exists := cronjobsByID[cronjobProjects[item.Project]]; exists && cronjob.Type == item.Type {
|
||||||
|
taskName = cronjob.Name
|
||||||
|
}
|
||||||
|
|
||||||
result = append(result, dto.AlertDTO{
|
result = append(result, dto.AlertDTO{
|
||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
@@ -119,6 +146,7 @@ func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO,
|
|||||||
Method: item.Method,
|
Method: item.Method,
|
||||||
Title: item.Title,
|
Title: item.Title,
|
||||||
Project: item.Project,
|
Project: item.Project,
|
||||||
|
TaskName: taskName,
|
||||||
Status: item.Status,
|
Status: item.Status,
|
||||||
SendCount: item.SendCount,
|
SendCount: item.SendCount,
|
||||||
AdvancedParams: item.AdvancedParams,
|
AdvancedParams: item.AdvancedParams,
|
||||||
@@ -190,6 +218,16 @@ func (a AlertService) CreateAlert(create dto.AlertCreate, operator string) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
advanced, err := prepareCronJobAlertParams(create.Type, "", create.AdvancedParams)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
create.AdvancedParams = advanced
|
||||||
|
if create.Status != constant.AlertDisable {
|
||||||
|
if err := a.validateCronJobAlertChannels(create.Type, advanced, create.Method); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
alertInfo.Status = constant.AlertEnable
|
alertInfo.Status = constant.AlertEnable
|
||||||
if err := copier.Copy(&alertInfo, &create); err != nil {
|
if err := copier.Copy(&alertInfo, &create); err != nil {
|
||||||
return buserr.WithErr("ErrStructTransform", err)
|
return buserr.WithErr("ErrStructTransform", err)
|
||||||
@@ -207,11 +245,24 @@ func (a AlertService) CreateAlert(create dto.AlertCreate, operator string) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) UpdateAlert(req dto.AlertUpdate, operator string) error {
|
func (a AlertService) UpdateAlert(req dto.AlertUpdate, operator string) error {
|
||||||
|
if alertUtil.GetCronJobType(req.Type) == "cronJob" {
|
||||||
|
previous, err := alertRepo.Get(repo.WithByID(req.ID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.AdvancedParams, err = prepareCronJobAlertParams(req.Type, previous.AdvancedParams, req.AdvancedParams)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
methodTypes, err := a.validateAlertMethodReferences(req.Method)
|
methodTypes, err := a.validateAlertMethodReferences(req.Method)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if req.Status != constant.AlertDisable {
|
if req.Status != constant.AlertDisable {
|
||||||
|
if err := a.validateCronJobAlertChannels(req.Type, req.AdvancedParams, req.Method); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
|
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -278,6 +329,9 @@ func (a AlertService) UpdateStatus(id uint, status string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if status == constant.AlertEnable {
|
if status == constant.AlertEnable {
|
||||||
|
if err := a.validateCronJobAlertChannels(alertInfo.Type, alertInfo.AdvancedParams, alertInfo.Method); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
|
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -299,122 +353,14 @@ func (a AlertService) UpdateStatus(id uint, status string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) GetDisks() ([]dto.DiskDTO, error) {
|
func (a AlertService) GetDisks() ([]dto.DiskDTO, error) {
|
||||||
var disks []dto.DiskDTO
|
infos := loadDiskInfo(true)
|
||||||
excludes := map[string]struct{}{
|
disks := make([]dto.DiskDTO, 0, len(infos))
|
||||||
"/mnt/cdrom": {}, "/boot": {}, "/boot/efi": {}, "/dev": {}, "/dev/shm": {},
|
for _, item := range infos {
|
||||||
"/run/lock": {}, "/run": {}, "/run/shm": {}, "/run/user": {},
|
disks = append(disks, dto.DiskDTO(item))
|
||||||
}
|
}
|
||||||
stdout, err := executeDiskCommand()
|
|
||||||
if err != nil {
|
|
||||||
return disks, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
lines := strings.Split(stdout, "\n")
|
|
||||||
var mounts []dto.AlertDiskInfo
|
|
||||||
|
|
||||||
for _, line := range lines {
|
|
||||||
fields := strings.Fields(line)
|
|
||||||
if len(fields) < 7 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
mountPoint := strings.Join(fields[6:], " ")
|
|
||||||
if shouldExclude(fields, mountPoint, excludes) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
mounts = append(mounts, dto.AlertDiskInfo{Type: fields[1], Device: fields[0], Mount: mountPoint})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
wg sync.WaitGroup
|
|
||||||
mu sync.Mutex
|
|
||||||
)
|
|
||||||
wg.Add(len(mounts))
|
|
||||||
for i := 0; i < len(mounts); i++ {
|
|
||||||
go func(timeoutCh <-chan time.Time, mount dto.AlertDiskInfo) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
var itemData dto.DiskDTO
|
|
||||||
itemData.Path = mount.Mount
|
|
||||||
itemData.Type = mount.Type
|
|
||||||
itemData.Device = mount.Device
|
|
||||||
select {
|
|
||||||
case <-timeoutCh:
|
|
||||||
mu.Lock()
|
|
||||||
disks = append(disks, itemData)
|
|
||||||
mu.Unlock()
|
|
||||||
global.LOG.Errorf("load disk info from %s failed, err: timeout", mount.Mount)
|
|
||||||
default:
|
|
||||||
state, err := disk.Usage(mount.Mount)
|
|
||||||
if err != nil {
|
|
||||||
mu.Lock()
|
|
||||||
disks = append(disks, itemData)
|
|
||||||
mu.Unlock()
|
|
||||||
global.LOG.Errorf("load disk info from %s failed, err: %v", mount.Mount, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
itemData.Total = state.Total
|
|
||||||
itemData.Free = state.Free
|
|
||||||
itemData.Used = state.Used
|
|
||||||
itemData.UsedPercent = state.UsedPercent
|
|
||||||
itemData.InodesTotal = state.InodesTotal
|
|
||||||
itemData.InodesUsed = state.InodesUsed
|
|
||||||
itemData.InodesFree = state.InodesFree
|
|
||||||
itemData.InodesUsedPercent = state.InodesUsedPercent
|
|
||||||
mu.Lock()
|
|
||||||
disks = append(disks, itemData)
|
|
||||||
mu.Unlock()
|
|
||||||
}
|
|
||||||
}(time.After(5*time.Second), mounts[i])
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
sort.Slice(disks, func(i, j int) bool {
|
|
||||||
return disks[i].Path < disks[j].Path
|
|
||||||
})
|
|
||||||
return disks, nil
|
return disks, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func executeDiskCommand() (string, error) {
|
|
||||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second))
|
|
||||||
stdout, err := cmdMgr.RunWithStdout("df", "-hT", "-P")
|
|
||||||
if err != nil {
|
|
||||||
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
|
|
||||||
stdout, err = cmdMgr2.RunWithStdout("df", "-lhT", "-P")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return stdout, err
|
|
||||||
}
|
|
||||||
var lines []string
|
|
||||||
for _, line := range strings.Split(stdout, "\n") {
|
|
||||||
if !strings.Contains(line, "/") || strings.Contains(line, "tmpfs") || strings.Contains(line, "snap/core") || strings.Contains(line, "udev") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
lines = append(lines, line)
|
|
||||||
}
|
|
||||||
if len(lines) == 0 {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
return strings.Join(lines, "\n"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func shouldExclude(fields []string, mountPoint string, excludes map[string]struct{}) bool {
|
|
||||||
if strings.HasPrefix(mountPoint, "/snap") || len(strings.Split(mountPoint, "/")) > 10 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(fields[1]) == "tmpfs" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if strings.Contains(fields[2], "K") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if strings.Contains(mountPoint, "docker") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
_, excluded := excludes[mountPoint]
|
|
||||||
return excluded
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a AlertService) PageAlertLogs(search dto.AlertLogSearch) (int64, []dto.AlertLogDTO, error) {
|
func (a AlertService) PageAlertLogs(search dto.AlertLogSearch) (int64, []dto.AlertLogDTO, error) {
|
||||||
var (
|
var (
|
||||||
opts []repo.DBOption
|
opts []repo.DBOption
|
||||||
@@ -677,25 +623,26 @@ func (a AlertService) updateCustomAlertConfig(req dto.AlertConfigUpdate, operato
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) error {
|
func (a AlertService) UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) (string, error) {
|
||||||
if err := validateAlertConfigStatus(req.Status); err != nil {
|
if err := validateAlertConfigStatus(req.Status); err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
config, err := alertRepo.GetConfigById(req.ID)
|
config, err := alertRepo.GetConfigById(req.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
|
name := alertConfigLogName(config.Config, config.Title)
|
||||||
if req.Status == constant.AlertEnable {
|
if req.Status == constant.AlertEnable {
|
||||||
if err := a.validateCommunityAlertConfigType(config.Type); err != nil {
|
if err := a.validateCommunityAlertConfigType(config.Type); err != nil {
|
||||||
return err
|
return name, err
|
||||||
}
|
}
|
||||||
if config.Type == constant.Custom {
|
if config.Type == constant.Custom {
|
||||||
if _, err := alertwebhook.Resolve(config); err != nil {
|
if _, err := alertwebhook.Resolve(config); err != nil {
|
||||||
return err
|
return name, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return alertRepo.UpdateAlertConfig(map[string]interface{}{
|
return name, alertRepo.UpdateAlertConfig(map[string]interface{}{
|
||||||
"status": req.Status,
|
"status": req.Status,
|
||||||
"update_user": operator,
|
"update_user": operator,
|
||||||
}, repo.WithByID(req.ID))
|
}, repo.WithByID(req.ID))
|
||||||
@@ -867,19 +814,20 @@ func alertConfigSMSPhone(configData string) string {
|
|||||||
return strings.TrimSpace(cfg.Phone)
|
return strings.TrimSpace(cfg.Phone)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) DeleteAlertConfig(id uint) error {
|
func (a AlertService) DeleteAlertConfig(id uint) (string, error) {
|
||||||
_, err := alertRepo.GetConfigById(id)
|
config, err := alertRepo.GetConfigById(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
|
name := alertConfigLogName(config.Config, config.Title)
|
||||||
usedAlerts, err := alertRepo.List(alertRepo.WithByAlertMethodContainsConfigID(id))
|
usedAlerts, err := alertRepo.List(alertRepo.WithByAlertMethodContainsConfigID(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return name, err
|
||||||
}
|
}
|
||||||
if len(usedAlerts) > 0 {
|
if len(usedAlerts) > 0 {
|
||||||
return buserr.New("ErrAlertConfigInUse")
|
return name, buserr.New("ErrAlertConfigInUse")
|
||||||
}
|
}
|
||||||
return alertRepo.DeleteAlertConfig(repo.WithByID(id))
|
return name, alertRepo.DeleteAlertConfig(repo.WithByID(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
|
func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
|
||||||
@@ -905,7 +853,7 @@ func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
|
|||||||
|
|
||||||
msg := email.EmailMessage{
|
msg := email.EmailMessage{
|
||||||
Subject: i18n.GetMsgByKey("TestAlertTitle"),
|
Subject: i18n.GetMsgByKey("TestAlertTitle"),
|
||||||
Body: i18n.GetMsgByKey("TestAlert"),
|
Body: i18n.GetMsgByKey("TestAlertMessage"),
|
||||||
IsHTML: false,
|
IsHTML: false,
|
||||||
}
|
}
|
||||||
transport := xpack.MultiNodeProvider.LoadRequestTransport()
|
transport := xpack.MultiNodeProvider.LoadRequestTransport()
|
||||||
@@ -953,7 +901,7 @@ func resolveEmailTestConfig(req dto.AlertConfigTest) (dto.AlertEmailConfig, erro
|
|||||||
return emailConfig, nil
|
return emailConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) TestCustomAlertConfig(req dto.AlertConfigTest) (dto.AlertConfigTestResult, error) {
|
func (a AlertService) TestCustomAlertConfig(req dto.AlertConfigTest, lang string) (dto.AlertConfigTestResult, error) {
|
||||||
if req.Type != constant.Custom {
|
if req.Type != constant.Custom {
|
||||||
return dto.AlertConfigTestResult{}, fmt.Errorf("alert config test type must be custom")
|
return dto.AlertConfigTestResult{}, fmt.Errorf("alert config test type must be custom")
|
||||||
}
|
}
|
||||||
@@ -982,12 +930,15 @@ func (a AlertService) TestCustomAlertConfig(req dto.AlertConfigTest) (dto.AlertC
|
|||||||
}
|
}
|
||||||
tester, ok := xpack.AlertProvider.(providers.CustomWebhookTester)
|
tester, ok := xpack.AlertProvider.(providers.CustomWebhookTester)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
if global.LOG != nil {
|
||||||
|
global.LOG.Warnf("webhook test failed: %v", providers.ErrCustomWebhookUnsupported)
|
||||||
|
}
|
||||||
return dto.AlertConfigTestResult{
|
return dto.AlertConfigTestResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
Message: providers.ErrCustomWebhookUnsupported.Error(),
|
Message: providers.ErrCustomWebhookUnsupported.Error(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
return tester.TestCustomWebhook(resolved)
|
return tester.TestCustomWebhook(resolved, lang)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator string) error {
|
func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator string) error {
|
||||||
@@ -1021,6 +972,23 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator
|
|||||||
alertRepo.WithByType(updateAlert.Type),
|
alertRepo.WithByType(updateAlert.Type),
|
||||||
alertRepo.WithByProject(updateAlert.Project),
|
alertRepo.WithByProject(updateAlert.Project),
|
||||||
)
|
)
|
||||||
|
advanced, err := prepareCronJobAlertParams(updateAlert.Type, alertInfo.AdvancedParams, updateAlert.AdvancedParams)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
updateAlert.AdvancedParams = advanced
|
||||||
|
if alertUtil.GetCronJobType(updateAlert.Type) == "cronJob" {
|
||||||
|
upMap["advanced_params"] = advanced
|
||||||
|
}
|
||||||
|
if newStatus == constant.AlertEnable {
|
||||||
|
method := updateAlert.Method
|
||||||
|
if method == "" {
|
||||||
|
method = alertInfo.Method
|
||||||
|
}
|
||||||
|
if err := a.validateCronJobAlertChannels(updateAlert.Type, advanced, method); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if alertInfo.ID > 0 {
|
if alertInfo.ID > 0 {
|
||||||
shouldUpdate := false
|
shouldUpdate := false
|
||||||
@@ -1034,6 +1002,9 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator
|
|||||||
if val, ok := upMap["method"]; ok && val != "" && val != alertInfo.Method {
|
if val, ok := upMap["method"]; ok && val != "" && val != alertInfo.Method {
|
||||||
shouldUpdate = true
|
shouldUpdate = true
|
||||||
}
|
}
|
||||||
|
if val, ok := upMap["advanced_params"]; ok && val != alertInfo.AdvancedParams {
|
||||||
|
shouldUpdate = true
|
||||||
|
}
|
||||||
|
|
||||||
if shouldUpdate {
|
if shouldUpdate {
|
||||||
if err := alertRepo.Update(
|
if err := alertRepo.Update(
|
||||||
@@ -1055,3 +1026,32 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func prepareCronJobAlertParams(alertType, previous, incoming string) (string, error) {
|
||||||
|
if alertUtil.GetCronJobType(alertType) != "cronJob" {
|
||||||
|
return incoming, nil
|
||||||
|
}
|
||||||
|
return alertUtil.MergeCronJobAlertParams(previous, incoming)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AlertService) validateCronJobAlertChannels(alertType, advanced, method string) error {
|
||||||
|
if alertUtil.GetCronJobType(alertType) != "cronJob" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
mode, err := alertUtil.CronJobAlertTriggerMode(advanced)
|
||||||
|
if err != nil || mode != alertUtil.CronJobAlertSuccess {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = a.validateAlertMethodReferences(method)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func alertConfigLogName(config, title string) string {
|
||||||
|
var value struct {
|
||||||
|
DisplayName string `json:"displayName"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal([]byte(config), &value) == nil && strings.TrimSpace(value.DisplayName) != "" {
|
||||||
|
return strings.TrimSpace(value.DisplayName)
|
||||||
|
}
|
||||||
|
return title
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,10 +30,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ResourceAlertInterval = 30
|
ResourceAlertInterval = 30
|
||||||
CheckIntervalSec = 3
|
CheckIntervalSec = 3
|
||||||
LoadCheckIntervalMin = 5
|
LoadCheckIntervalMin = 5
|
||||||
sshIPLoginWindow = 30 * time.Minute
|
sshIPLoginWindow = 30 * time.Minute
|
||||||
|
sslAutoRenewAlertSkipDays = 31
|
||||||
)
|
)
|
||||||
|
|
||||||
type AlertTaskHelper struct {
|
type AlertTaskHelper struct {
|
||||||
@@ -529,11 +530,15 @@ func loadSSHLogin(alert dto.AlertDTO) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
global.LOG.Errorf("Failed to load ssh login logs: %v", err)
|
global.LOG.Errorf("Failed to load ssh login logs: %v", err)
|
||||||
}
|
}
|
||||||
|
interfaceAddrs, err := net.InterfaceAddrs()
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Warnf("Failed to load local IP addresses for ssh login alert: %v", err)
|
||||||
|
}
|
||||||
count, records := summarizeSSHLoginHistories(
|
count, records := summarizeSSHLoginHistories(
|
||||||
histories,
|
histories,
|
||||||
now,
|
now,
|
||||||
failedWindow,
|
failedWindow,
|
||||||
strings.Split(strings.TrimSpace(alert.AdvancedParams), "\n"),
|
sshSuccessLoginWhitelist(alert.AdvancedParams, interfaceAddrs),
|
||||||
)
|
)
|
||||||
isAlert := count >= int(alert.Count)
|
isAlert := count >= int(alert.Count)
|
||||||
if isAlert {
|
if isAlert {
|
||||||
@@ -569,6 +574,19 @@ func loadSSHLogin(alert dto.AlertDTO) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sshSuccessLoginWhitelist(configured string, interfaceAddrs []net.Addr) []string {
|
||||||
|
whitelist := strings.Split(strings.TrimSpace(configured), "\n")
|
||||||
|
whitelist = append(whitelist, "127.0.0.0/8", "::1")
|
||||||
|
for _, addr := range interfaceAddrs {
|
||||||
|
ipNet, ok := addr.(*net.IPNet)
|
||||||
|
if !ok || ipNet.IP == nil || ipNet.IP.IsUnspecified() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
whitelist = append(whitelist, ipNet.IP.String())
|
||||||
|
}
|
||||||
|
return whitelist
|
||||||
|
}
|
||||||
|
|
||||||
func filterLoginLogsNotInWhitelist(records []model.LoginLog, whitelist []string) []model.LoginLog {
|
func filterLoginLogsNotInWhitelist(records []model.LoginLog, whitelist []string) []model.LoginLog {
|
||||||
filtered := make([]model.LoginLog, 0, len(records))
|
filtered := make([]model.LoginLog, 0, len(records))
|
||||||
for _, record := range records {
|
for _, record := range records {
|
||||||
@@ -864,7 +882,7 @@ func calculateSSLExpiryDays(sslList []model.WebsiteSSL, cycle uint) (map[int][]s
|
|||||||
daysDiff := int(math.Ceil(
|
daysDiff := int(math.Ceil(
|
||||||
ssl.ExpireDate.Sub(currentDate).Hours() / 24,
|
ssl.ExpireDate.Sub(currentDate).Hours() / 24,
|
||||||
))
|
))
|
||||||
if daysDiff > 0 && int(cycle) >= daysDiff {
|
if daysDiff > 0 && int(cycle) >= daysDiff && !shouldSuppressSSLExpiryAlert(ssl, daysDiff) {
|
||||||
daysDiffMap[daysDiff] = append(daysDiffMap[daysDiff], ssl.PrimaryDomain)
|
daysDiffMap[daysDiff] = append(daysDiffMap[daysDiff], ssl.PrimaryDomain)
|
||||||
projectMap[ssl.ID] = append(projectMap[ssl.ID], ssl.ExpireDate)
|
projectMap[ssl.ID] = append(projectMap[ssl.ID], ssl.ExpireDate)
|
||||||
}
|
}
|
||||||
@@ -872,6 +890,10 @@ func calculateSSLExpiryDays(sslList []model.WebsiteSSL, cycle uint) (map[int][]s
|
|||||||
return daysDiffMap, projectMap
|
return daysDiffMap, projectMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func shouldSuppressSSLExpiryAlert(ssl model.WebsiteSSL, remainingDays int) bool {
|
||||||
|
return ssl.AutoRenew && remainingDays < sslAutoRenewAlertSkipDays
|
||||||
|
}
|
||||||
|
|
||||||
func calculateWebsiteExpiryDays(websites []model.Website, cycle uint) (map[int][]string, map[uint][]time.Time) {
|
func calculateWebsiteExpiryDays(websites []model.Website, cycle uint) (map[int][]string, map[uint][]time.Time) {
|
||||||
currentDate := time.Now()
|
currentDate := time.Now()
|
||||||
daysDiffMap := make(map[int][]string)
|
daysDiffMap := make(map[int][]string)
|
||||||
@@ -1044,50 +1066,37 @@ func processAllDisks(alert dto.AlertDTO) error {
|
|||||||
global.LOG.Errorf("error getting disk list, err: %v", err)
|
global.LOG.Errorf("error getting disk list, err: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var errMsgs []string
|
|
||||||
for _, item := range diskList {
|
for _, item := range diskList {
|
||||||
err := checkAndCreateDiskAlert(alert, item.Path)
|
if item.Total == 0 {
|
||||||
if err != nil {
|
|
||||||
errMsg := fmt.Sprintf("disk path %s process failed: %v", item.Path, err)
|
|
||||||
errMsgs = append(errMsgs, errMsg)
|
|
||||||
global.LOG.Errorf("%s", errMsg)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
checkAndCreateDiskAlert(alert, item.Path, &disk.UsageStat{Used: item.Used, UsedPercent: item.UsedPercent})
|
||||||
if len(errMsgs) > 0 {
|
|
||||||
return fmt.Errorf("batch process disks failed, error count: %d, details: %s", len(errMsgs), strings.Join(errMsgs, "; "))
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func processSingleDisk(alert dto.AlertDTO) error {
|
func processSingleDisk(alert dto.AlertDTO) error {
|
||||||
err := checkAndCreateDiskAlert(alert, alert.Project)
|
usageStat, err := loadDiskUsageWithTimeout(alert.Project, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
global.LOG.Errorf("%s", err.Error())
|
global.LOG.Errorf("error getting disk usage for %s, err: %v", alert.Project, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
checkAndCreateDiskAlert(alert, alert.Project, usageStat)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkAndCreateDiskAlert(alert dto.AlertDTO, path string) error {
|
func checkAndCreateDiskAlert(alert dto.AlertDTO, path string, usageStat *disk.UsageStat) {
|
||||||
usageStat, err := psutil.DISK.GetUsage(path, false)
|
|
||||||
if err != nil {
|
|
||||||
global.LOG.Errorf("error getting disk usage for %s, err: %v", path, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
usedTotal, usedStr := calculateUsedTotal(alert.Cycle, usageStat)
|
usedTotal, usedStr := calculateUsedTotal(alert.Cycle, usageStat)
|
||||||
commonTotal := float64(alert.Count)
|
commonTotal := float64(alert.Count)
|
||||||
if alert.Cycle == 1 {
|
if alert.Cycle == 1 {
|
||||||
commonTotal *= 1024 * 1024 * 1024
|
commonTotal *= 1024 * 1024 * 1024
|
||||||
}
|
}
|
||||||
if usedTotal < commonTotal {
|
if usedTotal < commonTotal {
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
params := createAlertDiskParams(path, usedStr)
|
params := createAlertDiskParams(path, usedStr)
|
||||||
sender := NewAlertSender(alert, alert.Project)
|
sender := NewAlertSender(alert, alert.Project)
|
||||||
sender.ResourceSend(path, params)
|
sender.ResourceSend(path, params)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func calculateUsedTotal(cycle uint, usageStat *disk.UsageStat) (float64, string) {
|
func calculateUsedTotal(cycle uint, usageStat *disk.UsageStat) (float64, string) {
|
||||||
|
|||||||
+116
-77
@@ -59,7 +59,8 @@ type IAppService interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type appInstallHooks struct {
|
type appInstallHooks struct {
|
||||||
AfterCopyData func(appInstall *model.AppInstall) error
|
AfterCopyData func(appInstall *model.AppInstall) error
|
||||||
|
PrepareSiteDir func(model.App, model.AppDetail, request.AppInstallCreate, *task.Task) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIAppService() IAppService {
|
func NewIAppService() IAppService {
|
||||||
@@ -134,6 +135,7 @@ func (a AppService) PageApp(ctx *gin.Context, req request.AppSearch) (*response.
|
|||||||
ID: ap.ID,
|
ID: ap.ID,
|
||||||
Name: ap.Name,
|
Name: ap.Name,
|
||||||
Key: ap.Key,
|
Key: ap.Key,
|
||||||
|
Icon: appicon.RemoteURL(ap.Icon),
|
||||||
Limit: ap.Limit,
|
Limit: ap.Limit,
|
||||||
GpuSupport: ap.GpuSupport,
|
GpuSupport: ap.GpuSupport,
|
||||||
Recommend: ap.Recommend,
|
Recommend: ap.Recommend,
|
||||||
@@ -258,26 +260,45 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
|
|||||||
fileOp := files.NewFileOp()
|
fileOp := files.NewFileOp()
|
||||||
|
|
||||||
versionPath := filepath.Join(app.GetAppResourcePath(), detail.Version)
|
versionPath := filepath.Join(app.GetAppResourcePath(), detail.Version)
|
||||||
if !fileOp.Stat(versionPath) || detail.Update {
|
if err = downloadApp(app, detail, nil, nil); err != nil && !fileOp.Stat(versionPath) {
|
||||||
if err = downloadApp(app, detail, nil, nil); err != nil && !fileOp.Stat(versionPath) {
|
return appDetailDTO, err
|
||||||
|
}
|
||||||
|
packageReady := err == nil
|
||||||
|
if packageReady && app.Resource == constant.AppResourceRemote {
|
||||||
|
detail.Update = false
|
||||||
|
appDetailDTO.Update = false
|
||||||
|
}
|
||||||
|
if packageReady && strings.TrimSpace(detail.DockerCompose) == "" {
|
||||||
|
compose, err := fileOp.GetContent(filepath.Join(versionPath, "docker-compose.yml"))
|
||||||
|
if err != nil {
|
||||||
|
return appDetailDTO, err
|
||||||
|
}
|
||||||
|
if err := cacheAppDetailCompose(&detail, compose); err != nil {
|
||||||
return appDetailDTO, err
|
return appDetailDTO, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch app.Type {
|
switch app.Type {
|
||||||
case constant.RuntimePHP:
|
case constant.RuntimePHP:
|
||||||
paramsPath := filepath.Join(versionPath, "data.yml")
|
|
||||||
if !fileOp.Stat(paramsPath) {
|
|
||||||
return appDetailDTO, buserr.WithDetail("ErrFileNotExist", paramsPath, nil)
|
|
||||||
}
|
|
||||||
param, err := fileOp.GetContent(paramsPath)
|
|
||||||
if err != nil {
|
|
||||||
return appDetailDTO, err
|
|
||||||
}
|
|
||||||
paramMap := make(map[string]interface{})
|
paramMap := make(map[string]interface{})
|
||||||
if err = yaml.Unmarshal(param, ¶mMap); err != nil {
|
if app.Resource == constant.AppResourceRemote {
|
||||||
return appDetailDTO, err
|
if err = json.Unmarshal([]byte(detail.Params), ¶mMap); err != nil {
|
||||||
|
return appDetailDTO, err
|
||||||
|
}
|
||||||
|
appDetailDTO.Params = paramMap
|
||||||
|
} else {
|
||||||
|
paramsPath := filepath.Join(versionPath, "data.yml")
|
||||||
|
if !fileOp.Stat(paramsPath) {
|
||||||
|
return appDetailDTO, buserr.WithDetail("ErrFileNotExist", paramsPath, nil)
|
||||||
|
}
|
||||||
|
param, err := fileOp.GetContent(paramsPath)
|
||||||
|
if err != nil {
|
||||||
|
return appDetailDTO, err
|
||||||
|
}
|
||||||
|
if err = yaml.Unmarshal(param, ¶mMap); err != nil {
|
||||||
|
return appDetailDTO, err
|
||||||
|
}
|
||||||
|
appDetailDTO.Params = paramMap["additionalProperties"]
|
||||||
}
|
}
|
||||||
appDetailDTO.Params = paramMap["additionalProperties"]
|
|
||||||
composePath := filepath.Join(versionPath, "docker-compose.yml")
|
composePath := filepath.Join(versionPath, "docker-compose.yml")
|
||||||
if !fileOp.Stat(composePath) {
|
if !fileOp.Stat(composePath) {
|
||||||
return appDetailDTO, buserr.WithDetail("ErrFileNotExist", composePath, nil)
|
return appDetailDTO, buserr.WithDetail("ErrFileNotExist", composePath, nil)
|
||||||
@@ -286,15 +307,17 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return appDetailDTO, err
|
return appDetailDTO, err
|
||||||
}
|
}
|
||||||
composeMap := make(map[string]interface{})
|
if err := validateAppDetailCompose(compose); err != nil {
|
||||||
|
return appDetailDTO, err
|
||||||
|
}
|
||||||
|
var composeMap struct {
|
||||||
|
Services map[string]interface{} `yaml:"services"`
|
||||||
|
}
|
||||||
if err := yaml.Unmarshal(compose, &composeMap); err != nil {
|
if err := yaml.Unmarshal(compose, &composeMap); err != nil {
|
||||||
return appDetailDTO, err
|
return appDetailDTO, err
|
||||||
}
|
}
|
||||||
if service, ok := composeMap["services"]; ok {
|
for k := range composeMap.Services {
|
||||||
servicesMap := service.(map[string]interface{})
|
appDetailDTO.Image = k
|
||||||
for k := range servicesMap {
|
|
||||||
appDetailDTO.Image = k
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -305,23 +328,10 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
|
|||||||
appDetailDTO.Params = paramMap
|
appDetailDTO.Params = paramMap
|
||||||
}
|
}
|
||||||
|
|
||||||
if appDetailDTO.DockerCompose == "" {
|
if err := ensureAppDetailCompose(app, &detail); err != nil {
|
||||||
filename := filepath.Base(appDetailDTO.DownloadUrl)
|
return appDetailDTO, err
|
||||||
dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(appDetailDTO.DownloadUrl, filename), "docker-compose.yml")
|
|
||||||
statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
|
||||||
if statusCode == http.StatusNotFound {
|
|
||||||
return appDetailDTO, buserr.New("ErrAppVersionUnavailable")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return appDetailDTO, buserr.WithDetail("ErrGetCompose", err.Error(), err)
|
|
||||||
}
|
|
||||||
if statusCode > 200 {
|
|
||||||
return appDetailDTO, buserr.WithDetail("ErrGetCompose", string(composeRes), err)
|
|
||||||
}
|
|
||||||
detail.DockerCompose = string(composeRes)
|
|
||||||
_ = appDetailRepo.Update(context.Background(), detail)
|
|
||||||
appDetailDTO.DockerCompose = string(composeRes)
|
|
||||||
}
|
}
|
||||||
|
appDetailDTO.DockerCompose = detail.DockerCompose
|
||||||
|
|
||||||
appDetailDTO.HostMode = isHostModel(appDetailDTO.DockerCompose)
|
appDetailDTO.HostMode = isHostModel(appDetailDTO.DockerCompose)
|
||||||
|
|
||||||
@@ -339,6 +349,13 @@ func (a AppService) GetAppDetailByID(id uint) (*response.AppDetailDTO, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
app, err := appRepo.GetFirst(repo.WithByID(appDetail.AppId))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := ensureAppDetailCompose(app, &appDetail); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
res.AppDetail = appDetail
|
res.AppDetail = appDetail
|
||||||
paramMap := make(map[string]interface{})
|
paramMap := make(map[string]interface{})
|
||||||
if err := json.Unmarshal([]byte(appDetail.Params), ¶mMap); err != nil {
|
if err := json.Unmarshal([]byte(appDetail.Params), ¶mMap); err != nil {
|
||||||
@@ -426,25 +443,13 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
|||||||
App: app,
|
App: app,
|
||||||
}
|
}
|
||||||
composeMap := make(map[string]interface{})
|
composeMap := make(map[string]interface{})
|
||||||
var composeRes []byte
|
|
||||||
if req.EditCompose {
|
if req.EditCompose {
|
||||||
if err = yaml.Unmarshal([]byte(req.DockerCompose), &composeMap); err != nil {
|
if err = yaml.Unmarshal([]byte(req.DockerCompose), &composeMap); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if appDetail.DockerCompose == "" {
|
if err = ensureAppDetailCompose(app, &appDetail); err != nil {
|
||||||
dockerComposeUrl := fmt.Sprintf("%s/%s/1panel/%s/%s/docker-compose.yml", global.AppRepoURL(), global.CONF.Base.Mode, app.Key, appDetail.Version)
|
return
|
||||||
var statusCode int
|
|
||||||
statusCode, composeRes, err = req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
|
||||||
if statusCode == http.StatusNotFound {
|
|
||||||
err = buserr.New("ErrAppVersionUnavailable")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
appDetail.DockerCompose = string(composeRes)
|
|
||||||
_ = appDetailRepo.Update(context.Background(), appDetail)
|
|
||||||
}
|
}
|
||||||
if err = yaml.Unmarshal([]byte(appDetail.DockerCompose), &composeMap); err != nil {
|
if err = yaml.Unmarshal([]byte(appDetail.DockerCompose), &composeMap); err != nil {
|
||||||
return
|
return
|
||||||
@@ -478,6 +483,22 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
|||||||
req.Params[constant.ContainerName] = containerName
|
req.Params[constant.ContainerName] = containerName
|
||||||
appInstall.ContainerName = containerName
|
appInstall.ContainerName = containerName
|
||||||
|
|
||||||
|
modelAccountBaseURL := ""
|
||||||
|
if app.Key == "ollama" {
|
||||||
|
appInstall.SyncModelAccount = req.SyncModelAccount == nil || *req.SyncModelAccount
|
||||||
|
appInstall.ModelAccountBaseURLType = req.ModelAccountBaseURLType
|
||||||
|
if appInstall.ModelAccountBaseURLType == "" {
|
||||||
|
appInstall.ModelAccountBaseURLType = "container"
|
||||||
|
}
|
||||||
|
appInstall.ModelAccountBaseURL = req.ModelAccountBaseURL
|
||||||
|
}
|
||||||
|
if app.Key == "ollama" && appInstall.SyncModelAccount {
|
||||||
|
modelAccountBaseURL, err = ResolveLocalModelAccountBaseURL(req.ModelAccountBaseURLType, req.ModelAccountBaseURL, containerName, "11434", httpPort)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
index := 0
|
index := 0
|
||||||
serviceName := ""
|
serviceName := ""
|
||||||
for k := range servicesMap {
|
for k := range servicesMap {
|
||||||
@@ -504,6 +525,26 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
|||||||
composeByte []byte
|
composeByte []byte
|
||||||
paramByte []byte
|
paramByte []byte
|
||||||
)
|
)
|
||||||
|
if (app.Key == "ollama" || app.Key == "vllm") && req.GPU != nil {
|
||||||
|
gpuServiceName := appInstall.ServiceName
|
||||||
|
imageType := "nvidia"
|
||||||
|
if app.Key == "vllm" {
|
||||||
|
imageType = req.GPUImageType
|
||||||
|
if len(servicesMap) > 1 {
|
||||||
|
gpuServiceName = "vllm"
|
||||||
|
if _, ok := servicesMap[gpuServiceName]; !ok {
|
||||||
|
gpuServiceName = "vllm-dspark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
service, ok := servicesMap[gpuServiceName].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return nil, buserr.New("ErrFileParse")
|
||||||
|
}
|
||||||
|
if err = ApplyModelGPU(service, req.GPU, req.Params, imageType); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
composeByte, err = yaml.Marshal(composeMap)
|
composeByte, err = yaml.Marshal(composeMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -550,6 +591,11 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
|||||||
|
|
||||||
installTask, err := task.NewTaskWithOps(appInstall.Name, task.TaskInstall, task.TaskScopeApp, req.TaskID, appInstall.ID)
|
installTask, err := task.NewTaskWithOps(appInstall.Name, task.TaskInstall, task.TaskScopeApp, req.TaskID, appInstall.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
appInstall.Status = constant.StatusInstallErr
|
||||||
|
appInstall.Message = err.Error()
|
||||||
|
if saveErr := appInstallRepo.Save(context.Background(), appInstall); saveErr != nil {
|
||||||
|
err = fmt.Errorf("%w; save failed install status: %v", err, saveErr)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -557,7 +603,7 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
installApp := func(t *task.Task) error {
|
installApp := func(t *task.Task) (err error) {
|
||||||
if err = copyData(t, app, appDetail, appInstall, req); err != nil {
|
if err = copyData(t, app, appDetail, appInstall, req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -572,7 +618,7 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if app.Key == "openresty" {
|
if app.Key == "openresty" {
|
||||||
if err = handleSiteDir(app, appDetail, req, t); err != nil {
|
if err = prepareAppInstallSiteDirectory(app, appDetail, req, t, hooks); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = handleOpenrestyFile(appInstall); err != nil {
|
if err = handleOpenrestyFile(appInstall); err != nil {
|
||||||
@@ -583,6 +629,10 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
updateToolApp(appInstall)
|
updateToolApp(appInstall)
|
||||||
|
if app.Key == "ollama" && appInstall.SyncModelAccount {
|
||||||
|
_, err := SyncLocalModelAccount(*appInstall, 0, modelAccountBaseURL, nil, false, true)
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -875,22 +925,10 @@ func (a AppService) GetAppUpdate() (*response.AppUpdateRes, error) {
|
|||||||
res.CanUpdate = true
|
res.CanUpdate = true
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
apps, _ := appRepo.GetBy(appRepo.WithResource(constant.AppResourceRemote))
|
list, err := readAppList()
|
||||||
for _, app := range apps {
|
if err != nil || list.LastModified != lastModified {
|
||||||
if app.Icon == "" {
|
list, err = getAppList()
|
||||||
res.CanUpdate = true
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
if appicon.IsIconFile(app.Icon) {
|
|
||||||
fileName, _ := appicon.ParseIconField(app.Icon)
|
|
||||||
if fileName == "" || !appicon.IconFileExists(fileName) {
|
|
||||||
res.CanUpdate = true
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
list, err := getAppList()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
@@ -921,29 +959,26 @@ func getAppFromRepo(downloadPath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getAppList() (*dto.AppList, error) {
|
func getAppList() (*dto.AppList, error) {
|
||||||
list := &dto.AppList{}
|
|
||||||
if err := getAppFromRepo(fmt.Sprintf("%s/%s/1panel.json.zip", global.AppRepoURL(), global.CONF.Base.Mode)); err != nil {
|
if err := getAppFromRepo(fmt.Sprintf("%s/%s/1panel.json.zip", global.AppRepoURL(), global.CONF.Base.Mode)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return readAppList()
|
||||||
|
}
|
||||||
|
|
||||||
|
func readAppList() (*dto.AppList, error) {
|
||||||
listFile := filepath.Join(global.Dir.ResourceDir, "1panel.json")
|
listFile := filepath.Join(global.Dir.ResourceDir, "1panel.json")
|
||||||
file, err := os.Open(listFile)
|
data, err := os.ReadFile(listFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer file.Close()
|
list := &dto.AppList{}
|
||||||
if err = json.NewDecoder(file).Decode(list); err != nil {
|
if err = json.Unmarshal(data, list); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return list, nil
|
return list, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var InitTypes = map[string]struct{}{
|
|
||||||
"runtime": {},
|
|
||||||
"php": {},
|
|
||||||
"node": {},
|
|
||||||
}
|
|
||||||
|
|
||||||
func deleteCustomApp() {
|
func deleteCustomApp() {
|
||||||
installs, err := appInstallRepo.ListBy(context.Background())
|
installs, err := appInstallRepo.ListBy(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1041,6 +1076,10 @@ func (a AppService) GetAppIcon(key string) ([]byte, string, string, error) {
|
|||||||
return nil, "", "", err
|
return nil, "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if appicon.IsRemoteURL(app.Icon) {
|
||||||
|
return nil, app.Icon, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
if appicon.IsIconFile(app.Icon) {
|
if appicon.IsIconFile(app.Icon) {
|
||||||
fileName, etag := appicon.ParseIconField(app.Icon)
|
fileName, etag := appicon.ParseIconField(app.Icon)
|
||||||
iconBytes, err := appicon.ReadIconFile(fileName)
|
iconBytes, err := appicon.ReadIconFile(fileName)
|
||||||
|
|||||||
@@ -0,0 +1,341 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/glebarez/sqlite"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func appCacheTestDB(t *testing.T) *gorm.DB {
|
||||||
|
t.Helper()
|
||||||
|
db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "cache.db")), &gorm.Config{
|
||||||
|
Logger: logger.Default.LogMode(logger.Silent),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := db.AutoMigrate(&model.App{}, &model.AppDetail{}, &model.AppTag{}, &model.Setting{}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
oldDB, oldDir, oldLog := global.DB, global.Dir, global.LOG
|
||||||
|
global.DB = db
|
||||||
|
global.Dir = global.SystemDir{AppResourceDir: t.TempDir()}
|
||||||
|
global.Dir.RemoteAppResourceDir = filepath.Join(global.Dir.AppResourceDir, constant.AppResourceRemote)
|
||||||
|
global.Dir.LocalAppResourceDir = filepath.Join(global.Dir.AppResourceDir, constant.AppResourceLocal)
|
||||||
|
global.Dir.CustomAppResourceDir = filepath.Join(global.Dir.AppResourceDir, constant.AppResourceCustom)
|
||||||
|
global.LOG = logrus.New()
|
||||||
|
global.LOG.SetOutput(io.Discard)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
global.DB, global.Dir, global.LOG = oldDB, oldDir, oldLog
|
||||||
|
sqlDB, err := db.DB()
|
||||||
|
if err == nil {
|
||||||
|
_ = sqlDB.Close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err := db.Create(&model.Setting{Key: "AppStoreLastModified", Value: "42"}).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func appCacheTestArchive(t *testing.T, name, content string) []byte {
|
||||||
|
t.Helper()
|
||||||
|
var buf bytes.Buffer
|
||||||
|
gz := gzip.NewWriter(&buf)
|
||||||
|
tw := tar.NewWriter(gz)
|
||||||
|
if err := tw.WriteHeader(&tar.Header{Name: name, Mode: 0644, Size: int64(len(content))}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := io.WriteString(tw, content); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := tw.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := gz.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func appCacheTestWrite(t *testing.T, name, content string) {
|
||||||
|
t.Helper()
|
||||||
|
if err := os.MkdirAll(filepath.Dir(name), 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(name, []byte(content), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppDownloadURLPreservesSourceQuery(t *testing.T) {
|
||||||
|
for _, rawURL := range []string{
|
||||||
|
"https://apps.1panel.pro/php/8/php-8.tar.gz",
|
||||||
|
"https://apps.1panel.pro/php/8/php-8.tar.gz?token=a%2Bb®ion=intl&v=old#archive",
|
||||||
|
} {
|
||||||
|
t.Run(rawURL, func(t *testing.T) {
|
||||||
|
got, err := appDownloadURL(rawURL, "42 /next")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
parsed, err := url.Parse(got)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
original, _ := url.Parse(rawURL)
|
||||||
|
wantQuery := original.Query()
|
||||||
|
wantQuery.Set("v", "42 /next")
|
||||||
|
if !reflect.DeepEqual(parsed.Query(), wantQuery) || parsed.Path != original.Path || parsed.Fragment != original.Fragment {
|
||||||
|
t.Fatalf("download URL lost source fields: %s", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if _, err := appDownloadURL("https://apps.1panel.pro/%zz", "42"); err == nil {
|
||||||
|
t.Fatal("invalid source URL was accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDownloadAppRefreshesAllOfficialCachedPackages(t *testing.T) {
|
||||||
|
db := appCacheTestDB(t)
|
||||||
|
app := model.App{Key: "mysql", Name: "MySQL", Type: "website", Resource: constant.AppResourceRemote}
|
||||||
|
versionDir := filepath.Join(app.GetAppResourcePath(), "8")
|
||||||
|
dataPath := filepath.Join(versionDir, "data.yml")
|
||||||
|
stampPath := filepath.Join(versionDir, ".1panel-appstore-version")
|
||||||
|
appCacheTestWrite(t, dataPath, "stale")
|
||||||
|
archives := map[string][]byte{
|
||||||
|
"42": appCacheTestArchive(t, "mysql/8/data.yml", "fresh-42"),
|
||||||
|
"43": appCacheTestArchive(t, "mysql/8/data.yml", "fresh-43"),
|
||||||
|
}
|
||||||
|
var calls atomic.Int32
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
calls.Add(1)
|
||||||
|
if r.URL.Query().Get("token") != "a+b" || r.URL.Query().Get("region") != "intl" {
|
||||||
|
t.Errorf("source query was lost: %s", r.URL.RawQuery)
|
||||||
|
}
|
||||||
|
version := r.URL.Query().Get("v")
|
||||||
|
if version != "42" && version != "43" {
|
||||||
|
t.Errorf("unexpected app store version: %q", version)
|
||||||
|
}
|
||||||
|
_, _ = w.Write(archives[version])
|
||||||
|
}))
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
detail := model.AppDetail{Version: "8", DownloadUrl: server.URL + "/mysql-8.tar.gz?token=a%2Bb®ion=intl", Update: false}
|
||||||
|
if err := db.Create(&detail).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, step := range []struct {
|
||||||
|
version string
|
||||||
|
calls int32
|
||||||
|
update bool
|
||||||
|
}{
|
||||||
|
{"42", 1, false}, // An existing directory without a stamp must be refreshed.
|
||||||
|
{"42", 1, false},
|
||||||
|
{"43", 2, false},
|
||||||
|
{"43", 3, true},
|
||||||
|
} {
|
||||||
|
if err := db.Model(&model.Setting{}).Where("key = ?", "AppStoreLastModified").Update("value", step.version).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
detail.Update = step.update
|
||||||
|
if err := db.Model(&detail).UpdateColumn("update", detail.Update).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := downloadApp(app, detail, nil, nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if calls.Load() != step.calls {
|
||||||
|
t.Fatalf("version %s: HTTP calls = %d, want %d", step.version, calls.Load(), step.calls)
|
||||||
|
}
|
||||||
|
for name, want := range map[string]string{dataPath: "fresh-" + step.version, stampPath: step.version} {
|
||||||
|
data, err := os.ReadFile(name)
|
||||||
|
if err != nil || string(data) != want {
|
||||||
|
t.Fatalf("%s = %q, %v; want %q", name, data, err, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var stored model.AppDetail
|
||||||
|
if err := db.First(&stored, detail.ID).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if stored.Update || stored.DownloadUrl != detail.DownloadUrl {
|
||||||
|
t.Fatalf("refresh altered source URL or left update flag: %+v", stored)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDownloadAppFailureKeepsCacheVersionAndUpdateFlag(t *testing.T) {
|
||||||
|
for _, failure := range []string{"download", "decompress"} {
|
||||||
|
t.Run(failure, func(t *testing.T) {
|
||||||
|
db := appCacheTestDB(t)
|
||||||
|
app := model.App{Key: "mysql", Resource: constant.AppResourceRemote}
|
||||||
|
stampPath := filepath.Join(app.GetAppResourcePath(), "8", ".1panel-appstore-version")
|
||||||
|
appCacheTestWrite(t, stampPath, "41")
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if failure == "download" {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = io.WriteString(w, "not an archive")
|
||||||
|
}))
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
detail := model.AppDetail{Version: "8", DownloadUrl: server.URL + "/mysql-8.tar.gz", Update: true}
|
||||||
|
if err := db.Create(&detail).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
install := model.AppInstall{}
|
||||||
|
if err := downloadApp(app, detail, &install, nil); err == nil {
|
||||||
|
t.Fatal("failed refresh was accepted")
|
||||||
|
}
|
||||||
|
if install.Status != constant.StatusDownloadErr || install.Message == "" {
|
||||||
|
t.Fatalf("installation did not report refresh failure: %+v", install)
|
||||||
|
}
|
||||||
|
stamp, err := os.ReadFile(stampPath)
|
||||||
|
if err != nil || string(stamp) != "41" {
|
||||||
|
t.Fatalf("failed refresh updated stamp: %q, %v", stamp, err)
|
||||||
|
}
|
||||||
|
var stored model.AppDetail
|
||||||
|
if err := db.First(&stored, detail.ID).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !stored.Update || stored.DownloadUrl != detail.DownloadUrl {
|
||||||
|
t.Fatalf("failed refresh changed database state: %+v", stored)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPHPAppDetailUsesSynchronizedOfficialParams(t *testing.T) {
|
||||||
|
const oldYAML = "additionalProperties:\n formFields:\n - envKey: PHP_VERSION\n type: select\n default: 8.1.34\n"
|
||||||
|
const newJSON = `{"formFields":[{"envKey":"PHP_VERSION","type":"select","default":"8.1.35"}]}`
|
||||||
|
const compose = "services:\n php:\n image: php:test\n"
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name, resource, compose string
|
||||||
|
update bool
|
||||||
|
}{
|
||||||
|
{"remote", constant.AppResourceRemote, compose, false},
|
||||||
|
{"local", constant.AppResourceLocal, compose, false},
|
||||||
|
{"custom", constant.AppResourceCustom, compose, false},
|
||||||
|
{"remote-fetches-compose", constant.AppResourceRemote, "", true},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
db := appCacheTestDB(t)
|
||||||
|
app := model.App{Key: "php", Name: "PHP", Type: constant.RuntimePHP, Resource: tc.resource}
|
||||||
|
if err := db.Omit(clause.Associations).Create(&app).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
versionDir := filepath.Join(app.GetAppResourcePath(), "8")
|
||||||
|
appCacheTestWrite(t, filepath.Join(versionDir, "data.yml"), oldYAML)
|
||||||
|
appCacheTestWrite(t, filepath.Join(versionDir, "docker-compose.yml"), compose)
|
||||||
|
archive := appCacheTestArchive(t, "php/8/data.yml", oldYAML)
|
||||||
|
var calls atomic.Int32
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
calls.Add(1)
|
||||||
|
if r.URL.Query().Get("v") != "42" {
|
||||||
|
t.Errorf("missing app store version: %s", r.URL.RawQuery)
|
||||||
|
}
|
||||||
|
if r.URL.Path == "/docker-compose.yml" {
|
||||||
|
_, _ = io.WriteString(w, compose)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = w.Write(archive)
|
||||||
|
}))
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
detail := model.AppDetail{AppId: app.ID, Version: "8", Params: newJSON, DockerCompose: tc.compose, Update: tc.update, DownloadUrl: server.URL + "/php-8.tar.gz"}
|
||||||
|
if err := db.Create(&detail).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got, err := (AppService{}).GetAppDetail(app.ID, "8", "runtime")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
wantVersion, wantCalls := "8.1.34", int32(0)
|
||||||
|
if tc.resource == constant.AppResourceRemote {
|
||||||
|
wantVersion, wantCalls = "8.1.35", 1
|
||||||
|
}
|
||||||
|
want := map[string]interface{}{"formFields": []interface{}{map[string]interface{}{
|
||||||
|
"envKey": "PHP_VERSION", "type": "select", "default": wantVersion,
|
||||||
|
}}}
|
||||||
|
if !reflect.DeepEqual(got.Params, want) || got.Image != "php" || calls.Load() != wantCalls {
|
||||||
|
encoded, _ := json.Marshal(got.Params)
|
||||||
|
t.Fatalf("params=%s, image=%s, HTTP calls=%d; want version %s, calls %d", encoded, got.Image, calls.Load(), wantVersion, wantCalls)
|
||||||
|
}
|
||||||
|
var stored model.AppDetail
|
||||||
|
if err := db.First(&stored, detail.ID).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if stored.Update || got.Update {
|
||||||
|
t.Fatal("fetching the compose file restored a stale update flag")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessOneAppDefersComposeUntilRequested(t *testing.T) {
|
||||||
|
db := appCacheTestDB(t)
|
||||||
|
const compose = "services:\n php:\n image: php:test\n"
|
||||||
|
var calls atomic.Int32
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
calls.Add(1)
|
||||||
|
if r.URL.Path != "/php/8/docker-compose.yml" || r.URL.Query().Get("v") != "42" {
|
||||||
|
t.Errorf("unversioned initial compose request: %s", r.URL.String())
|
||||||
|
}
|
||||||
|
_, _ = io.WriteString(w, compose)
|
||||||
|
}))
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
app := model.App{Key: "php", Type: constant.RuntimePHP, Resource: constant.AppResourceRemote}
|
||||||
|
if err := db.Create(&app).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ctx := appSyncContext{
|
||||||
|
baseRemoteUrl: server.URL,
|
||||||
|
systemVersion: "2.4.0",
|
||||||
|
list: &dto.AppList{LastModified: 42},
|
||||||
|
}
|
||||||
|
result := ctx.processOneApp(appWorkItem{
|
||||||
|
app: app,
|
||||||
|
appDef: dto.AppDefine{
|
||||||
|
AppProperty: dto.AppProperty{Key: "php", Type: constant.RuntimePHP},
|
||||||
|
Versions: []dto.AppConfigVersion{{Name: "8", AppForm: dto.AppForm{
|
||||||
|
FormFields: []dto.AppFormFields{{EnvKey: "PHP_VERSION", Default: "8.1.35", Type: "select"}},
|
||||||
|
}}},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if calls.Load() != 0 || len(result.Details) != 1 {
|
||||||
|
t.Fatalf("initial compose refresh failed: calls=%d, result=%+v", calls.Load(), result)
|
||||||
|
}
|
||||||
|
detail := result.Details[0]
|
||||||
|
if detail.DockerCompose != "" || detail.DownloadUrl != server.URL+"/php/8/php-8.tar.gz" {
|
||||||
|
t.Fatalf("sync fetched compose or changed the canonical archive URL: %+v", detail)
|
||||||
|
}
|
||||||
|
detail.AppId = app.ID
|
||||||
|
if err := db.Create(&detail).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for range 2 {
|
||||||
|
if err := ensureAppDetailCompose(app, &detail); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if calls.Load() != 1 || detail.DockerCompose != compose {
|
||||||
|
t.Fatalf("on-demand compose did not download once and cache: calls=%d, detail=%+v", calls.Load(), detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/req_helper"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ensureAppDetailCompose(app model.App, detail *model.AppDetail) error {
|
||||||
|
if strings.TrimSpace(detail.DockerCompose) != "" && validateAppDetailCompose([]byte(detail.DockerCompose)) == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var content []byte
|
||||||
|
var err error
|
||||||
|
if app.IsLocalApp() || app.IsCustomApp() {
|
||||||
|
content, err = os.ReadFile(filepath.Join(app.GetAppResourcePath(), detail.Version, "docker-compose.yml"))
|
||||||
|
} else {
|
||||||
|
var composeURL string
|
||||||
|
if detail.DownloadUrl == "" {
|
||||||
|
composeURL = fmt.Sprintf("%s/%s/1panel/%s/%s/docker-compose.yml", global.AppRepoURL(), global.CONF.Base.Mode, app.Key, detail.Version)
|
||||||
|
} else {
|
||||||
|
var source *url.URL
|
||||||
|
source, err = url.Parse(detail.DownloadUrl)
|
||||||
|
if err == nil {
|
||||||
|
source.Path = path.Join(path.Dir(source.Path), "docker-compose.yml")
|
||||||
|
source.RawPath = ""
|
||||||
|
composeURL = source.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
composeURL, err = currentAppDownloadURL(composeURL)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
var status int
|
||||||
|
status, content, err = req_helper.HandleRequest(composeURL, http.MethodGet, constant.TimeOut20s)
|
||||||
|
if status == http.StatusNotFound {
|
||||||
|
return buserr.New("ErrAppVersionUnavailable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return buserr.WithDetail("ErrGetCompose", err.Error(), err)
|
||||||
|
}
|
||||||
|
return cacheAppDetailCompose(detail, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateAppDetailCompose(content []byte) error {
|
||||||
|
var compose struct {
|
||||||
|
Services map[string]map[string]interface{} `yaml:"services"`
|
||||||
|
}
|
||||||
|
if err := yaml.Unmarshal(content, &compose); err != nil {
|
||||||
|
return buserr.WithDetail("ErrGetCompose", err.Error(), err)
|
||||||
|
}
|
||||||
|
if len(compose.Services) == 0 {
|
||||||
|
return buserr.WithDetail("ErrGetCompose", "docker-compose.yml has no services", nil)
|
||||||
|
}
|
||||||
|
for name, service := range compose.Services {
|
||||||
|
if strings.TrimSpace(name) == "" || len(service) == 0 {
|
||||||
|
return buserr.WithDetail("ErrGetCompose", "docker-compose.yml has an empty service", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cacheAppDetailCompose(detail *model.AppDetail, content []byte) error {
|
||||||
|
if err := validateAppDetailCompose(content); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stored, err := appDetailRepo.CacheCompose(*detail, string(content))
|
||||||
|
if err != nil {
|
||||||
|
return buserr.WithDetail("ErrGetCompose", err.Error(), err)
|
||||||
|
}
|
||||||
|
if !stored {
|
||||||
|
return buserr.WithDetail("ErrGetCompose", "application resources changed; retry the operation", nil)
|
||||||
|
}
|
||||||
|
detail.DockerCompose = string(content)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestComposeSyncOnlyInvalidatesChangedVersionResources(t *testing.T) {
|
||||||
|
form := dto.AppForm{FormFields: []dto.AppFormFields{{EnvKey: "PORT", Default: 8080}}}
|
||||||
|
params, err := json.Marshal(form)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var calls atomic.Int32
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
calls.Add(1)
|
||||||
|
_, _ = io.WriteString(w, "services:\n demo:\n image: demo:unexpected\n")
|
||||||
|
}))
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
for _, appType := range []string{"runtime", "php", "node", "java", "go", "python", "dotnet", "website"} {
|
||||||
|
t.Run(appType, func(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
mutate func(*model.AppDetail, *dto.AppConfigVersion)
|
||||||
|
keepCache bool
|
||||||
|
}{
|
||||||
|
{"unchanged", func(_ *model.AppDetail, _ *dto.AppConfigVersion) {}, true},
|
||||||
|
{"package update flag does not expire compose", func(d *model.AppDetail, _ *dto.AppConfigVersion) { d.Update = true }, true},
|
||||||
|
{"version revision changed", func(_ *model.AppDetail, v *dto.AppConfigVersion) { v.LastModified++ }, false},
|
||||||
|
{"unknown revision", func(_ *model.AppDetail, v *dto.AppConfigVersion) { v.LastModified = 0 }, false},
|
||||||
|
{"negative revision", func(_ *model.AppDetail, v *dto.AppConfigVersion) { v.LastModified = -1 }, false},
|
||||||
|
{"form changed", func(d *model.AppDetail, _ *dto.AppConfigVersion) { d.Params = `{"formFields":[]}` }, false},
|
||||||
|
{"source changed", func(d *model.AppDetail, _ *dto.AppConfigVersion) { d.DownloadUrl += ".old" }, false},
|
||||||
|
{"new version", func(d *model.AppDetail, _ *dto.AppConfigVersion) { *d = model.AppDetail{Version: "1.0"} }, false},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
const cached = "services:\n demo:\n image: demo:cached\n"
|
||||||
|
old := model.AppDetail{Version: "1.0", LastModified: 42, Params: string(params), DownloadUrl: server.URL + "/demo/1.0/demo-1.0.tar.gz", DockerCompose: cached}
|
||||||
|
version := dto.AppConfigVersion{Name: "1.0", LastModified: 42, AppForm: form}
|
||||||
|
tc.mutate(&old, &version)
|
||||||
|
ctx := appSyncContext{baseRemoteUrl: server.URL, systemVersion: "2.4.0"}
|
||||||
|
app := model.App{Key: "demo", Type: appType, Resource: constant.AppResourceRemote, Details: []model.AppDetail{old}}
|
||||||
|
got := ctx.processOneApp(appWorkItem{app: app, appDef: dto.AppDefine{AppProperty: dto.AppProperty{Key: "demo"}, Versions: []dto.AppConfigVersion{version}}})
|
||||||
|
if len(got.Details) != 1 || calls.Load() != 0 {
|
||||||
|
t.Fatalf("sync fetched compose or lost version: %+v, calls=%d", got, calls.Load())
|
||||||
|
}
|
||||||
|
detail := got.Details[0]
|
||||||
|
want := ""
|
||||||
|
if tc.keepCache {
|
||||||
|
want = cached
|
||||||
|
}
|
||||||
|
if detail.DockerCompose != want || detail.LastModified != version.LastModified || detail.Params != string(params) || !detail.Update {
|
||||||
|
t.Fatalf("cache decision or version metadata changed: %+v", detail)
|
||||||
|
}
|
||||||
|
if app.Details[0].DockerCompose != old.DockerCompose {
|
||||||
|
t.Fatal("sync mutated its input template")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const composeTestTemplate = "services:\n demo:\n image: demo:latest\n network_mode: host\n"
|
||||||
|
|
||||||
|
func composeTestDetail(t *testing.T, resource, source string) (*gorm.DB, model.App, model.AppDetail) {
|
||||||
|
t.Helper()
|
||||||
|
db := appCacheTestDB(t)
|
||||||
|
app := model.App{Key: "demo", Name: "Demo", Resource: resource, Type: "website"}
|
||||||
|
if err := db.Create(&app).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
detail := model.AppDetail{AppId: app.ID, Version: "1.0", LastModified: 42, Params: `{"formFields":[]}`, DownloadUrl: source}
|
||||||
|
if err := db.Create(&detail).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return db, app, detail
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComposeOnDemandLoadsOnceAndPreservesSource(t *testing.T) {
|
||||||
|
var calls atomic.Int32
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
calls.Add(1)
|
||||||
|
if r.URL.Path != "/demo/1.0/docker-compose.yml" || r.URL.Query().Get("signature") != "a+b" || r.URL.Query().Get("v") != "42" {
|
||||||
|
t.Errorf("unexpected Compose source: %s", r.URL)
|
||||||
|
}
|
||||||
|
_, _ = io.WriteString(w, composeTestTemplate)
|
||||||
|
}))
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
db, app, detail := composeTestDetail(t, constant.AppResourceRemote, server.URL+"/demo/1.0/demo-1.0.tar.gz?signature=a%2Bb")
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
if err := ensureAppDetailCompose(app, &detail); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if calls.Load() != 1 || detail.DockerCompose != composeTestTemplate {
|
||||||
|
t.Fatalf("cache miss after loading: calls=%d, detail=%+v", calls.Load(), detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var stored model.AppDetail
|
||||||
|
if err := db.First(&stored, detail.ID).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if stored.DockerCompose != composeTestTemplate || stored.LastModified != 42 || stored.DownloadUrl != detail.DownloadUrl || stored.Params != detail.Params || !stored.UpdatedAt.Equal(detail.UpdatedAt) {
|
||||||
|
t.Fatalf("cache write altered version metadata: %+v", stored)
|
||||||
|
}
|
||||||
|
res, err := (AppService{}).GetAppDetailByID(detail.ID)
|
||||||
|
if err != nil || !res.HostMode || res.DockerCompose != composeTestTemplate || calls.Load() != 1 {
|
||||||
|
t.Fatalf("ID details did not reuse template/HostMode: %+v, %v, calls=%d", res, err, calls.Load())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComposeOnDemandIDDetailsFillMissingTemplate(t *testing.T) {
|
||||||
|
var calls atomic.Int32
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
calls.Add(1)
|
||||||
|
_, _ = io.WriteString(w, composeTestTemplate)
|
||||||
|
}))
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
_, _, detail := composeTestDetail(t, constant.AppResourceRemote, server.URL+"/demo.tar.gz")
|
||||||
|
res, err := (AppService{}).GetAppDetailByID(detail.ID)
|
||||||
|
if err != nil || !res.HostMode || res.DockerCompose != composeTestTemplate || calls.Load() != 1 {
|
||||||
|
t.Fatalf("missing template gave wrong ID details: %+v, %v, calls=%d", res, err, calls.Load())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComposeOnDemandLocalAndCustomUseTheirDirectories(t *testing.T) {
|
||||||
|
for _, resource := range []string{constant.AppResourceLocal, constant.AppResourceCustom} {
|
||||||
|
t.Run(resource, func(t *testing.T) {
|
||||||
|
var calls atomic.Int32
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls.Add(1) }))
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
_, app, detail := composeTestDetail(t, resource, server.URL+"/must-not-fetch.tar.gz")
|
||||||
|
appCacheTestWrite(t, filepath.Join(app.GetAppResourcePath(), detail.Version, "docker-compose.yml"), composeTestTemplate)
|
||||||
|
if err := ensureAppDetailCompose(app, &detail); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if detail.DockerCompose != composeTestTemplate || calls.Load() != 0 {
|
||||||
|
t.Fatalf("local resource used remote template: %+v, calls=%d", detail, calls.Load())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComposeOnDemandRejectsDownloadAndTemplateFailures(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name, body string
|
||||||
|
status int
|
||||||
|
}{
|
||||||
|
{"not found", "missing", http.StatusNotFound},
|
||||||
|
{"server error", "failure", http.StatusServiceUnavailable},
|
||||||
|
{"empty", "", http.StatusOK},
|
||||||
|
{"HTML", "<html>blocked</html>", http.StatusOK},
|
||||||
|
{"malformed YAML", "services: [", http.StatusOK},
|
||||||
|
{"missing services", "version: '3'", http.StatusOK},
|
||||||
|
{"empty services", "services: {}", http.StatusOK},
|
||||||
|
{"empty service", "services:\n demo:", http.StatusOK},
|
||||||
|
{"string service", "services:\n demo: blocked", http.StatusOK},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(tc.status)
|
||||||
|
_, _ = io.WriteString(w, tc.body)
|
||||||
|
}))
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
db, app, detail := composeTestDetail(t, constant.AppResourceRemote, server.URL+"/demo.tar.gz")
|
||||||
|
if err := ensureAppDetailCompose(app, &detail); err == nil {
|
||||||
|
t.Fatal("invalid template was accepted")
|
||||||
|
}
|
||||||
|
var stored model.AppDetail
|
||||||
|
if err := db.First(&stored, detail.ID).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if detail.DockerCompose != "" || stored.DockerCompose != "" {
|
||||||
|
t.Fatal("failed response was cached")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComposeCacheRejectsConcurrentMetadataUpdates(t *testing.T) {
|
||||||
|
for _, field := range []string{"last_modified", "params", "download_url"} {
|
||||||
|
t.Run(field, func(t *testing.T) {
|
||||||
|
db, _, detail := composeTestDetail(t, constant.AppResourceRemote, "https://example.com/demo.tar.gz")
|
||||||
|
value := interface{}("new value")
|
||||||
|
if field == "last_modified" {
|
||||||
|
value = 43
|
||||||
|
}
|
||||||
|
if err := db.Model(&model.AppDetail{}).Where("id = ?", detail.ID).Updates(map[string]interface{}{field: value, "docker_compose": "new template"}).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := cacheAppDetailCompose(&detail, []byte(composeTestTemplate)); err == nil {
|
||||||
|
t.Fatal("stale template overwrote newer metadata")
|
||||||
|
}
|
||||||
|
var stored model.AppDetail
|
||||||
|
if err := db.First(&stored, detail.ID).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if stored.DockerCompose != "new template" || detail.DockerCompose != "" {
|
||||||
|
t.Fatalf("stale cache result was retained: %+v", stored)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComposeCachePropagatesWriteFailure(t *testing.T) {
|
||||||
|
db, _, detail := composeTestDetail(t, constant.AppResourceRemote, "https://example.com/demo.tar.gz")
|
||||||
|
if err := db.Exec("CREATE TRIGGER reject_compose BEFORE UPDATE OF docker_compose ON app_details BEGIN SELECT RAISE(FAIL, 'test write failure'); END").Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := cacheAppDetailCompose(&detail, []byte(composeTestTemplate)); err == nil || detail.DockerCompose != "" {
|
||||||
|
t.Fatal("failed cache write was ignored")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarkDownloadedPreservesComposeAndRejectsChangedMetadata(t *testing.T) {
|
||||||
|
db, _, detail := composeTestDetail(t, constant.AppResourceRemote, "https://example.com/demo.tar.gz")
|
||||||
|
if err := db.Model(&model.AppDetail{}).Where("id = ?", detail.ID).Updates(map[string]interface{}{"update": true, "docker_compose": composeTestTemplate}).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
stored, err := appDetailRepo.MarkDownloaded(detail)
|
||||||
|
if err != nil || !stored {
|
||||||
|
t.Fatalf("matching download marker failed: stored=%v err=%v", stored, err)
|
||||||
|
}
|
||||||
|
var fresh model.AppDetail
|
||||||
|
if err := db.First(&fresh, detail.ID).Error; err != nil || fresh.Update || fresh.DockerCompose != composeTestTemplate {
|
||||||
|
t.Fatalf("download marker overwrote Compose: %+v, %v", fresh, err)
|
||||||
|
}
|
||||||
|
for _, field := range []string{"last_modified", "params", "download_url"} {
|
||||||
|
t.Run(field, func(t *testing.T) {
|
||||||
|
var snapshot model.AppDetail
|
||||||
|
if err := db.First(&snapshot, detail.ID).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
value := interface{}("new metadata")
|
||||||
|
if field == "last_modified" {
|
||||||
|
value = snapshot.LastModified + 1
|
||||||
|
}
|
||||||
|
if err := db.Model(&model.AppDetail{}).Where("id = ?", detail.ID).Updates(map[string]interface{}{field: value, "update": true}).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
stored, err := appDetailRepo.MarkDownloaded(snapshot)
|
||||||
|
if err != nil || stored {
|
||||||
|
t.Fatalf("stale download marked new resources ready: stored=%v err=%v", stored, err)
|
||||||
|
}
|
||||||
|
if err := db.First(&fresh, detail.ID).Error; err != nil || !fresh.Update || fresh.DockerCompose != composeTestTemplate {
|
||||||
|
t.Fatalf("stale marker overwrote template or update flag: %+v, %v", fresh, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRuntimeDetailUsesValidPackageComposeWithoutAnotherGET(t *testing.T) {
|
||||||
|
var calls atomic.Int32
|
||||||
|
archive := appCacheTestArchive(t, "demo/1.0/docker-compose.yml", composeTestTemplate)
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
calls.Add(1)
|
||||||
|
if !strings.HasSuffix(r.URL.Path, ".tar.gz") {
|
||||||
|
t.Errorf("runtime requested Compose after obtaining a valid package: %s", r.URL)
|
||||||
|
}
|
||||||
|
_, _ = w.Write(archive)
|
||||||
|
}))
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
_, app, detail := composeTestDetail(t, constant.AppResourceRemote, server.URL+"/demo.tar.gz")
|
||||||
|
app.Type = constant.RuntimeNode
|
||||||
|
if err := global.DB.Model(&app).UpdateColumn("type", app.Type).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
res, err := (AppService{}).GetAppDetail(app.ID, detail.Version, "runtime")
|
||||||
|
if err != nil || res.DockerCompose != composeTestTemplate || !res.HostMode || calls.Load() != 1 {
|
||||||
|
t.Fatalf("package Compose was not reused: %+v, %v, calls=%d", res, err, calls.Load())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRuntimeDetailDoesNotCacheStaleTemplateAfterPackageFailure(t *testing.T) {
|
||||||
|
var calls atomic.Int32
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
calls.Add(1)
|
||||||
|
if strings.HasSuffix(r.URL.Path, ".tar.gz") {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = io.WriteString(w, composeTestTemplate)
|
||||||
|
}))
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
db, app, detail := composeTestDetail(t, constant.AppResourceRemote, server.URL+"/demo.tar.gz")
|
||||||
|
app.Type = constant.RuntimeNode
|
||||||
|
if err := db.Model(&app).UpdateColumn("type", app.Type).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
appCacheTestWrite(t, filepath.Join(app.GetAppResourcePath(), detail.Version, "docker-compose.yml"), "services:\n stale:\n image: stale:old\n")
|
||||||
|
res, err := (AppService{}).GetAppDetail(app.ID, detail.Version, "runtime")
|
||||||
|
if err != nil || res.DockerCompose != composeTestTemplate || calls.Load() != 2 {
|
||||||
|
t.Fatalf("failed package download reused a stale local template: %+v, %v, calls=%d", res, err, calls.Load())
|
||||||
|
}
|
||||||
|
var stored model.AppDetail
|
||||||
|
if err := db.First(&stored, detail.ID).Error; err != nil || stored.DockerCompose != composeTestTemplate {
|
||||||
|
t.Fatalf("stale package contents were cached: %+v, %v", stored, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
const composeUpgradeCurrent = `services:
|
||||||
|
demo:
|
||||||
|
image: demo:1.0
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: ${CPUS}
|
||||||
|
memory: ${MEMORY_LIMIT}
|
||||||
|
`
|
||||||
|
|
||||||
|
func composeUpgradeDB(t *testing.T, resourceURL string) (*gorm.DB, model.App, model.AppInstall, []model.AppDetail) {
|
||||||
|
t.Helper()
|
||||||
|
db := iconSyncDB(t)
|
||||||
|
if err := db.AutoMigrate(&model.AppIgnoreUpgrade{}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
app := model.App{Key: "demo", Name: "Demo", Type: "runtime", Resource: constant.AppResourceRemote, CrossVersionUpdate: true}
|
||||||
|
if err := db.Create(&app).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var details []model.AppDetail
|
||||||
|
for _, version := range []string{"1.1", "1.0", "1.3", "1.2"} {
|
||||||
|
detail := model.AppDetail{
|
||||||
|
AppId: app.ID, Version: version, LastModified: 42, Params: `{"formFields":[]}`, Status: constant.AppNormal,
|
||||||
|
DownloadUrl: resourceURL + "/demo/" + version + "/demo-" + version + ".tar.gz",
|
||||||
|
}
|
||||||
|
if err := db.Create(&detail).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
details = append(details, detail)
|
||||||
|
}
|
||||||
|
if err := db.Create(&model.AppIgnoreUpgrade{AppID: app.ID, AppDetailID: details[2].ID, Scope: "version"}).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
install := model.AppInstall{
|
||||||
|
AppId: app.ID, AppDetailId: details[1].ID, Name: "demo-install", Version: "1.0", App: app,
|
||||||
|
ServiceName: "demo", ContainerName: "demo-container", Status: constant.StatusRunning,
|
||||||
|
Env: `{"CONTAINER_NAME":"demo-container"}`, DockerCompose: composeUpgradeCurrent,
|
||||||
|
}
|
||||||
|
if err := db.Omit(clause.Associations).Create(&install).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return db, app, install, details
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpgradeComposeLoadsOnlyEffectiveTarget(t *testing.T) {
|
||||||
|
var mu sync.Mutex
|
||||||
|
requests := make(map[string]int)
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mu.Lock()
|
||||||
|
requests[r.URL.Path]++
|
||||||
|
mu.Unlock()
|
||||||
|
if r.URL.Query().Get("v") != "42" {
|
||||||
|
t.Errorf("missing synchronized version: %s", r.URL)
|
||||||
|
}
|
||||||
|
version := strings.Split(r.URL.Path, "/")[2]
|
||||||
|
_, _ = fmt.Fprintf(w, "services:\n source:\n image: demo:%s\n", version)
|
||||||
|
}))
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
db, _, install, details := composeUpgradeDB(t, server.URL)
|
||||||
|
// An unselected bad cached template must not prevent listing or default preview.
|
||||||
|
if err := db.Model(&details[0]).UpdateColumn("docker_compose", "invalid: [").Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
service := NewIAppInstalledService()
|
||||||
|
noCompose := false
|
||||||
|
versions, err := service.GetUpdateVersions(request.AppUpdateVersion{AppInstallID: install.ID, LoadCompose: &noCompose})
|
||||||
|
if err != nil || len(versions) != 2 || versions[0].Version != "1.2" || versions[1].Version != "1.1" {
|
||||||
|
t.Fatalf("pure versions did not preserve filtering/sorting: %+v, %v", versions, err)
|
||||||
|
}
|
||||||
|
for _, version := range versions {
|
||||||
|
if version.DockerCompose != "" {
|
||||||
|
t.Fatal("pure version request returned a rendered template")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(requests) != 0 {
|
||||||
|
t.Fatalf("pure version request fetched templates: %v", requests)
|
||||||
|
}
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
versions, err = service.GetUpdateVersions(request.AppUpdateVersion{AppInstallID: install.ID})
|
||||||
|
if err != nil || len(versions) != 2 || !strings.Contains(versions[0].DockerCompose, "demo:1.2") || versions[1].DockerCompose != "" {
|
||||||
|
t.Fatalf("default target preview = %+v, %v", versions, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if requests["/demo/1.2/docker-compose.yml"] != 1 || len(requests) != 1 {
|
||||||
|
t.Fatalf("default preview did not reuse only its own cache: %v", requests)
|
||||||
|
}
|
||||||
|
if err := db.Model(&details[0]).UpdateColumn("docker_compose", "").Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
versions, err = service.GetUpdateVersions(request.AppUpdateVersion{AppInstallID: install.ID, UpdateVersion: "1.1"})
|
||||||
|
if err != nil || !strings.Contains(versions[1].DockerCompose, "demo:1.1") || versions[0].DockerCompose != "" {
|
||||||
|
t.Fatalf("selected target preview = %+v, %v", versions, err)
|
||||||
|
}
|
||||||
|
if requests["/demo/1.1/docker-compose.yml"] != 1 || len(requests) != 2 {
|
||||||
|
t.Fatalf("selection prefetched other candidates: %v", requests)
|
||||||
|
}
|
||||||
|
if _, err := service.GetUpdateVersions(request.AppUpdateVersion{AppInstallID: install.ID, UpdateVersion: "1.3"}); err == nil {
|
||||||
|
t.Fatal("ignored target was accepted")
|
||||||
|
}
|
||||||
|
if len(requests) != 2 {
|
||||||
|
t.Fatal("invalid target triggered a template request")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpgradeComposeFetchFailurePreservesInstanceAndCache(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusServiceUnavailable) }))
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
db, _, install, details := composeUpgradeDB(t, server.URL)
|
||||||
|
if _, err := NewIAppInstalledService().GetUpdateVersions(request.AppUpdateVersion{AppInstallID: install.ID}); err == nil {
|
||||||
|
t.Fatal("failed target template load was accepted")
|
||||||
|
}
|
||||||
|
var stored model.AppInstall
|
||||||
|
if err := db.First(&stored, install.ID).Error; err != nil || stored.DockerCompose != install.DockerCompose || stored.Status != install.Status || stored.Version != install.Version {
|
||||||
|
t.Fatalf("preview modified installed instance: %+v, %v", stored, err)
|
||||||
|
}
|
||||||
|
var detail model.AppDetail
|
||||||
|
if err := db.First(&detail, details[3].ID).Error; err != nil || detail.DockerCompose != "" {
|
||||||
|
t.Fatalf("failed response cached a template: %+v, %v", detail, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpgradePrepareComposeFailureDoesNotCutover(t *testing.T) {
|
||||||
|
for _, failure := range []string{"package download", "invalid package template"} {
|
||||||
|
t.Run(failure, func(t *testing.T) {
|
||||||
|
archive := appCacheTestArchive(t, "demo/1.2/docker-compose.yml", "services: []")
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if failure == "package download" {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = w.Write(archive)
|
||||||
|
}))
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
db, _, install, details := composeUpgradeDB(t, server.URL)
|
||||||
|
upgrade := &appUpgradeContext{original: install, detail: details[3]}
|
||||||
|
if err := upgrade.prepare(&task.Task{Logger: global.LOG}); err == nil {
|
||||||
|
t.Fatal("invalid upgrade resources passed prepare")
|
||||||
|
}
|
||||||
|
if upgrade.stopAttempted || upgrade.downAttempted || upgrade.stageDir != "" {
|
||||||
|
t.Fatal("failed template preparation reached live cutover")
|
||||||
|
}
|
||||||
|
var stored model.AppInstall
|
||||||
|
if err := db.First(&stored, install.ID).Error; err != nil || stored.DockerCompose != install.DockerCompose || stored.Env != install.Env || stored.Version != install.Version || stored.Status != install.Status {
|
||||||
|
t.Fatalf("failed prepare modified the instance: %+v, %v", stored, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExplicitConfigComposeDetectsCustomAndReusesCurrentCache(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
custom bool
|
||||||
|
failure bool
|
||||||
|
}{
|
||||||
|
{name: "original"},
|
||||||
|
{name: "custom", custom: true},
|
||||||
|
{name: "fetch failure", failure: true},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
calls := 0
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
calls++
|
||||||
|
if r.URL.Path != "/demo/1.0/docker-compose.yml" {
|
||||||
|
t.Errorf("configuration loaded a different version: %s", r.URL)
|
||||||
|
}
|
||||||
|
if tc.failure {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = io.WriteString(w, "services:\n source:\n image: demo:1.0\n")
|
||||||
|
}))
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
db, _, install, _ := composeUpgradeDB(t, server.URL)
|
||||||
|
if tc.custom {
|
||||||
|
install.DockerCompose = strings.Replace(install.DockerCompose, "image: demo:1.0", "image: demo:1.0\n environment:\n USER_SETTING: preserve", 1)
|
||||||
|
if err := db.Omit(clause.Associations).Save(&install).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isEditCompose(install) || calls != 0 {
|
||||||
|
t.Fatal("list-time custom check fetched a missing template")
|
||||||
|
}
|
||||||
|
result, err := NewIAppInstalledService().GetParams(install.ID)
|
||||||
|
if tc.failure {
|
||||||
|
if err == nil || result != nil {
|
||||||
|
t.Fatal("failed current-template load was treated as an unedited configuration")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil || result.RawCompose == "" || result.IsEdit != tc.custom || result.DockerCompose != install.DockerCompose {
|
||||||
|
t.Fatalf("explicit custom state = %+v, %v; want custom=%v", result, err, tc.custom)
|
||||||
|
}
|
||||||
|
if _, err := NewIAppInstalledService().GetParams(install.ID); err != nil || calls != 1 {
|
||||||
|
t.Fatalf("configuration cache was not reused: calls=%d, %v", calls, err)
|
||||||
|
}
|
||||||
|
if isEditCompose(install) != tc.custom || calls != 1 {
|
||||||
|
t.Fatal("cached list-time comparison changed custom state or made network requests")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVllmManagedComposeDoesNotRequireOfficialTemplate(t *testing.T) {
|
||||||
|
var calls atomic.Int32
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
calls.Add(1)
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
}))
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
db, app, install, _ := composeUpgradeDB(t, server.URL)
|
||||||
|
if err := db.Model(&app).UpdateColumn("key", vllmAppKeyForUpgrade).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
install.DockerCompose = strings.Replace(install.DockerCompose, "image: demo:1.0", "image: demo:1.0\n command: [--managed-user-option]", 1)
|
||||||
|
if err := db.Omit(clause.Associations).Save(&install).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
service := NewIAppInstalledService()
|
||||||
|
config, err := service.GetParams(install.ID)
|
||||||
|
if err != nil || config.RawCompose != install.DockerCompose || config.DockerCompose != install.DockerCompose || config.IsEdit {
|
||||||
|
t.Fatalf("managed configuration lost the instance template: %+v, %v", config, err)
|
||||||
|
}
|
||||||
|
versions, err := service.GetUpdateVersions(request.AppUpdateVersion{AppInstallID: install.ID})
|
||||||
|
if err != nil || len(versions) != 2 || versions[0].DockerCompose != install.DockerCompose {
|
||||||
|
t.Fatalf("managed upgrade preview lost the instance template: %+v, %v", versions, err)
|
||||||
|
}
|
||||||
|
if calls.Load() != 0 {
|
||||||
|
t.Fatalf("managed template attempted %d official resource requests", calls.Load())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/xpack/providers"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
goi18n "github.com/nicksnyder/go-i18n/v2/i18n"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type iconSyncTransport struct {
|
||||||
|
providers.MultiNodeProvider
|
||||||
|
transport *http.Transport
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p iconSyncTransport) LoadRequestTransport() *http.Transport { return p.transport }
|
||||||
|
|
||||||
|
func iconSyncDB(t *testing.T) *gorm.DB {
|
||||||
|
t.Helper()
|
||||||
|
db := appCacheTestDB(t)
|
||||||
|
if err := db.AutoMigrate(&model.Tag{}, &model.AppInstall{}, &model.AppInstallResource{}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := db.Create(&[]model.Setting{{Key: "SystemVersion", Value: "2.4.0"}, {Key: "AppStoreSyncStatus", Value: constant.StatusSyncSuccess}}).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
oldI18n := global.I18n
|
||||||
|
global.I18n = goi18n.NewLocalizer(goi18n.NewBundle(language.English), "en")
|
||||||
|
t.Cleanup(func() { global.I18n = oldI18n })
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOfficialIconSyncNeverDownloadsImages(t *testing.T) {
|
||||||
|
iconSyncDB(t)
|
||||||
|
var calls atomic.Int32
|
||||||
|
cdn := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
calls.Add(1)
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
}))
|
||||||
|
t.Cleanup(cdn.Close)
|
||||||
|
taskLog := &task.Task{Logger: global.LOG}
|
||||||
|
for _, oldIcon := range []string{"", "app_demo.png?etag=old", "b2xk", cdn.URL + "/old.png"} {
|
||||||
|
t.Run(oldIcon, func(t *testing.T) {
|
||||||
|
icon := cdn.URL + "/demo.png?signature=a%2Bb&v=one"
|
||||||
|
def := dto.AppDefine{Icon: icon, AppProperty: dto.AppProperty{Key: "demo", Type: "website"}, Versions: []dto.AppConfigVersion{{Name: "1.0", AppForm: dto.AppForm{}}}}
|
||||||
|
apps := getApps([]model.App{{Key: "demo", Icon: oldIcon}}, []dto.AppDefine{def}, "2.4.0", taskLog)
|
||||||
|
ctx := appSyncContext{baseRemoteUrl: cdn.URL, list: &dto.AppList{LastModified: 42}}
|
||||||
|
result := ctx.processOneApp(appWorkItem{app: apps["demo"], appDef: def})
|
||||||
|
if result.Icon != icon || len(result.Details) != 1 || result.Details[0].Version != "1.0" || calls.Load() != 0 {
|
||||||
|
t.Fatalf("sync changed metadata or fetched a CDN image: %+v, calls=%d", result, calls.Load())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, invalid := range []string{"", "javascript:alert(1)", "https://user:secret@example.com/a.png", "//example.com/a.png", "https://"} {
|
||||||
|
def := dto.AppDefine{Icon: invalid, AppProperty: dto.AppProperty{Key: "demo"}}
|
||||||
|
apps := getApps([]model.App{{Key: "demo", Icon: "b2xk"}}, []dto.AppDefine{def}, "2.4.0", taskLog)
|
||||||
|
if apps["demo"].Icon != "b2xk" {
|
||||||
|
t.Fatalf("invalid icon %q erased history", invalid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOfficialIconSameRevisionRefreshIsReadOnlyUntilSync(t *testing.T) {
|
||||||
|
db := iconSyncDB(t)
|
||||||
|
apps := []model.App{
|
||||||
|
{Key: "mysql", Type: "website", Status: constant.AppNormal, Icon: "app_mysql.png", BatchInstallSupport: true},
|
||||||
|
{Key: "demo", Name: "original", Type: "website", Status: constant.AppNormal, Icon: "b2xk", LastModified: 7},
|
||||||
|
{Key: "future", Type: "website", Status: constant.AppNormal, Icon: "b2xk"},
|
||||||
|
{Key: "local-demo", Type: "website", Resource: constant.AppResourceLocal, Status: constant.AppNormal, Icon: "bG9jYWw="},
|
||||||
|
{Key: "custom-demo", Type: "website", Resource: constant.AppResourceCustom, Status: constant.AppNormal, Icon: "Y3VzdG9t"},
|
||||||
|
{Key: "down", Type: "website", Status: constant.AppTakeDown, Icon: "b2xk"},
|
||||||
|
}
|
||||||
|
if err := db.Create(&apps).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
detail := model.AppDetail{AppId: apps[1].ID, Version: "1.0", Params: "keep", DockerCompose: "keep", Update: false}
|
||||||
|
if err := db.Create(&detail).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := db.First(&detail, detail.ID).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var imageCalls, indexCalls atomic.Int32
|
||||||
|
var failIndex atomic.Bool
|
||||||
|
list := dto.AppList{LastModified: 42}
|
||||||
|
for _, app := range apps {
|
||||||
|
version := 0.0
|
||||||
|
if app.Key == "future" {
|
||||||
|
version = 9.9
|
||||||
|
}
|
||||||
|
list.Apps = append(list.Apps, dto.AppDefine{Icon: "https://cdn.example/" + app.Key + ".png?signature=a%2Bb", AppProperty: dto.AppProperty{Key: app.Key, Version: version}})
|
||||||
|
}
|
||||||
|
var zipped bytes.Buffer
|
||||||
|
zw := zip.NewWriter(&zipped)
|
||||||
|
w, err := zw.Create("1panel.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := json.NewEncoder(w).Encode(list); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := zw.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch {
|
||||||
|
case strings.HasSuffix(r.URL.Path, "1panel.json.version.txt"):
|
||||||
|
_, _ = io.WriteString(w, "42")
|
||||||
|
case strings.HasSuffix(r.URL.Path, "1panel.json.zip"):
|
||||||
|
indexCalls.Add(1)
|
||||||
|
if failIndex.Load() {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = w.Write(zipped.Bytes())
|
||||||
|
default:
|
||||||
|
imageCalls.Add(1)
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
transport := server.Client().Transport.(*http.Transport).Clone()
|
||||||
|
transport.TLSClientConfig.InsecureSkipVerify = true
|
||||||
|
transport.DialContext = func(ctx context.Context, network, _ string) (net.Conn, error) {
|
||||||
|
return (&net.Dialer{}).DialContext(ctx, network, server.Listener.Addr().String())
|
||||||
|
}
|
||||||
|
oldProvider := xpack.MultiNodeProvider
|
||||||
|
xpack.MultiNodeProvider = iconSyncTransport{MultiNodeProvider: oldProvider, transport: transport}
|
||||||
|
t.Cleanup(func() { xpack.MultiNodeProvider = oldProvider; transport.CloseIdleConnections() })
|
||||||
|
global.Dir.ResourceDir = t.TempDir()
|
||||||
|
global.Dir.IconCacheDir = t.TempDir()
|
||||||
|
appCacheTestWrite(t, filepath.Join(global.Dir.IconCacheDir, "app_mysql.png"), "keep old image")
|
||||||
|
encodedList, err := json.Marshal(list)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
appCacheTestWrite(t, filepath.Join(global.Dir.ResourceDir, "1panel.json"), string(encodedList))
|
||||||
|
|
||||||
|
res, err := (AppService{}).GetAppUpdate()
|
||||||
|
if err != nil || res.CanUpdate || res.AppList == nil || indexCalls.Load() != 0 {
|
||||||
|
t.Fatalf("same revision/missing files forced a full sync: %+v, %v", res, err)
|
||||||
|
}
|
||||||
|
var original model.App
|
||||||
|
if err := db.First(&original, apps[1].ID).Error; err != nil || original.Icon != apps[1].Icon {
|
||||||
|
t.Fatalf("update check modified icon: %+v, %v", original, err)
|
||||||
|
}
|
||||||
|
var writes atomic.Int32
|
||||||
|
if err := db.Callback().Update().After("gorm:update").Register("test:icon-writes", func(tx *gorm.DB) {
|
||||||
|
if values, ok := tx.Statement.Dest.(map[string]interface{}); ok && tx.Statement.Table == "apps" && values["icon"] != nil {
|
||||||
|
writes.Add(1)
|
||||||
|
}
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var shared *appSyncContext
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
if err := (AppService{}).createSyncAppStoreTask(&shared)(&task.Task{Logger: global.LOG}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if shared == nil || !shared.skipMetaSync || writes.Load() != 2 || imageCalls.Load() != 0 || indexCalls.Load() != 0 {
|
||||||
|
t.Fatalf("unexpected same revision sync: writes=%d, image calls=%d, index calls=%d, context=%+v", writes.Load(), imageCalls.Load(), indexCalls.Load(), shared)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var stored model.App
|
||||||
|
if err := db.First(&stored, apps[1].ID).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := original
|
||||||
|
want.Icon = list.Apps[1].Icon
|
||||||
|
if !reflect.DeepEqual(stored, want) {
|
||||||
|
t.Fatalf("light refresh changed non-icon fields: got %+v, want %+v", stored, want)
|
||||||
|
}
|
||||||
|
for _, original := range apps[2:] {
|
||||||
|
stored = model.App{}
|
||||||
|
if err := db.First(&stored, original.ID).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if stored.Icon != original.Icon || stored.Status != original.Status {
|
||||||
|
t.Fatalf("filtered app was changed: %+v", stored)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var storedDetail model.AppDetail
|
||||||
|
if err := db.First(&storedDetail, detail.ID).Error; err != nil || !reflect.DeepEqual(storedDetail, detail) {
|
||||||
|
t.Fatalf("light refresh touched app details: %+v, %v", storedDetail, err)
|
||||||
|
}
|
||||||
|
failIndex.Store(true)
|
||||||
|
appCacheTestWrite(t, filepath.Join(global.Dir.ResourceDir, "1panel.json"), "{invalid")
|
||||||
|
if err := (AppService{}).createSyncAppStoreTask(&shared)(&task.Task{Logger: global.LOG}); err == nil || writes.Load() != 2 || indexCalls.Load() != 1 {
|
||||||
|
t.Fatal("failed index read was accepted or modified icons")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppIconURLsReachListDetailAndInstalled(t *testing.T) {
|
||||||
|
db := iconSyncDB(t)
|
||||||
|
for _, icon := range []string{"https://cdn.example/icon.png?signature=a%2Bb", "app_legacy.png?etag=old", "bG9jYWw="} {
|
||||||
|
app := model.App{Key: "demo-" + icon[:4], Name: "demo", Type: "website", Status: constant.AppNormal, Icon: icon}
|
||||||
|
app.Name = app.Key
|
||||||
|
if err := db.Create(&app).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||||
|
ctx.Request = httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
got, err := (AppService{}).PageApp(ctx, request.AppSearch{PageInfo: dto.PageInfo{Page: 1, PageSize: 10}, Name: app.Key})
|
||||||
|
if err != nil || len(got.Items) != 1 {
|
||||||
|
t.Fatalf("list failed: %+v, %v", got, err)
|
||||||
|
}
|
||||||
|
want := ""
|
||||||
|
if strings.HasPrefix(icon, "https:") {
|
||||||
|
want = icon
|
||||||
|
}
|
||||||
|
if got.Items[0].Icon != want {
|
||||||
|
t.Fatalf("list icon = %q, want %q", got.Items[0].Icon, want)
|
||||||
|
}
|
||||||
|
detail, err := (AppService{}).GetApp(ctx, app.Key)
|
||||||
|
if err != nil || detail.Icon != icon {
|
||||||
|
t.Fatalf("detail icon = %+v, %v", detail, err)
|
||||||
|
}
|
||||||
|
installed, err := handleInstalled([]model.AppInstall{{App: app, Status: constant.StatusRunning}}, false, false, false)
|
||||||
|
if err != nil || len(installed) != 1 || installed[0].Icon != want {
|
||||||
|
t.Fatalf("installed icon = %+v, %v", installed, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLocalAppLogoKeepsBase64Delivery(t *testing.T) {
|
||||||
|
db := iconSyncDB(t)
|
||||||
|
dir := t.TempDir()
|
||||||
|
const image = "\x89PNG\r\n\x1a\nlocal-icon"
|
||||||
|
appCacheTestWrite(t, filepath.Join(dir, "data.yml"), "additionalProperties:\n key: demo\n name: Local Demo\n type: website\n")
|
||||||
|
appCacheTestWrite(t, filepath.Join(dir, "README.md"), "local application")
|
||||||
|
appCacheTestWrite(t, filepath.Join(dir, "logo.png"), image)
|
||||||
|
app, err := handleLocalApp(dir)
|
||||||
|
if err != nil || app.Resource != constant.AppResourceLocal || app.Icon != base64.StdEncoding.EncodeToString([]byte(image)) {
|
||||||
|
t.Fatalf("local logo parsing changed: %+v, %v", app, err)
|
||||||
|
}
|
||||||
|
if err := db.Create(app).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
data, source, etag, err := (AppService{}).GetAppIcon(app.Key)
|
||||||
|
if err != nil || string(data) != image || source != "" || etag != "" {
|
||||||
|
t.Fatalf("local logo no longer uses historical delivery: %q, %q, %q, %v", data, source, etag, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,10 +6,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -31,7 +29,6 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/utils/env"
|
"github.com/1Panel-dev/1Panel/agent/utils/env"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/nginx"
|
"github.com/1Panel-dev/1Panel/agent/utils/nginx"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/req_helper"
|
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -159,6 +156,14 @@ func (a *AppInstallService) CheckExist(req request.AppInstalledInfo) (*response.
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.ContainerName = appInstall.ContainerName
|
res.ContainerName = appInstall.ContainerName
|
||||||
|
res.ModelAccountBaseURLType = appInstall.ModelAccountBaseURLType
|
||||||
|
res.ModelAccountBaseURL = appInstall.ModelAccountBaseURL
|
||||||
|
if app.Key == "ollama" {
|
||||||
|
res.ModelAccountBaseURLType, res.ModelAccountBaseURL, err = loadOllamaModelAccountAddress(appInstall)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
res.Name = appInstall.Name
|
res.Name = appInstall.Name
|
||||||
res.Version = appInstall.Version
|
res.Version = appInstall.Version
|
||||||
res.CreatedAt = appInstall.CreatedAt
|
res.CreatedAt = appInstall.CreatedAt
|
||||||
@@ -251,6 +256,15 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if install.Status == constant.StatusRestarting && (req.Operate == constant.Stop || req.Operate == constant.Delete) && !req.ForceDelete {
|
||||||
|
count, err := taskRepo.CountExecutingTask(repo.WithByType(task.TaskScopeApp), taskRepo.WithResourceID(install.ID), taskRepo.WithOperate(task.TaskRestart))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
return buserr.New("TaskIsExecuting")
|
||||||
|
}
|
||||||
|
}
|
||||||
if !req.ForceDelete && !files.NewFileOp().Stat(install.GetPath()) {
|
if !req.ForceDelete && !files.NewFileOp().Stat(install.GetPath()) {
|
||||||
return buserr.New("ErrInstallDirNotFound")
|
return buserr.New("ErrInstallDirNotFound")
|
||||||
}
|
}
|
||||||
@@ -297,13 +311,14 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
|
|||||||
return syncAppInstallStatus(&install, true)
|
return syncAppInstallStatus(&install, true)
|
||||||
case constant.Upgrade:
|
case constant.Upgrade:
|
||||||
upgradeReq := request.AppInstallUpgrade{
|
upgradeReq := request.AppInstallUpgrade{
|
||||||
InstallID: install.ID,
|
FeishuTransition: req.FeishuTransition,
|
||||||
DetailID: req.DetailId,
|
InstallID: install.ID,
|
||||||
Backup: req.Backup,
|
DetailID: req.DetailId,
|
||||||
PullImage: req.PullImage,
|
Backup: req.Backup,
|
||||||
DeleteImage: req.DeleteImage,
|
PullImage: req.PullImage,
|
||||||
DockerCompose: req.DockerCompose,
|
DeleteImage: req.DeleteImage,
|
||||||
TaskID: req.TaskID,
|
DockerCompose: req.DockerCompose,
|
||||||
|
TaskID: req.TaskID,
|
||||||
}
|
}
|
||||||
return upgradeInstall(upgradeReq)
|
return upgradeInstall(upgradeReq)
|
||||||
case constant.Reload:
|
case constant.Reload:
|
||||||
@@ -333,18 +348,27 @@ func operateAppWithLifecycleScripts(install model.AppInstall, req request.AppIns
|
|||||||
return errors.New("lifecycle script operation not supported")
|
return errors.New("lifecycle script operation not supported")
|
||||||
}
|
}
|
||||||
install.Message = ""
|
install.Message = ""
|
||||||
if err := appInstallRepo.Save(context.Background(), &install); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
operationTask, err := task.NewTaskWithOps(install.Name, taskType, task.TaskScopeApp, req.TaskID, install.ID)
|
operationTask, err := task.NewTaskWithOps(install.Name, taskType, task.TaskScopeApp, req.TaskID, install.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
install.Status = constant.StatusUpErr
|
||||||
|
install.Message = err.Error()
|
||||||
|
if saveErr := appInstallRepo.Save(context.Background(), &install); saveErr != nil {
|
||||||
|
return fmt.Errorf("%w; save failed operation status: %v", err, saveErr)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
ready := make(chan error, 1)
|
||||||
|
started := make(chan struct{})
|
||||||
operation := string(req.Operate)
|
operation := string(req.Operate)
|
||||||
operationTask.AddSubTaskWithOps(
|
operationTask.AddSubTaskWithOps(
|
||||||
task.GetTaskName(install.Name, taskType, task.TaskScopeApp),
|
task.GetTaskName(install.Name, taskType, task.TaskScopeApp),
|
||||||
func(t *task.Task) error {
|
func(t *task.Task) error {
|
||||||
|
if err := appInstallRepo.Save(context.Background(), &install); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
close(started)
|
||||||
|
ready <- nil
|
||||||
if err := runScript(t, &install, operation); err != nil {
|
if err := runScript(t, &install, operation); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -371,6 +395,12 @@ func operateAppWithLifecycleScripts(install model.AppInstall, req request.AppIns
|
|||||||
)
|
)
|
||||||
go func() {
|
go func() {
|
||||||
if taskErr := operationTask.Execute(); taskErr != nil {
|
if taskErr := operationTask.Execute(); taskErr != nil {
|
||||||
|
select {
|
||||||
|
case <-started:
|
||||||
|
default:
|
||||||
|
ready <- taskErr
|
||||||
|
return
|
||||||
|
}
|
||||||
if onFailure != nil {
|
if onFailure != nil {
|
||||||
onFailure(taskErr)
|
onFailure(taskErr)
|
||||||
return
|
return
|
||||||
@@ -380,7 +410,7 @@ func operateAppWithLifecycleScripts(install model.AppInstall, req request.AppIns
|
|||||||
_ = appInstallRepo.Save(context.Background(), &install)
|
_ = appInstallRepo.Save(context.Background(), &install)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
return nil
|
return <-ready
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AppInstallService) UpdateAppConfig(req request.AppConfigUpdate) error {
|
func (a *AppInstallService) UpdateAppConfig(req request.AppConfigUpdate) error {
|
||||||
@@ -433,10 +463,37 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
modelAccountBaseURL := ""
|
||||||
|
if installed.App.Key == "ollama" {
|
||||||
|
if req.SyncModelAccount != nil {
|
||||||
|
installed.SyncModelAccount = *req.SyncModelAccount
|
||||||
|
}
|
||||||
|
if req.ModelAccountBaseURLType != "" {
|
||||||
|
installed.ModelAccountBaseURLType = req.ModelAccountBaseURLType
|
||||||
|
installed.ModelAccountBaseURL = req.ModelAccountBaseURL
|
||||||
|
} else if installed.SyncModelAccount {
|
||||||
|
installed.ModelAccountBaseURLType, installed.ModelAccountBaseURL, err = loadOllamaModelAccountAddress(oldInstalled)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if installed.SyncModelAccount {
|
||||||
|
containerName := installed.ContainerName
|
||||||
|
if req.Advanced && req.ContainerName != "" {
|
||||||
|
containerName = req.ContainerName
|
||||||
|
}
|
||||||
|
modelAccountBaseURL, err = ResolveLocalModelAccountBaseURL(installed.ModelAccountBaseURLType, installed.ModelAccountBaseURL, containerName, "11434", installed.HttpPort)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
backupDockerCompose := installed.DockerCompose
|
backupDockerCompose := installed.DockerCompose
|
||||||
if req.Advanced {
|
updateGPU := installed.App.Key == "ollama" && req.GPU != nil
|
||||||
|
if req.Advanced || updateGPU {
|
||||||
composeMap := make(map[string]interface{})
|
composeMap := make(map[string]interface{})
|
||||||
if req.EditCompose {
|
if req.Advanced && req.EditCompose {
|
||||||
if err = yaml.Unmarshal([]byte(req.DockerCompose), &composeMap); err != nil {
|
if err = yaml.Unmarshal([]byte(req.DockerCompose), &composeMap); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -445,16 +502,35 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !req.SkipComposeCommonConfig {
|
if req.Advanced && !req.SkipComposeCommonConfig {
|
||||||
if err = addDockerComposeCommonParam(composeMap, installed.ServiceName, req.AppContainerConfig, req.Params); err != nil {
|
if err = addDockerComposeCommonParam(composeMap, installed.ServiceName, req.AppContainerConfig, req.Params); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if updateGPU {
|
||||||
|
services, _ := composeMap["services"].(map[string]interface{})
|
||||||
|
service, ok := services[installed.ServiceName].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return buserr.New("ErrFileParse")
|
||||||
|
}
|
||||||
|
gpuParams := map[string]interface{}{}
|
||||||
|
if err := json.Unmarshal([]byte(installed.Env), &gpuParams); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for key, value := range req.Params {
|
||||||
|
gpuParams[key] = value
|
||||||
|
}
|
||||||
|
if err := ApplyModelGPU(service, req.GPU, gpuParams, "nvidia"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
composeByte, err := yaml.Marshal(composeMap)
|
composeByte, err := yaml.Marshal(composeMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
installed.DockerCompose = string(composeByte)
|
installed.DockerCompose = string(composeByte)
|
||||||
|
}
|
||||||
|
if req.Advanced {
|
||||||
if req.ContainerName == "" {
|
if req.ContainerName == "" {
|
||||||
req.Params[constant.ContainerName] = installed.ContainerName
|
req.Params[constant.ContainerName] = installed.ContainerName
|
||||||
} else {
|
} else {
|
||||||
@@ -558,6 +634,9 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
|
|||||||
if proxyChanged && website.ID != 0 && currentProxyErr != nil {
|
if proxyChanged && website.ID != 0 && currentProxyErr != nil {
|
||||||
global.LOG.Error(buserr.WithErr("ErrUpdateBuWebsite", currentProxyErr).Error())
|
global.LOG.Error(buserr.WithErr("ErrUpdateBuWebsite", currentProxyErr).Error())
|
||||||
}
|
}
|
||||||
|
if installed.App.Key == "ollama" && installed.SyncModelAccount {
|
||||||
|
return syncOllamaModelAccount(installed, modelAccountBaseURL, true, true)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -567,11 +646,15 @@ func (a *AppInstallService) SyncAll(systemInit bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, i := range allList {
|
for _, i := range allList {
|
||||||
if i.Status == constant.StatusInstalling || i.Status == constant.StatusUpgrading || i.Status == constant.StatusRebuilding || i.Status == constant.StatusUninstalling {
|
if appInstallOperationInterruptedOnRestart(i.Status) {
|
||||||
if systemInit {
|
if systemInit {
|
||||||
i.Status = constant.StatusError
|
i.Status = appInstallOperationFailureStatus(i.Status)
|
||||||
i.Message = "1Panel restart causes the task to terminate"
|
i.Message = constant.InterruptedMsg
|
||||||
_ = appInstallRepo.Save(context.Background(), &i)
|
if err := appInstallRepo.Save(context.Background(), &i); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if err := syncAppInstallStatus(&i, false); err != nil {
|
||||||
|
global.LOG.Errorf("sync install app[%s] error,mgs: %s", i.Name, err.Error())
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -670,6 +753,7 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return versions, err
|
return versions, err
|
||||||
}
|
}
|
||||||
|
var feishuLegacy, feishuConfigured, feishuChecked bool
|
||||||
details, err := appDetailRepo.GetBy(appDetailRepo.WithAppId(app.ID))
|
details, err := appDetailRepo.GetBy(appDetailRepo.WithAppId(app.ID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return versions, err
|
return versions, err
|
||||||
@@ -678,39 +762,33 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
|
|||||||
if !canAccessVllmVersion(app.Key, detail.Version) {
|
if !canAccessVllmVersion(app.Key, detail.Version) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if !isAIGatewayUpgradeVersionAllowed(app.Key, install.Version, detail.Version) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
ignores, _ := appIgnoreUpgradeRepo.List(runtimeRepo.WithDetailId(detail.ID), appIgnoreUpgradeRepo.WithScope("version"))
|
ignores, _ := appIgnoreUpgradeRepo.List(runtimeRepo.WithDetailId(detail.ID), appIgnoreUpgradeRepo.WithScope("version"))
|
||||||
if len(ignores) > 0 {
|
if len(ignores) > 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if common.IsCrossVersion(install.Version, detail.Version) && !app.CrossVersionUpdate {
|
if isCrossAppVersion(app.Key, install.Version, detail.Version) && !app.CrossVersionUpdate {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
canUpgrade := common.CompareVersion(detail.Version, install.Version)
|
canUpgrade := isNewerAppVersion(app.Key, detail.Version, install.Version)
|
||||||
if app.Key == vllmAppKeyForUpgrade {
|
if app.Key == vllmAppKeyForUpgrade {
|
||||||
currentImage := loadVllmImageFromEnv(install.Env)
|
currentImage := loadVllmImageFromEnv(install.Env)
|
||||||
canUpgrade = isVllmUpgradeCandidate(install.Version, detail.Version, currentImage)
|
canUpgrade = isVllmUpgradeCandidate(install.Version, detail.Version, currentImage)
|
||||||
}
|
}
|
||||||
if canUpgrade {
|
if canUpgrade {
|
||||||
var newCompose string
|
feishuTarget := app.Key == constant.AppOpenclaw && isOpenclawBundledFeishuVersion(detail.Version)
|
||||||
if req.UpdateVersion != "" && req.UpdateVersion == detail.Version && detail.DockerCompose == "" && !app.IsLocalApp() {
|
if feishuTarget && !feishuChecked && (req.LoadCompose == nil || *req.LoadCompose) {
|
||||||
filename := filepath.Base(detail.DownloadUrl)
|
conf, readErr := readOpenclawConfig(path.Join(install.GetPath(), "data/conf/openclaw.json"))
|
||||||
dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(detail.DownloadUrl, filename), "docker-compose.yml")
|
if readErr != nil {
|
||||||
statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
return versions, readErr
|
||||||
if statusCode == http.StatusNotFound {
|
|
||||||
return versions, buserr.New("ErrAppVersionUnavailable")
|
|
||||||
}
|
}
|
||||||
|
feishuLegacy, feishuConfigured, err = classifyLegacyFeishu(conf, install.GetPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return versions, err
|
return versions, err
|
||||||
}
|
}
|
||||||
if statusCode > 200 {
|
feishuChecked = true
|
||||||
return versions, err
|
|
||||||
}
|
|
||||||
detail.DockerCompose = string(composeRes)
|
|
||||||
_ = appDetailRepo.Update(context.Background(), detail)
|
|
||||||
}
|
|
||||||
newCompose, err = getUpgradeCompose(install, detail)
|
|
||||||
if err != nil {
|
|
||||||
return versions, err
|
|
||||||
}
|
}
|
||||||
if app.Key == constant.AppMysql {
|
if app.Key == constant.AppMysql {
|
||||||
majorVersion := getMajorVersion(install.Version)
|
majorVersion := getMajorVersion(install.Version)
|
||||||
@@ -719,15 +797,50 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
versions = append(versions, dto.AppVersion{
|
versions = append(versions, dto.AppVersion{
|
||||||
Version: detail.Version,
|
FeishuLegacy: feishuTarget && feishuLegacy,
|
||||||
DetailId: detail.ID,
|
FeishuConfigured: feishuTarget && feishuConfigured,
|
||||||
DockerCompose: newCompose,
|
Version: detail.Version,
|
||||||
|
DetailId: detail.ID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sort.Slice(versions, func(i, j int) bool {
|
sort.Slice(versions, func(i, j int) bool {
|
||||||
return common.CompareVersion(versions[i].Version, versions[j].Version)
|
return appVersionSortBefore(app.Key, versions[i].Version, versions[j].Version)
|
||||||
})
|
})
|
||||||
|
if len(versions) == 0 || (req.LoadCompose != nil && !*req.LoadCompose) {
|
||||||
|
return versions, nil
|
||||||
|
}
|
||||||
|
target := 0
|
||||||
|
if req.UpdateVersion != "" {
|
||||||
|
target = -1
|
||||||
|
for i := range versions {
|
||||||
|
if versions[i].Version == req.UpdateVersion {
|
||||||
|
target = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if target == -1 {
|
||||||
|
return versions, buserr.New("ErrAppVersionUnavailable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if app.Key == vllmAppKeyForUpgrade {
|
||||||
|
versions[target].DockerCompose = install.DockerCompose
|
||||||
|
return versions, nil
|
||||||
|
}
|
||||||
|
for _, detail := range details {
|
||||||
|
if detail.ID != versions[target].DetailId {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := ensureAppDetailCompose(app, &detail); err != nil {
|
||||||
|
return versions, err
|
||||||
|
}
|
||||||
|
content, err := getUpgradeCompose(install, detail)
|
||||||
|
if err != nil {
|
||||||
|
return versions, err
|
||||||
|
}
|
||||||
|
versions[target].DockerCompose = content
|
||||||
|
break
|
||||||
|
}
|
||||||
return versions, nil
|
return versions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -916,6 +1029,28 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
|
|||||||
config := getAppCommonConfig(envs)
|
config := getAppCommonConfig(envs)
|
||||||
config.DockerCompose = install.DockerCompose
|
config.DockerCompose = install.DockerCompose
|
||||||
res.Params = params
|
res.Params = params
|
||||||
|
res.AppKey = install.App.Key
|
||||||
|
res.Name = install.Name
|
||||||
|
res.HttpPort = install.HttpPort
|
||||||
|
res.SyncModelAccount = install.SyncModelAccount
|
||||||
|
res.ModelAccountBaseURLType = install.ModelAccountBaseURLType
|
||||||
|
res.ModelAccountBaseURL = install.ModelAccountBaseURL
|
||||||
|
if install.App.Key == "ollama" {
|
||||||
|
var project map[string]interface{}
|
||||||
|
if err := yaml.Unmarshal([]byte(install.DockerCompose), &project); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
services, _ := project["services"].(map[string]interface{})
|
||||||
|
service, ok := services[install.ServiceName].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return nil, buserr.New("ErrFileParse")
|
||||||
|
}
|
||||||
|
res.GPU = ReadModelGPU(service, envs, "nvidia")
|
||||||
|
res.ModelAccountBaseURLType, res.ModelAccountBaseURL, err = loadOllamaModelAccountAddress(install)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
if config.ContainerName == "" {
|
if config.ContainerName == "" {
|
||||||
config.ContainerName = install.ContainerName
|
config.ContainerName = install.ContainerName
|
||||||
}
|
}
|
||||||
@@ -924,17 +1059,62 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
|
|||||||
res.RestartPolicy = getRestartPolicy(install.DockerCompose)
|
res.RestartPolicy = getRestartPolicy(install.DockerCompose)
|
||||||
res.WebUI = install.WebUI
|
res.WebUI = install.WebUI
|
||||||
res.Type = install.App.Type
|
res.Type = install.App.Type
|
||||||
if rawCompose, err := getUpgradeCompose(install, detail); err == nil {
|
rawCompose := install.DockerCompose
|
||||||
res.RawCompose = rawCompose
|
if install.App.Key != vllmAppKeyForUpgrade {
|
||||||
|
if err := ensureAppDetailCompose(install.App, &detail); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rawCompose, err = getUpgradeCompose(install, detail)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
equal, err := composeEqualExceptImage(rawCompose, install.DockerCompose)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res.RawCompose = rawCompose
|
||||||
|
res.IsEdit = !equal
|
||||||
return &res, nil
|
return &res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error {
|
func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error {
|
||||||
|
operation := ""
|
||||||
|
switch appInstall.Status {
|
||||||
|
case constant.StatusInstalling:
|
||||||
|
operation = task.TaskInstall
|
||||||
|
case constant.StatusUpgrading:
|
||||||
|
operation = task.TaskUpgrade
|
||||||
|
case constant.StatusRebuilding:
|
||||||
|
operation = task.TaskBuild
|
||||||
|
case constant.StatusUninstalling:
|
||||||
|
operation = task.TaskUninstall
|
||||||
|
case constant.StatusStarting, constant.StatusWaiting:
|
||||||
|
operation = task.TaskUpdate
|
||||||
|
case constant.StatusRestarting:
|
||||||
|
operation = task.TaskRestart
|
||||||
|
}
|
||||||
|
if operation != "" {
|
||||||
|
if err := reconcileAppInstallTaskFailure(appInstall, operation, global.DB, global.TaskDB); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
switch appInstall.Status {
|
switch appInstall.Status {
|
||||||
case constant.StatusInstalling, constant.StatusRebuilding, constant.StatusUpgrading, constant.StatusUninstalling,
|
case constant.StatusInstalling, constant.StatusRebuilding, constant.StatusUpgrading, constant.StatusUninstalling,
|
||||||
constant.StatusStarting, constant.StatusRestarting, constant.StatusWaiting:
|
constant.StatusStarting, constant.StatusWaiting:
|
||||||
return nil
|
return nil
|
||||||
|
case constant.StatusRestarting:
|
||||||
|
count, err := taskRepo.CountExecutingTask(repo.WithByType(task.TaskScopeApp), taskRepo.WithResourceID(appInstall.ID), taskRepo.WithOperate(task.TaskRestart))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
case constant.StatusInstallErr, constant.StatusUpgradeErr, constant.StatusUpErr:
|
||||||
|
if !force {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cli, err := docker.NewClient()
|
cli, err := docker.NewClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a AppService) InstallForMCP(req request.AppInstallCreate, executeScript bool) (*model.AppInstall, error) {
|
||||||
|
return a.installWithHooks(req, executeScript, &appInstallHooks{PrepareSiteDir: prepareMCPInstallSiteDirectory})
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareMCPInstallSiteDirectory(app model.App, detail model.AppDetail, req request.AppInstallCreate, _ *task.Task) error {
|
||||||
|
if !managesOpenrestySiteDirectory(app, detail) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
directory, _ := req.Params["WEBSITE_DIR"].(string)
|
||||||
|
return ensureCurrentWebsiteDirectory(directory)
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func appInstallOperationFailureStatus(status string) string {
|
||||||
|
switch status {
|
||||||
|
case constant.StatusInstalling:
|
||||||
|
return constant.StatusInstallErr
|
||||||
|
case constant.StatusUpgrading:
|
||||||
|
return constant.StatusUpgradeErr
|
||||||
|
case constant.StatusUninstalling:
|
||||||
|
return constant.StatusError
|
||||||
|
default:
|
||||||
|
return constant.StatusUpErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func appInstallOperationPending(status string) bool {
|
||||||
|
switch status {
|
||||||
|
case constant.StatusInstalling, constant.StatusRebuilding, constant.StatusUpgrading, constant.StatusUninstalling,
|
||||||
|
constant.StatusStarting, constant.StatusRestarting, constant.StatusWaiting:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func appInstallOperationInterruptedOnRestart(status string) bool {
|
||||||
|
// ReStarting can also describe a container's Docker restart policy.
|
||||||
|
return appInstallOperationPending(status) && status != constant.StatusRestarting
|
||||||
|
}
|
||||||
|
|
||||||
|
func reconcileAppInstallTaskFailure(install *model.AppInstall, operation string, appDB, taskDB *gorm.DB) error {
|
||||||
|
if !appInstallOperationPending(install.Status) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var operationTask model.Task
|
||||||
|
err := taskDB.Where("type = ? AND resource_id = ? AND operate = ?", "App", install.ID, operation).
|
||||||
|
Order("created_at DESC").First(&operationTask).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if operationTask.Status != constant.StatusFailed && operationTask.Status != constant.StatusCanceled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// A previous operation must not turn a newly queued retry into a failure.
|
||||||
|
if operationTask.CreatedAt.Before(install.UpdatedAt) &&
|
||||||
|
(operationTask.EndAt.IsZero() || operationTask.EndAt.Before(install.UpdatedAt)) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
message := operationTask.ErrorMsg
|
||||||
|
if message == "" {
|
||||||
|
message = "the application operation task failed"
|
||||||
|
}
|
||||||
|
status := appInstallOperationFailureStatus(install.Status)
|
||||||
|
result := appDB.Model(&model.AppInstall{}).
|
||||||
|
Where("id = ? AND status = ? AND updated_at = ?", install.ID, install.Status, install.UpdatedAt).
|
||||||
|
Updates(map[string]interface{}{"status": status, "message": message})
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected > 0 {
|
||||||
|
install.Status = status
|
||||||
|
install.Message = message
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/glebarez/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func operationStatusTestDB(t *testing.T) (*gorm.DB, *gorm.DB, model.AppInstall) {
|
||||||
|
t.Helper()
|
||||||
|
open := func(name string, schema interface{}) *gorm.DB {
|
||||||
|
db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), name)), &gorm.Config{
|
||||||
|
Logger: logger.Default.LogMode(logger.Silent),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := db.AutoMigrate(schema); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
sqlDB, _ := db.DB()
|
||||||
|
if sqlDB != nil {
|
||||||
|
_ = sqlDB.Close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
appDB := open("apps.db", &model.AppInstall{})
|
||||||
|
taskDB := open("tasks.db", &model.Task{})
|
||||||
|
install := model.AppInstall{
|
||||||
|
Name: "gb10-ds-vision", AppId: 1, AppDetailId: 1,
|
||||||
|
Version: "nvidia-gb10-dspark-0.1.1", ContainerName: "vllm-dspark-1", ServiceName: "vllm-dspark",
|
||||||
|
Status: constant.StatusStarting, Env: `{"MODEL_DIR":"/models/vision"}`,
|
||||||
|
}
|
||||||
|
if err := appDB.Omit(clause.Associations).Create(&install).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return appDB, taskDB, install
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppInstallFailedTaskRecoversPendingState(t *testing.T) {
|
||||||
|
for _, state := range []string{constant.StatusStarting, constant.StatusWaiting, constant.StatusRestarting, constant.StatusInstalling} {
|
||||||
|
t.Run(state, func(t *testing.T) {
|
||||||
|
appDB, taskDB, install := operationStatusTestDB(t)
|
||||||
|
install.Status = state
|
||||||
|
if err := appDB.Omit(clause.Associations).Save(&install).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
task := model.Task{ID: "failed", Type: "App", Operate: "TaskUpdate", ResourceID: install.ID,
|
||||||
|
Status: constant.StatusFailed, ErrorMsg: "model startup script failed", CreatedAt: install.UpdatedAt.Add(time.Second), EndAt: install.UpdatedAt.Add(2 * time.Second)}
|
||||||
|
if err := taskDB.Create(&task).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := reconcileAppInstallTaskFailure(&install, task.Operate, appDB, taskDB); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := constant.StatusUpErr
|
||||||
|
if state == constant.StatusInstalling {
|
||||||
|
want = constant.StatusInstallErr
|
||||||
|
}
|
||||||
|
var stored model.AppInstall
|
||||||
|
if err := appDB.First(&stored, install.ID).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if install.Status != want || stored.Status != want || stored.Message != task.ErrorMsg || stored.Env != install.Env {
|
||||||
|
t.Fatalf("recovered state = %s/%s, message %q, env %q", install.Status, stored.Status, stored.Message, stored.Env)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppInstallPendingStateKeepsActiveAndUnrelatedTasks(t *testing.T) {
|
||||||
|
for _, taskStatus := range []string{constant.StatusExecuting, constant.StatusSuccess, "missing", "previous-failure", "other-resource"} {
|
||||||
|
t.Run(taskStatus, func(t *testing.T) {
|
||||||
|
appDB, taskDB, install := operationStatusTestDB(t)
|
||||||
|
task := model.Task{ID: "task", Type: "App", Operate: "TaskUpdate", ResourceID: install.ID,
|
||||||
|
Status: taskStatus, CreatedAt: install.UpdatedAt.Add(time.Second), EndAt: install.UpdatedAt.Add(2 * time.Second)}
|
||||||
|
if taskStatus == "previous-failure" {
|
||||||
|
task.Status = constant.StatusFailed
|
||||||
|
task.CreatedAt = install.UpdatedAt.Add(-2 * time.Hour)
|
||||||
|
task.EndAt = install.UpdatedAt.Add(-time.Hour)
|
||||||
|
}
|
||||||
|
if taskStatus == "other-resource" {
|
||||||
|
task.Status = constant.StatusFailed
|
||||||
|
task.ResourceID++
|
||||||
|
}
|
||||||
|
if taskStatus != "missing" {
|
||||||
|
if err := taskDB.Create(&task).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := reconcileAppInstallTaskFailure(&install, task.Operate, appDB, taskDB); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if install.Status != constant.StatusStarting {
|
||||||
|
t.Fatalf("unrelated or active task changed pending state to %s", install.Status)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppInstallRecoveryDoesNotOverwriteConcurrentRetry(t *testing.T) {
|
||||||
|
appDB, taskDB, install := operationStatusTestDB(t)
|
||||||
|
task := model.Task{ID: "failed", Type: "App", Operate: "TaskUpdate", ResourceID: install.ID,
|
||||||
|
Status: constant.StatusCanceled, ErrorMsg: "interrupted", CreatedAt: install.UpdatedAt.Add(time.Second), EndAt: install.UpdatedAt.Add(2 * time.Second)}
|
||||||
|
if err := taskDB.Create(&task).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := appDB.Callback().Update().Before("gorm:update").Register("new-operation", func(db *gorm.DB) {
|
||||||
|
if err := db.Exec("UPDATE app_installs SET updated_at = ?, message = ? WHERE id = ?", install.UpdatedAt.Add(time.Hour), "new attempt", install.ID).Error; err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := reconcileAppInstallTaskFailure(&install, task.Operate, appDB, taskDB); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var stored model.AppInstall
|
||||||
|
if err := appDB.First(&stored, install.ID).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if stored.Status != constant.StatusStarting || stored.Message != "new attempt" {
|
||||||
|
t.Fatalf("recovery overwrote a newer operation: %s, %s", stored.Status, stored.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppInstallRestartRecoveryCoversLifecycleStates(t *testing.T) {
|
||||||
|
for _, state := range []string{constant.StatusStarting, constant.StatusWaiting, constant.StatusInstalling} {
|
||||||
|
if !appInstallOperationInterruptedOnRestart(state) {
|
||||||
|
t.Errorf("restart recovery omitted %s", state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, state := range []string{constant.StatusRunning, constant.StatusStopped, constant.StatusRestarting} {
|
||||||
|
if appInstallOperationInterruptedOnRestart(state) {
|
||||||
|
t.Errorf("restart recovery changes a Docker runtime state: %s", state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ensureCurrentWebsiteDirectory(directory string) error {
|
||||||
|
changed := errors.New("website directory changed; inspect the installation before retrying")
|
||||||
|
if directory == "" || global.DB == nil {
|
||||||
|
return changed
|
||||||
|
}
|
||||||
|
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
ctx := context.WithValue(tx.Statement.Context, constant.DB, tx)
|
||||||
|
setting, err := settingRepo.GetWithContext(ctx, settingRepo.WithByKey("WEBSITE_DIR"))
|
||||||
|
missing := errors.Is(err, gorm.ErrRecordNotFound)
|
||||||
|
if err != nil && !missing {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
current := setting.Value
|
||||||
|
if current == "" {
|
||||||
|
current = path.Join(global.Dir.BaseDir, "1panel", "www")
|
||||||
|
}
|
||||||
|
if directory != current {
|
||||||
|
return changed
|
||||||
|
}
|
||||||
|
if missing {
|
||||||
|
if err := settingRepo.CreateWithContext(ctx, &model.Setting{Key: "WEBSITE_DIR", Value: directory}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if setting.Value == "" {
|
||||||
|
updated, err := settingRepo.UpdateIfMatch(ctx, setting.ID, "", directory)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if updated != 1 {
|
||||||
|
return changed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return os.MkdirAll(path.Join(directory, "conf.d"), constant.DirPerm)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAppStoreIndexUsesCacheUntilRevisionChanges(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
cache string
|
||||||
|
version int
|
||||||
|
failZIP bool
|
||||||
|
wantZIP int32
|
||||||
|
wantUpdate bool
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{name: "matching cache", cache: `{"lastModified":42}`, version: 42},
|
||||||
|
{name: "missing cache", version: 42, wantZIP: 1},
|
||||||
|
{name: "malformed cache", cache: `{invalid`, version: 42, wantZIP: 1},
|
||||||
|
{name: "null cache", cache: `null`, version: 42, wantZIP: 1},
|
||||||
|
{name: "trailing corrupt data", cache: `{"lastModified":42} garbage`, version: 42, wantZIP: 1},
|
||||||
|
{name: "outdated cache", cache: `{"lastModified":41}`, version: 42, wantZIP: 1},
|
||||||
|
{name: "remote revision changed", cache: `{"lastModified":42}`, version: 43, wantUpdate: true},
|
||||||
|
{name: "download failure", cache: `{invalid`, version: 42, failZIP: true, wantZIP: 1, wantErr: true},
|
||||||
|
{name: "cached minimum panel version", cache: `{"lastModified":42,"additionalProperties":{"version":"9.9.0"}}`, version: 42, wantErr: true},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
db := iconSyncDB(t)
|
||||||
|
if err := db.Create(&model.App{Key: "mysql", Type: "website", Status: constant.AppNormal, Icon: "app_mysql.png", BatchInstallSupport: true}).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
global.Dir.ResourceDir = t.TempDir()
|
||||||
|
cachePath := filepath.Join(global.Dir.ResourceDir, "1panel.json")
|
||||||
|
if tc.cache != "" {
|
||||||
|
appCacheTestWrite(t, cachePath, tc.cache)
|
||||||
|
}
|
||||||
|
list := dto.AppList{LastModified: tc.version, Apps: []dto.AppDefine{{Icon: "https://cdn.example/mysql.png", AppProperty: dto.AppProperty{Key: "mysql"}}}}
|
||||||
|
var archive bytes.Buffer
|
||||||
|
zw := zip.NewWriter(&archive)
|
||||||
|
entry, err := zw.Create("1panel.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := json.NewEncoder(entry).Encode(list); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := zw.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var versionCalls, zipCalls atomic.Int32
|
||||||
|
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch {
|
||||||
|
case strings.HasSuffix(r.URL.Path, "1panel.json.version.txt"):
|
||||||
|
versionCalls.Add(1)
|
||||||
|
_, _ = io.WriteString(w, strconv.Itoa(tc.version))
|
||||||
|
case strings.HasSuffix(r.URL.Path, "1panel.json.zip"):
|
||||||
|
zipCalls.Add(1)
|
||||||
|
if tc.failZIP {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = w.Write(archive.Bytes())
|
||||||
|
default:
|
||||||
|
t.Errorf("unexpected remote resource request: %s", r.URL)
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
transport := server.Client().Transport.(*http.Transport).Clone()
|
||||||
|
transport.TLSClientConfig.InsecureSkipVerify = true
|
||||||
|
transport.DialContext = func(ctx context.Context, network, _ string) (net.Conn, error) {
|
||||||
|
return (&net.Dialer{}).DialContext(ctx, network, server.Listener.Addr().String())
|
||||||
|
}
|
||||||
|
oldProvider := xpack.MultiNodeProvider
|
||||||
|
xpack.MultiNodeProvider = iconSyncTransport{MultiNodeProvider: oldProvider, transport: transport}
|
||||||
|
t.Cleanup(func() { xpack.MultiNodeProvider = oldProvider; transport.CloseIdleConnections() })
|
||||||
|
|
||||||
|
res, err := (AppService{}).GetAppUpdate()
|
||||||
|
if (err != nil) != tc.wantErr || res == nil && !tc.wantErr {
|
||||||
|
t.Fatalf("update check = %+v, %v; want error %v", res, err, tc.wantErr)
|
||||||
|
}
|
||||||
|
if zipCalls.Load() != tc.wantZIP || versionCalls.Load() != 1 {
|
||||||
|
t.Fatalf("first check requests: version=%d, ZIP=%d; want 1/%d", versionCalls.Load(), zipCalls.Load(), tc.wantZIP)
|
||||||
|
}
|
||||||
|
if tc.wantErr {
|
||||||
|
if tc.wantZIP == 0 && !strings.Contains(err.Error(), "ErrVersionTooLow") {
|
||||||
|
t.Fatalf("cached panel-version check was lost: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if res.CanUpdate != tc.wantUpdate {
|
||||||
|
t.Fatalf("CanUpdate=%v, want %v", res.CanUpdate, tc.wantUpdate)
|
||||||
|
}
|
||||||
|
if tc.wantUpdate {
|
||||||
|
// Full synchronization still fetches the new index after detecting a revision change.
|
||||||
|
fresh, err := getAppList()
|
||||||
|
if err != nil || fresh.LastModified != tc.version || zipCalls.Load() != 1 {
|
||||||
|
t.Fatalf("changed revision did not fetch the new index: %+v, %v, ZIP=%d", fresh, err, zipCalls.Load())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if res.AppList == nil || res.AppList.LastModified != 42 {
|
||||||
|
t.Fatalf("same-revision icon refresh lost its index: %+v", res)
|
||||||
|
}
|
||||||
|
if _, err := (AppService{}).GetAppUpdate(); err != nil || zipCalls.Load() != tc.wantZIP || versionCalls.Load() != 2 {
|
||||||
|
t.Fatalf("second check redownloaded cached index: %v, version=%d, ZIP=%d", err, versionCalls.Load(), zipCalls.Load())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user