19 changed files with 1225 additions and 410 deletions
@ -1,18 +0,0 @@ |
|||||
curl 'https://ark.cn-beijing.volces.com/api/v3/bots/chat/completions' \ |
|
||||
-H "Authorization: Bearer 168deb3d-fd0c-4912-b9f1-aaee5c6743e6" \ |
|
||||
-H 'Content-Type: application/json' \ |
|
||||
-d '{ |
|
||||
"model": "bot-20250405211523-l7c9r", |
|
||||
"stream": true, |
|
||||
"stream_options": {"include_usage": true}, |
|
||||
"messages": [ |
|
||||
{ |
|
||||
"role": "system", |
|
||||
"content": "You are a helpful assistant." |
|
||||
}, |
|
||||
{ |
|
||||
"role": "user", |
|
||||
"content": "Hello!" |
|
||||
} |
|
||||
] |
|
||||
}' |
|
||||
@ -0,0 +1,225 @@ |
|||||
|
package com.yunqiinnovation.deepsound |
||||
|
|
||||
|
import android.content.Context |
||||
|
import android.content.Intent |
||||
|
import android.content.pm.PackageManager |
||||
|
import android.net.Uri |
||||
|
import android.provider.ContactsContract |
||||
|
import android.telephony.SmsManager |
||||
|
import com.yunqiinnovation.deepsound.core.utils.FileLogger |
||||
|
|
||||
|
/** |
||||
|
* 系统功能处理器 - 负责调用系统API执行各种功能 |
||||
|
*/ |
||||
|
class SystemFunctionHandler(private val context: Context) { |
||||
|
companion object { |
||||
|
private const val TAG = "SystemFunctionHandler" |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 启动翻译模式 |
||||
|
* @return 操作结果 |
||||
|
*/ |
||||
|
fun enterTranslationMode(): Result { |
||||
|
FileLogger.d(TAG, "进入翻译模式") |
||||
|
// 此处应添加实际的翻译模式实现 |
||||
|
// 可能需要启动一个专门的翻译服务或活动 |
||||
|
|
||||
|
// 广播通知APP的其他部分进入翻译模式 |
||||
|
// 使用VoiceInteractionService中定义的常量 |
||||
|
val intent = Intent(VoiceInteractionService.ACTION_ENTER_TRANSLATION_MODE).apply { |
||||
|
|
||||
|
putExtra("timestamp", System.currentTimeMillis()) |
||||
|
} |
||||
|
|
||||
|
// 发送广播 |
||||
|
context.sendBroadcast(intent) |
||||
|
FileLogger.d(TAG, "已发送广播: ${VoiceInteractionService.ACTION_ENTER_TRANSLATION_MODE}") |
||||
|
|
||||
|
return Result( |
||||
|
success = true, |
||||
|
message = "已进入翻译模式" |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 发送短信 |
||||
|
* @param contact 联系人(名称或电话号码) |
||||
|
* @param message 短信内容 |
||||
|
* @return 操作结果 |
||||
|
*/ |
||||
|
fun sendTextMessage(contact: String, message: String): Result { |
||||
|
FileLogger.d(TAG, "发送短信给 $contact: $message") |
||||
|
|
||||
|
try { |
||||
|
val phoneNumber = getPhoneNumberForContact(contact) |
||||
|
if (phoneNumber.isEmpty()) { |
||||
|
return Result( |
||||
|
success = false, |
||||
|
message = "未找到联系人信息: $contact" |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
// 使用SmsManager发送短信 |
||||
|
val smsManager = SmsManager.getDefault() |
||||
|
smsManager.sendTextMessage(phoneNumber, null, message, null, null) |
||||
|
|
||||
|
return Result( |
||||
|
success = true, |
||||
|
message = "已向${contact}发送短信: $message" |
||||
|
) |
||||
|
} catch (e: Exception) { |
||||
|
FileLogger.e(TAG, "发送短信失败: ${e.message}", e) |
||||
|
return Result( |
||||
|
success = false, |
||||
|
message = "发送短信失败: ${e.message}" |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 拨打电话 |
||||
|
* @param contact 联系人(名称或电话号码) |
||||
|
* @return 操作结果 |
||||
|
*/ |
||||
|
fun makePhoneCall(contact: String): Result { |
||||
|
FileLogger.d(TAG, "拨打电话给 $contact") |
||||
|
|
||||
|
try { |
||||
|
val phoneNumber = getPhoneNumberForContact(contact) |
||||
|
if (phoneNumber.isEmpty()) { |
||||
|
return Result( |
||||
|
success = false, |
||||
|
message = "未找到联系人信息: $contact" |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
// 创建拨号意图 |
||||
|
val intent = Intent(Intent.ACTION_CALL) |
||||
|
intent.data = Uri.parse("tel:$phoneNumber") |
||||
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK |
||||
|
context.startActivity(intent) |
||||
|
|
||||
|
return Result( |
||||
|
success = true, |
||||
|
message = "正在拨打${contact}的电话" |
||||
|
) |
||||
|
} catch (e: Exception) { |
||||
|
FileLogger.e(TAG, "拨打电话失败: ${e.message}", e) |
||||
|
return Result( |
||||
|
success = false, |
||||
|
message = "拨打电话失败: ${e.message}" |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 打开应用程序 |
||||
|
* @param appName 应用名称 |
||||
|
* @return 操作结果 |
||||
|
*/ |
||||
|
fun openApp(appName: String): Result { |
||||
|
FileLogger.d(TAG, "打开应用: $appName") |
||||
|
|
||||
|
try { |
||||
|
// 查找应用的包名 |
||||
|
val packageManager = context.packageManager |
||||
|
val intent = Intent(Intent.ACTION_MAIN, null) |
||||
|
intent.addCategory(Intent.CATEGORY_LAUNCHER) |
||||
|
|
||||
|
val apps = packageManager.queryIntentActivities(intent, 0) |
||||
|
|
||||
|
for (resolveInfo in apps) { |
||||
|
val appLabel = resolveInfo.loadLabel(packageManager).toString() |
||||
|
if (appLabel.equals(appName, ignoreCase = true) || |
||||
|
appLabel.contains(appName, ignoreCase = true)) { |
||||
|
|
||||
|
// 找到匹配的应用 |
||||
|
val packageName = resolveInfo.activityInfo.packageName |
||||
|
val launchIntent = packageManager.getLaunchIntentForPackage(packageName) |
||||
|
|
||||
|
if (launchIntent != null) { |
||||
|
launchIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK |
||||
|
context.startActivity(launchIntent) |
||||
|
|
||||
|
return Result( |
||||
|
success = true, |
||||
|
message = "正在打开${appName}应用" |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return Result( |
||||
|
success = false, |
||||
|
message = "未找到应用: $appName" |
||||
|
) |
||||
|
} catch (e: Exception) { |
||||
|
FileLogger.e(TAG, "打开应用失败: ${e.message}", e) |
||||
|
return Result( |
||||
|
success = false, |
||||
|
message = "打开应用失败: ${e.message}" |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 根据联系人名称获取电话号码 |
||||
|
* @param contact 联系人名称或电话号码 |
||||
|
* @return 电话号码 |
||||
|
*/ |
||||
|
private fun getPhoneNumberForContact(contact: String): String { |
||||
|
// 如果输入的是电话号码(简单判断,包含数字和特殊符号) |
||||
|
if (contact.matches(Regex("^[+\\d\\s\\-()]*\$")) && contact.any { it.isDigit() }) { |
||||
|
// 移除非数字字符 |
||||
|
return contact.replace(Regex("[^+\\d]"), "") |
||||
|
} |
||||
|
|
||||
|
// 从联系人数据库查询 |
||||
|
try { |
||||
|
val contentResolver = context.contentResolver |
||||
|
val uri = Uri.withAppendedPath( |
||||
|
ContactsContract.Contacts.CONTENT_FILTER_URI, |
||||
|
Uri.encode(contact) |
||||
|
) |
||||
|
|
||||
|
val projection = arrayOf( |
||||
|
ContactsContract.Contacts._ID, |
||||
|
ContactsContract.Contacts.DISPLAY_NAME, |
||||
|
ContactsContract.Contacts.HAS_PHONE_NUMBER |
||||
|
) |
||||
|
|
||||
|
contentResolver.query(uri, projection, null, null, null)?.use { cursor -> |
||||
|
if (cursor.moveToFirst()) { |
||||
|
val hasPhoneNumber = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.HAS_PHONE_NUMBER)) |
||||
|
|
||||
|
if (hasPhoneNumber > 0) { |
||||
|
val contactId = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts._ID)) |
||||
|
val phoneUri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI |
||||
|
val phoneProjection = arrayOf(ContactsContract.CommonDataKinds.Phone.NUMBER) |
||||
|
val phoneSelection = "${ContactsContract.CommonDataKinds.Phone.CONTACT_ID} = ?" |
||||
|
val phoneSelectionArgs = arrayOf(contactId) |
||||
|
|
||||
|
contentResolver.query(phoneUri, phoneProjection, phoneSelection, phoneSelectionArgs, null)?.use { phoneCursor -> |
||||
|
if (phoneCursor.moveToFirst()) { |
||||
|
return phoneCursor.getString(phoneCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER)) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} catch (e: Exception) { |
||||
|
FileLogger.e(TAG, "查询联系人信息失败: ${e.message}", e) |
||||
|
} |
||||
|
|
||||
|
return "" |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 操作结果数据类 |
||||
|
*/ |
||||
|
data class Result( |
||||
|
val success: Boolean, |
||||
|
val message: String |
||||
|
) |
||||
|
} |
||||
@ -0,0 +1,247 @@ |
|||||
|
import 'dart:async'; |
||||
|
import 'package:flutter_dotenv/flutter_dotenv.dart'; |
||||
|
import 'package:open_ai_service/open_ai_service.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
import 'ai_service.dart'; |
||||
|
|
||||
|
/// OpenAI服务适配器 - 连接AiService接口与OpenAIService插件 |
||||
|
class OpenAIServiceAdapter implements AiService { |
||||
|
final OpenAIService _openAIService = OpenAIService(); |
||||
|
StreamSubscription<OpenAIEvent>? _eventSubscription; |
||||
|
final StreamController<String> _tokenStreamController = StreamController<String>.broadcast(); |
||||
|
bool _isProcessingStream = false; |
||||
|
|
||||
|
/// 构造函数 |
||||
|
OpenAIServiceAdapter() { |
||||
|
printInfo(info: '创建OpenAIServiceAdapter实例'); |
||||
|
_setupEventListener(); |
||||
|
} |
||||
|
|
||||
|
/// 设置事件监听器 |
||||
|
void _setupEventListener() { |
||||
|
try { |
||||
|
// 首先确保访问eventStream以初始化底层事件通道 |
||||
|
_openAIService.eventStream; |
||||
|
|
||||
|
// 设置事件处理 |
||||
|
_eventSubscription = _openAIService.eventStream.listen( |
||||
|
(event) { |
||||
|
if (!_isProcessingStream) return; |
||||
|
|
||||
|
try { |
||||
|
switch (event.type) { |
||||
|
case OpenAIEventType.token: |
||||
|
if (event.content is String) { |
||||
|
_tokenStreamController.add(event.content as String); |
||||
|
} else { |
||||
|
printInfo(info: '收到非字符串类型的token: ${event.content}'); |
||||
|
} |
||||
|
break; |
||||
|
case OpenAIEventType.complete: |
||||
|
_isProcessingStream = false; |
||||
|
break; |
||||
|
case OpenAIEventType.error: |
||||
|
if (event.content is String) { |
||||
|
_tokenStreamController.addError(event.content as String); |
||||
|
} else { |
||||
|
_tokenStreamController.addError('未知错误: ${event.content}'); |
||||
|
} |
||||
|
_isProcessingStream = false; |
||||
|
break; |
||||
|
case OpenAIEventType.functionCall: |
||||
|
try { |
||||
|
if (event.content is Map) { |
||||
|
_tokenStreamController.addError('收到函数调用,该流仅支持文本响应'); |
||||
|
} else { |
||||
|
_tokenStreamController.addError('收到未知格式的函数调用'); |
||||
|
printError(info: '函数调用格式错误: ${event.content}'); |
||||
|
} |
||||
|
} catch (e) { |
||||
|
printError(info: '处理函数调用事件出错: $e'); |
||||
|
_tokenStreamController.addError('处理函数调用失败: $e'); |
||||
|
} |
||||
|
_isProcessingStream = false; |
||||
|
break; |
||||
|
} |
||||
|
} catch (e) { |
||||
|
printError(info: '处理事件出错: $e'); |
||||
|
_tokenStreamController.addError('处理事件失败: $e'); |
||||
|
_isProcessingStream = false; |
||||
|
} |
||||
|
}, |
||||
|
onError: (error) { |
||||
|
printError(info: '事件流错误: $error'); |
||||
|
_tokenStreamController.addError('事件流错误: $error'); |
||||
|
_isProcessingStream = false; |
||||
|
}, |
||||
|
onDone: () { |
||||
|
printInfo(info: '事件流已关闭'); |
||||
|
_isProcessingStream = false; |
||||
|
}, |
||||
|
); |
||||
|
} catch (e) { |
||||
|
printError(info: '设置事件监听器失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 初始化OpenAI服务 |
||||
|
Future<bool> initialize() async { |
||||
|
try { |
||||
|
// 从.env文件中读取配置 |
||||
|
final apiKey = dotenv.env['OPENAI_API_KEY'] ?? ''; |
||||
|
final baseUrl = dotenv.env['OPENAI_BASE_URL'] ?? ''; |
||||
|
final model = dotenv.env['OPENAI_MODEL'] ?? ''; |
||||
|
|
||||
|
printInfo(info: '从.env读取OpenAI配置'); |
||||
|
printInfo(info: '基础URL: $baseUrl'); |
||||
|
printInfo(info: '模型名称: $model'); |
||||
|
|
||||
|
if (apiKey.isEmpty) { |
||||
|
printError(info: '错误: OpenAI API密钥未配置,请在.env文件中设置OPENAI_API_KEY'); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// 初始化OpenAI服务 |
||||
|
final result = await _openAIService.initialize( |
||||
|
apiKey: apiKey, |
||||
|
baseUrl: baseUrl, |
||||
|
model: model, |
||||
|
); |
||||
|
|
||||
|
if (result) { |
||||
|
printInfo(info: 'OpenAI服务初始化成功'); |
||||
|
} else { |
||||
|
printError(info: 'OpenAI服务初始化失败'); |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} catch (e) { |
||||
|
printError(info: 'OpenAI服务初始化异常: $e'); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 发送消息并获取回复 |
||||
|
@override |
||||
|
Future<String> sendMessage({ |
||||
|
required List<Map<String, String>> messages, |
||||
|
required String systemPrompt, |
||||
|
}) async { |
||||
|
try { |
||||
|
// 在方法内直接转换 |
||||
|
final convertedMessages = messages.map((m) => |
||||
|
Map<String, dynamic>.from(m)).toList(); |
||||
|
|
||||
|
// 发送消息并获取回复 |
||||
|
final response = await _openAIService.sendMessage( |
||||
|
messages: convertedMessages, |
||||
|
); |
||||
|
|
||||
|
return response; |
||||
|
} catch (e) { |
||||
|
printError(info: 'OpenAI发送消息失败: $e'); |
||||
|
throw '发送消息失败: $e'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 发送消息并获取流式回复 |
||||
|
@override |
||||
|
Stream<String> sendMessageStream({ |
||||
|
required List<Map<String, String>> messages, |
||||
|
required String systemPrompt, |
||||
|
}) async* { |
||||
|
try { |
||||
|
// 在方法内直接转换 |
||||
|
final convertedMessages = messages.map((m) => |
||||
|
Map<String, dynamic>.from(m)).toList(); |
||||
|
|
||||
|
// 创建用于接收token的控制器 |
||||
|
final localController = StreamController<String>(); |
||||
|
|
||||
|
// 标记开始处理流 |
||||
|
_isProcessingStream = true; |
||||
|
|
||||
|
// 添加从广播流到本地流的订阅 |
||||
|
final subscription = _tokenStreamController.stream.listen( |
||||
|
(token) => localController.add(token), |
||||
|
onError: (error) { |
||||
|
printError(info: '令牌流错误: $error'); |
||||
|
localController.addError(error); |
||||
|
localController.close(); |
||||
|
}, |
||||
|
onDone: () { |
||||
|
printInfo(info: '令牌流已完成'); |
||||
|
localController.close(); |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
// 当本地控制器关闭时,取消订阅 |
||||
|
localController.onCancel = () { |
||||
|
subscription.cancel(); |
||||
|
}; |
||||
|
|
||||
|
// 启动流式消息请求 |
||||
|
bool started = false; |
||||
|
try { |
||||
|
started = await _openAIService.sendMessageStream( |
||||
|
messages: convertedMessages, |
||||
|
); |
||||
|
} catch (e) { |
||||
|
printError(info: '启动消息流失败: $e'); |
||||
|
localController.addError('启动消息流失败: $e'); |
||||
|
localController.close(); |
||||
|
_isProcessingStream = false; |
||||
|
throw '启动消息流失败: $e'; |
||||
|
} |
||||
|
|
||||
|
if (!started) { |
||||
|
printError(info: '无法启动消息流'); |
||||
|
localController.addError('无法启动消息流'); |
||||
|
localController.close(); |
||||
|
_isProcessingStream = false; |
||||
|
throw '无法启动消息流'; |
||||
|
} |
||||
|
|
||||
|
// 通过yield*将controller的流转发 |
||||
|
yield* localController.stream; |
||||
|
} catch (e) { |
||||
|
printError(info: 'OpenAI流式消息处理失败: $e'); |
||||
|
throw '流式消息处理失败: $e'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 注册函数 |
||||
|
Future<bool> registerFunction(String name, String description, Map<String, dynamic> parameters) async { |
||||
|
try { |
||||
|
final result = await _openAIService.registerFunction( |
||||
|
name: name, |
||||
|
description: description, |
||||
|
parameters: parameters, |
||||
|
); |
||||
|
|
||||
|
if (result) { |
||||
|
printInfo(info: '函数 "$name" 注册成功'); |
||||
|
} else { |
||||
|
printError(info: '函数 "$name" 注册失败'); |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} catch (e) { |
||||
|
printError(info: '注册函数失败: $e'); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 释放资源 |
||||
|
void dispose() { |
||||
|
try { |
||||
|
_isProcessingStream = false; |
||||
|
_eventSubscription?.cancel(); |
||||
|
_tokenStreamController.close(); |
||||
|
printInfo(info: 'OpenAIServiceAdapter资源已释放'); |
||||
|
} catch (e) { |
||||
|
printError(info: '释放资源时出错: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1 @@ |
|||||
|
curl -v -X POST -H 'Content-Type: application/json' -H 'Authorization: Bearer 168deb3d-fd0c-4912-b9f1-aaee5c6743e6' -H 'Accept: text/event-stream' -d '{"model":"bot-20250405211523-l7c9r","messages":[{"role":"system","content":" 你是一个智能语音助手,能够简洁明了地回答用户的问题。\n时刻关心用户的情绪和需求,主动提供鼓励和温暖。\n\n语言风格活泼、亲切,能够幽默地互动,陪伴用户,缓解压力,增添生活乐趣。\n\n请始终以用户为中心,保持回应的高效性、准确性和温暖体贴,成为用户真正的灵魂伴侣。\n \n 当用户说\"退出\"、\"再见\"、\"结束对话\"等类似意图时,你应该使用exit_interaction函数来结束对话,\n 并在结束前说一句友好的告别语,例如\"再见,有需要随时找我\"。"},{"role":"system","content":" 你是一个智能语音助手,能够简洁明了地回答用户的问题。\n时刻关心用户的情绪和需求,主动提供鼓励和温暖。\n\n语言风格活泼、亲切,能够幽默地互动,陪伴用户,缓解压力,增添生活乐趣。\n\n请始终以用户为中心,保持回应的高效性、准确性和温暖体贴,成为用户真正的灵魂伴侣。\n \n 当用户说\"退出\"、\"再见\"、\"结束对话\"等类似意图时,你应该使用exit_interaction函数来结束对话,\n 并在结束前说一句友好的告别语,例如\"再见,有需要随时找我\"。"},{"role":"user","content":"退下吧。"},{"role":"assistant","content":"","tool_calls":[{"id":"call_8k680azmfc4thqrrnwpwqxah","type":"function","function":{"name":"exit_interaction","arguments":" {}"}}]},{"role":"tool","content":"{\"result\": \"已退出语音交互\"}","tool_call_id":"call_8k680azmfc4thqrrnwpwqxah"}],"temperature":0.7,"max_tokens":2000,"stream":true,"tools":[{"type":"function","function":{"name":"exit_interaction","description":"退出当前语音交互","parameters":{"type":"object","properties":{},"required":[]}}}]}' 'https://ark.cn-beijing.volces.com/api/v3/bots/chat/completions' |
||||
Loading…
Reference in new issue