diff --git a/lib/app/app_controller_desktop_core.dart b/lib/app/app_controller_desktop_core.dart index bb7edb0f..2171f112 100644 --- a/lib/app/app_controller_desktop_core.dart +++ b/lib/app/app_controller_desktop_core.dart @@ -646,15 +646,6 @@ class AppController extends ChangeNotifier { AssistantExecutionTarget target, ) => resolvedDefaultModel.trim(); - List assistantImportedSkillsForSession( - String sessionKey, - ) => - assistantThreadRecordsInternal[normalizedAssistantSessionKeyInternal( - sessionKey, - )] - ?.importedSkills ?? - const []; - void navigateTo(WorkspaceDestination destination) => AppControllerDesktopNavigation(this).navigateTo(destination); @@ -692,6 +683,6 @@ class AppController extends ChangeNotifier { this, ).refreshMultiAgentMounts(sync: sync); - double get assistantSkillCount => 0; // Legacy - int get currentAssistantSkillCount => 0; // Legacy + double get assistantSkillCount => skills.length.toDouble(); + int get currentAssistantSkillCount => skills.length; } diff --git a/lib/app/app_controller_desktop_external_acp_routing.dart b/lib/app/app_controller_desktop_external_acp_routing.dart index 492a4c13..06c68a36 100644 --- a/lib/app/app_controller_desktop_external_acp_routing.dart +++ b/lib/app/app_controller_desktop_external_acp_routing.dart @@ -46,28 +46,27 @@ extension AppControllerDesktopExternalAcpRouting on AppController { ); final thread = assistantThreadRecordsInternal[normalizedSessionKey]; const preferredGatewayTarget = kCanonicalGatewayProviderId; - final availableSkills = - assistantImportedSkillsForSession(normalizedSessionKey) - .map((item) { - return ExternalCodeAgentAcpAvailableSkill( - id: item.key, - label: item.label, - description: item.description, - ); - }) - .toList(growable: false); - final selectedSkills = - assistantSelectedSkillsForSession(normalizedSessionKey) - .map((item) { - return item.label.trim().isNotEmpty ? item.label : item.key; - }) - .where((item) => item.trim().isNotEmpty) - .toList(growable: false); + final availableSkills = skills + .map((item) { + return ExternalCodeAgentAcpAvailableSkill( + id: item.skillKey, + label: item.name, + description: item.description, + ); + }) + .toList(growable: false); + final selectedSkills = assistantSelectedSkillKeysForSession( + normalizedSessionKey, + ); final currentTarget = assistantExecutionTargetForSession( normalizedSessionKey, ); final resolvedProvider = assistantProviderForSession(normalizedSessionKey); + final resolvedExecutionTarget = + explicitExecutionTarget?.trim().isNotEmpty == true + ? explicitExecutionTarget!.trim() + : _routingExecutionTargetValueInternal(currentTarget); final resolvedExplicitProviderId = thread?.hasExplicitProviderSelection == true && !currentTarget.isGateway && @@ -80,33 +79,10 @@ extension AppControllerDesktopExternalAcpRouting on AppController { final resolvedExplicitSkills = thread?.hasExplicitSkillSelection ?? false ? selectedSkills : const []; - final hasAnyExplicitSelection = - resolvedExplicitProviderId.isNotEmpty || - resolvedExplicitModel.trim().isNotEmpty || - resolvedExplicitSkills.isNotEmpty; - final resolvedExplicitExecutionTarget = - explicitExecutionTarget?.trim().isNotEmpty == true - ? explicitExecutionTarget!.trim() - : (hasAnyExplicitSelection || currentTarget.isGateway) - ? _routingExecutionTargetValueInternal(currentTarget) - : ''; - final hasExplicitSelection = - resolvedExplicitExecutionTarget.isNotEmpty || - resolvedExplicitProviderId.isNotEmpty || - resolvedExplicitModel.trim().isNotEmpty || - resolvedExplicitSkills.isNotEmpty; - - if (!hasExplicitSelection) { - return ExternalCodeAgentAcpRoutingConfig.auto( - preferredGatewayTarget: preferredGatewayTarget, - availableSkills: availableSkills, - ); - } - return ExternalCodeAgentAcpRoutingConfig( mode: ExternalCodeAgentAcpRoutingMode.explicit, preferredGatewayTarget: preferredGatewayTarget, - explicitExecutionTarget: resolvedExplicitExecutionTarget, + explicitExecutionTarget: resolvedExecutionTarget, explicitProviderId: resolvedExplicitProviderId, explicitModel: resolvedExplicitModel, explicitSkills: resolvedExplicitSkills, diff --git a/lib/app/app_controller_desktop_thread_sessions.dart b/lib/app/app_controller_desktop_thread_sessions.dart index f52735b8..f87fadb2 100644 --- a/lib/app/app_controller_desktop_thread_sessions.dart +++ b/lib/app/app_controller_desktop_thread_sessions.dart @@ -214,40 +214,20 @@ extension AppControllerDesktopThreadSessions on AppController { ); } - int assistantSkillCountForSession(String sessionKey) { - return skills.length; - } - - int get currentAssistantSkillCount => - assistantSkillCountForSession(currentSessionKey); - List assistantSelectedSkillKeysForSession(String sessionKey) { final normalizedSessionKey = normalizedAssistantSessionKeyInternal( sessionKey, ); - final importedKeys = assistantImportedSkillsForSession( - normalizedSessionKey, - ).map((item) => item.key).toSet(); final selected = assistantThreadRecordsInternal[normalizedSessionKey] ?.selectedSkillKeys ?? const []; + final availableKeys = skills.map((item) => item.skillKey).toSet(); return selected - .where((item) => importedKeys.contains(item)) + .where(availableKeys.contains) .toList(growable: false); } - List assistantSelectedSkillsForSession( - String sessionKey, - ) { - final selectedKeys = assistantSelectedSkillKeysForSession( - sessionKey, - ).toSet(); - return assistantImportedSkillsForSession( - sessionKey, - ).where((item) => selectedKeys.contains(item.key)).toList(growable: false); - } - String assistantModelForSession(String sessionKey) { final normalizedSessionKey = normalizedAssistantSessionKeyInternal( sessionKey, diff --git a/lib/app/app_controller_desktop_workspace_execution.dart b/lib/app/app_controller_desktop_workspace_execution.dart index 7e2f28b2..a3238105 100644 --- a/lib/app/app_controller_desktop_workspace_execution.dart +++ b/lib/app/app_controller_desktop_workspace_execution.dart @@ -372,10 +372,8 @@ extension AppControllerDesktopWorkspaceExecution on AppController { if (normalizedSkillKey.isEmpty) { return; } - final importedKeys = assistantImportedSkillsForSession( - normalizedSessionKey, - ).map((item) => item.key).toSet(); - if (!importedKeys.contains(normalizedSkillKey)) { + final availableKeys = skills.map((item) => item.skillKey).toSet(); + if (!availableKeys.contains(normalizedSkillKey)) { return; } final nextSelected = List.from( diff --git a/lib/features/assistant/assistant_page_composer_skill_models.dart b/lib/features/assistant/assistant_page_composer_skill_models.dart index 8bd21e90..37f1108a 100644 --- a/lib/features/assistant/assistant_page_composer_skill_models.dart +++ b/lib/features/assistant/assistant_page_composer_skill_models.dart @@ -37,58 +37,13 @@ import 'assistant_page_composer_skill_picker.dart'; import 'assistant_page_composer_clipboard.dart'; import 'assistant_page_components_core.dart'; -const List fallbackSkillOptionsInternal = - [ - ComposerSkillOptionInternal( - key: '1password', - label: '1password', - description: '安全读取和注入本地凭据。', - sourceLabel: 'Local', - icon: Icons.auto_awesome_rounded, - ), - ComposerSkillOptionInternal( - key: 'xlsx', - label: 'xlsx', - description: '读取、整理和生成表格文件。', - sourceLabel: 'Local', - icon: Icons.auto_awesome_rounded, - ), - ComposerSkillOptionInternal( - key: 'web-processing', - label: '网页处理', - description: '打开网页、提取内容并完成网页操作。', - sourceLabel: 'Web', - icon: Icons.language_rounded, - ), - ComposerSkillOptionInternal( - key: 'apple-reminders', - label: 'apple-reminders', - description: '管理提醒事项和任务提醒。', - sourceLabel: 'Local', - icon: Icons.auto_awesome_rounded, - ), - ComposerSkillOptionInternal( - key: 'blogwatcher', - label: 'blogwatcher', - description: '跟踪博客更新并生成摘要。', - sourceLabel: 'Local', - icon: Icons.auto_awesome_rounded, - ), - ]; - ComposerSkillOptionInternal skillOptionFromGatewayInternal( GatewaySkillSummary skill, ) { - final normalizedKey = skill.skillKey.trim().toLowerCase(); - final normalizedName = skill.name.trim().toLowerCase(); - final isWebSkill = - normalizedKey.contains('browser') || - normalizedKey.contains('open-link') || - normalizedKey.contains('web') || - normalizedName.contains('browser') || - normalizedName.contains('网页'); - final label = isWebSkill ? '网页处理' : skill.name.trim(); - final key = isWebSkill ? 'web-processing' : normalizedKey; + final key = skill.skillKey.trim().isEmpty + ? skill.name.trim().toLowerCase() + : skill.skillKey.trim(); + final label = skill.name.trim().isEmpty ? key : skill.name.trim(); final sourceLabel = skill.source.trim().isEmpty ? 'Gateway' : skill.source; final description = skill.description.trim().isEmpty ? appText('可在当前任务中调用的技能。', 'Skill available in the current task.') @@ -99,22 +54,6 @@ ComposerSkillOptionInternal skillOptionFromGatewayInternal( label: label, description: description, sourceLabel: sourceLabel, - icon: isWebSkill ? Icons.language_rounded : Icons.auto_awesome_rounded, - ); -} - -ComposerSkillOptionInternal skillOptionFromThreadSkillInternal( - AssistantThreadSkillEntry skill, -) { - return ComposerSkillOptionInternal( - key: skill.key, - label: skill.label.trim().isEmpty ? skill.key : skill.label.trim(), - description: skill.description.trim().isEmpty - ? appText('已绑定到当前线程的本地技能。', 'Local skill bound to this thread.') - : skill.description.trim(), - sourceLabel: skill.sourceLabel.trim().isEmpty - ? skill.sourcePath - : skill.sourceLabel.trim(), icon: Icons.auto_awesome_rounded, ); } diff --git a/lib/features/assistant/assistant_page_state_actions.dart b/lib/features/assistant/assistant_page_state_actions.dart index dcdf862f..a261963c 100644 --- a/lib/features/assistant/assistant_page_state_actions.dart +++ b/lib/features/assistant/assistant_page_state_actions.dart @@ -262,17 +262,20 @@ extension AssistantPageStateActionsInternal on AssistantPageStateInternal { addOption(option); } - for (final option in fallbackSkillOptionsInternal) { - addOption(option); - } - return options; } List selectedSkillKeysForInternal(AppController controller) { - return controller.assistantSelectedSkillKeysForSession( - controller.currentSessionKey, - ); + final selected = + controller.taskThreadForSessionInternal(controller.currentSessionKey) + ?.selectedSkillKeys ?? + const []; + final availableKeys = availableSkillOptionsInternal(controller) + .map((option) => option.key) + .toSet(); + return selected + .where(availableKeys.contains) + .toList(growable: false); } List resolveSelectedSkillLabelsInternal(AppController controller) { diff --git a/test/runtime/assistant_execution_target_test.dart b/test/runtime/assistant_execution_target_test.dart index c562d3e8..95a17ce4 100644 --- a/test/runtime/assistant_execution_target_test.dart +++ b/test/runtime/assistant_execution_target_test.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:xworkmate/app/app_controller.dart'; import 'package:xworkmate/app/app_controller_desktop_external_acp_routing.dart'; +import 'package:xworkmate/features/assistant/assistant_page_composer_skill_models.dart'; import 'package:xworkmate/runtime/go_task_service_client.dart'; import 'package:xworkmate/runtime/runtime_models.dart'; import 'package:xworkmate/runtime/secure_config_store.dart'; @@ -213,6 +214,30 @@ void main() { }, ); + test( + 'bridge skill summaries preserve bridge key and name without remap', + () { + final option = skillOptionFromGatewayInternal( + const GatewaySkillSummary( + name: 'Browser Fetch', + description: 'Bridge-managed browser skill', + source: 'bridge', + skillKey: 'browser-fetch', + primaryEnv: null, + eligible: true, + disabled: false, + missingBins: [], + missingEnv: [], + missingConfig: [], + ), + ); + + expect(option.key, 'browser-fetch'); + expect(option.label, 'Browser Fetch'); + expect(option.description, 'Bridge-managed browser skill'); + }, + ); + test( 'locks the gateway provider catalog to the canonical openclaw contract', () {