Commit Graph

1021 Commits

Author SHA1 Message Date
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
zjdndjf
4e86b53db7 fix: preserve Smart model in profile work dirs 2026-08-19 14:07:46 +08:00
zjdndjf
2330e7790e feat: support custom GitHub download proxy (#2096) 2026-08-19 13:30:01 +08:00
zjdndjf
fca31a84b3 fix: crash when geo-update-interval is null in config 2026-08-17 10:54:12 +08:00
Shen Zehou
41c2f26538 fix: load legacy sysproxy binding on Windows 8/8.1 2026-08-16 10:05:52 +08:00
zjdndjf
60dba7895d fix: use valid popover side for welcome tour step 2026-08-13 05:07:07 +08:00
zjdndjf
f4a4d9b31d fix: use valid Electron accelerator names for numpad operator keys 2026-08-13 05:05:51 +08:00
Memory
043ab2def0 chore: update dependencies and migrate to pnpm 11 2026-08-11 20:26:34 +08:00
zjdndjf
6cbd24e30a Revert "fix: stop log streaming when logs page unmounts"
This reverts commit 5532a85164.
2026-08-10 03:34:32 +08:00
zjdndjf
8ed132f735 fix: reject non-mapping tunnel profiles 2026-08-10 02:54:31 +08:00
MOMO0302-02
8d3a62a5e6 fix: do not let the tunnel editor overwrite a profile it failed to read (#2041)
EditTunnelsModal saves by spreading the in-memory `profile` state and
writing the whole document back with setProfileStr, which replaces the
profile file outright and then hot-reloads the core. `profile` starts as
`{}` and is only filled in once the async load succeeds.

Neither failure path stops the user from saving:

  - When yaml.load throws (a subscription with a duplicated mapping key,
    for example) the catch only shows a toast. `profile` stays `{}`, so
    pressing save serialises "{}\n" over the profile and every proxy,
    proxy-group and rule is gone.

  - When the parsed value is not an object the code silently substituted
    `{}` with no toast at all. An age-encrypted profile hits this: the
    file on disk is ciphertext and getProfileStr does not decrypt, so
    yaml.load returns a string, the dialog shows "no tunnels", and saving
    destroys the encrypted profile.

The save button was also enabled while the initial load was still in
flight.

Treat a non-object parse result as an error instead of silently
substituting `{}`, record the failure, and refuse to save - both in
handleSave and by disabling the button - until the profile has actually
been read.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 02:52:28 +08:00
zjdndjf
d9cd7712ab fix: serialize system proxy transitions
Closes #2045
2026-08-10 02:43:50 +08:00
zjdndjf
8b0b1ce0cb fix: avoid workdir setup during profile validation 2026-08-10 02:35:04 +08:00
Feng GAO
e4efc0865f fix: validate remote profiles before replacing them (#2048) 2026-08-10 02:33:38 +08:00
Memory
8af583e771 fix: bundle wl-copy for tray clipboard on Wayland
* fix: bundle wl-copy for tray clipboard on Wayland

* Update changelog.md
2026-08-08 17:08:36 +08:00
ezequielnick
5532a85164 fix: stop log streaming when logs page unmounts
Move the renderer listener into the page effect and gate the main-process log WebSocket on renderer demand. Prevent an in-flight stream startup from surviving page teardown.
2026-08-07 12:40:43 +08:00
agoudbg
aec41f5889 fix: rename border-swtich -> border-switch 2026-08-06 22:02:48 +08:00
zjdndjf
2210eedbdc feat: add independent core log toggle
Closes #414
2026-08-05 20:25:40 +08:00
zjdndjf
5a348fdf04 feat: add hidden proxy groups toggle
Closes #452
2026-08-05 20:25:40 +08:00
zjdndjf
a9e0c6d26f feat: add startup subscription update toggle
Closes #542
2026-08-05 20:25:40 +08:00
Tongyuxiu Zhou
8c297c764d fix: restore main window on second instance launch (#2050) 2026-08-05 19:28:47 +08:00
TonyCrane
d7a79bc054 fix: preserve and recover macOS helper service (#2052) 2026-08-05 19:25:48 +08:00
agoudbg
6b56108ec4 fix: exclude divider height from title bar overlay (#2054) 2026-08-05 00:28:53 +08:00
agoudbg
e9ef676d60 fix: switch style in usage card (#2053) 2026-08-05 00:28:30 +08:00
Memory
5046daaa6d fix: only allow disabling tray icon with floating window 2026-08-01 11:35:26 +08:00
MOMO0302-02
badebd10a4 test: make dirs.test.ts pass on Windows (#2044)
dirs.test.ts asserts on values produced by `path.join`, but compares
them against hardcoded POSIX literals such as
'/tmp/app-data/mihomo-party-dev'. On Windows `path.join` returns
'\tmp\app-data\mihomo-party-dev', so two of the three cases fail on a
clean checkout:

  × isolates an unpackaged local development app
  × keeps portable userData precedence over local development isolation

The fs mock had the same problem: it detected the portable marker with
`value.endsWith('/PORTABLE')`, which never matches the backslash path
that `path.join` produces on Windows.

dirs.ts itself is correct; only the test encodes a platform assumption.
Build the expected values with `path.join` and match the portable
marker with `path.basename`, so the suite is platform-agnostic.

Full suite now passes on Windows 11: 20 files, 176 tests.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 02:06:13 +08:00
MOMO0302-02
0f3d338e3a fix: updater HTTP handling, macOS pkg install path and proxy port (#2042)
* fix: treat HTTP errors as failures in the updater and fix the pkg install path

1. tryDownload returned the first response it got. chromeRequest only
   rejects on transport errors, so an HTTP 404/502 from a mirror was
   returned as success and the remaining mirrors were never tried.
   checkUpdate then ran `parse()` over an HTML error page; the result has
   no `version`, so compareVersions calls `.replace` on undefined and
   throws a TypeError instead of reporting that the check failed. Check
   the status code and throw so the fallback loop keeps going.

2. downloadAndInstallUpdate only cleared updateInstallPromise when
   installUpdate rejected. The macOS .pkg branch catches an osascript
   failure and falls back to `shell.openPath`, which resolves normally
   and leaves the app running. From then on updateInstallPromise held a
   settled promise, so every later attempt returned it immediately and
   the update button did nothing. Clear it in `finally`.

3. The pkg command escaped spaces with `.replace(' ', '\\\\ ')`. A string
   pattern replaces only the first occurrence, and the macOS data
   directory ("~/Library/Application Support/...") contains more than
   one space, so the installer command was still split. Quote the path
   once and escape it for the AppleScript string literal, and invoke
   osascript through execFile so the outer command is not shell-parsed.

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

* fix: validate latest.yml and skip the proxy when the mixed port is off

- checkUpdate parsed latest.yml and used the result without checking it.
  A proxy error page can still parse as YAML (`404: Not Found` is a valid
  mapping), so `latest.version` was undefined and compareVersions threw a
  TypeError on `a.replace` instead of reporting a failed check.

- Both the update check and the download built the proxy from the mixed
  port through a destructuring default, which only covers undefined.
  Turning the port off stores 0, so every request went to 127.0.0.1:0 and
  failed. Go direct when no port is enabled.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 02:05:58 +08:00
MOMO0302-02
5523129529 fix: restore the real tray icon when the traffic display is turned off (#2039)
Turning the traffic display off sent trayIconUpdate with enabled=false,
but the handler ignores that flag and always applies the supplied data
URL, so the tray kept showing the renderer-composed image until the app
was restarted. Ask the main process to redraw from its own resources
afterwards.

Refs #1080

This covers the "does not recover" half of the report. The colours being
wrong while the traffic display is on has the same root cause as #1143 -
the handler calls setTemplateImage(true) unconditionally, so macOS renders
alpha only - and needs a change in tray.ts.
2026-08-01 02:05:37 +08:00
MOMO0302-02
be1761ea15 fix: notify the renderer after a scheduled subscription update (#2034)
The scheduled updater calls addProfileItem, which never sends
profileConfigUpdated. The subscription list revalidates on that event, so
the "updated N minutes ago" line and the traffic figures stayed stale
until the window regained focus - which reads exactly like the interval
not working. The plugin update path already notifies; only the remote one
did not.

Refs #1570

This addresses the stale UI half of the report. A scheduled update of the
profile that is currently active still writes the new YAML without
reloading it into the running core, because changeCurrentProfile returns
early when the id is unchanged; that part is left for a separate change.
2026-08-01 02:05:15 +08:00
MOMO0302-02
841c3cfe22 fix: harden traffic stream parsing and sysproxy status lookup (#2022)
Two independent robustness defects in mihomoApi.

1. In mihomoTraffic(), `JSON.parse(data)` sat outside the try block,
   unlike the memory, logs and connections streams where it is inside.
   The handler was also `async`, so a non-JSON frame from the core threw
   inside a promise with no rejection handler, producing an
   unhandledRejection instead of being ignored like the other three
   streams. Move the parse inside the try and drop the unnecessary
   `async`.

2. SysProxyStatus() read `appConfig.sysProxy.enable` with no optional
   chaining, while the neighbouring TunStatus() already guards with
   `config?.tun?.enable`. If the app config is missing or truncated,
   sysProxy is undefined and the property access throws. That rejection
   propagates through getTrayIconStatus(), so the tray icon stops
   reflecting the real state. Guard it the same way TunStatus does.

Closes #1286

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 02:04:53 +08:00
zjdndjf
4c168ffa96 fix: validate system proxy bypass formats 2026-08-01 01:29:40 +08:00
Memory
ac7bf09612 fix: validate TUN MTU range 2026-07-28 09:52:04 +08:00
luantu
f9356aa657 feat(traffic): 为详情表格和子行展开添加 loading 动画 2026-07-27 09:56:20 +08:00
zjdndjf
beb54159bf fix: recover core before config push
Closes #2011
2026-07-27 09:44:09 +08:00
Wenjie Fu
0dd6481712 fix: restore macOS connection app icons 2026-07-25 19:11:40 +08:00
Tongyuxiu Zhou
e019290493 feat: redesign airport plugin UI & support per-plugin proxy settings (#2007)
* feat: redesign and streamline the airport plugin interface

* feat: support per-plugin proxy settings
2026-07-23 02:18:21 +08:00
zjdndjf
223b7f46de fix: restore proxy group expansion state
close #2009
2026-07-23 00:49:11 +08:00
zjdndjf
a84c18a7c3 feat: add silent app updates 2026-07-22 03:17:42 +08:00
Memory
6ae19f4ca1 feat: add QR code support for subscriptions and proxy providers 2026-07-20 21:54:14 +08:00
moon
196fdcbf86 fix: prevent killing unintended processes due to PID reuse in lightweight mode 2026-07-19 06:10:12 +08:00