mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-11-08 08:05:07 +00:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff700c036e | ||
|
|
3c1f2a64b4 | ||
|
|
94e2ee5403 | ||
|
|
1927e68010 | ||
|
|
ab506e4a3b | ||
|
|
58c81b9255 | ||
|
|
6fa378c13d | ||
|
|
451ec3398e | ||
|
|
726fd94e07 | ||
|
|
95f12c16c3 | ||
|
|
7a40ef9725 | ||
|
|
ea31289da3 |
+4
-14
@@ -4,8 +4,6 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/service"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/accelerator"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -21,20 +19,12 @@ func (b *BaseApi) LoadModelGPUDevices(c *gin.Context) {
|
||||
// @Security Timestamp
|
||||
// @Router /ai/gpu/load [get]
|
||||
func (b *BaseApi) LoadGpuInfo(c *gin.Context) {
|
||||
ok, client := accelerator.New()
|
||||
if ok {
|
||||
snapshot, err := client.Collect(c.Request.Context())
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
if warning := snapshot.Warning(); warning != nil {
|
||||
global.LOG.Warnf("load realtime accelerator data partially failed, err: %v", warning)
|
||||
}
|
||||
helper.SuccessWithData(c, &snapshot.Info)
|
||||
info, err := service.LoadGPUInfo(c.Request.Context(), c.Query("refresh") == "true")
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, &accelerator.Info{})
|
||||
helper.SuccessWithData(c, info)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
|
||||
@@ -10,6 +10,9 @@ type DockerStatus struct {
|
||||
}
|
||||
|
||||
type DaemonJsonConf struct {
|
||||
NVIDIA bool `json:"nvidia"`
|
||||
NVIDIASupported bool `json:"nvidiaSupported"`
|
||||
|
||||
IsSwarm bool `json:"isSwarm"`
|
||||
Version string `json:"version"`
|
||||
Mirrors []string `json:"registryMirrors"`
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/accelerator"
|
||||
)
|
||||
|
||||
type GPURealtimeInfo struct {
|
||||
accelerator.Info
|
||||
Environment GPUEnvironment `json:"environment"`
|
||||
Summaries []GPUSummary `json:"summaries"`
|
||||
}
|
||||
|
||||
type GPUEnvironment struct {
|
||||
CheckedAt time.Time `json:"checkedAt"`
|
||||
HardwareStatus string `json:"hardwareStatus"`
|
||||
Checks []GPUEnvironmentCheck `json:"checks"`
|
||||
}
|
||||
|
||||
type GPUEnvironmentCheck struct {
|
||||
Vendor string `json:"vendor"`
|
||||
Item string `json:"item"`
|
||||
Status string `json:"status"`
|
||||
Code string `json:"code,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
type GPUSummary struct {
|
||||
Vendor string `json:"vendor"`
|
||||
DriverVersion string `json:"driverVersion"`
|
||||
CudaVersion string `json:"cudaVersion"`
|
||||
CardCount *int `json:"cardCount"`
|
||||
MemoryTotal *float64 `json:"memoryTotal"`
|
||||
SharedMemoryTotal *float64 `json:"sharedMemoryTotal"`
|
||||
MemoryPartial bool `json:"memoryPartial"`
|
||||
}
|
||||
@@ -48,7 +48,7 @@ func NewIAppInstallRepo() IAppInstallRepo {
|
||||
|
||||
func (a *AppInstallRepo) UpdateStatus(install *model.AppInstall) error {
|
||||
return getDb().Model(&model.AppInstall{}).Where("id = ? AND updated_at = ?", install.ID, install.UpdatedAt).
|
||||
Updates(map[string]interface{}{"status": install.Status, "message": install.Message}).Error
|
||||
Updates(map[string]interface{}{"status": install.Status, "message": install.Message, "container_name": install.ContainerName}).Error
|
||||
}
|
||||
|
||||
func (a *AppInstallRepo) WithDetailIdsIn(detailIds []uint) DBOption {
|
||||
|
||||
@@ -117,6 +117,30 @@ func SyncLocalModelAccount(install model.AppInstall, accountID uint, baseURL str
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func UpdateLocalModelAccountAddress(accountID uint, previousURL, baseURL string) (*model.AgentAccount, error) {
|
||||
agentAccountMutationMu.Lock()
|
||||
defer agentAccountMutationMu.Unlock()
|
||||
account, err := agentAccountRepo.GetFirst(repo.WithByID(accountID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.TrimRight(account.BaseURL, "/") != previousURL || account.BaseURL == baseURL {
|
||||
return account, nil
|
||||
}
|
||||
account.BaseURL = baseURL
|
||||
if account.ValidateAvailability {
|
||||
account.Verified = false
|
||||
account.VerifiedAt = nil
|
||||
account.VerifyError = ""
|
||||
}
|
||||
if err := agentAccountRepo.Save(account); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
terminalai.InvalidateTerminalRuntimeCache()
|
||||
terminalai.InvalidateFileAIRuntimeCache()
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func SyncOllamaModelAccount(createIfMissing bool) error {
|
||||
app, err := appRepo.GetFirst(appRepo.WithKey("ollama"))
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
|
||||
@@ -518,7 +518,7 @@ func (a AppService) InstallWithHooks(req request.AppInstallCreate, executeScript
|
||||
appInstall.ServiceName = serviceName
|
||||
|
||||
if !req.SkipComposeCommonConfig {
|
||||
if err = addDockerComposeCommonParam(composeMap, appInstall.ServiceName, req.AppContainerConfig, req.Params); err != nil {
|
||||
if err = addDockerComposeCommonParam(composeMap, appInstall.ServiceName, req.AppContainerConfig, req.Params, app.Key); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -553,7 +553,7 @@ func (a *AppInstallService) UpdateWithBeforeRestart(req request.AppInstalledUpda
|
||||
}
|
||||
}
|
||||
if req.Advanced && !req.SkipComposeCommonConfig {
|
||||
if err = addDockerComposeCommonParam(composeMap, installed.ServiceName, req.AppContainerConfig, req.Params); err != nil {
|
||||
if err = addDockerComposeCommonParam(composeMap, installed.ServiceName, req.AppContainerConfig, req.Params, installed.App.Key); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -1207,7 +1207,11 @@ func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error {
|
||||
containersMap map[string]container.Summary
|
||||
containerNames = strings.Split(appInstall.ContainerName, ",")
|
||||
)
|
||||
containers, err = cli.ListContainersByName(containerNames)
|
||||
if appInstall.App.Key == "vllm" {
|
||||
containers, err = cli.ListAllContainers()
|
||||
} else {
|
||||
containers, err = cli.ListContainersByName(containerNames)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ func operationStatusTestDB(t *testing.T) (*gorm.DB, *gorm.DB, model.AppInstall)
|
||||
taskDB := open("tasks.db", &model.Task{})
|
||||
install := model.AppInstall{
|
||||
Name: "gb10-ds-vision", AppId: 1, AppDetailId: 1,
|
||||
Version: "nvidia-gb10-dspark-0.1.1", ContainerName: "vllm-dspark-1", ServiceName: "vllm-dspark",
|
||||
Version: "nvidia-0.25-optimized-for-ds-v4-flash", ContainerName: "vllm-dspark-1", ServiceName: "vllm-dspark",
|
||||
Status: constant.StatusStarting, Env: `{"MODEL_DIR":"/models/vision"}`,
|
||||
}
|
||||
if err := appDB.Omit(clause.Associations).Create(&install).Error; err != nil {
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/vllm"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
@@ -239,6 +240,16 @@ func (u *appUpgradeContext) prepare(t *task.Task) error {
|
||||
if err = copyUpgradeStageFile(u.original.GetPath(), u.stageDir, ".env"); err != nil {
|
||||
return err
|
||||
}
|
||||
if u.original.App.Key == vllmAppKeyForUpgrade && vllm.IsGB10Version(u.original.Version) {
|
||||
for _, directory := range []string{u.original.GetPath(), u.stageDir} {
|
||||
for _, script := range []string{"start.sh", "stop.sh"} {
|
||||
scriptPath := path.Join(directory, "scripts", script)
|
||||
if !fileOp.Stat(scriptPath) {
|
||||
return buserr.WithName("ErrFileNotFound", scriptPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if u.original.App.Key == constant.AppOpenclaw {
|
||||
if err = copyUpgradeStageFile(u.original.GetPath(), u.stageDir, path.Join("data", "conf", "openclaw.json")); err != nil {
|
||||
return err
|
||||
@@ -271,7 +282,7 @@ func (u *appUpgradeContext) prepare(t *task.Task) error {
|
||||
}
|
||||
image := buildVllmUpgradeImage(loadVllmImageFromEnv(stagedInstall.Env), u.original.Version, u.detail.Version)
|
||||
envs[vllmImageEnvKey] = image
|
||||
if strings.HasPrefix(strings.ToLower(strings.TrimSpace(u.detail.Version)), vllmGB10VersionPrefix) {
|
||||
if vllm.IsGB10Version(u.detail.Version) {
|
||||
envs["DSPARK_VLLM_IMAGE"] = image
|
||||
}
|
||||
paramBytes, marshalErr := json.Marshal(envs)
|
||||
@@ -410,14 +421,22 @@ func (u *appUpgradeContext) cutover(t *task.Task) error {
|
||||
if err := resetLocalModelAccountVerification(u.original); err != nil {
|
||||
return err
|
||||
}
|
||||
useGB10Lifecycle := u.original.App.Key == vllmAppKeyForUpgrade && vllm.IsGB10Version(u.original.Version)
|
||||
u.stopAttempted = true
|
||||
t.LogStart(i18n.GetMsgByKey("UpgradeStop"))
|
||||
if out, err := compose.Stop(u.original.GetComposePath()); err != nil {
|
||||
if out != "" {
|
||||
err = fmt.Errorf("%s: %w", out, err)
|
||||
var stopErr error
|
||||
if useGB10Lifecycle {
|
||||
stopErr = runScript(t, &u.original, "stop")
|
||||
} else {
|
||||
out, err := compose.Stop(u.original.GetComposePath())
|
||||
stopErr = err
|
||||
if err != nil && out != "" {
|
||||
stopErr = fmt.Errorf("%s: %w", out, err)
|
||||
}
|
||||
t.LogFailedWithErr(i18n.GetMsgByKey("UpgradeStop"), err)
|
||||
return err
|
||||
}
|
||||
if stopErr != nil {
|
||||
t.LogFailedWithErr(i18n.GetMsgByKey("UpgradeStop"), stopErr)
|
||||
return stopErr
|
||||
}
|
||||
t.LogSuccess(i18n.GetMsgByKey("UpgradeStop"))
|
||||
u.phase = appUpgradeStopped
|
||||
@@ -449,11 +468,13 @@ func (u *appUpgradeContext) cutover(t *task.Task) error {
|
||||
}
|
||||
|
||||
u.downAttempted = true
|
||||
if out, downErr := compose.Down(u.original.GetComposePath()); downErr != nil {
|
||||
if out != "" {
|
||||
downErr = fmt.Errorf("%s: %w", out, downErr)
|
||||
if !useGB10Lifecycle {
|
||||
if out, downErr := compose.Down(u.original.GetComposePath()); downErr != nil {
|
||||
if out != "" {
|
||||
downErr = fmt.Errorf("%s: %w", out, downErr)
|
||||
}
|
||||
return downErr
|
||||
}
|
||||
return downErr
|
||||
}
|
||||
u.phase = appUpgradeDown
|
||||
|
||||
@@ -473,12 +494,19 @@ func (u *appUpgradeContext) cutover(t *task.Task) error {
|
||||
|
||||
logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("Run"), i18n.GetMsgByKey("App"))
|
||||
t.LogStart(logStr)
|
||||
if out, upErr := compose.UpWithoutBuild(u.original.GetComposePath()); upErr != nil {
|
||||
if out != "" {
|
||||
upErr = fmt.Errorf("%s: %w", out, upErr)
|
||||
var startErr error
|
||||
if useGB10Lifecycle {
|
||||
startErr = runScript(t, &u.candidate, "start")
|
||||
} else {
|
||||
out, err := compose.UpWithoutBuild(u.original.GetComposePath())
|
||||
startErr = err
|
||||
if err != nil && out != "" {
|
||||
startErr = fmt.Errorf("%s: %w", out, err)
|
||||
}
|
||||
t.LogFailedWithErr(logStr, upErr)
|
||||
return upErr
|
||||
}
|
||||
if startErr != nil {
|
||||
t.LogFailedWithErr(logStr, startErr)
|
||||
return startErr
|
||||
}
|
||||
t.LogSuccess(logStr)
|
||||
u.phase = appUpgradeStarted
|
||||
@@ -596,6 +624,9 @@ func (u *appUpgradeContext) rollback(t *task.Task) (rollbackErr error) {
|
||||
t.LogSuccess(logStr)
|
||||
}
|
||||
}()
|
||||
if u.original.App.Key == vllmAppKeyForUpgrade && vllm.IsGB10Version(u.original.Version) {
|
||||
return u.rollbackVllmGB10(t)
|
||||
}
|
||||
if !u.downAttempted {
|
||||
if out, err := compose.Operate(u.original.GetComposePath(), "start"); err != nil {
|
||||
if out != "" {
|
||||
@@ -667,6 +698,31 @@ func (u *appUpgradeContext) rollback(t *task.Task) (rollbackErr error) {
|
||||
return errors.Join(rollbackErr, u.finishRollback())
|
||||
}
|
||||
|
||||
func (u *appUpgradeContext) rollbackVllmGB10(t *task.Task) error {
|
||||
current := u.original
|
||||
if u.phase >= appUpgradeMutated {
|
||||
current = u.candidate
|
||||
}
|
||||
stopErr := runScript(t, ¤t, "stop")
|
||||
if u.phase >= appUpgradeMutated {
|
||||
if err := u.restoreManagedFiles(); err != nil {
|
||||
return errors.Join(stopErr, err)
|
||||
}
|
||||
if u.backupFile != "" {
|
||||
if err := handleAppRecover(&u.original, t, u.backupFile, true, "", "", false); err != nil {
|
||||
return errors.Join(stopErr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := runScript(t, &u.original, "start"); err != nil {
|
||||
return errors.Join(stopErr, err)
|
||||
}
|
||||
if stopErr != nil {
|
||||
return stopErr
|
||||
}
|
||||
return u.finishRollback()
|
||||
}
|
||||
|
||||
func (u *appUpgradeContext) finishRollback() error {
|
||||
if u.feishuOriginal != nil {
|
||||
if err := waitOpenclawReady(u.original.ContainerName); err != nil {
|
||||
@@ -717,7 +773,8 @@ func (u *appUpgradeContext) cleanup() {
|
||||
if u.feishuLockPath != "" {
|
||||
feishuSwitches.Delete(u.feishuLockPath)
|
||||
}
|
||||
if u.feishuOriginal != nil && u.rollbackErr != nil {
|
||||
if u.rollbackErr != nil && (u.feishuOriginal != nil ||
|
||||
(u.original.App.Key == vllmAppKeyForUpgrade && vllm.IsGB10Version(u.original.Version))) {
|
||||
return
|
||||
}
|
||||
if u.snapshot != nil {
|
||||
|
||||
@@ -781,7 +781,7 @@ func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string
|
||||
envs[constant.ContainerName] = install.ContainerName
|
||||
}
|
||||
config.Advanced = true
|
||||
if err := addDockerComposeCommonParam(composeMap, install.ServiceName, config, envs); err != nil {
|
||||
if err := addDockerComposeCommonParam(composeMap, install.ServiceName, config, envs, install.App.Key); err != nil {
|
||||
return "", err
|
||||
}
|
||||
paramByte, err := json.Marshal(envs)
|
||||
@@ -1495,6 +1495,24 @@ func doNotNeedSync(installed model.AppInstall) bool {
|
||||
|
||||
func synAppInstall(containers map[string]container.Summary, appInstall *model.AppInstall, force bool) {
|
||||
oldStatus := appInstall.Status
|
||||
oldContainerName := appInstall.ContainerName
|
||||
if appInstall.App.Key == "vllm" && !strings.Contains(appInstall.ContainerName, ",") {
|
||||
if _, exists := containers["/"+appInstall.ContainerName]; !exists {
|
||||
matchedNames := make(map[string]struct{})
|
||||
for _, con := range containers {
|
||||
if con.Labels["com.docker.compose.project.working_dir"] == appInstall.GetPath() &&
|
||||
con.Labels["com.docker.compose.service"] == appInstall.ServiceName &&
|
||||
!strings.EqualFold(con.Labels["com.docker.compose.oneoff"], "true") && len(con.Names) > 0 {
|
||||
matchedNames[strings.TrimPrefix(con.Names[0], "/")] = struct{}{}
|
||||
}
|
||||
}
|
||||
if len(matchedNames) == 1 {
|
||||
for name := range matchedNames {
|
||||
appInstall.ContainerName = name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
containerNames := strings.Split(appInstall.ContainerName, ",")
|
||||
if len(containers) == 0 {
|
||||
if appInstall.Status == constant.StatusUpErr && !force {
|
||||
@@ -1538,7 +1556,7 @@ func synAppInstall(containers map[string]container.Summary, appInstall *model.Ap
|
||||
appInstall.Status = constant.StatusStopped
|
||||
case runningCount == total:
|
||||
appInstall.Status = constant.StatusRunning
|
||||
if oldStatus == constant.StatusRunning {
|
||||
if oldStatus == constant.StatusRunning && oldContainerName == appInstall.ContainerName {
|
||||
return
|
||||
}
|
||||
case restartingCount == total:
|
||||
@@ -1813,7 +1831,7 @@ func updateToolApp(installed *model.AppInstall) {
|
||||
}
|
||||
}
|
||||
|
||||
func addDockerComposeCommonParam(composeMap map[string]interface{}, serviceName string, req request.AppContainerConfig, params map[string]interface{}) error {
|
||||
func addDockerComposeCommonParam(composeMap map[string]interface{}, serviceName string, req request.AppContainerConfig, params map[string]interface{}, appKey string) error {
|
||||
services, serviceValid := composeMap["services"].(map[string]interface{})
|
||||
if !serviceValid {
|
||||
return buserr.New("ErrFileParse")
|
||||
@@ -1839,6 +1857,11 @@ func addDockerComposeCommonParam(composeMap map[string]interface{}, serviceName
|
||||
return buserr.New("ErrFileParse")
|
||||
}
|
||||
serviceValue := service.(map[string]interface{})
|
||||
if appKey == "vllm" {
|
||||
if name, _ := serviceValue["container_name"].(string); strings.TrimSpace(name) == "" {
|
||||
serviceValue["container_name"] = "${CONTAINER_NAME}"
|
||||
}
|
||||
}
|
||||
|
||||
deploy := map[string]interface{}{}
|
||||
if de, ok := serviceValue["deploy"]; ok {
|
||||
@@ -1877,15 +1900,44 @@ func addDockerComposeCommonParam(composeMap map[string]interface{}, serviceName
|
||||
ports, ok := serviceValue["ports"].([]interface{})
|
||||
if ok {
|
||||
for i, port := range ports {
|
||||
portStr, portOK := port.(string)
|
||||
if !portOK {
|
||||
if config, ok := port.(map[string]interface{}); ok {
|
||||
config["host_ip"] = "${HOST_IP}"
|
||||
if req.Advanced && req.AllowPort && req.SpecifyIP == "" {
|
||||
delete(config, "host_ip")
|
||||
}
|
||||
continue
|
||||
}
|
||||
portArray := strings.Split(portStr, ":")
|
||||
if len(portArray) == 2 {
|
||||
portArray = append([]string{"${HOST_IP}"}, portArray...)
|
||||
portStr := fmt.Sprint(port)
|
||||
parts := []string{}
|
||||
depth, start := 0, 0
|
||||
for index, char := range portStr {
|
||||
switch char {
|
||||
case '[', '{':
|
||||
depth++
|
||||
case ']', '}':
|
||||
depth--
|
||||
case ':':
|
||||
if depth == 0 {
|
||||
parts = append(parts, portStr[start:index])
|
||||
start = index + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
ports[i] = strings.Join(portArray, ":")
|
||||
parts = append(parts, portStr[start:])
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
parts = []string{"", "", parts[0]}
|
||||
case 2:
|
||||
parts = append([]string{""}, parts...)
|
||||
case 3:
|
||||
default:
|
||||
return fmt.Errorf("invalid port mapping: %s", portStr)
|
||||
}
|
||||
parts[0] = "${HOST_IP}"
|
||||
if req.Advanced && req.AllowPort && strings.Contains(req.SpecifyIP, ":") {
|
||||
parts[0] = "[${HOST_IP}]"
|
||||
}
|
||||
ports[i] = strings.Join(parts, ":")
|
||||
}
|
||||
serviceValue["ports"] = ports
|
||||
}
|
||||
|
||||
+220
-8
@@ -8,7 +8,9 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
@@ -192,6 +194,18 @@ func (u *DockerService) LoadDockerStatus() *dto.DockerStatus {
|
||||
return &data
|
||||
}
|
||||
|
||||
func readDockerDaemonConfig() (map[string]interface{}, error) {
|
||||
file, err := os.ReadFile(constant.DaemonJsonPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
daemonMap := make(map[string]interface{})
|
||||
if err := json.Unmarshal(file, &daemonMap); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return daemonMap, nil
|
||||
}
|
||||
|
||||
func (u *DockerService) LoadDockerConf() (*dto.DaemonJsonConf, error) {
|
||||
ctx := context.Background()
|
||||
var data dto.DaemonJsonConf
|
||||
@@ -201,6 +215,8 @@ func (u *DockerService) LoadDockerConf() (*dto.DaemonJsonConf, error) {
|
||||
if err != nil {
|
||||
return &data, err
|
||||
}
|
||||
defer client.Close()
|
||||
data.NVIDIASupported = client.DaemonHost() == "unix:///var/run/docker.sock"
|
||||
itemVersion, err := client.ServerVersion(ctx)
|
||||
if err == nil {
|
||||
data.Version = itemVersion.Version
|
||||
@@ -210,18 +226,14 @@ func (u *DockerService) LoadDockerConf() (*dto.DaemonJsonConf, error) {
|
||||
if strings.Contains(stdout2, "Swarm: active") {
|
||||
data.IsSwarm = true
|
||||
}
|
||||
if _, err := os.Stat(constant.DaemonJsonPath); err != nil {
|
||||
return &data, nil
|
||||
}
|
||||
file, err := os.ReadFile(constant.DaemonJsonPath)
|
||||
daemonMap, err := readDockerDaemonConfig()
|
||||
if err != nil {
|
||||
return &data, nil
|
||||
}
|
||||
var conf daemonJsonItem
|
||||
daemonMap := make(map[string]interface{})
|
||||
if err := json.Unmarshal(file, &daemonMap); err != nil {
|
||||
return &data, nil
|
||||
if runtimes, ok := daemonMap["runtimes"].(map[string]interface{}); ok {
|
||||
_, data.NVIDIA = runtimes["nvidia"]
|
||||
}
|
||||
var conf daemonJsonItem
|
||||
arr, err := json.Marshal(daemonMap)
|
||||
if err != nil {
|
||||
return &data, err
|
||||
@@ -253,6 +265,9 @@ func (u *DockerService) LoadDockerConf() (*dto.DaemonJsonConf, error) {
|
||||
}
|
||||
|
||||
func (u *DockerService) UpdateConf(req dto.SettingUpdate, withRestart bool) error {
|
||||
if req.Key == "NVIDIA" {
|
||||
return updateDockerNVIDIA(req.Value, withRestart)
|
||||
}
|
||||
err := createIfNotExistDaemonJsonFile()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -555,3 +570,200 @@ func validateDockerConfig() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateDockerNVIDIA(value string, withRestart bool) error {
|
||||
if value != "enable" && value != "disable" {
|
||||
return fmt.Errorf("unsupported NVIDIA setting %q", value)
|
||||
}
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
if client.DaemonHost() != "unix:///var/run/docker.sock" {
|
||||
return fmt.Errorf("NVIDIA configuration is only supported for the local default Docker socket")
|
||||
}
|
||||
enabled := value == "enable"
|
||||
original, readErr := os.ReadFile(constant.DaemonJsonPath)
|
||||
existed := readErr == nil
|
||||
if readErr != nil && !os.IsNotExist(readErr) {
|
||||
return readErr
|
||||
}
|
||||
daemonMap := make(map[string]interface{})
|
||||
if existed {
|
||||
if err := json.Unmarshal(original, &daemonMap); err != nil {
|
||||
return fmt.Errorf("failed to parse Docker configuration: %w", err)
|
||||
}
|
||||
if daemonMap == nil {
|
||||
return fmt.Errorf("Docker configuration must be a JSON object")
|
||||
}
|
||||
}
|
||||
runtimePath, err := applyDockerNVIDIAConfig(daemonMap, enabled)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if enabled {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
check := checkNVIDIAToolkit(ctx, func(name string) string {
|
||||
if name == "nvidia-container-runtime" {
|
||||
return runtimePath
|
||||
}
|
||||
return findGPUContainerCommand(name)
|
||||
}, func(ctx context.Context, executable string) (string, error) {
|
||||
return cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(3*time.Second)).RunWithStdout(executable, "--version")
|
||||
})
|
||||
cancel()
|
||||
if check.Status != "passed" {
|
||||
return fmt.Errorf("NVIDIA Container Toolkit is not ready: %s", check.Reason)
|
||||
}
|
||||
}
|
||||
updated, err := json.MarshalIndent(daemonMap, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(path.Dir(constant.DaemonJsonPath), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeDockerNVIDIAConfig(updated); err != nil {
|
||||
return err
|
||||
}
|
||||
restore := func(cause error, restart bool) error {
|
||||
var restoreErr error
|
||||
if existed {
|
||||
restoreErr = writeDockerNVIDIAConfig(original)
|
||||
} else {
|
||||
restoreErr = os.Remove(constant.DaemonJsonPath)
|
||||
}
|
||||
if restoreErr != nil {
|
||||
return fmt.Errorf("%v; failed to restore Docker configuration: %w", cause, restoreErr)
|
||||
}
|
||||
if restart {
|
||||
if err := controller.HandleRestart("docker"); err != nil {
|
||||
return fmt.Errorf("%v; configuration restored but Docker restart failed: %w", cause, err)
|
||||
}
|
||||
}
|
||||
return cause
|
||||
}
|
||||
if global.CACHE != nil {
|
||||
defer global.CACHE.Del(gpuEnvironmentCacheKey)
|
||||
}
|
||||
if err := validateDockerConfig(); err != nil {
|
||||
return restore(err, false)
|
||||
}
|
||||
if !withRestart {
|
||||
return nil
|
||||
}
|
||||
if err := controller.HandleRestart("docker"); err != nil {
|
||||
return restore(fmt.Errorf("failed to restart Docker: %w", err), true)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
info, err := client.Info(ctx)
|
||||
if err != nil {
|
||||
return restore(fmt.Errorf("failed to verify Docker NVIDIA runtime: %w", err), true)
|
||||
}
|
||||
if enabled {
|
||||
check := checkNVIDIADockerConfig(info, readDockerDaemonConfig, findGPUContainerCommand)
|
||||
if check.Status != "passed" {
|
||||
return restore(fmt.Errorf("Docker NVIDIA runtime did not become available: %s", check.Reason), true)
|
||||
}
|
||||
} else if _, registered := info.Runtimes["nvidia"]; registered {
|
||||
return restore(fmt.Errorf("Docker still registers the NVIDIA runtime; check other Docker configuration sources"), true)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyDockerNVIDIAConfig(config map[string]interface{}, enabled bool) (string, error) {
|
||||
runtimes, ok := config["runtimes"].(map[string]interface{})
|
||||
if config["runtimes"] != nil && !ok {
|
||||
return "", fmt.Errorf("Docker runtimes configuration must be a JSON object")
|
||||
}
|
||||
if !enabled {
|
||||
delete(runtimes, "nvidia")
|
||||
if len(runtimes) == 0 {
|
||||
delete(config, "runtimes")
|
||||
}
|
||||
if config["default-runtime"] == "nvidia" {
|
||||
delete(config, "default-runtime")
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
if runtimes == nil {
|
||||
runtimes = make(map[string]interface{})
|
||||
config["runtimes"] = runtimes
|
||||
}
|
||||
nvidia, ok := runtimes["nvidia"].(map[string]interface{})
|
||||
if runtimes["nvidia"] != nil && !ok {
|
||||
return "", fmt.Errorf("Docker NVIDIA runtime configuration must be a JSON object")
|
||||
}
|
||||
if nvidia == nil {
|
||||
nvidia = map[string]interface{}{"runtimeArgs": []string{}}
|
||||
runtimes["nvidia"] = nvidia
|
||||
}
|
||||
configured, ok := nvidia["path"].(string)
|
||||
if nvidia["path"] != nil && !ok {
|
||||
return "", fmt.Errorf("Docker NVIDIA runtime path must be a string")
|
||||
}
|
||||
runtimePath := findNVIDIAContainerCommand("nvidia-container-runtime", config)
|
||||
if runtimePath == "" || !cmd.Which(runtimePath) {
|
||||
if configured != "" {
|
||||
return "", fmt.Errorf("configured NVIDIA runtime %q is not executable", configured)
|
||||
}
|
||||
return "", fmt.Errorf("nvidia-container-runtime is not installed or executable")
|
||||
}
|
||||
if configured == "" {
|
||||
nvidia["path"] = runtimePath
|
||||
}
|
||||
return runtimePath, nil
|
||||
}
|
||||
|
||||
func writeDockerNVIDIAConfig(data []byte) error {
|
||||
target := constant.DaemonJsonPath
|
||||
mode := os.FileMode(0640)
|
||||
info, err := os.Lstat(target)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
if err == nil {
|
||||
target, err = filepath.EvalSymlinks(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info, err = os.Stat(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return fmt.Errorf("Docker configuration must be a regular file")
|
||||
}
|
||||
mode = info.Mode().Perm()
|
||||
}
|
||||
temp, err := os.CreateTemp(filepath.Dir(target), ".daemon-nvidia-*.json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = temp.Close()
|
||||
_ = os.Remove(temp.Name())
|
||||
}()
|
||||
if info != nil {
|
||||
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
|
||||
if err := temp.Chown(int(stat.Uid), int(stat.Gid)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := temp.Chmod(mode); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := temp.Write(data); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := temp.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := temp.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(temp.Name(), target)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,454 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/accelerator"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/npu"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/xpu"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
"github.com/docker/docker/api/types/system"
|
||||
)
|
||||
|
||||
const gpuEnvironmentCacheKey = "gpu:environment"
|
||||
|
||||
var gpuEnvironmentLock = make(chan struct{}, 1)
|
||||
|
||||
var gpuVendors = []string{"nvidia", "intel", "ascend", "amd"}
|
||||
|
||||
type gpuEnvironmentProbe struct {
|
||||
CheckedAt time.Time `json:"checkedAt"`
|
||||
Hardware map[string]int `json:"hardware"`
|
||||
HardwareError string `json:"hardwareError,omitempty"`
|
||||
Commands map[string]bool `json:"commands"`
|
||||
NVIDIAChecked bool `json:"nvidiaChecked"`
|
||||
Checks []dto.GPUEnvironmentCheck `json:"checks"`
|
||||
}
|
||||
|
||||
func LoadGPUInfo(ctx context.Context, refresh bool) (*dto.GPURealtimeInfo, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
snapshot := &accelerator.Snapshot{}
|
||||
var collectErr error
|
||||
if available, client := accelerator.New(); available {
|
||||
collected, err := client.Collect(ctx)
|
||||
collectErr = err
|
||||
if collected != nil {
|
||||
snapshot = collected
|
||||
}
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if collectErr != nil {
|
||||
snapshot.Info.Warnings = append(snapshot.Info.Warnings, collectErr.Error())
|
||||
}
|
||||
if snapshot.Info.CollectedAt.IsZero() {
|
||||
snapshot.Info.CollectedAt = time.Now()
|
||||
}
|
||||
if global.LOG != nil {
|
||||
for _, warning := range snapshot.Info.Warnings {
|
||||
global.LOG.Warnf("load realtime accelerator data: %s", warning)
|
||||
}
|
||||
}
|
||||
summaries := summarizeGPUs(snapshot)
|
||||
nvidiaPresent := slices.ContainsFunc(summaries, func(item dto.GPUSummary) bool { return item.Vendor == "nvidia" })
|
||||
probe := loadGPUEnvironmentProbe(ctx, refresh, nvidiaPresent)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
environment := assembleGPUEnvironment(probe, summaries, collectErr, snapshot.Info)
|
||||
for _, check := range environment.Checks {
|
||||
if check.Item == "driver" && check.Vendor != "" && !slices.ContainsFunc(summaries, func(item dto.GPUSummary) bool { return item.Vendor == check.Vendor }) {
|
||||
summaries = append(summaries, dto.GPUSummary{Vendor: check.Vendor, MemoryPartial: true})
|
||||
}
|
||||
}
|
||||
return &dto.GPURealtimeInfo{Info: snapshot.Info, Environment: environment, Summaries: summaries}, nil
|
||||
}
|
||||
|
||||
func loadGPUEnvironmentProbe(ctx context.Context, refresh, nvidiaPresent bool) gpuEnvironmentProbe {
|
||||
select {
|
||||
case gpuEnvironmentLock <- struct{}{}:
|
||||
defer func() { <-gpuEnvironmentLock }()
|
||||
case <-ctx.Done():
|
||||
return gpuEnvironmentProbe{CheckedAt: time.Now(), HardwareError: ctx.Err().Error()}
|
||||
}
|
||||
if !refresh && global.CACHE != nil {
|
||||
var cached gpuEnvironmentProbe
|
||||
if value := global.CACHE.Get(gpuEnvironmentCacheKey); value != "" && json.Unmarshal([]byte(value), &cached) == nil && (!nvidiaPresent || cached.NVIDIAChecked) {
|
||||
return cached
|
||||
}
|
||||
}
|
||||
probe := gpuEnvironmentProbe{CheckedAt: time.Now(), Commands: map[string]bool{}}
|
||||
var err error
|
||||
probe.Hardware, err = detectGPUHardware("/sys/bus/pci/devices")
|
||||
if err != nil {
|
||||
probe.HardwareError = err.Error()
|
||||
}
|
||||
for vendor := range gpu.ManagementCommands() {
|
||||
probe.Commands[vendor] = true
|
||||
}
|
||||
probe.Commands["intel"], _ = xpu.New()
|
||||
probe.Commands["ascend"], _ = npu.New()
|
||||
if nvidiaPresent || probe.Hardware["nvidia"] > 0 {
|
||||
probe.NVIDIAChecked = true
|
||||
probe.Checks = checkNVIDIAContainerEnvironment(ctx)
|
||||
}
|
||||
if ctx.Err() == nil && global.CACHE != nil {
|
||||
if data, err := json.Marshal(probe); err == nil {
|
||||
global.CACHE.SetWithTTL(gpuEnvironmentCacheKey, string(data), time.Minute)
|
||||
}
|
||||
}
|
||||
return probe
|
||||
}
|
||||
|
||||
func detectGPUHardware(directory string) (map[string]int, error) {
|
||||
devices := map[string]int{}
|
||||
entries, err := os.ReadDir(directory)
|
||||
if err != nil {
|
||||
return devices, err
|
||||
}
|
||||
var readErrors []string
|
||||
for _, entry := range entries {
|
||||
path := filepath.Join(directory, entry.Name())
|
||||
vendorData, err := os.ReadFile(filepath.Join(path, "vendor"))
|
||||
if err != nil {
|
||||
readErrors = append(readErrors, err.Error())
|
||||
continue
|
||||
}
|
||||
vendor := map[string]string{"0x10de": "nvidia", "0x8086": "intel", "0x19e5": "ascend", "0x1002": "amd"}[strings.ToLower(strings.TrimSpace(string(vendorData)))]
|
||||
if vendor == "" {
|
||||
continue
|
||||
}
|
||||
classData, err := os.ReadFile(filepath.Join(path, "class"))
|
||||
if err != nil {
|
||||
readErrors = append(readErrors, err.Error())
|
||||
continue
|
||||
}
|
||||
class, err := strconv.ParseUint(strings.TrimSpace(string(classData)), 0, 32)
|
||||
if err != nil {
|
||||
readErrors = append(readErrors, fmt.Sprintf("read PCI class for %s: %v", entry.Name(), err))
|
||||
continue
|
||||
}
|
||||
baseClass := class >> 16
|
||||
if (vendor == "ascend" && baseClass == 0x12) || (vendor != "ascend" && (baseClass == 0x03 || (vendor == "nvidia" && baseClass == 0x12))) {
|
||||
devices[vendor]++
|
||||
}
|
||||
}
|
||||
if len(readErrors) > 0 {
|
||||
return devices, fmt.Errorf("PCI inventory incomplete: %s", strings.Join(readErrors, "; "))
|
||||
}
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
func assembleGPUEnvironment(probe gpuEnvironmentProbe, summaries []dto.GPUSummary, collectErr error, info accelerator.Info) dto.GPUEnvironment {
|
||||
result := dto.GPUEnvironment{CheckedAt: probe.CheckedAt, HardwareStatus: "not_detected", Checks: []dto.GPUEnvironmentCheck{}}
|
||||
if len(probe.Hardware) > 0 || len(summaries) > 0 {
|
||||
result.HardwareStatus = "detected"
|
||||
} else if probe.HardwareError != "" {
|
||||
result.HardwareStatus = "unknown"
|
||||
}
|
||||
for _, vendor := range gpuVendors {
|
||||
index := slices.IndexFunc(summaries, func(item dto.GPUSummary) bool { return item.Vendor == vendor })
|
||||
if index < 0 && probe.Hardware[vendor] == 0 && !(probe.HardwareError != "" && probe.Commands[vendor] && collectErr != nil) {
|
||||
continue
|
||||
}
|
||||
check := dto.GPUEnvironmentCheck{Vendor: vendor, Item: "driver", Status: "unknown"}
|
||||
switch {
|
||||
case index >= 0:
|
||||
check.Status = "passed"
|
||||
check.Version = summaries[index].DriverVersion
|
||||
if vendor == "intel" {
|
||||
var readErrors []string
|
||||
for _, device := range info.XPUs {
|
||||
if device.ReadError != "" {
|
||||
readErrors = append(readErrors, device.ReadError)
|
||||
}
|
||||
}
|
||||
if len(readErrors) > 0 {
|
||||
check.Status = "unknown"
|
||||
if len(readErrors) == len(info.XPUs) {
|
||||
check.Status = "failed"
|
||||
}
|
||||
check.Reason = strings.Join(readErrors, "; ")
|
||||
}
|
||||
}
|
||||
case !probe.Commands[vendor]:
|
||||
check.Status = "missing"
|
||||
check.Reason = "The device management utility was not found; check the driver and management tool installation."
|
||||
case collectErr != nil:
|
||||
check.Status = "failed"
|
||||
check.Reason = collectErr.Error()
|
||||
default:
|
||||
check.Reason = "The management utility returned no readable devices."
|
||||
if len(info.Warnings) > 0 {
|
||||
check.Status = "failed"
|
||||
check.Reason = strings.Join(info.Warnings, "; ")
|
||||
}
|
||||
}
|
||||
result.Checks = append(result.Checks, check)
|
||||
}
|
||||
result.Checks = append(result.Checks, probe.Checks...)
|
||||
if probe.HardwareError != "" {
|
||||
result.Checks = append(result.Checks, dto.GPUEnvironmentCheck{Item: "driver", Status: "unknown", Reason: probe.HardwareError})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func findGPUContainerCommand(name string) string {
|
||||
for _, path := range []string{name, filepath.Join("/usr/bin", name), filepath.Join("/usr/sbin", name), filepath.Join("/usr/local/bin", name), filepath.Join("/usr/local/sbin", name), filepath.Join("/usr/local/nvidia/toolkit", name)} {
|
||||
if cmd.Which(path) {
|
||||
return path
|
||||
}
|
||||
if filepath.IsAbs(path) {
|
||||
if info, err := os.Stat(path); err == nil && !info.IsDir() {
|
||||
return path
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func findNVIDIAContainerCommand(name string, config map[string]interface{}) string {
|
||||
if name == "nvidia-container-runtime" {
|
||||
runtimes, _ := config["runtimes"].(map[string]interface{})
|
||||
nvidia, _ := runtimes["nvidia"].(map[string]interface{})
|
||||
if configured, ok := nvidia["path"].(string); ok && configured != "" {
|
||||
name = configured
|
||||
}
|
||||
}
|
||||
return findGPUContainerCommand(name)
|
||||
}
|
||||
|
||||
func checkNVIDIAContainerEnvironment(ctx context.Context) []dto.GPUEnvironmentCheck {
|
||||
config, configErr := readDockerDaemonConfig()
|
||||
toolkitCtx, cancelToolkit := context.WithTimeout(ctx, 5*time.Second)
|
||||
toolkit := checkNVIDIAToolkit(toolkitCtx, func(name string) string {
|
||||
return findNVIDIAContainerCommand(name, config)
|
||||
}, func(ctx context.Context, path string) (string, error) {
|
||||
return cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(3*time.Second)).RunWithStdout(path, "--version")
|
||||
})
|
||||
cancelToolkit()
|
||||
dockerCtx, cancelDocker := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancelDocker()
|
||||
dockerCheck := dto.GPUEnvironmentCheck{Vendor: "nvidia", Item: "docker", Status: "failed"}
|
||||
cli, err := docker.NewDockerClient()
|
||||
if err == nil {
|
||||
defer cli.Close()
|
||||
var info system.Info
|
||||
info, err = cli.Info(dockerCtx)
|
||||
if err == nil {
|
||||
dockerCheck = checkNVIDIADockerInfo(info, false)
|
||||
if cli.DaemonHost() == "unix:///var/run/docker.sock" {
|
||||
dockerCheck = checkNVIDIADockerConfig(info, func() (map[string]interface{}, error) { return config, configErr }, findGPUContainerCommand)
|
||||
} else {
|
||||
dockerCheck.Status = "unknown"
|
||||
dockerCheck.Reason = "The selected Docker daemon's NVIDIA runtime cannot be verified using local executables or the default daemon.json."
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
dockerCheck.Reason = err.Error()
|
||||
}
|
||||
return []dto.GPUEnvironmentCheck{toolkit, dockerCheck}
|
||||
}
|
||||
|
||||
func checkNVIDIAToolkit(ctx context.Context, findCommand func(string) string, runVersion func(context.Context, string) (string, error)) dto.GPUEnvironmentCheck {
|
||||
toolkit := dto.GPUEnvironmentCheck{Vendor: "nvidia", Item: "toolkit", Status: "missing"}
|
||||
cli := findCommand("nvidia-container-cli")
|
||||
if cli == "" {
|
||||
toolkit.Reason = "The required nvidia-container-cli component was not found."
|
||||
return toolkit
|
||||
}
|
||||
runtime := findCommand("nvidia-container-runtime")
|
||||
if runtime == "" {
|
||||
runtime = findCommand("nvidia-container-runtime-hook")
|
||||
}
|
||||
if runtime == "" {
|
||||
toolkit.Reason = "The NVIDIA container runtime and runtime hook were not found."
|
||||
return toolkit
|
||||
}
|
||||
data, err := runVersion(ctx, cli)
|
||||
if err != nil || strings.TrimSpace(data) == "" {
|
||||
toolkit.Status = "failed"
|
||||
toolkit.Reason = "nvidia-container-cli returned an empty version."
|
||||
if err != nil {
|
||||
toolkit.Reason = err.Error()
|
||||
}
|
||||
return toolkit
|
||||
}
|
||||
toolkit.Status = "passed"
|
||||
toolkit.Version = strings.SplitN(strings.TrimSpace(data), "\n", 2)[0]
|
||||
if data, err := runVersion(ctx, runtime); err != nil || strings.TrimSpace(data) == "" {
|
||||
toolkit.Status = "failed"
|
||||
toolkit.Reason = "The NVIDIA container runtime returned an empty version."
|
||||
if err != nil {
|
||||
toolkit.Reason = err.Error()
|
||||
}
|
||||
return toolkit
|
||||
}
|
||||
return toolkit
|
||||
}
|
||||
|
||||
func checkNVIDIADockerInfo(info system.Info, hookInstalled bool) dto.GPUEnvironmentCheck {
|
||||
result := dto.GPUEnvironmentCheck{Vendor: "nvidia", Item: "docker", Status: "unconfigured", Version: info.ServerVersion}
|
||||
if _, ok := info.Runtimes["nvidia"]; ok {
|
||||
result.Status = "passed"
|
||||
return result
|
||||
}
|
||||
if hookInstalled {
|
||||
result.Status = "unknown"
|
||||
result.Reason = "NVIDIA runtime hook is installed, but Docker does not expose its GPU device-driver registration."
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func checkNVIDIADockerConfig(info system.Info, readConfig func() (map[string]interface{}, error), findCommand func(string) string) dto.GPUEnvironmentCheck {
|
||||
result := checkNVIDIADockerInfo(info, false)
|
||||
if result.Status == "passed" {
|
||||
runtimePath := info.Runtimes["nvidia"].Path
|
||||
if runtimePath == "" {
|
||||
result.Status = "unknown"
|
||||
result.Reason = "Docker registered an NVIDIA runtime without an executable path; its availability cannot be verified."
|
||||
return result
|
||||
}
|
||||
resolved := runtimePath
|
||||
if !filepath.IsAbs(runtimePath) {
|
||||
resolved = findCommand(runtimePath)
|
||||
}
|
||||
if resolved == "" || !cmd.Which(resolved) {
|
||||
result.Status = "failed"
|
||||
result.Code = "dockerConfigInvalid"
|
||||
result.Reason = fmt.Sprintf("The registered NVIDIA runtime %q cannot be found or executed.", runtimePath)
|
||||
}
|
||||
return result
|
||||
}
|
||||
if hook := findCommand("nvidia-container-runtime-hook"); hook != "" && cmd.Which(hook) {
|
||||
result = checkNVIDIADockerInfo(info, true)
|
||||
}
|
||||
config, err := readConfig()
|
||||
if err != nil {
|
||||
var syntaxError *json.SyntaxError
|
||||
var typeError *json.UnmarshalTypeError
|
||||
switch {
|
||||
case os.IsNotExist(err):
|
||||
if result.Status == "unknown" {
|
||||
return result
|
||||
}
|
||||
result.Code = "dockerConfigMissing"
|
||||
result.Reason = fmt.Sprintf("Docker configuration %s is missing; configure runtimes.nvidia.path and restart Docker.", constant.DaemonJsonPath)
|
||||
case errors.As(err, &syntaxError), errors.As(err, &typeError):
|
||||
result.Status = "failed"
|
||||
result.Code = "dockerConfigInvalid"
|
||||
result.Reason = fmt.Sprintf("Cannot parse Docker configuration %s: %v", constant.DaemonJsonPath, err)
|
||||
default:
|
||||
result.Status = "failed"
|
||||
result.Code = "dockerConfigUnreadable"
|
||||
result.Reason = fmt.Sprintf("Cannot read Docker configuration %s: %v", constant.DaemonJsonPath, err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
if config == nil {
|
||||
result.Status = "failed"
|
||||
result.Code = "dockerConfigInvalid"
|
||||
result.Reason = "Docker configuration must be a JSON object."
|
||||
return result
|
||||
}
|
||||
runtimes, ok := config["runtimes"].(map[string]interface{})
|
||||
if config["runtimes"] == nil || (ok && runtimes["nvidia"] == nil) {
|
||||
if result.Status == "unknown" {
|
||||
return result
|
||||
}
|
||||
result.Code = "dockerConfigMissing"
|
||||
result.Reason = fmt.Sprintf("Docker configuration %s has no runtimes.nvidia entry.", constant.DaemonJsonPath)
|
||||
return result
|
||||
}
|
||||
nvidia, ok := runtimes["nvidia"].(map[string]interface{})
|
||||
runtimePath, pathOK := nvidia["path"].(string)
|
||||
if !ok || !pathOK || strings.TrimSpace(runtimePath) == "" || (!filepath.IsAbs(runtimePath) && filepath.Base(runtimePath) != runtimePath) {
|
||||
result.Status = "failed"
|
||||
result.Code = "dockerConfigInvalid"
|
||||
result.Reason = "runtimes.nvidia.path must contain a runtime executable name or absolute path."
|
||||
return result
|
||||
}
|
||||
resolved := findCommand(runtimePath)
|
||||
if resolved == "" || !cmd.Which(resolved) {
|
||||
result.Status = "failed"
|
||||
result.Code = "dockerConfigInvalid"
|
||||
result.Reason = fmt.Sprintf("The configured NVIDIA runtime %q cannot be found or executed.", runtimePath)
|
||||
return result
|
||||
}
|
||||
result.Status = "unconfigured"
|
||||
result.Code = "dockerConfigPending"
|
||||
result.Reason = "The default daemon.json contains runtimes.nvidia.path, but the running Docker daemon has not registered the NVIDIA runtime; confirm the daemon uses this configuration file, then restart Docker and check again."
|
||||
return result
|
||||
}
|
||||
|
||||
func summarizeGPUs(snapshot *accelerator.Snapshot) []dto.GPUSummary {
|
||||
result := []dto.GPUSummary{}
|
||||
for _, vendor := range gpuVendors {
|
||||
summary := dto.GPUSummary{Vendor: vendor}
|
||||
cards, chips := map[string]bool{}, map[string]bool{}
|
||||
var versions []string
|
||||
for _, device := range snapshot.Devices {
|
||||
deviceVendor := device.Vendor
|
||||
if device.XPU != nil {
|
||||
deviceVendor = "intel"
|
||||
}
|
||||
if deviceVendor != vendor || device.ParentID != "" || chips[device.ID] {
|
||||
continue
|
||||
}
|
||||
chips[device.ID] = true
|
||||
cardID := device.ID
|
||||
if device.NPU != nil {
|
||||
cardID = strconv.Itoa(device.NPUIndex)
|
||||
}
|
||||
cards[cardID] = true
|
||||
if device.DriverVersion != "" && !slices.Contains(versions, device.DriverVersion) {
|
||||
versions = append(versions, device.DriverVersion)
|
||||
}
|
||||
if !device.Metrics.MemoryTotal.Available() {
|
||||
summary.MemoryPartial = true
|
||||
continue
|
||||
}
|
||||
bytes := device.Metrics.MemoryTotal.ValueOrZero() * 1024 * 1024
|
||||
if device.GPU != nil && device.GPU.SharedMemory {
|
||||
if summary.SharedMemoryTotal == nil || bytes > *summary.SharedMemoryTotal {
|
||||
summary.SharedMemoryTotal = &bytes
|
||||
}
|
||||
} else {
|
||||
if summary.MemoryTotal == nil {
|
||||
summary.MemoryTotal = new(float64)
|
||||
}
|
||||
*summary.MemoryTotal += bytes
|
||||
}
|
||||
}
|
||||
if len(cards) > 0 {
|
||||
count := len(cards)
|
||||
summary.CardCount = &count
|
||||
summary.DriverVersion = strings.Join(versions, " / ")
|
||||
if vendor == "nvidia" {
|
||||
summary.CudaVersion = snapshot.Info.CudaVersion
|
||||
}
|
||||
result = append(result, summary)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -5,22 +5,22 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/vllm"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
|
||||
)
|
||||
|
||||
const (
|
||||
vllmAppKeyForUpgrade = "vllm"
|
||||
vllmGB10VersionPrefix = "nvidia-gb10-dspark-"
|
||||
vllmImageEnvKey = "IMAGE"
|
||||
vllmImageTypeNvidia = "nvidia"
|
||||
vllmImageTypeIntel = "intel"
|
||||
vllmImageTypeAscend = "ascend"
|
||||
vllmAppKeyForUpgrade = "vllm"
|
||||
vllmImageEnvKey = "IMAGE"
|
||||
vllmImageTypeNvidia = "nvidia"
|
||||
vllmImageTypeIntel = "intel"
|
||||
vllmImageTypeAscend = "ascend"
|
||||
)
|
||||
|
||||
func isVllmProOnlyVersion(appKey, version string) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(appKey), vllmAppKeyForUpgrade) &&
|
||||
strings.HasPrefix(strings.ToLower(strings.TrimSpace(version)), vllmGB10VersionPrefix)
|
||||
vllm.IsGB10Version(version)
|
||||
}
|
||||
|
||||
func canAccessVllmVersion(appKey, version string) bool {
|
||||
@@ -58,9 +58,6 @@ func resolveVllmVersionFamily(version, image string) string {
|
||||
func trimVllmVersionFamily(version string) string {
|
||||
trimmed := strings.TrimSpace(version)
|
||||
normalized := strings.ToLower(trimmed)
|
||||
if strings.HasPrefix(normalized, vllmGB10VersionPrefix) {
|
||||
return strings.TrimSpace(trimmed[len(vllmGB10VersionPrefix):])
|
||||
}
|
||||
for _, family := range []string{vllmImageTypeNvidia, vllmImageTypeIntel, vllmImageTypeAscend} {
|
||||
prefix := family + "-"
|
||||
if strings.HasPrefix(normalized, prefix) {
|
||||
@@ -71,10 +68,10 @@ func trimVllmVersionFamily(version string) string {
|
||||
}
|
||||
|
||||
func buildDefaultVllmImageByVersion(version string) string {
|
||||
tag := trimVllmVersionFamily(version)
|
||||
if strings.HasPrefix(strings.ToLower(strings.TrimSpace(version)), vllmGB10VersionPrefix) {
|
||||
return "1panel/vllm-gb10-dspark:" + tag
|
||||
if image := vllm.GB10Image(version); image != "" {
|
||||
return image
|
||||
}
|
||||
tag := trimVllmVersionFamily(version)
|
||||
family := resolveVllmVersionFamily(version, "")
|
||||
if family == vllmImageTypeIntel {
|
||||
return "intel/llm-scaler-vllm:" + tag
|
||||
@@ -89,8 +86,8 @@ func buildDefaultVllmImageByVersion(version string) string {
|
||||
}
|
||||
|
||||
func isVllmUpgradeVersionAllowed(currentVersion, targetVersion, currentImage string) bool {
|
||||
currentGB10 := strings.HasPrefix(strings.ToLower(strings.TrimSpace(currentVersion)), vllmGB10VersionPrefix)
|
||||
targetGB10 := strings.HasPrefix(strings.ToLower(strings.TrimSpace(targetVersion)), vllmGB10VersionPrefix)
|
||||
currentGB10 := vllm.IsGB10Version(currentVersion)
|
||||
targetGB10 := vllm.IsGB10Version(targetVersion)
|
||||
if currentGB10 != targetGB10 {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ func normalizeGPU(item *gpu.Device) Device {
|
||||
Vendor: item.Type,
|
||||
Index: int(item.Index),
|
||||
Name: item.ProductName,
|
||||
DriverVersion: item.DriverVersion,
|
||||
Label: fmt.Sprintf("%d - %s", item.Index, item.ProductName),
|
||||
BusID: item.BusID,
|
||||
Metrics: metrics,
|
||||
@@ -121,6 +122,7 @@ func normalizeXPU(item *xpu.Device) Device {
|
||||
Vendor: item.Basic.VendorName,
|
||||
Index: item.Basic.DeviceID,
|
||||
Name: item.Basic.DeviceName,
|
||||
DriverVersion: item.Basic.DriverVersion,
|
||||
Label: fmt.Sprintf("%d - %s", item.Basic.DeviceID, item.Basic.DeviceName),
|
||||
BusID: item.Basic.PciBdfAddress,
|
||||
Metrics: metrics,
|
||||
|
||||
@@ -56,7 +56,9 @@ func (p npuProvider) Collect(ctx context.Context) (*ProviderSnapshot, error) {
|
||||
NPUs: info.Devices,
|
||||
}
|
||||
for index := range result.NPUs {
|
||||
result.Devices = append(result.Devices, normalizeNPU(&result.NPUs[index]))
|
||||
device := normalizeNPU(&result.NPUs[index])
|
||||
device.DriverVersion = info.DriverVersion
|
||||
result.Devices = append(result.Devices, device)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -102,6 +102,7 @@ type Device struct {
|
||||
NPUIndex int
|
||||
ChipIndex int
|
||||
Name string
|
||||
DriverVersion string
|
||||
Label string
|
||||
BusID string
|
||||
Metrics Metrics
|
||||
|
||||
@@ -23,12 +23,24 @@ type providerResult struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func New() (bool, Client) {
|
||||
client := Client{}
|
||||
func ManagementCommands() map[string]string {
|
||||
commands := make(map[string]string)
|
||||
if cmd.Which(nvidiaSMICommand) {
|
||||
client.providers = append(client.providers, nvidiaSMI{})
|
||||
commands["nvidia"] = nvidiaSMICommand
|
||||
}
|
||||
if command, ok := findAMDSMI(); ok {
|
||||
commands["amd"] = command
|
||||
}
|
||||
return commands
|
||||
}
|
||||
|
||||
func New() (bool, Client) {
|
||||
client := Client{}
|
||||
commands := ManagementCommands()
|
||||
if commands["nvidia"] != "" {
|
||||
client.providers = append(client.providers, nvidiaSMI{})
|
||||
}
|
||||
if command := commands["amd"]; command != "" {
|
||||
client.providers = append(client.providers, amdSMI{command: command})
|
||||
}
|
||||
return len(client.providers) > 0, client
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package vllm
|
||||
|
||||
import "strings"
|
||||
|
||||
const (
|
||||
gb10VersionPrefix = "nvidia-"
|
||||
gb10VersionSuffix = "-optimized-for-ds-v4-flash"
|
||||
)
|
||||
|
||||
func IsGB10Version(version string) bool {
|
||||
normalized := strings.ToLower(strings.TrimSpace(version))
|
||||
tag := strings.TrimPrefix(normalized, gb10VersionPrefix)
|
||||
return tag != normalized && strings.HasSuffix(tag, gb10VersionSuffix) &&
|
||||
strings.TrimSpace(strings.TrimSuffix(tag, gb10VersionSuffix)) != ""
|
||||
}
|
||||
|
||||
func GB10Image(version string) string {
|
||||
trimmed := strings.TrimSpace(version)
|
||||
if !IsGB10Version(trimmed) {
|
||||
return ""
|
||||
}
|
||||
return "1panel/vllm-gb10-dual-node:" + strings.TrimSpace(trimmed[len(gb10VersionPrefix):])
|
||||
}
|
||||
@@ -63,6 +63,7 @@ type Info struct {
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
ReadError string `json:"-"`
|
||||
Tiles []TileStats `json:"tiles"`
|
||||
ProcessStatus string `json:"processStatus"`
|
||||
|
||||
|
||||
@@ -122,8 +122,9 @@ func (c Client) loadDeviceInfo(ctx context.Context, device discoveryDevice, wg *
|
||||
wgCmd.Wait()
|
||||
|
||||
if xpuErr != nil {
|
||||
xpu.ReadError = fmt.Sprintf("xpu-smi device %d: %v", device.DeviceID, xpuErr)
|
||||
mu.Lock()
|
||||
res.Warnings = append(res.Warnings, fmt.Sprintf("xpu-smi device %d: %v", device.DeviceID, xpuErr))
|
||||
res.Warnings = append(res.Warnings, xpu.ReadError)
|
||||
res.Devices = append(res.Devices, xpu)
|
||||
mu.Unlock()
|
||||
return
|
||||
@@ -131,8 +132,9 @@ func (c Client) loadDeviceInfo(ctx context.Context, device discoveryDevice, wg *
|
||||
|
||||
var info discoveryDevice
|
||||
if err := json.Unmarshal([]byte(xpuData), &info); err != nil {
|
||||
xpu.ReadError = fmt.Sprintf("xpu-smi device %d output: %v", device.DeviceID, err)
|
||||
mu.Lock()
|
||||
res.Warnings = append(res.Warnings, fmt.Sprintf("xpu-smi device %d output: %v", device.DeviceID, err))
|
||||
res.Warnings = append(res.Warnings, xpu.ReadError)
|
||||
res.Devices = append(res.Devices, xpu)
|
||||
mu.Unlock()
|
||||
return
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package req_helper
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func newExternalHTTPClient(transport *http.Transport, timeout time.Duration) *http.Client {
|
||||
if transport == nil {
|
||||
transport = http.DefaultTransport.(*http.Transport)
|
||||
}
|
||||
verified := transport.Clone()
|
||||
if verified.TLSClientConfig == nil {
|
||||
verified.TLSClientConfig = &tls.Config{}
|
||||
} else {
|
||||
verified.TLSClientConfig = verified.TLSClientConfig.Clone()
|
||||
}
|
||||
verified.TLSClientConfig.InsecureSkipVerify = false
|
||||
verified.TLSClientConfig.ServerName = ""
|
||||
verified.DialTLS = nil
|
||||
verified.DialTLSContext = nil
|
||||
return &http.Client{
|
||||
Timeout: timeout,
|
||||
Transport: verified,
|
||||
CheckRedirect: checkExternalRedirect,
|
||||
}
|
||||
}
|
||||
|
||||
func checkExternalRedirect(req *http.Request, via []*http.Request) error {
|
||||
if len(via) >= 10 {
|
||||
return errors.New("stopped after 10 redirects")
|
||||
}
|
||||
if len(via) > 0 && via[len(via)-1].URL.Scheme == "https" && req.URL.Scheme != "https" {
|
||||
return errors.New("refusing HTTPS redirect to an insecure scheme")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package req_helper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
@@ -21,10 +20,7 @@ import (
|
||||
)
|
||||
|
||||
func HandleGet(url string) (*http.Response, error) {
|
||||
client := &http.Client{
|
||||
Timeout: time.Second * 300,
|
||||
}
|
||||
client.Transport = loadRequestTransport()
|
||||
client := newExternalHTTPClient(loadRequestTransport(), time.Second*300)
|
||||
defer client.CloseIdleConnections()
|
||||
|
||||
req, err := http.NewRequestWithContext(context.Background(), "GET", url, nil)
|
||||
@@ -53,8 +49,9 @@ func HandleGet(url string) (*http.Response, error) {
|
||||
|
||||
func HandleRequest(url, method string, timeout int) (int, []byte, error) {
|
||||
transport := xpack.MultiNodeProvider.LoadRequestTransport()
|
||||
client := http.Client{Timeout: time.Duration(timeout) * time.Second, Transport: transport}
|
||||
return HandleRequestWithClient(&client, url, method, timeout)
|
||||
client := newExternalHTTPClient(transport, time.Duration(timeout)*time.Second)
|
||||
defer client.CloseIdleConnections()
|
||||
return HandleRequestWithClient(client, url, method, timeout)
|
||||
}
|
||||
|
||||
func HandleRequestWithClient(client *http.Client, url, method string, timeout int) (int, []byte, error) {
|
||||
@@ -136,7 +133,8 @@ func RequestFile(url, method string, timeout int) (io.ReadCloser, context.Cancel
|
||||
}
|
||||
}()
|
||||
transport := xpack.MultiNodeProvider.LoadRequestTransport()
|
||||
client := http.Client{Timeout: time.Duration(timeout) * time.Second, Transport: transport}
|
||||
client := newExternalHTTPClient(transport, time.Duration(timeout)*time.Second)
|
||||
defer client.CloseIdleConnections()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
|
||||
request, err := http.NewRequestWithContext(ctx, method, url, nil)
|
||||
if err != nil {
|
||||
@@ -156,7 +154,6 @@ func RequestFile(url, method string, timeout int) (io.ReadCloser, context.Cancel
|
||||
|
||||
func loadRequestTransport() *http.Transport {
|
||||
return &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 60 * time.Second,
|
||||
KeepAlive: 60 * time.Second,
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package req_helper
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func newExternalHTTPClient(transport *http.Transport, timeout time.Duration) *http.Client {
|
||||
if transport == nil {
|
||||
transport = http.DefaultTransport.(*http.Transport)
|
||||
}
|
||||
verified := transport.Clone()
|
||||
if verified.TLSClientConfig == nil {
|
||||
verified.TLSClientConfig = &tls.Config{}
|
||||
} else {
|
||||
verified.TLSClientConfig = verified.TLSClientConfig.Clone()
|
||||
}
|
||||
verified.TLSClientConfig.InsecureSkipVerify = false
|
||||
verified.TLSClientConfig.ServerName = ""
|
||||
verified.DialTLS = nil
|
||||
verified.DialTLSContext = nil
|
||||
return &http.Client{
|
||||
Timeout: timeout,
|
||||
Transport: verified,
|
||||
CheckRedirect: checkExternalRedirect,
|
||||
}
|
||||
}
|
||||
|
||||
func checkExternalRedirect(req *http.Request, via []*http.Request) error {
|
||||
if len(via) >= 10 {
|
||||
return errors.New("stopped after 10 redirects")
|
||||
}
|
||||
if len(via) > 0 && via[len(via)-1].URL.Scheme == "https" && req.URL.Scheme != "https" {
|
||||
return errors.New("refusing HTTPS redirect to an insecure scheme")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package req_helper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
@@ -17,7 +16,6 @@ import (
|
||||
|
||||
func HandleRequest(url, method string, timeout int) (int, []byte, error) {
|
||||
transport := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 60 * time.Second,
|
||||
KeepAlive: 60 * time.Second,
|
||||
@@ -42,7 +40,8 @@ func handleRequestWithTransport(url, method string, transport *http.Transport, t
|
||||
}
|
||||
}()
|
||||
|
||||
client := http.Client{Timeout: time.Duration(timeout) * time.Second, Transport: transport}
|
||||
client := newExternalHTTPClient(transport, time.Duration(timeout)*time.Second)
|
||||
defer client.CloseIdleConnections()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
|
||||
defer cancel()
|
||||
request, err := http.NewRequestWithContext(ctx, method, url, nil)
|
||||
@@ -65,7 +64,6 @@ func handleRequestWithTransport(url, method string, transport *http.Transport, t
|
||||
|
||||
func HandleGet(url string) (*http.Response, error) {
|
||||
transport := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 60 * time.Second,
|
||||
KeepAlive: 60 * time.Second,
|
||||
@@ -83,10 +81,7 @@ func HandleGetWithProxy(url string) (*http.Response, error) {
|
||||
}
|
||||
|
||||
func handleGetWithTransport(url string, transport *http.Transport) (*http.Response, error) {
|
||||
client := &http.Client{
|
||||
Timeout: time.Second * 300,
|
||||
}
|
||||
client.Transport = transport
|
||||
client := newExternalHTTPClient(transport, time.Second*300)
|
||||
defer client.CloseIdleConnections()
|
||||
|
||||
req, err := http.NewRequestWithContext(context.Background(), "GET", url, nil)
|
||||
|
||||
@@ -37,6 +37,28 @@ export namespace AI {
|
||||
info?: string;
|
||||
}
|
||||
|
||||
export interface GPUEnvironmentCheck {
|
||||
vendor: string;
|
||||
item: 'driver' | 'toolkit' | 'docker';
|
||||
status: 'passed' | 'missing' | 'unconfigured' | 'failed' | 'unknown';
|
||||
code?: string;
|
||||
version?: string;
|
||||
reason?: string;
|
||||
}
|
||||
export interface GPUEnvironment {
|
||||
checkedAt: string;
|
||||
hardwareStatus: 'detected' | 'not_detected' | 'unknown';
|
||||
checks: GPUEnvironmentCheck[];
|
||||
}
|
||||
export interface GPUSummary {
|
||||
vendor: string;
|
||||
driverVersion: string;
|
||||
cudaVersion: string;
|
||||
cardCount: number | null;
|
||||
memoryTotal: number | null;
|
||||
sharedMemoryTotal: number | null;
|
||||
memoryPartial: boolean;
|
||||
}
|
||||
export interface Info {
|
||||
collectedAt?: string;
|
||||
warnings?: string[];
|
||||
@@ -47,6 +69,8 @@ export namespace AI {
|
||||
npu: NPU[];
|
||||
xpuDriverVersion: string;
|
||||
xpu: XpuInfo['xpu'];
|
||||
environment?: GPUEnvironment;
|
||||
summaries?: GPUSummary[];
|
||||
}
|
||||
export interface GPU {
|
||||
sharedMemory?: boolean;
|
||||
|
||||
@@ -416,6 +416,8 @@ export namespace Container {
|
||||
isExist: boolean;
|
||||
}
|
||||
export interface DaemonJsonConf {
|
||||
nvidia: boolean;
|
||||
nvidiaSupported: boolean;
|
||||
isSwarm: boolean;
|
||||
isExist: boolean;
|
||||
isActive: boolean;
|
||||
|
||||
@@ -47,8 +47,11 @@ export const loadModelGPUDevices = () => {
|
||||
return http.get<{ devices: AI.ModelGPUDevice[]; warnings: string[] }>('/ai/model/gpu/devices');
|
||||
};
|
||||
|
||||
export const loadGPUInfo = () => {
|
||||
return http.get<AI.Info>(`/ai/gpu/load`);
|
||||
export const loadGPUInfo = (refresh = false, currentNode?: string) => {
|
||||
return http.get<AI.Info>(`/ai/gpu/load`, refresh ? { refresh: true } : {}, {
|
||||
timeout: TimeoutEnum.T_40S,
|
||||
headers: currentNode ? { CurrentNode: currentNode } : undefined,
|
||||
});
|
||||
};
|
||||
export const getGPUOptions = (currentNode?: string) => {
|
||||
return http.get<AI.MonitorGPUOptions>(
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<div class="ai-model-list" :class="{ 'ai-model-list--single': visibleCount === 1 }">
|
||||
<span v-for="(model, index) in models.slice(0, visibleCount)" :key="index" class="model-preview">
|
||||
<el-tag size="small" :type="tagType" class="model-tag">
|
||||
<span :title="model">{{ model }}</span>
|
||||
</el-tag>
|
||||
<CopyButton v-if="copyable" :content="model" />
|
||||
</span>
|
||||
<el-popover
|
||||
v-if="models.length > visibleCount"
|
||||
trigger="click"
|
||||
placement="bottom-start"
|
||||
:width="360"
|
||||
:popper-options="{ modifiers: [{ name: 'preventOverflow', options: { padding: 16, altAxis: true } }] }"
|
||||
:popper-style="{ maxWidth: 'calc(100vw - 32px)', padding: '12px' }"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button
|
||||
size="small"
|
||||
class="model-more"
|
||||
:aria-label="$t('aiTools.model.model') + ' +' + (models.length - visibleCount)"
|
||||
@click.stop
|
||||
>
|
||||
+{{ models.length - visibleCount }}
|
||||
</el-button>
|
||||
</template>
|
||||
<div class="model-popover" @click.stop>
|
||||
<div class="model-popover__header">
|
||||
<span>{{ $t('aiTools.model.model') }}</span>
|
||||
<el-tag size="small" type="info" round>{{ models.length }}</el-tag>
|
||||
</div>
|
||||
<ul class="model-popover__list" tabindex="0" :aria-label="$t('aiTools.model.model')">
|
||||
<li v-for="(model, index) in models" :key="index">
|
||||
<span class="model-popover__name">{{ model }}</span>
|
||||
<CopyButton v-if="copyable" :content="model" :aria-label="$t('commons.button.copy')" />
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="$slots.footer" class="model-popover__footer"><slot name="footer" /></div>
|
||||
</div>
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import CopyButton from '@/components/copy-button/index.vue';
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
models: string[];
|
||||
visibleCount?: number;
|
||||
copyable?: boolean;
|
||||
tagType?: 'primary' | 'success' | 'warning' | 'info' | 'danger';
|
||||
}>(),
|
||||
{ visibleCount: 3, copyable: false, tagType: 'primary' },
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ai-model-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
|
||||
&--single {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
.model-preview {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
.model-tag {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
:deep(.el-tag__content) {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
.model-more {
|
||||
flex-shrink: 0;
|
||||
margin: 0;
|
||||
padding: 0 8px;
|
||||
color: var(--el-color-primary);
|
||||
border-color: var(--el-color-primary-light-7);
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
.model-popover {
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding-bottom: 10px;
|
||||
font-weight: 500;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
&__list {
|
||||
max-height: min(320px, 50vh);
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
margin: 8px 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
li {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
line-height: 20px;
|
||||
border-radius: 4px;
|
||||
&:nth-child(odd) {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
&__name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
&__footer {
|
||||
margin-top: 8px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
:deep(.el-button) {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,57 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card class="cached-tabs">
|
||||
<el-radio-group :model-value="activeTab" @change="select">
|
||||
<el-radio-button v-for="item in items" :key="item.value" :value="item.value" size="large">
|
||||
{{ item.label }}
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-card>
|
||||
<slot :active="activeTab" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useCachedTab } from '@/composables/useCachedTab';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
items: { value: string; label: string }[];
|
||||
defaultValue: string;
|
||||
cacheKey?: string;
|
||||
requestedValue?: string;
|
||||
}>(),
|
||||
{ cacheKey: 'default' },
|
||||
);
|
||||
const emit = defineEmits<{ activate: [value: string] }>();
|
||||
const pagePath = useRoute().path;
|
||||
const { activeTab, select } = useCachedTab({
|
||||
key: () => JSON.stringify([pagePath, props.cacheKey]),
|
||||
values: () => props.items.map((item) => item.value),
|
||||
defaultValue: () => props.defaultValue,
|
||||
requestedValue: () => props.requestedValue,
|
||||
onActivate: (value) => emit('activate', value),
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.cached-tabs {
|
||||
--el-card-padding: 0;
|
||||
|
||||
:deep(.el-radio-button__inner) {
|
||||
min-width: 100px;
|
||||
height: 100%;
|
||||
background-color: var(--panel-button-active) !important;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
border: 2px solid transparent !important;
|
||||
}
|
||||
|
||||
:deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
|
||||
color: $primary-color;
|
||||
border-color: $primary-color !important;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<DialogPro v-model="submitVisible" :title="header" size="mini">
|
||||
<DialogPro v-model="submitVisible" :title="header" size="mini" @close="onCancel">
|
||||
<div>
|
||||
<span v-if="operationInfo" class="operation-info">{{ operationInfo }}</span>
|
||||
<slot />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-tooltip placement="top-start">
|
||||
<el-tooltip v-bind="TABLE_TOOLTIP_OPTIONS">
|
||||
<template #content>
|
||||
<div class="info-break" :style="{ width: width + 'px' }">{{ info }}</div>
|
||||
</template>
|
||||
@@ -7,6 +7,8 @@
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { TABLE_TOOLTIP_OPTIONS } from '@/components/table/shared';
|
||||
|
||||
defineProps({
|
||||
width: {
|
||||
type: String,
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
<el-radio-group v-model="activeName" @change="handleChange">
|
||||
<el-radio-button
|
||||
class="router_card_button"
|
||||
:class="{ 'opacity-50': button.disabled }"
|
||||
:label="button.label"
|
||||
:value="button.label"
|
||||
:disabled="button.disabled"
|
||||
v-for="(button, index) in buttonArray"
|
||||
size="large"
|
||||
:key="index"
|
||||
@@ -50,7 +52,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { openPage } from '@/utils/router';
|
||||
import { routerToNameWithQuery, routerToPathWithQuery } from '@/utils/router';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { hasPermissionMetaAccess, hasRouteAccess } from '@/utils/rbac';
|
||||
@@ -94,10 +96,13 @@ const expiresAlertKey = computed(() => (isEnterprise.value ? 'xpack.expiresEnter
|
||||
|
||||
const handleChange = async (label: string) => {
|
||||
const btn = buttonArray.value.find((btn) => btn.label === label);
|
||||
if (!btn) return;
|
||||
if (!btn || btn.disabled) {
|
||||
syncActiveName();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (btn.path) await openPage({ path: btn.path });
|
||||
else if (btn.name) await openPage({ name: btn.name });
|
||||
if (btn.path) await routerToPathWithQuery(btn.path, { uncached: 'true' });
|
||||
else if (btn.name) await routerToNameWithQuery(btn.name, { uncached: 'true' });
|
||||
} finally {
|
||||
syncActiveName();
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
:aria-disabled="isDisabled"
|
||||
@click.capture="handlePermissionClick"
|
||||
>
|
||||
<el-tooltip v-if="msg && msg != ''" effect="dark" placement="bottom">
|
||||
<el-tooltip v-if="msg && msg != ''" effect="dark" v-bind="TABLE_TOOLTIP_OPTIONS">
|
||||
<template #content>
|
||||
<div class="content">{{ msg }}</div>
|
||||
</template>
|
||||
@@ -40,6 +40,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { TABLE_TOOLTIP_OPTIONS } from '@/components/table/shared';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
@@ -5,36 +5,21 @@
|
||||
clearable
|
||||
v-model="searchInfo"
|
||||
@clear="search()"
|
||||
@keydown.enter.stop.prevent="search()"
|
||||
@blur="searchIfChanged()"
|
||||
suffix-icon="Search"
|
||||
@change="search()"
|
||||
:disabled="props.disabled"
|
||||
:placeholder="props.placeholder || $t('commons.button.search')"
|
||||
>
|
||||
<template #suffix>
|
||||
<button
|
||||
type="button"
|
||||
class="search-submit"
|
||||
:disabled="props.disabled"
|
||||
:aria-label="$t('commons.button.search')"
|
||||
@mousedown.prevent
|
||||
@click="search()"
|
||||
>
|
||||
<el-icon><Search /></el-icon>
|
||||
</button>
|
||||
</template>
|
||||
</el-input>
|
||||
></el-input>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onActivated, ref, watch } from 'vue';
|
||||
import { Search } from '@element-plus/icons-vue';
|
||||
defineOptions({ name: 'TableSearch' });
|
||||
|
||||
const emit = defineEmits(['search', 'update:searchName']);
|
||||
const searchInfo = ref();
|
||||
let lastSearch: string | number | undefined;
|
||||
const props = defineProps({
|
||||
placeholder: String,
|
||||
disabled: {
|
||||
@@ -52,7 +37,6 @@ watch(
|
||||
(newVal) => {
|
||||
if (searchInfo.value !== newVal) {
|
||||
searchInfo.value = newVal;
|
||||
lastSearch = newVal;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
@@ -63,28 +47,7 @@ onActivated(() => {
|
||||
});
|
||||
|
||||
const search = () => {
|
||||
if (props.disabled) return;
|
||||
lastSearch = searchInfo.value;
|
||||
emit('update:searchName', searchInfo.value);
|
||||
emit('search');
|
||||
};
|
||||
|
||||
const searchIfChanged = () => {
|
||||
if (searchInfo.value !== lastSearch) search();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search-submit {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.search-submit:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -170,13 +170,11 @@
|
||||
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';
|
||||
import { flattenVNodes, TABLE_OVERFLOW_TOOLTIP_OPTIONS, type FuTableOperationButton } from '@/components/table/shared';
|
||||
import { useCardColumns } from './useCardColumns';
|
||||
import { useContextMenu } from './useContextMenu';
|
||||
import { useResponsivePagination } from './useResponsivePagination';
|
||||
import { useTableSelection } from './useTableSelection';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { usePaginationCache } from './usePaginationCache';
|
||||
const slots = useSlots();
|
||||
const attrs = useAttrs();
|
||||
const { isMobile, openMenuTabs, currentNode } = useGlobalStore();
|
||||
@@ -189,16 +187,11 @@ type ViewMode = 'table' | 'card';
|
||||
const props = defineProps({
|
||||
header: String,
|
||||
data: { type: Array as PropType<any[]>, default: () => [] },
|
||||
loading: { type: Boolean, default: false },
|
||||
selectionContext: { type: [String, Number, Array, Object, Function], default: undefined },
|
||||
paginationConfig: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
cachePagination: { type: Boolean, default: undefined },
|
||||
refreshOnRestore: { type: Boolean, default: true },
|
||||
cacheId: String,
|
||||
cacheScope: [String, Number],
|
||||
pageSizes: {
|
||||
type: Array as PropType<number[]>,
|
||||
default: () => [5, 10, 20, 50, 100, 200, 500],
|
||||
@@ -224,15 +217,6 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['search', 'update:selects', 'update:paginationConfig', 'update:viewMode']);
|
||||
usePaginationCache({
|
||||
pagePath: useRoute().path,
|
||||
pagination: () => props.paginationConfig as { currentPage: number; pageSize: number; cacheSizeKey?: string },
|
||||
enabled: () => props.cachePagination,
|
||||
id: () => props.cacheId,
|
||||
scope: () => props.cacheScope,
|
||||
search: () => emit('search'),
|
||||
refreshOnRestore: () => props.refreshOnRestore,
|
||||
});
|
||||
const tableRef = ref();
|
||||
const complexTableRef = ref<HTMLElement>();
|
||||
const tableHeight = ref<number | string>('');
|
||||
@@ -290,7 +274,7 @@ const scheduleCardContentHeightSync = () => {
|
||||
});
|
||||
};
|
||||
const resolvedTooltipOptions = computed(() => ({
|
||||
placement: 'bottom-start',
|
||||
...TABLE_OVERFLOW_TOOLTIP_OPTIONS,
|
||||
...((attrs.tooltipOptions ?? attrs['tooltip-options']) as Record<string, unknown> | undefined),
|
||||
}));
|
||||
const getRowKey = (row: any, index: number) => {
|
||||
@@ -359,10 +343,10 @@ const handleRightClick = (row, column, event) => {
|
||||
};
|
||||
|
||||
watch(
|
||||
() => [props.data, props.loading],
|
||||
() => props.data,
|
||||
() => {
|
||||
const pagination = props.paginationConfig;
|
||||
if (props.loading || !pagination || !pagination.pageSize) return;
|
||||
if (!pagination || !pagination.pageSize) return;
|
||||
const lastPage = Math.max(1, Math.ceil(pagination.total / pagination.pageSize));
|
||||
if (pagination.currentPage > lastPage) {
|
||||
pagination.currentPage = lastPage;
|
||||
@@ -377,7 +361,7 @@ function currentChange() {
|
||||
}
|
||||
|
||||
function sizeChange() {
|
||||
if (props.cachePagination === false && props.paginationConfig.cacheSizeKey) {
|
||||
if (props.paginationConfig.cacheSizeKey) {
|
||||
writePanelCache(
|
||||
'pagination',
|
||||
panelCacheKey(`size:${props.paginationConfig.cacheSizeKey}`),
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import { nextTick, onActivated, onDeactivated, onScopeDispose, watch } from 'vue';
|
||||
import GlobalStore from '@/store/modules/global';
|
||||
import { panelCacheKey, readPanelCache, writePanelCache } from '@/utils/panel-cache';
|
||||
|
||||
interface Pagination {
|
||||
currentPage: number;
|
||||
pageSize: number;
|
||||
cacheSizeKey?: string;
|
||||
}
|
||||
interface Options {
|
||||
pagePath: string;
|
||||
pagination: () => Pagination | undefined;
|
||||
enabled: () => boolean | undefined;
|
||||
id: () => string | undefined;
|
||||
scope: () => string | number | undefined;
|
||||
search: () => void;
|
||||
refreshOnRestore?: () => boolean;
|
||||
}
|
||||
|
||||
/** Restore during child setup, before the owning page's onMounted query. */
|
||||
export const usePaginationCache = (options: Options) => {
|
||||
const store = GlobalStore();
|
||||
const defaults = new WeakMap<Pagination, { currentPage: number; pageSize: number }>();
|
||||
let active = true;
|
||||
let restoring = false;
|
||||
let activeKey: string | undefined;
|
||||
let queued = false;
|
||||
let disposed = false;
|
||||
let pending: { key: string; currentPage: number; pageSize: number } | undefined;
|
||||
let writeQueued = false;
|
||||
const flush = () => {
|
||||
if (!pending) return;
|
||||
const { key: target, currentPage, pageSize } = pending;
|
||||
pending = undefined;
|
||||
writePanelCache('pagination', target, { currentPage, pageSize });
|
||||
};
|
||||
const requestSearch = () => {
|
||||
if (queued || !key()) return;
|
||||
queued = true;
|
||||
void nextTick(() => {
|
||||
queued = false;
|
||||
if (active && !disposed && key() && options.refreshOnRestore?.() !== false) options.search();
|
||||
});
|
||||
};
|
||||
const key = () => {
|
||||
const pagination = options.pagination();
|
||||
if (options.enabled() === false || !pagination || !store.cacheUserID) return undefined;
|
||||
return panelCacheKey(
|
||||
JSON.stringify([
|
||||
'table-pagination',
|
||||
options.pagePath,
|
||||
options.id() ?? pagination.cacheSizeKey ?? 'default',
|
||||
options.scope() ?? '',
|
||||
]),
|
||||
);
|
||||
};
|
||||
const positiveInteger = (value: unknown): value is number => Number.isInteger(value) && Number(value) > 0;
|
||||
const restore = () => {
|
||||
flush();
|
||||
activeKey = key();
|
||||
const pagination = options.pagination();
|
||||
if (!activeKey || !pagination) return false;
|
||||
if (!defaults.has(pagination)) {
|
||||
defaults.set(pagination, { currentPage: pagination.currentPage, pageSize: pagination.pageSize });
|
||||
}
|
||||
const initial = defaults.get(pagination)!;
|
||||
const saved = readPanelCache<Partial<Pagination>>('pagination', activeKey);
|
||||
const page = positiveInteger(saved?.currentPage) ? saved.currentPage : initial.currentPage;
|
||||
const size = positiveInteger(saved?.pageSize) ? saved.pageSize : initial.pageSize;
|
||||
const changed = pagination.currentPage !== page || pagination.pageSize !== size;
|
||||
restoring = true;
|
||||
try {
|
||||
pagination.pageSize = size;
|
||||
pagination.currentPage = page;
|
||||
} finally {
|
||||
restoring = false;
|
||||
}
|
||||
return changed;
|
||||
};
|
||||
// The initial restore deliberately does not emit: the parent owns its first query.
|
||||
restore();
|
||||
watch(
|
||||
() => [key(), options.pagination(), store.panelCache.pagination, store.panelCache.duration],
|
||||
() => {
|
||||
if (!active) return;
|
||||
const previousKey = activeKey;
|
||||
const changed = restore();
|
||||
if (activeKey && (changed || previousKey !== activeKey)) requestSearch();
|
||||
},
|
||||
{ flush: 'sync' },
|
||||
);
|
||||
watch(
|
||||
() => [options.pagination()?.currentPage, options.pagination()?.pageSize],
|
||||
([currentPage, pageSize]) => {
|
||||
// Never write old-page state under a newly selected node/account.
|
||||
if (!active || restoring || !activeKey || activeKey !== key()) return;
|
||||
if (!positiveInteger(currentPage) || !positiveInteger(pageSize)) return;
|
||||
pending = { key: activeKey, currentPage, pageSize };
|
||||
if (!writeQueued) {
|
||||
writeQueued = true;
|
||||
queueMicrotask(() => {
|
||||
writeQueued = false;
|
||||
flush();
|
||||
});
|
||||
}
|
||||
},
|
||||
{ flush: 'sync' },
|
||||
);
|
||||
onDeactivated(() => {
|
||||
flush();
|
||||
active = false;
|
||||
});
|
||||
onActivated(() => {
|
||||
if (active) return;
|
||||
active = true;
|
||||
restore();
|
||||
requestSearch();
|
||||
});
|
||||
onScopeDispose(() => {
|
||||
flush();
|
||||
disposed = true;
|
||||
active = false;
|
||||
});
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { VNode } from 'vue';
|
||||
import type { UseTooltipProps } from 'element-plus';
|
||||
import { hasManagePermissionAccess, hasPermissionAccess, type PermissionBindingValue } from '@/utils/permission';
|
||||
|
||||
export { flattenVNodes } from '@/components/shared/vnode';
|
||||
@@ -28,6 +29,12 @@ export interface FuTableOperationButton<Row = any> {
|
||||
|
||||
export const FU_TABLE_STORAGE_PREFIX = 'FU-T-';
|
||||
|
||||
export const TABLE_TOOLTIP_OPTIONS: Pick<UseTooltipProps, 'placement'> = {
|
||||
placement: 'bottom',
|
||||
};
|
||||
|
||||
export const TABLE_OVERFLOW_TOOLTIP_OPTIONS = TABLE_TOOLTIP_OPTIONS;
|
||||
|
||||
export const isElTableColumnVNode = (vnode: VNode) => {
|
||||
const type = vnode.type as any;
|
||||
return type === 'el-table-column' || type?.name === 'ElTableColumn' || type?.__name === 'ElTableColumn';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="tooltip-container">
|
||||
<el-tooltip :disabled="showTooltip">
|
||||
<el-tooltip :disabled="showTooltip" v-bind="TABLE_TOOLTIP_OPTIONS">
|
||||
<template #content>
|
||||
<div :style="{ width: tootipWidth, 'word-break': 'break-all' }">{{ text }}</div>
|
||||
</template>
|
||||
@@ -13,6 +13,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { TABLE_TOOLTIP_OPTIONS } from '@/components/table/shared';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
defineOptions({ name: 'Tooltip' });
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import { nextTick, onActivated, onDeactivated, onMounted, onScopeDispose, readonly, ref, watch } from 'vue';
|
||||
import GlobalStore from '@/store/modules/global';
|
||||
import { panelCacheKey, readPanelCache, writePanelCache } from '@/utils/panel-cache';
|
||||
|
||||
interface CachedTabOptions {
|
||||
key: () => string;
|
||||
values: () => string[];
|
||||
defaultValue: () => string;
|
||||
requestedValue?: () => string | undefined;
|
||||
onActivate: (value: string) => unknown;
|
||||
}
|
||||
|
||||
export const useCachedTab = (options: CachedTabOptions) => {
|
||||
const store = GlobalStore();
|
||||
const activeTab = ref('');
|
||||
let active = true;
|
||||
let mounted = false;
|
||||
let version = 0;
|
||||
let restoredKey: string | undefined;
|
||||
const key = () => panelCacheKey(`inline-tabs:${options.key()}`);
|
||||
const valid = (value: unknown): value is string => typeof value === 'string' && options.values().includes(value);
|
||||
const restore = (useRequested = true) => {
|
||||
const requested = useRequested ? options.requestedValue?.() : undefined;
|
||||
const saved = readPanelCache<string>('tabs', key());
|
||||
restoredKey = key();
|
||||
activeTab.value = valid(requested)
|
||||
? requested
|
||||
: valid(saved)
|
||||
? saved
|
||||
: valid(options.defaultValue())
|
||||
? options.defaultValue()
|
||||
: options.values()[0] || '';
|
||||
};
|
||||
const activate = async () => {
|
||||
const current = ++version;
|
||||
await nextTick();
|
||||
if (current !== version || !active || !mounted || !valid(activeTab.value)) return;
|
||||
options.onActivate(activeTab.value);
|
||||
};
|
||||
const select = (value: string) => {
|
||||
if (!active || !valid(value) || value === activeTab.value) return;
|
||||
activeTab.value = value;
|
||||
writePanelCache('tabs', key(), value);
|
||||
void activate();
|
||||
};
|
||||
watch(
|
||||
[
|
||||
key,
|
||||
() => store.panelCache.tabs,
|
||||
() => store.panelCache.duration,
|
||||
() => JSON.stringify(options.values()),
|
||||
options.defaultValue,
|
||||
() => options.requestedValue?.(),
|
||||
],
|
||||
(values, previous) => {
|
||||
if (!active) return;
|
||||
// Reflecting the selected tab into the URL is not another selection.
|
||||
if (
|
||||
previous.length &&
|
||||
values.slice(0, 5).every((value, index) => value === previous[index]) &&
|
||||
values[5] === activeTab.value
|
||||
)
|
||||
return;
|
||||
// A URL left over from the previous node/account must not override its cache.
|
||||
restore(!previous.length || values[0] === previous[0]);
|
||||
if (mounted) void activate();
|
||||
},
|
||||
{ immediate: true, flush: 'sync' },
|
||||
);
|
||||
onMounted(() => {
|
||||
mounted = true;
|
||||
void activate();
|
||||
});
|
||||
onDeactivated(() => {
|
||||
active = false;
|
||||
version++;
|
||||
});
|
||||
onActivated(() => {
|
||||
if (active) return;
|
||||
active = true;
|
||||
restore(restoredKey === key());
|
||||
void activate();
|
||||
});
|
||||
onScopeDispose(() => {
|
||||
active = false;
|
||||
version++;
|
||||
});
|
||||
return { activeTab: readonly(activeTab), select };
|
||||
};
|
||||
@@ -1743,8 +1743,8 @@ const message = {
|
||||
running: 'Running requests',
|
||||
waiting: 'Waiting requests',
|
||||
cacheUsage: 'KV cache usage',
|
||||
promptThroughput: 'Input token throughput',
|
||||
generationThroughput: 'Output token throughput',
|
||||
promptThroughput: 'Input token rate',
|
||||
generationThroughput: 'Output token rate',
|
||||
requestThroughput: 'Completed requests',
|
||||
timeToFirstToken: 'Mean time to first token',
|
||||
timePerOutputToken: 'Mean inter-token latency',
|
||||
@@ -1764,6 +1764,74 @@ const message = {
|
||||
timePerOutputTokenChart: 'Inter-token latency',
|
||||
},
|
||||
gpu: {
|
||||
environment: {
|
||||
hostGPU: 'Host GPU',
|
||||
containerGPU: 'Container GPU support',
|
||||
monitorAvailable: 'Monitoring available',
|
||||
monitorPartial: 'Partial monitoring',
|
||||
notReady: 'Not ready',
|
||||
ready: 'Ready',
|
||||
checkedAt: 'Checked at',
|
||||
checkDetailsHelp: 'Review the check details, verify the environment, and check again.',
|
||||
driverActionTitle: 'Check the host GPU driver first',
|
||||
driverActionHelp:
|
||||
'Install or repair the affected device driver before continuing with the container environment checks.',
|
||||
driverGuide: 'View driver installation guide',
|
||||
toolkitActionTitle: 'Container GPU support is not ready',
|
||||
toolkitActionHelp: 'Install or repair NVIDIA Container Toolkit, then check the Docker configuration.',
|
||||
toolkitGuide: 'View Toolkit installation guide',
|
||||
dockerActionTitle: 'Docker NVIDIA GPU support is not ready',
|
||||
dockerActionHelp: 'Check the NVIDIA runtime configuration in Docker, then check again.',
|
||||
monitorUnaffected: 'Host GPU monitoring is unaffected by this issue.',
|
||||
title: 'Environment checks',
|
||||
passedLabel: 'Checks passed',
|
||||
issuesLabel: 'Environment issues · {count}',
|
||||
loadFailedLabel: 'Check failed',
|
||||
notDetectedLabel: 'No devices detected',
|
||||
unknownLabel: 'Status unknown',
|
||||
passed: 'Environment checks passed',
|
||||
issues: 'Detected {count} environment issues. Follow the guidance to check them.',
|
||||
recheck: 'Check again',
|
||||
installGuide: 'View installation guide',
|
||||
goContainerSettings: 'Open Container Settings',
|
||||
details: 'Check details',
|
||||
vendor: 'Vendor',
|
||||
gpuCount: 'GPU count',
|
||||
cardCount: 'Card count',
|
||||
notDetected: 'No supported GPU/NPU/XPU devices were detected on the current node.',
|
||||
unknown: 'Could not confirm device status. Review the check details and try again.',
|
||||
loadFailed: 'Could not read device information. Please try again.',
|
||||
notApplicable: 'Not applicable',
|
||||
cudaHelper:
|
||||
'This is the CUDA version supported by the driver, not the CUDA Toolkit version installed on the host.',
|
||||
partialMemory: 'Some memory data is unavailable',
|
||||
sharedMemory: 'Shared system memory: {memory}',
|
||||
driver: 'Driver and device management tools',
|
||||
nvidiaDriver: 'NVIDIA Driver',
|
||||
toolkit: 'NVIDIA Container Toolkit',
|
||||
docker: 'Docker NVIDIA GPU configuration',
|
||||
statusPassed: 'Normal',
|
||||
statusMissing: 'Not detected',
|
||||
statusUnconfigured: 'Configuration required',
|
||||
statusFailed: 'Check failed',
|
||||
statusUnknown: 'Unknown',
|
||||
driverMissing:
|
||||
'The {vendor} driver was not detected. Install or check the driver (detection tool: {tool}).',
|
||||
driverFailed: 'Could not read device information using {tool}. Check the driver status.',
|
||||
driverUnknown: 'Could not confirm the device driver status. Check {tool}.',
|
||||
toolkitMissing:
|
||||
'NVIDIA Container Toolkit was not detected. Install and configure it before using NVIDIA GPUs in containers.',
|
||||
toolkitFailed: 'Could not check NVIDIA Container Toolkit. Review the failure details.',
|
||||
dockerConfigMissing:
|
||||
'No runtimes.nvidia entry was detected in the default Docker configuration file. Complete the configuration using the documentation.',
|
||||
dockerConfigInvalid:
|
||||
'The NVIDIA runtime configuration in Docker is invalid. Check runtimes.nvidia.path in daemon.json.',
|
||||
dockerConfigPending:
|
||||
'The default configuration file contains NVIDIA runtime configuration, but the current Docker daemon has not loaded it. Confirm the configuration file, restart Docker, and check again.',
|
||||
dockerConfigUnreadable: 'Could not read the Docker configuration file. Review the check details.',
|
||||
dockerUnconfigured: 'Docker NVIDIA GPU configuration is incomplete.',
|
||||
dockerFailed: 'Could not check the Docker NVIDIA GPU configuration. Review the failure details.',
|
||||
},
|
||||
systemMemory: 'Shared system memory',
|
||||
systemMemoryUsed: 'System memory used',
|
||||
systemMemoryTotal: 'Total system memory',
|
||||
@@ -1823,6 +1891,8 @@ const message = {
|
||||
pcieWidth: 'PCIe width (current / maximum)',
|
||||
gpu: 'GPU Management',
|
||||
monitoringTitle: 'GPU Monitoring',
|
||||
currentHelper:
|
||||
'View real-time status and resource usage of GPU/NPU/XPU devices on the current node. The overview summarizes all devices; detailed metrics are shown per device.',
|
||||
gpuHelper: 'No supported GPU/XPU/NPU management command was detected. Please check and try again!',
|
||||
process: 'Process Information',
|
||||
processCount: 'Process Count',
|
||||
@@ -2072,6 +2142,8 @@ const message = {
|
||||
statusUnavailable: 'Service status unavailable. Refresh to retry.',
|
||||
},
|
||||
benchmarkPlan: {
|
||||
logAutoRefresh: 'Auto refresh',
|
||||
logWindowHelp: 'Only recent log output is shown. Download the selected log file for its full contents.',
|
||||
prefixTokens: 'Prefix tokens',
|
||||
seed: 'Random seed',
|
||||
report: 'Report',
|
||||
@@ -2285,6 +2357,12 @@ const message = {
|
||||
missing: 'Missing',
|
||||
},
|
||||
container: {
|
||||
nvidiaSupport: 'NVIDIA GPU support',
|
||||
nvidiaHelper:
|
||||
'Register the NVIDIA container runtime. Install NVIDIA Container Toolkit first. Assign GPUs to each container separately.',
|
||||
nvidiaLocalOnly: 'Only the local default Docker socket is supported.',
|
||||
nvidiaChangeHelper:
|
||||
'Disabling may prevent containers using the NVIDIA runtime from starting. This switch does not block GPU access.',
|
||||
imageRemoteSearch: 'Remote search',
|
||||
imageRemoteSearchHelper: 'Only Docker Hub repository images can be searched',
|
||||
imageRemoteSearchProxyRequired:
|
||||
|
||||
@@ -1754,8 +1754,8 @@ const message = {
|
||||
running: 'Solicitudes en ejecución',
|
||||
waiting: 'Solicitudes en espera',
|
||||
cacheUsage: 'Uso de caché KV',
|
||||
promptThroughput: 'Rendimiento de tokens de entrada',
|
||||
generationThroughput: 'Rendimiento de tokens de salida',
|
||||
promptThroughput: 'Tasa de tokens de entrada',
|
||||
generationThroughput: 'Tasa de tokens de salida',
|
||||
requestThroughput: 'Solicitudes completadas',
|
||||
timeToFirstToken: 'Latencia media del primer token',
|
||||
timePerOutputToken: 'Latencia media entre tokens',
|
||||
@@ -1776,6 +1776,79 @@ const message = {
|
||||
timePerOutputTokenChart: 'Latencia entre tokens',
|
||||
},
|
||||
gpu: {
|
||||
environment: {
|
||||
hostGPU: 'GPU del host',
|
||||
containerGPU: 'Compatibilidad GPU en contenedores',
|
||||
monitorAvailable: 'Supervisión disponible',
|
||||
monitorPartial: 'Supervisión parcial',
|
||||
notReady: 'No está listo',
|
||||
ready: 'Listo',
|
||||
checkedAt: 'Última comprobación',
|
||||
checkDetailsHelp: 'Revisa los detalles, verifica el entorno y vuelve a comprobarlo.',
|
||||
driverActionTitle: 'Comprueba primero el controlador GPU del host',
|
||||
driverActionHelp:
|
||||
'Instala o repara el controlador del dispositivo afectado antes de continuar con las comprobaciones del entorno de contenedores.',
|
||||
driverGuide: 'Ver guía de instalación del controlador',
|
||||
toolkitActionTitle: 'La compatibilidad GPU en contenedores no está lista',
|
||||
toolkitActionHelp:
|
||||
'Instala o repara NVIDIA Container Toolkit y después comprueba la configuración de Docker.',
|
||||
toolkitGuide: 'Ver guía de instalación de Toolkit',
|
||||
dockerActionTitle: 'La compatibilidad NVIDIA GPU de Docker no está lista',
|
||||
dockerActionHelp:
|
||||
'Comprueba la configuración del runtime NVIDIA en Docker y vuelve a realizar la comprobación.',
|
||||
monitorUnaffected: 'Este problema no afecta a la supervisión de la GPU del host.',
|
||||
title: 'Comprobaciones del entorno',
|
||||
passedLabel: 'Comprobación correcta',
|
||||
issuesLabel: 'Problemas del entorno · {count}',
|
||||
loadFailedLabel: 'Comprobación fallida',
|
||||
notDetectedLabel: 'Sin dispositivos detectados',
|
||||
unknownLabel: 'Estado desconocido',
|
||||
passed: 'Comprobaciones del entorno superadas',
|
||||
issues: 'Problemas del entorno detectados: {count}. Siga las indicaciones para revisarlos.',
|
||||
recheck: 'Comprobar de nuevo',
|
||||
installGuide: 'Ver guía de instalación',
|
||||
goContainerSettings: 'Abrir configuración de contenedores',
|
||||
details: 'Detalles de la comprobación',
|
||||
vendor: 'Fabricante',
|
||||
gpuCount: 'Cantidad de GPU',
|
||||
cardCount: 'Número de tarjetas',
|
||||
notDetected: 'No se detectaron dispositivos GPU/NPU/XPU compatibles en el nodo actual.',
|
||||
unknown: 'No se pudo confirmar el estado del dispositivo. Revise los detalles y vuelva a comprobarlo.',
|
||||
loadFailed: 'No se pudo leer la información del dispositivo. Inténtelo de nuevo.',
|
||||
notApplicable: 'No aplicable',
|
||||
cudaHelper:
|
||||
'Esta es la versión de CUDA compatible con el controlador, no la versión de CUDA Toolkit instalada en el host.',
|
||||
partialMemory: 'Algunos datos de memoria no están disponibles',
|
||||
sharedMemory: 'Memoria del sistema compartida: {memory}',
|
||||
driver: 'Controlador y herramientas de gestión de dispositivos',
|
||||
nvidiaDriver: 'Controlador NVIDIA',
|
||||
toolkit: 'NVIDIA Container Toolkit',
|
||||
docker: 'Configuración de GPU NVIDIA en Docker',
|
||||
statusPassed: 'Normal',
|
||||
statusMissing: 'No detectado',
|
||||
statusUnconfigured: 'Configuración pendiente',
|
||||
statusFailed: 'Error de comprobación',
|
||||
statusUnknown: 'Desconocido',
|
||||
driverMissing:
|
||||
'No se detectó el controlador de {vendor}. Instale o compruebe el controlador (herramienta de detección: {tool}).',
|
||||
driverFailed:
|
||||
'No se pudo leer la información del dispositivo mediante {tool}. Compruebe el estado del controlador.',
|
||||
driverUnknown: 'No se pudo confirmar el estado del controlador del dispositivo. Compruebe {tool}.',
|
||||
toolkitMissing:
|
||||
'No se detectó NVIDIA Container Toolkit. Instálelo y configúrelo antes de usar GPU NVIDIA en contenedores.',
|
||||
toolkitFailed: 'No se pudo comprobar NVIDIA Container Toolkit. Revise los detalles del error.',
|
||||
dockerConfigMissing:
|
||||
'No se detectó la entrada runtimes.nvidia en el archivo de configuración predeterminado de Docker. Complete la configuración siguiendo la documentación.',
|
||||
dockerConfigInvalid:
|
||||
'La configuración del entorno de ejecución de NVIDIA en Docker no es válida. Compruebe runtimes.nvidia.path en daemon.json.',
|
||||
dockerConfigPending:
|
||||
'El archivo de configuración predeterminado contiene la configuración del entorno de ejecución de NVIDIA, pero el servicio Docker actual aún no la ha cargado. Confirme el archivo de configuración, reinicie Docker y vuelva a comprobarlo.',
|
||||
dockerConfigUnreadable:
|
||||
'No se pudo leer el archivo de configuración de Docker. Revise los detalles de la comprobación.',
|
||||
dockerUnconfigured: 'La configuración de GPU NVIDIA en Docker está incompleta.',
|
||||
dockerFailed:
|
||||
'No se pudo comprobar la configuración de GPU NVIDIA en Docker. Revise los detalles del error.',
|
||||
},
|
||||
systemMemory: 'Memoria compartida del sistema',
|
||||
systemMemoryUsed: 'Memoria del sistema utilizada',
|
||||
systemMemoryTotal: 'Memoria total del sistema',
|
||||
@@ -1836,6 +1909,8 @@ const message = {
|
||||
pcieWidth: 'Ancho PCIe (actual / máximo)',
|
||||
gpu: 'Gestión de GPU',
|
||||
monitoringTitle: 'Monitoreo de GPU',
|
||||
currentHelper:
|
||||
'Consulte el estado en tiempo real y el uso de recursos de los dispositivos GPU/NPU/XPU del nodo actual. El resumen superior incluye todos los dispositivos; las métricas detalladas se muestran por dispositivo.',
|
||||
gpuHelper:
|
||||
'No se detectó ningún comando de administración de GPU/XPU/NPU compatible. ¡Compruebe e inténtelo de nuevo!',
|
||||
process: 'Información del Proceso',
|
||||
@@ -2092,6 +2167,9 @@ const message = {
|
||||
statusUnavailable: 'Service status unavailable. Refresh to retry.',
|
||||
},
|
||||
benchmarkPlan: {
|
||||
logAutoRefresh: 'Actualización automática',
|
||||
logWindowHelp:
|
||||
'Solo se muestran los registros recientes. Descargue el archivo seleccionado para ver todo el contenido.',
|
||||
prefixTokens: 'Tokens de prefijo',
|
||||
seed: 'Semilla aleatoria',
|
||||
report: 'Informe',
|
||||
@@ -2313,6 +2391,12 @@ const message = {
|
||||
missing: 'No existe',
|
||||
},
|
||||
container: {
|
||||
nvidiaSupport: 'Compatibilidad con GPU NVIDIA',
|
||||
nvidiaHelper:
|
||||
'Registra el entorno de ejecución de NVIDIA. Instala primero NVIDIA Container Toolkit. Asigna las GPU a cada contenedor por separado.',
|
||||
nvidiaLocalOnly: 'Solo se admite el socket predeterminado de Docker local.',
|
||||
nvidiaChangeHelper:
|
||||
'Al desactivar, los contenedores que usan el entorno de NVIDIA podrían no iniciarse. Este interruptor no bloquea el acceso a la GPU.',
|
||||
imageRemoteSearch: 'Búsqueda remota',
|
||||
imageRemoteSearchHelper: 'Solo se pueden buscar imágenes de repositorios de Docker Hub',
|
||||
imageRemoteSearchProxyRequired:
|
||||
|
||||
@@ -1719,8 +1719,8 @@ const message = {
|
||||
running: 'درخواستهای در حال اجرا',
|
||||
waiting: 'درخواستهای در انتظار',
|
||||
cacheUsage: 'مصرف حافظه نهان KV',
|
||||
promptThroughput: 'گذردهی توکن ورودی',
|
||||
generationThroughput: 'گذردهی توکن خروجی',
|
||||
promptThroughput: 'نرخ توکن ورودی',
|
||||
generationThroughput: 'نرخ توکن خروجی',
|
||||
requestThroughput: 'نرخ درخواستهای تکمیلشده',
|
||||
timeToFirstToken: 'میانگین تأخیر توکن اول',
|
||||
timePerOutputToken: 'میانگین تأخیر بین توکنها',
|
||||
@@ -1740,6 +1740,72 @@ const message = {
|
||||
timePerOutputTokenChart: 'تأخیر بین توکنها',
|
||||
},
|
||||
gpu: {
|
||||
environment: {
|
||||
hostGPU: 'GPU میزبان',
|
||||
containerGPU: 'پشتیبانی GPU در کانتینر',
|
||||
monitorAvailable: 'پایش در دسترس است',
|
||||
monitorPartial: 'پایش جزئی',
|
||||
notReady: 'آماده نیست',
|
||||
ready: 'آماده',
|
||||
checkedAt: 'زمان بررسی',
|
||||
checkDetailsHelp: 'جزئیات بررسی را ببینید، وضعیت محیط را تأیید کنید و دوباره بررسی کنید.',
|
||||
driverActionTitle: 'ابتدا درایور GPU میزبان را بررسی کنید',
|
||||
driverActionHelp: 'پیش از ادامه بررسی محیط کانتینر، درایور دستگاه دارای مشکل را نصب یا تعمیر کنید.',
|
||||
driverGuide: 'مشاهده راهنمای نصب درایور',
|
||||
toolkitActionTitle: 'پشتیبانی GPU در کانتینر آماده نیست',
|
||||
toolkitActionHelp:
|
||||
'ابتدا NVIDIA Container Toolkit را نصب یا تعمیر کنید، سپس پیکربندی Docker را بررسی کنید.',
|
||||
toolkitGuide: 'مشاهده راهنمای نصب Toolkit',
|
||||
dockerActionTitle: 'پشتیبانی NVIDIA GPU در Docker آماده نیست',
|
||||
dockerActionHelp: 'پیکربندی محیط اجرای NVIDIA در Docker را بررسی کنید و سپس دوباره بررسی کنید.',
|
||||
monitorUnaffected: 'این مشکل بر پایش GPU میزبان تأثیری ندارد.',
|
||||
title: 'بررسی محیط',
|
||||
passedLabel: 'بررسی موفق',
|
||||
issuesLabel: 'مشکلات محیط · {count}',
|
||||
loadFailedLabel: 'بررسی ناموفق',
|
||||
notDetectedLabel: 'دستگاهی شناسایی نشد',
|
||||
unknownLabel: 'وضعیت نامشخص',
|
||||
passed: 'بررسیهای محیط با موفقیت انجام شد',
|
||||
issues: '{count} مشکل در محیط شناسایی شد. برای بررسی آنها راهنما را دنبال کنید.',
|
||||
recheck: 'بررسی دوباره',
|
||||
installGuide: 'مشاهده راهنمای نصب',
|
||||
goContainerSettings: 'باز کردن تنظیمات کانتینر',
|
||||
details: 'جزئیات بررسی',
|
||||
vendor: 'سازنده',
|
||||
gpuCount: 'تعداد GPU',
|
||||
cardCount: 'تعداد کارتها',
|
||||
notDetected: 'هیچ دستگاه GPU/NPU/XPU پشتیبانیشدهای در گره فعلی شناسایی نشد.',
|
||||
unknown: 'وضعیت دستگاه قابل تأیید نیست. جزئیات بررسی را ببینید و دوباره تلاش کنید.',
|
||||
loadFailed: 'خواندن اطلاعات دستگاه ممکن نیست. دوباره تلاش کنید.',
|
||||
notApplicable: 'قابل اعمال نیست',
|
||||
cudaHelper: 'این نسخه CUDA پشتیبانیشده توسط درایور است، نه نسخه CUDA Toolkit نصبشده روی میزبان.',
|
||||
partialMemory: 'بخشی از دادههای حافظه در دسترس نیست',
|
||||
sharedMemory: 'حافظه مشترک سیستم: {memory}',
|
||||
driver: 'درایور و ابزارهای مدیریت دستگاه',
|
||||
nvidiaDriver: 'درایور NVIDIA',
|
||||
toolkit: 'NVIDIA Container Toolkit',
|
||||
docker: 'پیکربندی GPU انویدیا در Docker',
|
||||
statusPassed: 'عادی',
|
||||
statusMissing: 'شناسایی نشد',
|
||||
statusUnconfigured: 'نیازمند پیکربندی',
|
||||
statusFailed: 'بررسی ناموفق',
|
||||
statusUnknown: 'نامشخص',
|
||||
driverMissing: 'درایور {vendor} شناسایی نشد. درایور را نصب یا بررسی کنید (ابزار شناسایی: {tool}).',
|
||||
driverFailed: 'خواندن اطلاعات دستگاه با {tool} ممکن نیست. وضعیت درایور را بررسی کنید.',
|
||||
driverUnknown: 'وضعیت درایور دستگاه قابل تأیید نیست. {tool} را بررسی کنید.',
|
||||
toolkitMissing:
|
||||
'NVIDIA Container Toolkit شناسایی نشد. پیش از استفاده از GPU انویدیا در کانتینرها، نصب و پیکربندی آن را کامل کنید.',
|
||||
toolkitFailed: 'بررسی NVIDIA Container Toolkit ممکن نیست. جزئیات خطا را ببینید.',
|
||||
dockerConfigMissing:
|
||||
'گزینه runtimes.nvidia در فایل پیکربندی پیشفرض Docker شناسایی نشد. پیکربندی را طبق مستندات کامل کنید.',
|
||||
dockerConfigInvalid:
|
||||
'پیکربندی محیط اجرای NVIDIA در Docker نامعتبر است. مقدار runtimes.nvidia.path را در daemon.json بررسی کنید.',
|
||||
dockerConfigPending:
|
||||
'فایل پیکربندی پیشفرض شامل پیکربندی محیط اجرای NVIDIA است، اما سرویس فعلی Docker هنوز آن را بارگذاری نکرده است. فایل پیکربندی را تأیید کنید، Docker را دوباره راهاندازی کنید و سپس دوباره بررسی کنید.',
|
||||
dockerConfigUnreadable: 'خواندن فایل پیکربندی Docker ممکن نیست. جزئیات بررسی را ببینید.',
|
||||
dockerUnconfigured: 'پیکربندی GPU انویدیا در Docker کامل نشده است.',
|
||||
dockerFailed: 'بررسی پیکربندی GPU انویدیا در Docker ممکن نیست. جزئیات خطا را ببینید.',
|
||||
},
|
||||
systemMemory: 'حافظهٔ مشترک سیستم',
|
||||
systemMemoryUsed: 'حافظهٔ مصرفشدهٔ سیستم',
|
||||
systemMemoryTotal: 'کل حافظهٔ سیستم',
|
||||
@@ -1798,6 +1864,8 @@ const message = {
|
||||
pcieWidth: 'عرض PCIe (فعلی / حداکثر)',
|
||||
gpu: 'مدیریت GPU',
|
||||
monitoringTitle: 'نظارت بر GPU',
|
||||
currentHelper:
|
||||
'وضعیت لحظهای و میزان استفاده از منابع دستگاههای GPU/NPU/XPU در گره فعلی را مشاهده کنید. نمای کلی در بالا، خلاصه همه دستگاهها را نشان میدهد؛ شاخصهای دقیق برای هر دستگاه جداگانه نمایش داده میشوند.',
|
||||
gpuHelper: 'هیچ دستور مدیریت GPU/XPU/NPU پشتیبانیشدهای شناسایی نشد. لطفاً بررسی و دوباره تلاش کنید!',
|
||||
process: 'اطلاعات فرآیند',
|
||||
processCount: 'تعداد فرآیندها',
|
||||
@@ -2048,6 +2116,9 @@ const message = {
|
||||
statusUnavailable: 'Service status unavailable. Refresh to retry.',
|
||||
},
|
||||
benchmarkPlan: {
|
||||
logAutoRefresh: 'بازخوانی خودکار',
|
||||
logWindowHelp:
|
||||
'فقط بخش اخیر گزارش نمایش داده میشود. برای مشاهده محتوای کامل، فایل گزارش انتخابشده را دانلود کنید.',
|
||||
prefixTokens: 'تعداد توکن\u200cهای پیشوند',
|
||||
seed: 'بذر تصادفی',
|
||||
report: 'گزارش',
|
||||
@@ -2261,6 +2332,12 @@ const message = {
|
||||
missing: 'ناموجود',
|
||||
},
|
||||
container: {
|
||||
nvidiaSupport: 'پشتیبانی از GPU انویدیا',
|
||||
nvidiaHelper:
|
||||
'محیط اجرای کانتینر NVIDIA را ثبت میکند. ابتدا NVIDIA Container Toolkit را نصب کنید. GPU را جداگانه به هر کانتینر اختصاص دهید.',
|
||||
nvidiaLocalOnly: 'فقط سوکت پیشفرض Docker محلی پشتیبانی میشود.',
|
||||
nvidiaChangeHelper:
|
||||
'غیرفعالسازی ممکن است مانع شروع کانتینرهای دارای محیط اجرای NVIDIA شود. این کلید دسترسی به GPU را مسدود نمیکند.',
|
||||
imageRemoteSearch: 'جستجوی راه دور',
|
||||
imageRemoteSearchHelper: 'فقط جستجوی تصاویر مخزن Docker Hub پشتیبانی می\u200cشود',
|
||||
imageRemoteSearchProxyRequired: 'برای جستجوی تصاویر راه دور در محیط فعلی، ابتدا یک سرور پروکسی پیکربندی کنید.',
|
||||
|
||||
@@ -1726,8 +1726,8 @@ const message = {
|
||||
running: '実行中のリクエスト',
|
||||
waiting: '待機中のリクエスト',
|
||||
cacheUsage: 'KVキャッシュ使用率',
|
||||
promptThroughput: '入力トークンスループット',
|
||||
generationThroughput: '出力トークンスループット',
|
||||
promptThroughput: '入力トークン速度',
|
||||
generationThroughput: '出力トークン速度',
|
||||
requestThroughput: '完了リクエスト数/秒',
|
||||
timeToFirstToken: '平均初回トークン遅延',
|
||||
timePerOutputToken: '平均トークン間遅延',
|
||||
@@ -1747,6 +1747,75 @@ const message = {
|
||||
timePerOutputTokenChart: 'トークン間遅延',
|
||||
},
|
||||
gpu: {
|
||||
environment: {
|
||||
hostGPU: 'ホスト GPU',
|
||||
containerGPU: 'コンテナーの GPU 対応',
|
||||
monitorAvailable: '監視可能',
|
||||
monitorPartial: '一部監視可能',
|
||||
notReady: '未準備',
|
||||
ready: '準備完了',
|
||||
checkedAt: '確認日時',
|
||||
checkDetailsHelp: '確認の詳細を参照し、環境の状態を確認してから再チェックしてください。',
|
||||
driverActionTitle: 'まずホスト GPU ドライバーを確認してください',
|
||||
driverActionHelp:
|
||||
'問題のあるデバイスのドライバーをインストールまたは修復してから、コンテナー環境の確認を続けてください。',
|
||||
driverGuide: 'ドライバーのインストール手順',
|
||||
toolkitActionTitle: 'コンテナーの GPU 環境が未準備です',
|
||||
toolkitActionHelp:
|
||||
'NVIDIA Container Toolkit をインストールまたは修復してから、Docker の設定を確認してください。',
|
||||
toolkitGuide: 'Toolkit のインストール手順',
|
||||
dockerActionTitle: 'Docker の NVIDIA GPU 環境が未準備です',
|
||||
dockerActionHelp: 'Docker の NVIDIA ランタイム設定を確認してから、再チェックしてください。',
|
||||
monitorUnaffected: 'この問題はホスト GPU の監視に影響しません。',
|
||||
title: '環境チェック',
|
||||
passedLabel: 'チェック成功',
|
||||
issuesLabel: '環境の問題 · {count} 件',
|
||||
loadFailedLabel: 'チェック失敗',
|
||||
notDetectedLabel: 'デバイス未検出',
|
||||
unknownLabel: '状態不明',
|
||||
passed: '環境チェックに合格しました',
|
||||
issues: '環境の問題が {count} 件見つかりました。案内に従って確認してください。',
|
||||
recheck: '再チェック',
|
||||
installGuide: 'インストールガイドを見る',
|
||||
goContainerSettings: 'コンテナー設定を開く',
|
||||
details: 'チェックの詳細',
|
||||
vendor: 'メーカー',
|
||||
gpuCount: 'GPU 数',
|
||||
cardCount: 'カード数',
|
||||
notDetected: '現在のノードで対応する GPU/NPU/XPU デバイスが検出されませんでした。',
|
||||
unknown: 'デバイスの状態を確認できません。チェックの詳細を確認してから再実行してください。',
|
||||
loadFailed: 'デバイス情報を読み取れません。再試行してください。',
|
||||
notApplicable: '対象外',
|
||||
cudaHelper:
|
||||
'これはドライバーが対応する CUDA バージョンであり、ホストにインストールされた CUDA Toolkit のバージョンではありません。',
|
||||
partialMemory: '一部のメモリデータを取得できません',
|
||||
sharedMemory: '共有システムメモリ:{memory}',
|
||||
driver: 'ドライバーとデバイス管理ツール',
|
||||
nvidiaDriver: 'NVIDIA ドライバー',
|
||||
toolkit: 'NVIDIA Container Toolkit',
|
||||
docker: 'Docker の NVIDIA GPU 設定',
|
||||
statusPassed: '正常',
|
||||
statusMissing: '未検出',
|
||||
statusUnconfigured: '設定が必要',
|
||||
statusFailed: 'チェック失敗',
|
||||
statusUnknown: '不明',
|
||||
driverMissing:
|
||||
'{vendor} ドライバーが検出されませんでした。ドライバーをインストールするか、状態を確認してください(検出ツール:{tool})。',
|
||||
driverFailed: '{tool} でデバイス情報を読み取れません。ドライバーの状態を確認してください。',
|
||||
driverUnknown: 'デバイスのドライバーの状態を確認できません。{tool} を確認してください。',
|
||||
toolkitMissing:
|
||||
'NVIDIA Container Toolkit が検出されませんでした。コンテナーで NVIDIA GPU を使用する前にインストールと設定を完了してください。',
|
||||
toolkitFailed: 'NVIDIA Container Toolkit をチェックできません。失敗の詳細を確認してください。',
|
||||
dockerConfigMissing:
|
||||
'Docker の既定の設定ファイルに runtimes.nvidia が検出されませんでした。ドキュメントに従って設定を完了してください。',
|
||||
dockerConfigInvalid:
|
||||
'Docker の NVIDIA ランタイム設定が正しくありません。daemon.json の runtimes.nvidia.path を確認してください。',
|
||||
dockerConfigPending:
|
||||
'既定の設定ファイルに NVIDIA ランタイム設定がありますが、現在の Docker にはまだ読み込まれていません。設定ファイルを確認し、Docker を再起動してから再チェックしてください。',
|
||||
dockerConfigUnreadable: 'Docker の設定ファイルを読み取れません。チェックの詳細を確認してください。',
|
||||
dockerUnconfigured: 'Docker の NVIDIA GPU 設定が完了していません。',
|
||||
dockerFailed: 'Docker の NVIDIA GPU 設定をチェックできません。失敗の詳細を確認してください。',
|
||||
},
|
||||
systemMemory: '共有システムメモリ',
|
||||
systemMemoryUsed: 'システムメモリ使用量',
|
||||
systemMemoryTotal: 'システムメモリ合計',
|
||||
@@ -1804,6 +1873,8 @@ const message = {
|
||||
pcieWidth: 'PCIe 幅(現在 / 最大)',
|
||||
gpu: 'GPU 管理',
|
||||
monitoringTitle: 'GPU 監視',
|
||||
currentHelper:
|
||||
'現在のノードにある GPU/NPU/XPU デバイスのリアルタイムの稼働状態とリソース使用状況を確認できます。上部には全デバイスの概要を表示し、詳細な指標はデバイスごとに表示します。',
|
||||
gpuHelper: '対応する GPU/XPU/NPU 管理コマンドが検出されませんでした。確認して再試行してください!',
|
||||
process: 'プロセス情報',
|
||||
type: 'タイプ',
|
||||
@@ -2054,6 +2125,8 @@ const message = {
|
||||
statusUnavailable: 'Service status unavailable. Refresh to retry.',
|
||||
},
|
||||
benchmarkPlan: {
|
||||
logAutoRefresh: '自動更新',
|
||||
logWindowHelp: '最近のログのみ表示します。全内容は選択したログファイルをダウンロードして確認できます。',
|
||||
prefixTokens: 'プレフィックストークン数',
|
||||
seed: '乱数シード',
|
||||
report: 'レポート',
|
||||
@@ -2270,6 +2343,12 @@ const message = {
|
||||
missing: '存在しない',
|
||||
},
|
||||
container: {
|
||||
nvidiaSupport: 'NVIDIA GPU サポート',
|
||||
nvidiaHelper:
|
||||
'NVIDIA コンテナーランタイムを登録します。先に NVIDIA Container Toolkit をインストールしてください。GPU はコンテナーごとに割り当てる必要があります。',
|
||||
nvidiaLocalOnly: 'ローカルの既定の Docker ソケットのみ対応しています。',
|
||||
nvidiaChangeHelper:
|
||||
'無効にすると NVIDIA ランタイムを使用するコンテナーが起動できなくなる場合があります。このスイッチは GPU へのアクセスを遮断しません。',
|
||||
imageRemoteSearch: 'リモート検索',
|
||||
imageRemoteSearchHelper: 'Docker Hub リポジトリのイメージのみ検索できます',
|
||||
imageRemoteSearchProxyRequired:
|
||||
|
||||
@@ -1707,8 +1707,8 @@ const message = {
|
||||
running: '실행 중인 요청',
|
||||
waiting: '대기 중인 요청',
|
||||
cacheUsage: 'KV 캐시 사용률',
|
||||
promptThroughput: '입력 토큰 처리량',
|
||||
generationThroughput: '출력 토큰 처리량',
|
||||
promptThroughput: '입력 토큰 속도',
|
||||
generationThroughput: '출력 토큰 속도',
|
||||
requestThroughput: '완료 요청 속도',
|
||||
timeToFirstToken: '평균 첫 토큰 지연',
|
||||
timePerOutputToken: '평균 토큰 간 지연',
|
||||
@@ -1728,6 +1728,72 @@ const message = {
|
||||
timePerOutputTokenChart: '토큰 간 지연',
|
||||
},
|
||||
gpu: {
|
||||
environment: {
|
||||
hostGPU: '호스트 GPU',
|
||||
containerGPU: '컨테이너 GPU 지원',
|
||||
monitorAvailable: '모니터링 가능',
|
||||
monitorPartial: '일부 모니터링 가능',
|
||||
notReady: '준비되지 않음',
|
||||
ready: '준비됨',
|
||||
checkedAt: '확인 시간',
|
||||
checkDetailsHelp: '확인 세부 정보를 보고 환경 상태를 확인한 후 다시 검사하세요.',
|
||||
driverActionTitle: '먼저 호스트 GPU 드라이버를 확인하세요',
|
||||
driverActionHelp: '문제가 있는 장치의 드라이버를 설치하거나 복구한 후 컨테이너 환경 검사를 계속하세요.',
|
||||
driverGuide: '드라이버 설치 안내 보기',
|
||||
toolkitActionTitle: '컨테이너 GPU 지원이 준비되지 않았습니다',
|
||||
toolkitActionHelp: 'NVIDIA Container Toolkit을 설치하거나 복구한 후 Docker 구성을 확인하세요.',
|
||||
toolkitGuide: 'Toolkit 설치 안내 보기',
|
||||
dockerActionTitle: 'Docker의 NVIDIA GPU 지원이 준비되지 않았습니다',
|
||||
dockerActionHelp: 'Docker의 NVIDIA 런타임 구성을 확인한 후 다시 검사하세요.',
|
||||
monitorUnaffected: '이 문제는 호스트 GPU 모니터링에 영향을 주지 않습니다.',
|
||||
title: '환경 점검',
|
||||
passedLabel: '검사 통과',
|
||||
issuesLabel: '환경 문제 · {count}개',
|
||||
loadFailedLabel: '검사 실패',
|
||||
notDetectedLabel: '장치 감지 안 됨',
|
||||
unknownLabel: '상태 알 수 없음',
|
||||
passed: '환경 점검을 통과했습니다',
|
||||
issues: '환경 문제 {count}개가 발견되었습니다. 안내에 따라 확인하세요.',
|
||||
recheck: '다시 점검',
|
||||
installGuide: '설치 가이드 보기',
|
||||
goContainerSettings: '컨테이너 설정 열기',
|
||||
details: '점검 세부 정보',
|
||||
vendor: '제조사',
|
||||
gpuCount: 'GPU 수',
|
||||
cardCount: '카드 수',
|
||||
notDetected: '현재 노드에서 지원되는 GPU/NPU/XPU 장치가 감지되지 않았습니다.',
|
||||
unknown: '장치 상태를 확인할 수 없습니다. 점검 세부 정보를 확인한 후 다시 시도하세요.',
|
||||
loadFailed: '장치 정보를 읽을 수 없습니다. 다시 시도하세요.',
|
||||
notApplicable: '해당 없음',
|
||||
cudaHelper: '이 버전은 드라이버가 지원하는 CUDA 버전이며 호스트에 설치된 CUDA Toolkit 버전이 아닙니다.',
|
||||
partialMemory: '일부 메모리 데이터를 가져오지 못했습니다',
|
||||
sharedMemory: '공유 시스템 메모리: {memory}',
|
||||
driver: '드라이버 및 장치 관리 도구',
|
||||
nvidiaDriver: 'NVIDIA 드라이버',
|
||||
toolkit: 'NVIDIA Container Toolkit',
|
||||
docker: 'Docker NVIDIA GPU 구성',
|
||||
statusPassed: '정상',
|
||||
statusMissing: '감지되지 않음',
|
||||
statusUnconfigured: '구성 필요',
|
||||
statusFailed: '점검 실패',
|
||||
statusUnknown: '알 수 없음',
|
||||
driverMissing:
|
||||
'{vendor} 드라이버가 감지되지 않았습니다. 드라이버를 설치하거나 확인하세요(감지 도구: {tool}).',
|
||||
driverFailed: '{tool}를 통해 장치 정보를 읽을 수 없습니다. 드라이버 상태를 확인하세요.',
|
||||
driverUnknown: '장치 드라이버 상태를 확인할 수 없습니다. {tool}를 확인하세요.',
|
||||
toolkitMissing:
|
||||
'NVIDIA Container Toolkit이 감지되지 않았습니다. 컨테이너에서 NVIDIA GPU를 사용하기 전에 설치와 구성을 완료하세요.',
|
||||
toolkitFailed: 'NVIDIA Container Toolkit을 점검할 수 없습니다. 실패 세부 정보를 확인하세요.',
|
||||
dockerConfigMissing:
|
||||
'Docker 기본 구성 파일에서 runtimes.nvidia 항목이 감지되지 않았습니다. 문서에 따라 구성을 완료하세요.',
|
||||
dockerConfigInvalid:
|
||||
'Docker의 NVIDIA 런타임 구성이 올바르지 않습니다. daemon.json의 runtimes.nvidia.path를 확인하세요.',
|
||||
dockerConfigPending:
|
||||
'기본 구성 파일에 NVIDIA 런타임 구성이 있지만 현재 Docker에서 아직 로드하지 않았습니다. 구성 파일을 확인하고 Docker를 다시 시작한 후 다시 점검하세요.',
|
||||
dockerConfigUnreadable: 'Docker 구성 파일을 읽을 수 없습니다. 점검 세부 정보를 확인하세요.',
|
||||
dockerUnconfigured: 'Docker NVIDIA GPU 구성이 완료되지 않았습니다.',
|
||||
dockerFailed: 'Docker NVIDIA GPU 구성을 점검할 수 없습니다. 실패 세부 정보를 확인하세요.',
|
||||
},
|
||||
systemMemory: '공유 시스템 메모리',
|
||||
systemMemoryUsed: '사용 중인 시스템 메모리',
|
||||
systemMemoryTotal: '전체 시스템 메모리',
|
||||
@@ -1785,6 +1851,8 @@ const message = {
|
||||
pcieWidth: 'PCIe 너비 (현재 / 최대)',
|
||||
gpu: 'GPU 관리',
|
||||
monitoringTitle: 'GPU 모니터링',
|
||||
currentHelper:
|
||||
'현재 노드의 GPU/NPU/XPU 장치에 대한 실시간 상태와 리소스 사용량을 확인합니다. 상단에는 모든 장치의 요약이 표시되며, 상세 지표는 장치별로 표시됩니다.',
|
||||
gpuHelper: '지원되는 GPU/XPU/NPU 관리 명령을 감지하지 못했습니다. 확인하고 다시 시도하세요!',
|
||||
process: '프로세스 정보',
|
||||
type: '유형',
|
||||
@@ -2033,6 +2101,8 @@ const message = {
|
||||
statusUnavailable: 'Service status unavailable. Refresh to retry.',
|
||||
},
|
||||
benchmarkPlan: {
|
||||
logAutoRefresh: '자동 새로고침',
|
||||
logWindowHelp: '최근 로그만 표시합니다. 전체 내용을 보려면 선택한 로그 파일을 다운로드하세요.',
|
||||
prefixTokens: '접두사 토큰 수',
|
||||
seed: '난수 시드',
|
||||
report: '보고서',
|
||||
@@ -2242,6 +2312,12 @@ const message = {
|
||||
missing: '없음',
|
||||
},
|
||||
container: {
|
||||
nvidiaSupport: 'NVIDIA GPU 지원',
|
||||
nvidiaHelper:
|
||||
'NVIDIA 컨테이너 런타임을 등록합니다. 먼저 NVIDIA Container Toolkit을 설치하세요. GPU는 각 컨테이너에 별도로 할당해야 합니다.',
|
||||
nvidiaLocalOnly: '로컬 기본 Docker 소켓만 지원합니다.',
|
||||
nvidiaChangeHelper:
|
||||
'비활성화하면 NVIDIA 런타임을 사용하는 컨테이너가 시작되지 않을 수 있습니다. 이 스위치는 GPU 접근을 차단하지 않습니다.',
|
||||
imageRemoteSearch: '원격 검색',
|
||||
imageRemoteSearchHelper: 'Docker Hub 저장소 이미지만 검색할 수 있습니다',
|
||||
imageRemoteSearchProxyRequired: '현재 실행 환경에서 원격 이미지를 검색하려면 먼저 프록시 서버를 설정하세요.',
|
||||
|
||||
@@ -1717,6 +1717,71 @@ const message = {
|
||||
timePerOutputTokenChart: 'ເວລາລໍຖ້າລະຫວ່າງ Token',
|
||||
},
|
||||
gpu: {
|
||||
environment: {
|
||||
hostGPU: 'GPU ຂອງໂຮສ',
|
||||
containerGPU: 'ການຮອງຮັບ GPU ໃນຄອນເທນເນີ',
|
||||
monitorAvailable: 'ສາມາດຕິດຕາມໄດ້',
|
||||
monitorPartial: 'ຕິດຕາມໄດ້ບາງສ່ວນ',
|
||||
notReady: 'ຍັງບໍ່ພ້ອມ',
|
||||
ready: 'ພ້ອມແລ້ວ',
|
||||
checkedAt: 'ເວລາກວດສອບ',
|
||||
checkDetailsHelp: 'ເບິ່ງລາຍລະອຽດການກວດສອບ, ຢືນຢັນສະຖານະສະພາບແວດລ້ອມ ແລ້ວກວດສອບອີກຄັ້ງ.',
|
||||
driverActionTitle: 'ກວດສອບໄດຣເວີ GPU ຂອງໂຮສກ່ອນ',
|
||||
driverActionHelp: 'ຕິດຕັ້ງ ຫຼື ສ້ອມແປງໄດຣເວີຂອງອຸປະກອນທີ່ມີບັນຫາ ກ່ອນກວດສອບສະພາບແວດລ້ອມຄອນເທນເນີຕໍ່.',
|
||||
driverGuide: 'ເບິ່ງຄູ່ມືຕິດຕັ້ງໄດຣເວີ',
|
||||
toolkitActionTitle: 'ການຮອງຮັບ GPU ໃນຄອນເທນເນີຍັງບໍ່ພ້ອມ',
|
||||
toolkitActionHelp: 'ຕິດຕັ້ງ ຫຼື ສ້ອມແປງ NVIDIA Container Toolkit ກ່ອນ ແລ້ວກວດສອບການຕັ້ງຄ່າ Docker.',
|
||||
toolkitGuide: 'ເບິ່ງຄູ່ມືຕິດຕັ້ງ Toolkit',
|
||||
dockerActionTitle: 'ການຮອງຮັບ NVIDIA GPU ຂອງ Docker ຍັງບໍ່ພ້ອມ',
|
||||
dockerActionHelp: 'ກວດສອບການຕັ້ງຄ່າ NVIDIA runtime ໃນ Docker ແລ້ວກວດສອບອີກຄັ້ງ.',
|
||||
monitorUnaffected: 'ບັນຫານີ້ບໍ່ກະທົບຕໍ່ການຕິດຕາມ GPU ຂອງໂຮສ.',
|
||||
title: 'ກວດສອບສະພາບແວດລ້ອມ',
|
||||
passedLabel: 'ກວດສອບຜ່ານ',
|
||||
issuesLabel: 'ບັນຫາສະພາບແວດລ້ອມ · {count}',
|
||||
loadFailedLabel: 'ກວດສອບລົ້ມເຫຼວ',
|
||||
notDetectedLabel: 'ບໍ່ພົບອຸປະກອນ',
|
||||
unknownLabel: 'ບໍ່ຮູ້ສະຖານະ',
|
||||
passed: 'ຜ່ານການກວດສອບສະພາບແວດລ້ອມແລ້ວ',
|
||||
issues: 'ພົບບັນຫາສະພາບແວດລ້ອມ {count} ລາຍການ. ກະລຸນາກວດສອບຕາມຄຳແນະນຳ.',
|
||||
recheck: 'ກວດສອບອີກຄັ້ງ',
|
||||
installGuide: 'ເບິ່ງຄູ່ມືການຕິດຕັ້ງ',
|
||||
goContainerSettings: 'ເປີດການຕັ້ງຄ່າຄອນເທນເນີ',
|
||||
details: 'ລາຍລະອຽດການກວດສອບ',
|
||||
vendor: 'ຜູ້ຜະລິດ',
|
||||
gpuCount: 'ຈຳນວນ GPU',
|
||||
cardCount: 'ຈຳນວນບັດ',
|
||||
notDetected: 'ບໍ່ພົບອຸປະກອນ GPU/NPU/XPU ທີ່ຮອງຮັບໃນໂນດປັດຈຸບັນ.',
|
||||
unknown: 'ບໍ່ສາມາດຢືນຢັນສະຖານະອຸປະກອນໄດ້. ກະລຸນາເບິ່ງລາຍລະອຽດແລ້ວກວດສອບອີກຄັ້ງ.',
|
||||
loadFailed: 'ບໍ່ສາມາດອ່ານຂໍ້ມູນອຸປະກອນໄດ້. ກະລຸນາລອງໃໝ່.',
|
||||
notApplicable: 'ບໍ່ກ່ຽວຂ້ອງ',
|
||||
cudaHelper: 'ນີ້ແມ່ນເວີຊັນ CUDA ທີ່ໄດຣເວີຮອງຮັບ ບໍ່ແມ່ນເວີຊັນ CUDA Toolkit ທີ່ຕິດຕັ້ງໃນໂຮສ.',
|
||||
partialMemory: 'ບໍ່ສາມາດດຶງຂໍ້ມູນຄວາມຈຳບາງສ່ວນໄດ້',
|
||||
sharedMemory: 'ຄວາມຈຳລະບົບຮ່ວມກັນ: {memory}',
|
||||
driver: 'ໄດຣເວີ ແລະ ເຄື່ອງມືຈັດການອຸປະກອນ',
|
||||
nvidiaDriver: 'ໄດຣເວີ NVIDIA',
|
||||
toolkit: 'NVIDIA Container Toolkit',
|
||||
docker: 'ການຕັ້ງຄ່າ NVIDIA GPU ໃນ Docker',
|
||||
statusPassed: 'ປົກກະຕິ',
|
||||
statusMissing: 'ບໍ່ພົບ',
|
||||
statusUnconfigured: 'ຕ້ອງຕັ້ງຄ່າ',
|
||||
statusFailed: 'ການກວດສອບລົ້ມເຫຼວ',
|
||||
statusUnknown: 'ບໍ່ຮູ້',
|
||||
driverMissing: 'ບໍ່ພົບໄດຣເວີ {vendor}. ກະລຸນາຕິດຕັ້ງ ຫຼື ກວດສອບໄດຣເວີ (ເຄື່ອງມືກວດສອບ: {tool}).',
|
||||
driverFailed: 'ບໍ່ສາມາດອ່ານຂໍ້ມູນອຸປະກອນຜ່ານ {tool} ໄດ້. ກະລຸນາກວດສອບສະຖານະໄດຣເວີ.',
|
||||
driverUnknown: 'ບໍ່ສາມາດຢືນຢັນສະຖານະໄດຣເວີອຸປະກອນໄດ້. ກະລຸນາກວດສອບ {tool}.',
|
||||
toolkitMissing:
|
||||
'ບໍ່ພົບ NVIDIA Container Toolkit. ກະລຸນາຕິດຕັ້ງ ແລະ ຕັ້ງຄ່າກ່ອນໃຊ້ NVIDIA GPU ໃນຄອນເທນເນີ.',
|
||||
toolkitFailed: 'ບໍ່ສາມາດກວດສອບ NVIDIA Container Toolkit ໄດ້. ກະລຸນາເບິ່ງລາຍລະອຽດຂໍ້ຜິດພາດ.',
|
||||
dockerConfigMissing:
|
||||
'ບໍ່ພົບ runtimes.nvidia ໃນໄຟລ໌ການຕັ້ງຄ່າເລີ່ມຕົ້ນຂອງ Docker. ກະລຸນາຕັ້ງຄ່າໃຫ້ສຳເລັດຕາມເອກະສານ.',
|
||||
dockerConfigInvalid:
|
||||
'ການຕັ້ງຄ່າ NVIDIA runtime ໃນ Docker ບໍ່ຖືກຕ້ອງ. ກະລຸນາກວດສອບ runtimes.nvidia.path ໃນ daemon.json.',
|
||||
dockerConfigPending:
|
||||
'ໄຟລ໌ການຕັ້ງຄ່າເລີ່ມຕົ້ນມີການຕັ້ງຄ່າ NVIDIA runtime ແລ້ວ ແຕ່ບໍລິການ Docker ປັດຈຸບັນຍັງບໍ່ໄດ້ໂຫຼດ. ກະລຸນາຢືນຢັນໄຟລ໌ການຕັ້ງຄ່າ, ເລີ່ມ Docker ໃໝ່ ແລ້ວກວດສອບອີກຄັ້ງ.',
|
||||
dockerConfigUnreadable: 'ບໍ່ສາມາດອ່ານໄຟລ໌ການຕັ້ງຄ່າ Docker ໄດ້. ກະລຸນາເບິ່ງລາຍລະອຽດການກວດສອບ.',
|
||||
dockerUnconfigured: 'ການຕັ້ງຄ່າ NVIDIA GPU ໃນ Docker ຍັງບໍ່ສຳເລັດ.',
|
||||
dockerFailed: 'ບໍ່ສາມາດກວດສອບການຕັ້ງຄ່າ NVIDIA GPU ໃນ Docker ໄດ້. ກະລຸນາເບິ່ງລາຍລະອຽດຂໍ້ຜິດພາດ.',
|
||||
},
|
||||
systemMemory: 'ໜ່ວຍຄວາມຈຳລະບົບທີ່ໃຊ້ຮ່ວມກັນ',
|
||||
systemMemoryUsed: 'ໜ່ວຍຄວາມຈຳລະບົບທີ່ໃຊ້ແລ້ວ',
|
||||
systemMemoryTotal: 'ໜ່ວຍຄວາມຈຳລະບົບທັງໝົດ',
|
||||
@@ -1774,6 +1839,8 @@ const message = {
|
||||
pcieWidth: 'ຄວາມກວ້າງ PCIe (ປັດຈຸບັນ / ສູງສຸດ)',
|
||||
gpu: 'ການຈັດການ GPU',
|
||||
monitoringTitle: 'ຕິດຕາມ GPU',
|
||||
currentHelper:
|
||||
'ເບິ່ງສະຖານະການເຮັດວຽກ ແລະ ການໃຊ້ຊັບພະຍາກອນແບບເວລາຈິງຂອງອຸປະກອນ GPU/NPU/XPU ໃນໂນດປັດຈຸບັນ. ພາບລວມດ້ານເທິງສະຫຼຸບຂໍ້ມູນຂອງທຸກອຸປະກອນ ແລະ ສະແດງຕົວຊີ້ວັດລະອຽດແຍກຕາມອຸປະກອນ.',
|
||||
gpuHelper: 'ກວດບໍ່ພົບຄຳສັ່ງຈັດການ GPU/XPU/NPU ທີ່ຮອງຮັບ. ກະລຸນາກວດສອບ ແລະ ລອງໃໝ່!',
|
||||
process: 'ຂໍ້ມູນໂປຣເຊສ',
|
||||
processCount: 'ຈຳນວນໂປຣເຊສ',
|
||||
@@ -2020,6 +2087,8 @@ const message = {
|
||||
statusUnavailable: 'Service status unavailable. Refresh to retry.',
|
||||
},
|
||||
benchmarkPlan: {
|
||||
logAutoRefresh: 'ໂຫຼດຄືນອັດຕະໂນມັດ',
|
||||
logWindowHelp: 'ສະແດງສະເພາະບັນທຶກຫຼ້າສຸດ. ດາວໂຫຼດໄຟລ໌ບັນທຶກທີ່ເລືອກເພື່ອເບິ່ງເນື້ອຫາທັງໝົດ.',
|
||||
prefixTokens: 'ຈຳນວນ Token ຄຳນຳໜ້າ',
|
||||
seed: 'ຄ່າເລີ່ມຕົ້ນການສຸ່ມ',
|
||||
report: 'ລາຍງານ',
|
||||
@@ -2223,6 +2292,12 @@ const message = {
|
||||
missing: 'ບໍ່ມີ',
|
||||
},
|
||||
container: {
|
||||
nvidiaSupport: 'ຮອງຮັບ NVIDIA GPU',
|
||||
nvidiaHelper:
|
||||
'ລົງທະບຽນ NVIDIA runtime ສຳລັບຄອນເທນເນີ. ຕິດຕັ້ງ NVIDIA Container Toolkit ກ່ອນ ແລະ ຈັດສັນ GPU ໃຫ້ແຕ່ລະຄອນເທນເນີແຍກກັນ.',
|
||||
nvidiaLocalOnly: 'ຮອງຮັບສະເພາະ Docker socket ເລີ່ມຕົ້ນໃນເຄື່ອງນີ້.',
|
||||
nvidiaChangeHelper:
|
||||
'ການປິດອາດເຮັດໃຫ້ຄອນເທນເນີທີ່ໃຊ້ NVIDIA runtime ເລີ່ມບໍ່ໄດ້. ສະວິດນີ້ບໍ່ໄດ້ປິດກັ້ນການເຂົ້າໃຊ້ GPU.',
|
||||
imageRemoteSearch: 'ຄົ້ນຫາທາງໄກ',
|
||||
imageRemoteSearchHelper: 'ຮອງຮັບການຄົ້ນຫາອິມເມຈໃນ Docker Hub ເທົ່ານັ້ນ',
|
||||
imageRemoteSearchProxyRequired: 'ເພື່ອຄົ້ນຫາອິມເມຈທາງໄກໃນສະພາບແວດລ້ອມປັດຈຸບັນ ກະລຸນາຕັ້ງຄ່າພຣັອກຊີເຊີບເວີກ່ອນ.',
|
||||
|
||||
@@ -1757,8 +1757,8 @@ const message = {
|
||||
running: 'Permintaan berjalan',
|
||||
waiting: 'Permintaan menunggu',
|
||||
cacheUsage: 'Penggunaan cache KV',
|
||||
promptThroughput: 'Daya pemprosesan token input',
|
||||
generationThroughput: 'Daya pemprosesan token output',
|
||||
promptThroughput: 'Kadar token input',
|
||||
generationThroughput: 'Kadar token output',
|
||||
requestThroughput: 'Kadar permintaan selesai',
|
||||
timeToFirstToken: 'Purata kependaman token pertama',
|
||||
timePerOutputToken: 'Purata kependaman antara token',
|
||||
@@ -1778,6 +1778,73 @@ const message = {
|
||||
timePerOutputTokenChart: 'Kependaman antara token',
|
||||
},
|
||||
gpu: {
|
||||
environment: {
|
||||
hostGPU: 'GPU hos',
|
||||
containerGPU: 'Sokongan GPU kontena',
|
||||
monitorAvailable: 'Pemantauan tersedia',
|
||||
monitorPartial: 'Pemantauan separa',
|
||||
notReady: 'Belum sedia',
|
||||
ready: 'Sedia',
|
||||
checkedAt: 'Masa pemeriksaan',
|
||||
checkDetailsHelp: 'Semak butiran pemeriksaan, sahkan keadaan persekitaran dan periksa semula.',
|
||||
driverActionTitle: 'Periksa pemacu GPU hos terlebih dahulu',
|
||||
driverActionHelp:
|
||||
'Pasang atau baiki pemacu peranti yang bermasalah sebelum meneruskan pemeriksaan persekitaran kontena.',
|
||||
driverGuide: 'Lihat panduan pemasangan pemacu',
|
||||
toolkitActionTitle: 'Sokongan GPU kontena belum sedia',
|
||||
toolkitActionHelp: 'Pasang atau baiki NVIDIA Container Toolkit, kemudian periksa konfigurasi Docker.',
|
||||
toolkitGuide: 'Lihat panduan pemasangan Toolkit',
|
||||
dockerActionTitle: 'Sokongan NVIDIA GPU Docker belum sedia',
|
||||
dockerActionHelp: 'Periksa konfigurasi runtime NVIDIA dalam Docker, kemudian periksa semula.',
|
||||
monitorUnaffected: 'Isu ini tidak menjejaskan pemantauan GPU hos.',
|
||||
title: 'Semakan persekitaran',
|
||||
passedLabel: 'Semakan lulus',
|
||||
issuesLabel: 'Isu persekitaran · {count}',
|
||||
loadFailedLabel: 'Semakan gagal',
|
||||
notDetectedLabel: 'Tiada peranti dikesan',
|
||||
unknownLabel: 'Status tidak diketahui',
|
||||
passed: 'Semakan persekitaran berjaya',
|
||||
issues: 'Masalah persekitaran dikesan: {count}. Ikuti panduan untuk menyemaknya.',
|
||||
recheck: 'Semak semula',
|
||||
installGuide: 'Lihat panduan pemasangan',
|
||||
goContainerSettings: 'Buka tetapan bekas',
|
||||
details: 'Butiran semakan',
|
||||
vendor: 'Pengeluar',
|
||||
gpuCount: 'Bilangan GPU',
|
||||
cardCount: 'Bilangan kad',
|
||||
notDetected: 'Tiada peranti GPU/NPU/XPU yang disokong dikesan pada nod semasa.',
|
||||
unknown: 'Status peranti tidak dapat dipastikan. Lihat butiran semakan dan cuba lagi.',
|
||||
loadFailed: 'Maklumat peranti tidak dapat dibaca. Sila cuba lagi.',
|
||||
notApplicable: 'Tidak berkenaan',
|
||||
cudaHelper:
|
||||
'Ini ialah versi CUDA yang disokong oleh pemacu, bukan versi CUDA Toolkit yang dipasang pada hos.',
|
||||
partialMemory: 'Sebahagian data memori tidak tersedia',
|
||||
sharedMemory: 'Memori sistem dikongsi: {memory}',
|
||||
driver: 'Pemacu dan alat pengurusan peranti',
|
||||
nvidiaDriver: 'Pemacu NVIDIA',
|
||||
toolkit: 'NVIDIA Container Toolkit',
|
||||
docker: 'Konfigurasi NVIDIA GPU Docker',
|
||||
statusPassed: 'Normal',
|
||||
statusMissing: 'Tidak dikesan',
|
||||
statusUnconfigured: 'Konfigurasi diperlukan',
|
||||
statusFailed: 'Semakan gagal',
|
||||
statusUnknown: 'Tidak diketahui',
|
||||
driverMissing: 'Pemacu {vendor} tidak dikesan. Pasang atau semak pemacu (alat pengesanan: {tool}).',
|
||||
driverFailed: 'Maklumat peranti tidak dapat dibaca menggunakan {tool}. Semak status pemacu.',
|
||||
driverUnknown: 'Status pemacu peranti tidak dapat dipastikan. Semak {tool}.',
|
||||
toolkitMissing:
|
||||
'NVIDIA Container Toolkit tidak dikesan. Pasang dan konfigurasikannya sebelum menggunakan NVIDIA GPU dalam bekas.',
|
||||
toolkitFailed: 'NVIDIA Container Toolkit tidak dapat disemak. Lihat butiran kegagalan.',
|
||||
dockerConfigMissing:
|
||||
'Entri runtimes.nvidia tidak dikesan dalam fail konfigurasi lalai Docker. Lengkapkan konfigurasi mengikut dokumentasi.',
|
||||
dockerConfigInvalid:
|
||||
'Konfigurasi masa jalan NVIDIA dalam Docker tidak sah. Semak runtimes.nvidia.path dalam daemon.json.',
|
||||
dockerConfigPending:
|
||||
'Fail konfigurasi lalai mengandungi konfigurasi masa jalan NVIDIA, tetapi perkhidmatan Docker semasa belum memuatkannya. Sahkan fail konfigurasi, mulakan semula Docker dan semak sekali lagi.',
|
||||
dockerConfigUnreadable: 'Fail konfigurasi Docker tidak dapat dibaca. Lihat butiran semakan.',
|
||||
dockerUnconfigured: 'Konfigurasi NVIDIA GPU Docker belum lengkap.',
|
||||
dockerFailed: 'Konfigurasi NVIDIA GPU Docker tidak dapat disemak. Lihat butiran kegagalan.',
|
||||
},
|
||||
systemMemory: 'Memori sistem dikongsi',
|
||||
systemMemoryUsed: 'Memori sistem digunakan',
|
||||
systemMemoryTotal: 'Jumlah memori sistem',
|
||||
@@ -1838,6 +1905,8 @@ const message = {
|
||||
pcieWidth: 'Lebar PCIe (semasa / maksimum)',
|
||||
gpu: 'Pengurusan GPU',
|
||||
monitoringTitle: 'Pemantauan GPU',
|
||||
currentHelper:
|
||||
'Lihat status masa nyata dan penggunaan sumber peranti GPU/NPU/XPU pada nod semasa. Ringkasan di bahagian atas merangkumi semua peranti; metrik terperinci dipaparkan mengikut peranti.',
|
||||
gpuHelper: 'Tiada arahan pengurusan GPU/XPU/NPU yang disokong dikesan. Sila periksa dan cuba lagi!',
|
||||
process: 'Maklumat Proses',
|
||||
type: 'Jenis',
|
||||
@@ -2091,6 +2160,8 @@ const message = {
|
||||
statusUnavailable: 'Service status unavailable. Refresh to retry.',
|
||||
},
|
||||
benchmarkPlan: {
|
||||
logAutoRefresh: 'Muat semula automatik',
|
||||
logWindowHelp: 'Hanya log terkini dipaparkan. Muat turun fail log yang dipilih untuk melihat kandungan penuh.',
|
||||
prefixTokens: 'Token awalan',
|
||||
seed: 'Benih rawak',
|
||||
report: 'Laporan',
|
||||
@@ -2311,6 +2382,12 @@ const message = {
|
||||
missing: 'Tiada',
|
||||
},
|
||||
container: {
|
||||
nvidiaSupport: 'Sokongan GPU NVIDIA',
|
||||
nvidiaHelper:
|
||||
'Daftarkan runtime kontena NVIDIA. Pasang NVIDIA Container Toolkit dahulu. Peruntukkan GPU kepada setiap kontena secara berasingan.',
|
||||
nvidiaLocalOnly: 'Hanya soket Docker lalai setempat disokong.',
|
||||
nvidiaChangeHelper:
|
||||
'Melumpuhkan mungkin menghalang kontena yang menggunakan runtime NVIDIA daripada bermula. Suis ini tidak menyekat akses GPU.',
|
||||
imageRemoteSearch: 'Carian jauh',
|
||||
imageRemoteSearchHelper: 'Hanya imej repositori Docker Hub boleh dicari',
|
||||
imageRemoteSearchProxyRequired:
|
||||
|
||||
@@ -1776,6 +1776,80 @@ const message = {
|
||||
timePerOutputTokenChart: 'Latência entre tokens',
|
||||
},
|
||||
gpu: {
|
||||
environment: {
|
||||
hostGPU: 'GPU do host',
|
||||
containerGPU: 'Suporte a GPU em contêineres',
|
||||
monitorAvailable: 'Monitoramento disponível',
|
||||
monitorPartial: 'Monitoramento parcial',
|
||||
notReady: 'Não está pronto',
|
||||
ready: 'Pronto',
|
||||
checkedAt: 'Verificado em',
|
||||
checkDetailsHelp: 'Confira os detalhes, verifique o ambiente e execute a verificação novamente.',
|
||||
driverActionTitle: 'Verifique primeiro o driver da GPU do host',
|
||||
driverActionHelp:
|
||||
'Instale ou repare o driver do dispositivo afetado antes de continuar a verificação do ambiente de contêineres.',
|
||||
driverGuide: 'Ver guia de instalação do driver',
|
||||
toolkitActionTitle: 'O suporte a GPU em contêineres não está pronto',
|
||||
toolkitActionHelp:
|
||||
'Instale ou repare o NVIDIA Container Toolkit e depois verifique a configuração do Docker.',
|
||||
toolkitGuide: 'Ver guia de instalação do Toolkit',
|
||||
dockerActionTitle: 'O suporte a NVIDIA GPU no Docker não está pronto',
|
||||
dockerActionHelp:
|
||||
'Verifique a configuração do runtime NVIDIA no Docker e execute a verificação novamente.',
|
||||
monitorUnaffected: 'Este problema não afeta o monitoramento da GPU do host.',
|
||||
title: 'Verificações do ambiente',
|
||||
passedLabel: 'Verificação aprovada',
|
||||
issuesLabel: 'Problemas no ambiente · {count}',
|
||||
loadFailedLabel: 'Falha na verificação',
|
||||
notDetectedLabel: 'Nenhum dispositivo detectado',
|
||||
unknownLabel: 'Estado desconhecido',
|
||||
passed: 'Verificações do ambiente concluídas com sucesso',
|
||||
issues: 'Problemas de ambiente detectados: {count}. Siga as orientações para verificá-los.',
|
||||
recheck: 'Verificar novamente',
|
||||
installGuide: 'Ver guia de instalação',
|
||||
goContainerSettings: 'Abrir configurações de contêineres',
|
||||
details: 'Detalhes da verificação',
|
||||
vendor: 'Fabricante',
|
||||
gpuCount: 'Quantidade de GPUs',
|
||||
cardCount: 'Quantidade de placas',
|
||||
notDetected: 'Nenhum dispositivo GPU/NPU/XPU compatível foi detectado no nó atual.',
|
||||
unknown:
|
||||
'Não foi possível confirmar o estado do dispositivo. Consulte os detalhes e verifique novamente.',
|
||||
loadFailed: 'Não foi possível ler as informações do dispositivo. Tente novamente.',
|
||||
notApplicable: 'Não se aplica',
|
||||
cudaHelper:
|
||||
'Esta é a versão do CUDA compatível com o driver, não a versão do CUDA Toolkit instalada no host.',
|
||||
partialMemory: 'Alguns dados de memória estão indisponíveis',
|
||||
sharedMemory: 'Memória compartilhada do sistema: {memory}',
|
||||
driver: 'Driver e ferramentas de gerenciamento de dispositivos',
|
||||
nvidiaDriver: 'Driver NVIDIA',
|
||||
toolkit: 'NVIDIA Container Toolkit',
|
||||
docker: 'Configuração de GPU NVIDIA no Docker',
|
||||
statusPassed: 'Normal',
|
||||
statusMissing: 'Não detectado',
|
||||
statusUnconfigured: 'Configuração pendente',
|
||||
statusFailed: 'Falha na verificação',
|
||||
statusUnknown: 'Desconhecido',
|
||||
driverMissing:
|
||||
'O driver {vendor} não foi detectado. Instale ou verifique o driver (ferramenta de detecção: {tool}).',
|
||||
driverFailed:
|
||||
'Não foi possível ler as informações do dispositivo usando {tool}. Verifique o estado do driver.',
|
||||
driverUnknown: 'Não foi possível confirmar o estado do driver do dispositivo. Verifique {tool}.',
|
||||
toolkitMissing:
|
||||
'NVIDIA Container Toolkit não foi detectado. Instale e configure a ferramenta antes de usar GPUs NVIDIA em contêineres.',
|
||||
toolkitFailed: 'Não foi possível verificar o NVIDIA Container Toolkit. Consulte os detalhes da falha.',
|
||||
dockerConfigMissing:
|
||||
'Nenhuma entrada runtimes.nvidia foi detectada no arquivo de configuração padrão do Docker. Conclua a configuração seguindo a documentação.',
|
||||
dockerConfigInvalid:
|
||||
'A configuração do ambiente de execução NVIDIA no Docker é inválida. Verifique runtimes.nvidia.path no daemon.json.',
|
||||
dockerConfigPending:
|
||||
'O arquivo de configuração padrão contém a configuração do ambiente de execução NVIDIA, mas o serviço Docker atual ainda não a carregou. Confirme o arquivo de configuração, reinicie o Docker e verifique novamente.',
|
||||
dockerConfigUnreadable:
|
||||
'Não foi possível ler o arquivo de configuração do Docker. Consulte os detalhes da verificação.',
|
||||
dockerUnconfigured: 'A configuração de GPU NVIDIA no Docker está incompleta.',
|
||||
dockerFailed:
|
||||
'Não foi possível verificar a configuração de GPU NVIDIA no Docker. Consulte os detalhes da falha.',
|
||||
},
|
||||
systemMemory: 'Memória compartilhada do sistema',
|
||||
systemMemoryUsed: 'Memória do sistema em uso',
|
||||
systemMemoryTotal: 'Memória total do sistema',
|
||||
@@ -1836,6 +1910,8 @@ const message = {
|
||||
pcieWidth: 'Largura PCIe (atual / máxima)',
|
||||
gpu: 'Gerenciamento de GPU',
|
||||
monitoringTitle: 'Monitoramento de GPU',
|
||||
currentHelper:
|
||||
'Veja o estado em tempo real e o uso de recursos dos dispositivos GPU/NPU/XPU no nó atual. O resumo superior inclui todos os dispositivos; as métricas detalhadas são exibidas por dispositivo.',
|
||||
gpuHelper:
|
||||
'Nenhum comando de gerenciamento de GPU/XPU/NPU compatível foi detectado. Verifique e tente novamente!',
|
||||
process: 'Informações do Processo',
|
||||
@@ -2090,6 +2166,8 @@ const message = {
|
||||
statusUnavailable: 'Service status unavailable. Refresh to retry.',
|
||||
},
|
||||
benchmarkPlan: {
|
||||
logAutoRefresh: 'Atualização automática',
|
||||
logWindowHelp: 'Apenas os logs recentes são exibidos. Baixe o arquivo selecionado para ver todo o conteúdo.',
|
||||
prefixTokens: 'Tokens de prefixo',
|
||||
seed: 'Semente aleatória',
|
||||
report: 'Relatório',
|
||||
@@ -2311,6 +2389,12 @@ const message = {
|
||||
missing: 'Não existe',
|
||||
},
|
||||
container: {
|
||||
nvidiaSupport: 'Suporte a GPU NVIDIA',
|
||||
nvidiaHelper:
|
||||
'Registra o runtime de contêiner NVIDIA. Instale primeiro o NVIDIA Container Toolkit. Atribua GPUs a cada contêiner separadamente.',
|
||||
nvidiaLocalOnly: 'Apenas o socket padrão do Docker local é suportado.',
|
||||
nvidiaChangeHelper:
|
||||
'Desativar pode impedir a inicialização de contêineres que usam o runtime NVIDIA. Esta opção não bloqueia o acesso à GPU.',
|
||||
imageRemoteSearch: 'Pesquisa remota',
|
||||
imageRemoteSearchHelper: 'Apenas imagens de repositórios do Docker Hub podem ser pesquisadas',
|
||||
imageRemoteSearchProxyRequired:
|
||||
|
||||
@@ -1768,6 +1768,78 @@ const message = {
|
||||
timePerOutputTokenChart: 'Задержка между токенами',
|
||||
},
|
||||
gpu: {
|
||||
environment: {
|
||||
hostGPU: 'GPU хоста',
|
||||
containerGPU: 'Поддержка GPU в контейнерах',
|
||||
monitorAvailable: 'Мониторинг доступен',
|
||||
monitorPartial: 'Частичный мониторинг',
|
||||
notReady: 'Не готово',
|
||||
ready: 'Готово',
|
||||
checkedAt: 'Время проверки',
|
||||
checkDetailsHelp: 'Изучите результаты проверки, проверьте состояние среды и повторите проверку.',
|
||||
driverActionTitle: 'Сначала проверьте драйвер GPU хоста',
|
||||
driverActionHelp:
|
||||
'Установите или восстановите драйвер проблемного устройства, затем продолжите проверку среды контейнеров.',
|
||||
driverGuide: 'Инструкция по установке драйвера',
|
||||
toolkitActionTitle: 'Поддержка GPU в контейнерах не готова',
|
||||
toolkitActionHelp:
|
||||
'Установите или восстановите NVIDIA Container Toolkit, затем проверьте настройки Docker.',
|
||||
toolkitGuide: 'Инструкция по установке Toolkit',
|
||||
dockerActionTitle: 'Поддержка NVIDIA GPU в Docker не готова',
|
||||
dockerActionHelp: 'Проверьте настройки среды выполнения NVIDIA в Docker и повторите проверку.',
|
||||
monitorUnaffected: 'Эта проблема не влияет на мониторинг GPU хоста.',
|
||||
title: 'Проверка окружения',
|
||||
passedLabel: 'Проверка пройдена',
|
||||
issuesLabel: 'Проблемы окружения · {count}',
|
||||
loadFailedLabel: 'Ошибка проверки',
|
||||
notDetectedLabel: 'Устройства не обнаружены',
|
||||
unknownLabel: 'Статус неизвестен',
|
||||
passed: 'Проверки окружения пройдены',
|
||||
issues: 'Обнаружено проблем окружения: {count}. Проверьте их согласно подсказкам.',
|
||||
recheck: 'Проверить снова',
|
||||
installGuide: 'Руководство по установке',
|
||||
goContainerSettings: 'Открыть настройки контейнеров',
|
||||
details: 'Подробности проверки',
|
||||
vendor: 'Производитель',
|
||||
gpuCount: 'Количество GPU',
|
||||
cardCount: 'Количество карт',
|
||||
notDetected: 'На текущем узле не обнаружены поддерживаемые устройства GPU/NPU/XPU.',
|
||||
unknown:
|
||||
'Не удалось определить состояние устройства. Просмотрите подробности проверки и повторите попытку.',
|
||||
loadFailed: 'Не удалось прочитать сведения об устройстве. Повторите попытку.',
|
||||
notApplicable: 'Не применимо',
|
||||
cudaHelper:
|
||||
'Это версия CUDA, поддерживаемая драйвером, а не версия CUDA Toolkit, установленная на хосте.',
|
||||
partialMemory: 'Часть данных о памяти недоступна',
|
||||
sharedMemory: 'Общая системная память: {memory}',
|
||||
driver: 'Драйвер и средства управления устройствами',
|
||||
nvidiaDriver: 'Драйвер NVIDIA',
|
||||
toolkit: 'NVIDIA Container Toolkit',
|
||||
docker: 'Настройка NVIDIA GPU в Docker',
|
||||
statusPassed: 'Норма',
|
||||
statusMissing: 'Не обнаружено',
|
||||
statusUnconfigured: 'Требуется настройка',
|
||||
statusFailed: 'Ошибка проверки',
|
||||
statusUnknown: 'Неизвестно',
|
||||
driverMissing:
|
||||
'Драйвер {vendor} не обнаружен. Установите или проверьте драйвер (средство обнаружения: {tool}).',
|
||||
driverFailed:
|
||||
'Не удалось прочитать сведения об устройстве с помощью {tool}. Проверьте состояние драйвера.',
|
||||
driverUnknown: 'Не удалось определить состояние драйвера устройства. Проверьте {tool}.',
|
||||
toolkitMissing:
|
||||
'NVIDIA Container Toolkit не обнаружен. Установите и настройте его перед использованием NVIDIA GPU в контейнерах.',
|
||||
toolkitFailed: 'Не удалось проверить NVIDIA Container Toolkit. Просмотрите сведения об ошибке.',
|
||||
dockerConfigMissing:
|
||||
'Запись runtimes.nvidia не обнаружена в файле конфигурации Docker по умолчанию. Завершите настройку согласно документации.',
|
||||
dockerConfigInvalid:
|
||||
'Конфигурация среды выполнения NVIDIA в Docker некорректна. Проверьте runtimes.nvidia.path в daemon.json.',
|
||||
dockerConfigPending:
|
||||
'Файл конфигурации по умолчанию содержит настройки среды выполнения NVIDIA, но текущая служба Docker ещё не загрузила их. Проверьте файл конфигурации, перезапустите Docker и повторите проверку.',
|
||||
dockerConfigUnreadable:
|
||||
'Не удалось прочитать файл конфигурации Docker. Просмотрите подробности проверки.',
|
||||
dockerUnconfigured: 'Настройка NVIDIA GPU в Docker не завершена.',
|
||||
dockerFailed: 'Не удалось проверить настройку NVIDIA GPU в Docker. Просмотрите сведения об ошибке.',
|
||||
},
|
||||
systemMemory: 'Общая системная память',
|
||||
systemMemoryUsed: 'Использовано системной памяти',
|
||||
systemMemoryTotal: 'Всего системной памяти',
|
||||
@@ -1828,6 +1900,8 @@ const message = {
|
||||
pcieWidth: 'Ширина PCIe (текущая / максимальная)',
|
||||
gpu: 'Управление GPU',
|
||||
monitoringTitle: 'Мониторинг GPU',
|
||||
currentHelper:
|
||||
'Просматривайте состояние и использование ресурсов устройств GPU/NPU/XPU текущего узла в реальном времени. Вверху показана сводка по всем устройствам, а подробные показатели отображаются для каждого устройства отдельно.',
|
||||
gpuHelper: 'Поддерживаемая команда управления GPU/XPU/NPU не обнаружена. Проверьте и повторите попытку!',
|
||||
process: 'Информация о Процессе',
|
||||
type: 'Тип',
|
||||
@@ -2081,6 +2155,9 @@ const message = {
|
||||
statusUnavailable: 'Service status unavailable. Refresh to retry.',
|
||||
},
|
||||
benchmarkPlan: {
|
||||
logAutoRefresh: 'Автообновление',
|
||||
logWindowHelp:
|
||||
'Показана только последняя часть журнала. Скачайте выбранный файл, чтобы посмотреть его полностью.',
|
||||
prefixTokens: 'Токены префикса',
|
||||
seed: 'Начальное значение генератора случайных чисел',
|
||||
report: 'Отчёт',
|
||||
@@ -2304,6 +2381,12 @@ const message = {
|
||||
missing: 'Отсутствует',
|
||||
},
|
||||
container: {
|
||||
nvidiaSupport: 'Поддержка GPU NVIDIA',
|
||||
nvidiaHelper:
|
||||
'Регистрирует среду выполнения контейнеров NVIDIA. Сначала установите NVIDIA Container Toolkit. Назначайте GPU каждому контейнеру отдельно.',
|
||||
nvidiaLocalOnly: 'Поддерживается только стандартный локальный сокет Docker.',
|
||||
nvidiaChangeHelper:
|
||||
'Отключение может помешать запуску контейнеров со средой выполнения NVIDIA. Этот переключатель не блокирует доступ к GPU.',
|
||||
imageRemoteSearch: 'Удалённый поиск',
|
||||
imageRemoteSearchHelper: 'Поддерживается поиск образов только в репозиториях Docker Hub',
|
||||
imageRemoteSearchProxyRequired: 'Для поиска удалённых образов в текущей среде сначала настройте прокси-сервер.',
|
||||
|
||||
@@ -1772,6 +1772,76 @@ const message = {
|
||||
timePerOutputTokenChart: 'Token arası gecikme',
|
||||
},
|
||||
gpu: {
|
||||
environment: {
|
||||
hostGPU: 'Ana makine GPU’su',
|
||||
containerGPU: 'Konteyner GPU desteği',
|
||||
monitorAvailable: 'İzleme kullanılabilir',
|
||||
monitorPartial: 'Kısmi izleme',
|
||||
notReady: 'Hazır değil',
|
||||
ready: 'Hazır',
|
||||
checkedAt: 'Kontrol zamanı',
|
||||
checkDetailsHelp: 'Kontrol ayrıntılarını inceleyin, ortam durumunu doğrulayın ve yeniden kontrol edin.',
|
||||
driverActionTitle: 'Önce ana makine GPU sürücüsünü kontrol edin',
|
||||
driverActionHelp:
|
||||
'Konteyner ortamını kontrol etmeye devam etmeden önce sorunlu aygıtın sürücüsünü kurun veya onarın.',
|
||||
driverGuide: 'Sürücü kurulum kılavuzunu görüntüle',
|
||||
toolkitActionTitle: 'Konteyner GPU desteği hazır değil',
|
||||
toolkitActionHelp:
|
||||
'Önce NVIDIA Container Toolkit’i kurun veya onarın, ardından Docker yapılandırmasını kontrol edin.',
|
||||
toolkitGuide: 'Toolkit kurulum kılavuzunu görüntüle',
|
||||
dockerActionTitle: 'Docker NVIDIA GPU desteği hazır değil',
|
||||
dockerActionHelp:
|
||||
'Docker içindeki NVIDIA çalışma zamanı yapılandırmasını kontrol edip yeniden deneyin.',
|
||||
monitorUnaffected: 'Bu sorun ana makine GPU izlemesini etkilemez.',
|
||||
title: 'Ortam denetimleri',
|
||||
passedLabel: 'Kontrol başarılı',
|
||||
issuesLabel: 'Ortam sorunları · {count}',
|
||||
loadFailedLabel: 'Kontrol başarısız',
|
||||
notDetectedLabel: 'Cihaz algılanmadı',
|
||||
unknownLabel: 'Durum bilinmiyor',
|
||||
passed: 'Ortam denetimleri başarılı',
|
||||
issues: '{count} ortam sorunu algılandı. Denetlemek için yönergeleri izleyin.',
|
||||
recheck: 'Yeniden denetle',
|
||||
installGuide: 'Kurulum kılavuzunu görüntüle',
|
||||
goContainerSettings: 'Kapsayıcı ayarlarını aç',
|
||||
details: 'Denetim ayrıntıları',
|
||||
vendor: 'Üretici',
|
||||
gpuCount: 'GPU sayısı',
|
||||
cardCount: 'Kart sayısı',
|
||||
notDetected: 'Geçerli düğümde desteklenen GPU/NPU/XPU aygıtı algılanmadı.',
|
||||
unknown: 'Aygıt durumu doğrulanamadı. Denetim ayrıntılarını inceleyip yeniden deneyin.',
|
||||
loadFailed: 'Aygıt bilgileri okunamadı. Lütfen yeniden deneyin.',
|
||||
notApplicable: 'Uygulanamaz',
|
||||
cudaHelper:
|
||||
'Bu, sürücünün desteklediği CUDA sürümüdür; ana makinede kurulu CUDA Toolkit sürümü değildir.',
|
||||
partialMemory: 'Bazı bellek verileri alınamadı',
|
||||
sharedMemory: 'Paylaşılan sistem belleği: {memory}',
|
||||
driver: 'Sürücü ve aygıt yönetim araçları',
|
||||
nvidiaDriver: 'NVIDIA sürücüsü',
|
||||
toolkit: 'NVIDIA Container Toolkit',
|
||||
docker: 'Docker NVIDIA GPU yapılandırması',
|
||||
statusPassed: 'Normal',
|
||||
statusMissing: 'Algılanmadı',
|
||||
statusUnconfigured: 'Yapılandırma gerekli',
|
||||
statusFailed: 'Denetim başarısız',
|
||||
statusUnknown: 'Bilinmiyor',
|
||||
driverMissing:
|
||||
'{vendor} sürücüsü algılanmadı. Sürücüyü kurun veya denetleyin (algılama aracı: {tool}).',
|
||||
driverFailed: 'Aygıt bilgileri {tool} ile okunamadı. Sürücü durumunu denetleyin.',
|
||||
driverUnknown: 'Aygıt sürücüsünün durumu doğrulanamadı. {tool} aracını denetleyin.',
|
||||
toolkitMissing:
|
||||
'NVIDIA Container Toolkit algılanmadı. Kapsayıcılarda NVIDIA GPU kullanmadan önce kurulum ve yapılandırmayı tamamlayın.',
|
||||
toolkitFailed: 'NVIDIA Container Toolkit denetlenemedi. Hata ayrıntılarını inceleyin.',
|
||||
dockerConfigMissing:
|
||||
'Varsayılan Docker yapılandırma dosyasında runtimes.nvidia girdisi algılanmadı. Yapılandırmayı belgelere göre tamamlayın.',
|
||||
dockerConfigInvalid:
|
||||
'Docker NVIDIA çalışma zamanı yapılandırması geçersiz. daemon.json dosyasındaki runtimes.nvidia.path değerini denetleyin.',
|
||||
dockerConfigPending:
|
||||
'Varsayılan yapılandırma dosyasında NVIDIA çalışma zamanı yapılandırması var, ancak mevcut Docker hizmeti bunu henüz yüklemedi. Yapılandırma dosyasını doğrulayın, Docker hizmetini yeniden başlatıp tekrar denetleyin.',
|
||||
dockerConfigUnreadable: 'Docker yapılandırma dosyası okunamadı. Denetim ayrıntılarını inceleyin.',
|
||||
dockerUnconfigured: 'Docker NVIDIA GPU yapılandırması tamamlanmadı.',
|
||||
dockerFailed: 'Docker NVIDIA GPU yapılandırması denetlenemedi. Hata ayrıntılarını inceleyin.',
|
||||
},
|
||||
systemMemory: 'Paylaşılan sistem belleği',
|
||||
systemMemoryUsed: 'Kullanılan sistem belleği',
|
||||
systemMemoryTotal: 'Toplam sistem belleği',
|
||||
@@ -1831,6 +1901,8 @@ const message = {
|
||||
pcieWidth: 'PCIe genişliği (geçerli / en yüksek)',
|
||||
gpu: 'GPU Yönetimi',
|
||||
monitoringTitle: 'GPU İzleme',
|
||||
currentHelper:
|
||||
'Geçerli düğümdeki GPU/NPU/XPU aygıtlarının anlık durumunu ve kaynak kullanımını görüntüleyin. Üstteki özet tüm aygıtları kapsar; ayrıntılı ölçümler aygıt bazında gösterilir.',
|
||||
gpuHelper: 'Desteklenen bir GPU/XPU/NPU yönetim komutu algılanmadı. Lütfen kontrol edip tekrar deneyin!',
|
||||
process: 'İşlem Bilgisi',
|
||||
type: 'Tür',
|
||||
@@ -2083,6 +2155,9 @@ const message = {
|
||||
statusUnavailable: 'Service status unavailable. Refresh to retry.',
|
||||
},
|
||||
benchmarkPlan: {
|
||||
logAutoRefresh: 'Otomatik yenileme',
|
||||
logWindowHelp:
|
||||
'Yalnızca son günlük kayıtları gösterilir. Tam içeriği görmek için seçili günlük dosyasını indirin.',
|
||||
prefixTokens: 'Önek token sayısı',
|
||||
seed: 'Rastgelelik tohumu',
|
||||
report: 'Rapor',
|
||||
@@ -2302,6 +2377,12 @@ const message = {
|
||||
missing: 'Mevcut değil',
|
||||
},
|
||||
container: {
|
||||
nvidiaSupport: 'NVIDIA GPU desteği',
|
||||
nvidiaHelper:
|
||||
'NVIDIA konteyner çalışma zamanını kaydeder. Önce NVIDIA Container Toolkit kurun. GPU’ları her konteynere ayrı ayrı atayın.',
|
||||
nvidiaLocalOnly: 'Yalnızca yerel varsayılan Docker soketi desteklenir.',
|
||||
nvidiaChangeHelper:
|
||||
'Devre dışı bırakmak NVIDIA çalışma zamanını kullanan konteynerlerin başlamasını engelleyebilir. Bu anahtar GPU erişimini engellemez.',
|
||||
imageRemoteSearch: 'Uzak arama',
|
||||
imageRemoteSearchHelper: 'Yalnızca Docker Hub deposu imajları aranabilir',
|
||||
imageRemoteSearchProxyRequired:
|
||||
|
||||
@@ -1636,8 +1636,8 @@ const message = {
|
||||
running: '執行中請求',
|
||||
waiting: '等待中請求',
|
||||
cacheUsage: 'KV Cache 使用率',
|
||||
promptThroughput: '輸入 Token 吞吐',
|
||||
generationThroughput: '輸出 Token 吞吐',
|
||||
promptThroughput: '輸入 Token 速率',
|
||||
generationThroughput: '輸出 Token 速率',
|
||||
requestThroughput: '完成請求速率',
|
||||
timeToFirstToken: '平均首 Token 延遲',
|
||||
timePerOutputToken: '平均 Token 間延遲',
|
||||
@@ -1656,6 +1656,69 @@ const message = {
|
||||
timePerOutputTokenChart: 'Token 間延遲',
|
||||
},
|
||||
gpu: {
|
||||
environment: {
|
||||
hostGPU: '主機 GPU',
|
||||
containerGPU: '容器 GPU 支援',
|
||||
monitorAvailable: '可監控',
|
||||
monitorPartial: '部分可監控',
|
||||
notReady: '尚未就緒',
|
||||
ready: '已就緒',
|
||||
checkedAt: '檢測時間',
|
||||
checkDetailsHelp: '請查看檢測詳情,確認環境狀態後重新檢測。',
|
||||
driverActionTitle: '優先檢查主機 GPU 驅動程式',
|
||||
driverActionHelp: '請先安裝或修復異常裝置的驅動程式,驅動程式就緒後再繼續檢查容器環境。',
|
||||
driverGuide: '查看驅動程式安裝指引',
|
||||
toolkitActionTitle: '容器 GPU 支援尚未就緒',
|
||||
toolkitActionHelp: '請先安裝或修復 NVIDIA Container Toolkit,再檢查 Docker 設定。',
|
||||
toolkitGuide: '查看 Toolkit 安裝指引',
|
||||
dockerActionTitle: 'Docker 的 NVIDIA GPU 支援尚未就緒',
|
||||
dockerActionHelp: '請檢查 Docker 的 NVIDIA 執行環境設定,完成後重新檢測。',
|
||||
monitorUnaffected: '主機 GPU 監控不受此問題影響。',
|
||||
title: '環境檢查',
|
||||
passedLabel: '檢查通過',
|
||||
issuesLabel: '環境異常 · {count} 項',
|
||||
loadFailedLabel: '檢查失敗',
|
||||
notDetectedLabel: '未偵測到裝置',
|
||||
unknownLabel: '狀態未知',
|
||||
passed: '環境檢查通過',
|
||||
issues: '偵測到 {count} 項環境問題,請依照提示檢查。',
|
||||
recheck: '重新偵測',
|
||||
installGuide: '查看安裝文件',
|
||||
goContainerSettings: '前往容器設定',
|
||||
details: '偵測詳情',
|
||||
vendor: '廠商',
|
||||
gpuCount: 'GPU 數量',
|
||||
cardCount: '卡數量',
|
||||
notDetected: '目前節點未偵測到支援的 GPU/NPU/XPU 裝置。',
|
||||
unknown: '無法確認裝置狀態,請查看偵測詳情後重新偵測。',
|
||||
loadFailed: '無法讀取裝置資訊,請重試。',
|
||||
notApplicable: '不適用',
|
||||
cudaHelper: '此版本表示驅動程式支援的 CUDA 版本,不代表主機已安裝的 CUDA Toolkit。',
|
||||
partialMemory: '部分顯示記憶體資料未取得',
|
||||
sharedMemory: '共用系統記憶體:{memory}',
|
||||
driver: '驅動程式與裝置管理工具',
|
||||
nvidiaDriver: 'NVIDIA 驅動',
|
||||
toolkit: 'NVIDIA Container Toolkit',
|
||||
docker: 'Docker NVIDIA GPU 設定',
|
||||
statusPassed: '正常',
|
||||
statusMissing: '未偵測到',
|
||||
statusUnconfigured: '待設定',
|
||||
statusFailed: '偵測失敗',
|
||||
statusUnknown: '未知',
|
||||
driverMissing: '未偵測到 {vendor} 驅動程式,請安裝或檢查驅動程式(偵測工具:{tool})。',
|
||||
driverFailed: '無法透過 {tool} 讀取裝置資訊,請檢查驅動程式狀態。',
|
||||
driverUnknown: '無法確認裝置驅動程式狀態,請檢查 {tool}。',
|
||||
toolkitMissing: '未偵測到 NVIDIA Container Toolkit,容器使用 NVIDIA GPU 前需完成安裝與設定。',
|
||||
toolkitFailed: '無法偵測 NVIDIA Container Toolkit,請查看失敗原因。',
|
||||
dockerConfigMissing: 'Docker 預設設定檔中未偵測到 runtimes.nvidia,請依照文件完成設定。',
|
||||
dockerConfigInvalid:
|
||||
'Docker 的 NVIDIA 執行階段設定有誤,請檢查 daemon.json 中的 runtimes.nvidia.path。',
|
||||
dockerConfigPending:
|
||||
'預設設定檔中已存在 NVIDIA 執行階段設定,但目前 Docker 尚未載入,請確認設定檔並重新啟動 Docker 後重新偵測。',
|
||||
dockerConfigUnreadable: '無法讀取 Docker 設定檔,請查看偵測詳情。',
|
||||
dockerUnconfigured: '尚未完成 Docker 的 NVIDIA GPU 設定。',
|
||||
dockerFailed: '無法偵測 Docker 的 NVIDIA GPU 設定,請查看失敗原因。',
|
||||
},
|
||||
systemMemory: '系統共享記憶體',
|
||||
systemMemoryUsed: '系統已用記憶體',
|
||||
systemMemoryTotal: '系統總記憶體',
|
||||
@@ -1713,6 +1776,8 @@ const message = {
|
||||
pcieWidth: 'PCIe 寬度(目前 / 最大)',
|
||||
gpu: 'GPU 管理',
|
||||
monitoringTitle: 'GPU 監控',
|
||||
currentHelper:
|
||||
'查看目前節點 GPU/NPU/XPU 裝置的即時運作狀態與資源使用情況。頂部彙總所有裝置,詳細指標依裝置顯示。',
|
||||
gpuHelper: '目前系統未偵測到支援的 GPU/XPU/NPU 管理指令,請檢查後重試',
|
||||
process: '行程資訊',
|
||||
type: '類型',
|
||||
@@ -1953,6 +2018,8 @@ const message = {
|
||||
statusUnavailable: '無法取得服務狀態,請重新整理後重試。',
|
||||
},
|
||||
benchmarkPlan: {
|
||||
logAutoRefresh: '自動重新整理',
|
||||
logWindowHelp: '僅顯示最近部分日誌,可下載目前日誌檔案查看完整內容。',
|
||||
prefixTokens: '前綴 Token 數',
|
||||
seed: '隨機種子',
|
||||
report: '報告',
|
||||
@@ -2148,6 +2215,10 @@ const message = {
|
||||
missing: '不存在',
|
||||
},
|
||||
container: {
|
||||
nvidiaSupport: 'NVIDIA GPU 支援',
|
||||
nvidiaHelper: '註冊 NVIDIA 容器執行環境,需先安裝 NVIDIA Container Toolkit。各容器仍需個別分配 GPU。',
|
||||
nvidiaLocalOnly: '僅支援本機預設 Docker Socket。',
|
||||
nvidiaChangeHelper: '關閉後,使用 NVIDIA 執行環境的容器可能無法啟動;此開關不會禁止容器存取 GPU。',
|
||||
imageRemoteSearch: '遠端搜尋',
|
||||
imageRemoteSearchHelper: '僅支援搜尋 Docker Hub 倉庫映像',
|
||||
imageRemoteSearchProxyRequired: '目前運行環境下,如需搜尋遠端映像,請先設定代理伺服器',
|
||||
|
||||
@@ -1649,8 +1649,8 @@ const message = {
|
||||
running: '运行中请求',
|
||||
waiting: '等待中请求',
|
||||
cacheUsage: 'KV Cache 使用率',
|
||||
promptThroughput: '输入 Token 吞吐',
|
||||
generationThroughput: '输出 Token 吞吐',
|
||||
promptThroughput: '输入 Token 速率',
|
||||
generationThroughput: '输出 Token 速率',
|
||||
requestThroughput: '完成请求速率',
|
||||
timeToFirstToken: '平均首 Token 延迟',
|
||||
timePerOutputToken: '平均 Token 间延迟',
|
||||
@@ -1669,6 +1669,68 @@ const message = {
|
||||
timePerOutputTokenChart: 'Token 间延迟',
|
||||
},
|
||||
gpu: {
|
||||
environment: {
|
||||
hostGPU: '宿主机 GPU',
|
||||
containerGPU: '容器 GPU 支持',
|
||||
monitorAvailable: '可监控',
|
||||
monitorPartial: '部分可监控',
|
||||
notReady: '未就绪',
|
||||
ready: '已就绪',
|
||||
checkedAt: '检测时间',
|
||||
checkDetailsHelp: '请查看检测详情,确认环境状态后重新检测。',
|
||||
driverActionTitle: '优先检查宿主机 GPU 驱动',
|
||||
driverActionHelp: '请先安装或修复异常设备的驱动,驱动就绪后再继续检查容器环境。',
|
||||
driverGuide: '查看驱动安装指引',
|
||||
toolkitActionTitle: '容器 GPU 支持未就绪',
|
||||
toolkitActionHelp: '请先安装或修复 NVIDIA Container Toolkit,再检查 Docker 配置。',
|
||||
toolkitGuide: '查看 Toolkit 安装指引',
|
||||
dockerActionTitle: 'Docker 的 NVIDIA GPU 支持未就绪',
|
||||
dockerActionHelp: '请检查 Docker 的 NVIDIA 运行时配置,完成后重新检测。',
|
||||
monitorUnaffected: '宿主机 GPU 监控不受此问题影响。',
|
||||
title: '环境检查',
|
||||
passedLabel: '检查通过',
|
||||
issuesLabel: '环境异常 · {count} 项',
|
||||
loadFailedLabel: '检查失败',
|
||||
notDetectedLabel: '未检测到设备',
|
||||
unknownLabel: '状态未知',
|
||||
passed: '环境检查通过',
|
||||
issues: '检测到 {count} 项环境问题,请根据提示检查。',
|
||||
recheck: '重新检测',
|
||||
installGuide: '查看安装文档',
|
||||
goContainerSettings: '前往容器配置',
|
||||
details: '检测详情',
|
||||
vendor: '厂商',
|
||||
gpuCount: 'GPU 数量',
|
||||
cardCount: '卡数量',
|
||||
notDetected: '当前节点未检测到支持的 GPU/NPU/XPU 设备。',
|
||||
unknown: '无法确认设备状态,请查看检测详情后重新检测。',
|
||||
loadFailed: '无法读取设备信息,请重试。',
|
||||
notApplicable: '不适用',
|
||||
cudaHelper: '此版本表示驱动支持的 CUDA 版本,不代表宿主机已安装的 CUDA Toolkit。',
|
||||
partialMemory: '部分显存数据未获取',
|
||||
sharedMemory: '共享系统内存:{memory}',
|
||||
driver: '驱动与设备管理工具',
|
||||
nvidiaDriver: 'NVIDIA 驱动',
|
||||
toolkit: 'NVIDIA Container Toolkit',
|
||||
docker: 'Docker NVIDIA GPU 配置',
|
||||
statusPassed: '正常',
|
||||
statusMissing: '未检测到',
|
||||
statusUnconfigured: '待配置',
|
||||
statusFailed: '检测失败',
|
||||
statusUnknown: '未知',
|
||||
driverMissing: '未检测到 {vendor} 驱动,请安装或检查驱动(检测工具:{tool})。',
|
||||
driverFailed: '无法通过 {tool} 读取设备信息,请检查驱动状态。',
|
||||
driverUnknown: '无法确认设备驱动状态,请检查 {tool}。',
|
||||
toolkitMissing: '未检测到 NVIDIA Container Toolkit,容器使用 NVIDIA GPU 前需完成安装与配置。',
|
||||
toolkitFailed: '无法检测 NVIDIA Container Toolkit,请查看失败原因。',
|
||||
dockerConfigMissing: 'Docker 默认配置文件中未检测到 runtimes.nvidia,请根据文档完成配置。',
|
||||
dockerConfigInvalid: 'Docker 的 NVIDIA 运行时配置有误,请检查 daemon.json 中的 runtimes.nvidia.path。',
|
||||
dockerConfigPending:
|
||||
'默认配置文件中已存在 NVIDIA 运行时配置,但当前 Docker 尚未加载,请确认配置文件并重启 Docker 后重新检测。',
|
||||
dockerConfigUnreadable: '无法读取 Docker 配置文件,请查看检测详情。',
|
||||
dockerUnconfigured: '尚未完成 Docker 的 NVIDIA GPU 配置。',
|
||||
dockerFailed: '无法检测 Docker 的 NVIDIA GPU 配置,请查看失败原因。',
|
||||
},
|
||||
systemMemory: '系统共享内存',
|
||||
systemMemoryUsed: '系统已用内存',
|
||||
systemMemoryTotal: '系统总内存',
|
||||
@@ -1726,6 +1788,8 @@ const message = {
|
||||
pcieWidth: 'PCIe 宽度(当前 / 最大)',
|
||||
gpu: 'GPU 管理',
|
||||
monitoringTitle: 'GPU 监控',
|
||||
currentHelper:
|
||||
'查看当前节点 GPU/NPU/XPU 设备的实时运行状态与资源使用情况。顶部汇总全部设备,详细指标按设备展示。',
|
||||
gpuHelper: '当前系统未检测到支持的 GPU/XPU/NPU 管理指令,请检查后重试!',
|
||||
process: '进程信息',
|
||||
processCount: '进程数',
|
||||
@@ -1962,6 +2026,8 @@ const message = {
|
||||
statusUnavailable: '无法获取服务状态,请刷新重试。',
|
||||
},
|
||||
benchmarkPlan: {
|
||||
logAutoRefresh: '自动刷新',
|
||||
logWindowHelp: '仅展示最近部分日志,可下载当前日志文件查看完整内容。',
|
||||
prefixTokens: '前缀 Token 数',
|
||||
seed: '随机种子',
|
||||
report: '报告',
|
||||
@@ -2157,6 +2223,10 @@ const message = {
|
||||
missing: '不存在',
|
||||
},
|
||||
container: {
|
||||
nvidiaSupport: 'NVIDIA GPU 支持',
|
||||
nvidiaHelper: '注册 NVIDIA 容器运行时,需要先安装 NVIDIA Container Toolkit。具体容器仍需单独分配 GPU。',
|
||||
nvidiaLocalOnly: '仅支持本机默认 Docker Socket。',
|
||||
nvidiaChangeHelper: '关闭后,使用 NVIDIA 运行时的容器可能无法启动;这不是禁止容器访问 GPU 的安全开关。',
|
||||
imageRemoteSearch: '远程搜索',
|
||||
imageRemoteSearchHelper: '仅支持搜索 Docker Hub 仓库镜像',
|
||||
imageRemoteSearchProxyRequired: '当前运行环境下,如需搜索远程镜像,请优先配置代理服务器',
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<el-menu
|
||||
v-else
|
||||
:default-active="activeMenu"
|
||||
:router="false"
|
||||
:router="true"
|
||||
:collapse="isCollapse"
|
||||
:collapse-transition="false"
|
||||
:unique-opened="!menuAccordion"
|
||||
|
||||
@@ -62,7 +62,7 @@ import TerminalDock from '@/components/terminal/dock/index.vue';
|
||||
import useResize from './hooks/useResize';
|
||||
import { MenuStore, TabsStore } from '@/store';
|
||||
import { getSystemAvailable } from '@/api/modules/setting';
|
||||
import { openMenu } from '@/utils/router';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { loadMasterProductProFromDB, loadProductProFromDB } from '@/utils/xpack';
|
||||
import { useTheme } from '@/global/use-theme';
|
||||
import TaskList from '@/components/task-list/index.vue';
|
||||
@@ -95,6 +95,8 @@ const openTask = () => {
|
||||
menuStore.setCollapse();
|
||||
}
|
||||
};
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const menuStore = MenuStore();
|
||||
|
||||
const tabsStore = TabsStore();
|
||||
@@ -149,7 +151,9 @@ watch(
|
||||
},
|
||||
);
|
||||
const handleMenuClick = async (path) => {
|
||||
await openMenu(path);
|
||||
await router.push({ path: path });
|
||||
tabsStore.addTab(route);
|
||||
tabsStore.activeTabPath = route.path;
|
||||
};
|
||||
|
||||
const toLogin = () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { panelCacheKey, writePanelCache, prunePanelCache } from '@/utils/panel-cache';
|
||||
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';
|
||||
@@ -11,6 +11,7 @@ import { TerminalDockSessionStore, TerminalSessionStore } from '@/store';
|
||||
|
||||
const axiosCanceler = new AxiosCanceler();
|
||||
|
||||
let isRedirecting = false;
|
||||
const enterpriseLicenseCheckWhiteList = ['EnterpriseLicenseRequired', 'entrance', 'login', 'Expired'];
|
||||
const noLoginWhiteList = ['entrance', 'login', 'file-share', '404', 'Expired'];
|
||||
|
||||
@@ -29,7 +30,7 @@ const clearLoginStatus = () => {
|
||||
TerminalDockSessionStore().closeAll();
|
||||
};
|
||||
|
||||
router.beforeEach(async (to) => {
|
||||
router.beforeEach(async (to, from) => {
|
||||
const { entrance, isEnterprise, isEnterpriseLicenseLoaded, isEnterpriseLicensed, isLogin, isOnRestart } =
|
||||
useGlobalStore();
|
||||
NProgress.start();
|
||||
@@ -102,11 +103,34 @@ router.beforeEach(async (to) => {
|
||||
if (to.name === 'Expired') {
|
||||
return true;
|
||||
}
|
||||
// Compatibility for old links: explicit navigation no longer needs to clear a cache.
|
||||
const activeMenuKey = panelCacheKey(String(to.meta.activeMenu || ''));
|
||||
if (to.query.uncached != undefined) {
|
||||
const query = { ...to.query };
|
||||
delete query.uncached;
|
||||
return { path: to.path, query, hash: to.hash };
|
||||
removePanelCache('tabs', activeMenuKey);
|
||||
return { path: to.path, query };
|
||||
}
|
||||
|
||||
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 &&
|
||||
cachedRouteInfo.meta.activeMenu === to.meta.activeMenu &&
|
||||
hasRouteAccess(cachedRouteInfo)
|
||||
) {
|
||||
isRedirecting = true;
|
||||
NProgress.done();
|
||||
return cachedRoute;
|
||||
}
|
||||
removePanelCache('tabs', activeMenuKey);
|
||||
}
|
||||
|
||||
if (!hasRouteAccess(to)) {
|
||||
@@ -120,10 +144,11 @@ router.beforeEach(async (to) => {
|
||||
router.afterEach((to, _from, failure) => {
|
||||
prunePanelCache();
|
||||
if (failure) {
|
||||
isRedirecting = false;
|
||||
NProgress.done();
|
||||
return;
|
||||
}
|
||||
if (to.meta.activeMenu && !to.meta.ignoreTab) {
|
||||
if (to.meta.activeMenu && !to.meta.ignoreTab && !isRedirecting) {
|
||||
let notMathParam = true;
|
||||
if (to.matched.some((record) => record.path.includes(':'))) {
|
||||
notMathParam = false;
|
||||
@@ -141,6 +166,7 @@ router.afterEach((to, _from, failure) => {
|
||||
}
|
||||
}
|
||||
|
||||
isRedirecting = false;
|
||||
NProgress.done();
|
||||
});
|
||||
|
||||
|
||||
Vendored
+1
@@ -28,5 +28,6 @@ declare interface RouterButton {
|
||||
path?: string;
|
||||
name?: string;
|
||||
count?: number;
|
||||
disabled?: boolean;
|
||||
permission?: string | string[];
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { hasRouteAccess } from '@/utils/rbac';
|
||||
import i18n from '@/lang';
|
||||
import { MsgError } from '@/utils/message';
|
||||
import type { RouteLocationRaw } from 'vue-router';
|
||||
import { panelCacheKey, readPanelCache, removePanelCache } from '@/utils/panel-cache';
|
||||
|
||||
const pushWithAccessCheck = async (to: RouteLocationRaw) => {
|
||||
const resolvedRoute = router.resolve(to);
|
||||
@@ -16,53 +15,24 @@ const pushWithAccessCheck = async (to: RouteLocationRaw) => {
|
||||
tabStoreMiddleWare();
|
||||
};
|
||||
|
||||
/** Explicit navigation always opens the requested page, independent of tab caching. */
|
||||
export const openPage = pushWithAccessCheck;
|
||||
|
||||
/** Only module-menu entry points restore the last visited route within that module. */
|
||||
export const openMenu = async (to: RouteLocationRaw) => {
|
||||
let entry = router.resolve(to);
|
||||
const visited = new Set<string>();
|
||||
// Menu roots often redirect and only their destination declares activeMenu.
|
||||
while (!visited.has(entry.fullPath)) {
|
||||
visited.add(entry.fullPath);
|
||||
const redirect = entry.matched[entry.matched.length - 1]?.redirect;
|
||||
if (!redirect) break;
|
||||
entry = router.resolve(typeof redirect === 'function' ? redirect(entry, router.currentRoute.value) : redirect);
|
||||
}
|
||||
const menu = entry.meta.activeMenu;
|
||||
if (menu) {
|
||||
const key = panelCacheKey(String(menu));
|
||||
const cached = readPanelCache<unknown>('tabs', key);
|
||||
if (typeof cached === 'string') {
|
||||
const destination = router.resolve(cached);
|
||||
if (destination.matched.length && destination.meta.activeMenu === menu && hasRouteAccess(destination)) {
|
||||
return openPage(cached);
|
||||
}
|
||||
}
|
||||
if (cached !== undefined) removePanelCache('tabs', key);
|
||||
}
|
||||
return openPage(to);
|
||||
};
|
||||
|
||||
export const routerToName = async (name: string) => {
|
||||
await openPage({ name: name });
|
||||
await pushWithAccessCheck({ name: name });
|
||||
};
|
||||
|
||||
export const routerToPath = async (path: string) => {
|
||||
await openPage({ path: path });
|
||||
await pushWithAccessCheck({ path: path });
|
||||
};
|
||||
|
||||
export const routerToFileWithPath = async (pathItem: string) => {
|
||||
await openPage({ name: 'File', query: { path: pathItem, uncached: 'true' } });
|
||||
await pushWithAccessCheck({ name: 'File', query: { path: pathItem, uncached: 'true' } });
|
||||
};
|
||||
|
||||
export const routerToNameWithQuery = async (name: string, query: any) => {
|
||||
await openPage({ name: name, query: query });
|
||||
await pushWithAccessCheck({ name: name, query: query });
|
||||
};
|
||||
|
||||
export const routerToPathWithQuery = async (path: string, query: any) => {
|
||||
await openPage({ path: path, query: query });
|
||||
await pushWithAccessCheck({ path: path, query: query });
|
||||
};
|
||||
|
||||
export const jumpToPath = (router: any, path: string) => {
|
||||
@@ -70,7 +40,7 @@ export const jumpToPath = (router: any, path: string) => {
|
||||
};
|
||||
|
||||
export const routerToNameWithParams = async (name: string, params: any) => {
|
||||
await openPage({ name: name, params: params });
|
||||
await pushWithAccessCheck({ name: name, params: params });
|
||||
};
|
||||
|
||||
const tabStoreMiddleWare = () => {
|
||||
|
||||
@@ -139,11 +139,11 @@ export const bindSearchState = (key: () => string | undefined, fields: SearchFie
|
||||
|
||||
interface SearchStateOptions {
|
||||
scope?: () => string | number | undefined;
|
||||
onRevisit?: () => unknown;
|
||||
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 = {}) => {
|
||||
export const useSearchState = (page: string, fields: SearchFields, options: SearchStateOptions) => {
|
||||
const { cacheUserID } = storeToRefs(GlobalStore());
|
||||
const binding = bindSearchState(() => {
|
||||
const scope = options.scope?.();
|
||||
@@ -160,6 +160,6 @@ export const useSearchState = (page: string, fields: SearchFields, options: Sear
|
||||
onActivated(async () => {
|
||||
if (!deactivated) return;
|
||||
deactivated = false;
|
||||
if (await binding.resume()) return options.onRevisit?.();
|
||||
if (await binding.resume()) return options.onRevisit();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
v-if="!noApp"
|
||||
v-model:view-mode="viewMode"
|
||||
:data="items"
|
||||
:cache-pagination="false"
|
||||
:pagination-config="paginationConfig"
|
||||
@search="search"
|
||||
>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div v-loading="loading">
|
||||
<RouterMenu />
|
||||
<template v-if="supported">
|
||||
<template v-if="supported && options.length">
|
||||
<el-card class="history-toolbar">
|
||||
<div class="flex flex-wrap gap-3 items-center">
|
||||
<el-select style="max-width: 100%" class="p-w-400" v-model="selectedDevice" @change="search">
|
||||
@@ -26,7 +26,10 @@
|
||||
:clearable="false"
|
||||
@change="changeTimeRange"
|
||||
/>
|
||||
<TableRefresh @search="refresh" />
|
||||
<TableSetting title="gpu-refresh" @search="refresh" />
|
||||
<el-tooltip :content="$t('aiTools.gpu.environment.recheck')">
|
||||
<TableRefresh :disabled="loading" @search="refresh" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="history?.sampleCount && history.bucketSeconds > 0" class="history-summary">
|
||||
@@ -67,26 +70,23 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
<LayoutContent v-else-if="!loading" :title="$t('aiTools.gpu.monitoringTitle')" :divider="true">
|
||||
<LayoutContent v-else :title="$t('aiTools.gpu.monitoringTitle')">
|
||||
<template #rightToolBar>
|
||||
<TableRefresh @search="refresh" />
|
||||
<TableSetting title="gpu-refresh" @search="refresh" />
|
||||
<el-tooltip :content="$t('aiTools.gpu.environment.recheck')">
|
||||
<TableRefresh :disabled="loading" @search="refresh" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template #main>
|
||||
<el-alert
|
||||
v-if="failed"
|
||||
class="common-prompt"
|
||||
:title="$t('aiTools.gpu.historyLoadFailed')"
|
||||
type="error"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
<div v-else class="app-warn">
|
||||
<div class="flx-center">
|
||||
<span>{{ $t('aiTools.gpu.gpuHelper') }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<img src="@/assets/images/no_app.svg" />
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-else-if="!loading" :description="$t('aiTools.gpu.environment.notDetected')" />
|
||||
</template>
|
||||
</LayoutContent>
|
||||
</div>
|
||||
@@ -371,6 +371,7 @@ const loadOptions = async () => {
|
||||
}
|
||||
};
|
||||
const refresh = () => {
|
||||
if (loading.value) return;
|
||||
if (followNow) timeRangeGlobal.value = [timeRangeGlobal.value[0], new Date()];
|
||||
void loadOptions();
|
||||
};
|
||||
|
||||
@@ -9,8 +9,16 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import i18n from '@/lang';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const buttons = [
|
||||
const props = defineProps({
|
||||
historyDisabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const buttons = computed(() => [
|
||||
{
|
||||
label: i18n.global.t('aiTools.gpu.current'),
|
||||
path: '/ai/gpu/current',
|
||||
@@ -18,6 +26,7 @@ const buttons = [
|
||||
{
|
||||
label: i18n.global.t('aiTools.gpu.history'),
|
||||
path: '/ai/gpu/history',
|
||||
disabled: props.historyDisabled,
|
||||
},
|
||||
];
|
||||
]);
|
||||
</script>
|
||||
|
||||
@@ -16,12 +16,7 @@
|
||||
<TableRefresh @search="search()" />
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
:cache-pagination="false"
|
||||
:pagination-config="paginationConfig"
|
||||
:data="items"
|
||||
@search="search()"
|
||||
>
|
||||
<ComplexTable :pagination-config="paginationConfig" :data="items" @search="search()">
|
||||
<el-table-column
|
||||
:label="$t('commons.table.name')"
|
||||
fix
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
>
|
||||
<el-select
|
||||
v-model="form.localEngineId"
|
||||
fit-input-width
|
||||
filterable
|
||||
:loading="loadingEngines"
|
||||
@change="handleLocalEngineChange"
|
||||
@@ -45,26 +46,13 @@
|
||||
<el-option v-for="item in localEngines" :key="item.id" :label="item.name" :value="item.id">
|
||||
<div class="local-engine-option">
|
||||
<span class="local-engine-option__name" :title="item.name">{{ item.name }}</span>
|
||||
<el-tag
|
||||
<AiModelList
|
||||
v-if="item.models.length"
|
||||
size="small"
|
||||
type="info"
|
||||
class="local-engine-option__model"
|
||||
>
|
||||
<span :title="item.models[0]">{{ item.models[0] }}</span>
|
||||
</el-tag>
|
||||
<el-popover v-if="item.models.length > 1" trigger="click" placement="bottom" :width="320">
|
||||
<template #reference>
|
||||
<el-button size="small" class="local-engine-option__more" @click.stop>
|
||||
+{{ item.models.length - 1 }}
|
||||
</el-button>
|
||||
</template>
|
||||
<div class="local-engine-models">
|
||||
<el-tag v-for="model in item.models.slice(1)" :key="model" size="small" type="info">
|
||||
{{ model }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</el-popover>
|
||||
class="local-engine-option__models"
|
||||
:models="item.models"
|
||||
:visible-count="1"
|
||||
tag-type="info"
|
||||
/>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
@@ -331,6 +319,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AiModelList from '@/components/ai-model-list/index.vue';
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { FormInstance } from 'element-plus';
|
||||
import { AI } from '@/api/interface/ai';
|
||||
@@ -992,34 +981,8 @@ defineExpose({
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__model {
|
||||
max-width: 55%;
|
||||
min-width: 0;
|
||||
|
||||
:deep(.el-tag__content) {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
&__more {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.local-engine-models {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
max-height: 240px;
|
||||
overflow: auto;
|
||||
|
||||
.el-tag {
|
||||
height: auto;
|
||||
padding: 4px 6px;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
max-width: 100%;
|
||||
&__models {
|
||||
max-width: 65%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,6 @@
|
||||
<ComplexTable
|
||||
:data="connections"
|
||||
row-key="id"
|
||||
:cache-pagination="false"
|
||||
:pagination-config="pagination"
|
||||
:columns="accountColumns"
|
||||
local-key="modelServiceColumn"
|
||||
@@ -70,7 +69,7 @@
|
||||
: 'aiTools.model.cloudService',
|
||||
)
|
||||
"
|
||||
placement="top"
|
||||
v-bind="TABLE_TOOLTIP_OPTIONS"
|
||||
>
|
||||
<el-icon
|
||||
class="service-source-icon"
|
||||
@@ -116,36 +115,27 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="models" :label="$t('aiTools.model.model')" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.models?.length" class="model-line">
|
||||
<span class="cell-ellipsis" :title="row.models[0].id">{{ row.models[0].id }}</span>
|
||||
<el-button
|
||||
v-if="!row.appInstallId && row.models.length > 1"
|
||||
size="small"
|
||||
class="model-more"
|
||||
@click="poolRef.open(row)"
|
||||
>
|
||||
+{{ row.models.length - 1 }}
|
||||
</el-button>
|
||||
<el-tooltip
|
||||
v-if="row.appInstallId && row.models.length > 1"
|
||||
:content="
|
||||
row.models
|
||||
.slice(1)
|
||||
.map((item) => item.id)
|
||||
.join(', ')
|
||||
"
|
||||
placement="top"
|
||||
>
|
||||
<el-button size="small" class="model-more">+{{ row.models.length - 1 }}</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<AiModelList
|
||||
v-if="row.models?.length"
|
||||
:models="row.models.map((item) => item.id)"
|
||||
:visible-count="1"
|
||||
>
|
||||
<template v-if="!row.appInstallId" #footer>
|
||||
<el-button link type="primary" @click="poolRef.open(row)">
|
||||
{{ $t('aiTools.agents.modelPool') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</AiModelList>
|
||||
<span v-if="!row.models?.length">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="verified" :label="$t('aiTools.model.connectionStatus')" min-width="130">
|
||||
<template #default="{ row }">
|
||||
<div class="verification-status">
|
||||
<el-tooltip :disabled="!row.verifiedAt && !row.verifyError" placement="top">
|
||||
<el-tooltip
|
||||
:disabled="!row.verifiedAt && !row.verifyError"
|
||||
v-bind="TABLE_TOOLTIP_OPTIONS"
|
||||
>
|
||||
<template #content>
|
||||
<div v-if="row.verifiedAt">
|
||||
{{ $t('aiTools.model.verifiedAt') }}:
|
||||
@@ -165,7 +155,7 @@
|
||||
<el-tooltip
|
||||
v-if="row.validateAvailability"
|
||||
:content="$t('aiTools.model.reverify')"
|
||||
placement="top"
|
||||
v-bind="TABLE_TOOLTIP_OPTIONS"
|
||||
>
|
||||
<el-button
|
||||
v-permission
|
||||
@@ -269,12 +259,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AiModelList from '@/components/ai-model-list/index.vue';
|
||||
import { computed, reactive, ref, toRef, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import { Cloudy, Connection, Monitor, RefreshRight } from '@element-plus/icons-vue';
|
||||
import type { AI } from '@/api/interface/ai';
|
||||
import type { FuTableColumnConfig } from '@/components/table/shared';
|
||||
import { TABLE_TOOLTIP_OPTIONS, type FuTableColumnConfig } from '@/components/table/shared';
|
||||
import { deleteAgentAccount, getAgentProviders, pageAgentAccounts, verifyAgentAccount } from '@/api/modules/ai';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
import { getAgentProviderDisplayName } from '@/utils/agent';
|
||||
@@ -588,14 +579,6 @@ watch(
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.model-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.model-more {
|
||||
flex-shrink: 0;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.model-filter {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,6 @@
|
||||
v-else-if="modelInstalled"
|
||||
:selection-context="() => [searchName]"
|
||||
row-key="id"
|
||||
:cache-pagination="false"
|
||||
:pagination-config="paginationConfig"
|
||||
v-model:selects="selects"
|
||||
@sort-change="search"
|
||||
@@ -84,14 +83,18 @@
|
||||
<el-tag v-if="row.status === 'Deleted'" type="info">
|
||||
{{ $t('commons.status.notfound') }}
|
||||
</el-tag>
|
||||
<el-tooltip v-if="row.status === 'Unknown'" :content="row.message" placement="top">
|
||||
<el-tooltip
|
||||
v-if="row.status === 'Unknown'"
|
||||
:content="row.message"
|
||||
v-bind="TABLE_TOOLTIP_OPTIONS"
|
||||
>
|
||||
<el-tag type="warning">{{ $t('commons.status.unknown') }}</el-tag>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
v-if="row.status === 'Canceled'"
|
||||
:content="row.message"
|
||||
:disabled="!row.message"
|
||||
placement="top"
|
||||
v-bind="TABLE_TOOLTIP_OPTIONS"
|
||||
>
|
||||
<el-tag type="danger">{{ $t('commons.status.systemrestart') }}</el-tag>
|
||||
</el-tooltip>
|
||||
@@ -151,6 +154,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { TABLE_TOOLTIP_OPTIONS } from '@/components/table/shared';
|
||||
import { useSearchState } from '@/utils/search-reset';
|
||||
import AddDialog from '@/views/ai/ollama/add/index.vue';
|
||||
import TaskLog from '@/components/log/task/index.vue';
|
||||
|
||||
@@ -216,6 +216,7 @@
|
||||
<el-divider class="!my-3" />
|
||||
|
||||
<el-table
|
||||
:tooltip-options="TABLE_OVERFLOW_TOOLTIP_OPTIONS"
|
||||
v-if="composeContainers.length > 0"
|
||||
:data="tableData"
|
||||
size="small"
|
||||
@@ -522,6 +523,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { TABLE_OVERFLOW_TOOLTIP_OPTIONS } from '@/components/table/shared';
|
||||
import { useSearchState } from '@/utils/search-reset';
|
||||
import { computed, h, ref } from 'vue';
|
||||
import CodemirrorPro from '@/components/codemirror-pro/index.vue';
|
||||
|
||||
@@ -85,7 +85,6 @@
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
:selection-context="() => [searchName, activeTag, includeAppStore, props.filters]"
|
||||
:cache-pagination="false"
|
||||
:pagination-config="paginationConfig"
|
||||
v-model:view-mode="viewMode"
|
||||
v-model:selects="selects"
|
||||
@@ -308,7 +307,7 @@
|
||||
:key="item"
|
||||
:hide-after="20"
|
||||
:content="item"
|
||||
placement="top"
|
||||
v-bind="TABLE_TOOLTIP_OPTIONS"
|
||||
>
|
||||
<el-button
|
||||
v-if="item.indexOf('->') !== -1"
|
||||
@@ -448,7 +447,12 @@
|
||||
<el-checkbox border v-model="showIPv6Ports">IPv6</el-checkbox>
|
||||
</div>
|
||||
<div v-if="filteredPorts.length" class="container-port-dialog">
|
||||
<el-tooltip v-for="item in filteredPorts" :key="item" :content="item" placement="top">
|
||||
<el-tooltip
|
||||
v-for="item in filteredPorts"
|
||||
:key="item"
|
||||
:content="item"
|
||||
v-bind="TABLE_TOOLTIP_OPTIONS"
|
||||
>
|
||||
<el-button
|
||||
:icon="item.indexOf('->') !== -1 ? 'Position' : undefined"
|
||||
plain
|
||||
@@ -485,6 +489,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { TABLE_TOOLTIP_OPTIONS } from '@/components/table/shared';
|
||||
import { useSearchState } from '@/utils/search-reset';
|
||||
import PruneDialog from '@/views/container/container/prune/index.vue';
|
||||
import RenameDialog from '@/views/container/container/rename/index.vue';
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="$t('container.volume')" name="storage">
|
||||
<el-table :data="inspectData?.Mounts" border>
|
||||
<el-table :tooltip-options="TABLE_OVERFLOW_TOOLTIP_OPTIONS" :data="inspectData?.Mounts" border>
|
||||
<el-table-column :label="$t('commons.table.type')" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">
|
||||
@@ -169,6 +169,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { TABLE_OVERFLOW_TOOLTIP_OPTIONS } from '@/components/table/shared';
|
||||
import { ref } from 'vue';
|
||||
import CodemirrorPro from '@/components/codemirror-pro/index.vue';
|
||||
import { routerToFileWithPath } from '@/utils/router';
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<template #default="{ row }">
|
||||
<el-select v-if="row.type === 'volume'" filterable v-model="row.sourceDir">
|
||||
<div v-for="(item, indexV) of volumeOptions" :key="indexV">
|
||||
<el-tooltip :hide-after="20" :content="item.option" placement="top">
|
||||
<el-tooltip :hide-after="20" :content="item.option" v-bind="TABLE_TOOLTIP_OPTIONS">
|
||||
<el-option :value="item.option" :label="item.option.substring(0, 30)" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
@@ -82,6 +82,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { TABLE_TOOLTIP_OPTIONS } from '@/components/table/shared';
|
||||
import { Container } from '@/api/interface/container';
|
||||
import { listVolume } from '@/api/modules/container';
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
:cache-pagination="false"
|
||||
:pagination-config="paginationConfig"
|
||||
:data="data"
|
||||
@sort-change="search"
|
||||
@@ -51,6 +50,7 @@
|
||||
</el-text>
|
||||
<div class="float-right">
|
||||
<el-tooltip
|
||||
v-bind="TABLE_TOOLTIP_OPTIONS"
|
||||
:content="row.isPinned ? $t('commons.table.unpin') : $t('commons.table.pin')"
|
||||
>
|
||||
<el-button
|
||||
@@ -162,6 +162,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { TABLE_TOOLTIP_OPTIONS } from '@/components/table/shared';
|
||||
import { useSearchState } from '@/utils/search-reset';
|
||||
import { toRef, reactive, ref, computed, watch, onActivated, onDeactivated, onBeforeUnmount } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<span class="input-help">{{ $t('container.imageRemoteSearchHelper') }}</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table v-loading="loading" :data="items">
|
||||
<el-table :tooltip-options="TABLE_OVERFLOW_TOOLTIP_OPTIONS" v-loading="loading" :data="items">
|
||||
<el-table-column prop="name" :label="$t('container.imageName')" min-width="180" />
|
||||
<el-table-column
|
||||
prop="description"
|
||||
@@ -42,6 +42,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { TABLE_OVERFLOW_TOOLTIP_OPTIONS } from '@/components/table/shared';
|
||||
import { onBeforeUnmount, ref, watch } from 'vue';
|
||||
import { searchRemoteImages, type RemoteImage } from '@/api/modules/container';
|
||||
import { getSettingBaseInfo } from '@/api/modules/setting';
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
<ComplexTable
|
||||
:selection-context="() => [searchName]"
|
||||
row-key="id"
|
||||
:cache-pagination="false"
|
||||
:pagination-config="paginationConfig"
|
||||
v-model:selects="selects"
|
||||
:data="data"
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
:cache-pagination="false"
|
||||
:pagination-config="paginationConfig"
|
||||
v-model:selects="selects"
|
||||
:data="data"
|
||||
|
||||
@@ -149,6 +149,18 @@
|
||||
<el-switch v-permission v-model="form.iptables" @change="handleIptables"></el-switch>
|
||||
<span class="input-help">{{ $t('container.iptablesHelper1') }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('container.nvidiaSupport')" prop="nvidia">
|
||||
<el-switch
|
||||
v-permission
|
||||
v-model="form.nvidia"
|
||||
:disabled="!form.nvidiaSupported"
|
||||
@change="handleNVIDIA"
|
||||
/>
|
||||
<span class="input-help">{{ $t('container.nvidiaHelper') }}</span>
|
||||
<span v-if="!form.nvidiaSupported" class="input-help">
|
||||
{{ $t('container.nvidiaLocalOnly') }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="Live restore" prop="liveRestore">
|
||||
<el-switch
|
||||
v-permission
|
||||
@@ -239,6 +251,7 @@
|
||||
<ConfirmDialog ref="confirmDialogRefIpv6" @confirm="onSaveIPv6" @cancel="search" />
|
||||
<ConfirmDialog ref="confirmDialogRefIptable" @confirm="onSubmitOpenIPtable" @cancel="search" />
|
||||
<ConfirmDialog ref="confirmDialogRefLog" @confirm="onSubmitSaveLog" @cancel="search" />
|
||||
<ConfirmDialog ref="confirmDialogRefNVIDIA" @confirm="onSubmitSaveNVIDIA" @cancel="search" />
|
||||
<ConfirmDialog ref="confirmDialogRefLive" @confirm="onSubmitSaveLive" @cancel="search" />
|
||||
<ConfirmDialog ref="confirmDialogRefCgroup" @confirm="onSubmitSaveCgroup" @cancel="search" />
|
||||
|
||||
@@ -288,6 +301,7 @@ const confirmDialogRefLog = ref();
|
||||
const mirrorRef = ref();
|
||||
const registriesRef = ref();
|
||||
const confirmDialogRefLive = ref();
|
||||
const confirmDialogRefNVIDIA = ref();
|
||||
const confirmDialogRefCgroup = ref();
|
||||
const confirmDialogRefIptable = ref();
|
||||
const confirmDialogRefIpv6 = ref();
|
||||
@@ -302,6 +316,8 @@ const form = reactive({
|
||||
mirrors: '',
|
||||
registries: '',
|
||||
liveRestore: false,
|
||||
nvidia: false,
|
||||
nvidiaSupported: false,
|
||||
iptables: true,
|
||||
cgroupDriver: '',
|
||||
|
||||
@@ -407,6 +423,19 @@ const onSubmitOpenIPtable = () => {
|
||||
save('IPtables', 'enable');
|
||||
};
|
||||
|
||||
const handleNVIDIA = () => {
|
||||
confirmDialogRefNVIDIA.value.acceptParams({
|
||||
header: i18n.global.t('database.confChange'),
|
||||
operationInfo: form.nvidia
|
||||
? i18n.global.t('container.restartHelper')
|
||||
: `${i18n.global.t('container.nvidiaChangeHelper')} ${i18n.global.t('container.restartHelper')}`,
|
||||
submitInputInfo: i18n.global.t('database.restartNow'),
|
||||
});
|
||||
};
|
||||
const onSubmitSaveNVIDIA = () => {
|
||||
save('NVIDIA', form.nvidia ? 'enable' : 'disable');
|
||||
};
|
||||
|
||||
const handleLive = async () => {
|
||||
let params = {
|
||||
header: i18n.global.t('database.confChange'),
|
||||
@@ -509,6 +538,8 @@ const search = async () => {
|
||||
form.version = res.data.version;
|
||||
form.cgroupDriver = res.data.cgroupDriver || 'cgroupfs';
|
||||
form.liveRestore = res.data.liveRestore;
|
||||
form.nvidia = res.data.nvidia;
|
||||
form.nvidiaSupported = res.data.nvidiaSupported;
|
||||
form.iptables = res.data.iptables;
|
||||
form.mirrors = res.data.registryMirrors ? res.data.registryMirrors.join('\n') : '';
|
||||
form.registries = res.data.insecureRegistries ? res.data.insecureRegistries.join('\n') : '';
|
||||
|
||||
@@ -40,7 +40,6 @@
|
||||
<ComplexTable
|
||||
:selection-context="() => [searchName]"
|
||||
row-key="id"
|
||||
:cache-pagination="false"
|
||||
:pagination-config="paginationConfig"
|
||||
v-model:selects="selects"
|
||||
:data="data"
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
<ComplexTable
|
||||
:selection-context="() => [searchName]"
|
||||
row-key="name"
|
||||
:cache-pagination="false"
|
||||
:pagination-config="paginationConfig"
|
||||
v-model:selects="selects"
|
||||
:data="data"
|
||||
@@ -88,7 +87,7 @@
|
||||
prop="mountpoint"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tooltip :content="row.mountpoint">
|
||||
<el-tooltip :content="row.mountpoint" v-bind="TABLE_TOOLTIP_OPTIONS">
|
||||
<el-button
|
||||
v-permission:view="'host_file_view'"
|
||||
type="primary"
|
||||
@@ -125,6 +124,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { TABLE_TOOLTIP_OPTIONS } from '@/components/table/shared';
|
||||
import { useSearchState } from '@/utils/search-reset';
|
||||
import CreateDialog from '@/views/container/volume/create/index.vue';
|
||||
import CodemirrorDrawer from '@/components/codemirror-pro/drawer.vue';
|
||||
|
||||
@@ -55,7 +55,6 @@
|
||||
<ComplexTable
|
||||
:selection-context="() => [searchName, searchGroupID]"
|
||||
row-key="id"
|
||||
:cache-pagination="false"
|
||||
:pagination-config="paginationConfig"
|
||||
:default-sort="
|
||||
paginationConfig.order !== 'null'
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
:selection-context="() => [searchInfo, group]"
|
||||
row-key="id"
|
||||
v-model:selects="selects"
|
||||
:cache-pagination="false"
|
||||
:pagination-config="paginationConfig"
|
||||
:data="data"
|
||||
@search="search"
|
||||
|
||||
@@ -106,7 +106,6 @@
|
||||
<ComplexTable
|
||||
v-if="currentDB"
|
||||
:class="{ mask: maskShow }"
|
||||
:cache-pagination="false"
|
||||
:pagination-config="paginationConfig"
|
||||
@sort-change="search"
|
||||
@search="search"
|
||||
|
||||
@@ -10,13 +10,7 @@
|
||||
<TableSearch @search="search()" v-model:searchName="searchName" />
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
:cache-pagination="false"
|
||||
:pagination-config="paginationConfig"
|
||||
@sort-change="search"
|
||||
@search="search"
|
||||
:data="data"
|
||||
>
|
||||
<ComplexTable :pagination-config="paginationConfig" @sort-change="search" @search="search" :data="data">
|
||||
<el-table-column show-overflow-tooltip :label="$t('commons.table.name')" prop="name" sortable />
|
||||
<el-table-column :label="$t('database.version')" prop="version" />
|
||||
<el-table-column show-overflow-tooltip :label="$t('database.address')" prop="address" />
|
||||
|
||||
@@ -10,7 +10,11 @@
|
||||
{{ $t('database.addUserAuthorization') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table :data="authorizedUsers" :empty-text="$t('commons.msg.noneData')">
|
||||
<el-table
|
||||
:tooltip-options="TABLE_OVERFLOW_TOOLTIP_OPTIONS"
|
||||
:data="authorizedUsers"
|
||||
:empty-text="$t('commons.msg.noneData')"
|
||||
>
|
||||
<el-table-column :label="$t('commons.table.user')" show-overflow-tooltip min-width="180">
|
||||
<template #default="{ row }">{{ row.username }}@{{ row.host }}</template>
|
||||
</el-table-column>
|
||||
@@ -160,6 +164,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { TABLE_OVERFLOW_TOOLTIP_OPTIONS } from '@/components/table/shared';
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import Tooltip from '@/components/tooltip/index.vue';
|
||||
|
||||
@@ -118,7 +118,6 @@
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
v-if="currentDB"
|
||||
:cache-pagination="false"
|
||||
:pagination-config="paginationConfig"
|
||||
:class="{ mask: maskShow }"
|
||||
@sort-change="search"
|
||||
@@ -131,7 +130,9 @@
|
||||
<Tooltip v-if="!row.isDelete" :islink="false" :text="row.name" />
|
||||
<div v-else>
|
||||
<span v-if="row.name.length < 15">{{ row.name }}</span>
|
||||
<el-tooltip v-else :content="row.name">{{ row.name.substring(0, 10) }}...</el-tooltip>
|
||||
<el-tooltip v-else :content="row.name" v-bind="TABLE_TOOLTIP_OPTIONS">
|
||||
{{ row.name.substring(0, 10) }}...
|
||||
</el-tooltip>
|
||||
<el-tag round type="info" class="ml-1" size="small">
|
||||
{{ $t('database.isDelete') }}
|
||||
</el-tag>
|
||||
@@ -227,6 +228,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { TABLE_TOOLTIP_OPTIONS } from '@/components/table/shared';
|
||||
import { useSearchState } from '@/utils/search-reset';
|
||||
import BindDialog from '@/views/database/mysql/bind/index.vue';
|
||||
import UserDialog from '@/views/database/mysql/user/index.vue';
|
||||
|
||||
@@ -10,13 +10,7 @@
|
||||
<TableSearch @search="search()" v-model:searchName="searchName" />
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
:cache-pagination="false"
|
||||
:pagination-config="paginationConfig"
|
||||
@sort-change="search"
|
||||
@search="search"
|
||||
:data="data"
|
||||
>
|
||||
<ComplexTable :pagination-config="paginationConfig" @sort-change="search" @search="search" :data="data">
|
||||
<el-table-column show-overflow-tooltip :label="$t('commons.table.name')" prop="name" sortable />
|
||||
<el-table-column show-overflow-tooltip :label="$t('database.address')" prop="address" />
|
||||
<el-table-column :label="$t('commons.login.username')" prop="username" />
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
v-for="item in userDatabases(row).slice(0, 1)"
|
||||
:key="row.username + '@' + row.host + item"
|
||||
:content="item"
|
||||
placement="top"
|
||||
v-bind="TABLE_TOOLTIP_OPTIONS"
|
||||
>
|
||||
<el-tag size="small">{{ item }}</el-tag>
|
||||
</el-tooltip>
|
||||
@@ -66,7 +66,7 @@
|
||||
v-for="item in userDatabases(row)"
|
||||
:key="row.username + '@' + row.host + item"
|
||||
:content="item"
|
||||
placement="top"
|
||||
v-bind="TABLE_TOOLTIP_OPTIONS"
|
||||
>
|
||||
<el-tag size="small">{{ item }}</el-tag>
|
||||
</el-tooltip>
|
||||
@@ -203,6 +203,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { TABLE_TOOLTIP_OPTIONS } from '@/components/table/shared';
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import Tooltip from '@/components/tooltip/index.vue';
|
||||
import i18n from '@/lang';
|
||||
|
||||
@@ -93,7 +93,6 @@
|
||||
<ComplexTable
|
||||
v-if="currentDB"
|
||||
:class="{ mask: maskShow }"
|
||||
:cache-pagination="false"
|
||||
:pagination-config="paginationConfig"
|
||||
@sort-change="search"
|
||||
@search="search"
|
||||
@@ -105,7 +104,9 @@
|
||||
<Tooltip v-if="!row.isDelete" :islink="false" :text="row.name" />
|
||||
<div v-else>
|
||||
<span v-if="row.name.length < 15">{{ row.name }}</span>
|
||||
<el-tooltip v-else :content="row.name">{{ row.name.substring(0, 10) }}...</el-tooltip>
|
||||
<el-tooltip v-else :content="row.name" v-bind="TABLE_TOOLTIP_OPTIONS">
|
||||
{{ row.name.substring(0, 10) }}...
|
||||
</el-tooltip>
|
||||
<el-tag round type="info" class="ml-1" size="small">
|
||||
{{ $t('database.isDelete') }}
|
||||
</el-tag>
|
||||
@@ -242,6 +243,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { TABLE_TOOLTIP_OPTIONS } from '@/components/table/shared';
|
||||
import { useSearchState } from '@/utils/search-reset';
|
||||
import BindDialog from '@/views/database/postgresql/bind/index.vue';
|
||||
import OperateDialog from '@/views/database/postgresql/create/index.vue';
|
||||
|
||||
@@ -10,13 +10,7 @@
|
||||
<TableSearch @search="search()" v-model:searchName="searchName" />
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
:cache-pagination="false"
|
||||
:pagination-config="paginationConfig"
|
||||
@sort-change="search"
|
||||
@search="search"
|
||||
:data="data"
|
||||
>
|
||||
<ComplexTable :pagination-config="paginationConfig" @sort-change="search" @search="search" :data="data">
|
||||
<el-table-column show-overflow-tooltip :label="$t('commons.table.name')" prop="name" sortable />
|
||||
<el-table-column show-overflow-tooltip :label="$t('database.address')" prop="address" />
|
||||
<el-table-column :label="$t('commons.login.username')" prop="username" />
|
||||
|
||||
@@ -7,7 +7,12 @@
|
||||
<el-button v-permission @click="batchDelete(null)">
|
||||
{{ $t('commons.button.delete') }}
|
||||
</el-button>
|
||||
<el-table :data="data" class="mt-5" @selection-change="handleSelectionChange">
|
||||
<el-table
|
||||
:tooltip-options="TABLE_OVERFLOW_TOOLTIP_OPTIONS"
|
||||
:data="data"
|
||||
class="mt-5"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" fix />
|
||||
<el-table-column :label="$t('commons.table.name')" min-width="50" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
@@ -75,6 +80,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { TABLE_OVERFLOW_TOOLTIP_OPTIONS } from '@/components/table/shared';
|
||||
import { Command } from '@/api/interface/command';
|
||||
import { deleteCommand, getCommandPage, addCommand, editCommand } from '@/api/modules/command';
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
@@ -10,13 +10,7 @@
|
||||
<TableSearch @search="search()" v-model:searchName="searchName" />
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
:cache-pagination="false"
|
||||
:pagination-config="paginationConfig"
|
||||
@sort-change="search"
|
||||
@search="search"
|
||||
:data="data"
|
||||
>
|
||||
<ComplexTable :pagination-config="paginationConfig" @sort-change="search" @search="search" :data="data">
|
||||
<el-table-column show-overflow-tooltip :label="$t('commons.table.name')" prop="name" sortable />
|
||||
<el-table-column show-overflow-tooltip :label="$t('database.address')" prop="address" />
|
||||
<el-table-column :label="$t('commons.login.password')" prop="password">
|
||||
|
||||
@@ -4,7 +4,12 @@
|
||||
<template #header-r>
|
||||
<el-popover placement="left" :width="226" trigger="click">
|
||||
<el-input size="small" v-model="filter" clearable @input="loadOption()" />
|
||||
<el-table :show-header="false" :data="options" max-height="150px">
|
||||
<el-table
|
||||
:tooltip-options="TABLE_OVERFLOW_TOOLTIP_OPTIONS"
|
||||
:show-header="false"
|
||||
:data="options"
|
||||
max-height="150px"
|
||||
>
|
||||
<el-table-column prop="key" width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="name">
|
||||
<template #default="{ row }">
|
||||
@@ -143,6 +148,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { TABLE_OVERFLOW_TOOLTIP_OPTIONS } from '@/components/table/shared';
|
||||
import { getAppIconUrl, installedOp } from '@/api/modules/app';
|
||||
import { changeLauncherStatus, loadAppLauncher, loadAppLauncherOption } from '@/api/modules/dashboard';
|
||||
import i18n from '@/lang';
|
||||
|
||||
@@ -99,6 +99,9 @@
|
||||
>
|
||||
<el-radio-button value="network">{{ $t('home.network') }}</el-radio-button>
|
||||
<el-radio-button value="io">{{ $t('home.io') }}</el-radio-button>
|
||||
<el-radio-button v-if="hasVllm" value="vllm">
|
||||
{{ $t('aiTools.vllmMonitor.tokenThroughput') }}
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-select
|
||||
v-if="chartOption === 'network'"
|
||||
@@ -128,6 +131,26 @@
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select
|
||||
v-if="chartOption === 'vllm'"
|
||||
v-model="vllmInstallID"
|
||||
class="p-w-200 float-right"
|
||||
:placeholder="$t('aiTools.model.deployments')"
|
||||
@visible-change="(visible) => visible && loadVllmOptions()"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in vllmOptions"
|
||||
:key="item.appInstallId"
|
||||
:label="item.name + (item.servedModelName ? ' · ' + item.servedModelName : '')"
|
||||
:value="item.appInstallId"
|
||||
:disabled="item.status !== 'Running'"
|
||||
>
|
||||
<span>
|
||||
{{ item.name }}{{ item.servedModelName ? ' · ' + item.servedModelName : '' }}
|
||||
</span>
|
||||
<Status class="ml-2" :status="item.status" />
|
||||
</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="monitor-chart-content">
|
||||
@@ -169,6 +192,47 @@
|
||||
:dataZoom="true"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="chartOption === 'vllm'">
|
||||
<div class="monitor-tags" :style="monitorTagsStyle">
|
||||
<el-tag>
|
||||
{{ $t('aiTools.vllmMonitor.promptThroughput') }}:
|
||||
{{ formatVllmMetric(vllmCurrent?.promptThroughput) }} tokens/s
|
||||
</el-tag>
|
||||
<el-tag>
|
||||
{{ $t('aiTools.vllmMonitor.generationThroughput') }}:
|
||||
{{ formatVllmMetric(vllmCurrent?.generationThroughput) }} tokens/s
|
||||
</el-tag>
|
||||
<el-tag>
|
||||
{{ $t('aiTools.vllmMonitor.processing') }}:
|
||||
{{ formatVllmMetric(vllmCurrent?.running) }}
|
||||
</el-tag>
|
||||
<el-tag>
|
||||
{{ $t('aiTools.vllmMonitor.queued') }}:
|
||||
{{ formatVllmMetric(vllmCurrent?.waiting) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="mobile-monitor-chart">
|
||||
<v-charts
|
||||
height="383px"
|
||||
id="vllmChart"
|
||||
type="line"
|
||||
:option="vllmChartOption"
|
||||
:dataZoom="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="vllm-monitor-footer">
|
||||
<span>{{ vllmMonitorMessage }}</span>
|
||||
<el-button
|
||||
v-if="selectedVllm"
|
||||
link
|
||||
type="primary"
|
||||
@click="vllmMonitorRef?.acceptParams(selectedVllm)"
|
||||
>
|
||||
{{ $t('commons.button.view') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<VllmMonitor v-if="selectedVllm" ref="vllmMonitorRef" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</CardWithHeader>
|
||||
@@ -443,7 +507,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onBeforeUnmount, ref, reactive, computed, nextTick } from 'vue';
|
||||
import { onMounted, onBeforeUnmount, ref, reactive, computed, nextTick, watch, defineAsyncComponent } from 'vue';
|
||||
import SystemStatus from '@/views/home/status/index.vue';
|
||||
import AppLauncher from '@/views/home/app/index.vue';
|
||||
import VCharts from '@/components/v-charts/index.vue';
|
||||
@@ -478,6 +542,9 @@ import {
|
||||
} from '@/utils/dashboardCache';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { useCan } from '@/composables/useMenuManagePermission';
|
||||
import { readPages } from '@/utils/pagination';
|
||||
import { loadExtensionComponent } from '@/extensions/optional';
|
||||
import type { ReqPage, ResPage, ResultData } from '@/api/interface';
|
||||
const router = useRouter();
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
const {
|
||||
@@ -489,8 +556,146 @@ const {
|
||||
hasNewVersion,
|
||||
isAdminOrNodeAdmin,
|
||||
isXpackOrEE,
|
||||
isProductPro,
|
||||
currentNode,
|
||||
} = useGlobalStore();
|
||||
|
||||
interface VllmDeployment {
|
||||
appInstallId: number;
|
||||
name: string;
|
||||
servedModelName: string;
|
||||
status: string;
|
||||
}
|
||||
interface VllmSnapshot {
|
||||
appInstallID: number;
|
||||
createdAt: string;
|
||||
status: string;
|
||||
intervalSeconds: number;
|
||||
promptThroughput: number | null;
|
||||
generationThroughput: number | null;
|
||||
running: number | null;
|
||||
waiting: number | null;
|
||||
metrics?: Record<string, Record<string, number>>;
|
||||
}
|
||||
const vllmModules = import.meta.glob<{
|
||||
pageVllm: (req: ReqPage & { info: string }) => Promise<ResultData<ResPage<VllmDeployment>>>;
|
||||
loadVllmCurrent: (id: number, signal: AbortSignal) => Promise<ResultData<VllmSnapshot>>;
|
||||
}>('@/xpack/api/modules/vllm.ts', { eager: true });
|
||||
const vllmMetricsModules = import.meta.glob<{
|
||||
calculateCurrentMetrics: (snapshot: VllmSnapshot, previous?: VllmSnapshot) => VllmSnapshot;
|
||||
}>('@/xpack/views/vllm/monitor/current.ts', { eager: true });
|
||||
const vllmAPI = Object.values(vllmModules)[0];
|
||||
const vllmMetrics = Object.values(vllmMetricsModules)[0];
|
||||
const VllmMonitor = defineAsyncComponent(() => loadExtensionComponent('/vllm/monitor/index.vue'));
|
||||
const vllmMonitorRef = ref();
|
||||
const canViewVllm = useCan('ai_model_view');
|
||||
const vllmOptions = ref<VllmDeployment[]>([]);
|
||||
const vllmInstallID = ref<number>();
|
||||
const vllmCurrent = ref<VllmSnapshot>();
|
||||
const vllmLoadFailed = ref(false);
|
||||
const vllmPoints = ref<Array<{ time: string; prompt: number | null; generation: number | null }>>([]);
|
||||
let vllmTimer: ReturnType<typeof setTimeout>;
|
||||
let vllmController: AbortController;
|
||||
let vllmOptionsVersion = 0;
|
||||
const hasVllm = computed(() => isProductPro.value && canViewVllm.value && vllmOptions.value.length > 0);
|
||||
const selectedVllm = computed(() => vllmOptions.value.find((item) => item.appInstallId === vllmInstallID.value));
|
||||
const formatVllmMetric = (value?: number | null) =>
|
||||
value == null || !Number.isFinite(value) ? '--' : Number(value.toFixed(2)).toString();
|
||||
const vllmMonitorMessage = computed(() => {
|
||||
if (!selectedVllm.value) return i18n.global.t('aiTools.model.readinessNotRunning');
|
||||
if (vllmLoadFailed.value) return i18n.global.t('aiTools.vllmMonitor.loadFailed');
|
||||
if (vllmCurrent.value?.status === 'unavailable') return i18n.global.t('aiTools.vllmMonitor.unavailable');
|
||||
if (vllmCurrent.value?.status === 'no_data') return i18n.global.t('aiTools.vllmMonitor.noMetrics');
|
||||
if (!vllmCurrent.value?.intervalSeconds) return i18n.global.t('commons.table.statusWaiting');
|
||||
return `${i18n.global.t('aiTools.vllmMonitor.sampleWindow')}: ${formatVllmMetric(vllmCurrent.value.intervalSeconds)} s`;
|
||||
});
|
||||
const vllmChartOption = computed(() => ({
|
||||
xData: loadMonitorChartTime(vllmPoints.value.map((point) => point.time)),
|
||||
yData: [
|
||||
{
|
||||
name: i18n.global.t('aiTools.vllmMonitor.promptThroughput'),
|
||||
data: loadMonitorChartData(vllmPoints.value.map((point) => point.prompt)),
|
||||
},
|
||||
{
|
||||
name: i18n.global.t('aiTools.vllmMonitor.generationThroughput'),
|
||||
data: loadMonitorChartData(vllmPoints.value.map((point) => point.generation)),
|
||||
},
|
||||
],
|
||||
grid: monitorChartGrid,
|
||||
legend: monitorChartLegend,
|
||||
formatStr: 'tokens/s',
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '( tokens/s )',
|
||||
min: 0,
|
||||
max: ({ max }: { max: number }) => (max > 0 ? undefined : 1),
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
valueFormatter: (value: number | null) => `${formatVllmMetric(value)} tokens/s`,
|
||||
},
|
||||
}));
|
||||
|
||||
const loadVllmOptions = async () => {
|
||||
if (!isProductPro.value || !canViewVllm.value || !vllmAPI || !vllmMetrics) return;
|
||||
const version = ++vllmOptionsVersion;
|
||||
const node = currentNode.value;
|
||||
try {
|
||||
const items = await readPages((page) => {
|
||||
if (version !== vllmOptionsVersion || node !== currentNode.value) throw new Error('node changed');
|
||||
return vllmAPI.pageVllm({ ...page, info: '' });
|
||||
});
|
||||
if (version !== vllmOptionsVersion || node !== currentNode.value) return;
|
||||
vllmOptions.value = items;
|
||||
if (!items.some((item) => item.appInstallId === vllmInstallID.value && item.status === 'Running')) {
|
||||
vllmInstallID.value = items.find((item) => item.status === 'Running')?.appInstallId;
|
||||
}
|
||||
if (!items.length && chartOption.value === 'vllm') chartOption.value = 'network';
|
||||
} catch {
|
||||
if (version === vllmOptionsVersion) vllmLoadFailed.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
const stopVllmMonitor = (resetData = true) => {
|
||||
clearTimeout(vllmTimer);
|
||||
vllmController?.abort();
|
||||
if (resetData) {
|
||||
vllmCurrent.value = undefined;
|
||||
vllmPoints.value = [];
|
||||
vllmLoadFailed.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const refreshVllmMonitor = async () => {
|
||||
const controller = new AbortController();
|
||||
vllmController = controller;
|
||||
try {
|
||||
const { data } = await vllmAPI.loadVllmCurrent(vllmInstallID.value, controller.signal);
|
||||
if (controller.signal.aborted) return;
|
||||
const wasUnavailable = vllmCurrent.value?.status === 'unavailable';
|
||||
vllmCurrent.value = vllmMetrics.calculateCurrentMetrics(data, vllmCurrent.value);
|
||||
vllmLoadFailed.value = false;
|
||||
vllmPoints.value.push({
|
||||
time: dateFormatForSecond(new Date(data.createdAt)),
|
||||
prompt: vllmCurrent.value.promptThroughput,
|
||||
generation: vllmCurrent.value.generationThroughput,
|
||||
});
|
||||
if (data.status === 'unavailable' && !wasUnavailable) void loadVllmOptions();
|
||||
} catch {
|
||||
if (controller.signal.aborted) return;
|
||||
vllmCurrent.value = undefined;
|
||||
vllmLoadFailed.value = true;
|
||||
vllmPoints.value.push({ time: dateFormatForSecond(new Date()), prompt: null, generation: null });
|
||||
} finally {
|
||||
if (!controller.signal.aborted) {
|
||||
vllmPoints.value = vllmPoints.value.slice(-20);
|
||||
vllmTimer = setTimeout(refreshVllmMonitor, 3000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const DASHBOARD_CACHE_TTL = {
|
||||
safeStatus: 10 * 60 * 1000,
|
||||
netOptions: 60 * 60 * 1000,
|
||||
@@ -505,7 +710,7 @@ const monitorTagsStyle = {
|
||||
const monitorChartEmptyLength = 20;
|
||||
const loadMonitorEmptyData = () => Array.from({ length: monitorChartEmptyLength }, () => null);
|
||||
const loadMonitorEmptyTime = () => Array.from({ length: monitorChartEmptyLength }, () => '');
|
||||
const loadMonitorChartData = (data: Array<number>) => (data.length === 0 ? loadMonitorEmptyData() : data);
|
||||
const loadMonitorChartData = (data: Array<number | null>) => (data.length === 0 ? loadMonitorEmptyData() : data);
|
||||
const loadMonitorChartTime = (data: Array<string>) => (data.length === 0 ? loadMonitorEmptyTime() : data);
|
||||
const loadIOChartOption = () => ({
|
||||
xData: loadMonitorChartTime(timeIODatas.value),
|
||||
@@ -718,6 +923,7 @@ const updateCurrentInfo = (data: Dashboard.CurrentInfo) => {
|
||||
|
||||
const changeOption = async () => {
|
||||
isInit.value = true;
|
||||
if (chartOption.value === 'vllm') void loadVllmOptions();
|
||||
loadData();
|
||||
};
|
||||
|
||||
@@ -851,7 +1057,7 @@ const refreshDashboard = async () => {
|
||||
clearDashboardCache();
|
||||
onLoadBaseInfo(false, '');
|
||||
hasRefreshedOptionsOnHover.value = false;
|
||||
await Promise.allSettled([onLoadNetworkOptions(true), onLoadIOOptions(true), loadSettingInfo()]);
|
||||
await Promise.allSettled([onLoadNetworkOptions(true), onLoadIOOptions(true), loadSettingInfo(), loadVllmOptions()]);
|
||||
MsgSuccess(i18n.global.t('commons.msg.refreshSuccess'));
|
||||
};
|
||||
|
||||
@@ -1028,7 +1234,7 @@ const saveMemo = async () => {
|
||||
const loadData = async () => {
|
||||
if (chartOption.value === 'io') {
|
||||
chartsOption.value['ioChart'] = loadIOChartOption();
|
||||
} else {
|
||||
} else if (chartOption.value === 'network') {
|
||||
chartsOption.value['networkChart'] = loadNetworkChartOption();
|
||||
}
|
||||
};
|
||||
@@ -1097,12 +1303,14 @@ const loadSource = (row: any) => {
|
||||
const onFocus = () => {
|
||||
isActive.value = true;
|
||||
skipNextCurrentInfoDelta.value = true;
|
||||
void loadVllmOptions();
|
||||
};
|
||||
const onBlur = () => {
|
||||
isActive.value = false;
|
||||
};
|
||||
|
||||
const refreshOptionsOnHover = async () => {
|
||||
void loadVllmOptions();
|
||||
if (hasRefreshedOptionsOnHover.value) return;
|
||||
if (!netOptionsFromCache.value && !ioOptionsFromCache.value) return;
|
||||
hasRefreshedOptionsOnHover.value = true;
|
||||
@@ -1155,7 +1363,29 @@ onMounted(() => {
|
||||
loadMemo();
|
||||
});
|
||||
|
||||
watch(
|
||||
[currentNode, isProductPro, canViewVllm],
|
||||
() => {
|
||||
vllmOptionsVersion++;
|
||||
stopVllmMonitor();
|
||||
vllmOptions.value = [];
|
||||
vllmInstallID.value = undefined;
|
||||
if (chartOption.value === 'vllm') chartOption.value = 'network';
|
||||
void loadVllmOptions();
|
||||
},
|
||||
{ immediate: true, flush: 'sync' },
|
||||
);
|
||||
|
||||
watch([chartOption, vllmInstallID, isActive], ([, installID], [, previousInstallID]) => {
|
||||
stopVllmMonitor(installID !== previousInstallID);
|
||||
if (chartOption.value === 'vllm' && hasVllm.value && vllmInstallID.value && isActive.value) {
|
||||
void refreshVllmMonitor();
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
vllmOptionsVersion++;
|
||||
stopVllmMonitor();
|
||||
window.removeEventListener('focus', onFocus);
|
||||
window.removeEventListener('blur', onBlur);
|
||||
isCurrentActive.value = false;
|
||||
@@ -1189,6 +1419,16 @@ onBeforeUnmount(() => {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.vllm-monitor-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 0 20px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 767px) {
|
||||
.dashboard-entrance-alert {
|
||||
padding-right: 40px;
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
@keyup.enter="search"
|
||||
@clear="search"
|
||||
/>
|
||||
<el-button :disabled="loading" @click="search">{{ $t('commons.button.confirm') }}</el-button>
|
||||
<el-button :disabled="loading" @click="search">{{ $t('commons.button.search') }}</el-button>
|
||||
</div>
|
||||
<div class="directory-row directory-header">
|
||||
<span>{{ $t('file.path') }}</span>
|
||||
|
||||
@@ -93,14 +93,18 @@
|
||||
></el-table-column>
|
||||
<el-table-column :label="$t('commons.button.log')" prop="log" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip :content="row.log" placement="top">
|
||||
<el-tooltip :content="row.log" v-bind="TABLE_TOOLTIP_OPTIONS">
|
||||
{{ row.log }}
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('commons.table.message')" prop="status" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip v-if="row.status === 'FAILED'" :content="row.message" placement="top">
|
||||
<el-tooltip
|
||||
v-if="row.status === 'FAILED'"
|
||||
:content="row.message"
|
||||
v-bind="TABLE_TOOLTIP_OPTIONS"
|
||||
>
|
||||
<el-button type="danger" link>
|
||||
{{ $t('commons.status.' + row.status.toLowerCase()) }}
|
||||
</el-button>
|
||||
@@ -132,6 +136,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { TABLE_TOOLTIP_OPTIONS } from '@/components/table/shared';
|
||||
import { readCachedPageSize } from '@/utils/panel-cache';
|
||||
import i18n from '@/lang';
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<ComplexTable :pagination-config="paginationConfig" :data="data" @search="search">
|
||||
<el-table-column :label="$t('file.path')" show-overflow-tooltip prop="path">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip class="box-item" effect="dark" :content="row.path" placement="top">
|
||||
<el-tooltip class="box-item" effect="dark" :content="row.path" v-bind="TABLE_TOOLTIP_OPTIONS">
|
||||
<span class="table-link text-ellipsis" @click="toFavorite(row)" type="primary">
|
||||
<svg-icon v-if="row.isDir" className="table-icon" iconName="p-file-folder"></svg-icon>
|
||||
<svg-icon v-else className="table-icon" iconName="p-file-normal"></svg-icon>
|
||||
@@ -20,6 +20,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { TABLE_TOOLTIP_OPTIONS } from '@/components/table/shared';
|
||||
import { readCachedPageSize } from '@/utils/panel-cache';
|
||||
import { searchFavorite, removeFavorite } from '@/api/modules/files';
|
||||
import i18n from '@/lang';
|
||||
|
||||
@@ -506,7 +506,6 @@
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
class="file-table"
|
||||
:cache-pagination="false"
|
||||
:pagination-config="paginationConfig"
|
||||
v-model:selects="selects"
|
||||
:ref="(el) => setTableRef(item.id, el)"
|
||||
@@ -528,9 +527,6 @@
|
||||
show-overflow-tooltip
|
||||
:sortable="'custom'"
|
||||
prop="name"
|
||||
:tooltip-options="{
|
||||
placement: 'bottom-start',
|
||||
}"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="file-row">
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<ComplexTable :pagination-config="paginationConfig" :data="data" @search="search">
|
||||
<el-table-column :label="$t('file.path')" show-overflow-tooltip prop="path" min-width="250">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip class="box-item" effect="dark" :content="row.path" placement="top">
|
||||
<el-tooltip class="box-item" effect="dark" :content="row.path" v-bind="TABLE_TOOLTIP_OPTIONS">
|
||||
<span class="table-link text-ellipsis" @click="viewDetail(row)" type="primary">
|
||||
<svg-icon className="table-icon" iconName="p-file-normal"></svg-icon>
|
||||
{{ row.fileName }}
|
||||
@@ -24,6 +24,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { TABLE_TOOLTIP_OPTIONS } from '@/components/table/shared';
|
||||
import { readCachedPageSize } from '@/utils/panel-cache';
|
||||
import { removeFileShare, searchFileShare } from '@/api/modules/files';
|
||||
import { File } from '@/api/interface/file';
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
<ComplexTable
|
||||
v-model:selects="selects"
|
||||
:data="containerPageRows"
|
||||
:cache-pagination="false"
|
||||
:pagination-config="paginationConfig"
|
||||
:heightDiff="320"
|
||||
row-key="key"
|
||||
@@ -170,7 +169,6 @@
|
||||
v-model:selects="orphanSelects"
|
||||
v-loading="loading"
|
||||
:data="orphanPageRows"
|
||||
:cache-pagination="false"
|
||||
:pagination-config="orphanPaginationConfig"
|
||||
row-key="policyUUID"
|
||||
max-height="calc(100vh - 220px)"
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
row.interface,
|
||||
])
|
||||
"
|
||||
:cache-pagination="false"
|
||||
:pagination-config="paginationConfig"
|
||||
v-model:selects="selects"
|
||||
@search="search"
|
||||
|
||||
@@ -109,7 +109,6 @@
|
||||
<ComplexTable
|
||||
:selection-context="() => [searchName, selectedRuleFilters]"
|
||||
v-model:selects="selects"
|
||||
:cache-pagination="false"
|
||||
:pagination-config="paginationConfig"
|
||||
:data="allRows"
|
||||
:heightDiff="320 + noticeHeight"
|
||||
@@ -137,7 +136,7 @@
|
||||
<el-tooltip
|
||||
v-if="row.observed.protected && !row.isWhitelist"
|
||||
:content="$t('firewall.builtinRuleProtected')"
|
||||
placement="top"
|
||||
v-bind="TABLE_TOOLTIP_OPTIONS"
|
||||
:show-after="200"
|
||||
:popper-style="{ maxWidth: '320px' }"
|
||||
>
|
||||
@@ -205,7 +204,7 @@
|
||||
<div v-else class="firewall-used-cell">
|
||||
<el-tooltip
|
||||
:content="usageEntryLabel(usageEntriesByRow[row.rowKey][0])"
|
||||
placement="top"
|
||||
v-bind="TABLE_TOOLTIP_OPTIONS"
|
||||
>
|
||||
<el-tag
|
||||
class="cursor-pointer firewall-used-entry"
|
||||
@@ -245,7 +244,7 @@
|
||||
v-for="entry in usageEntriesByRow[row.rowKey]"
|
||||
:key="`${row.rowKey}:${entry.key}`"
|
||||
:content="usageEntryLabel(entry)"
|
||||
placement="top"
|
||||
v-bind="TABLE_TOOLTIP_OPTIONS"
|
||||
>
|
||||
<el-tag
|
||||
class="cursor-pointer firewall-used-popover-entry"
|
||||
@@ -305,7 +304,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { useSearchState } from '@/utils/search-reset';
|
||||
import { Firewall } from '@/api/interface/firewall';
|
||||
import type { FuTableOperationButton } from '@/components/table/shared';
|
||||
import { TABLE_TOOLTIP_OPTIONS, type FuTableOperationButton } from '@/components/table/shared';
|
||||
import { buildHostRuleExport } from './transfer';
|
||||
import { Process } from '@/api/interface/process';
|
||||
import {
|
||||
|
||||
@@ -26,13 +26,7 @@
|
||||
</template>
|
||||
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
:cache-pagination="false"
|
||||
:pagination-config="paginationConfig"
|
||||
:data="data"
|
||||
@search="search"
|
||||
:heightDiff="400"
|
||||
>
|
||||
<ComplexTable :pagination-config="paginationConfig" :data="data" @search="search" :heightDiff="400">
|
||||
<el-table-column min-width="80" :label="$t('logs.loginIP')" prop="address" />
|
||||
<el-table-column min-width="60" :label="$t('ssh.belong')" prop="area" />
|
||||
<el-table-column min-width="60" :label="$t('commons.table.port')" prop="port" />
|
||||
@@ -48,7 +42,12 @@
|
||||
<el-tag type="success">{{ $t('commons.status.success') }}</el-tag>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-tooltip class="box-item" effect="dark" :content="row.message" placement="top-start">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
:content="row.message"
|
||||
v-bind="TABLE_TOOLTIP_OPTIONS"
|
||||
>
|
||||
<el-tag type="danger">{{ $t('commons.status.failed') }}</el-tag>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
@@ -99,6 +98,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { TABLE_TOOLTIP_OPTIONS } from '@/components/table/shared';
|
||||
import { useSearchState } from '@/utils/search-reset';
|
||||
import ConfirmDialog from '@/components/confirm-dialog/index.vue';
|
||||
import { dateFormatServerTimezone } from '@/utils/date';
|
||||
|
||||
@@ -57,13 +57,7 @@
|
||||
<TableRefresh @search="resetAndLoadLogs" />
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
:data="logs"
|
||||
:cache-pagination="false"
|
||||
:pagination-config="paginationConfig"
|
||||
@row-click="openDetail"
|
||||
:heightDiff="320"
|
||||
>
|
||||
<ComplexTable :data="logs" :pagination-config="paginationConfig" @row-click="openDetail" :heightDiff="320">
|
||||
<el-table-column prop="time" :label="$t('commons.table.date')" width="220" show-overflow-tooltip />
|
||||
<el-table-column :label="$t('logs.priority')" width="120">
|
||||
<template #default="{ row }">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user