Compare commits

..
86 changed files with 1391 additions and 512 deletions
+8 -5
View File
@@ -293,7 +293,7 @@ func (b *BaseApi) PageAlertConfig(c *gin.Context) {
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /alert/config/update [post]
// @x-panel-log {"bodyKeys":["id","displayName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置 [id][displayName]","formatEN":"update alert config [id][displayName]"}
// @x-panel-log {"bodyKeys":["displayName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置 [displayName]","formatEN":"update alert config [displayName]"}
func (b *BaseApi) UpdateAlertConfig(c *gin.Context) {
var req dto.AlertConfigUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -321,13 +321,15 @@ func (b *BaseApi) UpdateAlertConfig(c *gin.Context) {
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /alert/config/status [post]
// @x-panel-log {"bodyKeys":["id","status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置状态 [id][status]","formatEN":"update alert config status [id][status]"}
// @x-panel-log {"bodyKeys":["status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置状态 [name][status]","formatEN":"update alert config status [name][status]"}
func (b *BaseApi) UpdateAlertConfigStatus(c *gin.Context) {
var req dto.AlertConfigStatusUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := alertService.UpdateAlertConfigStatus(req, loadAuditUser(c)); err != nil {
name, err := alertService.UpdateAlertConfigStatus(req, loadAuditUser(c))
helper.SetOperationLogValues(c, map[string]string{"name": name})
if err != nil {
helper.InternalServer(c, err)
return
}
@@ -353,13 +355,14 @@ func loadAuditUser(c *gin.Context) string {
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /alert/config/del [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除告警配置 [id]","formatEN":"delete alert config [id]"}
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除告警配置 [name]","formatEN":"delete alert config [name]"}
func (b *BaseApi) DeleteAlertConfig(c *gin.Context) {
var req dto.DeleteRequest
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := alertService.DeleteAlertConfig(req.ID)
name, err := alertService.DeleteAlertConfig(req.ID)
helper.SetOperationLogValues(c, map[string]string{"name": name})
if err != nil {
helper.InternalServer(c, err)
return
+21
View File
@@ -3,6 +3,7 @@ package helper
import (
"compress/gzip"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
@@ -184,3 +185,23 @@ func GetParamInt32(paramName string, c *gin.Context) (int32, error) {
intNum, _ := strconv.Atoi(idParam)
return int32(intNum), nil
}
// SetOperationLogValues supplies server-resolved values to Core's operation log.
// Call before writing the response. Only pass public audit fields, never credentials.
func SetOperationLogValues(c *gin.Context, values map[string]string) {
const header = "X-Op-Resolved"
resolved := make(map[string]string)
if data, err := base64.RawURLEncoding.DecodeString(c.Writer.Header().Get(header)); err == nil {
_ = json.Unmarshal(data, &resolved)
}
if resolved == nil {
resolved = make(map[string]string)
}
for key, value := range values {
resolved[key] = value
}
data, err := json.Marshal(resolved)
if err == nil {
c.Header(header, base64.RawURLEncoding.EncodeToString(data))
}
}
+3 -1
View File
@@ -9,7 +9,8 @@ type AgentCreateReq struct {
WebUIPort int `json:"webUIPort" validate:"required,min=1,max=65535"`
BridgePort int `json:"bridgePort"`
AllowedOrigins []string `json:"allowedOrigins"`
AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent"`
AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent deepseek-harness octop"`
AccessHost string `json:"accessHost"`
Model string `json:"model"`
AccountID uint `json:"accountId"`
Token string `json:"token"`
@@ -110,6 +111,7 @@ type AgentBatchOperateResult struct {
}
type AgentItem struct {
AccessHost string `json:"accessHost"`
ID uint `json:"id"`
Name string `json:"name"`
Remark string `json:"remark"`
+1
View File
@@ -26,6 +26,7 @@ type FirewallSubsystemStatus struct {
}
type FirewallLifecycleOperation struct {
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
Operation string `json:"operation" validate:"required,oneof=start stop restart disableBanPing enableBanPing"`
WithDockerRestart bool `json:"withDockerRestart"`
}
+6
View File
@@ -1,6 +1,7 @@
package dto
import (
"encoding/json"
"time"
"github.com/1Panel-dev/1Panel/agent/app/model"
@@ -144,6 +145,11 @@ type MonitorVLLMCurrent struct {
AppInstallID uint `json:"appInstallID" validate:"required"`
}
type MonitorVLLMCurrentData struct {
model.MonitorVLLM
Metrics map[string]json.RawMessage `json:"metrics,omitempty"`
}
type MonitorVLLMClean struct {
AppInstallID uint `json:"appInstallID" validate:"required"`
}
+2
View File
@@ -86,7 +86,9 @@ type MonitorVLLM struct {
RawMetrics string `json:"-"`
HistogramDeltas string `json:"-"`
IntervalSeconds float64 `json:"intervalSeconds" gorm:"-"`
SampleIntervalSeconds int `json:"-"`
RequestDeltas string `json:"-"`
CompletedRequests *float64 `json:"completedRequests" gorm:"-"`
QueueTime *float64 `json:"queueTime"`
AverageGenerationTokens *float64 `json:"averageGenerationTokens"`
PrefixCacheHitRate *float64 `json:"prefixCacheHitRate"`
+10 -2
View File
@@ -16,7 +16,8 @@ type VLLMMonitorRepo struct{}
type VLLMHistoryPoint struct {
model.MonitorVLLM
Bucket int64
Bucket int64
CompletedRequests *float64
}
func (r *VLLMMonitorRepo) Create(point *model.MonitorVLLM) error {
@@ -77,12 +78,19 @@ func (r *VLLMMonitorRepo) History(ctx context.Context, id uint, start, end time.
}
db = db.Group("bucket").Order("bucket ASC")
} else {
columns = append([]string{"id", "created_at", "app_install_id", "status"}, metrics...)
columns = append([]string{"id", "created_at", "app_install_id", "status", "sample_interval_seconds"}, metrics...)
if group == "" || group == "requests" {
columns = append(columns, "histogram_deltas", "request_deltas")
}
db = db.Order("created_at ASC, id ASC")
}
if group == "" || group == "overview" {
count := "CASE WHEN json_valid(request_deltas) THEN CASE WHEN json_extract(request_deltas, '$.seconds') > 0 THEN ROUND(request_throughput * json_extract(request_deltas, '$.seconds')) END END"
if seconds > 0 {
count = "SUM(" + count + ")"
}
columns = append(columns, count+" AS completed_requests")
}
var points []VLLMHistoryPoint
err := db.Select(strings.Join(columns, ", ")).Scan(&points).Error
return points, err
+12 -1
View File
@@ -142,6 +142,9 @@ const (
func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
agentType := req.AgentType
if err := validateNativeAgentCreate(req); err != nil {
return nil, err
}
if err := checkPortExist(req.WebUIPort); err != nil {
return nil, err
}
@@ -237,6 +240,8 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
}
} else if agentType == constant.AppCopaw {
dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
} else if agentType == constant.AppDeepSeekHarness || agentType == constant.AppOctop {
dashboardAuth = agentDashboardAuth{Username: req.DashboardUsername, Password: req.DashboardPassword}
}
params := map[string]interface{}{
@@ -245,6 +250,12 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
constant.HostIP: "",
}
setAgentWebUIParams(params, agentType, detail.Version, req.WebUIPort)
if agentType == constant.AppDeepSeekHarness {
params["HTTPS_ACCESS_HOST"] = req.AccessHost
}
if agentType == constant.AppOctop {
params["OCTOP_LOG_LEVEL"] = "info"
}
if agentType == constant.AppOpenclaw {
if allowedOrigin := firstAllowedOrigin(allowedOrigins); allowedOrigin != "" {
params["ALLOWED_ORIGIN"] = allowedOrigin
@@ -738,7 +749,7 @@ func requiresAgentAccount(agentType string) bool {
}
func setAgentWebUIParams(params map[string]interface{}, agentType, appVersion string, webUIPort int) {
if agentType == constant.AppOpenclaw && isOpenclawHTTPSWindowVersion(appVersion) {
if agentType == constant.AppDeepSeekHarness || agentType == constant.AppOpenclaw && isOpenclawHTTPSWindowVersion(appVersion) {
params["PANEL_APP_PORT_HTTPS"] = webUIPort
return
}
+69
View File
@@ -0,0 +1,69 @@
package service
import (
"net"
"regexp"
"strings"
"unicode"
"unicode/utf8"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
)
var deepSeekUsernamePattern = regexp.MustCompile(`^[A-Za-z0-9._-]+$`)
var deepSeekHostLabelPattern = regexp.MustCompile(`^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$`)
func isDeepSeekAccessHost(value string) bool {
if ip := net.ParseIP(value); ip != nil {
return ip.To4() != nil && !strings.Contains(value, ":")
}
if value == "" || len(value) > 253 || strings.Trim(value, "0123456789.") == "" {
return false
}
for _, label := range strings.Split(value, ".") {
if !deepSeekHostLabelPattern.MatchString(label) {
return false
}
}
return true
}
func validateNativeAgentCreate(req dto.AgentCreateReq) error {
if req.AgentType != constant.AppDeepSeekHarness && req.AgentType != constant.AppOctop {
return nil
}
if strings.TrimSpace(req.DashboardUsername) == "" || strings.ContainsAny(req.DashboardUsername, "\r\n\x00") {
return buserr.WithDetail("ErrInvalidParams", "dashboardUsername", nil)
}
if strings.ContainsAny(req.DashboardPassword, "\r\n\x00") || req.DashboardPassword != strings.TrimSpace(req.DashboardPassword) {
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
}
if req.AgentType == constant.AppDeepSeekHarness {
if !isDeepSeekAccessHost(req.AccessHost) {
return buserr.WithDetail("ErrInvalidParams", "accessHost", nil)
}
if !deepSeekUsernamePattern.MatchString(req.DashboardUsername) {
return buserr.WithDetail("ErrInvalidParams", "dashboardUsername", nil)
}
if utf8.RuneCountInString(req.DashboardPassword) < 12 {
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
}
return nil
}
hasLetter, hasDigit := false, false
for _, char := range req.DashboardPassword {
hasLetter = hasLetter || unicode.IsLetter(char)
hasDigit = hasDigit || unicode.IsDigit(char)
}
if utf8.RuneCountInString(req.DashboardPassword) < 8 || !hasLetter || !hasDigit {
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
}
switch strings.ToLower(req.DashboardPassword) {
case "password", "password1", "password12", "password123", "12345678", "123456789", "qwerty123",
"admin123", "welcome1", "letmein1", "changeme1", "octop123", "abc12345", "iloveyou1":
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
}
return nil
}
+15 -1
View File
@@ -415,6 +415,15 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
} else {
item.WebUIPort = appInstall.HttpPort
}
if agentType == constant.AppDeepSeekHarness {
item.WebUIPort = appInstall.HttpsPort
if values, err := readAgentEnvMap(appInstall.GetEnvPath()); err == nil {
accessHost := values["HTTPS_ACCESS_HOST"]
if isDeepSeekAccessHost(accessHost) {
item.AccessHost = accessHost
}
}
}
item.Path = appInstall.GetPath()
item.Status = appInstall.Status
item.Message = appInstall.Message
@@ -442,7 +451,8 @@ func localizedAgentProviderName(provider string) string {
}
func isAgentAppKey(appKey string) bool {
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent ||
appKey == constant.AppDeepSeekHarness || appKey == constant.AppOctop
}
func isOpenclawLegacyHTTPVersion(version string) bool {
@@ -1641,6 +1651,10 @@ func agentDashboardAuthEnvKeys(agentType string) (string, string, bool) {
return hermesDashboardUsernameEnvKey, hermesDashboardPasswordEnvKey, true
case constant.AppCopaw:
return qwenPawAuthUsernameEnvKey, qwenPawAuthPasswordEnvKey, true
case constant.AppDeepSeekHarness:
return "DSH_AUTH_USERNAME", "DSH_AUTH_PASSWORD", true
case constant.AppOctop:
return "OCTOP_ADMIN_USERNAME", "OCTOP_DEFAULT_PASSWORD", true
default:
return "", "", false
}
+26 -14
View File
@@ -74,8 +74,8 @@ type IAlertService interface {
GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertConfig, error)
PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error)
UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error
UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) error
DeleteAlertConfig(id uint) error
UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) (string, error)
DeleteAlertConfig(id uint) (string, error)
TestAlertConfig(req dto.AlertConfigTest) (bool, error)
TestCustomAlertConfig(req dto.AlertConfigTest, lang string) (dto.AlertConfigTestResult, error)
}
@@ -623,25 +623,26 @@ func (a AlertService) updateCustomAlertConfig(req dto.AlertConfigUpdate, operato
})
}
func (a AlertService) UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) error {
func (a AlertService) UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) (string, error) {
if err := validateAlertConfigStatus(req.Status); err != nil {
return err
return "", err
}
config, err := alertRepo.GetConfigById(req.ID)
if err != nil {
return err
return "", err
}
name := alertConfigLogName(config.Config, config.Title)
if req.Status == constant.AlertEnable {
if err := a.validateCommunityAlertConfigType(config.Type); err != nil {
return err
return name, err
}
if config.Type == constant.Custom {
if _, err := alertwebhook.Resolve(config); err != nil {
return err
return name, err
}
}
}
return alertRepo.UpdateAlertConfig(map[string]interface{}{
return name, alertRepo.UpdateAlertConfig(map[string]interface{}{
"status": req.Status,
"update_user": operator,
}, repo.WithByID(req.ID))
@@ -813,19 +814,20 @@ func alertConfigSMSPhone(configData string) string {
return strings.TrimSpace(cfg.Phone)
}
func (a AlertService) DeleteAlertConfig(id uint) error {
_, err := alertRepo.GetConfigById(id)
func (a AlertService) DeleteAlertConfig(id uint) (string, error) {
config, err := alertRepo.GetConfigById(id)
if err != nil {
return err
return "", err
}
name := alertConfigLogName(config.Config, config.Title)
usedAlerts, err := alertRepo.List(alertRepo.WithByAlertMethodContainsConfigID(id))
if err != nil {
return err
return name, err
}
if len(usedAlerts) > 0 {
return buserr.New("ErrAlertConfigInUse")
return name, buserr.New("ErrAlertConfigInUse")
}
return alertRepo.DeleteAlertConfig(repo.WithByID(id))
return name, alertRepo.DeleteAlertConfig(repo.WithByID(id))
}
func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
@@ -1043,3 +1045,13 @@ func (a AlertService) validateCronJobAlertChannels(alertType, advanced, method s
_, err = a.validateAlertMethodReferences(method)
return err
}
func alertConfigLogName(config, title string) string {
var value struct {
DisplayName string `json:"displayName"`
}
if json.Unmarshal([]byte(config), &value) == nil && strings.TrimSpace(value.DisplayName) != "" {
return strings.TrimSpace(value.DisplayName)
}
return title
}
+47 -55
View File
@@ -131,6 +131,9 @@ func (s *FirewallService) LoadBaseInfo(chainGroup string) (dto.FirewallSubsystem
func (s *FirewallService) QueueFirewallOperation(request dto.FirewallLifecycleOperation) (dto.FirewallLifecycleOperationResponse, error) {
response := dto.FirewallLifecycleOperationResponse{}
if request.BackupFile != "" && request.Operation != string(lifecycle.OperationStart) {
return response, fmt.Errorf("backup restoration is only supported when starting firewalld or ufw")
}
if request.Operation == "disableBanPing" || request.Operation == "enableBanPing" {
return response, s.OperateFirewall(request)
}
@@ -155,6 +158,19 @@ func (s *FirewallService) QueueFirewallOperation(request dto.FirewallLifecycleOp
if err != nil {
return response, err
}
var restoreRequest dto.FirewallRuleCreate
if request.BackupFile != "" {
if client.Name() != lifecycle.ProviderFirewalld && client.Name() != lifecycle.ProviderUFW {
return response, fmt.Errorf("backup restoration is only supported when starting firewalld or ufw")
}
restoreRequest, err = prepareFirewallRuleCreate(dto.FirewallRuleCreate{BackupFile: request.BackupFile, Initialize: true})
if err != nil {
return response, err
}
if err := validateFirewallCreateBatch(restoreRequest, filter.Provider(client.Name())); err != nil {
return response, err
}
}
if (client.Name() != lifecycle.ProviderFirewalld && client.Name() != lifecycle.ProviderUFW) ||
(request.Operation != string(lifecycle.OperationStart) && request.Operation != string(lifecycle.OperationStop) &&
request.Operation != string(lifecycle.OperationRestart)) {
@@ -173,7 +189,26 @@ func (s *FirewallService) QueueFirewallOperation(request dto.FirewallLifecycleOp
return response, err
}
taskItem.AddSubTaskWithOps(name, func(t *task.Task) error {
return s.runFirewallLifecycleTask(t, client, request, true)
if request.BackupFile != "" {
if err := s.checkSelectedProvider(t.TaskCtx, filter.Provider(client.Name())); err != nil {
return err
}
}
prepareStart := func(lifecycle.Client) error {
if request.BackupFile != "" {
return runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallCreateRulesStep"), func() error {
return s.runFirewallCreateRulesTask(t, restoreRequest, filter.Provider(client.Name()))
})
}
return runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallSyncWhitelistStep"), func() error {
err := s.SyncPortWhitelist(t.TaskCtx)
if err == nil && client.Name() == lifecycle.ProviderFirewalld {
err = lifecycleproviders.RemoveFirewalldSSHService()
}
return err
})
}
return s.runFirewallLifecycleTask(t, client, request, prepareStart)
}, nil, 0, 0)
if err := taskRepo.Save(context.Background(), taskItem.Task); err != nil {
closeUnstartedFirewallTask(taskItem)
@@ -554,29 +589,9 @@ func (s *FirewallService) LoadFirewallNativeDetail(ctx context.Context, request
}
func (s *FirewallService) Create(ctx context.Context, request dto.FirewallRuleCreate) (dto.FirewallRuleCreateResponse, error) {
if request.BackupFile != "" {
if len(request.Items) != 0 {
return dto.FirewallRuleCreateResponse{}, filter.ErrInvalidRule
}
items, err := readFirewallBackup(request.BackupFile)
if err != nil {
return dto.FirewallRuleCreateResponse{}, err
}
for _, item := range items {
request.Items = append(request.Items, dto.FirewallRuleCreateItem{Rule: item.FirewallRule, Raw: item.Raw, ParseStatus: item.ParseStatus, SourceKind: constant.FirewallRuleSourceImported})
}
}
for index, item := range request.Items {
if item.SourceKind != constant.FirewallRuleSourceImported || item.Rule.Scope.Provider != filter.ProviderUFW || item.ParseStatus != filter.ParseStatusPartial || item.Raw == "" {
continue
}
observed := filterufw.ParseRules(item.Rule.Scope, item.Raw)
if len(observed) == 1 && observed[0].ParseStatus == filter.ParseStatusSupported {
observed[0].Rule.Description = item.Rule.Description
request.Items[index].Rule = observed[0].Rule
request.Items[index].ParseStatus, request.Items[index].Raw = filter.ParseStatusSupported, ""
}
request, err := prepareFirewallRuleCreate(request)
if err != nil {
return dto.FirewallRuleCreateResponse{}, err
}
provider, err := s.selectedProvider(ctx)
@@ -609,30 +624,11 @@ func (s *FirewallService) Create(ctx context.Context, request dto.FirewallRuleCr
if err != nil {
return err
}
return s.runFirewallLifecycleTask(t, client, dto.FirewallLifecycleOperation{Operation: "start"}, false)
return s.runFirewallLifecycleTask(t, client, dto.FirewallLifecycleOperation{Operation: "start"}, nil)
}, nil)
}
ruleState := make(map[string]filter.RuleSet)
taskItem.AddSubTaskWithOps(i18n.GetMsgByKey("FirewallCreateRulesStep"), func(t *task.Task) error {
firewallRuleMutationMu.Lock()
if err := s.checkSelectedProvider(t.TaskCtx, provider); err != nil {
firewallRuleMutationMu.Unlock()
return err
}
_, err := s.createRules(t.TaskCtx, request, t, ruleState)
firewallRuleMutationMu.Unlock()
if !request.Initialize || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return err
}
if err != nil {
ruleState = nil
}
whitelistErr := s.syncPortWhitelist(t.TaskCtx, ruleState)
if whitelistErr == nil && provider == filter.ProviderFirewalld {
whitelistErr = lifecycleproviders.RemoveFirewalldSSHService()
}
t.LogWithStatus(i18n.GetMsgByKey("FirewallSyncWhitelistStep"), whitelistErr)
return errors.Join(err, whitelistErr)
return s.runFirewallCreateRulesTask(t, request, provider)
}, nil, 0, 0)
if err := taskRepo.Save(context.Background(), taskItem.Task); err != nil {
taskItem.LogFailedWithErr(taskItem.Name, err)
@@ -777,7 +773,11 @@ func (s *FirewallService) restoreFirewallAfterStart(client lifecycle.Client) err
return nil
}
}
recordFailure("restore whitelist allowances", s.SyncPortWhitelist(ctx))
whitelistErr := s.SyncPortWhitelist(ctx)
if whitelistErr == nil && provider == filter.ProviderFirewalld {
whitelistErr = lifecycleproviders.RemoveFirewalldSSHService()
}
recordFailure("restore whitelist allowances", whitelistErr)
return errors.Join(recoveryErrors...)
}
@@ -805,17 +805,9 @@ func (s *FirewallService) restoreFirewalldRuntimeDependents(ctx context.Context,
return restoreErr
}
func (s *FirewallService) runFirewallLifecycleTask(t *task.Task, client lifecycle.Client, request dto.FirewallLifecycleOperation, syncWhitelist bool) error {
func (s *FirewallService) runFirewallLifecycleTask(t *task.Task, client lifecycle.Client, request dto.FirewallLifecycleOperation, prepareStart func(lifecycle.Client) error) error {
ctx := t.TaskCtx
provider := filter.Provider(client.Name())
var prepareStart func(lifecycle.Client) error
if syncWhitelist {
prepareStart = func(lifecycle.Client) error {
return runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallSyncWhitelistStep"), func() error {
return s.SyncPortWhitelist(ctx)
})
}
}
operationErr := operateFirewallLifecycle(firewallLifecycleClient{client}, request.Operation, request.WithDockerRestart, prepareStart, t)
if request.Operation == string(lifecycle.OperationStop) {
return operationErr
+117 -3
View File
@@ -554,6 +554,58 @@ func (s *FirewallService) checkSelectedProvider(ctx context.Context, requested f
return nil
}
func prepareFirewallRuleCreate(request dto.FirewallRuleCreate) (dto.FirewallRuleCreate, error) {
if request.BackupFile != "" {
if len(request.Items) != 0 {
return request, filter.ErrInvalidRule
}
items, err := readFirewallBackup(request.BackupFile)
if err != nil {
return request, err
}
for _, item := range items {
request.Items = append(request.Items, dto.FirewallRuleCreateItem{Rule: item.FirewallRule, Raw: item.Raw, ParseStatus: item.ParseStatus, SourceKind: constant.FirewallRuleSourceImported})
}
}
for index, item := range request.Items {
if item.SourceKind != constant.FirewallRuleSourceImported || item.Rule.Scope.Provider != filter.ProviderUFW || item.ParseStatus != filter.ParseStatusPartial || item.Raw == "" {
continue
}
observed := filterufw.ParseRules(item.Rule.Scope, item.Raw)
if len(observed) == 1 && observed[0].ParseStatus == filter.ParseStatusSupported {
observed[0].Rule.Description = item.Rule.Description
request.Items[index].Rule = observed[0].Rule
request.Items[index].ParseStatus, request.Items[index].Raw = filter.ParseStatusSupported, ""
}
}
return request, nil
}
func (s *FirewallService) runFirewallCreateRulesTask(t *task.Task, request dto.FirewallRuleCreate, provider filter.Provider) error {
ruleState := make(map[string]filter.RuleSet)
firewallRuleMutationMu.Lock()
if err := s.checkSelectedProvider(t.TaskCtx, provider); err != nil {
firewallRuleMutationMu.Unlock()
return err
}
_, err := s.createRules(t.TaskCtx, request, t, ruleState)
firewallRuleMutationMu.Unlock()
if !request.Initialize || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return err
}
if err != nil {
ruleState = nil
}
whitelistErr := s.syncPortWhitelist(t.TaskCtx, ruleState)
if whitelistErr == nil && provider == filter.ProviderFirewalld {
whitelistErr = lifecycleproviders.RemoveFirewalldSSHService()
}
t.LogWithStatus(i18n.GetMsgByKey("FirewallSyncWhitelistStep"), whitelistErr)
return errors.Join(err, whitelistErr)
}
func (s *FirewallService) createRules(ctx context.Context, request dto.FirewallRuleCreate, t *task.Task, ruleState map[string]filter.RuleSet) (result dto.FirewallRuleCreateResponse, taskErr error) {
var firstFailure error
defer func() {
@@ -1734,9 +1786,6 @@ func operateFirewallLifecycle(client lifecycle.Client, operation string, withDoc
var recoveryErrors []error
if prepareStart != nil {
err := prepareStart(client)
if err == nil && client.Name() == constant.FirewallProviderFirewalld {
err = lifecycleproviders.RemoveFirewalldSSHService()
}
if err != nil {
recoveryErrors = append(recoveryErrors, fmt.Errorf("prepare firewall after %s: %w", operation, err))
}
@@ -1758,17 +1807,82 @@ func operateFirewallLifecycle(client lifecycle.Client, operation string, withDoc
}
func (c firewallLifecycleClient) Start() error {
firewallWhitelistMu.Lock()
defer firewallWhitelistMu.Unlock()
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
if err := c.prepareAccessWhitelistBeforeStart(); err != nil {
global.LOG.Warnf("prepare %s access whitelist before start failed: %v", c.Name(), err)
}
return c.Client.Start()
}
func (c firewallLifecycleClient) Restart() error {
firewallWhitelistMu.Lock()
defer firewallWhitelistMu.Unlock()
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
if err := c.prepareAccessWhitelistBeforeStart(); err != nil {
global.LOG.Warnf("prepare %s access whitelist before restart failed: %v", c.Name(), err)
}
return c.Client.Restart()
}
func (c firewallLifecycleClient) prepareAccessWhitelistBeforeStart() error {
provider := filter.Provider(c.Name())
if provider != filter.ProviderFirewalld && provider != filter.ProviderUFW {
return nil
}
ports, err := LoadRequiredFirewallPortWhiteList()
if err != nil {
return err
}
var adapter filter.Adapter
var active bool
if provider == filter.ProviderFirewalld {
active, err = c.Status()
if err != nil {
return err
}
adapter = filterfirewalld.NewAdapter()
} else {
adapter = filterufw.NewAdapter()
}
var commands []filter.NativeCommand
for _, port := range firewall.ExpandPortWhitelist(ports) {
rule := firewall.RuleForSystemPort(provider, port)
rule.Scope = rule.Scope.Normalize()
rule.UUID = uuid.NewString()
if provider == filter.ProviderUFW {
position := int64(1)
rule.OrderIndex = &position
}
plan, err := adapter.BuildCommands(filter.RuleSet{Scope: rule.Scope}, []filter.RuleChange{{Operation: filter.ChangeCreate, After: &rule}})
if err != nil {
return err
}
for _, item := range plan.Rules {
for _, command := range item.Commands {
if provider == filter.ProviderFirewalld {
if active {
command.Args = append(command.Args, "--permanent")
} else {
command.Executable = "firewall-offline-cmd"
}
}
commands = append(commands, command)
}
}
}
manager := cmd.NewCommandMgr(cmd.WithTimeout(60*time.Second), cmd.WithEnv("LC_ALL=C", "LANGUAGE=en_US:en"))
for _, command := range commands {
if err := manager.RunWithOptionalSudo(command.Executable, command.Args...); err != nil {
return fmt.Errorf("prepare %s access whitelist before start: %w", provider, err)
}
}
return nil
}
func runFirewallLifecycleAction(t *task.Task, name string, action func() error) error {
t.Log(i18n.GetWithName("TaskStart", name))
started := time.Now()
+1 -1
View File
@@ -55,7 +55,7 @@ type IMonitorService interface {
LoadIOOptions() []string
LoadNetworkOptions() []string
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) (dto.MonitorVLLMCurrentData, error)
CleanVLLMMonitor(req dto.MonitorVLLMClean) error
LoadGPUOptions() dto.MonitorGPUOptions
LoadGPUMonitorData(req dto.MonitorGPUSearch) (dto.MonitorGPUData, error)
+34 -28
View File
@@ -60,6 +60,9 @@ func (m *MonitorService) LoadVLLMMonitorData(ctx context.Context, req dto.Monito
if err != nil {
return data, err
}
for i := range points {
points[i].MonitorVLLM.CompletedRequests = points[i].CompletedRequests
}
if data.BucketSeconds > 0 {
aggregates := map[int64]*vllm.HistoryAccumulator{}
err = vllmMonitorRepo.WalkDeltas(ctx, req.AppInstallID, req.StartTime, req.EndTime, data.BucketSeconds, req.Aggregation, req.Group, func(bucket int64, histograms, requests string) error {
@@ -91,15 +94,6 @@ func (m *MonitorService) LoadVLLMMonitorData(ctx context.Context, req dto.Monito
data.Points = append(data.Points, point)
}
} else {
setting, err := settingRepo.GetValueByKey("VLLMMonitorInterval")
if err != nil {
return data, err
}
intervalSeconds, err := strconv.Atoi(setting)
if err != nil || intervalSeconds <= 0 {
return data, fmt.Errorf("invalid vLLM monitoring interval: %s", setting)
}
interval := time.Duration(intervalSeconds) * time.Second
for i, point := range points {
if err := ctx.Err(); err != nil {
return data, err
@@ -109,8 +103,20 @@ func (m *MonitorService) LoadVLLMMonitorData(ctx context.Context, req dto.Monito
return data, err
}
aggregate.Apply(&point.MonitorVLLM, "max")
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})
if i > 0 {
previous := points[i-1]
interval := time.Duration(max(previous.SampleIntervalSeconds, point.SampleIntervalSeconds)) * time.Second
if interval == 0 {
if i > 1 {
interval = previous.CreatedAt.Sub(points[i-2].CreatedAt)
}
if i+1 < len(points) {
interval = max(interval, points[i+1].CreatedAt.Sub(point.CreatedAt))
}
}
if interval > 0 && point.CreatedAt.Sub(previous.CreatedAt) > 2*interval {
data.Points = append(data.Points, model.MonitorVLLM{CreatedAt: previous.CreatedAt.Add(interval), AppInstallID: req.AppInstallID})
}
}
data.Points = append(data.Points, point.MonitorVLLM)
}
@@ -118,31 +124,30 @@ func (m *MonitorService) LoadVLLMMonitorData(ctx context.Context, req dto.Monito
return data, nil
}
func (m *MonitorService) LoadVLLMCurrent(ctx context.Context, req dto.MonitorVLLMCurrent) (model.MonitorVLLM, error) {
func (m *MonitorService) LoadVLLMCurrent(ctx context.Context, req dto.MonitorVLLMCurrent) (dto.MonitorVLLMCurrentData, error) {
install, err := appInstallRepo.GetFirst(repo.WithByID(req.AppInstallID))
if err != nil {
return model.MonitorVLLM{}, err
return dto.MonitorVLLMCurrentData{}, err
}
if install.App.Key != "vllm" {
return model.MonitorVLLM{}, fmt.Errorf("not a vLLM installation")
return dto.MonitorVLLMCurrentData{}, fmt.Errorf("not a vLLM installation")
}
previous, err := collectVLLMMetrics(ctx, install, model.MonitorVLLM{})
point, err := collectVLLMMetrics(ctx, install, model.MonitorVLLM{})
data := dto.MonitorVLLMCurrentData{MonitorVLLM: point}
if err != nil {
global.LOG.Debugf("Collect vLLM metrics on port %d failed: %v", install.HttpPort, err)
return previous, nil
return data, nil
}
timer := time.NewTimer(time.Second)
defer timer.Stop()
select {
case <-ctx.Done():
return model.MonitorVLLM{}, ctx.Err()
case <-timer.C:
if err := json.Unmarshal([]byte(point.RawMetrics), &data.Metrics); err != nil {
return data, err
}
point, err := collectVLLMMetrics(ctx, install, previous)
if err != nil {
global.LOG.Debugf("Collect vLLM metrics on port %d failed: %v", install.HttpPort, err)
for name := range data.Metrics {
if name == "request_prompt_tokens_sum" || name == "request_prompt_tokens_count" ||
(!strings.HasSuffix(name, "_total") && !strings.HasSuffix(name, "_sum") && !strings.HasSuffix(name, "_count")) {
delete(data.Metrics, name)
}
}
return point, nil
return data, nil
}
func (m *MonitorService) CleanVLLMMonitor(req dto.MonitorVLLMClean) error {
@@ -164,7 +169,7 @@ func StartVLLMMonitor(interval string) error {
return fmt.Errorf("invalid vLLM monitoring interval: %s", interval)
}
service := &MonitorService{}
job := cron.NewChain(cron.Recover(cron.DefaultLogger)).Then(cron.FuncJob(service.saveVLLMData))
job := cron.NewChain(cron.Recover(cron.DefaultLogger)).Then(cron.FuncJob(func() { service.saveVLLMData(seconds) }))
id, err := global.Cron.AddFunc(fmt.Sprintf("@every %ds", seconds), func() { go job.Run() })
if err != nil {
return err
@@ -176,7 +181,7 @@ func StartVLLMMonitor(interval string) error {
return nil
}
func (m *MonitorService) saveVLLMData() {
func (m *MonitorService) saveVLLMData(intervalSeconds int) {
if !vllmMonitorMutex.TryLock() {
return
}
@@ -228,6 +233,7 @@ func (m *MonitorService) saveVLLMData() {
return err
}
point, err := collectVLLMMetrics(context.Background(), install, previous)
point.SampleIntervalSeconds = intervalSeconds
if err != nil {
global.LOG.Debugf("Collect vLLM metrics for %s failed: %v", install.Name, err)
}
+10 -14
View File
@@ -108,33 +108,29 @@
"formatEN": "create alert [title]"
},
"/alert/config/del": {
"bodyKeys": [
"id"
],
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除告警配置 [id]",
"formatEN": "delete alert config [id]"
"formatZH": "删除告警配置 [name]",
"formatEN": "delete alert config [name]",
"beforeFunctions": []
},
"/alert/config/status": {
"bodyKeys": [
"id",
"status"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新告警配置状态 [id][status]",
"formatEN": "update alert config status [id][status]"
"formatZH": "更新告警配置状态 [name][status]",
"formatEN": "update alert config status [name][status]",
"beforeFunctions": []
},
"/alert/config/update": {
"bodyKeys": [
"id",
"displayName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新告警配置 [id][displayName]",
"formatEN": "update alert config [id][displayName]"
"formatZH": "更新告警配置 [displayName]",
"formatEN": "update alert config [displayName]",
"beforeFunctions": []
},
"/alert/del": {
"bodyKeys": [
+2
View File
@@ -10,6 +10,8 @@ const (
AppOpenclaw = "openclaw"
AppCopaw = "copaw"
AppHermesAgent = "hermes-agent"
AppDeepSeekHarness = "deepseek-harness"
AppOctop = "octop"
AppMysql = "mysql"
AppMariaDB = "mariadb"
AppPostgresql = "postgresql"
+2 -2
View File
@@ -38,7 +38,7 @@ require (
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pkg/errors v0.9.1
github.com/pkg/sftp v1.13.11
github.com/qiniu/go-sdk/v7 v7.29.0
github.com/qiniu/go-sdk/v7 v7.27.0
github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.26.9
github.com/sirupsen/logrus v1.10.2
@@ -66,7 +66,6 @@ require (
require (
filippo.io/edwards25519 v1.2.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect
@@ -217,6 +216,7 @@ require (
go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect
go4.org v0.0.0-20260112195520-a5071408f32f // 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
google.golang.org/grpc v1.83.2 // indirect
google.golang.org/protobuf v1.36.11 // indirect
+50 -6
View File
@@ -228,12 +228,18 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
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/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/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/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/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=
@@ -248,6 +254,8 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m
github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
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/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=
@@ -565,6 +573,8 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
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/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=
@@ -701,6 +711,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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
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/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=
@@ -723,8 +735,8 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/qiniu/go-sdk/v7 v7.29.0 h1:W6bnl2IEtpkKH/MJtYC3HKGwcEViu7hqZ2OWc0Dg78Q=
github.com/qiniu/go-sdk/v7 v7.29.0/go.mod h1:pTwVR1B+8SXcPLhDzBUasiKFTD9F7jRglRDR553BW3k=
github.com/qiniu/go-sdk/v7 v7.27.0 h1:n+2U0S5fhbmG/lN/agO8KcYJaQDqROUVShtnp56Mkw8=
github.com/qiniu/go-sdk/v7 v7.27.0/go.mod h1:pTwVR1B+8SXcPLhDzBUasiKFTD9F7jRglRDR553BW3k=
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/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
@@ -750,6 +762,10 @@ github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9t
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
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/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=
@@ -876,8 +892,14 @@ go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0=
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/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/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=
@@ -952,6 +974,10 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
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/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=
@@ -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-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.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-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -995,8 +1023,9 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
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.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
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-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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1051,6 +1080,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
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.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
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=
@@ -1077,6 +1108,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
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.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-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1138,7 +1171,6 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1155,6 +1187,10 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
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=
@@ -1170,8 +1206,10 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
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.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
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/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY=
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.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1188,6 +1226,10 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
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.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
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=
@@ -1251,8 +1293,10 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
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.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
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/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA=
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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+2
View File
@@ -114,6 +114,8 @@ func agentDBMigrations() []*gormigrate.Migration {
migrations.AddVLLMMonitor,
migrations.AddMonitorSettings,
migrations.AddVLLMRequestMetrics,
migrations.AddVLLMSampleInterval,
migrations.AddNativeAgentInstalls,
}
}
@@ -0,0 +1,47 @@
package migrations
import (
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
)
var AddNativeAgentInstalls = &gormigrate.Migration{
ID: "20261020-add-deepseek-harness-octop-agent-installs",
Migrate: func(tx *gorm.DB) error {
var installs []struct {
model.AppInstall
AgentType string
}
if err := tx.Table("app_installs").
Select("app_installs.*, apps.key AS agent_type").
Joins("JOIN apps ON apps.id = app_installs.app_id").
Where("apps.key IN ?", []string{constant.AppDeepSeekHarness, constant.AppOctop}).
Where("NOT EXISTS (SELECT 1 FROM agents WHERE agents.app_install_id = app_installs.id)").
Order("app_installs.id").
Scan(&installs).Error; err != nil {
return err
}
for _, install := range installs {
var count int64
if err := tx.Model(&model.Agent{}).Where("LOWER(name) = LOWER(?)", install.Name).Count(&count).Error; err != nil {
return err
}
if count > 0 {
global.LOG.Warnf("skip agent registration for app install %d: name already exists", install.ID)
continue
}
agent := model.Agent{
BaseModel: model.BaseModel{CreatedAt: install.CreatedAt, UpdatedAt: install.UpdatedAt},
Name: install.Name, AgentType: install.AgentType, AppInstallID: install.ID,
Status: install.Status, Message: install.Message,
}
if err := tx.Create(&agent).Error; err != nil {
return err
}
}
return nil
},
}
+7
View File
@@ -1881,3 +1881,10 @@ var AddVLLMRequestMetrics = &gormigrate.Migration{
return global.VLLMMonitorDB.AutoMigrate(&model.MonitorVLLM{})
},
}
var AddVLLMSampleInterval = &gormigrate.Migration{
ID: "20261016-vllm-sample-interval",
Migrate: func(tx *gorm.DB) error {
return global.VLLMMonitorDB.AutoMigrate(&model.MonitorVLLM{})
},
}
+11 -1
View File
@@ -314,7 +314,17 @@ func (s *Session) detach(a *attachment, clean, revalidate bool, cursor uint64) {
if !s.Persistent || revalidate {
timeout = revalidateGrace
}
s.grace = time.AfterFunc(timeout, s.Close)
// Stop cannot cancel a fired timer; a racing Attach clears s.grace.
var grace *time.Timer
grace = time.AfterFunc(timeout, func() {
s.mu.Lock()
expired := s.grace == grace
s.mu.Unlock()
if expired {
s.Close()
}
})
s.grace = grace
}
s.mu.Unlock()
if clean {
+9 -12
View File
@@ -3906,13 +3906,11 @@ const docTemplate = `{
"Alert"
],
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"id"
],
"formatEN": "delete alert config [id]",
"formatZH": "删除告警配置 [id]",
"paramKeys": []
"bodyKeys": [],
"paramKeys": [],
"formatZH": "删除告警配置 [name]",
"formatEN": "delete alert config [name]",
"BeforeFunctions": []
}
}
},
@@ -4041,14 +4039,13 @@ const docTemplate = `{
"Alert"
],
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"id",
"displayName"
],
"formatEN": "update alert config [id][displayName]",
"formatZH": "更新告警配置 [id][displayName]",
"paramKeys": []
"paramKeys": [],
"formatZH": "更新告警配置 [displayName]",
"formatEN": "update alert config [displayName]",
"BeforeFunctions": []
}
}
},
+9 -12
View File
@@ -3902,13 +3902,11 @@
"Alert"
],
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"id"
],
"formatEN": "delete alert config [id]",
"formatZH": "删除告警配置 [id]",
"paramKeys": []
"bodyKeys": [],
"paramKeys": [],
"formatZH": "删除告警配置 [name]",
"formatEN": "delete alert config [name]",
"BeforeFunctions": []
}
}
},
@@ -4037,14 +4035,13 @@
"Alert"
],
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"id",
"displayName"
],
"formatEN": "update alert config [id][displayName]",
"formatZH": "更新告警配置 [id][displayName]",
"paramKeys": []
"paramKeys": [],
"formatZH": "更新告警配置 [displayName]",
"formatEN": "update alert config [displayName]",
"BeforeFunctions": []
}
}
},
+10 -14
View File
@@ -108,33 +108,29 @@
"formatEN": "create alert [title]"
},
"/alert/config/del": {
"bodyKeys": [
"id"
],
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除告警配置 [id]",
"formatEN": "delete alert config [id]"
"formatZH": "删除告警配置 [name]",
"formatEN": "delete alert config [name]",
"beforeFunctions": []
},
"/alert/config/status": {
"bodyKeys": [
"id",
"status"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新告警配置状态 [id][status]",
"formatEN": "update alert config status [id][status]"
"formatZH": "更新告警配置状态 [name][status]",
"formatEN": "update alert config status [name][status]",
"beforeFunctions": []
},
"/alert/config/update": {
"bodyKeys": [
"id",
"displayName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新告警配置 [id][displayName]",
"formatEN": "update alert config [id][displayName]"
"formatZH": "更新告警配置 [displayName]",
"formatEN": "update alert config [displayName]",
"beforeFunctions": []
},
"/alert/del": {
"bodyKeys": [
+3
View File
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "An unrevoked API key with this name already exists"
ErrAPIKeyNotFound: "The API key does not exist or does not belong to the current user"
ErrAPIKeyAppBindingDisabled: "APP QR binding is not allowed for this API key"
ErrAPIKeyTerminalUpgradeRequired: "Unable to verify API key terminal authorization support. Check the node connection and upgrade its Agent before retrying."
LocalAppSyncDispatch: "Dispatch local app sync"
LocalAppSyncDispatchNode: "Dispatch local app sync to {{.node}} (node task: {{.taskID}})"
ErrLocalAppSyncDisabled: "Local app sync across nodes is disabled"
+3
View File
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "Ya existe una clave API no revocada con este nombre"
ErrAPIKeyNotFound: "La clave API no existe o no pertenece al usuario actual"
ErrAPIKeyAppBindingDisabled: "Esta clave API no permite vincular la APP mediante código QR"
ErrAPIKeyTerminalUpgradeRequired: "No se pudo verificar la autorización de terminal con clave API. Compruebe la conexión del nodo y actualice su Agent antes de reintentar."
LocalAppSyncDispatch: "Enviar sincronización de apps locales"
LocalAppSyncDispatchNode: "Enviar sincronización de apps locales a {{.node}} (tarea del nodo: {{.taskID}})"
ErrLocalAppSyncDisabled: "La sincronización de apps locales entre nodos está desactivada"
+3
View File
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "یک کلید API لغونشده با این نام از
ErrAPIKeyNotFound: "کلید API وجود ندارد یا متعلق به کاربر فعلی نیست"
ErrAPIKeyAppBindingDisabled: "اتصال برنامه با کد QR برای این کلید API مجاز نیست"
ErrAPIKeyTerminalUpgradeRequired: "امکان بررسی پشتیبانی از مجوزدهی ترمینال با کلید API وجود ندارد. اتصال گره را بررسی و Agent آن را ارتقا دهید، سپس دوباره تلاش کنید."
LocalAppSyncDispatch: "ارسال درخواست همگام‌سازی برنامه‌های محلی"
LocalAppSyncDispatchNode: "ارسال درخواست همگام‌سازی به {{.node}} (وظیفه گره: {{.taskID}})"
ErrLocalAppSyncDisabled: "همگام‌سازی برنامه‌های محلی بین گره‌ها غیرفعال است"
+3
View File
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "同じ名前の未失効 API キーが既に存在しま
ErrAPIKeyNotFound: "API キーが存在しないか、現在のユーザーに属していません"
ErrAPIKeyAppBindingDisabled: "この API キーでは APP の QR コード連携が許可されていません"
ErrAPIKeyTerminalUpgradeRequired: "API キーによるターミナル認証への対応を確認できません。ノードの接続を確認し、Agent を更新して再試行してください。"
LocalAppSyncDispatch: "ローカルアプリ同期要求の送信"
LocalAppSyncDispatchNode: "{{.node}} にローカルアプリ同期要求を送信(ノードタスク: {{.taskID}})"
ErrLocalAppSyncDisabled: "ノード間のローカルアプリ同期は無効です"
+3
View File
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "같은 이름의 폐기되지 않은 API Key가 이미 있
ErrAPIKeyNotFound: "API Key가 없거나 현재 사용자 소유가 아닙니다"
ErrAPIKeyAppBindingDisabled: "이 API Key는 APP QR 코드 연결을 허용하지 않습니다"
ErrAPIKeyTerminalUpgradeRequired: "API Key 터미널 인증 지원 여부를 확인할 수 없습니다. 노드 연결을 확인하고 Agent를 업그레이드한 후 다시 시도하세요."
LocalAppSyncDispatch: "로컬 앱 동기화 요청 전송"
LocalAppSyncDispatchNode: "{{.node}}에 로컬 앱 동기화 요청 전송 (노드 작업: {{.taskID}})"
ErrLocalAppSyncDisabled: "노드 간 로컬 앱 동기화가 비활성화되어 있습니다"
+3
View File
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "ມີ API Key ທີ່ຍັງບໍ່ຖືກຖອ
ErrAPIKeyNotFound: "ບໍ່ພົບ API Key ຫຼືບໍ່ແມ່ນຂອງຜູ້ໃຊ້ປັດຈຸບັນ"
ErrAPIKeyAppBindingDisabled: "API Key ນີ້ບໍ່ອະນຸຍາດໃຫ້ຜູກ APP ຜ່ານລະຫັດ QR"
ErrAPIKeyTerminalUpgradeRequired: "ບໍ່ສາມາດຢືນຢັນການຮອງຮັບການອະນຸຍາດເທີມິນອນດ້ວຍ API Key. ກວດສອບການເຊື່ອມຕໍ່ໂນດ ແລະອັບເກຣດ Agent ກ່ອນລອງອີກ."
LocalAppSyncDispatch: "ສົ່ງຄຳຂໍຊິງຄ໌ແອັບໃນເຄື່ອງ"
LocalAppSyncDispatchNode: "ສົ່ງຄຳຂໍຊິງຄ໌ໄປຫາ {{.node}} (ວຽກຂອງໂນດ: {{.taskID}})"
ErrLocalAppSyncDisabled: "ການຊິງຄ໌ແອັບລະຫວ່າງໂນດຍັງບໍ່ໄດ້ເປີດ"
+3
View File
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "Kunci API yang belum dibatalkan dengan nama ini sudah wuju
ErrAPIKeyNotFound: "Kunci API tidak wujud atau bukan milik pengguna semasa"
ErrAPIKeyAppBindingDisabled: "Pemautan APP melalui kod QR tidak dibenarkan untuk kunci API ini"
ErrAPIKeyTerminalUpgradeRequired: "Sokongan pengesahan terminal melalui kunci API tidak dapat disahkan. Semak sambungan nod dan naik taraf Agent sebelum mencuba lagi."
LocalAppSyncDispatch: "Hantar penyegerakan aplikasi tempatan"
LocalAppSyncDispatchNode: "Hantar penyegerakan aplikasi tempatan ke {{.node}} (tugas nod: {{.taskID}})"
ErrLocalAppSyncDisabled: "Penyegerakan aplikasi tempatan antara nod dinyahdayakan"
+3
View File
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "Já existe uma chave API não revogada com esse nome"
ErrAPIKeyNotFound: "A chave API não existe ou não pertence ao usuário atual"
ErrAPIKeyAppBindingDisabled: "Esta chave API não permite vincular o APP por código QR"
ErrAPIKeyTerminalUpgradeRequired: "Não foi possível verificar o suporte à autorização de terminal por chave API. Verifique a conexão do nó e atualize o Agent antes de tentar novamente."
LocalAppSyncDispatch: "Enviar sincronização de apps locais"
LocalAppSyncDispatchNode: "Enviar sincronização de apps locais para {{.node}} (tarefa do nó: {{.taskID}})"
ErrLocalAppSyncDisabled: "A sincronização de apps locais entre nós está desativada"
+3
View File
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "Неотозванный ключ API с таким име
ErrAPIKeyNotFound: "Ключ API не существует или не принадлежит текущему пользователю"
ErrAPIKeyAppBindingDisabled: "Для этого ключа API привязка приложения через QR-код запрещена"
ErrAPIKeyTerminalUpgradeRequired: "Не удалось проверить поддержку авторизации терминала по ключу API. Проверьте подключение узла, обновите его Agent и повторите попытку."
LocalAppSyncDispatch: "Отправка запросов синхронизации локальных приложений"
LocalAppSyncDispatchNode: "Отправка запроса синхронизации узлу {{.node}} (задача узла: {{.taskID}})"
ErrLocalAppSyncDisabled: "Синхронизация локальных приложений между узлами отключена"
+3
View File
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "Bu adda iptal edilmemiş bir API anahtarı zaten var"
ErrAPIKeyNotFound: "API anahtarı mevcut değil veya geçerli kullanıcıya ait değil"
ErrAPIKeyAppBindingDisabled: "Bu API anahtarı için QR koduyla APP bağlantısına izin verilmiyor"
ErrAPIKeyTerminalUpgradeRequired: "API anahtarıyla terminal yetkilendirme desteği doğrulanamadı. Düğüm bağlantısını kontrol edip Agent sürümünü yükselttikten sonra tekrar deneyin."
LocalAppSyncDispatch: "Yerel uygulama eşitleme isteği gönder"
LocalAppSyncDispatchNode: "{{.node}} düğümüne yerel uygulama eşitleme isteği gönder (düğüm görevi: {{.taskID}})"
ErrLocalAppSyncDisabled: "Düğümler arası yerel uygulama eşitleme devre dışı"
+3
View File
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "已存在同名且未撤銷的 API Key"
ErrAPIKeyNotFound: "API Key 不存在或不屬於目前使用者"
ErrAPIKeyAppBindingDisabled: "此 API Key 未允許 APP 掃碼綁定"
ErrAPIKeyTerminalUpgradeRequired: "無法確認節點支援 API Key 終端授權,請檢查節點連線並升級 Agent 後重試。"
LocalAppSyncDispatch: "下發本機應用同步"
LocalAppSyncDispatchNode: "向節點 {{.node}} 下發本機應用同步(節點任務:{{.taskID}})"
ErrLocalAppSyncDisabled: "尚未啟用本機應用跨節點同步"
+3
View File
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "已存在同名的未撤销 API Key"
ErrAPIKeyNotFound: "API Key 不存在或不属于当前用户"
ErrAPIKeyAppBindingDisabled: "此 API Key 未允许 APP 扫码绑定"
ErrAPIKeyTerminalUpgradeRequired: "无法确认节点支持 API Key 终端授权,请检查节点连接并升级 Agent 后重试。"
LocalAppSyncDispatch: "下发本地应用同步"
LocalAppSyncDispatchNode: "向节点 {{.node}} 下发本地应用同步(节点任务:{{.taskID}})"
ErrLocalAppSyncDisabled: "尚未开启本地应用跨节点同步"
+2 -4
View File
@@ -133,11 +133,9 @@ func OperationLog() gin.HandlerFunc {
record.AuthMethod = "session"
}
// Handlers may supply audit values even without database resolution metadata.
mergeResolvedData(writer.resolvedHeader, formatMap)
if len(operationDic.BeforeFunctions) != 0 {
if needAgentResolve {
mergeResolvedData(writer.resolvedHeader, formatMap)
}
if allowCoreFallback && !hasAllResolvedData(formatMap, operationDic.BeforeFunctions) {
dbItem, err := newDB(record.Path)
if err == nil {
+3 -1
View File
@@ -1,7 +1,7 @@
import { ReqPage } from '.';
export namespace AI {
export type AgentType = 'openclaw' | 'copaw' | 'hermes-agent';
export type AgentType = 'openclaw' | 'copaw' | 'hermes-agent' | 'deepseek-harness' | 'octop';
export interface OllamaModelInfo {
id: number;
@@ -423,6 +423,7 @@ export namespace AI {
}
export interface AgentCreateReq {
accessHost?: string;
name: string;
remark: string;
appVersion: string;
@@ -450,6 +451,7 @@ export namespace AI {
}
export interface AgentItem {
accessHost: string;
id: number;
name: string;
remark: string;
+3
View File
@@ -1,6 +1,9 @@
import { ReqPage, CommonModel } from '.';
export namespace App {
export interface LocalAppSyncConfig {
enabled: boolean;
}
export interface App extends CommonModel {
name: string;
icon?: string;
+12
View File
@@ -144,3 +144,15 @@ export function getAppIconUrl(appKey: string, node?: string): string {
export const installAppToNodes = (param: App.InstallAppToNodes) => {
return http.post(`/core/xpack/sync/app/install`, param);
};
export const getLocalAppSyncConfig = () => {
return http.get<App.LocalAppSyncConfig>('/core/xpack/settings/app/local-sync');
};
export const updateLocalAppSyncConfig = (config: App.LocalAppSyncConfig) => {
return http.post('/core/xpack/settings/app/local-sync', config);
};
export const dispatchLocalAppSync = (req: App.AppStoreSync) => {
return http.post('/core/xpack/settings/app/local-sync/dispatch', req);
};
+2 -2
View File
@@ -21,10 +21,10 @@ export const loadForwardBaseInfo = () =>
export const searchForwardRule = (request: Firewall.ForwardRuleSearch) =>
http.post<ResPage<Firewall.RuleInfo>>('/hosts/firewall/forward/search', request, TimeoutEnum.T_40S);
export const operateFire = (operation: string, withDockerRestart: boolean) =>
export const operateFire = (operation: string, withDockerRestart: boolean, backupFile?: string) =>
http.post<Firewall.FilterChainOperationResult>(
'/hosts/firewall/operate',
{ operation, withDockerRestart },
{ operation, withDockerRestart, backupFile },
TimeoutEnum.T_10M,
);
Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

@@ -8,10 +8,7 @@
<el-table-column :label="$t('commons.table.name')" prop="name">
<template #default="{ row }">
<div v-if="!row.edit">
<span v-if="row.name === 'Default'">
{{ $t('commons.table.default') }}
</span>
<span v-if="row.name !== 'Default'">{{ row.name }}</span>
<span>{{ row.name }}</span>
<el-tag v-if="row.isDefault" type="success" class="ml-2" size="small">
({{ $t('commons.table.default') }})
</el-tag>
+1 -4
View File
@@ -8,10 +8,7 @@
<el-table-column :label="$t('commons.table.name')" prop="name">
<template #default="{ row }">
<div v-if="!row.edit">
<span v-if="row.name === 'Default'">
{{ $t('commons.table.default') }}
</span>
<span v-if="row.name !== 'Default'">{{ row.name }}</span>
<span>{{ row.name }}</span>
<el-tag v-if="row.isDefault" type="success" class="ml-2" size="small">
({{ $t('commons.table.default') }})
</el-tag>
@@ -154,7 +154,7 @@ const connectionTree = computed<ConnectionTreeItem[]>(() => {
groups.push(
...hostTree.value.map((group): ConnectionTreeItem => ({
id: `host-group-${group.id}`,
label: group.label === 'Default' ? i18n.global.t('commons.table.default') : group.label,
label: group.label,
kind: 'group',
children: (group.children || []).map((host) => ({
id: `host-${host.id}`,
@@ -18,6 +18,7 @@
size="w-70"
:show-close="false"
:modal="false"
modal-penetrable
@opened="claim"
>
<template #header>
@@ -38,14 +38,7 @@
</el-form-item>
<el-form-item v-if="!form.isLocal" :label="$t('commons.table.group')" prop="groupID">
<el-select filterable v-model="form.groupID" clearable style="width: 100%">
<div v-for="item in groupList" :key="item.id">
<el-option
v-if="item.name === 'Default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
<el-option v-for="item in groupList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item v-if="!form.isLocal" :label="$t('commons.table.title')" prop="name">
@@ -7,6 +7,7 @@
:ref="(el: any) => store.setInstance(item.key, el)"
@session="(id: string) => store.setSessionId(item.key, id)"
@expired="store.onExpired(item.key)"
@reconnect="store.reconnect(item.key)"
/>
</Teleport>
</template>
+11 -1
View File
@@ -31,7 +31,7 @@ const { currentNode } = useGlobalStore();
// session: agent side session id known (fresh or reattached)
// expired: the agent no longer has the session; a reconnect must open a new one
const emit = defineEmits(['session', 'expired']);
const emit = defineEmits(['session', 'expired', 'reconnect']);
// Close codes of the agent's session protocol (agent/utils/terminal/session.go).
const CLOSE_SESSION_NOT_FOUND = 4404;
@@ -56,6 +56,7 @@ let closing = false;
let reconnecting = false;
let reconnectNoticeShown = false;
let revalidating = false;
let expired = false;
let reconnectStartedAt = 0;
let reconnectDelay = 1000;
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
@@ -279,6 +280,7 @@ function resizeTerminal() {
if (terminalElement.value.clientWidth <= 0 || terminalElement.value.clientHeight <= 0) {
return;
}
if (terminalElement.value.closest('.terminal-host')) return;
fitAddon.fit();
if (isWsOpen()) {
@@ -484,6 +486,7 @@ const closeRealTerminal = (ev: CloseEvent) => {
'31',
ev.code === 1000 ? 'The connection has been disconnected.' : i18n.global.t('terminal.sessionExpired'),
);
expired = true;
emit('expired');
return;
case CLOSE_ATTACHED_ELSEWHERE:
@@ -602,6 +605,13 @@ function sendMsg(data: string, line: string = '') {
function onTermData(data: string) {
if (!data) return;
if (expired) {
if (isEnterInputData(data)) {
expired = false;
emit('reconnect');
}
return;
}
if (aiNotice.value.loading) return;
sendMsg(data, isEnterInputData(data) ? getCurrentTerminalLine() : '');
}
@@ -4,11 +4,8 @@
<script lang="ts" setup>
import { onMounted, nextTick, watch, onBeforeUnmount, ref } from 'vue';
import echarts from '@/utils/echarts';
import { useGlobalStore } from '@/composables/useGlobalStore';
const { themeConfig } = useGlobalStore();
const isDarkTheme = ref(false);
const PieChartRef = ref<HTMLElement>();
let mediaQuery: MediaQueryList;
let themeObserver: MutationObserver | undefined;
let resizeObserver: ResizeObserver | undefined;
let resizeFrame: number | undefined;
@@ -47,16 +44,17 @@ function resizeChart() {
echarts.getInstanceByDom(PieChartRef.value)?.resize();
}
function getThemeColors() {
// Match the applied page theme, including while settings are still loading.
const isDarkTheme = document.documentElement.classList.contains('dark');
const style = getComputedStyle(document.documentElement);
return {
primaryLight2: getComputedStyle(document.documentElement)
.getPropertyValue('--panel-color-primary-light-3')
.trim(),
primaryLight1: getComputedStyle(document.documentElement).getPropertyValue('--panel-color-primary').trim(),
pieBgColor: isDarkTheme.value ? '#434552' : '#ffffff',
textColor: isDarkTheme.value ? '#ffffff' : '#0f0f0f',
subtextColor: isDarkTheme.value ? '#BBBFC4' : '#646A73',
shadowColor: isDarkTheme.value ? '#16191D' : 'rgba(0, 94, 235, 0.1)',
backgroundStyleColor: isDarkTheme.value ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 94, 235, 0.05)',
primaryLight2: style.getPropertyValue('--panel-color-primary-light-3').trim() || '#4c8ef1',
primaryLight1: style.getPropertyValue('--panel-color-primary').trim() || '#005eeb',
pieBgColor: isDarkTheme ? '#434552' : '#ffffff',
textColor: isDarkTheme ? '#ffffff' : '#0f0f0f',
subtextColor: isDarkTheme ? '#BBBFC4' : '#646A73',
shadowColor: isDarkTheme ? '#16191D' : 'rgba(0, 94, 235, 0.1)',
backgroundStyleColor: isDarkTheme ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 94, 235, 0.05)',
};
}
@@ -64,11 +62,6 @@ function initChart() {
if (!PieChartRef.value) {
return;
}
if (themeConfig.value.theme === 'auto') {
isDarkTheme.value = window.matchMedia('(prefers-color-scheme: dark)').matches;
} else {
isDarkTheme.value = themeConfig.value.theme === 'dark';
}
let myChart = echarts?.getInstanceByDom(PieChartRef.value);
if (myChart === null || myChart === undefined) {
myChart = echarts.init(PieChartRef.value);
@@ -194,8 +187,11 @@ function handleThemeChange() {
onMounted(() => {
nextTick(() => {
mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', handleThemeChange);
themeObserver = new MutationObserver(handleThemeChange);
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class', 'style'],
});
initChart();
window.addEventListener('resize', changeChartSize);
if (PieChartRef.value) {
@@ -214,7 +210,7 @@ onBeforeUnmount(() => {
echarts.getInstanceByDom(PieChartRef.value)?.dispose();
}
window.removeEventListener('resize', changeChartSize);
mediaQuery?.removeEventListener('change', handleThemeChange);
themeObserver?.disconnect();
resizeObserver?.disconnect();
});
</script>
+24 -1
View File
@@ -773,6 +773,15 @@ const message = {
},
aiTools: {
agents: {
deepSeekAccessHostHelper:
"Enter the IPv4 address or hostname used in your browser, without a scheme, port or path. HTTPS uses the application's local certificate.",
deepSeekAuthHelper:
'Username: letters, numbers, dot, underscore or hyphen. Password: at least 12 characters, without leading/trailing spaces or line breaks.',
octopAuthHelper:
'Password: at least 8 characters with letters and digits; avoid common passwords. If rejected during initialization, check the application logs for setup instructions.',
initialCredentials: "Initial credentials; use the application's current password if changed.",
nativeAgentAccessUnavailable:
'The existing installation has no compatible HTTPS access address or port. Check its application settings.',
agent: 'Agent',
account: 'Model Account',
batchOperation: 'Batch Operation',
@@ -1517,6 +1526,14 @@ const message = {
extraHeaders: 'Extra Headers',
},
vllmMonitor: {
concurrency: 'Current concurrency',
sampleMoment: 'At sampling time',
sampleWindow: 'Sample interval',
requestUnit: 'requests',
processing: 'Processing',
queued: 'Queued',
requestTiming: 'Request timing',
completedRequests: 'Requests completed in interval',
tokensPerSecond: 'tokens/s',
requestsPerSecond: 'Requests/s',
finishReasonLabels: {
@@ -3684,6 +3701,7 @@ const message = {
link: 'Link',
showCurrentArch: 'Architecture',
syncLocalApp: 'Sync Local App',
localAppSyncToNodes: 'Include child nodes when syncing local apps',
memoryRequiredHelper: 'Current application memory requirement {0}',
gpuConfig: 'Enable GPU Support',
gpuConfigHelper:
@@ -4299,9 +4317,14 @@ const message = {
familyIncomplete: '{0} chains are incomplete',
resetWithBackup: 'Back up rules and reset',
resetOnly: 'Reset only',
resetWithBackupHelper: 'Save the current module rules as JSON for restoration during initialization.',
resetWithBackupHelper:
'Save the current module rules as JSON for restoration during initialization or startup.',
resetOnlyHelper: 'Clear current rules without creating a new backup. Existing backups are kept.',
backupDirectoryHelper: 'Valid rule backups in {0} are listed below. The newest file is selected by default.',
startOnly: 'Start only',
startAndRestore: 'Start and restore from backup',
startWithBackupHelper:
'Start the firewall normally, or choose an existing JSON backup to start and restore rules.',
restoreAllRules: 'Initialize and restore rules',
importRuleBackup: 'Import from backup',
initializeOnly: 'Initialize only',
+23 -1
View File
@@ -776,6 +776,15 @@ const message = {
},
aiTools: {
agents: {
deepSeekAccessHostHelper:
'Introduzca la IPv4 o el nombre de host usado en el navegador, sin protocolo, puerto ni ruta. HTTPS usa el certificado local de la aplicación.',
deepSeekAuthHelper:
'Usuario: letras, números, punto, guion bajo o guion. Contraseña: al menos 12 caracteres, sin espacios al inicio o al final ni saltos de línea.',
octopAuthHelper:
'Contraseña: al menos 8 caracteres con letras y números; evite contraseñas comunes. Si se rechaza al iniciar, consulte las instrucciones en los registros de la aplicación.',
initialCredentials: 'Credenciales iniciales; si las cambió en la aplicación, use la contraseña actual.',
nativeAgentAccessUnavailable:
'La instalación existente no tiene una dirección o puerto HTTPS compatible. Revise la configuración de la aplicación.',
agent: 'Agente',
account: 'Cuenta de modelo',
batchOperation: 'Operación por lotes',
@@ -1526,6 +1535,14 @@ const message = {
extraHeaders: 'Encabezados adicionales',
},
vllmMonitor: {
concurrency: 'Concurrencia actual',
sampleMoment: 'Al tomar la muestra',
sampleWindow: 'Intervalo de muestreo',
requestUnit: 'solicitudes',
processing: 'En proceso',
queued: 'En cola',
requestTiming: 'Tiempo de las solicitudes',
completedRequests: 'Solicitudes completadas en el intervalo',
tokensPerSecond: 'tokens/s',
requestsPerSecond: 'Solicitudes/s',
finishReasonLabels: {
@@ -3751,6 +3768,7 @@ const message = {
link: 'Enlace',
showCurrentArch: 'Arquitectura',
syncLocalApp: 'Sincronizar app local',
localAppSyncToNodes: 'Incluir nodos secundarios al sincronizar apps locales',
memoryRequiredHelper: 'La app requiere {0} de memoria',
gpuConfig: 'Habilitar soporte GPU',
gpuConfigHelper: 'Asegúrese de tener GPU NVIDIA, drivers y NVIDIA Docker Container Toolkit instalados',
@@ -4347,10 +4365,14 @@ const message = {
resetWithBackup: 'Guardar reglas y restablecer',
resetOnly: 'Solo restablecer',
resetWithBackupHelper:
'Guarda las reglas del módulo actual en JSON para restaurarlas durante la inicialización.',
'Guarde las reglas del módulo actual en JSON para restaurarlas al inicializar o iniciar el cortafuegos.',
resetOnlyHelper: 'Borrar las reglas actuales sin crear una copia nueva. Se conservan las copias existentes.',
backupDirectoryHelper:
'Se muestran las copias válidas de reglas en {0}. Se selecciona la más reciente por defecto.',
startOnly: 'Solo iniciar',
startAndRestore: 'Iniciar y restaurar desde una copia de seguridad',
startWithBackupHelper:
'Inicie el cortafuegos normalmente o elija una copia JSON existente para iniciar y restaurar reglas.',
restoreAllRules: 'Inicializar y restaurar reglas',
importRuleBackup: 'Importar desde copia de seguridad',
initializeOnly: 'Solo inicializar',
+23 -1
View File
@@ -768,6 +768,14 @@ const message = {
},
aiTools: {
agents: {
deepSeekAccessHostHelper:
'نشانی IPv4 یا نام میزبان مورد استفاده در مرورگر را بدون پروتکل، درگاه یا مسیر وارد کنید. HTTPS از گواهی محلی برنامه استفاده می‌کند.',
deepSeekAuthHelper:
'نام کاربری: حروف لاتین، اعداد، نقطه، زیرخط یا خط تیره. گذرواژه: حداقل ۱۲ نویسه، بدون فاصله در ابتدا و انتها یا شکست خط.',
octopAuthHelper:
'گذرواژه: حداقل ۸ نویسه شامل حروف و اعداد؛ از گذرواژه‌های رایج پرهیز کنید. اگر رد شد، راهنمای تنظیمات را در گزارش‌های برنامه ببینید.',
initialCredentials: 'اطلاعات ورود اولیه؛ اگر در برنامه تغییر کرده، از گذرواژه فعلی استفاده کنید.',
nativeAgentAccessUnavailable: 'نصب موجود نشانی یا درگاه HTTPS سازگار ندارد. تنظیمات برنامه را بررسی کنید.',
agent: 'عامل',
account: 'حساب مدل',
batchOperation: 'عملیات دسته‌ای',
@@ -1502,6 +1510,14 @@ const message = {
extraHeaders: 'هدرهای اضافی',
},
vllmMonitor: {
concurrency: 'درخواست‌های هم‌زمان فعلی',
sampleMoment: 'در لحظه نمونه‌برداری',
sampleWindow: 'بازه نمونه‌برداری',
requestUnit: 'درخواست',
processing: 'در حال پردازش',
queued: 'در صف',
requestTiming: 'زمان پردازش درخواست',
completedRequests: 'تعداد درخواست‌های تکمیل‌شده در بازه',
tokensPerSecond: 'tokens/ثانیه',
requestsPerSecond: 'درخواست/ثانیه',
finishReasonLabels: {
@@ -3650,6 +3666,7 @@ const message = {
link: 'لینک',
showCurrentArch: 'معماری',
syncLocalApp: 'همگام‌سازی برنامه محلی',
localAppSyncToNodes: 'همگام‌سازی گره‌های فرزند هنگام همگام‌سازی برنامه‌های محلی',
memoryRequiredHelper: 'نیاز حافظه برنامه فعلی {0}',
gpuConfig: 'فعال‌سازی پشتیبانی GPU',
gpuConfigHelper:
@@ -4259,10 +4276,15 @@ const message = {
familyIncomplete: 'زنجیره‌های {0} ناقص هستند',
resetWithBackup: 'پشتیبان‌گیری قوانین و بازنشانی',
resetOnly: 'فقط بازنشانی',
resetWithBackupHelper: 'قوانین ماژول فعلی را به‌صورت JSON ذخیره کنید تا هنگام مقداردهی اولیه بازیابی شوند.',
resetWithBackupHelper:
'قوانین ماژول فعلی را به‌صورت JSON ذخیره کنید تا هنگام مقداردهی اولیه یا راه‌اندازی بازیابی شوند.',
resetOnlyHelper: 'قوانین فعلی بدون ایجاد نسخه پشتیبان جدید پاک می‌شوند. نسخه‌های پشتیبان قبلی حفظ می‌شوند.',
backupDirectoryHelper:
'نسخه‌های پشتیبان معتبر قوانین در {0} نمایش داده می‌شوند. جدیدترین فایل به‌طور پیش‌فرض انتخاب می‌شود.',
startOnly: 'فقط راه\u200cاندازی',
startAndRestore: 'راه\u200cاندازی و بازیابی از نسخه پشتیبان',
startWithBackupHelper:
'فایروال را به‌طور معمول راه‌اندازی کنید یا یک نسخه پشتیبان JSON موجود را برای راه‌اندازی و بازیابی قوانین انتخاب کنید.',
restoreAllRules: 'مقداردهی اولیه و بازیابی قوانین',
importRuleBackup: 'ورود از نسخه پشتیبان',
initializeOnly: 'فقط راه‌اندازی',
+22 -1
View File
@@ -772,6 +772,15 @@ const message = {
},
aiTools: {
agents: {
deepSeekAccessHostHelper:
'ブラウザーで使用する IPv4 アドレスまたはホスト名を、スキーム・ポート・パスなしで入力してください。HTTPS はアプリのローカル証明書を使用します。',
deepSeekAuthHelper:
'ユーザー名には英数字、ドット、アンダースコア、ハイフンを使用できます。パスワードは12文字以上で、前後の空白や改行を含めないでください。',
octopAuthHelper:
'パスワードは英字と数字を含む8文字以上とし、よく使われるパスワードを避けてください。初期化時に拒否された場合は、アプリのログで設定手順を確認してください。',
initialCredentials: '初期ログイン情報です。アプリ内で変更した場合は現在のパスワードを使用してください。',
nativeAgentAccessUnavailable:
'既存のインストールに対応する HTTPS アクセス先またはポートがありません。アプリの設定を確認してください。',
agent: 'エージェント',
account: 'モデルアカウント',
batchOperation: '一括操作',
@@ -1505,6 +1514,14 @@ const message = {
extraHeaders: '追加ヘッダー',
},
vllmMonitor: {
concurrency: '現在の同時リクエスト',
sampleMoment: 'サンプリング時点',
sampleWindow: '集計区間',
requestUnit: '件',
processing: '処理中',
queued: '待機中',
requestTiming: 'リクエスト処理時間',
completedRequests: '区間内の完了リクエスト数',
tokensPerSecond: 'tokens/秒',
requestsPerSecond: 'リクエスト/秒',
finishReasonLabels: {
@@ -3670,6 +3687,7 @@ const message = {
link: 'リンク',
showCurrentArch: '現在のサーバーアーキテクチャのアプリケーション',
syncLocalApp: 'ローカルアプリの同期',
localAppSyncToNodes: 'ローカルアプリ同期時に子ノードも同期',
memoryRequiredHelper: '現在のアプリケーションは {0} メモリが必要です',
gpuConfig: 'GPUサポートを有効化',
gpuConfigHelper:
@@ -4282,9 +4300,12 @@ const message = {
familyIncomplete: '{0} のチェーンが不完全です',
resetWithBackup: 'ルールをバックアップしてリセット',
resetOnly: 'リセットのみ',
resetWithBackupHelper: '現在のモジュールのルールを JSON に保存し、初期化時に復元できます。',
resetWithBackupHelper: '現在のモジュールのルールを JSON に保存し、初期化時または起動時に復元できます。',
resetOnlyHelper: '新しいバックアップを作成せずに現在のルールを消去します。既存のバックアップは保持されます。',
backupDirectoryHelper: '{0} 内の有効なルールバックアップを表示します。既定では最新のファイルが選択されます。',
startOnly: '起動のみ',
startAndRestore: '起動してバックアップから復元',
startWithBackupHelper: '通常起動するか、既存の JSON バックアップを選んで起動とルールの復元を行います。',
restoreAllRules: '初期化してルールを復元',
importRuleBackup: 'バックアップからインポート',
initializeOnly: '初期化のみ',
+22 -1
View File
@@ -763,6 +763,15 @@ const message = {
},
aiTools: {
agents: {
deepSeekAccessHostHelper:
'브라우저에서 사용하는 IPv4 주소 또는 호스트 이름을 입력하세요. 프로토콜, 포트, 경로는 제외하세요. HTTPS는 앱의 로컬 인증서를 사용합니다.',
deepSeekAuthHelper:
'사용자 이름은 영문, 숫자, 점, 밑줄, 하이픈만 지원합니다. 비밀번호는 12자 이상이며 앞뒤 공백이나 줄바꿈을 포함할 수 없습니다.',
octopAuthHelper:
'비밀번호는 문자와 숫자를 포함한 8자 이상이어야 하며 흔한 비밀번호는 피하세요. 초기화 시 거부되면 앱 로그의 설정 안내를 확인하세요.',
initialCredentials: '초기 로그인 정보입니다. 앱에서 변경했다면 현재 비밀번호를 사용하세요.',
nativeAgentAccessUnavailable:
'기존 설치에 호환되는 HTTPS 접속 주소 또는 포트가 없습니다. 앱 설정을 확인하세요.',
agent: '에이전트',
account: '모델 계정',
batchOperation: '일괄 작업',
@@ -1490,6 +1499,14 @@ const message = {
extraHeaders: '추가 헤더',
},
vllmMonitor: {
concurrency: '현재 동시 요청',
sampleMoment: '샘플링 시점',
sampleWindow: '집계 구간',
requestUnit: '개 요청',
processing: '처리 중',
queued: '대기 중',
requestTiming: '요청 소요 시간',
completedRequests: '구간 내 완료 요청 수',
tokensPerSecond: 'tokens/초',
requestsPerSecond: '요청/초',
finishReasonLabels: {
@@ -3605,6 +3622,7 @@ const message = {
link: '링크',
showCurrentArch: '현재 서버 아키텍처 애플리케이션',
syncLocalApp: '로컬 애플리케이션 동기화',
localAppSyncToNodes: '로컬 앱 동기화 시 하위 노드도 동기화',
memoryRequiredHelper: '현재 애플리케이션은 {0} 메모리가 필요합니다',
gpuConfig: 'GPU 지원 활성화',
gpuConfigHelper:
@@ -4204,9 +4222,12 @@ const message = {
familyIncomplete: '{0} 체인이 불완전합니다',
resetWithBackup: '규칙 백업 후 초기화',
resetOnly: '초기화만 수행',
resetWithBackupHelper: '현재 모듈의 규칙을 JSON으로 저장하여 초기화 시 복원할 수 있습니다.',
resetWithBackupHelper: '현재 모듈의 규칙을 JSON으로 저장하여 초기화 또는 시작 시 복원할 수 있습니다.',
resetOnlyHelper: '새 백업 없이 현재 규칙을 삭제합니다. 기존 백업은 유지됩니다.',
backupDirectoryHelper: '{0}의 유효한 규칙 백업을 표시합니다. 기본적으로 최신 파일이 선택됩니다.',
startOnly: '시작만',
startAndRestore: '시작 및 백업에서 복원',
startWithBackupHelper: '방화벽을 일반적으로 시작하거나 기존 JSON 백업을 선택하여 시작하고 규칙을 복원합니다.',
restoreAllRules: '초기화 및 규칙 복원',
importRuleBackup: '백업에서 가져오기',
initializeOnly: '초기화만 수행',
+22 -1
View File
@@ -761,6 +761,15 @@ const message = {
},
aiTools: {
agents: {
deepSeekAccessHostHelper:
'ປ້ອນທີ່ຢູ່ IPv4 ຫຼືຊື່ໂຮສທີ່ໃຊ້ໃນບຣາວເຊີ ໂດຍບໍ່ມີໂປຣໂຕຄອນ, ພອດ ຫຼືເສັ້ນທາງ. HTTPS ໃຊ້ໃບຢັ້ງຢືນພາຍໃນຂອງແອັບ.',
deepSeekAuthHelper:
'ຊື່ຜູ້ໃຊ້: ຕົວອັກສອນອັງກິດ, ຕົວເລກ, ຈຸດ, ຂີດລຸ່ມ ຫຼືຂີດກາງ. ລະຫັດຜ່ານ: ຢ່າງໜ້ອຍ 12 ຕົວ ໂດຍບໍ່ມີຊ່ອງຫວ່າງຢູ່ຫົວ/ທ້າຍ ຫຼືຂຶ້ນແຖວໃໝ່.',
octopAuthHelper:
'ລະຫັດຜ່ານ: ຢ່າງໜ້ອຍ 8 ຕົວ ປະກອບດ້ວຍຕົວອັກສອນແລະຕົວເລກ; ຫຼີກລ້ຽງລະຫັດທີ່ໃຊ້ທົ່ວໄປ. ຖ້າຖືກປະຕິເສດ ໃຫ້ເບິ່ງຄຳແນະນຳໃນບັນທຶກຂອງແອັບ.',
initialCredentials: 'ຂໍ້ມູນເຂົ້າລະບົບເບື້ອງຕົ້ນ; ຖ້າປ່ຽນໃນແອັບແລ້ວ ໃຫ້ໃຊ້ລະຫັດປັດຈຸບັນ.',
nativeAgentAccessUnavailable:
'ການຕິດຕັ້ງທີ່ມີຢູ່ບໍ່ມີທີ່ຢູ່ ຫຼືພອດ HTTPS ທີ່ເຂົ້າກັນໄດ້. ກະລຸນາກວດສອບການຕັ້ງຄ່າແອັບ.',
agent: 'ຕົວແທນ (Agent)',
account: 'ບັນຊີໂມເດວ',
batchOperation: 'ດຳເນີນການແບບກຸ່ມ',
@@ -1484,6 +1493,14 @@ const message = {
extraHeaders: 'Headers ເພີ່ມເຕີມ',
},
vllmMonitor: {
concurrency: 'ຄຳຮ້ອງຂໍພ້ອມກັນໃນປັດຈຸບັນ',
sampleMoment: 'ໃນເວລາເກັບຕົວຢ່າງ',
sampleWindow: 'ຊ່ວງເວລາເກັບຕົວຢ່າງ',
requestUnit: 'ຄຳຮ້ອງຂໍ',
processing: 'ກຳລັງປະມວນຜົນ',
queued: 'ກຳລັງລໍຖ້າ',
requestTiming: 'ເວລາປະມວນຜົນຄຳຮ້ອງຂໍ',
completedRequests: 'ຈຳນວນຄຳຮ້ອງຂໍທີ່ສຳເລັດໃນຊ່ວງເວລາ',
tokensPerSecond: 'tokens/ວິນາທີ',
requestsPerSecond: 'ຄຳຮ້ອງຂໍ/ວິນາທີ',
finishReasonLabels: {
@@ -3582,6 +3599,7 @@ const message = {
link: 'ລິ້ງ',
showCurrentArch: 'ສະຖາປັດຕະຍະກຳ',
syncLocalApp: 'ຊິງຄ໌ແອັບໃນເຄື່ອງ',
localAppSyncToNodes: 'ຮວມໂນດລູກເມື່ອຊິງຄ໌ແອັບໃນເຄື່ອງ',
memoryRequiredHelper: 'ຄວາມຕ້ອງການໜ່ວຍຄວາມຈຳຂອງແອັບປະຈຸບັນ {0}',
gpuConfig: 'ເປີດໃຊ້ການຮອງຮັບ GPU',
gpuConfigHelper:
@@ -4173,9 +4191,12 @@ const message = {
familyIncomplete: 'ຊຸດກົດ {0} ບໍ່ຄົບຖ້ວນ',
resetWithBackup: 'ສຳຮອງກົດແລ້ວຣີເຊັດ',
resetOnly: 'ຣີເຊັດເທົ່ານັ້ນ',
resetWithBackupHelper: 'ບັນທຶກກົດຂອງໂມດູນປັດຈຸບັນເປັນ JSON ເພື່ອກູ້ຄືນເມື່ອເລີ່ມຕົ້ນ.',
resetWithBackupHelper: 'ບັນທຶກກົດຂອງໂມດູນປັດຈຸບັນເປັນ JSON ເພື່ອກູ້ຄືນເມື່ອເລີ່ມຕົ້ນ ຫຼື ເປີດໃຊ້ໄຟວໍ.',
resetOnlyHelper: 'ລຶບກົດປັດຈຸບັນໂດຍບໍ່ສ້າງໄຟລ໌ສຳຮອງໃໝ່. ໄຟລ໌ສຳຮອງເກົ່າຍັງຄົງຢູ່.',
backupDirectoryHelper: 'ສະແດງໄຟລ໌ສຳຮອງກົດທີ່ໃຊ້ໄດ້ໃນ {0}. ເລືອກໄຟລ໌ຫຼ້າສຸດເປັນຄ່າເລີ່ມຕົ້ນ.',
startOnly: 'ເລີ່ມຕົ້ນເທົ່ານັ້ນ',
startAndRestore: 'ເລີ່ມຕົ້ນ ແລະ ກູ້ຄືນຈາກຂໍ້ມູນສຳຮອງ',
startWithBackupHelper: 'ເປີດໄຟວໍຕາມປົກກະຕິ ຫຼື ເລືອກຂໍ້ມູນສຳຮອງ JSON ທີ່ມີຢູ່ເພື່ອເລີ່ມຕົ້ນ ແລະ ກູ້ຄືນກົດ.',
restoreAllRules: 'ເລີ່ມຕົ້ນ ແລະ ກູ້ຄືນກົດ',
importRuleBackup: 'ນຳເຂົ້າຈາກຂໍ້ມູນສຳຮອງ',
initializeOnly: 'ເລີ່ມຕົ້ນເທົ່ານັ້ນ',
+24 -1
View File
@@ -781,6 +781,15 @@ const message = {
},
aiTools: {
agents: {
deepSeekAccessHostHelper:
'Masukkan alamat IPv4 atau nama hos yang digunakan dalam pelayar, tanpa protokol, port atau laluan. HTTPS menggunakan sijil setempat aplikasi.',
deepSeekAuthHelper:
'Nama pengguna: huruf, nombor, titik, garis bawah atau sempang. Kata laluan: sekurang-kurangnya 12 aksara, tanpa ruang di awal/akhir atau baris baharu.',
octopAuthHelper:
'Kata laluan: sekurang-kurangnya 8 aksara dengan huruf dan nombor; elakkan kata laluan lazim. Jika ditolak, semak arahan persediaan dalam log aplikasi.',
initialCredentials: 'Maklumat log masuk awal; gunakan kata laluan semasa jika telah diubah dalam aplikasi.',
nativeAgentAccessUnavailable:
'Pemasangan sedia ada tiada alamat atau port HTTPS yang serasi. Semak tetapan aplikasi.',
agent: 'Agen',
account: 'Akaun model',
batchOperation: 'Operasi pukal',
@@ -1531,6 +1540,14 @@ const message = {
extraHeaders: 'Pengepala Tambahan',
},
vllmMonitor: {
concurrency: 'Permintaan serentak semasa',
sampleMoment: 'Pada masa pensampelan',
sampleWindow: 'Selang pensampelan',
requestUnit: 'permintaan',
processing: 'Sedang diproses',
queued: 'Dalam baris gilir',
requestTiming: 'Tempoh permintaan',
completedRequests: 'Permintaan selesai dalam selang masa',
tokensPerSecond: 'tokens/s',
requestsPerSecond: 'Permintaan/s',
finishReasonLabels: {
@@ -3742,6 +3759,7 @@ const message = {
link: 'Pautan',
showCurrentArch: 'Aplikasi seni bina pelayan semasa',
syncLocalApp: 'Segerakkan Aplikasi Tempatan',
localAppSyncToNodes: 'Sertakan nod anak semasa menyegerakkan aplikasi tempatan',
memoryRequiredHelper: 'Aplikasi semasa memerlukan {0} memori',
gpuConfig: 'Aktifkan Sokongan GPU',
gpuConfigHelper:
@@ -4362,10 +4380,15 @@ const message = {
familyIncomplete: 'Rantaian {0} tidak lengkap',
resetWithBackup: 'Sandarkan peraturan dan tetapkan semula',
resetOnly: 'Tetapkan semula sahaja',
resetWithBackupHelper: 'Simpan peraturan modul semasa sebagai JSON untuk dipulihkan semasa pemulaan.',
resetWithBackupHelper:
'Simpan peraturan modul semasa sebagai JSON untuk dipulihkan semasa pemulaan atau semasa tembok api dimulakan.',
resetOnlyHelper: 'Padam peraturan semasa tanpa sandaran baharu. Sandaran sedia ada dikekalkan.',
backupDirectoryHelper:
'Sandaran peraturan yang sah dalam {0} disenaraikan di bawah. Fail terkini dipilih secara lalai.',
startOnly: 'Mula sahaja',
startAndRestore: 'Mula dan pulihkan daripada sandaran',
startWithBackupHelper:
'Mulakan tembok api seperti biasa atau pilih sandaran JSON sedia ada untuk memulakan dan memulihkan peraturan.',
restoreAllRules: 'Mulakan dan pulihkan peraturan',
importRuleBackup: 'Import daripada sandaran',
initializeOnly: 'Mulakan sahaja',
+24 -1
View File
@@ -774,6 +774,15 @@ const message = {
},
aiTools: {
agents: {
deepSeekAccessHostHelper:
'Informe o IPv4 ou nome do host usado no navegador, sem protocolo, porta ou caminho. O HTTPS usa o certificado local do aplicativo.',
deepSeekAuthHelper:
'Usuário: letras, números, ponto, sublinhado ou hífen. Senha: no mínimo 12 caracteres, sem espaços nas extremidades ou quebras de linha.',
octopAuthHelper:
'Senha: no mínimo 8 caracteres com letras e números; evite senhas comuns. Se rejeitada na inicialização, consulte as instruções nos logs do aplicativo.',
initialCredentials: 'Credenciais iniciais; se alteradas no aplicativo, use a senha atual.',
nativeAgentAccessUnavailable:
'A instalação existente não tem endereço ou porta HTTPS compatível. Verifique as configurações do aplicativo.',
agent: 'Agente',
account: 'Conta de modelo',
batchOperation: 'Operação em lote',
@@ -1527,6 +1536,14 @@ const message = {
extraHeaders: 'Cabeçalhos extras',
},
vllmMonitor: {
concurrency: 'Concorrência atual',
sampleMoment: 'No momento da coleta',
sampleWindow: 'Intervalo de coleta',
requestUnit: 'solicitações',
processing: 'Em processamento',
queued: 'Na fila',
requestTiming: 'Tempo das solicitações',
completedRequests: 'Solicitações concluídas no intervalo',
tokensPerSecond: 'tokens/s',
requestsPerSecond: 'Solicitações/s',
finishReasonLabels: {
@@ -3758,6 +3775,7 @@ const message = {
link: 'Link',
showCurrentArch: 'Aplicações da arquitetura atual do servidor',
syncLocalApp: 'Sincronizar Aplicativo Local',
localAppSyncToNodes: 'Incluir nós filhos ao sincronizar apps locais',
memoryRequiredHelper: 'O aplicativo atual requer {0} de memória',
gpuConfig: 'Ativar Suporte a GPU',
gpuConfigHelper:
@@ -4384,10 +4402,15 @@ const message = {
familyIncomplete: 'As cadeias de {0} estão incompletas',
resetWithBackup: 'Salvar regras e redefinir',
resetOnly: 'Apenas redefinir',
resetWithBackupHelper: 'Salve as regras do módulo atual em JSON para restaurá-las durante a inicialização.',
resetWithBackupHelper:
'Salve as regras do módulo atual em JSON para restaurá-las ao inicializar ou iniciar o firewall.',
resetOnlyHelper: 'Limpar as regras atuais sem criar um novo backup. Os backups existentes são mantidos.',
backupDirectoryHelper:
'Os backups válidos de regras em {0} são listados abaixo. O mais recente é selecionado por padrão.',
startOnly: 'Apenas iniciar',
startAndRestore: 'Iniciar e restaurar do backup',
startWithBackupHelper:
'Inicie o firewall normalmente ou escolha um backup JSON existente para iniciar e restaurar regras.',
restoreAllRules: 'Inicializar e restaurar regras',
importRuleBackup: 'Importar do backup',
initializeOnly: 'Apenas inicializar',
+24 -1
View File
@@ -772,6 +772,15 @@ const message = {
},
aiTools: {
agents: {
deepSeekAccessHostHelper:
'Введите IPv4-адрес или имя хоста, используемое в браузере, без протокола, порта и пути. HTTPS использует локальный сертификат приложения.',
deepSeekAuthHelper:
'Имя пользователя: латинские буквы, цифры, точка, подчёркивание или дефис. Пароль: минимум 12 символов, без пробелов по краям и переводов строк.',
octopAuthHelper:
'Пароль: минимум 8 символов с буквами и цифрами; избегайте распространённых паролей. Если пароль отклонён, инструкции доступны в журнале приложения.',
initialCredentials: 'Начальные учётные данные; если пароль изменён в приложении, используйте текущий.',
nativeAgentAccessUnavailable:
'У существующей установки нет совместимого HTTPS-адреса или порта. Проверьте настройки приложения.',
agent: 'Агент',
account: 'Аккаунт модели',
batchOperation: 'Пакетная операция',
@@ -1520,6 +1529,14 @@ const message = {
extraHeaders: 'Дополнительные заголовки',
},
vllmMonitor: {
concurrency: 'Текущие параллельные запросы',
sampleMoment: 'На момент замера',
sampleWindow: 'Интервал замера',
requestUnit: 'запросов',
processing: 'В обработке',
queued: 'В очереди',
requestTiming: 'Время обработки запросов',
completedRequests: 'Завершённые запросы за интервал',
tokensPerSecond: 'tokens/с',
requestsPerSecond: 'Запросов/с',
finishReasonLabels: {
@@ -3727,6 +3744,7 @@ const message = {
link: 'Ссылка',
showCurrentArch: 'Приложения для текущей архитектуры сервера',
syncLocalApp: 'Синхронизировать локальное приложение',
localAppSyncToNodes: 'Включать дочерние узлы при синхронизации локальных приложений',
memoryRequiredHelper: 'Текущее приложение требует {0} памяти',
gpuConfig: 'Включить поддержку GPU',
gpuConfigHelper:
@@ -4350,10 +4368,15 @@ const message = {
familyIncomplete: 'Цепочки {0} неполны',
resetWithBackup: 'Сохранить правила и сбросить',
resetOnly: 'Только сбросить',
resetWithBackupHelper: 'Сохраните правила текущего модуля в JSON для восстановления при инициализации.',
resetWithBackupHelper:
'Сохраните правила текущего модуля в JSON для восстановления при инициализации или запуске.',
resetOnlyHelper: 'Удалить текущие правила без создания новой копии. Существующие копии сохраняются.',
backupDirectoryHelper:
'Ниже перечислены действительные резервные копии правил из {0}. По умолчанию выбрана самая новая.',
startOnly: 'Только запуск',
startAndRestore: 'Запустить и восстановить из резервной копии',
startWithBackupHelper:
'Запустите брандмауэр как обычно или выберите существующую копию JSON для запуска и восстановления правил.',
restoreAllRules: 'Инициализировать и восстановить правила',
importRuleBackup: 'Импортировать из резервной копии',
initializeOnly: 'Только инициализировать',
+24 -1
View File
@@ -777,6 +777,15 @@ const message = {
},
aiTools: {
agents: {
deepSeekAccessHostHelper:
'Tarayıcıda kullanılan IPv4 adresini veya ana bilgisayar adını protokol, bağlantı noktası veya yol olmadan girin. HTTPS, uygulamanın yerel sertifikasını kullanır.',
deepSeekAuthHelper:
'Kullanıcı adı: harf, rakam, nokta, alt çizgi veya tire. Parola: en az 12 karakter; başta/sonda boşluk veya satır sonu içeremez.',
octopAuthHelper:
'Parola: harf ve rakam içeren en az 8 karakter; yaygın parolalardan kaçının. Reddedilirse kurulum yönergeleri için uygulama günlüklerini kontrol edin.',
initialCredentials: 'İlk giriş bilgileri; uygulamada değiştirildiyse güncel parolayı kullanın.',
nativeAgentAccessUnavailable:
'Mevcut kurulumda uyumlu HTTPS erişim adresi veya bağlantı noktası yok. Uygulama ayarlarını kontrol edin.',
agent: 'Ajan',
account: 'Model hesabı',
batchOperation: 'Toplu işlem',
@@ -1527,6 +1536,14 @@ const message = {
extraHeaders: 'Ek Başlıklar',
},
vllmMonitor: {
concurrency: 'Mevcut eşzamanlı istekler',
sampleMoment: 'Örnekleme anında',
sampleWindow: 'Örnekleme aralığı',
requestUnit: 'istek',
processing: 'İşleniyor',
queued: 'Kuyrukta',
requestTiming: 'İstek süreleri',
completedRequests: 'Aralıkta tamamlanan istek sayısı',
tokensPerSecond: 'tokens/sn',
requestsPerSecond: 'İstek/sn',
finishReasonLabels: {
@@ -3743,6 +3760,7 @@ const message = {
link: 'Bağlantı',
showCurrentArch: 'Mimari',
syncLocalApp: 'Yerel Uygulamayı Senkronize Et',
localAppSyncToNodes: 'Yerel uygulama eşitlemesine alt düğümleri dahil et',
memoryRequiredHelper: 'Geçerli uygulama bellek gereksinimi {0}',
gpuConfig: 'GPU Desteğini Etkinleştir',
gpuConfigHelper:
@@ -4368,10 +4386,15 @@ const message = {
familyIncomplete: '{0} zincirleri eksik',
resetWithBackup: 'Kuralları yedekle ve sıfırla',
resetOnly: 'Yalnızca sıfırla',
resetWithBackupHelper: 'Geçerli modül kurallarını başlatma sırasında geri yüklemek için JSON olarak kaydedin.',
resetWithBackupHelper:
'Geçerli modül kurallarını ilk kurulumda veya güvenlik duvarı başlatılırken geri yüklemek için JSON olarak kaydedin.',
resetOnlyHelper: 'Yeni yedek oluşturmadan mevcut kuralları temizleyin. Önceki yedekler korunur.',
backupDirectoryHelper:
'{0} içindeki geçerli kural yedekleri listelenir. Varsayılan olarak en yeni dosya seçilir.',
startOnly: 'Yalnızca başlat',
startAndRestore: 'Başlat ve yedekten geri yükle',
startWithBackupHelper:
'Güvenlik duvarını normal başlatın veya başlatıp kuralları geri yüklemek için mevcut bir JSON yedeği seçin.',
restoreAllRules: 'Başlat ve kuralları geri yükle',
importRuleBackup: 'Yedekten içe aktar',
initializeOnly: 'Yalnızca başlat',
+20 -1
View File
@@ -739,6 +739,13 @@ const message = {
},
aiTools: {
agents: {
deepSeekAccessHostHelper:
'填寫瀏覽器實際存取的 IPv4 位址或主機名稱,不含協定、連接埠或路徑。HTTPS 使用應用程式的本機憑證。',
deepSeekAuthHelper: '使用者名稱僅支援字母、數字、點、底線及連字號;密碼至少 12 位,不含前後空格或換行。',
octopAuthHelper:
'密碼至少 8 位且包含字母及數字,避免常見弱密碼。若初始化拒絕該密碼,請查看應用程式日誌中的設定說明。',
initialCredentials: '初始登入資訊;若已在應用程式內修改,以目前密碼為準。',
nativeAgentAccessUnavailable: '現有安裝缺少相容的 HTTPS 存取位址或連接埠,請檢查應用程式設定。',
agent: 'Agent',
account: '模型帳號',
batchOperation: '批次操作',
@@ -1437,6 +1444,14 @@ const message = {
extraHeaders: '額外請求標頭',
},
vllmMonitor: {
concurrency: '目前並行請求',
sampleMoment: '取樣時刻',
sampleWindow: '統計區間',
requestUnit: '個請求',
processing: '處理中',
queued: '排隊中',
requestTiming: '請求耗時',
completedRequests: '區間完成請求數',
tokensPerSecond: 'tokens/秒',
requestsPerSecond: '請求/秒',
finishReasonLabels: {
@@ -3443,6 +3458,7 @@ const message = {
link: '連結',
showCurrentArch: '目前伺服器架構應用',
syncLocalApp: '同步本機應用',
localAppSyncToNodes: '同步本機應用時聯動子節點',
memoryRequiredHelper: '目前應用記憶體需求 {0}',
gpuConfig: '開啟 GPU 支援',
gpuConfigHelper: '請確保機器有 NVIDIA GPU 並且安裝 NVIDIA 驅動 和 NVIDIA docker Container Toolkit',
@@ -4010,9 +4026,12 @@ const message = {
familyIncomplete: '{0} 鏈不完整',
resetWithBackup: '備份規則後重置',
resetOnly: '僅重置',
resetWithBackupHelper: '將目前模組的規則儲存為 JSON,之後可在初始化時選擇備份還原。',
resetWithBackupHelper: '將目前模組的規則儲存為 JSON,之後可在初始化或啟動時選擇備份還原。',
resetOnlyHelper: '不建立新備份,直接清理目前規則;保留既有歷史備份。',
backupDirectoryHelper: '自動讀取 {0} 下的有效規則備份,預設選擇最新檔案。',
startOnly: '僅啟動',
startAndRestore: '啟動並從備份還原',
startWithBackupHelper: '可正常啟動防火牆,或選擇現有的 JSON 備份啟動並還原規則。',
restoreAllRules: '初始化並還原規則',
importRuleBackup: '從備份匯入',
initializeOnly: '僅初始化',
+20 -1
View File
@@ -756,6 +756,13 @@ const message = {
},
aiTools: {
agents: {
deepSeekAccessHostHelper:
'填写浏览器实际访问的 IPv4 地址或主机名,不含协议、端口或路径。HTTPS 使用应用的本地证书。',
deepSeekAuthHelper: '用户名仅支持字母、数字、点、下划线和连字符;密码至少 12 位,不含首尾空格或换行。',
octopAuthHelper:
'密码至少 8 位且包含字母和数字,避免常见弱密码。若初始化拒绝该密码,请查看应用日志中的设置说明。',
initialCredentials: '初始登录信息;若已在应用内修改,以当前密码为准。',
nativeAgentAccessUnavailable: '现有安装缺少兼容的 HTTPS 访问地址或端口,请检查应用设置。',
agent: '智能体',
account: '模型账号',
batchOperation: '批量操作',
@@ -1450,6 +1457,14 @@ const message = {
extraHeaders: '额外请求头',
},
vllmMonitor: {
concurrency: '当前并发',
sampleMoment: '采样时刻',
sampleWindow: '统计区间',
requestUnit: '个请求',
processing: '处理中',
queued: '排队中',
requestTiming: '请求耗时',
completedRequests: '区间完成请求数',
tokensPerSecond: 'tokens/秒',
requestsPerSecond: '请求/秒',
finishReasonLabels: {
@@ -3484,6 +3499,7 @@ const message = {
link: '链接',
showCurrentArch: '本服务器架构应用',
syncLocalApp: '同步本地应用',
localAppSyncToNodes: '同步本地应用时联动子节点',
memoryRequiredHelper: '当前应用内存需求 {0}',
gpuConfig: '开启 GPU 支持',
gpuConfigHelper: '请确保机器有 NVIDIA GPU 并且安装 NVIDIA 驱动 和 NVIDIA docker Container Toolkit',
@@ -4055,9 +4071,12 @@ const message = {
familyIncomplete: '{0} 链不完整',
resetWithBackup: '备份规则后重置',
resetOnly: '仅重置',
resetWithBackupHelper: '将当前模块的规则保存为 JSON,后续可在初始化时选择备份恢复。',
resetWithBackupHelper: '将当前模块的规则保存为 JSON,后续可在初始化或启动时选择备份恢复。',
resetOnlyHelper: '不生成新备份,直接清理当前规则;已有历史备份保留。',
backupDirectoryHelper: '自动读取 {0} 下的有效规则备份,默认选择最新文件。',
startOnly: '仅启动',
startAndRestore: '启动并从备份恢复',
startWithBackupHelper: '可正常启动防火墙,或选择已有的 JSON 备份启动并恢复规则。',
restoreAllRules: '初始化并恢复规则',
importRuleBackup: '从备份导入',
initializeOnly: '仅初始化',
@@ -163,9 +163,6 @@ const displayNodeName = (item: NodeItem) => {
};
const displayGroupName = (item: NodeItem) => {
if (item.groupBelong === 'Default') {
return i18n.global.t('commons.table.default');
}
return item.group || item.groupBelong || i18n.global.t('commons.table.default');
};
+43 -1
View File
@@ -1,10 +1,52 @@
import i18n from '@/lang';
import { compareVersion } from '@/utils/version';
import type { AI } from '@/api/interface/ai';
const openclawHTTPSVersion = '2026.3.13';
const openclawHTTPVersion = '2026.3.23';
const openclawDefaultAccessHost = '127.0.0.1';
export const isAgentType = (value: unknown): value is AI.AgentType =>
typeof value === 'string' && ['openclaw', 'copaw', 'hermes-agent', 'deepseek-harness', 'octop'].includes(value);
export const isDeepSeekAccessHost = (value: string): boolean => {
if (/^[0-9.]+$/.test(value)) {
return (
value.split('.').length === 4 &&
value.split('.').every((part) => /^(0|[1-9]\d{0,2})$/.test(part) && +part <= 255)
);
}
return (
value.length > 0 &&
value.length <= 253 &&
value === value.trim() &&
value.split('.').every((part) => /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/.test(part))
);
};
export const isOctopPassword = (value: string): boolean =>
[...value].length >= 8 &&
value === value.trim() &&
!/[\r\n\0]/.test(value) &&
/\p{L}/u.test(value) &&
/\p{Nd}/u.test(value) &&
![
'password',
'password1',
'password12',
'password123',
'12345678',
'123456789',
'qwerty123',
'admin123',
'welcome1',
'letmein1',
'changeme1',
'octop123',
'abc12345',
'iloveyou1',
].includes(value.toLowerCase());
export const getAgentProviderDisplayName = (provider: string, displayName?: string): string => {
if (provider === 'custom' || displayName === 'Custom') {
return i18n.global.t('container.custom');
@@ -27,7 +69,7 @@ export const supportsAgentToken = (agentType: string): boolean => {
};
export const supportsAgentDashboardAuth = (agentType: string): boolean => {
return agentType === 'hermes-agent' || agentType === 'copaw';
return ['hermes-agent', 'copaw', 'deepseek-harness', 'octop'].includes(agentType);
};
export const isOpenclawHTTPSWindowVersion = (version: string): boolean => {
@@ -7,6 +7,8 @@
<el-option label="OpenClaw" value="openclaw" />
<el-option label="Hermes Agent" value="hermes-agent" />
<el-option label="QwenPaw" value="copaw" />
<el-option label="DeepSeek Harness" value="deepseek-harness" />
<el-option label="Octop" value="octop" />
</el-select>
</el-form-item>
<el-form-item :label="$t('commons.table.name')" prop="name">
@@ -19,10 +21,19 @@
<el-select v-model="form.appVersion" filterable>
<el-option v-for="item in versions" :key="item" :label="item" :value="item" />
</el-select>
<span v-if="versions.length === 0" class="input-help">{{ $t('commons.msg.noneData') }}</span>
</el-form-item>
<el-form-item :label="$t('aiTools.agents.webuiPort')" prop="webUIPort">
<el-input-number v-model="form.webUIPort" :min="1" :max="65535" />
</el-form-item>
<el-form-item
v-if="form.agentType === 'deepseek-harness'"
:label="$t('setting.systemIP')"
prop="accessHost"
>
<el-input v-model="form.accessHost" />
<span class="input-help">{{ $t('aiTools.agents.deepSeekAccessHostHelper') }}</span>
</el-form-item>
<template v-if="supportsAgentDashboardAuth(form.agentType)">
<el-form-item :label="$t('commons.login.username')" prop="dashboardUsername">
<el-input v-model="form.dashboardUsername" />
@@ -35,6 +46,12 @@
{{ $t('commons.button.random') }}
</el-button>
</div>
<span class="input-help" v-if="form.agentType === 'deepseek-harness'">
{{ $t('aiTools.agents.deepSeekAuthHelper') }}
</span>
<span class="input-help" v-if="form.agentType === 'octop'">
{{ $t('aiTools.agents.octopAuthHelper') }}
</span>
</el-form-item>
</template>
<el-form-item
@@ -130,6 +147,9 @@ import { getRandomStr, newUUID } from '@/utils/id';
import {
buildDefaultAllowedOrigin,
getAgentProviderDisplayName,
isAgentType,
isDeepSeekAccessHost,
isOctopPassword,
parseAllowedOriginsInput,
supportsAgentDashboardAuth,
validateAllowedOriginsInput,
@@ -138,6 +158,7 @@ import { App } from '@/api/interface/app';
import AdvancedSetting from '@/components/advanced-setting/index.vue';
import AccountAddDialog from '@/views/ai/agents/model/add/index.vue';
import { useGlobalStore } from '@/composables/useGlobalStore';
import i18n from '@/lang';
const emit = defineEmits(['search', 'task']);
@@ -161,6 +182,7 @@ const form = reactive({
agentType: 'openclaw' as AI.AgentType,
appVersion: '',
webUIPort: 18789,
accessHost: '',
allowedOrigins: '',
provider: '',
accountId: undefined as unknown as number,
@@ -185,7 +207,10 @@ const form = reactive({
const showModelConfig = computed(() => form.agentType === 'openclaw' || form.agentType === 'hermes-agent');
const generateDashboardPassword = () => {
form.dashboardPassword = getRandomStr(8);
form.dashboardPassword =
form.agentType === 'deepseek-harness' || form.agentType === 'octop'
? `Aa2${Array.from(crypto.getRandomValues(new Uint8Array(16)), (byte) => byte.toString(16).padStart(2, '0')).join('')}`
: getRandomStr(8);
};
const ensureDashboardAuth = () => {
@@ -201,7 +226,7 @@ const ensureDashboardAuth = () => {
};
const setDefaultWebUIPort = () => {
if (form.agentType === 'copaw') {
if (form.agentType === 'copaw' || form.agentType === 'octop') {
form.webUIPort = 8088;
return;
}
@@ -209,6 +234,10 @@ const setDefaultWebUIPort = () => {
form.webUIPort = 9119;
return;
}
if (form.agentType === 'deepseek-harness') {
form.webUIPort = 10443;
return;
}
form.webUIPort = 18789;
};
@@ -217,6 +246,19 @@ const rules = reactive({
agentType: [Rules.requiredSelect],
appVersion: [Rules.requiredSelect],
webUIPort: [Rules.requiredInput],
accessHost: [
Rules.requiredInput,
{
validator: (_rule: any, value: string, callback: (error?: Error) => void) => {
callback(
isDeepSeekAccessHost(value)
? undefined
: new Error(i18n.global.t('aiTools.agents.deepSeekAccessHostHelper')),
);
},
trigger: 'blur',
},
],
allowedOrigins: [
Rules.requiredInput,
{
@@ -234,8 +276,35 @@ const rules = reactive({
provider: [Rules.requiredSelect],
accountId: [Rules.requiredSelect],
model: [Rules.requiredInput],
dashboardUsername: [Rules.requiredInput],
dashboardPassword: [Rules.requiredInput],
dashboardUsername: [
Rules.requiredInput,
{
validator: (_rule: any, value: string, callback: (error?: Error) => void) => {
callback(
form.agentType === 'deepseek-harness' && !/^[A-Za-z0-9._-]+$/.test(value)
? new Error(i18n.global.t('aiTools.agents.deepSeekAuthHelper'))
: undefined,
);
},
trigger: 'blur',
},
],
dashboardPassword: [
Rules.requiredInput,
{
validator: (_rule: any, value: string, callback: (error?: Error) => void) => {
const invalidCharacters = value !== value.trim() || /[\r\n\0]/.test(value);
if (form.agentType === 'deepseek-harness' && (invalidCharacters || [...value].length < 12)) {
callback(new Error(i18n.global.t('aiTools.agents.deepSeekAuthHelper')));
} else if (form.agentType === 'octop' && !isOctopPassword(value)) {
callback(new Error(i18n.global.t('aiTools.agents.octopAuthHelper')));
} else {
callback();
}
},
trigger: 'blur',
},
],
containerName: [Rules.containerName],
restartPolicy: [Rules.requiredSelect],
cpuQuota: [checkNumberRange(0, 99999)],
@@ -273,7 +342,13 @@ const loadSystemIP = async () => {
};
const loadVersions = async (appKey: AI.AgentType) => {
versions.value = [];
appInfo.value = undefined;
form.dockerCompose = '';
const res = await getAppByKey(appKey);
if (form.agentType !== appKey) {
return;
}
appInfo.value = res.data;
versions.value = res.data.versions || [];
if (!form.appVersion && versions.value.length > 0) {
@@ -287,6 +362,10 @@ const getDefaultAgentName = (agentType: AI.AgentType) => {
return 'QwenPaw';
case 'hermes-agent':
return 'Hermes-Agent';
case 'deepseek-harness':
return 'DeepSeek-Harness';
case 'octop':
return 'Octop';
default:
return 'OpenClaw';
}
@@ -402,7 +481,9 @@ const handleAgentTypeChange = async () => {
form.name === 'OpenClaw' ||
form.name === 'CoPaw' ||
form.name === 'QwenPaw' ||
form.name === 'Hermes-Agent'
form.name === 'Hermes-Agent' ||
form.name === 'DeepSeek-Harness' ||
form.name === 'Octop'
) {
form.name = getDefaultAgentName(form.agentType);
}
@@ -413,6 +494,10 @@ const handleAgentTypeChange = async () => {
form.accountId = undefined as unknown as number;
form.baseURL = '';
ensureDashboardAuth();
if (form.agentType === 'deepseek-harness' && !form.accessHost) {
await loadSystemIP();
form.accessHost = isDeepSeekAccessHost(systemIP.value) ? systemIP.value : '';
}
if (form.agentType === 'openclaw') {
await loadSystemIP();
allowedOriginsAutoFilled.value = true;
@@ -485,6 +570,7 @@ const submit = async () => {
webUIPort: form.webUIPort,
allowedOrigins: form.agentType === 'openclaw' ? parseAllowedOriginsInput(form.allowedOrigins) : undefined,
agentType: form.agentType,
accessHost: form.agentType === 'deepseek-harness' ? form.accessHost : undefined,
model: showModelConfig.value ? form.model : undefined,
accountId: showModelConfig.value ? form.accountId : undefined,
token: form.agentType === 'openclaw' ? form.token : undefined,
@@ -524,45 +610,19 @@ const handleClose = () => {
form.dashboardPassword = '';
form.remark = '';
form.allowedOrigins = '';
form.accessHost = '';
form.dockerCompose = '';
lastAutoAllowedOrigins.value = '';
allowedOriginsAutoFilled.value = true;
};
const openDrawer = async (agentType?: AI.AgentType) => {
const targetType =
agentType === 'copaw' || agentType === 'hermes-agent' || agentType === 'openclaw' ? agentType : 'openclaw';
const targetType = isAgentType(agentType) ? agentType : 'openclaw';
form.name = getDefaultAgentName(targetType);
open.value = true;
form.agentType = targetType;
setDefaultWebUIPort();
form.token = getRandomStr(32).toLowerCase();
ensureDashboardAuth();
if (form.agentType === 'copaw') {
form.allowedOrigins = '';
lastAutoAllowedOrigins.value = '';
allowedOriginsAutoFilled.value = true;
await loadVersions('copaw');
providerOptions.value = [];
providerModels.value = {};
accountOptions.value = [];
return;
}
if (form.agentType === 'hermes-agent') {
form.allowedOrigins = '';
lastAutoAllowedOrigins.value = '';
allowedOriginsAutoFilled.value = true;
await loadVersions('hermes-agent');
await loadProviders();
await loadAccounts();
return;
}
await loadSystemIP();
allowedOriginsAutoFilled.value = true;
syncAllowedOriginsWithDefault(true);
await loadVersions('openclaw');
await loadProviders();
await loadAccounts();
await handleAgentTypeChange();
};
const openAccountCreate = () => {
+33 -12
View File
@@ -178,6 +178,9 @@
alignment="start"
:size="2"
>
<el-text v-if="row.agentType === 'octop'" type="info" size="small">
{{ $t('aiTools.agents.initialCredentials') }}
</el-text>
<span>{{ row.dashboardUsername || '-' }}</span>
<el-space alignment="center">
<span>{{ row.dashboardPassword ? '********' : '-' }}</span>
@@ -283,7 +286,7 @@ import { Website } from '@/api/interface/website';
import { SearchWithPage } from '@/api/interface';
import { dateFormat } from '@/utils/date';
import { newUUID } from '@/utils/id';
import { MsgSuccess } from '@/utils/message';
import { MsgError, MsgSuccess } from '@/utils/message';
import AddDialog from '@/views/ai/agents/agent/add/index.vue';
import DeleteDialog from '@/views/ai/agents/agent/delete/index.vue';
@@ -302,6 +305,8 @@ import DockerStatus from '@/views/container/docker-status/index.vue';
import {
getAgentProviderDisplayName,
getOpenclawAccessScheme,
isAgentType,
isDeepSeekAccessHost,
supportsAgentModelConfig,
supportsAgentToken,
supportsAgentDashboardAuth,
@@ -312,6 +317,8 @@ import NoApp from '@/views/app-store/apps/no-app/index.vue';
import openclawIcon from '@/assets/images/ai-agent-openclaw.svg';
import copawIcon from '@/assets/images/ai-agent-copaw.svg';
import hermesIcon from '@/assets/images/ai-agent-hermes-agent.svg';
import deepSeekIcon from '@/assets/images/ai-agent-deepseek-harness.png';
import octopIcon from '@/assets/images/ai-agent-octop.png';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { loadOptionalComponent } from '@/extensions/optional';
import { useOperateNodeContext } from '@/composables/useOperateNodeContext';
@@ -358,8 +365,7 @@ const buttons = [
{
label: i18n.global.t('menu.config'),
click: (row: AI.AgentItem) => openConfig(row),
show: (row: AI.AgentItem) =>
supportsAgentModelConfig(row.agentType) || supportsAgentDashboardAuth(row.agentType),
show: (row: AI.AgentItem) => supportsAgentModelConfig(row.agentType) || row.agentType === 'copaw',
disabled: (row: AI.AgentItem) => row.status !== 'Running',
},
{
@@ -441,6 +447,10 @@ const getAgentTypeLabel = (agentType: AI.AgentType) => {
return 'QwenPaw';
case 'hermes-agent':
return 'Hermes Agent';
case 'deepseek-harness':
return 'DeepSeek Harness';
case 'octop':
return 'Octop';
default:
return 'OpenClaw';
}
@@ -452,6 +462,10 @@ const getAgentTypeIcon = (agentType: AI.AgentType) => {
return copawIcon;
case 'hermes-agent':
return hermesIcon;
case 'deepseek-harness':
return deepSeekIcon;
case 'octop':
return octopIcon;
default:
return openclawIcon;
}
@@ -461,8 +475,7 @@ const openCreate = (agentType?: AI.AgentType) => {
if (noApp.value) {
return;
}
const targetType =
agentType === 'copaw' || agentType === 'hermes-agent' || agentType === 'openclaw' ? agentType : 'openclaw';
const targetType = isAgentType(agentType) ? agentType : 'openclaw';
if (addRef.value?.open) {
addRef.value.open(targetType);
}
@@ -473,12 +486,7 @@ const openCreateFromQuery = async () => {
if (!shouldOpen) {
return;
}
const agentType =
route.query.agentType === 'copaw'
? 'copaw'
: route.query.agentType === 'hermes-agent'
? 'hermes-agent'
: 'openclaw';
const agentType = isAgentType(route.query.agentType) ? route.query.agentType : 'openclaw';
openCreate(agentType);
const nextQuery = { ...route.query };
delete nextQuery.open;
@@ -547,7 +555,12 @@ const openTerminal = (row: AI.AgentItem) => {
openTerminalDialog({
containerID: row.containerName,
title,
users: row.agentType === 'hermes-agent' ? ['hermes', 'root'] : ['node', 'root'],
users:
row.agentType === 'hermes-agent'
? ['hermes', 'root']
: row.agentType === 'octop'
? ['root']
: ['node', 'root'],
shell: '/bin/bash',
initCmd: '',
});
@@ -579,6 +592,14 @@ const openWorkDir = (row: AI.AgentItem) => {
};
const jumpWebUI = (row: AI.AgentItem) => {
if (row.agentType === 'deepseek-harness') {
if (!isDeepSeekAccessHost(row.accessHost || '') || !row.webUIPort) {
MsgError(i18n.global.t('aiTools.agents.nativeAgentAccessUnavailable'));
return;
}
window.open(`https://${row.accessHost}:${row.webUIPort}`, '_blank', 'noopener,noreferrer');
return;
}
if (dialogPortJumpRef.value?.acceptParams) {
dialogPortJumpRef.value.acceptParams({
port: row.webUIPort,
+25 -11
View File
@@ -73,7 +73,15 @@
import { useSearchReset } from '@/composables/useSearchReset';
import { App } from '@/api/interface/app';
import { toRef, onMounted, reactive, ref } from 'vue';
import { searchApp, syncApp, syncCutomAppStore, syncLocalApp, getCurrentNodeCustomAppConfig } from '@/api/modules/app';
import {
searchApp,
syncApp,
syncCutomAppStore,
syncLocalApp,
getCurrentNodeCustomAppConfig,
getLocalAppSyncConfig,
dispatchLocalAppSync,
} from '@/api/modules/app';
import Install from '../detail/install/index.vue';
import router from '@/routers';
import { MsgSuccess } from '@/utils/message';
@@ -88,7 +96,7 @@ import AppCard from '@/views/app-store/apps/app/index.vue';
import MainDiv from '@/components/main-div/index.vue';
import { jumpToInstall } from '@/utils/app';
import { useGlobalStore } from '@/composables/useGlobalStore';
const { isProductPro, isOffline, isMobile, isEnterprise } = useGlobalStore();
const { isProductPro, isOffline, isMobile, isEnterprise, isMaster, isXpackOrEE, isAdmin } = useGlobalStore();
const paginationConfig = reactive({
cacheSizeKey: 'app-page-size',
@@ -201,21 +209,27 @@ const sync = async () => {
}
};
const syncLocal = () => {
const syncLocal = async () => {
const taskID = newUUID();
const syncReq = {
taskID: taskID,
};
syncing.value = true;
syncLocalApp(syncReq)
.then(() => {
try {
const dispatch =
isMaster.value && isXpackOrEE.value && isAdmin.value && (await getLocalAppSyncConfig()).data.enabled;
if (dispatch) {
await dispatchLocalAppSync(syncReq);
taskLogRef.value.openWithTaskID(taskID, true, 'local');
} else {
await syncLocalApp(syncReq);
openTaskLog(taskID);
canUpdate.value = false;
search(req);
})
.finally(() => {
syncing.value = false;
});
}
canUpdate.value = false;
search(req);
} finally {
syncing.value = false;
}
};
const changeTag = (key: string) => {
+9 -25
View File
@@ -45,14 +45,7 @@
<TableViewSwitch v-model="viewMode" storage-key="cronjob" />
<el-select v-model="searchGroupID" @change="search()" clearable class="p-w-200">
<template #prefix>{{ $t('commons.table.group') }}</template>
<div v-for="item in groupOptions" :key="item.id">
<el-option
v-if="item.name === 'Default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
<el-option v-for="item in groupOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<TableSearch @search="search()" v-model:searchName="searchName" />
<TableRefresh @search="search()" />
@@ -104,16 +97,14 @@
@change="updateGroup(row)"
>
<template #read>
{{ row.groupBelong === 'Default' ? $t('commons.table.default') : row.groupBelong }}
{{ row.groupBelong }}
</template>
<div v-for="item in groupOptions" :key="item.id">
<el-option
v-if="item.name === 'Default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
<el-option
v-for="item in groupOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</fu-select-rw-switch>
</template>
</el-table-column>
@@ -474,17 +465,10 @@ const loadGroups = async () => {
searchGroupID.value = undefined;
paginationConfig.currentPage = 1;
}
for (const group of groupOptions.value) {
if (group.name === 'Default') {
defaultGroupID.value = group.id;
break;
}
}
defaultGroupID.value = groupOptions.value.find((group) => group.isDefault)?.id;
for (const item of data.value) {
if (item.groupID === 0) {
item.groupBelong = 'Default';
item.groupID = defaultGroupID.value;
continue;
}
let hasGroup = false;
for (const group of groupOptions.value) {
@@ -80,14 +80,12 @@
<LayoutCol>
<el-form-item :label="$t('commons.table.group')" prop="groupID">
<el-select filterable v-model="form.groupID" clearable>
<div v-for="item in groupOptions" :key="item.id">
<el-option
v-if="item.name === 'Default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
<el-option
v-for="item in groupOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</LayoutCol>
+10 -18
View File
@@ -45,14 +45,7 @@
<template #rightToolBar>
<el-select v-model="group" @change="search()" clearable class="p-w-200">
<template #prefix>{{ $t('commons.table.group') }}</template>
<div v-for="item in groupOptions" :key="item.id">
<el-option
v-if="item.name === 'Default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
<el-option v-for="item in groupOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<TableSearch @search="search()" v-model:searchName="searchInfo" />
<TableRefresh @search="search()" />
@@ -69,7 +62,12 @@
:heightDiff="300"
>
<el-table-column type="selection" fix />
<el-table-column :label="$t('commons.table.name')" show-overflow-tooltip prop="name" min-width="60">
<el-table-column
:label="$t('commons.table.name')"
show-overflow-tooltip
prop="name"
min-width="140"
>
<template #default="{ row }">
<el-text type="primary" class="cursor-pointer" @click="showScript(row)">
{{ row.name }}
@@ -78,21 +76,15 @@
</el-table-column>
<el-table-column :label="$t('cronjob.library.isInteractive')" prop="isInteractive" min-width="60">
<template #default="{ row }">
<div class="-mb-1">
<el-icon v-if="row.isInteractive"><Check /></el-icon>
<el-icon v-else><Minus /></el-icon>
</div>
{{ $t(row.isInteractive ? 'commons.true' : 'commons.false') }}
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.group')" min-width="120" prop="group">
<el-table-column :label="$t('commons.table.group')" min-width="100" prop="group">
<template #default="{ row }">
<el-button class="mr-3" size="small" v-if="row.isSystem">{{ $t('menu.system') }}</el-button>
<span v-if="row.groupBelong">
<el-button size="small" v-for="(item, index) in row.groupBelong" :key="index">
<span v-if="item === 'Default'">
{{ $t('commons.table.default') }}
</span>
<span v-else>{{ item }}</span>
{{ item }}
</el-button>
</span>
</template>
@@ -21,14 +21,7 @@
</el-form-item>
<el-form-item :label="$t('commons.table.group')" prop="groupList">
<el-select filterable v-model="dialogData.rowData!.groupList" multiple>
<div v-for="item in groupOptions" :key="item.id">
<el-option
v-if="item.name === 'Default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
<el-option v-for="item in groupOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<span class="input-help">{{ $t('cronjob.library.groupHelper') }}</span>
</el-form-item>
@@ -33,7 +33,6 @@
import { ref, nextTick } from 'vue';
import Terminal from '@/components/terminal/index.vue';
import { getCommandTree } from '@/api/modules/command';
import i18n from '@/lang';
const terminalVisible = ref(false);
const terminalRef = ref<InstanceType<typeof Terminal> | null>(null);
@@ -64,11 +63,6 @@ const initTerm = async (cwd: string) => {
const loadCommandTree = async () => {
const res = await getCommandTree('command');
commandTree.value = res.data || [];
for (const item of commandTree.value) {
if (item.label === 'Default') {
item.label = i18n.global.t('commons.table.default');
}
}
};
function quickInput(val: Array<string>) {
@@ -149,3 +149,97 @@
}
}
}
.firewall-initialize-dialog.el-dialog,
.firewall-lifecycle-dialog.el-dialog {
display: flex;
flex-direction: column;
width: min(680px, calc(100vw - 32px));
max-height: calc(92dvh - 24px);
margin-top: max(24px, 8vh);
margin-bottom: 24px;
.el-dialog__body {
min-height: 0;
overflow-y: auto;
}
.firewall-rule-dialog__content {
gap: 16px;
}
.firewall-initialize-dialog__description {
margin: 0;
color: var(--el-text-color-secondary);
font-size: 13px;
line-height: 1.7;
}
.firewall-rule-dialog__docker {
padding: 14px 16px;
border-radius: 8px;
background: var(--el-fill-color-light);
.el-form-item {
margin-bottom: 0;
}
.el-form-item__label {
height: auto;
margin-bottom: 8px;
font-weight: 500;
line-height: 1.6;
}
.el-form-item__content {
display: block;
line-height: 1.6;
}
.el-radio-group {
gap: 4px 24px;
}
.el-radio {
height: auto;
min-height: 28px;
margin-right: 0;
white-space: normal;
}
}
.firewall-rule-dialog__docker-helper {
margin: 6px 0 0;
color: var(--el-text-color-secondary);
font-size: 12px;
line-height: 1.7;
}
.firewall-rule-dialog__confirmation {
padding: 14px 0 16px;
color: var(--el-text-color-regular);
text-align: left;
font-size: 13px;
line-height: 1.7;
}
.firewall-rule-dialog__files {
max-height: min(240px, 30vh);
}
.firewall-rule-dialog__modes {
align-items: stretch;
}
.firewall-initialize-dialog__backups {
display: flex;
flex-direction: column;
gap: 12px;
border-top: 1px solid var(--el-border-color-lighter);
padding-top: 16px;
}
.firewall-rule-dialog__file-name > .min-w-0 {
flex: 1;
}
}
@@ -1,12 +1,22 @@
<template>
<DialogPro
v-model="visible"
class="firewall-rule-dialog"
:title="initialize ? $t('commons.button.init') : $t('firewall.importRuleBackup')"
class="firewall-rule-dialog firewall-initialize-dialog"
:title="
starting
? $t('firewall.firewallHelper', [$t('commons.button.start')])
: initialize
? $t('commons.button.init')
: $t('firewall.importRuleBackup')
"
:show-close="!loading"
:close-on-press-escape="!loading"
>
<template #content>
<div v-loading="loading" class="firewall-rule-dialog__content">
<el-alert v-if="message" :title="message" type="warning" :closable="false" show-icon />
<slot name="before-options" />
<p v-if="message && starting" class="firewall-initialize-dialog__description">{{ message }}</p>
<el-alert v-else-if="message" :title="message" type="warning" :closable="false" show-icon />
<el-alert
v-if="backupLoadFailed"
:title="$t('firewall.ruleBackupLoadFailed')"
@@ -24,56 +34,61 @@
class="firewall-rule-dialog__modes"
:disabled="loading"
>
<el-radio value="restore" border :disabled="!loaded || backups.files.length === 0">
{{ $t('firewall.restoreAllRules') }}
<el-radio value="initialize" border>
<span class="firewall-rule-dialog__choice-title">{{ initializeLabel }}</span>
</el-radio>
<el-radio value="restore" border>
<span class="firewall-rule-dialog__choice-title">{{ restoreLabel }}</span>
</el-radio>
<el-radio value="initialize" border>{{ $t('firewall.initializeOnly') }}</el-radio>
</el-radio-group>
<div v-if="loaded" class="firewall-rule-dialog__directory">
<div v-if="loaded && mode === 'restore'" class="firewall-rule-dialog__directory">
<el-icon><FolderOpened /></el-icon>
<span>{{ $t('firewall.backupDirectoryHelper', [backups.directory]) }}</span>
</div>
<el-radio-group
v-if="mode === 'restore' && backups.files.length"
v-model="selected"
class="firewall-rule-dialog__files"
:disabled="loading"
:aria-label="$t('firewall.importRuleBackup')"
>
<el-radio v-for="file in backups.files" :key="file.name" :value="file.name" border>
<span class="firewall-rule-dialog__file-name">
<el-icon><Document /></el-icon>
<span class="min-w-0">{{ file.name }}</span>
<el-button
v-permission
class="shrink-0"
type="primary"
link
size="small"
:disabled="loading"
:aria-label="`${$t('firewall.downloadRuleBackup')}: ${file.name}`"
@click.stop.prevent="
downloadFile(`${backups.directory}/${file.name}`, globalStore.currentNode)
"
>
{{ $t('commons.button.download') }}
</el-button>
</span>
<span class="firewall-rule-dialog__file-meta">
<el-tag size="small" type="info" effect="plain">{{ file.provider }}</el-tag>
<span>{{ $t('firewall.backupRuleCount') }}: {{ file.ruleCount }}</span>
<span>{{ new Date(file.modifiedAt).toLocaleString() }}</span>
</span>
</el-radio>
</el-radio-group>
<el-empty
v-else-if="loaded && !backups.files.length"
:description="$t('firewall.noRuleBackup')"
:image-size="64"
/>
<div v-if="mode === 'restore'" class="firewall-initialize-dialog__backups">
<el-radio-group
v-if="backups.files.length"
v-model="selected"
class="firewall-rule-dialog__files"
:disabled="loading"
:aria-label="$t('firewall.importRuleBackup')"
>
<el-radio v-for="file in backups.files" :key="file.name" :value="file.name" border>
<span class="firewall-rule-dialog__file-name">
<el-icon><Document /></el-icon>
<span class="min-w-0">{{ file.name }}</span>
<el-button
v-permission
class="shrink-0"
type="primary"
link
size="small"
:disabled="loading"
:aria-label="`${$t('firewall.downloadRuleBackup')}: ${file.name}`"
@click.stop.prevent="
downloadFile(`${backups.directory}/${file.name}`, globalStore.currentNode)
"
>
{{ $t('commons.button.download') }}
</el-button>
</span>
<span class="firewall-rule-dialog__file-meta">
<el-tag size="small" type="info" effect="plain">{{ file.provider }}</el-tag>
<span>{{ $t('firewall.backupRuleCount') }}: {{ file.ruleCount }}</span>
<span>{{ new Date(file.modifiedAt).toLocaleString() }}</span>
</span>
</el-radio>
</el-radio-group>
<el-empty
v-else-if="!initialize && loaded && !backups.files.length"
:description="$t('firewall.noRuleBackup')"
:image-size="64"
/>
</div>
</div>
</template>
<template #footer>
<slot name="confirmation" />
<el-button :disabled="loading" @click="visible = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button
type="primary"
@@ -81,13 +96,7 @@
:disabled="loading || (mode === 'restore' && (!loaded || !selected))"
@click="submit"
>
{{
initialize
? mode === 'restore'
? $t('firewall.restoreAllRules')
: $t('firewall.initializeOnly')
: $t('commons.button.import')
}}
{{ $t('commons.button.confirm') }}
</el-button>
</template>
</DialogPro>
@@ -95,7 +104,8 @@
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { computed, ref } from 'vue';
import i18n from '@/lang';
import { Document, FolderOpened } from '@element-plus/icons-vue';
import DialogPro from '@/components/dialog-pro/index.vue';
import { Firewall } from '@/api/interface/firewall';
@@ -110,13 +120,24 @@ import { GlobalStore } from '@/store';
import { downloadFile } from '@/utils/file';
const globalStore = GlobalStore();
const emit = defineEmits<{ (event: 'initialize'): void; (event: 'complete'): void }>();
const emit = defineEmits<{
(event: 'initialize'): void;
(event: 'restore', backupFile: string): void;
(event: 'complete'): void;
}>();
const visible = ref(false);
const loading = ref(false);
const loaded = ref(false);
const backupLoadFailed = ref(false);
const message = ref('');
const initialize = ref(true);
const starting = ref(false);
const initializeLabel = computed(() =>
i18n.global.t(starting.value ? 'firewall.startOnly' : 'firewall.initializeOnly'),
);
const restoreLabel = computed(() =>
i18n.global.t(starting.value ? 'firewall.startAndRestore' : 'firewall.restoreAllRules'),
);
const subsystem = ref<Firewall.BackendSubsystem>('system');
const mode = ref<'restore' | 'initialize'>('initialize');
const selected = ref('');
@@ -127,8 +148,10 @@ const acceptParams = async (
warning: string,
withInitialization = true,
target: Firewall.BackendSubsystem = 'system',
start = false,
) => {
subsystem.value = target;
starting.value = start;
initialize.value = withInitialization;
message.value = warning;
visible.value = true;
@@ -146,7 +169,7 @@ const loadBackups = async () => {
try {
backups.value = (await listFirewallRuleBackups(subsystem.value)).data;
selected.value = backups.value.files[0]?.name || '';
mode.value = selected.value || !initialize.value ? 'restore' : 'initialize';
mode.value = !starting.value && (selected.value || !initialize.value) ? 'restore' : 'initialize';
loaded.value = true;
} catch {
backupLoadFailed.value = true;
@@ -163,6 +186,11 @@ const submit = async () => {
return;
}
if (!loaded.value || !selected.value) return;
if (starting.value) {
visible.value = false;
emit('restore', selected.value);
return;
}
loading.value = true;
try {
const response =
@@ -57,7 +57,7 @@
v-node-admin
type="primary"
:disabled="lifecycleBusy"
@click="onOperate('start')"
@click="onInit"
link
>
{{ $t('commons.button.start') }}
@@ -145,18 +145,58 @@
:title="$t('firewall.directBackendConflictWarning', [backendName, baseInfo.conflictBackend])"
/>
</div>
<DockerRestart
ref="dockerRef"
v-model:withDockerRestart="withDockerRestart"
@submit="onSubmit"
<DialogPro
v-model="operationVisible"
class="firewall-rule-dialog firewall-lifecycle-dialog"
:title="$t('firewall.firewallHelper', [$t('commons.button.' + operation)])"
>
<template #helper>
<span>{{ $t('firewall.' + operation + 'FirewallHelper') }}</span>
<template #content>
<el-form class="firewall-rule-dialog__docker" label-position="top" @submit.prevent>
<el-form-item :label="$t('firewall.dockerRestart')">
<el-radio-group v-model="withDockerRestart">
<el-radio :value="true">{{ $t('setting.restartNow') }}</el-radio>
<el-radio :value="false">{{ $t('setting.restartLater') }}</el-radio>
</el-radio-group>
<p class="firewall-rule-dialog__docker-helper">
{{ $t(withDockerRestart ? 'xpack.node.syncProxyHelper1' : 'xpack.node.syncProxyHelper2') }}
</p>
</el-form-item>
</el-form>
</template>
</DockerRestart>
<template #footer>
<div class="firewall-rule-dialog__confirmation">
<span>{{ $t('firewall.' + operation + 'FirewallHelper') }}</span>
</div>
<el-button @click="operationVisible = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="onSubmit">{{ $t('commons.button.confirm') }}</el-button>
</template>
</DialogPro>
<TaskLog ref="taskLogRef" @close="handleInitializationTaskClose" />
<RuleInitialize ref="initializeRef" @initialize="initializeOnly" @complete="loadBaseInfo" />
<RuleInitialize
ref="initializeRef"
@initialize="initializeOnly"
@restore="onOperate('start', $event)"
@complete="loadBaseInfo"
>
<template v-if="isServiceBackend && dockerStatus" #before-options>
<el-form class="firewall-rule-dialog__docker" label-position="top" @submit.prevent>
<el-form-item :label="$t('firewall.dockerRestart')">
<el-radio-group v-model="withDockerRestart">
<el-radio :value="true">{{ $t('setting.restartNow') }}</el-radio>
<el-radio :value="false">{{ $t('setting.restartLater') }}</el-radio>
</el-radio-group>
<p class="firewall-rule-dialog__docker-helper">
{{ $t(withDockerRestart ? 'xpack.node.syncProxyHelper1' : 'xpack.node.syncProxyHelper2') }}
</p>
</el-form-item>
</el-form>
</template>
<template v-if="isServiceBackend" #confirmation>
<div class="firewall-rule-dialog__confirmation">
<span>{{ $t('firewall.startFirewallHelper') }}</span>
</div>
</template>
</RuleInitialize>
</div>
</template>
@@ -171,7 +211,7 @@ import {
} from '@/api/modules/firewall';
import i18n from '@/lang';
import NoSuchService from '@/components/layout-content/no-such-service.vue';
import DockerRestart from '@/components/docker-proxy/docker-restart.vue';
import DialogPro from '@/components/dialog-pro/index.vue';
import { MsgSuccess } from '@/utils/message';
import { ElMessageBox } from 'element-plus';
import { computed, nextTick, onBeforeUnmount, ref } from 'vue';
@@ -187,7 +227,7 @@ import { useGlobalStore } from '@/composables/useGlobalStore';
const props = defineProps({
currentTab: String,
});
const { docsUrl } = useGlobalStore();
const { docsUrl, isIntl } = useGlobalStore();
const goToFirewallSetting = () => routerToName('FirewallSetting');
const goToScriptLibrary = () => routerToNameWithQuery('Library', { uncached: 'true' });
@@ -206,8 +246,9 @@ const baseInfo = ref<Firewall.FirewallBase>({
ipv4: { available: false, initialized: false, bound: false },
ipv6: { available: false, initialized: false, bound: false },
});
const dockerRef = ref();
const operationVisible = ref(false);
const operation = ref('restart');
const restoreBackupFile = ref('');
const dockerStatus = ref();
const withDockerRestart = ref(false);
const taskLogRef = ref();
@@ -276,7 +317,7 @@ const familyActions = computed(() =>
message: familyIssueText(issue),
helpUrl:
issue.available && issue.reason === 'ipv6_ra_required'
? `${docsUrl.value}/user_manual/hosts/firewall`
? `${docsUrl.value}/user_manual/${isIntl.value ? 'system' : 'hosts'}/firewall`
: undefined,
action,
};
@@ -365,12 +406,20 @@ const loadDocker = async () => {
const initializeRef = ref<InstanceType<typeof RuleInitialize>>();
const onInit = async () => {
if (lifecycleBusy.value) return;
if (isServiceBackend.value) withDockerRestart.value = !!dockerStatus.value;
const name = baseInfo.value.name || backendName.value;
const message =
props.currentTab === 'base' && baseInfo.value.conflictBackend
? i18n.global.t('firewall.initDirectBackendConflictMsg', [name, baseInfo.value.conflictBackend])
: i18n.global.t('firewall.initMsg', [name]);
await initializeRef.value?.acceptParams(message, true, props.currentTab === 'forward' ? 'forwarding' : 'system');
const message = isServiceBackend.value
? i18n.global.t('firewall.startWithBackupHelper')
: props.currentTab === 'base' && baseInfo.value.conflictBackend
? i18n.global.t('firewall.initDirectBackendConflictMsg', [name, baseInfo.value.conflictBackend])
: i18n.global.t('firewall.initMsg', [name]);
await initializeRef.value?.acceptParams(
message,
true,
props.currentTab === 'forward' ? 'forwarding' : 'system',
isServiceBackend.value,
);
};
const initializeOnly = async () => {
@@ -422,17 +471,22 @@ const onUnBind = async () => {
await loadBaseInfo();
};
const onOperate = async (op: string) => {
const onOperate = async (op: string, backupFile = '') => {
if (lifecycleBusy.value) return;
operation.value = op;
if (backendName.value === 'iptables' || backendName.value === 'nftables' || !dockerStatus.value) {
restoreBackupFile.value = backupFile;
if (op === 'start') {
await submitLifecycleOperation(!!dockerStatus.value && withDockerRestart.value);
} else if (backendName.value === 'iptables' || backendName.value === 'nftables' || !dockerStatus.value) {
await submitLifecycleOperation(false);
} else {
dockerRef.value.acceptParams({ title: i18n.global.t('firewall.dockerRestart') });
withDockerRestart.value = true;
operationVisible.value = true;
}
};
const onSubmit = async () => {
operationVisible.value = false;
await submitLifecycleOperation(withDockerRestart.value);
};
@@ -447,7 +501,7 @@ const submitLifecycleOperation = async (restartDocker: boolean) => {
lifecycleSubmitting.value = true;
emit('update:loading', true);
try {
const result = (await operateFire(operation.value, restartDocker)).data;
const result = (await operateFire(operation.value, restartDocker, restoreBackupFile.value || undefined)).data;
if (disposed) return;
if (result?.queued && result.taskID) {
baseInfo.value.lifecycleTaskID = result.taskID;
@@ -27,10 +27,7 @@
@change="changeGroup"
class="p-w-200 ml-2 mt-2"
>
<div v-for="item in groupList" :key="item.id">
<el-option v-if="item.name === 'Default'" :label="$t('commons.table.default')" :value="item.id" />
<el-option v-else :label="item.name" :value="item.id" />
</div>
<el-option v-for="item in groupList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<el-card class="mt-2 w-full" v-loading="loading">
@@ -54,12 +51,7 @@
min-width="80"
prop="groupBelong"
fix
>
<template #default="{ row }">
<span v-if="row.groupBelong === 'Default'">{{ $t('commons.table.default') }}</span>
<span v-else>{{ row.groupBelong }}</span>
</template>
</el-table-column>
/>
<el-table-column
:label="$t('terminal.command')"
:min-width="120"
+8 -17
View File
@@ -27,14 +27,7 @@
<template #rightToolBar>
<el-select v-model="group" @change="search()" clearable class="p-w-200">
<template #prefix>{{ $t('commons.table.group') }}</template>
<div v-for="item in groupList" :key="item.id">
<el-option
v-if="item.name === 'Default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
<el-option v-for="item in groupList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<TableSearch @search="search()" v-model:searchName="info" />
<TableRefresh @search="search()" />
@@ -74,16 +67,14 @@
<template #default="{ row }">
<fu-select-rw-switch v-model="row.groupID" @change="updateGroup(row)">
<template #read>
{{ row.groupBelong === 'Default' ? $t('commons.table.default') : row.groupBelong }}
{{ row.groupBelong }}
</template>
<div v-for="item in groupList" :key="item.id">
<el-option
v-if="item.name === 'Default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
<el-option
v-for="item in groupList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</fu-select-rw-switch>
</template>
</el-table-column>
@@ -22,14 +22,7 @@
</el-form-item>
<el-form-item :label="$t('commons.table.group')" prop="groupID">
<el-select filterable v-model="dialogData.rowData!.groupID" clearable style="width: 100%">
<div v-for="item in groupList" :key="item.id">
<el-option
v-if="item.name === 'Default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
<el-option v-for="item in groupList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item v-if="!batchMode" :label="$t('terminal.command')" prop="command">
+8 -17
View File
@@ -16,14 +16,7 @@
<el-select v-model="group" @change="search()" clearable class="p-w-200">
<template #prefix>{{ $t('commons.table.group') }}</template>
<el-option :label="$t('commons.table.all')" value=""></el-option>
<div v-for="item in groupList" :key="item.id">
<el-option
v-if="item.name === 'Default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
<el-option v-for="item in groupList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<TableSearch @search="search()" v-model:searchName="info" />
<TableRefresh @search="search()" />
@@ -48,16 +41,14 @@
<template #default="{ row }">
<fu-select-rw-switch v-model="row.groupID" @change="updateGroup(row)">
<template #read>
{{ row.groupBelong === 'Default' ? $t('commons.table.default') : row.groupBelong }}
{{ row.groupBelong }}
</template>
<div v-for="item in groupList" :key="item.id">
<el-option
v-if="item.name === 'Default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
<el-option
v-for="item in groupList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</fu-select-rw-switch>
</template>
</el-table-column>
@@ -50,14 +50,7 @@
</el-form-item>
<el-form-item :label="$t('commons.table.group')" prop="groupID">
<el-select filterable v-model="dialogData.rowData!.groupID" clearable style="width: 100%">
<div v-for="item in groupList" :key="item.id">
<el-option
v-if="item.name === 'Default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
<el-option v-for="item in groupList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item :label="$t('commons.table.title')" prop="name">
@@ -268,11 +268,6 @@ const handleTabsRemove = async (targetName: string, action: 'remove' | 'add') =>
const loadCommandTree = async () => {
const res = await getCommandTree('command');
commandTree.value = res.data || [];
for (const item of commandTree.value) {
if (item.label === 'Default') {
item.label = i18n.global.t('commons.table.default');
}
}
};
const sendToTerminals = (command: string, all: boolean) => {
@@ -1,12 +1,7 @@
<template>
<el-form-item :label="$t('commons.table.group')" :prop="prop">
<el-select v-model="selectedValue">
<el-option
v-for="(group, index) in groups"
:key="index"
:label="group.name === 'Default' ? $t('commons.table.default') : group.name"
:value="group.id"
/>
<el-option v-for="(group, index) in groups" :key="index" :label="group.name" :value="group.id" />
</el-select>
</el-form-item>
</template>
+1 -8
View File
@@ -45,14 +45,7 @@
<el-select v-model="req.websiteGroupId" @change="search()" class="p-w-200" :disabled="disabledConfig">
<template #prefix>{{ $t('commons.table.group') }}</template>
<el-option :label="$t('commons.table.all')" :value="0"></el-option>
<div v-for="item in groups" :key="item.id">
<el-option
v-if="item.name === 'Default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
<el-option v-for="item in groups" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<TableSearch @search="search()" v-model:searchName="req.name" :disabled="disabledConfig" />
<TableRefresh @search="search()" :disabled="disabledConfig" />