Compare commits

...
11 Commits
Author SHA1 Message Date
王贺 90d11a3b74 remove ssl helper (#12102) 2026-03-06 15:31:28 +08:00
CityFun 883f3db725 feat: add openclaw default tool config (#12096) 2026-03-06 13:40:03 +08:00
ssongliu 75da72c265 fix: avoid pulling build-only compose images (#12095)
Refs  #12092
2026-03-06 02:29:39 +00:00
ssongliu 78eef15f5b feat(ai): hide coding plan providers in intl mode (#12090) 2026-03-05 10:47:38 +00:00
ssongliu 735cf3cc71 chore(agent): downgrade docker/cli to v28.5.1 (#12091) 2026-03-05 18:38:42 +08:00
Rowan Chen 99c5d9a8b2 feat: show SSL certificate issuer and expiry date in certificate selectors (#12071) 2026-03-05 15:44:12 +08:00
Rowan Chen 3a0d2039d0 fix: set sort_order for newly installed apps to avoid random positioning (#12069) 2026-03-05 15:43:18 +08:00
dependabot[bot] 98e2d6390a chore(deps): bump github.com/docker/cli in /agent (#12079)
Bumps [github.com/docker/cli](https://github.com/docker/cli) from 28.5.1+incompatible to 29.2.0+incompatible.
- [Commits](https://github.com/docker/cli/compare/v28.5.1...v29.2.0)

---
updated-dependencies:
- dependency-name: github.com/docker/cli
  dependency-version: 29.2.0+incompatible
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-05 12:02:34 +08:00
ssongliu 975e7f9f58 revert: drop nginx safe string validation changes (#12076) 2026-03-05 11:55:23 +08:00
王贺 77c90a26f8 chore: change screenshot (#12075)
Updated UI display screenshot in README.
2026-03-05 11:07:33 +08:00
ssongliu 17cb12df8b fix: avoid sidebar menu flicker when loading and sorting (#12066) 2026-03-05 02:37:04 +00:00
32 changed files with 298 additions and 318 deletions
+1 -1
View File
@@ -53,7 +53,7 @@ bash -c "$(curl -sSL https://resource.1panel.pro/v2/quick_start.sh)"
## Screenshot ## Screenshot
![UI Display](https://resource.1panel.pro/img/1panel.png) ![UI Display](https://resource.1panel.pro/img/overview_en_v2.png)
## Star History ## Star History
+29 -3
View File
@@ -62,9 +62,11 @@ func NewIAgentService() IAgentService {
} }
const ( const (
defaultBrowserExecutablePath = "/home/node/.cache/ms-playwright/chromium-1208/chrome-linux64/chrome" defaultBrowserExecutablePath = "/home/node/.cache/ms-playwright/chromium-1208/chrome-linux64/chrome"
defaultBrowserProfile = "openclaw" defaultBrowserProfile = "openclaw"
defaultUserTimezone = "Asia/Shanghai" defaultUserTimezone = "Asia/Shanghai"
defaultToolsProfile = "full"
defaultToolsSessionVisibility = "all"
) )
func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) { func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
@@ -1435,9 +1437,19 @@ type openclawConfig struct {
Gateway gatewayConfig `json:"gateway"` Gateway gatewayConfig `json:"gateway"`
Agents agentsConfig `json:"agents"` Agents agentsConfig `json:"agents"`
Browser browserConfig `json:"browser"` Browser browserConfig `json:"browser"`
Tools toolsConfig `json:"tools"`
Models *modelsConfig `json:"models,omitempty"` Models *modelsConfig `json:"models,omitempty"`
} }
type toolsConfig struct {
Profile string `json:"profile,omitempty"`
Sessions toolSessionsConfig `json:"sessions,omitempty"`
}
type toolSessionsConfig struct {
Visibility string `json:"visibility,omitempty"`
}
type gatewayConfig struct { type gatewayConfig struct {
Mode string `json:"mode"` Mode string `json:"mode"`
Bind string `json:"bind"` Bind string `json:"bind"`
@@ -1550,6 +1562,12 @@ func writeOpenclawConfig(confDir, provider, modelName, apiType string, maxTokens
NoSandbox: true, NoSandbox: true,
DefaultProfile: defaultBrowserProfile, DefaultProfile: defaultBrowserProfile,
}, },
Tools: toolsConfig{
Profile: defaultToolsProfile,
Sessions: toolSessionsConfig{
Visibility: defaultToolsSessionVisibility,
},
},
} }
provider = strings.ToLower(strings.TrimSpace(provider)) provider = strings.ToLower(strings.TrimSpace(provider))
@@ -1870,6 +1888,14 @@ func writeOpenclawConfig(confDir, provider, modelName, apiType string, maxTokens
} }
conf["browser"] = browserMap conf["browser"] = browserMap
} }
toolsMap := ensureChildMap(conf, "tools")
if profile, ok := toolsMap["profile"]; !ok || strings.TrimSpace(fmt.Sprintf("%v", profile)) == "" {
toolsMap["profile"] = defaultToolsProfile
}
sessionsMap := ensureChildMap(toolsMap, "sessions")
if visibility, ok := sessionsMap["visibility"]; !ok || strings.TrimSpace(fmt.Sprintf("%v", visibility)) == "" {
sessionsMap["visibility"] = defaultToolsSessionVisibility
}
agentsMap := ensureChildMap(conf, "agents") agentsMap := ensureChildMap(conf, "agents")
defaultsMap := ensureChildMap(agentsMap, "defaults") defaultsMap := ensureChildMap(agentsMap, "defaults")
if tz, ok := defaultsMap["userTimezone"]; !ok || strings.TrimSpace(fmt.Sprintf("%v", tz)) == "" { if tz, ok := defaultsMap["userTimezone"]; !ok || strings.TrimSpace(fmt.Sprintf("%v", tz)) == "" {
+4
View File
@@ -516,6 +516,10 @@ func (a AppService) Install(req request.AppInstallCreate, executeScript bool) (a
} }
appInstall.Env = string(paramByte) appInstall.Env = string(paramByte)
var maxSort int
global.DB.Model(&model.AppInstall{}).Where("favorite = ?", false).Select("COALESCE(MAX(sort_order),0)").Scan(&maxSort)
appInstall.SortOrder = maxSort + 1
if err = appInstallRepo.Create(context.Background(), appInstall); err != nil { if err = appInstallRepo.Create(context.Background(), appInstall); err != nil {
return return
} }
+2 -12
View File
@@ -459,12 +459,7 @@ func addProxy(server *model.McpServer) {
} else { } else {
proxyPath = server.StreamableHttpPath proxyPath = server.StreamableHttpPath
} }
safePass, err := nginx.NginxSafeString(fmt.Sprintf("http://127.0.0.1:%d%s", server.Port, proxyPath), nginx.ModeURL) location.UpdateDirective("proxy_pass", []string{fmt.Sprintf("http://127.0.0.1:%d%s", server.Port, proxyPath)})
if err != nil {
global.LOG.Errorf("mcp add proxy failed, err: %v", err)
return
}
location.UpdateDirective("proxy_pass", []string{safePass})
location.ChangePath("^~", proxyPath) location.ChangePath("^~", proxyPath)
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
global.LOG.Errorf("write config failed, err: %v", buserr.WithErr("ErrUpdateBuWebsite", err)) global.LOG.Errorf("write config failed, err: %v", buserr.WithErr("ErrUpdateBuWebsite", err))
@@ -522,12 +517,7 @@ func addMCPProxy(websiteID uint) error {
} else { } else {
proxyPath = server.StreamableHttpPath proxyPath = server.StreamableHttpPath
} }
safePass, err := nginx.NginxSafeString(fmt.Sprintf("http://127.0.0.1:%d%s", server.Port, proxyPath), nginx.ModeURL) location.UpdateDirective("proxy_pass", []string{fmt.Sprintf("http://127.0.0.1:%d%s", server.Port, proxyPath)})
if err != nil {
global.LOG.Errorf("mcp add proxy failed, err: %v", err)
return err
}
location.UpdateDirective("proxy_pass", []string{safePass})
location.ChangePath("^~", proxyPath) location.ChangePath("^~", proxyPath)
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
return buserr.WithErr("ErrUpdateBuWebsite", err) return buserr.WithErr("ErrUpdateBuWebsite", err)
+1 -7
View File
@@ -15,7 +15,6 @@ import (
"github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/cmd/server/nginx_conf" "github.com/1Panel-dev/1Panel/agent/cmd/server/nginx_conf"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/files" "github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/1Panel-dev/1Panel/agent/utils/nginx" "github.com/1Panel-dev/1Panel/agent/utils/nginx"
"github.com/1Panel-dev/1Panel/agent/utils/nginx/components" "github.com/1Panel-dev/1Panel/agent/utils/nginx/components"
@@ -295,12 +294,7 @@ func (w WebsiteService) UpdatePathAuthBasic(req request.NginxPathAuthUpdate) err
config.FilePath = confPath config.FilePath = confPath
directives := config.Directives directives := config.Directives
location, _ := directives[0].(*components.Location) location, _ := directives[0].(*components.Location)
safePass, err := nginx.NginxSafeString(fmt.Sprintf("/www/sites/%s/path_auth/pass/%s", website.Alias, fmt.Sprintf("%s.pass", req.Name)), nginx.ModePath) location.UpdateDirective("auth_basic_user_file", []string{fmt.Sprintf("/www/sites/%s/path_auth/pass/%s", website.Alias, fmt.Sprintf("%s.pass", req.Name))})
if err != nil {
global.LOG.Errorf("invalid auth_basic_user_file path, err: %v", err)
return err
}
location.UpdateDirective("auth_basic_user_file", []string{safePass})
location.ChangePath("~*", fmt.Sprintf("^%s", req.Path)) location.ChangePath("~*", fmt.Sprintf("^%s", req.Path))
var passwdHash []byte var passwdHash []byte
passwdHash, err = bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) passwdHash, err = bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
+8 -28
View File
@@ -27,15 +27,15 @@ func (w WebsiteService) OperateProxy(req request.WebsiteProxyConfig) (err error)
switch req.Operate { switch req.Operate {
case "delete": case "delete":
return w.DeleteProxy(request.WebsiteProxyDel{ return w.DeleteProxy(request.WebsiteProxyDel{
ID: req.ID, ID: req.ID,
Name: req.Name, Name: req.Name,
}) })
case "disable": case "disable":
fallthrough fallthrough
case "enable": case "enable":
return w.UpdateProxyStatus(request.WebsiteProxyStatusUpdate{ return w.UpdateProxyStatus(request.WebsiteProxyStatusUpdate{
ID: req.ID, ID: req.ID,
Name: req.Name, Name: req.Name,
Status: req.Operate, Status: req.Operate,
}) })
} }
@@ -114,16 +114,8 @@ func (w WebsiteService) OperateProxy(req request.WebsiteProxyConfig) (err error)
err = errors.New("invalid proxy config, no location found") err = errors.New("invalid proxy config, no location found")
return return
} }
safePass, err := nginx.NginxSafeString(req.ProxyPass, nginx.ModeURL) location.UpdateDirective("proxy_pass", []string{req.ProxyPass})
if err != nil { location.UpdateDirective("proxy_set_header", []string{"Host", req.ProxyHost})
return err
}
safeHeader, err := nginx.NginxSafeString(req.ProxyHost, nginx.ModeHost)
if err != nil {
return err
}
location.UpdateDirective("proxy_pass", []string{safePass})
location.UpdateDirective("proxy_set_header", []string{"Host", safeHeader})
location.ChangePath(req.Modifier, req.Match) location.ChangePath(req.Modifier, req.Match)
// Server Cache Settings // Server Cache Settings
if req.Cache { if req.Cache {
@@ -162,26 +154,14 @@ func (w WebsiteService) OperateProxy(req request.WebsiteProxyConfig) (err error)
} }
// CORS Settings // CORS Settings
if req.Cors { if req.Cors {
safeAllowOrigins, err := nginx.NginxSafeString(req.AllowOrigins, nginx.ModeGeneric) location.UpdateDirective("add_header", []string{"Access-Control-Allow-Origin", req.AllowOrigins, "always"})
if err != nil {
return err
}
location.UpdateDirective("add_header", []string{"Access-Control-Allow-Origin", safeAllowOrigins, "always"})
if req.AllowMethods != "" { if req.AllowMethods != "" {
safeAllowMethods, err := nginx.NginxSafeString(req.AllowMethods, nginx.ModeAllowMethods) location.UpdateDirective("add_header", []string{"Access-Control-Allow-Methods", req.AllowMethods, "always"})
if err != nil {
return err
}
location.UpdateDirective("add_header", []string{"Access-Control-Allow-Methods", safeAllowMethods, "always"})
} else { } else {
location.RemoveDirective("add_header", []string{"Access-Control-Allow-Methods"}) location.RemoveDirective("add_header", []string{"Access-Control-Allow-Methods"})
} }
if req.AllowHeaders != "" { if req.AllowHeaders != "" {
safeAllowHeaders, err := nginx.NginxSafeString(req.AllowHeaders, nginx.ModeGeneric) location.UpdateDirective("add_header", []string{"Access-Control-Allow-Headers", req.AllowHeaders, "always"})
if err != nil {
return err
}
location.UpdateDirective("add_header", []string{"Access-Control-Allow-Headers", safeAllowHeaders, "always"})
} else { } else {
location.RemoveDirective("add_header", []string{"Access-Control-Allow-Headers"}) location.RemoveDirective("add_header", []string{"Access-Control-Allow-Headers"})
} }
+16 -6
View File
@@ -1,6 +1,7 @@
package compose package compose
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"os" "os"
@@ -139,18 +140,27 @@ func getComposeImagesByCommand(filePath string) ([]string, error) {
} }
base, extra := getComposeBaseCmd() base, extra := getComposeBaseCmd()
args := append(extra, strings.Fields(loadFiles(filePath))...) args := append(extra, strings.Fields(loadFiles(filePath))...)
args = append(args, "config", "--images") args = append(args, "config", "--format", "json", "--no-normalize")
stdout, err := cmd.NewCommandMgr(cmd.WithTimeout(5*time.Minute)). stdout, err := cmd.NewCommandMgr(cmd.WithTimeout(5*time.Minute)).
RunWithStdout(base, args...) RunWithStdout(base, args...)
if err != nil { if err != nil {
return nil, fmt.Errorf("run compose config --images failed, std: %s, err: %v", stdout, err) return nil, fmt.Errorf("run compose config --format json --no-normalize failed, std: %s, err: %v", stdout, err)
}
var composeConfig struct {
Services map[string]struct {
Image string `json:"image"`
} `json:"services"`
}
if err = json.Unmarshal([]byte(stdout), &composeConfig); err != nil {
return nil, fmt.Errorf("parse compose config json failed, std: %s, err: %v", stdout, err)
} }
var images []string var images []string
seen := make(map[string]struct{}) seen := make(map[string]struct{})
for _, line := range strings.Split(stdout, "\n") { for _, service := range composeConfig.Services {
image := strings.TrimSpace(line) image := strings.TrimSpace(service.Image)
if len(image) == 0 { if image == "" {
continue continue
} }
if _, ok := seen[image]; ok { if _, ok := seen[image]; ok {
@@ -160,7 +170,7 @@ func getComposeImagesByCommand(filePath string) ([]string, error) {
images = append(images, image) images = append(images, image)
} }
if len(images) == 0 { if len(images) == 0 {
return nil, errors.New("no images found from compose config") return nil, errors.New("no images found from compose config json")
} }
return images, nil return images, nil
} }
-102
View File
@@ -1,102 +0,0 @@
package nginx
import (
"errors"
"net/url"
"strings"
"unicode"
"github.com/1Panel-dev/1Panel/agent/utils/re"
)
type Mode int
const (
ModeGeneric Mode = iota
ModeHost
ModeURL
ModePath
ModeAllowMethods
)
func NginxSafeString(input string, mode Mode) (string, error) {
if input == "" {
return "", errors.New("empty value not allowed")
}
for _, r := range input {
if unicode.IsControl(r) {
return "", errors.New("control characters not allowed")
}
}
if strings.ContainsAny(input, ";\n\r{}#`$") {
return "", errors.New("illegal nginx syntax characters")
}
switch mode {
case ModeHost:
return validateHost(input)
case ModeURL:
return validateURL(input)
case ModePath:
return validatePath(input)
case ModeAllowMethods:
return validateAllowMethods(input)
default:
return input, nil
}
}
func validateHost(host string) (string, error) {
if !re.GetRegex(re.NginxHostPattern).MatchString(host) {
return "", errors.New("invalid host format")
}
return host, nil
}
func validateURL(raw string) (string, error) {
u, err := url.Parse(raw)
if err != nil {
return "", errors.New("invalid url")
}
if u.Scheme != "http" && u.Scheme != "https" {
return "", errors.New("unsupported scheme")
}
if u.Host == "" {
return "", errors.New("missing host")
}
return raw, nil
}
func validatePath(p string) (string, error) {
if !re.GetRegex(re.NginxPathPattern).MatchString(p) {
return "", errors.New("invalid path")
}
return p, nil
}
var allowedMethods = map[string]bool{
"GET": true,
"POST": true,
"PUT": true,
"DELETE": true,
"PATCH": true,
"OPTIONS": true,
"HEAD": true,
}
func validateAllowMethods(input string) (string, error) {
parts := strings.Split(input, ",")
for i, method := range parts {
method = strings.TrimSpace(strings.ToUpper(method))
if !allowedMethods[method] {
return "", errors.New("invalid HTTP method: " + method)
}
parts[i] = method
}
return strings.Join(parts, ", "), nil
}
+24
View File
@@ -16,6 +16,7 @@ import (
"net/url" "net/url"
"os" "os"
"path" "path"
"sort"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -82,6 +83,15 @@ func (u *SettingService) GetSettingInfo() (*dto.SettingInfo, error) {
for _, set := range setting { for _, set := range setting {
settingMap[set.Key] = set.Value settingMap[set.Key] = set.Value
} }
if hideMenu, ok := settingMap["HideMenu"]; ok && len(hideMenu) > 0 {
var menus []dto.ShowMenu
if err := json.Unmarshal([]byte(hideMenu), &menus); err == nil {
sortShowMenus(menus)
if sortedBytes, err := json.Marshal(menus); err == nil {
settingMap["HideMenu"] = string(sortedBytes)
}
}
}
var info dto.SettingInfo var info dto.SettingInfo
arr, err := json.Marshal(settingMap) arr, err := json.Marshal(settingMap)
if err != nil { if err != nil {
@@ -103,6 +113,20 @@ func (u *SettingService) GetSettingInfo() (*dto.SettingInfo, error) {
return &info, err return &info, err
} }
func sortShowMenus(menus []dto.ShowMenu) {
for i := range menus {
if len(menus[i].Children) > 0 {
sortShowMenus(menus[i].Children)
}
}
sort.SliceStable(menus, func(i, j int) bool {
if menus[i].Sort == menus[j].Sort {
return menus[i].ID < menus[j].ID
}
return menus[i].Sort < menus[j].Sort
})
}
func (u *SettingService) Update(key, value string) error { func (u *SettingService) Update(key, value string) error {
oldVal, err := settingRepo.Get(repo.WithByKey(key)) oldVal, err := settingRepo.Get(repo.WithByKey(key))
if err != nil { if err != nil {
+8 -4
View File
@@ -287,7 +287,11 @@ type noteDetailHelper struct {
func (u *UpgradeService) LoadRelease() ([]dto.ReleasesNotes, error) { func (u *UpgradeService) LoadRelease() ([]dto.ReleasesNotes, error) {
var notes []dto.ReleasesNotes var notes []dto.ReleasesNotes
resp, err := req_helper.HandleGet("https://1panel.cn/docs/v2/search/search_index.json") url := "https://docs.1panel.pro/v2/search/search_index.json"
if global.CONF.Base.Edition != "intl" {
url = "https://1panel.cn/docs/v2/search/search_index.json"
}
resp, err := req_helper.HandleGet(url)
if err != nil { if err != nil {
return notes, err return notes, err
} }
@@ -321,13 +325,13 @@ func analyzeDoc(version, content string) dto.ReleasesNotes {
} }
item.CreatedAt = strings.ReplaceAll(strings.TrimSpace(parts[1]), "</p>", "") item.CreatedAt = strings.ReplaceAll(strings.TrimSpace(parts[1]), "</p>", "")
for i := 1; i < len(parts); i++ { for i := 1; i < len(parts); i++ {
if strings.Contains(parts[i], "问题修复") { if strings.Contains(parts[i], "问题修复") || strings.Contains(parts[i], "Bug Fixes") {
item.FixCount = strings.Count(parts[i], "<li>") item.FixCount = strings.Count(parts[i], "<li>")
} }
if strings.Contains(parts[i], "新增功能") { if strings.Contains(parts[i], "新增功能") || strings.Contains(parts[i], "New Features") {
item.NewCount = strings.Count(parts[i], "<li>") item.NewCount = strings.Count(parts[i], "<li>")
} }
if strings.Contains(parts[i], "功能优化") { if strings.Contains(parts[i], "功能优化") || strings.Contains(parts[i], "Improvements") {
item.OptimizationCount = strings.Count(parts[i], "<li>") item.OptimizationCount = strings.Count(parts[i], "<li>")
} }
} }
@@ -106,7 +106,8 @@ const toLxware = () => {
}; };
const to1Panel = () => { const to1Panel = () => {
window.open('https://1panel.cn', '_blank', 'noopener,noreferrer'); let url = globalStore.isIntl ? 'https://1panel.pro' : 'https://1panel.cn';
window.open(url, '_blank', 'noopener,noreferrer');
}; };
const toDoc = () => { const toDoc = () => {
@@ -117,7 +118,7 @@ const toForum = () => {
let url = globalStore.isIntl let url = globalStore.isIntl
? 'https://github.com/1Panel-dev/1Panel/discussions' ? 'https://github.com/1Panel-dev/1Panel/discussions'
: 'https://bbs.fit2cloud.com/c/1p/7'; : 'https://bbs.fit2cloud.com/c/1p/7';
window.open(url, '_blank'); window.open(url, '_blank', 'noopener,noreferrer');
}; };
const toGithub = () => { const toGithub = () => {
@@ -2,21 +2,28 @@
<DrawerPro v-model="drawerVisible" :header="$t('setting.release')" @close="handleClose" size="large"> <DrawerPro v-model="drawerVisible" :header="$t('setting.release')" @close="handleClose" size="large">
<div class="note" v-loading="loading"> <div class="note" v-loading="loading">
<el-form ref="formRef" :model="form" :rules="rules"> <el-form ref="formRef" :model="form" :rules="rules">
<el-form-item :label="$t('setting.versionItem')" prop="version"> <div class="release-settings">
<el-input class="p-w-200" disabled v-model="form.version"> <el-form-item :label="$t('setting.versionItem')" prop="version" class="compact-form-item">
<template #append> <div class="setting-control">
<CopyButton class="w-16" :isIcon="false" :content="form.version" type="primary" /> <el-input disabled v-model="form.version" />
</template> <CopyButton
</el-input> class="setting-action-btn"
</el-form-item> :isIcon="false"
<el-form-item :label="$t('setting.backupCopies')" prop="backupCopies"> :content="form.version"
<el-input class="p-w-200" type="number" v-model.number="form.backupCopies"> type="primary"
<template #append> />
<el-button @click="onSave(formRef)" class="w-16">{{ $t('commons.button.save') }}</el-button> </div>
</template> </el-form-item>
</el-input> <el-form-item :label="$t('setting.backupCopies')" prop="backupCopies" class="compact-form-item">
<span class="input-help">{{ $t('setting.backupCopiesHelper') }}</span> <div class="setting-control">
</el-form-item> <el-input-number v-model="form.backupCopies" :min="0" controls-position="right" />
<el-button @click="onSave(formRef)" class="setting-action-btn" type="primary">
{{ $t('commons.button.save') }}
</el-button>
</div>
<span class="input-help">{{ $t('setting.backupCopiesHelper') }}</span>
</el-form-item>
</div>
</el-form> </el-form>
<el-collapse v-if="notes && notes.length !== 0" v-model="currentVersion" :accordion="true"> <el-collapse v-if="notes && notes.length !== 0" v-model="currentVersion" :accordion="true">
<div v-for="(item, index) in notes" :key="index"> <div v-for="(item, index) in notes" :key="index">
@@ -183,4 +190,31 @@ defineExpose({
margin-left: 5px; margin-left: 5px;
margin-top: -4px; margin-top: -4px;
} }
.release-settings {
margin-bottom: 16px;
padding: 14px 14px 10px;
border: 1px solid var(--el-border-color-light);
border-radius: 10px;
}
.compact-form-item {
margin-bottom: 12px;
}
.compact-form-item :deep(.el-form-item__label) {
padding-bottom: 6px;
}
.setting-control {
display: flex;
align-items: center;
gap: 8px;
max-width: 340px;
width: 100%;
}
.setting-control :deep(.el-input),
.setting-control :deep(.el-input-number) {
flex: 1;
width: 100%;
}
.setting-action-btn {
min-width: 64px;
}
</style> </style>
+2 -4
View File
@@ -2042,8 +2042,6 @@ const message = {
bindDomain: 'Bind domain', bindDomain: 'Bind domain',
unBindDomain: 'Unbind domain', unBindDomain: 'Unbind domain',
panelSSL: 'Panel SSL', panelSSL: 'Panel SSL',
panelSSLHelper:
'After the automatic renewal of the panel SSL, you need to manually restart the 1Panel service for the changes to take effect.',
unBindDomainHelper: 'The action of unbinding a domain name may cause system insecurity. Continue?', unBindDomainHelper: 'The action of unbinding a domain name may cause system insecurity. Continue?',
bindDomainHelper: 'After you bind the domain, only that domain can access 1Panel service.', bindDomainHelper: 'After you bind the domain, only that domain can access 1Panel service.',
bindDomainHelper1: 'Leave it blank to disable the domain name binding.', bindDomainHelper1: 'Leave it blank to disable the domain name binding.',
@@ -2196,7 +2194,7 @@ const message = {
about: 'About', about: 'About',
versionItem: 'Current Version', versionItem: 'Current Version',
backupCopies: 'Number of Copies to Keep', backupCopies: 'Backup Copies',
backupCopiesHelper: 'Set the number of upgrade backup copies to keep for version rollback. 0 means keep all.', backupCopiesHelper: 'Set the number of upgrade backup copies to keep for version rollback. 0 means keep all.',
backupCopiesRule: 'Please keep at least 3 upgrade backup records', backupCopiesRule: 'Please keep at least 3 upgrade backup records',
release: 'Release Notes', release: 'Release Notes',
@@ -3858,7 +3856,7 @@ const message = {
masterBackup: 'Master Node Backup', masterBackup: 'Master Node Backup',
backupNode: 'Backup Node', backupNode: 'Backup Node',
backupFrequency: 'Backup Frequency (hours)', backupFrequency: 'Backup Frequency (hours)',
backupCopies: 'Backup Retention Copies', backupCopies: 'Backup Copies',
noBackupNode: 'The backup node is currently empty. Select a backup node to save and try again!', noBackupNode: 'The backup node is currently empty. Select a backup node to save and try again!',
masterBackupAlert: masterBackupAlert:
'Master node backup is not currently configured. To ensure data security, please set up a backup node as soon as possible to facilitate manual switching to a new master node in case of failure.', 'Master node backup is not currently configured. To ensure data security, please set up a backup node as soon as possible to facilitate manual switching to a new master node in case of failure.',
+2 -4
View File
@@ -2067,8 +2067,6 @@ const message = {
bindDomain: 'Vincular dominio', bindDomain: 'Vincular dominio',
unBindDomain: 'Desvincular dominio', unBindDomain: 'Desvincular dominio',
panelSSL: 'SSL del panel', panelSSL: 'SSL del panel',
panelSSLHelper:
'Tras la renovación automática del SSL del panel, deberás reiniciar manualmente el servicio 1Panel para que los cambios surtan efecto.',
unBindDomainHelper: unBindDomainHelper:
'La acción de desvincular un dominio puede provocar inseguridad en el sistema. ¿Deseas continuar?', 'La acción de desvincular un dominio puede provocar inseguridad en el sistema. ¿Deseas continuar?',
bindDomainHelper: 'Tras vincular el dominio, solo ese dominio podrá acceder al servicio de 1Panel.', bindDomainHelper: 'Tras vincular el dominio, solo ese dominio podrá acceder al servicio de 1Panel.',
@@ -2223,7 +2221,7 @@ const message = {
about: 'Acerca de', about: 'Acerca de',
versionItem: 'Versión Actual', versionItem: 'Versión Actual',
backupCopies: 'Número de Copias a Conservar', backupCopies: 'Copias de Respaldo',
backupCopiesHelper: backupCopiesHelper:
'Establezca el número de copias de respaldo de actualización para conservar para la reversión de versión. 0 significa conservar todas.', 'Establezca el número de copias de respaldo de actualización para conservar para la reversión de versión. 0 significa conservar todas.',
backupCopiesRule: 'Conserve al menos 3 registros de respaldo de actualización', backupCopiesRule: 'Conserve al menos 3 registros de respaldo de actualización',
@@ -3843,7 +3841,7 @@ const message = {
masterBackup: 'Respaldo del Nodo Principal', masterBackup: 'Respaldo del Nodo Principal',
backupNode: 'Nodo de Respaldo', backupNode: 'Nodo de Respaldo',
backupFrequency: 'Frecuencia de Respaldo (horas)', backupFrequency: 'Frecuencia de Respaldo (horas)',
backupCopies: 'Copias de Retención de Respaldo', backupCopies: 'Copias de Respaldo',
noBackupNode: 'Actualmente no hay nodo de respaldo configurado. Selecciona uno y vuelve a intentarlo.', noBackupNode: 'Actualmente no hay nodo de respaldo configurado. Selecciona uno y vuelve a intentarlo.',
masterBackupAlert: masterBackupAlert:
'No se ha configurado un respaldo del nodo principal. Para garantizar la seguridad de los datos, configura un nodo de respaldo lo antes posible y así facilitar el cambio manual en caso de fallo.', 'No se ha configurado un respaldo del nodo principal. Para garantizar la seguridad de los datos, configura un nodo de respaldo lo antes posible y así facilitar el cambio manual en caso de fallo.',
+2 -2
View File
@@ -2145,7 +2145,7 @@ const message = {
about: 'について', about: 'について',
versionItem: '現在のバージョン', versionItem: '現在のバージョン',
backupCopies: '保持するバックアップ数', backupCopies: 'バックアップ数',
backupCopiesHelper: backupCopiesHelper:
'バージョンロールバック用に保持するアップグレードバックアップの数を設定します。0はすべて保持を意味します。', 'バージョンロールバック用に保持するアップグレードバックアップの数を設定します。0はすべて保持を意味します。',
backupCopiesRule: '少なくとも3つのアップグレードバックアップ記録を保持してください', backupCopiesRule: '少なくとも3つのアップグレードバックアップ記録を保持してください',
@@ -3781,7 +3781,7 @@ const message = {
masterBackup: 'マスターノードバックアップ', masterBackup: 'マスターノードバックアップ',
backupNode: 'バックアップノード', backupNode: 'バックアップノード',
backupFrequency: 'バックアップ頻度(時間)', backupFrequency: 'バックアップ頻度(時間)',
backupCopies: 'バックアップ保持数', backupCopies: 'バックアップ数',
noBackupNode: '現在バックアップノードが空です。保存するバックアップノードを選択して再試行してください!', noBackupNode: '現在バックアップノードが空です。保存するバックアップノードを選択して再試行してください!',
masterBackupAlert: masterBackupAlert:
'現在マスターノードのバックアップが設定されていません。データセキュリティを確保するため、障害時に新しいマスターノードに手動で切り替えられるよう、速やかにバックアップノードを設定してください。', '現在マスターノードのバックアップが設定されていません。データセキュリティを確保するため、障害時に新しいマスターノードに手動で切り替えられるよう、速やかにバックアップノードを設定してください。',
+2 -2
View File
@@ -2108,7 +2108,7 @@ const message = {
about: '정보', about: '정보',
versionItem: '현재 버전', versionItem: '현재 버전',
backupCopies: '보관할 백업 복사본 수', backupCopies: '백업 수',
backupCopiesHelper: backupCopiesHelper:
'버전 롤백을 위해 보관할 업그레이드 백업 복사본 수를 설정합니다. 0은 모두 보관을 의미합니다.', '버전 롤백을 위해 보관할 업그레이드 백업 복사본 수를 설정합니다. 0은 모두 보관을 의미합니다.',
backupCopiesRule: '최소 3개의 업그레이드 백업 기록을 보관하세요', backupCopiesRule: '최소 3개의 업그레이드 백업 기록을 보관하세요',
@@ -3708,7 +3708,7 @@ const message = {
masterBackup: '마스터 노드 백업', masterBackup: '마스터 노드 백업',
backupNode: '백업 노드', backupNode: '백업 노드',
backupFrequency: '백업 주기(시간)', backupFrequency: '백업 주기(시간)',
backupCopies: '백업 기록 보관 수', backupCopies: '백업 수',
noBackupNode: '현재 백업 노드가 비어 있습니다. 저장할 백업 노드를 선택한 후 다시 시도하십시오!', noBackupNode: '현재 백업 노드가 비어 있습니다. 저장할 백업 노드를 선택한 후 다시 시도하십시오!',
masterBackupAlert: masterBackupAlert:
'현재 마스터 노드 백업이 구성되지 않았습니다. 데이터 보안을 위해 장애 시 새로운 마스터 노드로 수동 전환이 가능하도록 가능한 빨리 백업 노드를 설정하십시오.', '현재 마스터 노드 백업이 구성되지 않았습니다. 데이터 보안을 위해 장애 시 새로운 마스터 노드로 수동 전환이 가능하도록 가능한 빨리 백업 노드를 설정하십시오.',
+2 -2
View File
@@ -2203,7 +2203,7 @@ const message = {
about: 'Mengenai', about: 'Mengenai',
versionItem: 'Versi Semasa', versionItem: 'Versi Semasa',
backupCopies: 'Bilangan Salinan untuk Disimpan', backupCopies: 'Salinan Sandaran',
backupCopiesHelper: backupCopiesHelper:
'Tetapkan bilangan salinan sandaran naik taraf untuk disimpan untuk pemulihan versi. 0 bermakna simpan semua.', 'Tetapkan bilangan salinan sandaran naik taraf untuk disimpan untuk pemulihan versi. 0 bermakna simpan semua.',
backupCopiesRule: 'Sila simpan sekurang-kurangnya 3 rekod sandaran naik taraf', backupCopiesRule: 'Sila simpan sekurang-kurangnya 3 rekod sandaran naik taraf',
@@ -3848,7 +3848,7 @@ const message = {
masterBackup: 'Sandaran Nod Master', masterBackup: 'Sandaran Nod Master',
backupNode: 'Nod Sandaran', backupNode: 'Nod Sandaran',
backupFrequency: 'Kekerapan Sandaran (jam)', backupFrequency: 'Kekerapan Sandaran (jam)',
backupCopies: 'Bilangan salinan sandaran yang disimpan', backupCopies: 'Salinan Sandaran',
noBackupNode: 'Nod sandaran kosong. Sila pilih nod sandaran untuk disimpan dan cuba lagi!', noBackupNode: 'Nod sandaran kosong. Sila pilih nod sandaran untuk disimpan dan cuba lagi!',
masterBackupAlert: masterBackupAlert:
'Sandaran nod master belum dikonfigurasikan. Untuk memastikan keselamatan data, sila sediakan nod sandaran secepat mungkin untuk memudahkan pertukaran manual ke nod master baru sekiranya berlaku kegagalan.', 'Sandaran nod master belum dikonfigurasikan. Untuk memastikan keselamatan data, sila sediakan nod sandaran secepat mungkin untuk memudahkan pertukaran manual ke nod master baru sekiranya berlaku kegagalan.',
+2 -2
View File
@@ -2192,7 +2192,7 @@ const message = {
about: 'Sobre', about: 'Sobre',
versionItem: 'Versão Atual', versionItem: 'Versão Atual',
backupCopies: 'Número de Cópias a Manter', backupCopies: 'Cópias de Backup',
backupCopiesHelper: backupCopiesHelper:
'Defina o número de cópias de backup de atualização para manter para reversão de versão. 0 significa manter todas.', 'Defina o número de cópias de backup de atualização para manter para reversão de versão. 0 significa manter todas.',
backupCopiesRule: 'Mantenha pelo menos 3 registros de backup de atualização', backupCopiesRule: 'Mantenha pelo menos 3 registros de backup de atualização',
@@ -3867,7 +3867,7 @@ const message = {
masterBackup: 'Backup do Nó Mestre', masterBackup: 'Backup do Nó Mestre',
backupNode: 'Nó de Backup', backupNode: 'Nó de Backup',
backupFrequency: 'Frequência de Backup (horas)', backupFrequency: 'Frequência de Backup (horas)',
backupCopies: 'Número de cópias de backup a reter', backupCopies: 'Cópias de Backup',
noBackupNode: noBackupNode:
'O nó de backup está vazio atualmente. Selecione um nó de backup para salvar e tente novamente!', 'O nó de backup está vazio atualmente. Selecione um nó de backup para salvar e tente novamente!',
masterBackupAlert: masterBackupAlert:
+2 -2
View File
@@ -2189,7 +2189,7 @@ const message = {
about: 'О программе', about: 'О программе',
versionItem: 'Текущая Версия', versionItem: 'Текущая Версия',
backupCopies: 'Количество Копий для Сохранения', backupCopies: 'Копии Бэкапа',
backupCopiesHelper: backupCopiesHelper:
'Установите количество копий резервных копий обновления для сохранения для отката версии. 0 означает сохранить все.', 'Установите количество копий резервных копий обновления для сохранения для отката версии. 0 означает сохранить все.',
backupCopiesRule: 'Пожалуйста, сохраните как минимум 3 записи резервных копий обновления', backupCopiesRule: 'Пожалуйста, сохраните как минимум 3 записи резервных копий обновления',
@@ -3852,7 +3852,7 @@ const message = {
masterBackup: 'Резервная копия главного узла', masterBackup: 'Резервная копия главного узла',
backupNode: 'Резервный узел', backupNode: 'Резервный узел',
backupFrequency: 'Частота резервного копирования (часы)', backupFrequency: 'Частота резервного копирования (часы)',
backupCopies: 'Количество сохраняемых резервных копий', backupCopies: 'Копии Бэкапа',
noBackupNode: noBackupNode:
'Резервный узел в настоящее время пуст. Выберите резервный узел для сохранения и повторите попытку!', 'Резервный узел в настоящее время пуст. Выберите резервный узел для сохранения и повторите попытку!',
masterBackupAlert: masterBackupAlert:
+2 -4
View File
@@ -2074,8 +2074,6 @@ const message = {
bindDomain: 'Alan adı bağla', bindDomain: 'Alan adı bağla',
unBindDomain: 'Alan adı bağlamasını kaldır', unBindDomain: 'Alan adı bağlamasını kaldır',
panelSSL: 'Panel SSL', panelSSL: 'Panel SSL',
panelSSLHelper:
'Panel SSL’nin otomatik yenilenmesinden sonra, değişikliklerin etkili olması için 1Panel servisini manuel olarak yeniden başlatmanız gerekir.',
unBindDomainHelper: unBindDomainHelper:
'Alan adı bağlamasını kaldırma işlemi sistem güvenliğini etkileyebilir. Devam etmek istiyor musunuz?', 'Alan adı bağlamasını kaldırma işlemi sistem güvenliğini etkileyebilir. Devam etmek istiyor musunuz?',
bindDomainHelper: 'Alan adı bağlandıktan sonra, yalnızca o alan adı 1Panel servisine erişebilir.', bindDomainHelper: 'Alan adı bağlandıktan sonra, yalnızca o alan adı 1Panel servisine erişebilir.',
@@ -2228,7 +2226,7 @@ const message = {
about: 'Hakkında', about: 'Hakkında',
versionItem: 'Mevcut Sürüm', versionItem: 'Mevcut Sürüm',
backupCopies: 'Saklanacak Kopya Sayısı', backupCopies: 'Yedek Kopya',
backupCopiesHelper: backupCopiesHelper:
'Sürüm geri alma için saklanacak yükseltme yedek kopya sayısını ayarlayın. 0, tümünü sakla anlamına gelir.', 'Sürüm geri alma için saklanacak yükseltme yedek kopya sayısını ayarlayın. 0, tümünü sakla anlamına gelir.',
backupCopiesRule: 'Lütfen en az 3 yükseltme yedek kaydı saklayın', backupCopiesRule: 'Lütfen en az 3 yükseltme yedek kaydı saklayın',
@@ -3911,7 +3909,7 @@ const message = {
masterBackup: 'Ana Düğüm Yedekleme', masterBackup: 'Ana Düğüm Yedekleme',
backupNode: 'Yedek Düğüm', backupNode: 'Yedek Düğüm',
backupFrequency: 'Yedekleme Sıklığı (saat)', backupFrequency: 'Yedekleme Sıklığı (saat)',
backupCopies: 'Saklanacak yedek kopya sayısı', backupCopies: 'Yedek Kopya',
noBackupNode: 'Yedek düğüm şu anda boş. Lütfen kaydetmek için bir yedek düğüm seçin ve tekrar deneyin!', noBackupNode: 'Yedek düğüm şu anda boş. Lütfen kaydetmek için bir yedek düğüm seçin ve tekrar deneyin!',
masterBackupAlert: masterBackupAlert:
'Ana düğüm yedeklemesi şu anda yapılandırılmamış. Veri güvenliği için, lütfen arıza durumunda yeni bir ana düğüme manuel geçiş yapabilmek amacıyla en kısa sürede bir yedek düğüm ayarlayın.', 'Ana düğüm yedeklemesi şu anda yapılandırılmamış. Veri güvenliği için, lütfen arıza durumunda yeni bir ana düğüme manuel geçiş yapabilmek amacıyla en kısa sürede bir yedek düğüm ayarlayın.',
+2 -3
View File
@@ -2006,7 +2006,6 @@ const message = {
bindDomain: '網域綁定', bindDomain: '網域綁定',
unBindDomain: '域名解綁', unBindDomain: '域名解綁',
panelSSL: '面板 SSL', panelSSL: '面板 SSL',
panelSSLHelper: '面板 SSL 自動續期後需要手動重新啟動 1Panel 服務才可生效',
unBindDomainHelper: '解除網域綁定可能造成系統不安全,是否繼續?', unBindDomainHelper: '解除網域綁定可能造成系統不安全,是否繼續?',
bindDomainHelper: '設定網域綁定後,僅能透過設定中域名存取 1Panel 服務', bindDomainHelper: '設定網域綁定後,僅能透過設定中域名存取 1Panel 服務',
bindDomainHelper1: '綁定域名為空時,則取消網域綁定', bindDomainHelper1: '綁定域名為空時,則取消網域綁定',
@@ -2062,7 +2061,7 @@ const message = {
about: '關於', about: '關於',
versionItem: '當前版本', versionItem: '當前版本',
backupCopies: '保留份數', backupCopies: '備份份數',
backupCopiesHelper: '設定用於版本回滾的升級備份保留份數。0 表示保留所有。', backupCopiesHelper: '設定用於版本回滾的升級備份保留份數。0 表示保留所有。',
backupCopiesRule: '請至少儲存 3 份升級備份記錄', backupCopiesRule: '請至少儲存 3 份升級備份記錄',
release: '版本更新日誌', release: '版本更新日誌',
@@ -3580,7 +3579,7 @@ const message = {
masterBackup: '主節點備份', masterBackup: '主節點備份',
backupNode: '備份節點', backupNode: '備份節點',
backupFrequency: '備份頻率(小時)', backupFrequency: '備份頻率(小時)',
backupCopies: '備份記錄保留份數', backupCopies: '備份份數',
noBackupNode: '目前備份節點為空,請選擇備份節點儲存後重試', noBackupNode: '目前備份節點為空,請選擇備份節點儲存後重試',
masterBackupAlert: masterBackupAlert:
'目前未設定主節點備份,為保障資料安全,請盡快設定備份節點,便於主節點故障時可人工切換新主節點。', '目前未設定主節點備份,為保障資料安全,請盡快設定備份節點,便於主節點故障時可人工切換新主節點。',
+2 -3
View File
@@ -1949,7 +1949,6 @@ const message = {
bindDomain: '域名绑定', bindDomain: '域名绑定',
unBindDomain: '域名解绑', unBindDomain: '域名解绑',
panelSSL: '面板 SSL', panelSSL: '面板 SSL',
panelSSLHelper: '面板 SSL 自动续期后需要手动重启 1Panel 服务才可生效',
unBindDomainHelper: '解除域名绑定可能造成系统不安全,是否继续?', unBindDomainHelper: '解除域名绑定可能造成系统不安全,是否继续?',
bindDomainHelper: '设置域名绑定后,仅能通过设置中域名访问 1Panel 服务', bindDomainHelper: '设置域名绑定后,仅能通过设置中域名访问 1Panel 服务',
bindDomainHelper1: '绑定域名为空时,则取消域名绑定', bindDomainHelper1: '绑定域名为空时,则取消域名绑定',
@@ -2032,7 +2031,7 @@ const message = {
about: '关于', about: '关于',
versionItem: '当前版本', versionItem: '当前版本',
backupCopies: '保留份数', backupCopies: '备份份数',
backupCopiesHelper: '设置用于版本回滚的升级备份保留份数,为 0 则保留所有。', backupCopiesHelper: '设置用于版本回滚的升级备份保留份数,为 0 则保留所有。',
backupCopiesRule: '请至少保存 3 份升级备份记录', backupCopiesRule: '请至少保存 3 份升级备份记录',
release: '版本更新日志', release: '版本更新日志',
@@ -3534,7 +3533,7 @@ const message = {
masterBackup: '主节点备份', masterBackup: '主节点备份',
backupNode: '备份节点', backupNode: '备份节点',
backupFrequency: '备份频率(小时)', backupFrequency: '备份频率(小时)',
backupCopies: '备份记录保留份数', backupCopies: '备份份数',
noBackupNode: '当前备份节点为空,请选择备份节点保存后重试!', noBackupNode: '当前备份节点为空,请选择备份节点保存后重试!',
masterBackupAlert: masterBackupAlert:
'当前未配置主节点备份,为保障数据安全,请尽快设置备份节点,便于主节点故障时可人工切换新主节点。', '当前未配置主节点备份,为保障数据安全,请尽快设置备份节点,便于主节点故障时可人工切换新主节点。',
@@ -39,7 +39,6 @@ import { GlobalStore, MenuStore } from '@/store';
import { isString } from '@vueuse/core'; import { isString } from '@vueuse/core';
import { getSettingInfo } from '@/api/modules/setting'; import { getSettingInfo } from '@/api/modules/setting';
import PrimaryMenu from '@/assets/images/menu-bg.svg?component'; import PrimaryMenu from '@/assets/images/menu-bg.svg?component';
import { sortMenu } from '@/utils/util';
const route = useRoute(); const route = useRoute();
const menuStore = MenuStore(); const menuStore = MenuStore();
@@ -72,13 +71,13 @@ const handleMenuClick = (path) => {
emit('menuClick', path); emit('menuClick', path);
}; };
function getCheckedLabels(menu: any, showMap: any) { function getCheckedLabels(menu: any, showSet: Set<string>) {
for (const item of menu) { for (const item of menu) {
if (item.isShow) { if (item.isShow) {
showMap[item.label] = true; showSet.add(item.label);
} }
if (item.children) { if (item.children) {
getCheckedLabels(item.children, showMap); getCheckedLabels(item.children, showSet);
} }
} }
} }
@@ -88,73 +87,50 @@ const openTask = () => {
}; };
const search = async () => { const search = async () => {
const res = await getSettingInfo(); try {
version.value = res.data.systemVersion; const res = await getSettingInfo();
let hideMenu = JSON.parse(res.data.hideMenu); version.value = res.data.systemVersion;
sortMenu(hideMenu); let hideMenu = JSON.parse(res.data.hideMenu);
const showMap = new Map(); const showSet = new Set<string>();
getCheckedLabels(hideMenu, showMap); getCheckedLabels(hideMenu, showSet);
const rootMap = new Map(); const rstMenuList: RouteRecordRaw[] = [];
hideMenu.forEach((m, index) => { const resMenuList = adjustAndCleanMenu(hideMenu, menuList);
rootMap.set(m.label, index); for (const menu of resMenuList) {
}); let menuItem = JSON.parse(JSON.stringify(menu));
let rstMenuList: RouteRecordRaw[] = []; if (!showSet.has(menuItem.name as string)) {
let resMenuList: RouteRecordRaw[] = []; continue;
resMenuList = adjustAndCleanMenu(hideMenu, menuStore.menuList); } else if (menuItem.name === 'Xpack-Menu') {
for (const menu of resMenuList) { menuItem.meta.hideInSidebar = false;
let menuItem = JSON.parse(JSON.stringify(menu)); }
if (!showMap[menuItem.name]) { const itemChildren =
continue; (menuItem.children ?? []).filter(
} else if (menuItem.name === 'Xpack-Menu') {
menuItem.meta.hideInSidebar = false;
}
const childMenu = hideMenu.find((item) => item.label == menu.name);
const childMap = buildIndexMap(childMenu?.children || []);
const itemChildren =
(menuItem.children ?? [])
.filter(
(item) => (item) =>
item.name && showMap[item.name as string] && !(item.name === 'Upage' && globalStore.isIntl), item.name && showSet.has(item.name as string) && !(item.name === 'Upage' && globalStore.isIntl),
) ) || [];
.sort(sortByMap(childMap)) || [];
if (itemChildren.length === 1) { if (itemChildren.length === 1) {
menuItem.meta.icon = itemChildren[0].meta.icon; menuItem.meta.icon = itemChildren[0].meta.icon;
menuItem.meta.title = itemChildren[0].meta.title; menuItem.meta.title = itemChildren[0].meta.title;
}
menuItem.children = itemChildren;
rstMenuList.push(menuItem);
}
if (!isSameMenuList(menuStore.menuList as RouteRecordRaw[], rstMenuList)) {
menuStore.setMenuList(rstMenuList);
}
} catch (error) {
if (!menuStore.menuList || menuStore.menuList.length === 0) {
menuStore.setMenuList(menuList);
} }
menuItem.children = itemChildren;
rstMenuList.push(menuItem);
} }
rstMenuList.sort((a, b) => {
const labelA = a.name;
const labelB = b.name;
const indexA = rootMap.get(labelA) ?? Infinity;
const indexB = rootMap.get(labelB) ?? Infinity;
return indexA - indexB;
});
menuStore.menuList = rstMenuList;
}; };
function buildIndexMap(list: any[]): Map<string, number> { function isSameMenuList(source: RouteRecordRaw[], target: RouteRecordRaw[]) {
const map = new Map<string, number>(); return JSON.stringify(source) === JSON.stringify(target);
list.forEach((m, i) => map.set(m.label, i));
return map;
}
function sortByMap(map: Map<string, number>) {
return (a: { name: string }, b: { name: string }) => {
const indexA = map.get(a.name) ?? Infinity;
const indexB = map.get(b.name) ?? Infinity;
return indexA - indexB;
};
} }
function adjustAndCleanMenu(menuItem, list) { function adjustAndCleanMenu(menuItem, list) {
const menuList = JSON.parse(JSON.stringify(list)); const menuList = JSON.parse(JSON.stringify(list));
const orderMap = new Map();
menuItem.forEach((item, index) => {
orderMap.set(item.label, index);
});
const itemMap = new Map(); const itemMap = new Map();
for (const parent of menuList) { for (const parent of menuList) {
itemMap.set(parent.name, parent); itemMap.set(parent.name, parent);
@@ -175,15 +151,7 @@ function adjustAndCleanMenu(menuItem, list) {
if (!matched) continue; if (!matched) continue;
if (Array.isArray(ref.children) && ref.children.length > 0) { if (Array.isArray(ref.children) && ref.children.length > 0) {
const childMap = buildIndexMap(ref.children || []);
matched.children = buildTree(ref.children); matched.children = buildTree(ref.children);
matched.children.sort((a, b) => {
const labelA = a.name;
const labelB = b.name;
const indexA = childMap.get(labelA) ?? Infinity;
const indexB = childMap.get(labelB) ?? Infinity;
return indexA - indexB;
});
} else { } else {
delete matched.children; delete matched.children;
} }
@@ -195,11 +163,6 @@ function adjustAndCleanMenu(menuItem, list) {
} }
const newMenu = buildTree(menuItem); const newMenu = buildTree(menuItem);
newMenu.sort((a, b) => {
const indexA = orderMap.get(a.name) ?? Infinity;
const indexB = orderMap.get(b.name) ?? Infinity;
return indexA - indexB;
});
for (const menu of newMenu) { for (const menu of newMenu) {
if (menu.children?.length === 1) { if (menu.children?.length === 1) {
menu.meta.icon = menu.children[0].meta.icon; menu.meta.icon = menu.children[0].meta.icon;
@@ -211,7 +174,9 @@ function adjustAndCleanMenu(menuItem, list) {
} }
onMounted(() => { onMounted(() => {
menuStore.setMenuList(menuList); if (!menuStore.menuList || menuStore.menuList.length === 0) {
menuStore.setMenuList(menuList);
}
search(); search();
}); });
</script> </script>
+1 -1
View File
@@ -60,7 +60,7 @@ const GlobalStore = defineStore({
state.themeConfig.theme === 'dark' || state.themeConfig.theme === 'dark' ||
(state.themeConfig.theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches), (state.themeConfig.theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches),
isDarkGoldTheme: (state) => state.themeConfig.primary === '#F0BE96' && state.isProductPro, isDarkGoldTheme: (state) => state.themeConfig.primary === '#F0BE96' && state.isProductPro,
docsUrl: (state) => (state.isIntl ? 'https://docs.1panel.pro/v1' : 'https://1panel.cn/docs/v2'), docsUrl: (state) => (state.isIntl ? 'https://docs.1panel.pro/v2' : 'https://1panel.cn/docs/v2'),
isMaster: (state) => state.currentNode === 'local', isMaster: (state) => state.currentNode === 'local',
}, },
actions: { actions: {
@@ -108,6 +108,7 @@ import { getAgentProviderDisplayName } from '@/utils/agent';
import { App } from '@/api/interface/app'; import { App } from '@/api/interface/app';
import AdvancedSetting from '@/components/advanced-setting/index.vue'; import AdvancedSetting from '@/components/advanced-setting/index.vue';
import AccountAddDialog from '@/views/ai/agents/model/add/index.vue'; import AccountAddDialog from '@/views/ai/agents/model/add/index.vue';
import { useGlobalStore } from '@/composables/useGlobalStore';
const emit = defineEmits(['search', 'task']); const emit = defineEmits(['search', 'task']);
@@ -121,6 +122,7 @@ const providerAccountCount = ref<Record<string, number>>({});
const manualModel = ref(false); const manualModel = ref(false);
const appInfo = ref<App.AppDTO>(); const appInfo = ref<App.AppDTO>();
const accountAddRef = ref(); const accountAddRef = ref();
const { isIntl } = useGlobalStore();
const form = reactive({ const form = reactive({
name: '', name: '',
@@ -215,11 +217,13 @@ const loadProviders = async () => {
} }
const res = await getAgentProviders(); const res = await getAgentProviders();
const data = res.data || []; const data = res.data || [];
providerOptions.value = data.map((item) => ({ const blockedProviders = new Set(['ark-coding-plan', 'bailian-coding-plan']);
const filteredData = isIntl.value ? data.filter((item) => !blockedProviders.has(item.provider)) : data;
providerOptions.value = filteredData.map((item) => ({
value: item.provider, value: item.provider,
label: getAgentProviderDisplayName(item.provider, item.displayName), label: getAgentProviderDisplayName(item.provider, item.displayName),
})); }));
providerModels.value = data.reduce((acc, item) => { providerModels.value = filteredData.reduce((acc, item) => {
acc[item.provider] = item.models || []; acc[item.provider] = item.models || [];
return acc; return acc;
}, {} as Record<string, AI.ProviderModelInfo[]>); }, {} as Record<string, AI.ProviderModelInfo[]>);
@@ -35,6 +35,7 @@ import { AI } from '@/api/interface/ai';
import { getAgentProviders, pageAgentAccounts, updateAgentModelConfig } from '@/api/modules/ai'; import { getAgentProviders, pageAgentAccounts, updateAgentModelConfig } from '@/api/modules/ai';
import { Rules } from '@/global/form-rules'; import { Rules } from '@/global/form-rules';
import { MsgSuccess } from '@/utils/message'; import { MsgSuccess } from '@/utils/message';
import { useGlobalStore } from '@/composables/useGlobalStore';
const emit = defineEmits(['updated']); const emit = defineEmits(['updated']);
const { t } = useI18n(); const { t } = useI18n();
@@ -42,6 +43,8 @@ const { t } = useI18n();
const loading = ref(false); const loading = ref(false);
const saving = ref(false); const saving = ref(false);
const formRef = ref<FormInstance>(); const formRef = ref<FormInstance>();
const { isIntl } = useGlobalStore();
const blockedProviders = new Set(['ark-coding-plan', 'bailian-coding-plan']);
const agentId = ref(0); const agentId = ref(0);
const providerModels = ref<Record<string, AI.ProviderModelInfo[]>>({}); const providerModels = ref<Record<string, AI.ProviderModelInfo[]>>({});
@@ -81,7 +84,8 @@ const loadProviders = async () => {
} }
const res = await getAgentProviders(); const res = await getAgentProviders();
const data = res.data || []; const data = res.data || [];
providerModels.value = data.reduce((acc, item) => { const filteredData = isIntl.value ? data.filter((item) => !blockedProviders.has(item.provider)) : data;
providerModels.value = filteredData.reduce((acc, item) => {
acc[item.provider] = item.models || []; acc[item.provider] = item.models || [];
return acc; return acc;
}, {} as Record<string, AI.ProviderModelInfo[]>); }, {} as Record<string, AI.ProviderModelInfo[]>);
@@ -94,7 +98,8 @@ const loadAccounts = async () => {
provider: '', provider: '',
name: '', name: '',
}); });
accountOptions.value = res.data.items || []; const items = res.data.items || [];
accountOptions.value = isIntl.value ? items.filter((item) => !blockedProviders.has(item.provider)) : items;
}; };
const setModelsByProvider = (provider: string) => { const setModelsByProvider = (provider: string) => {
@@ -76,6 +76,7 @@ import { Rules } from '@/global/form-rules';
import { createAgentAccount, getAgentProviders, updateAgentAccount } from '@/api/modules/ai'; import { createAgentAccount, getAgentProviders, updateAgentAccount } from '@/api/modules/ai';
import i18n from '@/lang'; import i18n from '@/lang';
import { getAgentProviderDisplayName } from '@/utils/agent'; import { getAgentProviderDisplayName } from '@/utils/agent';
import { useGlobalStore } from '@/composables/useGlobalStore';
const emit = defineEmits(['search']); const emit = defineEmits(['search']);
@@ -85,6 +86,7 @@ const providerOptions = ref<Array<{ label: string; value: string }>>([]);
const providerBaseURL = ref<Record<string, string>>({}); const providerBaseURL = ref<Record<string, string>>({});
const loading = ref(false); const loading = ref(false);
const editableBaseURLProviders = ['ollama', 'custom', 'zai']; const editableBaseURLProviders = ['ollama', 'custom', 'zai'];
const { isIntl } = useGlobalStore();
const form = reactive({ const form = reactive({
id: 0, id: 0,
@@ -225,11 +227,13 @@ const openDrawer = async (params?: OpenParams) => {
const loadProviders = async () => { const loadProviders = async () => {
const res = await getAgentProviders(); const res = await getAgentProviders();
const data = res.data || []; const data = res.data || [];
providerOptions.value = data.map((item) => ({ const blockedProviders = new Set(['ark-coding-plan', 'bailian-coding-plan']);
const filteredData = isIntl.value ? data.filter((item) => !blockedProviders.has(item.provider)) : data;
providerOptions.value = filteredData.map((item) => ({
value: item.provider, value: item.provider,
label: getAgentProviderDisplayName(item.provider, item.displayName), label: getAgentProviderDisplayName(item.provider, item.displayName),
})); }));
providerBaseURL.value = data.reduce((acc, item) => { providerBaseURL.value = filteredData.reduce((acc, item) => {
acc[item.provider] = item.baseUrl || ''; acc[item.provider] = item.baseUrl || '';
return acc; return acc;
}, {} as Record<string, string>); }, {} as Record<string, string>);
@@ -63,12 +63,15 @@
:placeholder="$t('website.selectSSL')" :placeholder="$t('website.selectSSL')"
@change="changeSSl(req.sslID)" @change="changeSSl(req.sslID)"
> >
<el-option <el-option v-for="(ssl, index) in ssls" :key="index" :label="ssl.primaryDomain" :value="ssl.id">
v-for="(ssl, index) in ssls" <span>{{ ssl.primaryDomain }}</span>
:key="index" <el-tag class="tagClass" v-if="ssl.expireDate">
:label="ssl.primaryDomain" {{ dateFormatSimple(ssl.expireDate) }}
:value="ssl.id" </el-tag>
></el-option> <el-tag class="tagClass" v-if="ssl.organization">
{{ ssl.organization }}
</el-tag>
</el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-form> </el-form>
@@ -90,15 +93,17 @@ import { listSSL, searchAcmeAccount } from '@/api/modules/website';
import { Rules } from '@/global/form-rules'; import { Rules } from '@/global/form-rules';
import { FormInstance, FormRules } from 'element-plus'; import { FormInstance, FormRules } from 'element-plus';
import { reactive, ref } from 'vue'; import { reactive, ref } from 'vue';
import { getAccountName } from '@/utils/util'; import { getAccountName, dateFormatSimple } from '@/utils/util';
import { bindMcpDomain, getMcpDomain, updateMcpDomain } from '@/api/modules/ai'; import { bindMcpDomain, getMcpDomain, updateMcpDomain } from '@/api/modules/ai';
import { MsgSuccess } from '@/utils/message'; import { MsgSuccess } from '@/utils/message';
import i18n from '@/lang'; import i18n from '@/lang';
type SSLItem = Website.SSL & { organization?: string };
const open = ref(false); const open = ref(false);
const operate = ref('create'); const operate = ref('create');
const loading = ref(false); const loading = ref(false);
const ssls = ref([]); const ssls = ref<SSLItem[]>([]);
const websiteSSL = ref<Website.SSL>(); const websiteSSL = ref<Website.SSL>();
const acmeAccounts = ref(); const acmeAccounts = ref();
const formRef = ref(); const formRef = ref();
@@ -225,4 +230,10 @@ defineExpose({
font-size: 12px; font-size: 12px;
margin-left: 5px; margin-left: 5px;
} }
.tagClass {
float: right;
margin-right: 10px;
font-size: 12px;
margin-top: 5px;
}
</style> </style>
@@ -62,12 +62,15 @@
:placeholder="$t('website.selectSSL')" :placeholder="$t('website.selectSSL')"
@change="changeSSl(req.sslID)" @change="changeSSl(req.sslID)"
> >
<el-option <el-option v-for="(ssl, index) in ssls" :key="index" :label="ssl.primaryDomain" :value="ssl.id">
v-for="(ssl, index) in ssls" <span>{{ ssl.primaryDomain }}</span>
:key="index" <el-tag class="tagClass" v-if="ssl.expireDate">
:label="ssl.primaryDomain" {{ dateFormatSimple(ssl.expireDate) }}
:value="ssl.id" </el-tag>
></el-option> <el-tag class="tagClass" v-if="ssl.organization">
{{ ssl.organization }}
</el-tag>
</el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-alert :closable="false"> <el-alert :closable="false">
@@ -95,15 +98,17 @@ import { listSSL, searchAcmeAccount } from '@/api/modules/website';
import { Rules } from '@/global/form-rules'; import { Rules } from '@/global/form-rules';
import { FormInstance, FormRules } from 'element-plus'; import { FormInstance, FormRules } from 'element-plus';
import { reactive, ref } from 'vue'; import { reactive, ref } from 'vue';
import { getAccountName } from '@/utils/util'; import { getAccountName, dateFormatSimple } from '@/utils/util';
import { bindDomain, getBindDomain, updateBindDomain } from '@/api/modules/ai'; import { bindDomain, getBindDomain, updateBindDomain } from '@/api/modules/ai';
import { MsgSuccess } from '@/utils/message'; import { MsgSuccess } from '@/utils/message';
import i18n from '@/lang'; import i18n from '@/lang';
type SSLItem = Website.SSL & { organization?: string };
const open = ref(false); const open = ref(false);
const operate = ref('create'); const operate = ref('create');
const loading = ref(false); const loading = ref(false);
const ssls = ref([]); const ssls = ref<SSLItem[]>([]);
const websiteSSL = ref<Website.SSL>(); const websiteSSL = ref<Website.SSL>();
const acmeAccounts = ref(); const acmeAccounts = ref();
const formRef = ref(); const formRef = ref();
@@ -239,4 +244,10 @@ defineExpose({
font-size: 12px; font-size: 12px;
margin-left: 5px; margin-left: 5px;
} }
.tagClass {
float: right;
margin-right: 10px;
font-size: 12px;
margin-top: 5px;
}
</style> </style>
@@ -1,12 +1,5 @@
<template> <template>
<DrawerPro v-model="drawerVisible" :header="$t('setting.panelSSL')" @close="handleClose" size="large"> <DrawerPro v-model="drawerVisible" :header="$t('setting.panelSSL')" @close="handleClose" size="large">
<el-alert class="common-prompt" :closable="false" type="error">
<template #default>
<span>
<span>{{ $t('setting.panelSSLHelper') }}</span>
</span>
</template>
</el-alert>
<el-form ref="formRef" label-position="top" :model="form" :rules="rules" v-loading="loading"> <el-form ref="formRef" label-position="top" :model="form" :rules="rules" v-loading="loading">
<el-form-item :label="$t('setting.mode')" prop="ssl"> <el-form-item :label="$t('setting.mode')" prop="ssl">
<el-radio-group v-model="form.ssl"> <el-radio-group v-model="form.ssl">
@@ -67,7 +67,15 @@
:label="ssl.primaryDomain" :label="ssl.primaryDomain"
:value="ssl.id" :value="ssl.id"
:disabled="ssl.pem === ''" :disabled="ssl.pem === ''"
></el-option> >
<span>{{ ssl.primaryDomain }}</span>
<el-tag class="tagClass" v-if="ssl.expireDate">
{{ dateFormatSimple(ssl.expireDate) }}
</el-tag>
<el-tag class="tagClass" v-if="ssl.organization">
{{ ssl.organization }}
</el-tag>
</el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
</div> </div>
@@ -139,11 +147,13 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, watch, onMounted, ref } from 'vue'; import { computed, watch, onMounted, ref } from 'vue';
import { getAccountName } from '@/utils/util'; import { getAccountName, dateFormatSimple } from '@/utils/util';
import WebsiteSSL from '@/views/website/website/components/website-ssl/index.vue'; import WebsiteSSL from '@/views/website/website/components/website-ssl/index.vue';
import { Website } from '@/api/interface/website'; import { Website } from '@/api/interface/website';
import { listSSL, searchAcmeAccount } from '@/api/modules/website'; import { listSSL, searchAcmeAccount } from '@/api/modules/website';
type SSLItem = Website.SSL & { organization?: string };
interface AcmeAccount { interface AcmeAccount {
id: number; id: number;
email: string; email: string;
@@ -187,7 +197,7 @@ const props = withDefaults(defineProps<Props>(), {
const emit = defineEmits<Emits>(); const emit = defineEmits<Emits>();
const acmeAccounts = ref<AcmeAccount[]>([]); const acmeAccounts = ref<AcmeAccount[]>([]);
const ssls = ref<Website.SSL[]>([]); const ssls = ref<SSLItem[]>([]);
const keyFileRef = ref(); const keyFileRef = ref();
const certFileRef = ref(); const certFileRef = ref();
@@ -298,4 +308,10 @@ onMounted(() => {
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
font-size: 12px; font-size: 12px;
} }
.tagClass {
float: right;
margin-right: 10px;
font-size: 12px;
margin-top: 5px;
}
</style> </style>
@@ -255,7 +255,15 @@
:label="ssl.primaryDomain" :label="ssl.primaryDomain"
:value="ssl.id" :value="ssl.id"
:disabled="ssl.pem == ''" :disabled="ssl.pem == ''"
></el-option> >
<span>{{ ssl.primaryDomain }}</span>
<el-tag class="tagClass" v-if="ssl.expireDate">
{{ dateFormatSimple(ssl.expireDate) }}
</el-tag>
<el-tag class="tagClass" v-if="ssl.organization">
{{ ssl.organization }}
</el-tag>
</el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item :label="' '" v-if="websiteSSL && websiteSSL.id > 0"> <el-form-item :label="' '" v-if="websiteSSL && websiteSSL.id > 0">
@@ -443,7 +451,7 @@ import { reactive, ref, watch } from 'vue';
import { MsgError, MsgSuccess } from '@/utils/message'; import { MsgError, MsgSuccess } from '@/utils/message';
import { SearchRuntimes } from '@/api/modules/runtime'; import { SearchRuntimes } from '@/api/modules/runtime';
import { Runtime } from '@/api/interface/runtime'; import { Runtime } from '@/api/interface/runtime';
import { getRandomStr, getRuntimeLabel } from '@/utils/util'; import { getRandomStr, getRuntimeLabel, dateFormatSimple } from '@/utils/util';
import { getAppService } from '@/api/modules/app'; import { getAppService } from '@/api/modules/app';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { getAccountName } from '@/utils/util'; import { getAccountName } from '@/utils/util';
@@ -1098,4 +1106,10 @@ defineExpose({
white-space: nowrap; white-space: nowrap;
display: inline-block; display: inline-block;
} }
.tagClass {
float: right;
margin-right: 10px;
font-size: 12px;
margin-top: 5px;
}
</style> </style>