Files
pangolin/client/lib/bridge/vpn_bridge_mock.dart
T
wangjia 4992d916d0 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>
2026-06-13 12:06:03 +08:00

138 lines
3.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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();
}
}