Compare commits

...

26 Commits

Author SHA1 Message Date
guangfei.zhao
07980fea87 新增 logger 打印方法 2025-09-30 14:48:12 +08:00
guangfei.zhao
1729d0f266 fix: 修复LogInterceptor 导致message不能流式吐字的问题 2025-09-30 14:38:03 +08:00
guangfei.zhao
c29a3f665a fix: 修复asr 启动就初始化的问题
asrInstance.initialize 这个方法的作用是初始化阿里云 NUI SDK 的 ASR 实例。根据阿里云 SDK 的设计,这个初始化过程不仅仅是加载配置,它还会立即建立与云端的长连接(WebSocket),并进入一个“待命”状态。SDK 会请求打开音频设备。

延迟初始化,只在第一次需要时才进行初始化
2025-09-30 11:43:40 +08:00
guangfei.zhao
a7ed838c92 feat: 新增ai_chat_core,将core 和 widget 分离(未完成)
迁移了 models enums, utils, http 封装,还有一些extensions;service 只迁移了 sse service
2025-09-29 18:33:46 +08:00
guangfei.zhao
ba73d6d780 唤醒流程更新,修复重复唤醒的问题 2025-09-29 14:11:33 +08:00
guangfei.zhao
5be50b4dbb feat: 添加唤醒功能 2025-09-28 18:42:56 +08:00
guangfei.zhao
c6765ab29c 加入 cn 和 en 简单的本地模型 2025-09-28 11:20:37 +08:00
guangfei.zhao
f3ff75c437 新增plugin flutter_vosk_wakeword 2025-09-27 21:01:20 +08:00
guangfei.zhao
f5699fd144 feat: 新增语音唤醒
唤醒的逻辑跟长按的逻辑不一样,不能像之前那样长按图标来录音,结束长按来结束录音
2025-09-26 16:23:48 +08:00
guangfei.zhao
4a41c25502 更新为flutter_markdown_plus 2025-09-25 18:08:09 +08:00
guangfei.zhao
9662ff6a56 聊天气泡集合图片 2025-09-25 17:33:34 +08:00
guangfei.zhao
2bdb15db1d 添加聊天气泡设计图 2025-09-25 16:51:30 +08:00
guangfei.zhao
50226b27a1 添加消息持久化和服务模块分离的内容 2025-09-25 14:23:57 +08:00
guangfei.zhao
5e8b9af17b 添加AI_Chat_Assistant PRD(未完成) 2025-09-24 15:42:51 +08:00
guangfei.zhao
d3580a6a0b basic_intl 修改为 t_basic_intl 2025-09-24 15:42:30 +08:00
guangfei.zhao
87cc15dd53 UPDATE README 2025-09-23 10:58:47 +08:00
guangfei.zhao
6523f8e512 feat: 改进聊天弹窗和遮罩层交互,优化状态管理和绘制逻辑 2025-09-23 09:53:49 +08:00
guangfei.zhao
b9ab384fde 使用Overlay来弹出聊天窗口 2025-09-22 17:51:21 +08:00
guangfei.zhao
049cb7b5c4 新增一个chat_popup 来显示聊天窗口 2025-09-22 17:15:44 +08:00
guangfei.zhao
e04110c6d5 feat:plugin编译为aar包引用 2025-09-22 11:12:45 +08:00
guangfei.zhao
0509096925 使用 assets_util 来管理 assets 2025-09-19 11:40:38 +08:00
guangfei.zhao
b84899ece3 feat: 修复了插件需要外部Provider的问题;加上了一个简易的车控状态返回 2025-09-18 15:53:39 +08:00
guangfei.zhao
922818f39f FloatingIcon 加入拖动的动画,以及自动吸附 2025-09-17 18:07:38 +08:00
guangfei.zhao
33aef48f84 同步 oneApp 的代码 2025-09-17 15:09:05 +08:00
guangfei.zhao
62924296b2 fix: 修复assets在example app中不能使用的问题 & 加入maven local的链接,修复flutter无法打包 aar的问题 2025-09-10 17:33:26 +08:00
guangfei.zhao
4176116bb3 feat: 添加README以及example项目,修复plugin的路径,新建basic_intl的本地依赖 2025-09-10 15:35:07 +08:00
206 changed files with 9886 additions and 801 deletions

3
.fvmrc Normal file
View File

@@ -0,0 +1,3 @@
{
"flutter": "3.27.4"
}

5
.gitignore vendored
View File

@@ -43,3 +43,8 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
.vscode/
# FVM Version Cache
.fvm/

644
CODE_ANALYSIS.md Normal file
View 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) ![alt](src)
// 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
View 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
View File

@@ -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,
samples, guidance on mobile development, and a full API reference.
```
lib/
├── 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
View 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
View File

@@ -12,3 +12,5 @@ GeneratedPluginRegistrant.java
key.properties
**/*.keystore
**/*.jks
/build/
/src/build/

112
android/build.gradle Normal file
View 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"))
}
}

File diff suppressed because one or more lines are too long

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
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

Binary file not shown.

View 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
View File

@@ -0,0 +1 @@
rootProject.name = 'ai_assistant_plugin'

View File

@@ -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")

View 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>

View File

@@ -1,32 +1,32 @@
package com.example.ai_chat_assistant;
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;
package com.example.ai_assistant_plugin;
import java.util.Map;
public class MainActivity extends FlutterActivity implements INativeNuiCallback {
private static final String TTS_CHANNEL = "com.example.ai_chat_assistant/tts";
private static final String ASR_CHANNEL = "com.example.ai_chat_assistant/asr";
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.MethodCall;
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 APP_KEY = "bXFFc1V65iYbW6EF";
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 englishVoice = "abby";
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 Handler asrHandler;
private AsrCallBack asrCallBack;
private Handler handler = new Handler(Looper.getMainLooper());
private final AudioPlayer ttsAudioTrack = new AudioPlayer(new AudioPlayerCallback() {
@Override
@@ -53,137 +63,94 @@ public class MainActivity extends FlutterActivity implements INativeNuiCallback
}
});
private MethodChannel asrMethodChannel;
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 boolean asrStopping = false;
private Handler asrHandler;
private String asrText = "";
@Override
public void configureFlutterEngine(FlutterEngine flutterEngine) {
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;
void initAsrInstance() {
if (!isAsrInitialized) {
Log.i(TAG, "Initializing ASR for the first time...");
asrCallBack = new AsrCallBack(channel);
int initResult = asrInstance.initialize(asrCallBack, genAsrInitParams(),
Constants.LogLevel.LOG_LEVEL_NONE, false);
Log.i(TAG, "ASR initialization result: " + initResult);
if (initResult != Constants.NuiResultCode.SUCCESS) {
Log.i(TAG, "ASR initialization failed with error code: " + initResult);
return;
}
});
isAsrInitialized = true;
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
public void onAttachedToEngine(FlutterPluginBinding flutterPluginBinding) {
channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), CHANNEL);
channel.setMethodCallHandler(this);
HandlerThread asrHandlerThread = new HandlerThread("process_thread");
asrHandlerThread.start();
asrHandler = new Handler(asrHandlerThread.getLooper());
// 不能在这里初始化 asr, 初始化之后会占用麦克风
// asrCallBack = new AsrCallBack(channel);
// asrInstance.initialize(asrCallBack, genAsrInitParams(),
// Constants.LogLevel.LOG_LEVEL_NONE, false);
}
@Override
protected void onStart() {
Log.i(TAG, "onStart");
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();
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
channel.setMethodCallHandler(null);
ttsAudioTrack.stop();
ttsAudioTrack.releaseAudioTrack();
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) {
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 onMethodCall(MethodCall call, @NonNull Result result) {
Map<String, Object> args = call.arguments();
switch (call.method) {
case "startTts":
assert args != null;
Object isChinese = args.get("isChinese");
if (isChinese == null || isChinese.toString().isBlank()) {
return;
}
}
@Override
public void onStreamInputTtsDataCallback(byte[] data) {
if (data.length > 0) {
ttsAudioTrack.setAudioData(data);
boolean isSuccess = startTts(Boolean.parseBoolean(isChinese.toString()));
result.success(isSuccess);
break;
case "sendTts":
assert args != null;
Object textArg = args.get("text");
if (textArg == null || textArg.toString().isBlank()) {
return;
}
}
}, 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;
sendTts(textArg.toString());
break;
case "completeTts":
completeTts();
break;
case "stopTts":
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() {
asrText = "";
asrHandler.post(() -> {
// 初始化
initAsrInstance();
// 启动
String setParamsString = genAsrParams();
Log.i(TAG, "nui set params " + setParamsString);
asrInstance.setParams(setParamsString);
@@ -213,105 +182,12 @@ public class MainActivity extends FlutterActivity implements INativeNuiCallback
private void stopAsr() {
asrHandler.post(() -> {
asrStopping = true;
long ret = asrInstance.stopDialog();
runOnUiThread(() -> asrMethodChannel.invokeMethod("onAsrStop", null));
handler.post(() -> channel.invokeMethod("onAsrStop", null));
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() {
String str = "";
try {
@@ -433,4 +309,51 @@ public class MainActivity extends FlutterActivity implements INativeNuiCallback
Log.i(TAG, "dialog params: " + 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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -1,4 +1,4 @@
package com.example.ai_chat_assistant;
package com.example.ai_assistant_plugin;
import android.media.AudioFormat;
import android.media.AudioManager;

View File

@@ -1,4 +1,4 @@
package com.example.ai_chat_assistant;
package com.example.ai_assistant_plugin;
public interface AudioPlayerCallback {
public void playStart();

View File

@@ -1,11 +1,10 @@
package com.example.ai_chat_assistant;
package com.example.ai_assistant_plugin;
import android.util.Log;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.example.ai_chat_assistant.token.AccessToken;
import com.example.ai_assistant_plugin.token.AccessToken;
import java.io.File;
import java.io.FileInputStream;

View File

@@ -1,4 +1,4 @@
package com.example.ai_chat_assistant.token;
package com.example.ai_assistant_plugin.token;
import android.util.Log;

View File

@@ -1,4 +1,4 @@
package com.example.ai_chat_assistant.token;
package com.example.ai_assistant_plugin.token;
import android.util.Log;

View File

@@ -1,4 +1,4 @@
package com.example.ai_chat_assistant.token;
package com.example.ai_assistant_plugin.token;
/**
* Say something

View File

@@ -1,4 +1,4 @@
package com.example.ai_chat_assistant.token;
package com.example.ai_assistant_plugin.token;
import android.util.Log;

View File

@@ -1,4 +1,4 @@
package com.example.ai_chat_assistant.token;
package com.example.ai_assistant_plugin.token;
import android.util.Base64;

BIN
assets/images/ai1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

BIN
assets/images/ai2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Binary file not shown.

3
devtools_options.yaml Normal file
View 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:

View 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误触策略短按抬起不触发录音。
![image.png](images/db1d92f4-107d-46c9-8154-fc58cb471acc.png)
![image.png](images/image.png)
#### **3.1.2 浮动聊天窗口ChatPopup**
- **用户场景**
用户与AI助手进行可视化的文本和语音交互。
- **功能描述**
提供弹出式聊天窗口,展示对话记录并提供输入方式,不承担复杂历史翻阅与批量操作
- **需求说明**
长按浮动按钮即可出现浮动聊天窗口,长按状态下展示聆听中,放开后则聆听结束,变为思考中状态。
思考中状态可以手动点击 **停止回答** 可以终止AI对话直到回答完成则不可点击停止回答。
![Screenshot_20250923_162311.jpg](images/Screenshot_20250923_162311.jpg)
![image.png](images/image%201.png)
#### **3.1.3 全屏聊天界面ChatFullscreen**
- **用户场景**
用户与AI助手进行可视化的文本和语音交互适用于长对话、多轮引用、查看历史与多类型气泡的交互。
- **功能描述**
提供全屏聊天界面,展示对话记录并提供输入方式。全屏的聊天页面功能更加丰富,多类型气泡、快捷指令区、功能按钮区、历史分页加载
- **需求说明**
- 全屏聊天模式下会保留之前浮动聊天窗口的聊天记录
- 不同的场景为不同的AI助手现有两种场景购车助手和用车助手(根据入口来判断)
- 顶部会有可配置的功能按钮区域(用户服务/购车服务)
- 功能按钮下方会有快捷对话的区域,可以点击换一换来更换快捷指令,支持“换一换”刷新策略(本地轮换或服务侧下发)
- 聊天页面底部会有功能栏,分别为**删除当前对话****按住说话****切换文本输入**
- 如果存在本地对话记录,点击 **显示历史对话记录** 会加载之前的5条对话记录同样可以通过上拉来加载历史对话记录
![image.png](images/image%202.png)
### 3.2 AI对话场景
#### **3.2.1 购车助手场景**
- **用户场景**
用户未绑定车辆的情况下爱车页面变为购车页面购车页面也会有AI助手未绑定车辆用户获取车型配置、优惠、推荐。
- **功能描述**
购车页面的AI助手没有聆听模式只能点击进入全屏的聊天页面首次的回复词也会有变化功能按钮区域和快捷回复区域都会跟用车助手不同
- **需求说明**
- 浮动按钮点击可进入全屏的AI助手页面顶部的功能按钮为热门车型推荐同样是icon+text的按钮点击之后会快捷输入车型的名称AI助手会回复车型的相关信息同时快捷对话的区域会按照对话来进行变化
- 购车助手同样可以通过文本的语义来进行推荐车型,以及解读购车的优惠信息;
- 如果在购车助手场景下,本身并没有绑定车辆,如果有车控的语音输入,则返回:您还未绑定车辆,无法使用车控指令;
- 如果是用车助手场景,包含购车助手的全部功能,同样可以回复相应车型来获取配置信息以及购车政策。
![image.png](images/image%203.png)
![image.png](images/image%204.png)
#### **3.2.2 用车助手场景**
- **用户场景**
用户在绑定车辆的情况下进入爱车页面使用AI助手则我用车助手场景
- **功能描述**
用车助手场景下的AI助手不同于购车场景可以按照文本的输入分为三种普通问答车控指令错误问题反馈
- **需求说明**
- 用车场景下的功能按钮为官方推荐,内容包括智能场景推荐、数据埋点和运营位,其中智能场景推荐包括售后维保提醒,智能场景建议;数据埋点包括能耗报告,行为分析,用户习惯操作;运营位则是推荐的车型,用户复购建议;
- 常见功能按钮 → 发送一条“功能集合”消息气泡(含多个按钮)。
- 点击 更多工具按钮跟常见功能按钮一样,会返回一条聊天记录,里面会包含多个按钮,点击按钮可以进行快捷回复;
- 快捷回复区域是可以变化的,会根据对话的最后一个回复进行变化,快捷回复区域会跟在最后一个对话的底部;例如刚进入会跟在 功能按钮的底部,但是新的聊天记录出现后,就会自动拼接到聊天气泡的底部(聊天气泡类型分为多种,后面会详细介绍);
![image.png](images/image%202.png)
#### **3.2.3 用户唤醒场景**
- **用户场景**
在开通语音唔醒功能之后,购车页面和爱车页面都可以使用语音唤醒,免手动触发,提高便利性
- **功能描述**
用户可以通过语音唤醒功能在不需要点击按钮的情况下激活AI助手提高使用便捷性和用户体验。唤醒词“你好众众”唤醒后直接进入聆听模式并高亮反馈。
- **需求说明**
- 设置页开启后常驻监听(低功耗策略)。
- 唤醒检测在本地执行,未唤醒音频不上传。
- 唤醒成功:音效 + 动画反馈。
- 低电量模式:可自动降敏或暂停。
- 权限缺失:首次引导开启麦克风权限。
### 3.3 聊天模块
#### **3.3.1 聊天气泡类型**
- **用户场景**
用户与AI助手对话过程中会展示不同类型的聊天气泡以适应各种交互需求。
- **功能描述**
系统支持多种聊天气泡类型,包括文本气泡、卡片气泡、按钮气泡、链接气泡和富媒体气泡等,以满足不同场景下的交互需求。
- **需求说明**
- 文本气泡展示普通文字对话内容完整支持Markdown格式
Markdown支持包括普通文本格式化、表格布局以及图片嵌入提供更丰富的内容展示方式
- 卡片气泡:用于展示结构化信息,如车辆信息、维保记录等;
- 按钮气泡:包含可交互按钮,用于快速回复或执行操作;
- 富媒体气泡:支持图片、视频、语音等多媒体内容展示;
富媒体气泡可能适用于显示特定的信息,或者是视频的教程,轮播的图片
- 链接气泡用于显示一个链接包含IconTitleDescription以及一个可以点击的链接
点击链接可以进入webview链接气泡主要是用于详细介绍某些活动或者查看详情
- 业务气泡:用于特定业务的定制化气泡,会根据对话的业务场景,显示不同的气泡。
业务气泡不同于其他的气泡,可以在用户询问金融方案后显示返回的计算结果,在结果后面还有一个按钮,可以进入金融计算器的页面;同时在用户说出我希望订购某款车型时,会出现车型的信息,并且有一个立即订购的按钮。业务气泡是根据返回的数据来判断具体的业务场景,混合显示的气泡类型。
![按钮气泡](images/button_bubble_image.png)
![快捷回复](images/quick_reply_image.png)
![业务气泡1](images/image%203.png)
![业务气泡2](images/bussine_image.png)
![聊天气泡](images/bubble_image_list.png)
#### **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. DebugJITRelease统一 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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 842 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 992 KiB

BIN
docs/images/image 1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
docs/images/image 2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
docs/images/image 3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

BIN
docs/images/image 4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
docs/images/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

45
example/.gitignore vendored Normal file
View 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
View 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
View 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-文档) 了解接口使用方法

View 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
View 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

View File

@@ -6,9 +6,9 @@ plugins {
}
android {
namespace = "com.example.ai_chat_assistant"
namespace = "com.example.example"
compileSdk = flutter.compileSdkVersion
ndkVersion = "29.0.13599879"
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
@@ -21,10 +21,10 @@ android {
defaultConfig {
// 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.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = 24
minSdk = 23
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
@@ -37,11 +37,14 @@ android {
signingConfig = signingConfigs.getByName("debug")
}
}
repositories {
flatDir {
dirs("libs")
}
viewBinding {
// isEnabled = viewBindingEnabled
}
dependenciesInfo {
includeInApk = includeInApk
includeInBundle = includeInBundle
}
ndkVersion = "27.0.12077973"
}
flutter {
@@ -49,6 +52,9 @@ flutter {
}
dependencies {
implementation(files("libs/fastjson-1.1.46.android.jar"))
implementation(files("libs/nuisdk-release.aar"))
// Process .aar files in the libs folder.
val aarFiles = fileTree(mapOf("dir" to "$rootDir/libs", "include" to listOf("*.aar")))
aarFiles.forEach { aar ->
implementation(files(aar))
}
}

View File

@@ -4,5 +4,4 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.intent.action.TTS_SERVICE" />
</manifest>

View File

@@ -1,6 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="ai_chat_assistant"
android:label="AI助手"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
@@ -30,6 +30,10 @@
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<service
android:name="com.tsystems.flutter_vosk_wakeword.WakewordService"
android:exported="false"
android:foregroundServiceType="microphone" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
@@ -41,8 +45,5 @@
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
<intent>
<action android:name="android.intent.action.TTS_SERVICE"/>
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,6 @@
package com.example.example;
import io.flutter.embedding.android.FlutterActivity;
public class MainActivity extends FlutterActivity {
}

View File

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 544 B

View File

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 442 B

View File

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 721 B

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View 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>

View File

@@ -1,16 +1,25 @@
allprojects {
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()
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)
subprojects {

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View 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

Binary file not shown.

View 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")

View 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
View 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(),
);
}
}

View 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,
),
);
}
}

View 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
View 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
View 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

View 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';

View 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;
}

View 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
View 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);
}

View File

@@ -1,6 +0,0 @@
enum MessageServiceState {
idle,
recording,
recognizing,
replying,
}

View File

@@ -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
View 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 单例销毁');
}
}

View File

@@ -1,7 +1,6 @@
import 'package:ai_chat_assistant/models/chat_message.dart';
import 'package:basic_intl/intl.dart';
import 'package:ai_chat_core/ai_chat_core.dart';
import 'package:t_basic_intl/intl.dart';
import 'package:flutter/material.dart';
import '../enums/message_status.dart';
import '../widgets/chat_box.dart';
import '../widgets/gradient_background.dart';
import '../services/message_service.dart';
@@ -9,14 +8,14 @@ import 'package:provider/provider.dart';
import '../widgets/chat_header.dart';
import '../widgets/chat_footer.dart';
class FullScreen extends StatefulWidget {
const FullScreen({super.key});
class FullScreenPage extends StatefulWidget {
const FullScreenPage({super.key});
@override
State<FullScreen> createState() => _FullScreenState();
State<FullScreenPage> createState() => _FullScreenState();
}
class _FullScreenState extends State<FullScreen> {
class _FullScreenState extends State<FullScreenPage> {
final ScrollController _scrollController = ScrollController();
@override

View File

@@ -1,5 +1,8 @@
import 'package:flutter/material.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 {
const MainScreen({super.key});
@@ -26,14 +29,15 @@ class _MainScreenState extends State<MainScreen> {
body: Stack(
children: [
Container(
decoration: const BoxDecoration(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/bg.jpg'),
image: AssetsUtil.getImage('bg.jpg'),
fit: BoxFit.cover,
),
),
),
FloatingIcon(),
// FloatingIcon(),
ChatPopup()
],
),
);

View File

@@ -1,15 +1,23 @@
import 'package:flutter/material.dart';
import 'dart:ui';
import '../widgets/chat_box.dart';
import '../screens/full_screen.dart';
import 'package:provider/provider.dart';
import '../services/message_service.dart';
import '../widgets/gradient_background.dart';
import '../utils/assets_util.dart';
import '../pages/full_screen.dart';
class PartScreen extends StatefulWidget {
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
State<PartScreen> createState() => _PartScreenState();
@@ -50,14 +58,80 @@ class _PartScreenState extends State<PartScreen> {
}
void _openFullScreen() async {
final messageService = context.read<MessageService>();
widget.onHide?.call();
await Navigator.of(context).push(
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
Widget build(BuildContext context) {
return Scaffold(
@@ -67,9 +141,12 @@ class _PartScreenState extends State<PartScreen> {
if (!_isInitialized) {
return const SizedBox.shrink();
}
final position = _calculatePosition(constraints);
final double minHeight = constraints.maxHeight * 0.16;
final double maxHeight = constraints.maxHeight * 0.4;
final double chatWidth = constraints.maxWidth * 0.94;
return Stack(
children: [
Positioned.fill(
@@ -92,74 +169,80 @@ class _PartScreenState extends State<PartScreen> {
),
),
),
Padding(
padding: EdgeInsets.only(bottom: constraints.maxHeight * 0.22),
child: Align(
alignment: Alignment.bottomCenter,
child: Consumer<MessageService>(
builder: (context, messageService, child) {
final messageCount = messageService.messages.length;
if (messageCount > _lastMessageCount) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
}
_lastMessageCount = messageCount;
return Container(
width: chatWidth,
constraints: BoxConstraints(
minHeight: minHeight,
maxHeight: maxHeight,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(24),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
child: GradientBackground(
colors: const [
Color(0XBF3B0A3F),
Color(0xBF0E0E24),
Color(0xBF0C0B33),
],
borderRadius: BorderRadius.circular(6),
child: Stack(
children: [
Padding(
padding: const EdgeInsets.only(
top: 50,
left: 6,
right: 6,
bottom: 30),
child: LayoutBuilder(
builder: (context, boxConstraints) {
return ChatBox(
scrollController: _scrollController,
messages: messageService.messages,
);
}
),
),
Positioned(
top: 6,
right: 6,
child: IconButton(
icon: Image.asset(
'assets/images/open_in_full.png',
width: 24,
height: 24
Positioned(
left: position.left,
right: position.right,
bottom: position.bottom,
child: Consumer<MessageService>(
builder: (context, messageService, child) {
final messageCount = messageService.messages.length;
if (messageCount > _lastMessageCount) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
}
_lastMessageCount = messageCount;
return TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0.8, end: 1.0),
duration: const Duration(milliseconds: 200),
curve: Curves.easeOutBack,
builder: (context, scale, child) {
return Transform.scale(
scale: scale,
child: Container(
width: chatWidth,
constraints: BoxConstraints(
minHeight: minHeight,
maxHeight: maxHeight,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(24),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
child: GradientBackground(
colors: const [
Color(0XBF3B0A3F),
Color(0xBF0E0E24),
Color(0xBF0C0B33),
],
borderRadius: BorderRadius.circular(6),
child: Stack(
children: [
Padding(
padding: const EdgeInsets.only(
top: 50, left: 6, right: 6, bottom: 30),
child: LayoutBuilder(
builder: (context, boxConstraints) {
return ChatBox(
scrollController: _scrollController,
messages: messageService.messages,
);
}),
),
onPressed: _openFullScreen,
padding: EdgeInsets.zero,
),
Positioned(
top: 6,
right: 6,
child: IconButton(
icon: AssetsUtil.getImageWidget(
'open_in_full.png',
width: 24,
height: 24,
),
onPressed: _openFullScreen,
padding: EdgeInsets.zero,
),
),
],
),
],
),
),
),
),
),
);
},
),
);
},
);
},
),
),
],

View File

@@ -1,9 +1,11 @@
import 'package:ai_chat_core/ai_chat_core.dart';
import 'package:record/record.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:async';
import 'dart:io';
// 负责录音相关功能
@Deprecated('AudioRecorderService Deprecated')
class AudioRecorderService {
final AudioRecorder _recorder = AudioRecorder();
bool _isRecording = false;
@@ -15,11 +17,11 @@ class AudioRecorderService {
Future<void> checkPermission() async {
final hasPermission = await _recorder.hasPermission();
print('麦克风权限状态: $hasPermission');
Logger.i('麦克风权限状态: $hasPermission');
}
Future<void> startRecording() async {
print('开始录音...');
Logger.i('开始录音...');
try {
// 使用文件录音可能更稳定
@@ -27,7 +29,7 @@ class AudioRecorderService {
_tempFilePath =
'${tempDir.path}/temp_audio_${DateTime.now().millisecondsSinceEpoch}.opus';
print('录音文件路径: $_tempFilePath');
Logger.i('录音文件路径: $_tempFilePath');
if (await _recorder.hasPermission()) {
await _recorder.start(
@@ -36,12 +38,12 @@ class AudioRecorderService {
);
_isRecording = true;
print('录音已开始使用OPUS格式文件: $_tempFilePath');
Logger.i('录音已开始使用OPUS格式文件: $_tempFilePath');
} else {
print('没有麦克风权限,无法开始录音');
Logger.e('没有麦克风权限,无法开始录音');
}
} catch (e) {
print('录音开始出错: $e');
Logger.e('录音开始出错: $e');
_isRecording = false;
}
}
@@ -51,30 +53,30 @@ class AudioRecorderService {
_isRecording = false;
print('停止录音...');
Logger.i('停止录音...');
try {
final path = await _recorder.stop();
print('录音已停止,文件路径: $path');
Logger.i('录音已停止,文件路径: $path');
if (path != null) {
final file = File(path);
if (await file.exists()) {
final bytes = await file.readAsBytes();
print('读取到录音数据: ${bytes.length} 字节');
Logger.i('读取到录音数据: ${bytes.length} 字节');
if (bytes.isNotEmpty) {
return bytes;
} else {
print('录音数据为空');
Logger.e('录音数据为空');
}
} else {
print('录音文件不存在: $path');
Logger.e('录音文件不存在: $path');
}
} else {
print('录音路径为空');
Logger.e('录音路径为空');
}
} catch (e) {
print('停止录音或发送过程出错: $e');
Logger.e('停止录音或发送过程出错: $e');
}
return null;
}

View File

@@ -1,19 +1,23 @@
// 负责SSE通信
import 'dart:convert';
import 'dart:io';
import 'dart:io' as IO;
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';
@Deprecated('ChatSseService Deprecated, Use ai_chat_core/ChatSseService instead')
/// SSE 服务类,处理与后端的流式通信
class ChatSseService {
// 缓存用户ID和会话ID
String? _cachedUserId;
String? _cachedConversationId;
HttpClient? _currentClient;
HttpClientResponse? _currentResponse;
IO.HttpClient? _currentClient;
IO.HttpClientResponse? _currentResponse;
bool _isAborted = true;
bool _isFirstData = true;
bool _isTtsStarted = false;
@@ -28,11 +32,11 @@ class ChatSseService {
required bool isChinese,
required Function(String, String, bool) onStreamResponse,
}) async {
print("----------------------SSE Start");
Logger.i("----------------------SSE Start");
_isAborted = false;
if (_cachedUserId == null) {
_cachedUserId = _generateRandomUserId(6);
print('初始化用户ID: $_cachedUserId');
Logger.i('初始化用户ID: $_cachedUserId');
}
String responseText = '';
StringBuffer buffer = StringBuffer();
@@ -59,7 +63,8 @@ class ChatSseService {
txt = txt.substring(0, imgStart) + txt.substring(imgEnd + 1);
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');
// 分句符
RegExp enders = isChinese ? zhEnders : enEnders;
@@ -76,14 +81,8 @@ class ChatSseService {
}
// 只在达到完整句子时调用 TtsUtil.send
for (final s in sentences) {
String ttsStr=CommonUtil.cleanText(s, true)+"\n";
ttsStr = ttsStr.replaceAllMapped(
RegExp(r'(?<!\s)(\d+)(?!\s)'),
(m) => ' ${m.group(1)} ',
);
print("发送数据到TTS: $ttsStr");
String ttsStr=CommonUtil.cleanText(s, true);
// Logger.i("发送数据到TTS: $ttsStr");
TtsUtil.send(ttsStr);
}
// 缓存剩余不完整部分
@@ -93,7 +92,7 @@ class ChatSseService {
buffer.write(remain);
}
}
_currentClient = HttpClient();
_currentClient = IO.HttpClient();
try {
final chatUri = Uri.parse('http://143.64.185.20:18606/chat');
final request = await _currentClient!.postUrl(chatUri);
@@ -117,6 +116,7 @@ class ChatSseService {
break;
}
if (line.startsWith('data:')) {
Logger.i('SSE line: $line');
final jsonStr = line.substring(5).trim();
if (jsonStr == '[DONE]' || jsonStr.contains('message_end')) {
TtsUtil.complete();
@@ -125,6 +125,7 @@ class ChatSseService {
}
try {
final jsonData = json.decode(jsonStr);
Logger.i('SSE jsonData: $jsonData');
if (jsonData.containsKey('conversation_id') &&
_cachedConversationId == null) {
_cachedConversationId = jsonData['conversation_id'];
@@ -141,12 +142,12 @@ class ChatSseService {
onStreamResponse(messageId, responseText, false);
}
} catch (e) {
print('解析 SSE 数据出错: $e, 原始数据: $jsonStr');
Logger.e('解析 SSE 数据出错: $e, 原始数据: $jsonStr');
}
}
}
} else {
print('SSE 连接失败,状态码: ${_currentResponse!.statusCode}');
Logger.e('SSE 连接失败,状态码: ${_currentResponse!.statusCode}');
}
} catch (e) {
// todo
@@ -156,7 +157,7 @@ class ChatSseService {
}
Future<void> startTts(bool isChinese) async {
print("----------------------TTS Start");
Logger.i("----------------------TTS Start");
if (!_isTtsStarted) {
if (await TtsUtil.start(isChinese) == true) {
_isTtsStarted = true;
@@ -193,7 +194,7 @@ class ChatSseService {
void resetSession() {
_cachedUserId = null;
_cachedConversationId = null;
print('SSE会话已重置');
Logger.i('SSE会话已重置');
}
String _generateRandomUserId(int length) {

View File

@@ -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;
}
}
}

View File

@@ -1,11 +1,11 @@
import '../enums/vehicle_command_type.dart';
import '../models/vehicle_status_info.dart';
import 'package:ai_chat_core/ai_chat_core.dart';
/// 命令处理回调函数定义
typedef CommandCallback = Future<(bool, Map<String, dynamic>? params)> Function(
VehicleCommandType type, Map<String, dynamic>? params);
/// 命令处理器类 - 负责处理来自AI的车辆控制命令
/// 命令处理器类 - 负责处理来自AI的车辆控制命令(使用回调的方式交给App去实现具体的车控逻辑)
class CommandService {
/// 保存主应用注册的回调函数
static CommandCallback? onCommandReceived;
@@ -24,10 +24,10 @@ class CommandService {
try {
return await onCommandReceived!(type, params);
} catch (e) {
print('执行命令出错: $e');
Logger.e('执行命令出错: $e');
}
} else {
print('警告: 命令处理回调未注册');
Logger.w('警告: 命令处理回调未注册');
}
return (false, null);
}

View File

@@ -1,9 +1,8 @@
import 'dart:convert';
import 'package:ai_chat_core/ai_chat_core.dart';
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 {
// final VehicleStateService vehicleStateService = VehicleStateService();
@@ -29,12 +28,12 @@ class VehicleCommandService {
.toList();
return VehicleCommandResponse(tips: tips, commands: vehicleCommandList);
} else {
print(
Logger.e(
'Vehicle command query failed: ${response.statusCode}, ${response.body}');
return null;
}
} catch (e) {
print('Error during vehicle command processing: $e');
Logger.e('Error during vehicle command processing: $e');
return null;
}
}
@@ -51,10 +50,10 @@ class VehicleCommandService {
if (response.statusCode == 200) {
return response.body;
} else {
print("请求控制回复失败: ${response.statusCode}");
Logger.e("请求控制回复失败: ${response.statusCode}");
}
} catch (e) {
print('请求控制回复异常: $e');
Logger.e('请求控制回复异常: $e');
}
return reply;
}

View File

@@ -1,5 +1,6 @@
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;
class LocationService {
@@ -13,7 +14,7 @@ class LocationService {
return json.decode(response.body)['address'];
}
} catch (e) {
print('Error during text classification: $e');
Logger.e('Error during text classification: $e');
}
return "";
}

View File

@@ -1,44 +1,73 @@
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/tts_util.dart';
import 'package:basic_intl/intl.dart';
import 'package:t_basic_intl/intl.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:uuid/uuid.dart';
import '../enums/vehicle_command_type.dart';
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/chat_sse_service.dart' as Service;
import '../services/control_recognition_service.dart';
// import '../services/audio_recorder_service.dart';
// import '../services/voice_recognition_service.dart';
import 'command_service.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'platform_tts_service.dart';
const aliSdkChannelName = 'com.example.ai_chat_assistant/ali_sdk';
// 用单例的模式创建
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;
MessageService._internal() {
_asrChannel.setMethodCallHandler((call) async {
switch (call.method) {
case "onAsrResult":
// 注册MethodChannel的handler
_asrChannel.setMethodCallHandler(_handleMethodCall);
}
@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(
id: _latestUserMessageId!,
text: call.arguments,
status: MessageStatus.normal);
id: _latestUserMessageId!, text: call.arguments, status: MessageStatus.normal);
break;
case "onAsrStop":
}
case "onAsrStop":
{
debugPrint("ASR 停止: ${call.arguments}");
int index = findMessageIndexById(_latestUserMessageId!);
if (index == -1) {
return;
@@ -52,16 +81,17 @@ class MessageService extends ChangeNotifier {
_asrCompleter!.complete(messages.last.text);
}
break;
}
});
}
}
}
final ChatSseService _chatSseService = ChatSseService();
final ChatSseService _chatSseService = ChatSseService(PlatformTtsService(aliSdkChannelName));
// final Service.ChatSseService _chatSseService = Service.ChatSseService();
// final LocalTtsService _ttsService = LocalTtsService();
// final AudioRecorderService _audioService = AudioRecorderService();
// final VoiceRecognitionService _recognitionService = VoiceRecognitionService();
final TextClassificationService _classificationService =
TextClassificationService();
final TextClassificationService _classificationService = TextClassificationService();
final VehicleCommandService _vehicleCommandService = VehicleCommandService();
final List<ChatMessage> _messages = [];
@@ -106,12 +136,11 @@ class MessageService extends ChangeNotifier {
abortReply();
_latestUserMessageId = null;
_latestAssistantMessageId = null;
_isReplyAborted = false;
changeState(MessageServiceState.recording);
_latestUserMessageId = addMessage("", true, MessageStatus.listening);
_asrChannel.invokeMethod("startAsr");
} catch (e) {
print('录音开始出错: $e');
Logger.e('录音开始出错: $e');
}
}
@@ -123,11 +152,7 @@ class MessageService extends ChangeNotifier {
String addMessage(String text, bool isUser, MessageStatus status) {
String uuid = Uuid().v1();
_messages.add(ChatMessage(
id: uuid,
text: text,
isUser: isUser,
timestamp: DateTime.now(),
status: status));
id: uuid, text: text, isUser: isUser, timestamp: DateTime.now(), status: status));
notifyListeners();
return uuid;
}
@@ -138,6 +163,7 @@ class MessageService extends ChangeNotifier {
return;
}
try {
_isReplyAborted = false;
changeState(MessageServiceState.recognizing);
_asrChannel.invokeMethod("stopAsr");
_asrCompleter = Completer<String>();
@@ -164,7 +190,7 @@ class MessageService extends ChangeNotifier {
changeState(MessageServiceState.replying);
await reply(recognizedText);
} catch (e) {
print(e);
Logger.e(e.toString());
} finally {
changeState(MessageServiceState.idle);
_asrCompleter = null;
@@ -226,8 +252,7 @@ class MessageService extends ChangeNotifier {
if (_isReplyAborted) {
return;
}
final vehicleCommandResponse =
await _vehicleCommandService.getCommandFromText(text);
final vehicleCommandResponse = await _vehicleCommandService.getCommandFromText(text);
if (vehicleCommandResponse == null) {
if (_isReplyAborted) {
return;
@@ -235,10 +260,7 @@ class MessageService extends ChangeNotifier {
String msg = isChinese
? "无法识别车辆控制命令,请重试"
: "Cannot recognize the vehicle control command, please try again";
replaceMessage(
id: _latestAssistantMessageId!,
text: msg,
status: MessageStatus.normal);
replaceMessage(id: _latestAssistantMessageId!, text: msg, status: MessageStatus.normal);
} else {
if (_isReplyAborted) {
return;
@@ -259,8 +281,7 @@ class MessageService extends ChangeNotifier {
if (_isReplyAborted) {
return;
}
if (command.type == VehicleCommandType.unknown ||
command.error.isNotEmpty) {
if (command.type == VehicleCommandType.unknown || command.error.isNotEmpty) {
continue;
}
if (command.type == VehicleCommandType.openAC) {
@@ -268,13 +289,13 @@ class MessageService extends ChangeNotifier {
}
bool isSuccess;
if (containOpenAC && command.type == VehicleCommandType.changeACTemp) {
isSuccess = await Future.delayed(const Duration(milliseconds: 2000),
() => processCommand(command, isChinese));
isSuccess = await Future.delayed(
const Duration(milliseconds: 2000), () => processCommand(command, isChinese));
} else {
isSuccess = await processCommand(command, isChinese);
}
if (isSuccess) {
successCommandList.add(command.type.name);
successCommandList.add(isChinese ? command.type.chinese : command.type.english);
}
}
replaceMessage(
@@ -285,8 +306,7 @@ class MessageService extends ChangeNotifier {
if (_isReplyAborted || successCommandList.isEmpty) {
return;
}
String controlResponse =
await _vehicleCommandService.getControlResponse(successCommandList);
String controlResponse = await _vehicleCommandService.getControlResponse(successCommandList);
if (_isReplyAborted || controlResponse.isEmpty) {
return;
}
@@ -297,20 +317,22 @@ class MessageService extends ChangeNotifier {
Future<bool> processCommand(VehicleCommand command, bool isChinese) async {
String msg = "";
MessageStatus status = MessageStatus.normal;
final (isSuccess, result) = await CommandService.executeCommand(
command.type,
params: command.params);
var commandObjc = VehicleCommand(type: command.type, params: command.params);
var client = AIChatAssistantManager.instance.commandClient;
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) {
msg = isChinese
? "很抱歉,定位车辆位置失败"
: "Sorry, locate the vehicle unsuccessfully";
msg = isChinese ? "很抱歉,定位车辆位置失败" : "Sorry, locate the vehicle unsuccessfully";
status = MessageStatus.failure;
if (isSuccess && result != null && result.isNotEmpty) {
String address = result['address'];
if (address.isNotEmpty) {
msg = isChinese
? "已为您找到车辆位置:\"$address\""
: "The vehicle location is \"$address\"";
msg = isChinese ? "已为您找到车辆位置:\"$address\"" : "The vehicle location is \"$address\"";
status = MessageStatus.success;
}
}
@@ -386,8 +408,7 @@ class MessageService extends ChangeNotifier {
status: MessageStatus.completed,
);
} else {
replaceMessage(
id: messageId, text: responseText, status: MessageStatus.thinking);
replaceMessage(id: messageId, text: responseText, status: MessageStatus.thinking);
}
} catch (e) {
abortReply();
@@ -401,8 +422,7 @@ class MessageService extends ChangeNotifier {
if (index == -1 || messages[index].status != MessageStatus.thinking) {
return;
}
replaceMessage(
id: _latestAssistantMessageId!, status: MessageStatus.aborted);
replaceMessage(id: _latestAssistantMessageId!, status: MessageStatus.aborted);
}
void removeMessageById(String id) {

View 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');
}
}

View File

@@ -1,11 +1,12 @@
import 'dart:async';
import 'dart:collection';
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_tts/flutter_tts.dart';
@Deprecated('Use TtsService interface and its implementations instead')
class LocalTtsService {
final FlutterTts _flutterTts = FlutterTts();
bool _isPlaying = false;
@@ -45,27 +46,27 @@ class LocalTtsService {
// 设置TTS事件回调
_flutterTts.setStartHandler(() {
print('TTS开始播放');
Logger.i('TTS开始播放');
_isPlaying = true;
_onStartHandler?.call();
});
_flutterTts.setCompletionHandler(() {
print('TTS播放完成');
Logger.i('TTS播放完成');
_isPlaying = false;
_handleTtsPlaybackComplete();
});
_flutterTts.setErrorHandler((msg) {
print('TTS错误: $msg');
Logger.e('TTS错误: $msg');
_isPlaying = false;
_onErrorHandler?.call(msg);
_handleTtsPlaybackComplete(); // 错误时也要处理下一个任务
});
print('TTS引擎初始化完成');
Logger.i('TTS引擎初始化完成');
} catch (e) {
print('TTS引擎初始化失败: $e');
Logger.e('TTS引擎初始化失败: $e');
}
}
@@ -77,17 +78,17 @@ class LocalTtsService {
try {
// 获取可用的TTS引擎
dynamic engines = await _flutterTts.getEngines;
print('可用的TTS引擎: $engines');
Logger.i('可用的TTS引擎: $engines');
// 获取可用的语言
dynamic languages = await _flutterTts.getLanguages;
print('支持的语言: $languages');
Logger.i('支持的语言: $languages');
// 获取当前默认引擎
dynamic defaultEngine = await _flutterTts.getDefaultEngine;
print('当前默认引擎: $defaultEngine');
Logger.i('当前默认引擎: $defaultEngine');
} catch (e) {
print('获取TTS引擎信息失败: $e');
Logger.e('获取TTS引擎信息失败: $e');
}
}
@@ -99,19 +100,19 @@ class LocalTtsService {
_isProcessingTasks = false;
_streamCompleted = false;
_isPlaying = false; // 重置播放状态
print('所有TTS队列和缓存已清空');
Logger.i('所有TTS队列和缓存已清空');
}
/// 推送文本到TTS队列进行流式处理保证顺序
void pushTextForStreamTTS(String text) {
if (text.trim().isEmpty) {
print('接收到空字符串跳过推送到TTS队列');
Logger.i('接收到空字符串跳过推送到TTS队列');
return;
}
String cleanedText = CommonUtil.cleanText(text, true);
if (cleanedText.isEmpty) {
print('清理后的文本为空跳过推送到TTS队列');
Logger.i('清理后的文本为空跳过推送到TTS队列');
return;
}
@@ -124,7 +125,7 @@ class LocalTtsService {
);
_orderedTasks.add(task);
print('添加TTS任务 (索引: $taskIndex): $cleanedText');
Logger.i('添加TTS任务 (索引: $taskIndex): $cleanedText');
// 尝试处理队列中的下一个任务
_processNextReadyTask();
@@ -134,7 +135,7 @@ class LocalTtsService {
void _processNextReadyTask() {
// 如果正在播放,则不处理下一个任务
if (_isPlaying) {
print('当前正在播放音频,等待播放完成');
Logger.i('当前正在播放音频,等待播放完成');
return;
}
@@ -142,7 +143,7 @@ class LocalTtsService {
while (_nextTaskIndex < _orderedTasks.length) {
TtsTask task = _orderedTasks[_nextTaskIndex];
if (task.status == TtsTaskStatus.ready) {
print('按顺序播放TTS (索引: ${task.index}): ${task.text}');
Logger.i('按顺序播放TTS (索引: ${task.index}): ${task.text}');
_playTts(task.text);
return; // 等待当前音频播放完成
} else {
@@ -154,7 +155,7 @@ class LocalTtsService {
// 检查是否所有任务都已处理完
if (_streamCompleted && _nextTaskIndex >= _orderedTasks.length) {
print('=== 所有TTS任务播放完成 ===');
Logger.i('=== 所有TTS任务播放完成 ===');
_isProcessingTasks = false;
_onCompletionHandler?.call();
}
@@ -163,28 +164,28 @@ class LocalTtsService {
/// 播放TTS
Future<void> _playTts(String text) async {
if (_isPlaying) {
print('已有TTS正在播放跳过本次播放');
Logger.i('已有TTS正在播放跳过本次播放');
return;
}
try {
print('开始播放TTS: $text');
Logger.i('开始播放TTS: $text');
// 检测语言并设置
bool isChinese = CommonUtil.containChinese(text);
String targetLanguage = isChinese ? "zh-CN" : "en-US";
// 检查语言是否可用,如果不可用则使用默认语言
bool isLanguageOk = await isLanguageAvailable(targetLanguage);
if (!isLanguageOk) {
print('语言 $targetLanguage 不可用,使用默认语言');
Logger.i('语言 $targetLanguage 不可用,使用默认语言');
// 可以在这里设置备用语言或保持当前语言
} else {
await _flutterTts.setLanguage(targetLanguage);
print('设置TTS语言为: $targetLanguage');
Logger.i('设置TTS语言为: $targetLanguage');
}
// 播放TTS
await _flutterTts.speak(text);
} catch (e) {
print('播放TTS失败: $e');
Logger.i('播放TTS失败: $e');
// 播放失败,重置状态并继续下一个
_isPlaying = false;
_handleTtsPlaybackComplete();
@@ -194,7 +195,7 @@ class LocalTtsService {
/// 处理TTS播放完成
void _handleTtsPlaybackComplete() {
if (!_isPlaying) {
print('TTS已停止播放处理下一个任务');
Logger.i('TTS已停止播放处理下一个任务');
// 标记当前任务为已完成
if (_nextTaskIndex < _orderedTasks.length &&
_orderedTasks[_nextTaskIndex].status == TtsTaskStatus.ready) {
@@ -208,7 +209,7 @@ class LocalTtsService {
/// 标记流完成
void markSSEStreamCompleted() {
print('=== 标记SSE流完成 ===');
Logger.i('=== 标记SSE流完成 ===');
_streamCompleted = true;
// 尝试完成剩余任务
@@ -217,7 +218,7 @@ class LocalTtsService {
/// 停止播放
void stopSSEPlayback() {
print("停止SSE播放");
Logger.i("停止SSE播放");
// 立即停止TTS播放无论当前是否在播报
_flutterTts.stop();
_isPlaying = false;
@@ -240,11 +241,11 @@ class LocalTtsService {
// 保留原有的播放逻辑用于完整文本处理
Future<void> processCompleteText(String text) async {
if (text.trim().isEmpty) {
print("文本为空跳过TTS处理");
Logger.i("文本为空跳过TTS处理");
return;
}
print("开始处理完整文本TTS原始文本长度: ${text.length}");
Logger.i("开始处理完整文本TTS原始文本长度: ${text.length}");
// 清理缓存和重置队列
clearAll();
@@ -260,23 +261,23 @@ class LocalTtsService {
Future<void> _processCompleteTextAsStream(String text) async {
// 清理markdown和异常字符
String cleanedText = _cleanTextForTts(text);
print("清理后文本长度: ${cleanedText.length}");
Logger.i("清理后文本长度: ${cleanedText.length}");
if (cleanedText.trim().isEmpty) {
print("清理后文本为空跳过TTS");
Logger.i("清理后文本为空跳过TTS");
return;
}
// 分割成句子并逐个处理
List<String> sentences = _splitTextIntoSentences(cleanedText);
print("=== 文本分段完成,共 ${sentences.length} 个句子 ===");
Logger.i("=== 文本分段完成,共 ${sentences.length} 个句子 ===");
// 推送每个句子到流式TTS处理
for (int i = 0; i < sentences.length; i++) {
final sentence = sentences[i].trim();
if (sentence.isEmpty) continue;
print("处理句子 ${i + 1}/${sentences.length}: $sentence");
Logger.i("处理句子 ${i + 1}/${sentences.length}: $sentence");
pushTextForStreamTTS(sentence);
}
@@ -287,19 +288,19 @@ class LocalTtsService {
// 停止当前播放的辅助方法
Future<void> _stopCurrentPlayback() async {
try {
print("停止当前播放");
Logger.i("停止当前播放");
if (_isPlaying) {
await _flutterTts.stop();
_isPlaying = false;
print("TTS.stop() 调用完成");
Logger.i("TTS.stop() 调用完成");
}
// 短暂延迟确保播放器状态稳定
await Future.delayed(Duration(milliseconds: 300));
print("播放器状态稳定延迟完成");
Logger.i("播放器状态稳定延迟完成");
} catch (e) {
print('停止当前播放错误: $e');
Logger.i('停止当前播放错误: $e');
}
}
@@ -456,7 +457,7 @@ class LocalTtsService {
await processCompleteText(text);
return true;
} catch (e) {
print('TTS 播放错误: $e');
Logger.i('TTS 播放错误: $e');
_onErrorHandler?.call('TTS 播放错误: $e');
return false;
}
@@ -468,7 +469,7 @@ class LocalTtsService {
await _stopCurrentPlayback();
clearAll();
} catch (e) {
print('停止音频播放错误: $e');
Logger.i('停止音频播放错误: $e');
}
}
@@ -478,7 +479,7 @@ class LocalTtsService {
await _flutterTts.pause();
}
} catch (e) {
print('暂停音频播放错误: $e');
Logger.i('暂停音频播放错误: $e');
}
}
@@ -486,9 +487,9 @@ class LocalTtsService {
try {
// flutter_tts没有resume方法需要重新播放当前文本
// 这里可以根据需要实现
print('TTS不支持恢复需要重新播放');
Logger.i('TTS不支持恢复需要重新播放');
} catch (e) {
print('恢复音频播放错误: $e');
Logger.i('恢复音频播放错误: $e');
}
}
@@ -536,7 +537,7 @@ class LocalTtsService {
dynamic engines = await _flutterTts.getEngines;
return engines ?? [];
} catch (e) {
print('获取TTS引擎列表失败: $e');
Logger.i('获取TTS引擎列表失败: $e');
return [];
}
}
@@ -545,10 +546,10 @@ class LocalTtsService {
Future<bool> setEngine(String engineName) async {
try {
int result = await _flutterTts.setEngine(engineName);
print('设置TTS引擎: $engineName, 结果: $result');
Logger.i('设置TTS引擎: $engineName, 结果: $result');
return result == 1; // 1表示成功
} catch (e) {
print('设置TTS引擎失败: $e');
Logger.i('设置TTS引擎失败: $e');
return false;
}
}
@@ -559,7 +560,7 @@ class LocalTtsService {
dynamic engine = await _flutterTts.getDefaultEngine;
return engine?.toString();
} catch (e) {
print('获取当前TTS引擎失败: $e');
Logger.i('获取当前TTS引擎失败: $e');
return null;
}
}
@@ -570,7 +571,7 @@ class LocalTtsService {
dynamic languages = await _flutterTts.getLanguages;
return languages ?? [];
} catch (e) {
print('获取支持的语言列表失败: $e');
Logger.i('获取支持的语言列表失败: $e');
return [];
}
}
@@ -581,7 +582,7 @@ class LocalTtsService {
dynamic result = await _flutterTts.isLanguageAvailable(language);
return result == true;
} catch (e) {
print('检查语言可用性失败: $e');
Logger.i('检查语言可用性失败: $e');
return false;
}
}

View File

@@ -1,14 +1,16 @@
import 'package:ai_chat_core/ai_chat_core.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:http_parser/http_parser.dart';
@Deprecated('VoiceRecognitionService is deprecated, please use the new implementation if available.')
class VoiceRecognitionService {
Future<String?> recognizeSpeech(List<int> audioBytes,
{String lang = 'cn'}) async {
try {
print('准备发送OPUS音频数据大小: ${audioBytes.length} 字节');
Logger.i('准备发送OPUS音频数据大小: ${audioBytes.length} 字节');
final uri = Uri.parse('http://143.64.185.20:18606/voice');
print('发送到: $uri');
Logger.i('发送到: $uri');
final request = http.MultipartRequest('POST', uri);
request.files.add(
@@ -21,26 +23,26 @@ class VoiceRecognitionService {
);
request.fields['lang'] = lang; // 根据参数设置语言
print('发送请求...');
Logger.i('发送请求...');
final streamResponse = await request.send();
print('收到响应,状态码: ${streamResponse.statusCode}');
Logger.i('收到响应,状态码: ${streamResponse.statusCode}');
final response = await http.Response.fromStream(streamResponse);
if (response.statusCode == 200) {
print('响应内容: ${response.body}');
Logger.i('响应内容: ${response.body}');
final text = _parseTextFromJson(response.body);
if (text != null) {
print('解析出文本: $text');
Logger.i('解析出文本: $text');
return text; // 返回识别的文本
} else {
print('解析文本失败');
Logger.i('解析文本失败');
}
} else {
print('请求失败,状态码: ${response.statusCode},响应: ${response.body}');
Logger.i('请求失败,状态码: ${response.statusCode},响应: ${response.body}');
}
} catch (e) {
print('发送录音到服务器时出错: $e');
Logger.i('发送录音到服务器时出错: $e');
}
return null;
}
@@ -54,7 +56,7 @@ class VoiceRecognitionService {
}
return null;
} catch (e) {
print('JSON解析错误: $e, 原始数据: $body');
Logger.i('JSON解析错误: $e, 原始数据: $body');
return null;
}
}

View File

@@ -1,4 +1,4 @@
import 'package:basic_intl/intl.dart';
import 'package:t_basic_intl/intl.dart';
import 'package:flutter/material.dart';
class AppTheme {

View 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,
);
}
}

Some files were not shown because too many files have changed in this diff Show More