From a5fbbfc4604a0b24440ca7294d949fec0bef3546 Mon Sep 17 00:00:00 2001 From: CityFun <31820853+zhengkunwang223@users.noreply.github.com> Date: Wed, 29 Jul 2026 17:19:52 +0800 Subject: [PATCH] feat: Optimize the application upgrade logic. (#13415) --- agent/app/service/app_utils.go | 420 +------------------------- agent/app/service/backup_app.go | 34 ++- agent/app/service/container_update.go | 26 +- agent/app/service/nginx_module.go | 1 + agent/i18n/lang/en.yaml | 5 + agent/i18n/lang/es-ES.yaml | 5 + agent/i18n/lang/fa.yaml | 5 + agent/i18n/lang/ja.yaml | 5 + agent/i18n/lang/ko.yaml | 5 + agent/i18n/lang/lo.yaml | 5 + agent/i18n/lang/ms.yaml | 5 + agent/i18n/lang/pt-BR.yaml | 5 + agent/i18n/lang/ru.yaml | 5 + agent/i18n/lang/tr.yaml | 5 + agent/i18n/lang/zh-Hant.yaml | 5 + agent/i18n/lang/zh.yaml | 5 + agent/utils/compose/compose.go | 38 ++- frontend/src/lang/modules/en.ts | 10 +- frontend/src/lang/modules/es-es.ts | 10 +- frontend/src/lang/modules/fa.ts | 10 +- frontend/src/lang/modules/ja.ts | 10 +- frontend/src/lang/modules/ko.ts | 10 +- frontend/src/lang/modules/lo.ts | 10 +- frontend/src/lang/modules/ms.ts | 10 +- frontend/src/lang/modules/pt-br.ts | 10 +- frontend/src/lang/modules/ru.ts | 10 +- frontend/src/lang/modules/tr.ts | 10 +- frontend/src/lang/modules/zh-Hant.ts | 10 +- frontend/src/lang/modules/zh.ts | 10 +- 29 files changed, 236 insertions(+), 463 deletions(-) diff --git a/agent/app/service/app_utils.go b/agent/app/service/app_utils.go index cd48dfe57..a3c213bb6 100644 --- a/agent/app/service/app_utils.go +++ b/agent/app/service/app_utils.go @@ -9,7 +9,6 @@ import ( "math" "net/http" "os" - "os/exec" "path" "path/filepath" "reflect" @@ -783,416 +782,6 @@ func buildNginx(parentTask *task.Task, nginxInstall model.AppInstall, catalogPat return commitNginxModuleBuilds(nginxInstall, previousModules, modules, false, catalogPath) } -func upgradeInstall(req request.AppInstallUpgrade) error { - install, err := appInstallRepo.GetFirst(repo.WithByID(req.InstallID)) - if err != nil { - return err - } - originalInstall := install - oldVersion := install.Version - detail, err := appDetailRepo.GetFirst(repo.WithByID(req.DetailID)) - if err != nil { - return err - } - if install.App.Key == vllmAppKeyForUpgrade && !isVllmUpgradeVersionAllowed(install.Version, detail.Version, loadVllmImageFromEnv(install.Env)) { - return errors.New("vLLM can only upgrade within the same image type") - } - if install.Version == detail.Version { - return errors.New("two version is same") - } - - upgradeTask, err := task.NewTaskWithOps(install.Name, task.TaskUpgrade, task.TaskScopeApp, req.TaskID, install.ID) - if err != nil { - return err - } - install.Status = constant.StatusUpgrading - - var ( - upErr error - backupFile string - nginxUpgradeSnapshot *openrestyUpgradeSnapshot - ) - backUpApp := func(t *task.Task) error { - backupService := NewIBackupService() - backupRecordService := NewIBackupRecordService() - fileName := fmt.Sprintf("upgrade_backup_%s_%s.tar.gz", install.Name, time.Now().Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5)) - backupRecord, err := backupService.AppBackup(dto.CommonBackup{Name: install.App.Key, DetailName: install.Name, FileName: fileName}) - if err == nil { - backups, _ := backupRecordService.ListAppRecords(install.App.Key, install.Name, "upgrade_backup") - if len(backups) > 3 { - backupsToDelete := backups[:len(backups)-3] - var deleteIDs []uint - for _, backup := range backupsToDelete { - deleteIDs = append(deleteIDs, backup.ID) - } - _ = backupRecordService.BatchDeleteRecord(deleteIDs) - } - backupFile = path.Join(global.Dir.LocalBackupDir, backupRecord.FileDir, backupRecord.FileName) - } else { - return buserr.WithNameAndErr("ErrAppBackup", install.Name, err) - } - return nil - } - if req.Backup { - upgradeTask.AddSubTask(task.GetTaskName(install.Name, task.TaskBackup, task.TaskScopeApp), backUpApp, nil) - } - - upgradeApp := func(t *task.Task) error { - fileOp := files.NewFileOp() - detailDir := path.Join(global.Dir.ResourceDir, "apps", install.App.Resource, install.App.Key, detail.Version) - if install.App.Resource == constant.AppResourceRemote { - if err = downloadApp(install.App, detail, &install, t.Logger); err != nil { - return err - } - if detail.DockerCompose == "" { - composeDetail, err := fileOp.GetContent(path.Join(detailDir, "docker-compose.yml")) - if err != nil { - return err - } - detail.DockerCompose = string(composeDetail) - _ = appDetailRepo.Update(context.Background(), detail) - } - go func() { - RequestDownloadCallBack(detail.DownloadCallBackUrl) - }() - } - if install.App.Resource == constant.AppResourceLocal { - detailDir = path.Join(global.Dir.ResourceDir, "apps", "local", strings.TrimPrefix(install.App.Key, "local"), detail.Version) - } - - content, err := fileOp.GetContent(install.GetEnvPath()) - if err != nil { - return err - } - oldEnvContent := append([]byte(nil), content...) - oldDockerCompose := install.DockerCompose - targetNginxCatalogPath := "" - if install.App.Key == constant.AppOpenresty { - nginxUpgradeSnapshot, err = createOpenrestyUpgradeSnapshot(install.GetPath()) - if err != nil { - return err - } - } - if install.App.Key == vllmAppKeyForUpgrade { - envs := make(map[string]interface{}) - if err = json.Unmarshal([]byte(install.Env), &envs); err != nil { - return err - } - image := buildVllmUpgradeImage(loadVllmImageFromEnv(install.Env), oldVersion, detail.Version) - envs[vllmImageEnvKey] = image - paramByte, err := json.Marshal(envs) - if err != nil { - return err - } - install.Env = string(paramByte) - content = setVllmImageInEnvContent(content, image) - } - _ = copyAppDetailMissing(fileOp, detailDir, install.GetPath()) - if install.App.Key == constant.AppOpenresty { - installBuildDir := path.Join(install.GetPath(), nginxModuleBuildDir) - detailBuildDir := path.Join(detailDir, nginxModuleBuildDir) - if !fileOp.Stat(installBuildDir) { - if err := fileOp.CreateDir(installBuildDir, constant.DirPerm); err != nil { - return err - } - } - if err := fileOp.DeleteDir(path.Join(installBuildDir, nginxModuleTmpDir)); err != nil { - return err - } - if err := fileOp.CopyDir(path.Join(detailBuildDir, nginxModuleTmpDir), installBuildDir); err != nil { - return err - } - if err := fileOp.CopyFile(path.Join(detailBuildDir, "Dockerfile"), installBuildDir); err != nil { - return err - } - if err := syncNginxModuleBuilder(detailBuildDir, installBuildDir); err != nil { - return err - } - targetCatalogSource := path.Join(detailBuildDir, nginxModuleCatalogFile) - if !fileOp.Stat(targetCatalogSource) { - return fmt.Errorf("target OpenResty module catalog not found: %s", targetCatalogSource) - } - targetNginxCatalogPath = path.Join(installBuildDir, nginxModuleCatalogPendingFile) - if err := stageNginxModuleCatalog(targetCatalogSource, targetNginxCatalogPath); err != nil { - return err - } - if err := fileOp.CopyFile(path.Join(detailBuildDir, "nginx.conf"), installBuildDir); err != nil { - return err - } - if err := fileOp.CopyFile(path.Join(detailBuildDir, "nginx.vh.default.conf"), installBuildDir); err != nil { - return err - } - } - sourceScripts := path.Join(detailDir, "scripts") - if fileOp.Stat(sourceScripts) { - dstScripts := path.Join(install.GetPath(), "scripts") - _ = fileOp.DeleteDir(dstScripts) - _ = fileOp.CreateDir(dstScripts, constant.DirPerm) - scriptCmd := exec.Command("cp", "-rf", sourceScripts+"/.", dstScripts+"/") - _, _ = scriptCmd.CombinedOutput() - } - - var newCompose string - if err = migrateOpenclawProtocolUpgrade(&install, oldVersion, detail.Version); err != nil { - return err - } - if req.DockerCompose == "" { - if install.App.Key == vllmAppKeyForUpgrade { - newCompose = install.DockerCompose - } else { - newCompose, err = getUpgradeCompose(install, detail) - if err != nil { - return err - } - } - } else { - newCompose = req.DockerCompose - } - - install.DockerCompose = newCompose - install.Version = detail.Version - install.AppDetailId = req.DetailID - - var oldImageIDs []appImageID - if req.DeleteImage { - dockerCLi, err := docker.NewClient() - if err != nil { - return err - } - oldImageIDs, err = getAppImageIDsByCompose(dockerCLi, oldEnvContent, []byte(oldDockerCompose)) - dockerCLi.Close() - if err != nil { - return err - } - } - - if req.PullImage { - images, err := docker.GetImagesFromDockerCompose(content, []byte(install.DockerCompose)) - if err != nil { - return err - } - dockerCLi, err := docker.NewClient() - if err != nil { - return err - } - defer dockerCLi.Close() - for _, image := range images { - t.Log(i18n.GetWithName("PullImageStart", image)) - if pullErr := dockerCLi.PullImageWithProcess(t, image); pullErr != nil { - if exist, _ := dockerCLi.ImageExists(image); exist { - t.Log(i18n.GetMsgByKey("UseExistImage")) - continue - } - return buserr.WithNameAndErr("ErrDockerPullImage", "", pullErr) - } - exist, err := dockerCLi.ImageExists(image) - if err != nil || !exist { - return buserr.WithNameAndErr("ErrDockerPullImage", "", fmt.Errorf("image %s does not exist after pull: %v", image, err)) - } - t.LogSuccess(i18n.GetMsgByKey("PullImage")) - } - } - - if install.App.Key == constant.AppOpenresty { - modules, moduleErr := loadNginxModulesWithCatalog(install, targetNginxCatalogPath) - if moduleErr != nil { - return moduleErr - } - // Build dynamic modules for the target version before stopping the - // current container. Static modules retain the full rebuild path. - if !hasEnabledStaticNginxModules(modules) { - previousModules := cloneNginxModules(modules) - modules, moduleErr = buildDynamicNginxModules(install, modules, nil, false, "", targetNginxCatalogPath, t) - if moduleErr != nil { - return moduleErr - } - if moduleErr = saveNginxModulesWithCatalog(install, modules, targetNginxCatalogPath); moduleErr != nil { - removeNginxModuleOutputsNotReferenced(install, modules, previousModules) - return moduleErr - } - } - } - - if out, err := compose.Down(install.GetComposePath()); err != nil { - if out != "" { - upErr = errors.New(out) - return upErr - } - return err - } - envs := make(map[string]interface{}) - if err = json.Unmarshal([]byte(install.Env), &envs); err != nil { - return err - } - envParams := make(map[string]string, len(envs)) - if install.App.Key == constant.AppOpenresty { - packageUrl, _ := env.GetEnvValueByKey(install.GetEnvPath(), "CONTAINER_PACKAGE_URL") - addPackage, _ := env.GetEnvValueByKey(install.GetEnvPath(), "RESTY_ADD_PACKAGE_BUILDDEPS") - options, _ := env.GetEnvValueByKey(install.GetEnvPath(), "RESTY_CONFIG_OPTIONS_MORE") - envParams["CONTAINER_PACKAGE_URL"] = packageUrl - envParams["RESTY_ADD_PACKAGE_BUILDDEPS"] = addPackage - envParams["RESTY_CONFIG_OPTIONS_MORE"] = options - } - handleMap(envs, envParams) - if err = env.Write(envParams, install.GetEnvPath()); err != nil { - return err - } - - if err = runScript(t, &install, "upgrade"); err != nil { - return err - } - - if err = fileOp.WriteFile(install.GetComposePath(), strings.NewReader(install.DockerCompose), constant.FilePerm); err != nil { - return err - } - - if install.App.Key == constant.AppOpenresty { - if err = buildNginx(t, install, targetNginxCatalogPath); err != nil { - t.Log(err.Error()) - return err - } - } - - logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("Run"), i18n.GetMsgByKey("App")) - t.Log(logStr) - if out, err := compose.Up(install.GetComposePath()); err != nil { - if out != "" { - return errors.New(out) - } - return err - } - t.LogSuccess(logStr) - install.Status = constant.StatusRunning - if install.App.Key == constant.AppOpenresty { - if err = commitStaticNginxModuleBuilds(install, targetNginxCatalogPath, t); err != nil { - return err - } - activeCatalogPath := path.Join(install.GetPath(), nginxModuleBuildDir, nginxModuleCatalogFile) - if err = activateNginxModuleCatalogAndCommit(targetNginxCatalogPath, activeCatalogPath, func() error { - return appInstallRepo.Save(context.Background(), &install) - }); err != nil { - return err - } - } else { - if err = appInstallRepo.Save(context.Background(), &install); err != nil { - return err - } - } - if nginxUpgradeSnapshot != nil { - nginxUpgradeSnapshot.Cleanup() - nginxUpgradeSnapshot = nil - } - if req.DeleteImage { - newEnvContent, err := fileOp.GetContent(install.GetEnvPath()) - if err != nil { - t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err) - return nil - } - excludeImages, err := docker.GetImagesFromDockerCompose(newEnvContent, []byte(install.DockerCompose)) - if err != nil { - t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err) - return nil - } - dockerCLi, err := docker.NewClient() - if err != nil { - t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err) - return nil - } - defer dockerCLi.Close() - if err = deleteAppImagesByIDs(t, dockerCLi, oldImageIDs, excludeImages); err != nil { - t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err) - } - } - return nil - } - - rollBackApp := func(t *task.Task) { - if req.Backup { - t.Log(i18n.GetWithName("AppRecover", install.Name)) - recoverErr := NewIBackupService().AppRecover(dto.CommonRecover{ - Name: install.App.Key, DetailName: install.Name, Type: "app", DownloadAccountID: 1, File: backupFile, - }) - if recoverErr == nil { - if nginxUpgradeSnapshot != nil { - nginxUpgradeSnapshot.Cleanup() - nginxUpgradeSnapshot = nil - } - t.LogSuccess(i18n.GetWithName("AppRecover", install.Name)) - return - } - t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), recoverErr) - if install.App.Key != constant.AppOpenresty { - return - } - } - if install.App.Key == constant.AppOpenresty && nginxUpgradeSnapshot != nil { - if out, rollbackErr := compose.Down(install.GetComposePath()); rollbackErr != nil { - if out != "" { - rollbackErr = fmt.Errorf("%s: %w", out, rollbackErr) - } - t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), rollbackErr) - } - if rollbackErr := nginxUpgradeSnapshot.Restore(); rollbackErr != nil { - t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), rollbackErr) - return - } - nginxUpgradeSnapshot.Cleanup() - nginxUpgradeSnapshot = nil - if out, rollbackErr := compose.Up(originalInstall.GetComposePath()); rollbackErr != nil { - if out != "" { - rollbackErr = fmt.Errorf("%s: %w", out, rollbackErr) - } - t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), rollbackErr) - return - } - originalInstall.Status = constant.StatusRunning - originalInstall.Message = "" - if rollbackErr := appInstallRepo.Save(context.Background(), &originalInstall); rollbackErr != nil { - t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), rollbackErr) - return - } - install = originalInstall - t.LogSuccess(i18n.GetWithName("AppRecover", install.Name)) - return - } - if install.App.Key == constant.AppOpenresty { - if rollbackErr := appInstallRepo.Save(context.Background(), &originalInstall); rollbackErr != nil { - t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), rollbackErr) - return - } - install = originalInstall - t.LogSuccess(i18n.GetWithName("AppRecover", install.Name)) - } - } - - upgradeTimeout := 1 * time.Hour - if install.App.Key == constant.AppOpenresty { - // Dynamic modules are built serially and each Docker build has its own - // timeout. An outer deadline would start rollback while upgradeApp is - // still mutating the installation because SubTask does not stop its - // action goroutine on timeout. - upgradeTimeout = 0 - } - upgradeTask.AddSubTaskWithOps(task.GetTaskName(install.Name, task.TaskUpgrade, task.TaskScopeApp), upgradeApp, rollBackApp, 0, upgradeTimeout) - - upgradingInstall := install - if err = appInstallRepo.Save(context.Background(), &upgradingInstall); err != nil { - return err - } - go func() { - if taskErr := upgradeTask.Execute(); taskErr != nil { - existInstall, _ := appInstallRepo.GetFirst(repo.WithByID(req.InstallID)) - if existInstall.ID > 0 && existInstall.Status != constant.StatusRunning { - existInstall.Status = constant.StatusUpgradeErr - existInstall.Message = taskErr.Error() - _ = appInstallRepo.Save(context.Background(), &existInstall) - } - } - }() - - return nil -} - func skipCheckStatus(service types.ServiceConfig) bool { for key := range service.Labels { if key == "skipStatusCheck" { @@ -2233,6 +1822,10 @@ func isHostModel(dockerCompose string) bool { } func copyAppDetailMissing(fileOp files.FileOp, srcDir, dstDir string) error { + return copyAppDetailMissingTracked(fileOp, srcDir, dstDir, nil) +} + +func copyAppDetailMissingTracked(fileOp files.FileOp, srcDir, dstDir string, createdPaths *[]string) error { entries, err := os.ReadDir(srcDir) if err != nil { return err @@ -2244,6 +1837,9 @@ func copyAppDetailMissing(fileOp files.FileOp, srcDir, dstDir string) error { srcPath := path.Join(srcDir, entry.Name()) dstPath := path.Join(dstDir, entry.Name()) if !fileOp.Stat(dstPath) { + if createdPaths != nil { + *createdPaths = append(*createdPaths, dstPath) + } if entry.IsDir() { if err := fileOp.CopyDir(srcPath, dstDir); err != nil { return err @@ -2258,7 +1854,7 @@ func copyAppDetailMissing(fileOp files.FileOp, srcDir, dstDir string) error { if !entry.IsDir() { continue } - if err := copyAppDetailMissing(fileOp, srcPath, dstPath); err != nil { + if err := copyAppDetailMissingTracked(fileOp, srcPath, dstPath, createdPaths); err != nil { return err } } diff --git a/agent/app/service/backup_app.go b/agent/app/service/backup_app.go index 33ca43134..8501eb284 100644 --- a/agent/app/service/backup_app.go +++ b/agent/app/service/backup_app.go @@ -90,6 +90,34 @@ func (u *BackupService) AppBackup(req dto.CommonBackup) (*model.BackupRecord, er return record, nil } +func backupAppWithParentTask(install *model.AppInstall, parentTask *task.Task, fileName string) (*model.BackupRecord, error) { + itemDir := fmt.Sprintf("app/%s/%s", install.App.Key, install.Name) + backupDir := path.Join(global.Dir.LocalBackupDir, itemDir) + record := &model.BackupRecord{ + Type: "app", + Name: install.App.Key, + DetailName: install.Name, + SourceAccountIDs: "1", + DownloadAccountID: 1, + FileDir: itemDir, + FileName: fileName, + TaskID: parentTask.TaskID, + Status: constant.StatusWaiting, + } + if err := backupRepo.CreateRecord(record); err != nil { + return nil, err + } + if err := handleAppBackup(install, parentTask, record.ID, backupDir, fileName, "", "", parentTask.TaskID); err != nil { + markBackupFailed(record.ID, err) + record.Status = constant.StatusFailed + record.Message = err.Error() + return record, err + } + backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusSuccess}) + record.Status = constant.StatusSuccess + return record, nil +} + func (u *BackupService) AppRecover(req dto.CommonRecover) error { app, err := appRepo.GetFirst(appRepo.WithKey(req.Name)) if err != nil { @@ -203,7 +231,11 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF return err } defer func() { - _, _ = compose.Up(install.GetComposePath()) + if isRollback { + _, _ = compose.UpWithoutPull(install.GetComposePath()) + } else { + _, _ = compose.Up(install.GetComposePath()) + } _ = os.RemoveAll(strings.ReplaceAll(recoverFile, ".tar.gz", "")) }() diff --git a/agent/app/service/container_update.go b/agent/app/service/container_update.go index 3d268de68..a5afe953a 100644 --- a/agent/app/service/container_update.go +++ b/agent/app/service/container_update.go @@ -170,6 +170,10 @@ type containerSwitchClient interface { NetworkDisconnect(context.Context, string, string, bool) error } +type containerInspectClient interface { + ContainerInspect(context.Context, string) (container.InspectResponse, error) +} + type containerOperationMutex struct { mutex sync.Mutex locks map[string]*containerOperationLockEntry @@ -335,7 +339,7 @@ const ( containerHealthCheckMaxWait = 10 * time.Minute ) -func waitContainerReady(ctx context.Context, cli containerSwitchClient, containerID string) error { +func waitContainerReady(ctx context.Context, cli containerInspectClient, containerID string) error { info, err := cli.ContainerInspect(ctx, containerID) if err != nil { return err @@ -347,14 +351,15 @@ func waitContainerReady(ctx context.Context, cli containerSwitchClient, containe return waitContainerStable(ctx, cli, containerID, info) } + initialRestartCount := info.RestartCount timeout := containerHealthCheckTimeout(info.Config) deadline := time.NewTimer(timeout) ticker := time.NewTicker(time.Second) defer deadline.Stop() defer ticker.Stop() for { - if info.State.Restarting || info.RestartCount != 0 { - return fmt.Errorf("container restarted %d times during startup", info.RestartCount) + if info.State.Restarting || info.RestartCount != initialRestartCount { + return fmt.Errorf("container restart count changed from %d to %d during startup", initialRestartCount, info.RestartCount) } if info.State.Health == nil { return fmt.Errorf("container health status is unavailable") @@ -382,9 +387,10 @@ func waitContainerReady(ctx context.Context, cli containerSwitchClient, containe } } -func waitContainerStable(ctx context.Context, cli containerSwitchClient, containerID string, initial container.InspectResponse) error { +func waitContainerStable(ctx context.Context, cli containerInspectClient, containerID string, initial container.InspectResponse) error { startedAt := initial.State.StartedAt - if err := checkContainerStableState(initial, startedAt); err != nil { + restartCount := initial.RestartCount + if err := checkContainerStableState(initial, startedAt, restartCount); err != nil { return err } deadline := time.NewTimer(containerStartStabilization) @@ -400,25 +406,25 @@ func waitContainerStable(ctx context.Context, cli containerSwitchClient, contain if err != nil { return err } - return checkContainerStableState(info, startedAt) + return checkContainerStableState(info, startedAt, restartCount) case <-ticker.C: info, err := cli.ContainerInspect(ctx, containerID) if err != nil { return err } - if err := checkContainerStableState(info, startedAt); err != nil { + if err := checkContainerStableState(info, startedAt, restartCount); err != nil { return err } } } } -func checkContainerStableState(info container.InspectResponse, startedAt string) error { +func checkContainerStableState(info container.InspectResponse, startedAt string, restartCount int) error { if err := checkContainerRunningState(info); err != nil { return err } - if info.State.Restarting || info.RestartCount != 0 { - return fmt.Errorf("container restarted %d times during startup", info.RestartCount) + if info.State.Restarting || info.RestartCount != restartCount { + return fmt.Errorf("container restart count changed from %d to %d during startup", restartCount, info.RestartCount) } if startedAt != "" && info.State.StartedAt != startedAt { return fmt.Errorf("container start time changed during startup") diff --git a/agent/app/service/nginx_module.go b/agent/app/service/nginx_module.go index 3e3d7a851..d299cb6b0 100644 --- a/agent/app/service/nginx_module.go +++ b/agent/app/service/nginx_module.go @@ -515,6 +515,7 @@ type openrestyUpgradeSnapshot struct { var openrestyUpgradeSnapshotPaths = []string{ nginxModuleBuildDir, + nginxModuleModulesDir, "scripts", path.Join(nginxModuleConfDir, nginxModuleEnabledConfDir), path.Join(nginxModuleConfDir, "nginx.conf"), diff --git a/agent/i18n/lang/en.yaml b/agent/i18n/lang/en.yaml index c1f5b1bd1..6a9b5d0d7 100644 --- a/agent/i18n/lang/en.yaml +++ b/agent/i18n/lang/en.yaml @@ -154,6 +154,11 @@ ErrAppVersionDeprecated: "The {{ .name }} application is not compatible with the ErrDockerFailed: 'Docker is abnormal; check service status' ErrDockerComposeCmdNotFound: 'Docker Compose not found on host' UseExistImage: 'Image exists; using existing image' +UpgradePrepare: 'Prepare application upgrade' +UpgradeStop: 'Stop original application' +UpgradeWaitReady: 'Wait for application readiness' +UpgradeBackupDisabled: 'Upgrade backup is disabled; data changes made by upgrade scripts cannot be fully rolled back' +UpgradeRollbackFailed: 'Rollback failed' ErrDatabaseNotFound: 'Database not found: {{ .name }}' # SSH diff --git a/agent/i18n/lang/es-ES.yaml b/agent/i18n/lang/es-ES.yaml index 6b68ba3f5..8e7e2ff10 100644 --- a/agent/i18n/lang/es-ES.yaml +++ b/agent/i18n/lang/es-ES.yaml @@ -154,6 +154,11 @@ ErrAppVersionDeprecated: 'La aplicación {{ .name }} no es compatible con la ver ErrDockerFailed: 'El estado de Docker es anómalo, revise el servicio' ErrDockerComposeCmdNotFound: 'El comando Docker Compose no existe instálelo primero en el host' UseExistImage: 'La imagen ya existe, usando imagen existente' +UpgradePrepare: 'Preparar actualización de la aplicación' +UpgradeStop: 'Detener la aplicación original' +UpgradeWaitReady: 'Esperar a que la aplicación esté lista' +UpgradeBackupDisabled: 'La copia de actualización está desactivada; los cambios de datos de los scripts no se pueden revertir por completo' +UpgradeRollbackFailed: 'Error al revertir' ErrDatabaseNotFound: 'La base de datos {{ .name }} no existe' # SSH diff --git a/agent/i18n/lang/fa.yaml b/agent/i18n/lang/fa.yaml index c61e8bca4..7eff1108b 100644 --- a/agent/i18n/lang/fa.yaml +++ b/agent/i18n/lang/fa.yaml @@ -154,6 +154,11 @@ ErrAppVersionDeprecated: "برنامه {{ .name }} با نسخه فعلی 1Panel ErrDockerFailed: 'Docker غیرعادی است؛ وضعیت سرویس را بررسی کنید' ErrDockerComposeCmdNotFound: 'Docker Compose روی میزبان یافت نشد' UseExistImage: 'تصویر وجود دارد؛ استفاده از تصویر موجود' +UpgradePrepare: 'آماده‌سازی ارتقاء برنامه' +UpgradeStop: 'توقف برنامه اصلی' +UpgradeWaitReady: 'انتظار برای آماده شدن برنامه' +UpgradeBackupDisabled: 'پشتیبان ارتقاء غیرفعال است؛ تغییرات داده اسکریپت‌های ارتقاء کاملاً قابل بازگشت نیست' +UpgradeRollbackFailed: 'بازگشت ناموفق بود' ErrDatabaseNotFound: 'پایگاه داده یافت نشد: {{ .name }}' # SSH diff --git a/agent/i18n/lang/ja.yaml b/agent/i18n/lang/ja.yaml index 98d80432d..cd065c66f 100644 --- a/agent/i18n/lang/ja.yaml +++ b/agent/i18n/lang/ja.yaml @@ -154,6 +154,11 @@ ErrAppVersionDeprecated: '{{ .name }} アプリケーションは現在の 1Pane ErrDockerFailed: 'Docker の状態が異常です。サービス状態を確認してください' ErrDockerComposeCmdNotFound: 'Docker Compose コマンドは存在しません。ホストマシンにこのコマンドを先にインストールしてください' UseExistImage: 'イメージは既に存在します。既存のイメージを使用します' +UpgradePrepare: 'アプリケーションのアップグレードを準備' +UpgradeStop: '元のアプリケーションを停止' +UpgradeWaitReady: 'アプリケーションの準備完了を待機' +UpgradeBackupDisabled: 'アップグレードバックアップが無効なため、アップグレードスクリプトによるデータ変更を完全にはロールバックできません' +UpgradeRollbackFailed: 'ロールバックに失敗しました' ErrDatabaseNotFound: 'データベース {{ .name }} は存在しません' # SSH diff --git a/agent/i18n/lang/ko.yaml b/agent/i18n/lang/ko.yaml index 17a222561..ffe451774 100644 --- a/agent/i18n/lang/ko.yaml +++ b/agent/i18n/lang/ko.yaml @@ -154,6 +154,11 @@ ErrAppVersionDeprecated: '{{ .name }} 응용 프로그램은 현재 1Panel 버 ErrDockerFailed: 'Docker의 상태가 비정상입니다. 서비스 상태를 확인하세요' ErrDockerComposeCmdNotFound: 'Docker Compose 명령이 없습니다. 호스트 머신에 먼저 이 명령을 설치하세요' UseExistImage: '이미지가 이미 존재하여 기존 이미지를 사용합니다' +UpgradePrepare: '애플리케이션 업그레이드 준비' +UpgradeStop: '기존 애플리케이션 중지' +UpgradeWaitReady: '애플리케이션 준비 상태 대기' +UpgradeBackupDisabled: '업그레이드 백업이 비활성화되어 업그레이드 스크립트의 데이터 변경을 완전히 롤백할 수 없습니다' +UpgradeRollbackFailed: '롤백 실패' ErrDatabaseNotFound: '데이터베이스 {{ .name }} 이(가) 존재하지 않습니다' # SSH diff --git a/agent/i18n/lang/lo.yaml b/agent/i18n/lang/lo.yaml index f75442dd7..ee55a1efe 100644 --- a/agent/i18n/lang/lo.yaml +++ b/agent/i18n/lang/lo.yaml @@ -144,6 +144,11 @@ ErrAppVersionDeprecated: "ແອັບພລິເຄຊັນ {{ .name }} ບ ErrDockerFailed: 'Docker ຜິດປົກກະຕິ; ກະລຸນາກວດສອບສະຖານະການບໍລິການ' ErrDockerComposeCmdNotFound: 'ບໍ່ພົບຄຳສັ່ງ Docker Compose ໃນເຄື່ອງ' UseExistImage: 'ມີຮູບພາບຢູ່ແລ້ວ; ກຳລັງໃຊ້ຮູບພາບທີ່ມີຢູ່' +UpgradePrepare: 'ກະກຽມອັບເກຣດແອັບ' +UpgradeStop: 'ຢຸດແອັບເດີມ' +UpgradeWaitReady: 'ລໍຖ້າແອັບພ້ອມ' +UpgradeBackupDisabled: 'ປິດການສຳຮອງກ່ອນອັບເກຣດ; ການປ່ຽນແປງຂໍ້ມູນຈາກສະຄຣິບບໍ່ສາມາດຍ້ອນກັບໄດ້ທັງໝົດ' +UpgradeRollbackFailed: 'ຍ້ອນກັບລົ້ມເຫຼວ' ErrDatabaseNotFound: 'ບໍ່ພົບຖານຂໍ້ມູນ: {{ .name }}' #ssh diff --git a/agent/i18n/lang/ms.yaml b/agent/i18n/lang/ms.yaml index 7e58549ac..b9b0e8ac2 100644 --- a/agent/i18n/lang/ms.yaml +++ b/agent/i18n/lang/ms.yaml @@ -154,6 +154,11 @@ ErrAppVersionDeprecated: 'Aplikasi {{ .name }} tidak sesuai dengan versi 1Panel ErrDockerFailed: 'Keadaan Docker tidak normal, sila periksa status perkhidmatan' ErrDockerComposeCmdNotFound: 'Perintah Docker Compose tidak wujud, sila pasang perintah ini di mesin tuan terlebih dahulu' UseExistImage: 'Imej sudah wujud, menggunakan imej sedia ada' +UpgradePrepare: 'Sediakan naik taraf aplikasi' +UpgradeStop: 'Hentikan aplikasi asal' +UpgradeWaitReady: 'Tunggu aplikasi sedia' +UpgradeBackupDisabled: 'Sandaran naik taraf dilumpuhkan; perubahan data oleh skrip naik taraf tidak dapat diundur sepenuhnya' +UpgradeRollbackFailed: 'Undur gagal' ErrDatabaseNotFound: 'Pangkalan data {{ .name }} tidak wujud' # SSH diff --git a/agent/i18n/lang/pt-BR.yaml b/agent/i18n/lang/pt-BR.yaml index 76b645373..15ef25846 100644 --- a/agent/i18n/lang/pt-BR.yaml +++ b/agent/i18n/lang/pt-BR.yaml @@ -154,6 +154,11 @@ ErrAppVersionDeprecated: 'O aplicativo {{ .name }} não é compatível com a ver ErrDockerFailed: 'O estado do Docker está anormal, verifique o status do serviço' ErrDockerComposeCmdNotFound: 'O comando Docker Compose não existe, instale este comando na máquina host primeiro' UseExistImage: 'A imagem já existe, usando imagem existente' +UpgradePrepare: 'Preparar atualização do aplicativo' +UpgradeStop: 'Parar aplicativo original' +UpgradeWaitReady: 'Aguardar o aplicativo ficar pronto' +UpgradeBackupDisabled: 'O backup de atualização está desativado; alterações de dados feitas pelos scripts não podem ser totalmente revertidas' +UpgradeRollbackFailed: 'Falha na reversão' ErrDatabaseNotFound: 'O banco de dados {{ .name }} não existe' # SSH diff --git a/agent/i18n/lang/ru.yaml b/agent/i18n/lang/ru.yaml index b24ec2617..18acd561f 100644 --- a/agent/i18n/lang/ru.yaml +++ b/agent/i18n/lang/ru.yaml @@ -154,6 +154,11 @@ ErrAppVersionDeprecated: 'Приложение {{ .name }} несовмести ErrDockerFailed: 'Состояние Docker аномально, проверьте состояние сервиса' ErrDockerComposeCmdNotFound: 'Команда Docker Compose отсутствует, пожалуйста, установите эту команду на хост-машине сначала' UseExistImage: 'Образ уже существует, используется существующий образ' +UpgradePrepare: 'Подготовка обновления приложения' +UpgradeStop: 'Остановка исходного приложения' +UpgradeWaitReady: 'Ожидание готовности приложения' +UpgradeBackupDisabled: 'Резервная копия обновления отключена; изменения данных из сценариев обновления нельзя полностью откатить' +UpgradeRollbackFailed: 'Ошибка отката' ErrDatabaseNotFound: 'База данных {{ .name }} не существует' # SSH diff --git a/agent/i18n/lang/tr.yaml b/agent/i18n/lang/tr.yaml index f1b75cbad..0ddbe67df 100644 --- a/agent/i18n/lang/tr.yaml +++ b/agent/i18n/lang/tr.yaml @@ -154,6 +154,11 @@ ErrAppVersionDeprecated: '{{ .name }} uygulaması mevcut 1Panel sürümü ile uy ErrDockerFailed: 'Docker durumu anormal, lütfen servis durumunu kontrol edin' ErrDockerComposeCmdNotFound: 'Docker Compose komutu mevcut değil, lütfen önce bu komutu host makinesine yükleyin' UseExistImage: 'Görüntü zaten mevcut, mevcut görüntü kullanılıyor' +UpgradePrepare: 'Uygulama yükseltmesini hazırla' +UpgradeStop: 'Özgün uygulamayı durdur' +UpgradeWaitReady: 'Uygulamanın hazır olmasını bekle' +UpgradeBackupDisabled: 'Yükseltme yedeği devre dışı; yükseltme betiklerinin veri değişiklikleri tamamen geri alınamaz' +UpgradeRollbackFailed: 'Geri alma başarısız' ErrDatabaseNotFound: '{{ .name }} veritabanı mevcut değil' # SSH diff --git a/agent/i18n/lang/zh-Hant.yaml b/agent/i18n/lang/zh-Hant.yaml index 82f97470e..2a1744166 100644 --- a/agent/i18n/lang/zh-Hant.yaml +++ b/agent/i18n/lang/zh-Hant.yaml @@ -154,6 +154,11 @@ ErrAppVersionDeprecated: '{{ .name }} 應用不適配目前 1Panel 版本,跳 ErrDockerFailed: 'Docker 狀態異常,請檢查服務狀態' ErrDockerComposeCmdNotFound: 'Docker Compose 指令不存在,請先在宿主機安裝此指令' UseExistImage: '映像已存在,使用現有映像' +UpgradePrepare: '準備應用程式升級' +UpgradeStop: '停止原應用程式' +UpgradeWaitReady: '等待應用程式就緒' +UpgradeBackupDisabled: '未啟用升級備份,升級腳本產生的資料變更無法完整回滾' +UpgradeRollbackFailed: '回滾失敗' ErrDatabaseNotFound: '資料庫 {{ .name }} 不存在' # SSH diff --git a/agent/i18n/lang/zh.yaml b/agent/i18n/lang/zh.yaml index 3030df83e..e13cc99e7 100644 --- a/agent/i18n/lang/zh.yaml +++ b/agent/i18n/lang/zh.yaml @@ -154,6 +154,11 @@ ErrAppVersionDeprecated: "{{ .name }} 应用不适配当前 1Panel 版本,跳 ErrDockerFailed: "Docker 状态异常,请检查服务状态" ErrDockerComposeCmdNotFound: "Docker Compose 命令不存在,请先在宿主机安装" UseExistImage: "镜像已存在,使用存量镜像" +UpgradePrepare: "准备应用升级" +UpgradeStop: "停止原应用" +UpgradeWaitReady: "等待应用就绪" +UpgradeBackupDisabled: "未启用升级备份,升级脚本产生的数据变更无法完整回滚" +UpgradeRollbackFailed: "回滚失败" ErrDatabaseNotFound: "数据库 {{ .name }} 不存在" # SSH diff --git a/agent/utils/compose/compose.go b/agent/utils/compose/compose.go index 5548eca34..7c4ab7c26 100644 --- a/agent/utils/compose/compose.go +++ b/agent/utils/compose/compose.go @@ -43,21 +43,51 @@ func Up(filePath string) (string, error) { return "", err } base, extra := getComposeBaseCmd() - args := append(extra, loadFiles(filePath)...) - args = append(args, "up", "-d") + args := append(extra, upArgs(filePath, false)...) return cmd.NewCommandMgr(cmd.WithTimeout(20*time.Minute)).RunWithStdout(base, args...) } +func UpWithoutPull(filePath string) (string, error) { + if err := checkCmd(); err != nil { + return "", err + } + base, extra := getComposeBaseCmd() + args := append(extra, upArgs(filePath, true)...) + return cmd.NewCommandMgr(cmd.WithTimeout(20*time.Minute)).RunWithStdout(base, args...) +} + +func upArgs(filePath string, withoutPull bool) []string { + args := loadFiles(filePath) + args = append(args, "up", "-d") + if withoutPull { + args = append(args, "--pull", "never", "--no-build") + } + return args +} + func UpWithTask(filePath string, task *task.Task, forcePull bool) error { if err := PullComposeImages(filePath, forcePull, task); err != nil { return err } base, extra := getComposeBaseCmd() - args := append(extra, loadFiles(filePath)...) - args = append(args, "up", "-d") + args := append(extra, upArgs(filePath, false)...) return cmd.NewCommandMgr(cmd.WithTask(*task), cmd.WithTimeout(20*time.Minute)).Run(base, args...) } +func BuildWithTask(filePath, projectName string, task *task.Task) error { + if err := checkCmd(); err != nil { + return err + } + base, extra := getComposeBaseCmd() + args := append([]string(nil), extra...) + if projectName != "" { + args = append(args, "--project-name", projectName) + } + args = append(args, loadFiles(filePath)...) + args = append(args, "build") + return cmd.NewCommandMgr(cmd.WithTask(*task), cmd.WithTimeout(120*time.Minute)).Run(base, args...) +} + func PullComposeImages(filePath string, forcePull bool, task *task.Task) error { return pullComposeImages(filePath, forcePull, task) } diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index f1fae8696..9e6d7a3ca 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -1013,6 +1013,9 @@ const message = { performanceSetting: 'Performance Settings', logSetting: 'Log Settings', lbPolicy: 'Load Balancing Policy', + protocolConversion: 'Protocol Conversion', + protocolConversionDesc: + 'Allow supported direct conversion between Chat Completions, Responses, and Anthropic Messages. Native protocols always take priority.', lbPolicyRoundRobin: 'Round Robin', lbPolicyRoundRobinDesc: 'Select available model accounts in order. Use this when backends have similar capacity.', @@ -1045,11 +1048,9 @@ const message = { requestBody: 'Request Body', requestBodySize: 'Request Body Size', requestBodyTruncated: 'Truncated', - requestBodyHash: 'Request Body Hash', responseBody: 'Response Body', responseBodySize: 'Response Body Size', responseBodyTruncated: 'Response Body Truncated', - responseBodyHash: 'Response Body Hash', readableContent: 'Readable Content', rawResponse: 'Raw Response', replyContent: 'Reply Content', @@ -1095,7 +1096,10 @@ const message = { usageKeywordPlaceholder: 'Search Request ID / model / upstream model / error message', attemptChain: 'Attempt Chain', attemptIndex: 'Call Order', - noAttemptDetails: 'This request hit once; no fallback details', + clientProtocol: 'Client API', + upstreamProtocol: 'Upstream API', + conversionPath: 'Conversion Path', + noAttemptDetails: 'No attempt chain records', input: 'Input', output: 'Output', total: 'Total', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index 30a1d995a..c1ff8f208 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -1023,6 +1023,9 @@ const message = { performanceSetting: 'Configuración de rendimiento', logSetting: 'Configuración de registros', lbPolicy: 'Estrategia de balanceo', + protocolConversion: 'Conversión de protocolo', + protocolConversionDesc: + 'Permite la conversión directa compatible entre Chat Completions, Responses y Anthropic Messages. Los protocolos nativos siempre tienen prioridad.', lbPolicyRoundRobin: 'Round Robin', lbPolicyRoundRobinDesc: 'Selecciona las cuentas de modelo disponibles en orden. Úselo cuando los backends tengan capacidad similar.', @@ -1055,11 +1058,9 @@ const message = { requestBody: 'Cuerpo de solicitud', requestBodySize: 'Tamaño del cuerpo de solicitud', requestBodyTruncated: 'Truncado', - requestBodyHash: 'Hash del cuerpo de solicitud', responseBody: 'Cuerpo de respuesta', responseBodySize: 'Tamaño del cuerpo de respuesta', responseBodyTruncated: 'Cuerpo de respuesta truncado', - responseBodyHash: 'Hash del cuerpo de respuesta', readableContent: 'Contenido legible', rawResponse: 'Respuesta original', replyContent: 'Contenido de respuesta', @@ -1105,7 +1106,10 @@ const message = { usageKeywordPlaceholder: 'Buscar Request ID / modelo / modelo upstream / mensaje de error', attemptChain: 'Cadena de intentos', attemptIndex: 'Orden de llamada', - noAttemptDetails: 'Esta solicitud acertó en el primer intento; no hay detalles de fallback', + clientProtocol: 'API del cliente', + upstreamProtocol: 'API upstream', + conversionPath: 'Ruta de conversión', + noAttemptDetails: 'No hay registros de la cadena de intentos', input: 'Entrada', output: 'Salida', total: 'Total', diff --git a/frontend/src/lang/modules/fa.ts b/frontend/src/lang/modules/fa.ts index b5719a420..b66b112fe 100644 --- a/frontend/src/lang/modules/fa.ts +++ b/frontend/src/lang/modules/fa.ts @@ -997,6 +997,9 @@ const message = { performanceSetting: 'تنظیمات عملکرد', logSetting: 'تنظیمات لاگ', lbPolicy: 'خط مشی توزیع بار', + protocolConversion: 'تبدیل پروتکل', + protocolConversionDesc: + 'تبدیل مستقیم پشتیبانی‌شده بین Chat Completions، Responses و Anthropic Messages را فعال می‌کند. پروتکل بومی همیشه اولویت دارد.', lbPolicyRoundRobin: 'چرخه‌ای', lbPolicyRoundRobinDesc: 'حساب‌های مدل موجود را به ترتیب انتخاب کنید. زمانی استفاده شود که بک‌اندها ظرفیت مشابهی دارند.', @@ -1029,11 +1032,9 @@ const message = { requestBody: 'بدنه درخواست', requestBodySize: 'اندازه بدنه درخواست', requestBodyTruncated: 'کوتاه شده', - requestBodyHash: 'هش بدنه درخواست', responseBody: 'بدنه پاسخ', responseBodySize: 'اندازه بدنه پاسخ', responseBodyTruncated: 'کوتاه شده', - responseBodyHash: 'هش بدنه پاسخ', readableContent: 'محتوا قابل خواندن', rawResponse: 'پاسخ خام', replyContent: 'محتوا پاسخ', @@ -1079,7 +1080,10 @@ const message = { usageKeywordPlaceholder: 'جستجوی شناسه درخواست / مدل / مدل بالادست / پیام خطا', attemptChain: 'زنجیره تلاش', attemptIndex: 'ترتیب فراخوانی', - noAttemptDetails: 'این درخواست یک بار انجام شده است؛ جزئیات خرابی وجود ندارد', + clientProtocol: 'API کاربر', + upstreamProtocol: 'API بالادست', + conversionPath: 'مسیر تبدیل', + noAttemptDetails: 'هیچ سابقه‌ای از زنجیره تلاش وجود ندارد', input: 'ورودی', output: 'خروجی', total: 'مجموع', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 7ca51217a..316dde138 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -1010,6 +1010,9 @@ const message = { performanceSetting: 'パフォーマンス設定', logSetting: 'ログ設定', lbPolicy: '負荷分散ポリシー', + protocolConversion: 'プロトコル変換', + protocolConversionDesc: + 'Chat Completions、Responses、Anthropic Messages 間でサポートされる直接変換を有効にします。ネイティブプロトコルが常に優先されます。', lbPolicyRoundRobin: 'ラウンドロビン', lbPolicyRoundRobinDesc: '利用可能なモデルアカウントを順番に選択します。バックエンドの性能が近い場合に適しています。', @@ -1042,11 +1045,9 @@ const message = { requestBody: 'リクエスト本文', requestBodySize: 'リクエスト本文サイズ', requestBodyTruncated: '切り詰め', - requestBodyHash: 'リクエスト本文 Hash', responseBody: 'レスポンス本文', responseBodySize: 'レスポンス本文サイズ', responseBodyTruncated: 'レスポンス本文の切り詰め', - responseBodyHash: 'レスポンス本文 Hash', readableContent: '読みやすい内容', rawResponse: '生レスポンス', replyContent: '返信内容', @@ -1092,7 +1093,10 @@ const message = { usageKeywordPlaceholder: 'Request ID / モデル / 上流モデル / エラーメッセージを検索', attemptChain: '試行チェーン', attemptIndex: '呼び出し順', - noAttemptDetails: 'このリクエストは 1 回で命中しました。fallback 明細はありません', + clientProtocol: 'クライアント API', + upstreamProtocol: 'アップストリーム API', + conversionPath: '変換パス', + noAttemptDetails: '呼び出しチェーンの記録はありません', input: '入力', output: '出力', total: '合計', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index 2284fe18b..05706b6b6 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -994,6 +994,9 @@ const message = { performanceSetting: '성능 설정', logSetting: '로그 설정', lbPolicy: '부하 분산 정책', + protocolConversion: '프로토콜 변환', + protocolConversionDesc: + 'Chat Completions, Responses 및 Anthropic Messages 간에 지원되는 직접 변환을 허용합니다. 네이티브 프로토콜이 항상 우선합니다.', lbPolicyRoundRobin: '라운드 로빈', lbPolicyRoundRobinDesc: '사용 가능한 모델 계정을 순서대로 선택합니다. 백엔드 성능이 비슷한 경우에 적합합니다.', @@ -1026,11 +1029,9 @@ const message = { requestBody: '요청 본문', requestBodySize: '요청 본문 크기', requestBodyTruncated: '잘림 여부', - requestBodyHash: '요청 본문 Hash', responseBody: '응답 본문', responseBodySize: '응답 본문 크기', responseBodyTruncated: '응답 본문 잘림 여부', - responseBodyHash: '응답 본문 Hash', readableContent: '읽기 쉬운 내용', rawResponse: '원본 응답', replyContent: '응답 내용', @@ -1076,7 +1077,10 @@ const message = { usageKeywordPlaceholder: 'Request ID / 모델 / 업스트림 모델 / 오류 메시지 검색', attemptChain: '시도 체인', attemptIndex: '호출 순서', - noAttemptDetails: '이 요청은 한 번에 적중했으며 fallback 상세가 없습니다', + clientProtocol: '클라이언트 API', + upstreamProtocol: '업스트림 API', + conversionPath: '변환 경로', + noAttemptDetails: '호출 체인 기록이 없습니다', input: '입력', output: '출력', total: '합계', diff --git a/frontend/src/lang/modules/lo.ts b/frontend/src/lang/modules/lo.ts index 5ed6abdfa..2114a9baa 100644 --- a/frontend/src/lang/modules/lo.ts +++ b/frontend/src/lang/modules/lo.ts @@ -1004,6 +1004,9 @@ const message = { performanceSetting: 'ຕັ້ງຄ່າປະສິດທິພາບ', logSetting: 'ຕັ້ງຄ່າລັອກ', lbPolicy: 'ນະໂຍບາຍການກະຈາຍພາລະ (Load Balancing)', + protocolConversion: 'ການປ່ຽນໂປຣໂຕຄອນ', + protocolConversionDesc: + 'ອະນຸຍາດການປ່ຽນໂດຍກົງທີ່ຮອງຮັບລະຫວ່າງ Chat Completions, Responses ແລະ Anthropic Messages. ໂປຣໂຕຄອນເດີມຈະຖືກໃຫ້ສິດກ່ອນສະເໝີ.', lbPolicyRoundRobin: 'Round Robin', lbPolicyRoundRobinDesc: 'ເລືອກບັນຊີໂມເດວຕາມລຳດັບ. ໃຊ້ເມື່ອລະບົບຫຼັງບ້ານມີຄວາມສາມາດໃກ້ຄຽງກັນ.', lbPolicyWeightedRoundRobin: 'Weighted Round Robin', @@ -1035,11 +1038,9 @@ const message = { requestBody: 'Request Body', requestBodySize: 'ຂະໜາດ Request Body', requestBodyTruncated: 'ຖືກຕັດບາງສ່ວນ', - requestBodyHash: 'Hash ຂອງ Request Body', responseBody: 'Response Body', responseBodySize: 'ຂະໜາດ Response Body', responseBodyTruncated: 'Response Body ຖືກຕັດບາງສ່ວນ', - responseBodyHash: 'Hash ຂອງ Response Body', readableContent: 'ເນື້ອໃນທີ່ອ່ານໄດ້', rawResponse: 'Response ດິບ', replyContent: 'ເນື້ອໃນການຕອບກັບ', @@ -1085,7 +1086,10 @@ const message = { usageKeywordPlaceholder: 'ຄົ້ນຫາ Request ID / ໂມເດວ / ໂມເດວຕົ້ນທາງ / ຂໍ້ຄວາມຜິດພາດ', attemptChain: 'ລຳດັບການລອງໃໝ່', attemptIndex: 'ລຳດັບການເອີ້ນ', - noAttemptDetails: 'ຄຳຂໍນີ້ສຳເລັດໃນຄັ້ງດຽວ; ບໍ່ມີລາຍລະອຽດການສຳຮອງ', + clientProtocol: 'API ລູກຄ້າ', + upstreamProtocol: 'API ຕົ້ນທາງ', + conversionPath: 'ເສັ້ນທາງການປ່ຽນ', + noAttemptDetails: 'ບໍ່ມີບັນທຶກລຳດັບການລອງ', input: 'ຂາເຂົ້າ', output: 'ຂາອອກ', total: 'ທັງໝົດ', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 6925ec1e1..d5d8318cc 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -1020,6 +1020,9 @@ const message = { performanceSetting: 'Tetapan Prestasi', logSetting: 'Tetapan Log', lbPolicy: 'Strategi imbangan beban', + protocolConversion: 'Penukaran protokol', + protocolConversionDesc: + 'Benarkan penukaran terus yang disokong antara Chat Completions, Responses dan Anthropic Messages. Protokol asli sentiasa diutamakan.', lbPolicyRoundRobin: 'Round Robin', lbPolicyRoundRobinDesc: 'Pilih akaun model tersedia mengikut turutan. Sesuai apabila kapasiti backend hampir sama.', @@ -1052,11 +1055,9 @@ const message = { requestBody: 'Badan Permintaan', requestBodySize: 'Saiz Badan Permintaan', requestBodyTruncated: 'Dipotong', - requestBodyHash: 'Hash Badan Permintaan', responseBody: 'Badan Respons', responseBodySize: 'Saiz Badan Respons', responseBodyTruncated: 'Badan Respons Dipotong', - responseBodyHash: 'Hash Badan Respons', readableContent: 'Kandungan Boleh Dibaca', rawResponse: 'Respons Asal', replyContent: 'Kandungan Balasan', @@ -1102,7 +1103,10 @@ const message = { usageKeywordPlaceholder: 'Cari Request ID / model / model upstream / mesej ralat', attemptChain: 'Rantaian percubaan', attemptIndex: 'Urutan panggilan', - noAttemptDetails: 'Permintaan ini berjaya sekali percubaan; tiada butiran fallback', + clientProtocol: 'API klien', + upstreamProtocol: 'API upstream', + conversionPath: 'Laluan penukaran', + noAttemptDetails: 'Tiada rekod rantaian percubaan', input: 'Input', output: 'Output', total: 'Jumlah', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 60bbe4c71..195caeaac 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -1016,6 +1016,9 @@ const message = { performanceSetting: 'Configurações de desempenho', logSetting: 'Configurações de log', lbPolicy: 'Política de balanceamento', + protocolConversion: 'Conversão de protocolo', + protocolConversionDesc: + 'Permite conversão direta compatível entre Chat Completions, Responses e Anthropic Messages. Protocolos nativos sempre têm prioridade.', lbPolicyRoundRobin: 'Round Robin', lbPolicyRoundRobinDesc: 'Seleciona contas de modelo disponíveis em ordem. Use quando os backends tiverem capacidade semelhante.', @@ -1048,11 +1051,9 @@ const message = { requestBody: 'Corpo da requisição', requestBodySize: 'Tamanho do corpo da requisição', requestBodyTruncated: 'Truncado', - requestBodyHash: 'Hash do corpo da requisição', responseBody: 'Corpo da resposta', responseBodySize: 'Tamanho do corpo da resposta', responseBodyTruncated: 'Corpo da resposta truncado', - responseBodyHash: 'Hash do corpo da resposta', readableContent: 'Conteúdo legível', rawResponse: 'Resposta bruta', replyContent: 'Conteúdo da resposta', @@ -1098,7 +1099,10 @@ const message = { usageKeywordPlaceholder: 'Pesquisar Request ID / modelo / modelo upstream / mensagem de erro', attemptChain: 'Cadeia de tentativas', attemptIndex: 'Ordem de chamada', - noAttemptDetails: 'Esta requisição acertou na primeira tentativa; sem detalhes de fallback', + clientProtocol: 'API do cliente', + upstreamProtocol: 'API upstream', + conversionPath: 'Caminho de conversão', + noAttemptDetails: 'Nenhum registro da cadeia de tentativas', input: 'Entrada', output: 'Saída', total: 'Total', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index 7e52bce1d..f73df6df0 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -1009,6 +1009,9 @@ const message = { performanceSetting: 'Настройки производительности', logSetting: 'Настройки журналов', lbPolicy: 'Политика балансировки', + protocolConversion: 'Преобразование протокола', + protocolConversionDesc: + 'Разрешает поддерживаемое прямое преобразование между Chat Completions, Responses и Anthropic Messages. Нативный протокол всегда имеет приоритет.', lbPolicyRoundRobin: 'Round Robin', lbPolicyRoundRobinDesc: 'Выбирает доступные аккаунты моделей по очереди. Подходит, когда бэкенды имеют схожую производительность.', @@ -1041,11 +1044,9 @@ const message = { requestBody: 'Тело запроса', requestBodySize: 'Размер тела запроса', requestBodyTruncated: 'Обрезано', - requestBodyHash: 'Hash тела запроса', responseBody: 'Тело ответа', responseBodySize: 'Размер тела ответа', responseBodyTruncated: 'Тело ответа обрезано', - responseBodyHash: 'Hash тела ответа', readableContent: 'Читаемое содержимое', rawResponse: 'Исходный ответ', replyContent: 'Содержимое ответа', @@ -1091,7 +1092,10 @@ const message = { usageKeywordPlaceholder: 'Поиск по Request ID / модели / upstream-модели / сообщению об ошибке', attemptChain: 'Цепочка попыток', attemptIndex: 'Порядок вызова', - noAttemptDetails: 'Запрос выполнен с первой попытки, деталей fallback нет', + clientProtocol: 'API клиента', + upstreamProtocol: 'Вышестоящий API', + conversionPath: 'Путь преобразования', + noAttemptDetails: 'Нет записей цепочки попыток', input: 'Вход', output: 'Выход', total: 'Итого', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index b0f96224d..96e7c5e47 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -1019,6 +1019,9 @@ const message = { performanceSetting: 'Performans Ayarları', logSetting: 'Günlük Ayarları', lbPolicy: 'Yük dengeleme politikası', + protocolConversion: 'Protokol dönüştürme', + protocolConversionDesc: + 'Chat Completions, Responses ve Anthropic Messages arasında desteklenen doğrudan dönüşüme izin verir. Yerel protokol her zaman önceliklidir.', lbPolicyRoundRobin: 'Round Robin', lbPolicyRoundRobinDesc: 'Kullanılabilir model hesaplarını sırayla seçer. Backend kapasiteleri benzer olduğunda kullanın.', @@ -1051,11 +1054,9 @@ const message = { requestBody: 'İstek Gövdesi', requestBodySize: 'İstek Gövdesi Boyutu', requestBodyTruncated: 'Kısaltıldı', - requestBodyHash: 'İstek Gövdesi Hash', responseBody: 'Yanıt Gövdesi', responseBodySize: 'Yanıt Gövdesi Boyutu', responseBodyTruncated: 'Yanıt Gövdesi Kısaltıldı', - responseBodyHash: 'Yanıt Gövdesi Hash', readableContent: 'Okunabilir İçerik', rawResponse: 'Ham Yanıt', replyContent: 'Yanıt İçeriği', @@ -1101,7 +1102,10 @@ const message = { usageKeywordPlaceholder: 'Request ID / model / upstream model / hata mesajı ara', attemptChain: 'Deneme zinciri', attemptIndex: 'Çağrı sırası', - noAttemptDetails: 'Bu istek tek denemede isabet etti; fallback ayrıntısı yok', + clientProtocol: 'İstemci API', + upstreamProtocol: 'Üst API', + conversionPath: 'Dönüşüm yolu', + noAttemptDetails: 'Deneme zinciri kaydı yok', input: 'Girdi', output: 'Çıktı', total: 'Toplam', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index 3a0777598..1ecc75dcb 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -955,6 +955,9 @@ const message = { performanceSetting: '效能設定', logSetting: '日誌設定', lbPolicy: '負載策略', + protocolConversion: '協議轉換', + protocolConversionDesc: + '啟用後,文字請求可在 Chat Completions、Responses 和 Anthropic Messages 的支援協議間直接轉換,原生協議始終優先。', lbPolicyRoundRobin: '輪詢', lbPolicyRoundRobinDesc: '按順序依次選擇可用模型帳號,適合後端能力接近的場景。', lbPolicyWeightedRoundRobin: '加權輪詢', @@ -983,11 +986,9 @@ const message = { requestBody: '請求體', requestBodySize: '請求體大小', requestBodyTruncated: '是否截斷', - requestBodyHash: '請求體 Hash', responseBody: '響應體', responseBodySize: '響應體大小', responseBodyTruncated: '響應體是否截斷', - responseBodyHash: '響應體 Hash', readableContent: '可讀內容', rawResponse: '原始響應', replyContent: '回覆內容', @@ -1033,7 +1034,10 @@ const message = { usageKeywordPlaceholder: '搜尋 Request ID / 模型 / 上游模型 / 錯誤資訊', attemptChain: '呼叫鏈路', attemptIndex: '調用順序', - noAttemptDetails: '本次請求一次命中,無 fallback 明細', + clientProtocol: '用戶端 API', + upstreamProtocol: '上游 API', + conversionPath: '轉換路徑', + noAttemptDetails: '暫無呼叫鏈路記錄', input: '輸入', output: '輸出', total: '總計', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 8ae25d1d1..c5a0f541b 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -952,6 +952,9 @@ const message = { performanceSetting: '性能设置', logSetting: '日志设置', lbPolicy: '负载策略', + protocolConversion: '协议转换', + protocolConversionDesc: + '开启后,文本请求可在 Chat Completions、Responses 和 Anthropic Messages 的受支持协议间直接转换,原生协议始终优先。', lbPolicyRoundRobin: '轮询', lbPolicyRoundRobinDesc: '按顺序依次选择可用模型账号,适合后端能力接近的场景。', lbPolicyWeightedRoundRobin: '加权轮询', @@ -980,11 +983,9 @@ const message = { requestBody: '请求体', requestBodySize: '请求体大小', requestBodyTruncated: '是否截断', - requestBodyHash: '请求体 Hash', responseBody: '响应体', responseBodySize: '响应体大小', responseBodyTruncated: '响应体是否截断', - responseBodyHash: '响应体 Hash', readableContent: '可读内容', rawResponse: '原始响应', replyContent: '回复内容', @@ -1030,7 +1031,10 @@ const message = { usageKeywordPlaceholder: '搜索 Request ID / 模型 / 上游模型 / 错误信息', attemptChain: '调用链路', attemptIndex: '调用顺序', - noAttemptDetails: '本次请求一次命中,无 fallback 明细', + clientProtocol: '客户端 API', + upstreamProtocol: '上游 API', + conversionPath: '转换路径', + noAttemptDetails: '暂无调用链路记录', input: '输入', output: '输出', total: '总计',