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):
- 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

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>
<key>NSCameraUsageDescription</key>
<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>
<dict>
<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);
};
// 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

View File

@ -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,11 +15,118 @@ class SettingsLogsPanel extends StatefulWidget {
}
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
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'),
crossAxisAlignment: CrossAxisAlignment.stretch,
@ -37,28 +146,42 @@ class _SettingsLogsPanelState extends State<SettingsLogsPanel> {
],
),
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),
),
),
],
),
);
},
),
),
],

View File

@ -17,33 +17,30 @@ class SettingsRemoteDesktopPanel extends StatefulWidget {
class _SettingsRemoteDesktopPanelState extends State<SettingsRemoteDesktopPanel> {
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(
return Material(
child: SafeArea(
child: DesktopView(
key: _desktopViewKey,
controller: widget.controller,
isMaximized: true,
onToggleMaximize: () {
Navigator.of(context).pop();
},
onToggleMaximize: _toggleMaximize,
),
),
);
},
).then((_) {
if (mounted) {
setState(() => _isMaximized = false);
}
});
);
Overlay.of(context).insert(_overlayEntry!);
}
}

View File

@ -124,6 +124,18 @@ class GatewayAcpClient {
const GatewayAcpCapabilities.empty();
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({
bool forceRefresh = false,
Uri? endpointOverride,

View File

@ -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)

View File

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

View File

@ -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

View File

@ -85,14 +85,15 @@ elif mode == "capabilities":
f"expected availableExecutionTargets {expected_targets!r}, got {result.get('availableExecutionTargets')!r}"
)
provider_catalog = result.get("providerCatalog")
if provider_catalog is not None:
if not isinstance(provider_catalog, list):
raise SystemExit("providerCatalog is missing or invalid")
gateway_providers = result.get("gatewayProviders")
if not isinstance(gateway_providers, list):
raise SystemExit("gatewayProviders is missing or invalid")
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")
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

View File

@ -143,12 +143,9 @@ if result.get("availableExecutionTargets") != expected_targets:
)
provider_catalog = result.get("providerCatalog")
if provider_catalog is not None:
if not isinstance(provider_catalog, list):
raise SystemExit("providerCatalog is missing or invalid")
gateway_providers = result.get("gatewayProviders")
if not isinstance(gateway_providers, list):
raise SystemExit("gatewayProviders is missing or invalid")
raise SystemExit("providerCatalog is invalid")
expected_agent_ids = ["codex", "opencode", "gemini", "hermes"]
expected_agent_labels = ["Codex", "OpenCode", "Gemini", "Hermes"]
@ -166,6 +163,10 @@ for index, (provider_id, label) in enumerate(zip(expected_agent_ids, expected_ag
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")
if len(gateway_providers) != 1:
raise SystemExit(f"expected exactly one gateway provider, got {gateway_providers!r}")