Browse Source

上传海外版本导航和搜索服务

weicu
liwei1dao 1 year ago
parent
commit
c23db2db1a
  1. 3
      ios/Runner/Info.plist
  2. 50
      lib/modules/agent/controllers/agent_controller.dart
  3. 447
      lib/modules/agent/views/google_navigation_card.dart
  4. 29
      lib/modules/agent/views/message_bubble.dart
  5. 18
      lib/modules/agent/views/navigation_card.dart
  6. 4
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  7. 27
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift
  8. 2
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift
  9. 22
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift

3
ios/Runner/Info.plist

@ -88,6 +88,9 @@
<string>fb-messenger-share-api</string>
<string>fbauth2</string>
<string>fbshareextension</string>
<string>google.navigation</string>
<string>googlemaps</string>
<string>comgooglemaps</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>

50
lib/modules/agent/controllers/agent_controller.dart

@ -3,6 +3,8 @@ import 'dart:convert';
import 'dart:ffi';
import 'dart:io';
import 'package:amap_service/amap_service.dart';
import 'package:http/http.dart' as http;
import 'package:url_launcher/url_launcher.dart';
import 'package:voitrans/data/models/song_model.dart';
import 'package:voitrans/data/services/ble_manager.dart';
import 'package:voitrans/data/services/bluetooth_manager.dart';
@ -800,7 +802,8 @@ class AgentController extends GetxController {
["card_navigation"]["start"] as String;
final end = metaResult["maps_route_navigation"]
["card_navigation"]["end"] as String;
final items = metaResult['items'];
final items = metaResult["maps_route_navigation"]
["card_navigation"]['items'];
if (items != null) {
Future.delayed(Duration(seconds: 2), () {
// Logger.i(TAG, 'liwei-------- 打开导航页面: $start $end ');
@ -808,6 +811,26 @@ class AgentController extends GetxController {
openNavigation(start, end, "");
});
}
} else if (metaResult
.containsKey("maps_google_route_navigation")) {
//导航
final maps_google_route_navigation_raw =
metaResult["maps_google_route_navigation"];
// 安全的类型检查
if (maps_google_route_navigation_raw is Map<String, dynamic>) {
final maps_google_route_navigation =
maps_google_route_navigation_raw;
final data = maps_google_route_navigation["data"];
if (data != null) {
final end = data["destination"] as String;
final mode = data["items"][0]["ntype"] as String;
Future.delayed(Duration(seconds: 1), () {
// Logger.i(TAG, 'liwei-------- 打开google地图: $end ');
openGoogleMap(end, mode);
});
}
}
}
}
}
@ -2146,4 +2169,29 @@ class AgentController extends GetxController {
print('播放控制失败: $e');
}
}
/// 打开Google地图进行导航
Future<void> openGoogleMap(String daddr, String mode) async {
try {
// 方案1: 尝试使用Google Maps应用的URL scheme
// final Uri googleMapsUri = Uri.parse(url);
// 方案1: Google Maps应用 URL scheme
final Uri appUri = Uri.parse(
'comgooglemaps://?saddr=CurrentLocation&daddr=$daddr&directionsmode=$mode');
if (await canLaunchUrl(appUri)) {
await launchUrl(appUri, mode: LaunchMode.externalApplication);
return;
} else {
final Uri webUrl = Uri.parse(
"https://www.google.com/maps/dir/?api=1&origin=Current+Location&destination=$daddr&travelmode=$mode");
if (await canLaunchUrl(webUrl)) {
await launchUrl(webUrl);
}
}
} catch (e) {
print('打开地图失败: $e');
Get.snackbar('错误', '打开地图时发生错误');
}
}
}

447
lib/modules/agent/views/google_navigation_card.dart

@ -0,0 +1,447 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../controllers/agent_controller.dart';
String formatDistance(String distanceStr) {
final distance = double.tryParse(distanceStr);
if (distance == null) return distanceStr; // 如果无法解析,则返回原始字符串
if (distance >= 1000) {
final km = distance / 1000;
return '${km.toStringAsFixed(1)} KM';
} else {
return '${distance.toStringAsFixed(0)} M';
}
}
String formatDuration(String secondsStr) {
int totalSeconds = int.tryParse(secondsStr) ?? 0;
Duration duration = Duration(seconds: totalSeconds);
int hours = duration.inHours;
int minutes = duration.inMinutes.remainder(60);
int seconds = duration.inSeconds.remainder(60);
List<String> parts = [];
if (hours > 0) parts.add('$hours${'hours'.tr}'); // 小时
if (minutes > 0) parts.add('$minutes${'minutes'.tr}'); // 分钟
//不需要秒
//if (seconds > 0 || parts.isEmpty) parts.add('$seconds${'seconds'.tr}'); // 秒
return parts.join('');
}
/// 导航卡片组件
///
/// 各元素含义分析:
/// - [start]: 起点坐标 (经纬度格式)
/// - [end]: 终点坐标 (经纬度格式)
/// - [destination]: 目的地名称显示
/// - [modes]: 导航方式列表,包含驾车、步行、公交等选项
/// - [navigation]: 当前导航状态信息(可选)
/// - [height]: 卡片高度
Widget googlenavigationCard(
BuildContext context, {
required String start,
required String end,
required String destination,
required List<Map<String, dynamic>> modes,
Map<String, dynamic>? navigation,
double? height, // 改为可选参数,允许自适应高度
}) {
int currentIndex = 0;
final controller = Get.find<AgentController>();
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Container(
height: height?.h ?? 350.h, // 如果没有指定高度,使用350作为默认高度
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.r),
color: isDarkMode ? const Color(0xFF1E1E1E) : Colors.white,
boxShadow: [
BoxShadow(
blurRadius: 12,
color: Colors.black.withValues(alpha: isDarkMode ? 0.4 : 0.08),
offset: const Offset(0, 2),
),
],
),
child: StatefulBuilder(
builder: (context, setInnerState) {
return Column(
children: [
// 顶部:路线预览图片区域
Expanded(
flex: 4, // 增加图片区域的flex值,让图片占更多空间
child: ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)),
child: Stack(
children: [
// 路线图片
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Image.network(
modes[currentIndex]["image"] as String,
key: ValueKey<String>(
modes[currentIndex]["ntype"] as String),
height: double.infinity,
width: double.infinity,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
color: isDarkMode
? Colors.grey[800]
: Colors.grey[200],
child: Icon(
Icons.map_outlined,
size: 48.sp,
color: isDarkMode
? Colors.grey[600]
: Colors.grey[400],
),
);
},
),
),
// 导航状态覆盖层
if (navigation != null)
Positioned(
top: 12.h,
left: 12.w,
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color:
Theme.of(context).primaryColor.withOpacity(0.9),
borderRadius: BorderRadius.circular(12.r),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.navigation,
color: Colors.white,
size: 14.sp,
),
SizedBox(width: 4.w),
Text(
'navigating'.tr, // 导航中
style: TextStyle(
color: Colors.white,
fontSize: 12.sp,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
],
),
),
),
// 中部:导航方式选择器
Container(
padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 16.w),
child: Row(
children: List.generate(modes.length, (index) {
final mode = modes[index];
final isSelected = index == currentIndex;
final isEnabled = mode['enable'] as bool;
return Expanded(
child: GestureDetector(
onTap: isEnabled
? () {
setInnerState(() => currentIndex = index);
}
: null,
child: Container(
margin: EdgeInsets.symmetric(horizontal: 4.w),
padding: EdgeInsets.symmetric(
vertical: 8.h, horizontal: 12.w),
decoration: BoxDecoration(
color: isSelected
? Theme.of(context).primaryColor.withOpacity(0.1)
: Colors.transparent,
borderRadius: BorderRadius.circular(8.r),
border: Border.all(
color: isSelected
? Theme.of(context).primaryColor
: (isDarkMode
? Colors.grey[700]!
: Colors.grey[300]!),
width: isSelected ? 2 : 1,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_getNavigationIcon(mode["ntype"] as String),
color: isEnabled
? (isSelected
? Theme.of(context).primaryColor
: (isDarkMode
? Colors.white70
: Colors.black87))
: Colors.grey,
size: 20.sp,
),
],
),
),
),
);
}),
),
),
// 底部:目的地信息和操作按钮
Container(
padding: EdgeInsets.fromLTRB(20.w, 8.h, 20.w, 16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 第一层:起点 → 目的地信息(独占整个宽度)
Row(
crossAxisAlignment: CrossAxisAlignment.center, // 确保垂直居中对齐
children: [
// 起点图标 - 统一图标容器宽度,确保对齐
SizedBox(
width: 16.sp, // 固定图标容器宽度
child: Icon(
Icons.my_location,
size: 15.sp, // 统一图标大小
color: Theme.of(context).primaryColor,
),
),
SizedBox(width: 6.w), // 统一间距
// 当前位置文本
Text(
'currentLocation'.tr, // 当前位置
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white70 : Colors.black54,
height: 1.3, // 设置行高,提升多行文本可读性
),
softWrap: true, // 启用软换行
overflow: TextOverflow.visible, // 确保文本完全显示
),
SizedBox(width: 8.w),
// 箭头图标
Icon(
Icons.arrow_forward,
size: 15.sp, // 统一图标大小
color: isDarkMode ? Colors.white54 : Colors.black45,
),
SizedBox(width: 8.w),
// 目的地图标
Icon(
Icons.location_on,
size: 15.sp, // 统一图标大小
color: Colors.red,
),
SizedBox(width: 6.w), // 统一间距
// 目的地名称 - 使用Expanded确保自动换行
Expanded(
child: Text(
destination,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black87,
height: 1.2, // 设置行高,提升多行文本可读性
),
softWrap: true, // 启用软换行
overflow: TextOverflow.visible, // 确保文本完全显示
),
),
],
),
SizedBox(height: 4.h), // 第一层和第二层之间的间距
// 第二层:距离时间信息(左侧)+ 导航按钮(右侧)
Row(
crossAxisAlignment: CrossAxisAlignment.end, // 底部对齐
children: [
// 左侧:距离和时间信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 距离信息
Row(
crossAxisAlignment:
CrossAxisAlignment.center, // 确保垂直居中对齐
children: [
// 距离图标 - 统一图标容器宽度,确保与第一层对齐
SizedBox(
width: 16.sp, // 与第一层图标容器宽度一致
child: Icon(
Icons.straighten,
size: 15.sp, // 统一图标大小
color: Theme.of(context).primaryColor,
),
),
SizedBox(width: 6.w), // 与第一层间距一致
Flexible(
child: Text(
formatDistance(
modes[currentIndex]["distance"]),
style: TextStyle(
fontSize: 14.sp,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.w500,
height: 1.3, // 设置行高,提升多行文本可读性
),
softWrap: true, // 启用软换行
overflow: TextOverflow.visible, // 确保文本完全显示
),
),
],
),
SizedBox(height: 4.h),
// 时间信息
Row(
crossAxisAlignment:
CrossAxisAlignment.center, // 确保垂直居中对齐
children: [
// 时间图标 - 统一图标容器宽度,确保与第一层对齐
SizedBox(
width: 16.sp, // 与第一层图标容器宽度一致
child: Icon(
Icons.access_time,
size: 15.sp, // 统一图标大小
color: Theme.of(context).primaryColor,
),
),
SizedBox(width: 6.w), // 与第一层间距一致
Flexible(
child: Text(
'${'approximate'.tr}${formatDuration(modes[currentIndex]["duration"])}', // 约
style: TextStyle(
fontSize: 14.sp,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.w500,
height: 1.3, // 设置行高,提升多行文本可读性
),
softWrap: true, // 启用软换行
overflow: TextOverflow.visible, // 确保文本完全显示
),
),
],
),
// 导航状态信息
if (navigation != null) ...[
SizedBox(height: 6.h),
Container(
padding: EdgeInsets.symmetric(
horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: Theme.of(context)
.primaryColor
.withOpacity(0.1),
borderRadius: BorderRadius.circular(6.r),
),
child: Text(
"${navigation["distanceText"]}${'enterRoad'.tr}${navigation["nextRoad"]}", // 进入
style: TextStyle(
fontSize: 12.sp,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.w500,
height: 1.3, // 设置行高,提升多行文本可读性
),
softWrap: true, // 启用软换行
overflow: TextOverflow.visible, // 确保文本完全显示
),
),
],
],
),
),
SizedBox(width: 16.w), // 左右内容之间的间距
// 右侧:开始导航按钮
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).primaryColor,
Theme.of(context).primaryColor.withOpacity(0.8),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Theme.of(context)
.primaryColor
.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(12.r),
onTap: () {
controller.openGoogleMap(
end, modes[currentIndex]["ntype"] as String);
},
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 16.w,
vertical: 12.h,
),
child: Icon(
Icons.navigation,
color: Colors.white,
size: 20.sp,
),
),
),
),
),
],
),
],
),
),
],
);
},
),
);
}
/// 根据导航类型获取对应图标
IconData _getNavigationIcon(String ntype) {
switch (ntype.toLowerCase()) {
case 'driving':
case '驾车':
return Icons.directions_car;
case 'walking':
case '步行':
return Icons.directions_walk;
case 'transit':
case 'tsransit': // 修正拼写错误
case '公交':
return Icons.directions_transit;
case 'bicycling': // 修正为正确的英文值
case 'cycling':
case '骑行':
return Icons.directions_bike;
default:
return Icons.navigation;
}
}

29
lib/modules/agent/views/message_bubble.dart

@ -1,4 +1,5 @@
import 'package:voitrans/core/utils/logger.dart';
import 'package:voitrans/modules/agent/views/google_navigation_card.dart';
import 'package:voitrans/modules/agent/views/music_card.dart';
import 'package:voitrans/modules/agent/views/qqmusic_card.dart';
@ -313,6 +314,34 @@ class MessageBubble extends GetView<AgentController> {
navigation: navigation);
}
}
} else if (meta!.keys.contains('maps_google_route_navigation')) {
final maps_google_route_navigation =
meta!['maps_google_route_navigation'] as Map<String, dynamic> ??
null;
if (maps_google_route_navigation != null) {
final data = maps_google_route_navigation['data'];
if (data != null) {
final origin = data['origin'] as String? ?? '';
final destination = data['destination'] as String? ?? '';
final end = data['end'] as String? ?? '';
final rawList = data['items'] as List? ?? null;
if (rawList != null) {
final list =
rawList.map((e) => Map<String, dynamic>.from(e)).toList();
final rawNavigation = data['navigation'];
final Map<String, dynamic>? navigation =
rawNavigation is Map<String, dynamic> ? rawNavigation : null;
// print("导航实时数据 ${rawNavigation}");
// 导航
return googlenavigationCard(context,
start: origin,
end: destination,
destination: end,
modes: list,
navigation: navigation);
}
}
}
}
// if (message.contains('播放列表') || message.contains('音乐')) {

18
lib/modules/agent/views/navigation_card.dart

@ -244,9 +244,7 @@ Widget navigationCard(
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: isDarkMode
? Colors.white70
: Colors.black54,
color: isDarkMode ? Colors.white70 : Colors.black54,
height: 1.3, // 设置行高,提升多行文本可读性
),
softWrap: true, // 启用软换行
@ -274,9 +272,7 @@ Widget navigationCard(
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode
? Colors.white
: Colors.black87,
color: isDarkMode ? Colors.white : Colors.black87,
height: 1.2, // 设置行高,提升多行文本可读性
),
softWrap: true, // 启用软换行
@ -299,7 +295,8 @@ Widget navigationCard(
children: [
// 距离信息
Row(
crossAxisAlignment: CrossAxisAlignment.center, // 确保垂直居中对齐
crossAxisAlignment:
CrossAxisAlignment.center, // 确保垂直居中对齐
children: [
// 距离图标 - 统一图标容器宽度,确保与第一层对齐
SizedBox(
@ -330,7 +327,8 @@ Widget navigationCard(
SizedBox(height: 4.h),
// 时间信息
Row(
crossAxisAlignment: CrossAxisAlignment.center, // 确保垂直居中对齐
crossAxisAlignment:
CrossAxisAlignment.center, // 确保垂直居中对齐
children: [
// 时间图标 - 统一图标容器宽度,确保与第一层对齐
SizedBox(
@ -402,7 +400,9 @@ Widget navigationCard(
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Theme.of(context).primaryColor.withOpacity(0.3),
color: Theme.of(context)
.primaryColor
.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),

4
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift

@ -566,8 +566,8 @@ audioStream.saveAudioDataTo(data: audioData)
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
info["current_time"] = dateFormatter.string(from: Date())
// 添加调试日志
print("系统提示词模板: \(systemPrompt)")
print("用户画像数据: \(info)")
// print("系统提示词模板: \(systemPrompt)")
// print("用户画像数据: \(info)")
let _systemPrompt = fillTemplate(systemPrompt, with: info)
print("替换后的系统提示词: \(_systemPrompt)")

27
local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift

@ -520,21 +520,20 @@ getSessionCallback(sessionId)?.onFunctionCall(sessionId, jsonFunctionCall)
}
if sessionId == self.currSessionId {
// 处理结果
if (!isError){
self.getSessionCallback(sessionId)?.onFunctionCallResult(
sessionId,
functionCall,
result
)
}else{
self.getSessionCallback(sessionId)?.onError(
sessionId,
ChatApiException(errorStr)
)
}
if (firstToolCall.name != "set_user_profile_field"){
// 处理结果
if (!isError){
self.getSessionCallback(sessionId)?.onFunctionCallResult(
sessionId,
functionCall,
result
)
}else{
self.getSessionCallback(sessionId)?.onError(
sessionId,
ChatApiException(errorStr)
)
}
// 将结果发送回OpenAI继续对话
await self.sendFunctionCallResultInternal(
sessionId: sessionId,

2
local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift

@ -98,6 +98,8 @@ public class MCPSubClient {
// 4. 获取工具列表
let (toolList, _) = try await self.mcpClient!.listTools()
print("[MCPSubClient] \(serverId):工具列表 \(toolList)")
// 5. 处理工具列表
processTools(toolList)

22
local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift

@ -713,14 +713,22 @@ private class SetReminderHandler: FunctionHandler {
// 设置闹钟级提醒(主方法)
func setAlarmReminder(title: String, content: String, time: String) async -> Result {
// 1. 请求日历权限
let calendarStatus = await EKEventStore.authorizationStatus(for: .reminder)
guard calendarStatus == .authorized else {
let message = calendarStatus == .denied
? "日历权限已拒绝,请前往「设置 → 隐私与安全 → 日历」开启"
: "日历权限不足"
return Result(success: false, message: message)
// 1. 请求权限(如果没有就会弹窗)
do {
let granted = try await self.eventStore!.requestAccess(to: .reminder) // or .event
guard granted else {
return Result(success: false, message: "提醒事项权限被拒绝")
}
} catch {
return Result(success: false, message: "请求提醒事项权限失败:\(error.localizedDescription)")
}
// 2. 再检查状态
let status = EKEventStore.authorizationStatus(for: .reminder)
guard status == .authorized else {
return Result(success: false, message: "提醒事项权限不足")
}
// 2. 请求关键通知权限(闹钟级提醒必需)
let notificationCenter = UNUserNotificationCenter.current()

Loading…
Cancel
Save