Merge branch 'codex/bridge-contract-task-dialog-rewrite'

This commit is contained in:
Haitao Pan 2026-04-12 11:34:18 +08:00
commit 47d3fa36ed
28 changed files with 281 additions and 311 deletions

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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,

View File

@ -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

View File

@ -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),

View File

@ -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(

View File

@ -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',

View File

@ -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

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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(

View File

@ -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

View File

@ -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(

View File

@ -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;

View File

@ -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(

View File

@ -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,
);

View File

@ -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),
),
);
}

View File

@ -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;
}

View File

@ -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(

View File

@ -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.',

View File

@ -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>{

View File

@ -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);
},
);
});

View File

@ -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(

View File

@ -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>{

View File

@ -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>{

View File

@ -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(

View File

@ -72,8 +72,6 @@ void main() {
selectedSkills: <String>[],
inlineAttachments: <GatewayChatAttachmentPayload>[],
localAttachments: <CollaborationAttachment>[],
aiGatewayBaseUrl: '',
aiGatewayApiKey: '',
agentId: '',
metadata: <String, dynamic>{},
),

View File

@ -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(