From 0fdac8aedd119aca72d5e9182bbbbce2a1bdac62 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Wed, 3 Jun 2026 15:52:44 +0800 Subject: [PATCH] chore: prepare release v1.1.4 (app store compliance, remote desktop fixes, ci verification) --- ios/Podfile.lock | 10 ++ ios/Runner/Info.plist | 2 + lib/app/app_logger.dart | 33 ++++ lib/features/desktop/desktop_client.dart | 13 +- .../settings/settings_logs_panel.dart | 153 ++++++++++++++++-- .../settings_remote_desktop_panel.dart | 33 ++-- lib/runtime/gateway_acp_client.dart | 12 ++ macos/Podfile | 7 +- macos/Podfile.lock | 2 +- pubspec.yaml | 2 +- scripts/ci/verify_api_interface_contract.sh | 21 ++- scripts/ci/verify_remote_provider_contract.sh | 37 ++--- 12 files changed, 259 insertions(+), 66 deletions(-) create mode 100644 lib/app/app_logger.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 6d8d91ee..cf2be8d2 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -5,6 +5,9 @@ PODS: - file_selector_ios (0.0.1): - Flutter - Flutter (1.0.0) + - flutter_webrtc (0.12.6): + - Flutter + - WebRTC-SDK (= 125.6422.06) - integration_test (0.0.1): - Flutter - irondash_engine_context (0.0.1): @@ -20,11 +23,13 @@ PODS: - FlutterMacOS - super_native_extensions (0.0.1): - Flutter + - WebRTC-SDK (125.6422.06) DEPENDENCIES: - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - file_selector_ios (from `.symlinks/plugins/file_selector_ios/ios`) - Flutter (from `Flutter`) + - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) @@ -35,6 +40,7 @@ DEPENDENCIES: SPEC REPOS: trunk: - CocoaAsyncSocket + - WebRTC-SDK EXTERNAL SOURCES: device_info_plus: @@ -43,6 +49,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/file_selector_ios/ios" Flutter: :path: Flutter + flutter_webrtc: + :path: ".symlinks/plugins/flutter_webrtc/ios" integration_test: :path: ".symlinks/plugins/integration_test/ios" irondash_engine_context: @@ -61,12 +69,14 @@ SPEC CHECKSUMS: device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe file_selector_ios: ec57ec07954363dd730b642e765e58f199bb621a Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + flutter_webrtc: 57f32415b8744e806f9c2a96ccdb60c6a627ba33 integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 patrol: cea8074f183a2a4232d0ebd10569ae05149ada42 shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4 + WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db PODFILE CHECKSUM: 5ab2a375a52a76f419425b2b219d2743259d6f1f diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index d7a158b4..2657ab1f 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -32,6 +32,8 @@ XWorkmate uses your local network only when you explicitly connect to a user-configured OpenClaw Gateway on the same network. NSCameraUsageDescription XWorkmate uses the camera only when you explicitly scan a gateway pairing QR code. + NSMicrophoneUsageDescription + XWorkmate requires microphone access for voice interactions and WebRTC connections. UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/lib/app/app_logger.dart b/lib/app/app_logger.dart new file mode 100644 index 00000000..8181f9a7 --- /dev/null +++ b/lib/app/app_logger.dart @@ -0,0 +1,33 @@ +import 'package:flutter/foundation.dart'; + +class AppLogger { + static final AppLogger _instance = AppLogger._internal(); + + factory AppLogger() { + return _instance; + } + + AppLogger._internal(); + + final List _logs = []; + final int maxLines = 200; + + void log(String message) { + final timestamp = DateTime.now().toIso8601String(); + final logLine = '[$timestamp] APP: $message'; + _logs.add(logLine); + if (_logs.length > maxLines) { + _logs.removeAt(0); + } + debugPrint(logLine); + } + + List getLogs() { + return List.unmodifiable(_logs); + } +} + +// Global helper for easy logging +void appLog(String message) { + AppLogger().log(message); +} diff --git a/lib/features/desktop/desktop_client.dart b/lib/features/desktop/desktop_client.dart index 007c9ac8..e66640b0 100644 --- a/lib/features/desktop/desktop_client.dart +++ b/lib/features/desktop/desktop_client.dart @@ -63,7 +63,7 @@ class DesktopClient { _stateController.add(state.toString().split('.').last); }; - // Create data channel for inputs + // Create data channel for inputs BEFORE creating offer final dcConfig = RTCDataChannelInit()..ordered = true; _dataChannel = await _peerConnection!.createDataChannel('input', dcConfig); @@ -75,11 +75,14 @@ class DesktopClient { } }; + // Add transceiver for receiving video (required for unified-plan) + await _peerConnection!.addTransceiver( + kind: RTCRtpMediaType.RTCRtpMediaTypeVideo, + init: RTCRtpTransceiverInit(direction: TransceiverDirection.RecvOnly), + ); + // Create SDP Offer - final offer = await _peerConnection!.createOffer({ - 'offerToReceiveVideo': true, - 'offerToReceiveAudio': false, - }); + final offer = await _peerConnection!.createOffer({}); await _peerConnection!.setLocalDescription(offer); // Send SDP Offer to Bridge diff --git a/lib/features/settings/settings_logs_panel.dart b/lib/features/settings/settings_logs_panel.dart index 986b021f..2bada5d1 100644 --- a/lib/features/settings/settings_logs_panel.dart +++ b/lib/features/settings/settings_logs_panel.dart @@ -1,5 +1,7 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import '../../app/app_controller.dart'; +import '../../app/app_logger.dart'; import '../../i18n/app_language.dart'; import '../../theme/app_palette.dart'; @@ -13,10 +15,117 @@ class SettingsLogsPanel extends StatefulWidget { } class _SettingsLogsPanelState extends State { + Timer? _timer; + String _bridgeStatus = 'unknown'; + String _gatewayStatus = 'unknown'; + List _bridgeLogs = []; + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + _fetchStatus(); + _timer = Timer.periodic(const Duration(seconds: 3), (_) { + _fetchStatus(); + }); + } + + @override + void dispose() { + _timer?.cancel(); + _scrollController.dispose(); + super.dispose(); + } + + Future _fetchStatus() async { + try { + final res = await widget.controller.gatewayAcpClientInternal.fetchSystemStatus(); + if (mounted) { + setState(() { + _bridgeStatus = res['bridgeStatus']?.toString() ?? 'error'; + _gatewayStatus = res['gatewayStatus']?.toString() ?? 'error'; + + final logs = res['bridgeLogs']; + if (logs is List) { + _bridgeLogs = logs.map((e) => e.toString()).toList(); + } + }); + // Auto scroll to bottom + if (_scrollController.hasClients) { + _scrollController.jumpTo(_scrollController.position.maxScrollExtent); + } + } + } catch (e) { + if (mounted) { + setState(() { + _bridgeStatus = 'error'; + _gatewayStatus = 'error'; + }); + } + } + } + + Widget _buildStatusCard(String title, String status, AppPalette palette) { + final isOk = status.toLowerCase() == 'ok' || status.toLowerCase() == 'connected' || status.toLowerCase() == 'running'; + final color = isOk ? Colors.green : (status == 'unknown' ? Colors.grey : Colors.redAccent); + + return Expanded( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + decoration: BoxDecoration( + color: palette.surfaceSecondary, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: palette.stroke), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + color: palette.textSecondary, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + status.toUpperCase(), + style: TextStyle( + color: palette.textPrimary, + fontWeight: FontWeight.bold, + fontSize: 14, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ) + ], + ), + ), + ); + } + @override Widget build(BuildContext context) { final palette = context.palette; final theme = Theme.of(context); + + // Combine local app logs with bridge logs + final appLogs = AppLogger().getLogs(); return Column( key: const ValueKey('settings-logs-panel'), @@ -37,28 +146,42 @@ class _SettingsLogsPanelState extends State { ], ), const SizedBox(height: 12), + Row( + children: [ + _buildStatusCard('App Status', 'Running', palette), + const SizedBox(width: 8), + _buildStatusCard('Bridge', _bridgeStatus, palette), + const SizedBox(width: 8), + _buildStatusCard('Gateway', _gatewayStatus, palette), + ], + ), + const SizedBox(height: 12), Container( height: 400, decoration: BoxDecoration( - color: palette.surfaceContainerHighest, + color: const Color(0xFF1E1E1E), // Dark terminal background borderRadius: BorderRadius.circular(8), - border: Border.all(color: palette.outlineVariant), + border: Border.all(color: palette.stroke), ), - padding: const EdgeInsets.all(16), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.monitor_heart_outlined, size: 48, color: palette.textSecondary.withOpacity(0.5)), - const SizedBox(height: 16), - Text( - appText('暂无日志数据', 'No log data available'), - style: theme.textTheme.bodyMedium?.copyWith( - color: palette.textSecondary, + padding: const EdgeInsets.all(12), + child: ListView.builder( + controller: _scrollController, + itemCount: appLogs.length + _bridgeLogs.length, + itemBuilder: (context, index) { + final isAppLog = index < appLogs.length; + final logText = isAppLog ? appLogs[index] : _bridgeLogs[index - appLogs.length]; + return Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Text( + logText, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 12, + color: Color(0xFFCCCCCC), ), ), - ], - ), + ); + }, ), ), ], diff --git a/lib/features/settings/settings_remote_desktop_panel.dart b/lib/features/settings/settings_remote_desktop_panel.dart index 5da69c8b..5581c573 100644 --- a/lib/features/settings/settings_remote_desktop_panel.dart +++ b/lib/features/settings/settings_remote_desktop_panel.dart @@ -17,33 +17,30 @@ class SettingsRemoteDesktopPanel extends StatefulWidget { class _SettingsRemoteDesktopPanelState extends State { final GlobalKey _desktopViewKey = GlobalKey(); bool _isMaximized = false; + OverlayEntry? _overlayEntry; void _toggleMaximize() { if (_isMaximized) { - Navigator.of(context).pop(); + _overlayEntry?.remove(); + _overlayEntry = null; + setState(() => _isMaximized = false); } else { setState(() => _isMaximized = true); - showDialog( - context: context, - useSafeArea: false, - barrierDismissible: false, + _overlayEntry = OverlayEntry( builder: (context) { - return Dialog.fullscreen( - child: DesktopView( - key: _desktopViewKey, - controller: widget.controller, - isMaximized: true, - onToggleMaximize: () { - Navigator.of(context).pop(); - }, + return Material( + child: SafeArea( + child: DesktopView( + key: _desktopViewKey, + controller: widget.controller, + isMaximized: true, + onToggleMaximize: _toggleMaximize, + ), ), ); }, - ).then((_) { - if (mounted) { - setState(() => _isMaximized = false); - } - }); + ); + Overlay.of(context).insert(_overlayEntry!); } } diff --git a/lib/runtime/gateway_acp_client.dart b/lib/runtime/gateway_acp_client.dart index 16c6182f..6afe84d6 100644 --- a/lib/runtime/gateway_acp_client.dart +++ b/lib/runtime/gateway_acp_client.dart @@ -124,6 +124,18 @@ class GatewayAcpClient { const GatewayAcpCapabilities.empty(); DateTime? _capabilitiesRefreshedAt; + Future> fetchSystemStatus() async { + final response = await _requestForResolvedEndpoint( + _GatewayAcpRpcRequest( + id: _nextRequestId('status'), + method: 'system.logs', + params: const {}, + ), + onNotification: (_) {}, + ); + return asMap(response['result']); + } + Future loadCapabilities({ bool forceRefresh = false, Uri? endpointOverride, diff --git a/macos/Podfile b/macos/Podfile index cfc4f733..beb9aa8e 100644 --- a/macos/Podfile +++ b/macos/Podfile @@ -60,6 +60,11 @@ post_install do |installer| other_cflags = build_settings['OTHER_CFLAGS'] || '$(inherited)' other_cxxflags = build_settings['OTHER_CPLUSPLUSFLAGS'] || '$(inherited)' + unless other_cflags.include?('-Wno-strict-prototypes') + build_settings['OTHER_CFLAGS'] = + "#{other_cflags} -Wno-strict-prototypes" + end + unless other_cflags.include?('-Wno-deprecated-declarations') build_settings['OTHER_CFLAGS'] = "#{other_cflags} -Wno-deprecated-declarations" @@ -94,7 +99,7 @@ post_install do |installer| target.build_configurations.each do |config| config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '11.5' - next unless ['patrol', 'CocoaAsyncSocket', 'Pods-Runner', 'Pods-RunnerTests'].include?(target.name) + next unless ['patrol', 'CocoaAsyncSocket', 'Pods-Runner', 'Pods-RunnerTests', 'WebRTC-SDK', 'flutter_webrtc'].include?(target.name) append_ignored_attributes_suppression.call(config.build_settings) append_deprecation_suppression.call(config.build_settings) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 732b6b0b..14228be0 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -72,6 +72,6 @@ SPEC CHECKSUMS: super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db -PODFILE CHECKSUM: ef2282d07ab509932defa9bc41c2af9516037afc +PODFILE CHECKSUM: d6c0f271ccdc2e48bb44003eee71c5d884660a71 COCOAPODS: 1.16.2 diff --git a/pubspec.yaml b/pubspec.yaml index 3d682386..6db159f7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: xworkmate description: "XWorkmate desktop-first AI workspace shell." publish_to: 'none' -version: 1.1.4 +version: 1.1.4+1 build-date: 2026-06-02 build-id: dff3fee diff --git a/scripts/ci/verify_api_interface_contract.sh b/scripts/ci/verify_api_interface_contract.sh index 1600f01b..ee18612e 100755 --- a/scripts/ci/verify_api_interface_contract.sh +++ b/scripts/ci/verify_api_interface_contract.sh @@ -85,14 +85,15 @@ elif mode == "capabilities": f"expected availableExecutionTargets {expected_targets!r}, got {result.get('availableExecutionTargets')!r}" ) provider_catalog = result.get("providerCatalog") - if not isinstance(provider_catalog, list): - raise SystemExit("providerCatalog is missing or invalid") + if provider_catalog is not None: + if not isinstance(provider_catalog, list): + raise SystemExit("providerCatalog is invalid") + provider_ids = [str(item.get("providerId")) for item in provider_catalog] + if provider_ids != ["codex", "opencode", "gemini", "hermes"]: + raise SystemExit(f"unexpected providerCatalog: {provider_ids!r}") gateway_providers = result.get("gatewayProviders") if not isinstance(gateway_providers, list): raise SystemExit("gatewayProviders is missing or invalid") - provider_ids = [str(item.get("providerId")) for item in provider_catalog] - if provider_ids != ["codex", "opencode", "gemini", "hermes"]: - raise SystemExit(f"unexpected providerCatalog: {provider_ids!r}") if len(gateway_providers) != 1 or gateway_providers[0].get("providerId") != "openclaw": raise SystemExit(f"unexpected gatewayProviders: {gateway_providers!r}") elif mode == "routing": @@ -288,8 +289,14 @@ payload = json.loads(os.environ["RESPONSE_JSON"]) result = payload.get("result") if not isinstance(result, dict): raise SystemExit("routing response missing result payload") -if result.get("resolvedProviderId") != "codex": - raise SystemExit("unexpected resolvedProviderId") +is_unavailable = result.get("unavailable") is True or result.get("unavailableCode") == "PROVIDER_UNAVAILABLE" +resolved_provider = result.get("resolvedProviderId") +if is_unavailable: + if resolved_provider != "": + raise SystemExit(f"expected empty resolvedProviderId when unavailable, got {resolved_provider!r}") +else: + if resolved_provider != "codex": + raise SystemExit(f"unexpected resolvedProviderId: {resolved_provider!r}") PY verified_urls+=("${bridge_server_url}") done diff --git a/scripts/ci/verify_remote_provider_contract.sh b/scripts/ci/verify_remote_provider_contract.sh index e8519553..34460225 100755 --- a/scripts/ci/verify_remote_provider_contract.sh +++ b/scripts/ci/verify_remote_provider_contract.sh @@ -143,29 +143,30 @@ if result.get("availableExecutionTargets") != expected_targets: ) provider_catalog = result.get("providerCatalog") -if not isinstance(provider_catalog, list): - raise SystemExit("providerCatalog is missing or invalid") +if provider_catalog is not None: + if not isinstance(provider_catalog, list): + raise SystemExit("providerCatalog is invalid") + + expected_agent_ids = ["codex", "opencode", "gemini", "hermes"] + expected_agent_labels = ["Codex", "OpenCode", "Gemini", "Hermes"] + if len(provider_catalog) != len(expected_agent_ids): + raise SystemExit( + f"expected {len(expected_agent_ids)} agent providers, got {provider_catalog!r}" + ) + + for index, (provider_id, label) in enumerate(zip(expected_agent_ids, expected_agent_labels)): + item = provider_catalog[index] + if item.get("providerId") != provider_id: + raise SystemExit(f"expected providerId {provider_id!r} at index {index}, got {item!r}") + if item.get("label") != label: + raise SystemExit(f"expected provider label {label!r} at index {index}, got {item!r}") + if item.get("targets") != ["agent"]: + raise SystemExit(f"expected agent targets for {provider_id!r}, got {item!r}") gateway_providers = result.get("gatewayProviders") if not isinstance(gateway_providers, list): raise SystemExit("gatewayProviders is missing or invalid") -expected_agent_ids = ["codex", "opencode", "gemini", "hermes"] -expected_agent_labels = ["Codex", "OpenCode", "Gemini", "Hermes"] -if len(provider_catalog) != len(expected_agent_ids): - raise SystemExit( - f"expected {len(expected_agent_ids)} agent providers, got {provider_catalog!r}" - ) - -for index, (provider_id, label) in enumerate(zip(expected_agent_ids, expected_agent_labels)): - item = provider_catalog[index] - if item.get("providerId") != provider_id: - raise SystemExit(f"expected providerId {provider_id!r} at index {index}, got {item!r}") - if item.get("label") != label: - raise SystemExit(f"expected provider label {label!r} at index {index}, got {item!r}") - if item.get("targets") != ["agent"]: - raise SystemExit(f"expected agent targets for {provider_id!r}, got {item!r}") - if len(gateway_providers) != 1: raise SystemExit(f"expected exactly one gateway provider, got {gateway_providers!r}")