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 {