feat(sub): auto-detect subscription format by User-Agent (Updated) (#5826)

* feat(settings): add subscription format controls

* feat(sub): auto-detect subscription formats

* fix(xray): validate balancer regexes before save

* Revert "fix(xray): validate balancer regexes before save"

This reverts commit 8a208ce71b.

* doc(endpoints): align indent spaces

* doc(settings): improve error message formatting in validateSubUserAgentRegex

- Use NewErrorf with proper formatting instead of NewError with string concatenation
- Add comment explaining the rationale for returning original pattern value
- This preserves the intentional design where empty input is stored as empty
  in the DB and inherited as the runtime default at read time

---------

Co-authored-by: Tomilla <5007859+Tomilla@users.noreply.github.com>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
Tomi lla
2026-07-14 19:01:40 +08:00
committed by GitHub
parent f2b17397f4
commit 129f50d92a
40 changed files with 1588 additions and 89 deletions
+10
View File
@@ -45,11 +45,13 @@ export const EXAMPLES: Record<string, unknown> = {
"smtpUsername": "",
"subAnnounce": "",
"subCertFile": "",
"subClashAutoDetect": false,
"subClashEnable": false,
"subClashEnableRouting": false,
"subClashPath": "",
"subClashRules": "",
"subClashURI": "",
"subClashUserAgentRegex": "",
"subDomain": "",
"subEnable": false,
"subEnableRouting": false,
@@ -57,12 +59,15 @@ export const EXAMPLES: Record<string, unknown> = {
"subHideSettings": false,
"subIncyEnableRouting": false,
"subIncyRoutingRules": "",
"subJsonAlwaysArray": false,
"subJsonAutoDetect": false,
"subJsonEnable": false,
"subJsonFinalMask": "",
"subJsonMux": "",
"subJsonPath": "",
"subJsonRules": "",
"subJsonURI": "",
"subJsonUserAgentRegex": "",
"subKeyFile": "",
"subListen": "",
"subPath": "",
@@ -150,11 +155,13 @@ export const EXAMPLES: Record<string, unknown> = {
"smtpUsername": "",
"subAnnounce": "",
"subCertFile": "",
"subClashAutoDetect": false,
"subClashEnable": false,
"subClashEnableRouting": false,
"subClashPath": "",
"subClashRules": "",
"subClashURI": "",
"subClashUserAgentRegex": "",
"subDomain": "",
"subEnable": false,
"subEnableRouting": false,
@@ -162,12 +169,15 @@ export const EXAMPLES: Record<string, unknown> = {
"subHideSettings": false,
"subIncyEnableRouting": false,
"subIncyRoutingRules": "",
"subJsonAlwaysArray": false,
"subJsonAutoDetect": false,
"subJsonEnable": false,
"subJsonFinalMask": "",
"subJsonMux": "",
"subJsonPath": "",
"subJsonRules": "",
"subJsonURI": "",
"subJsonUserAgentRegex": "",
"subKeyFile": "",
"subListen": "",
"subPath": "",
+40
View File
@@ -150,6 +150,9 @@ export const SCHEMAS: Record<string, unknown> = {
"subCertFile": {
"type": "string"
},
"subClashAutoDetect": {
"type": "boolean"
},
"subClashEnable": {
"type": "boolean"
},
@@ -165,6 +168,9 @@ export const SCHEMAS: Record<string, unknown> = {
"subClashURI": {
"type": "string"
},
"subClashUserAgentRegex": {
"type": "string"
},
"subDomain": {
"type": "string"
},
@@ -186,6 +192,12 @@ export const SCHEMAS: Record<string, unknown> = {
"subIncyRoutingRules": {
"type": "string"
},
"subJsonAlwaysArray": {
"type": "boolean"
},
"subJsonAutoDetect": {
"type": "boolean"
},
"subJsonEnable": {
"type": "boolean"
},
@@ -204,6 +216,9 @@ export const SCHEMAS: Record<string, unknown> = {
"subJsonURI": {
"type": "string"
},
"subJsonUserAgentRegex": {
"type": "string"
},
"subKeyFile": {
"type": "string"
},
@@ -365,11 +380,13 @@ export const SCHEMAS: Record<string, unknown> = {
"smtpUsername",
"subAnnounce",
"subCertFile",
"subClashAutoDetect",
"subClashEnable",
"subClashEnableRouting",
"subClashPath",
"subClashRules",
"subClashURI",
"subClashUserAgentRegex",
"subDomain",
"subEnable",
"subEnableRouting",
@@ -377,12 +394,15 @@ export const SCHEMAS: Record<string, unknown> = {
"subHideSettings",
"subIncyEnableRouting",
"subIncyRoutingRules",
"subJsonAlwaysArray",
"subJsonAutoDetect",
"subJsonEnable",
"subJsonFinalMask",
"subJsonMux",
"subJsonPath",
"subJsonRules",
"subJsonURI",
"subJsonUserAgentRegex",
"subKeyFile",
"subListen",
"subPath",
@@ -591,6 +611,9 @@ export const SCHEMAS: Record<string, unknown> = {
"subCertFile": {
"type": "string"
},
"subClashAutoDetect": {
"type": "boolean"
},
"subClashEnable": {
"type": "boolean"
},
@@ -606,6 +629,9 @@ export const SCHEMAS: Record<string, unknown> = {
"subClashURI": {
"type": "string"
},
"subClashUserAgentRegex": {
"type": "string"
},
"subDomain": {
"type": "string"
},
@@ -627,6 +653,12 @@ export const SCHEMAS: Record<string, unknown> = {
"subIncyRoutingRules": {
"type": "string"
},
"subJsonAlwaysArray": {
"type": "boolean"
},
"subJsonAutoDetect": {
"type": "boolean"
},
"subJsonEnable": {
"type": "boolean"
},
@@ -645,6 +677,9 @@ export const SCHEMAS: Record<string, unknown> = {
"subJsonURI": {
"type": "string"
},
"subJsonUserAgentRegex": {
"type": "string"
},
"subKeyFile": {
"type": "string"
},
@@ -813,11 +848,13 @@ export const SCHEMAS: Record<string, unknown> = {
"smtpUsername",
"subAnnounce",
"subCertFile",
"subClashAutoDetect",
"subClashEnable",
"subClashEnableRouting",
"subClashPath",
"subClashRules",
"subClashURI",
"subClashUserAgentRegex",
"subDomain",
"subEnable",
"subEnableRouting",
@@ -825,12 +862,15 @@ export const SCHEMAS: Record<string, unknown> = {
"subHideSettings",
"subIncyEnableRouting",
"subIncyRoutingRules",
"subJsonAlwaysArray",
"subJsonAutoDetect",
"subJsonEnable",
"subJsonFinalMask",
"subJsonMux",
"subJsonPath",
"subJsonRules",
"subJsonURI",
"subJsonUserAgentRegex",
"subKeyFile",
"subListen",
"subPath",
+10
View File
@@ -51,11 +51,13 @@ export interface AllSetting {
smtpUsername: string;
subAnnounce: string;
subCertFile: string;
subClashAutoDetect: boolean;
subClashEnable: boolean;
subClashEnableRouting: boolean;
subClashPath: string;
subClashRules: string;
subClashURI: string;
subClashUserAgentRegex: string;
subDomain: string;
subEnable: boolean;
subEnableRouting: boolean;
@@ -63,12 +65,15 @@ export interface AllSetting {
subHideSettings: boolean;
subIncyEnableRouting: boolean;
subIncyRoutingRules: string;
subJsonAlwaysArray: boolean;
subJsonAutoDetect: boolean;
subJsonEnable: boolean;
subJsonFinalMask: string;
subJsonMux: string;
subJsonPath: string;
subJsonRules: string;
subJsonURI: string;
subJsonUserAgentRegex: string;
subKeyFile: string;
subListen: string;
subPath: string;
@@ -157,11 +162,13 @@ export interface AllSettingView {
smtpUsername: string;
subAnnounce: string;
subCertFile: string;
subClashAutoDetect: boolean;
subClashEnable: boolean;
subClashEnableRouting: boolean;
subClashPath: string;
subClashRules: string;
subClashURI: string;
subClashUserAgentRegex: string;
subDomain: string;
subEnable: boolean;
subEnableRouting: boolean;
@@ -169,12 +176,15 @@ export interface AllSettingView {
subHideSettings: boolean;
subIncyEnableRouting: boolean;
subIncyRoutingRules: string;
subJsonAlwaysArray: boolean;
subJsonAutoDetect: boolean;
subJsonEnable: boolean;
subJsonFinalMask: string;
subJsonMux: string;
subJsonPath: string;
subJsonRules: string;
subJsonURI: string;
subJsonUserAgentRegex: string;
subKeyFile: string;
subListen: string;
subPath: string;
+10
View File
@@ -63,11 +63,13 @@ export const AllSettingSchema = z.object({
smtpUsername: z.string(),
subAnnounce: z.string(),
subCertFile: z.string(),
subClashAutoDetect: z.boolean(),
subClashEnable: z.boolean(),
subClashEnableRouting: z.boolean(),
subClashPath: z.string(),
subClashRules: z.string(),
subClashURI: z.string(),
subClashUserAgentRegex: z.string(),
subDomain: z.string(),
subEnable: z.boolean(),
subEnableRouting: z.boolean(),
@@ -75,12 +77,15 @@ export const AllSettingSchema = z.object({
subHideSettings: z.boolean(),
subIncyEnableRouting: z.boolean(),
subIncyRoutingRules: z.string(),
subJsonAlwaysArray: z.boolean(),
subJsonAutoDetect: z.boolean(),
subJsonEnable: z.boolean(),
subJsonFinalMask: z.string(),
subJsonMux: z.string(),
subJsonPath: z.string(),
subJsonRules: z.string(),
subJsonURI: z.string(),
subJsonUserAgentRegex: z.string(),
subKeyFile: z.string(),
subListen: z.string(),
subPath: z.string(),
@@ -170,11 +175,13 @@ export const AllSettingViewSchema = z.object({
smtpUsername: z.string(),
subAnnounce: z.string(),
subCertFile: z.string(),
subClashAutoDetect: z.boolean(),
subClashEnable: z.boolean(),
subClashEnableRouting: z.boolean(),
subClashPath: z.string(),
subClashRules: z.string(),
subClashURI: z.string(),
subClashUserAgentRegex: z.string(),
subDomain: z.string(),
subEnable: z.boolean(),
subEnableRouting: z.boolean(),
@@ -182,12 +189,15 @@ export const AllSettingViewSchema = z.object({
subHideSettings: z.boolean(),
subIncyEnableRouting: z.boolean(),
subIncyRoutingRules: z.string(),
subJsonAlwaysArray: z.boolean(),
subJsonAutoDetect: z.boolean(),
subJsonEnable: z.boolean(),
subJsonFinalMask: z.string(),
subJsonMux: z.string(),
subJsonPath: z.string(),
subJsonRules: z.string(),
subJsonURI: z.string(),
subJsonUserAgentRegex: z.string(),
subKeyFile: z.string(),
subListen: z.string(),
subPath: z.string(),