Compare commits

..
Author SHA1 Message Date
guoyuqi 1313462805 refactor: refactor user info 2026-10-10 10:27:47 +08:00
22 changed files with 161 additions and 377 deletions
+1 -4
View File
@@ -2,7 +2,6 @@ package repo
import (
"context"
"time"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
@@ -111,9 +110,7 @@ func (t TaskRepo) Update(ctx context.Context, task *model.Task) error {
}
func (t TaskRepo) UpdateRunningTaskToFailed() error {
return getTaskDb(t.WithByStatus(constant.StatusExecuting)).Model(&model.Task{}).Updates(map[string]interface{}{
"status": constant.StatusFailed, "error_msg": constant.InterruptedMsg, "end_at": time.Now(),
}).Error
return getTaskDb(t.WithByStatus(constant.StatusExecuting)).Model(&model.Task{}).Updates(map[string]interface{}{"status": constant.StatusFailed, "error_msg": "1Panel restart causes failure"}).Error
}
func (t TaskRepo) CountExecutingTask() (int64, error) {
+1 -6
View File
@@ -550,11 +550,6 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
installTask, err := task.NewTaskWithOps(appInstall.Name, task.TaskInstall, task.TaskScopeApp, req.TaskID, appInstall.ID)
if err != nil {
appInstall.Status = constant.StatusInstallErr
appInstall.Message = err.Error()
if saveErr := appInstallRepo.Save(context.Background(), appInstall); saveErr != nil {
err = fmt.Errorf("%w; save failed install status: %v", err, saveErr)
}
return
}
@@ -562,7 +557,7 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
return
}
installApp := func(t *task.Task) (err error) {
installApp := func(t *task.Task) error {
if err = copyData(t, app, appDetail, appInstall, req); err != nil {
return err
}
+4 -37
View File
@@ -339,11 +339,6 @@ func operateAppWithLifecycleScripts(install model.AppInstall, req request.AppIns
operationTask, err := task.NewTaskWithOps(install.Name, taskType, task.TaskScopeApp, req.TaskID, install.ID)
if err != nil {
install.Status = constant.StatusUpErr
install.Message = err.Error()
if saveErr := appInstallRepo.Save(context.Background(), &install); saveErr != nil {
return fmt.Errorf("%w; save failed operation status: %v", err, saveErr)
}
return err
}
operation := string(req.Operate)
@@ -572,15 +567,11 @@ func (a *AppInstallService) SyncAll(systemInit bool) error {
return err
}
for _, i := range allList {
if appInstallOperationInterruptedOnRestart(i.Status) {
if i.Status == constant.StatusInstalling || i.Status == constant.StatusUpgrading || i.Status == constant.StatusRebuilding || i.Status == constant.StatusUninstalling {
if systemInit {
i.Status = appInstallOperationFailureStatus(i.Status)
i.Message = constant.InterruptedMsg
if err := appInstallRepo.Save(context.Background(), &i); err != nil {
return err
}
} else if err := syncAppInstallStatus(&i, false); err != nil {
global.LOG.Errorf("sync install app[%s] error,mgs: %s", i.Name, err.Error())
i.Status = constant.StatusError
i.Message = "1Panel restart causes the task to terminate"
_ = appInstallRepo.Save(context.Background(), &i)
}
continue
}
@@ -940,34 +931,10 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
}
func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error {
operation := ""
switch appInstall.Status {
case constant.StatusInstalling:
operation = task.TaskInstall
case constant.StatusUpgrading:
operation = task.TaskUpgrade
case constant.StatusRebuilding:
operation = task.TaskBuild
case constant.StatusUninstalling:
operation = task.TaskUninstall
case constant.StatusStarting, constant.StatusWaiting:
operation = task.TaskUpdate
case constant.StatusRestarting:
operation = task.TaskRestart
}
if operation != "" {
if err := reconcileAppInstallTaskFailure(appInstall, operation, global.DB, global.TaskDB); err != nil {
return err
}
}
switch appInstall.Status {
case constant.StatusInstalling, constant.StatusRebuilding, constant.StatusUpgrading, constant.StatusUninstalling,
constant.StatusStarting, constant.StatusRestarting, constant.StatusWaiting:
return nil
case constant.StatusInstallErr, constant.StatusUpgradeErr, constant.StatusUpErr:
if !force {
return nil
}
}
cli, err := docker.NewClient()
if err != nil {
-76
View File
@@ -1,76 +0,0 @@
package service
import (
"errors"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/constant"
"gorm.io/gorm"
)
func appInstallOperationFailureStatus(status string) string {
switch status {
case constant.StatusInstalling:
return constant.StatusInstallErr
case constant.StatusUpgrading:
return constant.StatusUpgradeErr
case constant.StatusUninstalling:
return constant.StatusError
default:
return constant.StatusUpErr
}
}
func appInstallOperationPending(status string) bool {
switch status {
case constant.StatusInstalling, constant.StatusRebuilding, constant.StatusUpgrading, constant.StatusUninstalling,
constant.StatusStarting, constant.StatusRestarting, constant.StatusWaiting:
return true
default:
return false
}
}
func appInstallOperationInterruptedOnRestart(status string) bool {
// ReStarting can also describe a container's Docker restart policy.
return appInstallOperationPending(status) && status != constant.StatusRestarting
}
func reconcileAppInstallTaskFailure(install *model.AppInstall, operation string, appDB, taskDB *gorm.DB) error {
if !appInstallOperationPending(install.Status) {
return nil
}
var operationTask model.Task
err := taskDB.Where("type = ? AND resource_id = ? AND operate = ?", "App", install.ID, operation).
Order("created_at DESC").First(&operationTask).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
if err != nil {
return err
}
if operationTask.Status != constant.StatusFailed && operationTask.Status != constant.StatusCanceled {
return nil
}
// A previous operation must not turn a newly queued retry into a failure.
if operationTask.CreatedAt.Before(install.UpdatedAt) &&
(operationTask.EndAt.IsZero() || operationTask.EndAt.Before(install.UpdatedAt)) {
return nil
}
message := operationTask.ErrorMsg
if message == "" {
message = "the application operation task failed"
}
status := appInstallOperationFailureStatus(install.Status)
result := appDB.Model(&model.AppInstall{}).
Where("id = ? AND status = ? AND updated_at = ?", install.ID, install.Status, install.UpdatedAt).
Updates(map[string]interface{}{"status": status, "message": message})
if result.Error != nil {
return result.Error
}
if result.RowsAffected > 0 {
install.Status = status
install.Message = message
}
return nil
}
@@ -1,147 +0,0 @@
package service
import (
"path/filepath"
"testing"
"time"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/logger"
)
func operationStatusTestDB(t *testing.T) (*gorm.DB, *gorm.DB, model.AppInstall) {
t.Helper()
open := func(name string, schema interface{}) *gorm.DB {
db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), name)), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(schema); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
sqlDB, _ := db.DB()
if sqlDB != nil {
_ = sqlDB.Close()
}
})
return db
}
appDB := open("apps.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",
Status: constant.StatusStarting, Env: `{"MODEL_DIR":"/models/vision"}`,
}
if err := appDB.Omit(clause.Associations).Create(&install).Error; err != nil {
t.Fatal(err)
}
return appDB, taskDB, install
}
func TestAppInstallFailedTaskRecoversPendingState(t *testing.T) {
for _, state := range []string{constant.StatusStarting, constant.StatusWaiting, constant.StatusRestarting, constant.StatusInstalling} {
t.Run(state, func(t *testing.T) {
appDB, taskDB, install := operationStatusTestDB(t)
install.Status = state
if err := appDB.Omit(clause.Associations).Save(&install).Error; err != nil {
t.Fatal(err)
}
task := model.Task{ID: "failed", Type: "App", Operate: "TaskUpdate", ResourceID: install.ID,
Status: constant.StatusFailed, ErrorMsg: "model startup script failed", CreatedAt: install.UpdatedAt.Add(time.Second), EndAt: install.UpdatedAt.Add(2 * time.Second)}
if err := taskDB.Create(&task).Error; err != nil {
t.Fatal(err)
}
if err := reconcileAppInstallTaskFailure(&install, task.Operate, appDB, taskDB); err != nil {
t.Fatal(err)
}
want := constant.StatusUpErr
if state == constant.StatusInstalling {
want = constant.StatusInstallErr
}
var stored model.AppInstall
if err := appDB.First(&stored, install.ID).Error; err != nil {
t.Fatal(err)
}
if install.Status != want || stored.Status != want || stored.Message != task.ErrorMsg || stored.Env != install.Env {
t.Fatalf("recovered state = %s/%s, message %q, env %q", install.Status, stored.Status, stored.Message, stored.Env)
}
})
}
}
func TestAppInstallPendingStateKeepsActiveAndUnrelatedTasks(t *testing.T) {
for _, taskStatus := range []string{constant.StatusExecuting, constant.StatusSuccess, "missing", "previous-failure", "other-resource"} {
t.Run(taskStatus, func(t *testing.T) {
appDB, taskDB, install := operationStatusTestDB(t)
task := model.Task{ID: "task", Type: "App", Operate: "TaskUpdate", ResourceID: install.ID,
Status: taskStatus, CreatedAt: install.UpdatedAt.Add(time.Second), EndAt: install.UpdatedAt.Add(2 * time.Second)}
if taskStatus == "previous-failure" {
task.Status = constant.StatusFailed
task.CreatedAt = install.UpdatedAt.Add(-2 * time.Hour)
task.EndAt = install.UpdatedAt.Add(-time.Hour)
}
if taskStatus == "other-resource" {
task.Status = constant.StatusFailed
task.ResourceID++
}
if taskStatus != "missing" {
if err := taskDB.Create(&task).Error; err != nil {
t.Fatal(err)
}
}
if err := reconcileAppInstallTaskFailure(&install, task.Operate, appDB, taskDB); err != nil {
t.Fatal(err)
}
if install.Status != constant.StatusStarting {
t.Fatalf("unrelated or active task changed pending state to %s", install.Status)
}
})
}
}
func TestAppInstallRecoveryDoesNotOverwriteConcurrentRetry(t *testing.T) {
appDB, taskDB, install := operationStatusTestDB(t)
task := model.Task{ID: "failed", Type: "App", Operate: "TaskUpdate", ResourceID: install.ID,
Status: constant.StatusCanceled, ErrorMsg: "interrupted", CreatedAt: install.UpdatedAt.Add(time.Second), EndAt: install.UpdatedAt.Add(2 * time.Second)}
if err := taskDB.Create(&task).Error; err != nil {
t.Fatal(err)
}
if err := appDB.Callback().Update().Before("gorm:update").Register("new-operation", func(db *gorm.DB) {
if err := db.Exec("UPDATE app_installs SET updated_at = ?, message = ? WHERE id = ?", install.UpdatedAt.Add(time.Hour), "new attempt", install.ID).Error; err != nil {
t.Error(err)
}
}); err != nil {
t.Fatal(err)
}
if err := reconcileAppInstallTaskFailure(&install, task.Operate, appDB, taskDB); err != nil {
t.Fatal(err)
}
var stored model.AppInstall
if err := appDB.First(&stored, install.ID).Error; err != nil {
t.Fatal(err)
}
if stored.Status != constant.StatusStarting || stored.Message != "new attempt" {
t.Fatalf("recovery overwrote a newer operation: %s, %s", stored.Status, stored.Message)
}
}
func TestAppInstallRestartRecoveryCoversLifecycleStates(t *testing.T) {
for _, state := range []string{constant.StatusStarting, constant.StatusWaiting, constant.StatusInstalling} {
if !appInstallOperationInterruptedOnRestart(state) {
t.Errorf("restart recovery omitted %s", state)
}
}
for _, state := range []string{constant.StatusRunning, constant.StatusStopped, constant.StatusRestarting} {
if appInstallOperationInterruptedOnRestart(state) {
t.Errorf("restart recovery changes a Docker runtime state: %s", state)
}
}
}
+3 -7
View File
@@ -1406,8 +1406,9 @@ func handleErr(install model.AppInstall, err error, out string) error {
}
func doNotNeedSync(installed model.AppInstall) bool {
return appInstallOperationPending(installed.Status) || installed.Status == constant.StatusSyncing ||
installed.Status == constant.StatusInstallErr || installed.Status == constant.StatusUpgradeErr || installed.Status == constant.StatusUpErr
return installed.Status == constant.StatusInstalling || installed.Status == constant.StatusRebuilding || installed.Status == constant.StatusUpgrading ||
installed.Status == constant.StatusSyncing || installed.Status == constant.StatusUninstalling || installed.Status == constant.StatusInstallErr ||
installed.Status == constant.StatusStarting || installed.Status == constant.StatusRestarting || installed.Status == constant.StatusWaiting
}
func synAppInstall(containers map[string]container.Summary, appInstall *model.AppInstall, force bool) {
@@ -1501,11 +1502,6 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
}
for _, installed := range appInstallList {
if sync && appInstallOperationPending(installed.Status) {
if err := syncAppInstallStatus(&installed, false); err != nil {
return nil, err
}
}
if updated && ignoreUpdate(installed) {
continue
}
+1 -4
View File
@@ -57,10 +57,7 @@ func (u *TaskLogService) ReadByLine(req request.TaskLogReadReq) (*response.FileL
if req.TaskID != "" {
opts = append(opts, taskRepo.WithByID(req.TaskID))
} else {
opts = append(opts, repo.WithOrderRuleBy("created_at", "desc"), repo.WithByType(req.TaskType), taskRepo.WithResourceID(req.ResourceID))
if req.TaskOperate != "" {
opts = append(opts, taskRepo.WithOperate(req.TaskOperate))
}
opts = append(opts, repo.WithOrderRuleBy("created_at", "desc"), repo.WithByType(req.TaskType), taskRepo.WithOperate(req.TaskOperate), taskRepo.WithResourceID(req.ResourceID))
}
taskModel, err := taskRepo.GetFirst(opts...)
if err != nil {
+1
View File
@@ -87,6 +87,7 @@ const (
TaskScopeCronjob = "Cronjob"
TaskScopeClam = "Clam"
TaskScopeSystem = "System"
TaskScopeVm = "VirtualMachine"
TaskScopeFirewall = "Firewall"
TaskScopeAppStore = "AppStore"
TaskScopeSnapshot = "Snapshot"
+2 -7
View File
@@ -570,6 +570,8 @@ const message = {
cancelKeepAlive: 'Stop keeping page session',
},
header: {
userCenter: 'User Center',
exitSystem: 'Log Out',
logout: 'Logout',
},
database: {
@@ -1065,13 +1067,6 @@ const message = {
whiteListHelper: 'Restrict access to only IPs in the whitelist',
},
aiProxy: {
deploymentNoticeTitle: 'AI Gateway supports standalone deployment',
deploymentNoticeDescription:
'AI Gateway now supports standalone deployment. The built-in AI Gateway in 1Panel will no longer receive updates. We recommend installing the standalone version from the App Store. You can also continue using the current version.',
deploymentNoticeContinue: 'Understood, continue using',
deploymentNoticeAppStore: 'Go to App Store',
deploymentNoticeNoAppPermission:
'You do not have access to the App Store. Contact your administrator to install the standalone version.',
title: 'AI Gateway',
apiReference: 'API Reference',
notInstalled: 'Not installed',
+2 -7
View File
@@ -570,6 +570,8 @@ const message = {
cancelKeepAlive: 'Dejar de mantener la sesión de la página',
},
header: {
userCenter: 'Centro de usuario',
exitSystem: 'Cerrar sesión',
logout: 'Cerrar sesión',
},
database: {
@@ -1069,13 +1071,6 @@ const message = {
whiteListHelper: 'Restringir el acceso solo a las IP incluidas en la lista blanca',
},
aiProxy: {
deploymentNoticeTitle: 'AI Gateway admite la implementación independiente',
deploymentNoticeDescription:
'AI Gateway ahora admite la implementación independiente. AI Gateway integrado en 1Panel dejará de recibir actualizaciones. Recomendamos instalar la versión independiente desde la tienda de aplicaciones. También puede seguir usando la versión actual.',
deploymentNoticeContinue: 'Entendido, seguir usando',
deploymentNoticeAppStore: 'Ir a la tienda de aplicaciones',
deploymentNoticeNoAppPermission:
'No tiene acceso a la tienda de aplicaciones. Contacte con su administrador para instalar la versión independiente.',
title: 'Gateway de IA',
apiReference: 'Referencia de API',
notInstalled: 'No instalado',
+2 -7
View File
@@ -565,6 +565,8 @@ const message = {
cancelKeepAlive: 'لغو حفظ نشست صفحه',
},
header: {
userCenter: 'مرکز کاربری',
exitSystem: 'خروج از سیستم',
logout: 'خروج',
},
database: {
@@ -1051,13 +1053,6 @@ const message = {
whiteListHelper: 'دسترسی را فقط به IP‌های موجود در لیست سفید محدود کنید',
},
aiProxy: {
deploymentNoticeTitle: 'درگاه هوش مصنوعی از استقرار مستقل پشتیبانی می‌کند',
deploymentNoticeDescription:
'درگاه هوش مصنوعی اکنون از استقرار مستقل پشتیبانی می‌کند. درگاه هوش مصنوعی داخلی 1Panel دیگر به‌روزرسانی دریافت نخواهد کرد. توصیه می‌کنیم نسخه مستقل را از فروشگاه برنامه نصب کنید. همچنین می‌توانید به استفاده از نسخه فعلی ادامه دهید.',
deploymentNoticeContinue: 'متوجه شدم، ادامه استفاده',
deploymentNoticeAppStore: 'رفتن به فروشگاه برنامه',
deploymentNoticeNoAppPermission:
'شما به فروشگاه برنامه دسترسی ندارید. برای نصب نسخه مستقل با مدیر تماس بگیرید.',
title: 'دروازه AI',
apiReference: 'راهنمای API',
notInstalled: 'نصب نشده',
+2 -7
View File
@@ -568,6 +568,8 @@ const message = {
cancelKeepAlive: 'ページセッションの保持を解除',
},
header: {
userCenter: 'ユーザーセンター',
exitSystem: 'ログアウト',
logout: 'ログアウト',
},
database: {
@@ -1054,13 +1056,6 @@ const message = {
whiteListHelper: 'ホワイトリスト内のIPのみアクセスを許可する',
},
aiProxy: {
deploymentNoticeTitle: 'AI ゲートウェイの独立したデプロイに対応',
deploymentNoticeDescription:
'AI ゲートウェイは独立したデプロイに対応しました。1Panel 内蔵の AI ゲートウェイは今後更新されません。アプリストアから独立版をインストールすることをお勧めします。現在のバージョンを引き続き使用することもできます。',
deploymentNoticeContinue: '了解して使用を継続',
deploymentNoticeAppStore: 'アプリストアへ',
deploymentNoticeNoAppPermission:
'アプリストアへのアクセス権限がありません。管理者に独立版のインストールを依頼してください。',
title: 'AI ゲートウェイ',
apiReference: 'API リファレンス',
notInstalled: '未インストール',
+2 -6
View File
@@ -562,6 +562,8 @@ const message = {
cancelKeepAlive: '페이지 세션 유지 해제',
},
header: {
userCenter: '사용자 센터',
exitSystem: '로그아웃',
logout: '로그아웃',
},
database: {
@@ -1043,12 +1045,6 @@ const message = {
whiteListHelper: '화이트리스트에 있는 IP만 접근 허용',
},
aiProxy: {
deploymentNoticeTitle: 'AI 게이트웨이 독립 배포 지원',
deploymentNoticeDescription:
'AI 게이트웨이가 독립 배포를 지원합니다. 1Panel 내장 AI 게이트웨이는 더 이상 업데이트되지 않습니다. 앱 스토어에서 독립 버전을 설치하는 것을 권장합니다. 현재 버전을 계속 사용할 수도 있습니다.',
deploymentNoticeContinue: '확인, 계속 사용',
deploymentNoticeAppStore: '앱 스토어로 이동',
deploymentNoticeNoAppPermission: '앱 스토어 접근 권한이 없습니다. 관리자에게 독립 버전 설치를 요청하세요.',
title: 'AI 게이트웨이',
apiReference: 'API 안내',
notInstalled: '설치되지 않음',
+2 -7
View File
@@ -563,6 +563,8 @@ const message = {
cancelKeepAlive: 'ຍົກເລີກການຮັກສາເຊັດຊັນໜ້າ',
},
header: {
userCenter: 'ສູນຜູ້ໃຊ້',
exitSystem: 'ອອກຈາກລະບົບ',
logout: 'ອອກຈາກລະບົບ',
},
database: {
@@ -1040,13 +1042,6 @@ const message = {
whiteListHelper: 'ຈຳກັດການເຂົ້າເຖິງສະເພາະ IP ໃນລາຍຊື່ທີ່ອະນຸຍາດ',
},
aiProxy: {
deploymentNoticeTitle: 'AI Gateway ຮອງຮັບການຕິດຕັ້ງແບບແຍກຕ່າງຫາກ',
deploymentNoticeDescription:
'AI Gateway ຮອງຮັບການຕິດຕັ້ງແບບແຍກຕ່າງຫາກແລ້ວ. AI Gateway ທີ່ມາພ້ອມກັບ 1Panel ຈະບໍ່ໄດ້ຮັບການອັບເດດອີກ. ແນະນຳໃຫ້ຕິດຕັ້ງລຸ້ນແຍກຕ່າງຫາກຈາກຮ້ານແອັບ. ທ່ານຍັງສາມາດໃຊ້ລຸ້ນປັດຈຸບັນຕໍ່ໄດ້.',
deploymentNoticeContinue: 'ຮັບຊາບ, ໃຊ້ຕໍ່',
deploymentNoticeAppStore: 'ໄປທີ່ຮ້ານແອັບ',
deploymentNoticeNoAppPermission:
'ທ່ານບໍ່ມີສິດເຂົ້າເຖິງຮ້ານແອັບ. ກະລຸນາຕິດຕໍ່ຜູ້ດູແລເພື່ອຕິດຕັ້ງລຸ້ນແຍກຕ່າງຫາກ.',
title: 'AI Gateway',
apiReference: 'ຄູ່ມື API',
notInstalled: 'ຍັງບໍ່ໄດ້ຕິດຕັ້ງ',
+2 -7
View File
@@ -575,6 +575,8 @@ const message = {
cancelKeepAlive: 'Hentikan pengekalan sesi halaman',
},
header: {
userCenter: 'Pusat Pengguna',
exitSystem: 'Log keluar',
logout: 'Log keluar',
},
database: {
@@ -1077,13 +1079,6 @@ const message = {
whiteListHelper: 'Hadkan akses kepada hanya IP dalam senarai putih',
},
aiProxy: {
deploymentNoticeTitle: 'AI Gateway menyokong penggunaan kendiri',
deploymentNoticeDescription:
'AI Gateway kini menyokong penggunaan kendiri. AI Gateway terbina dalam 1Panel tidak lagi akan menerima kemas kini. Kami mengesyorkan pemasangan versi kendiri daripada gedung aplikasi. Anda juga boleh terus menggunakan versi semasa.',
deploymentNoticeContinue: 'Faham, terus gunakan',
deploymentNoticeAppStore: 'Pergi ke gedung aplikasi',
deploymentNoticeNoAppPermission:
'Anda tidak mempunyai akses ke gedung aplikasi. Hubungi pentadbir untuk memasang versi kendiri.',
title: 'Gateway AI',
apiReference: 'Rujukan API',
notInstalled: 'Belum dipasang',
+2 -7
View File
@@ -568,6 +568,8 @@ const message = {
cancelKeepAlive: 'Parar de manter a sessão da página',
},
header: {
userCenter: 'Central do usuário',
exitSystem: 'Sair',
logout: 'Logout',
},
database: {
@@ -1072,13 +1074,6 @@ const message = {
'Após ativar, você pode desativar o acesso externo à porta em Loja de Apps - Instalado - Ollama - Parâmetros para melhorar a segurança.',
},
aiProxy: {
deploymentNoticeTitle: 'AI Gateway permite implantação independente',
deploymentNoticeDescription:
'O AI Gateway agora permite implantação independente. O AI Gateway integrado ao 1Panel não receberá mais atualizações. Recomendamos instalar a versão independente pela loja de aplicativos. Você também pode continuar usando a versão atual.',
deploymentNoticeContinue: 'Entendido, continuar usando',
deploymentNoticeAppStore: 'Ir à loja de aplicativos',
deploymentNoticeNoAppPermission:
'Você não tem acesso à loja de aplicativos. Contate o administrador para instalar a versão independente.',
title: 'Gateway de IA',
apiReference: 'Referência da API',
notInstalled: 'Não instalado',
+2 -7
View File
@@ -566,6 +566,8 @@ const message = {
cancelKeepAlive: 'Не сохранять сеанс страницы',
},
header: {
userCenter: 'Центр пользователя',
exitSystem: 'Выйти из системы',
logout: 'Выход',
},
database: {
@@ -1065,13 +1067,6 @@ const message = {
whiteListHelper: 'Ограничить доступ только для IP-адресов из белого списка',
},
aiProxy: {
deploymentNoticeTitle: 'AI Gateway поддерживает отдельное развёртывание',
deploymentNoticeDescription:
'AI Gateway теперь поддерживает отдельное развёртывание. Встроенный AI Gateway в 1Panel больше не будет получать обновления. Рекомендуем установить отдельную версию из магазина приложений. Вы также можете продолжить использовать текущую версию.',
deploymentNoticeContinue: 'Понятно, продолжить использование',
deploymentNoticeAppStore: 'Перейти в магазин приложений',
deploymentNoticeNoAppPermission:
'У вас нет доступа к магазину приложений. Обратитесь к администратору для установки отдельной версии.',
title: 'AI-шлюз',
apiReference: 'Справочник API',
notInstalled: 'Не установлен',
+2 -7
View File
@@ -570,6 +570,8 @@ const message = {
cancelKeepAlive: 'Sayfa oturumunu korumayı bırak',
},
header: {
userCenter: 'Kullanıcı Merkezi',
exitSystem: 'Çıkış yap',
logout: 'Çıkış',
},
database: {
@@ -1076,13 +1078,6 @@ const message = {
whiteListHelper: 'Erişimi yalnızca beyaz listedeki IPlerle sınırlayın',
},
aiProxy: {
deploymentNoticeTitle: 'AI Gateway bağımsız dağıtımı destekliyor',
deploymentNoticeDescription:
'AI Gateway artık bağımsız dağıtımı destekliyor. 1Panel içindeki AI Gateway artık güncelleme almayacak. Uygulama mağazasından bağımsız sürümü yüklemenizi öneririz. Mevcut sürümü kullanmaya da devam edebilirsiniz.',
deploymentNoticeContinue: 'Anladım, kullanmaya devam et',
deploymentNoticeAppStore: 'Uygulama mağazasına git',
deploymentNoticeNoAppPermission:
'Uygulama mağazasına erişim izniniz yok. Bağımsız sürümü yüklemek için yöneticinizle iletişime geçin.',
title: 'AI Ağ Geçidi',
apiReference: 'API Referansı',
notInstalled: 'Kurulu değil',
+2 -6
View File
@@ -548,6 +548,8 @@ const message = {
cancelKeepAlive: '取消保持頁面工作階段',
},
header: {
userCenter: '使用者中心',
exitSystem: '登出系統',
logout: '登出',
},
database: {
@@ -1006,12 +1008,6 @@ const message = {
whiteListHelper: '限制僅白名單中的 IP 可瀏覽',
},
aiProxy: {
deploymentNoticeTitle: 'AI 網關已支援獨立部署',
deploymentNoticeDescription:
'AI 網關現已支援獨立部署,1Panel 內建 AI 網關後續將不再更新。建議前往應用程式商店安裝獨立版本,您也可以繼續使用目前版本。',
deploymentNoticeContinue: '已知悉,繼續使用',
deploymentNoticeAppStore: '前往應用程式商店',
deploymentNoticeNoAppPermission: '您沒有應用程式商店存取權限,請聯絡管理員安裝獨立版本。',
title: 'AI 閘道',
apiReference: '介面說明',
notInstalled: '未安裝',
+2 -6
View File
@@ -552,6 +552,8 @@ const message = {
cancelKeepAlive: '取消保持页面会话',
},
header: {
userCenter: '用户中心',
exitSystem: '退出系统',
logout: '退出登录',
},
database: {
@@ -1019,12 +1021,6 @@ const message = {
whiteListHelper: '限制仅白名单中的 IP 可访问',
},
aiProxy: {
deploymentNoticeTitle: 'AI 网关已支持独立部署',
deploymentNoticeDescription:
'AI 网关现已支持独立部署,1Panel 内置 AI 网关后续将不再更新。建议前往应用商店安装独立版本,您也可以继续使用当前版本。',
deploymentNoticeContinue: '已知晓,继续使用',
deploymentNoticeAppStore: '前往应用商店',
deploymentNoticeNoAppPermission: '您没有应用商店访问权限,请联系管理员安装独立版本。',
title: 'AI 网关',
apiReference: '接口说明',
notInstalled: '未安装',
@@ -2,7 +2,7 @@
<div>
<el-popover
v-model:visible="popoverVisible"
placement="right-end"
placement="right-start"
:show-arrow="false"
:offset="0"
:width="200"
@@ -26,12 +26,6 @@
</div>
</template>
<div class="dropdown-menu" v-loading="loading || switchingNode">
<div class="dropdown-item" v-if="currentUser" @click="changeUserInfo">
<SvgIcon class="icon" iconName="p-gerenzhongxin1" />
{{ currentUser.name }}
</div>
<el-divider class="divider" />
<div class="dropdown-item" @click="openTask">
<SvgIcon class="icon" iconName="p-renwuzhongxin1" />
{{ $t('menu.msgCenter') }}
@@ -74,11 +68,6 @@
</el-icon>
</div>
</div>
<el-divider class="divider" />
<div class="dropdown-item" @click="logout">
<SvgIcon class="icon" iconName="p-tuichudenglu3" />
{{ $t('commons.login.logout') }}
</div>
</div>
</el-popover>
<NodeDrawer
@@ -129,7 +118,7 @@ const props = defineProps({
const defaultNodeLimit = 8;
const emit = defineEmits(['openTask', 'refresh']);
const emit = defineEmits(['openTask', 'refresh', 'userChange']);
bus.on('refreshTask', () => {
checkTask();
});
@@ -272,6 +261,7 @@ const openNodeDashboard = () => {
};
const logout = () => {
popoverVisible.value = false;
ElMessageBox.confirm(i18n.global.t('commons.msg.sureLogOut'), i18n.global.t('commons.msg.infoTitle'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
@@ -297,13 +287,17 @@ const loadCurrentUser = async (currentNode?: string) => {
const authInfo = await syncAuthInfo(currentNode);
if (authInfo) {
currentUser.value = authInfo;
emit('userChange', authInfo);
}
};
const changeUserInfo = () => {
popoverVisible.value = false;
loadCurrentUser();
userInfoRef.value?.openDrawer();
};
defineExpose({ openUserInfo: changeUserInfo, logout });
onMounted(() => {
loadNodes();
checkTask();
@@ -318,7 +312,7 @@ onMounted(() => {
display: flex;
align-items: center;
box-sizing: border-box;
border-top: 1px solid var(--panel-footer-border);
border-bottom: 1px solid var(--panel-footer-border);
height: 48px;
.icon {
margin-left: 25px;
@@ -10,6 +10,14 @@
<PrimaryMenu />
</div>
<Logo :isCollapse="isCollapse" />
<Collapse
ref="nodeSelectorRef"
class="node-selector"
:version="version"
@open-task="openTask"
@refresh="search"
@user-change="currentUser = $event"
/>
<el-scrollbar>
<el-menu
:default-active="activeMenu"
@@ -23,7 +31,40 @@
<SubItem :menuList="routerMenus" :level="0" />
</el-menu>
</el-scrollbar>
<Collapse :version="version" @open-task="openTask" @refresh="search" />
<el-dropdown
v-if="currentUser"
class="user-dropdown"
trigger="click"
placement="right-end"
:popper-options="{ modifiers: [{ name: 'flip', enabled: false }] }"
:show-arrow="false"
:hide-on-click="true"
@visible-change="userMenuVisible = $event"
@command="handleUserCommand"
>
<button
type="button"
class="user-entry"
:class="{ 'is-active': userMenuVisible }"
:aria-label="currentUser.name"
:title="currentUser.name"
>
<SvgIcon class="icon" iconName="p-gerenzhongxin1" />
<span v-if="!isCollapse" class="user-name">{{ currentUser.name }}</span>
</button>
<template #dropdown>
<el-dropdown-menu class="user-menu">
<el-dropdown-item command="userCenter">
<SvgIcon class="user-menu-icon" iconName="p-gerenzhongxin1" />
<span>{{ $t('header.userCenter') }}</span>
</el-dropdown-item>
<el-dropdown-item command="logout">
<SvgIcon class="user-menu-icon" iconName="p-tuichudenglu3" />
<span>{{ $t('header.exitSystem') }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
@@ -40,12 +81,24 @@ import { getSettingBaseInfo } from '@/api/modules/setting';
import PrimaryMenu from '@/assets/images/menu-bg.svg?component';
import { hasPermissionMetaAccess, hasRouteRoleAccess } from '@/utils/rbac';
import { useGlobalStore } from '@/composables/useGlobalStore';
import type { Login } from '@/api/interface/auth';
const route = useRoute();
const router = useRouter();
const menuStore = MenuStore();
const { currentNode, isAdmin, menuAccordion, permissions } = useGlobalStore();
const version = ref();
const nodeSelectorRef = ref<InstanceType<typeof Collapse>>();
const currentUser = ref<Login.AuthInfo>();
const userMenuVisible = ref(false);
const handleUserCommand = (command: string) => {
if (command === 'userCenter') {
nodeSelectorRef.value?.openUserInfo();
} else if (command === 'logout') {
nodeSelectorRef.value?.logout();
}
};
const activeMenu = computed(() => {
const { meta, path } = route;
@@ -324,8 +377,14 @@ watch(
height: 100%;
background: var(--panel-menu-bg-color) no-repeat top;
.logo,
.node-selector {
flex-shrink: 0;
}
.el-scrollbar {
flex: 1;
min-height: 0;
.el-menu {
overflow: auto;
overflow-x: hidden;
@@ -337,4 +396,62 @@ watch(
.ico {
height: 20px !important;
}
.user-dropdown {
flex-shrink: 0;
width: 100%;
}
.user-menu {
min-width: 160px;
:deep(.el-dropdown-menu__item) {
gap: 8px;
padding: 10px 16px;
}
.user-menu-icon {
flex-shrink: 0;
font-size: 8px;
}
}
.user-entry {
display: flex;
align-items: center;
gap: 7px;
flex-shrink: 0;
position: relative;
width: 100%;
height: 48px;
padding: 0 25px;
border: 0;
border-top: 1px solid var(--panel-footer-border);
background: transparent;
color: var(--panel-main-bg-color-1);
font-family: inherit;
font-size: var(--el-font-size-base);
line-height: 1.5;
text-align: left;
cursor: pointer;
.icon {
flex-shrink: 0;
font-size: 8px;
}
.user-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&:hover,
&:focus-visible,
&.is-active {
color: var(--el-color-primary);
background: var(--el-menu-item-bg-color);
}
}
</style>