diff --git a/docs/architecture/task-control-plane-unification.md b/docs/architecture/task-control-plane-unification.md
index 35d09bd1..6949d7a8 100644
--- a/docs/architecture/task-control-plane-unification.md
+++ b/docs/architecture/task-control-plane-unification.md
@@ -18,7 +18,8 @@ Last Updated: 2026-04-08
- UI 不变
- `GoTaskService.executeTask` 是唯一公开入口
- ACP 是统一控制面
-- `single-agent / multi-agent / gateway` 是 ACP 解析后的执行器分支
+- `bridge` 是 app 客户端的发现 / 配置 / 连接 / 对话枢纽
+- 账户同步只同步 bridge 相关配置属性与安全引用,不做自动连接
## 目标态
@@ -32,13 +33,11 @@ flowchart TD
F --> G["Memory.Inject"]
G --> H["buildResolvedExecutionParams"]
H --> I{"resolvedExecutionTarget"}
- I -->|"single-agent"| J["single-agent executor"]
- I -->|"multi-agent"| K["multi-agent executor"]
- I -->|"gateway"| L["gateway executor"]
- J --> M["Adapter Layer"]
+ I -->|"single-agent"| J["single-agent ACP request"]
+ I -->|"multi-agent"| K["multi-agent ACP request"]
+ J --> M["bridge hub"]
K --> M
- L --> M
- M --> N["External Runtime / Relay / Gateway"]
+ M --> N["Gateway / Provider adapters"]
N --> O["stream events / result"]
O --> P["Memory.Record"]
P --> Q["Update Thread State"]
@@ -51,7 +50,8 @@ flowchart TD
- Desktop App 直接桥接 Go 代码
- Desktop 正常执行链路不以“先启动一个本地 HTTP server,再由 Desktop 自己回连”作为目标架构
-- Desktop 的 `sendMessage -> GoTaskService.executeTask -> ACP` 应理解为进程内或直接桥接语义,不是 Web server 回环语义
+- Desktop 的 `sendMessage -> GoTaskService.executeTask -> ACP` 应理解为进程内或直接桥接语义
+- 对 app 来说,bridge 是 discovery / config / connect / dialogue 的统一枢纽
### Web / Mobile
@@ -99,5 +99,5 @@ flowchart TD
1. 文档口径收敛
2. Dart 请求模型统一
3. route 决策内收到 `GoTaskService` / ACP
-4. `gateway` 成为 ACP executor
+4. app 侧 bridge 枢纽与 provider / gateway 适配关系收敛
5. `multi-agent` 成为统一请求语义
diff --git a/docs/architecture/xworkmate-bridge-migration.md b/docs/architecture/xworkmate-bridge-migration.md
index d8a6b417..8deca5c7 100644
--- a/docs/architecture/xworkmate-bridge-migration.md
+++ b/docs/architecture/xworkmate-bridge-migration.md
@@ -4,7 +4,7 @@
The ACP Bridge Server implementation was migrated out of `xworkmate-app` into the standalone sibling repository `xworkmate-bridge`.
-This migration separates the embedded Go bridge/server from the Flutter application repository while preserving the existing helper binary contract used by the app.
+This migration separates the embedded Go bridge/server from the Flutter application repository. The app now depends on the sibling `xworkmate-bridge` repo for the helper/runtime contract instead of carrying an in-repo Go bridge copy.
## New Repository
@@ -33,9 +33,9 @@ The following app-side concerns remain in `xworkmate-app`:
## Build Contract
-`xworkmate-app` still expects a helper named `xworkmate-go-core`.
+`xworkmate-app` expects the helper artifact named `xworkmate-go-core`.
-To preserve compatibility, `xworkmate-bridge` continues to build the helper using that binary name.
+This is the current cross-repo runtime contract, not a legacy compatibility shim. The helper is built from `xworkmate-bridge` and consumed by `xworkmate-app`.
## App Repository Changes
@@ -57,3 +57,5 @@ Validated during migration:
## Operational Note
For local development and packaging, `xworkmate-bridge` must exist as a sibling repository next to `xworkmate-app`, unless `XWORKMATE_BRIDGE_DIR` is set explicitly.
+
+At runtime, the app treats bridge-related discovery, provider sync, connection metadata, and ACP conversation forwarding as bridge-owned concerns. Account sync only updates bridge-linked configuration attributes and secure secret references; it does not auto-connect the bridge.
diff --git a/docs/architecture/xworkmate-layered-architecture.md b/docs/architecture/xworkmate-layered-architecture.md
index de08fa0d..dffd2059 100644
--- a/docs/architecture/xworkmate-layered-architecture.md
+++ b/docs/architecture/xworkmate-layered-architecture.md
@@ -11,8 +11,9 @@ Last Updated: 2026-04-08
- `TaskThread` 是线程控制面
- `GoTaskService.executeTask` 是唯一公开执行入口
- ACP 是统一控制面
-- `single-agent / multi-agent / gateway` 是 ACP 解析后的执行器分支
-- 兼容旁路不再作为架构目标
+- `bridge` 是 app 客户端侧的发现 / 配置 / 连接 / 对话枢纽
+- 账户同步只同步 bridge 相关配置属性与安全引用,不负责自动连接
+- 历史旁路与旧的直连叙述不再作为目标架构
## 总览图
@@ -51,11 +52,11 @@ flowchart TB
E5["buildResolvedExecutionParams"]
end
- subgraph L6["Executors / Adapters"]
- F1["single-agent executor"]
- F2["multi-agent executor"]
- F3["gateway executor"]
- F4["GatewayRuntime / Web relay / GatewayAcpClient"]
+ subgraph L6["Bridge / Executors / Adapters"]
+ F1["single-agent ACP request"]
+ F2["multi-agent ACP request"]
+ F3["bridge hub
discovery / config / connect / dialogue"]
+ F4["gateway / provider adapters"]
end
A1 --> B1
@@ -77,7 +78,8 @@ flowchart TB
E4 --> E5
E5 --> F1
E5 --> F2
- E5 --> F3
+ F1 --> F3
+ F2 --> F3
F3 --> F4
```
@@ -87,24 +89,25 @@ flowchart TB
2. `TaskThread` 承载线程级事实,不由页面局部状态拼装。
3. `GoTaskService.executeTask` 是唯一公开任务入口。
4. ACP 是统一控制面,负责 routing / skills / memory / resolved execution。
-5. `gateway` 是执行器分支,不是 UI 旁路目标。
+5. `bridge` 是 app 侧统一枢纽;gateway/provider 适配能力挂在 bridge 后面,不再把历史直连路径写成长期主链。
## 文档目录
### 目标规范
-- [任务执行链路统一收敛](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-task-control-plane-unification/docs/architecture/task-control-plane-unification.md)
+- [任务执行链路统一收敛](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app/docs/architecture/task-control-plane-unification.md)
+- [ACP Forwarding Topology](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-bridge/docs/architecture/acp-forwarding-topology.md)
### 当前实现观察
- 当前实现观察不再保留独立主设计文档
-- 如需判断规范,以 [任务执行链路统一收敛](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-task-control-plane-unification/docs/architecture/task-control-plane-unification.md) 为准
+- 如需判断规范,以 [任务执行链路统一收敛](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app/docs/architecture/task-control-plane-unification.md) 为准
### 边界与适配器说明
- 适配器边界统一收敛到本文件与主文档,不再保留旧的并列设计稿
-## Compatibility route (removed from target)
+## Removed From Target
- 旧的 `openClawTask` 公开语义不再是目标架构的一部分
-- `GatewayRuntime`、`Web relay`、`GatewayAcpClient` 只作为 adapter/executor 能力存在
+- 不再把“客户端直接围绕旧 gateway 默认值运转”写成长期主设计
diff --git a/lib/app/app_controller_desktop_external_acp_routing.dart b/lib/app/app_controller_desktop_external_acp_routing.dart
index e578de66..ff136d55 100644
--- a/lib/app/app_controller_desktop_external_acp_routing.dart
+++ b/lib/app/app_controller_desktop_external_acp_routing.dart
@@ -41,23 +41,43 @@ extension AppControllerDesktopExternalAcpRouting on AppController {
buildExternalAcpSyncedProvidersInternal() async {
final providers = [];
for (final profile in settings.externalAcpEndpoints) {
- final providerId = profile.providerKey.trim();
- final endpoint = profile.endpoint.trim();
+ final builtinProvider = profile.builtinProvider;
+ final effectiveProfile = builtinProvider == null
+ ? profile
+ : settings.externalAcpEndpointForProvider(builtinProvider);
+ final providerId = effectiveProfile.providerKey.trim();
+ final endpoint = effectiveProfile.endpoint.trim();
if (providerId.isEmpty || endpoint.isEmpty) {
continue;
}
- final authorizationHeader = profile.authRef.trim().isEmpty
+ var authorizationHeader = effectiveProfile.authRef.trim().isEmpty
? ''
: await settingsControllerInternal.resolveSecretValueInternal(
- refName: profile.authRef.trim(),
+ refName: effectiveProfile.authRef.trim(),
);
+ if (authorizationHeader.isEmpty &&
+ builtinProvider != null &&
+ settings.acpBridgeServerModeConfig.usesSelfHostedBase) {
+ final selfHosted = settings.acpBridgeServerModeConfig.selfHosted;
+ final username = selfHosted.username.trim();
+ final passwordRef = selfHosted.passwordRef.trim();
+ final password = passwordRef.isEmpty
+ ? ''
+ : await settingsControllerInternal.loadSecretValueByRef(
+ passwordRef,
+ );
+ if (username.isNotEmpty && password.trim().isNotEmpty) {
+ authorizationHeader =
+ 'Basic ${base64Encode(utf8.encode('$username:${password.trim()}'))}';
+ }
+ }
providers.add(
ExternalCodeAgentAcpSyncedProvider(
providerId: providerId,
- label: profile.label,
+ label: effectiveProfile.label,
endpoint: endpoint,
authorizationHeader: authorizationHeader,
- enabled: profile.enabled,
+ enabled: effectiveProfile.enabled,
),
);
}
diff --git a/lib/app/app_controller_desktop_runtime_exceptions.dart b/lib/app/app_controller_desktop_runtime_exceptions.dart
new file mode 100644
index 00000000..dde7df21
--- /dev/null
+++ b/lib/app/app_controller_desktop_runtime_exceptions.dart
@@ -0,0 +1,14 @@
+class AiGatewayChatExceptionInternal implements Exception {
+ const AiGatewayChatExceptionInternal(this.message);
+
+ final String message;
+
+ @override
+ String toString() => message;
+}
+
+class AiGatewayAbortExceptionInternal implements Exception {
+ const AiGatewayAbortExceptionInternal(this.partialText);
+
+ final String partialText;
+}
diff --git a/lib/app/app_controller_desktop_runtime_helpers.dart b/lib/app/app_controller_desktop_runtime_helpers.dart
index 5a6dac1b..154ccbc7 100644
--- a/lib/app/app_controller_desktop_runtime_helpers.dart
+++ b/lib/app/app_controller_desktop_runtime_helpers.dart
@@ -44,6 +44,7 @@ import 'app_controller_desktop_settings_runtime.dart';
import 'app_controller_desktop_thread_storage.dart';
import 'app_controller_desktop_skill_permissions.dart';
import 'app_controller_desktop_runtime_coordination_impl.dart';
+import 'app_controller_desktop_runtime_exceptions.dart';
// ignore_for_file: invalid_use_of_visible_for_testing_member, invalid_use_of_protected_member
extension AppControllerDesktopRuntimeHelpers on AppController {
@@ -758,7 +759,8 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
RuntimeConnectionMode mode,
) {
return switch (mode) {
- RuntimeConnectionMode.unconfigured => AssistantExecutionTarget.singleAgent,
+ RuntimeConnectionMode.unconfigured =>
+ AssistantExecutionTarget.singleAgent,
RuntimeConnectionMode.local => AssistantExecutionTarget.local,
RuntimeConnectionMode.remote => AssistantExecutionTarget.remote,
};
@@ -788,18 +790,3 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
};
}
}
-
-class AiGatewayChatExceptionInternal implements Exception {
- const AiGatewayChatExceptionInternal(this.message);
-
- final String message;
-
- @override
- String toString() => message;
-}
-
-class AiGatewayAbortExceptionInternal implements Exception {
- const AiGatewayAbortExceptionInternal(this.partialText);
-
- final String partialText;
-}
diff --git a/lib/app/app_controller_desktop_thread_binding.dart b/lib/app/app_controller_desktop_thread_binding.dart
index bd490807..b3375d0c 100644
--- a/lib/app/app_controller_desktop_thread_binding.dart
+++ b/lib/app/app_controller_desktop_thread_binding.dart
@@ -46,6 +46,23 @@ import 'app_controller_desktop_skill_permissions.dart';
import 'app_controller_desktop_runtime_helpers.dart';
extension AppControllerDesktopThreadBinding on AppController {
+ String managedLocalThreadWorkspaceSuffixInternal(String sessionKey) =>
+ '/.xworkmate/threads/${threadWorkspaceDirectoryNameInternal(sessionKey)}';
+
+ bool isManagedLocalThreadWorkspacePathInternal(
+ String path,
+ String sessionKey,
+ ) {
+ final normalizedPath = trimTrailingPathSeparatorInternal(path.trim());
+ if (normalizedPath.isEmpty) {
+ return false;
+ }
+ final normalizedSuffix = managedLocalThreadWorkspaceSuffixInternal(
+ sessionKey,
+ );
+ return normalizedPath.endsWith(normalizedSuffix);
+ }
+
String localThreadWorkspacePathInternal(String sessionKey) {
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
sessionKey,
@@ -161,6 +178,10 @@ extension AppControllerDesktopThreadBinding on AppController {
if (executionTarget == AssistantExecutionTarget.singleAgent) {
if (existingBinding != null &&
existingBinding.workspaceKind == WorkspaceKind.localFs &&
+ !isManagedLocalThreadWorkspacePathInternal(
+ existingBinding.workspacePath,
+ sessionKey,
+ ) &&
ensureLocalWorkspaceDirectoryInternal(
existingBinding.workspacePath,
)) {
diff --git a/lib/runtime/runtime_models_settings_snapshot.dart b/lib/runtime/runtime_models_settings_snapshot.dart
index e9eb974b..6d91668a 100644
--- a/lib/runtime/runtime_models_settings_snapshot.dart
+++ b/lib/runtime/runtime_models_settings_snapshot.dart
@@ -516,8 +516,21 @@ class SettingsSnapshot {
ExternalAcpEndpointProfile externalAcpEndpointForProvider(
SingleAgentProvider provider,
) {
- return externalAcpEndpointForProviderId(provider.providerId) ??
+ final profile =
+ externalAcpEndpointForProviderId(provider.providerId) ??
ExternalAcpEndpointProfile.defaultsForProvider(provider);
+ final bridgeBaseUrl = acpBridgeBuiltinEndpointBaseUrl;
+ if (provider.isAuto || bridgeBaseUrl.isEmpty) {
+ return profile;
+ }
+ return profile.copyWith(endpoint: bridgeBaseUrl);
+ }
+
+ String get acpBridgeBuiltinEndpointBaseUrl {
+ if (!acpBridgeServerModeConfig.usesSelfHostedBase) {
+ return '';
+ }
+ return acpBridgeServerModeConfig.selfHosted.serverUrl.trim();
}
ExternalAcpEndpointProfile? externalAcpEndpointForProviderId(
@@ -591,9 +604,17 @@ class SettingsSnapshot {
List get savedSingleAgentProviders =>
normalizeSingleAgentProviderList(
- externalAcpEndpoints
- .where((item) => item.enabled && item.endpoint.trim().isNotEmpty)
- .map((item) => item.toProvider()),
+ externalAcpEndpoints.map((item) {
+ final provider = item.toProvider();
+ if (provider.isAuto) {
+ return null;
+ }
+ final effective = externalAcpEndpointForProvider(provider);
+ if (!effective.enabled || effective.endpoint.trim().isEmpty) {
+ return null;
+ }
+ return effective.toProvider();
+ }).whereType(),
);
bool isGatewayTargetSaved(AssistantExecutionTarget target) {
diff --git a/test/runtime/account_bridge_smoke_suite.dart b/test/runtime/account_bridge_smoke_suite.dart
index ca97c947..024646b6 100644
--- a/test/runtime/account_bridge_smoke_suite.dart
+++ b/test/runtime/account_bridge_smoke_suite.dart
@@ -9,7 +9,6 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:xworkmate/app/app_controller.dart';
import 'package:xworkmate/runtime/account_runtime_client.dart';
-import 'package:xworkmate/runtime/gateway_acp_client.dart';
import 'package:xworkmate/runtime/go_task_service_client.dart';
import 'package:xworkmate/runtime/runtime_controllers.dart';
import 'package:xworkmate/runtime/runtime_models.dart';
@@ -130,10 +129,7 @@ void main() {
workingDirectory: tempDir.path,
prompt: '请检查 ACP 路由和 gateway 路由',
);
- expect(
- routeResolution['result'] != null,
- isTrue,
- );
+ expect(routeResolution['result'] != null, isTrue);
final workspacePath = controller.assistantWorkspacePathForSession(
controller.currentSessionKey,
);
@@ -188,14 +184,12 @@ class _SmokeEnv {
env['BRIDGE_URL'] ??
'https://xworkmate-bridge.svc.plus';
final codexProviderEndpoint =
- env['CODEX_PROVIDER_ENDPOINT'] ??
- 'https://acp-server.svc.plus/codex';
+ env['CODEX_PROVIDER_ENDPOINT'] ?? 'https://acp-server.svc.plus/codex';
final opencodeProviderEndpoint =
env['OPENCODE_PROVIDER_ENDPOINT'] ??
'https://acp-server.svc.plus/opencode';
final geminiProviderEndpoint =
- env['GEMINI_PROVIDER_ENDPOINT'] ??
- 'https://acp-server.svc.plus/gemini';
+ env['GEMINI_PROVIDER_ENDPOINT'] ?? 'https://acp-server.svc.plus/gemini';
if (accountLoginName.trim().isEmpty ||
accountLoginPassword.trim().isEmpty ||
bridgeAuthToken.trim().isEmpty) {
@@ -238,16 +232,11 @@ class _BridgeGoTaskServiceClient implements GoTaskServiceClient {
final String bridgeBaseUrl;
final String bridgeAuthToken;
- List _providers =
- const [];
@override
Future syncExternalProviders(
List providers,
) async {
- _providers = List.unmodifiable(
- providers,
- );
await _request(
method: 'xworkmate.providers.sync',
params: {
@@ -257,7 +246,8 @@ class _BridgeGoTaskServiceClient implements GoTaskServiceClient {
'providerId': item.providerId,
'label': item.label,
'endpoint': item.endpoint,
- 'authorizationHeader': item.authorizationHeader.startsWith('Bearer ')
+ 'authorizationHeader':
+ item.authorizationHeader.startsWith('Bearer ')
? item.authorizationHeader
: 'Bearer ${item.authorizationHeader}',
'enabled': item.enabled,
@@ -277,14 +267,17 @@ class _BridgeGoTaskServiceClient implements GoTaskServiceClient {
method: 'acp.capabilities',
params: const {},
);
- final result = (response['result'] as Map?)?.cast() ??
+ final result =
+ (response['result'] as Map?)?.cast() ??
const {};
final providers = {};
for (final raw in