feat(sync): sync php extended template

This commit is contained in:
guoyuqi
2026-11-04 16:55:22 +08:00
parent d1059c7356
commit c7bd036390
48 changed files with 399 additions and 21 deletions
+43
View File
@@ -103,3 +103,46 @@ func (b *BaseApi) DeletePHPExtensions(c *gin.Context) {
}
helper.Success(c)
}
// @Router /runtimes/php/extensions/detail [post]
func (b *BaseApi) GetPHPExtensions(c *gin.Context) {
var req request.PHPExtensionsDelete
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
item, err := phpExtensionsService.Get(req.ID)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, item)
}
// @Router /runtimes/php/extensions/import [post]
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 PHP 扩展模版 [name]","formatEN":"Sync PHP extension template [name]"}
func (b *BaseApi) ImportPHPExtensions(c *gin.Context) {
var req request.PHPExtensionsCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
status, err := phpExtensionsService.Import(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, status)
}
// @Router /runtimes/php/extensions/push [post]
// @x-panel-log {"bodyKeys":["sourceNode","targetNodes"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"推送 PHP 扩展模版 [sourceNode] 至 [targetNodes]","formatEN":"Push PHP extension template from [sourceNode] to [targetNodes]"}
func (b *BaseApi) PushPHPExtensions(c *gin.Context) {
var req dto.PHPExtensionsPush
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := phpExtensionsService.Push(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
+9
View File
@@ -0,0 +1,9 @@
package dto
// PHPExtensionsPush is submitted to the master agent, with an explicit source node.
type PHPExtensionsPush struct {
SourceNode string `json:"sourceNode" validate:"required"`
TargetNodes []string `json:"targetNodes" validate:"required,min=1,dive,required"`
ID uint `json:"id" validate:"required"`
TaskID string `json:"taskID" validate:"required,uuid"`
}
+2 -1
View File
@@ -4,7 +4,8 @@ import "github.com/1Panel-dev/1Panel/agent/app/dto"
type PHPExtensionsSearch struct {
dto.PageInfo
All bool `json:"all"`
All bool `json:"all"`
Name string `json:"name"`
}
type PHPExtensionsCreate struct {
+53 -1
View File
@@ -1,13 +1,22 @@
package service
import (
"errors"
"sync"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
"gorm.io/gorm"
)
var phpExtensionsMu sync.Mutex
type PHPExtensionsService struct {
}
@@ -17,6 +26,9 @@ type IPHPExtensionsService interface {
Create(req request.PHPExtensionsCreate) error
Update(req request.PHPExtensionsUpdate) error
Delete(req request.PHPExtensionsDelete) error
Get(id uint) (model.PHPExtensions, error)
Import(req request.PHPExtensionsCreate) (string, error)
Push(req dto.PHPExtensionsPush) error
}
func NewIPHPExtensionsService() IPHPExtensionsService {
@@ -30,7 +42,7 @@ func (p PHPExtensionsService) Page(req request.PHPExtensionsSearch) (int64, []re
err error
result []response.PHPExtensionsDTO
)
total, extensions, err = phpExtensionsRepo.Page(req.Page, req.PageSize)
total, extensions, err = phpExtensionsRepo.Page(req.Page, req.PageSize, repo.WithByLikeName(req.Name))
if err != nil {
return 0, nil, err
}
@@ -61,6 +73,8 @@ func (p PHPExtensionsService) List() ([]response.PHPExtensionsDTO, error) {
}
func (p PHPExtensionsService) Create(req request.PHPExtensionsCreate) error {
phpExtensionsMu.Lock()
defer phpExtensionsMu.Unlock()
exist, _ := phpExtensionsRepo.GetFirst(repo.WithByName(req.Name))
if exist.ID > 0 {
return buserr.New("ErrNameIsExist")
@@ -73,6 +87,8 @@ func (p PHPExtensionsService) Create(req request.PHPExtensionsCreate) error {
}
func (p PHPExtensionsService) Update(req request.PHPExtensionsUpdate) error {
phpExtensionsMu.Lock()
defer phpExtensionsMu.Unlock()
exist, err := phpExtensionsRepo.GetFirst(repo.WithByID(req.ID))
if err != nil {
return err
@@ -82,5 +98,41 @@ func (p PHPExtensionsService) Update(req request.PHPExtensionsUpdate) error {
}
func (p PHPExtensionsService) Delete(req request.PHPExtensionsDelete) error {
phpExtensionsMu.Lock()
defer phpExtensionsMu.Unlock()
return phpExtensionsRepo.DeleteBy(repo.WithByID(req.ID))
}
func (p PHPExtensionsService) Get(id uint) (model.PHPExtensions, error) {
return phpExtensionsRepo.GetFirst(repo.WithByID(id))
}
func (p PHPExtensionsService) Import(req request.PHPExtensionsCreate) (string, error) {
phpExtensionsMu.Lock()
defer phpExtensionsMu.Unlock()
existing, err := phpExtensionsRepo.GetFirst(repo.WithByName(req.Name))
if err == nil {
if existing.Extensions == req.Extensions {
return "skipped", nil
}
return "", buserr.New("ErrPHPExtensionsConflict")
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
return "", err
}
item := model.PHPExtensions{Name: req.Name, Extensions: req.Extensions}
if err := phpExtensionsRepo.Create(&item); err != nil {
return "", err
}
return "created", nil
}
func (p PHPExtensionsService) Push(req dto.PHPExtensionsPush) error {
if !global.IsMaster {
return errors.New("PHP extension template sync must be submitted to the master node")
}
if !xpack.MultiNodeProvider.IsXpack() {
return buserr.New("ErrXpackNotFound")
}
return xpack.MultiNodeProvider.PushPHPExtensionsToNode(req)
}
+2
View File
@@ -815,3 +815,5 @@ ContainerImageCheckSummary: "Check finished: {{ .checked }} checked, {{ .updates
ContainerImageCheckFailures: "{{ .count }} container checks failed; see the task log"
ErrImageCheckProxyRequired: "To check for image updates in the current runtime environment, please configure a proxy server first."
ErrPHPExtensionsConflict: "A PHP extension template with this name exists with different content"
+2
View File
@@ -815,3 +815,5 @@ ContainerImageCheckSummary: "Comprobación finalizada: comprobados {{ .checked }
ContainerImageCheckFailures: "Falló la comprobación de {{ .count }} contenedores; consulte el registro de la tarea"
ErrImageCheckProxyRequired: "Para comprobar actualizaciones de imágenes en el entorno actual, configura primero un servidor proxy."
ErrPHPExtensionsConflict: "A PHP extension template with this name exists with different content"
+2
View File
@@ -816,3 +816,5 @@ ContainerImageCheckSummary: "بررسی پایان یافت: بررسی‌شده
ContainerImageCheckFailures: "بررسی {{ .count }} کانتینر ناموفق بود؛ گزارش وظیفه را مشاهده کنید"
ErrImageCheckProxyRequired: "برای بررسی به‌روزرسانی تصاویر در محیط فعلی، ابتدا یک سرور پروکسی پیکربندی کنید."
ErrPHPExtensionsConflict: "A PHP extension template with this name exists with different content"
+2
View File
@@ -815,3 +815,5 @@ ContainerImageCheckSummary: "確認終了:成功 {{ .checked }}、更新可能
ContainerImageCheckFailures: "{{ .count }} コンテナの確認に失敗しました。タスクログを確認してください"
ErrImageCheckProxyRequired: "現在の実行環境でイメージの更新を確認するには、先にプロキシサーバーを設定してください。"
ErrPHPExtensionsConflict: "A PHP extension template with this name exists with different content"
+2
View File
@@ -815,3 +815,5 @@ ContainerImageCheckSummary: "확인 종료: 성공 {{ .checked }}, 업데이트
ContainerImageCheckFailures: "컨테이너 {{ .count }}개 확인에 실패했습니다. 작업 로그를 확인하세요"
ErrImageCheckProxyRequired: "현재 실행 환경에서 이미지 업데이트를 확인하려면 먼저 프록시 서버를 설정하세요."
ErrPHPExtensionsConflict: "A PHP extension template with this name exists with different content"
+2
View File
@@ -806,3 +806,5 @@ ContainerImageCheckSummary: "ກວດສອບສິ້ນສຸດ: ກວດ
ContainerImageCheckFailures: "ກວດສອບ {{ .count }} ຄອນເທນເນີລົ້ມເຫຼວ; ກະລຸນາເບິ່ງບັນທຶກວຽກ"
ErrImageCheckProxyRequired: "ເພື່ອກວດສອບການອັບເດດອິມເມຈໃນສະພາບແວດລ້ອມປັດຈຸບັນ ກະລຸນາຕັ້ງຄ່າພຣັອກຊີເຊີບເວີກ່ອນ."
ErrPHPExtensionsConflict: "A PHP extension template with this name exists with different content"
+2
View File
@@ -815,3 +815,5 @@ ContainerImageCheckSummary: "Semakan selesai: disemak {{ .checked }}, boleh dike
ContainerImageCheckFailures: "Semakan {{ .count }} kontena gagal; lihat log tugas"
ErrImageCheckProxyRequired: "Untuk menyemak kemas kini imej dalam persekitaran semasa, sila konfigurasikan pelayan proksi terlebih dahulu."
ErrPHPExtensionsConflict: "A PHP extension template with this name exists with different content"
+2
View File
@@ -815,3 +815,5 @@ ContainerImageCheckSummary: "Verificação concluída: verificados {{ .checked }
ContainerImageCheckFailures: "Falha na verificação de {{ .count }} contêineres; consulte o log da tarefa"
ErrImageCheckProxyRequired: "Para verificar atualizações de imagens no ambiente atual, configure primeiro um servidor proxy."
ErrPHPExtensionsConflict: "A PHP extension template with this name exists with different content"
+2
View File
@@ -815,3 +815,5 @@ ContainerImageCheckSummary: "Проверка завершена: провере
ContainerImageCheckFailures: "Не удалось проверить контейнеры: {{ .count }}. Подробности в журнале задачи"
ErrImageCheckProxyRequired: "Для проверки обновлений образов в текущей среде сначала настройте прокси-сервер."
ErrPHPExtensionsConflict: "A PHP extension template with this name exists with different content"
+2
View File
@@ -815,3 +815,5 @@ ContainerImageCheckSummary: "Denetim tamamlandı: denetlenen {{ .checked }}, gü
ContainerImageCheckFailures: "{{ .count }} konteyner denetlenemedi; görev günlüğünü inceleyin"
ErrImageCheckProxyRequired: "Geçerli çalışma ortamında imaj güncellemelerini kontrol etmek için önce bir proxy sunucusu yapılandırın."
ErrPHPExtensionsConflict: "A PHP extension template with this name exists with different content"
+2
View File
@@ -815,3 +815,5 @@ ContainerImageCheckSummary: "檢查結束:成功 {{ .checked }},可升級 {{
ContainerImageCheckFailures: "{{ .count }} 個容器檢查失敗,請查看任務日誌"
ErrImageCheckProxyRequired: "目前運行環境下,如需檢查映像是否更新,請先設定代理伺服器"
ErrPHPExtensionsConflict: "同名 PHP 擴充模版已存在且內容不同,未覆蓋"
+2
View File
@@ -815,3 +815,5 @@ ContainerImageCheckSummary: "检查结束:成功 {{ .checked }},可升级 {{
ContainerImageCheckFailures: "{{ .count }} 个容器检查失败,请查看任务日志"
ErrImageCheckProxyRequired: "当前运行环境下,如需检查镜像是否更新,请优先配置代理服务器"
ErrPHPExtensionsConflict: "同名 PHP 扩展模版已存在且内容不同,未覆盖"
+3
View File
@@ -27,6 +27,9 @@ func (r *RuntimeRouter) InitRouter(Router *gin.RouterGroup) {
groupRouter.POST("/node/modules/operate", baseApi.OperateNodeModules)
groupRouter.POST("/php/extensions/search", baseApi.PagePHPExtensions)
groupRouter.POST("/php/extensions/detail", baseApi.GetPHPExtensions)
groupRouter.POST("/php/extensions/import", baseApi.ImportPHPExtensions)
groupRouter.POST("/php/extensions/push", baseApi.PushPHPExtensions)
groupRouter.POST("/php/extensions", baseApi.CreatePHPExtensions)
groupRouter.POST("/php/extensions/update", baseApi.UpdatePHPExtensions)
groupRouter.POST("/php/extensions/del", baseApi.DeletePHPExtensions)
+4
View File
@@ -72,3 +72,7 @@ func (m *multiNodeHelper) PushSSLToNode(websiteSSL *model.WebsiteSSL) error {
func (m *multiNodeHelper) GetAgentInfo() (*dto.AgentInfo, error) {
return nil, nil
}
func (m *multiNodeHelper) PushPHPExtensionsToNode(req dto.PHPExtensionsPush) error {
return buserr.New("ErrXpackNotFound")
}
@@ -19,5 +19,6 @@ type MultiNodeProvider interface {
LoadRequestTransport() *http.Transport
ValidateCertificate(c *gin.Context) bool
PushSSLToNode(websiteSSL *model.WebsiteSSL) error
PushPHPExtensionsToNode(req dto.PHPExtensionsPush) error
GetAgentInfo() (*dto.AgentInfo, error)
}
+16 -15
View File
@@ -189,21 +189,22 @@ var WebUrlMap = map[string]struct{}{
"/xpack/monitor/trend": {},
"/xpack/monitor/websites": {},
"/xpack/tamper": {},
"/xpack/gpu": {},
"/xpack/alert/dashboard": {},
"/xpack/alert/log": {},
"/xpack/alert/setting": {},
"/xpack/setting": {},
"/xpack/node/dashboard": {},
"/xpack/node": {},
"/xpack/simple-node": {},
"/xpack/sync/file": {},
"/xpack/sync/image": {},
"/xpack/sync/ssl": {},
"/xpack/sync/app": {},
"/xpack/app": {},
"/xpack/app-upgrade": {},
"/xpack/tamper": {},
"/xpack/gpu": {},
"/xpack/alert/dashboard": {},
"/xpack/alert/log": {},
"/xpack/alert/setting": {},
"/xpack/setting": {},
"/xpack/node/dashboard": {},
"/xpack/node": {},
"/xpack/simple-node": {},
"/xpack/sync/file": {},
"/xpack/sync/image": {},
"/xpack/sync/php-extensions": {},
"/xpack/sync/ssl": {},
"/xpack/sync/app": {},
"/xpack/app": {},
"/xpack/app-upgrade": {},
"/xpack/cluster/mysql": {},
"/xpack/cluster/postgres": {},
+4
View File
@@ -587,6 +587,10 @@ MCPInstallRecoveryFailed: "Installation failed and service recovery is incomplet
MCPInstallLocalImage: "The image exists locally; skip downloading"
MCPInstallVerifyImage: "Validate the image version and managed service contract"
MCPInstallVerifiedImage: "Image validation passed"
PHPExtensionsNoTargets: "No target nodes remain after excluding the source node"
PHPExtensionsSyncTask: "Sync PHP extension template [{{ .name }}]"
PHPExtensionsSyncSkipped: "Node [{{ .node }}] already has an identical template; skipped"
MCPInstallCleanup: "Clean up the previous container"
MCPInstallOldContainerRemoved: "Previous container removed"
MCPInstallOldImageRemoved: "Previous image removed"
+4
View File
@@ -587,6 +587,10 @@ MCPInstallRecoveryFailed: "Instalación fallida y recuperación incompleta. Veri
MCPInstallLocalImage: "La imagen ya existe localmente; omitir la descarga"
MCPInstallVerifyImage: "Validar la versión de la imagen y el contrato del servicio administrado"
MCPInstallVerifiedImage: "La imagen superó la validación"
PHPExtensionsNoTargets: "No target nodes remain after excluding the source node"
PHPExtensionsSyncTask: "Sync PHP extension template [{{ .name }}]"
PHPExtensionsSyncSkipped: "Node [{{ .node }}] already has an identical template; skipped"
MCPInstallCleanup: "Limpiar el contenedor anterior"
MCPInstallOldContainerRemoved: "Contenedor anterior eliminado"
MCPInstallOldImageRemoved: "Imagen anterior eliminada"
+4
View File
@@ -587,6 +587,10 @@ MCPInstallRecoveryFailed: "نصب ناموفق و بازیابی سرویس نا
MCPInstallLocalImage: "تصویر به‌صورت محلی موجود است؛ دریافت انجام نمی‌شود"
MCPInstallVerifyImage: "اعتبارسنجی نسخه تصویر و قرارداد سرویس مدیریت‌شده"
MCPInstallVerifiedImage: "اعتبارسنجی تصویر موفق بود"
PHPExtensionsNoTargets: "No target nodes remain after excluding the source node"
PHPExtensionsSyncTask: "Sync PHP extension template [{{ .name }}]"
PHPExtensionsSyncSkipped: "Node [{{ .node }}] already has an identical template; skipped"
MCPInstallCleanup: "پاک‌سازی کانتینر قبلی"
MCPInstallOldContainerRemoved: "کانتینر قبلی حذف شد"
MCPInstallOldImageRemoved: "ایمیج قبلی حذف شد"
+4
View File
@@ -587,6 +587,10 @@ MCPInstallRecoveryFailed: "インストールに失敗し、サービスの復
MCPInstallLocalImage: "ローカルにイメージが存在するため、ダウンロードをスキップ"
MCPInstallVerifyImage: "イメージのバージョンと管理対象サービスの契約を検証"
MCPInstallVerifiedImage: "イメージの検証に成功しました"
PHPExtensionsNoTargets: "No target nodes remain after excluding the source node"
PHPExtensionsSyncTask: "Sync PHP extension template [{{ .name }}]"
PHPExtensionsSyncSkipped: "Node [{{ .node }}] already has an identical template; skipped"
MCPInstallCleanup: "以前のコンテナーをクリーンアップ"
MCPInstallOldContainerRemoved: "以前のコンテナーを削除しました"
MCPInstallOldImageRemoved: "以前のイメージを削除しました"
+4
View File
@@ -587,6 +587,10 @@ MCPInstallRecoveryFailed: "설치에 실패했고 서비스 복원이 불완전
MCPInstallLocalImage: "로컬 이미지가 있어 다운로드를 건너뜁니다"
MCPInstallVerifyImage: "이미지 버전 및 관리형 서비스 계약 검증"
MCPInstallVerifiedImage: "이미지 검증 통과"
PHPExtensionsNoTargets: "No target nodes remain after excluding the source node"
PHPExtensionsSyncTask: "Sync PHP extension template [{{ .name }}]"
PHPExtensionsSyncSkipped: "Node [{{ .node }}] already has an identical template; skipped"
MCPInstallCleanup: "이전 컨테이너 정리"
MCPInstallOldContainerRemoved: "이전 컨테이너가 삭제되었습니다"
MCPInstallOldImageRemoved: "이전 이미지가 삭제되었습니다"
+4
View File
@@ -587,6 +587,10 @@ MCPInstallRecoveryFailed: "ຕິດຕັ້ງບໍ່ສຳເລັດແ
MCPInstallLocalImage: "ມີອິມເມດຢູ່ໃນເຄື່ອງແລ້ວ; ຂ້າມການດາວໂຫຼດ"
MCPInstallVerifyImage: "ກວດສອບເວີຊັນອິມເມດ ແລະ ສັນຍາບໍລິການທີ່ຈັດການ"
MCPInstallVerifiedImage: "ກວດສອບອິມເມດຜ່ານແລ້ວ"
PHPExtensionsNoTargets: "No target nodes remain after excluding the source node"
PHPExtensionsSyncTask: "Sync PHP extension template [{{ .name }}]"
PHPExtensionsSyncSkipped: "Node [{{ .node }}] already has an identical template; skipped"
MCPInstallCleanup: "ກຳລັງລ້າງຄອນເທນເນີເກົ່າ"
MCPInstallOldContainerRemoved: "ລຶບຄອນເທນເນີເກົ່າແລ້ວ"
MCPInstallOldImageRemoved: "ລຶບອິມເມຈເກົ່າແລ້ວ"
+4
View File
@@ -587,6 +587,10 @@ MCPInstallRecoveryFailed: "Pemasangan gagal dan pemulihan tidak lengkap. Sahkan
MCPInstallLocalImage: "Imej sudah tersedia secara setempat; langkau muat turun"
MCPInstallVerifyImage: "Sahkan versi imej dan kontrak perkhidmatan terurus"
MCPInstallVerifiedImage: "Pengesahan imej berjaya"
PHPExtensionsNoTargets: "No target nodes remain after excluding the source node"
PHPExtensionsSyncTask: "Sync PHP extension template [{{ .name }}]"
PHPExtensionsSyncSkipped: "Node [{{ .node }}] already has an identical template; skipped"
MCPInstallCleanup: "Bersihkan bekas terdahulu"
MCPInstallOldContainerRemoved: "Bekas terdahulu dipadam"
MCPInstallOldImageRemoved: "Imej terdahulu dipadam"
+4
View File
@@ -587,6 +587,10 @@ MCPInstallRecoveryFailed: "Instalação falhou e a recuperação está incomplet
MCPInstallLocalImage: "A imagem já existe localmente; ignorar o download"
MCPInstallVerifyImage: "Validar a versão da imagem e o contrato do serviço gerenciado"
MCPInstallVerifiedImage: "A imagem passou na validação"
PHPExtensionsNoTargets: "No target nodes remain after excluding the source node"
PHPExtensionsSyncTask: "Sync PHP extension template [{{ .name }}]"
PHPExtensionsSyncSkipped: "Node [{{ .node }}] already has an identical template; skipped"
MCPInstallCleanup: "Limpar o contêiner anterior"
MCPInstallOldContainerRemoved: "Contêiner anterior removido"
MCPInstallOldImageRemoved: "Imagem anterior removida"
+4
View File
@@ -587,6 +587,10 @@ MCPInstallRecoveryFailed: "Установка не удалась, служба
MCPInstallLocalImage: "Образ уже есть локально; пропустить загрузку"
MCPInstallVerifyImage: "Проверить версию образа и контракт управляемой службы"
MCPInstallVerifiedImage: "Образ успешно проверен"
PHPExtensionsNoTargets: "No target nodes remain after excluding the source node"
PHPExtensionsSyncTask: "Sync PHP extension template [{{ .name }}]"
PHPExtensionsSyncSkipped: "Node [{{ .node }}] already has an identical template; skipped"
MCPInstallCleanup: "Очистка предыдущего контейнера"
MCPInstallOldContainerRemoved: "Предыдущий контейнер удалён"
MCPInstallOldImageRemoved: "Предыдущий образ удалён"
+4
View File
@@ -587,6 +587,10 @@ MCPInstallRecoveryFailed: "Kurulum başarısız ve hizmet tam geri yüklenemedi.
MCPInstallLocalImage: "İmaj yerel olarak mevcut; indirmeyi atla"
MCPInstallVerifyImage: "İmaj sürümünü ve yönetilen hizmet sözleşmesini doğrula"
MCPInstallVerifiedImage: "İmaj doğrulaması başarılı"
PHPExtensionsNoTargets: "No target nodes remain after excluding the source node"
PHPExtensionsSyncTask: "Sync PHP extension template [{{ .name }}]"
PHPExtensionsSyncSkipped: "Node [{{ .node }}] already has an identical template; skipped"
MCPInstallCleanup: "Önceki konteyneri temizle"
MCPInstallOldContainerRemoved: "Önceki konteyner silindi"
MCPInstallOldImageRemoved: "Önceki imaj silindi"
+4
View File
@@ -587,6 +587,10 @@ MCPInstallRecoveryFailed: "安裝失敗且原服務還原不完整,請核對
MCPInstallLocalImage: "本機映像已存在,略過下載"
MCPInstallVerifyImage: "驗證映像版本及受管契約"
MCPInstallVerifiedImage: "映像驗證通過"
PHPExtensionsNoTargets: "排除來源節點後,沒有可同步的目標節點"
PHPExtensionsSyncTask: "同步 PHP 擴充模版 [{{ .name }}]"
PHPExtensionsSyncSkipped: "節點 [{{ .node }}] 已存在相同模版,略過"
MCPInstallCleanup: "正在清理舊容器"
MCPInstallOldContainerRemoved: "舊容器已刪除"
MCPInstallOldImageRemoved: "舊映像已刪除"
+4
View File
@@ -587,6 +587,10 @@ MCPInstallRecoveryFailed: "安装失败且原服务恢复不完整,请核对
MCPInstallLocalImage: "本地镜像已存在,跳过下载"
MCPInstallVerifyImage: "校验镜像版本和受管契约"
MCPInstallVerifiedImage: "镜像校验通过"
PHPExtensionsNoTargets: "排除源节点后,没有可同步的目标节点"
PHPExtensionsSyncTask: "同步 PHP 扩展模版 [{{ .name }}]"
PHPExtensionsSyncSkipped: "节点 [{{ .node }}] 已存在相同模版,跳过"
MCPInstallCleanup: "正在清理旧容器"
MCPInstallOldContainerRemoved: "旧容器已删除"
MCPInstallOldImageRemoved: "旧镜像已删除"
+3 -1
View File
@@ -199,5 +199,7 @@ func checkSession(c *gin.Context, refresh bool) bool {
}
func isLocalAPI(urlPath string) bool {
return urlPath == "/api/v2/core/xpack/sync/ssl" || urlPath == "/api/v2/core/xpack/settings/search"
return urlPath == "/api/v2/core/xpack/sync/ssl" ||
urlPath == "/api/v2/core/xpack/sync/php/extensions" ||
urlPath == "/api/v2/core/xpack/settings/search"
}
+1 -1
View File
@@ -98,7 +98,7 @@ func isLocalSyncRequest(reqPath, clientIP, token string) bool {
}
switch reqPath {
case "/api/v2/core/xpack/sync/ssl":
case "/api/v2/core/xpack/sync/ssl", "/api/v2/core/xpack/sync/php/extensions":
return token != ""
case "/api/v2/core/settings/ssl/reload":
return token != ""
+7
View File
@@ -1,6 +1,13 @@
import { CommonModel, ReqPage } from '.';
import { App } from './app';
export namespace Runtime {
export interface PHPExtensionsPush {
sourceNode: string;
targetNodes: string[];
id: number;
taskID: string;
}
export interface Runtime extends CommonModel {
name: string;
appDetailID: number;
+11 -2
View File
@@ -46,8 +46,13 @@ export const OperateNodeModule = (req: Runtime.NodeModuleReq) => {
return http.post<any>(`/runtimes/node/modules/operate`, req, TimeoutEnum.T_10M);
};
export const SearchPHPExtensions = (req: ReqPage) => {
return http.post<ResPage<Runtime.PHPExtensions>>(`/runtimes/php/extensions/search`, req);
export const SearchPHPExtensions = (req: ReqPage & { name?: string }, currentNode?: string) => {
return http.post<ResPage<Runtime.PHPExtensions>>(
`/runtimes/php/extensions/search`,
req,
undefined,
currentNode ? { CurrentNode: currentNode } : undefined,
);
};
export const ListPHPExtensions = (req: Runtime.PHPExtensionsList) => {
@@ -137,3 +142,7 @@ export const updateRemark = (req: Runtime.RemarkUpdate) => {
export const getFPMStatus = (id: number) => {
return http.get<Runtime.FpmStatus[]>(`/runtimes/php/fpm/status/${id}`);
};
export const PushPHPExtensions = (req: Runtime.PHPExtensionsPush) => {
return http.post(`/runtimes/php/extensions/push`, req, undefined, { CurrentNode: 'local' });
};
+43
View File
@@ -4009,6 +4009,13 @@ const message = {
nodeApp: 'Upgrade to Commercial Edition to upgrade multi-node app versions without switching nodes manually.',
nodeDashboard:
'Upgrade to Commercial Edition for centralized management of multi-node apps, websites, databases, and scheduled tasks.',
imageExchange: 'Upgrade to Commercial Edition to transfer container images between nodes in batches.',
appExchange:
'Upgrade to Commercial Edition to migrate installed applications to target nodes and confirm their resource dependencies.',
sslSync:
'Upgrade to Commercial Edition to synchronize certificates from a source node to one or more target nodes.',
phpExtensionsSync:
'Upgrade to Commercial Edition to synchronize PHP extension templates to one or more target nodes.',
fileExchange: 'Upgrade to Commercial Edition to transfer files quickly between servers.',
app: 'Upgrade to Commercial Edition to view service details, anomaly monitoring, and more in the mobile app.',
cluster: 'Upgrade to Commercial Edition to manage MySQL/Postgres/Redis primary-replica clusters.',
@@ -7036,6 +7043,35 @@ const message = {
nodeContent3: "Real-time monitoring of each node's operational status",
nodeTitle4: 'Quick Remote Connection',
nodeContent4: 'One-click direct connection to node remote terminals',
imageExchangeTitle1: 'Select source images',
imageExchangeContent1: 'Browse container images on the source node and select the images to transfer.',
imageExchangeTitle2: 'Distribute images',
imageExchangeContent2: 'Transfer selected images to one or more target nodes.',
imageExchangeTitle3: 'View transfer logs',
imageExchangeContent3: 'Track image transfer progress and results in task logs.',
appExchangeTitle1: 'Select an application',
appExchangeContent1: 'Browse installed applications on the source node and select one to migrate.',
appExchangeTitle2: 'Confirm dependencies',
appExchangeContent2: 'Confirm target nodes and required resources, such as databases, before migration.',
appExchangeTitle3: 'Migrate applications',
appExchangeContent3:
'Transfer application backups to target nodes for restoration and view results in task logs.',
sslSyncTitle1: 'Select certificates',
sslSyncContent1: 'Browse certificate details on the source node and select certificates to synchronize.',
sslSyncTitle2: 'Synchronize across nodes',
sslSyncContent2:
'Synchronize selected certificates to target nodes, automatically excluding the source node.',
sslSyncTitle3: 'Confirm sync tasks',
sslSyncContent3: 'Review target nodes and certificates before execution, then view results in task logs.',
phpExtensionsSyncTitle1: 'Select a template',
phpExtensionsSyncContent1:
'Browse template names and extension lists on the source node and select a template to synchronize.',
phpExtensionsSyncTitle2: 'Synchronize templates',
phpExtensionsSyncContent2:
'Synchronize the selected template to target nodes, automatically excluding the source node.',
phpExtensionsSyncTitle3: 'Check existing templates',
phpExtensionsSyncContent3:
'Skip templates with identical names and content. Report conflicts without overwriting different content.',
fileExchangeTitle1: 'Key Authentication Transmission',
fileExchangeContent1: 'Authenticate via SSH keys to ensure transmission security.',
fileExchangeTitle2: 'Efficient File Synchronization',
@@ -7550,6 +7586,13 @@ const message = {
imageBatchConfirmPrefix: 'About to transfer {0} images to {1} nodes, for a total of {2} tasks.',
ssl: 'Certificate Sync',
phpExtensions: 'PHP Extension Template Sync',
phpExtensionsTitle: 'Extension Template Sync',
phpExtensionsDesc: 'Select a source node to sync its PHP extension template to target nodes.',
phpExtensionsList: 'PHP extensions',
phpExtensionsConfirm: 'Sync {0} PHP extension template(s) to {1} node(s).',
phpExtensionsConflictHint:
'Identical templates are skipped. Same-name templates with different content are reported as conflicts and will not be overwritten.',
sslBatchDesc:
'After selecting a source node, sync the specified certificate into the current panel environment.',
sslBatchConfirmPrefix: 'About to sync {0} certificates to {1} nodes, for a total of {2} tasks.',
+7
View File
@@ -7667,6 +7667,13 @@ const message = {
imageBatchConfirmPrefix: 'Se enviarán {0} imágenes a {1} nodos, creando {2} tareas.',
ssl: 'Sincronización de certificados',
phpExtensions: 'PHP Extension Template Sync',
phpExtensionsTitle: 'Extension Template Sync',
phpExtensionsDesc: 'Select a source node to sync its PHP extension template to target nodes.',
phpExtensionsList: 'PHP extensions',
phpExtensionsConfirm: 'Sync {0} PHP extension template(s) to {1} node(s).',
phpExtensionsConflictHint:
'Identical templates are skipped. Same-name templates with different content are reported as conflicts and will not be overwritten.',
sslBatchDesc:
'Después de seleccionar el nodo de origen, los certificados especificados pueden sincronizarse con el entorno actual del panel.',
sslBatchConfirmPrefix: 'Se sincronizarán {0} certificados a {1} nodos, creando {2} tareas.',
+7
View File
@@ -7508,6 +7508,13 @@ const message = {
imageBatchConfirmPrefix: 'در حال انتقال {0} تصویر به {1} گره، مجموعاً {2} وظیفه.',
ssl: 'همگام‌سازی گواهی',
phpExtensions: 'PHP Extension Template Sync',
phpExtensionsTitle: 'Extension Template Sync',
phpExtensionsDesc: 'Select a source node to sync its PHP extension template to target nodes.',
phpExtensionsList: 'PHP extensions',
phpExtensionsConfirm: 'Sync {0} PHP extension template(s) to {1} node(s).',
phpExtensionsConflictHint:
'Identical templates are skipped. Same-name templates with different content are reported as conflicts and will not be overwritten.',
sslBatchDesc: 'پس از انتخاب یک گره منبع، گواهی مشخص شده را به محیط پنل فعلی همگام‌سازی کنید.',
sslBatchConfirmPrefix: 'در حال همگام‌سازی {0} گواهی به {1} گره، مجموعاً {2} وظیفه.',
+7
View File
@@ -7538,6 +7538,13 @@ const message = {
'{0} 個のイメージを {1} 個のノードへ転送します。合計 {2} 件のタスクが作成されます。',
ssl: '証明書同期',
phpExtensions: 'PHP Extension Template Sync',
phpExtensionsTitle: 'Extension Template Sync',
phpExtensionsDesc: 'Select a source node to sync its PHP extension template to target nodes.',
phpExtensionsList: 'PHP extensions',
phpExtensionsConfirm: 'Sync {0} PHP extension template(s) to {1} node(s).',
phpExtensionsConflictHint:
'Identical templates are skipped. Same-name templates with different content are reported as conflicts and will not be overwritten.',
sslBatchDesc: 'ソースノードを選択すると、指定した証明書を現在のパネル環境へ同期できます。',
sslBatchConfirmPrefix: '{0} 件の証明書を {1} 個のノードへ同期します。合計 {2} 件のタスクが作成されます。',
+7
View File
@@ -7390,6 +7390,13 @@ const message = {
imageBatchConfirmPrefix: '{0}개의 이미지를 {1}개 노드로 전달합니다. 총 {2}개의 작업이 생성됩니다.',
ssl: '인증서 동기화',
phpExtensions: 'PHP Extension Template Sync',
phpExtensionsTitle: 'Extension Template Sync',
phpExtensionsDesc: 'Select a source node to sync its PHP extension template to target nodes.',
phpExtensionsList: 'PHP extensions',
phpExtensionsConfirm: 'Sync {0} PHP extension template(s) to {1} node(s).',
phpExtensionsConflictHint:
'Identical templates are skipped. Same-name templates with different content are reported as conflicts and will not be overwritten.',
sslBatchDesc: '원본 노드를 선택한 후 지정된 인증서를 현재 패널 환경으로 동기화할 수 있습니다.',
sslBatchConfirmPrefix: '{0}개의 인증서를 {1}개 노드로 동기화합니다. 총 {2}개의 작업이 생성됩니다.',
+7
View File
@@ -7302,6 +7302,13 @@ const message = {
imageBatchConfirmPrefix: 'ກຳລັງຈະໂອນ {0} images ໄປຫາ {1} ໂນດ, ລວມທັງໝົດ {2} ງານ.',
ssl: 'ຊິງໂຄຣໄນໃບຢັ້ງຢືນ',
phpExtensions: 'PHP Extension Template Sync',
phpExtensionsTitle: 'Extension Template Sync',
phpExtensionsDesc: 'Select a source node to sync its PHP extension template to target nodes.',
phpExtensionsList: 'PHP extensions',
phpExtensionsConfirm: 'Sync {0} PHP extension template(s) to {1} node(s).',
phpExtensionsConflictHint:
'Identical templates are skipped. Same-name templates with different content are reported as conflicts and will not be overwritten.',
sslBatchDesc: 'ເລືອກໂນດຕົ້ນທາງ ແລະ ຊິງໂຄຣໄນໃບຢັ້ງຢືນທີ່ກຳນົດເຂົ້າມາໃນພາແນລປັດຈຸບັນ.',
sslBatchConfirmPrefix: 'ກຳລັງຈະຊິງໂຄຣໄນ {0} ໃບຢັ້ງຢືນ ໄປຫາ {1} ໂນດ, ລວມທັງໝົດ {2} ງານ.',
+7
View File
@@ -7660,6 +7660,13 @@ const message = {
imageBatchConfirmPrefix: 'Akan memindahkan {0} imej ke {1} nod, dengan jumlah {2} tugasan.',
ssl: 'Penyegerakan Sijil',
phpExtensions: 'PHP Extension Template Sync',
phpExtensionsTitle: 'Extension Template Sync',
phpExtensionsDesc: 'Select a source node to sync its PHP extension template to target nodes.',
phpExtensionsList: 'PHP extensions',
phpExtensionsConfirm: 'Sync {0} PHP extension template(s) to {1} node(s).',
phpExtensionsConflictHint:
'Identical templates are skipped. Same-name templates with different content are reported as conflicts and will not be overwritten.',
sslBatchDesc:
'Selepas memilih nod sumber, sijil yang ditentukan boleh disegerakkan ke persekitaran panel semasa.',
sslBatchConfirmPrefix: 'Akan menyegerakkan {0} sijil ke {1} nod, dengan jumlah {2} tugasan.',
+7
View File
@@ -7697,6 +7697,13 @@ const message = {
imageBatchConfirmPrefix: 'Serão encaminhadas {0} imagens para {1} nós, totalizando {2} tarefas.',
ssl: 'Sincronização de Certificados',
phpExtensions: 'PHP Extension Template Sync',
phpExtensionsTitle: 'Extension Template Sync',
phpExtensionsDesc: 'Select a source node to sync its PHP extension template to target nodes.',
phpExtensionsList: 'PHP extensions',
phpExtensionsConfirm: 'Sync {0} PHP extension template(s) to {1} node(s).',
phpExtensionsConflictHint:
'Identical templates are skipped. Same-name templates with different content are reported as conflicts and will not be overwritten.',
sslBatchDesc:
'Após selecionar o nó de origem, os certificados especificados podem ser sincronizados com o ambiente atual do painel.',
sslBatchConfirmPrefix: 'Serão sincronizados {0} certificados para {1} nós, totalizando {2} tarefas.',
+7
View File
@@ -7676,6 +7676,13 @@ const message = {
imageBatchConfirmPrefix: 'Будет передано {0} образов на {1} узлов, всего {2} задач.',
ssl: 'Синхронизация сертификатов',
phpExtensions: 'PHP Extension Template Sync',
phpExtensionsTitle: 'Extension Template Sync',
phpExtensionsDesc: 'Select a source node to sync its PHP extension template to target nodes.',
phpExtensionsList: 'PHP extensions',
phpExtensionsConfirm: 'Sync {0} PHP extension template(s) to {1} node(s).',
phpExtensionsConflictHint:
'Identical templates are skipped. Same-name templates with different content are reported as conflicts and will not be overwritten.',
sslBatchDesc:
'После выбора исходного узла указанные сертификаты можно синхронизировать с текущей средой панели.',
sslBatchConfirmPrefix: 'Будет синхронизировано {0} сертификатов на {1} узлов, всего {2} задач.',
+7
View File
@@ -7673,6 +7673,13 @@ const message = {
imageBatchConfirmPrefix: '{0} imaj {1} düğüme aktarılacak, toplam {2} görev oluşturulacak.',
ssl: 'Sertifika Senkronizasyonu',
phpExtensions: 'PHP Extension Template Sync',
phpExtensionsTitle: 'Extension Template Sync',
phpExtensionsDesc: 'Select a source node to sync its PHP extension template to target nodes.',
phpExtensionsList: 'PHP extensions',
phpExtensionsConfirm: 'Sync {0} PHP extension template(s) to {1} node(s).',
phpExtensionsConflictHint:
'Identical templates are skipped. Same-name templates with different content are reported as conflicts and will not be overwritten.',
sslBatchDesc:
'Kaynak düğüm seçildikten sonra belirtilen sertifikalar mevcut panel ortamına senkronize edilebilir.',
sslBatchConfirmPrefix: '{0} sertifika {1} düğüme senkronize edilecek, toplam {2} görev oluşturulacak.',
+34
View File
@@ -3747,6 +3747,10 @@ const message = {
node: '升級商業版可以使用 1Panel 管理多台 Linux 伺服器。',
nodeApp: '升級商業版可以統一升級多節點應用版本,無需手動切換節點。',
nodeDashboard: '升級商業版可以集中管理多節點應用、網站、資料庫和計劃任務。',
imageExchange: '升級商業版可以在多個節點之間批次傳輸容器映像。',
appExchange: '升級商業版可以將已安裝的應用程式遷移到目標節點,並確認所需的關聯資源。',
sslSync: '升級商業版可以將來源節點的憑證同步到一個或多個目標節點。',
phpExtensionsSync: '升級商業版可以將 PHP 擴充範本同步到一個或多個目標節點。',
fileExchange: '升級商業版可以在多台伺服器之間快速傳輸檔案。',
app: '升級商業版可透過手機APP,檢視服務資訊、異常監控等。',
cluster: '升級商業版可以管理 MySQL/Postgres/Redis 主從叢集。',
@@ -6539,6 +6543,30 @@ const message = {
nodeContent3: '即時掌握各節點執行狀態',
nodeTitle4: '快速遠端連接',
nodeContent4: '一鍵直連節點遠端終端',
imageExchangeTitle1: '選擇來源映像',
imageExchangeContent1: '檢視來源節點的容器映像,選擇需要傳輸的映像。',
imageExchangeTitle2: '批次分發映像',
imageExchangeContent2: '將選取的映像傳輸到一個或多個目標節點。',
imageExchangeTitle3: '檢視傳輸日誌',
imageExchangeContent3: '透過任務日誌檢視映像傳輸進度和執行結果。',
appExchangeTitle1: '選擇來源應用程式',
appExchangeContent1: '檢視來源節點已安裝的應用程式,選擇需要遷移的應用程式。',
appExchangeTitle2: '確認資源相依性',
appExchangeContent2: '遷移前確認目標節點及應用程式所需的資料庫等關聯資源。',
appExchangeTitle3: '執行應用程式遷移',
appExchangeContent3: '將應用程式備份傳輸到目標節點進行還原,並透過任務日誌檢視結果。',
sslSyncTitle1: '選擇來源憑證',
sslSyncContent1: '檢視來源節點的憑證資訊,選擇需要同步的憑證。',
sslSyncTitle2: '多節點憑證同步',
sslSyncContent2: '將選取的憑證同步到目標節點,自動排除來源節點。',
sslSyncTitle3: '確認同步任務',
sslSyncContent3: '執行前確認目標節點和憑證,透過任務日誌檢視同步結果。',
phpExtensionsSyncTitle1: '選擇擴充範本',
phpExtensionsSyncContent1: '檢視來源節點的範本名稱和擴充清單,選擇需要同步的範本。',
phpExtensionsSyncTitle2: '多節點範本同步',
phpExtensionsSyncContent2: '將選取的擴充範本同步到目標節點,自動排除來源節點。',
phpExtensionsSyncTitle3: '同名範本檢查',
phpExtensionsSyncContent3: '同名且內容一致的範本會略過;內容不同則提示衝突,不會覆寫。',
fileExchangeTitle1: '金鑰認證傳輸',
fileExchangeContent1: '通過 SSH 金鑰進行身份驗證,確保傳輸的安全性。',
fileExchangeTitle2: '高效檔案同步',
@@ -7022,6 +7050,12 @@ const message = {
imageBatchConfirmPrefix: '即將把 {0} 張映像轉發到 {1} 個節點,共 {2} 個任務。',
ssl: '證書同步',
phpExtensions: 'PHP 擴充模版同步',
phpExtensionsTitle: '擴充模版同步',
phpExtensionsDesc: '選擇來源節點後,可將指定 PHP 擴充模版同步至目標節點。',
phpExtensionsList: '擴充清單',
phpExtensionsConfirm: '即將把 {0} 個 PHP 擴充模版同步至 {1} 個目標節點。',
phpExtensionsConflictHint: '同名且內容一致的模版將略過;同名但內容不同的模版將提示衝突,不會覆蓋。',
sslBatchDesc: '選擇來源節點後,可將指定證書同步到目前面板環境。',
sslBatchConfirmPrefix: '即將把 {0} 張證書同步到 {1} 個節點,共 {2} 個任務。',
+34
View File
@@ -3777,6 +3777,10 @@ const message = {
node: '升级商业版可以使用 1Panel 管理多台 linux 服务器。',
nodeApp: '升级商业版可以统一升级多节点应用版本,无需手动切换节点。',
nodeDashboard: '升级商业版可以集中管理多节点应用、网站、数据库和计划任务。',
imageExchange: '升级商业版可以在多个节点之间批量传输容器镜像。',
appExchange: '升级商业版可以将已安装的应用迁移到目标节点,并确认所需的关联资源。',
sslSync: '升级商业版可以将源节点的证书同步到一个或多个目标节点。',
phpExtensionsSync: '升级商业版可以将 PHP 扩展模版同步到一个或多个目标节点。',
fileExchange: '升级商业版可以在多台服务器之间快速传输文件。',
app: '升级商业版可通过手机 APP,查看服务信息、异常监控等。',
cluster: '升级商业版可以管理 MySQL/Postgres/Redis 主从集群。',
@@ -5494,6 +5498,30 @@ const message = {
nodeTitle4: '快速远程连接',
nodeContent4: '一键直连节点远程终端',
imageExchangeTitle1: '选择源镜像',
imageExchangeContent1: '查看源节点的容器镜像,选择需要传输的镜像。',
imageExchangeTitle2: '批量分发镜像',
imageExchangeContent2: '将选中的镜像传输到一个或多个目标节点。',
imageExchangeTitle3: '查看传输日志',
imageExchangeContent3: '通过任务日志查看镜像传输进度和执行结果。',
appExchangeTitle1: '选择源应用',
appExchangeContent1: '查看源节点已安装的应用,选择需要迁移的应用。',
appExchangeTitle2: '确认资源依赖',
appExchangeContent2: '迁移前确认目标节点及应用所需的数据库等关联资源。',
appExchangeTitle3: '执行应用迁移',
appExchangeContent3: '将应用备份传输到目标节点进行恢复,并通过任务日志查看结果。',
sslSyncTitle1: '选择源证书',
sslSyncContent1: '查看源节点的证书信息,选择需要同步的证书。',
sslSyncTitle2: '多节点证书同步',
sslSyncContent2: '将选中的证书同步到目标节点,自动排除源节点。',
sslSyncTitle3: '确认同步任务',
sslSyncContent3: '执行前确认目标节点和证书,通过任务日志查看同步结果。',
phpExtensionsSyncTitle1: '选择扩展模版',
phpExtensionsSyncContent1: '查看源节点的模版名称和扩展列表,选择需要同步的模版。',
phpExtensionsSyncTitle2: '多节点模版同步',
phpExtensionsSyncContent2: '将选中的扩展模版同步到目标节点,自动排除源节点。',
phpExtensionsSyncTitle3: '同名模版检查',
phpExtensionsSyncContent3: '同名且内容一致的模版会跳过;内容不同则提示冲突,不会覆盖。',
fileExchangeTitle1: '密钥认证传输',
fileExchangeContent1: '通过 SSH 密钥进行身份验证,确保传输的安全性。',
fileExchangeTitle2: '高效文件同步',
@@ -7079,6 +7107,12 @@ const message = {
imageBatchConfirmPrefix: '即将把 {0} 张镜像转发到 {1} 个节点,共 {2} 个任务。',
ssl: '证书同步',
phpExtensions: 'PHP 扩展模版同步',
phpExtensionsTitle: '扩展模版同步',
phpExtensionsDesc: '选择源节点后,可将指定 PHP 扩展模版同步到目标节点。',
phpExtensionsList: '扩展列表',
phpExtensionsConfirm: '即将把 {0} 个 PHP 扩展模版同步到 {1} 个目标节点。',
phpExtensionsConflictHint: '同名且内容一致的模版将跳过;同名但内容不同的模版将提示冲突,不会覆盖。',
sslBatchDesc: '选择源节点后,可将指定证书同步到当前面板环境。',
sslBatchConfirmPrefix: '即将把 {0} 张证书同步到 {1} 个节点,共 {2} 个任务。',