feat: AI 意图路由/草稿存储 + Prompts 模块化 + UI 视觉统一 + AI 同意门 + 资源更新
后端: - 新增 ai_intent_router 意图路由 - 新增 AiEntryDraft 草稿存储 + EF 迁移 - 新增 Prompts 模块化(global/modules/rag/router) - AI Agent handlers 扩展 前端: - 新增 AI 同意门 (ai_consent_gate/provider/details_page) - HealthMetricVisuals 视觉统一 - 设备扫描/管理页面优化 - agent 插图替换 + 趋势指标图标 配置: - DeepSeek 模型改 deepseek-v4-flash - .gitignore 加 artifacts/audit - build.gradle.kts 签名配置调整
This commit is contained in:
@@ -162,10 +162,13 @@ class _DeviceManagementPageState extends ConsumerState<DeviceManagementPage>
|
||||
Future<bool> _ensureBlePermissions() async {
|
||||
Map<Permission, PermissionStatus> statuses;
|
||||
try {
|
||||
statuses = await [
|
||||
Permission.bluetoothScan,
|
||||
Permission.bluetoothConnect,
|
||||
].request();
|
||||
final permissions = <Permission>[
|
||||
if (Platform.isAndroid) Permission.bluetoothScan,
|
||||
if (Platform.isAndroid) Permission.bluetoothConnect,
|
||||
// Android 11 and below require location permission for BLE scans.
|
||||
if (Platform.isAndroid) Permission.locationWhenInUse,
|
||||
];
|
||||
statuses = await permissions.request();
|
||||
} catch (_) {
|
||||
if (mounted) {
|
||||
setState(() => _pageMessage = '无法读取蓝牙权限状态');
|
||||
|
||||
@@ -27,14 +27,10 @@ class DeviceScanPage extends ConsumerStatefulWidget {
|
||||
|
||||
class _DeviceScanPageState extends ConsumerState<DeviceScanPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
static const _visibleDeviceWindow = Duration(seconds: 12);
|
||||
|
||||
final _results = <ScanResult>[];
|
||||
StreamSubscription<List<ScanResult>>? _scanSub;
|
||||
Timer? _scanStopTimer;
|
||||
Timer? _resultPruneTimer;
|
||||
String? _connectingId;
|
||||
DateTime? _scanStartedAt;
|
||||
bool _scanning = false;
|
||||
|
||||
late final AnimationController _pulseCtrl;
|
||||
@@ -59,7 +55,6 @@ class _DeviceScanPageState extends ConsumerState<DeviceScanPage>
|
||||
void dispose() {
|
||||
_pulseCtrl.dispose();
|
||||
_scanStopTimer?.cancel();
|
||||
_resultPruneTimer?.cancel();
|
||||
_scanSub?.cancel();
|
||||
unawaited(FlutterBluePlus.stopScan());
|
||||
unawaited(ref.read(healthBleServiceProvider).disconnect());
|
||||
@@ -99,7 +94,6 @@ class _DeviceScanPageState extends ConsumerState<DeviceScanPage>
|
||||
try {
|
||||
await FlutterBluePlus.stopScan();
|
||||
} catch (_) {}
|
||||
_scanStartedAt = DateTime.now();
|
||||
await _scanSub?.cancel();
|
||||
_scanSub = FlutterBluePlus.onScanResults.listen(_onScanResults);
|
||||
await FlutterBluePlus.startScan(
|
||||
@@ -112,38 +106,24 @@ class _DeviceScanPageState extends ConsumerState<DeviceScanPage>
|
||||
_scanStopTimer = Timer(const Duration(seconds: 30), () {
|
||||
if (mounted) setState(() => _scanning = false);
|
||||
});
|
||||
_resultPruneTimer?.cancel();
|
||||
_resultPruneTimer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||
if (!mounted || _connectingId != null) return;
|
||||
final pruned = _visibleResults(_results);
|
||||
if (_sameResults(_results, pruned)) return;
|
||||
setState(() {
|
||||
_results
|
||||
..clear()
|
||||
..addAll(pruned);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _onScanResults(List<ScanResult> list) {
|
||||
if (!mounted || _connectingId != null) return;
|
||||
final scanStartedAt = _scanStartedAt;
|
||||
if (scanStartedAt == null) return;
|
||||
final boundDevices = ref.read(omronDeviceProvider).devices;
|
||||
final supported = list.where((result) {
|
||||
if (result.timeStamp.isBefore(scanStartedAt)) return false;
|
||||
if (!result.advertisementData.connectable) return false;
|
||||
if (DateTime.now().difference(result.timeStamp) > _visibleDeviceWindow) {
|
||||
return false;
|
||||
}
|
||||
if (!HealthBleService.isSupportedHealthDevice(result)) return false;
|
||||
final candidates = list.where((result) {
|
||||
// Keep the result in the list first. Many health devices expose their
|
||||
// standard service only after connection, not in the advertisement.
|
||||
// Also avoid relying on ScanResult.timeStamp/connectable here: both can
|
||||
// be stale or conservative while a device is in communication mode.
|
||||
if (!_looksLikeHealthDevice(result)) return false;
|
||||
return boundDevices.every(
|
||||
(device) => device.id != result.device.remoteId.toString(),
|
||||
);
|
||||
});
|
||||
|
||||
final next = [..._results];
|
||||
for (final result in supported) {
|
||||
for (final result in candidates) {
|
||||
final index = next.indexWhere(
|
||||
(item) => item.device.remoteId == result.device.remoteId,
|
||||
);
|
||||
@@ -163,33 +143,32 @@ class _DeviceScanPageState extends ConsumerState<DeviceScanPage>
|
||||
}
|
||||
|
||||
List<ScanResult> _visibleResults(List<ScanResult> results) {
|
||||
final visible =
|
||||
results
|
||||
.where(
|
||||
(result) =>
|
||||
DateTime.now().difference(result.timeStamp) <=
|
||||
_visibleDeviceWindow,
|
||||
)
|
||||
.toList()
|
||||
..sort(
|
||||
(a, b) => a.device.remoteId.toString().compareTo(
|
||||
b.device.remoteId.toString(),
|
||||
),
|
||||
);
|
||||
final visible = [...results]
|
||||
..sort(
|
||||
(a, b) => a.device.remoteId.toString().compareTo(
|
||||
b.device.remoteId.toString(),
|
||||
),
|
||||
);
|
||||
return visible;
|
||||
}
|
||||
|
||||
bool _looksLikeHealthDevice(ScanResult result) {
|
||||
if (HealthBleService.deviceTypeFromScan(result) != null) return true;
|
||||
final name = [
|
||||
result.advertisementData.advName,
|
||||
result.device.platformName,
|
||||
].join(' ').toUpperCase();
|
||||
return name.contains('OMRON') ||
|
||||
name.contains('HEM') ||
|
||||
name.contains('BLESMART') ||
|
||||
name.contains('J735') ||
|
||||
name.contains('BPM') ||
|
||||
name.contains('BP');
|
||||
}
|
||||
|
||||
Future<void> _connectBindAndSync(ScanResult result) async {
|
||||
final remoteId = result.device.remoteId.toString();
|
||||
if (_connectingId != null) return;
|
||||
final scanStartedAt = _scanStartedAt;
|
||||
if (scanStartedAt == null ||
|
||||
result.timeStamp.isBefore(scanStartedAt) ||
|
||||
!result.advertisementData.connectable ||
|
||||
DateTime.now().difference(result.timeStamp) > _visibleDeviceWindow) {
|
||||
AppToast.show(context, '设备已离线,请重新进入通信状态', type: AppToastType.warning);
|
||||
return;
|
||||
}
|
||||
if (ref.read(omronDeviceProvider).isBound &&
|
||||
ref.read(omronDeviceProvider).findById(remoteId) != null) {
|
||||
AppToast.show(context, '该设备已绑定', type: AppToastType.info);
|
||||
@@ -348,10 +327,15 @@ class _DeviceScanPageState extends ConsumerState<DeviceScanPage>
|
||||
}
|
||||
|
||||
Future<bool> _ensureBlePermissions() async {
|
||||
final statuses = await [
|
||||
Permission.bluetoothScan,
|
||||
Permission.bluetoothConnect,
|
||||
].request();
|
||||
final permissions = <Permission>[
|
||||
if (Platform.isAndroid) Permission.bluetoothScan,
|
||||
if (Platform.isAndroid) Permission.bluetoothConnect,
|
||||
// Android 11 and below require a location runtime permission for BLE
|
||||
// scanning. Requesting it here also keeps older devices from silently
|
||||
// returning an empty scan stream.
|
||||
if (Platform.isAndroid) Permission.locationWhenInUse,
|
||||
];
|
||||
final statuses = await permissions.request();
|
||||
final granted = statuses.values.every((status) => status.isGranted);
|
||||
if (granted) return true;
|
||||
if (!mounted) return false;
|
||||
|
||||
Reference in New Issue
Block a user