diff --git a/frontend/src/pages/clients/ClientInfoModal.tsx b/frontend/src/pages/clients/ClientInfoModal.tsx index c3cb87df6..b755a571f 100644 --- a/frontend/src/pages/clients/ClientInfoModal.tsx +++ b/frontend/src/pages/clients/ClientInfoModal.tsx @@ -357,7 +357,7 @@ export default function ClientInfoModal({ const parts = parseLinkParts(link); const fallback = `${t('pages.clients.link')} ${idx + 1}`; const rowTitle = (parts && linkMetaText(parts)) || fallback; - const qrRemark = [parts?.remark, client.email].filter(Boolean).join('-') || rowTitle; + const qrRemark = parts?.remark || rowTitle; const canQr = !isPostQuantumLink(link); return (
diff --git a/internal/sub/characterization_test.go b/internal/sub/characterization_test.go index acccb567c..416baba31 100644 --- a/internal/sub/characterization_test.go +++ b/internal/sub/characterization_test.go @@ -45,8 +45,8 @@ func TestChar_C1_VlessExternalProxy(t *testing.T) { }` s := &SubService{} got := s.genVlessLink(charVlessInbound(stream), "user") - want := "vless://11111111-2222-4333-8444-555555555555@cdn1.example.com:8443?alpn=h3%2Ch2&encryption=none&fp=firefox&pcs=UElO&security=tls&sni=sni1.example.com&type=tcp#char-R1\n" + - "vless://11111111-2222-4333-8444-555555555555@cdn2.example.com:80?encryption=none&security=none&type=tcp#char-R2" + want := "vless://11111111-2222-4333-8444-555555555555@cdn1.example.com:8443?alpn=h3%2Ch2&encryption=none&fp=firefox&pcs=UElO&security=tls&sni=sni1.example.com&type=tcp#char-R1-user\n" + + "vless://11111111-2222-4333-8444-555555555555@cdn2.example.com:80?encryption=none&security=none&type=tcp#char-R2-user" if got != want { t.Fatalf("C1 mismatch.\n got: %q\nwant: %q", got, want) } @@ -106,8 +106,8 @@ func TestChar_C2_VmessExternalProxy(t *testing.T) { } s := &SubService{} got := s.genVmessLink(in, "user") - want := "vmess://ewogICJhZGQiOiAidm0xLmV4YW1wbGUuY29tIiwKICAiYWxwbiI6ICJoMiIsCiAgImZwIjogImNocm9tZSIsCiAgImlkIjogIjExMTExMTExLTIyMjItNDMzMy04NDQ0LTU1NTU1NTU1NTU1NSIsCiAgIm5ldCI6ICJ0Y3AiLAogICJwb3J0IjogODQ0MywKICAicHMiOiAiY2hhci1WMSIsCiAgInNjeSI6ICJhdXRvIiwKICAic25pIjogInNuaTEuZXhhbXBsZS5jb20iLAogICJ0bHMiOiAidGxzIiwKICAidHlwZSI6ICJub25lIiwKICAidiI6ICIyIgp9\n" + - "vmess://ewogICJhZGQiOiAidm0yLmV4YW1wbGUuY29tIiwKICAiaWQiOiAiMTExMTExMTEtMjIyMi00MzMzLTg0NDQtNTU1NTU1NTU1NTU1IiwKICAibmV0IjogInRjcCIsCiAgInBvcnQiOiA4MCwKICAicHMiOiAiY2hhci1WMiIsCiAgInNjeSI6ICJhdXRvIiwKICAidGxzIjogIm5vbmUiLAogICJ0eXBlIjogIm5vbmUiLAogICJ2IjogIjIiCn0=" + want := "vmess://ewogICJhZGQiOiAidm0xLmV4YW1wbGUuY29tIiwKICAiYWxwbiI6ICJoMiIsCiAgImZwIjogImNocm9tZSIsCiAgImlkIjogIjExMTExMTExLTIyMjItNDMzMy04NDQ0LTU1NTU1NTU1NTU1NSIsCiAgIm5ldCI6ICJ0Y3AiLAogICJwb3J0IjogODQ0MywKICAicHMiOiAiY2hhci1WMS11c2VyIiwKICAic2N5IjogImF1dG8iLAogICJzbmkiOiAic25pMS5leGFtcGxlLmNvbSIsCiAgInRscyI6ICJ0bHMiLAogICJ0eXBlIjogIm5vbmUiLAogICJ2IjogIjIiCn0=\n" + + "vmess://ewogICJhZGQiOiAidm0yLmV4YW1wbGUuY29tIiwKICAiaWQiOiAiMTExMTExMTEtMjIyMi00MzMzLTg0NDQtNTU1NTU1NTU1NTU1IiwKICAibmV0IjogInRjcCIsCiAgInBvcnQiOiA4MCwKICAicHMiOiAiY2hhci1WMi11c2VyIiwKICAic2N5IjogImF1dG8iLAogICJ0bHMiOiAibm9uZSIsCiAgInR5cGUiOiAibm9uZSIsCiAgInYiOiAiMiIKfQ==" if got != want { t.Fatalf("C2 mismatch.\n got: %q\nwant: %q", got, want) } @@ -143,7 +143,7 @@ func TestChar_C3_TrojanExternalProxy(t *testing.T) { } s := &SubService{} got := s.genTrojanLink(in, "user") - want := "trojan://p%40ss%2Fw%2Brd%3D@tj.example.com:8443?fp=chrome&security=tls&sni=tj.sni&type=tcp#char-TJ" + want := "trojan://p%40ss%2Fw%2Brd%3D@tj.example.com:8443?fp=chrome&security=tls&sni=tj.sni&type=tcp#char-TJ-user" if got != want { t.Fatalf("C3-Trojan mismatch.\n got: %q\nwant: %q", got, want) } @@ -168,7 +168,7 @@ func TestChar_C3_ShadowsocksExternalProxy(t *testing.T) { } s := &SubService{} got := s.genShadowsocksLink(in, "user") - want := "ss://2022-blake3-aes-256-gcm:inboundpw:clientpw@ss.example.com:8443?fp=chrome&security=tls&sni=ss.sni&type=tcp#char-SS" + want := "ss://2022-blake3-aes-256-gcm:inboundpw:clientpw@ss.example.com:8443?fp=chrome&security=tls&sni=ss.sni&type=tcp#char-SS-user" if got != want { t.Fatalf("C3-SS mismatch.\n got: %q\nwant: %q", got, want) } diff --git a/internal/sub/endpoint_test.go b/internal/sub/endpoint_test.go index b420982ce..230ef29f5 100644 --- a/internal/sub/endpoint_test.go +++ b/internal/sub/endpoint_test.go @@ -83,8 +83,8 @@ func TestBuildEndpointLinks_ParamForm(t *testing.T) { func(dest string, port int) string { return fmt.Sprintf("vless://uid@%s", joinHostPort(dest, port)) }, func(e ShareEndpoint) string { return s.genRemark(in, "user", e.Remark, "") }, ) - want := "vless://uid@a.example.com:8443?fp=chrome&security=tls&sni=a.sni&type=tcp#ib-A\n" + - "vless://uid@b.example.com:80?security=none&type=tcp#ib-B" + want := "vless://uid@a.example.com:8443?fp=chrome&security=tls&sni=a.sni&type=tcp#ib-A-user\n" + + "vless://uid@b.example.com:80?security=none&type=tcp#ib-B-user" if got != want { t.Fatalf("N3 mismatch.\n got: %q\nwant: %q", got, want) } @@ -105,8 +105,8 @@ func TestBuildEndpointVmessLinks(t *testing.T) { externalProxyToEndpoint(map[string]any{"forceTls": "none", "dest": "b.example.com", "port": float64(80), "remark": "B"}), } got := s.buildEndpointVmessLinks(eps, baseObj, in, "user", "tcp") - want := "vmess://ewogICJhZGQiOiAiYS5leGFtcGxlLmNvbSIsCiAgImFscG4iOiAiaDIiLAogICJmcCI6ICJjaHJvbWUiLAogICJpZCI6ICJ1aWQiLAogICJuZXQiOiAidGNwIiwKICAicG9ydCI6IDg0NDMsCiAgInBzIjogImliLUEiLAogICJzY3kiOiAiYXV0byIsCiAgInNuaSI6ICJhLnNuaSIsCiAgInRscyI6ICJ0bHMiLAogICJ0eXBlIjogIm5vbmUiLAogICJ2IjogIjIiCn0=\n" + - "vmess://ewogICJhZGQiOiAiYi5leGFtcGxlLmNvbSIsCiAgImlkIjogInVpZCIsCiAgIm5ldCI6ICJ0Y3AiLAogICJwb3J0IjogODAsCiAgInBzIjogImliLUIiLAogICJzY3kiOiAiYXV0byIsCiAgInRscyI6ICJub25lIiwKICAidHlwZSI6ICJub25lIiwKICAidiI6ICIyIgp9" + want := "vmess://ewogICJhZGQiOiAiYS5leGFtcGxlLmNvbSIsCiAgImFscG4iOiAiaDIiLAogICJmcCI6ICJjaHJvbWUiLAogICJpZCI6ICJ1aWQiLAogICJuZXQiOiAidGNwIiwKICAicG9ydCI6IDg0NDMsCiAgInBzIjogImliLUEtdXNlciIsCiAgInNjeSI6ICJhdXRvIiwKICAic25pIjogImEuc25pIiwKICAidGxzIjogInRscyIsCiAgInR5cGUiOiAibm9uZSIsCiAgInYiOiAiMiIKfQ==\n" + + "vmess://ewogICJhZGQiOiAiYi5leGFtcGxlLmNvbSIsCiAgImlkIjogInVpZCIsCiAgIm5ldCI6ICJ0Y3AiLAogICJwb3J0IjogODAsCiAgInBzIjogImliLUItdXNlciIsCiAgInNjeSI6ICJhdXRvIiwKICAidGxzIjogIm5vbmUiLAogICJ0eXBlIjogIm5vbmUiLAogICJ2IjogIjIiCn0=" if got != want { t.Fatalf("N4 mismatch.\n got: %q\nwant: %q", got, want) } diff --git a/internal/sub/remark_vars.go b/internal/sub/remark_vars.go index 29eac0f8a..e408926d3 100644 --- a/internal/sub/remark_vars.go +++ b/internal/sub/remark_vars.go @@ -524,13 +524,13 @@ func (s *SubService) genTemplatedRemark(inbound *model.Inbound, client model.Cli return ctx.configName() } -// genHostRemark builds one host endpoint's remark for a specific client. The -// config name is always the inbound's own remark; the host's remark is surfaced -// only through the {{HOST}} token. In the subscription body the rest of the -// remark template still applies; displays show just the config name. +// genHostRemark builds one host endpoint's remark for a specific client. In the +// subscription body the {{HOST}} token carries the host's remark and the rest of +// the template still applies; displays show the config name, host and email. func (s *SubService) genHostRemark(inbound *model.Inbound, client model.Client, hostRemark string, transport string) string { if !s.subscriptionBody { - return remarkContext{inbound: inbound, hostRemark: hostRemark}.configName() + name := remarkContext{inbound: inbound, hostRemark: hostRemark}.configName() + return fallbackRemark(name, hostRemark, client.Email) } return s.genTemplatedRemark(inbound, client, hostRemark, transport) } diff --git a/internal/sub/remark_vars_test.go b/internal/sub/remark_vars_test.go index 6c8c43a23..4a567238d 100644 --- a/internal/sub/remark_vars_test.go +++ b/internal/sub/remark_vars_test.go @@ -206,12 +206,11 @@ func TestGenRemark_GlobalTemplate(t *testing.T) { } } -// With no template, genRemark composes the fallback model and adds no suffix. -func TestGenRemark_NoTemplate_NoSuffix(t *testing.T) { +func TestGenRemark_NoTemplate_AppendsEmail(t *testing.T) { s, inbound, _ := hostRemarkService("") got := s.genRemark(inbound, "john@example.com", "Relay", "") - if got != "DE-Relay" { - t.Fatalf("genRemark = %q, want %q (no suffix)", got, "DE-Relay") + if got != "DE-Relay-john@example.com" { + t.Fatalf("genRemark = %q, want %q", got, "DE-Relay-john@example.com") } } @@ -232,24 +231,17 @@ func TestUsageOnFirstLinkOnly(t *testing.T) { } } -// Outside the subscription body (panel link/QR displays, sub info page) the -// template is bypassed entirely — links show just the config name, with no -// per-client email or usage info. func TestRemarkInDisplayContext(t *testing.T) { s, inbound, client := hostRemarkService("{{INBOUND}}|📊{{TRAFFIC_LEFT}}|⏳{{DAYS_LEFT}}D") s.subscriptionBody = false - // A host link in a display shows only the config name — the inbound's remark, - // with no per-client email or usage info and the host remark ignored. - if got := s.genHostRemark(inbound, client, "CDN", ""); got != "DE" { - t.Fatalf("display host link = %q, want config name %q", got, "DE") + if got := s.genHostRemark(inbound, client, "CDN", ""); got != "DE-CDN-john@example.com" { + t.Fatalf("display host link = %q, want %q", got, "DE-CDN-john@example.com") } - // With no host remark, the config name is likewise the inbound's own remark. - if got := s.genHostRemark(inbound, client, "", ""); got != "DE" { - t.Fatalf("display host link (no host) = %q, want %q", got, "DE") + if got := s.genHostRemark(inbound, client, "", ""); got != "DE-john@example.com" { + t.Fatalf("display host link (no host) = %q, want %q", got, "DE-john@example.com") } - // genRemark (non-host) likewise drops the template in display context. - if got := s.genRemark(inbound, client.Email, "", ""); got != "DE" { - t.Fatalf("display genRemark = %q, want %q", got, "DE") + if got := s.genRemark(inbound, client.Email, "", ""); got != "DE-john@example.com" { + t.Fatalf("display genRemark = %q, want %q", got, "DE-john@example.com") } } diff --git a/internal/sub/service.go b/internal/sub/service.go index 76d9c26dd..a9e57b4b8 100644 --- a/internal/sub/service.go +++ b/internal/sub/service.go @@ -1641,24 +1641,17 @@ func (s *SubService) genRemark(inbound *model.Inbound, email string, extra strin if s.remarkTemplate != "" && s.subscriptionBody { return s.genTemplatedRemark(inbound, s.lookupClient(inbound, email), extra, transport) } - // Sub info page + panel link/QR displays: just the config name (no template, - // so no per-client email/usage leaks into the shown remark). - return fallbackRemark(inbound.Remark, extra) + return fallbackRemark(inbound.Remark, extra, email) } -// fallbackRemark is the minimal remark used only when no template is configured -// (an operator explicitly cleared it): the inbound remark and the host/extra -// remark joined by "-", skipping empties. The configurable remark model was -// removed in favour of the template, whose default already includes the email. -func fallbackRemark(inboundRemark, extra string) string { - switch { - case inboundRemark == "": - return extra - case extra == "": - return inboundRemark - default: - return inboundRemark + "-" + extra +func fallbackRemark(parts ...string) string { + out := make([]string, 0, len(parts)) + for _, p := range parts { + if p != "" { + out = append(out, p) + } } + return strings.Join(out, "-") } // findClientStats returns the inbound's traffic record for email, if present.