You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
566 lines
18 KiB
566 lines
18 KiB
import 'dart:async';
|
|
import 'dart:convert';
|
|
|
|
import 'package:flutter/services.dart';
|
|
|
|
/// 代理服务事件类型
|
|
enum AgentServiceEventType {
|
|
/// 识别开始
|
|
recognitionStarted,
|
|
|
|
/// 识别中
|
|
recognizing,
|
|
|
|
/// 识别最终结果
|
|
recognitionResult,
|
|
|
|
/// 识别停止
|
|
recognitionStopped,
|
|
|
|
/// 识别取消
|
|
recognitionCanceled,
|
|
|
|
/// 自动停止
|
|
autoStop,
|
|
|
|
/// 识别错误
|
|
recognitionError,
|
|
|
|
/// TTS开始
|
|
ttsStarted,
|
|
|
|
/// TTS完成
|
|
ttsCompleted,
|
|
|
|
/// TTS取消
|
|
ttsCanceled,
|
|
|
|
/// TTS停止
|
|
ttsStopped,
|
|
|
|
/// 播放开始
|
|
playbackStarted,
|
|
|
|
/// 播放完成
|
|
playbackCompleted,
|
|
|
|
/// 响应中断
|
|
responseInterrupted,
|
|
|
|
/// AI助手Token
|
|
assistantToken,
|
|
|
|
/// AI助手完整响应
|
|
assistantResponse,
|
|
|
|
/// 函数调用
|
|
functionCall,
|
|
|
|
/// 函数调用结果
|
|
functionCallResult,
|
|
|
|
/// AI助手使用统计
|
|
assistantUsages,
|
|
|
|
/// 图片处理中
|
|
imageProcessing,
|
|
|
|
/// 图片准备就绪
|
|
imageReady,
|
|
|
|
/// 错误
|
|
error,
|
|
|
|
/// 未知事件
|
|
unknown
|
|
}
|
|
|
|
/// 代理服务事件数据
|
|
class AgentServiceEvent {
|
|
/// 事件类型
|
|
final AgentServiceEventType type;
|
|
|
|
/// 事件数据
|
|
final Map<String, dynamic> data;
|
|
|
|
AgentServiceEvent({required this.type, required this.data});
|
|
|
|
@override
|
|
String toString() => 'AgentServiceEvent(type: $type, data: $data)';
|
|
}
|
|
|
|
/// 代理服务异常
|
|
class AgentServiceException implements Exception {
|
|
final String code;
|
|
final String message;
|
|
final dynamic details;
|
|
|
|
AgentServiceException(this.code, this.message, [this.details]);
|
|
|
|
@override
|
|
String toString() => '代理服务异常($code): $message';
|
|
}
|
|
|
|
/// 代理服务插件
|
|
class AgentService {
|
|
static const MethodChannel _channel =
|
|
MethodChannel('com.yunqiinnovation.agent_service');
|
|
static const EventChannel _eventChannel =
|
|
EventChannel('com.yunqiinnovation.agent_service/events');
|
|
|
|
static Stream<AgentServiceEvent>? _eventStream;
|
|
|
|
/// 原生日志回调,由主 app 设置,用于将原生日志写入文件
|
|
static void Function(String level, String message)? nativeLogCallback;
|
|
|
|
/// 获取事件流
|
|
static Stream<AgentServiceEvent> get events {
|
|
_eventStream ??= _eventChannel.receiveBroadcastStream().where((event) {
|
|
// 拦截原生日志事件,不传入业务流
|
|
try {
|
|
final Map<String, dynamic> eventMap = jsonDecode(event);
|
|
if (eventMap['event'] == 'nativeLog') {
|
|
final data = eventMap['data'] as Map<String, dynamic>?;
|
|
nativeLogCallback?.call(
|
|
data?['level'] as String? ?? 'INFO',
|
|
data?['message'] as String? ?? '',
|
|
);
|
|
return false;
|
|
}
|
|
} catch (_) {}
|
|
return true;
|
|
}).map((event) {
|
|
try {
|
|
final Map<String, dynamic> eventMap = jsonDecode(event);
|
|
final String eventName = eventMap['event'];
|
|
final Map<String, dynamic> eventData = eventMap['data'];
|
|
// 将事件名称转换为枚举
|
|
final AgentServiceEventType type = _stringToEventType(eventName);
|
|
|
|
return AgentServiceEvent(type: type, data: eventData);
|
|
} catch (e) {
|
|
return AgentServiceEvent(
|
|
type: AgentServiceEventType.unknown,
|
|
data: {'error': e.toString(), 'rawEvent': event});
|
|
}
|
|
});
|
|
|
|
return _eventStream!;
|
|
}
|
|
|
|
/// 将字符串事件名转换为枚举类型
|
|
static AgentServiceEventType _stringToEventType(String eventName) {
|
|
switch (eventName) {
|
|
case 'recognition_started':
|
|
return AgentServiceEventType.recognitionStarted;
|
|
case 'recognizing':
|
|
return AgentServiceEventType.recognizing;
|
|
case 'recognition_result':
|
|
return AgentServiceEventType.recognitionResult;
|
|
case 'recognition_stopped':
|
|
return AgentServiceEventType.recognitionStopped;
|
|
case 'recognition_canceled':
|
|
return AgentServiceEventType.recognitionCanceled;
|
|
case 'auto_stop':
|
|
return AgentServiceEventType.autoStop;
|
|
case 'error':
|
|
return AgentServiceEventType.error;
|
|
case 'tts_started':
|
|
return AgentServiceEventType.ttsStarted;
|
|
case 'tts_completed':
|
|
return AgentServiceEventType.ttsCompleted;
|
|
case 'tts_canceled':
|
|
return AgentServiceEventType.ttsCanceled;
|
|
case 'tts_stopped':
|
|
return AgentServiceEventType.ttsStopped;
|
|
case 'playback_started':
|
|
return AgentServiceEventType.playbackStarted;
|
|
case 'playback_completed':
|
|
return AgentServiceEventType.playbackCompleted;
|
|
case 'response_interrupted':
|
|
return AgentServiceEventType.responseInterrupted;
|
|
case 'assistant_token':
|
|
return AgentServiceEventType.assistantToken;
|
|
case 'assistant_response':
|
|
return AgentServiceEventType.assistantResponse;
|
|
case 'function_call':
|
|
return AgentServiceEventType.functionCall;
|
|
case 'function_call_result':
|
|
return AgentServiceEventType.functionCallResult;
|
|
case 'assistant_usages':
|
|
return AgentServiceEventType.assistantUsages;
|
|
case 'image_processing':
|
|
return AgentServiceEventType.imageProcessing;
|
|
case 'image_ready':
|
|
return AgentServiceEventType.imageReady;
|
|
default:
|
|
return AgentServiceEventType.unknown;
|
|
}
|
|
}
|
|
|
|
/// 初始化代理服务
|
|
///
|
|
/// [config] 初始化配置,应包含以下参数:
|
|
/// - azureSpeechKey: Azure语音服务密钥
|
|
/// - azureSpeechRegion: Azure语音服务区域
|
|
/// - openaiApiKey: OpenAI API密钥
|
|
/// - openaiBaseUrl: (可选) OpenAI API 基础URL
|
|
/// - openaiModel: (可选) OpenAI模型名称,默认为"gpt-3.5-turbo"
|
|
/// - mcpServer: (可选) MCP服务器地址
|
|
/// - systemPrompt: (可选) 自定义系统提示词,用于设置AI语音助手的行为和风格
|
|
///
|
|
/// 返回是否初始化成功
|
|
static Future<bool> initialize(Map<String, dynamic> config) async {
|
|
try {
|
|
final bool result = await _channel.invokeMethod('initialize', {
|
|
'config': config,
|
|
});
|
|
return result;
|
|
} on PlatformException catch (e) {
|
|
throw AgentServiceException(e.code, e.message ?? '初始化失败', e.details);
|
|
}
|
|
}
|
|
|
|
/// 启动语音助手服务
|
|
///
|
|
/// 启动常驻的语音助手服务,支持后台蓝牙设备唤醒和媒体按钮唤醒
|
|
/// [config] 配置参数,应包含以下参数:
|
|
/// - azureSpeechKey: Azure语音服务密钥
|
|
/// - azureSpeechRegion: Azure语音服务区域
|
|
/// - openaiApiKey: OpenAI API密钥
|
|
/// - openaiBaseUrl: (可选) OpenAI API 基础URL
|
|
/// - openaiModel: (可选) OpenAI模型名称,默认为"gpt-3.5-turbo"
|
|
/// - mcpServer: (可选) MCP服务器地址
|
|
///
|
|
/// 返回是否成功启动服务
|
|
static Future<bool> startAgentService(Map<String, dynamic> config) async {
|
|
try {
|
|
final bool result = await _channel.invokeMethod('startAgentService', {
|
|
'config': config,
|
|
});
|
|
return result;
|
|
} on PlatformException catch (e) {
|
|
throw AgentServiceException(e.code, e.message ?? '启动语音助手服务失败', e.details);
|
|
}
|
|
}
|
|
|
|
/// 停止语音助手服务
|
|
///
|
|
/// 停止常驻的语音助手服务
|
|
/// 返回是否成功停止服务
|
|
static Future<bool> stopAgentService() async {
|
|
try {
|
|
final bool result = await _channel.invokeMethod('stopAgentService');
|
|
return result;
|
|
} on PlatformException catch (e) {
|
|
throw AgentServiceException(e.code, e.message ?? '停止语音助手服务失败', e.details);
|
|
}
|
|
}
|
|
|
|
/// 开始对话
|
|
///
|
|
/// 启动语音识别,开始监听用户语音输入
|
|
/// [mode] 语音识别模式:'ble_wakeup'(BLE唤醒), 'phone_call'(通话模式), 'push_to_talk'(按住说话), 'normal'(普通模式)
|
|
/// 返回是否成功开始对话
|
|
// 防抖相关静态字段(全局统一防抖)
|
|
// 说明:用于限制 startConversation/stopConversation 在极短时间内的重复触发
|
|
static DateTime? _lastStartConversationAt;
|
|
static const Duration _conversationDebounce = Duration(milliseconds: 600);
|
|
|
|
static Future<bool> startConversation({String mode = 'normal'}) async {
|
|
// 防抖:短时间内重复点击直接忽略(冷却式防抖)
|
|
final now = DateTime.now();
|
|
if (_lastStartConversationAt != null &&
|
|
now.difference(_lastStartConversationAt!) < _conversationDebounce) {
|
|
// 函数级注释:当在冷却时间内再次调用时,直接返回 false 表示此次调用被防抖拦截
|
|
// 对于启动操作,如果被防抖拦截,意味着用户点击太快。
|
|
// 返回 false 是正确的,因为我们确实没有启动新的会话。
|
|
return false;
|
|
}
|
|
_lastStartConversationAt = now;
|
|
|
|
try {
|
|
// 在启动新对话前,确保之前的对话已完全停止
|
|
// 这不仅可以清理状态,还能避免因快速切换导致的资源冲突
|
|
// await stopConversation();
|
|
print('liwei--------- view startConversation');
|
|
final bool result = await _channel.invokeMethod('startConversation', {
|
|
'mode': mode,
|
|
});
|
|
return result;
|
|
} on PlatformException catch (e) {
|
|
throw AgentServiceException(e.code, e.message ?? '启动对话失败', e.details);
|
|
}
|
|
}
|
|
|
|
static Future<bool> isInterrupt(bool isInterrupt) async {
|
|
try {
|
|
final bool result = await _channel.invokeMethod('isInterrupt', {
|
|
'isinterrupt': isInterrupt,
|
|
});
|
|
return result;
|
|
} on PlatformException catch (e) {
|
|
throw AgentServiceException(e.code, e.message ?? '检查是否打断失败', e.details);
|
|
}
|
|
}
|
|
|
|
/// 设备识别器
|
|
///
|
|
/// 启动语音识别,开始监听用户语音输入
|
|
/// 返回是否成功开始对话
|
|
static Future<bool> recognizeCallback() async {
|
|
try {
|
|
final bool result = await _channel.invokeMethod('recognizeCallback');
|
|
return result;
|
|
} on PlatformException catch (e) {
|
|
throw AgentServiceException(e.code, e.message ?? '设置识别器失败', e.details);
|
|
}
|
|
}
|
|
|
|
/// 恢复原始音频状态
|
|
///
|
|
/// 恢复原始音频状态
|
|
/// 返回是否成功恢复原始音频状态
|
|
static Future<bool> restoreOriginalAudioState() async {
|
|
try {
|
|
final bool result =
|
|
await _channel.invokeMethod('restoreOriginalAudioState');
|
|
return result;
|
|
} on PlatformException catch (e) {
|
|
throw AgentServiceException(e.code, e.message ?? '恢复原始音频状态失败', e.details);
|
|
}
|
|
}
|
|
|
|
/// 禁用蓝牙音频
|
|
///
|
|
/// 禁用蓝牙音频
|
|
/// 返回是否成功禁用蓝牙音频
|
|
static Future<bool> disableBluetoothAudio() async {
|
|
try {
|
|
final bool result = await _channel.invokeMethod('disableBluetoothAudio');
|
|
return result;
|
|
} on PlatformException catch (e) {
|
|
throw AgentServiceException(e.code, e.message ?? '禁用蓝牙音频失败', e.details);
|
|
}
|
|
}
|
|
|
|
/// 停止对话
|
|
///
|
|
/// 停止语音识别
|
|
/// 返回是否成功停止对话
|
|
static Future<bool> stopConversation() async {
|
|
// 移除防抖拦截!
|
|
// 停止操作是安全且必要的,不应该被拦截。
|
|
// 即使频繁调用,Native 层也能处理(我们已经修复了 Native 层的并发问题)。
|
|
|
|
try {
|
|
print('liwei--------- view stopConversation');
|
|
final bool result = await _channel.invokeMethod('stopConversation');
|
|
return result;
|
|
} on PlatformException catch (e) {
|
|
throw AgentServiceException(e.code, e.message ?? '停止对话失败', e.details);
|
|
}
|
|
}
|
|
|
|
// /// 停止对话
|
|
// ///
|
|
// /// 停止语音识别
|
|
// /// 返回是否成功停止对话
|
|
// static Future<bool> audioContinue() async {
|
|
// try {
|
|
// final bool result = await _channel.invokeMethod('audioContinue');
|
|
// return result;
|
|
// } on PlatformException catch (e) {
|
|
// throw AgentServiceException(e.code, e.message ?? '停止对话失败', e.details);
|
|
// }
|
|
// }
|
|
|
|
// /// 停止对话
|
|
// ///
|
|
// /// 停止语音识别
|
|
// /// 返回是否成功停止对话
|
|
// static Future<bool> audioPause() async {
|
|
// try {
|
|
// final bool result = await _channel.invokeMethod('audioPause');
|
|
// return result;
|
|
// } on PlatformException catch (e) {
|
|
// throw AgentServiceException(e.code, e.message ?? '停止对话失败', e.details);
|
|
// }
|
|
// }
|
|
|
|
/// 处理文本输入
|
|
///
|
|
/// [text] 文本内容
|
|
/// [speakResponse] 是否朗读响应
|
|
/// 返回是否成功处理文本
|
|
static Future<bool> processTextInput(String sessionid, String text,
|
|
{bool speakResponse = false}) async {
|
|
try {
|
|
final bool result = await _channel.invokeMethod('processTextInput', {
|
|
'sessionid': sessionid,
|
|
'text': text,
|
|
'speakResponse': speakResponse,
|
|
});
|
|
return result;
|
|
} on PlatformException catch (e) {
|
|
throw AgentServiceException(e.code, e.message ?? '处理文本输入失败', e.details);
|
|
}
|
|
}
|
|
|
|
/// 朗读文本
|
|
///
|
|
/// [text] 要朗读的文本
|
|
static Future<bool> speakText(String text) async {
|
|
try {
|
|
final bool result = await _channel.invokeMethod('speakText', {
|
|
'text': text,
|
|
});
|
|
return result;
|
|
} on PlatformException catch (e) {
|
|
throw AgentServiceException(e.code, e.message ?? '语音合成失败', e.details);
|
|
}
|
|
}
|
|
|
|
/// 设置TTS语速
|
|
static Future<bool> setTtsRate(int rate) async {
|
|
try {
|
|
final bool result = await _channel.invokeMethod('setTtsRate', {
|
|
'rate': rate,
|
|
});
|
|
return result;
|
|
} on PlatformException catch (e) {
|
|
throw AgentServiceException(e.code, e.message ?? '设置TTS语速失败', e.details);
|
|
}
|
|
}
|
|
|
|
/// 停止语音合成
|
|
static Future<bool> stopAiSteam() async {
|
|
try {
|
|
final bool result = await _channel.invokeMethod('stopAiSteam');
|
|
return result;
|
|
} on PlatformException catch (e) {
|
|
throw AgentServiceException(e.code, e.message ?? '停止语音合成失败', e.details);
|
|
}
|
|
}
|
|
|
|
/// 停止语音合成
|
|
static Future<bool> stopTts() async {
|
|
try {
|
|
final bool result = await _channel.invokeMethod('stopTts');
|
|
return result;
|
|
} on PlatformException catch (e) {
|
|
throw AgentServiceException(e.code, e.message ?? '停止语音合成失败', e.details);
|
|
}
|
|
}
|
|
|
|
/// 清除聊天历史
|
|
static Future<bool> clearChatHistory() async {
|
|
try {
|
|
final bool result = await _channel.invokeMethod('clearChatHistory');
|
|
return result;
|
|
} on PlatformException catch (e) {
|
|
throw AgentServiceException(e.code, e.message ?? '清除聊天历史失败', e.details);
|
|
}
|
|
}
|
|
|
|
/// 中断当前响应
|
|
///
|
|
/// 停止语音合成和AI流输出
|
|
static Future<bool> interruptCurrentResponse() async {
|
|
try {
|
|
final bool result =
|
|
await _channel.invokeMethod('interruptCurrentResponse');
|
|
return result;
|
|
} on PlatformException catch (e) {
|
|
throw AgentServiceException(e.code, e.message ?? '中断响应失败', e.details);
|
|
}
|
|
}
|
|
|
|
/// 处理图片输入
|
|
///
|
|
/// [imagePath] 图片文件路径
|
|
/// [text] 可选的文本描述或问题
|
|
/// [imageUrl] 图片url
|
|
/// [speakResponse] 是否朗读响应
|
|
/// 返回是否成功处理图片
|
|
static Future<bool> processImageInput(
|
|
String sessionid, String imagePath, String imageUrl,
|
|
{String text = "", bool speakResponse = false}) async {
|
|
try {
|
|
final bool result = await _channel.invokeMethod('processImageInput', {
|
|
'sessionid': sessionid,
|
|
'imagePath': imagePath,
|
|
'imageUrl': imageUrl,
|
|
'text': text,
|
|
'speakResponse': speakResponse,
|
|
});
|
|
return result;
|
|
} on PlatformException catch (e) {
|
|
throw AgentServiceException(e.code, e.message ?? '处理图片输入失败', e.details);
|
|
}
|
|
}
|
|
|
|
/// 设置支持的语音识别语言
|
|
///
|
|
/// [languages] 支持的语言列表,如 ["zh-CN", "en-US"]
|
|
/// 返回是否设置成功
|
|
static Future<bool> setSupportedLanguages(List<String> languages) async {
|
|
try {
|
|
final bool result = await _channel.invokeMethod('setSupportedLanguages', {
|
|
'languages': languages,
|
|
});
|
|
return result;
|
|
} on PlatformException catch (e) {
|
|
throw AgentServiceException(e.code, e.message ?? '设置支持语言失败', e.details);
|
|
}
|
|
}
|
|
|
|
/// 更新用户信息
|
|
///
|
|
/// 更新用户信息,包括姓名、手机号、邮箱等
|
|
/// [config] 配置参数,应包含以下参数:
|
|
/// - azureSpeechKey: Azure语音服务密钥
|
|
/// - azureSpeechRegion: Azure语音服务区域
|
|
/// - openaiApiKey: OpenAI API密钥
|
|
/// - openaiBaseUrl: (可选) OpenAI API 基础URL
|
|
/// - openaiModel: (可选) OpenAI模型名称,默认为"gpt-3.5-turbo"
|
|
/// - mcpServer: (可选) MCP服务器地址
|
|
///
|
|
/// 返回是否成功启动服务
|
|
static Future<bool> setVipExptime(int vipexptime) async {
|
|
try {
|
|
final bool result = await _channel.invokeMethod('setVipExptime', {
|
|
'vipexptime': vipexptime,
|
|
});
|
|
return result;
|
|
} on PlatformException catch (e) {
|
|
throw AgentServiceException(e.code, e.message ?? '更新用户信息失败', e.details);
|
|
}
|
|
}
|
|
|
|
/// 设置是否可以ble唤醒agent
|
|
///
|
|
/// [canWakeUp] 是否可以ble唤醒agent
|
|
/// 返回是否设置成功
|
|
static Future<bool> setCanWakeUp(bool canWakeUp) async {
|
|
try {
|
|
final bool result = await _channel.invokeMethod('setCanWakeUp', {
|
|
'canWakeUp': canWakeUp,
|
|
});
|
|
return result;
|
|
} on PlatformException catch (e) {
|
|
throw AgentServiceException(
|
|
e.code, e.message ?? '设置是否可以ble唤醒agent失败', e.details);
|
|
}
|
|
}
|
|
|
|
/// 释放资源
|
|
static Future<bool> dispose() async {
|
|
try {
|
|
final bool result = await _channel.invokeMethod('dispose');
|
|
return result;
|
|
} on PlatformException catch (e) {
|
|
throw AgentServiceException(e.code, e.message ?? '释放资源失败', e.details);
|
|
}
|
|
}
|
|
}
|
|
|