Убрал кнопку Файл
This commit is contained in:
parent
f3625ea970
commit
e1d6b78207
|
|
@ -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
|
||||||
|
|
|
||||||
169
mihomo_editor.py
169
mihomo_editor.py
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue