mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-22 16:00:51 +00:00
feat: Optimize the application upgrade logic. (#13415)
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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", ""))
|
||||
}()
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -515,6 +515,7 @@ type openrestyUpgradeSnapshot struct {
|
||||
|
||||
var openrestyUpgradeSnapshotPaths = []string{
|
||||
nginxModuleBuildDir,
|
||||
nginxModuleModulesDir,
|
||||
"scripts",
|
||||
path.Join(nginxModuleConfDir, nginxModuleEnabledConfDir),
|
||||
path.Join(nginxModuleConfDir, "nginx.conf"),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -154,6 +154,11 @@ ErrAppVersionDeprecated: "برنامه {{ .name }} با نسخه فعلی 1Panel
|
||||
ErrDockerFailed: 'Docker غیرعادی است؛ وضعیت سرویس را بررسی کنید'
|
||||
ErrDockerComposeCmdNotFound: 'Docker Compose روی میزبان یافت نشد'
|
||||
UseExistImage: 'تصویر وجود دارد؛ استفاده از تصویر موجود'
|
||||
UpgradePrepare: 'آمادهسازی ارتقاء برنامه'
|
||||
UpgradeStop: 'توقف برنامه اصلی'
|
||||
UpgradeWaitReady: 'انتظار برای آماده شدن برنامه'
|
||||
UpgradeBackupDisabled: 'پشتیبان ارتقاء غیرفعال است؛ تغییرات داده اسکریپتهای ارتقاء کاملاً قابل بازگشت نیست'
|
||||
UpgradeRollbackFailed: 'بازگشت ناموفق بود'
|
||||
ErrDatabaseNotFound: 'پایگاه داده یافت نشد: {{ .name }}'
|
||||
|
||||
# SSH
|
||||
|
||||
@@ -154,6 +154,11 @@ ErrAppVersionDeprecated: '{{ .name }} アプリケーションは現在の 1Pane
|
||||
ErrDockerFailed: 'Docker の状態が異常です。サービス状態を確認してください'
|
||||
ErrDockerComposeCmdNotFound: 'Docker Compose コマンドは存在しません。ホストマシンにこのコマンドを先にインストールしてください'
|
||||
UseExistImage: 'イメージは既に存在します。既存のイメージを使用します'
|
||||
UpgradePrepare: 'アプリケーションのアップグレードを準備'
|
||||
UpgradeStop: '元のアプリケーションを停止'
|
||||
UpgradeWaitReady: 'アプリケーションの準備完了を待機'
|
||||
UpgradeBackupDisabled: 'アップグレードバックアップが無効なため、アップグレードスクリプトによるデータ変更を完全にはロールバックできません'
|
||||
UpgradeRollbackFailed: 'ロールバックに失敗しました'
|
||||
ErrDatabaseNotFound: 'データベース {{ .name }} は存在しません'
|
||||
|
||||
# SSH
|
||||
|
||||
@@ -154,6 +154,11 @@ ErrAppVersionDeprecated: '{{ .name }} 응용 프로그램은 현재 1Panel 버
|
||||
ErrDockerFailed: 'Docker의 상태가 비정상입니다. 서비스 상태를 확인하세요'
|
||||
ErrDockerComposeCmdNotFound: 'Docker Compose 명령이 없습니다. 호스트 머신에 먼저 이 명령을 설치하세요'
|
||||
UseExistImage: '이미지가 이미 존재하여 기존 이미지를 사용합니다'
|
||||
UpgradePrepare: '애플리케이션 업그레이드 준비'
|
||||
UpgradeStop: '기존 애플리케이션 중지'
|
||||
UpgradeWaitReady: '애플리케이션 준비 상태 대기'
|
||||
UpgradeBackupDisabled: '업그레이드 백업이 비활성화되어 업그레이드 스크립트의 데이터 변경을 완전히 롤백할 수 없습니다'
|
||||
UpgradeRollbackFailed: '롤백 실패'
|
||||
ErrDatabaseNotFound: '데이터베이스 {{ .name }} 이(가) 존재하지 않습니다'
|
||||
|
||||
# SSH
|
||||
|
||||
@@ -144,6 +144,11 @@ ErrAppVersionDeprecated: "ແອັບພລິເຄຊັນ {{ .name }} ບ
|
||||
ErrDockerFailed: 'Docker ຜິດປົກກະຕິ; ກະລຸນາກວດສອບສະຖານະການບໍລິການ'
|
||||
ErrDockerComposeCmdNotFound: 'ບໍ່ພົບຄຳສັ່ງ Docker Compose ໃນເຄື່ອງ'
|
||||
UseExistImage: 'ມີຮູບພາບຢູ່ແລ້ວ; ກຳລັງໃຊ້ຮູບພາບທີ່ມີຢູ່'
|
||||
UpgradePrepare: 'ກະກຽມອັບເກຣດແອັບ'
|
||||
UpgradeStop: 'ຢຸດແອັບເດີມ'
|
||||
UpgradeWaitReady: 'ລໍຖ້າແອັບພ້ອມ'
|
||||
UpgradeBackupDisabled: 'ປິດການສຳຮອງກ່ອນອັບເກຣດ; ການປ່ຽນແປງຂໍ້ມູນຈາກສະຄຣິບບໍ່ສາມາດຍ້ອນກັບໄດ້ທັງໝົດ'
|
||||
UpgradeRollbackFailed: 'ຍ້ອນກັບລົ້ມເຫຼວ'
|
||||
ErrDatabaseNotFound: 'ບໍ່ພົບຖານຂໍ້ມູນ: {{ .name }}'
|
||||
|
||||
#ssh
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -154,6 +154,11 @@ ErrAppVersionDeprecated: 'Приложение {{ .name }} несовмести
|
||||
ErrDockerFailed: 'Состояние Docker аномально, проверьте состояние сервиса'
|
||||
ErrDockerComposeCmdNotFound: 'Команда Docker Compose отсутствует, пожалуйста, установите эту команду на хост-машине сначала'
|
||||
UseExistImage: 'Образ уже существует, используется существующий образ'
|
||||
UpgradePrepare: 'Подготовка обновления приложения'
|
||||
UpgradeStop: 'Остановка исходного приложения'
|
||||
UpgradeWaitReady: 'Ожидание готовности приложения'
|
||||
UpgradeBackupDisabled: 'Резервная копия обновления отключена; изменения данных из сценариев обновления нельзя полностью откатить'
|
||||
UpgradeRollbackFailed: 'Ошибка отката'
|
||||
ErrDatabaseNotFound: 'База данных {{ .name }} не существует'
|
||||
|
||||
# SSH
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -154,6 +154,11 @@ ErrAppVersionDeprecated: '{{ .name }} 應用不適配目前 1Panel 版本,跳
|
||||
ErrDockerFailed: 'Docker 狀態異常,請檢查服務狀態'
|
||||
ErrDockerComposeCmdNotFound: 'Docker Compose 指令不存在,請先在宿主機安裝此指令'
|
||||
UseExistImage: '映像已存在,使用現有映像'
|
||||
UpgradePrepare: '準備應用程式升級'
|
||||
UpgradeStop: '停止原應用程式'
|
||||
UpgradeWaitReady: '等待應用程式就緒'
|
||||
UpgradeBackupDisabled: '未啟用升級備份,升級腳本產生的資料變更無法完整回滾'
|
||||
UpgradeRollbackFailed: '回滾失敗'
|
||||
ErrDatabaseNotFound: '資料庫 {{ .name }} 不存在'
|
||||
|
||||
# SSH
|
||||
|
||||
@@ -154,6 +154,11 @@ ErrAppVersionDeprecated: "{{ .name }} 应用不适配当前 1Panel 版本,跳
|
||||
ErrDockerFailed: "Docker 状态异常,请检查服务状态"
|
||||
ErrDockerComposeCmdNotFound: "Docker Compose 命令不存在,请先在宿主机安装"
|
||||
UseExistImage: "镜像已存在,使用存量镜像"
|
||||
UpgradePrepare: "准备应用升级"
|
||||
UpgradeStop: "停止原应用"
|
||||
UpgradeWaitReady: "等待应用就绪"
|
||||
UpgradeBackupDisabled: "未启用升级备份,升级脚本产生的数据变更无法完整回滚"
|
||||
UpgradeRollbackFailed: "回滚失败"
|
||||
ErrDatabaseNotFound: "数据库 {{ .name }} 不存在"
|
||||
|
||||
# SSH
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: 'مجموع',
|
||||
|
||||
@@ -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: '合計',
|
||||
|
||||
@@ -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: '합계',
|
||||
|
||||
@@ -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: 'ທັງໝົດ',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: 'Итого',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: '總計',
|
||||
|
||||
@@ -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: '总计',
|
||||
|
||||
Reference in New Issue
Block a user