neoagent 3.1.1-beta.1 → 3.1.1-beta.3
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_controller.dart +17 -0
- package/flutter_app/lib/main_models.dart +55 -0
- package/flutter_app/lib/main_operations.dart +530 -2
- package/flutter_app/lib/src/backend_client.dart +20 -0
- package/lib/schema_migrations.js +23 -0
- package/package.json +1 -1
- package/server/admin/admin.js +1 -0
- package/server/admin/billing.js +35 -15
- package/server/db/database.js +6 -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 +63376 -62995
- package/server/routes/tasks.js +17 -0
- package/server/services/ai/tools.js +1 -1
- package/server/services/memory/consolidation.js +2 -0
- package/server/services/memory/manager.js +70 -8
- package/server/services/memory/retention.js +66 -0
- package/server/services/memory/routing.js +68 -0
- package/server/services/messaging/access_policy.js +39 -2
- package/server/services/messaging/manager.js +9 -4
- package/server/services/messaging/whatsapp.js +2 -3
- package/server/services/tasks/delivery_targets.js +260 -0
- package/server/utils/whatsapp.js +33 -1
|
@@ -2796,6 +2796,23 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
2796
2796
|
notifyListeners();
|
|
2797
2797
|
}
|
|
2798
2798
|
|
|
2799
|
+
Future<List<TaskDeliveryTarget>> fetchTaskDeliveryTargets({
|
|
2800
|
+
String? query,
|
|
2801
|
+
String? platform,
|
|
2802
|
+
String? agentId,
|
|
2803
|
+
}) async {
|
|
2804
|
+
final rows = await _backendClient.fetchTaskDeliveryTargets(
|
|
2805
|
+
backendUrl,
|
|
2806
|
+
query: query,
|
|
2807
|
+
platform: platform,
|
|
2808
|
+
agentId: agentId ?? _scopedAgentId,
|
|
2809
|
+
);
|
|
2810
|
+
return rows
|
|
2811
|
+
.map(TaskDeliveryTarget.fromJson)
|
|
2812
|
+
.where((target) => target.platform.isNotEmpty && target.to.isNotEmpty)
|
|
2813
|
+
.toList(growable: false);
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2799
2816
|
Future<void> refreshWidgets({bool all = false}) async {
|
|
2800
2817
|
widgets = _decodeModelList(
|
|
2801
2818
|
'widgets',
|
|
@@ -643,6 +643,61 @@ class MessagingMessage {
|
|
|
643
643
|
}
|
|
644
644
|
}
|
|
645
645
|
|
|
646
|
+
class TaskDeliveryTarget {
|
|
647
|
+
const TaskDeliveryTarget({
|
|
648
|
+
required this.platform,
|
|
649
|
+
required this.platformLabel,
|
|
650
|
+
required this.to,
|
|
651
|
+
required this.label,
|
|
652
|
+
required this.subtitle,
|
|
653
|
+
required this.source,
|
|
654
|
+
required this.connected,
|
|
655
|
+
required this.supportsDelivery,
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
factory TaskDeliveryTarget.fromJson(Map<dynamic, dynamic> json) {
|
|
659
|
+
final platform = json['platform']?.toString() ?? '';
|
|
660
|
+
final to = json['to']?.toString() ?? '';
|
|
661
|
+
return TaskDeliveryTarget(
|
|
662
|
+
platform: platform,
|
|
663
|
+
platformLabel:
|
|
664
|
+
json['platformLabel']?.toString().ifEmpty(platform.toUpperCase()) ??
|
|
665
|
+
platform.toUpperCase(),
|
|
666
|
+
to: to,
|
|
667
|
+
label: json['label']?.toString().ifEmpty(to) ?? to,
|
|
668
|
+
subtitle: json['subtitle']?.toString() ?? '',
|
|
669
|
+
source: json['source']?.toString().ifEmpty('discovered') ?? 'discovered',
|
|
670
|
+
connected: json['connected'] != false,
|
|
671
|
+
supportsDelivery: json['supportsDelivery'] != false,
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
final String platform;
|
|
676
|
+
final String platformLabel;
|
|
677
|
+
final String to;
|
|
678
|
+
final String label;
|
|
679
|
+
final String subtitle;
|
|
680
|
+
final String source;
|
|
681
|
+
final bool connected;
|
|
682
|
+
final bool supportsDelivery;
|
|
683
|
+
|
|
684
|
+
String get id => '$platform:$to';
|
|
685
|
+
bool get selectable => connected && supportsDelivery;
|
|
686
|
+
|
|
687
|
+
String get sourceLabel {
|
|
688
|
+
switch (source) {
|
|
689
|
+
case 'default':
|
|
690
|
+
return 'Default';
|
|
691
|
+
case 'recent':
|
|
692
|
+
return 'Recent';
|
|
693
|
+
case 'manual':
|
|
694
|
+
return 'Manual';
|
|
695
|
+
default:
|
|
696
|
+
return 'Discovered';
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
646
701
|
class MessagingQrState {
|
|
647
702
|
const MessagingQrState({required this.platform, required this.qr});
|
|
648
703
|
|
|
@@ -5289,6 +5289,426 @@ Future<String?> _pickTaskTriggerType(
|
|
|
5289
5289
|
);
|
|
5290
5290
|
}
|
|
5291
5291
|
|
|
5292
|
+
IconData _taskDeliveryPlatformIcon(String platform) {
|
|
5293
|
+
for (final descriptor in messagingPlatforms) {
|
|
5294
|
+
if (descriptor.id == platform) return descriptor.icon;
|
|
5295
|
+
}
|
|
5296
|
+
return Icons.forum_rounded;
|
|
5297
|
+
}
|
|
5298
|
+
|
|
5299
|
+
Color _taskDeliveryPlatformColor(String platform) {
|
|
5300
|
+
for (final descriptor in messagingPlatforms) {
|
|
5301
|
+
if (descriptor.id == platform) return descriptor.accent;
|
|
5302
|
+
}
|
|
5303
|
+
return _accent;
|
|
5304
|
+
}
|
|
5305
|
+
|
|
5306
|
+
String _taskDeliveryPlatformLabel(String platform) {
|
|
5307
|
+
for (final descriptor in messagingPlatforms) {
|
|
5308
|
+
if (descriptor.id == platform) return descriptor.label;
|
|
5309
|
+
}
|
|
5310
|
+
return platform.replaceAll('_', ' ').toUpperCase();
|
|
5311
|
+
}
|
|
5312
|
+
|
|
5313
|
+
TaskDeliveryTarget? _taskDeliveryTargetFromTask(TaskItem? task) {
|
|
5314
|
+
final platform = task?.taskConfig['notifyPlatform']?.toString().trim() ?? '';
|
|
5315
|
+
final to = task?.taskConfig['notifyTo']?.toString().trim() ?? '';
|
|
5316
|
+
if (platform.isEmpty || to.isEmpty) return null;
|
|
5317
|
+
return TaskDeliveryTarget(
|
|
5318
|
+
platform: platform,
|
|
5319
|
+
platformLabel: _taskDeliveryPlatformLabel(platform),
|
|
5320
|
+
to: to,
|
|
5321
|
+
label: to,
|
|
5322
|
+
subtitle: 'Saved delivery destination',
|
|
5323
|
+
source: 'manual',
|
|
5324
|
+
connected: true,
|
|
5325
|
+
supportsDelivery: true,
|
|
5326
|
+
);
|
|
5327
|
+
}
|
|
5328
|
+
|
|
5329
|
+
Future<TaskDeliveryTarget?> _pickTaskDeliveryTarget(
|
|
5330
|
+
BuildContext context, {
|
|
5331
|
+
required NeoAgentController controller,
|
|
5332
|
+
required String? agentId,
|
|
5333
|
+
required TaskDeliveryTarget? selected,
|
|
5334
|
+
}) {
|
|
5335
|
+
return showModalBottomSheet<TaskDeliveryTarget?>(
|
|
5336
|
+
context: context,
|
|
5337
|
+
isScrollControlled: true,
|
|
5338
|
+
backgroundColor: _bgCard,
|
|
5339
|
+
builder: (context) => _TaskDeliveryTargetPickerSheet(
|
|
5340
|
+
controller: controller,
|
|
5341
|
+
agentId: agentId,
|
|
5342
|
+
selected: selected,
|
|
5343
|
+
),
|
|
5344
|
+
);
|
|
5345
|
+
}
|
|
5346
|
+
|
|
5347
|
+
class _TaskDeliveryTargetPickerSheet extends StatefulWidget {
|
|
5348
|
+
const _TaskDeliveryTargetPickerSheet({
|
|
5349
|
+
required this.controller,
|
|
5350
|
+
required this.agentId,
|
|
5351
|
+
required this.selected,
|
|
5352
|
+
});
|
|
5353
|
+
|
|
5354
|
+
final NeoAgentController controller;
|
|
5355
|
+
final String? agentId;
|
|
5356
|
+
final TaskDeliveryTarget? selected;
|
|
5357
|
+
|
|
5358
|
+
@override
|
|
5359
|
+
State<_TaskDeliveryTargetPickerSheet> createState() =>
|
|
5360
|
+
_TaskDeliveryTargetPickerSheetState();
|
|
5361
|
+
}
|
|
5362
|
+
|
|
5363
|
+
class _TaskDeliveryTargetPickerSheetState
|
|
5364
|
+
extends State<_TaskDeliveryTargetPickerSheet> {
|
|
5365
|
+
late final TextEditingController _queryController;
|
|
5366
|
+
late final TextEditingController _manualController;
|
|
5367
|
+
Timer? _debounce;
|
|
5368
|
+
Future<List<TaskDeliveryTarget>>? _targetsFuture;
|
|
5369
|
+
String? _platformFilter;
|
|
5370
|
+
String? _manualPlatform;
|
|
5371
|
+
|
|
5372
|
+
@override
|
|
5373
|
+
void initState() {
|
|
5374
|
+
super.initState();
|
|
5375
|
+
_queryController = TextEditingController();
|
|
5376
|
+
_manualController = TextEditingController();
|
|
5377
|
+
_manualPlatform = widget.selected?.platform;
|
|
5378
|
+
_loadTargets();
|
|
5379
|
+
}
|
|
5380
|
+
|
|
5381
|
+
@override
|
|
5382
|
+
void dispose() {
|
|
5383
|
+
_debounce?.cancel();
|
|
5384
|
+
_queryController.dispose();
|
|
5385
|
+
_manualController.dispose();
|
|
5386
|
+
super.dispose();
|
|
5387
|
+
}
|
|
5388
|
+
|
|
5389
|
+
void _loadTargets() {
|
|
5390
|
+
setState(() {
|
|
5391
|
+
_targetsFuture = widget.controller.fetchTaskDeliveryTargets(
|
|
5392
|
+
query: _queryController.text,
|
|
5393
|
+
platform: _platformFilter,
|
|
5394
|
+
agentId: widget.agentId,
|
|
5395
|
+
);
|
|
5396
|
+
});
|
|
5397
|
+
}
|
|
5398
|
+
|
|
5399
|
+
void _scheduleSearch() {
|
|
5400
|
+
_debounce?.cancel();
|
|
5401
|
+
_debounce = Timer(const Duration(milliseconds: 220), _loadTargets);
|
|
5402
|
+
}
|
|
5403
|
+
|
|
5404
|
+
List<String> _platformIds(List<TaskDeliveryTarget> targets) {
|
|
5405
|
+
final ids = <String>{
|
|
5406
|
+
if (widget.selected?.platform.isNotEmpty == true)
|
|
5407
|
+
widget.selected!.platform,
|
|
5408
|
+
...targets.map((target) => target.platform),
|
|
5409
|
+
...messagingPlatforms
|
|
5410
|
+
.where(
|
|
5411
|
+
(platform) =>
|
|
5412
|
+
platform.id == 'whatsapp' ||
|
|
5413
|
+
platform.id == 'discord' ||
|
|
5414
|
+
platform.id == 'telegram' ||
|
|
5415
|
+
platform.id == 'slack',
|
|
5416
|
+
)
|
|
5417
|
+
.map((platform) => platform.id),
|
|
5418
|
+
}.toList();
|
|
5419
|
+
ids.sort(
|
|
5420
|
+
(left, right) => _taskDeliveryPlatformLabel(
|
|
5421
|
+
left,
|
|
5422
|
+
).compareTo(_taskDeliveryPlatformLabel(right)),
|
|
5423
|
+
);
|
|
5424
|
+
return ids;
|
|
5425
|
+
}
|
|
5426
|
+
|
|
5427
|
+
void _submitManual() {
|
|
5428
|
+
final to = _manualController.text.trim();
|
|
5429
|
+
final platform =
|
|
5430
|
+
_manualPlatform ?? _platformFilter ?? widget.selected?.platform ?? '';
|
|
5431
|
+
if (to.isEmpty || platform.isEmpty) return;
|
|
5432
|
+
Navigator.of(context).pop(
|
|
5433
|
+
TaskDeliveryTarget(
|
|
5434
|
+
platform: platform,
|
|
5435
|
+
platformLabel: _taskDeliveryPlatformLabel(platform),
|
|
5436
|
+
to: to,
|
|
5437
|
+
label: to,
|
|
5438
|
+
subtitle: 'Manual destination',
|
|
5439
|
+
source: 'manual',
|
|
5440
|
+
connected: true,
|
|
5441
|
+
supportsDelivery: true,
|
|
5442
|
+
),
|
|
5443
|
+
);
|
|
5444
|
+
}
|
|
5445
|
+
|
|
5446
|
+
Widget _buildTargetTile(TaskDeliveryTarget target) {
|
|
5447
|
+
final color = _taskDeliveryPlatformColor(target.platform);
|
|
5448
|
+
final selected = widget.selected?.id == target.id;
|
|
5449
|
+
return ListTile(
|
|
5450
|
+
contentPadding: const EdgeInsets.symmetric(horizontal: 2, vertical: 2),
|
|
5451
|
+
leading: Container(
|
|
5452
|
+
width: 42,
|
|
5453
|
+
height: 42,
|
|
5454
|
+
decoration: BoxDecoration(
|
|
5455
|
+
color: color.withValues(alpha: 0.12),
|
|
5456
|
+
borderRadius: BorderRadius.circular(12),
|
|
5457
|
+
),
|
|
5458
|
+
child: Icon(_taskDeliveryPlatformIcon(target.platform), color: color),
|
|
5459
|
+
),
|
|
5460
|
+
title: Text(
|
|
5461
|
+
target.label,
|
|
5462
|
+
maxLines: 1,
|
|
5463
|
+
overflow: TextOverflow.ellipsis,
|
|
5464
|
+
style: const TextStyle(fontWeight: FontWeight.w700),
|
|
5465
|
+
),
|
|
5466
|
+
subtitle: Text(
|
|
5467
|
+
'${target.platformLabel} · ${target.subtitle.ifEmpty(target.to)}',
|
|
5468
|
+
maxLines: 2,
|
|
5469
|
+
overflow: TextOverflow.ellipsis,
|
|
5470
|
+
),
|
|
5471
|
+
trailing: Wrap(
|
|
5472
|
+
spacing: 8,
|
|
5473
|
+
crossAxisAlignment: WrapCrossAlignment.center,
|
|
5474
|
+
children: <Widget>[
|
|
5475
|
+
_StatusPill(label: target.sourceLabel, color: color),
|
|
5476
|
+
Icon(
|
|
5477
|
+
selected ? Icons.check_circle_rounded : Icons.arrow_forward_rounded,
|
|
5478
|
+
color: selected ? color : _textSecondary,
|
|
5479
|
+
),
|
|
5480
|
+
],
|
|
5481
|
+
),
|
|
5482
|
+
enabled: target.selectable,
|
|
5483
|
+
onTap: target.selectable ? () => Navigator.of(context).pop(target) : null,
|
|
5484
|
+
);
|
|
5485
|
+
}
|
|
5486
|
+
|
|
5487
|
+
@override
|
|
5488
|
+
Widget build(BuildContext context) {
|
|
5489
|
+
return Padding(
|
|
5490
|
+
padding: EdgeInsets.only(
|
|
5491
|
+
left: 20,
|
|
5492
|
+
right: 20,
|
|
5493
|
+
top: 18,
|
|
5494
|
+
bottom: MediaQuery.of(context).viewInsets.bottom + 20,
|
|
5495
|
+
),
|
|
5496
|
+
child: ConstrainedBox(
|
|
5497
|
+
constraints: const BoxConstraints(maxWidth: 760, maxHeight: 720),
|
|
5498
|
+
child: FutureBuilder<List<TaskDeliveryTarget>>(
|
|
5499
|
+
future: _targetsFuture,
|
|
5500
|
+
builder: (context, snapshot) {
|
|
5501
|
+
final targets = snapshot.data ?? const <TaskDeliveryTarget>[];
|
|
5502
|
+
final platformIds = _platformIds(targets);
|
|
5503
|
+
return SingleChildScrollView(
|
|
5504
|
+
child: Column(
|
|
5505
|
+
mainAxisSize: MainAxisSize.min,
|
|
5506
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
5507
|
+
children: <Widget>[
|
|
5508
|
+
Row(
|
|
5509
|
+
children: <Widget>[
|
|
5510
|
+
Expanded(
|
|
5511
|
+
child: Text(
|
|
5512
|
+
'Result Delivery',
|
|
5513
|
+
style: TextStyle(
|
|
5514
|
+
fontSize: 18,
|
|
5515
|
+
fontWeight: FontWeight.w800,
|
|
5516
|
+
),
|
|
5517
|
+
),
|
|
5518
|
+
),
|
|
5519
|
+
TextButton.icon(
|
|
5520
|
+
onPressed: () => Navigator.of(context).pop(
|
|
5521
|
+
const TaskDeliveryTarget(
|
|
5522
|
+
platform: '',
|
|
5523
|
+
platformLabel: '',
|
|
5524
|
+
to: '',
|
|
5525
|
+
label: '',
|
|
5526
|
+
subtitle: '',
|
|
5527
|
+
source: 'default',
|
|
5528
|
+
connected: true,
|
|
5529
|
+
supportsDelivery: true,
|
|
5530
|
+
),
|
|
5531
|
+
),
|
|
5532
|
+
icon: const Icon(Icons.auto_mode_rounded),
|
|
5533
|
+
label: const Text('Use default'),
|
|
5534
|
+
),
|
|
5535
|
+
],
|
|
5536
|
+
),
|
|
5537
|
+
const SizedBox(height: 6),
|
|
5538
|
+
Text(
|
|
5539
|
+
'Search discovered messaging channels, contacts, groups, and recent conversations.',
|
|
5540
|
+
style: TextStyle(color: _textSecondary),
|
|
5541
|
+
),
|
|
5542
|
+
const SizedBox(height: 14),
|
|
5543
|
+
TextField(
|
|
5544
|
+
controller: _queryController,
|
|
5545
|
+
onChanged: (_) => _scheduleSearch(),
|
|
5546
|
+
decoration: const InputDecoration(
|
|
5547
|
+
prefixIcon: Icon(Icons.search_rounded),
|
|
5548
|
+
labelText: 'Search channels',
|
|
5549
|
+
),
|
|
5550
|
+
),
|
|
5551
|
+
const SizedBox(height: 12),
|
|
5552
|
+
SingleChildScrollView(
|
|
5553
|
+
scrollDirection: Axis.horizontal,
|
|
5554
|
+
child: Row(
|
|
5555
|
+
children: <Widget>[
|
|
5556
|
+
Padding(
|
|
5557
|
+
padding: const EdgeInsets.only(right: 8),
|
|
5558
|
+
child: ChoiceChip(
|
|
5559
|
+
label: const Text('All'),
|
|
5560
|
+
selected: _platformFilter == null,
|
|
5561
|
+
onSelected: (_) {
|
|
5562
|
+
_platformFilter = null;
|
|
5563
|
+
_loadTargets();
|
|
5564
|
+
},
|
|
5565
|
+
),
|
|
5566
|
+
),
|
|
5567
|
+
...platformIds.map((platform) {
|
|
5568
|
+
return Padding(
|
|
5569
|
+
padding: const EdgeInsets.only(right: 8),
|
|
5570
|
+
child: ChoiceChip(
|
|
5571
|
+
avatar: Icon(
|
|
5572
|
+
_taskDeliveryPlatformIcon(platform),
|
|
5573
|
+
size: 16,
|
|
5574
|
+
),
|
|
5575
|
+
label: Text(_taskDeliveryPlatformLabel(platform)),
|
|
5576
|
+
selected: _platformFilter == platform,
|
|
5577
|
+
onSelected: (_) {
|
|
5578
|
+
_platformFilter = platform;
|
|
5579
|
+
_loadTargets();
|
|
5580
|
+
},
|
|
5581
|
+
),
|
|
5582
|
+
);
|
|
5583
|
+
}),
|
|
5584
|
+
],
|
|
5585
|
+
),
|
|
5586
|
+
),
|
|
5587
|
+
const SizedBox(height: 16),
|
|
5588
|
+
if (snapshot.connectionState == ConnectionState.waiting)
|
|
5589
|
+
const Center(
|
|
5590
|
+
child: Padding(
|
|
5591
|
+
padding: EdgeInsets.all(24),
|
|
5592
|
+
child: CircularProgressIndicator(),
|
|
5593
|
+
),
|
|
5594
|
+
)
|
|
5595
|
+
else if (snapshot.hasError)
|
|
5596
|
+
_TaskDeliveryNotice(
|
|
5597
|
+
icon: Icons.warning_amber_rounded,
|
|
5598
|
+
title: 'Discovery failed',
|
|
5599
|
+
detail: snapshot.error.toString(),
|
|
5600
|
+
)
|
|
5601
|
+
else if (targets.isEmpty)
|
|
5602
|
+
_TaskDeliveryNotice(
|
|
5603
|
+
icon: Icons.search_off_rounded,
|
|
5604
|
+
title: 'No channels found',
|
|
5605
|
+
detail:
|
|
5606
|
+
'Try another search, connect a messaging platform, or enter a destination ID manually.',
|
|
5607
|
+
)
|
|
5608
|
+
else
|
|
5609
|
+
...targets.take(40).map(_buildTargetTile),
|
|
5610
|
+
const Divider(height: 28),
|
|
5611
|
+
Text(
|
|
5612
|
+
'Manual destination',
|
|
5613
|
+
style: TextStyle(fontWeight: FontWeight.w700),
|
|
5614
|
+
),
|
|
5615
|
+
const SizedBox(height: 8),
|
|
5616
|
+
Row(
|
|
5617
|
+
children: <Widget>[
|
|
5618
|
+
Expanded(
|
|
5619
|
+
flex: 2,
|
|
5620
|
+
child: DropdownButtonFormField<String>(
|
|
5621
|
+
initialValue: _manualPlatform,
|
|
5622
|
+
decoration: const InputDecoration(
|
|
5623
|
+
labelText: 'Platform',
|
|
5624
|
+
),
|
|
5625
|
+
items: platformIds
|
|
5626
|
+
.map(
|
|
5627
|
+
(platform) => DropdownMenuItem<String>(
|
|
5628
|
+
value: platform,
|
|
5629
|
+
child: Text(
|
|
5630
|
+
_taskDeliveryPlatformLabel(platform),
|
|
5631
|
+
),
|
|
5632
|
+
),
|
|
5633
|
+
)
|
|
5634
|
+
.toList(growable: false),
|
|
5635
|
+
onChanged: (value) =>
|
|
5636
|
+
setState(() => _manualPlatform = value),
|
|
5637
|
+
),
|
|
5638
|
+
),
|
|
5639
|
+
const SizedBox(width: 12),
|
|
5640
|
+
Expanded(
|
|
5641
|
+
flex: 3,
|
|
5642
|
+
child: TextField(
|
|
5643
|
+
controller: _manualController,
|
|
5644
|
+
decoration: const InputDecoration(
|
|
5645
|
+
labelText: 'Destination ID',
|
|
5646
|
+
),
|
|
5647
|
+
onSubmitted: (_) => _submitManual(),
|
|
5648
|
+
),
|
|
5649
|
+
),
|
|
5650
|
+
const SizedBox(width: 8),
|
|
5651
|
+
IconButton(
|
|
5652
|
+
tooltip: 'Use manual destination',
|
|
5653
|
+
onPressed: _submitManual,
|
|
5654
|
+
icon: const Icon(Icons.check_rounded),
|
|
5655
|
+
),
|
|
5656
|
+
],
|
|
5657
|
+
),
|
|
5658
|
+
],
|
|
5659
|
+
),
|
|
5660
|
+
);
|
|
5661
|
+
},
|
|
5662
|
+
),
|
|
5663
|
+
),
|
|
5664
|
+
);
|
|
5665
|
+
}
|
|
5666
|
+
}
|
|
5667
|
+
|
|
5668
|
+
class _TaskDeliveryNotice extends StatelessWidget {
|
|
5669
|
+
const _TaskDeliveryNotice({
|
|
5670
|
+
required this.icon,
|
|
5671
|
+
required this.title,
|
|
5672
|
+
required this.detail,
|
|
5673
|
+
});
|
|
5674
|
+
|
|
5675
|
+
final IconData icon;
|
|
5676
|
+
final String title;
|
|
5677
|
+
final String detail;
|
|
5678
|
+
|
|
5679
|
+
@override
|
|
5680
|
+
Widget build(BuildContext context) {
|
|
5681
|
+
return Container(
|
|
5682
|
+
width: double.infinity,
|
|
5683
|
+
padding: const EdgeInsets.all(14),
|
|
5684
|
+
decoration: BoxDecoration(
|
|
5685
|
+
border: Border.all(color: _border),
|
|
5686
|
+
borderRadius: BorderRadius.circular(12),
|
|
5687
|
+
),
|
|
5688
|
+
child: Row(
|
|
5689
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
5690
|
+
children: <Widget>[
|
|
5691
|
+
Icon(icon, color: _textSecondary),
|
|
5692
|
+
const SizedBox(width: 12),
|
|
5693
|
+
Expanded(
|
|
5694
|
+
child: Column(
|
|
5695
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
5696
|
+
children: <Widget>[
|
|
5697
|
+
Text(
|
|
5698
|
+
title,
|
|
5699
|
+
style: const TextStyle(fontWeight: FontWeight.w700),
|
|
5700
|
+
),
|
|
5701
|
+
const SizedBox(height: 4),
|
|
5702
|
+
Text(detail, style: TextStyle(color: _textSecondary)),
|
|
5703
|
+
],
|
|
5704
|
+
),
|
|
5705
|
+
),
|
|
5706
|
+
],
|
|
5707
|
+
),
|
|
5708
|
+
);
|
|
5709
|
+
}
|
|
5710
|
+
}
|
|
5711
|
+
|
|
5292
5712
|
class TasksPanel extends StatefulWidget {
|
|
5293
5713
|
const TasksPanel({super.key, required this.controller});
|
|
5294
5714
|
|
|
@@ -5871,6 +6291,9 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
5871
6291
|
? task!.triggerConfig['connectionId'] as int
|
|
5872
6292
|
: int.tryParse(task?.triggerConfig['connectionId']?.toString() ?? ''),
|
|
5873
6293
|
);
|
|
6294
|
+
final selectedDeliveryTarget = ValueNotifier<TaskDeliveryTarget?>(
|
|
6295
|
+
_taskDeliveryTargetFromTask(task),
|
|
6296
|
+
);
|
|
5874
6297
|
final queryController = TextEditingController(
|
|
5875
6298
|
text:
|
|
5876
6299
|
task?.triggerConfig['query']?.toString() ??
|
|
@@ -6203,6 +6626,101 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
6203
6626
|
decoration: const InputDecoration(labelText: 'Prompt'),
|
|
6204
6627
|
),
|
|
6205
6628
|
const SizedBox(height: 12),
|
|
6629
|
+
ValueListenableBuilder<TaskDeliveryTarget?>(
|
|
6630
|
+
valueListenable: selectedDeliveryTarget,
|
|
6631
|
+
builder: (context, deliveryTarget, _) {
|
|
6632
|
+
final color = deliveryTarget == null
|
|
6633
|
+
? _textSecondary
|
|
6634
|
+
: _taskDeliveryPlatformColor(
|
|
6635
|
+
deliveryTarget.platform,
|
|
6636
|
+
);
|
|
6637
|
+
return InkWell(
|
|
6638
|
+
borderRadius: BorderRadius.circular(14),
|
|
6639
|
+
onTap: () async {
|
|
6640
|
+
final picked = await _pickTaskDeliveryTarget(
|
|
6641
|
+
context,
|
|
6642
|
+
controller: controller,
|
|
6643
|
+
agentId: selectedAgentId,
|
|
6644
|
+
selected: deliveryTarget,
|
|
6645
|
+
);
|
|
6646
|
+
if (picked == null) return;
|
|
6647
|
+
selectedDeliveryTarget.value =
|
|
6648
|
+
picked.platform.isEmpty ? null : picked;
|
|
6649
|
+
},
|
|
6650
|
+
child: InputDecorator(
|
|
6651
|
+
decoration: const InputDecoration(
|
|
6652
|
+
labelText: 'Result Delivery',
|
|
6653
|
+
),
|
|
6654
|
+
child: Row(
|
|
6655
|
+
children: <Widget>[
|
|
6656
|
+
Container(
|
|
6657
|
+
width: 40,
|
|
6658
|
+
height: 40,
|
|
6659
|
+
decoration: BoxDecoration(
|
|
6660
|
+
color: color.withValues(alpha: 0.12),
|
|
6661
|
+
borderRadius: BorderRadius.circular(12),
|
|
6662
|
+
),
|
|
6663
|
+
child: Icon(
|
|
6664
|
+
deliveryTarget == null
|
|
6665
|
+
? Icons.auto_mode_rounded
|
|
6666
|
+
: _taskDeliveryPlatformIcon(
|
|
6667
|
+
deliveryTarget.platform,
|
|
6668
|
+
),
|
|
6669
|
+
color: color,
|
|
6670
|
+
),
|
|
6671
|
+
),
|
|
6672
|
+
const SizedBox(width: 12),
|
|
6673
|
+
Expanded(
|
|
6674
|
+
child: Column(
|
|
6675
|
+
crossAxisAlignment:
|
|
6676
|
+
CrossAxisAlignment.start,
|
|
6677
|
+
mainAxisSize: MainAxisSize.min,
|
|
6678
|
+
children: <Widget>[
|
|
6679
|
+
Text(
|
|
6680
|
+
deliveryTarget == null
|
|
6681
|
+
? 'Use default channel'
|
|
6682
|
+
: deliveryTarget.label,
|
|
6683
|
+
style: const TextStyle(
|
|
6684
|
+
fontWeight: FontWeight.w700,
|
|
6685
|
+
),
|
|
6686
|
+
),
|
|
6687
|
+
const SizedBox(height: 4),
|
|
6688
|
+
Text(
|
|
6689
|
+
deliveryTarget == null
|
|
6690
|
+
? 'AI-created and unspecified tasks use the current default.'
|
|
6691
|
+
: '${deliveryTarget.platformLabel} · ${deliveryTarget.to}',
|
|
6692
|
+
maxLines: 2,
|
|
6693
|
+
overflow: TextOverflow.ellipsis,
|
|
6694
|
+
style: TextStyle(
|
|
6695
|
+
color: _textSecondary,
|
|
6696
|
+
fontSize: 12.5,
|
|
6697
|
+
),
|
|
6698
|
+
),
|
|
6699
|
+
],
|
|
6700
|
+
),
|
|
6701
|
+
),
|
|
6702
|
+
if (deliveryTarget != null)
|
|
6703
|
+
IconButton(
|
|
6704
|
+
tooltip: 'Use default channel',
|
|
6705
|
+
onPressed: () =>
|
|
6706
|
+
selectedDeliveryTarget.value = null,
|
|
6707
|
+
icon: const Icon(
|
|
6708
|
+
Icons.close_rounded,
|
|
6709
|
+
size: 18,
|
|
6710
|
+
),
|
|
6711
|
+
)
|
|
6712
|
+
else
|
|
6713
|
+
Icon(
|
|
6714
|
+
Icons.unfold_more_rounded,
|
|
6715
|
+
color: _textSecondary,
|
|
6716
|
+
),
|
|
6717
|
+
],
|
|
6718
|
+
),
|
|
6719
|
+
),
|
|
6720
|
+
);
|
|
6721
|
+
},
|
|
6722
|
+
),
|
|
6723
|
+
const SizedBox(height: 12),
|
|
6206
6724
|
DropdownButtonFormField<String>(
|
|
6207
6725
|
initialValue: selectedModel,
|
|
6208
6726
|
decoration: const InputDecoration(
|
|
@@ -6322,8 +6840,10 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
6322
6840
|
),
|
|
6323
6841
|
)
|
|
6324
6842
|
.toList(),
|
|
6325
|
-
onChanged: (value) =>
|
|
6326
|
-
|
|
6843
|
+
onChanged: (value) => setLocalState(() {
|
|
6844
|
+
selectedAgentId = value;
|
|
6845
|
+
selectedDeliveryTarget.value = null;
|
|
6846
|
+
}),
|
|
6327
6847
|
),
|
|
6328
6848
|
],
|
|
6329
6849
|
const SizedBox(height: 12),
|
|
@@ -6459,6 +6979,14 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
6459
6979
|
'maxTokensPerDay': maxTokens,
|
|
6460
6980
|
},
|
|
6461
6981
|
};
|
|
6982
|
+
final deliveryTarget = selectedDeliveryTarget.value;
|
|
6983
|
+
if (deliveryTarget == null) {
|
|
6984
|
+
taskConfig.remove('notifyPlatform');
|
|
6985
|
+
taskConfig.remove('notifyTo');
|
|
6986
|
+
} else {
|
|
6987
|
+
taskConfig['notifyPlatform'] = deliveryTarget.platform;
|
|
6988
|
+
taskConfig['notifyTo'] = deliveryTarget.to;
|
|
6989
|
+
}
|
|
6462
6990
|
await controller.saveTask(
|
|
6463
6991
|
id: task?.id,
|
|
6464
6992
|
name: nameController.text.trim(),
|
|
@@ -1660,6 +1660,26 @@ class BackendClient {
|
|
|
1660
1660
|
return getList(baseUrl, _withAgentQuery('/api/tasks', agentId));
|
|
1661
1661
|
}
|
|
1662
1662
|
|
|
1663
|
+
Future<List<Map<String, dynamic>>> fetchTaskDeliveryTargets(
|
|
1664
|
+
String baseUrl, {
|
|
1665
|
+
String? query,
|
|
1666
|
+
String? platform,
|
|
1667
|
+
String? agentId,
|
|
1668
|
+
}) async {
|
|
1669
|
+
final params = <String>[
|
|
1670
|
+
if (query != null && query.trim().isNotEmpty)
|
|
1671
|
+
'q=${Uri.encodeQueryComponent(query.trim())}',
|
|
1672
|
+
if (platform != null && platform.trim().isNotEmpty)
|
|
1673
|
+
'platform=${Uri.encodeQueryComponent(platform.trim())}',
|
|
1674
|
+
if (agentId != null && agentId.trim().isNotEmpty)
|
|
1675
|
+
'agentId=${Uri.encodeQueryComponent(agentId.trim())}',
|
|
1676
|
+
];
|
|
1677
|
+
final path = params.isEmpty
|
|
1678
|
+
? _withAgentQuery('/api/tasks/delivery-targets', agentId)
|
|
1679
|
+
: '/api/tasks/delivery-targets?${params.join('&')}';
|
|
1680
|
+
return getList(baseUrl, path);
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1663
1683
|
Future<Map<String, dynamic>> saveTask(
|
|
1664
1684
|
String baseUrl, {
|
|
1665
1685
|
int? id,
|