fix: improve remote gateway bootstrap prefill

This commit is contained in:
Haitao Pan 2026-03-12 01:26:04 +08:00
parent c9c52de212
commit 70b6856adb
5 changed files with 117 additions and 18 deletions

View File

@ -507,7 +507,10 @@ class AppController extends ChangeNotifier {
Future<void> _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);

View File

@ -409,7 +409,7 @@ class _SettingsPageState extends State<SettingsPage> {
),
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),

View File

@ -17,10 +17,21 @@ class RuntimeBootstrapConfig {
final GatewayBootstrapTarget? localGateway;
final GatewayBootstrapTarget? remoteGateway;
static Future<RuntimeBootstrapConfig> load() async {
final env = await _loadEnvFile();
final workspaceRoot = _resolveWorkspaceRoot();
final openClawRoot = _resolveOpenClawRoot(workspaceRoot);
static Future<RuntimeBootstrapConfig> 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<Map<String, String>> _loadEnvFile() async {
final candidates = <File>{
File('${Directory.current.path}/.env'),
..._ancestorDirectories(
Directory.current,
).map((directory) => File('${directory.path}/.env')),
}.toList(growable: false);
Future<Map<String, String>> _loadEnvFile({
String? workspacePathHint,
String? cliPathHint,
Directory? workspaceRoot,
Directory? openClawRoot,
}) async {
final candidateDirectories = <Directory>{
Directory.current,
..._ancestorDirectories(Directory.current),
..._pathCandidates(workspacePathHint),
..._pathCandidates(
cliPathHint == null ? null : File(cliPathHint).parent.path,
),
...?workspaceRoot == null ? null : <Directory>[workspaceRoot],
...?workspaceRoot == null ? null : _ancestorDirectories(workspaceRoot),
...?openClawRoot == null ? null : <Directory>[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<Map<String, String>> _loadEnvFile() async {
return const <String, String>{};
}
Directory? _resolveWorkspaceRoot() {
final candidates = <Directory>[
Directory? _resolveWorkspaceRoot(String? workspacePathHint) {
final candidates = <Directory>{
..._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<Directory> _ancestorDirectories(Directory start) {
}
return ancestors;
}
List<Directory> _pathCandidates(String? rawPath) {
final trimmed = rawPath?.trim() ?? '';
if (trimmed.isEmpty) {
return const <Directory>[];
}
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 <Directory>[];
}
return <Directory>[directory, ..._ancestorDirectories(directory)];
}

View File

@ -235,7 +235,10 @@ class _GatewayConnectDialogState extends State<GatewayConnectDialog> {
}
Future<void> _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;

View File

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