Remove managed local bridge mode
This commit is contained in:
parent
c1d9b64a2c
commit
1c539d437f
@ -1,6 +1,8 @@
|
|||||||
# Gateway Dev Runbook
|
# Gateway Dev Runbook
|
||||||
|
|
||||||
This runbook covers the `XWorkmate.svc.plus` client when it connects directly to an OpenClaw gateway for local and remote development, pairing approval, and release verification.
|
This runbook covers the `XWorkmate.svc.plus` client when it connects to the managed bridge / remote gateway path for pairing approval and release verification.
|
||||||
|
|
||||||
|
Local gateway / loopback is no longer an app-facing runtime mode for account sync, bridge startup, or task dialog send flow.
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
@ -16,21 +18,17 @@ This runbook covers the `XWorkmate.svc.plus` client when it connects directly to
|
|||||||
- `.env` is development prefill only. It must not become the persisted source of truth and must not auto-connect the gateway.
|
- `.env` is development prefill only. It must not become the persisted source of truth and must not auto-connect the gateway.
|
||||||
- Shared tokens and passwords are user-entered auth inputs. Never hardcode them in Dart, native code, tests, or scripts.
|
- Shared tokens and passwords are user-entered auth inputs. Never hardcode them in Dart, native code, tests, or scripts.
|
||||||
- Long-lived secrets belong in secure storage. XWorkmate also keeps a file-backed fallback for device identity and operator device token so release builds keep a stable paired identity.
|
- Long-lived secrets belong in secure storage. XWorkmate also keeps a file-backed fallback for device identity and operator device token so release builds keep a stable paired identity.
|
||||||
- Local mode may use plain `ws://127.0.0.1:18789`.
|
- The app-facing bridge / gateway path is remote-only and must use TLS, for example `wss://openclaw.svc.plus:443`.
|
||||||
- Remote mode must use TLS, for example `wss://openclaw.svc.plus:443`.
|
- Loopback endpoints must not be revived as runtime truth sources for account sync or task dialog startup.
|
||||||
|
|
||||||
## Endpoint Matrix
|
## Endpoint Matrix
|
||||||
|
|
||||||
- XWorkmate direct local gateway auth:
|
|
||||||
- `ws://127.0.0.1:18789`
|
|
||||||
- XWorkmate direct remote gateway auth:
|
- XWorkmate direct remote gateway auth:
|
||||||
- `wss://openclaw.svc.plus:443`
|
- `wss://openclaw.svc.plus:443`
|
||||||
- OpenClaw operator control page for pairing approval:
|
- OpenClaw operator control page for pairing approval:
|
||||||
- [https://openclaw.svc.plus/nodes](https://openclaw.svc.plus/nodes)
|
- [https://openclaw.svc.plus/nodes](https://openclaw.svc.plus/nodes)
|
||||||
- Local web console style endpoint:
|
|
||||||
- `http://127.0.0.1:18789`
|
|
||||||
|
|
||||||
Do not enter `http://` or `https://` into the XWorkmate gateway dialog unless the code explicitly expects a browser console URL. The app-level gateway connection is `ws://` or `wss://`.
|
Do not enter loopback / local console URLs into the XWorkmate gateway dialog. The current app-level gateway connection is remote-only `wss://`.
|
||||||
|
|
||||||
## Config Sources
|
## Config Sources
|
||||||
|
|
||||||
@ -61,7 +59,6 @@ Do not enter `http://` or `https://` into the XWorkmate gateway dialog unless th
|
|||||||
|
|
||||||
### Symptom
|
### Symptom
|
||||||
|
|
||||||
- XWorkmate could connect locally and chat normally.
|
|
||||||
- Remote shared-token auth reached the gateway, but remote connect repeatedly ended with `NOT_PAIRED: pairing required`.
|
- Remote shared-token auth reached the gateway, but remote connect repeatedly ended with `NOT_PAIRED: pairing required`.
|
||||||
- The operator page showed one `Pending` `XWorkmate Mac` entry and one older `Paired` `XWorkmate Mac` entry at the same time.
|
- The operator page showed one `Pending` `XWorkmate Mac` entry and one older `Paired` `XWorkmate Mac` entry at the same time.
|
||||||
|
|
||||||
@ -218,9 +215,8 @@ If a device-run test hangs instead of failing with an assertion, record it as ma
|
|||||||
|
|
||||||
## Manual Acceptance
|
## Manual Acceptance
|
||||||
|
|
||||||
1. Verify local mode can connect and chat through `ws://127.0.0.1:18789`.
|
1. Verify remote mode can connect through `wss://openclaw.svc.plus:443`.
|
||||||
2. Verify remote mode can connect through `wss://openclaw.svc.plus:443`.
|
2. Verify first remote connect creates one pending pairing request.
|
||||||
3. Verify first remote connect creates one pending pairing request.
|
3. Approve that request from [https://openclaw.svc.plus/nodes](https://openclaw.svc.plus/nodes).
|
||||||
4. Approve that request from [https://openclaw.svc.plus/nodes](https://openclaw.svc.plus/nodes).
|
4. Reconnect and verify the same `deviceId` is now listed under `Paired`.
|
||||||
5. Reconnect and verify the same `deviceId` is now listed under `Paired`.
|
5. Restart the app and verify remote reconnect does not create a fresh pending request.
|
||||||
6. Restart the app and verify remote reconnect does not create a fresh pending request.
|
|
||||||
|
|||||||
@ -14,8 +14,10 @@ This project ships a Flutter desktop/mobile client that connects to an OpenClaw
|
|||||||
## 2. Gateway And Network Trust Boundary
|
## 2. Gateway And Network Trust Boundary
|
||||||
|
|
||||||
- Keep the gateway endpoint, auth token, password, and TLS choice explicit.
|
- Keep the gateway endpoint, auth token, password, and TLS choice explicit.
|
||||||
- Only loopback/local mode may use plain `ws` or equivalent non-TLS transport intentionally.
|
- The managed bridge / gateway runtime path is remote-only and pinned to the managed bridge origin.
|
||||||
|
- `BRIDGE_SERVER_URL` must not become the runtime source of truth for bridge startup or task dialog sends.
|
||||||
- Remote connections must not silently downgrade from TLS to non-TLS.
|
- Remote connections must not silently downgrade from TLS to non-TLS.
|
||||||
|
- Explicit loopback / non-TLS behavior is only allowed in isolated external ACP self-host test flows, not in the managed bridge / gateway main path.
|
||||||
- A user-initiated connect action may use the current form values directly for the active handshake. Persistence is a separate concern and must not be required for the immediate request.
|
- A user-initiated connect action may use the current form values directly for the active handshake. Persistence is a separate concern and must not be required for the immediate request.
|
||||||
- When changing auth behavior, verify both success and rejection paths.
|
- When changing auth behavior, verify both success and rejection paths.
|
||||||
|
|
||||||
|
|||||||
@ -15,17 +15,17 @@ It focuses on the runtime data path:
|
|||||||
|
|
||||||
and the two key client-side parsing assertions:
|
and the two key client-side parsing assertions:
|
||||||
|
|
||||||
- `BRIDGE_SERVER_URL` is written into account sync state
|
- `BRIDGE_SERVER_URL` may be retained in account sync metadata, but does not drive runtime endpoint selection
|
||||||
- `BRIDGE_AUTH_TOKEN` is written into secure storage
|
- `BRIDGE_AUTH_TOKEN` is written into secure storage
|
||||||
|
|
||||||
## Sync Chain
|
## Sync Chain
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
A["accounts.svc.plus\nprotected login / MFA / sync / bootstrap response"] -->|returns| B["xworkmate-app\nparse BRIDGE_SERVER_URL\nparse BRIDGE_AUTH_TOKEN"]
|
A["accounts.svc.plus\nprotected login / MFA / sync / bootstrap response"] -->|returns| B["xworkmate-app\nparse BRIDGE_SERVER_URL metadata\nparse BRIDGE_AUTH_TOKEN"]
|
||||||
B -->|write| C["AccountSyncState.syncedDefaults.bridgeServerUrl"]
|
B -->|write metadata only| C["AccountSyncState.syncedDefaults.bridgeServerUrl"]
|
||||||
B -->|write secure only| D["Secure Storage\nbridge.auth_token"]
|
B -->|write secure only| D["Secure Storage\nbridge.auth_token"]
|
||||||
C -->|drive runtime metadata| E["cloudSynced.remoteServerSummary.endpoint"]
|
B -->|pin runtime origin| E["cloudSynced.remoteServerSummary.endpoint\nhttps://xworkmate-bridge.svc.plus"]
|
||||||
D -->|Authorization: Bearer <token>| F["xworkmate-app runtime requests"]
|
D -->|Authorization: Bearer <token>| F["xworkmate-app runtime requests"]
|
||||||
F --> G["xworkmate-bridge"]
|
F --> G["xworkmate-bridge"]
|
||||||
```
|
```
|
||||||
@ -37,12 +37,13 @@ flowchart TD
|
|||||||
A["accounts.svc.plus"] --> A1["BRIDGE_SERVER_URL\nplain response field"]
|
A["accounts.svc.plus"] --> A1["BRIDGE_SERVER_URL\nplain response field"]
|
||||||
A --> A2["BRIDGE_AUTH_TOKEN\nprotected response field only"]
|
A --> A2["BRIDGE_AUTH_TOKEN\nprotected response field only"]
|
||||||
|
|
||||||
B["xworkmate-app"] --> B1["sync state\nstores BRIDGE_SERVER_URL-derived bridgeServerUrl"]
|
B["xworkmate-app"] --> B1["sync state\nmay retain BRIDGE_SERVER_URL-derived bridgeServerUrl as metadata"]
|
||||||
B --> B2["secure storage\nstores BRIDGE_AUTH_TOKEN as bridge.auth_token"]
|
B --> B2["secure storage\nstores BRIDGE_AUTH_TOKEN as bridge.auth_token"]
|
||||||
B --> B3["normal settings/profile\nmust not persist BRIDGE_AUTH_TOKEN"]
|
B --> B3["normal settings/profile\nmust not persist BRIDGE_AUTH_TOKEN"]
|
||||||
|
B --> B4["runtime bridge origin\nfixed to https://xworkmate-bridge.svc.plus"]
|
||||||
|
|
||||||
C["xworkmate-bridge"] --> C1["consume bootstrap response"]
|
C["xworkmate-bridge"] --> C1["consume runtime request"]
|
||||||
C1 --> C2["uses BRIDGE_SERVER_URL"]
|
C1 --> C2["does not depend on BRIDGE_SERVER_URL"]
|
||||||
C1 --> C3["uses BRIDGE_AUTH_TOKEN"]
|
C1 --> C3["uses BRIDGE_AUTH_TOKEN"]
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -57,8 +58,9 @@ sequenceDiagram
|
|||||||
participant Bridge as xworkmate-bridge
|
participant Bridge as xworkmate-bridge
|
||||||
|
|
||||||
Accounts->>App: protected response\nBRIDGE_SERVER_URL\nBRIDGE_AUTH_TOKEN
|
Accounts->>App: protected response\nBRIDGE_SERVER_URL\nBRIDGE_AUTH_TOKEN
|
||||||
App->>SyncState: save bridgeServerUrl from BRIDGE_SERVER_URL
|
App->>SyncState: save bridgeServerUrl as metadata when present
|
||||||
App->>SecureStore: save bridge.auth_token from BRIDGE_AUTH_TOKEN
|
App->>SecureStore: save bridge.auth_token from BRIDGE_AUTH_TOKEN
|
||||||
|
App->>App: resolve runtime bridge origin = https://xworkmate-bridge.svc.plus
|
||||||
App->>Bridge: connect with Authorization: Bearer <token>
|
App->>Bridge: connect with Authorization: Bearer <token>
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -66,8 +68,8 @@ sequenceDiagram
|
|||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
T["Account sync parsing tests"] --> T1["assert BRIDGE_SERVER_URL -> AccountSyncState.syncedDefaults.bridgeServerUrl"]
|
T["Account sync parsing tests"] --> T1["assert BRIDGE_SERVER_URL metadata can enter AccountSyncState.syncedDefaults.bridgeServerUrl"]
|
||||||
T --> T2["assert BRIDGE_SERVER_URL -> cloudSynced.remoteServerSummary.endpoint"]
|
T --> T2["assert runtime bridge endpoint stays pinned to https://xworkmate-bridge.svc.plus"]
|
||||||
T --> T3["assert BRIDGE_AUTH_TOKEN -> secure storage target bridge.auth_token"]
|
T --> T3["assert BRIDGE_AUTH_TOKEN -> secure storage target bridge.auth_token"]
|
||||||
T --> T4["assert BRIDGE_AUTH_TOKEN never enters normal settings/profile persistence"]
|
T --> T4["assert BRIDGE_AUTH_TOKEN never enters normal settings/profile persistence"]
|
||||||
T --> T5["assert offline path can still read token from secure storage"]
|
T --> T5["assert offline path can still read token from secure storage"]
|
||||||
@ -75,7 +77,9 @@ flowchart TD
|
|||||||
|
|
||||||
## Expected Invariants
|
## Expected Invariants
|
||||||
|
|
||||||
- `BRIDGE_SERVER_URL` is the only bridge endpoint field used by the sync contract.
|
- Runtime bridge endpoint selection must not depend on `BRIDGE_SERVER_URL`.
|
||||||
|
- The app-facing managed bridge origin is fixed to `https://xworkmate-bridge.svc.plus`.
|
||||||
|
- `BRIDGE_SERVER_URL`, when present, is metadata only.
|
||||||
- `BRIDGE_AUTH_TOKEN` is the only bridge token field used by the sync contract.
|
- `BRIDGE_AUTH_TOKEN` is the only bridge token field used by the sync contract.
|
||||||
- `BRIDGE_AUTH_TOKEN` must never be written into normal settings snapshot, profile JSON, or UI-visible text.
|
- `BRIDGE_AUTH_TOKEN` must never be written into normal settings snapshot, profile JSON, or UI-visible text.
|
||||||
- Client requests must assemble the header as `Authorization: Bearer <token>`.
|
- Client requests must assemble the header as `Authorization: Bearer <token>`.
|
||||||
|
|||||||
@ -12,8 +12,8 @@
|
|||||||
本文默认当前真实拓扑如下:
|
本文默认当前真实拓扑如下:
|
||||||
|
|
||||||
- 在线用户同步会向本地设置注入远程默认值
|
- 在线用户同步会向本地设置注入远程默认值
|
||||||
- ACP 支持 selfhost 远程服务端
|
- bridge / gateway 主链路固定走 managed remote bridge
|
||||||
- ACP 支持 local / loopback 模式
|
- 外部 ACP selfhost 仍可覆盖远程服务端
|
||||||
- 线程执行同时覆盖本地执行型任务与在线执行任务
|
- 线程执行同时覆盖本地执行型任务与在线执行任务
|
||||||
|
|
||||||
## 2. 现有可复用测试基础
|
## 2. 现有可复用测试基础
|
||||||
@ -74,7 +74,7 @@
|
|||||||
- endpoint 规范化
|
- endpoint 规范化
|
||||||
- 账户同步与 settings snapshot
|
- 账户同步与 settings snapshot
|
||||||
- 线程身份、技能绑定、artifact 写回、线程隔离
|
- 线程身份、技能绑定、artifact 写回、线程隔离
|
||||||
- local / remote 模式切换与 provider 选择
|
- remote / offline 模式切换与 provider 选择
|
||||||
|
|
||||||
### 3.2 feature
|
### 3.2 feature
|
||||||
|
|
||||||
@ -101,7 +101,7 @@
|
|||||||
- 结果写入当前线程 workspace 或 artifact snapshot
|
- 结果写入当前线程 workspace 或 artifact snapshot
|
||||||
- 本地执行型与在线执行型都通过同一结果表面暴露产物
|
- 本地执行型与在线执行型都通过同一结果表面暴露产物
|
||||||
- secret 不进入普通 settings snapshot
|
- secret 不进入普通 settings snapshot
|
||||||
- local 模式允许明确的非 TLS 边界,remote 模式不允许静默降级
|
- managed remote 主链路不允许 non-TLS / loopback fallback
|
||||||
- 错误信息按配置错误、连接失败、鉴权失败、任务失败分层呈现
|
- 错误信息按配置错误、连接失败、鉴权失败、任务失败分层呈现
|
||||||
|
|
||||||
## 5. 设置页面配置功能
|
## 5. 设置页面配置功能
|
||||||
@ -154,32 +154,26 @@
|
|||||||
- 兼并到 `test/runtime/external_acp_endpoint_settings_suite.dart`
|
- 兼并到 `test/runtime/external_acp_endpoint_settings_suite.dart`
|
||||||
- 设置页提示补充到 `test/features/settings_page_gateway_acp_messages_suite.dart`
|
- 设置页提示补充到 `test/features/settings_page_gateway_acp_messages_suite.dart`
|
||||||
|
|
||||||
### `ACP-CONFIG-003` local ACP loopback 模式允许非 TLS,remote 模式不允许静默降级
|
### `ACP-CONFIG-003` managed bridge 入口固定,且主链路不允许 loopback / non-TLS fallback
|
||||||
|
|
||||||
- 测试目标
|
- 测试目标
|
||||||
- 明确 local / loopback 与 remote transport trust boundary。
|
- 明确 bridge runtime 不依赖 `BRIDGE_SERVER_URL`,并固定走 managed remote bridge。
|
||||||
- 推荐测试层级
|
- 推荐测试层级
|
||||||
- `runtime`
|
- `runtime`
|
||||||
- `feature`
|
|
||||||
- 前置依赖与假服务
|
- 前置依赖与假服务
|
||||||
- endpoint normalization fixtures
|
- bridge runtime fixtures
|
||||||
- loopback host 样例:
|
- stale env / sync metadata 样例:
|
||||||
- `http://127.0.0.1:9001/opencode`
|
- `BRIDGE_SERVER_URL=https://stale.example.invalid`
|
||||||
- `ws://127.0.0.1:9001/codex`
|
|
||||||
- remote host 样例:
|
|
||||||
- `http://example.com/opencode`
|
|
||||||
- 关键断言
|
- 关键断言
|
||||||
- loopback/local 模式可接受非 TLS
|
- `resolveBridgeAcpEndpointInternal()` 固定返回 `https://xworkmate-bridge.svc.plus`
|
||||||
- remote 模式遇到非 TLS 时给出明确错误或阻止提交
|
- thread send / collaboration send 不再因缺少 `BRIDGE_SERVER_URL` 被阻断
|
||||||
- remote 模式不会 silently rewrite 成 insecure transport
|
- 不存在 local 模式枚举与 fallback
|
||||||
- 失败分类
|
- 失败分类
|
||||||
- loopback 被误拦截
|
- 旧 truth source 仍影响运行态入口
|
||||||
- remote 静默降级
|
- 缺少 `BRIDGE_SERVER_URL` 仍被当作主错误
|
||||||
- 错误分类不清晰
|
- local fallback 未清干净
|
||||||
- 后续实现建议文件落点
|
- 后续实现建议文件落点
|
||||||
- 首选扩展 `test/runtime/gateway_endpoint_normalization_suite.dart`
|
- `test/runtime/bridge_runtime_cleanup_test.dart`
|
||||||
- 连接策略落到 `test/runtime/external_acp_endpoint_settings_suite.dart`
|
|
||||||
- 表单提示补充到 `test/features/settings_page_gateway_acp_messages_suite.dart`
|
|
||||||
|
|
||||||
### `ACP-CONFIG-004` 设置页测试连接对 hosted base URL、自定义 auth、失败提示语分类正确
|
### `ACP-CONFIG-004` 设置页测试连接对 hosted base URL、自定义 auth、失败提示语分类正确
|
||||||
|
|
||||||
@ -190,7 +184,7 @@
|
|||||||
- `integration`
|
- `integration`
|
||||||
- 前置依赖与假服务
|
- 前置依赖与假服务
|
||||||
- fake gateway client
|
- fake gateway client
|
||||||
- hosted / selfhost / local 三类 endpoint fixture
|
- managed bridge / selfhost 两类 endpoint fixture
|
||||||
- fake failure 分类:
|
- fake failure 分类:
|
||||||
- 鉴权失败
|
- 鉴权失败
|
||||||
- 空响应
|
- 空响应
|
||||||
|
|||||||
@ -191,9 +191,13 @@ flutter test test/features/assistant_page_suite.dart
|
|||||||
- `https://accounts.svc.plus`
|
- `https://accounts.svc.plus`
|
||||||
- `review@svc.plus`
|
- `review@svc.plus`
|
||||||
- `***REMOVED-CREDENTIAL***`
|
- `***REMOVED-CREDENTIAL***`
|
||||||
- `BRIDGE_SERVER_URL=https:xworkmate-bridge.svc.plus`
|
- managed bridge origin: `https://xworkmate-bridge.svc.plus`
|
||||||
- `BRIDGE_AUTH_TOKEN=...`
|
- `BRIDGE_AUTH_TOKEN=...`
|
||||||
|
|
||||||
|
补充口径:
|
||||||
|
|
||||||
|
- `BRIDGE_SERVER_URL` 若仍出现在账户返回中,仅作为 metadata,不再是运行期入口前置条件。
|
||||||
|
|
||||||
额外约定:
|
额外约定:
|
||||||
|
|
||||||
- UI 本轮不改结构,只验证 provider 列表来源、展示结果与 thread 内状态。
|
- UI 本轮不改结构,只验证 provider 列表来源、展示结果与 thread 内状态。
|
||||||
|
|||||||
@ -177,9 +177,7 @@ extension AppControllerDesktopGateway on AppController {
|
|||||||
String token = '',
|
String token = '',
|
||||||
String password = '',
|
String password = '',
|
||||||
}) async {
|
}) async {
|
||||||
final normalizedMode = mode == RuntimeConnectionMode.local
|
final normalizedMode = RuntimeConnectionMode.remote;
|
||||||
? RuntimeConnectionMode.remote
|
|
||||||
: mode;
|
|
||||||
final nextTarget = assistantExecutionTargetForModeInternal(normalizedMode);
|
final nextTarget = assistantExecutionTargetForModeInternal(normalizedMode);
|
||||||
final nextProfileIndex = gatewayProfileIndexForExecutionTargetInternal(
|
final nextProfileIndex = gatewayProfileIndexForExecutionTargetInternal(
|
||||||
nextTarget,
|
nextTarget,
|
||||||
@ -198,7 +196,7 @@ extension AppControllerDesktopGateway on AppController {
|
|||||||
setupCode: '',
|
setupCode: '',
|
||||||
host: resolvedHost,
|
host: resolvedHost,
|
||||||
port: resolvedPort <= 0 ? 443 : resolvedPort,
|
port: resolvedPort <= 0 ? 443 : resolvedPort,
|
||||||
tls: normalizedMode == RuntimeConnectionMode.local ? false : tls,
|
tls: tls,
|
||||||
);
|
);
|
||||||
await AppControllerDesktopSettings(this).saveSettings(
|
await AppControllerDesktopSettings(this).saveSettings(
|
||||||
settings
|
settings
|
||||||
|
|||||||
@ -655,22 +655,7 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Uri? resolveBridgeAcpEndpointInternal() {
|
Uri? resolveBridgeAcpEndpointInternal() {
|
||||||
final endpoint =
|
final uri = Uri.tryParse(kManagedBridgeServerUrl);
|
||||||
runtimeEnvironmentValueInternal('BRIDGE_SERVER_URL') ??
|
|
||||||
(() {
|
|
||||||
final synced =
|
|
||||||
settingsControllerInternal
|
|
||||||
.accountSyncState
|
|
||||||
?.syncedDefaults
|
|
||||||
.bridgeServerUrl
|
|
||||||
.trim() ??
|
|
||||||
'';
|
|
||||||
return synced.isEmpty ? null : synced;
|
|
||||||
})();
|
|
||||||
if (endpoint == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final uri = Uri.tryParse(endpoint);
|
|
||||||
final scheme = uri?.scheme.trim().toLowerCase() ?? '';
|
final scheme = uri?.scheme.trim().toLowerCase() ?? '';
|
||||||
if (uri == null || !kSupportedExternalAcpEndpointSchemes.contains(scheme)) {
|
if (uri == null || !kSupportedExternalAcpEndpointSchemes.contains(scheme)) {
|
||||||
return null;
|
return null;
|
||||||
@ -734,18 +719,9 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
RuntimeConnectionMode modeFromHostInternal(String host) {
|
RuntimeConnectionMode modeFromHostInternal(String host) {
|
||||||
final trimmed = host.trim().toLowerCase();
|
|
||||||
if (isLoopbackHostInternal(trimmed)) {
|
|
||||||
return RuntimeConnectionMode.local;
|
|
||||||
}
|
|
||||||
return RuntimeConnectionMode.remote;
|
return RuntimeConnectionMode.remote;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isLoopbackHostInternal(String host) {
|
|
||||||
final trimmed = host.trim().toLowerCase();
|
|
||||||
return trimmed == '127.0.0.1' || trimmed == 'localhost';
|
|
||||||
}
|
|
||||||
|
|
||||||
AssistantExecutionTarget assistantExecutionTargetForModeInternal(
|
AssistantExecutionTarget assistantExecutionTargetForModeInternal(
|
||||||
RuntimeConnectionMode mode,
|
RuntimeConnectionMode mode,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@ -285,15 +285,6 @@ extension AppControllerDesktopThreadActions on AppController {
|
|||||||
recomputeTasksInternal();
|
recomputeTasksInternal();
|
||||||
notifyIfActiveInternal();
|
notifyIfActiveInternal();
|
||||||
try {
|
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
|
final dispatch = await codeAgentNodeOrchestratorInternal
|
||||||
.buildGatewayDispatch(buildCodeAgentNodeStateInternal());
|
.buildGatewayDispatch(buildCodeAgentNodeStateInternal());
|
||||||
final result = await goTaskServiceClientInternal.executeTask(
|
final result = await goTaskServiceClientInternal.executeTask(
|
||||||
|
|||||||
@ -106,34 +106,6 @@ Future<void> runMultiAgentCollaborationThreadSessionInternal(
|
|||||||
? 'main'
|
? 'main'
|
||||||
: controller.currentSessionKey;
|
: controller.currentSessionKey;
|
||||||
await controller.enqueueThreadTurnInternal<void>(sessionKey, () async {
|
await controller.enqueueThreadTurnInternal<void>(sessionKey, () async {
|
||||||
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(
|
await controller.ensureDesktopTaskThreadBindingInternal(
|
||||||
sessionKey,
|
sessionKey,
|
||||||
executionTarget: controller.assistantExecutionTargetForSession(
|
executionTarget: controller.assistantExecutionTargetForSession(
|
||||||
@ -391,9 +363,6 @@ bool canQuickConnectGatewayThreadSessionInternal(AppController controller) {
|
|||||||
if (host.isEmpty || profile.port <= 0) {
|
if (host.isEmpty || profile.port <= 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (profile.mode == RuntimeConnectionMode.local) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
final defaults = GatewayConnectionProfile.defaults();
|
final defaults = GatewayConnectionProfile.defaults();
|
||||||
return controller.hasStoredGatewayCredential ||
|
return controller.hasStoredGatewayCredential ||
|
||||||
host != defaults.host ||
|
host != defaults.host ||
|
||||||
|
|||||||
@ -643,7 +643,6 @@ String mobileSecurePathLabelInternal({
|
|||||||
? profile.mode
|
? profile.mode
|
||||||
: connection.mode;
|
: connection.mode;
|
||||||
return switch (mode) {
|
return switch (mode) {
|
||||||
RuntimeConnectionMode.local => appText('Loopback WS', 'Loopback WS'),
|
|
||||||
RuntimeConnectionMode.remote =>
|
RuntimeConnectionMode.remote =>
|
||||||
profile.tls
|
profile.tls
|
||||||
? appText('Secure Direct TLS', 'Secure Direct TLS')
|
? appText('Secure Direct TLS', 'Secure Direct TLS')
|
||||||
|
|||||||
@ -21,7 +21,6 @@ class SettingsAccountPanel extends StatelessWidget {
|
|||||||
required this.onVerifyMfa,
|
required this.onVerifyMfa,
|
||||||
required this.onCancelMfa,
|
required this.onCancelMfa,
|
||||||
required this.onSync,
|
required this.onSync,
|
||||||
required this.onDisconnect,
|
|
||||||
required this.onLogout,
|
required this.onLogout,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -40,11 +39,8 @@ class SettingsAccountPanel extends StatelessWidget {
|
|||||||
final Future<void> Function() onVerifyMfa;
|
final Future<void> Function() onVerifyMfa;
|
||||||
final Future<void> Function() onCancelMfa;
|
final Future<void> Function() onCancelMfa;
|
||||||
final Future<void> Function() onSync;
|
final Future<void> Function() onSync;
|
||||||
final Future<void> Function() onDisconnect;
|
|
||||||
final Future<void> Function() onLogout;
|
final Future<void> Function() onLogout;
|
||||||
|
|
||||||
bool get _managedConnected => !settings.accountLocalMode;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (!accountSignedIn && !accountMfaRequired) {
|
if (!accountSignedIn && !accountMfaRequired) {
|
||||||
@ -72,9 +68,7 @@ class SettingsAccountPanel extends StatelessWidget {
|
|||||||
accountSession: accountSession,
|
accountSession: accountSession,
|
||||||
accountState: accountState,
|
accountState: accountState,
|
||||||
accountBusy: accountBusy,
|
accountBusy: accountBusy,
|
||||||
managedConnected: _managedConnected,
|
|
||||||
onSync: onSync,
|
onSync: onSync,
|
||||||
onDisconnect: onDisconnect,
|
|
||||||
onLogout: onLogout,
|
onLogout: onLogout,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -290,9 +284,7 @@ class _SignedInAccountPanel extends StatelessWidget {
|
|||||||
required this.accountSession,
|
required this.accountSession,
|
||||||
required this.accountState,
|
required this.accountState,
|
||||||
required this.accountBusy,
|
required this.accountBusy,
|
||||||
required this.managedConnected,
|
|
||||||
required this.onSync,
|
required this.onSync,
|
||||||
required this.onDisconnect,
|
|
||||||
required this.onLogout,
|
required this.onLogout,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -300,9 +292,7 @@ class _SignedInAccountPanel extends StatelessWidget {
|
|||||||
final AccountSessionSummary? accountSession;
|
final AccountSessionSummary? accountSession;
|
||||||
final AccountSyncState? accountState;
|
final AccountSyncState? accountState;
|
||||||
final bool accountBusy;
|
final bool accountBusy;
|
||||||
final bool managedConnected;
|
|
||||||
final Future<void> Function() onSync;
|
final Future<void> Function() onSync;
|
||||||
final Future<void> Function() onDisconnect;
|
|
||||||
final Future<void> Function() onLogout;
|
final Future<void> Function() onLogout;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -320,14 +310,10 @@ class _SignedInAccountPanel extends StatelessWidget {
|
|||||||
final syncScope = accountState?.profileScope.trim().isNotEmpty == true
|
final syncScope = accountState?.profileScope.trim().isNotEmpty == true
|
||||||
? accountState!.profileScope.trim()
|
? accountState!.profileScope.trim()
|
||||||
: appText('待同步', 'Pending sync');
|
: appText('待同步', 'Pending sync');
|
||||||
final syncState = !managedConnected
|
final syncState = accountState?.syncState.trim().isNotEmpty == true
|
||||||
? appText('已断开', 'Disconnected')
|
|
||||||
: accountState?.syncState.trim().isNotEmpty == true
|
|
||||||
? accountState!.syncState.trim()
|
? accountState!.syncState.trim()
|
||||||
: 'idle';
|
: 'idle';
|
||||||
final syncMessage = !managedConnected
|
final syncMessage = accountState?.syncMessage.trim().isNotEmpty == true
|
||||||
? appText('当前使用本地连接配置', 'Using local connection settings')
|
|
||||||
: accountState?.syncMessage.trim().isNotEmpty == true
|
|
||||||
? accountState!.syncMessage.trim()
|
? accountState!.syncMessage.trim()
|
||||||
: appText('尚未同步远端配置', 'Remote config not synced yet');
|
: appText('尚未同步远端配置', 'Remote config not synced yet');
|
||||||
final mfaEnabled =
|
final mfaEnabled =
|
||||||
@ -389,7 +375,7 @@ class _SignedInAccountPanel extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
'${appText('连接来源', 'Connection Source')}: ${managedConnected ? appText('svc.plus 托管配置', 'svc.plus managed profile') : appText('本地配置', 'Local profile')}',
|
'${appText('连接来源', 'Connection Source')}: ${appText('svc.plus 托管配置', 'svc.plus managed profile')}',
|
||||||
key: const ValueKey(
|
key: const ValueKey(
|
||||||
'settings-account-summary-connection-source',
|
'settings-account-summary-connection-source',
|
||||||
),
|
),
|
||||||
@ -429,21 +415,13 @@ class _SignedInAccountPanel extends StatelessWidget {
|
|||||||
onPressed: accountBusy ? null : () => onSync(),
|
onPressed: accountBusy ? null : () => onSync(),
|
||||||
child: Text(appText('重新同步', 'Sync Again')),
|
child: Text(appText('重新同步', 'Sync Again')),
|
||||||
),
|
),
|
||||||
FilledButton.tonal(
|
TextButton(
|
||||||
key: const ValueKey('settings-account-disconnect-button'),
|
key: const ValueKey('settings-account-logout-button'),
|
||||||
onPressed: accountBusy || !managedConnected
|
onPressed: accountBusy ? null : () => onLogout(),
|
||||||
? null
|
child: Text(appText('退出登录', 'Log Out')),
|
||||||
: () => onDisconnect(),
|
|
||||||
child: Text(appText('断开', 'Disconnect')),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
|
||||||
TextButton(
|
|
||||||
key: const ValueKey('settings-account-logout-button'),
|
|
||||||
onPressed: accountBusy ? null : () => onLogout(),
|
|
||||||
child: Text(appText('退出登录', 'Log Out')),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -152,10 +152,6 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
_accountMfaCodeController.clear();
|
_accountMfaCodeController.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _disconnectManagedBase() async {
|
|
||||||
await widget.controller.settingsController.disconnectManagedAccountBase();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final controller = widget.controller;
|
final controller = widget.controller;
|
||||||
@ -217,7 +213,6 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
onVerifyMfa: () => _verifyAccountMfa(currentSettings),
|
onVerifyMfa: () => _verifyAccountMfa(currentSettings),
|
||||||
onCancelMfa: _cancelAccountMfa,
|
onCancelMfa: _cancelAccountMfa,
|
||||||
onSync: () => _syncAccount(currentSettings),
|
onSync: () => _syncAccount(currentSettings),
|
||||||
onDisconnect: _disconnectManagedBase,
|
|
||||||
onLogout: _logoutAccount,
|
onLogout: _logoutAccount,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,9 +1,4 @@
|
|||||||
// Gateway mode switching logic.
|
// Gateway mode switching logic for remote bridge mode and offline mode.
|
||||||
//
|
|
||||||
// Handles transitions between:
|
|
||||||
// - Local mode (127.0.0.1:18789): Full functionality, no cloud memory
|
|
||||||
// - Remote mode (configured bridge endpoint): Full functionality with cloud memory
|
|
||||||
// - Offline mode: Local Codex only, limited functionality
|
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
@ -14,9 +9,6 @@ import 'runtime_models.dart';
|
|||||||
|
|
||||||
/// Gateway operating mode.
|
/// Gateway operating mode.
|
||||||
enum GatewayMode {
|
enum GatewayMode {
|
||||||
/// Local mode: Gateway running locally at 127.0.0.1:18789
|
|
||||||
local,
|
|
||||||
|
|
||||||
/// Remote mode: Gateway connected through the configured bridge endpoint
|
/// Remote mode: Gateway connected through the configured bridge endpoint
|
||||||
remote,
|
remote,
|
||||||
|
|
||||||
@ -32,9 +24,6 @@ enum ModeSwitcherState {
|
|||||||
/// Attempting to connect
|
/// Attempting to connect
|
||||||
connecting,
|
connecting,
|
||||||
|
|
||||||
/// Connected in local mode
|
|
||||||
connectedLocal,
|
|
||||||
|
|
||||||
/// Connected in remote mode
|
/// Connected in remote mode
|
||||||
connectedRemote,
|
connectedRemote,
|
||||||
|
|
||||||
@ -76,15 +65,6 @@ class ModeCapabilities {
|
|||||||
required this.hasCodeAgent,
|
required this.hasCodeAgent,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Local mode capabilities.
|
|
||||||
static const ModeCapabilities local = ModeCapabilities(
|
|
||||||
hasCloudMemory: false,
|
|
||||||
hasTaskQueue: false,
|
|
||||||
hasMultiAgent: false,
|
|
||||||
hasLocalModels: true,
|
|
||||||
hasCodeAgent: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Remote mode capabilities.
|
/// Remote mode capabilities.
|
||||||
static const ModeCapabilities remote = ModeCapabilities(
|
static const ModeCapabilities remote = ModeCapabilities(
|
||||||
hasCloudMemory: true,
|
hasCloudMemory: true,
|
||||||
@ -112,7 +92,7 @@ class ModeCapabilities {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Manages mode switching between local, remote, and offline modes.
|
/// Manages mode switching between remote and offline modes.
|
||||||
class ModeSwitcher extends ChangeNotifier {
|
class ModeSwitcher extends ChangeNotifier {
|
||||||
final GatewayRuntime _gateway;
|
final GatewayRuntime _gateway;
|
||||||
|
|
||||||
@ -130,62 +110,6 @@ class ModeSwitcher extends ChangeNotifier {
|
|||||||
|
|
||||||
ModeSwitcher(this._gateway);
|
ModeSwitcher(this._gateway);
|
||||||
|
|
||||||
/// Switch to local mode.
|
|
||||||
Future<ModeSwitchResult> switchToLocal({
|
|
||||||
String host = '127.0.0.1',
|
|
||||||
int port = 18789,
|
|
||||||
String? token,
|
|
||||||
}) async {
|
|
||||||
if (_state == ModeSwitcherState.connectedLocal) {
|
|
||||||
return ModeSwitchResult(success: true, mode: GatewayMode.local);
|
|
||||||
}
|
|
||||||
|
|
||||||
_state = ModeSwitcherState.connecting;
|
|
||||||
_lastError = null;
|
|
||||||
notifyListeners();
|
|
||||||
|
|
||||||
try {
|
|
||||||
final profile = GatewayConnectionProfile.defaults().copyWith(
|
|
||||||
mode: RuntimeConnectionMode.local,
|
|
||||||
host: host,
|
|
||||||
port: port,
|
|
||||||
tls: false,
|
|
||||||
);
|
|
||||||
|
|
||||||
await _gateway.connectProfile(profile, authTokenOverride: token ?? '');
|
|
||||||
|
|
||||||
// Wait for connection
|
|
||||||
await _gateway.events
|
|
||||||
.where(
|
|
||||||
(e) => e.event == 'gateway/ready' || e.event == 'gateway/connected',
|
|
||||||
)
|
|
||||||
.first
|
|
||||||
.timeout(const Duration(seconds: 30));
|
|
||||||
|
|
||||||
_state = ModeSwitcherState.connectedLocal;
|
|
||||||
_currentMode = GatewayMode.local;
|
|
||||||
_capabilities = ModeCapabilities.local;
|
|
||||||
_lastModeChange = DateTime.now();
|
|
||||||
notifyListeners();
|
|
||||||
|
|
||||||
return ModeSwitchResult(
|
|
||||||
success: true,
|
|
||||||
mode: GatewayMode.local,
|
|
||||||
capabilities: _capabilities.toMap(),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
_state = ModeSwitcherState.error;
|
|
||||||
_lastError = e.toString();
|
|
||||||
notifyListeners();
|
|
||||||
|
|
||||||
return ModeSwitchResult(
|
|
||||||
success: false,
|
|
||||||
mode: GatewayMode.local,
|
|
||||||
error: e.toString(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Switch to remote mode.
|
/// Switch to remote mode.
|
||||||
Future<ModeSwitchResult> switchToRemote({
|
Future<ModeSwitchResult> switchToRemote({
|
||||||
String host = '',
|
String host = '',
|
||||||
@ -279,30 +203,6 @@ class ModeSwitcher extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Auto-select best available mode.
|
|
||||||
Future<ModeSwitchResult> autoSelect({
|
|
||||||
String? localToken,
|
|
||||||
String? remoteToken,
|
|
||||||
bool preferRemote = true,
|
|
||||||
}) async {
|
|
||||||
// Try remote first if preferred
|
|
||||||
if (preferRemote) {
|
|
||||||
final remoteResult = await switchToRemote(token: remoteToken);
|
|
||||||
if (remoteResult.success) {
|
|
||||||
return remoteResult;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try local
|
|
||||||
final localResult = await switchToLocal(token: localToken);
|
|
||||||
if (localResult.success) {
|
|
||||||
return localResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to offline
|
|
||||||
return switchToOffline();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get current state description.
|
/// Get current state description.
|
||||||
String get stateDescription {
|
String get stateDescription {
|
||||||
switch (_state) {
|
switch (_state) {
|
||||||
@ -310,8 +210,6 @@ class ModeSwitcher extends ChangeNotifier {
|
|||||||
return 'Disconnected';
|
return 'Disconnected';
|
||||||
case ModeSwitcherState.connecting:
|
case ModeSwitcherState.connecting:
|
||||||
return 'Connecting...';
|
return 'Connecting...';
|
||||||
case ModeSwitcherState.connectedLocal:
|
|
||||||
return 'Connected (Local)';
|
|
||||||
case ModeSwitcherState.connectedRemote:
|
case ModeSwitcherState.connectedRemote:
|
||||||
return 'Connected (Remote)';
|
return 'Connected (Remote)';
|
||||||
case ModeSwitcherState.offline:
|
case ModeSwitcherState.offline:
|
||||||
@ -324,8 +222,6 @@ class ModeSwitcher extends ChangeNotifier {
|
|||||||
/// Get current mode description.
|
/// Get current mode description.
|
||||||
String get modeDescription {
|
String get modeDescription {
|
||||||
switch (_currentMode) {
|
switch (_currentMode) {
|
||||||
case GatewayMode.local:
|
|
||||||
return 'Local Mode (127.0.0.1:18789)';
|
|
||||||
case GatewayMode.remote:
|
case GatewayMode.remote:
|
||||||
return 'Remote Mode (Configured bridge endpoint)';
|
return 'Remote Mode (Configured bridge endpoint)';
|
||||||
case GatewayMode.offline:
|
case GatewayMode.offline:
|
||||||
|
|||||||
@ -7,14 +7,12 @@ class RuntimeBootstrapConfig {
|
|||||||
required this.workspacePath,
|
required this.workspacePath,
|
||||||
required this.remoteProjectRoot,
|
required this.remoteProjectRoot,
|
||||||
required this.cliPath,
|
required this.cliPath,
|
||||||
required this.localGateway,
|
|
||||||
required this.remoteGateway,
|
required this.remoteGateway,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String? workspacePath;
|
final String? workspacePath;
|
||||||
final String? remoteProjectRoot;
|
final String? remoteProjectRoot;
|
||||||
final String? cliPath;
|
final String? cliPath;
|
||||||
final GatewayBootstrapTarget? localGateway;
|
|
||||||
final GatewayBootstrapTarget? remoteGateway;
|
final GatewayBootstrapTarget? remoteGateway;
|
||||||
|
|
||||||
static Future<RuntimeBootstrapConfig> load({
|
static Future<RuntimeBootstrapConfig> load({
|
||||||
@ -36,10 +34,6 @@ class RuntimeBootstrapConfig {
|
|||||||
workspacePath: workspaceRoot?.path,
|
workspacePath: workspaceRoot?.path,
|
||||||
remoteProjectRoot: workspaceRoot?.path,
|
remoteProjectRoot: workspaceRoot?.path,
|
||||||
cliPath: _resolveCliPath(openClawRoot),
|
cliPath: _resolveCliPath(openClawRoot),
|
||||||
localGateway: GatewayBootstrapTarget.tryParse(
|
|
||||||
env['local'],
|
|
||||||
token: env['local-token'],
|
|
||||||
),
|
|
||||||
remoteGateway: GatewayBootstrapTarget.tryParse(
|
remoteGateway: GatewayBootstrapTarget.tryParse(
|
||||||
env['remote'],
|
env['remote'],
|
||||||
token: env['remote-token'],
|
token: env['remote-token'],
|
||||||
@ -86,9 +80,8 @@ class RuntimeBootstrapConfig {
|
|||||||
|
|
||||||
GatewayBootstrapTarget? preferredGatewayFor(RuntimeConnectionMode mode) {
|
GatewayBootstrapTarget? preferredGatewayFor(RuntimeConnectionMode mode) {
|
||||||
return switch (mode) {
|
return switch (mode) {
|
||||||
RuntimeConnectionMode.local => localGateway ?? remoteGateway,
|
RuntimeConnectionMode.remote => remoteGateway,
|
||||||
RuntimeConnectionMode.remote => remoteGateway ?? localGateway,
|
RuntimeConnectionMode.unconfigured => remoteGateway,
|
||||||
RuntimeConnectionMode.unconfigured => remoteGateway ?? localGateway,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,11 +155,8 @@ class GatewayBootstrapTarget {
|
|||||||
final tls = scheme == 'wss' || scheme == 'https';
|
final tls = scheme == 'wss' || scheme == 'https';
|
||||||
final port = uri.hasPort ? uri.port : (tls ? 443 : 18789);
|
final port = uri.hasPort ? uri.port : (tls ? 443 : 18789);
|
||||||
final host = uri.host.trim();
|
final host = uri.host.trim();
|
||||||
final isLocal = host == '127.0.0.1' || host == 'localhost';
|
|
||||||
return GatewayBootstrapTarget(
|
return GatewayBootstrapTarget(
|
||||||
mode: isLocal
|
mode: RuntimeConnectionMode.remote,
|
||||||
? RuntimeConnectionMode.local
|
|
||||||
: RuntimeConnectionMode.remote,
|
|
||||||
url: trimmed,
|
url: trimmed,
|
||||||
host: host,
|
host: host,
|
||||||
port: port,
|
port: port,
|
||||||
|
|||||||
@ -95,9 +95,6 @@ extension SettingsControllerAccountExtension on SettingsController {
|
|||||||
Future<AccountSyncResult> syncAccountManagedSecrets({String baseUrl = ''}) =>
|
Future<AccountSyncResult> syncAccountManagedSecrets({String baseUrl = ''}) =>
|
||||||
syncAccountSettings(baseUrl: baseUrl);
|
syncAccountSettings(baseUrl: baseUrl);
|
||||||
|
|
||||||
Future<void> disconnectManagedAccountBase() =>
|
|
||||||
disconnectManagedAccountBaseSettingsInternal(this);
|
|
||||||
|
|
||||||
Future<void> logoutAccount() => logoutAccountSettingsInternal(this);
|
Future<void> logoutAccount() => logoutAccountSettingsInternal(this);
|
||||||
|
|
||||||
Future<void> cancelAccountMfaChallenge() =>
|
Future<void> cancelAccountMfaChallenge() =>
|
||||||
|
|||||||
@ -146,7 +146,6 @@ Future<void> completeAccountSignInSettingsInternal(
|
|||||||
controller,
|
controller,
|
||||||
baseUrl: baseUrl,
|
baseUrl: baseUrl,
|
||||||
bridgeTokenOverride: _resolveBridgeAuthorizationToken(payload),
|
bridgeTokenOverride: _resolveBridgeAuthorizationToken(payload),
|
||||||
bridgeServerUrlOverride: _resolveBridgeServerUrl(payload),
|
|
||||||
quiet: true,
|
quiet: true,
|
||||||
);
|
);
|
||||||
await controller.reloadDerivedStateInternal();
|
await controller.reloadDerivedStateInternal();
|
||||||
@ -224,7 +223,6 @@ Future<AccountSyncResult> syncAccountSettingsInternal(
|
|||||||
String baseUrl = '',
|
String baseUrl = '',
|
||||||
bool quiet = false,
|
bool quiet = false,
|
||||||
String bridgeTokenOverride = '',
|
String bridgeTokenOverride = '',
|
||||||
String bridgeServerUrlOverride = '',
|
|
||||||
}) async {
|
}) async {
|
||||||
final sessionToken =
|
final sessionToken =
|
||||||
(await controller.storeInternal.loadAccountSessionToken())?.trim() ?? '';
|
(await controller.storeInternal.loadAccountSessionToken())?.trim() ?? '';
|
||||||
@ -288,39 +286,7 @@ Future<AccountSyncResult> syncAccountSettingsInternal(
|
|||||||
target: kAccountManagedSecretTargetBridgeAuthToken,
|
target: kAccountManagedSecretTargetBridgeAuthToken,
|
||||||
value: bridgeToken,
|
value: bridgeToken,
|
||||||
);
|
);
|
||||||
|
const resolvedBridgeServerUrl = kManagedBridgeServerUrl;
|
||||||
final resolvedBridgeServerUrl = bridgeServerUrlOverride.trim().isNotEmpty
|
|
||||||
? bridgeServerUrlOverride.trim()
|
|
||||||
: controller.accountSyncStateInternal?.syncedDefaults.bridgeServerUrl
|
|
||||||
.trim() ??
|
|
||||||
'';
|
|
||||||
if (!isSupportedExternalAcpEndpoint(resolvedBridgeServerUrl)) {
|
|
||||||
const result = AccountSyncResult(
|
|
||||||
state: 'blocked',
|
|
||||||
message: 'BRIDGE_SERVER_URL is unavailable',
|
|
||||||
);
|
|
||||||
await _persistAccountSyncStateInternal(
|
|
||||||
controller,
|
|
||||||
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(
|
await controller.storeInternal.clearAccountManagedSecret(
|
||||||
target: kAccountManagedSecretTargetAIGatewayAccessToken,
|
target: kAccountManagedSecretTargetAIGatewayAccessToken,
|
||||||
);
|
);
|
||||||
@ -360,10 +326,7 @@ Future<AccountSyncResult> syncAccountSettingsInternal(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
final sanitizedSettings = _sanitizeBridgeOnlyAccountSyncSettings(
|
final sanitizedSettings = _sanitizeBridgeOnlyAccountSyncSettings(
|
||||||
currentSettings.copyWith(
|
currentSettings.copyWith(acpBridgeServerModeConfig: nextModeConfig),
|
||||||
accountLocalMode: false,
|
|
||||||
acpBridgeServerModeConfig: nextModeConfig,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
if (sanitizedSettings.toJsonString() != currentSettings.toJsonString()) {
|
if (sanitizedSettings.toJsonString() != currentSettings.toJsonString()) {
|
||||||
await controller.saveSnapshot(sanitizedSettings);
|
await controller.saveSnapshot(sanitizedSettings);
|
||||||
@ -392,9 +355,6 @@ Future<AccountSyncState?> recoverBridgeAccountSyncStateInternal(
|
|||||||
if (currentBridgeServerUrl.isNotEmpty) {
|
if (currentBridgeServerUrl.isNotEmpty) {
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
if (controller.snapshotInternal.accountLocalMode) {
|
|
||||||
return currentState;
|
|
||||||
}
|
|
||||||
|
|
||||||
final cloudSynced =
|
final cloudSynced =
|
||||||
controller.snapshotInternal.acpBridgeServerModeConfig.cloudSynced;
|
controller.snapshotInternal.acpBridgeServerModeConfig.cloudSynced;
|
||||||
@ -465,21 +425,12 @@ Future<void> logoutAccountSettingsInternal(
|
|||||||
.remoteServerSummary
|
.remoteServerSummary
|
||||||
.copyWith(endpoint: '', hasAdvancedOverrides: false),
|
.copyWith(endpoint: '', hasAdvancedOverrides: false),
|
||||||
);
|
);
|
||||||
if (!controller.snapshotInternal.accountLocalMode) {
|
await controller.saveSnapshot(
|
||||||
await controller.saveSnapshot(
|
currentSnapshot.copyWith(
|
||||||
currentSnapshot.copyWith(
|
|
||||||
accountLocalMode: true,
|
|
||||||
acpBridgeServerModeConfig: currentSnapshot.acpBridgeServerModeConfig
|
|
||||||
.copyWith(cloudSynced: clearedCloudSync),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
controller.snapshotInternal = currentSnapshot.copyWith(
|
|
||||||
acpBridgeServerModeConfig: currentSnapshot.acpBridgeServerModeConfig
|
acpBridgeServerModeConfig: currentSnapshot.acpBridgeServerModeConfig
|
||||||
.copyWith(cloudSynced: clearedCloudSync),
|
.copyWith(cloudSynced: clearedCloudSync),
|
||||||
);
|
),
|
||||||
await controller.reloadDerivedStateInternal();
|
);
|
||||||
}
|
|
||||||
controller.accountStatusInternal = statusMessage;
|
controller.accountStatusInternal = statusMessage;
|
||||||
if (!quiet) {
|
if (!quiet) {
|
||||||
controller.accountBusyInternal = false;
|
controller.accountBusyInternal = false;
|
||||||
@ -487,38 +438,6 @@ Future<void> logoutAccountSettingsInternal(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> disconnectManagedAccountBaseSettingsInternal(
|
|
||||||
SettingsController controller,
|
|
||||||
) async {
|
|
||||||
final currentSnapshot = controller.snapshotInternal;
|
|
||||||
final cloudSynced = currentSnapshot.acpBridgeServerModeConfig.cloudSynced;
|
|
||||||
final nextState =
|
|
||||||
controller.accountSyncStateInternal ?? AccountSyncState.defaults();
|
|
||||||
await _persistAccountSyncStateInternal(
|
|
||||||
controller,
|
|
||||||
nextState.copyWith(
|
|
||||||
syncState: 'disconnected',
|
|
||||||
syncMessage: 'Using local connection settings',
|
|
||||||
lastSyncError: '',
|
|
||||||
profileScope: nextState.profileScope.trim().isEmpty
|
|
||||||
? 'bridge'
|
|
||||||
: nextState.profileScope,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await controller.saveSnapshot(
|
|
||||||
currentSnapshot.copyWith(
|
|
||||||
accountLocalMode: true,
|
|
||||||
acpBridgeServerModeConfig: currentSnapshot.acpBridgeServerModeConfig
|
|
||||||
.copyWith(
|
|
||||||
cloudSynced: cloudSynced.copyWith(
|
|
||||||
accountBaseUrl: '',
|
|
||||||
accountIdentifier: '',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> cancelAccountMfaChallengeSettingsInternal(
|
Future<void> cancelAccountMfaChallengeSettingsInternal(
|
||||||
SettingsController controller,
|
SettingsController controller,
|
||||||
) async {
|
) async {
|
||||||
@ -593,14 +512,6 @@ String _resolveBridgeAuthorizationToken(Map<String, dynamic> payload) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
String _resolveBridgeServerUrl(Map<String, dynamic> payload) {
|
|
||||||
final explicit = _stringValue(payload['BRIDGE_SERVER_URL']);
|
|
||||||
if (explicit.isNotEmpty) {
|
|
||||||
return explicit;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
int _parseExpiresAtMs(Object? value) {
|
int _parseExpiresAtMs(Object? value) {
|
||||||
if (value is int) {
|
if (value is int) {
|
||||||
return value;
|
return value;
|
||||||
|
|||||||
@ -171,8 +171,7 @@ bool hasStoredGatewayTokenForProfileSettingsInternal(
|
|||||||
controller.secureRefsInternal.containsKey(
|
controller.secureRefsInternal.containsKey(
|
||||||
gatewayTokenRefForProfileSettingsInternal(controller, profileIndex),
|
gatewayTokenRefForProfileSettingsInternal(controller, profileIndex),
|
||||||
) ||
|
) ||
|
||||||
(!controller.snapshotInternal.accountLocalMode &&
|
(profileIndex == kGatewayRemoteProfileIndex &&
|
||||||
profileIndex == kGatewayRemoteProfileIndex &&
|
|
||||||
controller.secureRefsInternal.containsKey(
|
controller.secureRefsInternal.containsKey(
|
||||||
kAccountManagedSecretTargetBridgeAuthToken,
|
kAccountManagedSecretTargetBridgeAuthToken,
|
||||||
));
|
));
|
||||||
@ -192,8 +191,7 @@ String? storedGatewayTokenMaskForProfileSettingsInternal(
|
|||||||
controller,
|
controller,
|
||||||
profileIndex,
|
profileIndex,
|
||||||
)] ??
|
)] ??
|
||||||
(!controller.snapshotInternal.accountLocalMode &&
|
(profileIndex == kGatewayRemoteProfileIndex
|
||||||
profileIndex == kGatewayRemoteProfileIndex
|
|
||||||
? controller
|
? controller
|
||||||
.secureRefsInternal[kAccountManagedSecretTargetBridgeAuthToken]
|
.secureRefsInternal[kAccountManagedSecretTargetBridgeAuthToken]
|
||||||
: null);
|
: null);
|
||||||
|
|||||||
@ -21,7 +21,7 @@ enum CoordinatorState { disconnected, connecting, connected, ready, error }
|
|||||||
/// This class coordinates:
|
/// This class coordinates:
|
||||||
/// - GatewayRuntime: Connection to OpenClaw Gateway
|
/// - GatewayRuntime: Connection to OpenClaw Gateway
|
||||||
/// - CodexRuntime: Code agent runtime (external CLI or built-in runtime mode)
|
/// - CodexRuntime: Code agent runtime (external CLI or built-in runtime mode)
|
||||||
/// - ModeSwitcher: Local/Remote/Offline mode switching
|
/// - ModeSwitcher: Remote/Offline mode switching
|
||||||
/// - Extensible external code-agent provider descriptors for future CLIs
|
/// - Extensible external code-agent provider descriptors for future CLIs
|
||||||
class RuntimeCoordinator extends ChangeNotifier {
|
class RuntimeCoordinator extends ChangeNotifier {
|
||||||
final GatewayRuntime gateway;
|
final GatewayRuntime gateway;
|
||||||
@ -242,8 +242,9 @@ class RuntimeCoordinator extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Auto-select best available mode
|
final result = preferRemote
|
||||||
final result = await modeSwitcher.autoSelect(preferRemote: preferRemote);
|
? await modeSwitcher.switchToRemote()
|
||||||
|
: await modeSwitcher.switchToOffline();
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw StateError('No available connection mode: ${result.error}');
|
throw StateError('No available connection mode: ${result.error}');
|
||||||
@ -337,9 +338,6 @@ class RuntimeCoordinator extends ChangeNotifier {
|
|||||||
List<GatewayMode> getAvailableModes() {
|
List<GatewayMode> getAvailableModes() {
|
||||||
final modes = <GatewayMode>[];
|
final modes = <GatewayMode>[];
|
||||||
|
|
||||||
// Always can try local mode
|
|
||||||
modes.add(GatewayMode.local);
|
|
||||||
|
|
||||||
// Remote mode requires network
|
// Remote mode requires network
|
||||||
modes.add(GatewayMode.remote);
|
modes.add(GatewayMode.remote);
|
||||||
|
|
||||||
@ -370,8 +368,6 @@ class RuntimeCoordinator extends ChangeNotifier {
|
|||||||
|
|
||||||
Future<ModeSwitchResult> _switchMode(GatewayMode mode) {
|
Future<ModeSwitchResult> _switchMode(GatewayMode mode) {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case GatewayMode.local:
|
|
||||||
return modeSwitcher.switchToLocal();
|
|
||||||
case GatewayMode.remote:
|
case GatewayMode.remote:
|
||||||
return modeSwitcher.switchToRemote();
|
return modeSwitcher.switchToRemote();
|
||||||
case GatewayMode.offline:
|
case GatewayMode.offline:
|
||||||
|
|||||||
@ -671,6 +671,7 @@ class AccountSyncResult {
|
|||||||
final String message;
|
final String message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const String kManagedBridgeServerUrl = 'https://xworkmate-bridge.svc.plus';
|
||||||
const String kAccountManagedSecretTargetBridgeAuthToken = 'bridge.auth_token';
|
const String kAccountManagedSecretTargetBridgeAuthToken = 'bridge.auth_token';
|
||||||
const String kAccountManagedSecretTargetAIGatewayAccessToken =
|
const String kAccountManagedSecretTargetAIGatewayAccessToken =
|
||||||
'ai_gateway.access_token';
|
'ai_gateway.access_token';
|
||||||
|
|||||||
@ -151,28 +151,25 @@ List<GatewayConnectionProfile> normalizeGatewayProfiles({
|
|||||||
final fallback = defaults[index];
|
final fallback = defaults[index];
|
||||||
final current = index < incoming.length ? incoming[index] : fallback;
|
final current = index < incoming.length ? incoming[index] : fallback;
|
||||||
if (index == kGatewayRemoteProfileIndex) {
|
if (index == kGatewayRemoteProfileIndex) {
|
||||||
final hasEndpoint = current.host.trim().isNotEmpty && current.port > 0;
|
final hasEndpoint =
|
||||||
|
current.host.trim().isNotEmpty &&
|
||||||
|
current.port > 0 &&
|
||||||
|
!_isGatewayLoopbackHost(current.host);
|
||||||
final slotMode = switch (current.mode) {
|
final slotMode = switch (current.mode) {
|
||||||
RuntimeConnectionMode.local => RuntimeConnectionMode.local,
|
|
||||||
RuntimeConnectionMode.remote => RuntimeConnectionMode.remote,
|
RuntimeConnectionMode.remote => RuntimeConnectionMode.remote,
|
||||||
RuntimeConnectionMode.unconfigured => hasEndpoint
|
RuntimeConnectionMode.unconfigured =>
|
||||||
? RuntimeConnectionMode.remote
|
hasEndpoint
|
||||||
: RuntimeConnectionMode.unconfigured,
|
? RuntimeConnectionMode.remote
|
||||||
|
: RuntimeConnectionMode.unconfigured,
|
||||||
};
|
};
|
||||||
normalized.add(
|
normalized.add(
|
||||||
current.copyWith(
|
current.copyWith(
|
||||||
mode: slotMode,
|
mode: slotMode,
|
||||||
useSetupCode: slotMode == RuntimeConnectionMode.local
|
useSetupCode: current.useSetupCode,
|
||||||
? false
|
setupCode: current.setupCode,
|
||||||
: current.useSetupCode,
|
|
||||||
setupCode: slotMode == RuntimeConnectionMode.local
|
|
||||||
? ''
|
|
||||||
: current.setupCode,
|
|
||||||
host: hasEndpoint ? current.host : fallback.host,
|
host: hasEndpoint ? current.host : fallback.host,
|
||||||
port: current.port > 0 ? current.port : fallback.port,
|
port: current.port > 0 ? current.port : fallback.port,
|
||||||
tls: slotMode == RuntimeConnectionMode.local
|
tls: hasEndpoint ? current.tls : fallback.tls,
|
||||||
? false
|
|
||||||
: (hasEndpoint ? current.tls : fallback.tls),
|
|
||||||
tokenRef: current.tokenRef.trim().isEmpty
|
tokenRef: current.tokenRef.trim().isEmpty
|
||||||
? fallback.tokenRef
|
? fallback.tokenRef
|
||||||
: current.tokenRef,
|
: current.tokenRef,
|
||||||
@ -184,28 +181,19 @@ List<GatewayConnectionProfile> normalizeGatewayProfiles({
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
final slotMode = switch (current.mode) {
|
final slotMode = switch (current.mode) {
|
||||||
RuntimeConnectionMode.local => RuntimeConnectionMode.local,
|
|
||||||
RuntimeConnectionMode.remote => RuntimeConnectionMode.remote,
|
RuntimeConnectionMode.remote => RuntimeConnectionMode.remote,
|
||||||
RuntimeConnectionMode.unconfigured =>
|
RuntimeConnectionMode.unconfigured =>
|
||||||
current.host.trim().isNotEmpty
|
current.host.trim().isNotEmpty && !_isGatewayLoopbackHost(current.host)
|
||||||
? RuntimeConnectionMode.remote
|
? RuntimeConnectionMode.remote
|
||||||
: RuntimeConnectionMode.unconfigured,
|
: RuntimeConnectionMode.unconfigured,
|
||||||
};
|
};
|
||||||
normalized.add(
|
normalized.add(
|
||||||
current.copyWith(
|
current.copyWith(
|
||||||
mode: slotMode,
|
mode: slotMode,
|
||||||
useSetupCode: slotMode == RuntimeConnectionMode.local
|
useSetupCode: current.useSetupCode,
|
||||||
? false
|
setupCode: current.setupCode,
|
||||||
: current.useSetupCode,
|
port: current.port > 0 ? current.port : 443,
|
||||||
setupCode: slotMode == RuntimeConnectionMode.local
|
tls: current.tls,
|
||||||
? ''
|
|
||||||
: current.setupCode,
|
|
||||||
port: current.port > 0
|
|
||||||
? current.port
|
|
||||||
: slotMode == RuntimeConnectionMode.local
|
|
||||||
? 18789
|
|
||||||
: 443,
|
|
||||||
tls: slotMode == RuntimeConnectionMode.local ? false : current.tls,
|
|
||||||
tokenRef: current.tokenRef.trim().isEmpty
|
tokenRef: current.tokenRef.trim().isEmpty
|
||||||
? fallback.tokenRef
|
? fallback.tokenRef
|
||||||
: current.tokenRef,
|
: current.tokenRef,
|
||||||
@ -218,6 +206,11 @@ List<GatewayConnectionProfile> normalizeGatewayProfiles({
|
|||||||
return List<GatewayConnectionProfile>.unmodifiable(normalized);
|
return List<GatewayConnectionProfile>.unmodifiable(normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _isGatewayLoopbackHost(String host) {
|
||||||
|
final normalized = host.trim().toLowerCase();
|
||||||
|
return normalized == '127.0.0.1' || normalized == 'localhost';
|
||||||
|
}
|
||||||
|
|
||||||
List<GatewayConnectionProfile> replaceGatewayProfileAt(
|
List<GatewayConnectionProfile> replaceGatewayProfileAt(
|
||||||
List<GatewayConnectionProfile> profiles,
|
List<GatewayConnectionProfile> profiles,
|
||||||
int index,
|
int index,
|
||||||
|
|||||||
@ -10,12 +10,11 @@ import 'runtime_models_runtime_payloads.dart';
|
|||||||
import 'runtime_models_gateway_entities.dart';
|
import 'runtime_models_gateway_entities.dart';
|
||||||
import 'runtime_models_multi_agent.dart';
|
import 'runtime_models_multi_agent.dart';
|
||||||
|
|
||||||
enum RuntimeConnectionMode { unconfigured, local, remote }
|
enum RuntimeConnectionMode { unconfigured, remote }
|
||||||
|
|
||||||
extension RuntimeConnectionModeCopy on RuntimeConnectionMode {
|
extension RuntimeConnectionModeCopy on RuntimeConnectionMode {
|
||||||
String get label => switch (this) {
|
String get label => switch (this) {
|
||||||
RuntimeConnectionMode.unconfigured => appText('未配置', 'Unconfigured'),
|
RuntimeConnectionMode.unconfigured => appText('未配置', 'Unconfigured'),
|
||||||
RuntimeConnectionMode.local => appText('本地', 'Local'),
|
|
||||||
RuntimeConnectionMode.remote => appText('远程', 'Remote'),
|
RuntimeConnectionMode.remote => appText('远程', 'Remote'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -41,7 +41,6 @@ class SettingsSnapshot {
|
|||||||
required this.accountUsername,
|
required this.accountUsername,
|
||||||
required this.accountWorkspace,
|
required this.accountWorkspace,
|
||||||
required this.accountWorkspaceFollowed,
|
required this.accountWorkspaceFollowed,
|
||||||
required this.accountLocalMode,
|
|
||||||
required this.acpBridgeServerModeConfig,
|
required this.acpBridgeServerModeConfig,
|
||||||
required this.linuxDesktop,
|
required this.linuxDesktop,
|
||||||
required this.assistantExecutionTarget,
|
required this.assistantExecutionTarget,
|
||||||
@ -74,7 +73,6 @@ class SettingsSnapshot {
|
|||||||
final String accountUsername;
|
final String accountUsername;
|
||||||
final String accountWorkspace;
|
final String accountWorkspace;
|
||||||
final bool accountWorkspaceFollowed;
|
final bool accountWorkspaceFollowed;
|
||||||
final bool accountLocalMode;
|
|
||||||
final AcpBridgeServerModeConfig acpBridgeServerModeConfig;
|
final AcpBridgeServerModeConfig acpBridgeServerModeConfig;
|
||||||
final LinuxDesktopConfig linuxDesktop;
|
final LinuxDesktopConfig linuxDesktop;
|
||||||
final AssistantExecutionTarget assistantExecutionTarget;
|
final AssistantExecutionTarget assistantExecutionTarget;
|
||||||
@ -108,7 +106,6 @@ class SettingsSnapshot {
|
|||||||
accountUsername: '',
|
accountUsername: '',
|
||||||
accountWorkspace: 'Default Workspace',
|
accountWorkspace: 'Default Workspace',
|
||||||
accountWorkspaceFollowed: false,
|
accountWorkspaceFollowed: false,
|
||||||
accountLocalMode: true,
|
|
||||||
acpBridgeServerModeConfig: AcpBridgeServerModeConfig.defaults(),
|
acpBridgeServerModeConfig: AcpBridgeServerModeConfig.defaults(),
|
||||||
linuxDesktop: LinuxDesktopConfig.defaults(),
|
linuxDesktop: LinuxDesktopConfig.defaults(),
|
||||||
assistantExecutionTarget: AssistantExecutionTarget.agent,
|
assistantExecutionTarget: AssistantExecutionTarget.agent,
|
||||||
@ -143,7 +140,6 @@ class SettingsSnapshot {
|
|||||||
String? accountUsername,
|
String? accountUsername,
|
||||||
String? accountWorkspace,
|
String? accountWorkspace,
|
||||||
bool? accountWorkspaceFollowed,
|
bool? accountWorkspaceFollowed,
|
||||||
bool? accountLocalMode,
|
|
||||||
AcpBridgeServerModeConfig? acpBridgeServerModeConfig,
|
AcpBridgeServerModeConfig? acpBridgeServerModeConfig,
|
||||||
LinuxDesktopConfig? linuxDesktop,
|
LinuxDesktopConfig? linuxDesktop,
|
||||||
AssistantExecutionTarget? assistantExecutionTarget,
|
AssistantExecutionTarget? assistantExecutionTarget,
|
||||||
@ -187,7 +183,6 @@ class SettingsSnapshot {
|
|||||||
accountWorkspace: accountWorkspace ?? this.accountWorkspace,
|
accountWorkspace: accountWorkspace ?? this.accountWorkspace,
|
||||||
accountWorkspaceFollowed:
|
accountWorkspaceFollowed:
|
||||||
accountWorkspaceFollowed ?? this.accountWorkspaceFollowed,
|
accountWorkspaceFollowed ?? this.accountWorkspaceFollowed,
|
||||||
accountLocalMode: accountLocalMode ?? this.accountLocalMode,
|
|
||||||
acpBridgeServerModeConfig:
|
acpBridgeServerModeConfig:
|
||||||
acpBridgeServerModeConfig ?? this.acpBridgeServerModeConfig,
|
acpBridgeServerModeConfig ?? this.acpBridgeServerModeConfig,
|
||||||
linuxDesktop: linuxDesktop ?? this.linuxDesktop,
|
linuxDesktop: linuxDesktop ?? this.linuxDesktop,
|
||||||
@ -230,7 +225,6 @@ class SettingsSnapshot {
|
|||||||
'accountUsername': accountUsername,
|
'accountUsername': accountUsername,
|
||||||
'accountWorkspace': accountWorkspace,
|
'accountWorkspace': accountWorkspace,
|
||||||
'accountWorkspaceFollowed': accountWorkspaceFollowed,
|
'accountWorkspaceFollowed': accountWorkspaceFollowed,
|
||||||
'accountLocalMode': accountLocalMode,
|
|
||||||
'acpBridgeServerModeConfig': acpBridgeServerModeConfig.toJson(),
|
'acpBridgeServerModeConfig': acpBridgeServerModeConfig.toJson(),
|
||||||
'linuxDesktop': linuxDesktop.toJson(),
|
'linuxDesktop': linuxDesktop.toJson(),
|
||||||
'assistantExecutionTarget': assistantExecutionTarget.name,
|
'assistantExecutionTarget': assistantExecutionTarget.name,
|
||||||
@ -323,7 +317,6 @@ class SettingsSnapshot {
|
|||||||
SettingsSnapshot.defaults().accountWorkspace,
|
SettingsSnapshot.defaults().accountWorkspace,
|
||||||
accountWorkspaceFollowed:
|
accountWorkspaceFollowed:
|
||||||
json['accountWorkspaceFollowed'] as bool? ?? false,
|
json['accountWorkspaceFollowed'] as bool? ?? false,
|
||||||
accountLocalMode: json['accountLocalMode'] as bool? ?? true,
|
|
||||||
acpBridgeServerModeConfig: AcpBridgeServerModeConfig.fromJson(
|
acpBridgeServerModeConfig: AcpBridgeServerModeConfig.fromJson(
|
||||||
(json['acpBridgeServerModeConfig'] as Map?)?.cast<String, dynamic>() ??
|
(json['acpBridgeServerModeConfig'] as Map?)?.cast<String, dynamic>() ??
|
||||||
const {},
|
const {},
|
||||||
|
|||||||
@ -35,7 +35,6 @@ void main() {
|
|||||||
onVerifyMfa: () async {},
|
onVerifyMfa: () async {},
|
||||||
onCancelMfa: () async {},
|
onCancelMfa: () async {},
|
||||||
onSync: () async {},
|
onSync: () async {},
|
||||||
onDisconnect: () async {},
|
|
||||||
onLogout: () async {},
|
onLogout: () async {},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -51,7 +50,7 @@ void main() {
|
|||||||
findsNothing,
|
findsNothing,
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
find.byKey(const ValueKey('settings-account-disconnect-button')),
|
find.byKey(const ValueKey('settings-account-logout-button')),
|
||||||
findsNothing,
|
findsNothing,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -63,17 +62,16 @@ void main() {
|
|||||||
expect(loginCount, 1);
|
expect(loginCount, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('shows sync and disconnect actions for managed account state', (
|
testWidgets('shows sync and logout actions on the same row', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
final controllers = _TestControllers();
|
final controllers = _TestControllers();
|
||||||
addTearDown(controllers.dispose);
|
addTearDown(controllers.dispose);
|
||||||
|
|
||||||
var syncCount = 0;
|
var syncCount = 0;
|
||||||
var disconnectCount = 0;
|
var logoutCount = 0;
|
||||||
|
|
||||||
final settings = SettingsSnapshot.defaults().copyWith(
|
final settings = SettingsSnapshot.defaults().copyWith(
|
||||||
accountLocalMode: false,
|
|
||||||
accountBaseUrl: 'https://accounts.svc.plus',
|
accountBaseUrl: 'https://accounts.svc.plus',
|
||||||
accountUsername: 'review@svc.plus',
|
accountUsername: 'review@svc.plus',
|
||||||
acpBridgeServerModeConfig: AcpBridgeServerModeConfig.defaults()
|
acpBridgeServerModeConfig: AcpBridgeServerModeConfig.defaults()
|
||||||
@ -131,31 +129,45 @@ void main() {
|
|||||||
onSync: () async {
|
onSync: () async {
|
||||||
syncCount += 1;
|
syncCount += 1;
|
||||||
},
|
},
|
||||||
onDisconnect: () async {
|
onLogout: () async {
|
||||||
disconnectCount += 1;
|
logoutCount += 1;
|
||||||
},
|
},
|
||||||
onLogout: () async {},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(find.text('账号登录与同步'), findsOneWidget);
|
expect(find.text('账号登录与同步'), findsOneWidget);
|
||||||
expect(find.textContaining('svc.plus 托管配置'), findsOneWidget);
|
expect(find.textContaining('svc.plus 托管配置'), findsOneWidget);
|
||||||
|
expect(
|
||||||
|
find.byKey(const ValueKey('settings-account-disconnect-button')),
|
||||||
|
findsNothing,
|
||||||
|
);
|
||||||
|
expect(find.textContaining('本地配置'), findsNothing);
|
||||||
|
expect(find.textContaining('已断开'), findsNothing);
|
||||||
|
expect(find.textContaining('当前使用本地连接配置'), findsNothing);
|
||||||
|
|
||||||
|
final syncTop = tester.getTopLeft(
|
||||||
|
find.byKey(const ValueKey('settings-account-sync-button')),
|
||||||
|
);
|
||||||
|
final logoutTop = tester.getTopLeft(
|
||||||
|
find.byKey(const ValueKey('settings-account-logout-button')),
|
||||||
|
);
|
||||||
|
expect(syncTop.dy, logoutTop.dy);
|
||||||
|
|
||||||
await tester.tap(
|
await tester.tap(
|
||||||
find.byKey(const ValueKey('settings-account-sync-button')),
|
find.byKey(const ValueKey('settings-account-sync-button')),
|
||||||
);
|
);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.tap(
|
await tester.tap(
|
||||||
find.byKey(const ValueKey('settings-account-disconnect-button')),
|
find.byKey(const ValueKey('settings-account-logout-button')),
|
||||||
);
|
);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(syncCount, 1);
|
expect(syncCount, 1);
|
||||||
expect(disconnectCount, 1);
|
expect(logoutCount, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('disables disconnect when account already uses local config', (
|
testWidgets('keeps managed connection copy when account is signed in', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
final controllers = _TestControllers();
|
final controllers = _TestControllers();
|
||||||
@ -190,20 +202,19 @@ void main() {
|
|||||||
onVerifyMfa: () async {},
|
onVerifyMfa: () async {},
|
||||||
onCancelMfa: () async {},
|
onCancelMfa: () async {},
|
||||||
onSync: () async {},
|
onSync: () async {},
|
||||||
onDisconnect: () async {},
|
|
||||||
onLogout: () async {},
|
onLogout: () async {},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(find.textContaining('本地配置'), findsOneWidget);
|
expect(find.textContaining('svc.plus 托管配置'), findsOneWidget);
|
||||||
expect(find.textContaining('已断开'), findsOneWidget);
|
expect(find.textContaining('本地配置'), findsNothing);
|
||||||
expect(find.textContaining('当前使用本地连接配置'), findsOneWidget);
|
expect(find.textContaining('已断开'), findsNothing);
|
||||||
|
expect(find.textContaining('当前使用本地连接配置'), findsNothing);
|
||||||
final disconnectButton = tester.widget<FilledButton>(
|
expect(
|
||||||
find.byKey(const ValueKey('settings-account-disconnect-button')),
|
find.byKey(const ValueKey('settings-account-disconnect-button')),
|
||||||
|
findsNothing,
|
||||||
);
|
);
|
||||||
expect(disconnectButton.onPressed, isNull);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,7 +31,6 @@ void main() {
|
|||||||
onVerifyMfa: () async {},
|
onVerifyMfa: () async {},
|
||||||
onCancelMfa: () async {},
|
onCancelMfa: () async {},
|
||||||
onSync: () async {},
|
onSync: () async {},
|
||||||
onDisconnect: () async {},
|
|
||||||
onLogout: () async {},
|
onLogout: () async {},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -49,7 +48,6 @@ void main() {
|
|||||||
addTearDown(controllers.dispose);
|
addTearDown(controllers.dispose);
|
||||||
|
|
||||||
final settings = SettingsSnapshot.defaults().copyWith(
|
final settings = SettingsSnapshot.defaults().copyWith(
|
||||||
accountLocalMode: false,
|
|
||||||
accountBaseUrl: 'https://accounts.svc.plus',
|
accountBaseUrl: 'https://accounts.svc.plus',
|
||||||
accountUsername: 'review@svc.plus',
|
accountUsername: 'review@svc.plus',
|
||||||
acpBridgeServerModeConfig: AcpBridgeServerModeConfig.defaults()
|
acpBridgeServerModeConfig: AcpBridgeServerModeConfig.defaults()
|
||||||
@ -105,7 +103,6 @@ void main() {
|
|||||||
onVerifyMfa: () async {},
|
onVerifyMfa: () async {},
|
||||||
onCancelMfa: () async {},
|
onCancelMfa: () async {},
|
||||||
onSync: () async {},
|
onSync: () async {},
|
||||||
onDisconnect: () async {},
|
|
||||||
onLogout: () async {},
|
onLogout: () async {},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
43
test/runtime/bridge_runtime_cleanup_test.dart
Normal file
43
test/runtime/bridge_runtime_cleanup_test.dart
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:xworkmate/app/app_controller.dart';
|
||||||
|
import 'package:xworkmate/runtime/mode_switcher.dart';
|
||||||
|
import 'package:xworkmate/runtime/runtime_models.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Bridge runtime cleanup', () {
|
||||||
|
test('resolves the managed bridge endpoint without BRIDGE_SERVER_URL', () {
|
||||||
|
final controller = AppController(
|
||||||
|
environmentOverride: const <String, String>{
|
||||||
|
'BRIDGE_SERVER_URL': 'https://stale.example.invalid',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
addTearDown(controller.dispose);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
controller.resolveBridgeAcpEndpointInternal()?.toString(),
|
||||||
|
kManagedBridgeServerUrl,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
controller
|
||||||
|
.resolveExternalAcpEndpointForTargetInternal(
|
||||||
|
AssistantExecutionTarget.gateway,
|
||||||
|
)
|
||||||
|
?.toString(),
|
||||||
|
kManagedBridgeServerUrl,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'runtime coordinator only exposes remote and offline gateway modes',
|
||||||
|
() {
|
||||||
|
final controller = AppController();
|
||||||
|
addTearDown(controller.dispose);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
controller.runtimeCoordinatorInternal.getAvailableModes(),
|
||||||
|
const <GatewayMode>[GatewayMode.remote, GatewayMode.offline],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -58,85 +58,59 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
test(
|
test('syncAccountSettings pins the managed bridge cloud entry', () async {
|
||||||
'disconnectManagedAccountBase switches the snapshot to local mode',
|
final storeRoot = await Directory.systemTemp.createTemp(
|
||||||
() async {
|
'xworkmate-account-managed-bridge-',
|
||||||
final storeRoot = await Directory.systemTemp.createTemp(
|
);
|
||||||
'xworkmate-account-disconnect-',
|
addTearDown(() async {
|
||||||
);
|
if (await storeRoot.exists()) {
|
||||||
addTearDown(() async {
|
await storeRoot.delete(recursive: true);
|
||||||
if (await storeRoot.exists()) {
|
}
|
||||||
await storeRoot.delete(recursive: true);
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
final store = SecureConfigStore(
|
final store = SecureConfigStore(
|
||||||
secretRootPathResolver: () async => '${storeRoot.path}/secrets',
|
secretRootPathResolver: () async => '${storeRoot.path}/secrets',
|
||||||
appDataRootPathResolver: () async => '${storeRoot.path}/app-data',
|
appDataRootPathResolver: () async => '${storeRoot.path}/app-data',
|
||||||
supportRootPathResolver: () async => '${storeRoot.path}/support',
|
supportRootPathResolver: () async => '${storeRoot.path}/support',
|
||||||
enableSecureStorage: false,
|
enableSecureStorage: false,
|
||||||
);
|
);
|
||||||
await store.initialize();
|
await store.initialize();
|
||||||
await store.saveSettingsSnapshot(
|
await store.saveSettingsSnapshot(
|
||||||
SettingsSnapshot.defaults().copyWith(
|
SettingsSnapshot.defaults().copyWith(
|
||||||
accountLocalMode: false,
|
accountBaseUrl: 'https://accounts.svc.plus',
|
||||||
accountBaseUrl: 'https://accounts.svc.plus',
|
accountUsername: 'review@svc.plus',
|
||||||
accountUsername: 'review@svc.plus',
|
),
|
||||||
acpBridgeServerModeConfig: AcpBridgeServerModeConfig.defaults()
|
);
|
||||||
.copyWith(
|
await store.saveAccountSessionToken('session-token');
|
||||||
cloudSynced: AcpBridgeServerModeConfig.defaults().cloudSynced
|
await store.saveAccountManagedSecret(
|
||||||
.copyWith(
|
target: kAccountManagedSecretTargetBridgeAuthToken,
|
||||||
accountIdentifier: 'review@svc.plus',
|
value: 'bridge-token',
|
||||||
remoteServerSummary:
|
);
|
||||||
AcpBridgeServerModeConfig.defaults()
|
|
||||||
.cloudSynced
|
|
||||||
.remoteServerSummary
|
|
||||||
.copyWith(endpoint: 'https://bridge.svc.plus'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await store.saveAccountSyncState(
|
|
||||||
AccountSyncState.defaults().copyWith(
|
|
||||||
syncState: 'ready',
|
|
||||||
syncMessage: 'Bridge access synced',
|
|
||||||
profileScope: 'bridge',
|
|
||||||
lastSyncAtMs: DateTime(2026, 4, 12, 10).millisecondsSinceEpoch,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final controller = SettingsController(store);
|
final controller = SettingsController(store);
|
||||||
addTearDown(controller.dispose);
|
addTearDown(controller.dispose);
|
||||||
await controller.initialize();
|
await controller.initialize();
|
||||||
|
|
||||||
await controller.disconnectManagedAccountBase();
|
final result = await controller.syncAccountSettings(
|
||||||
|
baseUrl: 'https://accounts.svc.plus',
|
||||||
|
);
|
||||||
|
|
||||||
expect(controller.snapshot.accountLocalMode, isTrue);
|
expect(result.state, 'ready');
|
||||||
expect(
|
expect(controller.accountSyncState, isNotNull);
|
||||||
controller
|
expect(
|
||||||
.snapshot
|
controller.accountSyncState!.syncedDefaults.bridgeServerUrl,
|
||||||
.acpBridgeServerModeConfig
|
kManagedBridgeServerUrl,
|
||||||
.cloudSynced
|
);
|
||||||
.accountBaseUrl,
|
expect(
|
||||||
isEmpty,
|
controller
|
||||||
);
|
.snapshot
|
||||||
expect(
|
.acpBridgeServerModeConfig
|
||||||
controller
|
.cloudSynced
|
||||||
.snapshot
|
.remoteServerSummary
|
||||||
.acpBridgeServerModeConfig
|
.endpoint,
|
||||||
.cloudSynced
|
kManagedBridgeServerUrl,
|
||||||
.accountIdentifier,
|
);
|
||||||
isEmpty,
|
});
|
||||||
);
|
|
||||||
expect(controller.accountSyncState, isNotNull);
|
|
||||||
expect(controller.accountSyncState!.syncState, 'disconnected');
|
|
||||||
expect(
|
|
||||||
controller.accountSyncState!.syncMessage,
|
|
||||||
'Using local connection settings',
|
|
||||||
);
|
|
||||||
expect(controller.accountSyncState!.profileScope, 'bridge');
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'recovers bridge sync state from cloud-synced snapshot when support state is missing',
|
'recovers bridge sync state from cloud-synced snapshot when support state is missing',
|
||||||
@ -159,7 +133,6 @@ void main() {
|
|||||||
await store.initialize();
|
await store.initialize();
|
||||||
await store.saveSettingsSnapshot(
|
await store.saveSettingsSnapshot(
|
||||||
SettingsSnapshot.defaults().copyWith(
|
SettingsSnapshot.defaults().copyWith(
|
||||||
accountLocalMode: false,
|
|
||||||
acpBridgeServerModeConfig: AcpBridgeServerModeConfig.defaults()
|
acpBridgeServerModeConfig: AcpBridgeServerModeConfig.defaults()
|
||||||
.copyWith(
|
.copyWith(
|
||||||
cloudSynced: AcpBridgeServerModeConfig.defaults().cloudSynced
|
cloudSynced: AcpBridgeServerModeConfig.defaults().cloudSynced
|
||||||
@ -204,50 +177,5 @@ void main() {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
test(
|
|
||||||
'does not recover bridge sync state from cloud-synced snapshot in local mode',
|
|
||||||
() async {
|
|
||||||
final storeRoot = await Directory.systemTemp.createTemp(
|
|
||||||
'xworkmate-account-local-mode-',
|
|
||||||
);
|
|
||||||
addTearDown(() async {
|
|
||||||
if (await storeRoot.exists()) {
|
|
||||||
await storeRoot.delete(recursive: true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
final store = SecureConfigStore(
|
|
||||||
secretRootPathResolver: () async => '${storeRoot.path}/secrets',
|
|
||||||
appDataRootPathResolver: () async => '${storeRoot.path}/app-data',
|
|
||||||
supportRootPathResolver: () async => '${storeRoot.path}/support',
|
|
||||||
enableSecureStorage: false,
|
|
||||||
);
|
|
||||||
await store.initialize();
|
|
||||||
await store.saveSettingsSnapshot(
|
|
||||||
SettingsSnapshot.defaults().copyWith(
|
|
||||||
accountLocalMode: true,
|
|
||||||
acpBridgeServerModeConfig: AcpBridgeServerModeConfig.defaults()
|
|
||||||
.copyWith(
|
|
||||||
cloudSynced: AcpBridgeServerModeConfig.defaults().cloudSynced
|
|
||||||
.copyWith(
|
|
||||||
remoteServerSummary:
|
|
||||||
AcpBridgeServerModeConfig.defaults()
|
|
||||||
.cloudSynced
|
|
||||||
.remoteServerSummary
|
|
||||||
.copyWith(endpoint: 'https://bridge.svc.plus'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final controller = SettingsController(store);
|
|
||||||
addTearDown(controller.dispose);
|
|
||||||
await controller.initialize();
|
|
||||||
|
|
||||||
expect(controller.accountSyncState, isNull);
|
|
||||||
expect(await store.loadAccountSyncState(), isNull);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user