// vpn_bridge.dart — Dart↔原生桥接 API 契约(冻结字段) // // ═══════════════════════════════════════════════════════════════ // 通道名称(Channel names) // ═══════════════════════════════════════════════════════════════ // MethodChannel : pangolin/vpn // EventChannel : pangolin/vpn/status // EventChannel : pangolin/vpn/stats // // ═══════════════════════════════════════════════════════════════ // MethodChannel 方法签名(Method signatures)—— 不得私改 // ═══════════════════════════════════════════════════════════════ // start(String configJson) → void // stop() → void // getStatus() → String ("off"|"connecting"|"on"|"error") // selectOutbound(String tag) → void // getActiveOutbound() → String tag // setKillSwitch(bool on) → void // // ═══════════════════════════════════════════════════════════════ // EventChannel 事件 schema —— 不得私改 // ═══════════════════════════════════════════════════════════════ // pangolin/vpn/status → String // 值: "off" | "connecting" | "on" | "error" // 来源:内核回调(严禁 UI 乐观显示;start 返回 ≠ on) // // pangolin/vpn/stats → Map // { // "uploadBytes" : int, // 本次隧道累计上传字节 // "downloadBytes" : int, // 本次隧道累计下载字节 // "uploadSpeed" : double, // bytes/s 瞬时上行速率 // "downloadSpeed" : double, // bytes/s 瞬时下行速率 // "urltestResults" : [ // urltest 延迟结果列表 // { "tag": String, "delayMs": int }, // ... // ] // } // 周期:约 1 秒一帧(内核驱动,非固定间隔) // // 最后修订:任务 tsk_iP5-_Ztq5THx(桥接 API 面定稿) // ═══════════════════════════════════════════════════════════════ import 'dart:async'; import 'package:flutter/services.dart'; // ── 状态枚举 ──────────────────────────────────────────────────── // 与 design/flutter/widgets/connect_button.dart VpnStatus 对齐。 // error 为桥接层扩展态,仅由内核回调产生,UI 层按需处理。 enum VpnStatus { off, connecting, on, error; /// 从原生侧字符串解析 static VpnStatus fromString(String s) => switch (s) { 'off' => VpnStatus.off, 'connecting' => VpnStatus.connecting, 'on' => VpnStatus.on, 'error' => VpnStatus.error, _ => VpnStatus.error, }; /// 序列化为原生侧字符串 String toNativeString() => switch (this) { VpnStatus.off => 'off', VpnStatus.connecting => 'connecting', VpnStatus.on => 'on', VpnStatus.error => 'error', }; } // ── stats 数据模型 ─────────────────────────────────────────────── class UrltestResult { const UrltestResult({required this.tag, required this.delayMs}); final String tag; final int delayMs; factory UrltestResult.fromMap(Map m) => UrltestResult( tag: m['tag'] as String? ?? '', delayMs: (m['delayMs'] as num?)?.toInt() ?? -1, ); Map toMap() => {'tag': tag, 'delayMs': delayMs}; @override String toString() => 'UrltestResult(tag=$tag, delayMs=$delayMs)'; } class VpnStatsEvent { const VpnStatsEvent({ required this.uploadBytes, required this.downloadBytes, required this.uploadSpeed, required this.downloadSpeed, required this.urltestResults, }); final int uploadBytes; final int downloadBytes; final double uploadSpeed; final double downloadSpeed; final List urltestResults; factory VpnStatsEvent.fromMap(Map m) => VpnStatsEvent( uploadBytes: (m['uploadBytes'] as num?)?.toInt() ?? 0, downloadBytes: (m['downloadBytes'] as num?)?.toInt() ?? 0, uploadSpeed: (m['uploadSpeed'] as num?)?.toDouble() ?? 0.0, downloadSpeed: (m['downloadSpeed'] as num?)?.toDouble() ?? 0.0, urltestResults: ((m['urltestResults'] as List?) ?.whereType() .map((e) => UrltestResult.fromMap( Map.from(e), )) .toList()) ?? const [], ); /// 从平台通道原始 Map(Map)解析,自动转换键类型。 static VpnStatsEvent fromNativeMap(Map raw) => VpnStatsEvent.fromMap(raw.cast()); Map toMap() => { 'uploadBytes': uploadBytes, 'downloadBytes': downloadBytes, 'uploadSpeed': uploadSpeed, 'downloadSpeed': downloadSpeed, 'urltestResults': urltestResults.map((r) => r.toMap()).toList(), }; @override String toString() => 'VpnStatsEvent(up=${uploadBytes}B, down=${downloadBytes}B, ' 'upSpeed=${uploadSpeed.toStringAsFixed(0)}B/s, ' 'downSpeed=${downloadSpeed.toStringAsFixed(0)}B/s, ' 'urltest=${urltestResults.length}nodes)'; } // ── 抽象接口 ───────────────────────────────────────────────────── /// Dart 侧 VPN 桥接接口。 /// 实现方:[VpnNativeBridge](真实原生通道)与 [VpnBridgeMock](UI 联调)。 abstract class VpnBridge { // ── MethodChannel 方法 ── /// 启动 VPN。[configJson] 为 sing-box 兼容 JSON 配置字符串。 /// 返回不代表连接已建立,实际状态由 [statusStream] 驱动。 Future start(String configJson); /// 停止 VPN。 Future stop(); /// 查询当前状态(一次性拉取,实时监听用 [statusStream])。 Future getStatus(); /// 切换出口节点。[tag] 为 sing-box outbound tag。 Future selectOutbound(String tag); /// 获取当前激活出口节点 tag。 Future getActiveOutbound(); /// 设置 Kill Switch 开关。 Future setKillSwitch({required bool on}); // ── EventChannel 流 ── /// 实时 VPN 状态流(来自内核回调)。 Stream get statusStream; /// 约每秒推一帧统计数据流(来自内核计时器)。 Stream get statsStream; /// 释放资源。 void dispose(); } // ── 原生实现 ───────────────────────────────────────────────────── /// 生产实现:使用 Flutter MethodChannel / EventChannel 与原生侧通信。 class VpnNativeBridge implements VpnBridge { VpnNativeBridge() : _method = const MethodChannel('pangolin/vpn'), _statusRaw = const EventChannel('pangolin/vpn/status'), _statsRaw = const EventChannel('pangolin/vpn/stats'); // 允许测试注入 VpnNativeBridge.withChannels({ required MethodChannel method, required EventChannel status, required EventChannel stats, }) : _method = method, _statusRaw = status, _statsRaw = stats; final MethodChannel _method; final EventChannel _statusRaw; final EventChannel _statsRaw; @override Future start(String configJson) => _method.invokeMethod('start', configJson); @override Future stop() => _method.invokeMethod('stop'); @override Future getStatus() async { final s = await _method.invokeMethod('getStatus') ?? 'off'; return VpnStatus.fromString(s); } @override Future selectOutbound(String tag) => _method.invokeMethod('selectOutbound', tag); @override Future getActiveOutbound() async => await _method.invokeMethod('getActiveOutbound') ?? 'auto'; @override Future setKillSwitch({required bool on}) => _method.invokeMethod('setKillSwitch', on); // ⚠️ EventChannel 原生侧只保留「一个」sink:每次 receiveBroadcastStream() + listen 都会 // 触发 onListen 覆盖原生 sink → 多个订阅者(连接页速度 + ConnectionController 回写延迟) // 互相抢,后订阅的赢、先订阅的流变哑。必须只调一次 receiveBroadcastStream()、缓存成共享 // 广播流,所有 Dart 订阅者复用同一条原生订阅。 late final Stream _statusStream = _statusRaw .receiveBroadcastStream() .map((e) => VpnStatus.fromString(e?.toString() ?? 'error')) .asBroadcastStream(); late final Stream _statsStream = _statsRaw .receiveBroadcastStream() .where((e) => e is Map) .map((e) => VpnStatsEvent.fromNativeMap( Map.from(e as Map), )) .asBroadcastStream(); @override Stream get statusStream => _statusStream; @override Stream get statsStream => _statsStream; @override void dispose() { // EventChannel 由 Flutter 引擎管理,无需手动关闭 } }