v1.3.14 feat: add custom warp-ep route rule management

- add optional custom warp-ep route rule management via [sb -d]
- expand custom route entries by individual domain_suffix and rule_set values

v1.3.14 feat: 增加自定义 warp-ep 路由规则管理

- 增加可选的自定义 warp-ep 路由规则管理,可通过 [sb -d] 按需配置
- 将 domain_suffix 和 rule_set 数组按单项展开显示
This commit is contained in:
fscarmen
2026-05-29 22:46:10 +00:00
parent a683f289ea
commit 5644e6bebb
2 changed files with 443 additions and 3 deletions
+3
View File
@@ -25,6 +25,8 @@
* * * * * *
## 1.更新信息 ## 1.更新信息
2026.05.29 v1.3.14 Added optional custom warp route rule management, available after installation via [sb -d] without affecting the main installation flow; 增加可选的自定义 warp 路由规则管理,不影响主程序安装流程,安装后可通过 [sb -d] 按需管理
2026.05.18 v1.3.13 Added top-level http_clients configuration; 增加顶层的 http_clients 配置 2026.05.18 v1.3.13 Added top-level http_clients configuration; 增加顶层的 http_clients 配置
2026.05.14 v1.3.12 1. Add Hysteria2 Realm support for machines without public inbound access, with optional WARP-assisted hole punching for strict NAT environments; 2. Realm configuration export is supported for Clash/Mihomo and sing-box clients; 3. Hysteria2 Realm can be enabled or disabled directly via sb -d; 4. Non-interactive installs support --HY2_REALM and --HY2_WARP parameters; 1. 增加 Hysteria2 Realm 支持,适用于没有公网入口的机器,并可选 WARP 辅助打洞; 2. Realm 已支持导出 Clash/Mihomo 和 sing-box 客户端配置; 3. 修改节点配置时可直接开启或关闭 Hysteria2 Realm; 4. 无交互安装支持 --HY2_REALM 与 --HY2_WARP 参数 2026.05.14 v1.3.12 1. Add Hysteria2 Realm support for machines without public inbound access, with optional WARP-assisted hole punching for strict NAT environments; 2. Realm configuration export is supported for Clash/Mihomo and sing-box clients; 3. Hysteria2 Realm can be enabled or disabled directly via sb -d; 4. Non-interactive installs support --HY2_REALM and --HY2_WARP parameters; 1. 增加 Hysteria2 Realm 支持,适用于没有公网入口的机器,并可选 WARP 辅助打洞; 2. Realm 已支持导出 Clash/Mihomo 和 sing-box 客户端配置; 3. 修改节点配置时可直接开启或关闭 Hysteria2 Realm; 4. 无交互安装支持 --HY2_REALM 与 --HY2_WARP 参数
@@ -600,6 +602,7 @@ services:
| |-- 05_dns.json # DNS 规则文件 | |-- 05_dns.json # DNS 规则文件
| |-- 06_ntp.json # 服务端时间同步配置文件 | |-- 06_ntp.json # 服务端时间同步配置文件
| |-- 07_http_clients.json # 专门给 sing-box 内部组件发 HTTP 请求配置文件 | |-- 07_http_clients.json # 专门给 sing-box 内部组件发 HTTP 请求配置文件
| |-- 08_custom_route.json # 用户自定义 warp-ep 出站路由规则文件,支持 domain_suffix / rule_set 分流
| |-- 11_xtls-reality_inbounds.json # Reality vision 协议配置文件 | |-- 11_xtls-reality_inbounds.json # Reality vision 协议配置文件
| |-- 12_hysteria2_inbounds.json # Hysteria2 协议配置文件 | |-- 12_hysteria2_inbounds.json # Hysteria2 协议配置文件
| |-- 13_tuic_inbounds.json # Tuic V5 协议配置文件 # Hysteria2 协议配置文件 | |-- 13_tuic_inbounds.json # Tuic V5 协议配置文件 # Hysteria2 协议配置文件
+440 -3
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# 当前脚本版本号 # 当前脚本版本号
VERSION='v1.3.13 (2026.05.18)' VERSION='v1.3.14 (2026.05.29)'
# Github 反代加速代理 # Github 反代加速代理
GITHUB_PROXY=('https://hub.glowp.xyz/' 'https://proxy.vvvv.ee/') GITHUB_PROXY=('https://hub.glowp.xyz/' 'https://proxy.vvvv.ee/')
@@ -39,8 +39,8 @@ mkdir -p "$TEMP_DIR"
E[0]="Language:\n 1. English (default) \n 2. 简体中文" E[0]="Language:\n 1. English (default) \n 2. 简体中文"
C[0]="${E[0]}" C[0]="${E[0]}"
E[1]="1. Added Hysteria2 Realm support for machines without public inbound access, with optional WARP-assisted hole punching; 2. Realm is exported for Clash/Mihomo and sing-box clients; 3. Hysteria2 Realm can be toggled directly in node configuration changes; 4. Unified the fixed STUN server list for Hysteria2 Realm; 5. Added top-level http_clients configuration" E[1]="1. Added optional custom warp route rule management, available after installation via [sb -d] without affecting the main installation flow; 2. Added Hysteria2 Realm support for machines without public inbound access, with optional WARP-assisted hole punching"
C[1]="1. 增加 Hysteria2 Realm 支持,适用于没有公网入口的机器,并可选 WARP 辅助打洞; 2. Realm 已支持导出 Clash/Mihomo 和 sing-box 客户端配置; 3. 修改节点配置时可直接开启或关闭 Hysteria2 Realm; 4. 统一 Hysteria2 Realm 固定 STUN 服务器列表。 5. 增加顶层的 http_clients 配置" C[1]="1. 增加可选的自定义 warp 路由规则管理,不影响主程序安装流程,安装后可通过 [sb -d] 按需管理; 2. 增加 Hysteria2 Realm 支持,适用于没有公网入口的机器,并可选 WARP 辅助打洞"
E[2]="Downloading Sing-box. Please wait a seconds ..." E[2]="Downloading Sing-box. Please wait a seconds ..."
C[2]="下载 Sing-box 中,请稍等 ..." C[2]="下载 Sing-box 中,请稍等 ..."
E[3]="Input errors up to 5 times.The script is aborted." E[3]="Input errors up to 5 times.The script is aborted."
@@ -335,6 +335,34 @@ E[147]="Hysteria2 Realm is useful for China-back routing or machines without pub
C[147]="Hysteria2 Realm 适用于回国或者没有公网入口的机器;有公网入口时不建议使用。是否启用?[y/N]:" C[147]="Hysteria2 Realm 适用于回国或者没有公网入口的机器;有公网入口时不建议使用。是否启用?[y/N]:"
E[148]="WARP-assisted hole punching is useful in strict NAT environments. When direct hole punching fails, Cloudflare WARP can provide a CF egress path to improve success. Enable it? [y/N]:" E[148]="WARP-assisted hole punching is useful in strict NAT environments. When direct hole punching fails, Cloudflare WARP can provide a CF egress path to improve success. Enable it? [y/N]:"
C[148]="WARP 辅助打洞(适用于 NAT 严格环境):当 NAT 类型较严格(如对称 NAT)导致直连打洞失败时,可借助 Cloudflare WARP 获取一个 CF 出口 IP 作为中转,提升打洞成功率。是否启用?[y/N]:" C[148]="WARP 辅助打洞(适用于 NAT 严格环境):当 NAT 类型较严格(如对称 NAT)导致直连打洞失败时,可借助 Cloudflare WARP 获取一个 CF 出口 IP 作为中转,提升打洞成功率。是否启用?[y/N]:"
E[150]="Custom warp-ep outbounds rules (rules: \${CUSTOM_ROUTE_COUNT:-0})"
C[150]="自定义 warp-ep 出站路由规则 (规则数: \${CUSTOM_ROUTE_COUNT:-0})"
E[151]="1. Add rule\n 2. View rules\n 3. Delete rule\n 0. Back"
C[151]="1. 添加规则\n 2. 查看规则\n 3. 删除规则\n 0. 返回"
E[152]="Select rule type:\\n 1. domain_suffix\\n 2. rule_set"
C[152]="选择规则类型:\\n 1. domain_suffix (域名后缀)\\n 2. rule_set (规则集)"
E[153]="Enter domain suffix (comma-separated, e.g. google.com,telegram.org):"
C[153]="输入域名后缀 (逗号分隔,如 google.com,telegram.org):"
E[154]="Enter rule_set name (comma-separated, e.g. geosite-google,geosite-telegram):"
C[154]="输入规则集名称 (逗号分隔,如 geosite-google,geosite-telegram):"
E[155]="Matched custom route rules will use warp-ep outbound."
C[155]="命中的自定义路由规则将使用 warp-ep 出站。"
E[156]="Rule set \"\${RULE_NAME}\" not found in SagerNet or MetaCubeX repositories. Please re-enter:"
C[156]="规则集 \"\${RULE_NAME}\" 在 SagerNet 和 MetaCubeX 仓库中均未找到,请重新输入:"
E[157]="Custom route rule added successfully."
C[157]="自定义路由规则添加成功。"
E[158]="No custom route rules configured."
C[158]="未配置自定义路由规则。"
E[159]="Enter warp-ep outbound rule number(s) to delete (comma-separated):"
C[159]="输入要删除的 warp-ep 出站规则编号 (逗号分隔):"
E[160]="Custom route rule(s) deleted."
C[160]="自定义路由规则已删除。"
E[161]="Invalid domain format: \${DOMAIN}"
C[161]="无效的域名格式: \${DOMAIN}"
E[162]="API check failed, using SagerNet URL as default. (Warning: rule_set may not exist)"
C[162]="API 校验失败,默认使用 SagerNet 地址。(警告: 规则集可能不存在)"
E[163]="Current custom route rules:"
C[163]="当前自定义路由规则:"
# 自定义字体彩色,read 函数 # 自定义字体彩色,read 函数
warning() { echo -e "\033[31m\033[01m$*\033[0m"; } # 红色 warning() { echo -e "\033[31m\033[01m$*\033[0m"; } # 红色
@@ -630,6 +658,12 @@ change_config() {
MENU_IDX+=(139) && MENU_KEY+=(hy2hopping) && MENU_VAL+=("${HY2_PORT_HOPPING_RANGE}") MENU_IDX+=(139) && MENU_KEY+=(hy2hopping) && MENU_VAL+=("${HY2_PORT_HOPPING_RANGE}")
fi fi
# 自定义路由规则(仅在 warp-ep 存在时显示)
grep -q '"warp-ep"' ${WORK_DIR}/conf/02_endpoints.json 2>/dev/null && {
CUSTOM_ROUTE_COUNT=$(custom_route_count)
MENU_IDX+=(150) && MENU_KEY+=(customroute) && MENU_VAL+=("${CUSTOM_ROUTE_COUNT}")
}
[ "${#MENU_IDX[@]}" -eq 0 ] && error " $(text 110) " [ "${#MENU_IDX[@]}" -eq 0 ] && error " $(text 110) "
# 显示动态菜单 # 显示动态菜单
@@ -741,6 +775,9 @@ change_config() {
export_list export_list
return return
elif [ "$KEY" = "customroute" ]; then
custom_route_menu
return
fi fi
hint "" hint ""
@@ -1144,6 +1181,406 @@ sync_hy2_warp_route() {
fi fi
} }
# ===================== 自定义路由规则 =====================
# 统计自定义路由规则数量(按数组里的单项统计,不按整条 route rule 统计)
custom_route_count() {
local CUSTOM_FILE="${WORK_DIR}/conf/08_custom_route.json"
if [ -s "$CUSTOM_FILE" ]; then
jq_exec '[.route.rules[]? | ((.domain_suffix // []) | length) + ((.rule_set // []) | length)] | add // 0' "$CUSTOM_FILE" 2>/dev/null || echo 0
else
echo 0
fi
}
# 将 warp-ep 的 domain_suffix / rule_set 合并到同一条 route rule,保持 08_custom_route.json 更简洁
custom_route_compact_rules() {
local CUSTOM_FILE="${WORK_DIR}/conf/08_custom_route.json"
local TMP_FILE="${CUSTOM_FILE}.tmp"
[ ! -s "$CUSTOM_FILE" ] && return
jq_exec '
(.route.rules // []) as $rules |
($rules | map(select((.outbound // "warp-ep") == "warp-ep") | .domain_suffix // []) | add // [] | reduce .[] as $x ([]; if index($x) then . else . + [$x] end)) as $domains |
($rules | map(select((.outbound // "warp-ep") == "warp-ep") | .rule_set // []) | add // [] | reduce .[] as $x ([]; if index($x) then . else . + [$x] end)) as $sets |
($rules | map(select((.outbound // "warp-ep") != "warp-ep"))) as $others |
.route.rules = (
$others +
(if (($domains | length) + ($sets | length)) > 0 then
[((if ($sets | length) > 0 then {rule_set:$sets} else {} end)
+ (if ($domains | length) > 0 then {domain_suffix:$domains} else {} end)
+ {outbound:"warp-ep"})]
else [] end)
)
' "$CUSTOM_FILE" > "$TMP_FILE" && mv "$TMP_FILE" "$CUSTOM_FILE"
}
# 通过 GitHub API 校验 rule_set 是否存在,返回下载 URL
# 参数: $1 = rule_set 名称(不含 .srs
# 输出: 下载 URL 或空字符串
check_rule_set_exists() {
local NAME="$1"
local SRS_NAME="${NAME}.srs"
local CACHE_DIR="${TEMP_DIR}/ruleset_cache"
mkdir -p "$CACHE_DIR"
# SagerNet 源
local SAGERNET_CACHE="${CACHE_DIR}/sagernet_tree.json"
if [ ! -s "$SAGERNET_CACHE" ]; then
curl -sL --connect-timeout 5 --max-time 15 "https://api.github.com/repos/SagerNet/sing-geosite/git/trees/rule-set?recursive=1" > "$SAGERNET_CACHE" 2>/dev/null || true
fi
if [ -s "$SAGERNET_CACHE" ] && jq_exec -e ".tree[]? | select(.path == \"${SRS_NAME}\")" "$SAGERNET_CACHE" >/dev/null 2>&1; then
echo "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/${SRS_NAME}"
return 0
fi
# MetaCubeX 源
local METACUBEX_CACHE="${CACHE_DIR}/metacubex_tree.json"
if [ ! -s "$METACUBEX_CACHE" ]; then
curl -sL --connect-timeout 5 --max-time 15 "https://api.github.com/repos/MetaCubeX/meta-rules-dat/git/trees/sing?recursive=1" > "$METACUBEX_CACHE" 2>/dev/null || true
fi
if [ -s "$METACUBEX_CACHE" ]; then
local MATCH_PATH
MATCH_PATH=$(jq_exec -r "[.tree[]? | select(.path | endswith(\"/${SRS_NAME}\") or . == \"${SRS_NAME}\") | .path] | first // empty" "$METACUBEX_CACHE" 2>/dev/null)
if [ -n "$MATCH_PATH" ]; then
echo "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/${MATCH_PATH}"
return 0
fi
fi
# 两处 API 都没数据(可能是 rate limit 或网络问题)
if [ ! -s "$SAGERNET_CACHE" ] && [ ! -s "$METACUBEX_CACHE" ]; then
# API 不可用,降级使用默认 URL
warning " $(text 162) "
echo "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/${SRS_NAME}"
return 0
fi
# 确实在两处都没找到
return 1
}
# 添加自定义路由规则
custom_route_add() {
local CUSTOM_FILE="${WORK_DIR}/conf/08_custom_route.json"
# 选择规则类型
hint "\n $(text 152) "
reading " $(text 24) " RULE_TYPE_CHOICE
case "$RULE_TYPE_CHOICE" in
1 ) local RULE_TYPE="domain_suffix" ;;
2 ) local RULE_TYPE="rule_set" ;;
* ) info " $(text 135) " && return ;;
esac
local VALIDATED_VALUES=()
local RULE_SET_URLS=()
if [ "$RULE_TYPE" = "domain_suffix" ]; then
reading " $(text 153) " DOMAIN_INPUT
[ -z "$DOMAIN_INPUT" ] && info " $(text 135) " && return
local DOMAINS=()
custom_route_csv_to_array "$DOMAIN_INPUT" DOMAINS
local DOMAIN
for DOMAIN in "${DOMAINS[@]}"; do
DOMAIN=$(sed 's/。/./g' <<< "${DOMAIN,,}")
if [[ "$DOMAIN" =~ ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?\.[a-z]{2,}$ ]]; then
VALIDATED_VALUES+=("$DOMAIN")
else
warning " $(text 161) "
fi
done
[ "${#VALIDATED_VALUES[@]}" -eq 0 ] && warning " $(text 135) " && return
elif [ "$RULE_TYPE" = "rule_set" ]; then
# 输入规则集名称
reading " $(text 154) " RULESET_INPUT
[ -z "$RULESET_INPUT" ] && info " $(text 135) " && return
local RULESETS=()
custom_route_csv_to_array "$RULESET_INPUT" RULESETS
local RULE_NAME RETRY URL
for RULE_NAME in "${RULESETS[@]}"; do
RULE_NAME="${RULE_NAME,,}"
RULE_NAME=$(sed -E 's#^.*/##; s/\.srs$//I' <<< "$RULE_NAME")
[[ -n "$RULE_NAME" && ! "$RULE_NAME" =~ ^geo(site|ip)- ]] && RULE_NAME="geosite-${RULE_NAME}"
[ -z "$RULE_NAME" ] && continue
RETRY=3
URL=""
while [ $RETRY -gt 0 ]; do
URL=$(check_rule_set_exists "$RULE_NAME")
if [ -n "$URL" ]; then
VALIDATED_VALUES+=("$RULE_NAME")
RULE_SET_URLS+=("$URL")
break
else
((RETRY--))
if [ $RETRY -gt 0 ]; then
warning " $(text 156) "
reading " " RULE_NAME
RULE_NAME="${RULE_NAME,,}"
RULE_NAME=$(sed -E 's#^.*/##; s/\.srs$//I' <<< "$RULE_NAME")
[[ -n "$RULE_NAME" && ! "$RULE_NAME" =~ ^geo(site|ip)- ]] && RULE_NAME="geosite-${RULE_NAME}"
else
warning " $(text 156) "
fi
fi
done
done
[ "${#VALIDATED_VALUES[@]}" -eq 0 ] && warning " $(text 135) " && return
fi
# 自定义路由固定使用 warp-ep 出站
local OUTBOUND="warp-ep"
hint " $(text 155) "
# 初始化 JSON 文件(如果不存在)
if [ ! -s "$CUSTOM_FILE" ]; then
echo '{"route":{"rule_set":[],"rules":[]}}' | jq_exec '.' > "$CUSTOM_FILE"
fi
local TMP_FILE="${CUSTOM_FILE}.tmp"
if [ "$RULE_TYPE" = "domain_suffix" ]; then
# 构建 domain_suffix 数组 JSON,先对输入本身去重
local DOMAINS_JSON
DOMAINS_JSON=$(printf '%s\n' "${VALIDATED_VALUES[@]}" | jq_exec -R . | jq_exec -s 'reduce .[] as $x ([]; if index($x) then . else . + [$x] end)')
# 合并到同一条 warp-ep route rulerule_set 和 domain_suffix 共用一条规则
jq_exec --argjson domains "$DOMAINS_JSON" --arg out "$OUTBOUND" '
.route.rules = (.route.rules // []) |
(.route.rules | map(select((.outbound // $out) == $out) | .domain_suffix // []) | add // []) as $old_domains |
(.route.rules | map(select((.outbound // $out) == $out) | .rule_set // []) | add // []) as $old_sets |
(.route.rules | map(select((.outbound // $out) != $out))) as $others |
(($old_domains + $domains) | reduce .[] as $x ([]; if index($x) then . else . + [$x] end)) as $new_domains |
($old_sets | reduce .[] as $x ([]; if index($x) then . else . + [$x] end)) as $new_sets |
.route.rules = (
$others +
(if (($new_domains | length) + ($new_sets | length)) > 0 then
[((if ($new_sets | length) > 0 then {rule_set:$new_sets} else {} end)
+ (if ($new_domains | length) > 0 then {domain_suffix:$new_domains} else {} end)
+ {outbound:$out})]
else [] end)
)
' "$CUSTOM_FILE" > "$TMP_FILE" && mv "$TMP_FILE" "$CUSTOM_FILE"
elif [ "$RULE_TYPE" = "rule_set" ]; then
# 添加 rule_set 定义到 route.rule_set(去重)
for i in "${!VALIDATED_VALUES[@]}"; do
local RS_NAME="${VALIDATED_VALUES[$i]}"
local RS_URL="${RULE_SET_URLS[$i]}"
jq_exec --arg tag "$RS_NAME" --arg url "$RS_URL" '
.route.rule_set = (.route.rule_set // []) |
.route.rule_set |= (
if any(.[]; .tag == $tag) then .
else . + [{"tag": $tag, "type": "remote", "format": "binary", "url": $url}]
end
)
' "$CUSTOM_FILE" > "$TMP_FILE" && mv "$TMP_FILE" "$CUSTOM_FILE"
done
# 构建 rule_set 名称数组,先对输入本身去重
local RS_NAMES_JSON
RS_NAMES_JSON=$(printf '%s\n' "${VALIDATED_VALUES[@]}" | jq_exec -R . | jq_exec -s 'reduce .[] as $x ([]; if index($x) then . else . + [$x] end)')
# 合并到同一条 warp-ep route rulerule_set 和 domain_suffix 共用一条规则
jq_exec --argjson names "$RS_NAMES_JSON" --arg out "$OUTBOUND" '
.route.rules = (.route.rules // []) |
(.route.rules | map(select((.outbound // $out) == $out) | .domain_suffix // []) | add // []) as $old_domains |
(.route.rules | map(select((.outbound // $out) == $out) | .rule_set // []) | add // []) as $old_sets |
(.route.rules | map(select((.outbound // $out) != $out))) as $others |
($old_domains | reduce .[] as $x ([]; if index($x) then . else . + [$x] end)) as $new_domains |
(($old_sets + $names) | reduce .[] as $x ([]; if index($x) then . else . + [$x] end)) as $new_sets |
.route.rules = (
$others +
(if (($new_domains | length) + ($new_sets | length)) > 0 then
[((if ($new_sets | length) > 0 then {rule_set:$new_sets} else {} end)
+ (if ($new_domains | length) > 0 then {domain_suffix:$new_domains} else {} end)
+ {outbound:$out})]
else [] end)
)
' "$CUSTOM_FILE" > "$TMP_FILE" && mv "$TMP_FILE" "$CUSTOM_FILE"
fi
custom_route_compact_rules
info " $(text 157) "
hint " $(text 112) "
cmd_systemctl restart sing-box
sleep 2
cmd_systemctl status sing-box &>/dev/null && \
info "\n Sing-box $(text 28) $(text 37) \n" || \
warning "\n Sing-box $(text 27) $(text 38) \n"
}
# 将逗号分隔输入转为数组:支持半角/全角逗号、顿号、分号、竖线;不使用 IFS
custom_route_csv_to_array() {
local _input="$1"
local -n _out_array="$2"
_out_array=()
mapfile -t _out_array < <(
printf '%s\n' "$_input" |
sed 's/\x1b\[[0-9;?]*[A-Za-z]//g; s/\^\[\[[0-9;?]*[A-Za-z]//g; s/[,、;;|]/,/g; s/[[:space:]]//g; s/,/\n/g; /^$/d'
)
}
# 输出展开后的自定义路由项,每个 domain_suffix / rule_set 数组元素单独一行
custom_route_items_json() {
local CUSTOM_FILE="${WORK_DIR}/conf/08_custom_route.json"
jq_exec -c '
[.route.rules[]?] as $rules |
reduce range(0; ($rules | length)) as $i ([];
($rules[$i]) as $r |
.
+ [($r.rule_set[]? | {rule_index:$i,type:"rule_set",match:.,outbound:($r.outbound // "warp-ep")})]
+ [($r.domain_suffix[]? | {rule_index:$i,type:"domain_suffix",match:.,outbound:($r.outbound // "warp-ep")})]
+ (if (($r.rule_set? == null) and ($r.domain_suffix? == null)) then [{rule_index:$i,type:"unknown",match:"N/A",outbound:($r.outbound // "warp-ep")}] else [] end)
) | .[]
' "$CUSTOM_FILE" 2>/dev/null
}
# 查看自定义路由规则
custom_route_view() {
local CUSTOM_FILE="${WORK_DIR}/conf/08_custom_route.json"
if [ ! -s "$CUSTOM_FILE" ]; then
hint " $(text 158) "
return 1
fi
local ROUTE_ITEMS=()
mapfile -t ROUTE_ITEMS < <(custom_route_items_json)
if [ "${#ROUTE_ITEMS[@]}" -eq 0 ]; then
hint " $(text 158) "
return 1
fi
hint "\n $(text 163) \n"
printf " %-4s %-16s %s\n" "#" "Type" "Match"
printf " %-4s %-16s %s\n" "---" "---------------" "---------------------------------------"
local IDX=0
local ITEM TYPE MATCH
for ITEM in "${ROUTE_ITEMS[@]}"; do
((IDX++)) || true
TYPE=$(jq_exec -r '.type' <<< "$ITEM")
MATCH=$(jq_exec -r '.match' <<< "$ITEM")
printf " %-4s %-16s %s\n" "$IDX" "$TYPE" "$MATCH"
done
echo ""
return 0
}
# 删除自定义路由规则:按展开后的单项编号删除,支持删除数组里的某个元素
custom_route_delete() {
local CUSTOM_FILE="${WORK_DIR}/conf/08_custom_route.json"
custom_route_view || return
local ROUTE_ITEMS=()
mapfile -t ROUTE_ITEMS < <(custom_route_items_json)
[ "${#ROUTE_ITEMS[@]}" -eq 0 ] && info " $(text 135) " && return
reading " $(text 159) " DELETE_INPUT
[ -z "$DELETE_INPUT" ] && info " $(text 135) " && return
local DELETE_NUMS=()
custom_route_csv_to_array "$DELETE_INPUT" DELETE_NUMS
local DELETE_ITEM_LINES=()
local NUM
for NUM in "${DELETE_NUMS[@]}"; do
NUM=$(sed 's/[^0-9]//g' <<< "$NUM")
if [[ "$NUM" =~ ^[0-9]+$ ]] && [ "$NUM" -ge 1 ] && [ "$NUM" -le "${#ROUTE_ITEMS[@]}" ]; then
DELETE_ITEM_LINES+=("${ROUTE_ITEMS[$((NUM - 1))]}")
fi
done
[ "${#DELETE_ITEM_LINES[@]}" -eq 0 ] && info " $(text 135) " && return
local DELETE_ITEMS_JSON TMP_FILE
DELETE_ITEMS_JSON=$(printf '%s\n' "${DELETE_ITEM_LINES[@]}" | jq_exec -s 'unique_by(.rule_index, .type, .match)')
TMP_FILE="${CUSTOM_FILE}.tmp"
# 只删除被选中的数组元素;数组清空后才删除整条 route rule
jq_exec --argjson del "$DELETE_ITEMS_JSON" '
.route.rules |= (
[.[]?] as $rules |
reduce range(0; ($rules | length)) as $idx ([];
($rules[$idx]) as $rule |
($del | map(select(.rule_index == $idx and .type == "domain_suffix") | .match)) as $remove_domains |
($del | map(select(.rule_index == $idx and .type == "rule_set") | .match)) as $remove_sets |
($rule
| if .domain_suffix? != null then .domain_suffix = ([.domain_suffix[]? as $v | select(($remove_domains | index($v)) | not) | $v]) else . end
| if .rule_set? != null then .rule_set = ([.rule_set[]? as $v | select(($remove_sets | index($v)) | not) | $v]) else . end
| if ((.domain_suffix // []) | length) == 0 then del(.domain_suffix) else . end
| if ((.rule_set // []) | length) == 0 then del(.rule_set) else . end
) as $new_rule |
if (($new_rule.domain_suffix? != null) or ($new_rule.rule_set? != null)) then
. + [$new_rule]
elif ($del | any(.rule_index == $idx and .type == "unknown")) then
.
else
. + [$new_rule]
end
)
)
' "$CUSTOM_FILE" > "$TMP_FILE" && mv "$TMP_FILE" "$CUSTOM_FILE"
custom_route_compact_rules
# 清理孤立的 rule_set 定义:只保留仍被 rules 引用的 rule_set
jq_exec '
(.route.rules | [.[]? | .rule_set // [] | .[]] | unique) as $used |
.route.rule_set |= [.[]? | select(.tag as $t | $used | index($t) | not | not)]
' "$CUSTOM_FILE" > "$TMP_FILE" && mv "$TMP_FILE" "$CUSTOM_FILE"
# 如果没有任何规则了,删除文件
local REMAINING
REMAINING=$(jq_exec '.route.rules | length' "$CUSTOM_FILE" 2>/dev/null)
if [ "${REMAINING:-0}" -eq 0 ]; then
rm -f "$CUSTOM_FILE"
fi
info " $(text 160) "
hint " $(text 112) "
cmd_systemctl restart sing-box
sleep 2
cmd_systemctl status sing-box &>/dev/null && \
info "\n Sing-box $(text 28) $(text 37) \n" || \
warning "\n Sing-box $(text 27) $(text 38) \n"
}
# 自定义路由规则子菜单
custom_route_menu() {
while true; do
CUSTOM_ROUTE_COUNT=$(custom_route_count)
hint "\n $(text 150) \n"
hint " $(text 151) "
hint ""
reading " $(text 24) " CUSTOM_ROUTE_CHOICE
case "$CUSTOM_ROUTE_CHOICE" in
1 ) custom_route_add ;;
2 ) custom_route_view ;;
3 ) custom_route_delete ;;
0 ) return ;;
* ) info " $(text 135) " && return ;;
esac
done
}
# ===================== 自定义路由规则 END =====================
# 输入 Reality 密钥 # 输入 Reality 密钥
input_reality_key() { input_reality_key() {
[[ "$NONINTERACTIVE_INSTALL" != 'noninteractive_install' && "$IS_FAST_INSTALL" != 'is_fast_install' ]] && [ -z "$REALITY_PRIVATE" ] && reading "\n ${TOTAL_STEPS:+(${STEP_NUM}/${TOTAL_STEPS}) }$(text 70) " REALITY_PRIVATE [[ "$NONINTERACTIVE_INSTALL" != 'noninteractive_install' && "$IS_FAST_INSTALL" != 'is_fast_install' ]] && [ -z "$REALITY_PRIVATE" ] && reading "\n ${TOTAL_STEPS:+(${STEP_NUM}/${TOTAL_STEPS}) }$(text 70) " REALITY_PRIVATE