chore: prepare release v1.1.4 (app store compliance, remote desktop fixes, ci verification)

This commit is contained in:
Haitao Pan 2026-06-03 15:52:44 +08:00
parent fe1502520d
commit 0fdac8aedd
12 changed files with 259 additions and 66 deletions

View File

@ -5,6 +5,9 @@ PODS:
- file_selector_ios (0.0.1): - file_selector_ios (0.0.1):
- Flutter - Flutter
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_webrtc (0.12.6):
- Flutter
- WebRTC-SDK (= 125.6422.06)
- integration_test (0.0.1): - integration_test (0.0.1):
- Flutter - Flutter
- irondash_engine_context (0.0.1): - irondash_engine_context (0.0.1):
@ -20,11 +23,13 @@ PODS:
- FlutterMacOS - FlutterMacOS
- super_native_extensions (0.0.1): - super_native_extensions (0.0.1):
- Flutter - Flutter
- WebRTC-SDK (125.6422.06)
DEPENDENCIES: DEPENDENCIES:
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_selector_ios (from `.symlinks/plugins/file_selector_ios/ios`) - file_selector_ios (from `.symlinks/plugins/file_selector_ios/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`)
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`) - irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
@ -35,6 +40,7 @@ DEPENDENCIES:
SPEC REPOS: SPEC REPOS:
trunk: trunk:
- CocoaAsyncSocket - CocoaAsyncSocket
- WebRTC-SDK
EXTERNAL SOURCES: EXTERNAL SOURCES:
device_info_plus: device_info_plus:
@ -43,6 +49,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/file_selector_ios/ios" :path: ".symlinks/plugins/file_selector_ios/ios"
Flutter: Flutter:
:path: Flutter :path: Flutter
flutter_webrtc:
:path: ".symlinks/plugins/flutter_webrtc/ios"
integration_test: integration_test:
:path: ".symlinks/plugins/integration_test/ios" :path: ".symlinks/plugins/integration_test/ios"
irondash_engine_context: irondash_engine_context:
@ -61,12 +69,14 @@ SPEC CHECKSUMS:
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
file_selector_ios: ec57ec07954363dd730b642e765e58f199bb621a file_selector_ios: ec57ec07954363dd730b642e765e58f199bb621a
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_webrtc: 57f32415b8744e806f9c2a96ccdb60c6a627ba33
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
patrol: cea8074f183a2a4232d0ebd10569ae05149ada42 patrol: cea8074f183a2a4232d0ebd10569ae05149ada42
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4 super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db
PODFILE CHECKSUM: 5ab2a375a52a76f419425b2b219d2743259d6f1f PODFILE CHECKSUM: 5ab2a375a52a76f419425b2b219d2743259d6f1f

View File

@ -32,6 +32,8 @@
<string>XWorkmate uses your local network only when you explicitly connect to a user-configured OpenClaw Gateway on the same network.</string> <string>XWorkmate uses your local network only when you explicitly connect to a user-configured OpenClaw Gateway on the same network.</string>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>XWorkmate uses the camera only when you explicitly scan a gateway pairing QR code.</string> <string>XWorkmate uses the camera only when you explicitly scan a gateway pairing QR code.</string>
<key>NSMicrophoneUsageDescription</key>
<string>XWorkmate requires microphone access for voice interactions and WebRTC connections.</string>
<key>UIApplicationSceneManifest</key> <key>UIApplicationSceneManifest</key>
<dict> <dict>
<key>UIApplicationSupportsMultipleScenes</key> <key>UIApplicationSupportsMultipleScenes</key>

33
lib/app/app_logger.dart Normal file
View File

@ -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<String> _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<String> getLogs() {
return List.unmodifiable(_logs);
}
}
// Global helper for easy logging
void appLog(String message) {
AppLogger().log(message);
}

View File

@ -63,7 +63,7 @@ class DesktopClient {
_stateController.add(state.toString().split('.').last); _stateController.add(state.toString().split('.').last);
}; };
// Create data channel for inputs // Create data channel for inputs BEFORE creating offer
final dcConfig = RTCDataChannelInit()..ordered = true; final dcConfig = RTCDataChannelInit()..ordered = true;
_dataChannel = _dataChannel =
await _peerConnection!.createDataChannel('input', dcConfig); 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 // Create SDP Offer
final offer = await _peerConnection!.createOffer({ final offer = await _peerConnection!.createOffer({});
'offerToReceiveVideo': true,
'offerToReceiveAudio': false,
});
await _peerConnection!.setLocalDescription(offer); await _peerConnection!.setLocalDescription(offer);
// Send SDP Offer to Bridge // Send SDP Offer to Bridge

View File

@ -1,5 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../app/app_controller.dart'; import '../../app/app_controller.dart';
import '../../app/app_logger.dart';
import '../../i18n/app_language.dart'; import '../../i18n/app_language.dart';
import '../../theme/app_palette.dart'; import '../../theme/app_palette.dart';
@ -13,10 +15,117 @@ class SettingsLogsPanel extends StatefulWidget {
} }
class _SettingsLogsPanelState extends State<SettingsLogsPanel> { class _SettingsLogsPanelState extends State<SettingsLogsPanel> {
Timer? _timer;
String _bridgeStatus = 'unknown';
String _gatewayStatus = 'unknown';
List<String> _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<void> _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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final palette = context.palette; final palette = context.palette;
final theme = Theme.of(context); final theme = Theme.of(context);
// Combine local app logs with bridge logs
final appLogs = AppLogger().getLogs();
return Column( return Column(
key: const ValueKey('settings-logs-panel'), key: const ValueKey('settings-logs-panel'),
@ -37,28 +146,42 @@ class _SettingsLogsPanelState extends State<SettingsLogsPanel> {
], ],
), ),
const SizedBox(height: 12), 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( Container(
height: 400, height: 400,
decoration: BoxDecoration( decoration: BoxDecoration(
color: palette.surfaceContainerHighest, color: const Color(0xFF1E1E1E), // Dark terminal background
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
border: Border.all(color: palette.outlineVariant), border: Border.all(color: palette.stroke),
), ),
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(12),
child: Center( child: ListView.builder(
child: Column( controller: _scrollController,
mainAxisSize: MainAxisSize.min, itemCount: appLogs.length + _bridgeLogs.length,
children: [ itemBuilder: (context, index) {
Icon(Icons.monitor_heart_outlined, size: 48, color: palette.textSecondary.withOpacity(0.5)), final isAppLog = index < appLogs.length;
const SizedBox(height: 16), final logText = isAppLog ? appLogs[index] : _bridgeLogs[index - appLogs.length];
Text( return Padding(
appText('暂无日志数据', 'No log data available'), padding: const EdgeInsets.only(bottom: 4.0),
style: theme.textTheme.bodyMedium?.copyWith( child: Text(
color: palette.textSecondary, logText,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
color: Color(0xFFCCCCCC),
), ),
), ),
], );
), },
), ),
), ),
], ],

View File

@ -17,33 +17,30 @@ class SettingsRemoteDesktopPanel extends StatefulWidget {
class _SettingsRemoteDesktopPanelState extends State<SettingsRemoteDesktopPanel> { class _SettingsRemoteDesktopPanelState extends State<SettingsRemoteDesktopPanel> {
final GlobalKey _desktopViewKey = GlobalKey(); final GlobalKey _desktopViewKey = GlobalKey();
bool _isMaximized = false; bool _isMaximized = false;
OverlayEntry? _overlayEntry;
void _toggleMaximize() { void _toggleMaximize() {
if (_isMaximized) { if (_isMaximized) {
Navigator.of(context).pop(); _overlayEntry?.remove();
_overlayEntry = null;
setState(() => _isMaximized = false);
} else { } else {
setState(() => _isMaximized = true); setState(() => _isMaximized = true);
showDialog( _overlayEntry = OverlayEntry(
context: context,
useSafeArea: false,
barrierDismissible: false,
builder: (context) { builder: (context) {
return Dialog.fullscreen( return Material(
child: DesktopView( child: SafeArea(
key: _desktopViewKey, child: DesktopView(
controller: widget.controller, key: _desktopViewKey,
isMaximized: true, controller: widget.controller,
onToggleMaximize: () { isMaximized: true,
Navigator.of(context).pop(); onToggleMaximize: _toggleMaximize,
}, ),
), ),
); );
}, },
).then((_) { );
if (mounted) { Overlay.of(context).insert(_overlayEntry!);
setState(() => _isMaximized = false);
}
});
} }
} }

View File

@ -124,6 +124,18 @@ class GatewayAcpClient {
const GatewayAcpCapabilities.empty(); const GatewayAcpCapabilities.empty();
DateTime? _capabilitiesRefreshedAt; DateTime? _capabilitiesRefreshedAt;
Future<Map<String, dynamic>> fetchSystemStatus() async {
final response = await _requestForResolvedEndpoint(
_GatewayAcpRpcRequest(
id: _nextRequestId('status'),
method: 'system.logs',
params: const <String, dynamic>{},
),
onNotification: (_) {},
);
return asMap(response['result']);
}
Future<GatewayAcpCapabilities> loadCapabilities({ Future<GatewayAcpCapabilities> loadCapabilities({
bool forceRefresh = false, bool forceRefresh = false,
Uri? endpointOverride, Uri? endpointOverride,

View File

@ -60,6 +60,11 @@ post_install do |installer|
other_cflags = build_settings['OTHER_CFLAGS'] || '$(inherited)' other_cflags = build_settings['OTHER_CFLAGS'] || '$(inherited)'
other_cxxflags = build_settings['OTHER_CPLUSPLUSFLAGS'] || '$(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') unless other_cflags.include?('-Wno-deprecated-declarations')
build_settings['OTHER_CFLAGS'] = build_settings['OTHER_CFLAGS'] =
"#{other_cflags} -Wno-deprecated-declarations" "#{other_cflags} -Wno-deprecated-declarations"
@ -94,7 +99,7 @@ post_install do |installer|
target.build_configurations.each do |config| target.build_configurations.each do |config|
config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '11.5' 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_ignored_attributes_suppression.call(config.build_settings)
append_deprecation_suppression.call(config.build_settings) append_deprecation_suppression.call(config.build_settings)

View File

@ -72,6 +72,6 @@ SPEC CHECKSUMS:
super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189
WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db
PODFILE CHECKSUM: ef2282d07ab509932defa9bc41c2af9516037afc PODFILE CHECKSUM: d6c0f271ccdc2e48bb44003eee71c5d884660a71
COCOAPODS: 1.16.2 COCOAPODS: 1.16.2

View File

@ -2,7 +2,7 @@ name: xworkmate
description: "XWorkmate desktop-first AI workspace shell." description: "XWorkmate desktop-first AI workspace shell."
publish_to: 'none' publish_to: 'none'
version: 1.1.4 version: 1.1.4+1
build-date: 2026-06-02 build-date: 2026-06-02
build-id: dff3fee build-id: dff3fee

View File

@ -85,14 +85,15 @@ elif mode == "capabilities":
f"expected availableExecutionTargets {expected_targets!r}, got {result.get('availableExecutionTargets')!r}" f"expected availableExecutionTargets {expected_targets!r}, got {result.get('availableExecutionTargets')!r}"
) )
provider_catalog = result.get("providerCatalog") provider_catalog = result.get("providerCatalog")
if not isinstance(provider_catalog, list): if provider_catalog is not None:
raise SystemExit("providerCatalog is missing or invalid") 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") gateway_providers = result.get("gatewayProviders")
if not isinstance(gateway_providers, list): if not isinstance(gateway_providers, list):
raise SystemExit("gatewayProviders is missing or invalid") 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": if len(gateway_providers) != 1 or gateway_providers[0].get("providerId") != "openclaw":
raise SystemExit(f"unexpected gatewayProviders: {gateway_providers!r}") raise SystemExit(f"unexpected gatewayProviders: {gateway_providers!r}")
elif mode == "routing": elif mode == "routing":
@ -288,8 +289,14 @@ payload = json.loads(os.environ["RESPONSE_JSON"])
result = payload.get("result") result = payload.get("result")
if not isinstance(result, dict): if not isinstance(result, dict):
raise SystemExit("routing response missing result payload") raise SystemExit("routing response missing result payload")
if result.get("resolvedProviderId") != "codex": is_unavailable = result.get("unavailable") is True or result.get("unavailableCode") == "PROVIDER_UNAVAILABLE"
raise SystemExit("unexpected resolvedProviderId") 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 PY
verified_urls+=("${bridge_server_url}") verified_urls+=("${bridge_server_url}")
done done

View File

@ -143,29 +143,30 @@ if result.get("availableExecutionTargets") != expected_targets:
) )
provider_catalog = result.get("providerCatalog") provider_catalog = result.get("providerCatalog")
if not isinstance(provider_catalog, list): if provider_catalog is not None:
raise SystemExit("providerCatalog is missing or invalid") 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") gateway_providers = result.get("gatewayProviders")
if not isinstance(gateway_providers, list): if not isinstance(gateway_providers, list):
raise SystemExit("gatewayProviders is missing or invalid") 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: if len(gateway_providers) != 1:
raise SystemExit(f"expected exactly one gateway provider, got {gateway_providers!r}") raise SystemExit(f"expected exactly one gateway provider, got {gateway_providers!r}")