Compare commits
26 Commits
e39f42a3df
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07980fea87 | ||
|
|
1729d0f266 | ||
|
|
c29a3f665a | ||
|
|
a7ed838c92 | ||
|
|
ba73d6d780 | ||
|
|
5be50b4dbb | ||
|
|
c6765ab29c | ||
|
|
f3ff75c437 | ||
|
|
f5699fd144 | ||
|
|
4a41c25502 | ||
|
|
9662ff6a56 | ||
|
|
2bdb15db1d | ||
|
|
50226b27a1 | ||
|
|
5e8b9af17b | ||
|
|
d3580a6a0b | ||
|
|
87cc15dd53 | ||
|
|
6523f8e512 | ||
|
|
b9ab384fde | ||
|
|
049cb7b5c4 | ||
|
|
e04110c6d5 | ||
|
|
0509096925 | ||
|
|
b84899ece3 | ||
|
|
922818f39f | ||
|
|
33aef48f84 | ||
|
|
62924296b2 | ||
|
|
4176116bb3 |
5
.gitignore
vendored
@@ -43,3 +43,8 @@ app.*.map.json
|
|||||||
/android/app/debug
|
/android/app/debug
|
||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/android/app/release
|
||||||
|
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# FVM Version Cache
|
||||||
|
.fvm/
|
||||||
644
CODE_ANALYSIS.md
Normal file
@@ -0,0 +1,644 @@
|
|||||||
|
# AI Chat Assistant Flutter Plugin - 详细代码逻辑文档
|
||||||
|
|
||||||
|
## 📋 项目概述
|
||||||
|
|
||||||
|
这是一个专为车载系统设计的AI聊天助手Flutter插件,采用插件化架构设计,支持语音识别、AI对话、车控命令执行等功能。项目从原有的App结构重构为Plugin结构,便于集成到其他Flutter应用中。
|
||||||
|
|
||||||
|
## 🏗️ 核心架构分析
|
||||||
|
|
||||||
|
### 1. 架构模式
|
||||||
|
|
||||||
|
- **插件化架构**: 采用Flutter Plugin架构,便于第三方应用集成
|
||||||
|
- **单例模式**: 核心服务类(MessageService)采用单例模式确保全局状态一致
|
||||||
|
- **观察者模式**: 基于Provider的状态管理,实现响应式UI更新
|
||||||
|
- **回调机制**: 通过CommandCallback实现车控命令的解耦处理
|
||||||
|
|
||||||
|
### 2. 目录结构与职责
|
||||||
|
|
||||||
|
```
|
||||||
|
lib/
|
||||||
|
├── app.dart # 插件主入口,提供初始化接口
|
||||||
|
├── enums/ # 枚举定义层
|
||||||
|
├── models/ # 数据模型层
|
||||||
|
├── services/ # 业务逻辑服务层
|
||||||
|
├── screens/ # UI界面层
|
||||||
|
├── widgets/ # UI组件层
|
||||||
|
├── utils/ # 工具类层
|
||||||
|
└── themes/ # 主题样式层
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 核心枚举定义 (Enums)
|
||||||
|
|
||||||
|
### MessageServiceState
|
||||||
|
```dart
|
||||||
|
enum MessageServiceState {
|
||||||
|
idle, // 空闲状态
|
||||||
|
recording, // 录音中
|
||||||
|
recognizing, // 识别中
|
||||||
|
replying, // 回复中
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### MessageStatus
|
||||||
|
```dart
|
||||||
|
enum MessageStatus {
|
||||||
|
normal, // 普通消息
|
||||||
|
listening, // 聆听中
|
||||||
|
recognizing, // 识别中
|
||||||
|
thinking, // 思考中
|
||||||
|
completed, // 完成回答
|
||||||
|
executing, // 执行中
|
||||||
|
success, // 执行成功
|
||||||
|
failure, // 执行失败
|
||||||
|
aborted, // 已中止
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### VehicleCommandType
|
||||||
|
支持22种车控命令类型:
|
||||||
|
- 车门控制:lock, unlock
|
||||||
|
- 车窗控制:openWindow, closeWindow
|
||||||
|
- 空调控制:openAC, closeAC, changeACTemp, coolSharply
|
||||||
|
- 特殊功能:prepareCar, meltSnow, honk, locateCar
|
||||||
|
- 加热功能:座椅加热、方向盘加热等
|
||||||
|
|
||||||
|
## 📊 数据模型层 (Models)
|
||||||
|
|
||||||
|
### ChatMessage
|
||||||
|
```dart
|
||||||
|
class ChatMessage {
|
||||||
|
final String id; // 消息唯一标识
|
||||||
|
final String text; // 消息内容
|
||||||
|
final bool isUser; // 是否为用户消息
|
||||||
|
final DateTime timestamp; // 时间戳
|
||||||
|
MessageStatus status; // 消息状态
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### VehicleCommand
|
||||||
|
```dart
|
||||||
|
class VehicleCommand {
|
||||||
|
final VehicleCommandType type; // 命令类型
|
||||||
|
final Map<String, dynamic>? params; // 命令参数
|
||||||
|
final String error; // 错误信息
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 核心服务层 (Services) - 完整解析
|
||||||
|
|
||||||
|
### 1. MessageService - 核心消息管理服务
|
||||||
|
|
||||||
|
**设计模式**: 单例模式 + 观察者模式
|
||||||
|
|
||||||
|
**主要职责**:
|
||||||
|
- 统一管理聊天消息
|
||||||
|
- 协调语音识别、AI对话、车控命令执行流程
|
||||||
|
- 维护应用状态机
|
||||||
|
- 通过Method Channel与原生ASR服务通信
|
||||||
|
|
||||||
|
**核心方法分析**:
|
||||||
|
|
||||||
|
#### 语音输入流程
|
||||||
|
```dart
|
||||||
|
// 开始语音输入
|
||||||
|
Future<void> startVoiceInput() async {
|
||||||
|
// 1. 权限检查 - 使用permission_handler检查麦克风权限
|
||||||
|
// 2. 状态验证 - 确保当前状态为idle
|
||||||
|
// 3. 初始化录音 - 创建用户消息占位符
|
||||||
|
// 4. 调用原生ASR服务 - 通过Method Channel启动阿里云ASR
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止并处理语音输入
|
||||||
|
Future<void> stopAndProcessVoiceInput() async {
|
||||||
|
// 1. 停止录音 - 调用原生ASR停止接口
|
||||||
|
// 2. 等待ASR结果 - 使用Completer等待异步结果
|
||||||
|
// 3. 调用reply()处理识别结果
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 智能回复流程
|
||||||
|
```dart
|
||||||
|
Future<void> reply(String text) async {
|
||||||
|
// 1. 文本分类(TextClassificationService)
|
||||||
|
// 2. 根据分类结果路由到不同处理逻辑:
|
||||||
|
// - 车控命令 (category=2) -> handleVehicleControl()
|
||||||
|
// - 普通问答 (default) -> answerQuestion()
|
||||||
|
// - 错误问题 (category=4) -> answerWrongQuestion()
|
||||||
|
// - 系统错误 (category=-1) -> occurError()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 车控命令处理
|
||||||
|
```dart
|
||||||
|
Future<void> handleVehicleControl(String text, bool isChinese) async {
|
||||||
|
// 1. 调用VehicleCommandService解析命令
|
||||||
|
// 2. 执行TTS播报
|
||||||
|
// 3. 逐个执行车控命令
|
||||||
|
// 4. 处理命令执行结果
|
||||||
|
// 5. 生成执行反馈
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**状态管理机制**:
|
||||||
|
- 使用`ChangeNotifier`实现响应式状态更新
|
||||||
|
- 通过`_state`维护服务状态机
|
||||||
|
- 使用`_isReplyAborted`实现流程中断控制
|
||||||
|
|
||||||
|
### 2. ChatSseService - SSE流式通信服务
|
||||||
|
|
||||||
|
**设计模式**: 单例服务 + 流式处理
|
||||||
|
|
||||||
|
**核心功能**:
|
||||||
|
- 基于SSE的实时AI对话
|
||||||
|
- 流式文本处理和TTS播报
|
||||||
|
- 会话状态管理
|
||||||
|
- 智能分句和语音合成
|
||||||
|
|
||||||
|
**关键实现**:
|
||||||
|
|
||||||
|
#### SSE连接管理
|
||||||
|
```dart
|
||||||
|
void request({
|
||||||
|
required String messageId,
|
||||||
|
required String text,
|
||||||
|
required bool isChinese,
|
||||||
|
required Function(String, String, bool) onStreamResponse,
|
||||||
|
}) async {
|
||||||
|
// 1. 初始化用户ID和会话ID
|
||||||
|
// 2. 建立HTTP SSE连接
|
||||||
|
// 3. 处理流式响应数据
|
||||||
|
// 4. 实时文本清理和分句
|
||||||
|
// 5. 协调TTS播报
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 智能分句算法
|
||||||
|
```dart
|
||||||
|
// 支持中英文分句符识别
|
||||||
|
final enEnders = RegExp(r'[.!?]');
|
||||||
|
final zhEnders = RegExp(r'[。!?]');
|
||||||
|
|
||||||
|
// Markdown内容清理
|
||||||
|
// 1. 清理图片语法 ![...]
|
||||||
|
// 2. 移除列表序号
|
||||||
|
// 3. 处理不完整语法
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. AudioRecorderService - 音频录制服务
|
||||||
|
|
||||||
|
**核心功能**:
|
||||||
|
- 基于record插件的音频录制
|
||||||
|
- OPUS格式音频编码
|
||||||
|
- 权限管理
|
||||||
|
- 临时文件管理
|
||||||
|
|
||||||
|
**实现细节**:
|
||||||
|
```dart
|
||||||
|
class AudioRecorderService {
|
||||||
|
final AudioRecorder _recorder = AudioRecorder();
|
||||||
|
bool _isRecording = false;
|
||||||
|
String? _tempFilePath;
|
||||||
|
|
||||||
|
Future<void> startRecording() async {
|
||||||
|
// 1. 生成临时文件路径
|
||||||
|
// 2. 配置OPUS编码格式
|
||||||
|
// 3. 启动录音
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<int>?> stopRecording() async {
|
||||||
|
// 1. 停止录音
|
||||||
|
// 2. 读取音频文件
|
||||||
|
// 3. 返回字节数组
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. VoiceRecognitionService - 语音识别服务
|
||||||
|
|
||||||
|
**功能**:
|
||||||
|
- HTTP多媒体文件上传
|
||||||
|
- 音频格式转换
|
||||||
|
- 中英文语言识别
|
||||||
|
|
||||||
|
**核心逻辑**:
|
||||||
|
```dart
|
||||||
|
Future<String?> recognizeSpeech(List<int> audioBytes, {String lang = 'cn'}) async {
|
||||||
|
// 1. 构建MultipartRequest
|
||||||
|
// 2. 上传音频文件到识别服务
|
||||||
|
// 3. 解析JSON响应获取文本
|
||||||
|
// 4. 错误处理和日志记录
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. LocalTtsService - 语音合成服务
|
||||||
|
|
||||||
|
**设计亮点**: 流式TTS处理 + 有序播放队列
|
||||||
|
|
||||||
|
**核心特性**:
|
||||||
|
- 基于flutter_tts的跨平台TTS
|
||||||
|
- 流式文本分句播报
|
||||||
|
- 有序任务队列管理
|
||||||
|
- 中英文语言自动切换
|
||||||
|
|
||||||
|
**关键数据结构**:
|
||||||
|
```dart
|
||||||
|
class TtsTask {
|
||||||
|
final int index; // 任务索引
|
||||||
|
final String text; // 播报文本
|
||||||
|
TtsTaskStatus status; // 任务状态(ready/completed)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TtsTaskStatus {
|
||||||
|
ready, // 准备播放
|
||||||
|
completed, // 播放完成
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**流式播放流程**:
|
||||||
|
```dart
|
||||||
|
void pushTextForStreamTTS(String text) {
|
||||||
|
// 1. 创建有序任务
|
||||||
|
// 2. 添加到任务队列
|
||||||
|
// 3. 触发下一个任务处理
|
||||||
|
}
|
||||||
|
|
||||||
|
void _processNextReadyTask() {
|
||||||
|
// 1. 检查播放状态
|
||||||
|
// 2. 按序查找就绪任务
|
||||||
|
// 3. 执行TTS播放
|
||||||
|
// 4. 等待播放完成回调
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. TextClassificationService - 文本分类服务
|
||||||
|
|
||||||
|
**功能**: NLP文本意图分类
|
||||||
|
|
||||||
|
**分类结果**:
|
||||||
|
- `-1`: 系统错误
|
||||||
|
- `2`: 车控命令
|
||||||
|
- `4`: 无效问题
|
||||||
|
- 其他: 普通问答
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future<int> classifyText(String text) async {
|
||||||
|
// HTTP POST到分类服务
|
||||||
|
// 返回分类category整数
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. VehicleCommandService - 车控命令解析服务
|
||||||
|
|
||||||
|
**职责**:
|
||||||
|
- 自然语言转车控命令
|
||||||
|
- 命令参数解析
|
||||||
|
- 执行结果反馈生成
|
||||||
|
|
||||||
|
**核心方法**:
|
||||||
|
```dart
|
||||||
|
Future<VehicleCommandResponse?> getCommandFromText(String text) async {
|
||||||
|
// 1. 发送文本到NLP解析服务
|
||||||
|
// 2. 解析返回的命令列表
|
||||||
|
// 3. 构建VehicleCommandResponse对象
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> getControlResponse(List<String> successCommandList) async {
|
||||||
|
// 根据成功命令列表生成友好反馈文本
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. CommandService - 车控命令执行服务
|
||||||
|
|
||||||
|
**设计模式**: 静态方法 + 回调机制
|
||||||
|
|
||||||
|
**解耦设计**:
|
||||||
|
```dart
|
||||||
|
typedef CommandCallback = Future<(bool, Map<String, dynamic>? params)> Function(
|
||||||
|
VehicleCommandType type, Map<String, dynamic>? params);
|
||||||
|
|
||||||
|
static Future<(bool, Map<String, dynamic>? params)> executeCommand(
|
||||||
|
VehicleCommandType type, {Map<String, dynamic>? params}) async {
|
||||||
|
// 调用主应用注册的回调函数
|
||||||
|
// 返回执行结果元组(成功状态, 返回参数)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. RedisService - 缓存服务
|
||||||
|
|
||||||
|
**功能**: 基于HTTP的Redis缓存操作
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class RedisService {
|
||||||
|
Future<void> setKeyValue(String key, Object value) async {
|
||||||
|
// HTTP POST设置键值对
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> getValue(String key) async {
|
||||||
|
// HTTP GET获取值
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10. LocationService - 位置服务
|
||||||
|
|
||||||
|
用于车辆定位相关功能(具体实现需查看完整代码)
|
||||||
|
|
||||||
|
### 11. VehicleStateService - 车辆状态服务
|
||||||
|
|
||||||
|
**注释代码分析**:
|
||||||
|
- 原设计基于InGeek MDK车载SDK
|
||||||
|
- 支持车辆状态实时监听
|
||||||
|
- 与Redis服务配合缓存状态数据
|
||||||
|
- 当前版本已注释,可能在插件化过程中移除
|
||||||
|
|
||||||
|
## 🎨 UI层架构分析
|
||||||
|
|
||||||
|
### 1. 主界面 (MainScreen)
|
||||||
|
|
||||||
|
**设计**: 采用Stack布局,背景图片 + 浮动图标
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
// 背景图片
|
||||||
|
Container(decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: AssetImage('assets/images/bg.jpg', package: 'ai_chat_assistant'),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
// 浮动AI助手图标
|
||||||
|
FloatingIcon(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 浮动图标 (FloatingIcon)
|
||||||
|
|
||||||
|
**功能特性**:
|
||||||
|
- 可拖拽定位
|
||||||
|
- 状态指示(通过不同图标)
|
||||||
|
- 点击展开聊天界面
|
||||||
|
- 动画效果支持
|
||||||
|
|
||||||
|
**交互流程**:
|
||||||
|
1. 长按拖拽调整位置
|
||||||
|
2. 点击展开部分屏幕聊天界面
|
||||||
|
3. 根据MessageService状态切换图标
|
||||||
|
|
||||||
|
### 3. 聊天界面架构
|
||||||
|
|
||||||
|
**部分屏模式** (PartScreen):
|
||||||
|
- 简化的聊天界面
|
||||||
|
- 支持语音输入和文本显示
|
||||||
|
- 可展开到全屏模式
|
||||||
|
|
||||||
|
**全屏模式** (FullScreen):
|
||||||
|
- 完整的聊天功能
|
||||||
|
- 历史消息展示
|
||||||
|
- 更多操作按钮
|
||||||
|
|
||||||
|
## 🔌 原生平台集成
|
||||||
|
|
||||||
|
### Android ASR集成
|
||||||
|
|
||||||
|
**Method Channel通信**:
|
||||||
|
```dart
|
||||||
|
static const MethodChannel _asrChannel =
|
||||||
|
MethodChannel('com.example.ai_chat_assistant/ali_sdk');
|
||||||
|
```
|
||||||
|
|
||||||
|
**ASR事件处理**:
|
||||||
|
```dart
|
||||||
|
_asrChannel.setMethodCallHandler((call) async {
|
||||||
|
switch (call.method) {
|
||||||
|
case "onAsrResult":
|
||||||
|
// 实时更新识别结果
|
||||||
|
replaceMessage(id: _latestUserMessageId!, text: call.arguments);
|
||||||
|
break;
|
||||||
|
case "onAsrStop":
|
||||||
|
// 识别结束,触发后续处理
|
||||||
|
if (_asrCompleter != null && !_asrCompleter!.isCompleted) {
|
||||||
|
_asrCompleter!.complete(messages.last.text);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ 工具类分析
|
||||||
|
|
||||||
|
### CommonUtil
|
||||||
|
**主要功能**:
|
||||||
|
- 中文检测: `containChinese(String text)`
|
||||||
|
- Markdown清理: `cleanText(String text, bool forTts)`
|
||||||
|
- 公共颜色常量定义
|
||||||
|
|
||||||
|
**Markdown清理逻辑**:
|
||||||
|
```dart
|
||||||
|
static String cleanText(String text, bool forTts) {
|
||||||
|
// 1. 清理粗体/斜体标记 **text** *text*
|
||||||
|
// 2. 处理代码块 ```code```
|
||||||
|
// 3. 清理表格格式 |---|---|
|
||||||
|
// 4. 处理链接和图片 [text](url) 
|
||||||
|
// 5. 清理列表和引用 1. - * >
|
||||||
|
// 6. 规范化空白字符
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 数据流分析
|
||||||
|
|
||||||
|
### 完整对话流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[用户点击语音按钮] --> B[startVoiceInput]
|
||||||
|
B --> C[检查麦克风权限]
|
||||||
|
C --> D[开始录音/ASR]
|
||||||
|
D --> E[实时显示识别结果]
|
||||||
|
E --> F[用户松开按钮]
|
||||||
|
F --> G[stopAndProcessVoiceInput]
|
||||||
|
G --> H[调用reply处理文本]
|
||||||
|
H --> I[文本分类]
|
||||||
|
I --> J{分类结果}
|
||||||
|
J -->|车控命令| K[handleVehicleControl]
|
||||||
|
J -->|普通问答| L[answerQuestion - ChatSseService]
|
||||||
|
J -->|错误问题| M[answerWrongQuestion]
|
||||||
|
K --> N[解析车控命令]
|
||||||
|
N --> O[执行TTS播报]
|
||||||
|
O --> P[执行车控命令]
|
||||||
|
P --> Q[生成执行反馈]
|
||||||
|
Q --> R[更新UI显示]
|
||||||
|
L --> S[SSE流式对话]
|
||||||
|
S --> T[实时TTS播报]
|
||||||
|
T --> R
|
||||||
|
```
|
||||||
|
|
||||||
|
### 服务间协作关系
|
||||||
|
|
||||||
|
```
|
||||||
|
MessageService (核心协调器)
|
||||||
|
├── ChatSseService (AI对话)
|
||||||
|
│ └── LocalTtsService (流式TTS)
|
||||||
|
├── AudioRecorderService (音频录制)
|
||||||
|
├── VoiceRecognitionService (语音识别)
|
||||||
|
├── TextClassificationService (文本分类)
|
||||||
|
├── VehicleCommandService (车控解析)
|
||||||
|
│ └── CommandService (命令执行)
|
||||||
|
└── RedisService (状态缓存)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 状态管理流程
|
||||||
|
|
||||||
|
**MessageService状态变化**:
|
||||||
|
```
|
||||||
|
idle -> recording -> recognizing -> replying -> idle
|
||||||
|
```
|
||||||
|
|
||||||
|
**消息状态变化**:
|
||||||
|
```
|
||||||
|
listening -> normal -> thinking -> executing -> success/failure
|
||||||
|
```
|
||||||
|
|
||||||
|
**TTS任务状态**:
|
||||||
|
```
|
||||||
|
ready -> playing -> completed
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 配置与扩展
|
||||||
|
|
||||||
|
### 1. 插件初始化
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// 在主应用中初始化插件
|
||||||
|
ChatAssistantApp.initialize(
|
||||||
|
commandCallback: (VehicleCommandType type, Map<String, dynamic>? params) async {
|
||||||
|
// 实现具体的车控逻辑
|
||||||
|
return (true, {'message': '命令执行成功'});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 服务端接口配置
|
||||||
|
|
||||||
|
**当前硬编码的服务端地址**:
|
||||||
|
- 文本分类: `http://143.64.185.20:18606/classify`
|
||||||
|
- 车控解析: `http://143.64.185.20:18606/control`
|
||||||
|
- 控制反馈: `http://143.64.185.20:18606/control_resp`
|
||||||
|
- SSE对话: `http://143.64.185.20:18606/chat`
|
||||||
|
- 语音识别: `http://143.64.185.20:18606/voice`
|
||||||
|
- Redis缓存: `http://143.64.185.20:18606/redis/*`
|
||||||
|
|
||||||
|
**建议改进**: 将服务端地址配置化,支持动态配置
|
||||||
|
|
||||||
|
### 3. 资源文件引用
|
||||||
|
|
||||||
|
**图片资源**:
|
||||||
|
- 使用`package: 'ai_chat_assistant'`前缀
|
||||||
|
- 路径: `assets/images/`
|
||||||
|
|
||||||
|
**字体资源**:
|
||||||
|
- VWHead_Bold.otf
|
||||||
|
- VWHead_Regular.otf
|
||||||
|
|
||||||
|
## 🐛 已知问题与注意事项
|
||||||
|
|
||||||
|
### 1. 资源路径问题
|
||||||
|
当前资源文件引用可能存在路径问题,需要确保在example项目中正确配置package前缀。
|
||||||
|
|
||||||
|
### 2. 硬编码服务地址
|
||||||
|
服务端API地址硬编码,不利于部署和环境切换。
|
||||||
|
|
||||||
|
### 3. 错误处理
|
||||||
|
部分网络请求缺少完善的错误处理和重试机制。
|
||||||
|
|
||||||
|
### 4. 内存管理
|
||||||
|
长时间运行可能存在内存泄漏风险,需要关注:
|
||||||
|
- Completer的正确释放
|
||||||
|
- HTTP连接的及时关闭
|
||||||
|
- TTS任务队列的清理
|
||||||
|
|
||||||
|
### 5. 并发控制
|
||||||
|
- SSE连接的并发控制
|
||||||
|
- TTS播放队列的线程安全
|
||||||
|
- 语音识别状态的同步
|
||||||
|
|
||||||
|
## 🚀 扩展建议
|
||||||
|
|
||||||
|
### 1. 配置化改进
|
||||||
|
- 服务端地址配置化
|
||||||
|
- 支持多语言配置
|
||||||
|
- 主题自定义配置
|
||||||
|
- TTS引擎选择配置
|
||||||
|
|
||||||
|
### 2. 功能增强
|
||||||
|
- 添加离线语音识别支持
|
||||||
|
- 实现消息历史持久化
|
||||||
|
- 添加更多车控命令类型
|
||||||
|
- 支持自定义唤醒词
|
||||||
|
|
||||||
|
### 3. 性能优化
|
||||||
|
- 实现网络请求缓存
|
||||||
|
- 优化UI渲染性能
|
||||||
|
- 添加资源预加载
|
||||||
|
- TTS队列优化
|
||||||
|
|
||||||
|
### 4. 测试完善
|
||||||
|
- 添加单元测试覆盖所有服务
|
||||||
|
- 集成测试覆盖完整流程
|
||||||
|
- 性能测试工具
|
||||||
|
- 错误场景测试
|
||||||
|
|
||||||
|
### 5. 架构优化
|
||||||
|
- 依赖注入容器
|
||||||
|
- 事件总线机制
|
||||||
|
- 插件热重载支持
|
||||||
|
- 模块化拆分
|
||||||
|
|
||||||
|
## 📝 开发流程建议
|
||||||
|
|
||||||
|
### 1. 添加新功能
|
||||||
|
1. 在相应的枚举中定义新类型
|
||||||
|
2. 在models中添加数据模型
|
||||||
|
3. 在services中实现业务逻辑
|
||||||
|
4. 在widgets中创建UI组件
|
||||||
|
5. 更新主流程集成新功能
|
||||||
|
|
||||||
|
### 2. 调试技巧
|
||||||
|
- 使用`debugPrint`添加关键节点日志
|
||||||
|
- 通过Provider DevTools监控状态变化
|
||||||
|
- 使用Flutter Inspector检查UI结构
|
||||||
|
- 监控Method Channel通信日志
|
||||||
|
|
||||||
|
### 3. 集成测试
|
||||||
|
- 在example项目中测试完整流程
|
||||||
|
- 验证车控命令回调机制
|
||||||
|
- 测试不同场景下的错误处理
|
||||||
|
- 验证资源文件引用
|
||||||
|
|
||||||
|
## 💡 技术亮点总结
|
||||||
|
|
||||||
|
### 1. 流式处理架构
|
||||||
|
- SSE实时对话流
|
||||||
|
- TTS流式播报
|
||||||
|
- 有序任务队列管理
|
||||||
|
|
||||||
|
### 2. 智能语音处理
|
||||||
|
- 实时语音识别显示
|
||||||
|
- 中英文自动检测
|
||||||
|
- 智能分句算法
|
||||||
|
|
||||||
|
### 3. 解耦设计
|
||||||
|
- 插件与主应用解耦
|
||||||
|
- 服务间松耦合
|
||||||
|
- 回调机制灵活扩展
|
||||||
|
|
||||||
|
### 4. 状态管理
|
||||||
|
- 响应式UI更新
|
||||||
|
- 多层状态协调
|
||||||
|
- 异步状态同步
|
||||||
|
|
||||||
|
---
|
||||||
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 AI Chat Assistant
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
527
README.md
@@ -1,16 +1,523 @@
|
|||||||
# app003
|
# AI Chat Assistant Flutter Plugin
|
||||||
|
|
||||||
A new Flutter project.
|
一个功能丰富的 AI 聊天助手 Flutter 插件,支持语音识别、AI 对话、车控命令执行、语音合成等功能。
|
||||||
|
|
||||||
## Getting Started
|
## 📱 功能特性
|
||||||
|
|
||||||
This project is a starting point for a Flutter application.
|
- 🎤 **语音识别 (ASR)**: 支持实时语音转文字,自动停止检测
|
||||||
|
- 🤖 **AI 对话**: 基于 SSE (Server-Sent Events) 的流式 AI 对话
|
||||||
|
- 🚗 **车控命令**: 支持 20+ 种车辆控制命令(空调、车窗、车门、座椅加热等)
|
||||||
|
- 🔊 **语音合成 (TTS)**: 支持中英文语音播报
|
||||||
|
- 🎨 **精美 UI**: 浮动图标、波纹动画、渐变背景
|
||||||
|
- 📱 **多界面模式**: 支持全屏和部分屏幕聊天界面
|
||||||
|
- 🌐 **国际化**: 支持中英文切换
|
||||||
|
- 📊 **状态管理**: 基于 Provider 的状态管理
|
||||||
|
|
||||||
A few resources to get you started if this is your first Flutter project:
|
## 🏗️ 项目架构
|
||||||
|
|
||||||
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
### 📁 目录结构
|
||||||
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
|
||||||
|
|
||||||
For help getting started with Flutter development, view the
|
```
|
||||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
lib/
|
||||||
samples, guidance on mobile development, and a full API reference.
|
├── app.dart # 主应用入口
|
||||||
|
├── manager.dart # AI 聊天助手管理器
|
||||||
|
├── ai_chat_assistant.dart # 导出文件
|
||||||
|
├── bloc/ # 状态管理
|
||||||
|
│ ├── easy_bloc.dart # 自定义轻量级状态管理框架
|
||||||
|
│ ├── ai_chat_cubit.dart # AI 聊天命令状态管理
|
||||||
|
│ └── command_state.dart # 命令状态定义
|
||||||
|
├── enums/ # 枚举定义
|
||||||
|
│ ├── message_service_state.dart # 消息服务状态
|
||||||
|
│ ├── message_status.dart # 消息状态
|
||||||
|
│ └── vehicle_command_type.dart # 车控命令类型
|
||||||
|
├── models/ # 数据模型
|
||||||
|
│ ├── chat_message.dart # 聊天消息模型
|
||||||
|
│ ├── vehicle_cmd.dart # 车控命令模型
|
||||||
|
│ ├── vehicle_cmd_response.dart # 车控命令响应
|
||||||
|
│ └── vehicle_status_info.dart # 车辆状态信息
|
||||||
|
├── pages/ # 页面
|
||||||
|
│ └── full_screen.dart # 全屏聊天页面
|
||||||
|
├── screens/ # 界面页面
|
||||||
|
│ ├── main_screen.dart # 主界面
|
||||||
|
│ └── part_screen.dart # 部分屏聊天界面
|
||||||
|
├── services/ # 核心服务
|
||||||
|
│ ├── chat_sse_service.dart # SSE 聊天服务
|
||||||
|
│ ├── classification_service.dart # 分类服务
|
||||||
|
│ ├── command_service.dart # 车控命令服务
|
||||||
|
│ ├── control_recognition_service.dart # 控制识别服务
|
||||||
|
│ ├── location_service.dart # 位置服务
|
||||||
|
│ ├── message_service.dart # 消息管理服务
|
||||||
|
│ ├── redis_service.dart # Redis 服务
|
||||||
|
│ ├── tts_service.dart # 语音合成服务
|
||||||
|
│ ├── vehicle_state_service.dart # 车辆状态服务
|
||||||
|
│ └── voice_recognition_service.dart # 语音识别服务
|
||||||
|
├── themes/ # 主题样式
|
||||||
|
│ └── AppTheme.dart # 应用主题定义
|
||||||
|
├── utils/ # 工具类
|
||||||
|
│ ├── assets_util.dart # 资源工具
|
||||||
|
│ ├── common_util.dart # 通用工具
|
||||||
|
│ ├── tts_engine_manager.dart # TTS 引擎管理
|
||||||
|
│ └── tts_util.dart # TTS 工具
|
||||||
|
└── widgets/ # UI 组件
|
||||||
|
├── chat/ # 聊天相关组件
|
||||||
|
│ └── chat_window_content.dart # 聊天窗口内容
|
||||||
|
├── assistant_avatar.dart # AI 助手头像
|
||||||
|
├── chat_box.dart # 聊天框
|
||||||
|
├── chat_bubble.dart # 聊天气泡
|
||||||
|
├── chat_floating_icon.dart # 聊天浮动图标
|
||||||
|
├── chat_footer.dart # 聊天底部
|
||||||
|
├── chat_header.dart # 聊天头部
|
||||||
|
├── chat_popup.dart # 聊天弹出层
|
||||||
|
├── floating_icon.dart # 浮动图标
|
||||||
|
├── floating_icon_with_wave.dart # 带波纹的浮动图标
|
||||||
|
├── gradient_background.dart # 渐变背景
|
||||||
|
├── large_audio_wave.dart # 大音频波形
|
||||||
|
├── mini_audio_wave.dart # 小音频波形
|
||||||
|
├── rotating_image.dart # 旋转图片组件
|
||||||
|
├── voice_animation.dart # 语音动画
|
||||||
|
└── _hole_overlay.dart # 洞穴遮罩层
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🎯 核心架构设计
|
||||||
|
|
||||||
|
#### 1. 服务层架构 (Services Layer)
|
||||||
|
|
||||||
|
- **MessageService**: 核心消息管理服务,负责统一管理聊天消息、语音识别、AI 对话流程
|
||||||
|
- **ChatSseService**: SSE 流式通信服务,处理与 AI 后端的实时对话
|
||||||
|
- **CommandService**: 车控命令处理服务,提供车辆控制命令的回调机制
|
||||||
|
- **TTSService**: 语音合成服务,支持中英文语音播报
|
||||||
|
- **VoiceRecognitionService**: 语音识别服务,基于阿里云SDK处理音频转文字
|
||||||
|
|
||||||
|
#### 2. 状态管理架构 (State Management)
|
||||||
|
|
||||||
|
- **EasyBloc**: 轻量级状态管理框架,支持事件驱动和状态流
|
||||||
|
- **EasyCubit**: 简化的状态管理,用于单一状态变更
|
||||||
|
- **AIChatCommandCubit**: 专门处理车控命令的状态管理
|
||||||
|
- **MessageService**: 基于Provider的消息状态管理
|
||||||
|
|
||||||
|
#### 3. UI 层架构 (UI Layer)
|
||||||
|
|
||||||
|
- **ChatPopup**: 主要的聊天弹出层组件,集成浮动图标和聊天窗口
|
||||||
|
- **ChatFloatingIcon**: 可拖拽的浮动图标
|
||||||
|
- **ChatWindowContent**: 聊天窗口内容组件
|
||||||
|
- **全屏模式**: 完整的聊天界面,支持历史记录、操作按钮等
|
||||||
|
|
||||||
|
#### 4. 管理器模式 (Manager Pattern)
|
||||||
|
|
||||||
|
- **AIChatAssistantManager**: 单例管理器,负责整体协调
|
||||||
|
- **VehicleCommandHandler**: 抽象的车控命令处理器,支持自定义实现
|
||||||
|
|
||||||
|
## 🚗 支持的车控命令
|
||||||
|
|
||||||
|
| 命令类型 | 中文描述 | 英文描述 |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| lock | 上锁车门 | lock |
|
||||||
|
| unlock | 解锁车门 | unlock |
|
||||||
|
| openWindow | 打开车窗 | open window |
|
||||||
|
| closeWindow | 关闭车窗 | close window |
|
||||||
|
| appointAC | 预约空调 | appoint AC |
|
||||||
|
| openAC | 打开空调 | open AC |
|
||||||
|
| closeAC | 关闭空调 | close AC |
|
||||||
|
| changeACTemp | 修改空调温度 | change AC temperature |
|
||||||
|
| coolSharply | 极速降温 | cool sharply |
|
||||||
|
| prepareCar | 一键备车 | prepare car |
|
||||||
|
| meltSnow | 一键融雪 | melt snow |
|
||||||
|
| openTrunk | 打开后备箱 | open trunk |
|
||||||
|
| closeTrunk | 关闭后备箱 | close trunk |
|
||||||
|
| honk | 鸣笛 | honk |
|
||||||
|
| locateCar | 定位车辆 | locate car |
|
||||||
|
| openWheelHeat | 开启方向盘加热 | open wheel heat |
|
||||||
|
| closeWheelHeat | 关闭方向盘加热 | close wheel heat |
|
||||||
|
| openMainSeatHeat | 开启主座椅加热 | open main seat heat |
|
||||||
|
| closeMainSeatHeat | 关闭主座椅加热 | close main seat heat |
|
||||||
|
| openMinorSeatHeat | 开启副座椅加热 | open minor seat heat |
|
||||||
|
| closeMinorSeatHeat | 关闭副座椅加热 | close minor seat heat |
|
||||||
|
|
||||||
|
## 🔧 技术栈
|
||||||
|
|
||||||
|
### 核心依赖
|
||||||
|
|
||||||
|
- **Flutter SDK**: ^3.5.0
|
||||||
|
- **Provider**: ^6.1.5 - 状态管理
|
||||||
|
- **HTTP**: ^1.4.0 - 网络请求
|
||||||
|
- **Record**: ^6.0.0 - 音频录制
|
||||||
|
- **AudioPlayers**: ^5.2.1 - 音频播放
|
||||||
|
- **Flutter TTS**: ^4.2.0 - 语音合成
|
||||||
|
- **Permission Handler**: ^12.0.0 - 权限管理
|
||||||
|
- **UUID**: ^3.0.5 - 唯一标识符生成
|
||||||
|
- **Flutter Markdown**: ^0.7.7+1 - Markdown 渲染
|
||||||
|
- **FlutterToast**: ^8.2.12 - 消息提示
|
||||||
|
- **Path Provider**: ^2.1.5 - 文件路径管理
|
||||||
|
- **Meta**: ^1.15.0 - 元数据注解
|
||||||
|
|
||||||
|
### 自定义包
|
||||||
|
|
||||||
|
- **basic_intl**: 基础国际化支持包
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 1. 添加依赖
|
||||||
|
|
||||||
|
在您的 `pubspec.yaml` 文件中添加:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
dependencies:
|
||||||
|
ai_chat_assistant:
|
||||||
|
path: path/to/ai_chat_assistant
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 权限配置
|
||||||
|
|
||||||
|
#### Android 权限 (android/app/src/main/AndroidManifest.xml)
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 基础集成
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:ai_chat_assistant/ai_chat_assistant.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
|
// 实现车控命令处理器
|
||||||
|
class MyVehicleCommandHandler extends VehicleCommandHandler {
|
||||||
|
@override
|
||||||
|
Future<(bool, Map<String, dynamic>?)> executeCommand(VehicleCommand command) async {
|
||||||
|
// 处理车控命令的业务逻辑
|
||||||
|
print('收到车控命令: ${command.type}, 参数: ${command.params}');
|
||||||
|
|
||||||
|
// 更新命令状态为执行中
|
||||||
|
commandCubit?.emit(AIChatCommandState(
|
||||||
|
commandId: command.commandId,
|
||||||
|
commandType: command.type,
|
||||||
|
params: command.params,
|
||||||
|
status: AIChatCommandStatus.executing,
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
));
|
||||||
|
|
||||||
|
// 模拟命令执行
|
||||||
|
await Future.delayed(const Duration(seconds: 2));
|
||||||
|
|
||||||
|
// 更新命令状态为成功
|
||||||
|
commandCubit?.emit(AIChatCommandState(
|
||||||
|
commandId: command.commandId,
|
||||||
|
commandType: command.type,
|
||||||
|
params: command.params,
|
||||||
|
status: AIChatCommandStatus.success,
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
result: {'message': '命令执行成功'},
|
||||||
|
));
|
||||||
|
|
||||||
|
// 返回执行结果
|
||||||
|
return (true, {'message': '命令已执行'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
// 请求麦克风权限
|
||||||
|
if (!await Permission.microphone.isGranted) {
|
||||||
|
await Permission.microphone.request();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化 AI Chat Assistant,注册车控命令处理器
|
||||||
|
AIChatAssistantManager.instance.setupCommandHandle(
|
||||||
|
commandHandler: MyVehicleCommandHandler(),
|
||||||
|
);
|
||||||
|
|
||||||
|
runApp(
|
||||||
|
ChangeNotifierProvider(
|
||||||
|
create: (_) => MessageService(),
|
||||||
|
child: const MyApp(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyApp extends StatelessWidget {
|
||||||
|
const MyApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
title: 'My Car App',
|
||||||
|
home: const ChatAssistantApp(), // 直接使用 AI 助手
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 自定义集成
|
||||||
|
|
||||||
|
如果您想在现有应用中集成聊天助手,可以使用聊天弹出层:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:ai_chat_assistant/widgets/chat_popup.dart';
|
||||||
|
|
||||||
|
class MyHomePage extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
// 您的现有 UI
|
||||||
|
YourExistingWidget(),
|
||||||
|
|
||||||
|
// AI 助手聊天弹出层
|
||||||
|
const ChatPopup(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📖 示例项目
|
||||||
|
|
||||||
|
项目包含完整的示例应用,位于 `example/` 目录下:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd example
|
||||||
|
flutter run
|
||||||
|
```
|
||||||
|
|
||||||
|
示例展示了:
|
||||||
|
- 基础集成方法
|
||||||
|
- 车控命令处理
|
||||||
|
- 自定义 UI 主题
|
||||||
|
- 权限管理
|
||||||
|
|
||||||
|
## 🎨 UI 组件
|
||||||
|
|
||||||
|
### 聊天弹出层 (ChatPopup)
|
||||||
|
|
||||||
|
集成式聊天弹出层组件,包含:
|
||||||
|
- 浮动图标 (ChatFloatingIcon)
|
||||||
|
- 聊天窗口 (ChatWindowContent)
|
||||||
|
- 拖拽定位
|
||||||
|
- 动画效果
|
||||||
|
|
||||||
|
### 浮动图标相关组件
|
||||||
|
|
||||||
|
- **ChatFloatingIcon**: 专用聊天浮动图标
|
||||||
|
- **FloatingIcon**: 通用浮动图标
|
||||||
|
- **FloatingIconWithWave**: 带波纹效果的浮动图标
|
||||||
|
|
||||||
|
### 聊天界面组件
|
||||||
|
|
||||||
|
- **ChatWindowContent**: 聊天窗口内容组件
|
||||||
|
- **ChatBubble**: 聊天气泡组件
|
||||||
|
- **ChatBox**: 聊天输入框
|
||||||
|
- **ChatHeader**: 聊天头部
|
||||||
|
- **ChatFooter**: 聊天底部操作栏
|
||||||
|
- **AssistantAvatar**: AI 助手头像
|
||||||
|
|
||||||
|
### 动画组件
|
||||||
|
|
||||||
|
- **VoiceAnimation**: 语音动画组件
|
||||||
|
- **LargeAudioWave**: 大音频波形动画
|
||||||
|
- **MiniAudioWave**: 小音频波形动画
|
||||||
|
- **RotatingImage**: 旋转图片组件
|
||||||
|
- **GradientBackground**: 渐变背景
|
||||||
|
|
||||||
|
## ⚙️ 配置选项
|
||||||
|
|
||||||
|
### 主题自定义
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// 在 AppTheme.dart 中自定义主题
|
||||||
|
class AppTheme {
|
||||||
|
static ThemeData get lightTheme => ThemeData(
|
||||||
|
// 自定义浅色主题
|
||||||
|
);
|
||||||
|
|
||||||
|
static ThemeData get darkTheme => ThemeData(
|
||||||
|
// 自定义深色主题
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 资源文件
|
||||||
|
|
||||||
|
确保在 `pubspec.yaml` 中包含必要的资源:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
flutter:
|
||||||
|
assets:
|
||||||
|
- assets/images/
|
||||||
|
fonts:
|
||||||
|
- family: VWHead_Bold
|
||||||
|
fonts:
|
||||||
|
- asset: assets/fonts/VWHead-Bold.otf
|
||||||
|
- family: VWHead_Regular
|
||||||
|
fonts:
|
||||||
|
- asset: assets/fonts/VWHead-Regular.otf
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔌 Native 平台集成
|
||||||
|
|
||||||
|
### Android 平台
|
||||||
|
|
||||||
|
项目包含 Android 原生代码,支持:
|
||||||
|
- ASR (自动语音识别) 功能,基于阿里云SDK
|
||||||
|
- 原生音频处理
|
||||||
|
- 系统权限管理
|
||||||
|
|
||||||
|
### 阿里云SDK集成
|
||||||
|
|
||||||
|
使用阿里云语音识别SDK,配置文件在:
|
||||||
|
- `android/libs/nui-release-1.0.0.aar`
|
||||||
|
- `android/libs/fastjson-1.1.46.android.jar`
|
||||||
|
|
||||||
|
### 插件架构
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
plugin:
|
||||||
|
platforms:
|
||||||
|
android:
|
||||||
|
package: com.example.ai_assistant_plugin
|
||||||
|
pluginClass: AiAssistantPlugin
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 API 文档
|
||||||
|
|
||||||
|
### AIChatAssistantManager
|
||||||
|
|
||||||
|
核心管理器(单例模式):
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class AIChatAssistantManager {
|
||||||
|
static final instance = AIChatAssistantManager._internal();
|
||||||
|
|
||||||
|
// 设置命令处理器
|
||||||
|
void setupCommandHandle({required VehicleCommandHandler commandHandler});
|
||||||
|
|
||||||
|
// 获取命令状态流
|
||||||
|
Stream<AIChatCommandState> get commandStateStream;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### VehicleCommandHandler
|
||||||
|
|
||||||
|
车控命令处理器抽象类:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
abstract class VehicleCommandHandler {
|
||||||
|
AIChatCommandCubit? commandCubit;
|
||||||
|
|
||||||
|
// 执行车辆控制命令
|
||||||
|
Future<(bool, Map<String, dynamic>?)> executeCommand(VehicleCommand command);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### MessageService
|
||||||
|
|
||||||
|
核心消息管理服务:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class MessageService extends ChangeNotifier {
|
||||||
|
// 发送消息
|
||||||
|
Future<void> sendMessage(String text);
|
||||||
|
|
||||||
|
// 开始语音识别
|
||||||
|
Future<void> startVoiceRecognition();
|
||||||
|
|
||||||
|
// 停止语音识别
|
||||||
|
void stopVoiceRecognition();
|
||||||
|
|
||||||
|
// 获取聊天历史
|
||||||
|
List<ChatMessage> get messages;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### EasyBloc / EasyCubit
|
||||||
|
|
||||||
|
轻量级状态管理框架:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// 基于事件的状态管理
|
||||||
|
class MyBloc extends EasyBloc<MyEvent, MyState> {
|
||||||
|
MyBloc(MyState initialState) : super(initialState);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void _mapEventToState(MyEvent event) {
|
||||||
|
// 处理事件并发射新状态
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简化的状态管理
|
||||||
|
class MyCubit extends EasyCubit<MyState> {
|
||||||
|
MyCubit(MyState initialState) : super(initialState);
|
||||||
|
|
||||||
|
void updateState(MyState newState) {
|
||||||
|
emit(newState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐛 调试和故障排除
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
1. **麦克风权限问题**
|
||||||
|
- 确保在 AndroidManifest.xml 中添加了录音权限
|
||||||
|
- 在应用启动时请求权限
|
||||||
|
|
||||||
|
2. **网络连接问题**
|
||||||
|
- 检查网络权限配置
|
||||||
|
- 确认 SSE 服务端地址配置正确
|
||||||
|
|
||||||
|
3. **TTS 播放问题**
|
||||||
|
- 检查音频播放权限
|
||||||
|
- 确认设备支持 TTS 功能
|
||||||
|
|
||||||
|
### 日志调试
|
||||||
|
|
||||||
|
启用详细日志:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// 在 main.dart 中启用调试日志
|
||||||
|
void main() {
|
||||||
|
debugPrint('AI Chat Assistant 启动中...');
|
||||||
|
// ... 其他初始化代码
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📄 许可证
|
||||||
|
|
||||||
|
本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。
|
||||||
|
|
||||||
|
## 🔗 相关链接
|
||||||
|
|
||||||
|
- [Flutter 官方文档](https://flutter.dev/docs)
|
||||||
|
- [Provider 状态管理](https://pub.dev/packages/provider)
|
||||||
|
- [Flutter TTS](https://pub.dev/packages/flutter_tts)
|
||||||
|
- [Record 插件](https://pub.dev/packages/record)
|
||||||
|
- [阿里云语音识别SDK](https://ai.aliyun.com/nls)
|
||||||
|
|
||||||
|
## 📋 更新日志
|
||||||
|
|
||||||
|
### v1.0.0+1 (2025-09-23)
|
||||||
|
|
||||||
|
#### 新增功能
|
||||||
|
- 🎯 新增轻量级状态管理框架 (EasyBloc/EasyCubit)
|
||||||
|
- 🔧 新增 AIChatAssistantManager 单例管理器
|
||||||
|
- 📦 新增 ChatPopup 集成组件
|
||||||
|
- 🎨 新增多个动画组件 (VoiceAnimation, MiniAudioWave, RotatingImage 等)
|
||||||
|
- 💾 集成阿里云语音识别SDK
|
||||||
|
|
||||||
|
#### 架构改进
|
||||||
|
- 🏗️ 采用管理器模式替代回调模式
|
||||||
|
- 🔄 支持面向对象的车控命令处理器
|
||||||
|
- 📱 优化聊天界面组件结构
|
||||||
|
- 🎵 改进语音识别和TTS服务
|
||||||
|
|
||||||
|
#### 技术栈更新
|
||||||
|
- 🔧 添加 Meta 注解支持
|
||||||
|
- 🌐 保持与 basic_intl 国际化包的兼容
|
||||||
|
|||||||
127
README_ChatPopup.md
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
# ChatFloatingIcon 和 ChatPopup 组件说明
|
||||||
|
|
||||||
|
这两个组件用于替代原来的 `FloatingIcon`,解决了在 Stack 中会被上层遮挡的问题。
|
||||||
|
|
||||||
|
## 组件结构
|
||||||
|
|
||||||
|
### ChatFloatingIcon
|
||||||
|
可拖拽的悬浮图标组件,支持:
|
||||||
|
- 自定义图标和大小
|
||||||
|
- 拖拽功能(自动吸附屏幕边缘)
|
||||||
|
- 长按开始语音输入
|
||||||
|
- 点击进入全屏聊天
|
||||||
|
- 多种动画效果
|
||||||
|
|
||||||
|
### ChatPopup
|
||||||
|
基于 Overlay 的弹窗容器,负责:
|
||||||
|
- 管理 PartScreen 的显示/隐藏
|
||||||
|
- 根据图标位置自动计算弹窗位置
|
||||||
|
- 处理背景点击关闭
|
||||||
|
- 动画效果管理
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 基本使用(默认AI图标)
|
||||||
|
```dart
|
||||||
|
ChatPopup(
|
||||||
|
child: ChatFloatingIcon(
|
||||||
|
iconSize: 80, // 可选,默认80
|
||||||
|
),
|
||||||
|
),
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自定义图标
|
||||||
|
```dart
|
||||||
|
ChatPopup(
|
||||||
|
child: ChatFloatingIcon(
|
||||||
|
iconSize: 60,
|
||||||
|
icon: Container(
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.blue,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.chat, color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
```
|
||||||
|
|
||||||
|
### 添加自定义回调
|
||||||
|
```dart
|
||||||
|
ChatPopup(
|
||||||
|
child: ChatFloatingIcon(
|
||||||
|
iconSize: 70,
|
||||||
|
onTap: () {
|
||||||
|
// 自定义点击事件
|
||||||
|
},
|
||||||
|
onPositionChanged: (position) {
|
||||||
|
// 位置变化回调
|
||||||
|
},
|
||||||
|
onDragStart: () {
|
||||||
|
// 拖拽开始
|
||||||
|
},
|
||||||
|
onDragEnd: () {
|
||||||
|
// 拖拽结束
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
```
|
||||||
|
|
||||||
|
## 参数说明
|
||||||
|
|
||||||
|
### ChatFloatingIcon 参数
|
||||||
|
- `iconSize`: 图标大小,默认80
|
||||||
|
- `icon`: 自定义图标Widget,不传则使用默认AI图标
|
||||||
|
- `onTap`: 点击回调,默认进入全屏聊天
|
||||||
|
- `onLongPress`: 长按开始回调,默认开始语音输入
|
||||||
|
- `onLongPressUp`: 长按结束回调,默认结束语音输入
|
||||||
|
- `onLongPressEnd`: 长按结束详细回调
|
||||||
|
- `onDragStart`: 拖拽开始回调
|
||||||
|
- `onDragEnd`: 拖拽结束回调
|
||||||
|
- `onPositionChanged`: 位置变化回调
|
||||||
|
|
||||||
|
### ChatPopup 参数
|
||||||
|
- `child`: 子组件,通常是ChatFloatingIcon
|
||||||
|
- `chatSize`: 聊天框大小,默认320x450
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
1. **避免遮挡**: 使用 Overlay 显示弹窗,不会被 Stack 上层遮挡
|
||||||
|
2. **位置自适应**: 根据图标位置自动计算弹窗显示位置
|
||||||
|
3. **保持原有逻辑**: 与原 FloatingIcon 功能完全兼容
|
||||||
|
4. **灵活自定义**: 支持自定义图标、回调等
|
||||||
|
5. **动画丰富**: 保持原有的各种动画效果
|
||||||
|
|
||||||
|
## 迁移指南
|
||||||
|
|
||||||
|
### 从 FloatingIcon 迁移
|
||||||
|
原来的代码:
|
||||||
|
```dart
|
||||||
|
Stack(
|
||||||
|
children: [
|
||||||
|
// 其他内容
|
||||||
|
FloatingIcon(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
新的代码:
|
||||||
|
```dart
|
||||||
|
Stack(
|
||||||
|
children: [
|
||||||
|
// 其他内容
|
||||||
|
ChatPopup(
|
||||||
|
child: ChatFloatingIcon(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. ChatPopup 必须在有 Overlay 的 Widget 树中使用(通常是 MaterialApp 下)
|
||||||
|
2. 如果要自定义图标,建议保持圆形设计以匹配拖拽动画
|
||||||
|
3. 长按语音输入功能需要 MessageService 正确配置
|
||||||
|
4. 位置计算基于屏幕坐标,确保在正确的上下文中使用
|
||||||
2
android/.gitignore
vendored
@@ -12,3 +12,5 @@ GeneratedPluginRegistrant.java
|
|||||||
key.properties
|
key.properties
|
||||||
**/*.keystore
|
**/*.keystore
|
||||||
**/*.jks
|
**/*.jks
|
||||||
|
/build/
|
||||||
|
/src/build/
|
||||||
|
|||||||
112
android/build.gradle
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
group 'com.example.ai_chat_assistant'
|
||||||
|
version '1.0-SNAPSHOT'
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
ext.kotlin_version = '2.1.20'
|
||||||
|
repositories {
|
||||||
|
maven { url file("mavenLocal") }
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
maven {
|
||||||
|
url "https://storage.googleapis.com/download.flutter.io"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.android.tools.build:gradle:8.11.1'
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
maven { url file("mavenLocal") }
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
maven {
|
||||||
|
url "https://storage.googleapis.com/download.flutter.io"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'com.android.library'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace 'com.example.ai_chat_assistant'
|
||||||
|
|
||||||
|
|
||||||
|
compileSdk 33
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
main.java.srcDirs += 'src/main/kotlin'
|
||||||
|
test.java.srcDirs += 'src/test/kotlin'
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion 21
|
||||||
|
ndk {
|
||||||
|
abiFilters "armeabi-v7a", "arm64-v8a"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
dataBinding true
|
||||||
|
viewBinding = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取配置文件
|
||||||
|
def localProperties = new Properties()
|
||||||
|
def localPropertiesFile = rootProject.file('local.properties')
|
||||||
|
if (localPropertiesFile.exists()) {
|
||||||
|
localPropertiesFile.withReader('UTF-8') {
|
||||||
|
localProperties.load(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取flutter sdk 路径
|
||||||
|
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
||||||
|
if (flutterRoot == null) {
|
||||||
|
throw new GradleException("Flutter SDK not found. Define location with flutter.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找到 flutter.jar 文件
|
||||||
|
def flutterJar = new File(flutterRoot, "bin/cache/artifacts/engine/android-arm/flutter.jar")
|
||||||
|
if (!flutterJar.exists()) {
|
||||||
|
// 尝试其他路径
|
||||||
|
flutterJar = new File(flutterRoot, "bin/cache/artifacts/engine/android-arm-release/flutter.jar")
|
||||||
|
}
|
||||||
|
if (!flutterJar.exists()) {
|
||||||
|
// Windows 上可能在这个位置
|
||||||
|
flutterJar = new File(flutterRoot, "bin/cache/artifacts/engine/android-x64/flutter.jar")
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
flatDir {
|
||||||
|
dirs("libs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation 'com.alibaba.idst:nui-release:1.0.0'
|
||||||
|
implementation 'com.alibaba:fastjson:1.2.83'
|
||||||
|
// 使用Flutter的官方Maven依赖
|
||||||
|
// compileOnly 'io.flutter:flutter_embedding_debug:1.0.0-3.27.4'
|
||||||
|
// 或者使用release版本
|
||||||
|
// compileOnly '"io.flutter:flutter_embedding_release:1.0.0-de555ebd6cfe3e606a101b9ae45bbf5e62d4e4e1"'
|
||||||
|
// compileOnly files("$flutterRoot/bin/cache/artifacts/engine/android-arm/flutter.jar")
|
||||||
|
compileOnly files(files("libs/flutter.jar"))
|
||||||
|
// implementation(files("libs/fastjson-1.1.46.android.jar"))
|
||||||
|
// implementation(files("libs/nui-release-1.0.0.aar"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://mirrors.aliyun.com/macports/distfiles/gradle/gradle-8.12-all.zip
|
distributionUrl=https\://mirrors.aliyun.com/macports/distfiles/gradle/gradle-8.13-all.zip
|
||||||
|
|||||||
BIN
android/libs/flutter.jar
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<groupId>com.alibaba.idst</groupId>
|
||||||
|
<artifactId>nui-release</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<packaging>aar</packaging>
|
||||||
|
</project>
|
||||||
1
android/settings.gradle
Normal file
@@ -0,0 +1 @@
|
|||||||
|
rootProject.name = 'ai_assistant_plugin'
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
pluginManagement {
|
|
||||||
val flutterSdkPath = run {
|
|
||||||
val properties = java.util.Properties()
|
|
||||||
file("local.properties").inputStream().use { properties.load(it) }
|
|
||||||
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
|
||||||
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
|
||||||
flutterSdkPath
|
|
||||||
}
|
|
||||||
|
|
||||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
gradlePluginPortal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
plugins {
|
|
||||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
|
||||||
id("com.android.application") version "8.7.3" apply false
|
|
||||||
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
|
||||||
}
|
|
||||||
|
|
||||||
include(":app")
|
|
||||||
4
android/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
</manifest>
|
||||||
@@ -1,32 +1,32 @@
|
|||||||
package com.example.ai_chat_assistant;
|
package com.example.ai_assistant_plugin;
|
||||||
|
|
||||||
import android.media.AudioFormat;
|
|
||||||
import android.media.AudioRecord;
|
|
||||||
import android.media.MediaRecorder;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.HandlerThread;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity;
|
|
||||||
import io.flutter.embedding.engine.FlutterEngine;
|
|
||||||
import io.flutter.plugin.common.MethodChannel;
|
|
||||||
|
|
||||||
import com.alibaba.fastjson.JSON;
|
|
||||||
import com.alibaba.fastjson.JSONException;
|
|
||||||
import com.alibaba.fastjson.JSONObject;
|
|
||||||
import com.alibaba.idst.nui.AsrResult;
|
|
||||||
import com.alibaba.idst.nui.Constants;
|
|
||||||
import com.alibaba.idst.nui.INativeNuiCallback;
|
|
||||||
import com.alibaba.idst.nui.INativeStreamInputTtsCallback;
|
|
||||||
import com.alibaba.idst.nui.KwsResult;
|
|
||||||
import com.alibaba.idst.nui.NativeNui;
|
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public class MainActivity extends FlutterActivity implements INativeNuiCallback {
|
import io.flutter.embedding.engine.plugins.FlutterPlugin;
|
||||||
private static final String TTS_CHANNEL = "com.example.ai_chat_assistant/tts";
|
import io.flutter.plugin.common.MethodCall;
|
||||||
private static final String ASR_CHANNEL = "com.example.ai_chat_assistant/asr";
|
import io.flutter.plugin.common.MethodChannel;
|
||||||
|
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
|
||||||
|
import io.flutter.plugin.common.MethodChannel.Result;
|
||||||
|
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.HandlerThread;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONException;
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import com.alibaba.idst.nui.Constants;
|
||||||
|
import com.alibaba.idst.nui.INativeStreamInputTtsCallback;
|
||||||
|
import com.alibaba.idst.nui.NativeNui;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AiAssistantPlugin
|
||||||
|
*/
|
||||||
|
public class AiAssistantPlugin implements FlutterPlugin, MethodCallHandler {
|
||||||
|
|
||||||
|
private static final String CHANNEL = "com.example.ai_chat_assistant/ali_sdk";
|
||||||
private static final String TAG = "AliyunSDK";
|
private static final String TAG = "AliyunSDK";
|
||||||
private static final String APP_KEY = "bXFFc1V65iYbW6EF";
|
private static final String APP_KEY = "bXFFc1V65iYbW6EF";
|
||||||
private static final String ACCESS_KEY = "LTAI5t71JHxXRvt2mGuEVz9X";
|
private static final String ACCESS_KEY = "LTAI5t71JHxXRvt2mGuEVz9X";
|
||||||
@@ -35,7 +35,17 @@ public class MainActivity extends FlutterActivity implements INativeNuiCallback
|
|||||||
private static final String chineseVoice = "zhitian_emo";
|
private static final String chineseVoice = "zhitian_emo";
|
||||||
private static final String englishVoice = "abby";
|
private static final String englishVoice = "abby";
|
||||||
private final NativeNui streamInputTtsInstance = new NativeNui(Constants.ModeType.MODE_STREAM_INPUT_TTS);
|
private final NativeNui streamInputTtsInstance = new NativeNui(Constants.ModeType.MODE_STREAM_INPUT_TTS);
|
||||||
|
|
||||||
|
// asr 是否初始化
|
||||||
|
private boolean isAsrInitialized = false;
|
||||||
|
|
||||||
|
// asr property
|
||||||
|
private MethodChannel channel;
|
||||||
|
// asr instance
|
||||||
private final NativeNui asrInstance = new NativeNui();
|
private final NativeNui asrInstance = new NativeNui();
|
||||||
|
private Handler asrHandler;
|
||||||
|
private AsrCallBack asrCallBack;
|
||||||
|
private Handler handler = new Handler(Looper.getMainLooper());
|
||||||
|
|
||||||
private final AudioPlayer ttsAudioTrack = new AudioPlayer(new AudioPlayerCallback() {
|
private final AudioPlayer ttsAudioTrack = new AudioPlayer(new AudioPlayerCallback() {
|
||||||
@Override
|
@Override
|
||||||
@@ -53,137 +63,94 @@ public class MainActivity extends FlutterActivity implements INativeNuiCallback
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
private MethodChannel asrMethodChannel;
|
void initAsrInstance() {
|
||||||
private final static int ASR_SAMPLE_RATE = 16000;
|
if (!isAsrInitialized) {
|
||||||
private final static int ASR_WAVE_FRAM_SIZE = 20 * 2 * 1 * ASR_SAMPLE_RATE / 1000; //20ms audio for 16k/16bit/mono
|
Log.i(TAG, "Initializing ASR for the first time...");
|
||||||
private AudioRecord asrAudioRecorder = null;
|
asrCallBack = new AsrCallBack(channel);
|
||||||
private boolean asrStopping = false;
|
int initResult = asrInstance.initialize(asrCallBack, genAsrInitParams(),
|
||||||
private Handler asrHandler;
|
Constants.LogLevel.LOG_LEVEL_NONE, false);
|
||||||
private String asrText = "";
|
Log.i(TAG, "ASR initialization result: " + initResult);
|
||||||
|
if (initResult != Constants.NuiResultCode.SUCCESS) {
|
||||||
@Override
|
Log.i(TAG, "ASR initialization failed with error code: " + initResult);
|
||||||
public void configureFlutterEngine(FlutterEngine flutterEngine) {
|
return;
|
||||||
super.configureFlutterEngine(flutterEngine);
|
|
||||||
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), TTS_CHANNEL)
|
|
||||||
.setMethodCallHandler((call, result) -> {
|
|
||||||
Map<String, Object> args = (Map<String, Object>) call.arguments;
|
|
||||||
switch (call.method) {
|
|
||||||
case "startTts":
|
|
||||||
Object isChinese = args.get("isChinese");
|
|
||||||
if (isChinese == null || isChinese.toString().isBlank()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
boolean isSuccess = startTts(Boolean.parseBoolean(isChinese.toString()));
|
|
||||||
result.success(isSuccess);
|
|
||||||
break;
|
|
||||||
case "sendTts":
|
|
||||||
Object textArg = args.get("text");
|
|
||||||
if (textArg == null || textArg.toString().isBlank()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sendTts(textArg.toString());
|
|
||||||
break;
|
|
||||||
case "completeTts":
|
|
||||||
completeTts();
|
|
||||||
break;
|
|
||||||
case "stopTts":
|
|
||||||
stopTts();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
result.notImplemented();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
asrMethodChannel = new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), ASR_CHANNEL);
|
|
||||||
asrMethodChannel.setMethodCallHandler((call, result) -> {
|
|
||||||
switch (call.method) {
|
|
||||||
case "startAsr":
|
|
||||||
startAsr();
|
|
||||||
break;
|
|
||||||
case "stopAsr":
|
|
||||||
stopAsr();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
result.notImplemented();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
});
|
isAsrInitialized = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
public void onAttachedToEngine(FlutterPluginBinding flutterPluginBinding) {
|
||||||
super.onCreate(savedInstanceState);
|
channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), CHANNEL);
|
||||||
|
channel.setMethodCallHandler(this);
|
||||||
HandlerThread asrHandlerThread = new HandlerThread("process_thread");
|
HandlerThread asrHandlerThread = new HandlerThread("process_thread");
|
||||||
asrHandlerThread.start();
|
asrHandlerThread.start();
|
||||||
asrHandler = new Handler(asrHandlerThread.getLooper());
|
asrHandler = new Handler(asrHandlerThread.getLooper());
|
||||||
|
// 不能在这里初始化 asr, 初始化之后会占用麦克风
|
||||||
|
// asrCallBack = new AsrCallBack(channel);
|
||||||
|
// asrInstance.initialize(asrCallBack, genAsrInitParams(),
|
||||||
|
// Constants.LogLevel.LOG_LEVEL_NONE, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onStart() {
|
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
|
||||||
Log.i(TAG, "onStart");
|
channel.setMethodCallHandler(null);
|
||||||
super.onStart();
|
|
||||||
asrInstance.initialize(this, genAsrInitParams(),
|
|
||||||
Constants.LogLevel.LOG_LEVEL_NONE, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStop() {
|
|
||||||
Log.i(TAG, "onStop");
|
|
||||||
super.onStop();
|
|
||||||
|
|
||||||
asrInstance.release();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
ttsAudioTrack.stop();
|
ttsAudioTrack.stop();
|
||||||
ttsAudioTrack.releaseAudioTrack();
|
ttsAudioTrack.releaseAudioTrack();
|
||||||
streamInputTtsInstance.stopStreamInputTts();
|
streamInputTtsInstance.stopStreamInputTts();
|
||||||
|
streamInputTtsInstance.release();
|
||||||
|
|
||||||
|
if (isAsrInitialized) {
|
||||||
|
asrInstance.release();
|
||||||
|
if (asrCallBack != null) {
|
||||||
|
asrCallBack.release();
|
||||||
|
asrCallBack = null;
|
||||||
|
}
|
||||||
|
isAsrInitialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// asrInstance.release();
|
||||||
|
// if (asrCallBack != null) {
|
||||||
|
// asrCallBack.release();
|
||||||
|
// asrCallBack = null;
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean startTts(boolean isChinese) {
|
@Override
|
||||||
int ret = streamInputTtsInstance.startStreamInputTts(new INativeStreamInputTtsCallback() {
|
public void onMethodCall(MethodCall call, @NonNull Result result) {
|
||||||
@Override
|
Map<String, Object> args = call.arguments();
|
||||||
public void onStreamInputTtsEventCallback(INativeStreamInputTtsCallback.StreamInputTtsEvent event, String task_id, String session_id, int ret_code, String error_msg, String timestamp, String all_response) {
|
switch (call.method) {
|
||||||
Log.i(TAG, "stream input tts event(" + event + ") session id(" + session_id + ") task id(" + task_id + ") retCode(" + ret_code + ") errMsg(" + error_msg + ")");
|
case "startTts":
|
||||||
if (event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_SYNTHESIS_STARTED) {
|
assert args != null;
|
||||||
Log.i(TAG, "STREAM_INPUT_TTS_EVENT_SYNTHESIS_STARTED");
|
Object isChinese = args.get("isChinese");
|
||||||
ttsAudioTrack.play();
|
if (isChinese == null || isChinese.toString().isBlank()) {
|
||||||
Log.i(TAG, "start play");
|
return;
|
||||||
} else if (event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_SENTENCE_SYNTHESIS) {
|
|
||||||
Log.i(TAG, "STREAM_INPUT_TTS_EVENT_SENTENCE_SYNTHESIS:" + timestamp);
|
|
||||||
} else if (event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_SYNTHESIS_COMPLETE || event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_TASK_FAILED) {
|
|
||||||
/*
|
|
||||||
* 提示: STREAM_INPUT_TTS_EVENT_SYNTHESIS_COMPLETE事件表示TTS已经合成完并通过回调传回了所有音频数据, 而不是表示播放器已经播放完了所有音频数据。
|
|
||||||
*/
|
|
||||||
Log.i(TAG, "play end");
|
|
||||||
|
|
||||||
// 表示推送完数据, 当播放器播放结束则会有playOver回调
|
|
||||||
ttsAudioTrack.isFinishSend(true);
|
|
||||||
|
|
||||||
if (event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_TASK_FAILED) {
|
|
||||||
Log.e(TAG, "STREAM_INPUT_TTS_EVENT_TASK_FAILED: " + "error_code(" + ret_code + ") error_message(" + error_msg + ")");
|
|
||||||
}
|
|
||||||
} else if (event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_SENTENCE_BEGIN) {
|
|
||||||
Log.i(TAG, "STREAM_INPUT_TTS_EVENT_SENTENCE_BEGIN:" + all_response);
|
|
||||||
} else if (event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_SENTENCE_END) {
|
|
||||||
Log.i(TAG, "STREAM_INPUT_TTS_EVENT_SENTENCE_END:" + all_response);
|
|
||||||
}
|
}
|
||||||
}
|
boolean isSuccess = startTts(Boolean.parseBoolean(isChinese.toString()));
|
||||||
|
result.success(isSuccess);
|
||||||
@Override
|
break;
|
||||||
public void onStreamInputTtsDataCallback(byte[] data) {
|
case "sendTts":
|
||||||
if (data.length > 0) {
|
assert args != null;
|
||||||
ttsAudioTrack.setAudioData(data);
|
Object textArg = args.get("text");
|
||||||
|
if (textArg == null || textArg.toString().isBlank()) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
sendTts(textArg.toString());
|
||||||
}, genTtsTicket(), genTtsParameters(isChinese), "", Constants.LogLevel.toInt(Constants.LogLevel.LOG_LEVEL_NONE), false);
|
break;
|
||||||
if (Constants.NuiResultCode.SUCCESS != ret) {
|
case "completeTts":
|
||||||
Log.i(TAG, "start tts failed " + ret);
|
completeTts();
|
||||||
return false;
|
break;
|
||||||
} else {
|
case "stopTts":
|
||||||
return true;
|
stopTts();
|
||||||
|
break;
|
||||||
|
case "startAsr":
|
||||||
|
startAsr();
|
||||||
|
break;
|
||||||
|
case "stopAsr":
|
||||||
|
stopAsr();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
result.notImplemented();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,8 +168,10 @@ public class MainActivity extends FlutterActivity implements INativeNuiCallback
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void startAsr() {
|
private void startAsr() {
|
||||||
asrText = "";
|
|
||||||
asrHandler.post(() -> {
|
asrHandler.post(() -> {
|
||||||
|
// 初始化
|
||||||
|
initAsrInstance();
|
||||||
|
// 启动
|
||||||
String setParamsString = genAsrParams();
|
String setParamsString = genAsrParams();
|
||||||
Log.i(TAG, "nui set params " + setParamsString);
|
Log.i(TAG, "nui set params " + setParamsString);
|
||||||
asrInstance.setParams(setParamsString);
|
asrInstance.setParams(setParamsString);
|
||||||
@@ -213,105 +182,12 @@ public class MainActivity extends FlutterActivity implements INativeNuiCallback
|
|||||||
|
|
||||||
private void stopAsr() {
|
private void stopAsr() {
|
||||||
asrHandler.post(() -> {
|
asrHandler.post(() -> {
|
||||||
asrStopping = true;
|
|
||||||
long ret = asrInstance.stopDialog();
|
long ret = asrInstance.stopDialog();
|
||||||
runOnUiThread(() -> asrMethodChannel.invokeMethod("onAsrStop", null));
|
handler.post(() -> channel.invokeMethod("onAsrStop", null));
|
||||||
Log.i(TAG, "cancel dialog " + ret + " end");
|
Log.i(TAG, "cancel dialog " + ret + " end");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNuiEventCallback(Constants.NuiEvent event, final int resultCode,
|
|
||||||
final int arg2, KwsResult kwsResult,
|
|
||||||
AsrResult asrResult) {
|
|
||||||
Log.i(TAG, "event=" + event + " resultCode=" + resultCode);
|
|
||||||
if (event == Constants.NuiEvent.EVENT_TRANSCRIBER_STARTED) {
|
|
||||||
|
|
||||||
} else if (event == Constants.NuiEvent.EVENT_TRANSCRIBER_COMPLETE) {
|
|
||||||
asrStopping = false;
|
|
||||||
} else if (event == Constants.NuiEvent.EVENT_ASR_PARTIAL_RESULT) {
|
|
||||||
JSONObject jsonObject = JSON.parseObject(asrResult.allResponse);
|
|
||||||
JSONObject payload = jsonObject.getJSONObject("payload");
|
|
||||||
String result = payload.getString("result");
|
|
||||||
if (asrMethodChannel != null && result != null && !result.isBlank()) {
|
|
||||||
runOnUiThread(() -> asrMethodChannel.invokeMethod("onAsrResult", asrText + result));
|
|
||||||
}
|
|
||||||
} else if (event == Constants.NuiEvent.EVENT_SENTENCE_END) {
|
|
||||||
JSONObject jsonObject = JSON.parseObject(asrResult.allResponse);
|
|
||||||
JSONObject payload = jsonObject.getJSONObject("payload");
|
|
||||||
String result = payload.getString("result");
|
|
||||||
if (asrMethodChannel != null && result != null && !result.isBlank()) {
|
|
||||||
asrText += result;
|
|
||||||
runOnUiThread(() -> asrMethodChannel.invokeMethod("onAsrResult", asrText));
|
|
||||||
}
|
|
||||||
} else if (event == Constants.NuiEvent.EVENT_VAD_START) {
|
|
||||||
|
|
||||||
} else if (event == Constants.NuiEvent.EVENT_VAD_END) {
|
|
||||||
|
|
||||||
} else if (event == Constants.NuiEvent.EVENT_ASR_ERROR) {
|
|
||||||
asrStopping = false;
|
|
||||||
} else if (event == Constants.NuiEvent.EVENT_MIC_ERROR) {
|
|
||||||
asrStopping = false;
|
|
||||||
} else if (event == Constants.NuiEvent.EVENT_DIALOG_EX) { /* unused */
|
|
||||||
Log.i(TAG, "dialog extra message = " + asrResult.asrResult);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//当调用NativeNui的start后,会一定时间反复回调该接口,底层会提供buffer并告知这次需要数据的长度
|
|
||||||
//返回值告知底层读了多少数据,应该尽量保证return的长度等于需要的长度,如果返回<=0,则表示出错
|
|
||||||
@Override
|
|
||||||
public int onNuiNeedAudioData(byte[] buffer, int len) {
|
|
||||||
if (asrAudioRecorder == null) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (asrAudioRecorder.getState() != AudioRecord.STATE_INITIALIZED) {
|
|
||||||
Log.e(TAG, "audio recorder not init");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return asrAudioRecorder.read(buffer, 0, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
//当录音状态发送变化的时候调用
|
|
||||||
@Override
|
|
||||||
public void onNuiAudioStateChanged(Constants.AudioState state) {
|
|
||||||
Log.i(TAG, "onNuiAudioStateChanged");
|
|
||||||
if (state == Constants.AudioState.STATE_OPEN) {
|
|
||||||
Log.i(TAG, "audio recorder start");
|
|
||||||
asrAudioRecorder = new AudioRecord(MediaRecorder.AudioSource.DEFAULT,
|
|
||||||
ASR_SAMPLE_RATE,
|
|
||||||
AudioFormat.CHANNEL_IN_MONO,
|
|
||||||
AudioFormat.ENCODING_PCM_16BIT,
|
|
||||||
ASR_WAVE_FRAM_SIZE * 4);
|
|
||||||
asrAudioRecorder.startRecording();
|
|
||||||
Log.i(TAG, "audio recorder start done");
|
|
||||||
} else if (state == Constants.AudioState.STATE_CLOSE) {
|
|
||||||
Log.i(TAG, "audio recorder close");
|
|
||||||
if (asrAudioRecorder != null) {
|
|
||||||
asrAudioRecorder.release();
|
|
||||||
}
|
|
||||||
} else if (state == Constants.AudioState.STATE_PAUSE) {
|
|
||||||
Log.i(TAG, "audio recorder pause");
|
|
||||||
if (asrAudioRecorder != null) {
|
|
||||||
asrAudioRecorder.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNuiAudioRMSChanged(float val) {
|
|
||||||
// Log.i(TAG, "onNuiAudioRMSChanged vol " + val);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNuiVprEventCallback(Constants.NuiVprEvent event) {
|
|
||||||
Log.i(TAG, "onNuiVprEventCallback event " + event);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNuiLogTrackCallback(Constants.LogLevel level, String log) {
|
|
||||||
Log.i(TAG, "onNuiLogTrackCallback log level:" + level + ", message -> " + log);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String genTtsTicket() {
|
private String genTtsTicket() {
|
||||||
String str = "";
|
String str = "";
|
||||||
try {
|
try {
|
||||||
@@ -433,4 +309,51 @@ public class MainActivity extends FlutterActivity implements INativeNuiCallback
|
|||||||
Log.i(TAG, "dialog params: " + params);
|
Log.i(TAG, "dialog params: " + params);
|
||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean startTts(boolean isChinese) {
|
||||||
|
int ret = streamInputTtsInstance.startStreamInputTts(new INativeStreamInputTtsCallback() {
|
||||||
|
@Override
|
||||||
|
public void onStreamInputTtsEventCallback(INativeStreamInputTtsCallback.StreamInputTtsEvent event, String task_id, String session_id, int ret_code, String error_msg, String timestamp, String all_response) {
|
||||||
|
Log.i(TAG, "stream input tts event(" + event + ") session id(" + session_id + ") task id(" + task_id + ") retCode(" + ret_code + ") errMsg(" + error_msg + ")");
|
||||||
|
if (event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_SYNTHESIS_STARTED) {
|
||||||
|
Log.i(TAG, "STREAM_INPUT_TTS_EVENT_SYNTHESIS_STARTED");
|
||||||
|
ttsAudioTrack.play();
|
||||||
|
Log.i(TAG, "start play");
|
||||||
|
} else if (event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_SENTENCE_SYNTHESIS) {
|
||||||
|
Log.i(TAG, "STREAM_INPUT_TTS_EVENT_SENTENCE_SYNTHESIS:" + timestamp);
|
||||||
|
} else if (event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_SYNTHESIS_COMPLETE || event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_TASK_FAILED) {
|
||||||
|
/*
|
||||||
|
* 提示: STREAM_INPUT_TTS_EVENT_SYNTHESIS_COMPLETE事件表示TTS已经合成完并通过回调传回了所有音频数据, 而不是表示播放器已经播放完了所有音频数据。
|
||||||
|
*/
|
||||||
|
Log.i(TAG, "play end");
|
||||||
|
|
||||||
|
// 表示推送完数据, 当播放器播放结束则会有playOver回调
|
||||||
|
ttsAudioTrack.isFinishSend(true);
|
||||||
|
|
||||||
|
if (event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_TASK_FAILED) {
|
||||||
|
Log.e(TAG, "STREAM_INPUT_TTS_EVENT_TASK_FAILED: " + "error_code(" + ret_code + ") error_message(" + error_msg + ")");
|
||||||
|
}
|
||||||
|
} else if (event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_SENTENCE_BEGIN) {
|
||||||
|
Log.i(TAG, "STREAM_INPUT_TTS_EVENT_SENTENCE_BEGIN:" + all_response);
|
||||||
|
} else if (event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_SENTENCE_END) {
|
||||||
|
Log.i(TAG, "STREAM_INPUT_TTS_EVENT_SENTENCE_END:" + all_response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStreamInputTtsDataCallback(byte[] data) {
|
||||||
|
if (data.length > 0) {
|
||||||
|
ttsAudioTrack.setAudioData(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, genTtsTicket(), genTtsParameters(isChinese), "", Constants.LogLevel.toInt(Constants.LogLevel.LOG_LEVEL_NONE), false);
|
||||||
|
if (Constants.NuiResultCode.SUCCESS != ret) {
|
||||||
|
Log.i(TAG, "start tts failed " + ret);
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package com.example.ai_assistant_plugin;
|
||||||
|
|
||||||
|
import android.media.AudioFormat;
|
||||||
|
import android.media.AudioRecord;
|
||||||
|
import android.media.MediaRecorder;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSON;
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import com.alibaba.idst.nui.AsrResult;
|
||||||
|
import com.alibaba.idst.nui.Constants;
|
||||||
|
import com.alibaba.idst.nui.INativeNuiCallback;
|
||||||
|
import com.alibaba.idst.nui.KwsResult;
|
||||||
|
|
||||||
|
import io.flutter.plugin.common.MethodChannel;
|
||||||
|
|
||||||
|
public class AsrCallBack implements INativeNuiCallback {
|
||||||
|
|
||||||
|
private static final String TAG = "AliAsr-Callbask";
|
||||||
|
private final static int ASR_SAMPLE_RATE = 16000;
|
||||||
|
private final static int ASR_WAVE_FRAM_SIZE = 20 * 2 * 1 * ASR_SAMPLE_RATE / 1000; //20ms audio for 16k/16bit/mono
|
||||||
|
private AudioRecord asrAudioRecorder = null;
|
||||||
|
private MethodChannel channel;
|
||||||
|
private String asrText = "";
|
||||||
|
private Handler handler = new Handler(Looper.getMainLooper());
|
||||||
|
|
||||||
|
public AsrCallBack(MethodChannel channel) {
|
||||||
|
this.channel = channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNuiEventCallback(Constants.NuiEvent event, final int resultCode,
|
||||||
|
final int arg2, KwsResult kwsResult,
|
||||||
|
AsrResult asrResult) {
|
||||||
|
Log.i(TAG, "event=" + event + " resultCode=" + resultCode);
|
||||||
|
if (event == Constants.NuiEvent.EVENT_TRANSCRIBER_STARTED) {
|
||||||
|
asrText = "";
|
||||||
|
} else if (event == Constants.NuiEvent.EVENT_TRANSCRIBER_COMPLETE) {
|
||||||
|
// complete
|
||||||
|
} else if (event == Constants.NuiEvent.EVENT_ASR_PARTIAL_RESULT) {
|
||||||
|
JSONObject jsonObject = JSON.parseObject(asrResult.allResponse);
|
||||||
|
JSONObject payload = jsonObject.getJSONObject("payload");
|
||||||
|
String result = payload.getString("result");
|
||||||
|
if (channel != null && result != null && !result.isBlank()) {
|
||||||
|
handler.post(() -> channel.invokeMethod("onAsrResult", asrText + result));
|
||||||
|
}
|
||||||
|
} else if (event == Constants.NuiEvent.EVENT_SENTENCE_END) {
|
||||||
|
JSONObject jsonObject = JSON.parseObject(asrResult.allResponse);
|
||||||
|
JSONObject payload = jsonObject.getJSONObject("payload");
|
||||||
|
String result = payload.getString("result");
|
||||||
|
if (channel != null && result != null && !result.isBlank()) {
|
||||||
|
asrText += result;
|
||||||
|
handler.post(() -> channel.invokeMethod("onAsrResult", asrText));
|
||||||
|
}
|
||||||
|
} else if (event == Constants.NuiEvent.EVENT_VAD_START) {
|
||||||
|
|
||||||
|
} else if (event == Constants.NuiEvent.EVENT_VAD_END) {
|
||||||
|
|
||||||
|
} else if (event == Constants.NuiEvent.EVENT_ASR_ERROR) {
|
||||||
|
|
||||||
|
} else if (event == Constants.NuiEvent.EVENT_MIC_ERROR) {
|
||||||
|
|
||||||
|
} else if (event == Constants.NuiEvent.EVENT_DIALOG_EX) { /* unused */
|
||||||
|
Log.i(TAG, "dialog extra message = " + asrResult.asrResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//当调用NativeNui的start后,会一定时间反复回调该接口,底层会提供buffer并告知这次需要数据的长度
|
||||||
|
//返回值告知底层读了多少数据,应该尽量保证return的长度等于需要的长度,如果返回<=0,则表示出错
|
||||||
|
@Override
|
||||||
|
public int onNuiNeedAudioData(byte[] buffer, int len) {
|
||||||
|
if (asrAudioRecorder == null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (asrAudioRecorder.getState() != AudioRecord.STATE_INITIALIZED) {
|
||||||
|
Log.e(TAG, "audio recorder not init");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return asrAudioRecorder.read(buffer, 0, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
//当录音状态发送变化的时候调用
|
||||||
|
@Override
|
||||||
|
public void onNuiAudioStateChanged(Constants.AudioState state) {
|
||||||
|
Log.i(TAG, "onNuiAudioStateChanged");
|
||||||
|
if (state == Constants.AudioState.STATE_OPEN) {
|
||||||
|
Log.i(TAG, "audio recorder start");
|
||||||
|
asrAudioRecorder = new AudioRecord(MediaRecorder.AudioSource.DEFAULT,
|
||||||
|
ASR_SAMPLE_RATE,
|
||||||
|
AudioFormat.CHANNEL_IN_MONO,
|
||||||
|
AudioFormat.ENCODING_PCM_16BIT,
|
||||||
|
ASR_WAVE_FRAM_SIZE * 4);
|
||||||
|
asrAudioRecorder.startRecording();
|
||||||
|
Log.i(TAG, "audio recorder start done");
|
||||||
|
} else if (state == Constants.AudioState.STATE_CLOSE) {
|
||||||
|
Log.i(TAG, "audio recorder close");
|
||||||
|
if (asrAudioRecorder != null) {
|
||||||
|
asrAudioRecorder.release();
|
||||||
|
}
|
||||||
|
} else if (state == Constants.AudioState.STATE_PAUSE) {
|
||||||
|
Log.i(TAG, "audio recorder pause");
|
||||||
|
if (asrAudioRecorder != null) {
|
||||||
|
asrAudioRecorder.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNuiAudioRMSChanged(float val) {
|
||||||
|
// Log.i(TAG, "onNuiAudioRMSChanged vol " + val);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNuiVprEventCallback(Constants.NuiVprEvent event) {
|
||||||
|
Log.i(TAG, "onNuiVprEventCallback event " + event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNuiLogTrackCallback(Constants.LogLevel level, String log) {
|
||||||
|
Log.i(TAG, "onNuiLogTrackCallback log level:" + level + ", message -> " + log);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void release() {
|
||||||
|
// 释放音频录制资源
|
||||||
|
if (asrAudioRecorder != null) {
|
||||||
|
try {
|
||||||
|
asrAudioRecorder.stop();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "release error", e);
|
||||||
|
}
|
||||||
|
asrAudioRecorder.release();
|
||||||
|
asrAudioRecorder = null;
|
||||||
|
}
|
||||||
|
channel = null;
|
||||||
|
asrText = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.ai_chat_assistant;
|
package com.example.ai_assistant_plugin;
|
||||||
|
|
||||||
import android.media.AudioFormat;
|
import android.media.AudioFormat;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.ai_chat_assistant;
|
package com.example.ai_assistant_plugin;
|
||||||
|
|
||||||
public interface AudioPlayerCallback {
|
public interface AudioPlayerCallback {
|
||||||
public void playStart();
|
public void playStart();
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
package com.example.ai_chat_assistant;
|
package com.example.ai_assistant_plugin;
|
||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.alibaba.fastjson.JSON;
|
import com.alibaba.fastjson.JSON;
|
||||||
import com.alibaba.fastjson.JSONObject;
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import com.example.ai_assistant_plugin.token.AccessToken;
|
||||||
import com.example.ai_chat_assistant.token.AccessToken;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.ai_chat_assistant.token;
|
package com.example.ai_assistant_plugin.token;
|
||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.ai_chat_assistant.token;
|
package com.example.ai_assistant_plugin.token;
|
||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.ai_chat_assistant.token;
|
package com.example.ai_assistant_plugin.token;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Say something
|
* Say something
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.ai_chat_assistant.token;
|
package com.example.ai_assistant_plugin.token;
|
||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.ai_chat_assistant.token;
|
package com.example.ai_assistant_plugin.token;
|
||||||
|
|
||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
|
|
||||||
BIN
assets/images/ai1.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
assets/images/ai2.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
assets/porcupine_params_zh.pv
Normal file
BIN
assets/zhongzhong_zh_android.ppn
Normal file
3
devtools_options.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
description: This file stores settings for Dart & Flutter DevTools.
|
||||||
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||||
|
extensions:
|
||||||
686
docs/AI_Chat_Assistant PRD.md
Normal file
@@ -0,0 +1,686 @@
|
|||||||
|
# AI_Chat_Assistant PRD
|
||||||
|
|
||||||
|
# AI 聊天助手需求设计文档(产品需求文档 PRD)
|
||||||
|
|
||||||
|
### 文档介绍
|
||||||
|
|
||||||
|
| 项目 | 信息 |
|
||||||
|
| --- | --- |
|
||||||
|
| 产品名称 | AI Chat Assistant Flutter Plugin |
|
||||||
|
| 版本 | v1.0.0+1 |
|
||||||
|
| 文档版本 | v1.0 |
|
||||||
|
| 创建日期 | 2025-09-23 |
|
||||||
|
| 最近更新时间 | 2025-09-24 |
|
||||||
|
| 产品经理 | (待指定) |
|
||||||
|
| 技术负责人 | (待指定) |
|
||||||
|
| 目标平台 | Flutter (Android / iOS,首期 Android 优先) |
|
||||||
|
| 发布形态 | Flutter 第三方插件(支持示例 App) |
|
||||||
|
|
||||||
|
### 修订记录
|
||||||
|
|
||||||
|
| 版本 | 日期 | 作者 | 内容 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1.0 | 2025-09-23 | - | 初版功能需求整理 |
|
||||||
|
|
||||||
|
## **1. 引言**
|
||||||
|
|
||||||
|
### **1.1 文档目的**
|
||||||
|
|
||||||
|
本文档旨在定义“AI Chat Assistant Flutter Plugin”项目的详细功能需求、非功能需求、技术架构及项目规划。它为设计、开发、测试及项目相关人员提供了统一的参考依据和验收标准。
|
||||||
|
|
||||||
|
### **1.2 适用范围**
|
||||||
|
|
||||||
|
本文档目标读者为:AI Chat Assistant Flutter Plugin 的系统设计人员、开发人员、测试人员、产品经理以及项目管理人员。
|
||||||
|
|
||||||
|
### **1.3 名词解释**
|
||||||
|
|
||||||
|
| **序号** | **名词术语** | **名词解释** |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 1 | Flutter Plugin | 一种可复用的代码包,用于为Flutter应用添加特定的原生平台功能。 |
|
||||||
|
| 2 | ASR | Automatic Speech Recognition,自动语音识别,将语音转换为文本。 |
|
||||||
|
| 3 | TTS | Text-To-Speech,语音合成,将文本转换为语音。 |
|
||||||
|
| 4 | SSE | Server-Sent Events,一种服务器向客户端推送数据的技术。 |
|
||||||
|
| 5 | 车控命令 | 通过语音或界面触发的,用于控制车辆功能(如门锁、空调)的指令。 |
|
||||||
|
| 6 | 场景(Scenario)| 不同业务上下文(购车 / 用车) |
|
||||||
|
|
||||||
|
### **1.4 统一异常处理**
|
||||||
|
|
||||||
|
1. **网络异常**:语音识别或AI对话过程中出现网络错误,应有明确提示(如:“网络连接失败,请检查后重试”),并提供重试选项。
|
||||||
|
2. **权限拒绝**:当未授予麦克风或音频录制权限时,应引导用户前往系统设置开启权限。
|
||||||
|
3. **语音识别失败**:返回空文本或低置信度,提示“抱歉,我没有听清,请再说一遍”。(补充:不缓存失败结果。)
|
||||||
|
4. **命令执行失败**:车控执行失败需双通道反馈(UI + TTS),并显示失败原因或错误类型标签。(补充:通用错误尽量转化为用户友好语义。)
|
||||||
|
|
||||||
|
## 2. 产品概述
|
||||||
|
|
||||||
|
### 2.1 产品简介
|
||||||
|
|
||||||
|
AI Chat Assistant 是一个集成语音识别、智能对话、车控指令解析与执行、语音反馈渲染于一体的 Flutter 插件。支持“车控助手”与“用车助手”双场景:
|
||||||
|
- 车控助手:支持 20+ 车辆控制命令(如空调、车窗、车门、座椅加热等)。
|
||||||
|
- 用车助手:基于用户手册 / 知识库进行问答。(包含 保养咨询、能耗优化建议等)
|
||||||
|
- 购车助手:基于厂家的经销政策以及车辆配置信息进行问答
|
||||||
|
|
||||||
|
### 2.2 产品流程图
|
||||||
|
|
||||||
|
|
||||||
|
现有核心流程:用户长按语音按钮 → 权限校验 → 录音 + 识别 → 文本分类 → 路由到问答/车控/错误反馈 → 结果播报与展示。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[用户点击语音按钮] --> B[startVoiceInput]
|
||||||
|
B --> C[检查麦克风权限]
|
||||||
|
C --> D[开始录音/ASR]
|
||||||
|
D --> E[实时显示识别结果]
|
||||||
|
E --> F[用户松开按钮]
|
||||||
|
F --> G[stopAndProcessVoiceInput]
|
||||||
|
G --> H[调用reply处理文本]
|
||||||
|
H --> I[文本分类]
|
||||||
|
I --> J{分类结果}
|
||||||
|
J -->|车控命令| K[handleVehicleControl]
|
||||||
|
J -->|普通问答| L[answerQuestion - ChatSseService]
|
||||||
|
J -->|错误问题| M[answerWrongQuestion]
|
||||||
|
K --> N[解析车控命令]
|
||||||
|
N --> O[执行TTS播报]
|
||||||
|
O --> P[执行车控命令]
|
||||||
|
P --> Q[生成执行反馈]
|
||||||
|
Q --> R[更新UI显示]
|
||||||
|
L --> S[SSE流式对话]
|
||||||
|
S --> T[实时TTS播报]
|
||||||
|
T --> R
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 产品目的
|
||||||
|
|
||||||
|
- 提供开箱即用的AI语音交互能力
|
||||||
|
- 支持自然语言车控命令识别与执行
|
||||||
|
- 轻量级架构,易于集成和定制
|
||||||
|
|
||||||
|
### 2.4 顶层功能结构
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
ASR[语音采集/ASR] --> CLASS[文本/意图分类]
|
||||||
|
CLASS -->|车控| CMD[车控解析/执行]
|
||||||
|
CLASS -->|问答| QA[对话引擎 SSE/WS]
|
||||||
|
CLASS -->|兜底| FALL[引导重试/澄清]
|
||||||
|
QA --> TTS[TTS 播报]
|
||||||
|
CMD --> FEED[执行反馈]
|
||||||
|
TTS --> MSG[消息渲染]
|
||||||
|
FEED --> MSG
|
||||||
|
MSG --> STORE[(本地持久化)]
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### 2.5 语音交互主流程
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
START[长按/唤醒触发] --> PERM[权限校验]
|
||||||
|
PERM -->|通过| REC[开始录音]
|
||||||
|
PERM -->|拒绝| EXIT[权限引导提示]
|
||||||
|
REC --> ASR[实时增量识别]
|
||||||
|
ASR --> RELEASE[松手/结束]
|
||||||
|
RELEASE --> CLASSIFY[分类/意图识别]
|
||||||
|
CLASSIFY -->|车控| CONTROL[结构化命令]
|
||||||
|
CLASSIFY -->|问答| STREAM[流式对话输出]
|
||||||
|
CLASSIFY -->|无匹配| GUIDE[兜底提示]
|
||||||
|
CONTROL --> EXEC[调用车控处理器]
|
||||||
|
EXEC --> RESULT[结果反馈]
|
||||||
|
STREAM --> MERGE[增量合并/渲染]
|
||||||
|
RESULT --> RENDER[消息渲染 + TTS]
|
||||||
|
MERGE --> RENDER
|
||||||
|
RENDER --> STORE[(持久化过滤保存)]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 功能需求
|
||||||
|
|
||||||
|
### 3.1 用户页面展示
|
||||||
|
|
||||||
|
#### **3.1.1 浮动图标组件(ChatFloatingIcon)**
|
||||||
|
|
||||||
|
- **用户场景**
|
||||||
|
|
||||||
|
在主应用界面上提供一个常驻的、可随意拖拽的AI助手入口,作为全局统一入口,维持轻量可达性,不干扰主业务操作。
|
||||||
|
|
||||||
|
- **功能描述**
|
||||||
|
|
||||||
|
一个可拖拽的浮动按钮,点击展开全屏聊天界面,长按直接启动语音输入(只有聊天窗口)。
|
||||||
|
|
||||||
|
- **需求说明**
|
||||||
|
- **拖拽吸附**:支持在屏幕内自由拖拽,松开时自动吸附到最近的屏幕边缘。
|
||||||
|
- **交互响应**:
|
||||||
|
- **点击**: 进入全屏的聊天页面,全屏模式为一个新的页面
|
||||||
|
- **长按**:变为聆听模式,直接开始语音录制,出现聊天窗口
|
||||||
|
- **位置自适应**:聊天窗口的位置需要根据浮动按钮的位置变化,如果浮动按钮位置过于靠上,则聊天窗口显示在浮动按钮的下方,默认显示在浮动按钮的上方
|
||||||
|
|
||||||
|
- **状态指示**:通过动画(如呼吸效果)表示待机、聆听、思考等不同状态。
|
||||||
|
|
||||||
|
默认为待机状态,长按会进入聆听状态,思考中的状态会在聊天显示思考中的状态聊天气泡
|
||||||
|
- **补充说明**:长按阈值建议 300ms;误触策略:短按抬起不触发录音。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### **3.1.2 浮动聊天窗口(ChatPopup)**
|
||||||
|
|
||||||
|
- **用户场景**
|
||||||
|
|
||||||
|
用户与AI助手进行可视化的文本和语音交互。
|
||||||
|
|
||||||
|
- **功能描述**
|
||||||
|
|
||||||
|
提供弹出式聊天窗口,展示对话记录并提供输入方式,不承担复杂历史翻阅与批量操作
|
||||||
|
|
||||||
|
- **需求说明**
|
||||||
|
|
||||||
|
长按浮动按钮即可出现浮动聊天窗口,长按状态下展示聆听中,放开后则聆听结束,变为思考中状态。
|
||||||
|
|
||||||
|
思考中状态可以手动点击 **停止回答** 可以终止AI对话,直到回答完成,则不可点击停止回答。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### **3.1.3 全屏聊天界面(ChatFullscreen)**
|
||||||
|
|
||||||
|
- **用户场景**
|
||||||
|
|
||||||
|
用户与AI助手进行可视化的文本和语音交互,适用于长对话、多轮引用、查看历史与多类型气泡的交互。
|
||||||
|
|
||||||
|
- **功能描述**
|
||||||
|
|
||||||
|
提供全屏聊天界面,展示对话记录并提供输入方式。全屏的聊天页面功能更加丰富,多类型气泡、快捷指令区、功能按钮区、历史分页加载
|
||||||
|
|
||||||
|
- **需求说明**
|
||||||
|
- 全屏聊天模式下会保留之前浮动聊天窗口的聊天记录
|
||||||
|
- 不同的场景为不同的AI助手,现有两种场景,购车助手和用车助手(根据入口来判断)
|
||||||
|
- 顶部会有可配置的功能按钮区域(用户服务/购车服务)
|
||||||
|
- 功能按钮下方会有快捷对话的区域,可以点击换一换来更换快捷指令,支持“换一换”刷新策略(本地轮换或服务侧下发)
|
||||||
|
- 聊天页面底部会有功能栏,分别为**删除当前对话**,**按住说话**,**切换文本输入**
|
||||||
|
- 如果存在本地对话记录,点击 **显示历史对话记录** 会加载之前的5条对话记录,同样可以通过上拉来加载历史对话记录
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 3.2 AI对话场景
|
||||||
|
|
||||||
|
#### **3.2.1 购车助手场景**
|
||||||
|
|
||||||
|
- **用户场景**
|
||||||
|
|
||||||
|
用户未绑定车辆的情况下,爱车页面变为购车页面,购车页面也会有AI助手,未绑定车辆用户获取车型配置、优惠、推荐。
|
||||||
|
|
||||||
|
- **功能描述**
|
||||||
|
|
||||||
|
购车页面的AI助手没有聆听模式,只能点击进入全屏的聊天页面,首次的回复词也会有变化;功能按钮区域和快捷回复区域都会跟用车助手不同
|
||||||
|
|
||||||
|
- **需求说明**
|
||||||
|
- 浮动按钮点击可进入全屏的AI助手页面,顶部的功能按钮为热门车型推荐,同样是icon+text的按钮,点击之后会快捷输入车型的名称,AI助手会回复车型的相关信息,同时快捷对话的区域会按照对话来进行变化;
|
||||||
|
- 购车助手同样可以通过文本的语义来进行推荐车型,以及解读购车的优惠信息;
|
||||||
|
- 如果在购车助手场景下,本身并没有绑定车辆,如果有车控的语音输入,则返回:您还未绑定车辆,无法使用车控指令;
|
||||||
|
- 如果是用车助手场景,包含购车助手的全部功能,同样可以回复相应车型来获取配置信息以及购车政策。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### **3.2.2 用车助手场景**
|
||||||
|
|
||||||
|
- **用户场景**
|
||||||
|
|
||||||
|
用户在绑定车辆的情况下,进入爱车页面,使用AI助手则我用车助手场景
|
||||||
|
|
||||||
|
- **功能描述**
|
||||||
|
|
||||||
|
用车助手场景下的AI助手不同于购车场景,可以按照文本的输入分为三种:普通问答,车控指令,错误问题反馈
|
||||||
|
|
||||||
|
- **需求说明**
|
||||||
|
- 用车场景下的功能按钮为官方推荐,内容包括智能场景推荐、数据埋点和运营位,其中智能场景推荐包括售后维保提醒,智能场景建议;数据埋点包括能耗报告,行为分析,用户习惯操作;运营位则是推荐的车型,用户复购建议;
|
||||||
|
- 常见功能按钮 → 发送一条“功能集合”消息气泡(含多个按钮)。
|
||||||
|
- 点击 更多工具按钮跟常见功能按钮一样,会返回一条聊天记录,里面会包含多个按钮,点击按钮可以进行快捷回复;
|
||||||
|
- 快捷回复区域是可以变化的,会根据对话的最后一个回复进行变化,快捷回复区域会跟在最后一个对话的底部;例如刚进入会跟在 功能按钮的底部,但是新的聊天记录出现后,就会自动拼接到聊天气泡的底部(聊天气泡类型分为多种,后面会详细介绍);
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### **3.2.3 用户唤醒场景**
|
||||||
|
|
||||||
|
- **用户场景**
|
||||||
|
|
||||||
|
在开通语音唔醒功能之后,购车页面和爱车页面都可以使用语音唤醒,免手动触发,提高便利性
|
||||||
|
|
||||||
|
- **功能描述**
|
||||||
|
|
||||||
|
用户可以通过语音唤醒功能在不需要点击按钮的情况下激活AI助手,提高使用便捷性和用户体验。唤醒词“你好,众众”,唤醒后直接进入聆听模式并高亮反馈。
|
||||||
|
|
||||||
|
- **需求说明**
|
||||||
|
- 设置页开启后常驻监听(低功耗策略)。
|
||||||
|
- 唤醒检测在本地执行,未唤醒音频不上传。
|
||||||
|
- 唤醒成功:音效 + 动画反馈。
|
||||||
|
- 低电量模式:可自动降敏或暂停。
|
||||||
|
- 权限缺失:首次引导开启麦克风权限。
|
||||||
|
|
||||||
|
### 3.3 聊天模块
|
||||||
|
|
||||||
|
#### **3.3.1 聊天气泡类型**
|
||||||
|
|
||||||
|
- **用户场景**
|
||||||
|
|
||||||
|
用户与AI助手对话过程中,会展示不同类型的聊天气泡以适应各种交互需求。
|
||||||
|
|
||||||
|
- **功能描述**
|
||||||
|
|
||||||
|
系统支持多种聊天气泡类型,包括文本气泡、卡片气泡、按钮气泡、链接气泡和富媒体气泡等,以满足不同场景下的交互需求。
|
||||||
|
|
||||||
|
- **需求说明**
|
||||||
|
- 文本气泡:展示普通文字对话内容,完整支持Markdown格式;
|
||||||
|
Markdown支持包括普通文本格式化、表格布局以及图片嵌入,提供更丰富的内容展示方式
|
||||||
|
- 卡片气泡:用于展示结构化信息,如车辆信息、维保记录等;
|
||||||
|
- 按钮气泡:包含可交互按钮,用于快速回复或执行操作;
|
||||||
|
- 富媒体气泡:支持图片、视频、语音等多媒体内容展示;
|
||||||
|
富媒体气泡可能适用于显示特定的信息,或者是视频的教程,轮播的图片
|
||||||
|
- 链接气泡:用于显示一个链接,包含Icon,Title,Description,以及一个可以点击的链接;
|
||||||
|
点击链接可以进入webview,链接气泡主要是用于详细介绍某些活动或者查看详情
|
||||||
|
- 业务气泡:用于特定业务的定制化气泡,会根据对话的业务场景,显示不同的气泡。
|
||||||
|
业务气泡不同于其他的气泡,可以在用户询问金融方案后显示返回的计算结果,在结果后面还有一个按钮,可以进入金融计算器的页面;同时在用户说出我希望订购某款车型时,会出现车型的信息,并且有一个立即订购的按钮。业务气泡是根据返回的数据来判断具体的业务场景,混合显示的气泡类型。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### **3.3.2 普通问答流程**
|
||||||
|
|
||||||
|
- **用户场景**
|
||||||
|
|
||||||
|
用户与AI助手对话过程中,AI助手会把语音转换为文本,通过文本的语义来判断场景,是普通问答或者是车控指令
|
||||||
|
|
||||||
|
- **功能描述**
|
||||||
|
|
||||||
|
普通问答现在只有用车助手这个场景,后续增加新的场景:购车助手;需要先通过文本语义判断是哪个场景,然后根据相应场景来回复问题。
|
||||||
|
|
||||||
|
- **需求说明**
|
||||||
|
- 场景识别由后端完成,前端仅消费分类结果
|
||||||
|
- SSE:按流片段累积,合并为 Markdown
|
||||||
|
|
||||||
|
不管是购车场景还是用车场景,或者是开放式对话,都有一个问答的流程,场景的区分不是前端来判断。现在流程是将语音转化的文本通过接口发给后台服务,然后后台会根据body里面的text和conversation_id 来返回一个文件流,客户端这边会把文件流的全部数据接受完成后合并为一个markdown语法的文本。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
|
||||||
|
A[文本数据] --> B{Chat api}
|
||||||
|
B --> C[文件流] --> D{SSE Service} --> E[Markdown文本]
|
||||||
|
E --> F{TTS} --> G[播放语音]
|
||||||
|
E --> H{Message} --> I[显示]
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
现在有两个新的调整方案,一个是Socket连接,另外一个方案是在现有的流程基础上新增消息类型。
|
||||||
|
|
||||||
|
1. Socket方案
|
||||||
|
|
||||||
|
每次进入AI助手会话会创建一个随机的taskId,然后每次对话都会有一个conversationId,使用taskId 连接Socket成功后,就可以通过Socket发送语音或者文本消息。然后Socket 会返回三种不同的消息类型,`it消息 (识别文本)`,`gb消息 (回复文本)`,`tts消息 (语音数据)`。
|
||||||
|
|
||||||
|
流程图如下:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant C as 客户端
|
||||||
|
participant S as 服务器
|
||||||
|
|
||||||
|
Note over C, S: 第一步:建立WebSocket连接
|
||||||
|
C->>S: WebSocket Handshake (携带认证信息)
|
||||||
|
|
||||||
|
Note over C, S: 第二步:客户端发送语音数据
|
||||||
|
C->>S: 发送消息: {taskId, conversationId, audioData}
|
||||||
|
|
||||||
|
Note over C, S: 第三步:服务器异步返回多条消息
|
||||||
|
par 并行处理与推送
|
||||||
|
S-->>C: it消息 (识别文本)
|
||||||
|
and
|
||||||
|
S-->>C: gb消息 (回复文本)
|
||||||
|
and
|
||||||
|
S-->>C: tts消息 (语音数据)
|
||||||
|
end
|
||||||
|
|
||||||
|
Note over C, S: 连接保持,可继续发送新语音或关闭
|
||||||
|
```
|
||||||
|
|
||||||
|
2. HTTP方案
|
||||||
|
|
||||||
|
HTTP方案是保持原有流程的情况下进行扩展,chat API 不再是返回一个数据流,而是返回一个json数据,json数据里面会有不同的消息返回,对应上面不同的气泡类型,消息类似分为`stream`,`text`,`media`,`bussines`,`stream`消息就是之前的数据流的消息,通过`messageId`就可以获取到数据;`text`消息是普通的文本消息,可以实现上面的链接气泡,按钮气泡;`media`消息则可以实现负责的图文混排,或者是一个单纯的视频教程;`bussines`消息主要以业务相关的,会根据业务类型显示不一样的样式,内容和按钮也会有变化。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"conversationId": 1,
|
||||||
|
"taskId": "task_123",
|
||||||
|
"code": 200,
|
||||||
|
"scenario": "buying_assistant",
|
||||||
|
"messageList": [
|
||||||
|
{
|
||||||
|
"messageId": "messageId_xxxxx",
|
||||||
|
"type": "stream"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"messageId": "messageId_xxxxx",
|
||||||
|
"type": "text",
|
||||||
|
"content": "xxxxx"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"messageId": "messageId_xxxxx",
|
||||||
|
"type": "media"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"messageId": "messageId_xxxxx",
|
||||||
|
"type": "bussines",
|
||||||
|
"bussineType": "vehicle_commendation"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **3.3.3 车控指令流程**
|
||||||
|
|
||||||
|
- **用户场景**
|
||||||
|
|
||||||
|
用户与AI助手对话过程中,AI助手会把语音转换为文本,通过文本的语义来判断场景,是普通问答或者是车控指令
|
||||||
|
|
||||||
|
- **功能描述**
|
||||||
|
|
||||||
|
如果根据分类结果,将语音或者文本信息归类为车控指令,则会进入车控指令的流程。识别语音指令中的车控意图,解析参数,并调用相应的车辆控制接口。
|
||||||
|
|
||||||
|
- **需求说明**
|
||||||
|
- **命令识别**:支持识别20+种车控命令类型。
|
||||||
|
- **参数解析**:能从指令中解析出关键参数(如温度值、座椅位置)。
|
||||||
|
- **执行反馈**:命令执行成功或失败,都必须有明确的TTS和UI反馈。
|
||||||
|
- **可扩展性**:提供抽象类`VehicleCommandHandler`,允许开发者根据具体车型实现自定义控制逻辑。
|
||||||
|
- **支持命令示例**:
|
||||||
|
- **车门控制**:上锁、解锁
|
||||||
|
- **车窗控制**:开启、关闭
|
||||||
|
- **空调控制**:开启、关闭、温度调节、极速降温
|
||||||
|
- **座椅控制**:主/副驾驶座椅加热、通风
|
||||||
|
- **后备箱控制**:开启、关闭
|
||||||
|
- **特殊功能**:鸣笛、车辆定位、一键备车、一键融雪
|
||||||
|
|
||||||
|
|
||||||
|
#### **3.3.4 聊天信息持久化**
|
||||||
|
|
||||||
|
- **用户场景**
|
||||||
|
|
||||||
|
用户在与AI助手进行对话时,会产生多条聊天记录。为保护用户隐私和提升体验,这些聊天记录默认仅保存在本地设备,不会上传至云端。
|
||||||
|
|
||||||
|
- **功能描述**
|
||||||
|
|
||||||
|
系统支持本地化持久化存储普通问答类型的聊天记录。所有文本类消息将以结构化数据形式保存至本地数据库(推荐使用SQLite或Hive等Flutter主流轻量级数据库),便于后续检索和管理。
|
||||||
|
|
||||||
|
- **需求说明**
|
||||||
|
- **本地存储**:所有与AI助手的聊天记录(包括文本、语音、气泡类型、时间戳等)需保存在本地设备,支持持久化;
|
||||||
|
- **存储方式**:采用本地数据库(如SQLite、Hive等Flutter常用方案),或文件存储,保证数据安全和高效读写;
|
||||||
|
- **读取与展示**:用户重新进入聊天界面时,不会自动加载历史聊天记录,点击显示历史记录之后才会加载聊天记录,按时间顺序展示;
|
||||||
|
- **数据结构**:每条聊天记录需包含:消息ID、会话ID、消息类型(文本/语音/卡片/按钮/富媒体)、内容、时间戳、发送方(用户/AI)、气泡类型等;
|
||||||
|
- **删除与清理**:支持用户手动删除单条或全部聊天记录,支持定期自动清理(如超过30天自动删除);
|
||||||
|
- **持久化逻辑**:在Socket和HTTP两种消息方案下,收到消息体后,系统会优先将stream和text类型消息持久化保存,其他类型消息仅用于即时展示,不做持久化处理。
|
||||||
|
|
||||||
|
有了消息的持久化,AI助手聊天消息的显示逻辑也会跟着变化,按照上面的`Socket`方案和`HTTP`方案,收到消息体之后首先会保存到本地,特别是`HTTP`方案中会有多条消息返回,持久化只会保存stream和text消息,其他消息不会保存。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[收到AI助手消息] --> B[显示消息到聊天界面]
|
||||||
|
B --> C{消息类型判断}
|
||||||
|
C -->|stream/text| D[持久化保存到本地数据库]
|
||||||
|
C -->|其他类型| E[仅显示,不持久化]
|
||||||
|
D --> F[历史消息管理]
|
||||||
|
F --> G[用户点击显示历史记录]
|
||||||
|
G --> H[加载历史消息并展示]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 非功能需求
|
||||||
|
|
||||||
|
### 4.1 服务模块
|
||||||
|
|
||||||
|
#### **4.1.1 模块设计目标**
|
||||||
|
|
||||||
|
服务模块负责“输入 → 解析 → 对话/车控 → 反馈 → 持久化”全链路的能力封装,向上提供简单一致的编程接口,向下屏蔽多种后端形态(HTTP / SSE)与原生能力(录音、TTS、权限、ASR)。
|
||||||
|
设计目标:
|
||||||
|
- 解耦:各服务单一职责,可独立替换;
|
||||||
|
- 可扩展:新协议 / 新命令 / 新气泡类型通过注册或策略扩展;
|
||||||
|
- 可裁剪:按需选择(如不需要车控,可不接入 command 子模块)。
|
||||||
|
|
||||||
|
#### **4.1.2 模块目录设计**
|
||||||
|
|
||||||
|
拟将现有核心服务抽离为独立基础包(例如:ai_chat_core),供 UI 插件依赖。
|
||||||
|
|
||||||
|
建议目录结构:
|
||||||
|
```
|
||||||
|
packages/
|
||||||
|
ai_chat_core/
|
||||||
|
lib/
|
||||||
|
ai_chat_core.dart # 对外统一入口
|
||||||
|
src/ # 代码目录
|
||||||
|
models/ # models
|
||||||
|
message.dart # 消息和消息块
|
||||||
|
message_chunk.dart
|
||||||
|
vehicle_command.dart # 车控
|
||||||
|
vehicle_command_response.dart
|
||||||
|
command_result.dart
|
||||||
|
enums/ # enums
|
||||||
|
message_type.dart
|
||||||
|
message_status.dart
|
||||||
|
command_type.dart
|
||||||
|
service_state.dart # 服务状态
|
||||||
|
input/
|
||||||
|
audio_recorder_service.dart # 录音以及保存文件
|
||||||
|
voice_recognition_service.dart # 目前项目未使用
|
||||||
|
wake_word_service.dart # 预留(可选)
|
||||||
|
nlp/ # 自然语言处理相关
|
||||||
|
text_classification_service.dart
|
||||||
|
vehicle_command_service.dart
|
||||||
|
dialog/ #对话相关的
|
||||||
|
chat_channel/
|
||||||
|
chat_sse_service.dart # SSE
|
||||||
|
chat_ws_service.dart # 预留 WebSocket(可选)
|
||||||
|
chat_http_service.dart # 批量 HTTP 方案(可选)
|
||||||
|
message_service.dart # 核心编排/状态更新
|
||||||
|
tts/ # TTS
|
||||||
|
tts_service.dart
|
||||||
|
tts_task_queue.dart
|
||||||
|
command/ # 车控相关
|
||||||
|
command_service.dart # Command Register Callback(已更新为抽象类的方式)
|
||||||
|
vehicle_command_handler.dart # 自定义实现车控命令
|
||||||
|
persistence/ # 持久化
|
||||||
|
persistence_service.dart # 提供持久化服务
|
||||||
|
dao/ # dao
|
||||||
|
message_dao.dart # 消息体的 dao 对象
|
||||||
|
infra/ # infra 的服务
|
||||||
|
config_service.dart
|
||||||
|
redis_service.dart
|
||||||
|
location_service.dart
|
||||||
|
vehicle_state_service.dart # 预留
|
||||||
|
logger.dart
|
||||||
|
error_mapper.dart
|
||||||
|
utils/ # 工具类
|
||||||
|
markdown_cleaner.dart
|
||||||
|
id_generator.dart
|
||||||
|
throttle.dart
|
||||||
|
```
|
||||||
|
|
||||||
|
对外暴露(ai_chat_core.dart)仅导出:
|
||||||
|
- models / enums
|
||||||
|
- MessageService(主入口)
|
||||||
|
- VehicleCommandHandler 抽象
|
||||||
|
- 配置初始化(initCore / setConfig)
|
||||||
|
|
||||||
|
#### **4.1.3 核心服务说明**
|
||||||
|
|
||||||
|
| 服务 | 角色 | 主要职责 | 关键点 |
|
||||||
|
| ---- | ---- | -------- | ------ |
|
||||||
|
| MessageService | 总编排 | 状态机、消息生命周期、调用链调度 | 统一入口 |
|
||||||
|
| AudioRecorderService | 输入 | 录音启动/停止、静音检测 | 录制参数配置 |
|
||||||
|
| VoiceRecognitionService | 输入处理 | 语音转文本(在线识别 / 中英判别) | 失败降级提示 |
|
||||||
|
| WakeWordService | 输入唤醒 | 本地唤醒词检测 | 低功耗策略 |
|
||||||
|
| TextClassificationService | 语义分类 | 业务场景 / 指令 / 问答判断 | 分类缓存 |
|
||||||
|
| VehicleCommandService | 语义→结构 | 解析多条车控命令列表 + 参数 | 容错解析 |
|
||||||
|
| CommandService | 执行派发 | 调用 Host 注入的 VehicleCommandHandler | 失败重试 |
|
||||||
|
| ChatSseService | 对话通道 | SSE 流接收与分片回调 | 首 Token 优化 |
|
||||||
|
| ChatWsService(预留) | 对话通道 | WebSocket 统一多类型消息 | 心跳+重连 |
|
||||||
|
| ChatHttpService(预留) | 对话通道 | 一次性批量消息 | 并行补拉 |
|
||||||
|
| TtsService + TtsTaskQueue | 输出播报 | 分句 / 排队 / 抢占 / 语言切换 | 顺序一致性 |
|
||||||
|
| PersistenceService | 持久化 | 消息落地 / 分页 / 过期清理 | schema 迁移 |
|
||||||
|
| RedisService | 辅助缓存 | 临时状态(可选) | 可替换 |
|
||||||
|
| LocationService | 扩展 | 位置信息(用于上下文) | 可选注入 |
|
||||||
|
| VehicleStateService | 扩展 | 实时车况(未来支持) | 监听通道 |
|
||||||
|
| ConfigService | 基础设施 | 端点 / 超时 / 重试策略统一 | 环境隔离 |
|
||||||
|
| Logger | 基础设施 | 分级日志 + 调试开关 | 生产开关 |
|
||||||
|
| MarkdownCleaner | 工具 | 清理/裁剪 Markdown 用于 TTS | 多语言规则 |
|
||||||
|
| ErrorMapper | 工具 | 后端错误码 → 用户友好文案 | 本地化适配 |
|
||||||
|
|
||||||
|
#### **4.1.4 服务分层**
|
||||||
|
|
||||||
|
- 链路图
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
autonumber
|
||||||
|
participant UI as UI层
|
||||||
|
participant MS as MessageService
|
||||||
|
participant AR as AudioRecorder
|
||||||
|
participant ASR as VoiceRecognition
|
||||||
|
participant CLS as TextClassification
|
||||||
|
participant VCS as VehicleCommandService
|
||||||
|
participant DLG as ChatSse/WS/HTTP
|
||||||
|
participant CMD as CommandService
|
||||||
|
participant HND as VehicleCommandHandler(Host)
|
||||||
|
participant TTS as TtsService
|
||||||
|
participant PERS as PersistenceService
|
||||||
|
|
||||||
|
UI->>MS: startVoiceInput()
|
||||||
|
MS->>AR: start()
|
||||||
|
AR-->>MS: audioChunks / silenceEnd
|
||||||
|
MS->>AR: stop()
|
||||||
|
MS->>ASR: recognize(audio)
|
||||||
|
ASR-->>MS: recognizedText
|
||||||
|
MS->>CLS: classify(text)
|
||||||
|
CLS-->>MS: category / scenario
|
||||||
|
alt 车控指令
|
||||||
|
MS->>VCS: parseCommands(text)
|
||||||
|
VCS-->>MS: commands[]
|
||||||
|
loop each command
|
||||||
|
MS->>CMD: execute(command)
|
||||||
|
CMD->>HND: executeCommand(command)
|
||||||
|
HND-->>CMD: result(success/fail)
|
||||||
|
CMD-->>MS: executionResult
|
||||||
|
MS->>TTS: enqueue("执行结果反馈")
|
||||||
|
MS->>PERS: save(control_feedback)
|
||||||
|
end
|
||||||
|
else 普通问答
|
||||||
|
MS->>DLG: requestStream(text, convId)
|
||||||
|
DLG-->>MS: streamChunk/ finalMessage
|
||||||
|
MS->>TTS: enqueue(chunk/segments)
|
||||||
|
MS->>PERS: save(text / merged)
|
||||||
|
else 错误/兜底
|
||||||
|
MS->>TTS: enqueue(提示语)
|
||||||
|
MS->>PERS: save(system_message)
|
||||||
|
end
|
||||||
|
TTS-->>UI: 播放中事件
|
||||||
|
PERS-->>MS: persist OK
|
||||||
|
MS-->>UI: 更新消息列表
|
||||||
|
```
|
||||||
|
|
||||||
|
- 逻辑分层
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[UI层] --> B[MessageService]
|
||||||
|
B --> C1[输入层 AudioRecorder / WakeWord]
|
||||||
|
B --> C2[语义层 Classification / VehicleCommand]
|
||||||
|
B --> C3[对话通道层 SSE / WS / HTTP]
|
||||||
|
B --> C4[执行层 CommandService + Host Handler]
|
||||||
|
B --> C5[输出层 TtsService]
|
||||||
|
B --> C6[存储层 PersistenceService]
|
||||||
|
B --> C7[基础设施 Config / Logger / Redis / Location]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **4.1.5 代码示例**
|
||||||
|
|
||||||
|
- 初始化:
|
||||||
|
```dart
|
||||||
|
await AiChatCore.init(
|
||||||
|
config: CoreConfig(
|
||||||
|
endpoints: Endpoints(
|
||||||
|
classify: "...",
|
||||||
|
sse: "...",
|
||||||
|
ws: "...",
|
||||||
|
controlParse: "...",
|
||||||
|
controlExec: "...",
|
||||||
|
voice: "...",
|
||||||
|
),
|
||||||
|
timeouts: Timeouts(connectMs: 8000, receiveMs: 20000),
|
||||||
|
enablePersistence: true,
|
||||||
|
persistenceMode: PersistenceMode.hive,
|
||||||
|
retryPolicy: RetryPolicy(maxAttempts: 2, backoffBaseMs: 400),
|
||||||
|
),
|
||||||
|
commandHandler: MyVehicleCommandHandler(),
|
||||||
|
logger: CustomLogger(),
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
- 统一公共接口:
|
||||||
|
```dart
|
||||||
|
abstract class AiChatCore {
|
||||||
|
static Future<void> init({required CoreConfig config, required VehicleCommandHandler commandHandler, Logger? logger});
|
||||||
|
static MessageService get messageService;
|
||||||
|
static PersistenceService? get persistence;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MessageService {
|
||||||
|
Stream<List<Message>> get messageStream;
|
||||||
|
Future<void> startVoiceInput();
|
||||||
|
Future<void> stopAndProcessVoiceInput();
|
||||||
|
Future<void> sendText(String text);
|
||||||
|
Future<void> abortAnswer();
|
||||||
|
Future<void> loadHistory({required int page, required int pageSize});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **4.1.6 编译与分发说明(path 子包 vs 单包)**
|
||||||
|
- 当前结构:插件(ai_chat_assistant)+ 仓库内 path 依赖(packages/ai_chat_core、packages/basic_intl)。
|
||||||
|
|
||||||
|
- 下游集成(pubspec)仅需:
|
||||||
|
```yaml
|
||||||
|
ai_chat_assistant:
|
||||||
|
git:
|
||||||
|
url: <repo_url>
|
||||||
|
ref: <branch_or_tag>
|
||||||
|
```
|
||||||
|
- Flutter 构建行为:
|
||||||
|
1. 克隆仓库 → 解析主插件 pubspec → 解析 path 子包 → 汇总全部 Dart 源码。
|
||||||
|
2. Debug:JIT,Release:统一 AOT 编译为一个 snapshot(无包边界差异)。
|
||||||
|
- 性能与产物差异:将子包“内联”进 lib/ 与保持 path 组织,在编译后无本质差异(tree shaking 后输出等价)。
|
||||||
|
- 适用场景对比:
|
||||||
|
| 目标 | 保持多包 | 合并单包 |
|
||||||
|
| ---- | -------- | -------- |
|
||||||
|
| 代码隔离 / 复用 | 优 | 弱 |
|
||||||
|
| 发布到 pub.dev | 需逐包发布 | 更简单 |
|
||||||
|
| 测试隔离性 | 优 | 一般 |
|
||||||
|
| 用户认知成本 | 稍高 | 低 |
|
||||||
|
- 暂不需发布:保持 path 结构即可;若未来开放生态 → 发布次序:ai_chat_core → basic_intl → ai_chat_assistant。
|
||||||
|
|
||||||
|
#### **4.1.7 AAR 与闭源分发说明**
|
||||||
|
| 诉求 | 可行性 | 说明 |
|
||||||
|
| ---- | ------ | ---- |
|
||||||
|
| 给“另一个 Flutter App”仅提供预编译 AAR 使用 | 不推荐/不可行 | Flutter 仍需源文件参与整体编译流程 |
|
||||||
|
| 给纯原生 Android App 内嵌使用 | 可用 `flutter build aar` | 适用于“原生 App 集成 Flutter 模块”,不是插件分发 |
|
||||||
|
| Dart 逻辑闭源 | 官方未支持 | 只能转移核心到 native/FFI 或服务端 |
|
||||||
|
|
||||||
|
结论:对 Flutter 使用方,直接 Git 依赖即可,无需 AAR。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## 参考文件
|
||||||
|
|
||||||
|
> [AI Assistant in One App.pdf](https://telekom-my.sharepoint.de/:b:/r/personal/guangfei_zhao_t-systems_com/Documents/Dokumente/AI%20Assistant%20in%20One%20App.pdf?csf=1&web=1&e=On4V2o)
|
||||||
|
|
||||||
|
> [Figma 设计稿](https://www.figma.com/design/04VXNLNrLdVDHhKBuOr06o/AI_Chat_Assistant?node-id=2-4607&t=qofYGMFzBLQ7cKus-1)
|
||||||
|
|
||||||
|
> [分子送检系统需求规格书.docx](images/%E5%88%86%E5%AD%90%E9%80%81%E6%A3%80%E7%B3%BB%E7%BB%9F%E9%9C%80%E6%B1%82%E8%A7%84%E6%A0%BC%E4%B9%A6.docx)
|
||||||
BIN
docs/images/Screenshot_20250923_162311.jpg
Normal file
|
After Width: | Height: | Size: 842 KiB |
BIN
docs/images/bubble_image_list.png
Normal file
|
After Width: | Height: | Size: 6.2 MiB |
BIN
docs/images/bussine_image.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
docs/images/button_bubble_image.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
docs/images/db1d92f4-107d-46c9-8154-fc58cb471acc.png
Normal file
|
After Width: | Height: | Size: 992 KiB |
BIN
docs/images/image 1.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
docs/images/image 2.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
docs/images/image 3.png
Normal file
|
After Width: | Height: | Size: 229 KiB |
BIN
docs/images/image 4.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
docs/images/image.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
docs/images/quick_reply_image.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
docs/images/分子送检系统需求规格书.docx
Normal file
45
example/.gitignore
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.build/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
.swiftpm/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
/coverage/
|
||||||
|
|
||||||
|
# Symbolication related
|
||||||
|
app.*.symbols
|
||||||
|
|
||||||
|
# Obfuscation related
|
||||||
|
app.*.map.json
|
||||||
|
|
||||||
|
# Android Studio will place build artifacts here
|
||||||
|
/android/app/debug
|
||||||
|
/android/app/profile
|
||||||
|
/android/app/release
|
||||||
30
example/.metadata
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# This file tracks properties of this Flutter project.
|
||||||
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
|
#
|
||||||
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
|
version:
|
||||||
|
revision: "05db9689081f091050f01aed79f04dce0c750154"
|
||||||
|
channel: "stable"
|
||||||
|
|
||||||
|
project_type: app
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: 05db9689081f091050f01aed79f04dce0c750154
|
||||||
|
base_revision: 05db9689081f091050f01aed79f04dce0c750154
|
||||||
|
- platform: android
|
||||||
|
create_revision: 05db9689081f091050f01aed79f04dce0c750154
|
||||||
|
base_revision: 05db9689081f091050f01aed79f04dce0c750154
|
||||||
|
|
||||||
|
# User provided section
|
||||||
|
|
||||||
|
# List of Local paths (relative to this file) that should be
|
||||||
|
# ignored by the migrate tool.
|
||||||
|
#
|
||||||
|
# Files that are not part of the templates will be ignored by default.
|
||||||
|
unmanaged_files:
|
||||||
|
- 'lib/main.dart'
|
||||||
|
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||||
92
example/README.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# AI Chat Assistant Example
|
||||||
|
|
||||||
|
这是 AI Chat Assistant Flutter 插件的示例应用,展示了如何集成和使用 AI 聊天助手功能。
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 1. 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd example
|
||||||
|
flutter pub get
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 运行应用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flutter run
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📱 示例功能
|
||||||
|
|
||||||
|
### 车控命令处理器示例
|
||||||
|
|
||||||
|
本示例展示了如何实现自定义的车控命令处理器:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class VehicleCommandClent extends VehicleCommandHandler {
|
||||||
|
@override
|
||||||
|
Future<(bool, Map<String, dynamic>?)> executeCommand(VehicleCommand command) async {
|
||||||
|
// 在这里实现具体的车控命令执行逻辑
|
||||||
|
print('执行车控命令: ${command.type}, 参数: ${command.params}');
|
||||||
|
|
||||||
|
// 更新命令状态为执行中
|
||||||
|
commandCubit?.emit(AIChatCommandState(
|
||||||
|
commandId: command.commandId,
|
||||||
|
commandType: command.type,
|
||||||
|
params: command.params,
|
||||||
|
status: AIChatCommandStatus.executing,
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
));
|
||||||
|
|
||||||
|
// 模拟命令执行
|
||||||
|
await Future.delayed(const Duration(seconds: 2));
|
||||||
|
|
||||||
|
// 更新命令状态为成功
|
||||||
|
commandCubit?.emit(AIChatCommandState(
|
||||||
|
commandId: command.commandId,
|
||||||
|
commandType: command.type,
|
||||||
|
params: command.params,
|
||||||
|
status: AIChatCommandStatus.success,
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
result: {'message': '命令执行成功'},
|
||||||
|
));
|
||||||
|
|
||||||
|
return (true, {'message': '命令已执行'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 初始化示例
|
||||||
|
|
||||||
|
```dart
|
||||||
|
void main() async {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
// 请求麦克风权限
|
||||||
|
if (!await Permission.microphone.isGranted) {
|
||||||
|
await Permission.microphone.request();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化 AI Chat Assistant,注册车控命令处理器
|
||||||
|
AIChatAssistantManager.instance.setupCommandHandle(
|
||||||
|
commandHandler: VehicleCommandClent()
|
||||||
|
);
|
||||||
|
|
||||||
|
runApp(const MyApp());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 示例特性
|
||||||
|
|
||||||
|
- ✅ 完整的车控命令处理示例
|
||||||
|
- ✅ 权限管理示例
|
||||||
|
- ✅ 状态管理集成
|
||||||
|
- ✅ AI 聊天界面集成
|
||||||
|
- ✅ 语音识别功能
|
||||||
|
- ✅ TTS 语音播报
|
||||||
|
|
||||||
|
## 📖 了解更多
|
||||||
|
|
||||||
|
- 查看主项目 [README](../README.md) 了解完整的功能特性
|
||||||
|
- 查看 [API 文档](../README.md#-api-文档) 了解接口使用方法
|
||||||
28
example/analysis_options.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# This file configures the analyzer, which statically analyzes Dart code to
|
||||||
|
# check for errors, warnings, and lints.
|
||||||
|
#
|
||||||
|
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||||
|
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||||
|
# invoked from the command line by running `flutter analyze`.
|
||||||
|
|
||||||
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
linter:
|
||||||
|
# The lint rules applied to this project can be customized in the
|
||||||
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||||
|
# included above or to enable additional rules. A list of all available lints
|
||||||
|
# and their documentation is published at https://dart.dev/lints.
|
||||||
|
#
|
||||||
|
# Instead of disabling a lint rule for the entire project in the
|
||||||
|
# section below, it can also be suppressed for a single line of code
|
||||||
|
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||||
|
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||||
|
# producing the lint.
|
||||||
|
rules:
|
||||||
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
14
example/android/.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
gradle-wrapper.jar
|
||||||
|
/.gradle
|
||||||
|
/captures/
|
||||||
|
/gradlew
|
||||||
|
/gradlew.bat
|
||||||
|
/local.properties
|
||||||
|
GeneratedPluginRegistrant.java
|
||||||
|
.cxx/
|
||||||
|
|
||||||
|
# Remember to never publicly share your keystore.
|
||||||
|
# See https://flutter.dev/to/reference-keystore
|
||||||
|
key.properties
|
||||||
|
**/*.keystore
|
||||||
|
**/*.jks
|
||||||
@@ -6,9 +6,9 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.example.ai_chat_assistant"
|
namespace = "com.example.example"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
ndkVersion = "29.0.13599879"
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
@@ -21,10 +21,10 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
applicationId = "com.example.ai_chat_assistant"
|
applicationId = "com.example.example"
|
||||||
// You can update the following values to match your application needs.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
minSdk = 24
|
minSdk = 23
|
||||||
targetSdk = flutter.targetSdkVersion
|
targetSdk = flutter.targetSdkVersion
|
||||||
versionCode = flutter.versionCode
|
versionCode = flutter.versionCode
|
||||||
versionName = flutter.versionName
|
versionName = flutter.versionName
|
||||||
@@ -37,11 +37,14 @@ android {
|
|||||||
signingConfig = signingConfigs.getByName("debug")
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
repositories {
|
viewBinding {
|
||||||
flatDir {
|
// isEnabled = viewBindingEnabled
|
||||||
dirs("libs")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
dependenciesInfo {
|
||||||
|
includeInApk = includeInApk
|
||||||
|
includeInBundle = includeInBundle
|
||||||
|
}
|
||||||
|
ndkVersion = "27.0.12077973"
|
||||||
}
|
}
|
||||||
|
|
||||||
flutter {
|
flutter {
|
||||||
@@ -49,6 +52,9 @@ flutter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(files("libs/fastjson-1.1.46.android.jar"))
|
// Process .aar files in the libs folder.
|
||||||
implementation(files("libs/nuisdk-release.aar"))
|
val aarFiles = fileTree(mapOf("dir" to "$rootDir/libs", "include" to listOf("*.aar")))
|
||||||
|
aarFiles.forEach { aar ->
|
||||||
|
implementation(files(aar))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,5 +4,4 @@
|
|||||||
to allow setting breakpoints, to provide hot reload, etc.
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
-->
|
-->
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.intent.action.TTS_SERVICE" />
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<application
|
<application
|
||||||
android:label="ai_chat_assistant"
|
android:label="AI助手"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
<activity
|
<activity
|
||||||
@@ -30,6 +30,10 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
|
<service
|
||||||
|
android:name="com.tsystems.flutter_vosk_wakeword.WakewordService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="microphone" />
|
||||||
</application>
|
</application>
|
||||||
<!-- Required to query activities that can process text, see:
|
<!-- Required to query activities that can process text, see:
|
||||||
https://developer.android.com/training/package-visibility and
|
https://developer.android.com/training/package-visibility and
|
||||||
@@ -41,8 +45,5 @@
|
|||||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
<data android:mimeType="text/plain"/>
|
<data android:mimeType="text/plain"/>
|
||||||
</intent>
|
</intent>
|
||||||
<intent>
|
|
||||||
<action android:name="android.intent.action.TTS_SERVICE"/>
|
|
||||||
</intent>
|
|
||||||
</queries>
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.example.example;
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity;
|
||||||
|
|
||||||
|
public class MainActivity extends FlutterActivity {
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 544 B |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 442 B |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 721 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
7
example/android/app/src/profile/AndroidManifest.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
@@ -1,16 +1,25 @@
|
|||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
|
val mavenLocalPath = rootProject.file("../../android/mavenLocal")
|
||||||
|
// println("Maven local path: ${mavenLocalPath.absolutePath}")
|
||||||
|
// println("Maven local exists: ${mavenLocalPath.exists()}")
|
||||||
|
|
||||||
|
maven { url = uri(mavenLocalPath) }
|
||||||
|
|
||||||
|
maven { url = uri("https://maven.aliyun.com/repository/public") }
|
||||||
|
maven { url = uri("https://maven.aliyun.com/repository/google") }
|
||||||
|
maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") }
|
||||||
|
maven { url = uri("https://maven.aliyun.com/repository/central") }
|
||||||
|
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven { url = uri("https://maven.aliyun.com/repository/public/") }
|
|
||||||
maven { url = uri("https://maven.aliyun.com/repository/google/") }
|
|
||||||
maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin/") }
|
|
||||||
maven { url = uri("https://storage.googleapis.com/download.flutter.io")}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
|
val newBuildDir: Directory =
|
||||||
|
rootProject.layout.buildDirectory
|
||||||
|
.dir("../../build")
|
||||||
|
.get()
|
||||||
rootProject.layout.buildDirectory.value(newBuildDir)
|
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||||
|
|
||||||
subprojects {
|
subprojects {
|
||||||
663
example/android/build/reports/problems/problems-report.html
Normal file
3
example/android/gradle.properties
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.enableJetifier=true
|
||||||
5
example/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
|
||||||
BIN
example/android/libs/ai_assistant_plugin-release.aar
Normal file
37
example/android/settings.gradle.kts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
pluginManagement {
|
||||||
|
val flutterSdkPath =
|
||||||
|
run {
|
||||||
|
val properties = java.util.Properties()
|
||||||
|
file("local.properties").inputStream().use { properties.load(it) }
|
||||||
|
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
|
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||||
|
flutterSdkPath
|
||||||
|
}
|
||||||
|
|
||||||
|
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
maven { url = uri("https://maven.aliyun.com/repository/public") }
|
||||||
|
maven { url = uri("https://maven.aliyun.com/repository/google") }
|
||||||
|
maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") }
|
||||||
|
maven { url = uri("https://maven.aliyun.com/repository/central") }
|
||||||
|
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
|
||||||
|
flatDir { dirs(file("libs")) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
|
id("com.android.application") version "8.9.1" apply false
|
||||||
|
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
include(":app")
|
||||||
|
|
||||||
|
// 显式包含 flutter_vosk_wakeword 插件的 android 模块
|
||||||
|
include(":flutter_vosk_wakeword")
|
||||||
|
project(":flutter_vosk_wakeword").projectDir = File(settings.rootDir, "../../packages/flutter_vosk_wakeword/android")
|
||||||
3
example/devtools_options.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
description: This file stores settings for Dart & Flutter DevTools.
|
||||||
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||||
|
extensions:
|
||||||
88
example/lib/main.dart
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:ai_chat_assistant/ai_chat_assistant.dart';
|
||||||
|
|
||||||
|
import 'pages/home.dart';
|
||||||
|
|
||||||
|
class VehicleCommandClent extends VehicleCommandHandler {
|
||||||
|
@override
|
||||||
|
AIChatCommandCubit? get commandCubit => super.commandCubit;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<(bool, Map<String, dynamic>?)> executeCommand(VehicleCommand command) async {
|
||||||
|
// 在这里实现具体的车控命令执行逻辑
|
||||||
|
print('执行车控命令: ${command.type}, 参数: ${command.params}');
|
||||||
|
|
||||||
|
if (commandCubit != null) {
|
||||||
|
commandCubit?.emit(AIChatCommandState(
|
||||||
|
commandId: command.commandId,
|
||||||
|
commandType: command.type,
|
||||||
|
params: command.params,
|
||||||
|
status: AIChatCommandStatus.executing,
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
errorMessage: null,
|
||||||
|
result: null,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟命令执行完成
|
||||||
|
await Future.delayed(const Duration(seconds: 2));
|
||||||
|
|
||||||
|
if (commandCubit != null) {
|
||||||
|
commandCubit?.emit(AIChatCommandState(
|
||||||
|
commandId: command.commandId,
|
||||||
|
commandType: command.type,
|
||||||
|
params: command.params,
|
||||||
|
status: AIChatCommandStatus.success,
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
errorMessage: null,
|
||||||
|
result: {'message': '命令执行成功'},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Future.value((true, {'message': '命令已执行'}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
// 请求麦克风权限
|
||||||
|
if (!await Permission.microphone.isGranted) {
|
||||||
|
await Permission.microphone.request();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化 AI Chat Assistant,注册车控命令回调
|
||||||
|
// ChatAssistantApp.initialize(
|
||||||
|
// commandCallback: (VehicleCommandType type, Map<String, dynamic>? params) async {
|
||||||
|
// // 这里是示例的车控命令处理逻辑
|
||||||
|
// print('收到车控命令: $type, 参数: $params');
|
||||||
|
// return Future.value((true, {'message': '命令已执行'}));
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
|
||||||
|
AIChatAssistantManager.instance.setupCommandHandle(commandHandler: VehicleCommandClent());
|
||||||
|
|
||||||
|
runApp(const MyApp());
|
||||||
|
|
||||||
|
// AIChatAssistantManager.instance.setWakeWordDetection(true);
|
||||||
|
// AIChatAssistantManager.instance.startVoskWakeword();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyApp extends StatelessWidget {
|
||||||
|
const MyApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
title: 'AI Chat Assistant Example',
|
||||||
|
theme: ThemeData(
|
||||||
|
primarySwatch: Colors.blue,
|
||||||
|
useMaterial3: true,
|
||||||
|
),
|
||||||
|
home: HomePage(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
62
example/lib/pages/home.dart
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:ai_chat_assistant/ai_chat_assistant.dart';
|
||||||
|
|
||||||
|
import 'setting.dart';
|
||||||
|
|
||||||
|
class ChatAssistantPage extends StatelessWidget {
|
||||||
|
const ChatAssistantPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: ChatAssistantApp(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HomePage extends StatefulWidget {
|
||||||
|
HomePage({super.key});
|
||||||
|
|
||||||
|
final items = [
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
icon: Icon(Icons.home),
|
||||||
|
label: 'Home',
|
||||||
|
),
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
icon: Icon(Icons.settings),
|
||||||
|
label: 'Settings',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<HomePage> createState() => _HomePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomePageState extends State<HomePage> {
|
||||||
|
int _selectedIndex = 0;
|
||||||
|
|
||||||
|
final List<Widget> _pages = <Widget>[
|
||||||
|
ChatAssistantPage(),
|
||||||
|
SettingPage(),
|
||||||
|
];
|
||||||
|
|
||||||
|
void _onItemTapped(int index) {
|
||||||
|
setState(() {
|
||||||
|
_selectedIndex = index;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: _pages.elementAt(_selectedIndex),
|
||||||
|
bottomNavigationBar: BottomNavigationBar(
|
||||||
|
items: widget.items,
|
||||||
|
selectedItemColor: Colors.blue,
|
||||||
|
unselectedItemColor: Colors.grey,
|
||||||
|
currentIndex: _selectedIndex,
|
||||||
|
onTap: _onItemTapped,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
example/lib/pages/setting.dart
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class SettingPage extends StatefulWidget {
|
||||||
|
const SettingPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SettingPage> createState() => _SettingPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SettingPageState extends State<SettingPage> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('设置'),
|
||||||
|
),
|
||||||
|
body: const Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text('设置页面'),
|
||||||
|
SizedBox(height: 20),
|
||||||
|
Text('这里可以添加更多设置选项'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
646
example/pubspec.lock
Normal file
@@ -0,0 +1,646 @@
|
|||||||
|
# Generated by pub
|
||||||
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
|
packages:
|
||||||
|
ai_chat_assistant:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
path: ".."
|
||||||
|
relative: true
|
||||||
|
source: path
|
||||||
|
version: "1.0.0+1"
|
||||||
|
ai_chat_core:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
path: "../packages/ai_chat_core"
|
||||||
|
relative: true
|
||||||
|
source: path
|
||||||
|
version: "0.1.0"
|
||||||
|
args:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: args
|
||||||
|
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.0"
|
||||||
|
async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: async
|
||||||
|
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.11.0"
|
||||||
|
audioplayers:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: audioplayers
|
||||||
|
sha256: c05c6147124cd63e725e861335a8b4d57300b80e6e92cea7c145c739223bbaef
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "5.2.1"
|
||||||
|
audioplayers_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: audioplayers_android
|
||||||
|
sha256: b00e1a0e11365d88576320ec2d8c192bc21f1afb6c0e5995d1c57ae63156acb5
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.3"
|
||||||
|
audioplayers_darwin:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: audioplayers_darwin
|
||||||
|
sha256: "3034e99a6df8d101da0f5082dcca0a2a99db62ab1d4ddb3277bed3f6f81afe08"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "5.0.2"
|
||||||
|
audioplayers_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: audioplayers_linux
|
||||||
|
sha256: "60787e73fefc4d2e0b9c02c69885402177e818e4e27ef087074cf27c02246c9e"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.0"
|
||||||
|
audioplayers_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: audioplayers_platform_interface
|
||||||
|
sha256: "365c547f1bb9e77d94dd1687903a668d8f7ac3409e48e6e6a3668a1ac2982adb"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.0"
|
||||||
|
audioplayers_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: audioplayers_web
|
||||||
|
sha256: "22cd0173e54d92bd9b2c80b1204eb1eb159ece87475ab58c9788a70ec43c2a62"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.0"
|
||||||
|
audioplayers_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: audioplayers_windows
|
||||||
|
sha256: "9536812c9103563644ada2ef45ae523806b0745f7a78e89d1b5fb1951de90e1a"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.0"
|
||||||
|
boolean_selector:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: boolean_selector
|
||||||
|
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
|
characters:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: characters
|
||||||
|
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
|
clock:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: clock
|
||||||
|
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
|
collection:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: collection
|
||||||
|
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.19.0"
|
||||||
|
crypto:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: crypto
|
||||||
|
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.6"
|
||||||
|
cupertino_icons:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: cupertino_icons
|
||||||
|
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.8"
|
||||||
|
fake_async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fake_async
|
||||||
|
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.1"
|
||||||
|
ffi:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ffi
|
||||||
|
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.3"
|
||||||
|
file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file
|
||||||
|
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.1"
|
||||||
|
flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
flutter_lints:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: flutter_lints
|
||||||
|
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "5.0.0"
|
||||||
|
flutter_markdown:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_markdown
|
||||||
|
sha256: "08fb8315236099ff8e90cb87bb2b935e0a724a3af1623000a9cec930468e0f27"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.7+1"
|
||||||
|
flutter_markdown_plus:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_markdown_plus
|
||||||
|
sha256: fe74214c5ac2f850d93efda290dcde3f18006e90a87caa9e3e6c13222a5db4de
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.3"
|
||||||
|
flutter_test:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
flutter_tts:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_tts
|
||||||
|
sha256: bdf2fc4483e74450dc9fc6fe6a9b6a5663e108d4d0dad3324a22c8e26bf48af4
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "4.2.3"
|
||||||
|
flutter_voice_processor:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_voice_processor
|
||||||
|
sha256: "8ae3fc196d6060a13392e4ca557f7a2f4a6b87898ae519787f6929f986538572"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.2"
|
||||||
|
flutter_vosk_wakeword:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
path: "../packages/flutter_vosk_wakeword"
|
||||||
|
relative: true
|
||||||
|
source: path
|
||||||
|
version: "0.0.1"
|
||||||
|
flutter_web_plugins:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
fluttertoast:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fluttertoast
|
||||||
|
sha256: "25e51620424d92d3db3832464774a6143b5053f15e382d8ffbfd40b6e795dcf1"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "8.2.12"
|
||||||
|
http:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http
|
||||||
|
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.0"
|
||||||
|
http_parser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http_parser
|
||||||
|
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.2"
|
||||||
|
js:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: js
|
||||||
|
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.7"
|
||||||
|
leak_tracker:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker
|
||||||
|
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "10.0.7"
|
||||||
|
leak_tracker_flutter_testing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker_flutter_testing
|
||||||
|
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.8"
|
||||||
|
leak_tracker_testing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker_testing
|
||||||
|
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.1"
|
||||||
|
lints:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: lints
|
||||||
|
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "5.1.1"
|
||||||
|
markdown:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: markdown
|
||||||
|
sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "7.3.0"
|
||||||
|
matcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: matcher
|
||||||
|
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.12.16+1"
|
||||||
|
material_color_utilities:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: material_color_utilities
|
||||||
|
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.11.1"
|
||||||
|
meta:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: meta
|
||||||
|
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.15.0"
|
||||||
|
nested:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: nested
|
||||||
|
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
path:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path
|
||||||
|
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.9.0"
|
||||||
|
path_provider:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider
|
||||||
|
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.5"
|
||||||
|
path_provider_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_android
|
||||||
|
sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.17"
|
||||||
|
path_provider_foundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_foundation
|
||||||
|
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
path_provider_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_linux
|
||||||
|
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.1"
|
||||||
|
path_provider_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_platform_interface
|
||||||
|
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
path_provider_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_windows
|
||||||
|
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.0"
|
||||||
|
permission_handler:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler
|
||||||
|
sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "12.0.1"
|
||||||
|
permission_handler_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_android
|
||||||
|
sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "13.0.1"
|
||||||
|
permission_handler_apple:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_apple
|
||||||
|
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "9.4.7"
|
||||||
|
permission_handler_html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_html
|
||||||
|
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.3+5"
|
||||||
|
permission_handler_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_platform_interface
|
||||||
|
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "4.3.0"
|
||||||
|
permission_handler_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_windows
|
||||||
|
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.1"
|
||||||
|
platform:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: platform
|
||||||
|
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.6"
|
||||||
|
plugin_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: plugin_platform_interface
|
||||||
|
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.8"
|
||||||
|
porcupine_flutter:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: porcupine_flutter
|
||||||
|
sha256: "5618b20a00c44aab80763a70cab993037dd73d924dd2a24154c36dc13bdf5c8e"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.5"
|
||||||
|
provider:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: provider
|
||||||
|
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.5+1"
|
||||||
|
record:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: record
|
||||||
|
sha256: "9dbc6ff3e784612f90a9b001373c45ff76b7a08abd2bd9fdf72c242320c8911c"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.1"
|
||||||
|
record_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: record_android
|
||||||
|
sha256: "8361a791c9a3fa5c065f0b8b5adb10f12531f8538c86b19474cf7b56ea80d426"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.1"
|
||||||
|
record_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: record_ios
|
||||||
|
sha256: "13e241ed9cbc220534a40ae6b66222e21288db364d96dd66fb762ebd3cb77c71"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.2"
|
||||||
|
record_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: record_linux
|
||||||
|
sha256: "235b1f1fb84e810f8149cc0c2c731d7d697f8d1c333b32cb820c449bf7bb72d8"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.1"
|
||||||
|
record_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: record_macos
|
||||||
|
sha256: "2849068bb59072f300ad63ed146e543d66afaef8263edba4de4834fc7c8d4d35"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
|
record_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: record_platform_interface
|
||||||
|
sha256: b0065fdf1ec28f5a634d676724d388a77e43ce7646fb049949f58c69f3fcb4ed
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
|
record_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: record_web
|
||||||
|
sha256: "4f0adf20c9ccafcc02d71111fd91fba1ca7b17a7453902593e5a9b25b74a5c56"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.0"
|
||||||
|
record_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: record_windows
|
||||||
|
sha256: "223258060a1d25c62bae18282c16783f28581ec19401d17e56b5205b9f039d78"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.7"
|
||||||
|
sky_engine:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
source_span:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_span
|
||||||
|
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.10.0"
|
||||||
|
stack_trace:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stack_trace
|
||||||
|
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.12.0"
|
||||||
|
stream_channel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stream_channel
|
||||||
|
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
string_scanner:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: string_scanner
|
||||||
|
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
|
synchronized:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: synchronized
|
||||||
|
sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.3.0+3"
|
||||||
|
t_basic_intl:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
path: "../packages/basic_intl"
|
||||||
|
relative: true
|
||||||
|
source: path
|
||||||
|
version: "0.2.0"
|
||||||
|
term_glyph:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: term_glyph
|
||||||
|
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.1"
|
||||||
|
test_api:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: test_api
|
||||||
|
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.3"
|
||||||
|
typed_data:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: typed_data
|
||||||
|
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
|
uuid:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: uuid
|
||||||
|
sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.7"
|
||||||
|
vector_math:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_math
|
||||||
|
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.4"
|
||||||
|
vm_service:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vm_service
|
||||||
|
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "14.3.0"
|
||||||
|
web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web
|
||||||
|
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
|
xdg_directories:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xdg_directories
|
||||||
|
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
|
sdks:
|
||||||
|
dart: ">=3.6.2 <4.0.0"
|
||||||
|
flutter: ">=3.27.1"
|
||||||
52
example/pubspec.yaml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
name: ai_chat_example
|
||||||
|
description: "AI Chat Assistant Example App"
|
||||||
|
# The following line prevents the package from being accidentally published to
|
||||||
|
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||||
|
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
|
|
||||||
|
# The following defines the version and build number for your application.
|
||||||
|
# A version number is three numbers separated by dots, like 1.2.43
|
||||||
|
# followed by an optional build number separated by a +.
|
||||||
|
# Both the version and the builder number may be overridden in flutter
|
||||||
|
# build by specifying --build-name and --build-number, respectively.
|
||||||
|
# In Android, build-name is used as versionName while build-number used as versionCode.
|
||||||
|
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
||||||
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
|
||||||
|
# Read more about iOS versioning at
|
||||||
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
|
version: 1.0.0+1
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ^3.5.0
|
||||||
|
|
||||||
|
# Dependencies specify other packages that your package needs in order to work.
|
||||||
|
# To automatically upgrade your package dependencies to the latest versions
|
||||||
|
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
||||||
|
# dependencies can be manually updated by changing the version numbers below to
|
||||||
|
# the latest version available on pub.dev. To see which dependencies have newer
|
||||||
|
# versions available, run `flutter pub outdated`.
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
|
# The following adds the Cupertino Icons font to your application.
|
||||||
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
|
cupertino_icons: ^1.0.8
|
||||||
|
|
||||||
|
# 添加本地 AI Chat Assistant plugin 依赖
|
||||||
|
ai_chat_assistant:
|
||||||
|
path: ../
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_lints: ^5.0.0
|
||||||
|
|
||||||
|
# overrides_dependencies:
|
||||||
|
# flutter_vosk_wakeword:
|
||||||
|
# path: ../packages/flutter_vosk_wakeword
|
||||||
|
|
||||||
|
flutter:
|
||||||
|
uses-material-design: true
|
||||||
22
lib/ai_chat_assistant.dart
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
|
||||||
|
// widgets
|
||||||
|
export 'pages/full_screen.dart';
|
||||||
|
export 'screens/part_screen.dart';
|
||||||
|
export 'app.dart';
|
||||||
|
export 'widgets/floating_icon.dart';
|
||||||
|
|
||||||
|
// easy bloc
|
||||||
|
export 'bloc/easy_bloc.dart';
|
||||||
|
export 'bloc/ai_chat_cubit.dart';
|
||||||
|
export 'bloc/command_state.dart';
|
||||||
|
|
||||||
|
// services
|
||||||
|
export 'manager.dart';
|
||||||
|
export 'services/message_service.dart';
|
||||||
|
export 'services/command_service.dart';
|
||||||
|
|
||||||
|
// types && enums && models
|
||||||
|
export 'package:ai_chat_core/ai_chat_core.dart';
|
||||||
|
|
||||||
|
// utils
|
||||||
|
export 'utils/assets_util.dart';
|
||||||
22
lib/bloc/ai_chat_cubit.dart
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import 'easy_bloc.dart';
|
||||||
|
import 'command_state.dart';
|
||||||
|
|
||||||
|
class AIChatCommandCubit extends EasyCubit<AIChatCommandState> {
|
||||||
|
AIChatCommandCubit() : super(const AIChatCommandState());
|
||||||
|
|
||||||
|
// 重置状态
|
||||||
|
void reset() {
|
||||||
|
emit(const AIChatCommandState());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成唯一命令ID
|
||||||
|
String _generateCommandId() {
|
||||||
|
return '${DateTime.now().millisecondsSinceEpoch}_${state.commandType?.name ?? 'unknown'}';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查当前是否有命令在执行
|
||||||
|
bool get isExecuting => state.status == AIChatCommandStatus.executing;
|
||||||
|
|
||||||
|
// 获取当前命令ID
|
||||||
|
String? get currentCommandId => state.commandId;
|
||||||
|
}
|
||||||
69
lib/bloc/command_state.dart
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
|
||||||
|
// AI Chat Command States
|
||||||
|
import 'package:ai_chat_core/ai_chat_core.dart';
|
||||||
|
|
||||||
|
enum AIChatCommandStatus {
|
||||||
|
idle,
|
||||||
|
executing,
|
||||||
|
success,
|
||||||
|
failure,
|
||||||
|
cancelled,
|
||||||
|
}
|
||||||
|
|
||||||
|
class AIChatCommandState {
|
||||||
|
final String? commandId;
|
||||||
|
final VehicleCommandType? commandType;
|
||||||
|
final Map<String, dynamic>? params;
|
||||||
|
final AIChatCommandStatus status;
|
||||||
|
final String? errorMessage;
|
||||||
|
final Map<String, dynamic>? result;
|
||||||
|
final DateTime? timestamp;
|
||||||
|
|
||||||
|
const AIChatCommandState({
|
||||||
|
this.commandId,
|
||||||
|
this.commandType,
|
||||||
|
this.params,
|
||||||
|
this.status = AIChatCommandStatus.idle,
|
||||||
|
this.errorMessage,
|
||||||
|
this.result,
|
||||||
|
this.timestamp,
|
||||||
|
});
|
||||||
|
|
||||||
|
AIChatCommandState copyWith({
|
||||||
|
String? commandId,
|
||||||
|
VehicleCommandType? commandType,
|
||||||
|
Map<String, dynamic>? params,
|
||||||
|
AIChatCommandStatus? status,
|
||||||
|
String? errorMessage,
|
||||||
|
Map<String, dynamic>? result,
|
||||||
|
DateTime? timestamp,
|
||||||
|
}) {
|
||||||
|
return AIChatCommandState(
|
||||||
|
commandId: commandId ?? this.commandId,
|
||||||
|
commandType: commandType ?? this.commandType,
|
||||||
|
params: params ?? this.params,
|
||||||
|
status: status ?? this.status,
|
||||||
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
|
result: result ?? this.result,
|
||||||
|
timestamp: timestamp ?? this.timestamp,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
return other is AIChatCommandState &&
|
||||||
|
other.commandId == commandId &&
|
||||||
|
other.commandType == commandType &&
|
||||||
|
other.status == status &&
|
||||||
|
other.errorMessage == errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return commandId.hashCode ^
|
||||||
|
commandType.hashCode ^
|
||||||
|
status.hashCode ^
|
||||||
|
errorMessage.hashCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
78
lib/bloc/easy_bloc.dart
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
// 简易的Bloc, 简易的状态管理
|
||||||
|
|
||||||
|
abstract class EasyBlocBase<State> {
|
||||||
|
|
||||||
|
EasyBlocBase(this.state): assert(state != null);
|
||||||
|
|
||||||
|
final StreamController<State> _stateController = StreamController<State>.broadcast();
|
||||||
|
|
||||||
|
Stream<State> get stream => _stateController.stream;
|
||||||
|
State state;
|
||||||
|
|
||||||
|
bool _emitted = false;
|
||||||
|
|
||||||
|
// 添加监听器方法
|
||||||
|
StreamSubscription<State> listen(
|
||||||
|
void Function(State state) onData, {
|
||||||
|
Function? onError,
|
||||||
|
void Function()? onDone,
|
||||||
|
bool? cancelOnError,
|
||||||
|
}) {
|
||||||
|
return stream.listen(
|
||||||
|
onData,
|
||||||
|
onError: onError,
|
||||||
|
onDone: onDone,
|
||||||
|
cancelOnError: cancelOnError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新状态
|
||||||
|
void emit(State newState) {
|
||||||
|
if (_stateController.isClosed) return;
|
||||||
|
if (newState == state && _emitted) return;
|
||||||
|
this.state = newState;
|
||||||
|
_stateController.add(state);
|
||||||
|
_emitted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mustCallSuper
|
||||||
|
Future<void> close() async {
|
||||||
|
await _stateController.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EasyBloc<Event, State> extends EasyBlocBase<State> {
|
||||||
|
EasyBloc(State initialState) : super(initialState) {
|
||||||
|
_eventController.stream.listen(_mapEventToState);
|
||||||
|
}
|
||||||
|
|
||||||
|
final StreamController<Event> _eventController = StreamController<Event>();
|
||||||
|
|
||||||
|
// 子类需要实现这个方法来处理事件
|
||||||
|
@mustBeOverridden
|
||||||
|
void _mapEventToState(Event event) {
|
||||||
|
// 默认不做任何处理,如果不实现会抛出异常
|
||||||
|
throw UnimplementedError('_mapEventToState must be implemented by subclasses');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加事件
|
||||||
|
void add(Event event) {
|
||||||
|
if (_eventController.isClosed) return;
|
||||||
|
_eventController.add(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() async {
|
||||||
|
await _eventController.close();
|
||||||
|
await super.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EasyCubit<State> extends EasyBlocBase<State> {
|
||||||
|
EasyCubit(State initialState) : super(initialState);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
enum MessageServiceState {
|
|
||||||
idle,
|
|
||||||
recording,
|
|
||||||
recognizing,
|
|
||||||
replying,
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
|
||||||
import 'app.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'services/message_service.dart';
|
|
||||||
|
|
||||||
void main() async {
|
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
|
||||||
if (!await Permission.microphone.isGranted) {
|
|
||||||
await Permission.microphone.request();
|
|
||||||
}
|
|
||||||
runApp(
|
|
||||||
ChangeNotifierProvider(
|
|
||||||
create: (_) => MessageService(),
|
|
||||||
child: const ChatAssistantApp(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
183
lib/manager.dart
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:ai_chat_core/ai_chat_core.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_vosk_wakeword/flutter_vosk_wakeword.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:porcupine_flutter/porcupine_error.dart';
|
||||||
|
import 'package:porcupine_flutter/porcupine_manager.dart';
|
||||||
|
|
||||||
|
import 'bloc/ai_chat_cubit.dart';
|
||||||
|
import 'bloc/command_state.dart';
|
||||||
|
|
||||||
|
/// 车辆命令处理器抽象类
|
||||||
|
abstract class VehicleCommandHandler {
|
||||||
|
AIChatCommandCubit? commandCubit;
|
||||||
|
|
||||||
|
/// 执行车辆控制命令
|
||||||
|
Future<(bool, Map<String, dynamic>?)> executeCommand(VehicleCommand command);
|
||||||
|
}
|
||||||
|
|
||||||
|
class AIChatAssistantManager {
|
||||||
|
static final AIChatAssistantManager _instance = AIChatAssistantManager._internal();
|
||||||
|
|
||||||
|
static AIChatAssistantManager get instance => _instance;
|
||||||
|
|
||||||
|
AIChatAssistantManager._internal() {
|
||||||
|
// 初始化代码
|
||||||
|
Logger.i('AIChatAssistant 单例创建');
|
||||||
|
_commandCubit = AIChatCommandCubit();
|
||||||
|
}
|
||||||
|
|
||||||
|
AIChatCommandCubit? _commandCubit;
|
||||||
|
|
||||||
|
VehicleCommandHandler? commandClient;
|
||||||
|
|
||||||
|
/// 初始化命令处理器
|
||||||
|
void setupCommandHandle({
|
||||||
|
required VehicleCommandHandler commandHandler,
|
||||||
|
}) {
|
||||||
|
commandClient = commandHandler;
|
||||||
|
commandClient?.commandCubit = _commandCubit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取命令状态流
|
||||||
|
Stream<AIChatCommandState> get commandStateStream {
|
||||||
|
if (_commandCubit == null) {
|
||||||
|
throw StateError('AIChatAssistant 未初始化');
|
||||||
|
}
|
||||||
|
return _commandCubit!.stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Porcupine 唤醒词属性 ---
|
||||||
|
|
||||||
|
PorcupineManager? _porcupineManager;
|
||||||
|
final StreamController<void> _wakeWordController = StreamController<void>.broadcast();
|
||||||
|
bool _isWakeWordEnabled = false;
|
||||||
|
bool _wakeWordDetected = false;
|
||||||
|
|
||||||
|
// PicoVoice AccessKey
|
||||||
|
final String _picoAccessKey = "CRkyWZ3DRFXVogXEhE2eK2/ZN1q9YEYweTnjVWlve86nKAid5i1zsQ==";
|
||||||
|
|
||||||
|
// 资产逻辑路径 (用于 rootBundle.load)
|
||||||
|
static const String _keywordAssetPath = "packages/ai_chat_assistant/assets/zhongzhong_zh_android.ppn";
|
||||||
|
static const String _modelAssetPath = "packages/ai_chat_assistant/assets/porcupine_params_zh.pv";
|
||||||
|
|
||||||
|
// 真实文件系统路径 (Porcupine 使用)
|
||||||
|
String? _keywordFilePath;
|
||||||
|
String? _modelFilePath;
|
||||||
|
|
||||||
|
/// 监听唤醒词事件的 Stream
|
||||||
|
Stream<void> get onWakeWordDetected => _wakeWordController.stream;
|
||||||
|
|
||||||
|
/// 当前是否检测到了唤醒词
|
||||||
|
bool get isWakeWordDetected => _wakeWordDetected;
|
||||||
|
|
||||||
|
/// 将 Flutter asset 写入设备文件系统,返回其真实路径
|
||||||
|
Future<String> _extractAsset(String assetPath, String fileName) async {
|
||||||
|
final dir = await getApplicationSupportDirectory();
|
||||||
|
final file = File('${dir.path}/$fileName');
|
||||||
|
if (!file.existsSync()) {
|
||||||
|
final data = await rootBundle.load(assetPath);
|
||||||
|
await file.writeAsBytes(data.buffer.asUint8List(), flush: true);
|
||||||
|
Logger.i('Asset "$assetPath" extracted to "${file.path}"');
|
||||||
|
}
|
||||||
|
return file.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> startVoskWakeword() async {
|
||||||
|
FlutterVoskWakeword.instance.initialize(
|
||||||
|
wakeWords: ['你好众众', '你好', '众众', '测试', '哈喽', '唤醒'], // 唤醒词列表
|
||||||
|
);
|
||||||
|
|
||||||
|
Logger.i('Starting Vosk Wakeword detection...');
|
||||||
|
FlutterVoskWakeword.instance.start();
|
||||||
|
|
||||||
|
FlutterVoskWakeword.instance.recognitionStream.listen((event) {
|
||||||
|
Logger.i('Vosk Wakeword detected: ${event.text}');
|
||||||
|
_wakeWordDetected = true;
|
||||||
|
// 通知所有监听者
|
||||||
|
_wakeWordController.add(null);
|
||||||
|
// 监听到之后就停止,这个时候已经弹出了对话框
|
||||||
|
// 对话框消失就可以继续监听 recognitionStream 的订阅不会断开
|
||||||
|
FlutterVoskWakeword.instance.stop();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> stopVoskWakeword() async {
|
||||||
|
if (_wakeWordDetected) {
|
||||||
|
_wakeWordDetected = false;
|
||||||
|
}
|
||||||
|
if (FlutterVoskWakeword.instance != null) {
|
||||||
|
FlutterVoskWakeword.instance.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 开启或关闭唤醒词检测
|
||||||
|
Future<void> setWakeWordDetection(bool enable) async {
|
||||||
|
if (enable == _isWakeWordEnabled && _porcupineManager != null) {
|
||||||
|
return; // 状态未改变
|
||||||
|
}
|
||||||
|
_isWakeWordEnabled = enable;
|
||||||
|
|
||||||
|
if (enable) {
|
||||||
|
try {
|
||||||
|
// 确保关键词和模型文件已提取到文件系统
|
||||||
|
_keywordFilePath ??= await _extractAsset(_keywordAssetPath, "zhongzhong_zh_android.ppn");
|
||||||
|
_modelFilePath ??= await _extractAsset(_modelAssetPath, "porcupine_params_zh.pv");
|
||||||
|
|
||||||
|
_porcupineManager = await PorcupineManager.fromKeywordPaths(
|
||||||
|
_picoAccessKey,
|
||||||
|
[_keywordFilePath!],
|
||||||
|
_wakeWordDetectedCallback,
|
||||||
|
modelPath: _modelFilePath, // 中文模型必须指定 modelPath
|
||||||
|
errorCallback: _porcupineErrorCallback,
|
||||||
|
sensitivities: [0.6], // 可以调整灵敏度
|
||||||
|
);
|
||||||
|
await _porcupineManager?.start();
|
||||||
|
Logger.i("Porcupine manager started for wake-word detection.");
|
||||||
|
} on PorcupineException catch (e) {
|
||||||
|
Logger.e("Failed to init Porcupine: ${e.message}");
|
||||||
|
_isWakeWordEnabled = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await _porcupineManager?.stop();
|
||||||
|
await _porcupineManager?.delete();
|
||||||
|
_porcupineManager = null;
|
||||||
|
_wakeWordDetected = false; // 关闭时重置状态
|
||||||
|
Logger.i("Porcupine manager stopped and deleted.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 唤醒词检测回调
|
||||||
|
void _wakeWordDetectedCallback(int keywordIndex) {
|
||||||
|
// 我们只用了一个关键词,所以索引总是 0
|
||||||
|
if (keywordIndex == 0) {
|
||||||
|
_wakeWordDetected = true;
|
||||||
|
Logger.i("Wake-word '众众' detected!");
|
||||||
|
// 通知所有监听者
|
||||||
|
_wakeWordController.add(null);
|
||||||
|
|
||||||
|
// 可选:一段时间后自动重置状态
|
||||||
|
Future.delayed(const Duration(seconds: 2), () {
|
||||||
|
_wakeWordDetected = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _porcupineErrorCallback(PorcupineException error) {
|
||||||
|
Logger.e("Porcupine error: ${error.message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 释放资源
|
||||||
|
void dispose() {
|
||||||
|
_commandCubit?.close();
|
||||||
|
_commandCubit = null;
|
||||||
|
_wakeWordController.close();
|
||||||
|
setWakeWordDetection(false); // 确保 Porcupine 停止并释放
|
||||||
|
FlutterVoskWakeword.instance.dispose();
|
||||||
|
Logger.i('AIChatAssistant 单例销毁');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'package:ai_chat_assistant/models/chat_message.dart';
|
import 'package:ai_chat_core/ai_chat_core.dart';
|
||||||
import 'package:basic_intl/intl.dart';
|
import 'package:t_basic_intl/intl.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../enums/message_status.dart';
|
|
||||||
import '../widgets/chat_box.dart';
|
import '../widgets/chat_box.dart';
|
||||||
import '../widgets/gradient_background.dart';
|
import '../widgets/gradient_background.dart';
|
||||||
import '../services/message_service.dart';
|
import '../services/message_service.dart';
|
||||||
@@ -9,14 +8,14 @@ import 'package:provider/provider.dart';
|
|||||||
import '../widgets/chat_header.dart';
|
import '../widgets/chat_header.dart';
|
||||||
import '../widgets/chat_footer.dart';
|
import '../widgets/chat_footer.dart';
|
||||||
|
|
||||||
class FullScreen extends StatefulWidget {
|
class FullScreenPage extends StatefulWidget {
|
||||||
const FullScreen({super.key});
|
const FullScreenPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<FullScreen> createState() => _FullScreenState();
|
State<FullScreenPage> createState() => _FullScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FullScreenState extends State<FullScreen> {
|
class _FullScreenState extends State<FullScreenPage> {
|
||||||
final ScrollController _scrollController = ScrollController();
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../widgets/floating_icon.dart';
|
import '../widgets/floating_icon.dart';
|
||||||
|
import '../utils/assets_util.dart';
|
||||||
|
import '../widgets/chat_popup.dart';
|
||||||
|
import '../widgets/chat_floating_icon.dart';
|
||||||
|
|
||||||
class MainScreen extends StatefulWidget {
|
class MainScreen extends StatefulWidget {
|
||||||
const MainScreen({super.key});
|
const MainScreen({super.key});
|
||||||
@@ -26,14 +29,15 @@ class _MainScreenState extends State<MainScreen> {
|
|||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
image: DecorationImage(
|
image: DecorationImage(
|
||||||
image: AssetImage('assets/images/bg.jpg'),
|
image: AssetsUtil.getImage('bg.jpg'),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
FloatingIcon(),
|
// FloatingIcon(),
|
||||||
|
ChatPopup()
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
import '../widgets/chat_box.dart';
|
import '../widgets/chat_box.dart';
|
||||||
import '../screens/full_screen.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../services/message_service.dart';
|
import '../services/message_service.dart';
|
||||||
import '../widgets/gradient_background.dart';
|
import '../widgets/gradient_background.dart';
|
||||||
|
import '../utils/assets_util.dart';
|
||||||
|
import '../pages/full_screen.dart';
|
||||||
|
|
||||||
class PartScreen extends StatefulWidget {
|
class PartScreen extends StatefulWidget {
|
||||||
final VoidCallback? onHide;
|
final VoidCallback? onHide;
|
||||||
|
final Offset floatingIconPosition;
|
||||||
|
final double iconSize;
|
||||||
|
|
||||||
const PartScreen({super.key, this.onHide});
|
const PartScreen({
|
||||||
|
super.key,
|
||||||
|
this.onHide,
|
||||||
|
required this.floatingIconPosition,
|
||||||
|
required this.iconSize,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PartScreen> createState() => _PartScreenState();
|
State<PartScreen> createState() => _PartScreenState();
|
||||||
@@ -50,14 +58,80 @@ class _PartScreenState extends State<PartScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _openFullScreen() async {
|
void _openFullScreen() async {
|
||||||
|
final messageService = context.read<MessageService>();
|
||||||
|
|
||||||
widget.onHide?.call();
|
widget.onHide?.call();
|
||||||
|
|
||||||
await Navigator.of(context).push(
|
await Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => const FullScreen(),
|
builder: (context) => ChangeNotifierProvider.value(
|
||||||
|
value: messageService, // 传递同一个单例实例
|
||||||
|
child: const FullScreenPage(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 计算PartScreen的位置,使其显示在FloatingIcon附近
|
||||||
|
EdgeInsets _calculatePosition(BoxConstraints constraints) {
|
||||||
|
final screenWidth = constraints.maxWidth;
|
||||||
|
final screenHeight = constraints.maxHeight;
|
||||||
|
// 聊天框的尺寸
|
||||||
|
final chatWidth = screenWidth * 0.94;
|
||||||
|
final maxChatHeight = screenHeight * 0.4;
|
||||||
|
|
||||||
|
final iconX = screenWidth - widget.floatingIconPosition.dx;
|
||||||
|
final iconY = screenHeight - widget.floatingIconPosition.dy;
|
||||||
|
|
||||||
|
// 1. 先将icon的right/bottom转为屏幕坐标(icon左下角)
|
||||||
|
var iconPosition = widget.floatingIconPosition;
|
||||||
|
|
||||||
|
final iconLeft = screenWidth - iconPosition.dx - widget.iconSize;
|
||||||
|
final iconBottom = iconPosition.dy;
|
||||||
|
final iconTop = iconBottom + widget.iconSize;
|
||||||
|
final iconCenterX = iconLeft + widget.iconSize / 2;
|
||||||
|
|
||||||
|
// 判断FloatingIcon在屏幕的哪一边
|
||||||
|
final isOnRightSide = iconX < screenWidth / 2;
|
||||||
|
|
||||||
|
// 计算水平位置
|
||||||
|
double leftPadding;
|
||||||
|
if (isOnRightSide) {
|
||||||
|
// 图标在右边,聊天框显示在左边
|
||||||
|
leftPadding = (screenWidth - chatWidth) * 0.1;
|
||||||
|
} else {
|
||||||
|
// 图标在左边,聊天框显示在右边
|
||||||
|
leftPadding = screenWidth - chatWidth - (screenWidth - chatWidth) * 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优先尝试放在icon上方
|
||||||
|
double chatBottom = iconTop + 12; // 聊天框底部距离icon顶部12px
|
||||||
|
double chatTop = screenHeight - chatBottom - maxChatHeight;
|
||||||
|
|
||||||
|
// 如果上方空间不足,则放在icon下方
|
||||||
|
if (chatTop < 0) {
|
||||||
|
chatBottom = iconBottom - 12 - maxChatHeight / 2; // 聊天框顶部距离icon底部12px
|
||||||
|
// 如果下方也不足,则贴底
|
||||||
|
if (chatBottom < 0) {
|
||||||
|
chatBottom = 20; // 距离底部20px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// // 计算垂直位置,确保聊天框在图标上方
|
||||||
|
// double bottomPadding = iconY + widget.iconSize + 20;
|
||||||
|
//
|
||||||
|
// // 确保聊天框不会超出屏幕
|
||||||
|
// final minBottomPadding = screenHeight * 0.15;
|
||||||
|
// final maxBottomPadding = screenHeight - maxChatHeight - 50;
|
||||||
|
// bottomPadding = bottomPadding.clamp(minBottomPadding, maxBottomPadding);
|
||||||
|
|
||||||
|
return EdgeInsets.only(
|
||||||
|
left: leftPadding,
|
||||||
|
right: screenWidth - leftPadding - chatWidth,
|
||||||
|
bottom: chatBottom,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -67,9 +141,12 @@ class _PartScreenState extends State<PartScreen> {
|
|||||||
if (!_isInitialized) {
|
if (!_isInitialized) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final position = _calculatePosition(constraints);
|
||||||
final double minHeight = constraints.maxHeight * 0.16;
|
final double minHeight = constraints.maxHeight * 0.16;
|
||||||
final double maxHeight = constraints.maxHeight * 0.4;
|
final double maxHeight = constraints.maxHeight * 0.4;
|
||||||
final double chatWidth = constraints.maxWidth * 0.94;
|
final double chatWidth = constraints.maxWidth * 0.94;
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
@@ -92,74 +169,80 @@ class _PartScreenState extends State<PartScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Positioned(
|
||||||
padding: EdgeInsets.only(bottom: constraints.maxHeight * 0.22),
|
left: position.left,
|
||||||
child: Align(
|
right: position.right,
|
||||||
alignment: Alignment.bottomCenter,
|
bottom: position.bottom,
|
||||||
child: Consumer<MessageService>(
|
child: Consumer<MessageService>(
|
||||||
builder: (context, messageService, child) {
|
builder: (context, messageService, child) {
|
||||||
final messageCount = messageService.messages.length;
|
final messageCount = messageService.messages.length;
|
||||||
if (messageCount > _lastMessageCount) {
|
if (messageCount > _lastMessageCount) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_scrollToBottom();
|
_scrollToBottom();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
_lastMessageCount = messageCount;
|
_lastMessageCount = messageCount;
|
||||||
return Container(
|
|
||||||
width: chatWidth,
|
return TweenAnimationBuilder<double>(
|
||||||
constraints: BoxConstraints(
|
tween: Tween<double>(begin: 0.8, end: 1.0),
|
||||||
minHeight: minHeight,
|
duration: const Duration(milliseconds: 200),
|
||||||
maxHeight: maxHeight,
|
curve: Curves.easeOutBack,
|
||||||
),
|
builder: (context, scale, child) {
|
||||||
child: ClipRRect(
|
return Transform.scale(
|
||||||
borderRadius: BorderRadius.circular(24),
|
scale: scale,
|
||||||
child: BackdropFilter(
|
child: Container(
|
||||||
filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
|
width: chatWidth,
|
||||||
child: GradientBackground(
|
constraints: BoxConstraints(
|
||||||
colors: const [
|
minHeight: minHeight,
|
||||||
Color(0XBF3B0A3F),
|
maxHeight: maxHeight,
|
||||||
Color(0xBF0E0E24),
|
),
|
||||||
Color(0xBF0C0B33),
|
child: ClipRRect(
|
||||||
],
|
borderRadius: BorderRadius.circular(24),
|
||||||
borderRadius: BorderRadius.circular(6),
|
child: BackdropFilter(
|
||||||
child: Stack(
|
filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
|
||||||
children: [
|
child: GradientBackground(
|
||||||
Padding(
|
colors: const [
|
||||||
padding: const EdgeInsets.only(
|
Color(0XBF3B0A3F),
|
||||||
top: 50,
|
Color(0xBF0E0E24),
|
||||||
left: 6,
|
Color(0xBF0C0B33),
|
||||||
right: 6,
|
],
|
||||||
bottom: 30),
|
borderRadius: BorderRadius.circular(6),
|
||||||
child: LayoutBuilder(
|
child: Stack(
|
||||||
builder: (context, boxConstraints) {
|
children: [
|
||||||
return ChatBox(
|
Padding(
|
||||||
scrollController: _scrollController,
|
padding: const EdgeInsets.only(
|
||||||
messages: messageService.messages,
|
top: 50, left: 6, right: 6, bottom: 30),
|
||||||
);
|
child: LayoutBuilder(
|
||||||
}
|
builder: (context, boxConstraints) {
|
||||||
),
|
return ChatBox(
|
||||||
),
|
scrollController: _scrollController,
|
||||||
Positioned(
|
messages: messageService.messages,
|
||||||
top: 6,
|
);
|
||||||
right: 6,
|
}),
|
||||||
child: IconButton(
|
|
||||||
icon: Image.asset(
|
|
||||||
'assets/images/open_in_full.png',
|
|
||||||
width: 24,
|
|
||||||
height: 24
|
|
||||||
),
|
),
|
||||||
onPressed: _openFullScreen,
|
Positioned(
|
||||||
padding: EdgeInsets.zero,
|
top: 6,
|
||||||
),
|
right: 6,
|
||||||
|
child: IconButton(
|
||||||
|
icon: AssetsUtil.getImageWidget(
|
||||||
|
'open_in_full.png',
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
),
|
||||||
|
onPressed: _openFullScreen,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
);
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import 'package:ai_chat_core/ai_chat_core.dart';
|
||||||
import 'package:record/record.dart';
|
import 'package:record/record.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
// 负责录音相关功能
|
// 负责录音相关功能
|
||||||
|
@Deprecated('AudioRecorderService Deprecated')
|
||||||
class AudioRecorderService {
|
class AudioRecorderService {
|
||||||
final AudioRecorder _recorder = AudioRecorder();
|
final AudioRecorder _recorder = AudioRecorder();
|
||||||
bool _isRecording = false;
|
bool _isRecording = false;
|
||||||
@@ -15,11 +17,11 @@ class AudioRecorderService {
|
|||||||
|
|
||||||
Future<void> checkPermission() async {
|
Future<void> checkPermission() async {
|
||||||
final hasPermission = await _recorder.hasPermission();
|
final hasPermission = await _recorder.hasPermission();
|
||||||
print('麦克风权限状态: $hasPermission');
|
Logger.i('麦克风权限状态: $hasPermission');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> startRecording() async {
|
Future<void> startRecording() async {
|
||||||
print('开始录音...');
|
Logger.i('开始录音...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 使用文件录音可能更稳定
|
// 使用文件录音可能更稳定
|
||||||
@@ -27,7 +29,7 @@ class AudioRecorderService {
|
|||||||
_tempFilePath =
|
_tempFilePath =
|
||||||
'${tempDir.path}/temp_audio_${DateTime.now().millisecondsSinceEpoch}.opus';
|
'${tempDir.path}/temp_audio_${DateTime.now().millisecondsSinceEpoch}.opus';
|
||||||
|
|
||||||
print('录音文件路径: $_tempFilePath');
|
Logger.i('录音文件路径: $_tempFilePath');
|
||||||
|
|
||||||
if (await _recorder.hasPermission()) {
|
if (await _recorder.hasPermission()) {
|
||||||
await _recorder.start(
|
await _recorder.start(
|
||||||
@@ -36,12 +38,12 @@ class AudioRecorderService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
_isRecording = true;
|
_isRecording = true;
|
||||||
print('录音已开始,使用OPUS格式,文件: $_tempFilePath');
|
Logger.i('录音已开始,使用OPUS格式,文件: $_tempFilePath');
|
||||||
} else {
|
} else {
|
||||||
print('没有麦克风权限,无法开始录音');
|
Logger.e('没有麦克风权限,无法开始录音');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('录音开始出错: $e');
|
Logger.e('录音开始出错: $e');
|
||||||
_isRecording = false;
|
_isRecording = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,30 +53,30 @@ class AudioRecorderService {
|
|||||||
|
|
||||||
_isRecording = false;
|
_isRecording = false;
|
||||||
|
|
||||||
print('停止录音...');
|
Logger.i('停止录音...');
|
||||||
try {
|
try {
|
||||||
final path = await _recorder.stop();
|
final path = await _recorder.stop();
|
||||||
print('录音已停止,文件路径: $path');
|
Logger.i('录音已停止,文件路径: $path');
|
||||||
|
|
||||||
if (path != null) {
|
if (path != null) {
|
||||||
final file = File(path);
|
final file = File(path);
|
||||||
if (await file.exists()) {
|
if (await file.exists()) {
|
||||||
final bytes = await file.readAsBytes();
|
final bytes = await file.readAsBytes();
|
||||||
print('读取到录音数据: ${bytes.length} 字节');
|
Logger.i('读取到录音数据: ${bytes.length} 字节');
|
||||||
|
|
||||||
if (bytes.isNotEmpty) {
|
if (bytes.isNotEmpty) {
|
||||||
return bytes;
|
return bytes;
|
||||||
} else {
|
} else {
|
||||||
print('录音数据为空');
|
Logger.e('录音数据为空');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
print('录音文件不存在: $path');
|
Logger.e('录音文件不存在: $path');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
print('录音路径为空');
|
Logger.e('录音路径为空');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('停止录音或发送过程出错: $e');
|
Logger.e('停止录音或发送过程出错: $e');
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
// 负责SSE通信
|
// 负责SSE通信
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io' as IO;
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:ai_chat_assistant/utils/tts_util.dart';
|
import 'package:ai_chat_assistant/ai_chat_assistant.dart';
|
||||||
|
|
||||||
|
import '../utils/tts_util.dart';
|
||||||
|
|
||||||
import '../utils/common_util.dart';
|
import '../utils/common_util.dart';
|
||||||
|
|
||||||
|
@Deprecated('ChatSseService Deprecated, Use ai_chat_core/ChatSseService instead')
|
||||||
|
/// SSE 服务类,处理与后端的流式通信
|
||||||
class ChatSseService {
|
class ChatSseService {
|
||||||
// 缓存用户ID和会话ID
|
// 缓存用户ID和会话ID
|
||||||
String? _cachedUserId;
|
String? _cachedUserId;
|
||||||
String? _cachedConversationId;
|
String? _cachedConversationId;
|
||||||
|
|
||||||
HttpClient? _currentClient;
|
IO.HttpClient? _currentClient;
|
||||||
HttpClientResponse? _currentResponse;
|
IO.HttpClientResponse? _currentResponse;
|
||||||
bool _isAborted = true;
|
bool _isAborted = true;
|
||||||
bool _isFirstData = true;
|
bool _isFirstData = true;
|
||||||
bool _isTtsStarted = false;
|
bool _isTtsStarted = false;
|
||||||
@@ -28,11 +32,11 @@ class ChatSseService {
|
|||||||
required bool isChinese,
|
required bool isChinese,
|
||||||
required Function(String, String, bool) onStreamResponse,
|
required Function(String, String, bool) onStreamResponse,
|
||||||
}) async {
|
}) async {
|
||||||
print("----------------------SSE Start");
|
Logger.i("----------------------SSE Start");
|
||||||
_isAborted = false;
|
_isAborted = false;
|
||||||
if (_cachedUserId == null) {
|
if (_cachedUserId == null) {
|
||||||
_cachedUserId = _generateRandomUserId(6);
|
_cachedUserId = _generateRandomUserId(6);
|
||||||
print('初始化用户ID: $_cachedUserId');
|
Logger.i('初始化用户ID: $_cachedUserId');
|
||||||
}
|
}
|
||||||
String responseText = '';
|
String responseText = '';
|
||||||
StringBuffer buffer = StringBuffer();
|
StringBuffer buffer = StringBuffer();
|
||||||
@@ -59,7 +63,8 @@ class ChatSseService {
|
|||||||
txt = txt.substring(0, imgStart) + txt.substring(imgEnd + 1);
|
txt = txt.substring(0, imgStart) + txt.substring(imgEnd + 1);
|
||||||
imgStart = txt.indexOf('![');
|
imgStart = txt.indexOf('![');
|
||||||
}
|
}
|
||||||
|
// 彻底移除 markdown 有序/无序列表序号(如 1.、2.、-、*、+)
|
||||||
|
txt = txt.replaceAll(RegExp(r'(^|\n)[ \t]*[0-9]+\.[ \t]*'), '\n');
|
||||||
txt = txt.replaceAll(RegExp(r'(^|\n)[ \t]*[-\*\+][ \t]+'), '\n');
|
txt = txt.replaceAll(RegExp(r'(^|\n)[ \t]*[-\*\+][ \t]+'), '\n');
|
||||||
// 分句符
|
// 分句符
|
||||||
RegExp enders = isChinese ? zhEnders : enEnders;
|
RegExp enders = isChinese ? zhEnders : enEnders;
|
||||||
@@ -76,14 +81,8 @@ class ChatSseService {
|
|||||||
}
|
}
|
||||||
// 只在达到完整句子时调用 TtsUtil.send
|
// 只在达到完整句子时调用 TtsUtil.send
|
||||||
for (final s in sentences) {
|
for (final s in sentences) {
|
||||||
String ttsStr=CommonUtil.cleanText(s, true)+"\n";
|
String ttsStr=CommonUtil.cleanText(s, true);
|
||||||
|
// Logger.i("发送数据到TTS: $ttsStr");
|
||||||
ttsStr = ttsStr.replaceAllMapped(
|
|
||||||
RegExp(r'(?<!\s)(\d+)(?!\s)'),
|
|
||||||
(m) => ' ${m.group(1)} ',
|
|
||||||
);
|
|
||||||
|
|
||||||
print("发送数据到TTS: $ttsStr");
|
|
||||||
TtsUtil.send(ttsStr);
|
TtsUtil.send(ttsStr);
|
||||||
}
|
}
|
||||||
// 缓存剩余不完整部分
|
// 缓存剩余不完整部分
|
||||||
@@ -93,7 +92,7 @@ class ChatSseService {
|
|||||||
buffer.write(remain);
|
buffer.write(remain);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_currentClient = HttpClient();
|
_currentClient = IO.HttpClient();
|
||||||
try {
|
try {
|
||||||
final chatUri = Uri.parse('http://143.64.185.20:18606/chat');
|
final chatUri = Uri.parse('http://143.64.185.20:18606/chat');
|
||||||
final request = await _currentClient!.postUrl(chatUri);
|
final request = await _currentClient!.postUrl(chatUri);
|
||||||
@@ -117,6 +116,7 @@ class ChatSseService {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (line.startsWith('data:')) {
|
if (line.startsWith('data:')) {
|
||||||
|
Logger.i('SSE line: $line');
|
||||||
final jsonStr = line.substring(5).trim();
|
final jsonStr = line.substring(5).trim();
|
||||||
if (jsonStr == '[DONE]' || jsonStr.contains('message_end')) {
|
if (jsonStr == '[DONE]' || jsonStr.contains('message_end')) {
|
||||||
TtsUtil.complete();
|
TtsUtil.complete();
|
||||||
@@ -125,6 +125,7 @@ class ChatSseService {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
final jsonData = json.decode(jsonStr);
|
final jsonData = json.decode(jsonStr);
|
||||||
|
Logger.i('SSE jsonData: $jsonData');
|
||||||
if (jsonData.containsKey('conversation_id') &&
|
if (jsonData.containsKey('conversation_id') &&
|
||||||
_cachedConversationId == null) {
|
_cachedConversationId == null) {
|
||||||
_cachedConversationId = jsonData['conversation_id'];
|
_cachedConversationId = jsonData['conversation_id'];
|
||||||
@@ -141,12 +142,12 @@ class ChatSseService {
|
|||||||
onStreamResponse(messageId, responseText, false);
|
onStreamResponse(messageId, responseText, false);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('解析 SSE 数据出错: $e, 原始数据: $jsonStr');
|
Logger.e('解析 SSE 数据出错: $e, 原始数据: $jsonStr');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
print('SSE 连接失败,状态码: ${_currentResponse!.statusCode}');
|
Logger.e('SSE 连接失败,状态码: ${_currentResponse!.statusCode}');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// todo
|
// todo
|
||||||
@@ -156,7 +157,7 @@ class ChatSseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> startTts(bool isChinese) async {
|
Future<void> startTts(bool isChinese) async {
|
||||||
print("----------------------TTS Start");
|
Logger.i("----------------------TTS Start");
|
||||||
if (!_isTtsStarted) {
|
if (!_isTtsStarted) {
|
||||||
if (await TtsUtil.start(isChinese) == true) {
|
if (await TtsUtil.start(isChinese) == true) {
|
||||||
_isTtsStarted = true;
|
_isTtsStarted = true;
|
||||||
@@ -193,7 +194,7 @@ class ChatSseService {
|
|||||||
void resetSession() {
|
void resetSession() {
|
||||||
_cachedUserId = null;
|
_cachedUserId = null;
|
||||||
_cachedConversationId = null;
|
_cachedConversationId = null;
|
||||||
print('SSE会话已重置');
|
Logger.i('SSE会话已重置');
|
||||||
}
|
}
|
||||||
|
|
||||||
String _generateRandomUserId(int length) {
|
String _generateRandomUserId(int length) {
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
|
|
||||||
class TextClassificationService {
|
|
||||||
Future<int> classifyText(String text) async {
|
|
||||||
try {
|
|
||||||
final uri = Uri.parse('http://143.64.185.20:18606/classify');
|
|
||||||
|
|
||||||
final response = await http.post(
|
|
||||||
uri,
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: json.encode({'text': text}),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
return json.decode(response.body)['category'];
|
|
||||||
} else {
|
|
||||||
print(
|
|
||||||
'Classification failed: ${response.statusCode}, ${response.body}');
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
print('Error during text classification: $e');
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import '../enums/vehicle_command_type.dart';
|
import 'package:ai_chat_core/ai_chat_core.dart';
|
||||||
import '../models/vehicle_status_info.dart';
|
|
||||||
|
|
||||||
/// 命令处理回调函数定义
|
/// 命令处理回调函数定义
|
||||||
typedef CommandCallback = Future<(bool, Map<String, dynamic>? params)> Function(
|
typedef CommandCallback = Future<(bool, Map<String, dynamic>? params)> Function(
|
||||||
VehicleCommandType type, Map<String, dynamic>? params);
|
VehicleCommandType type, Map<String, dynamic>? params);
|
||||||
|
|
||||||
/// 命令处理器类 - 负责处理来自AI的车辆控制命令
|
/// 命令处理器类 - 负责处理来自AI的车辆控制命令(使用回调的方式交给App去实现具体的车控逻辑)
|
||||||
class CommandService {
|
class CommandService {
|
||||||
/// 保存主应用注册的回调函数
|
/// 保存主应用注册的回调函数
|
||||||
static CommandCallback? onCommandReceived;
|
static CommandCallback? onCommandReceived;
|
||||||
@@ -24,10 +24,10 @@ class CommandService {
|
|||||||
try {
|
try {
|
||||||
return await onCommandReceived!(type, params);
|
return await onCommandReceived!(type, params);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('执行命令出错: $e');
|
Logger.e('执行命令出错: $e');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
print('警告: 命令处理回调未注册');
|
Logger.w('警告: 命令处理回调未注册');
|
||||||
}
|
}
|
||||||
return (false, null);
|
return (false, null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'package:ai_chat_core/ai_chat_core.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import '../models/vehicle_cmd.dart';
|
|
||||||
import '../models/vehicle_cmd_response.dart';
|
|
||||||
import 'vehicle_state_service.dart';
|
|
||||||
|
|
||||||
|
/// 车辆命令服务 - 负责与后端交互以获取和处理车辆控制命令
|
||||||
class VehicleCommandService {
|
class VehicleCommandService {
|
||||||
// final VehicleStateService vehicleStateService = VehicleStateService();
|
// final VehicleStateService vehicleStateService = VehicleStateService();
|
||||||
|
|
||||||
@@ -29,12 +28,12 @@ class VehicleCommandService {
|
|||||||
.toList();
|
.toList();
|
||||||
return VehicleCommandResponse(tips: tips, commands: vehicleCommandList);
|
return VehicleCommandResponse(tips: tips, commands: vehicleCommandList);
|
||||||
} else {
|
} else {
|
||||||
print(
|
Logger.e(
|
||||||
'Vehicle command query failed: ${response.statusCode}, ${response.body}');
|
'Vehicle command query failed: ${response.statusCode}, ${response.body}');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error during vehicle command processing: $e');
|
Logger.e('Error during vehicle command processing: $e');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,10 +50,10 @@ class VehicleCommandService {
|
|||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
return response.body;
|
return response.body;
|
||||||
} else {
|
} else {
|
||||||
print("请求控制回复失败: ${response.statusCode}");
|
Logger.e("请求控制回复失败: ${response.statusCode}");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('请求控制回复异常: $e');
|
Logger.e('请求控制回复异常: $e');
|
||||||
}
|
}
|
||||||
return reply;
|
return reply;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:basic_intl/intl.dart';
|
import 'package:ai_chat_core/ai_chat_core.dart';
|
||||||
|
import 'package:t_basic_intl/intl.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
class LocationService {
|
class LocationService {
|
||||||
@@ -13,7 +14,7 @@ class LocationService {
|
|||||||
return json.decode(response.body)['address'];
|
return json.decode(response.body)['address'];
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error during text classification: $e');
|
Logger.e('Error during text classification: $e');
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,73 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:ai_chat_assistant/ai_chat_assistant.dart';
|
||||||
import 'package:ai_chat_assistant/utils/common_util.dart';
|
import 'package:ai_chat_assistant/utils/common_util.dart';
|
||||||
import 'package:ai_chat_assistant/utils/tts_util.dart';
|
import 'package:ai_chat_assistant/utils/tts_util.dart';
|
||||||
import 'package:basic_intl/intl.dart';
|
import 'package:t_basic_intl/intl.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
import '../enums/vehicle_command_type.dart';
|
import '../services/chat_sse_service.dart' as Service;
|
||||||
import '../enums/message_service_state.dart';
|
|
||||||
import '../enums/message_status.dart';
|
|
||||||
import '../models/chat_message.dart';
|
|
||||||
import '../models/vehicle_cmd.dart';
|
|
||||||
import '../services/chat_sse_service.dart';
|
|
||||||
import '../services/classification_service.dart';
|
|
||||||
import '../services/control_recognition_service.dart';
|
import '../services/control_recognition_service.dart';
|
||||||
// import '../services/audio_recorder_service.dart';
|
// import '../services/audio_recorder_service.dart';
|
||||||
// import '../services/voice_recognition_service.dart';
|
// import '../services/voice_recognition_service.dart';
|
||||||
import 'command_service.dart';
|
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
|
|
||||||
|
import 'platform_tts_service.dart';
|
||||||
|
|
||||||
|
const aliSdkChannelName = 'com.example.ai_chat_assistant/ali_sdk';
|
||||||
|
|
||||||
|
// 用单例的模式创建
|
||||||
class MessageService extends ChangeNotifier {
|
class MessageService extends ChangeNotifier {
|
||||||
static const MethodChannel _asrChannel = MethodChannel('com.example.ai_chat_assistant/asr');
|
static const MethodChannel _asrChannel = MethodChannel('com.example.ai_chat_assistant/ali_sdk');
|
||||||
|
|
||||||
static final MessageService _instance = MessageService._internal();
|
static MessageService? _instance;
|
||||||
|
|
||||||
factory MessageService() => _instance;
|
static MessageService get instance {
|
||||||
|
return _instance ??= MessageService._internal();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提供工厂构造函数供 Provider 使用
|
||||||
|
factory MessageService() => instance;
|
||||||
|
|
||||||
Completer<String>? _asrCompleter;
|
Completer<String>? _asrCompleter;
|
||||||
|
|
||||||
MessageService._internal() {
|
MessageService._internal() {
|
||||||
_asrChannel.setMethodCallHandler((call) async {
|
// 注册MethodChannel的handler
|
||||||
switch (call.method) {
|
_asrChannel.setMethodCallHandler(_handleMethodCall);
|
||||||
case "onAsrResult":
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
// 取消注册MethodChannel的handler,避免内存泄漏和意外回调
|
||||||
|
_asrChannel.setMethodCallHandler(null);
|
||||||
|
_asrCompleter?.completeError('Disposed');
|
||||||
|
_asrCompleter = null;
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提供真正的销毁方法(可选,用于应用退出时)
|
||||||
|
void destroyInstance() {
|
||||||
|
_asrChannel.setMethodCallHandler(null);
|
||||||
|
_asrCompleter?.completeError('Destroyed');
|
||||||
|
_asrCompleter = null;
|
||||||
|
super.dispose();
|
||||||
|
_instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> _handleMethodCall(MethodCall call) async {
|
||||||
|
switch (call.method) {
|
||||||
|
case "onAsrResult":
|
||||||
|
{
|
||||||
|
debugPrint("ASR 结果: ${call.arguments}");
|
||||||
replaceMessage(
|
replaceMessage(
|
||||||
id: _latestUserMessageId!,
|
id: _latestUserMessageId!, text: call.arguments, status: MessageStatus.normal);
|
||||||
text: call.arguments,
|
|
||||||
status: MessageStatus.normal);
|
|
||||||
break;
|
break;
|
||||||
case "onAsrStop":
|
}
|
||||||
|
case "onAsrStop":
|
||||||
|
{
|
||||||
|
debugPrint("ASR 停止: ${call.arguments}");
|
||||||
int index = findMessageIndexById(_latestUserMessageId!);
|
int index = findMessageIndexById(_latestUserMessageId!);
|
||||||
if (index == -1) {
|
if (index == -1) {
|
||||||
return;
|
return;
|
||||||
@@ -52,16 +81,17 @@ class MessageService extends ChangeNotifier {
|
|||||||
_asrCompleter!.complete(messages.last.text);
|
_asrCompleter!.complete(messages.last.text);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final ChatSseService _chatSseService = ChatSseService();
|
final ChatSseService _chatSseService = ChatSseService(PlatformTtsService(aliSdkChannelName));
|
||||||
|
// final Service.ChatSseService _chatSseService = Service.ChatSseService();
|
||||||
|
|
||||||
// final LocalTtsService _ttsService = LocalTtsService();
|
// final LocalTtsService _ttsService = LocalTtsService();
|
||||||
// final AudioRecorderService _audioService = AudioRecorderService();
|
// final AudioRecorderService _audioService = AudioRecorderService();
|
||||||
// final VoiceRecognitionService _recognitionService = VoiceRecognitionService();
|
// final VoiceRecognitionService _recognitionService = VoiceRecognitionService();
|
||||||
final TextClassificationService _classificationService =
|
final TextClassificationService _classificationService = TextClassificationService();
|
||||||
TextClassificationService();
|
|
||||||
final VehicleCommandService _vehicleCommandService = VehicleCommandService();
|
final VehicleCommandService _vehicleCommandService = VehicleCommandService();
|
||||||
|
|
||||||
final List<ChatMessage> _messages = [];
|
final List<ChatMessage> _messages = [];
|
||||||
@@ -106,12 +136,11 @@ class MessageService extends ChangeNotifier {
|
|||||||
abortReply();
|
abortReply();
|
||||||
_latestUserMessageId = null;
|
_latestUserMessageId = null;
|
||||||
_latestAssistantMessageId = null;
|
_latestAssistantMessageId = null;
|
||||||
_isReplyAborted = false;
|
|
||||||
changeState(MessageServiceState.recording);
|
changeState(MessageServiceState.recording);
|
||||||
_latestUserMessageId = addMessage("", true, MessageStatus.listening);
|
_latestUserMessageId = addMessage("", true, MessageStatus.listening);
|
||||||
_asrChannel.invokeMethod("startAsr");
|
_asrChannel.invokeMethod("startAsr");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('录音开始出错: $e');
|
Logger.e('录音开始出错: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,11 +152,7 @@ class MessageService extends ChangeNotifier {
|
|||||||
String addMessage(String text, bool isUser, MessageStatus status) {
|
String addMessage(String text, bool isUser, MessageStatus status) {
|
||||||
String uuid = Uuid().v1();
|
String uuid = Uuid().v1();
|
||||||
_messages.add(ChatMessage(
|
_messages.add(ChatMessage(
|
||||||
id: uuid,
|
id: uuid, text: text, isUser: isUser, timestamp: DateTime.now(), status: status));
|
||||||
text: text,
|
|
||||||
isUser: isUser,
|
|
||||||
timestamp: DateTime.now(),
|
|
||||||
status: status));
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return uuid;
|
return uuid;
|
||||||
}
|
}
|
||||||
@@ -138,6 +163,7 @@ class MessageService extends ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
_isReplyAborted = false;
|
||||||
changeState(MessageServiceState.recognizing);
|
changeState(MessageServiceState.recognizing);
|
||||||
_asrChannel.invokeMethod("stopAsr");
|
_asrChannel.invokeMethod("stopAsr");
|
||||||
_asrCompleter = Completer<String>();
|
_asrCompleter = Completer<String>();
|
||||||
@@ -164,7 +190,7 @@ class MessageService extends ChangeNotifier {
|
|||||||
changeState(MessageServiceState.replying);
|
changeState(MessageServiceState.replying);
|
||||||
await reply(recognizedText);
|
await reply(recognizedText);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print(e);
|
Logger.e(e.toString());
|
||||||
} finally {
|
} finally {
|
||||||
changeState(MessageServiceState.idle);
|
changeState(MessageServiceState.idle);
|
||||||
_asrCompleter = null;
|
_asrCompleter = null;
|
||||||
@@ -226,8 +252,7 @@ class MessageService extends ChangeNotifier {
|
|||||||
if (_isReplyAborted) {
|
if (_isReplyAborted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final vehicleCommandResponse =
|
final vehicleCommandResponse = await _vehicleCommandService.getCommandFromText(text);
|
||||||
await _vehicleCommandService.getCommandFromText(text);
|
|
||||||
if (vehicleCommandResponse == null) {
|
if (vehicleCommandResponse == null) {
|
||||||
if (_isReplyAborted) {
|
if (_isReplyAborted) {
|
||||||
return;
|
return;
|
||||||
@@ -235,10 +260,7 @@ class MessageService extends ChangeNotifier {
|
|||||||
String msg = isChinese
|
String msg = isChinese
|
||||||
? "无法识别车辆控制命令,请重试"
|
? "无法识别车辆控制命令,请重试"
|
||||||
: "Cannot recognize the vehicle control command, please try again";
|
: "Cannot recognize the vehicle control command, please try again";
|
||||||
replaceMessage(
|
replaceMessage(id: _latestAssistantMessageId!, text: msg, status: MessageStatus.normal);
|
||||||
id: _latestAssistantMessageId!,
|
|
||||||
text: msg,
|
|
||||||
status: MessageStatus.normal);
|
|
||||||
} else {
|
} else {
|
||||||
if (_isReplyAborted) {
|
if (_isReplyAborted) {
|
||||||
return;
|
return;
|
||||||
@@ -259,8 +281,7 @@ class MessageService extends ChangeNotifier {
|
|||||||
if (_isReplyAborted) {
|
if (_isReplyAborted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (command.type == VehicleCommandType.unknown ||
|
if (command.type == VehicleCommandType.unknown || command.error.isNotEmpty) {
|
||||||
command.error.isNotEmpty) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (command.type == VehicleCommandType.openAC) {
|
if (command.type == VehicleCommandType.openAC) {
|
||||||
@@ -268,13 +289,13 @@ class MessageService extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
bool isSuccess;
|
bool isSuccess;
|
||||||
if (containOpenAC && command.type == VehicleCommandType.changeACTemp) {
|
if (containOpenAC && command.type == VehicleCommandType.changeACTemp) {
|
||||||
isSuccess = await Future.delayed(const Duration(milliseconds: 2000),
|
isSuccess = await Future.delayed(
|
||||||
() => processCommand(command, isChinese));
|
const Duration(milliseconds: 2000), () => processCommand(command, isChinese));
|
||||||
} else {
|
} else {
|
||||||
isSuccess = await processCommand(command, isChinese);
|
isSuccess = await processCommand(command, isChinese);
|
||||||
}
|
}
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
successCommandList.add(command.type.name);
|
successCommandList.add(isChinese ? command.type.chinese : command.type.english);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
replaceMessage(
|
replaceMessage(
|
||||||
@@ -285,8 +306,7 @@ class MessageService extends ChangeNotifier {
|
|||||||
if (_isReplyAborted || successCommandList.isEmpty) {
|
if (_isReplyAborted || successCommandList.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String controlResponse =
|
String controlResponse = await _vehicleCommandService.getControlResponse(successCommandList);
|
||||||
await _vehicleCommandService.getControlResponse(successCommandList);
|
|
||||||
if (_isReplyAborted || controlResponse.isEmpty) {
|
if (_isReplyAborted || controlResponse.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -297,20 +317,22 @@ class MessageService extends ChangeNotifier {
|
|||||||
Future<bool> processCommand(VehicleCommand command, bool isChinese) async {
|
Future<bool> processCommand(VehicleCommand command, bool isChinese) async {
|
||||||
String msg = "";
|
String msg = "";
|
||||||
MessageStatus status = MessageStatus.normal;
|
MessageStatus status = MessageStatus.normal;
|
||||||
final (isSuccess, result) = await CommandService.executeCommand(
|
var commandObjc = VehicleCommand(type: command.type, params: command.params);
|
||||||
command.type,
|
var client = AIChatAssistantManager.instance.commandClient;
|
||||||
params: command.params);
|
if (client == null) {
|
||||||
|
msg = isChinese ? "车辆控制服务未初始化" : "Vehicle control service is not initialized";
|
||||||
|
status = MessageStatus.failure;
|
||||||
|
addMessage(msg, false, status);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final (isSuccess, result) = await client.executeCommand(commandObjc);
|
||||||
if (command.type == VehicleCommandType.locateCar) {
|
if (command.type == VehicleCommandType.locateCar) {
|
||||||
msg = isChinese
|
msg = isChinese ? "很抱歉,定位车辆位置失败" : "Sorry, locate the vehicle unsuccessfully";
|
||||||
? "很抱歉,定位车辆位置失败"
|
|
||||||
: "Sorry, locate the vehicle unsuccessfully";
|
|
||||||
status = MessageStatus.failure;
|
status = MessageStatus.failure;
|
||||||
if (isSuccess && result != null && result.isNotEmpty) {
|
if (isSuccess && result != null && result.isNotEmpty) {
|
||||||
String address = result['address'];
|
String address = result['address'];
|
||||||
if (address.isNotEmpty) {
|
if (address.isNotEmpty) {
|
||||||
msg = isChinese
|
msg = isChinese ? "已为您找到车辆位置:\"$address\"" : "The vehicle location is \"$address\"";
|
||||||
? "已为您找到车辆位置:\"$address\""
|
|
||||||
: "The vehicle location is \"$address\"";
|
|
||||||
status = MessageStatus.success;
|
status = MessageStatus.success;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -386,8 +408,7 @@ class MessageService extends ChangeNotifier {
|
|||||||
status: MessageStatus.completed,
|
status: MessageStatus.completed,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
replaceMessage(
|
replaceMessage(id: messageId, text: responseText, status: MessageStatus.thinking);
|
||||||
id: messageId, text: responseText, status: MessageStatus.thinking);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
abortReply();
|
abortReply();
|
||||||
@@ -401,8 +422,7 @@ class MessageService extends ChangeNotifier {
|
|||||||
if (index == -1 || messages[index].status != MessageStatus.thinking) {
|
if (index == -1 || messages[index].status != MessageStatus.thinking) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
replaceMessage(
|
replaceMessage(id: _latestAssistantMessageId!, status: MessageStatus.aborted);
|
||||||
id: _latestAssistantMessageId!, status: MessageStatus.aborted);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeMessageById(String id) {
|
void removeMessageById(String id) {
|
||||||
|
|||||||
36
lib/services/platform_tts_service.dart
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import 'package:ai_chat_core/ai_chat_core.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
/// TtsService 的平台特定实现,通过 MethodChannel 调用原生代码
|
||||||
|
class PlatformTtsService implements TtsService {
|
||||||
|
@override
|
||||||
|
final String channelName;
|
||||||
|
|
||||||
|
final MethodChannel _channel;
|
||||||
|
|
||||||
|
PlatformTtsService(this.channelName): _channel = MethodChannel(channelName);
|
||||||
|
|
||||||
|
Future<T?> _execute<T>(String method, [Map<Object, Object>? arguments]) {
|
||||||
|
return _channel.invokeMethod(method, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> start(bool isChinese) async {
|
||||||
|
return await _execute('startTts', {'isChinese': isChinese}) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> send(String text) async {
|
||||||
|
await _execute('sendTts', {'text': text});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> complete() async {
|
||||||
|
await _execute('completeTts');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> stop() async {
|
||||||
|
await _execute('stopTts');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
import 'package:ai_chat_assistant/utils/common_util.dart';
|
import 'package:ai_chat_assistant/utils/common_util.dart';
|
||||||
|
import 'package:ai_chat_core/ai_chat_core.dart';
|
||||||
|
|
||||||
// ...existing code...
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_tts/flutter_tts.dart';
|
import 'package:flutter_tts/flutter_tts.dart';
|
||||||
|
|
||||||
|
@Deprecated('Use TtsService interface and its implementations instead')
|
||||||
class LocalTtsService {
|
class LocalTtsService {
|
||||||
final FlutterTts _flutterTts = FlutterTts();
|
final FlutterTts _flutterTts = FlutterTts();
|
||||||
bool _isPlaying = false;
|
bool _isPlaying = false;
|
||||||
@@ -45,27 +46,27 @@ class LocalTtsService {
|
|||||||
|
|
||||||
// 设置TTS事件回调
|
// 设置TTS事件回调
|
||||||
_flutterTts.setStartHandler(() {
|
_flutterTts.setStartHandler(() {
|
||||||
print('TTS开始播放');
|
Logger.i('TTS开始播放');
|
||||||
_isPlaying = true;
|
_isPlaying = true;
|
||||||
_onStartHandler?.call();
|
_onStartHandler?.call();
|
||||||
});
|
});
|
||||||
|
|
||||||
_flutterTts.setCompletionHandler(() {
|
_flutterTts.setCompletionHandler(() {
|
||||||
print('TTS播放完成');
|
Logger.i('TTS播放完成');
|
||||||
_isPlaying = false;
|
_isPlaying = false;
|
||||||
_handleTtsPlaybackComplete();
|
_handleTtsPlaybackComplete();
|
||||||
});
|
});
|
||||||
|
|
||||||
_flutterTts.setErrorHandler((msg) {
|
_flutterTts.setErrorHandler((msg) {
|
||||||
print('TTS错误: $msg');
|
Logger.e('TTS错误: $msg');
|
||||||
_isPlaying = false;
|
_isPlaying = false;
|
||||||
_onErrorHandler?.call(msg);
|
_onErrorHandler?.call(msg);
|
||||||
_handleTtsPlaybackComplete(); // 错误时也要处理下一个任务
|
_handleTtsPlaybackComplete(); // 错误时也要处理下一个任务
|
||||||
});
|
});
|
||||||
|
|
||||||
print('TTS引擎初始化完成');
|
Logger.i('TTS引擎初始化完成');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('TTS引擎初始化失败: $e');
|
Logger.e('TTS引擎初始化失败: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,17 +78,17 @@ class LocalTtsService {
|
|||||||
try {
|
try {
|
||||||
// 获取可用的TTS引擎
|
// 获取可用的TTS引擎
|
||||||
dynamic engines = await _flutterTts.getEngines;
|
dynamic engines = await _flutterTts.getEngines;
|
||||||
print('可用的TTS引擎: $engines');
|
Logger.i('可用的TTS引擎: $engines');
|
||||||
|
|
||||||
// 获取可用的语言
|
// 获取可用的语言
|
||||||
dynamic languages = await _flutterTts.getLanguages;
|
dynamic languages = await _flutterTts.getLanguages;
|
||||||
print('支持的语言: $languages');
|
Logger.i('支持的语言: $languages');
|
||||||
|
|
||||||
// 获取当前默认引擎
|
// 获取当前默认引擎
|
||||||
dynamic defaultEngine = await _flutterTts.getDefaultEngine;
|
dynamic defaultEngine = await _flutterTts.getDefaultEngine;
|
||||||
print('当前默认引擎: $defaultEngine');
|
Logger.i('当前默认引擎: $defaultEngine');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('获取TTS引擎信息失败: $e');
|
Logger.e('获取TTS引擎信息失败: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,19 +100,19 @@ class LocalTtsService {
|
|||||||
_isProcessingTasks = false;
|
_isProcessingTasks = false;
|
||||||
_streamCompleted = false;
|
_streamCompleted = false;
|
||||||
_isPlaying = false; // 重置播放状态
|
_isPlaying = false; // 重置播放状态
|
||||||
print('所有TTS队列和缓存已清空');
|
Logger.i('所有TTS队列和缓存已清空');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 推送文本到TTS队列进行流式处理(保证顺序)
|
/// 推送文本到TTS队列进行流式处理(保证顺序)
|
||||||
void pushTextForStreamTTS(String text) {
|
void pushTextForStreamTTS(String text) {
|
||||||
if (text.trim().isEmpty) {
|
if (text.trim().isEmpty) {
|
||||||
print('接收到空字符串,跳过推送到TTS队列');
|
Logger.i('接收到空字符串,跳过推送到TTS队列');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String cleanedText = CommonUtil.cleanText(text, true);
|
String cleanedText = CommonUtil.cleanText(text, true);
|
||||||
if (cleanedText.isEmpty) {
|
if (cleanedText.isEmpty) {
|
||||||
print('清理后的文本为空,跳过推送到TTS队列');
|
Logger.i('清理后的文本为空,跳过推送到TTS队列');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +125,7 @@ class LocalTtsService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
_orderedTasks.add(task);
|
_orderedTasks.add(task);
|
||||||
print('添加TTS任务 (索引: $taskIndex): $cleanedText');
|
Logger.i('添加TTS任务 (索引: $taskIndex): $cleanedText');
|
||||||
|
|
||||||
// 尝试处理队列中的下一个任务
|
// 尝试处理队列中的下一个任务
|
||||||
_processNextReadyTask();
|
_processNextReadyTask();
|
||||||
@@ -134,7 +135,7 @@ class LocalTtsService {
|
|||||||
void _processNextReadyTask() {
|
void _processNextReadyTask() {
|
||||||
// 如果正在播放,则不处理下一个任务
|
// 如果正在播放,则不处理下一个任务
|
||||||
if (_isPlaying) {
|
if (_isPlaying) {
|
||||||
print('当前正在播放音频,等待播放完成');
|
Logger.i('当前正在播放音频,等待播放完成');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +143,7 @@ class LocalTtsService {
|
|||||||
while (_nextTaskIndex < _orderedTasks.length) {
|
while (_nextTaskIndex < _orderedTasks.length) {
|
||||||
TtsTask task = _orderedTasks[_nextTaskIndex];
|
TtsTask task = _orderedTasks[_nextTaskIndex];
|
||||||
if (task.status == TtsTaskStatus.ready) {
|
if (task.status == TtsTaskStatus.ready) {
|
||||||
print('按顺序播放TTS (索引: ${task.index}): ${task.text}');
|
Logger.i('按顺序播放TTS (索引: ${task.index}): ${task.text}');
|
||||||
_playTts(task.text);
|
_playTts(task.text);
|
||||||
return; // 等待当前音频播放完成
|
return; // 等待当前音频播放完成
|
||||||
} else {
|
} else {
|
||||||
@@ -154,7 +155,7 @@ class LocalTtsService {
|
|||||||
|
|
||||||
// 检查是否所有任务都已处理完
|
// 检查是否所有任务都已处理完
|
||||||
if (_streamCompleted && _nextTaskIndex >= _orderedTasks.length) {
|
if (_streamCompleted && _nextTaskIndex >= _orderedTasks.length) {
|
||||||
print('=== 所有TTS任务播放完成 ===');
|
Logger.i('=== 所有TTS任务播放完成 ===');
|
||||||
_isProcessingTasks = false;
|
_isProcessingTasks = false;
|
||||||
_onCompletionHandler?.call();
|
_onCompletionHandler?.call();
|
||||||
}
|
}
|
||||||
@@ -163,28 +164,28 @@ class LocalTtsService {
|
|||||||
/// 播放TTS
|
/// 播放TTS
|
||||||
Future<void> _playTts(String text) async {
|
Future<void> _playTts(String text) async {
|
||||||
if (_isPlaying) {
|
if (_isPlaying) {
|
||||||
print('已有TTS正在播放,跳过本次播放');
|
Logger.i('已有TTS正在播放,跳过本次播放');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
print('开始播放TTS: $text');
|
Logger.i('开始播放TTS: $text');
|
||||||
// 检测语言并设置
|
// 检测语言并设置
|
||||||
bool isChinese = CommonUtil.containChinese(text);
|
bool isChinese = CommonUtil.containChinese(text);
|
||||||
String targetLanguage = isChinese ? "zh-CN" : "en-US";
|
String targetLanguage = isChinese ? "zh-CN" : "en-US";
|
||||||
// 检查语言是否可用,如果不可用则使用默认语言
|
// 检查语言是否可用,如果不可用则使用默认语言
|
||||||
bool isLanguageOk = await isLanguageAvailable(targetLanguage);
|
bool isLanguageOk = await isLanguageAvailable(targetLanguage);
|
||||||
if (!isLanguageOk) {
|
if (!isLanguageOk) {
|
||||||
print('语言 $targetLanguage 不可用,使用默认语言');
|
Logger.i('语言 $targetLanguage 不可用,使用默认语言');
|
||||||
// 可以在这里设置备用语言或保持当前语言
|
// 可以在这里设置备用语言或保持当前语言
|
||||||
} else {
|
} else {
|
||||||
await _flutterTts.setLanguage(targetLanguage);
|
await _flutterTts.setLanguage(targetLanguage);
|
||||||
print('设置TTS语言为: $targetLanguage');
|
Logger.i('设置TTS语言为: $targetLanguage');
|
||||||
}
|
}
|
||||||
// 播放TTS
|
// 播放TTS
|
||||||
await _flutterTts.speak(text);
|
await _flutterTts.speak(text);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('播放TTS失败: $e');
|
Logger.i('播放TTS失败: $e');
|
||||||
// 播放失败,重置状态并继续下一个
|
// 播放失败,重置状态并继续下一个
|
||||||
_isPlaying = false;
|
_isPlaying = false;
|
||||||
_handleTtsPlaybackComplete();
|
_handleTtsPlaybackComplete();
|
||||||
@@ -194,7 +195,7 @@ class LocalTtsService {
|
|||||||
/// 处理TTS播放完成
|
/// 处理TTS播放完成
|
||||||
void _handleTtsPlaybackComplete() {
|
void _handleTtsPlaybackComplete() {
|
||||||
if (!_isPlaying) {
|
if (!_isPlaying) {
|
||||||
print('TTS已停止播放,处理下一个任务');
|
Logger.i('TTS已停止播放,处理下一个任务');
|
||||||
// 标记当前任务为已完成
|
// 标记当前任务为已完成
|
||||||
if (_nextTaskIndex < _orderedTasks.length &&
|
if (_nextTaskIndex < _orderedTasks.length &&
|
||||||
_orderedTasks[_nextTaskIndex].status == TtsTaskStatus.ready) {
|
_orderedTasks[_nextTaskIndex].status == TtsTaskStatus.ready) {
|
||||||
@@ -208,7 +209,7 @@ class LocalTtsService {
|
|||||||
|
|
||||||
/// 标记流完成
|
/// 标记流完成
|
||||||
void markSSEStreamCompleted() {
|
void markSSEStreamCompleted() {
|
||||||
print('=== 标记SSE流完成 ===');
|
Logger.i('=== 标记SSE流完成 ===');
|
||||||
_streamCompleted = true;
|
_streamCompleted = true;
|
||||||
|
|
||||||
// 尝试完成剩余任务
|
// 尝试完成剩余任务
|
||||||
@@ -217,7 +218,7 @@ class LocalTtsService {
|
|||||||
|
|
||||||
/// 停止播放
|
/// 停止播放
|
||||||
void stopSSEPlayback() {
|
void stopSSEPlayback() {
|
||||||
print("停止SSE播放");
|
Logger.i("停止SSE播放");
|
||||||
// 立即停止TTS播放,无论当前是否在播报
|
// 立即停止TTS播放,无论当前是否在播报
|
||||||
_flutterTts.stop();
|
_flutterTts.stop();
|
||||||
_isPlaying = false;
|
_isPlaying = false;
|
||||||
@@ -240,11 +241,11 @@ class LocalTtsService {
|
|||||||
// 保留原有的播放逻辑用于完整文本处理
|
// 保留原有的播放逻辑用于完整文本处理
|
||||||
Future<void> processCompleteText(String text) async {
|
Future<void> processCompleteText(String text) async {
|
||||||
if (text.trim().isEmpty) {
|
if (text.trim().isEmpty) {
|
||||||
print("文本为空,跳过TTS处理");
|
Logger.i("文本为空,跳过TTS处理");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
print("开始处理完整文本TTS,原始文本长度: ${text.length}字");
|
Logger.i("开始处理完整文本TTS,原始文本长度: ${text.length}字");
|
||||||
|
|
||||||
// 清理缓存和重置队列
|
// 清理缓存和重置队列
|
||||||
clearAll();
|
clearAll();
|
||||||
@@ -260,23 +261,23 @@ class LocalTtsService {
|
|||||||
Future<void> _processCompleteTextAsStream(String text) async {
|
Future<void> _processCompleteTextAsStream(String text) async {
|
||||||
// 清理markdown和异常字符
|
// 清理markdown和异常字符
|
||||||
String cleanedText = _cleanTextForTts(text);
|
String cleanedText = _cleanTextForTts(text);
|
||||||
print("清理后文本长度: ${cleanedText.length}字");
|
Logger.i("清理后文本长度: ${cleanedText.length}字");
|
||||||
|
|
||||||
if (cleanedText.trim().isEmpty) {
|
if (cleanedText.trim().isEmpty) {
|
||||||
print("清理后文本为空,跳过TTS");
|
Logger.i("清理后文本为空,跳过TTS");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分割成句子并逐个处理
|
// 分割成句子并逐个处理
|
||||||
List<String> sentences = _splitTextIntoSentences(cleanedText);
|
List<String> sentences = _splitTextIntoSentences(cleanedText);
|
||||||
print("=== 文本分段完成,共 ${sentences.length} 个句子 ===");
|
Logger.i("=== 文本分段完成,共 ${sentences.length} 个句子 ===");
|
||||||
|
|
||||||
// 推送每个句子到流式TTS处理
|
// 推送每个句子到流式TTS处理
|
||||||
for (int i = 0; i < sentences.length; i++) {
|
for (int i = 0; i < sentences.length; i++) {
|
||||||
final sentence = sentences[i].trim();
|
final sentence = sentences[i].trim();
|
||||||
if (sentence.isEmpty) continue;
|
if (sentence.isEmpty) continue;
|
||||||
|
|
||||||
print("处理句子 ${i + 1}/${sentences.length}: $sentence");
|
Logger.i("处理句子 ${i + 1}/${sentences.length}: $sentence");
|
||||||
pushTextForStreamTTS(sentence);
|
pushTextForStreamTTS(sentence);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,19 +288,19 @@ class LocalTtsService {
|
|||||||
// 停止当前播放的辅助方法
|
// 停止当前播放的辅助方法
|
||||||
Future<void> _stopCurrentPlayback() async {
|
Future<void> _stopCurrentPlayback() async {
|
||||||
try {
|
try {
|
||||||
print("停止当前播放");
|
Logger.i("停止当前播放");
|
||||||
|
|
||||||
if (_isPlaying) {
|
if (_isPlaying) {
|
||||||
await _flutterTts.stop();
|
await _flutterTts.stop();
|
||||||
_isPlaying = false;
|
_isPlaying = false;
|
||||||
print("TTS.stop() 调用完成");
|
Logger.i("TTS.stop() 调用完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 短暂延迟确保播放器状态稳定
|
// 短暂延迟确保播放器状态稳定
|
||||||
await Future.delayed(Duration(milliseconds: 300));
|
await Future.delayed(Duration(milliseconds: 300));
|
||||||
print("播放器状态稳定延迟完成");
|
Logger.i("播放器状态稳定延迟完成");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('停止当前播放错误: $e');
|
Logger.i('停止当前播放错误: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -456,7 +457,7 @@ class LocalTtsService {
|
|||||||
await processCompleteText(text);
|
await processCompleteText(text);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('TTS 播放错误: $e');
|
Logger.i('TTS 播放错误: $e');
|
||||||
_onErrorHandler?.call('TTS 播放错误: $e');
|
_onErrorHandler?.call('TTS 播放错误: $e');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -468,7 +469,7 @@ class LocalTtsService {
|
|||||||
await _stopCurrentPlayback();
|
await _stopCurrentPlayback();
|
||||||
clearAll();
|
clearAll();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('停止音频播放错误: $e');
|
Logger.i('停止音频播放错误: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,7 +479,7 @@ class LocalTtsService {
|
|||||||
await _flutterTts.pause();
|
await _flutterTts.pause();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('暂停音频播放错误: $e');
|
Logger.i('暂停音频播放错误: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,9 +487,9 @@ class LocalTtsService {
|
|||||||
try {
|
try {
|
||||||
// flutter_tts没有resume方法,需要重新播放当前文本
|
// flutter_tts没有resume方法,需要重新播放当前文本
|
||||||
// 这里可以根据需要实现
|
// 这里可以根据需要实现
|
||||||
print('TTS不支持恢复,需要重新播放');
|
Logger.i('TTS不支持恢复,需要重新播放');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('恢复音频播放错误: $e');
|
Logger.i('恢复音频播放错误: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -536,7 +537,7 @@ class LocalTtsService {
|
|||||||
dynamic engines = await _flutterTts.getEngines;
|
dynamic engines = await _flutterTts.getEngines;
|
||||||
return engines ?? [];
|
return engines ?? [];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('获取TTS引擎列表失败: $e');
|
Logger.i('获取TTS引擎列表失败: $e');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -545,10 +546,10 @@ class LocalTtsService {
|
|||||||
Future<bool> setEngine(String engineName) async {
|
Future<bool> setEngine(String engineName) async {
|
||||||
try {
|
try {
|
||||||
int result = await _flutterTts.setEngine(engineName);
|
int result = await _flutterTts.setEngine(engineName);
|
||||||
print('设置TTS引擎: $engineName, 结果: $result');
|
Logger.i('设置TTS引擎: $engineName, 结果: $result');
|
||||||
return result == 1; // 1表示成功
|
return result == 1; // 1表示成功
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('设置TTS引擎失败: $e');
|
Logger.i('设置TTS引擎失败: $e');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -559,7 +560,7 @@ class LocalTtsService {
|
|||||||
dynamic engine = await _flutterTts.getDefaultEngine;
|
dynamic engine = await _flutterTts.getDefaultEngine;
|
||||||
return engine?.toString();
|
return engine?.toString();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('获取当前TTS引擎失败: $e');
|
Logger.i('获取当前TTS引擎失败: $e');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -570,7 +571,7 @@ class LocalTtsService {
|
|||||||
dynamic languages = await _flutterTts.getLanguages;
|
dynamic languages = await _flutterTts.getLanguages;
|
||||||
return languages ?? [];
|
return languages ?? [];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('获取支持的语言列表失败: $e');
|
Logger.i('获取支持的语言列表失败: $e');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -581,7 +582,7 @@ class LocalTtsService {
|
|||||||
dynamic result = await _flutterTts.isLanguageAvailable(language);
|
dynamic result = await _flutterTts.isLanguageAvailable(language);
|
||||||
return result == true;
|
return result == true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('检查语言可用性失败: $e');
|
Logger.i('检查语言可用性失败: $e');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
|
import 'package:ai_chat_core/ai_chat_core.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:http_parser/http_parser.dart';
|
import 'package:http_parser/http_parser.dart';
|
||||||
|
|
||||||
|
@Deprecated('VoiceRecognitionService is deprecated, please use the new implementation if available.')
|
||||||
class VoiceRecognitionService {
|
class VoiceRecognitionService {
|
||||||
Future<String?> recognizeSpeech(List<int> audioBytes,
|
Future<String?> recognizeSpeech(List<int> audioBytes,
|
||||||
{String lang = 'cn'}) async {
|
{String lang = 'cn'}) async {
|
||||||
try {
|
try {
|
||||||
print('准备发送OPUS音频数据,大小: ${audioBytes.length} 字节');
|
Logger.i('准备发送OPUS音频数据,大小: ${audioBytes.length} 字节');
|
||||||
final uri = Uri.parse('http://143.64.185.20:18606/voice');
|
final uri = Uri.parse('http://143.64.185.20:18606/voice');
|
||||||
print('发送到: $uri');
|
Logger.i('发送到: $uri');
|
||||||
|
|
||||||
final request = http.MultipartRequest('POST', uri);
|
final request = http.MultipartRequest('POST', uri);
|
||||||
request.files.add(
|
request.files.add(
|
||||||
@@ -21,26 +23,26 @@ class VoiceRecognitionService {
|
|||||||
);
|
);
|
||||||
request.fields['lang'] = lang; // 根据参数设置语言
|
request.fields['lang'] = lang; // 根据参数设置语言
|
||||||
|
|
||||||
print('发送请求...');
|
Logger.i('发送请求...');
|
||||||
final streamResponse = await request.send();
|
final streamResponse = await request.send();
|
||||||
print('收到响应,状态码: ${streamResponse.statusCode}');
|
Logger.i('收到响应,状态码: ${streamResponse.statusCode}');
|
||||||
|
|
||||||
final response = await http.Response.fromStream(streamResponse);
|
final response = await http.Response.fromStream(streamResponse);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
print('响应内容: ${response.body}');
|
Logger.i('响应内容: ${response.body}');
|
||||||
final text = _parseTextFromJson(response.body);
|
final text = _parseTextFromJson(response.body);
|
||||||
if (text != null) {
|
if (text != null) {
|
||||||
print('解析出文本: $text');
|
Logger.i('解析出文本: $text');
|
||||||
return text; // 返回识别的文本
|
return text; // 返回识别的文本
|
||||||
} else {
|
} else {
|
||||||
print('解析文本失败');
|
Logger.i('解析文本失败');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
print('请求失败,状态码: ${response.statusCode},响应: ${response.body}');
|
Logger.i('请求失败,状态码: ${response.statusCode},响应: ${response.body}');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('发送录音到服务器时出错: $e');
|
Logger.i('发送录音到服务器时出错: $e');
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -54,7 +56,7 @@ class VoiceRecognitionService {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('JSON解析错误: $e, 原始数据: $body');
|
Logger.i('JSON解析错误: $e, 原始数据: $body');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:basic_intl/intl.dart';
|
import 'package:t_basic_intl/intl.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class AppTheme {
|
class AppTheme {
|
||||||
|
|||||||
31
lib/utils/assets_util.dart
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// assets_util.dart
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AssetsUtil
|
||||||
|
* @description: 资源工具类
|
||||||
|
* @author guangfei.zhao
|
||||||
|
*/
|
||||||
|
class AssetsUtil {
|
||||||
|
|
||||||
|
static const String packageName = 'ai_chat_assistant';
|
||||||
|
|
||||||
|
static AssetImage getImage(String imageName) {
|
||||||
|
return AssetImage(
|
||||||
|
'assets/images/$imageName',
|
||||||
|
package: packageName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Widget getImageWidget(String imageName, {BoxFit? fit, double? width, double? height}) {
|
||||||
|
return Image.asset(
|
||||||
|
'assets/images/$imageName',
|
||||||
|
package: packageName,
|
||||||
|
fit: fit,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||