neoagent 3.1.0 → 3.1.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/flutter_app/lib/main_integrations.dart +252 -0
- package/flutter_app/lib/main_operations.dart +364 -322
- package/package.json +2 -1
- package/server/guest-agent.browser.package.json +1 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +38078 -37802
- package/server/services/integrations/neomail/provider.js +993 -0
- package/server/services/integrations/registry.js +2 -0
- package/server/services/tasks/adapters/index.js +1 -1
- package/server/services/tasks/adapters/neomail_email_received.js +46 -0
- package/server/services/tasks/integration_runtime.js +67 -3
- package/server/services/tasks/runtime.js +6 -1
- package/server/services/tasks/task_repository.js +13 -6
|
@@ -231,12 +231,262 @@ void _openOfficialIntegrationSetupDialog(
|
|
|
231
231
|
case 'home_assistant':
|
|
232
232
|
_showHomeAssistantSetupDialog(context, controller);
|
|
233
233
|
return;
|
|
234
|
+
case 'neomail':
|
|
235
|
+
_showNeoMailSetupDialog(context, controller);
|
|
236
|
+
return;
|
|
234
237
|
case 'trello':
|
|
235
238
|
_showTrelloSetupDialog(context, controller);
|
|
236
239
|
return;
|
|
237
240
|
}
|
|
238
241
|
}
|
|
239
242
|
|
|
243
|
+
Future<void> _showNeoMailSetupDialog(
|
|
244
|
+
BuildContext context,
|
|
245
|
+
NeoAgentController controller,
|
|
246
|
+
) async {
|
|
247
|
+
Map<String, dynamic> existing;
|
|
248
|
+
try {
|
|
249
|
+
existing = await controller.getOfficialIntegrationConfig('neomail');
|
|
250
|
+
} catch (error) {
|
|
251
|
+
if (context.mounted) {
|
|
252
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
253
|
+
SnackBar(content: Text(controller.errorMessage ?? error.toString())),
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
final savedBaseUrl = existing['baseUrl']?.toString() ?? '';
|
|
260
|
+
final accountCount = (existing['accountCount'] as num?)?.toInt() ?? 0;
|
|
261
|
+
final hasConnectedAccount =
|
|
262
|
+
existing['hasConnectedAccount'] == true || accountCount > 0;
|
|
263
|
+
var formError = '';
|
|
264
|
+
var busy = false;
|
|
265
|
+
|
|
266
|
+
final baseUrlController = TextEditingController(text: savedBaseUrl);
|
|
267
|
+
|
|
268
|
+
Future<void> saveConfig(
|
|
269
|
+
StateSetter setState, {
|
|
270
|
+
required BuildContext dialogContext,
|
|
271
|
+
required bool connectAfterSave,
|
|
272
|
+
}) async {
|
|
273
|
+
setState(() {
|
|
274
|
+
formError = '';
|
|
275
|
+
busy = true;
|
|
276
|
+
});
|
|
277
|
+
try {
|
|
278
|
+
final baseUrl = baseUrlController.text.trim();
|
|
279
|
+
if (baseUrl.isEmpty) {
|
|
280
|
+
setState(() {
|
|
281
|
+
formError = 'NeoMail backend URL is required.';
|
|
282
|
+
busy = false;
|
|
283
|
+
});
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
await controller.saveOfficialIntegrationConfig(
|
|
287
|
+
'neomail',
|
|
288
|
+
config: <String, dynamic>{'baseUrl': baseUrl},
|
|
289
|
+
);
|
|
290
|
+
if (connectAfterSave) {
|
|
291
|
+
await controller.connectOfficialIntegration(
|
|
292
|
+
'neomail',
|
|
293
|
+
appId: 'mailbox',
|
|
294
|
+
);
|
|
295
|
+
if ((controller.errorMessage ?? '').trim().isNotEmpty) {
|
|
296
|
+
setState(() {
|
|
297
|
+
formError = controller.errorMessage!;
|
|
298
|
+
busy = false;
|
|
299
|
+
});
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (dialogContext.mounted) {
|
|
304
|
+
Navigator.of(dialogContext).pop();
|
|
305
|
+
}
|
|
306
|
+
} catch (_) {
|
|
307
|
+
setState(() {
|
|
308
|
+
formError = controller.errorMessage ?? 'Could not save NeoMail setup.';
|
|
309
|
+
busy = false;
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (!context.mounted) return;
|
|
315
|
+
await showDialog<void>(
|
|
316
|
+
context: context,
|
|
317
|
+
barrierDismissible: false,
|
|
318
|
+
builder: (dialogContext) {
|
|
319
|
+
return StatefulBuilder(
|
|
320
|
+
builder: (dialogContext, setState) {
|
|
321
|
+
return AlertDialog(
|
|
322
|
+
title: const Text('NeoMail Setup'),
|
|
323
|
+
content: SizedBox(
|
|
324
|
+
width: 540,
|
|
325
|
+
child: Column(
|
|
326
|
+
mainAxisSize: MainAxisSize.min,
|
|
327
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
328
|
+
children: <Widget>[
|
|
329
|
+
Text(
|
|
330
|
+
'Add the NeoMail backend URL once. NeoAgent will then open NeoMail OAuth so the user can sign in and approve mailbox access without API keys.',
|
|
331
|
+
style: TextStyle(color: _textSecondary),
|
|
332
|
+
),
|
|
333
|
+
const SizedBox(height: 16),
|
|
334
|
+
const _IntegrationSetupStatusItem(
|
|
335
|
+
label: 'Connection Method',
|
|
336
|
+
status: 'OAuth companion flow',
|
|
337
|
+
isConnected: true,
|
|
338
|
+
),
|
|
339
|
+
const SizedBox(height: 12),
|
|
340
|
+
_IntegrationSetupStatusItem(
|
|
341
|
+
label: 'Backend URL',
|
|
342
|
+
status: savedBaseUrl.trim().isNotEmpty
|
|
343
|
+
? 'Configured'
|
|
344
|
+
: 'Not configured',
|
|
345
|
+
isConnected: savedBaseUrl.trim().isNotEmpty,
|
|
346
|
+
),
|
|
347
|
+
const SizedBox(height: 12),
|
|
348
|
+
_IntegrationSetupStatusItem(
|
|
349
|
+
label: 'Connected NeoMail User',
|
|
350
|
+
status: hasConnectedAccount
|
|
351
|
+
? '$accountCount ${accountCount == 1 ? 'connected user' : 'connected users'}'
|
|
352
|
+
: 'Not connected',
|
|
353
|
+
isConnected: hasConnectedAccount,
|
|
354
|
+
),
|
|
355
|
+
const SizedBox(height: 12),
|
|
356
|
+
TextField(
|
|
357
|
+
controller: baseUrlController,
|
|
358
|
+
onChanged: (_) => setState(() {}),
|
|
359
|
+
keyboardType: TextInputType.url,
|
|
360
|
+
decoration: const InputDecoration(
|
|
361
|
+
labelText: 'NeoMail Backend URL',
|
|
362
|
+
hintText: 'https://mail.example.com',
|
|
363
|
+
border: OutlineInputBorder(),
|
|
364
|
+
),
|
|
365
|
+
),
|
|
366
|
+
const SizedBox(height: 8),
|
|
367
|
+
Text(
|
|
368
|
+
'Use the public base URL of the NeoMail server. Local self-hosted URLs are also supported when NeoAgent can reach them.',
|
|
369
|
+
style: TextStyle(color: _textSecondary, fontSize: 12),
|
|
370
|
+
),
|
|
371
|
+
if (formError.isNotEmpty) ...<Widget>[
|
|
372
|
+
const SizedBox(height: 12),
|
|
373
|
+
Container(
|
|
374
|
+
padding: const EdgeInsets.all(8),
|
|
375
|
+
decoration: BoxDecoration(
|
|
376
|
+
color: _danger.withValues(alpha: 0.1),
|
|
377
|
+
borderRadius: BorderRadius.circular(4),
|
|
378
|
+
border: Border.all(
|
|
379
|
+
color: _danger.withValues(alpha: 0.3),
|
|
380
|
+
),
|
|
381
|
+
),
|
|
382
|
+
child: Text(
|
|
383
|
+
formError,
|
|
384
|
+
style: TextStyle(color: _danger, fontSize: 12),
|
|
385
|
+
),
|
|
386
|
+
),
|
|
387
|
+
],
|
|
388
|
+
],
|
|
389
|
+
),
|
|
390
|
+
),
|
|
391
|
+
actions: <Widget>[
|
|
392
|
+
if (savedBaseUrl.trim().isNotEmpty)
|
|
393
|
+
TextButton(
|
|
394
|
+
onPressed: busy
|
|
395
|
+
? null
|
|
396
|
+
: () async {
|
|
397
|
+
final shouldClear =
|
|
398
|
+
await showDialog<bool>(
|
|
399
|
+
context: dialogContext,
|
|
400
|
+
builder: (context) {
|
|
401
|
+
return AlertDialog(
|
|
402
|
+
title: const Text('Disconnect NeoMail?'),
|
|
403
|
+
content: const Text(
|
|
404
|
+
'This removes the NeoMail backend URL and all connected NeoMail accounts for this agent.',
|
|
405
|
+
),
|
|
406
|
+
actions: [
|
|
407
|
+
TextButton(
|
|
408
|
+
onPressed: () =>
|
|
409
|
+
Navigator.of(context).pop(false),
|
|
410
|
+
child: const Text('Cancel'),
|
|
411
|
+
),
|
|
412
|
+
FilledButton(
|
|
413
|
+
onPressed: () =>
|
|
414
|
+
Navigator.of(context).pop(true),
|
|
415
|
+
child: const Text('Disconnect'),
|
|
416
|
+
),
|
|
417
|
+
],
|
|
418
|
+
);
|
|
419
|
+
},
|
|
420
|
+
) ??
|
|
421
|
+
false;
|
|
422
|
+
if (!shouldClear) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
setState(() {
|
|
426
|
+
formError = '';
|
|
427
|
+
busy = true;
|
|
428
|
+
});
|
|
429
|
+
try {
|
|
430
|
+
await controller.clearOfficialIntegrationConfig(
|
|
431
|
+
'neomail',
|
|
432
|
+
);
|
|
433
|
+
if (dialogContext.mounted) {
|
|
434
|
+
Navigator.of(dialogContext).pop();
|
|
435
|
+
}
|
|
436
|
+
} catch (_) {
|
|
437
|
+
setState(() {
|
|
438
|
+
formError =
|
|
439
|
+
controller.errorMessage ??
|
|
440
|
+
'Could not disconnect NeoMail.';
|
|
441
|
+
busy = false;
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
child: const Text('Disconnect'),
|
|
446
|
+
),
|
|
447
|
+
TextButton(
|
|
448
|
+
onPressed: busy
|
|
449
|
+
? null
|
|
450
|
+
: () => Navigator.of(dialogContext).pop(),
|
|
451
|
+
child: const Text('Close'),
|
|
452
|
+
),
|
|
453
|
+
if (!hasConnectedAccount)
|
|
454
|
+
TextButton(
|
|
455
|
+
onPressed: busy
|
|
456
|
+
? null
|
|
457
|
+
: () => saveConfig(
|
|
458
|
+
setState,
|
|
459
|
+
dialogContext: dialogContext,
|
|
460
|
+
connectAfterSave: false,
|
|
461
|
+
),
|
|
462
|
+
child: const Text('Save Only'),
|
|
463
|
+
),
|
|
464
|
+
FilledButton(
|
|
465
|
+
onPressed: busy
|
|
466
|
+
? null
|
|
467
|
+
: () => saveConfig(
|
|
468
|
+
setState,
|
|
469
|
+
dialogContext: dialogContext,
|
|
470
|
+
connectAfterSave: !hasConnectedAccount,
|
|
471
|
+
),
|
|
472
|
+
child: Text(
|
|
473
|
+
busy
|
|
474
|
+
? 'Working...'
|
|
475
|
+
: hasConnectedAccount
|
|
476
|
+
? 'Update Setup'
|
|
477
|
+
: 'Save & Connect',
|
|
478
|
+
),
|
|
479
|
+
),
|
|
480
|
+
],
|
|
481
|
+
);
|
|
482
|
+
},
|
|
483
|
+
);
|
|
484
|
+
},
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
baseUrlController.dispose();
|
|
488
|
+
}
|
|
489
|
+
|
|
240
490
|
Future<void> _showHomeAssistantSetupDialog(
|
|
241
491
|
BuildContext context,
|
|
242
492
|
NeoAgentController controller,
|
|
@@ -1132,12 +1382,14 @@ class _OfficialIntegrationIcon extends StatelessWidget {
|
|
|
1132
1382
|
final color = switch (item.icon) {
|
|
1133
1383
|
'google' => const Color(0xFF4285F4),
|
|
1134
1384
|
'home_assistant' => const Color(0xFF41BDF5),
|
|
1385
|
+
'neomail' => const Color(0xFF0F9D8A),
|
|
1135
1386
|
'trello' => const Color(0xFF0C66E4),
|
|
1136
1387
|
_ => _accent,
|
|
1137
1388
|
};
|
|
1138
1389
|
final label = switch (item.icon) {
|
|
1139
1390
|
'google' => 'G',
|
|
1140
1391
|
'home_assistant' => 'H',
|
|
1392
|
+
'neomail' => 'N',
|
|
1141
1393
|
'trello' => 'T',
|
|
1142
1394
|
_ => item.label.isNotEmpty ? item.label[0] : '?',
|
|
1143
1395
|
};
|