mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-29 16:05:34 +00:00
Compare commits
13
Commits
release-2.4.1
...
v2.4.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9f9169a4d | ||
|
|
1e01761ba3 | ||
|
|
15283fde34 | ||
|
|
49f8e7f040 | ||
|
|
98b27af6a0 | ||
|
|
a393224729 | ||
|
|
b851c84290 | ||
|
|
372855157d | ||
|
|
577d771495 | ||
|
|
ca3e6511e5 | ||
|
|
8af14eae25 | ||
|
|
175fbaf3f2 | ||
|
|
13199efd62 |
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -9,7 +9,8 @@ type AgentCreateReq struct {
|
||||
WebUIPort int `json:"webUIPort" validate:"required,min=1,max=65535"`
|
||||
BridgePort int `json:"bridgePort"`
|
||||
AllowedOrigins []string `json:"allowedOrigins"`
|
||||
AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent"`
|
||||
AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent deepseek-harness octop"`
|
||||
AccessHost string `json:"accessHost"`
|
||||
Model string `json:"model"`
|
||||
AccountID uint `json:"accountId"`
|
||||
Token string `json:"token"`
|
||||
@@ -110,6 +111,7 @@ type AgentBatchOperateResult struct {
|
||||
}
|
||||
|
||||
type AgentItem struct {
|
||||
AccessHost string `json:"accessHost"`
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Remark string `json:"remark"`
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -31,7 +31,27 @@ type DashboardBase 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 {
|
||||
|
||||
@@ -5,6 +5,13 @@ type AppLauncher struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
type QuickDirectory struct {
|
||||
BaseModel
|
||||
Path string `json:"path"`
|
||||
Alias string `json:"alias"`
|
||||
Legacy bool `json:"legacy" gorm:"-"`
|
||||
}
|
||||
|
||||
type QuickJump struct {
|
||||
BaseModel
|
||||
Name string `json:"name"`
|
||||
|
||||
@@ -66,7 +66,7 @@ var catalog = map[string]Meta{
|
||||
{APIType: "openai-responses", BaseURL: "https://api.deepseek.com"},
|
||||
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": {
|
||||
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: "qwen3.5-plus", Name: "Qwen3.5-Plus"},
|
||||
{ID: "glm-5", Name: "GLM-5"},
|
||||
{ID: "MiniMax-M2.5", Name: "MiniMax M2.5"},
|
||||
{ID: "qwen3.6-plus", Name: "Qwen3.6-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: "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": {
|
||||
Key: "xiaomi", DisplayName: "Xiaomi", Sort: 46, DefaultAPIType: "openai-completions", EnvKey: "XIAOMI_API_KEY",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,10 +118,15 @@ func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model stri
|
||||
headers["x-api-key"] = apiKey
|
||||
}
|
||||
headers["anthropic-version"] = "2023-06-01"
|
||||
request.Body = mustJSON(map[string]interface{}{
|
||||
body := map[string]interface{}{
|
||||
"model": model, "max_tokens": defaultVerifyMaxTokens, "stream": false,
|
||||
"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":
|
||||
request.URL = baseURL + "/responses"
|
||||
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) != "" {
|
||||
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,
|
||||
})
|
||||
}
|
||||
if provider == "minimax" && model == "MiniMax-M3.1-Flash" {
|
||||
body["reasoning_effort"] = "max"
|
||||
}
|
||||
request.Body = mustJSON(body)
|
||||
}
|
||||
return request
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package repo
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type LauncherRepo struct{}
|
||||
@@ -17,6 +18,11 @@ type ILauncherRepo interface {
|
||||
GetQuickJump(opts ...DBOption) (model.QuickJump, error)
|
||||
ListQuickJump(withAll bool) []model.QuickJump
|
||||
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 {
|
||||
@@ -95,13 +101,73 @@ func (u *LauncherRepo) UpdateQuicks(quicks []model.QuickJump) error {
|
||||
for _, item := range quicks {
|
||||
if err := tx.Model(&model.QuickJump{}).Where("id = ?", item.ID).Updates(map[string]interface{}{
|
||||
"is_show": item.IsShow,
|
||||
"detail": item.Detail,
|
||||
"alias": item.Alias,
|
||||
}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
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
|
||||
}
|
||||
|
||||
@@ -142,6 +142,9 @@ const (
|
||||
|
||||
func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
agentType := req.AgentType
|
||||
if err := validateNativeAgentCreate(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := checkPortExist(req.WebUIPort); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -237,6 +240,8 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
}
|
||||
} else if agentType == constant.AppCopaw {
|
||||
dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
||||
} else if agentType == constant.AppDeepSeekHarness || agentType == constant.AppOctop {
|
||||
dashboardAuth = agentDashboardAuth{Username: req.DashboardUsername, Password: req.DashboardPassword}
|
||||
}
|
||||
|
||||
params := map[string]interface{}{
|
||||
@@ -245,6 +250,12 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
constant.HostIP: "",
|
||||
}
|
||||
setAgentWebUIParams(params, agentType, detail.Version, req.WebUIPort)
|
||||
if agentType == constant.AppDeepSeekHarness {
|
||||
params["HTTPS_ACCESS_HOST"] = req.AccessHost
|
||||
}
|
||||
if agentType == constant.AppOctop {
|
||||
params["OCTOP_LOG_LEVEL"] = "info"
|
||||
}
|
||||
if agentType == constant.AppOpenclaw {
|
||||
if allowedOrigin := firstAllowedOrigin(allowedOrigins); allowedOrigin != "" {
|
||||
params["ALLOWED_ORIGIN"] = allowedOrigin
|
||||
@@ -738,7 +749,7 @@ func requiresAgentAccount(agentType string) bool {
|
||||
}
|
||||
|
||||
func setAgentWebUIParams(params map[string]interface{}, agentType, appVersion string, webUIPort int) {
|
||||
if agentType == constant.AppOpenclaw && isOpenclawHTTPSWindowVersion(appVersion) {
|
||||
if agentType == constant.AppDeepSeekHarness || agentType == constant.AppOpenclaw && isOpenclawHTTPSWindowVersion(appVersion) {
|
||||
params["PANEL_APP_PORT_HTTPS"] = webUIPort
|
||||
return
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -415,6 +415,15 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
|
||||
} else {
|
||||
item.WebUIPort = appInstall.HttpPort
|
||||
}
|
||||
if agentType == constant.AppDeepSeekHarness {
|
||||
item.WebUIPort = appInstall.HttpsPort
|
||||
if values, err := readAgentEnvMap(appInstall.GetEnvPath()); err == nil {
|
||||
accessHost := values["HTTPS_ACCESS_HOST"]
|
||||
if isDeepSeekAccessHost(accessHost) {
|
||||
item.AccessHost = accessHost
|
||||
}
|
||||
}
|
||||
}
|
||||
item.Path = appInstall.GetPath()
|
||||
item.Status = appInstall.Status
|
||||
item.Message = appInstall.Message
|
||||
@@ -442,7 +451,8 @@ func localizedAgentProviderName(provider string) string {
|
||||
}
|
||||
|
||||
func isAgentAppKey(appKey string) bool {
|
||||
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent
|
||||
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent ||
|
||||
appKey == constant.AppDeepSeekHarness || appKey == constant.AppOctop
|
||||
}
|
||||
|
||||
func isOpenclawLegacyHTTPVersion(version string) bool {
|
||||
@@ -1253,7 +1263,12 @@ func buildOpenclawAccountModelConfig(account *model.AgentAccount, model dto.Agen
|
||||
if err != nil {
|
||||
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,
|
||||
BaseUrl: providerPatch.BaseURL,
|
||||
Api: providerPatch.APIType,
|
||||
@@ -1641,6 +1656,10 @@ func agentDashboardAuthEnvKeys(agentType string) (string, string, bool) {
|
||||
return hermesDashboardUsernameEnvKey, hermesDashboardPasswordEnvKey, true
|
||||
case constant.AppCopaw:
|
||||
return qwenPawAuthUsernameEnvKey, qwenPawAuthPasswordEnvKey, true
|
||||
case constant.AppDeepSeekHarness:
|
||||
return "DSH_AUTH_USERNAME", "DSH_AUTH_PASSWORD", true
|
||||
case constant.AppOctop:
|
||||
return "OCTOP_ADMIN_USERNAME", "OCTOP_DEFAULT_PASSWORD", true
|
||||
default:
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
@@ -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
@@ -258,26 +258,35 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
|
||||
fileOp := files.NewFileOp()
|
||||
|
||||
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) {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
if err = downloadApp(app, detail, nil, nil); err != nil && !fileOp.Stat(versionPath) {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
if err == nil && app.Resource == constant.AppResourceRemote {
|
||||
detail.Update = false
|
||||
appDetailDTO.Update = false
|
||||
}
|
||||
switch app.Type {
|
||||
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{})
|
||||
if err = yaml.Unmarshal(param, ¶mMap); err != nil {
|
||||
return appDetailDTO, err
|
||||
if app.Resource == constant.AppResourceRemote {
|
||||
if err = json.Unmarshal([]byte(detail.Params), ¶mMap); 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, ¶mMap); err != nil {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
appDetailDTO.Params = paramMap["additionalProperties"]
|
||||
}
|
||||
appDetailDTO.Params = paramMap["additionalProperties"]
|
||||
composePath := filepath.Join(versionPath, "docker-compose.yml")
|
||||
if !fileOp.Stat(composePath) {
|
||||
return appDetailDTO, buserr.WithDetail("ErrFileNotExist", composePath, nil)
|
||||
@@ -308,6 +317,12 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
|
||||
if appDetailDTO.DockerCompose == "" {
|
||||
filename := filepath.Base(appDetailDTO.DownloadUrl)
|
||||
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)
|
||||
if statusCode == http.StatusNotFound {
|
||||
return appDetailDTO, buserr.New("ErrAppVersionUnavailable")
|
||||
@@ -434,6 +449,12 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
||||
} else {
|
||||
if appDetail.DockerCompose == "" {
|
||||
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
|
||||
statusCode, composeRes, err = req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
||||
if statusCode == http.StatusNotFound {
|
||||
|
||||
@@ -707,6 +707,12 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
|
||||
if req.UpdateVersion != "" && req.UpdateVersion == detail.Version && detail.DockerCompose == "" && !app.IsLocalApp() {
|
||||
filename := filepath.Base(detail.DownloadUrl)
|
||||
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)
|
||||
if statusCode == http.StatusNotFound {
|
||||
return versions, buserr.New("ErrAppVersionUnavailable")
|
||||
|
||||
@@ -191,7 +191,11 @@ func (c *appSyncContext) processOneApp(item appWorkItem) appWorkResult {
|
||||
|
||||
if _, ok := InitTypes[app.Type]; ok {
|
||||
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 {
|
||||
detail.DockerCompose = string(composeRes)
|
||||
} else {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"maps"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"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) {
|
||||
if app.IsLocalApp() || app.IsCustomApp() {
|
||||
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)
|
||||
appDownloadDir := app.GetAppResourcePath()
|
||||
appVersionDir := path.Join(appDownloadDir, appDetail.Version)
|
||||
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) {
|
||||
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) {
|
||||
_ = 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)
|
||||
}
|
||||
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 {
|
||||
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")
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
if appInstall != nil {
|
||||
appInstall.Status = constant.StatusDownloadErr
|
||||
appInstall.Message = err.Error()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err = files.DownloadFileWithProxy(appDetail.DownloadUrl, filePath); err != nil {
|
||||
if err = files.DownloadFileWithProxy(downloadURL, filePath); err != nil {
|
||||
if logger == nil {
|
||||
global.LOG.Errorf("download app [%s] error %v", app.Name, err)
|
||||
} else {
|
||||
@@ -942,6 +970,9 @@ func downloadApp(app model.App, appDetail model.AppDetail, appInstall *model.App
|
||||
return
|
||||
}
|
||||
_ = fileOp.DeleteFile(filePath)
|
||||
if err = os.WriteFile(cacheVersionPath, []byte(storeVersion), constant.FilePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
appDetail.Update = false
|
||||
_ = appDetailRepo.Update(context.Background(), appDetail)
|
||||
return
|
||||
|
||||
@@ -43,6 +43,10 @@ type IDashboardService interface {
|
||||
|
||||
LoadQuickOptions() []dto.QuickJump
|
||||
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)
|
||||
ChangeShow(req dto.SettingUpdate) error
|
||||
@@ -397,16 +401,15 @@ func (u *DashboardService) LoadQuickOptions() []dto.QuickJump {
|
||||
}
|
||||
func (u *DashboardService) ChangeQuick(req dto.ChangeQuicks) error {
|
||||
showCount := 0
|
||||
var quicks []model.QuickJump
|
||||
quicks := make([]model.QuickJump, 0, len(req.Quicks))
|
||||
for _, item := range req.Quicks {
|
||||
var quick model.QuickJump
|
||||
if item.IsShow {
|
||||
showCount++
|
||||
}
|
||||
if err := copier.Copy(&quick, &item); err != nil {
|
||||
return err
|
||||
}
|
||||
quicks = append(quicks, quick)
|
||||
quicks = append(quicks, model.QuickJump{
|
||||
BaseModel: model.BaseModel{ID: item.ID},
|
||||
IsShow: item.IsShow,
|
||||
})
|
||||
}
|
||||
if showCount == 0 {
|
||||
return buserr.New("ErrMinQuickJump")
|
||||
@@ -706,6 +709,13 @@ func loadQuickJump(base *dto.DashboardBase) {
|
||||
quicks := launcherRepo.ListQuickJump(false)
|
||||
for i := 0; i < len(quicks); i++ {
|
||||
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":
|
||||
quicks[i].Detail = fmt.Sprintf("%d", base.AgentNumber)
|
||||
case "Website":
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -212,10 +212,17 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
|
||||
websiteSSL.Shell = req.Shell
|
||||
}
|
||||
if req.PushDir {
|
||||
if !files.NewFileOp().Stat(req.Dir) {
|
||||
dirs := certificateDirectories(req.Dir)
|
||||
if len(dirs) == 0 {
|
||||
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 != "" {
|
||||
domainArray := strings.Split(req.Domains, "\n")
|
||||
@@ -348,7 +355,9 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
|
||||
if websiteSSL.ExecShell {
|
||||
workDir := global.Dir.DataDir
|
||||
if websiteSSL.PushDir {
|
||||
workDir = websiteSSL.Dir
|
||||
if dirs := certificateDirectories(websiteSSL.Dir); len(dirs) > 0 {
|
||||
workDir = dirs[0]
|
||||
}
|
||||
}
|
||||
logger.Println(i18n.GetMsgByKey("ExecShellStart"))
|
||||
if err = runShellScriptFile(workDir, websiteSSL.Shell, logger); err != nil {
|
||||
|
||||
@@ -209,11 +209,17 @@ func (w WebsiteSSLService) Create(create request.WebsiteSSLCreate) (request.Webs
|
||||
websiteSSL.Shell = create.Shell
|
||||
}
|
||||
if create.PushDir {
|
||||
fileOP := files.NewFileOp()
|
||||
if !fileOP.Stat(create.Dir) {
|
||||
dirs := certificateDirectories(create.Dir)
|
||||
if len(dirs) == 0 {
|
||||
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)
|
||||
|
||||
@@ -573,7 +579,9 @@ func (w WebsiteSSLService) obtainSSL(id uint, autoRenew bool) error {
|
||||
if websiteSSL.ExecShell {
|
||||
workDir := global.Dir.DataDir
|
||||
if websiteSSL.PushDir {
|
||||
workDir = websiteSSL.Dir
|
||||
if dirs := certificateDirectories(websiteSSL.Dir); len(dirs) > 0 {
|
||||
workDir = dirs[0]
|
||||
}
|
||||
}
|
||||
printSSLLog(logger, "ExecShellStart", 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 {
|
||||
fileOP := files.NewFileOp()
|
||||
if !fileOP.Stat(update.Dir) {
|
||||
_ = fileOP.CreateDir(update.Dir, constant.DirPerm)
|
||||
dirs := certificateDirectories(update.Dir)
|
||||
if len(dirs) == 0 {
|
||||
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
|
||||
if update.OtherDomains != "" {
|
||||
|
||||
@@ -1529,26 +1529,41 @@ func checkWebsitePort(defaultHTTPsPort, port int, websiteType string) error {
|
||||
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) {
|
||||
if websiteSSL.PushDir {
|
||||
fileOp := files.NewFileOp()
|
||||
var (
|
||||
pushErr error
|
||||
MsgMap = map[string]interface{}{"path": websiteSSL.Dir, "status": i18n.GetMsgByKey("Success")}
|
||||
)
|
||||
if pushErr = fileOp.SaveFile(path.Join(websiteSSL.Dir, "privkey.pem"), websiteSSL.PrivateKey, constant.FilePerm); pushErr != nil {
|
||||
MsgMap["status"] = i18n.GetMsgByKey("Failed")
|
||||
logger.Println(i18n.GetMsgWithMap("PushDirLog", MsgMap))
|
||||
logger.Println("Push dir failed:" + pushErr.Error())
|
||||
}
|
||||
if pushErr = fileOp.SaveFile(path.Join(websiteSSL.Dir, "fullchain.pem"), websiteSSL.Pem, constant.FilePerm); pushErr != nil {
|
||||
MsgMap["status"] = i18n.GetMsgByKey("Failed")
|
||||
logger.Println(i18n.GetMsgWithMap("PushDirLog", MsgMap))
|
||||
logger.Println("Push dir failed:" + pushErr.Error())
|
||||
}
|
||||
if pushErr == nil {
|
||||
logger.Println(i18n.GetMsgWithMap("PushDirLog", MsgMap))
|
||||
if !websiteSSL.PushDir {
|
||||
return
|
||||
}
|
||||
fileOp := files.NewFileOp()
|
||||
for _, dir := range certificateDirectories(websiteSSL.Dir) {
|
||||
status := i18n.GetMsgByKey("Success")
|
||||
for _, file := range []struct{ name, content string }{
|
||||
{"privkey.pem", websiteSSL.PrivateKey},
|
||||
{"fullchain.pem", websiteSSL.Pem},
|
||||
} {
|
||||
if err := fileOp.SaveFile(path.Join(dir, file.name), file.content, constant.FilePerm); err != nil {
|
||||
status = i18n.GetMsgByKey("Failed")
|
||||
logger.Printf("Push dir %s failed: %s", dir, err)
|
||||
}
|
||||
}
|
||||
logger.Println(i18n.GetMsgWithMap("PushDirLog", map[string]interface{}{"path": dir, "status": status}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ const (
|
||||
AppOpenclaw = "openclaw"
|
||||
AppCopaw = "copaw"
|
||||
AppHermesAgent = "hermes-agent"
|
||||
AppDeepSeekHarness = "deepseek-harness"
|
||||
AppOctop = "octop"
|
||||
AppMysql = "mysql"
|
||||
AppMariaDB = "mariadb"
|
||||
AppPostgresql = "postgresql"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
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
|
||||
|
||||
|
||||
@@ -115,6 +115,8 @@ func agentDBMigrations() []*gormigrate.Migration {
|
||||
migrations.AddMonitorSettings,
|
||||
migrations.AddVLLMRequestMetrics,
|
||||
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
|
||||
},
|
||||
}
|
||||
@@ -1888,3 +1888,10 @@ var AddVLLMSampleInterval = &gormigrate.Migration{
|
||||
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{})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
|
||||
baseApi := v1.ApiGroupApp.BaseApi
|
||||
{
|
||||
aiToolsRouter.GET("/benchmark/sources", baseApi.AIBenchmarkSources)
|
||||
aiToolsRouter.POST("/ollama/close", baseApi.CloseOllamaModel)
|
||||
aiToolsRouter.POST("/ollama/model", baseApi.CreateOllamaModel)
|
||||
aiToolsRouter.POST("/ollama/model/recreate", baseApi.RecreateOllamaModel)
|
||||
|
||||
@@ -14,6 +14,10 @@ func (s *DashboardRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
cmdRouter.GET("/base/os", baseApi.LoadDashboardOsInfo)
|
||||
cmdRouter.GET("/quick/option", baseApi.LoadQuickOption)
|
||||
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.POST("/app/launcher/show", baseApi.UpdateAppLauncher)
|
||||
cmdRouter.POST("/app/launcher/option", baseApi.LoadAppLauncherOption)
|
||||
|
||||
@@ -266,6 +266,7 @@ func (b *BaseApi) GetLoginSetting(c *gin.Context) {
|
||||
Language: settingInfo.Language,
|
||||
MenuTabs: settingInfo.MenuTabs,
|
||||
MenuAccordion: settingInfo.MenuAccordion,
|
||||
PanelCache: settingInfo.PanelCache,
|
||||
PanelName: settingInfo.PanelName,
|
||||
Theme: settingInfo.Theme,
|
||||
NeedCaptcha: needCaptcha,
|
||||
|
||||
@@ -4,6 +4,16 @@ import (
|
||||
"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 {
|
||||
SystemVersion string `json:"systemVersion"`
|
||||
DeveloperMode string `json:"developerMode"`
|
||||
@@ -18,6 +28,7 @@ type SettingInfo struct {
|
||||
Theme string `json:"theme"`
|
||||
MenuTabs string `json:"menuTabs"`
|
||||
MenuAccordion string `json:"menuAccordion"`
|
||||
PanelCache string `json:"panelCache"`
|
||||
Language string `json:"language"`
|
||||
DocSource string `json:"docSource"`
|
||||
|
||||
@@ -62,6 +73,7 @@ type SettingBaseInfo struct {
|
||||
Theme string `json:"theme"`
|
||||
MenuTabs string `json:"menuTabs"`
|
||||
MenuAccordion string `json:"menuAccordion"`
|
||||
PanelCache string `json:"panelCache"`
|
||||
Language string `json:"language"`
|
||||
HideMenu string `json:"hideMenu"`
|
||||
DocSource string `json:"docSource"`
|
||||
@@ -288,6 +300,7 @@ type LoginSetting struct {
|
||||
Language string `json:"language"`
|
||||
MenuTabs string `json:"menuTabs"`
|
||||
MenuAccordion string `json:"menuAccordion"`
|
||||
PanelCache string `json:"panelCache"`
|
||||
PanelName string `json:"panelName"`
|
||||
Theme string `json:"theme"`
|
||||
NeedCaptcha bool `json:"needCaptcha"`
|
||||
|
||||
@@ -120,6 +120,9 @@ func (u *SettingService) GetSettingInfo() (*dto.SettingInfo, error) {
|
||||
for _, set := range setting {
|
||||
settingMap[set.Key] = set.Value
|
||||
}
|
||||
if settingMap["PanelCache"] == "" {
|
||||
settingMap["PanelCache"] = dto.DefaultPanelCache
|
||||
}
|
||||
repairAndSortHideMenu(settingMap)
|
||||
var info dto.SettingInfo
|
||||
arr, err := json.Marshal(settingMap)
|
||||
@@ -155,6 +158,9 @@ func (u *SettingService) GetSettingBaseInfo() (*dto.SettingBaseInfo, error) {
|
||||
for _, set := range setting {
|
||||
settingMap[set.Key] = set.Value
|
||||
}
|
||||
if settingMap["PanelCache"] == "" {
|
||||
settingMap["PanelCache"] = dto.DefaultPanelCache
|
||||
}
|
||||
repairAndSortHideMenu(settingMap)
|
||||
var info dto.SettingBaseInfo
|
||||
arr, err := json.Marshal(settingMap)
|
||||
@@ -224,6 +230,27 @@ func (u *SettingService) Update(c *gin.Context, key, value string) error {
|
||||
if auth.IsAPICredentialSetting(key) {
|
||||
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))
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
module github.com/1Panel-dev/1Panel/core
|
||||
|
||||
go 1.26.6
|
||||
go 1.27.3
|
||||
|
||||
require (
|
||||
github.com/beevik/etree v1.7.0
|
||||
|
||||
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "An unrevoked API key with this name already exists"
|
||||
ErrAPIKeyNotFound: "The API key does not exist or does not belong to the current user"
|
||||
ErrAPIKeyAppBindingDisabled: "APP QR binding is not allowed for this API key"
|
||||
ErrAPIKeyTerminalUpgradeRequired: "Unable to verify API key terminal authorization support. Check the node connection and upgrade its Agent before retrying."
|
||||
LocalAppSyncDispatch: "Dispatch local app sync"
|
||||
LocalAppSyncDispatchNode: "Dispatch local app sync to {{.node}} (node task: {{.taskID}})"
|
||||
ErrLocalAppSyncDisabled: "Local app sync across nodes is disabled"
|
||||
|
||||
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "Ya existe una clave API no revocada con este nombre"
|
||||
ErrAPIKeyNotFound: "La clave API no existe o no pertenece al usuario actual"
|
||||
ErrAPIKeyAppBindingDisabled: "Esta clave API no permite vincular la APP mediante código QR"
|
||||
ErrAPIKeyTerminalUpgradeRequired: "No se pudo verificar la autorización de terminal con clave API. Compruebe la conexión del nodo y actualice su Agent antes de reintentar."
|
||||
LocalAppSyncDispatch: "Enviar sincronización de apps locales"
|
||||
LocalAppSyncDispatchNode: "Enviar sincronización de apps locales a {{.node}} (tarea del nodo: {{.taskID}})"
|
||||
ErrLocalAppSyncDisabled: "La sincronización de apps locales entre nodos está desactivada"
|
||||
|
||||
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "یک کلید API لغونشده با این نام از
|
||||
ErrAPIKeyNotFound: "کلید API وجود ندارد یا متعلق به کاربر فعلی نیست"
|
||||
ErrAPIKeyAppBindingDisabled: "اتصال برنامه با کد QR برای این کلید API مجاز نیست"
|
||||
ErrAPIKeyTerminalUpgradeRequired: "امکان بررسی پشتیبانی از مجوزدهی ترمینال با کلید API وجود ندارد. اتصال گره را بررسی و Agent آن را ارتقا دهید، سپس دوباره تلاش کنید."
|
||||
LocalAppSyncDispatch: "ارسال درخواست همگامسازی برنامههای محلی"
|
||||
LocalAppSyncDispatchNode: "ارسال درخواست همگامسازی به {{.node}} (وظیفه گره: {{.taskID}})"
|
||||
ErrLocalAppSyncDisabled: "همگامسازی برنامههای محلی بین گرهها غیرفعال است"
|
||||
|
||||
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "同じ名前の未失効 API キーが既に存在しま
|
||||
ErrAPIKeyNotFound: "API キーが存在しないか、現在のユーザーに属していません"
|
||||
ErrAPIKeyAppBindingDisabled: "この API キーでは APP の QR コード連携が許可されていません"
|
||||
ErrAPIKeyTerminalUpgradeRequired: "API キーによるターミナル認証への対応を確認できません。ノードの接続を確認し、Agent を更新して再試行してください。"
|
||||
LocalAppSyncDispatch: "ローカルアプリ同期要求の送信"
|
||||
LocalAppSyncDispatchNode: "{{.node}} にローカルアプリ同期要求を送信(ノードタスク: {{.taskID}})"
|
||||
ErrLocalAppSyncDisabled: "ノード間のローカルアプリ同期は無効です"
|
||||
|
||||
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "같은 이름의 폐기되지 않은 API Key가 이미 있
|
||||
ErrAPIKeyNotFound: "API Key가 없거나 현재 사용자 소유가 아닙니다"
|
||||
ErrAPIKeyAppBindingDisabled: "이 API Key는 APP QR 코드 연결을 허용하지 않습니다"
|
||||
ErrAPIKeyTerminalUpgradeRequired: "API Key 터미널 인증 지원 여부를 확인할 수 없습니다. 노드 연결을 확인하고 Agent를 업그레이드한 후 다시 시도하세요."
|
||||
LocalAppSyncDispatch: "로컬 앱 동기화 요청 전송"
|
||||
LocalAppSyncDispatchNode: "{{.node}}에 로컬 앱 동기화 요청 전송 (노드 작업: {{.taskID}})"
|
||||
ErrLocalAppSyncDisabled: "노드 간 로컬 앱 동기화가 비활성화되어 있습니다"
|
||||
|
||||
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "ມີ API Key ທີ່ຍັງບໍ່ຖືກຖອ
|
||||
ErrAPIKeyNotFound: "ບໍ່ພົບ API Key ຫຼືບໍ່ແມ່ນຂອງຜູ້ໃຊ້ປັດຈຸບັນ"
|
||||
ErrAPIKeyAppBindingDisabled: "API Key ນີ້ບໍ່ອະນຸຍາດໃຫ້ຜູກ APP ຜ່ານລະຫັດ QR"
|
||||
ErrAPIKeyTerminalUpgradeRequired: "ບໍ່ສາມາດຢືນຢັນການຮອງຮັບການອະນຸຍາດເທີມິນອນດ້ວຍ API Key. ກວດສອບການເຊື່ອມຕໍ່ໂນດ ແລະອັບເກຣດ Agent ກ່ອນລອງອີກ."
|
||||
LocalAppSyncDispatch: "ສົ່ງຄຳຂໍຊິງຄ໌ແອັບໃນເຄື່ອງ"
|
||||
LocalAppSyncDispatchNode: "ສົ່ງຄຳຂໍຊິງຄ໌ໄປຫາ {{.node}} (ວຽກຂອງໂນດ: {{.taskID}})"
|
||||
ErrLocalAppSyncDisabled: "ການຊິງຄ໌ແອັບລະຫວ່າງໂນດຍັງບໍ່ໄດ້ເປີດ"
|
||||
|
||||
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "Kunci API yang belum dibatalkan dengan nama ini sudah wuju
|
||||
ErrAPIKeyNotFound: "Kunci API tidak wujud atau bukan milik pengguna semasa"
|
||||
ErrAPIKeyAppBindingDisabled: "Pemautan APP melalui kod QR tidak dibenarkan untuk kunci API ini"
|
||||
ErrAPIKeyTerminalUpgradeRequired: "Sokongan pengesahan terminal melalui kunci API tidak dapat disahkan. Semak sambungan nod dan naik taraf Agent sebelum mencuba lagi."
|
||||
LocalAppSyncDispatch: "Hantar penyegerakan aplikasi tempatan"
|
||||
LocalAppSyncDispatchNode: "Hantar penyegerakan aplikasi tempatan ke {{.node}} (tugas nod: {{.taskID}})"
|
||||
ErrLocalAppSyncDisabled: "Penyegerakan aplikasi tempatan antara nod dinyahdayakan"
|
||||
|
||||
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "Já existe uma chave API não revogada com esse nome"
|
||||
ErrAPIKeyNotFound: "A chave API não existe ou não pertence ao usuário atual"
|
||||
ErrAPIKeyAppBindingDisabled: "Esta chave API não permite vincular o APP por código QR"
|
||||
ErrAPIKeyTerminalUpgradeRequired: "Não foi possível verificar o suporte à autorização de terminal por chave API. Verifique a conexão do nó e atualize o Agent antes de tentar novamente."
|
||||
LocalAppSyncDispatch: "Enviar sincronização de apps locais"
|
||||
LocalAppSyncDispatchNode: "Enviar sincronização de apps locais para {{.node}} (tarefa do nó: {{.taskID}})"
|
||||
ErrLocalAppSyncDisabled: "A sincronização de apps locais entre nós está desativada"
|
||||
|
||||
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "Неотозванный ключ API с таким име
|
||||
ErrAPIKeyNotFound: "Ключ API не существует или не принадлежит текущему пользователю"
|
||||
ErrAPIKeyAppBindingDisabled: "Для этого ключа API привязка приложения через QR-код запрещена"
|
||||
ErrAPIKeyTerminalUpgradeRequired: "Не удалось проверить поддержку авторизации терминала по ключу API. Проверьте подключение узла, обновите его Agent и повторите попытку."
|
||||
LocalAppSyncDispatch: "Отправка запросов синхронизации локальных приложений"
|
||||
LocalAppSyncDispatchNode: "Отправка запроса синхронизации узлу {{.node}} (задача узла: {{.taskID}})"
|
||||
ErrLocalAppSyncDisabled: "Синхронизация локальных приложений между узлами отключена"
|
||||
|
||||
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "Bu adda iptal edilmemiş bir API anahtarı zaten var"
|
||||
ErrAPIKeyNotFound: "API anahtarı mevcut değil veya geçerli kullanıcıya ait değil"
|
||||
ErrAPIKeyAppBindingDisabled: "Bu API anahtarı için QR koduyla APP bağlantısına izin verilmiyor"
|
||||
ErrAPIKeyTerminalUpgradeRequired: "API anahtarıyla terminal yetkilendirme desteği doğrulanamadı. Düğüm bağlantısını kontrol edip Agent sürümünü yükselttikten sonra tekrar deneyin."
|
||||
LocalAppSyncDispatch: "Yerel uygulama eşitleme isteği gönder"
|
||||
LocalAppSyncDispatchNode: "{{.node}} düğümüne yerel uygulama eşitleme isteği gönder (düğüm görevi: {{.taskID}})"
|
||||
ErrLocalAppSyncDisabled: "Düğümler arası yerel uygulama eşitleme devre dışı"
|
||||
|
||||
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "已存在同名且未撤銷的 API Key"
|
||||
ErrAPIKeyNotFound: "API Key 不存在或不屬於目前使用者"
|
||||
ErrAPIKeyAppBindingDisabled: "此 API Key 未允許 APP 掃碼綁定"
|
||||
ErrAPIKeyTerminalUpgradeRequired: "無法確認節點支援 API Key 終端授權,請檢查節點連線並升級 Agent 後重試。"
|
||||
LocalAppSyncDispatch: "下發本機應用同步"
|
||||
LocalAppSyncDispatchNode: "向節點 {{.node}} 下發本機應用同步(節點任務:{{.taskID}})"
|
||||
ErrLocalAppSyncDisabled: "尚未啟用本機應用跨節點同步"
|
||||
|
||||
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "已存在同名的未撤销 API Key"
|
||||
ErrAPIKeyNotFound: "API Key 不存在或不属于当前用户"
|
||||
ErrAPIKeyAppBindingDisabled: "此 API Key 未允许 APP 扫码绑定"
|
||||
ErrAPIKeyTerminalUpgradeRequired: "无法确认节点支持 API Key 终端授权,请检查节点连接并升级 Agent 后重试。"
|
||||
LocalAppSyncDispatch: "下发本地应用同步"
|
||||
LocalAppSyncDispatchNode: "向节点 {{.node}} 下发本地应用同步(节点任务:{{.taskID}})"
|
||||
ErrLocalAppSyncDisabled: "尚未开启本地应用跨节点同步"
|
||||
|
||||
@@ -26,6 +26,7 @@ func Init() {
|
||||
}
|
||||
|
||||
var baseSettingKeys = map[string]struct{}{
|
||||
"PanelCache": {},
|
||||
"PanelName": {},
|
||||
"Theme": {},
|
||||
"MenuTabs": {},
|
||||
|
||||
@@ -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' });
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ReqPage } from '.';
|
||||
|
||||
export namespace AI {
|
||||
export type AgentType = 'openclaw' | 'copaw' | 'hermes-agent';
|
||||
export type AgentType = 'openclaw' | 'copaw' | 'hermes-agent' | 'deepseek-harness' | 'octop';
|
||||
|
||||
export interface OllamaModelInfo {
|
||||
id: number;
|
||||
@@ -423,6 +423,7 @@ export namespace AI {
|
||||
}
|
||||
|
||||
export interface AgentCreateReq {
|
||||
accessHost?: string;
|
||||
name: string;
|
||||
remark: string;
|
||||
appVersion: string;
|
||||
@@ -450,6 +451,7 @@ export namespace AI {
|
||||
}
|
||||
|
||||
export interface AgentItem {
|
||||
accessHost: string;
|
||||
id: number;
|
||||
name: string;
|
||||
remark: string;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { ReqPage, CommonModel } from '.';
|
||||
|
||||
export namespace App {
|
||||
export interface LocalAppSyncConfig {
|
||||
enabled: boolean;
|
||||
}
|
||||
export interface App extends CommonModel {
|
||||
name: string;
|
||||
icon?: string;
|
||||
|
||||
@@ -77,6 +77,7 @@ export namespace Login {
|
||||
language: string;
|
||||
menuTabs: string;
|
||||
menuAccordion: string;
|
||||
panelCache: string;
|
||||
panelName: string;
|
||||
theme: string;
|
||||
isOffline: boolean;
|
||||
|
||||
@@ -8,6 +8,12 @@ export namespace Dashboard {
|
||||
|
||||
diskSize: number;
|
||||
}
|
||||
export interface QuickDirectory {
|
||||
id: number;
|
||||
legacy?: boolean;
|
||||
path: string;
|
||||
alias: string;
|
||||
}
|
||||
export interface QuickJump {
|
||||
id: number;
|
||||
name: string;
|
||||
|
||||
@@ -41,6 +41,7 @@ export namespace Setting {
|
||||
theme: string;
|
||||
menuTabs: string;
|
||||
menuAccordion: string;
|
||||
panelCache: string;
|
||||
language: string;
|
||||
docSource: string;
|
||||
|
||||
@@ -86,6 +87,7 @@ export namespace Setting {
|
||||
theme: string;
|
||||
menuTabs: string;
|
||||
menuAccordion: string;
|
||||
panelCache: string;
|
||||
language: string;
|
||||
hideMenu: string;
|
||||
docSource: string;
|
||||
|
||||
@@ -144,3 +144,15 @@ export function getAppIconUrl(appKey: string, node?: string): string {
|
||||
export const installAppToNodes = (param: App.InstallAppToNodes) => {
|
||||
return http.post(`/core/xpack/sync/app/install`, param);
|
||||
};
|
||||
|
||||
export const getLocalAppSyncConfig = () => {
|
||||
return http.get<App.LocalAppSyncConfig>('/core/xpack/settings/app/local-sync');
|
||||
};
|
||||
|
||||
export const updateLocalAppSyncConfig = (config: App.LocalAppSyncConfig) => {
|
||||
return http.post('/core/xpack/settings/app/local-sync', config);
|
||||
};
|
||||
|
||||
export const dispatchLocalAppSync = (req: App.AppStoreSync) => {
|
||||
return http.post('/core/xpack/settings/app/local-sync/dispatch', req);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import http from '@/api';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
import { ResPage, SearchWithPage } from '../interface';
|
||||
import { Container } from '../interface/container';
|
||||
import { TimeoutEnum } from '@/enums/http-enum';
|
||||
@@ -23,10 +24,11 @@ export const loadContainerUsers = (name: string) => {
|
||||
export const listContainerFiles = (params: Container.ContainerFileReq) => {
|
||||
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, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
timeout: TimeoutEnum.T_5M,
|
||||
...config,
|
||||
headers: { 'Content-Type': 'multipart/form-data', ...config?.headers },
|
||||
});
|
||||
};
|
||||
export const getContainerFileContent = (params: Container.ContainerFileReq) => {
|
||||
|
||||
@@ -8,7 +8,7 @@ export const loadOsInfo = () => {
|
||||
export const loadQuickOption = () => {
|
||||
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 });
|
||||
};
|
||||
export const loadAppLauncher = () => {
|
||||
@@ -48,3 +48,11 @@ export const loadTopMem = () => {
|
||||
export const systemRestart = (operation: string) => {
|
||||
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 });
|
||||
|
||||
@@ -10,8 +10,13 @@ export type FileUploadRequestConfig = AxiosRequestConfig & {
|
||||
skipErrorMessage?: boolean;
|
||||
};
|
||||
|
||||
export const getFilesList = (params: File.ReqFile) => {
|
||||
return http.post<File.File>('files/search', params, TimeoutEnum.T_5M);
|
||||
export const getFilesList = (params: File.ReqFile, currentNode?: string) => {
|
||||
return http.post<File.File>(
|
||||
'files/search',
|
||||
params,
|
||||
TimeoutEnum.T_5M,
|
||||
currentNode ? { CurrentNode: currentNode } : undefined,
|
||||
);
|
||||
};
|
||||
|
||||
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 });
|
||||
};
|
||||
|
||||
export const computeDirSize = (params: File.DirSizeReq) => {
|
||||
return http.post<File.DirSizeRes>('files/size', params, TimeoutEnum.T_5M);
|
||||
export const computeDirSize = (params: File.DirSizeReq, currentNode?: string) => {
|
||||
return http.post<File.DirSizeRes>(
|
||||
'files/size',
|
||||
params,
|
||||
TimeoutEnum.T_5M,
|
||||
currentNode ? { CurrentNode: currentNode } : undefined,
|
||||
);
|
||||
};
|
||||
|
||||
export const computeDepthDirSize = (params: File.DirSizeReq) => {
|
||||
@@ -241,8 +251,13 @@ export const clearRecycle = () => {
|
||||
return http.post<any>('files/recycle/clear');
|
||||
};
|
||||
|
||||
export const searchFavorite = (params: ReqPage) => {
|
||||
return http.post<ResPage<File.Favorite>>('files/favorite/search', params);
|
||||
export const searchFavorite = (params: ReqPage, currentNode?: string) => {
|
||||
return http.post<ResPage<File.Favorite>>(
|
||||
'files/favorite/search',
|
||||
params,
|
||||
undefined,
|
||||
currentNode ? { CurrentNode: currentNode } : undefined,
|
||||
);
|
||||
};
|
||||
|
||||
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 |
@@ -79,6 +79,16 @@
|
||||
@row-click="handleRowClick"
|
||||
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>
|
||||
<template #default="{ row }">
|
||||
<svg-icon
|
||||
@@ -131,6 +141,13 @@
|
||||
</el-table>
|
||||
</div>
|
||||
<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">
|
||||
{{ $t('file.currentSelect') }}
|
||||
<el-tooltip :content="selectRow.path" placement="top">
|
||||
@@ -163,7 +180,11 @@
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<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') }}
|
||||
</el-button>
|
||||
</span>
|
||||
@@ -174,18 +195,20 @@
|
||||
<script lang="ts" setup>
|
||||
import { File } from '@/api/interface/file';
|
||||
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 { MsgSuccess, MsgWarning } from '@/utils/message';
|
||||
import { useSearchableForSelect } from '@/views/host/file-management/hooks/searchable';
|
||||
import { computeSize } from '@/utils/size';
|
||||
import { debounce } from '@/utils/misc';
|
||||
const props = defineProps<{ currentNode?: string }>();
|
||||
const data = ref([]);
|
||||
const loading = ref(false);
|
||||
const paths = ref<string[]>([]);
|
||||
const req = reactive({ path: '/', expand: true, page: 1, pageSize: 300, showHidden: true });
|
||||
const selectRow = ref({ path: '', name: '' });
|
||||
const selectRows = ref<Array<{ path: string; name: string }>>([]);
|
||||
const selectedPathSet = computed(() => new Set(selectRows.value.map((item) => item.path)));
|
||||
const isMultiple = ref(false);
|
||||
const rowRefs = ref();
|
||||
const open = ref(false);
|
||||
@@ -196,7 +219,7 @@ const favorites = ref<File.Favorite[]>([]);
|
||||
const { searchableStatus, searchablePath, searchableInputRef, searchableInputBlur } = useSearchableForSelect(paths);
|
||||
const oldUrl = ref<string>('');
|
||||
|
||||
const em = defineEmits(['choose']);
|
||||
const em = defineEmits(['choose', 'close']);
|
||||
|
||||
const form = reactive({
|
||||
path: '/',
|
||||
@@ -211,6 +234,7 @@ interface DialogProps {
|
||||
isAll?: boolean;
|
||||
disabled?: boolean;
|
||||
multiple?: boolean;
|
||||
selectedPaths?: string[];
|
||||
}
|
||||
const acceptParams = (props: DialogProps): void => {
|
||||
form.path = props.path || '/';
|
||||
@@ -218,7 +242,8 @@ const acceptParams = (props: DialogProps): void => {
|
||||
form.isAll = props.isAll || false;
|
||||
form.disabled = props.disabled || 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: '' };
|
||||
openPage();
|
||||
req.path = form.path;
|
||||
@@ -245,6 +270,7 @@ const handleClose = () => {
|
||||
open.value = false;
|
||||
selectRow.value = { path: '', name: '' };
|
||||
selectRows.value = [];
|
||||
em('close');
|
||||
};
|
||||
|
||||
const openPage = () => {
|
||||
@@ -281,7 +307,7 @@ const openDir = async (row: File.File, column: any, event: any) => {
|
||||
}
|
||||
// Handle file click
|
||||
const fullPath = (req.path === '/' ? req.path : req.path + '/') + row.name;
|
||||
if (isMultiple.value) {
|
||||
if (isMultiple.value && (form.isAll || !form.dir)) {
|
||||
// Multiple mode: toggle selection
|
||||
const index = selectRows.value.findIndex((r) => r.path === fullPath);
|
||||
if (index > -1) {
|
||||
@@ -342,7 +368,7 @@ const getFileSize = async (path: string) => {
|
||||
};
|
||||
updateByPath(path, { btnLoading: true });
|
||||
try {
|
||||
const res = await getFileContent(params);
|
||||
const res = await getFileContent(params, props.currentNode);
|
||||
updateByPath(path, { dirSize: res.data.size });
|
||||
} finally {
|
||||
updateByPath(path, { btnLoading: false });
|
||||
@@ -355,7 +381,7 @@ const getDirSize = async (path: string) => {
|
||||
};
|
||||
updateByPath(path, { btnLoading: true });
|
||||
try {
|
||||
const res = await computeDirSize(req);
|
||||
const res = await computeDirSize(req, props.currentNode);
|
||||
updateByPath(path, { dirSize: res.data.size });
|
||||
} finally {
|
||||
updateByPath(path, { btnLoading: false });
|
||||
@@ -385,7 +411,7 @@ const getPaths = (reqPath: string) => {
|
||||
const search = async (req: File.ReqFile) => {
|
||||
req.dir = form.dir;
|
||||
loading.value = true;
|
||||
await getFilesList(req)
|
||||
await getFilesList(req, props.currentNode)
|
||||
.then((res) => {
|
||||
if (!res.data.path) {
|
||||
req.path = oldUrl.value;
|
||||
@@ -472,23 +498,26 @@ const createFolder = async (row: any) => {
|
||||
let addItem = {};
|
||||
Object.assign(addItem, addForm);
|
||||
loading.value = true;
|
||||
createFile(addItem as File.FileCreate)
|
||||
.then(() => {
|
||||
createFile(addItem as File.FileCreate, props.currentNode)
|
||||
.then(async () => {
|
||||
row.isCreate = false;
|
||||
disBtn.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
|
||||
await search(req);
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
onUpdated(() => {
|
||||
if (form.path != '') {
|
||||
req.path = form.path;
|
||||
const toggleDirectory = (path: string) => {
|
||||
const index = selectRows.value.findIndex((item) => item.path === path);
|
||||
if (index >= 0) {
|
||||
selectRows.value.splice(index, 1);
|
||||
} else {
|
||||
selectRows.value.push({ path, name: path });
|
||||
}
|
||||
search(req);
|
||||
});
|
||||
};
|
||||
|
||||
const removeSelected = (index: number) => {
|
||||
selectRows.value.splice(index, 1);
|
||||
@@ -496,7 +525,7 @@ const removeSelected = (index: number) => {
|
||||
|
||||
const loadFavorites = async () => {
|
||||
try {
|
||||
const res = await searchFavorite({ page: 1, pageSize: 100 });
|
||||
const res = await searchFavorite({ page: 1, pageSize: 100 }, props.currentNode);
|
||||
favorites.value = res.data.items || [];
|
||||
} catch {}
|
||||
};
|
||||
|
||||
@@ -20,57 +20,55 @@
|
||||
</template>
|
||||
|
||||
<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' });
|
||||
|
||||
const refreshRateUnit = ref<number>(0);
|
||||
const refreshRateUnit = ref(0);
|
||||
const emit = defineEmits(['search']);
|
||||
const props = defineProps({
|
||||
title: String,
|
||||
rate: Number,
|
||||
});
|
||||
|
||||
let timer: NodeJS.Timer | null = null;
|
||||
|
||||
const changeRefresh = (command: number) => {
|
||||
refreshRateUnit.value = command || 0;
|
||||
if (refreshRateUnit.value !== 0) {
|
||||
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 props = defineProps({ title: String, rate: Number });
|
||||
const route = useRoute();
|
||||
// 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: ReturnType<typeof setInterval> | undefined;
|
||||
let active = false;
|
||||
const stopTimer = () => {
|
||||
clearInterval(timer);
|
||||
timer = undefined;
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(Number(timer));
|
||||
timer = null;
|
||||
if (props.title) {
|
||||
localStorage.setItem(props.title, refreshRateUnit.value + '');
|
||||
const startTimer = () => {
|
||||
stopTimer();
|
||||
if (active && refreshRateUnit.value > 0) {
|
||||
timer = setInterval(() => emit('search'), 1000 * 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(() => {
|
||||
if (props.title && localStorage.getItem(props.title) != null) {
|
||||
let rate = Number(localStorage.getItem(props.title));
|
||||
refreshRateUnit.value = rate ? Number(rate) : 0;
|
||||
changeRefresh(refreshRateUnit.value);
|
||||
return;
|
||||
}
|
||||
if (props.rate) {
|
||||
refreshRateUnit.value = props.rate;
|
||||
changeRefresh(refreshRateUnit.value);
|
||||
}
|
||||
onMounted(activate);
|
||||
onActivated(activate);
|
||||
onDeactivated(() => {
|
||||
active = false;
|
||||
stopTimer();
|
||||
});
|
||||
onUnmounted(stopTimer);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -167,6 +167,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { panelCacheKey, writePanelCache } from '@/utils/panel-cache';
|
||||
import { computed, onDeactivated, onMounted, ref, useAttrs } from 'vue';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
import { flattenVNodes, type FuTableOperationButton } from '@/components/table/shared';
|
||||
@@ -341,13 +342,33 @@ const handleRightClick = (row, column, 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() {
|
||||
emit('search');
|
||||
}
|
||||
|
||||
function sizeChange() {
|
||||
if (props.paginationConfig.cacheSizeKey) {
|
||||
writePanelCache(
|
||||
'pagination',
|
||||
panelCacheKey(`size:${props.paginationConfig.cacheSizeKey}`),
|
||||
props.paginationConfig.pageSize,
|
||||
);
|
||||
}
|
||||
props.paginationConfig.currentPage = 1;
|
||||
localStorage.setItem(props.paginationConfig.cacheSizeKey, props.paginationConfig.pageSize);
|
||||
emit('search');
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { readCachedPageSize } from '@/utils/panel-cache';
|
||||
import TaskLog from '@/components/log/task/index.vue';
|
||||
import NodeSelect from '@/components/node-select/index.vue';
|
||||
|
||||
@@ -73,7 +74,7 @@ const data = ref();
|
||||
const paginationConfig = reactive({
|
||||
cacheSizeKey: 'task-list-page-size',
|
||||
currentPage: 1,
|
||||
pageSize: Number(localStorage.getItem('task-list-page-size')) || 20,
|
||||
pageSize: readCachedPageSize('task-list-page-size') || 20,
|
||||
total: 0,
|
||||
small: true,
|
||||
});
|
||||
|
||||
@@ -313,9 +313,10 @@ const initWebSocket = async (endpoint_: string, args: string = '') => {
|
||||
const protocol = href.split('//')[0] === 'http:' ? 'ws' : 'wss';
|
||||
const host = href.split('//')[1].split('/')[0];
|
||||
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}`;
|
||||
if (args.indexOf('operateNode=') !== -1) {
|
||||
if (params.has('operateNode')) {
|
||||
conn = `${protocol}://${host}/${endpoint}?cols=${term.value.cols}&rows=${term.value.rows}&${args}`;
|
||||
}
|
||||
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();
|
||||
});
|
||||
};
|
||||
Vendored
+5
@@ -5,3 +5,8 @@ declare module '*.vue' {
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
|
||||
declare module 'virtual:benchmark-report' {
|
||||
export const script: string;
|
||||
export const styles: string;
|
||||
}
|
||||
|
||||
@@ -494,6 +494,7 @@ const message = {
|
||||
recommend: 'Recommended',
|
||||
dir: 'Directory',
|
||||
alias: 'Alias',
|
||||
quickDirDelete: 'Delete ({0})?',
|
||||
quickDir: 'Quick Access',
|
||||
minQuickJump: 'Add at least one quick access entry',
|
||||
maxQuickJump: 'You can add up to four quick access entries',
|
||||
@@ -773,6 +774,15 @@ const message = {
|
||||
},
|
||||
aiTools: {
|
||||
agents: {
|
||||
deepSeekAccessHostHelper:
|
||||
"Enter the IPv4 address or hostname used in your browser, without a scheme, port or path. HTTPS uses the application's local certificate.",
|
||||
deepSeekAuthHelper:
|
||||
'Username: letters, numbers, dot, underscore or hyphen. Password: at least 12 characters, without leading/trailing spaces or line breaks.',
|
||||
octopAuthHelper:
|
||||
'Password: at least 8 characters with letters and digits; avoid common passwords. If rejected during initialization, check the application logs for setup instructions.',
|
||||
initialCredentials: "Initial credentials; use the application's current password if changed.",
|
||||
nativeAgentAccessUnavailable:
|
||||
'The existing installation has no compatible HTTPS access address or port. Check its application settings.',
|
||||
agent: 'Agent',
|
||||
account: 'Model Account',
|
||||
batchOperation: 'Batch Operation',
|
||||
@@ -1483,16 +1493,13 @@ const message = {
|
||||
statusWaiting: 'Waiting',
|
||||
base: 'Basic Information',
|
||||
backend: 'Backend',
|
||||
baseUrl: 'Base URL',
|
||||
endpoint: 'Endpoint',
|
||||
tokenizer: 'Tokenizer',
|
||||
tokenizerPlaceholder:
|
||||
'Select or enter a local tokenizer directory, e.g. /opt/1panel/tokenizers/DeepSeek-V3',
|
||||
tokenizerPlaceholder: 'Enter an absolute directory on the main node or a Hugging Face repository ID',
|
||||
runConfig: 'Run Config',
|
||||
rawResult: 'Raw Result',
|
||||
resultMetrics: 'Result Metrics',
|
||||
contextTokens: 'Context Length',
|
||||
contextTokensHelper: 'Input token limit plus output token limit for this benchmark.',
|
||||
outputThroughput: 'Output Throughput',
|
||||
outputThroughputHelper: 'Generated output tokens per second. Higher means faster generation.',
|
||||
totalThroughput: 'Total Throughput',
|
||||
@@ -1500,20 +1507,19 @@ const message = {
|
||||
firstTokenLatency: 'First Token Latency',
|
||||
firstTokenLatencyHelper:
|
||||
'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',
|
||||
concurrency: 'Concurrency',
|
||||
requestRate: 'Request Rate',
|
||||
requestRateUnlimited: 'Unlimited (max throughput)',
|
||||
requestRateCustom: 'Custom QPS',
|
||||
requestRateCustomPlaceholder: 'Requests per second, e.g. 2.5',
|
||||
successfulRequests: 'Successful Requests',
|
||||
requestThroughput: 'Request Throughput',
|
||||
mean: 'Mean',
|
||||
median: 'Median',
|
||||
timeout: 'Timeout (seconds)',
|
||||
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',
|
||||
},
|
||||
vllmMonitor: {
|
||||
@@ -1727,6 +1733,155 @@ const message = {
|
||||
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: {
|
||||
createByCommand: 'Create by command',
|
||||
importContainerBackupTip: 'Import container backup file, only .tar.gz is supported',
|
||||
@@ -2970,6 +3125,20 @@ const message = {
|
||||
remote: 'Remote SSH',
|
||||
},
|
||||
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',
|
||||
user: 'Panel user',
|
||||
userChange: 'Change panel user',
|
||||
@@ -3692,6 +3861,7 @@ const message = {
|
||||
link: 'Link',
|
||||
showCurrentArch: 'Architecture',
|
||||
syncLocalApp: 'Sync Local App',
|
||||
localAppSyncToNodes: 'Include child nodes when syncing local apps',
|
||||
memoryRequiredHelper: 'Current application memory requirement {0}',
|
||||
gpuConfig: 'Enable GPU Support',
|
||||
gpuConfigHelper:
|
||||
@@ -4252,7 +4422,7 @@ const message = {
|
||||
pushDir: 'Push the certificate to the local directory',
|
||||
dir: 'Directory',
|
||||
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',
|
||||
fromWebsite: 'From website',
|
||||
dnsMauanlHelper:
|
||||
|
||||
@@ -493,6 +493,7 @@ const message = {
|
||||
home: {
|
||||
recommend: 'Recomendado',
|
||||
dir: 'Directorio',
|
||||
quickDirDelete: '¿Confirmar la eliminación de ({0})?',
|
||||
quickDir: 'Acceso rápido',
|
||||
minQuickJump: 'Agrega al menos una entrada de acceso rápido',
|
||||
maxQuickJump: 'Puedes agregar hasta cuatro entradas de acceso rápido',
|
||||
@@ -776,6 +777,15 @@ const message = {
|
||||
},
|
||||
aiTools: {
|
||||
agents: {
|
||||
deepSeekAccessHostHelper:
|
||||
'Introduzca la IPv4 o el nombre de host usado en el navegador, sin protocolo, puerto ni ruta. HTTPS usa el certificado local de la aplicación.',
|
||||
deepSeekAuthHelper:
|
||||
'Usuario: letras, números, punto, guion bajo o guion. Contraseña: al menos 12 caracteres, sin espacios al inicio o al final ni saltos de línea.',
|
||||
octopAuthHelper:
|
||||
'Contraseña: al menos 8 caracteres con letras y números; evite contraseñas comunes. Si se rechaza al iniciar, consulte las instrucciones en los registros de la aplicación.',
|
||||
initialCredentials: 'Credenciales iniciales; si las cambió en la aplicación, use la contraseña actual.',
|
||||
nativeAgentAccessUnavailable:
|
||||
'La instalación existente no tiene una dirección o puerto HTTPS compatible. Revise la configuración de la aplicación.',
|
||||
agent: 'Agente',
|
||||
account: 'Cuenta de modelo',
|
||||
batchOperation: 'Operación por lotes',
|
||||
@@ -1491,16 +1501,14 @@ const message = {
|
||||
statusWaiting: 'En espera',
|
||||
base: 'Información básica',
|
||||
backend: 'Backend',
|
||||
baseUrl: 'URL base',
|
||||
endpoint: 'Endpoint',
|
||||
tokenizer: 'Tokenizador',
|
||||
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',
|
||||
rawResult: 'Resultado bruto',
|
||||
resultMetrics: 'Métricas de resultado',
|
||||
contextTokens: 'Longitud de contexto',
|
||||
contextTokensHelper: 'Suma del límite de tokens de entrada y salida de este benchmark.',
|
||||
outputThroughput: 'Rendimiento de salida',
|
||||
outputThroughputHelper: 'Tokens de salida generados por segundo. Un valor mayor indica más velocidad.',
|
||||
totalThroughput: 'Rendimiento total',
|
||||
@@ -1509,20 +1517,19 @@ const message = {
|
||||
firstTokenLatency: 'Latencia del primer token',
|
||||
firstTokenLatencyHelper:
|
||||
'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',
|
||||
concurrency: 'Concurrencia',
|
||||
mean: 'Media',
|
||||
median: 'Mediana',
|
||||
requestRate: 'Tasa de solicitudes',
|
||||
requestRateUnlimited: 'Ilimitado (máximo rendimiento)',
|
||||
requestRateCustom: 'QPS personalizado',
|
||||
requestRateCustomPlaceholder: 'Solicitudes por segundo, p. ej. 2.5',
|
||||
successfulRequests: 'Solicitudes correctas',
|
||||
requestThroughput: 'Rendimiento de solicitudes',
|
||||
timeout: 'Tiempo de espera (segundos)',
|
||||
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',
|
||||
},
|
||||
vllmMonitor: {
|
||||
@@ -1740,6 +1747,156 @@ const message = {
|
||||
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: {
|
||||
createByCommand: 'Crear por comando',
|
||||
importContainerBackupTip: 'Importar archivo de copia de seguridad de contenedor, solo se admite .tar.gz',
|
||||
@@ -3015,6 +3172,20 @@ const message = {
|
||||
remote: 'SSH remoto',
|
||||
},
|
||||
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',
|
||||
user: 'Usuario del panel',
|
||||
userChange: 'Cambiar usuario del panel',
|
||||
@@ -3759,6 +3930,7 @@ const message = {
|
||||
link: 'Enlace',
|
||||
showCurrentArch: 'Arquitectura',
|
||||
syncLocalApp: 'Sincronizar app local',
|
||||
localAppSyncToNodes: 'Incluir nodos secundarios al sincronizar apps locales',
|
||||
memoryRequiredHelper: 'La app requiere {0} de memoria',
|
||||
gpuConfig: 'Habilitar soporte GPU',
|
||||
gpuConfigHelper: 'Asegúrese de tener GPU NVIDIA, drivers y NVIDIA Docker Container Toolkit instalados',
|
||||
@@ -4301,7 +4473,8 @@ const message = {
|
||||
domainHelper: 'Un dominio por línea, admite * y dirección IP',
|
||||
pushDir: 'Guardar certificado en el directorio local',
|
||||
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',
|
||||
fromWebsite: 'Desde el sitio web',
|
||||
dnsMauanlHelper: 'En modo manual, haz clic en solicitar tras la creación para obtener el valor DNS',
|
||||
|
||||
@@ -489,6 +489,7 @@ const message = {
|
||||
recommend: 'پیشنهادی',
|
||||
dir: 'دایرکتوری',
|
||||
alias: 'نام مستعار',
|
||||
quickDirDelete: 'حذف ({0}) را تأیید میکنید؟',
|
||||
quickDir: 'دسترسی سریع',
|
||||
minQuickJump: 'حداقل یک ورودی دسترسی سریع اضافه کنید',
|
||||
maxQuickJump: 'میتوانید حداکثر چهار ورودی دسترسی سریع اضافه کنید',
|
||||
@@ -768,6 +769,14 @@ const message = {
|
||||
},
|
||||
aiTools: {
|
||||
agents: {
|
||||
deepSeekAccessHostHelper:
|
||||
'نشانی IPv4 یا نام میزبان مورد استفاده در مرورگر را بدون پروتکل، درگاه یا مسیر وارد کنید. HTTPS از گواهی محلی برنامه استفاده میکند.',
|
||||
deepSeekAuthHelper:
|
||||
'نام کاربری: حروف لاتین، اعداد، نقطه، زیرخط یا خط تیره. گذرواژه: حداقل ۱۲ نویسه، بدون فاصله در ابتدا و انتها یا شکست خط.',
|
||||
octopAuthHelper:
|
||||
'گذرواژه: حداقل ۸ نویسه شامل حروف و اعداد؛ از گذرواژههای رایج پرهیز کنید. اگر رد شد، راهنمای تنظیمات را در گزارشهای برنامه ببینید.',
|
||||
initialCredentials: 'اطلاعات ورود اولیه؛ اگر در برنامه تغییر کرده، از گذرواژه فعلی استفاده کنید.',
|
||||
nativeAgentAccessUnavailable: 'نصب موجود نشانی یا درگاه HTTPS سازگار ندارد. تنظیمات برنامه را بررسی کنید.',
|
||||
agent: 'عامل',
|
||||
account: 'حساب مدل',
|
||||
batchOperation: 'عملیات دستهای',
|
||||
@@ -1468,16 +1477,13 @@ const message = {
|
||||
statusWaiting: 'در انتظار',
|
||||
base: 'اطلاعات پایه',
|
||||
backend: 'بکاند',
|
||||
baseUrl: 'URL پایه',
|
||||
endpoint: 'نقطه پایانی',
|
||||
tokenizer: 'توکنساز',
|
||||
tokenizerPlaceholder:
|
||||
'یک دایرکتوری توکنساز محلی را انتخاب یا وارد کنید، مثلاً /opt/1panel/tokenizers/DeepSeek-V3',
|
||||
tokenizerPlaceholder: 'مسیر مطلق پوشه در گره اصلی یا شناسه مخزن Hugging Face را وارد کنید',
|
||||
runConfig: 'پیکربندی اجرا',
|
||||
rawResult: 'نتیجه خام',
|
||||
resultMetrics: 'معیارهای نتیجه',
|
||||
contextTokens: 'طول زمینه',
|
||||
contextTokensHelper: 'محدودیت توکن ورودی به اضافه محدودیت توکن خروجی برای این معیار سنجش.',
|
||||
outputThroughput: 'توان خروجی',
|
||||
outputThroughputHelper: 'توکنهای خروجی تولید شده در ثانیه. بیشتر به معنای تولید سریعتر است.',
|
||||
totalThroughput: 'توان کل',
|
||||
@@ -1485,20 +1491,19 @@ const message = {
|
||||
'توکنهای ورودی و خروجی پردازش شده در ثانیه. برای اندازهگیری ظرفیت کلی استفاده میشود.',
|
||||
firstTokenLatency: 'تأخیر اولین توکن',
|
||||
firstTokenLatencyHelper: 'زمان از ارسال درخواست تا دریافت اولین توکن. کمتر به معنای پاسخ سریعتر است.',
|
||||
tokenValueHelper: 'یک عدد صحیح مثبت یا مقدار k وارد کنید، مثلاً 512، 1k، 32k',
|
||||
numPrompts: 'راهنماها',
|
||||
concurrency: 'همروندی',
|
||||
requestRate: 'نرخ درخواست',
|
||||
requestRateUnlimited: 'نامحدود (حداکثر توان)',
|
||||
requestRateCustom: 'QPS سفارشی',
|
||||
requestRateCustomPlaceholder: 'درخواست در ثانیه، مثلاً 2.5',
|
||||
successfulRequests: 'درخواستهای موفق',
|
||||
requestThroughput: 'توان درخواست',
|
||||
mean: 'میانگین',
|
||||
median: 'میانه',
|
||||
timeout: 'مهلت (ثانیه)',
|
||||
image: 'تصویر vLLM',
|
||||
ignoreEos: 'نادیده گرفتن EOS',
|
||||
ignoreEos: 'نادیده گرفتن توکن پایان دنباله (EOS)',
|
||||
ignoreEosHelp:
|
||||
'از توقف زودهنگام به دلیل EOS جلوگیری میکند تا طول خروجی تنظیمشده آزموده شود. سایر شرایط توقف همچنان اعمال میشوند و پشتیبانی سرور لازم است.',
|
||||
extraHeaders: 'هدرهای اضافی',
|
||||
},
|
||||
vllmMonitor: {
|
||||
@@ -1709,6 +1714,153 @@ const message = {
|
||||
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: {
|
||||
createByCommand: 'ایجاد با دستور',
|
||||
importContainerBackupTip: 'واردات فایل پشتیبان کانتینر، فقط .tar.gz پشتیبانی میشود',
|
||||
@@ -2940,6 +3092,20 @@ const message = {
|
||||
remote: 'SSH از راه دور',
|
||||
},
|
||||
setting: {
|
||||
panelCache: {
|
||||
title: 'تنظیمات حافظهٔ نهان',
|
||||
tabs: 'حافظهٔ نهان زبانهها',
|
||||
pagination: 'حافظهٔ نهان صفحهبندی',
|
||||
refresh: 'حافظهٔ نهان فاصلهٔ بازخوانی',
|
||||
search: 'حافظهٔ نهان ورودی جستجو',
|
||||
duration: 'مدت نگهداری حافظهٔ نهان',
|
||||
'24h': '۲۴ ساعت',
|
||||
week: 'یک هفته',
|
||||
month: 'یک ماه',
|
||||
forever: 'دائمی',
|
||||
helper: 'مدت اعتبار از آخرین تغییر وضعیت محاسبه میشود. پس از انقضا، مقادیر پیشفرض بازیابی میشوند.',
|
||||
},
|
||||
|
||||
panel: 'پنل',
|
||||
user: 'کاربر پنل',
|
||||
userChange: 'تغییر کاربر پنل',
|
||||
@@ -3658,6 +3824,7 @@ const message = {
|
||||
link: 'لینک',
|
||||
showCurrentArch: 'معماری',
|
||||
syncLocalApp: 'همگامسازی برنامه محلی',
|
||||
localAppSyncToNodes: 'همگامسازی گرههای فرزند هنگام همگامسازی برنامههای محلی',
|
||||
memoryRequiredHelper: 'نیاز حافظه برنامه فعلی {0}',
|
||||
gpuConfig: 'فعالسازی پشتیبانی GPU',
|
||||
gpuConfigHelper:
|
||||
@@ -4214,7 +4381,8 @@ const message = {
|
||||
domainHelper: 'هر خط یک نام دامنه، پشتیبانی از * و آدرس IP',
|
||||
pushDir: 'ارسال گواهی به دایرکتوری محلی',
|
||||
dir: 'دایرکتوری',
|
||||
pushDirHelper: 'فایل گواهی "fullchain.pem" و فایل کلید "privkey.pem" در این دایرکتوری تولید میشوند.',
|
||||
pushDirHelper:
|
||||
'چند دایرکتوری انتخاب کنید یا در هر خط یک دایرکتوری وارد کنید. فایلهای "fullchain.pem" و "privkey.pem" در هر دایرکتوری ایجاد میشوند.',
|
||||
organizationDetail: 'جزئیات سازمان',
|
||||
fromWebsite: 'از وبسایت',
|
||||
dnsMauanlHelper:
|
||||
|
||||
@@ -492,6 +492,7 @@ const message = {
|
||||
recommend: 'おすすめ',
|
||||
dir: 'ディレクトリ',
|
||||
alias: 'エイリアス',
|
||||
quickDirDelete: '({0})を削除しますか?',
|
||||
quickDir: 'クイックアクセス',
|
||||
minQuickJump: 'クイックアクセス項目を1つ以上追加してください',
|
||||
maxQuickJump: '最大4件のクイックアクセス項目を追加できます',
|
||||
@@ -772,6 +773,15 @@ const message = {
|
||||
},
|
||||
aiTools: {
|
||||
agents: {
|
||||
deepSeekAccessHostHelper:
|
||||
'ブラウザーで使用する IPv4 アドレスまたはホスト名を、スキーム・ポート・パスなしで入力してください。HTTPS はアプリのローカル証明書を使用します。',
|
||||
deepSeekAuthHelper:
|
||||
'ユーザー名には英数字、ドット、アンダースコア、ハイフンを使用できます。パスワードは12文字以上で、前後の空白や改行を含めないでください。',
|
||||
octopAuthHelper:
|
||||
'パスワードは英字と数字を含む8文字以上とし、よく使われるパスワードを避けてください。初期化時に拒否された場合は、アプリのログで設定手順を確認してください。',
|
||||
initialCredentials: '初期ログイン情報です。アプリ内で変更した場合は現在のパスワードを使用してください。',
|
||||
nativeAgentAccessUnavailable:
|
||||
'既存のインストールに対応する HTTPS アクセス先またはポートがありません。アプリの設定を確認してください。',
|
||||
agent: 'エージェント',
|
||||
account: 'モデルアカウント',
|
||||
batchOperation: '一括操作',
|
||||
@@ -1472,36 +1482,32 @@ const message = {
|
||||
statusWaiting: '待機中',
|
||||
base: '基本情報',
|
||||
backend: 'バックエンド',
|
||||
baseUrl: 'Base URL',
|
||||
endpoint: 'エンドポイント',
|
||||
tokenizer: 'トークナイザー',
|
||||
tokenizerPlaceholder:
|
||||
'ローカルトークナイザーディレクトリを選択または入力してください。例: /opt/1panel/tokenizers/DeepSeek-V3',
|
||||
tokenizerPlaceholder: 'メインノード上の絶対ディレクトリパス、または Hugging Face リポジトリ ID を入力',
|
||||
runConfig: '実行設定',
|
||||
rawResult: '生データ',
|
||||
resultMetrics: '結果指標',
|
||||
contextTokens: 'コンテキスト長',
|
||||
contextTokensHelper: 'このベンチマークの入力 Token 上限と出力 Token 上限の合計です。',
|
||||
outputThroughput: '出力スループット',
|
||||
outputThroughputHelper: '1 秒あたりに生成される出力 Token 数です。高いほど生成が速くなります。',
|
||||
totalThroughput: '合計スループット',
|
||||
totalThroughputHelper: '1 秒あたりに処理される入力と出力 Token の合計で、全体性能の測定に使います。',
|
||||
firstTokenLatency: '初回 Token レイテンシ',
|
||||
firstTokenLatencyHelper: 'リクエスト送信から最初の Token 受信までの時間です。低いほど応答が速くなります。',
|
||||
tokenValueHelper: '正の整数または k 単位を入力してください。例: 512、1k、32k',
|
||||
numPrompts: 'プロンプト数',
|
||||
concurrency: '同時実行数',
|
||||
mean: '平均値',
|
||||
median: '中央値',
|
||||
requestRate: 'リクエストレート',
|
||||
requestRateUnlimited: '無制限(最大スループット)',
|
||||
requestRateCustom: 'カスタム QPS',
|
||||
requestRateCustomPlaceholder: '1 秒あたりのリクエスト数。例: 2.5',
|
||||
successfulRequests: '成功リクエスト',
|
||||
requestThroughput: 'リクエストスループット',
|
||||
timeout: 'タイムアウト(秒)',
|
||||
image: 'vLLM イメージ',
|
||||
ignoreEos: 'EOS を無視',
|
||||
ignoreEos: '終了トークン(EOS)を無視',
|
||||
ignoreEosHelp:
|
||||
'EOS による早期終了を防ぎ、指定した出力長でのテストを助けます。他の停止条件は有効で、サーバー側の対応が必要です。',
|
||||
extraHeaders: '追加ヘッダー',
|
||||
},
|
||||
vllmMonitor: {
|
||||
@@ -1711,6 +1717,153 @@ const message = {
|
||||
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: {
|
||||
createByCommand: 'コマンドで作成',
|
||||
importContainerBackupTip: 'コンテナバックアップファイルをインポートします。.tar.gz のみ対応です',
|
||||
@@ -2945,6 +3098,20 @@ const message = {
|
||||
remote: 'リモートSSH',
|
||||
},
|
||||
setting: {
|
||||
panelCache: {
|
||||
title: 'キャッシュ設定',
|
||||
tabs: 'タブのキャッシュ',
|
||||
pagination: 'ページネーションのキャッシュ',
|
||||
refresh: '更新間隔のキャッシュ',
|
||||
search: '検索入力のキャッシュ',
|
||||
duration: 'キャッシュ期間',
|
||||
'24h': '24時間',
|
||||
week: '1週間',
|
||||
month: '1か月',
|
||||
forever: '無期限',
|
||||
helper: '有効期間は状態を最後に変更した時点から計算されます。期限が切れると既定値に戻ります。',
|
||||
},
|
||||
|
||||
panel: 'パネル',
|
||||
user: 'パネルユーザー',
|
||||
userChange: 'パネルユーザーを変更します',
|
||||
@@ -3678,6 +3845,7 @@ const message = {
|
||||
link: 'リンク',
|
||||
showCurrentArch: '現在のサーバーアーキテクチャのアプリケーション',
|
||||
syncLocalApp: 'ローカルアプリの同期',
|
||||
localAppSyncToNodes: 'ローカルアプリ同期時に子ノードも同期',
|
||||
memoryRequiredHelper: '現在のアプリケーションは {0} メモリが必要です',
|
||||
gpuConfig: 'GPUサポートを有効化',
|
||||
gpuConfigHelper:
|
||||
@@ -4237,7 +4405,8 @@ const message = {
|
||||
domainHelper: '1行ごとに1つのドメイン名、 *およびIPアドレスをサポートします',
|
||||
pushDir: '証明書をローカルディレクトリにプッシュします',
|
||||
dir: 'ディレクトリ',
|
||||
pushDirHelper: '証明書ファイル「FullChain.PEM」とキーファイル「Privkey.Pem」がこのディレクトリで生成されます。',
|
||||
pushDirHelper:
|
||||
'複数のディレクトリを選択するか、1行に1つずつディレクトリを入力してください。各ディレクトリに「fullchain.pem」と「privkey.pem」が生成されます。',
|
||||
organizationDetail: '組織の詳細',
|
||||
fromWebsite: 'ウェブサイトから',
|
||||
dnsMauanlHelper: '手動解像度モードでは、作成後に適用ボタンをクリックしてDNS解像度値を取得する必要があります',
|
||||
|
||||
@@ -487,6 +487,7 @@ const message = {
|
||||
recommend: '추천',
|
||||
dir: '디렉토리',
|
||||
alias: '별칭',
|
||||
quickDirDelete: '({0})을(를) 삭제하시겠습니까?',
|
||||
quickDir: '빠른 액세스',
|
||||
minQuickJump: '최소 하나의 빠른 액세스 항목을 추가하세요',
|
||||
maxQuickJump: '빠른 액세스를 최대 4개까지 추가할 수 있습니다',
|
||||
@@ -763,6 +764,15 @@ const message = {
|
||||
},
|
||||
aiTools: {
|
||||
agents: {
|
||||
deepSeekAccessHostHelper:
|
||||
'브라우저에서 사용하는 IPv4 주소 또는 호스트 이름을 입력하세요. 프로토콜, 포트, 경로는 제외하세요. HTTPS는 앱의 로컬 인증서를 사용합니다.',
|
||||
deepSeekAuthHelper:
|
||||
'사용자 이름은 영문, 숫자, 점, 밑줄, 하이픈만 지원합니다. 비밀번호는 12자 이상이며 앞뒤 공백이나 줄바꿈을 포함할 수 없습니다.',
|
||||
octopAuthHelper:
|
||||
'비밀번호는 문자와 숫자를 포함한 8자 이상이어야 하며 흔한 비밀번호는 피하세요. 초기화 시 거부되면 앱 로그의 설정 안내를 확인하세요.',
|
||||
initialCredentials: '초기 로그인 정보입니다. 앱에서 변경했다면 현재 비밀번호를 사용하세요.',
|
||||
nativeAgentAccessUnavailable:
|
||||
'기존 설치에 호환되는 HTTPS 접속 주소 또는 포트가 없습니다. 앱 설정을 확인하세요.',
|
||||
agent: '에이전트',
|
||||
account: '모델 계정',
|
||||
batchOperation: '일괄 작업',
|
||||
@@ -1457,36 +1467,32 @@ const message = {
|
||||
statusWaiting: '대기 중',
|
||||
base: '기본 정보',
|
||||
backend: '백엔드',
|
||||
baseUrl: 'Base URL',
|
||||
endpoint: '엔드포인트',
|
||||
tokenizer: '토크나이저',
|
||||
tokenizerPlaceholder:
|
||||
'로컬 토크나이저 디렉터리를 선택하거나 입력하세요. 예: /opt/1panel/tokenizers/DeepSeek-V3',
|
||||
tokenizerPlaceholder: '메인 노드의 절대 디렉터리 경로 또는 Hugging Face 저장소 ID를 입력하세요',
|
||||
runConfig: '실행 구성',
|
||||
rawResult: '원시 결과',
|
||||
resultMetrics: '결과 지표',
|
||||
contextTokens: '컨텍스트 길이',
|
||||
contextTokensHelper: '이 벤치마크의 입력 Token 제한과 출력 Token 제한의 합계입니다.',
|
||||
outputThroughput: '출력 처리량',
|
||||
outputThroughputHelper: '초당 생성된 출력 Token 수입니다. 높을수록 생성 속도가 빠릅니다.',
|
||||
totalThroughput: '총 처리량',
|
||||
totalThroughputHelper: '초당 처리된 입력 및 출력 Token 수로 전체 처리 능력을 측정합니다.',
|
||||
firstTokenLatency: '첫 Token 지연 시간',
|
||||
firstTokenLatencyHelper: '요청 전송부터 첫 Token 수신까지의 시간입니다. 낮을수록 응답이 빠릅니다.',
|
||||
tokenValueHelper: '양의 정수 또는 k 값을 입력하세요. 예: 512, 1k, 32k',
|
||||
numPrompts: '프롬프트 수',
|
||||
concurrency: '동시성',
|
||||
mean: '평균값',
|
||||
median: '중앙값',
|
||||
requestRate: '요청 속도',
|
||||
requestRateUnlimited: '무제한(최대 처리량)',
|
||||
requestRateCustom: '사용자 지정 QPS',
|
||||
requestRateCustomPlaceholder: '초당 요청 수, 예: 2.5',
|
||||
successfulRequests: '성공한 요청',
|
||||
requestThroughput: '요청 처리량',
|
||||
timeout: '시간 초과(초)',
|
||||
image: 'vLLM 이미지',
|
||||
ignoreEos: 'EOS 무시',
|
||||
ignoreEos: '종료 토큰(EOS) 무시',
|
||||
ignoreEosHelp:
|
||||
'EOS로 인한 조기 종료를 방지하여 설정한 출력 길이로 테스트하도록 돕습니다. 다른 종료 조건은 계속 적용되며 서버 지원이 필요합니다.',
|
||||
extraHeaders: '추가 헤더',
|
||||
},
|
||||
vllmMonitor: {
|
||||
@@ -1694,6 +1700,150 @@ const message = {
|
||||
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: {
|
||||
createByCommand: '명령으로 생성',
|
||||
importContainerBackupTip: '컨테이너 백업 파일 가져오기, .tar.gz 형식만 지원합니다',
|
||||
@@ -2906,6 +3056,20 @@ const message = {
|
||||
remote: '원격 SSH',
|
||||
},
|
||||
setting: {
|
||||
panelCache: {
|
||||
title: '캐시 설정',
|
||||
tabs: '탭 캐시',
|
||||
pagination: '페이지 이동 캐시',
|
||||
refresh: '새로 고침 간격 캐시',
|
||||
search: '검색 입력 캐시',
|
||||
duration: '캐시 유지 기간',
|
||||
'24h': '24시간',
|
||||
week: '1주',
|
||||
month: '1개월',
|
||||
forever: '무기한',
|
||||
helper: '유효 기간은 상태를 마지막으로 변경한 시점부터 계산됩니다. 만료되면 기본값으로 돌아갑니다.',
|
||||
},
|
||||
|
||||
panel: '패널',
|
||||
user: '패널 사용자',
|
||||
userChange: '패널 사용자 변경',
|
||||
@@ -3613,6 +3777,7 @@ const message = {
|
||||
link: '링크',
|
||||
showCurrentArch: '현재 서버 아키텍처 애플리케이션',
|
||||
syncLocalApp: '로컬 애플리케이션 동기화',
|
||||
localAppSyncToNodes: '로컬 앱 동기화 시 하위 노드도 동기화',
|
||||
memoryRequiredHelper: '현재 애플리케이션은 {0} 메모리가 필요합니다',
|
||||
gpuConfig: 'GPU 지원 활성화',
|
||||
gpuConfigHelper:
|
||||
@@ -4159,7 +4324,8 @@ const message = {
|
||||
domainHelper: '줄당 하나의 도메인 이름, * 및 IP 주소 지원',
|
||||
pushDir: '로컬 디렉토리로 인증서 푸시',
|
||||
dir: '디렉토리',
|
||||
pushDirHelper: '인증서 파일 "fullchain.pem" 및 키 파일 "privkey.pem"이 이 디렉토리에 생성됩니다.',
|
||||
pushDirHelper:
|
||||
'여러 디렉터리를 선택하거나 한 줄에 하나씩 입력하세요. 각 디렉터리에 "fullchain.pem"과 "privkey.pem" 파일이 생성됩니다.',
|
||||
organizationDetail: '조직 세부 정보',
|
||||
fromWebsite: '웹사이트에서 가져오기',
|
||||
dnsMauanlHelper: '수동 해석 모드에서는 생성 후 신청 버튼을 클릭하여 DNS 해석 값을 얻어야 합니다.',
|
||||
|
||||
@@ -487,6 +487,7 @@ const message = {
|
||||
recommend: 'ແນະນຳ',
|
||||
dir: 'ໄດເຣັກທໍຣີ',
|
||||
alias: 'ຊື່ຫຼິ້ນ',
|
||||
quickDirDelete: 'ຢືນຢັນການລຶບ ({0}) ຫຼືບໍ່?',
|
||||
quickDir: 'ເຂົ້າເຖິງດ່ວນ',
|
||||
minQuickJump: 'ເພີ່ມການເຂົ້າເຖິງດ່ວນຢ່າງໜ້ອຍໜຶ່ງລາຍການ',
|
||||
maxQuickJump: 'ທ່ານສາມາດເພີ່ມການເຂົ້າເຖິງດ່ວນໄດ້ສູງສຸດສີ່ລາຍການ',
|
||||
@@ -761,6 +762,15 @@ const message = {
|
||||
},
|
||||
aiTools: {
|
||||
agents: {
|
||||
deepSeekAccessHostHelper:
|
||||
'ປ້ອນທີ່ຢູ່ IPv4 ຫຼືຊື່ໂຮສທີ່ໃຊ້ໃນບຣາວເຊີ ໂດຍບໍ່ມີໂປຣໂຕຄອນ, ພອດ ຫຼືເສັ້ນທາງ. HTTPS ໃຊ້ໃບຢັ້ງຢືນພາຍໃນຂອງແອັບ.',
|
||||
deepSeekAuthHelper:
|
||||
'ຊື່ຜູ້ໃຊ້: ຕົວອັກສອນອັງກິດ, ຕົວເລກ, ຈຸດ, ຂີດລຸ່ມ ຫຼືຂີດກາງ. ລະຫັດຜ່ານ: ຢ່າງໜ້ອຍ 12 ຕົວ ໂດຍບໍ່ມີຊ່ອງຫວ່າງຢູ່ຫົວ/ທ້າຍ ຫຼືຂຶ້ນແຖວໃໝ່.',
|
||||
octopAuthHelper:
|
||||
'ລະຫັດຜ່ານ: ຢ່າງໜ້ອຍ 8 ຕົວ ປະກອບດ້ວຍຕົວອັກສອນແລະຕົວເລກ; ຫຼີກລ້ຽງລະຫັດທີ່ໃຊ້ທົ່ວໄປ. ຖ້າຖືກປະຕິເສດ ໃຫ້ເບິ່ງຄຳແນະນຳໃນບັນທຶກຂອງແອັບ.',
|
||||
initialCredentials: 'ຂໍ້ມູນເຂົ້າລະບົບເບື້ອງຕົ້ນ; ຖ້າປ່ຽນໃນແອັບແລ້ວ ໃຫ້ໃຊ້ລະຫັດປັດຈຸບັນ.',
|
||||
nativeAgentAccessUnavailable:
|
||||
'ການຕິດຕັ້ງທີ່ມີຢູ່ບໍ່ມີທີ່ຢູ່ ຫຼືພອດ HTTPS ທີ່ເຂົ້າກັນໄດ້. ກະລຸນາກວດສອບການຕັ້ງຄ່າແອັບ.',
|
||||
agent: 'ຕົວແທນ (Agent)',
|
||||
account: 'ບັນຊີໂມເດວ',
|
||||
batchOperation: 'ດຳເນີນການແບບກຸ່ມ',
|
||||
@@ -1451,36 +1461,32 @@ const message = {
|
||||
statusWaiting: 'ກຳລັງລໍຖ້າ',
|
||||
base: 'ຂໍ້ມູນພື້ນຖານ',
|
||||
backend: 'Backend',
|
||||
baseUrl: 'Base URL',
|
||||
endpoint: 'Endpoint',
|
||||
tokenizer: 'Tokenizer',
|
||||
tokenizerPlaceholder:
|
||||
'ເລືອກ ຫຼື ປ້ອນໂຟນເດີ tokenizer ພາຍໃນເຄື່ອງ, ເຊັ່ນ: /opt/1panel/tokenizers/DeepSeek-V3',
|
||||
tokenizerPlaceholder: 'ປ້ອນເສັ້ນທາງໂຟນເດີແບບສົມບູນໃນໂນດຫຼັກ ຫຼື ID ຄັງ Hugging Face',
|
||||
runConfig: 'ການກຳນົດຄ່າການລັນ',
|
||||
rawResult: 'ຜົນລັບດິບ (Raw Result)',
|
||||
resultMetrics: 'ຕົວຊີ້ວັດຜົນລັບ',
|
||||
contextTokens: 'ຄວາມຍາວ Context',
|
||||
contextTokensHelper: 'ຂີດຈຳກັດ input token ບວກກັບ output token ສຳລັບການທົດສອບນີ້.',
|
||||
outputThroughput: 'ອັດຕາການສົ່ງອອກ',
|
||||
outputThroughputHelper: 'ຈຳນວນ output tokens ທີ່ສ້າງໄດ້ຕໍ່ວິນາທີ. ຍິ່ງສູງຍິ່ງໄວ.',
|
||||
totalThroughput: 'ອັດຕາການປະມວນຜົນລວມ',
|
||||
totalThroughputHelper: 'ຈຳນວນ input ແລະ output tokens ທີ່ປະມວນຜົນຕໍ່ວິນາທີ. ໃຊ້ວັດແທກຄວາມສາມາດໂດຍລວມ.',
|
||||
firstTokenLatency: 'ຄວາມໜ່ວງຂອງ Token ທຳອິດ',
|
||||
firstTokenLatencyHelper: 'ເວລາຕັ້ງແຕ່ສົ່ງຄຳຂໍຈົນເຖິງການໄດ້ຮັບ token ທຳອິດ. ຍິ່ງຕ່ຳຍິ່ງຕອບສະໜອງໄວ.',
|
||||
tokenValueHelper: 'ປ້ອນຕົວເລກຈຳນວນເຕັມ ຫຼື ຄ່າ k, ເຊັ່ນ: 512, 1k, 32k',
|
||||
numPrompts: 'ຈຳນວນ Prompt',
|
||||
concurrency: 'ການເຮັດວຽກພ້ອມກັນ',
|
||||
requestRate: 'ອັດຕາຄຳຂໍ',
|
||||
requestRateUnlimited: 'ບໍ່ຈຳກັດ (max throughput)',
|
||||
requestRateCustom: 'ກຳນົດ QPS ເອງ',
|
||||
requestRateCustomPlaceholder: 'ຄຳຂໍຕໍ່ວິນາທີ, ເຊັ່ນ: 2.5',
|
||||
successfulRequests: 'ຄຳຂໍທີ່ສຳເລັດ',
|
||||
requestThroughput: 'ອັດຕາຄຳຮ້ອງຂໍ',
|
||||
mean: 'ຄ່າສະເລ່ຍ',
|
||||
median: 'ຄ່າມັດທະຍະຖານ',
|
||||
timeout: 'ໝົດເວລາ (ວິນາທີ)',
|
||||
image: 'vLLM Image',
|
||||
ignoreEos: 'ລະເວັ້ນ EOS',
|
||||
ignoreEos: 'ລະເວັ້ນໂທເຄັນສິ້ນສຸດ (EOS)',
|
||||
ignoreEosHelp:
|
||||
'ປ້ອງກັນການຢຸດກ່ອນກຳນົດຍ້ອນ EOS ເພື່ອທົດສອບຄວາມຍາວຜົນລັບທີ່ຕັ້ງໄວ້. ເງື່ອນໄຂຢຸດອື່ນຍັງມີຜົນ ແລະຕ້ອງມີການຮອງຮັບຈາກເຊີບເວີ.',
|
||||
extraHeaders: 'Headers ເພີ່ມເຕີມ',
|
||||
},
|
||||
vllmMonitor: {
|
||||
@@ -1685,6 +1691,147 @@ const message = {
|
||||
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: {
|
||||
createByCommand: 'ສ້າງດ້ວຍຄຳສັ່ງ',
|
||||
importContainerBackupTip: 'ນຳເຂົ້າໄຟລ໌ສຳຮອງຄອນເທນເນີ, ຮອງຮັບສະເພາະ .tar.gz',
|
||||
@@ -2887,6 +3034,20 @@ const message = {
|
||||
remote: 'SSH ທາງໄກ',
|
||||
},
|
||||
setting: {
|
||||
panelCache: {
|
||||
title: 'ການຕັ້ງຄ່າແຄດ',
|
||||
tabs: 'ແຄດແຖບ',
|
||||
pagination: 'ແຄດການແບ່ງໜ້າ',
|
||||
refresh: 'ແຄດຊ່ວງເວລາຣີເຟຣຊ',
|
||||
search: 'ແຄດຂໍ້ຄວາມຄົ້ນຫາ',
|
||||
duration: 'ໄລຍະເວລາເກັບແຄດ',
|
||||
'24h': '24 ຊົ່ວໂມງ',
|
||||
week: 'ໜຶ່ງອາທິດ',
|
||||
month: 'ໜຶ່ງເດືອນ',
|
||||
forever: 'ຖາວອນ',
|
||||
helper: 'ໄລຍະເວລາຈະນັບຈາກການປ່ຽນສະຖານະຄັ້ງຫຼ້າສຸດ. ເມື່ອໝົດອາຍຸ ຈະກັບຄືນເປັນຄ່າເລີ່ມຕົ້ນ.',
|
||||
},
|
||||
|
||||
panel: 'ພາເນລ',
|
||||
user: 'ຜູ້ໃຊ້ພາເນລ',
|
||||
userChange: 'ປ່ຽນຜູ້ໃຊ້ພາເນລ',
|
||||
@@ -3590,6 +3751,7 @@ const message = {
|
||||
link: 'ລິ້ງ',
|
||||
showCurrentArch: 'ສະຖາປັດຕະຍະກຳ',
|
||||
syncLocalApp: 'ຊິງຄ໌ແອັບໃນເຄື່ອງ',
|
||||
localAppSyncToNodes: 'ຮວມໂນດລູກເມື່ອຊິງຄ໌ແອັບໃນເຄື່ອງ',
|
||||
memoryRequiredHelper: 'ຄວາມຕ້ອງການໜ່ວຍຄວາມຈຳຂອງແອັບປະຈຸບັນ {0}',
|
||||
gpuConfig: 'ເປີດໃຊ້ການຮອງຮັບ GPU',
|
||||
gpuConfigHelper:
|
||||
@@ -4131,7 +4293,8 @@ const message = {
|
||||
domainHelper: 'ໜຶ່ງໂດເມນຕໍ່ແຖວ, ຮອງຮັບ * ແລະ ທີ່ຢູ່ IP',
|
||||
pushDir: 'ສົ່ງໃບຢັ້ງຢືນໄປຍັງໂຟນເດີໃນເຄື່ອງ',
|
||||
dir: 'ໂຟນເດີ',
|
||||
pushDirHelper: 'ໄຟລ໌ "fullchain.pem" ແລະ "privkey.pem" ຈະຖືກສ້າງໃນໂຟນເດີນີ້.',
|
||||
pushDirHelper:
|
||||
'ເລືອກຫຼາຍໂຟນເດີ ຫຼື ປ້ອນໜຶ່ງໂຟນເດີຕໍ່ແຖວ. ໄຟລ໌ "fullchain.pem" ແລະ "privkey.pem" ຈະຖືກສ້າງໃນແຕ່ລະໂຟນເດີ.',
|
||||
organizationDetail: 'ລາຍລະອຽດອົງກອນ',
|
||||
fromWebsite: 'ຈາກເວັບໄຊ',
|
||||
dnsMauanlHelper: 'ໃນໂໝດຈັດການ DNS ດ້ວຍຕົນເອງ, ທ່ານຕ້ອງຄລິກປຸ່ມ "ຂໍໃບຢັ້ງຢືນ" ຫຼັງຈາກສ້າງເພື່ອຮັບຄ່າ DNS',
|
||||
|
||||
@@ -499,6 +499,7 @@ const message = {
|
||||
recommend: 'Disyorkan',
|
||||
dir: 'Direktori',
|
||||
alias: 'Alias',
|
||||
quickDirDelete: 'Sahkan pemadaman ({0})?',
|
||||
quickDir: 'Akses Pantas',
|
||||
minQuickJump: 'Tambah sekurang-kurangnya satu entri akses pantas',
|
||||
maxQuickJump: 'Anda boleh menambah sehingga empat entri akses pantas',
|
||||
@@ -781,6 +782,15 @@ const message = {
|
||||
},
|
||||
aiTools: {
|
||||
agents: {
|
||||
deepSeekAccessHostHelper:
|
||||
'Masukkan alamat IPv4 atau nama hos yang digunakan dalam pelayar, tanpa protokol, port atau laluan. HTTPS menggunakan sijil setempat aplikasi.',
|
||||
deepSeekAuthHelper:
|
||||
'Nama pengguna: huruf, nombor, titik, garis bawah atau sempang. Kata laluan: sekurang-kurangnya 12 aksara, tanpa ruang di awal/akhir atau baris baharu.',
|
||||
octopAuthHelper:
|
||||
'Kata laluan: sekurang-kurangnya 8 aksara dengan huruf dan nombor; elakkan kata laluan lazim. Jika ditolak, semak arahan persediaan dalam log aplikasi.',
|
||||
initialCredentials: 'Maklumat log masuk awal; gunakan kata laluan semasa jika telah diubah dalam aplikasi.',
|
||||
nativeAgentAccessUnavailable:
|
||||
'Pemasangan sedia ada tiada alamat atau port HTTPS yang serasi. Semak tetapan aplikasi.',
|
||||
agent: 'Agen',
|
||||
account: 'Akaun model',
|
||||
batchOperation: 'Operasi pukal',
|
||||
@@ -1496,16 +1506,13 @@ const message = {
|
||||
statusWaiting: 'Menunggu',
|
||||
base: 'Maklumat Asas',
|
||||
backend: 'Backend',
|
||||
baseUrl: 'URL Asas',
|
||||
endpoint: 'Endpoint',
|
||||
tokenizer: 'Tokenizer',
|
||||
tokenizerPlaceholder:
|
||||
'Pilih atau masukkan direktori tokenizer tempatan, cth. /opt/1panel/tokenizers/DeepSeek-V3',
|
||||
tokenizerPlaceholder: 'Masukkan laluan direktori mutlak pada nod utama atau ID repositori Hugging Face',
|
||||
runConfig: 'Konfigurasi Jalan',
|
||||
rawResult: 'Keputusan Mentah',
|
||||
resultMetrics: 'Metrik Keputusan',
|
||||
contextTokens: 'Panjang Konteks',
|
||||
contextTokensHelper: 'Had token input ditambah had token output untuk penanda aras ini.',
|
||||
outputThroughput: 'Throughput Output',
|
||||
outputThroughputHelper: 'Token output dijana setiap saat. Lebih tinggi bermaksud penjanaan lebih pantas.',
|
||||
totalThroughput: 'Jumlah Throughput',
|
||||
@@ -1514,20 +1521,19 @@ const message = {
|
||||
firstTokenLatency: 'Latensi Token Pertama',
|
||||
firstTokenLatencyHelper:
|
||||
'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',
|
||||
concurrency: 'Keserentakan',
|
||||
mean: 'Min',
|
||||
median: 'Median',
|
||||
requestRate: 'Kadar Permintaan',
|
||||
requestRateUnlimited: 'Tanpa had (throughput maksimum)',
|
||||
requestRateCustom: 'QPS tersuai',
|
||||
requestRateCustomPlaceholder: 'Permintaan sesaat, cth. 2.5',
|
||||
successfulRequests: 'Permintaan berjaya',
|
||||
requestThroughput: 'Throughput Permintaan',
|
||||
timeout: 'Tamat Masa (saat)',
|
||||
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',
|
||||
},
|
||||
vllmMonitor: {
|
||||
@@ -1741,6 +1747,157 @@ const message = {
|
||||
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: {
|
||||
createByCommand: 'Cipta melalui perintah',
|
||||
importContainerBackupTip: 'Import fail sandaran kontena, hanya format .tar.gz disokong',
|
||||
@@ -3013,6 +3170,20 @@ const message = {
|
||||
remote: 'SSH jauh',
|
||||
},
|
||||
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',
|
||||
user: 'Pengguna panel',
|
||||
userChange: 'Tukar pengguna panel',
|
||||
@@ -3750,6 +3921,7 @@ const message = {
|
||||
link: 'Pautan',
|
||||
showCurrentArch: 'Aplikasi seni bina pelayan semasa',
|
||||
syncLocalApp: 'Segerakkan Aplikasi Tempatan',
|
||||
localAppSyncToNodes: 'Sertakan nod anak semasa menyegerakkan aplikasi tempatan',
|
||||
memoryRequiredHelper: 'Aplikasi semasa memerlukan {0} memori',
|
||||
gpuConfig: 'Aktifkan Sokongan GPU',
|
||||
gpuConfigHelper:
|
||||
@@ -4315,7 +4487,8 @@ const message = {
|
||||
domainHelper: 'Satu nama domain setiap baris, menyokong * dan alamat IP',
|
||||
pushDir: 'Tolakkan sijil ke direktori tempatan',
|
||||
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',
|
||||
fromWebsite: 'Daripada laman web',
|
||||
dnsMauanlHelper:
|
||||
|
||||
@@ -492,6 +492,7 @@ const message = {
|
||||
recommend: 'Recomendado',
|
||||
dir: 'Diretório',
|
||||
alias: 'Apelido',
|
||||
quickDirDelete: 'Confirmar a exclusão de ({0})?',
|
||||
quickDir: 'Acesso rápido',
|
||||
minQuickJump: 'Adicione pelo menos uma entrada de acesso rápido',
|
||||
maxQuickJump: 'Você pode adicionar até quatro entradas de acesso rápido',
|
||||
@@ -774,6 +775,15 @@ const message = {
|
||||
},
|
||||
aiTools: {
|
||||
agents: {
|
||||
deepSeekAccessHostHelper:
|
||||
'Informe o IPv4 ou nome do host usado no navegador, sem protocolo, porta ou caminho. O HTTPS usa o certificado local do aplicativo.',
|
||||
deepSeekAuthHelper:
|
||||
'Usuário: letras, números, ponto, sublinhado ou hífen. Senha: no mínimo 12 caracteres, sem espaços nas extremidades ou quebras de linha.',
|
||||
octopAuthHelper:
|
||||
'Senha: no mínimo 8 caracteres com letras e números; evite senhas comuns. Se rejeitada na inicialização, consulte as instruções nos logs do aplicativo.',
|
||||
initialCredentials: 'Credenciais iniciais; se alteradas no aplicativo, use a senha atual.',
|
||||
nativeAgentAccessUnavailable:
|
||||
'A instalação existente não tem endereço ou porta HTTPS compatível. Verifique as configurações do aplicativo.',
|
||||
agent: 'Agente',
|
||||
account: 'Conta de modelo',
|
||||
batchOperation: 'Operação em lote',
|
||||
@@ -1492,16 +1502,14 @@ const message = {
|
||||
statusWaiting: 'Aguardando',
|
||||
base: 'Informações básicas',
|
||||
backend: 'Backend',
|
||||
baseUrl: 'URL base',
|
||||
endpoint: 'Endpoint',
|
||||
tokenizer: 'Tokenizer',
|
||||
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',
|
||||
rawResult: 'Resultado bruto',
|
||||
resultMetrics: 'Métricas de resultado',
|
||||
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',
|
||||
outputThroughputHelper: 'Tokens de saída gerados por segundo. Quanto maior, mais rápida a geração.',
|
||||
totalThroughput: 'Throughput total',
|
||||
@@ -1510,20 +1518,19 @@ const message = {
|
||||
firstTokenLatency: 'Latência do primeiro token',
|
||||
firstTokenLatencyHelper:
|
||||
'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',
|
||||
concurrency: 'Concorrência',
|
||||
mean: 'Média',
|
||||
median: 'Mediana',
|
||||
requestRate: 'Taxa de requisições',
|
||||
requestRateUnlimited: 'Ilimitado (throughput máximo)',
|
||||
requestRateCustom: 'QPS personalizado',
|
||||
requestRateCustomPlaceholder: 'Requisições por segundo, ex.: 2.5',
|
||||
successfulRequests: 'Requisições bem-sucedidas',
|
||||
requestThroughput: 'Throughput de requisições',
|
||||
timeout: 'Timeout (segundos)',
|
||||
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',
|
||||
},
|
||||
vllmMonitor: {
|
||||
@@ -1740,6 +1747,155 @@ const message = {
|
||||
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: {
|
||||
createByCommand: 'Criar por comando',
|
||||
importContainerBackupTip: 'Importar arquivo de backup de contêiner, apenas .tar.gz é suportado',
|
||||
@@ -3012,6 +3168,20 @@ const message = {
|
||||
remote: 'SSH remoto',
|
||||
},
|
||||
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',
|
||||
user: 'Usuário do painel',
|
||||
userChange: 'Alterar usuário do painel',
|
||||
@@ -3766,6 +3936,7 @@ const message = {
|
||||
link: 'Link',
|
||||
showCurrentArch: 'Aplicações da arquitetura atual do servidor',
|
||||
syncLocalApp: 'Sincronizar Aplicativo Local',
|
||||
localAppSyncToNodes: 'Incluir nós filhos ao sincronizar apps locais',
|
||||
memoryRequiredHelper: 'O aplicativo atual requer {0} de memória',
|
||||
gpuConfig: 'Ativar Suporte a GPU',
|
||||
gpuConfigHelper:
|
||||
@@ -4335,7 +4506,7 @@ const message = {
|
||||
pushDir: 'Enviar o certificado para o diretório local',
|
||||
dir: 'Diretório',
|
||||
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',
|
||||
fromWebsite: 'Do site',
|
||||
dnsMauanlHelper:
|
||||
|
||||
@@ -491,6 +491,7 @@ const message = {
|
||||
recommend: 'Рекомендуемое',
|
||||
dir: 'Каталог',
|
||||
alias: 'Псевдоним',
|
||||
quickDirDelete: 'Подтвердить удаление ({0})?',
|
||||
quickDir: 'Быстрый доступ',
|
||||
minQuickJump: 'Добавьте хотя бы одну запись быстрого доступа',
|
||||
maxQuickJump: 'Вы можете добавить до четырех записей быстрого доступа',
|
||||
@@ -772,6 +773,15 @@ const message = {
|
||||
},
|
||||
aiTools: {
|
||||
agents: {
|
||||
deepSeekAccessHostHelper:
|
||||
'Введите IPv4-адрес или имя хоста, используемое в браузере, без протокола, порта и пути. HTTPS использует локальный сертификат приложения.',
|
||||
deepSeekAuthHelper:
|
||||
'Имя пользователя: латинские буквы, цифры, точка, подчёркивание или дефис. Пароль: минимум 12 символов, без пробелов по краям и переводов строк.',
|
||||
octopAuthHelper:
|
||||
'Пароль: минимум 8 символов с буквами и цифрами; избегайте распространённых паролей. Если пароль отклонён, инструкции доступны в журнале приложения.',
|
||||
initialCredentials: 'Начальные учётные данные; если пароль изменён в приложении, используйте текущий.',
|
||||
nativeAgentAccessUnavailable:
|
||||
'У существующей установки нет совместимого HTTPS-адреса или порта. Проверьте настройки приложения.',
|
||||
agent: 'Агент',
|
||||
account: 'Аккаунт модели',
|
||||
batchOperation: 'Пакетная операция',
|
||||
@@ -1484,16 +1494,13 @@ const message = {
|
||||
statusWaiting: 'Ожидание',
|
||||
base: 'Основная информация',
|
||||
backend: 'Backend',
|
||||
baseUrl: 'Base URL',
|
||||
endpoint: 'Endpoint',
|
||||
tokenizer: 'Токенизатор',
|
||||
tokenizerPlaceholder:
|
||||
'Выберите или введите локальный каталог токенизатора, например /opt/1panel/tokenizers/DeepSeek-V3',
|
||||
tokenizerPlaceholder: 'Введите абсолютный путь к каталогу на главном узле или ID репозитория Hugging Face',
|
||||
runConfig: 'Конфигурация запуска',
|
||||
rawResult: 'Исходный результат',
|
||||
resultMetrics: 'Метрики результата',
|
||||
contextTokens: 'Длина контекста',
|
||||
contextTokensHelper: 'Лимит входных токенов плюс лимит выходных токенов для этого бенчмарка.',
|
||||
outputThroughput: 'Выходная пропускная способность',
|
||||
outputThroughputHelper:
|
||||
'Выходные токены, генерируемые в секунду. Чем выше значение, тем быстрее генерация.',
|
||||
@@ -1503,20 +1510,19 @@ const message = {
|
||||
firstTokenLatency: 'Задержка первого токена',
|
||||
firstTokenLatencyHelper:
|
||||
'Время от отправки запроса до получения первого токена. Чем ниже значение, тем быстрее ответ.',
|
||||
tokenValueHelper: 'Введите положительное целое число или значение с k, например 512, 1k, 32k',
|
||||
numPrompts: 'Промпты',
|
||||
concurrency: 'Параллелизм',
|
||||
mean: 'Среднее',
|
||||
median: 'Медиана',
|
||||
requestRate: 'Частота запросов',
|
||||
requestRateUnlimited: 'Без ограничений (макс. пропускная способность)',
|
||||
requestRateCustom: 'Пользовательский QPS',
|
||||
requestRateCustomPlaceholder: 'Запросов в секунду, например 2.5',
|
||||
successfulRequests: 'Успешные запросы',
|
||||
requestThroughput: 'Пропускная способность запросов',
|
||||
timeout: 'Тайм-аут (секунды)',
|
||||
image: 'Образ vLLM',
|
||||
ignoreEos: 'Игнорировать EOS',
|
||||
ignoreEos: 'Игнорировать токен конца последовательности (EOS)',
|
||||
ignoreEosHelp:
|
||||
'Предотвращает раннюю остановку по EOS для тестирования заданной длины вывода. Другие условия остановки действуют; требуется поддержка сервера.',
|
||||
extraHeaders: 'Дополнительные заголовки',
|
||||
},
|
||||
vllmMonitor: {
|
||||
@@ -1730,6 +1736,161 @@ const message = {
|
||||
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: {
|
||||
createByCommand: 'Создать с помощью команды',
|
||||
importContainerBackupTip: 'Импорт файла резервной копии контейнера, поддерживается только .tar.gz',
|
||||
@@ -2985,6 +3146,20 @@ const message = {
|
||||
remote: 'Удаленный SSH',
|
||||
},
|
||||
setting: {
|
||||
panelCache: {
|
||||
title: 'Настройки кэша',
|
||||
tabs: 'Кэш вкладок',
|
||||
pagination: 'Кэш пагинации',
|
||||
refresh: 'Кэш интервала обновления',
|
||||
search: 'Кэш поискового запроса',
|
||||
duration: 'Срок хранения кэша',
|
||||
'24h': '24 часа',
|
||||
week: 'Одна неделя',
|
||||
month: 'Один месяц',
|
||||
forever: 'Бессрочно',
|
||||
helper: 'Срок хранения отсчитывается с последнего изменения состояния. После его истечения восстанавливаются значения по умолчанию.',
|
||||
},
|
||||
|
||||
panel: 'Панель',
|
||||
user: 'Пользователь панели',
|
||||
userChange: 'Изменить пользователя панели',
|
||||
@@ -3735,6 +3910,7 @@ const message = {
|
||||
link: 'Ссылка',
|
||||
showCurrentArch: 'Приложения для текущей архитектуры сервера',
|
||||
syncLocalApp: 'Синхронизировать локальное приложение',
|
||||
localAppSyncToNodes: 'Включать дочерние узлы при синхронизации локальных приложений',
|
||||
memoryRequiredHelper: 'Текущее приложение требует {0} памяти',
|
||||
gpuConfig: 'Включить поддержку GPU',
|
||||
gpuConfigHelper:
|
||||
@@ -4302,7 +4478,8 @@ const message = {
|
||||
domainHelper: 'Одно доменное имя в строке, поддерживает * и IP-адрес',
|
||||
pushDir: 'Отправить сертификат в локальную директорию',
|
||||
dir: 'Директория',
|
||||
pushDirHelper: 'В этой директории будут созданы файл сертификата "fullchain.pem" и файл ключа "privkey.pem".',
|
||||
pushDirHelper:
|
||||
'Выберите несколько директорий или укажите по одной директории в строке. В каждой директории будут созданы файлы "fullchain.pem" и "privkey.pem".',
|
||||
organizationDetail: 'Детали организации',
|
||||
fromWebsite: 'С веб-сайта',
|
||||
dnsMauanlHelper:
|
||||
|
||||
@@ -495,6 +495,7 @@ const message = {
|
||||
recommend: 'Önerilen',
|
||||
dir: 'Dizin',
|
||||
alias: 'Takma Ad',
|
||||
quickDirDelete: '({0}) silinsin mi?',
|
||||
quickDir: 'Hızlı Erişim',
|
||||
minQuickJump: 'En az bir hızlı erişim girdisi ekleyin',
|
||||
maxQuickJump: 'En fazla dört hızlı erişim girdisi ekleyebilirsiniz',
|
||||
@@ -777,6 +778,15 @@ const message = {
|
||||
},
|
||||
aiTools: {
|
||||
agents: {
|
||||
deepSeekAccessHostHelper:
|
||||
'Tarayıcıda kullanılan IPv4 adresini veya ana bilgisayar adını protokol, bağlantı noktası veya yol olmadan girin. HTTPS, uygulamanın yerel sertifikasını kullanır.',
|
||||
deepSeekAuthHelper:
|
||||
'Kullanıcı adı: harf, rakam, nokta, alt çizgi veya tire. Parola: en az 12 karakter; başta/sonda boşluk veya satır sonu içeremez.',
|
||||
octopAuthHelper:
|
||||
'Parola: harf ve rakam içeren en az 8 karakter; yaygın parolalardan kaçının. Reddedilirse kurulum yönergeleri için uygulama günlüklerini kontrol edin.',
|
||||
initialCredentials: 'İlk giriş bilgileri; uygulamada değiştirildiyse güncel parolayı kullanın.',
|
||||
nativeAgentAccessUnavailable:
|
||||
'Mevcut kurulumda uyumlu HTTPS erişim adresi veya bağlantı noktası yok. Uygulama ayarlarını kontrol edin.',
|
||||
agent: 'Ajan',
|
||||
account: 'Model hesabı',
|
||||
batchOperation: 'Toplu işlem',
|
||||
@@ -1492,15 +1502,13 @@ const message = {
|
||||
statusWaiting: 'Bekliyor',
|
||||
base: 'Temel Bilgiler',
|
||||
backend: 'Backend',
|
||||
baseUrl: 'Base URL',
|
||||
endpoint: 'Endpoint',
|
||||
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ı',
|
||||
rawResult: 'Ham Sonuç',
|
||||
resultMetrics: 'Sonuç Metrikleri',
|
||||
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',
|
||||
outputThroughputHelper:
|
||||
'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',
|
||||
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.',
|
||||
tokenValueHelper: 'Pozitif tam sayı veya k değeri girin, örn. 512, 1k, 32k',
|
||||
numPrompts: 'Prompt',
|
||||
concurrency: 'Eşzamanlılık',
|
||||
mean: 'Ortalama',
|
||||
median: 'Medyan',
|
||||
requestRate: 'İstek Hızı',
|
||||
requestRateUnlimited: 'Sınırsız (maksimum throughput)',
|
||||
requestRateCustom: 'Özel QPS',
|
||||
requestRateCustomPlaceholder: 'Saniye başına istek, örn. 2.5',
|
||||
successfulRequests: 'Başarılı istekler',
|
||||
requestThroughput: 'İstek Throughput',
|
||||
timeout: 'Zaman aşımı (saniye)',
|
||||
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',
|
||||
},
|
||||
vllmMonitor: {
|
||||
@@ -1737,6 +1744,157 @@ const message = {
|
||||
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: {
|
||||
createByCommand: 'Komutla oluştur',
|
||||
importContainerBackupTip: 'Konteyner yedek dosyasını içe aktar, yalnızca .tar.gz desteklenir',
|
||||
@@ -3003,6 +3161,20 @@ const message = {
|
||||
remote: 'Uzak SSH',
|
||||
},
|
||||
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',
|
||||
user: 'Panel kullanıcısı',
|
||||
userChange: 'Panel kullanıcısını değiştir',
|
||||
@@ -3751,6 +3923,7 @@ const message = {
|
||||
link: 'Bağlantı',
|
||||
showCurrentArch: 'Mimari',
|
||||
syncLocalApp: 'Yerel Uygulamayı Senkronize Et',
|
||||
localAppSyncToNodes: 'Yerel uygulama eşitlemesine alt düğümleri dahil et',
|
||||
memoryRequiredHelper: 'Geçerli uygulama bellek gereksinimi {0}',
|
||||
gpuConfig: 'GPU Desteğini Etkinleştir',
|
||||
gpuConfigHelper:
|
||||
@@ -4321,7 +4494,7 @@ const message = {
|
||||
pushDir: 'Sertifikayı yerel dizine aktar',
|
||||
dir: 'Dizin',
|
||||
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ı',
|
||||
fromWebsite: 'Web sitesinden',
|
||||
dnsMauanlHelper:
|
||||
|
||||
@@ -473,6 +473,7 @@ const message = {
|
||||
recommend: '推薦',
|
||||
dir: '目錄',
|
||||
alias: '別名',
|
||||
quickDirDelete: '確定刪除({0})',
|
||||
quickDir: '快捷目錄',
|
||||
minQuickJump: '請至少設定一個快速跳轉入口',
|
||||
maxQuickJump: '最多可設定四個快速跳轉入口',
|
||||
@@ -739,6 +740,13 @@ const message = {
|
||||
},
|
||||
aiTools: {
|
||||
agents: {
|
||||
deepSeekAccessHostHelper:
|
||||
'填寫瀏覽器實際存取的 IPv4 位址或主機名稱,不含協定、連接埠或路徑。HTTPS 使用應用程式的本機憑證。',
|
||||
deepSeekAuthHelper: '使用者名稱僅支援字母、數字、點、底線及連字號;密碼至少 12 位,不含前後空格或換行。',
|
||||
octopAuthHelper:
|
||||
'密碼至少 8 位且包含字母及數字,避免常見弱密碼。若初始化拒絕該密碼,請查看應用程式日誌中的設定說明。',
|
||||
initialCredentials: '初始登入資訊;若已在應用程式內修改,以目前密碼為準。',
|
||||
nativeAgentAccessUnavailable: '現有安裝缺少相容的 HTTPS 存取位址或連接埠,請檢查應用程式設定。',
|
||||
agent: 'Agent',
|
||||
account: '模型帳號',
|
||||
batchOperation: '批次操作',
|
||||
@@ -1405,35 +1413,31 @@ const message = {
|
||||
statusWaiting: '等待中',
|
||||
base: '基礎資訊',
|
||||
backend: '後端',
|
||||
baseUrl: '服務地址',
|
||||
endpoint: '介面路徑',
|
||||
tokenizer: '分詞器',
|
||||
tokenizerPlaceholder: '請選擇或輸入本機分詞器目錄,例如 /opt/1panel/tokenizers/DeepSeek-V3',
|
||||
tokenizerPlaceholder: '請輸入主節點上的絕對目錄或 Hugging Face 儲存庫 ID',
|
||||
runConfig: '執行設定',
|
||||
rawResult: '原始結果',
|
||||
resultMetrics: '結果指標',
|
||||
contextTokens: '上下文長度',
|
||||
contextTokensHelper: '本次測試的輸入 Token 與輸出 Token 上限之和。',
|
||||
outputThroughput: '輸出吞吐',
|
||||
outputThroughputHelper: '模型每秒產生的輸出 Token 數,越高表示生成速度越快。',
|
||||
totalThroughput: '總吞吐',
|
||||
totalThroughputHelper: '每秒處理的輸入與輸出 Token 總數,用於衡量整體處理能力。',
|
||||
firstTokenLatency: '首 Token 延遲',
|
||||
firstTokenLatencyHelper: '從請求送出到收到第一個 Token 的時間,越低表示回應越快。',
|
||||
tokenValueHelper: '請輸入正整數或 k 單位,例如 512、1k、32k',
|
||||
numPrompts: '請求數量',
|
||||
concurrency: '併發數',
|
||||
mean: '平均值',
|
||||
median: '中位數',
|
||||
requestRate: '請求速率',
|
||||
requestRateUnlimited: '不限速(最大吞吐)',
|
||||
requestRateCustom: '自訂 QPS',
|
||||
requestRateCustomPlaceholder: '請輸入每秒請求數,如 2.5',
|
||||
successfulRequests: '成功請求',
|
||||
requestThroughput: '請求吞吐',
|
||||
timeout: '逾時時間(秒)',
|
||||
image: 'vLLM 映像',
|
||||
ignoreEos: '忽略 EOS',
|
||||
ignoreEos: '忽略結束標記(EOS)',
|
||||
ignoreEosHelp: '避免因結束標記提前停止,便於依設定輸出長度壓測;其他停止條件仍有效,需服務支援。',
|
||||
extraHeaders: '額外請求標頭',
|
||||
},
|
||||
vllmMonitor: {
|
||||
@@ -1637,6 +1641,140 @@ const message = {
|
||||
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: {
|
||||
createByCommand: '指令建立',
|
||||
importContainerBackupTip: '匯入容器備份檔案,僅支援 .tar.gz 格式',
|
||||
@@ -2794,6 +2932,20 @@ const message = {
|
||||
stopSSHWarn: '是否斷開此SSH連接',
|
||||
},
|
||||
setting: {
|
||||
panelCache: {
|
||||
title: '快取設定',
|
||||
tabs: '標籤頁快取',
|
||||
pagination: '分頁快取',
|
||||
refresh: '重新整理頻率快取',
|
||||
search: '搜尋輸入框快取',
|
||||
duration: '快取時間',
|
||||
'24h': '24h',
|
||||
week: '一週',
|
||||
month: '一個月',
|
||||
forever: '永久',
|
||||
helper: '有效期從狀態最後一次修改開始計算;過期後恢復預設值。',
|
||||
},
|
||||
|
||||
panel: '面板',
|
||||
user: '面板使用者',
|
||||
userChange: '修改面板使用者',
|
||||
@@ -3451,6 +3603,7 @@ const message = {
|
||||
link: '連結',
|
||||
showCurrentArch: '目前伺服器架構應用',
|
||||
syncLocalApp: '同步本機應用',
|
||||
localAppSyncToNodes: '同步本機應用時聯動子節點',
|
||||
memoryRequiredHelper: '目前應用記憶體需求 {0}',
|
||||
gpuConfig: '開啟 GPU 支援',
|
||||
gpuConfigHelper: '請確保機器有 NVIDIA GPU 並且安裝 NVIDIA 驅動 和 NVIDIA docker Container Toolkit',
|
||||
@@ -3970,7 +4123,7 @@ const message = {
|
||||
domainHelper: '一行一個網域名稱,支援*和IP位址',
|
||||
pushDir: '推送憑證到本機目錄',
|
||||
dir: '目錄',
|
||||
pushDirHelper: '會在此目錄下產生兩個檔案,憑證檔案:fullchain.pem 金鑰檔案:privkey.pem',
|
||||
pushDirHelper: '支援選擇多個目錄,也可每行填寫一個目錄。每個目錄下都會產生 fullchain.pem 和 privkey.pem。',
|
||||
organizationDetail: '機構詳情',
|
||||
fromWebsite: '從網站取得',
|
||||
dnsMauanlHelper: '手動解析模式需要在建立完之後點選申請按鈕取得 DNS 解析值',
|
||||
|
||||
@@ -473,6 +473,7 @@ const message = {
|
||||
recommend: '推荐',
|
||||
dir: '目录',
|
||||
alias: '别名',
|
||||
quickDirDelete: '确定删除({0})',
|
||||
quickDir: '快捷目录',
|
||||
minQuickJump: '请至少设置一个快速跳转入口!',
|
||||
maxQuickJump: '最多可设置四个快速跳转入口!',
|
||||
@@ -756,6 +757,13 @@ const message = {
|
||||
},
|
||||
aiTools: {
|
||||
agents: {
|
||||
deepSeekAccessHostHelper:
|
||||
'填写浏览器实际访问的 IPv4 地址或主机名,不含协议、端口或路径。HTTPS 使用应用的本地证书。',
|
||||
deepSeekAuthHelper: '用户名仅支持字母、数字、点、下划线和连字符;密码至少 12 位,不含首尾空格或换行。',
|
||||
octopAuthHelper:
|
||||
'密码至少 8 位且包含字母和数字,避免常见弱密码。若初始化拒绝该密码,请查看应用日志中的设置说明。',
|
||||
initialCredentials: '初始登录信息;若已在应用内修改,以当前密码为准。',
|
||||
nativeAgentAccessUnavailable: '现有安装缺少兼容的 HTTPS 访问地址或端口,请检查应用设置。',
|
||||
agent: '智能体',
|
||||
account: '模型账号',
|
||||
batchOperation: '批量操作',
|
||||
@@ -1418,35 +1426,31 @@ const message = {
|
||||
statusWaiting: '等待中',
|
||||
base: '基础信息',
|
||||
backend: '后端',
|
||||
baseUrl: '服务地址',
|
||||
endpoint: '接口路径',
|
||||
tokenizer: '分词器',
|
||||
tokenizerPlaceholder: '请选择或输入本地分词器目录,例如 /opt/1panel/tokenizers/DeepSeek-V3',
|
||||
tokenizerPlaceholder: '请输入主节点上的绝对目录或 Hugging Face 仓库 ID',
|
||||
runConfig: '运行配置',
|
||||
rawResult: '原始结果',
|
||||
resultMetrics: '结果指标',
|
||||
contextTokens: '上下文长度',
|
||||
contextTokensHelper: '本次测试的输入 Token 与输出 Token 上限之和。',
|
||||
outputThroughput: '输出吞吐',
|
||||
outputThroughputHelper: '模型每秒生成的输出 Token 数,越高表示生成速度越快。',
|
||||
totalThroughput: '总吞吐',
|
||||
totalThroughputHelper: '每秒处理的输入与输出 Token 总数,用于衡量整体处理能力。',
|
||||
firstTokenLatency: '首 Token 延迟',
|
||||
firstTokenLatencyHelper: '从请求发出到收到第一个 Token 的时间,越低表示响应越快。',
|
||||
tokenValueHelper: '请输入正整数或 k 单位,例如 512、1k、32k',
|
||||
numPrompts: '请求数量',
|
||||
concurrency: '并发数',
|
||||
requestRate: '请求速率',
|
||||
requestRateUnlimited: '不限速(最大吞吐)',
|
||||
requestRateCustom: '自定义 QPS',
|
||||
requestRateCustomPlaceholder: '请输入每秒请求数,如 2.5',
|
||||
successfulRequests: '成功请求',
|
||||
requestThroughput: '请求吞吐',
|
||||
mean: '平均',
|
||||
median: '中位数',
|
||||
timeout: '超时时间(秒)',
|
||||
image: 'vLLM 镜像',
|
||||
ignoreEos: '忽略 EOS',
|
||||
ignoreEos: '忽略结束标记(EOS)',
|
||||
ignoreEosHelp: '避免因结束标记提前停止,便于按设定输出长度压测;其他停止条件仍有效,需服务支持。',
|
||||
extraHeaders: '额外请求头',
|
||||
},
|
||||
vllmMonitor: {
|
||||
@@ -1651,6 +1655,140 @@ const message = {
|
||||
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: {
|
||||
createByCommand: '命令创建',
|
||||
importContainerBackupTip: '导入容器备份文件,仅支持 .tar.gz 格式',
|
||||
@@ -2825,6 +2963,20 @@ const message = {
|
||||
stopSSHWarn: '是否断开此SSH连接',
|
||||
},
|
||||
setting: {
|
||||
panelCache: {
|
||||
title: '缓存设置',
|
||||
tabs: '标签页缓存',
|
||||
pagination: '翻页缓存',
|
||||
refresh: '刷新频率缓存',
|
||||
search: '搜索输入框缓存',
|
||||
duration: '缓存时间',
|
||||
'24h': '24h',
|
||||
week: '一周',
|
||||
month: '一月',
|
||||
forever: '永久',
|
||||
helper: '有效期从状态最后一次修改开始计算;过期后恢复默认值。',
|
||||
},
|
||||
|
||||
panel: '面板',
|
||||
user: '面板用户',
|
||||
userChange: '修改面板用户',
|
||||
@@ -3492,6 +3644,7 @@ const message = {
|
||||
link: '链接',
|
||||
showCurrentArch: '本服务器架构应用',
|
||||
syncLocalApp: '同步本地应用',
|
||||
localAppSyncToNodes: '同步本地应用时联动子节点',
|
||||
memoryRequiredHelper: '当前应用内存需求 {0}',
|
||||
gpuConfig: '开启 GPU 支持',
|
||||
gpuConfigHelper: '请确保机器有 NVIDIA GPU 并且安装 NVIDIA 驱动 和 NVIDIA docker Container Toolkit',
|
||||
@@ -4015,7 +4168,8 @@ const message = {
|
||||
domainHelper: '一行一个域名,支持*和IP地址',
|
||||
pushDir: '推送证书到本地目录',
|
||||
dir: '目录',
|
||||
pushDirHelper: '会在此目录下生成两个文件,证书文件:fullchain.pem 密钥文件:privkey.pem',
|
||||
pushDirHelper:
|
||||
'支持选择多个目录,也可每行填写一个目录。每个目录下都会生成证书文件 fullchain.pem 和密钥文件 privkey.pem',
|
||||
organizationDetail: '机构详情',
|
||||
fromWebsite: '从网站中获取',
|
||||
dnsMauanlHelper: '手动解析模式需要在创建完之后点击申请按钮获取 DNS 解析值',
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { applyPanelCacheSettings } from '@/utils/panel-cache';
|
||||
import { ref, computed, onBeforeUnmount, onMounted, watch } from 'vue';
|
||||
import { RouteRecordRaw, useRoute, useRouter } from 'vue-router';
|
||||
import { loadingSvg } from '@/utils/svg';
|
||||
@@ -141,6 +142,7 @@ const search = async () => {
|
||||
settingInfo = res.data;
|
||||
version.value = res.data.systemVersion;
|
||||
menuAccordion.value = res.data.menuAccordion === 'Enable';
|
||||
applyPanelCacheSettings(res.data.panelCache);
|
||||
} catch (error) {
|
||||
version.value = '';
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { prunePanelCache } from '@/utils/panel-cache';
|
||||
import { onMounted, computed, ref, watch, onBeforeUnmount } from 'vue';
|
||||
import { Sidebar, Footer, AppMain, MobileHeader, Tabs } from './components';
|
||||
import TerminalHost from '@/components/terminal/host.vue';
|
||||
@@ -106,6 +107,7 @@ const loadingText = computed(() =>
|
||||
);
|
||||
|
||||
let timer: NodeJS.Timer | null = null;
|
||||
let cacheCleanupTimer: ReturnType<typeof setInterval> | undefined;
|
||||
|
||||
const classObj = computed(() => {
|
||||
return {
|
||||
@@ -186,10 +188,12 @@ const loadStatus = async () => {
|
||||
}
|
||||
};
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(cacheCleanupTimer);
|
||||
clearInterval(Number(timer));
|
||||
timer = null;
|
||||
});
|
||||
onMounted(() => {
|
||||
cacheCleanupTimer = setInterval(() => prunePanelCache(), 300000);
|
||||
if (openMenuTabs.value && !tabsStore.activeTabPath) {
|
||||
handleMenuClick('/');
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { panelCacheKey, readPanelCache, writePanelCache, removePanelCache, prunePanelCache } from '@/utils/panel-cache';
|
||||
import router from '@/routers/router';
|
||||
import NProgress from '@/config/nprogress';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
@@ -102,29 +103,34 @@ router.beforeEach(async (to, from) => {
|
||||
if (to.name === 'Expired') {
|
||||
return true;
|
||||
}
|
||||
const activeMenuKey = 'cachedRoute' + (to.meta.activeMenu || '');
|
||||
const activeMenuKey = panelCacheKey(String(to.meta.activeMenu || ''));
|
||||
if (to.query.uncached != undefined) {
|
||||
const query = { ...to.query };
|
||||
delete query.uncached;
|
||||
localStorage.removeItem(activeMenuKey);
|
||||
removePanelCache('tabs', activeMenuKey);
|
||||
return { path: to.path, query };
|
||||
}
|
||||
|
||||
const cachedRoute = localStorage.getItem(activeMenuKey);
|
||||
const cachedRoute = readPanelCache<string>('tabs', activeMenuKey);
|
||||
if (
|
||||
to.meta.activeMenu &&
|
||||
to.meta.activeMenu != from.meta.activeMenu &&
|
||||
typeof cachedRoute === 'string' &&
|
||||
cachedRoute &&
|
||||
cachedRoute !== to.path &&
|
||||
!isRedirecting
|
||||
) {
|
||||
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;
|
||||
NProgress.done();
|
||||
return cachedRoute;
|
||||
}
|
||||
localStorage.removeItem(activeMenuKey);
|
||||
removePanelCache('tabs', activeMenuKey);
|
||||
}
|
||||
|
||||
if (!hasRouteAccess(to)) {
|
||||
@@ -135,7 +141,13 @@ router.beforeEach(async (to, from) => {
|
||||
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) {
|
||||
let notMathParam = true;
|
||||
if (to.matched.some((record) => record.path.includes(':'))) {
|
||||
@@ -143,13 +155,13 @@ router.afterEach((to) => {
|
||||
}
|
||||
if (notMathParam) {
|
||||
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') {
|
||||
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') {
|
||||
localStorage.setItem('cachedRoute' + to.meta.activeMenu, '/toolbox/clam');
|
||||
writePanelCache('tabs', panelCacheKey(String(to.meta.activeMenu)), '/toolbox/clam');
|
||||
} else {
|
||||
localStorage.setItem('cachedRoute' + to.meta.activeMenu, to.path);
|
||||
writePanelCache('tabs', panelCacheKey(String(to.meta.activeMenu)), to.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { RouteRecordRaw } from 'vue-router';
|
||||
import { DeviceType } from '@/enums/app';
|
||||
import type { PanelCacheSettings } from '@/utils/panel-cache';
|
||||
export interface ThemeConfigProp {
|
||||
panelName: string;
|
||||
primary: string;
|
||||
@@ -34,6 +35,8 @@ export interface GlobalState {
|
||||
isFullScreen: boolean;
|
||||
openMenuTabs: boolean;
|
||||
menuAccordion: boolean;
|
||||
panelCache: PanelCacheSettings;
|
||||
cacheUserID: string;
|
||||
watermark: Watermark | null;
|
||||
watermarkShow: boolean;
|
||||
isLoading: boolean;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { DeviceType } from '@/enums/app';
|
||||
import i18n, { setActiveLocale } from '@/lang';
|
||||
import { isMasterOnlyPermissionCode, setMasterOnlyPermissionCodes, toManageCode } from '@/utils/permission-codes';
|
||||
import { clearPageStateCache } from '@/utils/page-state-cache';
|
||||
import { defaultPanelCache } from '@/utils/panel-cache';
|
||||
|
||||
const CN_DOCS_URL = 'https://docs.fit2cloud.com/1panel';
|
||||
const INTL_DOCS_URL = 'https://1panel.pro/docs/v2';
|
||||
@@ -33,6 +34,8 @@ const GlobalStore = defineStore('GlobalState', {
|
||||
isFullScreen: false,
|
||||
openMenuTabs: false,
|
||||
menuAccordion: false,
|
||||
panelCache: { ...defaultPanelCache },
|
||||
cacheUserID: '',
|
||||
watermark: null,
|
||||
watermarkShow: false,
|
||||
isLoading: false,
|
||||
@@ -123,6 +126,7 @@ const GlobalStore = defineStore('GlobalState', {
|
||||
},
|
||||
clearAuthInfo() {
|
||||
clearPageStateCache();
|
||||
this.cacheUserID = '';
|
||||
this.permissions = [];
|
||||
this.masterOnlyPermissions = [];
|
||||
this.nodeRoles = [];
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { bindSearchReset } from '@/utils/search-reset';
|
||||
import router from '@/routers/router';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
import { bindSearchState } from '@/utils/search-reset';
|
||||
import { panelCacheKey } from '@/utils/panel-cache';
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, reactive, toRef } from 'vue';
|
||||
import { MsgError } from '@/utils/message';
|
||||
@@ -49,8 +48,7 @@ export const ProcessStore = defineStore('ProcessStore', () => {
|
||||
port: undefined,
|
||||
});
|
||||
|
||||
const { currentNode } = useGlobalStore();
|
||||
bindSearchReset([() => router.currentRoute.value.path, currentNode], {
|
||||
const searchState = bindSearchState(() => panelCacheKey('host/process'), {
|
||||
pid: toRef(psSearch, 'pid'),
|
||||
name: toRef(psSearch, 'name'),
|
||||
username: toRef(psSearch, 'username'),
|
||||
@@ -321,6 +319,7 @@ export const ProcessStore = defineStore('ProcessStore', () => {
|
||||
};
|
||||
|
||||
return {
|
||||
restoreSearchState: searchState.restore,
|
||||
isConnected,
|
||||
isConnecting,
|
||||
psData,
|
||||
|
||||
@@ -1,10 +1,52 @@
|
||||
import i18n from '@/lang';
|
||||
import { compareVersion } from '@/utils/version';
|
||||
import type { AI } from '@/api/interface/ai';
|
||||
|
||||
const openclawHTTPSVersion = '2026.3.13';
|
||||
const openclawHTTPVersion = '2026.3.23';
|
||||
const openclawDefaultAccessHost = '127.0.0.1';
|
||||
|
||||
export const isAgentType = (value: unknown): value is AI.AgentType =>
|
||||
typeof value === 'string' && ['openclaw', 'copaw', 'hermes-agent', 'deepseek-harness', 'octop'].includes(value);
|
||||
|
||||
export const isDeepSeekAccessHost = (value: string): boolean => {
|
||||
if (/^[0-9.]+$/.test(value)) {
|
||||
return (
|
||||
value.split('.').length === 4 &&
|
||||
value.split('.').every((part) => /^(0|[1-9]\d{0,2})$/.test(part) && +part <= 255)
|
||||
);
|
||||
}
|
||||
return (
|
||||
value.length > 0 &&
|
||||
value.length <= 253 &&
|
||||
value === value.trim() &&
|
||||
value.split('.').every((part) => /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/.test(part))
|
||||
);
|
||||
};
|
||||
|
||||
export const isOctopPassword = (value: string): boolean =>
|
||||
[...value].length >= 8 &&
|
||||
value === value.trim() &&
|
||||
!/[\r\n\0]/.test(value) &&
|
||||
/\p{L}/u.test(value) &&
|
||||
/\p{Nd}/u.test(value) &&
|
||||
![
|
||||
'password',
|
||||
'password1',
|
||||
'password12',
|
||||
'password123',
|
||||
'12345678',
|
||||
'123456789',
|
||||
'qwerty123',
|
||||
'admin123',
|
||||
'welcome1',
|
||||
'letmein1',
|
||||
'changeme1',
|
||||
'octop123',
|
||||
'abc12345',
|
||||
'iloveyou1',
|
||||
].includes(value.toLowerCase());
|
||||
|
||||
export const getAgentProviderDisplayName = (provider: string, displayName?: string): string => {
|
||||
if (provider === 'custom' || displayName === 'Custom') {
|
||||
return i18n.global.t('container.custom');
|
||||
@@ -27,7 +69,7 @@ export const supportsAgentToken = (agentType: string): boolean => {
|
||||
};
|
||||
|
||||
export const supportsAgentDashboardAuth = (agentType: string): boolean => {
|
||||
return agentType === 'hermes-agent' || agentType === 'copaw';
|
||||
return ['hermes-agent', 'copaw', 'deepseek-harness', 'octop'].includes(agentType);
|
||||
};
|
||||
|
||||
export const isOpenclawHTTPSWindowVersion = (version: string): boolean => {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -22,6 +22,7 @@ export const syncAuthInfo = async (currentNode?: string) => {
|
||||
const storeCurrentNode = globalStore.currentNode;
|
||||
if (!globalStore.isEnterprise) {
|
||||
const res = await getUserInfo();
|
||||
globalStore.cacheUserID = `community:${res.data.id}`;
|
||||
globalStore.setAuthInfo({
|
||||
isAdmin: res.data.role === 'ADMIN',
|
||||
permissions: res.data.permissions || [],
|
||||
@@ -31,6 +32,7 @@ export const syncAuthInfo = async (currentNode?: string) => {
|
||||
return res.data;
|
||||
}
|
||||
const res = await getEnterpriseUserInfo(currentNode ?? storeCurrentNode);
|
||||
globalStore.cacheUserID = `enterprise:${res.data.id}`;
|
||||
globalStore.setAuthInfo({
|
||||
isAdmin: res.data.role === 'ADMIN',
|
||||
permissions: res.data.permissions || [],
|
||||
|
||||
@@ -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>>;
|
||||
type SearchValues = Record<string, string | number | undefined>;
|
||||
|
||||
export const bindSearchReset = (sources: WatchSource[], fields: SearchFields) => {
|
||||
const defaults = Object.values(fields).map((field) => ({ field, value: field.value }));
|
||||
const reset = () => {
|
||||
for (const { field, value } of defaults) field.value = value;
|
||||
// Keep categories separate so changing search text does not renew pagination's lifetime.
|
||||
const searchStorage = {
|
||||
save(key: string, values: SearchValues) {
|
||||
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' });
|
||||
return reset;
|
||||
let restoring = false;
|
||||
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();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { applyPanelCacheSettings } from '@/utils/panel-cache';
|
||||
import {
|
||||
getLicenseStatus,
|
||||
getMasterLicenseStatus,
|
||||
@@ -86,6 +87,7 @@ const loadDataFromDB = async () => {
|
||||
globalStore.entrance = res.data.securityEntrance;
|
||||
globalStore.openMenuTabs = res.data.menuTabs === 'Enable';
|
||||
globalStore.menuAccordion = res.data.menuAccordion === 'Enable';
|
||||
applyPanelCacheSettings(res.data.panelCache);
|
||||
};
|
||||
|
||||
export async function loadProductProFromDB() {
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
<el-option label="OpenClaw" value="openclaw" />
|
||||
<el-option label="Hermes Agent" value="hermes-agent" />
|
||||
<el-option label="QwenPaw" value="copaw" />
|
||||
<el-option label="DeepSeek Harness" value="deepseek-harness" />
|
||||
<el-option label="Octop" value="octop" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('commons.table.name')" prop="name">
|
||||
@@ -19,10 +21,19 @@
|
||||
<el-select v-model="form.appVersion" filterable>
|
||||
<el-option v-for="item in versions" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
<span v-if="versions.length === 0" class="input-help">{{ $t('commons.msg.noneData') }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('aiTools.agents.webuiPort')" prop="webUIPort">
|
||||
<el-input-number v-model="form.webUIPort" :min="1" :max="65535" />
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="form.agentType === 'deepseek-harness'"
|
||||
:label="$t('setting.systemIP')"
|
||||
prop="accessHost"
|
||||
>
|
||||
<el-input v-model="form.accessHost" />
|
||||
<span class="input-help">{{ $t('aiTools.agents.deepSeekAccessHostHelper') }}</span>
|
||||
</el-form-item>
|
||||
<template v-if="supportsAgentDashboardAuth(form.agentType)">
|
||||
<el-form-item :label="$t('commons.login.username')" prop="dashboardUsername">
|
||||
<el-input v-model="form.dashboardUsername" />
|
||||
@@ -35,6 +46,12 @@
|
||||
{{ $t('commons.button.random') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<span class="input-help" v-if="form.agentType === 'deepseek-harness'">
|
||||
{{ $t('aiTools.agents.deepSeekAuthHelper') }}
|
||||
</span>
|
||||
<span class="input-help" v-if="form.agentType === 'octop'">
|
||||
{{ $t('aiTools.agents.octopAuthHelper') }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<el-form-item
|
||||
@@ -130,6 +147,9 @@ import { getRandomStr, newUUID } from '@/utils/id';
|
||||
import {
|
||||
buildDefaultAllowedOrigin,
|
||||
getAgentProviderDisplayName,
|
||||
isAgentType,
|
||||
isDeepSeekAccessHost,
|
||||
isOctopPassword,
|
||||
parseAllowedOriginsInput,
|
||||
supportsAgentDashboardAuth,
|
||||
validateAllowedOriginsInput,
|
||||
@@ -138,6 +158,7 @@ import { App } from '@/api/interface/app';
|
||||
import AdvancedSetting from '@/components/advanced-setting/index.vue';
|
||||
import AccountAddDialog from '@/views/ai/agents/model/add/index.vue';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
import i18n from '@/lang';
|
||||
|
||||
const emit = defineEmits(['search', 'task']);
|
||||
|
||||
@@ -161,6 +182,7 @@ const form = reactive({
|
||||
agentType: 'openclaw' as AI.AgentType,
|
||||
appVersion: '',
|
||||
webUIPort: 18789,
|
||||
accessHost: '',
|
||||
allowedOrigins: '',
|
||||
provider: '',
|
||||
accountId: undefined as unknown as number,
|
||||
@@ -185,7 +207,10 @@ const form = reactive({
|
||||
const showModelConfig = computed(() => form.agentType === 'openclaw' || form.agentType === 'hermes-agent');
|
||||
|
||||
const generateDashboardPassword = () => {
|
||||
form.dashboardPassword = getRandomStr(8);
|
||||
form.dashboardPassword =
|
||||
form.agentType === 'deepseek-harness' || form.agentType === 'octop'
|
||||
? `Aa2${Array.from(crypto.getRandomValues(new Uint8Array(16)), (byte) => byte.toString(16).padStart(2, '0')).join('')}`
|
||||
: getRandomStr(8);
|
||||
};
|
||||
|
||||
const ensureDashboardAuth = () => {
|
||||
@@ -201,7 +226,7 @@ const ensureDashboardAuth = () => {
|
||||
};
|
||||
|
||||
const setDefaultWebUIPort = () => {
|
||||
if (form.agentType === 'copaw') {
|
||||
if (form.agentType === 'copaw' || form.agentType === 'octop') {
|
||||
form.webUIPort = 8088;
|
||||
return;
|
||||
}
|
||||
@@ -209,6 +234,10 @@ const setDefaultWebUIPort = () => {
|
||||
form.webUIPort = 9119;
|
||||
return;
|
||||
}
|
||||
if (form.agentType === 'deepseek-harness') {
|
||||
form.webUIPort = 10443;
|
||||
return;
|
||||
}
|
||||
form.webUIPort = 18789;
|
||||
};
|
||||
|
||||
@@ -217,6 +246,19 @@ const rules = reactive({
|
||||
agentType: [Rules.requiredSelect],
|
||||
appVersion: [Rules.requiredSelect],
|
||||
webUIPort: [Rules.requiredInput],
|
||||
accessHost: [
|
||||
Rules.requiredInput,
|
||||
{
|
||||
validator: (_rule: any, value: string, callback: (error?: Error) => void) => {
|
||||
callback(
|
||||
isDeepSeekAccessHost(value)
|
||||
? undefined
|
||||
: new Error(i18n.global.t('aiTools.agents.deepSeekAccessHostHelper')),
|
||||
);
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
allowedOrigins: [
|
||||
Rules.requiredInput,
|
||||
{
|
||||
@@ -234,8 +276,35 @@ const rules = reactive({
|
||||
provider: [Rules.requiredSelect],
|
||||
accountId: [Rules.requiredSelect],
|
||||
model: [Rules.requiredInput],
|
||||
dashboardUsername: [Rules.requiredInput],
|
||||
dashboardPassword: [Rules.requiredInput],
|
||||
dashboardUsername: [
|
||||
Rules.requiredInput,
|
||||
{
|
||||
validator: (_rule: any, value: string, callback: (error?: Error) => void) => {
|
||||
callback(
|
||||
form.agentType === 'deepseek-harness' && !/^[A-Za-z0-9._-]+$/.test(value)
|
||||
? new Error(i18n.global.t('aiTools.agents.deepSeekAuthHelper'))
|
||||
: undefined,
|
||||
);
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
dashboardPassword: [
|
||||
Rules.requiredInput,
|
||||
{
|
||||
validator: (_rule: any, value: string, callback: (error?: Error) => void) => {
|
||||
const invalidCharacters = value !== value.trim() || /[\r\n\0]/.test(value);
|
||||
if (form.agentType === 'deepseek-harness' && (invalidCharacters || [...value].length < 12)) {
|
||||
callback(new Error(i18n.global.t('aiTools.agents.deepSeekAuthHelper')));
|
||||
} else if (form.agentType === 'octop' && !isOctopPassword(value)) {
|
||||
callback(new Error(i18n.global.t('aiTools.agents.octopAuthHelper')));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
containerName: [Rules.containerName],
|
||||
restartPolicy: [Rules.requiredSelect],
|
||||
cpuQuota: [checkNumberRange(0, 99999)],
|
||||
@@ -273,7 +342,13 @@ const loadSystemIP = async () => {
|
||||
};
|
||||
|
||||
const loadVersions = async (appKey: AI.AgentType) => {
|
||||
versions.value = [];
|
||||
appInfo.value = undefined;
|
||||
form.dockerCompose = '';
|
||||
const res = await getAppByKey(appKey);
|
||||
if (form.agentType !== appKey) {
|
||||
return;
|
||||
}
|
||||
appInfo.value = res.data;
|
||||
versions.value = res.data.versions || [];
|
||||
if (!form.appVersion && versions.value.length > 0) {
|
||||
@@ -287,6 +362,10 @@ const getDefaultAgentName = (agentType: AI.AgentType) => {
|
||||
return 'QwenPaw';
|
||||
case 'hermes-agent':
|
||||
return 'Hermes-Agent';
|
||||
case 'deepseek-harness':
|
||||
return 'DeepSeek-Harness';
|
||||
case 'octop':
|
||||
return 'Octop';
|
||||
default:
|
||||
return 'OpenClaw';
|
||||
}
|
||||
@@ -402,7 +481,9 @@ const handleAgentTypeChange = async () => {
|
||||
form.name === 'OpenClaw' ||
|
||||
form.name === 'CoPaw' ||
|
||||
form.name === 'QwenPaw' ||
|
||||
form.name === 'Hermes-Agent'
|
||||
form.name === 'Hermes-Agent' ||
|
||||
form.name === 'DeepSeek-Harness' ||
|
||||
form.name === 'Octop'
|
||||
) {
|
||||
form.name = getDefaultAgentName(form.agentType);
|
||||
}
|
||||
@@ -413,6 +494,10 @@ const handleAgentTypeChange = async () => {
|
||||
form.accountId = undefined as unknown as number;
|
||||
form.baseURL = '';
|
||||
ensureDashboardAuth();
|
||||
if (form.agentType === 'deepseek-harness' && !form.accessHost) {
|
||||
await loadSystemIP();
|
||||
form.accessHost = isDeepSeekAccessHost(systemIP.value) ? systemIP.value : '';
|
||||
}
|
||||
if (form.agentType === 'openclaw') {
|
||||
await loadSystemIP();
|
||||
allowedOriginsAutoFilled.value = true;
|
||||
@@ -485,6 +570,7 @@ const submit = async () => {
|
||||
webUIPort: form.webUIPort,
|
||||
allowedOrigins: form.agentType === 'openclaw' ? parseAllowedOriginsInput(form.allowedOrigins) : undefined,
|
||||
agentType: form.agentType,
|
||||
accessHost: form.agentType === 'deepseek-harness' ? form.accessHost : undefined,
|
||||
model: showModelConfig.value ? form.model : undefined,
|
||||
accountId: showModelConfig.value ? form.accountId : undefined,
|
||||
token: form.agentType === 'openclaw' ? form.token : undefined,
|
||||
@@ -524,45 +610,19 @@ const handleClose = () => {
|
||||
form.dashboardPassword = '';
|
||||
form.remark = '';
|
||||
form.allowedOrigins = '';
|
||||
form.accessHost = '';
|
||||
form.dockerCompose = '';
|
||||
lastAutoAllowedOrigins.value = '';
|
||||
allowedOriginsAutoFilled.value = true;
|
||||
};
|
||||
|
||||
const openDrawer = async (agentType?: AI.AgentType) => {
|
||||
const targetType =
|
||||
agentType === 'copaw' || agentType === 'hermes-agent' || agentType === 'openclaw' ? agentType : 'openclaw';
|
||||
const targetType = isAgentType(agentType) ? agentType : 'openclaw';
|
||||
form.name = getDefaultAgentName(targetType);
|
||||
open.value = true;
|
||||
form.agentType = targetType;
|
||||
setDefaultWebUIPort();
|
||||
form.token = getRandomStr(32).toLowerCase();
|
||||
ensureDashboardAuth();
|
||||
if (form.agentType === 'copaw') {
|
||||
form.allowedOrigins = '';
|
||||
lastAutoAllowedOrigins.value = '';
|
||||
allowedOriginsAutoFilled.value = true;
|
||||
await loadVersions('copaw');
|
||||
providerOptions.value = [];
|
||||
providerModels.value = {};
|
||||
accountOptions.value = [];
|
||||
return;
|
||||
}
|
||||
if (form.agentType === 'hermes-agent') {
|
||||
form.allowedOrigins = '';
|
||||
lastAutoAllowedOrigins.value = '';
|
||||
allowedOriginsAutoFilled.value = true;
|
||||
await loadVersions('hermes-agent');
|
||||
await loadProviders();
|
||||
await loadAccounts();
|
||||
return;
|
||||
}
|
||||
await loadSystemIP();
|
||||
allowedOriginsAutoFilled.value = true;
|
||||
syncAllowedOriginsWithDefault(true);
|
||||
await loadVersions('openclaw');
|
||||
await loadProviders();
|
||||
await loadAccounts();
|
||||
await handleAgentTypeChange();
|
||||
};
|
||||
|
||||
const openAccountCreate = () => {
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSearchReset } from '@/composables/useSearchReset';
|
||||
import { useSearchState } from '@/utils/search-reset';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { Refresh, Search } from '@element-plus/icons-vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
@@ -193,7 +193,8 @@ const searching = ref(false);
|
||||
const operating = ref('');
|
||||
const keyword = ref('');
|
||||
const marketKeyword = ref('');
|
||||
useSearchReset(
|
||||
useSearchState(
|
||||
'ai/agents/agent/config/tabs/plugins',
|
||||
{ keyword, marketKeyword },
|
||||
{
|
||||
scope: () => agentId.value || undefined,
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSearchReset } from '@/composables/useSearchReset';
|
||||
import { useSearchState } from '@/utils/search-reset';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { Refresh, Search } from '@element-plus/icons-vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@@ -169,7 +169,8 @@ const getDefaultMarketSource = (): HermesSkillMarketSource => {
|
||||
const marketSource = ref<HermesSkillMarketSource>(getDefaultMarketSource());
|
||||
const marketSearched = ref(false);
|
||||
const agentId = ref(0);
|
||||
useSearchReset(
|
||||
useSearchState(
|
||||
'ai/agents/agent/config/tabs/skills/hermes',
|
||||
{ installedKeyword, marketKeyword },
|
||||
{
|
||||
scope: () => agentId.value || undefined,
|
||||
|
||||
@@ -142,7 +142,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSearchReset } from '@/composables/useSearchReset';
|
||||
import { useSearchState } from '@/utils/search-reset';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { Refresh, Search } from '@element-plus/icons-vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@@ -181,7 +181,8 @@ const getDefaultMarketSource = (): SkillMarketSource => {
|
||||
const marketSource = ref<SkillMarketSource>(getDefaultMarketSource());
|
||||
const marketSearched = ref(false);
|
||||
const agentId = ref(0);
|
||||
useSearchReset(
|
||||
useSearchState(
|
||||
'ai/agents/agent/config/tabs/skills/openclaw',
|
||||
{ installedKeyword, marketKeyword },
|
||||
{
|
||||
scope: () => agentId.value || undefined,
|
||||
|
||||
@@ -178,6 +178,9 @@
|
||||
alignment="start"
|
||||
:size="2"
|
||||
>
|
||||
<el-text v-if="row.agentType === 'octop'" type="info" size="small">
|
||||
{{ $t('aiTools.agents.initialCredentials') }}
|
||||
</el-text>
|
||||
<span>{{ row.dashboardUsername || '-' }}</span>
|
||||
<el-space alignment="center">
|
||||
<span>{{ row.dashboardPassword ? '********' : '-' }}</span>
|
||||
@@ -272,7 +275,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSearchReset } from '@/composables/useSearchReset';
|
||||
import { useSearchState } from '@/utils/search-reset';
|
||||
import { toRef, defineAsyncComponent, onMounted, reactive, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
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 { dateFormat } from '@/utils/date';
|
||||
import { newUUID } from '@/utils/id';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||
|
||||
import AddDialog from '@/views/ai/agents/agent/add/index.vue';
|
||||
import DeleteDialog from '@/views/ai/agents/agent/delete/index.vue';
|
||||
@@ -302,6 +305,8 @@ import DockerStatus from '@/views/container/docker-status/index.vue';
|
||||
import {
|
||||
getAgentProviderDisplayName,
|
||||
getOpenclawAccessScheme,
|
||||
isAgentType,
|
||||
isDeepSeekAccessHost,
|
||||
supportsAgentModelConfig,
|
||||
supportsAgentToken,
|
||||
supportsAgentDashboardAuth,
|
||||
@@ -312,6 +317,8 @@ import NoApp from '@/views/app-store/apps/no-app/index.vue';
|
||||
import openclawIcon from '@/assets/images/ai-agent-openclaw.svg';
|
||||
import copawIcon from '@/assets/images/ai-agent-copaw.svg';
|
||||
import hermesIcon from '@/assets/images/ai-agent-hermes-agent.svg';
|
||||
import deepSeekIcon from '@/assets/images/ai-agent-deepseek-harness.png';
|
||||
import octopIcon from '@/assets/images/ai-agent-octop.png';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
import { loadOptionalComponent } from '@/extensions/optional';
|
||||
import { useOperateNodeContext } from '@/composables/useOperateNodeContext';
|
||||
@@ -358,8 +365,7 @@ const buttons = [
|
||||
{
|
||||
label: i18n.global.t('menu.config'),
|
||||
click: (row: AI.AgentItem) => openConfig(row),
|
||||
show: (row: AI.AgentItem) =>
|
||||
supportsAgentModelConfig(row.agentType) || supportsAgentDashboardAuth(row.agentType),
|
||||
show: (row: AI.AgentItem) => supportsAgentModelConfig(row.agentType) || row.agentType === 'copaw',
|
||||
disabled: (row: AI.AgentItem) => row.status !== 'Running',
|
||||
},
|
||||
{
|
||||
@@ -400,7 +406,11 @@ const paginationConfig = reactive({
|
||||
pageSize: 10,
|
||||
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 () => {
|
||||
try {
|
||||
@@ -441,6 +451,10 @@ const getAgentTypeLabel = (agentType: AI.AgentType) => {
|
||||
return 'QwenPaw';
|
||||
case 'hermes-agent':
|
||||
return 'Hermes Agent';
|
||||
case 'deepseek-harness':
|
||||
return 'DeepSeek Harness';
|
||||
case 'octop':
|
||||
return 'Octop';
|
||||
default:
|
||||
return 'OpenClaw';
|
||||
}
|
||||
@@ -452,6 +466,10 @@ const getAgentTypeIcon = (agentType: AI.AgentType) => {
|
||||
return copawIcon;
|
||||
case 'hermes-agent':
|
||||
return hermesIcon;
|
||||
case 'deepseek-harness':
|
||||
return deepSeekIcon;
|
||||
case 'octop':
|
||||
return octopIcon;
|
||||
default:
|
||||
return openclawIcon;
|
||||
}
|
||||
@@ -461,8 +479,7 @@ const openCreate = (agentType?: AI.AgentType) => {
|
||||
if (noApp.value) {
|
||||
return;
|
||||
}
|
||||
const targetType =
|
||||
agentType === 'copaw' || agentType === 'hermes-agent' || agentType === 'openclaw' ? agentType : 'openclaw';
|
||||
const targetType = isAgentType(agentType) ? agentType : 'openclaw';
|
||||
if (addRef.value?.open) {
|
||||
addRef.value.open(targetType);
|
||||
}
|
||||
@@ -473,12 +490,7 @@ const openCreateFromQuery = async () => {
|
||||
if (!shouldOpen) {
|
||||
return;
|
||||
}
|
||||
const agentType =
|
||||
route.query.agentType === 'copaw'
|
||||
? 'copaw'
|
||||
: route.query.agentType === 'hermes-agent'
|
||||
? 'hermes-agent'
|
||||
: 'openclaw';
|
||||
const agentType = isAgentType(route.query.agentType) ? route.query.agentType : 'openclaw';
|
||||
openCreate(agentType);
|
||||
const nextQuery = { ...route.query };
|
||||
delete nextQuery.open;
|
||||
@@ -547,7 +559,12 @@ const openTerminal = (row: AI.AgentItem) => {
|
||||
openTerminalDialog({
|
||||
containerID: row.containerName,
|
||||
title,
|
||||
users: row.agentType === 'hermes-agent' ? ['hermes', 'root'] : ['node', 'root'],
|
||||
users:
|
||||
row.agentType === 'hermes-agent'
|
||||
? ['hermes', 'root']
|
||||
: row.agentType === 'octop'
|
||||
? ['root']
|
||||
: ['node', 'root'],
|
||||
shell: '/bin/bash',
|
||||
initCmd: '',
|
||||
});
|
||||
@@ -579,6 +596,14 @@ const openWorkDir = (row: AI.AgentItem) => {
|
||||
};
|
||||
|
||||
const jumpWebUI = (row: AI.AgentItem) => {
|
||||
if (row.agentType === 'deepseek-harness') {
|
||||
if (!isDeepSeekAccessHost(row.accessHost || '') || !row.webUIPort) {
|
||||
MsgError(i18n.global.t('aiTools.agents.nativeAgentAccessUnavailable'));
|
||||
return;
|
||||
}
|
||||
window.open(`https://${row.accessHost}:${row.webUIPort}`, '_blank', 'noopener,noreferrer');
|
||||
return;
|
||||
}
|
||||
if (dialogPortJumpRef.value?.acceptParams) {
|
||||
dialogPortJumpRef.value.acceptParams({
|
||||
port: row.webUIPort,
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSearchReset } from '@/composables/useSearchReset';
|
||||
import { useSearchState } from '@/utils/search-reset';
|
||||
import { toRef, onMounted, reactive, ref } from 'vue';
|
||||
import { deleteAgentAccount, getAgentProviders, pageAgentAccounts } from '@/api/modules/ai';
|
||||
import { AI } from '@/api/interface/ai';
|
||||
@@ -111,7 +111,11 @@ const paginationConfig = reactive({
|
||||
pageSize: 10,
|
||||
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 req: AI.AgentAccountSearch = {
|
||||
|
||||
@@ -98,6 +98,8 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRef } from 'vue';
|
||||
import { useSearchState } from '@/utils/search-reset';
|
||||
import { AI } from '@/api/interface/ai';
|
||||
import {
|
||||
deleteMcpServer,
|
||||
@@ -129,11 +131,18 @@ const bindDomainRef = ref();
|
||||
const configRef = ref();
|
||||
const items = ref<AI.McpServer[]>([]);
|
||||
const paginationConfig = reactive({
|
||||
cacheSizeKey: 'mcp-server-page-size',
|
||||
currentPage: 1,
|
||||
pageSize: Number(localStorage.getItem('mcp-server-page-size')) || 20,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
});
|
||||
useSearchState(
|
||||
'ai/mcp/server/index',
|
||||
{
|
||||
page: toRef(paginationConfig, 'currentPage'),
|
||||
pageSize: toRef(paginationConfig, 'pageSize'),
|
||||
},
|
||||
{ onRevisit: () => search() },
|
||||
);
|
||||
|
||||
const getUrl = (row: AI.McpServer) => {
|
||||
if (row.outputTransport == 'sse') {
|
||||
|
||||
@@ -186,7 +186,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSearchReset } from '@/composables/useSearchReset';
|
||||
import { useSearchState } from '@/utils/search-reset';
|
||||
import AppStatus from '@/components/app-status/index.vue';
|
||||
import AddDialog from '@/views/ai/model/ollama/add/index.vue';
|
||||
import Conn from '@/views/ai/model/ollama/conn/index.vue';
|
||||
@@ -231,13 +231,16 @@ const bindDomainRef = ref();
|
||||
const taskLogRef = ref();
|
||||
const data = ref();
|
||||
const paginationConfig = reactive({
|
||||
cacheSizeKey: 'model-page-size',
|
||||
currentPage: 1,
|
||||
pageSize: Number(localStorage.getItem('page-size')) || 20,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
});
|
||||
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 opRef = ref();
|
||||
|
||||
@@ -77,6 +77,8 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRef } from 'vue';
|
||||
import { useSearchState } from '@/utils/search-reset';
|
||||
import OperateDialog from './operate/index.vue';
|
||||
import ComposeLogs from '@/components/log/compose/index.vue';
|
||||
import PortJumpDialog from '@/components/port-jump/index.vue';
|
||||
@@ -101,6 +103,14 @@ const paginationConfig = reactive({
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
useSearchState(
|
||||
'ai/model/tensorrt/index',
|
||||
{
|
||||
page: toRef(paginationConfig, 'currentPage'),
|
||||
pageSize: toRef(paginationConfig, 'pageSize'),
|
||||
},
|
||||
{ onRevisit: () => search() },
|
||||
);
|
||||
const searchName = ref();
|
||||
const opRef = ref();
|
||||
const dialogRef = ref();
|
||||
|
||||
@@ -70,10 +70,18 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSearchReset } from '@/composables/useSearchReset';
|
||||
import { useSearchState } from '@/utils/search-reset';
|
||||
import { App } from '@/api/interface/app';
|
||||
import { toRef, onMounted, reactive, ref } from 'vue';
|
||||
import { searchApp, syncApp, syncCutomAppStore, syncLocalApp, getCurrentNodeCustomAppConfig } from '@/api/modules/app';
|
||||
import {
|
||||
searchApp,
|
||||
syncApp,
|
||||
syncCutomAppStore,
|
||||
syncLocalApp,
|
||||
getCurrentNodeCustomAppConfig,
|
||||
getLocalAppSyncConfig,
|
||||
dispatchLocalAppSync,
|
||||
} from '@/api/modules/app';
|
||||
import Install from '../detail/install/index.vue';
|
||||
import router from '@/routers';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
@@ -88,12 +96,11 @@ import AppCard from '@/views/app-store/apps/app/index.vue';
|
||||
import MainDiv from '@/components/main-div/index.vue';
|
||||
import { jumpToInstall } from '@/utils/app';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
const { isProductPro, isOffline, isMobile, isEnterprise } = useGlobalStore();
|
||||
const { isProductPro, isOffline, isMobile, isEnterprise, isMaster, isXpackOrEE, isAdmin } = useGlobalStore();
|
||||
|
||||
const paginationConfig = reactive({
|
||||
cacheSizeKey: 'app-page-size',
|
||||
currentPage: 1,
|
||||
pageSize: Number(localStorage.getItem('app-page-size')) || 60,
|
||||
pageSize: 60,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
@@ -105,8 +112,13 @@ const req = reactive({
|
||||
resource: 'all',
|
||||
showCurrentArch: false,
|
||||
});
|
||||
useSearchReset(
|
||||
{ search: toRef(req, 'name'), page: toRef(paginationConfig, 'currentPage') },
|
||||
useSearchState(
|
||||
'app-store/apps/index',
|
||||
{
|
||||
search: toRef(req, 'name'),
|
||||
page: toRef(paginationConfig, 'currentPage'),
|
||||
pageSize: toRef(paginationConfig, 'pageSize'),
|
||||
},
|
||||
{ onRevisit: () => search(req) },
|
||||
);
|
||||
|
||||
@@ -133,7 +145,6 @@ const search = async (req: App.AppReq) => {
|
||||
loading.value = true;
|
||||
req.pageSize = paginationConfig.pageSize;
|
||||
req.page = paginationConfig.currentPage;
|
||||
localStorage.setItem('app-page-size', req.pageSize + '');
|
||||
|
||||
const customReq = {
|
||||
page: req.page,
|
||||
@@ -201,21 +212,27 @@ const sync = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const syncLocal = () => {
|
||||
const syncLocal = async () => {
|
||||
const taskID = newUUID();
|
||||
const syncReq = {
|
||||
taskID: taskID,
|
||||
};
|
||||
syncing.value = true;
|
||||
syncLocalApp(syncReq)
|
||||
.then(() => {
|
||||
try {
|
||||
const dispatch =
|
||||
isMaster.value && isXpackOrEE.value && isAdmin.value && (await getLocalAppSyncConfig()).data.enabled;
|
||||
if (dispatch) {
|
||||
await dispatchLocalAppSync(syncReq);
|
||||
taskLogRef.value.openWithTaskID(taskID, true, 'local');
|
||||
} else {
|
||||
await syncLocalApp(syncReq);
|
||||
openTaskLog(taskID);
|
||||
canUpdate.value = false;
|
||||
search(req);
|
||||
})
|
||||
.finally(() => {
|
||||
syncing.value = false;
|
||||
});
|
||||
}
|
||||
canUpdate.value = false;
|
||||
search(req);
|
||||
} finally {
|
||||
syncing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const changeTag = (key: string) => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user