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.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" = "Метод"