From d08bc29cebc2f3cfcd32e83bb3069963140ab631 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Wed, 3 Jun 2026 10:50:06 +0800 Subject: [PATCH] feat: Remote Desktop UI and Client WebRTC Integration --- config/feature_flags.yaml | 12 + .../README.md | 1 + .../ai-security-evolution-pdf.md | 65 +++ .../ai-security-evolution-pdf.pdf | Bin 0 -> 103385 bytes ..._controller_desktop_skill_permissions.dart | 3 + ...app_controller_desktop_thread_actions.dart | 94 +++- lib/app/ui_feature_manifest_core.dart | 2 + .../assistant_attachment_payloads.dart | 3 + lib/features/desktop/desktop_client.dart | 158 ++++++ .../desktop/desktop_input_handler.dart | 94 ++++ lib/features/desktop/desktop_view.dart | 458 ++++++++++++++++++ lib/features/settings/settings_page_core.dart | 17 +- .../settings_remote_desktop_panel.dart | 49 ++ lib/models/app_models.dart | 3 +- lib/runtime/go_task_service_client.dart | 20 + .../runtime_models_gateway_entities.dart | 3 + .../runtime_models_runtime_payloads.dart | 82 ++++ linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 40 ++ pubspec.yaml | 1 + .../settings_remote_desktop_panel_test.dart | 115 +++++ .../assistant_execution_target_test.dart | 54 +++ .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 26 files changed, 1278 insertions(+), 7 deletions(-) create mode 100644 docs/cases/ai-security-evolution-content-scenario/ai-security-evolution-pdf.md create mode 100644 docs/cases/ai-security-evolution-content-scenario/ai-security-evolution-pdf.pdf create mode 100644 lib/features/desktop/desktop_client.dart create mode 100644 lib/features/desktop/desktop_input_handler.dart create mode 100644 lib/features/desktop/desktop_view.dart create mode 100644 lib/features/settings/settings_remote_desktop_panel.dart create mode 100644 test/features/settings/settings_remote_desktop_panel_test.dart diff --git a/config/feature_flags.yaml b/config/feature_flags.yaml index 558cbc25..6bbececa 100644 --- a/config/feature_flags.yaml +++ b/config/feature_flags.yaml @@ -67,6 +67,12 @@ mobile: build_modes: [debug, profile, release] description: Mobile archived task management ui_surface: settings_page + remote_desktop: + enabled: true + release_tier: stable + build_modes: [debug, profile, release] + description: Mobile WebRTC Remote Desktop view + ui_surface: settings_page account_access: enabled: true release_tier: stable @@ -162,6 +168,12 @@ desktop: build_modes: [debug, profile, release] description: Desktop archived task management ui_surface: settings_page + remote_desktop: + enabled: true + release_tier: stable + build_modes: [debug, profile, release] + description: Desktop WebRTC Remote Desktop view + ui_surface: settings_page account_access: enabled: true release_tier: stable diff --git a/docs/cases/ai-security-evolution-content-scenario/README.md b/docs/cases/ai-security-evolution-content-scenario/README.md index 787332ce..d7dbf30d 100644 --- a/docs/cases/ai-security-evolution-content-scenario/README.md +++ b/docs/cases/ai-security-evolution-content-scenario/README.md @@ -20,6 +20,7 @@ - [XHS 风格文案](./xhs-copy.md) - [微信文章文案](./wechat-article.md) - [PPT 演示稿](./ai-security-evolution-scenario.pptx) +- [章节拆分 PDF 文稿](./ai-security-evolution-pdf.pdf) ## App 手动测试提示词 diff --git a/docs/cases/ai-security-evolution-content-scenario/ai-security-evolution-pdf.md b/docs/cases/ai-security-evolution-content-scenario/ai-security-evolution-pdf.md new file mode 100644 index 00000000..48693fa5 --- /dev/null +++ b/docs/cases/ai-security-evolution-content-scenario/ai-security-evolution-pdf.md @@ -0,0 +1,65 @@ +# 从单机权限到 AI 模型与知识保护 + +## 内容生产流程 + +拆章节 -> 每章调用 Codex 生成正文 -> 每章调用 GPT Images2 生成配图 -> 汇总排版 -> 输出 PDF。 + +## 1. 单机权限:安全从“这台机器归谁管”开始 + +早期安全的核心问题很朴素:谁能登录这台机器,谁能读写本地文件,谁能安装程序、修改配置、提升权限。管理员、普通用户、服务账号构成了最基本的权限边界。这个阶段的重点是账号、密码、本地组、文件 ACL 和系统审计。 + +但单机权限并没有消失。今天的终端仍然是身份、数据和任务链路的入口。开发者电脑、运维跳板机、员工笔记本一旦失守,攻击者就可能拿到浏览器会话、SSH Key、云凭证和内部系统访问能力。 + +图像提示:一台现代工作站,屏幕上叠加本地账号、文件权限、SSH Key 和浏览器会话的安全边界,写实科技风,清晰信息图。 + +## 2. 网络边界:从机房防火墙到组合攻击面 + +企业上网、服务器入站、内外网隔离、防火墙策略,曾经定义了很长一段时间的安全主战场。只要守住入口,默认内部网络可信,这是传统网络安全的基本假设。 + +云、移动办公、SaaS 和 API 普及后,边界被拆散了。今天的网络边界不只是 IP 和端口,而是 API、设备、身份、供应链、运行时和第三方集成的组合面。攻击也不再只从“打穿防火墙”开始,而可能从一个泄露 Token、一个开放对象存储桶、一个未校验 Webhook 或一个依赖包开始。 + +图像提示:传统防火墙边界逐渐碎裂,变成 API、设备、身份、供应链、运行时节点组成的网络拓扑,冷静企业安全风格。 + +## 3. Web 安全:页面漏洞扩展为业务流风险 + +Web 安全曾经最容易被理解为漏洞清单:SQL 注入、XSS、CSRF、文件上传、越权访问。它们仍然重要,但真正的风险已经扩展到业务流本身。攻击者更关心如何绕过审批、滥用接口、批量枚举、接管会话、自动化薅取权益,或者把插件和第三方脚本变成数据外泄通道。 + +所以 Web 安全不能只停留在扫描器和 WAF。它需要和身份、风控、审计、数据分级、API 治理一起工作。尤其当 AI Agent 可以自动浏览网页、调用后台接口、读写文件时,Web 不再只是用户界面,而是 Agent 执行动作的工作台。 + +图像提示:浏览器窗口中展示登录、API、插件、自动化脚本和数据流,旁边标注业务流风险,专业信息图,白底高对比。 + +## 4. 云身份:默认控制平面 + +进入云时代后,身份成为事实上的控制平面。谁能 Assume Role,谁能创建密钥,谁能访问对象存储,谁能改安全组,谁能跨账号部署资源,往往比某台服务器本身更关键。云环境里的风险也常常来自权限漂移、长期密钥暴露、过宽角色、跨账号信任和 CI/CD 凭证泄露。 + +云身份的治理重点,是把权限从“能不能登录机器”升级为“能不能代表某个主体操作某类资源”。最小权限、短期凭证、条件访问、资源标签、审计日志和权限边界,构成了云安全的基础。 + +图像提示:云控制平面中的 IAM、角色、短期凭证、跨账号访问和审计日志连接到计算与数据资源,现代云安全架构图。 + +## 5. Zero Trust:持续验证替代默认信任 + +Zero Trust 的价值不在口号,而在执行层:不因为你在内网就信任你,不因为你登录过一次就永久信任你,不因为你是某个部门的人就默认给你全部权限。每一次访问都要结合身份、设备、位置、会话、资源敏感度和行为上下文进行判断。 + +在实践中,Zero Trust 更像一套纪律:强身份、设备可信、最小权限、分段访问、持续评估、完整审计。它把过去一次性的准入控制,变成贯穿整个会话和操作过程的动态决策。 + +图像提示:用户、设备、应用、数据之间有动态策略引擎逐次校验访问请求,表达持续验证和最小权限,清爽企业图形风。 + +## 6. AI Agent 身份:谁在替人行动 + +AI Agent 的出现,把安全问题继续向前推了一步。过去系统主要判断“人是谁”;现在还要判断“这个 Agent 是谁创建的、代表谁、能做什么、能持续多久、能不能被撤销”。Agent 不是普通 API Key,也不应该混用人的全部权限。 + +一个可控的 Agent 身份需要独立标识、授权范围、任务上下文、工具白名单、数据访问边界、审计链路和撤销机制。它可以替人执行任务,但不能无限继承人的身份,也不能在缺少上下文时自行扩大权限。 + +图像提示:一个 AI Agent 作为独立身份站在人类用户与工具系统之间,权限边界、任务授权、审计链和撤销按钮清晰可见。 + +## 7. AI 模型与知识保护:新资产边界 + +AI 时代的新资产不只有模型权重。提示词、系统指令、RAG 知识库、企业文档、工具调用记录、对话历史、评测集和业务语料,都会影响模型行为,也都可能泄露业务能力。保护模型,本质上是在保护知识、意图和执行路径。 + +因此,安全边界最终从“谁能访问系统”演进为“谁能携带什么知识,以什么身份,替谁执行什么动作”。未来的安全体系必须同时治理身份边界、知识边界和行动边界。人、应用、Agent、模型和工具都要可区分、可授权、可审计、可撤销。 + +图像提示:模型、RAG 知识库、提示词、业务文档和工具调用记录构成新的资产边界,中心是受保护的 AI 系统,精致信息图。 + +## 汇总判断 + +安全的演进不是旧问题被新问题替代,而是边界不断外移、主体不断增多、资产不断抽象。单机权限仍是入口,网络边界仍是基础,Web 安全仍是主战场,云身份成为控制平面,Zero Trust 成为执行纪律,而 AI Agent 身份与模型知识保护,正在成为下一轮企业安全的核心命题。 diff --git a/docs/cases/ai-security-evolution-content-scenario/ai-security-evolution-pdf.pdf b/docs/cases/ai-security-evolution-content-scenario/ai-security-evolution-pdf.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4d8ce45c788b2cdad1c88f4961aaa41728b6517f GIT binary patch literal 103385 zcma&NQ>-vhvn0B0+qP}n_WrhQ+qP}nwr$(C?fGwVlR1+)nVh$NTuG&>yK7aGDu{^D zGSaa^ksjaQyhAZE5ik(g8CpW|@X(7{SUa0I(u-LeIGc!=7}*({(94+EnmL;jurRQ( z@$o@9IXjvd*g&~&wrEYnUUMXNᒝl(cUkvz@|xB)w1=i3Hxh8HfE@jd_PZBBPc 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

7h0{qdinS`rfM~SLj1q?(PHF8+Z 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 BK`7Z?Eyi9R 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 lastTaskArtifactRelativePaths, OpenClawTaskAssociation? openClawTaskAssociation, bool clearOpenClawTaskAssociation = false, + List? taskInputAttachments, }) { final normalizedSessionKey = normalizedAssistantSessionKeyInternal( sessionKey, @@ -204,6 +205,7 @@ extension AppControllerDesktopSkillPermissions on AppController { lastArtifactSyncAtMs: null, lastArtifactSyncStatus: null, lastTaskArtifactRelativePaths: const [], + taskInputAttachments: const [], )) .copyWith( messages: nextMessages, @@ -229,6 +231,7 @@ extension AppControllerDesktopSkillPermissions on AppController { lastTaskArtifactRelativePaths: lastTaskArtifactRelativePaths, openClawTaskAssociation: openClawTaskAssociation, clearOpenClawTaskAssociation: clearOpenClawTaskAssociation, + taskInputAttachments: taskInputAttachments, ); final nextStatus = lifecycleStatus ?? diff --git a/lib/app/app_controller_desktop_thread_actions.dart b/lib/app/app_controller_desktop_thread_actions.dart index d0e3422d..1c13b671 100644 --- a/lib/app/app_controller_desktop_thread_actions.dart +++ b/lib/app/app_controller_desktop_thread_actions.dart @@ -459,8 +459,13 @@ extension AppControllerDesktopThreadActions on AppController { final capturedSelectedSkillLabels = List.unmodifiable( selectedSkillLabels, ); + final inlineAttachmentsToUpload = + registerTaskInputAttachmentsForGatewayTurnInternal( + normalizedSessionKey, + attachments, + ); final capturedAttachments = List.unmodifiable( - attachments, + inlineAttachmentsToUpload, ); final capturedLocalAttachments = List.unmodifiable( localAttachments, @@ -523,6 +528,61 @@ extension AppControllerDesktopThreadActions on AppController { recomputeTasksInternal(); } + List + registerTaskInputAttachmentsForGatewayTurnInternal( + String sessionKey, + List attachments, + ) { + if (attachments.isEmpty) { + return const []; + } + final normalizedSessionKey = normalizedAssistantSessionKeyInternal( + sessionKey, + ); + final existing = taskThreadForSessionInternal(normalizedSessionKey); + final existingByKey = { + for (final item + in existing?.taskInputAttachments ?? + const []) + if (item.key.isNotEmpty) item.key: item, + }; + final inlineAttachmentsToUpload = []; + final nextByKey = {...existingByKey}; + final uploadedAtMs = DateTime.now().millisecondsSinceEpoch.toDouble(); + for (final attachment in attachments) { + final key = gatewayAttachmentPayloadSha256Internal(attachment); + if (key.isEmpty) { + inlineAttachmentsToUpload.add(attachment); + continue; + } + if (!existingByKey.containsKey(key)) { + inlineAttachmentsToUpload.add(attachment); + } + nextByKey.putIfAbsent( + key, + () => TaskInputAttachmentRecord( + name: attachment.fileName.trim(), + mimeType: attachment.mimeType.trim(), + sha256: key, + type: attachment.type.trim(), + uploadedAtMs: uploadedAtMs, + ), + ); + } + if (nextByKey.length != existingByKey.length) { + upsertTaskThreadInternal( + normalizedSessionKey, + taskInputAttachments: nextByKey.values.toList(growable: false), + updatedAtMs: uploadedAtMs, + ); + } + return inlineAttachmentsToUpload; + } + + String gatewayAttachmentPayloadSha256Internal( + GatewayChatAttachmentPayload attachment, + ) => goTaskServiceAttachmentSha256(attachment); + Future runGatewayChatTurnInternal({ required String sessionKey, required AssistantExecutionTarget target, @@ -558,6 +618,9 @@ extension AppControllerDesktopThreadActions on AppController { executionWorkingDirectory: executionWorkingDirectory ?? workingDirectory, remoteWorkingDirectoryHint: remoteWorkingDirectoryHint, target: target, + taskInputAttachments: + taskThreadForSessionInternal(sessionKey)?.taskInputAttachments ?? + const [], ); if (appendUserTurn) { appendGatewayUserTurnInternal(sessionKey, message); @@ -592,7 +655,11 @@ extension AppControllerDesktopThreadActions on AppController { } }, ); - if (!aiGatewayPendingSessionKeysInternal.contains(sessionKey)) { + if (!aiGatewayPendingSessionKeysInternal.contains(sessionKey) && + taskThreadForSessionInternal( + sessionKey, + )?.lifecycleState.lastResultCode == + 'aborted') { clearAiGatewayStreamingTextInternal(sessionKey); return; } @@ -802,6 +869,8 @@ extension AppControllerDesktopThreadActions on AppController { String? executionWorkingDirectory, required String remoteWorkingDirectoryHint, required AssistantExecutionTarget target, + List taskInputAttachments = + const [], }) { final requestText = userPrompt.trim().isEmpty ? 'See attached.' @@ -824,6 +893,17 @@ extension AppControllerDesktopThreadActions on AppController { ? '(Use the runtime-provided default workspace)' : workingDirectory.trim(); buffer.writeln('- currentTaskWorkspace: $resolvedWorkspace'); + final visibleTaskInputAttachments = taskInputAttachments + .where((item) => item.name.trim().isNotEmpty && item.key.isNotEmpty) + .toList(growable: false); + if (visibleTaskInputAttachments.isNotEmpty) { + buffer.writeln('- taskInputAttachments:'); + for (final attachment in visibleTaskInputAttachments) { + buffer.writeln( + ' - ${attachment.name.trim()} (${attachment.mimeType.trim()}, sha256: ${attachment.key})', + ); + } + } buffer ..writeln() ..writeln('Workspace isolation rules:') @@ -845,6 +925,9 @@ extension AppControllerDesktopThreadActions on AppController { ..writeln( '6. The app syncs final artifacts from currentTaskWorkspace back into localWorkspace.', ) + ..writeln( + '7. Files listed in taskInputAttachments already belong to this TaskThread; reuse them from the task context and do not ask the user to upload them again.', + ) ..writeln(); buffer ..writeln('User request:') @@ -1250,6 +1333,13 @@ extension AppControllerDesktopThreadActions on AppController { recomputeTasksInternal(); notifyIfActiveInternal(); await persistGoTaskArtifactsForSessionInternal(sessionKey, result); + upsertTaskThreadInternal( + sessionKey, + lifecycleStatus: 'ready', + lastRunAtMs: completedAtMs, + lastResultCode: terminalResultCode, + updatedAtMs: completedAtMs, + ); } Future applyGatewayChatFailureInternal({ diff --git a/lib/app/ui_feature_manifest_core.dart b/lib/app/ui_feature_manifest_core.dart index ab17b846..f6f32cf5 100644 --- a/lib/app/ui_feature_manifest_core.dart +++ b/lib/app/ui_feature_manifest_core.dart @@ -47,6 +47,7 @@ abstract final class UiFeatureKeys { static const settingsGateway = 'settings.gateway'; static const settingsArchivedTasks = 'settings.archived_tasks'; + static const settingsRemoteDesktop = 'settings.remote_desktop'; static const settingsAccountAccess = 'settings.account_access'; static const settingsVaultServer = 'settings.vault_server'; static const settingsExperimentalCanvas = 'settings.experimental_canvas'; @@ -366,6 +367,7 @@ class UiFeatureAccess { { UiFeatureKeys.settingsGateway: SettingsTab.gateway, UiFeatureKeys.settingsArchivedTasks: SettingsTab.archivedTasks, + UiFeatureKeys.settingsRemoteDesktop: SettingsTab.remoteDesktop, }; bool isEnabledPath(String path) { diff --git a/lib/features/assistant/assistant_attachment_payloads.dart b/lib/features/assistant/assistant_attachment_payloads.dart index 5a7f551c..b95d46a6 100644 --- a/lib/features/assistant/assistant_attachment_payloads.dart +++ b/lib/features/assistant/assistant_attachment_payloads.dart @@ -1,6 +1,8 @@ import 'dart:convert'; import 'dart:io'; +import 'package:crypto/crypto.dart' as crypto; + import '../../runtime/runtime_models.dart'; import 'assistant_page_composer_clipboard.dart'; @@ -81,6 +83,7 @@ buildAssistantAttachmentPayloadsInternal( mimeType: mimeType, fileName: attachment.name, content: base64Encode(bytes), + sha256: crypto.sha256.convert(bytes).toString(), ), ); } diff --git a/lib/features/desktop/desktop_client.dart b/lib/features/desktop/desktop_client.dart new file mode 100644 index 00000000..455a206b --- /dev/null +++ b/lib/features/desktop/desktop_client.dart @@ -0,0 +1,158 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:flutter_webrtc/flutter_webrtc.dart'; +import '../../app/app_controller.dart'; + +class DesktopClient { + DesktopClient({required this.controller, required this.sessionId}); + + final AppController controller; + final String sessionId; + + RTCPeerConnection? _peerConnection; + RTCDataChannel? _dataChannel; + MediaStream? _remoteStream; + + final StreamController _streamController = + StreamController.broadcast(); + Stream get onRemoteStream => _streamController.stream; + + final StreamController _stateController = + StreamController.broadcast(); + Stream get onConnectionState => _stateController.stream; + + bool _isConnecting = false; + bool get isConnecting => _isConnecting; + bool get isConnected => + _peerConnection?.connectionState == + RTCPeerConnectionState.RTCPeerConnectionStateConnected; + + Future connect({ + String display = '', + int width = 1280, + int height = 720, + int fps = 30, + int bitrate = 2000, + bool useGpu = false, + }) async { + if (_isConnecting || isConnected) return; + _isConnecting = true; + _stateController.add('connecting'); + + try { + final config = { + 'iceServers': [ + {'url': 'stun:stun.l.google.com:19302'} + ], + 'sdpSemantics': 'unified-plan', + }; + + _peerConnection = await createPeerConnection(config); + + // Listen for remote streams + _peerConnection!.onTrack = (event) { + if (event.track.kind == 'video') { + if (event.streams.isNotEmpty) { + _remoteStream = event.streams.first; + _streamController.add(_remoteStream!); + } + } + }; + + _peerConnection!.onConnectionState = (state) { + _stateController.add(state.toString().split('.').last); + }; + + // Create data channel for inputs + final dcConfig = RTCDataChannelInit()..ordered = true; + _dataChannel = + await _peerConnection!.createDataChannel('input', dcConfig); + + // Handle ICE Candidates generated locally + _peerConnection!.onIceCandidate = (RTCIceCandidate? candidate) { + if (candidate != null) { + _sendIceCandidate(candidate); + } + }; + + // Create SDP Offer + final offer = await _peerConnection!.createOffer({ + 'offerToReceiveVideo': true, + 'offerToReceiveAudio': false, + }); + await _peerConnection!.setLocalDescription(offer); + + // Send SDP Offer to Bridge + final response = await controller.gatewayAcpClientInternal.request( + method: 'xworkmate.desktop.offer', + params: { + 'sessionId': sessionId, + 'sdpOffer': offer.sdp, + 'display': display, + 'width': width.toString(), + 'height': height.toString(), + 'fps': fps.toString(), + 'bitrate': bitrate.toString(), + 'useGpu': useGpu.toString(), + }, + ); + + final sdpAnswer = response['result']?['sdpAnswer'] as String?; + if (sdpAnswer == null) { + throw Exception('Bridge failed to return SDP Answer'); + } + + // Apply SDP Answer + final answer = RTCSessionDescription(sdpAnswer, 'answer'); + await _peerConnection!.setRemoteDescription(answer); + + _isConnecting = false; + } catch (e) { + _isConnecting = false; + _stateController.add('failed'); + await disconnect(); + rethrow; + } + } + + Future _sendIceCandidate(RTCIceCandidate candidate) async { + try { + await controller.gatewayAcpClientInternal.request( + method: 'xworkmate.desktop.ice', + params: { + 'sessionId': sessionId, + 'candidate': { + 'candidate': candidate.candidate, + 'sdpMid': candidate.sdpMid, + 'sdpMLineIndex': candidate.sdpMLineIndex, + }, + }, + ); + } catch (_) {} + } + + void sendInput(Map event) { + final channel = _dataChannel; + if (channel != null && + channel.state == RTCDataChannelState.RTCDataChannelOpen) { + final jsonStr = jsonEncode(event); + channel.send(RTCDataChannelMessage(jsonStr)); + } + } + + Future disconnect() async { + try { + await controller.gatewayAcpClientInternal.request( + method: 'xworkmate.desktop.close', + params: {'sessionId': sessionId}, + ); + } catch (_) {} + + await _dataChannel?.close(); + await _peerConnection?.close(); + _dataChannel = null; + _peerConnection = null; + _remoteStream = null; + _stateController.add('disconnected'); + } +} diff --git a/lib/features/desktop/desktop_input_handler.dart b/lib/features/desktop/desktop_input_handler.dart new file mode 100644 index 00000000..07cfbbeb --- /dev/null +++ b/lib/features/desktop/desktop_input_handler.dart @@ -0,0 +1,94 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +class DesktopInputHandler { + DesktopInputHandler({required this.onSendInput}); + + final void Function(Map event) onSendInput; + int _lastPressedButton = 1; // Default to left click + + void handlePointerMove(PointerEvent event, Size widgetSize) { + if (widgetSize.width == 0 || widgetSize.height == 0) return; + + final x = event.localPosition.dx / widgetSize.width; + final y = event.localPosition.dy / widgetSize.height; + + final cx = x.clamp(0.0, 1.0); + final cy = y.clamp(0.0, 1.0); + + onSendInput({ + 'type': 'mouse_move', + 'x': cx, + 'y': cy, + }); + } + + void handlePointerDown(PointerDownEvent event, Size widgetSize) { + if (widgetSize.width == 0 || widgetSize.height == 0) return; + + final x = event.localPosition.dx / widgetSize.width; + final y = event.localPosition.dy / widgetSize.height; + final cx = x.clamp(0.0, 1.0); + final cy = y.clamp(0.0, 1.0); + + // Send move event first to ensure click hits the exact coordinates + onSendInput({ + 'type': 'mouse_move', + 'x': cx, + 'y': cy, + }); + + _lastPressedButton = _mapPointerButtons(event.buttons); + + onSendInput({ + 'type': 'mouse_down', + 'button': _lastPressedButton, + }); + } + + void handlePointerUp(PointerUpEvent event, Size widgetSize) { + // Under pointer up, buttons bitmask represents STILL pressed buttons. + // If it is 0, then the released button is the one we tracked in PointerDown. + int releasedButton = _lastPressedButton; + if (event.buttons != 0) { + releasedButton = _mapPointerButtons(event.buttons); + } + + onSendInput({ + 'type': 'mouse_up', + 'button': releasedButton, + }); + } + + void handleScroll(PointerScrollEvent event) { + // 4 = scroll up, 5 = scroll down in X11/xdotool button maps + final button = event.scrollDelta.dy < 0 ? 4 : 5; + onSendInput({ + 'type': 'scroll', + 'button': button, + }); + } + + void handleKeyEvent(KeyEvent event) { + final isDown = event is KeyDownEvent || event is KeyRepeatEvent; + final keyLabel = event.logicalKey.keyLabel; + + onSendInput({ + 'type': isDown ? 'key_down' : 'key_up', + 'key': keyLabel, + }); + } + + int _mapPointerButtons(int buttons) { + // Flutter buttons bitmask: + // 1 = primary (left click) + // 2 = secondary (right click) + // 4 = middle click + // Linux/xdotool mouse mapping: 1=left, 2=middle, 3=right + if (buttons & 1 != 0) return 1; + if (buttons & 4 != 0) return 2; // middle click + if (buttons & 2 != 0) return 3; // right click + return 1; + } +} diff --git a/lib/features/desktop/desktop_view.dart b/lib/features/desktop/desktop_view.dart new file mode 100644 index 00000000..1cfa201d --- /dev/null +++ b/lib/features/desktop/desktop_view.dart @@ -0,0 +1,458 @@ +import 'dart:async'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart'; +import 'desktop_client.dart'; +import 'desktop_input_handler.dart'; +import '../../app/app_controller.dart'; +import '../../widgets/surface_card.dart'; + +class DesktopView extends StatefulWidget { + const DesktopView({super.key, required this.controller}); + + final AppController controller; + + @override + State createState() => _DesktopViewState(); +} + +class _DesktopViewState extends State { + final RTCVideoRenderer _localRenderer = RTCVideoRenderer(); + late DesktopClient _client; + DesktopInputHandler? _inputHandler; + + // Settings controllers + final TextEditingController _displayController = TextEditingController(text: ':0.0'); + final TextEditingController _widthController = TextEditingController(text: '1280'); + final TextEditingController _heightController = TextEditingController(text: '720'); + final TextEditingController _fpsController = TextEditingController(text: '30'); + final TextEditingController _bitrateController = TextEditingController(text: '2000'); + + bool _useGpu = false; + String _connectionState = 'disconnected'; + bool _hasStream = false; + bool _isFocused = false; + + final FocusNode _viewportFocusNode = FocusNode(); + final GlobalKey _viewportKey = GlobalKey(); + + StreamSubscription? _streamSubscription; + StreamSubscription? _stateSubscription; + + @override + void initState() { + super.initState(); + _initRenderer(); + _client = DesktopClient( + controller: widget.controller, + sessionId: 'remote-desktop-session', + ); + _inputHandler = DesktopInputHandler(onSendInput: (event) { + if (_connectionState == 'connected') { + _client.sendInput(event); + } + }); + + _streamSubscription = _client.onRemoteStream.listen((stream) { + if (mounted) { + setState(() { + _localRenderer.srcObject = stream; + _hasStream = true; + }); + } + }); + + _stateSubscription = _client.onConnectionState.listen((state) { + if (mounted) { + setState(() { + _connectionState = state.toLowerCase(); + if (_connectionState == 'disconnected' || _connectionState == 'failed') { + _hasStream = false; + _localRenderer.srcObject = null; + } + }); + } + }); + } + + Future _initRenderer() async { + await _localRenderer.initialize(); + } + + @override + void dispose() { + _streamSubscription?.cancel(); + _stateSubscription?.cancel(); + _client.disconnect(); + _localRenderer.dispose(); + _displayController.dispose(); + _widthController.dispose(); + _heightController.dispose(); + _fpsController.dispose(); + _bitrateController.dispose(); + _viewportFocusNode.dispose(); + super.dispose(); + } + + void _toggleConnection() async { + if (_connectionState == 'connected' || _connectionState == 'connecting') { + await _client.disconnect(); + } else { + final display = _displayController.text.trim(); + final width = int.tryParse(_widthController.text) ?? 1280; + final height = int.tryParse(_heightController.text) ?? 720; + final fps = int.tryParse(_fpsController.text) ?? 30; + final bitrate = int.tryParse(_bitrateController.text) ?? 2000; + + try { + await _client.connect( + display: display, + width: width, + height: height, + fps: fps, + bitrate: bitrate, + useGpu: _useGpu, + ); + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to connect remote desktop: $e'), + backgroundColor: Colors.redAccent, + ), + ); + } + } + } + } + + Size _getViewportSize() { + final renderBox = _viewportKey.currentContext?.findRenderObject() as RenderBox?; + return renderBox?.size ?? Size.zero; + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Control panel card + SurfaceCard( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Wrap( + spacing: 16, + runSpacing: 16, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + // Connection Button + ElevatedButton.icon( + onPressed: _toggleConnection, + style: ElevatedButton.styleFrom( + backgroundColor: _connectionState == 'connected' + ? Colors.redAccent + : (_connectionState == 'connecting' + ? Colors.orangeAccent + : theme.colorScheme.primary), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + icon: Icon( + _connectionState == 'connected' + ? Icons.portable_wifi_off_rounded + : Icons.settings_remote_rounded, + ), + label: Text( + _connectionState == 'connected' + ? '断开连接' + : (_connectionState == 'connecting' ? '正在连接...' : '连接桌面'), + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + + // Display Selector + SizedBox( + width: 100, + child: TextField( + controller: _displayController, + enabled: _connectionState == 'disconnected', + decoration: const InputDecoration( + labelText: 'Display', + prefixIcon: Icon(Icons.monitor_rounded, size: 16), + ), + ), + ), + + // Resolution settings + SizedBox( + width: 90, + child: TextField( + controller: _widthController, + enabled: _connectionState == 'disconnected', + keyboardType: TextInputType.number, + decoration: const InputDecoration(labelText: '宽度'), + ), + ), + SizedBox( + width: 90, + child: TextField( + controller: _heightController, + enabled: _connectionState == 'disconnected', + keyboardType: TextInputType.number, + decoration: const InputDecoration(labelText: '高度'), + ), + ), + + // FPS / Bitrate + SizedBox( + width: 70, + child: TextField( + controller: _fpsController, + enabled: _connectionState == 'disconnected', + keyboardType: TextInputType.number, + decoration: const InputDecoration(labelText: '帧率'), + ), + ), + SizedBox( + width: 90, + child: TextField( + controller: _bitrateController, + enabled: _connectionState == 'disconnected', + keyboardType: TextInputType.number, + decoration: const InputDecoration( + labelText: '码率 (kbps)', + ), + ), + ), + + // GPU accelerator toggle + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('GPU 加速'), + Switch( + value: _useGpu, + onChanged: _connectionState == 'disconnected' + ? (val) => setState(() => _useGpu = val) + : null, + ), + ], + ), + + // Status Indicator + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: _connectionState == 'connected' + ? Colors.green.withValues(alpha: 0.15) + : (_connectionState == 'connecting' + ? Colors.orange.withValues(alpha: 0.15) + : Colors.grey.withValues(alpha: 0.15)), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: _connectionState == 'connected' + ? Colors.green + : (_connectionState == 'connecting' + ? Colors.orange + : Colors.grey), + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: _connectionState == 'connected' + ? Colors.green + : (_connectionState == 'connecting' + ? Colors.orange + : Colors.grey), + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 8), + Text( + _connectionState == 'connected' + ? '已连接' + : (_connectionState == 'connecting' ? '连接中' : '未连接'), + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: _connectionState == 'connected' + ? Colors.green + : (_connectionState == 'connecting' + ? Colors.orange + : (isDark ? Colors.white70 : Colors.black87)), + ), + ), + ], + ), + ), + ], + ), + ), + ), + + const SizedBox(height: 16), + + // Stream Viewport Card + Expanded( + child: Focus( + focusNode: _viewportFocusNode, + onFocusChange: (focused) { + setState(() { + _isFocused = focused; + }); + }, + onKeyEvent: (node, event) { + if (_isFocused && _connectionState == 'connected' && _inputHandler != null) { + _inputHandler!.handleKeyEvent(event); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + child: Container( + key: _viewportKey, + decoration: BoxDecoration( + color: isDark ? Colors.black26 : Colors.black.withValues(alpha: 0.04), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: _isFocused + ? theme.colorScheme.primary + : (isDark ? Colors.white10 : Colors.black12), + width: 2, + ), + ), + clipBehavior: Clip.antiAlias, + child: Stack( + children: [ + // Stream Viewport Renderer + if (_hasStream) + Positioned.fill( + child: Listener( + behavior: HitTestBehavior.opaque, + onPointerHover: (event) { + if (_inputHandler != null) { + _inputHandler!.handlePointerMove(event, _getViewportSize()); + } + }, + onPointerMove: (event) { + if (_inputHandler != null) { + _inputHandler!.handlePointerMove(event, _getViewportSize()); + } + }, + onPointerDown: (event) { + if (!_viewportFocusNode.hasFocus) { + _viewportFocusNode.requestFocus(); + } + if (_inputHandler != null) { + _inputHandler!.handlePointerDown(event, _getViewportSize()); + } + }, + onPointerUp: (event) { + if (_inputHandler != null) { + _inputHandler!.handlePointerUp(event, _getViewportSize()); + } + }, + onPointerSignal: (event) { + if (event is PointerScrollEvent && _inputHandler != null) { + _inputHandler!.handleScroll(event); + } + }, + child: RTCVideoView( + _localRenderer, + objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain, + ), + ), + ), + + // Placeholder/Status UI overlay + if (!_hasStream) + Positioned.fill( + child: Container( + color: isDark ? Colors.black54 : Colors.grey[100], + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.monitor_rounded, + size: 64, + color: theme.colorScheme.onSurface.withValues(alpha: 0.2), + ), + const SizedBox(height: 16), + Text( + _connectionState == 'connecting' + ? '正在建立 WebRTC 连接,请稍候...' + : '未开启远程桌面流。点击“连接桌面”启动视频流。', + style: TextStyle( + color: theme.colorScheme.onSurface.withValues(alpha: 0.6), + fontSize: 14, + ), + ), + if (_connectionState == 'connecting') ...[ + const SizedBox(height: 24), + const CircularProgressIndicator(), + ], + ], + ), + ), + ), + ), + + // Focus watermark badge + if (_hasStream) + Positioned( + right: 8, + bottom: 8, + child: AnimatedOpacity( + opacity: _isFocused ? 0.3 : 0.8, + duration: const Duration(milliseconds: 200), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.6), + borderRadius: BorderRadius.circular(4), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + _isFocused + ? Icons.keyboard_rounded + : Icons.keyboard_hide_rounded, + color: Colors.white, + size: 14, + ), + const SizedBox(width: 4), + Text( + _isFocused ? '捕获键盘输入中' : '点击屏幕以捕获键盘', + style: const TextStyle( + color: Colors.white, + fontSize: 11, + ), + ), + ], + ), + ), + ), + ), + ], + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/settings/settings_page_core.dart b/lib/features/settings/settings_page_core.dart index 64a641bf..146c93a5 100644 --- a/lib/features/settings/settings_page_core.dart +++ b/lib/features/settings/settings_page_core.dart @@ -17,6 +17,7 @@ import '../../widgets/surface_card.dart'; import 'settings_account_panel.dart'; import 'settings_about_panel.dart'; import 'settings_archived_tasks_panel.dart'; +import 'settings_remote_desktop_panel.dart'; Future> loadBridgeMetadataForSettingsAbout({ required Uri bridgeEndpoint, @@ -514,7 +515,7 @@ class _SettingsPageState extends State { onRefresh: _refreshAboutSnapshot, ), ), - ] else if (currentTab == SettingsTab.archivedTasks) + ] else if (currentTab == SettingsTab.archivedTasks) ...[ SurfaceCard( key: const ValueKey('settings-archived-tasks-panel-card'), child: SettingsArchivedTasksPanel( @@ -523,6 +524,12 @@ class _SettingsPageState extends State { onDelete: _deleteArchivedTask, ), ), + ] else if (currentTab == SettingsTab.remoteDesktop) ...[ + SurfaceCard( + key: const ValueKey('settings-remote-desktop-panel-card'), + child: SettingsRemoteDesktopPanel(controller: controller), + ), + ], ], ); }, @@ -555,9 +562,11 @@ class _SettingsTabSelector extends StatelessWidget { ButtonSegment( value: tab, icon: Icon( - tab == SettingsTab.archivedTasks - ? Icons.inventory_2_outlined - : Icons.hub_outlined, + tab == SettingsTab.remoteDesktop + ? Icons.desktop_windows_outlined + : (tab == SettingsTab.archivedTasks + ? Icons.inventory_2_outlined + : Icons.hub_outlined), ), label: Text(tab.label), ), diff --git a/lib/features/settings/settings_remote_desktop_panel.dart b/lib/features/settings/settings_remote_desktop_panel.dart new file mode 100644 index 00000000..899d9825 --- /dev/null +++ b/lib/features/settings/settings_remote_desktop_panel.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import '../../app/app_controller.dart'; +import '../../i18n/app_language.dart'; +import '../../theme/app_palette.dart'; +import '../desktop/desktop_view.dart'; + +class SettingsRemoteDesktopPanel extends StatefulWidget { + const SettingsRemoteDesktopPanel({super.key, required this.controller}); + + final AppController controller; + + @override + State createState() => + _SettingsRemoteDesktopPanelState(); +} + +class _SettingsRemoteDesktopPanelState extends State { + @override + Widget build(BuildContext context) { + final palette = context.palette; + final theme = Theme.of(context); + + return Column( + key: const ValueKey('settings-remote-desktop-panel'), + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Icon(Icons.desktop_windows_outlined, color: palette.textSecondary), + const SizedBox(width: 10), + Expanded( + child: Text( + appText('远程桌面', 'Remote Desktop'), + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + SizedBox( + height: 640, + child: DesktopView(controller: widget.controller), + ), + ], + ); + } +} diff --git a/lib/models/app_models.dart b/lib/models/app_models.dart index 8e650367..6160c991 100644 --- a/lib/models/app_models.dart +++ b/lib/models/app_models.dart @@ -149,12 +149,13 @@ extension AssistantModeCopy on AssistantMode { }; } -enum SettingsTab { gateway, archivedTasks } +enum SettingsTab { gateway, archivedTasks, remoteDesktop } extension SettingsTabCopy on SettingsTab { String get label => switch (this) { SettingsTab.gateway => appText('集成', 'Integrations'), SettingsTab.archivedTasks => appText('归档任务', 'Archived tasks'), + SettingsTab.remoteDesktop => appText('远程桌面', 'Remote Desktop'), }; } diff --git a/lib/runtime/go_task_service_client.dart b/lib/runtime/go_task_service_client.dart index 14621d6a..8da3724a 100644 --- a/lib/runtime/go_task_service_client.dart +++ b/lib/runtime/go_task_service_client.dart @@ -1,3 +1,7 @@ +import 'dart:convert'; + +import 'package:crypto/crypto.dart' as crypto; + import 'runtime_models.dart'; enum GoTaskServiceRoute { externalAcpSingle, externalAcpMulti } @@ -308,6 +312,8 @@ class GoTaskServiceRequest { 'mimeType': item.mimeType, 'content': item.content, 'sizeBytes': goTaskServiceBase64Size(item.content), + if (goTaskServiceAttachmentSha256(item).isNotEmpty) + 'sha256': goTaskServiceAttachmentSha256(item), }, ) .toList(growable: false), @@ -1005,6 +1011,20 @@ int goTaskServiceBase64Size(String base64) { return (normalized.length * 3 ~/ 4) - padding; } +String goTaskServiceAttachmentSha256(GatewayChatAttachmentPayload attachment) { + final declared = attachment.sha256.trim().toLowerCase(); + if (declared.isNotEmpty) { + return declared; + } + try { + return crypto.sha256 + .convert(base64Decode(attachment.content.trim().split(',').last.trim())) + .toString(); + } on FormatException { + return ''; + } +} + Map _castMap(Object? value) { if (value is Map) { return value; diff --git a/lib/runtime/runtime_models_gateway_entities.dart b/lib/runtime/runtime_models_gateway_entities.dart index 150f53c1..3cdfd540 100644 --- a/lib/runtime/runtime_models_gateway_entities.dart +++ b/lib/runtime/runtime_models_gateway_entities.dart @@ -16,12 +16,14 @@ class GatewayChatAttachmentPayload { required this.mimeType, required this.fileName, required this.content, + this.sha256 = '', }); final String type; final String mimeType; final String fileName; final String content; + final String sha256; Map toJson() { return { @@ -29,6 +31,7 @@ class GatewayChatAttachmentPayload { 'mimeType': mimeType, 'fileName': fileName, 'content': content, + if (sha256.trim().isNotEmpty) 'sha256': sha256.trim(), }; } } diff --git a/lib/runtime/runtime_models_runtime_payloads.dart b/lib/runtime/runtime_models_runtime_payloads.dart index eac01bf3..43c1aa93 100644 --- a/lib/runtime/runtime_models_runtime_payloads.dart +++ b/lib/runtime/runtime_models_runtime_payloads.dart @@ -678,6 +678,7 @@ class ThreadContextState { this.lastArtifactSyncStatus, this.lastTaskArtifactRelativePaths = const [], this.openClawTaskAssociation, + this.taskInputAttachments = const [], }); final List messages; @@ -696,6 +697,7 @@ class ThreadContextState { final String? lastArtifactSyncStatus; final List lastTaskArtifactRelativePaths; final OpenClawTaskAssociation? openClawTaskAssociation; + final List taskInputAttachments; ThreadContextState copyWith({ List? messages, @@ -716,6 +718,7 @@ class ThreadContextState { List? lastTaskArtifactRelativePaths, OpenClawTaskAssociation? openClawTaskAssociation, bool clearOpenClawTaskAssociation = false, + List? taskInputAttachments, }) { return ThreadContextState( messages: messages ?? this.messages, @@ -745,6 +748,7 @@ class ThreadContextState { openClawTaskAssociation: clearOpenClawTaskAssociation ? null : (openClawTaskAssociation ?? this.openClawTaskAssociation), + taskInputAttachments: taskInputAttachments ?? this.taskInputAttachments, ); } @@ -766,6 +770,9 @@ class ThreadContextState { 'lastArtifactSyncStatus': lastArtifactSyncStatus, 'lastTaskArtifactRelativePaths': lastTaskArtifactRelativePaths, 'openClawTaskAssociation': openClawTaskAssociation?.toJson(), + 'taskInputAttachments': taskInputAttachments + .map((item) => item.toJson()) + .toList(growable: false), }; } @@ -834,10 +841,77 @@ class ThreadContextState { openClawTaskAssociation: OpenClawTaskAssociation.fromJsonOrNull( json['openClawTaskAssociation'], ), + taskInputAttachments: _taskInputAttachmentRecordsFromJson( + json['taskInputAttachments'], + ), ); } } +class TaskInputAttachmentRecord { + const TaskInputAttachmentRecord({ + required this.name, + required this.mimeType, + required this.sha256, + required this.type, + required this.uploadedAtMs, + }); + + final String name; + final String mimeType; + final String sha256; + final String type; + final double uploadedAtMs; + + String get key => sha256.trim().toLowerCase(); + + Map toJson() { + return { + 'name': name.trim(), + 'mimeType': mimeType.trim(), + 'sha256': key, + 'type': type.trim(), + 'uploadedAtMs': uploadedAtMs, + }; + } + + factory TaskInputAttachmentRecord.fromJson(Map json) { + double uploadedAtMs(Object? value) { + if (value is num) { + return value.toDouble(); + } + return double.tryParse(value?.toString() ?? '') ?? 0; + } + + return TaskInputAttachmentRecord( + name: json['name']?.toString().trim() ?? '', + mimeType: json['mimeType']?.toString().trim() ?? '', + sha256: json['sha256']?.toString().trim().toLowerCase() ?? '', + type: json['type']?.toString().trim() ?? '', + uploadedAtMs: uploadedAtMs(json['uploadedAtMs']), + ); + } +} + +List _taskInputAttachmentRecordsFromJson( + Object? value, +) { + if (value is! List) { + return const []; + } + final byKey = {}; + for (final item in value.whereType()) { + final record = TaskInputAttachmentRecord.fromJson( + item.cast(), + ); + if (record.key.isEmpty || record.name.isEmpty) { + continue; + } + byKey.putIfAbsent(record.key, () => record); + } + return byKey.values.toList(growable: false); +} + class OpenClawTaskAssociation { const OpenClawTaskAssociation({ required this.sessionId, @@ -1097,6 +1171,7 @@ class TaskThread { String? lastArtifactSyncStatus, List? lastTaskArtifactRelativePaths, OpenClawTaskAssociation? openClawTaskAssociation, + List? taskInputAttachments, }) : threadId = _resolveThreadId(threadId), title = title ?? '', ownerScope = @@ -1144,6 +1219,8 @@ class TaskThread { lastTaskArtifactRelativePaths, ), openClawTaskAssociation: openClawTaskAssociation, + taskInputAttachments: + taskInputAttachments ?? const [], ), lifecycleState = lifecycleState ?? @@ -1184,6 +1261,8 @@ class TaskThread { contextState.lastTaskArtifactRelativePaths; OpenClawTaskAssociation? get openClawTaskAssociation => contextState.openClawTaskAssociation; + List get taskInputAttachments => + contextState.taskInputAttachments; String get latestResolvedRuntimeModel => contextState.latestResolvedRuntimeModel; String get latestResolvedProviderId => contextState.latestResolvedProviderId; @@ -1229,6 +1308,7 @@ class TaskThread { List? lastTaskArtifactRelativePaths, OpenClawTaskAssociation? openClawTaskAssociation, bool clearOpenClawTaskAssociation = false, + List? taskInputAttachments, }) { return TaskThread( threadId: threadId ?? this.threadId, @@ -1254,6 +1334,7 @@ class TaskThread { lastTaskArtifactRelativePaths: lastTaskArtifactRelativePaths, openClawTaskAssociation: openClawTaskAssociation, clearOpenClawTaskAssociation: clearOpenClawTaskAssociation, + taskInputAttachments: taskInputAttachments, ), lifecycleState: (lifecycleState ?? this.lifecycleState).copyWith( archived: archived, @@ -1371,6 +1452,7 @@ class TaskThread { 'lastArtifactSyncStatus': json['lastArtifactSyncStatus'], 'lastTaskArtifactRelativePaths': json['lastTaskArtifactRelativePaths'], 'openClawTaskAssociation': json['openClawTaskAssociation'], + 'taskInputAttachments': json['taskInputAttachments'], }; } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 0dbee6e6..c4e8c71b 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include @@ -14,6 +15,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin"); + flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar); g_autoptr(FlPluginRegistrar) irondash_engine_context_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "IrondashEngineContextPlugin"); irondash_engine_context_plugin_register_with_registrar(irondash_engine_context_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index c7ac82b9..072a5478 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux + flutter_webrtc irondash_engine_context super_native_extensions ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 9fdc93fd..4e32b0e3 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,6 +7,7 @@ import Foundation import device_info_plus import file_selector_macos +import flutter_webrtc import irondash_engine_context import package_info_plus import patrol @@ -16,6 +17,7 @@ import super_native_extensions func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PatrolPlugin.register(with: registry.registrar(forPlugin: "PatrolPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 7f184593..cdc2431d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -97,6 +97,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + dart_webrtc: + dependency: transitive + description: + name: dart_webrtc + sha256: f6d615bddea5e458ce180a914f3055c234ffb52fb7397a51b3491e76d6d7edb2 + url: "https://pub.dev" + source: hosted + version: "1.8.1" device_info_plus: dependency: "direct main" description: @@ -274,6 +282,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_webrtc: + dependency: "direct main" + description: + name: flutter_webrtc + sha256: b832dc76c0d1577f14aaf35e9c38d4ed7667cbc89c492b7bf4505d8d5f62e08b + url: "https://pub.dev" + source: hosted + version: "0.12.12+hotfix.1" fuchsia_remote_debug_protocol: dependency: transitive description: flutter @@ -348,6 +364,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev" + source: hosted + version: "0.7.2" json_annotation: dependency: transitive description: @@ -711,6 +735,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "63896c27e81b28f8cb4e69ead0d3e8f03f1d1e5fc531a3e579cabed6a2c7c9e5" + url: "https://pub.dev" + source: hosted + version: "3.4.0+1" term_glyph: dependency: transitive description: @@ -791,6 +823,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" + webrtc_interface: + dependency: transitive + description: + name: webrtc_interface + sha256: c6f100eac5057d9a817a60473126f9828c796d42884d498af4f339c97b21014f + url: "https://pub.dev" + source: hosted + version: "1.5.1" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c24f9a28..3d682386 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,6 +28,7 @@ dependencies: shared_preferences: ^2.5.3 super_clipboard: ^0.9.0 web_socket_channel: ^3.0.3 + flutter_webrtc: ^0.12.3 yaml: ^3.1.3 dev_dependencies: diff --git a/test/features/settings/settings_remote_desktop_panel_test.dart b/test/features/settings/settings_remote_desktop_panel_test.dart new file mode 100644 index 00000000..1c1ef4c0 --- /dev/null +++ b/test/features/settings/settings_remote_desktop_panel_test.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:xworkmate/app/app_controller.dart'; +import 'package:xworkmate/features/settings/settings_remote_desktop_panel.dart'; +import 'package:xworkmate/runtime/secure_config_store.dart'; +import 'package:xworkmate/runtime/runtime_models.dart'; +import 'package:xworkmate/theme/app_theme.dart'; +import 'package:xworkmate/widgets/surface_card.dart'; + +void main() { + group('SettingsRemoteDesktopPanel', () { + testWidgets('renders the panel title and connection dashboard', (tester) async { + // Set desktop window size + tester.view.physicalSize = const Size(1280, 900); + tester.view.devicePixelRatio = 1.0; + + final store = _MemorySecureConfigStore(); + final controller = _NoopRefreshAppController(store: store); + addTearDown(() { + controller.dispose(); + tester.view.resetPhysicalSize(); + tester.view.resetDevicePixelRatio(); + }); + + await tester.pumpWidget( + _buildTestApp( + child: SettingsRemoteDesktopPanel(controller: controller), + ), + ); + + // Verify the panel headers and titles + expect(find.text('远程桌面'), findsOneWidget); + expect(find.text('连接桌面'), findsOneWidget); + expect(find.text('GPU 加速'), findsOneWidget); + + // Verify inputs + expect(find.widgetWithText(TextField, 'Display'), findsOneWidget); + expect(find.text('Display'), findsOneWidget); + }); + }); +} + +Widget _buildTestApp({required Widget child}) { + return MaterialApp( + theme: AppTheme.light(), + home: Material( + child: Center( + child: SizedBox( + width: 1100, + child: SurfaceCard(child: child), + ), + ), + ), + ); +} + +class _NoopRefreshAppController extends AppController { + _NoopRefreshAppController({required SecureConfigStore store}) + : super(environmentOverride: const {}, store: store); + + Future refreshAcpCapabilitiesInternal({ + bool forceRefresh = false, + bool persistMountTargets = false, + }) async {} + + Future refreshSingleAgentCapabilitiesInternal({ + bool forceRefresh = false, + }) async {} +} + +class _MemorySecureConfigStore extends SecureConfigStore { + _MemorySecureConfigStore() : super(enableSecureStorage: false); + + SettingsSnapshot _settings = SettingsSnapshot.defaults(); + final Map _secrets = {}; + + @override + Future initialize() async {} + + @override + Future loadSettingsSnapshot() async => _settings; + + @override + Future saveSettingsSnapshot(SettingsSnapshot snapshot) async { + _settings = snapshot; + } + + @override + Future> loadSecureRefs() async => _secrets; + + @override + Future> loadAuditTrail() async => + const []; + + @override + Future appendAudit(SecretAuditEntry entry) async {} + + @override + Future loadSecretValueByRef(String refName) async => + _secrets[refName]; + + @override + Future saveSecretValueByRef(String refName, String value) async { + _secrets[refName] = value; + } + + @override + Future loadAccountSessionToken() async => null; + + @override + Future loadAccountSessionSummary() async => null; + + @override + Future loadAccountSyncState() async => null; +} diff --git a/test/runtime/assistant_execution_target_test.dart b/test/runtime/assistant_execution_target_test.dart index 601cefba..0f4fd10c 100644 --- a/test/runtime/assistant_execution_target_test.dart +++ b/test/runtime/assistant_execution_target_test.dart @@ -1351,6 +1351,10 @@ void main() { base64Encode(utf8.encode('note body')), ); expect(inlineAttachment['sizeBytes'], 9); + expect( + inlineAttachment['sha256'], + '1c727d26215adccb96d725e8b63b3ee11cf73215a554e60295877244b0778847', + ); final attachments = params['attachments'] as List; final attachment = attachments.single as Map; expect(attachment['name'], 'note.txt'); @@ -1358,6 +1362,56 @@ void main() { }, ); + test( + 'sendChatMessage records task input attachments and does not reupload duplicates', + () async { + final fakeGoTaskService = _RecordingGoTaskServiceClient(); + final controller = _connectedGatewayController(fakeGoTaskService); + addTearDown(controller.dispose); + + await controller.ensureActiveAssistantThreadInternal(); + await controller.setAssistantExecutionTarget( + AssistantExecutionTarget.gateway, + ); + + final imageAttachment = GatewayChatAttachmentPayload( + type: 'image', + mimeType: 'image/png', + fileName: 'diagram.png', + content: base64Encode(utf8.encode('image bytes')), + ); + + await controller.sendChatMessage( + 'use the image', + attachments: [imageAttachment], + ); + await controller.sendChatMessage( + 'continue with the same image', + attachments: [imageAttachment], + ); + + expect(fakeGoTaskService.requests, hasLength(2)); + expect( + fakeGoTaskService.requests.first.inlineAttachments, + hasLength(1), + ); + expect(fakeGoTaskService.requests.last.inlineAttachments, isEmpty); + expect( + fakeGoTaskService.requests.last.prompt, + contains('- taskInputAttachments:'), + ); + expect( + fakeGoTaskService.requests.last.prompt, + contains('diagram.png (image/png, sha256:'), + ); + final thread = controller.requireTaskThreadForSessionInternal( + fakeGoTaskService.requests.last.sessionId, + ); + expect(thread.taskInputAttachments, hasLength(1)); + expect(thread.taskInputAttachments.single.name, 'diagram.png'); + }, + ); + test( 'sendChatMessage resumes existing task after response interruption', () async { diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 4fcbf216..c37a235c 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,12 +7,15 @@ #include "generated_plugin_registrant.h" #include +#include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); + FlutterWebRTCPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterWebRTCPlugin")); IrondashEngineContextPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("IrondashEngineContextPluginCApi")); SuperNativeExtensionsPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 829fcfea..bbf48b19 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows + flutter_webrtc irondash_engine_context super_native_extensions )