Fix Xray config subscription visibility

This commit is contained in:
ushan0v
2026-06-11 19:43:42 +03:00
parent 83f8362758
commit 174259893e
4 changed files with 310 additions and 27 deletions
+1 -1
View File
@@ -358,7 +358,7 @@ SING_BOX_URLTEST_DEFAULT_IDLE_TIMEOUT="30m"
SERVER_COUNTRY_METHOD_FLAG_EMOJI="flag_emoji"
SERVER_COUNTRY_METHOD_COUNTRY_IS="country_is"
PODKOP_RELOAD_STATE_FORMAT=1
PODKOP_RUNTIME_CACHE_FORMAT=5
PODKOP_RUNTIME_CACHE_FORMAT=6
PODKOP_RUNTIME_STABLE_MIN_AGE=2
PODKOP_SYSTEM_INFO_CACHE_TTL=3600
PODKOP_SUBSCRIPTION_UPDATE_JOB_FINISHED_TTL_MINUTES=60
+16 -3
View File
@@ -640,6 +640,17 @@ sing_box_cf_subscription_prepared_slice() {
sing_box_cf_ucode_input prepared-slice "$prepared_json" "$start" "$end" 2>/dev/null
}
sing_box_cf_prepared_visible_count() {
local prepared_json="$1"
local count
count="$(sing_box_cf_ucode_input prepared-visible-count "$prepared_json" 2>/dev/null)"
case "$count" in
'' | *[!0-9]*) printf '0\n' ;;
*) printf '%s\n' "$count" ;;
esac
}
sing_box_cf_prepared_field_to_file() {
local prepared_json="$1"
local field="$2"
@@ -785,8 +796,9 @@ sing_box_cf_apply_subscription_batch() {
fi
rm -f "$new_outbounds_tmp"
SUBSCRIPTION_OUTBOUND_COUNT="$new_outbounds_count"
sing_box_cf_load_prepared_state "$prepared_json" || return 1
SUBSCRIPTION_OUTBOUND_COUNT="$(sing_box_cf_tags_json_count "$SUBSCRIPTION_OUTBOUND_TAGS_JSON")"
[ "${SUBSCRIPTION_OUTBOUND_COUNT:-0}" -gt 0 ] || return 1
SING_BOX_CF_LAST_CONFIG="$SING_BOX_CF_VALIDATED_CONFIG"
return 0
@@ -795,7 +807,7 @@ sing_box_cf_apply_subscription_batch() {
sing_box_cf_apply_subscription_outbounds_range() {
local start="$1"
local count="$2"
local end chunk outbounds_json display_name half rest index
local end chunk outbounds_json display_name half rest index visible_count
[ "$count" -gt 0 ] || return 0
@@ -817,7 +829,8 @@ sing_box_cf_apply_subscription_outbounds_range() {
log "Failed to append subscription metadata for rule '$SING_BOX_CF_SOURCE_SECTION'. Aborted." "fatal"
exit 1
fi
SING_BOX_CF_FALLBACK_ADDED_COUNT=$((SING_BOX_CF_FALLBACK_ADDED_COUNT + count))
visible_count="$(sing_box_cf_prepared_visible_count "$chunk")"
SING_BOX_CF_FALLBACK_ADDED_COUNT=$((SING_BOX_CF_FALLBACK_ADDED_COUNT + visible_count))
return 0
fi
+110 -14
View File
@@ -401,6 +401,21 @@ function supported_transport_type(transport) {
transport == "httpupgrade" || transport == "xhttp" || transport == "kcp";
}
function internal_flag(value) {
if (value === true)
return true;
value = lc(as_string(value));
return value == "1" || value == "true" || value == "yes" || value == "on";
}
function outbound_is_hidden(outbound) {
return type(outbound) == "object" && internal_flag(outbound.__podkop_hidden);
}
function outbound_allows_group_type(outbound) {
return type(outbound) == "object" && internal_flag(outbound.__podkop_allow_group);
}
let supported_shadowsocks_methods = {
"none": true,
"aes-128-gcm": true,
@@ -441,6 +456,8 @@ function prefilter_skip_reason(outbound, supports_xhttp, plugin_supports) {
let proxy_type = as_string(outbound.type || "");
if (proxy_type == "")
return "missing type";
if ((proxy_type == "selector" || proxy_type == "urltest") && outbound_allows_group_type(outbound))
return "";
if (proxy_type == "direct" || proxy_type == "selector" || proxy_type == "urltest" ||
proxy_type == "dns" || proxy_type == "block")
return "non-proxy outbound type: " + proxy_type;
@@ -503,12 +520,33 @@ function unique_tag(base, taken) {
function copy_outbound(outbound) {
let copy = {};
for (let key, value in outbound) {
if (key != "tag" && key != "remark" && key != "share_link")
if (key != "tag" && key != "remark" && key != "share_link" &&
key != "__podkop_hidden" && key != "__podkop_allow_group")
copy[key] = value;
}
return copy;
}
function rewrite_prepared_outbound_references(outbounds, tag_map) {
for (let outbound in outbounds) {
if (type(outbound) != "object")
continue;
let detour = as_string(outbound.detour || "");
if (detour != "" && tag_map[detour])
outbound.detour = tag_map[detour];
if (type(outbound.outbounds) == "array") {
let rewritten = [];
for (let tag in outbound.outbounds) {
tag = as_string(tag);
push(rewritten, tag_map[tag] || tag);
}
outbound.outbounds = rewritten;
}
}
}
function candidate_outbounds(path) {
let subscription = object_or_empty(read_json_file(path));
let result = [];
@@ -521,7 +559,11 @@ function candidate_outbounds(path) {
};
for (let outbound in array_or_empty(subscription.outbounds)) {
if (type(outbound) == "object" && !skipped_types[outbound.type])
if (type(outbound) != "object")
continue;
if ((outbound.type == "selector" || outbound.type == "urltest") && outbound_allows_group_type(outbound))
push(result, outbound);
else if (!skipped_types[outbound.type])
push(result, outbound);
}
@@ -548,9 +590,11 @@ function prepare_subscription(config_path, outbounds_path, output_path, supports
let servers = [];
let links = [];
let source_indices = [];
let visible = [];
let skipped = 0;
let skipped_reason_counts = {};
let prepared = [];
let tag_map = {};
for (let i = 0; i < length(outbounds); i++) {
let outbound = outbounds[i];
@@ -568,6 +612,8 @@ function prepare_subscription(config_path, outbounds_path, output_path, supports
let tag = unique_tag(base_tag, taken);
let outbound_copy = copy_outbound(outbound);
outbound_copy.tag = tag;
let is_hidden = outbound_is_hidden(outbound);
tag_map[base_tag] = tag;
push(prepared, outbound_copy);
push(tags, tag);
@@ -575,9 +621,12 @@ function prepare_subscription(config_path, outbounds_path, output_path, supports
push(servers, outbound.server || "");
push(links, outbound.share_link || "");
push(source_indices, i + 1);
push(visible, !is_hidden);
taken[tag] = true;
}
rewrite_prepared_outbound_references(prepared, tag_map);
if (!write_file_json(output_path, {
outbounds: prepared,
links,
@@ -586,7 +635,8 @@ function prepare_subscription(config_path, outbounds_path, output_path, supports
servers,
skipped,
skipped_reason_counts: length(keys(skipped_reason_counts)) > 0 ? skipped_reason_counts : [],
tags
tags,
visible
}))
exit(1);
}
@@ -691,15 +741,36 @@ function prepare_validation(new_outbounds_path, updated_path, validation_path) {
exit(1);
}
function prepared_entry_visible(prepared, index) {
let visible = array_or_empty(prepared.visible);
return index >= length(visible) || visible[index] !== false;
}
function prepared_visible_tags(prepared) {
let tags = array_or_empty(prepared.tags);
let result = [];
for (let i = 0; i < length(tags); i++) {
if (prepared_entry_visible(prepared, i))
push(result, tags[i]);
}
return result;
}
function prepared_link_refs(prepared, source_section) {
let tags = array_or_empty(prepared.tags);
let source_indices = array_or_empty(prepared.source_indices);
let outbounds = array_or_empty(prepared.outbounds);
let result = {};
if (source_section == "")
return result;
for (let i = 0; i < length(tags); i++) {
if (!prepared_entry_visible(prepared, i))
continue;
let outbound = type(outbounds[i]) == "object" ? outbounds[i] : {};
if (outbound.type == "selector" || outbound.type == "urltest")
continue;
result[tags[i]] = {
sourceSection: source_section,
sourceIndex: int(source_indices[i] || (i + 1))
@@ -712,8 +783,11 @@ function prepared_names(prepared) {
let tags = array_or_empty(prepared.tags);
let names = array_or_empty(prepared.names);
let result = {};
for (let i = 0; i < length(tags); i++)
for (let i = 0; i < length(tags); i++) {
if (!prepared_entry_visible(prepared, i))
continue;
result[tags[i]] = as_string(names[i] || tags[i]);
}
return result;
}
@@ -722,6 +796,8 @@ function prepared_servers(prepared) {
let servers = array_or_empty(prepared.servers);
let result = {};
for (let i = 0; i < length(tags); i++) {
if (!prepared_entry_visible(prepared, i))
continue;
let server = as_string(servers[i]);
if (server != "")
result[tags[i]] = server;
@@ -731,8 +807,11 @@ function prepared_servers(prepared) {
function prepared_names_text(prepared) {
let lines = [];
for (let name in array_or_empty(prepared.names))
push(lines, as_string(name));
let names = array_or_empty(prepared.names);
for (let i = 0; i < length(names); i++) {
if (prepared_entry_visible(prepared, i))
push(lines, as_string(names[i]));
}
return length(lines) > 0 ? join("\n", lines) + "\n" : "";
}
@@ -764,6 +843,7 @@ function prepared_slice(start, end) {
prepared.servers = slice(array_or_empty(prepared.servers), start, end);
prepared.links = slice(array_or_empty(prepared.links), start, end);
prepared.source_indices = slice(array_or_empty(prepared.source_indices), start, end);
prepared.visible = slice(array_or_empty(prepared.visible), start, end);
prepared.skipped = 0;
prepared.skipped_reason_counts = {};
write_json(prepared);
@@ -771,14 +851,14 @@ function prepared_slice(start, end) {
function field_json(field) {
let value = object_or_empty(read_stdin_json())[field];
if (field == "outbounds" || field == "tags" || field == "names" || field == "servers" || field == "links" || field == "source_indices")
if (field == "outbounds" || field == "tags" || field == "names" || field == "servers" || field == "links" || field == "source_indices" || field == "visible")
value = array_or_empty(value);
write_json(value == null ? [] : value);
}
function prepared_field_to_file(field, output_path, count_path) {
let value = object_or_empty(read_stdin_json())[field];
if (field == "outbounds" || field == "tags" || field == "names" || field == "servers" || field == "links" || field == "source_indices")
if (field == "outbounds" || field == "tags" || field == "names" || field == "servers" || field == "links" || field == "source_indices" || field == "visible")
value = array_or_empty(value);
if (value == null)
value = [];
@@ -798,6 +878,11 @@ function field_length(field) {
print(length(value), "\n");
}
function visible_length() {
let prepared = object_or_empty(read_stdin_json());
print(length(prepared_visible_tags(prepared)), "\n");
}
function stdin_collection_length() {
let value = read_stdin_json();
if (type(value) != "array" && type(value) != "object")
@@ -815,7 +900,7 @@ function print_string_array_csv(values) {
function tags_csv() {
let prepared = object_or_empty(read_stdin_json());
print_string_array_csv(array_or_empty(prepared.tags));
print_string_array_csv(prepared_visible_tags(prepared));
}
function stdin_string_array_csv() {
@@ -824,7 +909,7 @@ function stdin_string_array_csv() {
function prepared_state_to_files(source_section, tags_path, tags_csv_path, names_lines_path, link_refs_path, names_path, servers_path) {
let prepared = object_or_empty(read_stdin_json());
let tags = array_or_empty(prepared.tags);
let tags = prepared_visible_tags(prepared);
let tag_values = [];
for (let tag in tags)
push(tag_values, as_string(tag));
@@ -841,7 +926,7 @@ function prepared_state_to_files(source_section, tags_path, tags_csv_path, names
function append_prepared_state_to_files(source_section, tags_path, names_lines_path, link_refs_path, names_path, servers_path) {
let prepared = object_or_empty(read_stdin_json());
let tags = array_or_empty(read_json_file(tags_path));
for (let tag in array_or_empty(prepared.tags))
for (let tag in prepared_visible_tags(prepared))
push(tags, tag);
let names_lines = trim_trailing_newlines(fs.readfile(names_lines_path));
@@ -861,9 +946,18 @@ function display_name() {
let prepared = object_or_empty(read_stdin_json());
let names = array_or_empty(prepared.names);
let outbounds = array_or_empty(prepared.outbounds);
let result = as_string(names[0]);
if (result == "" && length(outbounds) > 0 && type(outbounds[0]) == "object")
result = as_string(outbounds[0].tag);
let result = "";
let first_visible = -1;
for (let i = 0; i < length(outbounds); i++) {
if (prepared_entry_visible(prepared, i)) {
first_visible = i;
break;
}
}
if (first_visible >= 0)
result = as_string(names[first_visible]);
if (result == "" && first_visible >= 0 && type(outbounds[first_visible]) == "object")
result = as_string(outbounds[first_visible].tag);
if (result == "")
result = "unknown";
result = replace(result, /[\r\n\t]/g, " ");
@@ -904,6 +998,8 @@ else if (mode == "prepared-field-to-file")
prepared_field_to_file(ARGV[1], ARGV[2], ARGV[3]);
else if (mode == "prepared-length")
field_length(ARGV[1]);
else if (mode == "prepared-visible-count")
visible_length();
else if (mode == "prepared-tags-csv")
tags_csv();
else if (mode == "stdin-collection-length")
+183 -9
View File
@@ -2151,6 +2151,11 @@ function xray_display_name(original_tag, tag_counts, config_index) {
return original_tag;
}
function xray_config_display_name(config, fallback) {
let name = xray_first_string([config.remarks, config.remark, config.name, config.ps]);
return name != "" ? name : fallback;
}
function xray_source_tag(outbound, fallback) {
let tag = as_string(outbound.tag || "");
return tag != "" ? tag : fallback;
@@ -2298,6 +2303,89 @@ function xray_outbound_detour(outbound) {
return as_string(sockopt.dialerProxy || "");
}
function xray_outbound_by_source_tag(source_outbounds, tag) {
tag = as_string(tag);
if (tag == "")
return null;
for (let outbound in source_outbounds) {
if (type(outbound) == "object" && xray_source_tag(outbound, "") == tag)
return outbound;
}
return null;
}
function xray_supported_source_tags(source_outbounds) {
let result = {};
for (let i = 0; i < length(source_outbounds); i++) {
let outbound = source_outbounds[i];
if (type(outbound) != "object" || !xray_protocol_supported(outbound.protocol))
continue;
let tag = xray_source_tag(outbound, lc(as_string(outbound.protocol || "server")) + "-" + (i + 1));
result[tag] = true;
}
return result;
}
function xray_primary_source_tag(config, source_outbounds) {
let supported_tags = xray_supported_source_tags(source_outbounds);
for (let rule in array_or_empty(object_or_empty(config.routing).rules)) {
let outbound_tag = as_string(object_or_empty(rule).outboundTag || "");
if (supported_tags[outbound_tag])
return outbound_tag;
}
if (supported_tags["proxy"])
return "proxy";
for (let i = 0; i < length(source_outbounds); i++) {
let outbound = source_outbounds[i];
if (type(outbound) != "object" || !xray_protocol_supported(outbound.protocol))
continue;
let tag = xray_source_tag(outbound, lc(as_string(outbound.protocol || "server")) + "-" + (i + 1));
if (xray_outbound_detour(outbound) == "")
return tag;
}
for (let tag in keys(supported_tags))
return tag;
return "";
}
function xray_balancer_selector_matches(selector, tag) {
selector = as_string(selector);
tag = as_string(tag);
if (selector == "" || tag == "")
return false;
return tag == selector || starts_with(tag, selector + "-") || starts_with(tag, selector);
}
function xray_balancer_source_tags(config, source_outbounds) {
let supported_tags = xray_supported_source_tags(source_outbounds);
let selected = {};
let result = [];
for (let balancer in array_or_empty(object_or_empty(config.routing).balancers)) {
let selectors = array_or_empty(object_or_empty(balancer).selector);
for (let tag in keys(supported_tags)) {
for (let selector in selectors) {
if (xray_balancer_selector_matches(selector, tag) && !selected[tag]) {
selected[tag] = true;
push(result, tag);
break;
}
}
}
}
return result;
}
function xray_tag_counts(configs) {
let counts = {};
for (let config in configs) {
@@ -2312,7 +2400,7 @@ function xray_tag_counts(configs) {
return counts;
}
function xray_config_outbounds(config, config_index, config_count, tag_counts, taken) {
function xray_plan_config_outbounds(config, config_index, config_count, tag_counts, taken) {
let source_outbounds = array_or_empty(config.outbounds);
let planned = [];
let tag_map = {};
@@ -2329,21 +2417,107 @@ function xray_config_outbounds(config, config_index, config_count, tag_counts, t
push(planned, { outbound, original_tag, tag });
}
let result = [];
return { planned, tag_map };
}
function xray_retag_planned_outbound(planned, tag_map, source_tag, new_base, config_index, config_count, taken) {
source_tag = as_string(source_tag);
new_base = as_string(new_base);
if (source_tag == "" || new_base == "")
return;
for (let item in planned) {
let converted = convert_xray_outbound(item.outbound, item.tag);
if (!converted)
if (item.original_tag != source_tag)
continue;
let display_name = xray_display_name(item.original_tag, tag_counts, config_index);
if (display_name != "")
converted.remark = display_name;
let old_tag = as_string(item.tag || "");
let new_tag = xray_unique_tag(new_base, taken);
if (new_tag == old_tag)
return;
if (old_tag != "")
delete taken[old_tag];
taken[new_tag] = true;
item.tag = new_tag;
tag_map[source_tag] = new_tag;
return;
}
}
function xray_add_converted_outbound(result, item, tag_map, visible, display_name) {
let converted = convert_xray_outbound(item.outbound, item.tag);
if (!converted)
return;
if (visible && display_name != "")
converted.remark = display_name;
if (!visible)
converted.__podkop_hidden = true;
let detour = xray_outbound_detour(item.outbound);
if (detour != "" && tag_map[detour])
converted.detour = tag_map[detour];
push(result, converted);
}
function xray_config_outbounds(config, config_index, config_count, tag_counts, taken) {
let source_outbounds = array_or_empty(config.outbounds);
let plan = xray_plan_config_outbounds(config, config_index, config_count, tag_counts, taken);
let planned = array_or_empty(plan.planned);
let tag_map = object_or_empty(plan.tag_map);
let display_name = xray_config_display_name(config, "");
let balancer_tags = xray_balancer_source_tags(config, source_outbounds);
let visible_source_tags = {};
let dependency_tags = {};
let result = [];
if (length(balancer_tags) > 0) {
let urltest_outbounds = [];
for (let source_tag in balancer_tags) {
if (tag_map[source_tag]) {
visible_source_tags[source_tag] = false;
push(urltest_outbounds, tag_map[source_tag]);
}
}
if (length(urltest_outbounds) > 0) {
let group_tag = xray_unique_tag(
display_name != "" ? display_name : xray_tag_base("urltest", config_index, config_count),
taken
);
taken[group_tag] = true;
let group = {
type: "urltest",
tag: group_tag,
outbounds: urltest_outbounds,
url: "https://www.gstatic.com/generate_204",
interval: "10m",
tolerance: 50,
remark: display_name != "" ? display_name : group_tag,
__podkop_allow_group: true
};
push(result, group);
}
} else {
let primary_tag = xray_primary_source_tag(config, source_outbounds);
if (primary_tag != "") {
xray_retag_planned_outbound(planned, tag_map, primary_tag, display_name, config_index, config_count, taken);
visible_source_tags[primary_tag] = true;
}
}
for (let item in planned) {
let detour = xray_outbound_detour(item.outbound);
if (detour != "" && tag_map[detour])
converted.detour = tag_map[detour];
dependency_tags[detour] = true;
}
push(result, converted);
for (let item in planned) {
let visible = visible_source_tags[item.original_tag] === true;
let hidden = !visible;
if (visible || dependency_tags[item.original_tag] || visible_source_tags[item.original_tag] === false)
xray_add_converted_outbound(result, item, tag_map, !hidden, display_name != "" ? display_name : xray_display_name(item.original_tag, tag_counts, config_index));
}
return result;