Merge branch 'codex/bridge-only-routing-cleanup'
This commit is contained in:
commit
55a80d2891
@ -5,8 +5,7 @@ with the provider catalog aligned to the bridge-only design.
|
|||||||
|
|
||||||
## Current Rule
|
## Current Rule
|
||||||
|
|
||||||
- Settings only manages bridge connection parameters and upstream sync
|
- Settings only manages bridge connection parameters and account sync metadata.
|
||||||
definitions.
|
|
||||||
- The provider picker is not derived from local endpoint presets.
|
- The provider picker is not derived from local endpoint presets.
|
||||||
- `xworkmate-bridge` is the only source of truth for the provider catalog.
|
- `xworkmate-bridge` is the only source of truth for the provider catalog.
|
||||||
|
|
||||||
@ -16,15 +15,7 @@ with the provider catalog aligned to the bridge-only design.
|
|||||||
flowchart TD
|
flowchart TD
|
||||||
A["Settings UI
|
A["Settings UI
|
||||||
仅管理 Bridge 连接参数
|
仅管理 Bridge 连接参数
|
||||||
与自定义 upstream sync 定义"] --> B["SettingsSnapshot.externalAcpEndpoints
|
与账号同步元数据"] --> G["acp.capabilities"]
|
||||||
仅作为 sync 输入"]
|
|
||||||
|
|
||||||
B --> C["buildExternalAcpSyncedProvidersInternal()"]
|
|
||||||
C --> D["syncExternalAcpProvidersInternal()"]
|
|
||||||
D --> E["xworkmate.providers.sync"]
|
|
||||||
E --> F["xworkmate-bridge providerCatalog"]
|
|
||||||
|
|
||||||
F --> G["acp.capabilities"]
|
|
||||||
G --> H["providerCatalog[]
|
G --> H["providerCatalog[]
|
||||||
singleAgent / multiAgent"]
|
singleAgent / multiAgent"]
|
||||||
|
|
||||||
@ -77,7 +68,7 @@ flowchart TD
|
|||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- `externalAcpEndpoints` still matters, but only as bridge sync input.
|
- Production cloud mode does not use app-side provider sync.
|
||||||
- Provider visibility and picker contents come from
|
- Provider visibility and picker contents come from
|
||||||
`acp.capabilities.providerCatalog`.
|
`acp.capabilities.providerCatalog`.
|
||||||
- Auto-provider resolution and unavailable messaging come from
|
- Auto-provider resolution and unavailable messaging come from
|
||||||
|
|||||||
@ -53,15 +53,7 @@ Single-agent provider catalog and availability are owned by
|
|||||||
flowchart TD
|
flowchart TD
|
||||||
A["Settings UI
|
A["Settings UI
|
||||||
仅管理 Bridge 连接参数
|
仅管理 Bridge 连接参数
|
||||||
与自定义 upstream sync 定义"] --> B["SettingsSnapshot.externalAcpEndpoints
|
与账号同步元数据"] --> G["acp.capabilities"]
|
||||||
仅作为 sync 输入"]
|
|
||||||
|
|
||||||
B --> C["buildExternalAcpSyncedProvidersInternal()"]
|
|
||||||
C --> D["syncExternalAcpProvidersInternal()"]
|
|
||||||
D --> E["xworkmate.providers.sync"]
|
|
||||||
E --> F["xworkmate-bridge providerCatalog"]
|
|
||||||
|
|
||||||
F --> G["acp.capabilities"]
|
|
||||||
G --> H["providerCatalog[]
|
G --> H["providerCatalog[]
|
||||||
singleAgent / multiAgent"]
|
singleAgent / multiAgent"]
|
||||||
|
|
||||||
@ -118,6 +110,8 @@ flowchart TD
|
|||||||
- Desktop App 直接桥接 Go 代码
|
- Desktop App 直接桥接 Go 代码
|
||||||
- Desktop 正常执行链路不以“先启动一个本地 HTTP server,再由 Desktop 自己回连”作为目标架构
|
- Desktop 正常执行链路不以“先启动一个本地 HTTP server,再由 Desktop 自己回连”作为目标架构
|
||||||
- Desktop 的 `sendMessage -> GoTaskService.executeTask -> ACP` 应理解为进程内或直接桥接语义
|
- Desktop 的 `sendMessage -> GoTaskService.executeTask -> ACP` 应理解为进程内或直接桥接语义
|
||||||
|
- Production cloud mode does not call `xworkmate.providers.sync`
|
||||||
|
- Production provider upstreams are bridge-owned, not app-owned
|
||||||
- 对 app 来说,bridge 是 discovery / config / connect / dialogue 的统一枢纽
|
- 对 app 来说,bridge 是 discovery / config / connect / dialogue 的统一枢纽
|
||||||
|
|
||||||
### Web / Mobile
|
### Web / Mobile
|
||||||
|
|||||||
@ -6,96 +6,108 @@ Date: 2026-04-11
|
|||||||
|
|
||||||
Scope:
|
Scope:
|
||||||
- `xworkmate-app`
|
- `xworkmate-app`
|
||||||
- Settings / account sync / local UI state / task thread persistence
|
- settings / account sync / cloud runtime state
|
||||||
|
|
||||||
## V1 Decision
|
## V1 Decision
|
||||||
|
|
||||||
This worktree implements the first app-side simplification:
|
Production cloud mode is bridge-only:
|
||||||
|
|
||||||
- keep a single persisted config file: `config/settings.yaml`
|
- app-facing cloud endpoint is fixed to `https://xworkmate-bridge.svc.plus`
|
||||||
- move local recoverable UI state to `ui/state.json`
|
- production provider catalog is bridge-owned
|
||||||
- keep task title/archive in `tasks/*.json`
|
- production gateway upstream is bridge-owned
|
||||||
- make account sync one-way overwrite for sync-owned fields
|
- account sync is metadata-only for session state, status, and managed secret references
|
||||||
- keep bridge provider catalog / runtime capabilities runtime-only
|
- account sync does not own executable ACP or gateway upstream endpoints
|
||||||
|
|
||||||
## Overview Workflow
|
## Production Routing Truth
|
||||||
|
|
||||||
|
The app does not define or sync production upstreams.
|
||||||
|
|
||||||
|
Bridge-owned production routing is:
|
||||||
|
|
||||||
|
- `codex` -> `https://acp-server.svc.plus/codex/acp/rpc`
|
||||||
|
- `opencode` -> `https://acp-server.svc.plus/opencode/acp/rpc`
|
||||||
|
- `gemini` -> `https://acp-server.svc.plus/gemini/acp/rpc`
|
||||||
|
- gateway -> `wss://openclaw.svc.plus`
|
||||||
|
|
||||||
|
The app only talks to:
|
||||||
|
|
||||||
|
- `https://xworkmate-bridge.svc.plus`
|
||||||
|
|
||||||
|
## App Responsibilities
|
||||||
|
|
||||||
|
- sign in to `accounts.svc.plus`
|
||||||
|
- persist account session and sync metadata
|
||||||
|
- call bridge runtime methods:
|
||||||
|
- `acp.capabilities`
|
||||||
|
- `xworkmate.routing.resolve`
|
||||||
|
- `session.start`
|
||||||
|
- `session.message`
|
||||||
|
- `session.cancel`
|
||||||
|
- `session.close`
|
||||||
|
- bridge-owned gateway methods
|
||||||
|
- render bridge/provider/gateway status from bridge runtime results
|
||||||
|
|
||||||
|
## Removed Responsibilities
|
||||||
|
|
||||||
|
- no app-side direct-connect cloud path
|
||||||
|
- no production `xworkmate.providers.sync`
|
||||||
|
- no production provider catalog from `providerSyncDefinitions`
|
||||||
|
- no execution-time use of account-synced `openclawUrl`
|
||||||
|
- no execution-time use of account-synced `apisixUrl`
|
||||||
|
- no direct app calls to `acp-server.svc.plus/*`
|
||||||
|
- no direct app calls to `openclaw.svc.plus`
|
||||||
|
|
||||||
|
## State Rules
|
||||||
|
|
||||||
|
`settings.yaml`
|
||||||
|
|
||||||
|
- stores current user settings and local editing state
|
||||||
|
- does not own production ACP upstream definitions
|
||||||
|
- does not get executable provider endpoints from account sync
|
||||||
|
|
||||||
|
`account/sync_state.json`
|
||||||
|
|
||||||
|
- stores synced account metadata only
|
||||||
|
- may retain `openclawUrl` / `apisixUrl` as account profile metadata
|
||||||
|
- does not overwrite executable cloud routing targets
|
||||||
|
|
||||||
|
`acpBridgeServerModeConfig.cloudSynced.remoteServerSummary.endpoint`
|
||||||
|
|
||||||
|
- represents bridge cloud entry only
|
||||||
|
- fixed to `https://xworkmate-bridge.svc.plus` while signed in and synced
|
||||||
|
- is not an upstream provider URL
|
||||||
|
- is not a gateway upstream URL
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
UI["Settings UI / App Startup"] --> INIT["SettingsController.initialize()"]
|
UI["Settings UI / App Startup"] --> INIT["SettingsController.initialize()"]
|
||||||
|
INIT --> LOAD["load settings + UI state + task state"]
|
||||||
subgraph LocalStores["APP Local Stores"]
|
|
||||||
YAML["config/settings.yaml"]
|
|
||||||
UISTATE["ui/state.json"]
|
|
||||||
SYNCJSON["account/sync_state.json"]
|
|
||||||
SECRET["secrets/*.secret\naccount session token / managed secrets"]
|
|
||||||
TASKS["tasks/*.json\nthread title / archived / thread-owned state"]
|
|
||||||
end
|
|
||||||
|
|
||||||
INIT --> LOAD["SecureConfigStore.loadSettingsSnapshot()"]
|
|
||||||
LOAD --> YAML
|
|
||||||
|
|
||||||
INIT --> LOADUI["SecureConfigStore.loadAppUiState()"]
|
|
||||||
LOADUI --> UISTATE
|
|
||||||
|
|
||||||
INIT --> LOADTHREADS["loadTaskThreads()"]
|
|
||||||
LOADTHREADS --> TASKS
|
|
||||||
|
|
||||||
INIT --> RESTORE["restoreAccountSession()"]
|
INIT --> RESTORE["restoreAccountSession()"]
|
||||||
RESTORE --> TOKEN["loadAccountSessionToken()"]
|
RESTORE --> CHECK{"account session ready?"}
|
||||||
TOKEN --> SECRET
|
CHECK -->|no| BLOCK["blocked"]
|
||||||
|
|
||||||
TOKEN --> CHECK{"baseUrl + session token ready?"}
|
|
||||||
CHECK -->|no| BLOCK["blocked\nAccount session is unavailable"]
|
|
||||||
CHECK -->|yes| SYNC["syncAccountSettingsInternal(baseUrl)"]
|
CHECK -->|yes| SYNC["syncAccountSettingsInternal(baseUrl)"]
|
||||||
|
|
||||||
SYNC --> API["AccountRuntimeClient.loadProfile(token)"]
|
SYNC --> API["AccountRuntimeClient.loadProfile(token)"]
|
||||||
API --> SAVE_SYNC["saveAccountSyncState(nextState)"]
|
API --> SAVE_SYNC["save account sync metadata"]
|
||||||
SAVE_SYNC --> SYNCJSON
|
API --> SAVE_SUMMARY["set cloud summary endpoint = bridge base URL"]
|
||||||
|
|
||||||
API --> MODECFG["saveSnapshot(\naccountLocalMode=false,\nacpBridgeServerModeConfig.cloudSynced=remote summary\n)"]
|
|
||||||
MODECFG --> YAML
|
|
||||||
|
|
||||||
API --> APPLY["applyAccountSyncedDefaultsSettingsInternal(state)"]
|
API --> APPLY["applyAccountSyncedDefaultsSettingsInternal(state)"]
|
||||||
|
|
||||||
APPLY --> O1["overwrite remote gateway endpoint"]
|
APPLY --> KEEP1["keep vault metadata"]
|
||||||
APPLY --> O2["overwrite gateway tokenRef"]
|
APPLY --> KEEP2["keep managed secret refs"]
|
||||||
APPLY --> O3["overwrite vault address / namespace"]
|
APPLY --> SKIP1["do not overwrite gateway executable endpoint"]
|
||||||
APPLY --> O4["overwrite aiGateway baseUrl / apiKeyRef"]
|
APPLY --> SKIP2["do not overwrite ACP executable endpoint"]
|
||||||
APPLY --> O5["overwrite ollamaCloud apiKeyRef"]
|
|
||||||
APPLY --> O6["update cloudSynced metadata"]
|
|
||||||
|
|
||||||
O1 --> SAVE["saveSnapshot(next settings)"]
|
UI --> BRIDGE_CAPS["acp.capabilities via bridge"]
|
||||||
O2 --> SAVE
|
UI --> BRIDGE_ROUTE["xworkmate.routing.resolve via bridge"]
|
||||||
O3 --> SAVE
|
UI --> BRIDGE_RUN["session.* via bridge"]
|
||||||
O4 --> SAVE
|
UI --> BRIDGE_GATEWAY["xworkmate.gateway.* via bridge"]
|
||||||
O5 --> SAVE
|
|
||||||
O6 --> SAVE
|
|
||||||
|
|
||||||
SAVE --> YAML
|
|
||||||
SAVE --> DERIVED["reloadDerivedStateInternal()"]
|
|
||||||
DERIVED --> VIEW["Settings / Runtime ViewModel"]
|
|
||||||
|
|
||||||
VIEW --> NOTE1["does not auto-connect gateway"]
|
|
||||||
APPLY -. not touched .-> NOTE2["providerSyncDefinitions\n(sync payload definitions)\nnot overwritten here"]
|
|
||||||
|
|
||||||
UI --> LOCAL_EDIT["local settings edit"]
|
|
||||||
LOCAL_EDIT --> SAVE_LOCAL["saveSnapshot()"]
|
|
||||||
SAVE_LOCAL --> YAML
|
|
||||||
|
|
||||||
UI --> UI_EDIT["local ui restore edit"]
|
|
||||||
UI_EDIT --> SAVE_UI["saveAppUiState()"]
|
|
||||||
SAVE_UI --> UISTATE
|
|
||||||
|
|
||||||
UI --> THREAD_EDIT["rename / archive / restore thread"]
|
|
||||||
THREAD_EDIT --> SAVE_THREAD["saveTaskThreads()"]
|
|
||||||
SAVE_THREAD --> TASKS
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## V1 Boundaries
|
## Invariants
|
||||||
|
|
||||||
- `settings.yaml` only stores current schema V1 config intent and sync-owned local snapshots.
|
- `providerSyncDefinitions` is not a production truth source.
|
||||||
- `ui/state.json` stores `assistantLastSessionKey`, `assistantNavigationDestinations`, and `savedGatewayTargets`.
|
- account sync may update metadata, but not production execution targets.
|
||||||
- `tasks/*.json` stores thread-owned display facts such as `title` and `archived`.
|
- gateway runtime status shown in the app must come from bridge runtime results.
|
||||||
- `account/sync_state.json` stores sync metadata only, not local override policy.
|
- bridge capability/provider availability shown in the app must come from `acp.capabilities`.
|
||||||
- bridge-advertised providers and ACP capability state stay runtime-only.
|
|
||||||
|
|||||||
@ -300,7 +300,8 @@ class AppController extends ChangeNotifier {
|
|||||||
late final MultiAgentOrchestrator multiAgentOrchestratorInternal;
|
late final MultiAgentOrchestrator multiAgentOrchestratorInternal;
|
||||||
late final MultiAgentMountManager multiAgentMountManagerInternal;
|
late final MultiAgentMountManager multiAgentMountManagerInternal;
|
||||||
|
|
||||||
GoTaskServiceClient get goTaskServiceClientForTest => goTaskServiceClientInternal;
|
GoTaskServiceClient get goTaskServiceClientForTest =>
|
||||||
|
goTaskServiceClientInternal;
|
||||||
|
|
||||||
Map<SingleAgentProvider, SingleAgentCapabilities>
|
Map<SingleAgentProvider, SingleAgentCapabilities>
|
||||||
singleAgentCapabilitiesByProviderInternal =
|
singleAgentCapabilitiesByProviderInternal =
|
||||||
@ -654,10 +655,7 @@ class AppController extends ChangeNotifier {
|
|||||||
if (selection == SingleAgentProvider.auto) {
|
if (selection == SingleAgentProvider.auto) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final resolvedSelection = settings.resolveSingleAgentProvider(selection);
|
return canUseSingleAgentProviderInternal(selection) ? selection : null;
|
||||||
return canUseSingleAgentProviderInternal(resolvedSelection)
|
|
||||||
? resolvedSelection
|
|
||||||
: null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> get aiGatewayConversationModelChoices {
|
List<String> get aiGatewayConversationModelChoices {
|
||||||
|
|||||||
@ -57,7 +57,6 @@ Future<void> refreshAcpCapabilitiesRuntimeInternal(
|
|||||||
controller.sessionsControllerInternal.currentSessionKey,
|
controller.sessionsControllerInternal.currentSessionKey,
|
||||||
);
|
);
|
||||||
if (target == AssistantExecutionTarget.singleAgent) {
|
if (target == AssistantExecutionTarget.singleAgent) {
|
||||||
await controller.syncExternalAcpProvidersInternal();
|
|
||||||
await controller.goTaskServiceClientInternal.loadExternalAcpCapabilities(
|
await controller.goTaskServiceClientInternal.loadExternalAcpCapabilities(
|
||||||
target: AssistantExecutionTarget.singleAgent,
|
target: AssistantExecutionTarget.singleAgent,
|
||||||
forceRefresh: forceRefresh,
|
forceRefresh: forceRefresh,
|
||||||
@ -94,7 +93,6 @@ Future<void> refreshSingleAgentCapabilitiesRuntimeInternal(
|
|||||||
AppController controller, {
|
AppController controller, {
|
||||||
bool forceRefresh = false,
|
bool forceRefresh = false,
|
||||||
}) async {
|
}) async {
|
||||||
await controller.syncExternalAcpProvidersInternal();
|
|
||||||
final capabilities = await controller.goTaskServiceClientInternal
|
final capabilities = await controller.goTaskServiceClientInternal
|
||||||
.loadExternalAcpCapabilities(
|
.loadExternalAcpCapabilities(
|
||||||
target: AssistantExecutionTarget.singleAgent,
|
target: AssistantExecutionTarget.singleAgent,
|
||||||
@ -221,40 +219,15 @@ String? resolveSingleAgentWorkingDirectoryForSessionRuntimeInternal(
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool singleAgentProviderRequiresLocalPathRuntimeInternal(
|
bool singleAgentProviderRequiresLocalPathRuntimeInternal(
|
||||||
AppController controller,
|
AppController _,
|
||||||
SingleAgentProvider provider,
|
SingleAgentProvider provider,
|
||||||
) {
|
) {
|
||||||
final configuredEndpoint = controller.settings
|
if (provider == SingleAgentProvider.codex ||
|
||||||
.providerSyncDefinitionForProvider(provider)
|
provider == SingleAgentProvider.opencode ||
|
||||||
.endpoint
|
provider == SingleAgentProvider.gemini) {
|
||||||
.trim();
|
|
||||||
if (configuredEndpoint.isEmpty) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
final normalizedInput = configuredEndpoint.contains('://')
|
|
||||||
? configuredEndpoint
|
|
||||||
: 'ws://$configuredEndpoint';
|
|
||||||
final endpoint = Uri.tryParse(normalizedInput);
|
|
||||||
if (endpoint == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
final scheme = endpoint.scheme.trim().toLowerCase();
|
|
||||||
if (scheme == 'wss' || scheme == 'https') {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final host = endpoint.host.trim();
|
return true;
|
||||||
if (host.isEmpty) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
final address = InternetAddress.tryParse(host);
|
|
||||||
if (address != null) {
|
|
||||||
return !(address.isLoopback || address.type == InternetAddressType.unix);
|
|
||||||
}
|
|
||||||
final normalizedHost = host.toLowerCase();
|
|
||||||
if (normalizedHost == 'localhost') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CodeAgentNodeState buildCodeAgentNodeStateRuntimeInternal(
|
CodeAgentNodeState buildCodeAgentNodeStateRuntimeInternal(
|
||||||
|
|||||||
@ -661,42 +661,6 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<ExternalCodeAgentAcpSyncedProvider>>
|
|
||||||
buildExternalAcpSyncedProvidersInternal() async {
|
|
||||||
final providers = <ExternalCodeAgentAcpSyncedProvider>[];
|
|
||||||
for (final profile in settings.providerSyncDefinitions) {
|
|
||||||
final provider = settings.singleAgentProviderForId(profile.providerKey);
|
|
||||||
if (provider == SingleAgentProvider.auto) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final endpoint = profile.endpoint.trim();
|
|
||||||
if (!profile.enabled || endpoint.isEmpty) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final authorizationHeader = profile.authRef.trim().isEmpty
|
|
||||||
? ''
|
|
||||||
: await settingsControllerInternal.resolveSecretValueInternal(
|
|
||||||
refName: profile.authRef.trim(),
|
|
||||||
);
|
|
||||||
providers.add(
|
|
||||||
ExternalCodeAgentAcpSyncedProvider(
|
|
||||||
providerId: provider.providerId,
|
|
||||||
label: provider.label,
|
|
||||||
endpoint: endpoint,
|
|
||||||
authorizationHeader: authorizationHeader,
|
|
||||||
enabled: true,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return providers;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> syncExternalAcpProvidersInternal() async {
|
|
||||||
await goTaskServiceClientInternal.syncExternalProviders(
|
|
||||||
await buildExternalAcpSyncedProvidersInternal(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> persistGoTaskArtifactsForSessionInternal(
|
Future<void> persistGoTaskArtifactsForSessionInternal(
|
||||||
String sessionKey,
|
String sessionKey,
|
||||||
GoTaskServiceResult result,
|
GoTaskServiceResult result,
|
||||||
|
|||||||
@ -152,7 +152,6 @@ Future<void> sendSingleAgentMessageDesktopGoTaskFlowInternal(
|
|||||||
.map((item) => item.label.trim().isNotEmpty ? item.label : item.key)
|
.map((item) => item.label.trim().isNotEmpty ? item.label : item.key)
|
||||||
.where((item) => item.trim().isNotEmpty)
|
.where((item) => item.trim().isNotEmpty)
|
||||||
.toList(growable: false);
|
.toList(growable: false);
|
||||||
await controller.syncExternalAcpProvidersInternal();
|
|
||||||
final result = await controller.goTaskServiceClientInternal.executeTask(
|
final result = await controller.goTaskServiceClientInternal.executeTask(
|
||||||
GoTaskServiceRequest(
|
GoTaskServiceRequest(
|
||||||
sessionId: sessionKey,
|
sessionId: sessionKey,
|
||||||
|
|||||||
@ -306,7 +306,6 @@ extension AppControllerDesktopThreadActions on AppController {
|
|||||||
try {
|
try {
|
||||||
final dispatch = await codeAgentNodeOrchestratorInternal
|
final dispatch = await codeAgentNodeOrchestratorInternal
|
||||||
.buildGatewayDispatch(buildCodeAgentNodeStateInternal());
|
.buildGatewayDispatch(buildCodeAgentNodeStateInternal());
|
||||||
await syncExternalAcpProvidersInternal();
|
|
||||||
final result = await goTaskServiceClientInternal.executeTask(
|
final result = await goTaskServiceClientInternal.executeTask(
|
||||||
GoTaskServiceRequest(
|
GoTaskServiceRequest(
|
||||||
sessionId: sessionKey,
|
sessionId: sessionKey,
|
||||||
|
|||||||
@ -160,7 +160,6 @@ Future<void> runMultiAgentCollaborationThreadSessionInternal(
|
|||||||
);
|
);
|
||||||
controller.recomputeTasksInternal();
|
controller.recomputeTasksInternal();
|
||||||
try {
|
try {
|
||||||
await controller.syncExternalAcpProvidersInternal();
|
|
||||||
final result = await controller.goTaskServiceClientInternal.executeTask(
|
final result = await controller.goTaskServiceClientInternal.executeTask(
|
||||||
GoTaskServiceRequest(
|
GoTaskServiceRequest(
|
||||||
sessionId: sessionKey,
|
sessionId: sessionKey,
|
||||||
|
|||||||
@ -13,8 +13,6 @@ class ExternalCodeAgentAcpDesktopTransport
|
|||||||
: _bridge = bridge ?? GoAcpStdioBridge();
|
: _bridge = bridge ?? GoAcpStdioBridge();
|
||||||
|
|
||||||
final GoAcpStdioBridge _bridge;
|
final GoAcpStdioBridge _bridge;
|
||||||
List<ExternalCodeAgentAcpSyncedProvider> _syncedProviders =
|
|
||||||
const <ExternalCodeAgentAcpSyncedProvider>[];
|
|
||||||
|
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
GoAcpStdioBridge get bridgeForTest => _bridge;
|
GoAcpStdioBridge get bridgeForTest => _bridge;
|
||||||
@ -22,19 +20,13 @@ class ExternalCodeAgentAcpDesktopTransport
|
|||||||
@override
|
@override
|
||||||
Future<void> syncExternalProviders(
|
Future<void> syncExternalProviders(
|
||||||
List<ExternalCodeAgentAcpSyncedProvider> providers,
|
List<ExternalCodeAgentAcpSyncedProvider> providers,
|
||||||
) async {
|
) async {}
|
||||||
_syncedProviders = List<ExternalCodeAgentAcpSyncedProvider>.unmodifiable(
|
|
||||||
providers,
|
|
||||||
);
|
|
||||||
await _syncProviders();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<ExternalCodeAgentAcpCapabilities> loadExternalAcpCapabilities({
|
Future<ExternalCodeAgentAcpCapabilities> loadExternalAcpCapabilities({
|
||||||
required AssistantExecutionTarget target,
|
required AssistantExecutionTarget target,
|
||||||
bool forceRefresh = false,
|
bool forceRefresh = false,
|
||||||
}) async {
|
}) async {
|
||||||
await _syncProviders();
|
|
||||||
final response = await _bridge.request(
|
final response = await _bridge.request(
|
||||||
method: 'acp.capabilities',
|
method: 'acp.capabilities',
|
||||||
params: const <String, dynamic>{},
|
params: const <String, dynamic>{},
|
||||||
@ -66,7 +58,6 @@ class ExternalCodeAgentAcpDesktopTransport
|
|||||||
String aiGatewayBaseUrl = '',
|
String aiGatewayBaseUrl = '',
|
||||||
String aiGatewayApiKey = '',
|
String aiGatewayApiKey = '',
|
||||||
}) async {
|
}) async {
|
||||||
await _syncProviders();
|
|
||||||
final response = await _bridge.request(
|
final response = await _bridge.request(
|
||||||
method: 'xworkmate.routing.resolve',
|
method: 'xworkmate.routing.resolve',
|
||||||
params: <String, dynamic>{
|
params: <String, dynamic>{
|
||||||
@ -89,7 +80,6 @@ class ExternalCodeAgentAcpDesktopTransport
|
|||||||
GoTaskServiceRequest request, {
|
GoTaskServiceRequest request, {
|
||||||
required void Function(GoTaskServiceUpdate update) onUpdate,
|
required void Function(GoTaskServiceUpdate update) onUpdate,
|
||||||
}) async {
|
}) async {
|
||||||
await _syncProviders();
|
|
||||||
late final StreamSubscription<Map<String, dynamic>> subscription;
|
late final StreamSubscription<Map<String, dynamic>> subscription;
|
||||||
var streamedText = '';
|
var streamedText = '';
|
||||||
String? completedMessage;
|
String? completedMessage;
|
||||||
@ -158,28 +148,6 @@ class ExternalCodeAgentAcpDesktopTransport
|
|||||||
@override
|
@override
|
||||||
Future<void> dispose() => _bridge.dispose();
|
Future<void> dispose() => _bridge.dispose();
|
||||||
|
|
||||||
Future<void> _syncProviders() async {
|
|
||||||
if (_syncedProviders.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await _bridge.request(
|
|
||||||
method: 'xworkmate.providers.sync',
|
|
||||||
params: <String, dynamic>{
|
|
||||||
'providers': _syncedProviders
|
|
||||||
.map(
|
|
||||||
(item) => <String, dynamic>{
|
|
||||||
'providerId': item.providerId,
|
|
||||||
'endpoint': item.endpoint,
|
|
||||||
'label': item.label,
|
|
||||||
'authorizationHeader': item.authorizationHeader,
|
|
||||||
'enabled': item.enabled,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.toList(growable: false),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> _castMap(Object? value) {
|
Map<String, dynamic> _castMap(Object? value) {
|
||||||
if (value is Map<String, dynamic>) {
|
if (value is Map<String, dynamic>) {
|
||||||
return value;
|
return value;
|
||||||
|
|||||||
@ -25,10 +25,7 @@ extension SettingsControllerAccountExtension on SettingsController {
|
|||||||
if (local.isNotEmpty) {
|
if (local.isNotEmpty) {
|
||||||
return local;
|
return local;
|
||||||
}
|
}
|
||||||
if (!snapshotInternal.acpBridgeServerModeConfig.usesCloudSyncBase) {
|
return '';
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return accountSyncStateInternal?.syncedDefaults.apisixUrl.trim() ?? '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> get effectiveAiGatewayAvailableModels {
|
List<String> get effectiveAiGatewayAvailableModels {
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import 'account_runtime_client.dart';
|
|||||||
import 'runtime_controllers_settings.dart';
|
import 'runtime_controllers_settings.dart';
|
||||||
import 'runtime_models.dart';
|
import 'runtime_models.dart';
|
||||||
|
|
||||||
|
const _kProductionBridgeEndpoint = 'https://xworkmate-bridge.svc.plus';
|
||||||
|
|
||||||
Future<void> loginAccountSettingsInternal(
|
Future<void> loginAccountSettingsInternal(
|
||||||
SettingsController controller, {
|
SettingsController controller, {
|
||||||
required String baseUrl,
|
required String baseUrl,
|
||||||
@ -270,9 +272,7 @@ Future<AccountSyncResult> syncAccountSettingsInternal(
|
|||||||
lastSyncAt: nextState.lastSyncAtMs,
|
lastSyncAt: nextState.lastSyncAtMs,
|
||||||
remoteServerSummary: currentModeConfig.cloudSynced.remoteServerSummary
|
remoteServerSummary: currentModeConfig.cloudSynced.remoteServerSummary
|
||||||
.copyWith(
|
.copyWith(
|
||||||
endpoint: response.profile.openclawUrl.trim().isNotEmpty
|
endpoint: _kProductionBridgeEndpoint,
|
||||||
? response.profile.openclawUrl.trim()
|
|
||||||
: response.profile.apisixUrl.trim(),
|
|
||||||
hasAdvancedOverrides: false,
|
hasAdvancedOverrides: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -352,37 +352,6 @@ Future<void> applyAccountSyncedDefaultsSettingsInternal(
|
|||||||
final previous = controller.snapshotInternal;
|
final previous = controller.snapshotInternal;
|
||||||
var next = previous;
|
var next = previous;
|
||||||
final defaults = state.syncedDefaults;
|
final defaults = state.syncedDefaults;
|
||||||
if (defaults.openclawUrl.trim().isNotEmpty) {
|
|
||||||
final remoteProfile = previous.gatewayProfiles[kGatewayRemoteProfileIndex];
|
|
||||||
final normalized = normalizeGatewayManualEndpointInternal(
|
|
||||||
host: defaults.openclawUrl,
|
|
||||||
port: remoteProfile.port,
|
|
||||||
tls: remoteProfile.tls,
|
|
||||||
);
|
|
||||||
next = next.copyWithGatewayProfileAt(
|
|
||||||
kGatewayRemoteProfileIndex,
|
|
||||||
remoteProfile.copyWith(
|
|
||||||
mode: RuntimeConnectionMode.remote,
|
|
||||||
useSetupCode: false,
|
|
||||||
setupCode: '',
|
|
||||||
host: normalized.host,
|
|
||||||
port: normalized.port,
|
|
||||||
tls: normalized.tls,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final gatewayTokenLocator = defaults.locatorForTarget(
|
|
||||||
kAccountManagedSecretTargetOpenclawGatewayToken,
|
|
||||||
);
|
|
||||||
if (gatewayTokenLocator != null) {
|
|
||||||
final remoteProfile = next.gatewayProfiles[kGatewayRemoteProfileIndex];
|
|
||||||
next = next.copyWithGatewayProfileAt(
|
|
||||||
kGatewayRemoteProfileIndex,
|
|
||||||
remoteProfile.copyWith(tokenRef: gatewayTokenLocator.target),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (defaults.vaultUrl.trim().isNotEmpty) {
|
if (defaults.vaultUrl.trim().isNotEmpty) {
|
||||||
next = next.copyWith(
|
next = next.copyWith(
|
||||||
vault: next.vault.copyWith(address: defaults.vaultUrl.trim()),
|
vault: next.vault.copyWith(address: defaults.vaultUrl.trim()),
|
||||||
@ -395,12 +364,6 @@ Future<void> applyAccountSyncedDefaultsSettingsInternal(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (defaults.apisixUrl.trim().isNotEmpty) {
|
|
||||||
next = next.copyWith(
|
|
||||||
aiGateway: next.aiGateway.copyWith(baseUrl: defaults.apisixUrl.trim()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final aiGatewayLocator = defaults.locatorForTarget(
|
final aiGatewayLocator = defaults.locatorForTarget(
|
||||||
kAccountManagedSecretTargetAIGatewayAccessToken,
|
kAccountManagedSecretTargetAIGatewayAccessToken,
|
||||||
);
|
);
|
||||||
@ -433,9 +396,7 @@ Future<void> applyAccountSyncedDefaultsSettingsInternal(
|
|||||||
.cloudSynced
|
.cloudSynced
|
||||||
.remoteServerSummary
|
.remoteServerSummary
|
||||||
.copyWith(
|
.copyWith(
|
||||||
endpoint: defaults.openclawUrl.trim().isNotEmpty
|
endpoint: _kProductionBridgeEndpoint,
|
||||||
? defaults.openclawUrl.trim()
|
|
||||||
: defaults.apisixUrl.trim(),
|
|
||||||
hasAdvancedOverrides: false,
|
hasAdvancedOverrides: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -208,7 +208,7 @@ SettingsSnapshot _buildCanonicalSettings() {
|
|||||||
accountIdentifier: 'canonical@svc.plus',
|
accountIdentifier: 'canonical@svc.plus',
|
||||||
lastSyncAt: 123456789,
|
lastSyncAt: 123456789,
|
||||||
remoteServerSummary: const AcpBridgeServerRemoteServerSummary(
|
remoteServerSummary: const AcpBridgeServerRemoteServerSummary(
|
||||||
endpoint: 'wss://gateway.svc.plus',
|
endpoint: 'https://xworkmate-bridge.svc.plus',
|
||||||
hasAdvancedOverrides: false,
|
hasAdvancedOverrides: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -73,20 +73,20 @@ void main() {
|
|||||||
expect(first.state, 'ready');
|
expect(first.state, 'ready');
|
||||||
expect(
|
expect(
|
||||||
controller.snapshot.gatewayProfiles[kGatewayRemoteProfileIndex].host,
|
controller.snapshot.gatewayProfiles[kGatewayRemoteProfileIndex].host,
|
||||||
'remote.gateway.svc.plus',
|
'local.example.com',
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
controller
|
controller
|
||||||
.snapshot
|
.snapshot
|
||||||
.gatewayProfiles[kGatewayRemoteProfileIndex]
|
.gatewayProfiles[kGatewayRemoteProfileIndex]
|
||||||
.tokenRef,
|
.tokenRef,
|
||||||
kAccountManagedSecretTargetOpenclawGatewayToken,
|
'local_ref',
|
||||||
);
|
);
|
||||||
expect(controller.snapshot.vault.address, 'https://vault.svc.plus');
|
expect(controller.snapshot.vault.address, 'https://vault.svc.plus');
|
||||||
expect(controller.snapshot.vault.namespace, 'prod');
|
expect(controller.snapshot.vault.namespace, 'prod');
|
||||||
expect(
|
expect(
|
||||||
controller.snapshot.aiGateway.baseUrl,
|
controller.snapshot.aiGateway.baseUrl,
|
||||||
'https://apisix.svc.plus',
|
'https://local-apisix.example.com',
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
controller.snapshot.aiGateway.apiKeyRef,
|
controller.snapshot.aiGateway.apiKeyRef,
|
||||||
@ -116,7 +116,16 @@ void main() {
|
|||||||
expect(controller.snapshot.vault.address, 'https://vault.svc.plus');
|
expect(controller.snapshot.vault.address, 'https://vault.svc.plus');
|
||||||
expect(
|
expect(
|
||||||
controller.snapshot.aiGateway.baseUrl,
|
controller.snapshot.aiGateway.baseUrl,
|
||||||
'https://apisix.svc.plus',
|
'https://edited-apisix.example.com',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
controller
|
||||||
|
.snapshot
|
||||||
|
.acpBridgeServerModeConfig
|
||||||
|
.cloudSynced
|
||||||
|
.remoteServerSummary
|
||||||
|
.endpoint,
|
||||||
|
'https://xworkmate-bridge.svc.plus',
|
||||||
);
|
);
|
||||||
|
|
||||||
final rawSyncState = await store.loadSupportJson(
|
final rawSyncState = await store.loadSupportJson(
|
||||||
|
|||||||
@ -46,48 +46,22 @@ class _FakeGoAcpStdioBridgeWithSyncOrder extends GoAcpStdioBridge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('External ACP bridge sync order', () {
|
group('External ACP bridge routing order', () {
|
||||||
test('syncs providers before capabilities requests', () async {
|
test('loads capabilities without app-side provider sync', () async {
|
||||||
final bridge = _FakeGoAcpStdioBridgeWithSyncOrder();
|
final bridge = _FakeGoAcpStdioBridgeWithSyncOrder();
|
||||||
final transport = ExternalCodeAgentAcpDesktopTransport(bridge: bridge);
|
final transport = ExternalCodeAgentAcpDesktopTransport(bridge: bridge);
|
||||||
|
|
||||||
await transport
|
|
||||||
.syncExternalProviders(const <ExternalCodeAgentAcpSyncedProvider>[
|
|
||||||
ExternalCodeAgentAcpSyncedProvider(
|
|
||||||
providerId: 'codex',
|
|
||||||
label: 'Codex',
|
|
||||||
endpoint: 'https://acp-server.svc.plus/codex/acp/rpc',
|
|
||||||
authorizationHeader: '',
|
|
||||||
enabled: true,
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
await transport.loadExternalAcpCapabilities(
|
await transport.loadExternalAcpCapabilities(
|
||||||
target: AssistantExecutionTarget.singleAgent,
|
target: AssistantExecutionTarget.singleAgent,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(bridge.methods, <String>[
|
expect(bridge.methods, <String>['acp.capabilities']);
|
||||||
'xworkmate.providers.sync',
|
|
||||||
'xworkmate.providers.sync',
|
|
||||||
'acp.capabilities',
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('syncs providers before session start requests', () async {
|
test('starts sessions without app-side provider sync', () async {
|
||||||
final bridge = _FakeGoAcpStdioBridgeWithSyncOrder();
|
final bridge = _FakeGoAcpStdioBridgeWithSyncOrder();
|
||||||
final transport = ExternalCodeAgentAcpDesktopTransport(bridge: bridge);
|
final transport = ExternalCodeAgentAcpDesktopTransport(bridge: bridge);
|
||||||
|
|
||||||
await transport
|
|
||||||
.syncExternalProviders(const <ExternalCodeAgentAcpSyncedProvider>[
|
|
||||||
ExternalCodeAgentAcpSyncedProvider(
|
|
||||||
providerId: 'codex',
|
|
||||||
label: 'Codex',
|
|
||||||
endpoint: 'https://acp-server.svc.plus/codex/acp/rpc',
|
|
||||||
authorizationHeader: '',
|
|
||||||
enabled: true,
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
await transport.executeTask(
|
await transport.executeTask(
|
||||||
const GoTaskServiceRequest(
|
const GoTaskServiceRequest(
|
||||||
sessionId: 's1',
|
sessionId: 's1',
|
||||||
@ -108,11 +82,7 @@ void main() {
|
|||||||
onUpdate: (_) {},
|
onUpdate: (_) {},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(bridge.methods, <String>[
|
expect(bridge.methods, <String>['session.start']);
|
||||||
'xworkmate.providers.sync',
|
|
||||||
'xworkmate.providers.sync',
|
|
||||||
'session.start',
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -77,26 +77,23 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
test(
|
test('ignores app-side provider sync in bridge-only mode', () async {
|
||||||
'only syncs when app has explicit provider overrides to send',
|
final bridge = _FakeGoAcpStdioBridge();
|
||||||
() async {
|
final transport = ExternalCodeAgentAcpDesktopTransport(bridge: bridge);
|
||||||
final bridge = _FakeGoAcpStdioBridge();
|
|
||||||
final transport = ExternalCodeAgentAcpDesktopTransport(bridge: bridge);
|
|
||||||
|
|
||||||
await transport
|
await transport
|
||||||
.syncExternalProviders(const <ExternalCodeAgentAcpSyncedProvider>[
|
.syncExternalProviders(const <ExternalCodeAgentAcpSyncedProvider>[
|
||||||
ExternalCodeAgentAcpSyncedProvider(
|
ExternalCodeAgentAcpSyncedProvider(
|
||||||
providerId: 'codex',
|
providerId: 'codex',
|
||||||
label: 'Codex',
|
label: 'Codex',
|
||||||
endpoint: 'https://acp-server.svc.plus/codex/acp/rpc',
|
endpoint: 'https://acp-server.svc.plus/codex/acp/rpc',
|
||||||
authorizationHeader: '',
|
authorizationHeader: '',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(bridge.methods, <String>['xworkmate.providers.sync']);
|
expect(bridge.methods, isEmpty);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'uses bridge routing resolve for preflight provider selection',
|
'uses bridge routing resolve for preflight provider selection',
|
||||||
|
|||||||
@ -79,7 +79,7 @@ void main() {
|
|||||||
lastSyncAt: 123456789,
|
lastSyncAt: 123456789,
|
||||||
remoteServerSummary:
|
remoteServerSummary:
|
||||||
const AcpBridgeServerRemoteServerSummary(
|
const AcpBridgeServerRemoteServerSummary(
|
||||||
endpoint: 'wss://gateway.svc.plus',
|
endpoint: 'https://xworkmate-bridge.svc.plus',
|
||||||
hasAdvancedOverrides: false,
|
hasAdvancedOverrides: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -58,6 +58,15 @@ void main() {
|
|||||||
expect(controller.accountSyncState?.profileScope, 'tenant-shared');
|
expect(controller.accountSyncState?.profileScope, 'tenant-shared');
|
||||||
expect(controller.accountSyncState?.tokenConfigured.apisix, isTrue);
|
expect(controller.accountSyncState?.tokenConfigured.apisix, isTrue);
|
||||||
expect(await store.loadAccountSessionToken(), 'session-token');
|
expect(await store.loadAccountSessionToken(), 'session-token');
|
||||||
|
expect(
|
||||||
|
controller
|
||||||
|
.snapshot
|
||||||
|
.acpBridgeServerModeConfig
|
||||||
|
.cloudSynced
|
||||||
|
.remoteServerSummary
|
||||||
|
.endpoint,
|
||||||
|
'https://xworkmate-bridge.svc.plus',
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user