feat: M1.1 完成 + M1.2 启动 — 全量更新

M1.1 收尾:
- 24项 P0/P1/P2 bug 修复 (Rust 107 tests + Flutter 15 tests)
- Flutter App v0.3: cupertino_icons 修复, 单元测试, 调试面板, APK 52.6MB
- 示例插件完善: manifest.json + 请求/响应示范 + 7个测试
- API 文档重写 (以 routes.rs 为唯一权威)
- MILESTONES.md 更新至 100%

M1.2 启动:
- P0: 插件管理 API 闭环 (handle_manager_message Custom 分支 + broadcast_plugin_states)
- ServiceManager 集成测试 8/8 (tests/m1_2_service_manager.rs)
- M1.2 测试计划 (docs/M1.2_TEST_PLAN.md, 18个E2E场景)
- 动态插件系统: auto_rollback + version_manager GC + 路径穿越防护

总计: Rust 115/115 测试, Flutter 15/15 测试, 零 warning

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
showen
2026-03-14 18:12:42 +08:00
parent 8ed9cb2d9d
commit d30c111c71
68 changed files with 8115 additions and 1201 deletions

View File

@@ -5,15 +5,24 @@ import 'package:web_socket_channel/web_socket_channel.dart';
import '../models/app_event.dart';
enum WsConnectionState { connected, connecting, disconnected }
@Deprecated('Use WsConnectionState instead')
enum SocketConnectionStatus { disconnected, connecting, connected }
class WebSocketService {
static const Duration _initialReconnectDelay = Duration(seconds: 2);
static const Duration _maxReconnectDelay = Duration(seconds: 60);
WebSocketChannel? _channel;
StreamSubscription<dynamic>? _subscription;
Timer? _reconnectTimer;
String? _deviceIp;
int _devicePort = 5000;
bool _manualDisconnect = false;
SocketConnectionStatus _connectionStatus = SocketConnectionStatus.disconnected;
WsConnectionState _connectionState = WsConnectionState.disconnected;
int _retryCount = 0;
Duration _nextReconnectDelay = _initialReconnectDelay;
final StreamController<AppEvent> _eventController =
StreamController<AppEvent>.broadcast();
@@ -27,8 +36,8 @@ class WebSocketService {
StreamController<Map<String, dynamic>>.broadcast();
final StreamController<Map<String, dynamic>> _bleController =
StreamController<Map<String, dynamic>>.broadcast();
final StreamController<SocketConnectionStatus> _connectionController =
StreamController<SocketConnectionStatus>.broadcast();
final StreamController<WsConnectionState> _connectionStateController =
StreamController<WsConnectionState>.broadcast();
Stream<AppEvent> get events => _eventController.stream;
Stream<Map<String, dynamic>> get onStatusUpdate => _statusController.stream;
@@ -36,32 +45,76 @@ class WebSocketService {
Stream<Map<String, dynamic>> get onConfigUpdate => _configController.stream;
Stream<Map<String, dynamic>> get onWifiUpdate => _wifiController.stream;
Stream<Map<String, dynamic>> get onBleUpdate => _bleController.stream;
Stream<SocketConnectionStatus> get connectionState =>
_connectionStateController.stream.map(_toLegacyConnectionStatus);
Stream<WsConnectionState> get connectionStateStream =>
_connectionStateController.stream;
Stream<SocketConnectionStatus> get onConnectionChanged =>
_connectionController.stream;
_connectionStateController.stream.map(_toLegacyConnectionStatus);
SocketConnectionStatus get connectionStatus => _connectionStatus;
bool get isConnected => _connectionStatus == SocketConnectionStatus.connected;
WsConnectionState get wsConnectionState => _connectionState;
SocketConnectionStatus get connectionStatus =>
_toLegacyConnectionStatus(_connectionState);
bool get isConnected => _connectionState == WsConnectionState.connected;
int get retryCount => _retryCount;
Future<void> connect(String deviceIp) async {
Future<void> connect(String deviceIp, {int port = 5000}) async {
_manualDisconnect = false;
_deviceIp = _normalizeDeviceIp(deviceIp);
_devicePort = _normalizePort(port);
_reconnectTimer?.cancel();
await _establishConnection(resetBackoff: true);
}
Future<void> manualReconnect() async {
final deviceIp = _deviceIp;
if (deviceIp == null || deviceIp.isEmpty) {
return;
}
_manualDisconnect = false;
_reconnectTimer?.cancel();
await _establishConnection(resetBackoff: true);
}
Future<void> _establishConnection({bool resetBackoff = false}) async {
if (resetBackoff) {
_retryCount = 0;
_nextReconnectDelay = _initialReconnectDelay;
}
await _subscription?.cancel();
_subscription = null;
await _channel?.sink.close();
_channel = null;
_setConnectionStatus(SocketConnectionStatus.connecting);
_setConnectionState(WsConnectionState.connecting);
final url = Uri.parse('ws://$_deviceIp:8080/ws');
_channel = WebSocketChannel.connect(url);
_subscription = _channel!.stream.listen(
_handleMessage,
onDone: _handleSocketClosed,
onError: (_) => _handleSocketClosed(),
cancelOnError: true,
);
try {
final url = Uri.parse('ws://$_deviceIp:$_devicePort/ws');
_channel = WebSocketChannel.connect(url);
await _channel!.ready;
_subscription = _channel!.stream.listen(
_handleMessage,
onDone: _handleSocketClosed,
onError: (_) => _handleSocketClosed(),
cancelOnError: true,
);
_setConnectionStatus(SocketConnectionStatus.connected);
_retryCount = 0;
_nextReconnectDelay = _initialReconnectDelay;
_setConnectionState(WsConnectionState.connected);
} catch (_) {
_channel = null;
_subscription = null;
if (_manualDisconnect) {
_setConnectionState(WsConnectionState.disconnected);
return;
}
await reconnect();
}
}
void sendCommand(Map<String, dynamic> command) {
@@ -77,20 +130,38 @@ class WebSocketService {
return;
}
_setConnectionState(WsConnectionState.connecting);
_scheduleReconnect();
}
void _scheduleReconnect() {
if (_manualDisconnect) {
return;
}
_reconnectTimer?.cancel();
_reconnectTimer = Timer(const Duration(seconds: 2), () {
unawaited(connect(deviceIp));
final delay = _nextReconnectDelay;
_retryCount += 1;
_nextReconnectDelay = _nextReconnectDelay * 2;
if (_nextReconnectDelay > _maxReconnectDelay) {
_nextReconnectDelay = _maxReconnectDelay;
}
_reconnectTimer = Timer(delay, () {
unawaited(_establishConnection());
});
}
Future<void> disconnect() async {
_manualDisconnect = true;
_reconnectTimer?.cancel();
_retryCount = 0;
_nextReconnectDelay = _initialReconnectDelay;
await _subscription?.cancel();
_subscription = null;
await _channel?.sink.close();
_channel = null;
_setConnectionStatus(SocketConnectionStatus.disconnected);
_setConnectionState(WsConnectionState.disconnected);
}
Future<void> dispose() async {
@@ -101,7 +172,7 @@ class WebSocketService {
await _configController.close();
await _wifiController.close();
await _bleController.close();
await _connectionController.close();
await _connectionStateController.close();
}
void _handleMessage(dynamic data) {
@@ -136,13 +207,30 @@ class WebSocketService {
void _handleSocketClosed() {
_channel = null;
_subscription = null;
_setConnectionStatus(SocketConnectionStatus.disconnected);
if (_manualDisconnect) {
_retryCount = 0;
_nextReconnectDelay = _initialReconnectDelay;
_setConnectionState(WsConnectionState.disconnected);
return;
}
unawaited(reconnect());
}
void _setConnectionStatus(SocketConnectionStatus status) {
_connectionStatus = status;
_connectionController.add(status);
void _setConnectionState(WsConnectionState state) {
_connectionState = state;
_connectionStateController.add(state);
}
SocketConnectionStatus _toLegacyConnectionStatus(WsConnectionState state) {
switch (state) {
case WsConnectionState.connected:
return SocketConnectionStatus.connected;
case WsConnectionState.connecting:
return SocketConnectionStatus.connecting;
case WsConnectionState.disconnected:
return SocketConnectionStatus.disconnected;
}
}
String _normalizeDeviceIp(String raw) {
@@ -169,4 +257,11 @@ class WebSocketService {
return value;
}
int _normalizePort(int value) {
if (value <= 0 || value > 65535) {
return 5000;
}
return value;
}
}