From d60bb7e1e85b7c48e53c0f3c5ced8c90cfbdbe6c Mon Sep 17 00:00:00 2001 From: Kai Lu Date: Wed, 26 Aug 2026 12:03:50 +0800 Subject: [PATCH] Add local YAML file import feature for subscription manager --- fancyss/scripts/ss_node_subscribe.sh | 205 +++++++++++++++++++++++++++ fancyss/webs/Module_shadowsocks.asp | 93 +++++++++++- 2 files changed, 296 insertions(+), 2 deletions(-) diff --git a/fancyss/scripts/ss_node_subscribe.sh b/fancyss/scripts/ss_node_subscribe.sh index 28f8aa82..7807a6f5 100755 --- a/fancyss/scripts/ss_node_subscribe.sh +++ b/fancyss/scripts/ss_node_subscribe.sh @@ -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" @@ -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 diff --git a/fancyss/webs/Module_shadowsocks.asp b/fancyss/webs/Module_shadowsocks.asp index 356a719a..e176a343 100644 --- a/fancyss/webs/Module_shadowsocks.asp +++ b/fancyss/webs/Module_shadowsocks.asp @@ -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; @@ -5751,10 +5752,22 @@ function render_subscription_manager_body() { html += '
'; html += '订阅配置管理'; html += '分享链接导入'; + html += '本地文件导入'; html += '
'; if (subscribeManagerActiveTab == "uri") { html += '
用于快速解析单条或多条分享链接,沿用现有 URI 导入逻辑,不进入订阅 profile。
'; html += '
'; + } else if (subscribeManagerActiveTab == "yaml") { + html += '
上传本地 Clash/Mihomo YAML 配置文件,自动解析其中的代理节点并导入。支持标准 Clash proxy-groups 格式。
'; + html += '
'; + html += ''; + html += '
'; + html += '
📄
'; + html += '
点击选择文件或拖拽文件到此处
'; + html += '
支持 .yaml / .yml / .txt 格式
'; + html += ''; + html += '
'; + html += '
'; } else { html += render_subscription_profiles_cards(); } @@ -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(); @@ -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() { @@ -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(''); @@ -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) { @@ -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();