diff --git a/install.sh b/install.sh index c3934f8..7b7817c 100644 --- a/install.sh +++ b/install.sh @@ -17,7 +17,7 @@ echo "=== Установка Mihomo Studio из репозитория (v18.3 + echo "[1/4] Проверка Python и модулей..." opkg update # python3-codecs ОБЯЗАТЕЛЕН для работы urllib и кодировки idna -PACKAGES="python3-base python3-light python3-email python3-urllib python3-codecs" +PACKAGES="python3-base python3-light python3-email python3-urllib python3-codecs python3-pyyaml" for pkg in $PACKAGES; do if ! opkg list-installed | grep -q "^$pkg"; then diff --git a/mihomo_editor.py b/mihomo_editor.py index 9c5ff3a..0683576 100644 --- a/mihomo_editor.py +++ b/mihomo_editor.py @@ -12,6 +12,7 @@ import time import shutil import glob import json +import yaml from datetime import datetime # --- НАСТРОЙКИ --- @@ -38,12 +39,17 @@ elif not os.path.exists(CONFIG_PATH): # --- ПАРСЕРЫ --- -def parse_vless(link): +def parse_vless(link, custom_name=None): try: if not link.startswith("vless://"): return None, "Link error" main = link[8:] name = "VLESS" - if '#' in main: main, n = main.split('#', 1); name = urllib.parse.unquote(n).strip() + if custom_name: + name = custom_name + elif '#' in main: + main, n = main.split('#', 1) + name = urllib.parse.unquote(n).strip() + name = re.sub(r'[\[\]\{\}\"\']', '', name) user_srv = main.split('?')[0] params = urllib.parse.parse_qs(main.split('?')[1]) if '?' in main else {} @@ -90,7 +96,7 @@ def parse_vless(link): return None, str(e) -def parse_wireguard(config_text): +def parse_wireguard(config_text, custom_name=None): try: name = "WireGuard" params = {} @@ -115,57 +121,64 @@ def parse_wireguard(config_text): interface = params.get('interface', {}) peer = params.get('peer', {}) - # Extract name from comment if exists - name_match = re.search(r'#\s*(.+)', config_text.splitlines()[0]) - if name_match: - name = name_match.group(1).strip() - - name = f"WG-{name}" + server, port_str = peer.get('Endpoint', ':').rsplit(':', 1) - server, port = peer.get('Endpoint', ':').rsplit(':', 1) + if custom_name: + name = custom_name + else: + name_match = re.search(r'#\s*(.+)', config_text.splitlines()[0]) + if name_match: + name = name_match.group(1).strip() + elif server: + name = f"WG_{server}" + else: + name = "WireGuard" - # 3. `Address` из `.conf` файла должен быть разделен на `ip` и `prefix` + # Преобразуем порт в int, если возможно, иначе оставляем как есть + try: + port = int(port_str) + except (ValueError, TypeError): + port = port_str + ip_address = interface.get("Address", "").split("/")[0] - y = [ - f'- name: "{name}"', - ' type: wireguard', - f' server: {server}', - f' port: {port}', - f' private-key: "{interface.get("PrivateKey")}"', - f' public-key: "{peer.get("PublicKey")}"', - f' ip: {ip_address}' - ] - - # 2. Значения для `dns` и `allowed-ips` должны быть представлены в виде списков + proxy_dict = { + 'name': name, + 'type': 'wireguard', + 'server': server, + 'port': port, + 'private-key': interface.get("PrivateKey"), + 'public-key': peer.get("PublicKey"), + 'ip': ip_address + } + if interface.get('DNS'): - dns_servers = [d.strip() for d in interface.get('DNS').split(',')] - y.append(' dns:') - for d in dns_servers: - y.append(f' - {d}') + proxy_dict['dns'] = [d.strip() for d in interface.get('DNS').split(',')] if peer.get('AllowedIPs'): - allowed_ips = [ip.strip() for ip in peer.get('AllowedIPs').split(',')] - y.append(' allowed-ips:') - for ip in allowed_ips: - y.append(f' - "{ip}"') + proxy_dict['allowed-ips'] = [ip.strip() for ip in peer.get('AllowedIPs').split(',')] if peer.get('PresharedKey'): - y.append(f' preshared-key: "{peer.get("PresharedKey")}"') + proxy_dict['preshared-key'] = peer.get("PresharedKey") if peer.get('PersistentKeepalive'): - y.append(f' keep-alive: {peer.get("PersistentKeepalive")}') + proxy_dict['keep-alive'] = int(peer.get("PersistentKeepalive")) - # 1. Параметры AmneziaWG должны быть сгруппированы во вложенном словаре amnezia_keys = ['Jc', 'Jmin', 'Jmax', 'S1', 'S2', 'H1', 'H2', 'H3', 'H4'] amnezia_opts = {key: interface.get(key) for key in amnezia_keys if interface.get(key) is not None} if amnezia_opts: - y.append(' amnezia-wg-option:') - for key, value in amnezia_opts.items(): - y.append(f' {key}: {value}') + proxy_dict['amnezia-wg-option'] = {k: v for k, v in amnezia_opts.items()} - return {"yaml": "\n".join(y), "name": name}, None + # Создаем YAML, который будет добавлен в `proxies:` + # Используем NoQuoteDumper для вывода без кавычек, где это возможно + yaml_string = yaml.dump([proxy_dict], default_flow_style=False, sort_keys=False, allow_unicode=True, indent=2) + + # Удаляем `- ` с первой строки, т.к. мы добавляем только один прокси за раз + # и обертка в список нужна только для yaml.dump + final_yaml = yaml_string.replace('- ', ' ', 1).replace(' name:', '- name:') + + return {"yaml": final_yaml.strip(), "name": name}, None except Exception as e: return None, str(e) @@ -486,12 +499,16 @@ button:hover{filter:brightness(1.1)}
+ +
+ + @@ -519,6 +536,24 @@ function openPanel() { } ed.setValue(initialConfig); ed.clearSelection(); +document.getElementById('vlessLink').addEventListener('input', function() { + var link = this.value; + if (link.startsWith("vless://") && link.includes("#")) { + var name = link.split('#')[1]; + document.getElementById('vlessProxyName').value = decodeURIComponent(name).trim(); + } +}); + +document.getElementById('wgConfig').addEventListener('input', function() { + var conf = this.value; + var nameField = document.getElementById('wgProxyName'); + var endpointMatch = conf.match(/Endpoint\s*=\s*(.+)/); + if (endpointMatch && endpointMatch[1]) { + var server = endpointMatch[1].split(':')[0].trim(); + if (server) nameField.value = 'WG_' + server; + } +}); + function closeM(i){document.getElementById(i).style.display='none'} function showToast(msg){ var t=document.getElementById('toast'); t.innerText=msg||'✅ Выполнено'; t.style.display='block'; setTimeout(()=>{t.style.display='none'}, 2000); } @@ -691,35 +726,48 @@ function openAddProxyModal() { function switchTab(evt, tabName) { var i, tabcontent, tablinks; + + // Сначала скрываем все панели с контентом tabcontent = document.getElementsByClassName("tab-content"); for (i = 0; i < tabcontent.length; i++) { - tabcontent[i].style.display = "none"; tabcontent[i].classList.remove("active"); } + + // Затем убираем активный класс со всех кнопок-вкладок tablinks = document.getElementsByClassName("modal-tabs")[0].getElementsByTagName("button"); for (i = 0; i < tablinks.length; i++) { - tablinks[i].className = tablinks[i].className.replace(" active", ""); + tablinks[i].classList.remove("active"); } - document.getElementById(tabName).style.display = "block"; + + // И только потом показываем нужную вкладку и делаем ее активной document.getElementById(tabName).classList.add("active"); - evt.currentTarget.className += " active"; + evt.currentTarget.classList.add("active"); } function loadWgFile(input) { - var f=input.files[0]; + var f = input.files[0]; if (!f) return; - var r=new FileReader(); - r.onload=function(e){ document.getElementById('wgConfig').value = e.target.result; }; + var r = new FileReader(); + r.onload = function(e) { + var content = e.target.result; + document.getElementById('wgConfig').value = content; + // Trigger the input event to auto-fill the name + document.getElementById('wgConfig').dispatchEvent(new Event('input')); + }; r.readAsText(f); input.value = ''; } function addWireguard() { var conf = document.getElementById('wgConfig').value; + var name = document.getElementById('wgProxyName').value.trim(); if (!conf) return alert("Конфигурация WireGuard не может быть пустой."); var p = new URLSearchParams(); p.append('act', 'add_wireguard'); p.append('config_text', conf); + if (name) { + p.append('proxy_name', name); + } fetch('/', { method: 'POST', body: p }) .then(r => r.json()) .then(d => { @@ -729,15 +777,35 @@ function addWireguard() { pData = d; closeM('addProxyModal'); document.getElementById('wgConfig').value = ''; + document.getElementById('wgProxyName').value = ''; showG(); } }); } function parseVless(){ - var l=document.getElementById('vlessLink').value;if(!l)return; - var p=new URLSearchParams();p.append('act','parse');p.append('link',l); - fetch('/',{method:'POST',body:p}).then(r=>r.json()).then(d=>{if(d.error)alert(d.error);else{pData=d; closeM('addProxyModal'); document.getElementById('vlessLink').value=''; showG();}}) + var link = document.getElementById('vlessLink').value; + var name = document.getElementById('vlessProxyName').value.trim(); + if (!link) return; + var p = new URLSearchParams(); + p.append('act', 'parse'); + p.append('link', link); + if (name) { + p.append('proxy_name', name); + } + fetch('/', { method: 'POST', body: p }) + .then(r => r.json()) + .then(d => { + if (d.error) { + alert(d.error); + } else { + pData = d; + closeM('addProxyModal'); + document.getElementById('vlessLink').value = ''; + document.getElementById('vlessProxyName').value = ''; + showG(); + } + }); } function showG(){ var txt=ed.getValue(); var ls=txt.split(/\\r?\\n/); var grps=[], inG=false; @@ -1054,17 +1122,20 @@ class H(http.server.SimpleHTTPRequestHandler): # --- EXISTING ACTIONS --- if a == 'parse': - d, e = parse_vless(p.get('link', '')) + link = p.get('link', '') + custom_name = p.get('proxy_name') + d, e = parse_vless(link, custom_name) s.wfile.write(json.dumps(d if d else {'error': e}).encode('utf-8')); return if a == 'add_wireguard': config_text = p.get('config_text', '') + custom_name = p.get('proxy_name') if not config_text: s.wfile.write(json.dumps({'error': 'Empty config'}).encode('utf-8')) return - proxy_data, err = parse_wireguard(config_text) + proxy_data, err = parse_wireguard(config_text, custom_name) if err: s.wfile.write(json.dumps({'error': err}).encode('utf-8')) return