Compare commits

..
13 Commits
Author SHA1 Message Date
CityFun f9f9169a4d fix(build): upgrade Go to 1.27.3 (#14161) 2026-10-22 15:41:34 +08:00
zhengkunwang223 1e01761ba3 fix(appstore): refresh cached resources and PHP versions
Version official app package and compose downloads using the synchronized store revision. Refresh outdated local resource caches and use synchronized database parameters for official PHP version options.
2026-10-22 11:53:36 +08:00
CityFun 15283fde34 feat: update deepseek default model (#14146) 2026-10-21 18:37:22 +08:00
Octopusandocto-patch 49f8e7f040 feat(ai-agents): refresh MiniMax Flash models and defaults (#14136)
Co-authored-by: octo-patch <octo-patch@users.noreply.github.com>
2026-10-21 18:23:53 +08:00
Yuki Guo 98b27af6a0 fix: container list search (#14134) 2026-10-21 18:22:02 +08:00
ssongliu a393224729 feat(ai): support benchmark node options and report integration (#14145) 2026-10-21 18:07:39 +08:00
Woolsen b851c84290 fix(container): reset sort to default when cleared (#14140)
The container list opens with name ascending but shows no sort indicator. Clearing the name sort kept the previous descending order, so two no-indicator states represented different orders. Reset the request to the default name ascending when the sort is cleared.
2026-10-21 18:01:18 +08:00
Woolsen 372855157d feat(container): support sorting by creation time (#14141)
Add a creation time column to the container list. The column uses the backend createdAt order field and displays the existing createTime value. It follows the standard three-state sort cycle, starting with newest first.
2026-10-21 18:01:07 +08:00
CityFunandssongliu 577d771495 feat(agent): add DeepSeek Harness and OctoP support (#14138)
* feat(agent): add DeepSeek Harness and OctoP support

* fix: restore default core edition configuration

---------

Co-authored-by: ssongliu <sloooop1x@gmail.com>
2026-10-21 17:25:38 +08:00
Yuki Guo ca3e6511e5 feat: ssl push multiple directories (#14137) 2026-10-21 17:23:56 +08:00
Yuki Guo 8af14eae25 feat: add container catalog batch upload (#14133) 2026-10-21 17:09:01 +08:00
ssongliu 175fbaf3f2 fix: correct terminal node routing for containers (#14142) 2026-10-21 16:40:42 +08:00
Yuki Guo 13199efd62 feat: add overview more quick menu (#14132) 2026-10-21 16:21:07 +08:00
175 changed files with 5033 additions and 698 deletions
+16
View File
@@ -0,0 +1,16 @@
package v2
import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/service"
"github.com/gin-gonic/gin"
)
func (b *BaseApi) AIBenchmarkSources(c *gin.Context) {
sources, err := service.AIBenchmarkSources(c.Query("kind"), c.Query("selected"))
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, sources)
}
+84
View File
@@ -0,0 +1,84 @@
package v2
import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/gin-gonic/gin"
)
// @Tags Dashboard
// @Summary List quick directories
// @Success 200 {array} model.QuickDirectory
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /dashboard/quick/directory [get]
func (b *BaseApi) ListQuickDirectories(c *gin.Context) {
items, err := dashboardService.ListQuickDirectories()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, items)
}
// @Tags Dashboard
// @Summary Create quick directory
// @Accept json
// @Param request body dto.QuickDirectoryCreate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /dashboard/quick/directory [post]
func (b *BaseApi) CreateQuickDirectory(c *gin.Context) {
var req dto.QuickDirectoryCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
item, err := dashboardService.CreateQuickDirectory(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, item)
}
// @Tags Dashboard
// @Summary Update quick directory
// @Accept json
// @Param request body dto.QuickDirectoryUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /dashboard/quick/directory/update [post]
func (b *BaseApi) UpdateQuickDirectory(c *gin.Context) {
var req dto.QuickDirectoryUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
item, err := dashboardService.UpdateQuickDirectory(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, item)
}
// @Tags Dashboard
// @Summary Delete quick directory
// @Accept json
// @Param request body dto.QuickDirectoryDelete true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /dashboard/quick/directory/del [post]
func (b *BaseApi) DeleteQuickDirectory(c *gin.Context) {
var req dto.QuickDirectoryDelete
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := dashboardService.DeleteQuickDirectory(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
+3 -1
View File
@@ -9,7 +9,8 @@ type AgentCreateReq struct {
WebUIPort int `json:"webUIPort" validate:"required,min=1,max=65535"` WebUIPort int `json:"webUIPort" validate:"required,min=1,max=65535"`
BridgePort int `json:"bridgePort"` BridgePort int `json:"bridgePort"`
AllowedOrigins []string `json:"allowedOrigins"` AllowedOrigins []string `json:"allowedOrigins"`
AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent"` AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent deepseek-harness octop"`
AccessHost string `json:"accessHost"`
Model string `json:"model"` Model string `json:"model"`
AccountID uint `json:"accountId"` AccountID uint `json:"accountId"`
Token string `json:"token"` Token string `json:"token"`
@@ -110,6 +111,7 @@ type AgentBatchOperateResult struct {
} }
type AgentItem struct { type AgentItem struct {
AccessHost string `json:"accessHost"`
ID uint `json:"id"` ID uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Remark string `json:"remark"` Remark string `json:"remark"`
+13
View File
@@ -0,0 +1,13 @@
package dto
type AIBenchmarkSource struct {
ID string `json:"id"`
Kind string `json:"kind"`
Name string `json:"name"`
Status string `json:"status"`
BaseURL string `json:"baseUrl"`
Endpoint string `json:"endpoint"`
Models []string `json:"models"`
Tokenizer string `json:"tokenizer"`
Message string `json:"message"`
}
+21 -1
View File
@@ -31,7 +31,27 @@ type DashboardBase struct {
} }
type ChangeQuicks struct { type ChangeQuicks struct {
Quicks []QuickJump `json:"quicks"` Quicks []QuickVisibility `json:"quicks" validate:"required,dive"`
}
type QuickVisibility struct {
ID uint `json:"id" validate:"required"`
IsShow bool `json:"isShow"`
}
type QuickDirectoryCreate struct {
Path string `json:"path" validate:"required"`
Alias string `json:"alias"`
}
type QuickDirectoryDelete struct {
ID uint `json:"id" validate:"required"`
Legacy bool `json:"legacy"`
}
type QuickDirectoryUpdate struct {
QuickDirectoryDelete
QuickDirectoryCreate
} }
type QuickJump struct { type QuickJump struct {
+7
View File
@@ -5,6 +5,13 @@ type AppLauncher struct {
Key string `json:"key"` Key string `json:"key"`
} }
type QuickDirectory struct {
BaseModel
Path string `json:"path"`
Alias string `json:"alias"`
Legacy bool `json:"legacy" gorm:"-"`
}
type QuickJump struct { type QuickJump struct {
BaseModel BaseModel
Name string `json:"name"` Name string `json:"name"`
+2 -3
View File
@@ -66,7 +66,7 @@ var catalog = map[string]Meta{
{APIType: "openai-responses", BaseURL: "https://api.deepseek.com"}, {APIType: "openai-responses", BaseURL: "https://api.deepseek.com"},
anthropicAPIConfig("https://api.deepseek.com/anthropic", AuthModeXAPIKey), anthropicAPIConfig("https://api.deepseek.com/anthropic", AuthModeXAPIKey),
}, },
Models: []Model{{ID: "deepseek-v4-flash", Name: "deepseek-v4-flash"}, {ID: "deepseek-v4-pro", Name: "deepseek-v4-pro"}}, Models: []Model{{ID: "deepseek-flash", Name: "deepseek-flash"}, {ID: "deepseek-v4-pro", Name: "deepseek-v4-pro"}},
}, },
"bailian-coding-plan": { "bailian-coding-plan": {
Key: "bailian-coding-plan", DisplayNameKey: "AIProviderBailianCodingPlan", Sort: 30, DefaultAPIType: "openai-completions", EnvKey: "QWEN_API_KEY", Key: "bailian-coding-plan", DisplayNameKey: "AIProviderBailianCodingPlan", Sort: 30, DefaultAPIType: "openai-completions", EnvKey: "QWEN_API_KEY",
@@ -82,7 +82,6 @@ var catalog = map[string]Meta{
{ID: "kimi-k2.5", Name: "Kimi K2.5"}, {ID: "kimi-k2.5", Name: "Kimi K2.5"},
{ID: "qwen3.5-plus", Name: "Qwen3.5-Plus"}, {ID: "qwen3.5-plus", Name: "Qwen3.5-Plus"},
{ID: "glm-5", Name: "GLM-5"}, {ID: "glm-5", Name: "GLM-5"},
{ID: "MiniMax-M2.5", Name: "MiniMax M2.5"},
{ID: "qwen3.6-plus", Name: "Qwen3.6-Plus"}, {ID: "qwen3.6-plus", Name: "Qwen3.6-Plus"},
{ID: "qwen3.7-plus", Name: "Qwen3.7-Plus"}, {ID: "qwen3.7-plus", Name: "Qwen3.7-Plus"},
}, },
@@ -114,7 +113,7 @@ var catalog = map[string]Meta{
{APIType: "openai-completions", BaseURL: "https://api.minimaxi.com/v1"}, {APIType: "openai-completions", BaseURL: "https://api.minimaxi.com/v1"},
{APIType: "minimax-images", BaseURL: "https://api.minimaxi.com"}, {APIType: "minimax-images", BaseURL: "https://api.minimaxi.com"},
}, },
Models: []Model{{ID: "MiniMax-M3", Name: "MiniMax M3"}, {ID: "MiniMax-M2.7", Name: "MiniMax M2.7"}, {ID: "MiniMax-M2.7-highspeed", Name: "MiniMax M2.7 highspeed"}}, Models: []Model{{ID: "MiniMax-M3.1-Flash", Name: "MiniMax M3.1 Flash"}, {ID: "MiniMax-M3", Name: "MiniMax M3"}},
}, },
"xiaomi": { "xiaomi": {
Key: "xiaomi", DisplayName: "Xiaomi", Sort: 46, DefaultAPIType: "openai-completions", EnvKey: "XIAOMI_API_KEY", Key: "xiaomi", DisplayName: "Xiaomi", Sort: 46, DefaultAPIType: "openai-completions", EnvKey: "XIAOMI_API_KEY",
+100
View File
@@ -0,0 +1,100 @@
package provider
import (
"encoding/json"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
)
func TestMiniMaxCatalog(t *testing.T) {
meta, ok := Get("minimax")
if !ok || !reflect.DeepEqual(meta.Models, []Model{{ID: "MiniMax-M3.1-Flash", Name: "MiniMax M3.1 Flash"}, {ID: "MiniMax-M3", Name: "MiniMax M3"}}) {
t.Fatalf("unexpected MiniMax models: %+v", meta.Models)
}
if meta.DefaultAPIType != "anthropic-messages" {
t.Fatalf("changed default protocol: %s", meta.DefaultAPIType)
}
for _, provider := range All() {
for _, model := range provider.Models {
if strings.Contains(strings.ToLower(model.ID), "minimax") && model.ID != "MiniMax-M3.1-Flash" && model.ID != "MiniMax-M3" {
t.Errorf("legacy suggestion in %s: %s", provider.Key, model.ID)
}
}
}
}
func TestMiniMaxVerificationRequests(t *testing.T) {
for _, region := range []string{"https://api.minimaxi.com", "https://api.minimax.io"} {
for _, protocol := range []string{"anthropic-messages", "openai-completions"} {
for _, auth := range []string{AuthModeXAPIKey, AuthModeBearer} {
for _, model := range []string{"MiniMax-M3.1-Flash", "MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.7-highspeed", "custom-model"} {
t.Run(region+"/"+protocol+"/"+auth+"/"+model, func(t *testing.T) {
base, endpoint := region+"/v1", "/chat/completions"
if protocol == "anthropic-messages" {
base, endpoint = region+"/anthropic", "/v1/messages"
}
built := BuildVerifyRequest("minimax", protocol, auth, base, "test-key", model)
if built.URL != base+endpoint {
t.Fatalf("URL = %s", built.URL)
}
var captured map[string]interface{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != strings.TrimPrefix(base, region)+endpoint {
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
}
if protocol == "anthropic-messages" && auth == AuthModeXAPIKey {
if r.Header.Get("x-api-key") != "test-key" || r.Header.Get("Authorization") != "" {
t.Error("incorrect API key authentication")
}
} else if r.Header.Get("Authorization") != "Bearer test-key" {
t.Error("incorrect bearer authentication")
}
if err := json.NewDecoder(r.Body).Decode(&captured); err != nil {
t.Error(err)
}
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
if err := VerifyAccount("minimax", protocol, auth, server.URL+strings.TrimPrefix(base, region), "test-key", model); err != nil {
t.Fatal(err)
}
if captured["model"] != model || captured["stream"] != false || captured["max_tokens"] != float64(defaultVerifyMaxTokens) {
t.Fatalf("unexpected serialized body: %#v", captured)
}
if model == "MiniMax-M3.1-Flash" {
if protocol == "anthropic-messages" {
if !reflect.DeepEqual(captured["thinking"], map[string]interface{}{"type": "adaptive"}) || !reflect.DeepEqual(captured["output_config"], map[string]interface{}{"effort": "max"}) || captured["reasoning_effort"] != nil {
t.Fatalf("invalid Anthropic Flash reasoning: %#v", captured)
}
} else if captured["reasoning_effort"] != "max" || captured["thinking"] != nil || captured["output_config"] != nil {
t.Fatalf("invalid OpenAI Flash reasoning: %#v", captured)
}
} else if captured["reasoning_effort"] != nil || captured["thinking"] != nil || captured["output_config"] != nil {
t.Fatalf("changed legacy/custom reasoning: %#v", captured)
}
})
}
}
}
}
}
func TestMiniMaxExplicitGatewayAndCustomModels(t *testing.T) {
for _, provider := range []string{"bailian-coding-plan", "custom", "openrouter"} {
for _, protocol := range []string{"openai-completions", "anthropic-messages"} {
for _, model := range []string{"MiniMax-M2.5", "MiniMax-M3.1-Flash", "my-custom-model"} {
request := BuildVerifyRequest(provider, protocol, AuthModeBearer, "https://example.test/v1", "test-key", model)
var body map[string]interface{}
if err := json.Unmarshal(request.Body, &body); err != nil {
t.Fatal(err)
}
if body["model"] != model || body["reasoning_effort"] != nil || body["output_config"] != nil || body["thinking"] != nil {
t.Fatalf("changed explicit %s request: %#v", provider, body)
}
}
}
}
}
+13 -4
View File
@@ -118,10 +118,15 @@ func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model stri
headers["x-api-key"] = apiKey headers["x-api-key"] = apiKey
} }
headers["anthropic-version"] = "2023-06-01" headers["anthropic-version"] = "2023-06-01"
request.Body = mustJSON(map[string]interface{}{ body := map[string]interface{}{
"model": model, "max_tokens": defaultVerifyMaxTokens, "stream": false, "model": model, "max_tokens": defaultVerifyMaxTokens, "stream": false,
"messages": []map[string]interface{}{{"role": "user", "content": []map[string]string{{"type": "text", "text": "test"}}}}, "messages": []map[string]interface{}{{"role": "user", "content": []map[string]string{{"type": "text", "text": "test"}}}},
}) }
if provider == "minimax" && model == "MiniMax-M3.1-Flash" {
body["thinking"] = map[string]string{"type": "adaptive"}
body["output_config"] = map[string]string{"effort": "max"}
}
request.Body = mustJSON(body)
case "openai-responses": case "openai-responses":
request.URL = baseURL + "/responses" request.URL = baseURL + "/responses"
headers["Authorization"] = "Bearer " + apiKey headers["Authorization"] = "Bearer " + apiKey
@@ -131,9 +136,13 @@ func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model stri
if (provider != "ollama" && provider != "llmman") || strings.TrimSpace(apiKey) != "" { if (provider != "ollama" && provider != "llmman") || strings.TrimSpace(apiKey) != "" {
headers["Authorization"] = "Bearer " + apiKey headers["Authorization"] = "Bearer " + apiKey
} }
request.Body = mustJSON(map[string]interface{}{ body := map[string]interface{}{
"model": model, "messages": []map[string]string{{"role": "user", "content": "test"}}, "max_tokens": defaultVerifyMaxTokens, "stream": false, "model": model, "messages": []map[string]string{{"role": "user", "content": "test"}}, "max_tokens": defaultVerifyMaxTokens, "stream": false,
}) }
if provider == "minimax" && model == "MiniMax-M3.1-Flash" {
body["reasoning_effort"] = "max"
}
request.Body = mustJSON(body)
} }
return request return request
} }
+69 -3
View File
@@ -3,6 +3,7 @@ package repo
import ( import (
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"gorm.io/gorm"
) )
type LauncherRepo struct{} type LauncherRepo struct{}
@@ -17,6 +18,11 @@ type ILauncherRepo interface {
GetQuickJump(opts ...DBOption) (model.QuickJump, error) GetQuickJump(opts ...DBOption) (model.QuickJump, error)
ListQuickJump(withAll bool) []model.QuickJump ListQuickJump(withAll bool) []model.QuickJump
UpdateQuicks(quicks []model.QuickJump) error UpdateQuicks(quicks []model.QuickJump) error
ListQuickDirectories() ([]model.QuickDirectory, error)
CountQuickDirectories() (int64, error)
CreateQuickDirectory(item *model.QuickDirectory) error
UpdateQuickDirectory(item *model.QuickDirectory) error
DeleteQuickDirectory(id uint, legacy bool) error
} }
func NewILauncherRepo() ILauncherRepo { func NewILauncherRepo() ILauncherRepo {
@@ -95,13 +101,73 @@ func (u *LauncherRepo) UpdateQuicks(quicks []model.QuickJump) error {
for _, item := range quicks { for _, item := range quicks {
if err := tx.Model(&model.QuickJump{}).Where("id = ?", item.ID).Updates(map[string]interface{}{ if err := tx.Model(&model.QuickJump{}).Where("id = ?", item.ID).Updates(map[string]interface{}{
"is_show": item.IsShow, "is_show": item.IsShow,
"detail": item.Detail,
"alias": item.Alias,
}).Error; err != nil { }).Error; err != nil {
tx.Rollback() tx.Rollback()
return err return err
} }
} }
tx.Commit() return tx.Commit().Error
}
// Legacy entries remain in the overview configuration until explicitly deleted.
func legacyQuickDirectories(db *gorm.DB) *gorm.DB {
return db.Model(&model.QuickJump{}).Where("name = ? AND is_show = ? AND detail <> ?", "File", true, "")
}
func (u *LauncherRepo) ListQuickDirectories() ([]model.QuickDirectory, error) {
items := make([]model.QuickDirectory, 0)
if err := global.DB.Order("id ASC").Find(&items).Error; err != nil {
return nil, err
}
var quicks []model.QuickJump
if err := legacyQuickDirectories(global.DB).Order("id ASC").Find(&quicks).Error; err != nil {
return nil, err
}
for _, quick := range quicks {
items = append(items, model.QuickDirectory{BaseModel: quick.BaseModel, Path: quick.Detail, Alias: quick.Alias, Legacy: true})
}
return items, nil
}
func (u *LauncherRepo) CountQuickDirectories() (int64, error) {
var count, legacyCount int64
if err := global.DB.Model(&model.QuickDirectory{}).Count(&count).Error; err != nil {
return 0, err
}
if err := legacyQuickDirectories(global.DB).Count(&legacyCount).Error; err != nil {
return 0, err
}
return count + legacyCount, nil
}
func (u *LauncherRepo) CreateQuickDirectory(item *model.QuickDirectory) error {
return global.DB.Create(item).Error
}
func (u *LauncherRepo) UpdateQuickDirectory(item *model.QuickDirectory) error {
var result *gorm.DB
if item.Legacy {
result = legacyQuickDirectories(global.DB).Where("id = ?", item.ID).Updates(map[string]interface{}{"detail": item.Path, "alias": item.Alias})
} else {
result = global.DB.Model(&model.QuickDirectory{}).Where("id = ?", item.ID).Updates(map[string]interface{}{"path": item.Path, "alias": item.Alias})
}
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
func (u *LauncherRepo) DeleteQuickDirectory(id uint, legacy bool) error {
var result *gorm.DB
if legacy {
result = legacyQuickDirectories(global.DB).Where("id = ?", id).Updates(map[string]interface{}{"detail": nil, "alias": nil})
} else {
result = global.DB.Delete(&model.QuickDirectory{}, id)
}
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil return nil
} }
+12 -1
View File
@@ -142,6 +142,9 @@ const (
func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) { func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
agentType := req.AgentType agentType := req.AgentType
if err := validateNativeAgentCreate(req); err != nil {
return nil, err
}
if err := checkPortExist(req.WebUIPort); err != nil { if err := checkPortExist(req.WebUIPort); err != nil {
return nil, err return nil, err
} }
@@ -237,6 +240,8 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
} }
} else if agentType == constant.AppCopaw { } else if agentType == constant.AppCopaw {
dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword) dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
} else if agentType == constant.AppDeepSeekHarness || agentType == constant.AppOctop {
dashboardAuth = agentDashboardAuth{Username: req.DashboardUsername, Password: req.DashboardPassword}
} }
params := map[string]interface{}{ params := map[string]interface{}{
@@ -245,6 +250,12 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
constant.HostIP: "", constant.HostIP: "",
} }
setAgentWebUIParams(params, agentType, detail.Version, req.WebUIPort) setAgentWebUIParams(params, agentType, detail.Version, req.WebUIPort)
if agentType == constant.AppDeepSeekHarness {
params["HTTPS_ACCESS_HOST"] = req.AccessHost
}
if agentType == constant.AppOctop {
params["OCTOP_LOG_LEVEL"] = "info"
}
if agentType == constant.AppOpenclaw { if agentType == constant.AppOpenclaw {
if allowedOrigin := firstAllowedOrigin(allowedOrigins); allowedOrigin != "" { if allowedOrigin := firstAllowedOrigin(allowedOrigins); allowedOrigin != "" {
params["ALLOWED_ORIGIN"] = allowedOrigin params["ALLOWED_ORIGIN"] = allowedOrigin
@@ -738,7 +749,7 @@ func requiresAgentAccount(agentType string) bool {
} }
func setAgentWebUIParams(params map[string]interface{}, agentType, appVersion string, webUIPort int) { func setAgentWebUIParams(params map[string]interface{}, agentType, appVersion string, webUIPort int) {
if agentType == constant.AppOpenclaw && isOpenclawHTTPSWindowVersion(appVersion) { if agentType == constant.AppDeepSeekHarness || agentType == constant.AppOpenclaw && isOpenclawHTTPSWindowVersion(appVersion) {
params["PANEL_APP_PORT_HTTPS"] = webUIPort params["PANEL_APP_PORT_HTTPS"] = webUIPort
return return
} }
+115
View File
@@ -0,0 +1,115 @@
package service
import (
"encoding/json"
"path/filepath"
"reflect"
"testing"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
providercatalog "github.com/1Panel-dev/1Panel/agent/app/provider"
)
func TestMiniMaxOpenclawModelMetadata(t *testing.T) {
for _, provider := range []string{"minimax", "custom", "bailian-coding-plan"} {
for _, protocol := range []string{"anthropic-messages", "openai-completions"} {
for _, id := range []string{"MiniMax-M3.1-Flash", "MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.5", "saved-custom"} {
t.Run(provider+"/"+protocol+"/"+id, func(t *testing.T) {
account := &model.AgentAccount{Provider: provider, APIType: protocol, BaseURL: "https://example.test/v1", APIKey: "test-key"}
_, entry, _, _, err := buildOpenclawAccountModelConfig(account, dto.AgentAccountModel{ID: id, Name: "Saved name"})
if err != nil {
t.Fatal(err)
}
payload, err := json.Marshal(entry)
if err != nil {
t.Fatal(err)
}
var actual modelEntry
if err := json.Unmarshal(payload, &actual); err != nil {
t.Fatal(err)
}
if actual.ID != id || actual.Name != "Saved name" {
t.Fatalf("changed saved identity: %s", payload)
}
if provider == "minimax" && id == "MiniMax-M3.1-Flash" {
if actual.ContextWindow != 1000000 || actual.MaxTokens != 524288 || !reflect.DeepEqual(actual.Input, []string{"text", "image"}) {
t.Fatalf("incorrect direct Flash metadata: %s", payload)
}
} else if actual.ContextWindow != 0 || actual.MaxTokens != 0 {
t.Fatalf("changed existing/gateway limits: %s", payload)
}
})
}
}
}
}
func TestMiniMaxPreservesSavedOpenclawMetadata(t *testing.T) {
current := &modelsConfig{Mode: "merge", Providers: map[string]modelProvider{
"minimax": {Models: []modelEntry{{ID: "MiniMax-M3.1-Flash", Input: []string{"text"}, ContextWindow: 32000, MaxTokens: 4096}}},
}}
conf := map[string]interface{}{}
if err := applyOpenclawModelsConfig(conf, current); err != nil {
t.Fatal(err)
}
next := &modelsConfig{Mode: "merge", Providers: map[string]modelProvider{
"minimax": {Models: []modelEntry{{ID: "MiniMax-M3.1-Flash", Input: []string{"text", "image"}, ContextWindow: 1000000, MaxTokens: 524288}}},
}}
preserveOpenclawModelMetadata(conf, next)
if !reflect.DeepEqual(next.Providers["minimax"].Models, current.Providers["minimax"].Models) {
t.Fatalf("saved metadata changed: %+v", next)
}
}
func TestMiniMaxDefaultAccountRequest(t *testing.T) {
for _, protocol := range []string{"anthropic-messages", "openai-completions"} {
account := &model.AgentAccount{Provider: "minimax", APIType: protocol}
models, err := buildInitialAgentAccountModels(account, nil)
if err != nil {
t.Fatal(err)
}
selected, err := resolveAgentAccountVerifyModel("minimax", "", models)
if err != nil || selected != "MiniMax-M3.1-Flash" || len(models) != 2 || models[1].ID != "MiniMax-M3" {
t.Fatalf("unexpected default: %s, %+v, %v", selected, models, err)
}
request := providercatalog.BuildVerifyRequest("minimax", protocol, "", "https://example.test", "test-key", selected)
var body map[string]interface{}
if err := json.Unmarshal(request.Body, &body); err != nil || body["model"] != selected {
t.Fatalf("default request: %s, %v", request.Body, err)
}
}
}
func TestMiniMaxHermesConfiguration(t *testing.T) {
for _, protocol := range []string{"anthropic-messages", "openai-completions"} {
for _, id := range []string{"MiniMax-M3.1-Flash", "MiniMax-M3", "MiniMax-M2.7", "my-custom-model"} {
dir := t.TempDir()
account := &model.AgentAccount{Provider: "minimax", APIType: protocol, BaseURL: "https://api.minimaxi.com/anthropic", APIKey: "test-key"}
if err := writeHermesConfig(dir, account, id, ""); err != nil {
t.Fatal(err)
}
conf, err := readHermesConfig(filepath.Join(dir, "config.yaml"))
if err != nil {
t.Fatal(err)
}
if conf.Model.Default != id || conf.Model.Provider != "minimax-cn" || conf.Model.BaseURL != account.BaseURL {
t.Fatalf("changed Hermes selection: %+v", conf.Model)
}
}
}
}
func TestMiniMaxExplicitAccountModels(t *testing.T) {
for _, provider := range []string{"minimax", "bailian-coding-plan", "custom"} {
account := &model.AgentAccount{Provider: provider, APIType: "openai-completions"}
requested := []dto.AgentAccountModel{{ID: "MiniMax-M2.5", Name: "Saved legacy"}, {ID: "my-custom-model", Name: "Saved custom"}}
actual, err := buildInitialAgentAccountModels(account, requested)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(actual, requested) {
t.Fatalf("changed explicit %s models: %+v", provider, actual)
}
}
}
+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
}
+21 -2
View File
@@ -415,6 +415,15 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
} else { } else {
item.WebUIPort = appInstall.HttpPort item.WebUIPort = appInstall.HttpPort
} }
if agentType == constant.AppDeepSeekHarness {
item.WebUIPort = appInstall.HttpsPort
if values, err := readAgentEnvMap(appInstall.GetEnvPath()); err == nil {
accessHost := values["HTTPS_ACCESS_HOST"]
if isDeepSeekAccessHost(accessHost) {
item.AccessHost = accessHost
}
}
}
item.Path = appInstall.GetPath() item.Path = appInstall.GetPath()
item.Status = appInstall.Status item.Status = appInstall.Status
item.Message = appInstall.Message item.Message = appInstall.Message
@@ -442,7 +451,8 @@ func localizedAgentProviderName(provider string) string {
} }
func isAgentAppKey(appKey string) bool { func isAgentAppKey(appKey string) bool {
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent ||
appKey == constant.AppDeepSeekHarness || appKey == constant.AppOctop
} }
func isOpenclawLegacyHTTPVersion(version string) bool { func isOpenclawLegacyHTTPVersion(version string) bool {
@@ -1253,7 +1263,12 @@ func buildOpenclawAccountModelConfig(account *model.AgentAccount, model dto.Agen
if err != nil { if err != nil {
return "", modelEntry{}, "", modelProvider{}, err return "", modelEntry{}, "", modelProvider{}, err
} }
return providerPatch.PrimaryModel, buildOpenclawModelEntry(providerPatch.ModelID, model), providerPatch.ProviderKey, modelProvider{ entry := buildOpenclawModelEntry(providerPatch.ModelID, model)
if account.Provider == "minimax" && providerPatch.ModelID == "MiniMax-M3.1-Flash" {
entry.ContextWindow = 1000000
entry.MaxTokens = 524288
}
return providerPatch.PrimaryModel, entry, providerPatch.ProviderKey, modelProvider{
ApiKey: providerPatch.APIKey, ApiKey: providerPatch.APIKey,
BaseUrl: providerPatch.BaseURL, BaseUrl: providerPatch.BaseURL,
Api: providerPatch.APIType, Api: providerPatch.APIType,
@@ -1641,6 +1656,10 @@ func agentDashboardAuthEnvKeys(agentType string) (string, string, bool) {
return hermesDashboardUsernameEnvKey, hermesDashboardPasswordEnvKey, true return hermesDashboardUsernameEnvKey, hermesDashboardPasswordEnvKey, true
case constant.AppCopaw: case constant.AppCopaw:
return qwenPawAuthUsernameEnvKey, qwenPawAuthPasswordEnvKey, true return qwenPawAuthUsernameEnvKey, qwenPawAuthPasswordEnvKey, true
case constant.AppDeepSeekHarness:
return "DSH_AUTH_USERNAME", "DSH_AUTH_PASSWORD", true
case constant.AppOctop:
return "OCTOP_ADMIN_USERNAME", "OCTOP_DEFAULT_PASSWORD", true
default: default:
return "", "", false return "", "", false
} }
+209
View File
@@ -0,0 +1,209 @@
package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/mattn/go-shellwords"
)
func AIBenchmarkSources(kind, selected string) ([]dto.AIBenchmarkSource, error) {
if kind != "vllm" && kind != "ollama" {
return nil, errors.New("unsupported benchmark source")
}
selectedID := strings.SplitN(selected, ":", 2)[0]
type instance struct {
id uint
name, container string
port int
}
instances := []instance{}
apps, err := appRepo.GetBy(appRepo.WithKey(kind))
if err != nil {
return nil, err
}
for _, app := range apps {
installs, err := appInstallRepo.ListBy(context.Background(), appInstallRepo.WithAppId(app.ID))
if err != nil {
return nil, err
}
for _, install := range installs {
if selected != "" && strconv.FormatUint(uint64(install.ID), 10) != selectedID {
continue
}
instances = append(instances, instance{id: install.ID, name: install.Name, container: install.ContainerName, port: install.HttpPort})
}
}
results := []dto.AIBenchmarkSource{}
if len(instances) == 0 {
return results, nil
}
client, err := docker.NewDockerClient()
if err != nil {
return nil, err
}
defer client.Close()
if !strings.HasPrefix(client.DaemonHost(), "unix://") {
return nil, errors.New("benchmark shortcuts require a Docker daemon on the main node; enter the remote service address manually")
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
for _, instance := range instances {
base := dto.AIBenchmarkSource{Kind: kind, Name: instance.name, Endpoint: "/v1/chat/completions", Models: []string{}, Status: "unknown"}
inspected, err := client.ContainerInspect(ctx, instance.container)
if err != nil {
base.ID = strconv.FormatUint(uint64(instance.id), 10)
base.Message = "container inspection unavailable"
results = append(results, base)
continue
}
if inspected.State != nil {
base.Status = inspected.State.Status
}
args := []string{}
if inspected.Config != nil {
args = append(args, inspected.Config.Entrypoint...)
args = append(args, inspected.Config.Cmd...)
}
if len(args) >= 3 && (args[0] == "sh" || args[0] == "bash" || strings.HasSuffix(args[0], "/sh") || strings.HasSuffix(args[0], "/bash")) && args[1] == "-c" {
parser := shellwords.NewParser()
parser.ParseEnv = false
parser.ParseBacktick = false
if parsed, err := parser.Parse(args[2]); err == nil {
args = parsed
}
}
flags := map[string]string{}
for i, arg := range args {
if key, value, ok := strings.Cut(arg, "="); ok && strings.HasPrefix(key, "--") {
flags[key] = value
} else if strings.HasPrefix(arg, "--") && i+1 < len(args) && !strings.HasPrefix(args[i+1], "--") {
flags[arg] = args[i+1]
}
}
if name := flags["--served-model-name"]; name != "" {
base.Models = []string{name}
}
tokenizer := flags["--tokenizer"]
if tokenizer == "" {
tokenizer = flags["--model"]
for i, arg := range args {
if tokenizer == "" && arg == "serve" && i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
tokenizer = args[i+1]
}
}
}
if kind != "ollama" && tokenizer != "" {
if !filepath.IsAbs(tokenizer) {
base.Tokenizer = tokenizer
} else {
longest := 0
for _, mount := range inspected.Mounts {
relative, err := filepath.Rel(mount.Destination, tokenizer)
if err == nil && relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator)) && len(mount.Destination) > longest {
candidate := filepath.Join(mount.Source, relative)
if info, err := os.Stat(candidate); err == nil && info.IsDir() {
base.Tokenizer = candidate
longest = len(mount.Destination)
}
}
}
}
}
ports := map[int]string{}
if instance.port > 0 {
ports[instance.port] = "127.0.0.1"
}
if inspected.NetworkSettings != nil {
for _, bindings := range inspected.NetworkSettings.Ports {
for _, binding := range bindings {
port, _ := strconv.Atoi(binding.HostPort)
if port > 0 {
host := binding.HostIP
if host == "" || host == "0.0.0.0" || host == "::" {
host = "127.0.0.1"
}
ports[port] = host
}
}
}
}
if len(ports) == 0 && inspected.HostConfig != nil && inspected.HostConfig.NetworkMode == "host" {
port, _ := strconv.Atoi(flags["--port"])
if port > 0 {
ports[port] = "127.0.0.1"
}
}
ordered := []int{}
for port := range ports {
ordered = append(ordered, port)
}
sort.Ints(ordered)
if len(ordered) == 0 {
ordered = append(ordered, 0)
}
for _, port := range ordered {
source := base
source.ID = fmt.Sprintf("%d:%d", instance.id, port)
if selected != "" && source.ID != selected {
continue
}
if port > 0 {
source.BaseURL = "http://" + net.JoinHostPort(ports[port], strconv.Itoa(port))
}
if selected == source.ID && source.BaseURL != "" && source.Status == "running" {
endpoint := "/v1/models"
if kind == "ollama" {
endpoint = "/api/tags"
}
request, err := http.NewRequestWithContext(ctx, http.MethodGet, source.BaseURL+endpoint, nil)
if err != nil {
return nil, err
}
response, err := (&http.Client{Timeout: 3 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}).Do(request)
if err != nil {
source.Message = "model list unavailable; enter the served model name"
} else {
raw, readErr := io.ReadAll(io.LimitReader(response.Body, 2*1024*1024))
response.Body.Close()
if response.StatusCode == http.StatusOK && readErr == nil {
var data struct {
Data []struct {
ID string `json:"id"`
} `json:"data"`
Models []struct {
Name string `json:"name"`
} `json:"models"`
}
if err := json.Unmarshal(raw, &data); err == nil {
source.Models = []string{}
for _, model := range data.Data {
source.Models = append(source.Models, model.ID)
}
for _, model := range data.Models {
source.Models = append(source.Models, model.Name)
}
}
} else {
source.Message = fmt.Sprintf("model list HTTP %d; provide model name and credentials if required", response.StatusCode)
}
}
}
results = append(results, source)
}
}
return results, nil
}
+36 -15
View File
@@ -258,26 +258,35 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
fileOp := files.NewFileOp() fileOp := files.NewFileOp()
versionPath := filepath.Join(app.GetAppResourcePath(), detail.Version) versionPath := filepath.Join(app.GetAppResourcePath(), detail.Version)
if !fileOp.Stat(versionPath) || detail.Update { if err = downloadApp(app, detail, nil, nil); err != nil && !fileOp.Stat(versionPath) {
if err = downloadApp(app, detail, nil, nil); err != nil && !fileOp.Stat(versionPath) { return appDetailDTO, err
return appDetailDTO, err }
} if err == nil && app.Resource == constant.AppResourceRemote {
detail.Update = false
appDetailDTO.Update = false
} }
switch app.Type { switch app.Type {
case constant.RuntimePHP: case constant.RuntimePHP:
paramsPath := filepath.Join(versionPath, "data.yml")
if !fileOp.Stat(paramsPath) {
return appDetailDTO, buserr.WithDetail("ErrFileNotExist", paramsPath, nil)
}
param, err := fileOp.GetContent(paramsPath)
if err != nil {
return appDetailDTO, err
}
paramMap := make(map[string]interface{}) paramMap := make(map[string]interface{})
if err = yaml.Unmarshal(param, &paramMap); err != nil { if app.Resource == constant.AppResourceRemote {
return appDetailDTO, err if err = json.Unmarshal([]byte(detail.Params), &paramMap); err != nil {
return appDetailDTO, err
}
appDetailDTO.Params = paramMap
} else {
paramsPath := filepath.Join(versionPath, "data.yml")
if !fileOp.Stat(paramsPath) {
return appDetailDTO, buserr.WithDetail("ErrFileNotExist", paramsPath, nil)
}
param, err := fileOp.GetContent(paramsPath)
if err != nil {
return appDetailDTO, err
}
if err = yaml.Unmarshal(param, &paramMap); err != nil {
return appDetailDTO, err
}
appDetailDTO.Params = paramMap["additionalProperties"]
} }
appDetailDTO.Params = paramMap["additionalProperties"]
composePath := filepath.Join(versionPath, "docker-compose.yml") composePath := filepath.Join(versionPath, "docker-compose.yml")
if !fileOp.Stat(composePath) { if !fileOp.Stat(composePath) {
return appDetailDTO, buserr.WithDetail("ErrFileNotExist", composePath, nil) return appDetailDTO, buserr.WithDetail("ErrFileNotExist", composePath, nil)
@@ -308,6 +317,12 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
if appDetailDTO.DockerCompose == "" { if appDetailDTO.DockerCompose == "" {
filename := filepath.Base(appDetailDTO.DownloadUrl) filename := filepath.Base(appDetailDTO.DownloadUrl)
dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(appDetailDTO.DownloadUrl, filename), "docker-compose.yml") dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(appDetailDTO.DownloadUrl, filename), "docker-compose.yml")
if app.Resource == constant.AppResourceRemote {
dockerComposeUrl, err = currentAppDownloadURL(dockerComposeUrl)
if err != nil {
return appDetailDTO, err
}
}
statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s) statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
if statusCode == http.StatusNotFound { if statusCode == http.StatusNotFound {
return appDetailDTO, buserr.New("ErrAppVersionUnavailable") return appDetailDTO, buserr.New("ErrAppVersionUnavailable")
@@ -434,6 +449,12 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
} else { } else {
if appDetail.DockerCompose == "" { if appDetail.DockerCompose == "" {
dockerComposeUrl := fmt.Sprintf("%s/%s/1panel/%s/%s/docker-compose.yml", global.AppRepoURL(), global.CONF.Base.Mode, app.Key, appDetail.Version) dockerComposeUrl := fmt.Sprintf("%s/%s/1panel/%s/%s/docker-compose.yml", global.AppRepoURL(), global.CONF.Base.Mode, app.Key, appDetail.Version)
if app.Resource == constant.AppResourceRemote {
dockerComposeUrl, err = currentAppDownloadURL(dockerComposeUrl)
if err != nil {
return
}
}
var statusCode int var statusCode int
statusCode, composeRes, err = req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s) statusCode, composeRes, err = req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
if statusCode == http.StatusNotFound { if statusCode == http.StatusNotFound {
+6
View File
@@ -707,6 +707,12 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
if req.UpdateVersion != "" && req.UpdateVersion == detail.Version && detail.DockerCompose == "" && !app.IsLocalApp() { if req.UpdateVersion != "" && req.UpdateVersion == detail.Version && detail.DockerCompose == "" && !app.IsLocalApp() {
filename := filepath.Base(detail.DownloadUrl) filename := filepath.Base(detail.DownloadUrl)
dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(detail.DownloadUrl, filename), "docker-compose.yml") dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(detail.DownloadUrl, filename), "docker-compose.yml")
if app.Resource == constant.AppResourceRemote {
dockerComposeUrl, err = currentAppDownloadURL(dockerComposeUrl)
if err != nil {
return versions, err
}
}
statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s) statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
if statusCode == http.StatusNotFound { if statusCode == http.StatusNotFound {
return versions, buserr.New("ErrAppVersionUnavailable") return versions, buserr.New("ErrAppVersionUnavailable")
+5 -1
View File
@@ -191,7 +191,11 @@ func (c *appSyncContext) processOneApp(item appWorkItem) appWorkResult {
if _, ok := InitTypes[app.Type]; ok { if _, ok := InitTypes[app.Type]; ok {
dockerComposeUrl := fmt.Sprintf("%s/%s", versionUrl, "docker-compose.yml") dockerComposeUrl := fmt.Sprintf("%s/%s", versionUrl, "docker-compose.yml")
_, composeRes, err := req_helper.HandleRequestWithClient(&c.httpClient, dockerComposeUrl, http.MethodGet, constant.TimeOut20s) dockerComposeUrl, err := appDownloadURL(dockerComposeUrl, strconv.Itoa(c.list.LastModified))
var composeRes []byte
if err == nil {
_, composeRes, err = req_helper.HandleRequestWithClient(&c.httpClient, dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
}
if err == nil { if err == nil {
detail.DockerCompose = string(composeRes) detail.DockerCompose = string(composeRes)
} else { } else {
+44 -13
View File
@@ -8,6 +8,7 @@ import (
"maps" "maps"
"math" "math"
"net/http" "net/http"
"net/url"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
@@ -891,16 +892,52 @@ func handleMap(params map[string]interface{}, envParams map[string]string) {
} }
} }
func appDownloadURL(rawURL, version string) (string, error) {
parsedURL, err := url.Parse(rawURL)
if err != nil {
return "", err
}
query := parsedURL.Query()
query.Set("v", version)
parsedURL.RawQuery = query.Encode()
return parsedURL.String(), nil
}
func currentAppDownloadURL(rawURL string) (string, error) {
storeVersion, err := settingRepo.GetValueByKey("AppStoreLastModified")
if err != nil {
return "", err
}
return appDownloadURL(rawURL, storeVersion)
}
func downloadApp(app model.App, appDetail model.AppDetail, appInstall *model.AppInstall, logger *logrus.Logger) (err error) { func downloadApp(app model.App, appDetail model.AppDetail, appInstall *model.AppInstall, logger *logrus.Logger) (err error) {
if app.IsLocalApp() || app.IsCustomApp() { if app.IsLocalApp() || app.IsCustomApp() {
return nil return nil
} }
defer func() {
if err != nil && appInstall != nil {
appInstall.Status = constant.StatusDownloadErr
appInstall.Message = err.Error()
}
}()
appResourceDir := path.Join(global.Dir.AppResourceDir, app.Resource) appResourceDir := path.Join(global.Dir.AppResourceDir, app.Resource)
appDownloadDir := app.GetAppResourcePath() appDownloadDir := app.GetAppResourcePath()
appVersionDir := path.Join(appDownloadDir, appDetail.Version) appVersionDir := path.Join(appDownloadDir, appDetail.Version)
fileOp := files.NewFileOp() fileOp := files.NewFileOp()
storeVersion, err := settingRepo.GetValueByKey("AppStoreLastModified")
if err != nil {
return err
}
cacheVersionPath := path.Join(appVersionDir, ".1panel-appstore-version")
if !appDetail.Update && fileOp.Stat(appVersionDir) { if !appDetail.Update && fileOp.Stat(appVersionDir) {
return if cachedVersion, readErr := os.ReadFile(cacheVersionPath); readErr == nil && string(cachedVersion) == storeVersion {
return nil
}
}
downloadURL, err := appDownloadURL(appDetail.DownloadUrl, storeVersion)
if err != nil {
return err
} }
if !fileOp.Stat(appDownloadDir) { if !fileOp.Stat(appDownloadDir) {
_ = fileOp.CreateDir(appDownloadDir, constant.DirPerm) _ = fileOp.CreateDir(appDownloadDir, constant.DirPerm)
@@ -909,23 +946,14 @@ func downloadApp(app model.App, appDetail model.AppDetail, appInstall *model.App
_ = fileOp.CreateDir(appVersionDir, constant.DirPerm) _ = fileOp.CreateDir(appVersionDir, constant.DirPerm)
} }
if logger == nil { if logger == nil {
global.LOG.Infof("download app [%s] from %s", app.Name, appDetail.DownloadUrl) global.LOG.Infof("download app [%s] from %s", app.Name, downloadURL)
} else { } else {
logger.Printf("download app [%s] from %s", app.Name, appDetail.DownloadUrl) logger.Printf("download app [%s] from %s", app.Name, downloadURL)
} }
filePath := path.Join(appVersionDir, app.Key+"-"+appDetail.Version+".tar.gz") filePath := path.Join(appVersionDir, app.Key+"-"+appDetail.Version+".tar.gz")
defer func() { if err = files.DownloadFileWithProxy(downloadURL, filePath); err != nil {
if err != nil {
if appInstall != nil {
appInstall.Status = constant.StatusDownloadErr
appInstall.Message = err.Error()
}
}
}()
if err = files.DownloadFileWithProxy(appDetail.DownloadUrl, filePath); err != nil {
if logger == nil { if logger == nil {
global.LOG.Errorf("download app [%s] error %v", app.Name, err) global.LOG.Errorf("download app [%s] error %v", app.Name, err)
} else { } else {
@@ -942,6 +970,9 @@ func downloadApp(app model.App, appDetail model.AppDetail, appInstall *model.App
return return
} }
_ = fileOp.DeleteFile(filePath) _ = fileOp.DeleteFile(filePath)
if err = os.WriteFile(cacheVersionPath, []byte(storeVersion), constant.FilePerm); err != nil {
return err
}
appDetail.Update = false appDetail.Update = false
_ = appDetailRepo.Update(context.Background(), appDetail) _ = appDetailRepo.Update(context.Background(), appDetail)
return return
+16 -6
View File
@@ -43,6 +43,10 @@ type IDashboardService interface {
LoadQuickOptions() []dto.QuickJump LoadQuickOptions() []dto.QuickJump
ChangeQuick(req dto.ChangeQuicks) error ChangeQuick(req dto.ChangeQuicks) error
ListQuickDirectories() ([]model.QuickDirectory, error)
CreateQuickDirectory(req dto.QuickDirectoryCreate) (*model.QuickDirectory, error)
UpdateQuickDirectory(req dto.QuickDirectoryUpdate) (*model.QuickDirectory, error)
DeleteQuickDirectory(req dto.QuickDirectoryDelete) error
LoadAppLauncher(ctx *gin.Context) ([]dto.AppLauncher, error) LoadAppLauncher(ctx *gin.Context) ([]dto.AppLauncher, error)
ChangeShow(req dto.SettingUpdate) error ChangeShow(req dto.SettingUpdate) error
@@ -397,16 +401,15 @@ func (u *DashboardService) LoadQuickOptions() []dto.QuickJump {
} }
func (u *DashboardService) ChangeQuick(req dto.ChangeQuicks) error { func (u *DashboardService) ChangeQuick(req dto.ChangeQuicks) error {
showCount := 0 showCount := 0
var quicks []model.QuickJump quicks := make([]model.QuickJump, 0, len(req.Quicks))
for _, item := range req.Quicks { for _, item := range req.Quicks {
var quick model.QuickJump
if item.IsShow { if item.IsShow {
showCount++ showCount++
} }
if err := copier.Copy(&quick, &item); err != nil { quicks = append(quicks, model.QuickJump{
return err BaseModel: model.BaseModel{ID: item.ID},
} IsShow: item.IsShow,
quicks = append(quicks, quick) })
} }
if showCount == 0 { if showCount == 0 {
return buserr.New("ErrMinQuickJump") return buserr.New("ErrMinQuickJump")
@@ -706,6 +709,13 @@ func loadQuickJump(base *dto.DashboardBase) {
quicks := launcherRepo.ListQuickJump(false) quicks := launcherRepo.ListQuickJump(false)
for i := 0; i < len(quicks); i++ { for i := 0; i < len(quicks); i++ {
switch quicks[i].Name { switch quicks[i].Name {
case "File":
count, err := launcherRepo.CountQuickDirectories()
if err != nil {
global.LOG.Errorf("load quick directory count: %v", err)
}
quicks[i].Detail = fmt.Sprintf("%d", count)
quicks[i].Alias = ""
case "Agent": case "Agent":
quicks[i].Detail = fmt.Sprintf("%d", base.AgentNumber) quicks[i].Detail = fmt.Sprintf("%d", base.AgentNumber)
case "Website": case "Website":
+49
View File
@@ -0,0 +1,49 @@
package service
import (
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/buserr"
)
func (u *DashboardService) ListQuickDirectories() ([]model.QuickDirectory, error) {
return launcherRepo.ListQuickDirectories()
}
func quickDirectoryValues(req dto.QuickDirectoryCreate) (model.QuickDirectory, error) {
item := model.QuickDirectory{Path: req.Path, Alias: strings.TrimSpace(req.Alias)}
if strings.TrimSpace(item.Path) == "" {
return item, buserr.WithDetail("ErrInvalidParams", "path is required", nil)
}
return item, nil
}
func (u *DashboardService) CreateQuickDirectory(req dto.QuickDirectoryCreate) (*model.QuickDirectory, error) {
item, err := quickDirectoryValues(req)
if err != nil {
return nil, err
}
if err := launcherRepo.CreateQuickDirectory(&item); err != nil {
return nil, err
}
return &item, nil
}
func (u *DashboardService) UpdateQuickDirectory(req dto.QuickDirectoryUpdate) (*model.QuickDirectory, error) {
item, err := quickDirectoryValues(req.QuickDirectoryCreate)
if err != nil {
return nil, err
}
item.ID = req.ID
item.Legacy = req.Legacy
if err := launcherRepo.UpdateQuickDirectory(&item); err != nil {
return nil, err
}
return &item, nil
}
func (u *DashboardService) DeleteQuickDirectory(req dto.QuickDirectoryDelete) error {
return launcherRepo.DeleteQuickDirectory(req.ID, req.Legacy)
}
+12 -3
View File
@@ -212,10 +212,17 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
websiteSSL.Shell = req.Shell websiteSSL.Shell = req.Shell
} }
if req.PushDir { if req.PushDir {
if !files.NewFileOp().Stat(req.Dir) { dirs := certificateDirectories(req.Dir)
if len(dirs) == 0 {
return nil, buserr.New("ErrLinkPathNotFound") return nil, buserr.New("ErrLinkPathNotFound")
} }
websiteSSL.Dir = req.Dir for _, dir := range dirs {
info, err := os.Stat(dir)
if err != nil || !info.IsDir() {
return nil, buserr.New("ErrLinkPathNotFound")
}
}
websiteSSL.Dir = strings.Join(dirs, "\n")
} }
if req.Domains != "" { if req.Domains != "" {
domainArray := strings.Split(req.Domains, "\n") domainArray := strings.Split(req.Domains, "\n")
@@ -348,7 +355,9 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
if websiteSSL.ExecShell { if websiteSSL.ExecShell {
workDir := global.Dir.DataDir workDir := global.Dir.DataDir
if websiteSSL.PushDir { if websiteSSL.PushDir {
workDir = websiteSSL.Dir if dirs := certificateDirectories(websiteSSL.Dir); len(dirs) > 0 {
workDir = dirs[0]
}
} }
logger.Println(i18n.GetMsgByKey("ExecShellStart")) logger.Println(i18n.GetMsgByKey("ExecShellStart"))
if err = runShellScriptFile(workDir, websiteSSL.Shell, logger); err != nil { if err = runShellScriptFile(workDir, websiteSSL.Shell, logger); err != nil {
+21 -8
View File
@@ -209,11 +209,17 @@ func (w WebsiteSSLService) Create(create request.WebsiteSSLCreate) (request.Webs
websiteSSL.Shell = create.Shell websiteSSL.Shell = create.Shell
} }
if create.PushDir { if create.PushDir {
fileOP := files.NewFileOp() dirs := certificateDirectories(create.Dir)
if !fileOP.Stat(create.Dir) { if len(dirs) == 0 {
return res, buserr.New("ErrLinkPathNotFound") return res, buserr.New("ErrLinkPathNotFound")
} }
websiteSSL.Dir = create.Dir for _, dir := range dirs {
info, err := os.Stat(dir)
if err != nil || !info.IsDir() {
return res, buserr.New("ErrLinkPathNotFound")
}
}
websiteSSL.Dir = strings.Join(dirs, "\n")
} }
setSSLPushConfig(&websiteSSL, create.PushNode, create.Nodes) setSSLPushConfig(&websiteSSL, create.PushNode, create.Nodes)
@@ -573,7 +579,9 @@ func (w WebsiteSSLService) obtainSSL(id uint, autoRenew bool) error {
if websiteSSL.ExecShell { if websiteSSL.ExecShell {
workDir := global.Dir.DataDir workDir := global.Dir.DataDir
if websiteSSL.PushDir { if websiteSSL.PushDir {
workDir = websiteSSL.Dir if dirs := certificateDirectories(websiteSSL.Dir); len(dirs) > 0 {
workDir = dirs[0]
}
} }
printSSLLog(logger, "ExecShellStart", nil) printSSLLog(logger, "ExecShellStart", nil)
if err = runShellScriptFile(workDir, websiteSSL.Shell, logger); err != nil { if err = runShellScriptFile(workDir, websiteSSL.Shell, logger); err != nil {
@@ -793,11 +801,16 @@ func (w WebsiteSSLService) Update(update request.WebsiteSSLUpdate) error {
} }
if update.PushDir { if update.PushDir {
fileOP := files.NewFileOp() dirs := certificateDirectories(update.Dir)
if !fileOP.Stat(update.Dir) { if len(dirs) == 0 {
_ = fileOP.CreateDir(update.Dir, constant.DirPerm) return buserr.New("ErrLinkPathNotFound")
} }
updateParams["dir"] = update.Dir for _, dir := range dirs {
if err := os.MkdirAll(dir, constant.DirPerm); err != nil {
return err
}
}
updateParams["dir"] = strings.Join(dirs, "\n")
} }
var domains []string var domains []string
if update.OtherDomains != "" { if update.OtherDomains != "" {
+33 -18
View File
@@ -1529,26 +1529,41 @@ func checkWebsitePort(defaultHTTPsPort, port int, websiteType string) error {
return nil return nil
} }
// certificateDirectories accepts both legacy single paths and newline-separated paths.
func certificateDirectories(value string) []string {
var dirs []string
seen := make(map[string]bool)
for _, dir := range strings.Split(value, "\n") {
dir = strings.TrimSpace(dir)
if dir == "" {
continue
}
dir = filepath.Clean(dir)
if !seen[dir] {
seen[dir] = true
dirs = append(dirs, dir)
}
}
return dirs
}
func saveCertificateFile(websiteSSL *model.WebsiteSSL, logger *log.Logger) { func saveCertificateFile(websiteSSL *model.WebsiteSSL, logger *log.Logger) {
if websiteSSL.PushDir { if !websiteSSL.PushDir {
fileOp := files.NewFileOp() return
var ( }
pushErr error fileOp := files.NewFileOp()
MsgMap = map[string]interface{}{"path": websiteSSL.Dir, "status": i18n.GetMsgByKey("Success")} for _, dir := range certificateDirectories(websiteSSL.Dir) {
) status := i18n.GetMsgByKey("Success")
if pushErr = fileOp.SaveFile(path.Join(websiteSSL.Dir, "privkey.pem"), websiteSSL.PrivateKey, constant.FilePerm); pushErr != nil { for _, file := range []struct{ name, content string }{
MsgMap["status"] = i18n.GetMsgByKey("Failed") {"privkey.pem", websiteSSL.PrivateKey},
logger.Println(i18n.GetMsgWithMap("PushDirLog", MsgMap)) {"fullchain.pem", websiteSSL.Pem},
logger.Println("Push dir failed:" + pushErr.Error()) } {
} if err := fileOp.SaveFile(path.Join(dir, file.name), file.content, constant.FilePerm); err != nil {
if pushErr = fileOp.SaveFile(path.Join(websiteSSL.Dir, "fullchain.pem"), websiteSSL.Pem, constant.FilePerm); pushErr != nil { status = i18n.GetMsgByKey("Failed")
MsgMap["status"] = i18n.GetMsgByKey("Failed") logger.Printf("Push dir %s failed: %s", dir, err)
logger.Println(i18n.GetMsgWithMap("PushDirLog", MsgMap)) }
logger.Println("Push dir failed:" + pushErr.Error())
}
if pushErr == nil {
logger.Println(i18n.GetMsgWithMap("PushDirLog", MsgMap))
} }
logger.Println(i18n.GetMsgWithMap("PushDirLog", map[string]interface{}{"path": dir, "status": status}))
} }
} }
+2
View File
@@ -10,6 +10,8 @@ const (
AppOpenclaw = "openclaw" AppOpenclaw = "openclaw"
AppCopaw = "copaw" AppCopaw = "copaw"
AppHermesAgent = "hermes-agent" AppHermesAgent = "hermes-agent"
AppDeepSeekHarness = "deepseek-harness"
AppOctop = "octop"
AppMysql = "mysql" AppMysql = "mysql"
AppMariaDB = "mariadb" AppMariaDB = "mariadb"
AppPostgresql = "postgresql" AppPostgresql = "postgresql"
+1 -1
View File
@@ -1,6 +1,6 @@
module github.com/1Panel-dev/1Panel/agent module github.com/1Panel-dev/1Panel/agent
go 1.26.6 go 1.27.3
replace github.com/go-acme/lego/v5 => github.com/1Panel-dev/lego/v5 v5.3.1 replace github.com/go-acme/lego/v5 => github.com/1Panel-dev/lego/v5 v5.3.1
+2
View File
@@ -115,6 +115,8 @@ func agentDBMigrations() []*gormigrate.Migration {
migrations.AddMonitorSettings, migrations.AddMonitorSettings,
migrations.AddVLLMRequestMetrics, migrations.AddVLLMRequestMetrics,
migrations.AddVLLMSampleInterval, migrations.AddVLLMSampleInterval,
migrations.AddNativeAgentInstalls,
migrations.AddQuickDirectory,
} }
} }
@@ -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
@@ -1888,3 +1888,10 @@ var AddVLLMSampleInterval = &gormigrate.Migration{
return global.VLLMMonitorDB.AutoMigrate(&model.MonitorVLLM{}) return global.VLLMMonitorDB.AutoMigrate(&model.MonitorVLLM{})
}, },
} }
var AddQuickDirectory = &gormigrate.Migration{
ID: "20261016-add-quick-directory",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(&model.QuickDirectory{})
},
}
+1
View File
@@ -13,6 +13,7 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
baseApi := v1.ApiGroupApp.BaseApi baseApi := v1.ApiGroupApp.BaseApi
{ {
aiToolsRouter.GET("/benchmark/sources", baseApi.AIBenchmarkSources)
aiToolsRouter.POST("/ollama/close", baseApi.CloseOllamaModel) aiToolsRouter.POST("/ollama/close", baseApi.CloseOllamaModel)
aiToolsRouter.POST("/ollama/model", baseApi.CreateOllamaModel) aiToolsRouter.POST("/ollama/model", baseApi.CreateOllamaModel)
aiToolsRouter.POST("/ollama/model/recreate", baseApi.RecreateOllamaModel) aiToolsRouter.POST("/ollama/model/recreate", baseApi.RecreateOllamaModel)
+4
View File
@@ -14,6 +14,10 @@ func (s *DashboardRouter) InitRouter(Router *gin.RouterGroup) {
cmdRouter.GET("/base/os", baseApi.LoadDashboardOsInfo) cmdRouter.GET("/base/os", baseApi.LoadDashboardOsInfo)
cmdRouter.GET("/quick/option", baseApi.LoadQuickOption) cmdRouter.GET("/quick/option", baseApi.LoadQuickOption)
cmdRouter.POST("/quick/change", baseApi.UpdateQuickJump) cmdRouter.POST("/quick/change", baseApi.UpdateQuickJump)
cmdRouter.GET("/quick/directory", baseApi.ListQuickDirectories)
cmdRouter.POST("/quick/directory", baseApi.CreateQuickDirectory)
cmdRouter.POST("/quick/directory/update", baseApi.UpdateQuickDirectory)
cmdRouter.POST("/quick/directory/del", baseApi.DeleteQuickDirectory)
cmdRouter.GET("/app/launcher", baseApi.LoadAppLauncher) cmdRouter.GET("/app/launcher", baseApi.LoadAppLauncher)
cmdRouter.POST("/app/launcher/show", baseApi.UpdateAppLauncher) cmdRouter.POST("/app/launcher/show", baseApi.UpdateAppLauncher)
cmdRouter.POST("/app/launcher/option", baseApi.LoadAppLauncherOption) cmdRouter.POST("/app/launcher/option", baseApi.LoadAppLauncherOption)
+1
View File
@@ -266,6 +266,7 @@ func (b *BaseApi) GetLoginSetting(c *gin.Context) {
Language: settingInfo.Language, Language: settingInfo.Language,
MenuTabs: settingInfo.MenuTabs, MenuTabs: settingInfo.MenuTabs,
MenuAccordion: settingInfo.MenuAccordion, MenuAccordion: settingInfo.MenuAccordion,
PanelCache: settingInfo.PanelCache,
PanelName: settingInfo.PanelName, PanelName: settingInfo.PanelName,
Theme: settingInfo.Theme, Theme: settingInfo.Theme,
NeedCaptcha: needCaptcha, NeedCaptcha: needCaptcha,
+13
View File
@@ -4,6 +4,16 @@ import (
"time" "time"
) )
const DefaultPanelCache = `{"tabs":true,"pagination":true,"refresh":true,"search":false,"duration":"24h"}`
type PanelCache struct {
Tabs *bool `json:"tabs"`
Pagination *bool `json:"pagination"`
Refresh *bool `json:"refresh"`
Search *bool `json:"search"`
Duration string `json:"duration"`
}
type SettingInfo struct { type SettingInfo struct {
SystemVersion string `json:"systemVersion"` SystemVersion string `json:"systemVersion"`
DeveloperMode string `json:"developerMode"` DeveloperMode string `json:"developerMode"`
@@ -18,6 +28,7 @@ type SettingInfo struct {
Theme string `json:"theme"` Theme string `json:"theme"`
MenuTabs string `json:"menuTabs"` MenuTabs string `json:"menuTabs"`
MenuAccordion string `json:"menuAccordion"` MenuAccordion string `json:"menuAccordion"`
PanelCache string `json:"panelCache"`
Language string `json:"language"` Language string `json:"language"`
DocSource string `json:"docSource"` DocSource string `json:"docSource"`
@@ -62,6 +73,7 @@ type SettingBaseInfo struct {
Theme string `json:"theme"` Theme string `json:"theme"`
MenuTabs string `json:"menuTabs"` MenuTabs string `json:"menuTabs"`
MenuAccordion string `json:"menuAccordion"` MenuAccordion string `json:"menuAccordion"`
PanelCache string `json:"panelCache"`
Language string `json:"language"` Language string `json:"language"`
HideMenu string `json:"hideMenu"` HideMenu string `json:"hideMenu"`
DocSource string `json:"docSource"` DocSource string `json:"docSource"`
@@ -288,6 +300,7 @@ type LoginSetting struct {
Language string `json:"language"` Language string `json:"language"`
MenuTabs string `json:"menuTabs"` MenuTabs string `json:"menuTabs"`
MenuAccordion string `json:"menuAccordion"` MenuAccordion string `json:"menuAccordion"`
PanelCache string `json:"panelCache"`
PanelName string `json:"panelName"` PanelName string `json:"panelName"`
Theme string `json:"theme"` Theme string `json:"theme"`
NeedCaptcha bool `json:"needCaptcha"` NeedCaptcha bool `json:"needCaptcha"`
+27
View File
@@ -120,6 +120,9 @@ 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 settingMap["PanelCache"] == "" {
settingMap["PanelCache"] = dto.DefaultPanelCache
}
repairAndSortHideMenu(settingMap) repairAndSortHideMenu(settingMap)
var info dto.SettingInfo var info dto.SettingInfo
arr, err := json.Marshal(settingMap) arr, err := json.Marshal(settingMap)
@@ -155,6 +158,9 @@ func (u *SettingService) GetSettingBaseInfo() (*dto.SettingBaseInfo, error) {
for _, set := range setting { for _, set := range setting {
settingMap[set.Key] = set.Value settingMap[set.Key] = set.Value
} }
if settingMap["PanelCache"] == "" {
settingMap["PanelCache"] = dto.DefaultPanelCache
}
repairAndSortHideMenu(settingMap) repairAndSortHideMenu(settingMap)
var info dto.SettingBaseInfo var info dto.SettingBaseInfo
arr, err := json.Marshal(settingMap) arr, err := json.Marshal(settingMap)
@@ -224,6 +230,27 @@ func (u *SettingService) Update(c *gin.Context, key, value string) error {
if auth.IsAPICredentialSetting(key) { if auth.IsAPICredentialSetting(key) {
return buserr.New("ErrInvalidParams") return buserr.New("ErrInvalidParams")
} }
if key == "PanelCache" {
var config dto.PanelCache
decoder := json.NewDecoder(strings.NewReader(value))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&config); err != nil {
return buserr.New("ErrInvalidParams")
}
if err := decoder.Decode(new(any)); err != io.EOF {
return buserr.New("ErrInvalidParams")
}
if config.Tabs == nil || config.Pagination == nil || config.Refresh == nil || config.Search == nil {
return buserr.New("ErrInvalidParams")
}
switch config.Duration {
case "24h", "week", "month", "forever":
default:
return buserr.New("ErrInvalidParams")
}
data, _ := json.Marshal(config)
return settingRepo.UpdateOrCreate(key, string(data))
}
oldVal, err := settingRepo.Get(repo.WithByKey(key)) oldVal, err := settingRepo.Get(repo.WithByKey(key))
if err != nil { if err != nil {
return err return err
+1 -1
View File
@@ -1,6 +1,6 @@
module github.com/1Panel-dev/1Panel/core module github.com/1Panel-dev/1Panel/core
go 1.26.6 go 1.27.3
require ( require (
github.com/beevik/etree v1.7.0 github.com/beevik/etree v1.7.0
+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" 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" 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." 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" 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" 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." 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 وجود ندارد یا متعلق به کاربر فعلی نیست" ErrAPIKeyNotFound: "کلید API وجود ندارد یا متعلق به کاربر فعلی نیست"
ErrAPIKeyAppBindingDisabled: "اتصال برنامه با کد QR برای این کلید API مجاز نیست" ErrAPIKeyAppBindingDisabled: "اتصال برنامه با کد QR برای این کلید API مجاز نیست"
ErrAPIKeyTerminalUpgradeRequired: "امکان بررسی پشتیبانی از مجوزدهی ترمینال با کلید API وجود ندارد. اتصال گره را بررسی و Agent آن را ارتقا دهید، سپس دوباره تلاش کنید." ErrAPIKeyTerminalUpgradeRequired: "امکان بررسی پشتیبانی از مجوزدهی ترمینال با کلید API وجود ندارد. اتصال گره را بررسی و Agent آن را ارتقا دهید، سپس دوباره تلاش کنید."
LocalAppSyncDispatch: "ارسال درخواست همگام‌سازی برنامه‌های محلی"
LocalAppSyncDispatchNode: "ارسال درخواست همگام‌سازی به {{.node}} (وظیفه گره: {{.taskID}})"
ErrLocalAppSyncDisabled: "همگام‌سازی برنامه‌های محلی بین گره‌ها غیرفعال است"
+3
View File
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "同じ名前の未失効 API キーが既に存在しま
ErrAPIKeyNotFound: "API キーが存在しないか、現在のユーザーに属していません" ErrAPIKeyNotFound: "API キーが存在しないか、現在のユーザーに属していません"
ErrAPIKeyAppBindingDisabled: "この API キーでは APP の QR コード連携が許可されていません" ErrAPIKeyAppBindingDisabled: "この API キーでは APP の QR コード連携が許可されていません"
ErrAPIKeyTerminalUpgradeRequired: "API キーによるターミナル認証への対応を確認できません。ノードの接続を確認し、Agent を更新して再試行してください。" ErrAPIKeyTerminalUpgradeRequired: "API キーによるターミナル認証への対応を確認できません。ノードの接続を確認し、Agent を更新して再試行してください。"
LocalAppSyncDispatch: "ローカルアプリ同期要求の送信"
LocalAppSyncDispatchNode: "{{.node}} にローカルアプリ同期要求を送信(ノードタスク: {{.taskID}})"
ErrLocalAppSyncDisabled: "ノード間のローカルアプリ同期は無効です"
+3
View File
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "같은 이름의 폐기되지 않은 API Key가 이미 있
ErrAPIKeyNotFound: "API Key가 없거나 현재 사용자 소유가 아닙니다" ErrAPIKeyNotFound: "API Key가 없거나 현재 사용자 소유가 아닙니다"
ErrAPIKeyAppBindingDisabled: "이 API Key는 APP QR 코드 연결을 허용하지 않습니다" ErrAPIKeyAppBindingDisabled: "이 API Key는 APP QR 코드 연결을 허용하지 않습니다"
ErrAPIKeyTerminalUpgradeRequired: "API Key 터미널 인증 지원 여부를 확인할 수 없습니다. 노드 연결을 확인하고 Agent를 업그레이드한 후 다시 시도하세요." ErrAPIKeyTerminalUpgradeRequired: "API Key 터미널 인증 지원 여부를 확인할 수 없습니다. 노드 연결을 확인하고 Agent를 업그레이드한 후 다시 시도하세요."
LocalAppSyncDispatch: "로컬 앱 동기화 요청 전송"
LocalAppSyncDispatchNode: "{{.node}}에 로컬 앱 동기화 요청 전송 (노드 작업: {{.taskID}})"
ErrLocalAppSyncDisabled: "노드 간 로컬 앱 동기화가 비활성화되어 있습니다"
+3
View File
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "ມີ API Key ທີ່ຍັງບໍ່ຖືກຖອ
ErrAPIKeyNotFound: "ບໍ່ພົບ API Key ຫຼືບໍ່ແມ່ນຂອງຜູ້ໃຊ້ປັດຈຸບັນ" ErrAPIKeyNotFound: "ບໍ່ພົບ API Key ຫຼືບໍ່ແມ່ນຂອງຜູ້ໃຊ້ປັດຈຸບັນ"
ErrAPIKeyAppBindingDisabled: "API Key ນີ້ບໍ່ອະນຸຍາດໃຫ້ຜູກ APP ຜ່ານລະຫັດ QR" ErrAPIKeyAppBindingDisabled: "API Key ນີ້ບໍ່ອະນຸຍາດໃຫ້ຜູກ APP ຜ່ານລະຫັດ QR"
ErrAPIKeyTerminalUpgradeRequired: "ບໍ່ສາມາດຢືນຢັນການຮອງຮັບການອະນຸຍາດເທີມິນອນດ້ວຍ API Key. ກວດສອບການເຊື່ອມຕໍ່ໂນດ ແລະອັບເກຣດ Agent ກ່ອນລອງອີກ." 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" ErrAPIKeyNotFound: "Kunci API tidak wujud atau bukan milik pengguna semasa"
ErrAPIKeyAppBindingDisabled: "Pemautan APP melalui kod QR tidak dibenarkan untuk kunci API ini" 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." 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" 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" 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." 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 не существует или не принадлежит текущему пользователю" ErrAPIKeyNotFound: "Ключ API не существует или не принадлежит текущему пользователю"
ErrAPIKeyAppBindingDisabled: "Для этого ключа API привязка приложения через QR-код запрещена" ErrAPIKeyAppBindingDisabled: "Для этого ключа API привязка приложения через QR-код запрещена"
ErrAPIKeyTerminalUpgradeRequired: "Не удалось проверить поддержку авторизации терминала по ключу API. Проверьте подключение узла, обновите его Agent и повторите попытку." 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" 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" 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." 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 不存在或不屬於目前使用者" ErrAPIKeyNotFound: "API Key 不存在或不屬於目前使用者"
ErrAPIKeyAppBindingDisabled: "此 API Key 未允許 APP 掃碼綁定" ErrAPIKeyAppBindingDisabled: "此 API Key 未允許 APP 掃碼綁定"
ErrAPIKeyTerminalUpgradeRequired: "無法確認節點支援 API Key 終端授權,請檢查節點連線並升級 Agent 後重試。" ErrAPIKeyTerminalUpgradeRequired: "無法確認節點支援 API Key 終端授權,請檢查節點連線並升級 Agent 後重試。"
LocalAppSyncDispatch: "下發本機應用同步"
LocalAppSyncDispatchNode: "向節點 {{.node}} 下發本機應用同步(節點任務:{{.taskID}})"
ErrLocalAppSyncDisabled: "尚未啟用本機應用跨節點同步"
+3
View File
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "已存在同名的未撤销 API Key"
ErrAPIKeyNotFound: "API Key 不存在或不属于当前用户" ErrAPIKeyNotFound: "API Key 不存在或不属于当前用户"
ErrAPIKeyAppBindingDisabled: "此 API Key 未允许 APP 扫码绑定" ErrAPIKeyAppBindingDisabled: "此 API Key 未允许 APP 扫码绑定"
ErrAPIKeyTerminalUpgradeRequired: "无法确认节点支持 API Key 终端授权,请检查节点连接并升级 Agent 后重试。" ErrAPIKeyTerminalUpgradeRequired: "无法确认节点支持 API Key 终端授权,请检查节点连接并升级 Agent 后重试。"
LocalAppSyncDispatch: "下发本地应用同步"
LocalAppSyncDispatchNode: "向节点 {{.node}} 下发本地应用同步(节点任务:{{.taskID}})"
ErrLocalAppSyncDisabled: "尚未开启本地应用跨节点同步"
+1
View File
@@ -26,6 +26,7 @@ func Init() {
} }
var baseSettingKeys = map[string]struct{}{ var baseSettingKeys = map[string]struct{}{
"PanelCache": {},
"PanelName": {}, "PanelName": {},
"Theme": {}, "Theme": {},
"MenuTabs": {}, "MenuTabs": {},
+73
View File
@@ -0,0 +1,73 @@
import { existsSync } from 'node:fs';
import { resolve } from 'node:path';
import { build, type Plugin } from 'vite';
import vue from '@vitejs/plugin-vue';
export function benchmarkReportPlugin(root: string): Plugin | undefined {
const entry = resolve(root, 'src/enterprise/views/ai-benchmark/report/main.ts');
if (!existsSync(entry)) return;
const id = 'virtual:benchmark-report';
const resolvedID = `\0${id}`;
const dependencies = new Set<string>();
return {
name: 'benchmark-report',
resolveId(source) {
if (source === id) return resolvedID;
},
async load(source) {
if (source !== resolvedID) return;
const result = await build({
root,
configFile: false,
publicDir: false,
logLevel: 'warn',
resolve: { preserveSymlinks: true },
plugins: [
vue(),
{
name: 'benchmark-report-dependencies',
buildEnd() {
for (const file of this.getModuleIds()) {
if (!file.includes('node_modules') && !file.startsWith('\0')) {
dependencies.add(file.split('?')[0]);
}
}
},
},
],
define: {
'process.env.NODE_ENV': JSON.stringify('production'),
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false,
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false,
},
build: {
write: false,
minify: 'oxc',
cssCodeSplit: false,
lib: { entry, name: 'BenchmarkReport', formats: ['iife'] },
},
});
const outputs = (Array.isArray(result) ? result : [result]).flatMap((bundle) =>
'output' in bundle ? bundle.output : [],
);
const script = outputs
.filter((output) => output.type === 'chunk')
.map((output) => output.code)
.join('\n');
const styles = outputs
.filter((output) => output.type === 'asset' && output.fileName.endsWith('.css'))
.map((output) => (output.type === 'asset' ? String(output.source) : ''))
.join('\n');
if (!script || !styles) throw new Error('Benchmark report bundle is incomplete');
for (const file of dependencies) this.addWatchFile(file);
return `export const script = ${JSON.stringify(script)}; export const styles = ${JSON.stringify(styles)};`;
},
handleHotUpdate(context) {
if (!dependencies.has(context.file)) return;
const module = context.server.moduleGraph.getModuleById(resolvedID);
if (module) context.server.moduleGraph.invalidateModule(module);
context.server.ws.send({ type: 'full-reload' });
},
};
}
+3 -1
View File
@@ -1,7 +1,7 @@
import { ReqPage } from '.'; import { ReqPage } from '.';
export namespace AI { export namespace AI {
export type AgentType = 'openclaw' | 'copaw' | 'hermes-agent'; export type AgentType = 'openclaw' | 'copaw' | 'hermes-agent' | 'deepseek-harness' | 'octop';
export interface OllamaModelInfo { export interface OllamaModelInfo {
id: number; id: number;
@@ -423,6 +423,7 @@ export namespace AI {
} }
export interface AgentCreateReq { export interface AgentCreateReq {
accessHost?: string;
name: string; name: string;
remark: string; remark: string;
appVersion: string; appVersion: string;
@@ -450,6 +451,7 @@ export namespace AI {
} }
export interface AgentItem { export interface AgentItem {
accessHost: string;
id: number; id: number;
name: string; name: string;
remark: string; remark: string;
+3
View File
@@ -1,6 +1,9 @@
import { ReqPage, CommonModel } from '.'; import { ReqPage, CommonModel } from '.';
export namespace App { export namespace App {
export interface LocalAppSyncConfig {
enabled: boolean;
}
export interface App extends CommonModel { export interface App extends CommonModel {
name: string; name: string;
icon?: string; icon?: string;
+1
View File
@@ -77,6 +77,7 @@ export namespace Login {
language: string; language: string;
menuTabs: string; menuTabs: string;
menuAccordion: string; menuAccordion: string;
panelCache: string;
panelName: string; panelName: string;
theme: string; theme: string;
isOffline: boolean; isOffline: boolean;
+6
View File
@@ -8,6 +8,12 @@ export namespace Dashboard {
diskSize: number; diskSize: number;
} }
export interface QuickDirectory {
id: number;
legacy?: boolean;
path: string;
alias: string;
}
export interface QuickJump { export interface QuickJump {
id: number; id: number;
name: string; name: string;
+2
View File
@@ -41,6 +41,7 @@ export namespace Setting {
theme: string; theme: string;
menuTabs: string; menuTabs: string;
menuAccordion: string; menuAccordion: string;
panelCache: string;
language: string; language: string;
docSource: string; docSource: string;
@@ -86,6 +87,7 @@ export namespace Setting {
theme: string; theme: string;
menuTabs: string; menuTabs: string;
menuAccordion: string; menuAccordion: string;
panelCache: string;
language: string; language: string;
hideMenu: string; hideMenu: string;
docSource: string; docSource: string;
+12
View File
@@ -144,3 +144,15 @@ export function getAppIconUrl(appKey: string, node?: string): string {
export const installAppToNodes = (param: App.InstallAppToNodes) => { export const installAppToNodes = (param: App.InstallAppToNodes) => {
return http.post(`/core/xpack/sync/app/install`, param); 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);
};
+4 -2
View File
@@ -1,4 +1,5 @@
import http from '@/api'; import http from '@/api';
import type { AxiosRequestConfig } from 'axios';
import { ResPage, SearchWithPage } from '../interface'; import { ResPage, SearchWithPage } from '../interface';
import { Container } from '../interface/container'; import { Container } from '../interface/container';
import { TimeoutEnum } from '@/enums/http-enum'; import { TimeoutEnum } from '@/enums/http-enum';
@@ -23,10 +24,11 @@ export const loadContainerUsers = (name: string) => {
export const listContainerFiles = (params: Container.ContainerFileReq) => { export const listContainerFiles = (params: Container.ContainerFileReq) => {
return http.post<Array<Container.ContainerFileInfo>>(`/containers/files/search`, params, TimeoutEnum.T_40S); return http.post<Array<Container.ContainerFileInfo>>(`/containers/files/search`, params, TimeoutEnum.T_40S);
}; };
export const uploadContainerFile = (params: FormData) => { export const uploadContainerFile = (params: FormData, config?: AxiosRequestConfig & { skipErrorMessage?: boolean }) => {
return http.upload(`/containers/files/upload`, params, { return http.upload(`/containers/files/upload`, params, {
headers: { 'Content-Type': 'multipart/form-data' },
timeout: TimeoutEnum.T_5M, timeout: TimeoutEnum.T_5M,
...config,
headers: { 'Content-Type': 'multipart/form-data', ...config?.headers },
}); });
}; };
export const getContainerFileContent = (params: Container.ContainerFileReq) => { export const getContainerFileContent = (params: Container.ContainerFileReq) => {
+9 -1
View File
@@ -8,7 +8,7 @@ export const loadOsInfo = () => {
export const loadQuickOption = () => { export const loadQuickOption = () => {
return http.get<Array<Dashboard.QuickJump>>(`/dashboard/quick/option`); return http.get<Array<Dashboard.QuickJump>>(`/dashboard/quick/option`);
}; };
export const changeQuick = (quicks: Array<Dashboard.QuickJump>) => { export const changeQuick = (quicks: Array<Pick<Dashboard.QuickJump, 'id' | 'isShow'>>) => {
return http.post(`/dashboard/quick/change`, { quicks: quicks }); return http.post(`/dashboard/quick/change`, { quicks: quicks });
}; };
export const loadAppLauncher = () => { export const loadAppLauncher = () => {
@@ -48,3 +48,11 @@ export const loadTopMem = () => {
export const systemRestart = (operation: string) => { export const systemRestart = (operation: string) => {
return http.post(`/dashboard/system/restart/${operation}`); return http.post(`/dashboard/system/restart/${operation}`);
}; };
export const loadQuickDirectories = () => http.get<Dashboard.QuickDirectory[]>('/dashboard/quick/directory');
export const createQuickDirectory = (data: Omit<Dashboard.QuickDirectory, 'id'>) =>
http.post<Dashboard.QuickDirectory>('/dashboard/quick/directory', data);
export const updateQuickDirectory = (data: Dashboard.QuickDirectory) =>
http.post<Dashboard.QuickDirectory>('/dashboard/quick/directory/update', data);
export const deleteQuickDirectory = (id: number, legacy = false) =>
http.post('/dashboard/quick/directory/del', { id, legacy });
+21 -6
View File
@@ -10,8 +10,13 @@ export type FileUploadRequestConfig = AxiosRequestConfig & {
skipErrorMessage?: boolean; skipErrorMessage?: boolean;
}; };
export const getFilesList = (params: File.ReqFile) => { export const getFilesList = (params: File.ReqFile, currentNode?: string) => {
return http.post<File.File>('files/search', params, TimeoutEnum.T_5M); return http.post<File.File>(
'files/search',
params,
TimeoutEnum.T_5M,
currentNode ? { CurrentNode: currentNode } : undefined,
);
}; };
export const fileAiSearch = (params: File.FileAISearchReq) => { export const fileAiSearch = (params: File.FileAISearchReq) => {
@@ -217,8 +222,13 @@ export const checkFileShare = (params: File.FileShareCheck, headers?: AxiosReque
return http.get('files/share/check', params, { headers }); return http.get('files/share/check', params, { headers });
}; };
export const computeDirSize = (params: File.DirSizeReq) => { export const computeDirSize = (params: File.DirSizeReq, currentNode?: string) => {
return http.post<File.DirSizeRes>('files/size', params, TimeoutEnum.T_5M); return http.post<File.DirSizeRes>(
'files/size',
params,
TimeoutEnum.T_5M,
currentNode ? { CurrentNode: currentNode } : undefined,
);
}; };
export const computeDepthDirSize = (params: File.DirSizeReq) => { export const computeDepthDirSize = (params: File.DirSizeReq) => {
@@ -241,8 +251,13 @@ export const clearRecycle = () => {
return http.post<any>('files/recycle/clear'); return http.post<any>('files/recycle/clear');
}; };
export const searchFavorite = (params: ReqPage) => { export const searchFavorite = (params: ReqPage, currentNode?: string) => {
return http.post<ResPage<File.Favorite>>('files/favorite/search', params); return http.post<ResPage<File.Favorite>>(
'files/favorite/search',
params,
undefined,
currentNode ? { CurrentNode: currentNode } : undefined,
);
}; };
export const addFavorite = (path: string) => { export const addFavorite = (path: string) => {
Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

+45 -16
View File
@@ -79,6 +79,16 @@
@row-click="handleRowClick" @row-click="handleRowClick"
class="cursor-pointer dialog-file-list" class="cursor-pointer dialog-file-list"
> >
<el-table-column v-if="isMultiple && form.dir" width="48">
<template #default="{ row }">
<el-checkbox
v-if="row.isDir && !row.isCreate"
:model-value="selectedPathSet.has(row.path)"
@click.stop
@change="toggleDirectory(row.path)"
/>
</template>
</el-table-column>
<el-table-column prop="name" show-overflow-tooltip fix> <el-table-column prop="name" show-overflow-tooltip fix>
<template #default="{ row }"> <template #default="{ row }">
<svg-icon <svg-icon
@@ -131,6 +141,13 @@
</el-table> </el-table>
</div> </div>
<div class="file-list-bottom"> <div class="file-list-bottom">
<el-checkbox
v-if="isMultiple && form.dir"
:model-value="selectedPathSet.has(req.path)"
@change="toggleDirectory(req.path)"
>
{{ req.path }}
</el-checkbox>
<div v-if="!isMultiple && selectRow?.path"> <div v-if="!isMultiple && selectRow?.path">
{{ $t('file.currentSelect') }} {{ $t('file.currentSelect') }}
<el-tooltip :content="selectRow.path" placement="top"> <el-tooltip :content="selectRow.path" placement="top">
@@ -163,7 +180,11 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="handleClose">{{ $t('commons.button.cancel') }}</el-button> <el-button @click="handleClose">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="selectFile" :disabled="disBtn"> <el-button
type="primary"
@click="selectFile"
:disabled="disBtn || (isMultiple && selectRows.length === 0)"
>
{{ $t('commons.button.confirm') }} {{ $t('commons.button.confirm') }}
</el-button> </el-button>
</span> </span>
@@ -174,18 +195,20 @@
<script lang="ts" setup> <script lang="ts" setup>
import { File } from '@/api/interface/file'; import { File } from '@/api/interface/file';
import { computeDirSize, createFile, getFileContent, getFilesList, searchFavorite } from '@/api/modules/files'; import { computeDirSize, createFile, getFileContent, getFilesList, searchFavorite } from '@/api/modules/files';
import { nextTick, onUpdated, reactive, ref } from 'vue'; import { computed, nextTick, reactive, ref } from 'vue';
import i18n from '@/lang'; import i18n from '@/lang';
import { MsgSuccess, MsgWarning } from '@/utils/message'; import { MsgSuccess, MsgWarning } from '@/utils/message';
import { useSearchableForSelect } from '@/views/host/file-management/hooks/searchable'; import { useSearchableForSelect } from '@/views/host/file-management/hooks/searchable';
import { computeSize } from '@/utils/size'; import { computeSize } from '@/utils/size';
import { debounce } from '@/utils/misc'; import { debounce } from '@/utils/misc';
const props = defineProps<{ currentNode?: string }>();
const data = ref([]); const data = ref([]);
const loading = ref(false); const loading = ref(false);
const paths = ref<string[]>([]); const paths = ref<string[]>([]);
const req = reactive({ path: '/', expand: true, page: 1, pageSize: 300, showHidden: true }); const req = reactive({ path: '/', expand: true, page: 1, pageSize: 300, showHidden: true });
const selectRow = ref({ path: '', name: '' }); const selectRow = ref({ path: '', name: '' });
const selectRows = ref<Array<{ path: string; name: string }>>([]); const selectRows = ref<Array<{ path: string; name: string }>>([]);
const selectedPathSet = computed(() => new Set(selectRows.value.map((item) => item.path)));
const isMultiple = ref(false); const isMultiple = ref(false);
const rowRefs = ref(); const rowRefs = ref();
const open = ref(false); const open = ref(false);
@@ -196,7 +219,7 @@ const favorites = ref<File.Favorite[]>([]);
const { searchableStatus, searchablePath, searchableInputRef, searchableInputBlur } = useSearchableForSelect(paths); const { searchableStatus, searchablePath, searchableInputRef, searchableInputBlur } = useSearchableForSelect(paths);
const oldUrl = ref<string>(''); const oldUrl = ref<string>('');
const em = defineEmits(['choose']); const em = defineEmits(['choose', 'close']);
const form = reactive({ const form = reactive({
path: '/', path: '/',
@@ -211,6 +234,7 @@ interface DialogProps {
isAll?: boolean; isAll?: boolean;
disabled?: boolean; disabled?: boolean;
multiple?: boolean; multiple?: boolean;
selectedPaths?: string[];
} }
const acceptParams = (props: DialogProps): void => { const acceptParams = (props: DialogProps): void => {
form.path = props.path || '/'; form.path = props.path || '/';
@@ -218,7 +242,8 @@ const acceptParams = (props: DialogProps): void => {
form.isAll = props.isAll || false; form.isAll = props.isAll || false;
form.disabled = props.disabled || false; form.disabled = props.disabled || false;
isMultiple.value = props.multiple || false; isMultiple.value = props.multiple || false;
selectRows.value = []; disBtn.value = false;
selectRows.value = [...new Set(props.selectedPaths || [])].map((path) => ({ path, name: path }));
selectRow.value = { path: '', name: '' }; selectRow.value = { path: '', name: '' };
openPage(); openPage();
req.path = form.path; req.path = form.path;
@@ -245,6 +270,7 @@ const handleClose = () => {
open.value = false; open.value = false;
selectRow.value = { path: '', name: '' }; selectRow.value = { path: '', name: '' };
selectRows.value = []; selectRows.value = [];
em('close');
}; };
const openPage = () => { const openPage = () => {
@@ -281,7 +307,7 @@ const openDir = async (row: File.File, column: any, event: any) => {
} }
// Handle file click // Handle file click
const fullPath = (req.path === '/' ? req.path : req.path + '/') + row.name; const fullPath = (req.path === '/' ? req.path : req.path + '/') + row.name;
if (isMultiple.value) { if (isMultiple.value && (form.isAll || !form.dir)) {
// Multiple mode: toggle selection // Multiple mode: toggle selection
const index = selectRows.value.findIndex((r) => r.path === fullPath); const index = selectRows.value.findIndex((r) => r.path === fullPath);
if (index > -1) { if (index > -1) {
@@ -342,7 +368,7 @@ const getFileSize = async (path: string) => {
}; };
updateByPath(path, { btnLoading: true }); updateByPath(path, { btnLoading: true });
try { try {
const res = await getFileContent(params); const res = await getFileContent(params, props.currentNode);
updateByPath(path, { dirSize: res.data.size }); updateByPath(path, { dirSize: res.data.size });
} finally { } finally {
updateByPath(path, { btnLoading: false }); updateByPath(path, { btnLoading: false });
@@ -355,7 +381,7 @@ const getDirSize = async (path: string) => {
}; };
updateByPath(path, { btnLoading: true }); updateByPath(path, { btnLoading: true });
try { try {
const res = await computeDirSize(req); const res = await computeDirSize(req, props.currentNode);
updateByPath(path, { dirSize: res.data.size }); updateByPath(path, { dirSize: res.data.size });
} finally { } finally {
updateByPath(path, { btnLoading: false }); updateByPath(path, { btnLoading: false });
@@ -385,7 +411,7 @@ const getPaths = (reqPath: string) => {
const search = async (req: File.ReqFile) => { const search = async (req: File.ReqFile) => {
req.dir = form.dir; req.dir = form.dir;
loading.value = true; loading.value = true;
await getFilesList(req) await getFilesList(req, props.currentNode)
.then((res) => { .then((res) => {
if (!res.data.path) { if (!res.data.path) {
req.path = oldUrl.value; req.path = oldUrl.value;
@@ -472,23 +498,26 @@ const createFolder = async (row: any) => {
let addItem = {}; let addItem = {};
Object.assign(addItem, addForm); Object.assign(addItem, addForm);
loading.value = true; loading.value = true;
createFile(addItem as File.FileCreate) createFile(addItem as File.FileCreate, props.currentNode)
.then(() => { .then(async () => {
row.isCreate = false; row.isCreate = false;
disBtn.value = false; disBtn.value = false;
MsgSuccess(i18n.global.t('commons.msg.createSuccess')); MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
await search(req);
}) })
.finally(() => { .finally(() => {
loading.value = false; loading.value = false;
}); });
}; };
onUpdated(() => { const toggleDirectory = (path: string) => {
if (form.path != '') { const index = selectRows.value.findIndex((item) => item.path === path);
req.path = form.path; if (index >= 0) {
selectRows.value.splice(index, 1);
} else {
selectRows.value.push({ path, name: path });
} }
search(req); };
});
const removeSelected = (index: number) => { const removeSelected = (index: number) => {
selectRows.value.splice(index, 1); selectRows.value.splice(index, 1);
@@ -496,7 +525,7 @@ const removeSelected = (index: number) => {
const loadFavorites = async () => { const loadFavorites = async () => {
try { try {
const res = await searchFavorite({ page: 1, pageSize: 100 }); const res = await searchFavorite({ page: 1, pageSize: 100 }, props.currentNode);
favorites.value = res.data.items || []; favorites.value = res.data.items || [];
} catch {} } catch {}
}; };
+43 -45
View File
@@ -20,57 +20,55 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'; import { onActivated, onDeactivated, onMounted, onUnmounted, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { panelCacheKey, readPanelCache, writePanelCache } from '@/utils/panel-cache';
defineOptions({ name: 'TableSetting' }); defineOptions({ name: 'TableSetting' });
const refreshRateUnit = ref(0);
const refreshRateUnit = ref<number>(0);
const emit = defineEmits(['search']); const emit = defineEmits(['search']);
const props = defineProps({ const props = defineProps({ title: String, rate: Number });
title: String, const route = useRoute();
rate: Number, // Capture the owning page; useRoute continues to change while a page is deactivated.
}); const pagePath = route.path;
const cacheKey = () => panelCacheKey(`refresh:${pagePath}:${props.title || ''}`);
let timer: NodeJS.Timer | null = null; let timer: ReturnType<typeof setInterval> | undefined;
let active = false;
const changeRefresh = (command: number) => { const stopTimer = () => {
refreshRateUnit.value = command || 0; clearInterval(timer);
if (refreshRateUnit.value !== 0) { timer = undefined;
if (timer) {
clearInterval(Number(timer));
timer = null;
}
timer = setInterval(() => {
emit('search');
}, 1000 * refreshRateUnit.value);
} else {
if (timer) {
clearInterval(Number(timer));
timer = null;
}
}
localStorage.setItem(props.title, refreshRateUnit.value + '');
}; };
const startTimer = () => {
onUnmounted(() => { stopTimer();
clearInterval(Number(timer)); if (active && refreshRateUnit.value > 0) {
timer = null; timer = setInterval(() => emit('search'), 1000 * refreshRateUnit.value);
if (props.title) {
localStorage.setItem(props.title, refreshRateUnit.value + '');
} }
};
const restore = () => {
const rate = readPanelCache<number>('refresh', cacheKey());
refreshRateUnit.value = Number.isFinite(rate) && rate >= 0 ? rate : props.rate || 0;
startTimer();
};
const changeRefresh = (rate: number) => {
refreshRateUnit.value = rate;
writePanelCache('refresh', cacheKey(), rate);
startTimer();
};
const activate = () => {
if (active) return;
active = true;
restore();
};
watch(cacheKey, () => {
if (active) restore();
}); });
onMounted(activate);
onMounted(() => { onActivated(activate);
if (props.title && localStorage.getItem(props.title) != null) { onDeactivated(() => {
let rate = Number(localStorage.getItem(props.title)); active = false;
refreshRateUnit.value = rate ? Number(rate) : 0; stopTimer();
changeRefresh(refreshRateUnit.value);
return;
}
if (props.rate) {
refreshRateUnit.value = props.rate;
changeRefresh(refreshRateUnit.value);
}
}); });
onUnmounted(stopTimer);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -167,6 +167,7 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { panelCacheKey, writePanelCache } from '@/utils/panel-cache';
import { computed, onDeactivated, onMounted, ref, useAttrs } from 'vue'; import { computed, onDeactivated, onMounted, ref, useAttrs } from 'vue';
import { useGlobalStore } from '@/composables/useGlobalStore'; import { useGlobalStore } from '@/composables/useGlobalStore';
import { flattenVNodes, type FuTableOperationButton } from '@/components/table/shared'; import { flattenVNodes, type FuTableOperationButton } from '@/components/table/shared';
@@ -341,13 +342,33 @@ const handleRightClick = (row, column, event) => {
openContextMenu(row, event); openContextMenu(row, event);
}; };
watch(
() => props.data,
() => {
const pagination = props.paginationConfig;
if (!pagination || !pagination.pageSize) return;
const lastPage = Math.max(1, Math.ceil(pagination.total / pagination.pageSize));
if (pagination.currentPage > lastPage) {
pagination.currentPage = lastPage;
emit('search');
}
},
{ flush: 'post' },
);
function currentChange() { function currentChange() {
emit('search'); emit('search');
} }
function sizeChange() { function sizeChange() {
if (props.paginationConfig.cacheSizeKey) {
writePanelCache(
'pagination',
panelCacheKey(`size:${props.paginationConfig.cacheSizeKey}`),
props.paginationConfig.pageSize,
);
}
props.paginationConfig.currentPage = 1; props.paginationConfig.currentPage = 1;
localStorage.setItem(props.paginationConfig.cacheSizeKey, props.paginationConfig.pageSize);
emit('search'); emit('search');
} }
+2 -1
View File
@@ -52,6 +52,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { readCachedPageSize } from '@/utils/panel-cache';
import TaskLog from '@/components/log/task/index.vue'; import TaskLog from '@/components/log/task/index.vue';
import NodeSelect from '@/components/node-select/index.vue'; import NodeSelect from '@/components/node-select/index.vue';
@@ -73,7 +74,7 @@ const data = ref();
const paginationConfig = reactive({ const paginationConfig = reactive({
cacheSizeKey: 'task-list-page-size', cacheSizeKey: 'task-list-page-size',
currentPage: 1, currentPage: 1,
pageSize: Number(localStorage.getItem('task-list-page-size')) || 20, pageSize: readCachedPageSize('task-list-page-size') || 20,
total: 0, total: 0,
small: true, small: true,
}); });
+3 -2
View File
@@ -313,9 +313,10 @@ const initWebSocket = async (endpoint_: string, args: string = '') => {
const protocol = href.split('//')[0] === 'http:' ? 'ws' : 'wss'; const protocol = href.split('//')[0] === 'http:' ? 'ws' : 'wss';
const host = href.split('//')[1].split('/')[0]; const host = href.split('//')[1].split('/')[0];
const endpoint = endpoint_.replace(/^\/+/, ''); const endpoint = endpoint_.replace(/^\/+/, '');
let node = args.indexOf('id=') !== -1 ? 'local' : currentNode.value; const params = new URLSearchParams(args);
const node = params.has('id') ? 'local' : currentNode.value;
let conn = `${protocol}://${host}/${endpoint}?cols=${term.value.cols}&rows=${term.value.rows}&${args}&operateNode=${node}`; let conn = `${protocol}://${host}/${endpoint}?cols=${term.value.cols}&rows=${term.value.rows}&${args}&operateNode=${node}`;
if (args.indexOf('operateNode=') !== -1) { if (params.has('operateNode')) {
conn = `${protocol}://${host}/${endpoint}?cols=${term.value.cols}&rows=${term.value.rows}&${args}`; conn = `${protocol}://${host}/${endpoint}?cols=${term.value.cols}&rows=${term.value.rows}&${args}`;
} }
if (sessionId.value) { if (sessionId.value) {
@@ -1,24 +0,0 @@
import { storeToRefs } from 'pinia';
import { onActivated, onDeactivated } from 'vue';
import GlobalStore from '@/store/modules/global';
import { bindSearchReset, type SearchFields } from '@/utils/search-reset';
interface SearchResetOptions {
scope?: () => string | number | undefined;
onRevisit: () => unknown;
}
export const useSearchReset = (fields: SearchFields, options: SearchResetOptions) => {
const { currentNode } = storeToRefs(GlobalStore());
const reset = bindSearchReset(options.scope ? [currentNode, options.scope] : [currentNode], fields);
let deactivated = false;
onDeactivated(() => {
deactivated = true;
});
onActivated(() => {
if (!deactivated) return;
deactivated = false;
reset();
return options.onRevisit();
});
};
+5
View File
@@ -5,3 +5,8 @@ declare module '*.vue' {
const component: DefineComponent<{}, {}, any>; const component: DefineComponent<{}, {}, any>;
export default component; export default component;
} }
declare module 'virtual:benchmark-report' {
export const script: string;
export const styles: string;
}
+179 -9
View File
@@ -494,6 +494,7 @@ const message = {
recommend: 'Recommended', recommend: 'Recommended',
dir: 'Directory', dir: 'Directory',
alias: 'Alias', alias: 'Alias',
quickDirDelete: 'Delete ({0})?',
quickDir: 'Quick Access', quickDir: 'Quick Access',
minQuickJump: 'Add at least one quick access entry', minQuickJump: 'Add at least one quick access entry',
maxQuickJump: 'You can add up to four quick access entries', maxQuickJump: 'You can add up to four quick access entries',
@@ -773,6 +774,15 @@ const message = {
}, },
aiTools: { aiTools: {
agents: { 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', agent: 'Agent',
account: 'Model Account', account: 'Model Account',
batchOperation: 'Batch Operation', batchOperation: 'Batch Operation',
@@ -1483,16 +1493,13 @@ const message = {
statusWaiting: 'Waiting', statusWaiting: 'Waiting',
base: 'Basic Information', base: 'Basic Information',
backend: 'Backend', backend: 'Backend',
baseUrl: 'Base URL',
endpoint: 'Endpoint', endpoint: 'Endpoint',
tokenizer: 'Tokenizer', tokenizer: 'Tokenizer',
tokenizerPlaceholder: tokenizerPlaceholder: 'Enter an absolute directory on the main node or a Hugging Face repository ID',
'Select or enter a local tokenizer directory, e.g. /opt/1panel/tokenizers/DeepSeek-V3',
runConfig: 'Run Config', runConfig: 'Run Config',
rawResult: 'Raw Result', rawResult: 'Raw Result',
resultMetrics: 'Result Metrics', resultMetrics: 'Result Metrics',
contextTokens: 'Context Length', contextTokens: 'Context Length',
contextTokensHelper: 'Input token limit plus output token limit for this benchmark.',
outputThroughput: 'Output Throughput', outputThroughput: 'Output Throughput',
outputThroughputHelper: 'Generated output tokens per second. Higher means faster generation.', outputThroughputHelper: 'Generated output tokens per second. Higher means faster generation.',
totalThroughput: 'Total Throughput', totalThroughput: 'Total Throughput',
@@ -1500,20 +1507,19 @@ const message = {
firstTokenLatency: 'First Token Latency', firstTokenLatency: 'First Token Latency',
firstTokenLatencyHelper: firstTokenLatencyHelper:
'Time from sending the request to receiving the first token. Lower means faster response.', 'Time from sending the request to receiving the first token. Lower means faster response.',
tokenValueHelper: 'Enter a positive integer or k value, e.g. 512, 1k, 32k',
numPrompts: 'Prompts', numPrompts: 'Prompts',
concurrency: 'Concurrency', concurrency: 'Concurrency',
requestRate: 'Request Rate', requestRate: 'Request Rate',
requestRateUnlimited: 'Unlimited (max throughput)', requestRateUnlimited: 'Unlimited (max throughput)',
requestRateCustom: 'Custom QPS',
requestRateCustomPlaceholder: 'Requests per second, e.g. 2.5', requestRateCustomPlaceholder: 'Requests per second, e.g. 2.5',
successfulRequests: 'Successful Requests', successfulRequests: 'Successful Requests',
requestThroughput: 'Request Throughput', requestThroughput: 'Request Throughput',
mean: 'Mean', mean: 'Mean',
median: 'Median', median: 'Median',
timeout: 'Timeout (seconds)',
image: 'vLLM Image', image: 'vLLM Image',
ignoreEos: 'Ignore EOS', ignoreEos: 'Ignore end-of-sequence token (EOS)',
ignoreEosHelp:
'Prevents early stopping at EOS to help test the configured output length. Other stop conditions still apply; server support is required.',
extraHeaders: 'Extra Headers', extraHeaders: 'Extra Headers',
}, },
vllmMonitor: { vllmMonitor: {
@@ -1727,6 +1733,155 @@ const message = {
modelType: 'Model type', modelType: 'Model type',
}, },
}, },
benchmarkPlan: {
prepareTimeoutHelp: 'Preparation time limit, excluded from the overall timeout.',
pointTimeoutHelp:
'Separate timer per point, including startup, warmups and all requests; capped by the remaining overall time.',
totalTimeoutHelp: 'Starts after preparation, covers all points and intervals, and stops the test on timeout.',
concurrencyLevels: 'Concurrency levels',
concurrencyHelp:
'Levels run sequentially. Each value limits the number of requests in progress at the same time.',
requestCountMode: 'Request count',
fixedRequestMode: 'Fixed requests per level',
scaledRequestMode: 'Calculate from concurrency',
requestsPerPoint: 'Requests per level',
requestCountFormula:
'Per level: fixed value, or max(concurrency × multiplier, minimum requests). Scenario total: sum across levels × trials, excluding warmups.',
controlLog: 'Preparation and control log',
executionLog: 'Execution log',
pointLog: 'Point #{0} · {1} · Execution log',
archivedLog: '{0} (archive {1})',
tokenLengths: 'Input / output tokens',
kind: 'Result category',
kindServe: 'serve (regular tests)',
kindPD: 'pd (Prefill/Decode probes)',
saveDetailed: 'Save request details',
saveDetailedHelp:
'Save per-request lengths, latencies, generated text and errors. Summary metrics remain available when disabled, reducing result file size.',
parameters: 'Parameters',
temperatureHelp:
'Controls output randomness. Zero usually gives more stable output; higher values increase randomness.',
pointTimeout: 'Per-point timeout (seconds)',
temperature: 'Sampling temperature',
requestUrl: 'API URL',
invalidRequestUrl:
'Enter a full HTTP(S) URL ending in /chat/completions or /completions, without credentials, query parameters, or fragments.',
requestRateHelp:
'Applies only to this scenario. Unlimited rate still respects concurrency limits; fractional QPS is supported.',
tokenizerModeHelp:
'Available modes depend on the vLLM version in the benchmark image. Tokenizer loading is checked during task preparation.',
invalidBaseUrl: 'Enter a valid HTTP(S) URL without credentials, query parameters or fragments.',
invalidEndpoint:
'Enter an endpoint path matching the selected protocol and ending in {0}, without query parameters or fragments.',
invalidTokenizer:
'Use an absolute directory or repository ID; relative paths, commas and line breaks are not supported.',
invalidRequestRate: 'Select unlimited or enter a finite number greater than 0; decimals are supported.',
invalidCaCert: 'Enter an absolute CA certificate file path on the primary node, without commas or line breaks.',
bodyOverride: 'Extra body cannot override {0}.',
invalidBody: 'Enter a valid JSON object.',
invalidConcurrencies: 'Provide 1–100 concurrency values, each an integer between 1 and 100000.',
emptyRequestCount: 'When calculating from concurrency, multiplier and minimum requests cannot both be 0.',
requestCountLimit: 'Calculated requests per point must not exceed 1000000.',
contextExceeded: 'Input tokens, output tokens and template reserve exceed the context limit.',
planLimit: 'Provide 1–100 scenarios with no more than 2000 expanded points.',
caCertHelp:
'Select a CA certificate file on the primary server to verify HTTPS services. Leave empty to use system certificates.',
extraBody: 'Extra request body',
entryKey: 'Parameter name',
entryValue: 'Parameter value',
tokenizerMode: 'Tokenizer mode',
requireIdle: 'Check service idle before starting',
requireIdleHelp:
'Checks /metrics once before warmup and load testing. Stops if the service is busy or its idle state cannot be verified. This does not prevent other traffic during the test.',
trustRemoteCode: 'Trust remote code',
trustRemoteCodeHelp:
'Allows custom tokenizer code from the model repository to run inside the benchmark container. Enable only when required and the source is trusted.',
tokenizerModes: { auto: 'Auto (recommended)', slow: 'Slow tokenizer', mistral: 'Mistral tokenizer' },
quickDescription: 'Verify connectivity and measurement with a single request.',
regularDescription: 'Short, medium and long workloads for routine performance testing.',
fullDescription:
'Multiple scenarios, concurrency levels and repeated runs, including long inputs and shared prefixes.',
customDescription: 'Start with one scenario, set its parameters and add more as needed.',
scenarios: 'Scenarios',
builtIn: 'Built-in',
builtInHelp: 'Built-in schemes cannot be edited or deleted. Adjust a copy when creating a test.',
modified: 'Modified',
imageExists: 'Available locally',
imageNeedsPull: 'Pull required',
remark: 'Remarks',
source: 'Local service',
mode: 'Test plan',
quick: 'Quick check',
regular: 'Regular',
full: 'Full',
custom: 'Custom',
addScenario: 'Add scenario',
advanced: 'Advanced settings',
points: 'Points',
warmups: 'Warmup requests',
fixedRequests: 'Fixed requests',
multiplier: 'Requests per concurrency',
minRequests: 'Minimum requests',
trials: 'Trials',
prefixRatio: 'Shared prefix ratio',
totalTimeout: 'Round timeout (s)',
prepareTimeout: 'Preparation timeout (s)',
interval: 'Point interval (s)',
context: 'Context limit (0: unknown)',
reserve: 'Template token reserve',
storage: 'Storage limit (MiB)',
stopOnError: 'Stop subsequent points on error',
caCert: 'CA certificate path',
validTrials: 'Valid / total trials',
retry: 'Retry selected points',
environment: 'Environment',
plan: 'Frozen configuration',
clientHelp:
'The client container runs on the main node and sends requests to an existing service. It does not start inference or require a GPU.',
retryHelp: 'Only selected abnormal points are retried, with the same parameters and new seeds.',
secretHelp: 'API keys and headers are excluded from copies. Enter them again if needed.',
resultHelp:
'Only successful runs without request anomalies enter the median and min/max summaries. P99 is the median of per-run P99 values. Cache and external traffic are uncontrolled.',
},
benchmarkOverview: {
noResults: 'No valid test results. Check the failure reason and logs.',
noGroupResults: 'No valid results for this combination. Select another scenario or concurrency.',
partial: 'Partial results: the test did not fully succeed. Metrics cover valid points only.',
highlights: 'Key metrics',
aggregate: 'Median across valid runs in this combination. Click a metric for details.',
trend: 'Concurrency trend',
std: 'Std. deviation',
min: 'Minimum',
max: 'Maximum',
tpot: 'Time per output token',
tpotHelp: 'Average generation time per output token after the first token.',
itl: 'Inter-token latency',
itlHelp: 'Time between consecutive tokens in a streaming response.',
e2el: 'End-to-end latency',
e2elHelp: 'Time from sending the request to receiving the complete response.',
requestHelp: 'Number of requests completed per second.',
usageHelp:
'Medians of valid runs in this combination, not request totals for the entire test. See the Points tab for abnormal runs.',
duration: 'Run duration',
throughput: 'Throughput',
latency: 'Latency',
usage: 'Requests and usage (valid runs)',
},
benchmarkTokenizer: {
title: 'Tokenizer management',
local: 'Register local directory',
upload: 'Upload tokenizer package',
uploadHelp:
'Upload a ZIP / tar.gz containing tokenizer files without model weights. Maximum: 128 MiB compressed, 256 MiB extracted.',
source: 'Import method',
deleteLocal: 'Remove the registration for tokenizer “{0}”? The original directory and files will be kept.',
deleteUpload: 'Delete tokenizer “{0}” and its uploaded files? Historical test reports will be kept.',
select: 'Select a tokenizer',
selectHelp:
'For offline testing, select a managed tokenizer. Custom input still accepts a local directory or repository name.',
},
container: { container: {
createByCommand: 'Create by command', createByCommand: 'Create by command',
importContainerBackupTip: 'Import container backup file, only .tar.gz is supported', importContainerBackupTip: 'Import container backup file, only .tar.gz is supported',
@@ -2970,6 +3125,20 @@ const message = {
remote: 'Remote SSH', remote: 'Remote SSH',
}, },
setting: { setting: {
panelCache: {
title: 'Cache settings',
tabs: 'Tab cache',
pagination: 'Pagination cache',
refresh: 'Refresh interval cache',
search: 'Search input cache',
duration: 'Cache duration',
'24h': '24h',
week: 'One week',
month: 'One month',
forever: 'Forever',
helper: 'Expires after the last state change. Expired values return to page defaults.',
},
panel: 'Panel', panel: 'Panel',
user: 'Panel user', user: 'Panel user',
userChange: 'Change panel user', userChange: 'Change panel user',
@@ -3692,6 +3861,7 @@ const message = {
link: 'Link', link: 'Link',
showCurrentArch: 'Architecture', showCurrentArch: 'Architecture',
syncLocalApp: 'Sync Local App', syncLocalApp: 'Sync Local App',
localAppSyncToNodes: 'Include child nodes when syncing local apps',
memoryRequiredHelper: 'Current application memory requirement {0}', memoryRequiredHelper: 'Current application memory requirement {0}',
gpuConfig: 'Enable GPU Support', gpuConfig: 'Enable GPU Support',
gpuConfigHelper: gpuConfigHelper:
@@ -4252,7 +4422,7 @@ const message = {
pushDir: 'Push the certificate to the local directory', pushDir: 'Push the certificate to the local directory',
dir: 'Directory', dir: 'Directory',
pushDirHelper: pushDirHelper:
'Certificate file "fullchain.pem" and key file "privkey.pem" will be generated in this directory.', 'Select multiple directories or enter one directory per line. Each directory will receive "fullchain.pem" and "privkey.pem".',
organizationDetail: 'Organization details', organizationDetail: 'Organization details',
fromWebsite: 'From website', fromWebsite: 'From website',
dnsMauanlHelper: dnsMauanlHelper:
+181 -8
View File
@@ -493,6 +493,7 @@ const message = {
home: { home: {
recommend: 'Recomendado', recommend: 'Recomendado',
dir: 'Directorio', dir: 'Directorio',
quickDirDelete: '¿Confirmar la eliminación de ({0})?',
quickDir: 'Acceso rápido', quickDir: 'Acceso rápido',
minQuickJump: 'Agrega al menos una entrada de acceso rápido', minQuickJump: 'Agrega al menos una entrada de acceso rápido',
maxQuickJump: 'Puedes agregar hasta cuatro entradas de acceso rápido', maxQuickJump: 'Puedes agregar hasta cuatro entradas de acceso rápido',
@@ -776,6 +777,15 @@ const message = {
}, },
aiTools: { aiTools: {
agents: { 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', agent: 'Agente',
account: 'Cuenta de modelo', account: 'Cuenta de modelo',
batchOperation: 'Operación por lotes', batchOperation: 'Operación por lotes',
@@ -1491,16 +1501,14 @@ const message = {
statusWaiting: 'En espera', statusWaiting: 'En espera',
base: 'Información básica', base: 'Información básica',
backend: 'Backend', backend: 'Backend',
baseUrl: 'URL base',
endpoint: 'Endpoint', endpoint: 'Endpoint',
tokenizer: 'Tokenizador', tokenizer: 'Tokenizador',
tokenizerPlaceholder: tokenizerPlaceholder:
'Seleccione o introduzca un directorio local de tokenizador, p. ej. /opt/1panel/tokenizers/DeepSeek-V3', 'Introduzca un directorio absoluto del nodo principal o un ID de repositorio de Hugging Face',
runConfig: 'Configuración de ejecución', runConfig: 'Configuración de ejecución',
rawResult: 'Resultado bruto', rawResult: 'Resultado bruto',
resultMetrics: 'Métricas de resultado', resultMetrics: 'Métricas de resultado',
contextTokens: 'Longitud de contexto', contextTokens: 'Longitud de contexto',
contextTokensHelper: 'Suma del límite de tokens de entrada y salida de este benchmark.',
outputThroughput: 'Rendimiento de salida', outputThroughput: 'Rendimiento de salida',
outputThroughputHelper: 'Tokens de salida generados por segundo. Un valor mayor indica más velocidad.', outputThroughputHelper: 'Tokens de salida generados por segundo. Un valor mayor indica más velocidad.',
totalThroughput: 'Rendimiento total', totalThroughput: 'Rendimiento total',
@@ -1509,20 +1517,19 @@ const message = {
firstTokenLatency: 'Latencia del primer token', firstTokenLatency: 'Latencia del primer token',
firstTokenLatencyHelper: firstTokenLatencyHelper:
'Tiempo desde el envío de la solicitud hasta recibir el primer token. Un valor menor indica respuesta más rápida.', 'Tiempo desde el envío de la solicitud hasta recibir el primer token. Un valor menor indica respuesta más rápida.',
tokenValueHelper: 'Introduzca un entero positivo o valor con k, p. ej. 512, 1k, 32k',
numPrompts: 'Prompts', numPrompts: 'Prompts',
concurrency: 'Concurrencia', concurrency: 'Concurrencia',
mean: 'Media', mean: 'Media',
median: 'Mediana', median: 'Mediana',
requestRate: 'Tasa de solicitudes', requestRate: 'Tasa de solicitudes',
requestRateUnlimited: 'Ilimitado (máximo rendimiento)', requestRateUnlimited: 'Ilimitado (máximo rendimiento)',
requestRateCustom: 'QPS personalizado',
requestRateCustomPlaceholder: 'Solicitudes por segundo, p. ej. 2.5', requestRateCustomPlaceholder: 'Solicitudes por segundo, p. ej. 2.5',
successfulRequests: 'Solicitudes correctas', successfulRequests: 'Solicitudes correctas',
requestThroughput: 'Rendimiento de solicitudes', requestThroughput: 'Rendimiento de solicitudes',
timeout: 'Tiempo de espera (segundos)',
image: 'Imagen vLLM', image: 'Imagen vLLM',
ignoreEos: 'Ignorar EOS', ignoreEos: 'Ignorar el token de fin de secuencia (EOS)',
ignoreEosHelp:
'Evita detenerse antes por EOS para probar la longitud de salida configurada. Las demás condiciones de parada siguen vigentes; requiere soporte del servidor.',
extraHeaders: 'Encabezados adicionales', extraHeaders: 'Encabezados adicionales',
}, },
vllmMonitor: { vllmMonitor: {
@@ -1740,6 +1747,156 @@ const message = {
modelType: 'Tipo de modelo', modelType: 'Tipo de modelo',
}, },
}, },
benchmarkPlan: {
prepareTimeoutHelp: 'Límite de preparación, excluido del tiempo límite total.',
pointTimeoutHelp:
'Tiempo independiente por punto, con inicio, calentamiento y solicitudes; limitado por el tiempo total restante.',
totalTimeoutHelp: 'Empieza tras la preparación, incluye puntos e intervalos y detiene la prueba al agotarse.',
concurrencyLevels: 'Niveles de concurrencia',
concurrencyHelp: 'Los niveles se ejecutan en secuencia. Cada valor limita las solicitudes simultáneas.',
requestCountMode: 'Cantidad de solicitudes',
fixedRequestMode: 'Solicitudes fijas por nivel',
scaledRequestMode: 'Calcular según la concurrencia',
requestsPerPoint: 'Solicitudes por nivel',
requestCountFormula:
'Por nivel: valor fijo o max(concurrencia × multiplicador, mínimo de solicitudes). Total del escenario: suma de los niveles × repeticiones, sin calentamiento.',
controlLog: 'Registro de preparación y control',
executionLog: 'Registro de ejecución',
pointLog: 'Punto n.º {0} · {1} · Registro de ejecución',
archivedLog: '{0} (archivo histórico {1})',
tokenLengths: 'Tokens de entrada / salida',
kind: 'Categoría de resultados',
kindServe: 'serve (pruebas normales)',
kindPD: 'pd (mediciones de Prefill/Decode)',
saveDetailed: 'Guardar detalles de solicitudes',
saveDetailedHelp:
'Guarda longitudes, latencias, texto generado y errores de cada solicitud. Al desactivarlo se conservan las métricas resumidas y se reduce el tamaño de los archivos.',
parameters: 'Parámetros',
temperatureHelp:
'Controla la aleatoriedad de la respuesta. Cero suele producir resultados más estables; los valores altos aumentan la aleatoriedad.',
pointTimeout: 'Tiempo límite por punto (segundos)',
temperature: 'Temperatura de muestreo',
requestUrl: 'URL de API',
invalidRequestUrl:
'Introduzca una URL HTTP(S) completa terminada en /chat/completions o /completions, sin credenciales, parámetros de consulta ni fragmentos.',
requestRateHelp:
'Se aplica solo a este escenario. La tasa ilimitada respeta los límites de concurrencia; se admite QPS fraccionario.',
tokenizerModeHelp:
'Los modos disponibles dependen de la versión de vLLM de la imagen de prueba. La carga del tokenizador se comprueba al preparar la tarea.',
invalidBaseUrl: 'Introduzca una URL HTTP(S) válida sin credenciales, consultas ni fragmentos.',
invalidEndpoint:
'Introduzca una ruta compatible con el protocolo seleccionado que termine en {0}, sin parámetros de consulta ni fragmentos.',
invalidTokenizer:
'Use un directorio absoluto o un ID de repositorio; no se admiten rutas relativas, comas ni saltos de línea.',
invalidRequestRate: 'Seleccione sin límite o introduzca un número finito mayor que 0; se admiten decimales.',
invalidCaCert:
'Introduzca la ruta absoluta del certificado CA en el nodo principal, sin comas ni saltos de línea.',
bodyOverride: 'El cuerpo adicional no puede sobrescribir {0}.',
invalidBody: 'Introduzca un objeto JSON válido.',
invalidConcurrencies: 'Indique entre 1 y 100 valores de concurrencia, cada uno un entero entre 1 y 100000.',
emptyRequestCount:
'Al calcular según la concurrencia, el multiplicador y el mínimo de solicitudes no pueden ser ambos 0.',
requestCountLimit: 'Las solicitudes calculadas por punto no pueden superar 1000000.',
contextExceeded: 'Los tokens de entrada, salida y reserva de plantilla superan el límite de contexto.',
planLimit: 'Indique entre 1 y 100 escenarios con un máximo de 2000 puntos.',
caCertHelp:
'Seleccione un certificado CA del servidor principal para verificar servicios HTTPS. Déjelo vacío para usar los certificados del sistema.',
extraBody: 'Cuerpo adicional de la solicitud',
entryKey: 'Nombre del parámetro',
entryValue: 'Valor del parámetro',
tokenizerMode: 'Modo del tokenizador',
requireIdle: 'Comprobar que el servicio esté inactivo antes de comenzar',
requireIdleHelp:
'Consulta /metrics una vez antes del calentamiento y la prueba de carga. Se detiene si el servicio está ocupado o no se puede verificar su estado. No impide otro tráfico durante la prueba.',
trustRemoteCode: 'Confiar en código remoto',
trustRemoteCodeHelp:
'Permite ejecutar código de tokenización del repositorio del modelo dentro del contenedor de pruebas. Actívelo solo si es necesario y confía en la fuente.',
tokenizerModes: { auto: 'Automático (recomendado)', slow: 'Tokenizador lento', mistral: 'Tokenizador Mistral' },
quickDescription: 'Verifica la conectividad y la medición con una sola solicitud.',
regularDescription: 'Cargas cortas, medias y largas para pruebas habituales de rendimiento.',
fullDescription:
'Varios escenarios, niveles de concurrencia y repeticiones, con entradas largas y prefijos compartidos.',
customDescription: 'Empiece con un escenario, configure sus parámetros y añada más según necesite.',
scenarios: 'Escenarios',
builtIn: 'Integrado',
builtInHelp: 'Los esquemas integrados no se pueden editar ni eliminar. Ajuste una copia al crear una prueba.',
modified: 'Modificado',
imageExists: 'Disponible localmente',
imageNeedsPull: 'Requiere descarga',
remark: 'Observaciones',
source: 'Servicio local',
mode: 'Plan de prueba',
quick: 'Comprobación rápida',
regular: 'Normal',
full: 'Completa',
custom: 'Personalizada',
addScenario: 'Añadir escenario',
advanced: 'Configuración avanzada',
points: 'Puntos',
warmups: 'Solicitudes de calentamiento',
fixedRequests: 'Solicitudes fijas',
multiplier: 'Solicitudes por concurrencia',
minRequests: 'Solicitudes mínimas',
trials: 'Repeticiones',
prefixRatio: 'Proporción de prefijo compartido',
totalTimeout: 'Tiempo límite de ronda (s)',
prepareTimeout: 'Tiempo límite de preparación (s)',
interval: 'Intervalo entre puntos (s)',
context: 'Límite de contexto (0: desconocido)',
reserve: 'Tokens reservados para plantilla',
storage: 'Límite de almacenamiento (MiB)',
stopOnError: 'Detener los siguientes puntos si hay errores',
caCert: 'Ruta del certificado CA',
validTrials: 'Válidas / total',
retry: 'Repetir puntos seleccionados',
environment: 'Entorno',
plan: 'Configuración fijada',
clientHelp:
'El contenedor cliente se ejecuta en el nodo principal y envía solicitudes a un servicio existente. No inicia inferencia ni requiere GPU.',
retryHelp: 'Solo se repiten los puntos anómalos seleccionados, con los mismos parámetros y nuevas semillas.',
secretHelp: 'Las copias no incluyen claves API ni cabeceras. Vuelva a introducirlas si es necesario.',
resultHelp:
'La mediana y los mínimos/máximos solo incluyen ejecuciones correctas sin anomalías. P99 es la mediana de los P99 de cada ejecución. La caché y el tráfico externo no están controlados.',
},
benchmarkOverview: {
noResults: 'No hay resultados válidos. Consulte la causa del fallo y los registros.',
noGroupResults: 'Esta combinación no tiene resultados válidos. Seleccione otro escenario o concurrencia.',
partial: 'Resultados parciales: la prueba no se completó con éxito. Solo se incluyen puntos válidos.',
highlights: 'Métricas principales',
aggregate: 'Mediana de las ejecuciones válidas de esta combinación. Pulse una métrica para ver los detalles.',
trend: 'Tendencia por concurrencia',
std: 'Desviación estándar',
min: 'Mínimo',
max: 'Máximo',
tpot: 'Tiempo por token de salida',
tpotHelp: 'Tiempo medio de generación por token de salida después del primero.',
itl: 'Latencia entre tokens',
itlHelp: 'Tiempo entre tokens consecutivos de una respuesta en streaming.',
e2el: 'Latencia de extremo a extremo',
e2elHelp: 'Tiempo desde el envío de la solicitud hasta la recepción de la respuesta completa.',
requestHelp: 'Número de solicitudes completadas por segundo.',
usageHelp:
'Medianas de las ejecuciones válidas de esta combinación, no totales de solicitudes de toda la prueba. Consulte las ejecuciones anómalas en la pestaña de puntos.',
duration: 'Duración de la ejecución',
throughput: 'Rendimiento',
latency: 'Latencia',
usage: 'Solicitudes y uso (ejecuciones válidas)',
},
benchmarkTokenizer: {
title: 'Gestión de tokenizadores',
local: 'Registrar directorio local',
upload: 'Subir paquete de tokenizador',
uploadHelp: 'Suba un ZIP / tar.gz sin pesos del modelo. Máximo: 128 MiB comprimido y 256 MiB extraído.',
source: 'Método de importación',
deleteLocal:
'¿Quitar el registro del tokenizador «{0}»? Se conservarán el directorio y los archivos originales.',
deleteUpload: '¿Eliminar el tokenizador «{0}» y sus archivos subidos? Se conservarán los informes anteriores.',
select: 'Seleccionar un tokenizador',
selectHelp:
'Para pruebas sin conexión, seleccione un tokenizador gestionado. La entrada personalizada admite directorios locales o nombres de repositorios.',
},
container: { container: {
createByCommand: 'Crear por comando', createByCommand: 'Crear por comando',
importContainerBackupTip: 'Importar archivo de copia de seguridad de contenedor, solo se admite .tar.gz', importContainerBackupTip: 'Importar archivo de copia de seguridad de contenedor, solo se admite .tar.gz',
@@ -3015,6 +3172,20 @@ const message = {
remote: 'SSH remoto', remote: 'SSH remoto',
}, },
setting: { setting: {
panelCache: {
title: 'Configuración de caché',
tabs: 'Caché de pestañas',
pagination: 'Caché de paginación',
refresh: 'Caché del intervalo de actualización',
search: 'Caché del campo de búsqueda',
duration: 'Duración de la caché',
'24h': '24 horas',
week: 'Una semana',
month: 'Un mes',
forever: 'Permanente',
helper: 'El plazo se calcula desde el último cambio de estado. Al caducar, se restablecen los valores predeterminados.',
},
panel: 'Panel', panel: 'Panel',
user: 'Usuario del panel', user: 'Usuario del panel',
userChange: 'Cambiar usuario del panel', userChange: 'Cambiar usuario del panel',
@@ -3759,6 +3930,7 @@ const message = {
link: 'Enlace', link: 'Enlace',
showCurrentArch: 'Arquitectura', showCurrentArch: 'Arquitectura',
syncLocalApp: 'Sincronizar app local', syncLocalApp: 'Sincronizar app local',
localAppSyncToNodes: 'Incluir nodos secundarios al sincronizar apps locales',
memoryRequiredHelper: 'La app requiere {0} de memoria', memoryRequiredHelper: 'La app requiere {0} de memoria',
gpuConfig: 'Habilitar soporte GPU', gpuConfig: 'Habilitar soporte GPU',
gpuConfigHelper: 'Asegúrese de tener GPU NVIDIA, drivers y NVIDIA Docker Container Toolkit instalados', gpuConfigHelper: 'Asegúrese de tener GPU NVIDIA, drivers y NVIDIA Docker Container Toolkit instalados',
@@ -4301,7 +4473,8 @@ const message = {
domainHelper: 'Un dominio por línea, admite * y dirección IP', domainHelper: 'Un dominio por línea, admite * y dirección IP',
pushDir: 'Guardar certificado en el directorio local', pushDir: 'Guardar certificado en el directorio local',
dir: 'Directorio', dir: 'Directorio',
pushDirHelper: 'Se generarán los archivos "fullchain.pem" y "privkey.pem" en este directorio.', pushDirHelper:
'Seleccione varios directorios o introduzca un directorio por línea. En cada directorio se generarán los archivos "fullchain.pem" y "privkey.pem".',
organizationDetail: 'Detalles de la organización', organizationDetail: 'Detalles de la organización',
fromWebsite: 'Desde el sitio web', fromWebsite: 'Desde el sitio web',
dnsMauanlHelper: 'En modo manual, haz clic en solicitar tras la creación para obtener el valor DNS', dnsMauanlHelper: 'En modo manual, haz clic en solicitar tras la creación para obtener el valor DNS',
+177 -9
View File
@@ -489,6 +489,7 @@ const message = {
recommend: 'پیشنهادی', recommend: 'پیشنهادی',
dir: 'دایرکتوری', dir: 'دایرکتوری',
alias: 'نام مستعار', alias: 'نام مستعار',
quickDirDelete: 'حذف ({0}) را تأیید می‌کنید؟',
quickDir: 'دسترسی سریع', quickDir: 'دسترسی سریع',
minQuickJump: 'حداقل یک ورودی دسترسی سریع اضافه کنید', minQuickJump: 'حداقل یک ورودی دسترسی سریع اضافه کنید',
maxQuickJump: 'می‌توانید حداکثر چهار ورودی دسترسی سریع اضافه کنید', maxQuickJump: 'می‌توانید حداکثر چهار ورودی دسترسی سریع اضافه کنید',
@@ -768,6 +769,14 @@ const message = {
}, },
aiTools: { aiTools: {
agents: { agents: {
deepSeekAccessHostHelper:
'نشانی IPv4 یا نام میزبان مورد استفاده در مرورگر را بدون پروتکل، درگاه یا مسیر وارد کنید. HTTPS از گواهی محلی برنامه استفاده می‌کند.',
deepSeekAuthHelper:
'نام کاربری: حروف لاتین، اعداد، نقطه، زیرخط یا خط تیره. گذرواژه: حداقل ۱۲ نویسه، بدون فاصله در ابتدا و انتها یا شکست خط.',
octopAuthHelper:
'گذرواژه: حداقل ۸ نویسه شامل حروف و اعداد؛ از گذرواژه‌های رایج پرهیز کنید. اگر رد شد، راهنمای تنظیمات را در گزارش‌های برنامه ببینید.',
initialCredentials: 'اطلاعات ورود اولیه؛ اگر در برنامه تغییر کرده، از گذرواژه فعلی استفاده کنید.',
nativeAgentAccessUnavailable: 'نصب موجود نشانی یا درگاه HTTPS سازگار ندارد. تنظیمات برنامه را بررسی کنید.',
agent: 'عامل', agent: 'عامل',
account: 'حساب مدل', account: 'حساب مدل',
batchOperation: 'عملیات دسته‌ای', batchOperation: 'عملیات دسته‌ای',
@@ -1468,16 +1477,13 @@ const message = {
statusWaiting: 'در انتظار', statusWaiting: 'در انتظار',
base: 'اطلاعات پایه', base: 'اطلاعات پایه',
backend: 'بک‌اند', backend: 'بک‌اند',
baseUrl: 'URL پایه',
endpoint: 'نقطه پایانی', endpoint: 'نقطه پایانی',
tokenizer: 'توکن‌ساز', tokenizer: 'توکن‌ساز',
tokenizerPlaceholder: tokenizerPlaceholder: 'مسیر مطلق پوشه در گره اصلی یا شناسه مخزن Hugging Face را وارد کنید',
'یک دایرکتوری توکن‌ساز محلی را انتخاب یا وارد کنید، مثلاً /opt/1panel/tokenizers/DeepSeek-V3',
runConfig: 'پیکربندی اجرا', runConfig: 'پیکربندی اجرا',
rawResult: 'نتیجه خام', rawResult: 'نتیجه خام',
resultMetrics: 'معیارهای نتیجه', resultMetrics: 'معیارهای نتیجه',
contextTokens: 'طول زمینه', contextTokens: 'طول زمینه',
contextTokensHelper: 'محدودیت توکن ورودی به اضافه محدودیت توکن خروجی برای این معیار سنجش.',
outputThroughput: 'توان خروجی', outputThroughput: 'توان خروجی',
outputThroughputHelper: 'توکن‌های خروجی تولید شده در ثانیه. بیشتر به معنای تولید سریع‌تر است.', outputThroughputHelper: 'توکن‌های خروجی تولید شده در ثانیه. بیشتر به معنای تولید سریع‌تر است.',
totalThroughput: 'توان کل', totalThroughput: 'توان کل',
@@ -1485,20 +1491,19 @@ const message = {
'توکن‌های ورودی و خروجی پردازش شده در ثانیه. برای اندازه‌گیری ظرفیت کلی استفاده می‌شود.', 'توکن‌های ورودی و خروجی پردازش شده در ثانیه. برای اندازه‌گیری ظرفیت کلی استفاده می‌شود.',
firstTokenLatency: 'تأخیر اولین توکن', firstTokenLatency: 'تأخیر اولین توکن',
firstTokenLatencyHelper: 'زمان از ارسال درخواست تا دریافت اولین توکن. کمتر به معنای پاسخ سریع‌تر است.', firstTokenLatencyHelper: 'زمان از ارسال درخواست تا دریافت اولین توکن. کمتر به معنای پاسخ سریع‌تر است.',
tokenValueHelper: 'یک عدد صحیح مثبت یا مقدار k وارد کنید، مثلاً 512، 1k، 32k',
numPrompts: 'راهنماها', numPrompts: 'راهنماها',
concurrency: 'هم‌روندی', concurrency: 'هم‌روندی',
requestRate: 'نرخ درخواست', requestRate: 'نرخ درخواست',
requestRateUnlimited: 'نامحدود (حداکثر توان)', requestRateUnlimited: 'نامحدود (حداکثر توان)',
requestRateCustom: 'QPS سفارشی',
requestRateCustomPlaceholder: 'درخواست در ثانیه، مثلاً 2.5', requestRateCustomPlaceholder: 'درخواست در ثانیه، مثلاً 2.5',
successfulRequests: 'درخواست‌های موفق', successfulRequests: 'درخواست‌های موفق',
requestThroughput: 'توان درخواست', requestThroughput: 'توان درخواست',
mean: 'میانگین', mean: 'میانگین',
median: 'میانه', median: 'میانه',
timeout: 'مهلت (ثانیه)',
image: 'تصویر vLLM', image: 'تصویر vLLM',
ignoreEos: 'نادیده گرفتن EOS', ignoreEos: 'نادیده گرفتن توکن پایان دنباله (EOS)',
ignoreEosHelp:
'از توقف زودهنگام به دلیل EOS جلوگیری می‌کند تا طول خروجی تنظیم‌شده آزموده شود. سایر شرایط توقف همچنان اعمال می‌شوند و پشتیبانی سرور لازم است.',
extraHeaders: 'هدرهای اضافی', extraHeaders: 'هدرهای اضافی',
}, },
vllmMonitor: { vllmMonitor: {
@@ -1709,6 +1714,153 @@ const message = {
modelType: 'نوع مدل', modelType: 'نوع مدل',
}, },
}, },
benchmarkPlan: {
prepareTimeoutHelp: 'مهلت آماده‌سازی در مهلت کلی آزمون محاسبه نمی‌شود.',
pointTimeoutHelp:
'زمان‌سنج مستقل هر نقطه، شامل راه‌اندازی، گرم‌کردن و همه درخواست‌ها؛ محدود به زمان کلی باقی‌مانده.',
totalTimeoutHelp:
'پس از آماده‌سازی شروع می‌شود و همه نقاط و فاصله‌ها را شامل می‌شود. با پایان مهلت، آزمون متوقف می‌شود.',
concurrencyLevels: 'سطوح هم‌زمانی',
concurrencyHelp:
'سطوح به‌ترتیب آزمایش می‌شوند. هر مقدار سقف تعداد درخواست‌های در حال اجرا به‌صورت هم‌زمان است.',
requestCountMode: 'تنظیم تعداد درخواست‌ها',
fixedRequestMode: 'تعداد ثابت برای هر سطح',
scaledRequestMode: 'محاسبه بر اساس هم‌زمانی',
requestsPerPoint: 'درخواست در هر سطح',
requestCountFormula:
'هر سطح: مقدار ثابت یا max(هم‌زمانی × ضریب، حداقل درخواست‌ها). مجموع سناریو: مجموع سطوح × تعداد تکرار، بدون گرم‌کردن.',
controlLog: 'گزارش آماده‌سازی و کنترل',
executionLog: 'گزارش اجرا',
pointLog: 'نقطهٔ {0} · {1} · گزارش اجرا',
archivedLog: '{0} (بایگانی {1})',
tokenLengths: 'توکن‌های ورودی / خروجی',
kind: 'دسته‌بندی نتایج',
kindServe: 'serve (آزمون‌های معمول)',
kindPD: 'pd (سنجش Prefill/Decode)',
saveDetailed: 'ذخیره جزئیات درخواست‌ها',
saveDetailedHelp:
'طول، تأخیر، متن تولیدشده و خطاهای هر درخواست را ذخیره می‌کند. با غیرفعال کردن، شاخص‌های خلاصه حفظ و حجم فایل نتیجه کمتر می‌شود.',
parameters: 'پارامترها',
temperatureHelp:
'میزان تصادفی بودن خروجی را کنترل می‌کند. صفر معمولاً خروجی پایدارتری می‌دهد؛ مقادیر بالاتر تصادفی بودن را افزایش می‌دهند.',
pointTimeout: 'مهلت هر نقطه (ثانیه)',
temperature: 'دمای نمونه‌برداری',
requestUrl: 'نشانی API',
invalidRequestUrl:
'نشانی کامل HTTP(S) را با پایان /chat/completions یا /completions وارد کنید؛ بدون اطلاعات ورود، پارامترهای پرس‌وجو یا بخش پس از #.',
requestRateHelp:
'فقط برای این سناریو اعمال می‌شود. نرخ نامحدود همچنان از سقف هم‌زمانی پیروی می‌کند؛ QPS اعشاری پشتیبانی می‌شود.',
tokenizerModeHelp:
'حالت‌های موجود به نسخه vLLM در تصویر آزمون بستگی دارند. بارگذاری توکن‌ساز هنگام آماده‌سازی کار بررسی می‌شود.',
invalidBaseUrl: 'نشانی معتبر HTTP(S) بدون اطلاعات ورود، پارامتر پرس‌وجو یا قطعه وارد کنید.',
invalidEndpoint:
'مسیر سازگار با پروتکل انتخاب‌شده را وارد کنید که به {0} ختم شود و پارامتر پرس‌وجو یا بخش نشانی نداشته باشد.',
invalidTokenizer: 'مسیر مطلق پوشه یا شناسه مخزن را وارد کنید؛ مسیر نسبی، ویرگول و خط جدید پشتیبانی نمی‌شوند.',
invalidRequestRate: 'نامحدود را انتخاب کنید یا عدد متناهی بزرگ‌تر از 0 وارد کنید؛ اعشار مجاز است.',
invalidCaCert: 'مسیر مطلق فایل گواهی CA در گره اصلی را بدون ویرگول یا خط جدید وارد کنید.',
bodyOverride: 'بدنه اضافی نمی‌تواند {0} را بازنویسی کند.',
invalidBody: 'یک شیء JSON معتبر وارد کنید.',
invalidConcurrencies: 'بین 1 تا 100 مقدار هم‌زمانی وارد کنید؛ هر مقدار عدد صحیح بین 1 و 100000 باشد.',
emptyRequestCount: 'هنگام محاسبه بر اساس هم‌زمانی، ضریب و حداقل درخواست‌ها نمی‌توانند هر دو 0 باشند.',
requestCountLimit: 'تعداد محاسبه‌شده درخواست‌ها در هر نقطه نباید بیشتر از 1000000 باشد.',
contextExceeded: 'مجموع توکن‌های ورودی، خروجی و ذخیره قالب از حد زمینه بیشتر است.',
planLimit: 'بین 1 تا 100 سناریو با حداکثر 2000 نقطه وارد کنید.',
caCertHelp:
'فایل گواهی CA را از سرور اصلی برای تأیید سرویس HTTPS انتخاب کنید. برای استفاده از گواهی‌های سیستم خالی بگذارید.',
extraBody: 'بدنه اضافی درخواست',
entryKey: 'نام پارامتر',
entryValue: 'مقدار پارامتر',
tokenizerMode: 'حالت توکن‌ساز',
requireIdle: 'بررسی بیکار بودن سرویس پیش از شروع',
requireIdleHelp:
'پیش از گرم‌کردن و آزمون بار، /metrics یک بار بررسی می‌شود. اگر سرویس مشغول باشد یا بیکار بودن آن قابل تأیید نباشد، آزمون متوقف می‌شود. این بررسی مانع ترافیک دیگر در طول آزمون نمی‌شود.',
trustRemoteCode: 'اعتماد به کد راه‌دور',
trustRemoteCodeHelp:
'اجرای کد سفارشی توکن‌ساز از مخزن مدل در کانتینر آزمون را مجاز می‌کند. فقط در صورت نیاز و اعتماد به منبع فعال کنید.',
tokenizerModes: { auto: 'خودکار (پیشنهادی)', slow: 'توکن‌ساز کند', mistral: 'توکن‌ساز Mistral' },
quickDescription: 'اتصال و فرایند اندازه‌گیری را با یک درخواست بررسی می‌کند.',
regularDescription: 'بارهای کوتاه، متوسط و بلند برای آزمون روزمره عملکرد.',
fullDescription: 'چندین سناریو، سطح هم‌زمانی و اجرای تکراری، شامل ورودی‌های بلند و پیشوندهای مشترک.',
customDescription:
'با یک سناریو شروع کنید، پارامترهای آن را تنظیم کنید و در صورت نیاز سناریوهای بیشتری اضافه کنید.',
scenarios: 'سناریوها',
builtIn: 'داخلی',
builtInHelp: 'طرح‌های داخلی قابل ویرایش یا حذف نیستند. هنگام ایجاد آزمون یک نسخه را تنظیم کنید.',
modified: 'تغییریافته',
imageExists: 'موجود در میزبان',
imageNeedsPull: 'نیازمند دریافت',
remark: 'یادداشت',
source: 'سرویس محلی',
mode: 'برنامه آزمون',
quick: 'بررسی سریع',
regular: 'معمولی',
full: 'کامل',
custom: 'سفارشی',
addScenario: 'افزودن سناریو',
advanced: 'تنظیمات پیشرفته',
points: 'نقاط اندازه‌گیری',
warmups: 'درخواست‌های گرم‌سازی',
fixedRequests: 'درخواست ثابت',
multiplier: 'ضریب درخواست بر هم‌زمانی',
minRequests: 'حداقل درخواست',
trials: 'تکرارها',
prefixRatio: 'نسبت پیشوند مشترک',
totalTimeout: 'مهلت دور (ثانیه)',
prepareTimeout: 'مهلت آماده‌سازی (ثانیه)',
interval: 'فاصله نقاط (ثانیه)',
context: 'حد زمینه (۰: نامشخص)',
reserve: 'توکن ذخیره قالب',
storage: 'حد ذخیره‌سازی (MiB)',
stopOnError: 'توقف نقاط بعدی هنگام خطا',
caCert: 'مسیر گواهی CA',
validTrials: 'معتبر / کل تکرارها',
retry: 'تکرار نقاط انتخاب‌شده',
environment: 'محیط اجرا',
plan: 'پیکربندی ثابت',
clientHelp:
'کانتینر کلاینت در گره اصلی به سرویس موجود درخواست می‌فرستد. سرویس استنتاج راه‌اندازی نمی‌کند و به GPU نیاز ندارد.',
retryHelp: 'فقط نقاط غیرعادی انتخاب‌شده با همان پارامترها و بذرهای جدید تکرار می‌شوند.',
secretHelp: 'کپی‌ها شامل کلید API و سرآیندها نیستند. در صورت نیاز دوباره وارد کنید.',
resultHelp:
'میانه و کمینه/بیشینه فقط از اجراهای موفق بدون ناهنجاری درخواست محاسبه می‌شوند. P99 میانه P99 هر اجرا است. کش و ترافیک بیرونی کنترل نشده‌اند.',
},
benchmarkOverview: {
noResults: 'نتیجه معتبر وجود ندارد. علت شکست و گزارش‌ها را بررسی کنید.',
noGroupResults: 'برای این ترکیب نتیجه معتبر وجود ندارد. سناریو یا هم‌زمانی دیگری انتخاب کنید.',
partial: 'نتایج جزئی: آزمون کاملاً موفق نبوده است. معیارها فقط نقاط معتبر را شامل می‌شوند.',
highlights: 'معیارهای اصلی',
aggregate: 'میانه اجراهای معتبر این ترکیب. برای جزئیات روی معیار کلیک کنید.',
trend: 'روند هم‌زمانی',
std: 'انحراف معیار',
min: 'کمینه',
max: 'بیشینه',
tpot: 'زمان هر توکن خروجی',
tpotHelp: 'میانگین زمان تولید هر توکن خروجی پس از توکن نخست.',
itl: 'تأخیر بین توکن‌ها',
itlHelp: 'زمان بین توکن‌های متوالی در پاسخ جریانی.',
e2el: 'تأخیر سرتاسری',
e2elHelp: 'زمان ارسال درخواست تا دریافت کامل پاسخ.',
requestHelp: 'تعداد درخواست‌های تکمیل‌شده در ثانیه.',
usageHelp:
'میانه اجراهای معتبر این ترکیب است، نه مجموع درخواست‌های کل آزمون. اجراهای غیرعادی را در زبانه نقاط بررسی کنید.',
duration: 'مدت اجرا',
throughput: 'توان عملیاتی',
latency: 'تأخیر',
usage: 'درخواست‌ها و مصرف (اجراهای معتبر)',
},
benchmarkTokenizer: {
title: 'مدیریت توکن‌ساز',
local: 'ثبت پوشه محلی',
upload: 'بارگذاری بسته توکن‌ساز',
uploadHelp: 'فایل ZIP / tar.gz بدون وزن‌های مدل بارگذاری کنید. حداکثر ۱۲۸ MiB فشرده و ۲۵۶ MiB پس از استخراج.',
source: 'روش ورود',
deleteLocal: 'ثبت توکن‌ساز «{0}» حذف شود؟ پوشه و فایل‌های اصلی حفظ می‌شوند.',
deleteUpload: 'توکن‌ساز «{0}» و فایل‌های بارگذاری‌شده حذف شوند؟ گزارش‌های پیشین حفظ می‌شوند.',
select: 'انتخاب توکنایزر',
selectHelp: 'برای آزمون آفلاین از فهرست مدیریت انتخاب کنید. ورودی سفارشی پوشه محلی یا نام مخزن را می‌پذیرد.',
},
container: { container: {
createByCommand: 'ایجاد با دستور', createByCommand: 'ایجاد با دستور',
importContainerBackupTip: 'واردات فایل پشتیبان کانتینر، فقط .tar.gz پشتیبانی می‌شود', importContainerBackupTip: 'واردات فایل پشتیبان کانتینر، فقط .tar.gz پشتیبانی می‌شود',
@@ -2940,6 +3092,20 @@ const message = {
remote: 'SSH از راه دور', remote: 'SSH از راه دور',
}, },
setting: { setting: {
panelCache: {
title: 'تنظیمات حافظهٔ نهان',
tabs: 'حافظهٔ نهان زبانه‌ها',
pagination: 'حافظهٔ نهان صفحه‌بندی',
refresh: 'حافظهٔ نهان فاصلهٔ بازخوانی',
search: 'حافظهٔ نهان ورودی جستجو',
duration: 'مدت نگهداری حافظهٔ نهان',
'24h': '۲۴ ساعت',
week: 'یک هفته',
month: 'یک ماه',
forever: 'دائمی',
helper: 'مدت اعتبار از آخرین تغییر وضعیت محاسبه می‌شود. پس از انقضا، مقادیر پیش‌فرض بازیابی می‌شوند.',
},
panel: 'پنل', panel: 'پنل',
user: 'کاربر پنل', user: 'کاربر پنل',
userChange: 'تغییر کاربر پنل', userChange: 'تغییر کاربر پنل',
@@ -3658,6 +3824,7 @@ const message = {
link: 'لینک', link: 'لینک',
showCurrentArch: 'معماری', showCurrentArch: 'معماری',
syncLocalApp: 'همگام‌سازی برنامه محلی', syncLocalApp: 'همگام‌سازی برنامه محلی',
localAppSyncToNodes: 'همگام‌سازی گره‌های فرزند هنگام همگام‌سازی برنامه‌های محلی',
memoryRequiredHelper: 'نیاز حافظه برنامه فعلی {0}', memoryRequiredHelper: 'نیاز حافظه برنامه فعلی {0}',
gpuConfig: 'فعال‌سازی پشتیبانی GPU', gpuConfig: 'فعال‌سازی پشتیبانی GPU',
gpuConfigHelper: gpuConfigHelper:
@@ -4214,7 +4381,8 @@ const message = {
domainHelper: 'هر خط یک نام دامنه، پشتیبانی از * و آدرس IP', domainHelper: 'هر خط یک نام دامنه، پشتیبانی از * و آدرس IP',
pushDir: 'ارسال گواهی به دایرکتوری محلی', pushDir: 'ارسال گواهی به دایرکتوری محلی',
dir: 'دایرکتوری', dir: 'دایرکتوری',
pushDirHelper: 'فایل گواهی "fullchain.pem" و فایل کلید "privkey.pem" در این دایرکتوری تولید می‌شوند.', pushDirHelper:
'چند دایرکتوری انتخاب کنید یا در هر خط یک دایرکتوری وارد کنید. فایل‌های "fullchain.pem" و "privkey.pem" در هر دایرکتوری ایجاد می‌شوند.',
organizationDetail: 'جزئیات سازمان', organizationDetail: 'جزئیات سازمان',
fromWebsite: 'از وب‌سایت', fromWebsite: 'از وب‌سایت',
dnsMauanlHelper: dnsMauanlHelper:
+178 -9
View File
@@ -492,6 +492,7 @@ const message = {
recommend: 'おすすめ', recommend: 'おすすめ',
dir: 'ディレクトリ', dir: 'ディレクトリ',
alias: 'エイリアス', alias: 'エイリアス',
quickDirDelete: '({0})を削除しますか?',
quickDir: 'クイックアクセス', quickDir: 'クイックアクセス',
minQuickJump: 'クイックアクセス項目を1つ以上追加してください', minQuickJump: 'クイックアクセス項目を1つ以上追加してください',
maxQuickJump: '最大4件のクイックアクセス項目を追加できます', maxQuickJump: '最大4件のクイックアクセス項目を追加できます',
@@ -772,6 +773,15 @@ const message = {
}, },
aiTools: { aiTools: {
agents: { agents: {
deepSeekAccessHostHelper:
'ブラウザーで使用する IPv4 アドレスまたはホスト名を、スキーム・ポート・パスなしで入力してください。HTTPS はアプリのローカル証明書を使用します。',
deepSeekAuthHelper:
'ユーザー名には英数字、ドット、アンダースコア、ハイフンを使用できます。パスワードは12文字以上で、前後の空白や改行を含めないでください。',
octopAuthHelper:
'パスワードは英字と数字を含む8文字以上とし、よく使われるパスワードを避けてください。初期化時に拒否された場合は、アプリのログで設定手順を確認してください。',
initialCredentials: '初期ログイン情報です。アプリ内で変更した場合は現在のパスワードを使用してください。',
nativeAgentAccessUnavailable:
'既存のインストールに対応する HTTPS アクセス先またはポートがありません。アプリの設定を確認してください。',
agent: 'エージェント', agent: 'エージェント',
account: 'モデルアカウント', account: 'モデルアカウント',
batchOperation: '一括操作', batchOperation: '一括操作',
@@ -1472,36 +1482,32 @@ const message = {
statusWaiting: '待機中', statusWaiting: '待機中',
base: '基本情報', base: '基本情報',
backend: 'バックエンド', backend: 'バックエンド',
baseUrl: 'Base URL',
endpoint: 'エンドポイント', endpoint: 'エンドポイント',
tokenizer: 'トークナイザー', tokenizer: 'トークナイザー',
tokenizerPlaceholder: tokenizerPlaceholder: 'メインノード上の絶対ディレクトリパス、または Hugging Face リポジトリ ID を入力',
'ローカルトークナイザーディレクトリを選択または入力してください。例: /opt/1panel/tokenizers/DeepSeek-V3',
runConfig: '実行設定', runConfig: '実行設定',
rawResult: '生データ', rawResult: '生データ',
resultMetrics: '結果指標', resultMetrics: '結果指標',
contextTokens: 'コンテキスト長', contextTokens: 'コンテキスト長',
contextTokensHelper: 'このベンチマークの入力 Token 上限と出力 Token 上限の合計です。',
outputThroughput: '出力スループット', outputThroughput: '出力スループット',
outputThroughputHelper: '1 秒あたりに生成される出力 Token 数です。高いほど生成が速くなります。', outputThroughputHelper: '1 秒あたりに生成される出力 Token 数です。高いほど生成が速くなります。',
totalThroughput: '合計スループット', totalThroughput: '合計スループット',
totalThroughputHelper: '1 秒あたりに処理される入力と出力 Token の合計で、全体性能の測定に使います。', totalThroughputHelper: '1 秒あたりに処理される入力と出力 Token の合計で、全体性能の測定に使います。',
firstTokenLatency: '初回 Token レイテンシ', firstTokenLatency: '初回 Token レイテンシ',
firstTokenLatencyHelper: 'リクエスト送信から最初の Token 受信までの時間です。低いほど応答が速くなります。', firstTokenLatencyHelper: 'リクエスト送信から最初の Token 受信までの時間です。低いほど応答が速くなります。',
tokenValueHelper: '正の整数または k 単位を入力してください。例: 512、1k、32k',
numPrompts: 'プロンプト数', numPrompts: 'プロンプト数',
concurrency: '同時実行数', concurrency: '同時実行数',
mean: '平均値', mean: '平均値',
median: '中央値', median: '中央値',
requestRate: 'リクエストレート', requestRate: 'リクエストレート',
requestRateUnlimited: '無制限(最大スループット)', requestRateUnlimited: '無制限(最大スループット)',
requestRateCustom: 'カスタム QPS',
requestRateCustomPlaceholder: '1 秒あたりのリクエスト数。例: 2.5', requestRateCustomPlaceholder: '1 秒あたりのリクエスト数。例: 2.5',
successfulRequests: '成功リクエスト', successfulRequests: '成功リクエスト',
requestThroughput: 'リクエストスループット', requestThroughput: 'リクエストスループット',
timeout: 'タイムアウト(秒)',
image: 'vLLM イメージ', image: 'vLLM イメージ',
ignoreEos: 'EOS を無視', ignoreEos: '終了トークン(EOS)を無視',
ignoreEosHelp:
'EOS による早期終了を防ぎ、指定した出力長でのテストを助けます。他の停止条件は有効で、サーバー側の対応が必要です。',
extraHeaders: '追加ヘッダー', extraHeaders: '追加ヘッダー',
}, },
vllmMonitor: { vllmMonitor: {
@@ -1711,6 +1717,153 @@ const message = {
modelType: 'モデルタイプ', modelType: 'モデルタイプ',
}, },
}, },
benchmarkPlan: {
prepareTimeoutHelp: '準備時間の上限です。テスト全体のタイムアウトには含みません。',
pointTimeoutHelp:
'各測定点で個別に計時し、起動、ウォームアップ、全リクエストを含みます。全体の残り時間にも制限されます。',
totalTimeoutHelp: '準備完了後に開始し、全測定点と待機時間を含みます。時間切れでテストを停止します。',
concurrencyLevels: '同時実行数の段階',
concurrencyHelp: '各段階を順番にテストします。各値は同時に処理するリクエスト数の上限です。',
requestCountMode: 'リクエスト数の設定',
fixedRequestMode: '各段階のリクエスト数を固定',
scaledRequestMode: '同時実行数から計算',
requestsPerPoint: '各段階のリクエスト数',
requestCountFormula:
'各段階:固定値、または max(同時実行数 × 倍率, 最小リクエスト数)。 シナリオ合計:各段階の合計 × 繰り返し回数(ウォームアップを除く)。',
controlLog: '準備・制御ログ',
executionLog: '実行ログ',
pointLog: '測定点 #{0} · {1} · 実行ログ',
archivedLog: '{0}(履歴 {1})',
tokenLengths: '入力 / 出力トークン',
kind: '結果分類',
kindServe: 'serve(通常のテスト)',
kindPD: 'pd(Prefill/Decode 測定)',
saveDetailed: 'リクエスト詳細を保存',
saveDetailedHelp:
'各リクエストの長さ、遅延、生成テキスト、エラーを保存します。無効にしても集計指標は残り、結果ファイルを小さくできます。',
parameters: 'パラメーター',
temperatureHelp:
'生成内容のランダム性を制御します。0 は通常より安定した出力になり、値が高いほどランダム性が増します。',
pointTimeout: '測定点ごとのタイムアウト(秒)',
temperature: 'サンプリング温度',
requestUrl: 'API URL',
invalidRequestUrl:
'/chat/completions または /completions で終わる完全な HTTP(S) URL を入力してください。認証情報、クエリ、フラグメントは使用できません。',
requestRateHelp:
'このシナリオにのみ適用します。無制限でも同時実行数の上限に従います。小数の QPS も使用できます。',
tokenizerModeHelp:
'利用可能なモードはベンチマークイメージの vLLM バージョンによって異なります。タスクの準備時にトークナイザーを読み込めるか確認します。',
invalidBaseUrl: '認証情報、クエリ、フラグメントを含まない有効な HTTP(S) URL を入力してください。',
invalidEndpoint:
'選択したプロトコルに対応し、{0} で終わるパスを入力してください。クエリパラメーターやフラグメントは含めないでください。',
invalidTokenizer:
'絶対ディレクトリパスまたはリポジトリ ID を指定してください。相対パス、カンマ、改行は使用できません。',
invalidRequestRate: '無制限を選択するか、0 より大きい有限の数値を入力してください。小数も使用できます。',
invalidCaCert: 'プライマリノード上の CA 証明書の絶対パスを入力してください。カンマや改行は使用できません。',
bodyOverride: '追加本文で {0} を上書きできません。',
invalidBody: '有効な JSON オブジェクトを入力してください。',
invalidConcurrencies: '同時実行数は 1~100 項目、各値は 1~100000 の整数にしてください。',
emptyRequestCount: '同時実行数から計算する場合、倍率と最小リクエスト数の両方を 0 にできません。',
requestCountLimit: '測定点ごとの計算済みリクエスト数は 1000000 以下にしてください。',
contextExceeded: '入力・出力トークンとテンプレート予約分の合計がコンテキスト上限を超えています。',
planLimit: 'シナリオは 1~100 件、展開後の測定点は 2000 件以下にしてください。',
caCertHelp:
'HTTPS サービスの検証に使う CA 証明書をプライマリサーバーから選択します。空欄の場合はシステム証明書を使用します。',
extraBody: '追加のリクエスト本文',
entryKey: 'パラメーター名',
entryValue: 'パラメーター値',
tokenizerMode: 'トークナイザーモード',
requireIdle: '開始前にサービスのアイドル状態を確認',
requireIdleHelp:
'ウォームアップと負荷テストの前に /metrics を一度確認します。サービスが処理中、またはアイドル状態を確認できない場合は停止します。テスト中の他のトラフィックは防げません。',
trustRemoteCode: 'リモートコードを信頼',
trustRemoteCodeHelp:
'モデルリポジトリのカスタムトークナイザーコードをベンチマークコンテナー内で実行します。必要な場合に、信頼できる提供元に対してのみ有効にしてください。',
tokenizerModes: { auto: '自動(推奨)', slow: '低速トークナイザー', mistral: 'Mistral 専用' },
quickDescription: '単一リクエストで接続と測定処理を確認します。',
regularDescription: '短・中・長の負荷で日常的な性能テストを行います。',
fullDescription: '長い入力と共有プレフィックスを含む、複数シナリオ・同時実行数での反復測定です。',
customDescription: '1つのシナリオから始め、パラメーターを設定し、必要に応じて追加します。',
scenarios: 'シナリオ',
builtIn: '組み込み',
builtInHelp: '組み込みプランは編集・削除できません。テスト作成時にコピーを調整できます。',
modified: '変更済み',
imageExists: 'ローカルに存在',
imageNeedsPull: '取得が必要',
remark: '備考',
source: 'ローカルサービス',
mode: 'テスト計画',
quick: '簡易確認',
regular: '通常',
full: '完全',
custom: 'カスタム',
addScenario: 'シナリオを追加',
advanced: '詳細設定',
points: '測定点',
warmups: 'ウォームアップ要求数',
fixedRequests: '固定要求数',
multiplier: '同時実行数に対する要求数倍率',
minRequests: '最小要求数',
trials: '繰り返し回数',
prefixRatio: '共通プレフィックス比率',
totalTimeout: '全体のタイムアウト(秒)',
prepareTimeout: '準備のタイムアウト(秒)',
interval: '測定間隔(秒)',
context: 'コンテキスト上限(0:不明)',
reserve: 'テンプレート用予約トークン',
storage: '保存容量上限(MiB)',
stopOnError: 'エラー時に後続の測定を停止',
caCert: 'CA 証明書のパス',
validTrials: '有効 / 総試行数',
retry: '選択した測定点を再試行',
environment: '実行環境',
plan: '固定された設定',
clientHelp:
'クライアントコンテナはメインノードで既存サービスに要求を送信します。推論サービスの起動や GPU の割り当ては不要です。',
retryHelp: '選択した異常な測定点のみ、同じパラメータと新しいシードで再試行します。',
secretHelp: 'コピーに API キーとヘッダーは含まれません。必要に応じて再入力してください。',
resultHelp:
'要求異常のない成功した試行のみを集計し、中央値と最小・最大値を表示します。P99 は試行ごとの P99 の中央値です。キャッシュと外部トラフィックは制御されません。',
},
benchmarkOverview: {
noResults: '有効なテスト結果がありません。失敗の原因とログを確認してください。',
noGroupResults: 'この組み合わせに有効な結果はありません。シナリオまたは同時実行数を変更してください。',
partial: '部分的な結果:テストは完全には成功していません。有効な測定点のみを表示します。',
highlights: '主要指標',
aggregate: 'この組み合わせの有効な実行の中央値です。指標をクリックすると詳細を表示します。',
trend: '同時実行数による推移',
std: '標準偏差',
min: '最小値',
max: '最大値',
tpot: '出力トークンあたりの時間',
tpotHelp: '最初のトークン以降の、出力トークンあたりの平均生成時間。',
itl: 'トークン間レイテンシ',
itlHelp: 'ストリーミング応答の連続するトークン間の時間。',
e2el: 'エンドツーエンドのレイテンシ',
e2elHelp: 'リクエスト送信から応答の受信完了までの時間。',
requestHelp: '1 秒あたりに完了したリクエスト数。',
usageHelp:
'この組み合わせの有効な実行の中央値であり、テスト全体のリクエスト総数ではありません。異常な実行は測定点タブで確認してください。',
duration: '実行時間',
throughput: 'スループット',
latency: 'レイテンシ',
usage: 'リクエストと使用量(有効な実行)',
},
benchmarkTokenizer: {
title: 'トークナイザー管理',
local: 'ローカルディレクトリを登録',
upload: 'トークナイザーをアップロード',
uploadHelp:
'モデルの重みを含まない ZIP / tar.gz をアップロードしてください。圧縮時 128 MiB、展開後 256 MiB まで。',
source: 'インポート方法',
deleteLocal: '「{0}」の登録を解除しますか?元のディレクトリとファイルは保持されます。',
deleteUpload: '「{0}」とアップロード済みファイルを削除しますか?過去のテストレポートは保持されます。',
select: 'トークナイザーを選択',
selectHelp:
'オフラインでは管理リストから選択してください。カスタム入力ではローカルパスやリポジトリ名も使えます。',
},
container: { container: {
createByCommand: 'コマンドで作成', createByCommand: 'コマンドで作成',
importContainerBackupTip: 'コンテナバックアップファイルをインポートします。.tar.gz のみ対応です', importContainerBackupTip: 'コンテナバックアップファイルをインポートします。.tar.gz のみ対応です',
@@ -2945,6 +3098,20 @@ const message = {
remote: 'リモートSSH', remote: 'リモートSSH',
}, },
setting: { setting: {
panelCache: {
title: 'キャッシュ設定',
tabs: 'タブのキャッシュ',
pagination: 'ページネーションのキャッシュ',
refresh: '更新間隔のキャッシュ',
search: '検索入力のキャッシュ',
duration: 'キャッシュ期間',
'24h': '24時間',
week: '1週間',
month: '1か月',
forever: '無期限',
helper: '有効期間は状態を最後に変更した時点から計算されます。期限が切れると既定値に戻ります。',
},
panel: 'パネル', panel: 'パネル',
user: 'パネルユーザー', user: 'パネルユーザー',
userChange: 'パネルユーザーを変更します', userChange: 'パネルユーザーを変更します',
@@ -3678,6 +3845,7 @@ const message = {
link: 'リンク', link: 'リンク',
showCurrentArch: '現在のサーバーアーキテクチャのアプリケーション', showCurrentArch: '現在のサーバーアーキテクチャのアプリケーション',
syncLocalApp: 'ローカルアプリの同期', syncLocalApp: 'ローカルアプリの同期',
localAppSyncToNodes: 'ローカルアプリ同期時に子ノードも同期',
memoryRequiredHelper: '現在のアプリケーションは {0} メモリが必要です', memoryRequiredHelper: '現在のアプリケーションは {0} メモリが必要です',
gpuConfig: 'GPUサポートを有効化', gpuConfig: 'GPUサポートを有効化',
gpuConfigHelper: gpuConfigHelper:
@@ -4237,7 +4405,8 @@ const message = {
domainHelper: '1行ごとに1つのドメイン名、 *およびIPアドレスをサポートします', domainHelper: '1行ごとに1つのドメイン名、 *およびIPアドレスをサポートします',
pushDir: '証明書をローカルディレクトリにプッシュします', pushDir: '証明書をローカルディレクトリにプッシュします',
dir: 'ディレクトリ', dir: 'ディレクトリ',
pushDirHelper: '証明書ファイル「FullChain.PEM」とキーファイル「Privkey.Pem」がこのディレクトリで生成されます。', pushDirHelper:
'複数のディレクトリを選択するか、1行に1つずつディレクトリを入力してください。各ディレクトリに「fullchain.pem」と「privkey.pem」が生成されます。',
organizationDetail: '組織の詳細', organizationDetail: '組織の詳細',
fromWebsite: 'ウェブサイトから', fromWebsite: 'ウェブサイトから',
dnsMauanlHelper: '手動解像度モードでは、作成後に適用ボタンをクリックしてDNS解像度値を取得する必要があります', dnsMauanlHelper: '手動解像度モードでは、作成後に適用ボタンをクリックしてDNS解像度値を取得する必要があります',
+175 -9
View File
@@ -487,6 +487,7 @@ const message = {
recommend: '추천', recommend: '추천',
dir: '디렉토리', dir: '디렉토리',
alias: '별칭', alias: '별칭',
quickDirDelete: '({0})을(를) 삭제하시겠습니까?',
quickDir: '빠른 액세스', quickDir: '빠른 액세스',
minQuickJump: '최소 하나의 빠른 액세스 항목을 추가하세요', minQuickJump: '최소 하나의 빠른 액세스 항목을 추가하세요',
maxQuickJump: '빠른 액세스를 최대 4개까지 추가할 수 있습니다', maxQuickJump: '빠른 액세스를 최대 4개까지 추가할 수 있습니다',
@@ -763,6 +764,15 @@ const message = {
}, },
aiTools: { aiTools: {
agents: { agents: {
deepSeekAccessHostHelper:
'브라우저에서 사용하는 IPv4 주소 또는 호스트 이름을 입력하세요. 프로토콜, 포트, 경로는 제외하세요. HTTPS는 앱의 로컬 인증서를 사용합니다.',
deepSeekAuthHelper:
'사용자 이름은 영문, 숫자, 점, 밑줄, 하이픈만 지원합니다. 비밀번호는 12자 이상이며 앞뒤 공백이나 줄바꿈을 포함할 수 없습니다.',
octopAuthHelper:
'비밀번호는 문자와 숫자를 포함한 8자 이상이어야 하며 흔한 비밀번호는 피하세요. 초기화 시 거부되면 앱 로그의 설정 안내를 확인하세요.',
initialCredentials: '초기 로그인 정보입니다. 앱에서 변경했다면 현재 비밀번호를 사용하세요.',
nativeAgentAccessUnavailable:
'기존 설치에 호환되는 HTTPS 접속 주소 또는 포트가 없습니다. 앱 설정을 확인하세요.',
agent: '에이전트', agent: '에이전트',
account: '모델 계정', account: '모델 계정',
batchOperation: '일괄 작업', batchOperation: '일괄 작업',
@@ -1457,36 +1467,32 @@ const message = {
statusWaiting: '대기 중', statusWaiting: '대기 중',
base: '기본 정보', base: '기본 정보',
backend: '백엔드', backend: '백엔드',
baseUrl: 'Base URL',
endpoint: '엔드포인트', endpoint: '엔드포인트',
tokenizer: '토크나이저', tokenizer: '토크나이저',
tokenizerPlaceholder: tokenizerPlaceholder: '메인 노드의 절대 디렉터리 경로 또는 Hugging Face 저장소 ID를 입력하세요',
'로컬 토크나이저 디렉터리를 선택하거나 입력하세요. 예: /opt/1panel/tokenizers/DeepSeek-V3',
runConfig: '실행 구성', runConfig: '실행 구성',
rawResult: '원시 결과', rawResult: '원시 결과',
resultMetrics: '결과 지표', resultMetrics: '결과 지표',
contextTokens: '컨텍스트 길이', contextTokens: '컨텍스트 길이',
contextTokensHelper: '이 벤치마크의 입력 Token 제한과 출력 Token 제한의 합계입니다.',
outputThroughput: '출력 처리량', outputThroughput: '출력 처리량',
outputThroughputHelper: '초당 생성된 출력 Token 수입니다. 높을수록 생성 속도가 빠릅니다.', outputThroughputHelper: '초당 생성된 출력 Token 수입니다. 높을수록 생성 속도가 빠릅니다.',
totalThroughput: '총 처리량', totalThroughput: '총 처리량',
totalThroughputHelper: '초당 처리된 입력 및 출력 Token 수로 전체 처리 능력을 측정합니다.', totalThroughputHelper: '초당 처리된 입력 및 출력 Token 수로 전체 처리 능력을 측정합니다.',
firstTokenLatency: '첫 Token 지연 시간', firstTokenLatency: '첫 Token 지연 시간',
firstTokenLatencyHelper: '요청 전송부터 첫 Token 수신까지의 시간입니다. 낮을수록 응답이 빠릅니다.', firstTokenLatencyHelper: '요청 전송부터 첫 Token 수신까지의 시간입니다. 낮을수록 응답이 빠릅니다.',
tokenValueHelper: '양의 정수 또는 k 값을 입력하세요. 예: 512, 1k, 32k',
numPrompts: '프롬프트 수', numPrompts: '프롬프트 수',
concurrency: '동시성', concurrency: '동시성',
mean: '평균값', mean: '평균값',
median: '중앙값', median: '중앙값',
requestRate: '요청 속도', requestRate: '요청 속도',
requestRateUnlimited: '무제한(최대 처리량)', requestRateUnlimited: '무제한(최대 처리량)',
requestRateCustom: '사용자 지정 QPS',
requestRateCustomPlaceholder: '초당 요청 수, 예: 2.5', requestRateCustomPlaceholder: '초당 요청 수, 예: 2.5',
successfulRequests: '성공한 요청', successfulRequests: '성공한 요청',
requestThroughput: '요청 처리량', requestThroughput: '요청 처리량',
timeout: '시간 초과(초)',
image: 'vLLM 이미지', image: 'vLLM 이미지',
ignoreEos: 'EOS 무시', ignoreEos: '종료 토큰(EOS) 무시',
ignoreEosHelp:
'EOS로 인한 조기 종료를 방지하여 설정한 출력 길이로 테스트하도록 돕습니다. 다른 종료 조건은 계속 적용되며 서버 지원이 필요합니다.',
extraHeaders: '추가 헤더', extraHeaders: '추가 헤더',
}, },
vllmMonitor: { vllmMonitor: {
@@ -1694,6 +1700,150 @@ const message = {
modelType: '모델 유형', modelType: '모델 유형',
}, },
}, },
benchmarkPlan: {
prepareTimeoutHelp: '준비 시간 제한이며 전체 시간 제한에서 제외됩니다.',
pointTimeoutHelp: '측정점별로 시작, 워밍업 및 모든 요청 시간을 측정하며, 남은 전체 시간의 제한도 받습니다.',
totalTimeoutHelp: '준비 후 시작하며 모든 측정점과 대기 시간을 포함합니다. 시간 초과 시 테스트를 중단합니다.',
concurrencyLevels: '동시 실행 단계',
concurrencyHelp: '각 단계를 순서대로 테스트합니다. 각 값은 동시에 진행 중인 요청 수의 상한입니다.',
requestCountMode: '요청 수 설정',
fixedRequestMode: '단계별 고정 요청 수',
scaledRequestMode: '동시 실행 수로 계산',
requestsPerPoint: '단계별 요청 수',
requestCountFormula:
'단계별: 고정값 또는 max(동시 실행 수 × 배수, 최소 요청 수). 시나리오 총량: 모든 단계의 합계 × 반복 횟수 (워밍업 제외).',
controlLog: '준비 및 제어 로그',
executionLog: '실행 로그',
pointLog: '측정 지점 #{0} · {1} · 실행 로그',
archivedLog: '{0} (이전 로그 {1})',
tokenLengths: '입력 / 출력 토큰',
kind: '결과 분류',
kindServe: 'serve (일반 테스트)',
kindPD: 'pd (Prefill/Decode 측정)',
saveDetailed: '요청 상세 정보 저장',
saveDetailedHelp:
'각 요청의 길이, 지연 시간, 생성 텍스트 및 오류를 저장합니다. 비활성화해도 요약 지표는 유지되며 결과 파일 크기를 줄일 수 있습니다.',
parameters: '매개변수',
temperatureHelp:
'생성 내용의 무작위성을 제어합니다. 0은 일반적으로 더 안정적인 출력을 제공하며 값이 높을수록 무작위성이 커집니다.',
pointTimeout: '측정점별 제한 시간(초)',
temperature: '샘플링 온도',
requestUrl: 'API 주소',
invalidRequestUrl:
'/chat/completions 또는 /completions로 끝나는 전체 HTTP(S) 주소를 입력하세요. 인증 정보, 쿼리 매개변수, 프래그먼트는 사용할 수 없습니다.',
requestRateHelp: '현재 시나리오에만 적용됩니다. 무제한 속도도 동시 실행 제한을 따르며 소수 QPS를 지원합니다.',
tokenizerModeHelp:
'사용 가능한 모드는 벤치마크 이미지의 vLLM 버전에 따라 다릅니다. 작업 준비 중 토크나이저 로드를 확인합니다.',
invalidBaseUrl: '인증 정보, 쿼리, 프래그먼트가 없는 유효한 HTTP(S) URL을 입력하세요.',
invalidEndpoint:
'선택한 프로토콜에 맞고 {0}(으)로 끝나는 경로를 입력하세요. 쿼리 매개변수나 프래그먼트는 제외하세요.',
invalidTokenizer: '절대 디렉터리 경로나 저장소 ID를 입력하세요. 상대 경로, 쉼표, 줄바꿈은 지원하지 않습니다.',
invalidRequestRate: '무제한을 선택하거나 0보다 큰 유한한 수를 입력하세요. 소수도 지원합니다.',
invalidCaCert: '기본 노드에 있는 CA 인증서의 절대 파일 경로를 입력하세요. 쉼표나 줄바꿈은 사용할 수 없습니다.',
bodyOverride: '추가 본문에서 {0}을 덮어쓸 수 없습니다.',
invalidBody: '유효한 JSON 객체를 입력하세요.',
invalidConcurrencies: '동시 실행 값은 1~100개이며 각 값은 1~100000의 정수여야 합니다.',
emptyRequestCount: '동시 실행 수로 계산할 때 배수와 최소 요청 수를 모두 0으로 설정할 수 없습니다.',
requestCountLimit: '측정점별 계산된 요청 수는 1000000을 초과할 수 없습니다.',
contextExceeded: '입력·출력 토큰과 템플릿 예약량의 합이 컨텍스트 한도를 초과합니다.',
planLimit: '시나리오는 1~100개, 전체 측정점은 2000개 이하여야 합니다.',
caCertHelp:
'HTTPS 서비스를 검증할 CA 인증서 파일을 주 서버에서 선택하세요. 비워 두면 시스템 인증서를 사용합니다.',
extraBody: '추가 요청 본문',
entryKey: '매개변수 이름',
entryValue: '매개변수 값',
tokenizerMode: '토크나이저 모드',
requireIdle: '시작 전 서비스 유휴 상태 확인',
requireIdleHelp:
'워밍업과 부하 테스트 전에 /metrics를 한 번 확인합니다. 서비스가 사용 중이거나 유휴 상태를 확인할 수 없으면 중지합니다. 테스트 중 다른 트래픽을 차단하지는 않습니다.',
trustRemoteCode: '원격 코드 신뢰',
trustRemoteCodeHelp:
'모델 저장소의 사용자 정의 토크나이저 코드를 벤치마크 컨테이너에서 실행하도록 허용합니다. 필요하고 출처를 신뢰할 때만 켜세요.',
tokenizerModes: { auto: '자동 (권장)', slow: '느린 토크나이저', mistral: 'Mistral 전용' },
quickDescription: '단일 요청으로 연결과 측정 과정을 확인합니다.',
regularDescription: '일상적인 성능 테스트를 위한 짧은 입력, 중간 입력, 긴 입력 부하입니다.',
fullDescription: '긴 입력과 공유 접두사를 포함하여 여러 시나리오와 동시 요청 수로 반복 측정합니다.',
customDescription: '시나리오 하나로 시작하여 매개변수를 설정하고 필요에 따라 추가합니다.',
scenarios: '시나리오',
builtIn: '기본 제공',
builtInHelp: '기본 제공 계획은 수정하거나 삭제할 수 없습니다. 테스트 생성 시 복사본을 조정하세요.',
modified: '수정됨',
imageExists: '로컬에 있음',
imageNeedsPull: '다운로드 필요',
remark: '비고',
source: '로컬 서비스',
mode: '테스트 계획',
quick: '빠른 확인',
regular: '일반',
full: '전체',
custom: '사용자 지정',
addScenario: '시나리오 추가',
advanced: '고급 설정',
points: '측정점',
warmups: '준비 요청 수',
fixedRequests: '고정 요청 수',
multiplier: '동시 실행 수당 요청 배수',
minRequests: '최소 요청 수',
trials: '반복 횟수',
prefixRatio: '공유 접두사 비율',
totalTimeout: '전체 제한 시간 (초)',
prepareTimeout: '준비 제한 시간 (초)',
interval: '측정 간격 (초)',
context: '컨텍스트 한도 (0: 알 수 없음)',
reserve: '템플릿 예약 토큰',
storage: '저장 한도 (MiB)',
stopOnError: '오류 발생 시 후속 측정 중지',
caCert: 'CA 인증서 경로',
validTrials: '유효 / 전체 횟수',
retry: '선택한 측정점 재시도',
environment: '실행 환경',
plan: '고정 설정',
clientHelp:
'클라이언트 컨테이너는 주 노드에서 기존 서비스에 요청을 보냅니다. 추론 서비스를 시작하거나 GPU를 할당하지 않습니다.',
retryHelp: '선택한 비정상 측정점만 같은 매개변수와 새 시드로 재시도합니다.',
secretHelp: '복사본에는 API 키와 헤더가 없습니다. 필요한 경우 다시 입력하세요.',
resultHelp:
'요청 이상이 없는 성공한 실행만 중앙값과 최솟값/최댓값에 포함합니다. P99는 각 실행 P99의 중앙값입니다. 캐시와 외부 트래픽은 통제되지 않습니다.',
},
benchmarkOverview: {
noResults: '유효한 테스트 결과가 없습니다. 실패 원인과 로그를 확인하세요.',
noGroupResults: '이 조합에 유효한 결과가 없습니다. 시나리오 또는 동시 실행 수를 변경하세요.',
partial: '부분 결과: 테스트가 완전히 성공하지 않았습니다. 유효한 측정점만 반영합니다.',
highlights: '주요 지표',
aggregate: '현재 조합의 유효한 실행 중앙값입니다. 지표를 클릭하면 상세 통계를 볼 수 있습니다.',
trend: '동시 실행 수별 추세',
std: '표준 편차',
min: '최솟값',
max: '최댓값',
tpot: '출력 토큰당 시간',
tpotHelp: '첫 토큰 이후 출력 토큰 하나를 생성하는 평균 시간입니다.',
itl: '토큰 간 지연',
itlHelp: '스트리밍 응답에서 연속 토큰 사이의 시간입니다.',
e2el: '종단 간 지연',
e2elHelp: '요청을 전송한 시점부터 전체 응답 수신 완료까지의 시간입니다.',
requestHelp: '초당 완료된 요청 수입니다.',
usageHelp:
'현재 조합의 유효한 실행 중앙값이며 전체 테스트의 요청 합계가 아닙니다. 비정상 실행은 측정점 탭에서 확인하세요.',
duration: '실행 시간',
throughput: '처리량',
latency: '지연',
usage: '요청 및 사용량(유효한 실행)',
},
benchmarkTokenizer: {
title: '토크나이저 관리',
local: '로컬 디렉터리 등록',
upload: '토크나이저 패키지 업로드',
uploadHelp:
'모델 가중치가 없는 ZIP / tar.gz 파일을 업로드하세요. 압축 파일은 128 MiB, 압축 해제 후에는 256 MiB까지 가능합니다.',
source: '가져오기 방식',
deleteLocal: '토크나이저 “{0}”의 등록을 삭제할까요? 원본 디렉터리와 파일은 유지됩니다.',
deleteUpload: '토크나이저 “{0}” 및 업로드한 파일을 삭제할까요? 기존 테스트 보고서는 유지됩니다.',
select: '토크나이저 선택',
selectHelp:
'오프라인 환경에서는 관리 목록에서 선택하세요. 사용자 지정 입력은 로컬 디렉터리나 저장소 이름도 지원합니다.',
},
container: { container: {
createByCommand: '명령으로 생성', createByCommand: '명령으로 생성',
importContainerBackupTip: '컨테이너 백업 파일 가져오기, .tar.gz 형식만 지원합니다', importContainerBackupTip: '컨테이너 백업 파일 가져오기, .tar.gz 형식만 지원합니다',
@@ -2906,6 +3056,20 @@ const message = {
remote: '원격 SSH', remote: '원격 SSH',
}, },
setting: { setting: {
panelCache: {
title: '캐시 설정',
tabs: '탭 캐시',
pagination: '페이지 이동 캐시',
refresh: '새로 고침 간격 캐시',
search: '검색 입력 캐시',
duration: '캐시 유지 기간',
'24h': '24시간',
week: '1주',
month: '1개월',
forever: '무기한',
helper: '유효 기간은 상태를 마지막으로 변경한 시점부터 계산됩니다. 만료되면 기본값으로 돌아갑니다.',
},
panel: '패널', panel: '패널',
user: '패널 사용자', user: '패널 사용자',
userChange: '패널 사용자 변경', userChange: '패널 사용자 변경',
@@ -3613,6 +3777,7 @@ const message = {
link: '링크', link: '링크',
showCurrentArch: '현재 서버 아키텍처 애플리케이션', showCurrentArch: '현재 서버 아키텍처 애플리케이션',
syncLocalApp: '로컬 애플리케이션 동기화', syncLocalApp: '로컬 애플리케이션 동기화',
localAppSyncToNodes: '로컬 앱 동기화 시 하위 노드도 동기화',
memoryRequiredHelper: '현재 애플리케이션은 {0} 메모리가 필요합니다', memoryRequiredHelper: '현재 애플리케이션은 {0} 메모리가 필요합니다',
gpuConfig: 'GPU 지원 활성화', gpuConfig: 'GPU 지원 활성화',
gpuConfigHelper: gpuConfigHelper:
@@ -4159,7 +4324,8 @@ const message = {
domainHelper: '줄당 하나의 도메인 이름, * 및 IP 주소 지원', domainHelper: '줄당 하나의 도메인 이름, * 및 IP 주소 지원',
pushDir: '로컬 디렉토리로 인증서 푸시', pushDir: '로컬 디렉토리로 인증서 푸시',
dir: '디렉토리', dir: '디렉토리',
pushDirHelper: '인증서 파일 "fullchain.pem" 및 키 파일 "privkey.pem"이 이 디렉토리에 생성됩니다.', pushDirHelper:
'여러 디렉터리를 선택하거나 한 줄에 하나씩 입력하세요. 각 디렉터리에 "fullchain.pem"과 "privkey.pem" 파일이 생성됩니다.',
organizationDetail: '조직 세부 정보', organizationDetail: '조직 세부 정보',
fromWebsite: '웹사이트에서 가져오기', fromWebsite: '웹사이트에서 가져오기',
dnsMauanlHelper: '수동 해석 모드에서는 생성 후 신청 버튼을 클릭하여 DNS 해석 값을 얻어야 합니다.', dnsMauanlHelper: '수동 해석 모드에서는 생성 후 신청 버튼을 클릭하여 DNS 해석 값을 얻어야 합니다.',
+172 -9
View File
@@ -487,6 +487,7 @@ const message = {
recommend: 'ແນະນຳ', recommend: 'ແນະນຳ',
dir: 'ໄດເຣັກທໍຣີ', dir: 'ໄດເຣັກທໍຣີ',
alias: 'ຊື່ຫຼິ້ນ', alias: 'ຊື່ຫຼິ້ນ',
quickDirDelete: 'ຢືນຢັນການລຶບ ({0}) ຫຼືບໍ່?',
quickDir: 'ເຂົ້າເຖິງດ່ວນ', quickDir: 'ເຂົ້າເຖິງດ່ວນ',
minQuickJump: 'ເພີ່ມການເຂົ້າເຖິງດ່ວນຢ່າງໜ້ອຍໜຶ່ງລາຍການ', minQuickJump: 'ເພີ່ມການເຂົ້າເຖິງດ່ວນຢ່າງໜ້ອຍໜຶ່ງລາຍການ',
maxQuickJump: 'ທ່ານສາມາດເພີ່ມການເຂົ້າເຖິງດ່ວນໄດ້ສູງສຸດສີ່ລາຍການ', maxQuickJump: 'ທ່ານສາມາດເພີ່ມການເຂົ້າເຖິງດ່ວນໄດ້ສູງສຸດສີ່ລາຍການ',
@@ -761,6 +762,15 @@ const message = {
}, },
aiTools: { aiTools: {
agents: { agents: {
deepSeekAccessHostHelper:
'ປ້ອນທີ່ຢູ່ IPv4 ຫຼືຊື່ໂຮສທີ່ໃຊ້ໃນບຣາວເຊີ ໂດຍບໍ່ມີໂປຣໂຕຄອນ, ພອດ ຫຼືເສັ້ນທາງ. HTTPS ໃຊ້ໃບຢັ້ງຢືນພາຍໃນຂອງແອັບ.',
deepSeekAuthHelper:
'ຊື່ຜູ້ໃຊ້: ຕົວອັກສອນອັງກິດ, ຕົວເລກ, ຈຸດ, ຂີດລຸ່ມ ຫຼືຂີດກາງ. ລະຫັດຜ່ານ: ຢ່າງໜ້ອຍ 12 ຕົວ ໂດຍບໍ່ມີຊ່ອງຫວ່າງຢູ່ຫົວ/ທ້າຍ ຫຼືຂຶ້ນແຖວໃໝ່.',
octopAuthHelper:
'ລະຫັດຜ່ານ: ຢ່າງໜ້ອຍ 8 ຕົວ ປະກອບດ້ວຍຕົວອັກສອນແລະຕົວເລກ; ຫຼີກລ້ຽງລະຫັດທີ່ໃຊ້ທົ່ວໄປ. ຖ້າຖືກປະຕິເສດ ໃຫ້ເບິ່ງຄຳແນະນຳໃນບັນທຶກຂອງແອັບ.',
initialCredentials: 'ຂໍ້ມູນເຂົ້າລະບົບເບື້ອງຕົ້ນ; ຖ້າປ່ຽນໃນແອັບແລ້ວ ໃຫ້ໃຊ້ລະຫັດປັດຈຸບັນ.',
nativeAgentAccessUnavailable:
'ການຕິດຕັ້ງທີ່ມີຢູ່ບໍ່ມີທີ່ຢູ່ ຫຼືພອດ HTTPS ທີ່ເຂົ້າກັນໄດ້. ກະລຸນາກວດສອບການຕັ້ງຄ່າແອັບ.',
agent: 'ຕົວແທນ (Agent)', agent: 'ຕົວແທນ (Agent)',
account: 'ບັນຊີໂມເດວ', account: 'ບັນຊີໂມເດວ',
batchOperation: 'ດຳເນີນການແບບກຸ່ມ', batchOperation: 'ດຳເນີນການແບບກຸ່ມ',
@@ -1451,36 +1461,32 @@ const message = {
statusWaiting: 'ກຳລັງລໍຖ້າ', statusWaiting: 'ກຳລັງລໍຖ້າ',
base: 'ຂໍ້ມູນພື້ນຖານ', base: 'ຂໍ້ມູນພື້ນຖານ',
backend: 'Backend', backend: 'Backend',
baseUrl: 'Base URL',
endpoint: 'Endpoint', endpoint: 'Endpoint',
tokenizer: 'Tokenizer', tokenizer: 'Tokenizer',
tokenizerPlaceholder: tokenizerPlaceholder: 'ປ້ອນເສັ້ນທາງໂຟນເດີແບບສົມບູນໃນໂນດຫຼັກ ຫຼື ID ຄັງ Hugging Face',
'ເລືອກ ຫຼື ປ້ອນໂຟນເດີ tokenizer ພາຍໃນເຄື່ອງ, ເຊັ່ນ: /opt/1panel/tokenizers/DeepSeek-V3',
runConfig: 'ການກຳນົດຄ່າການລັນ', runConfig: 'ການກຳນົດຄ່າການລັນ',
rawResult: 'ຜົນລັບດິບ (Raw Result)', rawResult: 'ຜົນລັບດິບ (Raw Result)',
resultMetrics: 'ຕົວຊີ້ວັດຜົນລັບ', resultMetrics: 'ຕົວຊີ້ວັດຜົນລັບ',
contextTokens: 'ຄວາມຍາວ Context', contextTokens: 'ຄວາມຍາວ Context',
contextTokensHelper: 'ຂີດຈຳກັດ input token ບວກກັບ output token ສຳລັບການທົດສອບນີ້.',
outputThroughput: 'ອັດຕາການສົ່ງອອກ', outputThroughput: 'ອັດຕາການສົ່ງອອກ',
outputThroughputHelper: 'ຈຳນວນ output tokens ທີ່ສ້າງໄດ້ຕໍ່ວິນາທີ. ຍິ່ງສູງຍິ່ງໄວ.', outputThroughputHelper: 'ຈຳນວນ output tokens ທີ່ສ້າງໄດ້ຕໍ່ວິນາທີ. ຍິ່ງສູງຍິ່ງໄວ.',
totalThroughput: 'ອັດຕາການປະມວນຜົນລວມ', totalThroughput: 'ອັດຕາການປະມວນຜົນລວມ',
totalThroughputHelper: 'ຈຳນວນ input ແລະ output tokens ທີ່ປະມວນຜົນຕໍ່ວິນາທີ. ໃຊ້ວັດແທກຄວາມສາມາດໂດຍລວມ.', totalThroughputHelper: 'ຈຳນວນ input ແລະ output tokens ທີ່ປະມວນຜົນຕໍ່ວິນາທີ. ໃຊ້ວັດແທກຄວາມສາມາດໂດຍລວມ.',
firstTokenLatency: 'ຄວາມໜ່ວງຂອງ Token ທຳອິດ', firstTokenLatency: 'ຄວາມໜ່ວງຂອງ Token ທຳອິດ',
firstTokenLatencyHelper: 'ເວລາຕັ້ງແຕ່ສົ່ງຄຳຂໍຈົນເຖິງການໄດ້ຮັບ token ທຳອິດ. ຍິ່ງຕ່ຳຍິ່ງຕອບສະໜອງໄວ.', firstTokenLatencyHelper: 'ເວລາຕັ້ງແຕ່ສົ່ງຄຳຂໍຈົນເຖິງການໄດ້ຮັບ token ທຳອິດ. ຍິ່ງຕ່ຳຍິ່ງຕອບສະໜອງໄວ.',
tokenValueHelper: 'ປ້ອນຕົວເລກຈຳນວນເຕັມ ຫຼື ຄ່າ k, ເຊັ່ນ: 512, 1k, 32k',
numPrompts: 'ຈຳນວນ Prompt', numPrompts: 'ຈຳນວນ Prompt',
concurrency: 'ການເຮັດວຽກພ້ອມກັນ', concurrency: 'ການເຮັດວຽກພ້ອມກັນ',
requestRate: 'ອັດຕາຄຳຂໍ', requestRate: 'ອັດຕາຄຳຂໍ',
requestRateUnlimited: 'ບໍ່ຈຳກັດ (max throughput)', requestRateUnlimited: 'ບໍ່ຈຳກັດ (max throughput)',
requestRateCustom: 'ກຳນົດ QPS ເອງ',
requestRateCustomPlaceholder: 'ຄຳຂໍຕໍ່ວິນາທີ, ເຊັ່ນ: 2.5', requestRateCustomPlaceholder: 'ຄຳຂໍຕໍ່ວິນາທີ, ເຊັ່ນ: 2.5',
successfulRequests: 'ຄຳຂໍທີ່ສຳເລັດ', successfulRequests: 'ຄຳຂໍທີ່ສຳເລັດ',
requestThroughput: 'ອັດຕາຄຳຮ້ອງຂໍ', requestThroughput: 'ອັດຕາຄຳຮ້ອງຂໍ',
mean: 'ຄ່າສະເລ່ຍ', mean: 'ຄ່າສະເລ່ຍ',
median: 'ຄ່າມັດທະຍະຖານ', median: 'ຄ່າມັດທະຍະຖານ',
timeout: 'ໝົດເວລາ (ວິນາທີ)',
image: 'vLLM Image', image: 'vLLM Image',
ignoreEos: 'ລະເວັ້ນ EOS', ignoreEos: 'ລະເວັ້ນໂທເຄັນສິ້ນສຸດ (EOS)',
ignoreEosHelp:
'ປ້ອງກັນການຢຸດກ່ອນກຳນົດຍ້ອນ EOS ເພື່ອທົດສອບຄວາມຍາວຜົນລັບທີ່ຕັ້ງໄວ້. ເງື່ອນໄຂຢຸດອື່ນຍັງມີຜົນ ແລະຕ້ອງມີການຮອງຮັບຈາກເຊີບເວີ.',
extraHeaders: 'Headers ເພີ່ມເຕີມ', extraHeaders: 'Headers ເພີ່ມເຕີມ',
}, },
vllmMonitor: { vllmMonitor: {
@@ -1685,6 +1691,147 @@ const message = {
modelType: 'ປະເພດໂມເດວ', modelType: 'ປະເພດໂມເດວ',
}, },
}, },
benchmarkPlan: {
prepareTimeoutHelp: 'ເວລາຈຳກັດການກຽມການ ບໍ່ນັບໃນເວລາທັງຮອບ.',
pointTimeoutHelp: 'ຈັບເວລາແຍກຕໍ່ຈຸດ ລວມການເລີ່ມ, ອຸ່ນເຄື່ອງ ແລະ ທຸກຄຳຮ້ອງຂໍ; ຈຳກັດດ້ວຍເວລາທັງຮອບທີ່ເຫຼືອ.',
totalTimeoutHelp: 'ເລີ່ມຫຼັງກຽມການ ລວມທຸກຈຸດແລະໄລຍະຫ່າງ. ຢຸດທົດສອບເມື່ອໝົດເວລາ.',
concurrencyLevels: 'ລະດັບການເຮັດວຽກພ້ອມກັນ',
concurrencyHelp: 'ທົດສອບແຕ່ລະລະດັບຕາມລຳດັບ. ແຕ່ລະຄ່າກຳນົດຈຳນວນຄຳຮ້ອງຂໍສູງສຸດທີ່ດຳເນີນພ້ອມກັນ.',
requestCountMode: 'ຕັ້ງຄ່າຈຳນວນຄຳຮ້ອງຂໍ',
fixedRequestMode: 'ຈຳນວນຄົງທີ່ຕໍ່ລະດັບ',
scaledRequestMode: 'ຄຳນວນຕາມຈຳນວນທີ່ເຮັດວຽກພ້ອມກັນ',
requestsPerPoint: 'ຄຳຮ້ອງຂໍຕໍ່ລະດັບ',
requestCountFormula:
'ຕໍ່ລະດັບ: ຄ່າຄົງທີ່ ຫຼື max(ຈຳນວນພ້ອມກັນ × ຕົວຄູນ, ຄຳຮ້ອງຂໍຂັ້ນຕ່ຳ). ລວມຕໍ່ສະຖານະການ: ຜົນລວມທຸກລະດັບ × ຈຳນວນຮອບຊ້ຳ, ບໍ່ລວມອຸ່ນເຄື່ອງ.',
controlLog: 'ບັນທຶກການກະກຽມ ແລະ ຄວບຄຸມ',
executionLog: 'ບັນທຶກການດຳເນີນງານ',
pointLog: 'ຈຸດທົດສອບ #{0} · {1} · ບັນທຶກການດຳເນີນງານ',
archivedLog: '{0} (ປະຫວັດ {1})',
tokenLengths: 'ໂທເຄັນຂາເຂົ້າ / ຂາອອກ',
kind: 'ໝວດໝູ່ຜົນລັບ',
kindServe: 'serve (ການທົດສອບທົ່ວໄປ)',
kindPD: 'pd (ການວັດແທກ Prefill/Decode)',
saveDetailed: 'ບັນທຶກລາຍລະອຽດຄຳຮ້ອງ',
saveDetailedHelp:
'ບັນທຶກຄວາມຍາວ, ຄວາມລ່າຊ້າ, ຂໍ້ຄວາມທີ່ສ້າງ ແລະຂໍ້ຜິດພາດຂອງແຕ່ລະຄຳຮ້ອງ. ເມື່ອປິດ ຍັງຮັກສາຄ່າສະຫຼຸບໄວ້ ແລະຫຼຸດຂະໜາດໄຟລ໌ຜົນລັບ.',
parameters: 'ພາລາມິເຕີ',
temperatureHelp: 'ຄວບຄຸມຄວາມສຸ່ມຂອງຜົນລັບ. ຄ່າ 0 ມັກໃຫ້ຜົນລັບທີ່ຄົງທີ່ກວ່າ; ຄ່າສູງຂຶ້ນຈະເພີ່ມຄວາມສຸ່ມ.',
pointTimeout: 'ເວລາຈຳກັດຕໍ່ຈຸດ (ວິນາທີ)',
temperature: 'ອຸນຫະພູມການສຸ່ມ',
requestUrl: 'ທີ່ຢູ່ API',
invalidRequestUrl:
'ປ້ອນທີ່ຢູ່ HTTP(S) ເຕັມທີ່ລົງທ້າຍດ້ວຍ /chat/completions ຫຼື /completions ໂດຍບໍ່ມີຂໍ້ມູນເຂົ້າລະບົບ, ພາລາມິເຕີສອບຖາມ ຫຼື ສ່ວນຫຼັງ #.',
requestRateHelp:
'ໃຊ້ກັບສະຖານະການນີ້ເທົ່ານັ້ນ. ອັດຕາບໍ່ຈຳກັດຍັງຢູ່ພາຍໃຕ້ຂີດຈຳກັດພ້ອມກັນ; ຮອງຮັບ QPS ທົດສະນິຍົມ.',
tokenizerModeHelp:
'ໂໝດທີ່ໃຊ້ໄດ້ຂຶ້ນກັບເວີຊັນ vLLM ໃນອິມເມຈທົດສອບ. ການໂຫຼດຕົວແຍກໂທເຄັນຈະຖືກກວດສອບໃນຂັ້ນຕອນກະກຽມວຽກ.',
invalidBaseUrl: 'ປ້ອນ URL HTTP(S) ທີ່ຖືກຕ້ອງ ໂດຍບໍ່ມີຂໍ້ມູນເຂົ້າລະບົບ, ຄຳຖາມ ຫຼືສ່ວນຍ່ອຍ.',
invalidEndpoint:
'ປ້ອນເສັ້ນທາງທີ່ກົງກັບໂປຣໂຕຄໍທີ່ເລືອກ ແລະລົງທ້າຍດ້ວຍ {0} ໂດຍບໍ່ມີພາລາມິເຕີຄຳຖາມ ຫຼືສ່ວນຍ່ອຍຂອງ URL.',
invalidTokenizer: 'ໃຊ້ເສັ້ນທາງໂຟນເດີແບບເຕັມ ຫຼື ID ຄັງເກັບ; ບໍ່ຮອງຮັບເສັ້ນທາງສຳພັນ, ຈຸດຈ້ຳ ຫຼືຂຶ້ນແຖວໃໝ່.',
invalidRequestRate: 'ເລືອກບໍ່ຈຳກັດ ຫຼືປ້ອນຈຳນວນຈຳກັດທີ່ໃຫຍ່ກວ່າ 0; ຮອງຮັບທົດສະນິຍົມ.',
invalidCaCert: 'ປ້ອນເສັ້ນທາງເຕັມຂອງໄຟລ໌ CA ໃນໂນດຫຼັກ ໂດຍບໍ່ມີຈຸດຈ້ຳ ຫຼືຂຶ້ນແຖວໃໝ່.',
bodyOverride: 'ເນື້ອຫາເພີ່ມບໍ່ສາມາດຂຽນທັບ {0}.',
invalidBody: 'ປ້ອນວັດຖຸ JSON ທີ່ຖືກຕ້ອງ.',
invalidConcurrencies: 'ປ້ອນຄ່າພ້ອມກັນ 1–100 ລາຍການ, ແຕ່ລະຄ່າເປັນຈຳນວນເຕັມ 1–100000.',
emptyRequestCount: 'ເມື່ອຄຳນວນຕາມຈຳນວນທີ່ເຮັດວຽກພ້ອມກັນ, ຕົວຄູນແລະຈຳນວນຄຳຮ້ອງຂໍຂັ້ນຕ່ຳບໍ່ສາມາດເປັນ 0 ພ້ອມກັນ.',
requestCountLimit: 'ຄຳຮ້ອງຂໍທີ່ຄຳນວນຕໍ່ຈຸດຕ້ອງບໍ່ເກີນ 1000000.',
contextExceeded: 'ຜົນລວມໂທເຄັນຂາເຂົ້າ, ຂາອອກ ແລະສຳຮອງແມ່ແບບເກີນຂີດຈຳກັດບໍລິບົດ.',
planLimit: 'ຕ້ອງມີ 1–100 ສະຖານະການ ແລະລວມບໍ່ເກີນ 2000 ຈຸດ.',
caCertHelp: 'ເລືອກໄຟລ໌ໃບຢັ້ງຢືນ CA ໃນເຊີບເວີຫຼັກເພື່ອກວດສອບ HTTPS. ປະວ່າງເພື່ອໃຊ້ໃບຢັ້ງຢືນຂອງລະບົບ.',
extraBody: 'ເນື້ອຫາຄຳຮ້ອງຂໍເພີ່ມເຕີມ',
entryKey: 'ຊື່ພາຣາມິເຕີ',
entryValue: 'ຄ່າພາຣາມິເຕີ',
tokenizerMode: 'ໂໝດຕົວແບ່ງໂທເຄັນ',
requireIdle: 'ກວດສອບວ່າບໍລິການວ່າງກ່ອນເລີ່ມ',
requireIdleHelp:
'ກວດສອບ /metrics ໜຶ່ງຄັ້ງກ່ອນອຸ່ນເຄື່ອງ ແລະ ທົດສອບໂຫຼດ. ຢຸດຖ້າບໍລິການບໍ່ວ່າງ ຫຼື ບໍ່ສາມາດຢືນຢັນໄດ້ວ່າວ່າງ. ການກວດສອບນີ້ບໍ່ປ້ອງກັນການຮ້ອງຂໍອື່ນໃນລະຫວ່າງທົດສອບ.',
trustRemoteCode: 'ເຊື່ອຖືລະຫັດທາງໄກ',
trustRemoteCodeHelp:
'ອະນຸຍາດໃຫ້ແລ່ນລະຫັດຕົວແບ່ງໂທເຄັນທີ່ກຳນົດເອງຈາກຄັງໂມເດວໃນຄອນເທນເນີທົດສອບ. ເປີດເມື່ອຈຳເປັນແລະແຫຼ່ງທີ່ມາເຊື່ອຖືໄດ້ເທົ່ານັ້ນ.',
tokenizerModes: { auto: 'ອັດຕະໂນມັດ (ແນະນຳ)', slow: 'ຕົວແບ່ງໂທເຄັນແບບຊ້າ', mistral: 'ຕົວແບ່ງໂທເຄັນ Mistral' },
quickDescription: 'ກວດສອບການເຊື່ອມຕໍ່ແລະການວັດແທກດ້ວຍຄຳຮ້ອງຂໍດຽວ.',
regularDescription: 'ພາລະງານສັ້ນ, ປານກາງ ແລະຍາວ ສຳລັບທົດສອບປະສິດທິພາບປະຈຳວັນ.',
fullDescription:
'ຫຼາຍສະຖານະການ, ຫຼາຍລະດັບຄຳຮ້ອງຂໍພ້ອມກັນ ແລະການວັດແທກຊ້ຳ ລວມທັງຂໍ້ມູນເຂົ້າຍາວແລະຄຳນຳໜ້າຮ່ວມກັນ.',
customDescription: 'ເລີ່ມຈາກໜຶ່ງສະຖານະການ, ຕັ້ງຄ່າພາລາມິເຕີ ແລະເພີ່ມຕາມຄວາມຕ້ອງການ.',
scenarios: 'ສະຖານະການ',
builtIn: 'ມາພ້ອມລະບົບ',
builtInHelp: 'ແຜນທີ່ມາພ້ອມລະບົບແກ້ໄຂຫຼືລຶບບໍ່ໄດ້. ປັບສຳເນົາໃນເວລາສ້າງການທົດສອບ.',
modified: 'ປັບແລ້ວ',
imageExists: 'ມີຢູ່ໃນເຄື່ອງແລ້ວ',
imageNeedsPull: 'ຕ້ອງດາວໂຫຼດ',
remark: 'ໝາຍເຫດ',
source: 'ບໍລິການພາຍໃນເຄື່ອງ',
mode: 'ແຜນທົດສອບ',
quick: 'ກວດສອບດ່ວນ',
regular: 'ປົກກະຕິ',
full: 'ເຕັມຮູບແບບ',
custom: 'ກຳນົດເອງ',
addScenario: 'ເພີ່ມສະຖານະການ',
advanced: 'ການຕັ້ງຄ່າຂັ້ນສູງ',
points: 'ຈຸດວັດແທກ',
warmups: 'ຄຳຮ້ອງຂໍອຸ່ນເຄື່ອງ',
fixedRequests: 'ຄຳຮ້ອງຂໍຄົງທີ່',
multiplier: 'ຕົວຄູນຄຳຮ້ອງຂໍຕໍ່ການເຮັດວຽກພ້ອມກັນ',
minRequests: 'ຄຳຮ້ອງຂໍຕ່ຳສຸດ',
trials: 'ຈຳນວນຊ້ຳ',
prefixRatio: 'ອັດຕາສ່ວນຄຳນຳໜ້າຮ່ວມ',
totalTimeout: 'ເວລາຈຳກັດຮອບ (ວິນາທີ)',
prepareTimeout: 'ເວລາຈຳກັດກະກຽມ (ວິນາທີ)',
interval: 'ໄລຍະຫ່າງຈຸດ (ວິນາທີ)',
context: 'ຂີດຈຳກັດບໍລິບົດ (0: ບໍ່ຮູ້)',
reserve: 'ໂທເຄນສຳຮອງແມ່ແບບ',
storage: 'ຂີດຈຳກັດບ່ອນເກັບ (MiB)',
stopOnError: 'ຢຸດຈຸດຕໍ່ໄປເມື່ອຜິດພາດ',
caCert: 'ເສັ້ນທາງໃບຢັ້ງຢືນ CA',
validTrials: 'ຖືກຕ້ອງ / ທັງໝົດ',
retry: 'ທົດສອບຈຸດທີ່ເລືອກຄືນ',
environment: 'ສະພາບແວດລ້ອມ',
plan: 'ການຕັ້ງຄ່າຄົງທີ່',
clientHelp:
'ຄອນເທນເນີລູກຂ່າຍເຮັດວຽກຢູ່ໂນດຫຼັກ ແລະ ສົ່ງຄຳຮ້ອງຂໍໄປບໍລິການທີ່ມີແລ້ວ. ບໍ່ເລີ່ມບໍລິການອະນຸມານ ແລະ ບໍ່ຕ້ອງການ GPU.',
retryHelp: 'ທົດສອບຄືນສະເພາະຈຸດຜິດປົກກະຕິທີ່ເລືອກ ດ້ວຍພາລາມິເຕີເດີມ ແລະ ຄ່າສຸ່ມໃໝ່.',
secretHelp: 'ສຳເນົາບໍ່ມີລະຫັດ API ແລະ ສ່ວນຫົວ. ກະລຸນາປ້ອນໃໝ່ຖ້າຈຳເປັນ.',
resultHelp:
'ຄ່າກາງ ແລະ ຄ່າຕ່ຳສຸດ/ສູງສຸດໃຊ້ສະເພາະການທົດສອບສຳເລັດທີ່ບໍ່ມີຄຳຮ້ອງຂໍຜິດປົກກະຕິ. P99 ແມ່ນຄ່າກາງຂອງ P99 ແຕ່ລະຄັ້ງ. ແຄຊ ແລະ ການຈະລາຈອນພາຍນອກບໍ່ໄດ້ຄວບຄຸມ.',
},
benchmarkOverview: {
noResults: 'ບໍ່ມີຜົນການທົດສອບທີ່ໃຊ້ໄດ້. ກວດສອບສາເຫດ ແລະ ບັນທຶກ.',
noGroupResults: 'ບໍ່ມີຜົນທີ່ໃຊ້ໄດ້ສຳລັບຊຸດນີ້. ເລືອກສະຖານະການ ຫຼື ຈຳນວນພ້ອມກັນອື່ນ.',
partial: 'ຜົນບາງສ່ວນ: ການທົດສອບບໍ່ສຳເລັດທັງໝົດ. ຕົວຊີ້ວັດລວມສະເພາະຈຸດທີ່ໃຊ້ໄດ້.',
highlights: 'ຕົວຊີ້ວັດຫຼັກ',
aggregate: 'ຄ່າມັດທະຍະຖານຂອງການແລ່ນທີ່ໃຊ້ໄດ້ໃນຊຸດນີ້. ຄລິກຕົວຊີ້ວັດເພື່ອເບິ່ງລາຍລະອຽດ.',
trend: 'ແນວໂນ້ມຕາມຈຳນວນພ້ອມກັນ',
std: 'ສ່ວນບ່ຽງເບນມາດຕະຖານ',
min: 'ຕ່ຳສຸດ',
max: 'ສູງສຸດ',
tpot: 'ເວລາຕໍ່ Token ຂາອອກ',
tpotHelp: 'ເວລາສ້າງສະເລ່ຍຕໍ່ Token ຂາອອກຫຼັງ Token ທຳອິດ.',
itl: 'ຄວາມຊັກຊ້າລະຫວ່າງ Token',
itlHelp: 'ເວລາລະຫວ່າງ Token ຕໍ່ເນື່ອງໃນຄຳຕອບແບບສະຕຣີມ.',
e2el: 'ຄວາມຊັກຊ້າຕັ້ງແຕ່ຕົ້ນຈົນຈົບ',
e2elHelp: 'ເວລາຈາກການສົ່ງຄຳຂໍຈົນໄດ້ຮັບຄຳຕອບຄົບຖ້ວນ.',
requestHelp: 'ຈຳນວນຄຳຂໍທີ່ສຳເລັດຕໍ່ວິນາທີ.',
usageHelp: 'ຄ່າມັດທະຍະຖານຂອງການແລ່ນທີ່ໃຊ້ໄດ້ໃນຊຸດນີ້ ບໍ່ແມ່ນຈຳນວນຄຳຂໍທັງໝົດ. ເບິ່ງການແລ່ນຜິດປົກກະຕິໃນແຖບຈຸດ.',
duration: 'ໄລຍະເວລາການແລ່ນ',
throughput: 'ອັດຕາປະມວນຜົນ',
latency: 'ຄວາມຊັກຊ້າ',
usage: 'ຄຳຂໍ ແລະ ການໃຊ້ງານ (ການແລ່ນທີ່ໃຊ້ໄດ້)',
},
benchmarkTokenizer: {
title: 'ຈັດການຕົວແຍກໂທເຄັນ',
local: 'ລົງທະບຽນໂຟນເດີໃນເຄື່ອງ',
upload: 'ອັບໂຫຼດຊຸດຕົວແຍກໂທເຄັນ',
uploadHelp: 'ອັບໂຫຼດ ZIP / tar.gz ທີ່ບໍ່ມີນ້ຳໜັກໂມເດລ. ສູງສຸດ 128 MiB ກ່ອນແຕກ ແລະ 256 MiB ຫຼັງແຕກ.',
source: 'ວິທີນຳເຂົ້າ',
deleteLocal: 'ລຶບການລົງທະບຽນ «{0}» ບໍ? ໂຟນເດີ ແລະ ໄຟລ໌ຕົ້ນສະບັບຈະຖືກເກັບໄວ້.',
deleteUpload: 'ລຶບ «{0}» ແລະ ໄຟລ໌ທີ່ອັບໂຫຼດບໍ? ລາຍງານທົດສອບເກົ່າຈະຖືກເກັບໄວ້.',
select: 'ເລືອກຕົວແບ່ງໂທເຄັນ',
selectHelp: 'ສຳລັບອອບລາຍ ໃຫ້ເລືອກຈາກລາຍການ. ການປ້ອນເອງຮອງຮັບໂຟນເດີ ຫຼື ຊື່ຄັງ.',
},
container: { container: {
createByCommand: 'ສ້າງດ້ວຍຄຳສັ່ງ', createByCommand: 'ສ້າງດ້ວຍຄຳສັ່ງ',
importContainerBackupTip: 'ນຳເຂົ້າໄຟລ໌ສຳຮອງຄອນເທນເນີ, ຮອງຮັບສະເພາະ .tar.gz', importContainerBackupTip: 'ນຳເຂົ້າໄຟລ໌ສຳຮອງຄອນເທນເນີ, ຮອງຮັບສະເພາະ .tar.gz',
@@ -2887,6 +3034,20 @@ const message = {
remote: 'SSH ທາງໄກ', remote: 'SSH ທາງໄກ',
}, },
setting: { setting: {
panelCache: {
title: 'ການຕັ້ງຄ່າແຄດ',
tabs: 'ແຄດແຖບ',
pagination: 'ແຄດການແບ່ງໜ້າ',
refresh: 'ແຄດຊ່ວງເວລາຣີເຟຣຊ',
search: 'ແຄດຂໍ້ຄວາມຄົ້ນຫາ',
duration: 'ໄລຍະເວລາເກັບແຄດ',
'24h': '24 ຊົ່ວໂມງ',
week: 'ໜຶ່ງອາທິດ',
month: 'ໜຶ່ງເດືອນ',
forever: 'ຖາວອນ',
helper: 'ໄລຍະເວລາຈະນັບຈາກການປ່ຽນສະຖານະຄັ້ງຫຼ້າສຸດ. ເມື່ອໝົດອາຍຸ ຈະກັບຄືນເປັນຄ່າເລີ່ມຕົ້ນ.',
},
panel: 'ພາເນລ', panel: 'ພາເນລ',
user: 'ຜູ້ໃຊ້ພາເນລ', user: 'ຜູ້ໃຊ້ພາເນລ',
userChange: 'ປ່ຽນຜູ້ໃຊ້ພາເນລ', userChange: 'ປ່ຽນຜູ້ໃຊ້ພາເນລ',
@@ -3590,6 +3751,7 @@ const message = {
link: 'ລິ້ງ', link: 'ລິ້ງ',
showCurrentArch: 'ສະຖາປັດຕະຍະກຳ', showCurrentArch: 'ສະຖາປັດຕະຍະກຳ',
syncLocalApp: 'ຊິງຄ໌ແອັບໃນເຄື່ອງ', syncLocalApp: 'ຊິງຄ໌ແອັບໃນເຄື່ອງ',
localAppSyncToNodes: 'ຮວມໂນດລູກເມື່ອຊິງຄ໌ແອັບໃນເຄື່ອງ',
memoryRequiredHelper: 'ຄວາມຕ້ອງການໜ່ວຍຄວາມຈຳຂອງແອັບປະຈຸບັນ {0}', memoryRequiredHelper: 'ຄວາມຕ້ອງການໜ່ວຍຄວາມຈຳຂອງແອັບປະຈຸບັນ {0}',
gpuConfig: 'ເປີດໃຊ້ການຮອງຮັບ GPU', gpuConfig: 'ເປີດໃຊ້ການຮອງຮັບ GPU',
gpuConfigHelper: gpuConfigHelper:
@@ -4131,7 +4293,8 @@ const message = {
domainHelper: 'ໜຶ່ງໂດເມນຕໍ່ແຖວ, ຮອງຮັບ * ແລະ ທີ່ຢູ່ IP', domainHelper: 'ໜຶ່ງໂດເມນຕໍ່ແຖວ, ຮອງຮັບ * ແລະ ທີ່ຢູ່ IP',
pushDir: 'ສົ່ງໃບຢັ້ງຢືນໄປຍັງໂຟນເດີໃນເຄື່ອງ', pushDir: 'ສົ່ງໃບຢັ້ງຢືນໄປຍັງໂຟນເດີໃນເຄື່ອງ',
dir: 'ໂຟນເດີ', dir: 'ໂຟນເດີ',
pushDirHelper: 'ໄຟລ໌ "fullchain.pem" ແລະ "privkey.pem" ຈະຖືກສ້າງໃນໂຟນເດີນີ້.', pushDirHelper:
'ເລືອກຫຼາຍໂຟນເດີ ຫຼື ປ້ອນໜຶ່ງໂຟນເດີຕໍ່ແຖວ. ໄຟລ໌ "fullchain.pem" ແລະ "privkey.pem" ຈະຖືກສ້າງໃນແຕ່ລະໂຟນເດີ.',
organizationDetail: 'ລາຍລະອຽດອົງກອນ', organizationDetail: 'ລາຍລະອຽດອົງກອນ',
fromWebsite: 'ຈາກເວັບໄຊ', fromWebsite: 'ຈາກເວັບໄຊ',
dnsMauanlHelper: 'ໃນໂໝດຈັດການ DNS ດ້ວຍຕົນເອງ, ທ່ານຕ້ອງຄລິກປຸ່ມ "ຂໍໃບຢັ້ງຢືນ" ຫຼັງຈາກສ້າງເພື່ອຮັບຄ່າ DNS', dnsMauanlHelper: 'ໃນໂໝດຈັດການ DNS ດ້ວຍຕົນເອງ, ທ່ານຕ້ອງຄລິກປຸ່ມ "ຂໍໃບຢັ້ງຢືນ" ຫຼັງຈາກສ້າງເພື່ອຮັບຄ່າ DNS',
+182 -9
View File
@@ -499,6 +499,7 @@ const message = {
recommend: 'Disyorkan', recommend: 'Disyorkan',
dir: 'Direktori', dir: 'Direktori',
alias: 'Alias', alias: 'Alias',
quickDirDelete: 'Sahkan pemadaman ({0})?',
quickDir: 'Akses Pantas', quickDir: 'Akses Pantas',
minQuickJump: 'Tambah sekurang-kurangnya satu entri akses pantas', minQuickJump: 'Tambah sekurang-kurangnya satu entri akses pantas',
maxQuickJump: 'Anda boleh menambah sehingga empat entri akses pantas', maxQuickJump: 'Anda boleh menambah sehingga empat entri akses pantas',
@@ -781,6 +782,15 @@ const message = {
}, },
aiTools: { aiTools: {
agents: { 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', agent: 'Agen',
account: 'Akaun model', account: 'Akaun model',
batchOperation: 'Operasi pukal', batchOperation: 'Operasi pukal',
@@ -1496,16 +1506,13 @@ const message = {
statusWaiting: 'Menunggu', statusWaiting: 'Menunggu',
base: 'Maklumat Asas', base: 'Maklumat Asas',
backend: 'Backend', backend: 'Backend',
baseUrl: 'URL Asas',
endpoint: 'Endpoint', endpoint: 'Endpoint',
tokenizer: 'Tokenizer', tokenizer: 'Tokenizer',
tokenizerPlaceholder: tokenizerPlaceholder: 'Masukkan laluan direktori mutlak pada nod utama atau ID repositori Hugging Face',
'Pilih atau masukkan direktori tokenizer tempatan, cth. /opt/1panel/tokenizers/DeepSeek-V3',
runConfig: 'Konfigurasi Jalan', runConfig: 'Konfigurasi Jalan',
rawResult: 'Keputusan Mentah', rawResult: 'Keputusan Mentah',
resultMetrics: 'Metrik Keputusan', resultMetrics: 'Metrik Keputusan',
contextTokens: 'Panjang Konteks', contextTokens: 'Panjang Konteks',
contextTokensHelper: 'Had token input ditambah had token output untuk penanda aras ini.',
outputThroughput: 'Throughput Output', outputThroughput: 'Throughput Output',
outputThroughputHelper: 'Token output dijana setiap saat. Lebih tinggi bermaksud penjanaan lebih pantas.', outputThroughputHelper: 'Token output dijana setiap saat. Lebih tinggi bermaksud penjanaan lebih pantas.',
totalThroughput: 'Jumlah Throughput', totalThroughput: 'Jumlah Throughput',
@@ -1514,20 +1521,19 @@ const message = {
firstTokenLatency: 'Latensi Token Pertama', firstTokenLatency: 'Latensi Token Pertama',
firstTokenLatencyHelper: firstTokenLatencyHelper:
'Masa dari menghantar permintaan hingga menerima token pertama. Lebih rendah bermaksud respons lebih pantas.', 'Masa dari menghantar permintaan hingga menerima token pertama. Lebih rendah bermaksud respons lebih pantas.',
tokenValueHelper: 'Masukkan integer positif atau nilai k, cth. 512, 1k, 32k',
numPrompts: 'Prompt', numPrompts: 'Prompt',
concurrency: 'Keserentakan', concurrency: 'Keserentakan',
mean: 'Min', mean: 'Min',
median: 'Median', median: 'Median',
requestRate: 'Kadar Permintaan', requestRate: 'Kadar Permintaan',
requestRateUnlimited: 'Tanpa had (throughput maksimum)', requestRateUnlimited: 'Tanpa had (throughput maksimum)',
requestRateCustom: 'QPS tersuai',
requestRateCustomPlaceholder: 'Permintaan sesaat, cth. 2.5', requestRateCustomPlaceholder: 'Permintaan sesaat, cth. 2.5',
successfulRequests: 'Permintaan berjaya', successfulRequests: 'Permintaan berjaya',
requestThroughput: 'Throughput Permintaan', requestThroughput: 'Throughput Permintaan',
timeout: 'Tamat Masa (saat)',
image: 'Imej vLLM', image: 'Imej vLLM',
ignoreEos: 'Abaikan EOS', ignoreEos: 'Abaikan token akhir jujukan (EOS)',
ignoreEosHelp:
'Mengelakkan penghentian awal akibat EOS untuk menguji panjang output yang ditetapkan. Syarat henti lain masih terpakai; sokongan pelayan diperlukan.',
extraHeaders: 'Pengepala Tambahan', extraHeaders: 'Pengepala Tambahan',
}, },
vllmMonitor: { vllmMonitor: {
@@ -1741,6 +1747,157 @@ const message = {
modelType: 'Jenis model', modelType: 'Jenis model',
}, },
}, },
benchmarkPlan: {
prepareTimeoutHelp: 'Had masa persediaan, tidak termasuk had masa keseluruhan.',
pointTimeoutHelp:
'Pemasa berasingan setiap titik, termasuk permulaan, pemanasan dan semua permintaan; dihadkan oleh baki masa keseluruhan.',
totalTimeoutHelp:
'Bermula selepas persediaan, meliputi semua titik dan sela. Ujian berhenti apabila tamat masa.',
concurrencyLevels: 'Tahap keserentakan',
concurrencyHelp:
'Tahap diuji mengikut urutan. Setiap nilai mengehadkan bilangan permintaan yang berjalan serentak.',
requestCountMode: 'Tetapan bilangan permintaan',
fixedRequestMode: 'Permintaan tetap setiap tahap',
scaledRequestMode: 'Kira mengikut keserentakan',
requestsPerPoint: 'Permintaan setiap tahap',
requestCountFormula:
'Setiap tahap: nilai tetap atau max(keserentakan × pengganda, permintaan minimum). Jumlah senario: jumlah semua tahap × ulangan, tidak termasuk pemanasan.',
controlLog: 'Log persediaan dan kawalan',
executionLog: 'Log pelaksanaan',
pointLog: 'Titik #{0} · {1} · Log pelaksanaan',
archivedLog: '{0} (arkib {1})',
tokenLengths: 'Token input / output',
kind: 'Kategori hasil',
kindServe: 'serve (ujian biasa)',
kindPD: 'pd (pengukuran Prefill/Decode)',
saveDetailed: 'Simpan butiran permintaan',
saveDetailedHelp:
'Simpan panjang, kependaman, teks yang dijana dan ralat bagi setiap permintaan. Metrik ringkasan dikekalkan apabila dimatikan, sekali gus mengurangkan saiz fail hasil.',
parameters: 'Parameter',
temperatureHelp:
'Mengawal kerawakan output. Sifar biasanya memberikan output yang lebih stabil; nilai lebih tinggi meningkatkan kerawakan.',
pointTimeout: 'Had masa setiap titik (saat)',
temperature: 'Suhu pensampelan',
requestUrl: 'URL API',
invalidRequestUrl:
'Masukkan URL HTTP(S) penuh yang berakhir dengan /chat/completions atau /completions, tanpa kelayakan log masuk, parameter pertanyaan atau fragmen.',
requestRateHelp:
'Terpakai hanya pada senario ini. Kadar tanpa had masih mematuhi had keserentakan; QPS perpuluhan disokong.',
tokenizerModeHelp:
'Mod yang tersedia bergantung pada versi vLLM dalam imej penanda aras. Pemuatan pentoken disemak semasa penyediaan tugas.',
invalidBaseUrl: 'Masukkan URL HTTP(S) yang sah tanpa kelayakan, pertanyaan atau fragmen.',
invalidEndpoint:
'Masukkan laluan yang sepadan dengan protokol dipilih dan berakhir dengan {0}, tanpa parameter pertanyaan atau fragmen.',
invalidTokenizer:
'Gunakan direktori mutlak atau ID repositori; laluan relatif, koma dan baris baharu tidak disokong.',
invalidRequestRate:
'Pilih tanpa had atau masukkan nombor terhingga lebih besar daripada 0; perpuluhan disokong.',
invalidCaCert: 'Masukkan laluan mutlak sijil CA pada nod utama tanpa koma atau baris baharu.',
bodyOverride: 'Badan tambahan tidak boleh menimpa {0}.',
invalidBody: 'Masukkan objek JSON yang sah.',
invalidConcurrencies: 'Berikan 1–100 nilai serentak, setiap satu integer antara 1 dan 100000.',
emptyRequestCount:
'Apabila mengira mengikut keserentakan, pengganda dan permintaan minimum tidak boleh kedua-duanya 0.',
requestCountLimit: 'Permintaan yang dikira bagi setiap titik tidak boleh melebihi 1000000.',
contextExceeded: 'Jumlah token input, output dan rizab templat melebihi had konteks.',
planLimit: 'Berikan 1–100 senario dengan tidak lebih daripada 2000 titik.',
caCertHelp:
'Pilih sijil CA pada pelayan utama untuk mengesahkan perkhidmatan HTTPS. Biarkan kosong untuk menggunakan sijil sistem.',
extraBody: 'Badan permintaan tambahan',
entryKey: 'Nama parameter',
entryValue: 'Nilai parameter',
tokenizerMode: 'Mod pentoken',
requireIdle: 'Semak sama ada perkhidmatan melahu sebelum bermula',
requireIdleHelp:
'Menyemak /metrics sekali sebelum pemanasan dan ujian beban. Berhenti jika perkhidmatan sibuk atau keadaan melahu tidak dapat disahkan. Ini tidak menghalang trafik lain semasa ujian.',
trustRemoteCode: 'Percayai kod jauh',
trustRemoteCodeHelp:
'Membenarkan kod pentoken tersuai daripada repositori model dijalankan dalam bekas penanda aras. Aktifkan hanya jika diperlukan dan sumber dipercayai.',
tokenizerModes: { auto: 'Automatik (disyorkan)', slow: 'Pentoken perlahan', mistral: 'Pentoken Mistral' },
quickDescription: 'Sahkan sambungan dan pengukuran dengan satu permintaan.',
regularDescription: 'Beban pendek, sederhana dan panjang untuk ujian prestasi rutin.',
fullDescription:
'Pelbagai senario, tahap keserentakan dan ulangan, termasuk input panjang dan awalan dikongsi.',
customDescription: 'Mulakan dengan satu senario, tetapkan parameternya dan tambah lagi mengikut keperluan.',
scenarios: 'Senario',
builtIn: 'Terbina dalam',
builtInHelp: 'Pelan terbina dalam tidak boleh disunting atau dipadam. Laraskan salinan semasa mencipta ujian.',
modified: 'Diubah suai',
imageExists: 'Tersedia secara setempat',
imageNeedsPull: 'Perlu dimuat turun',
remark: 'Catatan',
source: 'Perkhidmatan setempat',
mode: 'Pelan ujian',
quick: 'Semakan pantas',
regular: 'Biasa',
full: 'Penuh',
custom: 'Tersuai',
addScenario: 'Tambah senario',
advanced: 'Tetapan lanjutan',
points: 'Titik',
warmups: 'Permintaan pemanasan',
fixedRequests: 'Permintaan tetap',
multiplier: 'Permintaan bagi keserentakan',
minRequests: 'Permintaan minimum',
trials: 'Ulangan',
prefixRatio: 'Nisbah awalan dikongsi',
totalTimeout: 'Had masa pusingan (s)',
prepareTimeout: 'Had masa persediaan (s)',
interval: 'Sela titik (s)',
context: 'Had konteks (0: tidak diketahui)',
reserve: 'Token simpanan templat',
storage: 'Had storan (MiB)',
stopOnError: 'Hentikan titik seterusnya jika ralat',
caCert: 'Laluan sijil CA',
validTrials: 'Sah / jumlah ulangan',
retry: 'Ulang titik terpilih',
environment: 'Persekitaran',
plan: 'Konfigurasi dibekukan',
clientHelp:
'Kontena klien berjalan pada nod utama dan menghantar permintaan kepada perkhidmatan sedia ada. Ia tidak memulakan inferens atau memerlukan GPU.',
retryHelp: 'Hanya titik bermasalah yang dipilih diulang, dengan parameter sama dan benih baharu.',
secretHelp: 'Salinan tidak mengandungi kunci API atau pengepala. Masukkan semula jika perlu.',
resultHelp:
'Median dan minimum/maksimum hanya melibatkan ujian berjaya tanpa anomali permintaan. P99 ialah median P99 setiap ujian. Cache dan trafik luaran tidak dikawal.',
},
benchmarkOverview: {
noResults: 'Tiada keputusan ujian yang sah. Semak sebab kegagalan dan log.',
noGroupResults: 'Tiada keputusan sah untuk gabungan ini. Pilih senario atau keserentakan lain.',
partial: 'Keputusan separa: ujian tidak berjaya sepenuhnya. Metrik hanya merangkumi titik yang sah.',
highlights: 'Metrik utama',
aggregate: 'Median pelaksanaan sah bagi gabungan ini. Klik metrik untuk butiran.',
trend: 'Trend keserentakan',
std: 'Sisihan piawai',
min: 'Minimum',
max: 'Maksimum',
tpot: 'Masa setiap token output',
tpotHelp: 'Purata masa penjanaan setiap token output selepas token pertama.',
itl: 'Kependaman antara token',
itlHelp: 'Masa antara token berturutan dalam respons penstriman.',
e2el: 'Kependaman hujung ke hujung',
e2elHelp: 'Masa dari penghantaran permintaan hingga penerimaan respons lengkap.',
requestHelp: 'Bilangan permintaan yang selesai setiap saat.',
usageHelp:
'Median pelaksanaan sah bagi gabungan ini, bukan jumlah permintaan keseluruhan ujian. Lihat pelaksanaan tidak normal dalam tab titik.',
duration: 'Tempoh pelaksanaan',
throughput: 'Daya pemprosesan',
latency: 'Kependaman',
usage: 'Permintaan dan penggunaan (pelaksanaan sah)',
},
benchmarkTokenizer: {
title: 'Pengurusan tokenizer',
local: 'Daftar direktori setempat',
upload: 'Muat naik pakej tokenizer',
uploadHelp: 'Muat naik ZIP / tar.gz tanpa pemberat model. Had: 128 MiB dimampatkan, 256 MiB selepas diekstrak.',
source: 'Kaedah import',
deleteLocal: 'Alih keluar pendaftaran tokenizer “{0}”? Direktori dan fail asal akan dikekalkan.',
deleteUpload: 'Padam tokenizer “{0}” serta fail yang dimuat naik? Laporan ujian terdahulu akan dikekalkan.',
select: 'Pilih tokenizer',
selectHelp:
'Untuk ujian luar talian, pilih tokenizer terurus. Input tersuai menerima direktori setempat atau nama repositori.',
},
container: { container: {
createByCommand: 'Cipta melalui perintah', createByCommand: 'Cipta melalui perintah',
importContainerBackupTip: 'Import fail sandaran kontena, hanya format .tar.gz disokong', importContainerBackupTip: 'Import fail sandaran kontena, hanya format .tar.gz disokong',
@@ -3013,6 +3170,20 @@ const message = {
remote: 'SSH jauh', remote: 'SSH jauh',
}, },
setting: { setting: {
panelCache: {
title: 'Tetapan cache',
tabs: 'Cache tab',
pagination: 'Cache halaman',
refresh: 'Cache sela penyegaran',
search: 'Cache input carian',
duration: 'Tempoh cache',
'24h': '24 jam',
week: 'Satu minggu',
month: 'Satu bulan',
forever: 'Kekal',
helper: 'Tempoh sah dikira dari perubahan keadaan terakhir. Selepas tamat tempoh, nilai lalai dipulihkan.',
},
panel: 'Panel', panel: 'Panel',
user: 'Pengguna panel', user: 'Pengguna panel',
userChange: 'Tukar pengguna panel', userChange: 'Tukar pengguna panel',
@@ -3750,6 +3921,7 @@ const message = {
link: 'Pautan', link: 'Pautan',
showCurrentArch: 'Aplikasi seni bina pelayan semasa', showCurrentArch: 'Aplikasi seni bina pelayan semasa',
syncLocalApp: 'Segerakkan Aplikasi Tempatan', syncLocalApp: 'Segerakkan Aplikasi Tempatan',
localAppSyncToNodes: 'Sertakan nod anak semasa menyegerakkan aplikasi tempatan',
memoryRequiredHelper: 'Aplikasi semasa memerlukan {0} memori', memoryRequiredHelper: 'Aplikasi semasa memerlukan {0} memori',
gpuConfig: 'Aktifkan Sokongan GPU', gpuConfig: 'Aktifkan Sokongan GPU',
gpuConfigHelper: gpuConfigHelper:
@@ -4315,7 +4487,8 @@ const message = {
domainHelper: 'Satu nama domain setiap baris, menyokong * dan alamat IP', domainHelper: 'Satu nama domain setiap baris, menyokong * dan alamat IP',
pushDir: 'Tolakkan sijil ke direktori tempatan', pushDir: 'Tolakkan sijil ke direktori tempatan',
dir: 'Direktori', dir: 'Direktori',
pushDirHelper: 'Fail sijil "fullchain.pem" dan fail kunci "privkey.pem" akan dihasilkan dalam direktori ini.', pushDirHelper:
'Pilih beberapa direktori atau masukkan satu direktori pada setiap baris. Fail "fullchain.pem" dan "privkey.pem" akan dihasilkan dalam setiap direktori.',
organizationDetail: 'Butiran organisasi', organizationDetail: 'Butiran organisasi',
fromWebsite: 'Daripada laman web', fromWebsite: 'Daripada laman web',
dnsMauanlHelper: dnsMauanlHelper:
+179 -8
View File
@@ -492,6 +492,7 @@ const message = {
recommend: 'Recomendado', recommend: 'Recomendado',
dir: 'Diretório', dir: 'Diretório',
alias: 'Apelido', alias: 'Apelido',
quickDirDelete: 'Confirmar a exclusão de ({0})?',
quickDir: 'Acesso rápido', quickDir: 'Acesso rápido',
minQuickJump: 'Adicione pelo menos uma entrada de acesso rápido', minQuickJump: 'Adicione pelo menos uma entrada de acesso rápido',
maxQuickJump: 'Você pode adicionar até quatro entradas de acesso rápido', maxQuickJump: 'Você pode adicionar até quatro entradas de acesso rápido',
@@ -774,6 +775,15 @@ const message = {
}, },
aiTools: { aiTools: {
agents: { 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', agent: 'Agente',
account: 'Conta de modelo', account: 'Conta de modelo',
batchOperation: 'Operação em lote', batchOperation: 'Operação em lote',
@@ -1492,16 +1502,14 @@ const message = {
statusWaiting: 'Aguardando', statusWaiting: 'Aguardando',
base: 'Informações básicas', base: 'Informações básicas',
backend: 'Backend', backend: 'Backend',
baseUrl: 'URL base',
endpoint: 'Endpoint', endpoint: 'Endpoint',
tokenizer: 'Tokenizer', tokenizer: 'Tokenizer',
tokenizerPlaceholder: tokenizerPlaceholder:
'Selecione ou informe um diretório local de tokenizer, ex.: /opt/1panel/tokenizers/DeepSeek-V3', 'Insira um diretório absoluto no nó principal ou um ID de repositório do Hugging Face',
runConfig: 'Configuração de execução', runConfig: 'Configuração de execução',
rawResult: 'Resultado bruto', rawResult: 'Resultado bruto',
resultMetrics: 'Métricas de resultado', resultMetrics: 'Métricas de resultado',
contextTokens: 'Comprimento de contexto', contextTokens: 'Comprimento de contexto',
contextTokensHelper: 'Limite de tokens de entrada mais limite de tokens de saída para este benchmark.',
outputThroughput: 'Throughput de saída', outputThroughput: 'Throughput de saída',
outputThroughputHelper: 'Tokens de saída gerados por segundo. Quanto maior, mais rápida a geração.', outputThroughputHelper: 'Tokens de saída gerados por segundo. Quanto maior, mais rápida a geração.',
totalThroughput: 'Throughput total', totalThroughput: 'Throughput total',
@@ -1510,20 +1518,19 @@ const message = {
firstTokenLatency: 'Latência do primeiro token', firstTokenLatency: 'Latência do primeiro token',
firstTokenLatencyHelper: firstTokenLatencyHelper:
'Tempo entre enviar a requisição e receber o primeiro token. Quanto menor, mais rápida a resposta.', 'Tempo entre enviar a requisição e receber o primeiro token. Quanto menor, mais rápida a resposta.',
tokenValueHelper: 'Informe um inteiro positivo ou valor com k, ex.: 512, 1k, 32k',
numPrompts: 'Prompts', numPrompts: 'Prompts',
concurrency: 'Concorrência', concurrency: 'Concorrência',
mean: 'Média', mean: 'Média',
median: 'Mediana', median: 'Mediana',
requestRate: 'Taxa de requisições', requestRate: 'Taxa de requisições',
requestRateUnlimited: 'Ilimitado (throughput máximo)', requestRateUnlimited: 'Ilimitado (throughput máximo)',
requestRateCustom: 'QPS personalizado',
requestRateCustomPlaceholder: 'Requisições por segundo, ex.: 2.5', requestRateCustomPlaceholder: 'Requisições por segundo, ex.: 2.5',
successfulRequests: 'Requisições bem-sucedidas', successfulRequests: 'Requisições bem-sucedidas',
requestThroughput: 'Throughput de requisições', requestThroughput: 'Throughput de requisições',
timeout: 'Timeout (segundos)',
image: 'Imagem vLLM', image: 'Imagem vLLM',
ignoreEos: 'Ignorar EOS', ignoreEos: 'Ignorar token de fim de sequência (EOS)',
ignoreEosHelp:
'Evita parar antes por EOS para testar o comprimento de saída configurado. Outras condições de parada continuam válidas; requer suporte do servidor.',
extraHeaders: 'Cabeçalhos extras', extraHeaders: 'Cabeçalhos extras',
}, },
vllmMonitor: { vllmMonitor: {
@@ -1740,6 +1747,155 @@ const message = {
modelType: 'Tipo de modelo', modelType: 'Tipo de modelo',
}, },
}, },
benchmarkPlan: {
prepareTimeoutHelp: 'Limite da preparação, excluído do tempo limite total.',
pointTimeoutHelp:
'Tempo separado por ponto, incluindo início, aquecimento e solicitações; limitado pelo tempo total restante.',
totalTimeoutHelp: 'Começa após a preparação, inclui pontos e intervalos e interrompe o teste ao expirar.',
concurrencyLevels: 'Níveis de concorrência',
concurrencyHelp: 'Os níveis são executados em sequência. Cada valor limita as solicitações simultâneas.',
requestCountMode: 'Quantidade de solicitações',
fixedRequestMode: 'Solicitações fixas por nível',
scaledRequestMode: 'Calcular pela concorrência',
requestsPerPoint: 'Solicitações por nível',
requestCountFormula:
'Por nível: valor fixo ou max(concorrência × multiplicador, mínimo de solicitações). Total do cenário: soma dos níveis × repetições, sem aquecimento.',
controlLog: 'Log de preparação e controle',
executionLog: 'Log de execução',
pointLog: 'Ponto nº {0} · {1} · Log de execução',
archivedLog: '{0} (arquivo histórico {1})',
tokenLengths: 'Tokens de entrada / saída',
kind: 'Categoria dos resultados',
kindServe: 'serve (testes comuns)',
kindPD: 'pd (medições de Prefill/Decode)',
saveDetailed: 'Salvar detalhes das solicitações',
saveDetailedHelp:
'Salva comprimentos, latências, texto gerado e erros de cada solicitação. Ao desativar, as métricas resumidas são mantidas e o tamanho dos arquivos é reduzido.',
parameters: 'Parâmetros',
temperatureHelp:
'Controla a aleatoriedade da saída. Zero geralmente produz resultados mais estáveis; valores maiores aumentam a aleatoriedade.',
pointTimeout: 'Tempo limite por ponto (segundos)',
temperature: 'Temperatura de amostragem',
requestUrl: 'URL da API',
invalidRequestUrl:
'Informe uma URL HTTP(S) completa terminada em /chat/completions ou /completions, sem credenciais, parâmetros de consulta ou fragmentos.',
requestRateHelp:
'Aplica-se apenas a este cenário. A taxa ilimitada respeita os limites de concorrência; aceita QPS fracionário.',
tokenizerModeHelp:
'Os modos disponíveis dependem da versão do vLLM na imagem de teste. O carregamento do tokenizador é verificado durante a preparação da tarefa.',
invalidBaseUrl: 'Insira uma URL HTTP(S) válida sem credenciais, consulta ou fragmento.',
invalidEndpoint:
'Informe um caminho compatível com o protocolo selecionado que termine em {0}, sem parâmetros de consulta ou fragmentos.',
invalidTokenizer:
'Use um diretório absoluto ou ID de repositório; caminhos relativos, vírgulas e quebras de linha não são aceitos.',
invalidRequestRate: 'Selecione ilimitado ou insira um número finito maior que 0; decimais são aceitos.',
invalidCaCert: 'Insira o caminho absoluto do certificado CA no nó principal, sem vírgulas ou quebras de linha.',
bodyOverride: 'O corpo adicional não pode sobrescrever {0}.',
invalidBody: 'Insira um objeto JSON válido.',
invalidConcurrencies: 'Informe de 1 a 100 valores de concorrência, cada um inteiro entre 1 e 100000.',
emptyRequestCount:
'Ao calcular pela concorrência, o multiplicador e o mínimo de solicitações não podem ser ambos 0.',
requestCountLimit: 'As solicitações calculadas por ponto não podem exceder 1000000.',
contextExceeded: 'Os tokens de entrada, saída e reserva do modelo excedem o limite de contexto.',
planLimit: 'Informe de 1 a 100 cenários com no máximo 2000 pontos.',
caCertHelp:
'Selecione um certificado CA no servidor principal para verificar serviços HTTPS. Deixe vazio para usar os certificados do sistema.',
extraBody: 'Corpo adicional da solicitação',
entryKey: 'Nome do parâmetro',
entryValue: 'Valor do parâmetro',
tokenizerMode: 'Modo do tokenizador',
requireIdle: 'Verificar se o serviço está ocioso antes de iniciar',
requireIdleHelp:
'Consulta /metrics uma vez antes do aquecimento e do teste de carga. Interrompe se o serviço estiver ocupado ou se não for possível confirmar que está ocioso. Isso não impede outro tráfego durante o teste.',
trustRemoteCode: 'Confiar em código remoto',
trustRemoteCodeHelp:
'Permite executar código de tokenização do repositório do modelo no contêiner de teste. Ative apenas quando necessário e se a fonte for confiável.',
tokenizerModes: { auto: 'Automático (recomendado)', slow: 'Tokenizador lento', mistral: 'Tokenizador Mistral' },
quickDescription: 'Verifica a conectividade e a medição com uma única solicitação.',
regularDescription: 'Cargas curtas, médias e longas para testes rotineiros de desempenho.',
fullDescription:
'Vários cenários, níveis de concorrência e repetições, incluindo entradas longas e prefixos compartilhados.',
customDescription: 'Comece com um cenário, configure os parâmetros e adicione outros conforme necessário.',
scenarios: 'Cenários',
builtIn: 'Integrado',
builtInHelp: 'Planos integrados não podem ser editados ou excluídos. Ajuste uma cópia ao criar um teste.',
modified: 'Modificado',
imageExists: 'Disponível localmente',
imageNeedsPull: 'Requer download',
remark: 'Observações',
source: 'Serviço local',
mode: 'Plano de teste',
quick: 'Verificação rápida',
regular: 'Normal',
full: 'Completo',
custom: 'Personalizado',
addScenario: 'Adicionar cenário',
advanced: 'Configurações avançadas',
points: 'Pontos',
warmups: 'Requisições de aquecimento',
fixedRequests: 'Requisições fixas',
multiplier: 'Requisições por concorrência',
minRequests: 'Mínimo de requisições',
trials: 'Repetições',
prefixRatio: 'Proporção do prefixo compartilhado',
totalTimeout: 'Tempo limite da rodada (s)',
prepareTimeout: 'Tempo limite de preparação (s)',
interval: 'Intervalo entre pontos (s)',
context: 'Limite de contexto (0: desconhecido)',
reserve: 'Tokens reservados para o modelo de texto',
storage: 'Limite de armazenamento (MiB)',
stopOnError: 'Parar os próximos pontos em caso de erro',
caCert: 'Caminho do certificado CA',
validTrials: 'Válidas / total',
retry: 'Repetir pontos selecionados',
environment: 'Ambiente',
plan: 'Configuração fixada',
clientHelp:
'O contêiner cliente roda no nó principal e envia requisições a um serviço existente. Não inicia inferência nem exige GPU.',
retryHelp: 'Apenas os pontos anormais selecionados são repetidos, com os mesmos parâmetros e novas sementes.',
secretHelp: 'As cópias não incluem chaves de API nem cabeçalhos. Informe-os novamente se necessário.',
resultHelp:
'A mediana e os valores mínimo/máximo incluem apenas execuções bem-sucedidas sem anomalias. P99 é a mediana dos P99 de cada execução. Cache e tráfego externo não são controlados.',
},
benchmarkOverview: {
noResults: 'Nenhum resultado válido. Consulte o motivo da falha e os logs.',
noGroupResults: 'Nenhum resultado válido nesta combinação. Selecione outro cenário ou concorrência.',
partial:
'Resultados parciais: o teste não foi totalmente bem-sucedido. As métricas incluem apenas pontos válidos.',
highlights: 'Métricas principais',
aggregate: 'Mediana das execuções válidas nesta combinação. Clique em uma métrica para ver detalhes.',
trend: 'Tendência por concorrência',
std: 'Desvio padrão',
min: 'Mínimo',
max: 'Máximo',
tpot: 'Tempo por token de saída',
tpotHelp: 'Tempo médio de geração por token de saída após o primeiro token.',
itl: 'Latência entre tokens',
itlHelp: 'Tempo entre tokens consecutivos em uma resposta em streaming.',
e2el: 'Latência de ponta a ponta',
e2elHelp: 'Tempo entre o envio da requisição e o recebimento da resposta completa.',
requestHelp: 'Número de requisições concluídas por segundo.',
usageHelp:
'Medianas das execuções válidas nesta combinação, não os totais de requisições do teste inteiro. Consulte execuções anormais na aba de pontos.',
duration: 'Duração da execução',
throughput: 'Taxa de processamento',
latency: 'Latência',
usage: 'Requisições e uso (execuções válidas)',
},
benchmarkTokenizer: {
title: 'Gerenciamento de tokenizadores',
local: 'Registrar diretório local',
upload: 'Enviar pacote de tokenizador',
uploadHelp: 'Envie um ZIP / tar.gz sem os pesos do modelo. Máximo: 128 MiB compactado e 256 MiB extraído.',
source: 'Método de importação',
deleteLocal: 'Remover o registro do tokenizador “{0}”? O diretório e os arquivos originais serão mantidos.',
deleteUpload: 'Excluir o tokenizador “{0}” e seus arquivos enviados? Os relatórios anteriores serão mantidos.',
select: 'Selecionar um tokenizador',
selectHelp:
'Para testes offline, selecione um tokenizador gerenciado. A entrada personalizada aceita diretórios locais ou nomes de repositórios.',
},
container: { container: {
createByCommand: 'Criar por comando', createByCommand: 'Criar por comando',
importContainerBackupTip: 'Importar arquivo de backup de contêiner, apenas .tar.gz é suportado', importContainerBackupTip: 'Importar arquivo de backup de contêiner, apenas .tar.gz é suportado',
@@ -3012,6 +3168,20 @@ const message = {
remote: 'SSH remoto', remote: 'SSH remoto',
}, },
setting: { setting: {
panelCache: {
title: 'Configurações de cache',
tabs: 'Cache de abas',
pagination: 'Cache de paginação',
refresh: 'Cache do intervalo de atualização',
search: 'Cache do campo de pesquisa',
duration: 'Duração do cache',
'24h': '24 horas',
week: 'Uma semana',
month: 'Um mês',
forever: 'Permanente',
helper: 'O prazo é contado a partir da última alteração de estado. Após a expiração, os valores padrão são restaurados.',
},
panel: 'Painel', panel: 'Painel',
user: 'Usuário do painel', user: 'Usuário do painel',
userChange: 'Alterar usuário do painel', userChange: 'Alterar usuário do painel',
@@ -3766,6 +3936,7 @@ const message = {
link: 'Link', link: 'Link',
showCurrentArch: 'Aplicações da arquitetura atual do servidor', showCurrentArch: 'Aplicações da arquitetura atual do servidor',
syncLocalApp: 'Sincronizar Aplicativo Local', syncLocalApp: 'Sincronizar Aplicativo Local',
localAppSyncToNodes: 'Incluir nós filhos ao sincronizar apps locais',
memoryRequiredHelper: 'O aplicativo atual requer {0} de memória', memoryRequiredHelper: 'O aplicativo atual requer {0} de memória',
gpuConfig: 'Ativar Suporte a GPU', gpuConfig: 'Ativar Suporte a GPU',
gpuConfigHelper: gpuConfigHelper:
@@ -4335,7 +4506,7 @@ const message = {
pushDir: 'Enviar o certificado para o diretório local', pushDir: 'Enviar o certificado para o diretório local',
dir: 'Diretório', dir: 'Diretório',
pushDirHelper: pushDirHelper:
'O arquivo do certificado "fullchain.pem" e o arquivo de chave "privkey.pem" serão gerados neste diretório.', 'Selecione vários diretórios ou informe um diretório por linha. Os arquivos "fullchain.pem" e "privkey.pem" serão gerados em cada diretório.',
organizationDetail: 'Detalhes da organização', organizationDetail: 'Detalhes da organização',
fromWebsite: 'Do site', fromWebsite: 'Do site',
dnsMauanlHelper: dnsMauanlHelper:
+186 -9
View File
@@ -491,6 +491,7 @@ const message = {
recommend: 'Рекомендуемое', recommend: 'Рекомендуемое',
dir: 'Каталог', dir: 'Каталог',
alias: 'Псевдоним', alias: 'Псевдоним',
quickDirDelete: 'Подтвердить удаление ({0})?',
quickDir: 'Быстрый доступ', quickDir: 'Быстрый доступ',
minQuickJump: 'Добавьте хотя бы одну запись быстрого доступа', minQuickJump: 'Добавьте хотя бы одну запись быстрого доступа',
maxQuickJump: 'Вы можете добавить до четырех записей быстрого доступа', maxQuickJump: 'Вы можете добавить до четырех записей быстрого доступа',
@@ -772,6 +773,15 @@ const message = {
}, },
aiTools: { aiTools: {
agents: { agents: {
deepSeekAccessHostHelper:
'Введите IPv4-адрес или имя хоста, используемое в браузере, без протокола, порта и пути. HTTPS использует локальный сертификат приложения.',
deepSeekAuthHelper:
'Имя пользователя: латинские буквы, цифры, точка, подчёркивание или дефис. Пароль: минимум 12 символов, без пробелов по краям и переводов строк.',
octopAuthHelper:
'Пароль: минимум 8 символов с буквами и цифрами; избегайте распространённых паролей. Если пароль отклонён, инструкции доступны в журнале приложения.',
initialCredentials: 'Начальные учётные данные; если пароль изменён в приложении, используйте текущий.',
nativeAgentAccessUnavailable:
'У существующей установки нет совместимого HTTPS-адреса или порта. Проверьте настройки приложения.',
agent: 'Агент', agent: 'Агент',
account: 'Аккаунт модели', account: 'Аккаунт модели',
batchOperation: 'Пакетная операция', batchOperation: 'Пакетная операция',
@@ -1484,16 +1494,13 @@ const message = {
statusWaiting: 'Ожидание', statusWaiting: 'Ожидание',
base: 'Основная информация', base: 'Основная информация',
backend: 'Backend', backend: 'Backend',
baseUrl: 'Base URL',
endpoint: 'Endpoint', endpoint: 'Endpoint',
tokenizer: 'Токенизатор', tokenizer: 'Токенизатор',
tokenizerPlaceholder: tokenizerPlaceholder: 'Введите абсолютный путь к каталогу на главном узле или ID репозитория Hugging Face',
'Выберите или введите локальный каталог токенизатора, например /opt/1panel/tokenizers/DeepSeek-V3',
runConfig: 'Конфигурация запуска', runConfig: 'Конфигурация запуска',
rawResult: 'Исходный результат', rawResult: 'Исходный результат',
resultMetrics: 'Метрики результата', resultMetrics: 'Метрики результата',
contextTokens: 'Длина контекста', contextTokens: 'Длина контекста',
contextTokensHelper: 'Лимит входных токенов плюс лимит выходных токенов для этого бенчмарка.',
outputThroughput: 'Выходная пропускная способность', outputThroughput: 'Выходная пропускная способность',
outputThroughputHelper: outputThroughputHelper:
'Выходные токены, генерируемые в секунду. Чем выше значение, тем быстрее генерация.', 'Выходные токены, генерируемые в секунду. Чем выше значение, тем быстрее генерация.',
@@ -1503,20 +1510,19 @@ const message = {
firstTokenLatency: 'Задержка первого токена', firstTokenLatency: 'Задержка первого токена',
firstTokenLatencyHelper: firstTokenLatencyHelper:
'Время от отправки запроса до получения первого токена. Чем ниже значение, тем быстрее ответ.', 'Время от отправки запроса до получения первого токена. Чем ниже значение, тем быстрее ответ.',
tokenValueHelper: 'Введите положительное целое число или значение с k, например 512, 1k, 32k',
numPrompts: 'Промпты', numPrompts: 'Промпты',
concurrency: 'Параллелизм', concurrency: 'Параллелизм',
mean: 'Среднее', mean: 'Среднее',
median: 'Медиана', median: 'Медиана',
requestRate: 'Частота запросов', requestRate: 'Частота запросов',
requestRateUnlimited: 'Без ограничений (макс. пропускная способность)', requestRateUnlimited: 'Без ограничений (макс. пропускная способность)',
requestRateCustom: 'Пользовательский QPS',
requestRateCustomPlaceholder: 'Запросов в секунду, например 2.5', requestRateCustomPlaceholder: 'Запросов в секунду, например 2.5',
successfulRequests: 'Успешные запросы', successfulRequests: 'Успешные запросы',
requestThroughput: 'Пропускная способность запросов', requestThroughput: 'Пропускная способность запросов',
timeout: 'Тайм-аут (секунды)',
image: 'Образ vLLM', image: 'Образ vLLM',
ignoreEos: 'Игнорировать EOS', ignoreEos: 'Игнорировать токен конца последовательности (EOS)',
ignoreEosHelp:
'Предотвращает раннюю остановку по EOS для тестирования заданной длины вывода. Другие условия остановки действуют; требуется поддержка сервера.',
extraHeaders: 'Дополнительные заголовки', extraHeaders: 'Дополнительные заголовки',
}, },
vllmMonitor: { vllmMonitor: {
@@ -1730,6 +1736,161 @@ const message = {
modelType: 'Тип модели', modelType: 'Тип модели',
}, },
}, },
benchmarkPlan: {
prepareTimeoutHelp: 'Лимит подготовки, не входит в общий тайм-аут.',
pointTimeoutHelp:
'Отдельный отсчёт для каждой точки, включая запуск, прогрев и запросы; ограничен оставшимся общим временем.',
totalTimeoutHelp: 'Отсчёт после подготовки, включая все точки и интервалы. По тайм-ауту тест останавливается.',
concurrencyLevels: 'Уровни параллелизма',
concurrencyHelp:
'Уровни проверяются последовательно. Каждое значение ограничивает число одновременных запросов.',
requestCountMode: 'Количество запросов',
fixedRequestMode: 'Фиксированное число на уровень',
scaledRequestMode: 'Рассчитать по параллелизму',
requestsPerPoint: 'Запросов на уровень',
requestCountFormula:
'На уровень: фиксированное значение или max(параллелизм × множитель, минимум запросов). Всего в сценарии: сумма по уровням × повторы, без прогрева.',
controlLog: 'Журнал подготовки и управления',
executionLog: 'Журнал выполнения',
pointLog: 'Точка №{0} · {1} · Журнал выполнения',
archivedLog: '{0} (архив {1})',
tokenLengths: 'Токены ввода / вывода',
kind: 'Категория результатов',
kindServe: 'serve (обычные тесты)',
kindPD: 'pd (измерения Prefill/Decode)',
saveDetailed: 'Сохранять подробности запросов',
saveDetailedHelp:
'Сохраняет длину, задержки, сгенерированный текст и ошибки каждого запроса. При отключении сводные метрики сохраняются, а размер файлов уменьшается.',
parameters: 'Параметры',
temperatureHelp:
'Управляет случайностью генерации. Ноль обычно даёт более стабильный результат; более высокие значения увеличивают случайность.',
pointTimeout: 'Тайм-аут одной точки (секунды)',
temperature: 'Температура сэмплирования',
requestUrl: 'Адрес API',
invalidRequestUrl:
'Введите полный HTTP(S) URL, заканчивающийся на /chat/completions или /completions, без учётных данных, параметров запроса и фрагментов.',
requestRateHelp:
'Применяется только к этому сценарию. Неограниченная частота учитывает предел параллелизма; допускается дробный QPS.',
tokenizerModeHelp:
'Доступные режимы зависят от версии vLLM в образе тестирования. Загрузка токенизатора проверяется при подготовке задачи.',
invalidBaseUrl: 'Введите корректный HTTP(S) URL без учётных данных, параметров запроса и фрагмента.',
invalidEndpoint:
'Введите путь, соответствующий выбранному протоколу и заканчивающийся на {0}, без параметров запроса и фрагментов.',
invalidTokenizer:
'Укажите абсолютный каталог или ID репозитория; относительные пути, запятые и переносы строк недопустимы.',
invalidRequestRate:
'Выберите отсутствие ограничения или введите конечное число больше 0; дробные числа допустимы.',
invalidCaCert: 'Укажите абсолютный путь к сертификату CA на основном узле без запятых и переносов строк.',
bodyOverride: 'Дополнительное тело не может переопределять {0}.',
invalidBody: 'Введите корректный объект JSON.',
invalidConcurrencies: 'Нужно от 1 до 100 значений параллелизма, каждое — целое число от 1 до 100000.',
emptyRequestCount:
'При расчёте по параллелизму множитель и минимальное число запросов не могут оба быть равны 0.',
requestCountLimit: 'Расчётное число запросов на точку не должно превышать 1000000.',
contextExceeded: 'Сумма входных, выходных токенов и резерва шаблона превышает лимит контекста.',
planLimit: 'Нужно от 1 до 100 сценариев и не более 2000 точек.',
caCertHelp:
'Выберите сертификат CA на основном сервере для проверки HTTPS. Оставьте поле пустым для системных сертификатов.',
extraBody: 'Дополнительное тело запроса',
entryKey: 'Имя параметра',
entryValue: 'Значение параметра',
tokenizerMode: 'Режим токенизатора',
requireIdle: 'Проверять отсутствие нагрузки перед запуском',
requireIdleHelp:
'Однократно проверяет /metrics перед прогревом и нагрузочным тестом. Останавливается, если сервис занят или отсутствие нагрузки нельзя подтвердить. Не блокирует другой трафик во время теста.',
trustRemoteCode: 'Доверять удалённому коду',
trustRemoteCodeHelp:
'Разрешает выполнять код токенизатора из репозитория модели в контейнере тестирования. Включайте только при необходимости и доверии к источнику.',
tokenizerModes: {
auto: 'Автоматически (рекомендуется)',
slow: 'Медленный токенизатор',
mistral: 'Токенизатор Mistral',
},
quickDescription: 'Проверка соединения и измерений одним запросом.',
regularDescription: 'Короткие, средние и длинные нагрузки для регулярных тестов производительности.',
fullDescription:
'Разные сценарии, уровни параллелизма и повторные измерения, включая длинные входы и общие префиксы.',
customDescription: 'Начните с одного сценария, настройте параметры и добавьте другие при необходимости.',
scenarios: 'Сценарии',
builtIn: 'Встроенный',
builtInHelp: 'Встроенные планы нельзя изменить или удалить. Настройте копию при создании теста.',
modified: 'Изменён',
imageExists: 'Есть локально',
imageNeedsPull: 'Требуется загрузка',
remark: 'Примечания',
source: 'Локальный сервис',
mode: 'План теста',
quick: 'Быстрая проверка',
regular: 'Обычный',
full: 'Полный',
custom: 'Свой',
addScenario: 'Добавить сценарий',
advanced: 'Дополнительные настройки',
points: 'Точки',
warmups: 'Запросы прогрева',
fixedRequests: 'Число запросов',
multiplier: 'Множитель параллелизма',
minRequests: 'Минимум запросов',
trials: 'Повторения',
prefixRatio: 'Доля общего префикса',
totalTimeout: 'Тайм-аут раунда (с)',
prepareTimeout: 'Тайм-аут подготовки (с)',
interval: 'Интервал точек (с)',
context: 'Лимит контекста (0: неизвестен)',
reserve: 'Резерв токенов шаблона',
storage: 'Лимит хранения (МиБ)',
stopOnError: 'Останавливать следующие точки при ошибке',
caCert: 'Путь к сертификату CA',
validTrials: 'Успешных / всего повторений',
retry: 'Повторить выбранные точки',
environment: 'Среда выполнения',
plan: 'Зафиксированная конфигурация',
clientHelp:
'Клиентский контейнер работает на главном узле и отправляет запросы существующему сервису. Он не запускает модель и не требует GPU.',
retryHelp: 'Повторяются только выбранные проблемные точки с прежними параметрами и новыми начальными числами.',
secretHelp: 'Копии не содержат ключи API и заголовки. При необходимости введите их заново.',
resultHelp:
'Медиана и минимум/максимум рассчитываются только по успешным запускам без аномалий запросов. P99 — медиана P99 отдельных запусков. Кеш и внешний трафик не контролируются.',
},
benchmarkOverview: {
noResults: 'Нет достоверных результатов. Проверьте причину сбоя и журналы.',
noGroupResults: 'Нет достоверных результатов для этой комбинации. Выберите другой сценарий или параллелизм.',
partial:
'Частичные результаты: тест завершён не полностью успешно. Метрики учитывают только достоверные точки.',
highlights: 'Основные метрики',
aggregate: 'Медиана достоверных запусков этой комбинации. Нажмите на метрику для подробностей.',
trend: 'Зависимость от параллелизма',
std: 'Станд. отклонение',
min: 'Минимум',
max: 'Максимум',
tpot: 'Время на выходной токен',
tpotHelp: 'Среднее время генерации выходного токена после первого.',
itl: 'Задержка между токенами',
itlHelp: 'Время между последовательными токенами потокового ответа.',
e2el: 'Полная задержка',
e2elHelp: 'Время от отправки запроса до получения полного ответа.',
requestHelp: 'Число завершённых запросов в секунду.',
usageHelp:
'Медианы достоверных запусков этой комбинации, а не общее число запросов теста. Ошибочные запуски доступны на вкладке точек.',
duration: 'Длительность запуска',
throughput: 'Пропускная способность',
latency: 'Задержка',
usage: 'Запросы и использование (достоверные запуски)',
},
benchmarkTokenizer: {
title: 'Управление токенизаторами',
local: 'Зарегистрировать локальный каталог',
upload: 'Загрузить архив токенизатора',
uploadHelp: 'Загрузите ZIP / tar.gz без весов модели. Максимум: 128 МиБ в архиве и 256 МиБ после распаковки.',
source: 'Способ импорта',
deleteLocal: 'Удалить регистрацию токенизатора «{0}»? Исходный каталог и файлы будут сохранены.',
deleteUpload: 'Удалить токенизатор «{0}» и загруженные файлы? Предыдущие отчёты сохранятся.',
select: 'Выберите токенизатор',
selectHelp:
'Для автономных тестов выберите токенизатор из списка. Ручной ввод поддерживает локальный каталог или имя репозитория.',
},
container: { container: {
createByCommand: 'Создать с помощью команды', createByCommand: 'Создать с помощью команды',
importContainerBackupTip: 'Импорт файла резервной копии контейнера, поддерживается только .tar.gz', importContainerBackupTip: 'Импорт файла резервной копии контейнера, поддерживается только .tar.gz',
@@ -2985,6 +3146,20 @@ const message = {
remote: 'Удаленный SSH', remote: 'Удаленный SSH',
}, },
setting: { setting: {
panelCache: {
title: 'Настройки кэша',
tabs: 'Кэш вкладок',
pagination: 'Кэш пагинации',
refresh: 'Кэш интервала обновления',
search: 'Кэш поискового запроса',
duration: 'Срок хранения кэша',
'24h': '24 часа',
week: 'Одна неделя',
month: 'Один месяц',
forever: 'Бессрочно',
helper: 'Срок хранения отсчитывается с последнего изменения состояния. После его истечения восстанавливаются значения по умолчанию.',
},
panel: 'Панель', panel: 'Панель',
user: 'Пользователь панели', user: 'Пользователь панели',
userChange: 'Изменить пользователя панели', userChange: 'Изменить пользователя панели',
@@ -3735,6 +3910,7 @@ const message = {
link: 'Ссылка', link: 'Ссылка',
showCurrentArch: 'Приложения для текущей архитектуры сервера', showCurrentArch: 'Приложения для текущей архитектуры сервера',
syncLocalApp: 'Синхронизировать локальное приложение', syncLocalApp: 'Синхронизировать локальное приложение',
localAppSyncToNodes: 'Включать дочерние узлы при синхронизации локальных приложений',
memoryRequiredHelper: 'Текущее приложение требует {0} памяти', memoryRequiredHelper: 'Текущее приложение требует {0} памяти',
gpuConfig: 'Включить поддержку GPU', gpuConfig: 'Включить поддержку GPU',
gpuConfigHelper: gpuConfigHelper:
@@ -4302,7 +4478,8 @@ const message = {
domainHelper: 'Одно доменное имя в строке, поддерживает * и IP-адрес', domainHelper: 'Одно доменное имя в строке, поддерживает * и IP-адрес',
pushDir: 'Отправить сертификат в локальную директорию', pushDir: 'Отправить сертификат в локальную директорию',
dir: 'Директория', dir: 'Директория',
pushDirHelper: 'В этой директории будут созданы файл сертификата "fullchain.pem" и файл ключа "privkey.pem".', pushDirHelper:
'Выберите несколько директорий или укажите по одной директории в строке. В каждой директории будут созданы файлы "fullchain.pem" и "privkey.pem".',
organizationDetail: 'Детали организации', organizationDetail: 'Детали организации',
fromWebsite: 'С веб-сайта', fromWebsite: 'С веб-сайта',
dnsMauanlHelper: dnsMauanlHelper:
+181 -8
View File
@@ -495,6 +495,7 @@ const message = {
recommend: 'Önerilen', recommend: 'Önerilen',
dir: 'Dizin', dir: 'Dizin',
alias: 'Takma Ad', alias: 'Takma Ad',
quickDirDelete: '({0}) silinsin mi?',
quickDir: 'Hızlı Erişim', quickDir: 'Hızlı Erişim',
minQuickJump: 'En az bir hızlı erişim girdisi ekleyin', minQuickJump: 'En az bir hızlı erişim girdisi ekleyin',
maxQuickJump: 'En fazla dört hızlı erişim girdisi ekleyebilirsiniz', maxQuickJump: 'En fazla dört hızlı erişim girdisi ekleyebilirsiniz',
@@ -777,6 +778,15 @@ const message = {
}, },
aiTools: { aiTools: {
agents: { 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', agent: 'Ajan',
account: 'Model hesabı', account: 'Model hesabı',
batchOperation: 'Toplu işlem', batchOperation: 'Toplu işlem',
@@ -1492,15 +1502,13 @@ const message = {
statusWaiting: 'Bekliyor', statusWaiting: 'Bekliyor',
base: 'Temel Bilgiler', base: 'Temel Bilgiler',
backend: 'Backend', backend: 'Backend',
baseUrl: 'Base URL',
endpoint: 'Endpoint', endpoint: 'Endpoint',
tokenizer: 'Tokenizer', tokenizer: 'Tokenizer',
tokenizerPlaceholder: 'Yerel tokenizer dizini seçin veya girin, örn. /opt/1panel/tokenizers/DeepSeek-V3', tokenizerPlaceholder: 'Ana düğümdeki mutlak dizin yolunu veya Hugging Face depo kimliğini girin',
runConfig: 'Çalıştırma Yapılandırması', runConfig: 'Çalıştırma Yapılandırması',
rawResult: 'Ham Sonuç', rawResult: 'Ham Sonuç',
resultMetrics: 'Sonuç Metrikleri', resultMetrics: 'Sonuç Metrikleri',
contextTokens: 'Bağlam Uzunluğu', contextTokens: 'Bağlam Uzunluğu',
contextTokensHelper: 'Bu benchmark için giriş token sınırı ile çıkış token sınırının toplamı.',
outputThroughput: 'Çıkış Throughput', outputThroughput: 'Çıkış Throughput',
outputThroughputHelper: outputThroughputHelper:
'Saniye başına üretilen çıkış token sayısı. Daha yüksek değer daha hızlı üretimdir.', 'Saniye başına üretilen çıkış token sayısı. Daha yüksek değer daha hızlı üretimdir.',
@@ -1510,20 +1518,19 @@ const message = {
firstTokenLatency: 'İlk Token Gecikmesi', firstTokenLatency: 'İlk Token Gecikmesi',
firstTokenLatencyHelper: firstTokenLatencyHelper:
'İsteğin gönderilmesinden ilk token alınana kadar geçen süre. Daha düşük değer daha hızlı yanıttır.', 'İsteğin gönderilmesinden ilk token alınana kadar geçen süre. Daha düşük değer daha hızlı yanıttır.',
tokenValueHelper: 'Pozitif tam sayı veya k değeri girin, örn. 512, 1k, 32k',
numPrompts: 'Prompt', numPrompts: 'Prompt',
concurrency: 'Eşzamanlılık', concurrency: 'Eşzamanlılık',
mean: 'Ortalama', mean: 'Ortalama',
median: 'Medyan', median: 'Medyan',
requestRate: 'İstek Hızı', requestRate: 'İstek Hızı',
requestRateUnlimited: 'Sınırsız (maksimum throughput)', requestRateUnlimited: 'Sınırsız (maksimum throughput)',
requestRateCustom: 'Özel QPS',
requestRateCustomPlaceholder: 'Saniye başına istek, örn. 2.5', requestRateCustomPlaceholder: 'Saniye başına istek, örn. 2.5',
successfulRequests: 'Başarılı istekler', successfulRequests: 'Başarılı istekler',
requestThroughput: 'İstek Throughput', requestThroughput: 'İstek Throughput',
timeout: 'Zaman aşımı (saniye)',
image: 'vLLM İmajı', image: 'vLLM İmajı',
ignoreEos: "EOS'u yok say", ignoreEos: 'Dizi sonu belirtecini (EOS) yok say',
ignoreEosHelp:
'Ayarlanan çıktı uzunluğunu test etmek için EOS nedeniyle erken durmayı önler. Diğer durdurma koşulları geçerlidir; sunucu desteği gerekir.',
extraHeaders: 'Ek Başlıklar', extraHeaders: 'Ek Başlıklar',
}, },
vllmMonitor: { vllmMonitor: {
@@ -1737,6 +1744,157 @@ const message = {
modelType: 'Model türü', modelType: 'Model türü',
}, },
}, },
benchmarkPlan: {
prepareTimeoutHelp: 'Hazırlık süresi sınırıdır; toplam zaman aşımına dahil değildir.',
pointTimeoutHelp:
'Her nokta ayrı ölçülür; başlangıç, ısınma ve tüm istekler dahildir. Kalan toplam süreyle sınırlıdır.',
totalTimeoutHelp: 'Hazırlık sonrası başlar, tüm noktaları ve aralıkları kapsar. Süre dolunca test durur.',
concurrencyLevels: 'Eşzamanlılık seviyeleri',
concurrencyHelp: 'Seviyeler sırayla test edilir. Her değer aynı anda devam eden istek sayısını sınırlar.',
requestCountMode: 'İstek sayısı ayarı',
fixedRequestMode: 'Seviye başına sabit istek',
scaledRequestMode: 'Eşzamanlılığa göre hesapla',
requestsPerPoint: 'Seviye başına istek',
requestCountFormula:
'Seviye başına: sabit değer veya max(eşzamanlılık × çarpan, minimum istek). Senaryo toplamı: seviyelerin toplamı × tekrar sayısı, ısınma hariç.',
controlLog: 'Hazırlık ve kontrol günlüğü',
executionLog: 'Yürütme günlüğü',
pointLog: 'Test noktası #{0} · {1} · Yürütme günlüğü',
archivedLog: '{0} (arşiv {1})',
tokenLengths: 'Girdi / çıktı tokenları',
kind: 'Sonuç kategorisi',
kindServe: 'serve (normal testler)',
kindPD: 'pd (Prefill/Decode ölçümleri)',
saveDetailed: 'İstek ayrıntılarını kaydet',
saveDetailedHelp:
'Her isteğin uzunluğunu, gecikmesini, üretilen metni ve hatalarını kaydeder. Devre dışı bırakıldığında özet ölçümler korunur ve sonuç dosyalarının boyutu azalır.',
parameters: 'Parametreler',
temperatureHelp:
'Çıktının rastgeleliğini kontrol eder. Sıfır genellikle daha kararlı çıktı sağlar; yüksek değerler rastgeleliği artırır.',
pointTimeout: 'Nokta başına zaman aşımı (saniye)',
temperature: 'Örnekleme sıcaklığı',
requestUrl: 'API adresi',
invalidRequestUrl:
'/chat/completions veya /completions ile biten tam bir HTTP(S) adresi girin; kimlik bilgisi, sorgu parametresi veya parça içermemelidir.',
requestRateHelp:
'Yalnızca bu senaryoya uygulanır. Sınırsız hız da eşzamanlılık sınırına uyar; kesirli QPS desteklenir.',
tokenizerModeHelp:
'Kullanılabilir modlar, test imajındaki vLLM sürümüne bağlıdır. Görev hazırlanırken belirteçleyicinin yüklenebildiği kontrol edilir.',
invalidBaseUrl: 'Kimlik bilgisi, sorgu veya parça içermeyen geçerli bir HTTP(S) URL girin.',
invalidEndpoint:
'Seçilen protokole uygun, {0} ile biten ve sorgu parametresi veya parça içermeyen bir uç nokta yolu girin.',
invalidTokenizer:
'Mutlak dizin veya depo kimliği kullanın; göreli yollar, virgüller ve satır sonları desteklenmez.',
invalidRequestRate: 'Sınırsızı seçin veya 0’dan büyük sonlu bir sayı girin; ondalık sayılar desteklenir.',
invalidCaCert: 'Birincil düğümdeki CA sertifikasının mutlak dosya yolunu virgül veya satır sonu olmadan girin.',
bodyOverride: 'Ek gövde {0} parametresini geçersiz kılamaz.',
invalidBody: 'Geçerli bir JSON nesnesi girin.',
invalidConcurrencies: 'Her biri 1–100000 arasında tam sayı olan 1–100 eşzamanlılık değeri girin.',
emptyRequestCount: 'Eşzamanlılığa göre hesaplamada çarpan ve minimum istek sayısı birlikte 0 olamaz.',
requestCountLimit: 'Nokta başına hesaplanan istek sayısı 1000000 değerini aşamaz.',
contextExceeded: 'Giriş, çıkış tokenları ve şablon rezervi bağlam sınırını aşıyor.',
planLimit: 'Toplam en fazla 2000 noktadan oluşan 1–100 senaryo girin.',
caCertHelp:
'HTTPS hizmetlerini doğrulamak için ana sunucudan bir CA sertifikası seçin. Sistem sertifikaları için boş bırakın.',
extraBody: 'Ek istek gövdesi',
entryKey: 'Parametre adı',
entryValue: 'Parametre değeri',
tokenizerMode: 'Belirteçleyici modu',
requireIdle: 'Başlamadan önce hizmetin boşta olduğunu denetle',
requireIdleHelp:
'Isınma ve yük testi öncesinde /metrics bir kez denetlenir. Hizmet meşgulse veya boşta olduğu doğrulanamazsa durur. Test sırasında başka trafiği engellemez.',
trustRemoteCode: 'Uzak koda güven',
trustRemoteCodeHelp:
'Model deposundaki özel belirteçleyici kodunun test konteynerinde çalışmasına izin verir. Yalnızca gerektiğinde ve kaynağa güvendiğinizde etkinleştirin.',
tokenizerModes: {
auto: 'Otomatik (önerilen)',
slow: 'Yavaş belirteçleyici',
mistral: 'Mistral belirteçleyicisi',
},
quickDescription: 'Tek bir istekle bağlantıyı ve ölçüm sürecini doğrular.',
regularDescription: 'Günlük performans testleri için kısa, orta ve uzun iş yükleri.',
fullDescription:
'Uzun girdiler ve ortak önekler dahil, farklı senaryolar ve eşzamanlılık düzeylerinde tekrarlı ölçümler.',
customDescription: 'Bir senaryoyla başlayın, parametrelerini ayarlayın ve gerektiğinde yenilerini ekleyin.',
scenarios: 'Senaryolar',
builtIn: 'Yerleşik',
builtInHelp: 'Yerleşik planlar düzenlenemez veya silinemez. Test oluştururken bir kopyayı ayarlayın.',
modified: 'Değiştirildi',
imageExists: 'Yerelde mevcut',
imageNeedsPull: 'İndirme gerekli',
remark: 'Notlar',
source: 'Yerel hizmet',
mode: 'Test planı',
quick: 'Hızlı kontrol',
regular: 'Normal',
full: 'Tam',
custom: 'Özel',
addScenario: 'Senaryo ekle',
advanced: 'Gelişmiş ayarlar',
points: 'Ölçüm noktaları',
warmups: 'Isınma istekleri',
fixedRequests: 'Sabit istekler',
multiplier: 'Eşzamanlılık başına istek',
minRequests: 'En az istek',
trials: 'Tekrarlar',
prefixRatio: 'Ortak önek oranı',
totalTimeout: 'Tur zaman aşımı (sn)',
prepareTimeout: 'Hazırlık zaman aşımı (sn)',
interval: 'Ölçüm aralığı (sn)',
context: 'Bağlam sınırı (0: bilinmiyor)',
reserve: 'Şablon için ayrılan token',
storage: 'Depolama sınırı (MiB)',
stopOnError: 'Hatada sonraki ölçümleri durdur',
caCert: 'CA sertifikası yolu',
validTrials: 'Geçerli / toplam tekrar',
retry: 'Seçili ölçümleri yinele',
environment: 'Çalışma ortamı',
plan: 'Sabitlenmiş yapılandırma',
clientHelp:
'İstemci konteyneri ana düğümde çalışır ve mevcut hizmete istek gönderir. Çıkarım hizmeti başlatmaz ve GPU gerektirmez.',
retryHelp: 'Yalnızca seçili sorunlu ölçümler aynı parametreler ve yeni tohumlarla yinelenir.',
secretHelp: 'Kopyalar API anahtarlarını ve başlıkları içermez. Gerekiyorsa yeniden girin.',
resultHelp:
'Medyan ve en düşük/en yüksek değerler yalnızca istek anomalisi olmayan başarılı çalışmaları içerir. P99, çalışma başına P99 değerlerinin medyanıdır. Önbellek ve dış trafik kontrol edilmez.',
},
benchmarkOverview: {
noResults: 'Geçerli test sonucu yok. Hata nedenini ve günlükleri kontrol edin.',
noGroupResults: 'Bu birleşim için geçerli sonuç yok. Başka bir senaryo veya eşzamanlılık seçin.',
partial: 'Kısmi sonuçlar: test tamamen başarılı olmadı. Metrikler yalnızca geçerli noktaları kapsar.',
highlights: 'Temel metrikler',
aggregate: 'Bu birleşimdeki geçerli çalıştırmaların medyanı. Ayrıntılar için metriğe tıklayın.',
trend: 'Eşzamanlılık eğilimi',
std: 'Standart sapma',
min: 'Minimum',
max: 'Maksimum',
tpot: 'Çıktı tokeni başına süre',
tpotHelp: 'İlk tokenden sonra çıktı tokeni başına ortalama üretim süresi.',
itl: 'Tokenler arası gecikme',
itlHelp: 'Akış yanıtındaki ardışık tokenler arasındaki süre.',
e2el: 'Uçtan uca gecikme',
e2elHelp: 'İsteğin gönderilmesinden yanıtın tamamının alınmasına kadar geçen süre.',
requestHelp: 'Saniyede tamamlanan istek sayısı.',
usageHelp:
'Bu birleşimdeki geçerli çalıştırmaların medyanlarıdır; tüm testin istek toplamları değildir. Anormal çalıştırmalar için noktalar sekmesine bakın.',
duration: 'Çalıştırma süresi',
throughput: 'İşleme hızı',
latency: 'Gecikme',
usage: 'İstekler ve kullanım (geçerli çalıştırmalar)',
},
benchmarkTokenizer: {
title: 'Tokenizer yönetimi',
local: 'Yerel dizini kaydet',
upload: 'Tokenizer paketini yükle',
uploadHelp: 'Model ağırlıkları içermeyen ZIP / tar.gz yükleyin. Sınır: sıkıştırılmış 128 MiB, açılmış 256 MiB.',
source: 'İçe aktarma yöntemi',
deleteLocal: '“{0}” tokenizer kaydı kaldırılsın mı? Özgün dizin ve dosyalar korunur.',
deleteUpload: '“{0}” tokenizer ve yüklenen dosyaları silinsin mi? Geçmiş test raporları korunur.',
select: 'Bir tokenizer seçin',
selectHelp:
'Çevrimdışı test için yönetilen bir tokenizer seçin. Özel giriş yerel dizin veya depo adını destekler.',
},
container: { container: {
createByCommand: 'Komutla oluştur', createByCommand: 'Komutla oluştur',
importContainerBackupTip: 'Konteyner yedek dosyasını içe aktar, yalnızca .tar.gz desteklenir', importContainerBackupTip: 'Konteyner yedek dosyasını içe aktar, yalnızca .tar.gz desteklenir',
@@ -3003,6 +3161,20 @@ const message = {
remote: 'Uzak SSH', remote: 'Uzak SSH',
}, },
setting: { setting: {
panelCache: {
title: 'Önbellek ayarları',
tabs: 'Sekme önbelleği',
pagination: 'Sayfalama önbelleği',
refresh: 'Yenileme aralığı önbelleği',
search: 'Arama alanı önbelleği',
duration: 'Önbellek süresi',
'24h': '24 saat',
week: 'Bir hafta',
month: 'Bir ay',
forever: 'Süresiz',
helper: 'Geçerlilik süresi son durum değişikliğinden itibaren hesaplanır. Süre dolduğunda varsayılan değerler geri yüklenir.',
},
panel: 'Panel', panel: 'Panel',
user: 'Panel kullanıcısı', user: 'Panel kullanıcısı',
userChange: 'Panel kullanıcısını değiştir', userChange: 'Panel kullanıcısını değiştir',
@@ -3751,6 +3923,7 @@ const message = {
link: 'Bağlantı', link: 'Bağlantı',
showCurrentArch: 'Mimari', showCurrentArch: 'Mimari',
syncLocalApp: 'Yerel Uygulamayı Senkronize Et', syncLocalApp: 'Yerel Uygulamayı Senkronize Et',
localAppSyncToNodes: 'Yerel uygulama eşitlemesine alt düğümleri dahil et',
memoryRequiredHelper: 'Geçerli uygulama bellek gereksinimi {0}', memoryRequiredHelper: 'Geçerli uygulama bellek gereksinimi {0}',
gpuConfig: 'GPU Desteğini Etkinleştir', gpuConfig: 'GPU Desteğini Etkinleştir',
gpuConfigHelper: gpuConfigHelper:
@@ -4321,7 +4494,7 @@ const message = {
pushDir: 'Sertifikayı yerel dizine aktar', pushDir: 'Sertifikayı yerel dizine aktar',
dir: 'Dizin', dir: 'Dizin',
pushDirHelper: pushDirHelper:
'Sertifika dosyası "fullchain.pem" ve anahtar dosyası "privkey.pem" bu dizinde oluşturulacaktır.', 'Birden fazla dizin seçin veya her satıra bir dizin girin. Her dizinde "fullchain.pem" ve "privkey.pem" dosyaları oluşturulacaktır.',
organizationDetail: 'Organizasyon detayları', organizationDetail: 'Organizasyon detayları',
fromWebsite: 'Web sitesinden', fromWebsite: 'Web sitesinden',
dnsMauanlHelper: dnsMauanlHelper:
+161 -8
View File
@@ -473,6 +473,7 @@ const message = {
recommend: '推薦', recommend: '推薦',
dir: '目錄', dir: '目錄',
alias: '別名', alias: '別名',
quickDirDelete: '確定刪除({0})',
quickDir: '快捷目錄', quickDir: '快捷目錄',
minQuickJump: '請至少設定一個快速跳轉入口', minQuickJump: '請至少設定一個快速跳轉入口',
maxQuickJump: '最多可設定四個快速跳轉入口', maxQuickJump: '最多可設定四個快速跳轉入口',
@@ -739,6 +740,13 @@ const message = {
}, },
aiTools: { aiTools: {
agents: { agents: {
deepSeekAccessHostHelper:
'填寫瀏覽器實際存取的 IPv4 位址或主機名稱,不含協定、連接埠或路徑。HTTPS 使用應用程式的本機憑證。',
deepSeekAuthHelper: '使用者名稱僅支援字母、數字、點、底線及連字號;密碼至少 12 位,不含前後空格或換行。',
octopAuthHelper:
'密碼至少 8 位且包含字母及數字,避免常見弱密碼。若初始化拒絕該密碼,請查看應用程式日誌中的設定說明。',
initialCredentials: '初始登入資訊;若已在應用程式內修改,以目前密碼為準。',
nativeAgentAccessUnavailable: '現有安裝缺少相容的 HTTPS 存取位址或連接埠,請檢查應用程式設定。',
agent: 'Agent', agent: 'Agent',
account: '模型帳號', account: '模型帳號',
batchOperation: '批次操作', batchOperation: '批次操作',
@@ -1405,35 +1413,31 @@ const message = {
statusWaiting: '等待中', statusWaiting: '等待中',
base: '基礎資訊', base: '基礎資訊',
backend: '後端', backend: '後端',
baseUrl: '服務地址',
endpoint: '介面路徑', endpoint: '介面路徑',
tokenizer: '分詞器', tokenizer: '分詞器',
tokenizerPlaceholder: '請選擇或輸入本機分詞器目錄,例如 /opt/1panel/tokenizers/DeepSeek-V3', tokenizerPlaceholder: '請輸入主節點上的絕對目錄或 Hugging Face 儲存庫 ID',
runConfig: '執行設定', runConfig: '執行設定',
rawResult: '原始結果', rawResult: '原始結果',
resultMetrics: '結果指標', resultMetrics: '結果指標',
contextTokens: '上下文長度', contextTokens: '上下文長度',
contextTokensHelper: '本次測試的輸入 Token 與輸出 Token 上限之和。',
outputThroughput: '輸出吞吐', outputThroughput: '輸出吞吐',
outputThroughputHelper: '模型每秒產生的輸出 Token 數,越高表示生成速度越快。', outputThroughputHelper: '模型每秒產生的輸出 Token 數,越高表示生成速度越快。',
totalThroughput: '總吞吐', totalThroughput: '總吞吐',
totalThroughputHelper: '每秒處理的輸入與輸出 Token 總數,用於衡量整體處理能力。', totalThroughputHelper: '每秒處理的輸入與輸出 Token 總數,用於衡量整體處理能力。',
firstTokenLatency: '首 Token 延遲', firstTokenLatency: '首 Token 延遲',
firstTokenLatencyHelper: '從請求送出到收到第一個 Token 的時間,越低表示回應越快。', firstTokenLatencyHelper: '從請求送出到收到第一個 Token 的時間,越低表示回應越快。',
tokenValueHelper: '請輸入正整數或 k 單位,例如 512、1k、32k',
numPrompts: '請求數量', numPrompts: '請求數量',
concurrency: '併發數', concurrency: '併發數',
mean: '平均值', mean: '平均值',
median: '中位數', median: '中位數',
requestRate: '請求速率', requestRate: '請求速率',
requestRateUnlimited: '不限速(最大吞吐)', requestRateUnlimited: '不限速(最大吞吐)',
requestRateCustom: '自訂 QPS',
requestRateCustomPlaceholder: '請輸入每秒請求數,如 2.5', requestRateCustomPlaceholder: '請輸入每秒請求數,如 2.5',
successfulRequests: '成功請求', successfulRequests: '成功請求',
requestThroughput: '請求吞吐', requestThroughput: '請求吞吐',
timeout: '逾時時間(秒)',
image: 'vLLM 映像', image: 'vLLM 映像',
ignoreEos: '忽略 EOS', ignoreEos: '忽略結束標記(EOS)',
ignoreEosHelp: '避免因結束標記提前停止,便於依設定輸出長度壓測;其他停止條件仍有效,需服務支援。',
extraHeaders: '額外請求標頭', extraHeaders: '額外請求標頭',
}, },
vllmMonitor: { vllmMonitor: {
@@ -1637,6 +1641,140 @@ const message = {
modelType: '模型類型', modelType: '模型類型',
}, },
}, },
benchmarkPlan: {
prepareTimeoutHelp: '準備階段的時間上限,不計入整輪逾時。',
pointTimeoutHelp: '每個測點獨立計時,含啟動、預熱和全部請求,受整輪剩餘時間限制。',
totalTimeoutHelp: '準備完成後計時,涵蓋所有測點及間隔,逾時後停止。',
concurrencyLevels: '並行檔位',
concurrencyHelp: '各檔位依序測試,數值表示同時進行的請求數量上限。',
requestCountMode: '請求數量設定',
fixedRequestMode: '每檔固定請求數',
scaledRequestMode: '依並行數計算',
requestsPerPoint: '每檔請求數',
requestCountFormula:
'每檔:固定值,或 max(並行數 × 倍數,最少請求數);場景總量:各檔請求數之和 × 重複次數,不含預熱。',
controlLog: '準備與控制日誌',
executionLog: '執行日誌',
pointLog: '測點 #{0} · {1} · 執行日誌',
archivedLog: '{0}(歷史 {1})',
tokenLengths: '輸入 / 輸出 Token',
kind: '結果分類',
kindServe: 'serve(一般測試)',
kindPD: 'pd(Prefill/Decode 探測)',
saveDetailed: '儲存請求明細',
saveDetailedHelp: '儲存每筆請求的長度、延遲、生成文字和錯誤資訊。關閉後仍保留彙總指標,可減少結果檔案大小。',
parameters: '參數',
temperatureHelp: '控制生成內容的隨機性;0 通常較穩定,值越高越隨機。',
pointTimeout: '單測點逾時(秒)',
temperature: '取樣溫度',
requestUrl: '介面位址',
invalidRequestUrl:
'請輸入以 /chat/completions 或 /completions 結尾的完整 HTTP(S) 位址,不含使用者名稱密碼、查詢參數或片段。',
requestRateHelp: '僅套用於目前場景;不限速仍受並行上限限制,可輸入小數 QPS。',
tokenizerModeHelp: '可用模式取決於壓測映像中的 vLLM 版本;任務準備時會驗證分詞器能否載入。',
invalidBaseUrl: '請輸入有效的 HTTP(S) 位址,不含使用者密碼、查詢參數或片段。',
invalidEndpoint: '請輸入符合目前協定、以 {0} 結尾的介面路徑,不含查詢參數或片段。',
invalidTokenizer: '請輸入絕對目錄路徑或儲存庫識別碼,不支援相對路徑、逗號或換行。',
invalidRequestRate: '請選擇不限速或輸入大於 0 的有限數值,支援小數。',
invalidCaCert: '請輸入主節點上 CA 憑證的絕對檔案路徑,不含逗號或換行。',
bodyOverride: '額外請求本文不能覆寫參數 {0}。',
invalidBody: '請輸入有效的 JSON 物件。',
invalidConcurrencies: '並行數需要 1–100 項,每項為 1–100000 的正整數。',
emptyRequestCount: '依並行數計算時,並行倍數和最少請求數不能同時為 0。',
requestCountLimit: '計算後的單測點請求數不能超過 1000000。',
contextExceeded: '輸入 Token、輸出 Token 與範本預留之和超過上下文上限。',
planLimit: '需要 1–100 個場景,展開後的測點總數不能超過 2000。',
caCertHelp: '選擇主節點伺服器上的 CA 憑證檔案,用於驗證 HTTPS 服務;留空使用系統憑證。',
extraBody: '額外請求本文',
entryKey: '參數名稱',
entryValue: '參數值',
tokenizerMode: '分詞器模式',
requireIdle: '開始前檢查服務閒置狀態',
requireIdleHelp:
'在預熱和壓力測試前檢查一次 /metrics。服務忙碌或無法確認閒置時停止;無法阻止測試期間的其他流量。',
trustRemoteCode: '信任遠端程式碼',
trustRemoteCodeHelp: '允許在壓測容器內執行模型儲存庫的自訂分詞器程式碼。僅在需要且來源可信時開啟。',
tokenizerModes: { auto: '自動(建議)', slow: '慢速分詞器', mistral: 'Mistral 專用' },
quickDescription: '單一請求驗證服務連線與測量流程。',
regularDescription: '短、中、長三類負載,適合日常效能測試。',
fullDescription: '多場景、多並行與重複測量,包含長輸入和共用前綴。',
customDescription: '從一個場景開始,依需求設定參數並新增場景。',
scenarios: '場景',
builtIn: '內建',
builtInHelp: '內建方案不可編輯或刪除,可在建立測試時調整本次設定。',
modified: '已調整',
imageExists: '本機已存在',
imageNeedsPull: '需拉取',
remark: '備註',
source: '本機服務',
mode: '測試方案',
quick: '快速驗證',
regular: '常規測試',
full: '完整測試',
custom: '自訂',
addScenario: '新增場景',
advanced: '進階設定',
points: '測點',
warmups: '預熱請求數',
fixedRequests: '固定請求數',
multiplier: '請求數並行倍數',
minRequests: '最少請求數',
trials: '重複次數',
prefixRatio: '共用前綴比例',
totalTimeout: '整輪逾時(秒)',
prepareTimeout: '準備逾時(秒)',
interval: '測點間隔(秒)',
context: '上下文上限(0 表示未知)',
reserve: '範本預留 Token',
storage: '儲存上限(MiB)',
stopOnError: '發生錯誤時停止後續測點',
caCert: 'CA 憑證路徑',
validTrials: '有效 / 總次數',
retry: '重測選取測點',
environment: '執行環境',
plan: '凍結設定',
clientHelp: '壓測容器在主節點執行,向既有服務傳送請求。不會啟動推論服務,也不需分配 GPU。',
retryHelp: '本輪只重測選取的異常測點,保留參數並重新分配隨機種子。',
secretHelp: '複製設定不包含 API Key 與請求標頭,請視需要重新填寫。',
resultHelp:
'彙總僅採用成功且無請求異常的重複測試,顯示中位數與最小/最大值。P99 是各次 P99 的中位數;快取與外部流量未受控。',
},
benchmarkOverview: {
noResults: '沒有有效的測試結果,請查看失敗原因和日誌。',
noGroupResults: '此組合沒有有效結果,請切換場景或並行數。',
partial: '部分結果:本次測試未完整成功,指標僅代表有效測點。',
highlights: '核心指標',
aggregate: '目前組合的有效重複測試中位數;點選指標查看統計詳情。',
trend: '並行趨勢',
std: '標準差',
min: '最小值',
max: '最大值',
tpot: '單 Token 耗時',
tpotHelp: '首 Token 之後,每個輸出 Token 的平均產生時間。',
itl: 'Token 間延遲',
itlHelp: '串流回應中相鄰 Token 傳回的時間間隔。',
e2el: '端到端延遲',
e2elHelp: '從送出請求到完整接收回應的時間。',
requestHelp: '每秒完成的請求數。',
usageHelp: '僅統計目前組合的有效重複測試,數值為各次結果的中位數,並非整輪請求總計;異常測點請至測點頁查看。',
duration: '測試耗時',
throughput: '吞吐指標',
latency: '延遲指標',
usage: '請求與用量(有效測試)',
},
benchmarkTokenizer: {
title: '分詞器管理',
local: '登記本機目錄',
upload: '上傳分詞器套件',
uploadHelp: '上傳不含模型權重的 ZIP / tar.gz 分詞器套件。壓縮檔上限 128 MiB,解壓縮後上限 256 MiB。',
source: '匯入方式',
deleteLocal: '移除分詞器「{0}」的登記紀錄?原始目錄與檔案將保留。',
deleteUpload: '刪除分詞器「{0}」及其上傳檔案?歷史測試報告將保留。',
select: '選擇分詞器',
selectHelp: '離線環境建議從管理清單選擇;自訂輸入仍支援本機目錄或儲存庫名稱。',
},
container: { container: {
createByCommand: '指令建立', createByCommand: '指令建立',
importContainerBackupTip: '匯入容器備份檔案,僅支援 .tar.gz 格式', importContainerBackupTip: '匯入容器備份檔案,僅支援 .tar.gz 格式',
@@ -2794,6 +2932,20 @@ const message = {
stopSSHWarn: '是否斷開此SSH連接', stopSSHWarn: '是否斷開此SSH連接',
}, },
setting: { setting: {
panelCache: {
title: '快取設定',
tabs: '標籤頁快取',
pagination: '分頁快取',
refresh: '重新整理頻率快取',
search: '搜尋輸入框快取',
duration: '快取時間',
'24h': '24h',
week: '一週',
month: '一個月',
forever: '永久',
helper: '有效期從狀態最後一次修改開始計算;過期後恢復預設值。',
},
panel: '面板', panel: '面板',
user: '面板使用者', user: '面板使用者',
userChange: '修改面板使用者', userChange: '修改面板使用者',
@@ -3451,6 +3603,7 @@ const message = {
link: '連結', link: '連結',
showCurrentArch: '目前伺服器架構應用', showCurrentArch: '目前伺服器架構應用',
syncLocalApp: '同步本機應用', syncLocalApp: '同步本機應用',
localAppSyncToNodes: '同步本機應用時聯動子節點',
memoryRequiredHelper: '目前應用記憶體需求 {0}', memoryRequiredHelper: '目前應用記憶體需求 {0}',
gpuConfig: '開啟 GPU 支援', gpuConfig: '開啟 GPU 支援',
gpuConfigHelper: '請確保機器有 NVIDIA GPU 並且安裝 NVIDIA 驅動 和 NVIDIA docker Container Toolkit', gpuConfigHelper: '請確保機器有 NVIDIA GPU 並且安裝 NVIDIA 驅動 和 NVIDIA docker Container Toolkit',
@@ -3970,7 +4123,7 @@ const message = {
domainHelper: '一行一個網域名稱,支援*和IP位址', domainHelper: '一行一個網域名稱,支援*和IP位址',
pushDir: '推送憑證到本機目錄', pushDir: '推送憑證到本機目錄',
dir: '目錄', dir: '目錄',
pushDirHelper: '會在此目錄下產生兩個檔案,憑證檔案:fullchain.pem 金鑰檔案:privkey.pem', pushDirHelper: '支援選擇多個目錄,也可每行填寫一個目錄。每個目錄下都會產生 fullchain.pem 和 privkey.pem。',
organizationDetail: '機構詳情', organizationDetail: '機構詳情',
fromWebsite: '從網站取得', fromWebsite: '從網站取得',
dnsMauanlHelper: '手動解析模式需要在建立完之後點選申請按鈕取得 DNS 解析值', dnsMauanlHelper: '手動解析模式需要在建立完之後點選申請按鈕取得 DNS 解析值',
+162 -8
View File
@@ -473,6 +473,7 @@ const message = {
recommend: '推荐', recommend: '推荐',
dir: '目录', dir: '目录',
alias: '别名', alias: '别名',
quickDirDelete: '确定删除({0})',
quickDir: '快捷目录', quickDir: '快捷目录',
minQuickJump: '请至少设置一个快速跳转入口!', minQuickJump: '请至少设置一个快速跳转入口!',
maxQuickJump: '最多可设置四个快速跳转入口!', maxQuickJump: '最多可设置四个快速跳转入口!',
@@ -756,6 +757,13 @@ const message = {
}, },
aiTools: { aiTools: {
agents: { agents: {
deepSeekAccessHostHelper:
'填写浏览器实际访问的 IPv4 地址或主机名,不含协议、端口或路径。HTTPS 使用应用的本地证书。',
deepSeekAuthHelper: '用户名仅支持字母、数字、点、下划线和连字符;密码至少 12 位,不含首尾空格或换行。',
octopAuthHelper:
'密码至少 8 位且包含字母和数字,避免常见弱密码。若初始化拒绝该密码,请查看应用日志中的设置说明。',
initialCredentials: '初始登录信息;若已在应用内修改,以当前密码为准。',
nativeAgentAccessUnavailable: '现有安装缺少兼容的 HTTPS 访问地址或端口,请检查应用设置。',
agent: '智能体', agent: '智能体',
account: '模型账号', account: '模型账号',
batchOperation: '批量操作', batchOperation: '批量操作',
@@ -1418,35 +1426,31 @@ const message = {
statusWaiting: '等待中', statusWaiting: '等待中',
base: '基础信息', base: '基础信息',
backend: '后端', backend: '后端',
baseUrl: '服务地址',
endpoint: '接口路径', endpoint: '接口路径',
tokenizer: '分词器', tokenizer: '分词器',
tokenizerPlaceholder: '请选择或输入本地分词器目录,例如 /opt/1panel/tokenizers/DeepSeek-V3', tokenizerPlaceholder: '请输入主节点上的绝对目录或 Hugging Face 仓库 ID',
runConfig: '运行配置', runConfig: '运行配置',
rawResult: '原始结果', rawResult: '原始结果',
resultMetrics: '结果指标', resultMetrics: '结果指标',
contextTokens: '上下文长度', contextTokens: '上下文长度',
contextTokensHelper: '本次测试的输入 Token 与输出 Token 上限之和。',
outputThroughput: '输出吞吐', outputThroughput: '输出吞吐',
outputThroughputHelper: '模型每秒生成的输出 Token 数,越高表示生成速度越快。', outputThroughputHelper: '模型每秒生成的输出 Token 数,越高表示生成速度越快。',
totalThroughput: '总吞吐', totalThroughput: '总吞吐',
totalThroughputHelper: '每秒处理的输入与输出 Token 总数,用于衡量整体处理能力。', totalThroughputHelper: '每秒处理的输入与输出 Token 总数,用于衡量整体处理能力。',
firstTokenLatency: '首 Token 延迟', firstTokenLatency: '首 Token 延迟',
firstTokenLatencyHelper: '从请求发出到收到第一个 Token 的时间,越低表示响应越快。', firstTokenLatencyHelper: '从请求发出到收到第一个 Token 的时间,越低表示响应越快。',
tokenValueHelper: '请输入正整数或 k 单位,例如 512、1k、32k',
numPrompts: '请求数量', numPrompts: '请求数量',
concurrency: '并发数', concurrency: '并发数',
requestRate: '请求速率', requestRate: '请求速率',
requestRateUnlimited: '不限速(最大吞吐)', requestRateUnlimited: '不限速(最大吞吐)',
requestRateCustom: '自定义 QPS',
requestRateCustomPlaceholder: '请输入每秒请求数,如 2.5', requestRateCustomPlaceholder: '请输入每秒请求数,如 2.5',
successfulRequests: '成功请求', successfulRequests: '成功请求',
requestThroughput: '请求吞吐', requestThroughput: '请求吞吐',
mean: '平均', mean: '平均',
median: '中位数', median: '中位数',
timeout: '超时时间(秒)',
image: 'vLLM 镜像', image: 'vLLM 镜像',
ignoreEos: '忽略 EOS', ignoreEos: '忽略结束标记(EOS)',
ignoreEosHelp: '避免因结束标记提前停止,便于按设定输出长度压测;其他停止条件仍有效,需服务支持。',
extraHeaders: '额外请求头', extraHeaders: '额外请求头',
}, },
vllmMonitor: { vllmMonitor: {
@@ -1651,6 +1655,140 @@ const message = {
modelType: '模型类型', modelType: '模型类型',
}, },
}, },
benchmarkPlan: {
prepareTimeoutHelp: '准备阶段的时间上限,不计入整轮超时。',
pointTimeoutHelp: '每个测点独立计时,含启动、预热和全部请求,受整轮剩余时间限制。',
totalTimeoutHelp: '准备完成后计时,覆盖所有测点及间隔,超时后停止。',
concurrencyLevels: '并发档位',
concurrencyHelp: '各档位依次测试,数值表示同时进行的请求数量上限。',
requestCountMode: '请求数量设置',
fixedRequestMode: '每档固定请求数',
scaledRequestMode: '按并发数计算',
requestsPerPoint: '每档请求数',
requestCountFormula:
'每档:固定值,或 max(并发数 × 倍数,最少请求数);场景总量:各档请求数之和 × 重复次数,不含预热。',
controlLog: '准备与控制日志',
executionLog: '执行日志',
pointLog: '测点 #{0} · {1} · 执行日志',
archivedLog: '{0}(历史 {1})',
tokenLengths: '输入 / 输出 Token',
kind: '结果分类',
kindServe: 'serve(常规测试)',
kindPD: 'pd(Prefill/Decode 探测)',
saveDetailed: '保存请求明细',
saveDetailedHelp: '保存每条请求的长度、延迟、生成文本和错误信息。关闭后仍保留汇总指标,可减少结果文件体积。',
parameters: '参数',
temperatureHelp: '控制生成内容的随机性;0 通常更稳定,值越高越随机。',
pointTimeout: '单测点超时(秒)',
temperature: '采样温度',
requestUrl: '接口地址',
invalidRequestUrl:
'请输入以 /chat/completions 或 /completions 结尾的完整 HTTP(S) 地址,不含用户名密码、查询参数或片段。',
requestRateHelp: '仅作用于当前场景;不限速仍受并发上限约束,可输入小数 QPS。',
tokenizerModeHelp: '可用模式取决于压测镜像中的 vLLM 版本;任务准备时会验证分词器能否加载。',
invalidBaseUrl: '请输入有效的 HTTP(S) 地址,不含用户名密码、查询参数或片段。',
invalidEndpoint: '请输入符合当前协议、以 {0} 结尾的接口路径,不含查询参数或片段。',
invalidTokenizer: '请输入绝对目录路径或仓库标识,不支持相对路径、逗号或换行。',
invalidRequestRate: '请选择不限速或输入大于 0 的有限数值,支持小数。',
invalidCaCert: '请输入主节点上 CA 证书的绝对文件路径,不含逗号或换行。',
bodyOverride: '额外请求体不能覆盖参数 {0}。',
invalidBody: '请输入合法的 JSON 对象。',
invalidConcurrencies: '并发数需要 1–100 项,每项为 1–100000 的正整数。',
emptyRequestCount: '按并发数计算时,并发倍数和最少请求数不能同时为 0。',
requestCountLimit: '计算后的单测点请求数不能超过 1000000。',
contextExceeded: '输入 Token、输出 Token 与模板预留之和超过上下文上限。',
planLimit: '需要 1–100 个场景,展开后的测点总数不能超过 2000。',
caCertHelp: '选择主节点服务器上的 CA 证书文件,用于校验 HTTPS 服务;留空使用系统证书。',
extraBody: '额外请求体',
entryKey: '参数名',
entryValue: '参数值',
tokenizerMode: '分词器模式',
requireIdle: '开始前检查服务空闲',
requireIdleHelp: '在预热和压测前检查一次 /metrics。服务忙或无法确认空闲时停止;不能阻止测试期间的其他流量。',
trustRemoteCode: '信任远程代码',
trustRemoteCodeHelp: '允许在压测容器内执行模型仓库的自定义分词器代码。仅在需要且来源可信时开启。',
tokenizerModes: { auto: '自动(推荐)', slow: '慢速分词器', mistral: 'Mistral 专用' },
quickDescription: '单请求验证服务连通性与测量流程。',
regularDescription: '短、中、长三类负载,适合日常性能测试。',
fullDescription: '多场景、多并发与重复测量,包含长输入和共享前缀。',
customDescription: '从一个场景开始,按需设置参数并添加场景。',
scenarios: '场景',
builtIn: '内置',
builtInHelp: '内置方案不可编辑或删除,可在创建测试时调整本次配置。',
modified: '已调整',
imageExists: '本机已存在',
imageNeedsPull: '需拉取',
remark: '备注',
source: '本机服务',
mode: '测试方案',
quick: '快速验证',
regular: '常规测试',
full: '完整测试',
custom: '自定义',
addScenario: '添加场景',
advanced: '高级配置',
points: '测点',
warmups: '预热请求数',
fixedRequests: '固定请求数',
multiplier: '请求数并发倍数',
minRequests: '最少请求数',
trials: '重复次数',
prefixRatio: '公共前缀比例',
totalTimeout: '整轮超时(秒)',
prepareTimeout: '准备超时(秒)',
interval: '测点间隔(秒)',
context: '上下文上限(0 表示未知)',
reserve: '模板预留 Token',
storage: '存储上限(MiB)',
stopOnError: '遇到错误停止后续测点',
caCert: 'CA 证书路径',
validTrials: '有效 / 总次数',
retry: '补测选中测点',
environment: '执行环境',
plan: '冻结配置',
clientHelp: '压测容器在主节点运行,向已有服务发送请求。不会启动推理服务,也不需要分配 GPU。',
retryHelp: '本轮只补测选中的异常测点,保留参数并重新分配随机种子。',
secretHelp: '复制配置不包含 API Key 和请求头,请按需重新填写。',
resultHelp:
'汇总仅使用成功且无请求异常的重复测试,显示中位数和最小/最大值。P99 是每次测试 P99 的中位数;缓存和外部流量未受控。',
},
benchmarkOverview: {
noResults: '没有有效的测试结果,请查看失败原因和日志。',
noGroupResults: '此组合没有有效结果,请切换场景或并发数。',
partial: '部分结果:本次测试未完整成功,指标仅代表有效测点。',
highlights: '核心指标',
aggregate: '当前组合的有效重复测试中位数;点击指标查看统计详情。',
trend: '并发趋势',
std: '标准差',
min: '最小值',
max: '最大值',
tpot: '单 Token 耗时',
tpotHelp: '首 Token 之后,每个输出 Token 的平均生成耗时。',
itl: 'Token 间延迟',
itlHelp: '流式响应中相邻 Token 返回的时间间隔。',
e2el: '端到端延迟',
e2elHelp: '从请求发出到完整响应接收完成的时间。',
requestHelp: '每秒完成的请求数。',
usageHelp:
'仅统计当前组合的有效重复测试,数值为每次测试结果的中位数,不是整轮请求总计;异常测点请在测点页查看。',
duration: '测试耗时',
throughput: '吞吐指标',
latency: '延迟指标',
usage: '请求与用量(有效测试)',
},
benchmarkTokenizer: {
title: '分词器管理',
local: '登记本地目录',
upload: '上传分词器包',
uploadHelp: '上传 ZIP / tar.gz 分词器包,不含模型权重。压缩包最大 128 MiB,解压后最大 256 MiB。',
source: '导入方式',
deleteLocal: '移除分词器「{0}」的登记记录?原目录和文件将保留。',
deleteUpload: '删除分词器「{0}」及其上传文件?历史测试报告将保留。',
select: '选择分词器',
selectHelp: '离线环境推荐从管理列表选择;自定义输入仍支持本地目录或仓库名称。',
},
container: { container: {
createByCommand: '命令创建', createByCommand: '命令创建',
importContainerBackupTip: '导入容器备份文件,仅支持 .tar.gz 格式', importContainerBackupTip: '导入容器备份文件,仅支持 .tar.gz 格式',
@@ -2825,6 +2963,20 @@ const message = {
stopSSHWarn: '是否断开此SSH连接', stopSSHWarn: '是否断开此SSH连接',
}, },
setting: { setting: {
panelCache: {
title: '缓存设置',
tabs: '标签页缓存',
pagination: '翻页缓存',
refresh: '刷新频率缓存',
search: '搜索输入框缓存',
duration: '缓存时间',
'24h': '24h',
week: '一周',
month: '一月',
forever: '永久',
helper: '有效期从状态最后一次修改开始计算;过期后恢复默认值。',
},
panel: '面板', panel: '面板',
user: '面板用户', user: '面板用户',
userChange: '修改面板用户', userChange: '修改面板用户',
@@ -3492,6 +3644,7 @@ const message = {
link: '链接', link: '链接',
showCurrentArch: '本服务器架构应用', showCurrentArch: '本服务器架构应用',
syncLocalApp: '同步本地应用', syncLocalApp: '同步本地应用',
localAppSyncToNodes: '同步本地应用时联动子节点',
memoryRequiredHelper: '当前应用内存需求 {0}', memoryRequiredHelper: '当前应用内存需求 {0}',
gpuConfig: '开启 GPU 支持', gpuConfig: '开启 GPU 支持',
gpuConfigHelper: '请确保机器有 NVIDIA GPU 并且安装 NVIDIA 驱动 和 NVIDIA docker Container Toolkit', gpuConfigHelper: '请确保机器有 NVIDIA GPU 并且安装 NVIDIA 驱动 和 NVIDIA docker Container Toolkit',
@@ -4015,7 +4168,8 @@ const message = {
domainHelper: '一行一个域名,支持*和IP地址', domainHelper: '一行一个域名,支持*和IP地址',
pushDir: '推送证书到本地目录', pushDir: '推送证书到本地目录',
dir: '目录', dir: '目录',
pushDirHelper: '会在此目录下生成两个文件,证书文件:fullchain.pem 密钥文件:privkey.pem', pushDirHelper:
'支持选择多个目录,也可每行填写一个目录。每个目录下都会生成证书文件 fullchain.pem 和密钥文件 privkey.pem',
organizationDetail: '机构详情', organizationDetail: '机构详情',
fromWebsite: '从网站中获取', fromWebsite: '从网站中获取',
dnsMauanlHelper: '手动解析模式需要在创建完之后点击申请按钮获取 DNS 解析值', dnsMauanlHelper: '手动解析模式需要在创建完之后点击申请按钮获取 DNS 解析值',
@@ -55,6 +55,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { applyPanelCacheSettings } from '@/utils/panel-cache';
import { ref, computed, onBeforeUnmount, onMounted, watch } from 'vue'; import { ref, computed, onBeforeUnmount, onMounted, watch } from 'vue';
import { RouteRecordRaw, useRoute, useRouter } from 'vue-router'; import { RouteRecordRaw, useRoute, useRouter } from 'vue-router';
import { loadingSvg } from '@/utils/svg'; import { loadingSvg } from '@/utils/svg';
@@ -141,6 +142,7 @@ const search = async () => {
settingInfo = res.data; settingInfo = res.data;
version.value = res.data.systemVersion; version.value = res.data.systemVersion;
menuAccordion.value = res.data.menuAccordion === 'Enable'; menuAccordion.value = res.data.menuAccordion === 'Enable';
applyPanelCacheSettings(res.data.panelCache);
} catch (error) { } catch (error) {
version.value = ''; version.value = '';
} }
+4
View File
@@ -54,6 +54,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { prunePanelCache } from '@/utils/panel-cache';
import { onMounted, computed, ref, watch, onBeforeUnmount } from 'vue'; import { onMounted, computed, ref, watch, onBeforeUnmount } from 'vue';
import { Sidebar, Footer, AppMain, MobileHeader, Tabs } from './components'; import { Sidebar, Footer, AppMain, MobileHeader, Tabs } from './components';
import TerminalHost from '@/components/terminal/host.vue'; import TerminalHost from '@/components/terminal/host.vue';
@@ -106,6 +107,7 @@ const loadingText = computed(() =>
); );
let timer: NodeJS.Timer | null = null; let timer: NodeJS.Timer | null = null;
let cacheCleanupTimer: ReturnType<typeof setInterval> | undefined;
const classObj = computed(() => { const classObj = computed(() => {
return { return {
@@ -186,10 +188,12 @@ const loadStatus = async () => {
} }
}; };
onBeforeUnmount(() => { onBeforeUnmount(() => {
clearInterval(cacheCleanupTimer);
clearInterval(Number(timer)); clearInterval(Number(timer));
timer = null; timer = null;
}); });
onMounted(() => { onMounted(() => {
cacheCleanupTimer = setInterval(() => prunePanelCache(), 300000);
if (openMenuTabs.value && !tabsStore.activeTabPath) { if (openMenuTabs.value && !tabsStore.activeTabPath) {
handleMenuClick('/'); handleMenuClick('/');
} }
+22 -10
View File
@@ -1,3 +1,4 @@
import { panelCacheKey, readPanelCache, writePanelCache, removePanelCache, prunePanelCache } from '@/utils/panel-cache';
import router from '@/routers/router'; import router from '@/routers/router';
import NProgress from '@/config/nprogress'; import NProgress from '@/config/nprogress';
import { useGlobalStore } from '@/composables/useGlobalStore'; import { useGlobalStore } from '@/composables/useGlobalStore';
@@ -102,29 +103,34 @@ router.beforeEach(async (to, from) => {
if (to.name === 'Expired') { if (to.name === 'Expired') {
return true; return true;
} }
const activeMenuKey = 'cachedRoute' + (to.meta.activeMenu || ''); const activeMenuKey = panelCacheKey(String(to.meta.activeMenu || ''));
if (to.query.uncached != undefined) { if (to.query.uncached != undefined) {
const query = { ...to.query }; const query = { ...to.query };
delete query.uncached; delete query.uncached;
localStorage.removeItem(activeMenuKey); removePanelCache('tabs', activeMenuKey);
return { path: to.path, query }; return { path: to.path, query };
} }
const cachedRoute = localStorage.getItem(activeMenuKey); const cachedRoute = readPanelCache<string>('tabs', activeMenuKey);
if ( if (
to.meta.activeMenu && to.meta.activeMenu &&
to.meta.activeMenu != from.meta.activeMenu && to.meta.activeMenu != from.meta.activeMenu &&
typeof cachedRoute === 'string' &&
cachedRoute && cachedRoute &&
cachedRoute !== to.path && cachedRoute !== to.path &&
!isRedirecting !isRedirecting
) { ) {
const cachedRouteInfo = router.resolve(cachedRoute); const cachedRouteInfo = router.resolve(cachedRoute);
if (cachedRouteInfo.matched.length > 0 && hasRouteAccess(cachedRouteInfo)) { if (
cachedRouteInfo.matched.length > 0 &&
cachedRouteInfo.meta.activeMenu === to.meta.activeMenu &&
hasRouteAccess(cachedRouteInfo)
) {
isRedirecting = true; isRedirecting = true;
NProgress.done(); NProgress.done();
return cachedRoute; return cachedRoute;
} }
localStorage.removeItem(activeMenuKey); removePanelCache('tabs', activeMenuKey);
} }
if (!hasRouteAccess(to)) { if (!hasRouteAccess(to)) {
@@ -135,7 +141,13 @@ router.beforeEach(async (to, from) => {
return true; return true;
}); });
router.afterEach((to) => { router.afterEach((to, _from, failure) => {
prunePanelCache();
if (failure) {
isRedirecting = false;
NProgress.done();
return;
}
if (to.meta.activeMenu && !to.meta.ignoreTab && !isRedirecting) { if (to.meta.activeMenu && !to.meta.ignoreTab && !isRedirecting) {
let notMathParam = true; let notMathParam = true;
if (to.matched.some((record) => record.path.includes(':'))) { if (to.matched.some((record) => record.path.includes(':'))) {
@@ -143,13 +155,13 @@ router.afterEach((to) => {
} }
if (notMathParam) { if (notMathParam) {
if (to.meta.activeMenu === '/cronjobs' && to.path === '/cronjobs/cronjob/operate') { if (to.meta.activeMenu === '/cronjobs' && to.path === '/cronjobs/cronjob/operate') {
localStorage.setItem('cachedRoute' + to.meta.activeMenu, '/cronjobs/cronjob'); writePanelCache('tabs', panelCacheKey(String(to.meta.activeMenu)), '/cronjobs/cronjob');
} else if (to.meta.activeMenu === '/containers' && to.path === '/containers/container/operate') { } else if (to.meta.activeMenu === '/containers' && to.path === '/containers/container/operate') {
localStorage.setItem('cachedRoute' + to.meta.activeMenu, '/containers/container'); writePanelCache('tabs', panelCacheKey(String(to.meta.activeMenu)), '/containers/container');
} else if (to.meta.activeMenu === '/toolbox' && to.path === '/toolbox/clam/setting') { } else if (to.meta.activeMenu === '/toolbox' && to.path === '/toolbox/clam/setting') {
localStorage.setItem('cachedRoute' + to.meta.activeMenu, '/toolbox/clam'); writePanelCache('tabs', panelCacheKey(String(to.meta.activeMenu)), '/toolbox/clam');
} else { } else {
localStorage.setItem('cachedRoute' + to.meta.activeMenu, to.path); writePanelCache('tabs', panelCacheKey(String(to.meta.activeMenu)), to.path);
} }
} }
} }
+3
View File
@@ -1,5 +1,6 @@
import { RouteRecordRaw } from 'vue-router'; import { RouteRecordRaw } from 'vue-router';
import { DeviceType } from '@/enums/app'; import { DeviceType } from '@/enums/app';
import type { PanelCacheSettings } from '@/utils/panel-cache';
export interface ThemeConfigProp { export interface ThemeConfigProp {
panelName: string; panelName: string;
primary: string; primary: string;
@@ -34,6 +35,8 @@ export interface GlobalState {
isFullScreen: boolean; isFullScreen: boolean;
openMenuTabs: boolean; openMenuTabs: boolean;
menuAccordion: boolean; menuAccordion: boolean;
panelCache: PanelCacheSettings;
cacheUserID: string;
watermark: Watermark | null; watermark: Watermark | null;
watermarkShow: boolean; watermarkShow: boolean;
isLoading: boolean; isLoading: boolean;
+4
View File
@@ -6,6 +6,7 @@ import { DeviceType } from '@/enums/app';
import i18n, { setActiveLocale } from '@/lang'; import i18n, { setActiveLocale } from '@/lang';
import { isMasterOnlyPermissionCode, setMasterOnlyPermissionCodes, toManageCode } from '@/utils/permission-codes'; import { isMasterOnlyPermissionCode, setMasterOnlyPermissionCodes, toManageCode } from '@/utils/permission-codes';
import { clearPageStateCache } from '@/utils/page-state-cache'; import { clearPageStateCache } from '@/utils/page-state-cache';
import { defaultPanelCache } from '@/utils/panel-cache';
const CN_DOCS_URL = 'https://docs.fit2cloud.com/1panel'; const CN_DOCS_URL = 'https://docs.fit2cloud.com/1panel';
const INTL_DOCS_URL = 'https://1panel.pro/docs/v2'; const INTL_DOCS_URL = 'https://1panel.pro/docs/v2';
@@ -33,6 +34,8 @@ const GlobalStore = defineStore('GlobalState', {
isFullScreen: false, isFullScreen: false,
openMenuTabs: false, openMenuTabs: false,
menuAccordion: false, menuAccordion: false,
panelCache: { ...defaultPanelCache },
cacheUserID: '',
watermark: null, watermark: null,
watermarkShow: false, watermarkShow: false,
isLoading: false, isLoading: false,
@@ -123,6 +126,7 @@ const GlobalStore = defineStore('GlobalState', {
}, },
clearAuthInfo() { clearAuthInfo() {
clearPageStateCache(); clearPageStateCache();
this.cacheUserID = '';
this.permissions = []; this.permissions = [];
this.masterOnlyPermissions = []; this.masterOnlyPermissions = [];
this.nodeRoles = []; this.nodeRoles = [];
+4 -5
View File
@@ -1,6 +1,5 @@
import { bindSearchReset } from '@/utils/search-reset'; import { bindSearchState } from '@/utils/search-reset';
import router from '@/routers/router'; import { panelCacheKey } from '@/utils/panel-cache';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { ref, reactive, toRef } from 'vue'; import { ref, reactive, toRef } from 'vue';
import { MsgError } from '@/utils/message'; import { MsgError } from '@/utils/message';
@@ -49,8 +48,7 @@ export const ProcessStore = defineStore('ProcessStore', () => {
port: undefined, port: undefined,
}); });
const { currentNode } = useGlobalStore(); const searchState = bindSearchState(() => panelCacheKey('host/process'), {
bindSearchReset([() => router.currentRoute.value.path, currentNode], {
pid: toRef(psSearch, 'pid'), pid: toRef(psSearch, 'pid'),
name: toRef(psSearch, 'name'), name: toRef(psSearch, 'name'),
username: toRef(psSearch, 'username'), username: toRef(psSearch, 'username'),
@@ -321,6 +319,7 @@ export const ProcessStore = defineStore('ProcessStore', () => {
}; };
return { return {
restoreSearchState: searchState.restore,
isConnected, isConnected,
isConnecting, isConnecting,
psData, psData,
+43 -1
View File
@@ -1,10 +1,52 @@
import i18n from '@/lang'; import i18n from '@/lang';
import { compareVersion } from '@/utils/version'; import { compareVersion } from '@/utils/version';
import type { AI } from '@/api/interface/ai';
const openclawHTTPSVersion = '2026.3.13'; const openclawHTTPSVersion = '2026.3.13';
const openclawHTTPVersion = '2026.3.23'; const openclawHTTPVersion = '2026.3.23';
const openclawDefaultAccessHost = '127.0.0.1'; 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 => { export const getAgentProviderDisplayName = (provider: string, displayName?: string): string => {
if (provider === 'custom' || displayName === 'Custom') { if (provider === 'custom' || displayName === 'Custom') {
return i18n.global.t('container.custom'); return i18n.global.t('container.custom');
@@ -27,7 +69,7 @@ export const supportsAgentToken = (agentType: string): boolean => {
}; };
export const supportsAgentDashboardAuth = (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 => { export const isOpenclawHTTPSWindowVersion = (version: string): boolean => {
+130
View File
@@ -0,0 +1,130 @@
import GlobalStore from '@/store/modules/global';
export type CacheCategory = 'tabs' | 'pagination' | 'refresh' | 'search';
export interface PanelCacheSettings {
tabs: boolean;
pagination: boolean;
refresh: boolean;
search: boolean;
duration: '24h' | 'week' | 'month' | 'forever';
}
export const defaultPanelCache: PanelCacheSettings = {
tabs: true,
pagination: true,
refresh: true,
search: false,
duration: '24h',
};
const categories: CacheCategory[] = ['tabs', 'pagination', 'refresh', 'search'];
const durations = { '24h': 86400000, week: 604800000, month: 2592000000, forever: Infinity };
const prefix = '1panel:page-cache:v1:';
interface CacheEntry {
category: CacheCategory;
updatedAt: number;
value: unknown;
}
export const applyPanelCacheSettings = (raw: string) => {
let value: Partial<PanelCacheSettings> = {};
try {
value = JSON.parse(raw) || {};
} catch {
// Older servers do not return this setting yet.
}
const settings = { ...defaultPanelCache };
for (const category of categories) {
if (typeof value[category] === 'boolean') settings[category] = value[category];
}
if (Object.hasOwn(durations, value.duration || '')) settings.duration = value.duration;
const store = GlobalStore();
const changed =
settings.duration !== store.panelCache.duration ||
categories.some((category) => settings[category] !== store.panelCache[category]);
if (changed) store.panelCache = settings;
// Repeated settings loads share the normal cleanup interval.
prunePanelCache(changed);
};
export const panelCacheKey = (key: string): string => {
const { cacheUserID, currentNode } = GlobalStore();
return JSON.stringify([cacheUserID, currentNode, key]);
};
const storageKey = (category: CacheCategory, key: string) => prefix + JSON.stringify([category, key]);
const validEntry = (entry: CacheEntry, settings: PanelCacheSettings, now: number) => {
return (
entry &&
categories.includes(entry.category) &&
settings[entry.category] &&
Number.isFinite(entry.updatedAt) &&
entry.updatedAt <= now &&
now - entry.updatedAt < durations[settings.duration]
);
};
export const readPanelCache = <T>(category: CacheCategory, key: string): T | undefined => {
const store = GlobalStore();
if (!store.cacheUserID || !store.panelCache[category]) return;
try {
const target = storageKey(category, key);
const raw = localStorage.getItem(target);
if (!raw) return;
const entry: CacheEntry = JSON.parse(raw);
if (entry.category === category && validEntry(entry, store.panelCache, Date.now())) return entry.value as T;
localStorage.removeItem(target);
} catch {
// Storage can be unavailable or contain invalid data; use page defaults.
}
};
export const writePanelCache = (category: CacheCategory, key: string, value: unknown) => {
const store = GlobalStore();
if (!store.cacheUserID || !store.panelCache[category]) return;
try {
// Repeated visits and unchanged values must not extend the expiration time.
const previous = readPanelCache(category, key);
if (previous !== undefined && JSON.stringify(previous) === JSON.stringify(value)) return;
localStorage.setItem(storageKey(category, key), JSON.stringify({ category, updatedAt: Date.now(), value }));
} catch {
// A full or disabled localStorage must not block page interactions.
}
};
export const removePanelCache = (category: CacheCategory, key: string) => {
try {
localStorage.removeItem(storageKey(category, key));
} catch {
/* Storage is optional. */
}
};
let lastPruned = 0;
export const prunePanelCache = (force = false) => {
const now = Date.now();
if (!force && now - lastPruned < 300000) return;
lastPruned = now;
const settings = GlobalStore().panelCache;
try {
for (let index = localStorage.length - 1; index >= 0; index--) {
const key = localStorage.key(index);
if (!key) continue;
if (key.startsWith('1panel:search-state:v2:') || key.startsWith('cachedRoute')) {
// Legacy entries have neither ownership nor timestamps and cannot be safely restored.
localStorage.removeItem(key);
} else if (key.startsWith(prefix)) {
try {
if (validEntry(JSON.parse(localStorage.getItem(key)), settings, now)) continue;
} catch {
/* Remove malformed entries too. */
}
localStorage.removeItem(key);
}
}
} catch {
/* Storage is optional. */
}
};
export const readCachedPageSize = (key: string): number | undefined => {
const value = readPanelCache<number>('pagination', panelCacheKey(`size:${key}`));
return Number.isInteger(value) && value > 0 ? value : undefined;
};
+2
View File
@@ -22,6 +22,7 @@ export const syncAuthInfo = async (currentNode?: string) => {
const storeCurrentNode = globalStore.currentNode; const storeCurrentNode = globalStore.currentNode;
if (!globalStore.isEnterprise) { if (!globalStore.isEnterprise) {
const res = await getUserInfo(); const res = await getUserInfo();
globalStore.cacheUserID = `community:${res.data.id}`;
globalStore.setAuthInfo({ globalStore.setAuthInfo({
isAdmin: res.data.role === 'ADMIN', isAdmin: res.data.role === 'ADMIN',
permissions: res.data.permissions || [], permissions: res.data.permissions || [],
@@ -31,6 +32,7 @@ export const syncAuthInfo = async (currentNode?: string) => {
return res.data; return res.data;
} }
const res = await getEnterpriseUserInfo(currentNode ?? storeCurrentNode); const res = await getEnterpriseUserInfo(currentNode ?? storeCurrentNode);
globalStore.cacheUserID = `enterprise:${res.data.id}`;
globalStore.setAuthInfo({ globalStore.setAuthInfo({
isAdmin: res.data.role === 'ADMIN', isAdmin: res.data.role === 'ADMIN',
permissions: res.data.permissions || [], permissions: res.data.permissions || [],
+160 -7
View File
@@ -1,12 +1,165 @@
import { watch, type Ref, type WatchSource } from 'vue'; import { storeToRefs } from 'pinia';
import { nextTick, onActivated, onDeactivated, onScopeDispose, watch, type Ref } from 'vue';
import { panelCacheKey, readPanelCache, writePanelCache } from '@/utils/panel-cache';
import GlobalStore from '@/store/modules/global';
export type SearchFields = Record<string, Ref<string | number | undefined>>; export type SearchFields = Record<string, Ref<string | number | undefined>>;
type SearchValues = Record<string, string | number | undefined>;
export const bindSearchReset = (sources: WatchSource[], fields: SearchFields) => { // Keep categories separate so changing search text does not renew pagination's lifetime.
const defaults = Object.values(fields).map((field) => ({ field, value: field.value })); const searchStorage = {
const reset = () => { save(key: string, values: SearchValues) {
for (const { field, value } of defaults) field.value = value; for (const category of ['search', 'pagination'] as const) {
const entries = Object.entries(values).filter(
([name]) => (name === 'page' || name === 'pageSize') === (category === 'pagination'),
);
if (entries.length) {
writePanelCache(
category,
key,
Object.fromEntries(entries.map(([name, value]) => [name, value ?? null])),
);
}
}
},
read(key: string): SearchValues {
const values = {
...readPanelCache<SearchValues>('search', key),
...readPanelCache<SearchValues>('pagination', key),
};
return Object.fromEntries(
Object.entries(values)
.filter(
([, value]) =>
value === null ||
typeof value === 'string' ||
(typeof value === 'number' && Number.isFinite(value)),
)
.map(([name, value]) => [name, value === null ? undefined : value]),
);
},
};
export const bindSearchState = (key: () => string | undefined, fields: SearchFields, storage = searchStorage) => {
const defaults = Object.fromEntries(Object.entries(fields).map(([name, field]) => [name, field.value]));
const names = Object.keys(fields).sort((left, right) => Number(left === 'page') - Number(right === 'page'));
let pending: { key: string; values: SearchValues } | undefined;
let queued = false;
const flush = () => {
if (!pending) return;
const snapshot = pending;
pending = undefined;
storage.save(snapshot.key, snapshot.values);
}; };
watch(sources, reset, { flush: 'sync' }); let restoring = false;
return reset; let activeKey: string | undefined;
let active = true;
let resuming = false;
let resumeVersion = 0;
const pause = () => {
flush();
active = false;
resuming = false;
resumeVersion++;
};
onScopeDispose(pause);
const restore = () => {
// Commit the previous node/resource before restoring another one.
flush();
activeKey = key();
if (!activeKey) return;
const saved = storage.read(activeKey) || defaults;
restoring = true;
try {
// Restore pagination last: search/filter watchers may reset it to page one.
for (const name of names) {
const value = Object.hasOwn(saved, name) ? saved[name] : defaults[name];
const initial = defaults[name];
const invalidPage =
(name === 'page' || name === 'pageSize') &&
(typeof value !== 'number' || !Number.isInteger(value) || value < 1);
fields[name].value =
invalidPage || (initial !== undefined && value !== undefined && typeof value !== typeof initial)
? initial
: value;
}
} finally {
restoring = false;
}
};
watch(
key,
() => {
if (active || resuming) restore();
},
{ immediate: true, flush: 'sync' },
);
watch(
() => Object.fromEntries(Object.entries(fields).map(([name, field]) => [name, field.value])),
(values, previous) => {
if (restoring || !active || !activeKey) return;
const changed = Object.entries(values).filter(([name, value]) => value !== previous[name]);
const paginationChanged = changed.some(([name]) => name === 'page' || name === 'pageSize');
const searchChanged = changed.some(([name]) => name !== 'page' && name !== 'pageSize');
const snapshot = Object.fromEntries(
Object.entries(values).filter(([name]) =>
name === 'page' || name === 'pageSize' ? paginationChanged : searchChanged,
),
);
pending = { key: activeKey, values: { ...pending?.values, ...snapshot } };
if (queued) return;
queued = true;
queueMicrotask(() => {
queued = false;
flush();
});
},
{ flush: 'sync' },
);
return {
restore,
pause,
resume: async () => {
const version = ++resumeVersion;
resuming = true;
active = false;
restore();
const restoredKey = activeKey;
const page = fields.page?.value;
await nextTick();
if (version !== resumeVersion) return false;
// Search watchers may reset the page while restoring a changed query.
if (activeKey === restoredKey && fields.page && page !== undefined) fields.page.value = page;
active = true;
resuming = false;
return true;
},
};
};
interface SearchStateOptions {
scope?: () => string | number | undefined;
onRevisit: () => unknown;
}
/** Keep queries across page visits and reload data when a cached page becomes active. */
export const useSearchState = (page: string, fields: SearchFields, options: SearchStateOptions) => {
const { cacheUserID } = storeToRefs(GlobalStore());
const binding = bindSearchState(() => {
const scope = options.scope?.();
if (options.scope && scope === undefined) return undefined;
// Track account and node changes, including initial auth synchronisation.
if (!cacheUserID.value) return undefined;
return panelCacheKey(JSON.stringify([page, scope ?? '']));
}, fields);
let deactivated = false;
onDeactivated(() => {
deactivated = true;
binding.pause();
});
onActivated(async () => {
if (!deactivated) return;
deactivated = false;
if (await binding.resume()) return options.onRevisit();
});
}; };
+2
View File
@@ -1,3 +1,4 @@
import { applyPanelCacheSettings } from '@/utils/panel-cache';
import { import {
getLicenseStatus, getLicenseStatus,
getMasterLicenseStatus, getMasterLicenseStatus,
@@ -86,6 +87,7 @@ const loadDataFromDB = async () => {
globalStore.entrance = res.data.securityEntrance; globalStore.entrance = res.data.securityEntrance;
globalStore.openMenuTabs = res.data.menuTabs === 'Enable'; globalStore.openMenuTabs = res.data.menuTabs === 'Enable';
globalStore.menuAccordion = res.data.menuAccordion === 'Enable'; globalStore.menuAccordion = res.data.menuAccordion === 'Enable';
applyPanelCacheSettings(res.data.panelCache);
}; };
export async function loadProductProFromDB() { export async function loadProductProFromDB() {
@@ -7,6 +7,8 @@
<el-option label="OpenClaw" value="openclaw" /> <el-option label="OpenClaw" value="openclaw" />
<el-option label="Hermes Agent" value="hermes-agent" /> <el-option label="Hermes Agent" value="hermes-agent" />
<el-option label="QwenPaw" value="copaw" /> <el-option label="QwenPaw" value="copaw" />
<el-option label="DeepSeek Harness" value="deepseek-harness" />
<el-option label="Octop" value="octop" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item :label="$t('commons.table.name')" prop="name"> <el-form-item :label="$t('commons.table.name')" prop="name">
@@ -19,10 +21,19 @@
<el-select v-model="form.appVersion" filterable> <el-select v-model="form.appVersion" filterable>
<el-option v-for="item in versions" :key="item" :label="item" :value="item" /> <el-option v-for="item in versions" :key="item" :label="item" :value="item" />
</el-select> </el-select>
<span v-if="versions.length === 0" class="input-help">{{ $t('commons.msg.noneData') }}</span>
</el-form-item> </el-form-item>
<el-form-item :label="$t('aiTools.agents.webuiPort')" prop="webUIPort"> <el-form-item :label="$t('aiTools.agents.webuiPort')" prop="webUIPort">
<el-input-number v-model="form.webUIPort" :min="1" :max="65535" /> <el-input-number v-model="form.webUIPort" :min="1" :max="65535" />
</el-form-item> </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)"> <template v-if="supportsAgentDashboardAuth(form.agentType)">
<el-form-item :label="$t('commons.login.username')" prop="dashboardUsername"> <el-form-item :label="$t('commons.login.username')" prop="dashboardUsername">
<el-input v-model="form.dashboardUsername" /> <el-input v-model="form.dashboardUsername" />
@@ -35,6 +46,12 @@
{{ $t('commons.button.random') }} {{ $t('commons.button.random') }}
</el-button> </el-button>
</div> </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> </el-form-item>
</template> </template>
<el-form-item <el-form-item
@@ -130,6 +147,9 @@ import { getRandomStr, newUUID } from '@/utils/id';
import { import {
buildDefaultAllowedOrigin, buildDefaultAllowedOrigin,
getAgentProviderDisplayName, getAgentProviderDisplayName,
isAgentType,
isDeepSeekAccessHost,
isOctopPassword,
parseAllowedOriginsInput, parseAllowedOriginsInput,
supportsAgentDashboardAuth, supportsAgentDashboardAuth,
validateAllowedOriginsInput, validateAllowedOriginsInput,
@@ -138,6 +158,7 @@ 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'; import { useGlobalStore } from '@/composables/useGlobalStore';
import i18n from '@/lang';
const emit = defineEmits(['search', 'task']); const emit = defineEmits(['search', 'task']);
@@ -161,6 +182,7 @@ const form = reactive({
agentType: 'openclaw' as AI.AgentType, agentType: 'openclaw' as AI.AgentType,
appVersion: '', appVersion: '',
webUIPort: 18789, webUIPort: 18789,
accessHost: '',
allowedOrigins: '', allowedOrigins: '',
provider: '', provider: '',
accountId: undefined as unknown as number, accountId: undefined as unknown as number,
@@ -185,7 +207,10 @@ const form = reactive({
const showModelConfig = computed(() => form.agentType === 'openclaw' || form.agentType === 'hermes-agent'); const showModelConfig = computed(() => form.agentType === 'openclaw' || form.agentType === 'hermes-agent');
const generateDashboardPassword = () => { 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 = () => { const ensureDashboardAuth = () => {
@@ -201,7 +226,7 @@ const ensureDashboardAuth = () => {
}; };
const setDefaultWebUIPort = () => { const setDefaultWebUIPort = () => {
if (form.agentType === 'copaw') { if (form.agentType === 'copaw' || form.agentType === 'octop') {
form.webUIPort = 8088; form.webUIPort = 8088;
return; return;
} }
@@ -209,6 +234,10 @@ const setDefaultWebUIPort = () => {
form.webUIPort = 9119; form.webUIPort = 9119;
return; return;
} }
if (form.agentType === 'deepseek-harness') {
form.webUIPort = 10443;
return;
}
form.webUIPort = 18789; form.webUIPort = 18789;
}; };
@@ -217,6 +246,19 @@ const rules = reactive({
agentType: [Rules.requiredSelect], agentType: [Rules.requiredSelect],
appVersion: [Rules.requiredSelect], appVersion: [Rules.requiredSelect],
webUIPort: [Rules.requiredInput], 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: [ allowedOrigins: [
Rules.requiredInput, Rules.requiredInput,
{ {
@@ -234,8 +276,35 @@ const rules = reactive({
provider: [Rules.requiredSelect], provider: [Rules.requiredSelect],
accountId: [Rules.requiredSelect], accountId: [Rules.requiredSelect],
model: [Rules.requiredInput], model: [Rules.requiredInput],
dashboardUsername: [Rules.requiredInput], dashboardUsername: [
dashboardPassword: [Rules.requiredInput], 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], containerName: [Rules.containerName],
restartPolicy: [Rules.requiredSelect], restartPolicy: [Rules.requiredSelect],
cpuQuota: [checkNumberRange(0, 99999)], cpuQuota: [checkNumberRange(0, 99999)],
@@ -273,7 +342,13 @@ const loadSystemIP = async () => {
}; };
const loadVersions = async (appKey: AI.AgentType) => { const loadVersions = async (appKey: AI.AgentType) => {
versions.value = [];
appInfo.value = undefined;
form.dockerCompose = '';
const res = await getAppByKey(appKey); const res = await getAppByKey(appKey);
if (form.agentType !== appKey) {
return;
}
appInfo.value = res.data; appInfo.value = res.data;
versions.value = res.data.versions || []; versions.value = res.data.versions || [];
if (!form.appVersion && versions.value.length > 0) { if (!form.appVersion && versions.value.length > 0) {
@@ -287,6 +362,10 @@ const getDefaultAgentName = (agentType: AI.AgentType) => {
return 'QwenPaw'; return 'QwenPaw';
case 'hermes-agent': case 'hermes-agent':
return 'Hermes-Agent'; return 'Hermes-Agent';
case 'deepseek-harness':
return 'DeepSeek-Harness';
case 'octop':
return 'Octop';
default: default:
return 'OpenClaw'; return 'OpenClaw';
} }
@@ -402,7 +481,9 @@ const handleAgentTypeChange = async () => {
form.name === 'OpenClaw' || form.name === 'OpenClaw' ||
form.name === 'CoPaw' || form.name === 'CoPaw' ||
form.name === 'QwenPaw' || form.name === 'QwenPaw' ||
form.name === 'Hermes-Agent' form.name === 'Hermes-Agent' ||
form.name === 'DeepSeek-Harness' ||
form.name === 'Octop'
) { ) {
form.name = getDefaultAgentName(form.agentType); form.name = getDefaultAgentName(form.agentType);
} }
@@ -413,6 +494,10 @@ const handleAgentTypeChange = async () => {
form.accountId = undefined as unknown as number; form.accountId = undefined as unknown as number;
form.baseURL = ''; form.baseURL = '';
ensureDashboardAuth(); ensureDashboardAuth();
if (form.agentType === 'deepseek-harness' && !form.accessHost) {
await loadSystemIP();
form.accessHost = isDeepSeekAccessHost(systemIP.value) ? systemIP.value : '';
}
if (form.agentType === 'openclaw') { if (form.agentType === 'openclaw') {
await loadSystemIP(); await loadSystemIP();
allowedOriginsAutoFilled.value = true; allowedOriginsAutoFilled.value = true;
@@ -485,6 +570,7 @@ const submit = async () => {
webUIPort: form.webUIPort, webUIPort: form.webUIPort,
allowedOrigins: form.agentType === 'openclaw' ? parseAllowedOriginsInput(form.allowedOrigins) : undefined, allowedOrigins: form.agentType === 'openclaw' ? parseAllowedOriginsInput(form.allowedOrigins) : undefined,
agentType: form.agentType, agentType: form.agentType,
accessHost: form.agentType === 'deepseek-harness' ? form.accessHost : undefined,
model: showModelConfig.value ? form.model : undefined, model: showModelConfig.value ? form.model : undefined,
accountId: showModelConfig.value ? form.accountId : undefined, accountId: showModelConfig.value ? form.accountId : undefined,
token: form.agentType === 'openclaw' ? form.token : undefined, token: form.agentType === 'openclaw' ? form.token : undefined,
@@ -524,45 +610,19 @@ const handleClose = () => {
form.dashboardPassword = ''; form.dashboardPassword = '';
form.remark = ''; form.remark = '';
form.allowedOrigins = ''; form.allowedOrigins = '';
form.accessHost = '';
form.dockerCompose = ''; form.dockerCompose = '';
lastAutoAllowedOrigins.value = ''; lastAutoAllowedOrigins.value = '';
allowedOriginsAutoFilled.value = true; allowedOriginsAutoFilled.value = true;
}; };
const openDrawer = async (agentType?: AI.AgentType) => { const openDrawer = async (agentType?: AI.AgentType) => {
const targetType = const targetType = isAgentType(agentType) ? agentType : 'openclaw';
agentType === 'copaw' || agentType === 'hermes-agent' || agentType === 'openclaw' ? agentType : 'openclaw';
form.name = getDefaultAgentName(targetType); form.name = getDefaultAgentName(targetType);
open.value = true; open.value = true;
form.agentType = targetType; form.agentType = targetType;
setDefaultWebUIPort();
form.token = getRandomStr(32).toLowerCase(); form.token = getRandomStr(32).toLowerCase();
ensureDashboardAuth(); await handleAgentTypeChange();
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();
}; };
const openAccountCreate = () => { const openAccountCreate = () => {
@@ -172,7 +172,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useSearchReset } from '@/composables/useSearchReset'; import { useSearchState } from '@/utils/search-reset';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { Refresh, Search } from '@element-plus/icons-vue'; import { Refresh, Search } from '@element-plus/icons-vue';
import { ElMessageBox } from 'element-plus'; import { ElMessageBox } from 'element-plus';
@@ -193,7 +193,8 @@ const searching = ref(false);
const operating = ref(''); const operating = ref('');
const keyword = ref(''); const keyword = ref('');
const marketKeyword = ref(''); const marketKeyword = ref('');
useSearchReset( useSearchState(
'ai/agents/agent/config/tabs/plugins',
{ keyword, marketKeyword }, { keyword, marketKeyword },
{ {
scope: () => agentId.value || undefined, scope: () => agentId.value || undefined,
@@ -138,7 +138,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useSearchReset } from '@/composables/useSearchReset'; import { useSearchState } from '@/utils/search-reset';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { Refresh, Search } from '@element-plus/icons-vue'; import { Refresh, Search } from '@element-plus/icons-vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -169,7 +169,8 @@ const getDefaultMarketSource = (): HermesSkillMarketSource => {
const marketSource = ref<HermesSkillMarketSource>(getDefaultMarketSource()); const marketSource = ref<HermesSkillMarketSource>(getDefaultMarketSource());
const marketSearched = ref(false); const marketSearched = ref(false);
const agentId = ref(0); const agentId = ref(0);
useSearchReset( useSearchState(
'ai/agents/agent/config/tabs/skills/hermes',
{ installedKeyword, marketKeyword }, { installedKeyword, marketKeyword },
{ {
scope: () => agentId.value || undefined, scope: () => agentId.value || undefined,
@@ -142,7 +142,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useSearchReset } from '@/composables/useSearchReset'; import { useSearchState } from '@/utils/search-reset';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { Refresh, Search } from '@element-plus/icons-vue'; import { Refresh, Search } from '@element-plus/icons-vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -181,7 +181,8 @@ const getDefaultMarketSource = (): SkillMarketSource => {
const marketSource = ref<SkillMarketSource>(getDefaultMarketSource()); const marketSource = ref<SkillMarketSource>(getDefaultMarketSource());
const marketSearched = ref(false); const marketSearched = ref(false);
const agentId = ref(0); const agentId = ref(0);
useSearchReset( useSearchState(
'ai/agents/agent/config/tabs/skills/openclaw',
{ installedKeyword, marketKeyword }, { installedKeyword, marketKeyword },
{ {
scope: () => agentId.value || undefined, scope: () => agentId.value || undefined,
+39 -14
View File
@@ -178,6 +178,9 @@
alignment="start" alignment="start"
:size="2" :size="2"
> >
<el-text v-if="row.agentType === 'octop'" type="info" size="small">
{{ $t('aiTools.agents.initialCredentials') }}
</el-text>
<span>{{ row.dashboardUsername || '-' }}</span> <span>{{ row.dashboardUsername || '-' }}</span>
<el-space alignment="center"> <el-space alignment="center">
<span>{{ row.dashboardPassword ? '********' : '-' }}</span> <span>{{ row.dashboardPassword ? '********' : '-' }}</span>
@@ -272,7 +275,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useSearchReset } from '@/composables/useSearchReset'; import { useSearchState } from '@/utils/search-reset';
import { toRef, defineAsyncComponent, onMounted, reactive, ref } from 'vue'; import { toRef, defineAsyncComponent, onMounted, reactive, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { deleteAgentCheck, pageAgents, resetAgentToken, unbindAgentWebsite, updateAgentRemark } from '@/api/modules/ai'; import { deleteAgentCheck, pageAgents, resetAgentToken, unbindAgentWebsite, updateAgentRemark } from '@/api/modules/ai';
@@ -283,7 +286,7 @@ import { Website } from '@/api/interface/website';
import { SearchWithPage } from '@/api/interface'; import { SearchWithPage } from '@/api/interface';
import { dateFormat } from '@/utils/date'; import { dateFormat } from '@/utils/date';
import { newUUID } from '@/utils/id'; 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 AddDialog from '@/views/ai/agents/agent/add/index.vue';
import DeleteDialog from '@/views/ai/agents/agent/delete/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 { import {
getAgentProviderDisplayName, getAgentProviderDisplayName,
getOpenclawAccessScheme, getOpenclawAccessScheme,
isAgentType,
isDeepSeekAccessHost,
supportsAgentModelConfig, supportsAgentModelConfig,
supportsAgentToken, supportsAgentToken,
supportsAgentDashboardAuth, 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 openclawIcon from '@/assets/images/ai-agent-openclaw.svg';
import copawIcon from '@/assets/images/ai-agent-copaw.svg'; import copawIcon from '@/assets/images/ai-agent-copaw.svg';
import hermesIcon from '@/assets/images/ai-agent-hermes-agent.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 { useGlobalStore } from '@/composables/useGlobalStore';
import { loadOptionalComponent } from '@/extensions/optional'; import { loadOptionalComponent } from '@/extensions/optional';
import { useOperateNodeContext } from '@/composables/useOperateNodeContext'; import { useOperateNodeContext } from '@/composables/useOperateNodeContext';
@@ -358,8 +365,7 @@ const buttons = [
{ {
label: i18n.global.t('menu.config'), label: i18n.global.t('menu.config'),
click: (row: AI.AgentItem) => openConfig(row), click: (row: AI.AgentItem) => openConfig(row),
show: (row: AI.AgentItem) => show: (row: AI.AgentItem) => supportsAgentModelConfig(row.agentType) || row.agentType === 'copaw',
supportsAgentModelConfig(row.agentType) || supportsAgentDashboardAuth(row.agentType),
disabled: (row: AI.AgentItem) => row.status !== 'Running', disabled: (row: AI.AgentItem) => row.status !== 'Running',
}, },
{ {
@@ -400,7 +406,11 @@ const paginationConfig = reactive({
pageSize: 10, pageSize: 10,
total: 0, total: 0,
}); });
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() }); useSearchState(
'ai/agents/agent/index',
{ search: searchName, page: toRef(paginationConfig, 'currentPage'), pageSize: toRef(paginationConfig, 'pageSize') },
{ onRevisit: () => search() },
);
const checkNoApp = async () => { const checkNoApp = async () => {
try { try {
@@ -441,6 +451,10 @@ const getAgentTypeLabel = (agentType: AI.AgentType) => {
return 'QwenPaw'; return 'QwenPaw';
case 'hermes-agent': case 'hermes-agent':
return 'Hermes Agent'; return 'Hermes Agent';
case 'deepseek-harness':
return 'DeepSeek Harness';
case 'octop':
return 'Octop';
default: default:
return 'OpenClaw'; return 'OpenClaw';
} }
@@ -452,6 +466,10 @@ const getAgentTypeIcon = (agentType: AI.AgentType) => {
return copawIcon; return copawIcon;
case 'hermes-agent': case 'hermes-agent':
return hermesIcon; return hermesIcon;
case 'deepseek-harness':
return deepSeekIcon;
case 'octop':
return octopIcon;
default: default:
return openclawIcon; return openclawIcon;
} }
@@ -461,8 +479,7 @@ const openCreate = (agentType?: AI.AgentType) => {
if (noApp.value) { if (noApp.value) {
return; return;
} }
const targetType = const targetType = isAgentType(agentType) ? agentType : 'openclaw';
agentType === 'copaw' || agentType === 'hermes-agent' || agentType === 'openclaw' ? agentType : 'openclaw';
if (addRef.value?.open) { if (addRef.value?.open) {
addRef.value.open(targetType); addRef.value.open(targetType);
} }
@@ -473,12 +490,7 @@ const openCreateFromQuery = async () => {
if (!shouldOpen) { if (!shouldOpen) {
return; return;
} }
const agentType = const agentType = isAgentType(route.query.agentType) ? route.query.agentType : 'openclaw';
route.query.agentType === 'copaw'
? 'copaw'
: route.query.agentType === 'hermes-agent'
? 'hermes-agent'
: 'openclaw';
openCreate(agentType); openCreate(agentType);
const nextQuery = { ...route.query }; const nextQuery = { ...route.query };
delete nextQuery.open; delete nextQuery.open;
@@ -547,7 +559,12 @@ const openTerminal = (row: AI.AgentItem) => {
openTerminalDialog({ openTerminalDialog({
containerID: row.containerName, containerID: row.containerName,
title, 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', shell: '/bin/bash',
initCmd: '', initCmd: '',
}); });
@@ -579,6 +596,14 @@ const openWorkDir = (row: AI.AgentItem) => {
}; };
const jumpWebUI = (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) { if (dialogPortJumpRef.value?.acceptParams) {
dialogPortJumpRef.value.acceptParams({ dialogPortJumpRef.value.acceptParams({
port: row.webUIPort, port: row.webUIPort,
+6 -2
View File
@@ -69,7 +69,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useSearchReset } from '@/composables/useSearchReset'; import { useSearchState } from '@/utils/search-reset';
import { toRef, onMounted, reactive, ref } from 'vue'; import { toRef, onMounted, reactive, ref } from 'vue';
import { deleteAgentAccount, getAgentProviders, pageAgentAccounts } from '@/api/modules/ai'; import { deleteAgentAccount, getAgentProviders, pageAgentAccounts } from '@/api/modules/ai';
import { AI } from '@/api/interface/ai'; import { AI } from '@/api/interface/ai';
@@ -111,7 +111,11 @@ const paginationConfig = reactive({
pageSize: 10, pageSize: 10,
total: 0, total: 0,
}); });
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() }); useSearchState(
'ai/agents/model/index',
{ search: searchName, page: toRef(paginationConfig, 'currentPage'), pageSize: toRef(paginationConfig, 'pageSize') },
{ onRevisit: () => search() },
);
const search = async () => { const search = async () => {
const req: AI.AgentAccountSearch = { const req: AI.AgentAccountSearch = {
+11 -2
View File
@@ -98,6 +98,8 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { toRef } from 'vue';
import { useSearchState } from '@/utils/search-reset';
import { AI } from '@/api/interface/ai'; import { AI } from '@/api/interface/ai';
import { import {
deleteMcpServer, deleteMcpServer,
@@ -129,11 +131,18 @@ const bindDomainRef = ref();
const configRef = ref(); const configRef = ref();
const items = ref<AI.McpServer[]>([]); const items = ref<AI.McpServer[]>([]);
const paginationConfig = reactive({ const paginationConfig = reactive({
cacheSizeKey: 'mcp-server-page-size',
currentPage: 1, currentPage: 1,
pageSize: Number(localStorage.getItem('mcp-server-page-size')) || 20, pageSize: 20,
total: 0, total: 0,
}); });
useSearchState(
'ai/mcp/server/index',
{
page: toRef(paginationConfig, 'currentPage'),
pageSize: toRef(paginationConfig, 'pageSize'),
},
{ onRevisit: () => search() },
);
const getUrl = (row: AI.McpServer) => { const getUrl = (row: AI.McpServer) => {
if (row.outputTransport == 'sse') { if (row.outputTransport == 'sse') {
+7 -4
View File
@@ -186,7 +186,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useSearchReset } from '@/composables/useSearchReset'; import { useSearchState } from '@/utils/search-reset';
import AppStatus from '@/components/app-status/index.vue'; import AppStatus from '@/components/app-status/index.vue';
import AddDialog from '@/views/ai/model/ollama/add/index.vue'; import AddDialog from '@/views/ai/model/ollama/add/index.vue';
import Conn from '@/views/ai/model/ollama/conn/index.vue'; import Conn from '@/views/ai/model/ollama/conn/index.vue';
@@ -231,13 +231,16 @@ const bindDomainRef = ref();
const taskLogRef = ref(); const taskLogRef = ref();
const data = ref(); const data = ref();
const paginationConfig = reactive({ const paginationConfig = reactive({
cacheSizeKey: 'model-page-size',
currentPage: 1, currentPage: 1,
pageSize: Number(localStorage.getItem('page-size')) || 20, pageSize: 20,
total: 0, total: 0,
}); });
const searchName = ref(); const searchName = ref();
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() }); useSearchState(
'ai/model/ollama/index',
{ search: searchName, page: toRef(paginationConfig, 'currentPage'), pageSize: toRef(paginationConfig, 'pageSize') },
{ onRevisit: () => search() },
);
const appInstallID = ref(0); const appInstallID = ref(0);
const opRef = ref(); const opRef = ref();
@@ -77,6 +77,8 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { toRef } from 'vue';
import { useSearchState } from '@/utils/search-reset';
import OperateDialog from './operate/index.vue'; import OperateDialog from './operate/index.vue';
import ComposeLogs from '@/components/log/compose/index.vue'; import ComposeLogs from '@/components/log/compose/index.vue';
import PortJumpDialog from '@/components/port-jump/index.vue'; import PortJumpDialog from '@/components/port-jump/index.vue';
@@ -101,6 +103,14 @@ const paginationConfig = reactive({
pageSize: 10, pageSize: 10,
total: 0, total: 0,
}); });
useSearchState(
'ai/model/tensorrt/index',
{
page: toRef(paginationConfig, 'currentPage'),
pageSize: toRef(paginationConfig, 'pageSize'),
},
{ onRevisit: () => search() },
);
const searchName = ref(); const searchName = ref();
const opRef = ref(); const opRef = ref();
const dialogRef = ref(); const dialogRef = ref();
+34 -17
View File
@@ -70,10 +70,18 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useSearchReset } from '@/composables/useSearchReset'; import { useSearchState } from '@/utils/search-reset';
import { App } from '@/api/interface/app'; import { App } from '@/api/interface/app';
import { toRef, onMounted, reactive, ref } from 'vue'; 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 Install from '../detail/install/index.vue';
import router from '@/routers'; import router from '@/routers';
import { MsgSuccess } from '@/utils/message'; import { MsgSuccess } from '@/utils/message';
@@ -88,12 +96,11 @@ import AppCard from '@/views/app-store/apps/app/index.vue';
import MainDiv from '@/components/main-div/index.vue'; import MainDiv from '@/components/main-div/index.vue';
import { jumpToInstall } from '@/utils/app'; import { jumpToInstall } from '@/utils/app';
import { useGlobalStore } from '@/composables/useGlobalStore'; import { useGlobalStore } from '@/composables/useGlobalStore';
const { isProductPro, isOffline, isMobile, isEnterprise } = useGlobalStore(); const { isProductPro, isOffline, isMobile, isEnterprise, isMaster, isXpackOrEE, isAdmin } = useGlobalStore();
const paginationConfig = reactive({ const paginationConfig = reactive({
cacheSizeKey: 'app-page-size',
currentPage: 1, currentPage: 1,
pageSize: Number(localStorage.getItem('app-page-size')) || 60, pageSize: 60,
total: 0, total: 0,
}); });
@@ -105,8 +112,13 @@ const req = reactive({
resource: 'all', resource: 'all',
showCurrentArch: false, showCurrentArch: false,
}); });
useSearchReset( useSearchState(
{ search: toRef(req, 'name'), page: toRef(paginationConfig, 'currentPage') }, 'app-store/apps/index',
{
search: toRef(req, 'name'),
page: toRef(paginationConfig, 'currentPage'),
pageSize: toRef(paginationConfig, 'pageSize'),
},
{ onRevisit: () => search(req) }, { onRevisit: () => search(req) },
); );
@@ -133,7 +145,6 @@ const search = async (req: App.AppReq) => {
loading.value = true; loading.value = true;
req.pageSize = paginationConfig.pageSize; req.pageSize = paginationConfig.pageSize;
req.page = paginationConfig.currentPage; req.page = paginationConfig.currentPage;
localStorage.setItem('app-page-size', req.pageSize + '');
const customReq = { const customReq = {
page: req.page, page: req.page,
@@ -201,21 +212,27 @@ const sync = async () => {
} }
}; };
const syncLocal = () => { const syncLocal = async () => {
const taskID = newUUID(); const taskID = newUUID();
const syncReq = { const syncReq = {
taskID: taskID, taskID: taskID,
}; };
syncing.value = true; syncing.value = true;
syncLocalApp(syncReq) try {
.then(() => { 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); openTaskLog(taskID);
canUpdate.value = false; }
search(req); canUpdate.value = false;
}) search(req);
.finally(() => { } finally {
syncing.value = false; syncing.value = false;
}); }
}; };
const changeTag = (key: string) => { const changeTag = (key: string) => {

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