github Leadaxe/singbox-launcher v0.9.1
release v0.9.1

6 hours ago

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.1

The 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

  1. Download: singbox-launcher-v0.9.1-macos.zip
  2. Extract the ZIP file
  3. Remove quarantine attribute (required):
    xattr -cr "singbox-launcher.app" && chmod +x "singbox-launcher.app/Contents/MacOS/singbox-launcher"
  4. Double-click singbox-launcher.app to 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)

  1. Download: singbox-launcher-v0.9.1-win64.zip
  2. Extract the ZIP file to a folder, for example: C:\Program Files\singbox-launcher\
  3. Run singbox-launcher.exe from that folder
    • You may need administrator rights to install to Program Files
    • The launcher will automatically download sing-box and wintun.dll on first launch

Windows 7 (x86, legacy)

  1. Download: singbox-launcher-v0.9.1-win7-32.zip
  2. 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-out on first Start. When the wizard loaded from wizard_template.json with no state.json present, only the legacy model.ParserConfigJSON view was populated — the canonical model.GlobalOutbounds (the v5 source of truth for selector / urltest groups) stayed empty. On Save → state.Connections.Outbounds = []. On Rebuild → config.json got the parsed nodes plus direct-out, but no proxy-out / auto-proxy-out / vpn ① / vpn ② selectors. The template-preserved route.final = "proxy-out" then pointed at nothing, and sing-box exit-2'd. Two-part fix: (1) loadConfigFromFile now seeds model.GlobalOutbounds from the parsed template parser_config, so fresh installs save state correctly; (2) presenter.restoreParserConfig heals already-broken state.json files on next wizard load — if connections.outbounds is 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.json on disk. The Refresh button on each source row called ConfigService.RefreshSingleSubscription, which did state.Load before mutating meta. On a fresh install with template-loaded sources but no Save yet, Load returned ErrNotFound and the UI showed Refresh 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). New RefreshSourceInPlace(*state.Source) operates on an in-memory pointer, fetches the URL, writes raw cache to disk, and mutates Meta in place; state.json is not touched. The wizard takes a deep-copy snapshot of the source on the UI thread (including Meta clone — 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-aware RefreshSingleSubscription is kept for the auto-update heartbeat and VPN-event retry paths where state always exists and SubscriptionMu serialization matters.
  • [ERROR] log noise on every cold start. LoadClashAPIConfig (api/clash.go) and the clash_api_tab selector-group reader logged at ERROR severity whenever config.json was missing — which is the normal state on a fresh install before the user has done Save → Update → Rebuild. Both now branch on os.IsNotExist(err) and route the missing-file case through DebugLog with a (cold start) tag. Any other failure (corrupt JSON, permission denied, missing experimental.clash_api section) 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 Update step between Save and Start so config.json is 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 via go:embed) takes effect immediately; bin/locale/*.json files ship in fresh installs.

Technical / Internal

  • New ConfigService.RefreshSourceInPlace(*state.Source) (changed bool, err error) in core/config_service.go. Mirror methods added to the wizard-side ConfigService interface and ConfigServiceAdapter. No SubscriptionMu lock — 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 imports encoding/json + core/config and parses the template parserConfigJSON once to seed model.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 using model.TemplateData.ParserConfig. The check is conservative: only triggers when len(GlobalOutbounds) == 0 AND 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 the Meta pointer'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 *.
  • LoadClashAPIConfig and clash_api_tab.CreateClashAPITab + popup callback now branch on os.IsNotExist(err) for log severity. The os import was added to ui/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.Outbounds on 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, заполнялся только legacy model.ParserConfigJSON-view, а canonical model.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-aware RefreshSingleSubscription оставлен для 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-side ConfigService интерфейс и ConfigServiceAdapter. Lock SubscriptionMu НЕ берётся — вызов только пишет per-source raw cache (atomic .tmp + Rename); параллельные клики на одной строке блокируются busy-состоянием самой кнопки.
  • loadConfigFromFile (ui/configurator/configurator.go) теперь импортирует encoding/json + core/config и один раз парсит template parserConfigJSON для 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.

Don't miss a new singbox-launcher release

NewReleases is sending notifications on new releases.