Release 1.3.0: Hysteria2 BBR/Salamander/masquerade options and subscription URL import.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Navis
2026-07-28 07:26:17 +03:00
co-authored by Cursor
parent 7eaf53f7c2
commit 5d2cb6d76a
18 changed files with 807 additions and 52 deletions
+157 -4
View File
@@ -112,6 +112,32 @@
.stack { display: grid; gap: 10px; margin-bottom: 14px; }
details.hy2 {
margin-bottom: 14px;
border: 1px solid var(--line);
border-radius: 16px;
padding: 10px 12px;
background: rgba(255,255,255,.55);
}
details.hy2 summary {
cursor: pointer;
font-weight: 700;
font-size: .84rem;
color: var(--accent);
list-style: none;
}
details.hy2 .grid2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-top: 10px;
}
details.hy2 label.field { margin-top: 6px; }
.check {
display: flex; align-items: center; gap: 8px;
font-size: .88rem; margin-top: 8px;
}
.row {
display: flex; align-items: center; justify-content: space-between;
gap: 12px; padding: 12px 14px;
@@ -299,8 +325,71 @@
<label class="field" for="proxy">Ссылка сервера</label>
<input id="proxy" type="text" placeholder="naive+https://… или hysteria2://pass@host:443/" spellcheck="false" />
</div>
<div>
<label class="field" for="subUrl">URL подписки</label>
<div class="profile-bar">
<input id="subUrl" type="text" placeholder="https://…/sub" spellcheck="false" />
<button class="icon-btn" id="subBtn" type="button" title="Обновить подписку" style="width:auto;padding:0 14px;font-size:.8rem"></button>
</div>
</div>
</div>
<details class="hy2" id="hy2Box">
<summary>Hysteria 2 · BBR / Salamander / маскировка</summary>
<div class="grid2">
<div>
<label class="field" for="hy2Congestion">Congestion</label>
<select id="hy2Congestion">
<option value="bbr">BBR</option>
<option value="reno">Reno</option>
</select>
</div>
<div>
<label class="field" for="hy2Bbr">BBR profile</label>
<select id="hy2Bbr">
<option value="standard">standard</option>
<option value="conservative">conservative</option>
<option value="aggressive">aggressive</option>
</select>
</div>
<div>
<label class="field" for="hy2Up">Brutal up</label>
<input id="hy2Up" type="text" placeholder="пусто = BBR" />
</div>
<div>
<label class="field" for="hy2Down">Brutal down</label>
<input id="hy2Down" type="text" placeholder="пусто = BBR" />
</div>
<div>
<label class="field" for="hy2Obfs">Obfs</label>
<select id="hy2Obfs">
<option value="">нет</option>
<option value="salamander">salamander</option>
<option value="gecko">gecko</option>
</select>
</div>
<div>
<label class="field" for="hy2ObfsPass">Obfs password</label>
<input id="hy2ObfsPass" type="text" placeholder="пароль salamander/gecko" />
</div>
<div>
<label class="field" for="hy2Sni">SNI (маскарад HTTPS)</label>
<input id="hy2Sni" type="text" placeholder="cdn.example.com" />
</div>
<div>
<label class="field" for="hy2Pin">pinSHA256</label>
<input id="hy2Pin" type="text" placeholder="опционально" />
</div>
<div>
<label class="field" for="hy2Hop">Hop interval</label>
<input id="hy2Hop" type="text" placeholder="30s" />
</div>
</div>
<label class="check"><input id="hy2Insecure" type="checkbox" /> TLS insecure</label>
<label class="check"><input id="hy2Fast" type="checkbox" /> Fast Open</label>
<label class="check"><input id="hy2Lazy" type="checkbox" /> Lazy connect</label>
</details>
<div class="row">
<span>Системный прокси Windows</span>
<label class="switch">
@@ -373,8 +462,51 @@
const updateBtn = $("updateBtn");
const pingList = $("pingList");
const verLabel = $("verLabel");
const subUrl = $("subUrl");
const subBtn = $("subBtn");
const SHOP_URL = "https://evilfox.win/";
function readHy2() {
return {
congestion: $("hy2Congestion").value,
bbr_profile: $("hy2Bbr").value,
bandwidth_up: $("hy2Up").value.trim(),
bandwidth_down: $("hy2Down").value.trim(),
obfs: $("hy2Obfs").value,
obfs_password: $("hy2ObfsPass").value.trim(),
sni: $("hy2Sni").value.trim(),
insecure: !!$("hy2Insecure").checked,
pin_sha256: $("hy2Pin").value.trim(),
fast_open: !!$("hy2Fast").checked,
lazy: !!$("hy2Lazy").checked,
hop_interval: $("hy2Hop").value.trim()
};
}
function fillHy2(h) {
if (!h) return;
if (h.congestion) $("hy2Congestion").value = h.congestion;
if (h.bbr_profile) $("hy2Bbr").value = h.bbr_profile;
$("hy2Up").value = h.bandwidth_up || "";
$("hy2Down").value = h.bandwidth_down || "";
$("hy2Obfs").value = h.obfs || "";
$("hy2ObfsPass").value = h.obfs_password || "";
$("hy2Sni").value = h.sni || "";
$("hy2Pin").value = h.pin_sha256 || "";
$("hy2Hop").value = h.hop_interval || "";
$("hy2Insecure").checked = !!h.insecure;
$("hy2Fast").checked = !!h.fast_open;
$("hy2Lazy").checked = !!h.lazy;
}
async function persistHy2IfNeeded(protocol) {
const p = (protocol || "").toLowerCase();
const link = proxy.value.trim().toLowerCase();
if (p === "hysteria2" || link.startsWith("hysteria2://") || link.startsWith("hy2://")) {
await saveHy2(readHy2());
}
}
let busy = false;
let connected = false;
let formHydrated = false;
@@ -382,10 +514,15 @@
let profiles = [];
function markDirty() { dirty = true; }
[proxy, nameInput, sysproxy].forEach((el) => {
[
proxy, nameInput, sysproxy, subUrl,
$("hy2Congestion"), $("hy2Bbr"), $("hy2Up"), $("hy2Down"),
$("hy2Obfs"), $("hy2ObfsPass"), $("hy2Sni"), $("hy2Pin"), $("hy2Hop"),
$("hy2Insecure"), $("hy2Fast"), $("hy2Lazy")
].forEach((el) => {
if (!el) return;
el.addEventListener("input", markDirty);
el.addEventListener("change", markDirty);
el.addEventListener("focus", markDirty);
});
function setMeta(text, kind) {
@@ -465,12 +602,16 @@
fillProfiles(state.profiles || [], state.active_profile || state.profile);
renderPings(state.pings || []);
renderUpdate(state.update, state.version);
if (typeof state.subscription_url === "string" && !dirty) {
subUrl.value = state.subscription_url;
}
if (syncForm || (!formHydrated && !dirty)) {
const active = (state.profiles || []).find((p) => p.name === profile.value) || {};
nameInput.value = active.name || state.active_profile || "";
proxy.value = typeof state.proxy === "string" ? state.proxy : (active.proxy || "");
if (typeof state.system_proxy === "boolean") sysproxy.checked = state.system_proxy;
fillHy2(state.hy2);
formHydrated = true;
if (syncForm) dirty = false;
}
@@ -498,7 +639,7 @@
async function withBusy(fn) {
if (busy) return;
busy = true;
[btn, coreBtn, saveBtn, addBtn, delBtn, profile, pingBtn, updCheckBtn, updateBtn].forEach((b) => { if (b) b.disabled = true; });
[btn, coreBtn, saveBtn, addBtn, delBtn, profile, pingBtn, updCheckBtn, updateBtn, subBtn].forEach((b) => { if (b) b.disabled = true; });
try {
await fn();
} finally {
@@ -507,7 +648,7 @@
await refresh({ syncForm: true });
} catch (e) {
setMeta(String(e), "err");
[btn, coreBtn, saveBtn, addBtn, delBtn, profile, pingBtn, updCheckBtn, updateBtn].forEach((b) => { if (b) b.disabled = false; });
[btn, coreBtn, saveBtn, addBtn, delBtn, profile, pingBtn, updCheckBtn, updateBtn, subBtn].forEach((b) => { if (b) b.disabled = false; });
}
}
}
@@ -527,6 +668,7 @@
} else {
setMeta("Подключение…");
await saveProfile(nameInput.value.trim() || profile.value, proxy.value.trim(), !!sysproxy.checked);
await persistHy2IfNeeded();
await connect();
setMeta("Туннель активен", "ok");
}
@@ -536,6 +678,7 @@
saveBtn.addEventListener("click", () => withBusy(async () => {
try {
await saveProfile(nameInput.value.trim() || profile.value, proxy.value.trim(), !!sysproxy.checked);
await persistHy2IfNeeded();
setMeta("Профиль сохранён", "ok");
} catch (e) { setMeta(String(e), "err"); }
}));
@@ -589,6 +732,16 @@
shopBtn.addEventListener("click", openShop);
shopLink.addEventListener("click", openShop);
subBtn.addEventListener("click", () => withBusy(async () => {
try {
setMeta("Загрузка подписки…");
const n = await importSubscription(subUrl.value.trim());
formHydrated = false;
dirty = false;
setMeta("Импортировано профилей: " + n, "ok");
} catch (e) { setMeta(String(e), "err"); }
}));
pingBtn.addEventListener("click", () => withBusy(async () => {
try {
setMeta("Проверка серверов…");