Browse Source

feat(Clash): Add routing rules and enable routing option for Clash subscriptions (#4904)

* feat(clash): add routing rules and enable routing option for Clash/Mihomo subscriptions

Allows adding custom YAML blocks and placeholders to Clash exports.

Why: Shifting routing to the client prevents server IP exposure for
DIRECT traffic and reduces unnecessary server bandwidth/CPU usage.

* fix

---------

Co-authored-by: Misfit-s <>
Misfit-s 14 hours ago
parent
commit
f947fbd6c6

+ 4 - 0
frontend/src/generated/types.ts

@@ -34,7 +34,9 @@ export interface AllSetting {
   subAnnounce: string;
   subCertFile: string;
   subClashEnable: boolean;
+  subClashEnableRouting: boolean;
   subClashPath: string;
+  subClashRules: string;
   subClashURI: string;
   subDomain: string;
   subEmailInRemark: boolean;
@@ -121,7 +123,9 @@ export interface AllSettingView {
   subAnnounce: string;
   subCertFile: string;
   subClashEnable: boolean;
+  subClashEnableRouting: boolean;
   subClashPath: string;
+  subClashRules: string;
   subClashURI: string;
   subDomain: string;
   subEmailInRemark: boolean;

+ 4 - 0
frontend/src/generated/zod.ts

@@ -36,7 +36,9 @@ export const AllSettingSchema = z.object({
   subAnnounce: z.string(),
   subCertFile: z.string(),
   subClashEnable: z.boolean(),
+  subClashEnableRouting: z.boolean(),
   subClashPath: z.string(),
+  subClashRules: z.string(),
   subClashURI: z.string(),
   subDomain: z.string(),
   subEmailInRemark: z.boolean(),
@@ -124,7 +126,9 @@ export const AllSettingViewSchema = z.object({
   subAnnounce: z.string(),
   subCertFile: z.string(),
   subClashEnable: z.boolean(),
+  subClashEnableRouting: z.boolean(),
   subClashPath: z.string(),
+  subClashRules: z.string(),
   subClashURI: z.string(),
   subDomain: z.string(),
   subEmailInRemark: z.boolean(),

+ 2 - 0
frontend/src/models/setting.ts

@@ -55,6 +55,8 @@ export class AllSetting {
   subURI = '';
   subJsonURI = '';
   subClashURI = '';
+  subClashEnableRouting = false;
+  subClashRules = '';
   subJsonFragment = '';
   subJsonNoises = '';
   subJsonMux = '';

+ 1 - 1
frontend/src/pages/api-docs/endpoints.ts

@@ -1114,7 +1114,7 @@ export const sections: readonly Section[] = [
       {
         method: 'GET',
         path: '/{clashPath}:subid',
-        summary: 'Return subscription as a Clash/Mihomo-compatible YAML config. Only when Clash subscription is enabled in settings. Default path: /clash/:subid.',
+        summary: 'Return subscription as a Clash/Mihomo-compatible YAML config, including configured global Clash routing rules. Only when Clash subscription is enabled in settings. Default path: /clash/:subid.',
         params: [
           { name: 'subid', in: 'path', type: 'string', desc: 'Client subscription ID.' },
         ],

+ 14 - 0
frontend/src/pages/settings/SubscriptionGeneralTab.tsx

@@ -166,6 +166,20 @@ export default function SubscriptionGeneralTab({ allSetting, updateSetting }: Su
               <Input.TextArea value={allSetting.subRoutingRules} placeholder="happ://routing/add/..."
                 onChange={(e) => updateSetting({ subRoutingRules: e.target.value })} />
             </SettingListItem>
+
+            <Divider>Clash / Mihomo</Divider>
+
+            <SettingListItem paddings="small" title={t('pages.settings.subClashEnableRouting')} description={t('pages.settings.subClashEnableRoutingDesc')}>
+              <Switch checked={allSetting.subClashEnableRouting} onChange={(v) => updateSetting({ subClashEnableRouting: v })} />
+            </SettingListItem>
+            <SettingListItem paddings="small" title={t('pages.settings.subClashRoutingRules')} description={t('pages.settings.subClashRoutingRulesDesc')}>
+              <Input.TextArea
+                value={allSetting.subClashRules}
+                rows={8}
+                placeholder={'GEOSITE,category-ir,DIRECT\nGEOIP,private,DIRECT'}
+                onChange={(e) => updateSetting({ subClashRules: e.target.value })}
+              />
+            </SettingListItem>
           </>
         ),
       },

+ 2 - 0
frontend/src/schemas/setting.ts

@@ -59,6 +59,8 @@ export const AllSettingSchema = z.object({
   subURI: z.string().optional(),
   subJsonURI: z.string().optional(),
   subClashURI: z.string().optional(),
+  subClashEnableRouting: z.boolean().optional(),
+  subClashRules: z.string().optional(),
   subJsonFragment: z.string().optional(),
   subJsonNoises: z.string().optional(),
   subJsonMux: z.string().optional(),

+ 11 - 1
sub/sub.go

@@ -140,6 +140,16 @@ func (s *Server) initRouter() (*gin.Engine, error) {
 		SubJsonRules = ""
 	}
 
+	SubClashEnableRouting, err := s.settingService.GetSubClashEnableRouting()
+	if err != nil {
+		SubClashEnableRouting = false
+	}
+
+	SubClashRules, err := s.settingService.GetSubClashRules()
+	if err != nil {
+		SubClashRules = ""
+	}
+
 	SubTitle, err := s.settingService.GetSubTitle()
 	if err != nil {
 		SubTitle = ""
@@ -226,7 +236,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
 
 	s.sub = NewSUBController(
 		g, LinksPath, JsonPath, ClashPath, subJsonEnable, subClashEnable, Encrypt, ShowInfo, RemarkModel, SubUpdates,
-		SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules, SubTitle, SubSupportUrl,
+		SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules, SubClashEnableRouting, SubClashRules, SubTitle, SubSupportUrl,
 		SubProfileUrl, SubAnnounce, SubEnableRouting, SubRoutingRules)
 
 	return engine, nil

+ 107 - 12
sub/subClashService.go

@@ -15,17 +15,13 @@ import (
 
 type SubClashService struct {
 	inboundService service.InboundService
+	enableRouting  bool
+	clashRules     string
 	SubService     *SubService
 }
 
-type ClashConfig struct {
-	Proxies     []map[string]any `yaml:"proxies"`
-	ProxyGroups []map[string]any `yaml:"proxy-groups"`
-	Rules       []string         `yaml:"rules"`
-}
-
-func NewSubClashService(subService *SubService) *SubClashService {
-	return &SubClashService{SubService: subService}
+func NewSubClashService(enableRouting bool, clashRules string, subService *SubService) *SubClashService {
+	return &SubClashService{enableRouting: enableRouting, clashRules: clashRules, SubService: subService}
 }
 
 func (s *SubClashService) GetClash(subId string, host string) (string, string, error) {
@@ -76,14 +72,20 @@ func (s *SubClashService) GetClash(subId string, host string) (string, string, e
 	}
 	proxyNames = append(proxyNames, "DIRECT")
 
-	config := ClashConfig{
-		Proxies: proxies,
-		ProxyGroups: []map[string]any{{
+	config := map[string]any{
+		"proxies": proxies,
+		"proxy-groups": []map[string]any{{
 			"name":    "PROXY",
 			"type":    "select",
 			"proxies": proxyNames,
 		}},
-		Rules: []string{"MATCH,PROXY"},
+		"rules": []string{"MATCH,PROXY"},
+	}
+
+	if s.enableRouting {
+		if err := mergeClashRulesYAML(config, s.clashRules); err != nil {
+			return "", "", err
+		}
 	}
 
 	finalYAML, err := yaml.Marshal(config)
@@ -554,3 +556,96 @@ func cloneMap(src map[string]any) map[string]any {
 	maps.Copy(dst, src)
 	return dst
 }
+
+func mergeClashRulesYAML(base map[string]any, raw string) error {
+	raw = strings.TrimSpace(raw)
+	if raw == "" {
+		return nil
+	}
+
+	var custom any
+	if err := yaml.Unmarshal([]byte(raw), &custom); err != nil {
+		mergeClashRules(base, linesToClashRules(raw))
+		return nil
+	}
+
+	switch typed := custom.(type) {
+	case []any:
+		mergeClashRules(base, typed)
+	case map[string]any:
+		if rules, ok := typed["rules"]; ok {
+			if ruleList, ok := asAnySlice(rules); ok {
+				mergeClashRules(base, ruleList)
+			}
+		}
+	default:
+		mergeClashRules(base, linesToClashRules(raw))
+	}
+
+	return nil
+}
+
+func mergeClashRules(base map[string]any, customRules []any) {
+	if len(customRules) == 0 {
+		return
+	}
+
+	baseRules, _ := asAnySlice(base["rules"])
+	if hasClashMatchRule(customRules) {
+		base["rules"] = customRules
+		return
+	}
+
+	merged := make([]any, 0, len(customRules)+len(baseRules))
+	merged = append(merged, customRules...)
+	merged = append(merged, baseRules...)
+	base["rules"] = merged
+}
+
+func asAnySlice(value any) ([]any, bool) {
+	switch typed := value.(type) {
+	case []any:
+		return typed, true
+	case []string:
+		out := make([]any, 0, len(typed))
+		for _, item := range typed {
+			out = append(out, item)
+		}
+		return out, true
+	case []map[string]any:
+		out := make([]any, 0, len(typed))
+		for _, item := range typed {
+			out = append(out, item)
+		}
+		return out, true
+	default:
+		return nil, false
+	}
+}
+
+func hasClashMatchRule(rules []any) bool {
+	for _, rule := range rules {
+		ruleText, ok := rule.(string)
+		if !ok {
+			continue
+		}
+		parts := strings.SplitN(ruleText, ",", 2)
+		if strings.EqualFold(strings.TrimSpace(parts[0]), "MATCH") {
+			return true
+		}
+	}
+	return false
+}
+
+func linesToClashRules(raw string) []any {
+	lines := strings.Split(raw, "\n")
+	rules := make([]any, 0, len(lines))
+	for _, line := range lines {
+		line = strings.TrimSpace(line)
+		if line == "" || strings.HasPrefix(line, "#") {
+			continue
+		}
+		rules = append(rules, line)
+	}
+	return rules
+}

+ 3 - 1
sub/subController.go

@@ -66,6 +66,8 @@ func NewSUBController(
 	jsonNoise string,
 	jsonMux string,
 	jsonRules string,
+	clashEnableRouting bool,
+	clashRules string,
 	subTitle string,
 	subSupportUrl string,
 	subProfileUrl string,
@@ -91,7 +93,7 @@ func NewSUBController(
 
 		subService:      sub,
 		subJsonService:  NewSubJsonService(jsonFragment, jsonNoise, jsonMux, jsonRules, sub),
-		subClashService: NewSubClashService(sub),
+		subClashService: NewSubClashService(clashEnableRouting, clashRules, sub),
 	}
 	a.initRouter(g)
 	return a

+ 2 - 0
web/entity/entity.go

@@ -83,6 +83,8 @@ type AllSetting struct {
 	SubClashEnable              bool   `json:"subClashEnable" form:"subClashEnable"`                           // Enable Clash/Mihomo subscription endpoint
 	SubClashPath                string `json:"subClashPath" form:"subClashPath"`                               // Path for Clash/Mihomo subscription endpoint
 	SubClashURI                 string `json:"subClashURI" form:"subClashURI"`                                 // Clash/Mihomo subscription server URI
+	SubClashEnableRouting       bool   `json:"subClashEnableRouting" form:"subClashEnableRouting"`             // Enable global routing rules for Clash/Mihomo
+	SubClashRules               string `json:"subClashRules" form:"subClashRules"`                             // Clash/Mihomo global routing rules
 	SubJsonFragment             string `json:"subJsonFragment" form:"subJsonFragment"`                         // JSON subscription fragment configuration
 	SubJsonNoises               string `json:"subJsonNoises" form:"subJsonNoises"`                             // JSON subscription noise configuration
 	SubJsonMux                  string `json:"subJsonMux" form:"subJsonMux"`                                   // JSON subscription mux configuration

+ 10 - 0
web/service/setting.go

@@ -79,6 +79,8 @@ var defaultValueMap = map[string]string{
 	"subClashEnable":              "false",
 	"subClashPath":                "/clash/",
 	"subClashURI":                 "",
+	"subClashEnableRouting":       "false",
+	"subClashRules":               "",
 	"subJsonFragment":             "",
 	"subJsonNoises":               "",
 	"subJsonMux":                  "",
@@ -658,6 +660,14 @@ func (s *SettingService) GetSubClashURI() (string, error) {
 	return s.getString("subClashURI")
 }
 
+func (s *SettingService) GetSubClashEnableRouting() (bool, error) {
+	return s.getBool("subClashEnableRouting")
+}
+
+func (s *SettingService) GetSubClashRules() (string, error) {
+	return s.getString("subClashRules")
+}
+
 func (s *SettingService) GetSubJsonFragment() (string, error) {
 	return s.getString("subJsonFragment")
 }

+ 4 - 0
web/translation/ar-EG.json

@@ -1004,6 +1004,10 @@
       "subEnableRoutingDesc": "إعداد عام لتمكين التوجيه (Routing) في عميل VPN. (فقط لـ Happ)",
       "subRoutingRules": "قواعد التوجيه",
       "subRoutingRulesDesc": "قواعد التوجيه العامة لعميل VPN. (فقط لـ Happ)",
+      "subClashEnableRouting": "تفعيل التوجيه",
+      "subClashEnableRoutingDesc": "تضمين قواعد توجيه Clash/Mihomo العامة في اشتراكات YAML المُنشأة.",
+      "subClashRoutingRules": "قواعد التوجيه العامة",
+      "subClashRoutingRulesDesc": "قواعد Clash/Mihomo التي تُضاف في بداية كل اشتراك YAML قبل MATCH,PROXY.",
       "subListen": "IP الاستماع",
       "subListenDesc": "عنوان IP لخدمة الاشتراك. (سيبه فاضي عشان يستمع على كل الـ IPs)",
       "subPort": "بورت الاستماع",

+ 4 - 0
web/translation/en-US.json

@@ -1004,6 +1004,10 @@
       "subEnableRoutingDesc": "Global setting to enable routing in the VPN client. (Only for Happ)",
       "subRoutingRules": "Routing rules",
       "subRoutingRulesDesc": "Global routing rules for the VPN client. (Only for Happ)",
+      "subClashEnableRouting": "Enable routing",
+      "subClashEnableRoutingDesc": "Include global Clash/Mihomo routing rules in generated YAML subscriptions.",
+      "subClashRoutingRules": "Global routing rules",
+      "subClashRoutingRulesDesc": "Default Clash/Mihomo rules prepended to every generated YAML subscription before MATCH,PROXY.",
       "subListen": "Listen IP",
       "subListenDesc": "The IP address for the subscription service. (leave blank to listen on all IPs)",
       "subPort": "Listen Port",

+ 4 - 0
web/translation/es-ES.json

@@ -1004,6 +1004,10 @@
       "subEnableRoutingDesc": "Configuración global para habilitar el enrutamiento en el cliente VPN. (Solo para Happ)",
       "subRoutingRules": "Reglas de enrutamiento",
       "subRoutingRulesDesc": "Reglas de enrutamiento globales para el cliente VPN. (Solo para Happ)",
+      "subClashEnableRouting": "Habilitar enrutamiento",
+      "subClashEnableRoutingDesc": "Incluir reglas globales de enrutamiento Clash/Mihomo en las suscripciones YAML generadas.",
+      "subClashRoutingRules": "Reglas globales de enrutamiento",
+      "subClashRoutingRulesDesc": "Reglas Clash/Mihomo agregadas al inicio de cada suscripción YAML antes de MATCH,PROXY.",
       "subListen": "Listening IP",
       "subListenDesc": "Dejar en blanco por defecto para monitorear todas las IPs.",
       "subPort": "Puerto de Suscripción",

+ 4 - 0
web/translation/fa-IR.json

@@ -1004,6 +1004,10 @@
       "subEnableRoutingDesc": "تنظیمات سراسری برای فعال‌سازی مسیریابی در کلاینت VPN. (فقط برای Happ)",
       "subRoutingRules": "قوانین مسیریابی",
       "subRoutingRulesDesc": "قوانین مسیریابی سراسری برای کلاینت VPN. (فقط برای Happ)",
+      "subClashEnableRouting": "فعال‌سازی مسیریابی",
+      "subClashEnableRoutingDesc": "قوانین مسیریابی سراسری Clash/Mihomo را در اشتراک‌های YAML تولیدشده وارد کن.",
+      "subClashRoutingRules": "قوانین مسیریابی سراسری",
+      "subClashRoutingRulesDesc": "قوانین Clash/Mihomo که پیش از MATCH,PROXY به ابتدای هر اشتراک YAML افزوده می‌شوند.",
       "subListen": "آدرس آی‌پی",
       "subListenDesc": "آدرس آی‌پی برای سرویس سابسکریپشن. برای گوش دادن به‌تمام آی‌پی‌ها خالی‌بگذارید",
       "subPort": "پورت",

+ 4 - 0
web/translation/id-ID.json

@@ -1004,6 +1004,10 @@
       "subEnableRoutingDesc": "Pengaturan global untuk mengaktifkan perutean (routing) di klien VPN. (Hanya untuk Happ)",
       "subRoutingRules": "Aturan routing",
       "subRoutingRulesDesc": "Aturan routing global untuk klien VPN. (Hanya untuk Happ)",
+      "subClashEnableRouting": "Aktifkan routing",
+      "subClashEnableRoutingDesc": "Sertakan aturan routing global Clash/Mihomo dalam langganan YAML yang dibuat.",
+      "subClashRoutingRules": "Aturan routing global",
+      "subClashRoutingRulesDesc": "Aturan Clash/Mihomo yang ditambahkan di awal setiap langganan YAML sebelum MATCH,PROXY.",
       "subListen": "IP Pendengar",
       "subListenDesc": "Alamat IP untuk layanan langganan. (biarkan kosong untuk mendengarkan semua IP)",
       "subPort": "Port Pendengar",

+ 4 - 0
web/translation/ja-JP.json

@@ -1004,6 +1004,10 @@
       "subEnableRoutingDesc": "VPNクライアントでルーティングを有効にするためのグローバル設定。(Happのみ)",
       "subRoutingRules": "ルーティングルール",
       "subRoutingRulesDesc": "VPNクライアントのグローバルルーティングルール。(Happのみ)",
+      "subClashEnableRouting": "ルーティングを有効化",
+      "subClashEnableRoutingDesc": "生成されたYAMLサブスクリプションにClash/Mihomoのグローバルルーティングルールを含めます。",
+      "subClashRoutingRules": "グローバルルーティングルール",
+      "subClashRoutingRulesDesc": "各YAMLサブスクリプションのMATCH,PROXYより前に追加されるClash/Mihomoルール。",
       "subListen": "監視IP",
       "subListenDesc": "サブスクリプションサービスが監視するIPアドレス(空白にするとすべてのIPを監視)",
       "subPort": "監視ポート",

+ 4 - 0
web/translation/pt-BR.json

@@ -1004,6 +1004,10 @@
       "subEnableRoutingDesc": "Configuração global para habilitar o roteamento no cliente VPN. (Apenas para Happ)",
       "subRoutingRules": "Regras de roteamento",
       "subRoutingRulesDesc": "Regras de roteamento globais para o cliente VPN. (Apenas para Happ)",
+      "subClashEnableRouting": "Ativar roteamento",
+      "subClashEnableRoutingDesc": "Incluir regras globais de roteamento Clash/Mihomo nas assinaturas YAML geradas.",
+      "subClashRoutingRules": "Regras globais de roteamento",
+      "subClashRoutingRulesDesc": "Regras Clash/Mihomo adicionadas ao início de cada assinatura YAML antes de MATCH,PROXY.",
       "subListen": "IP de Escuta",
       "subListenDesc": "O endereço IP para o serviço de assinatura. (deixe em branco para escutar em todos os IPs)",
       "subPort": "Porta de Escuta",

+ 4 - 0
web/translation/ru-RU.json

@@ -1004,6 +1004,10 @@
       "subEnableRoutingDesc": "Глобальная настройка для включения маршрутизации в VPN-клиенте. (Только для Happ)",
       "subRoutingRules": "Правила маршрутизации",
       "subRoutingRulesDesc": "Глобальные правила маршрутизации для VPN-клиента. (Только для Happ)",
+      "subClashEnableRouting": "Включить маршрутизацию",
+      "subClashEnableRoutingDesc": "Добавлять глобальные правила маршрутизации Clash/Mihomo в сгенерированные YAML-подписки.",
+      "subClashRoutingRules": "Глобальные правила маршрутизации",
+      "subClashRoutingRulesDesc": "Правила Clash/Mihomo, добавляемые в начало каждой YAML-подписки перед MATCH,PROXY.",
       "subListen": "Прослушивание IP",
       "subListenDesc": "Оставьте пустым по умолчанию, чтобы отслеживать все IP-адреса",
       "subPort": "Порт подписки",

+ 4 - 0
web/translation/tr-TR.json

@@ -1004,6 +1004,10 @@
       "subEnableRoutingDesc": "VPN istemcisinde yönlendirmeyi etkinleştirmek için genel ayar. (Yalnızca Happ için)",
       "subRoutingRules": "Yönlendirme kuralları",
       "subRoutingRulesDesc": "VPN istemcisi için genel yönlendirme kuralları. (Yalnızca Happ için)",
+      "subClashEnableRouting": "Yönlendirmeyi etkinleştir",
+      "subClashEnableRoutingDesc": "Oluşturulan YAML aboneliklerine genel Clash/Mihomo yönlendirme kurallarını ekle.",
+      "subClashRoutingRules": "Genel yönlendirme kuralları",
+      "subClashRoutingRulesDesc": "Her YAML aboneliğinin başına MATCH,PROXY öncesinde eklenen Clash/Mihomo kuralları.",
       "subListen": "Dinleme IP",
       "subListenDesc": "Abonelik hizmeti için IP adresi. (tüm IP'leri dinlemek için boş bırakın)",
       "subPort": "Dinleme Portu",

+ 4 - 0
web/translation/uk-UA.json

@@ -1004,6 +1004,10 @@
       "subEnableRoutingDesc": "Глобальне налаштування для увімкнення маршрутизації у VPN-клієнті. (Тільки для Happ)",
       "subRoutingRules": "Правила маршрутизації",
       "subRoutingRulesDesc": "Глобальні правила маршрутизації для VPN-клієнта. (Тільки для Happ)",
+      "subClashEnableRouting": "Увімкнути маршрутизацію",
+      "subClashEnableRoutingDesc": "Додавати глобальні правила маршрутизації Clash/Mihomo до згенерованих YAML-підписок.",
+      "subClashRoutingRules": "Глобальні правила маршрутизації",
+      "subClashRoutingRulesDesc": "Правила Clash/Mihomo, що додаються на початок кожної YAML-підписки перед MATCH,PROXY.",
       "subListen": "Слухати IP",
       "subListenDesc": "IP-адреса для служби підписки. (залиште порожнім, щоб слухати всі IP-адреси)",
       "subPort": "Слухати порт",

+ 4 - 0
web/translation/vi-VN.json

@@ -1004,6 +1004,10 @@
       "subEnableRoutingDesc": "Cài đặt toàn cục để bật định tuyến trong ứng dụng khách VPN. (Chỉ dành cho Happ)",
       "subRoutingRules": "Quy tắc định tuyến",
       "subRoutingRulesDesc": "Quy tắc định tuyến toàn cầu cho client VPN. (Chỉ dành cho Happ)",
+      "subClashEnableRouting": "Bật định tuyến",
+      "subClashEnableRoutingDesc": "Bao gồm quy tắc định tuyến Clash/Mihomo toàn cầu trong các đăng ký YAML được tạo.",
+      "subClashRoutingRules": "Quy tắc định tuyến toàn cầu",
+      "subClashRoutingRulesDesc": "Quy tắc Clash/Mihomo được thêm vào đầu mỗi đăng ký YAML trước MATCH,PROXY.",
       "subListen": "Listening IP",
       "subListenDesc": "Mặc định để trống để nghe tất cả các IP",
       "subPort": "Cổng gói đăng ký",

+ 4 - 0
web/translation/zh-CN.json

@@ -1004,6 +1004,10 @@
       "subEnableRoutingDesc": "在 VPN 客户端中启用路由的全局设置。(僅限 Happ)",
       "subRoutingRules": "路由規則",
       "subRoutingRulesDesc": "VPN 用戶端的全域路由規則。(僅限 Happ)",
+      "subClashEnableRouting": "启用路由",
+      "subClashEnableRoutingDesc": "在生成的 YAML 订阅中包含 Clash/Mihomo 全局路由规则。",
+      "subClashRoutingRules": "全局路由规则",
+      "subClashRoutingRulesDesc": "添加到每个 YAML 订阅开头、MATCH,PROXY 之前的 Clash/Mihomo 规则。",
       "subListen": "监听 IP",
       "subListenDesc": "订阅服务监听的 IP 地址(留空表示监听所有 IP)",
       "subPort": "监听端口",

+ 4 - 0
web/translation/zh-TW.json

@@ -1004,6 +1004,10 @@
       "subEnableRoutingDesc": "在 VPN 用戶端中啟用路由的全域設定。(僅限 Happ)",
       "subRoutingRules": "路由規則",
       "subRoutingRulesDesc": "VPN 用戶端的全域路由規則。(僅限 Happ)",
+      "subClashEnableRouting": "啟用路由",
+      "subClashEnableRoutingDesc": "在產生的 YAML 訂閱中包含 Clash/Mihomo 全域路由規則。",
+      "subClashRoutingRules": "全域路由規則",
+      "subClashRoutingRulesDesc": "加入到每個 YAML 訂閱開頭、MATCH,PROXY 之前的 Clash/Mihomo 規則。",
       "subListen": "監聽 IP",
       "subListenDesc": "訂閱服務監聽的 IP 地址(留空表示監聽所有 IP)",
       "subPort": "監聽埠",