Compare commits

...
57 changed files with 1175 additions and 244 deletions
+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
}
+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)
}
+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
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 {
+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: "尚未开启本地应用跨节点同步"
+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

@@ -18,6 +18,7 @@
size="w-70"
:show-close="false"
:modal="false"
modal-penetrable
@opened="claim"
>
<template #header>
@@ -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() : '');
}
+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: '仅初始化',
+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) => {
@@ -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;