chore: prepare release v1.1.4 (app store compliance, remote desktop fixes, ci verification)
This commit is contained in:
parent
fe1502520d
commit
0fdac8aedd
@ -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
|
||||
|
||||
|
||||
@ -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
33
lib/app/app_logger.dart
Normal 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);
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@ -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(
|
||||
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!);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -72,6 +72,6 @@ SPEC CHECKSUMS:
|
||||
super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189
|
||||
WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db
|
||||
|
||||
PODFILE CHECKSUM: ef2282d07ab509932defa9bc41c2af9516037afc
|
||||
PODFILE CHECKSUM: d6c0f271ccdc2e48bb44003eee71c5d884660a71
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}")
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user