diff --git a/config-sub-converter/scripts/assemble-mihomo-config-dev.js b/config-sub-converter/scripts/assemble-mihomo-config-dev.js index 30ba921..240728f 100644 --- a/config-sub-converter/scripts/assemble-mihomo-config-dev.js +++ b/config-sub-converter/scripts/assemble-mihomo-config-dev.js @@ -1,17 +1,15 @@ /** - * SUB STORE YAML ASSEMBLER (v3: With Cleaning Options) - * * Arguments (via URL hash or Script args): - * - clear-comments=true : Remove full-line comments and debug headers. - * - clear-manifest=true : Remove 'manifest' block from x-substore. - * - clear-replacements=true : Remove 'replacements' block from x-substore. - * * Requires: Files must have header "# @file: filename.yaml" + * SUB STORE YAML ASSEMBLER (v4: Fix Duplicate Headers) + * * Arguments: + * - clear-comments=true + * - clear-manifest=true + * - clear-replacements=true + * * Requires: Header "# @file: filename.yaml" in input files. */ // --- OPTIONS PARSING --- - function normalizeOptions() { const args = (typeof $arguments !== "undefined" && $arguments) ? $arguments : {}; - const asBool = (v, def = false) => { if (v === undefined || v === null || v === "") return def; if (typeof v === "boolean") return v; @@ -20,7 +18,6 @@ function normalizeOptions() { if (["0", "false", "no", "n", "off"].includes(s)) return false; return def; }; - return { clearComments: asBool(args['clear-comments'], false), clearManifest: asBool(args['clear-manifest'], false), @@ -29,17 +26,13 @@ function normalizeOptions() { } // --- UTILS --- - function normalizeFiles(rawFiles) { const map = new Map(); if (!rawFiles || !Array.isArray(rawFiles)) return map; - rawFiles.forEach((content) => { if (typeof content !== 'string') return; const match = content.match(/^#\s*@file:\s*(.+?)(\s|$)/m); - if (match) { - map.set(match[1].trim(), content); - } + if (match) map.set(match[1].trim(), content); }); return map; } @@ -60,7 +53,6 @@ function extractKey(block, indentLevel) { return match ? match[1].trim().replace(/['"]/g, "") : null; } -// Remove lines that are purely comments function stripFullLineComments(text) { return text.split('\n') .filter(line => !line.trim().startsWith('#')) @@ -85,7 +77,6 @@ function splitBlocks(text, type) { for (let i = 0; i < lines.length; i++) { const line = lines[i]; - // Skip internal @file tags if (line.match(/^#\s*@file:/)) continue; if (isStart(line)) { @@ -97,18 +88,16 @@ function splitBlocks(text, type) { if (currentBuf.length === 0 && !line.trim()) continue; currentBuf.push(line); } - if (currentBuf.length > 0) blocks.push(currentBuf.join("\n")); return blocks; } // --- MERGE LOGIC --- - function processSection(sectionName, manifestEntries, fileMap, opts) { let sectionOutput = []; const seenKeys = new Set(); - // Header + // Добавляем заголовок секции if (!['root', 'x-substore', 'rules'].includes(sectionName)) { sectionOutput.push(`${sectionName}:`); } @@ -120,16 +109,20 @@ function processSection(sectionName, manifestEntries, fileMap, opts) { let content = fileMap.get(entry.file); if (!content) throw new Error(`CRITICAL: File "${entry.file}" not found.`); - // --- APPLY OPTION: clearComments (Part 1: Skip debug headers) --- if (!opts.clearComments) { sectionOutput.push(`\n# --- source: ${entry.file} | mode: ${entry.mode || "concat"} ---`); } - // --- APPLY OPTION: clearComments (Part 2: Strip content) --- if (opts.clearComments) { content = stripFullLineComments(content); } + // [FIX] Удаляем дублирующий заголовок x-substore из контента файла, + // но оставляем содержимое (оно уже имеет правильный отступ 2 пробела) + if (sectionName === 'x-substore') { + content = content.replace(/^x-substore:\s*(?:#.*)?$/m, ''); + } + const mode = entry.mode || "concat"; if (mode === 'concat') { @@ -151,8 +144,6 @@ function processSection(sectionName, manifestEntries, fileMap, opts) { for (const block of blocks) { let id = null; - - // Logic to extract ID if (blockType === 'list') { id = extractName(block); } else if (blockType === 'map0') { @@ -161,7 +152,7 @@ function processSection(sectionName, manifestEntries, fileMap, opts) { id = extractKey(block, 2); } - // --- APPLY OPTIONS: clearManifest / clearReplacements --- + // Apply cleaning options for x-substore content if (sectionName === 'x-substore' && id) { if (opts.clearManifest && id === 'manifest') continue; if (opts.clearReplacements && id === 'replacements') continue; @@ -174,7 +165,6 @@ function processSection(sectionName, manifestEntries, fileMap, opts) { } seenKeys.add(id); } - sectionOutput.push(block); } } @@ -184,12 +174,10 @@ function processSection(sectionName, manifestEntries, fileMap, opts) { } // --- MAIN EXECUTION --- - try { const opts = normalizeOptions(); const fileMap = normalizeFiles($files); - // Find Manifest let manifestKey = null; for (const k of fileMap.keys()) { if (k.startsWith("00-manifest")) { @@ -199,7 +187,6 @@ try { } if (!manifestKey) throw new Error("Manifest file (00-manifest-...) not found."); - // Parse Manifest const manifestRaw = fileMap.get(manifestKey); const manifestObj = ProxyUtils.yaml.safeLoad(manifestRaw); @@ -210,7 +197,6 @@ try { const manifestList = manifestObj['x-substore'].manifest; const replacements = manifestObj['x-substore'].replacements || []; - // Plan const sectionOrder = [ "x-substore", "root", "hosts", "sniffer", "tun", "dns", "proxies", "proxy-providers", "proxy-groups", "rule-providers", "rules" @@ -223,20 +209,15 @@ try { plan[entry.section].push(entry); }); - // Assemble const finalChunks = []; - for (const sec of sectionOrder) { if (!plan[sec] || plan[sec].length === 0) continue; const secStr = processSection(sec, plan[sec], fileMap, opts); finalChunks.push(secStr); } - // Join with double newline if comments allowed, else single/compact might be preferred, - // but double is safer for YAML structure readability. let result = finalChunks.join("\n\n"); - // Replacements if (Array.isArray(replacements)) { replacements.forEach(rep => { if (rep.from && rep.to) {