Commit Graph

1381 Commits

Author SHA1 Message Date
Orion
df50e7cbf1 🐛 update noto-emoji font URL (#2154) 2026-09-18 22:22:40 +08:00
MOMO0302-02
28f7ac005e feat: show Smart model status and allow downloading it from the UI (#2135)
* feat: read and download the Smart routing model in the main process

* feat: expose the Smart model helpers over IPC

* feat: add a Smart model card to the resources page

* fix: close tray style type after rebase
2026-09-18 10:55:52 +08:00
MOMO0302-02
9a0099db2b fix: surface core startup and TUN failures instead of hiding them (#2127)
* fix: show the core's stderr when the config check produces no stdout

`checkProfileConfig` built the user-facing message only from the failing
core process' stdout. When the core never gets far enough to log anything
- a dynamic-link failure, a missing symbol, an architecture mismatch -
mihomo writes to stderr and exits, leaving stdout empty. Both branches
then produced `<profileCheckFailed>:` with nothing after the colon, so
the user saw an unexplained "config check failed" while the real reason
(`dyld: Symbol not found: _SecTrustCopyCertificateChain ... which was
built for Mac OS X 12.0`) was only visible in the app log, which is where
the reporters of #1404 / #1163 / #1132 had to dig it out of.

stderr is already read and logged two lines above; fall back to it when
stdout carries no usable line. Behaviour is unchanged whenever stdout has
content, so the existing `level=error` path is untouched.

(cherry picked from commit 0d02e9b5f536476f527d37b081a3eae376d83a56)

* fix: surface TUN startup failures instead of silently leaving the switch on

When the kernel fails to bring up the virtual adapter for any reason other
than missing privileges (wintun install corruption, adapter name in use,
Code 56 class-config damage, ...), it only prints

    Start TUN listening error: <cause>

and the deferred handler in listener.ReCreateTun sets tunConf.Enable = false.
The core keeps running happily, but the GUI still shows TUN as enabled and the
tray icon still reports TUN mode, so the user only sees "I turned it on and
nothing happens" with no error anywhere in the UI.

The stdout watcher only matched the privilege case
("configure tun interface: operation not permitted"), so every other failure
was swallowed. Match the generic error line as well: log it, mirror the
kernel's own decision by turning the TUN switch off, refresh the tray, and show
the kernel's raw message to the user.

The dialog is intentionally the async showMessageBox - a modal
showErrorBox/showMessageBoxSync inside the stdout 'data' handler blocks the
main process, which in turn stalls the core once its stdout pipe fills up.
Repeated lines from a single failure are collapsed into one prompt per 10s.

Closes #453

(cherry picked from commit dc3fd8be7008018826b92ffbabdea423b4ded473)

* fix: add the missing TUN permission error message for zh-CN/en-US/zh-TW

tun.error.tunPermissionDenied only existed in ru-RU and fa-IR. Because the
fallback language is en-US and en-US was missing it too, i18next returned the
key itself, so when the core failed with
"configure tun interface: operation not permitted" Chinese and English users
got a dialog whose body was literally "tun.error.tunPermissionDenied" - no
indication that the fix is to grant the core permissions.

Relates to #655

(cherry picked from commit 2076d504341c4df0b311b1efc4146f3c3c5532f1)

* fix: make sure the core is dead before the app exits on Linux (#1341)

On exit `stopCoreForExit()` sent SIGINT to the core and immediately tore
down the Linux watchdog, then `app.exit()` ran after a 1.2s budget. There
was no SIGKILL escalation on that path: `stopPidFileCore()` only does
anything in lightweight mode, because `core.pid` is written by
`keepCoreAlive()` alone. So a core that needs longer than the budget to
shut down (TUN teardown, for example) survived the GUI, and the watchdog
that would have reaped it had already been killed.

Keep the watchdog alive across the shutdown, reuse the existing
`ensureCoreProcessExited()` (SIGINT -> wait -> SIGKILL -> wait) to confirm
the core is gone, and only then stop the watchdog. If the core still can't
be confirmed dead, the watchdog is left running so its `kill -9` fires when
the main process exits. The watchdog is still stopped in the normal case,
so the PID-reuse guard from 196fdcb is preserved.

(cherry picked from commit 22a65693cf842e24c48c6480e9035fb5d0ed3b90)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-09-18 10:55:25 +08:00
MOMO0302-02
3b42348c6d fix: stop dropping tolerance when converting a group to Smart (#2112)
The Smart override converts url-test and load-balance groups and then
strips their type-specific keys. tolerance is in that list, but the Smart
core supports it: SmartOption.Tolerance feeds the sort comparator, where
delays within tolerance compare equal so the group stops flapping between
nodes of near-identical latency.

Because a normal override always runs before the Smart one, this leaves no
way to set tolerance at all while the auto override is on - a value in the
subscription is deleted, and so is one set by an earlier override. The
branch that handles an already-smart group never deleted it, so the two
paths disagreed.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 08:03:33 +08:00
Memory
ac1bb9ef78 fix: parse subinfo with Number 2026-09-17 16:48:36 +08:00
MOMO0302-02
5ace1da9c8 feat: expose the kernel's LightGBM model auto-update options (#2136)
* feat: pass the kernel's LightGBM updater options through mihomo.yaml

* feat: add LightGBM auto-update settings to the Smart core card
2026-09-15 20:37:12 +08:00
MOMO0302-02
0e3116abe1 fix: recover the core, TUN and system proxy after the machine wakes up (#2124)
* fix: recover core, TUN and system proxy after the machine wakes up

Neither side handled suspend/resume: the app had no powerMonitor listener at
all, and the core's own component/power package (which registers
PowerRegisterSuspendResumeNotification on Windows) has no callers anywhere in
the core tree, so nothing reacted to a wake-up. After a long sleep users were
left with a running core that no longer routed traffic, and had to re-run a
latency test, refresh a subscription or restart the app to get back online.

On 'resume', once the network stack is back:

  - if the core process is gone or its API no longer answers, restart it;
  - if TUN is enabled but its adapter has disappeared from the OS interface
    list, toggle tun.enable off and on. The core skips ReCreateTun whenever the
    TUN config compares equal to LastTunConf, so an off/on cycle is the only
    way to force the adapter to be rebuilt - this is exactly the "switch TUN
    off and on again" workaround users already report;
  - re-apply the system proxy when it is enabled (idempotent, and
    triggerSysProxy already retries on its own while the machine is offline).

The TUN branch only fires when the adapter is provably missing, so machines
whose TUN survives the sleep see no interruption.

Closes #1933
Closes #727

(cherry picked from commit 5978a7f280a593421cf3b2d9ba3ec552d11dc40f)

* fix: reload the core config after the system resumes from sleep

Suspending the machine tears down the routes and the DNS-hijack rules that
the TUN device installs, but it does not kill the core process. After
resume the mixed-port proxy still answers while the TUN DNS listener no
longer does - #1231 reproduced exactly that with dig against the hijack
port - so name resolution falls back to the real upstream and the user
sees "no network after it has been running for a while" (#159, where
restarting the core is the reported workaround).

The app never listened for powerMonitor 'resume', so nothing rebuilt that
state. The manual workaround users found - open DNS settings and save -
goes through mihomoHotReloadConfig, and on the kernel side that is
executor.ApplyConfig, which re-runs updateDNS, updateTun, updateIPTables
and resolver.ResetConnection. Do the same automatically on resume.

Scoped to the case that is actually reported: only when TUN is enabled and
a core process is running. The reload is delayed 5s because the physical
interface is usually still down at resume and auto-detect-interface would
latch the wrong one; the pending timer is cancelled during shutdown.

Refs #1231, #159

(cherry picked from commit 831597dcf025bcece51ce98af89194514adb5da6)

* feat: re-apply the system proxy after the machine resumes (#240)

Waking from sleep or hibernation can leave the OS proxy settings cleared
while the app still believes the system proxy is on, so the user keeps
browsing unproxied without any indication. Nothing in the app listened for
powerMonitor 'resume'.

Re-issue the system proxy on resume when it is enabled in the config.
triggerSysProxy() already disables before enabling, so it is idempotent, and
it retries by itself when the network is not up yet.
2026-09-15 16:00:30 +08:00
MOMO0302-02
7d4caf833c fix: a batch of small UI, tray, floating window and file handling bugs (#2129)
* fix: keep the Dock icon in sync when the tray is clicked rapidly

On macOS the Dock icon is toggled from two places: the window `show` event
calls showDockIcon(), and the window `close` handler calls hideDockIcon()
after awaiting the app config. Both delegate to Electron, where
`Browser::DockShow` defers TransformProcessType to the main queue and
`Browser::DockHide` returns immediately when it is called within one second
of the last DockShow - a workaround for macOS leaving duplicate Dock icons
behind. Toggling the window from the tray twice in quick succession therefore
lost the hide: the Dock icon stayed visible although the window was hidden
and "show Dock icon" was off, and only another slow toggle brought it back.

Serialize the two operations on a single chain, coalesce them on the desired
state so a stale request cannot win, and wait out Electron's suppression
window before hiding. The `isVisible()` guard has to go with it: after a
suppressed hide the activation policy is still regular, so the guard would
skip the retry that actually fixes the state.

The close handler additionally skips the hide when the window became visible
again while the config was being read.

Refs #1867

(cherry picked from commit 9dbc79da14abc7504d66b00a32d011776b6f8f06)

* fix: keep the tray status colour when the macOS traffic display is on

With "show speed in the status bar" enabled the tray icon is composed in the
renderer (logo + text on a canvas) and the main process only displays it.
updateTrayIcon()/updateTrayIconImmediate() therefore return early, and the
trayIconUpdate handler called setTemplateImage(true) unconditionally, so
macOS renders the alpha channel only: the system proxy / TUN colours were
dropped as soon as the traffic display was turned on. Turning it off brought
them straight back, which is exactly what the report describes.

The main process cannot tint an image it did not draw, so it now tells the
renderer which icon to use and what colour to write the text in
(getTrayTrafficStyle), and the renderer reports back whether the frame it
produced carries a colour. Only coloured frames stop being template images;
without a status colour, or with "disable tray icon colour" on, nothing
changes and the icon stays a template image as before.

The text colour follows the system appearance rather than nativeTheme,
because the menu bar keeps using the system appearance even when the user
forces a light or dark theme inside the app - a template image used to get
that inversion for free.

Refs #1143

(cherry picked from commit 88ed2997869e362715ae7c25fa1051da6b9740fc)

* fix: bring the tray icon back when the floating window is gone

Hiding the tray icon is only offered while the floating window is enabled,
because the floating window is then the remaining way to reach the app. That
guarantee is dropped at runtime: if the floating window fails to be created,
or its renderer crashes and the window is destroyed, `disableTray` stays on.
The app is then left with no tray icon and no floating window, so closing the
main window hides it for good and the process has to be killed (#2046).

Restore the tray icon (and clear `disableTray`) on both paths, the same way
`closeFloatingWindow` already does when the window is closed deliberately.

(cherry picked from commit e6d1c735f5047a1bdf0da5d7777606c79c39c0fe)

* fix: keep the floating window position when it sits at a screen edge (#415)

The floating window restores its position through electron-window-state,
whose validateState() only accepts a saved position when the window is
*fully* contained in one display; otherwise it calls resetStateToDefault()
which hardcodes x/y to 0. Users routinely park the floating window against
the right or bottom edge with part of it off screen, so every restart moved
it to the top-left corner.

Read the saved position directly and clamp it into the work area of the
nearest display instead. A position that is already fully on screen is
returned unchanged, so nothing moves for windows that were not affected.

Measured on Windows 11 with a 2560x1440 primary display: a saved position of
(2540, 400) makes electron-window-state return (0, 0), while clamping returns
(2425, 400).

(cherry picked from commit d12cb1dfc1fde5c7a32ea40d2e061fcb0f69dc80)

* fix: keep the proxy group search box usable while filtering

The per-group search box lives inside the group header, which GroupedVirtuoso
renders virtually. As soon as typing shrinks the group, the list gets shorter,
the header scrolls out of the render window and is unmounted: the box loses
focus, collapses back to width 0 and the still-applied filter becomes
invisible. Composition-based IMEs (WeChat, Sogou) are aborted mid-word by the
same unmount, which is why letters end up typed literally (#332, #1621).

- pin the group being filtered to the top of the list on every search change,
  so its header stays mounted
- keep the input expanded whenever it holds a search term instead of only
  while focused
- take focus back when the input is remounted with a search term and nothing
  else has claimed focus
- show the filtered node count in the group chip instead of the total

(cherry picked from commit 8efecd62d591f287458318bfd8977e2c24b87677)

* fix: keep TUN enabled when saving TUN settings

The save button on the TUN page sent

    { tun: { device, stack, auto-route, ..., mtu } }

with no `enable` field. patchControledMihomoConfig forwards that patch verbatim
to the running core as PATCH /configs, and in the core `tunSchema.Enable` is a
plain bool, not a pointer - an absent `enable` decodes to false, and
pointerOrDefaultTun then overwrites the live value, so ReCreateTun immediately
tears the virtual adapter down.

Today the follow-up mihomoHotReloadConfig() puts it back, so the visible effect
is that the adapter is destroyed and recreated twice per save. But if that hot
reload fails (bad profile, PUT /configs error) the user is left with TUN off in
the core while the switch, the tray icon and the config file all still say it
is on.

Send the current enable state along with the rest of the TUN settings so the
patch can never mean "turn TUN off".

Relates to #554

(cherry picked from commit 921bffbd2e4bf0a41ad5e67d81af584a0fa3ac98)

* fix: send WebDAV basic auth credentials as UTF-8

The webdav client builds its Basic auth header with the `base-64` package,
which only accepts Latin1 characters and throws `InvalidCharacterError` for
anything outside that range. A WebDAV account with a non-ASCII user name or
password therefore made every backup/restore call fail before a request was
even sent (#323).

Build the Authorization header locally with `Buffer.from(..., "utf-8")` as
RFC 7617 requires and disable the library-side auth so it does not overwrite
it. The header is byte-identical to the previous one for ASCII credentials.

(cherry picked from commit 5d6593e683b4da4f13d214e832d45a61d113da8d)

* fix: stop asking Windows for the SSID on every poll cycle

The SSID watcher polls `netsh wlan show interfaces` on a timer (30s at the time
of the report, 15s today). On Windows 11 the SSID counts as location data, so
every one of those calls makes "Network Command Shell" (netsh.exe) request the
user's location and flashes the location indicator - forever, for as long as
the app runs, without the user doing anything.

The SSID cannot change while the network interfaces are untouched, so take a
cheap fingerprint of os.networkInterfaces() (names plus addresses and MACs)
first and only shell out to netsh when it differs from the previous probe. Any
association change - joining, leaving or roaming to another network - changes
the wireless adapter's addresses, so real SSID changes are still picked up on
the next tick. When a probe returns no SSID at all (wireless not up yet, or no
wireless adapter) the fingerprint gate is bypassed every 5 minutes so the
watcher can still recover.

Measured on Windows 11: the fingerprint stayed byte-identical across 10
consecutive one-second samples, so a steady-state machine goes from one netsh
call every 15s to none.

Closes #479

(cherry picked from commit fb1a6646e189c78b2f8c92b58d8b0bd6362cc892)

* fix: retry resource copy when the target is replaced mid-copy

initFiles() rethrows every error other than EPERM/EBUSY/EACCES, and
geoip.dat / geosite.dat / country.mmdb are on the critical list, so a
single failure aborts the whole runtime file initialization.

The log attached to #1510 shows exactly that happening with an ENOENT:

  Failed to copy geoip.dat: Error: ENOENT: no such file or directory,
  unlink '...\mihomo-party\work\geoip.dat'

ENOENT here does not mean the source is missing - that is checked before
the copy starts. fs.cp() with force:true stats the destination and then
unlinks/chmods it, and anything else touching the work directory in
between (the kernel's own geo auto-update, antivirus, a second instance)
turns that into ENOENT. A tight loop that deletes the destination while
copying reproduces it on 2275 of 2321 attempts.

Retry the copy once instead of failing initialization outright.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 35ff76173f921dc6be2e2fd508b6365ea9ce8db1)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 00:15:56 +08:00
MOMO0302-02
5e07e31ef5 fix: do not let a failed window creation surface as an uncaught exception (#2021)
`createWindow()` throws once its retries are exhausted. The call site in
`showMainWindow()` only attached a `then`, so that rejection became an unhandled
promise rejection and the user got a raw main-process error dialog instead of a
log line.

Attach a `catch` and log it.
2026-09-13 11:46:37 +08:00
MOMO0302-02
9ddc3ac479 fix: let users recover from a crash without restarting the app (#2111)
The error boundary wraps the whole tree, so any render error replaces the
entire window - sidebar included - with the fallback. The fallback only
offers GitHub, Telegram and a copy button, and never resets, so the app
stays on that screen until the user kills the process.

Add a reload button that reloads the renderer. The main process, the core
and the system proxy are untouched, so a transient failure recovers in
place; a persistent one lands back on the same screen, no worse than now.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 11:46:06 +08:00
MOMO0302-02
9b2e45b932 fix: reject NaN from the remaining numeric settings inputs (#2023)
Clearing a type="number" input fires onValueChange with an empty string, and
parseInt('') is NaN. NaN survives the structured clone across IPC, but
JSON.parse(JSON.stringify(patch)) in the config layer turns it into null, which
deepMerge then writes over the default.

Three inputs still commit unguarded: subscription timeout (repaired later by its
onBlur handler, but the invalid value reaches disk in the meantime) and the two
delay-test fields, which have no onBlur at all.

Only commit finite values. The geo-update-interval case this PR originally also
covered was fixed upstream in fca31a8, so it is dropped here.
2026-09-13 11:45:15 +08:00
zjdndjf
a4b1a7f59a fix: improve core startup failure handling 2026-09-13 01:27:23 +08:00
zjdndjf
0e66ee90d3 fix: handle core process errors 2026-09-13 01:27:23 +08:00
MOMO0302-02
ef6f73ae27 fix(macos): register the login item through Service Management
`checkAutoRun` / `enableAutoRun` / `disableAutoRun` drove macOS auto start by
sending Apple events to System Events (`osascript -e 'tell application "System
Events" ...'`). That path fails in several ways that all look identical to the
user, because the switch state is re-read from `checkAutoRun` right after the
write and simply snaps back with no error:

- The app ships with `hardenedRuntime: true` but neither the
  `com.apple.security.automation.apple-events` entitlement
  (`build/entitlements.mac.plist`) nor `NSAppleEventsUsageDescription`
  (`electron-builder.yml` `mac.extendInfo`), so the Automation request against
  System Events can be denied outright instead of prompting.
- `checkAutoRun` matched the login item by
  `exePath().split('.app')[0].replace('/Applications/', '')` against a list of
  login item *names*. Outside `/Applications` the needle is an absolute path and
  can never match, so the switch stays off even after the item was created.
- `disableAutoRun` deleted the item by that same guessed name and threw when it
  did not exist.

Use Electron's own login item API instead, which goes through
`SMAppService` on macOS 13+ and `LSSharedFileList` below that. No Apple event,
no name guessing, and the read-back state comes from the OS. Registration
failures now surface the `SMAppService` status instead of silently doing
nothing.

Login items written by earlier versions through System Events are not managed by
Service Management, so they are cleaned up (best effort) when the user toggles
auto start, otherwise the app would launch twice at login.

Closes #1302
Closes #1102

(cherry picked from commit 3784139f951896c4818add5374e2cf8a858644fd)
2026-09-12 14:38:25 +08:00
MOMO0302-02
7367fa58e4 fix: remove the Smart strategy setting, which the core never reads 2026-09-12 09:54:15 +08:00
ezequielnick
eb0498b33d feat: protect subscription DNS settings from unconfirmed overrides
- Detect custom DNS in raw subscriptions and require confirmation
- Bind confirmation to the subscription and DNS content for the current session
- Sync the override switch after successful core application
- Retain auto-disable notices until the window becomes visible
2026-09-10 14:05:26 +08:00
Memory
0cd5d86193 fix: allow named GEOIP categories in rule editor 2026-09-10 11:16:36 +08:00
Cervol Liu
1700bfebe6 fix: preserve numeric override IDs before JSON cloning (#2142) 2026-09-09 18:12:03 +08:00
ezequielnick
348db6baa0 chore: bump version 2026-09-09 12:17:51 +08:00
ezequielnick
39e26c60f7 update changelog 2026-09-09 12:17:51 +08:00
Memory
74cbbfeaa8 fix: enable profile auto-update by default 2026-09-08 23:49:42 +08:00
ezequielnick
a092eb50c3 feat(plugin): CPX v2 availability hardening (squash of cpx-v2-availability-hardening)
Client-side hardening of the CPX airport plugin, squashed from 46 commits
forked at 89c2bb0e. Behaviour changes:

- Unified operation model (§0.4/0.5): one deadline + one AbortSignal + one
  persistence commit per operation; per-plugin lock → vault lock hierarchy;
  tombstone on delete; every wait (lock, DNS preflight, proxy resolution,
  vault decrypt) is bounded by the same budget.
- Routing: direct/proxy auto-fallback with a pre-send guard; proxied https
  builds its own CONNECT tunnel (an aborted hung CONNECT closes its socket);
  invalid local-proxy ports are refused instead of falling back to :80; the
  core's inbound credentials are carried to the local proxy; NAT64 and
  site-local IPv6 ranges are non-public.
- Gateways: multi-gateway recovery with one rediscovery per operation,
  normalized endpoint paths, signed discovery documents (Ed25519, seq/digest
  accept/align/rollback/equivocation), commit order vault → plugin.yaml.
- Subscriptions: a fetched subscription is validated by the core (mihomo -t)
  against the current override set before it replaces the profile, inside
  the profile write critical section (profile.yaml and override.yaml share
  one write queue); schedule fields are read at write time; the first
  subscription is activated through the real switch flow; profile deletion
  removes the record last so any failure stays retryable.
- Devices: a re-login that replaces a still-valid device records it in the
  vault (staleDevices) and retires it after the login, after later
  successful fetches and on removal; enroll compensation restores the old
  vault and keeps an un-revoked new device for retirement.
- Vault: on Linux the vault is persisted only behind a system secret store
  (backend name + ciphertext-prefix canary); otherwise it stays in memory.
  A cache-miss read releases the caller at the budget while the lock is held
  until the decrypt ends.
- Config caches (plugin.yaml, profile.yaml, override.yaml) can no longer be
  rolled back by a late cold read.
- Reference gateway and provider guides updated (deploy contract, discoveryUrls
  same-origin rule, /revoke after re-login); https-proxy-agent dropped.

Reviewed in a Codex loop (gpt-6, 38 calls): 74 findings, 68 fixed and
verified, 6 invalid, no backlog. Tests: vitest 501, gateway 106.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 22:44:13 +08:00
v-star0719
2a19b02266 fix: stop IPC ghost listeners leaking across contextBridge calls (#2131)
contextBridge wraps every function crossing the world boundary in a fresh
proxy object, so ipcRenderer.on() and removeListener() each receive a
different wrapper and reference-based removal always fails. Components
that subscribe in useEffect then return removeListener as cleanup leave a
ghost listener behind after unmount.

The connections page is the worst offender: the /connections stream
pushes the full connection list every second, and each ghost handler
dispatches a fresh connection tree into a React update queue that is
never rendered — heap grows ~22-25MB/min after visiting the page a few
times, scaling with the number of visits.

Make on() return an unsubscribe closure that captures the exact wrapper
registered with ipcRenderer, and switch every renderer call site to use
it as its effect cleanup.

Verified with a minimal Electron harness (removed listener stops firing)
and against the real app: hammering the connections page 20x now leaves
memory flat instead of growing ~25MB/min, with zero connections-chunk
allocations sampled.

Co-authored-by: tuqiming <tuqiming@camel4u>
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-09-03 21:47:31 +08:00
Memory
0f26f22987 chore: update electron 2026-09-03 21:40:48 +08:00
zjdndjf
7271e0dfda fix: coalesce connection updates 2026-09-03 02:31:48 +08:00
zjdndjf
b5293b3f65 refactor: rewrite traffic usage logging 2026-09-03 01:54:28 +08:00
zjdndjf
89c2bb0ec0 fix: cancel pending WebDAV config updates
Prevent delayed writes after leaving the WebDAV settings page.
2026-08-31 22:26:53 +08:00
MOMO0302-02
d82ab58081 fix: correct config fetching, debouncing, pagination and rules editor (#2030)
- create-config-context passed the SWR key straight to the fetcher. SWR
  calls fetcher(key), so getAppConfig/getProfileConfig/getOverrideConfig/
  getPluginConfig all received a non-empty string as their `force`
  argument and re-read from disk on every single revalidation. Call the
  fetcher with no arguments.

- webdav-config created its debounced writer inline during render, so
  every keystroke produced a fresh function with a fresh timer and the
  debounce never took effect - each character rewrote config.yaml. Keep
  one instance across renders and cancel it on unmount.

- The traffic table's pager acted on the raw page number rather than the
  clamped one, so after the list shrank the "previous page" button did
  nothing until it had been clicked as many times as the overshoot.

- The rules editor swallowed load failures, then saved anyway - which
  overwrote the user's existing prepend/append/delete rule overrides with
  an empty set. Refuse to save when the current content was never loaded.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 22:22:05 +08:00
MOMO0302-02
3b296bf54b fix: apply Smart core settings on hot reload and stop re-downloading the model
* fix: apply Smart core settings on hot reload

The Smart core switches (LightGBM, data collection, strategy, collector
size) are delivered to the kernel through a generated global override
script, but that script was only regenerated inside prepareCore(). The
settings page persists the change and then hot reloads, so the reload
re-applied the previous script and the new values silently did not reach
the kernel until the core happened to be restarted.

Regenerate the override before generating the profile on hot reload, and
skip the write when the generated content is unchanged so routine hot
reloads no longer churn the override file.

* fix: reuse the downloaded Smart model when checking a profile

The Smart core downloads Model.bin when it is missing from its working
directory. Profile checks run the core with `-d <test dir>`, which never
receives the model, so every check starts a fresh download.

That download runs after the previous core has already been stopped and
the system proxy torn down, so it times out instead of completing, and a
failed download leaves nothing behind - the next restart repeats it. On a
restart here it cost 20s, against 0.5s once the model is in place.

Copy the model over from the working directory before running the check,
mirroring how the geo databases are already shared with the test dir.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 17:04:14 +08:00
zjdndjf
911e090537 fix: stop connection resource work on unmount 2026-08-30 19:40:00 +08:00
zjdndjf
cc94edda3f fix: dispose Monaco editor models 2026-08-30 18:55:00 +08:00
zjdndjf
d50e281383 fix: stop orphaned macOS cores on restart
Closes #2115
2026-08-30 18:03:24 +08:00
Memory
26dd08e07b refactor: streamline validation utilities 2026-08-28 21:51:20 +08:00
Memory
061faeefd1 fix: support YAML merge tags in rule editor 2026-08-25 15:45:17 +08:00
Memory
de8ebb2f62 fix: hot reload config after saving rules 2026-08-24 21:36:14 +08:00
zjdndjf
9776b8791a fix: patch vite-plugin-monaco-editor for modern Node 2026-08-24 01:12:00 +08:00
zjdndjf
ebbcc9d64d perf: speed up app startup 2026-08-24 01:12:00 +08:00
Felix
6bb385822b fix: reset proxy list measurements after visibility toggle (#1884)
Co-authored-by: MackJack023 <141124084+MackJack023@users.noreply.github.com>
2026-08-21 18:14:40 +08:00
Felix
dafc2393dd feat: add quick global toggle for overrides (#1280) (#1928)
Co-authored-by: MackJack023 <141124084+MackJack023@users.noreply.github.com>
2026-08-21 18:12:58 +08:00
MOMO0302-02
8356020931 fix: make core downloads fail loudly and stop clobbering a working core (#2018)
Three problems in installMihomoCore / downloadGitHubAsset.

1. chromeRequest resolves for any status code, so downloadGitHubAsset
   never noticed an HTTP error. A mirror answering 404 or 502 with an
   HTML page was treated as a successful download: the error page was
   written out as the core binary and the function returned, so the
   remaining mirrors and the direct GitHub URL were never tried. Check
   the status and throw so the fallback loop continues.

2. The .gz path decompressed straight onto the live core file. Any
   failure mid-stream left the previously working core truncated with
   nothing to fall back to. Decompress to a staging file and rename it
   into place only after it is complete, removing the staging file if
   anything goes wrong. The pipeline also only had an error handler on
   the gunzip and write streams, so a read error was unhandled; wire all
   three and destroy them on failure.

3. Permissions were set with `execSync('chmod 755 ' + targetPath)`, with
   the path neither quoted nor escaped. On macOS the data directory is
   under "~/Library/Application Support/", so the command always split on
   the space and failed. The failure was only logged as a warning, so the
   result was a freshly installed core with no execute permission - it
   looks installed but will not start. Use fs.chmodSync.

The downloaded archive was also left behind whenever extraction failed,
accumulating one copy per retry; clean it up on the error path.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 18:11:56 +08:00
MOMO0302-02
9490285a83 fix: remove read-modify-write races and validate override downloads (#2024)
- addOverrideItem read the config outside the write queue, then awaited
  createOverride - which for a remote override downloads over the network
  and can take seconds - and finally wrote the stale snapshot back in
  full. Any change made meanwhile was lost. updateOverrideItem and
  removeOverrideItem had the same shape. Move the read inside the queue
  and re-read from disk there, matching what profile.ts already does.

- The remote override download never checked the HTTP status, so a 404 or
  502 error page was written out as the override body. applyOverrides then
  parses it with no try, and the failure surfaces as a core restart error.

- The download built its proxy from the mixed port with a destructuring
  default, so with the port turned off (0) it went to 127.0.0.1:0.

- patchControledMihomoConfig ran the patch through JSON.parse(stringify),
  which drops keys whose value is undefined. The startup migration uses
  exactly that to delete obsolete keys such as external-controller-unix,
  so those deletions never happened - the keys stayed in mihomo.yaml and
  the migration re-ran its full rewrite, config regeneration and Gist
  upload on every launch. Restore top-level undefined keys after the
  round-trip so yaml.stringify can drop them for real.
2026-08-21 18:11:36 +08:00
MOMO0302-02
9714b7435b fix: repair drag-and-drop on the override page (#2026)
- Dropping a file read `(file as File & { path: string }).path`. Electron
  removed File.path, so the value is undefined and readTextFile always
  failed. Use webUtils.getPathForFile, which preload already exposes and
  which profiles.tsx already uses for the same purpose.

- Reordering computed indices against the sorted copy but spliced in
  `items[activeIndex]` - an element from the unsorted array. Whenever the
  two orders differ this duplicated one entry and dropped another. Splice
  out and reinsert the same element, as profiles.tsx does, and bail out if
  either index is -1.

- The drop handler read `file.name` without checking that the FileList was
  non-empty, so dropping selected text threw a TypeError. The throw also
  skipped the setFileOver(false) reset, leaving the whole page stuck
  behind a blur. Guard the access and reset the flag in a finally block.
2026-08-21 18:11:09 +08:00
MOMO0302-02
ef50d8e9a0 fix: keep the copy button clickable on profile cards (#2028)
The copy button only rendered while the card was hovered, and the proxy
checkbox that sits next to it re-laid the row out on hover, so the button
moved out from under the pointer as it was being approached.

Closes #476
2026-08-21 18:10:43 +08:00
MOMO0302-02
637a875aba fix: handle failures in the PAC server, Gist sync and floating window (#2032)
- The PAC server called listen() with no 'error' listener. A host the user
  cannot bind to - or a port already in use - emitted an unhandled 'error'
  event, which in Node terminates the process, so a bad PAC host setting
  crashed the whole main process. Attach an error handler and surface the
  failure to the caller.

- The Gist upload never checked the HTTP status. Once the token expired,
  GitHub answered 401 and the code still recorded the runtime config as
  successfully synced, so the user saw a healthy backup that did not
  exist. Check the status and report the failure.

- On 'render-process-gone' the floating window handler only set the module
  reference to null without destroying the BrowserWindow. The dead window
  stayed on screen, always-on-top, with nothing left holding a reference
  to close it. Destroy it before clearing the reference.
2026-08-21 18:10:23 +08:00
MOMO0302-02
7caecd6474 fix: never rewrite rules to a smart group that does not exist (#2036)
The generated Smart override creates the "Smart Group" conditionally but
rewrites rules unconditionally, so the rules can end up pointing at a
group that was never added.

Two reachable cases:

  - The group is only created when `config.proxies` is a non-empty array.
    A subscription that ships only `proxy-providers` (which the app
    explicitly accepts) skips creation, yet every non-builtin rule target
    is still replaced with the literal string 'Smart Group'.

  - When the profile already contains a `type: smart` group under a
    different name, `smartGroupExists` becomes true so nothing is
    created, and the existing group is never renamed - but the rules are
    still rewritten to 'Smart Group'.

In both cases the generated config.yaml references a missing proxy
group, so the core refuses to start ("proxy Smart Group not found") and
the app is left with no working core.

Track the name of the smart group that actually exists - the existing
group's own name, or 'Smart Group' when one was created - and use it for
the replacement. When neither applies, skip rule rewriting entirely and
leave the profile's own targets untouched.

Closes #885

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 18:04:47 +08:00
luantu
f9c841a6f6 feat: auto-switch profile by WiFi SSID
* feat: auto-switch profile by WiFi SSID

Add ssidProfileMap (SSID -> profile ID) and ssidProfileRestore to app config.
When connecting to a mapped WiFi SSID, automatically switch to the
corresponding subscription profile. Optionally restore the previous
profile when leaving the mapped SSID. pauseSSID takes priority.

* fix: send profileConfigUpdated after SSID-triggered profile switch

changeCurrentProfile() does not notify the renderer via IPC.
The renderer relies on the profileConfigUpdated event to revalidate
its SWR cache, so SSID-based switching was effective in the backend
but the UI didn't reflect it until a manual refresh.

* perf: event-driven SSID detection on macOS via scutil -w

Replace 30s polling with scutil -w child process on macOS.
scutil -w blocks on a SystemConfiguration key, consuming zero CPU
until the network state changes, then exits. On exit we debounce
500ms and re-check the SSID.

Windows/Linux keep polling, reduced from 30s to 15s.
Also add anti-tight-loop guard (1s minimum restart interval).
2026-08-20 10:17:47 +08:00
MOMO0302-02
588aaa6991 fix: prevent command injection in convertMrsRuleset
convertMrsRuleset built a shell command string from `behavior` and the
resolved ruleset path and ran it through `exec`. `behavior` comes from
the runtime config's `rule-providers`, which originates from the user's
subscription YAML and is therefore untrusted input.

A malicious or compromised subscription can set a behavior such as
`domain & <command> &`; opening that ruleset in the resource viewer then
executes arbitrary commands with the app's privileges. The quoted paths
are also breakable on Windows via an embedded double quote.

Switch to `execFile` with an argument array so nothing is parsed by a
shell, and reject any behavior outside the set the core supports.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 10:16:50 +08:00
MOMO0302-02
316c41c05b ci: run lint, typecheck, format and unit tests on PRs and pushes
The existing build.yml only runs on v* tags and never invokes the test
suite, so pull requests get no automated checks. Add a lightweight CI
workflow that runs format:check, lint:check and typecheck once on Linux
and the unit tests on both Linux and Windows. Uses --ignore-scripts so it
does not download the mihomo cores, which the checks and tests do not need.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-20 10:12:19 +08:00
MOMO0302-02
c779799bee ci: staple the notarization ticket to the macOS pkg
The macos and macos10 jobs submit the signed .pkg to notarytool but never
staple the resulting ticket to it. Without a stapled ticket Gatekeeper has to
reach Apple's notary service to verify the package, so an install on a machine
that is offline — or behind a blocked network, which is a plausible state for
this particular app before it is running — fails with "cannot be opened because
Apple cannot check it for malicious software".

Staple after a successful submission, which is the documented final step of the
Developer ID distribution flow.
2026-08-20 10:10:21 +08:00
zjdndjf
4e86b53db7 fix: preserve Smart model in profile work dirs 2026-08-19 14:07:46 +08:00