feat(routing): 存档案后连接态自动重连使新规则生效

Task 9: RoutingProfileNotifier._persist() 保存成功后,若 connectionProvider.phase
== on,触发 ConnectionController.reapplyRoutingProfile()(disconnect→connect,
不改选中节点)重连,让服务端按新档案渲染的规则生效;off 态不触发。

新增 l10n 键 routingRulesReconnecting(单源 design/i18n/strings.json → codegen),
「规则已更新,正在重连…」区别于既有 nodeReconnecting(弱网抖动语义)。

reapplyRoutingProfile 内先置 _userDisconnect=true 再 disconnect,避免其触发的
kernel off 事件被 _onKernelStatus 误判「意外掉线」、再抢跑一次 watchdog 自动重连。
This commit is contained in:
wangjia
2026-07-28 09:17:32 +08:00
parent 86e5b6d451
commit 4c76c2a701
11 changed files with 228 additions and 0 deletions
+1
View File
@@ -387,6 +387,7 @@ abstract class AppText {
String get nodeUnhealthySwitched; // 智能选择已自动切换
String get nodeUnhealthyError; // 手动节点:断开并提示
String get nodeReconnecting; // 弱网抖动:自动重连当前节点中(#18)
String get routingRulesReconnecting; // 分流档案保存后自动重连(Task 9)
String get smartSub;
String get recommended;
+2
View File
@@ -89,6 +89,8 @@ class StringsEn extends AppText {
@override
String get nodeReconnecting => 'Network unstable, reconnecting…';
@override
String get routingRulesReconnecting => 'Rules updated, reconnecting…';
@override
String get smartSub => 'Picks the best node for your network';
@override
String get recommended => 'Recommended';
+2
View File
@@ -89,6 +89,8 @@ class StringsEs extends AppText {
@override
String get nodeReconnecting => 'Red inestable, reconectando…';
@override
String get routingRulesReconnecting => 'Reglas actualizadas, reconectando…';
@override
String get smartSub => 'Elige el mejor nodo para tu red';
@override
String get recommended => 'Recomendado';
+2
View File
@@ -89,6 +89,8 @@ class StringsJa extends AppText {
@override
String get nodeReconnecting => 'ネットワークが不安定です。再接続中…';
@override
String get routingRulesReconnecting => 'ルールが更新されました。再接続中…';
@override
String get smartSub => 'お使いのネットワークに最適なノードを選びます';
@override
String get recommended => 'おすすめ';
+2
View File
@@ -89,6 +89,8 @@ class StringsKo extends AppText {
@override
String get nodeReconnecting => '네트워크가 불안정하여 다시 연결 중…';
@override
String get routingRulesReconnecting => '규칙이 업데이트되어 다시 연결 중…';
@override
String get smartSub => '네트워크에 가장 적합한 노드를 선택합니다';
@override
String get recommended => '추천';
+2
View File
@@ -89,6 +89,8 @@ class StringsRu extends AppText {
@override
String get nodeReconnecting => 'Сеть нестабильна, переподключение…';
@override
String get routingRulesReconnecting => 'Правила обновлены, переподключение…';
@override
String get smartSub => 'Подбирает лучший узел для вашей сети';
@override
String get recommended => 'Рекомендуется';
+2
View File
@@ -89,6 +89,8 @@ class StringsZh extends AppText {
@override
String get nodeReconnecting => '网络波动,正在重连…';
@override
String get routingRulesReconnecting => '规则已更新,正在重连…';
@override
String get smartSub => '根据当前网络环境,自动选择最优节点';
@override
String get recommended => '推荐';
+18
View File
@@ -182,6 +182,24 @@ class ConnectionController extends StateNotifier<ConnectionState> {
}
}
/// 分流档案保存成功后触发(Task 9):仅当前已连接(on)才重连,使服务端按新档案
/// 渲染的规则生效;off 态不触发(下次连接自然拿到新配置)。复刻 onNodeChanged 的
/// disconnect→connect 序列(不改选中节点),附加「规则已更新」瞬态提示 ——
/// 与 _tryAutoReconnectCurrent 同法:重连发起后若仍在握手/已连上(非 off),把
/// error 字段临时置成提示文案,连上后随下一次 copyWith(仅 phase/elapsed)自然清除。
Future<void> reapplyRoutingProfile() async {
if (state.phase != VpnPhase.on) return;
// 标记「非节点异常」的主动断开:否则 bridge.stop() 触发的 kernel off 事件会被
// _onKernelStatus 当成意外掉线,拉着 _handleUnexpectedOff 再抢跑一次自动重连,
// 与这里的 _connect() 撞车、多连一次(同 toggle() 主动断开时的用法)。
_userDisconnect = true;
await _disconnect();
await _connect();
if (mounted && state.phase != VpnPhase.off) {
state = ConnectionState(phase: state.phase, error: _ref.read(appTextProvider).routingRulesReconnecting);
}
}
// ── 内部 ─────────────────────────────────────────────────────
Future<void> _connect() async {
+15
View File
@@ -10,11 +10,14 @@
// **当前已存的 state**(此时已是乐观更新后的 next)重新 copyWithPrevious 一次,
// 会把我们想还原的旧值又覆盖回 next,达不到「回退」效果。故失败时直接整体
// `state = AsyncData(previous)`(纯回退,无错误标记)+ rethrow 传递异常。
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/routing_profile.dart';
import 'account_providers.dart';
import 'auth_provider.dart';
import 'connection_provider.dart';
/// 当前账户的分流档案。未登录返回默认档案(不打后端)。
class RoutingProfileNotifier extends AsyncNotifier<RoutingProfile> {
@@ -81,6 +84,18 @@ class RoutingProfileNotifier extends AsyncNotifier<RoutingProfile> {
state = AsyncData(previous);
rethrow;
}
_reconnectIfConnected(); // Task 9:保存成功且已连接 → 重连使新规则生效
}
/// 用 ref.read(非 watch)单向读取 connectionProvider:仅本 notifier 在保存成功后
/// 顺带触发一次重连,不建立「connectionProvider 依赖 routingProfileProvider」的反向
/// 引用,provider 依赖图仍是单向(routing → connection),不成环。off 态不触发——
/// 下次连接本就会按最新档案渲染配置。重连失败由 connection_provider 自身状态承接
/// (沿用其既有错误提示),不 rethrow 到这里,不影响「保存」这个操作本身的成败。
void _reconnectIfConnected() {
if (ref.read(connectionProvider).phase == VpnPhase.on) {
unawaited(ref.read(connectionProvider.notifier).reapplyRoutingProfile());
}
}
}