feat: enforce iOS AI safety policy and citations

This commit is contained in:
MingNian
2026-07-29 13:29:51 +08:00
parent b4bc15b9b9
commit 70c1b3eae0
16 changed files with 513 additions and 67 deletions

View File

@@ -26,10 +26,6 @@
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>需要使用蓝牙连接支持的血压计等健康设备,以同步用户主动测量的健康数据</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>需要使用蓝牙连接支持的血压计等健康设备</string>
<key>NSCameraUsageDescription</key>
<string>需要使用相机拍摄饮食照片和体检报告,以便 AI 分析和记录</string>
<key>NSMicrophoneUsageDescription</key>

View File

@@ -9,9 +9,22 @@ const String baseUrl = String.fromEnvironment(
'API_BASE_URL',
defaultValue: kReleaseMode
? 'https://erpapi.datalumina.cn/xiaomai'
: 'http://10.4.225.209:5000',
: 'http://10.4.232.91:5000',
);
const String _configuredAppPolicy = String.fromEnvironment(
'APP_POLICY_PROFILE',
defaultValue: '',
);
String get appPolicyProfile {
if (_configuredAppPolicy == 'android-standard' ||
_configuredAppPolicy == 'ios-medical-citations') {
return _configuredAppPolicy;
}
return Platform.isIOS ? 'ios-medical-citations' : 'android-standard';
}
class ApiException implements Exception {
final int? code;
final String message;
@@ -56,7 +69,10 @@ class ApiClient {
baseUrl: baseUrl,
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 60),
headers: {'Content-Type': 'application/json'},
headers: {
'Content-Type': 'application/json',
'X-App-Policy': appPolicyProfile,
},
),
) {
_dio.interceptors.add(_AuthInterceptor(this));

View File

@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'navigation_provider.dart';
@@ -120,9 +122,9 @@ Widget buildPage(RouteInfo route, WidgetRef ref) {
case 'doctorFollowUpEdit':
return DoctorFollowUpEditPage(id: params['id']);
case 'devices':
return const DeviceManagementPage();
return Platform.isIOS ? const HomePage() : const DeviceManagementPage();
case 'deviceScan':
return const DeviceScanPage();
return Platform.isIOS ? const HomePage() : const DeviceScanPage();
case 'healthArchive':
return const HealthArchivePage();
case 'followups':

View File

@@ -1296,7 +1296,7 @@ class ChatMessagesView extends ConsumerWidget {
pushRoute(ref, 'reports');
break;
case 'device':
pushRoute(ref, 'devices');
if (!Platform.isIOS) pushRoute(ref, 'devices');
break;
default:
if (uri.host.isNotEmpty) pushRoute(ref, uri.host);
@@ -2199,12 +2199,13 @@ final _agentActions = <ActiveAgent, List<_AgentAction>>{
isWide: true,
route: 'trend',
),
_AgentAction(
label: '蓝牙录入',
icon: Icons.bluetooth,
isWide: true,
route: 'devices',
),
if (!Platform.isIOS)
_AgentAction(
label: '蓝牙录入',
icon: Icons.bluetooth,
isWide: true,
route: 'devices',
),
],
ActiveAgent.diet: [
_AgentAction(

View File

@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
@@ -49,11 +51,12 @@ class SettingsPage extends ConsumerWidget {
title: '长辈模式',
onTap: () => pushRoute(ref, 'elderMode'),
),
_SettingsTile(
icon: LucideIcons.bluetooth,
title: '蓝牙设备',
onTap: () => pushRoute(ref, 'devices'),
),
if (!Platform.isIOS)
_SettingsTile(
icon: LucideIcons.bluetooth,
title: '蓝牙设备',
onTap: () => pushRoute(ref, 'devices'),
),
_SettingsTile(
icon: LucideIcons.bell,
title: '消息通知',

View File

@@ -63,7 +63,10 @@ class SseHandler {
cancelToken: cancelToken,
options: Options(
responseType: ResponseType.stream,
headers: {'Authorization': 'Bearer $token'},
headers: {
'Authorization': 'Bearer $token',
'X-App-Policy': appPolicyProfile,
},
),
);

View File

@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
@@ -431,12 +433,13 @@ class _NavigationSection extends StatelessWidget {
route: 'exercisePlan',
colors: AppColors.exerciseGradient.colors,
),
_NavItem(
icon: LucideIcons.bluetooth,
title: '设备',
route: 'devices',
colors: AppColors.deviceGradient.colors,
),
if (!Platform.isIOS)
_NavItem(
icon: LucideIcons.bluetooth,
title: '设备',
route: 'devices',
colors: AppColors.deviceGradient.colors,
),
];
return _LightSection(