From 71bae611fc20e81ffa7d173e4f1dbda6ce7aab35 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Fri, 29 May 2026 14:30:19 +0800 Subject: [PATCH 1/2] refactor: classify gateway task load --- ...app_controller_desktop_thread_actions.dart | 82 ++++++++++++++++++- .../assistant_execution_target_test.dart | 80 ++++++++++++++++++ 2 files changed, 160 insertions(+), 2 deletions(-) diff --git a/lib/app/app_controller_desktop_thread_actions.dart b/lib/app/app_controller_desktop_thread_actions.dart index 031bd64c..096f046e 100644 --- a/lib/app/app_controller_desktop_thread_actions.dart +++ b/lib/app/app_controller_desktop_thread_actions.dart @@ -451,6 +451,11 @@ extension AppControllerDesktopThreadActions on AppController { final capturedLocalAttachments = List.unmodifiable( localAttachments, ); + final taskLoadClass = classifyGatewayTaskLoadInternal(message); + final taskMetadata = Map.unmodifiable({ + ...dispatch.metadata, + 'taskLoadClass': taskLoadClass, + }); final executionWorkingDirectory = gatewayExecutionWorkingDirectoryInternal( target: currentTarget, workingDirectory: workingDirectory, @@ -475,7 +480,7 @@ extension AppControllerDesktopThreadActions on AppController { model: model, routing: routing, agentId: dispatch.agentId ?? '', - metadata: Map.unmodifiable(dispatch.metadata), + metadata: taskMetadata, resumeSessionHint: resumeSessionHint, appendUserTurn: appendUserTurn, ), @@ -500,7 +505,7 @@ extension AppControllerDesktopThreadActions on AppController { model: model, routing: routing, agentId: dispatch.agentId ?? '', - metadata: Map.unmodifiable(dispatch.metadata), + metadata: taskMetadata, resumeSessionHint: resumeSessionHint, appendUserTurn: appendUserTurn, ), @@ -672,11 +677,84 @@ extension AppControllerDesktopThreadActions on AppController { '6. The app syncs final artifacts from currentTaskWorkspace back into localWorkspace.', ) ..writeln() + ..writeln('Task load classification:') + ..writeln('- class: ${classifyGatewayTaskLoadInternal(requestText)}') + ..writeln( + '- Gateway owns execution decomposition, scheduling, retries, and resumability for this class.', + ) + ..writeln() + ..writeln( + 'Available classes: short_task, long_task, complex_long_chain_task.', + ) + ..writeln(); + buffer ..writeln('User request:') ..write(requestText); return buffer.toString(); } + String classifyGatewayTaskLoadInternal(String requestText) { + final normalized = requestText.trim().toLowerCase(); + if (normalized.isEmpty) { + return 'short_task'; + } + final hasChapterSplit = + normalized.contains('拆章节') || + normalized.contains('chapter') || + normalized.contains('章节'); + final hasAgentStage = + normalized.contains('codex') || + normalized.contains('agent') || + normalized.contains('调用'); + final hasImageStage = + normalized.contains('gpt images') || + normalized.contains('images2') || + normalized.contains('生成图') || + normalized.contains('图片'); + final hasPackagingStage = + normalized.contains('汇总排版') || + normalized.contains('排版') || + normalized.contains('制作视频') || + normalized.contains('视频') || + normalized.contains('mp4'); + final hasChainArrows = + normalized.contains('->') || normalized.contains('→'); + if (hasChapterSplit && + hasAgentStage && + hasImageStage && + hasPackagingStage && + hasChainArrows) { + return 'complex_long_chain_task'; + } + const longTaskMarkers = [ + '生成文件', + '产物', + '附件', + '图片提示词', + '完整调研ppt', + 'markdown格式', + '输出markdown', + 'ppt', + 'pptx', + 'powerpoint', + 'word', + 'docx', + 'png', + 'mp4', + 'jpg', + 'markdown', + '.md', + 'image prompt', + 'artifacts', + 'downloadurl', + ]; + if (requestText.length >= 1200 || + longTaskMarkers.any(normalized.contains)) { + return 'long_task'; + } + return 'short_task'; + } + bool usesOpenClawGatewayQueueInternal( AssistantExecutionTarget target, SingleAgentProvider provider, diff --git a/test/runtime/assistant_execution_target_test.dart b/test/runtime/assistant_execution_target_test.dart index c5b079a5..9510093c 100644 --- a/test/runtime/assistant_execution_target_test.dart +++ b/test/runtime/assistant_execution_target_test.dart @@ -1143,6 +1143,86 @@ void main() { ); }); + test( + 'sendChatMessage classifies complex artifact chains for Gateway', + () async { + final fakeGoTaskService = _RecordingGoTaskServiceClient(); + final controller = _connectedGatewayController(fakeGoTaskService); + addTearDown(controller.dispose); + + await controller.ensureActiveAssistantThreadInternal(); + await controller.setAssistantExecutionTarget( + AssistantExecutionTarget.gateway, + ); + await controller.sendChatMessage( + '围绕\n\n' + '从单机权限 → 网络边界 → Web安全 → 云身份 → Zero Trust → AI Agent 身份 → AI模型与知识保护 演进\n\n' + '拆章节 -> 每章调用 Codex -> 每章 GPT images2 生成图 -> 汇总排版 -> 制作视频', + ); + + expect(fakeGoTaskService.requests, hasLength(1)); + final request = fakeGoTaskService.requests.single; + expect(request.metadata['taskLoadClass'], 'complex_long_chain_task'); + expect(request.prompt, contains('Task load classification:')); + expect(request.prompt, contains('- class: complex_long_chain_task')); + expect( + request.prompt, + contains( + 'Gateway owns execution decomposition, scheduling, retries, and resumability for this class.', + ), + ); + expect( + request.prompt, + isNot(contains('First write the chapter breakdown')), + ); + expect( + request.prompt, + isNot(contains('Run heavyweight stages in order')), + ); + expect( + request.prompt, + contains('User request:\n围绕\n\n从单机权限 → 网络边界 → Web安全'), + ); + }, + ); + + test( + 'sendChatMessage classifies simple Gateway prompts as short tasks', + () async { + final fakeGoTaskService = _RecordingGoTaskServiceClient(); + final controller = _connectedGatewayController(fakeGoTaskService); + addTearDown(controller.dispose); + + await controller.ensureActiveAssistantThreadInternal(); + await controller.setAssistantExecutionTarget( + AssistantExecutionTarget.gateway, + ); + await controller.sendChatMessage('写一段普通说明'); + + expect(fakeGoTaskService.requests, hasLength(1)); + final request = fakeGoTaskService.requests.single; + expect(request.metadata['taskLoadClass'], 'short_task'); + expect(request.prompt, contains('- class: short_task')); + }, + ); + + test('sendChatMessage classifies artifact output as a long task', () async { + final fakeGoTaskService = _RecordingGoTaskServiceClient(); + final controller = _connectedGatewayController(fakeGoTaskService); + addTearDown(controller.dispose); + + await controller.ensureActiveAssistantThreadInternal(); + await controller.setAssistantExecutionTarget( + AssistantExecutionTarget.gateway, + ); + await controller.sendChatMessage('生成 Markdown 和 PNG 产物'); + + expect(fakeGoTaskService.requests, hasLength(1)); + final request = fakeGoTaskService.requests.single; + expect(request.metadata['taskLoadClass'], 'long_task'); + expect(request.prompt, contains('- class: long_task')); + }); + test( 'sendChatMessage runs Gateway task with remote workspace when local workspace is unavailable', () async { From d814f79bb36fce8feb79c5a2bfffa47d5e56652d Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sat, 30 May 2026 10:50:26 +0800 Subject: [PATCH 2/2] Use manual bridge config for ACP runtime --- ...pp_controller_desktop_runtime_helpers.dart | 35 ++++++++++ ...ime_controllers_settings_account_test.dart | 65 +++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/lib/app/app_controller_desktop_runtime_helpers.dart b/lib/app/app_controller_desktop_runtime_helpers.dart index 9e101e6b..38d0638e 100644 --- a/lib/app/app_controller_desktop_runtime_helpers.dart +++ b/lib/app/app_controller_desktop_runtime_helpers.dart @@ -1016,6 +1016,16 @@ extension AppControllerDesktopRuntimeHelpers on AppController { } Uri? resolveBridgeAcpEndpointInternal() { + final selfHosted = + settingsControllerInternal.snapshot.acpBridgeServerModeConfig.selfHosted; + final selfHostedUrl = selfHosted.serverUrl.trim(); + if (selfHosted.isConfigured && selfHostedUrl.isNotEmpty) { + final uri = Uri.tryParse(selfHostedUrl); + if (uri != null && uri.hasScheme && uri.host.trim().isNotEmpty) { + return uri.replace(query: null, fragment: null); + } + } + final uri = Uri.parse(kManagedBridgeServerUrl); return uri.replace(query: null, fragment: null); } @@ -1029,6 +1039,11 @@ extension AppControllerDesktopRuntimeHelpers on AppController { if (bridgeEndpoint == null) { return false; } + final selfHosted = + settingsControllerInternal.snapshot.acpBridgeServerModeConfig.selfHosted; + if (selfHosted.isConfigured) { + return true; + } final accountSyncState = settingsControllerInternal.accountSyncState; if (settingsControllerInternal.accountSignedIn && accountSyncState?.tokenConfigured.bridge == true) { @@ -1072,6 +1087,10 @@ extension AppControllerDesktopRuntimeHelpers on AppController { normalizedHost == bridgeHost && (bridgePort <= 0 || endpoint.port == bridgePort); if (matchesBridgeEndpoint) { + final manualBridgeToken = await _resolveManualBridgeAuthTokenInternal(); + if (manualBridgeToken != null && manualBridgeToken.isNotEmpty) { + return manualBridgeToken; + } final bridgeToken = await _resolveManagedBridgeAuthTokenInternal(); if (bridgeToken != null && bridgeToken.isNotEmpty) { return bridgeToken; @@ -1097,6 +1116,22 @@ extension AppControllerDesktopRuntimeHelpers on AppController { return null; } + Future _resolveManualBridgeAuthTokenInternal() async { + final selfHosted = + settingsControllerInternal.snapshot.acpBridgeServerModeConfig.selfHosted; + if (!selfHosted.isConfigured) { + return null; + } + final passwordRef = selfHosted.passwordRef.trim(); + if (passwordRef.isEmpty) { + return null; + } + final token = (await storeInternal.loadSecretValueByRef( + passwordRef, + ))?.trim(); + return token?.isNotEmpty == true ? token : null; + } + Future _resolveManagedBridgeAuthTokenInternal() async { final accountSyncState = settingsControllerInternal.accountSyncState; if (settingsControllerInternal.accountSignedIn && diff --git a/test/runtime/runtime_controllers_settings_account_test.dart b/test/runtime/runtime_controllers_settings_account_test.dart index 5951127f..e5167247 100644 --- a/test/runtime/runtime_controllers_settings_account_test.dart +++ b/test/runtime/runtime_controllers_settings_account_test.dart @@ -441,6 +441,71 @@ void main() { }, ); + test('manual bridge config becomes the runtime ACP source', () async { + final storeRoot = await Directory.systemTemp.createTemp( + 'xworkmate-manual-bridge-runtime-', + ); + addTearDown(() async { + if (await storeRoot.exists()) { + try { + await storeRoot.delete(recursive: true); + } on FileSystemException { + // Temp cleanup is best effort here. The controller may still be + // releasing files when teardown starts. + } + } + }); + + final store = SecureConfigStore( + secretRootPathResolver: () async => '${storeRoot.path}/secrets', + appDataRootPathResolver: () async => '${storeRoot.path}/app-data', + supportRootPathResolver: () async => '${storeRoot.path}/support', + enableSecureStorage: false, + ); + await store.initialize(); + await store.saveSettingsSnapshot( + SettingsSnapshot.defaults().copyWith( + acpBridgeServerModeConfig: AcpBridgeServerModeConfig.defaults() + .copyWith( + selfHosted: AcpBridgeServerModeConfig.defaults().selfHosted + .copyWith( + serverUrl: 'https://private-bridge.svc.plus', + username: 'admin', + ), + ), + ), + ); + await store.saveSecretValueByRef( + AcpBridgeServerSelfHostedConfig.defaults().passwordRef, + 'manual-bridge-token', + ); + + final controller = AppController( + environmentOverride: const {}, + store: store, + ); + addTearDown(controller.dispose); + await controller.settingsControllerInternal.initialize(); + + expect( + controller.resolveGatewayAcpEndpointInternal()?.toString(), + 'https://private-bridge.svc.plus', + ); + expect(controller.isBridgeAcpRuntimeConfiguredInternal(), isTrue); + expect( + await controller.resolveGatewayAcpAuthorizationHeaderInternal( + Uri.parse('https://private-bridge.svc.plus/acp/rpc'), + ), + 'manual-bridge-token', + ); + expect( + await controller.resolveGatewayAcpAuthorizationHeaderInternal( + Uri.parse('$kManagedBridgeServerUrl/acp/rpc'), + ), + isNull, + ); + }); + test( 'syncAccountSettings succeeds when bridge url metadata is missing', () async {