feat(bridge): 桥接 API 面 Channel 契约 + 三端原生骨架 [tsk_iP5-_Ztq5THx]

MethodChannel pangolin/vpn (start/stop/getStatus/selectOutbound/
getActiveOutbound/setKillSwitch) + 双 EventChannel (status/stats)
契约定义在 lib/bridge/vpn_bridge.dart 头部注释冻结。

- Dart: VpnBridge 抽象接口 + VpnNativeBridge 原生实现 + VpnBridgeMock
  假内核(含 connectDelay/statsInterval 可配参数)
- Dart 桌面: kernel_process.dart KernelProcess/ClashApiClient 接口骨架
- Android: PangolinVpnService (VpnService 骨架 + 前台通知占位) +
  VpnEventBus 解耦总线 + MainActivity MethodChannel/EventChannel 注册
- iOS: PacketTunnelProvider (NEPacketTunnelProvider 骨架) + VpnManager
  (NETunnelProviderManager + NEVPNStatus 订阅) + AppDelegate 通道注册
  + Xcode project.pbxproj 添加 PacketTunnel extension target
- Widget: VpnStatus enum 迁移至 bridge/vpn_bridge.dart (+error 态),
  connect_button/home_shell 更新穷举匹配
- Test: test/bridge/vpn_bridge_mock_test.dart 覆盖 start→connecting→on
  →stop→off 序列、stats 周期推帧、selectOutbound 重连序列、schema 回测

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-13 12:06:03 +08:00
parent 787151245e
commit 4992d916d0
16 changed files with 1677 additions and 11 deletions
+137
View File
@@ -0,0 +1,137 @@
// vpn_bridge_mock.dart — 假内核实现,供 UI 层先行联调
//
// 行为:
// start() → 立即 emit connecting[connectDelay] 后 emit on,启动统计计时器
// stop() → 立即 emit off,停止统计计时器
// stats → on 状态下按 [statsInterval] 周期推假数据帧
//
// 用法示例(UI 联调):
// final bridge = VpnBridgeMock();
//
// 用法示例(测试):
// final bridge = VpnBridgeMock(
// connectDelay: Duration(milliseconds: 50),
// statsInterval: Duration(milliseconds: 50),
// );
import 'dart:async';
import 'dart:math';
import 'vpn_bridge.dart';
class VpnBridgeMock implements VpnBridge {
VpnBridgeMock({
this.connectDelay = const Duration(milliseconds: 1500),
this.statsInterval = const Duration(seconds: 1),
});
final Duration connectDelay;
final Duration statsInterval;
// 内部状态
VpnStatus _status = VpnStatus.off;
String _activeOutbound = 'auto';
bool _killSwitch = false;
final _statusCtrl = StreamController<VpnStatus>.broadcast();
final _statsCtrl = StreamController<VpnStatsEvent>.broadcast();
Timer? _connectTimer;
Timer? _statsTimer;
int _tickCount = 0;
final _rng = Random();
// ── helpers ───────────────────────────────────────────────────
void _emitStatus(VpnStatus s) {
_status = s;
if (!_statusCtrl.isClosed) _statusCtrl.add(s);
}
void _startStats() {
_tickCount = 0;
_statsTimer?.cancel();
_statsTimer = Timer.periodic(statsInterval, (_) {
_tickCount++;
if (!_statsCtrl.isClosed) _statsCtrl.add(_fakeTick(_tickCount));
});
}
VpnStatsEvent _fakeTick(int t) {
// 模拟稳定带宽 + 微抖动
final upSpeed = 10240.0 + _rng.nextDouble() * 2048;
final downSpeed = 51200.0 + _rng.nextDouble() * 8192;
return VpnStatsEvent(
uploadBytes: (upSpeed * t * statsInterval.inMilliseconds / 1000).round(),
downloadBytes: (downSpeed * t * statsInterval.inMilliseconds / 1000).round(),
uploadSpeed: upSpeed,
downloadSpeed: downSpeed,
urltestResults: const [
UrltestResult(tag: 'hk-01', delayMs: 18),
UrltestResult(tag: 'jp-01', delayMs: 32),
UrltestResult(tag: 'sg-01', delayMs: 54),
],
);
}
// ── VpnBridge methods ─────────────────────────────────────────
@override
Future<void> start(String configJson) async {
_connectTimer?.cancel();
_statsTimer?.cancel();
_emitStatus(VpnStatus.connecting);
_connectTimer = Timer(connectDelay, () {
_emitStatus(VpnStatus.on);
_startStats();
});
}
@override
Future<void> stop() async {
_connectTimer?.cancel();
_statsTimer?.cancel();
_connectTimer = null;
_statsTimer = null;
_emitStatus(VpnStatus.off);
}
@override
Future<VpnStatus> getStatus() async => _status;
@override
Future<void> selectOutbound(String tag) async {
_activeOutbound = tag;
// 切换出口时模拟短暂重连
if (_status == VpnStatus.on) {
_statsTimer?.cancel();
_emitStatus(VpnStatus.connecting);
_connectTimer = Timer(connectDelay ~/ 2, () {
_emitStatus(VpnStatus.on);
_startStats();
});
}
}
@override
Future<String> getActiveOutbound() async => _activeOutbound;
@override
Future<void> setKillSwitch({required bool on}) async {
_killSwitch = on;
}
@override
Stream<VpnStatus> get statusStream => _statusCtrl.stream;
@override
Stream<VpnStatsEvent> get statsStream => _statsCtrl.stream;
@override
void dispose() {
_connectTimer?.cancel();
_statsTimer?.cancel();
_statusCtrl.close();
_statsCtrl.close();
}
}