Compare commits

...
Author SHA1 Message Date
ssongliu 547f9775b0 fix(firewall): support legacy ip RA checks and link documentation (#14083) 2026-10-14 15:57:07 +08:00
ssongliu f37afb1a37 fix: sync core panel port whitelist on agent startup (#14080) 2026-10-14 11:28:46 +08:00
CityFun 3b89d83554 feat(hermes-agent): support version upgrades (#14079) 2026-10-14 11:28:32 +08:00
蘭 12bfe6a1bd fix: Fix Webhook Response Judgment and Optimize Configuration and Testing Experience (#14077) 2026-10-14 11:28:21 +08:00
ssongliu aaf70cfc41 fix(log): improve operation logs (#14076) 2026-10-13 18:28:34 +08:00
Yuki Guo 1ecd4f7f0c fix: container log text (#14066) 2026-10-13 17:56:52 +08:00
CityFun a8ccdcade7 fix(ai-gateway): support backup logic during app upgrades (#14072) 2026-10-13 17:28:42 +08:00
ssongliu 4a89c0c031 fix: prevent refresh loop after unbinding current node license (#14064) 2026-10-13 11:20:48 +08:00
ssongliu f21ebb1a36 chore(deps): consolidate dependency and security updates (#14063) 2026-10-13 11:20:43 +08:00
ssongliu 3011383b07 fix(compose): handle relative paths and preserve multi-file projects (#14062) 2026-10-13 11:03:17 +08:00
Yuki Guo fd12866897 fix: fix website files (#14042)
* refactor: refactor website files

* refactor: refactor website files
2026-10-13 10:56:30 +08:00
ssongliu bc6cf519ce fix: improve database file handling (#14043) 2026-10-13 09:39:27 +08:00
ssongliu f9c43989fe fix: improve firewall compatibility (#14046) 2026-10-13 09:39:18 +08:00
Yuki Guo e90b201001 refactor: remove user tooltip (#14041) 2026-10-12 18:18:44 +08:00
ssongliu f7394938f7 chore(deps): update remaining dependencies and VueUse 15 (#14040) 2026-10-12 16:05:35 +08:00
ssongliu dada7294dd fix(firewall): preserve fail2ban priority when binding iptables chains (#14039)
Refs #14019
2026-10-12 15:54:55 +08:00
ssongliu afcb94f6ef feat(vllm): add request metrics and optimize monitor history (#14032) 2026-10-12 15:54:42 +08:00
ssongliu 5a2e430dff fix(home): calculate overview rates from actual sample intervals (#14033) 2026-10-12 15:52:34 +08:00
Yuki Guo f2ca4f56e6 feat: add Netcup DNS (#14038) 2026-10-12 15:03:07 +08:00
ssongliu b6f609aaee chore(deps): update dependencies and apply security patches (#14037) 2026-10-12 14:49:01 +08:00
Yuki Guo c960c0137d refactor: refactor user info (#14036) 2026-10-12 14:17:13 +08:00
Yuki Guo 09a901294e fix: local image upload package (#14031) 2026-10-12 10:08:07 +08:00
Yuki Guo 4d481a8f31 refactor: refactor website ssl delete text (#14024) 2026-10-12 10:07:58 +08:00
Yuki Guo f1e0aa3391 feat(container): support local image uploads and improve import task … (#14025)
* feat(container): support local image uploads and improve import task logs

* feat(container): support local image uploads and improve import task logs

* fix(container): remove duplicate image load response helper
2026-10-12 09:41:36 +08:00
Yuki Guo f475500543 fix(frontend): reset search filters and pagination on page visits (#14016) 2026-10-10 17:56:38 +08:00
Yuki Guo 8415a1ce28 refactor: refactor user info (#14021) 2026-10-10 17:50:05 +08:00
Yuki Guo faf4933a02 refactor: refactor website ssl keyType default value (#14023) 2026-10-10 17:49:14 +08:00
Yuki Guo c692a9f5e3 refactor: refactor website upload css (#14022)
* refactor: refactor website upload css

* refactor: refactor website upload css
2026-10-10 17:48:57 +08:00
CityFun e7f0c21715 fix(vllm): resolve stuck startup state blocking operations (#14017) 2026-10-10 11:13:17 +08:00
ssongliu da7e8f1dbf fix: Remove duplicate TaskScopeVm (#14020) 2026-10-10 10:59:22 +08:00
Yuki Guoandssongliu 486cd5d0ad feat(redis): enable remote terminal through tracked background tasks (#14012)
* feat(redis): enable remote terminal through tracked background tasks

* feat(redis): enable remote terminal through tracked background tasks

* refactor(redis): remove redundant CLI status tracking

* refactor(redis): retain existing task execution flow

---------

Co-authored-by: ssongliu <sloooop1x@gmail.com>
2026-10-09 18:20:51 +08:00
ssongliu 00ce5a7b36 fix(firewall): preserve IPv6 RA protection and simplify error messages (#14015) 2026-10-09 17:45:57 +08:00
ssongliu c17f055e06 fix: preserve VM task timeout cleanup and terminate command groups (#14008) 2026-10-09 17:45:41 +08:00
王贺 f22df4b29c fix(file): stabilize closable tab width to avoid layout jitter (#14013)
The closable card tabs animated the close icon width between active and inactive tabs, so each tab switch reflowed the tab bar and caused the page to jitter. Reserve the close icon slot and toggle it with opacity instead, matching the terminal tab implementation.
2026-10-09 17:16:10 +08:00
Yuki Guoandssongliu b9bab90a14 fix: fix clean network (#14009)
* fix: fix clean network

* refactor(container): simplify network cleanup

---------

Co-authored-by: ssongliu <sloooop1x@gmail.com>
2026-10-09 17:14:50 +08:00
175 changed files with 5459 additions and 3569 deletions
+20 -3
View File
@@ -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"
) )
@@ -378,10 +381,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
+10 -4
View File
@@ -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
+3 -3
View File
@@ -318,7 +318,7 @@ 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":["instanceKey"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙规则 [instanceKey]","formatEN":"update firewall rule [instanceKey]"} // @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 {
@@ -340,7 +340,7 @@ 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":["instanceKey"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"调整防火墙规则顺序 [instanceKey]","formatEN":"reorder firewall rule [instanceKey]"} // @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 {
@@ -556,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 {
+1 -1
View File
@@ -11,7 +11,7 @@ func (b *BaseApi) LoadVLLMMonitor(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
data, err := monitorService.LoadVLLMMonitorData(req) data, err := monitorService.LoadVLLMMonitorData(c.Request.Context(), req)
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
+4
View File
@@ -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
-1
View File
@@ -55,7 +55,6 @@ type FirewallBackendFamilyStatus struct {
Initialized bool `json:"initialized"` Initialized bool `json:"initialized"`
Bound bool `json:"bound"` Bound bool `json:"bound"`
Reason string `json:"reason,omitempty"` Reason string `json:"reason,omitempty"`
ForwardPolicy string `json:"forwardPolicy,omitempty"`
RAInterfaces []string `json:"raInterfaces,omitempty"` RAInterfaces []string `json:"raInterfaces,omitempty"`
} }
+1
View File
@@ -127,6 +127,7 @@ type GPUProcess struct {
} }
type MonitorVLLMSearch struct { type MonitorVLLMSearch struct {
Group string `json:"group" validate:"omitempty,oneof=overview latency requests"`
AppInstallID uint `json:"appInstallID" validate:"required"` AppInstallID uint `json:"appInstallID" validate:"required"`
StartTime time.Time `json:"startTime" validate:"required"` StartTime time.Time `json:"startTime" validate:"required"`
EndTime time.Time `json:"endTime" validate:"required"` EndTime time.Time `json:"endTime" validate:"required"`
+8
View File
@@ -85,6 +85,14 @@ type MonitorVLLM struct {
Status string `json:"status"` Status string `json:"status"`
RawMetrics string `json:"-"` RawMetrics string `json:"-"`
HistogramDeltas string `json:"-"` HistogramDeltas string `json:"-"`
IntervalSeconds float64 `json:"intervalSeconds" gorm:"-"`
RequestDeltas string `json:"-"`
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"` Running *float64 `json:"running"`
Waiting *float64 `json:"waiting"` Waiting *float64 `json:"waiting"`
+58 -12
View File
@@ -1,6 +1,7 @@
package repo package repo
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
@@ -16,7 +17,6 @@ type VLLMMonitorRepo struct{}
type VLLMHistoryPoint struct { type VLLMHistoryPoint struct {
model.MonitorVLLM model.MonitorVLLM
Bucket int64 Bucket int64
HistogramSamples string
} }
func (r *VLLMMonitorRepo) Create(point *model.MonitorVLLM) error { func (r *VLLMMonitorRepo) Create(point *model.MonitorVLLM) error {
@@ -41,16 +41,30 @@ func (r *VLLMMonitorRepo) DeleteBefore(before time.Time) error {
return global.VLLMMonitorDB.Where("created_at < ?", before).Delete(&model.MonitorVLLM{}).Error return global.VLLMMonitorDB.Where("created_at < ?", before).Delete(&model.MonitorVLLM{}).Error
} }
func (r *VLLMMonitorRepo) Count(id uint, start, end time.Time) (int64, error) { func (r *VLLMMonitorRepo) Count(ctx context.Context, id uint, start, end time.Time) (int64, error) {
var count int64 var count int64
db := global.VLLMMonitorDB.Model(&model.MonitorVLLM{}).Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end) 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 err := db.Count(&count).Error
return count, err return count, err
} }
func (r *VLLMMonitorRepo) History(id uint, start, end time.Time, seconds int64, aggregation string) ([]VLLMHistoryPoint, error) { func (r *VLLMMonitorRepo) History(ctx context.Context, id uint, start, end time.Time, seconds int64, aggregation, group string) ([]VLLMHistoryPoint, error) {
db := global.VLLMMonitorDB.Model(&model.MonitorVLLM{}).Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end) db := global.VLLMMonitorDB.WithContext(ctx).Model(&model.MonitorVLLM{}).Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end)
metrics := []string{"running", "waiting", "cache_usage", "prompt_throughput", "generation_throughput", "request_throughput", "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"} 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 var columns []string
if seconds > 0 { if seconds > 0 {
operation := "AVG" operation := "AVG"
@@ -59,20 +73,52 @@ func (r *VLLMMonitorRepo) History(id uint, start, end time.Time, seconds int64,
} }
columns = []string{fmt.Sprintf("(CAST(strftime('%%s', created_at) AS INTEGER) - %d) / %d AS bucket", start.Unix(), seconds)} columns = []string{fmt.Sprintf("(CAST(strftime('%%s', created_at) AS INTEGER) - %d) / %d AS bucket", start.Unix(), seconds)}
for _, column := range metrics { for _, column := range metrics {
if aggregation != "max" && (strings.HasPrefix(column, "time_to_first_token_p") || strings.HasPrefix(column, "time_per_output_token_p") || strings.HasPrefix(column, "request_latency_p")) {
continue
}
columns = append(columns, operation+"("+column+") AS "+column) columns = append(columns, operation+"("+column+") AS "+column)
} }
if aggregation != "max" {
columns = append(columns, "json_group_array(json(NULLIF(histogram_deltas, ''))) AS histogram_samples")
}
db = db.Group("bucket").Order("bucket ASC") db = db.Group("bucket").Order("bucket ASC")
} else { } else {
columns = append([]string{"id", "created_at", "app_install_id", "status"}, metrics...) columns = append([]string{"id", "created_at", "app_install_id", "status"}, metrics...)
if group == "" || group == "requests" {
columns = append(columns, "histogram_deltas", "request_deltas")
}
db = db.Order("created_at ASC, id ASC") db = db.Order("created_at ASC, id ASC")
} }
var points []VLLMHistoryPoint var points []VLLMHistoryPoint
err := db.Select(strings.Join(columns, ", ")).Scan(&points).Error err := db.Select(strings.Join(columns, ", ")).Scan(&points).Error
return points, err 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()
}
+12
View File
@@ -16,6 +16,7 @@ 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)
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
Update(key, value string) error Update(key, value string) error
WithByKey(key string) DBOption WithByKey(key string) DBOption
@@ -74,6 +75,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)
+4 -1
View File
@@ -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"
@@ -110,7 +111,9 @@ func (t TaskRepo) Update(ctx context.Context, task *model.Task) error {
} }
func (t TaskRepo) UpdateRunningTaskToFailed() error { func (t TaskRepo) UpdateRunningTaskToFailed() error {
return getTaskDb(t.WithByStatus(constant.StatusExecuting)).Model(&model.Task{}).Updates(map[string]interface{}{"status": constant.StatusFailed, "error_msg": "1Panel restart causes failure"}).Error return getTaskDb(t.WithByStatus(constant.StatusExecuting)).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() (int64, error) {
+4 -4
View File
@@ -652,7 +652,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)
@@ -660,12 +660,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 {
+63
View File
@@ -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
}
+37
View File
@@ -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])
})
}
+7 -4
View File
@@ -77,7 +77,7 @@ type IAlertService interface {
UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) error UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) error
DeleteAlertConfig(id uint) error DeleteAlertConfig(id uint) 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 {
@@ -851,7 +851,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()
@@ -899,7 +899,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")
} }
@@ -928,12 +928,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 {
+6 -1
View File
@@ -550,6 +550,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 +562,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
} }
+43 -7
View File
@@ -339,6 +339,11 @@ func operateAppWithLifecycleScripts(install model.AppInstall, req request.AppIns
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
} }
operation := string(req.Operate) operation := string(req.Operate)
@@ -567,11 +572,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
} }
@@ -678,14 +687,17 @@ 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)
@@ -726,7 +738,7 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
} }
} }
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)
}) })
return versions, nil return versions, nil
} }
@@ -931,10 +943,34 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
} }
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.StatusRestarting, constant.StatusWaiting:
return nil 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 {
+76
View File
@@ -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)
}
}
}
+8
View File
@@ -106,6 +106,14 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
if err != nil { if err != nil {
return err return err
} }
if !isAIGatewayUpgradeVersionAllowed(install.App.Key, install.Version, detail.Version) {
return buserr.New("ErrAIGatewayDatabaseTypeMismatch")
}
if isHermesApp(install.App.Key) &&
(!isNewerAppVersion(install.App.Key, detail.Version, install.Version) ||
(isCrossAppVersion(install.App.Key, install.Version, detail.Version) && !install.App.CrossVersionUpdate)) {
return buserr.New("ErrHermesUpgradeVersion")
}
if err = checkVllmVersionAccess(install.App.Key, detail.Version); err != nil { if err = checkVllmVersionAccess(install.App.Key, detail.Version); err != nil {
return err return err
} }
+20 -8
View File
@@ -1406,9 +1406,8 @@ func handleErr(install model.AppInstall, err error, out string) error {
} }
func doNotNeedSync(installed model.AppInstall) bool { func doNotNeedSync(installed model.AppInstall) bool {
return installed.Status == constant.StatusInstalling || installed.Status == constant.StatusRebuilding || installed.Status == constant.StatusUpgrading || return appInstallOperationPending(installed.Status) || installed.Status == constant.StatusSyncing ||
installed.Status == constant.StatusSyncing || installed.Status == constant.StatusUninstalling || installed.Status == constant.StatusInstallErr || installed.Status == constant.StatusInstallErr || installed.Status == constant.StatusUpgradeErr || installed.Status == constant.StatusUpErr
installed.Status == constant.StatusStarting || installed.Status == constant.StatusRestarting || installed.Status == constant.StatusWaiting
} }
func synAppInstall(containers map[string]container.Summary, appInstall *model.AppInstall, force bool) { func synAppInstall(containers map[string]container.Summary, appInstall *model.AppInstall, force bool) {
@@ -1502,6 +1501,11 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
} }
for _, installed := range appInstallList { for _, installed := range appInstallList {
if sync && appInstallOperationPending(installed.Status) {
if err := syncAppInstallStatus(&installed, false); err != nil {
return nil, err
}
}
if updated && ignoreUpdate(installed) { if updated && ignoreUpdate(installed) {
continue continue
} }
@@ -1567,11 +1571,14 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
var versions []string var versions []string
for _, appDetail := range details { for _, appDetail := range details {
if !isAIGatewayUpgradeVersionAllowed(installed.App.Key, installed.Version, appDetail.Version) {
continue
}
ignores, _ := appIgnoreUpgradeRepo.List(runtimeRepo.WithDetailId(appDetail.ID), appIgnoreUpgradeRepo.WithScope("version")) ignores, _ := appIgnoreUpgradeRepo.List(runtimeRepo.WithDetailId(appDetail.ID), appIgnoreUpgradeRepo.WithScope("version"))
if len(ignores) > 0 { if len(ignores) > 0 {
continue continue
} }
if common.IsCrossVersion(installed.Version, appDetail.Version) && !installed.App.CrossVersionUpdate { if isCrossAppVersion(installed.App.Key, installed.Version, appDetail.Version) && !installed.App.CrossVersionUpdate {
continue continue
} }
versions = append(versions, appDetail.Version) versions = append(versions, appDetail.Version)
@@ -1586,7 +1593,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
continue continue
} }
versions = common.GetSortedVersions(versions) versions = sortAppVersions(installed.App.Key, versions)
lastVersion := versions[0] lastVersion := versions[0]
if installed.App.Key == constant.AppMysql || installed.App.Key == constant.AppMysqlCluster { if installed.App.Key == constant.AppMysql || installed.App.Key == constant.AppMysqlCluster {
@@ -1601,10 +1608,10 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
} }
} }
if common.IsCrossVersion(installed.Version, lastVersion) { if isCrossAppVersion(installed.App.Key, installed.Version, lastVersion) {
installDTO.CanUpdate = installed.App.CrossVersionUpdate installDTO.CanUpdate = installed.App.CrossVersionUpdate
} else { } else {
installDTO.CanUpdate = common.CompareVersion(lastVersion, installed.Version) installDTO.CanUpdate = isNewerAppVersion(installed.App.Key, lastVersion, installed.Version)
} }
if updated { if updated {
@@ -2275,7 +2282,12 @@ func getAppVersions(key string, details []model.AppDetail) []string {
} }
versionsRaw = append(versionsRaw, detail.Version) versionsRaw = append(versionsRaw, detail.Version)
} }
versions = common.GetSortedVersions(versionsRaw) if isAIGatewayApp(key) {
sortAIGatewayVersions(versionsRaw)
versions = versionsRaw
} else {
versions = sortAppVersions(key, versionsRaw)
}
if hasLatest { if hasLatest {
versions = append([]string{latestVersion}, versions...) versions = append([]string{latestVersion}, versions...)
} }
+22 -6
View File
@@ -132,11 +132,9 @@ func (u *BackupService) AppRecover(req dto.CommonRecover) error {
if !fileOp.Stat(req.File) { if !fileOp.Stat(req.File) {
return buserr.WithName("ErrFileNotFound", req.File) return buserr.WithName("ErrFileNotFound", req.File)
} }
if _, err := compose.Down(install.GetComposePath()); err != nil {
return err
}
if err := handleAppRecover(&install, nil, req.File, false, req.Secret, req.TaskID); err != nil { if err := handleAppRecover(&install, nil, req.File, false, req.Secret, req.TaskID); err != nil {
global.LOG.Errorf("recover app %s failed, err: %v", req.DetailName, err) global.LOG.Errorf("recover app %s failed, err: %v", req.DetailName, err)
return err
} }
return nil return nil
} }
@@ -200,8 +198,20 @@ func handleAppBackup(install *model.AppInstall, parentTask *task.Task, recordID
} }
func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverFile string, isRollback bool, secret, taskID string) error { func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverFile string, isRollback bool, secret, taskID string) error {
// Validate AI Gateway backups before stopping the app, creating rollback
// backups, or touching its files/database. Keep the validated extraction for
// the restore so uploaded/encrypted archives are not unpacked twice.
preparedPath, cleanup, err := prepareAIGatewayAppRecover(install, recoverFile, secret)
if err != nil {
return err
}
if parentTask == nil && !isRollback {
if _, err := compose.Down(install.GetComposePath()); err != nil {
cleanup()
return err
}
}
var ( var (
err error
recoverTask *task.Task recoverTask *task.Task
isOk = false isOk = false
rollbackFile string rollbackFile string
@@ -210,11 +220,13 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
if parentTask == nil { if parentTask == nil {
recoverTask, err = task.NewTaskWithOps(install.Name, task.TaskRecover, task.TaskScopeBackup, taskID, install.ID) recoverTask, err = task.NewTaskWithOps(install.Name, task.TaskRecover, task.TaskScopeBackup, taskID, install.ID)
if err != nil { if err != nil {
cleanup()
return err return err
} }
} }
recoverApp := func(t *task.Task) error { recoverApp := func(t *task.Task) error {
defer cleanup()
fileOp := files.NewFileOp() fileOp := files.NewFileOp()
if !isRollback { if !isRollback {
rollbackFile = path.Join(global.Dir.TmpDir, fmt.Sprintf("app/%s_%s.tar.gz", install.Name, time.Now().Format(constant.DateTimeSlimLayout))) rollbackFile = path.Join(global.Dir.TmpDir, fmt.Sprintf("app/%s_%s.tar.gz", install.Name, time.Now().Format(constant.DateTimeSlimLayout)))
@@ -223,10 +235,13 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
} }
} }
tmpPath := preparedPath
if tmpPath == "" {
if err := fileOp.TarGzExtractPro(recoverFile, path.Dir(recoverFile), secret); err != nil { if err := fileOp.TarGzExtractPro(recoverFile, path.Dir(recoverFile), secret); err != nil {
return err return err
} }
tmpPath := strings.ReplaceAll(recoverFile, ".tar.gz", "") tmpPath = strings.ReplaceAll(recoverFile, ".tar.gz", "")
}
if err := fileOp.TarGzExtractPro(tmpPath+"/app.tar.gz", tmpPath, ""); err != nil { if err := fileOp.TarGzExtractPro(tmpPath+"/app.tar.gz", tmpPath, ""); err != nil {
return err return err
} }
@@ -236,7 +251,7 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
} else { } else {
_, _ = compose.Up(install.GetComposePath()) _, _ = compose.Up(install.GetComposePath())
} }
_ = os.RemoveAll(strings.ReplaceAll(recoverFile, ".tar.gz", "")) _ = os.RemoveAll(tmpPath)
}() }()
if !fileOp.Stat(tmpPath+"/app.json") || !fileOp.Stat(tmpPath+"/app.tar.gz") { if !fileOp.Stat(tmpPath+"/app.json") || !fileOp.Stat(tmpPath+"/app.tar.gz") {
@@ -397,6 +412,7 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
} }
recoverTask.AddSubTask(task.GetTaskName(install.Name, task.TaskRecover, task.TaskScopeBackup), recoverApp, rollBackApp) recoverTask.AddSubTask(task.GetTaskName(install.Name, task.TaskRecover, task.TaskScopeBackup), recoverApp, rollBackApp)
go func() { go func() {
defer cleanup()
_ = recoverTask.Execute() _ = recoverTask.Execute()
}() }()
return nil return nil
+80 -44
View File
@@ -371,43 +371,39 @@ func loadComposePathAndFiles(composeName string, dockerClient *client.Client) (s
} }
config := targetContainer.Labels[composeConfigLabel] config := targetContainer.Labels[composeConfigLabel]
workdir := targetContainer.Labels[composeWorkdirLabel] workdir := targetContainer.Labels[composeWorkdirLabel]
if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) { files, err := resolveComposeFiles(workdir, config)
composePath = config if err != nil {
} else { return "", nil, err
composePath = workdir
} }
composePath = strings.Join(files, ",")
} }
composeFiles := normalizeComposeFiles(composePath) composeFiles, err := normalizeComposeFiles(composePath)
if len(composeFiles) == 0 { if err != nil {
return "", nil, fmt.Errorf("compose file not found for %s", composeName) return "", nil, err
} }
return composePath, composeFiles, nil return strings.Join(composeFiles, ","), composeFiles, nil
} }
func normalizeComposeFiles(composePath string) []string { func normalizeComposeFiles(composePath string) ([]string, error) {
items := strings.Split(composePath, ",") files, err := resolveComposeFiles("", composePath)
result := make([]string, 0) if err != nil {
seen := make(map[string]struct{}) return nil, err
for _, item := range items {
item = strings.TrimSpace(item)
if item == "" {
continue
} }
stat, err := os.Stat(item) for i, file := range files {
if err == nil && stat.IsDir() { info, err := os.Stat(file)
item = path.Join(item, "docker-compose.yml") if err == nil && info.IsDir() {
file = filepath.Join(file, "docker-compose.yml")
info, err = os.Stat(file)
files[i] = file
} }
if _, err := os.Stat(item); err != nil { if err != nil {
continue return nil, err
} }
if _, ok := seen[item]; ok { if !info.Mode().IsRegular() {
continue return nil, fmt.Errorf("compose path %s is not a regular file", file)
} }
seen[item] = struct{}{}
result = append(result, item)
} }
sort.Strings(result) return files, nil
return result
} }
func (c *composeBackupContext) close() { func (c *composeBackupContext) close() {
@@ -621,20 +617,46 @@ func safeJoinWithinBase(baseDir, name string) (string, error) {
} }
func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error { func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
if recoverCtx.targetDir != "" { composeFiles := recoverCtx.meta.Files
_ = os.RemoveAll(recoverCtx.targetDir) if strings.TrimSpace(recoverCtx.meta.ComposePath) != "" {
filesByPath := make(map[string]composeBackupFile, len(composeFiles))
for _, item := range composeFiles {
filesByPath[filepath.Clean(item.OriginalPath)] = item
} }
if err := os.MkdirAll(recoverCtx.targetDir, os.ModePerm); err != nil { orderedFiles := make([]composeBackupFile, 0, len(composeFiles))
return err for _, file := range strings.Split(recoverCtx.meta.ComposePath, ",") {
file = strings.TrimSpace(file)
if file == "" {
continue
} }
restored := make([]string, 0, len(recoverCtx.meta.Files)) file = filepath.Clean(file)
for _, item := range recoverCtx.meta.Files { item, ok := filesByPath[file]
if !ok {
if len(composeFiles) != 1 || file != filepath.Dir(composeFiles[0].OriginalPath) {
return fmt.Errorf("compose file %s is missing from backup metadata", file)
}
item = composeFiles[0]
}
orderedFiles = append(orderedFiles, item)
}
composeFiles = orderedFiles
}
if len(composeFiles) == 0 {
return fmt.Errorf("compose files are missing from backup metadata")
}
restored := make([]string, 0, len(composeFiles))
sourceByTarget := make(map[string]string, len(composeFiles))
for _, item := range composeFiles {
backupPath, err := safeJoinWithinBase(recoverCtx.tmpDir, item.BackupPath) backupPath, err := safeJoinWithinBase(recoverCtx.tmpDir, item.BackupPath)
if err != nil { if err != nil {
return fmt.Errorf("invalid compose backup path %q, err: %v", item.BackupPath, err) return fmt.Errorf("invalid compose backup path %q, err: %v", item.BackupPath, err)
} }
if !recoverCtx.fileOp.Stat(backupPath) { info, err := os.Stat(backupPath)
continue if err != nil {
return err
}
if !info.Mode().IsRegular() {
return fmt.Errorf("compose backup path %s is not a regular file", backupPath)
} }
targetName := item.FileName targetName := item.FileName
if item.RelativePath != "" { if item.RelativePath != "" {
@@ -650,6 +672,29 @@ func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
if err != nil { if err != nil {
return fmt.Errorf("invalid compose target path %q, err: %v", targetName, err) return fmt.Errorf("invalid compose target path %q, err: %v", targetName, err)
} }
sourcePath := item.OriginalPath
if sourcePath == "" {
sourcePath = backupPath
}
sourcePath = filepath.Clean(sourcePath)
if previous, ok := sourceByTarget[targetPath]; ok && previous != sourcePath {
return fmt.Errorf("compose files %q and %q resolve to the same restore path %q", previous, sourcePath, targetPath)
}
sourceByTarget[targetPath] = sourcePath
restored = append(restored, targetPath)
}
if recoverCtx.targetDir != "" {
_ = os.RemoveAll(recoverCtx.targetDir)
}
if err := os.MkdirAll(recoverCtx.targetDir, os.ModePerm); err != nil {
return err
}
for i, item := range composeFiles {
backupPath, err := safeJoinWithinBase(recoverCtx.tmpDir, item.BackupPath)
if err != nil {
return fmt.Errorf("invalid compose backup path %q, err: %v", item.BackupPath, err)
}
targetPath := restored[i]
if err := os.MkdirAll(path.Dir(targetPath), os.ModePerm); err != nil { if err := os.MkdirAll(path.Dir(targetPath), os.ModePerm); err != nil {
return err return err
} }
@@ -660,7 +705,6 @@ func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
if err := recoverCtx.fileOp.SaveFile(targetPath, string(content), fs.ModePerm); err != nil { if err := recoverCtx.fileOp.SaveFile(targetPath, string(content), fs.ModePerm); err != nil {
return err return err
} }
restored = append(restored, targetPath)
} }
envPath := path.Join(recoverCtx.tmpDir, "compose_files", ".env") envPath := path.Join(recoverCtx.tmpDir, "compose_files", ".env")
if recoverCtx.fileOp.Stat(envPath) { if recoverCtx.fileOp.Stat(envPath) {
@@ -672,14 +716,6 @@ func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
return err return err
} }
} }
if len(restored) == 0 {
defaultPath := path.Join(recoverCtx.targetDir, "docker-compose.yml")
if !recoverCtx.fileOp.Stat(defaultPath) {
return fmt.Errorf("compose file not found in backup data")
}
restored = append(restored, defaultPath)
}
sort.Strings(restored)
recoverCtx.composePath = strings.Join(restored, ",") recoverCtx.composePath = strings.Join(restored, ",")
return nil return nil
} }
+63 -56
View File
@@ -326,40 +326,47 @@ func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) {
case "container": case "container":
inspectInfo, err = client.ContainerInspect(context.Background(), req.ID) inspectInfo, err = client.ContainerInspect(context.Background(), req.ID)
case "compose": case "compose":
filePath := "" config, workdir := "", ""
cli, err := docker.NewDockerClient()
if err != nil {
return "", err
}
defer cli.Close()
options := container.ListOptions{All: true} options := container.ListOptions{All: true}
options.Filters = filters.NewArgs() options.Filters = filters.NewArgs()
options.Filters.Add("label", fmt.Sprintf("%s=%s", composeProjectLabel, req.ID)) options.Filters.Add("label", fmt.Sprintf("%s=%s", composeProjectLabel, req.ID))
containers, err := cli.ContainerList(context.Background(), options) containers, err := client.ContainerList(context.Background(), options)
if err != nil { if err != nil {
return "", err return "", err
} }
for _, container := range containers {
config := container.Labels[composeConfigLabel]
if len(req.Detail) != 0 && strings.Contains(config, req.Detail) {
config = req.Detail
}
workdir := container.Labels[composeWorkdirLabel]
if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) {
filePath = config
break
} else {
filePath = workdir
break
}
}
if len(containers) == 0 { if len(containers) == 0 {
composeItem, _ := composeRepo.GetRecord(repo.WithByName(req.ID)) composeItem, err := composeRepo.GetRecord(repo.WithByName(req.ID))
filePath = composeItem.Path if err != nil {
}
if _, err := os.Stat(filePath); err != nil {
return "", err return "", err
} }
config = composeItem.Path
workdir = filepath.Dir(primaryComposePath(config))
} else {
config = containers[0].Labels[composeConfigLabel]
workdir = containers[0].Labels[composeWorkdirLabel]
}
composeFiles, err := resolveComposeFiles(workdir, config)
if err != nil {
return "", err
}
filePath := composeFiles[0]
if strings.TrimSpace(req.Detail) != "" {
selectedFiles, err := resolveComposeFiles(workdir, req.Detail)
if err != nil {
return "", err
}
if len(selectedFiles) != 1 || !slices.Contains(composeFiles, selectedFiles[0]) {
return "", fmt.Errorf("compose file %s does not belong to project %s", req.Detail, req.ID)
}
filePath = selectedFiles[0]
}
info, err := os.Stat(filePath)
if err != nil {
return "", err
}
if !info.Mode().IsRegular() {
return "", fmt.Errorf("compose path %s is not a regular file", filePath)
}
content, err := os.ReadFile(filePath) content, err := os.ReadFile(filePath)
if err != nil { if err != nil {
return "", err return "", err
@@ -383,11 +390,6 @@ func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) {
} }
func (u *ContainerService) Prune(req dto.ContainerPrune) error { func (u *ContainerService) Prune(req dto.ContainerPrune) error {
client, err := docker.NewDockerClient()
if err != nil {
return err
}
defer client.Close()
name := "" name := ""
switch req.PruneType { switch req.PruneType {
case "container": case "container":
@@ -408,6 +410,14 @@ func (u *ContainerService) Prune(req dto.ContainerPrune) error {
} }
taskItem.AddSubTask(i18n.GetMsgByKey("TaskClean"), func(t *task.Task) error { taskItem.AddSubTask(i18n.GetMsgByKey("TaskClean"), func(t *task.Task) error {
if err := t.TaskCtx.Err(); err != nil {
return err
}
client, err := docker.NewDockerClient()
if err != nil {
return err
}
defer client.Close()
pruneFilters := filters.NewArgs() pruneFilters := filters.NewArgs()
if req.WithTagAll { if req.WithTagAll {
pruneFilters.Add("dangling", "false") pruneFilters.Add("dangling", "false")
@@ -431,10 +441,7 @@ func (u *ContainerService) Prune(req dto.ContainerPrune) error {
} }
SpaceReclaimed = int(rep.SpaceReclaimed) SpaceReclaimed = int(rep.SpaceReclaimed)
case "network": case "network":
_, err := client.NetworksPrune(context.Background(), pruneFilters) return cleanUnusedNetworks(t, client)
if err != nil {
return err
}
case "volume": case "volume":
versions, err := client.ServerVersion(context.Background()) versions, err := client.ServerVersion(context.Background())
if err != nil { if err != nil {
@@ -803,33 +810,33 @@ func (u *ContainerService) ContainerLogClean(req dto.OperationWithName) error {
func (u *ContainerService) StreamLogs(ctx *gin.Context, params dto.StreamLog) { func (u *ContainerService) StreamLogs(ctx *gin.Context, params dto.StreamLog) {
messageChan := make(chan string, 1024) messageChan := make(chan string, 1024)
errorChan := make(chan error, 1) errorChan := make(chan error, 1)
doneChan := make(chan struct{}) logCtx, cancel := context.WithCancel(ctx.Request.Context())
defer cancel()
go func() { doneChan := logCtx.Done()
<-ctx.Request.Context().Done()
close(doneChan)
}()
go collectLogs(doneChan, params, messageChan, errorChan) go collectLogs(doneChan, params, messageChan, errorChan)
ctx.Stream(func(w io.Writer) bool { ctx.Stream(func(w io.Writer) bool {
select { return writeContainerLogMessage(w, doneChan, messageChan, errorChan)
case msg, ok := <-messageChan:
if !ok {
return false
}
_, err := fmt.Fprintf(w, "data: %s\n\n", msg)
if err != nil {
return false
}
return true
case err := <-errorChan:
if err != nil {
_, _ = fmt.Fprintf(w, "event: error\ndata: %v\n\n", err.Error())
}
return false
}
}) })
} }
func writeContainerLogMessage(w io.Writer, done <-chan struct{}, messages <-chan string, errors <-chan error) bool {
select {
case <-done:
return false
case message, ok := <-messages:
if ok {
_, err := fmt.Fprintf(w, "data: %s\n\n", message)
return err == nil
}
// The collector closes both channels when it finishes. Drain all buffered
// logs before handling its result, or a ready error channel can drop the tail.
if err := <-errors; err != nil {
_, _ = fmt.Fprintf(w, "event: error\ndata: %v\n\n", err)
}
return false
}
}
func collectLogs(done <-chan struct{}, params dto.StreamLog, messageChan chan<- string, errorChan chan<- error) { func collectLogs(done <-chan struct{}, params dto.StreamLog, messageChan chan<- string, errorChan chan<- error) {
defer close(messageChan) defer close(messageChan)
defer close(errorChan) defer close(errorChan)
+71 -49
View File
@@ -71,10 +71,9 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
ContainerCount: 0, ContainerCount: 0,
CreatedAt: localItem.CreatedAt.Format(constant.DateTimeLayout), CreatedAt: localItem.CreatedAt.Format(constant.DateTimeLayout),
ConfigFile: localItem.Path, ConfigFile: localItem.Path,
Workdir: strings.TrimSuffix(localItem.Path, "/docker-compose.yml"), Workdir: filepath.Dir(primaryComposePath(localItem.Path)),
} }
composeItemLocal.CreatedBy = "1Panel" composeItemLocal.CreatedBy = "1Panel"
composeItemLocal.Path = localItem.Path
composeLocalMap[localItem.Name] = composeItemLocal composeLocalMap[localItem.Name] = composeItemLocal
} }
@@ -112,11 +111,6 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
if ok { if ok {
composeItem.CreatedBy = createdBy composeItem.CreatedBy = createdBy
} }
if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) {
composeItem.Path = config
} else {
composeItem.Path = workdir
}
for i := 0; i < len(composeCreatedByLocal); i++ { for i := 0; i < len(composeCreatedByLocal); i++ {
if composeCreatedByLocal[i].Name == name { if composeCreatedByLocal[i].Name == name {
composeItem.CreatedBy = "1Panel" composeItem.CreatedBy = "1Panel"
@@ -147,7 +141,9 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
for key, value := range mergedMap { for key, value := range mergedMap {
value.Name = key value.Name = key
value.ComposeFileExists = composeFileExists(value.Workdir, value.ConfigFile) composeFiles, err := resolveComposeFiles(value.Workdir, value.ConfigFile)
value.Path = strings.Join(composeFiles, ",")
value.ComposeFileExists = err == nil && composeFileExists(composeFiles)
value.IsPinned = pinnedByName[key] value.IsPinned = pinnedByName[key]
records = append(records, value) records = append(records, value)
} }
@@ -192,29 +188,6 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
return int64(total), listItem, nil return int64(total), listItem, nil
} }
func composeFileExists(workdir, configFile string) bool {
workdir = strings.TrimSpace(workdir)
configFile = strings.TrimSpace(configFile)
if configFile == "" {
return false
}
for _, item := range strings.Split(configFile, ",") {
file := strings.TrimSpace(item)
if file == "" {
continue
}
if !filepath.IsAbs(file) && workdir != "" {
file = filepath.Join(workdir, file)
}
file = filepath.Clean(file)
info, err := os.Stat(file)
if err == nil && !info.IsDir() {
return true
}
}
return false
}
func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) { func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
if err := validateComposeCreateName(req); err != nil { if err := validateComposeCreateName(req); err != nil {
return false, err return false, err
@@ -522,10 +495,6 @@ func isComposeProjectNameEmptyError(err error) bool {
} }
func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error { func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
if len(req.Path) == 0 && req.Operation == "delete" {
_ = composeRepo.DeleteRecord(repo.WithByName(req.Name))
return nil
}
if cmd.CheckIllegal(req.Path, req.Operation) { if cmd.CheckIllegal(req.Path, req.Operation) {
return buserr.New("ErrCmdIllegal") return buserr.New("ErrCmdIllegal")
} }
@@ -535,13 +504,19 @@ func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
} }
if req.WithFile { if req.WithFile {
for _, item := range strings.Split(req.Path, ",") { for _, item := range strings.Split(req.Path, ",") {
if len(item) != 0 { item = strings.TrimSpace(item)
_ = os.RemoveAll(path.Dir(item)) if !filepath.IsAbs(item) || filepath.Dir(item) == "/" {
continue
}
info, err := os.Stat(item)
if err == nil && info.Mode().IsRegular() {
if err := os.RemoveAll(filepath.Dir(item)); err != nil && !req.Force {
return err
} }
} }
} }
_ = composeRepo.DeleteRecord(repo.WithByName(req.Name)) }
return nil return composeRepo.DeleteRecord(repo.WithByName(req.Name))
} }
if req.Operation == "up" { if req.Operation == "up" {
if stdout, err := compose.Up(req.Path, req.Name); err != nil { if stdout, err := compose.Up(req.Path, req.Name); err != nil {
@@ -584,13 +559,13 @@ func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error {
write.Flush() write.Flush()
global.LOG.Infof("docker-compose.yml %s has been replaced, now start to docker-compose restart", req.DetailPath) global.LOG.Infof("docker-compose.yml %s has been replaced, now start to docker-compose restart", req.DetailPath)
if err := newComposeEnv(req.DetailPath, req.Env); err != nil { if err := newComposeEnv(primaryComposePath(req.Path), req.Env); err != nil {
return err return err
} }
if err := compose.UpWithTask(req.Path, t, req.ForcePull, req.Name); err != nil { if err := compose.UpWithTask(req.Path, t, req.ForcePull, req.Name); err != nil {
global.LOG.Errorf("update failed when handle compose up, err: %s, now try to recreate the old compose file", err) global.LOG.Errorf("update failed when handle compose up, err: %s, now try to recreate the old compose file", err)
if err := recreateCompose(string(oldFile), req.Path, req.Name); err != nil { if err := recreateCompose(string(oldFile), req.Path, req.DetailPath, req.Name); err != nil {
return fmt.Errorf("update failed and recreate old compose file also failed, err: %v", err) return fmt.Errorf("update failed and recreate old compose file also failed, err: %v", err)
} }
return fmt.Errorf("update failed when handle compose up, err: %s", err) return fmt.Errorf("update failed when handle compose up, err: %s", err)
@@ -702,9 +677,18 @@ func (u *ContainerService) loadPath(req *dto.ComposeCreate) error {
} }
func removeContainerForCompose(composeName, composePath string) error { func removeContainerForCompose(composeName, composePath string) error {
if _, err := os.Stat(composePath); err == nil { composeFiles, err := resolveComposeFiles("", composePath)
if stdout, err := compose.Operate(composePath, "down", composeName); err != nil { filesAvailable := err == nil
return errors.New(stdout) for _, file := range composeFiles {
info, err := os.Stat(file)
if err != nil || !info.Mode().IsRegular() {
filesAvailable = false
break
}
}
if filesAvailable {
if stdout, err := compose.Operate(strings.Join(composeFiles, ","), "down", composeName); err != nil {
return fmt.Errorf("docker-compose down failed, std: %s, err: %w", stdout, err)
} }
return nil return nil
} }
@@ -722,14 +706,17 @@ func removeContainerForCompose(composeName, composePath string) error {
if err != nil { if err != nil {
return err return err
} }
var removeErrors []error
for _, c := range containers { for _, c := range containers {
_ = client.ContainerRemove(ctx, c.ID, container.RemoveOptions{RemoveVolumes: true, Force: true}) if err := client.ContainerRemove(ctx, c.ID, container.RemoveOptions{RemoveVolumes: true, Force: true}); err != nil {
removeErrors = append(removeErrors, fmt.Errorf("remove container %s failed: %w", c.ID, err))
} }
return nil }
return errors.Join(removeErrors...)
} }
func recreateCompose(content, path, projectName string) error { func recreateCompose(content, composePath, detailPath, projectName string) error {
file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0640) file, err := os.OpenFile(detailPath, os.O_WRONLY|os.O_TRUNC, 0640)
if err != nil { if err != nil {
return err return err
} }
@@ -738,7 +725,7 @@ func recreateCompose(content, path, projectName string) error {
_, _ = write.WriteString(content) _, _ = write.WriteString(content)
write.Flush() write.Flush()
if stdout, err := compose.Up(path, projectName); err != nil { if stdout, err := compose.Up(composePath, projectName); err != nil {
return errors.New(string(stdout)) return errors.New(string(stdout))
} }
return nil return nil
@@ -746,6 +733,9 @@ func recreateCompose(content, path, projectName string) error {
func loadEnv(list []dto.ComposeInfo) []dto.ComposeInfo { func loadEnv(list []dto.ComposeInfo) []dto.ComposeInfo {
for i := 0; i < len(list); i++ { for i := 0; i < len(list); i++ {
if list[i].Path == "" {
continue
}
tmpPath := list[i].Path tmpPath := list[i].Path
if strings.Contains(list[i].Path, ",") { if strings.Contains(list[i].Path, ",") {
tmpPath = strings.Split(list[i].Path, ",")[0] tmpPath = strings.Split(list[i].Path, ",")[0]
@@ -774,3 +764,35 @@ func newComposeEnv(pathItem string, env string) error {
} }
return nil return nil
} }
func resolveComposeFiles(workdir, configFile string) ([]string, error) {
workdir = strings.TrimSpace(workdir)
var files []string
for _, item := range strings.Split(configFile, ",") {
file := strings.TrimSpace(item)
if file == "" {
continue
}
if !filepath.IsAbs(file) {
if !filepath.IsAbs(workdir) {
return nil, fmt.Errorf("cannot resolve compose file %s without an absolute working directory", file)
}
file = filepath.Join(workdir, file)
}
files = append(files, filepath.Clean(file))
}
if len(files) == 0 {
return nil, fmt.Errorf("compose config file is not specified")
}
return files, nil
}
func composeFileExists(files []string) bool {
for _, file := range files {
info, err := os.Stat(file)
if err == nil && info.Mode().IsRegular() {
return true
}
}
return false
}
+119
View File
@@ -2,16 +2,24 @@ package service
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"sort" "sort"
"strings" "strings"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/docker" "github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"github.com/docker/docker/errdefs"
) )
var networkCleanupSlot = make(chan struct{}, 1)
func (u *ContainerService) PageNetwork(req dto.SearchWithPage) (int64, interface{}, error) { func (u *ContainerService) PageNetwork(req dto.SearchWithPage) (int64, interface{}, error) {
client, err := docker.NewDockerClient() client, err := docker.NewDockerClient()
if err != nil { if err != nil {
@@ -172,3 +180,114 @@ func (u *ContainerService) CreateNetwork(req dto.NetworkCreate) error {
} }
return nil return nil
} }
func cleanUnusedNetworks(t *task.Task, cli *client.Client) error {
ctx := t.TaskCtx
select {
case networkCleanupSlot <- struct{}{}:
defer func() { <-networkCleanupSlot }()
case <-ctx.Done():
return ctx.Err()
}
if err := ctx.Err(); err != nil {
return err
}
networks, err := cli.NetworkList(ctx, network.ListOptions{})
if err != nil {
if ctx.Err() != nil {
return ctx.Err()
}
return err
}
deleted, skipped, failed := 0, 0, 0
defer func() {
t.Log(i18n.GetMsgWithMap("NetworkCleanupSummary", map[string]interface{}{
"deleted": deleted, "skipped": skipped, "failed": failed,
}))
}()
var used map[string]bool
sort.Slice(networks, func(i, j int) bool { return networks[i].Name < networks[j].Name })
for _, n := range networks {
if err := ctx.Err(); err != nil {
return err
}
switch n.Name {
case "none", "host", "bridge", "1panel-network":
skipped++
continue
}
if n.Scope != "local" || n.Ingress || n.ConfigOnly {
skipped++
continue
}
values := map[string]interface{}{"name": n.Name, "id": n.ID}
if used == nil {
containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
if err != nil {
if ctx.Err() != nil {
return ctx.Err()
}
return err
}
used = make(map[string]bool)
for _, c := range containers {
if c.NetworkSettings == nil {
continue
}
for name, endpoint := range c.NetworkSettings.Networks {
used[name] = true
if endpoint != nil {
used[endpoint.NetworkID] = true
}
}
}
}
if used[n.Name] || used[n.ID] {
skipped++
t.Log(i18n.GetMsgWithMap("NetworkCleanupConnected", values))
continue
}
inspected, err := cli.NetworkInspect(ctx, n.ID, network.InspectOptions{})
if err != nil {
if ctx.Err() != nil {
return ctx.Err()
}
if errdefs.IsNotFound(err) {
skipped++
} else {
failed++
t.Logf("Failed to inspect network [%s] (%s): %v", n.Name, n.ID, err)
}
continue
}
if len(inspected.Containers) > 0 {
skipped++
t.Log(i18n.GetMsgWithMap("NetworkCleanupConnected", values))
continue
}
if err := cli.NetworkRemove(ctx, n.ID); err != nil {
if ctx.Err() != nil {
return ctx.Err()
}
switch {
case errdefs.IsNotFound(err):
skipped++
case errdefs.IsConflict(err):
skipped++
t.Log(i18n.GetMsgWithMap("NetworkCleanupConnected", values))
default:
failed++
t.Logf("Failed to remove network [%s] (%s): %v", n.Name, n.ID, err)
}
continue
}
deleted++
}
if err := ctx.Err(); err != nil {
return err
}
if failed > 0 {
return errors.New(i18n.GetMsgByKey("NetworkCleanupPartialFailure"))
}
return nil
}
+63 -19
View File
@@ -8,6 +8,12 @@ import (
"os" "os"
"os/exec" "os/exec"
"strings" "strings"
"sync"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/google/uuid"
"gorm.io/gorm"
"github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
@@ -17,12 +23,15 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/compose" "github.com/1Panel-dev/1Panel/agent/utils/compose"
"github.com/1Panel-dev/1Panel/agent/utils/docker" "github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/encrypt" "github.com/1Panel-dev/1Panel/agent/utils/encrypt"
"github.com/docker/docker/api/types/container" dockerclient "github.com/docker/docker/client"
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
) )
type RedisService struct{} type RedisService struct{}
// The CLI container is shared by all remote Redis databases on this node.
var redisCliInstallMutex sync.Mutex
type IRedisService interface { type IRedisService interface {
UpdateConf(req dto.RedisConfUpdate) error UpdateConf(req dto.RedisConfUpdate) error
UpdatePersistenceConf(req dto.RedisConfPersistenceUpdate) error UpdatePersistenceConf(req dto.RedisConfPersistenceUpdate) error
@@ -33,7 +42,7 @@ type IRedisService interface {
LoadPersistenceConf(req dto.LoadRedisStatus) (*dto.RedisPersistence, error) LoadPersistenceConf(req dto.LoadRedisStatus) (*dto.RedisPersistence, error)
CheckHasCli() bool CheckHasCli() bool
InstallCli() error InstallCli(req dto.RedisCliInstall) (string, error)
} }
func NewIRedisService() IRedisService { func NewIRedisService() IRedisService {
@@ -61,30 +70,65 @@ func (u *RedisService) UpdateConf(req dto.RedisConfUpdate) error {
} }
func (u *RedisService) CheckHasCli() bool { func (u *RedisService) CheckHasCli() bool {
client, err := docker.NewDockerClient() installed, _ := u.checkCliInstalled()
if err != nil { return installed
return false
}
defer client.Close()
containerLists, err := client.ContainerList(context.Background(), container.ListOptions{})
if err != nil {
return false
}
for _, item := range containerLists {
if strings.ReplaceAll(item.Names[0], "/", "") == "1Panel-redis-cli-tools" {
return true
}
}
return false
} }
func (u *RedisService) InstallCli() error { func (u *RedisService) checkCliInstalled() (bool, error) {
client, err := docker.NewDockerClient()
if err != nil {
return false, err
}
defer client.Close()
info, err := client.ContainerInspect(context.Background(), "1Panel-redis-cli-tools")
if dockerclient.IsErrNotFound(err) {
return false, nil
}
if err != nil {
return false, err
}
return info.ContainerJSONBase != nil && info.State != nil && info.State.Running, nil
}
func (u *RedisService) InstallCli(req dto.RedisCliInstall) (string, error) {
if !redisCliInstallMutex.TryLock() {
return "", buserr.New("TaskIsExecuting")
}
defer redisCliInstallMutex.Unlock()
item := dto.ContainerOperate{ item := dto.ContainerOperate{
TaskID: req.TaskID,
Name: "1Panel-redis-cli-tools", Name: "1Panel-redis-cli-tools",
Image: "redis:7.4.4", Image: "redis:7.4.4",
Networks: []dto.ContainerNetwork{{Network: "1panel-network"}}, Networks: []dto.ContainerNetwork{{Network: "1panel-network"}},
} }
return NewIContainerService().ContainerCreate(item, false) running, err := taskRepo.GetFirst(
repo.WithByName(task.GetTaskName(item.Name, task.TaskCreate, task.TaskScopeContainer)),
repo.WithByType(task.TaskScopeContainer),
taskRepo.WithByStatus(constant.StatusExecuting),
)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return "", err
}
if running.ID != "" {
return running.ID, nil
}
installed, err := u.checkCliInstalled()
if err != nil || installed {
return "", err
}
if item.TaskID == "" {
item.TaskID = uuid.NewString()
}
if _, err := taskRepo.GetFirst(taskRepo.WithByID(item.TaskID)); !errors.Is(err, gorm.ErrRecordNotFound) {
if err != nil {
return "", err
}
return "", buserr.New("TaskIsExecuting")
}
if err := NewIContainerService().ContainerCreate(item, true); err != nil {
return "", err
}
return item.TaskID, nil
} }
func (u *RedisService) ChangePassword(req dto.ChangeRedisPass) error { func (u *RedisService) ChangePassword(req dto.ChangeRedisPass) error {
+1 -3
View File
@@ -108,9 +108,7 @@ func (s *FirewallService) LoadBaseInfo(chainGroup string) (dto.FirewallSubsystem
status.Reason = constant.FirewallBackendNotInstalled status.Reason = constant.FirewallBackendNotInstalled
return status, nil return status, nil
} }
status.IsExist = true return status, err
status.Message = err.Error()
return status, nil
} }
status.IsExist = true status.IsExist = true
runtimeStatus, err := lifecycle.LoadStatus(client) runtimeStatus, err := lifecycle.LoadStatus(client)
+75 -6
View File
@@ -80,6 +80,50 @@ func LoadPanelPort() string {
return portSetting.Value return portSetting.Value
} }
func SyncPanelPortWhitelist() error {
firewallWhitelistMu.Lock()
defer firewallWhitelistMu.Unlock()
portValue := global.CONF.Base.Port
if global.IsMaster {
var err error
portValue, err = settingRepo.GetCoreValueByKey("ServerPort")
if err != nil {
return err
}
}
port, err := strconv.ParseUint(portValue, 10, 16)
if err != nil || port == 0 {
return fmt.Errorf("invalid panel port %q", portValue)
}
portValue = strconv.FormatUint(port, 10)
value, err := settingRepo.GetValueByKey(constant.FirewallPortWhiteList)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
if err != nil {
return err
}
var entries []firewall.PortWhitelist
if err := json.Unmarshal([]byte(value), &entries); err != nil {
return err
}
changed := false
for index := range entries {
if entries[index].Type == firewall.PortWhitelistTypePanel && entries[index].Port != portValue {
entries[index].Port = portValue
changed = true
}
}
if !changed {
return nil
}
updated, err := json.Marshal(entries)
if err != nil {
return err
}
return settingRepo.Update(constant.FirewallPortWhiteList, string(updated))
}
func (s *FirewallService) updateSystemAccessPortWhitelist(ctx context.Context, serviceType string, ports []string) error { func (s *FirewallService) updateSystemAccessPortWhitelist(ctx context.Context, serviceType string, ports []string) error {
if len(ports) == 0 { if len(ports) == 0 {
return fmt.Errorf("firewall whitelist %s requires a port", serviceType) return fmt.Errorf("firewall whitelist %s requires a port", serviceType)
@@ -582,6 +626,18 @@ func (s *FirewallService) createRules(ctx context.Context, request dto.FirewallR
if err != nil { if err != nil {
return result, err return result, err
} }
importPrioritySupported := true
if adapter, ok := runtime.(*filterfirewalld.Adapter); ok && slices.ContainsFunc(request.Items, func(item dto.FirewallRuleCreateItem) bool {
return item.SourceKind == constant.FirewallRuleSourceImported
}) {
importPrioritySupported, err = adapter.SupportsRichRulePriority(ctx)
if err != nil {
return result, err
}
if !importPrioritySupported && t != nil {
t.Log("firewalld does not support rich rule priority; ignoring priority for imported rules, original rule ordering is not preserved")
}
}
preparedItems := make([]*createItem, 0, len(request.Items)) preparedItems := make([]*createItem, 0, len(request.Items))
var stop error var stop error
for index, item := range request.Items { for index, item := range request.Items {
@@ -593,6 +649,16 @@ func (s *FirewallService) createRules(ctx context.Context, request dto.FirewallR
continue continue
} }
item.Rule.OrderIndex = nil item.Rule.OrderIndex = nil
if item.SourceKind == constant.FirewallRuleSourceImported && !importPrioritySupported {
item.Rule.Priority = nil
if item.Rule.Scope.Provider == filter.ProviderFirewalld && item.Raw != "" && item.ParseStatus != "" && item.ParseStatus != filter.ParseStatusSupported && item.Rule.NativeKind != filter.NativeKindZoneService {
item.Raw, err = filterfirewalld.RemoveRichRulePriority(item.Raw)
if err != nil {
record(createItem{index: index, request: item}, "skipped", err)
continue
}
}
}
rules, err := expandFirewallCreateRule(item, selected) rules, err := expandFirewallCreateRule(item, selected)
if err != nil { if err != nil {
status := "failed" status := "failed"
@@ -693,7 +759,7 @@ func (s *FirewallService) createRules(ctx context.Context, request dto.FirewallR
record(*item, "skipped", buserr.New("ErrRecordExist")) record(*item, "skipped", buserr.New("ErrRecordExist"))
continue continue
} }
if converted { if converted && importPrioritySupported {
priority++ priority++
} }
if existing[key] { if existing[key] {
@@ -713,7 +779,7 @@ func (s *FirewallService) createRules(ctx context.Context, request dto.FirewallR
} }
ufwActions[matchKey] = map[filter.Action]bool{item.request.Rule.Action: true} ufwActions[matchKey] = map[filter.Action]bool{item.request.Rule.Action: true}
} }
if converted { if converted && importPrioritySupported {
if priority > 32767 { if priority > 32767 {
return result, fmt.Errorf("%w: firewalld import order exceeds the priority range", filter.ErrExpansionLimit) return result, fmt.Errorf("%w: firewalld import order exceeds the priority range", filter.ErrExpansionLimit)
} }
@@ -729,18 +795,19 @@ func (s *FirewallService) createRules(ctx context.Context, request dto.FirewallR
continue continue
} }
item.request = prepared item.request = prepared
actualKey, err := firewallRuleDuplicateKey(filter.ObservedRule{Rule: prepared.Rule, ParseStatus: prepared.ParseStatus}, false) }
pending[key] = true
if converted {
actualKey, err := firewallRuleDuplicateKey(filter.ObservedRule{Rule: item.request.Rule, ParseStatus: item.request.ParseStatus}, false)
if err != nil { if err != nil {
return result, err return result, err
} }
pending[key] = true
if existing[actualKey] || pending[actualKey] { if existing[actualKey] || pending[actualKey] {
record(*item, "skipped", buserr.New("ErrRecordExist")) record(*item, "skipped", buserr.New("ErrRecordExist"))
continue continue
} }
pending[actualKey] = true pending[actualKey] = true
} }
pending[key] = true
mergedItems = append(mergedItems, item) mergedItems = append(mergedItems, item)
} }
for _, item := range mergedItems { for _, item := range mergedItems {
@@ -1185,15 +1252,17 @@ func loadForwardingFirewallOverview(manager forwarding.Adapter, families []strin
} }
ipv6, ipv6Err := loadForwardingFamilyInfo(manager, constant.FirewallFamilyIPv6) ipv6, ipv6Err := loadForwardingFamilyInfo(manager, constant.FirewallFamilyIPv6)
if ipv6.Available { if ipv6.Available {
interfaces, err := forwarding.IPv6RAInterfaces(os.ReadFile) interfaces, err := forwarding.IPv6RAInterfaces(os.ReadFile, forwarding.LoadDockerIPv4BridgePorts)
var pathError *os.PathError var pathError *os.PathError
switch { switch {
case errors.As(err, &pathError) && errors.Is(err, os.ErrNotExist) && pathError.Path == "/proc/net/if_inet6": case errors.As(err, &pathError) && errors.Is(err, os.ErrNotExist) && pathError.Path == "/proc/net/if_inet6":
ipv6.Available, ipv6.Initialized, ipv6.Bound = false, false, false ipv6.Available, ipv6.Initialized, ipv6.Bound = false, false, false
case err != nil: case err != nil:
global.LOG.Warnf("IPv6 forwarding RA check failed: %v", err)
ipv6.Reason = "ipv6_ra_check_failed" ipv6.Reason = "ipv6_ra_check_failed"
ipv6.Bound = false ipv6.Bound = false
case len(interfaces) > 0: case len(interfaces) > 0:
global.LOG.Warnf("IPv6 forwarding RA risk: interfaces %s may depend on RA/SLAAC with accept_ra=1", strings.Join(interfaces, ", "))
ipv6.Reason, ipv6.RAInterfaces = "ipv6_ra_required", interfaces ipv6.Reason, ipv6.RAInterfaces = "ipv6_ra_required", interfaces
ipv6.Bound = false ipv6.Bound = false
case !ipv6.Bound: case !ipv6.Bound:
-45
View File
@@ -9,13 +9,10 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "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/task" "github.com/1Panel-dev/1Panel/agent/app/task"
"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/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/firewall" "github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding" "github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
@@ -82,23 +79,6 @@ func (s *ForwardingService) LoadBaseInfo(ctx context.Context) (dto.FirewallSubsy
baseInfo.PingStatus = firewall.LoadPingStatus() baseInfo.PingStatus = firewall.LoadPingStatus()
baseInfo.IsInit, baseInfo.IsBind = status.IsInit, status.IsBind baseInfo.IsInit, baseInfo.IsBind = status.IsInit, status.IsBind
baseInfo.IPv4, baseInfo.IPv6 = status.IPv4, status.IPv6 baseInfo.IPv4, baseInfo.IPv6 = status.IPv4, status.IPv6
for _, family := range []struct {
command string
status *dto.FirewallBackendFamilyStatus
}{
{"iptables", &baseInfo.IPv4},
{"ip6tables", &baseInfo.IPv6},
} {
if family.command == "ip6tables" && !baseInfo.IPv6Enabled {
continue
}
policy, err := loadForwardPolicy(ctx, family.command)
if err != nil {
global.LOG.Warnf("inspect %s FORWARD policy: %v", family.command, err)
continue
}
family.status.ForwardPolicy = policy
}
return baseInfo, nil return baseInfo, nil
} }
@@ -282,8 +262,6 @@ func (s *ForwardingService) Restore(ctx context.Context) error {
if err := manager.OperateFamily(family, missing[family]); err != nil { if err := manager.OperateFamily(family, missing[family]); err != nil {
return err return err
} }
}
for _, family := range families {
if !missing[family] { if !missing[family] {
continue continue
} }
@@ -308,26 +286,3 @@ func (s *ForwardingService) Restore(ctx context.Context) error {
func NewIForwardingService() IForwardingService { func NewIForwardingService() IForwardingService {
return newForwardingService() return newForwardingService()
} }
func loadForwardPolicy(ctx context.Context, command string) (string, error) {
if !cmd.Which(command) {
command += "-nft"
if !cmd.Which(command) {
return "", nil
}
}
output, err := cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(5*time.Second)).RunWithOptionalSudoAndStdout(command, "-t", "filter", "-w", "2", "-S", "FORWARD")
if err != nil {
return "", err
}
for _, line := range strings.Split(output, "\n") {
fields := strings.Fields(line)
if len(fields) == 3 && fields[0] == "-P" && fields[1] == "FORWARD" {
if fields[2] != "ACCEPT" && fields[2] != "DROP" {
return "", fmt.Errorf("unexpected FORWARD policy: %s", fields[2])
}
return fields[2], nil
}
}
return "", errors.New("FORWARD default policy was not found")
}
+126
View File
@@ -0,0 +1,126 @@
package service
import (
"regexp"
"sort"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/common"
)
// Release names at https://github.com/NousResearch/hermes-agent/releases pair
// these historical calendar tags with their semantic versions. This includes
// every calendar version published in the 1Panel app store. Do not rewrite
// stored versions: backup metadata, restore and rollback retain the original tag.
var hermesHistoricalVersions = map[string]string{
"2026.3.12": "0.2.0",
"2026.3.17": "0.3.0",
"2026.3.23": "0.4.0",
"2026.3.28": "0.5.0",
"2026.3.30": "0.6.0",
"2026.4.3": "0.7.0",
"2026.4.8": "0.8.0",
"2026.4.13": "0.9.0",
"2026.4.16": "0.10.0",
"2026.4.23": "0.11.0",
"2026.4.30": "0.12.0",
"2026.5.7": "0.13.0",
"2026.5.16": "0.14.0",
"2026.5.28": "0.15.0",
"2026.5.29": "0.15.1",
"2026.5.29.2": "0.15.2",
"2026.6.5": "0.16.0",
"2026.6.19": "0.17.0",
"2026.7.1": "0.18.0",
"2026.7.7": "0.18.1",
"2026.7.7.2": "0.18.2",
"2026.7.20": "0.19.0",
"2026.7.30": "0.19.1",
"2026.8.3": "0.20.0",
"2026.8.13": "0.20.1",
"2026.8.16": "0.20.2",
"2026.8.16.2": "0.20.3",
"2026.8.18": "0.20.4",
"2026.8.19": "0.20.5",
"2026.8.27": "0.20.6",
"2026.8.31": "0.21.0",
"2026.9.7": "0.21.1",
"2026.9.11": "0.21.2",
"2026.9.14": "0.21.3",
"2026.9.21": "0.21.4",
"2026.9.24": "0.21.5",
}
var hermesReleaseVersion = regexp.MustCompile(`^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$`)
func isHermesApp(key string) bool {
return key == constant.AppHermesAgent || key == "local"+constant.AppHermesAgent
}
func hermesComparableVersion(version string) (string, bool) {
version = strings.TrimPrefix(version, "v")
if mapped, ok := hermesHistoricalVersions[version]; ok {
return mapped, true
}
parts := hermesReleaseVersion.FindStringSubmatch(version)
if len(parts) == 0 {
return "", false
}
for i, part := range parts[1:] {
n, err := strconv.Atoi(part)
if err != nil || (i == 0 && n >= 2000) {
// An unknown calendar tag must not become a huge semantic major.
return "", false
}
}
return version, true
}
func isNewerAppVersion(key, target, current string) bool {
if !isHermesApp(key) {
return common.CompareVersion(target, current)
}
targetVersion, targetOK := hermesComparableVersion(target)
currentVersion, currentOK := hermesComparableVersion(current)
return targetOK && currentOK && common.CompareVersion(targetVersion, currentVersion)
}
func isCrossAppVersion(key, current, target string) bool {
if !isHermesApp(key) {
return common.IsCrossVersion(current, target)
}
currentVersion, currentOK := hermesComparableVersion(current)
targetVersion, targetOK := hermesComparableVersion(target)
return currentOK && targetOK && common.IsCrossVersion(currentVersion, targetVersion)
}
func appVersionSortBefore(key, left, right string) bool {
if !isHermesApp(key) {
return common.CompareVersion(left, right)
}
lv, lok := hermesComparableVersion(left)
rv, rok := hermesComparableVersion(right)
if lok != rok {
return lok
}
if !lok {
return common.CompareVersion(left, right)
}
if lv != rv {
return common.CompareVersion(lv, rv)
}
// Prefer the official semantic spelling when both aliases are available.
return strings.TrimPrefix(left, "v") == lv && strings.TrimPrefix(right, "v") != rv
}
func sortAppVersions(key string, versions []string) []string {
if !isHermesApp(key) {
return common.GetSortedVersions(versions)
}
sort.SliceStable(versions, func(i, j int) bool {
return appVersionSortBefore(key, versions[i], versions[j])
})
return versions
}
+30 -15
View File
@@ -28,6 +28,7 @@ import (
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/registry" "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/moby/go-archive" "github.com/moby/go-archive"
) )
@@ -325,18 +326,16 @@ func (u *ImageService) ImageLoad(req dto.ImageLoad) error {
} }
go func() { go func() {
client, err := docker.NewDockerClient()
if err != nil {
taskItem.Log("Failed to create Docker client: " + err.Error())
return
}
defer client.Close()
for _, itemPath := range req.Paths { for _, itemPath := range req.Paths {
currentPath := itemPath currentPath := itemPath
itemName := path.Base(currentPath) itemName := path.Base(currentPath)
taskItem.AddSubTask(i18n.GetWithName("TaskImport", itemName), func(t *task.Task) error { taskItem.AddSubTask(i18n.GetWithName("TaskImport", itemName), func(t *task.Task) error {
taskItem.Logf("----------------- %s -----------------", itemName) taskItem.Logf("----------------- %s -----------------", itemName)
client, err := docker.NewDockerClient()
if err != nil {
return err
}
defer client.Close()
file, err := os.Open(currentPath) file, err := os.Open(currentPath)
if err != nil { if err != nil {
return err return err
@@ -347,14 +346,7 @@ func (u *ImageService) ImageLoad(req dto.ImageLoad) error {
return err return err
} }
defer res.Body.Close() defer res.Body.Close()
content, err := io.ReadAll(res.Body) return consumeImageLoadResponse(res.Body, taskItem.Log)
if err != nil {
return err
}
if strings.Contains(string(content), "Error") {
return errors.New(string(content))
}
return nil
}, nil) }, nil)
} }
_ = taskItem.Execute() _ = taskItem.Execute()
@@ -362,6 +354,29 @@ func (u *ImageService) ImageLoad(req dto.ImageLoad) error {
return nil return nil
} }
// Docker may report load failures in a successful HTTP response's JSON stream.
func consumeImageLoadResponse(reader io.Reader, log func(string)) error {
decoder := json.NewDecoder(reader)
for {
var message jsonmessage.JSONMessage
if err := decoder.Decode(&message); err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return err
}
if message.Error != nil && message.Error.Message != "" {
return message.Error
}
if message.ErrorMessage != "" {
return errors.New(message.ErrorMessage)
}
if text := strings.TrimSpace(message.Stream); text != "" {
log(text)
}
}
}
func (u *ImageService) ImageSave(req dto.ImageSave) error { func (u *ImageService) ImageSave(req dto.ImageSave) error {
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskExport, task.TaskScopeImage, req.TaskID, 1) taskItem, err := task.NewTaskWithOps(req.Name, task.TaskExport, task.TaskScopeImage, req.TaskID, 1)
if err != nil { if err != nil {
+1 -1
View File
@@ -54,7 +54,7 @@ type IMonitorService interface {
LoadIOOptions() []string LoadIOOptions() []string
LoadNetworkOptions() []string LoadNetworkOptions() []string
LoadVLLMMonitorData(req dto.MonitorVLLMSearch) (dto.MonitorVLLMData, error) LoadVLLMMonitorData(ctx context.Context, req dto.MonitorVLLMSearch) (dto.MonitorVLLMData, error)
LoadVLLMCurrent(ctx context.Context, req dto.MonitorVLLMCurrent) (model.MonitorVLLM, error) LoadVLLMCurrent(ctx context.Context, req dto.MonitorVLLMCurrent) (model.MonitorVLLM, error)
CleanVLLMMonitor(req dto.MonitorVLLMClean) error CleanVLLMMonitor(req dto.MonitorVLLMClean) error
LoadGPUOptions() dto.MonitorGPUOptions LoadGPUOptions() dto.MonitorGPUOptions
+23 -7
View File
@@ -31,12 +31,12 @@ var vllmMetricsClient = &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse },
} }
func (m *MonitorService) LoadVLLMMonitorData(req dto.MonitorVLLMSearch) (dto.MonitorVLLMData, error) { func (m *MonitorService) LoadVLLMMonitorData(ctx context.Context, req dto.MonitorVLLMSearch) (dto.MonitorVLLMData, error) {
data := dto.MonitorVLLMData{Points: []model.MonitorVLLM{}} data := dto.MonitorVLLMData{Points: []model.MonitorVLLM{}}
if !req.EndTime.After(req.StartTime) { if !req.EndTime.After(req.StartTime) {
return data, fmt.Errorf("invalid vLLM history request") return data, fmt.Errorf("invalid vLLM history request")
} }
install, err := appInstallRepo.GetFirst(repo.WithByID(req.AppInstallID)) install, err := appInstallRepo.GetFirstByCtx(ctx, repo.WithByID(req.AppInstallID))
if err != nil { if err != nil {
return data, err return data, err
} }
@@ -49,23 +49,31 @@ func (m *MonitorService) LoadVLLMMonitorData(req dto.MonitorVLLMSearch) (dto.Mon
return data, err return data, err
} }
req.StartTime, req.EndTime = req.StartTime.In(loc), req.EndTime.In(loc) req.StartTime, req.EndTime = req.StartTime.In(loc), req.EndTime.In(loc)
data.SampleCount, err = vllmMonitorRepo.Count(req.AppInstallID, req.StartTime, req.EndTime) data.SampleCount, err = vllmMonitorRepo.Count(ctx, req.AppInstallID, req.StartTime, req.EndTime)
if err != nil || data.SampleCount == 0 { if err != nil || data.SampleCount == 0 {
return data, err return data, err
} }
if data.SampleCount > 1200 { if data.SampleCount > 1200 {
data.BucketSeconds = (req.EndTime.Unix() - req.StartTime.Unix() + 600) / 600 data.BucketSeconds = (req.EndTime.Unix() - req.StartTime.Unix() + 600) / 600
} }
points, err := vllmMonitorRepo.History(req.AppInstallID, req.StartTime, req.EndTime, data.BucketSeconds, req.Aggregation) points, err := vllmMonitorRepo.History(ctx, req.AppInstallID, req.StartTime, req.EndTime, data.BucketSeconds, req.Aggregation, req.Group)
if err != nil { if err != nil {
return data, err return data, err
} }
if data.BucketSeconds > 0 { if data.BucketSeconds > 0 {
if req.Aggregation != "max" { aggregates := map[int64]*vllm.HistoryAccumulator{}
for i := range points { err = vllmMonitorRepo.WalkDeltas(ctx, req.AppInstallID, req.StartTime, req.EndTime, data.BucketSeconds, req.Aggregation, req.Group, func(bucket int64, histograms, requests string) error {
if err := vllm.AggregateHistograms(points[i].HistogramSamples, &points[i].MonitorVLLM); err != nil { if aggregates[bucket] == nil {
aggregates[bucket] = &vllm.HistoryAccumulator{}
}
return aggregates[bucket].Add(histograms, requests, req.Aggregation, req.Group)
})
if err != nil {
return data, err return data, err
} }
for i := range points {
if aggregate := aggregates[points[i].Bucket]; aggregate != nil {
aggregate.Apply(&points[i].MonitorVLLM, req.Aggregation)
} }
} }
next := 0 next := 0
@@ -93,6 +101,14 @@ func (m *MonitorService) LoadVLLMMonitorData(req dto.MonitorVLLMSearch) (dto.Mon
} }
interval := time.Duration(intervalSeconds) * time.Second interval := time.Duration(intervalSeconds) * time.Second
for i, point := range points { for i, point := range points {
if err := ctx.Err(); err != nil {
return data, err
}
var aggregate vllm.HistoryAccumulator
if err := aggregate.Add(point.HistogramDeltas, point.RequestDeltas, "max", req.Group); err != nil {
return data, err
}
aggregate.Apply(&point.MonitorVLLM, "max")
if i > 0 && point.CreatedAt.Sub(points[i-1].CreatedAt) > 2*interval { if i > 0 && point.CreatedAt.Sub(points[i-1].CreatedAt) > 2*interval {
data.Points = append(data.Points, model.MonitorVLLM{CreatedAt: points[i-1].CreatedAt.Add(interval), AppInstallID: req.AppInstallID}) data.Points = append(data.Points, model.MonitorVLLM{CreatedAt: points[i-1].CreatedAt.Add(interval), AppInstallID: req.AppInstallID})
} }
+3
View File
@@ -494,6 +494,9 @@ func restartCompose(composePath string, itemHelper *snapRecoverHelper) error {
func recoverDBData(src string, itemHelper *snapRecoverHelper) error { func recoverDBData(src string, itemHelper *snapRecoverHelper) error {
itemHelper.Task.Log("---------------------- 11 / 11 ----------------------") itemHelper.Task.Log("---------------------- 11 / 11 ----------------------")
itemHelper.Task.LogStart(i18n.GetMsgByKey("RecoverDBData")) itemHelper.Task.LogStart(i18n.GetMsgByKey("RecoverDBData"))
if err := os.Chmod(src, 0700); err != nil {
return err
}
dbDir := path.Join(global.Dir.DataDir, "db") dbDir := path.Join(global.Dir.DataDir, "db")
if err := os.RemoveAll(dbDir); err != nil { if err := os.RemoveAll(dbDir); err != nil {
itemHelper.Task.LogWithStatus(i18n.GetMsgByKey("RecoverDBData"), err) itemHelper.Task.LogWithStatus(i18n.GetMsgByKey("RecoverDBData"), err)
+3
View File
@@ -106,6 +106,9 @@ func (u *SnapshotService) SnapshotRollback(req dto.SnapshotRecover) error {
taskItem.AddSubTask( taskItem.AddSubTask(
i18n.GetWithName("SnapCopy", global.Dir.BaseDir), i18n.GetWithName("SnapCopy", global.Dir.BaseDir),
func(t *task.Task) error { func(t *task.Task) error {
if err := os.Chmod(path.Join(rootDir, "1panel/db"), 0700); err != nil {
return err
}
return FileOp.CopyDir(path.Join(rootDir, "1panel"), global.Dir.BaseDir) return FileOp.CopyDir(path.Join(rootDir, "1panel"), global.Dir.BaseDir)
}, },
nil, nil,
+4 -1
View File
@@ -57,7 +57,10 @@ func (u *TaskLogService) ReadByLine(req request.TaskLogReadReq) (*response.FileL
if req.TaskID != "" { if req.TaskID != "" {
opts = append(opts, taskRepo.WithByID(req.TaskID)) opts = append(opts, taskRepo.WithByID(req.TaskID))
} else { } else {
opts = append(opts, repo.WithOrderRuleBy("created_at", "desc"), repo.WithByType(req.TaskType), taskRepo.WithOperate(req.TaskOperate), taskRepo.WithResourceID(req.ResourceID)) opts = append(opts, repo.WithOrderRuleBy("created_at", "desc"), repo.WithByType(req.TaskType), taskRepo.WithResourceID(req.ResourceID))
if req.TaskOperate != "" {
opts = append(opts, taskRepo.WithOperate(req.TaskOperate))
}
} }
taskModel, err := taskRepo.GetFirst(opts...) taskModel, err := taskRepo.GetFirst(opts...)
if err != nil { if err != nil {
+26 -8
View File
@@ -526,10 +526,8 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
website.SiteDir = create.SiteDir website.SiteDir = create.SiteDir
} }
createdFiles := &websiteCreateFiles{}
configNginx := func(t *task.Task) error { configNginx := func(t *task.Task) error {
if err = configDefaultNginx(website, domains, appInstall, runtime, create.StreamConfig, initialSSL); err != nil {
return err
}
if create.Type == constant.Static && create.TemplateOutputID > 0 { if create.Type == constant.Static && create.TemplateOutputID > 0 {
templateOutput, err := websiteTemplateOutputRepo.GetFirst(repo.WithByID(create.TemplateOutputID)) templateOutput, err := websiteTemplateOutputRepo.GetFirst(repo.WithByID(create.TemplateOutputID))
if err != nil { if err != nil {
@@ -541,12 +539,15 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
if _, err := os.Stat(templateOutput.OutputPath); err != nil { if _, err := os.Stat(templateOutput.OutputPath); err != nil {
return buserr.New("ErrFileNotFound") return buserr.New("ErrFileNotFound")
} }
if err := copyDir(templateOutput.OutputPath, GetSitePath(*website, SiteIndexDir)); err != nil { if err := createdFiles.copyTemplate(templateOutput.OutputPath, GetSitePath(*website, SiteIndexDir)); err != nil {
return err return err
} }
} }
if err = configDefaultNginx(website, domains, appInstall, runtime, create.StreamConfig, initialSSL, createdFiles); err != nil {
return err
}
if website.Type != constant.Stream { if website.Type != constant.Stream {
if err = createWafConfig(website, domains); err != nil { if err = createWafConfig(website, domains, createdFiles); err != nil {
return err return err
} }
if create.Type == constant.Runtime { if create.Type == constant.Runtime {
@@ -555,10 +556,20 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
return err return err
} }
if runtime.Type == constant.RuntimePHP && runtime.Resource == constant.ResourceAppstore { if runtime.Type == constant.RuntimePHP && runtime.Resource == constant.ResourceAppstore {
createOpenBasedirConfig(website) if err := createdFiles.write(path.Join(GetSitePath(*website, SiteIndexDir), ".user.ini"), []byte(fmt.Sprintf("open_basedir=/www/sites/%s/index:/tmp/", website.Alias)), 0644, true); err != nil {
return err
} }
} }
} }
}
// Apply the site only after all Nginx, WAF, and PHP configuration is ready.
if err := opNginx(nginxInstall.ContainerName, constant.NginxCheck); err != nil {
return err
}
if err := opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil {
return err
}
tx, ctx := helper.GetTxAndContext() tx, ctx := helper.GetTxAndContext()
defer tx.Rollback() defer tx.Rollback()
@@ -575,12 +586,19 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
} }
} }
tx.Commit() if err := tx.Commit().Error; err != nil {
return err
}
return nil return nil
} }
deleteWebsite := func(t *task.Task) { deleteWebsite := func(t *task.Task) {
_ = deleteWebsiteFolder(website) if rollbackErr := createdFiles.rollback(); rollbackErr != nil {
t.Log(fmt.Sprintf("website file rollback: %v", rollbackErr))
}
if reloadErr := opNginx(nginxInstall.ContainerName, constant.NginxReload); reloadErr != nil {
t.Log(fmt.Sprintf("reload OpenResty after rollback: %v", reloadErr))
}
} }
createTask.AddSubTask(i18n.GetMsgByKey("ConfigOpenresty"), configNginx, deleteWebsite) createTask.AddSubTask(i18n.GetMsgByKey("ConfigOpenresty"), configNginx, deleteWebsite)
+6 -31
View File
@@ -2,7 +2,6 @@ package service
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"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"
@@ -46,16 +45,7 @@ func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate)
fileOp := files.NewFileOp() fileOp := files.NewFileOp()
if fileOp.Stat(wafDataPath) { if fileOp.Stat(wafDataPath) {
websitesConfigPath := path.Join(wafDataPath, "conf", "sites.json") websitesConfigPath := path.Join(wafDataPath, "conf", "sites.json")
content, err := fileOp.GetContent(websitesConfigPath) if err := updateWAFWebsites(websitesConfigPath, func(websitesArray []request.WafWebsite) ([]request.WafWebsite, error) {
if err != nil {
return nil, err
}
var websitesArray []request.WafWebsite
if content != nil {
if err := json.Unmarshal(content, &websitesArray); err != nil {
return nil, err
}
}
for index, wafWebsite := range websitesArray { for index, wafWebsite := range websitesArray {
if wafWebsite.Key == website.Alias { if wafWebsite.Key == website.Alias {
wafSite := request.WafWebsite{ wafSite := request.WafWebsite{
@@ -74,11 +64,8 @@ func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate)
break break
} }
} }
websitesContent, err := json.Marshal(websitesArray) return websitesArray, nil
if err != nil { }); err != nil {
return nil, err
}
if err := fileOp.SaveFileWithByte(websitesConfigPath, websitesContent, constant.DirPerm); err != nil {
return nil, err return nil, err
} }
} }
@@ -135,17 +122,8 @@ func (w WebsiteService) DeleteWebsiteDomain(domainId uint) error {
fileOp := files.NewFileOp() fileOp := files.NewFileOp()
if fileOp.Stat(wafDataPath) { if fileOp.Stat(wafDataPath) {
websitesConfigPath := path.Join(wafDataPath, "conf", "sites.json") websitesConfigPath := path.Join(wafDataPath, "conf", "sites.json")
content, err := fileOp.GetContent(websitesConfigPath) if err := updateWAFWebsites(websitesConfigPath, func(websitesArray []request.WafWebsite) ([]request.WafWebsite, error) {
if err != nil {
return err
}
var websitesArray []request.WafWebsite
var newWebsitesArray []request.WafWebsite var newWebsitesArray []request.WafWebsite
if content != nil {
if err := json.Unmarshal(content, &websitesArray); err != nil {
return err
}
}
for _, wafWebsite := range websitesArray { for _, wafWebsite := range websitesArray {
if wafWebsite.Key == website.Alias { if wafWebsite.Key == website.Alias {
wafSite := wafWebsite wafSite := wafWebsite
@@ -175,11 +153,8 @@ func (w WebsiteService) DeleteWebsiteDomain(domainId uint) error {
newWebsitesArray = append(newWebsitesArray, wafWebsite) newWebsitesArray = append(newWebsitesArray, wafWebsite)
} }
} }
websitesContent, err := json.Marshal(newWebsitesArray) return newWebsitesArray, nil
if err != nil { }); err != nil {
return err
}
if err = fileOp.SaveFileWithByte(websitesConfigPath, websitesContent, constant.DirPerm); err != nil {
return err return err
} }
} }
-35
View File
@@ -403,38 +403,3 @@ func renderMainHTMLFromZip(zipPath string, values map[string]string) (string, er
} }
return renderContent(string(content), values), nil return renderContent(string(content), values), nil
} }
func copyDir(src, dst string) error {
return filepath.Walk(src, func(p string, info os.FileInfo, err error) error {
if err != nil {
return err
}
relPath, err := filepath.Rel(src, p)
if err != nil {
return err
}
targetPath := filepath.Join(dst, relPath)
if info.IsDir() {
return os.MkdirAll(targetPath, constant.DirPerm)
}
return copyFile(p, targetPath)
})
}
func copyFile(src, dst string) error {
srcFile, err := os.Open(src)
if err != nil {
return err
}
defer srcFile.Close()
if err := os.MkdirAll(filepath.Dir(dst), constant.DirPerm); err != nil {
return err
}
dstFile, err := os.Create(dst)
if err != nil {
return err
}
defer dstFile.Close()
_, err = io.Copy(dstFile, srcFile)
return err
}
+308 -122
View File
@@ -1,11 +1,13 @@
package service package service
import ( import (
"bytes"
"context" "context"
"crypto/x509" "crypto/x509"
"encoding/json" "encoding/json"
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"io"
"log" "log"
"net" "net"
"os" "os"
@@ -14,6 +16,7 @@ import (
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"sync"
"syscall" "syscall"
"time" "time"
@@ -124,7 +127,257 @@ func applyLocationProxyPass(location *components.Location, proxyPass string, sni
location.UpdateDirective("proxy_ssl_name", []string{sslName}) location.UpdateDirective("proxy_ssl_name", []string{sslName})
} }
func createIndexFile(website *model.Website, runtime *model.Runtime) error { // websiteCreateFiles owns only paths actually created by this attempt.
// Never recursively remove a reused directory (or a newly populated one).
type websiteCreateFiles struct {
mu sync.Mutex
entries []websiteCreatedPath
rolledBack bool
restores []func() error
}
type websiteCreatedPath struct {
path string
info os.FileInfo
}
func (f *websiteCreateFiles) mkdir(name string) error {
f.mu.Lock()
defer f.mu.Unlock()
if f.rolledBack {
return fmt.Errorf("website creation already rolled back")
}
return f.mkdirLocked(filepath.Clean(name))
}
func (f *websiteCreateFiles) mkdirLocked(name string) error {
info, err := os.Lstat(name)
if err == nil {
if !info.IsDir() {
return fmt.Errorf("website path is not a directory: %s", name)
}
return nil
}
if !os.IsNotExist(err) {
return err
}
if err = f.mkdirLocked(filepath.Dir(name)); err != nil {
return err
}
if err = os.Mkdir(name, constant.DirPerm); err != nil {
if os.IsExist(err) {
return f.mkdirLocked(name)
}
return err
}
info, err = os.Lstat(name)
if err != nil {
return err
}
f.entries = append(f.entries, websiteCreatedPath{name, info})
return nil
}
// preserveExisting is for user content and logs; generated configuration must
// fail on collision. Exclusive creation also protects against concurrent writes.
func (f *websiteCreateFiles) write(name string, data []byte, mode os.FileMode, preserveExisting bool) error {
return f.writeReader(name, bytes.NewReader(data), mode, preserveExisting)
}
func (f *websiteCreateFiles) writeReader(name string, data io.Reader, mode os.FileMode, preserveExisting bool) error {
f.mu.Lock()
defer f.mu.Unlock()
if f.rolledBack {
return fmt.Errorf("website creation already rolled back")
}
if err := f.mkdirLocked(filepath.Dir(name)); err != nil {
return err
}
file, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, mode)
if os.IsExist(err) && preserveExisting {
info, statErr := os.Lstat(name)
if statErr != nil {
return statErr
}
if !info.Mode().IsRegular() {
return fmt.Errorf("website path is not a regular file: %s", name)
}
return nil
}
if err != nil {
return fmt.Errorf("create website file %s: %w", name, err)
}
info, err := file.Stat()
if err != nil {
_ = file.Close()
return err
}
f.entries = append(f.entries, websiteCreatedPath{name, info})
_, err = io.Copy(file, data)
closeErr := file.Close()
if err != nil {
return err
}
return closeErr
}
func (f *websiteCreateFiles) rollback() error {
f.mu.Lock()
defer f.mu.Unlock()
f.rolledBack = true
var firstErr error
for i := len(f.restores) - 1; i >= 0; i-- {
if err := f.restores[i](); err != nil && firstErr == nil {
firstErr = err
}
}
f.restores = nil
for i := len(f.entries) - 1; i >= 0; i-- {
entry := f.entries[i]
info, err := os.Lstat(entry.path)
if os.IsNotExist(err) {
continue
}
if err != nil {
if firstErr == nil {
firstErr = err
}
continue
}
if !os.SameFile(entry.info, info) {
continue
}
// os.Remove refuses non-empty directories, preserving untracked content.
if err = os.Remove(entry.path); err != nil && firstErr == nil {
firstErr = err
}
}
return firstErr
}
func (f *websiteCreateFiles) copyTemplate(src, dst string) error {
// Reject symlinks and create files exclusively. The caller rolls back any
// paths created before a collision or copy error.
return filepath.Walk(src, func(name string, info os.FileInfo, err error) error {
if err != nil {
return err
}
rel, err := filepath.Rel(src, name)
if err != nil {
return err
}
target := filepath.Join(dst, rel)
if info.IsDir() {
return f.mkdir(target)
}
if !info.Mode().IsRegular() {
return fmt.Errorf("unsupported template file: %s", name)
}
file, err := os.Open(name)
if err != nil {
return err
}
defer file.Close()
return f.writeReader(target, file, constant.FilePerm, false)
})
}
// Only change ownership on files created by this attempt, never recursively on
// an existing document root.
func (f *websiteCreateFiles) chownNew(root string) error {
f.mu.Lock()
defer f.mu.Unlock()
if f.rolledBack {
return fmt.Errorf("website creation already rolled back")
}
for _, entry := range f.entries {
rel, err := filepath.Rel(root, entry.path)
if err != nil {
return err
}
if rel == ".." || filepath.IsAbs(rel) || (len(rel) > 3 && rel[:3] == "../") {
continue
}
info, err := os.Lstat(entry.path)
if err != nil {
return err
}
if !os.SameFile(entry.info, info) {
continue
}
if err := os.Chown(entry.path, constant.WebsiteUID, constant.WebsiteGID); err != nil {
return err
}
}
return nil
}
// Serialize sites.json read-modify-write operations across website creations,
// deletions, domain updates, and creation rollbacks.
var wafWebsitesMu sync.Mutex
func updateWAFWebsites(name string, update func([]request.WafWebsite) ([]request.WafWebsite, error)) error {
wafWebsitesMu.Lock()
defer wafWebsitesMu.Unlock()
info, err := os.Lstat(name)
if err != nil {
return err
}
if !info.Mode().IsRegular() {
return fmt.Errorf("website config is not a regular file: %s", name)
}
content, err := os.ReadFile(name)
if err != nil {
return err
}
var websites []request.WafWebsite
if len(content) > 0 {
if err := json.Unmarshal(content, &websites); err != nil {
return err
}
}
websites, err = update(websites)
if err != nil {
return err
}
content, err = json.Marshal(websites)
if err != nil {
return err
}
return os.WriteFile(name, content, info.Mode().Perm())
}
func (f *websiteCreateFiles) addWAFWebsite(name string, website request.WafWebsite) error {
f.mu.Lock()
defer f.mu.Unlock()
if f.rolledBack {
return fmt.Errorf("website creation already rolled back")
}
if err := updateWAFWebsites(name, func(websites []request.WafWebsite) ([]request.WafWebsite, error) {
for _, existing := range websites {
if existing.Key == website.Key {
return nil, fmt.Errorf("WAF website already exists: %s", website.Key)
}
}
return append(websites, website), nil
}); err != nil {
return err
}
f.restores = append(f.restores, func() error {
return updateWAFWebsites(name, func(websites []request.WafWebsite) ([]request.WafWebsite, error) {
// Keep the latest records for other sites, including concurrent domain changes.
remaining := make([]request.WafWebsite, 0, len(websites))
for _, existing := range websites {
if existing.Key != website.Key {
remaining = append(remaining, existing)
}
}
return remaining, nil
})
})
return nil
}
func createIndexFile(website *model.Website, runtime *model.Runtime, created *websiteCreateFiles) error {
var ( var (
indexPath string indexPath string
indexContent string indexContent string
@@ -145,49 +398,23 @@ func createIndexFile(website *model.Website, runtime *model.Runtime) error {
} }
} }
fileOp := files.NewFileOp() if err := created.write(indexPath, []byte(indexContent), constant.FilePerm, true); err != nil {
if !fileOp.Stat(indexFolder) {
if err := fileOp.CreateDir(indexFolder, constant.DirPerm); err != nil {
return err return err
} }
} html404, _ := websiteService.GetDefaultHtml("404")
if !fileOp.Stat(indexPath) { if err := created.write(path.Join(indexFolder, "404.html"), []byte(html404.Content), constant.FilePerm, true); err != nil {
if err := fileOp.CreateFile(indexPath); err != nil {
return err return err
} }
}
if website.Type == constant.Runtime && runtime.Resource == constant.ResourceAppstore { if website.Type == constant.Runtime && runtime.Resource == constant.ResourceAppstore {
if err := chownRootDir(indexFolder); err != nil { return created.chownNew(indexFolder)
return err
}
}
if err := fileOp.WriteFile(indexPath, strings.NewReader(indexContent), constant.DirPerm); err != nil {
return err
}
html404, _ := websiteService.GetDefaultHtml("404")
path404 := path.Join(indexFolder, "404.html")
if err := fileOp.WriteFile(path404, strings.NewReader(html404.Content), constant.DirPerm); err != nil {
return err
} }
return nil return nil
} }
func createProxyFile(website *model.Website) error { func createProxyFile(website *model.Website, created *websiteCreateFiles) error {
proxyFolder := GetSitePath(*website, SiteProxyDir) proxyFolder := GetSitePath(*website, SiteProxyDir)
filePath := path.Join(proxyFolder, "root.conf") filePath := path.Join(proxyFolder, "root.conf")
fileOp := files.NewFileOp()
if !fileOp.Stat(proxyFolder) {
if err := fileOp.CreateDir(proxyFolder, constant.DirPerm); err != nil {
return err
}
}
if !fileOp.Stat(filePath) {
if err := fileOp.CreateFile(filePath); err != nil {
return err
}
}
config, err := parser.NewStringParser(string(nginx_conf.Proxy)).Parse() config, err := parser.NewStringParser(string(nginx_conf.Proxy)).Parse()
if err != nil { if err != nil {
return err return err
@@ -201,57 +428,40 @@ func createProxyFile(website *model.Website) error {
location.ChangePath("^~", "/") location.ChangePath("^~", "/")
applyLocationProxyPass(location, website.Proxy, nil, "") applyLocationProxyPass(location, website.Proxy, nil, "")
location.UpdateDirective("proxy_set_header", []string{"Host", "$host"}) location.UpdateDirective("proxy_set_header", []string{"Host", "$host"})
if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { if err := created.write(filePath, []byte(nginx.DumpConfig(config, nginx.IndentedStyle)), constant.FilePerm, false); err != nil {
return buserr.WithErr("ErrUpdateBuWebsite", err) return buserr.WithErr("ErrUpdateBuWebsite", err)
} }
return nil return nil
} }
func createWebsiteFolder(website *model.Website, runtime *model.Runtime) error { func createWebsiteFolder(website *model.Website, runtime *model.Runtime, created *websiteCreateFiles) error {
siteFolder := GetSiteDir(website.Alias) siteFolder := GetSiteDir(website.Alias)
fileOp := files.NewFileOp() dirs := []string{siteFolder, path.Join(siteFolder, "log")}
if !fileOp.Stat(siteFolder) {
if err := fileOp.CreateDir(siteFolder, constant.DirPerm); err != nil {
return err
}
if err := fileOp.CreateDir(path.Join(siteFolder, "log"), constant.DirPerm); err != nil {
return err
}
if err := fileOp.CreateFile(path.Join(siteFolder, "log", "access.log")); err != nil {
return err
}
if err := fileOp.CreateFile(path.Join(siteFolder, "log", "error.log")); err != nil {
return err
}
if website.Type != constant.Stream { if website.Type != constant.Stream {
if err := fileOp.CreateDir(path.Join(siteFolder, "index"), constant.DirPerm); err != nil { dirs = append(dirs, path.Join(siteFolder, "index"), path.Join(siteFolder, "ssl"))
return err
} }
if err := fileOp.CreateDir(path.Join(siteFolder, "ssl"), constant.DirPerm); err != nil { for _, dir := range dirs {
if err := created.mkdir(dir); err != nil {
return err return err
} }
} }
if website.Type == constant.Runtime { for _, name := range []string{"access.log", "error.log"} {
if runtime.Type == constant.RuntimePHP && runtime.Resource == constant.ResourceLocal { if err := created.write(path.Join(siteFolder, "log", name), nil, constant.FilePerm, true); err != nil {
phpPoolDir := path.Join(siteFolder, "php-pool")
if err := fileOp.CreateDir(phpPoolDir, constant.DirPerm); err != nil {
return err return err
} }
if err := fileOp.CreateFile(path.Join(phpPoolDir, "php-fpm.sock")); err != nil {
return err
} }
if website.Type == constant.Runtime && runtime.Type == constant.RuntimePHP && runtime.Resource == constant.ResourceLocal {
if err := created.write(path.Join(siteFolder, "php-pool", "php-fpm.sock"), nil, constant.FilePerm, true); err != nil {
return err
} }
} }
if website.Type == constant.Static || (website.Type == constant.Runtime && runtime.Type == constant.RuntimePHP) { if website.Type == constant.Static || (website.Type == constant.Runtime && runtime.Type == constant.RuntimePHP) {
if err := createIndexFile(website, runtime); err != nil { if err := createIndexFile(website, runtime, created); err != nil {
return err return err
} }
} }
if website.Type == constant.Proxy { if website.Type == constant.Proxy {
if err := createProxyFile(website); err != nil { return createProxyFile(website, created)
return err
}
}
} }
return nil return nil
} }
@@ -261,12 +471,12 @@ type websiteInitialSSL struct {
request request.WebsiteHTTPSOp request request.WebsiteHTTPSOp
} }
func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, appInstall *model.AppInstall, runtime *model.Runtime, streamConfig request.StreamConfig, initialSSL *websiteInitialSSL) error { func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, appInstall *model.AppInstall, runtime *model.Runtime, streamConfig request.StreamConfig, initialSSL *websiteInitialSSL, created *websiteCreateFiles) error {
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil { if err != nil {
return err return err
} }
if err = createWebsiteFolder(website, runtime); err != nil { if err = createWebsiteFolder(website, runtime, created); err != nil {
return err return err
} }
var ( var (
@@ -334,7 +544,10 @@ func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, a
if initialSSL != nil { if initialSSL != nil {
plan := buildWebsiteTLSPlan(domains, nginxInstall.HttpPort, nginxInstall.HttpsPort) plan := buildWebsiteTLSPlan(domains, nginxInstall.HttpPort, nginxInstall.HttpsPort)
applyWebsiteSSLConfig(server, *website, plan, initialSSL.request) applyWebsiteSSLConfig(server, *website, plan, initialSSL.request)
if err = createPemFile(*website, initialSSL.certificate); err != nil { if err = created.write(path.Join(GetSitePath(*website, SiteSSLDir), "fullchain.pem"), []byte(initialSSL.certificate.Pem), constant.FilePerm, false); err != nil {
return err
}
if err = created.write(path.Join(GetSitePath(*website, SiteSSLDir), "privkey.pem"), []byte(initialSSL.certificate.PrivateKey), 0600, false); err != nil {
return err return err
} }
} }
@@ -404,18 +617,14 @@ func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, a
} }
} }
config.FilePath = configPath config.FilePath = configPath
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { if err = created.write(configPath, []byte(nginx.DumpConfig(config, nginx.IndentedStyle)), constant.FilePerm, false); err != nil {
return err
}
if err = opNginx(nginxInstall.ContainerName, constant.NginxCheck); err != nil {
return err
}
if err = opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil {
return err return err
} }
return nil return nil
} }
var defaultWAFRuleNames = []string{"acl", "args", "cookie", "defaultUaBlack", "defaultUrlBlack", "fileExt", "header", "methodWhite", "cdn"}
func moveDefaultWafConfig(websiteDir string, defaultConfigContent []byte, defaultRuleDir string, fileOp files.FileOp) error { func moveDefaultWafConfig(websiteDir string, defaultConfigContent []byte, defaultRuleDir string, fileOp files.FileOp) error {
if !fileOp.Stat(websiteDir) { if !fileOp.Stat(websiteDir) {
if err := fileOp.CreateDir(websiteDir, constant.DirPerm); err != nil { if err := fileOp.CreateDir(websiteDir, constant.DirPerm); err != nil {
@@ -431,8 +640,7 @@ func moveDefaultWafConfig(websiteDir string, defaultConfigContent []byte, defaul
return err return err
} }
} }
defaultRulesName := []string{"acl", "args", "cookie", "defaultUaBlack", "defaultUrlBlack", "fileExt", "header", "methodWhite", "cdn"} for _, ruleName := range defaultWAFRuleNames {
for _, ruleName := range defaultRulesName {
srcPath := path.Join(defaultRuleDir, ruleName+".json") srcPath := path.Join(defaultRuleDir, ruleName+".json")
if fileOp.Stat(srcPath) { if fileOp.Stat(srcPath) {
_ = fileOp.Copy(srcPath, websiteRuleDir) _ = fileOp.Copy(srcPath, websiteRuleDir)
@@ -470,7 +678,10 @@ func createAllWebsitesWAFConfig(websites []model.Website) error {
if err != nil { if err != nil {
return err return err
} }
if err := fileOp.SaveFileWithByte(websitesConfigPath, websitesContent, constant.DirPerm); err != nil { wafWebsitesMu.Lock()
err = fileOp.SaveFileWithByte(websitesConfigPath, websitesContent, constant.DirPerm)
wafWebsitesMu.Unlock()
if err != nil {
return err return err
} }
var ( var (
@@ -499,7 +710,7 @@ func createOpenBasedirConfig(website *model.Website) {
_ = fileOp.SaveFile(userIniPath, fmt.Sprintf("open_basedir=/www/sites/%s/index:/tmp/", website.Alias), 0644) _ = fileOp.SaveFile(userIniPath, fmt.Sprintf("open_basedir=/www/sites/%s/index:/tmp/", website.Alias), 0644)
} }
func createWafConfig(website *model.Website, domains []model.WebsiteDomain) error { func createWafConfig(website *model.Website, domains []model.WebsiteDomain, created *websiteCreateFiles) error {
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil { if err != nil {
return err return err
@@ -510,16 +721,6 @@ func createWafConfig(website *model.Website, domains []model.WebsiteDomain) erro
return nil return nil
} }
websitesConfigPath := path.Join(wafDataPath, "conf", "sites.json") websitesConfigPath := path.Join(wafDataPath, "conf", "sites.json")
content, err := fileOp.GetContent(websitesConfigPath)
if err != nil {
return err
}
var websitesArray []request.WafWebsite
if len(content) != 0 {
if err := json.Unmarshal(content, &websitesArray); err != nil {
return err
}
}
wafWebsite := request.WafWebsite{ wafWebsite := request.WafWebsite{
Key: website.Alias, Key: website.Alias,
Domains: make([]string, 0), Domains: make([]string, 0),
@@ -530,14 +731,6 @@ func createWafConfig(website *model.Website, domains []model.WebsiteDomain) erro
wafWebsite.Domains = append(wafWebsite.Domains, domain.Domain) wafWebsite.Domains = append(wafWebsite.Domains, domain.Domain)
wafWebsite.Host = append(wafWebsite.Host, domain.Domain+":"+strconv.Itoa(domain.Port)) wafWebsite.Host = append(wafWebsite.Host, domain.Domain+":"+strconv.Itoa(domain.Port))
} }
websitesArray = append(websitesArray, wafWebsite)
websitesContent, err := json.Marshal(websitesArray)
if err != nil {
return err
}
if err := fileOp.SaveFileWithByte(websitesConfigPath, websitesContent, constant.DirPerm); err != nil {
return err
}
var ( var (
sitesDir = path.Join(wafDataPath, "sites") sitesDir = path.Join(wafDataPath, "sites")
@@ -551,20 +744,25 @@ func createWafConfig(website *model.Website, domains []model.WebsiteDomain) erro
return err return err
} }
defer func() { if err := created.write(path.Join(websiteDir, "config.json"), defaultConfigContent, constant.FilePerm, false); err != nil {
return err
}
if err := created.mkdir(path.Join(websiteDir, "rules")); err != nil {
return err
}
for _, name := range defaultWAFRuleNames {
data, err := os.ReadFile(path.Join(defaultRuleDir, name+".json"))
if os.IsNotExist(err) {
continue
}
if err != nil { if err != nil {
_ = fileOp.DeleteDir(websiteDir)
}
}()
if err := moveDefaultWafConfig(websiteDir, defaultConfigContent, defaultRuleDir, fileOp); err != nil {
return err return err
} }
if err := created.write(path.Join(websiteDir, "rules", name+".json"), data, constant.FilePerm, false); err != nil {
if err = opNginx(nginxInstall.ContainerName, constant.NginxCheck); err != nil {
return err return err
} }
if err = opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil { }
if err := created.addWAFWebsite(websitesConfigPath, wafWebsite); err != nil {
return err return err
} }
@@ -627,27 +825,15 @@ func delWafConfig(website model.Website, force bool) error {
return nil return nil
} }
websitesConfigPath := path.Join(wafDataPath, "conf", "sites.json") websitesConfigPath := path.Join(wafDataPath, "conf", "sites.json")
content, err := fileOp.GetContent(websitesConfigPath) if err := updateWAFWebsites(websitesConfigPath, func(websites []request.WafWebsite) ([]request.WafWebsite, error) {
if err != nil { remaining := make([]request.WafWebsite, 0, len(websites))
return err for _, existing := range websites {
} if existing.Key != website.Alias {
var websitesArray []request.WafWebsite remaining = append(remaining, existing)
var newWebsiteArray []request.WafWebsite
if len(content) > 0 {
if err = json.Unmarshal(content, &websitesArray); err != nil {
return err
} }
} }
for _, wafWebsite := range websitesArray { return remaining, nil
if wafWebsite.Key != website.Alias { }); err != nil {
newWebsiteArray = append(newWebsiteArray, wafWebsite)
}
}
websitesContent, err := json.Marshal(newWebsiteArray)
if err != nil {
return err
}
if err := fileOp.SaveFileWithByte(websitesConfigPath, websitesContent, constant.DirPerm); err != nil {
return err return err
} }
+8
View File
@@ -258,7 +258,15 @@ func (s *SubTask) Execute() error {
case <-ctx.Done(): case <-ctx.Done():
s.RootTask.Log(i18n.GetWithName("TaskTimeout", subTaskName)) s.RootTask.Log(i18n.GetWithName("TaskTimeout", subTaskName))
err = errors.New("timeout!") err = errors.New("timeout!")
if s.RootTask.Task.Type == TaskScopeVm {
<-done
}
case err = <-done: case err = <-done:
if s.RootTask.Task.Type == TaskScopeVm && ctx.Err() != nil {
s.RootTask.Log(i18n.GetWithName("TaskTimeout", subTaskName))
err = errors.New("timeout!")
break
}
if err != nil { if err != nil {
s.RootTask.Log(i18n.GetWithNameAndErr("SubTaskFailed", subTaskName, err)) s.RootTask.Log(i18n.GetWithNameAndErr("SubTaskFailed", subTaskName, err))
if err.Error() == i18n.GetMsgByKey("ErrShutDown") { if err.Error() == i18n.GetMsgByKey("ErrShutDown") {
+348 -352
View File
@@ -265,8 +265,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "应用端口修改 [key]-[name] => [port]", "formatZH": "应用端口修改 [key]-[name] =\u003e [port]",
"formatEN": "Application port update [key]-[name] => [port]" "formatEN": "Application port update [key]-[name] =\u003e [port]"
}, },
"/apps/installed/sort/update": { "/apps/installed/sort/update": {
"bodyKeys": [], "bodyKeys": [],
@@ -679,8 +679,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "容器重命名 [name] => [newName]", "formatZH": "容器重命名 [name] =\u003e [newName]",
"formatEN": "rename container [name] => [newName]" "formatEN": "rename container [name] =\u003e [newName]"
}, },
"/containers/repo": { "/containers/repo": {
"bodyKeys": [ "bodyKeys": [
@@ -831,8 +831,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "更新 API 接口配置 => IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]", "formatZH": "更新 API 接口配置 =\u003e IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
"formatEN": "update api config => IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]" "formatEN": "update api config =\u003e IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
}, },
"/core/auth/expired/reset": { "/core/auth/expired/reset": {
"bodyKeys": [], "bodyKeys": [],
@@ -1716,8 +1716,8 @@
"output_value": "name" "output_value": "name"
} }
], ],
"formatZH": "更新用户 [name] API 接口配置 => IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]", "formatZH": "更新用户 [name] API 接口配置 =\u003e IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
"formatEN": "update user [name] api config => IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]" "formatEN": "update user [name] api config =\u003e IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
}, },
"/core/enterprise/users/del": { "/core/enterprise/users/del": {
"bodyKeys": [ "bodyKeys": [
@@ -1918,8 +1918,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改系统监听信息 => ipv6: [ipv6], 监听 IP: [bindAddress]", "formatZH": "修改系统监听信息 =\u003e ipv6: [ipv6], 监听 IP: [bindAddress]",
"formatEN": "update system bind info => ipv6: [ipv6], 监听 IP: [bindAddress]" "formatEN": "update system bind info =\u003e ipv6: [ipv6], 监听 IP: [bindAddress]"
}, },
"/core/settings/memo": { "/core/settings/memo": {
"bodyKeys": [], "bodyKeys": [],
@@ -1948,8 +1948,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改系统端口 => [serverPort]", "formatZH": "修改系统端口 =\u003e [serverPort]",
"formatEN": "update system port => [serverPort]" "formatEN": "update system port =\u003e [serverPort]"
}, },
"/core/settings/proxy/update": { "/core/settings/proxy/update": {
"bodyKeys": [ "bodyKeys": [
@@ -1974,8 +1974,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改系统 ssl => [ssl]", "formatZH": "修改系统 ssl =\u003e [ssl]",
"formatEN": "update system ssl => [ssl]" "formatEN": "update system ssl =\u003e [ssl]"
}, },
"/core/settings/terminal/update": { "/core/settings/terminal/update": {
"bodyKeys": [], "bodyKeys": [],
@@ -1991,8 +1991,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改系统配置 [key] => [value]", "formatZH": "修改系统配置 [key] =\u003e [value]",
"formatEN": "update system setting [key] => [value]" "formatEN": "update system setting [key] =\u003e [value]"
}, },
"/core/settings/upgrade": { "/core/settings/upgrade": {
"bodyKeys": [ "bodyKeys": [
@@ -2000,8 +2000,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "更新系统 => [version]", "formatZH": "更新系统 =\u003e [version]",
"formatEN": "upgrade system => [version]" "formatEN": "upgrade system =\u003e [version]"
}, },
"/core/xapp/appQRCode": { "/core/xapp/appQRCode": {
"bodyKeys": [ "bodyKeys": [
@@ -2459,282 +2459,6 @@
"formatZH": "同步 SSL 证书 [primaryDomain]", "formatZH": "同步 SSL 证书 [primaryDomain]",
"formatEN": "sync SSL certificate [primaryDomain]" "formatEN": "sync SSL certificate [primaryDomain]"
}, },
"/xpack/vms": {
"bodyKeys": [
"name"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机 [name]",
"formatEN": "create VM [name]"
},
"/xpack/vms/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "virtual_machines",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机 [name]",
"formatEN": "delete VM [name]"
},
"/xpack/vms/disks/resize": {
"bodyKeys": [
"name",
"target",
"sizeGiB"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "扩容虚拟机 [name] 磁盘 [target] 至 [sizeGiB] GiB",
"formatEN": "expand VM [name] disk [target] to [sizeGiB] GiB"
},
"/xpack/vms/environment/enable": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "启用虚拟机运行环境",
"formatEN": "enable VM runtime environment"
},
"/xpack/vms/iso": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机镜像 [name] [type]",
"formatEN": "create VM ISO [name] [type]"
},
"/xpack/vms/iso/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_isos",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机镜像 [name]",
"formatEN": "delete VM ISO [name]"
},
"/xpack/vms/iso/update": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新虚拟机镜像 [name] [type]",
"formatEN": "update VM ISO [name] [type]"
},
"/xpack/vms/networks": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机网络 [name] [type]",
"formatEN": "create VM network [name] [type]"
},
"/xpack/vms/networks/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_networks",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机网络 [name]",
"formatEN": "delete VM network [name]"
},
"/xpack/vms/networks/update": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_networks",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "更新虚拟机网络 [name]",
"formatEN": "update VM network [name]"
},
"/xpack/vms/operate": {
"bodyKeys": [
"name",
"operate"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "操作虚拟机 [name] [operate]",
"formatEN": "operate VM [name] [operate]"
},
"/xpack/vms/orphans/clean": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "清理虚拟机孤立数据",
"formatEN": "clean VM orphaned data"
},
"/xpack/vms/rename": {
"bodyKeys": [
"name",
"newName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "重命名虚拟机 [name] 为 [newName]",
"formatEN": "rename VM [name] to [newName]"
},
"/xpack/vms/snapshots": {
"bodyKeys": [
"name",
"snapshotName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机 [name] 快照 [snapshotName]",
"formatEN": "create VM [name] snapshot [snapshotName]"
},
"/xpack/vms/snapshots/del": {
"bodyKeys": [
"name",
"snapshotName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除虚拟机 [name] 快照 [snapshotName]",
"formatEN": "delete VM [name] snapshot [snapshotName]"
},
"/xpack/vms/snapshots/recover": {
"bodyKeys": [
"name",
"snapshotName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "恢复虚拟机 [name] 快照 [snapshotName]",
"formatEN": "recover VM [name] snapshot [snapshotName]"
},
"/xpack/vms/storages": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机存储 [name] [type]",
"formatEN": "create VM storage [name] [type]"
},
"/xpack/vms/storages/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_storages",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机存储 [name]",
"formatEN": "delete VM storage [name]"
},
"/xpack/vms/storages/update": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_storages",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "更新虚拟机存储 [name]",
"formatEN": "update VM storage [name]"
},
"/xpack/vms/sync": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "同步虚拟机",
"formatEN": "sync virtual machines"
},
"/xpack/vms/templates": {
"bodyKeys": [
"name",
"templateName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机 [name] 模板 [templateName]",
"formatEN": "create VM [name] template [templateName]"
},
"/xpack/vms/templates/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_templates",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机模板 [name]",
"formatEN": "delete VM template [name]"
},
"/xpack/vms/update": {
"bodyKeys": [
"name"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新虚拟机 [name]",
"formatEN": "update VM [name]"
},
"/cronjobs": { "/cronjobs": {
"bodyKeys": [ "bodyKeys": [
"type", "type",
@@ -2888,8 +2612,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "首页应用 [key] => 显示:[value]", "formatZH": "首页应用 [key] =\u003e 显示:[value]",
"formatEN": "app launcher [key] => show: [value]" "formatEN": "app launcher [key] =\u003e show: [value]"
}, },
"/dashboard/quick/change": { "/dashboard/quick/change": {
"bodyKeys": [], "bodyKeys": [],
@@ -3017,7 +2741,7 @@
} }
], ],
"formatZH": "mysql 数据库 [name] 描述信息修改 [description]", "formatZH": "mysql 数据库 [name] 描述信息修改 [description]",
"formatEN": "The description of the mysql database [name] is modified => [description]" "formatEN": "The description of the mysql database [name] is modified =\u003e [description]"
}, },
"/databases/grants": { "/databases/grants": {
"bodyKeys": [ "bodyKeys": [
@@ -3098,7 +2822,7 @@
} }
], ],
"formatZH": "mongodb 数据库 [name] 描述信息修改 [description]", "formatZH": "mongodb 数据库 [name] 描述信息修改 [description]",
"formatEN": "The description of the mongodb database [name] is modified => [description]" "formatEN": "The description of the mongodb database [name] is modified =\u003e [description]"
}, },
"/databases/mongodb/password": { "/databases/mongodb/password": {
"bodyKeys": [ "bodyKeys": [
@@ -3183,7 +2907,7 @@
} }
], ],
"formatZH": "postgresql 数据库 [name] 描述信息修改 [description]", "formatZH": "postgresql 数据库 [name] 描述信息修改 [description]",
"formatEN": "The description of the postgresql database [name] is modified => [description]" "formatEN": "The description of the postgresql database [name] is modified =\u003e [description]"
}, },
"/databases/pg/password": { "/databases/pg/password": {
"bodyKeys": [ "bodyKeys": [
@@ -3288,8 +3012,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "更新 mysql 数据库 [database] 用户 [username] 访问权限 [host] => [newHost] 描述 [description]", "formatZH": "更新 mysql 数据库 [database] 用户 [username] 访问权限 [host] =\u003e [newHost] 描述 [description]",
"formatEN": "update mysql database [database] user [username] access [host] => [newHost] description [description]" "formatEN": "update mysql database [database] user [username] access [host] =\u003e [newHost] description [description]"
}, },
"/databases/variables/update": { "/databases/variables/update": {
"bodyKeys": [], "bodyKeys": [],
@@ -3325,8 +3049,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "批量修改文件权限和用户/组 [paths] => [mode]/[user]/[group]", "formatZH": "批量修改文件权限和用户/组 [paths] =\u003e [mode]/[user]/[group]",
"formatEN": "Batch change file mode and owner [paths] => [mode]/[user]/[group]" "formatEN": "Batch change file mode and owner [paths] =\u003e [mode]/[user]/[group]"
}, },
"/files/chunkdownload": { "/files/chunkdownload": {
"bodyKeys": [ "bodyKeys": [
@@ -3416,8 +3140,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改权限 [path] => [mode]", "formatZH": "修改权限 [path] =\u003e [mode]",
"formatEN": "Change mode [path] => [mode]" "formatEN": "Change mode [path] =\u003e [mode]"
}, },
"/files/move": { "/files/move": {
"bodyKeys": [ "bodyKeys": [
@@ -3426,8 +3150,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "移动文件 [oldPaths] => [newPath]", "formatZH": "移动文件 [oldPaths] =\u003e [newPath]",
"formatEN": "Move [oldPaths] => [newPath]" "formatEN": "Move [oldPaths] =\u003e [newPath]"
}, },
"/files/owner": { "/files/owner": {
"bodyKeys": [ "bodyKeys": [
@@ -3437,8 +3161,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改用户/组 [path] => [user]/[group]", "formatZH": "修改用户/组 [path] =\u003e [user]/[group]",
"formatEN": "Change owner [path] => [user]/[group]" "formatEN": "Change owner [path] =\u003e [user]/[group]"
}, },
"/files/preview": { "/files/preview": {
"bodyKeys": [ "bodyKeys": [
@@ -3472,8 +3196,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "重命名 [oldName] => [newName]", "formatZH": "重命名 [oldName] =\u003e [newName]",
"formatEN": "Rename [oldName] => [newName]" "formatEN": "Rename [oldName] =\u003e [newName]"
}, },
"/files/save": { "/files/save": {
"bodyKeys": [ "bodyKeys": [
@@ -3530,8 +3254,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "下载 url => [path]/[name]", "formatZH": "下载 url =\u003e [path]/[name]",
"formatEN": "Download url => [path]/[name]" "formatEN": "Download url =\u003e [path]/[name]"
}, },
"/files/wget/process/remove": { "/files/wget/process/remove": {
"bodyKeys": [ "bodyKeys": [
@@ -3673,13 +3397,11 @@
"formatEN": "batch update Docker port guard policies" "formatEN": "batch update Docker port guard policies"
}, },
"/hosts/firewall/docker/policies/delete/batch": { "/hosts/firewall/docker/policies/delete/batch": {
"bodyKeys": [ "bodyKeys": [],
"uuids"
],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "删除 Docker 端口防护策略 [uuids]", "formatZH": "删除 Docker 端口防护策略",
"formatEN": "delete Docker port guard policies [uuids]" "formatEN": "delete Docker port guard policies"
}, },
"/hosts/firewall/family/operate": { "/hosts/firewall/family/operate": {
"bodyKeys": [ "bodyKeys": [
@@ -3687,10 +3409,10 @@
"family", "family",
"operation" "operation"
], ],
"formatEN": "[operation] [subsystem] [family] firewall chains",
"formatZH": "[operation] [subsystem] [family] 防火墙链",
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [] "beforeFunctions": [],
"formatZH": "[operation] [subsystem] [family] 防火墙链",
"formatEN": "[operation] [subsystem] [family] firewall chains"
}, },
"/hosts/firewall/filter/operate": { "/hosts/firewall/filter/operate": {
"bodyKeys": [ "bodyKeys": [
@@ -3739,13 +3461,11 @@
"formatEN": "delete firewall rules" "formatEN": "delete firewall rules"
}, },
"/hosts/firewall/rules/reorder": { "/hosts/firewall/rules/reorder": {
"bodyKeys": [ "bodyKeys": [],
"instanceKey"
],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "调整防火墙规则顺序 [instanceKey]", "formatZH": "调整防火墙规则顺序",
"formatEN": "reorder firewall rule [instanceKey]" "formatEN": "reorder firewall rule"
}, },
"/hosts/firewall/rules/reset": { "/hosts/firewall/rules/reset": {
"bodyKeys": [], "bodyKeys": [],
@@ -3755,22 +3475,20 @@
"formatEN": "reset firewall rules" "formatEN": "reset firewall rules"
}, },
"/hosts/firewall/rules/update": { "/hosts/firewall/rules/update": {
"bodyKeys": [ "bodyKeys": [],
"instanceKey"
],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "更新防火墙规则 [instanceKey]", "formatZH": "更新防火墙规则",
"formatEN": "update firewall rule [instanceKey]" "formatEN": "update firewall rule"
}, },
"/hosts/firewall/settings/ipv6": { "/hosts/firewall/settings/ipv6": {
"bodyKeys": [ "bodyKeys": [
"status" "status"
], ],
"formatEN": "Set firewall IPv6 support to [status]",
"formatZH": "设置防火墙 IPv6 支持为 [status]",
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [] "beforeFunctions": [],
"formatZH": "设置防火墙 IPv6 支持为 [status]",
"formatEN": "Set firewall IPv6 support to [status]"
}, },
"/hosts/firewall/settings/operate": { "/hosts/firewall/settings/operate": {
"bodyKeys": [ "bodyKeys": [
@@ -3812,11 +3530,13 @@
"formatEN": "update firewall port whitelist" "formatEN": "update firewall port whitelist"
}, },
"/hosts/monitor/clean": { "/hosts/monitor/clean": {
"bodyKeys": [], "bodyKeys": [
"type"
],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "清空监控数据", "formatZH": "清空 [type] 监控数据",
"formatEN": "clean monitor datas" "formatEN": "clean [type] monitoring data"
}, },
"/hosts/monitor/setting/update": { "/hosts/monitor/setting/update": {
"bodyKeys": [ "bodyKeys": [
@@ -3888,8 +3608,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改 SSH 配置 [key] => [newValue]", "formatZH": "修改 SSH 配置 [key] =\u003e [newValue]",
"formatEN": "update SSH setting [key] => [newValue]" "formatEN": "update SSH setting [key] =\u003e [newValue]"
}, },
"/hosts/tool/config/set": { "/hosts/tool/config/set": {
"bodyKeys": [ "bodyKeys": [
@@ -4150,7 +3870,7 @@
} }
], ],
"formatZH": "快照 [name] 描述信息修改 [description]", "formatZH": "快照 [name] 描述信息修改 [description]",
"formatEN": "The description of the snapshot [name] is modified => [description]" "formatEN": "The description of the snapshot [name] is modified =\u003e [description]"
}, },
"/settings/snapshot/import": { "/settings/snapshot/import": {
"bodyKeys": [ "bodyKeys": [
@@ -4242,8 +3962,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改系统配置 [key] => [value]", "formatZH": "修改系统配置 [key] =\u003e [value]",
"formatEN": "update system setting [key] => [value]" "formatEN": "update system setting [key] =\u003e [value]"
}, },
"/toolbox/clam": { "/toolbox/clam": {
"bodyKeys": [ "bodyKeys": [
@@ -4361,8 +4081,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改主机参数 [key] => [value]", "formatZH": "修改主机参数 [key] =\u003e [value]",
"formatEN": "update device conf [key] => [value]" "formatEN": "update device conf [key] =\u003e [value]"
}, },
"/toolbox/device/update/host": { "/toolbox/device/update/host": {
"bodyKeys": [ "bodyKeys": [
@@ -4371,8 +4091,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改主机 Host [key] => [value]", "formatZH": "修改主机 Host [key] =\u003e [value]",
"formatEN": "update device host [key] => [value]" "formatEN": "update device host [key] =\u003e [value]"
}, },
"/toolbox/device/update/swap": { "/toolbox/device/update/swap": {
"bodyKeys": [ "bodyKeys": [
@@ -4400,8 +4120,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改 Fail2ban 配置 [key] => [value]", "formatZH": "修改 Fail2ban 配置 [key] =\u003e [value]",
"formatEN": "update fail2ban conf [key] => [value]" "formatEN": "update fail2ban conf [key] =\u003e [value]"
}, },
"/toolbox/ftp": { "/toolbox/ftp": {
"bodyKeys": [ "bodyKeys": [
@@ -4660,8 +4380,8 @@
"output_value": "domain" "output_value": "domain"
} }
], ],
"formatZH": "修改默认 server => [domain]", "formatZH": "修改默认 server =\u003e [domain]",
"formatEN": "Change default server => [domain]" "formatEN": "Change default server =\u003e [domain]"
}, },
"/websites/del": { "/websites/del": {
"bodyKeys": [ "bodyKeys": [
@@ -5424,8 +5144,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "更新防篡改信息 [website][path] => [status]", "formatZH": "更新防篡改信息 [website][path] =\u003e [status]",
"formatEN": "update tamper info [website][path] => [status]" "formatEN": "update tamper info [website][path] =\u003e [status]"
}, },
"/xpack/vllm/command-template/create": { "/xpack/vllm/command-template/create": {
"bodyKeys": [ "bodyKeys": [
@@ -5504,6 +5224,282 @@
"formatZH": "更新 vLLM [name]", "formatZH": "更新 vLLM [name]",
"formatEN": "update vLLM [name]" "formatEN": "update vLLM [name]"
}, },
"/xpack/vms": {
"bodyKeys": [
"name"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机 [name]",
"formatEN": "create VM [name]"
},
"/xpack/vms/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "virtual_machines",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机 [name]",
"formatEN": "delete VM [name]"
},
"/xpack/vms/disks/resize": {
"bodyKeys": [
"name",
"target",
"sizeGiB"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "扩容虚拟机 [name] 磁盘 [target] 至 [sizeGiB] GiB",
"formatEN": "expand VM [name] disk [target] to [sizeGiB] GiB"
},
"/xpack/vms/environment/enable": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "启用虚拟机运行环境",
"formatEN": "enable VM runtime environment"
},
"/xpack/vms/iso": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机镜像 [name] [type]",
"formatEN": "create VM ISO [name] [type]"
},
"/xpack/vms/iso/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_isos",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机镜像 [name]",
"formatEN": "delete VM ISO [name]"
},
"/xpack/vms/iso/update": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新虚拟机镜像 [name] [type]",
"formatEN": "update VM ISO [name] [type]"
},
"/xpack/vms/networks": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机网络 [name] [type]",
"formatEN": "create VM network [name] [type]"
},
"/xpack/vms/networks/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_networks",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机网络 [name]",
"formatEN": "delete VM network [name]"
},
"/xpack/vms/networks/update": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_networks",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "更新虚拟机网络 [name]",
"formatEN": "update VM network [name]"
},
"/xpack/vms/operate": {
"bodyKeys": [
"name",
"operate"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "操作虚拟机 [name] [operate]",
"formatEN": "operate VM [name] [operate]"
},
"/xpack/vms/orphans/clean": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "清理虚拟机孤立数据",
"formatEN": "clean VM orphaned data"
},
"/xpack/vms/rename": {
"bodyKeys": [
"name",
"newName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "重命名虚拟机 [name] 为 [newName]",
"formatEN": "rename VM [name] to [newName]"
},
"/xpack/vms/snapshots": {
"bodyKeys": [
"name",
"snapshotName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机 [name] 快照 [snapshotName]",
"formatEN": "create VM [name] snapshot [snapshotName]"
},
"/xpack/vms/snapshots/del": {
"bodyKeys": [
"name",
"snapshotName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除虚拟机 [name] 快照 [snapshotName]",
"formatEN": "delete VM [name] snapshot [snapshotName]"
},
"/xpack/vms/snapshots/recover": {
"bodyKeys": [
"name",
"snapshotName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "恢复虚拟机 [name] 快照 [snapshotName]",
"formatEN": "recover VM [name] snapshot [snapshotName]"
},
"/xpack/vms/storages": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机存储 [name] [type]",
"formatEN": "create VM storage [name] [type]"
},
"/xpack/vms/storages/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_storages",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机存储 [name]",
"formatEN": "delete VM storage [name]"
},
"/xpack/vms/storages/update": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_storages",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "更新虚拟机存储 [name]",
"formatEN": "update VM storage [name]"
},
"/xpack/vms/sync": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "同步虚拟机",
"formatEN": "sync virtual machines"
},
"/xpack/vms/templates": {
"bodyKeys": [
"name",
"templateName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机 [name] 模板 [templateName]",
"formatEN": "create VM [name] template [templateName]"
},
"/xpack/vms/templates/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_templates",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机模板 [name]",
"formatEN": "delete VM template [name]"
},
"/xpack/vms/update": {
"bodyKeys": [
"name"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新虚拟机 [name]",
"formatEN": "update VM [name]"
},
"/xpack/waf/block/export": { "/xpack/waf/block/export": {
"bodyKeys": [], "bodyKeys": [],
"paramKeys": [], "paramKeys": [],
+14 -13
View File
@@ -8,9 +8,9 @@ require (
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
github.com/compose-spec/compose-go/v2 v2.15.0 github.com/compose-spec/compose-go/v2 v2.15.0
github.com/creack/pty v1.1.24 github.com/creack/pty v1.1.24
github.com/docker/cli v29.7.2+incompatible github.com/docker/cli v29.9.0+incompatible
github.com/docker/docker v28.5.2+incompatible github.com/docker/docker v28.5.2+incompatible
github.com/docker/go-connections v0.8.1 github.com/docker/go-connections v0.8.2
github.com/fsnotify/fsnotify v1.10.1 github.com/fsnotify/fsnotify v1.10.1
github.com/gin-gonic/gin v1.12.0 github.com/gin-gonic/gin v1.12.0
github.com/glebarez/sqlite v1.11.0 github.com/glebarez/sqlite v1.11.0
@@ -27,7 +27,7 @@ require (
github.com/jinzhu/copier v0.4.0 github.com/jinzhu/copier v0.4.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/klauspost/compress v1.19.2 github.com/klauspost/compress v1.19.2
github.com/mattn/go-shellwords v1.0.14 github.com/mattn/go-shellwords v1.0.16
github.com/mholt/archiver/v4 v4.0.0-alpha.8 github.com/mholt/archiver/v4 v4.0.0-alpha.8
github.com/miekg/dns v1.1.73 github.com/miekg/dns v1.1.73
github.com/minio/minio-go/v7 v7.3.0 github.com/minio/minio-go/v7 v7.3.0
@@ -40,7 +40,7 @@ require (
github.com/pkg/sftp v1.13.11 github.com/pkg/sftp v1.13.11
github.com/qiniu/go-sdk/v7 v7.27.0 github.com/qiniu/go-sdk/v7 v7.27.0
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.26.7 github.com/shirou/gopsutil/v4 v4.26.9
github.com/sirupsen/logrus v1.10.2 github.com/sirupsen/logrus v1.10.2
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/spf13/afero v1.15.0 github.com/spf13/afero v1.15.0
@@ -51,12 +51,12 @@ require (
github.com/tklauser/go-sysconf v0.4.0 github.com/tklauser/go-sysconf v0.4.0
github.com/tomasen/fcgi_client v0.0.0-20180423082037-2bb3d819fd19 github.com/tomasen/fcgi_client v0.0.0-20180423082037-2bb3d819fd19
github.com/upyun/go-sdk v2.1.0+incompatible github.com/upyun/go-sdk v2.1.0+incompatible
go.mongodb.org/mongo-driver/v2 v2.9.0 go.mongodb.org/mongo-driver/v2 v2.9.2
golang.org/x/crypto v0.56.0 golang.org/x/crypto v0.58.0
golang.org/x/net v0.58.0 golang.org/x/net v0.60.0
golang.org/x/sync v0.23.0 golang.org/x/sync v0.24.0
golang.org/x/sys v0.47.0 golang.org/x/sys v0.49.0
golang.org/x/text v0.41.0 golang.org/x/text v0.43.0
golang.org/x/time v0.16.0 golang.org/x/time v0.16.0
google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478 google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478
gopkg.in/ini.v1 v1.67.3 gopkg.in/ini.v1 v1.67.3
@@ -112,7 +112,7 @@ require (
github.com/docker/go-units v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.10.2 // indirect github.com/ebitengine/purego v0.11.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gin-contrib/sse v1.1.1 // indirect github.com/gin-contrib/sse v1.1.1 // indirect
@@ -177,7 +177,7 @@ require (
github.com/pelletier/go-toml/v2 v2.3.1 // indirect github.com/pelletier/go-toml/v2 v2.3.1 // indirect
github.com/philhofer/fwd v1.2.0 // indirect github.com/philhofer/fwd v1.2.0 // indirect
github.com/pierrec/lz4/v4 v4.1.26 // indirect github.com/pierrec/lz4/v4 v4.1.26 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6 // indirect
github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.1 // indirect github.com/quic-go/quic-go v0.59.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
@@ -203,7 +203,7 @@ require (
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zeebo/xxh3 v1.1.0 // indirect github.com/zeebo/xxh3 v1.1.0 // indirect
go.mongodb.org/mongo-driver v1.17.9 // indirect go.mongodb.org/mongo-driver v1.17.10 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
go.opentelemetry.io/otel v1.45.0 // indirect go.opentelemetry.io/otel v1.45.0 // indirect
@@ -216,6 +216,7 @@ require (
go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect
go4.org v0.0.0-20260112195520-a5071408f32f // indirect go4.org v0.0.0-20260112195520-a5071408f32f // indirect
golang.org/x/arch v0.26.0 // indirect golang.org/x/arch v0.26.0 // indirect
golang.org/x/image v0.45.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect
google.golang.org/grpc v1.83.2 // indirect google.golang.org/grpc v1.83.2 // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
+45
View File
@@ -230,12 +230,18 @@ github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/cli v29.7.2+incompatible h1:dlkwallR8XqfeVnA2ELEhdwvb4lsSwuB4IgsG8Q9cLY= github.com/docker/cli v29.7.2+incompatible h1:dlkwallR8XqfeVnA2ELEhdwvb4lsSwuB4IgsG8Q9cLY=
github.com/docker/cli v29.7.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v29.7.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.8.1+incompatible h1:qYL1bCp6cRw2SB1xmLlIOPyV171dilw9W2Jew38vy9c=
github.com/docker/cli v29.8.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.9.0+incompatible h1:Cw9x50ioSGgGwXJ4eO+hIBry/w3LLbyF8GPAEKz1fnE=
github.com/docker/cli v29.9.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY= github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M= github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M=
github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/go-connections v0.8.2 h1:9d9DEokx5W7qlr0shkFXIxEA7+TTD+llwzcykSlxMNc=
github.com/docker/go-connections v0.8.2/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4= github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
@@ -250,6 +256,8 @@ github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE= github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE=
github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/purego v0.11.1 h1:2zpWRSQNVKN4eKsKO9eM1ILDgWfYMY9GwqRmK6XeQ/0=
github.com/ebitengine/purego v0.11.1/go.mod h1:DCHPP08djqhNSoTfImcnHYQRZmd0qhakvrozqaEYhGQ=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -567,6 +575,8 @@ github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-shellwords v1.0.14 h1:yUKzIgsCnosndOASY6/enly1EAuaXeFSQ7cdyA3OuYg= github.com/mattn/go-shellwords v1.0.14 h1:yUKzIgsCnosndOASY6/enly1EAuaXeFSQ7cdyA3OuYg=
github.com/mattn/go-shellwords v1.0.14/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-shellwords v1.0.14/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-shellwords v1.0.16 h1:RRxAaRzU1YbzOSCj9NJqg2/VIbSWv0dnPoD3EwE8kxI=
github.com/mattn/go-shellwords v1.0.16/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
@@ -703,6 +713,8 @@ github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndr
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6 h1:jL3a8soXdzuTCcRnKhOmtcsVOObdDTFf4O2B403HPRU=
github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
@@ -752,6 +764,10 @@ github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCw
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shirou/gopsutil/v4 v4.26.7 h1:IXzpHz/dkMRYAhKkOXr1HB6SuzWU3eoyyeWe7g3bNZc= github.com/shirou/gopsutil/v4 v4.26.7 h1:IXzpHz/dkMRYAhKkOXr1HB6SuzWU3eoyyeWe7g3bNZc=
github.com/shirou/gopsutil/v4 v4.26.7/go.mod h1:5O9FjBiXoTDFatIWjZZosqj4pV0DRtLx598xGbBehzM= github.com/shirou/gopsutil/v4 v4.26.7/go.mod h1:5O9FjBiXoTDFatIWjZZosqj4pV0DRtLx598xGbBehzM=
github.com/shirou/gopsutil/v4 v4.26.8 h1:YQMTF/1J50B5+Y0vlo1eDRf5DoR7Gk69hY+8wjYkQeo=
github.com/shirou/gopsutil/v4 v4.26.8/go.mod h1:5O9FjBiXoTDFatIWjZZosqj4pV0DRtLx598xGbBehzM=
github.com/shirou/gopsutil/v4 v4.26.9 h1:CaBo/hBFqvlJoLvAQEKdHdf5GD97/MN2ACfyv63+2Ig=
github.com/shirou/gopsutil/v4 v4.26.9/go.mod h1:nKH+8wX2zxr/mDbsR+AAy6Qd2z4ZOQ7wfQaEp+yrYmg=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
@@ -878,8 +894,14 @@ go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lL
go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo= go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=
go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
go.mongodb.org/mongo-driver v1.17.10 h1:kdAgQvu8TROXZpSkJQd5wzfaNCCrMbpZyKFtQ6qkPCE=
go.mongodb.org/mongo-driver v1.17.10/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
go.mongodb.org/mongo-driver/v2 v2.9.0 h1:e2mQdOmbkiYz+dj3faM7lVDwl7WdnRD+g5VicafMhL0= go.mongodb.org/mongo-driver/v2 v2.9.0 h1:e2mQdOmbkiYz+dj3faM7lVDwl7WdnRD+g5VicafMhL0=
go.mongodb.org/mongo-driver/v2 v2.9.0/go.mod h1:SHKN0IWkKmEVGHLjXnni6s4wPKX4v86FTgOeJJFuXcA= go.mongodb.org/mongo-driver/v2 v2.9.0/go.mod h1:SHKN0IWkKmEVGHLjXnni6s4wPKX4v86FTgOeJJFuXcA=
go.mongodb.org/mongo-driver/v2 v2.9.1 h1:jewiFs2m1/VOQp8qhFshX6hWZ+EAXDhZHXExAUMcOgQ=
go.mongodb.org/mongo-driver/v2 v2.9.1/go.mod h1:SHKN0IWkKmEVGHLjXnni6s4wPKX4v86FTgOeJJFuXcA=
go.mongodb.org/mongo-driver/v2 v2.9.2 h1:OCAdjI37pC/SVxucz/9CM8zD4jCdegtsY5QcKzYFCgE=
go.mongodb.org/mongo-driver/v2 v2.9.2/go.mod h1:SHKN0IWkKmEVGHLjXnni6s4wPKX4v86FTgOeJJFuXcA=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -954,6 +976,10 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y= golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I= golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -970,6 +996,8 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0=
golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -997,6 +1025,7 @@ golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1053,6 +1082,8 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1079,6 +1110,8 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1156,6 +1189,10 @@ golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -1171,6 +1208,8 @@ golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1189,6 +1228,10 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1252,6 +1295,8 @@ golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+11 -2
View File
@@ -140,6 +140,9 @@ ErrComposeProjectNameEmpty: 'Unable to derive a project name from Docker Compose
ErrComposeNameInvalid: 'Invalid Compose project name: it must start with a lowercase letter or number, contain only lowercase letters, numbers, hyphens, and underscores, and be 1-256 characters long.' ErrComposeNameInvalid: 'Invalid Compose project name: it must start with a lowercase letter or number, contain only lowercase letters, numbers, hyphens, and underscores, and be 1-256 characters long.'
ErrAppVersionUnavailable: 'This application version has been removed from the remote service. Please select another version and try again.' ErrAppVersionUnavailable: 'This application version has been removed from the remote service. Please select another version and try again.'
ErrVllmGB10ProOnly: 'This vLLM version is available only in 1Panel Professional Edition.' ErrVllmGB10ProOnly: 'This vLLM version is available only in 1Panel Professional Edition.'
ErrAIGatewayDatabaseTypeMismatch: 'AI Gateway can only upgrade within the same database type (SQLite or PostgreSQL).'
ErrAIGatewayRecoverDatabaseTypeMismatch: 'The AI Gateway backup and target application must use the same database type (SQLite or PostgreSQL).'
ErrHermesUpgradeVersion: 'Hermes Agent can only upgrade to a newer recognized release allowed by its cross-version upgrade policy. Restore an older version from a backup instead.'
ErrAppWarn: 'App status abnormal; check logs' ErrAppWarn: 'App status abnormal; check logs'
ErrAppParamKey: 'Invalid app parameter: {{ .name }}' ErrAppParamKey: 'Invalid app parameter: {{ .name }}'
ErrAppUpgrade: 'App upgrade failed: {{ .name }} {{ .err }}' ErrAppUpgrade: 'App upgrade failed: {{ .name }} {{ .err }}'
@@ -300,6 +303,9 @@ BuildCache: 'Build Cache'
Volume: 'Volume' Volume: 'Volume'
Network: "Network" Network: "Network"
PruneStart: 'Cleaning in progress' PruneStart: 'Cleaning in progress'
NetworkCleanupConnected: "Skipped network [{{ .name }}] ({{ .id }}): containers connected or network in use; not deleted"
NetworkCleanupSummary: "Network cleanup finished: deleted {{ .deleted }}, skipped {{ .skipped }}, failed {{ .failed }}"
NetworkCleanupPartialFailure: "Some networks could not be cleaned; see the task log"
ContainerBackupPrepare: 'Prepare container backup workspace' ContainerBackupPrepare: 'Prepare container backup workspace'
ContainerBackupStop: 'Stop container before backup' ContainerBackupStop: 'Stop container before backup'
ContainerBackupInspect: 'Backup container inspect and network' ContainerBackupInspect: 'Backup container inspect and network'
@@ -628,14 +634,15 @@ AppAttributesNotMatch: 'Application type or name does not match'
# alert # alert
ErrAlertMethodNotSupported: 'This version does not support this alert method' ErrAlertMethodNotSupported: 'This version does not support this alert method'
ErrAlertSync: 'Failed to sync alert message' ErrAlertSync: 'Failed to sync alert message'
ErrWebhookTestFailed: 'Please check the Webhook configuration. See the system logs for details.'
# task - runtime # task - runtime
ErrInstallExtension: 'Another extension install task is running' ErrInstallExtension: 'Another extension install task is running'
# alert mail template # alert mail template
PanelAlertTitle: "Panel Alert Notification" PanelAlertTitle: "Panel Alert Notification"
TestAlertTitle: "Test Email - Verify Email Connectivity" TestAlertTitle: "Panel Test Notification"
TestAlert: "This is a test email to verify that your email sending configuration is correct." TestAlertMessage: "This is a test notification to verify the notification channel configuration and connectivity."
LicenseExpirationAlert: "Panel {{ .node }}{{ .ip }}: license expires in {{ .day }} days. Log in to view details." LicenseExpirationAlert: "Panel {{ .node }}{{ .ip }}: license expires in {{ .day }} days. Log in to view details."
CronJobFailedAlert: "Panel {{ .node }}{{ .ip }}: scheduled task {{ .name }} failed. Log in to view details." CronJobFailedAlert: "Panel {{ .node }}{{ .ip }}: scheduled task {{ .name }} failed. Log in to view details."
ClamAlert: "Panel {{ .node }}{{ .ip }}: virus scan found {{ .num }} infected files. Log in to view details." ClamAlert: "Panel {{ .node }}{{ .ip }}: virus scan found {{ .num }} infected files. Log in to view details."
@@ -718,6 +725,8 @@ ErrDockerIPv4ForwardingDisabled: "IPv4 forwarding is disabled. Set net.ipv4.ip_f
ErrFirewallRuleSavedApplyFailed: "The new rule configuration was saved, but applying it to the firewall failed. Retry by synchronizing: {{ .detail }}" ErrFirewallRuleSavedApplyFailed: "The new rule configuration was saved, but applying it to the firewall failed. Retry by synchronizing: {{ .detail }}"
ErrFirewallRuleConflict: "A UFW rule with the same match conditions and a different action already exists. Edit the existing rule instead." ErrFirewallRuleConflict: "A UFW rule with the same match conditions and a different action already exists. Edit the existing rule instead."
ErrForwardInterfaceNotFound: "Network interface {{ .name }} specified by the forwarding rule does not exist. Check the interface name and retry." ErrForwardInterfaceNotFound: "Network interface {{ .name }} specified by the forwarding rule does not exist. Check the interface name and retry."
ErrIPv6RARisk: "IPv6 forwarding is blocked to prevent possible network loss. See the documentation and system logs for details."
ErrIPv6RACheckFailed: "IPv6 forwarding is blocked because the network configuration could not be checked. See the system logs for details."
ErrSSHPublicKey: "Enter a valid SSH public key" ErrSSHPublicKey: "Enter a valid SSH public key"
ErrSSHPrivateKey: "Invalid SSH private key or incorrect passphrase; encrypted keys require the original passphrase" ErrSSHPrivateKey: "Invalid SSH private key or incorrect passphrase; encrypted keys require the original passphrase"
+11 -2
View File
@@ -140,6 +140,9 @@ ErrComposeProjectNameEmpty: 'No se pudo derivar un nombre de proyecto desde la c
ErrComposeNameInvalid: 'Nombre de proyecto Compose no válido: debe comenzar con una letra minúscula o un número, contener solo letras minúsculas, números, guiones y guiones bajos, y tener entre 1 y 256 caracteres.' ErrComposeNameInvalid: 'Nombre de proyecto Compose no válido: debe comenzar con una letra minúscula o un número, contener solo letras minúsculas, números, guiones y guiones bajos, y tener entre 1 y 256 caracteres.'
ErrAppVersionUnavailable: 'Esta versión de la aplicación se ha eliminado del servicio remoto. Seleccione otra versión e inténtelo de nuevo.' ErrAppVersionUnavailable: 'Esta versión de la aplicación se ha eliminado del servicio remoto. Seleccione otra versión e inténtelo de nuevo.'
ErrVllmGB10ProOnly: 'Esta versión de vLLM solo está disponible en 1Panel Professional Edition.' ErrVllmGB10ProOnly: 'Esta versión de vLLM solo está disponible en 1Panel Professional Edition.'
ErrAIGatewayDatabaseTypeMismatch: 'AI Gateway solo puede actualizarse dentro del mismo tipo de base de datos (SQLite o PostgreSQL).'
ErrAIGatewayRecoverDatabaseTypeMismatch: 'La copia de seguridad de AI Gateway y la aplicación de destino deben usar el mismo tipo de base de datos (SQLite o PostgreSQL).'
ErrHermesUpgradeVersion: 'Hermes Agent solo puede actualizarse a una versión reconocida más reciente permitida por la configuración de actualización entre versiones principales. Para volver a una versión anterior, restaure una copia de seguridad.'
ErrAppWarn: 'Estado anómalo, revise el log' ErrAppWarn: 'Estado anómalo, revise el log'
ErrAppParamKey: 'El campo de parámetro {{ .name }} es anómalo' ErrAppParamKey: 'El campo de parámetro {{ .name }} es anómalo'
ErrAppUpgrade: 'La actualización de la aplicación {{ .name }} falló {{ .err }}' ErrAppUpgrade: 'La actualización de la aplicación {{ .name }} falló {{ .err }}'
@@ -300,6 +303,9 @@ BuildCache: 'Caché de compilación'
Volume: 'Volumen de almacenamiento' Volume: 'Volumen de almacenamiento'
Network: 'Red' Network: 'Red'
PruneStart: 'Limpiando en progreso espere...' PruneStart: 'Limpiando en progreso espere...'
NetworkCleanupConnected: "Se omitió la red [{{ .name }}] ({{ .id }}): hay contenedores conectados o la red está en uso; no se eliminó"
NetworkCleanupSummary: "Limpieza de redes finalizada: eliminadas {{ .deleted }}, omitidas {{ .skipped }}, fallidas {{ .failed }}"
NetworkCleanupPartialFailure: "No se pudieron limpiar algunas redes; consulte el registro de la tarea"
ContainerBackupPrepare: 'Preparar espacio de respaldo del contenedor' ContainerBackupPrepare: 'Preparar espacio de respaldo del contenedor'
ContainerBackupStop: 'Detener contenedor antes del respaldo' ContainerBackupStop: 'Detener contenedor antes del respaldo'
ContainerBackupInspect: 'Respaldar inspect y red del contenedor' ContainerBackupInspect: 'Respaldar inspect y red del contenedor'
@@ -628,14 +634,15 @@ AppAttributesNotMatch: 'El tipo o nombre de la aplicación no coincide'
# alerta # alerta
ErrAlertMethodNotSupported: 'Esta versión no admite este método de alerta' ErrAlertMethodNotSupported: 'Esta versión no admite este método de alerta'
ErrAlertSync: 'Error en la sincronización de la alerta, revise e intente de nuevo' ErrAlertSync: 'Error en la sincronización de la alerta, revise e intente de nuevo'
ErrWebhookTestFailed: 'Revisa la configuración del Webhook. Consulta los registros del sistema para más detalles.'
# tarea - entorno de ejecución # tarea - entorno de ejecución
ErrInstallExtension: 'Ya hay una tarea de instalación en curso, espere a que termine' ErrInstallExtension: 'Ya hay una tarea de instalación en curso, espere a que termine'
# plantilla de correo de alerta # plantilla de correo de alerta
PanelAlertTitle: 'Notificación de alerta del panel' PanelAlertTitle: 'Notificación de alerta del panel'
TestAlertTitle: 'Correo de prueba - Verificar conectividad de correo' TestAlertTitle: 'Notificación de prueba del panel'
TestAlert: 'Este es un correo de prueba para verificar que la configuración de envío de correos es correcta.' TestAlertMessage: 'Esta es una notificación de prueba para verificar la configuración y la conectividad del canal de notificaciones.'
LicenseExpirationAlert: 'Su Panel {{ .node }}{{ .ip }}, la licencia expirará en {{ .day }} días. Inicie sesión en el panel para ver los detalles.' LicenseExpirationAlert: 'Su Panel {{ .node }}{{ .ip }}, la licencia expirará en {{ .day }} días. Inicie sesión en el panel para ver los detalles.'
CronJobFailedAlert: 'Su Panel {{ .node }}{{ .ip }}, la ejecución de la tarea programada {{ .name }} falló. Inicie sesión en el panel para ver los detalles.' CronJobFailedAlert: 'Su Panel {{ .node }}{{ .ip }}, la ejecución de la tarea programada {{ .name }} falló. Inicie sesión en el panel para ver los detalles.'
ClamAlert: 'Su Panel {{ .node }}{{ .ip }}, la tarea de escaneo antivirus detectó {{ .num }} archivos infectados. Inicie sesión en el panel para ver los detalles.' ClamAlert: 'Su Panel {{ .node }}{{ .ip }}, la tarea de escaneo antivirus detectó {{ .num }} archivos infectados. Inicie sesión en el panel para ver los detalles.'
@@ -718,6 +725,8 @@ ErrDockerIPv4ForwardingDisabled: "El reenvío IPv4 está desactivado. Configure
ErrFirewallRuleSavedApplyFailed: "Se guardó la configuración de la nueva regla, pero no se pudo aplicar al cortafuegos. Vuelva a intentarlo mediante la sincronización: {{ .detail }}" ErrFirewallRuleSavedApplyFailed: "Se guardó la configuración de la nueva regla, pero no se pudo aplicar al cortafuegos. Vuelva a intentarlo mediante la sincronización: {{ .detail }}"
ErrFirewallRuleConflict: "Ya existe una regla UFW con las mismas condiciones y una acción diferente. Edite la regla existente." ErrFirewallRuleConflict: "Ya existe una regla UFW con las mismas condiciones y una acción diferente. Edite la regla existente."
ErrForwardInterfaceNotFound: "La interfaz de red {{ .name }} indicada en la regla de reenvío no existe. Compruebe el nombre e inténtelo de nuevo." ErrForwardInterfaceNotFound: "La interfaz de red {{ .name }} indicada en la regla de reenvío no existe. Compruebe el nombre e inténtelo de nuevo."
ErrIPv6RARisk: "El reenvío IPv6 está bloqueado para evitar una posible pérdida de conectividad. Consulte la documentación y los registros del sistema."
ErrIPv6RACheckFailed: "El reenvío IPv6 está bloqueado porque no se pudo comprobar la configuración de red. Consulte los registros del sistema."
ErrSSHPublicKey: "Introduce una clave pública SSH válida" ErrSSHPublicKey: "Introduce una clave pública SSH válida"
ErrSSHPrivateKey: "Clave privada SSH no válida o contraseña incorrecta; las claves cifradas requieren la contraseña original" ErrSSHPrivateKey: "Clave privada SSH no válida o contraseña incorrecta; las claves cifradas requieren la contraseña original"
+11 -2
View File
@@ -140,6 +140,9 @@ ErrComposeProjectNameEmpty: 'نام پروژه از پیکربندی Docker Comp
ErrComposeNameInvalid: 'نام پروژه Compose نامعتبر است: باید با حرف کوچک یا عدد شروع شود، فقط شامل حروف کوچک، اعداد، خط تیره و زیرخط باشد و ۱ تا ۲۵۶ نویسه داشته باشد.' ErrComposeNameInvalid: 'نام پروژه Compose نامعتبر است: باید با حرف کوچک یا عدد شروع شود، فقط شامل حروف کوچک، اعداد، خط تیره و زیرخط باشد و ۱ تا ۲۵۶ نویسه داشته باشد.'
ErrAppVersionUnavailable: 'این نسخه برنامه از سرویس راه دور حذف شده است. لطفاً نسخه دیگری را انتخاب کرده و دوباره تلاش کنید.' ErrAppVersionUnavailable: 'این نسخه برنامه از سرویس راه دور حذف شده است. لطفاً نسخه دیگری را انتخاب کرده و دوباره تلاش کنید.'
ErrVllmGB10ProOnly: 'این نسخه vLLM فقط در نسخه حرفه‌ای 1Panel در دسترس است.' ErrVllmGB10ProOnly: 'این نسخه vLLM فقط در نسخه حرفه‌ای 1Panel در دسترس است.'
ErrAIGatewayDatabaseTypeMismatch: 'AI Gateway فقط در همان نوع پایگاه داده (SQLite یا PostgreSQL) قابل ارتقا است.'
ErrAIGatewayRecoverDatabaseTypeMismatch: 'نسخه پشتیبان AI Gateway و برنامه مقصد باید از یک نوع پایگاه داده (SQLite یا PostgreSQL) استفاده کنند.'
ErrHermesUpgradeVersion: 'Hermes Agent فقط می‌تواند به نسخه جدیدتر و شناخته‌شده‌ای ارتقا یابد که تنظیمات ارتقا بین نسخه‌های اصلی اجازه می‌دهد. برای بازگشت به نسخه قدیمی، نسخه پشتیبان را بازیابی کنید.'
ErrAppWarn: 'وضعیت برنامه غیرعادی است؛ لاگ‌ها را بررسی کنید' ErrAppWarn: 'وضعیت برنامه غیرعادی است؛ لاگ‌ها را بررسی کنید'
ErrAppParamKey: 'پارامتر برنامه نامعتبر است: {{ .name }}' ErrAppParamKey: 'پارامتر برنامه نامعتبر است: {{ .name }}'
ErrAppUpgrade: 'ارتقاء برنامه ناموفق بود: {{ .name }} {{ .err }}' ErrAppUpgrade: 'ارتقاء برنامه ناموفق بود: {{ .name }} {{ .err }}'
@@ -300,6 +303,9 @@ BuildCache: 'کش ساخت'
Volume: 'حجم' Volume: 'حجم'
Network: "شبکه" Network: "شبکه"
PruneStart: 'پاک‌سازی در حال انجام است' PruneStart: 'پاک‌سازی در حال انجام است'
NetworkCleanupConnected: "از شبکه [{{ .name }}] ({{ .id }}) صرف‌نظر شد: کانتینر متصل دارد یا در حال استفاده است؛ حذف نشد"
NetworkCleanupSummary: "پاک‌سازی شبکه‌ها پایان یافت: حذف‌شده {{ .deleted }}، ردشده {{ .skipped }}، ناموفق {{ .failed }}"
NetworkCleanupPartialFailure: "پاک‌سازی برخی شبکه‌ها ناموفق بود؛ گزارش وظیفه را بررسی کنید"
ContainerBackupPrepare: 'آماده‌سازی فضای کاری پشتیبان کانتینر' ContainerBackupPrepare: 'آماده‌سازی فضای کاری پشتیبان کانتینر'
ContainerBackupStop: 'توقف کانتینر قبل از پشتیبان‌گیری' ContainerBackupStop: 'توقف کانتینر قبل از پشتیبان‌گیری'
ContainerBackupInspect: 'بررسی و شبکه کانتینر پشتیبان' ContainerBackupInspect: 'بررسی و شبکه کانتینر پشتیبان'
@@ -628,14 +634,15 @@ AppAttributesNotMatch: 'نوع یا نام برنامه مطابقت ندارد'
# هشدار # هشدار
ErrAlertMethodNotSupported: 'این نسخه از این روش هشدار پشتیبانی نمی‌کند' ErrAlertMethodNotSupported: 'این نسخه از این روش هشدار پشتیبانی نمی‌کند'
ErrAlertSync: 'همگام‌سازی پیام هشدار ناموفق بود' ErrAlertSync: 'همگام‌سازی پیام هشدار ناموفق بود'
ErrWebhookTestFailed: 'لطفاً پیکربندی وب‌هوک را بررسی کنید. برای جزئیات، گزارش‌های سیستم را ببینید.'
# وظیفه - محیط اجرا # وظیفه - محیط اجرا
ErrInstallExtension: 'وظیفه نصب پسوند دیگری در حال اجراست' ErrInstallExtension: 'وظیفه نصب پسوند دیگری در حال اجراست'
# قالب ایمیل هشدار # قالب ایمیل هشدار
PanelAlertTitle: "اعلان هشدار پنل" PanelAlertTitle: "اعلان هشدار پنل"
TestAlertTitle: "ایمیل آزمایشی - تأیید اتصال ایمیل" TestAlertTitle: "اعلان آزمایشی پنل"
TestAlert: "این یک ایمیل آزمایشی برای تأیید صحت پیکربندی ارسال ایمیل شما است." TestAlertMessage: "این یک اعلان آزمایشی برای بررسی پیکربندی و اتصال کانال اعلان است."
LicenseExpirationAlert: "پنل {{ .node }}{{ .ip }}: مجوز در {{ .day }} روز منقضی می‌شود. برای مشاهده جزئیات وارد شوید." LicenseExpirationAlert: "پنل {{ .node }}{{ .ip }}: مجوز در {{ .day }} روز منقضی می‌شود. برای مشاهده جزئیات وارد شوید."
CronJobFailedAlert: "پنل {{ .node }}{{ .ip }}: وظیفه زمان‌بندی {{ .name }} ناموفق بود. برای مشاهده جزئیات وارد شوید." CronJobFailedAlert: "پنل {{ .node }}{{ .ip }}: وظیفه زمان‌بندی {{ .name }} ناموفق بود. برای مشاهده جزئیات وارد شوید."
ClamAlert: "پنل {{ .node }}{{ .ip }}: اسکن ویروس {{ .num }} فایل آلوده پیدا کرد. برای مشاهده جزئیات وارد شوید." ClamAlert: "پنل {{ .node }}{{ .ip }}: اسکن ویروس {{ .num }} فایل آلوده پیدا کرد. برای مشاهده جزئیات وارد شوید."
@@ -719,6 +726,8 @@ ErrDockerIPv4ForwardingDisabled: "ارسال IPv4 غیرفعال است. پیش
ErrFirewallRuleSavedApplyFailed: "پیکربندی قانون جدید ذخیره شد، اما اعمال آن در دیوار آتش ناموفق بود. با همگام‌سازی دوباره تلاش کنید: {{ .detail }}" ErrFirewallRuleSavedApplyFailed: "پیکربندی قانون جدید ذخیره شد، اما اعمال آن در دیوار آتش ناموفق بود. با همگام‌سازی دوباره تلاش کنید: {{ .detail }}"
ErrFirewallRuleConflict: "یک قانون UFW با شرایط تطبیق یکسان و عملکرد متفاوت از قبل وجود دارد. قانون موجود را ویرایش کنید." ErrFirewallRuleConflict: "یک قانون UFW با شرایط تطبیق یکسان و عملکرد متفاوت از قبل وجود دارد. قانون موجود را ویرایش کنید."
ErrForwardInterfaceNotFound: "رابط شبکه {{ .name }} مشخص‌شده در قانون انتقال وجود ندارد. نام رابط را بررسی کرده و دوباره تلاش کنید." ErrForwardInterfaceNotFound: "رابط شبکه {{ .name }} مشخص‌شده در قانون انتقال وجود ندارد. نام رابط را بررسی کرده و دوباره تلاش کنید."
ErrIPv6RARisk: "فعال‌سازی هدایت IPv6 به دلیل خطر قطع شبکه مسدود شد. مستندات و گزارش‌های سیستم را بررسی کنید."
ErrIPv6RACheckFailed: "فعال‌سازی هدایت IPv6 به دلیل عدم امکان بررسی پیکربندی شبکه مسدود شد. جزئیات در گزارش‌های سیستم موجود است."
ErrSSHPublicKey: "یک کلید عمومی SSH معتبر وارد کنید" ErrSSHPublicKey: "یک کلید عمومی SSH معتبر وارد کنید"
ErrSSHPrivateKey: "کلید خصوصی SSH نامعتبر یا عبارت عبور نادرست است؛ کلیدهای رمزگذاری‌شده به عبارت عبور اصلی نیاز دارند" ErrSSHPrivateKey: "کلید خصوصی SSH نامعتبر یا عبارت عبور نادرست است؛ کلیدهای رمزگذاری‌شده به عبارت عبور اصلی نیاز دارند"
+11 -2
View File
@@ -140,6 +140,9 @@ ErrComposeProjectNameEmpty: 'Docker Compose 設定またはファイルの親デ
ErrComposeNameInvalid: 'Compose プロジェクト名の形式が正しくありません。小文字または数字で始まり、小文字、数字、ハイフン、アンダースコアのみを使用した 1~256 文字にしてください。' ErrComposeNameInvalid: 'Compose プロジェクト名の形式が正しくありません。小文字または数字で始まり、小文字、数字、ハイフン、アンダースコアのみを使用した 1~256 文字にしてください。'
ErrAppVersionUnavailable: '現在のアプリケーションバージョンはリモートサービスから削除されています。別のバージョンを選択して再試行してください。' ErrAppVersionUnavailable: '現在のアプリケーションバージョンはリモートサービスから削除されています。別のバージョンを選択して再試行してください。'
ErrVllmGB10ProOnly: 'この vLLM バージョンは 1Panel プロフェッショナル版でのみ利用できます。' ErrVllmGB10ProOnly: 'この vLLM バージョンは 1Panel プロフェッショナル版でのみ利用できます。'
ErrAIGatewayDatabaseTypeMismatch: 'AI Gateway は同じデータベース種別(SQLite または PostgreSQL)間でのみアップグレードできます。'
ErrAIGatewayRecoverDatabaseTypeMismatch: 'AI Gateway のバックアップと復元先アプリのデータベース種別(SQLite または PostgreSQL)は一致している必要があります。'
ErrHermesUpgradeVersion: 'Hermes Agent は、認識可能でメジャーバージョン間のアップグレード設定に適合する新しいリリースにのみアップグレードできます。以前のバージョンに戻すには、バックアップから復元してください。'
ErrAppWarn: '異常な状態です。ログを確認してください' ErrAppWarn: '異常な状態です。ログを確認してください'
ErrAppParamKey: 'パラメータ {{ .name }} フィールドが異常です' ErrAppParamKey: 'パラメータ {{ .name }} フィールドが異常です'
ErrAppUpgrade: 'アプリケーション {{ .name }} のアップグレードに失敗しました {{ .err }}' ErrAppUpgrade: 'アプリケーション {{ .name }} のアップグレードに失敗しました {{ .err }}'
@@ -300,6 +303,9 @@ BuildCache: 'ビルドキャッシュ'
Volume: 'ストレージボリューム' Volume: 'ストレージボリューム'
Network: 'ネットワーク' Network: 'ネットワーク'
PruneStart: 'クリーンアップ中、しばらくお待ちください...' PruneStart: 'クリーンアップ中、しばらくお待ちください...'
NetworkCleanupConnected: "ネットワーク [{{ .name }}] ({{ .id }}) をスキップしました:コンテナが接続されているか使用中のため、削除しませんでした"
NetworkCleanupSummary: "ネットワークのクリーンアップが完了しました:削除 {{ .deleted }} 件、スキップ {{ .skipped }} 件、失敗 {{ .failed }} 件"
NetworkCleanupPartialFailure: "一部のネットワークをクリーンアップできませんでした。タスクログを確認してください"
ContainerBackupPrepare: 'コンテナバックアップ作業領域を準備' ContainerBackupPrepare: 'コンテナバックアップ作業領域を準備'
ContainerBackupStop: 'バックアップ前にコンテナを停止' ContainerBackupStop: 'バックアップ前にコンテナを停止'
ContainerBackupInspect: 'コンテナ inspect とネットワーク情報をバックアップ' ContainerBackupInspect: 'コンテナ inspect とネットワーク情報をバックアップ'
@@ -628,14 +634,15 @@ AppAttributesNotMatch: 'アプリケーションの種類または名前が一
# アラート # アラート
ErrAlertMethodNotSupported: 'このバージョンではこの通知方法は利用できません' ErrAlertMethodNotSupported: 'このバージョンではこの通知方法は利用できません'
ErrAlertSync: 'アラーム情報の同期エラーです。確認してもう一度お試しください。' ErrAlertSync: 'アラーム情報の同期エラーです。確認してもう一度お試しください。'
ErrWebhookTestFailed: 'Webhook の設定を確認してください。詳細はシステムログを参照してください。'
# タスク - ランタイム # タスク - ランタイム
ErrInstallExtension: 'インストールタスクが進行中です、タスクが終了するのを待ってください' ErrInstallExtension: 'インストールタスクが進行中です、タスクが終了するのを待ってください'
# アラートメールテンプレート # アラートメールテンプレート
PanelAlertTitle: 'パネルアラート通知' PanelAlertTitle: 'パネルアラート通知'
TestAlertTitle: 'テストメール - メール接続の確認' TestAlertTitle: 'パネルのテスト通知'
TestAlert: 'これはテストメールです。メール送信設定が正しく構成されているかを確認します。' TestAlertMessage: 'これは通知チャネルの設定と接続性を確認するためのテスト通知です。'
LicenseExpirationAlert: 'あなたの {{ .node }}{{ .ip }} パネル、ライセンスは {{ .day }} 日後に期限切れになります。詳細はパネルにログインして' LicenseExpirationAlert: 'あなたの {{ .node }}{{ .ip }} パネル、ライセンスは {{ .day }} 日後に期限切れになります。詳細はパネルにログインして'
CronJobFailedAlert: 'あなたの {{ .node }}{{ .ip }} パネル、スケジュールタスク {{ .name }} の実行に失敗しました。詳細はパネルにログインして' CronJobFailedAlert: 'あなたの {{ .node }}{{ .ip }} パネル、スケジュールタスク {{ .name }} の実行に失敗しました。詳細はパネルにログインして'
ClamAlert: 'あなたの {{ .node }}{{ .ip }} パネル、ウイルススキャンタスクで {{ .num }} 個の感染ファイルを検出しました。詳細はパネルにログインして' ClamAlert: 'あなたの {{ .node }}{{ .ip }} パネル、ウイルススキャンタスクで {{ .num }} 個の感染ファイルを検出しました。詳細はパネルにログインして'
@@ -718,6 +725,8 @@ ErrDockerIPv4ForwardingDisabled: "IPv4 転送が無効です。Docker のファ
ErrFirewallRuleSavedApplyFailed: "新しいルール設定は保存されましたが、ファイアウォールへの適用に失敗しました。同期で再試行してください:{{ .detail }}" ErrFirewallRuleSavedApplyFailed: "新しいルール設定は保存されましたが、ファイアウォールへの適用に失敗しました。同期で再試行してください:{{ .detail }}"
ErrFirewallRuleConflict: "UFW に同じ一致条件でアクションが異なるルールが既に存在します。既存のルールを編集してください。" ErrFirewallRuleConflict: "UFW に同じ一致条件でアクションが異なるルールが既に存在します。既存のルールを編集してください。"
ErrForwardInterfaceNotFound: "転送ルールで指定されたネットワークインターフェース {{ .name }} は存在しません。名前を確認して再試行してください。" ErrForwardInterfaceNotFound: "転送ルールで指定されたネットワークインターフェース {{ .name }} は存在しません。名前を確認して再試行してください。"
ErrIPv6RARisk: "ネットワーク切断のおそれがあるため、IPv6 転送の有効化をブロックしました。ドキュメントとシステムログをご確認ください。"
ErrIPv6RACheckFailed: "ネットワーク設定を確認できないため、IPv6 転送の有効化をブロックしました。詳細はシステムログをご確認ください。"
ErrSSHPublicKey: "有効な SSH 公開鍵を入力してください" ErrSSHPublicKey: "有効な SSH 公開鍵を入力してください"
ErrSSHPrivateKey: "SSH 秘密鍵の形式が無効か、パスフレーズが間違っています。暗号化された鍵には元のパスフレーズが必要です" ErrSSHPrivateKey: "SSH 秘密鍵の形式が無効か、パスフレーズが間違っています。暗号化された鍵には元のパスフレーズが必要です"
+11 -2
View File
@@ -140,6 +140,9 @@ ErrComposeProjectNameEmpty: 'Docker Compose 구성 또는 파일의 상위 디
ErrComposeNameInvalid: 'Compose 프로젝트 이름 형식이 잘못되었습니다. 소문자 또는 숫자로 시작하고 소문자, 숫자, 하이픈, 밑줄만 사용하여 1~256자로 입력하세요.' ErrComposeNameInvalid: 'Compose 프로젝트 이름 형식이 잘못되었습니다. 소문자 또는 숫자로 시작하고 소문자, 숫자, 하이픈, 밑줄만 사용하여 1~256자로 입력하세요.'
ErrAppVersionUnavailable: '현재 애플리케이션 버전이 원격 서비스에서 제거되었습니다. 다른 버전을 선택한 후 다시 시도해 주세요.' ErrAppVersionUnavailable: '현재 애플리케이션 버전이 원격 서비스에서 제거되었습니다. 다른 버전을 선택한 후 다시 시도해 주세요.'
ErrVllmGB10ProOnly: '이 vLLM 버전은 1Panel 프로페셔널 에디션에서만 사용할 수 있습니다.' ErrVllmGB10ProOnly: '이 vLLM 버전은 1Panel 프로페셔널 에디션에서만 사용할 수 있습니다.'
ErrAIGatewayDatabaseTypeMismatch: 'AI Gateway는 동일한 데이터베이스 유형(SQLite 또는 PostgreSQL) 내에서만 업그레이드할 수 있습니다.'
ErrAIGatewayRecoverDatabaseTypeMismatch: 'AI Gateway 백업과 대상 애플리케이션의 데이터베이스 유형(SQLite 또는 PostgreSQL)이 같아야 합니다.'
ErrHermesUpgradeVersion: 'Hermes Agent는 인식 가능하고 주 버전 간 업그레이드 설정을 충족하는 최신 릴리스로만 업그레이드할 수 있습니다. 이전 버전으로 되돌리려면 백업을 복원하세요.'
ErrAppWarn: '비정상적인 상태입니다. 로그를 확인해 주세요.' ErrAppWarn: '비정상적인 상태입니다. 로그를 확인해 주세요.'
ErrAppParamKey: '매개변수 {{ .name }} 필드가 비정상입니다.' ErrAppParamKey: '매개변수 {{ .name }} 필드가 비정상입니다.'
ErrAppUpgrade: '애플리케이션 {{ .name }} 업그레이드에 실패했습니다 {{ .err }}' ErrAppUpgrade: '애플리케이션 {{ .name }} 업그레이드에 실패했습니다 {{ .err }}'
@@ -300,6 +303,9 @@ BuildCache: '빌드 캐시'
Volume: '스토리지 볼륨' Volume: '스토리지 볼륨'
Network: '네트워크' Network: '네트워크'
PruneStart: '정리 중입니다. 잠시만 기다려주세요...' PruneStart: '정리 중입니다. 잠시만 기다려주세요...'
NetworkCleanupConnected: "네트워크 [{{ .name }}] ({{ .id }}) 건너뜀: 연결된 컨테이너가 있거나 사용 중이므로 삭제하지 않음"
NetworkCleanupSummary: "네트워크 정리 완료: 삭제 {{ .deleted }}개, 건너뜀 {{ .skipped }}개, 실패 {{ .failed }}개"
NetworkCleanupPartialFailure: "일부 네트워크를 정리하지 못했습니다. 작업 로그를 확인하세요"
ContainerBackupPrepare: '컨테이너 백업 작업 공간 준비' ContainerBackupPrepare: '컨테이너 백업 작업 공간 준비'
ContainerBackupStop: '백업 전에 컨테이너 중지' ContainerBackupStop: '백업 전에 컨테이너 중지'
ContainerBackupInspect: '컨테이너 inspect 및 네트워크 정보 백업' ContainerBackupInspect: '컨테이너 inspect 및 네트워크 정보 백업'
@@ -628,14 +634,15 @@ AppAttributesNotMatch: '애플리케이션 유형 또는 이름이 일치하지
# 알림 # 알림
ErrAlertMethodNotSupported: '현재 버전에서는 이 알림 방식을 지원하지 않습니다' ErrAlertMethodNotSupported: '현재 버전에서는 이 알림 방식을 지원하지 않습니다'
ErrAlertSync: '알람 정보 동기화 오류입니다. 확인하고 다시 시도하세요' ErrAlertSync: '알람 정보 동기화 오류입니다. 확인하고 다시 시도하세요'
ErrWebhookTestFailed: 'Webhook 설정을 확인하세요. 자세한 원인은 시스템 로그를 확인하세요.'
# 작업 - 런타임 # 작업 - 런타임
ErrInstallExtension: '이미 설치 작업이 진행 중입니다. 작업이 완료될 때까지 기다려 주세요.' ErrInstallExtension: '이미 설치 작업이 진행 중입니다. 작업이 완료될 때까지 기다려 주세요.'
# 알림 메일 템플릿 # 알림 메일 템플릿
PanelAlertTitle: '패널 알림 통지' PanelAlertTitle: '패널 알림 통지'
TestAlertTitle: '테스트 이메일 - 이메일 연결 확인' TestAlertTitle: '패널 테스트 알림'
TestAlert: '이것은 테스트 이메일이며 이메일 발송 설정이 올바른지 확인합니다.' TestAlertMessage: '알림 채널 설정 및 연결 상태를 확인하기 위한 테스트 알림입니다.'
LicenseExpirationAlert: '귀하의 {{ .node }}{{ .ip }} 패널, 라이센스가 {{ .day }}일 후에 만료됩니다. 자세한 내용은 패널에 로그인하십시오.' LicenseExpirationAlert: '귀하의 {{ .node }}{{ .ip }} 패널, 라이센스가 {{ .day }}일 후에 만료됩니다. 자세한 내용은 패널에 로그인하십시오.'
CronJobFailedAlert: '귀하의 {{ .node }}{{ .ip }} 패널, 예약 작업 {{ .name }} 실행에 실패했습니다. 자세한 내용은 패널에 로그인하십시오.' CronJobFailedAlert: '귀하의 {{ .node }}{{ .ip }} 패널, 예약 작업 {{ .name }} 실행에 실패했습니다. 자세한 내용은 패널에 로그인하십시오.'
ClamAlert: '귀하의 {{ .node }}{{ .ip }} 패널, 바이러스 스캔 작업에서 {{ .num }}개의 감염 파일이 감지되었습니다. 자세한 내용은 패널에 로그인하십시오.' ClamAlert: '귀하의 {{ .node }}{{ .ip }} 패널, 바이러스 스캔 작업에서 {{ .num }}개의 감염 파일이 감지되었습니다. 자세한 내용은 패널에 로그인하십시오.'
@@ -718,6 +725,8 @@ ErrDockerIPv4ForwardingDisabled: "IPv4 전달이 비활성화되어 있습니다
ErrFirewallRuleSavedApplyFailed: "새 규칙 설정이 저장되었지만 방화벽에 적용하지 못했습니다. 동기화하여 다시 시도하세요: {{ .detail }}" ErrFirewallRuleSavedApplyFailed: "새 규칙 설정이 저장되었지만 방화벽에 적용하지 못했습니다. 동기화하여 다시 시도하세요: {{ .detail }}"
ErrFirewallRuleConflict: "UFW에 일치 조건이 같지만 동작이 다른 규칙이 이미 있습니다. 기존 규칙을 편집하세요." ErrFirewallRuleConflict: "UFW에 일치 조건이 같지만 동작이 다른 규칙이 이미 있습니다. 기존 규칙을 편집하세요."
ErrForwardInterfaceNotFound: "전달 규칙에 지정된 네트워크 인터페이스 {{ .name }}이 존재하지 않습니다. 이름을 확인한 후 다시 시도하세요." ErrForwardInterfaceNotFound: "전달 규칙에 지정된 네트워크 인터페이스 {{ .name }}이 존재하지 않습니다. 이름을 확인한 후 다시 시도하세요."
ErrIPv6RARisk: "네트워크 연결이 끊길 위험이 있어 IPv6 전달 활성화를 차단했습니다. 문서와 시스템 로그를 확인하세요."
ErrIPv6RACheckFailed: "네트워크 구성을 확인할 수 없어 IPv6 전달 활성화를 차단했습니다. 자세한 내용은 시스템 로그를 확인하세요."
ErrSSHPublicKey: "유효한 SSH 공개 키를 입력하세요" ErrSSHPublicKey: "유효한 SSH 공개 키를 입력하세요"
ErrSSHPrivateKey: "SSH 개인 키 형식이 잘못되었거나 암호가 틀립니다. 암호화된 키에는 원래 암호가 필요합니다" ErrSSHPrivateKey: "SSH 개인 키 형식이 잘못되었거나 암호가 틀립니다. 암호화된 키에는 원래 암호가 필요합니다"
+11 -2
View File
@@ -130,6 +130,9 @@ ErrComposeProjectNameEmpty: 'ບໍ່ສາມາດກຳນົດຊື່ໂ
ErrComposeNameInvalid: 'ຊື່ໂຄງການ Compose ບໍ່ຖືກຕ້ອງ: ຕ້ອງເລີ່ມດ້ວຍຕົວພິມນ້ອຍ ຫຼື ຕົວເລກ, ໃຊ້ໄດ້ສະເພາະຕົວພິມນ້ອຍ, ຕົວເລກ, ຂີດກາງ ແລະ ຂີດລຸ່ມ, ຄວາມຍາວ 1-256 ຕົວອັກສອນ.' ErrComposeNameInvalid: 'ຊື່ໂຄງການ Compose ບໍ່ຖືກຕ້ອງ: ຕ້ອງເລີ່ມດ້ວຍຕົວພິມນ້ອຍ ຫຼື ຕົວເລກ, ໃຊ້ໄດ້ສະເພາະຕົວພິມນ້ອຍ, ຕົວເລກ, ຂີດກາງ ແລະ ຂີດລຸ່ມ, ຄວາມຍາວ 1-256 ຕົວອັກສອນ.'
ErrAppVersionUnavailable: 'ເວີຊັນແອັບນີ້ຖືກລຶບອອກຈາກບໍລິການທາງໄກແລ້ວ ກະລຸນາເລືອກເວີຊັນອື່ນແລ້ວລອງໃໝ່' ErrAppVersionUnavailable: 'ເວີຊັນແອັບນີ້ຖືກລຶບອອກຈາກບໍລິການທາງໄກແລ້ວ ກະລຸນາເລືອກເວີຊັນອື່ນແລ້ວລອງໃໝ່'
ErrVllmGB10ProOnly: 'ເວີຊັນ vLLM ນີ້ມີໃຫ້ໃຊ້ສະເພາະໃນ 1Panel Professional Edition ເທົ່ານັ້ນ.' ErrVllmGB10ProOnly: 'ເວີຊັນ vLLM ນີ້ມີໃຫ້ໃຊ້ສະເພາະໃນ 1Panel Professional Edition ເທົ່ານັ້ນ.'
ErrAIGatewayDatabaseTypeMismatch: 'AI Gateway ສາມາດອັບເກຣດໄດ້ພາຍໃນປະເພດຖານຂໍ້ມູນດຽວກັນເທົ່ານັ້ນ (SQLite ຫຼື PostgreSQL).'
ErrAIGatewayRecoverDatabaseTypeMismatch: 'ຂໍ້ມູນສຳຮອງ AI Gateway ແລະ ແອັບເປົ້າໝາຍຕ້ອງໃຊ້ປະເພດຖານຂໍ້ມູນດຽວກັນ (SQLite ຫຼື PostgreSQL).'
ErrHermesUpgradeVersion: 'Hermes Agent ສາມາດອັບເກຣດໄປຫາລຸ້ນໃໝ່ກວ່າທີ່ຮູ້ຈັກ ແລະ ອະນຸຍາດໂດຍການຕັ້ງຄ່າອັບເກຣດຂ້າມລຸ້ນຫຼັກເທົ່ານັ້ນ. ຖ້າຕ້ອງການກັບໄປລຸ້ນເກົ່າ ໃຫ້ກູ້ຄືນຈາກຂໍ້ມູນສຳຮອງ.'
ErrAppWarn: 'ສະຖານະແອັບຜິດປົກກະຕິ; ກະລຸນາກວດສອບບັນທຶກ' ErrAppWarn: 'ສະຖານະແອັບຜິດປົກກະຕິ; ກະລຸນາກວດສອບບັນທຶກ'
ErrAppParamKey: 'ພາຣາມິເຕີຂອງແອັບບໍ່ຖືກຕ້ອງ: {{ .name }}' ErrAppParamKey: 'ພາຣາມິເຕີຂອງແອັບບໍ່ຖືກຕ້ອງ: {{ .name }}'
ErrAppUpgrade: 'ອັບເກຣດແອັບລົ້ມເຫຼວ: {{ .name }} {{ .err }}' ErrAppUpgrade: 'ອັບເກຣດແອັບລົ້ມເຫຼວ: {{ .name }} {{ .err }}'
@@ -290,6 +293,9 @@ BuildCache: 'ແຄດການສ້າງ'
Volume: 'ວໍລຸມ (Volume)' Volume: 'ວໍລຸມ (Volume)'
Network: "ເຄືອຂ່າຍ" Network: "ເຄືອຂ່າຍ"
PruneStart: 'ກຳລັງດຳເນີນການທຳຄວາມສະອາດ' PruneStart: 'ກຳລັງດຳເນີນການທຳຄວາມສະອາດ'
NetworkCleanupConnected: "ຂ້າມເຄືອຂ່າຍ [{{ .name }}] ({{ .id }}): ມີຄອນເທນເນີເຊື່ອມຕໍ່ຢູ່ ຫຼື ເຄືອຂ່າຍກຳລັງຖືກໃຊ້ງານ; ບໍ່ໄດ້ລຶບ"
NetworkCleanupSummary: "ການລ້າງເຄືອຂ່າຍສຳເລັດ: ລຶບແລ້ວ {{ .deleted }}, ຂ້າມ {{ .skipped }}, ລົ້ມເຫຼວ {{ .failed }}"
NetworkCleanupPartialFailure: "ບໍ່ສາມາດລ້າງບາງເຄືອຂ່າຍໄດ້; ກະລຸນາເບິ່ງບັນທຶກວຽກ"
ContainerBackupPrepare: 'ກຽມພື້ນທີ່ເຮັດວຽກສຳລັບສຳຮອງຂໍ້ມູນຄອນເທນເນີ' ContainerBackupPrepare: 'ກຽມພື້ນທີ່ເຮັດວຽກສຳລັບສຳຮອງຂໍ້ມູນຄອນເທນເນີ'
ContainerBackupStop: 'ຢຸດຄອນເທນເນີກ່ອນການສຳຮອງຂໍ້ມູນ' ContainerBackupStop: 'ຢຸດຄອນເທນເນີກ່ອນການສຳຮອງຂໍ້ມູນ'
ContainerBackupInspect: 'ກວດສອບຄອນເທນເນີ ແລະ ເຄືອຂ່າຍສຳລັບການສຳຮອງຂໍ້ມູນ' ContainerBackupInspect: 'ກວດສອບຄອນເທນເນີ ແລະ ເຄືອຂ່າຍສຳລັບການສຳຮອງຂໍ້ມູນ'
@@ -618,14 +624,15 @@ AppAttributesNotMatch: 'ປະເພດ ຫຼື ຊື່ຂອງແອັບ
#alert #alert
ErrAlertSync: 'ຊິງຄ໌ຂໍ້ຄວາມແຈ້ງເຕືອນລົ້ມເຫຼວ' ErrAlertSync: 'ຊິງຄ໌ຂໍ້ຄວາມແຈ້ງເຕືອນລົ້ມເຫຼວ'
ErrWebhookTestFailed: 'ກະລຸນາກວດສອບການຕັ້ງຄ່າ Webhook. ເບິ່ງບັນທຶກລະບົບສຳລັບລາຍລະອຽດ.'
#task - runtime #task - runtime
ErrInstallExtension: 'ມີວຽກຕິດຕັ້ງສ່ວນເສີມອື່ນກຳລັງເຮັດວຽກຢູ່' ErrInstallExtension: 'ມີວຽກຕິດຕັ້ງສ່ວນເສີມອື່ນກຳລັງເຮັດວຽກຢູ່'
# alert mail template # alert mail template
PanelAlertTitle: "ການແຈ້ງເຕືອນຈາກແຜງຄວບຄຸມ" PanelAlertTitle: "ການແຈ້ງເຕືອນຈາກແຜງຄວບຄຸມ"
TestAlertTitle: "ອີເມວທົດສອບ - ກວດສອບການເຊື່ອມຕໍ່ອີເມວ" TestAlertTitle: "ການແຈ້ງເຕືອນທົດສອບຈາກແຜງຄວບຄຸມ"
TestAlert: "ນີ້ແມ່ນອີເມວທົດສອບເພື່ອຢືນຢັນວ່າການຕັ້ງຄ່າການສົ່ງອີເມວຂອງທ່ານຖືກຕ້ອງ." TestAlertMessage: "ນີ້ແມ່ນການແຈ້ງເຕືອນທົດສອບເພື່ອກວດສອບການຕັ້ງຄ່າ ແລະ ການເຊື່ອມຕໍ່ຂອງຊ່ອງທາງແຈ້ງເຕືອນ."
LicenseExpirationAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ໃບອະນຸຍາດຈະໝົດອາຍຸໃນ {{ .day }} ວັນ. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ." LicenseExpirationAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ໃບອະນຸຍາດຈະໝົດອາຍຸໃນ {{ .day }} ວັນ. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ."
CronJobFailedAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ວຽກທີ່ຕັ້ງເວລາ {{ .name }} ລົ້ມເຫຼວ. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ." CronJobFailedAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ວຽກທີ່ຕັ້ງເວລາ {{ .name }} ລົ້ມເຫຼວ. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ."
ClamAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ການສະແກນໄວຣັສພົບ {{ .num }} ໄຟລ໌ທີ່ຕິດເຊື້ອ. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ." ClamAlert: "ແຜງຄວບຄຸມ {{ .node }}{{ .ip }}: ການສະແກນໄວຣັສພົບ {{ .num }} ໄຟລ໌ທີ່ຕິດເຊື້ອ. ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງລາຍລະອຽດ."
@@ -709,6 +716,8 @@ ErrDockerIPv4ForwardingDisabled: "ການສົ່ງຕໍ່ IPv4 ຖືກ
ErrFirewallRuleSavedApplyFailed: "ບັນທຶກການຕັ້ງຄ່າກົດໃໝ່ແລ້ວ ແຕ່ນຳໃຊ້ກັບໄຟວໍບໍ່ສຳເລັດ. ລອງອີກຄັ້ງດ້ວຍການຊິງຂໍ້ມູນ: {{ .detail }}" ErrFirewallRuleSavedApplyFailed: "ບັນທຶກການຕັ້ງຄ່າກົດໃໝ່ແລ້ວ ແຕ່ນຳໃຊ້ກັບໄຟວໍບໍ່ສຳເລັດ. ລອງອີກຄັ້ງດ້ວຍການຊິງຂໍ້ມູນ: {{ .detail }}"
ErrFirewallRuleConflict: "ມີກົດ UFW ທີ່ມີເງື່ອນໄຂກົງກັນແຕ່ການດຳເນີນການຕ່າງກັນແລ້ວ. ກະລຸນາແກ້ໄຂກົດທີ່ມີຢູ່." ErrFirewallRuleConflict: "ມີກົດ UFW ທີ່ມີເງື່ອນໄຂກົງກັນແຕ່ການດຳເນີນການຕ່າງກັນແລ້ວ. ກະລຸນາແກ້ໄຂກົດທີ່ມີຢູ່."
ErrForwardInterfaceNotFound: "ບໍ່ພົບອິນເຕີເຟດເຄືອຂ່າຍ {{ .name }} ທີ່ລະບຸໃນກົດສົ່ງຕໍ່. ກະລຸນາກວດສອບຊື່ ແລ້ວລອງໃໝ່." ErrForwardInterfaceNotFound: "ບໍ່ພົບອິນເຕີເຟດເຄືອຂ່າຍ {{ .name }} ທີ່ລະບຸໃນກົດສົ່ງຕໍ່. ກະລຸນາກວດສອບຊື່ ແລ້ວລອງໃໝ່."
ErrIPv6RARisk: "ການສົ່ງຕໍ່ IPv6 ຖືກບລັອກເພື່ອປ້ອງກັນຄວາມສ່ຽງທີ່ເຄືອຂ່າຍຈະຂາດການເຊື່ອມຕໍ່. ເບິ່ງເອກະສານ ແລະ ບັນທຶກລະບົບ."
ErrIPv6RACheckFailed: "ການສົ່ງຕໍ່ IPv6 ຖືກບລັອກເນື່ອງຈາກບໍ່ສາມາດກວດສອບການຕັ້ງຄ່າເຄືອຂ່າຍໄດ້. ເບິ່ງລາຍລະອຽດໃນບັນທຶກລະບົບ."
ErrSSHPublicKey: "ກະລຸນາປ້ອນຄີສາທາລະນະ SSH ທີ່ຖືກຕ້ອງ" ErrSSHPublicKey: "ກະລຸນາປ້ອນຄີສາທາລະນະ SSH ທີ່ຖືກຕ້ອງ"
ErrSSHPrivateKey: "ຄີສ່ວນຕົວ SSH ບໍ່ຖືກຕ້ອງ ຫຼື ລະຫັດຜ່ານຜິດ; ຄີທີ່ເຂົ້າລະຫັດຕ້ອງໃຊ້ລະຫັດຜ່ານເດີມ" ErrSSHPrivateKey: "ຄີສ່ວນຕົວ SSH ບໍ່ຖືກຕ້ອງ ຫຼື ລະຫັດຜ່ານຜິດ; ຄີທີ່ເຂົ້າລະຫັດຕ້ອງໃຊ້ລະຫັດຜ່ານເດີມ"
+11 -2
View File
@@ -140,6 +140,9 @@ ErrComposeProjectNameEmpty: 'Nama projek tidak dapat diperoleh daripada konfigur
ErrComposeNameInvalid: 'Nama projek Compose tidak sah: mesti bermula dengan huruf kecil atau nombor, hanya mengandungi huruf kecil, nombor, tanda sempang dan garis bawah, serta sepanjang 1-256 aksara.' ErrComposeNameInvalid: 'Nama projek Compose tidak sah: mesti bermula dengan huruf kecil atau nombor, hanya mengandungi huruf kecil, nombor, tanda sempang dan garis bawah, serta sepanjang 1-256 aksara.'
ErrAppVersionUnavailable: 'Versi aplikasi ini telah dialih keluar daripada perkhidmatan jauh. Sila pilih versi lain dan cuba lagi.' ErrAppVersionUnavailable: 'Versi aplikasi ini telah dialih keluar daripada perkhidmatan jauh. Sila pilih versi lain dan cuba lagi.'
ErrVllmGB10ProOnly: 'Versi vLLM ini hanya tersedia dalam 1Panel Edisi Profesional.' ErrVllmGB10ProOnly: 'Versi vLLM ini hanya tersedia dalam 1Panel Edisi Profesional.'
ErrAIGatewayDatabaseTypeMismatch: 'AI Gateway hanya boleh dinaik taraf dalam jenis pangkalan data yang sama (SQLite atau PostgreSQL).'
ErrAIGatewayRecoverDatabaseTypeMismatch: 'Sandaran AI Gateway dan aplikasi sasaran mesti menggunakan jenis pangkalan data yang sama (SQLite atau PostgreSQL).'
ErrHermesUpgradeVersion: 'Hermes Agent hanya boleh dinaik taraf kepada keluaran lebih baharu yang dikenal pasti dan dibenarkan oleh tetapan naik taraf antara versi utama. Untuk kembali kepada versi lama, pulihkan sandaran.'
ErrAppWarn: 'Status tidak normal, sila semak log' ErrAppWarn: 'Status tidak normal, sila semak log'
ErrAppParamKey: 'Parameter {{ .name }} medan tidak normal' ErrAppParamKey: 'Parameter {{ .name }} medan tidak normal'
ErrAppUpgrade: 'Peningkatan {{ .name }} aplikasi gagal {{ .err }}' ErrAppUpgrade: 'Peningkatan {{ .name }} aplikasi gagal {{ .err }}'
@@ -300,6 +303,9 @@ BuildCache: 'Cache binaan'
Volume: 'Jilid storan' Volume: 'Jilid storan'
Network: 'Rangkaian' Network: 'Rangkaian'
PruneStart: 'Pembersihan sedang dijalankan, sila tunggu...' PruneStart: 'Pembersihan sedang dijalankan, sila tunggu...'
NetworkCleanupConnected: "Rangkaian [{{ .name }}] ({{ .id }}) dilangkau: terdapat kontena yang disambungkan atau rangkaian sedang digunakan; tidak dipadamkan"
NetworkCleanupSummary: "Pembersihan rangkaian selesai: dipadamkan {{ .deleted }}, dilangkau {{ .skipped }}, gagal {{ .failed }}"
NetworkCleanupPartialFailure: "Sesetengah rangkaian tidak dapat dibersihkan; semak log tugas"
ContainerBackupPrepare: 'Sediakan ruang kerja sandaran kontena' ContainerBackupPrepare: 'Sediakan ruang kerja sandaran kontena'
ContainerBackupStop: 'Hentikan kontena sebelum sandaran' ContainerBackupStop: 'Hentikan kontena sebelum sandaran'
ContainerBackupInspect: 'Sandarkan inspect dan rangkaian kontena' ContainerBackupInspect: 'Sandarkan inspect dan rangkaian kontena'
@@ -628,14 +634,15 @@ AppAttributesNotMatch: 'Jenis atau nama aplikasi tidak sepadan'
# amaran # amaran
ErrAlertMethodNotSupported: 'Versi ini tidak menyokong kaedah amaran ini' ErrAlertMethodNotSupported: 'Versi ini tidak menyokong kaedah amaran ini'
ErrAlertSync: 'Ralat penyegerakan maklumat penggera, sila semak dan cuba lagi' ErrAlertSync: 'Ralat penyegerakan maklumat penggera, sila semak dan cuba lagi'
ErrWebhookTestFailed: 'Sila semak konfigurasi Webhook. Lihat log sistem untuk butiran lanjut.'
# tugas - runtime # tugas - runtime
ErrInstallExtension: 'Tugas pemasangan sudah sedang berjalan, silakan tunggu tugas selesai' ErrInstallExtension: 'Tugas pemasangan sudah sedang berjalan, silakan tunggu tugas selesai'
# templat mel amaran # templat mel amaran
PanelAlertTitle: 'Notifikasi Amaran Panel' PanelAlertTitle: 'Notifikasi Amaran Panel'
TestAlertTitle: 'E-mel Ujian - Sahkan Sambungan E-mel' TestAlertTitle: 'Notifikasi Ujian Panel'
TestAlert: 'Ini adalah e-mel ujian untuk mengesahkan konfigurasi penghantaran e-mel anda betul.' TestAlertMessage: 'Ini ialah notifikasi ujian untuk mengesahkan konfigurasi dan kesambungan saluran notifikasi.'
LicenseExpirationAlert: 'Panel {{ .node }}{{ .ip }} Anda, lesen akan tamat tempoh dalam {{ .day }} hari. Sila log masuk ke panel untuk butiran lanjut.' LicenseExpirationAlert: 'Panel {{ .node }}{{ .ip }} Anda, lesen akan tamat tempoh dalam {{ .day }} hari. Sila log masuk ke panel untuk butiran lanjut.'
CronJobFailedAlert: 'Panel {{ .node }}{{ .ip }} Anda, tugas jadual {{ .name }} gagal dilaksanakan. Sila log masuk ke panel untuk butiran lanjut.' CronJobFailedAlert: 'Panel {{ .node }}{{ .ip }} Anda, tugas jadual {{ .name }} gagal dilaksanakan. Sila log masuk ke panel untuk butiran lanjut.'
ClamAlert: 'Panel {{ .node }}{{ .ip }} Anda, tugas imbasan virus mengesan {{ .num }} fail yang dijangkiti. Sila log masuk ke panel untuk butiran lanjut.' ClamAlert: 'Panel {{ .node }}{{ .ip }} Anda, tugas imbasan virus mengesan {{ .num }} fail yang dijangkiti. Sila log masuk ke panel untuk butiran lanjut.'
@@ -718,6 +725,8 @@ ErrDockerIPv4ForwardingDisabled: "Pemajuan IPv4 dilumpuhkan. Tetapkan net.ipv4.i
ErrFirewallRuleSavedApplyFailed: "Konfigurasi peraturan baharu telah disimpan, tetapi gagal digunakan pada tembok api. Cuba lagi melalui penyegerakan: {{ .detail }}" ErrFirewallRuleSavedApplyFailed: "Konfigurasi peraturan baharu telah disimpan, tetapi gagal digunakan pada tembok api. Cuba lagi melalui penyegerakan: {{ .detail }}"
ErrFirewallRuleConflict: "Peraturan UFW dengan syarat padanan yang sama tetapi tindakan berbeza sudah wujud. Sila edit peraturan sedia ada." ErrFirewallRuleConflict: "Peraturan UFW dengan syarat padanan yang sama tetapi tindakan berbeza sudah wujud. Sila edit peraturan sedia ada."
ErrForwardInterfaceNotFound: "Antara muka rangkaian {{ .name }} yang dinyatakan dalam peraturan pemajuan tidak wujud. Semak namanya dan cuba lagi." ErrForwardInterfaceNotFound: "Antara muka rangkaian {{ .name }} yang dinyatakan dalam peraturan pemajuan tidak wujud. Semak namanya dan cuba lagi."
ErrIPv6RARisk: "Pemajuan IPv6 disekat bagi mengelakkan risiko kehilangan sambungan rangkaian. Rujuk dokumentasi dan log sistem."
ErrIPv6RACheckFailed: "Pemajuan IPv6 disekat kerana konfigurasi rangkaian tidak dapat diperiksa. Lihat log sistem untuk butiran."
ErrSSHPublicKey: "Masukkan kunci awam SSH yang sah" ErrSSHPublicKey: "Masukkan kunci awam SSH yang sah"
ErrSSHPrivateKey: "Kunci peribadi SSH tidak sah atau frasa laluan salah; kunci yang disulitkan memerlukan frasa laluan asal" ErrSSHPrivateKey: "Kunci peribadi SSH tidak sah atau frasa laluan salah; kunci yang disulitkan memerlukan frasa laluan asal"
+11 -2
View File
@@ -140,6 +140,9 @@ ErrComposeProjectNameEmpty: 'Não foi possível derivar um nome de projeto da co
ErrComposeNameInvalid: 'Nome de projeto Compose inválido: deve começar com uma letra minúscula ou número, conter apenas letras minúsculas, números, hífens e sublinhados e ter de 1 a 256 caracteres.' ErrComposeNameInvalid: 'Nome de projeto Compose inválido: deve começar com uma letra minúscula ou número, conter apenas letras minúsculas, números, hífens e sublinhados e ter de 1 a 256 caracteres.'
ErrAppVersionUnavailable: 'Esta versão do aplicativo foi removida do serviço remoto. Selecione outra versão e tente novamente.' ErrAppVersionUnavailable: 'Esta versão do aplicativo foi removida do serviço remoto. Selecione outra versão e tente novamente.'
ErrVllmGB10ProOnly: 'Esta versão do vLLM está disponível apenas no 1Panel Professional Edition.' ErrVllmGB10ProOnly: 'Esta versão do vLLM está disponível apenas no 1Panel Professional Edition.'
ErrAIGatewayDatabaseTypeMismatch: 'O AI Gateway só pode ser atualizado dentro do mesmo tipo de banco de dados (SQLite ou PostgreSQL).'
ErrAIGatewayRecoverDatabaseTypeMismatch: 'O backup do AI Gateway e o aplicativo de destino devem usar o mesmo tipo de banco de dados (SQLite ou PostgreSQL).'
ErrHermesUpgradeVersion: 'O Hermes Agent só pode ser atualizado para uma versão reconhecida mais recente permitida pela configuração de atualização entre versões principais. Para voltar a uma versão anterior, restaure um backup.'
ErrAppWarn: 'Status anormal, verifique o log' ErrAppWarn: 'Status anormal, verifique o log'
ErrAppParamKey: 'O campo de parâmetro {{ .name }} está anormal' ErrAppParamKey: 'O campo de parâmetro {{ .name }} está anormal'
ErrAppUpgrade: 'Falha na atualização do aplicativo {{ .name }} {{ .err }}' ErrAppUpgrade: 'Falha na atualização do aplicativo {{ .name }} {{ .err }}'
@@ -300,6 +303,9 @@ BuildCache: 'Cache de construção'
Volume: 'Volume de armazenamento' Volume: 'Volume de armazenamento'
Network: 'Rede' Network: 'Rede'
PruneStart: 'Limpando em andamento aguarde...' PruneStart: 'Limpando em andamento aguarde...'
NetworkCleanupConnected: "Rede [{{ .name }}] ({{ .id }}) ignorada: há contêineres conectados ou a rede está em uso; não foi excluída"
NetworkCleanupSummary: "Limpeza de redes concluída: excluídas {{ .deleted }}, ignoradas {{ .skipped }}, falhas {{ .failed }}"
NetworkCleanupPartialFailure: "Não foi possível limpar algumas redes; consulte o log da tarefa"
ContainerBackupPrepare: 'Preparar área de backup do contêiner' ContainerBackupPrepare: 'Preparar área de backup do contêiner'
ContainerBackupStop: 'Parar contêiner antes do backup' ContainerBackupStop: 'Parar contêiner antes do backup'
ContainerBackupInspect: 'Fazer backup do inspect e rede do contêiner' ContainerBackupInspect: 'Fazer backup do inspect e rede do contêiner'
@@ -628,14 +634,15 @@ AppAttributesNotMatch: 'O tipo ou nome do aplicativo não corresponde'
# alerta # alerta
ErrAlertMethodNotSupported: 'Esta versão não oferece suporte a este método de alerta' ErrAlertMethodNotSupported: 'Esta versão não oferece suporte a este método de alerta'
ErrAlertSync: 'Erro de sincronização de informações de alarme, verifique e' ErrAlertSync: 'Erro de sincronização de informações de alarme, verifique e'
ErrWebhookTestFailed: 'Verifique a configuração do Webhook. Consulte os logs do sistema para mais detalhes.'
# tarefa - runtime # tarefa - runtime
ErrInstallExtension: 'Já existe uma tarefa de instalação em andamento, aguarde a conclusão da tarefa' ErrInstallExtension: 'Já existe uma tarefa de instalação em andamento, aguarde a conclusão da tarefa'
# modelo de e-mail de alerta # modelo de e-mail de alerta
PanelAlertTitle: 'Notificação de Alerta do Painel' PanelAlertTitle: 'Notificação de Alerta do Painel'
TestAlertTitle: 'E-mail de Teste - Verificar Conectividade de E-mail' TestAlertTitle: 'Notificação de teste do painel'
TestAlert: 'Este é um e-mail de teste para verificar se sua configuração de envio de e-mails está correta.' TestAlertMessage: 'Esta é uma notificação de teste para verificar a configuração e a conectividade do canal de notificações.'
LicenseExpirationAlert: 'Seu Painel {{ .node }}{{ .ip }}, a licença expirará em {{ .day }} dias. Faça login no painel para obter detalhes.' LicenseExpirationAlert: 'Seu Painel {{ .node }}{{ .ip }}, a licença expirará em {{ .day }} dias. Faça login no painel para obter detalhes.'
CronJobFailedAlert: 'Seu Painel {{ .node }}{{ .ip }}, a execução da tarefa agendada {{ .name }} falhou. Faça login no painel para obter detalhes.' CronJobFailedAlert: 'Seu Painel {{ .node }}{{ .ip }}, a execução da tarefa agendada {{ .name }} falhou. Faça login no painel para obter detalhes.'
ClamAlert: 'Seu Painel {{ .node }}{{ .ip }}, a tarefa de verificação de vírus detectou {{ .num }} arquivos infectados. Faça login no painel para obter detalhes.' ClamAlert: 'Seu Painel {{ .node }}{{ .ip }}, a tarefa de verificação de vírus detectou {{ .num }} arquivos infectados. Faça login no painel para obter detalhes.'
@@ -718,6 +725,8 @@ ErrDockerIPv4ForwardingDisabled: "O encaminhamento IPv4 está desativado. Defina
ErrFirewallRuleSavedApplyFailed: "A configuração da nova regra foi salva, mas não pôde ser aplicada ao firewall. Tente novamente por meio da sincronização: {{ .detail }}" ErrFirewallRuleSavedApplyFailed: "A configuração da nova regra foi salva, mas não pôde ser aplicada ao firewall. Tente novamente por meio da sincronização: {{ .detail }}"
ErrFirewallRuleConflict: "Já existe uma regra UFW com as mesmas condições e uma ação diferente. Edite a regra existente." ErrFirewallRuleConflict: "Já existe uma regra UFW com as mesmas condições e uma ação diferente. Edite a regra existente."
ErrForwardInterfaceNotFound: "A interface de rede {{ .name }} especificada na regra de encaminhamento não existe. Verifique o nome e tente novamente." ErrForwardInterfaceNotFound: "A interface de rede {{ .name }} especificada na regra de encaminhamento não existe. Verifique o nome e tente novamente."
ErrIPv6RARisk: "O encaminhamento IPv6 foi bloqueado para evitar uma possível perda de conexão. Consulte a documentação e os logs do sistema."
ErrIPv6RACheckFailed: "O encaminhamento IPv6 foi bloqueado porque não foi possível verificar a configuração de rede. Consulte os logs do sistema."
ErrSSHPublicKey: "Informe uma chave pública SSH válida" ErrSSHPublicKey: "Informe uma chave pública SSH válida"
ErrSSHPrivateKey: "Chave privada SSH inválida ou senha incorreta; chaves criptografadas exigem a senha original" ErrSSHPrivateKey: "Chave privada SSH inválida ou senha incorreta; chaves criptografadas exigem a senha original"
+11 -2
View File
@@ -140,6 +140,9 @@ ErrComposeProjectNameEmpty: 'Не удалось получить имя про
ErrComposeNameInvalid: 'Недопустимое имя проекта Compose: оно должно начинаться со строчной буквы или цифры, содержать только строчные буквы, цифры, дефисы и подчёркивания и иметь длину от 1 до 256 символов.' ErrComposeNameInvalid: 'Недопустимое имя проекта Compose: оно должно начинаться со строчной буквы или цифры, содержать только строчные буквы, цифры, дефисы и подчёркивания и иметь длину от 1 до 256 символов.'
ErrAppVersionUnavailable: 'Эта версия приложения удалена из удаленного сервиса. Выберите другую версию и повторите попытку.' ErrAppVersionUnavailable: 'Эта версия приложения удалена из удаленного сервиса. Выберите другую версию и повторите попытку.'
ErrVllmGB10ProOnly: 'Эта версия vLLM доступна только в профессиональной редакции 1Panel.' ErrVllmGB10ProOnly: 'Эта версия vLLM доступна только в профессиональной редакции 1Panel.'
ErrAIGatewayDatabaseTypeMismatch: 'AI Gateway можно обновлять только в пределах одного типа базы данных (SQLite или PostgreSQL).'
ErrAIGatewayRecoverDatabaseTypeMismatch: 'Резервная копия AI Gateway и целевое приложение должны использовать один и тот же тип базы данных (SQLite или PostgreSQL).'
ErrHermesUpgradeVersion: 'Hermes Agent можно обновить только до более нового распознанного выпуска, разрешённого настройкой обновления между основными версиями. Для возврата к старой версии восстановите резервную копию.'
ErrAppWarn: 'Ненормальное состояние, проверьте журнал' ErrAppWarn: 'Ненормальное состояние, проверьте журнал'
ErrAppParamKey: 'Поле параметра {{ .name }} ненормально' ErrAppParamKey: 'Поле параметра {{ .name }} ненормально'
ErrAppUpgrade: 'Обновление приложения {{ .name }} не удалось {{ .err }}' ErrAppUpgrade: 'Обновление приложения {{ .name }} не удалось {{ .err }}'
@@ -300,6 +303,9 @@ BuildCache: 'Кэш сборки'
Volume: 'Том хранилища' Volume: 'Том хранилища'
Network: 'Сеть' Network: 'Сеть'
PruneStart: 'Очистка выполняется, пожалуйста, подождите...' PruneStart: 'Очистка выполняется, пожалуйста, подождите...'
NetworkCleanupConnected: "Сеть [{{ .name }}] ({{ .id }}) пропущена: подключены контейнеры или сеть используется; не удалена"
NetworkCleanupSummary: "Очистка сетей завершена: удалено {{ .deleted }}, пропущено {{ .skipped }}, ошибок {{ .failed }}"
NetworkCleanupPartialFailure: "Не удалось очистить некоторые сети; проверьте журнал задачи"
ContainerBackupPrepare: 'Подготовить рабочую область резервной копии контейнера' ContainerBackupPrepare: 'Подготовить рабочую область резервной копии контейнера'
ContainerBackupStop: 'Остановить контейнер перед резервным копированием' ContainerBackupStop: 'Остановить контейнер перед резервным копированием'
ContainerBackupInspect: 'Сделать резервную копию inspect и сети контейнера' ContainerBackupInspect: 'Сделать резервную копию inspect и сети контейнера'
@@ -628,14 +634,15 @@ AppAttributesNotMatch: 'Тип или имя приложения не совп
# оповещение # оповещение
ErrAlertMethodNotSupported: 'Эта версия не поддерживает данный способ оповещения' ErrAlertMethodNotSupported: 'Эта версия не поддерживает данный способ оповещения'
ErrAlertSync: 'Ошибка синхронизации информации о тревоге, проверьте и повторите попытку' ErrAlertSync: 'Ошибка синхронизации информации о тревоге, проверьте и повторите попытку'
ErrWebhookTestFailed: 'Проверьте конфигурацию Webhook. Подробности доступны в системных журналах.'
# задача - среда выполнения # задача - среда выполнения
ErrInstallExtension: 'Уже выполняется задача установки, подождите, пока задача завершится' ErrInstallExtension: 'Уже выполняется задача установки, подождите, пока задача завершится'
# шаблон письма оповещения # шаблон письма оповещения
PanelAlertTitle: 'Уведомление панели' PanelAlertTitle: 'Уведомление панели'
TestAlertTitle: 'Тестовое письмо - проверка подключения' TestAlertTitle: 'Тестовое уведомление панели'
TestAlert: 'Это тестовое письмо для проверки правильности настройки отправки писем.' TestAlertMessage: 'Это тестовое уведомление для проверки настроек канала уведомлений и подключения.'
LicenseExpirationAlert: 'Ваш панель {{ .node }}{{ .ip }}, лицензия истечет через {{ .day }} дней. Войдите в панель для получения деталей.' LicenseExpirationAlert: 'Ваш панель {{ .node }}{{ .ip }}, лицензия истечет через {{ .day }} дней. Войдите в панель для получения деталей.'
CronJobFailedAlert: 'Ваш панель {{ .node }}{{ .ip }}, выполнение запланированной задачи {{ .name }} не удалось. Войдите в панель для получения деталей.' CronJobFailedAlert: 'Ваш панель {{ .node }}{{ .ip }}, выполнение запланированной задачи {{ .name }} не удалось. Войдите в панель для получения деталей.'
ClamAlert: 'Ваш панель {{ .node }}{{ .ip }}, задача сканирования на вирусы обнаружила {{ .num }} зараженных файлов. Войдите в панель для получения деталей.' ClamAlert: 'Ваш панель {{ .node }}{{ .ip }}, задача сканирования на вирусы обнаружила {{ .num }} зараженных файлов. Войдите в панель для получения деталей.'
@@ -718,6 +725,8 @@ ErrDockerIPv4ForwardingDisabled: "Пересылка IPv4 отключена. П
ErrFirewallRuleSavedApplyFailed: "Настройки нового правила сохранены, но применить их к межсетевому экрану не удалось. Повторите попытку с помощью синхронизации: {{ .detail }}" ErrFirewallRuleSavedApplyFailed: "Настройки нового правила сохранены, но применить их к межсетевому экрану не удалось. Повторите попытку с помощью синхронизации: {{ .detail }}"
ErrFirewallRuleConflict: "В UFW уже есть правило с такими же условиями и другим действием. Отредактируйте существующее правило." ErrFirewallRuleConflict: "В UFW уже есть правило с такими же условиями и другим действием. Отредактируйте существующее правило."
ErrForwardInterfaceNotFound: "Сетевой интерфейс {{ .name }}, указанный в правиле перенаправления, не существует. Проверьте имя и повторите попытку." ErrForwardInterfaceNotFound: "Сетевой интерфейс {{ .name }}, указанный в правиле перенаправления, не существует. Проверьте имя и повторите попытку."
ErrIPv6RARisk: "Включение переадресации IPv6 заблокировано из-за риска потери связи. См. документацию и системные журналы."
ErrIPv6RACheckFailed: "Включение переадресации IPv6 заблокировано: не удалось проверить настройки сети. Подробности в системных журналах."
ErrSSHPublicKey: "Введите действительный открытый ключ SSH" ErrSSHPublicKey: "Введите действительный открытый ключ SSH"
ErrSSHPrivateKey: "Недопустимый закрытый ключ SSH или неверная парольная фраза; для зашифрованных ключей требуется исходная парольная фраза" ErrSSHPrivateKey: "Недопустимый закрытый ключ SSH или неверная парольная фраза; для зашифрованных ключей требуется исходная парольная фраза"
+11 -2
View File
@@ -140,6 +140,9 @@ ErrComposeProjectNameEmpty: 'Docker Compose yapılandırmasından veya dosyanın
ErrComposeNameInvalid: 'Geçersiz Compose proje adı: küçük harf veya rakamla başlamalı, yalnızca küçük harf, rakam, kısa çizgi ve alt çizgi içermeli ve 1-256 karakter uzunluğunda olmalıdır.' ErrComposeNameInvalid: 'Geçersiz Compose proje adı: küçük harf veya rakamla başlamalı, yalnızca küçük harf, rakam, kısa çizgi ve alt çizgi içermeli ve 1-256 karakter uzunluğunda olmalıdır.'
ErrAppVersionUnavailable: 'Bu uygulama sürümü uzak hizmetten kaldırıldı. Lütfen başka bir sürüm seçip tekrar deneyin.' ErrAppVersionUnavailable: 'Bu uygulama sürümü uzak hizmetten kaldırıldı. Lütfen başka bir sürüm seçip tekrar deneyin.'
ErrVllmGB10ProOnly: 'Bu vLLM sürümü yalnızca 1Panel Professional Edition sürümünde kullanılabilir.' ErrVllmGB10ProOnly: 'Bu vLLM sürümü yalnızca 1Panel Professional Edition sürümünde kullanılabilir.'
ErrAIGatewayDatabaseTypeMismatch: 'AI Gateway yalnızca aynı veritabanı türü (SQLite veya PostgreSQL) içinde yükseltilebilir.'
ErrAIGatewayRecoverDatabaseTypeMismatch: 'AI Gateway yedeği ve hedef uygulama aynı veritabanı türünü (SQLite veya PostgreSQL) kullanmalıdır.'
ErrHermesUpgradeVersion: 'Hermes Agent yalnızca ana sürümler arası yükseltme ayarının izin verdiği, tanınan daha yeni bir sürüme yükseltilebilir. Eski bir sürüme dönmek için yedekten geri yükleyin.'
ErrAppWarn: 'Anormal durum, lütfen günlüğü kontrol edin' ErrAppWarn: 'Anormal durum, lütfen günlüğü kontrol edin'
ErrAppParamKey: 'Parametre {{ .name }} alanı anormal' ErrAppParamKey: 'Parametre {{ .name }} alanı anormal'
ErrAppUpgrade: 'Uygulama {{ .name }} yükseltmesi başarısız {{ .err }}' ErrAppUpgrade: 'Uygulama {{ .name }} yükseltmesi başarısız {{ .err }}'
@@ -300,6 +303,9 @@ BuildCache: 'Derleme önbelleği'
Volume: 'Depolama hacmi' Volume: 'Depolama hacmi'
Network: 'Ağ' Network: 'Ağ'
PruneStart: 'Temizlik devam ediyor, lütfen bekleyin...' PruneStart: 'Temizlik devam ediyor, lütfen bekleyin...'
NetworkCleanupConnected: "Ağ [{{ .name }}] ({{ .id }}) atlandı: bağlı konteynerler var veya ağ kullanımda; silinmedi"
NetworkCleanupSummary: "Ağ temizliği tamamlandı: silinen {{ .deleted }}, atlanan {{ .skipped }}, başarısız {{ .failed }}"
NetworkCleanupPartialFailure: "Bazı ağlar temizlenemedi; görev günlüğünü kontrol edin"
ContainerBackupPrepare: 'Konteyner yedekleme çalışma alanını hazırla' ContainerBackupPrepare: 'Konteyner yedekleme çalışma alanını hazırla'
ContainerBackupStop: 'Yedeklemeden önce konteyneri durdur' ContainerBackupStop: 'Yedeklemeden önce konteyneri durdur'
ContainerBackupInspect: 'Konteyner inspect ve ağ bilgisini yedekle' ContainerBackupInspect: 'Konteyner inspect ve ağ bilgisini yedekle'
@@ -628,14 +634,15 @@ AppAttributesNotMatch: 'Uygulama türü veya adı eşleşmiyor'
# uyarı # uyarı
ErrAlertMethodNotSupported: 'Bu sürüm bu uyarı yöntemini desteklemiyor' ErrAlertMethodNotSupported: 'Bu sürüm bu uyarı yöntemini desteklemiyor'
ErrAlertSync: 'Alarm bilgisi senkronizasyon hatası, lütfen kontrol edin ve tekrar deneyin' ErrAlertSync: 'Alarm bilgisi senkronizasyon hatası, lütfen kontrol edin ve tekrar deneyin'
ErrWebhookTestFailed: 'Lütfen Webhook yapılandırmasını kontrol edin. Ayrıntılar için sistem günlüklerine bakın.'
# görev - çalışma zamanı # görev - çalışma zamanı
ErrInstallExtension: 'Zaten devam eden bir kurulum görevi var, görevin bitmesini bekleyin' ErrInstallExtension: 'Zaten devam eden bir kurulum görevi var, görevin bitmesini bekleyin'
# uyarı e-posta şablonu # uyarı e-posta şablonu
PanelAlertTitle: 'Panel Uyarı Bildirimi' PanelAlertTitle: 'Panel Uyarı Bildirimi'
TestAlertTitle: 'Test E-postası - E-posta Bağlantısını Doğrula' TestAlertTitle: 'Panel Test Bildirimi'
TestAlert: 'Bu, e-posta gönderim ayarlarınızın doğru yapılandırıldığını doğrulamak için gönderilen bir test e-postasıdır.' TestAlertMessage: 'Bu, bildirim kanalı yapılandırmasını ve bağlantısını doğrulamak için gönderilen bir test bildirimidir.'
LicenseExpirationAlert: 'Paneliniz {{ .node }}{{ .ip }}, lisans {{ .day }} gün içinde süresi dolacak. Detaylar için paneline giriş yapın.' LicenseExpirationAlert: 'Paneliniz {{ .node }}{{ .ip }}, lisans {{ .day }} gün içinde süresi dolacak. Detaylar için paneline giriş yapın.'
CronJobFailedAlert: 'Paneliniz {{ .node }}{{ .ip }}, zamanlanmış görev {{ .name }} çalıştırma başarısız oldu. Detaylar için paneline giriş yapın.' CronJobFailedAlert: 'Paneliniz {{ .node }}{{ .ip }}, zamanlanmış görev {{ .name }} çalıştırma başarısız oldu. Detaylar için paneline giriş yapın.'
ClamAlert: 'Paneliniz {{ .node }}{{ .ip }}, virüs tarama görevi {{ .num }} enfekte dosya tespit etti. Detaylar için paneline giriş yapın.' ClamAlert: 'Paneliniz {{ .node }}{{ .ip }}, virüs tarama görevi {{ .num }} enfekte dosya tespit etti. Detaylar için paneline giriş yapın.'
@@ -718,6 +725,8 @@ ErrDockerIPv4ForwardingDisabled: "IPv4 yönlendirmesi devre dışı. Docker güv
ErrFirewallRuleSavedApplyFailed: "Yeni kural yapılandırması kaydedildi, ancak güvenlik duvarına uygulanamadı. Eşitleme yaparak yeniden deneyin: {{ .detail }}" ErrFirewallRuleSavedApplyFailed: "Yeni kural yapılandırması kaydedildi, ancak güvenlik duvarına uygulanamadı. Eşitleme yaparak yeniden deneyin: {{ .detail }}"
ErrFirewallRuleConflict: "UFW içinde aynı eşleşme koşullarına sahip, farklı bir eylem uygulayan bir kural zaten var. Mevcut kuralı düzenleyin." ErrFirewallRuleConflict: "UFW içinde aynı eşleşme koşullarına sahip, farklı bir eylem uygulayan bir kural zaten var. Mevcut kuralı düzenleyin."
ErrForwardInterfaceNotFound: "Yönlendirme kuralında belirtilen {{ .name }} ağ arayüzü mevcut değil. Arayüz adını kontrol edip yeniden deneyin." ErrForwardInterfaceNotFound: "Yönlendirme kuralında belirtilen {{ .name }} ağ arayüzü mevcut değil. Arayüz adını kontrol edip yeniden deneyin."
ErrIPv6RARisk: "Ağ bağlantısının kesilme riski nedeniyle IPv6 yönlendirmesi engellendi. Belgeleri ve sistem günlüklerini inceleyin."
ErrIPv6RACheckFailed: "Ağ yapılandırması denetlenemediği için IPv6 yönlendirmesi engellendi. Ayrıntılar için sistem günlüklerini inceleyin."
ErrSSHPublicKey: "Geçerli bir SSH açık anahtarı girin" ErrSSHPublicKey: "Geçerli bir SSH açık anahtarı girin"
ErrSSHPrivateKey: "SSH özel anahtarı geçersiz veya parola yanlış; şifreli anahtarlar mevcut parolayı gerektirir" ErrSSHPrivateKey: "SSH özel anahtarı geçersiz veya parola yanlış; şifreli anahtarlar mevcut parolayı gerektirir"
+11 -2
View File
@@ -140,6 +140,9 @@ ErrComposeProjectNameEmpty: '無法從 Docker Compose 設定或檔案父目錄
ErrComposeNameInvalid: 'Compose 專案名稱格式錯誤:必須以小寫字母或數字開頭,僅支援小寫字母、數字、- 和 _,長度為 1-256 個字元' ErrComposeNameInvalid: 'Compose 專案名稱格式錯誤:必須以小寫字母或數字開頭,僅支援小寫字母、數字、- 和 _,長度為 1-256 個字元'
ErrAppVersionUnavailable: '目前應用程式版本已從遠端服務下架,請選擇其他版本後重試' ErrAppVersionUnavailable: '目前應用程式版本已從遠端服務下架,請選擇其他版本後重試'
ErrVllmGB10ProOnly: '此 vLLM 版本僅支援 1Panel 專業版' ErrVllmGB10ProOnly: '此 vLLM 版本僅支援 1Panel 專業版'
ErrAIGatewayDatabaseTypeMismatch: 'AI 閘道只能在相同資料庫類型(SQLite 或 PostgreSQL)之間升級。'
ErrAIGatewayRecoverDatabaseTypeMismatch: 'AI 閘道備份與目標應用程式的資料庫類型(SQLite 或 PostgreSQL)必須一致,無法跨資料庫類型還原。'
ErrHermesUpgradeVersion: 'Hermes Agent 只能升級至可辨識且符合跨版本升級設定的較新版本。如需還原舊版本,請使用備份還原。'
ErrAppWarn: '狀態異常,請檢視日誌' ErrAppWarn: '狀態異常,請檢視日誌'
ErrAppParamKey: '參數{{ .name }} 欄位異常' ErrAppParamKey: '參數{{ .name }} 欄位異常'
ErrAppUpgrade: '應用程式{{ .name }} 升級失敗{{ .err }}' ErrAppUpgrade: '應用程式{{ .name }} 升級失敗{{ .err }}'
@@ -300,6 +303,9 @@ BuildCache: '構建快取'
Volume: '磁碟區' Volume: '磁碟區'
Network: '網路' Network: '網路'
PruneStart: '清理中,請稍候...' PruneStart: '清理中,請稍候...'
NetworkCleanupConnected: "跳過網路 [{{ .name }}] ({{ .id }}):仍有容器連接或正在使用,未刪除"
NetworkCleanupSummary: "網路清理完成:已刪除 {{ .deleted }},已跳過 {{ .skipped }},失敗 {{ .failed }}"
NetworkCleanupPartialFailure: "部分網路清理失敗,請查看任務日誌"
ContainerBackupPrepare: '準備容器備份工作目錄' ContainerBackupPrepare: '準備容器備份工作目錄'
ContainerBackupStop: '備份前停止容器' ContainerBackupStop: '備份前停止容器'
ContainerBackupInspect: '備份容器 Inspect 與網路資訊' ContainerBackupInspect: '備份容器 Inspect 與網路資訊'
@@ -628,14 +634,15 @@ AppAttributesNotMatch: '應用程式類型或名稱不一致'
# 告警 # 告警
ErrAlertMethodNotSupported: '當前版本不支援此告警方式' ErrAlertMethodNotSupported: '當前版本不支援此告警方式'
ErrAlertSync: '警告訊息同步錯誤,請重試。' ErrAlertSync: '警告訊息同步錯誤,請重試。'
ErrWebhookTestFailed: '請檢查 Webhook 設定,詳細原因請查看系統日誌'
# 任務 - 執行環境 # 任務 - 執行環境
ErrInstallExtension: '已有安裝任務正在進行,請等待任務結束' ErrInstallExtension: '已有安裝任務正在進行,請等待任務結束'
# 告警郵件範本 # 告警郵件範本
PanelAlertTitle: '面板警示通知' PanelAlertTitle: '面板警示通知'
TestAlertTitle: '測試郵件 - 驗證信箱連通性' TestAlertTitle: '面板測試通知'
TestAlert: '這是一封測試郵件,用於確認您的郵件發送設定是否正確。' TestAlertMessage: '這是一則測試通知,用於驗證通知管道設定及連通性。'
LicenseExpirationAlert: '您的 {{ .node }}{{ .ip }} 面板,授權將於 {{ .day }} 天後到期,詳情請登入面板檢視。' LicenseExpirationAlert: '您的 {{ .node }}{{ .ip }} 面板,授權將於 {{ .day }} 天後到期,詳情請登入面板檢視。'
CronJobFailedAlert: '您的 {{ .node }}{{ .ip }} 面板,排程任務{{ .name }}執行失敗,詳情請登入面板檢視。' CronJobFailedAlert: '您的 {{ .node }}{{ .ip }} 面板,排程任務{{ .name }}執行失敗,詳情請登入面板檢視。'
ClamAlert: '您的 {{ .node }}{{ .ip }} 面板,病毒掃描任務偵測到 {{ .num }} 個感染檔案,詳情請登入面板檢視。' ClamAlert: '您的 {{ .node }}{{ .ip }} 面板,病毒掃描任務偵測到 {{ .num }} 個感染檔案,詳情請登入面板檢視。'
@@ -718,6 +725,8 @@ ErrDockerIPv4ForwardingDisabled: "IPv4 轉送尚未啟用,請先設定 net.ipv
ErrFirewallRuleSavedApplyFailed: "新規則設定已儲存,但套用至防火牆失敗。可透過同步重試:{{ .detail }}" ErrFirewallRuleSavedApplyFailed: "新規則設定已儲存,但套用至防火牆失敗。可透過同步重試:{{ .detail }}"
ErrFirewallRuleConflict: "UFW 中已存在比對條件相同但動作不同的規則,請編輯既有規則。" ErrFirewallRuleConflict: "UFW 中已存在比對條件相同但動作不同的規則,請編輯既有規則。"
ErrForwardInterfaceNotFound: "轉發規則指定的網卡 {{ .name }} 不存在,請檢查網卡名稱後重試。" ErrForwardInterfaceNotFound: "轉發規則指定的網卡 {{ .name }} 不存在,請檢查網卡名稱後重試。"
ErrIPv6RARisk: "IPv6 轉送可能導致網路中斷,已阻止啟用。請參閱文件,詳情見系統日誌。"
ErrIPv6RACheckFailed: "無法檢查 IPv6 網路設定,已阻止啟用轉送。詳情見系統日誌。"
ErrSSHPublicKey: "請輸入有效的 SSH 公鑰" ErrSSHPublicKey: "請輸入有效的 SSH 公鑰"
ErrSSHPrivateKey: "SSH 私鑰格式無效或密碼錯誤;加密私鑰需要填寫原密碼" ErrSSHPrivateKey: "SSH 私鑰格式無效或密碼錯誤;加密私鑰需要填寫原密碼"
+11 -2
View File
@@ -140,6 +140,9 @@ ErrComposeProjectNameEmpty: "无法从 Docker Compose 配置或文件父目录
ErrComposeNameInvalid: "Compose 项目名称格式错误:必须以小写字母或数字开头,仅支持小写字母、数字、- 和 _,长度为 1-256 个字符" ErrComposeNameInvalid: "Compose 项目名称格式错误:必须以小写字母或数字开头,仅支持小写字母、数字、- 和 _,长度为 1-256 个字符"
ErrAppVersionUnavailable: "当前应用版本已从远程服务下架,请选择其他版本后重试" ErrAppVersionUnavailable: "当前应用版本已从远程服务下架,请选择其他版本后重试"
ErrVllmGB10ProOnly: "该 vLLM 版本仅支持 1Panel 专业版" ErrVllmGB10ProOnly: "该 vLLM 版本仅支持 1Panel 专业版"
ErrAIGatewayDatabaseTypeMismatch: 'AI 网关只能在相同数据库类型(SQLite 或 PostgreSQL)之间升级。'
ErrAIGatewayRecoverDatabaseTypeMismatch: 'AI 网关备份与目标应用的数据库类型(SQLite 或 PostgreSQL)必须一致,无法跨数据库类型恢复。'
ErrHermesUpgradeVersion: 'Hermes Agent 只能升级到可识别且符合跨版本升级设置的更新版本。如需恢复旧版本,请使用备份恢复。'
ErrAppWarn: "状态异常,请查看日志" ErrAppWarn: "状态异常,请查看日志"
ErrAppParamKey: "参数 {{ .name }} 字段异常" ErrAppParamKey: "参数 {{ .name }} 字段异常"
ErrAppUpgrade: "应用 {{ .name }} 升级失败 {{ .err }}" ErrAppUpgrade: "应用 {{ .name }} 升级失败 {{ .err }}"
@@ -300,6 +303,9 @@ BuildCache: "构建缓存"
Volume: "存储卷" Volume: "存储卷"
Network: "网络" Network: "网络"
PruneStart: "清理中,请稍候..." PruneStart: "清理中,请稍候..."
NetworkCleanupConnected: "跳过网络 [{{ .name }}] ({{ .id }}):仍有容器连接或正在使用,未删除"
NetworkCleanupSummary: "网络清理完成:已删除 {{ .deleted }},已跳过 {{ .skipped }},失败 {{ .failed }}"
NetworkCleanupPartialFailure: "部分网络清理失败,请查看任务日志"
ContainerBackupPrepare: "准备容器备份工作目录" ContainerBackupPrepare: "准备容器备份工作目录"
ContainerBackupStop: "备份前停止容器" ContainerBackupStop: "备份前停止容器"
ContainerBackupInspect: "备份容器 Inspect 与网络信息" ContainerBackupInspect: "备份容器 Inspect 与网络信息"
@@ -628,14 +634,15 @@ AppAttributesNotMatch: "应用类型或名称不一致"
# 告警 # 告警
ErrAlertMethodNotSupported: "当前版本不支持此告警方式" ErrAlertMethodNotSupported: "当前版本不支持此告警方式"
ErrAlertSync: "告警信息同步错误,请检查后重试" ErrAlertSync: "告警信息同步错误,请检查后重试"
ErrWebhookTestFailed: "请检查 Webhook 配置,详细原因请查看系统日志"
# 任务 - 运行环境 # 任务 - 运行环境
ErrInstallExtension: "已有安装任务正在进行,请等待任务结束" ErrInstallExtension: "已有安装任务正在进行,请等待任务结束"
# 告警邮件模板 # 告警邮件模板
PanelAlertTitle: "面板告警通知" PanelAlertTitle: "面板告警通知"
TestAlertTitle: "测试邮件 - 验证邮箱连通性" TestAlertTitle: "面板测试通知"
TestAlert: "这是一封测试邮件,用于验证邮箱发件配置。" TestAlertMessage: "这是一条测试通知,用于验证通知渠道配置及连通性。"
LicenseExpirationAlert: "您的 {{ .node }}{{ .ip }} 面板,许可证将在 {{ .day }} 天后到期,请登录面板查看详情。" LicenseExpirationAlert: "您的 {{ .node }}{{ .ip }} 面板,许可证将在 {{ .day }} 天后到期,请登录面板查看详情。"
CronJobFailedAlert: "您的 {{ .node }}{{ .ip }} 面板,计划任务-{{ .name }}执行失败,请登录面板查看详情。" CronJobFailedAlert: "您的 {{ .node }}{{ .ip }} 面板,计划任务-{{ .name }}执行失败,请登录面板查看详情。"
ClamAlert: "您的 {{ .node }}{{ .ip }} 面板,病毒扫描任务发现 {{ .num }} 个感染文件,请登录面板查看详情。" ClamAlert: "您的 {{ .node }}{{ .ip }} 面板,病毒扫描任务发现 {{ .num }} 个感染文件,请登录面板查看详情。"
@@ -718,6 +725,8 @@ ErrDockerIPv4ForwardingDisabled: "IPv4 转发未开启,请先设置 net.ipv4.i
ErrFirewallRuleSavedApplyFailed: "新规则配置已保存,但应用到防火墙失败。可通过同步重试:{{ .detail }}" ErrFirewallRuleSavedApplyFailed: "新规则配置已保存,但应用到防火墙失败。可通过同步重试:{{ .detail }}"
ErrFirewallRuleConflict: "UFW 中已存在匹配条件相同但动作不同的规则,请编辑已有规则。" ErrFirewallRuleConflict: "UFW 中已存在匹配条件相同但动作不同的规则,请编辑已有规则。"
ErrForwardInterfaceNotFound: "转发规则指定的网卡 {{ .name }} 不存在,请检查网卡名称后重试。" ErrForwardInterfaceNotFound: "转发规则指定的网卡 {{ .name }} 不存在,请检查网卡名称后重试。"
ErrIPv6RARisk: "IPv6 转发存在断网风险,已阻止启用。请参阅文档,详情见系统日志。"
ErrIPv6RACheckFailed: "无法检查 IPv6 网络配置,已阻止启用转发。详情见系统日志。"
ErrSSHPublicKey: "请输入有效的 SSH 公钥" ErrSSHPublicKey: "请输入有效的 SSH 公钥"
ErrSSHPrivateKey: "SSH 私钥格式无效或密码错误;加密私钥需要填写原密码" ErrSSHPrivateKey: "SSH 私钥格式无效或密码错误;加密私钥需要填写原密码"
+3
View File
@@ -23,6 +23,9 @@ import (
func Init() { func Init() {
ctx := context.Background() ctx := context.Background()
defer initDockerPortGuard(ctx) defer initDockerPortGuard(ctx)
if err := service.SyncPanelPortWhitelist(); err != nil {
global.LOG.Warnf("synchronize panel port whitelist on startup failed, err: %v", err)
}
if err := initForwardingRules(ctx); err != nil { if err := initForwardingRules(ctx); err != nil {
global.LOG.Warnf("restore forwarding rules from file failed, err: %v", err) global.LOG.Warnf("restore forwarding rules from file failed, err: %v", err)
} }
+1
View File
@@ -113,6 +113,7 @@ func agentDBMigrations() []*gormigrate.Migration {
migrations.AddAcceleratorMetrics, migrations.AddAcceleratorMetrics,
migrations.AddVLLMMonitor, migrations.AddVLLMMonitor,
migrations.AddMonitorSettings, migrations.AddMonitorSettings,
migrations.AddVLLMRequestMetrics,
} }
} }
+7
View File
@@ -1874,3 +1874,10 @@ var AddMonitorSettings = &gormigrate.Migration{
return nil return nil
}, },
} }
var AddVLLMRequestMetrics = &gormigrate.Migration{
ID: "20261010-vllm-request-metrics",
Migrate: func(tx *gorm.DB) error {
return global.VLLMMonitorDB.AutoMigrate(&model.MonitorVLLM{})
},
}
+156
View File
@@ -0,0 +1,156 @@
package vllm
import (
"encoding/json"
"math"
"github.com/1Panel-dev/1Panel/agent/app/model"
)
type HistoryAccumulator struct {
histograms map[string]histogram
prefixHits float64
prefixQueries float64
finishReasons map[string]float64
finishSeconds map[string]float64
}
func (a *HistoryAccumulator) Add(histograms, requests, aggregation, group string) error {
if histograms != "" {
var sample map[string]histogram
if aggregation == "max" || group == "requests" {
var distributions struct {
Prompt *histogram `json:"request_prompt_tokens"`
Generation *histogram `json:"request_generation_tokens"`
}
if err := json.Unmarshal([]byte(histograms), &distributions); err != nil {
return err
}
sample = map[string]histogram{}
if distributions.Prompt != nil {
sample["request_prompt_tokens"] = *distributions.Prompt
}
if distributions.Generation != nil {
sample["request_generation_tokens"] = *distributions.Generation
}
} else if group == "latency" {
var latency struct {
FirstToken *histogram `json:"time_to_first_token_seconds"`
InterToken *histogram `json:"inter_token_latency_seconds"`
Request *histogram `json:"e2e_request_latency_seconds"`
Prefill *histogram `json:"request_prefill_time_seconds"`
Decode *histogram `json:"request_decode_time_seconds"`
Queue *histogram `json:"request_queue_time_seconds"`
}
if err := json.Unmarshal([]byte(histograms), &latency); err != nil {
return err
}
sample = map[string]histogram{}
for name, item := range map[string]*histogram{
"time_to_first_token_seconds": latency.FirstToken,
"inter_token_latency_seconds": latency.InterToken,
"e2e_request_latency_seconds": latency.Request,
"request_prefill_time_seconds": latency.Prefill,
"request_decode_time_seconds": latency.Decode,
"request_queue_time_seconds": latency.Queue,
} {
if item != nil {
sample[name] = *item
}
}
} else if err := json.Unmarshal([]byte(histograms), &sample); err != nil {
return err
}
if a.histograms == nil {
a.histograms = map[string]histogram{}
}
for name, item := range sample {
switch name {
case "request_prefill_time_seconds", "request_decode_time_seconds", "request_queue_time_seconds":
item.Buckets = nil
}
total, exists := a.histograms[name]
if !exists {
a.histograms[name] = item
continue
}
total.Seconds += item.Seconds
total.Sum += item.Sum
total.Count += item.Count
if len(total.Buckets) != len(item.Buckets) {
total.Buckets = nil
}
for bound, count := range total.Buckets {
addition, ok := item.Buckets[bound]
if !ok {
total.Buckets = nil
break
}
total.Buckets[bound] = count + addition
}
a.histograms[name] = total
}
}
if requests == "" {
return nil
}
if group == "overview" {
var prefix struct {
Seconds float64 `json:"seconds"`
Hits *float64 `json:"prefixHits"`
Queries *float64 `json:"prefixQueries"`
}
if err := json.Unmarshal([]byte(requests), &prefix); err != nil {
return err
}
if prefix.Seconds > 0 && prefix.Hits != nil && prefix.Queries != nil {
a.prefixHits += *prefix.Hits
a.prefixQueries += *prefix.Queries
}
return nil
}
var sample requestMetrics
if err := json.Unmarshal([]byte(requests), &sample); err != nil {
return err
}
if sample.Seconds <= 0 {
return nil
}
if group == "" && aggregation != "max" && sample.PrefixHits != nil && sample.PrefixQueries != nil {
a.prefixHits += *sample.PrefixHits
a.prefixQueries += *sample.PrefixQueries
}
for reason, count := range sample.FinishReasons {
if a.finishReasons == nil {
a.finishReasons = map[string]float64{}
}
if aggregation == "max" {
a.finishReasons[reason] = math.Max(a.finishReasons[reason], count/sample.Seconds)
} else {
if a.finishSeconds == nil {
a.finishSeconds = map[string]float64{}
}
a.finishReasons[reason] += count
a.finishSeconds[reason] += sample.Seconds
}
}
return nil
}
func (a *HistoryAccumulator) Apply(result *model.MonitorVLLM, aggregation string) {
applyHistograms(result, a.histograms, aggregation != "max")
for reason, count := range a.finishReasons {
if result.FinishReasons == nil {
result.FinishReasons = map[string]float64{}
}
if aggregation == "max" {
result.FinishReasons[reason] = count
} else {
result.FinishReasons[reason] = count / a.finishSeconds[reason]
}
}
if aggregation != "max" && a.prefixQueries > 0 {
rate := a.prefixHits / a.prefixQueries * 100
result.PrefixCacheHitRate = &rate
}
}
+36 -38
View File
@@ -17,6 +17,7 @@ import (
type Metrics map[string]map[string]float64 type Metrics map[string]map[string]float64
type histogram struct { type histogram struct {
Seconds float64 `json:"seconds,omitempty"`
Sum float64 `json:"sum"` Sum float64 `json:"sum"`
Count float64 `json:"count"` Count float64 `json:"count"`
Buckets map[string]float64 `json:"buckets,omitempty"` Buckets map[string]float64 `json:"buckets,omitempty"`
@@ -37,14 +38,16 @@ func Parse(reader io.Reader) (Metrics, error) {
} }
name := strings.TrimPrefix(match[1], "vllm:") name := strings.TrimPrefix(match[1], "vllm:")
switch name { switch name {
case "num_requests_running", "num_requests_waiting", "kv_cache_usage_perc", "gpu_cache_usage_perc", "prompt_tokens_total", "generation_tokens_total", "request_success_total": case "request_prefill_time_seconds_bucket", "request_decode_time_seconds_bucket", "request_queue_time_seconds_bucket":
continue
case "num_requests_running", "num_requests_waiting", "kv_cache_usage_perc", "gpu_cache_usage_perc", "prompt_tokens_total", "generation_tokens_total", "request_success_total", "prefix_cache_hits_total", "prefix_cache_queries_total":
default: default:
base := strings.TrimSuffix(strings.TrimSuffix(strings.TrimSuffix(name, "_sum"), "_count"), "_bucket") base := strings.TrimSuffix(strings.TrimSuffix(strings.TrimSuffix(name, "_sum"), "_count"), "_bucket")
if base == name { if base == name {
continue continue
} }
switch base { switch base {
case "time_to_first_token_seconds", "time_per_output_token_seconds", "inter_token_latency_seconds", "e2e_request_latency_seconds", "request_prefill_time_seconds", "request_decode_time_seconds": case "time_to_first_token_seconds", "time_per_output_token_seconds", "inter_token_latency_seconds", "e2e_request_latency_seconds", "request_prefill_time_seconds", "request_decode_time_seconds", "request_queue_time_seconds", "request_prompt_tokens", "request_generation_tokens":
default: default:
continue continue
} }
@@ -117,7 +120,7 @@ func Calculate(current, previous Metrics, elapsed float64) (model.MonitorVLLM, e
} }
} }
histograms := map[string]histogram{} histograms := map[string]histogram{}
for _, name := range []string{"time_to_first_token_seconds", "inter_token_latency_seconds", "e2e_request_latency_seconds", "request_prefill_time_seconds", "request_decode_time_seconds"} { for _, name := range []string{"time_to_first_token_seconds", "inter_token_latency_seconds", "e2e_request_latency_seconds", "request_prefill_time_seconds", "request_decode_time_seconds", "request_queue_time_seconds", "request_prompt_tokens", "request_generation_tokens"} {
source := name source := name
if name == "inter_token_latency_seconds" && len(current[name+"_count"]) == 0 { if name == "inter_token_latency_seconds" && len(current[name+"_count"]) == 0 {
source = "time_per_output_token_seconds" source = "time_per_output_token_seconds"
@@ -127,9 +130,21 @@ func Calculate(current, previous Metrics, elapsed float64) (model.MonitorVLLM, e
if sum == nil || count == nil { if sum == nil || count == nil {
continue continue
} }
histograms[name] = histogram{Sum: *sum, Count: *count, Buckets: bucketDeltas(current[source+"_bucket"], previous[source+"_bucket"], *count)} item := histogram{Sum: *sum, Count: *count}
switch name {
case "request_prefill_time_seconds", "request_decode_time_seconds", "request_queue_time_seconds":
default:
item.Buckets = bucketDeltas(current[source+"_bucket"], previous[source+"_bucket"], *count)
}
if name == "request_prompt_tokens" || name == "request_generation_tokens" {
item.Seconds = elapsed
}
histograms[name] = item
}
applyHistograms(&result, histograms, true)
if err := calculateRequestMetrics(current, previous, elapsed, &result); err != nil {
return model.MonitorVLLM{}, err
} }
applyHistograms(&result, histograms)
encoded, err := json.Marshal(histograms) encoded, err := json.Marshal(histograms)
if err != nil { if err != nil {
return model.MonitorVLLM{}, err return model.MonitorVLLM{}, err
@@ -138,43 +153,24 @@ func Calculate(current, previous Metrics, elapsed float64) (model.MonitorVLLM, e
return result, nil return result, nil
} }
func AggregateHistograms(encoded string, result *model.MonitorVLLM) error { func applyHistograms(result *model.MonitorVLLM, histograms map[string]histogram, includeMeans bool) {
var samples []map[string]histogram for name, target := range map[string]*map[string]float64{
if err := json.Unmarshal([]byte(encoded), &samples); err != nil { "request_prompt_tokens": &result.PromptDistribution,
return err "request_generation_tokens": &result.GenerationDistribution,
} } {
merged := map[string]histogram{} item := histograms[name]
for _, sample := range samples { if item.Seconds <= 0 || len(item.Buckets) == 0 {
for name, item := range sample {
if item.Count == 0 {
continue continue
} }
total, exists := merged[name] result.IntervalSeconds = math.Max(result.IntervalSeconds, item.Seconds)
if !exists { *target = make(map[string]float64, len(item.Buckets))
merged[name] = item for bound, count := range item.Buckets {
continue (*target)[bound] = count / item.Seconds
}
total.Sum += item.Sum
total.Count += item.Count
if len(total.Buckets) != len(item.Buckets) {
total.Buckets = nil
}
for bound, count := range total.Buckets {
addition, ok := item.Buckets[bound]
if !ok {
total.Buckets = nil
break
}
total.Buckets[bound] = count + addition
}
merged[name] = total
} }
} }
applyHistograms(result, merged) if !includeMeans {
return nil return
} }
func applyHistograms(result *model.MonitorVLLM, histograms map[string]histogram) {
for _, item := range []struct { for _, item := range []struct {
name string name string
mean **float64 mean **float64
@@ -185,6 +181,8 @@ func applyHistograms(result *model.MonitorVLLM, histograms map[string]histogram)
{"e2e_request_latency_seconds", &result.RequestLatency, []**float64{&result.RequestLatencyP50, &result.RequestLatencyP90, &result.RequestLatencyP95, &result.RequestLatencyP99}}, {"e2e_request_latency_seconds", &result.RequestLatency, []**float64{&result.RequestLatencyP50, &result.RequestLatencyP90, &result.RequestLatencyP95, &result.RequestLatencyP99}},
{"request_prefill_time_seconds", &result.PrefillTime, nil}, {"request_prefill_time_seconds", &result.PrefillTime, nil},
{"request_decode_time_seconds", &result.DecodeTime, nil}, {"request_decode_time_seconds", &result.DecodeTime, nil},
{"request_queue_time_seconds", &result.QueueTime, nil},
{"request_generation_tokens", &result.AverageGenerationTokens, nil},
} { } {
histogram, ok := histograms[item.name] histogram, ok := histograms[item.name]
if !ok || histogram.Count <= 0 { if !ok || histogram.Count <= 0 {
@@ -0,0 +1,65 @@
package vllm
import (
"encoding/json"
"strconv"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/utils/re"
)
type requestMetrics struct {
Seconds float64 `json:"seconds"`
PrefixHits *float64 `json:"prefixHits,omitempty"`
PrefixQueries *float64 `json:"prefixQueries,omitempty"`
FinishReasons map[string]float64 `json:"finishReasons,omitempty"`
}
func calculateRequestMetrics(current, previous Metrics, elapsed float64, result *model.MonitorVLLM) error {
sample := requestMetrics{Seconds: elapsed, FinishReasons: map[string]float64{}}
hits := counterDelta(current["prefix_cache_hits_total"], previous["prefix_cache_hits_total"])
queries := counterDelta(current["prefix_cache_queries_total"], previous["prefix_cache_queries_total"])
if hits != nil && queries != nil && *hits <= *queries {
sample.PrefixHits, sample.PrefixQueries = hits, queries
if *queries > 0 {
rate := *hits / *queries * 100
result.PrefixCacheHitRate = &rate
}
}
before := groupFinishReasons(previous["request_success_total"])
for reason, values := range groupFinishReasons(current["request_success_total"]) {
if delta := counterDelta(values, before[reason]); delta != nil {
sample.FinishReasons[reason] = *delta
if result.FinishReasons == nil {
result.FinishReasons = map[string]float64{}
}
result.FinishReasons[reason] = *delta / elapsed
}
}
encoded, err := json.Marshal(sample)
if err != nil {
return err
}
result.RequestDeltas = string(encoded)
return nil
}
func groupFinishReasons(samples map[string]float64) map[string]map[string]float64 {
groups := map[string]map[string]float64{}
for labels, value := range samples {
for _, label := range re.GetRegex(re.VLLMMetricLabelPattern).FindAllStringSubmatch(labels, -1) {
if label[1] != "finished_reason" {
continue
}
reason, err := strconv.Unquote(label[2])
if err != nil || reason == "" {
continue
}
if groups[reason] == nil {
groups[reason] = map[string]float64{}
}
groups[reason][labels] = value
}
}
return groups
}
+5 -17
View File
@@ -139,33 +139,21 @@ func TestCustomWebhook(
resolved dto.AlertCustomWebhookResolvedConfig, resolved dto.AlertCustomWebhookResolvedConfig,
transport *http.Transport, transport *http.Transport,
agentInfo *dto.AgentInfo, agentInfo *dto.AgentInfo,
lang string,
) (dto.AlertConfigTestResult, error) { ) (dto.AlertConfigTestResult, error) {
request, err := buildResolvedCustomWebhookRequest(resolved, webhook_sender.TemplateData{ request, err := buildResolvedCustomWebhookRequest(resolved, webhook_sender.TemplateData{
Title: "1Panel Webhook Test", Title: i18n.GetMsgByKeyAndLang(lang, "TestAlertTitle"),
Message: "This is a test notification from 1Panel.", Message: i18n.GetMsgByKeyAndLang(lang, "TestAlertMessage"),
Type: "test", Type: "test",
NodeName: customWebhookNodeName(agentInfo), NodeName: customWebhookNodeName(agentInfo),
Timestamp: customWebhookNow(), Timestamp: customWebhookNow(),
}, transport) }, transport)
if err != nil { if err != nil {
return dto.AlertConfigTestResult{}, err return CustomWebhookTestResult(webhook_sender.Request{}, webhook_sender.Result{}, err), nil
} }
request.CaptureResponse = true request.CaptureResponse = true
result, executeErr := webhook_sender.Execute(context.Background(), request) result, executeErr := webhook_sender.Execute(context.Background(), request)
durationMillis := result.Duration.Milliseconds() return CustomWebhookTestResult(request, result, executeErr), nil
if result.Duration > 0 && durationMillis == 0 {
durationMillis = 1
}
testResult := dto.AlertConfigTestResult{
Success: executeErr == nil,
StatusCode: result.StatusCode,
Duration: durationMillis,
Response: result.Response,
}
if executeErr != nil {
testResult.Message = executeErr.Error()
}
return testResult, nil
} }
func buildResolvedCustomWebhookRequest( func buildResolvedCustomWebhookRequest(
@@ -0,0 +1,29 @@
package alert
import (
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/webhook_sender"
)
func CustomWebhookTestResult(request webhook_sender.Request, result webhook_sender.Result, executeErr error) dto.AlertConfigTestResult {
durationMillis := result.Duration.Milliseconds()
if result.Duration > 0 && durationMillis == 0 {
durationMillis = 1
}
testResult := dto.AlertConfigTestResult{
Success: executeErr == nil,
StatusCode: result.StatusCode,
Duration: durationMillis,
}
if executeErr == nil {
return testResult
}
if global.LOG != nil {
global.LOG.Warnf("webhook test failed: statusCode=%d durationMs=%d error=%q response=%q",
result.StatusCode, durationMillis,
webhook_sender.DiagnosticText(executeErr.Error(), request),
webhook_sender.DiagnosticText(result.Response, request))
}
return testResult
}
+13
View File
@@ -251,6 +251,10 @@ func (c *CommandHelper) buildPipeCommands(ctx context.Context, commands []PipeCo
cmdItem.SysProcAttr = &syscall.SysProcAttr{ cmdItem.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true, Setpgid: true,
} }
cmdItem.Cancel = func() error {
_ = syscall.Kill(-cmdItem.Process.Pid, syscall.SIGKILL)
return cmdItem.Process.Kill()
}
cmds = append(cmds, cmdItem) cmds = append(cmds, cmdItem)
} }
return cmds return cmds
@@ -335,6 +339,12 @@ func (c *CommandHelper) run(name string, arg ...string) (string, error) {
cmd.SysProcAttr = &syscall.SysProcAttr{ cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true, Setpgid: true,
} }
if newContext != nil {
cmd.Cancel = func() error {
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
return cmd.Process.Kill()
}
}
customWriter := &CustomWriter{taskItem: c.taskItem} customWriter := &CustomWriter{taskItem: c.taskItem}
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
@@ -398,6 +408,9 @@ func (c *CommandHelper) run(name string, arg ...string) (string, error) {
}() }()
select { select {
case err := <-done: case err := <-done:
if newContext != nil && newContext.Err() != nil {
return "", c.pipeResultErr(newContext, err)
}
if err != nil { if err != nil {
return handleErr(&stdout, &stderr, c.IgnoreExist1, err) return handleErr(&stdout, &stderr, c.IgnoreExist1, err)
} }
+25 -38
View File
@@ -7,43 +7,20 @@ import (
"path" "path"
"time" "time"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/glebarez/sqlite" "github.com/glebarez/sqlite"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/logger" "gorm.io/gorm/logger"
) )
func LoadDBConnByPath(fullPath, dbName string) *gorm.DB { func LoadDBConnByPath(fullPath, dbName string) *gorm.DB {
if _, err := createDirWhenNotExist(true, path.Dir(fullPath)); err != nil {
panic(fmt.Errorf("init db dir failed, err: %v", err))
}
if _, err := os.Stat(fullPath); err != nil {
f, err := os.Create(fullPath)
if err != nil {
panic(fmt.Errorf("init %s db file failed, err: %v", dbName, err))
}
_ = f.Close()
}
db, err := GetDBWithPath(fullPath) db, err := GetDBWithPath(fullPath)
if err != nil { if err != nil {
panic(err) panic(fmt.Errorf("init %s db failed, err: %w", dbName, err))
} }
return db return db
} }
func LoadDBConnByPathWithErr(fullPath, dbName string) (*gorm.DB, error) { func LoadDBConnByPathWithErr(fullPath, dbName string) (*gorm.DB, error) {
if _, err := createDirWhenNotExist(true, path.Dir(fullPath)); err != nil {
return nil, fmt.Errorf("init db dir failed, err: %v", err)
}
if _, err := os.Stat(fullPath); err != nil {
f, err := os.Create(fullPath)
if err != nil {
return nil, fmt.Errorf("init %s db file failed, err: %v", dbName, err)
}
_ = f.Close()
}
db, err := GetDBWithPath(fullPath) db, err := GetDBWithPath(fullPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("init %s db failed, err: %v", dbName, err) return nil, fmt.Errorf("init %s db failed, err: %v", dbName, err)
@@ -60,6 +37,30 @@ func CloseDB(db *gorm.DB) {
} }
func GetDBWithPath(dbPath string) (*gorm.DB, error) { func GetDBWithPath(dbPath string) (*gorm.DB, error) {
dbDir := path.Dir(dbPath)
if err := os.MkdirAll(dbDir, 0700); err != nil {
return nil, fmt.Errorf("init db dir failed, err: %w", err)
}
if err := os.Chmod(dbDir, 0700); err != nil {
return nil, fmt.Errorf("set db dir permissions failed, err: %w", err)
}
file, err := os.OpenFile(dbPath, os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
return nil, fmt.Errorf("init db file failed, err: %w", err)
}
if err := file.Chmod(0600); err != nil {
_ = file.Close()
return nil, fmt.Errorf("set db file permissions failed, err: %w", err)
}
if err := file.Close(); err != nil {
return nil, err
}
for _, suffix := range []string{"-wal", "-shm", "-journal"} {
if err := os.Chmod(dbPath+suffix, 0600); err != nil && !os.IsNotExist(err) {
return nil, fmt.Errorf("set db sidecar permissions failed, err: %w", err)
}
}
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{ db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true, DisableForeignKeyConstraintWhenMigrating: true,
Logger: newLogger(), Logger: newLogger(),
@@ -104,17 +105,3 @@ func newLogger() logger.Interface {
}, },
) )
} }
func createDirWhenNotExist(isDir bool, pathItem string) (string, error) {
checkPath := pathItem
if !isDir {
checkPath = path.Dir(pathItem)
}
if _, err := os.Stat(checkPath); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(checkPath, os.ModePerm); err != nil {
global.LOG.Errorf("mkdir %s failed, err: %v", checkPath, err)
return pathItem, err
}
}
return pathItem, nil
}
+3
View File
@@ -158,6 +158,9 @@ func GetComposeImages(filePath string, projectName ...string) ([]string, error)
if err == nil { if err == nil {
return images, nil return images, nil
} }
if strings.Contains(filePath, ",") {
return nil, err
}
content, readErr := os.ReadFile(filePath) content, readErr := os.ReadFile(filePath)
if readErr != nil { if readErr != nil {
+10 -1
View File
@@ -92,6 +92,9 @@ func dockerPolicyRuleKey(tokens []string, backend string) string {
if address, err := netip.ParseAddr(value); err == nil { if address, err := netip.ParseAddr(value); err == nil {
value = netip.PrefixFrom(address, address.BitLen()).String() value = netip.PrefixFrom(address, address.BitLen()).String()
} else if prefix, err := netip.ParsePrefix(value); err == nil { } else if prefix, err := netip.ParsePrefix(value); err == nil {
if prefix.Bits() == 0 {
continue
}
value = prefix.Masked().String() value = prefix.Masked().String()
} }
} }
@@ -156,7 +159,13 @@ func parseDockerGuardPolicies(output, family string) (PolicyInventory, error) {
group.acceptedSource = append(group.acceptedSource, source) group.acceptedSource = append(group.acceptedSource, source)
case action == "accept": case action == "accept":
group.acceptAll = true group.acceptAll = true
case action == "return" && source != "": case action == "return":
if source == "" {
source = "0.0.0.0/0"
if family == FamilyIPv6 {
source = "::/0"
}
}
group.allowedSource = append(group.allowedSource, source) group.allowedSource = append(group.allowedSource, source)
case action == "drop" && source != "": case action == "drop" && source != "":
group.droppedSource = append(group.droppedSource, source) group.droppedSource = append(group.droppedSource, source)
@@ -50,7 +50,7 @@ func (a *Adapter) CheckRule(ctx context.Context, rule filter.FirewallRule) error
if rule.Priority == nil || *rule.Priority == 0 { if rule.Priority == nil || *rule.Priority == 0 {
return nil return nil
} }
supported, err := a.supportsRichRulePriority(ctx) supported, err := a.SupportsRichRulePriority(ctx)
if err != nil { if err != nil {
return err return err
} }
@@ -60,7 +60,7 @@ func (a *Adapter) CheckRule(ctx context.Context, rule filter.FirewallRule) error
return nil return nil
} }
func (a *Adapter) supportsRichRulePriority(ctx context.Context) (bool, error) { func (a *Adapter) SupportsRichRulePriority(ctx context.Context) (bool, error) {
a.prioritySupportMu.Lock() a.prioritySupportMu.Lock()
defer a.prioritySupportMu.Unlock() defer a.prioritySupportMu.Unlock()
if a.richRulePrioritySupported != nil { if a.richRulePrioritySupported != nil {
@@ -82,6 +82,52 @@ func (a *Adapter) supportsRichRulePriority(ctx context.Context) (bool, error) {
return supported, nil return supported, nil
} }
func RemoveRichRulePriority(raw string) (string, error) {
if strings.ContainsAny(raw, "\r\n\x00") {
return "", filter.ErrInvalidRule
}
parts := make([]string, 0)
removed := false
for start := 0; start < len(raw); {
if raw[start] == ' ' || raw[start] == '\t' {
start++
continue
}
end, quote := start, byte(0)
for end < len(raw) {
char := raw[end]
if char == '\\' && quote != '\'' {
end += 2
continue
}
if quote != 0 {
if char == quote {
quote = 0
}
} else if char == '\'' || char == '"' {
quote = char
} else if char == ' ' || char == '\t' {
break
}
end++
}
if quote != 0 || end > len(raw) {
return "", filter.ErrInvalidRule
}
token := raw[start:end]
if strings.HasPrefix(token, "priority=") {
removed = true
} else {
parts = append(parts, token)
}
start = end
}
if !removed || len(parts) == 0 || parts[0] != "rule" {
return raw, nil
}
return strings.Join(parts, " "), nil
}
func parseFirewalldVersion(output string) (int, int, error) { func parseFirewalldVersion(output string) (int, int, error) {
version := strings.TrimSpace(output) version := strings.TrimSpace(output)
parts := strings.Split(version, ".") parts := strings.Split(version, ".")
+124 -13
View File
@@ -1,6 +1,7 @@
package forwarding package forwarding
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"os" "os"
@@ -8,19 +9,27 @@ import (
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/1Panel-dev/1Panel/agent/buserr"
"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/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/docker/docker/api/types/network"
) )
func ensureFamilyForwardingSysctls(system forwardingSystem, family string) error { func ensureFamilyForwardingSysctls(system forwardingSystem, family string) error {
withIPv6 := family == FamilyIPv6 withIPv6 := family == FamilyIPv6
if withIPv6 { if withIPv6 {
interfaces, err := IPv6RAInterfaces(system.ReadFile) interfaces, err := IPv6RAInterfaces(system.ReadFile, LoadDockerIPv4BridgePorts)
if err != nil { if err != nil {
return fmt.Errorf("check IPv6 Router Advertisement: %w", err) global.LOG.Warnf("IPv6 forwarding blocked: failed to check Router Advertisement: %v", err)
return buserr.New("ErrIPv6RACheckFailed")
} }
if len(interfaces) > 0 { if len(interfaces) > 0 {
return fmt.Errorf("IPv6 forwarding blocked: interfaces %s may depend on RA/SLAAC with accept_ra=1; persist accept_ra=2 on interfaces that require RA before retrying", strings.Join(interfaces, ", ")) global.LOG.Warnf("IPv6 forwarding blocked: interfaces %s may depend on RA/SLAAC with accept_ra=1; review the network configuration and persist accept_ra=2 on interfaces that require RA before retrying", strings.Join(interfaces, ", "))
return buserr.New("ErrIPv6RARisk")
} }
} }
paths := []string{"/proc/sys/net/ipv4/ip_forward"} paths := []string{"/proc/sys/net/ipv4/ip_forward"}
@@ -43,7 +52,7 @@ func ensureFamilyForwardingSysctls(system forwardingSystem, family string) error
return nil return nil
} }
func IPv6RAInterfaces(readFile func(string) ([]byte, error)) ([]string, error) { func IPv6RAInterfaces(readFile func(string) ([]byte, error), loadDockerPorts func() (map[string]bool, error)) ([]string, error) {
addresses, err := readFile("/proc/net/if_inet6") addresses, err := readFile("/proc/net/if_inet6")
if err != nil { if err != nil {
return nil, err return nil, err
@@ -67,21 +76,16 @@ func IPv6RAInterfaces(readFile func(string) ([]byte, error)) ([]string, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if _, exists := globalAddresses[name]; !exists { globalAddresses[name] = globalAddresses[name] || fields[3] == "00"
globalAddresses[name] = false if fields[3] == "00" && flags&permanentAddress == 0 {
}
if fields[3] == "00" {
globalAddresses[name] = true
if flags&permanentAddress == 0 {
raInterfaces[name] = true raInterfaces[name] = true
} }
} }
}
routes, err := readFile("/proc/net/ipv6_route") routes, err := readFile("/proc/net/ipv6_route")
if err != nil { if err != nil {
return nil, err return nil, err
} }
const raRouteFlags = 0x00010000 | 0x00800000 const raRouteFlags = 0x00010000 | 0x00080000 | 0x00800000
for _, line := range strings.Split(string(routes), "\n") { for _, line := range strings.Split(string(routes), "\n") {
fields := strings.Fields(line) fields := strings.Fields(line)
if len(fields) == 0 { if len(fields) == 0 {
@@ -98,12 +102,15 @@ func IPv6RAInterfaces(readFile func(string) ([]byte, error)) ([]string, error) {
raInterfaces[fields[9]] = true raInterfaces[fields[9]] = true
} }
} }
uncertainInterfaces := make(map[string]bool)
for name, hasGlobalAddress := range globalAddresses { for name, hasGlobalAddress := range globalAddresses {
if !hasGlobalAddress { if !hasGlobalAddress && !raInterfaces[name] {
raInterfaces[name] = true raInterfaces[name] = true
uncertainInterfaces[name] = true
} }
} }
var interfaces []string var interfaces []string
checkDockerPorts := false
for name := range raInterfaces { for name := range raInterfaces {
if name == "lo" { if name == "lo" {
continue continue
@@ -118,15 +125,119 @@ func IPv6RAInterfaces(readFile func(string) ([]byte, error)) ([]string, error) {
switch strings.TrimSpace(string(value)) { switch strings.TrimSpace(string(value)) {
case "1": case "1":
interfaces = append(interfaces, name) interfaces = append(interfaces, name)
checkDockerPorts = checkDockerPorts || uncertainInterfaces[name]
case "0", "2": case "0", "2":
default: default:
return nil, fmt.Errorf("invalid accept_ra value for %s", name) return nil, fmt.Errorf("invalid accept_ra value for %s", name)
} }
} }
if checkDockerPorts {
ports, err := loadDockerPorts()
if err != nil {
global.LOG.Warnf("IPv6 forwarding RA check: cannot exclude Docker bridge ports: %v", err)
} else {
kept := interfaces[:0]
for _, name := range interfaces {
if !uncertainInterfaces[name] || !ports[name] {
kept = append(kept, name)
}
}
interfaces = kept
}
}
sort.Strings(interfaces) sort.Strings(interfaces)
return interfaces, nil return interfaces, nil
} }
func LoadDockerIPv4BridgePorts() (map[string]bool, error) {
cli, err := docker.NewDockerClient()
if err != nil {
return nil, err
}
defer cli.Close()
if !strings.HasPrefix(cli.DaemonHost(), "unix://") {
return nil, errors.New("cannot verify local bridge ports using a remote Docker daemon")
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
info, err := cli.Info(ctx)
if err != nil {
return nil, err
}
for _, option := range info.SecurityOptions {
if option == "name=rootless" {
return nil, errors.New("cannot verify host bridge ports using a rootless Docker daemon")
}
}
networks, err := cli.NetworkList(ctx, network.ListOptions{})
if err != nil {
return nil, err
}
command := cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(5*time.Second))
bridges, err := command.RunWithOptionalSudoAndStdout("ip", "-o", "link", "show", "type", "bridge")
if err != nil {
return nil, err
}
ports, err := command.RunWithOptionalSudoAndStdout("ip", "-o", "link", "show", "type", "veth")
if err != nil {
return nil, err
}
return dockerIPv4BridgePorts(networks, bridges, ports)
}
func dockerIPv4BridgePorts(networks []network.Summary, bridgeOutput, portOutput string) (map[string]bool, error) {
bridges := make(map[string]bool)
for _, item := range networks {
if item.Driver != "bridge" || item.EnableIPv6 || item.ConfigOnly {
continue
}
name := item.Options["com.docker.network.bridge.name"]
if name == "" {
if item.Options["com.docker.network.bridge.default_bridge"] == "true" {
name = "docker0"
} else if len(item.ID) >= 12 {
name = "br-" + item.ID[:12]
}
}
if name != "" {
bridges[name] = true
}
}
confirmedBridges := make(map[string]bool)
ports := make(map[string]bool)
for index, output := range []string{bridgeOutput, portOutput} {
for _, line := range strings.Split(output, "\n") {
header, _, _ := strings.Cut(line, "\\")
fields := strings.Fields(header)
if len(fields) == 0 {
continue
}
if len(fields) < 3 || !strings.HasSuffix(fields[0], ":") || !strings.HasSuffix(fields[1], ":") {
return nil, fmt.Errorf("invalid network link entry: %q", line)
}
if _, err := strconv.Atoi(strings.TrimSuffix(fields[0], ":")); err != nil {
return nil, fmt.Errorf("invalid network link entry: %q", line)
}
name, _, _ := strings.Cut(strings.TrimSuffix(fields[1], ":"), "@")
if index == 0 {
if bridges[name] {
confirmedBridges[name] = true
}
continue
}
for i := 3; i+1 < len(fields); i++ {
if fields[i] == "master" {
if confirmedBridges[fields[i+1]] {
ports[name] = true
}
break
}
}
}
}
return ports, nil
}
func enableFamilyForwardingSysctls(content, family string) string { func enableFamilyForwardingSysctls(content, family string) string {
lines := strings.Split(strings.TrimRight(content, "\n"), "\n") lines := strings.Split(strings.TrimRight(content, "\n"), "\n")
wanted := map[string]string{"net.ipv4.ip_forward": "net.ipv4.ip_forward = 1"} wanted := map[string]string{"net.ipv4.ip_forward": "net.ipv4.ip_forward = 1"}
@@ -209,18 +209,36 @@ func buildBaseChainBindingsRestoreScript(output string, bind bool) string {
} }
func baseChainBindingCommands(output string, bind bool) []string { func baseChainBindingCommands(output string, bind bool) []string {
lines := make([]string, 0, len(BasicChains())*2) chains := BasicChains()
for _, chain := range BasicChains() { lines := make([]string, 0, len(chains)*2)
binding := "-A " + InputChain + " -j " + chain position, insertAt := 0, 1
for _, current := range strings.Split(output, "\n") { for _, current := range strings.Split(output, "\n") {
if strings.TrimSpace(current) == binding { current = strings.TrimSpace(current)
if !strings.HasPrefix(current, "-A "+InputChain+" ") {
continue
}
if chain := strings.TrimPrefix(current, "-A "+InputChain+" -j "); slices.Contains(chains, chain) {
lines = append(lines, "-D "+InputChain+" -j "+chain) lines = append(lines, "-D "+InputChain+" -j "+chain)
continue
}
position++
if !bind {
continue
}
fields, err := shellwords.Parse(current)
if err != nil {
continue
}
for index := 2; index+1 < len(fields); index++ {
if fields[index] == "-j" && strings.HasPrefix(fields[index+1], "f2b-") {
insertAt = position + 1
break
} }
} }
} }
if bind { if bind {
for index, chain := range BasicChains() { for index, chain := range chains {
lines = append(lines, fmt.Sprintf("-I %s %d -j %s", InputChain, index+1, chain)) lines = append(lines, fmt.Sprintf("-I %s %d -j %s", InputChain, insertAt+index, chain))
} }
} }
return lines return lines
+11
View File
@@ -25,6 +25,7 @@ import (
"github.com/go-acme/lego/v5/providers/dns/namecheap" "github.com/go-acme/lego/v5/providers/dns/namecheap"
"github.com/go-acme/lego/v5/providers/dns/namedotcom" "github.com/go-acme/lego/v5/providers/dns/namedotcom"
"github.com/go-acme/lego/v5/providers/dns/namesilo" "github.com/go-acme/lego/v5/providers/dns/namesilo"
"github.com/go-acme/lego/v5/providers/dns/netcup"
"github.com/go-acme/lego/v5/providers/dns/ovh" "github.com/go-acme/lego/v5/providers/dns/ovh"
"github.com/go-acme/lego/v5/providers/dns/porkbun" "github.com/go-acme/lego/v5/providers/dns/porkbun"
"github.com/go-acme/lego/v5/providers/dns/rainyun" "github.com/go-acme/lego/v5/providers/dns/rainyun"
@@ -58,6 +59,7 @@ const (
NameCheap DnsType = "NameCheap" NameCheap DnsType = "NameCheap"
NameCom DnsType = "NameCom" NameCom DnsType = "NameCom"
NameSilo DnsType = "NameSilo" NameSilo DnsType = "NameSilo"
Netcup DnsType = "Netcup"
Ovh DnsType = "Ovh" Ovh DnsType = "Ovh"
PorkBun DnsType = "PorkBun" PorkBun DnsType = "PorkBun"
RainYun DnsType = "RainYun" RainYun DnsType = "RainYun"
@@ -92,6 +94,7 @@ type DNSParam struct {
Endpoint string `json:"endpoint"` Endpoint string `json:"endpoint"`
AccessToken string `json:"accessToken"` AccessToken string `json:"accessToken"`
BaseURL string `json:"baseURL"` BaseURL string `json:"baseURL"`
CustomerNumber string `json:"customerNumber"`
} }
var ( var (
@@ -164,6 +167,14 @@ func getDNSProviderConfig(dnsType DnsType, params string, httpClient *http.Clien
config.PropagationTimeout = propagationTimeout config.PropagationTimeout = propagationTimeout
config.PollingInterval = pollingInterval config.PollingInterval = pollingInterval
p, err = namesilo.NewDNSProviderConfig(config) p, err = namesilo.NewDNSProviderConfig(config)
case Netcup:
config := newDNSProviderConfig(netcup.NewDefaultConfig(), httpClient)
config.Customer = param.CustomerNumber
config.Key = param.APIkey
config.Password = param.Password
config.PropagationTimeout = propagationTimeout
config.PollingInterval = pollingInterval
p, err = netcup.NewDNSProviderConfig(config)
case Godaddy: case Godaddy:
config := newDNSProviderConfig(godaddy.NewDefaultConfig(), httpClient) config := newDNSProviderConfig(godaddy.NewDefaultConfig(), httpClient)
config.APIKey = param.APIkey config.APIKey = param.APIkey
+99 -23
View File
@@ -142,7 +142,7 @@ func Execute(ctx context.Context, input Request) (Result, error) {
} }
if err := validateResponse(input.Preset, resp.StatusCode, responseBody); err != nil { if err := validateResponse(input.Preset, resp.StatusCode, responseBody); err != nil {
return result, err return result, safeResponseError(err, input, target)
} }
return result, nil return result, nil
} }
@@ -332,6 +332,14 @@ func (b *proxyConnectionBody) Close() error {
} }
func captureResponse(body []byte, input Request, target preparedTarget) string { func captureResponse(body []byte, input Request, target preparedTarget) string {
var document any
decoder := json.NewDecoder(bytes.NewReader(body))
decoder.UseNumber()
if json.Valid(body) && decoder.Decode(&document) == nil {
if canonical, err := json.Marshal(document); err == nil {
body = canonical
}
}
response := strings.ToValidUTF8(string(body), "\uFFFD") response := strings.ToValidUTF8(string(body), "\uFFFD")
redactions := make([]string, 0, len(input.Headers)+8) redactions := make([]string, 0, len(input.Headers)+8)
redactions = append(redactions, urlRedactionValues(input.URL)...) redactions = append(redactions, urlRedactionValues(input.URL)...)
@@ -345,17 +353,105 @@ func captureResponse(body []byte, input Request, target preparedTarget) string {
} }
redactions = append(redactions, headerRedactionValues(name, value)...) redactions = append(redactions, headerRedactionValues(name, value)...)
} }
redactions = append(redactions, bodyRedactionValues(input)...)
for _, value := range append([]string(nil), redactions...) {
if encoded, err := json.Marshal(value); err == nil && len(encoded) > 2 {
redactions = append(redactions, string(encoded[1:len(encoded)-1]))
}
}
sort.SliceStable(redactions, func(i, j int) bool { sort.SliceStable(redactions, func(i, j int) bool {
return len(redactions[i]) > len(redactions[j]) return len(redactions[i]) > len(redactions[j])
}) })
seen := make(map[string]struct{}, len(redactions))
replacements := make([]string, 0, len(redactions)*2)
for _, value := range redactions { for _, value := range redactions {
if value != "" { if value == "" {
response = strings.ReplaceAll(response, value, "[REDACTED]") continue
} }
if _, exists := seen[value]; exists {
continue
}
seen[value] = struct{}{}
replacements = append(replacements, value, "[REDACTED]")
}
if len(replacements) > 0 {
response = strings.NewReplacer(replacements...).Replace(response)
} }
return truncateUTF8(response, MaxCapturedResponseBytes) return truncateUTF8(response, MaxCapturedResponseBytes)
} }
func bodyRedactionValues(input Request) []string {
values := []string{string(input.Body)}
switch input.Format {
case BodyJSON:
var document any
decoder := json.NewDecoder(bytes.NewReader(input.Body))
decoder.UseNumber()
if decoder.Decode(&document) == nil {
var collect func(any, string)
collect = func(value any, key string) {
switch typed := value.(type) {
case string:
if len(typed) >= 4 || typed != "" && sensitiveBodyKey(key) {
values = append(values, typed)
}
case json.Number:
if sensitiveBodyKey(key) {
values = append(values, typed.String())
}
case map[string]any:
for childKey, child := range typed {
collect(child, childKey)
}
case []any:
for _, child := range typed {
collect(child, key)
}
}
}
collect(document, "")
}
case BodyForm:
if fields, err := url.ParseQuery(string(input.Body)); err == nil {
for key, fieldValues := range fields {
for _, value := range fieldValues {
if len(value) >= 4 || value != "" && sensitiveBodyKey(key) {
values = append(values, value)
encoded := url.QueryEscape(value)
percentSpaces := strings.ReplaceAll(encoded, "+", "%20")
values = append(values, encoded, percentSpaces, lowercasePercentEscapes(encoded), lowercasePercentEscapes(percentSpaces))
}
}
}
}
}
return values
}
func lowercasePercentEscapes(value string) string {
encoded := []byte(value)
for i := 0; i+2 < len(encoded); i++ {
if encoded[i] != '%' {
continue
}
for j := i + 1; j <= i+2; j++ {
if encoded[j] >= 'A' && encoded[j] <= 'F' {
encoded[j] += 'a' - 'A'
}
}
i += 2
}
return string(encoded)
}
func sensitiveBodyKey(key string) bool {
normalized := strings.ToLower(strings.NewReplacer("-", "", "_", "").Replace(key))
return normalized == "key" || normalized == "authorization" || normalized == "cookie" ||
strings.Contains(normalized, "password") || strings.Contains(normalized, "passwd") ||
strings.Contains(normalized, "token") || strings.Contains(normalized, "secret") ||
strings.Contains(normalized, "credential") || strings.Contains(normalized, "apikey")
}
func urlRedactionValues(rawURL string) []string { func urlRedactionValues(rawURL string) []string {
trimmed := strings.TrimSpace(rawURL) trimmed := strings.TrimSpace(rawURL)
values := []string{trimmed} values := []string{trimmed}
@@ -745,23 +841,3 @@ func isPinnedWebhookAddress(host string, addresses []net.IP) bool {
} }
return false return false
} }
func validateResponse(preset Preset, statusCode int, body []byte) error {
if preset == "" {
preset = PresetGeneric
}
switch preset {
case PresetSlack:
if statusCode != http.StatusOK || strings.TrimSpace(string(body)) != "ok" {
return errors.New("webhook provider rejected response")
}
return nil
case PresetGeneric, PresetDiscord, PresetTeams:
if statusCode < http.StatusOK || statusCode >= http.StatusMultipleChoices {
return errors.New("webhook provider rejected response")
}
return nil
default:
return errors.New("unsupported webhook preset")
}
}
+180
View File
@@ -0,0 +1,180 @@
package webhook_sender
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
)
type responseRejection struct {
statusCode int
reason string
}
func DiagnosticText(value string, input Request) string {
return captureResponse([]byte(value), input, preparedTarget{})
}
func (e *responseRejection) Error() string {
return fmt.Sprintf("webhook provider rejected response (HTTP %d)", e.statusCode)
}
func validateResponse(preset Preset, statusCode int, body []byte) error {
if preset == "" {
preset = PresetGeneric
}
switch preset {
case PresetGeneric, PresetDiscord, PresetTeams, PresetSlack:
default:
return errors.New("unsupported webhook preset")
}
reason, failed := businessResponseFailure(body)
if preset == PresetSlack && reason == "" {
switch value := strings.TrimSpace(string(body)); value {
case "invalid_payload", "no_text", "invalid_token", "no_service", "no_service_id",
"no_active_hooks", "channel_not_found", "channel_is_archived",
"action_prohibited", "posting_to_general_channel_denied",
"team_disabled", "user_not_found", "too_many_attachments", "rollup_error":
reason = value
}
}
if statusCode < http.StatusOK || statusCode >= http.StatusMultipleChoices {
return &responseRejection{statusCode: statusCode, reason: reason}
}
if preset == PresetSlack {
if statusCode != http.StatusOK || strings.TrimSpace(string(body)) != "ok" {
return &responseRejection{statusCode: statusCode, reason: reason}
}
return nil
}
if failed {
return &responseRejection{statusCode: statusCode, reason: reason}
}
return nil
}
func businessResponseFailure(body []byte) (string, bool) {
var response map[string]json.RawMessage
if err := json.Unmarshal(body, &response); err != nil || response == nil {
return "", false
}
message := responseMessage(response)
if message == "" {
var errorText string
if json.Unmarshal(response["error"], &errorText) == nil {
message = strings.TrimSpace(errorText)
} else {
var nested map[string]json.RawMessage
if json.Unmarshal(response["error"], &nested) == nil {
message = responseMessage(nested)
}
}
}
for _, key := range []string{"errcode", "errCode", "error_code", "ErrorCode"} {
if raw, exists := response[key]; exists {
code, valid := responseCode(raw)
if !valid || !isSuccessResponseCode(code, false) {
return responseFailureReason(key, code, message), true
}
}
}
for _, key := range []string{"success", "ok"} {
var success bool
if raw, exists := response[key]; exists && string(raw) != "null" && json.Unmarshal(raw, &success) == nil && !success {
return responseFailureReason(key, "false", message), true
}
}
if raw, exists := response["error"]; exists {
var value any
if json.Unmarshal(raw, &value) == nil {
switch typed := value.(type) {
case string:
if strings.TrimSpace(typed) != "" {
return responseFailureReason("error", "", typed), true
}
case map[string]any:
if len(typed) != 0 {
return responseFailureReason("error", "", message), true
}
case bool:
if typed {
return responseFailureReason("error", "true", message), true
}
case float64:
if typed != 0 {
return responseFailureReason("error", strconv.FormatFloat(typed, 'g', -1, 64), message), true
}
}
}
}
if message != "" {
for _, key := range []string{"code", "Code", "StatusCode", "statusCode"} {
if raw, exists := response[key]; exists {
code, valid := responseCode(raw)
if !valid || !isSuccessResponseCode(code, true) {
return responseFailureReason(key, code, message), true
}
}
}
}
return message, false
}
func responseMessage(response map[string]json.RawMessage) string {
for _, key := range []string{"errmsg", "msg", "message", "description", "StatusMessage", "Message"} {
var message string
if json.Unmarshal(response[key], &message) == nil && strings.TrimSpace(message) != "" {
return strings.TrimSpace(message)
}
}
return ""
}
func responseCode(raw json.RawMessage) (string, bool) {
var code json.Number
if json.Unmarshal(raw, &code) == nil && code != "" {
return code.String(), true
}
var text string
if json.Unmarshal(raw, &text) == nil && strings.TrimSpace(text) != "" {
return strings.TrimSpace(text), true
}
return "", false
}
func isSuccessResponseCode(code string, allowHTTP bool) bool {
number, err := strconv.ParseFloat(code, 64)
if err == nil {
return number == 0 || allowHTTP && number >= 200 && number < 300
}
return allowHTTP && (strings.EqualFold(code, "ok") || strings.EqualFold(code, "success"))
}
func responseFailureReason(field, code, message string) string {
reason := field
if code != "" {
reason += "=" + code
}
if message != "" {
reason += ": " + message
}
return reason
}
func safeResponseError(err error, input Request, target preparedTarget) error {
var rejection *responseRejection
if !input.CaptureResponse || !errors.As(err, &rejection) || rejection.reason == "" {
return err
}
reason := captureResponse([]byte(rejection.reason), input, target)
reason = strings.Join(strings.Fields(reason), " ")
if reason == "" {
return err
}
return fmt.Errorf("%w: %s", rejection, truncateUTF8(reason, 512))
}
+2 -2
View File
@@ -59,9 +59,9 @@ func (a *alertHelper) CreateTaskScanCustomWebhookAlertLog(alert dto.AlertDTO, al
return providers.DeliveryResult{}, err return providers.DeliveryResult{}, err
} }
func (a *alertHelper) TestCustomWebhook(config dto.AlertCustomWebhookResolvedConfig) (dto.AlertConfigTestResult, error) { func (a *alertHelper) TestCustomWebhook(config dto.AlertCustomWebhookResolvedConfig, lang string) (dto.AlertConfigTestResult, error) {
transport, agentInfo := loadCommunityCustomWebhookContext() transport, agentInfo := loadCommunityCustomWebhookContext()
return alertUtil.TestCustomWebhook(config, transport, agentInfo) return alertUtil.TestCustomWebhook(config, transport, agentInfo, lang)
} }
func (a *alertHelper) GetLicenseErrorAlert() (uint, error) { func (a *alertHelper) GetLicenseErrorAlert() (uint, error) {
+1 -1
View File
@@ -21,7 +21,7 @@ type AlertProvider interface {
} }
type CustomWebhookTester interface { type CustomWebhookTester interface {
TestCustomWebhook(config dto.AlertCustomWebhookResolvedConfig) (dto.AlertConfigTestResult, error) TestCustomWebhook(config dto.AlertCustomWebhookResolvedConfig, lang string) (dto.AlertConfigTestResult, error)
} }
type DeliveryResult struct { type DeliveryResult struct {
+3 -1
View File
@@ -495,7 +495,9 @@ func (u *UpgradeService) handleRollback(originalDir string, errStep int, svcInfo
_ = settingRepo.Update("SystemStatus", "Free") _ = settingRepo.Update("SystemStatus", "Free")
dbPath := path.Join(global.CONF.Base.InstallDir, "1panel") dbPath := path.Join(global.CONF.Base.InstallDir, "1panel")
if _, err := os.Stat(path.Join(originalDir, "db")); err == nil { if _, err := os.Stat(path.Join(originalDir, "db")); err == nil {
if err := files.CopyItem(true, true, path.Join(originalDir, "db"), dbPath); err != nil { if err := os.Chmod(path.Join(originalDir, "db"), 0700); err != nil {
global.LOG.Errorf("set rollback db dir permissions failed, err: %v", err)
} else if err := files.CopyItem(true, true, path.Join(originalDir, "db"), dbPath); err != nil {
global.LOG.Errorf("rollback 1panel db failed, err: %v", err) global.LOG.Errorf("rollback 1panel db failed, err: %v", err)
} }
} }
+3
View File
@@ -91,6 +91,9 @@ var restoreCmd = &cobra.Command{
} }
fmt.Println(i18n.GetMsgByKeyForCmd("RestoreStep4")) fmt.Println(i18n.GetMsgByKeyForCmd("RestoreStep4"))
if _, err := os.Stat(path.Join(tmpPath, "db")); err == nil { if _, err := os.Stat(path.Join(tmpPath, "db")); err == nil {
if err := os.Chmod(path.Join(tmpPath, "db"), 0700); err != nil {
return err
}
dbPath := path.Join(baseDir, "1panel") dbPath := path.Join(baseDir, "1panel")
targetDBDir := path.Join(dbPath, "db") targetDBDir := path.Join(dbPath, "db")
if err := os.RemoveAll(targetDBDir); err != nil { if err := os.RemoveAll(targetDBDir); err != nil {
+35 -19
View File
@@ -15092,9 +15092,26 @@ const docTemplate = `{
}, },
"/databases/redis/install/cli": { "/databases/redis/install/cli": {
"post": { "post": {
"consumes": [
"application/json"
],
"parameters": [
{
"description": "request",
"in": "body",
"name": "request",
"required": true,
"schema": {
"$ref": "#/definitions/dto.RedisCliInstall"
}
}
],
"responses": { "responses": {
"200": { "200": {
"description": "OK" "description": "OK",
"schema": {
"type": "string"
}
} }
}, },
"security": [ "security": [
@@ -18801,11 +18818,9 @@ const docTemplate = `{
], ],
"x-panel-log": { "x-panel-log": {
"BeforeFunctions": [], "BeforeFunctions": [],
"bodyKeys": [ "bodyKeys": [],
"uuids" "formatEN": "delete Docker port guard policies",
], "formatZH": "删除 Docker 端口防护策略",
"formatEN": "delete Docker port guard policies [uuids]",
"formatZH": "删除 Docker 端口防护策略 [uuids]",
"paramKeys": [] "paramKeys": []
} }
} }
@@ -19365,11 +19380,9 @@ const docTemplate = `{
], ],
"x-panel-log": { "x-panel-log": {
"BeforeFunctions": [], "BeforeFunctions": [],
"bodyKeys": [ "bodyKeys": [],
"instanceKey" "formatEN": "reorder firewall rule",
], "formatZH": "调整防火墙规则顺序",
"formatEN": "reorder firewall rule [instanceKey]",
"formatZH": "调整防火墙规则顺序 [instanceKey]",
"paramKeys": [] "paramKeys": []
} }
} }
@@ -19510,11 +19523,9 @@ const docTemplate = `{
], ],
"x-panel-log": { "x-panel-log": {
"BeforeFunctions": [], "BeforeFunctions": [],
"bodyKeys": [ "bodyKeys": [],
"instanceKey" "formatEN": "update firewall rule",
], "formatZH": "更新防火墙规则",
"formatEN": "update firewall rule [instanceKey]",
"formatZH": "更新防火墙规则 [instanceKey]",
"paramKeys": [] "paramKeys": []
} }
} }
@@ -36372,9 +36383,6 @@ const docTemplate = `{
"bound": { "bound": {
"type": "boolean" "type": "boolean"
}, },
"forwardPolicy": {
"type": "string"
},
"initialized": { "initialized": {
"type": "boolean" "type": "boolean"
}, },
@@ -40152,6 +40160,14 @@ const docTemplate = `{
], ],
"type": "object" "type": "object"
}, },
"dto.RedisCliInstall": {
"type": "object",
"properties": {
"taskID": {
"type": "string"
}
}
},
"dto.RedisConf": { "dto.RedisConf": {
"properties": { "properties": {
"containerName": { "containerName": {
+35 -19
View File
@@ -15088,9 +15088,26 @@
}, },
"/databases/redis/install/cli": { "/databases/redis/install/cli": {
"post": { "post": {
"consumes": [
"application/json"
],
"parameters": [
{
"description": "request",
"in": "body",
"name": "request",
"required": true,
"schema": {
"$ref": "#/definitions/dto.RedisCliInstall"
}
}
],
"responses": { "responses": {
"200": { "200": {
"description": "OK" "description": "OK",
"schema": {
"type": "string"
}
} }
}, },
"security": [ "security": [
@@ -18797,11 +18814,9 @@
], ],
"x-panel-log": { "x-panel-log": {
"BeforeFunctions": [], "BeforeFunctions": [],
"bodyKeys": [ "bodyKeys": [],
"uuids" "formatEN": "delete Docker port guard policies",
], "formatZH": "删除 Docker 端口防护策略",
"formatEN": "delete Docker port guard policies [uuids]",
"formatZH": "删除 Docker 端口防护策略 [uuids]",
"paramKeys": [] "paramKeys": []
} }
} }
@@ -19361,11 +19376,9 @@
], ],
"x-panel-log": { "x-panel-log": {
"BeforeFunctions": [], "BeforeFunctions": [],
"bodyKeys": [ "bodyKeys": [],
"instanceKey" "formatEN": "reorder firewall rule",
], "formatZH": "调整防火墙规则顺序",
"formatEN": "reorder firewall rule [instanceKey]",
"formatZH": "调整防火墙规则顺序 [instanceKey]",
"paramKeys": [] "paramKeys": []
} }
} }
@@ -19506,11 +19519,9 @@
], ],
"x-panel-log": { "x-panel-log": {
"BeforeFunctions": [], "BeforeFunctions": [],
"bodyKeys": [ "bodyKeys": [],
"instanceKey" "formatEN": "update firewall rule",
], "formatZH": "更新防火墙规则",
"formatEN": "update firewall rule [instanceKey]",
"formatZH": "更新防火墙规则 [instanceKey]",
"paramKeys": [] "paramKeys": []
} }
} }
@@ -36368,9 +36379,6 @@
"bound": { "bound": {
"type": "boolean" "type": "boolean"
}, },
"forwardPolicy": {
"type": "string"
},
"initialized": { "initialized": {
"type": "boolean" "type": "boolean"
}, },
@@ -40148,6 +40156,14 @@
], ],
"type": "object" "type": "object"
}, },
"dto.RedisCliInstall": {
"type": "object",
"properties": {
"taskID": {
"type": "string"
}
}
},
"dto.RedisConf": { "dto.RedisConf": {
"properties": { "properties": {
"containerName": { "containerName": {
+348 -352
View File
@@ -265,8 +265,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "应用端口修改 [key]-[name] => [port]", "formatZH": "应用端口修改 [key]-[name] =\u003e [port]",
"formatEN": "Application port update [key]-[name] => [port]" "formatEN": "Application port update [key]-[name] =\u003e [port]"
}, },
"/apps/installed/sort/update": { "/apps/installed/sort/update": {
"bodyKeys": [], "bodyKeys": [],
@@ -679,8 +679,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "容器重命名 [name] => [newName]", "formatZH": "容器重命名 [name] =\u003e [newName]",
"formatEN": "rename container [name] => [newName]" "formatEN": "rename container [name] =\u003e [newName]"
}, },
"/containers/repo": { "/containers/repo": {
"bodyKeys": [ "bodyKeys": [
@@ -831,8 +831,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "更新 API 接口配置 => IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]", "formatZH": "更新 API 接口配置 =\u003e IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
"formatEN": "update api config => IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]" "formatEN": "update api config =\u003e IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
}, },
"/core/auth/expired/reset": { "/core/auth/expired/reset": {
"bodyKeys": [], "bodyKeys": [],
@@ -1716,8 +1716,8 @@
"output_value": "name" "output_value": "name"
} }
], ],
"formatZH": "更新用户 [name] API 接口配置 => IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]", "formatZH": "更新用户 [name] API 接口配置 =\u003e IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
"formatEN": "update user [name] api config => IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]" "formatEN": "update user [name] api config =\u003e IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
}, },
"/core/enterprise/users/del": { "/core/enterprise/users/del": {
"bodyKeys": [ "bodyKeys": [
@@ -1918,8 +1918,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改系统监听信息 => ipv6: [ipv6], 监听 IP: [bindAddress]", "formatZH": "修改系统监听信息 =\u003e ipv6: [ipv6], 监听 IP: [bindAddress]",
"formatEN": "update system bind info => ipv6: [ipv6], 监听 IP: [bindAddress]" "formatEN": "update system bind info =\u003e ipv6: [ipv6], 监听 IP: [bindAddress]"
}, },
"/core/settings/memo": { "/core/settings/memo": {
"bodyKeys": [], "bodyKeys": [],
@@ -1948,8 +1948,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改系统端口 => [serverPort]", "formatZH": "修改系统端口 =\u003e [serverPort]",
"formatEN": "update system port => [serverPort]" "formatEN": "update system port =\u003e [serverPort]"
}, },
"/core/settings/proxy/update": { "/core/settings/proxy/update": {
"bodyKeys": [ "bodyKeys": [
@@ -1974,8 +1974,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改系统 ssl => [ssl]", "formatZH": "修改系统 ssl =\u003e [ssl]",
"formatEN": "update system ssl => [ssl]" "formatEN": "update system ssl =\u003e [ssl]"
}, },
"/core/settings/terminal/update": { "/core/settings/terminal/update": {
"bodyKeys": [], "bodyKeys": [],
@@ -1991,8 +1991,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改系统配置 [key] => [value]", "formatZH": "修改系统配置 [key] =\u003e [value]",
"formatEN": "update system setting [key] => [value]" "formatEN": "update system setting [key] =\u003e [value]"
}, },
"/core/settings/upgrade": { "/core/settings/upgrade": {
"bodyKeys": [ "bodyKeys": [
@@ -2000,8 +2000,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "更新系统 => [version]", "formatZH": "更新系统 =\u003e [version]",
"formatEN": "upgrade system => [version]" "formatEN": "upgrade system =\u003e [version]"
}, },
"/core/xapp/appQRCode": { "/core/xapp/appQRCode": {
"bodyKeys": [ "bodyKeys": [
@@ -2459,282 +2459,6 @@
"formatZH": "同步 SSL 证书 [primaryDomain]", "formatZH": "同步 SSL 证书 [primaryDomain]",
"formatEN": "sync SSL certificate [primaryDomain]" "formatEN": "sync SSL certificate [primaryDomain]"
}, },
"/xpack/vms": {
"bodyKeys": [
"name"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机 [name]",
"formatEN": "create VM [name]"
},
"/xpack/vms/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "virtual_machines",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机 [name]",
"formatEN": "delete VM [name]"
},
"/xpack/vms/disks/resize": {
"bodyKeys": [
"name",
"target",
"sizeGiB"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "扩容虚拟机 [name] 磁盘 [target] 至 [sizeGiB] GiB",
"formatEN": "expand VM [name] disk [target] to [sizeGiB] GiB"
},
"/xpack/vms/environment/enable": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "启用虚拟机运行环境",
"formatEN": "enable VM runtime environment"
},
"/xpack/vms/iso": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机镜像 [name] [type]",
"formatEN": "create VM ISO [name] [type]"
},
"/xpack/vms/iso/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_isos",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机镜像 [name]",
"formatEN": "delete VM ISO [name]"
},
"/xpack/vms/iso/update": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新虚拟机镜像 [name] [type]",
"formatEN": "update VM ISO [name] [type]"
},
"/xpack/vms/networks": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机网络 [name] [type]",
"formatEN": "create VM network [name] [type]"
},
"/xpack/vms/networks/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_networks",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机网络 [name]",
"formatEN": "delete VM network [name]"
},
"/xpack/vms/networks/update": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_networks",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "更新虚拟机网络 [name]",
"formatEN": "update VM network [name]"
},
"/xpack/vms/operate": {
"bodyKeys": [
"name",
"operate"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "操作虚拟机 [name] [operate]",
"formatEN": "operate VM [name] [operate]"
},
"/xpack/vms/orphans/clean": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "清理虚拟机孤立数据",
"formatEN": "clean VM orphaned data"
},
"/xpack/vms/rename": {
"bodyKeys": [
"name",
"newName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "重命名虚拟机 [name] 为 [newName]",
"formatEN": "rename VM [name] to [newName]"
},
"/xpack/vms/snapshots": {
"bodyKeys": [
"name",
"snapshotName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机 [name] 快照 [snapshotName]",
"formatEN": "create VM [name] snapshot [snapshotName]"
},
"/xpack/vms/snapshots/del": {
"bodyKeys": [
"name",
"snapshotName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除虚拟机 [name] 快照 [snapshotName]",
"formatEN": "delete VM [name] snapshot [snapshotName]"
},
"/xpack/vms/snapshots/recover": {
"bodyKeys": [
"name",
"snapshotName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "恢复虚拟机 [name] 快照 [snapshotName]",
"formatEN": "recover VM [name] snapshot [snapshotName]"
},
"/xpack/vms/storages": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机存储 [name] [type]",
"formatEN": "create VM storage [name] [type]"
},
"/xpack/vms/storages/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_storages",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机存储 [name]",
"formatEN": "delete VM storage [name]"
},
"/xpack/vms/storages/update": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_storages",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "更新虚拟机存储 [name]",
"formatEN": "update VM storage [name]"
},
"/xpack/vms/sync": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "同步虚拟机",
"formatEN": "sync virtual machines"
},
"/xpack/vms/templates": {
"bodyKeys": [
"name",
"templateName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机 [name] 模板 [templateName]",
"formatEN": "create VM [name] template [templateName]"
},
"/xpack/vms/templates/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_templates",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机模板 [name]",
"formatEN": "delete VM template [name]"
},
"/xpack/vms/update": {
"bodyKeys": [
"name"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新虚拟机 [name]",
"formatEN": "update VM [name]"
},
"/cronjobs": { "/cronjobs": {
"bodyKeys": [ "bodyKeys": [
"type", "type",
@@ -2888,8 +2612,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "首页应用 [key] => 显示:[value]", "formatZH": "首页应用 [key] =\u003e 显示:[value]",
"formatEN": "app launcher [key] => show: [value]" "formatEN": "app launcher [key] =\u003e show: [value]"
}, },
"/dashboard/quick/change": { "/dashboard/quick/change": {
"bodyKeys": [], "bodyKeys": [],
@@ -3017,7 +2741,7 @@
} }
], ],
"formatZH": "mysql 数据库 [name] 描述信息修改 [description]", "formatZH": "mysql 数据库 [name] 描述信息修改 [description]",
"formatEN": "The description of the mysql database [name] is modified => [description]" "formatEN": "The description of the mysql database [name] is modified =\u003e [description]"
}, },
"/databases/grants": { "/databases/grants": {
"bodyKeys": [ "bodyKeys": [
@@ -3098,7 +2822,7 @@
} }
], ],
"formatZH": "mongodb 数据库 [name] 描述信息修改 [description]", "formatZH": "mongodb 数据库 [name] 描述信息修改 [description]",
"formatEN": "The description of the mongodb database [name] is modified => [description]" "formatEN": "The description of the mongodb database [name] is modified =\u003e [description]"
}, },
"/databases/mongodb/password": { "/databases/mongodb/password": {
"bodyKeys": [ "bodyKeys": [
@@ -3183,7 +2907,7 @@
} }
], ],
"formatZH": "postgresql 数据库 [name] 描述信息修改 [description]", "formatZH": "postgresql 数据库 [name] 描述信息修改 [description]",
"formatEN": "The description of the postgresql database [name] is modified => [description]" "formatEN": "The description of the postgresql database [name] is modified =\u003e [description]"
}, },
"/databases/pg/password": { "/databases/pg/password": {
"bodyKeys": [ "bodyKeys": [
@@ -3288,8 +3012,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "更新 mysql 数据库 [database] 用户 [username] 访问权限 [host] => [newHost] 描述 [description]", "formatZH": "更新 mysql 数据库 [database] 用户 [username] 访问权限 [host] =\u003e [newHost] 描述 [description]",
"formatEN": "update mysql database [database] user [username] access [host] => [newHost] description [description]" "formatEN": "update mysql database [database] user [username] access [host] =\u003e [newHost] description [description]"
}, },
"/databases/variables/update": { "/databases/variables/update": {
"bodyKeys": [], "bodyKeys": [],
@@ -3325,8 +3049,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "批量修改文件权限和用户/组 [paths] => [mode]/[user]/[group]", "formatZH": "批量修改文件权限和用户/组 [paths] =\u003e [mode]/[user]/[group]",
"formatEN": "Batch change file mode and owner [paths] => [mode]/[user]/[group]" "formatEN": "Batch change file mode and owner [paths] =\u003e [mode]/[user]/[group]"
}, },
"/files/chunkdownload": { "/files/chunkdownload": {
"bodyKeys": [ "bodyKeys": [
@@ -3416,8 +3140,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改权限 [path] => [mode]", "formatZH": "修改权限 [path] =\u003e [mode]",
"formatEN": "Change mode [path] => [mode]" "formatEN": "Change mode [path] =\u003e [mode]"
}, },
"/files/move": { "/files/move": {
"bodyKeys": [ "bodyKeys": [
@@ -3426,8 +3150,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "移动文件 [oldPaths] => [newPath]", "formatZH": "移动文件 [oldPaths] =\u003e [newPath]",
"formatEN": "Move [oldPaths] => [newPath]" "formatEN": "Move [oldPaths] =\u003e [newPath]"
}, },
"/files/owner": { "/files/owner": {
"bodyKeys": [ "bodyKeys": [
@@ -3437,8 +3161,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改用户/组 [path] => [user]/[group]", "formatZH": "修改用户/组 [path] =\u003e [user]/[group]",
"formatEN": "Change owner [path] => [user]/[group]" "formatEN": "Change owner [path] =\u003e [user]/[group]"
}, },
"/files/preview": { "/files/preview": {
"bodyKeys": [ "bodyKeys": [
@@ -3472,8 +3196,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "重命名 [oldName] => [newName]", "formatZH": "重命名 [oldName] =\u003e [newName]",
"formatEN": "Rename [oldName] => [newName]" "formatEN": "Rename [oldName] =\u003e [newName]"
}, },
"/files/save": { "/files/save": {
"bodyKeys": [ "bodyKeys": [
@@ -3530,8 +3254,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "下载 url => [path]/[name]", "formatZH": "下载 url =\u003e [path]/[name]",
"formatEN": "Download url => [path]/[name]" "formatEN": "Download url =\u003e [path]/[name]"
}, },
"/files/wget/process/remove": { "/files/wget/process/remove": {
"bodyKeys": [ "bodyKeys": [
@@ -3673,13 +3397,11 @@
"formatEN": "batch update Docker port guard policies" "formatEN": "batch update Docker port guard policies"
}, },
"/hosts/firewall/docker/policies/delete/batch": { "/hosts/firewall/docker/policies/delete/batch": {
"bodyKeys": [ "bodyKeys": [],
"uuids"
],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "删除 Docker 端口防护策略 [uuids]", "formatZH": "删除 Docker 端口防护策略",
"formatEN": "delete Docker port guard policies [uuids]" "formatEN": "delete Docker port guard policies"
}, },
"/hosts/firewall/family/operate": { "/hosts/firewall/family/operate": {
"bodyKeys": [ "bodyKeys": [
@@ -3687,10 +3409,10 @@
"family", "family",
"operation" "operation"
], ],
"formatEN": "[operation] [subsystem] [family] firewall chains",
"formatZH": "[operation] [subsystem] [family] 防火墙链",
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [] "beforeFunctions": [],
"formatZH": "[operation] [subsystem] [family] 防火墙链",
"formatEN": "[operation] [subsystem] [family] firewall chains"
}, },
"/hosts/firewall/filter/operate": { "/hosts/firewall/filter/operate": {
"bodyKeys": [ "bodyKeys": [
@@ -3739,13 +3461,11 @@
"formatEN": "delete firewall rules" "formatEN": "delete firewall rules"
}, },
"/hosts/firewall/rules/reorder": { "/hosts/firewall/rules/reorder": {
"bodyKeys": [ "bodyKeys": [],
"instanceKey"
],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "调整防火墙规则顺序 [instanceKey]", "formatZH": "调整防火墙规则顺序",
"formatEN": "reorder firewall rule [instanceKey]" "formatEN": "reorder firewall rule"
}, },
"/hosts/firewall/rules/reset": { "/hosts/firewall/rules/reset": {
"bodyKeys": [], "bodyKeys": [],
@@ -3755,22 +3475,20 @@
"formatEN": "reset firewall rules" "formatEN": "reset firewall rules"
}, },
"/hosts/firewall/rules/update": { "/hosts/firewall/rules/update": {
"bodyKeys": [ "bodyKeys": [],
"instanceKey"
],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "更新防火墙规则 [instanceKey]", "formatZH": "更新防火墙规则",
"formatEN": "update firewall rule [instanceKey]" "formatEN": "update firewall rule"
}, },
"/hosts/firewall/settings/ipv6": { "/hosts/firewall/settings/ipv6": {
"bodyKeys": [ "bodyKeys": [
"status" "status"
], ],
"formatEN": "Set firewall IPv6 support to [status]",
"formatZH": "设置防火墙 IPv6 支持为 [status]",
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [] "beforeFunctions": [],
"formatZH": "设置防火墙 IPv6 支持为 [status]",
"formatEN": "Set firewall IPv6 support to [status]"
}, },
"/hosts/firewall/settings/operate": { "/hosts/firewall/settings/operate": {
"bodyKeys": [ "bodyKeys": [
@@ -3812,11 +3530,13 @@
"formatEN": "update firewall port whitelist" "formatEN": "update firewall port whitelist"
}, },
"/hosts/monitor/clean": { "/hosts/monitor/clean": {
"bodyKeys": [], "bodyKeys": [
"type"
],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "清空监控数据", "formatZH": "清空 [type] 监控数据",
"formatEN": "clean monitor datas" "formatEN": "clean [type] monitoring data"
}, },
"/hosts/monitor/setting/update": { "/hosts/monitor/setting/update": {
"bodyKeys": [ "bodyKeys": [
@@ -3888,8 +3608,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改 SSH 配置 [key] => [newValue]", "formatZH": "修改 SSH 配置 [key] =\u003e [newValue]",
"formatEN": "update SSH setting [key] => [newValue]" "formatEN": "update SSH setting [key] =\u003e [newValue]"
}, },
"/hosts/tool/config/set": { "/hosts/tool/config/set": {
"bodyKeys": [ "bodyKeys": [
@@ -4150,7 +3870,7 @@
} }
], ],
"formatZH": "快照 [name] 描述信息修改 [description]", "formatZH": "快照 [name] 描述信息修改 [description]",
"formatEN": "The description of the snapshot [name] is modified => [description]" "formatEN": "The description of the snapshot [name] is modified =\u003e [description]"
}, },
"/settings/snapshot/import": { "/settings/snapshot/import": {
"bodyKeys": [ "bodyKeys": [
@@ -4242,8 +3962,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改系统配置 [key] => [value]", "formatZH": "修改系统配置 [key] =\u003e [value]",
"formatEN": "update system setting [key] => [value]" "formatEN": "update system setting [key] =\u003e [value]"
}, },
"/toolbox/clam": { "/toolbox/clam": {
"bodyKeys": [ "bodyKeys": [
@@ -4361,8 +4081,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改主机参数 [key] => [value]", "formatZH": "修改主机参数 [key] =\u003e [value]",
"formatEN": "update device conf [key] => [value]" "formatEN": "update device conf [key] =\u003e [value]"
}, },
"/toolbox/device/update/host": { "/toolbox/device/update/host": {
"bodyKeys": [ "bodyKeys": [
@@ -4371,8 +4091,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改主机 Host [key] => [value]", "formatZH": "修改主机 Host [key] =\u003e [value]",
"formatEN": "update device host [key] => [value]" "formatEN": "update device host [key] =\u003e [value]"
}, },
"/toolbox/device/update/swap": { "/toolbox/device/update/swap": {
"bodyKeys": [ "bodyKeys": [
@@ -4400,8 +4120,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "修改 Fail2ban 配置 [key] => [value]", "formatZH": "修改 Fail2ban 配置 [key] =\u003e [value]",
"formatEN": "update fail2ban conf [key] => [value]" "formatEN": "update fail2ban conf [key] =\u003e [value]"
}, },
"/toolbox/ftp": { "/toolbox/ftp": {
"bodyKeys": [ "bodyKeys": [
@@ -4660,8 +4380,8 @@
"output_value": "domain" "output_value": "domain"
} }
], ],
"formatZH": "修改默认 server => [domain]", "formatZH": "修改默认 server =\u003e [domain]",
"formatEN": "Change default server => [domain]" "formatEN": "Change default server =\u003e [domain]"
}, },
"/websites/del": { "/websites/del": {
"bodyKeys": [ "bodyKeys": [
@@ -5424,8 +5144,8 @@
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "更新防篡改信息 [website][path] => [status]", "formatZH": "更新防篡改信息 [website][path] =\u003e [status]",
"formatEN": "update tamper info [website][path] => [status]" "formatEN": "update tamper info [website][path] =\u003e [status]"
}, },
"/xpack/vllm/command-template/create": { "/xpack/vllm/command-template/create": {
"bodyKeys": [ "bodyKeys": [
@@ -5504,6 +5224,282 @@
"formatZH": "更新 vLLM [name]", "formatZH": "更新 vLLM [name]",
"formatEN": "update vLLM [name]" "formatEN": "update vLLM [name]"
}, },
"/xpack/vms": {
"bodyKeys": [
"name"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机 [name]",
"formatEN": "create VM [name]"
},
"/xpack/vms/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "virtual_machines",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机 [name]",
"formatEN": "delete VM [name]"
},
"/xpack/vms/disks/resize": {
"bodyKeys": [
"name",
"target",
"sizeGiB"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "扩容虚拟机 [name] 磁盘 [target] 至 [sizeGiB] GiB",
"formatEN": "expand VM [name] disk [target] to [sizeGiB] GiB"
},
"/xpack/vms/environment/enable": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "启用虚拟机运行环境",
"formatEN": "enable VM runtime environment"
},
"/xpack/vms/iso": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机镜像 [name] [type]",
"formatEN": "create VM ISO [name] [type]"
},
"/xpack/vms/iso/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_isos",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机镜像 [name]",
"formatEN": "delete VM ISO [name]"
},
"/xpack/vms/iso/update": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新虚拟机镜像 [name] [type]",
"formatEN": "update VM ISO [name] [type]"
},
"/xpack/vms/networks": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机网络 [name] [type]",
"formatEN": "create VM network [name] [type]"
},
"/xpack/vms/networks/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_networks",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机网络 [name]",
"formatEN": "delete VM network [name]"
},
"/xpack/vms/networks/update": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_networks",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "更新虚拟机网络 [name]",
"formatEN": "update VM network [name]"
},
"/xpack/vms/operate": {
"bodyKeys": [
"name",
"operate"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "操作虚拟机 [name] [operate]",
"formatEN": "operate VM [name] [operate]"
},
"/xpack/vms/orphans/clean": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "清理虚拟机孤立数据",
"formatEN": "clean VM orphaned data"
},
"/xpack/vms/rename": {
"bodyKeys": [
"name",
"newName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "重命名虚拟机 [name] 为 [newName]",
"formatEN": "rename VM [name] to [newName]"
},
"/xpack/vms/snapshots": {
"bodyKeys": [
"name",
"snapshotName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机 [name] 快照 [snapshotName]",
"formatEN": "create VM [name] snapshot [snapshotName]"
},
"/xpack/vms/snapshots/del": {
"bodyKeys": [
"name",
"snapshotName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除虚拟机 [name] 快照 [snapshotName]",
"formatEN": "delete VM [name] snapshot [snapshotName]"
},
"/xpack/vms/snapshots/recover": {
"bodyKeys": [
"name",
"snapshotName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "恢复虚拟机 [name] 快照 [snapshotName]",
"formatEN": "recover VM [name] snapshot [snapshotName]"
},
"/xpack/vms/storages": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机存储 [name] [type]",
"formatEN": "create VM storage [name] [type]"
},
"/xpack/vms/storages/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_storages",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机存储 [name]",
"formatEN": "delete VM storage [name]"
},
"/xpack/vms/storages/update": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_storages",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "更新虚拟机存储 [name]",
"formatEN": "update VM storage [name]"
},
"/xpack/vms/sync": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "同步虚拟机",
"formatEN": "sync virtual machines"
},
"/xpack/vms/templates": {
"bodyKeys": [
"name",
"templateName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机 [name] 模板 [templateName]",
"formatEN": "create VM [name] template [templateName]"
},
"/xpack/vms/templates/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_templates",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机模板 [name]",
"formatEN": "delete VM template [name]"
},
"/xpack/vms/update": {
"bodyKeys": [
"name"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新虚拟机 [name]",
"formatEN": "update VM [name]"
},
"/xpack/waf/block/export": { "/xpack/waf/block/export": {
"bodyKeys": [], "bodyKeys": [],
"paramKeys": [], "paramKeys": [],
+14 -14
View File
@@ -30,7 +30,7 @@ require (
github.com/pkg/sftp v1.13.11 github.com/pkg/sftp v1.13.11
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/russellhaering/goxmldsig v1.6.1 github.com/russellhaering/goxmldsig v1.6.1
github.com/shirou/gopsutil/v4 v4.26.7 github.com/shirou/gopsutil/v4 v4.26.9
github.com/sirupsen/logrus v1.10.2 github.com/sirupsen/logrus v1.10.2
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/soheilhy/cmux v0.1.5 github.com/soheilhy/cmux v0.1.5
@@ -39,13 +39,13 @@ require (
github.com/swaggo/files/v2 v2.0.2 github.com/swaggo/files/v2 v2.0.2
github.com/swaggo/swag v1.16.6 github.com/swaggo/swag v1.16.6
github.com/xlzd/gotp v0.1.0 github.com/xlzd/gotp v0.1.0
golang.org/x/crypto v0.56.0 golang.org/x/crypto v0.58.0
golang.org/x/image v0.45.0 golang.org/x/image v0.45.0
golang.org/x/net v0.58.0 golang.org/x/net v0.60.0
golang.org/x/oauth2 v0.36.0 golang.org/x/oauth2 v0.36.0
golang.org/x/sys v0.47.0 golang.org/x/sys v0.49.0
golang.org/x/term v0.45.0 golang.org/x/term v0.47.0
golang.org/x/text v0.41.0 golang.org/x/text v0.43.0
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
gorm.io/gorm v1.31.2 gorm.io/gorm v1.31.2
@@ -61,7 +61,7 @@ require (
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect github.com/cloudwego/base64x v0.1.6 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.10.2 // indirect github.com/ebitengine/purego v0.11.1 // indirect
github.com/f-amaral/go-async v0.3.0 // indirect github.com/f-amaral/go-async v0.3.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.2 // indirect github.com/fxamacker/cbor/v2 v2.9.2 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect
@@ -110,7 +110,7 @@ require (
github.com/pelletier/go-toml/v2 v2.3.0 // indirect github.com/pelletier/go-toml/v2 v2.3.0 // indirect
github.com/philhofer/fwd v1.2.0 // indirect github.com/philhofer/fwd v1.2.0 // indirect
github.com/phpdave11/gofpdf v1.4.3 // indirect github.com/phpdave11/gofpdf v1.4.3 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6 // indirect
github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.1 // indirect github.com/quic-go/quic-go v0.59.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
@@ -120,18 +120,18 @@ require (
github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/tinylib/msgp v1.6.4 // indirect github.com/tinylib/msgp v1.6.4 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect github.com/tklauser/go-sysconf v0.4.0 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect github.com/tklauser/numcpus v0.12.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.1 // indirect go.mongodb.org/mongo-driver/v2 v2.9.2 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/arch v0.26.0 // indirect golang.org/x/arch v0.26.0 // indirect
golang.org/x/mod v0.40.0 // indirect golang.org/x/mod v0.41.0 // indirect
golang.org/x/sync v0.22.0 // indirect golang.org/x/sync v0.24.0 // indirect
golang.org/x/tools v0.49.0 // indirect golang.org/x/tools v0.51.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
modernc.org/libc v1.72.0 // indirect modernc.org/libc v1.72.0 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
+40
View File
@@ -35,6 +35,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE= github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE=
github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/purego v0.11.1 h1:2zpWRSQNVKN4eKsKO9eM1ILDgWfYMY9GwqRmK6XeQ/0=
github.com/ebitengine/purego v0.11.1/go.mod h1:DCHPP08djqhNSoTfImcnHYQRZmd0qhakvrozqaEYhGQ=
github.com/f-amaral/go-async v0.3.0 h1:h4kLsX7aKfdWaHvV0lf+/EE3OIeCzyeDYJDb/vDZUyg= github.com/f-amaral/go-async v0.3.0 h1:h4kLsX7aKfdWaHvV0lf+/EE3OIeCzyeDYJDb/vDZUyg=
github.com/f-amaral/go-async v0.3.0/go.mod h1:Hz5Qr6DAWpbTTUjytnrg1WIsDgS7NtOei5y8SipYS7U= github.com/f-amaral/go-async v0.3.0/go.mod h1:Hz5Qr6DAWpbTTUjytnrg1WIsDgS7NtOei5y8SipYS7U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
@@ -219,6 +221,8 @@ github.com/pkg/sftp v1.13.11/go.mod h1:uNkH9roSXglNJqM+glJJi+TQXQUm0fXFWqCFmT8hs
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6 h1:jL3a8soXdzuTCcRnKhOmtcsVOObdDTFf4O2B403HPRU=
github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic= github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
@@ -237,6 +241,10 @@ github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88ee
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
github.com/shirou/gopsutil/v4 v4.26.7 h1:IXzpHz/dkMRYAhKkOXr1HB6SuzWU3eoyyeWe7g3bNZc= github.com/shirou/gopsutil/v4 v4.26.7 h1:IXzpHz/dkMRYAhKkOXr1HB6SuzWU3eoyyeWe7g3bNZc=
github.com/shirou/gopsutil/v4 v4.26.7/go.mod h1:5O9FjBiXoTDFatIWjZZosqj4pV0DRtLx598xGbBehzM= github.com/shirou/gopsutil/v4 v4.26.7/go.mod h1:5O9FjBiXoTDFatIWjZZosqj4pV0DRtLx598xGbBehzM=
github.com/shirou/gopsutil/v4 v4.26.8 h1:YQMTF/1J50B5+Y0vlo1eDRf5DoR7Gk69hY+8wjYkQeo=
github.com/shirou/gopsutil/v4 v4.26.8/go.mod h1:5O9FjBiXoTDFatIWjZZosqj4pV0DRtLx598xGbBehzM=
github.com/shirou/gopsutil/v4 v4.26.9 h1:CaBo/hBFqvlJoLvAQEKdHdf5GD97/MN2ACfyv63+2Ig=
github.com/shirou/gopsutil/v4 v4.26.9/go.mod h1:nKH+8wX2zxr/mDbsR+AAy6Qd2z4ZOQ7wfQaEp+yrYmg=
github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo= github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q= github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
@@ -279,8 +287,12 @@ github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU=
github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI=
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4=
github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
@@ -293,6 +305,8 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.mongodb.org/mongo-driver/v2 v2.5.1 h1:j2U/Qp+wvueSpqitLCSZPT/+ZpVc1xzuwdHWwl7d8ro= go.mongodb.org/mongo-driver/v2 v2.5.1 h1:j2U/Qp+wvueSpqitLCSZPT/+ZpVc1xzuwdHWwl7d8ro=
go.mongodb.org/mongo-driver/v2 v2.5.1/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.mongodb.org/mongo-driver/v2 v2.5.1/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.mongodb.org/mongo-driver/v2 v2.9.2 h1:OCAdjI37pC/SVxucz/9CM8zD4jCdegtsY5QcKzYFCgE=
go.mongodb.org/mongo-driver/v2 v2.9.2/go.mod h1:SHKN0IWkKmEVGHLjXnni6s4wPKX4v86FTgOeJJFuXcA=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
@@ -304,19 +318,31 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y= golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I= golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0= golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0=
golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4= golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4=
golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -324,17 +350,31 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM=
golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI=
golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+15 -4
View File
@@ -230,6 +230,10 @@ func (c *CommandHelper) buildPipeCommands(ctx context.Context, commands []PipeCo
cmdItem.SysProcAttr = &syscall.SysProcAttr{ cmdItem.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true, Setpgid: true,
} }
cmdItem.Cancel = func() error {
_ = syscall.Kill(-cmdItem.Process.Pid, syscall.SIGKILL)
return cmdItem.Process.Kill()
}
cmds = append(cmds, cmdItem) cmds = append(cmds, cmdItem)
} }
return cmds return cmds
@@ -305,6 +309,12 @@ func (c *CommandHelper) run(name string, arg ...string) (string, error) {
cmd.SysProcAttr = &syscall.SysProcAttr{ cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true, Setpgid: true,
} }
if newContext != nil {
cmd.Cancel = func() error {
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
return cmd.Process.Kill()
}
}
customWriter := &CustomWriter{taskItem: c.taskItem} customWriter := &CustomWriter{taskItem: c.taskItem}
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
@@ -357,11 +367,12 @@ func (c *CommandHelper) run(name string, arg ...string) (string, error) {
}() }()
select { select {
case err := <-done: case err := <-done:
if c.preserveErrorCause && newContext != nil && newContext.Err() != nil { if newContext != nil && newContext.Err() != nil {
if cmd.Process != nil && cmd.Process.Pid > 0 { resultErr := c.pipeResultErr(newContext, err)
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) if c.preserveErrorCause {
resultErr = &commandError{message: resultErr.Error(), cause: newContext.Err()}
} }
return "", newContext.Err() return "", resultErr
} }
if err != nil { if err != nil {
out, resultErr := handleErr(&stdout, &stderr, c.IgnoreExist1, err) out, resultErr := handleErr(&stdout, &stderr, c.IgnoreExist1, err)
+25 -12
View File
@@ -13,20 +13,9 @@ import (
) )
func LoadDBConnByPath(fullPath, dbName string) *gorm.DB { func LoadDBConnByPath(fullPath, dbName string) *gorm.DB {
if _, err := CreateDirWhenNotExist(true, path.Dir(fullPath)); err != nil {
panic(fmt.Errorf("init db dir failed, err: %v", err))
}
if _, err := os.Stat(fullPath); err != nil {
f, err := os.Create(fullPath)
if err != nil {
panic(fmt.Errorf("init %s db file failed, err: %v", dbName, err))
}
_ = f.Close()
}
db, err := GetDBWithPath(fullPath) db, err := GetDBWithPath(fullPath)
if err != nil { if err != nil {
panic(err) panic(fmt.Errorf("init %s db failed, err: %w", dbName, err))
} }
return db return db
} }
@@ -40,6 +29,30 @@ func CloseDB(db *gorm.DB) {
} }
func GetDBWithPath(dbPath string) (*gorm.DB, error) { func GetDBWithPath(dbPath string) (*gorm.DB, error) {
dbDir := path.Dir(dbPath)
if err := os.MkdirAll(dbDir, 0700); err != nil {
return nil, fmt.Errorf("init db dir failed, err: %w", err)
}
if err := os.Chmod(dbDir, 0700); err != nil {
return nil, fmt.Errorf("set db dir permissions failed, err: %w", err)
}
file, err := os.OpenFile(dbPath, os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
return nil, fmt.Errorf("init db file failed, err: %w", err)
}
if err := file.Chmod(0600); err != nil {
_ = file.Close()
return nil, fmt.Errorf("set db file permissions failed, err: %w", err)
}
if err := file.Close(); err != nil {
return nil, err
}
for _, suffix := range []string{"-wal", "-shm", "-journal"} {
if err := os.Chmod(dbPath+suffix, 0600); err != nil && !os.IsNotExist(err) {
return nil, fmt.Errorf("set db sidecar permissions failed, err: %w", err)
}
}
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{ db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true, DisableForeignKeyConstraintWhenMigrating: true,
Logger: newLogger(), Logger: newLogger(),
+331 -1372
View File
File diff suppressed because it is too large Load Diff
+18 -14
View File
@@ -29,16 +29,16 @@
"@novnc/novnc": "^1.7.0", "@novnc/novnc": "^1.7.0",
"@vue-office/docx": "^1.6.2", "@vue-office/docx": "^1.6.2",
"@vue-office/excel": "^1.7.8", "@vue-office/excel": "^1.7.8",
"@vueuse/core": "^14.4.0", "@vueuse/core": "^15.0.0",
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.11.0",
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^6.0.0",
"anser": "^2.3.5", "anser": "^2.3.5",
"axios": "^1.18.0", "axios": "^1.20.0",
"codemirror": "^6.0.2", "codemirror": "^6.0.2",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dompurify": "^3.4.13", "dompurify": "^3.4.16",
"echarts": "^6.1.0", "echarts": "^6.1.0",
"element-plus": "2.14.4", "element-plus": "2.14.7",
"js-base64": "^3.9.1", "js-base64": "^3.9.1",
"jsencrypt": "^3.3.2", "jsencrypt": "^3.3.2",
"lodash-es": "^4.18.1", "lodash-es": "^4.18.1",
@@ -52,7 +52,7 @@
"screenfull": "^6.0.2", "screenfull": "^6.0.2",
"sortablejs": "^1.15.7", "sortablejs": "^1.15.7",
"uuid": "^14.0.1", "uuid": "^14.0.1",
"vue": "^3.5.42", "vue": "^3.5.43",
"vue-clipboard3": "^2.0.0", "vue-clipboard3": "^2.0.0",
"vue-codemirror": "^6.1.1", "vue-codemirror": "^6.1.1",
"vue-demi": "^0.14.6", "vue-demi": "^0.14.6",
@@ -63,22 +63,22 @@
"@tailwindcss/postcss": "^4.3.3", "@tailwindcss/postcss": "^4.3.3",
"@types/node": "^26.6.3", "@types/node": "^26.6.3",
"@types/uuid": "^11.0.0", "@types/uuid": "^11.0.0",
"@typescript-eslint/eslint-plugin": "^8.65.0", "@typescript-eslint/eslint-plugin": "^8.71.2",
"@typescript-eslint/parser": "^8.68.0", "@typescript-eslint/parser": "^8.71.2",
"@vitejs/plugin-vue": "^6.0.7", "@vitejs/plugin-vue": "^6.0.10",
"@vitejs/plugin-vue-jsx": "^5.1.6", "@vitejs/plugin-vue-jsx": "^5.1.6",
"@vue/compiler-sfc": "^3.5.42", "@vue/compiler-sfc": "^3.5.43",
"autoprefixer": "^10.6.1", "autoprefixer": "^10.6.1",
"esbuild": "^0.28.2", "esbuild": "^0.28.2",
"eslint": "^10.8.0", "eslint": "^10.8.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.6", "eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-vue": "^10.10.0", "eslint-plugin-vue": "^10.10.0",
"lint-staged": "^17.0.7", "lint-staged": "^17.6.0",
"postcss": "^8.5.14", "postcss": "^8.5.14",
"postcss-html": "^2.0.0", "postcss-html": "^2.0.0",
"prettier": "^3.9.6", "prettier": "^3.9.6",
"rollup-plugin-visualizer": "^7.0.1", "rollup-plugin-visualizer": "^7.1.1",
"sass": "^1.105.0", "sass": "^1.105.0",
"standard-version": "^9.5.0", "standard-version": "^9.5.0",
"tailwindcss": "^4.3.3", "tailwindcss": "^4.3.3",
@@ -100,7 +100,11 @@
], ],
"overrides": { "overrides": {
"monaco-editor": { "monaco-editor": {
"dompurify": "3.4.13" "dompurify": "3.4.16"
} }
},
"allowScripts": {
"@vue-office/docx@1.6.3": true,
"@vue-office/excel@1.7.14": true
} }
} }
+3 -3
View File
@@ -76,7 +76,7 @@ class RequestHttp {
); );
this.service.interceptors.response.use( this.service.interceptors.response.use(
(response: AxiosResponse) => { async (response: AxiosResponse) => {
const globalStore = GlobalStore(); const globalStore = GlobalStore();
const { data } = response; const { data } = response;
const authResult = handleAuthResponseCode(data, { showRBACMessage: true }); const authResult = handleAuthResponseCode(data, { showRBACMessage: true });
@@ -100,9 +100,9 @@ class RequestHttp {
return Promise.reject(data); return Promise.reject(data);
} }
if (data.code == ResultEnum.NODE_UNBIND) { if (data.code == ResultEnum.NODE_UNBIND) {
changeToLocal(); await changeToLocal();
window.location.reload(); window.location.reload();
return; return Promise.reject(data);
} }
if (data.code == ResultEnum.ERR_GLOBAL_LOADING) { if (data.code == ResultEnum.ERR_GLOBAL_LOADING) {
globalStore.isLoading = true; globalStore.isLoading = true;
-1
View File
@@ -23,7 +23,6 @@ export namespace Firewall {
initialized: boolean; initialized: boolean;
bound: boolean; bound: boolean;
reason?: string; reason?: string;
forwardPolicy?: 'ACCEPT' | 'DROP';
raInterfaces?: string[]; raInterfaces?: string[];
} }
export interface BackendGroup { export interface BackendGroup {
+1
View File
@@ -311,6 +311,7 @@ export namespace Website {
email: string; email: string;
url: string; url: string;
type: string; type: string;
keyType: string;
useProxy: boolean; useProxy: boolean;
} }
+7 -2
View File
@@ -144,8 +144,13 @@ export const imagePull = (params: Container.ImagePull) => {
export const imagePush = (params: Container.ImagePush) => { export const imagePush = (params: Container.ImagePush) => {
return http.post<string>(`/containers/image/push`, params); return http.post<string>(`/containers/image/push`, params);
}; };
export const imageLoad = (params: Container.ImageLoad) => { export const imageLoad = (params: Container.ImageLoad, currentNode?: string) => {
return http.post(`/containers/image/load`, params, TimeoutEnum.T_10M); return http.post(
`/containers/image/load`,
params,
TimeoutEnum.T_10M,
currentNode ? { CurrentNode: currentNode } : undefined,
);
}; };
export const imageSave = (params: Container.ImageSave) => { export const imageSave = (params: Container.ImageSave) => {
return http.post(`/containers/image/save`, params, TimeoutEnum.T_10M); return http.post(`/containers/image/save`, params, TimeoutEnum.T_10M);
+4 -2
View File
@@ -193,8 +193,10 @@ export const redisPersistenceConf = (type: string, database: string) => {
export const checkRedisCli = () => { export const checkRedisCli = () => {
return http.get<boolean>(`/databases/redis/check`); return http.get<boolean>(`/databases/redis/check`);
}; };
export const installRedisCli = () => { export const installRedisCli = (taskID: string, node: string) => {
return http.post(`/databases/redis/install/cli`, {}, TimeoutEnum.T_5M); return http.post<string>(`/databases/redis/install/cli?operateNode=${encodeURIComponent(node)}`, {
taskID,
});
}; };
export const changeRedisPassword = (database: string, password: string) => { export const changeRedisPassword = (database: string, password: string) => {
if (password) { if (password) {
+26 -32
View File
@@ -126,14 +126,14 @@ const styleVars = computed(() => ({
const terminalElement = ref<HTMLDivElement | null>(null); const terminalElement = ref<HTMLDivElement | null>(null);
let eventSource: EventSource | null = null; let eventSource: EventSource | null = null;
let searchVersion = 0;
let term: Terminal | null = null; let term: Terminal | null = null;
const fitAddon = new FitAddon(); const fitAddon = new FitAddon();
let onScrollDisposable: { dispose: () => void } | null = null;
const MAX_VIEW_LINES = 20000; const MAX_VIEW_LINES = 20000;
const followBottom = ref(true); let followBottom = true;
const logSearch = reactive({ const logSearch = reactive({
isWatch: props.defaultFollow ? true : true, isWatch: true,
isShowTimestamp: props.defaultIsShowTimestamp, isShowTimestamp: props.defaultIsShowTimestamp,
container: '', container: '',
mode: 'all', mode: 'all',
@@ -168,6 +168,7 @@ const timeOptions = ref([
]); ]);
const stopListening = () => { const stopListening = () => {
searchVersion++;
if (eventSource) { if (eventSource) {
eventSource.close(); eventSource.close();
eventSource = null; eventSource = null;
@@ -175,26 +176,13 @@ const stopListening = () => {
}; };
const clearTerminal = () => { const clearTerminal = () => {
term?.reset(); // Queue the reset after pending writes so old logs cannot appear in a new search.
followBottom.value = true; term?.write('\x1bc');
followBottom = true;
}; };
const writeLogLine = (data: string) => { const writeLogLine = (data: string) => {
if (!term) return; term?.writeln(data);
term.writeln(data);
if (followBottom.value) {
term.scrollToBottom();
}
};
const bindXTermEvents = () => {
if (!term) return;
onScrollDisposable?.dispose();
onScrollDisposable = term.onScroll(() => {
if (!term) return;
const active = term.buffer.active;
followBottom.value = active.baseY + active.cursorY >= active.length - 2;
});
}; };
const showEventSourceAuthError = (message: string) => { const showEventSourceAuthError = (message: string) => {
@@ -235,11 +223,15 @@ const initTerminal = () => {
term.open(terminalElement.value); term.open(terminalElement.value);
term.loadAddon(fitAddon); term.loadAddon(fitAddon);
fitAddon.fit(); fitAddon.fit();
bindXTermEvents(); // Terminal.dispose() also disposes these listeners.
}; term.onScroll(() => {
if (!term) return;
const handleClose = async () => { const active = term.buffer.active;
stopListening(); followBottom = active.viewportY >= active.baseY;
});
term.onWriteParsed(() => {
if (followBottom) term?.scrollToBottom();
});
}; };
const searchLogs = async () => { const searchLogs = async () => {
@@ -248,6 +240,7 @@ const searchLogs = async () => {
return; return;
} }
stopListening(); stopListening();
const version = searchVersion;
clearTerminal(); clearTerminal();
let currentNode = globalCurrentNode.value; let currentNode = globalCurrentNode.value;
@@ -261,15 +254,19 @@ const searchLogs = async () => {
} }
const authError = await checkStreamAuth(url, currentNode); const authError = await checkStreamAuth(url, currentNode);
if (version !== searchVersion) return;
if (authError) { if (authError) {
showEventSourceAuthError(authError); showEventSourceAuthError(authError);
return; return;
} }
eventSource = new EventSource(url); const source = new EventSource(url);
eventSource.onmessage = (event: MessageEvent) => { eventSource = source;
source.onmessage = (event: MessageEvent) => {
if (version !== searchVersion) return;
writeLogLine(event.data); writeLogLine(event.data);
}; };
eventSource.onerror = (event: MessageEvent) => { source.onerror = (event: MessageEvent) => {
if (version !== searchVersion) return;
stopListening(); stopListening();
if (event.data && event.data != '') { if (event.data && event.data != '') {
MsgError(event.data); MsgError(event.data);
@@ -353,8 +350,6 @@ onMounted(() => {
logSearch.resource = props.resource; logSearch.resource = props.resource;
logSearch.tail = 100; logSearch.tail = 100;
logSearch.mode = 'all';
logSearch.isWatch = true;
nextTick(() => { nextTick(() => {
initTerminal(); initTerminal();
@@ -369,8 +364,7 @@ onMounted(() => {
}); });
onUnmounted(() => { onUnmounted(() => {
handleClose(); stopListening();
onScrollDisposable?.dispose();
if (term) { if (term) {
term.dispose(); term.dispose();
term = null; term = null;
@@ -15,7 +15,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue'; import { onActivated, ref, watch } from 'vue';
defineOptions({ name: 'TableSearch' }); defineOptions({ name: 'TableSearch' });
const emit = defineEmits(['search', 'update:searchName']); const emit = defineEmits(['search', 'update:searchName']);
@@ -42,6 +42,10 @@ watch(
{ immediate: true }, { immediate: true },
); );
onActivated(() => {
searchInfo.value = props.searchName;
});
const search = () => { const search = () => {
emit('update:searchName', searchInfo.value); emit('update:searchName', searchInfo.value);
emit('search'); emit('search');
@@ -167,7 +167,7 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref, useAttrs } from 'vue'; import { computed, onDeactivated, onMounted, ref, useAttrs } from 'vue';
import { useGlobalStore } from '@/composables/useGlobalStore'; import { useGlobalStore } from '@/composables/useGlobalStore';
import { flattenVNodes, type FuTableOperationButton } from '@/components/table/shared'; import { flattenVNodes, type FuTableOperationButton } from '@/components/table/shared';
import { useCardColumns } from './useCardColumns'; import { useCardColumns } from './useCardColumns';
@@ -459,6 +459,8 @@ watch([currentViewMode, tableData, () => props.syncCardContentHeight], scheduleC
flush: 'post', flush: 'post',
}); });
onDeactivated(clearSelects);
watch([() => props.paginationConfig?.currentPage, () => props.paginationConfig?.pageSize], clearSelects, { watch([() => props.paginationConfig?.currentPage, () => props.paginationConfig?.pageSize], clearSelects, {
flush: 'sync', flush: 'sync',
}); });
+2 -7
View File
@@ -1,11 +1,6 @@
import { reactive, type UnwrapNestedRefs } from 'vue'; import { reactive, type UnwrapNestedRefs } from 'vue';
import { useRoute } from 'vue-router';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { getPageState } from '@/utils/page-state-cache';
/** A new page visit starts from its defaults instead of restoring the previous page number. */
export const usePageState = <T extends object>(factory: () => T): UnwrapNestedRefs<T> => { export const usePageState = <T extends object>(factory: () => T): UnwrapNestedRefs<T> => {
const route = useRoute(); return reactive(factory());
const { currentNode } = useGlobalStore();
const key = `${currentNode.value}:${String(route.name || route.path)}`;
return reactive(getPageState(key, factory));
}; };
@@ -1,63 +0,0 @@
import { storeToRefs } from 'pinia';
import { watch, type Ref } from 'vue';
import GlobalStore from '@/store/modules/global';
type SearchValue = string | number | undefined;
type SearchFields = Record<string, Ref<SearchValue>>;
export const useSearchPersistence = (page: string, fields: SearchFields, scope?: () => SearchValue) => {
const { currentNode } = storeToRefs(GlobalStore());
const defaults = Object.fromEntries(Object.entries(fields).map(([name, field]) => [name, field.value]));
let initialized = false;
let restoring = false;
let activeKey: string | undefined;
watch(
() => {
const context = scope?.();
if (scope && context === undefined) return undefined;
return `1panel:search:v1:${JSON.stringify([currentNode.value, page, context ?? ''])}`;
},
(key) => {
activeKey = key;
let saved: Record<string, unknown> = {};
try {
const parsed = activeKey ? JSON.parse(localStorage.getItem(activeKey) || '{}') : {};
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) saved = parsed;
} catch {}
restoring = true;
for (const [name, field] of Object.entries(fields)) {
const value = saved[name];
const initial = defaults[name];
let fallback = initial;
if (initialized) {
fallback = typeof initial === 'string' ? '' : undefined;
}
const valid =
(typeof value === 'string' || (typeof value === 'number' && Number.isFinite(value))) &&
(fallback === undefined || typeof value === typeof fallback);
if (value === null) {
field.value = typeof initial === 'string' ? '' : undefined;
} else if (valid) {
field.value = value as SearchValue;
} else {
field.value = fallback;
}
}
restoring = false;
initialized = true;
},
{ immediate: true, flush: 'sync' },
);
watch(
() => Object.fromEntries(Object.entries(fields).map(([name, field]) => [name, field.value ?? null])),
(value) => {
if (restoring || !activeKey) return;
try {
localStorage.setItem(activeKey, JSON.stringify(value));
} catch {}
},
{ flush: 'sync' },
);
};
@@ -0,0 +1,24 @@
import { storeToRefs } from 'pinia';
import { onActivated, onDeactivated } from 'vue';
import GlobalStore from '@/store/modules/global';
import { bindSearchReset, type SearchFields } from '@/utils/search-reset';
interface SearchResetOptions {
scope?: () => string | number | undefined;
onRevisit: () => unknown;
}
export const useSearchReset = (fields: SearchFields, options: SearchResetOptions) => {
const { currentNode } = storeToRefs(GlobalStore());
const reset = bindSearchReset(options.scope ? [currentNode, options.scope] : [currentNode], fields);
let deactivated = false;
onDeactivated(() => {
deactivated = true;
});
onActivated(() => {
if (!deactivated) return;
deactivated = false;
reset();
return options.onRevisit();
});
};
+4
View File
@@ -216,6 +216,10 @@ export const DNSTypes = [
label: 'NameSilo', label: 'NameSilo',
value: 'NameSilo', value: 'NameSilo',
}, },
{
label: 'Netcup',
value: 'Netcup',
},
{ {
label: 'NameCheap', label: 'NameCheap',
value: 'NameCheap', value: 'NameCheap',

Some files were not shown because too many files have changed in this diff Show More