Make workspace advanced configs extensible
This commit is contained in:
parent
d33e1df4d2
commit
2e76d833e4
@ -23,10 +23,7 @@ class PlaybookRunner {
|
||||
required String installPath,
|
||||
required bool installMissingPrerequisites,
|
||||
required ServerInfo? serverInfo,
|
||||
String? deepseekApiKey,
|
||||
String? nvidiaApiKey,
|
||||
String? ollamaApiKey,
|
||||
String? openclawGatewayToken,
|
||||
List<WorkspaceExtraConfig>? extraConfigs,
|
||||
required void Function(String stepId, StepStatus status, String? message)
|
||||
onStepUpdate,
|
||||
required void Function(String logLine) onLog,
|
||||
@ -79,10 +76,7 @@ class PlaybookRunner {
|
||||
workspaceDomain: workspaceDomain,
|
||||
bridgeDomain: bridgeDomain,
|
||||
bridgeToken: bridgeToken,
|
||||
deepseekApiKey: deepseekApiKey,
|
||||
nvidiaApiKey: nvidiaApiKey,
|
||||
ollamaApiKey: ollamaApiKey,
|
||||
openclawGatewayToken: openclawGatewayToken,
|
||||
extraConfigs: extraConfigs,
|
||||
),
|
||||
onLog,
|
||||
);
|
||||
@ -183,25 +177,26 @@ class PlaybookRunner {
|
||||
required String workspaceDomain,
|
||||
required String bridgeDomain,
|
||||
required String bridgeToken,
|
||||
String? deepseekApiKey,
|
||||
String? nvidiaApiKey,
|
||||
String? ollamaApiKey,
|
||||
String? openclawGatewayToken,
|
||||
List<WorkspaceExtraConfig>? extraConfigs,
|
||||
}) {
|
||||
final domain = workspaceDomain.trim();
|
||||
final bridge = bridgeDomain.trim();
|
||||
final bridgeUrl = 'https://$bridge';
|
||||
final extraEnvVars = <String>[
|
||||
if ((deepseekApiKey ?? '').trim().isNotEmpty)
|
||||
'deepseek_api_key: ${shellQuote(deepseekApiKey!.trim())}',
|
||||
if ((nvidiaApiKey ?? '').trim().isNotEmpty)
|
||||
'nvidia_api_key: ${shellQuote(nvidiaApiKey!.trim())}',
|
||||
if ((ollamaApiKey ?? '').trim().isNotEmpty)
|
||||
'ollama_api_key: ${shellQuote(ollamaApiKey!.trim())}',
|
||||
if ((openclawGatewayToken ?? '').trim().isNotEmpty)
|
||||
'openclaw_gateway_token: ${shellQuote(openclawGatewayToken!.trim())}',
|
||||
];
|
||||
final extraEnvBlock = extraEnvVars.isEmpty ? '' : '${extraEnvVars.join('\n')}\n';
|
||||
final extraEnvVars = <String>[];
|
||||
for (final config in extraConfigs ?? const <WorkspaceExtraConfig>[]) {
|
||||
final key = config.key.trim();
|
||||
final value = config.value.trim();
|
||||
if (key.isEmpty || value.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
extraEnvVars.add('$key: ${shellQuote(value)}');
|
||||
final note = config.note.trim();
|
||||
if (note.isNotEmpty) {
|
||||
extraEnvVars.add('# ${note.length > 20 ? note.substring(0, 20) : note}');
|
||||
}
|
||||
}
|
||||
final extraEnvBlock =
|
||||
extraEnvVars.isEmpty ? '' : '${extraEnvVars.join('\n')}\n';
|
||||
return '''
|
||||
cat > ${shellQuote(inventoryPath)} <<'EOF'
|
||||
[all]
|
||||
|
||||
@ -31,10 +31,7 @@ class _WorkspaceManagementFormState extends State<WorkspaceManagementForm> {
|
||||
late final TextEditingController _portController;
|
||||
late final TextEditingController _sudoController;
|
||||
late final TextEditingController _installPathController;
|
||||
late final TextEditingController _deepseekKeyController;
|
||||
late final TextEditingController _nvidiaKeyController;
|
||||
late final TextEditingController _ollamaKeyController;
|
||||
late final TextEditingController _openclawTokenController;
|
||||
final List<_ExtraRowControllers> _extraRows = <_ExtraRowControllers>[];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -49,11 +46,15 @@ class _WorkspaceManagementFormState extends State<WorkspaceManagementForm> {
|
||||
_portController = TextEditingController(text: c.sshPort.toString());
|
||||
_sudoController = TextEditingController(text: c.sudoPassword ?? '');
|
||||
_installPathController = TextEditingController(text: c.installPath);
|
||||
_deepseekKeyController = TextEditingController(text: c.deepseekApiKey ?? '');
|
||||
_nvidiaKeyController = TextEditingController(text: c.nvidiaApiKey ?? '');
|
||||
_ollamaKeyController = TextEditingController(text: c.ollamaApiKey ?? '');
|
||||
_openclawTokenController =
|
||||
TextEditingController(text: c.openclawGatewayToken ?? '');
|
||||
for (final row in c.extraConfigs) {
|
||||
_extraRows.add(
|
||||
_ExtraRowControllers(
|
||||
keyController: TextEditingController(text: row.key),
|
||||
valueController: TextEditingController(text: row.value),
|
||||
noteController: TextEditingController(text: row.note),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -67,10 +68,9 @@ class _WorkspaceManagementFormState extends State<WorkspaceManagementForm> {
|
||||
_portController.dispose();
|
||||
_sudoController.dispose();
|
||||
_installPathController.dispose();
|
||||
_deepseekKeyController.dispose();
|
||||
_nvidiaKeyController.dispose();
|
||||
_ollamaKeyController.dispose();
|
||||
_openclawTokenController.dispose();
|
||||
for (final row in _extraRows) {
|
||||
row.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -87,10 +87,16 @@ class _WorkspaceManagementFormState extends State<WorkspaceManagementForm> {
|
||||
installPath: _installPathController.text.trim().isEmpty
|
||||
? '/opt/xworkspace/playbooks'
|
||||
: _installPathController.text.trim(),
|
||||
deepseekApiKey: _deepseekKeyController.text,
|
||||
nvidiaApiKey: _nvidiaKeyController.text,
|
||||
ollamaApiKey: _ollamaKeyController.text,
|
||||
openclawGatewayToken: _openclawTokenController.text,
|
||||
extraConfigs: _extraRows
|
||||
.map(
|
||||
(row) => WorkspaceExtraConfig(
|
||||
key: row.keyController.text.trim(),
|
||||
value: row.valueController.text,
|
||||
note: row.noteController.text.trim(),
|
||||
),
|
||||
)
|
||||
.where((row) => row.key.trim().isNotEmpty)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
@ -134,8 +140,8 @@ class _WorkspaceManagementFormState extends State<WorkspaceManagementForm> {
|
||||
padding: const EdgeInsets.only(left: 14, top: 2),
|
||||
child: Text(
|
||||
appText(
|
||||
'将检测桥接域名:${controller.bridgeDomain}',
|
||||
'Bridge domain will be checked: ${controller.bridgeDomain}',
|
||||
'将按当前输入检测桥接域名:${controller.bridgeDomain}',
|
||||
'Bridge domain will be checked from the current input: ${controller.bridgeDomain}',
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
@ -248,37 +254,26 @@ class _WorkspaceManagementFormState extends State<WorkspaceManagementForm> {
|
||||
label: appText('安装路径', 'Install path'),
|
||||
icon: Icons.storage_outlined,
|
||||
),
|
||||
_field(
|
||||
width: 320,
|
||||
controller: _deepseekKeyController,
|
||||
_ExtraConfigEditor(
|
||||
rows: _extraRows,
|
||||
enabled: !disabled,
|
||||
label: 'DEEPSEEK_API_KEY',
|
||||
icon: Icons.key_outlined,
|
||||
obscureText: true,
|
||||
),
|
||||
_field(
|
||||
width: 320,
|
||||
controller: _nvidiaKeyController,
|
||||
enabled: !disabled,
|
||||
label: 'NVIDIA_API_KEY',
|
||||
icon: Icons.key_outlined,
|
||||
obscureText: true,
|
||||
),
|
||||
_field(
|
||||
width: 320,
|
||||
controller: _ollamaKeyController,
|
||||
enabled: !disabled,
|
||||
label: 'OLLAMA_API_KEY',
|
||||
icon: Icons.key_outlined,
|
||||
obscureText: true,
|
||||
),
|
||||
_field(
|
||||
width: 320,
|
||||
controller: _openclawTokenController,
|
||||
enabled: !disabled,
|
||||
label: 'OPENCLAW_GATEWAY_TOKEN',
|
||||
icon: Icons.key_outlined,
|
||||
obscureText: true,
|
||||
onAdd: () {
|
||||
setState(() {
|
||||
_extraRows.add(
|
||||
_ExtraRowControllers(
|
||||
keyController: TextEditingController(),
|
||||
valueController: TextEditingController(),
|
||||
noteController: TextEditingController(),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
onRemove: (index) {
|
||||
setState(() {
|
||||
final row = _extraRows.removeAt(index);
|
||||
row.dispose();
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -359,3 +354,166 @@ class _WorkspaceManagementFormState extends State<WorkspaceManagementForm> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExtraConfigEditor extends StatelessWidget {
|
||||
const _ExtraConfigEditor({
|
||||
required this.rows,
|
||||
required this.enabled,
|
||||
required this.onAdd,
|
||||
required this.onRemove,
|
||||
});
|
||||
|
||||
final List<_ExtraRowControllers> rows;
|
||||
final bool enabled;
|
||||
final VoidCallback onAdd;
|
||||
final ValueChanged<int> onRemove;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
appText('额外配置', 'Extra configs'),
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton.icon(
|
||||
onPressed: enabled ? onAdd : null,
|
||||
icon: const Icon(Icons.add),
|
||||
label: Text(appText('添加行', 'Add row')),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...List.generate(rows.length, (index) {
|
||||
final row = rows[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: _ExtraConfigRow(
|
||||
index: index,
|
||||
row: row,
|
||||
enabled: enabled,
|
||||
onRemove: () => onRemove(index),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExtraConfigRow extends StatelessWidget {
|
||||
const _ExtraConfigRow({
|
||||
required this.index,
|
||||
required this.row,
|
||||
required this.enabled,
|
||||
required this.onRemove,
|
||||
});
|
||||
|
||||
final int index;
|
||||
final _ExtraRowControllers row;
|
||||
final bool enabled;
|
||||
final VoidCallback onRemove;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final columns = constraints.maxWidth > 820 ? 3 : 1;
|
||||
final itemWidth = columns == 3
|
||||
? (constraints.maxWidth - 24) / 3
|
||||
: constraints.maxWidth;
|
||||
return Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: itemWidth,
|
||||
child: TextField(
|
||||
controller: row.keyController,
|
||||
enabled: enabled,
|
||||
decoration: InputDecoration(
|
||||
labelText: appText('KEY', 'KEY'),
|
||||
prefixIcon: const Icon(Icons.key_outlined, size: 18),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: itemWidth,
|
||||
child: TextField(
|
||||
controller: row.valueController,
|
||||
enabled: enabled,
|
||||
obscureText: row.isSensitiveKey,
|
||||
decoration: InputDecoration(
|
||||
labelText: appText('VALUE', 'VALUE'),
|
||||
prefixIcon: const Icon(Icons.data_object_outlined, size: 18),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: itemWidth,
|
||||
child: TextField(
|
||||
controller: row.noteController,
|
||||
enabled: enabled,
|
||||
maxLength: 20,
|
||||
decoration: InputDecoration(
|
||||
labelText: appText('备注(20字内)', 'Note (<=20 chars)'),
|
||||
prefixIcon: const Icon(Icons.note_outlined, size: 18),
|
||||
counterText: '',
|
||||
),
|
||||
),
|
||||
),
|
||||
if (columns == 1)
|
||||
SizedBox(
|
||||
width: constraints.maxWidth,
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: IconButton(
|
||||
onPressed: enabled ? onRemove : null,
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
tooltip: appText('删除', 'Delete'),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
onPressed: enabled ? onRemove : null,
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
tooltip: appText('删除', 'Delete'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExtraRowControllers {
|
||||
_ExtraRowControllers({
|
||||
required this.keyController,
|
||||
required this.valueController,
|
||||
required this.noteController,
|
||||
});
|
||||
|
||||
final TextEditingController keyController;
|
||||
final TextEditingController valueController;
|
||||
final TextEditingController noteController;
|
||||
|
||||
bool get isSensitiveKey {
|
||||
final key = keyController.text.trim().toUpperCase();
|
||||
return key.contains('KEY') || key.contains('TOKEN') || key.contains('SECRET');
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
keyController.dispose();
|
||||
valueController.dispose();
|
||||
noteController.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,10 +28,12 @@ class WorkspaceProvisionController extends ChangeNotifier {
|
||||
int sshPort = 22;
|
||||
String? sudoPassword;
|
||||
String installPath = '/opt/xworkspace/playbooks';
|
||||
String? deepseekApiKey;
|
||||
String? nvidiaApiKey;
|
||||
String? ollamaApiKey;
|
||||
String? openclawGatewayToken;
|
||||
final List<WorkspaceExtraConfig> extraConfigs = <WorkspaceExtraConfig>[
|
||||
WorkspaceExtraConfig(key: 'DEEPSEEK_API_KEY', value: '', note: ''),
|
||||
WorkspaceExtraConfig(key: 'NVIDIA_API_KEY', value: '', note: ''),
|
||||
WorkspaceExtraConfig(key: 'OLLAMA_API_KEY', value: '', note: ''),
|
||||
WorkspaceExtraConfig(key: 'OPENCLAW_GATEWAY_TOKEN', value: '', note: ''),
|
||||
];
|
||||
bool showAdvanced = false;
|
||||
bool logsExpanded = false;
|
||||
|
||||
@ -146,10 +148,7 @@ class WorkspaceProvisionController extends ChangeNotifier {
|
||||
workspaceDomain: workspaceDomain.trim(),
|
||||
bridgeDomain: bridgeDomain,
|
||||
bridgeToken: bridgeToken,
|
||||
deepseekApiKey: deepseekApiKey,
|
||||
nvidiaApiKey: nvidiaApiKey,
|
||||
ollamaApiKey: ollamaApiKey,
|
||||
openclawGatewayToken: openclawGatewayToken,
|
||||
extraConfigs: extraConfigs,
|
||||
installPath: installPath.trim(),
|
||||
installMissingPrerequisites: installMissingPrerequisites,
|
||||
serverInfo: serverInfo,
|
||||
@ -206,10 +205,7 @@ class WorkspaceProvisionController extends ChangeNotifier {
|
||||
int? sshPort,
|
||||
String? sudoPassword,
|
||||
String? installPath,
|
||||
String? deepseekApiKey,
|
||||
String? nvidiaApiKey,
|
||||
String? ollamaApiKey,
|
||||
String? openclawGatewayToken,
|
||||
List<WorkspaceExtraConfig>? extraConfigs,
|
||||
bool? showAdvanced,
|
||||
bool? logsExpanded,
|
||||
}) {
|
||||
@ -223,39 +219,41 @@ class WorkspaceProvisionController extends ChangeNotifier {
|
||||
this.sshPort = sshPort ?? this.sshPort;
|
||||
this.sudoPassword = sudoPassword ?? this.sudoPassword;
|
||||
this.installPath = installPath ?? this.installPath;
|
||||
this.deepseekApiKey = deepseekApiKey ?? this.deepseekApiKey;
|
||||
this.nvidiaApiKey = nvidiaApiKey ?? this.nvidiaApiKey;
|
||||
this.ollamaApiKey = ollamaApiKey ?? this.ollamaApiKey;
|
||||
this.openclawGatewayToken =
|
||||
openclawGatewayToken ?? this.openclawGatewayToken;
|
||||
if (extraConfigs != null) {
|
||||
this.extraConfigs
|
||||
..clear()
|
||||
..addAll(extraConfigs.map((config) => config.copyWith()));
|
||||
}
|
||||
this.showAdvanced = showAdvanced ?? this.showAdvanced;
|
||||
this.logsExpanded = logsExpanded ?? this.logsExpanded;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
String exportYaml() {
|
||||
final data = <String, Object?>{
|
||||
'server_address': serverAddress.trim(),
|
||||
'workspace_domain': workspaceDomain.trim(),
|
||||
'ssh_username': sshUsername.trim(),
|
||||
'auth_method': authMethod.name,
|
||||
'ssh_port': sshPort,
|
||||
'install_path': installPath.trim(),
|
||||
'show_advanced': showAdvanced,
|
||||
'logs_expanded': logsExpanded,
|
||||
'ssh_password': redact(sshPassword),
|
||||
'ssh_key_content': redact(sshKeyContent),
|
||||
'ssh_key_path': redact(sshKeyPath),
|
||||
'sudo_password': redact(sudoPassword),
|
||||
'deepseek_api_key': redact(deepseekApiKey),
|
||||
'nvidia_api_key': redact(nvidiaApiKey),
|
||||
'ollama_api_key': redact(ollamaApiKey),
|
||||
'openclaw_gateway_token': redact(openclawGatewayToken),
|
||||
};
|
||||
final buffer = StringBuffer();
|
||||
for (final entry in data.entries) {
|
||||
final entries = <MapEntry<String, Object?>>[
|
||||
MapEntry('server_address', serverAddress.trim()),
|
||||
MapEntry('workspace_domain', workspaceDomain.trim()),
|
||||
MapEntry('ssh_username', sshUsername.trim()),
|
||||
MapEntry('auth_method', authMethod.name),
|
||||
MapEntry('ssh_port', sshPort),
|
||||
MapEntry('install_path', installPath.trim()),
|
||||
MapEntry('show_advanced', showAdvanced),
|
||||
MapEntry('logs_expanded', logsExpanded),
|
||||
MapEntry('ssh_password', redact(sshPassword)),
|
||||
MapEntry('ssh_key_content', redact(sshKeyContent)),
|
||||
MapEntry('ssh_key_path', redact(sshKeyPath)),
|
||||
MapEntry('sudo_password', redact(sudoPassword)),
|
||||
];
|
||||
for (final entry in entries) {
|
||||
buffer.writeln('${entry.key}: ${yamlScalar(entry.value)}');
|
||||
}
|
||||
buffer.writeln('extra_configs:');
|
||||
for (final config in extraConfigs) {
|
||||
buffer.writeln(' - key: ${yamlScalar(config.key.trim())}');
|
||||
buffer.writeln(' value: ${yamlScalar(redact(config.value))}');
|
||||
buffer.writeln(' note: ${yamlScalar(sanitizeNote(config.note))}');
|
||||
}
|
||||
return buffer.toString().trimRight();
|
||||
}
|
||||
|
||||
@ -279,13 +277,7 @@ class WorkspaceProvisionController extends ChangeNotifier {
|
||||
sshPort: intValue(map['ssh_port'], sshPort),
|
||||
sudoPassword: secretValue(map['sudo_password'], sudoPassword),
|
||||
installPath: stringValue(map['install_path']),
|
||||
deepseekApiKey: secretValue(map['deepseek_api_key'], deepseekApiKey),
|
||||
nvidiaApiKey: secretValue(map['nvidia_api_key'], nvidiaApiKey),
|
||||
ollamaApiKey: secretValue(map['ollama_api_key'], ollamaApiKey),
|
||||
openclawGatewayToken: secretValue(
|
||||
map['openclaw_gateway_token'],
|
||||
openclawGatewayToken,
|
||||
),
|
||||
extraConfigs: parseExtraConfigs(map['extra_configs'], extraConfigs),
|
||||
showAdvanced: boolValue(map['show_advanced'], showAdvanced),
|
||||
logsExpanded: boolValue(map['logs_expanded'], logsExpanded),
|
||||
);
|
||||
@ -386,10 +378,6 @@ class WorkspaceProvisionController extends ChangeNotifier {
|
||||
sshPassword = null;
|
||||
sshKeyContent = null;
|
||||
sudoPassword = null;
|
||||
deepseekApiKey = null;
|
||||
nvidiaApiKey = null;
|
||||
ollamaApiKey = null;
|
||||
openclawGatewayToken = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -398,7 +386,7 @@ class WorkspaceProvisionController extends ChangeNotifier {
|
||||
if (domain.isEmpty) {
|
||||
return '';
|
||||
}
|
||||
if (domain.startsWith('xworkmate-bridge.')) {
|
||||
if (domain.contains('bridge.') || domain.startsWith('bridge.')) {
|
||||
return domain;
|
||||
}
|
||||
return 'xworkmate-bridge.$domain';
|
||||
@ -461,6 +449,47 @@ class WorkspaceProvisionController extends ChangeNotifier {
|
||||
final text = value?.toString().trim().toLowerCase() ?? '';
|
||||
return text == 'password' ? AuthMethod.password : AuthMethod.sshKey;
|
||||
}
|
||||
|
||||
static String sanitizeNote(String note) {
|
||||
final trimmed = note.trim();
|
||||
return trimmed.length <= 20 ? trimmed : trimmed.substring(0, 20);
|
||||
}
|
||||
|
||||
static List<WorkspaceExtraConfig> parseExtraConfigs(
|
||||
Object? value,
|
||||
List<WorkspaceExtraConfig> current,
|
||||
) {
|
||||
final existing = {
|
||||
for (final config in current) config.key.trim(): config,
|
||||
};
|
||||
final parsed = <WorkspaceExtraConfig>[];
|
||||
if (value is YamlList) {
|
||||
for (final item in value) {
|
||||
if (item is! YamlMap) {
|
||||
continue;
|
||||
}
|
||||
final key = item['key']?.toString().trim() ?? '';
|
||||
if (key.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
final rawValue = item['value']?.toString().trim() ?? '';
|
||||
final note = sanitizeNote(item['note']?.toString() ?? '');
|
||||
parsed.add(
|
||||
WorkspaceExtraConfig(
|
||||
key: key,
|
||||
value: rawValue == redactedValue
|
||||
? (existing[key]?.value ?? '')
|
||||
: rawValue,
|
||||
note: note,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (parsed.isNotEmpty) {
|
||||
return parsed;
|
||||
}
|
||||
return current.map((config) => config.copyWith()).toList();
|
||||
}
|
||||
}
|
||||
|
||||
class WorkspaceProvisionPrecheckException implements Exception {
|
||||
|
||||
@ -131,6 +131,30 @@ class SshConfig {
|
||||
String get targetLabel => '$username@$host:$port';
|
||||
}
|
||||
|
||||
class WorkspaceExtraConfig {
|
||||
WorkspaceExtraConfig({
|
||||
required this.key,
|
||||
required this.value,
|
||||
this.note = '',
|
||||
});
|
||||
|
||||
String key;
|
||||
String value;
|
||||
String note;
|
||||
|
||||
WorkspaceExtraConfig copyWith({
|
||||
String? key,
|
||||
String? value,
|
||||
String? note,
|
||||
}) {
|
||||
return WorkspaceExtraConfig(
|
||||
key: key ?? this.key,
|
||||
value: value ?? this.value,
|
||||
note: note ?? this.note,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SshResult {
|
||||
const SshResult({
|
||||
required this.exitCode,
|
||||
|
||||
@ -90,6 +90,13 @@ BRIDGE_PORT_443_OPEN=yes
|
||||
expect(command, contains('getent hosts'));
|
||||
});
|
||||
|
||||
test('bridge domain uses user input when already a bridge host', () {
|
||||
expect(
|
||||
WorkspaceProvisionController.deriveBridgeDomain('acp-bridge.onwalk.net'),
|
||||
'acp-bridge.onwalk.net',
|
||||
);
|
||||
});
|
||||
|
||||
test('exported yaml redacts sensitive values', () {
|
||||
final controller = WorkspaceProvisionController();
|
||||
addTearDown(controller.dispose);
|
||||
@ -98,17 +105,28 @@ BRIDGE_PORT_443_OPEN=yes
|
||||
workspaceDomain: 'onwalk.net',
|
||||
sshUsername: 'root',
|
||||
sshPassword: 'ssh-secret',
|
||||
deepseekApiKey: 'deepseek-secret',
|
||||
openclawGatewayToken: 'gateway-secret',
|
||||
showAdvanced: true,
|
||||
extraConfigs: [
|
||||
WorkspaceExtraConfig(
|
||||
key: 'DEEPSEEK_API_KEY',
|
||||
value: 'deepseek-secret',
|
||||
note: '深度搜索',
|
||||
),
|
||||
WorkspaceExtraConfig(
|
||||
key: 'OPENCLAW_GATEWAY_TOKEN',
|
||||
value: 'gateway-secret',
|
||||
note: 'OpenClaw',
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final yaml = controller.exportYaml();
|
||||
|
||||
expect(yaml, contains('server_address: 203.0.113.10'));
|
||||
expect(yaml, contains('ssh_password_fixture: "example"'));
|
||||
expect(yaml, contains('deepseek_api_key: "__redacted__"'));
|
||||
expect(yaml, contains('openclaw_gateway_token: "__redacted__"'));
|
||||
expect(yaml, contains('extra_configs:'));
|
||||
expect(yaml, contains('key: DEEPSEEK_API_KEY'));
|
||||
expect(yaml, contains('value: "__redacted__"'));
|
||||
});
|
||||
});
|
||||
|
||||
@ -314,16 +332,21 @@ install_path: /opt/xworkspace/playbooks
|
||||
show_advanced: true
|
||||
logs_expanded: false
|
||||
ssh_password_fixture: "example"
|
||||
deepseek_api_key: "deepseek-new"
|
||||
openclaw_gateway_token: "__redacted__"
|
||||
extra_configs:
|
||||
- key: DEEPSEEK_API_KEY
|
||||
value: "deepseek-new"
|
||||
note: "深度搜索"
|
||||
- key: OPENCLAW_GATEWAY_TOKEN
|
||||
value: "__redacted__"
|
||||
note: "OpenClaw"
|
||||
''');
|
||||
|
||||
expect(controller.serverAddress, '167.179.110.129');
|
||||
expect(controller.workspaceDomain, 'onwalk.net');
|
||||
expect(controller.showAdvanced, isTrue);
|
||||
expect(controller.sshPassword, 'keep-secret');
|
||||
expect(controller.deepseekApiKey, 'deepseek-new');
|
||||
expect(controller.openclawGatewayToken, isNull);
|
||||
expect(controller.extraConfigs.first.value, 'deepseek-new');
|
||||
expect(controller.extraConfigs.last.value, '');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user