Release v0.9.1
Downloads
macOS (Universal) - Supports both Apple Silicon and Intel
Option 1: Installation Script (Recommended)
Install with a single command (version v0.9.1):
curl -fsSL https://raw.githubusercontent.com/Leadaxe/singbox-launcher/develop/scripts/install-macos.sh | bash -s -- v0.9.1The script will:
- Download the release archive
- Extract and install to
/Applications/ - Fix macOS quarantine attributes and permissions
- Launch the application automatically
Option 2: Manual Installation
- Download:
singbox-launcher-v0.9.1-macos.zip - Extract the ZIP file
- Remove quarantine attribute (required):
xattr -cr "singbox-launcher.app" && chmod +x "singbox-launcher.app/Contents/MacOS/singbox-launcher"
- Double-click
singbox-launcher.appto run- If macOS blocks the app, go to System Settings → Privacy & Security and click "Open Anyway"
- Alternatively, right-click the app and select "Open" (first time only)
Windows (amd64)
- Download:
singbox-launcher-v0.9.1-win64.zip - Extract the ZIP file to a folder, for example:
C:\Program Files\singbox-launcher\ - Run
singbox-launcher.exefrom that folder- You may need administrator rights to install to Program Files
- The launcher will automatically download
sing-boxandwintun.dllon first launch
Windows 7 (x86, legacy)
- Download:
singbox-launcher-v0.9.1-win7-32.zip - Extract the ZIP file to a folder and run
singbox-launcher-win7-32.exe- For Windows 7 / 32-bit or legacy compatibility only
Linux Support
⚠️ Linux build temporarily unavailable - мы ищем тестировщика для ручного тестирования перед включением автоматической сборки.
Checksums
See checksums.txt for SHA256 checksums of all files.
Highlights (EN)
Hotfix release for v0.9.0 cold-start regressions. If you installed v0.9.0 cleanly (no state.json from a previous version) and tried to start sing-box, you would have hit a FATAL: default outbound not found: proxy-out and an unfriendly Refresh failed: load state: state: file not found from the per-source Refresh button. Both are fixed; existing broken installs heal themselves on the next wizard load.
Fixed
FATAL: default outbound not found: proxy-outon first Start. When the wizard loaded fromwizard_template.jsonwith nostate.jsonpresent, only the legacymodel.ParserConfigJSONview was populated — the canonicalmodel.GlobalOutbounds(the v5 source of truth for selector / urltest groups) stayed empty. On Save →state.Connections.Outbounds = []. On Rebuild →config.jsongot the parsed nodes plusdirect-out, but noproxy-out/auto-proxy-out/vpn ①/vpn ②selectors. The template-preservedroute.final = "proxy-out"then pointed at nothing, and sing-box exit-2'd. Two-part fix: (1)loadConfigFromFilenow seedsmodel.GlobalOutboundsfrom the parsed template parser_config, so fresh installs save state correctly; (2)presenter.restoreParserConfigheals already-brokenstate.jsonfiles on next wizard load — ifconnections.outboundsis empty AND the template defines selectors, they are re-seeded. Non-empty state is left alone (the user may have explicitly removed outbounds).- Per-source Refresh required
state.jsonon disk. The Refresh button on each source row calledConfigService.RefreshSingleSubscription, which didstate.Loadbefore mutating meta. On a fresh install with template-loaded sources but no Save yet, Load returnedErrNotFoundand the UI showedRefresh failed: load state: state: file not found. The whole point of SPEC 052's per-source raw cache is that subscription fetch is decoupled from state save — Refresh should not silently force a Save (Save is a deliberate user action that commits all wizard edits). NewRefreshSourceInPlace(*state.Source)operates on an in-memory pointer, fetches the URL, writes raw cache to disk, and mutates Meta in place;state.jsonis not touched. The wizard takes a deep-copy snapshot of the source on the UI thread (includingMetaclone — the inner refresher mutates Meta via pointer on failure), runs the network fetch in a goroutine, and on UI thread re-locates the source by ID in the model and assigns the snapshot back. The state-awareRefreshSingleSubscriptionis kept for the auto-update heartbeat and VPN-event retry paths where state always exists andSubscriptionMuserialization matters. [ERROR]log noise on every cold start.LoadClashAPIConfig(api/clash.go) and theclash_api_tabselector-group reader logged at ERROR severity wheneverconfig.jsonwas missing — which is the normal state on a fresh install before the user has done Save → Update → Rebuild. Both now branch onos.IsNotExist(err)and route the missing-file case throughDebugLogwith a(cold start)tag. Any other failure (corrupt JSON, permission denied, missingexperimental.clash_apisection) still goes through ErrorLog as before. Behavior unchanged — only severity differs.- "Configuration Not Found" dialog instructions. The text shown on cold start still pointed at the pre-SPEC 045 / 052 mental model ("download Wizard, use Wizard to generate a configuration file, press Start"). "Download Wizard" never reflected reality — the Wizard is not downloadable. After SPEC 045 the user-facing label is also "Configurator", not "Wizard". And the modern flow needs an explicit
Updatestep between Save and Start soconfig.jsonis rebuilt from the freshly-fetched raw cache. New text in all 11 locales: "open Configurator → add subscription URLs and Save → click Update on dashboard → press Start".internal/locale/en.json(embedded viago:embed) takes effect immediately;bin/locale/*.jsonfiles ship in fresh installs.
Technical / Internal
- New
ConfigService.RefreshSourceInPlace(*state.Source) (changed bool, err error)incore/config_service.go. Mirror methods added to the wizard-sideConfigServiceinterface andConfigServiceAdapter. NoSubscriptionMulock — the call only writes per-source raw cache (atomic.tmp + Rename); concurrent UI clicks on the same source row are blocked by the button's own busy state. loadConfigFromFile(ui/configurator/configurator.go) now importsencoding/json+core/configand parses the templateparserConfigJSONonce to seedmodel.GlobalOutbounds. Failure to parse is logged as a warning; the wizard still opens.restoreParserConfig(ui/configurator/presentation/presenter_state.go) gains a heal-on-empty branch usingmodel.TemplateData.ParserConfig. The check is conservative: only triggers whenlen(GlobalOutbounds) == 0AND template has outbounds defined.refreshOneSourceFromUI(ui/configurator/tabs/source_tab.go) was rewritten around the snapshot+goroutine pattern: take a deep copy on the UI thread (clone theMetapointer's pointee — refresh internals mutate via pointer on failure), refresh the copy in background, on UI thread find the source by ID (slice may have reallocated due to concurrent Add/Del) and assign the snapshot back. Marks the model dirty so Save shows*.LoadClashAPIConfigandclash_api_tab.CreateClashAPITab+ popup callback now branch onos.IsNotExist(err)for log severity. Theosimport was added toui/clash_api_tab.go.
Migration notes
- No migration required. Existing v0.9.0 installs that hit the empty-outbounds bug will be auto-healed on next wizard open: the heal-on-empty branch reads template selectors and writes them into
state.Connections.Outboundson the next Save. After Save → 🔄 Update → Start, sing-box launches cleanly. - The unfriendly "Refresh failed: state: file not found" toast is gone; on a fresh install you can click Refresh on a row immediately after adding the URL — no Save needed first.
Основное (RU)
Hotfix к v0.9.0 — починка двух cold-start регрессий. Если вы поставили v0.9.0 на чистый профиль (без state.json от предыдущей версии) и попытались стартовать sing-box, получали FATAL: default outbound not found: proxy-out и непонятный Refresh failed: load state: state: file not found с per-source кнопки Refresh. Оба бага починены; уже сломанные установки лечатся сами при следующем открытии конфигуратора.
Исправлено
FATAL: default outbound not found: proxy-outна первом Start. Когда конфигуратор грузил данные изwizard_template.jsonбезstate.json, заполнялся только legacymodel.ParserConfigJSON-view, а canonicalmodel.GlobalOutbounds(в v5 — источник правды для selector/urltest групп) оставался пустым. На Save →state.Connections.Outbounds = []. На Rebuild →config.jsonсобирался с распарсенными нодами +direct-out, безproxy-out/auto-proxy-out/vpn ①/vpn ②. Сохранённый из шаблонаroute.final = "proxy-out"ссылался в никуда, sing-box падал. Двойной фикс: (1)loadConfigFromFileтеперь парсит template parser_config и копирует Outbounds[] вmodel.GlobalOutbounds— свежие установки сохраняются корректно; (2)presenter.restoreParserConfigлечит уже сломанныйstate.jsonпри следующей загрузке — еслиconnections.outboundsпуст И template содержит селекторы, они подсаживаются обратно. Непустой state не трогаем (пользователь мог явно убрать outbounds).- Per-source Refresh требовал
state.jsonна диске. Кнопка Refresh per-row вызывалаConfigService.RefreshSingleSubscription, который делалstate.Loadдо мутации meta. На свежей установке с template-загруженными источниками (но без Save) Load возвращалErrNotFound, и UI показывал «Refresh failed: load state: state: file not found». Смысл per-source raw-кэша из SPEC 052 как раз в том, что subscription fetch отвязан от state save — Refresh не должен втихую форсить Save (Save — это явное пользовательское действие, коммитящее ВСЕ правки в конфигураторе). НовыйRefreshSourceInPlace(*state.Source)работает с in-memory указателем: фетчит URL, пишет raw cache на диск, мутирует Meta in place;state.jsonне трогается. UI делает deep-copy snapshot источника на UI thread (включая клонMeta— внутренний refresh мутирует Meta через указатель на failure-path), запускает fetch в горутине, на UI thread по ID находит source в модели (slice мог реаллокнуться из-за параллельного Add/Del) и кладёт snapshot обратно. State-awareRefreshSingleSubscriptionоставлен для auto-update heartbeat и VPN-event retry, где state всегда есть иSubscriptionMu-сериализация важна. [ERROR]шум в логе на каждом cold start.LoadClashAPIConfig(api/clash.go) и читалка selector-групп вclash_api_tabлогировали ERROR при отсутствииconfig.json— но это нормальное состояние свежей установки до того, как пользователь сделал Save → Update → Rebuild. Обе точки теперь ветвятся поos.IsNotExist(err)и пускают missing-file черезDebugLogс тегом(cold start). Любая другая ошибка (битый JSON, нет прав, нет секцииexperimental.clash_api) всё так же идёт через ErrorLog. Поведение не изменилось — только severity.- Текст диалога «Configuration Not Found». Инструкция всё ещё была про до-SPEC-045/052 поток («скачайте Мастер, используйте Мастер для генерации конфигурации, нажмите Старт»). «Скачайте Мастер» никогда не отражал реальность — Wizard не скачивается. После SPEC 045 user-facing название тоже не «Мастер», а «Конфигуратор». И в современном потоке между Save и Start нужен явный шаг «Update на дашборде» —
config.jsonпересобирается из свежекеша. Новый текст во всех 11 локалях: «открыть Конфигуратор → добавить URL подписок и Save → нажать Update на дашборде → нажать Start».internal/locale/en.json(embed черезgo:embed) применяется сразу;bin/locale/*.jsonедут с чистой установкой.
Техническое / Внутреннее
- Новый
ConfigService.RefreshSourceInPlace(*state.Source) (changed bool, err error)вcore/config_service.go. Зеркальные методы добавлены в wizard-sideConfigServiceинтерфейс иConfigServiceAdapter. LockSubscriptionMuНЕ берётся — вызов только пишет per-source raw cache (atomic.tmp + Rename); параллельные клики на одной строке блокируются busy-состоянием самой кнопки. loadConfigFromFile(ui/configurator/configurator.go) теперь импортируетencoding/json+core/configи один раз парсит templateparserConfigJSONдля seed'аmodel.GlobalOutbounds. Ошибка парсинга логируется warning'ом; конфигуратор всё равно открывается.restoreParserConfig(ui/configurator/presentation/presenter_state.go) получил heal-on-empty ветку черезmodel.TemplateData.ParserConfig. Проверка консервативная: триггерится только когдаlen(GlobalOutbounds) == 0И template содержит outbounds.refreshOneSourceFromUI(ui/configurator/tabs/source_tab.go) переписан вокруг snapshot+goroutine: deep-copy на UI thread (включая клон pointee дляMeta— внутренности refresh мутируют через указатель на failure), fetch в фоне, на UI thread поиск source по ID (slice мог реаллокнуться из-за параллельного Add/Del) и assign snapshot обратно. Помечает model dirty — Save показывает*.LoadClashAPIConfigиclash_api_tab.CreateClashAPITab+ popup-callback ветвятся поos.IsNotExist(err)для severity. Импортosдобавлен вui/clash_api_tab.go.
Миграция
- Миграция не требуется. Существующие v0.9.0-инсталляции, поймавшие баг с пустыми outbounds, автоматически вылечатся при следующем открытии конфигуратора: heal-on-empty прочитает селекторы из template и положит их в
state.Connections.Outboundsна следующем Save. После Save → 🔄 Update → Start sing-box стартует чисто. - Непонятный тост «Refresh failed: state: file not found» ушёл; на свежей установке Refresh на строке работает сразу после добавления URL, без предварительного Save.