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