Merge branch 'codex/bridge-contract-task-dialog-rewrite'
This commit is contained in:
commit
47d3fa36ed
@ -124,7 +124,6 @@ class AppController extends ChangeNotifier {
|
||||
SkillDirectoryAccessService? skillDirectoryAccessService,
|
||||
AccountRuntimeClient Function(String baseUrl)? accountClientFactory,
|
||||
List<String>? singleAgentSharedSkillScanRootOverrides,
|
||||
List<SingleAgentProvider>? availableSingleAgentProvidersOverride,
|
||||
ArisBundleRepository? arisBundleRepository,
|
||||
GoTaskServiceClient? goTaskServiceClient,
|
||||
MultiAgentMountManager? multiAgentMountManager,
|
||||
@ -197,8 +196,6 @@ class AppController extends ChangeNotifier {
|
||||
endpointResolver: resolveGatewayAcpEndpointInternal,
|
||||
authorizationResolver: resolveGatewayAcpAuthorizationHeaderInternal,
|
||||
);
|
||||
availableSingleAgentProvidersOverrideInternal =
|
||||
availableSingleAgentProvidersOverride;
|
||||
arisBundleRepositoryInternal =
|
||||
arisBundleRepository ?? ArisBundleRepository();
|
||||
runtimeCoordinatorInternal.attachDispatchResolver(
|
||||
@ -287,8 +284,6 @@ class AppController extends ChangeNotifier {
|
||||
late final SkillDirectoryAccessService skillDirectoryAccessServiceInternal;
|
||||
late final List<String>? singleAgentSharedSkillScanRootOverridesInternal;
|
||||
late final GatewayAcpClient gatewayAcpClientInternal;
|
||||
late final List<SingleAgentProvider>?
|
||||
availableSingleAgentProvidersOverrideInternal;
|
||||
late final ArisBundleRepository arisBundleRepositoryInternal;
|
||||
late final GoTaskServiceClient goTaskServiceClientInternal;
|
||||
late final MultiAgentOrchestrator multiAgentOrchestratorInternal;
|
||||
@ -584,16 +579,14 @@ class AppController extends ChangeNotifier {
|
||||
);
|
||||
|
||||
List<SingleAgentProvider> get configuredSingleAgentProviders =>
|
||||
normalizeSingleAgentProviderList(bridgeAdvertisedProvidersInternal);
|
||||
normalizeBridgeOwnedSingleAgentProviderList(
|
||||
bridgeAdvertisedProvidersInternal,
|
||||
);
|
||||
|
||||
List<SingleAgentProvider> get availableSingleAgentProviders =>
|
||||
availableSingleAgentProvidersOverrideInternal != null
|
||||
? normalizeSingleAgentProviderList(
|
||||
availableSingleAgentProvidersOverrideInternal!,
|
||||
)
|
||||
: configuredSingleAgentProviders
|
||||
.where(canUseSingleAgentProviderInternal)
|
||||
.toList(growable: false);
|
||||
configuredSingleAgentProviders
|
||||
.where(canUseSingleAgentProviderInternal)
|
||||
.toList(growable: false);
|
||||
|
||||
List<AssistantExecutionTarget> visibleAssistantExecutionTargets(
|
||||
Iterable<AssistantExecutionTarget> supportedTargets,
|
||||
@ -604,8 +597,7 @@ class AppController extends ChangeNotifier {
|
||||
availableSingleAgentProviders.isNotEmpty) {
|
||||
visible.add(AssistantExecutionTarget.singleAgent);
|
||||
}
|
||||
if (supported.contains(AssistantExecutionTarget.gateway) &&
|
||||
appUiState.isGatewayTargetSaved(AssistantExecutionTarget.gateway)) {
|
||||
if (supported.contains(AssistantExecutionTarget.gateway)) {
|
||||
visible.add(AssistantExecutionTarget.gateway);
|
||||
}
|
||||
if (!supportedTargets.contains(AssistantExecutionTarget.singleAgent) ||
|
||||
@ -624,10 +616,6 @@ class AppController extends ChangeNotifier {
|
||||
availableSingleAgentProviders.isNotEmpty;
|
||||
|
||||
bool canUseSingleAgentProviderInternal(SingleAgentProvider provider) {
|
||||
final override = availableSingleAgentProvidersOverrideInternal;
|
||||
if (override != null) {
|
||||
return !provider.isUnspecified && override.contains(provider);
|
||||
}
|
||||
if (provider.isUnspecified) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -736,7 +736,26 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
||||
}
|
||||
|
||||
Uri? resolveBridgeAcpEndpointInternal() {
|
||||
final uri = Uri.tryParse(kCanonicalBridgeAcpEndpoint);
|
||||
final endpoint =
|
||||
settingsControllerInternal
|
||||
.accountSyncState
|
||||
?.syncedDefaults
|
||||
.bridgeServerUrl
|
||||
.trim()
|
||||
.isNotEmpty ==
|
||||
true
|
||||
? settingsControllerInternal
|
||||
.accountSyncState!
|
||||
.syncedDefaults
|
||||
.bridgeServerUrl
|
||||
.trim()
|
||||
: settings
|
||||
.acpBridgeServerModeConfig
|
||||
.cloudSynced
|
||||
.remoteServerSummary
|
||||
.endpoint
|
||||
.trim();
|
||||
final uri = Uri.tryParse(endpoint);
|
||||
final scheme = uri?.scheme.trim().toLowerCase() ?? '';
|
||||
if (uri == null || !kSupportedExternalAcpEndpointSchemes.contains(scheme)) {
|
||||
return null;
|
||||
@ -781,20 +800,6 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
||||
return 'Bearer $bridgeToken';
|
||||
}
|
||||
}
|
||||
final profileIndex =
|
||||
gatewayProfileIndexMatchingEndpointInternal(endpoint) ??
|
||||
kGatewayRemoteProfileIndex;
|
||||
final gatewayToken = await settingsControllerInternal
|
||||
.loadEffectiveGatewayToken(profileIndex: profileIndex);
|
||||
if (gatewayToken.isNotEmpty) {
|
||||
return 'Bearer $gatewayToken';
|
||||
}
|
||||
final gatewayPassword = await settingsControllerInternal
|
||||
.loadEffectiveGatewayPassword(profileIndex: profileIndex);
|
||||
if (gatewayPassword.isNotEmpty) {
|
||||
final encoded = base64Encode(utf8.encode('operator:$gatewayPassword'));
|
||||
return 'Basic $encoded';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@ -86,9 +86,7 @@ Future<void> sendSingleAgentMessageDesktopGoTaskFlowInternal(
|
||||
sessionKey,
|
||||
null,
|
||||
)
|
||||
: controller.singleAgentNeedsAiGatewayConfigurationForSession(
|
||||
sessionKey,
|
||||
)
|
||||
: controller.singleAgentNeedsBridgeProviderForSession(sessionKey)
|
||||
? singleAgentUnavailableLabelDesktopInternal(
|
||||
controller,
|
||||
sessionKey,
|
||||
@ -124,7 +122,6 @@ Future<void> sendSingleAgentMessageDesktopGoTaskFlowInternal(
|
||||
return;
|
||||
}
|
||||
|
||||
final aiGatewayApiKey = await controller.loadAiGatewayApiKey();
|
||||
if (!effectiveProvider.isUnspecified) {
|
||||
appendSingleAgentRuntimeStatusDesktopInternal(
|
||||
controller,
|
||||
@ -161,8 +158,6 @@ Future<void> sendSingleAgentMessageDesktopGoTaskFlowInternal(
|
||||
selectedSkills: selectedSkills,
|
||||
inlineAttachments: attachments,
|
||||
localAttachments: localAttachments,
|
||||
aiGatewayBaseUrl: controller.aiGatewayUrl,
|
||||
aiGatewayApiKey: aiGatewayApiKey,
|
||||
agentId: '',
|
||||
metadata: const <String, dynamic>{},
|
||||
routing: routing,
|
||||
|
||||
@ -91,7 +91,7 @@ String singleAgentUnavailableLabelDesktopInternal(
|
||||
'This thread is pinned to ${selection.label}: $detail XWorkmate will not reroute to another bridge provider automatically. Switch to an available provider manually.',
|
||||
);
|
||||
}
|
||||
if (controller.singleAgentNeedsAiGatewayConfigurationForSession(
|
||||
if (controller.singleAgentNeedsBridgeProviderForSession(
|
||||
normalizedSessionKey,
|
||||
)) {
|
||||
return detail.isEmpty
|
||||
|
||||
@ -306,6 +306,15 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
recomputeTasksInternal();
|
||||
notifyIfActiveInternal();
|
||||
try {
|
||||
if (resolveExternalAcpEndpointForTargetInternal(currentTarget) ==
|
||||
null) {
|
||||
throw StateError(
|
||||
appText(
|
||||
'BRIDGE_SERVER_URL 未配置,无法启动任务对话。',
|
||||
'BRIDGE_SERVER_URL is unavailable, so task chat cannot start.',
|
||||
),
|
||||
);
|
||||
}
|
||||
final dispatch = await codeAgentNodeOrchestratorInternal
|
||||
.buildGatewayDispatch(buildCodeAgentNodeStateInternal());
|
||||
final result = await goTaskServiceClientInternal.executeTask(
|
||||
@ -320,8 +329,6 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
selectedSkills: selectedSkillLabels,
|
||||
inlineAttachments: attachments,
|
||||
localAttachments: localAttachments,
|
||||
aiGatewayBaseUrl: aiGatewayUrl,
|
||||
aiGatewayApiKey: await loadAiGatewayApiKey(),
|
||||
agentId: dispatch.agentId ?? '',
|
||||
metadata: dispatch.metadata,
|
||||
routing: buildExternalAcpRoutingForSessionInternal(sessionKey),
|
||||
|
||||
@ -277,7 +277,7 @@ extension AppControllerDesktopThreadSessions on AppController {
|
||||
SingleAgentProvider? get currentSingleAgentResolvedProvider =>
|
||||
singleAgentResolvedProviderForSession(currentSessionKey);
|
||||
|
||||
bool singleAgentNeedsAiGatewayConfigurationForSession(String sessionKey) {
|
||||
bool singleAgentNeedsBridgeProviderForSession(String sessionKey) {
|
||||
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
|
||||
sessionKey,
|
||||
);
|
||||
@ -288,8 +288,8 @@ extension AppControllerDesktopThreadSessions on AppController {
|
||||
return !hasAnyAvailableSingleAgentProvider;
|
||||
}
|
||||
|
||||
bool get currentSingleAgentNeedsAiGatewayConfiguration =>
|
||||
singleAgentNeedsAiGatewayConfigurationForSession(currentSessionKey);
|
||||
bool get currentSingleAgentNeedsBridgeProvider =>
|
||||
singleAgentNeedsBridgeProviderForSession(currentSessionKey);
|
||||
|
||||
bool singleAgentHasResolvedProviderForSession(String sessionKey) {
|
||||
return singleAgentResolvedProviderForSession(sessionKey) != null;
|
||||
@ -419,7 +419,7 @@ extension AppControllerDesktopThreadSessions on AppController {
|
||||
'${provider.label} 当前不可用,请改成 Bridge 当前可用的 Provider。',
|
||||
'${provider.label} is unavailable. Switch to a provider currently advertised by the bridge.',
|
||||
)
|
||||
: singleAgentNeedsAiGatewayConfigurationForSession(
|
||||
: singleAgentNeedsBridgeProviderForSession(
|
||||
normalizedSessionKey,
|
||||
)
|
||||
? appText(
|
||||
|
||||
@ -107,9 +107,34 @@ Future<void> runMultiAgentCollaborationThreadSessionInternal(
|
||||
? 'main'
|
||||
: controller.currentSessionKey;
|
||||
await controller.enqueueThreadTurnInternal<void>(sessionKey, () async {
|
||||
final aiGatewayApiKey = await loadAiGatewayApiKeyThreadSessionInternal(
|
||||
controller,
|
||||
);
|
||||
if (controller.resolveExternalAcpEndpointForTargetInternal(
|
||||
controller.assistantExecutionTargetForSession(sessionKey),
|
||||
) ==
|
||||
null) {
|
||||
final error = StateError(
|
||||
appText(
|
||||
'BRIDGE_SERVER_URL 未配置,无法启动任务对话。',
|
||||
'BRIDGE_SERVER_URL is unavailable, so task chat cannot start.',
|
||||
),
|
||||
);
|
||||
controller.appendLocalSessionMessageInternal(
|
||||
sessionKey,
|
||||
GatewayChatMessage(
|
||||
id: controller.nextLocalMessageIdInternal(),
|
||||
role: 'assistant',
|
||||
text: error.message.toString(),
|
||||
timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
toolCallId: null,
|
||||
toolName: 'Multi-Agent',
|
||||
stopReason: null,
|
||||
pending: false,
|
||||
error: true,
|
||||
),
|
||||
);
|
||||
controller.recomputeTasksInternal();
|
||||
controller.notifyIfActiveInternal();
|
||||
throw error;
|
||||
}
|
||||
await controller.ensureDesktopTaskThreadBindingInternal(
|
||||
sessionKey,
|
||||
executionTarget: controller.assistantExecutionTargetForSession(
|
||||
@ -172,8 +197,6 @@ Future<void> runMultiAgentCollaborationThreadSessionInternal(
|
||||
selectedSkills: selectedSkillLabels,
|
||||
inlineAttachments: const <GatewayChatAttachmentPayload>[],
|
||||
localAttachments: attachments,
|
||||
aiGatewayBaseUrl: controller.aiGatewayUrl,
|
||||
aiGatewayApiKey: aiGatewayApiKey,
|
||||
agentId: '',
|
||||
metadata: const <String, dynamic>{},
|
||||
routingHint: 'gateway',
|
||||
|
||||
@ -502,8 +502,8 @@ class AssistantEmptyStateInternal extends StatelessWidget {
|
||||
final connectionState = controller.currentAssistantConnectionState;
|
||||
final singleAgent = connectionState.isSingleAgent;
|
||||
final connected = connectionState.connected;
|
||||
final singleAgentNeedsAiGateway =
|
||||
controller.currentSingleAgentNeedsAiGatewayConfiguration;
|
||||
final singleAgentNeedsBridgeProvider =
|
||||
controller.currentSingleAgentNeedsBridgeProvider;
|
||||
final singleAgentSuggestsAcpSwitch =
|
||||
controller.currentSingleAgentShouldSuggestAcpSwitch;
|
||||
final providerLabel = controller.currentSingleAgentProvider.label;
|
||||
@ -511,7 +511,7 @@ class AssistantEmptyStateInternal extends StatelessWidget {
|
||||
final title = singleAgent
|
||||
? connected
|
||||
? appText('开始智能体任务', 'Start an agent task')
|
||||
: singleAgentNeedsAiGateway
|
||||
: singleAgentNeedsBridgeProvider
|
||||
? appText(
|
||||
'先配置 Bridge Provider',
|
||||
'Configure a bridge provider first',
|
||||
@ -536,7 +536,7 @@ class AssistantEmptyStateInternal extends StatelessWidget {
|
||||
'当前线程固定为 $providerLabel,但它在这台设备上不可用。请改成 Bridge 当前可用的 Provider。',
|
||||
'This thread is pinned to $providerLabel, but it is unavailable on this device. Switch to a provider currently advertised by the bridge.',
|
||||
)
|
||||
: singleAgentNeedsAiGateway
|
||||
: singleAgentNeedsBridgeProvider
|
||||
? appText(
|
||||
'请先在 设置 -> 集成 中配置并同步可用的外部 Agent 连接,然后再继续当前任务。',
|
||||
'Configure and sync an available external agent connection in Settings -> Integrations before continuing this task.',
|
||||
@ -602,7 +602,7 @@ class AssistantEmptyStateInternal extends StatelessWidget {
|
||||
onPressed: connected
|
||||
? onFocusComposer
|
||||
: singleAgent
|
||||
? singleAgentNeedsAiGateway
|
||||
? singleAgentNeedsBridgeProvider
|
||||
? onOpenAiGatewaySettings
|
||||
: onFocusComposer
|
||||
: reconnectAvailable
|
||||
@ -614,7 +614,7 @@ class AssistantEmptyStateInternal extends StatelessWidget {
|
||||
connected
|
||||
? Icons.edit_rounded
|
||||
: singleAgent
|
||||
? singleAgentNeedsAiGateway
|
||||
? singleAgentNeedsBridgeProvider
|
||||
? Icons.tune_rounded
|
||||
: Icons.smart_toy_outlined
|
||||
: reconnectAvailable
|
||||
@ -625,7 +625,7 @@ class AssistantEmptyStateInternal extends StatelessWidget {
|
||||
connected
|
||||
? appText('开始输入', 'Start typing')
|
||||
: singleAgent
|
||||
? singleAgentNeedsAiGateway
|
||||
? singleAgentNeedsBridgeProvider
|
||||
? appText('打开配置中心', 'Open settings')
|
||||
: appText('查看线程工具栏', 'Open toolbar')
|
||||
: reconnectAvailable
|
||||
@ -644,7 +644,7 @@ class AssistantEmptyStateInternal extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
if (!connected &&
|
||||
(!singleAgent || singleAgentNeedsAiGateway))
|
||||
(!singleAgent || singleAgentNeedsBridgeProvider))
|
||||
OutlinedButton.icon(
|
||||
onPressed: singleAgent
|
||||
? onOpenAiGatewaySettings
|
||||
|
||||
@ -63,18 +63,12 @@ class ExternalCodeAgentAcpDesktopTransport
|
||||
required String taskPrompt,
|
||||
required String workingDirectory,
|
||||
required ExternalCodeAgentAcpRoutingConfig routing,
|
||||
String aiGatewayBaseUrl = '',
|
||||
String aiGatewayApiKey = '',
|
||||
}) async {
|
||||
final response = await _client.request(
|
||||
method: 'xworkmate.routing.resolve',
|
||||
params: <String, dynamic>{
|
||||
'taskPrompt': taskPrompt,
|
||||
'workingDirectory': workingDirectory.trim(),
|
||||
if (aiGatewayBaseUrl.trim().isNotEmpty)
|
||||
'aiGatewayBaseUrl': aiGatewayBaseUrl.trim(),
|
||||
if (aiGatewayApiKey.trim().isNotEmpty)
|
||||
'aiGatewayApiKey': aiGatewayApiKey.trim(),
|
||||
'routing': routing.toJson(),
|
||||
},
|
||||
endpointOverride: _endpointResolver(AssistantExecutionTarget.singleAgent),
|
||||
@ -218,6 +212,6 @@ class ExternalCodeAgentAcpDesktopTransport
|
||||
providers.add(provider);
|
||||
}
|
||||
}
|
||||
return normalizeSingleAgentProviderList(providers);
|
||||
return normalizeBridgeOwnedSingleAgentProviderList(providers);
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,8 +69,6 @@ class GatewayAcpMultiAgentRequest {
|
||||
required this.workingDirectory,
|
||||
required this.attachments,
|
||||
required this.selectedSkills,
|
||||
required this.aiGatewayBaseUrl,
|
||||
required this.aiGatewayApiKey,
|
||||
required this.resumeSession,
|
||||
});
|
||||
|
||||
@ -80,8 +78,6 @@ class GatewayAcpMultiAgentRequest {
|
||||
final String workingDirectory;
|
||||
final List<CollaborationAttachment> attachments;
|
||||
final List<String> selectedSkills;
|
||||
final String aiGatewayBaseUrl;
|
||||
final String aiGatewayApiKey;
|
||||
final bool resumeSession;
|
||||
}
|
||||
|
||||
@ -162,7 +158,7 @@ class GatewayAcpClient {
|
||||
providers.add(provider);
|
||||
}
|
||||
}
|
||||
return normalizeSingleAgentProviderList(providers);
|
||||
return normalizeBridgeOwnedSingleAgentProviderList(providers);
|
||||
}
|
||||
|
||||
Stream<MultiAgentRunEvent> runMultiAgent(
|
||||
@ -196,8 +192,6 @@ class GatewayAcpClient {
|
||||
)
|
||||
.toList(growable: false),
|
||||
'selectedSkills': request.selectedSkills,
|
||||
'aiGatewayBaseUrl': request.aiGatewayBaseUrl,
|
||||
'aiGatewayApiKey': request.aiGatewayApiKey,
|
||||
},
|
||||
);
|
||||
var lastSequence = -1;
|
||||
|
||||
@ -219,8 +219,6 @@ class GoTaskServiceRequest {
|
||||
required this.selectedSkills,
|
||||
required this.inlineAttachments,
|
||||
required this.localAttachments,
|
||||
required this.aiGatewayBaseUrl,
|
||||
required this.aiGatewayApiKey,
|
||||
required this.agentId,
|
||||
required this.metadata,
|
||||
this.routing,
|
||||
@ -242,8 +240,6 @@ class GoTaskServiceRequest {
|
||||
final List<String> selectedSkills;
|
||||
final List<GatewayChatAttachmentPayload> inlineAttachments;
|
||||
final List<CollaborationAttachment> localAttachments;
|
||||
final String aiGatewayBaseUrl;
|
||||
final String aiGatewayApiKey;
|
||||
final String agentId;
|
||||
final Map<String, dynamic> metadata;
|
||||
final ExternalCodeAgentAcpRoutingConfig? routing;
|
||||
@ -328,10 +324,6 @@ class GoTaskServiceRequest {
|
||||
'remoteWorkingDirectoryHint': remoteWorkingDirectoryHint.trim(),
|
||||
if (model.trim().isNotEmpty) 'model': model.trim(),
|
||||
if (thinking.trim().isNotEmpty) 'thinking': thinking.trim(),
|
||||
if (aiGatewayBaseUrl.trim().isNotEmpty)
|
||||
'aiGatewayBaseUrl': aiGatewayBaseUrl.trim(),
|
||||
if (aiGatewayApiKey.trim().isNotEmpty)
|
||||
'aiGatewayApiKey': aiGatewayApiKey.trim(),
|
||||
'routing': resolvedRouting.toJson(),
|
||||
if (routingHint.trim().isNotEmpty) 'routingHint': routingHint.trim(),
|
||||
'requestedExecutionTarget': normalizedTarget.promptValue,
|
||||
@ -625,8 +617,6 @@ abstract class ExternalCodeAgentAcpTransport {
|
||||
required String taskPrompt,
|
||||
required String workingDirectory,
|
||||
required ExternalCodeAgentAcpRoutingConfig routing,
|
||||
String aiGatewayBaseUrl = '',
|
||||
String aiGatewayApiKey = '',
|
||||
});
|
||||
|
||||
Future<GoTaskServiceResult> executeTask(
|
||||
@ -663,8 +653,6 @@ abstract class GoTaskServiceClient {
|
||||
required String taskPrompt,
|
||||
required String workingDirectory,
|
||||
required ExternalCodeAgentAcpRoutingConfig routing,
|
||||
String aiGatewayBaseUrl = '',
|
||||
String aiGatewayApiKey = '',
|
||||
});
|
||||
|
||||
Future<GoTaskServiceResult> executeTask(
|
||||
|
||||
@ -30,14 +30,10 @@ class DesktopGoTaskService implements GoTaskServiceClient {
|
||||
required String taskPrompt,
|
||||
required String workingDirectory,
|
||||
required ExternalCodeAgentAcpRoutingConfig routing,
|
||||
String aiGatewayBaseUrl = '',
|
||||
String aiGatewayApiKey = '',
|
||||
}) => _acpTransport.resolveExternalAcpRouting(
|
||||
taskPrompt: taskPrompt,
|
||||
workingDirectory: workingDirectory,
|
||||
routing: routing,
|
||||
aiGatewayBaseUrl: aiGatewayBaseUrl,
|
||||
aiGatewayApiKey: aiGatewayApiKey,
|
||||
);
|
||||
|
||||
@override
|
||||
|
||||
@ -147,8 +147,6 @@ class MultiAgentOrchestrator extends ChangeNotifier {
|
||||
required String workingDirectory,
|
||||
List<CollaborationAttachment> attachments = const [],
|
||||
List<String> selectedSkills = const [],
|
||||
String aiGatewayBaseUrl = '',
|
||||
String aiGatewayApiKey = '',
|
||||
void Function(MultiAgentRunEvent event)? onEvent,
|
||||
}) async {
|
||||
assertEmbeddedProcessesAllowedInternal();
|
||||
@ -192,8 +190,6 @@ class MultiAgentOrchestrator extends ChangeNotifier {
|
||||
taskPrompt,
|
||||
preset: preset,
|
||||
selectedSkills: selectedSkills,
|
||||
aiGatewayBaseUrl: aiGatewayBaseUrl,
|
||||
aiGatewayApiKey: aiGatewayApiKey,
|
||||
);
|
||||
steps.add(
|
||||
CollaborationStep(
|
||||
@ -242,8 +238,6 @@ class MultiAgentOrchestrator extends ChangeNotifier {
|
||||
attachments,
|
||||
preset: preset,
|
||||
selectedSkills: selectedSkills,
|
||||
aiGatewayBaseUrl: aiGatewayBaseUrl,
|
||||
aiGatewayApiKey: aiGatewayApiKey,
|
||||
);
|
||||
steps.add(
|
||||
CollaborationStep(
|
||||
@ -286,8 +280,6 @@ class MultiAgentOrchestrator extends ChangeNotifier {
|
||||
final testerResult = await runTesterInternal(
|
||||
engineerResult.codeOutput,
|
||||
preset: preset,
|
||||
aiGatewayBaseUrl: aiGatewayBaseUrl,
|
||||
aiGatewayApiKey: aiGatewayApiKey,
|
||||
);
|
||||
steps.add(
|
||||
CollaborationStep(
|
||||
@ -335,8 +327,6 @@ class MultiAgentOrchestrator extends ChangeNotifier {
|
||||
testerResult.feedback,
|
||||
workingDirectory,
|
||||
preset: preset,
|
||||
aiGatewayBaseUrl: aiGatewayBaseUrl,
|
||||
aiGatewayApiKey: aiGatewayApiKey,
|
||||
);
|
||||
steps.add(
|
||||
CollaborationStep(
|
||||
@ -352,8 +342,6 @@ class MultiAgentOrchestrator extends ChangeNotifier {
|
||||
final reReview = await runTesterInternal(
|
||||
fixedResult.codeOutput,
|
||||
preset: preset,
|
||||
aiGatewayBaseUrl: aiGatewayBaseUrl,
|
||||
aiGatewayApiKey: aiGatewayApiKey,
|
||||
);
|
||||
steps.add(
|
||||
CollaborationStep(
|
||||
|
||||
@ -15,23 +15,12 @@ import 'multi_agent_orchestrator_workflow.dart';
|
||||
import 'multi_agent_orchestrator_core.dart';
|
||||
|
||||
extension MultiAgentOrchestratorSupportInternal on MultiAgentOrchestrator {
|
||||
String openAiCompatibleBaseUrlInternal({required String aiGatewayBaseUrl}) {
|
||||
if (configInternal.aiGatewayInjectionPolicy !=
|
||||
AiGatewayInjectionPolicy.disabled &&
|
||||
aiGatewayBaseUrl.trim().isNotEmpty) {
|
||||
final normalized = aiGatewayBaseUrl.trim();
|
||||
return normalized.endsWith('/v1') ? normalized : '$normalized/v1';
|
||||
}
|
||||
String openAiCompatibleBaseUrlInternal() {
|
||||
final normalized = configInternal.ollamaEndpoint.trim();
|
||||
return normalized.endsWith('/v1') ? normalized : '$normalized/v1';
|
||||
}
|
||||
|
||||
String openAiCompatibleApiKeyInternal({required String aiGatewayApiKey}) {
|
||||
if (configInternal.aiGatewayInjectionPolicy !=
|
||||
AiGatewayInjectionPolicy.disabled &&
|
||||
aiGatewayApiKey.trim().isNotEmpty) {
|
||||
return aiGatewayApiKey.trim();
|
||||
}
|
||||
String openAiCompatibleApiKeyInternal() {
|
||||
return 'ollama';
|
||||
}
|
||||
|
||||
@ -236,27 +225,8 @@ extension MultiAgentOrchestratorSupportInternal on MultiAgentOrchestrator {
|
||||
}
|
||||
|
||||
/// 构建 Ollama 环境变量
|
||||
Map<String, String> buildCliEnvVarsInternal({
|
||||
required String tool,
|
||||
required String aiGatewayBaseUrl,
|
||||
required String aiGatewayApiKey,
|
||||
}) {
|
||||
Map<String, String> buildCliEnvVarsInternal({required String tool}) {
|
||||
final baseEnv = <String, String>{...Platform.environment};
|
||||
if (configInternal.aiGatewayInjectionPolicy !=
|
||||
AiGatewayInjectionPolicy.disabled &&
|
||||
aiGatewayBaseUrl.trim().isNotEmpty &&
|
||||
aiGatewayApiKey.trim().isNotEmpty) {
|
||||
baseEnv['OPENAI_BASE_URL'] = aiGatewayBaseUrl.trim();
|
||||
baseEnv['OPENAI_API_KEY'] = aiGatewayApiKey.trim();
|
||||
baseEnv['OLLAMA_BASE_URL'] = aiGatewayBaseUrl.trim();
|
||||
baseEnv['OLLAMA_HOST'] = aiGatewayBaseUrl.trim();
|
||||
if (tool == 'claude') {
|
||||
baseEnv['ANTHROPIC_BASE_URL'] = aiGatewayBaseUrl.trim();
|
||||
baseEnv['ANTHROPIC_AUTH_TOKEN'] = aiGatewayApiKey.trim();
|
||||
baseEnv['ANTHROPIC_API_KEY'] = aiGatewayApiKey.trim();
|
||||
}
|
||||
return baseEnv;
|
||||
}
|
||||
final ollamaEndpoint = configInternal.ollamaEndpoint.trim();
|
||||
if (ollamaEndpoint.isNotEmpty) {
|
||||
baseEnv['OLLAMA_BASE_URL'] = ollamaEndpoint;
|
||||
|
||||
@ -20,8 +20,6 @@ extension MultiAgentOrchestratorWorkflowInternal on MultiAgentOrchestrator {
|
||||
String task, {
|
||||
required FrameworkPreset preset,
|
||||
required List<String> selectedSkills,
|
||||
required String aiGatewayBaseUrl,
|
||||
required String aiGatewayApiKey,
|
||||
}) async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
@ -50,8 +48,6 @@ extension MultiAgentOrchestratorWorkflowInternal on MultiAgentOrchestrator {
|
||||
instructionBlock,
|
||||
),
|
||||
cwd: '',
|
||||
aiGatewayBaseUrl: aiGatewayBaseUrl,
|
||||
aiGatewayApiKey: aiGatewayApiKey,
|
||||
);
|
||||
stopwatch.stop();
|
||||
|
||||
@ -91,8 +87,6 @@ extension MultiAgentOrchestratorWorkflowInternal on MultiAgentOrchestrator {
|
||||
List<CollaborationAttachment> attachments, {
|
||||
required FrameworkPreset preset,
|
||||
required List<String> selectedSkills,
|
||||
required String aiGatewayBaseUrl,
|
||||
required String aiGatewayApiKey,
|
||||
}) async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
final tool = await resolveToolForRoleInternal(
|
||||
@ -139,8 +133,6 @@ ${selectedSkills.isEmpty ? '- 无' : selectedSkills.map((item) => '- $item').joi
|
||||
),
|
||||
prompt: prompt,
|
||||
cwd: workingDirectory,
|
||||
aiGatewayBaseUrl: aiGatewayBaseUrl,
|
||||
aiGatewayApiKey: aiGatewayApiKey,
|
||||
);
|
||||
stopwatch.stop();
|
||||
|
||||
@ -156,8 +148,6 @@ ${selectedSkills.isEmpty ? '- 无' : selectedSkills.map((item) => '- $item').joi
|
||||
Future<TesterResult> runTesterInternal(
|
||||
String codeOutput, {
|
||||
required FrameworkPreset preset,
|
||||
required String aiGatewayBaseUrl,
|
||||
required String aiGatewayApiKey,
|
||||
}) async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
final tool = await resolveToolForRoleInternal(
|
||||
@ -212,8 +202,6 @@ ${codeOutput.length > 4000 ? '${codeOutput.substring(0, 4000)}\n...[代码已截
|
||||
model: testerModel,
|
||||
prompt: prompt,
|
||||
cwd: '',
|
||||
aiGatewayBaseUrl: aiGatewayBaseUrl,
|
||||
aiGatewayApiKey: aiGatewayApiKey,
|
||||
);
|
||||
stopwatch.stop();
|
||||
|
||||
@ -234,8 +222,6 @@ ${codeOutput.length > 4000 ? '${codeOutput.substring(0, 4000)}\n...[代码已截
|
||||
String feedback,
|
||||
String workingDirectory, {
|
||||
required FrameworkPreset preset,
|
||||
required String aiGatewayBaseUrl,
|
||||
required String aiGatewayApiKey,
|
||||
}) async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
final tool = await resolveToolForRoleInternal(
|
||||
@ -272,8 +258,6 @@ $originalCode
|
||||
),
|
||||
prompt: prompt,
|
||||
cwd: workingDirectory,
|
||||
aiGatewayBaseUrl: aiGatewayBaseUrl,
|
||||
aiGatewayApiKey: aiGatewayApiKey,
|
||||
);
|
||||
stopwatch.stop();
|
||||
|
||||
@ -292,8 +276,6 @@ $originalCode
|
||||
required String model,
|
||||
required String prompt,
|
||||
required String cwd,
|
||||
required String aiGatewayBaseUrl,
|
||||
required String aiGatewayApiKey,
|
||||
}) async {
|
||||
late final List<String> args;
|
||||
late final String command;
|
||||
@ -306,11 +288,7 @@ $originalCode
|
||||
switch (tool) {
|
||||
case 'claude':
|
||||
command = useOllamaLaunch ? 'ollama' : resolveCliPathInternal('claude');
|
||||
envVars = buildCliEnvVarsInternal(
|
||||
tool: tool,
|
||||
aiGatewayBaseUrl: aiGatewayBaseUrl,
|
||||
aiGatewayApiKey: aiGatewayApiKey,
|
||||
);
|
||||
envVars = buildCliEnvVarsInternal(tool: tool);
|
||||
if (useOllamaLaunch) {
|
||||
args = buildOllamaLaunchArgsInternal(
|
||||
tool: tool,
|
||||
@ -327,11 +305,7 @@ $originalCode
|
||||
|
||||
case 'codex':
|
||||
command = useOllamaLaunch ? 'ollama' : resolveCliPathInternal('codex');
|
||||
envVars = buildCliEnvVarsInternal(
|
||||
tool: tool,
|
||||
aiGatewayBaseUrl: aiGatewayBaseUrl,
|
||||
aiGatewayApiKey: aiGatewayApiKey,
|
||||
);
|
||||
envVars = buildCliEnvVarsInternal(tool: tool);
|
||||
if (useOllamaLaunch) {
|
||||
args = buildOllamaLaunchArgsInternal(
|
||||
tool: tool,
|
||||
@ -364,11 +338,7 @@ $originalCode
|
||||
|
||||
case 'gemini':
|
||||
command = resolveCliPathInternal('gemini');
|
||||
envVars = buildCliEnvVarsInternal(
|
||||
tool: tool,
|
||||
aiGatewayBaseUrl: aiGatewayBaseUrl,
|
||||
aiGatewayApiKey: aiGatewayApiKey,
|
||||
);
|
||||
envVars = buildCliEnvVarsInternal(tool: tool);
|
||||
if (model.isNotEmpty) {
|
||||
args = ['--model', model, '-p', prompt];
|
||||
} else {
|
||||
@ -380,11 +350,7 @@ $originalCode
|
||||
command = useOllamaLaunch
|
||||
? 'ollama'
|
||||
: resolveCliPathInternal('opencode');
|
||||
envVars = buildCliEnvVarsInternal(
|
||||
tool: tool,
|
||||
aiGatewayBaseUrl: aiGatewayBaseUrl,
|
||||
aiGatewayApiKey: aiGatewayApiKey,
|
||||
);
|
||||
envVars = buildCliEnvVarsInternal(tool: tool);
|
||||
args = useOllamaLaunch
|
||||
? buildOllamaLaunchArgsInternal(
|
||||
tool: tool,
|
||||
@ -408,13 +374,7 @@ $originalCode
|
||||
|
||||
final cliAvailable = await binaryExistsInternal(command);
|
||||
if (configInternal.usesAris && !cliAvailable) {
|
||||
return runArisFallbackInternal(
|
||||
role: role,
|
||||
model: model,
|
||||
prompt: prompt,
|
||||
aiGatewayBaseUrl: aiGatewayBaseUrl,
|
||||
aiGatewayApiKey: aiGatewayApiKey,
|
||||
);
|
||||
return runArisFallbackInternal(role: role, model: model, prompt: prompt);
|
||||
}
|
||||
|
||||
try {
|
||||
@ -464,8 +424,6 @@ $originalCode
|
||||
role: role,
|
||||
model: model,
|
||||
prompt: prompt,
|
||||
aiGatewayBaseUrl: aiGatewayBaseUrl,
|
||||
aiGatewayApiKey: aiGatewayApiKey,
|
||||
);
|
||||
}
|
||||
return cliResult;
|
||||
@ -476,8 +434,6 @@ $originalCode
|
||||
role: role,
|
||||
model: model,
|
||||
prompt: prompt,
|
||||
aiGatewayBaseUrl: aiGatewayBaseUrl,
|
||||
aiGatewayApiKey: aiGatewayApiKey,
|
||||
);
|
||||
}
|
||||
return CliResult(output: '', error: e.toString(), exitCode: -1);
|
||||
@ -606,15 +562,11 @@ ${selectedSkills.isEmpty ? '- 无' : selectedSkills.map((item) => '- $item').joi
|
||||
required MultiAgentRole role,
|
||||
required String model,
|
||||
required String prompt,
|
||||
required String aiGatewayBaseUrl,
|
||||
required String aiGatewayApiKey,
|
||||
}) async {
|
||||
if (role == MultiAgentRole.testerDoc) {
|
||||
final viaLlmChat = await runArisTesterViaLlmChatInternal(
|
||||
model: model,
|
||||
prompt: prompt,
|
||||
aiGatewayBaseUrl: aiGatewayBaseUrl,
|
||||
aiGatewayApiKey: aiGatewayApiKey,
|
||||
);
|
||||
if (viaLlmChat.success) {
|
||||
return viaLlmChat;
|
||||
@ -624,23 +576,17 @@ ${selectedSkills.isEmpty ? '- 无' : selectedSkills.map((item) => '- $item').joi
|
||||
role: role,
|
||||
model: model,
|
||||
prompt: prompt,
|
||||
aiGatewayBaseUrl: aiGatewayBaseUrl,
|
||||
aiGatewayApiKey: aiGatewayApiKey,
|
||||
);
|
||||
}
|
||||
|
||||
Future<CliResult> runArisTesterViaLlmChatInternal({
|
||||
required String model,
|
||||
required String prompt,
|
||||
required String aiGatewayBaseUrl,
|
||||
required String aiGatewayApiKey,
|
||||
}) async {
|
||||
return runOpenAiCompatiblePromptInternal(
|
||||
role: MultiAgentRole.testerDoc,
|
||||
model: model,
|
||||
prompt: prompt,
|
||||
aiGatewayBaseUrl: aiGatewayBaseUrl,
|
||||
aiGatewayApiKey: aiGatewayApiKey,
|
||||
);
|
||||
}
|
||||
|
||||
@ -655,8 +601,6 @@ ${selectedSkills.isEmpty ? '- 无' : selectedSkills.map((item) => '- $item').joi
|
||||
model: model,
|
||||
prompt: prompt,
|
||||
cwd: '',
|
||||
aiGatewayBaseUrl: '',
|
||||
aiGatewayApiKey: '',
|
||||
);
|
||||
}
|
||||
return CliResult(
|
||||
@ -670,21 +614,19 @@ ${selectedSkills.isEmpty ? '- 无' : selectedSkills.map((item) => '- $item').joi
|
||||
required MultiAgentRole role,
|
||||
required String model,
|
||||
required String prompt,
|
||||
required String aiGatewayBaseUrl,
|
||||
required String aiGatewayApiKey,
|
||||
}) async {
|
||||
final client = httpClientFactoryInternal();
|
||||
activeHttpClientInternal = client;
|
||||
try {
|
||||
final request = await client.postUrl(
|
||||
Uri.parse(
|
||||
'${openAiCompatibleBaseUrlInternal(aiGatewayBaseUrl: aiGatewayBaseUrl).replaceAll(RegExp(r'/$'), '')}/chat/completions',
|
||||
'${openAiCompatibleBaseUrlInternal().replaceAll(RegExp(r'/$'), '')}/chat/completions',
|
||||
),
|
||||
);
|
||||
request.headers.set(HttpHeaders.contentTypeHeader, 'application/json');
|
||||
request.headers.set(
|
||||
HttpHeaders.authorizationHeader,
|
||||
'Bearer ${openAiCompatibleApiKeyInternal(aiGatewayApiKey: aiGatewayApiKey)}',
|
||||
'Bearer ${openAiCompatibleApiKeyInternal()}',
|
||||
);
|
||||
request.add(
|
||||
utf8.encode(
|
||||
|
||||
@ -280,7 +280,7 @@ Future<AccountSyncResult> syncAccountSettingsInternal(
|
||||
value: bridgeToken,
|
||||
);
|
||||
|
||||
final bridgeServerUrl = bridgeServerUrlOverride.trim().isNotEmpty
|
||||
final resolvedBridgeServerUrl = bridgeServerUrlOverride.trim().isNotEmpty
|
||||
? bridgeServerUrlOverride.trim()
|
||||
: controller.accountSyncStateInternal?.syncedDefaults.bridgeServerUrl
|
||||
.trim()
|
||||
@ -294,20 +294,33 @@ Future<AccountSyncResult> syncAccountSettingsInternal(
|
||||
.cloudSynced
|
||||
.remoteServerSummary
|
||||
.endpoint
|
||||
.trim()
|
||||
.isNotEmpty
|
||||
? controller
|
||||
.snapshotInternal
|
||||
.acpBridgeServerModeConfig
|
||||
.cloudSynced
|
||||
.remoteServerSummary
|
||||
.endpoint
|
||||
.trim()
|
||||
: '';
|
||||
final resolvedBridgeServerUrl =
|
||||
isSupportedExternalAcpEndpoint(bridgeServerUrl)
|
||||
? bridgeServerUrl
|
||||
: kCanonicalBridgeAcpEndpoint;
|
||||
.trim();
|
||||
if (!isSupportedExternalAcpEndpoint(resolvedBridgeServerUrl)) {
|
||||
const result = AccountSyncResult(
|
||||
state: 'blocked',
|
||||
message: 'BRIDGE_SERVER_URL is unavailable',
|
||||
);
|
||||
await controller.storeInternal.saveAccountSyncState(
|
||||
AccountSyncState.defaults().copyWith(
|
||||
syncState: result.state,
|
||||
syncMessage: result.message,
|
||||
lastSyncAtMs: DateTime.now().millisecondsSinceEpoch,
|
||||
lastSyncError: result.message,
|
||||
profileScope: 'bridge',
|
||||
tokenConfigured: const AccountTokenConfigured(
|
||||
bridge: true,
|
||||
vault: false,
|
||||
apisix: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
controller.accountStatusInternal = result.message;
|
||||
if (!quiet) {
|
||||
controller.accountBusyInternal = false;
|
||||
controller.notifyListeners();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
await controller.storeInternal.clearAccountManagedSecret(
|
||||
target: kAccountManagedSecretTargetAIGatewayAccessToken,
|
||||
);
|
||||
|
||||
@ -335,14 +335,27 @@ const List<SingleAgentProvider> kPresetExternalAcpProviders =
|
||||
|
||||
const String kCanonicalGatewayProviderId = 'openclaw';
|
||||
const String kCanonicalGatewayProviderLabel = 'OpenClaw';
|
||||
const String kCanonicalBridgeAcpEndpoint = 'https://xworkmate-bridge.svc.plus';
|
||||
|
||||
const List<SingleAgentProvider> kKnownSingleAgentProviders =
|
||||
const List<SingleAgentProvider> kBridgeOwnedSingleAgentProviders =
|
||||
<SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
SingleAgentProvider.opencode,
|
||||
SingleAgentProvider.claude,
|
||||
SingleAgentProvider.gemini,
|
||||
];
|
||||
|
||||
const Set<String> kLegacyExternalAcpProviderIds = <String>{'claude'};
|
||||
bool isBridgeOwnedSingleAgentProviderId(String providerId) {
|
||||
final normalized = normalizeSingleAgentProviderId(providerId);
|
||||
return kBridgeOwnedSingleAgentProviders.any(
|
||||
(item) => item.providerId == normalized,
|
||||
);
|
||||
}
|
||||
|
||||
List<SingleAgentProvider> normalizeBridgeOwnedSingleAgentProviderList(
|
||||
Iterable<SingleAgentProvider> providers,
|
||||
) {
|
||||
return normalizeSingleAgentProviderList(
|
||||
providers.where(
|
||||
(provider) => isBridgeOwnedSingleAgentProviderId(provider.providerId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -62,7 +62,7 @@ class ExternalAcpEndpointProfile {
|
||||
|
||||
SingleAgentProvider? get builtinProvider {
|
||||
final normalized = providerKey.trim().toLowerCase();
|
||||
for (final provider in kKnownSingleAgentProviders) {
|
||||
for (final provider in kPresetExternalAcpProviders) {
|
||||
if (provider.providerId == normalized) {
|
||||
return provider;
|
||||
}
|
||||
@ -131,14 +131,14 @@ List<ExternalAcpEndpointProfile> normalizeExternalAcpEndpoints({
|
||||
ExternalAcpEndpointProfile profile,
|
||||
) {
|
||||
final key = profile.providerKey.trim().toLowerCase();
|
||||
for (final provider in kKnownSingleAgentProviders) {
|
||||
for (final provider in kPresetExternalAcpProviders) {
|
||||
if (provider.providerId == key) {
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
final label = profile.label.trim();
|
||||
final badge = profile.badge.trim();
|
||||
for (final provider in kKnownSingleAgentProviders) {
|
||||
for (final provider in kPresetExternalAcpProviders) {
|
||||
if (provider.label == label && provider.badge == badge) {
|
||||
return provider;
|
||||
}
|
||||
@ -153,12 +153,7 @@ List<ExternalAcpEndpointProfile> normalizeExternalAcpEndpoints({
|
||||
if (key.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
if (kLegacyExternalAcpProviderIds.contains(originalKey) &&
|
||||
item.endpoint.trim().isEmpty) {
|
||||
continue;
|
||||
}
|
||||
if (originalKey.startsWith('custom-agent-') &&
|
||||
canonicalProvider != null &&
|
||||
if (!isBridgeOwnedSingleAgentProviderId(originalKey) &&
|
||||
item.endpoint.trim().isEmpty) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -474,12 +474,10 @@ class SettingsSnapshot {
|
||||
if (resolved.isUnspecified) {
|
||||
return SingleAgentProvider.unspecified;
|
||||
}
|
||||
if (kKnownSingleAgentProviders.any(
|
||||
(item) => item.providerId == resolved.providerId,
|
||||
)) {
|
||||
if (isBridgeOwnedSingleAgentProviderId(resolved.providerId)) {
|
||||
return resolved;
|
||||
}
|
||||
return resolved;
|
||||
return SingleAgentProvider.unspecified;
|
||||
}
|
||||
|
||||
SettingsSnapshot copyWithProviderSyncDefinitionForProvider(
|
||||
|
||||
@ -114,7 +114,7 @@ class SkillsFocusPreviewInternal extends StatelessWidget {
|
||||
if (items.isEmpty) {
|
||||
return PreviewEmptyStateInternal(
|
||||
message: typedController.isSingleAgentMode
|
||||
? (typedController.currentSingleAgentNeedsAiGatewayConfiguration
|
||||
? (typedController.currentSingleAgentNeedsBridgeProvider
|
||||
? appText(
|
||||
'当前没有可用的 Bridge Provider,请先在设置里配置并同步连接。',
|
||||
'No bridge provider is available. Configure and sync a connection in Settings first.',
|
||||
|
||||
@ -10,6 +10,7 @@ import 'package:xworkmate/runtime/desktop_platform_service.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';
|
||||
import 'package:xworkmate/runtime/single_agent_capabilities.dart';
|
||||
import 'package:xworkmate/runtime/skill_directory_access.dart';
|
||||
|
||||
void main() {
|
||||
@ -114,10 +115,10 @@ void main() {
|
||||
),
|
||||
goTaskServiceClient: const _FakeGoTaskServiceClient(),
|
||||
singleAgentSharedSkillScanRootOverrides: const <String>[],
|
||||
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
],
|
||||
);
|
||||
_seedBridgeProviders(controller, const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
]);
|
||||
addTearDown(() async {
|
||||
controller.dispose();
|
||||
await server.close(force: true);
|
||||
@ -196,10 +197,10 @@ void main() {
|
||||
),
|
||||
goTaskServiceClient: const _FakeGoTaskServiceClient(),
|
||||
singleAgentSharedSkillScanRootOverrides: const <String>[],
|
||||
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
],
|
||||
);
|
||||
_seedBridgeProviders(controller, const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
]);
|
||||
addTearDown(() async {
|
||||
controller.dispose();
|
||||
if (root.existsSync()) {
|
||||
@ -217,23 +218,41 @@ void main() {
|
||||
executionTarget: AssistantExecutionTarget.singleAgent,
|
||||
);
|
||||
|
||||
expect(
|
||||
controller.singleAgentProviderForSession('draft:bridge-default'),
|
||||
SingleAgentProvider.codex,
|
||||
);
|
||||
expect(
|
||||
controller.singleAgentResolvedProviderForSession(
|
||||
'draft:bridge-default',
|
||||
),
|
||||
SingleAgentProvider.codex,
|
||||
);
|
||||
|
||||
final thread = controller.taskThreadForSessionInternal(
|
||||
'draft:bridge-default',
|
||||
);
|
||||
expect(thread, isNotNull);
|
||||
expect(
|
||||
thread!.executionBinding.providerId,
|
||||
SingleAgentProvider.codex.providerId,
|
||||
);
|
||||
expect(
|
||||
thread.executionBinding.providerSource,
|
||||
ThreadSelectionSource.inherited,
|
||||
);
|
||||
expect(thread.hasExplicitProviderSelection, isFalse);
|
||||
expect(thread!.hasExplicitProviderSelection, isFalse);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _seedBridgeProviders(
|
||||
AppController controller,
|
||||
List<SingleAgentProvider> providers,
|
||||
) {
|
||||
controller.bridgeAdvertisedProvidersInternal = providers;
|
||||
controller.singleAgentCapabilitiesByProviderInternal = {
|
||||
for (final provider in providers)
|
||||
provider: SingleAgentCapabilities(
|
||||
available: true,
|
||||
supportedProviders: <SingleAgentProvider>[provider],
|
||||
endpoint: 'bridge',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
class _FakeSkillDirectoryAccessService implements SkillDirectoryAccessService {
|
||||
const _FakeSkillDirectoryAccessService(this.homeDirectory);
|
||||
|
||||
@ -326,8 +345,6 @@ class _FakeGoTaskServiceClient implements GoTaskServiceClient {
|
||||
required String taskPrompt,
|
||||
required String workingDirectory,
|
||||
required ExternalCodeAgentAcpRoutingConfig routing,
|
||||
String aiGatewayBaseUrl = '',
|
||||
String aiGatewayApiKey = '',
|
||||
}) async {
|
||||
return const ExternalCodeAgentAcpRoutingResolution(
|
||||
raw: <String, dynamic>{
|
||||
|
||||
@ -333,19 +333,16 @@ void main() {
|
||||
});
|
||||
|
||||
group('resolveGatewayAcpAuthorizationHeaderInternal', () {
|
||||
test('resolves ACP endpoint through the canonical bridge entry', () {
|
||||
test('uses only synced or persisted BRIDGE_SERVER_URL values', () {
|
||||
final controller = AppController();
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
expect(
|
||||
controller.resolveBridgeAcpEndpointInternal(),
|
||||
Uri.parse(kCanonicalBridgeAcpEndpoint),
|
||||
);
|
||||
expect(controller.resolveBridgeAcpEndpointInternal(), isNull);
|
||||
expect(
|
||||
controller.resolveExternalAcpEndpointForTargetInternal(
|
||||
AssistantExecutionTarget.singleAgent,
|
||||
),
|
||||
Uri.parse(kCanonicalBridgeAcpEndpoint),
|
||||
isNull,
|
||||
);
|
||||
|
||||
controller.settingsController.snapshotInternal = controller.settings
|
||||
@ -370,19 +367,19 @@ void main() {
|
||||
|
||||
expect(
|
||||
controller.resolveBridgeAcpEndpointInternal(),
|
||||
Uri.parse(kCanonicalBridgeAcpEndpoint),
|
||||
Uri.parse('https://bridge.customer.example/acp'),
|
||||
);
|
||||
expect(
|
||||
controller.resolveExternalAcpEndpointForTargetInternal(
|
||||
AssistantExecutionTarget.singleAgent,
|
||||
),
|
||||
Uri.parse(kCanonicalBridgeAcpEndpoint),
|
||||
Uri.parse('https://bridge.customer.example/acp'),
|
||||
);
|
||||
expect(
|
||||
controller.resolveExternalAcpEndpointForTargetInternal(
|
||||
AssistantExecutionTarget.gateway,
|
||||
),
|
||||
Uri.parse(kCanonicalBridgeAcpEndpoint),
|
||||
Uri.parse('https://bridge.customer.example/acp'),
|
||||
);
|
||||
});
|
||||
|
||||
@ -442,7 +439,7 @@ void main() {
|
||||
);
|
||||
|
||||
expect(bridgeAuthorization, 'Bearer bridge-token');
|
||||
expect(nonBridgeAuthorization, 'Bearer local-token');
|
||||
expect(nonBridgeAuthorization, isNull);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@ -9,6 +9,7 @@ import 'package:xworkmate/app/app_controller_desktop_workspace_execution.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';
|
||||
import 'package:xworkmate/runtime/single_agent_capabilities.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
@ -49,10 +50,10 @@ void main() {
|
||||
final controller = AppController(
|
||||
store: store,
|
||||
goTaskServiceClient: client,
|
||||
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
],
|
||||
);
|
||||
_seedBridgeProviders(controller, const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
]);
|
||||
addTearDown(() async {
|
||||
controller.dispose();
|
||||
store.dispose();
|
||||
@ -101,16 +102,33 @@ void main() {
|
||||
);
|
||||
|
||||
test(
|
||||
'single-agent turns go through the canonical bridge entry without synced endpoint state',
|
||||
'single-agent turns stop before dispatch when BRIDGE_SERVER_URL is missing',
|
||||
() async {
|
||||
final root = await Directory.systemTemp.createTemp(
|
||||
'xworkmate-missing-bridge-server-',
|
||||
);
|
||||
final store = SecureConfigStore(
|
||||
enableSecureStorage: false,
|
||||
appDataRootPathResolver: () async => '${root.path}/settings.sqlite3',
|
||||
secretRootPathResolver: () async => root.path,
|
||||
supportRootPathResolver: () async => root.path,
|
||||
);
|
||||
await store.initialize();
|
||||
final client = _CapturingGoTaskServiceClient();
|
||||
final controller = AppController(
|
||||
store: store,
|
||||
goTaskServiceClient: client,
|
||||
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
],
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
_seedBridgeProviders(controller, const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
]);
|
||||
addTearDown(() async {
|
||||
controller.dispose();
|
||||
store.dispose();
|
||||
if (await root.exists()) {
|
||||
await root.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
|
||||
const sessionKey = 'draft:single-agent-missing-bridge-server';
|
||||
controller.initializeAssistantThreadContext(
|
||||
@ -121,18 +139,15 @@ void main() {
|
||||
|
||||
await controller.sendChatMessage('first turn');
|
||||
|
||||
expect(client.requests, hasLength(1));
|
||||
expect(client.requests.single.sessionId, sessionKey);
|
||||
expect(client.requests.single.threadId, sessionKey);
|
||||
expect(client.requests, isEmpty);
|
||||
},
|
||||
);
|
||||
|
||||
test('each task thread keeps an independent workingDirectory', () async {
|
||||
final controller = AppController(
|
||||
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
],
|
||||
);
|
||||
final controller = AppController();
|
||||
_seedBridgeProviders(controller, const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
]);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
const sessionKey = 'draft:thread-working-directory-a';
|
||||
@ -166,12 +181,11 @@ void main() {
|
||||
});
|
||||
|
||||
test('new task threads do not inherit another thread provider choice', () {
|
||||
final controller = AppController(
|
||||
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
SingleAgentProvider.gemini,
|
||||
],
|
||||
);
|
||||
final controller = AppController();
|
||||
_seedBridgeProviders(controller, const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
SingleAgentProvider.gemini,
|
||||
]);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
const firstSessionKey = 'draft:thread-provider-a';
|
||||
@ -199,6 +213,21 @@ void main() {
|
||||
});
|
||||
}
|
||||
|
||||
void _seedBridgeProviders(
|
||||
AppController controller,
|
||||
List<SingleAgentProvider> providers,
|
||||
) {
|
||||
controller.bridgeAdvertisedProvidersInternal = providers;
|
||||
controller.singleAgentCapabilitiesByProviderInternal = {
|
||||
for (final provider in providers)
|
||||
provider: SingleAgentCapabilities(
|
||||
available: true,
|
||||
supportedProviders: <SingleAgentProvider>[provider],
|
||||
endpoint: 'bridge',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
class _CapturingGoTaskServiceClient implements GoTaskServiceClient {
|
||||
final List<GoTaskServiceRequest> requests = <GoTaskServiceRequest>[];
|
||||
int resolveExternalAcpRoutingCallCount = 0;
|
||||
@ -269,8 +298,6 @@ class _CapturingGoTaskServiceClient implements GoTaskServiceClient {
|
||||
required String taskPrompt,
|
||||
required String workingDirectory,
|
||||
required ExternalCodeAgentAcpRoutingConfig routing,
|
||||
String aiGatewayBaseUrl = '',
|
||||
String aiGatewayApiKey = '',
|
||||
}) async {
|
||||
resolveExternalAcpRoutingCallCount += 1;
|
||||
return const ExternalCodeAgentAcpRoutingResolution(
|
||||
|
||||
@ -11,6 +11,7 @@ import 'package:xworkmate/runtime/desktop_platform_service.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';
|
||||
import 'package:xworkmate/runtime/single_agent_capabilities.dart';
|
||||
import 'package:xworkmate/runtime/skill_directory_access.dart';
|
||||
import 'package:xworkmate/theme/app_theme.dart';
|
||||
|
||||
@ -37,10 +38,10 @@ void main() {
|
||||
),
|
||||
goTaskServiceClient: const _FakeGoTaskServiceClient(),
|
||||
singleAgentSharedSkillScanRootOverrides: const <String>[],
|
||||
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
],
|
||||
);
|
||||
_seedBridgeProviders(controller, const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
]);
|
||||
final inputController = TextEditingController();
|
||||
final focusNode = FocusNode();
|
||||
addTearDown(() async {
|
||||
@ -152,10 +153,10 @@ void main() {
|
||||
skillDirectoryAccessService: _FakeSkillDirectoryAccessService(root.path),
|
||||
goTaskServiceClient: const _FakeGoTaskServiceClient(),
|
||||
singleAgentSharedSkillScanRootOverrides: const <String>[],
|
||||
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
],
|
||||
);
|
||||
_seedBridgeProviders(controller, const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
]);
|
||||
final inputController = TextEditingController();
|
||||
final focusNode = FocusNode();
|
||||
addTearDown(() async {
|
||||
@ -239,6 +240,21 @@ void main() {
|
||||
});
|
||||
}
|
||||
|
||||
void _seedBridgeProviders(
|
||||
AppController controller,
|
||||
List<SingleAgentProvider> providers,
|
||||
) {
|
||||
controller.bridgeAdvertisedProvidersInternal = providers;
|
||||
controller.singleAgentCapabilitiesByProviderInternal = {
|
||||
for (final provider in providers)
|
||||
provider: SingleAgentCapabilities(
|
||||
available: true,
|
||||
supportedProviders: <SingleAgentProvider>[provider],
|
||||
endpoint: 'bridge',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
class _FakeSkillDirectoryAccessService implements SkillDirectoryAccessService {
|
||||
const _FakeSkillDirectoryAccessService(this.homeDirectory);
|
||||
|
||||
@ -317,8 +333,6 @@ class _FakeGoTaskServiceClient implements GoTaskServiceClient {
|
||||
required String taskPrompt,
|
||||
required String workingDirectory,
|
||||
required ExternalCodeAgentAcpRoutingConfig routing,
|
||||
String aiGatewayBaseUrl = '',
|
||||
String aiGatewayApiKey = '',
|
||||
}) async {
|
||||
return const ExternalCodeAgentAcpRoutingResolution(
|
||||
raw: <String, dynamic>{
|
||||
|
||||
@ -10,6 +10,7 @@ import 'package:xworkmate/runtime/desktop_platform_service.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';
|
||||
import 'package:xworkmate/runtime/single_agent_capabilities.dart';
|
||||
import 'package:xworkmate/runtime/skill_directory_access.dart';
|
||||
import 'package:xworkmate/theme/app_theme.dart';
|
||||
|
||||
@ -39,10 +40,10 @@ void main() {
|
||||
),
|
||||
goTaskServiceClient: const _GoldenGoTaskServiceClient(),
|
||||
singleAgentSharedSkillScanRootOverrides: const <String>[],
|
||||
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
],
|
||||
);
|
||||
_seedBridgeProviders(controller, const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
]);
|
||||
final inputController = TextEditingController(text: '请整理今天的任务进展');
|
||||
final focusNode = FocusNode();
|
||||
|
||||
@ -112,6 +113,21 @@ void main() {
|
||||
});
|
||||
}
|
||||
|
||||
void _seedBridgeProviders(
|
||||
AppController controller,
|
||||
List<SingleAgentProvider> providers,
|
||||
) {
|
||||
controller.bridgeAdvertisedProvidersInternal = providers;
|
||||
controller.singleAgentCapabilitiesByProviderInternal = {
|
||||
for (final provider in providers)
|
||||
provider: SingleAgentCapabilities(
|
||||
available: true,
|
||||
supportedProviders: <SingleAgentProvider>[provider],
|
||||
endpoint: 'bridge',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
class _GoldenSkillDirectoryAccessService
|
||||
implements SkillDirectoryAccessService {
|
||||
const _GoldenSkillDirectoryAccessService(this.homeDirectory);
|
||||
@ -205,8 +221,6 @@ class _GoldenGoTaskServiceClient implements GoTaskServiceClient {
|
||||
required String taskPrompt,
|
||||
required String workingDirectory,
|
||||
required ExternalCodeAgentAcpRoutingConfig routing,
|
||||
String aiGatewayBaseUrl = '',
|
||||
String aiGatewayApiKey = '',
|
||||
}) async {
|
||||
return const ExternalCodeAgentAcpRoutingResolution(
|
||||
raw: <String, dynamic>{
|
||||
|
||||
@ -32,9 +32,7 @@ void main() {
|
||||
late ExternalCodeAgentAcpDesktopTransport transport;
|
||||
|
||||
setUpAll(() async {
|
||||
if (!runRealE2E ||
|
||||
bridgeAuthToken.isEmpty ||
|
||||
bridgeAcpEndpoint.isEmpty) {
|
||||
if (!runRealE2E || bridgeAuthToken.isEmpty || bridgeAcpEndpoint.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final client = GatewayAcpClient(
|
||||
@ -69,9 +67,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('loads external ACP capabilities and provider catalog', () async {
|
||||
if (!runRealE2E ||
|
||||
bridgeAuthToken.isEmpty ||
|
||||
bridgeAcpEndpoint.isEmpty) {
|
||||
if (!runRealE2E || bridgeAuthToken.isEmpty || bridgeAcpEndpoint.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final capabilities = await transport.loadExternalAcpCapabilities(
|
||||
@ -350,8 +346,6 @@ GoTaskServiceRequest _buildRequest({
|
||||
selectedSkills: selectedSkills,
|
||||
inlineAttachments: const <GatewayChatAttachmentPayload>[],
|
||||
localAttachments: const <CollaborationAttachment>[],
|
||||
aiGatewayBaseUrl: '',
|
||||
aiGatewayApiKey: '',
|
||||
agentId: '',
|
||||
metadata: const <String, dynamic>{},
|
||||
routing: ExternalCodeAgentAcpRoutingConfig(
|
||||
|
||||
@ -72,8 +72,6 @@ void main() {
|
||||
selectedSkills: <String>[],
|
||||
inlineAttachments: <GatewayChatAttachmentPayload>[],
|
||||
localAttachments: <CollaborationAttachment>[],
|
||||
aiGatewayBaseUrl: '',
|
||||
aiGatewayApiKey: '',
|
||||
agentId: '',
|
||||
metadata: <String, dynamic>{},
|
||||
),
|
||||
|
||||
@ -135,7 +135,7 @@ void main() {
|
||||
});
|
||||
|
||||
test(
|
||||
'login still syncs bridge access when sync data omits bridge server',
|
||||
'login blocks bridge sync when sync data omits BRIDGE_SERVER_URL',
|
||||
() async {
|
||||
final root = await Directory.systemTemp.createTemp(
|
||||
'xworkmate-account-auth-missing-bridge-server-',
|
||||
@ -179,10 +179,10 @@ void main() {
|
||||
controller.accountStatus,
|
||||
'Signed in as review@customer.example',
|
||||
);
|
||||
expect(controller.accountSyncState?.syncState, 'ready');
|
||||
expect(controller.accountSyncState?.syncState, 'blocked');
|
||||
expect(
|
||||
controller.accountSyncState?.syncMessage,
|
||||
'Bridge access synced',
|
||||
'BRIDGE_SERVER_URL is unavailable',
|
||||
);
|
||||
expect(
|
||||
controller
|
||||
@ -191,7 +191,7 @@ void main() {
|
||||
.cloudSynced
|
||||
.remoteServerSummary
|
||||
.endpoint,
|
||||
kCanonicalBridgeAcpEndpoint,
|
||||
isEmpty,
|
||||
);
|
||||
expect(
|
||||
await store.loadAccountManagedSecret(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user