From 28f8e5ebcd0dc25dab3b9a05a940cf8f7fc94b2d Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Mon, 29 Jun 2026 19:08:49 +0800 Subject: [PATCH] =?UTF-8?q?fix(client):=20=E8=87=AA=E5=8A=A8=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5=E7=AB=9E=E6=80=81=E7=9C=9F=E5=9B=A0=20=E2=80=94=20?= =?UTF-8?q?=E7=AD=89=E8=AE=BE=E7=BD=AE=E5=8A=A0=E8=BD=BD=E5=AE=8C=E5=86=8D?= =?UTF-8?q?=E5=88=A4(1.0.22)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 日志实证:节点就绪(~450ms)与设置加载几乎同时完成,节点回调早 1ms 读到还没加载完的 autoConnect=false → 不连。AppSettings 加 loaded 标志(_load 完成置 true,失败也置); _maybeAutoConnect 同时监听 nodes + settings,二者都就绪(settings.loaded && 节点 uuid 非空) 才决策一次。 Co-Authored-By: Claude Opus 4.8 --- client/lib/main.dart | 45 ++++++++++++++----------- client/lib/state/settings_provider.dart | 19 +++++++---- client/pubspec.yaml | 2 +- client/windows/installer/pangolin.iss | 2 +- 4 files changed, 40 insertions(+), 28 deletions(-) diff --git a/client/lib/main.dart b/client/lib/main.dart index 14d4d6c..c56bcd5 100644 --- a/client/lib/main.dart +++ b/client/lib/main.dart @@ -121,34 +121,39 @@ class _RootFlowState extends ConsumerState { bool _autoConnectArmed = false; // 自动连接每进程只尝试一次 bool _homeLogged = false; // 仅日志:HomeShell 首次显示打一行 - // 进入登录态主界面后:若开了「自动连接」且当前未连接,等节点就绪后自动连一次。 + // 进入登录态主界面后:开了「自动连接」且当前未连接,就自动连一次。 + // 关键:必须同时等「设置已从持久化加载完(settings.loaded)」与「节点已就绪」—— + // 二者都是异步且完成时刻接近,早读 autoConnect 会撞上还没加载完的默认 false(实测差 1ms)。 + // 同时监听两者,任一变化就重判,直到都就绪再决策(只决策一次)。 void _maybeAutoConnect() { if (_autoConnectArmed) return; _autoConnectArmed = true; - logLine('AutoConnect', 'armed; waiting for nodes ready'); - // 注意:不在此处早读 settingsProvider.autoConnect —— 其 _load 是异步(SharedPreferences), - // 启动这一刻多半还没加载完,会读到默认 false 而永久跳过。改到「节点就绪」回调里再判: - // 节点要走网络拉取,远慢于本地设置读取,那时 autoConnect 必已加载到位。 - // 不用回调的 next(裸 ProviderSubscription 会让它退化成 dynamic,valueOrNull 这种 - // 扩展 getter 在 dynamic 上不解析 → 运行期 NoSuchMethod)。直接 ref.read 拿强类型值。 - late final ProviderSubscription>> sub; - sub = ref.listenManual(nodesProvider, (prev, next) { - final av = ref.read(nodesProvider); - final nodes = av.valueOrNull ?? const []; - final ready = nodes.any((n) => n.uuid.isNotEmpty); - logLine('AutoConnect', 'nodes update: total=${nodes.length} ready=$ready loading=${av.isLoading} err=${av.hasError}'); - if (!ready) return; - sub.close(); - final ac = ref.read(settingsProvider).autoConnect; + logLine('AutoConnect', 'armed; waiting for settings loaded + nodes ready'); + ProviderSubscription>>? nodeSub; + ProviderSubscription? settingsSub; + var decided = false; + void tryGo() { + if (decided) return; + final settings = ref.read(settingsProvider); + final nodes = ref.read(nodesProvider).valueOrNull ?? const []; + final nodesReady = nodes.any((n) => n.uuid.isNotEmpty); + if (!settings.loaded || !nodesReady) return; // 等两者都就绪 + decided = true; + nodeSub?.close(); + settingsSub?.close(); final phase = ref.read(connectionProvider).phase; - logLine('AutoConnect', 'nodes ready → autoConnect=$ac phase=$phase'); - if (ac && phase == VpnPhase.off) { + logLine('AutoConnect', 'ready: autoConnect=${settings.autoConnect} phase=$phase nodes=${nodes.length}'); + if (settings.autoConnect && phase == VpnPhase.off) { logLine('AutoConnect', 'triggering connect'); ref.read(connectionProvider.notifier).toggle(); } else { - logLine('AutoConnect', 'NOT connecting (autoConnect=$ac phase=$phase)'); + logLine('AutoConnect', 'NOT connecting (autoConnect=${settings.autoConnect} phase=$phase)'); } - }, fireImmediately: true); + } + + nodeSub = ref.listenManual(nodesProvider, (_, __) => tryGo()); + settingsSub = ref.listenManual(settingsProvider, (_, __) => tryGo()); + tryGo(); } // 登录/注册成功后:未看过引导才进引导,否则直接主界面。 diff --git a/client/lib/state/settings_provider.dart b/client/lib/state/settings_provider.dart index 42461ea..69d5ed8 100644 --- a/client/lib/state/settings_provider.dart +++ b/client/lib/state/settings_provider.dart @@ -20,17 +20,21 @@ class AppSettings { this.autoConnect = false, this.smartRoute = true, this.killSwitch = true, + this.loaded = false, }); final bool autostart; final bool autoConnect; // 启动 app 时自动连接(opt-in,默认关) final bool smartRoute; final bool killSwitch; + final bool loaded; // 是否已从持久化加载完(自动连接要等它 true 再读 autoConnect,避免竞态) - AppSettings copyWith({bool? autostart, bool? autoConnect, bool? smartRoute, bool? killSwitch}) => AppSettings( + AppSettings copyWith({bool? autostart, bool? autoConnect, bool? smartRoute, bool? killSwitch, bool? loaded}) => + AppSettings( autostart: autostart ?? this.autostart, autoConnect: autoConnect ?? this.autoConnect, smartRoute: smartRoute ?? this.smartRoute, killSwitch: killSwitch ?? this.killSwitch, + loaded: loaded ?? this.loaded, ); } @@ -51,22 +55,25 @@ class SettingsController extends StateNotifier { final bool _desktop = Platform.isMacOS || Platform.isWindows || Platform.isLinux; Future _load() async { + var s = const AppSettings(loaded: true); // 失败也标 loaded:true,自动连接才不会永久等 try { final p = await SharedPreferences.getInstance(); if (!mounted) return; // 异步初始化期间已 dispose:别再写 state。 - state = AppSettings( + s = AppSettings( autostart: p.getBool(_kAutostart) ?? true, autoConnect: p.getBool(_kAutoConnect) ?? false, smartRoute: p.getBool(_kSmartRoute) ?? true, killSwitch: p.getBool(_kKillSwitch) ?? true, + loaded: true, ); - logLine('Settings', 'loaded autoConnect=${state.autoConnect} autostart=${state.autostart}'); } catch (_) { - // 测试/无平台:保留默认值。 + // 测试/无平台:用默认值(已标 loaded)。 } if (!mounted) return; - _applyKillSwitch(state.killSwitch); - if (_desktop) await _applyAutostart(state.autostart); + state = s; + logLine('Settings', 'loaded autoConnect=${s.autoConnect} autostart=${s.autostart}'); + _applyKillSwitch(s.killSwitch); + if (_desktop) await _applyAutostart(s.autostart); } Future setAutostart(bool v) async { diff --git a/client/pubspec.yaml b/client/pubspec.yaml index 6d86c3e..4e69bfa 100644 --- a/client/pubspec.yaml +++ b/client/pubspec.yaml @@ -1,7 +1,7 @@ name: pangolin_vpn description: 穿山甲 · Pangolin — 极简、稳定、跨平台网络加速客户端。 publish_to: "none" -version: 1.0.21+22 +version: 1.0.22+23 environment: sdk: ^3.5.0 diff --git a/client/windows/installer/pangolin.iss b/client/windows/installer/pangolin.iss index 9bff8b5..e4cdbb4 100644 --- a/client/windows/installer/pangolin.iss +++ b/client/windows/installer/pangolin.iss @@ -3,7 +3,7 @@ ; 前置: 先在 client/ 跑 `flutter build windows`(Release),产物在 ; ..\..\build\windows\x64\runner\Release #define MyAppName "穿山甲 Pangolin" -#define MyAppVersion "1.0.21" +#define MyAppVersion "1.0.22" #define MyAppPublisher "Pangolin" #define MyAppExeName "pangolin_vpn.exe" #define BuildDir "..\..\build\windows\x64\runner\Release"