From 70b6856adb1dc7f78dd5e68415a61815dcb0b983 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Thu, 12 Mar 2026 01:26:04 +0800 Subject: [PATCH] fix: improve remote gateway bootstrap prefill --- lib/app/app_controller.dart | 5 +- lib/features/settings/settings_page.dart | 2 +- lib/runtime/runtime_bootstrap.dart | 83 +++++++++++++++++++----- lib/widgets/gateway_connect_dialog.dart | 5 +- test/runtime/runtime_bootstrap_test.dart | 40 ++++++++++++ 5 files changed, 117 insertions(+), 18 deletions(-) diff --git a/lib/app/app_controller.dart b/lib/app/app_controller.dart index 1fab2dfd..fd283aa8 100644 --- a/lib/app/app_controller.dart +++ b/lib/app/app_controller.dart @@ -507,7 +507,10 @@ class AppController extends ChangeNotifier { Future _initialize() async { try { await _settingsController.initialize(); - final bootstrap = await RuntimeBootstrapConfig.load(); + final bootstrap = await RuntimeBootstrapConfig.load( + workspacePathHint: settings.workspacePath, + cliPathHint: settings.cliPath, + ); final seeded = bootstrap.mergeIntoSettings(settings); if (seeded.toJsonString() != settings.toJsonString()) { await _settingsController.saveSnapshot(seeded); diff --git a/lib/features/settings/settings_page.dart b/lib/features/settings/settings_page.dart index 1d9c8e87..a7813e7a 100644 --- a/lib/features/settings/settings_page.dart +++ b/lib/features/settings/settings_page.dart @@ -409,7 +409,7 @@ class _SettingsPageState extends State { ), const SizedBox(height: 16), Text( - '${controller.connection.status.label} · ${controller.connection.remoteAddress ?? settings.gateway.host}:${settings.gateway.port}', + '${controller.connection.status.label} · ${controller.connection.remoteAddress ?? '${settings.gateway.host}:${settings.gateway.port}'}', style: Theme.of(context).textTheme.bodyLarge, ), const SizedBox(height: 16), diff --git a/lib/runtime/runtime_bootstrap.dart b/lib/runtime/runtime_bootstrap.dart index 33318a46..d08fb439 100644 --- a/lib/runtime/runtime_bootstrap.dart +++ b/lib/runtime/runtime_bootstrap.dart @@ -17,10 +17,21 @@ class RuntimeBootstrapConfig { final GatewayBootstrapTarget? localGateway; final GatewayBootstrapTarget? remoteGateway; - static Future load() async { - final env = await _loadEnvFile(); - final workspaceRoot = _resolveWorkspaceRoot(); - final openClawRoot = _resolveOpenClawRoot(workspaceRoot); + static Future load({ + String? workspacePathHint, + String? cliPathHint, + }) async { + final workspaceRoot = _resolveWorkspaceRoot(workspacePathHint); + final openClawRoot = _resolveOpenClawRoot( + workspaceRoot, + cliPathHint: cliPathHint, + ); + final env = await _loadEnvFile( + workspacePathHint: workspacePathHint, + cliPathHint: cliPathHint, + workspaceRoot: workspaceRoot, + openClawRoot: openClawRoot, + ); return RuntimeBootstrapConfig( workspacePath: workspaceRoot?.path, remoteProjectRoot: workspaceRoot?.path, @@ -120,13 +131,27 @@ class GatewayBootstrapTarget { } } -Future> _loadEnvFile() async { - final candidates = { - File('${Directory.current.path}/.env'), - ..._ancestorDirectories( - Directory.current, - ).map((directory) => File('${directory.path}/.env')), - }.toList(growable: false); +Future> _loadEnvFile({ + String? workspacePathHint, + String? cliPathHint, + Directory? workspaceRoot, + Directory? openClawRoot, +}) async { + final candidateDirectories = { + Directory.current, + ..._ancestorDirectories(Directory.current), + ..._pathCandidates(workspacePathHint), + ..._pathCandidates( + cliPathHint == null ? null : File(cliPathHint).parent.path, + ), + ...?workspaceRoot == null ? null : [workspaceRoot], + ...?workspaceRoot == null ? null : _ancestorDirectories(workspaceRoot), + ...?openClawRoot == null ? null : [openClawRoot], + ...?openClawRoot == null ? null : _ancestorDirectories(openClawRoot), + }; + final candidates = candidateDirectories + .map((directory) => File('${directory.path}/.env')) + .toList(growable: false); for (final file in candidates) { if (!await file.exists()) { @@ -155,11 +180,12 @@ Future> _loadEnvFile() async { return const {}; } -Directory? _resolveWorkspaceRoot() { - final candidates = [ +Directory? _resolveWorkspaceRoot(String? workspacePathHint) { + final candidates = { + ..._pathCandidates(workspacePathHint), Directory.current, ..._ancestorDirectories(Directory.current), - ]; + }.toList(growable: false); for (final candidate in candidates) { if (File('${candidate.path}/pubspec.yaml').existsSync() && File('${candidate.path}/lib/main.dart').existsSync()) { @@ -169,7 +195,17 @@ Directory? _resolveWorkspaceRoot() { return null; } -Directory? _resolveOpenClawRoot(Directory? workspaceRoot) { +Directory? _resolveOpenClawRoot( + Directory? workspaceRoot, { + String? cliPathHint, +}) { + final cliFile = cliPathHint == null ? null : File(cliPathHint); + if (cliFile != null && cliFile.existsSync()) { + final cliParent = cliFile.parent; + if (File('${cliParent.path}/openclaw.mjs').existsSync()) { + return cliParent; + } + } if (workspaceRoot == null) { return null; } @@ -204,3 +240,20 @@ List _ancestorDirectories(Directory start) { } return ancestors; } + +List _pathCandidates(String? rawPath) { + final trimmed = rawPath?.trim() ?? ''; + if (trimmed.isEmpty) { + return const []; + } + final fileSystemEntityType = FileSystemEntity.typeSync(trimmed); + final directory = switch (fileSystemEntityType) { + FileSystemEntityType.directory => Directory(trimmed), + FileSystemEntityType.file => File(trimmed).parent, + _ => Directory(trimmed), + }; + if (!directory.existsSync()) { + return const []; + } + return [directory, ..._ancestorDirectories(directory)]; +} diff --git a/lib/widgets/gateway_connect_dialog.dart b/lib/widgets/gateway_connect_dialog.dart index 1fe28f35..424e4984 100644 --- a/lib/widgets/gateway_connect_dialog.dart +++ b/lib/widgets/gateway_connect_dialog.dart @@ -235,7 +235,10 @@ class _GatewayConnectDialogState extends State { } Future _loadBootstrapPrefill() async { - final bootstrap = await RuntimeBootstrapConfig.load(); + final bootstrap = await RuntimeBootstrapConfig.load( + workspacePathHint: widget.controller.settings.workspacePath, + cliPathHint: widget.controller.settings.cliPath, + ); final preferred = bootstrap.preferredGatewayFor(_connectionMode); if (!mounted || preferred == null) { return; diff --git a/test/runtime/runtime_bootstrap_test.dart b/test/runtime/runtime_bootstrap_test.dart index 05ff5d1f..b2556ace 100644 --- a/test/runtime/runtime_bootstrap_test.dart +++ b/test/runtime/runtime_bootstrap_test.dart @@ -48,4 +48,44 @@ remote-token: remote-test-token ); }, ); + + test( + 'RuntimeBootstrapConfig resolves .env from workspace path hints outside the repo cwd', + () async { + final tempDir = await Directory.systemTemp.createTemp( + 'xworkmate-bootstrap-hint-', + ); + final outsideDir = await Directory.systemTemp.createTemp( + 'xworkmate-bootstrap-outside-', + ); + addTearDown(() async { + Directory.current = outsideDir.parent; + await tempDir.delete(recursive: true); + await outsideDir.delete(recursive: true); + }); + + await File( + '${tempDir.path}/pubspec.yaml', + ).writeAsString('name: xworkmate_test\n'); + await Directory('${tempDir.path}/lib').create(recursive: true); + await File( + '${tempDir.path}/lib/main.dart', + ).writeAsString('void main() {}\n'); + await File('${tempDir.path}/.env').writeAsString(''' +remote: wss://openclaw.example.com:443 +remote-token: remote-test-token +'''); + + Directory.current = outsideDir; + + final config = await RuntimeBootstrapConfig.load( + workspacePathHint: tempDir.path, + ); + + expect(config.remoteGateway, isNotNull); + expect(config.remoteGateway!.host, 'openclaw.example.com'); + expect(config.remoteGateway!.token, 'remote-test-token'); + expect(config.workspacePath, tempDir.path); + }, + ); }