From eac7296a5d38d7438b402d99a62b73606208b157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=B5=D1=81=D1=82=D0=B5=D1=80=D0=BE=D0=B2=20=D0=A0?= =?UTF-8?q?=D1=83=D1=81=D0=BB=D0=B0=D0=BD?= Date: Wed, 22 Apr 2026 16:54:54 +0300 Subject: [PATCH] fix: copy clients modal not opening --- web/controller/inbound.go | 3 +- web/html/inbounds.html | 42 ++++++++++++++++++++++++++-- web/service/inbound.go | 14 +++++++--- web/translation/translate.en_US.toml | 5 ++++ web/translation/translate.ru_RU.toml | 5 ++++ 5 files changed, 61 insertions(+), 8 deletions(-) diff --git a/web/controller/inbound.go b/web/controller/inbound.go index 471a693b..ee024cc6 100644 --- a/web/controller/inbound.go +++ b/web/controller/inbound.go @@ -58,6 +58,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) { type CopyInboundClientsRequest struct { SourceInboundID int `form:"sourceInboundId" json:"sourceInboundId"` ClientEmails []string `form:"clientEmails" json:"clientEmails"` + Flow string `form:"flow" json:"flow"` } // getInbounds retrieves the list of inbounds for the logged-in user. @@ -285,7 +286,7 @@ func (a *InboundController) copyInboundClients(c *gin.Context) { return } - result, needRestart, err := a.inboundService.CopyInboundClients(targetID, req.SourceInboundID, req.ClientEmails) + result, needRestart, err := a.inboundService.CopyInboundClients(targetID, req.SourceInboundID, req.ClientEmails, req.Flow) if err != nil { jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) return diff --git a/web/html/inbounds.html b/web/html/inbounds.html index d3e423a2..b8485702 100644 --- a/web/html/inbounds.html +++ b/web/html/inbounds.html @@ -826,6 +826,20 @@ +
+
{{ i18n "pages.client.copyFlowLabel" }}
+ + {{ i18n "none" }} + xtls-rprx-vision + xtls-rprx-vision-udp443 + +
+ {{ i18n "pages.client.copyFlowHint" }} +
+
{{ i18n "pages.client.copyEmailPreview" }}
@@ -849,6 +863,9 @@ title: '', targetInboundId: 0, targetInboundRemark: '', + targetProtocol: '', + showFlow: false, + flow: '', sourceInboundId: undefined, sources: [], sourceClients: [], @@ -861,8 +878,18 @@ const clients = app.getInboundClients(row) || []; return { id: row.id, label: `${row.remark} (${row.protocol}, ${clients.length})` }; }); + let showFlow = false; + try { + const targetInbound = targetDbInbound.toInbound(); + showFlow = !!(targetInbound && typeof targetInbound.canEnableTlsFlow === 'function' && targetInbound.canEnableTlsFlow()); + } catch (e) { + showFlow = false; + } copyClientsModal.targetInboundId = targetDbInbound.id; copyClientsModal.targetInboundRemark = targetDbInbound.remark; + copyClientsModal.targetProtocol = targetDbInbound.protocol; + copyClientsModal.showFlow = showFlow; + copyClientsModal.flow = ''; copyClientsModal.title = `{{ i18n "pages.client.copyToInbound" }} ${targetDbInbound.remark}`; copyClientsModal.sources = sources; copyClientsModal.sourceInboundId = undefined; @@ -925,14 +952,23 @@ sourceInboundId: copyClientsModal.sourceInboundId, clientEmails: copyClientsModal.selectedEmails, }; + if (copyClientsModal.showFlow && copyClientsModal.flow) { + payload.flow = copyClientsModal.flow; + } try { const msg = await HttpUtil.post(`/panel/api/inbounds/${copyClientsModal.targetInboundId}/copyClients`, payload); if (!msg || !msg.success) return; const obj = msg.obj || {}; const addedCount = (obj.added || []).length; - const skippedCount = (obj.skipped || []).length; - const errorCount = (obj.errors || []).length; - app.$message.success(`{{ i18n "pages.client.copyResult" }}: +${addedCount}, ~${skippedCount}, !${errorCount}`); + const errorList = obj.errors || []; + if (addedCount > 0) { + app.$message.success(`{{ i18n "pages.client.copyResultSuccess" }}: ${addedCount}`); + } else { + app.$message.warning('{{ i18n "pages.client.copyResultNone" }}'); + } + if (errorList.length > 0) { + app.$message.error(`{{ i18n "pages.client.copyResultErrors" }}: ${errorList.join('; ')}`); + } copyClientsModal.close(); await app.getDBInbounds(); } finally { diff --git a/web/service/inbound.go b/web/service/inbound.go index 823ece7f..82b39e58 100644 --- a/web/service/inbound.go +++ b/web/service/inbound.go @@ -801,7 +801,7 @@ func (s *InboundService) generateRandomCredential(targetProtocol model.Protocol) } } -func (s *InboundService) buildTargetClientFromSource(source model.Client, targetProtocol model.Protocol, email string) (model.Client, error) { +func (s *InboundService) buildTargetClientFromSource(source model.Client, targetProtocol model.Protocol, email string, flow string) (model.Client, error) { nowTs := time.Now().UnixMilli() target := source target.Email = email @@ -811,10 +811,16 @@ func (s *InboundService) buildTargetClientFromSource(source model.Client, target target.ID = "" target.Password = "" target.Auth = "" + target.Flow = "" switch targetProtocol { - case model.VMESS, model.VLESS: + case model.VMESS: target.ID = s.generateRandomCredential(targetProtocol) + case model.VLESS: + target.ID = s.generateRandomCredential(targetProtocol) + if flow == "xtls-rprx-vision" || flow == "xtls-rprx-vision-udp443" { + target.Flow = flow + } case model.Trojan, model.Shadowsocks: target.Password = s.generateRandomCredential(targetProtocol) case model.Hysteria: @@ -840,7 +846,7 @@ func (s *InboundService) nextAvailableCopiedEmail(originalEmail string, targetID } } -func (s *InboundService) CopyInboundClients(targetInboundID int, sourceInboundID int, clientEmails []string) (*CopyClientsResult, bool, error) { +func (s *InboundService) CopyInboundClients(targetInboundID int, sourceInboundID int, clientEmails []string, flow string) (*CopyClientsResult, bool, error) { result := &CopyClientsResult{ Added: []string{}, Skipped: []string{}, @@ -913,7 +919,7 @@ func (s *InboundService) CopyInboundClients(targetInboundID int, sourceInboundID } targetEmail := s.nextAvailableCopiedEmail(originalEmail, targetInboundID, occupiedEmails) - targetClient, buildErr := s.buildTargetClientFromSource(sourceClient, targetInbound.Protocol, targetEmail) + targetClient, buildErr := s.buildTargetClientFromSource(sourceClient, targetInbound.Protocol, targetEmail, flow) if buildErr != nil { result.Errors = append(result.Errors, fmt.Sprintf("%s: %v", originalEmail, buildErr)) continue diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml index 497717ef..45186187 100644 --- a/web/translation/translate.en_US.toml +++ b/web/translation/translate.en_US.toml @@ -305,6 +305,11 @@ "copyEmailPreview" = "Resulting email preview" "copySelectSourceFirst" = "Please select a source inbound first." "copyResult" = "Copy result" +"copyResultSuccess" = "Copied successfully" +"copyResultNone" = "Nothing to copy: no clients selected or source is empty" +"copyResultErrors" = "Copy errors" +"copyFlowLabel" = "Flow for new clients (VLESS)" +"copyFlowHint" = "Applied to all copied clients. Leave empty to skip." "selectAll" = "Select all" "clearAll" = "Clear all" "method" = "Method" diff --git a/web/translation/translate.ru_RU.toml b/web/translation/translate.ru_RU.toml index 80d22791..5bf89dfd 100644 --- a/web/translation/translate.ru_RU.toml +++ b/web/translation/translate.ru_RU.toml @@ -305,6 +305,11 @@ "copyEmailPreview" = "Предпросмотр итоговых email" "copySelectSourceFirst" = "Сначала выберите источник." "copyResult" = "Результат копирования" +"copyResultSuccess" = "Успешно скопировано" +"copyResultNone" = "Нечего копировать: ни одного клиента не выбрано или список источника пуст" +"copyResultErrors" = "Ошибки при копировании" +"copyFlowLabel" = "Flow для новых клиентов (VLESS)" +"copyFlowHint" = "Применится ко всем копируемым клиентам. Оставьте пустым, чтобы не задавать." "selectAll" = "Выбрать всех" "clearAll" = "Снять всё" "method" = "Метод"