Release v0.1.2

This commit is contained in:
nikit3s
2026-10-10 14:58:04 +03:00
parent b2ae91b5b3
commit 3cdc30ec2a
33 changed files with 1218 additions and 212 deletions
+142 -17
View File
@@ -1,7 +1,8 @@
// SbLauncher — GUI для sing-box.
//
// ПРИНЦИП (изменён по сравнению с прошлой версией):
// Источник правды — profiles.json (серверы, общее) и local.json (ключ,
// Источник правды — profiles.base.json (базовые серверы), profiles.json
// (личные серверы) и local.json (ключ,
// адрес, адаптер — своё на каждой машине). Конфиг sing-box НЕ хранится:
// он собирается из шаблона templates\*.json при каждом запуске и кладётся
// в run\config.json. Поэтому папок conf_* больше нет, и править один и
@@ -187,6 +188,7 @@ namespace SbLauncher
public Paths(string root) { Root = root; }
public string Profiles { get { return Path.Combine(Root, "profiles.json"); } }
public string BaseProfiles { get { return Path.Combine(Root, "profiles.base.json"); } }
public string Local { get { return Path.Combine(Root, "local.json"); } }
public string TemplatesDir { get { return Path.Combine(Root, "templates"); } }
public string SbExe { get { return Path.Combine(Root, Path.Combine("bin", "sb.exe")); } }
@@ -217,6 +219,7 @@ namespace SbLauncher
public string SpiderX = ""; // старые профили сохраняют это поле
public string Protocol = "vless";
public Dictionary<string, object> Outbound; // точная конфигурация импортированного ключа
public string BaseId = ""; // стабильный id только у серверов из релиза
public string Endpoint
{
@@ -232,19 +235,115 @@ namespace SbLauncher
public int Mtu = 1280;
}
// profiles.json — локальная копия серверов; в тестовом репозитории не коммитится
// Релиз обновляет profiles.base.json. profiles.json хранит только личные
// серверы и скрытые базовые id; local.json независимо хранит ключ и адрес.
public class Profiles
{
public WgCfg Wg = new WgCfg();
public List<ServerCfg> Servers = new List<ServerCfg>();
public HashSet<string> HiddenBaseServers = new HashSet<string>(StringComparer.Ordinal);
public bool Layered;
public bool NeedsMigration;
bool _wgOverride;
public static Profiles Load(string path)
{
Profiles personal = ReadFile(path, false);
string basePath = Path.Combine(Path.GetDirectoryName(Path.GetFullPath(path)), "profiles.base.json");
if (!string.Equals(Path.GetFileName(path), "profiles.json", StringComparison.OrdinalIgnoreCase) ||
!File.Exists(basePath)) return personal;
Profiles baseline = ReadFile(basePath, true);
Profiles merged = new Profiles();
merged.Layered = true;
merged.NeedsMigration = File.Exists(path) && !personal.Layered;
merged.HiddenBaseServers.UnionWith(personal.HiddenBaseServers);
merged._wgOverride = personal._wgOverride && (personal.Layered || !SameWg(personal.Wg, baseline.Wg));
merged.Wg = merged._wgOverride ? personal.Wg : baseline.Wg;
// Старый общий список превращается в личный слой без дубликатов.
// Непохожие на базовые записи сохраняем, включая старые настройки.
List<ServerCfg> own = new List<ServerCfg>();
foreach (ServerCfg server in personal.Servers)
{
bool sharedLegacy = false;
if (!personal.Layered)
foreach (ServerCfg shared in baseline.Servers)
if (SameProfile(shared, server)) { sharedLegacy = true; break; }
if (!sharedLegacy) own.Add(server);
}
// Личные коды не меняем при появлении базового сервера с тем же кодом.
List<ServerCfg> reserved = new List<ServerCfg>(own);
foreach (ServerCfg shared in baseline.Servers)
{
if (merged.HiddenBaseServers.Contains(shared.BaseId)) continue;
GiveUniqueCode(shared, reserved);
reserved.Add(shared);
merged.Servers.Add(shared);
}
merged.Servers.AddRange(own);
return merged;
}
static bool SameWg(WgCfg a, WgCfg b)
{
return a.Server == b.Server && a.Port == b.Port &&
a.PeerPublicKey == b.PeerPublicKey && a.Mtu == b.Mtu;
}
static bool SameProfile(ServerCfg a, ServerCfg b)
{
// Compare complete connection settings: a customized SNI/transport
// with the same endpoint and UUID must remain a personal profile.
return KeyImporter.Same(
new ServerCfg { Protocol = a.Protocol, Server = a.Server, Port = a.Port,
Outbound = ProxyOutbound.Build(a, ConfigBuilder.ModeDirect) },
new ServerCfg { Protocol = b.Protocol, Server = b.Server, Port = b.Port,
Outbound = ProxyOutbound.Build(b, ConfigBuilder.ModeDirect) });
}
public static void GiveUniqueCode(ServerCfg server, List<ServerCfg> all)
{
string original = server.Code;
int number = 2;
while (all.Exists(delegate(ServerCfg existing) {
return string.Equals(existing.Code, server.Code, StringComparison.OrdinalIgnoreCase);
})) server.Code = original + (number++).ToString(CultureInfo.InvariantCulture);
}
public Profiles Copy()
{
Profiles next = new Profiles();
next.Wg = Wg;
next.Servers.AddRange(Servers);
next.HiddenBaseServers.UnionWith(HiddenBaseServers);
next.Layered = Layered;
next.NeedsMigration = NeedsMigration;
next._wgOverride = _wgOverride;
return next;
}
public void Remove(ServerCfg server)
{
Servers.Remove(server);
if (server.BaseId.Length > 0) HiddenBaseServers.Add(server.BaseId);
}
static Profiles ReadFile(string path, bool isBase)
{
Profiles p = new Profiles();
if (!File.Exists(path)) return p;
Dictionary<string, object> root = J.Parse(File.ReadAllText(path, Encoding.UTF8));
if (root == null) return p;
p.Layered = J.Int(root, "schema_version", 0) >= 2;
object hidden;
if (root.TryGetValue("hidden_base_servers", out hidden))
{
object[] ids = hidden as object[];
if (ids != null) foreach (object id in ids)
if (id is string) p.HiddenBaseServers.Add((string)id);
}
object wo;
if (root.TryGetValue("wireguard", out wo))
@@ -252,6 +351,7 @@ namespace SbLauncher
Dictionary<string, object> w = J.Obj(wo);
if (w != null)
{
p._wgOverride = true;
p.Wg.Server = J.Str(w, "server", "");
p.Wg.Port = J.Int(w, "port", 51820);
p.Wg.PeerPublicKey = J.Str(w, "peer_public_key", "");
@@ -276,6 +376,12 @@ namespace SbLauncher
s.Flow = J.Str(d, "flow", "xtls-rprx-vision");
s.SpiderX = J.Str(d, "spider_x", "");
s.Protocol = J.Str(d, "protocol", "vless");
if (isBase)
{
s.BaseId = J.Str(d, "id", s.Code);
if (s.BaseId.Length == 0 || p.Servers.Exists(delegate(ServerCfg existing) { return existing.BaseId == s.BaseId; }))
throw new FormatException("У базовых серверов должны быть уникальные непустые id.");
}
object outbound;
if (d.TryGetValue("outbound", out outbound)) s.Outbound = J.Obj(outbound);
if (s.Outbound != null) s.Protocol = J.Str(s.Outbound, "type", s.Protocol);
@@ -284,25 +390,43 @@ namespace SbLauncher
return p;
}
// Пишется только когда сервер добавили/удалили из GUI. Файл
// Пишется при миграции или добавлении/удалении сервера из GUI. Файл
// пересобирается целиком, поэтому свои комментарии ВНУТРИ списка
// серверов при этом теряются — шапка восстанавливается.
public void Save(string path)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("// Серверы и общие параметры этого тестового экземпляра.");
sb.AppendLine("// Храните данные серверов локально, а ключ WireGuard — в local.json.");
sb.AppendLine("{");
sb.AppendLine(" \"wireguard\": {");
sb.AppendLine(" \"server\": \"" + J.Esc(Wg.Server) + "\",");
sb.AppendLine(" \"port\": " + Wg.Port.ToString(CultureInfo.InvariantCulture) + ",");
sb.AppendLine(" \"peer_public_key\": \"" + J.Esc(Wg.PeerPublicKey) + "\",");
sb.AppendLine(" \"mtu\": " + Wg.Mtu.ToString(CultureInfo.InvariantCulture));
sb.AppendLine(" },");
sb.AppendLine(" \"servers\": [");
for (int i = 0; i < Servers.Count; i++)
if (NeedsMigration && File.Exists(path))
{
ServerCfg s = Servers[i];
string backup = Path.Combine(Path.GetDirectoryName(Path.GetFullPath(path)), "profiles.legacy-backup.json");
if (!File.Exists(backup)) File.Copy(path, backup);
}
List<ServerCfg> stored = Layered
? Servers.FindAll(delegate(ServerCfg server) { return server.BaseId.Length == 0; })
: Servers;
StringBuilder sb = new StringBuilder();
sb.AppendLine("// Личные серверы и настройки списка. Не включайте этот файл в релиз.");
sb.AppendLine("// Базовые серверы — profiles.base.json; ключ и адрес — local.json.");
sb.AppendLine("{");
if (Layered)
{
sb.AppendLine(" \"schema_version\": 2,");
List<string> hidden = new List<string>(HiddenBaseServers);
hidden.Sort(StringComparer.Ordinal);
sb.AppendLine(" \"hidden_base_servers\": " + J.ToJson(hidden.ToArray()) + ",");
}
if (!Layered || _wgOverride)
{
sb.AppendLine(" \"wireguard\": {");
sb.AppendLine(" \"server\": \"" + J.Esc(Wg.Server) + "\",");
sb.AppendLine(" \"port\": " + Wg.Port.ToString(CultureInfo.InvariantCulture) + ",");
sb.AppendLine(" \"peer_public_key\": \"" + J.Esc(Wg.PeerPublicKey) + "\",");
sb.AppendLine(" \"mtu\": " + Wg.Mtu.ToString(CultureInfo.InvariantCulture));
sb.AppendLine(" },");
}
sb.AppendLine(" \"servers\": [");
for (int i = 0; i < stored.Count; i++)
{
ServerCfg s = stored[i];
sb.AppendLine(" {");
sb.AppendLine(" \"code\": \"" + J.Esc(s.Code) + "\",");
sb.AppendLine(" \"name\": \"" + J.Esc(s.Name) + "\",");
@@ -322,11 +446,12 @@ namespace SbLauncher
sb.AppendLine(" \"protocol\": \"" + J.Esc(s.Protocol) + "\"" + (s.Outbound != null ? "," : ""));
if (s.Outbound != null)
sb.AppendLine(" \"outbound\": " + J.ToJson(s.Outbound));
sb.AppendLine(i == Servers.Count - 1 ? " }" : " },");
sb.AppendLine(i == stored.Count - 1 ? " }" : " },");
}
sb.AppendLine(" ]");
sb.AppendLine("}");
J.Write(path, sb.ToString());
NeedsMigration = false;
}
}