Убрал кнопку Файл

This commit is contained in:
Petro1990 2025-11-26 11:34:30 +03:00
parent f3625ea970
commit e1d6b78207
2 changed files with 121 additions and 50 deletions

View File

@ -17,7 +17,7 @@ echo "=== Установка Mihomo Studio из репозитория (v18.3 +
echo "[1/4] Проверка Python и модулей..." echo "[1/4] Проверка Python и модулей..."
opkg update opkg update
# python3-codecs ОБЯЗАТЕЛЕН для работы urllib и кодировки idna # 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 for pkg in $PACKAGES; do
if ! opkg list-installed | grep -q "^$pkg"; then if ! opkg list-installed | grep -q "^$pkg"; then

View File

@ -12,6 +12,7 @@ import time
import shutil import shutil
import glob import glob
import json import json
import yaml
from datetime import datetime 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: try:
if not link.startswith("vless://"): return None, "Link error" if not link.startswith("vless://"): return None, "Link error"
main = link[8:] main = link[8:]
name = "VLESS" 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) name = re.sub(r'[\[\]\{\}\"\']', '', name)
user_srv = main.split('?')[0] user_srv = main.split('?')[0]
params = urllib.parse.parse_qs(main.split('?')[1]) if '?' in main else {} params = urllib.parse.parse_qs(main.split('?')[1]) if '?' in main else {}
@ -90,7 +96,7 @@ def parse_vless(link):
return None, str(e) return None, str(e)
def parse_wireguard(config_text): def parse_wireguard(config_text, custom_name=None):
try: try:
name = "WireGuard" name = "WireGuard"
params = {} params = {}
@ -115,57 +121,64 @@ def parse_wireguard(config_text):
interface = params.get('interface', {}) interface = params.get('interface', {})
peer = params.get('peer', {}) peer = params.get('peer', {})
# Extract name from comment if exists server, port_str = peer.get('Endpoint', ':').rsplit(':', 1)
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 = 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] ip_address = interface.get("Address", "").split("/")[0]
y = [ proxy_dict = {
f'- name: "{name}"', 'name': name,
' type: wireguard', 'type': 'wireguard',
f' server: {server}', 'server': server,
f' port: {port}', 'port': port,
f' private-key: "{interface.get("PrivateKey")}"', 'private-key': interface.get("PrivateKey"),
f' public-key: "{peer.get("PublicKey")}"', 'public-key': peer.get("PublicKey"),
f' ip: {ip_address}' 'ip': ip_address
] }
# 2. Значения для `dns` и `allowed-ips` должны быть представлены в виде списков
if interface.get('DNS'): if interface.get('DNS'):
dns_servers = [d.strip() for d in interface.get('DNS').split(',')] proxy_dict['dns'] = [d.strip() for d in interface.get('DNS').split(',')]
y.append(' dns:')
for d in dns_servers:
y.append(f' - {d}')
if peer.get('AllowedIPs'): if peer.get('AllowedIPs'):
allowed_ips = [ip.strip() for ip in peer.get('AllowedIPs').split(',')] proxy_dict['allowed-ips'] = [ip.strip() for ip in peer.get('AllowedIPs').split(',')]
y.append(' allowed-ips:')
for ip in allowed_ips:
y.append(f' - "{ip}"')
if peer.get('PresharedKey'): if peer.get('PresharedKey'):
y.append(f' preshared-key: "{peer.get("PresharedKey")}"') proxy_dict['preshared-key'] = peer.get("PresharedKey")
if peer.get('PersistentKeepalive'): 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_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} amnezia_opts = {key: interface.get(key) for key in amnezia_keys if interface.get(key) is not None}
if amnezia_opts: if amnezia_opts:
y.append(' amnezia-wg-option:') proxy_dict['amnezia-wg-option'] = {k: v for k, v in amnezia_opts.items()}
for key, value in amnezia_opts.items():
y.append(f' {key}: {value}')
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: except Exception as e:
return None, str(e) return None, str(e)
@ -486,12 +499,16 @@ button:hover{filter:brightness(1.1)}
<div id="vlessTab" class="tab-content active"> <div id="vlessTab" class="tab-content active">
<label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)">Ссылка VLESS:</label> <label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)">Ссылка VLESS:</label>
<input id="vlessLink" placeholder="vless://..." style="margin-bottom:10px;"> <input id="vlessLink" placeholder="vless://..." style="margin-bottom:10px;">
<label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)">Имя прокси (необязательно):</label>
<input id="vlessProxyName" placeholder="Автоматически из ссылки" style="margin-bottom:10px;">
<button onclick="parseVless()" class="btn-s" style="width:100%; justify-content:center;">Добавить</button> <button onclick="parseVless()" class="btn-s" style="width:100%; justify-content:center;">Добавить</button>
</div> </div>
<div id="wgTab" class="tab-content"> <div id="wgTab" class="tab-content">
<label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)">Конфигурация WireGuard:</label> <label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)">Конфигурация WireGuard:</label>
<textarea id="wgConfig" rows="8" placeholder="Вставьте содержимое .conf файла сюда..." style="width:100%; margin-bottom:10px;"></textarea> <textarea id="wgConfig" rows="8" placeholder="Вставьте содержимое .conf файла сюда..." style="width:100%; margin-bottom:10px;"></textarea>
<label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)">Имя прокси (необязательно):</label>
<input id="wgProxyName" placeholder="Автоматически из Endpoint" style="margin-bottom:10px;">
<input type="file" id="wgFile" accept=".conf" style="display:none" onchange="loadWgFile(this)"> <input type="file" id="wgFile" accept=".conf" style="display:none" onchange="loadWgFile(this)">
<button onclick="document.getElementById('wgFile').click()" class="btn-u" style="width:100%; justify-content:center; margin-bottom:10px;">📂 Или загрузить .conf файл</button> <button onclick="document.getElementById('wgFile').click()" class="btn-u" style="width:100%; justify-content:center; margin-bottom:10px;">📂 Или загрузить .conf файл</button>
<button onclick="addWireguard()" class="btn-s" style="width:100%; justify-content:center;">Добавить</button> <button onclick="addWireguard()" class="btn-s" style="width:100%; justify-content:center;">Добавить</button>
@ -519,6 +536,24 @@ function openPanel() {
} }
ed.setValue(initialConfig); ed.clearSelection(); 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 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); } 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) { function switchTab(evt, tabName) {
var i, tabcontent, tablinks; var i, tabcontent, tablinks;
// Сначала скрываем все панели с контентом
tabcontent = document.getElementsByClassName("tab-content"); tabcontent = document.getElementsByClassName("tab-content");
for (i = 0; i < tabcontent.length; i++) { for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
tabcontent[i].classList.remove("active"); tabcontent[i].classList.remove("active");
} }
// Затем убираем активный класс со всех кнопок-вкладок
tablinks = document.getElementsByClassName("modal-tabs")[0].getElementsByTagName("button"); tablinks = document.getElementsByClassName("modal-tabs")[0].getElementsByTagName("button");
for (i = 0; i < tablinks.length; i++) { 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"); document.getElementById(tabName).classList.add("active");
evt.currentTarget.className += " active"; evt.currentTarget.classList.add("active");
} }
function loadWgFile(input) { function loadWgFile(input) {
var f=input.files[0]; var f = input.files[0];
if (!f) return; if (!f) return;
var r=new FileReader(); var r = new FileReader();
r.onload=function(e){ document.getElementById('wgConfig').value = e.target.result; }; 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); r.readAsText(f);
input.value = ''; input.value = '';
} }
function addWireguard() { function addWireguard() {
var conf = document.getElementById('wgConfig').value; var conf = document.getElementById('wgConfig').value;
var name = document.getElementById('wgProxyName').value.trim();
if (!conf) return alert("Конфигурация WireGuard не может быть пустой."); if (!conf) return alert("Конфигурация WireGuard не может быть пустой.");
var p = new URLSearchParams(); var p = new URLSearchParams();
p.append('act', 'add_wireguard'); p.append('act', 'add_wireguard');
p.append('config_text', conf); p.append('config_text', conf);
if (name) {
p.append('proxy_name', name);
}
fetch('/', { method: 'POST', body: p }) fetch('/', { method: 'POST', body: p })
.then(r => r.json()) .then(r => r.json())
.then(d => { .then(d => {
@ -729,15 +777,35 @@ function addWireguard() {
pData = d; pData = d;
closeM('addProxyModal'); closeM('addProxyModal');
document.getElementById('wgConfig').value = ''; document.getElementById('wgConfig').value = '';
document.getElementById('wgProxyName').value = '';
showG(); showG();
} }
}); });
} }
function parseVless(){ function parseVless(){
var l=document.getElementById('vlessLink').value;if(!l)return; var link = document.getElementById('vlessLink').value;
var p=new URLSearchParams();p.append('act','parse');p.append('link',l); var name = document.getElementById('vlessProxyName').value.trim();
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();}}) 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(){ function showG(){
var txt=ed.getValue(); var ls=txt.split(/\\r?\\n/); var grps=[], inG=false; 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 --- # --- EXISTING ACTIONS ---
if a == 'parse': 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')); s.wfile.write(json.dumps(d if d else {'error': e}).encode('utf-8'));
return return
if a == 'add_wireguard': if a == 'add_wireguard':
config_text = p.get('config_text', '') config_text = p.get('config_text', '')
custom_name = p.get('proxy_name')
if not config_text: if not config_text:
s.wfile.write(json.dumps({'error': 'Empty config'}).encode('utf-8')) s.wfile.write(json.dumps({'error': 'Empty config'}).encode('utf-8'))
return return
proxy_data, err = parse_wireguard(config_text) proxy_data, err = parse_wireguard(config_text, custom_name)
if err: if err:
s.wfile.write(json.dumps({'error': err}).encode('utf-8')) s.wfile.write(json.dumps({'error': err}).encode('utf-8'))
return return