Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 205 additions & 0 deletions fancyss/scripts/ss_node_subscribe.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8099,6 +8099,202 @@ start_offline_update() {
echo_date "==================================================================="
}

# 创建本地导入的订阅配置 profile
subprof_create_local_import_profile() {
local profile_name="$1"
local profile_id=""
local profile_key=""
local state_key=""
local normalized_json=""
local state_json=""
local jq_bin=""

profile_id="$(subprof_generate_profile_id 2>/dev/null)" || return 1
profile_key="$(subprof_profile_key "${profile_id}")" || return 1
state_key="$(subprof_state_key "${profile_id}")" || return 1
jq_bin="$(subprof_jq_bin)" || jq_bin="jq"

normalized_json="$(printf '%s' "{}" | "${jq_bin}" -c \
--arg id "${profile_id}" \
--arg name "${profile_name}" \
'{
version: 1,
id: $id,
name: $name,
url: "",
enabled: true,
subscribe_mode: "2",
download: { policy: "auto" },
ua: { mode: "fixed", preset: "default", custom: "" },
filter: { exclude: "", include: "", keep_info_node: true },
flags: { allow_insecure: false, node_log: true },
hy2: { up: "", dl: "", tfo_switch: "2", cg_opt: "bbr" },
schedule: {
enabled: false,
type: "1",
week: "1",
day: "7",
hour: "3",
minute: "5",
interval_value: "1",
interval_unit: "2",
custom_hours: ""
}
}')" || return 1

subprof_dbus_set_json_by_key "${profile_key}" "${normalized_json}" || return 1

state_json="$(printf '%s' "{}" | "${jq_bin}" -c \
--arg id "${profile_id}" \
'{version:1,id:$id,last_ok_ts:0,last_error_ts:0,last_error:"",last_url_hash:"",last_group:""}')" || true
subprof_dbus_set_json_by_key "${state_key}" "${state_json}" >/dev/null 2>&1 || true

subprof_add_profile_id "${profile_id}"
printf '%s\n' "${profile_id}"
}

# 从本地上传的 YAML 配置文件导入节点
start_yaml_file_import() {
echo_date "==================================================================="
echo_date "ℹ️通过本地 YAML 配置文件导入节点..."
mkdir -p $DIR
rm -rf $DIR/*

# 查找上传的 YAML 文件
local yaml_file=""
for candidate in /tmp/upload/ss_yaml_upload.yaml /tmp/upload/ss_yaml_upload.yml; do
if [ -f "${candidate}" ]; then
yaml_file="${candidate}"
break
fi
done

# 也尝试查找任何最近上传的 yaml 文件
if [ -z "${yaml_file}" ]; then
yaml_file=$(find /tmp/upload -maxdepth 1 -name "*.yaml" -o -name "*.yml" 2>/dev/null | head -n1)
fi

if [ -z "${yaml_file}" ] || [ ! -f "${yaml_file}" ]; then
echo_date "❌未找到上传的 YAML 文件!"
echo_date "请确保文件已正确上传。"
echo_date "==================================================================="
return 1
fi

echo_date "📁找到上传的文件:${yaml_file}"

# 检查文件大小
local file_size=$(wc -c < "${yaml_file}" 2>/dev/null | tr -d ' ')
if [ -z "${file_size}" ] || [ "${file_size}" -lt 10 ]; then
echo_date "❌文件内容为空或过小,无法解析。"
rm -f "${yaml_file}"
echo_date "==================================================================="
return 1
fi

echo_date "📊文件大小:${file_size} 字节"

# 先创建订阅配置 profile,以便节点写入时关联到此 profile
local profile_name="本地YAML导入"
local import_profile_id=""
import_profile_id="$(subprof_create_local_import_profile "${profile_name}" 2>/dev/null)"
if [ -z "${import_profile_id}" ]; then
echo_date "⚠️创建订阅配置失败,节点将作为独立导入写入..."
import_profile_id=""
else
echo_date "📋已创建订阅配置:${profile_name} (${import_profile_id})"
fi

# 设置 profile ID 以便节点解析时关联
SUB_ACTIVE_PROFILE_ID="${import_profile_id}"

# 使用 sub-tool 检测文件格式
local sub_tool=""
sub_tool="$(pick_sub_tool 2>/dev/null)"
local payload_kind="unknown"

if [ -n "${sub_tool}" ]; then
echo_date "🔧使用 sub-tool 检测文件格式..."
local inspect_file="${DIR}/yaml_inspect.json"
if "${sub_tool}" inspect --input "${yaml_file}" > "${inspect_file}" 2>/dev/null; then
payload_kind=$(jq -r '.kind // ""' "${inspect_file}" 2>/dev/null)
echo_date "📋文件格式检测:${payload_kind}"
fi
rm -f "${inspect_file}" 2>/dev/null
fi

# 根据文件类型选择解析方式
local parsed_file="${DIR}/yaml_parsed_nodes.txt"
local pkg_type=$(dbus get ss_basic_pkg_type)
[ -n "${pkg_type}" ] || pkg_type="full"

case "${payload_kind}" in
clash-yaml)
echo_date "✅检测到 Clash/Mihomo YAML 配置文件"
echo_date "🔍开始解析 Clash YAML 中的代理节点..."
if [ -n "${sub_tool}" ]; then
local default_group="YAML导入"
sub_try_parse_uri_lines_with_tool "${yaml_file}" "${parsed_file}" "${default_group}" "yaml_import" "${pkg_type}"
if [ $? -eq 0 ] && [ -s "${parsed_file}" ]; then
echo_date "✅sub-tool 解析成功"
else
echo_date "⚠️sub-tool 解析失败,尝试直接使用 URI 解析..."
rm -f "${parsed_file}" 2>/dev/null
fi
fi
;;
uri-lines|base64-uri-lines)
echo_date "✅检测到 URI 列表格式"
local default_group="YAML导入"
sub_try_parse_uri_lines_with_tool "${yaml_file}" "${parsed_file}" "${default_group}" "yaml_import" "${pkg_type}"
;;
*)
echo_date "⚠️未识别的文件格式:${payload_kind}"
echo_date "📋尝试作为 URI 列表解析..."
local default_group="YAML导入"
sub_try_parse_uri_lines_with_tool "${yaml_file}" "${parsed_file}" "${default_group}" "yaml_import" "${pkg_type}"
;;
esac

# 处理解析结果
echo_date "-------------------------------------------------------------------"
if [ -f "${parsed_file}" ] && [ -s "${parsed_file}" ]; then
local node_count=$(wc -l < "${parsed_file}" 2>/dev/null | tr -d ' ')
echo_date "📊共解析出 ${node_count} 个节点"

sub_filter_offline_duplicate_nodes "${parsed_file}"
[ -f "${parsed_file}" ] && echo_date "ℹ️YAML 文件解析完毕,开始写入节点..."
SUB_FAST_APPEND=1
SUB_FAST_APPEND_REUSE=0
if [ -f "${parsed_file}" ] && json2skipd "yaml_parsed_nodes"; then
sub_after_nodes_written "${parsed_file}"
echo_date "✅本地 YAML 文件节点导入成功!"
if [ -n "${import_profile_id}" ]; then
echo_date "📋节点已关联到订阅配置:${profile_name}"
echo_date "💡您可以在「订阅配置管理」中编辑此配置,设置订阅链接后即可自动更新。"
fi
else
echo_date "❌节点写入失败!"
fi
else
echo_date "❌YAML 文件解析失败,未找到可用的代理节点。"
echo_date "ℹ️请确保 YAML 文件包含标准的 Clash/Mihomo 代理配置(proxies 字段)。"
echo_date "ℹ️支持的格式:"
echo_date " - Clash/Mihomo YAML 配置文件(包含 proxies 字段)"
echo_date " - URI 列表文件(每行一个 ss://、vmess:// 等链接)"
echo_date " - Base64 编码的 URI 列表"
# 解析失败时移除空 profile
if [ -n "${import_profile_id}" ]; then
subprof_remove_profile "${import_profile_id}" 2>/dev/null || true
echo_date "🧹已清理空的订阅配置。"
fi
fi

# 清理临时文件
rm -f "${yaml_file}" 2>/dev/null
echo_date "==================================================================="
}

SUB_SELECTED_PROFILE_ID=""
if [ "$1" = "3" ] && [ -n "$2" ]; then
SUB_SELECTED_PROFILE_ID="$2"
Expand Down Expand Up @@ -8177,4 +8373,13 @@ case $SH_ARG in
echo XU6J03M6 | tee -a $LOG_FILE
unset_lock
;;
5)
# 上传本地 YAML 配置文件导入节点
set_lock
true > $LOG_FILE
[ "${WEB_ACTION}" == "1" ] && http_response "$1"
start_yaml_file_import | tee -a $LOG_FILE
echo XU6J03M6 | tee -a $LOG_FILE
unset_lock
;;
esac
93 changes: 91 additions & 2 deletions fancyss/webs/Module_shadowsocks.asp
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,7 @@ var subscribeLogLayerIndex = null;
var subscribeLogWs = null;
var subscribeLogWsFallbackTimer = null;
var subscribeLogRefreshNodesAfterDone = false;
var submgrDroppedFile = null;
var subscribeLogTaskDone = false;
var subscribeLogCloseCountdown = -1;
var subscribeLogCloseTimer = null;
Expand Down Expand Up @@ -5751,10 +5752,22 @@ function render_subscription_manager_body() {
html += '<div class="submgr-tabs">';
html += '<a href="javascript:void(0);" class="submgr-tab' + (subscribeManagerActiveTab == "profiles" ? ' active' : '') + '" onclick="handle_subscription_manager_tab(\'profiles\')">订阅配置管理</a>';
html += '<a href="javascript:void(0);" class="submgr-tab' + (subscribeManagerActiveTab == "uri" ? ' active' : '') + '" onclick="handle_subscription_manager_tab(\'uri\')">分享链接导入</a>';
html += '<a href="javascript:void(0);" class="submgr-tab' + (subscribeManagerActiveTab == "yaml" ? ' active' : '') + '" onclick="handle_subscription_manager_tab(\'yaml\')">本地文件导入</a>';
html += '</div>';
if (subscribeManagerActiveTab == "uri") {
html += '<div class="submgr-toolbar"><div class="submgr-toolbar-note">用于快速解析单条或多条分享链接,沿用现有 URI 导入逻辑,不进入订阅 profile。</div></div>';
html += '<div class="submgr-uri-box"><textarea id="submgr_uri_input" placeholder="填入 ss://、ssr://、vmess://、vless://、trojan://、hy2://、hysteria2://、tuic://、naive+https://、naive+quic://、anytls:// 等链接,支持多行。"></textarea></div>';
} else if (subscribeManagerActiveTab == "yaml") {
html += '<div class="submgr-toolbar"><div class="submgr-toolbar-note">上传本地 Clash/Mihomo YAML 配置文件,自动解析其中的代理节点并导入。支持标准 Clash proxy-groups 格式。</div></div>';
html += '<div class="submgr-uri-box" style="padding:20px;text-align:center;">';
html += '<input type="file" id="submgr_yaml_file" accept=".yaml,.yml,.txt" style="display:none;" onchange="on_submgr_yaml_file_selected()">';
html += '<div id="submgr_yaml_drop_zone" style="border:2px dashed rgba(255,255,255,0.15);border-radius:8px;padding:40px 20px;cursor:pointer;transition:all 0.2s;" onmouseover="this.style.borderColor=\'rgba(70,160,255,0.5)\'" onmouseout="this.style.borderColor=\'rgba(255,255,255,0.15)\'" onclick="$(\'#submgr_yaml_file\').click();" ondragover="event.preventDefault();this.style.borderColor=\'rgba(70,160,255,0.8)\'" ondragleave="this.style.borderColor=\'rgba(255,255,255,0.15)\'" ondrop="event.preventDefault();this.style.borderColor=\'rgba(255,255,255,0.15)\';on_submgr_yaml_file_dropped(event);">';
html += '<div style="font-size:32px;margin-bottom:10px;">📄</div>';
html += '<div style="font-size:13px;color:#cfe0f1;margin-bottom:8px;">点击选择文件或拖拽文件到此处</div>';
html += '<div style="font-size:11px;color:#8899aa;">支持 .yaml / .yml / .txt 格式</div>';
html += '<div id="submgr_yaml_file_info" style="display:none;margin-top:12px;font-size:12px;color:#37c67f;"></div>';
html += '</div>';
html += '</div>';
} else {
html += render_subscription_profiles_cards();
}
Expand All @@ -5780,6 +5793,10 @@ function update_subscription_manager_buttons() {
btn0.text("解析并保存为节点").show();
btn1.hide();
btn2.text("关闭窗口").show();
} else if (subscribeManagerActiveTab == "yaml") {
btn0.text("上传并解析节点").show();
btn1.hide();
btn2.text("关闭窗口").show();
} else {
btn0.text("新增订阅").show();
btn1.text("全部同步").show();
Expand Down Expand Up @@ -5811,7 +5828,11 @@ function wait_subscription_profiles_until(checkFn, cb, attempt) {
});
}
function handle_subscription_manager_tab(tabKey) {
subscribeManagerActiveTab = tabKey == "uri" ? "uri" : "profiles";
if (tabKey == "uri" || tabKey == "yaml") {
subscribeManagerActiveTab = tabKey;
} else {
subscribeManagerActiveTab = "profiles";
}
render_subscription_manager();
}
function open_subscription_manager() {
Expand Down Expand Up @@ -5841,13 +5862,15 @@ function open_subscription_manager() {
yes: function(index) {
if (subscribeManagerActiveTab == "uri") {
submit_subscription_uri_from_manager();
} else if (subscribeManagerActiveTab == "yaml") {
submit_subscription_yaml_from_manager();
} else {
open_subscription_profile_editor('');
}
return false;
},
btn2: function(index) {
if (subscribeManagerActiveTab == "uri") {
if (subscribeManagerActiveTab == "uri" || subscribeManagerActiveTab == "yaml") {
return false;
}
sync_subscription_profiles('');
Expand Down Expand Up @@ -6222,6 +6245,71 @@ function submit_subscription_uri_from_manager() {
push_subscription_data("4", dbus_post, "分享链接导入日志", "", true);
return false;
}
function on_submgr_yaml_file_dropped(event) {
var files = event.dataTransfer && event.dataTransfer.files;
if (!files || files.length === 0) return;
var file = files[0];
var ext = file.name.split('.').pop().toLowerCase();
if (ext !== "yaml" && ext !== "yml" && ext !== "txt") {
alert("文件格式不正确,请选择 .yaml、.yml 或 .txt 文件!");
return;
}
submgrDroppedFile = file;
$("#submgr_yaml_file_info").text("已选择: " + file.name + " (" + (file.size / 1024).toFixed(1) + " KB)").show();
}
function on_submgr_yaml_file_selected() {
var fileInput = $("#submgr_yaml_file")[0];
if (fileInput && fileInput.files && fileInput.files.length > 0) {
var file = fileInput.files[0];
var ext = file.name.split('.').pop().toLowerCase();
if (ext !== "yaml" && ext !== "yml" && ext !== "txt") {
alert("文件格式不正确,请选择 .yaml、.yml 或 .txt 文件!");
fileInput.value = "";
return;
}
$("#submgr_yaml_file_info").text("已选择: " + file.name + " (" + (file.size / 1024).toFixed(1) + " KB)").show();
}
}
function submit_subscription_yaml_from_manager() {
var file = null;
if (submgrDroppedFile) {
file = submgrDroppedFile;
submgrDroppedFile = null;
} else {
var fileInput = $("#submgr_yaml_file")[0];
if (fileInput && fileInput.files && fileInput.files.length > 0) {
file = fileInput.files[0];
}
}
if (!file) {
alert("请先选择一个 YAML 配置文件。");
return false;
}
var ext = file.name.split('.').pop().toLowerCase();
if (ext !== "yaml" && ext !== "yml" && ext !== "txt") {
alert("文件格式不正确,请选择 .yaml、.yml 或 .txt 文件!");
return false;
}
db_ss["ss_basic_action"] = "13";
var formData = new FormData();
formData.append("ss_yaml_upload.yaml", file);
$.ajax({
url: '/_upload',
type: 'POST',
cache: false,
data: formData,
processData: false,
contentType: false,
complete: function(res) {
if (res.status == 200) {
push_subscription_data("5", {}, "本地文件导入日志", "", true);
} else {
alert("文件上传失败,请重试。");
}
}
});
return false;
}
function get_legacy_node_ids() {
var ids = [];
for (var field in db_ss) {
Expand Down Expand Up @@ -8036,6 +8124,7 @@ function close_subscription_log_ws() {
function finish_subscription_log_with_refresh() {
if (subscribeLogRefreshNodesAfterDone) {
subscribeLogRefreshNodesAfterDone = false;
subscribeManagerActiveTab = "profiles";
refresh_table(function() {
fetch_subscription_profiles_dbus(function() {
render_subscription_manager();
Expand Down